@cdc/chart 4.23.3 → 4.23.4

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 (84) hide show
  1. package/dist/cdcchart.js +24397 -24193
  2. package/examples/feature/__data__/area-chart.json +56 -0
  3. package/examples/{planet-example-data.json → feature/__data__/planet-example-data.json} +16 -4
  4. package/examples/{area-chart.json → feature/area/area-chart.json} +70 -13
  5. package/examples/{horizontal-chart-max-increase.json → feature/bar/horizontal-chart-max-increase.json} +10 -4
  6. package/examples/{horizontal-chart.json → feature/bar/horizontal-chart.json} +10 -4
  7. package/examples/{horizontal-stacked-bar-chart.json → feature/bar/horizontal-stacked-bar-chart.json} +7 -3
  8. package/examples/{planet-chart-horizontal-example-config.json → feature/bar/planet-chart-horizontal-example-config.json} +8 -3
  9. package/examples/{planet-example-config.json → feature/bar/planet-example-config.json} +2 -2
  10. package/examples/{box-plot.json → feature/boxplot/boxplot.json} +7 -7
  11. package/examples/feature/boxplot/testing.csv +38 -0
  12. package/examples/feature/combo/combochart-categories_are_numbers .json +18 -0
  13. package/examples/{planet-combo-example-config.json → feature/combo/planet-combo-example-config.json} +1 -1
  14. package/examples/{planet-deviation-config.json → feature/deviation/planet-deviation-config.json} +2 -2
  15. package/examples/{planet-deviation-data.json → feature/deviation/planet-deviation-data.json} +9 -9
  16. package/examples/feature/filters/filter-testing.json +178 -0
  17. package/examples/feature/forecasting/case_date_example.csv +130 -0
  18. package/examples/feature/forecasting/effective_reproduction.json +202 -0
  19. package/examples/feature/forecasting/r_data.csv +130 -0
  20. package/examples/feature/line/line-chart.json +124 -0
  21. package/examples/{paired-bar-example.json → feature/paired-bar/paired-bar-example.json} +10 -4
  22. package/examples/{planet-pie-example-config.json → feature/pie/planet-pie-example-config.json} +2 -2
  23. package/examples/{scatterplot.json → feature/scatterplot/scatterplot.json} +1 -1
  24. package/examples/{case-rate-example-config.json → feature/tests-case-rate/case-rate-example-config.json} +2 -2
  25. package/examples/{covid-confidence-example-config.json → feature/tests-covid/covid-confidence-example-config.json} +8 -3
  26. package/examples/{covid-example-config.json → feature/tests-covid/covid-example-config.json} +7 -3
  27. package/examples/{cutoff-example-config.json → feature/tests-cutoff/cutoff-example-config.json} +7 -3
  28. package/examples/{date-exclusions-config.json → feature/tests-date-exclusions/date-exclusions-config.json} +2 -2
  29. package/examples/{example-bar-chart-nonnumeric.json → feature/tests-non-numerics/example-bar-chart-nonnumeric.json} +1 -1
  30. package/examples/{planet-pie-example-config-nonnumeric.json → feature/tests-non-numerics/planet-pie-example-config-nonnumeric.json} +2 -2
  31. package/examples/{sparkline-chart-nonnumeric.json → feature/tests-non-numerics/sparkline-chart-nonnumeric.json} +1 -1
  32. package/examples/gallery/bar-chart-vertical/combo-line-chart.json +145 -7
  33. package/examples/gallery/paired-bar/paired-bar-chart.json +1 -0
  34. package/index.html +73 -49
  35. package/package.json +2 -2
  36. package/src/CdcChart.jsx +111 -26
  37. package/src/components/AreaChart.jsx +105 -70
  38. package/src/components/BarChart.jsx +45 -28
  39. package/src/components/BoxPlot.jsx +28 -20
  40. package/src/components/DataTable.jsx +7 -6
  41. package/src/components/DeviationBar.jsx +2 -2
  42. package/src/components/EditorPanel.jsx +252 -193
  43. package/src/components/Legend.jsx +1 -1
  44. package/src/components/LineChart.jsx +10 -16
  45. package/src/components/LinearChart.jsx +30 -34
  46. package/src/components/PairedBarChart.jsx +6 -6
  47. package/src/components/PieChart.jsx +2 -4
  48. package/src/components/SparkLine.jsx +6 -42
  49. package/src/data/initial-state.js +7 -3
  50. package/src/index.jsx +2 -1
  51. package/src/scss/editor-panel.scss +15 -0
  52. package/src/scss/main.scss +8 -6
  53. package/examples/box-plot.csv +0 -5
  54. package/examples/dynamic-legends.json +0 -125
  55. package/examples/line-chart.json +0 -34
  56. package/examples/temp-example-config.json +0 -64
  57. package/examples/temp-example-data.json +0 -130
  58. package/src/components/Filters.jsx +0 -126
  59. /package/examples/{age-adjusted-rates.json → feature/__data__/age-adjusted-rates.json} +0 -0
  60. /package/examples/{new-data.csv → feature/__data__/new-data.csv} +0 -0
  61. /package/examples/{planet-example-data-max-increase.json → feature/__data__/planet-example-data-max-increase.json} +0 -0
  62. /package/examples/{Barchart_with_negative.json → feature/bar/Barchart_with_negative.json} +0 -0
  63. /package/examples/{example-bar-chart.json → feature/bar/example-bar-chart.json} +0 -0
  64. /package/examples/{stacked-vertical-bar-example-negative.json → feature/bar/stacked-vertical-bar-example-negative.json} +0 -0
  65. /package/examples/{stacked-vertical-bar-example.json → feature/bar/stacked-vertical-bar-example.json} +0 -0
  66. /package/examples/{box-plot-data.json → feature/boxplot/box-plot-data.json} +0 -0
  67. /package/examples/{newdata.json → feature/boxplot/boxplot-data.json} +0 -0
  68. /package/examples/{line-chart-max-increase.json → feature/line/line-chart-max-increase.json} +0 -0
  69. /package/examples/{paired-bar-data.json → feature/paired-bar/paired-bar-data.json} +0 -0
  70. /package/examples/{paired-bar-formatted.json → feature/paired-bar/paired-bar-formatted.json} +0 -0
  71. /package/examples/{scatterplot-continuous.csv → feature/scatterplot/scatterplot-continuous.csv} +0 -0
  72. /package/examples/{example-sparkline.json → feature/sparkline/example-sparkline.json} +0 -0
  73. /package/examples/{big-small-test-bar.json → feature/tests-big-small/big-small-test-bar.json} +0 -0
  74. /package/examples/{big-small-test-line.json → feature/tests-big-small/big-small-test-line.json} +0 -0
  75. /package/examples/{big-small-test-negative.json → feature/tests-big-small/big-small-test-negative.json} +0 -0
  76. /package/examples/{case-rate-example-data.json → feature/tests-case-rate/case-rate-example-data.json} +0 -0
  77. /package/examples/{covid-example-data-confidence.json → feature/tests-covid/covid-example-data-confidence.json} +0 -0
  78. /package/examples/{covid-example-data.json → feature/tests-covid/covid-example-data.json} +0 -0
  79. /package/examples/{cutoff-example-data.json → feature/tests-cutoff/cutoff-example-data.json} +0 -0
  80. /package/examples/{date-exclusions-data.json → feature/tests-date-exclusions/date-exclusions-data.json} +0 -0
  81. /package/examples/{example-combo-bar-nonnumeric.json → feature/tests-non-numerics/example-combo-bar-nonnumeric.json} +0 -0
  82. /package/examples/{line-chart-nonnumeric.json → feature/tests-non-numerics/line-chart-nonnumeric.json} +0 -0
  83. /package/examples/{planet-example-data-nonnumeric.json → feature/tests-non-numerics/planet-example-data-nonnumeric.json} +0 -0
  84. /package/examples/{stacked-vertical-bar-example-nonnumerics.json → feature/tests-non-numerics/stacked-vertical-bar-example-nonnumerics.json} +0 -0
