React ChartJS - difficulty manipulating data to a certain format - reactjs

I have data stored in MongoDB that looks like this: (Example of 1 document below)
{_id: XXX,
project: 'Project ABC',
amount: 300,
expenseIncurredDate: 2022-03-15T08:24:46.000+00:00
}
I am trying to display my data in a stacked bar chart where the x-axis is year-month, and the y-axis is amount. Within each year-month, there can be several expenses incurred by different projects.
I am trying to achieve something like this, except that x-axis should show year-month (e.g. Jan 2021). Each project should be represented by one color for every year-month.
I am using Chart.js. Below is my Bar component.
import { Bar } from "react-chartjs-2";
import { Chart as ChartJS } from "chart.js/auto";
import React from "react";
const BarChart = ({ chartData }) => {
const options = {
responsive: true,
legend: {
display: false,
},
type: "bar",
scales: {
xAxes: [
{
stacked: true,
},
],
yAxes: [
{
stacked: true,
},
],
},
};
return <Bar data={chartData} options={options} />;
};
export default BarChart;
I am manipulating my data in another component:
let datasets = [];
function random_bg_color() {
var x = Math.floor(Math.random() * 256);
var y = 100 + Math.floor(Math.random() * 256);
var z = 50 + Math.floor(Math.random() * 256);
var bgColor = "rgb(" + x + "," + y + "," + z + ")";
return bgColor;
}
let amounts = myExpenses.map((expense) => expense.amount);
myExpenses.forEach((expense) => {
const expenseObj = {
label: expense.project,
stack: 1,
borderWidth: 1,
backgroundColor: random_bg_color(),
data: amounts,
};
datasets.push(expenseObj);
});
let data = {
labels: myExpenses.map((expense) =>
moment(expense.expenseDate, "DD-MM-YYYY")
),
datasets: datasets,
};
I know this doesn't work as it isn't returning amounts in the correct order/grouped by year & month.
let amounts = myExpenses.map((expense) => expense.amount);
I am having difficulty trying to group the amount by year month. Would appreciate any help on this, thank you.

Related

why bad handling chartjs in a react component when render that component

