10000 refactor: prevent unnecessary redrawing · mastercodekw/coreui-react@dfccf07 · GitHub
[go: up one dir, main page]

Skip to content

Commit dfccf07

Browse files
committed
refactor: prevent unnecessary redrawing
1 parent 09f959f commit dfccf07

File tree

2 files changed

+208
-178
lines changed

2 files changed

+208
-178
lines changed

packages/coreui-react-chartjs/src/CChart.tsx

Lines changed: 183 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,21 @@ import React, {
55
useEffect,
66
useImperativeHandle,
77
useMemo,
8-
useState,
98
useRef,
109
} from 'react'
1110
import classNames from 'classnames'
1211

13-
import Chart, { ChartData, ChartOptions, ChartType, InteractionItem, Plugin } from 'chart.js/auto'
12+
import { Chart as ChartJS, registerables } from 'chart.js'
13+
import type {
14+
ChartData,
15+
ChartOptions,
16+
ChartType,
17+
ChartTypeRegistry,
18+
InteractionItem,
19+
Plugin,
20+
ScatterDataPoint,
21+
BubbleDataPoint,
22+
} from 'chart.js'
1423
import { customTooltips as cuiCustomTooltips } from '@coreui/chartjs'
1524

1625
import assign from 'lodash/assign'
@@ -92,7 +101,7 @@ export interface CChartProps extends HTMLAttributes<HTMLCanvasElement | HTMLDivE
92101
*
93102
* @type {'line' | 'bar' | 'radar' | 'doughnut' | 'polarArea' | 'bubble' | 'pie' | 'scatter'}
94103
*/
95-
type: ChartType
104+
type?: ChartType
96105
/**
97106
* Width attribute applied to the rendered canvas.
98< B41A /code>107
*
@@ -107,187 +116,208 @@ export interface CChartProps extends HTMLAttributes<HTMLCanvasElement | HTMLDivE
107116
wrapper?: boolean
108117
}
109118

110-
export const CChart = forwardRef<Chart | undefined, CChartProps>((props, ref) => {
111-
const {
112-
className,
113-
customTooltips = true,
114-
data,
115-
id,
116-
fallbackContent,
117-
getDatasetAtEvent,
118-
getElementAtEvent,
119-
getElementsAtEvent,
120-
height = 150,
121-
options,
122-
plugins = [],
123-
redraw = false,
124-
type,
125-
width = 300,
126-
wrapper = true,
127-
...rest
128-
} = props
129-
130-
const canvasRef = useRef<HTMLCanvasElement>(null)
131-
132-
const computedData = useMemo(() => {
133-
if (typeof data === 'function') {
134-
return canvasRef.current ? data(canvasRef.current) : { datasets: [] }
135-
} else return merge({}, data)
136-
}, [data, canvasRef.current])
137-
138-
const computedOptions = useMemo(() => {
139-
return customTooltips
140-
? merge({}, options, {
141-
plugins: {
142-
tooltip: {
143-
enabled: false,
144-
mode: 'index',
145-
position: 'nearest',
146-
external: cuiCustomTooltips,
147-
},
148-
},
149-
})
150-
: options
151-
}, [data, canvasRef.current, options])
119+
export const CChart = forwardRef<ChartJS | undefined, CChartProps>(
120+
(
121+
{
122+
className,
123+
customTooltips = true,
124+
data,
125+
id,
126+
fallbackContent,
127+
getDatasetAtEvent,
128+
getElementAtEvent,
129+
getElementsAtEvent,
130+
height = 150,
131+
options,
132+
plugins = [],
133+
redraw = false,
134+
type = 'bar',
135+
width = 300,
136+
wrapper = true,
137+
...rest
138+
},
139+
ref,
140+
) => {
141+
ChartJS.register(...registerables)
142+
143+
const canvasRef = useRef<HTMLCanvasElement>(null)
144+
const chartRef = useRef<
145+
| ChartJS<
146+
keyof ChartTypeRegistry,
147+
(number | ScatterDataPoint | BubbleDataPoint | null)[],
148+
unknown
149+
>
150+
| undefined
151+
>()
152+
153+
useImperativeHandle<ChartJS | undefined, ChartJS | undefined>(ref, () => chartRef.current, [
154+
chartRef,
155+
])
156+
157+
const computedData = useMemo(() => {
158+
if (typeof data === 'function') {
159+
return canvasRef.current ? data(canvasRef.current) : { datasets: [] }
160+
}
152161

153-
const [chart, setChart] = useState<Chart>()
162+
return merge({}, data)
163+
}, [canvasRef.current, JSON.stringify(data)])
154164

155-
useImperativeHandle<Chart | undefined, Chart | undefined>(ref, () => chart, [chart])
165+
const computedOptions = useMemo(() => {
166+
return customTooltips
167+
? merge({}, options, {
168+
plugins: {
169+
tooltip: {
170+
enabled: false,
171+
mode: 'index',
172+
position: 'nearest',
173+
external: cuiCustomTooltips,
174+
},
175+
},
176+
})
177+
: options
178+
}, [canvasRef.current, JSON.stringify(options)])
156179

157-
const renderChart = () => {
158-
if (!canvasRef.current) return
180+
const renderChart = () => {
181+
if (!canvasRef.current) return
159182

160-
setChart(
161-
new Chart(canvasRef.current, {
183+
chartRef.current = new ChartJS(canvasRef.current, {
162184
type,
163185
data: computedData,
164186
options: computedOptions,
165187
plugins,
166-
}),
167-
)
168-
}
188+
})
189+
}
169190

170-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
171-
const handleOnClick = (e: any) => {
172-
if (!chart) return
191+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
192+
const handleOnClick = (e: any) => {
193+
if (!chartRef.current) return
173194

174-
getDatasetAtEvent &&
175-
getDatasetAtEvent(
176-
chart.getElementsAtEventForMode(e, 'dataset', { intersect: true }, false),
177-
e,
178-
)
179-
getElementAtEvent &&
180-
getElementAtEvent(
181-
chart.getElementsAtEventForMode(e, 'nearest', { intersect: true }, false),
182-
e,
183-
)
184-
getElementsAtEvent &&
185-
getElementsAtEvent(chart.getElementsAtEventForMode(e, 'index', { intersect: true }, false), e)
186-
}
195+
getDatasetAtEvent &&
196+
getDatasetAtEvent(
197+
chartRef.current.getElementsAtEventForMode(e, 'dataset', { intersect: true }, false),
198+
e,
199+
)
200+
getElementAtEvent &&
201+
getElementAtEvent(
202+
chartRef.current.getElementsAtEventForMode(e, 'nearest', { intersect: true }, false),
203+
e,
204+
)
205+
getElementsAtEvent &&
206+
getElementsAtEvent(
207+
chartRef.current.getElementsAtEventForMode(e, 'index', { intersect: true }, false),
208+
e,
209+
)
210+
}
187211

188-
const updateChart = () => {
189-
if (!chart) return
212+
const updateChart = () => {
213+
if (!chartRef.current) return
190214

191-
if (options) {
192-
chart.options = { ...computedOptions }
193-
}
215+
if (options) {
216+
chartRef.current.options = { ...computedOptions }
217+
}
194218

195-
if (!chart.config.data) {
196-
chart.config.data = computedData
197-
chart.update()
198-
return
199-
}
219+
if (!chartRef.current.config.data) {
220+
chartRef.current.config.data = computedData
221+
chartRef.current.update()
222+
return
223+
}
200224

201-
const { datasets: newDataSets = [], ...newChartData } = computedData
202-
const { datasets: currentDataSets = [] } = chart.config.data
225+
const { datasets: newDataSets = [], ...newChartData } = computedData
226+
const { datasets: currentDataSets = [] } = chartRef.current.config.data
203227

204-
// copy values
205-
assign(chart.config.data, newChartData)
206-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
207-
chart.config.data.datasets = newDataSets.map((newDataSet: any) => {
208-
// given the new set, find it's current match
209-
const currentDataSet = find(
210-
currentDataSets,
211-
(d) => d.label === newDataSet.label && d.type === newDataSet.type,
212-
)
228+
// copy values
229+
assign(chartRef.current.config.data, newChartData)
230+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
231+
chartRef.current.config.data.datasets = newDataSets.map((newDataSet: any) => {
232+
// given the new set, find it's current match
233+
const currentDataSet = find(
234+
currentDataSets,
235+
(d) => d.label === newDataSet.label && d.type === newDataSet.type,
236+
)
213237

214-
// There is no original to update, so simply add new one
215-
if (!currentDataSet || !newDataSet.data) return newDataSet
238+
// There is no original to update, so simply add new one
239+
if (!currentDataSet || !newDataSet.data) return newDataSet
216240

217-
if (!currentDataSet.data) {
218-
currentDataSet.data = []
219-
} else {
220-
currentDataSet.data.length = newDataSet.data.length
221-
}
241+
if (!currentDataSet.data) {
242+
currentDataSet.data = []
243+
} else {
244+
currentDataSet.data.length = newDataSet.data.length
245+
}
246+
247+
// copy in values
248+
assign(currentDataSet.data, newDataSet.data)
249+
250+
// apply dataset changes, but keep copied data
251+
return {
252+
...currentDataSet,
253+
...newDataSet,
254+
data: currentDataSet.data,
255+
}
256+
})
222257

223-
// copy in values
224-
assign(currentDataSet.data, newDataSet.data)
258+
chartRef.current.update()
259+
}
225260

226-
// apply dataset changes, but keep copied data
227-
return {
228-
...currentDataSet,
229-
...newDataSet,
230-
data: currentDataSet.data,
261+
const destroyChart = () => {
262+
if (chartRef.current) {
263+
chartRef.current.destroy()
264+
chartRef.current = undefined
231265
}
232-
})
266+
}
233267

234-
chart.update()
235-
}
268+
useEffect(() => {
269+
renderChart()
236270

237-
const destroyChart = () => {
238-
if (chart) chart.destroy()
239-
}
271+
return () => destroyChart()
272+
}, [])
240273

241-
useEffect(() => {
242-
renderChart()
274+
useEffect(() => {
275+
if (!chartRef.current) return
243276

244-
return () => destroyChart()
245-
}, [])
277+
if (redraw) {
278+
destroyChart()
279+
setTimeout(() => {
280+
renderChart()
281+
}, 0)
282+
} else {
283+
updateChart()
284+
}
285+
}, [JSON.stringify(data), computedData])
246286

247-
useEffect(() => {
248-
if (redraw) {
249-
destroyChart()
250-
setTimeout(() => {
251-
renderChart()
252-
}, 0)
253-
} else {
254-
updateChart()
287+
const canvas = (ref: React.Ref<HTMLCanvasElement>) => {
288+
return (
289+
<canvas
290+
{...(!wrapper && className && { className: className })}
291+
data-testid="canvas"
292+
height={height}
293+
id={id}
294+
onClick={(e: React.MouseEvent<HTMLCanvasElement>) => {
295+
handleOnClick(e)
296+
}}
297+
ref={ref}
298+
role="img"
299+
width={width}
300+
{...rest}
301+
>
302+
{fallbackContent}
303+
</canvas>
304+
)
255305
}
256-
}, [props, computedData])
257-
258-
const canvas = (ref: React.Ref<HTMLCanvasElement>) => {
259-
return (
260-
<canvas
261-
{...(!wrapper && className && { className: className })}
262-
data-testid="canvas"
263-
height={height}
264-
id={id}
265-
onClick={(e: React.MouseEvent<HTMLCanvasElement>) => {
266-
handleOnClick(e)
267-
}}
268-
ref={ref}
269-
role="img"
270-
width={width}
271-
{...rest}
272-
>
273-
{fallbackContent}
274-
</canvas>
275-
)
276-
}
277306

278-
return wrapper ? (
279-
<div className={classNames('chart-wrapper', className)} {...rest}>
280-
{canvas(canvasRef)}
281-
</div>
282-
) : (
283-
canvas(canvasRef)
284-
)
285-
})
307+
return wrapper ? (
308+
<div className={classNames('chart-wrapper', className)} {...rest}>
309+
{canvas(canvasRef)}
310+
</div>
311+
) : (
312+
canvas(canvasRef)
313+
)
314+
},
315+
)
286316

287317
CChart.propTypes = {
288318
className: PropTypes.string,
289319
customTooltips: PropTypes.bool,
290-
data: PropTypes.any.isRequired, // TODO: check
320+
data: PropTypes.any.isRequired, // TODO: improve this type
291321
fallbackContent: PropTypes.node,
292322
getDatasetAtEvent: PropTypes.func,
293323
getElementAtEvent: PropTypes.func,

0 commit comments

Comments
 (0)
0