@cdc/chart 4.25.10 → 4.25.11

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 (85) hide show
  1. package/dist/{cdcchart-1a1724a1.es.js → cdcchart-dgT_1dIT.es.js} +136 -151
  2. package/dist/cdcchart.js +36258 -34658
  3. package/examples/feature/__data__/planet-example-data.json +1 -1
  4. package/examples/feature/boxplot/valid-boxplot.csv +38 -17
  5. package/examples/private/DEV-11825.json +573 -0
  6. package/examples/private/na.json +913 -0
  7. package/examples/private/test-data.csv +28 -0
  8. package/index.html +2 -121
  9. package/package.json +4 -4
  10. package/src/CdcChart.tsx +8 -11
  11. package/src/CdcChartComponent.tsx +256 -87
  12. package/src/_stories/Chart.Combo.stories.tsx +18 -0
  13. package/src/_stories/Chart.Forecast.stories.tsx +36 -0
  14. package/src/_stories/Chart.HTMLInDataTable.stories.tsx +520 -0
  15. package/src/_stories/Chart.Patterns.stories.tsx +2 -1
  16. package/src/_stories/Chart.PreserveDecimals.stories.tsx +220 -0
  17. package/src/_stories/Chart.SmallMultiples.stories.tsx +47 -0
  18. package/src/_stories/ChartAnnotation.stories.tsx +6 -3
  19. package/src/_stories/ChartBar.Editor.stories.tsx +3580 -0
  20. package/src/_stories/ChartEditor.Editor.stories.tsx +658 -0
  21. package/src/_stories/ChartEditor.stories.tsx +1 -2
  22. package/src/_stories/_mock/combo.json +451 -0
  23. package/src/_stories/_mock/editor-test-configs.json +376 -0
  24. package/src/_stories/_mock/editor-test-datasets.json +477 -0
  25. package/src/_stories/_mock/editor-tests/bar-chart-editor-test.json +255 -0
  26. package/src/_stories/_mock/editor-tests/bar-chart-general-test.json +267 -0
  27. package/src/_stories/_mock/editor-tests/bar-chart-test.json +237 -0
  28. package/src/_stories/_mock/forecast_combo_with_gaps.json +913 -0
  29. package/src/_stories/_mock/pie_config.json +257 -62
  30. package/src/_stories/_mock/small_multiples/small_multiples_bars.json +1944 -0
  31. package/src/_stories/_mock/small_multiples/small_multiples_big_data_bars.json +1114 -0
  32. package/src/_stories/_mock/small_multiples/small_multiples_lines.json +2646 -0
  33. package/src/_stories/_mock/small_multiples/small_multiples_lines_colors.json +1305 -0
  34. package/src/_stories/_mock/small_multiples/small_multiples_stacked_bars.json +1936 -0
  35. package/src/components/Annotations/components/findNearestDatum.ts +6 -41
  36. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +10 -6
  37. package/src/components/AreaChart/index.tsx +1 -2
  38. package/src/components/BarChart/components/BarChart.Horizontal.tsx +4 -4
  39. package/src/components/BarChart/components/BarChart.Vertical.tsx +3 -2
  40. package/src/components/BoxPlot/helpers/index.ts +3 -3
  41. package/src/components/Brush/BrushChart.tsx +1 -1
  42. package/src/components/EditorPanel/EditorPanel.tsx +199 -190
  43. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +96 -111
  44. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +19 -1
  45. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +102 -55
  46. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +54 -49
  47. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +422 -0
  48. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +75 -21
  49. package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
  50. package/src/components/EditorPanel/editor-panel.scss +0 -20
  51. package/src/components/EditorPanel/useEditorPermissions.ts +7 -15
  52. package/src/components/Forecasting/Forecasting.tsx +139 -21
  53. package/src/components/Legend/Legend.Component.tsx +16 -9
  54. package/src/components/Legend/helpers/createFormatLabels.tsx +181 -181
  55. package/src/components/Legend/helpers/getLegendClasses.ts +0 -1
  56. package/src/components/LineChart/LineChartProps.ts +0 -3
  57. package/src/components/LineChart/helpers.ts +1 -1
  58. package/src/components/LineChart/index.tsx +36 -13
  59. package/src/components/LinearChart.tsx +75 -80
  60. package/src/components/Regions/components/Regions.tsx +3 -24
  61. package/src/components/Sankey/types/index.ts +1 -1
  62. package/src/components/SmallMultiples/SmallMultipleTile.tsx +198 -0
  63. package/src/components/SmallMultiples/SmallMultiples.css +32 -0
  64. package/src/components/SmallMultiples/SmallMultiples.tsx +271 -0
  65. package/src/components/SmallMultiples/index.ts +2 -0
  66. package/src/data/initial-state.js +13 -1
  67. package/src/helpers/buildForecastPaletteOptions.ts +0 -38
  68. package/src/helpers/getColorScale.ts +10 -0
  69. package/src/{hooks/useMinMax.ts → helpers/getMinMax.ts} +14 -7
  70. package/src/helpers/getYAxisAutoPadding.ts +53 -0
  71. package/src/helpers/smallMultiplesHelpers.ts +529 -0
  72. package/src/hooks/useProgrammaticTooltip.ts +96 -0
  73. package/src/hooks/useScales.ts +88 -34
  74. package/src/hooks/useSmallMultipleSynchronization.ts +59 -0
  75. package/src/hooks/useTooltip.tsx +60 -15
  76. package/src/scss/main.scss +1 -80
  77. package/src/store/chart.actions.ts +2 -0
  78. package/src/store/chart.reducer.ts +4 -0
  79. package/src/types/ChartConfig.ts +24 -6
  80. package/src/types/ChartContext.ts +3 -0
  81. package/src/_stories/_mock/pie_data.json +0 -218
  82. package/src/components/AreaChart/components/AreaChart.jsx +0 -109
  83. package/src/helpers/sort.ts +0 -7
  84. package/src/hooks/useActiveElement.js +0 -19
  85. package/src/hooks/useChartClasses.js +0 -41