package/src/CdcChart.jsx CHANGED
@@ -31,7 +31,7 @@ import DataTable from './components/DataTable'
31
31
  import defaults from './data/initial-state'
32
32
  import EditorPanel from './components/EditorPanel'
33
33
  import Loading from '@cdc/core/components/Loading'
34
- import Filters from './components/Filters'
34
+ import Filters from '@cdc/core/components/Filters'
35
35
  import CoveMediaControls from '@cdc/core/components/CoveMediaControls'
36
36
 
37
37
  // Helpers
@@ -40,11 +40,10 @@ import getViewport from '@cdc/core/helpers/getViewport'
40
40
  import { DataTransform } from '@cdc/core/helpers/DataTransform'
41
41
  import cacheBustingString from '@cdc/core/helpers/cacheBustingString'
42
42
  import isNumber from '@cdc/core/helpers/isNumber'
43
- import cleanData from '@cdc/core/helpers/cleanData'
44
43
 
45
44
  import './scss/main.scss'
46
45
 
47
- export default function CdcChart({ configUrl, config: configObj, isEditor = false, isDashboard = false, setConfig: setParentConfig, setEditing, hostname, link }) {
46
+ export default function CdcChart({ configUrl, config: configObj, isEditor = false, isDebug = false, isDashboard = false, setConfig: setParentConfig, setEditing, hostname, link }) {
48
47
  const transform = new DataTransform()
49
48
  const [loading, setLoading] = useState(true)
50
49
  const [colorScale, setColorScale] = useState(null)
@@ -84,7 +83,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
84
83
  }
85
84
 
86
85
  const handleChartAriaLabels = (state, testing = false) => {
87
- if (testing) console.log(`handleChartAriaLabels Testing On:`, state)
86
+ if (testing) console.log(`handleChartAriaLabels Testing On:`, state) // eslint-disable-line
88
87
  try {
89
88
  if (!state.visualizationType) throw Error('handleChartAriaLabels: no visualization type found in state')
90
89
  let ariaLabel = ''
@@ -144,7 +143,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
144
143
  data = await fetch(response.dataUrl + `?v=${cacheBustingString()}`).then(response => response.json())
145
144
  }
146
145
  } catch {
147
- console.error(`COVE: Cannot parse URL: ${response.dataUrl}`)
146
+ console.error(`COVE: Cannot parse URL: ${response.dataUrl}`) // eslint-disable-line
148
147
  data = []
149
148
  }
150
149
 
@@ -164,6 +163,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
164
163
  newConfig.legend.hide = true
165
164
  }
