@cdc/chart 4.23.2 → 4.23.3

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 (48) hide show
  1. package/dist/cdcchart.js +41198 -39447
  2. package/examples/area-chart.json +187 -0
  3. package/examples/big-small-test-bar.json +328 -0
  4. package/examples/big-small-test-line.json +328 -0
  5. package/examples/big-small-test-negative.json +328 -0
  6. package/examples/box-plot.json +0 -1
  7. package/examples/example-bar-chart.json +4 -1
  8. package/examples/example-sparkline.json +76 -0
  9. package/examples/gallery/bar-chart-horizontal/horizontal-bar-chart.json +31 -172
  10. package/examples/gallery/bar-chart-vertical/vertical-bar-chart-confidence.json +1 -0
  11. package/examples/gallery/bar-chart-vertical/vertical-bar-chart-with-confidence.json +96 -14
  12. package/examples/gallery/line/line.json +1 -0
  13. package/examples/horizontal-chart-max-increase.json +38 -0
  14. package/examples/line-chart-max-increase.json +32 -0
  15. package/examples/line-chart-nonnumeric.json +5 -5
  16. package/examples/line-chart.json +6 -6
  17. package/examples/planet-deviation-config.json +168 -0
  18. package/examples/planet-deviation-data.json +38 -0
  19. package/examples/planet-example-config.json +139 -20
  20. package/examples/planet-example-data-max-increase.json +56 -0
  21. package/examples/planet-example-data.json +10 -10
  22. package/examples/scatterplot-continuous.csv +3 -3
  23. package/examples/scatterplot.json +2 -2
  24. package/examples/sparkline-chart-nonnumeric.json +3 -3
  25. package/index.html +26 -9
  26. package/package.json +6 -3
  27. package/src/CdcChart.jsx +146 -92
  28. package/src/components/AreaChart.jsx +198 -0
  29. package/src/components/BarChart.jsx +58 -34
  30. package/src/components/BoxPlot.jsx +28 -15
  31. package/src/components/DataTable.jsx +21 -17
  32. package/src/components/DeviationBar.jsx +191 -0
  33. package/src/components/EditorPanel.jsx +473 -168
  34. package/src/components/Filters.jsx +3 -2
  35. package/src/components/Legend.jsx +59 -46
  36. package/src/components/LineChart.jsx +3 -21
  37. package/src/components/LinearChart.jsx +158 -55
  38. package/src/components/PairedBarChart.jsx +0 -1
  39. package/src/components/PieChart.jsx +11 -14
  40. package/src/components/ScatterPlot.jsx +19 -16
  41. package/src/components/SparkLine.jsx +87 -85
  42. package/src/components/useIntersectionObserver.jsx +1 -1
  43. package/src/data/initial-state.js +20 -4
  44. package/src/hooks/useColorPalette.js +58 -48
  45. package/src/hooks/useReduceData.js +3 -4
  46. package/src/index.jsx +1 -1
  47. package/src/scss/editor-panel.scss +5 -0
  48. package/src/test/CdcChart.test.jsx +6 -0
package/src/CdcChart.jsx CHANGED
@@ -12,7 +12,6 @@ import { timeParse, timeFormat } from 'd3-time-format'
12
12
  import { format } from 'd3-format'
13
13
  import Papa from 'papaparse'
14
14
  import parse from 'html-react-parser'
15
- import { Base64 } from 'js-base64'
16
15
  import 'react-tooltip/dist/react-tooltip.css'
17
16
 
18
17
  // Primary Components
@@ -20,7 +19,7 @@ import ConfigContext from './ConfigContext'
20
19
  import PieChart from './components/PieChart'
21
20
  import LinearChart from './components/LinearChart'
22
21
 
23
- import { colorPalettesChart as colorPalettes } from '../../core/data/colorPalettes'
22
+ import { colorPalettesChart as colorPalettes, twoColorPalette } from '@cdc/core/data/colorPalettes'
24
23
 
25
24
  import { publish, subscribe, unsubscribe } from '@cdc/core/helpers/events'
26
25
 
@@ -47,7 +46,6 @@ import './scss/main.scss'
47
46
 
