@cdc/chart 4.24.7 → 4.24.9

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 (51) hide show
  1. package/dist/cdcchart.js +40313 -37543
  2. package/examples/cases-year.json +13379 -0
  3. package/examples/gallery/bar-chart-vertical/combo-line-chart.json +76 -15
  4. package/examples/gallery/bar-chart-vertical/vertical-bar-chart-stacked.json +5 -5
  5. package/index.html +17 -8
  6. package/package.json +2 -2
  7. package/src/CdcChart.tsx +383 -133
  8. package/src/_stories/Chart.Legend.Gradient.tsx +19 -0
  9. package/src/_stories/_mock/legend.gradient_mock.json +236 -0
  10. package/src/components/Annotations/components/AnnotationDraggable.tsx +64 -11
  11. package/src/components/Axis/Categorical.Axis.tsx +145 -0
  12. package/src/components/BarChart/components/BarChart.Horizontal.tsx +4 -3
  13. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +1 -1
  14. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +2 -5
  15. package/src/components/BarChart/components/BarChart.Vertical.tsx +17 -8
  16. package/src/components/BarChart/helpers/index.ts +5 -16
  17. package/src/components/BrushChart.tsx +205 -0
  18. package/src/components/EditorPanel/EditorPanel.tsx +1766 -509
  19. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +19 -5
  20. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +190 -37
  21. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +43 -7
  22. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +4 -4
  23. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +1 -11
  24. package/src/components/EditorPanel/editor-panel.scss +16 -3
  25. package/src/components/EditorPanel/{useEditorPermissions.js → useEditorPermissions.ts} +90 -19
  26. package/src/components/Legend/Legend.Component.tsx +185 -193
  27. package/src/components/Legend/Legend.Suppression.tsx +146 -0
  28. package/src/components/Legend/Legend.tsx +21 -5
  29. package/src/components/Legend/helpers/index.ts +33 -3
  30. package/src/components/LegendWrapper.tsx +26 -0
  31. package/src/components/LineChart/LineChartProps.ts +1 -18
  32. package/src/components/LineChart/components/LineChart.BumpCircle.tsx +103 -0
  33. package/src/components/LineChart/components/LineChart.Circle.tsx +47 -8
  34. package/src/components/LineChart/helpers.ts +55 -11
  35. package/src/components/LineChart/index.tsx +113 -38
  36. package/src/components/LinearChart.tsx +1366 -0
  37. package/src/components/PieChart/PieChart.tsx +74 -17
  38. package/src/components/Sankey/index.tsx +22 -16
  39. package/src/components/Sparkline/components/SparkLine.tsx +2 -2
  40. package/src/data/initial-state.js +13 -3
  41. package/src/hooks/useLegendClasses.ts +52 -15
  42. package/src/hooks/useMinMax.ts +4 -4
  43. package/src/hooks/useScales.ts +34 -24
  44. package/src/hooks/useTooltip.tsx +85 -22
  45. package/src/scss/DataTable.scss +2 -1
  46. package/src/scss/main.scss +107 -14
  47. package/src/types/ChartConfig.ts +34 -8
  48. package/src/types/ChartContext.ts +5 -4
  49. package/examples/feature/line/line-chart.json +0 -449
  50. package/src/components/BrushHandle.jsx +0 -17
  51. package/src/components/LineChart/index.scss +0 -1
package/src/CdcChart.tsx CHANGED
@@ -7,6 +7,9 @@ import * as d3 from 'd3-array'
7
7
  import Layout from '@cdc/core/components/Layout'
8
8
  import Button from '@cdc/core/components/elements/Button'
9
9
 
10
+ //types
11
+ import { DimensionsType } from '@cdc/core/types/Dimensions'
12
+
10
13
  // External Libraries
11
14
  import { scaleOrdinal } from '@visx/scale'
12
15
  import ParentSize from '@visx/responsive/lib/components/ParentSize'
@@ -42,6 +45,7 @@ import MediaControls from '@cdc/core/components/MediaControls'
42
45
  import Annotation from './components/Annotations'
43
46
 
44
47
  // Helpers
48
+ import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
45
49
  import { publish, subscribe, unsubscribe } from '@cdc/core/helpers/events'
46
50
  import useDataVizClasses from '@cdc/core/helpers/useDataVizClasses'
47
51
  import numberFromString from '@cdc/core/helpers/numberFromString'
@@ -64,8 +68,22 @@ import { type ViewportSize } from './types/ChartConfig'
64
68
  import { isSolrCsv, isSolrJson } from '@cdc/core/helpers/isSolr'
65
69
  import SkipTo from '@cdc/core/components/elements/SkipTo'
66
70
  import { filterVizData } from '@cdc/core/helpers/filterVizData'