166
165
  if (undefined === newConfig.table.show) newConfig.table.show = !isDashboard
166
+
167
167
  updateConfig(newConfig, data)
168
168
  }
169
169
 
@@ -220,6 +220,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
220
220
  newConfig.filters[index].values = filterValues
221
221
  // Initial filter should be active
222
222
  newConfig.filters[index].active = filterValues[0]
223
+ newConfig.filters[index].filterStyle = newConfig.filters[index].filterStyle ? newConfig.filters[index].filterStyle : 'dropdown'
223
224
  })
224
225
  currentData = filterData(newConfig.filters, newExcludedData)
225
226
  setFilteredData(currentData)
@@ -258,6 +259,37 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
258
259
  let tableData = []
259
260
  const plots = []
260
261
 
262
+ /**
263
+ * Calculates the first quartile (q1) and third quartile (q3) from an array of integers or decimals.
264
+ *
265
+ * @param {Array} arr - The array of integers or decimals.
266
+ * @returns {Object} An object containing the q1 and q3 values.
267
+ */
268
+ const getQuartiles = arr => {
269
+ arr.sort((a, b) => a - b)
270
+
271
+ // Calculate the index of the median value of the array
272
+ const medianIndex = Math.floor(arr.length / 2)
273
+
274
+ // Check if the length of the array is even or odd
275
+ const isEvenLength = arr.length % 2 === 0
276
+
277
+ // Split the array into two subarrays based on the median index
278
+ const q1Array = isEvenLength ? arr.slice(0, medianIndex) : arr.slice(0, medianIndex + 1)
279
+ const q3Array = isEvenLength ? arr.slice(medianIndex) : arr.slice(medianIndex + 1)
280
+
281
+ // Calculate the median of the first subarray to get the q1 value
282
+ const q1Index = Math.floor(q1Array.length / 2)
283
+ const q1 = isEvenLength ? (q1Array[q1Index - 1] + q1Array[q1Index]) / 2 : q1Array[q1Index]
284
+
285
+ // Calculate the median of the second subarray to get the q3 value
286
+ const q3Index = Math.floor(q3Array.length / 2)
287
+ const q3 = isEvenLength ? (q3Array[q3Index - 1] + q3Array[q3Index]) / 2 : q3Array[q3Index]
288
+
289
+ // Return an object containing the q1 and q3 values
290
+ return { q1, q3 }
291
+ }
292
+
261
293
  // group specific statistics
262
294
  // prevent re-renders
263
295
  if (!groups) return
@@ -268,10 +300,16 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
268
300
  // filter data by group
269
301
  let filteredData = newExcludedData ? newExcludedData.filter(item => item[newConfig.xAxis.dataKey] === g) : data.filter(item => item[newConfig.xAxis.dataKey] === g)
270
302
  let filteredDataValues = filteredData.map(item => Number(item[newConfig?.series[0]?.dataKey]))
271
- // let filteredDataValues = filteredData.map(item => Number(item[newConfig.yAxis.dataKey]))
303
+
304
+ // Sort the data for upcoming functions.
305
+ let sortedData = filteredDataValues.sort((a, b) => a - b)
306
+
307
+ // ! - Notice d3.quantile doesn't work here, and we had to take a custom route.
308
+ const quartiles = getQuartiles(sortedData)
272
309
 