I am working on a digital currency personal project.
My problem is that when I click on any digital currency to show the details of that coin,
The new page must be refreshed to display the chart correctly, or use a tag,
but I use the Link (react-router-dom),
And before refreshing the page, my chart is shown as below.
(https://ibb.co/NFgNMmX)
And after refreshing the page, my chart is shown as below.
(https://ibb.co/DzWRXRY)
and this is my code
const CoinChart = () => {
let id = useParams();
// state
const [chartDay, setChartDay] = useState({
value: "1",
text: "1d"
})
// redux
const dispatch = useDispatch<any>();
const detail = useSelector((state: State) => state.coin_detail.chart);
useEffect(() => {
dispatch(coinChartFetchRequestFunc(id.coin_id, chartDay.value));
}, [chartDay]);
// chart config
const labels = detail.chart?.prices.map((item) => {
if (chartDay.value == "max") {
return (
new Date(item[0]).toLocaleDateString()
)
}
else {
return (
new Date(item[0]).getDate() +
"." +
new Date(item[0]).toDateString().split(/[0-9]/)[0].split(" ")[1] +
" " +
new Date(item[0]).getHours() +
":" +
new Date(item[0]).getMinutes()
);
}
});
const Data = {
labels,
datasets: [
{
fill: true,
drawActiveElementsOnTop: false,
data: detail.chart?.prices.map((item) => {
return item[1];
}),
label: "price(usd)",
borderColor: "#3861fb",
backgroundColor: "#3861fb10",
pointBorderWidth: 0,
borderWidth: 2.5,
},
],
};
return (
//some code
<Line data={Data} />
);
};
Is this problem solvable?
Also, I used translate in some part, and if there are any problems in the above texts, excuse me!

Issue rendering react charts

I am dynamically changing the values that are displayed on the areachart. But for some reason the chart is only displayed if I change one of the dynamic variable with a hard coded number in the array. For example
const data = [
["Year", "Free Cash Flow", "Revenue"],
[this.props.date1, this.props.cashFlow1, this.props.revenue1],
[this.props.date2, this.props.cashFlow2, this.props.revenue2],
[this.props.date3, this.props.cashFlow3, this.props.revenue3],
[this.props.date4, this.props.cashFlow4, this.props.revenue4],
[this.props.date5, this.props.cashFlow5, this.props.revenue5],
];
that is how I structured my data array, but it doesn't renders and give the following error All series on a given axis must be of the same data type. However, if I replace this.props.revenue1 with let say 100 the area chart renders
const data = [
["Year", "Free Cash Flow", "Revenue"],
[this.props.date1, this.props.cashFlow1, 100],
[this.props.date2, this.props.cashFlow2, this.props.revenue2],
[this.props.date3, this.props.cashFlow3, this.props.revenue3],
[this.props.date4, this.props.cashFlow4, this.props.revenue4],
[this.props.date5, this.props.cashFlow5, this.props.revenue5],
];
I have looked at other examples and I can't seem to find a mistake I could've made.
import React, {Component} from "react";
import { Chart } from "react-google-charts";
class AreaChart extends Component {
render () {
const chartEvents = [
{
callback: ({ chartWrapper, google }) => {
const chart = chartWrapper.getChart();
chart.container.addEventListener("click", (ev) => console.log(ev))
},
eventName: "ready"
}
];
const rev1 = this.props.revenue1;
const FCF1 = this.props.cashFlow1;
const data = [
["Year", "Free Cash Flow", "Revenue"],
[this.props.date1, this.props.cashFlow1, this.props.revenue1],
[this.props.date2, this.props.cashFlow2, this.props.revenue2],
[this.props.date3, this.props.cashFlow3, this.props.revenue3],
[this.props.date4, this.props.cashFlow4, this.props.revenue4],
[this.props.date5, this.props.cashFlow5, this.props.revenue5],
];
const options = {
isStacked: true,
height: 300,
legend: { position: "top", maxLines: 3 },
vAxis: { minValue: 0 },
};
return (
<Chart
chartType="AreaChart"
width="75%"
height="400px"
data={data}
options={options}
chartEvents={chartEvents}
/>
);
}
}
export default AreaChart;

Cleaning up react-chartjs-2 chart points when making updates

I would like to create a chart from react-chartjs-2 that only renders the first and last points. You'll notice I have that implemented inside pointRadius. For a simple static chart that renders once this actually works fine.
The problem is I have another component that will update someDynamicData which causes the line chart to be updated and when it gets updated the previously drawn points are still visible.
Is there a way to cleanup each chart so when I dynamically update it I always get just the first and last points drawn for the current chart im looking at?
I have tried hooking up a ref and manually calling update and delete which both do not seem to work.
function getData (someData) {
return {
labels: someData.map(d => d.point),
datasets: [
{
data: someData.map(d => d.point),
fill: false,
borderColor: 'white',
tension: 0.5
},
],
};
}
function getOptions (someData) {
return {
elements: {
point: {
radius: 0,
pointRadius: function (context) {
if (context.dataIndex === 0 || context.dataIndex === someData.length - 1) {
return 10;
}
},
},
tooltips: {
enabled: false
},
},
plugins: {
legend: {
display: false
}
}
};
}
const LineChart = ({someDynamicData}) => ( <Line data={getData(someDynamicData)} options={getOptions(someDynamicData)} />);
export default LineChart;

React hooks async useEffect to load database data

I'm using async useEffect in React because I need to do database requests. Then, add this data to my react-charts-2
const [ plSnapShot, setPlSnapShot ] = useState({
grossIncome: 0.00,
opeExpenses: 0.00,
nonOpeExpenses: 0.00,
netIncome: 0.00,
grossPotencialRent: 0.00,
lastMonthIncome: 0.00
});
const [ thisMonthPayment, setThisMonthPayments ] = useState({
labels: [],
data: [],
color: 'blue'
});
useEffect(() => {
async function fetchData() {
await axios.get(`${url.REQ_URL}/home/getUserFullName/${userID}`)
.then(async (res) => {
setUserFullName(res.data);
await axios.get(`${url.REQ_URL}/home/getThisMonthPayments/${propertyID}`)
.then(async (resMonthPay) => {
let total = 0;
let obj = {
labels: [],
data: [],
color: 'blue'
};
const data = resMonthPay.data;
for(const d of data) {
obj.labels.push(helper.formatDate(new Date(d.date)));
obj.data.push(d.amount);
total += d.amount;
}
setThisMonthPayments(obj);
setTotalEarnedMonth(parseFloat(total));
await axios.get(`${url.REQ_URL}/home/plSnapshot/${propertyID}`)
.then(async (resPL) => {
const data = resPL.data;
setPlSnapShot({
grossIncome: parseFloat(data.GrossIncome || 0).toFixed(2),
opeExpenses: parseFloat(data.OperatingExpenses || 0).toFixed(2),
nonOpeExpenses: parseFloat(data.NonOperatingExpenses || 0).toFixed(2),
netIncome: parseFloat(data.NetIncome || 0).toFixed(2),
grossPotencialRent: parseFloat(data.GrossPotencialRent || 0).toFixed(2),
lastMonthIncome: parseFloat(data.LastMonthIncome || 0).toFixed(2)
});
});
});
});
}
fetchData();
}, [propertyID, userID]);
const pieChart = {
chartData: {
labels: ['Gross Income', 'Operating Expenses', 'Non Operating Expenses'],
datasets: [{
data: [plSnapShot.grossIncome, plSnapShot.opeExpenses, plSnapShot.nonOpeExpenses],
backgroundColor: [
ChartConfig.color.primary,
ChartConfig.color.warning,
ChartConfig.color.info
],
hoverBackgroundColor: [
ChartConfig.color.primary,
ChartConfig.color.warning,
ChartConfig.color.info
]
}]
}
};
const horizontalChart = {
label: 'Last Month Income',
labels: ['Gross Potencial Rent', 'Last Month Income'],
chartdata: [plSnapShot.grossPotencialRent, plSnapShot.lastMonthIncome]
};
Here is an example of how I call the Chart component in my code in the render/return method.
<TinyPieChart
labels={pieChart.chartData.labels}
datasets={pieChart.chartData.datasets}
height={110}
width={100}
/>
And my Pie Chart component is just to display it
import React from 'react';
import { Pie } from 'react-chartjs-2';
// chart congig
import ChartConfig from '../../Constants/chart-config';
const options = {
legend: {
display: false,
labels: {
fontColor: ChartConfig.legendFontColor
}
}
};
const TinyPieChart = ({ labels, datasets, width, height }) => {
const data = {
labels,
datasets
};
return (
<Pie height={height} width={width} data={data} options={options} />
);
}
export default TinyPieChart;
Mostly of the times it works just fine, but sometimes the chart data is loaded and displayed in the screen real quick, then it disappear and the chart is displayed empty (no data). Am I loading it properly with the useEffect or should I use another method?
Thanks you.
The momentary flashing is likely due to the fact that the chart data is empty on first render. So depending on the time it take for your useEffect to fetch the data, that flashing may present a real problem.
One common solution is to use a state variable to indicate that the data is being loaded and either not display anything in place of the chart or display a loaded of some sort. So you can add something like you suggested in the comments const [ loader, setLoader ] = useState(true). Then once the data is loaded, togged it to false.
Meanwhile, inside your render function, you would do:
...
...
{loader ?
<div>Loading....</div>
:
<TinyPieChart
labels={pieChart.chartData.labels}
datasets={pieChart.chartData.datasets}
height={110}
width={100}
/>
}
loader can go from true to false or vise versa, depending on what make more intuitive sense to you.

Testing Chart.js Plugin with React and Jest/Enzyme

For my project, I am using React and Jest/Enzyme. I built out a reusable Doughnut Chart component and used a plugin to render text in the middle of the Doughnut Chart. Here is my code.
export default class DoughnutChart extends React.Component {
renderDoughnutChart = () => {
const { labels, datasetsLabel, datasetsData, datasetsBackgroundColor, displayTitle, titleText, displayLabels, doughnutCenterText, doughnutCenterColor, height, width, onClick } = this.props;
const plugin = {
beforeDraw: (chart) => {
if (chart.config.options.elements.center) {
//Get ctx from string
let ctx = chart.chart.ctx;
//Get options from the center object in options
let centerConfig = chart.config.options.elements.center;
let fontStyle = centerConfig.fontStyle || "Arial";
let txt = centerConfig.text;
let color = centerConfig.color || "#000";
let sidePadding = centerConfig.sidePadding || 20;
let sidePaddingCalculated = (sidePadding / 100) * (chart.innerRadius * 2);
//Start with a base font of 30px
ctx.font = "30px " + fontStyle;
//Get the width of the string and also the width of the element minus 10 to give it 5px side padding
let stringWidth = ctx.measureText(txt).width;
let elementWidth = (chart.innerRadius * 2) - sidePaddingCalculated;
// Find out how much the font can grow in width.
let widthRatio = elementWidth / stringWidth;
let newFontSize = Math.floor(30 * widthRatio);
let elementHeight = (chart.innerRadius * 2);
// Pick a new font size so it will not be larger than the height of label.
let fontSizeToUse = Math.min(newFontSize, elementHeight);
//Set font settings to draw it correctly.
ctx.textAlign = "center";
ctx.textBaseline = "middle";
let centerX = ((chart.chartArea.left + chart.chartArea.right) / 2);
let centerY = ((chart.chartArea.top + chart.chartArea.bottom) / 2);
ctx.font = fontSizeToUse + "px " + fontStyle;
ctx.fillStyle = color;
//Draw text in center
ctx.fillText(txt, centerX, centerY);
}
}
};
const data = {
labels: labels,
datasets: [{
label: datasetsLabel,
data: datasetsData,
backgroundColor: datasetsBackgroundColor,
}],
config: {
animation: {
animateRotate: true,
}
}
};
const options = {
maintainAspectRatio: false,
responsive: true,
title: {
display: displayTitle,
text: titleText,
fontSize: 24,
},
legend: {
display: displayLabels,
},
elements: {
center: {
text: doughnutCenterText,
color: doughnutCenterColor,
fontStyle: "Arial",
sidePadding: 40,
}
},
animation: {
animateRotate: true,
},
onClick: onClick,
};
if (!labels || !datasetsLabel || !datasetsData || !titleText) {
return null;
}
Chart.pluginService.register(plugin);
return (
<Doughnut
data={data}
options={options}
height={height}
width={width}
/>
);
}
render() {
return (
this.renderDoughnutChart()
);
}
}
This code displays the chart and the text in the middle perfectly for me. My problem is when I try to write tests for this component, it says the lines for the plugin isn't covered. I have a basic test here that tests the component renders itself and its props.
it("should render based on passed in props", () => {
let testOnClick = jest.fn();
let labels = ["Red", "Yellow", "Blue"];
let datasetsLabel = "test data sets label";
let datasetsData = [10, 20, 30];
let datasetsBackgroundColor = ["red", "yellow", "blue"];
let titleText = "Title";
let height = 300;
let width = 300;
let doughnutCenterText = "Hello";
let doughnutCenterColor = "white";
let wrapper = shallow(
<DoughnutChart
labels={labels}
datasetsLabel={datasetsLabel}
datasetsData={datasetsData}
datasetsBackgroundColor={datasetsBackgroundColor}
titleText={titleText}
height={height}
width={width}
onClick={testOnClick}
doughnutCenterText={doughnutCenterText}
doughnutCenterColor={doughnutCenterColor}
/>
);
expect(wrapper.find("Doughnut").props().data.labels).toEqual(labels);
expect(wrapper.find("Doughnut").props().data.datasets[0].label).toEqual(datasetsLabel);
expect(wrapper.find("Doughnut").props().data.datasets[0].data).toEqual(datasetsData);
expect(wrapper.find("Doughnut").props().data.datasets[0].backgroundColor).toEqual(datasetsBackgroundColor);
expect(wrapper.find("Doughnut").props().options.title.text).toEqual(titleText);
expect(wrapper.find("Doughnut").props().height).toEqual(height);
expect(wrapper.find("Doughnut").props().width).toEqual(width);
expect(wrapper.find("Doughnut").props().options.elements.center.text).toEqual(doughnutCenterText);
expect(wrapper.find("Doughnut").props().options.elements.center.color).toEqual(doughnutCenterColor);
});
I'm guessing the plugin applies all the configuration changes when the chart is drawn, so I don't understand why the tests do not hit the plugin lines.
If anyone can show me how to test the plugin or guide me in the right direction, I would greatly appreciate it! Thank you!
Can you post the output of your test? Is it checking the actual node_module plugin for chart.js or is it the one that you've built?
If it's trying to test against node_modules then it's best to ignore that entire directory from your tests via your package.json like so.
"jest": {
"testPathIgnorePatterns": [
"./node_modules/"
],
"collectCoverageFrom": [
"!/node_modules/*"
]
}

Resources