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]);
Related
so recently we updated ag-grid-react and ag-grid-community from 27.0.1 to 28.0.0 and previous working tests now fail.
Test tries to get a value from a row cell and compares to the given one.
Test (v. 27.3.0)
describe("Simple list rendering", () => {
const handleRowDoubleClick = () => { }
const handleRowSelect = () => { }
const formDataWithId = formData.map((item, index) => {
const newItem = checkNumberLength(item, items);
return { ...newItem, id: index };
});
const colDefs = [{
headerName: "test",
valueGetter: "7"
}]
const listRender = (
<AgGridReact
columnDefs={colDefs}
rowData={formDataWithId}
rowSelection="multiple"
suppressClickEdit={true}
onRowClicked={handleRowSelect}
onRowDoubleClicked={handleRowDoubleClick}
immutableData={true}
rowHeight={28}
/>
)
let component
let agGridReact
beforeEach((done) => {
component = mount(listRender);
agGridReact = component.find(AgGridReact).instance();
// don't start our tests until the grid is ready
ensureGridApiHasBeenSet(component).then(() => done(), () => fail("Grid API not set within expected time limits"));
});
it('stateful component returns a valid component instance', () => {
expect(agGridReact.api).toBeTruthy();
});
it("agGrid shows password field as * instead of string", () => {
console.log(component.find(".ag-cell-value").first().html())
expect(component.render().find(".ag-cell-value").first()).toEqual("7");
});
it("List contains derivative field", () => {
render(listRender);
expect(screen.getAllByText("5")).toHaveLength(1);
})})
RowData Used
[
{ CPS: 2, TST: 2, DERIVATIVE: "" },
{ CPS: 5, TST: 2, DERIVATIVE: "" },
]
Console log in test
When running the App the Grid renders the cell values using custom valueGetters.
Test is running in v27.3.0 and renders the div using ag-cell-value class but not the value (in v28.0.0 does not even render the div when trying to find node using ag-cell-value class)
Is there something wrong we are doing? Any help is appreciated!
I have the following custom render function for my component.
It has two modes Create and Edit.
Create is synchronous and Edit is asynchronous.
The function is as follows:
const renderComponent = async (
scheduleId = "",
dialogMode = DialogMode.CREATE,
cohorts = MOCK_COHORT_LIST,
jobs = JOB_LIST,
availableForSchedule = domain === Domain.COHORT ? jobs : cohorts,
) => {
render(
<AddEditScheduleDialog
cohorts={cohorts}
jobs={jobs}
availableForSchedule={availableForSchedule}
scheduleToEdit={scheduleId}
handleToggleDialog={mockToggleDialog}
isDialogVisible={true}
domain={domain}
/>,
{
wrapper: queryWrapper,
},
);
if (dialogMode === DialogMode.EDIT) {
expect(screen.getByRole("progressbar")).toBeInTheDocument();
}
await waitFor(() =>
expect(screen.queryByRole("progressbar")).not.toBeInTheDocument(),
);
return {
header: screen.getByRole("heading", {
level: 1,
name: `schedules:addEditDialog.${domain}.${dialogMode}.title`,
}),
name: {
field: screen.getByTestId(`${domain}-name-field`),
button: within(screen.getByTestId(`${domain}-name-field`)).getByRole(
"button",
),
},
frequency: {
field: screen.getByTestId("schedule-frequency-field"),
input: within(
screen.getByTestId("schedule-frequency-field"),
).getByRole("textbox"),
helperText: `schedules:addEditDialog.form.frequency.helperText`,
},
};
};
Sometimes I get intermittent problems finding elements on the screen. Is this because the function returns before the progressbar has been awaited?
Is there a way i can wait for everything to be rendered prior to returning the screen elements that I need?
If your Edit mode is causing the progress bar to be displayed asynchronously you should wait for it by making use of findByRole e.g.
if (dialogMode === DialogMode.EDIT) {
expect(await screen.findByRole("progressbar")).toBeInTheDocument());
}
This is because find* queries use waitFor "under the hood".
Here useVideos() give us all videos form database. After adding a new video the new entry is not append in the Material UI table , but if I refresh the page then it's showing that new entry. Now I want to show this new entry after add operation. Please help me to do this! Thanks in Advance.
const initialState = [{}];
const reducer = (state, action) => {
switch (action.type) {
case "videos":
const data = [];
let cnt = 1;
action.value.forEach((video, index) => {
data.push({
sl: cnt,
noq: video.noq,
id: index,
youtubeID: video.youtubeID,
title: video.title,
});
cnt = cnt + 1;
});
return data;
default:
return state;
}
};
export default function ManageVideos() {
const { videos, addVideo, updateVideo, deleteVideo } = useVideos("");
const [videoList, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
dispatch({
type: "videos",
value: videos,
});
}, [videos]);
const columns = [
{ title: "Serial", field: "sl" },
{ title: "Title", field: "title" },
{ title: "Number of Qusetion", field: "noq" },
{ title: "Youtube Video Id", field: "youtubeID" },
];
return (
<div>
<MaterialTable
title="Videos Table"
data={videoList}
columns={columns}
editable={{
onRowAdd: (newData) =>
new Promise((resolve, reject) => {
setTimeout(() => {
addVideo(newData);
resolve();
}, 1000);
}),
onRowUpdate: (newData) =>
new Promise((resolve, reject) => {
setTimeout(() => {
updateVideo(newData);
resolve();
}, 1000);
}),
}}
/>
</div>
);
}
Since the information provided is a bit lacking, I'll assume that the useEffect hook is not working when you update your videos (check it with consle.log("I'm not working") and if it does work then you can just ignore this answer).
You can define a simple state in this component, let's call it reRender and set the value to 0, whenever the user clicks on the button to add a video you should call a function which adds 1 to the value of reRender (()=>setReRender(prevState=>prevState+1)). In your useEffect hook , for the second argument pass reRender. This way, when the user clicks to submit the changes , reRender causes useEffect to run and dispatch to get the new data.
If this doesn't work , I have a solution which takes a bit more work. You will need to use a state manager like redux or context api to store your state at a global level. You should store your videos there and use 1 of the various ways to access the states in this component (mapStateToProps or store.subscribe() or ...). Then pass the video global state as the second argument to useEffect and voilĂ , it will definitely work.
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.
According to the documentation, you can Set the chart-specific events you want to listen to and the corresponding callback. The example is given uses select which works fine (I've setup an example here. The problem comes when I try to use any other chart type.
From the google charts documentation, for a bar chart, I should be able to use a click event:
When I add a click event like so:
{
eventName: "click",
callback({}) {
console.log("clicked");
}
}
Nothing happens, but the select event still works. I've setup a code sandbox here to demonstrate this behavior. This also happens for animationfinish, onmouseover, and every other event I've checked.
Looks like rakannimer already answered this in #263 on the GitHub repository, but figured I'd answer this anyway in case anyone else was having this issue.
As this stack overflow answer does a great job of explaining, the ready event must be fired before chart events (like those in the screenshot) can be triggered. Therefore, if you want to use any other event, you have to initiate them in a callback like this:
<Chart
chartType="ScatterChart"
width="80%"
height="400px"
data={data}
options={options}
legendToggle
chartEvents={[
{
eventName: "ready",
callback: ({ chartWrapper, google }) => {
const chart = chartWrapper.getChart();
google.visualization.events.addListener(chart, "onmouseover", e => {
const { row, column } = e;
console.warn("MOUSE OVER ", { row, column });
});
google.visualization.events.addListener(chart, "onmouseout", e => {
const { row, column } = e;
console.warn("MOUSE OUT ", { row, column });
});
}
}
]}
/>
Please check below code snippet for adding click event:
import * as React from "react";
import { Chart } from "react-google-charts";
const chartEvents = [
{
callback: ({ chartWrapper, google }) => {
const chart = chartWrapper.getChart();
chart.container.addEventListener("click", (ev) => console.log(ev))
},
eventName: "ready"
}
];
const data = [
["age", "weight"],
[8, 12],
[4, 5.5],
[11, 14],
[4, 5],
[3, 3.5],
[6.5, 7]
];
const options = {
title: "Age vs. Weight comparison",
hAxis: { title: "Age", viewWindow: { min: 0, max: 15 } },
vAxis: { title: "Weight", viewWindow: { min: 0, max: 15 } },
legend: "none"
};
const ExampleChart = () => {
return (
<Chart
chartType="ScatterChart"
data={data}
options={options}
graphID="ScatterChart"
width="100%"
height="400px"
chartEvents={chartEvents}
/>
);
};
export default ExampleChart;
In addition to the solution provided by #jake, the chartWrapper is no more available in the callback event. In my case, replacing it with wrapper works. Like
<Chart
chartType="ScatterChart"
width="80%"
height="400px"
data={data}
options={options}
legendToggle
chartEvents={[
{
eventName: "ready",
callback: ({ wrapper, google }) => {
const chart = wrapper.getChart();
google.visualization.events.addListener(chart, "onmouseover", e => {
const { row, column } = e;
console.warn("MOUSE OVER ", { row, column });
});
google.visualization.events.addListener(chart, "onmouseout", e => {
const { row, column } = e;
console.warn("MOUSE OUT ", { row, column });
});
}
}
]}
/>```