48
47
  export default function CdcChart({ configUrl, config: configObj, isEditor = false, isDashboard = false, setConfig: setParentConfig, setEditing, hostname, link }) {
49
48
  const transform = new DataTransform()
50
-
51
49
  const [loading, setLoading] = useState(true)
52
50
  const [colorScale, setColorScale] = useState(null)
53
51
  const [config, setConfig] = useState({})
@@ -63,15 +61,28 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
63
61
  const [dynamicLegendItems, setDynamicLegendItems] = useState([])
64
62
  const [imageId] = useState(`cove-${Math.random().toString(16).slice(-4)}`)
65
63
 
66
- const legendGlyphSize = 15
67
- const legendGlyphSizeHalf = legendGlyphSize / 2
68
-
69
64
  // Destructure items from config for more readable JSX
70
- const { legend, title, description, visualizationType } = config
71
- const { barBorderClass, lineDatapointClass, contentClasses, innerContainerClasses, sparkLineStyles } = useDataVizClasses(config)
65
+ let { legend, title, description, visualizationType } = config
66
+
67
+ // set defaults on titles if blank AND only in editor
68
+ if (isEditor) {
69
+ if (!title || title === '') title = 'Chart Title'
70
+ }
71
+
72
+ if (config.table && (!config.table?.label || config.table?.label === '')) config.table.label = 'Data Table'
73
+
74
+ const { barBorderClass, lineDatapointClass, contentClasses, sparkLineStyles } = useDataVizClasses(config)
72
75
 
73
76
  const handleChartTabbing = config.showSidebar ? `#legend` : config?.title ? `#dataTableSection__${config.title.replace(/\s/g, '')}` : `#dataTableSection`
74
77
 
78
+ const sortAsc = (a, b) => {
79
+ return a.toString().localeCompare(b.toString(), 'en', { numeric: true })
80
+ }
81
+
82
+ const sortDesc = (a, b) => {
83
+ return b.toString().localeCompare(a.toString(), 'en', { numeric: true })
84
+ }
85
+
75
86
  const handleChartAriaLabels = (state, testing = false) => {
76
87
  if (testing) console.log(`handleChartAriaLabels Testing On:`, state)
77
88
  try {
@@ -88,7 +99,20 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
88
99
 
89
100
  return ariaLabel
90
101
  } catch (e) {
91
- console.error(e.message)
102
+ console.error('COVE: ', e.message) // eslint-disable-line
103
+ }
104
+ }
105
+
106
+ const handleLineType = lineType => {
107
+ switch (lineType) {
108
+ case 'dashed-sm':
109
+ return '5 5'
110
+ case 'dashed-md':
111
+ return '10 5'
112
+ case 'dashed-lg':
113
+ return '15 5'
114
+ default:
115
+ return 0
92
116
  }
93
117
  }
94
118
 
@@ -120,7 +144,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
120
144
  data = await fetch(response.dataUrl + `?v=${cacheBustingString()}`).then(response => response.json())
121
145
  }
122
146
  } catch {
123
- console.error(`Cannot parse URL: ${response.dataUrl}`)
147
+ console.error(`COVE: Cannot parse URL: ${response.dataUrl}`)
124
148
  data = []
125
149
  }
126
150
 
@@ -136,6 +160,9 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
136
160
  }
137
161
 
138
162
  let newConfig = { ...defaults, ...response }
163
+ if (newConfig.visualizationType === 'Box Plot') {
164
+ newConfig.legend.hide = true
165
+ }
139
166
  if (undefined === newConfig.table.show) newConfig.table.show = !isDashboard
140
167
  updateConfig(newConfig, data)
141
168
  }
