@cdc/chart 4.25.10 → 4.26.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 (135) hide show
  1. package/dist/{cdcchart-1a1724a1.es.js → cdcchart-dgT_1dIT.es.js} +136 -151
  2. package/dist/cdcchart.js +44003 -43518
  3. package/examples/feature/__data__/planet-example-data.json +1 -1
  4. package/examples/feature/boxplot/valid-boxplot.csv +38 -17
  5. package/examples/feature/pie/planet-pie-example-config.json +48 -2
  6. package/examples/private/DEV-11825.json +573 -0
  7. package/examples/private/DEV-12100.json +1303 -0
  8. package/examples/private/cat-y.json +1235 -0
  9. package/examples/private/data-points.json +228 -0
  10. package/examples/private/height.json +3915 -0
  11. package/examples/private/links.json +569 -0
  12. package/examples/private/na.json +913 -0
  13. package/examples/private/quadrant.txt +30 -0
  14. package/examples/private/test-data.csv +28 -0
  15. package/examples/private/test-forecast.json +5510 -0
  16. package/examples/private/warming-stripe-test.json +2578 -0
  17. package/examples/private/warming-stripes.json +4763 -0
  18. package/examples/tech-adoption-with-links.json +560 -0
  19. package/index.html +16 -140
  20. package/package.json +6 -5
  21. package/preview.html +1616 -0
  22. package/src/CdcChart.tsx +8 -11
  23. package/src/CdcChartComponent.tsx +329 -124
  24. package/src/_stories/Chart.Combo.stories.tsx +18 -0
  25. package/src/_stories/Chart.Forecast.stories.tsx +36 -0
  26. package/src/_stories/Chart.HTMLInDataTable.stories.tsx +520 -0
  27. package/src/_stories/Chart.Patterns.stories.tsx +2 -1
  28. package/src/_stories/Chart.PreserveDecimals.stories.tsx +220 -0
  29. package/src/_stories/Chart.Regions.Categorical.stories.tsx +148 -0
  30. package/src/_stories/Chart.Regions.DateScale.stories.tsx +197 -0
  31. package/src/_stories/Chart.Regions.DateTimeScale.stories.tsx +297 -0
  32. package/src/_stories/Chart.SmallMultiples.stories.tsx +47 -0
  33. package/src/_stories/Chart.stories.tsx +8 -0
  34. package/src/_stories/ChartAnnotation.stories.tsx +6 -3
  35. package/src/_stories/ChartBar.Editor.stories.tsx +3585 -0
  36. package/src/_stories/ChartBrush.Editor.stories.tsx +295 -0
  37. package/src/_stories/ChartBrush.stories.tsx +50 -0
  38. package/src/_stories/ChartEditor.Editor.stories.tsx +656 -0
  39. package/src/_stories/ChartEditor.stories.tsx +1 -2
  40. package/src/_stories/TechAdoptionWithLinks.stories.tsx +27 -0
  41. package/src/_stories/_mock/brush_enabled.json +326 -0
  42. package/src/_stories/_mock/brush_mock.json +2 -69
  43. package/src/_stories/_mock/combo.json +451 -0
  44. package/src/_stories/_mock/editor-test-configs.json +376 -0
  45. package/src/_stories/_mock/editor-test-datasets.json +477 -0
  46. package/src/_stories/_mock/editor-tests/bar-chart-editor-test.json +255 -0
  47. package/src/_stories/_mock/editor-tests/bar-chart-general-test.json +267 -0
  48. package/src/_stories/_mock/editor-tests/bar-chart-test.json +237 -0
  49. package/src/_stories/_mock/forecast_combo_with_gaps.json +913 -0
  50. package/src/_stories/_mock/horizontal-bars-dynamic-y-axis.json +413 -0
  51. package/src/_stories/_mock/pie_config.json +257 -62
  52. package/src/_stories/_mock/small_multiples/small_multiples_bars.json +1944 -0
  53. package/src/_stories/_mock/small_multiples/small_multiples_big_data_bars.json +1114 -0
  54. package/src/_stories/_mock/small_multiples/small_multiples_lines.json +2646 -0
  55. package/src/_stories/_mock/small_multiples/small_multiples_lines_colors.json +1305 -0
  56. package/src/_stories/_mock/small_multiples/small_multiples_stacked_bars.json +1936 -0
  57. package/src/components/Annotations/components/findNearestDatum.ts +6 -41
  58. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +10 -7
  59. package/src/components/AreaChart/index.tsx +1 -2
  60. package/src/components/Axis/Categorical.Axis.tsx +6 -7
  61. package/src/components/BarChart/components/BarChart.Horizontal.tsx +181 -27
  62. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +3 -1
  63. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +1 -0
  64. package/src/components/BarChart/components/BarChart.Vertical.tsx +8 -9
  65. package/src/components/BarChart/components/context.tsx +1 -0
  66. package/src/components/BarChart/helpers/useBarChart.ts +14 -2
  67. package/src/components/BoxPlot/helpers/index.ts +3 -3
  68. package/src/components/Brush/BrushSelector.tsx +1258 -0
  69. package/src/components/Brush/MiniChartPreview.tsx +283 -0
  70. package/src/components/DeviationBar.jsx +9 -7
  71. package/src/components/EditorPanel/EditorPanel.tsx +2720 -2586
  72. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +96 -111
  73. package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +56 -34
  74. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +76 -31
  75. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +104 -55
  76. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +54 -49
  77. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +427 -0
  78. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +96 -48
  79. package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
  80. package/src/components/EditorPanel/editor-panel.scss +0 -20
  81. package/src/components/EditorPanel/useEditorPermissions.ts +36 -31
  82. package/src/components/Forecasting/Forecasting.tsx +139 -21
  83. package/src/components/Legend/Legend.Component.tsx +16 -9
  84. package/src/components/Legend/Legend.tsx +3 -2
  85. package/src/components/Legend/helpers/createFormatLabels.tsx +325 -176
  86. package/src/components/Legend/helpers/getLegendClasses.ts +0 -1
  87. package/src/components/Legend/helpers/index.ts +10 -6
  88. package/src/components/LineChart/LineChartProps.ts +0 -3
  89. package/src/components/LineChart/helpers.ts +1 -1
  90. package/src/components/LineChart/index.tsx +36 -13
  91. package/src/components/LinearChart.tsx +559 -499
  92. package/src/components/PairedBarChart.jsx +20 -3
  93. package/src/components/Regions/components/Regions.tsx +366 -144
  94. package/src/components/Sankey/types/index.ts +1 -1
  95. package/src/components/ScatterPlot/ScatterPlot.jsx +2 -2
  96. package/src/components/SmallMultiples/SmallMultipleTile.tsx +202 -0
  97. package/src/components/SmallMultiples/SmallMultiples.css +32 -0
  98. package/src/components/SmallMultiples/SmallMultiples.tsx +271 -0
  99. package/src/components/SmallMultiples/index.ts +2 -0
  100. package/src/components/WarmingStripes/WarmingStripes.tsx +160 -0
  101. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +35 -0
  102. package/src/components/WarmingStripes/WarmingStripesGradientLegend.tsx +104 -0
  103. package/src/components/WarmingStripes/index.tsx +3 -0
  104. package/src/data/initial-state.js +16 -2
  105. package/src/helpers/buildForecastPaletteOptions.ts +0 -38
  106. package/src/helpers/calculateHorizontalBarCategoryLabelWidth.ts +57 -0
  107. package/src/helpers/getColorScale.ts +10 -0
  108. package/src/{hooks/useMinMax.ts → helpers/getMinMax.ts} +26 -14
  109. package/src/helpers/getYAxisAutoPadding.ts +53 -0
  110. package/src/helpers/sizeHelpers.ts +0 -20
  111. package/src/helpers/smallMultiplesHelpers.ts +529 -0
  112. package/src/hooks/useChartHoverAnalytics.tsx +10 -9
  113. package/src/hooks/useProgrammaticTooltip.ts +96 -0
  114. package/src/hooks/useScales.ts +98 -34
  115. package/src/hooks/useSmallMultipleSynchronization.ts +59 -0
  116. package/src/hooks/useTooltip.tsx +91 -25
  117. package/src/scss/DataTable.scss +0 -4
  118. package/src/scss/main.scss +18 -83
  119. package/src/store/chart.actions.ts +2 -0
  120. package/src/store/chart.reducer.ts +4 -0
  121. package/src/test/CdcChart.test.jsx +1 -1
  122. package/src/types/ChartConfig.ts +27 -6
  123. package/src/types/ChartContext.ts +3 -0
  124. package/src/types/Label.ts +1 -0
  125. package/src/utils/analyticsTracking.ts +19 -0
  126. package/LICENSE +0 -201
  127. package/src/_stories/_mock/pie_data.json +0 -218
  128. package/src/components/AreaChart/components/AreaChart.jsx +0 -109
  129. package/src/components/Brush/BrushChart.tsx +0 -128
  130. package/src/components/Brush/BrushController.tsx +0 -71
  131. package/src/components/Brush/types.tsx +0 -8
  132. package/src/components/BrushChart.tsx +0 -223
  133. package/src/helpers/sort.ts +0 -7
  134. package/src/hooks/useActiveElement.js +0 -19
  135. package/src/hooks/useChartClasses.js +0 -41
