Skip to content

Commit

Permalink
fix: improve and fix rerendering (#790)
Browse files Browse the repository at this point in the history
* perf: reduce rerenders

* test: added redraw story
  • Loading branch information
dangreen committed Oct 20, 2021
1 parent 2f19eb3 commit 330fb1c
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 97 deletions.
164 changes: 70 additions & 94 deletions src/chart.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import React, {
useEffect,
useState,
useRef,
useImperativeHandle,
useMemo,
forwardRef,
} from 'react';
import type { Ref, MouseEvent } from 'react';
import React, { useEffect, useRef, useState, forwardRef } from 'react';
import type { ForwardedRef, MouseEvent } from 'react';
import ChartJS from 'chart.js/auto';
import type { ChartData, ChartType, DefaultDataPoint } from 'chart.js';

import { Props, ChartJSOrUndefined, TypedChartComponent } from './types';
import type { Props, TypedChartComponent } from './types';
import { reforwardRef, setNextDatasets } from './utils';

const noopData = {
datasets: [],
};

function ChartComponent<
TType extends ChartType = ChartType,
Expand All @@ -22,7 +20,7 @@ function ChartComponent<
width = 300,
redraw = false,
type,
data,
data: dataProp,
options,
plugins = [],
getDatasetAtEvent,
Expand All @@ -32,45 +30,49 @@ function ChartComponent<
onClick: onClickProp,
...props
}: Props<TType, TData, TLabel>,
ref: Ref<ChartJS<TType, TData, TLabel>>
ref: ForwardedRef<ChartJS<TType, TData, TLabel>>
) {
type TypedChartJS = ChartJSOrUndefined<TType, TData, TLabel>;
type TypedChartJS = ChartJS<TType, TData, TLabel>;
type TypedChartData = ChartData<TType, TData, TLabel>;

const canvas = useRef<HTMLCanvasElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const chartRef = useRef<TypedChartJS | null>();
/**
* In case `dataProp` is function use internal state
*/
const [computedData, setComputedData] = useState<TypedChartData>();
const data: TypedChartData =
computedData || (typeof dataProp === 'function' ? noopData : dataProp);

const computedData = useMemo<TypedChartData>(() => {
if (typeof data === 'function') {
return canvas.current
? data(canvas.current)
: {
datasets: [],
};
} else return data;
}, [data, canvas.current]);
const renderChart = () => {
if (!canvasRef.current) return;

const [chart, setChart] = useState<TypedChartJS>();
chartRef.current = new ChartJS(canvasRef.current, {
type,
data,
options,
plugins,
});

useImperativeHandle<TypedChartJS, TypedChartJS>(ref, () => chart, [chart]);
reforwardRef(ref, chartRef.current);
};

const renderChart = () => {
if (!canvas.current) return;

setChart(
new ChartJS(canvas.current, {
type,
data: computedData,
options,
plugins,
})
);
const destroyChart = () => {
reforwardRef(ref, null);

if (chartRef.current) {
chartRef.current.destroy();
chartRef.current = null;
}
};

const onClick = (event: MouseEvent<HTMLCanvasElement>) => {
if (onClickProp) {
onClickProp(event);
}

const { current: chart } = chartRef;

if (!chart) return;

getDatasetAtEvent &&
Expand Down Expand Up @@ -105,80 +107,54 @@ function ChartComponent<
);
};

const updateChart = () => {
if (!chart) return;

if (options) {
chart.options = { ...options };
/**
* In case `dataProp` is function,
* then update internal state
*/
useEffect(() => {
if (typeof dataProp === 'function' && canvasRef.current) {
setComputedData(dataProp(canvasRef.current));
}
}, [dataProp]);

if (!chart.config.data) {
chart.config.data = computedData;
chart.update();
return;
useEffect(() => {
if (!redraw && chartRef.current && options) {
chartRef.current.options = { ...options };
}
}, [redraw, options]);

const { datasets: newDataSets = [], ...newChartData } = computedData;
const { datasets: currentDataSets = [] } = chart.config.data;

// copy values
Object.assign(chart.config.data, newChartData);

chart.config.data.datasets = newDataSets.map((newDataSet: any) => {
// given the new set, find it's current match
const currentDataSet = currentDataSets.find(
d => d.label === newDataSet.label && d.type === newDataSet.type
);
useEffect(() => {
if (!redraw && chartRef.current) {
chartRef.current.config.data.labels = data.labels;
}
}, [redraw, data.labels]);

// There is no original to update, so simply add new one
if (!currentDataSet || !newDataSet.data) return { ...newDataSet };

if (!currentDataSet.data) {
// @ts-expect-error Need to refactor
currentDataSet.data = [];
} else {
// @ts-expect-error Need to refactor
currentDataSet.data.length = newDataSet.data.length;
}

// copy in values
Object.assign(currentDataSet.data, newDataSet.data);

// apply dataset changes, but keep copied data
Object.assign(currentDataSet, {
...newDataSet,
data: currentDataSet.data,
});
return currentDataSet;
});
useEffect(() => {
if (!redraw && chartRef.current && data.datasets) {
setNextDatasets(chartRef.current.config.data, data.datasets);
}
}, [redraw, data.datasets]);

chart.update();
};
useEffect(() => {
if (!chartRef.current) return;

const destroyChart = () => {
if (chart) chart.destroy();
};
if (redraw) {
destroyChart();
setTimeout(renderChart);
} else {
chartRef.current.update();
}
}, [redraw, options, data.labels, data.datasets]);

useEffect(() => {
renderChart();

return () => destroyChart();
}, []);

useEffect(() => {
if (redraw) {
destroyChart();
setTimeout(() => {
renderChart();
}, 0);
} else {
updateChart();
}
});

return (
<canvas
ref={canvas}
ref={canvasRef}
role='img'
height={height}
width={width}
Expand Down
39 changes: 39 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { ForwardedRef } from 'react';
import type {
ChartType,
ChartData,
DefaultDataPoint,
ChartDataset,
} from 'chart.js';

export function reforwardRef<T>(ref: ForwardedRef<T>, value: T) {
if (typeof ref === 'function') {
ref(value);
} else if (ref) {
ref.current = value;
}
}

export function setNextDatasets<
TType extends ChartType = ChartType,
TData = DefaultDataPoint<TType>,
TLabel = unknown
>(
currentData: ChartData<TType, TData, TLabel>,
nextDatasets: ChartDataset<TType, TData>[]
) {
currentData.datasets = nextDatasets.map(nextDataset => {
// given the new set, find it's current match
const currentDataset = currentData.datasets.find(
dataset =>
dataset.label === nextDataset.label && dataset.type === nextDataset.type
);

// There is no original to update, so simply add new one
if (!currentDataset || !nextDataset.data) return nextDataset;

Object.assign(currentDataset, nextDataset);

return currentDataset;
});
}
7 changes: 7 additions & 0 deletions stories/Chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,10 @@ ClickEvents.argTypes = {
onElementClick: { action: 'element clicked' },
onElementsClick: { action: 'elements clicked' },
};

export const Redraw = args => <Chart {...args} />;

Redraw.args = {
data: data.multiTypeData,
redraw: true,
};
20 changes: 19 additions & 1 deletion stories/Doughnut.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import { Doughnut } from '../src';
import { data } from './Doughnut.data';

Expand All @@ -19,3 +19,21 @@ export const Default = args => <Doughnut {...args} />;
Default.args = {
data,
};

export const Rotation = args => {
const [rotation, setRotation] = useState(0);

useEffect(() => {
const interval = setInterval(() => {
setRotation(rotation => rotation + 90);
}, 3000);

return () => clearInterval(interval);
});

return <Doughnut {...args} options={{ rotation }} />;
};

Rotation.args = {
data,
};
9 changes: 9 additions & 0 deletions stories/Pie.data.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import faker from 'faker';

export const data = {
labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
datasets: [
Expand All @@ -24,3 +26,10 @@ export const data = {
},
],
};

export function randomDataset() {
return {
value: faker.datatype.number({ min: -100, max: 100 }),
color: faker.internet.color(),
};
}
38 changes: 36 additions & 2 deletions stories/Pie.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import React, { useState } from 'react';
import { Pie } from '../src';
import { data } from './Pie.data';
import { data, randomDataset } from './Pie.data';

export default {
title: 'Components/Pie',
Expand All @@ -19,3 +19,37 @@ export const Default = args => <Pie {...args} />;
Default.args = {
data,
};

export const Dynamic = args => {
const [datasets, setDatasets] = useState(() => [randomDataset()]);
const onAdd = () => {
setDatasets(datasets => [...datasets, randomDataset()]);
};
const onRemove = () => {
setDatasets(datasets => datasets.slice(0, -1));
};
const data = {
labels: datasets.map((_, i) => `#${i}`),
datasets: [
{
data: datasets.map(({ value }) => value),
backgroundColor: datasets.map(({ color }) => color),
},
],
};

return (
<>
<Pie {...args} data={data} />
<button onClick={onRemove}>Remove</button>
<button onClick={onAdd}>Add</button>
<ul>
{datasets.map(({ value, color }, i) => (
<li key={i} style={{ backgroundColor: color }}>
{value}
</li>
))}
</ul>
</>
);
};

0 comments on commit 330fb1c

Please sign in to comment.