@@ -75,6 +75,7 @@ import { getExcludedData } from './helpers/getExcludedData'
75
75
  import { getColorScale } from './helpers/getColorScale'
76
76
  import { getTransformedData } from './helpers/getTransformedData'
77
77
  import { getPiePercent } from './helpers/getPiePercent'
78
+ import { prepareSmallMultiplesDataTable } from './helpers/smallMultiplesHelpers'
78
79
 
79
80
  // styles
80
81
  import './scss/main.scss'
@@ -275,30 +276,62 @@ const CdcChart: React.FC<CdcChartProps> = ({
275
276
  return newConfig
276
277
  }
277
278
 
279
+ const getProcessedAxisLabels = useCallback(
280
+ (targetConfig: AllChartsConfig, dataSource: any[] = []) => {
281
+ let processedXAxis = targetConfig.xAxis?.label
282
+ let processedYAxis = targetConfig.yAxis?.label
283
+
284
+ if (targetConfig.enableMarkupVariables && targetConfig.markupVariables?.length) {
285
+ if (targetConfig.xAxis?.label) {
286
+ processedXAxis = processMarkupVariables(
287
+ targetConfig.xAxis.label,
288
+ dataSource || [],
289
+ targetConfig.markupVariables,
290
+ {
291
+ isEditor,
292
+ filters: targetConfig.filters || []
293
+ }
294
+ ).processedContent
295
+ }
296
+ if (targetConfig.yAxis?.label) {
297
+ processedYAxis = processMarkupVariables(
298
+ targetConfig.yAxis.label,
299
+ dataSource || [],
300
+ targetConfig.markupVariables,
301
+ {
302
+ isEditor,
303
+ filters: targetConfig.filters || []
304
+ }
305
+ ).processedContent
306
+ }
307
+ }
308
+
309
+ const isHorizontalVariant =
310
+ ((targetConfig.visualizationType === 'Bar' || targetConfig.visualizationType === 'Box Plot') &&
311
+ targetConfig.orientation === 'horizontal') ||
312
+ ['Deviation Bar', 'Paired Bar', 'Forest Plot'].includes(targetConfig.visualizationType)
313
+
314
+ const runtimeXAxisLabel = isHorizontalVariant
315
+ ? processedYAxis ?? (targetConfig.yAxis as any)?.yAxis?.label ?? targetConfig.yAxis?.label
316
+ : processedXAxis ?? targetConfig.xAxis?.label
317
+
318
+ const runtimeYAxisLabel = isHorizontalVariant
319
+ ? processedXAxis ?? (targetConfig.xAxis as any)?.xAxis?.label ?? targetConfig.xAxis?.label
320
+ : processedYAxis ?? targetConfig.yAxis?.label
321
+
322
+ return { processedXAxis, processedYAxis, runtimeXAxisLabel, runtimeYAxisLabel, isHorizontalVariant }
323
+ },
324
+ [isEditor]
325
+ )
326
+
278
327
  const updateConfig = (_config: AllChartsConfig, dataOverride?: any[]) => {
279
328
  const newConfig = cloneConfig(_config)
280
329
  let data = dataOverride || stateData
281
330
 
282
331
  data = handleRankByValue(data, newConfig)
283
332
 
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
- }
333
+ const { processedXAxis, processedYAxis, runtimeXAxisLabel, runtimeYAxisLabel, isHorizontalVariant } =
334
+ getProcessedAxisLabels(newConfig, data || [])
302
335
 
303
336
  // Deeper copy
304
337
  Object.keys(defaults).forEach(key => {
@@ -323,6 +356,14 @@ const CdcChart: React.FC<CdcChartProps> = ({
323
356
  }
324
357
 
325
358
  //Enforce default values that need to be calculated at runtime
359
+ // Preserve error messages that were set outside of updateConfig (e.g., from pattern settings)
360
+ const existingErrorMessage = _config.runtime?.editorErrorMessage || ''
361
+ const isPieChartValidationError =
362
+ existingErrorMessage === 'Data column section must be set for pie charts.' ||
363
+ existingErrorMessage === 'Segment labels section must be set for pie charts.' ||
364
+ existingErrorMessage === 'Data column and Segment labels sections must be set for pie charts.'
365
+ const shouldPreserveError = existingErrorMessage && !isPieChartValidationError
366
+
326
367
  newConfig.runtime = {} as Runtime
327
368
  newConfig.runtime.series = _.cloneDeep(newConfig.series)
328
369
  newConfig.runtime.seriesLabels = {}
@@ -384,6 +425,18 @@ const CdcChart: React.FC<CdcChartProps> = ({
384
425
  newConfig.runtime.forecastingSeriesKeys.push(series)
385
426
  }
386
427
  })
428
+
429
+ // Default to date scaling type for Forecasting charts
430
+ if (newConfig.xAxis.type === 'categorical') {
431
+ newConfig.xAxis.type = 'date'
432
+ // Initialize date parsing formats if they don't exist
433
+ if (!newConfig.xAxis.dateParseFormat) {
434
+ newConfig.xAxis.dateParseFormat = '%Y-%m-%d'
435
+ }
436
+ if (!newConfig.xAxis.dateDisplayFormat) {
437
+ newConfig.xAxis.dateDisplayFormat = '%Y-%m-%d'
438
+ }
439
+ }
387
440
  }
388
441
 
389
442
  if (newConfig.visualizationType === 'Area Chart' && newConfig.series) {
@@ -395,19 +448,17 @@ const CdcChart: React.FC<CdcChartProps> = ({
395
448
  newConfig.visualizationSubType = 'stacked'
396
449
  }
397
450
 
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
- ) {
451
+ if (isHorizontalVariant) {
403
452
  // For horizontal charts, axes are swapped, so processedYAxis goes to runtime.xAxis and vice versa
453
+ const horizontalXAxisSource = _.cloneDeep((newConfig.yAxis as any)?.yAxis || newConfig.yAxis)
454
+ const horizontalYAxisSource = _.cloneDeep((newConfig.xAxis as any)?.xAxis || newConfig.xAxis)
404
455
  newConfig.runtime.xAxis = {
405
- ..._.cloneDeep(newConfig.yAxis.yAxis || newConfig.yAxis),
406
- label: processedYAxis || (newConfig.yAxis.yAxis || newConfig.yAxis).label
456
+ ...horizontalXAxisSource,
457
+ label: runtimeXAxisLabel ?? horizontalXAxisSource?.label
407
458
  }
408
459
  newConfig.runtime.yAxis = {
409
- ..._.cloneDeep(newConfig.xAxis.xAxis || newConfig.xAxis),
410
- label: processedXAxis || (newConfig.xAxis.xAxis || newConfig.xAxis).label
460
+ ...horizontalYAxisSource,
461
+ label: runtimeYAxisLabel ?? horizontalYAxisSource?.label
411
462
  }
412
463
  newConfig.runtime.yAxis.labelOffset *= -1
413
464
 
@@ -419,24 +470,40 @@ const CdcChart: React.FC<CdcChartProps> = ({
419
470
  ['Scatter Plot', 'Area Chart', 'Line', 'Forecasting'].includes(newConfig.visualizationType) &&
420
471
  !convertLineToBarGraph
421
472
  ) {
422
- newConfig.runtime.xAxis = { ...newConfig.xAxis, label: processedXAxis || newConfig.xAxis.label }
423
- newConfig.runtime.yAxis = { ...newConfig.yAxis, label: processedYAxis || newConfig.yAxis.label }
473
+ newConfig.runtime.xAxis = { ...newConfig.xAxis, label: runtimeXAxisLabel ?? newConfig.xAxis.label }
474
+ newConfig.runtime.yAxis = { ...newConfig.yAxis, label: runtimeYAxisLabel ?? newConfig.yAxis.label }
424
475
  newConfig.runtime.horizontal = false
425
476
  newConfig.orientation = 'vertical'
426
477
  } else {
427
- newConfig.runtime.xAxis = { ...newConfig.xAxis, label: processedXAxis || newConfig.xAxis.label }
428
- newConfig.runtime.yAxis = { ...newConfig.yAxis, label: processedYAxis || newConfig.yAxis.label }
478
+ newConfig.runtime.xAxis = { ...newConfig.xAxis, label: runtimeXAxisLabel ?? newConfig.xAxis.label }
479
+ newConfig.runtime.yAxis = { ...newConfig.yAxis, label: runtimeYAxisLabel ?? newConfig.yAxis.label }
429
480
  newConfig.runtime.horizontal = false
430
481
  }
431
482
 
432
483
  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
484
 
438
- // Sankey Description box error message
439
- newConfig.runtime.editorErrorMessage = ''
485
+ // Set error messages: preserve external errors (from pattern settings, etc.) or set validation errors
486
+ if (shouldPreserveError) {
487
+ // Preserve error messages set by editor panels (e.g., pattern contrast errors)
488
+ newConfig.runtime.editorErrorMessage = existingErrorMessage
489
+ } else if (newConfig.visualizationType === 'Pie') {
490
+ // Check for Pie chart validation errors
491
+ const missingDataColumn = !newConfig.yAxis.dataKey || newConfig.yAxis.dataKey === ''
492
+ const missingSegmentLabels = !newConfig.xAxis.dataKey || newConfig.xAxis.dataKey === ''
493
+
494
+ if (missingDataColumn && missingSegmentLabels) {
495
+ newConfig.runtime.editorErrorMessage = 'Data column and Segment labels sections must be set for pie charts.'
496
+ } else if (missingDataColumn) {
497
+ newConfig.runtime.editorErrorMessage = 'Data column section must be set for pie charts.'
498
+ } else if (missingSegmentLabels) {
499
+ newConfig.runtime.editorErrorMessage = 'Segment labels section must be set for pie charts.'
500
+ } else {
501
+ newConfig.runtime.editorErrorMessage = ''
502
+ }
503
+ } else {
504
+ // No errors
505
+ newConfig.runtime.editorErrorMessage = ''
506
+ }
440
507
 
441
508
  if (newConfig.legend.seriesHighlight?.length) {
442
509
  dispatch({ type: 'SET_SERIES_HIGHLIGHT', payload: newConfig.legend?.seriesHighlight })
@@ -492,11 +559,13 @@ const CdcChart: React.FC<CdcChartProps> = ({
492
559
  for (let entry of entries) {
493
560
  let { width, height } = entry.contentRect
494
561
 
495
- width = isEditor ? width - EDITOR_WIDTH : width
562
+ const editorIsOpen = isEditor && !!document.querySelector('.editor-panel:not(.hidden)')
563
+ width = editorIsOpen ? width - EDITOR_WIDTH : width
496
564
 
497
565
  const newViewport = getViewport(width)
498
566
 
499
567
  dispatch({ type: 'SET_VIEWPORT', payload: newViewport })
568
+ dispatch({ type: 'SET_VIZ_VIEWPORT', payload: newViewport })
500
569
 
501
570
  if (entry.target.dataset.lollipop === 'true') {
502
571
  width = width - 2.5
@@ -654,6 +723,49 @@ const CdcChart: React.FC<CdcChartProps> = ({
654
723
  }
655
724
  }, [config, stateData]) // eslint-disable-line
656
725
 
726
+ // Updates runtime axis labels when config or data changes when using markup variables
727
+ useEffect(() => {
728
+ if (
729
+ !config?.runtime ||
730
+ _.isEmpty(config.runtime) ||
731
+ (!config.runtime.xAxis && !config.runtime.yAxis) ||
732
+ !config.markupVariables?.length
733
+ ) {
734
+ return
735
+ }
736
+
737
+ const dataSource = (stateData && stateData.length ? stateData : config.data) || []
738
+ const { runtimeXAxisLabel, runtimeYAxisLabel, isHorizontalVariant } = getProcessedAxisLabels(config, dataSource)
739
+
740
+ const runtimeClone = _.cloneDeep(config.runtime)
741
+
742
+ if (!runtimeClone?.xAxis || !runtimeClone?.yAxis) {
743
+ return
744
+ }
745
+
746
+ let shouldUpdateLabels = false
747
+
748
+ if (typeof runtimeXAxisLabel !== 'undefined' && runtimeClone.xAxis.label !== runtimeXAxisLabel) {
749
+ runtimeClone.xAxis = { ...runtimeClone.xAxis, label: runtimeXAxisLabel }
750
+ shouldUpdateLabels = true
751
+ }
752
+
753
+ if (typeof runtimeYAxisLabel !== 'undefined' && runtimeClone.yAxis.label !== runtimeYAxisLabel) {
754
+ runtimeClone.yAxis = { ...runtimeClone.yAxis, label: runtimeYAxisLabel }
755
+ shouldUpdateLabels = true
756
+ }
757
+
758
+ if (shouldUpdateLabels) {
759
+ runtimeClone.uniqueId = Date.now()
760
+ const updatedConfig = { ...config, runtime: runtimeClone } as ChartConfig
761
+ dispatch({ type: 'SET_CONFIG', payload: updatedConfig })
762
+
763
+ if (isEditor && !isDashboard) {
764
+ editorContext.setTempConfig(updatedConfig)
765
+ }
766
+ }
767
+ }, [config, stateData, getProcessedAxisLabels, dispatch, editorContext, isEditor, isDashboard])
768
+
657
769
  // Called on legend click, highlights/unhighlights the data series with the given label
658
770
  const highlight = (label: Label): void => {
659
771
  if (
@@ -769,7 +881,8 @@ const CdcChart: React.FC<CdcChartProps> = ({
769
881
  rightSuffix,
770
882
  bottomPrefix,
771
883
  bottomSuffix,
772
- bottomAbbreviated
884
+ bottomAbbreviated,
885
+ preserveOriginalDecimals
773
886
  }
774
887
  } = config
775
888
 
@@ -788,32 +901,52 @@ const CdcChart: React.FC<CdcChartProps> = ({
788
901
  } else {
789
902
  roundToPlace = roundTo ? Number(roundTo) : 0
790
903
  }
791
- stringFormattingOptions = {
792
- useGrouping: addColRoundTo ? true : config.dataFormat.commas ? true : false,
793
- minimumFractionDigits: roundToPlace,
794
- maximumFractionDigits: roundToPlace
904
+
905
+ // If preserveOriginalDecimals is enabled, don't force decimal places
906
+ if (preserveOriginalDecimals) {
907
+ stringFormattingOptions = {
908
+ useGrouping: addColRoundTo ? true : config.dataFormat.commas ? true : false
909
+ }
910
+ } else {
911
+ stringFormattingOptions = {
912
+ useGrouping: addColRoundTo ? true : config.dataFormat.commas ? true : false,
913
+ minimumFractionDigits: roundToPlace,
914
+ maximumFractionDigits: roundToPlace
915
+ }
795
916
  }
796
917
  }
797
918
 
798
919
  if (axis === 'right') {
799
- stringFormattingOptions = {
800
- useGrouping: config.dataFormat.rightCommas ? true : false,
801
- minimumFractionDigits: rightRoundTo ? Number(rightRoundTo) : 0,
802
- maximumFractionDigits: rightRoundTo ? Number(rightRoundTo) : 0
920
+ if (preserveOriginalDecimals) {
921
+ stringFormattingOptions = {
922
+ useGrouping: config.dataFormat.rightCommas ? true : false
923
+ }
924
+ } else {
925
+ stringFormattingOptions = {
926
+ useGrouping: config.dataFormat.rightCommas ? true : false,
927
+ minimumFractionDigits: rightRoundTo ? Number(rightRoundTo) : 0,
928
+ maximumFractionDigits: rightRoundTo ? Number(rightRoundTo) : 0
929
+ }
803
930
  }
804
931
  }
805
932
 
806
933
  const resolveBottomTickRounding = () => {
807
- if (config.forestPlot.type === 'Logarithmic' && !bottomRoundTo) return 2
934
+ if (config.forestPlot?.type === 'Logarithmic' && !bottomRoundTo) return 2
808
935
  if (Number(bottomRoundTo)) return Number(bottomRoundTo)
809
936
  return 0
810
937
  }
811
938
 
812
939
  if (axis === 'bottom') {
813
- stringFormattingOptions = {
814
- useGrouping: config.dataFormat.bottomCommas ? true : false,
815
- minimumFractionDigits: resolveBottomTickRounding(),
816
- maximumFractionDigits: resolveBottomTickRounding()
940
+ if (preserveOriginalDecimals) {
941
+ stringFormattingOptions = {
942
+ useGrouping: config.dataFormat.bottomCommas ? true : false
943
+ }
944
+ } else {
945
+ stringFormattingOptions = {
946
+ useGrouping: config.dataFormat.bottomCommas ? true : false,
947
+ minimumFractionDigits: resolveBottomTickRounding(),
948
+ maximumFractionDigits: resolveBottomTickRounding()
949
+ }
817
950
  }
818
951
  }
819
952
 
@@ -1008,9 +1141,6 @@ const CdcChart: React.FC<CdcChartProps> = ({
1008
1141
  {isEditor && <EditorPanel datasets={datasets} />}
1009
1142
  <Layout.Responsive isEditor={isEditor}>
1010
1143
  {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
1144
  {!missingRequiredSections(config) && !config.newViz && (
1015
1145
  <div
1016
1146
  className={`cdc-chart-inner-container cove-component__content type-${makeClassName(
@@ -1029,6 +1159,14 @@ const CdcChart: React.FC<CdcChartProps> = ({
1029
1159
  config={config}
1030
1160
  />
1031
1161
 
1162
+ {/* Error Message Display - Show at top before visualization wrapper */}
1163
+ {/* {(() => {
1164
+ const errorMessage = config.runtime?.editorErrorMessage
1165
+ const hasError = errorMessage && typeof errorMessage === 'string' && errorMessage.trim() !== ''
1166
+ const shouldShow = undefined === config.newViz && isEditor && config.runtime && hasError
1167
+ return shouldShow ? <Error errorMessage={errorMessage} /> : null
1168
+ })()} */}
1169
+
1032
1170
  {/* Visualization Wrapper */}
1033
1171
  <div className={getChartWrapperClasses().join(' ')}>
1034
1172
  {/* Intro Text/Message */}
@@ -1067,18 +1205,27 @@ const CdcChart: React.FC<CdcChartProps> = ({
1067
1205
  : 'w-75'
1068
1206
  }
1069
1207
  >
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>
1208
+ {/* Check if there is data to display */}
1209
+ {(!filteredData || filteredData.length === 0) && (
1210
+ <div className='no-data-message' style={{ padding: '2rem', textAlign: 'center', color: '#666' }}>
1211
+ {config.chartMessage?.noData || 'No Data Available'}
1078
1212
  </div>
1079
1213
  )}
1080
1214
 
1081
- {config.visualizationType === 'Pie' && (
1215
+ {/* All charts with LinearChart */}
1216
+ {filteredData &&
1217
+ filteredData.length > 0 &&
1218
+ !['Spark Line', 'Line', 'Sankey', 'Pie', 'Sankey'].includes(config.visualizationType) && (
1219
+ <div ref={parentRef} style={{ width: `100%` }}>
1220
+ <ParentSize>
1221
+ {parent => (
1222
+ <LinearChart ref={svgRef} parentWidth={parent.width} parentHeight={parent.height} />
1223
+ )}
1224
+ </ParentSize>
1225
+ </div>
1226
+ )}
1227
+
1228
+ {filteredData && filteredData.length > 0 && config.visualizationType === 'Pie' && (
1082
1229
  <ParentSize className='justify-content-center d-flex' style={{ width: `100%` }}>
1083
1230
  {parent => (
1084
1231
  <PieChart
@@ -1091,7 +1238,9 @@ const CdcChart: React.FC<CdcChartProps> = ({
1091
1238
  </ParentSize>
1092
1239
  )}
1093
1240
  {/* Line Chart */}
1094
- {config.visualizationType === 'Line' &&
1241
+ {filteredData &&
1242
+ filteredData.length > 0 &&
1243
+ config.visualizationType === 'Line' &&
1095
1244
  (convertLineToBarGraph ? (
1096
1245
  <div ref={parentRef} style={{ width: `100%` }}>
1097
1246
  <ParentSize>
@@ -1202,34 +1351,51 @@ const CdcChart: React.FC<CdcChartProps> = ({
1202
1351
  config.table.show &&
1203
1352
  config.visualizationType !== 'Spark Line' &&
1204
1353
  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={
1354
+ (config.visualizationType === 'Sankey' && config.table.show)) &&
1355
+ (() => {
1356
+ let dataTableConfig = pivotDynamicSeries(config)
1357
+ let dataTableColumns = config.columns
1358
+ let dataTableRuntimeData = getTableRuntimeData()
1359
+ let dataTableRawData =
1212
1360
  config.visualizationType === 'Sankey'
1213
1361
  ? config?.data?.[0]?.tableData
1214
1362
  : config.table.customTableConfig
1215
1363
  ? filterVizData(config.filters, config.data)
1216
1364
  : config.data
1365
+
1366
+ if (config.smallMultiples?.mode) {
1367
+ const prepared = prepareSmallMultiplesDataTable(config, config.columns, dataTableRuntimeData)
1368
+ dataTableConfig = prepared.config
1369
+ dataTableColumns = prepared.columns
1370
+ dataTableRuntimeData = prepared.runtimeData
1371
+ if (config.smallMultiples.mode === 'by-column') {
1372
+ dataTableRawData = prepared.config.data
1373
+ }
1217
1374
  }
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
- )}
1375
+
1376
+ return (
1377
+ <DataTable
1378
+ /* changing the "key" will force the table to re-render
1379
+ when the default sort changes while editing */
1380
+ key={dataTableDefaultSortBy}
1381
+ config={dataTableConfig}
1382
+ rawData={dataTableRawData}
1383
+ runtimeData={dataTableRuntimeData}
1384
+ expandDataTable={config.table.expanded}
1385
+ columns={dataTableColumns}
1386
+ defaultSortBy={dataTableDefaultSortBy}
1387
+ displayGeoName={name => name}
1388
+ applyLegendToRow={applyLegendToRow}
1389
+ tableTitle={config.table.label}
1390
+ indexTitle={config.table.indexLabel}
1391
+ vizTitle={title}
1392
+ viewport={currentViewport}
1393
+ tabbingId={handleChartTabbing(config, legendId)}
1394
+ colorScale={colorScale}
1395
+ interactionLabel={interactionLabel}
1396
+ />
1397
+ )
1398
+ })()}
1233
1399
  {config?.annotations?.length > 0 && <Annotation.Dropdown />}
1234
1400
  {/* show pdf or image button */}
1235
1401
  {processedLegacyFootnotes && (
@@ -1239,6 +1405,9 @@ const CdcChart: React.FC<CdcChartProps> = ({
1239
1405
  <FootnotesStandAlone
1240
1406
  config={configObj.footnotes}
1241
1407
  filters={config.filters?.filter(f => f.filterFootnotes)}
1408
+ markupVariables={config.markupVariables}
1409
+ enableMarkupVariables={config.enableMarkupVariables}
1410
+ data={config.data}
1242
1411
  />
1243
1412
  </div>
1244
1413
  )}
@@ -0,0 +1,18 @@
1
+ // generate a combo chart story
2
+ import React from 'react'
3
+ import { Meta, Story } from '@storybook/react'
4
+ import CdcChart from '@cdc/chart/src/CdcChart'
5
+ import comboChartConfig from './_mock/combo.json'
6
+
7
+ export default {
8
+ title: 'Components/Templates/Chart/Combo Chart',
9
+ component: CdcChart
10
+ } as Meta
11
+
12
+ const Template: Story = args => <CdcChart {...args} />
13
+
14
+ export const ComboChart = Template.bind({})
15
+ ComboChart.args = {
16
+ config: comboChartConfig,
17
+ isEditor: true
18
+ }
@@ -0,0 +1,36 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+
3
+ import Chart from '../CdcChart'
4
+ import forecastComboWithGaps from './_mock/forecast_combo_with_gaps.json'
5
+
6
+ const meta: Meta<typeof Chart> = {
7
+ title: 'Components/Templates/Chart/Forecast Chart',
8
+ component: Chart,
9
+ parameters: {
10
+ docs: {
11
+ description: {
12
+ component:
13
+ 'Forecast charts with gap handling. This story demonstrates how forecast charts properly handle gaps in data by filtering invalid values (NA) and splitting segments at gaps.'
14
+ }
15
+ }
16
+ }
17
+ }
18
+
19
+ type Story = StoryObj<typeof Chart>
20
+
21
+ export const Forecast_Combo_With_Gaps: Story = {
22
+ args: {
23
+ config: forecastComboWithGaps,
24
+ isEditor: true
25
+ },
26
+ parameters: {
27
+ docs: {
28
+ description: {
29
+ story:
30
+ 'A combo chart with forecast confidence intervals that demonstrates proper gap handling. The forecast areas and lines are split into separate segments when gaps are detected, preventing misleading connections across missing data periods. Invalid data points (NA values) are automatically filtered out.'
31
+ }
32
+ }
33
+ }
34
+ }
35
+
36
+ export default meta