@cdc/chart 4.23.8 → 4.23.10-alpha

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 (42) hide show
  1. package/dist/cdcchart.js +43990 -44283
  2. package/examples/feature/__data__/area-chart-date-apple.json +1 -5073
  3. package/examples/feature/area/area-chart-date-apple.json +73 -10316
  4. package/examples/feature/area/area-chart-date-city-temperature.json +204 -80
  5. package/examples/{private/confidence_interval_test.json → feature/area/area-chart-stacked.json} +65 -74
  6. package/examples/feature/bar/lollipop.json +156 -0
  7. package/examples/feature/combo/planet-combo-example-config.json +99 -9
  8. package/examples/feature/filters/bar-filter.json +5027 -0
  9. package/examples/feature/legend-highlights/highlights.json +567 -0
  10. package/examples/private/TESTING.json +0 -0
  11. package/examples/private/forest-plot.json +356 -0
  12. package/examples/private/{tooltip-issue.json → full.json} +25288 -25239
  13. package/examples/private/missing-color.json +333 -0
  14. package/index.html +30 -8
  15. package/package.json +3 -2
  16. package/src/{CdcChart.jsx → CdcChart.tsx} +81 -74
  17. package/src/_stories/Chart.stories.tsx +188 -0
  18. package/src/components/AreaChart.Stacked.jsx +73 -0
  19. package/src/components/AreaChart.jsx +24 -26
  20. package/src/components/BarChart.StackedVertical.jsx +2 -0
  21. package/src/components/DeviationBar.jsx +67 -13
  22. package/src/components/EditorPanel.jsx +493 -454
  23. package/src/components/Forecasting.jsx +5 -5
  24. package/src/components/ForestPlotSettings.jsx +5 -6
  25. package/src/components/Legend.jsx +18 -9
  26. package/src/components/LineChart.Circle.tsx +102 -0
  27. package/src/components/{LineChart.jsx → LineChart.tsx} +9 -48
  28. package/src/components/LinearChart.jsx +460 -443
  29. package/src/components/PieChart.jsx +54 -25
  30. package/src/components/Series.jsx +63 -17
  31. package/src/components/SparkLine.jsx +7 -19
  32. package/src/data/initial-state.js +10 -1
  33. package/src/hooks/useBarChart.js +1 -1
  34. package/src/hooks/useEditorPermissions.js +87 -24
  35. package/src/hooks/useLegendClasses.js +14 -11
  36. package/src/hooks/useReduceData.js +6 -1
  37. package/src/hooks/useScales.js +4 -4
  38. package/src/hooks/useTooltip.jsx +21 -8
  39. package/src/scss/legend.scss +206 -0
  40. package/src/scss/main.scss +25 -24
  41. package/src/components/DataTable.jsx +0 -374
  42. /package/src/{components → hooks}/useIntersectionObserver.jsx +0 -0
@@ -41,8 +41,8 @@ import coveUpdateWorker from '@cdc/core/helpers/coveUpdateWorker'
41
41
 
42
42
  import './scss/main.scss'
43
43
  // load both then config below determines which to use
44
- import DataTable_horiz from './components/DataTable'
45
- import DataTable_vert from '@cdc/core/components/DataTable'
44
+ import DataTable from '@cdc/core/components/DataTable'
45
+ import { getFileExtension } from '@cdc/core/helpers/getFileExtension'
46
46
 
