@@ -5,12 +5,21 @@ import React, {
55 useEffect ,
66 useImperativeHandle ,
77 useMemo ,
8- useState ,
98 useRef ,
109} from 'react'
1110import 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'
1423import { customTooltips as cuiCustomTooltips } from '@coreui/chartjs'
1524
1625import 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.
98107 *
@@ -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
287317CChart . 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