@@ -5,12 +5,21 @@ import React, {
5
5
useEffect ,
6
6
useImperativeHandle ,
7
7
useMemo ,
8
- useState ,
9
8
useRef ,
10
9
} from 'react'
11
10
import classNames from 'classnames'
12
11
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'
14
23
import { customTooltips as cuiCustomTooltips } from '@coreui/chartjs'
15
24
16
25
import assign from 'lodash/assign'
@@ -92,7 +101,7 @@ export interface CChartProps extends HTMLAttributes<HTMLCanvasElement | HTMLDivE
92
101
*
93
102
* @type {'line' | 'bar' | 'radar' | 'doughnut' | 'polarArea' | 'bubble' | 'pie' | 'scatter' }
94
103
*/
95
- type : ChartType
104
+ type ? : ChartType
96
105
/**
97
106
* Width attribute applied to the rendered canvas.
98<
B41A
/code>
107
*
@@ -107,187 +116,208 @@ export interface CChartProps extends HTMLAttributes<HTMLCanvasElement | HTMLDivE
107
116
wrapper ?: boolean
108
117
}
109
118
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
+ }
152
161
153
- const [ chart , setChart ] = useState < Chart > ( )
162
+ return merge ( { } , data )
163
+ } , [ canvasRef . current , JSON . stringify ( data ) ] )
154
164
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 ) ] )
156
179
157
- const renderChart = ( ) => {
158
- if ( ! canvasRef . current ) return
180
+ const renderChart = ( ) => {
181
+ if ( ! canvasRef . current ) return
159
182
160
- setChart (
161
- new Chart ( canvasRef . current , {
183
+ chartRef . current = new ChartJS ( canvasRef . current , {
162
184
type,
163
185
data : computedData ,
164
186
options : computedOptions ,
165
187
plugins,
166
- } ) ,
167
- )
168
- }
188
+ } )
189
+ }
169
190
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
173
194
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
+ }
187
211
188
- const updateChart = ( ) => {
189
- if ( ! chart ) return
212
+ const updateChart = ( ) => {
213
+ if ( ! chartRef . current ) return
190
214
191
- if ( options ) {
192
- chart . options = { ...computedOptions }
193
- }
215
+ if ( options ) {
216
+ chartRef . current . options = { ...computedOptions }
217
+ }
194
218
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
+ }
200
224
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
203
227
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
+ )
213
237
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
216
240
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
+ } )
222
257
223
- // copy in values
224
- assign ( currentDataSet . data , newDataSet . data )
258
+ chartRef . current . update ( )
259
+ }
225
260
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
231
265
}
232
- } )
266
+ }
233
267
234
- chart . update ( )
235
- }
268
+ useEffect ( ( ) => {
269
+ renderChart ( )
236
270
237
- const destroyChart = ( ) => {
238
- if ( chart ) chart . destroy ( )
239
- }
271
+ return ( ) => destroyChart ( )
272
+ } , [ ] )
240
273
241
- useEffect ( ( ) => {
242
- renderChart ( )
274
+ useEffect ( ( ) => {
275
+ if ( ! chartRef . current ) return
243
276
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 ] )
246
286
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
+ )
255
305
}
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
- }
277
306
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
+ )
286
316
287
317
CChart . propTypes = {
288
318
className : PropTypes . string ,
289
319
customTooltips : PropTypes . bool ,
290
- data : PropTypes . any . isRequired , // TODO: check
320
+ data : PropTypes . any . isRequired , // TODO: improve this type
291
321
fallbackContent : PropTypes . node ,
292
322
getDatasetAtEvent : PropTypes . func ,
293
323
getElementAtEvent : PropTypes . func ,
0 commit comments