I have an app which contains checkbox at the bottom of the chart (by the legend item).
When I resize the app window, event listener (which logs 'resize' events) changes the chart width accordingly and a new checkbox appears on the chart.
A new checkbox also appears as well when the chart theme is changed.
This makes me believe that whenever Highcharts are re-rendered (I use React), a new checkbox is created.
Initially, there is only one checkbox:
After single resize of the page:
After multiple resizes:
Also, there is always one checkbox at the top right corner of the chart (appears after the first resize):
The checkBox is added to the chart only once, at the componendDidMount() cycle. It is added to a single series using this option:
showCheckbox: true
The first and the only time the function that generates charts and sets this option is called in the main components cycle componentDidMount:
Main Container
import Charts from './Charts';
import * as highcharts from 'highcharts';
class MainComponent exteds React.Component<Props, State> {
public async componentDidMount() {
try {
// Here the charts are generated
const initialOpts = Charts.generateCharts();
const min = 1000;
const max = 5000;
highcharts.charts.map(chart => {
chart.xAxis[0].setExtremes(min, max, undefined, false)
});
} catch { ... }
this.chartDimensionsHandler();
window.addEventListener('resize', this.chartDimensionsHandler);
}
private chartDimensionsHandler() {
const chartHeight: number = ZoomService.getChartHeight();
this.setState({ chartHeight });
// When the state is set, component re-renders and causes the issue
}
/*
Theme is passed as props to the main component thus whenever theme changes,
new props arrive and component along with higcharts is also
re-rendered what causes a new checkbox to appear on top right of the chart
*/
}
Charts class
export class ChartService {
public generateCharts() {
return this.charts.map((char) => this.generateDataChart(chart));
}
private generateDataChart(chart: Chart): Options {
const { slices } = chart;
const xValues = slices.map(slice =>
Number(slice[chart.xAxis.name].value)
);
const xAxis = this.generateXAxisOptions(chart);
const yAxis = [ ... ];
const options = { xAxis, yAxis };
// Create line series
try {
const lineSeries = this.constructDataSeries(chart, xValues);
// Create events series
const eventsCharts = this.charts.filter(
x => x.type === 'events'
);
const eventsSeries = eventsCharts.map((evChart: Chart) => {
// Here I call a function which sets 'showCheckbox' to true
const allEvents = this.createEventSeries(evChart!)[0];
allEvents.yAxis = 1;
return allEvents;
});
const otherSeries = ...; // Irrelevant code
// Add all series
const series = [...otherSeries, ...eventsSeries];
return Object.assign(options, { series });
} catch (e) {
return Object.assign(
options,
{ series: [] },
{
lang: {
noData: `Fail.`
}
}
);
}
}
private constructEventSeries(chart) {
const { slices, yAxis, xAxis } = chart;
const xValues = slices.map(slice =>
Number(slice[xAxis.name].value)
);
return yAxis.map((item) => ({
name: item.label,
type: 'line',
data: this.convertEventData(slices, xValues),
lineWidth: 0,
showCheckbox: true, // THE PLACE WHERE I SET showCheckbox to true
marker: {
enabled: true,
radius: 10,
symbol : 'circle'
},
meta: {
type: 'events'
}
}));
}
}
Does anyone have an idea why a new checkbox is added to the chart every single render of it (no matter if the chart is resized or not)?
I found out that whenever resize event is logged, Highcharts automatically calls chart.reflow() function which caused the checkmarks to be multiplied. Component setState function made the situation even worse.
By changing the function which is called by the event from setting component's state into setting chart's dimensions directly with higcharts function chart.setSize(width, height, animation: boolean).
Then in the CSS I set the display of the second checkbox to none by using nth child property (because resize event always creates the second checkbox, there is no way to shut it (probably a bug from highcharts)).
This was my solution.
Related
I have an animated background using canvas and requestAnimationFrame in my React app and I am trying to have its moving particles interact with the mouse pointer, but every solution I try ranges from significantly slowing down the animation the moment I start moving the mouse to pretty much crashing the browser.
The structure of the animated background component goes something like this:
<BackgroundParentComponent> // Gets mounted only once
<Canvas> // Reutilizable canvas, updates every frame.
// v - dozens of moving particles (just canvas drawing logic, no JSX return).
// Each particle calculates its next frame updated state every frame.
{particlesArray.map(particle => <Particle/>}
<Canvas/>
<BackgroundParentComponent />
I have tried moving the event listeners to every level of the component structure, calling them with a custom hook with an useRef to hold the value without rerendering, throttling the mouse event listener so that it does not fire that often... nothing seems to help. This is my custom hook right now:
const useMousePosition = () => {
const mousePosition = useRef({ x: null, y: null });
useEffect(() => {
window.addEventListener('mousemove', throttle(200, (event) => {
mousePosition.current = { x: event.x, y: event.y };
}))
});
useEffect(() => {
window.addEventListener('mouseout', throttle(500, () => {
mousePosition.current = { x: null, y: null };
}));
});
return mousePosition.current;
}
const throttle = (delay: number, fn: (...args: any[]) => void) => {
let shouldWait = false;
return (...args: any[]) => {
if (shouldWait) return;
fn(...args);
shouldWait = true;
setTimeout(() => shouldWait = false, delay);
return;
// return fn(...args);
}
}
For reference, my canvas component responsible of the animation looks roughly like this:
const AnimatedCanvas = ({ children, dimensions }) => {
const canvasRef = useRef(null);
const [renderingContext, setRenderingContext] = useState(null);
const [frameCount, setFrameCount] = useState(0);
// Initialize Canvas
useEffect(() => {
if (!canvasRef.current) return;
const canvas = canvasRef.current;
canvas.width = dimensions.width;
canvas.height = dimensions.height;
const canvas2DContext = canvas.getContext('2d');
setRenderingContext(canvas2DContext);
}, [dimensions]);
// make component re-render every frame
useEffect(() => {
const frameId = requestAnimationFrame(() => {
setFrameCount(frameCount + 1);
});
return () => {cancelAnimationFrame(frameId)};
}, [frameCount, setFrameCount]);
// clear canvas with each render to erase previous frame
if (renderingContext !== null) {
renderingContext.clearRect(0, 0, dimensions.width, dimensions.height);
}
return (
<Canvas2dContext.Provider value={renderingContext}>
<FrameContext.Provider value={frameCount}>
<canvas ref={canvasRef}>
{children}
</canvas>
</FrameContext.Provider>
</Canvas2dContext.Provider>
);
};
The mapped <Particle/> components are are fed to the above canvas component as children:
const Particle = (props) => {
const canvas = useContext(Canvas2dContext);
useContext(FrameContext); // only here to force the force that the particle re-render each frame after the canvas is cleared.
// lots of state calculating logic here
// This is where I need to know mouse position every (or every few) frames in order to modify each particle's behaviour when near the pointer.
canvas.beginPath();
// canvas drawing logic
return null;
}
Just to clarify, the animation is always moving regardless of the mouse being idle, I've seen other solutions that only work for animations triggered exclusively by mouse movement.
Is there any performant way of accessing the mouse position each frame in the Particle mapped components without choking the browser? Is there a better way of handling this type of interactive animation with React?
This question is merely for curiosity. So, I have a parent component that fetches some data (Firebase) and saves that data in the state, and also passes the data to the child. The child's code is the following:
import { Bar, Doughnut } from 'react-chartjs-2'
import { Chart } from 'chart.js/auto'
import { useState, useEffect } from 'react'
import _ from 'lodash'
const initialData = {
labels: [],
datasets: [
{
id: 0,
data: [],
backgroundColor: ['#FF6384', '#36A2EB', '#FFCE56', '#FF6384'],
color: ['#FF6384', '#36A2EB', '#FFCE56', '#FF6384'],
borderWidth: 0,
},
],
}
var config2 = {
maintainAspectRatio: true,
responsive: true,
}
export default function DoughnutChart(props) {
const [data, setData] = useState(initialData)
useEffect(() => {
const newData = _.cloneDeep(initialData)
var wins = 0
var losses = 0
var labels = ['wins', ' losses']
//Whithout seTimeout the chart is not updated
setTimeout(() => {
props.trades.forEach((trade) => {
if (trade.cprice) {
if (trade.cprice >= trade.price) {
wins++
} else {
losses++
}
}
})
newData.datasets[0].data = [wins, losses]
newData.labels = labels
setData(newData)
}, 1000)
}, [props.trades])
return (
<Doughnut
data={data}
options={config2}
redraw
/>
)
}
As you can see the child listens with useEffect to props changes, and then I process the props as I want to plot the necessary information on the Chart.
The thing is that in the beginning, the code didn't work (the Chart didn't display anything despite the props changed), I console logged the props, and it seems that something was happening too fast (if I console the length of the props.trades it showed me 0, but if I consoled the object it shows data in it) So that the forEach statement wasn't starting to iterate in the first place. When I added the setTimeout it started working if a put a 1000 milliseconds (with 500 milliseconds it doesn't work).
I'm a beginner at React and would be very interested in why this happens and what is the best approach to handle these small delays in memory that I quite don't understand.
github: https://github.com/Brent-W-Anderson/shoe_store/tree/main
Hi, I need help using my react ref correctly. I'm creating an image slider and I'm centering the first image in the set based on the image width (which is based on a height all of the images share --which means the widths are different from image to image).
When my component mounts, the ref width is undefined (I'm assuming because it's not mounted yet? but I can't figure it out). I know this should work because I'm setting a "resize" event within the same componentDidMount function and as soon as I go to resize the window ( or after some time passes ), then the width of each image is showing exactly as it should be.
on initial page load:
after resizing the window:
src > components > shoes > shoe.tsx (where the magic happens)
Please do this and let me know the outcome. There could be weird behavior with classes without constructors. Also check this: why we write super props for reasons behind use of constructors.
export default class Shoe extends Component<{ shoe: { asset:string, name:string, price:number }, handleShoePos:Function }> {
// add this
constructor(props){ // use some tsx behavior if you want
super(props);
this.cardRef = React.createRef<HTMLDivElement>();
}
componentDidMount() {
window.addEventListener( 'resize', this.handleResize );
this.handleResize();
}
handleResize = () => {
const { handleShoePos } = this.props;
const width = this.cardRef.current?.clientWidth;
if( width ) {
handleShoePos( width / 2 );
}
}
render() {
const { asset, name, price } = this.props.shoe;
return (
<div ref={ this.cardRef } className="shoe_card">
......
</div>
);
}
}
I am using ResponsiveGridLayout, React-Grid-Layout in my application, and I am using echarts as grid items.
The drag and drop works fine, but when i resize the grid item, the chart did not resize together with it. I have tried implementing the onLayoutchange properties, but it is not working.
can someone can help me out here
this is my codesandbox that reproduce the issue
I was able to achieve this, at least when modifying grid items width (not height yet...), by using this hook, then in your chart component :
[...]
const chartRef = useRef<HTMLDivElement>();
const size = useComponentSize(chartRef);
useEffect(() => {
const chart = chartRef.current && echarts.getInstanceByDom(chartRef.current);
if (chart) {
chart.resize();
}
}, [size]);
[...]
return <div ref={chartRef}></div>;
...so your chart will resize when the grid item is resized. I'm not sure about that, still a WIP for me but it works.
Extract this as a custom hook
You can create useEchartResizer.ts, based on #rehooks/component-size :
import useComponentSize from '#rehooks/component-size';
import * as echarts from 'echarts';
import React, { useEffect } from 'react';
export const useEchartResizer = (chartRef: React.MutableRefObject<HTMLDivElement>) => {
const size = useComponentSize(chartRef);
useEffect(() => {
const chart = chartRef.current && echarts.getInstanceByDom(chartRef.current);
if (chart) {
chart.resize();
}
}, [chartRef, size]);
};
Then use it in the component which holds the chart :
export const ComponentWithChart = (props): React.ReactElement => {
const chartRef = useRef<HTMLDivElement>();
useEchartResizer(chartRef);
useEffect(() => {
const chart = echarts.init(chartRef.current, null);
// do not set chart height in options
// but you need to ensure that the containing div is not "flat" (height = 0)
chart.setOption({...} as EChartsOption);
});
return (<div ref={chartRef}></div>);
});
So each time the div is resized, useEchartResizer will trigger a chart.resize(). Works well with react-grid-layout.
I am using react-highchart-official in my project. I need to retrieve some values from the click event. In the component, the event listener is set using plotOptions like this:
plotOptions: {
series: {
point: {
events: {
click: event => {
this.props.onClick && this.props.onClick(event);
}
}
}
}
}
It gets invoked with event object which contains a point entry. point contains useful information regarding chart.
The chart component is used as:
import React from 'react';
import { render } from 'react-dom';
// Import Highcharts
import Highcharts from 'highcharts';
// Import our demo components
import Chart from './components/Chart.jsx';
// Load Highcharts modules
require('highcharts/modules/exporting')(Highcharts);
const chartOptions = {
title: {
text: ''
},
series: [
{
data: [[1, 'Highcharts'], [1, 'React'], [3, 'Highsoft']],
keys: ['y', 'name'],
type: 'pie'
}
]
};
class App extends React.Component {
state = { flag: true };
onChartClick(event) {
// All values of event.point are null. Not sure why.
// Even though it's not a synthetic event.
console.log(event.point);
this.setState({ flag: false });
}
render() {
const { flag } = this.state;
return (
<div>
<h1>Demos</h1>
<h2>Highcharts</h2>
{flag && (
<Chart
highcharts={Highcharts}
onClick={this.onChartClick.bind(this)}
/>
)}
</div>
);
}
}
For some unknown reasons, all the value inside point property of click event is set to null if I hide the chart. Is there any way I can avoid it? In the actual project on chart click I need to redirect user to somewhere. To imitate the situation, I have hidden the chart.
Codesandbox
This situation exactly shows what happened with your point data, see:
Demo: https://jsfiddle.net/BlackLabel/o70mLrnt/
var chart = Highcharts.chart('container', {
series: [{
data: [43934, 52503, 57177, 69658, 97031, 119931, 137133, 154175]
}]
});
console.log(chart.series[0].points[1])
chart.destroy();
Setting the flag to false unmounting the component which destroys the chart and later console.log doesn't have 'access' to chart and particular point properties.
If you need only a few data from the point a good option will be setting them in the state before the flag will be changed.
Like this - this will keep your point y value.
this.setState({
pointValue: event.point.y
});
Another solution which came to my mind is to set the whole point object to some variable, but here the setTimeout needs to be used.
Demo with setState solution and saving object to variable: https://codesandbox.io/s/highcharts-react-demo-dnbdv