@cdc/chart 4.23.7 → 4.23.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/LICENSE +201 -0
  2. package/dist/cdcchart.js +27964 -26942
  3. package/examples/feature/__data__/area-chart-date-apple.json +5122 -0
  4. package/examples/feature/__data__/city-temperature.json +2198 -0
  5. package/examples/feature/area/area-chart-category.json +45 -45
  6. package/examples/feature/area/area-chart-date-apple.json +10376 -0
  7. package/examples/feature/area/area-chart-date-city-temperature.json +4528 -0
  8. package/examples/feature/area/area-chart-date.json +111 -3
  9. package/examples/feature/forest-plot/broken.json +700 -0
  10. package/examples/feature/forest-plot/data.csv +24 -0
  11. package/examples/feature/forest-plot/forest-plot.json +717 -0
  12. package/examples/feature/pie/planet-pie-example-config.json +1 -1
  13. package/examples/private/confidence_interval_test.json +248 -0
  14. package/examples/private/tooltip-issue.json +45275 -0
  15. package/index.html +13 -11
  16. package/package.json +4 -3
  17. package/src/CdcChart.jsx +24 -14
  18. package/src/components/AreaChart.jsx +84 -59
  19. package/src/components/BarChart.Horizontal.jsx +251 -0
  20. package/src/components/BarChart.StackedHorizontal.jsx +118 -0
  21. package/src/components/BarChart.StackedVertical.jsx +93 -0
  22. package/src/components/BarChart.Vertical.jsx +204 -0
  23. package/src/components/BarChart.jsx +14 -674
  24. package/src/components/BarChartType.jsx +15 -0
  25. package/src/components/BrushHandle.jsx +17 -0
  26. package/src/components/DataTable.jsx +63 -21
  27. package/src/components/EditorPanel.jsx +351 -303
  28. package/src/components/ForestPlot.jsx +191 -0
  29. package/src/components/ForestPlotSettings.jsx +508 -0
  30. package/src/components/LineChart.jsx +2 -2
  31. package/src/components/LinearChart.jsx +115 -310
  32. package/src/data/initial-state.js +43 -0
  33. package/src/hooks/useBarChart.js +186 -0
  34. package/src/hooks/useEditorPermissions.js +218 -0
  35. package/src/hooks/useMinMax.js +15 -3
  36. package/src/hooks/useScales.js +45 -2
  37. package/src/hooks/useTooltip.jsx +407 -0
  38. package/src/scss/main.scss +7 -0