67
-
68
- export default function CdcChart({ configUrl, config: configObj, isEditor = false, isDebug = false, isDashboard = false, setConfig: setParentConfig, setEditing, hostname, link, setSharedFilter, setSharedFilterValue, dashboardConfig }) {
71
+ import LegendWrapper from './components/LegendWrapper'
72
+
73
+ export default function CdcChart({
74
+ configUrl,
75
+ config: configObj,
76
+ isEditor = false,
77
+ isDebug = false,
78
+ isDashboard = false,
79
+ setConfig: setParentConfig,
80
+ setEditing,
81
+ hostname,
82
+ link,
83
+ setSharedFilter,
84
+ setSharedFilterValue,
85
+ dashboardConfig
86
+ }) {
69
87
  const transform = new DataTransform()
70
88
  const [loading, setLoading] = useState(true)
71
89
  const [colorScale, setColorScale] = useState(null)
@@ -73,9 +91,11 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
73
91
  const [stateData, setStateData] = useState(config.data || [])
74
92
  const [excludedData, setExcludedData] = useState<Record<string, number>[] | undefined>(undefined)
75
93
  const [filteredData, setFilteredData] = useState<Record<string, any>[] | undefined>(undefined)
76
- const [seriesHighlight, setSeriesHighlight] = useState<string[]>(configObj && configObj?.legend?.seriesHighlight?.length ? [...configObj?.legend?.seriesHighlight] : [])
94
+ const [seriesHighlight, setSeriesHighlight] = useState<string[]>(
95
+ configObj && configObj?.legend?.seriesHighlight?.length ? [...configObj?.legend?.seriesHighlight] : []
96
+ )
77
97
  const [currentViewport, setCurrentViewport] = useState<ViewportSize>('lg')
78
- const [dimensions, setDimensions] = useState<[number?, number?]>([])
98
+ const [dimensions, setDimensions] = useState<DimensionsType>([0, 0])
79
99
  const [externalFilters, setExternalFilters] = useState<any[]>()
80
100
  const [container, setContainer] = useState()
81
101
  const [coveLoadedEventRan, setCoveLoadedEventRan] = useState(false)
@@ -87,6 +107,16 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
87
107
  isActive: false,
88
108
  isBrushing: false
89
109
  })
110
+
111
+ let [width] = dimensions
112
+ const useVertical = config.orientation === 'vertical'
113
+ const useMobileVertical = config.heights?.mobileVertical && ['xs', 'xxs'].includes(currentViewport)
114
+ const responsiveVertical = useMobileVertical ? 'mobileVertical' : 'vertical'
115
+ const renderedOrientation = useVertical ? responsiveVertical : 'horizontal'
116
+ let height = config.aspectRatio ? width * config.aspectRatio : config?.heights?.[renderedOrientation]
117
+ if (config.visualizationType === 'Pie') height = config?.heights?.[renderedOrientation]
118
+ height = height + Number(config?.xAxis?.size) + 45
119
+
90
120
  type Config = typeof config
91
121
  let legendMemo = useRef(new Map()) // map collection
92
122
  let innerContainerRef = useRef()
@@ -187,7 +217,11 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
187
217
  // If data is included through a URL, fetch that and store
188
218
  let data: any[] = response.data || []
189
219
 
190
- const urlFilters = response.filters ? (response.filters.filter(filter => filter.type === 'url').length > 0 ? true : false) : false
220
+ const urlFilters = response.filters
221
+ ? response.filters.filter(filter => filter.type === 'url').length > 0
222
+ ? true
223
+ : false
224
+ : false
191
225
 
192
226
  if (response.dataUrl && !urlFilters) {
193
227
  try {
@@ -267,7 +301,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
267
301
  newConfig.data = data
268
302
  }
269
303
 
270
- const processedConfig = { ...(await coveUpdateWorker(newConfig)) }
304
+ const processedConfig = { ...coveUpdateWorker(newConfig) }
271
305
 
272
306
  updateConfig(processedConfig, data)
273
307
  }