273
310
  if (!filteredData) throw new Error('boxplots dont have data yet')
274
311
  if (!plots) throw new Error('boxplots dont have plots yet')
312
+
275
313
  if (newConfig.boxplot.firstQuartilePercentage === '') {
276
314
  newConfig.boxplot.firstQuartilePercentage = 0
277
315
  }
@@ -280,27 +318,30 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
280
318
  newConfig.boxplot.thirdQuartilePercentage = 0
281
319
  }
282
320
 
283
- const q1 = d3.quantile(filteredDataValues, parseFloat(newConfig.boxplot.firstQuartilePercentage) / 100)
284
- const q3 = d3.quantile(filteredDataValues, parseFloat(newConfig.boxplot.thirdQuartilePercentage) / 100)
321
+ const q1 = quartiles.q1
322
+ const q3 = quartiles.q3
285
323
  const iqr = q3 - q1
286
324
  const lowerBounds = q1 - (q3 - q1) * 1.5
287
325
  const upperBounds = q3 + (q3 - q1) * 1.5
288
- const outliers = filteredDataValues.filter(v => v < lowerBounds || v > upperBounds)
326
+
327
+ const outliers = sortedData.filter(v => v < lowerBounds || v > upperBounds)
289
328
  let nonOutliers = filteredDataValues
290
329
 
291
330
  nonOutliers = nonOutliers.filter(item => !outliers.includes(item))
292
331
 