47
47
  const generateColorsArray = (color = '#000000', special = false) => {
48
48
  let colorObj = chroma(color)
@@ -78,17 +78,17 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
78
78
  const transform = new DataTransform()
79
79
  const [loading, setLoading] = useState(true)
80
80
  const [colorScale, setColorScale] = useState(null)
81
- const [config, setConfig] = useState({})
81
+ const [config, setConfig] = useState<any>({})
82
82
  const [stateData, setStateData] = useState(config.data || [])
83
- const [excludedData, setExcludedData] = useState()
84
- const [filteredData, setFilteredData] = useState()
85
- const [seriesHighlight, setSeriesHighlight] = useState([])
83
+ const [excludedData, setExcludedData] = useState<Record<string, number>[] | undefined>(undefined)
84
+ const [filteredData, setFilteredData] = useState<Record<string, any>[] | undefined>(undefined)
85
+ const [seriesHighlight, setSeriesHighlight] = useState<any[]>([])
86
86
  const [currentViewport, setCurrentViewport] = useState('lg')
87
- const [dimensions, setDimensions] = useState([])
88
- const [externalFilters, setExternalFilters] = useState(null)
87
+ const [dimensions, setDimensions] = useState<[number?, number?]>([])
88
+ const [externalFilters, setExternalFilters] = useState<any[]>()
89
89
  const [container, setContainer] = useState()
90
90
  const [coveLoadedEventRan, setCoveLoadedEventRan] = useState(false)
91
- const [dynamicLegendItems, setDynamicLegendItems] = useState([])
91
+ const [dynamicLegendItems, setDynamicLegendItems] = useState<any[]>([])
92
92
  const [imageId] = useState(`cove-${Math.random().toString(16).slice(-4)}`)
93
93
 
94
94
  let legendMemo = useRef(new Map()) // map collection
@@ -96,8 +96,6 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
96
96
 
97
97
  if (isDebug) console.log('Chart config, isEditor', config, isEditor)
98
98
 
99
- const DataTable = config?.table?.showVertical ? DataTable_vert : DataTable_horiz
100
-
101
99
  // Destructure items from config for more readable JSX
102
100
  let { legend, title, description, visualizationType } = config
103
101
 
@@ -142,7 +140,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
142
140
 
143
141
  const reloadURLData = async () => {
144
142
  if (config.dataUrl) {
145
- const dataUrl = new URL(config.runtimeDataUrl || config.dataUrl)
143
+ const dataUrl = new URL(config.runtimeDataUrl || config.dataUrl, window.location.origin)
146
144
  let qsParams = Object.fromEntries(new URLSearchParams(dataUrl.search))
147
145
 
148
146
  let isUpdateNeeded = false
@@ -164,12 +162,10 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
164
162
  })
165
163
  .join('')}`
166
164
 
167
- let data
165
+ let data: any[] = []
168
166
 
169
167
  try {
170
- const regex = /(?:\.([^.]+))?$/
171
-
172
- const ext = regex.exec(dataUrl.pathname)[1]
168
+ const ext = getFileExtension(dataUrl.pathname)
173
169
  if ('csv' === ext) {
174
170
  data = await fetch(dataUrlFinal)
175
171
  .then(response => response.text())
@@ -250,15 +246,13 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
250
246
  let response = configObj || (await (await fetch(configUrl)).json())
251
247
 
252
248
  // If data is included through a URL, fetch that and store
253
- let data = response.formattedData || response.data || {}
249
+ let data: any[] = response.data || []
254
250
 
255
251
  const urlFilters = response.filters ? (response.filters.filter(filter => filter.type === 'url').length > 0 ? true : false) : false
256
252
 
257
253
  if (response.dataUrl && !urlFilters) {
258
254
  try {
259
- const regex = /(?:\.([^.]+))?$/
260
-
261
- const ext = regex.exec(response.dataUrl)[1]
255
+ const ext = getFileExtension(response.dataUrl)
262
256
  if ('csv' === ext) {
263
257
  data = await fetch(response.dataUrl + `?v=${cacheBustingString()}`)
264
258
  .then(response => response.text())
@@ -289,11 +283,11 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
289
283
  console.error(`COVE: Cannot parse URL: ${response.dataUrl}`) // eslint-disable-line
290
284
  data = []
291
285
  }
286
+ }
292
287
 
293
- if (response.dataDescription) {
294
- data = transform.autoStandardize(data)
295
- data = transform.developerStandardize(data, response.dataDescription)
296
- }
288
+ if (response.dataDescription) {
289
+ data = transform.autoStandardize(data)
290
+ data = transform.developerStandardize(data, response.dataDescription)
297
291
  }
298
292
 
299
293
  if (data) {
@@ -323,7 +317,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
323
317
  updateConfig(processedConfig, data)
324
318
  }
325
319
 
326
- const updateConfig = (newConfig, dataOverride = undefined) => {
320
+ const updateConfig = (newConfig, dataOverride?: any[]) => {
327
321
  let data = dataOverride || stateData
328
322
 
329
323
  // Deeper copy
@@ -333,7 +327,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
333
327
  }
334
328
  })
335
329
 
336
- let newExcludedData
330
+ let newExcludedData: any[] = []
337
331
 
338
332
  if (newConfig.exclusions && newConfig.exclusions.active) {
339
333
  if (newConfig.xAxis.type === 'categorical' && newConfig.exclusions.keys?.length > 0) {
@@ -365,7 +359,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
365
359
  setExcludedData(newExcludedData)
366
360
 
367
361
  // After data is grabbed, loop through and generate filter column values if there are any
368
- let currentData
362
+ let currentData: any[] = []
369
363
  if (newConfig.filters) {
370
364
  newConfig.filters.forEach((filter, index) => {
371
365
  let filterValues = []
@@ -394,8 +388,8 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
394
388
  } else {
395
389
  newConfig.runtime.seriesKeys = newConfig.series
396
390
  ? newConfig.series.map(series => {
397
- newConfig.runtime.seriesLabels[series.dataKey] = series.label || series.dataKey
398
- newConfig.runtime.seriesLabelsAll.push(series.name || series.label || series.dataKey)
391
+ newConfig.runtime.seriesLabels[series.dataKey] = series.name || series.label || series.dataKey
392
+ newConfig.runtime.seriesLabelsAll.push(series.name || series.dataKey)
399
393
  return series.dataKey
400
394
  })
401
395
  : []
@@ -412,8 +406,8 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
412
406
  }
413
407
 
414
408
  const groups = uniqueArray(allKeys)
415
- let tableData = []
416
- const plots = []
409
+ let tableData: any[] = []
410
+ const plots: any[] = []
417
411
 
418
412
  /**
419
413
  * Calculates the first quartile (q1) and third quartile (q3) from an array of integers or decimals.
@@ -449,13 +443,13 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
449
443
  // group specific statistics
450
444
  // prevent re-renders
451
445
  if (!groups) return
452
- groups.forEach((g, index) => {
446
+ groups.forEach(g => {
453
447
  try {
454
448
  if (!g) throw new Error('No groups resolved in box plots')
455
449
 
456
450
  // filter data by group
457
451
  let filteredData = newExcludedData ? newExcludedData.filter(item => item[newConfig.xAxis.dataKey] === g) : data.filter(item => item[newConfig.xAxis.dataKey] === g)
458
- let filteredDataValues = filteredData.map(item => Number(item[newConfig?.series[0]?.dataKey]))
452
+ let filteredDataValues: number[] = filteredData.map(item => Number(item[newConfig?.series[0]?.dataKey]))
459
453
 
460
454
  // Sort the data for upcoming functions.
461
455
  let sortedData = filteredDataValues.sort((a, b) => a - b)
@@ -484,19 +478,20 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
484
478
  let nonOutliers = filteredDataValues
485
479
 
486
480
  nonOutliers = nonOutliers.filter(item => !outliers.includes(item))
487
-
481
+ const minValue: number = d3.min<number>(filteredDataValues) || 0
482
+ const _colMin = d3.max<number>([minValue, q1 - 1.5 * iqr])
488
483
  plots.push({
489
484
  columnCategory: g,
490
485
  columnMax: d3.min([d3.max(filteredDataValues), q1 + 1.5 * iqr]),
491
486
  columnThirdQuartile: Number(q3).toFixed(newConfig.dataFormat.roundTo),
492
487
  columnMedian: Number(d3.median(filteredDataValues)).toFixed(newConfig.dataFormat.roundTo),
493
488
  columnFirstQuartile: q1.toFixed(newConfig.dataFormat.roundTo),
494
- columnMin: d3.max([d3.min(filteredDataValues), q1 - 1.5 * iqr]),
489
+ columnMin: _colMin,
495
490
  columnTotal: filteredDataValues.reduce((partialSum, a) => partialSum + a, 0),
496
491
  columnSd: Number(d3.deviation(filteredDataValues)).toFixed(newConfig.dataFormat.roundTo),
497
492
  columnMean: Number(d3.mean(filteredDataValues)).toFixed(newConfig.dataFormat.roundTo),
498
493
  columnIqr: Number(iqr).toFixed(newConfig.dataFormat.roundTo),
499
- columnLowerBounds: d3.max([d3.min(filteredDataValues), q1 - 1.5 * iqr]),
494
+ columnLowerBounds: _colMin,
500
495
  columnUpperBounds: d3.min([d3.max(sortedData), q1 + 1.5 * iqr]),
501
496
  columnOutliers: outliers,
502
497
  values: filteredDataValues,
@@ -511,10 +506,10 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
511
506
  // this appears to be the easiest option instead of running logic against the datatable cell...
512
507
  tableData = JSON.parse(JSON.stringify(plots))
513
508
  tableData.map(table => {
514
- delete table.columnIqr
515
- delete table.nonOutlierValues
516
- delete table.columnLowerBounds
517
- delete table.columnUpperBounds
509
+ table.columnIqr = undefined
510
+ table.nonOutlierValues = undefined
511
+ table.columnLowerBounds = undefined
512
+ table.columnUpperBounds = undefined
518
513
  return null // resolve eslint
519
514
  })
520
515
 
@@ -570,7 +565,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
570
565
  newConfig.runtime.yAxis = newConfig.xAxis
571
566
  newConfig.runtime.horizontal = true
572
567
  newConfig.orientation = 'horizontal'
573
- } else if (['Box Plot', 'Scatter Plot', 'Area Chart'].includes(newConfig.visualizationType)) {
568
+ } else if (['Box Plot', 'Scatter Plot', 'Area Chart', 'Line', 'Forecasting'].includes(newConfig.visualizationType)) {
574
569
  newConfig.runtime.xAxis = newConfig.xAxis
575
570
  newConfig.runtime.yAxis = newConfig.yAxis
576
571
  newConfig.runtime.horizontal = false
@@ -587,7 +582,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
587
582
  }
588
583
 
589
584
  const filterData = (filters, data) => {
590
- let filteredData = []
585
+ let filteredData: any[] = []
591
586
 
592
587
  data.forEach(row => {
593
588
  let add = true
@@ -607,7 +602,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
607
602
 
608
603
  // Gets filter values from dataset
609
604
  const generateValuesForFilter = (columnName, data = this.state.data) => {
610
- const values = []
605
+ const values: any[] = []
611
606
 
612
607
  data.forEach(row => {
613
608
  const value = row[columnName]
@@ -697,7 +692,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
697
692
  */
698
693
  useEffect(() => {
699
694
  const handleFilterData = e => {
700
- let tmp = []
695
+ let tmp: any[] = []
701
696
  tmp.push(e.detail)
702
697
  setExternalFilters(tmp)
703
698
  }
@@ -744,7 +739,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
744
739
  // Generates color palette to pass to child chart component
745
740
  useEffect(() => {
746
741
  if (stateData && config.xAxis && config.runtime.seriesKeys) {
747
- const configPalette = config.visualizationType === 'Paired Bar' || config.visualizationType === 'Deviation Bar' ? config.twoColor.palette : config.palette
742
+ const configPalette = config.customColors ? config.customColors : config.visualizationType === 'Paired Bar' || config.visualizationType === 'Deviation Bar' ? config.twoColor.palette : config.palette
748
743
  const allPalettes = { ...colorPalettes, ...twoColorPalette }
749
744
  let palette = config.customColors || allPalettes[configPalette]
750
745
  let numberOfKeys = config.runtime.seriesKeys.length
@@ -759,7 +754,8 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
759
754
  newColorScale = () =>
760
755
  scaleOrdinal({
761
756
  domain: config.runtime.seriesLabelsAll,
762
- range: palette
757
+ range: palette,
758
+ unknown: null
763
759
  })
764
760
 
765
761
  setColorScale(newColorScale)
@@ -773,10 +769,10 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
773
769
 
774
770
  // Called on legend click, highlights/unhighlights the data series with the given label
775
771
  const highlight = label => {
776
- const newSeriesHighlight = []
772
+ const newSeriesHighlight: any[] = []
777
773
 
778
774
  // If we're highlighting all the series, reset them
779
- if (seriesHighlight.length + 1 === config.runtime.seriesKeys.length && !config.legend.dynamicLegend && config.visualizationType !== 'Forecasting') {
775
+ if (seriesHighlight.length + 1 === config.runtime.seriesKeys.length && config.visualizationType !== 'Forecasting') {
780
776
  highlightReset()
781
777
  return
782
778
  }
@@ -806,25 +802,28 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
806
802
  * - pushes series.dataKey into the series highlight based on the found series.name
807
803
  * @param {String} value
808
804
  */
809
- const pushDataKeyBySeriesName = value => {
810
- let matchingSeries = config.series.filter(series => series.name === value.text)
811
- if (matchingSeries?.length > 0) {
812
- newSeriesHighlight.push(matchingSeries[0].dataKey)
813
- }
814
- }
805
+ // const pushDataKeyBySeriesName = value => {
806
+ // let matchingSeries = config.series.filter(series => series.name === value.text)
807
+ // if (matchingSeries?.length > 0) {
808
+ // newSeriesHighlight.push(matchingSeries[0].dataKey)
809
+ // }
810
+ // }
815
811
 
816
- pushDataKeyBySeriesName(label)
812
+ // pushDataKeyBySeriesName(label)
817
813
 
818
814
  setSeriesHighlight(newSeriesHighlight)
819
815
  }
820
816
 
821
817
  // Called on reset button click, unhighlights all data series
822
818
  const highlightReset = () => {
823
- if (config.legend.dynamicLegend && dynamicLegendItems) {
824
- setSeriesHighlight(dynamicLegendItems.map(item => item.text))
825
- } else {
826
- setSeriesHighlight([])
819
+ try {
820
+ const legend = document.getElementById('legend')
821
+ if (!legend) throw new Error('No legend available to set previous focus on.')
822
+ legend.focus()
823
+ } catch (e) {
824
+ console.error('COVE:', e.message)
827
825
  }
826
+ setSeriesHighlight([])
828
827
  }
829
828
 
830
829
  const section = config.orientation === 'horizontal' ? 'yAxis' : 'xAxis'
@@ -849,7 +848,10 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
849
848
  function getTextWidth(text, font) {
850
849
  const canvas = document.createElement('canvas')
851
850
  const context = canvas.getContext('2d')
852
-
851
+ if (!context) {
852
+ console.error('2d context not found')
853
+ return
854
+ }
853
855
  context.font = font || getComputedStyle(document.body).font
854
856
 
855
857
  return Math.ceil(context.measureText(text).width)
@@ -875,6 +877,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
875
877
 
876
878
  // Format numeric data based on settings in config OR from passed in settings for Additional Columns
877
879
  // - use only for old horizontal data - newer formatNumber is in helper/formatNumber
880
+ // TODO: we should combine various formatNumber functions across this project.
878
881
  const formatNumber = (num, axis, shouldAbbreviate = false, addColPrefix, addColSuffix, addColRoundTo) => {
879
882
  // if num is NaN return num
880
883
  if (isNaN(num) || !num) return num
@@ -897,7 +900,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
897
900
  if (String(num).indexOf(',') !== -1) num = num.replaceAll(',', '')
898
901
 
899
902
  let original = num
900
- let stringFormattingOptions = {
903
+ let stringFormattingOptions: any = {
901
904
  useGrouping: commas ? true : false // for old chart data table to work right cant just leave this to undefined
902
905
  }
903
906
  if (axis === 'left' || axis === undefined) {
@@ -1144,8 +1147,10 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
1144
1147
  }
1145
1148
  }
1146
1149
 
1150
+ // TODO: should be part of the DataTransform class.
1147
1151
  const clean = data => {
1148
1152
  // cleaning is deleting data we need in forecasting charts.
1153
+ if (!Array.isArray(data)) return []
1149
1154
  if (config.visualizationType === 'Forecasting') return data
1150
1155
  return config?.xAxis?.dataKey ? transform.cleanData(data, config.xAxis.dataKey) : data
1151
1156
  }
@@ -1180,9 +1185,9 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
1180
1185
  Skip Over Chart Container
1181
1186
  </a>
1182
1187
  {/* Filters */}
1183
- {config.filters && !externalFilters && <Filters config={config} setConfig={setConfig} setFilteredData={setFilteredData} filteredData={filteredData} excludedData={excludedData} filterData={filterData} isNumber={isNumber} dimensions={dimensions} />}
1188
+ {config.filters && !externalFilters && <Filters config={config} setConfig={setConfig} setFilteredData={setFilteredData} filteredData={filteredData} excludedData={excludedData} filterData={filterData} dimensions={dimensions} />}
1184
1189
  {/* Visualization */}
1185
- {config?.introText && <section className='introText'>{parse(config.introText)}</section>}
1190
+ {config?.introText && config.visualizationType !== 'Spark Line' && <section className='introText'>{parse(config.introText)}</section>}
1186
1191
  <div
1187
1192
  style={{ marginBottom: config.legend.position !== 'bottom' && config.orientation === 'horizontal' ? `${config.runtime.xAxis.size}px` : '0px' }}
1188
1193
  className={`chart-container p-relative ${config.legend.position === 'bottom' ? 'bottom' : ''}${config.legend.hide ? ' legend-hidden' : ''}${lineDatapointClass}${barBorderClass} ${contentClasses.join(' ')}`}
@@ -1193,16 +1198,19 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
1193
1198
  {/* Sparkline */}
1194
1199
  {config.visualizationType === 'Spark Line' && (
1195
1200
  <>
1196
- {description && <div className='subtext'>{parse(description)}</div>}
1197
- <div style={sparkLineStyles}>
1198
- <ParentSize>
1199
- {parent => (
1200
- <>
1201
- <SparkLine width={parent.width} height={parent.height} />
1202
- </>
1203
- )}
1204
- </ParentSize>
1201
+ {config?.introText && (
1202
+ <section className='introText' style={{ padding: '0px 0 35px' }}>
1203
+ {parse(config.introText)}
1204
+ </section>
1205
+ )}
1206
+ <div style={{ height: `100px`, width: `100%`, ...sparkLineStyles }}>
1207
+ <ParentSize>{parent => <SparkLine width={parent.width} height={parent.height} />}</ParentSize>
1205
1208
  </div>
1209
+ {description && (
1210
+ <div className='subtext' style={{ padding: '35px 0 15px' }}>
1211
+ {parse(description)}
1212
+ </div>
1213
+ )}
1206
1214
  </>
1207
1215
  )}
1208
1216
  {!config.legend.hide && config.visualizationType !== 'Spark Line' && <Legend />}
@@ -1225,9 +1233,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
1225
1233
  config={config}
1226
1234
  rawData={config.data}
1227
1235
  runtimeData={filteredData || excludedData}
1228
- //navigationHandler={navigationHandler} // do we need this? What does it do?
1229
1236
  expandDataTable={config.table.expanded}
1230
- //headerColor={general.headerColor} // have this in map but not chart
1231
1237
  columns={config.columns}
1232
1238
  showDownloadButton={config.general.showDownloadButton}
1233
1239
  runtimeLegend={dynamicLegendItems}
@@ -1247,6 +1253,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
1247
1253
  innerContainerRef={innerContainerRef}
1248
1254
  outerContainerRef={outerContainerRef}
1249
1255
  imageRef={imageId}
1256
+ colorScale={colorScale}
1250
1257
  isDebug={isDebug}
1251
1258
  isEditor={isEditor}
1252
1259
  />
@@ -0,0 +1,188 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+
3
+ import Chart from '../CdcChart'
4
+
5
+ const meta: Meta<typeof Chart> = {
6
+ title: 'Components/Templates/Chart',
7
+ component: Chart
8
+ }
9
+
10
+ type Story = StoryObj<typeof Chart>
11
+
12
+ export const Lollipop: Story = {
13
+ args: {
14
+ config: {
15
+ type: 'chart',
16
+ title: 'Lollipop Style Horizontal Bar Chart - Number of Spills Occurring in the Home',
17
+ showTitle: true,
18
+ showDownloadMediaButton: false,
19
+ theme: 'theme-blue',
20
+ animate: true,
21
+ fontSize: 'medium',
22
+ lineDatapointStyle: 'hover',
23
+ barHasBorder: 'false',
24
+ isLollipopChart: true,
25
+ lollipopShape: 'circle',
26
+ lollipopColorStyle: 'two-tone',
27
+ visualizationSubType: 'horizontal',
28
+ barStyle: '',
29
+ roundingStyle: 'standard',
30
+ tipRounding: 'top',
31
+ isResponsiveTicks: false,
32
+ general: { showDownloadButton: false },
33
+ padding: { left: 5, right: 5 },
34
+ yAxis: {
35
+ hideAxis: true,
36
+ displayNumbersOnBar: true,
37
+ hideLabel: false,
38
+ hideTicks: false,
39
+ size: '13',
40
+ gridLines: false,
41
+ enablePadding: false,
42
+ min: '',
43
+ max: '',
44
+ labelColor: '#333',
45
+ tickLabelColor: '#333',
46
+ tickColor: '#333',
47
+ rightHideAxis: true,
48
+ rightAxisSize: 50,
49
+ rightLabel: '',
50
+ rightLabelOffsetSize: 0,
51
+ rightAxisLabelColor: '#333',
52
+ rightAxisTickLabelColor: '#333',
53
+ rightAxisTickColor: '#333',
54
+ numTicks: '9',
55
+ axisPadding: 0,
56
+ tickRotation: 0,
57
+ anchors: [],
58
+ type: 'chart',
59
+ title: 'Lollipop Style Horizontal Bar Chart',
60
+ theme: 'theme-blue',
61
+ fontSize: 'medium',
62
+ lineDatapointStyle: 'hover',
63
+ barHasBorder: 'false',
64
+ isLollipopChart: false,
65
+ lollipopShape: 'circle',
66
+ lollipopColorStyle: 'two-tone',
67
+ visualizationSubType: 'horizontal',
68
+ padding: { left: 5, right: 5 },
69
+ yAxis: { size: 50, gridLines: false },
70
+ barThickness: 0.35,
71
+ height: 260,
72
+ xAxis: { type: 'categorical', size: 75, tickRotation: 0, dataKey: 'Vehicle' },
73
+ table: { label: 'Data Table', expanded: true, show: true },
74
+ legend: { behavior: 'isolate', position: 'right' },
75
+ exclusions: { active: false, keys: [] },
76
+ palette: 'qualitative-bold',
77
+ labels: false,
78
+ dataFormat: {},
79
+ confidenceKeys: {},
80
+ data: [
81
+ { Group: 'Combined Total of Group A', Vehicle: '100', Home: '120', Work: '140', Office: '120' },
82
+ { Group: 'Combined Total of Group B', Vehicle: '150', Home: '140', Work: '100', Office: '90' },
83
+ { Group: 'Combined Total of Group C', Vehicle: '90', Home: '90', Work: '80', Office: '80' },
84
+ { Group: 'Combined Total of Group D', Vehicle: '70', Home: '60', Work: '50', Office: '70' }
85
+ ],
86
+ dataFileName: 'CSV_Source_Example_for_Horizontal_Bar_viz-cdcwp1619811744363.csv',
87
+ dataFileSourceType: 'file',
88
+ visualizationType: 'Bar',
89
+ runtime: {
90
+ seriesLabels: { Vehicle: 'Vehicle' },
91
+ seriesLabelsAll: ['Vehicle'],
92
+ originalXAxis: { type: 'categorical', size: 75, tickRotation: 0, dataKey: 'Vehicle' },
93
+ seriesKeys: ['Vehicle'],
94
+ xAxis: { size: 50, gridLines: false },
95
+ yAxis: { type: 'categorical', size: 75, tickRotation: 0, dataKey: 'Vehicle' },
96
+ horizontal: true,
97
+ uniqueId: 1651765968212,
98
+ editorErrorMessage: ''
99
+ },
100
+ description: 'Subtext can be added here for options like citing data sources or insight into reading the bar chart.',
101
+ series: [{ dataKey: 'Vehicle', type: 'Bar' }],
102
+ barHeight: 25,
103
+ barPadding: 40,
104
+ labelPlacement: 'Below Bar',
105
+ label: 'Number of Accidents'
106
+ },
107
+ boxplot: [],
108
+ topAxis: { hasLine: false },
109
+ isLegendValue: false,
110
+ barThickness: 0.35,
111
+ barHeight: 6,
112
+ barSpace: 15,
113
+ heights: { vertical: 300, horizontal: 170.39999999999998 },
114
+ xAxis: {
115
+ anchors: [],
116
+ type: 'categorical',
117
+ showTargetLabel: true,
118
+ targetLabel: 'Target',
119
+ hideAxis: true,
120
+ hideLabel: true,
121
+ hideTicks: true,
122
+ size: '16',
123
+ tickRotation: 0,
124
+ min: '',
125
+ max: '160',
126
+ labelColor: '#333',
127
+ tickLabelColor: '#333',
128
+ tickColor: '#333',
129
+ numTicks: '',
130
+ labelOffset: 65,
131
+ axisPadding: 0,
132
+ target: 0,
133
+ maxTickRotation: 0,
134
+ dataKey: 'Group'
135
+ },
136
+ table: { label: 'Data Table', expanded: false, limitHeight: false, height: '', caption: '', showDownloadUrl: false, showDataTableLink: true, indexLabel: 'Group', download: false, showVertical: true, show: true },
137
+ orientation: 'horizontal',
138
+ color: 'pinkpurple',
139
+ columns: {},
140
+ legend: {
141
+ behavior: 'isolate',
142
+ singleRow: false,
143
+ colorCode: '',
144
+ reverseLabelOrder: false,
145
+ description: '',
146
+ dynamicLegend: false,
147
+ dynamicLegendDefaultText: 'Show All',
148
+ dynamicLegendItemLimit: 5,
149
+ dynamicLegendItemLimitMessage: 'Dynamic Legend Item Limit Hit.',
150
+ dynamicLegendChartMessage: 'Select Options from the Legend',
151
+ position: 'right',
152
+ hide: true,
153
+ label: 'Accident Location'
154
+ },
155
+ exclusions: { active: false, keys: [] },
156
+ palette: 'qualitative-bold',
157
+ isPaletteReversed: false,
158
+ twoColor: { palette: 'monochrome-1', isPaletteReversed: false },
159
+ labels: false,
160
+ dataFormat: { commas: false, prefix: '', suffix: '', abbreviated: false, bottomSuffix: '', bottomPrefix: '', bottomAbbreviated: false },
161
+ confidenceKeys: {},
162
+ visual: { border: true, accent: true, background: true, verticalHoverLine: false, horizontalHoverLine: false },
163
+ useLogScale: false,
164
+ filterBehavior: 'Filter Change',
165
+ highlightedBarValues: [],
166
+ series: [{ dataKey: 'Home', type: 'Bar', tooltip: true }],
167
+ tooltips: { opacity: 90 },
168
+ height: 212,
169
+ data: [
170
+ { Group: 'Combined Total of Group A', Vehicle: '100', Home: '120', Work: '140', Office: '120' },
171
+ { Group: 'Combined Total of Group B', Vehicle: '150', Home: '140', Work: '100', Office: '90' },
172
+ { Group: 'Combined Total of Group C', Vehicle: '90', Home: '90', Work: '80', Office: '80' },
173
+ { Group: 'Combined Total of Group D', Vehicle: '70', Home: '60', Work: '50', Office: '70' }
174
+ ],
175
+ dataFileName: 'CSV_Source_Example_for_Horizontal_Bar_viz-cdcwp1619811744363.csv',
176
+ dataFileSourceType: 'file',
177
+ visualizationType: 'Bar',
178
+ description: 'Subtext can be added here for options like citing data sources or insight into reading the bar chart.',
179
+ barPadding: 47,
180
+ filters: [],
181
+ lollipopSize: 'medium',
182
+ validated: 4.23,
183
+ dynamicMarginTop: 0
184
+ }
185
+ }
186
+ }
187
+
188
+ export default meta
@@ -0,0 +1,73 @@
1
+ import React, { useContext, memo } from 'react'
2
+
3
+ // cdc
4
+ import ConfigContext from '../ConfigContext'
5
+ import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
6
+
7
+ // visx & d3
8
+ import * as allCurves from '@visx/curve'
9
+ import { Bar, AreaStack } from '@visx/shape'
10
+ import { Group } from '@visx/group'
11
+ import { approvedCurveTypes } from '@cdc/core/helpers/lineChartHelpers'
12
+
13
+ const AreaChartStacked = ({ xScale, yScale, yMax, xMax, handleTooltipMouseOver, handleTooltipMouseOff, isDebug, isBrush }) => {
14
+ // import data from context
15
+ let { transformedData: data, config, seriesHighlight, colorScale, rawData } = useContext(ConfigContext)
16
+
17
+ // Draw transparent bars over the chart to get tooltip data
18
+ // Turn DEBUG on for additional context.
19
+ if (!data) return
20
+
21
+ const handleDateCategory = value => {
22
+ if (config.xAxis.type === 'categorical') return xScale(value)
23
+ if (config.xAxis.type === 'date') {
24
+ let date = new Date(value)
25
+ return xScale(date)
26
+ }
27
+ }
28
+
29
+ const strokeWidth = 2
30
+
31
+ return (
32
+ data && (
33
+ <svg height={Number(yMax)}>
34
+ <ErrorBoundary component='AreaChartStacked'>
35
+ <Group className='area-chart' key='area-wrapper' left={Number(config.yAxis.size) + strokeWidth / 2} height={Number(yMax)} style={{ overflow: 'hidden' }}>
36
+ <AreaStack
37
+ data={data}
38
+ keys={config.runtime.areaSeriesKeys.map(s => s.dataKey) || config.series.map(s => s.dataKey)}
39
+ x0={d => handleDateCategory(d.data[config.xAxis.dataKey])}
40
+ y0={d => Number(yScale(d[0]))}
41
+ y1={d => Number(yScale(d[1]))}
42
+ curve={allCurves[approvedCurveTypes[config.stackedAreaChartLineType]]}
43
+ >
44
+ {({ stacks, path }) => {
45
+ return stacks.map((stack, stackIndex) => {
46
+ let transparentArea = config.legend.behavior === 'highlight' && seriesHighlight.length > 0 && seriesHighlight.indexOf(stack.key) === -1
47
+ let displayArea = config.legend.behavior === 'highlight' || seriesHighlight.length === 0 || seriesHighlight.indexOf(stack.key) !== -1
48
+
49
+ return (
50
+ // prettier-ignore
51
+ <path
52
+ key={stack.key}
53
+ d={path(stack) || ''}
54
+ strokeWidth={2}
55
+ stroke={displayArea ? colorScale ? colorScale(config.runtime.seriesLabels ? config.runtime.seriesLabels[stack.key] : stack.key) : '#000' : 'transparent'}
56
+ fillOpacity={transparentArea ? 0.25 : 0.5}
57
+ fill={displayArea ? colorScale ? colorScale(config.runtime.seriesLabels ? config.runtime.seriesLabels[stack.key] : stack.key) : '#000' : 'transparent'}
58
+ />
59
+ )
60
+ })
61
+ }}
62
+ </AreaStack>
63
+
64
+ {/* prettier-ignore */}
65
+ {!isBrush && <Bar width={Number(xMax)} height={Number(yMax)} fill={isDebug ? 'red' : 'transparent'} fillOpacity={0.05} style={isDebug ? { stroke: 'black', strokeWidth: 2 } : {}} onMouseMove={e => handleTooltipMouseOver(e, rawData)} onMouseLeave={handleTooltipMouseOff} />}
66
+ </Group>
67
+ </ErrorBoundary>
68
+ </svg>
69
+ )
70
+ )
71
+ }
72
+
73
+ export default memo(AreaChartStacked)