diff --git a/src/chart.tsx b/src/chart.tsx index b296cca7b..357890a5e 100644 --- a/src/chart.tsx +++ b/src/chart.tsx @@ -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, @@ -22,7 +20,7 @@ function ChartComponent< width = 300, redraw = false, type, - data, + data: dataProp, options, plugins = [], getDatasetAtEvent, @@ -32,38 +30,40 @@ function ChartComponent< onClick: onClickProp, ...props }: Props, - ref: Ref> + ref: ForwardedRef> ) { - type TypedChartJS = ChartJSOrUndefined; + type TypedChartJS = ChartJS; type TypedChartData = ChartData; - const canvas = useRef(null); + const canvasRef = useRef(null); + const chartRef = useRef(); + /** + * In case `dataProp` is function use internal state + */ + const [computedData, setComputedData] = useState(); + const data: TypedChartData = + computedData || (typeof dataProp === 'function' ? noopData : dataProp); - const computedData = useMemo(() => { - 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(); + chartRef.current = new ChartJS(canvasRef.current, { + type, + data, + options, + plugins, + }); - useImperativeHandle(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) => { @@ -71,6 +71,8 @@ function ChartComponent< onClickProp(event); } + const { current: chart } = chartRef; + if (!chart) return; getDatasetAtEvent && @@ -105,59 +107,44 @@ 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(); @@ -165,20 +152,9 @@ function ChartComponent< return () => destroyChart(); }, []); - useEffect(() => { - if (redraw) { - destroyChart(); - setTimeout(() => { - renderChart(); - }, 0); - } else { - updateChart(); - } - }); - return ( extends CanvasHTMLAttributes { type: TType; + /** + * @todo Remove function variant. + */ data: | ChartData | ((canvas: HTMLCanvasElement) => ChartData); diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 000000000..3b27bc49f --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,39 @@ +import type { ForwardedRef } from 'react'; +import type { + ChartType, + ChartData, + DefaultDataPoint, + ChartDataset, +} from 'chart.js'; + +export function reforwardRef(ref: ForwardedRef, value: T) { + if (typeof ref === 'function') { + ref(value); + } else if (ref) { + ref.current = value; + } +} + +export function setNextDatasets< + TType extends ChartType = ChartType, + TData = DefaultDataPoint, + TLabel = unknown +>( + currentData: ChartData, + nextDatasets: ChartDataset[] +) { + 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; + }); +} diff --git a/stories/Doughnut.tsx b/stories/Doughnut.tsx index c33a7e68c..23f26ad34 100644 --- a/stories/Doughnut.tsx +++ b/stories/Doughnut.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { Doughnut } from '../src'; import { data } from './Doughnut.data'; @@ -19,3 +19,21 @@ export const Default = 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 ; +}; + +Rotation.args = { + data, +}; diff --git a/stories/Pie.data.ts b/stories/Pie.data.ts index 4df6a4699..0dcee5995 100644 --- a/stories/Pie.data.ts +++ b/stories/Pie.data.ts @@ -1,3 +1,5 @@ +import faker from 'faker'; + export const data = { labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], datasets: [ @@ -24,3 +26,10 @@ export const data = { }, ], }; + +export function randomDataset() { + return { + value: faker.datatype.number({ min: -100, max: 100 }), + color: faker.internet.color(), + }; +} diff --git a/stories/Pie.tsx b/stories/Pie.tsx index b372d0a29..cd2a8ab2d 100644 --- a/stories/Pie.tsx +++ b/stories/Pie.tsx @@ -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', @@ -19,3 +19,37 @@ export const Default = args => ; Default.args = { data, }; + +export const Dynamoc = 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 ( + <> + + + +
    + {datasets.map(({ value, color }, i) => ( +
  • + {value} +
  • + ))} +
+ + ); +};