@@ -37,6 +37,7 @@ import { filterChartColorPalettes } from '@cdc/core/helpers/filterColorPalettes'
37
37
 
38
38
  import SparkLine from './components/Sparkline'
39
39
  import Legend from './components/Legend'
40
+ import WarmingStripesGradientLegend from './components/WarmingStripes/WarmingStripesGradientLegend'
40
41
  import defaults from './data/initial-state'
41
42
  import EditorPanel from './components/EditorPanel'
42
43
  import { abbreviateNumber } from './helpers/abbreviateNumber'
@@ -75,6 +76,7 @@ import { getExcludedData } from './helpers/getExcludedData'
75
76
  import { getColorScale } from './helpers/getColorScale'
76
77
  import { getTransformedData } from './helpers/getTransformedData'
77
78
  import { getPiePercent } from './helpers/getPiePercent'
79
+ import { prepareSmallMultiplesDataTable } from './helpers/smallMultiplesHelpers'
78
80
 
79
81
  // styles
80
82
  import './scss/main.scss'
@@ -238,6 +240,14 @@ const CdcChart: React.FC<CdcChartProps> = ({
238
240
 
239
241
  const convertLineToBarGraph = isConvertLineToBarGraph(config, filteredData)
240
242
 
243
+ // Declaratively calculate series keys for pie charts based on filtered data
244
+ const pieSeriesKeys = useMemo(() => {
245
+ if (config.visualizationType !== 'Pie' || !config.xAxis?.dataKey) return null
246
+ const data = filteredData?.length > 0 ? filteredData : excludedData
247
+ if (!data) return null
248
+ return _.uniq(data.map(d => d[config.xAxis.dataKey]))
249
+ }, [config.visualizationType, config.xAxis?.dataKey, filteredData, excludedData])
250
+
241
251
  const prepareConfig = (loadedConfig: ChartConfig) => {
242
252
  // Create defaults without version to avoid overriding legacy configs
243
253
  const defaultsWithoutPalette = { ...defaults }
@@ -275,30 +285,62 @@ const CdcChart: React.FC<CdcChartProps> = ({
275
285
  return newConfig
276
286
  }
277
287
 
288
+ const getProcessedAxisLabels = useCallback(
289
+ (targetConfig: AllChartsConfig, dataSource: any[] = []) => {
290
+ let processedXAxis = targetConfig.xAxis?.label
291
+ let processedYAxis = targetConfig.yAxis?.label
292
+
293
+ if (targetConfig.enableMarkupVariables && targetConfig.markupVariables?.length) {
294
+ if (targetConfig.xAxis?.label) {
295
+ processedXAxis = processMarkupVariables(
296
+ targetConfig.xAxis.label,
297
+ dataSource || [],
298
+ targetConfig.markupVariables,
299
+ {
300
+ isEditor,
301
+ filters: targetConfig.filters || []
302
+ }
303
+ ).processedContent
304
+ }
305
+ if (targetConfig.yAxis?.label) {
306
+ processedYAxis = processMarkupVariables(
307
+ targetConfig.yAxis.label,
308
+ dataSource || [],
309
+ targetConfig.markupVariables,
310
+ {
311
+ isEditor,
312
+ filters: targetConfig.filters || []
313
+ }
314
+ ).processedContent
315
+ }
316
+ }
317
+
318
+ const isHorizontalVariant =
319
+ ((targetConfig.visualizationType === 'Bar' || targetConfig.visualizationType === 'Box Plot') &&
320
+ targetConfig.orientation === 'horizontal') ||
321
+ ['Deviation Bar', 'Paired Bar', 'Forest Plot'].includes(targetConfig.visualizationType)
322
+
323
+ const runtimeXAxisLabel = isHorizontalVariant
324
+ ? processedYAxis ?? (targetConfig.yAxis as any)?.yAxis?.label ?? targetConfig.yAxis?.label
325
+ : processedXAxis ?? targetConfig.xAxis?.label
326
+
327
+ const runtimeYAxisLabel = isHorizontalVariant
328
+ ? processedXAxis ?? (targetConfig.xAxis as any)?.xAxis?.label ?? targetConfig.xAxis?.label
329
+ : processedYAxis ?? targetConfig.yAxis?.label
330
+
331
+ return { processedXAxis, processedYAxis, runtimeXAxisLabel, runtimeYAxisLabel, isHorizontalVariant }
332
+ },
333
+ [isEditor]
334
+ )
335
+
278
336
  const updateConfig = (_config: AllChartsConfig, dataOverride?: any[]) => {
279
337
  const newConfig = cloneConfig(_config)
280
338
  let data = dataOverride || stateData
281
339
 
282
340
  data = handleRankByValue(data, newConfig)
283
341
 
284
- // Process axis labels for markup variables if enabled
285
- let processedXAxis = newConfig.xAxis?.label
286
- let processedYAxis = newConfig.yAxis?.label
287
-
288
- if (newConfig.enableMarkupVariables && newConfig.markupVariables?.length) {
289
- if (newConfig.xAxis?.label) {
290
- processedXAxis = processMarkupVariables(newConfig.xAxis.label, data || [], newConfig.markupVariables, {
291
- isEditor,
292
- filters: newConfig.filters || []
293
- }).processedContent
294
- }
295
- if (newConfig.yAxis?.label) {
296
- processedYAxis = processMarkupVariables(newConfig.yAxis.label, data || [], newConfig.markupVariables, {
297
- isEditor,
298
- filters: newConfig.filters || []
299
- }).processedContent
300
- }
301
- }
342
+ const { processedXAxis, processedYAxis, runtimeXAxisLabel, runtimeYAxisLabel, isHorizontalVariant } =
343
+ getProcessedAxisLabels(newConfig, data || [])
302
344
 
303
345
  // Deeper copy
304
346
  Object.keys(defaults).forEach(key => {
@@ -323,6 +365,14 @@ const CdcChart: React.FC<CdcChartProps> = ({
323
365
  }
324
366
 
325
367
  //Enforce default values that need to be calculated at runtime
368
+ // Preserve error messages that were set outside of updateConfig (e.g., from pattern settings)
369
+ const existingErrorMessage = _config.runtime?.editorErrorMessage || ''
370
+ const isPieChartValidationError =
371
+ existingErrorMessage === 'Data column section must be set for pie charts.' ||
372
+ existingErrorMessage === 'Segment labels section must be set for pie charts.' ||
373
+ existingErrorMessage === 'Data column and Segment labels sections must be set for pie charts.'
374
+ const shouldPreserveError = existingErrorMessage && !isPieChartValidationError
375
+
326
376
  newConfig.runtime = {} as Runtime
327
377
  newConfig.runtime.series = _.cloneDeep(newConfig.series)
328
378
  newConfig.runtime.seriesLabels = {}
@@ -334,6 +384,7 @@ const CdcChart: React.FC<CdcChartProps> = ({
334
384
  const pieData = currentData.length > 0 ? currentData : newExcludedData
335
385
  newConfig.runtime.seriesKeys = _.uniq(pieData.map(d => d[newConfig.xAxis.dataKey]))
336
386
  newConfig.runtime.seriesLabelsAll = newConfig.runtime.seriesKeys
387
+ newConfig.runtime.isPieChart = true // Flag to know when to use derived keys
337
388
  } else {
338
389
  const finalData = dataOverride || newConfig.formattedData || newConfig.data
339
390
  newConfig.runtime.seriesKeys = (newConfig.runtime.series || []).flatMap(series => {
@@ -384,6 +435,18 @@ const CdcChart: React.FC<CdcChartProps> = ({
384
435
  newConfig.runtime.forecastingSeriesKeys.push(series)
385
436
  }
386
437
  })
438
+
439
+ // Default to date scaling type for Forecasting charts
440
+ if (newConfig.xAxis.type === 'categorical') {
441
+ newConfig.xAxis.type = 'date'
442
+ // Initialize date parsing formats if they don't exist
443
+ if (!newConfig.xAxis.dateParseFormat) {
444
+ newConfig.xAxis.dateParseFormat = '%Y-%m-%d'
445
+ }
446
+ if (!newConfig.xAxis.dateDisplayFormat) {
447
+ newConfig.xAxis.dateDisplayFormat = '%Y-%m-%d'
448
+ }
449
+ }
387
450
  }
388
451
 
389
452
  if (newConfig.visualizationType === 'Area Chart' && newConfig.series) {
@@ -395,19 +458,17 @@ const CdcChart: React.FC<CdcChartProps> = ({
395
458
  newConfig.visualizationSubType = 'stacked'
396
459
  }
397
460
 
398
- if (
399
- ((newConfig.visualizationType === 'Bar' || newConfig.visualizationType === 'Box Plot') &&
400
- newConfig.orientation === 'horizontal') ||
401
- ['Deviation Bar', 'Paired Bar', 'Forest Plot'].includes(newConfig.visualizationType)
402
- ) {
461
+ if (isHorizontalVariant) {
403
462
  // For horizontal charts, axes are swapped, so processedYAxis goes to runtime.xAxis and vice versa
463
+ const horizontalXAxisSource = _.cloneDeep((newConfig.yAxis as any)?.yAxis || newConfig.yAxis)
464
+ const horizontalYAxisSource = _.cloneDeep((newConfig.xAxis as any)?.xAxis || newConfig.xAxis)
404
465
  newConfig.runtime.xAxis = {
405
- ..._.cloneDeep(newConfig.yAxis.yAxis || newConfig.yAxis),
406
- label: processedYAxis || (newConfig.yAxis.yAxis || newConfig.yAxis).label
466
+ ...horizontalXAxisSource,
467
+ label: runtimeXAxisLabel ?? horizontalXAxisSource?.label
407
468
  }
408
469
  newConfig.runtime.yAxis = {
409
- ..._.cloneDeep(newConfig.xAxis.xAxis || newConfig.xAxis),
410
- label: processedXAxis || (newConfig.xAxis.xAxis || newConfig.xAxis).label
470
+ ...horizontalYAxisSource,
471
+ label: runtimeYAxisLabel ?? horizontalYAxisSource?.label
411
472
  }
412
473
  newConfig.runtime.yAxis.labelOffset *= -1
413
474
 
@@ -419,24 +480,40 @@ const CdcChart: React.FC<CdcChartProps> = ({
419
480
  ['Scatter Plot', 'Area Chart', 'Line', 'Forecasting'].includes(newConfig.visualizationType) &&
420
481
  !convertLineToBarGraph
421
482
  ) {
422
- newConfig.runtime.xAxis = { ...newConfig.xAxis, label: processedXAxis || newConfig.xAxis.label }
423
- newConfig.runtime.yAxis = { ...newConfig.yAxis, label: processedYAxis || newConfig.yAxis.label }
483
+ newConfig.runtime.xAxis = { ...newConfig.xAxis, label: runtimeXAxisLabel ?? newConfig.xAxis.label }
484
+ newConfig.runtime.yAxis = { ...newConfig.yAxis, label: runtimeYAxisLabel ?? newConfig.yAxis.label }
424
485
  newConfig.runtime.horizontal = false
425
486
  newConfig.orientation = 'vertical'
426
487
  } else {
427
- newConfig.runtime.xAxis = { ...newConfig.xAxis, label: processedXAxis || newConfig.xAxis.label }
428
- newConfig.runtime.yAxis = { ...newConfig.yAxis, label: processedYAxis || newConfig.yAxis.label }
488
+ newConfig.runtime.xAxis = { ...newConfig.xAxis, label: runtimeXAxisLabel ?? newConfig.xAxis.label }
489
+ newConfig.runtime.yAxis = { ...newConfig.yAxis, label: runtimeYAxisLabel ?? newConfig.yAxis.label }
429
490
  newConfig.runtime.horizontal = false
430
491
  }
431
492
 
432
493
  newConfig.runtime.uniqueId = Date.now()
433
- newConfig.runtime.editorErrorMessage =
434
- newConfig.visualizationType === 'Pie' && !newConfig.yAxis.dataKey
435
- ? 'Data Key property in Y Axis section must be set for pie charts.'
436
- : ''
437
494
 
438
- // Sankey Description box error message
439
- newConfig.runtime.editorErrorMessage = ''
495
+ // Set error messages: preserve external errors (from pattern settings, etc.) or set validation errors
496
+ if (shouldPreserveError) {
497
+ // Preserve error messages set by editor panels (e.g., pattern contrast errors)
498
+ newConfig.runtime.editorErrorMessage = existingErrorMessage
499
+ } else if (newConfig.visualizationType === 'Pie') {
500
+ // Check for Pie chart validation errors
501
+ const missingDataColumn = !newConfig.yAxis.dataKey || newConfig.yAxis.dataKey === ''
502
+ const missingSegmentLabels = !newConfig.xAxis.dataKey || newConfig.xAxis.dataKey === ''
503
+
504
+ if (missingDataColumn && missingSegmentLabels) {
505
+ newConfig.runtime.editorErrorMessage = 'Data column and Segment labels sections must be set for pie charts.'
506
+ } else if (missingDataColumn) {
507
+ newConfig.runtime.editorErrorMessage = 'Data column section must be set for pie charts.'
508
+ } else if (missingSegmentLabels) {
509
+ newConfig.runtime.editorErrorMessage = 'Segment labels section must be set for pie charts.'
510
+ } else {
511
+ newConfig.runtime.editorErrorMessage = ''
512
+ }
513
+ } else {
514
+ // No errors
515
+ newConfig.runtime.editorErrorMessage = ''
516
+ }
440
517
 
441
518
  if (newConfig.legend.seriesHighlight?.length) {
442
519
  dispatch({ type: 'SET_SERIES_HIGHLIGHT', payload: newConfig.legend?.seriesHighlight })
@@ -492,11 +569,13 @@ const CdcChart: React.FC<CdcChartProps> = ({
492
569
  for (let entry of entries) {
493
570
  let { width, height } = entry.contentRect
494
571
 
495
- width = isEditor ? width - EDITOR_WIDTH : width
572
+ const editorIsOpen = isEditor
573
+ width = editorIsOpen ? width - EDITOR_WIDTH : width
496
574
 
497
575
  const newViewport = getViewport(width)
498
576
 
499
577
  dispatch({ type: 'SET_VIEWPORT', payload: newViewport })
578
+ dispatch({ type: 'SET_VIZ_VIEWPORT', payload: newViewport })
500
579
 
501
580
  if (entry.target.dataset.lollipop === 'true') {
502
581
  width = width - 2.5
@@ -640,6 +719,19 @@ const CdcChart: React.FC<CdcChartProps> = ({
640
719
  }
641
720
  }, [externalFilters]) // eslint-disable-line
642
721
 
722
+ // Declaratively update runtime series keys for pie charts when derived value changes
723
+ if (config.runtime?.isPieChart && pieSeriesKeys && !_.isEqual(pieSeriesKeys, config.runtime?.seriesKeys)) {
724
+ const newConfig = {
725
+ ...config,
726
+ runtime: {
727
+ ...config.runtime,
728
+ seriesKeys: pieSeriesKeys,
729
+ seriesLabelsAll: pieSeriesKeys
730
+ }
731
+ }
732
+ setConfig(newConfig)
733
+ }
734
+
643
735
  // Generates color palette to pass to child chart component
644
736
  useEffect(() => {
645
737
  if (stateData && config.xAxis && config.runtime?.seriesKeys) {
@@ -654,6 +746,49 @@ const CdcChart: React.FC<CdcChartProps> = ({
654
746
  }
655
747
  }, [config, stateData]) // eslint-disable-line
656
748
 
749
+ // Updates runtime axis labels when config or data changes when using markup variables
750
+ useEffect(() => {
751
+ if (
752
+ !config?.runtime ||
753
+ _.isEmpty(config.runtime) ||
754
+ (!config.runtime.xAxis && !config.runtime.yAxis) ||
755
+ !config.markupVariables?.length
756
+ ) {
757
+ return
758
+ }
759
+
760
+ const dataSource = (stateData && stateData.length ? stateData : config.data) || []
761
+ const { runtimeXAxisLabel, runtimeYAxisLabel, isHorizontalVariant } = getProcessedAxisLabels(config, dataSource)
762
+
763
+ const runtimeClone = _.cloneDeep(config.runtime)
764
+
765
+ if (!runtimeClone?.xAxis || !runtimeClone?.yAxis) {
766
+ return
767
+ }
768
+
769
+ let shouldUpdateLabels = false
770
+
771
+ if (typeof runtimeXAxisLabel !== 'undefined' && runtimeClone.xAxis.label !== runtimeXAxisLabel) {
772
+ runtimeClone.xAxis = { ...runtimeClone.xAxis, label: runtimeXAxisLabel }
773
+ shouldUpdateLabels = true
774
+ }
775
+
776
+ if (typeof runtimeYAxisLabel !== 'undefined' && runtimeClone.yAxis.label !== runtimeYAxisLabel) {
777
+ runtimeClone.yAxis = { ...runtimeClone.yAxis, label: runtimeYAxisLabel }
778
+ shouldUpdateLabels = true
779
+ }
780
+
781
+ if (shouldUpdateLabels) {
782
+ runtimeClone.uniqueId = Date.now()
783
+ const updatedConfig = { ...config, runtime: runtimeClone } as ChartConfig
784
+ dispatch({ type: 'SET_CONFIG', payload: updatedConfig })
785
+
786
+ if (isEditor && !isDashboard) {
787
+ editorContext.setTempConfig(updatedConfig)
788
+ }
789
+ }
790
+ }, [config, stateData, getProcessedAxisLabels, dispatch, editorContext, isEditor, isDashboard])
791
+
657
792
  // Called on legend click, highlights/unhighlights the data series with the given label
658
793
  const highlight = (label: Label): void => {
659
794
  if (
@@ -707,15 +842,17 @@ const CdcChart: React.FC<CdcChartProps> = ({
707
842
  }
708
843
 
709
844
  const formatDate = (date, i, ticks) => {
710
- let formattedDate = timeFormat(config.runtime[section].dateDisplayFormat)(date)
845
+ const displayFormat =
846
+ config.runtime[section].dateDisplayFormat || config.runtime[section].dateParseFormat || '%Y-%m-%d'
847
+ let formattedDate = timeFormat(displayFormat)(date)
711
848
  // Handle the case where all months work with '%b.' except for May
712
- if (config.runtime[section].dateDisplayFormat?.includes('%b.') && formattedDate.includes('May.')) {
849
+ if (displayFormat?.includes('%b.') && formattedDate.includes('May.')) {
713
850
  formattedDate = formattedDate.replace(/May\./g, 'May')
714
851
  }
715
852
  // Show years only once
716
- if (config.xAxis.showYearsOnce && config.runtime[section].dateDisplayFormat?.includes('%Y') && ticks) {
853
+ if (config.xAxis.showYearsOnce && displayFormat?.includes('%Y') && ticks) {
717
854
  const prevDate = ticks[i - 1] ? ticks[i - 1].value : null
718
- const prevFormattedDate = timeFormat(config.runtime[section].dateDisplayFormat)(prevDate)
855
+ const prevFormattedDate = timeFormat(displayFormat)(prevDate)
719
856
  const year = formattedDate.match(/\d{4}/)
720
857
  const prevYear = prevFormattedDate.match(/\d{4}/)
721
858
  if (year && prevYear && year[0] === prevYear[0]) {
@@ -769,7 +906,8 @@ const CdcChart: React.FC<CdcChartProps> = ({
769
906
  rightSuffix,
770
907
  bottomPrefix,
771
908
  bottomSuffix,
772
- bottomAbbreviated
909
+ bottomAbbreviated,
910
+ preserveOriginalDecimals
773
911
  }
774
912
  } = config
775
913
 
@@ -788,32 +926,52 @@ const CdcChart: React.FC<CdcChartProps> = ({
788
926
  } else {
789
927
  roundToPlace = roundTo ? Number(roundTo) : 0
790
928
  }
791
- stringFormattingOptions = {
792
- useGrouping: addColRoundTo ? true : config.dataFormat.commas ? true : false,
793
- minimumFractionDigits: roundToPlace,
794
- maximumFractionDigits: roundToPlace
929
+
930
+ // If preserveOriginalDecimals is enabled, don't force decimal places
931
+ if (preserveOriginalDecimals) {
932
+ stringFormattingOptions = {
933
+ useGrouping: addColRoundTo ? true : config.dataFormat.commas ? true : false
934
+ }
935
+ } else {
936
+ stringFormattingOptions = {
937
+ useGrouping: addColRoundTo ? true : config.dataFormat.commas ? true : false,
938
+ minimumFractionDigits: roundToPlace,
939
+ maximumFractionDigits: roundToPlace
940
+ }
795
941
  }
796
942
  }
797
943
 
798
944
  if (axis === 'right') {
799
- stringFormattingOptions = {
800
- useGrouping: config.dataFormat.rightCommas ? true : false,
801
- minimumFractionDigits: rightRoundTo ? Number(rightRoundTo) : 0,
802
- maximumFractionDigits: rightRoundTo ? Number(rightRoundTo) : 0
945
+ if (preserveOriginalDecimals) {
946
+ stringFormattingOptions = {
947
+ useGrouping: config.dataFormat.rightCommas ? true : false
948
+ }
949
+ } else {
950
+ stringFormattingOptions = {
951
+ useGrouping: config.dataFormat.rightCommas ? true : false,
952
+ minimumFractionDigits: rightRoundTo ? Number(rightRoundTo) : 0,
953
+ maximumFractionDigits: rightRoundTo ? Number(rightRoundTo) : 0
954
+ }
803
955
  }
804
956
  }
805
957
 
806
958
  const resolveBottomTickRounding = () => {
807
- if (config.forestPlot.type === 'Logarithmic' && !bottomRoundTo) return 2
959
+ if (config.forestPlot?.type === 'Logarithmic' && !bottomRoundTo) return 2
808
960
  if (Number(bottomRoundTo)) return Number(bottomRoundTo)
809
961
  return 0
810
962
  }
811
963
 
812
964
  if (axis === 'bottom') {
813
- stringFormattingOptions = {
814
- useGrouping: config.dataFormat.bottomCommas ? true : false,
815
- minimumFractionDigits: resolveBottomTickRounding(),
816
- maximumFractionDigits: resolveBottomTickRounding()
965
+ if (preserveOriginalDecimals) {
966
+ stringFormattingOptions = {
967
+ useGrouping: config.dataFormat.bottomCommas ? true : false
968
+ }
969
+ } else {
970
+ stringFormattingOptions = {
971
+ useGrouping: config.dataFormat.bottomCommas ? true : false,
972
+ minimumFractionDigits: resolveBottomTickRounding(),
973
+ maximumFractionDigits: resolveBottomTickRounding()
974
+ }
817
975
  }
818
976
  }
819
977
 
@@ -992,8 +1150,6 @@ const CdcChart: React.FC<CdcChartProps> = ({
992
1150
  const isLegendOnBottom = legend?.position === 'bottom' || isLegendWrapViewport(currentViewport)
993
1151
 
994
1152
  if (config.isResponsiveTicks) classes.push('subtext--responsive-ticks ')
995
- if (config.xAxis.brushActive && !isLegendOnBottom) classes.push('subtext--brush-active ')
996
- if (config.xAxis.brushActive && config.legend.hide) classes.push('subtext--brush-active ')
997
1153
  return classes
998
1154
  }
999
1155
 
@@ -1008,9 +1164,6 @@ const CdcChart: React.FC<CdcChartProps> = ({
1008
1164
  {isEditor && <EditorPanel datasets={datasets} />}
1009
1165
  <Layout.Responsive isEditor={isEditor}>
1010
1166
  {config.newViz && <Confirm updateConfig={updateConfig} config={config} />}
1011
- {undefined === config.newViz && isEditor && config.runtime && config.runtime?.editorErrorMessage && (
1012
- <Error errorMessage={config.runtime.editorErrorMessage} />
1013
- )}
1014
1167
  {!missingRequiredSections(config) && !config.newViz && (
1015
1168
  <div
1016
1169
  className={`cdc-chart-inner-container cove-component__content type-${makeClassName(
@@ -1024,11 +1177,20 @@ const CdcChart: React.FC<CdcChartProps> = ({
1024
1177
  isDashboard={isDashboard}
1025
1178
  title={title}
1026
1179
  superTitle={processedSuperTitle}
1180
+ titleStyle={config.titleStyle}
1027
1181
  classes={['chart-title', `${config.theme}`, 'cove-component__header', 'mb-3']}
1028
1182
  style={undefined}
1029
1183
  config={config}
1030
1184
  />
1031
1185
 
1186
+ {/* Error Message Display - Show at top before visualization wrapper */}
1187
+ {/* {(() => {
1188
+ const errorMessage = config.runtime?.editorErrorMessage
1189
+ const hasError = errorMessage && typeof errorMessage === 'string' && errorMessage.trim() !== ''
1190
+ const shouldShow = undefined === config.newViz && isEditor && config.runtime && hasError
1191
+ return shouldShow ? <Error errorMessage={errorMessage} /> : null
1192
+ })()} */}
1193
+
1032
1194
  {/* Visualization Wrapper */}
1033
1195
  <div className={getChartWrapperClasses().join(' ')}>
1034
1196
  {/* Intro Text/Message */}
@@ -1067,18 +1229,27 @@ const CdcChart: React.FC<CdcChartProps> = ({
1067
1229
  : 'w-75'
1068
1230
  }
1069
1231
  >
1070
- {/* All charts with LinearChart */}
1071
- {!['Spark Line', 'Line', 'Sankey', 'Pie', 'Sankey'].includes(config.visualizationType) && (
1072
- <div ref={parentRef} style={{ width: `100%` }}>
1073
- <ParentSize>
1074
- {parent => (
1075
- <LinearChart ref={svgRef} parentWidth={parent.width} parentHeight={parent.height} />
1076
- )}
1077
- </ParentSize>
1232
+ {/* Check if there is data to display */}
1233
+ {(!filteredData || filteredData.length === 0) && (
1234
+ <div className='no-data-message' style={{ padding: '2rem', textAlign: 'center', color: '#666' }}>
1235
+ {config.chartMessage?.noData || 'No Data Available'}
1078
1236
  </div>
1079
1237
  )}
1080
1238
 
1081
- {config.visualizationType === 'Pie' && (
1239
+ {/* All charts with LinearChart */}
1240
+ {filteredData &&
1241
+ filteredData.length > 0 &&
1242
+ !['Spark Line', 'Line', 'Sankey', 'Pie', 'Sankey'].includes(config.visualizationType) && (
1243
+ <div ref={parentRef} style={{ width: `100%` }}>
1244
+ <ParentSize>
1245
+ {parent => (
1246
+ <LinearChart ref={svgRef} parentWidth={parent.width} parentHeight={parent.height} />
1247
+ )}
1248
+ </ParentSize>
1249
+ </div>
1250
+ )}
1251
+
1252
+ {filteredData && filteredData.length > 0 && config.visualizationType === 'Pie' && (
1082
1253
  <ParentSize className='justify-content-center d-flex' style={{ width: `100%` }}>
1083
1254
  {parent => (
1084
1255
  <PieChart
@@ -1091,7 +1262,9 @@ const CdcChart: React.FC<CdcChartProps> = ({
1091
1262
  </ParentSize>
1092
1263
  )}
1093
1264
  {/* Line Chart */}
1094
- {config.visualizationType === 'Line' &&
1265
+ {filteredData &&
1266
+ filteredData.length > 0 &&
1267
+ config.visualizationType === 'Line' &&
1095
1268
  (convertLineToBarGraph ? (
1096
1269
  <div ref={parentRef} style={{ width: `100%` }}>
1097
1270
  <ParentSize>
@@ -1156,13 +1329,18 @@ const CdcChart: React.FC<CdcChartProps> = ({
1156
1329
  {/* Legend */}
1157
1330
  {!config.legend.hide &&
1158
1331
  config.visualizationType !== 'Spark Line' &&
1159
- config.visualizationType !== 'Sankey' && (
1332
+ config.visualizationType !== 'Sankey' &&
1333
+ !(config.visualizationType === 'Warming Stripes' && config.legend?.style === 'gradient') &&
1334
+ !(config.visualizationType === 'Warming Stripes' && config.smallMultiples?.mode) && (
1160
1335
  <Legend
1161
1336
  ref={legendRef}
1162
1337
  skipId={handleChartTabbing(config, legendId)}
1163
1338
  interactionLabel={interactionLabel}
1164
1339
  />
1165
1340
  )}
1341
+ {config.visualizationType === 'Warming Stripes' &&
1342
+ config.legend?.style === 'gradient' &&
1343
+ !config.smallMultiples?.mode && <WarmingStripesGradientLegend />}
1166
1344
  </LegendWrapper>
1167
1345
  {/* Link */}
1168
1346
  {isDashboard && config.table && config.table.show && config.table.showDataTableLink
@@ -1174,62 +1352,86 @@ const CdcChart: React.FC<CdcChartProps> = ({
1174
1352
  <div className={getChartSubTextClasses().join(' ')}>{parse(processedDescription)}</div>
1175
1353
  )}
1176
1354
 
1177
- {/* buttons */}
1178
- <MediaControls.Section classes={['download-buttons']}>
1179
- {config.table.showDownloadImgButton && (
1180
- <MediaControls.Button
1181
- text='Download Image'
1182
- title='Download Chart as Image'
1183
- type='image'
1184
- state={config}
1185
- elementToCapture={imageId}
1186
- interactionLabel={interactionLabel}
1187
- />
1188
- )}
1189
- {config.table.showDownloadPdfButton && (
1190
- <MediaControls.Button
1191
- text='Download PDF'
1192
- title='Download Chart as PDF'
1193
- type='pdf'
1194
- state={config}
1195
- elementToCapture={imageId}
1196
- interactionLabel={interactionLabel}
1197
- />
1198
- )}
1199
- </MediaControls.Section>
1200
1355
  {/* Data Table */}
1201
- {((config.xAxis.dataKey &&
1356
+ {(config.xAxis.dataKey &&
1202
1357
  config.table.show &&
1203
1358
  config.visualizationType !== 'Spark Line' &&
1204
1359
  config.visualizationType !== 'Sankey') ||
1205
- (config.visualizationType === 'Sankey' && config.table.show)) && (
1206
- <DataTable
1207
- /* changing the "key" will force the table to re-render
1208
- when the default sort changes while editing */
1209
- key={dataTableDefaultSortBy}
1210
- config={pivotDynamicSeries(config)}
1211
- rawData={
1212
- config.visualizationType === 'Sankey'
1213
- ? config?.data?.[0]?.tableData
1214
- : config.table.customTableConfig
1215
- ? filterVizData(config.filters, config.data)
1216
- : config.data
1217
- }
1218
- runtimeData={getTableRuntimeData()}
1219
- expandDataTable={config.table.expanded}
1220
- columns={config.columns}
1221
- defaultSortBy={dataTableDefaultSortBy}
1222
- displayGeoName={name => name}
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
- interactionLabel={interactionLabel}
1231
- />
1232
- )}
1360
+ (config.visualizationType === 'Sankey' && config.table.show)
1361
+ ? (() => {
1362
+ let dataTableConfig = pivotDynamicSeries(config)
1363
+ let dataTableColumns = config.columns
1364
+ let dataTableRuntimeData = getTableRuntimeData()
1365
+ let dataTableRawData =
1366
+ config.visualizationType === 'Sankey'
1367
+ ? config?.data?.[0]?.tableData
1368
+ : config.table.customTableConfig
1369
+ ? filterVizData(config.filters, config.data)
1370
+ : config.data
1371
+
1372
+ if (config.smallMultiples?.mode) {
1373
+ const prepared = prepareSmallMultiplesDataTable(config, config.columns, dataTableRuntimeData)
1374
+ dataTableConfig = prepared.config
1375
+ dataTableColumns = prepared.columns
1376
+ dataTableRuntimeData = prepared.runtimeData
1377
+ if (config.smallMultiples.mode === 'by-column') {
1378
+ dataTableRawData = prepared.config.data
1379
+ }
1380
+ }
1381
+
1382
+ return (
1383
+ <DataTable
1384
+ /* changing the "key" will force the table to re-render
1385
+ when the default sort changes while editing */
1386
+ key={dataTableDefaultSortBy}
1387
+ config={dataTableConfig}
1388
+ rawData={dataTableRawData}
1389
+ runtimeData={dataTableRuntimeData}
1390
+ expandDataTable={config.table.expanded}
1391
+ columns={dataTableColumns}
1392
+ defaultSortBy={dataTableDefaultSortBy}
1393
+ displayGeoName={name => name}
1394
+ applyLegendToRow={applyLegendToRow}
1395
+ tableTitle={config.table.label}
1396
+ indexTitle={config.table.indexLabel}
1397
+ vizTitle={title}
1398
+ viewport={currentViewport}
1399
+ tabbingId={handleChartTabbing(config, legendId)}
1400
+ colorScale={colorScale}
1401
+ imageRef={imageId}
1402
+ showDownloadImgButton={config.table.showDownloadImgButton}
1403
+ showDownloadPdfButton={config.table.showDownloadPdfButton}
1404
+ includeContextInDownload={config.table?.includeContextInDownload}
1405
+ interactionLabel={interactionLabel}
1406
+ />
1407
+ )
1408
+ })()
1409
+ : (config.table.showDownloadImgButton || config.table.showDownloadPdfButton) && (
1410
+ <div className='w-100 d-flex justify-content-end'>
1411
+ <MediaControls.Section classes={['download-links', 'mt-4', 'mb-2']}>
1412
+ {config.table.showDownloadImgButton && (
1413
+ <MediaControls.DownloadLink
1414
+ type='image'
1415
+ title='Download Chart as Image'
1416
+ state={config}
1417
+ elementToCapture={imageId}
1418
+ interactionLabel={interactionLabel}
1419
+ includeContextInDownload={config.table?.includeContextInDownload}
1420
+ />
1421
+ )}
1422
+ {config.table.showDownloadPdfButton && (
1423
+ <MediaControls.DownloadLink
1424
+ type='pdf'
1425
+ title='Download Chart as PDF'
1426
+ state={config}
1427
+ elementToCapture={imageId}
1428
+ interactionLabel={interactionLabel}
1429
+ includeContextInDownload={config.table?.includeContextInDownload}
1430
+ />
1431
+ )}
1432
+ </MediaControls.Section>
1433
+ </div>
1434
+ )}
1233
1435
  {config?.annotations?.length > 0 && <Annotation.Dropdown />}
1234
1436
  {/* show pdf or image button */}
1235
1437
  {processedLegacyFootnotes && (
@@ -1239,6 +1441,9 @@ const CdcChart: React.FC<CdcChartProps> = ({
1239
1441
  <FootnotesStandAlone
1240
1442
  config={configObj.footnotes}
1241
1443
  filters={config.filters?.filter(f => f.filterFootnotes)}
1444
+ markupVariables={config.markupVariables}
1445
+ enableMarkupVariables={config.enableMarkupVariables}
1446
+ data={config.data}
1242
1447
  />
1243
1448
  </div>
1244
1449
  )}