@@ -188,7 +215,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
188
215
  newConfig.filters.forEach((filter, index) => {
189
216
  let filterValues = []
190
217
 
191
- filterValues = filter.orderedValues || generateValuesForFilter(filter.columnName, newExcludedData)
218
+ filterValues = filter.orderedValues || generateValuesForFilter(filter.columnName, newExcludedData).sort(filter.order === 'desc' ? sortDesc : sortAsc)
192
219
 
193
220
  newConfig.filters[index].values = filterValues
194
221
  // Initial filter should be active
@@ -218,9 +245,8 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
218
245
  }
219
246
 
220
247
  if (newConfig.visualizationType === 'Box Plot' && newConfig.series) {
221
- // stats
222
- let allKeys = data.map(d => d[newConfig.xAxis.dataKey])
223
- let allValues = data.map(d => Number(d[newConfig?.series[0]?.dataKey]))
248
+ let allKeys = newExcludedData ? newExcludedData.map(d => d[newConfig.xAxis.dataKey]) : data.map(d => d[newConfig.xAxis.dataKey])
249
+ let allValues = newExcludedData ? newExcludedData.map(d => Number(d[newConfig?.series[0]?.dataKey])) : data.map(d => Number(d[newConfig?.series[0]?.dataKey]))
224
250
 
225
251
  const uniqueArray = function (arrArg) {
226
252
  return arrArg.filter(function (elem, pos, arr) {
@@ -234,37 +260,54 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
234
260
 
235
261
  // group specific statistics
236
262
  // prevent re-renders
263
+ if (!groups) return
237
264
  groups.forEach((g, index) => {
238
- if (!g) return
239
- // filter data by group
240
- let filteredData = data.filter(item => item[newConfig.xAxis.dataKey] === g)
241
- let filteredDataValues = filteredData.map(item => Number(item[newConfig?.series[0]?.dataKey]))
242
- // let filteredDataValues = filteredData.map(item => Number(item[newConfig.yAxis.dataKey]))
243
- const q1 = d3.quantile(filteredDataValues, parseFloat(newConfig.boxplot.firstQuartilePercentage) / 100)
244
- const q3 = d3.quantile(filteredDataValues, parseFloat(newConfig.boxplot.thirdQuartilePercentage) / 100)
245
- const iqr = q3 - q1
246
- const lowerBounds = q1 - (q3 - q1) * 1.5
247
- const upperBounds = q3 + (q3 - q1) * 1.5
248
- const outliers = filteredDataValues.filter(v => v < lowerBounds || v > upperBounds)
249
- let nonOutliers = filteredDataValues
250
-
251
- nonOutliers = nonOutliers.filter(item => !outliers.includes(item))
252
-
253
- plots.push({
254
- columnCategory: g,
255
- columnMax: Number(q3 + 1.5 * iqr).toFixed(2),
256
- columnThirdQuartile: q3.toFixed(2),
257
- columnMedian: d3.median(filteredDataValues),
258
- columnFirstQuartile: q1.toFixed(2),
259
- columnMin: Number(q1 - 1.5 * iqr).toFixed(2),
260
- columnCount: filteredDataValues.reduce((partialSum, a) => partialSum + a, 0),
261
- columnSd: d3.deviation(filteredDataValues).toFixed(2),
262
- columnMean: d3.mean(filteredDataValues).toFixed(2),
263
- columnIqr: iqr.toFixed(2),
264
- columnOutliers: outliers,
265
- values: filteredDataValues,
266
- nonOutlierValues: nonOutliers
267
- })
265
+ try {
266
+ if (!g) throw new Error('No groups resolved in box plots')
267
+
268
+ // filter data by group
269
+ let filteredData = newExcludedData ? newExcludedData.filter(item => item[newConfig.xAxis.dataKey] === g) : data.filter(item => item[newConfig.xAxis.dataKey] === g)
270
+ let filteredDataValues = filteredData.map(item => Number(item[newConfig?.series[0]?.dataKey]))
271
+ // let filteredDataValues = filteredData.map(item => Number(item[newConfig.yAxis.dataKey]))
272
+
273
+ if (!filteredData) throw new Error('boxplots dont have data yet')
274
+ if (!plots) throw new Error('boxplots dont have plots yet')
275
+ if (newConfig.boxplot.firstQuartilePercentage === '') {
276
+ newConfig.boxplot.firstQuartilePercentage = 0
277
+ }
278
+
279
+ if (newConfig.boxplot.thirdQuartilePercentage === '') {
280
+ newConfig.boxplot.thirdQuartilePercentage = 0
281
+ }
282
+
283
+ const q1 = d3.quantile(filteredDataValues, parseFloat(newConfig.boxplot.firstQuartilePercentage) / 100)
284
+ const q3 = d3.quantile(filteredDataValues, parseFloat(newConfig.boxplot.thirdQuartilePercentage) / 100)
285
+ const iqr = q3 - q1
286
+ const lowerBounds = q1 - (q3 - q1) * 1.5
287
+ const upperBounds = q3 + (q3 - q1) * 1.5
288
+ const outliers = filteredDataValues.filter(v => v < lowerBounds || v > upperBounds)
289
+ let nonOutliers = filteredDataValues
290
+
291
+ nonOutliers = nonOutliers.filter(item => !outliers.includes(item))
292
+
293
+ plots.push({
294
+ columnCategory: g,
295
+ columnMax: Number(q3 + 1.5 * iqr).toFixed(newConfig.dataFormat.roundTo),
296
+ columnThirdQuartile: Number(q3).toFixed(newConfig.dataFormat.roundTo),
297
+ columnMedian: Number(d3.median(filteredDataValues)).toFixed(newConfig.dataFormat.roundTo),
298
+ columnFirstQuartile: q1.toFixed(newConfig.dataFormat.roundTo),
299
+ columnMin: Number(q1 - 1.5 * iqr).toFixed(newConfig.dataFormat.roundTo),
300
+ columnTotal: filteredDataValues.reduce((partialSum, a) => partialSum + a, 0),
301
+ columnSd: Number(d3.deviation(filteredDataValues)).toFixed(newConfig.dataFormat.roundTo),
302
+ columnMean: Number(d3.mean(filteredDataValues)).toFixed(newConfig.dataFormat.roundTo),
303
+ columnIqr: Number(iqr).toFixed(newConfig.dataFormat.roundTo),
304
+ columnOutliers: outliers,
305
+ values: filteredDataValues,
306
+ nonOutlierValues: nonOutliers
307
+ })
308
+ } catch (e) {
309
+ console.error('COVE: ', e.message) // eslint-disable-line
310
+ }
268
311
  })
269
312
 
270
313
  // make deep copy so we can remove some fields for data
@@ -273,6 +316,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
273
316
  tableData.map(table => {
274
317
  delete table.columnIqr
275
318
  delete table.nonOutlierValues
319
+ return null // resolve eslint
276
320
  })
277
321
 
278
322
  // any other data we can add to boxplots
@@ -295,7 +339,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
295
339
  })
296
340
  }
297
341
 
298
- if ((newConfig.visualizationType === 'Bar' && newConfig.orientation === 'horizontal') || newConfig.visualizationType === 'Paired Bar') {
342
+ if (((newConfig.visualizationType === 'Bar' || newConfig.visualizationType === 'Deviation Bar') && newConfig.orientation === 'horizontal') || newConfig.visualizationType === 'Paired Bar') {
299
343
  newConfig.runtime.xAxis = newConfig.yAxis
300
344
  newConfig.runtime.yAxis = newConfig.xAxis
301
345
  newConfig.runtime.horizontal = true
@@ -386,7 +430,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
386
430
  }
387
431
 
388
432
  setContainer(node)
389
- }, [])
433
+ }, []) // eslint-disable-line
390
434
 