@@ -287,7 +321,11 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
287
321
  if (newConfig.exclusions && newConfig.exclusions.active) {
288
322
  if (newConfig.xAxis.type === 'categorical' && newConfig.exclusions.keys?.length > 0) {
289
323
  newExcludedData = data.filter(e => !newConfig.exclusions.keys.includes(e[newConfig.xAxis.dataKey]))
290
- } else if (isDateScale(newConfig.xAxis) && (newConfig.exclusions.dateStart || newConfig.exclusions.dateEnd) && newConfig.xAxis.dateParseFormat) {
324
+ } else if (
325
+ isDateScale(newConfig.xAxis) &&
326
+ (newConfig.exclusions.dateStart || newConfig.exclusions.dateEnd) &&
327
+ newConfig.xAxis.dateParseFormat
328
+ ) {
291
329
  // Filter dates
292
330
  const timestamp = e => new Date(e).getTime()
293
331
 
@@ -298,7 +336,9 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
298
336
  let endDateValid = undefined !== typeof endDate && false === isNaN(endDate)
299
337
 
300
338
  if (startDateValid && endDateValid) {
301
- newExcludedData = data.filter(e => timestamp(e[newConfig.xAxis.dataKey]) >= startDate && timestamp(e[newConfig.xAxis.dataKey]) <= endDate)
339
+ newExcludedData = data.filter(
340
+ e => timestamp(e[newConfig.xAxis.dataKey]) >= startDate && timestamp(e[newConfig.xAxis.dataKey]) <= endDate
341
+ )
302
342
  } else if (startDateValid) {
303
343
  newExcludedData = data.filter(e => timestamp(e[newConfig.xAxis.dataKey]) >= startDate)
304
344
  } else if (endDateValid) {
@@ -317,15 +357,25 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
317
357
  let currentData: any[] = []
318
358
  if (newConfig.filters) {
319
359
  newConfig.filters.forEach((filter, index) => {
320
- let filterValues = []
321
-
322
- filterValues = filter.orderedValues || generateValuesForFilter(filter.columnName, newExcludedData).sort(filter.order === 'desc' ? sortDesc : sortAsc)
360
+ const filterValues =
361
+ filter.filterStyle === 'nested-dropdown'
362
+ ? filter.values
363
+ : filter.orderedValues ||
364
+ generateValuesForFilter(filter.columnName, newExcludedData).sort(
365
+ filter.order === 'desc' ? sortDesc : sortAsc
366
+ )
323
367
 
324
368
  newConfig.filters[index].values = filterValues
325
369
  // Initial filter should be active
326
370
 
327
- newConfig.filters[index].active = !newConfig.filters[index].active || filterValues.indexOf(newConfig.filters[index].active) === -1 ? filterValues[0] : newConfig.filters[index].active
328
- newConfig.filters[index].filterStyle = newConfig.filters[index].filterStyle ? newConfig.filters[index].filterStyle : 'dropdown'
371
+ const includes = (arr: any[], val: any): boolean => (arr || []).map(val => String(val)).includes(String(val))
372
+ newConfig.filters[index].active =
373
+ !newConfig.filters[index].active || !includes(filterValues, newConfig.filters[index].active)
374
+ ? filterValues[0]
375
+ : newConfig.filters[index].active
376
+ newConfig.filters[index].filterStyle = newConfig.filters[index].filterStyle
377
+ ? newConfig.filters[index].filterStyle
378
+ : 'dropdown'
329
379
  })
330
380
  currentData = filterVizData(newConfig.filters, newExcludedData)
331
381
  setFilteredData(currentData)
@@ -337,16 +387,38 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
337
387
 
338
388
  //Enforce default values that need to be calculated at runtime
339
389
  newConfig.runtime = {}
390
+ newConfig.runtime.series = newConfig.dynamicSeries ? [] : newConfig.series
340
391
  newConfig.runtime.seriesLabels = {}
341
392
  newConfig.runtime.seriesLabelsAll = []
342
393
  newConfig.runtime.originalXAxis = newConfig.xAxis
343
394
 
395
+ if (newConfig.dynamicSeries) {
396
+ let finalData = dataOverride || newConfig.formattedData || newConfig.data
397
+ if (finalData && finalData.length && finalData.length > 0) {
398
+ Object.keys(finalData[0]).forEach(seriesKey => {
399
+ if (
400
+ seriesKey !== newConfig.xAxis.dataKey &&
401
+ finalData[0][seriesKey] &&
402
+ (!newConfig.filters || newConfig.filters.filter(filter => filter.columnName === seriesKey).length === 0) &&
403
+ (!newConfig.columns || Object.keys(newConfig.columns).indexOf(seriesKey) === -1)
404
+ ) {
405
+ newConfig.runtime.series.push({
406
+ dataKey: seriesKey,
407
+ type: newConfig.dynamicSeriesType,
408
+ lineType: newConfig.dynamicSeriesLineType,
409
+ tooltip: true
410
+ })
411
+ }
412
+ })
413
+ }
414
+ }
415
+
344
416
  if (newConfig.visualizationType === 'Pie') {
345
417
  newConfig.runtime.seriesKeys = (dataOverride || data).map(d => d[newConfig.xAxis.dataKey])
346
418
  newConfig.runtime.seriesLabelsAll = newConfig.runtime.seriesKeys
347
419
  } else {
348
- newConfig.runtime.seriesKeys = newConfig.series
349
- ? newConfig.series.map(series => {
420
+ newConfig.runtime.seriesKeys = newConfig.runtime.series
421
+ ? newConfig.runtime.series.map(series => {
350
422
  newConfig.runtime.seriesLabels[series.dataKey] = series.name || series.label || series.dataKey
351
423
  newConfig.runtime.seriesLabelsAll.push(series.name || series.dataKey)
352
424
  return series.dataKey
@@ -355,8 +427,12 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
355
427
  }
356
428
 
357
429
  if (newConfig.visualizationType === 'Box Plot' && newConfig.series) {
358
- let allKeys = newExcludedData ? newExcludedData.map(d => d[newConfig.xAxis.dataKey]) : data.map(d => d[newConfig.xAxis.dataKey])
359
- let allValues = newExcludedData ? newExcludedData.map(d => Number(d[newConfig?.series[0]?.dataKey])) : data.map(d => Number(d[newConfig?.series[0]?.dataKey]))
430
+ let allKeys = newExcludedData
431
+ ? newExcludedData.map(d => d[newConfig.xAxis.dataKey])
432
+ : data.map(d => d[newConfig.xAxis.dataKey])
433
+ let allValues = newExcludedData
434
+ ? newExcludedData.map(d => Number(d[newConfig?.series[0]?.dataKey]))
435
+ : data.map(d => Number(d[newConfig?.series[0]?.dataKey]))
360
436
 
361
437
  const uniqueArray = function (arrArg) {
362
438
  return arrArg.filter(function (elem, pos, arr) {
@@ -376,7 +452,9 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
376
452
  if (!g) throw new Error('No groups resolved in box plots')
377
453
 
378
454
  // filter data by group
379
- let filteredData = newExcludedData ? newExcludedData.filter(item => item[newConfig.xAxis.dataKey] === g) : data.filter(item => item[newConfig.xAxis.dataKey] === g)
455
+ let filteredData = newExcludedData
456
+ ? newExcludedData.filter(item => item[newConfig.xAxis.dataKey] === g)
457
+ : data.filter(item => item[newConfig.xAxis.dataKey] === g)
380
458
  let filteredDataValues: number[] = filteredData.map(item => Number(item[newConfig?.series[0]?.dataKey]))
381
459
 
382
460
  // Sort the data for upcoming functions.
@@ -464,7 +542,12 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
464
542
  if (series.type === 'Bar' || series.type === 'Combo') {
465
543
  newConfig.runtime.barSeriesKeys.push(series.dataKey)
466
544
  }
467
- if (series.type === 'Line' || series.type === 'dashed-sm' || series.type === 'dashed-md' || series.type === 'dashed-lg') {
545
+ if (
546
+ series.type === 'Line' ||
547
+ series.type === 'dashed-sm' ||
548
+ series.type === 'dashed-md' ||
549
+ series.type === 'dashed-lg'
550
+ ) {
468
551
  newConfig.runtime.lineSeriesKeys.push(series.dataKey)
469
552
  }
470
553
  if (series.type === 'Combo') {
@@ -491,13 +574,21 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
491
574
  })
492
575
  }
493
576
 
494
- if ((newConfig.visualizationType === 'Bar' && newConfig.orientation === 'horizontal') || ['Deviation Bar', 'Paired Bar', 'Forest Plot'].includes(newConfig.visualizationType)) {
577
+ if (
578
+ (newConfig.visualizationType === 'Bar' && newConfig.orientation === 'horizontal') ||
579
+ ['Deviation Bar', 'Paired Bar', 'Forest Plot'].includes(newConfig.visualizationType)
580
+ ) {
495
581
  newConfig.runtime.xAxis = newConfig.yAxis['yAxis'] ? newConfig.yAxis['yAxis'] : newConfig.yAxis
496
582
  newConfig.runtime.yAxis = newConfig.xAxis['xAxis'] ? newConfig.xAxis['xAxis'] : newConfig.xAxis
497
583
 
498
584
  newConfig.runtime.horizontal = false
499
585
  newConfig.orientation = 'horizontal'
500
- } else if (['Box Plot', 'Scatter Plot', 'Area Chart', 'Line', 'Forecasting'].includes(newConfig.visualizationType) && !checkLineToBarGraph()) {
586
+ // remove after COVE supports categorical axis on horizonatal bars
587
+ newConfig.yAxis.type = newConfig.yAxis.type === 'categorical' ? 'linear' : newConfig.yAxis.type
588
+ } else if (
589
+ ['Box Plot', 'Scatter Plot', 'Area Chart', 'Line', 'Forecasting'].includes(newConfig.visualizationType) &&
590
+ !checkLineToBarGraph()
591
+ ) {
501
592
  newConfig.runtime.xAxis = newConfig.xAxis
502
593
  newConfig.runtime.yAxis = newConfig.yAxis
503
594
  newConfig.runtime.horizontal = false
@@ -509,10 +600,16 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
509
600
  }
510
601
 
511
602
  newConfig.runtime.uniqueId = Date.now()
512
- newConfig.runtime.editorErrorMessage = newConfig.visualizationType === 'Pie' && !newConfig.yAxis.dataKey ? 'Data Key property in Y Axis section must be set for pie charts.' : ''
603
+ newConfig.runtime.editorErrorMessage =
604
+ newConfig.visualizationType === 'Pie' && !newConfig.yAxis.dataKey
605
+ ? 'Data Key property in Y Axis section must be set for pie charts.'
606
+ : ''
513
607
 
514
608
  // Sankey Description box error message
515
- newConfig.runtime.editorErrorMessage = newConfig.visualizationType === 'Sankey' && !newConfig.description ? 'SUBTEXT/CITATION field is empty: A description of the Sankey Diagram data must be inputted.' : ''
609
+ newConfig.runtime.editorErrorMessage =
610
+ newConfig.visualizationType === 'Sankey' && !newConfig.description
611
+ ? 'SUBTEXT/CITATION field is empty: A description of the Sankey Diagram data must be inputted.'
612
+ : ''
516
613
 
517
614
  if (newConfig.legend.seriesHighlight?.length) {
518
615
  setSeriesHighlight(newConfig.legend?.seriesHighlight)
@@ -538,7 +635,10 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
538
635
 
539
636
  // Sorts data series for horizontal bar charts
540
637
  const sortData = (a, b) => {
541
- let sortKey = config.visualizationType === 'Bar' && config.visualizationSubType === 'horizontal' ? config.xAxis.dataKey : config.yAxis.sortKey
638
+ let sortKey =
639
+ config.visualizationType === 'Bar' && config.visualizationSubType === 'horizontal'
640
+ ? config.xAxis.dataKey
641
+ : config.yAxis.sortKey
542
642
  let aData = parseFloat(a[sortKey])
543
643
  let bData = parseFloat(b[sortKey])
544
644
 
@@ -555,15 +655,14 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
555
655
  const resizeObserver = new ResizeObserver(entries => {
556
656
  for (let entry of entries) {
557
657
  let { width, height } = entry.contentRect
558
- let newViewport = getViewport(width)
559
658
  let svgMarginWidth = 32
560
659
  let editorWidth = 350
561
660
 
562
- setCurrentViewport(newViewport)
661
+ width = isEditor ? width - editorWidth : width
563
662
 
564
- if (isEditor) {
565
- width = width - editorWidth
566
- }
663
+ let newViewport = getViewport(width)
664
+
665
+ setCurrentViewport(newViewport)
567
666
 
568
667
  if (entry.target.dataset.lollipop === 'true') {
569
668
  width = width - 2.5
@@ -642,7 +741,12 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
642
741
  }
643
742
  }
644
743
 
645
- if (externalFilters && externalFilters.length > 0 && externalFilters.length > 0 && externalFilters[0].hasOwnProperty('active')) {
744
+ if (
745
+ externalFilters &&
746
+ externalFilters.length > 0 &&
747
+ externalFilters.length > 0 &&
748
+ externalFilters[0].hasOwnProperty('active')
749
+ ) {
646
750
  let newConfigHere = { ...config, filters: externalFilters }
647
751
  setConfig(newConfigHere)
648
752
  setFilteredData(filterVizData(externalFilters, excludedData))
@@ -657,10 +761,25 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
657
761
  }, [configObj.data]) // eslint-disable-line
658
762
  }
659
763
 
764
+ // This will set the bump chart's default scaling type to date-time
765
+ useEffect(() => {
766
+ if (['Bump Chart'].includes(config.visualizationType)) {
767
+ setConfig({
768
+ ...config,
769
+ xAxis: {
770
+ ...config.xAxis,
771
+ type: 'date-time'
772
+ }
773
+ })
774
+ }
775
+ }, [config.visualizationType])
776
+
660
777
  // Generates color palette to pass to child chart component
661
778
  useEffect(() => {
662
779
  if (stateData && config.xAxis && config.runtime?.seriesKeys) {
663
- const configPalette = ['Paired Bar', 'Deviation Bar'].includes(config.visualizationType) ? config.twoColor.palette : config.palette
780
+ const configPalette = ['Paired Bar', 'Deviation Bar'].includes(config.visualizationType)
781
+ ? config.twoColor.palette
782
+ : config.palette
664
783
  const allPalettes: Record<string, string[]> = { ...colorPalettes, ...twoColorPalette }
665
784
  let palette = config.customColors || allPalettes[configPalette]
666
785
  let numberOfKeys = config.runtime.seriesKeys.length
@@ -764,19 +883,6 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
764
883
  return timeFormat(config.tooltips.dateDisplayFormat)(date)
765
884
  }
766
885
 
767
- // function calculates the width of given text and its font-size
768
- function getTextWidth(text: string, font: string): number | undefined {
769
- const canvas = document.createElement('canvas')
770
- const context = canvas.getContext('2d')
771
- if (!context) {
772
- console.error('2d context not found')
773
- return
774
- }
775
- context.font = font || getComputedStyle(document.body).font
776
-
777
- return Math.ceil(context.measureText(text).width)
778
- }
779
-
780
886
  // Format numeric data based on settings in config OR from passed in settings for Additional Columns
781
887
  // - use only for old horizontal data - newer formatNumber is in helper/formatNumber
782
888
  // TODO: we should combine various formatNumber functions across this project.
@@ -795,7 +901,20 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
795
901
 
796
902
  // destructure dataFormat values
797
903
  let {
798
- dataFormat: { commas, abbreviated, roundTo, prefix, suffix, rightRoundTo, bottomRoundTo, rightPrefix, rightSuffix, bottomPrefix, bottomSuffix, bottomAbbreviated }
904
+ dataFormat: {
905
+ commas,
906
+ abbreviated,
907
+ roundTo,
908
+ prefix,
909
+ suffix,
910
+ rightRoundTo,
911
+ bottomRoundTo,
912
+ rightPrefix,
913
+ rightSuffix,
914
+ bottomPrefix,
915
+ bottomSuffix,
916
+ bottomAbbreviated
917
+ }
799
918
  } = config
800
919
 
801
920
  // check if value contains comma and remove it. later will add comma below.
@@ -866,7 +985,10 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
866
985
  // Edge case for small numbers with decimals
867
986
  // - if roundTo undefined which means it is blank, then do not round
868
987
 
869
- if ((axis === 'left' && commas && abbreviated && shouldAbbreviate) || (axis === 'bottom' && commas && abbreviated && shouldAbbreviate)) {
988
+ if (
989
+ (axis === 'left' && commas && abbreviated && shouldAbbreviate) ||
990
+ (axis === 'bottom' && commas && abbreviated && shouldAbbreviate)
991
+ ) {
870
992
  num = num // eslint-disable-line
871
993
  } else {
872
994
  num = num.toLocaleString('en-US', stringFormattingOptions)
@@ -922,21 +1044,6 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
922
1044
  return String(result)
923
1045
  }
924
1046
 
925
- // Select appropriate chart type
926
- const ChartComponents = {
927
- 'Paired Bar': <LinearChart />,
928
- Forecasting: <LinearChart />,
929
- Bar: <LinearChart />,
930
- Line: <LinearChart />,
931
- Combo: <LinearChart />,
932
- Pie: <PieChart />,
933
- 'Box Plot': <LinearChart />,
934
- 'Area Chart': <LinearChart />,
935
- 'Scatter Plot': <LinearChart />,
936
- 'Deviation Bar': <LinearChart />,
937
- 'Forest Plot': <LinearChart />
938
- }
939
-
940
1047
  const missingRequiredSections = () => {
941
1048
  if (config.visualizationType === 'Sankey') return false // skip checks for now
942
1049
  if (config.visualizationType === 'Forecasting') return false // skip required checks for now.
@@ -946,7 +1053,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
946
1053
  return true
947
1054
  }
948
1055
  } else {
949
- if (undefined === config?.series || false === config?.series.length > 0) {
1056
+ if ((undefined === config?.series || false === config?.series.length > 0) && !config?.dynamicSeries) {
950
1057
  return true
951
1058
  }
952
1059
  }
@@ -1053,7 +1160,12 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
1053
1160
  <section className='waiting-container'>
1054
1161
  <h3>Finish Configuring</h3>
1055
1162
  <p>Set all required options to the left and confirm below to display a preview of the chart.</p>
1056
- <Button className='btn' style={{ margin: '1em auto' }} disabled={missingRequiredSections()} onClick={e => confirmDone(e)}>
1163
+ <Button
1164
+ className='btn'
1165
+ style={{ margin: '1em auto' }}
1166
+ disabled={missingRequiredSections()}
1167
+ onClick={e => confirmDone(e)}
1168
+ >
1057
1169
  I'm Done
1058
1170
  </Button>
1059
1171
  </section>
@@ -1125,11 +1237,18 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
1125
1237
  const getChartWrapperClasses = () => {
1126
1238
  const isLegendOnBottom = legend?.position === 'bottom' || ['sm', 'xs', 'xxs'].includes(currentViewport)
1127
1239
  const classes = ['chart-container', 'p-relative']
1128
- if (config.legend?.position === 'bottom') classes.push('bottom')
1129
- if (config.legend?.hide) classes.push('legend-hidden')
1240
+ if (legend?.position) {
1241
+ if (['sm', 'xs', 'xxs'].includes(currentViewport) && legend?.position !== 'top') {
1242
+ classes.push('legend-bottom')
1243
+ } else {
1244
+ classes.push(`legend-${legend.position}`)
1245
+ }
1246
+ }
1247
+ if (legend?.hide) classes.push('legend-hidden')
1130
1248
  if (lineDatapointClass) classes.push(lineDatapointClass)
1131
1249
  if (!config.barHasBorder) classes.push('chart-bar--no-border')
1132
- if (config.brush?.active && dashboardConfig?.type === 'dashboard' && (!isLegendOnBottom || config.legend.hide)) classes.push('dashboard-brush')
1250
+ if (config.brush?.active && dashboardConfig?.type === 'dashboard' && (!isLegendOnBottom || legend.hide))
1251
+ classes.push('dashboard-brush')
1133
1252
  classes.push(...contentClasses)
1134
1253
  return classes
1135
1254
  }
@@ -1157,81 +1276,202 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
1157
1276
  {config.newViz && <Confirm />}
1158
1277
  {undefined === config.newViz && isEditor && config.runtime && config.runtime?.editorErrorMessage && <Error />}
1159
1278
  {!missingRequiredSections() && !config.newViz && (
1160
- <div className={`cdc-chart-inner-container cove-component__content type-${makeClassName(config.visualizationType)}`} aria-label={handleChartAriaLabels(config)} tabIndex={0}>
1161
- <Title showTitle={config.showTitle} isDashboard={isDashboard} title={title} superTitle={config.superTitle} classes={['chart-title', `${config.theme}`, 'cove-component__header']} style={undefined} />
1279
+ <div
1280
+ className={`cdc-chart-inner-container cove-component__content type-${makeClassName(
1281
+ config.visualizationType
1282
+ )}`}
1283
+ aria-label={handleChartAriaLabels(config)}
1284
+ tabIndex={0}
1285
+ >
1286
+ <Title
1287
+ showTitle={config.showTitle}
1288
+ isDashboard={isDashboard}
1289
+ title={title}
1290
+ superTitle={config.superTitle}
1291
+ classes={['chart-title', `${config.theme}`, 'cove-component__header']}
1292
+ style={undefined}
1293
+ />
1294
+ {/* Intro Text/Message */}
1295
+ {config?.introText && config.visualizationType !== 'Spark Line' && (
1296
+ <section
1297
+ className={`introText legend_${config.legend.hide ? 'hidden' : 'visible'}_${config.legend.position} `}
1298
+ >
1299
+ {parse(config.introText)}
1300
+ </section>
1301
+ )}
1162
1302
 
1163
1303
  {/* Filters */}
1164
- {config.filters && !externalFilters && config.visualizationType !== 'Spark Line' && <Filters config={config} setConfig={setConfig} setFilteredData={setFilteredData} filteredData={filteredData} excludedData={excludedData} filterData={filterVizData} dimensions={dimensions} />}
1304
+ {config.filters && !externalFilters && config.visualizationType !== 'Spark Line' && (
1305
+ <Filters
1306
+ config={config}
1307
+ setConfig={setConfig}
1308
+ setFilteredData={setFilteredData}
1309
+ filteredData={filteredData}
1310
+ excludedData={excludedData}
1311
+ filterData={filterVizData}
1312
+ dimensions={dimensions}
1313
+ />
1314
+ )}
1165
1315
  <SkipTo skipId={handleChartTabbing(config, legendId)} skipMessage='Skip Over Chart Container' />
1166
- {config.annotations?.length > 0 && <SkipTo skipId={handleChartTabbing(config, legendId)} skipMessage={`Skip over annotations`} key={`skip-annotations`} />}
1167
-
1168
- {/* Visualization */}
1169
- {config?.introText && config.visualizationType !== 'Spark Line' && <section className='introText'>{parse(config.introText)}</section>}
1316
+ {config.annotations?.length > 0 && (
1317
+ <SkipTo
1318
+ skipId={handleChartTabbing(config, legendId)}
1319
+ skipMessage={`Skip over annotations`}
1320
+ key={`skip-annotations`}
1321
+ />
1322
+ )}
1170
1323
 
1324
+ {/* Visualization Wrapper */}
1171
1325
  <div className={getChartWrapperClasses().join(' ')}>
1172
- {/* All charts except line and sparkline */}
1173
- {config.visualizationType !== 'Spark Line' && config.visualizationType !== 'Line' && ChartComponents[config.visualizationType]}
1174
-
1175
- {/* Line Chart */}
1176
- {config.visualizationType === 'Line' && (checkLineToBarGraph() ? ChartComponents['Bar'] : ChartComponents['Line'])}
1177
-
1178
- {/* Sparkline */}
1179
- {config.visualizationType === 'Spark Line' && (
1180
- <>
1181
- <Filters config={config} setConfig={setConfig} setFilteredData={setFilteredData} filteredData={filteredData} excludedData={excludedData} filterData={filterVizData} dimensions={dimensions} />
1182
- {config?.introText && (
1183
- <section className='introText' style={{ padding: '0px 0 35px' }}>
1184
- {parse(config.introText)}
1185
- </section>
1186
- )}
1187
- <div style={{ height: `100px`, width: `100%`, ...sparkLineStyles }}>
1188
- <ParentSize>{parent => <SparkLine width={parent.width} height={parent.height} />}</ParentSize>
1189
- </div>
1190
- {description && (
1191
- <div className='subtext' style={{ padding: '35px 0 15px' }}>
1192
- {parse(description)}
1326
+ <LegendWrapper>
1327
+ <div
1328
+ className={
1329
+ legend.hide || ['xxs', 'xs', 'sm'].includes(currentViewport)
1330
+ ? 'w-100'
1331
+ : legend.position === 'bottom' || legend.position === 'top' || visualizationType === 'Sankey'
1332
+ ? 'w-100'
1333
+ : 'w-75'
1334
+ }
1335
+ >
1336
+ {/* All charts with LinearChart */}
1337
+ {!['Spark Line', 'Line', 'Sankey', 'Pie', 'Sankey'].includes(config.visualizationType) && (
1338
+ <div style={{ height, width: `100%` }}>
1339
+ <ParentSize>
1340
+ {parent => <LinearChart parentWidth={parent.width} parentHeight={parent.height} />}
1341
+ </ParentSize>
1193
1342
  </div>
1194
1343
  )}
1195
- </>
1344
+
1345
+ {config.visualizationType === 'Pie' && (
1346
+ <ParentSize className='justify-content-center d-flex' style={{ height, width: `100%` }}>
1347
+ {parent => <PieChart parentWidth={parent.width} parentHeight={parent.height} />}
1348
+ </ParentSize>
1349
+ )}
1350
+ {/* Line Chart */}
1351
+ {config.visualizationType === 'Line' &&
1352
+ (checkLineToBarGraph() ? (
1353
+ <div style={{ height: config?.heights?.vertical, width: `100%` }}>
1354
+ <ParentSize>
1355
+ {parent => <LinearChart parentWidth={parent.width} parentHeight={parent.height} />}
1356
+ </ParentSize>
1357
+ </div>
1358
+ ) : (
1359
+ <div style={{ height, width: `100%` }}>
1360
+ <ParentSize>
1361
+ {parent => <LinearChart parentWidth={parent.width} parentHeight={parent.height} />}
1362
+ </ParentSize>
1363
+ </div>
1364
+ ))}
1365
+ {/* Sparkline */}
1366
+ {config.visualizationType === 'Spark Line' && (
1367
+ <>
1368
+ <Filters
1369
+ config={config}
1370
+ setConfig={setConfig}
1371
+ setFilteredData={setFilteredData}
1372
+ filteredData={filteredData}
1373
+ excludedData={excludedData}
1374
+ filterData={filterVizData}
1375
+ dimensions={dimensions}
1376
+ />
1377
+ {config?.introText && (
1378
+ <section className='introText' style={{ padding: '0px 0 35px' }}>
1379
+ {parse(config.introText)}
1380
+ </section>
1381
+ )}
1382
+ <div style={{ height: `100px`, width: `100%`, ...sparkLineStyles }}>
1383
+ <ParentSize>{parent => <SparkLine width={parent.width} height={parent.height} />}</ParentSize>
1384
+ </div>
1385
+ {description && (
1386
+ <div className='subtext' style={{ padding: '35px 0 15px' }}>
1387
+ {parse(description)}
1388
+ </div>
1389
+ )}
1390
+ </>
1391
+ )}
1392
+ {/* Sankey */}
1393
+ {config.visualizationType === 'Sankey' && (
1394
+ <ParentSize aria-hidden='true'>
1395
+ {parent => <SankeyChart runtime={config.runtime} width={parent.width} height={parent.height} />}
1396
+ </ParentSize>
1397
+ )}
1398
+ </div>
1399
+ {/* Legend */}
1400
+ {!config.legend.hide &&
1401
+ config.visualizationType !== 'Spark Line' &&
1402
+ config.visualizationType !== 'Sankey' && (
1403
+ <Legend ref={legendRef} skipId={handleChartTabbing(config, legendId)} />
1404
+ )}
1405
+ </LegendWrapper>
1406
+ {/* Link */}
1407
+ {isDashboard && config.table && config.table.show && config.table.showDataTableLink
1408
+ ? tableLink
1409
+ : link && link}
1410
+ {/* Description */}
1411
+
1412
+ {description && config.visualizationType !== 'Spark Line' && (
1413
+ <div className={getChartSubTextClasses().join('')}>{parse(description)}</div>
1414
+ )}
1415
+ {false && <Annotation.List />}
1416
+
1417
+ {/* buttons */}
1418
+ <MediaControls.Section classes={['download-buttons']}>
1419
+ {config.table.showDownloadImgButton && (
1420
+ <MediaControls.Button
1421
+ text='Download Image'
1422
+ title='Download Chart as Image'
1423
+ type='image'
1424
+ state={config}
1425
+ elementToCapture={imageId}
1426
+ />
1427
+ )}
1428
+ {config.table.showDownloadPdfButton && (
1429
+ <MediaControls.Button
1430
+ text='Download PDF'
1431
+ title='Download Chart as PDF'
1432
+ type='pdf'
1433
+ state={config}
1434
+ elementToCapture={imageId}
1435
+ />
1436
+ )}
1437
+ </MediaControls.Section>
1438
+ {/* Data Table */}
1439
+ {((config.xAxis.dataKey &&
1440
+ config.table.show &&
1441
+ config.visualizationType !== 'Spark Line' &&
1442
+ config.visualizationType !== 'Sankey') ||
1443
+ (config.visualizationType === 'Sankey' && config.table.show)) && (
1444
+ <DataTable
1445
+ config={config}
1446
+ rawData={
1447
+ config.visualizationType === 'Sankey'
1448
+ ? config?.data?.[0]?.tableData
1449
+ : config.table.customTableConfig
1450
+ ? filterVizData(config.filters, config.data)
1451
+ : config.data
1452
+ }
1453
+ runtimeData={
1454
+ config.visualizationType === 'Sankey'
1455
+ ? config?.data?.[0]?.tableData
1456
+ : filteredData || excludedData
1457
+ }
1458
+ expandDataTable={config.table.expanded}
1459
+ columns={config.columns}
1460
+ displayDataAsText={displayDataAsText}
1461
+ displayGeoName={displayGeoName}
1462
+ applyLegendToRow={applyLegendToRow}
1463
+ tableTitle={config.table.label}
1464
+ indexTitle={config.table.indexLabel}
1465
+ vizTitle={title}
1466
+ viewport={currentViewport}
1467
+ tabbingId={handleChartTabbing(config, legendId)}
1468
+ colorScale={colorScale}
1469
+ />
1196
1470
  )}
1197
- {/* Sankey */}
1198
- {config.visualizationType === 'Sankey' && <ParentSize aria-hidden='true'>{parent => <SankeyChart runtime={config.runtime} width={parent.width} height={parent.height} />}</ParentSize>}
1199
- {!config.legend.hide && config.visualizationType !== 'Spark Line' && config.visualizationType !== 'Sankey' && <Legend ref={legendRef} skipId={handleChartTabbing(config, legendId)} />}
1471
+ {config?.annotations?.length > 0 && <Annotation.Dropdown />}
1472
+ {/* show pdf or image button */}
1200
1473
  </div>
1201
- {/* Link */}
1202
- {isDashboard && config.table && config.table.show && config.table.showDataTableLink ? tableLink : link && link}
1203
- {/* Description */}
1204
-
1205
- {description && config.visualizationType !== 'Spark Line' && <div className={getChartSubTextClasses().join('')}>{parse(description)}</div>}
1206
- {false && <Annotation.List />}
1207
-
1208
- {/* buttons */}
1209
- <MediaControls.Section classes={['download-buttons']}>
1210
- {config.table.showDownloadImgButton && <MediaControls.Button text='Download Image' title='Download Chart as Image' type='image' state={config} elementToCapture={imageId} />}
1211
- {config.table.showDownloadPdfButton && <MediaControls.Button text='Download PDF' title='Download Chart as PDF' type='pdf' state={config} elementToCapture={imageId} />}
1212
- </MediaControls.Section>
1213
- {/* Data Table */}
1214
- {((config.xAxis.dataKey && config.table.show && config.visualizationType !== 'Spark Line' && config.visualizationType !== 'Sankey') || (config.visualizationType === 'Sankey' && config.table.show)) && (
1215
- <DataTable
1216
- config={config}
1217
- rawData={config.visualizationType === 'Sankey' ? config?.data?.[0]?.tableData : config.table.customTableConfig ? filterVizData(config.filters, config.data) : config.data}
1218
- runtimeData={config.visualizationType === 'Sankey' ? config?.data?.[0]?.tableData : filteredData || excludedData}
1219
- expandDataTable={config.table.expanded}
1220
- columns={config.columns}
1221
- displayDataAsText={displayDataAsText}
1222
- displayGeoName={displayGeoName}
1223
- applyLegendToRow={applyLegendToRow}
1224
- tableTitle={config.table.label}
1225
- indexTitle={config.table.indexLabel}
1226
- vizTitle={title}
1227
- viewport={currentViewport}
1228
- tabbingId={handleChartTabbing(config, legendId)}
1229
- colorScale={colorScale}
1230
- />
1231
- )}
1232
- {config?.annotations?.length > 0 && <Annotation.Dropdown />}
1233
1474
  {config?.footnotes && <section className='footnotes'>{parse(config.footnotes)}</section>}
1234
- {/* show pdf or image button */}
1235
1475
  </div>
1236
1476
  )}
1237
1477
  </Layout.Responsive>
@@ -1239,7 +1479,10 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
1239
1479
  )
1240
1480
  }
1241
1481
 
1242
- const getXAxisData = d => (isDateScale(config.runtime.xAxis) ? parseDate(d[config.runtime.originalXAxis.dataKey]).getTime() : d[config.runtime.originalXAxis.dataKey])
1482
+ const getXAxisData = d =>
1483
+ isDateScale(config.runtime.xAxis)
1484
+ ? parseDate(d[config.runtime.originalXAxis.dataKey]).getTime()
1485
+ : d[config.runtime.originalXAxis.dataKey]
1243
1486
  const getYAxisData = (d, seriesKey) => d[seriesKey]
1244
1487
 
1245
1488
  const capitalize = str => {
@@ -1303,7 +1546,14 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
1303
1546
 
1304
1547
  return (
1305
1548
  <ConfigContext.Provider value={contextValues}>
1306
- <Layout.VisualizationWrapper config={config} isEditor={isEditor} currentViewport={currentViewport} ref={outerContainerRef} imageId={imageId} showEditorPanel={config?.showEditorPanel}>
1549
+ <Layout.VisualizationWrapper
1550
+ config={config}
1551
+ isEditor={isEditor}
1552
+ currentViewport={currentViewport}
1553
+ ref={outerContainerRef}
1554
+ imageId={imageId}
1555
+ showEditorPanel={config?.showEditorPanel}
1556
+ >
1307
1557
  {body}
1308
1558
  </Layout.VisualizationWrapper>
1309
1559
  </ConfigContext.Provider>