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

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.

Related

Two React useEffect hooks accessing the window onscroll?

I am new to React dev so this may be something simple I am missing with hooks.
Using a template, I have used a header bar which shrinks in height if you scroll down in the page far enough (i.e it is only at max height if you scroll to the top).
I have been customising a sidebar to go along with the headerbar, and I'm trying to get the items within it to also move up when the bottom of the headerbar moves up.
The app bar uses a pre-made function:
import { useState, useEffect } from 'react';
// ----------------------------------------------------------------------
export default function useOffSetTop(top: number) {
const [offsetTop, setOffSetTop] = useState(false);
const isTop = top || 100;
useEffect(() => {
window.onscroll = () => {
if (window.pageYOffset > isTop) {
setOffSetTop(true);
} else {
setOffSetTop(false);
}
};
return () => {
window.onscroll = null;
};
}, [isTop]);
return offsetTop;
}
Then you can just import it, assign a constant bool to useOffSetTop(HEADER.DASHBOARD_DESKTOP_HEIGHT) and base the layout on the state of that const.
In the app bar it controls the height, so in the nav bar I made it control he height of an empty .
It does work, but the app bar stops working.
I do have hot-reload on and if I make a change to the app bar it starts working but the nav bar stops working.
I guess it is just because whichever loads last is the one which binds something to window.onscroll and the other is wiped.
I am just wondering how I could change this function or restructure the code so that this could be imported by multiple components on the same page - possibly without having to just import it higher up and pass the true/false value down through the components?
The issue is that you are actually "overriding" the onScroll function (or replacing it) instead of listening for the event.
by doing this
window.onScroll = null;
you are effectively overriding the onScroll function to do nothing.
Best to listen for the onscroll event.
import { useState, useEffect } from 'react';
export default function useOffSetTop(top: number) {
const [offsetTop, setOffSetTop] = useState(false);
const isTop = top || 100;
const handleOnScroll = () => {
if (window.pageYOffset > isTop) {
setOffSetTop(true);
} else {
setOffSetTop(false);
}
}
useEffect(() => {
window.addEventListener('scroll', handleOnScroll )
return () => {
window.removeEventListener('scroll', handleOnScroll)
};
}, [isTop, handleOnScroll]);
return offsetTop;
}

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 } />
)
}

React Native - Modal - Dynamic Max Height

I'm using a modal within a view - which contains a form. The form is longer than the viewport - so, the content is taking up the height of the page and scrolling out of view.
Can anyone advise on the best approach for dynamic height?
currently i'm using the following approach, but doesnt work if phone orientation switched and i'm sure there must be a better solution?
heightScreen = () => {
return Dimensions.get('window').height - 150;
}
<Modal
isVisible={this.props.showModal}
animationInTiming={500}
backdropColor={'#f79431'}
style={{ marginVertical:50, maxHeight: this.heightScreen()}}
>
import {useEffect, useState} from 'react';
import {Dimensions} from 'react-native';
export const useOrientation = () => {
const [orientation, setOrientation] = useState("PORTRAIT");
useEffect(() => {
Dimensions.addEventListener('change', ({ window:{ width, height } }) => {
setOrientation(width < height ? "PORTRAIT" : "LANDSCAPE")
})
}, []);
return orientation;
}
You can add this function as a helper to detect the orientation (portrait/landscape) and based on that to apply the correct height.

Custom react-admin drag & drop list

It can't drag. What is wrong with it?
I'm using react-sortable-hoc with material-ui to custom react-admin list page with drag & drop sortable.
Demo : https://codesandbox.io/s/vibrant-visvesvaraya-4k3gs
Source code: https://github.com/tangbearrrr/poc-ra-sort-drag/tree/main
As I checked you are getting data from the props and in props there is no data field exists, so the error is coming from there
Here is the all props list
The sortable method you are using is from react-sortable-hoc, which adds huge complexity to react-admin.
Not so fast, I have run out of attempts trying to debug your code and come up with another solution works just fine but not so ideal, is to use sortablejs:
yarn add sortablejs
yarn add #types/sortablejs --dev
Do not mess up with react-sortablejs, this also applies the same complexity level as react-sortable-hoc.
Let's use your cmsLanguage as an example, with changes to use Datagrid instead.
Just be reminded that this working solution needs several retries on null el (e.g. your data is fetching, slow network speed, etc). The code below has 3 retries, 1500 milliseconds per each retry. The initialisation will stop after 3 attempts.
import {Datagrid, ShowButton, TextField} from "react-admin";
import * as React from "react";
import MenuIcon from '#mui/icons-material/Menu';
import {useEffect} from "react";
import Sortable from 'sortablejs';
const LanguageList = () => {
// This will run the effect after every render
useEffect(() => {
// https://github.com/SortableJS/Sortable
const retries = 3;
const millisecords = 1500;
let attempts = 0;
const retrySortable = () => {
const el = document.querySelector('#sortable-list tbody');
if (!el) {
if (++attempts >= retries) {
console.log(`cannot initialise sortable after ${retries} retries.`);
} else {
setTimeout(() => retrySortable(), millisecords);
}
} else {
// #ts-ignore
new Sortable(el, {
handle: ".handle",
draggable: "tr",
animation: 150, // ms, animation speed moving items when sorting, `0` — without animation
easing: "cubic-bezier(1, 0, 0, 1)", // Easing for animation. Defaults to null. See https://easings.net/ for examples.
// Element dragging ended
onEnd: (evt) => {
// #ts-ignore
const reorderedList: string[] = [];
const list = document.querySelectorAll('#sortable-list tbody td.column-name span');
[].forEach.call(list, function (span: Element) {
reorderedList.push(span.innerHTML);
});
console.log(JSON.stringify(reorderedList));
console.log(evt);
},
});
}
}
retrySortable();
}, []);
return (
<section id="sortable-list">
<Datagrid>
<MenuIcon sx={{cursor: "pointer"}} className="handle"/>
<TextField source="name"/>
<ShowButton/>
</Datagrid>
</section>
);
};
export default LanguageList;
When someone has a request for a demo, I will draw some time to make this a GitHub repo for better reference.

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