@cdc/chart 4.24.7 → 4.24.9-1

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 (53) hide show
  1. package/LICENSE +201 -0
  2. package/dist/cdcchart.js +47567 -42391
  3. package/examples/cases-year.json +13379 -0
  4. package/examples/gallery/bar-chart-vertical/combo-line-chart.json +76 -15
  5. package/examples/gallery/bar-chart-vertical/vertical-bar-chart-stacked.json +5 -5
  6. package/index.html +17 -8
  7. package/package.json +2 -2
  8. package/src/CdcChart.tsx +382 -133
  9. package/src/_stories/Chart.Legend.Gradient.tsx +19 -0
  10. package/src/_stories/_mock/legend.gradient_mock.json +236 -0
  11. package/src/components/Annotations/components/AnnotationDraggable.tsx +64 -11
  12. package/src/components/Axis/Categorical.Axis.tsx +145 -0
  13. package/src/components/BarChart/components/BarChart.Horizontal.tsx +4 -3
  14. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +1 -1
  15. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +2 -5
  16. package/src/components/BarChart/components/BarChart.Vertical.tsx +17 -8
  17. package/src/components/BarChart/helpers/index.ts +5 -16
  18. package/src/components/BrushChart.tsx +205 -0
  19. package/src/components/EditorPanel/EditorPanel.tsx +1767 -510
  20. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +22 -8
  21. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +190 -37
  22. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +43 -7
  23. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +4 -4
  24. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +1 -11
  25. package/src/components/EditorPanel/editor-panel.scss +16 -3
  26. package/src/components/EditorPanel/{useEditorPermissions.js → useEditorPermissions.ts} +90 -19
  27. package/src/components/Legend/Legend.Component.tsx +185 -193
  28. package/src/components/Legend/Legend.Suppression.tsx +146 -0
  29. package/src/components/Legend/Legend.tsx +21 -5
  30. package/src/components/Legend/helpers/index.ts +33 -3
  31. package/src/components/LegendWrapper.tsx +26 -0
  32. package/src/components/LineChart/LineChartProps.ts +1 -18
  33. package/src/components/LineChart/components/LineChart.BumpCircle.tsx +103 -0
  34. package/src/components/LineChart/components/LineChart.Circle.tsx +57 -8
  35. package/src/components/LineChart/helpers.ts +55 -11
  36. package/src/components/LineChart/index.tsx +113 -38
  37. package/src/components/LinearChart.tsx +1366 -0
  38. package/src/components/PieChart/PieChart.tsx +74 -17
  39. package/src/components/Sankey/index.tsx +22 -16
  40. package/src/components/Sparkline/components/SparkLine.tsx +2 -2
  41. package/src/data/initial-state.js +13 -3
  42. package/src/hooks/useLegendClasses.ts +52 -15
  43. package/src/hooks/useMinMax.ts +4 -4
  44. package/src/hooks/useScales.ts +34 -24
  45. package/src/hooks/useTooltip.tsx +85 -22
  46. package/src/scss/DataTable.scss +2 -1
  47. package/src/scss/main.scss +107 -14
  48. package/src/types/ChartConfig.ts +34 -8
  49. package/src/types/ChartContext.ts +5 -4
  50. package/examples/feature/line/line-chart.json +0 -449
  51. package/src/components/BrushHandle.jsx +0 -17
  52. package/src/components/LineChart/index.scss +0 -1
  53. package/src/components/LinearChart.jsx +0 -817
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.orientation === 'horizontal' ? config.yAxis.size : 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,37 @@ 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
+ (!newConfig.filters || newConfig.filters.filter(filter => filter.columnName === seriesKey).length === 0) &&
402
+ (!newConfig.columns || Object.keys(newConfig.columns).indexOf(seriesKey) === -1)
403
+ ) {
404
+ newConfig.runtime.series.push({
405
+ dataKey: seriesKey,
406
+ type: newConfig.dynamicSeriesType,
407
+ lineType: newConfig.dynamicSeriesLineType,
408
+ tooltip: true
409
+ })
410
+ }
411
+ })
412
+ }
413
+ }
414
+
344
415
  if (newConfig.visualizationType === 'Pie') {
345
416
  newConfig.runtime.seriesKeys = (dataOverride || data).map(d => d[newConfig.xAxis.dataKey])
346
417
  newConfig.runtime.seriesLabelsAll = newConfig.runtime.seriesKeys
347
418
  } else {
348
- newConfig.runtime.seriesKeys = newConfig.series
349
- ? newConfig.series.map(series => {
419
+ newConfig.runtime.seriesKeys = newConfig.runtime.series
420
+ ? newConfig.runtime.series.map(series => {
350
421
  newConfig.runtime.seriesLabels[series.dataKey] = series.name || series.label || series.dataKey
351
422
  newConfig.runtime.seriesLabelsAll.push(series.name || series.dataKey)
352
423
  return series.dataKey
@@ -355,8 +426,12 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
355
426
  }
356
427
 
357
428
  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]))
429
+ let allKeys = newExcludedData
430
+ ? newExcludedData.map(d => d[newConfig.xAxis.dataKey])
431
+ : data.map(d => d[newConfig.xAxis.dataKey])
432
+ let allValues = newExcludedData
433
+ ? newExcludedData.map(d => Number(d[newConfig?.series[0]?.dataKey]))
434
+ : data.map(d => Number(d[newConfig?.series[0]?.dataKey]))
360
435
 
361
436
  const uniqueArray = function (arrArg) {
362
437
  return arrArg.filter(function (elem, pos, arr) {
@@ -376,7 +451,9 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
376
451
  if (!g) throw new Error('No groups resolved in box plots')
377
452
 
378
453
  // filter data by group
379
- let filteredData = newExcludedData ? newExcludedData.filter(item => item[newConfig.xAxis.dataKey] === g) : data.filter(item => item[newConfig.xAxis.dataKey] === g)
454
+ let filteredData = newExcludedData
455
+ ? newExcludedData.filter(item => item[newConfig.xAxis.dataKey] === g)
456
+ : data.filter(item => item[newConfig.xAxis.dataKey] === g)
380
457
  let filteredDataValues: number[] = filteredData.map(item => Number(item[newConfig?.series[0]?.dataKey]))
381
458
 
382
459
  // Sort the data for upcoming functions.
@@ -464,7 +541,12 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
464
541
  if (series.type === 'Bar' || series.type === 'Combo') {
465
542
  newConfig.runtime.barSeriesKeys.push(series.dataKey)
466
543
  }
467
- if (series.type === 'Line' || series.type === 'dashed-sm' || series.type === 'dashed-md' || series.type === 'dashed-lg') {
544
+ if (
545
+ series.type === 'Line' ||
546
+ series.type === 'dashed-sm' ||
547
+ series.type === 'dashed-md' ||
548
+ series.type === 'dashed-lg'
549
+ ) {
468
550
  newConfig.runtime.lineSeriesKeys.push(series.dataKey)
469
551
  }
470
552
  if (series.type === 'Combo') {
@@ -491,13 +573,21 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
491
573
  })
492
574
  }
493
575
 
494
- if ((newConfig.visualizationType === 'Bar' && newConfig.orientation === 'horizontal') || ['Deviation Bar', 'Paired Bar', 'Forest Plot'].includes(newConfig.visualizationType)) {
576
+ if (
577
+ (newConfig.visualizationType === 'Bar' && newConfig.orientation === 'horizontal') ||
578
+ ['Deviation Bar', 'Paired Bar', 'Forest Plot'].includes(newConfig.visualizationType)
579
+ ) {
495
580
  newConfig.runtime.xAxis = newConfig.yAxis['yAxis'] ? newConfig.yAxis['yAxis'] : newConfig.yAxis
496
581
  newConfig.runtime.yAxis = newConfig.xAxis['xAxis'] ? newConfig.xAxis['xAxis'] : newConfig.xAxis
497
582
 
498
583
  newConfig.runtime.horizontal = false
499
584
  newConfig.orientation = 'horizontal'
500
- } else if (['Box Plot', 'Scatter Plot', 'Area Chart', 'Line', 'Forecasting'].includes(newConfig.visualizationType) && !checkLineToBarGraph()) {
585
+ // remove after COVE supports categorical axis on horizonatal bars
586
+ newConfig.yAxis.type = newConfig.yAxis.type === 'categorical' ? 'linear' : newConfig.yAxis.type
587
+ } else if (
588
+ ['Box Plot', 'Scatter Plot', 'Area Chart', 'Line', 'Forecasting'].includes(newConfig.visualizationType) &&
589
+ !checkLineToBarGraph()
590
+ ) {
501
591
  newConfig.runtime.xAxis = newConfig.xAxis
502
592
  newConfig.runtime.yAxis = newConfig.yAxis
503
593
  newConfig.runtime.horizontal = false
@@ -509,10 +599,16 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
509
599
  }
510
600
 
511
601
  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.' : ''
602
+ newConfig.runtime.editorErrorMessage =
603
+ newConfig.visualizationType === 'Pie' && !newConfig.yAxis.dataKey
604
+ ? 'Data Key property in Y Axis section must be set for pie charts.'
605
+ : ''
513
606
 
514
607
  // 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.' : ''
608
+ newConfig.runtime.editorErrorMessage =
609
+ newConfig.visualizationType === 'Sankey' && !newConfig.description
610
+ ? 'SUBTEXT/CITATION field is empty: A description of the Sankey Diagram data must be inputted.'
611
+ : ''
516
612
 
517
613
  if (newConfig.legend.seriesHighlight?.length) {
518
614
  setSeriesHighlight(newConfig.legend?.seriesHighlight)
@@ -538,7 +634,10 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
538
634
 
539
635
  // Sorts data series for horizontal bar charts
540
636
  const sortData = (a, b) => {
541
- let sortKey = config.visualizationType === 'Bar' && config.visualizationSubType === 'horizontal' ? config.xAxis.dataKey : config.yAxis.sortKey
637
+ let sortKey =
638
+ config.visualizationType === 'Bar' && config.visualizationSubType === 'horizontal'
639
+ ? config.xAxis.dataKey
640
+ : config.yAxis.sortKey
542
641
  let aData = parseFloat(a[sortKey])
543
642
  let bData = parseFloat(b[sortKey])
544
643
 
@@ -555,15 +654,14 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
555
654
  const resizeObserver = new ResizeObserver(entries => {
556
655
  for (let entry of entries) {
557
656
  let { width, height } = entry.contentRect
558
- let newViewport = getViewport(width)
559
657
  let svgMarginWidth = 32
560
658
  let editorWidth = 350
561
659
 
562
- setCurrentViewport(newViewport)
660
+ width = isEditor ? width - editorWidth : width
563
661
 
564
- if (isEditor) {
565
- width = width - editorWidth
566
- }
662
+ let newViewport = getViewport(width)
663
+
664
+ setCurrentViewport(newViewport)
567
665
 
568
666
  if (entry.target.dataset.lollipop === 'true') {
569
667
  width = width - 2.5
@@ -642,7 +740,12 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
642
740
  }
643
741
  }
644
742
 
645
- if (externalFilters && externalFilters.length > 0 && externalFilters.length > 0 && externalFilters[0].hasOwnProperty('active')) {
743
+ if (
744
+ externalFilters &&
745
+ externalFilters.length > 0 &&
746
+ externalFilters.length > 0 &&
747
+ externalFilters[0].hasOwnProperty('active')
748
+ ) {
646
749
  let newConfigHere = { ...config, filters: externalFilters }
647
750
  setConfig(newConfigHere)
648
751
  setFilteredData(filterVizData(externalFilters, excludedData))
@@ -657,10 +760,25 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
657
760
  }, [configObj.data]) // eslint-disable-line
658
761
  }
659
762
 
763
+ // This will set the bump chart's default scaling type to date-time
764
+ useEffect(() => {
765
+ if (['Bump Chart'].includes(config.visualizationType)) {
766
+ setConfig({
767
+ ...config,
768
+ xAxis: {
769
+ ...config.xAxis,
770
+ type: 'date-time'
771
+ }
772
+ })
773
+ }
774
+ }, [config.visualizationType])
775
+
660
776
  // Generates color palette to pass to child chart component
661
777
  useEffect(() => {
662
778
  if (stateData && config.xAxis && config.runtime?.seriesKeys) {
663
- const configPalette = ['Paired Bar', 'Deviation Bar'].includes(config.visualizationType) ? config.twoColor.palette : config.palette
779
+ const configPalette = ['Paired Bar', 'Deviation Bar'].includes(config.visualizationType)
780
+ ? config.twoColor.palette
781
+ : config.palette
664
782
  const allPalettes: Record<string, string[]> = { ...colorPalettes, ...twoColorPalette }
665
783
  let palette = config.customColors || allPalettes[configPalette]
666
784
  let numberOfKeys = config.runtime.seriesKeys.length
@@ -764,19 +882,6 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
764
882
  return timeFormat(config.tooltips.dateDisplayFormat)(date)
765
883
  }
766
884
 
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
885
  // Format numeric data based on settings in config OR from passed in settings for Additional Columns
781
886
  // - use only for old horizontal data - newer formatNumber is in helper/formatNumber
782
887
  // TODO: we should combine various formatNumber functions across this project.
@@ -795,7 +900,20 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
795
900
 
796
901
  // destructure dataFormat values
797
902
  let {
798
- dataFormat: { commas, abbreviated, roundTo, prefix, suffix, rightRoundTo, bottomRoundTo, rightPrefix, rightSuffix, bottomPrefix, bottomSuffix, bottomAbbreviated }
903
+ dataFormat: {
904
+ commas,
905
+ abbreviated,
906
+ roundTo,
907
+ prefix,
908
+ suffix,
909
+ rightRoundTo,
910
+ bottomRoundTo,
911
+ rightPrefix,
912
+ rightSuffix,
913
+ bottomPrefix,
914
+ bottomSuffix,
915
+ bottomAbbreviated
916
+ }
799
917
  } = config
800
918
 
801
919
  // check if value contains comma and remove it. later will add comma below.
@@ -866,7 +984,10 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
866
984
  // Edge case for small numbers with decimals
867
985
  // - if roundTo undefined which means it is blank, then do not round
868
986
 
869
- if ((axis === 'left' && commas && abbreviated && shouldAbbreviate) || (axis === 'bottom' && commas && abbreviated && shouldAbbreviate)) {
987
+ if (
988
+ (axis === 'left' && commas && abbreviated && shouldAbbreviate) ||
989
+ (axis === 'bottom' && commas && abbreviated && shouldAbbreviate)
990
+ ) {
870
991
  num = num // eslint-disable-line
871
992
  } else {
872
993
  num = num.toLocaleString('en-US', stringFormattingOptions)
@@ -922,21 +1043,6 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
922
1043
  return String(result)
923
1044
  }
924
1045
 
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
1046
  const missingRequiredSections = () => {
941
1047
  if (config.visualizationType === 'Sankey') return false // skip checks for now
942
1048
  if (config.visualizationType === 'Forecasting') return false // skip required checks for now.
@@ -946,7 +1052,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
946
1052
  return true
947
1053
  }
948
1054
  } else {
949
- if (undefined === config?.series || false === config?.series.length > 0) {
1055
+ if ((undefined === config?.series || false === config?.series.length > 0) && !config?.dynamicSeries) {
950
1056
  return true
951
1057
  }
952
1058
  }
@@ -1053,7 +1159,12 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
1053
1159
  <section className='waiting-container'>
1054
1160
  <h3>Finish Configuring</h3>
1055
1161
  <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)}>
1162
+ <Button
1163
+ className='btn'
1164
+ style={{ margin: '1em auto' }}
1165
+ disabled={missingRequiredSections()}
1166
+ onClick={e => confirmDone(e)}
1167
+ >
1057
1168
  I'm Done
1058
1169
  </Button>
1059
1170
  </section>
@@ -1125,11 +1236,18 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
1125
1236
  const getChartWrapperClasses = () => {
1126
1237
  const isLegendOnBottom = legend?.position === 'bottom' || ['sm', 'xs', 'xxs'].includes(currentViewport)
1127
1238
  const classes = ['chart-container', 'p-relative']
1128
- if (config.legend?.position === 'bottom') classes.push('bottom')
1129
- if (config.legend?.hide) classes.push('legend-hidden')
1239
+ if (legend?.position) {
1240
+ if (['sm', 'xs', 'xxs'].includes(currentViewport) && legend?.position !== 'top') {
1241
+ classes.push('legend-bottom')
1242
+ } else {
1243
+ classes.push(`legend-${legend.position}`)
1244
+ }
1245
+ }
1246
+ if (legend?.hide) classes.push('legend-hidden')
1130
1247
  if (lineDatapointClass) classes.push(lineDatapointClass)
1131
1248
  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')
1249
+ if (config.brush?.active && dashboardConfig?.type === 'dashboard' && (!isLegendOnBottom || legend.hide))
1250
+ classes.push('dashboard-brush')
1133
1251
  classes.push(...contentClasses)
1134
1252
  return classes
1135
1253
  }
@@ -1157,81 +1275,202 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
1157
1275
  {config.newViz && <Confirm />}
1158
1276
  {undefined === config.newViz && isEditor && config.runtime && config.runtime?.editorErrorMessage && <Error />}
1159
1277
  {!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} />
1278
+ <div
1279
+ className={`cdc-chart-inner-container cove-component__content type-${makeClassName(
1280
+ config.visualizationType
1281
+ )}`}
1282
+ aria-label={handleChartAriaLabels(config)}
1283
+ tabIndex={0}
1284
+ >
1285
+ <Title
1286
+ showTitle={config.showTitle}
1287
+ isDashboard={isDashboard}
1288
+ title={title}
1289
+ superTitle={config.superTitle}
1290
+ classes={['chart-title', `${config.theme}`, 'cove-component__header']}
1291
+ style={undefined}
1292
+ />
1293
+ {/* Intro Text/Message */}
1294
+ {config?.introText && config.visualizationType !== 'Spark Line' && (
1295
+ <section
1296
+ className={`introText legend_${config.legend.hide ? 'hidden' : 'visible'}_${config.legend.position} `}
1297
+ >
1298
+ {parse(config.introText)}
1299
+ </section>
1300
+ )}
1162
1301
 
1163
1302
  {/* Filters */}
1164
- {config.filters && !externalFilters && config.visualizationType !== 'Spark Line' && <Filters config={config} setConfig={setConfig} setFilteredData={setFilteredData} filteredData={filteredData} excludedData={excludedData} filterData={filterVizData} dimensions={dimensions} />}
1303
+ {config.filters && !externalFilters && config.visualizationType !== 'Spark Line' && (
1304
+ <Filters
1305
+ config={config}
1306
+ setConfig={setConfig}
1307
+ setFilteredData={setFilteredData}
1308
+ filteredData={filteredData}
1309
+ excludedData={excludedData}
1310
+ filterData={filterVizData}
1311
+ dimensions={dimensions}
1312
+ />
1313
+ )}
1165
1314
  <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>}
1315
+ {config.annotations?.length > 0 && (
1316
+ <SkipTo
1317
+ skipId={handleChartTabbing(config, legendId)}
1318
+ skipMessage={`Skip over annotations`}
1319
+ key={`skip-annotations`}
1320
+ />
1321
+ )}
1170
1322
 
1323
+ {/* Visualization Wrapper */}
1171
1324
  <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)}
1325
+ <LegendWrapper>
1326
+ <div
1327
+ className={
1328
+ legend.hide || ['xxs', 'xs', 'sm'].includes(currentViewport)
1329
+ ? 'w-100'
1330
+ : legend.position === 'bottom' || legend.position === 'top' || visualizationType === 'Sankey'
1331
+ ? 'w-100'
1332
+ : 'w-75'
1333
+ }
1334
+ >
1335
+ {/* All charts with LinearChart */}
1336
+ {!['Spark Line', 'Line', 'Sankey', 'Pie', 'Sankey'].includes(config.visualizationType) && (
1337
+ <div style={{ height, width: `100%` }}>
1338
+ <ParentSize>
1339
+ {parent => <LinearChart parentWidth={parent.width} parentHeight={parent.height} />}
1340
+ </ParentSize>
1193
1341
  </div>
1194
1342
  )}
1195
- </>
1343
+
1344
+ {config.visualizationType === 'Pie' && (
1345
+ <ParentSize className='justify-content-center d-flex' style={{ height, width: `100%` }}>
1346
+ {parent => <PieChart parentWidth={parent.width} parentHeight={parent.height} />}
1347
+ </ParentSize>
1348
+ )}
1349
+ {/* Line Chart */}
1350
+ {config.visualizationType === 'Line' &&
1351
+ (checkLineToBarGraph() ? (
1352
+ <div style={{ height: config?.heights?.vertical, width: `100%` }}>
1353
+ <ParentSize>
1354
+ {parent => <LinearChart parentWidth={parent.width} parentHeight={parent.height} />}
1355
+ </ParentSize>
1356
+ </div>
1357
+ ) : (
1358
+ <div style={{ height, width: `100%` }}>
1359
+ <ParentSize>
1360
+ {parent => <LinearChart parentWidth={parent.width} parentHeight={parent.height} />}
1361
+ </ParentSize>
1362
+ </div>
1363
+ ))}
1364
+ {/* Sparkline */}
1365
+ {config.visualizationType === 'Spark Line' && (
1366
+ <>
1367
+ <Filters
1368
+ config={config}
1369
+ setConfig={setConfig}
1370
+ setFilteredData={setFilteredData}
1371
+ filteredData={filteredData}
1372
+ excludedData={excludedData}
1373
+ filterData={filterVizData}
1374
+ dimensions={dimensions}
1375
+ />
1376
+ {config?.introText && (
1377
+ <section className='introText' style={{ padding: '0px 0 35px' }}>
1378
+ {parse(config.introText)}
1379
+ </section>
1380
+ )}
1381
+ <div style={{ height: `100px`, width: `100%`, ...sparkLineStyles }}>
1382
+ <ParentSize>{parent => <SparkLine width={parent.width} height={parent.height} />}</ParentSize>
1383
+ </div>
1384
+ {description && (
1385
+ <div className='subtext' style={{ padding: '35px 0 15px' }}>
1386
+ {parse(description)}
1387
+ </div>
1388
+ )}
1389
+ </>
1390
+ )}
1391
+ {/* Sankey */}
1392
+ {config.visualizationType === 'Sankey' && (
1393
+ <ParentSize aria-hidden='true'>
1394
+ {parent => <SankeyChart runtime={config.runtime} width={parent.width} height={parent.height} />}
1395
+ </ParentSize>
1396
+ )}
1397
+ </div>
1398
+ {/* Legend */}
1399
+ {!config.legend.hide &&
1400
+ config.visualizationType !== 'Spark Line' &&
1401
+ config.visualizationType !== 'Sankey' && (
1402
+ <Legend ref={legendRef} skipId={handleChartTabbing(config, legendId)} />
1403
+ )}
1404
+ </LegendWrapper>
1405
+ {/* Link */}
1406
+ {isDashboard && config.table && config.table.show && config.table.showDataTableLink
1407
+ ? tableLink
1408
+ : link && link}
1409
+ {/* Description */}
1410
+
1411
+ {description && config.visualizationType !== 'Spark Line' && (
1412
+ <div className={getChartSubTextClasses().join('')}>{parse(description)}</div>
1413
+ )}
1414
+ {false && <Annotation.List />}
1415
+
1416
+ {/* buttons */}
1417
+ <MediaControls.Section classes={['download-buttons']}>
1418
+ {config.table.showDownloadImgButton && (
1419
+ <MediaControls.Button
1420
+ text='Download Image'
1421
+ title='Download Chart as Image'
1422
+ type='image'
1423
+ state={config}
1424
+ elementToCapture={imageId}
1425
+ />
1426
+ )}
1427
+ {config.table.showDownloadPdfButton && (
1428
+ <MediaControls.Button
1429
+ text='Download PDF'
1430
+ title='Download Chart as PDF'
1431
+ type='pdf'
1432
+ state={config}
1433
+ elementToCapture={imageId}
1434
+ />
1435
+ )}
1436
+ </MediaControls.Section>
1437
+ {/* Data Table */}
1438
+ {((config.xAxis.dataKey &&
1439
+ config.table.show &&
1440
+ config.visualizationType !== 'Spark Line' &&
1441
+ config.visualizationType !== 'Sankey') ||
1442
+ (config.visualizationType === 'Sankey' && config.table.show)) && (
1443
+ <DataTable
1444
+ config={config}
1445
+ rawData={
1446
+ config.visualizationType === 'Sankey'
1447
+ ? config?.data?.[0]?.tableData
1448
+ : config.table.customTableConfig
1449
+ ? filterVizData(config.filters, config.data)
1450
+ : config.data
1451
+ }
1452
+ runtimeData={
1453
+ config.visualizationType === 'Sankey'
1454
+ ? config?.data?.[0]?.tableData
1455
+ : filteredData || excludedData
1456
+ }
1457
+ expandDataTable={config.table.expanded}
1458
+ columns={config.columns}
1459
+ displayDataAsText={displayDataAsText}
1460
+ displayGeoName={displayGeoName}
1461
+ applyLegendToRow={applyLegendToRow}
1462
+ tableTitle={config.table.label}
1463
+ indexTitle={config.table.indexLabel}
1464
+ vizTitle={title}
1465
+ viewport={currentViewport}
1466
+ tabbingId={handleChartTabbing(config, legendId)}
1467
+ colorScale={colorScale}
1468
+ />
1196
1469
  )}
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)} />}
1470
+ {config?.annotations?.length > 0 && <Annotation.Dropdown />}
1471
+ {/* show pdf or image button */}
1200
1472
  </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
1473
  {config?.footnotes && <section className='footnotes'>{parse(config.footnotes)}</section>}
1234
- {/* show pdf or image button */}
1235
1474
  </div>
1236
1475
  )}