293
332
  plots.push({
294
333
  columnCategory: g,
295
- columnMax: Number(q3 + 1.5 * iqr).toFixed(newConfig.dataFormat.roundTo),
334
+ columnMax: d3.min([d3.max(filteredDataValues), q1 + 1.5 * iqr]),
296
335
  columnThirdQuartile: Number(q3).toFixed(newConfig.dataFormat.roundTo),
297
336
  columnMedian: Number(d3.median(filteredDataValues)).toFixed(newConfig.dataFormat.roundTo),
298
337
  columnFirstQuartile: q1.toFixed(newConfig.dataFormat.roundTo),
299
- columnMin: Number(q1 - 1.5 * iqr).toFixed(newConfig.dataFormat.roundTo),
338
+ columnMin: d3.max([d3.min(filteredDataValues), q1 - 1.5 * iqr]),
300
339
  columnTotal: filteredDataValues.reduce((partialSum, a) => partialSum + a, 0),
301
340
  columnSd: Number(d3.deviation(filteredDataValues)).toFixed(newConfig.dataFormat.roundTo),
302
341
  columnMean: Number(d3.mean(filteredDataValues)).toFixed(newConfig.dataFormat.roundTo),
303
342
  columnIqr: Number(iqr).toFixed(newConfig.dataFormat.roundTo),
343
+ columnLowerBounds: d3.max([d3.min(filteredDataValues), q1 - 1.5 * iqr]),
344
+ columnUpperBounds: d3.min([d3.max(sortedData), q1 + 1.5 * iqr]),
304
345
  columnOutliers: outliers,
305
346
  values: filteredDataValues,
306
347
  nonOutlierValues: nonOutliers
@@ -316,6 +357,8 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
316
357
  tableData.map(table => {
317
358
  delete table.columnIqr
318
359
  delete table.nonOutlierValues
360
+ delete table.columnLowerBounds
361
+ delete table.columnUpperBounds
319
362
  return null // resolve eslint
320
363
  })
321
364
 
@@ -329,7 +372,12 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
329
372
  if (newConfig.visualizationType === 'Combo' && newConfig.series) {
330
373
  newConfig.runtime.barSeriesKeys = []
331
374
  newConfig.runtime.lineSeriesKeys = []
375
+ newConfig.runtime.areaSeriesKeys = []
376
+
332
377
  newConfig.series.forEach(series => {
378
+ if (series.type === 'Area Chart') {
379
+ newConfig.runtime.areaSeriesKeys.push(series)
380
+ }
333
381
  if (series.type === 'Bar') {
334
382
  newConfig.runtime.barSeriesKeys.push(series.dataKey)
335
383
  }
@@ -338,6 +386,15 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
338
386
  }
339
387
  })
340
388
  }
389
+ if (newConfig.visualizationType === 'Area Chart' && newConfig.series) {
390
+ newConfig.runtime.areaSeriesKeys = []
391
+
392
+ newConfig.series.forEach(series => {
393
+ if (series.type === 'Area Chart') {
394
+ newConfig.runtime.areaSeriesKeys.push(series)
395
+ }
396
+ })
397
+ }
341
398
 
342
399
  if (((newConfig.visualizationType === 'Bar' || newConfig.visualizationType === 'Deviation Bar') && newConfig.orientation === 'horizontal') || newConfig.visualizationType === 'Paired Bar') {
343
400
  newConfig.runtime.xAxis = newConfig.yAxis
@@ -370,7 +427,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
370
427
  return filteredData
371
428
  }
372
429
 
373
- // Gets filer values from dataset
430
+ // Gets filter values from dataset
374
431
  const generateValuesForFilter = (columnName, data = this.state.data) => {
375
432
  const values = []
376
433
 
@@ -599,8 +656,26 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
599
656
  return Math.ceil(context.measureText(text).width)
600
657
  }
601
658
 
659
+ const abbreviateNumber = num => {
660
+ let unit = ''
661
+ let absNum = Math.abs(num)
662
+
663
+ if (absNum >= 1e9) {
664
+ unit = 'B'
665
+ num = num / 1e9
666
+ } else if (absNum >= 1e6) {
667
+ unit = 'M'
668
+ num = num / 1e6
669
+ } else if (absNum >= 1e3) {
670
+ unit = 'K'
671
+ num = num / 1e3
672
+ }
673
+
674
+ return num + unit
675
+ }
676
+
602
677
  // Format numeric data based on settings in config
603
- const formatNumber = (num, axis) => {
678
+ const formatNumber = (num, axis, shouldAbbreviate = false) => {
604
679
  // if num is NaN return num
605
680
  if (isNaN(num) || !num) return num
606
681
  // Check if the input number is negative
@@ -616,8 +691,6 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
616
691
  dataFormat: { commas, abbreviated, roundTo, prefix, suffix, rightRoundTo, bottomRoundTo, rightPrefix, rightSuffix, bottomPrefix, bottomSuffix, bottomAbbreviated }
617
692
  } = config
618
693
 
619
- let formatSuffix = format('.2s')
620
-
621
694
  // check if value contains comma and remove it. later will add comma below.
622
695
  if (String(num).indexOf(',') !== -1) num = num.replaceAll(',', '')
623
696
 
@@ -670,19 +743,20 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
670
743
  //
671
744
  // Edge case for small numbers with decimals
672
745
  // - if roundTo undefined which means it is blank, then do not round
673
- if ((axis === 'left' && commas && abbreviated) || (axis === 'bottom' && commas && abbreviated)) {
746
+
747
+ if ((axis === 'left' && commas && abbreviated && shouldAbbreviate) || (axis === 'bottom' && commas && abbreviated && shouldAbbreviate)) {
674
748
  num = num // eslint-disable-line
675
749
  } else {
676
750
  num = num.toLocaleString('en-US', stringFormattingOptions)
677
751
  }
678
752
  let result = ''
679
753
 
680
- if (abbreviated && axis === 'left') {
681
- num = formatSuffix(parseFloat(num)).replace('G', 'B')
754
+ if (abbreviated && axis === 'left' && shouldAbbreviate) {
755
+ num = abbreviateNumber(parseFloat(num))
682
756
  }
683
757
 
684
- if (bottomAbbreviated && axis === 'bottom') {
685
- num = formatSuffix(parseFloat(num)).replace('G', 'B')
758
+ if (bottomAbbreviated && axis === 'bottom' && shouldAbbreviate) {
759
+ num = abbreviateNumber(parseFloat(num))
686
760
  }
687
761
 
688
762
  if (prefix && axis === 'left') {
@@ -748,10 +822,19 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
748
822
  return false
749
823
  }
750
824
 
825
+ const clean = data => {
826
+ return config?.xAxis?.dataKey ? transform.cleanData(data, config.xAxis.dataKey) : data
827
+ }
828
+
751
829
  // Prevent render if loading
752
830
  let body = <Loading />
753
831
 
754
832
  if (!loading) {
833
+ const tableLink = (
834
+ <a href={`#data-table-${config.dataKey}`} className='margin-left-href'>
835
+ {config.dataKey} (Go to Table)
836
+ </a>
837
+ )
755
838
  body = (
756
839
  <>
757
840
  {isEditor && <EditorPanel />}
@@ -769,11 +852,11 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
769
852
  Skip Over Chart Container
770
853
  </a>
771
854
  {/* Filters */}
772
- {config.filters && !externalFilters && <Filters />}
855
+ {config.filters && !externalFilters && <Filters config={config} setConfig={setConfig} setFilteredData={setFilteredData} filteredData={filteredData} excludedData={excludedData} filterData={filterData} isNumber={isNumber} dimensions={dimensions} />}
773
856
  {/* Visualization */}
774
857
  {config?.introText && <section className='introText'>{parse(config.introText)}</section>}
775
858
  <div
776
- style={{ marginBottom: config.legend.position !== 'bottom' && currentViewport !== 'sm' && currentViewport !== 'xs' && config.orientation === 'horizontal' ? `${config.runtime.xAxis.size}px` : '0px' }}
859
+ style={{ marginBottom: config.legend.position !== 'bottom' && config.orientation === 'horizontal' ? `${config.runtime.xAxis.size}px` : '0px' }}
777
860
  className={`chart-container ${config.legend.position === 'bottom' ? 'bottom' : ''}${config.legend.hide ? ' legend-hidden' : ''}${lineDatapointClass}${barBorderClass} ${contentClasses.join(' ')}`}
778
861
  >
779
862
  {/* All charts except sparkline */}
@@ -797,7 +880,8 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
797
880
  {!config.legend.hide && config.visualizationType !== 'Spark Line' && <Legend />}
798
881
  </div>
799
882
  {/* Link */}
800
- {link && link}
883
+ {isDashboard && config.table && config.table.show && config.table.showDataTableLink ? tableLink : link && link}
884
+
801
885
  {/* Description */}
802
886
  {description && config.visualizationType !== 'Spark Line' && <div className='subtext'>{parse(description)}</div>}
803
887
 
@@ -827,7 +911,8 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
827
911
  setConfig,
828
912
  rawData: stateData ?? {},
829
913
  excludedData: excludedData,
830
- transformedData: filteredData || excludedData,
914
+ transformedData: clean(filteredData || excludedData), // do this right before passing to components
915
+ tableData: filteredData || excludedData, // do not clean table data
831
916
  unfilteredData: stateData,
832
917
  seriesHighlight,
833
918
  colorScale,
@@ -855,9 +940,9 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
855
940
  imageId,
856
941
  handleLineType,
857
942
  isNumber,
858
- cleanData,
859
943
  getTextWidth,
860
- twoColorPalette
944
+ twoColorPalette,
945
+ isDebug
861
946
  }
862
947
 
863
948
  const classes = ['cdc-open-viz-module', 'type-chart', `${currentViewport}`, `font-${config.fontSize}`, `${config.theme}`]
@@ -1,4 +1,4 @@
1
- import React, { useContext, useCallback } from 'react'
1
+ import React, { useContext, useEffect, useState } from 'react'
2
2
 
3
3
  // cdc
4
4
  import ConfigContext from '../ConfigContext'
@@ -6,26 +6,29 @@ import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
6
6
  import { colorPalettesChart } from '@cdc/core/data/colorPalettes'
7
7
 
8
8
  // visx & d3
9
+ import * as allCurves from '@visx/curve'
9
10
  import { AreaClosed, LinePath, Bar } from '@visx/shape'
10
11
  import { Group } from '@visx/group'
11
- import * as allCurves from '@visx/curve'
12
- import { useTooltip, useTooltipInPortal, defaultStyles } from '@visx/tooltip'
12
+ import { useTooltip, useTooltipInPortal, defaultStyles, Tooltip } from '@visx/tooltip'
13
13
  import { localPoint } from '@visx/event'
14
14
  import { bisector } from 'd3-array'
15
15
 
16
- const CoveAreaChart = ({ xScale, yScale, yMax, xMax }) => {
16
+ const CoveAreaChart = ({ xScale, yScale, yMax, xMax, chartRef }) => {
17
17
  // enable various console logs in the file
18
18
  const DEBUG = false
19
+ const [chartPosition, setChartPosition] = useState(null)
20
+
21
+ useEffect(() => {
22
+ setChartPosition(chartRef.current.getBoundingClientRect())
23
+ }, [chartRef])
19
24
 
20
25
  // import data from context
21
- const { transformedData: data, config, handleLineType, parseDate, formatDate, formatNumber, seriesHighlight } = useContext(ConfigContext)
26
+ const { transformedData: data, config, handleLineType, parseDate, formatDate, formatNumber, seriesHighlight, colorScale } = useContext(ConfigContext)
27
+ const tooltip_id = `cdc-open-viz-tooltip-${config.runtime.uniqueId}`
22
28
 
23
29
  // import tooltip helpers
24
30
  const { tooltipData, showTooltip } = useTooltip()
25
31
 
26
- // used for offset on tooltip hover
27
- let isEditor = window.location.href.includes('editor=true')
28
-
29
32
  // here we're inside of the svg,
30
33
  // it appears we need to use TooltipInPortal.
31
34
  const { TooltipInPortal } = useTooltipInPortal({
@@ -36,7 +39,8 @@ const CoveAreaChart = ({ xScale, yScale, yMax, xMax }) => {
36
39
 
37
40
  // Draw transparent bars over the chart to get tooltip data
38
41
  // Turn DEBUG on for additional context.
39
- let barThickness = xMax / config.data.length
42
+ if (!data) return
43
+ let barThickness = xMax / data
40
44
  let barThicknessAdjusted = barThickness * (config.barThickness || 0.8)
41
45
  let offset = (barThickness * (1 - (config.barThickness || 0.8))) / 2
42
46
 
@@ -51,6 +55,8 @@ const CoveAreaChart = ({ xScale, yScale, yMax, xMax }) => {
51
55
 
52
56
  if (config.xAxis.type === 'date') {
53
57
  const bisectDate = bisector(d => parseDate(d[config.xAxis.dataKey])).left
58
+ if (!x) return
59
+ if (!xScale) return
54
60
  const x0 = xScale.invert(x)
55
61
  const index = bisectDate(config.data, x0, 1)
56
62
  const val = parseDate(config.data[index - 1][config.xAxis.dataKey])
@@ -58,51 +64,55 @@ const CoveAreaChart = ({ xScale, yScale, yMax, xMax }) => {
58
64
  }
59
65
  }
60
66
 
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]
67
+ const handleMouseOver = (e, data) => {
68
+ // get the svg coordinates of the mouse
69
+ // and get the closest values
70
+ const eventSvgCoords = localPoint(e)
71
+ const { x, y } = eventSvgCoords
81
72
 
82
- itemsToLoop.map(seriesKey => {
83
- return Object.entries(yScaleValues[0]).forEach(item => item[0] === seriesKey && seriesToInclude.push(item))
84
- })
73
+ let closestXScaleValue = getXValueFromCoordinate(x)
74
+ let formattedDate = formatDate(closestXScaleValue)
85
75
 
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
76
+ let yScaleValues
77
+ if (config.xAxis.type === 'categorical') {
78
+ yScaleValues = data.filter(d => d[config.xAxis.dataKey] === closestXScaleValue)
79
+ } else {
80
+ yScaleValues = data.filter(d => formatDate(parseDate(d[config.xAxis.dataKey])) === formattedDate)
81
+ }
94
82
 
95
- let tooltipInformation = {
96
- tooltipData: tooltipData,
97
- tooltipTop: 0,
98
- tooltipValues: yScaleValues,
99
- tooltipLeft: x
83
+ let seriesToInclude = []
84
+ let yScaleMaxValues = []
85
+ let itemsToLoop = [config.runtime.xAxis.dataKey, ...config.runtime.seriesKeys]
86
+
87
+ itemsToLoop.map(seriesKey => {
88
+ if (!seriesKey) return
89
+ if (!yScaleValues[0]) return
90
+ for (const item of Object.entries(yScaleValues[0])) {
91
+ if (item[0] === seriesKey) {
92
+ seriesToInclude.push(item)
93
+ }
100
94
  }
95
+ })
96
+
97
+ // filter out the series that aren't added to the map.
98
+ seriesToInclude.map(series => yScaleMaxValues.push(Number(yScaleValues[0][series])))
99
+ if (!seriesToInclude) return
100
+ let tooltipDataFromSeries = Object.fromEntries(seriesToInclude) ? Object.fromEntries(seriesToInclude) : {}
101
+
102
+ let tooltipData = {}
103
+ tooltipData.data = tooltipDataFromSeries
104
+ tooltipData.dataXPosition = x + 20
105
+ tooltipData.dataYPosition = y - 100
106
+
107
+ let tooltipInformation = {
108
+ tooltipData: tooltipData,
109
+ tooltipTop: 0,
110
+ tooltipValues: yScaleValues,
111
+ tooltipLeft: x
112
+ }
101
113
 
102
- showTooltip(tooltipInformation)
103
- },
104
- [showTooltip] // eslint-disable-line
105
- )
114
+ showTooltip(tooltipInformation)
115
+ }
106
116
 
107
117
  const TooltipListItem = ({ item }) => {
108
118
  const [label, value] = item
@@ -120,8 +130,8 @@ const CoveAreaChart = ({ xScale, yScale, yMax, xMax }) => {
120
130
  return (
121
131
  data && (
122
132
  <ErrorBoundary component='AreaChart'>
123
- <Group className='area-chart' key='area-wrapper' left={config.yAxis.size}>
124
- {config.series.map((s, index) => {
133
+ <Group className='area-chart' key='area-wrapper' left={Number(config.yAxis.size)}>
134
+ {(config.runtime.areaSeriesKeys || config.runtime.seriesKeys).map((s, index) => {
125
135
  let seriesColor = colorPalettesChart[config.palette][index]
126
136
  let curveType = allCurves[s.lineType]
127
137
  let transparentArea = config.legend.behavior === 'highlight' && seriesHighlight.length > 0 && seriesHighlight.indexOf(s.dataKey) === -1
@@ -130,16 +140,42 @@ const CoveAreaChart = ({ xScale, yScale, yMax, xMax }) => {
130
140
  data.map(d => xScale(parseDate(d[config.xAxis.dataKey])))
131
141
 
132
142
  return (
133
- <>
143
+ <React.Fragment key={index}>
134
144
  {/* 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} />
145
+ <LinePath
146
+ data={data}
147
+ x={d => handleX(d)}
148
+ y={d => yScale(d[config.series[index].dataKey])}
149
+ stroke={displayArea ? seriesColor : 'transparent'}
150
+ strokeWidth={2}
151
+ strokeOpacity={1}
152
+ shapeRendering='geometricPrecision'
153
+ curve={curveType}
154
+ strokeDasharray={s.type ? handleLineType(s.type) : 0}
155
+ />
137
156
 
138
157
  {/* 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} />
158
+ <AreaClosed
159
+ key={'area-chart'}
160
+ fill={ displayArea ? colorScale ? colorScale(config.runtime.seriesLabels ? config.runtime.seriesLabels[s.dataKey] : s.dataKey) : '#000' : 'transparent'}
161
+ fillOpacity={transparentArea ? 0.25 : 0.5}
162
+ data={data} x={d => handleX(d)}
163
+ y={d => handleY(d, index)}
164
+ yScale={yScale}
165
+ curve={curveType}
166
+ strokeDasharray={s.type ? handleLineType(s.typ) : 0}
167
+ />
141
168
 
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)} />
169
+ {/* Transparent bar for tooltips */}
170
+ {/* prettier-ignore */}
171
+ <Bar
172
+ width={ Number(xMax)}
173
+ height={ Number(yMax)}
174
+ fill={DEBUG ? 'red' : 'transparent'}
175
+ fillOpacity={0.05}
176
+ style={DEBUG ? { stroke: 'black', strokeWidth: 2 } : {}}
177
+ onMouseMove={e => handleMouseOver(e, data)}
178
+ />
143
179
 
144
180
  {/* circles that appear on hover */}
145
181
  {tooltipData && (
@@ -149,19 +185,19 @@ const CoveAreaChart = ({ xScale, yScale, yMax, xMax }) => {
149
185
  r={4.5}
150
186
  opacity={1}
151
187
  fillOpacity={1}
152
- fill={seriesColor}
188
+ fill={displayArea ? (colorScale ? colorScale(config.runtime.seriesLabels ? config.runtime.seriesLabels[s.dataKey] : s.dataKey) : '#000') : 'transparent'}
153
189
  style={{ filter: 'unset', opacity: 1 }}
154
190
  />
155
191
  )}
156
192
 
157
- {/* bars to handle tooltips */}
193
+ {/* another tool for showing bars during debug mode. */}
158
194
  {DEBUG &&
159
- config.data.map((item, index) => {
195
+ data.map((item, index) => {
160
196
  return (
161
197
  <Bar
162
198
  className='bar-here'
163
- x={barThickness * index + offset}
164
- y={d => yScale(d[config.series[index].dataKey])}
199
+ x={Number(barThickness * index + offset)}
200
+ y={d => Number(yScale(d[config.series[index].dataKey]))}
165
201
  yScale={yScale}
166
202
  width={barThicknessAdjusted}
167
203
  height={yMax}
@@ -174,19 +210,18 @@ const CoveAreaChart = ({ xScale, yScale, yMax, xMax }) => {
174
210
  })}
175
211
 
176
212
  {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>
213
+ <TooltipInPortal key={Math.random()} top={tooltipData.dataYPosition + chartPosition?.top} left={tooltipData.dataXPosition + chartPosition?.left} style={defaultStyles}>
214
+ <ul style={{ listStyle: 'none', paddingLeft: 'unset', fontFamily: 'sans-serif', margin: 'auto', lineHeight: '1rem' }} data-tooltip-id={tooltip_id}>
215
+ {typeof tooltipData === 'object' &&
216
+ Object.entries(tooltipData.data).map(item => (
217
+ <li style={{ padding: '2.5px 0' }}>
182
218
  <TooltipListItem item={item} />
183
219
  </li>
184
220
  ))}
185
- </ul>
186
- </Group>
221
+ </ul>
187
222
  </TooltipInPortal>
188
223
  )}
189
- </>
224
+ </React.Fragment>
190
225
  )
191
226
  })}
192
227
  </Group>