@@ -0,0 +1,407 @@
1
+ import { useContext } from 'react'
2
+ import ConfigContext from '../ConfigContext'
3
+
4
+ // third party
5
+ import { localPoint } from '@visx/event'
6
+ import { bisector } from 'd3-array'
7
+
8
+ import { formatNumber as formatColNumber } from '@cdc/core/helpers/cove/number'
9
+
10
+ export const useTooltip = props => {
11
+ const { tableData: data, config, formatNumber, capitalize, formatDate, parseDate } = useContext(ConfigContext)
12
+ const { xScale, yScale, showTooltip, hideTooltip } = props
13
+ const { xAxis, visualizationType, orientation, yAxis, runtime } = config
14
+
15
+ /**
16
+ * Provides the tooltip information based on the tooltip data array and svg cursor coordinates
17
+ * @function getTooltipInformation
18
+ * @param {Array} tooltipDataArray - The array containing the tooltip data.
19
+ * @param {Object} eventSvgCoords - The object containing the SVG coordinates of the event.
20
+ * @return {Object} - The tooltip information with tooltip data.
21
+ */
22
+ const getTooltipInformation = (tooltipDataArray, eventSvgCoords) => {
23
+ const { x, y } = eventSvgCoords
24
+ let initialTooltipData = tooltipDataArray || {}
25
+
26
+ const tooltipData = {
27
+ data: initialTooltipData,
28
+ dataXPosition: x + 10,
29
+ dataYPosition: y
30
+ }
31
+
32
+ const tooltipInformation = {
33
+ tooltipData: tooltipData
34
+ }
35
+
36
+ return tooltipInformation
37
+ }
38
+
39
+ /**
40
+ * Handles the mouse over event for the tooltip.
41
+ * @function handleTooltipMouseOver
42
+ * @param {Event} e - The event object.
43
+ * @return {void} - The tooltip information is displayed
44
+ */
45
+ const handleTooltipMouseOver = (e, additionalChartData) => {
46
+ e.stopPropagation()
47
+ const eventSvgCoords = localPoint(e)
48
+ const { x, y } = eventSvgCoords
49
+
50
+ // Additional data for pie charts
51
+ const { data: pieChartData, arc } = additionalChartData
52
+
53
+ const closestXScaleValue = getXValueFromCoordinate(x)
54
+
55
+ const includedSeries = visualizationType !== 'Pie' ? config.series.filter(series => series.tooltip === true).map(item => item.dataKey) : config.series.map(item => item.dataKey)
56
+ includedSeries.push(config.xAxis.dataKey)
57
+
58
+ const yScaleValues = getYScaleValues(closestXScaleValue, includedSeries)
59
+
60
+ const xScaleValues = data.filter(d => d[xAxis.dataKey] === getClosestYValue(y))
61
+
62
+ const resolvedScaleValues = orientation === 'vertical' ? yScaleValues : xScaleValues
63
+
64
+ const forestPlotXValue = visualizationType === 'Forest Plot' ? data?.filter(d => d[xAxis.dataKey] === getClosestYValue(y))[0][config.forestPlot.estimateField] : null
65
+
66
+ const getAxisPosition = seriesKey => {
67
+ const seriesObj = config.series.filter(s => s.dataKey === seriesKey)[0]
68
+ const position = seriesObj?.axis ? String(seriesObj.axis).toLowerCase() : 'left'
69
+ return position
70
+ }
71
+
72
+ const getTooltipDataArray = () => {
73
+ if (visualizationType === 'Forest Plot') {
74
+ const columns = config.columns
75
+ const columnsWithTooltips = []
76
+
77
+ for (const [colKeys, colVals] of Object.entries(columns)) {
78
+ const formattingParams = {
79
+ addColPrefix: config.columns[colKeys].prefix,
80
+ addColSuffix: config.columns[colKeys].suffix,
81
+ addColRoundTo: config.columns[colKeys].roundToPlace ? config.columns[colKeys].roundToPlace : '',
82
+ addColCommas: config.columns[colKeys].commas
83
+ }
84
+
85
+ let closestValue = getClosestYValue(y, colVals.name)
86
+
87
+ const formattedValue = formatColNumber(closestValue, 'left', true, config, formattingParams)
88
+
89
+ if (colVals.tooltips) {
90
+ columnsWithTooltips.push([colVals.label, formattedValue])
91
+ }
92
+ }
93
+
94
+ const tooltipItems = []
95
+ tooltipItems.push([config.xAxis.dataKey, getClosestYValue(y)])
96
+
97
+ columnsWithTooltips.forEach(columnData => {
98
+ tooltipItems.push([columnData[0], columnData[1]])
99
+ })
100
+ return tooltipItems
101
+ }
102
+
103
+ if (visualizationType === 'Pie') {
104
+ return [
105
+ [config.xAxis.dataKey, pieChartData],
106
+ [config.runtime.yAxis.dataKey, formatNumber(arc.data[config.runtime.yAxis.dataKey])],
107
+ ['Percent', `${Math.round((((arc.endAngle - arc.startAngle) * 180) / Math.PI / 360) * 100) + '%'}`]
108
+ ]
109
+ }
110
+
111
+ return getIncludedTooltipSeries()
112
+ .filter(Boolean)
113
+ .flatMap(seriesKey => {
114
+ return resolvedScaleValues[0][seriesKey] ? [[seriesKey, resolvedScaleValues[0][seriesKey], getAxisPosition(seriesKey)]] : []
115
+ })
116
+ }
117
+
118
+ // Returns an array of arrays.
119
+ // ie. [ ['Date', '01/01/2023'], ['close', 300] ]
120
+ const tooltipDataArray = getTooltipDataArray()
121
+
122
+ if (!tooltipDataArray) return
123
+ const tooltipInformation = getTooltipInformation(tooltipDataArray, eventSvgCoords)
124
+ showTooltip(tooltipInformation)
125
+ }
126
+
127
+ /**
128
+ * Handles the mouse off event for the tooltip.
129
+ * @function handleTooltipMouseOff
130
+ * @returns {void} - The tooltip information is hidden
131
+ */
132
+ const handleTooltipMouseOff = () => {
133
+ if (config.visualizationType === 'Area Chart') {
134
+ console.log('HERE IN OFF')
135
+ setTimeout(() => {
136
+ hideTooltip()
137
+ }, 3000)
138
+ } else {
139
+ hideTooltip()
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Helper for getting data to the closest date/category hovered.
145
+ * @function getXValueFromCoordinateDate
146
+ * @returns {String} - the closest x value to the cursor position
147
+ */
148
+ const getXValueFromCoordinateDate = x => {
149
+ if (config.xAxis.type === 'categorical' || config.visualizationType === 'Combo') {
150
+ let eachBand = xScale.step()
151
+ let numerator = x
152
+ const index = Math.floor(Number(numerator) / eachBand)
153
+ return xScale.domain()[index - 1] // fixes off by 1 error
154
+ }
155
+
156
+ if (config.xAxis.type === 'date' && config.visualizationType !== 'Combo') {
157
+ const bisectDate = bisector(d => parseDate(d[config.xAxis.dataKey])).left
158
+ const x0 = xScale.invert(xScale(x))
159
+ const index = bisectDate(config.data, x0, 1)
160
+ const val = parseDate(config.data[index - 1][config.xAxis.dataKey])
161
+ return val
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Helper for getting data to the closest date/category hovered.
167
+ * @function getXValueFromCoordinate
168
+ * @returns {String} - the closest x value to the cursor position
169
+ */
170
+ const getXValueFromCoordinate = x => {
171
+ if (visualizationType === 'Pie') return
172
+ if (orientation === 'horizontal') return
173
+ if (xScale.type === 'point' || xAxis.type === 'continuous') {
174
+ // Find the closest x value by calculating the minimum distance
175
+ let closestX = null
176
+ let minDistance = Number.MAX_VALUE
177
+ let offset = x - yAxis.size
178
+
179
+ data.forEach(d => {
180
+ const xPosition = xAxis.type === 'date' ? xScale(parseDate(d[xAxis.dataKey])) : xScale(d[xAxis.dataKey])
181
+ const distance = Math.abs(Number(xPosition - offset))
182
+
183
+ if (distance < minDistance) {
184
+ minDistance = distance
185
+ closestX = xAxis.type === 'date' ? parseDate(d[xAxis.dataKey]) : d[xAxis.dataKey]
186
+ }
187
+ })
188
+ return closestX
189
+ }
190
+
191
+ if (config.xAxis.type === 'categorical' || (visualizationType === 'Combo' && orientation !== 'horizontal' && visualizationType !== 'Forest Plot')) {
192
+ let eachBand = xScale.step()
193
+ let numerator = x
194
+ const index = Math.floor(Number(numerator) / eachBand)
195
+ return xScale.domain()[index - 1] // fixes off by 1 error
196
+ }
197
+
198
+ if (config.xAxis.type === 'date' && visualizationType !== 'Combo' && orientation !== 'horizontal') {
199
+ const bisectDate = bisector(d => parseDate(d[config.xAxis.dataKey])).left
200
+ const x0 = xScale.invert(x)
201
+ const index = bisectDate(config.data, x0, 1)
202
+ const val = parseDate(config.data[index - 1][config.xAxis.dataKey])
203
+ return val
204
+ }
205
+ }
206
+
207
+ const getClosestYValue = (yPosition, key) => {
208
+ if (visualizationType === 'Pie') return
209
+ let minDistance = Number.MAX_VALUE
210
+ let closestYValue = null
211
+
212
+ data.forEach((d, index) => {
213
+ const yPositionOnPlot = visualizationType !== 'Forest Plot' ? yScale(d[config.xAxis.dataKey]) : yScale(index)
214
+
215
+ const distance = Math.abs(yPositionOnPlot - yPosition)
216
+
217
+ if (distance < minDistance) {
218
+ minDistance = distance
219
+ closestYValue = key ? d[key] : d[config.xAxis.dataKey]
220
+ }
221
+ })
222
+ return closestYValue
223
+ }
224
+
225
+ /**
226
+ * handleTooltipClick - used on dashboard filters
227
+ * with visx tooltips, the handler is overwritten and we have to get the closest
228
+ * x axis value.
229
+ *
230
+ * @param {*} e
231
+ * @param {*} data
232
+ */
233
+ const handleTooltipClick = e => {
234
+ try {
235
+ // Get the closest x axis value from the pointer.
236
+ // After getting the closest value, return the data entry with that x scale value.
237
+ // Pass the config.visual uid (not uuid) along with that data entry to setSharedFilters
238
+ const eventSvgCoords = localPoint(e)
239
+ const { x } = eventSvgCoords
240
+ if (!x) throw new Error('COVE: no x value in handleTooltipClick.')
241
+ let closestXScaleValue = getXValueFromCoordinate(x)
242
+ if (!closestXScaleValue) throw new Error('COVE: no closest x scale value in handleTooltipClick')
243
+ let datum = config.data.filter(item => item[config.xAxis.dataKey] === closestXScaleValue)
244
+
245
+ if (setSharedFilter) {
246
+ setSharedFilter(config.uid, datum[0])
247
+ }
248
+ } catch (e) {
249
+ // eslint-disable-next-line no-console
250
+ console.error(e.message)
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Provides an array of objects with the closest y series data items
256
+ * @param {String} closestXScaleValue
257
+ * @param {Array} includedSeries
258
+ * @returns an array of objects with the closest y series data items
259
+ */
260
+ const getYScaleValues = (closestXScaleValue, includedSeries) => {
261
+ try {
262
+ const formattedDate = formatDate(closestXScaleValue)
263
+
264
+ let dataToSearch
265
+
266
+ if (xAxis.type === 'categorical') {
267
+ dataToSearch = data.filter(d => d[xAxis.dataKey] === closestXScaleValue)
268
+ } else {
269
+ dataToSearch = data.filter(d => formatDate(parseDate(d[xAxis.dataKey])) === formattedDate)
270
+ }
271
+
272
+ // Return an empty array if no matching data is found.
273
+ if (!dataToSearch || dataToSearch.length === 0) {
274
+ return []
275
+ }
276
+
277
+ const yScaleValues = dataToSearch.map(object => {
278
+ return Object.fromEntries(Object.entries(object).filter(([key, value]) => includedSeries.includes(key)))
279
+ })
280
+
281
+ return yScaleValues
282
+ } catch (error) {
283
+ console.error('COVE', error)
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Retrieves an array of items to be included in a tooltip.
289
+ *
290
+ * @function getIncludedTooltipSeries
291
+ * @returns {Array} Array of items to be included in the tooltip.
292
+ */
293
+ const getIncludedTooltipSeries = () => {
294
+ try {
295
+ let standardLoopItems
296
+
297
+ let stageColumns = []
298
+ let ciItems = []
299
+
300
+ // loop through series for items to add to tooltip.
301
+ // there is probably a better way of doing this.
302
+ config.series?.forEach(s => {
303
+ if (s.type === 'Forecasting') {
304
+ stageColumns.push(s.stageColumn)
305
+
306
+ s?.confidenceIntervals.forEach(ci => {
307
+ if (ci.showInTooltip === true) {
308
+ ciItems.push(ci.low)
309
+ ciItems.push(ci.high)
310
+ }
311
+ })
312
+ }
313
+ })
314
+
315
+ if (!config.dashboard) {
316
+ switch (visualizationType) {
317
+ case 'Combo':
318
+ standardLoopItems = [runtime.xAxis.dataKey, ...runtime?.barSeriesKeys, ...runtime?.lineSeriesKeys, ...stageColumns, ...ciItems]
319
+ break
320
+ case 'Forecasting':
321
+ standardLoopItems = [runtime.xAxis.dataKey, ...stageColumns, ...ciItems]
322
+ break
323
+ case 'Line':
324
+ standardLoopItems = [runtime.xAxis.dataKey, ...runtime?.seriesKeys]
325
+ break
326
+ case 'Area Chart':
327
+ standardLoopItems = [runtime.xAxis.dataKey, ...runtime?.seriesKeys]
328
+ break
329
+ case 'Bar':
330
+ standardLoopItems = orientation === 'vertical' ? [runtime.xAxis.dataKey, ...runtime?.seriesKeys] : [runtime.yAxis.dataKey, ...runtime?.seriesKeys]
331
+ break
332
+ case 'Pie':
333
+ standardLoopItems = [runtime.xAxis.dataKey, ...runtime?.seriesKeys]
334
+ default:
335
+ throw new Error('No visualization type found in handleTooltipMouseOver')
336
+ break
337
+ }
338
+ }
339
+
340
+ if (config.dashboard) {
341
+ standardLoopItems = [runtime.xAxis.dataKey, ...runtime?.barSeriesKeys, ...runtime?.lineSeriesKeys, ...stageColumns, ...ciItems]
342
+ }
343
+
344
+ return standardLoopItems
345
+ } catch (error) {
346
+ console.error('COVE', error)
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Updates the tooltip style dynamically, primarily opacity and tooltip x/y positions
352
+ * @param {*} tooltipData
353
+ * @returns {Object} - tooltip styles
354
+ */
355
+ const tooltipStyles = tooltipData => {
356
+ const { dataXPosition, dataYPosition } = tooltipData
357
+
358
+ return {
359
+ opacity: config.tooltips.opacity ? config.tooltips.opacity / 100 : 1,
360
+ position: 'absolute',
361
+ backgroundColor: 'white',
362
+ borderRadius: '4px',
363
+ transform: `translate(${dataXPosition}px, ${Number(dataYPosition)}px)`
364
+ }
365
+ }
366
+
367
+ /**
368
+ * find the original series and use the name property if available
369
+ * otherwise default back to the original column name.
370
+ * @param {String} input - original columnName
371
+ * @returns user defined series name.
372
+ */
373
+ const getSeriesNameFromLabel = originalColumnName => {
374
+ let series = config.series.filter(s => s.dataKey === originalColumnName)
375
+ if (series[0]?.name) return series[0]?.name
376
+ return originalColumnName
377
+ }
378
+
379
+ const TooltipListItem = ({ item }) => {
380
+ const [index, additionalData] = item
381
+ const [key, value, axisPosition] = additionalData
382
+
383
+ if (visualizationType === 'Forest Plot') {
384
+ if (key === config.xAxis.dataKey) return <li className='tooltip-heading'>{`${capitalize(config.xAxis.dataKey ? `${config.xAxis.dataKey}: ` : '')} ${config.yAxis.type === 'date' ? formatDate(parseDate(key, false)) : value}`}</li>
385
+ return <li className='tooltip-body'>{`${getSeriesNameFromLabel(key)}: ${formatNumber(value, 'left')}`}</li>
386
+ }
387
+
388
+ // TOOLTIP HEADING
389
+ if (visualizationType === 'Bar' && orientation === 'horizontal' && key === config.xAxis.dataKey) return <li className='tooltip-heading'>{`${capitalize(config.runtime.yAxis.label ? `${config.runtime.yAxis.label}: ` : '')} ${value}`}</li>
390
+ if (key === config.xAxis.dataKey) return <li className='tooltip-heading'>{`${capitalize(config.runtime.xAxis.label ? `${config.runtime.xAxis.label}: ` : '')} ${config.xAxis.type === 'date' ? value : value}`}</li>
391
+
392
+ // TOOLTIP BODY
393
+ return <li className='tooltip-body'>{`${getSeriesNameFromLabel(key)}: ${formatNumber(value, axisPosition)}`}</li>
394
+ }
395
+
396
+ return {
397
+ getIncludedTooltipSeries,
398
+ getXValueFromCoordinate,
399
+ getXValueFromCoordinateDate,
400
+ getYScaleValues,
401
+ handleTooltipClick,
402
+ handleTooltipMouseOff,
403
+ handleTooltipMouseOver,
404
+ TooltipListItem,
405
+ tooltipStyles
406
+ }
407
+ }
@@ -109,6 +109,9 @@
109
109
  .section-subtext {
110
110
  padding: 15px;
111
111
  }
112
+ .subtext--responsive-ticks {
113
+ margin-top: 3em;
114
+ }
112
115
 
113
116
  .legend-container {
114
117
  background: #fff;
@@ -703,3 +706,7 @@
703
706
  transform: translate(-20px, 0);
704
707
  }
705
708
  }
709
+
710
+ .cdc-open-viz-module .debug {
711
+ border: 2px solid red;
712
+ }