1237
1476
  </Layout.Responsive>
@@ -1239,7 +1478,10 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
1239
1478
  )
1240
1479
  }
1241
1480
 
1242
- const getXAxisData = d => (isDateScale(config.runtime.xAxis) ? parseDate(d[config.runtime.originalXAxis.dataKey]).getTime() : d[config.runtime.originalXAxis.dataKey])
1481
+ const getXAxisData = d =>
1482
+ isDateScale(config.runtime.xAxis)
1483
+ ? parseDate(d[config.runtime.originalXAxis.dataKey]).getTime()
1484
+ : d[config.runtime.originalXAxis.dataKey]
1243
1485
  const getYAxisData = (d, seriesKey) => d[seriesKey]
1244
1486
 
1245
1487
  const capitalize = str => {
@@ -1303,7 +1545,14 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
1303
1545
 
1304
1546
  return (
1305
1547
  <ConfigContext.Provider value={contextValues}>
1306
- <Layout.VisualizationWrapper config={config} isEditor={isEditor} currentViewport={currentViewport} ref={outerContainerRef} imageId={imageId} showEditorPanel={config?.showEditorPanel}>
1548
+ <Layout.VisualizationWrapper
1549
+ config={config}
1550
+ isEditor={isEditor}
1551
+ currentViewport={currentViewport}
1552
+ ref={outerContainerRef}
1553
+ imageId={imageId}
1554
+ showEditorPanel={config?.showEditorPanel}
1555
+ >
1307
1556
  {body}
1308
1557
  </Layout.VisualizationWrapper>
1309
1558
  </ConfigContext.Provider>