391
435
  function isEmpty(obj) {
392
436
  return Object.keys(obj).length === 0
@@ -395,7 +439,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
395
439
  // Load data when component first mounts
396
440
  useEffect(() => {
397
441
  loadConfig()
398
- }, [])
442
+ }, []) // eslint-disable-line
399
443
 
400
444
  /**
401
445
  * When cove has a config and container ref publish the cove_loaded event.
@@ -405,7 +449,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
405
449
  publish('cove_loaded', { config: config })
406
450
  setCoveLoadedEventRan(true)
407
451
  }
408
- }, [container, config])
452
+ }, [container, config]) // eslint-disable-line
409
453
 
410
454
  /**
411
455
  * Handles filter change events outside of COVE
@@ -448,20 +492,22 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
448
492
  setConfig(newConfigHere)
449
493
  setFilteredData(filterData(externalFilters, excludedData))
450
494
  }
451
- }, [externalFilters])
495
+ }, [externalFilters]) // eslint-disable-line
452
496
 
453
497
  // Load data when configObj data changes
454
498
  if (configObj) {
455
499
  // eslint-disable-next-line react-hooks/rules-of-hooks
456
500
  useEffect(() => {
457
501
  loadConfig()
458
- }, [configObj.data])
502
+ }, [configObj.data]) // eslint-disable-line
459
503
  }
460
504
 
461
505
  // Generates color palette to pass to child chart component
462
506
  useEffect(() => {
463
507
  if (stateData && config.xAxis && config.runtime.seriesKeys) {
464
- let palette = config.customColors || colorPalettes[config.palette]
508
+ const configPalette = config.visualizationType === 'Paired Bar' || config.visualizationType === 'Deviation Bar' ? config.twoColor.palette : config.palette
509
+ const allPalettes = { ...colorPalettes, ...twoColorPalette }
510
+ let palette = config.customColors || allPalettes[configPalette]
465
511
  let numberOfKeys = config.runtime.seriesKeys.length
466
512
  let newColorScale
467
513
 
@@ -484,7 +530,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
484
530
  if (config && stateData && config.sortData) {
485
531
  stateData.sort(sortData)
486
532
  }
487
- }, [config, stateData])
533
+ }, [config, stateData]) // eslint-disable-line
488
534
 
489
535
  // Called on legend click, highlights/unhighlights the data series with the given label
490
536
  const highlight = label => {
@@ -543,33 +589,6 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
543
589
  return timeFormat(config.runtime[section].dateDisplayFormat)(date)
544
590
  }
545
591
 
546
- const DownloadButton = ({ data }, type = 'link') => {
547
- const fileName = `${config.title.substring(0, 50)}.csv`
548
-
549
- const csvData = Papa.unparse(data)
550
-
551
- const saveBlob = () => {
552
- if (typeof window.navigator.msSaveBlob === 'function') {
553
- const dataBlob = new Blob([csvData], { type: 'text/csv;charset=utf-8;' })
554
- window.navigator.msSaveBlob(dataBlob, fileName)
555
- }
556
- }
557
-
558
- if (type === 'download') {
559
- return (
560
- <a download={fileName} onClick={saveBlob} href={`data:text/csv;base64,${Base64.encode(csvData)}`} aria-label='Download this data in a CSV file format.' className={`btn btn-download no-border`}>
561
- Download Data (CSV)
562
- </a>
563
- )
564
- } else {
565
- return (
566
- <a download={fileName} onClick={saveBlob} href={`data:text/csv;base64,${Base64.encode(csvData)}`} aria-label='Download this data in a CSV file format.' className={`btn no-border`}>
567
- Download Data (CSV)
568
- </a>
569
- )
570
- }
571
- }
572
-
573
592
  // function calculates the width of given text and its font-size
574
593
  function getTextWidth(text, font) {
575
594
  const canvas = document.createElement('canvas')
@@ -584,11 +603,19 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
584
603
  const formatNumber = (num, axis) => {
585
604
  // if num is NaN return num
586
605
  if (isNaN(num) || !num) return num
606
+ // Check if the input number is negative
607
+ const isNegative = num < 0
608
+
609
+ // If the input number is negative, take the absolute value
610
+ if (isNegative) {
611
+ num = Math.abs(num)
612
+ }
587
613
 
588
614
  // destructure dataFormat values
589
615
  let {
590
- dataFormat: { commas, abbreviated, roundTo, prefix, suffix, rightRoundTo, rightPrefix, rightSuffix }
616
+ dataFormat: { commas, abbreviated, roundTo, prefix, suffix, rightRoundTo, bottomRoundTo, rightPrefix, rightSuffix, bottomPrefix, bottomSuffix, bottomAbbreviated }
591
617
  } = config
618
+
592
619
  let formatSuffix = format('.2s')
593
620
 
594
621
  // check if value contains comma and remove it. later will add comma below.
@@ -596,14 +623,15 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
596
623
 
597
624
  let original = num
598
625
  let stringFormattingOptions
599
-
600
- if (axis !== 'right') {
626
+ if (axis === 'left') {
601
627
  stringFormattingOptions = {
602
628
  useGrouping: config.dataFormat.commas ? true : false,
603
629
  minimumFractionDigits: roundTo ? Number(roundTo) : 0,
604
630
  maximumFractionDigits: roundTo ? Number(roundTo) : 0
605
631
  }
606
- } else {
632
+ }
633
+
634
+ if (axis === 'right') {
607
635
  stringFormattingOptions = {
608
636
  useGrouping: config.dataFormat.rightCommas ? true : false,
609
637
  minimumFractionDigits: rightRoundTo ? Number(rightRoundTo) : 0,
@@ -611,6 +639,14 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
611
639
  }
612
640
  }
613
641
 
642
+ if (axis === 'bottom') {
643
+ stringFormattingOptions = {
644
+ useGrouping: config.dataFormat.bottomCommas ? true : false,
645
+ minimumFractionDigits: bottomRoundTo ? Number(bottomRoundTo) : 0,
646
+ maximumFractionDigits: bottomRoundTo ? Number(bottomRoundTo) : 0
647
+ }
648
+ }
649
+
614
650
  num = numberFromString(num)
615
651
 
616
652
  if (isNaN(num)) {
@@ -631,8 +667,11 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
631
667
  // Use commas also updates bars and the data table
632
668
  // We can't use commas when we're formatting the dataFormatted number
633
669
  // Example: commas -> 12,000; abbreviated -> 12k (correct); abbreviated & commas -> 12 (incorrect)
670
+ //
671
+ // Edge case for small numbers with decimals
672
+ // - if roundTo undefined which means it is blank, then do not round
634
673
  if ((axis === 'left' && commas && abbreviated) || (axis === 'bottom' && commas && abbreviated)) {
635
- num = num
674
+ num = num // eslint-disable-line
636
675
  } else {
637
676
  num = num.toLocaleString('en-US', stringFormattingOptions)
638
677
  }
@@ -642,11 +681,11 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
642
681
  num = formatSuffix(parseFloat(num)).replace('G', 'B')
643
682
  }
644
683
 
645
- if (abbreviated && axis === 'bottom') {
684
+ if (bottomAbbreviated && axis === 'bottom') {
646
685
  num = formatSuffix(parseFloat(num)).replace('G', 'B')
647
686
  }
648
687
 
649
- if (prefix && axis !== 'right') {
688
+ if (prefix && axis === 'left') {
650
689
  result += prefix
651
690
  }
652
691
 
@@ -654,9 +693,13 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
654
693
  result += rightPrefix
655
694
  }
656
695
 
696
+ if (bottomPrefix && axis === 'bottom') {
697
+ result += bottomPrefix
698
+ }
699
+
657
700
  result += num
658
701
 
659
- if (suffix && axis !== 'right') {
702
+ if (suffix && axis === 'left') {
660
703
  result += suffix
661
704
  }
662
705
 
@@ -664,6 +707,13 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
664
707
  result += rightSuffix
665
708
  }
666
709
 
710
+ if (bottomSuffix && axis === 'bottom') {
711
+ result += bottomSuffix
712
+ }
713
+ if (isNegative) {
714
+ result = '-' + result
715
+ }
716
+
667
717
  return String(result)
668
718
  }
669
719
 
@@ -675,7 +725,9 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
675
725
  Combo: <LinearChart />,
676
726
  Pie: <PieChart />,
677
727
  'Box Plot': <LinearChart />,
678
- 'Scatter Plot': <LinearChart />
728
+ 'Area Chart': <LinearChart />,
729
+ 'Scatter Plot': <LinearChart />,
730
+ 'Deviation Bar': <LinearChart />
679
731
  }
680
732
 
681
733
  const missingRequiredSections = () => {
@@ -707,7 +759,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
707
759
  <div className='cdc-chart-inner-container'>
708
760
  {/* Title */}
