Why does D3 Brush Not Clear Multiple Graphs in React App - reactjs

I have multiple graphs being displayed on the same page in different elements within the same React Component. Each graph uses the same bit of code, but only the 1st graph clears the Brush after a selection. All graphs work fine in a regular js file without React.
const plotArea = async (props: any) => {
...
// Handler for the end of a brush event from D3.
const brushEnded = (event: any) => {
const s = event.selection;
// Consume the brush action
if (s) {
d3.select('.brush').call(brush.move, null);
}
}
// Create a brush for selecting regions to zoom on.
const brush: any = d3
.brushX()
.extent([
[0, 0],
[width, height - 1],
])
.on('end', brushEnded);
// Zoom brush
svg.append('g').attr('class', 'brush').call(brush);
}
useEffect(() => {
// plotArea() occurs for each graph, there are multiple graphs
plotArea(...);
...
}, []);

When d3 runs the selection in d3.select('.brush').call(brush.move, null);, it does not limit the search to the component. It will search the whole document for a .brush element, and will stop as soon as it finds the first.
As a quick fix, you can save the specific brush of the component, so that you already have the reference and don't need to run a d3.select to get it back:
const plotArea = async (props: any) => {
...
// Create brushElement first
const brushElement = svg.append('g').attr('class', 'brush');
// Handler for the end of a brush event from D3.
const brushEnded = (event: any) => {
const s = event.selection;
// Consume the brush action
if (s) {
brushElement.call(brush.move, null); // Now uses the variable
}
}
// Create a brush for selecting regions to zoom on.
const brush: any = d3
.brushX()
.extent([
[0, 0],
[width, height - 1],
])
.on('end', brushEnded);
brushElement.call(brush);
}

Related

how to access image data on React-three-fiber

I would like to take image data (RGBA) from react-three-fiber
like this code on HTML
var myCanvas = document.createElement("canvas");
myCanvas.width = texture.image.width;
myCanvas.height = texture.image.height;
var myCanvasContext = myCanvas.getContext("2d"); // Get canvas 2d context
myCanvasContext.drawImage(texture.image, 0, 0); // Draw the texture
var texels = myCanvasContext.getImageData(0,0, width, height); // Read the texels/pixels back
However, I cannot access Canvas to drawImage or getImageData.
const ref = React.useRef();
React.useEffect(() => {
if (ref) {
//error
ref.current.getContext("2d");
}
}, []);
<Canvas
dpr={[1, 1.5]} //pixelRatio
ref={ref}
>
<Particles />
</Canvas>
and I tried to use useThree. there is no method to get imageData
const {
gl,
} = useThree();
lastly, I tired to get an image using useLoader as a texture
const map = useLoader(TextureLoader, "/scroll_images/img1.jpg");
so, how can I get the imageData?
In your first example, try useLayoutEffect instead of useEffect. Refs are not actually populated until the first DOM render happened. Unlike useEffect, useLayoutEffect waits until this occurred.
React.useLayoutEffect(() => {
ref.current.getContext("2d"); // <-- hopefully doesnt error
}, []);
In your second example I think its because you need to pass gl={{ preserveDrawingBuffer: true }} prop to your Canvas. Then you can use gl.domElement.toDataURL('image/png') or similar.

Performant way of getting mouse position every frame for canvas animation in React?

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?

how do i make echarts resize together with the react-grid-layout?

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.

React, insert/append/render existing <HTMLCanvasElement>

In my <App> Context, I have a canvas element (#offScreen) that is already hooked in the requestAnimationFrame loop and appropriately drawing to that canvas, verified by .captureStream to a <video> element.
In my <Canvas> react component, I have the following code (which works, but seems clunky/not the best way to copy an offscreen canvas to the DOM):
NOTE: master is the data object for the <App> Context.
function Canvas({ master, ...rest } = {}) {
const canvasRef = useRef(master.canvas);
const draw = ctx => {
ctx.drawImage(master.canvas, 0, 0);
};
useEffect(() => {
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
let animationFrameId;
const render = () => {
draw(ctx)
animationFrameId = window.requestAnimationFrame(render)
}
render();
return () => {
window.cancelAnimationFrame(animationFrameId);
}
}, [ draw ]);
return (
<canvas
ref={ canvasRef }
onMouseDown={ e => console.log(master, e) }
/>
);
};
Edited for clarity based on comments
In my attempts to render the master.canvas directly (e.g. return master.canvas; in <Canvas>), I get some variation of the error "Objects cannot be React children" or I get [object HTMLCanvasElement] verbatim on the screen.
It feels redundant to take the #offScreen canvas and repaint it each frame. Is there, instead, a way to insert or append #offScreen into <Canvas>, so that react is just directly utilizing #offScreen without having to repaint it into the react component canvas via the ref?
Specific Issue: Functionally, I'm rendering a canvas twice--once off screen and once in the react component. How do I (replace/append?) the component's <canvas> element with the offscreen canvas (#offScreen), instead of repainting it like I'm doing now?
For anyone interested, this was actually fairly straightforward, as I overcomplicated it substantially.
export function Canvas({ canvas, ...rest }) {
const container = useRef(null);
useEffect(() => {
container.current.innerHTML = "";
container.current.append(canvas);
}, [ container, canvas ]);
return (
<div ref={ container } />
)
}

Highcharts checkboxes multiply when window resize event fires

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.

Resources