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;
Related
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;
So I just started using Material UI and pretty much I am loving it. Now, we are working on a project that involves data from users, employees and addresses in a specific city here in Philippines and I decided to use a table to display it to the client since I find this much easier. So, this table needs to be paginated, sorted, filtered, etc. and we are doing it in the server's side. Apparently, client needs to send couple of data like { page: 1, page_size: 50, ....} and that's what we did in my react.
The problem that I have right now is I think it is with the DataGrid. I think the table does not re-render the rowCount after fetching the totalRows data in the database. I have the sandbox here (PS: You have to enlarge the output screen to have the rowsPerPageOptions visible.) But as you can notice in there the first time it loads the next arrow is disabled and it does not re-render the time the actual data including the number of rows was loaded. But if you keep navigating like changing the page size it goes available like nothing is wrong.
I'm kind of stuck with this issue right now and I don't even know if I am using it the right way. Any help will be appreciated.
import { useState, useEffect } from "react";
import { DataGrid } from "#material-ui/data-grid";
import { Box } from "#material-ui/core";
const dummyColorsDB = [
{ id: 1, color: "red" },
{ id: 2, color: "green" },
{ id: 3, color: "blue" },
{ id: 4, color: "violet" },
{ id: 5, color: "orange" },
{ id: 6, color: "burgundy" },
{ id: 7, color: "pink" },
{ id: 8, color: "yellow" },
{ id: 9, color: "magenta" },
{ id: 10, color: "random color" },
{ id: 11, color: "another random color" },
{ id: 12, color: "last one" }
];
export default function App() {
const [data, setData] = useState({
loading: true,
rows: [],
totalRows: 0,
rowsPerPageOptions: [5, 10, 15],
pageSize: 5,
page: 1
});
const updateData = (k, v) => setData((prev) => ({ ...prev, [k]: v }));
useEffect(() => {
updateData("loading", true);
setTimeout(() => {
const rows = dummyColorsDB.slice(
(data.page - 1) * data.pageSize,
(data.page - 1) * data.pageSize + data.pageSize
);
console.log(rows);
updateData("rows", rows);
updateData("totalRows", dummyColorsDB.length);
updateData("loading", false);
}, 500);
}, [data.page, data.pageSize]);
return (
<Box p={5}>
<DataGrid
density="compact"
autoHeight
rowHeight={50}
//
pagination
paginationMode="server"
loading={data.loading}
rowCount={data.totalRows}
rowsPerPageOptions={data.rowsPerPageOptions}
page={data.page - 1}
pageSize={data.pageSize}
rows={data.rows}
columns={[{ field: "color", headerName: "Color", flex: 1 }]}
onPageChange={(data) => {
updateData("page", data.page + 1);
}}
onPageSizeChange={(data) => {
updateData("page", 1);
updateData("pageSize", data.pageSize);
}}
/>
</Box>
);
}
When you update the component state by calling multiple times like this:
updateData("rows", rows);
updateData("rowCount", dummyColorsDB.length);
updateData("loading", false);
Your updateData calls setState but because setState executes asynchronously, they are not updated at the same time. In fact, the reason why the pagination doesn't work at the first render is because you set the grid rows before setting its rowCount. My guess is that this is a Material-UI bug after inspecting the codebase. They don't seem to add state.options.rowCount to the dependency array in useEffect so nothing get re-render when you update rowCount later.
This is clearer when you defer each call a little bit. The code below does not work.
// set rows first
updateData("rows", rows);
setTimeout(() => {
// set rowCount later
updateData("rowCount", dummyColorsDB.length);
updateData("loading", false);
}, 100);
But try setting the rowCount first and the pagination works again
// set rowCount first
updateData("rowCount", dummyColorsDB.length);
setTimeout(() => {
updateData("rows", rows);
updateData("loading", false);
}, 100);
Another solution is to update all related state at the same time:
setData((d) => ({
...d,
rowCount: dummyColorsDB.length,
rows,
loading: false
}));
Live Demo
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.
I am trying to achieve a behavior in simple column chart in React, where I can click on series point and have xAxis label change style. Also, when you click again, that style should be removed. It is the same behavior as we have for mouse over and mouse out but for click event. I can get it to work with mouse events, but not click event.
Is this possible to achieve? This is a code sample I have.
Do the following:
Maintain a state say current update its value with the current axis number upon onClick
Define x-Axis and labels in your config-options
Use formatter function inside label. This function provides current axis value as argument. use it and compare it with your current state and adjust the style dynamically.
Working copy of code sample is here
Code Snippet
class App extends React.Component {
state = {
current: "black"
};
options = {
tooltip: {
enabled: false
},
xAxis: {
labels: {
formatter: item => {
const color = this.state.current === item.value ? "red" : "black";
const fontWeight =
this.state.current === item.value ? "bold" : "normal";
return `<span style="color: ${color}; font-weight: ${fontWeight}">${
item.value
}</span>`;
}
}
},
series: [
{
data: [1, 2, 3, 4],
type: "column",
colors: ["#000000"],
cursor: "pointer",
point: {
events: {
click: (e, x, y) => {
this.setState({ current: e.point.x });
console.log(e.target, e.point.x);
}
// mouseOver: function(e) {
// $(this.series.chart.xAxis[0].labelGroup.element.childNodes[this.x]).css({fontWeight: 'bold'});
// },
// mouseOut: function() {
// $(this.series.chart.xAxis[0].labelGroup.element.childNodes[this.x]).css({fontWeight: 'normal'});
// }
}
}
}
]
};
render() {
return (
<div>
<h2>Highcharts</h2>
<ReactHighcharts config={this.options} />
</div>
);
}
}
Just use the click event function to change the label CSS style. For example:
series: [{
...,
point: {
events: {
click: function() {
var ticks = this.series.xAxis.ticks,
label,
fontWeight;
if (ticks[this.x]) {
label = ticks[this.x].label;
fontWeight = (
label.styles.fontWeight && label.styles.fontWeight === 'bold'
) ? 'normal' : 'bold';
ticks[this.x].label.css({
'fontWeight': fontWeight
});
}
}
}
}
}]
Live demo: http://jsfiddle.net/BlackLabel/6m4e8x0y/4991/
API Reference:
https://api.highcharts.com/highcharts/series.column.events.click
https://api.highcharts.com/class-reference/Highcharts.SVGElement#css
I am using Highcharts React wrapper in an app using Hooks, when my chart is either loaded or zoomed it fires both setExtremes and setAfterExtremes multiple times each. I've looked through for similar questions but they are related to different issues.
I've reduced the code to the minimum setup, the page is not refreshing, the data is only parsed once and added to the chart once yet, animation is disabled and it's still consistently firing both events 7 times on:
* initial population
* on zoom
Versions: react 16.9, highcharts 7.2, highcharts-react-official 2.2.2
Chart
<HighchartsReact
ref={chart1}
allowChartUpdate
highcharts={Highcharts}
options={OPTIONS1}
/>
Chart Options:
const [series1, setSeries1] = useState([]);
const OPTIONS1 = {
chart: {
type: 'spline',
zoomType: 'x',
animation: false
},
title: {
text: ''
},
xAxis: {
events: {
setExtremes: () => {
console.log('EVENT setExtremes');
},
afterSetExtremes: () => {
console.log('EVENT sfterSetExtremes');
}
},
},
plotOptions: {
series: {
animation: false
}
},
series: series1
};
Data Population:
useEffect(() => {
if (data1) {
const a = [];
_.each(data1.labels, (sLabel) => {
a.push({
name: sLabel,
data: [],
})
});
... POPULATES DATA ARRAYS...
setSeries1(a);
}
}, [data1]);
Rather the question is old I also faced the same situation. The solution is to move the chart options to a state variable. Then the event will not fire multiple times.
It is mentioned on the library docs. https://github.com/highcharts/highcharts-react -- see the "optimal way to update"
import { render } from 'react-dom';
import HighchartsReact from 'highcharts-react-official';
import Highcharts from 'highcharts';
const LineChart = () => {
const [hoverData, setHoverData] = useState(null);
// options in the state
const [chartOptions, setChartOptions] = useState({
xAxis: {
categories: ['A', 'B', 'C'],
events: {
afterSetExtremes: afterSetExtremes,
},
},
series: [
{ data: [1, 2, 3] }
],
});
function afterSetExtremes(e: Highcharts.AxisSetExtremesEventObject) {
handleDateRangeChange(e);
}
return (
<div>
<HighchartsReact
highcharts={Highcharts}
options={chartOptions}
/>
</div>
)
}
render(<LineChart />, document.getElementById('root'));```
Your useEffect is getting fired multiple times probably because you are checking for data1 and data1 is changing. have you tried putting an empty array in your useEffect and see if it is firing multiple times?
if it only fires up once then the problem is that your useEffect is checking for a value that is constantly changing
if it still fires multiple times then there is something that is triggering your useEffect
I struggled the same problem after I SetState in useEffect().
My problem was I did a (lodash) deepcopy of the Whole options.
This also create a new Event every time.
// Create options with afterSetExtremes() event
const optionsStart: Highcharts.Options = {
...
xAxis: {
events: {
afterSetExtremes: afterSetExtremesFunc,
}
},
....
// Save in state
const [chartOptions, setChartOptions] = useState(optionsStart);
// On Prop Change I update Series
// This update deepcopy whole Options. This adds one Event Every time
React.useEffect(() => {
var optionsDeepCopy = _.cloneDeep(chartOptions);
optionsDeepCopy.series?.push({
// ... Add series data
});
setChartOptions(optionsDeepCopy);
}, [xxx]);
The fix is to Only update the Series. Not whole Options.
React.useEffect(() => {
var optionsDeepCopy = _.cloneDeep(chartOptions);
optionsDeepCopy.series?.push({
// ... Add series data
});
const optionsSeries: Highcharts.Options = { series: []};
optionsSeries.series = optionsDeepCopy.series;
setChartOptions(optionsSeries);
}, [xxx]);