709
761
 
710
- {title && (
762
+ {title && config.showTitle && (
711
763
  <div role='heading' className={`chart-title ${config.theme} cove-component__header`} aria-level={2}>
712
764
  {config && <sup className='superTitle'>{parse(config.superTitle || '')}</sup>}
713
765
  <div>{parse(title)}</div>
@@ -721,7 +773,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
721
773
  {/* Visualization */}
722
774
  {config?.introText && <section className='introText'>{parse(config.introText)}</section>}
723
775
  <div
724
- style={{ marginBottom: config.legend.position !== 'bottom' && config.orientation === 'horizontal' ? `${config.runtime.xAxis.size}px` : '0px' }}
776
+ style={{ marginBottom: config.legend.position !== 'bottom' && currentViewport !== 'sm' && currentViewport !== 'xs' && config.orientation === 'horizontal' ? `${config.runtime.xAxis.size}px` : '0px' }}
725
777
  className={`chart-container ${config.legend.position === 'bottom' ? 'bottom' : ''}${config.legend.hide ? ' legend-hidden' : ''}${lineDatapointClass}${barBorderClass} ${contentClasses.join(' ')}`}
726
778
  >
727
779
  {/* All charts except sparkline */}
@@ -800,10 +852,12 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
800
852
  dynamicLegendItems,
801
853
  setDynamicLegendItems,
802
854
  filterData,
855
+ imageId,
856
+ handleLineType,
803
857
  isNumber,
804
858
  cleanData,
805
- imageId,
806
- getTextWidth
859
+ getTextWidth,
860
+ twoColorPalette
807
861
  }
808
862
 
809
863
  const classes = ['cdc-open-viz-module', 'type-chart', `${currentViewport}`, `font-${config.fontSize}`, `${config.theme}`]
@@ -0,0 +1,198 @@
1
+ import React, { useContext, useCallback } from 'react'
2
+
3
+ // cdc
4
+ import ConfigContext from '../ConfigContext'
5
+ import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
6
+ import { colorPalettesChart } from '@cdc/core/data/colorPalettes'
7
+
8
+ // visx & d3
9
+ import { AreaClosed, LinePath, Bar } from '@visx/shape'
10
+ import { Group } from '@visx/group'
11
+ import * as allCurves from '@visx/curve'
12
+ import { useTooltip, useTooltipInPortal, defaultStyles } from '@visx/tooltip'
13
+ import { localPoint } from '@visx/event'
14
+ import { bisector } from 'd3-array'
15
+
16
+ const CoveAreaChart = ({ xScale, yScale, yMax, xMax }) => {
17
+ // enable various console logs in the file
18
+ const DEBUG = false
19
+
20
+ // import data from context
21
+ const { transformedData: data, config, handleLineType, parseDate, formatDate, formatNumber, seriesHighlight } = useContext(ConfigContext)
22
+
23
+ // import tooltip helpers
24
+ const { tooltipData, showTooltip } = useTooltip()
25
+
26
+ // used for offset on tooltip hover
27
+ let isEditor = window.location.href.includes('editor=true')
28
+
29
+ // here we're inside of the svg,
30
+ // it appears we need to use TooltipInPortal.
31
+ const { TooltipInPortal } = useTooltipInPortal({
32
+ detectBounds: true,
33
+ // when tooltip containers are scrolled, this will correctly update the Tooltip position
34
+ scroll: true
35
+ })
36
+
37
+ // Draw transparent bars over the chart to get tooltip data
38
+ // Turn DEBUG on for additional context.
39
+ let barThickness = xMax / config.data.length
40
+ let barThicknessAdjusted = barThickness * (config.barThickness || 0.8)
41
+ let offset = (barThickness * (1 - (config.barThickness || 0.8))) / 2
42
+
43
+ // Tooltip helper for getting data to the closest date/category hovered.
44
+ const getXValueFromCoordinate = x => {
45
+ if (config.xAxis.type === 'categorical') {
46
+ let eachBand = xScale.step()
47
+ let numerator = x
48
+ const index = Math.floor(Number(numerator) / eachBand)
49
+ return xScale.domain()[index - 1] // fixes off by 1 error
50
+ }
51
+
52
+ if (config.xAxis.type === 'date') {
53
+ const bisectDate = bisector(d => parseDate(d[config.xAxis.dataKey])).left
54
+ const x0 = xScale.invert(x)
55
+ const index = bisectDate(config.data, x0, 1)
56
+ const val = parseDate(config.data[index - 1][config.xAxis.dataKey])
57
+ return val
58
+ }
59
+ }
60
+
61
+ const handleMouseOver = useCallback(
62
+ (e, data) => {
63
+ // get the svg coordinates of the mouse
64
+ // and get the closest values
65
+ const eventSvgCoords = localPoint(e)
66
+ const { x, y } = eventSvgCoords
67
+
68
+ let closestXScaleValue = getXValueFromCoordinate(x)
69
+ let formattedDate = formatDate(closestXScaleValue)
70
+
71
+ let yScaleValues
72
+ if (config.xAxis.type === 'categorical') {
73
+ yScaleValues = data.filter(d => d[config.xAxis.dataKey] === closestXScaleValue)
74
+ } else {
75
+ yScaleValues = data.filter(d => d[config.xAxis.dataKey] === formattedDate)
76
+ }
77
+
78
+ let seriesToInclude = []
79
+ let yScaleMaxValues = []
80
+ let itemsToLoop = [config.runtime.xAxis.dataKey, ...config.runtime.seriesKeys]
81
+
82
+ itemsToLoop.map(seriesKey => {
83
+ return Object.entries(yScaleValues[0]).forEach(item => item[0] === seriesKey && seriesToInclude.push(item))
84
+ })
85
+
86
+ // filter out the series that aren't added to the map.
87
+ seriesToInclude.map(series => yScaleMaxValues.push(Number(yScaleValues[0][series])))
88
+ seriesToInclude = Object.fromEntries(seriesToInclude)
89
+
90
+ let tooltipData = {}
91
+ tooltipData.data = seriesToInclude
92
+ tooltipData.dataXPosition = isEditor ? 300 + x + 20 : x + 20
93
+ tooltipData.dataYPosition = y - 20
94
+
95
+ let tooltipInformation = {
96
+ tooltipData: tooltipData,
97
+ tooltipTop: 0,
98
+ tooltipValues: yScaleValues,
99
+ tooltipLeft: x
100
+ }
101
+
102
+ showTooltip(tooltipInformation)
103
+ },
104
+ [showTooltip] // eslint-disable-line
105
+ )
106
+
107
+ const TooltipListItem = ({ item }) => {
108
+ const [label, value] = item
109
+ return label === config.xAxis.dataKey ? `${label}: ${value}` : `${label}: ${formatNumber(value, 'left')}`
110
+ }
111
+
112
+ const handleX = d => {
113
+ return config.xAxis.type === 'date' ? xScale(parseDate(d[config.xAxis.dataKey])) : xScale(d[config.xAxis.dataKey])
114
+ }
115
+
116
+ const handleY = (d, index) => {
117
+ return yScale(d[config.series[index].dataKey])
118
+ }
119
+
120
+ return (
121
+ data && (
122
+ <ErrorBoundary component='AreaChart'>
123
+ <Group className='area-chart' key='area-wrapper' left={config.yAxis.size}>
124
+ {config.series.map((s, index) => {
125
+ let seriesColor = colorPalettesChart[config.palette][index]
126
+ let curveType = allCurves[s.lineType]
127
+ let transparentArea = config.legend.behavior === 'highlight' && seriesHighlight.length > 0 && seriesHighlight.indexOf(s.dataKey) === -1
128
+ let displayArea = config.legend.behavior === 'highlight' || seriesHighlight.length === 0 || seriesHighlight.indexOf(s.dataKey) !== -1
129
+
130
+ data.map(d => xScale(parseDate(d[config.xAxis.dataKey])))
131
+
132
+ return (
133
+ <>
134
+ {/* prettier-ignore */}
135
+ {/* this is the line that appears on top of the area chart */}
136
+ <LinePath data={data} x={d => handleX(d)} y={d => yScale(d[config.series[index].dataKey])} stroke={seriesColor} strokeWidth={2} strokeOpacity={1} shapeRendering='geometricPrecision' curve={curveType} strokeDasharray={s.type ? handleLineType(s.type) : 0} />
137
+
138
+ {/* prettier-ignore */}
139
+ {/* filled in sections */}
140
+ <AreaClosed key={'area-chart'} fill={displayArea ? seriesColor : 'transparent'} fillOpacity={transparentArea ? 0.25 : 0.5} data={data} x={d => handleX(d)} y={d => handleY(d, index)} yScale={yScale} curve={curveType} strokeDasharray={s.type ? handleLineType(s.typ) : 0} />
141
+
142
+ <Bar x={d => handleX(d)} y={d => yScale(d[config.series[index].dataKey])} yScale={yScale} width={xMax} height={yMax} fill={DEBUG ? 'red' : 'transparent'} fillOpacity={0.05} style={DEBUG ? { stroke: 'black', strokeWidth: 2 } : {}} onMouseMove={e => handleMouseOver(e, data)} />
143
+
144
+ {/* circles that appear on hover */}
145
+ {tooltipData && (
146
+ <circle
147
+ cx={config.xAxis.type === 'categorical' ? xScale(tooltipData.data[config.xAxis.dataKey]) : xScale(parseDate(tooltipData.data[config.xAxis.dataKey]))}
148
+ cy={yScale(tooltipData.data[s.dataKey])}
149
+ r={4.5}
150
+ opacity={1}
151
+ fillOpacity={1}
152
+ fill={seriesColor}
153
+ style={{ filter: 'unset', opacity: 1 }}
154
+ />
155
+ )}
156
+
157
+ {/* bars to handle tooltips */}
158
+ {DEBUG &&
159
+ config.data.map((item, index) => {
160
+ return (
161
+ <Bar
162
+ className='bar-here'
163
+ x={barThickness * index + offset}
164
+ y={d => yScale(d[config.series[index].dataKey])}
165
+ yScale={yScale}
166
+ width={barThicknessAdjusted}
167
+ height={yMax}
168
+ fill={'transparent'}
169
+ fillOpacity={1}
170
+ style={{ stroke: 'black', strokeWidth: 2 }}
171
+ onMouseMove={e => handleMouseOver(e, data)}
172
+ />
173
+ )
174
+ })}
175
+
176
+ {tooltipData && (
177
+ <TooltipInPortal key={Math.random()} top={tooltipData.dataYPosition} left={tooltipData.dataXPosition} style={defaultStyles}>
178
+ <Group x={config.yAxis.size + 10} y={0}>
179
+ <ul style={{ listStyle: 'none', paddingLeft: 'unset' }}>
180
+ {Object.entries(tooltipData.data).map(item => (
181
+ <li>
182
+ <TooltipListItem item={item} />
183
+ </li>
184
+ ))}
185
+ </ul>
186
+ </Group>
187
+ </TooltipInPortal>
188
+ )}
189
+ </>
190
+ )
191
+ })}
192
+ </Group>
193
+ </ErrorBoundary>
194
+ )
195
+ )
196
+ }
197
+
198
+ export default CoveAreaChart