Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Heatmap chart instance management #749

Merged
merged 1 commit into from
Feb 15, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 43 additions & 42 deletions packages/data-viz/src/core/HeatmapChart/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
useState,
} from "react";

import { EMPTY_OBJECT } from "../../common/utils";
import { EMPTY_OBJECT } from "src/common/utils";
import { useUpdateChart } from "./hooks/useUpdateChart";
import { CreateChartOptionsProps } from "./hooks/utils";
import { ChartContainer } from "./style";
Expand Down Expand Up @@ -52,22 +52,42 @@ const HeatmapChart = forwardRef(
throw Error("Chart must have width and height > 0");
}

// State for chart initialization
const [isChartInitialized, setIsChartInitialized] = useState(false);

// State for ECharts instance
const [chart, setChart] = useState<ECharts | null>(null);

// Ref for the chart container
const innerRef = useRef<HTMLDivElement | null>(null);

/**
* (thuang): We need both a state and a ref to store the chart instance, so
* some hooks can opt out of re-rendering when the chart instance changes.
*/
const [chart, setChart] = useState<ECharts | null>(null);
const chartRef = useRef(chart);

/**
* (thuang): Use this ref to store the onEvents prop to prevent
* unnecessary re-renders when the onEvents prop changes.
* NOTE: This implies that `onEvents` prop changes alone from the parent
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this potentially cause issues later on?
I mean the fact that changing the onEvents won't trigger re-renders as you've mentioned!

Copy link
Contributor Author

@tihuan tihuan Feb 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I was debating about this too 🤔

Really about finding a balance between UX (asking users to useCallback() and useMemo() to memoize their onEvents object, vs. performance (re-render every time onEvents changes or not)

I think in this case it feels like the event handlers are less likely to change (besides users re-creating the object every time) than data being passed, so I feel it's probably an okay tradeoff here?

Also I think the current implementation in this PR also ignores all the prop changes except for [echartsRendererMode] as well, so changing onEvents also won't re-render the chart, so behavior wise it should be the same as the current PR!

* won't re-render the chart
*/
const onEventsRef = useRef(onEvents);

/**
* (thuang): Use this function to dispose the chart instance for both
* the state and the ref. This is to prevent memory leaks.
*/
const disposeChart = useCallback(() => {
chartRef.current?.dispose();
chartRef.current = null;
setChart(null);
}, []);

// Function to initialize the chart
const initChart = useCallback(() => {
const onEventsCurrent = onEventsRef.current;
const { current } = innerRef;

if (
!current ||
isChartInitialized ||
chartRef.current ||
// (thuang): echart's `init()` will throw error if the container has 0 width or height
current?.getAttribute("height") === "0" ||
current?.getAttribute("width") === "0"
Expand All @@ -81,60 +101,41 @@ const HeatmapChart = forwardRef(
useDirtyRect: true,
});

setIsChartInitialized(true);

// Bind events if provided
if (onEvents) {
bindEvents(rawChart, onEvents);
if (onEventsCurrent) {
bindEvents(rawChart, onEventsCurrent);
}

setChart(rawChart);
chartRef.current = rawChart;

// Cleanup function
return () => {
chart?.dispose();
disposeChart();

// Unbind events if provided
if (onEvents) {
for (const eventName in onEvents) {
if (onEventsCurrent) {
for (const eventName in onEventsCurrent) {
if (
Object.prototype.hasOwnProperty.call(onEvents, eventName) &&
Object.prototype.hasOwnProperty.call(
onEventsCurrent,
eventName
) &&
typeof eventName === "string" &&
typeof onEvents[eventName] === "function"
typeof onEventsCurrent[eventName] === "function"
) {
rawChart.off(eventName, onEvents[eventName]);
rawChart.off(eventName, onEventsCurrent[eventName]);
}
}
}
};
}, [chart, echartsRendererMode, isChartInitialized, onEvents]);
}, [echartsRendererMode, disposeChart]);

// Initialize charts on component mount
useEffect(() => {
disposeChart();
initChart();
}, [initChart]);

// Reinitialize the chart when the renderer mode changes
useEffect(() => {
// Dispose of the current chart
chart?.dispose();

// Reset chart initialization state and set chart to null
setIsChartInitialized(false);
setChart(null);

// Initialize a new chart
initChart();

/**
* (masoudmanson): We don't include the 'chart' dependency in the array because
* doing so would lead to an infinite loop of initializing and disposing the chart.
* We specifically want to reinitialize the chart only when the renderer mode changes.
* However, since we use the 'chart' state to dispose of the chart, we disable the
* eslint rule for exhaustive-deps to prevent warnings.
*/
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [echartsRendererMode]);
}, [initChart, disposeChart]);

// Hook to update chart data and options
useUpdateChart({
Expand Down
Loading