@cdc/chart 4.26.3 → 4.26.4

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 (128) hide show
  1. package/CONFIG.md +169 -0
  2. package/README.md +42 -17
  3. package/dist/cdcchart.js +34725 -33730
  4. package/examples/feature/bar/additional-column-tooltip.json +3 -3
  5. package/examples/feature/deviation/planet-deviation-config.json +2 -2
  6. package/examples/feature/paired-bar/paired-bar-example.json +2 -2
  7. package/examples/feature/tests-case-rate/case-rate-example-config.json +2 -2
  8. package/examples/feature/tests-covid/covid-confidence-example-config.json +2 -2
  9. package/examples/feature/tests-covid/covid-example-config.json +2 -2
  10. package/examples/feature/tests-cutoff/cutoff-example-config.json +2 -2
  11. package/examples/feature/tests-non-numerics/planet-pie-example-config-nonnumeric.json +2 -2
  12. package/examples/metadata-variables.json +1 -1
  13. package/examples/minimal-example.json +24 -0
  14. package/examples/private/test2.json +35012 -0
  15. package/package.json +4 -4
  16. package/src/CdcChartComponent.tsx +9 -6
  17. package/src/_stories/{Chart.HTMLInDataTable.stories.tsx → Chart.HTMLInDataTable.smoke.stories.tsx} +10 -0
  18. package/src/_stories/Chart.Minimal.smoke.stories.tsx +25 -0
  19. package/src/_stories/{Chart.SmallMultiples.stories.tsx → Chart.SmallMultiples.smoke.stories.tsx} +17 -1
  20. package/src/_stories/{Chart.stories.tsx → Chart.smoke.stories.tsx} +12 -0
  21. package/src/_stories/{Chart.tooltip.stories.tsx → Chart.tooltip.smoke.stories.tsx} +68 -0
  22. package/src/_stories/ChartBar.Editor.stories.tsx +88 -85
  23. package/src/_stories/ChartEditor.Editor.stories.tsx +23 -0
  24. package/src/_stories/ChartLine.DashedDates.smoke.stories.tsx +23 -0
  25. package/src/_stories/_mock/line_chart_dashed_dates.json +276 -0
  26. package/src/_stories/_mock/paired-bar-abbr.json +1 -1
  27. package/src/_stories/_mock/paired-bar.json +1 -1
  28. package/src/components/Annotations/components/AnnotationDraggable.tsx +1 -1
  29. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +10 -2
  30. package/src/components/Axis/BottomAxis.tsx +1 -1
  31. package/src/components/Axis/LeftAxis.tsx +2 -4
  32. package/src/components/Axis/LeftAxisGridlines.tsx +1 -3
  33. package/src/components/BarChart/components/BarChart.Horizontal.tsx +9 -5
  34. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +9 -5
  35. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +9 -5
  36. package/src/components/BarChart/components/BarChart.Vertical.tsx +9 -6
  37. package/src/components/BarChart/components/BarChart.tsx +2 -6
  38. package/src/components/BarChart/helpers/useBarChart.ts +1 -1
  39. package/src/components/BoxPlot/BoxPlot.Vertical.tsx +3 -3
  40. package/src/components/EditorPanel/EditorPanel.tsx +633 -908
  41. package/src/components/EditorPanel/EditorPanelContext.ts +1 -1
  42. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +216 -165
  43. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +168 -116
  44. package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +154 -109
  45. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +3 -3
  46. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +12 -26
  47. package/src/components/EditorPanel/useEditorPermissions.ts +1 -10
  48. package/src/components/Forecasting/Forecasting.tsx +2 -2
  49. package/src/components/HorizonChart/HorizonChart.tsx +10 -2
  50. package/src/components/HoverLine/HoverLine.tsx +6 -8
  51. package/src/components/Legend/Legend.Component.tsx +4 -4
  52. package/src/components/Legend/LegendGroup/LegendGroup.styles.css +5 -0
  53. package/src/components/Legend/LegendGroup/LegendGroup.tsx +3 -1
  54. package/src/components/Legend/helpers/index.ts +0 -2
  55. package/src/components/LineChart/LineChartProps.ts +1 -0
  56. package/src/components/LineChart/components/LineChart.BumpCircle.tsx +3 -2
  57. package/src/components/LineChart/index.tsx +11 -4
  58. package/src/components/LinearChart/VisualizationRenderer.tsx +11 -1
  59. package/src/components/LinearChart/tests/LinearChart.test.tsx +147 -0
  60. package/src/components/LinearChart.tsx +98 -41
  61. package/src/components/PairedBarChart.jsx +2 -2
  62. package/src/components/PieChart/PieChart.tsx +11 -9
  63. package/src/components/Regions/components/Regions.tsx +30 -20
  64. package/src/components/ScatterPlot/ScatterPlot.jsx +19 -17
  65. package/src/components/SmallMultiples/SmallMultipleTile.tsx +1 -0
  66. package/src/components/Sparkline/components/SparkLine.tsx +1 -1
  67. package/src/components/WarmingStripes/WarmingStripes.tsx +5 -3
  68. package/src/data/initial-state.js +1 -0
  69. package/src/helpers/calculateLeftYAxisWidth.ts +193 -0
  70. package/src/helpers/tests/tooltipHelpers.test.ts +151 -0
  71. package/src/helpers/tooltipHelpers.ts +176 -0
  72. package/src/hooks/useHighlightedBars.ts +9 -16
  73. package/src/hooks/useProgrammaticTooltip.ts +16 -2
  74. package/src/hooks/useSmallMultipleSynchronization.ts +3 -4
  75. package/src/hooks/useTooltip.tsx +167 -38
  76. package/src/test/CdcChart.test.jsx +24 -0
  77. package/src/types/ChartConfig.ts +1 -0
  78. package/LICENSE +0 -201
  79. package/examples/chart-regression-2.json +0 -2360
  80. package/examples/feature/__data__/age-adjusted-rates.json +0 -1486
  81. package/examples/feature/__data__/city-temperature.json +0 -2198
  82. package/examples/feature/bar/Barchart_with_negative.json +0 -34
  83. package/examples/feature/boxplot/box-plot-data.json +0 -71
  84. package/examples/feature/boxplot/boxplot-data.json +0 -156
  85. package/examples/feature/combo/combochart-categories_are_numbers .json +0 -18
  86. package/examples/feature/forest-plot/log.json +0 -26
  87. package/examples/feature/tests-date-exclusions/date-exclusions-data.json +0 -162
  88. package/examples/region-issue.json +0 -2065
  89. package/examples/xaxis.json +0 -493
  90. /package/examples/{data → __data__}/data-with-metadata.json +0 -0
  91. /package/examples/feature/bar/{tall-data.json → __data__/tall-data.json} +0 -0
  92. /package/examples/feature/deviation/{planet-deviation-data.json → __data__/planet-deviation-data.json} +0 -0
  93. /package/examples/feature/paired-bar/{paired-bar-data.json → __data__/paired-bar-data.json} +0 -0
  94. /package/examples/feature/paired-bar/{paired-bar-formatted.json → __data__/paired-bar-formatted.json} +0 -0
  95. /package/examples/feature/tests-case-rate/{case-rate-example-data.json → __data__/case-rate-example-data.json} +0 -0
  96. /package/examples/feature/tests-covid/{covid-example-data-confidence.json → __data__/covid-example-data-confidence.json} +0 -0
  97. /package/examples/feature/tests-covid/{covid-example-data.json → __data__/covid-example-data.json} +0 -0
  98. /package/examples/feature/tests-cutoff/{cutoff-example-data.json → __data__/cutoff-example-data.json} +0 -0
  99. /package/examples/feature/tests-non-numerics/{planet-example-data-nonnumeric.json → __data__/planet-example-data-nonnumeric.json} +0 -0
  100. /package/src/_stories/{Chart.Anchors.stories.tsx → Chart.Anchors.smoke.stories.tsx} +0 -0
  101. /package/src/_stories/{Chart.BoxPlot.stories.tsx → Chart.BoxPlot.smoke.stories.tsx} +0 -0
  102. /package/src/_stories/{Chart.CI.stories.tsx → Chart.CI.smoke.stories.tsx} +0 -0
  103. /package/src/_stories/{Chart.Combo.stories.tsx → Chart.Combo.smoke.stories.tsx} +0 -0
  104. /package/src/_stories/{Chart.CustomColors.stories.tsx → Chart.CustomColors.smoke.stories.tsx} +0 -0
  105. /package/src/_stories/{Chart.Defaults.stories.tsx → Chart.Defaults.smoke.stories.tsx} +0 -0
  106. /package/src/_stories/{Chart.DynamicSeries.stories.tsx → Chart.DynamicSeries.smoke.stories.tsx} +0 -0
  107. /package/src/_stories/{Chart.Filters.stories.tsx → Chart.Filters.smoke.stories.tsx} +0 -0
  108. /package/src/_stories/{Chart.Forecast.stories.tsx → Chart.Forecast.smoke.stories.tsx} +0 -0
  109. /package/src/_stories/{Chart.Legend.Gradient.stories.tsx → Chart.Legend.Gradient.smoke.stories.tsx} +0 -0
  110. /package/src/_stories/{Chart.Patterns.stories.tsx → Chart.Patterns.smoke.stories.tsx} +0 -0
  111. /package/src/_stories/{Chart.PreserveDecimals.stories.tsx → Chart.PreserveDecimals.smoke.stories.tsx} +0 -0
  112. /package/src/_stories/{Chart.Regions.Categorical.stories.tsx → Chart.Regions.Categorical.smoke.stories.tsx} +0 -0
  113. /package/src/_stories/{Chart.Regions.DateScale.stories.tsx → Chart.Regions.DateScale.smoke.stories.tsx} +0 -0
  114. /package/src/_stories/{Chart.Regions.DateTimeScale.stories.tsx → Chart.Regions.DateTimeScale.smoke.stories.tsx} +0 -0
  115. /package/src/_stories/{Chart.ScatterPlot.stories.tsx → Chart.ScatterPlot.smoke.stories.tsx} +0 -0
  116. /package/src/_stories/{Chart.SmallestLeftAxisMax.stories.tsx → Chart.SmallestLeftAxisMax.smoke.stories.tsx} +0 -0
  117. /package/src/_stories/{ChartAnnotation.stories.tsx → ChartAnnotation.smoke.stories.tsx} +0 -0
  118. /package/src/_stories/{ChartAxisLabels.stories.tsx → ChartAxisLabels.smoke.stories.tsx} +0 -0
  119. /package/src/_stories/{ChartAxisTitles.stories.tsx → ChartAxisTitles.smoke.stories.tsx} +0 -0
  120. /package/src/_stories/{ChartBrush.Matrix.Continuous.stories.tsx → ChartBrush.Matrix.Continuous.smoke.stories.tsx} +0 -0
  121. /package/src/_stories/{ChartBrush.Matrix.Date.stories.tsx → ChartBrush.Matrix.Date.smoke.stories.tsx} +0 -0
  122. /package/src/_stories/{ChartBrush.Matrix.DateTime.stories.tsx → ChartBrush.Matrix.DateTime.smoke.stories.tsx} +0 -0
  123. /package/src/_stories/{ChartBrush.stories.tsx → ChartBrush.smoke.stories.tsx} +0 -0
  124. /package/src/_stories/{ChartLine.QuadrantAngles.stories.tsx → ChartLine.QuadrantAngles.smoke.stories.tsx} +0 -0
  125. /package/src/_stories/{ChartLine.Suppression.stories.tsx → ChartLine.Suppression.smoke.stories.tsx} +0 -0
  126. /package/src/_stories/{ChartLine.Symbols.stories.tsx → ChartLine.Symbols.smoke.stories.tsx} +0 -0
  127. /package/src/_stories/{ChartPrefixSuffix.stories.tsx → ChartPrefixSuffix.smoke.stories.tsx} +0 -0
  128. /package/src/_stories/{TechAdoptionWithLinks.stories.tsx → TechAdoptionWithLinks.smoke.stories.tsx} +0 -0
@@ -35,6 +35,7 @@ import { useTickFormatters } from './LinearChart/utils/tickFormatting'
35
35
  import { isLegendWrapViewport, isMobileFontViewport } from '@cdc/core/helpers/viewports'
36
36
  import { calcInitialHeight } from '../helpers/sizeHelpers'
37
37
  import { calculateHorizontalBarCategoryLabelWidth } from '../helpers/calculateHorizontalBarCategoryLabelWidth'
38
+ import { calculateLeftYAxisWidth } from '../helpers/calculateLeftYAxisWidth'
38
39
 
39
40
  // Hooks
40
41
  import useReduceData from '../hooks/useReduceData'
@@ -51,6 +52,7 @@ import Annotation from './Annotations'
51
52
  import { countNumOfTicks } from '../helpers/countNumOfTicks'
52
53
  import HoverLine from './HoverLine/HoverLine'
53
54
  import { SmallMultiples } from './SmallMultiples'
55
+ import { type TooltipDisplayData } from '../helpers/tooltipHelpers'
54
56
 
55
57
  type LinearChartProps = {
56
58
  parentWidth: number
@@ -62,6 +64,7 @@ const BOTTOM_LABEL_PADDING = 9
62
64
  const X_TICK_LABEL_PADDING = 4.5
63
65
  const DEFAULT_TICK_LENGTH = 8
64
66
  const DEFAULT_MAX_TICK_ROTATION = 90
67
+ const DEFAULT_LEFT_Y_AXIS_WIDTH = 50
65
68
 
66
69
  // Font sizes
67
70
  const TICK_LABEL_FONT_SIZE = 16
@@ -88,10 +91,7 @@ const WARMING_STRIPES_HEIGHT = 78
88
91
  // Time constants
89
92
  const MONTH_AS_MS = 1000 * 60 * 60 * 24 * 30
90
93
 
91
- type TooltipData = {
92
- dataXPosition?: number
93
- dataYPosition?: number
94
- }
94
+ type TooltipData = Partial<TooltipDisplayData>
95
95
 
96
96
  type UseTooltipReturn<T = TooltipData> = {
97
97
  tooltipData: T | null
@@ -176,8 +176,6 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
176
176
  const isDateTime = config.xAxis.type === 'date-time'
177
177
  const inlineLabelHasNoSpace = !inlineLabel?.includes(' ')
178
178
  const needsYAxisAutoPadding = inlineLabel && !inlineLabelHasNoSpace
179
- const padding = orientation === 'horizontal' ? Number(config.xAxis.size) : Number(config.yAxis.size)
180
- const yLabelOffset = isNaN(parseInt(`${runtime.yAxis.labelOffset}`)) ? 0 : parseInt(`${runtime.yAxis.labelOffset}`)
181
179
  const tickLabelFontSize = isMobileFontViewport(vizViewport) ? TICK_LABEL_FONT_SIZE_SMALL : TICK_LABEL_FONT_SIZE
182
180
  const axisLabelFontSize = isMobileFontViewport(vizViewport) ? AXIS_LABEL_FONT_SIZE_SMALL : AXIS_LABEL_FONT_SIZE
183
181
  const GET_TEXT_WIDTH_FONT = `normal ${tickLabelFontSize}px Nunito, sans-serif`
@@ -258,11 +256,9 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
258
256
 
259
257
  const { yScaleRight, hasRightAxis } = useRightAxis({ config, yMax, data })
260
258
 
261
- // State for computed y-axis width - allows re-render when horizontal bar label space is calculated
262
- const [computedYAxisWidth, setComputedYAxisWidth] = useState<number | null>(null)
263
-
264
- // Use computed width if available, otherwise fall back to config value
265
- const yAxisWidth = computedYAxisWidth ?? Number(runtime.yAxis.size)
259
+ // State for computed left-axis width - shared across all linear-chart types.
260
+ const [currentYAxisWidth, setCurrentYAxisWidth] = useState<number>(DEFAULT_LEFT_Y_AXIS_WIDTH)
261
+ const yAxisWidth = currentYAxisWidth
266
262
 
267
263
  // Chart width calculation using the current y-axis width
268
264
  const xMax = parentWidth - yAxisWidth - (hasRightAxis ? config.yAxis.rightAxisSize : 0)
@@ -303,39 +299,77 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
303
299
  shouldAbbreviate
304
300
  })
305
301
 
306
- // Calculate category label space for horizontal bar charts
302
+ // Horizontal charts swap axes at runtime, but the left-side category label visibility
303
+ // still follows config.yAxis.hideLabel to match the actual axis renderers.
307
304
  const categoryLabelSpace = useMemo(() => {
305
+ if (config.yAxis.hideLabel) return 0
306
+
308
307
  return calculateHorizontalBarCategoryLabelWidth({
309
308
  yScale,
310
309
  chartWidth: parentWidth,
311
310
  formatDate,
312
311
  parseDate,
313
312
  tickLabelFont: GET_TEXT_WIDTH_FONT,
314
- xAxisType: config.runtime.xAxis?.type,
313
+ xAxisType: runtime.xAxis?.type,
315
314
  labelPlacement: config.yAxis.labelPlacement
316
315
  })
317
- }, [isHorizontal, config.visualizationType, config.yAxis.labelPlacement, yScale, parentWidth])
316
+ }, [
317
+ config.yAxis.hideLabel,
318
+ config.yAxis.labelPlacement,
319
+ yScale,
320
+ parentWidth,
321
+ formatDate,
322
+ parseDate,
323
+ GET_TEXT_WIDTH_FONT,
324
+ runtime.xAxis?.type
325
+ ])
318
326
 
319
327
  const horizontalYAxisLabelSpace = runtime.yAxis.label && !config.hideYAxisLabel ? 30 : 0
320
328
 
321
- // Update y-axis width state when computed value changes (for horizontal bar charts)
322
- useEffect(() => {
323
- if (isHorizontal && config.visualizationType === 'Bar') {
324
- const newWidth = categoryLabelSpace + horizontalYAxisLabelSpace
325
- if (newWidth !== computedYAxisWidth) {
326
- setComputedYAxisWidth(newWidth)
327
- }
328
- } else if (computedYAxisWidth !== null) {
329
- // Reset to null for non-horizontal bar charts so we use config value
330
- setComputedYAxisWidth(null)
331
- }
332
- }, [isHorizontal, config.visualizationType, categoryLabelSpace, horizontalYAxisLabelSpace, computedYAxisWidth])
333
-
334
329
  const [yTickCount, xTickCount] = ['yAxis', 'xAxis'].map(axis =>
335
330
  countNumOfTicks({ axis, max, runtime, currentViewport, isHorizontal, data, config, min })
336
331
  )
337
332
  const handleNumTicks = isForestPlot ? config.data.length : yTickCount
338
333
 
334
+ const calculatedYAxisWidth = useMemo(() => {
335
+ if (isHorizontal && config.visualizationType === 'Bar') {
336
+ return categoryLabelSpace + horizontalYAxisLabelSpace
337
+ }
338
+
339
+ return calculateLeftYAxisWidth({
340
+ config,
341
+ data,
342
+ yScale,
343
+ numTicks: handleNumTicks,
344
+ parentWidth,
345
+ tickLabelFont: GET_TEXT_WIDTH_FONT,
346
+ axisLabelFontSize,
347
+ handleLeftTickFormatting,
348
+ categoryLabelSpace,
349
+ horizontalYAxisLabelSpace
350
+ })
351
+ }, [
352
+ config,
353
+ data,
354
+ yScale,
355
+ handleNumTicks,
356
+ parentWidth,
357
+ isHorizontal,
358
+ GET_TEXT_WIDTH_FONT,
359
+ axisLabelFontSize,
360
+ handleLeftTickFormatting,
361
+ categoryLabelSpace,
362
+ horizontalYAxisLabelSpace
363
+ ])
364
+
365
+ useLayoutEffect(() => {
366
+ if (parentWidth <= 0) return
367
+
368
+ if (calculatedYAxisWidth !== currentYAxisWidth) {
369
+ setCurrentYAxisWidth(calculatedYAxisWidth)
370
+ }
371
+ }, [calculatedYAxisWidth, currentYAxisWidth, parentWidth])
372
+
339
373
  // Tooltip Helpers
340
374
 
341
375
  const { tooltipData, showTooltip, hideTooltip, tooltipOpen, tooltipLeft, tooltipTop } =
@@ -353,6 +387,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
353
387
  xScale,
354
388
  yScale,
355
389
  seriesScale,
390
+ yAxisWidth,
356
391
  showTooltip,
357
392
  hideTooltip,
358
393
  interactionLabel
@@ -387,7 +422,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
387
422
  return manualStep
388
423
  }, [config.xAxis.manualStep, config.xAxis.viewportStepCount, currentViewport])
389
424
 
390
- const smallMultiplesSync = useSmallMultipleSynchronization(xMax, yMax, getXValueFromCoordinate)
425
+ const smallMultiplesSync = useSmallMultipleSynchronization(xMax, yMax, yAxisWidth, getXValueFromCoordinate)
391
426
 
392
427
  const onMouseMove = event => {
393
428
  const svgRect = event.currentTarget.getBoundingClientRect()
@@ -416,7 +451,9 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
416
451
  const internalSvgRef = useProgrammaticTooltip({
417
452
  svgRef,
418
453
  getCoordinateFromXValue,
454
+ getXValueFromCoordinate,
419
455
  config,
456
+ yAxisWidth,
420
457
  setPoint,
421
458
  setShowHoverLine,
422
459
  handleTooltipMouseOver,
@@ -589,7 +626,6 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
589
626
  xMax={xMax}
590
627
  yAxisWidth={yAxisWidth}
591
628
  numTicks={handleNumTicks}
592
- yLabelOffset={yLabelOffset}
593
629
  axisLabelFontSize={axisLabelFontSize}
594
630
  />
595
631
  )}
@@ -673,8 +709,8 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
673
709
  strokeDasharray={handleLineType(anchor.lineStyle)}
674
710
  stroke={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
675
711
  className='anchor-y'
676
- from={{ x: Number(runtime.yAxis.size), y: position - middleOffset }}
677
- to={{ x: Number(runtime.yAxis.size) + Number(xMax), y: position - middleOffset }}
712
+ from={{ x: Number(yAxisWidth), y: position - middleOffset }}
713
+ to={{ x: Number(yAxisWidth) + Number(xMax), y: position - middleOffset }}
678
714
  />
679
715
  )
680
716
  })}
@@ -710,8 +746,8 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
710
746
  stroke={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
711
747
  fill={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
712
748
  className='anchor-x'
713
- from={{ x: Number(anchorPosition) + Number(padding), y: 0 }}
714
- to={{ x: Number(anchorPosition) + Number(padding), y: yMax }}
749
+ from={{ x: Number(anchorPosition) + Number(yAxisWidth), y: 0 }}
750
+ to={{ x: Number(anchorPosition) + Number(yAxisWidth), y: yMax }}
715
751
  />
716
752
  )
717
753
  })}
@@ -728,6 +764,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
728
764
  tooltipData={tooltipData}
729
765
  yMax={yMax}
730
766
  xMax={xMax}
767
+ yAxisWidth={yAxisWidth}
731
768
  />
732
769
  )}
733
770
  {isNoDataAvailable && (
@@ -741,8 +778,22 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
741
778
  )}
742
779
  {showHoverLine && (
743
780
  <>
744
- <HoverLine xMax={xMax} yMax={yMax} point={point} tooltipData={tooltipData} orientation='horizontal' />
745
- <HoverLine xMax={xMax} yMax={yMax} point={point} tooltipData={tooltipData} orientation='vertical' />
781
+ <HoverLine
782
+ xMax={xMax}
783
+ yMax={yMax}
784
+ point={point}
785
+ tooltipData={tooltipData}
786
+ orientation='horizontal'
787
+ yAxisWidth={yAxisWidth}
788
+ />
789
+ <HoverLine
790
+ xMax={xMax}
791
+ yMax={yMax}
792
+ point={point}
793
+ tooltipData={tooltipData}
794
+ orientation='vertical'
795
+ yAxisWidth={yAxisWidth}
796
+ />
746
797
  </>
747
798
  )}
748
799
  <Group left={yAxisWidth}>
@@ -778,7 +829,6 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
778
829
  suffixWidth={suffixWidth}
779
830
  horizontalYAxisLabelSpace={horizontalYAxisLabelSpace}
780
831
  categoryLabelSpace={categoryLabelSpace}
781
- yLabelOffset={yLabelOffset}
782
832
  />
783
833
  )}
784
834
  {config.yAxis.type === 'categorical' && config.orientation === 'vertical' && (
@@ -839,10 +889,10 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
839
889
  </svg>
840
890
  {!isDraggingAnnotation &&
841
891
  tooltipData &&
842
- Object.entries(tooltipData.data).length > 0 &&
892
+ tooltipData.data?.length > 0 &&
843
893
  tooltipOpen &&
844
894
  showTooltip &&
845
- !tooltipData?.data?.some(subArray => subArray.some(item => item === undefined)) &&
895
+ !tooltipData?.data?.some(row => row?.value === undefined) &&
846
896
  tooltipData.dataYPosition &&
847
897
  tooltipData.dataXPosition &&
848
898
  !config.tooltips.singleSeries && (
@@ -860,9 +910,16 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
860
910
  >
861
911
  <ul>
862
912
  {typeof tooltipData === 'object' &&
863
- Object.entries(tooltipData.data)
864
- .filter(([_, values]) => Array.isArray(values) && !values.includes(undefined))
865
- .map((item, index) => <TooltipListItem item={item} key={index} />)}
913
+ tooltipData.data
914
+ .filter(row => row && row.value !== undefined)
915
+ .map((row, index) => (
916
+ <TooltipListItem
917
+ row={row}
918
+ index={index}
919
+ key={index}
920
+ useMarkerColumn={tooltipData.useMarkerColumn}
921
+ />
922
+ ))}
866
923
  </ul>
867
924
  </TooltipWithBounds>
868
925
  </>
@@ -10,7 +10,7 @@ import { APP_FONT_COLOR } from '@cdc/core/helpers/constants'
10
10
  import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
11
11
  import { isMobileFontViewport } from '@cdc/core/helpers/viewports'
12
12
 
13
- const PairedBarChart = ({ width, height, originalWidth }) => {
13
+ const PairedBarChart = ({ width, height, originalWidth, yAxisWidth }) => {
14
14
  const {
15
15
  config,
16
16
  colorScale,
@@ -93,7 +93,7 @@ const PairedBarChart = ({ width, height, originalWidth }) => {
93
93
  id='cdc-visualization__paired-bar-chart'
94
94
  width={originalWidth}
95
95
  height={height}
96
- viewBox={`0 0 ${width + Number(config.runtime.yAxis.size)} ${height}`}
96
+ viewBox={`0 0 ${width + yAxisWidth} ${height}`}
97
97
  role='img'
98
98
  tabIndex={0}
99
99
  >
@@ -24,14 +24,9 @@ import { handleChartAriaLabels } from '../../helpers/handleChartAriaLabels'
24
24
  import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
25
25
  import { scaleOrdinal } from '@visx/scale'
26
26
  import { getContrastColor } from '@cdc/core/helpers/cove/accessibility'
27
+ import { type TooltipDisplayData } from '../../helpers/tooltipHelpers'
27
28
 
28
- type TooltipData = {
29
- data: {
30
- [key: string]: string | number
31
- }
32
- dataXPosition: number
33
- dataYPosition: number
34
- }
29
+ type TooltipData = TooltipDisplayData
35
30
 
36
31
  type PieChartProps = {
37
32
  parentWidth?: number
@@ -426,7 +421,7 @@ const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) =>
426
421
  <div ref={triggerRef} />
427
422
  {!isDraggingAnnotation &&
428
423
  tooltipData &&
429
- Object.entries(tooltipData.data).length > 0 &&
424
+ tooltipData.data?.length > 0 &&
430
425
  tooltipOpen &&
431
426
  showTooltip &&
432
427
  tooltipData.dataYPosition &&
@@ -442,7 +437,14 @@ const PieChart = React.forwardRef<SVGSVGElement, PieChartProps>((props, ref) =>
442
437
  >
443
438
  <ul>
444
439
  {typeof tooltipData === 'object' &&
445
- Object.entries(tooltipData.data).map((item, index) => <TooltipListItem item={item} key={index} />)}
440
+ tooltipData.data.map((row, index) => (
441
+ <TooltipListItem
442
+ row={row}
443
+ index={index}
444
+ key={index}
445
+ useMarkerColumn={tooltipData.useMarkerColumn}
446
+ />
447
+ ))}
446
448
  </ul>
447
449
  </TooltipWithBounds>
448
450
  </>
@@ -10,7 +10,8 @@ const VIZ_TYPES = {
10
10
  BAR: 'Bar',
11
11
  LINE: 'Line',
12
12
  AREA: 'Area Chart',
13
- COMBO: 'Combo'
13
+ COMBO: 'Combo',
14
+ HORIZON: 'Horizon Chart'
14
15
  } as const
15
16
 
16
17
  type Region = {
@@ -35,6 +36,7 @@ type RegionsProps = {
35
36
  barWidth?: number
36
37
  totalBarsInGroup?: number
37
38
  xMax?: number
39
+ yAxisWidth?: number
38
40
  }
39
41
 
40
42
  type HighlightedAreaProps = {
@@ -63,14 +65,22 @@ const findClosestDate = <T,>(targetTime: number, domain: T[], getTime: (d: T) =>
63
65
  return closest
64
66
  }
65
67
 
66
- /** Check if visualization type is line-like (Line or Area Chart) */
67
- const isLineLike = (type: string): boolean => type === VIZ_TYPES.LINE || type === VIZ_TYPES.AREA
68
+ /** Check if visualization type is line-like (Line, Area Chart, or Horizon Chart) */
69
+ const isLineLike = (type: string): boolean =>
70
+ type === VIZ_TYPES.LINE || type === VIZ_TYPES.AREA || type === VIZ_TYPES.HORIZON
68
71
 
69
72
  /** Check if visualization type is bar-like (Bar or Combo) */
70
73
  const isBarLike = (type: string): boolean => type === VIZ_TYPES.BAR || type === VIZ_TYPES.COMBO
71
74
 
72
75
  // TODO: should regions be removed on categorical axis?
73
- const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGroup = 1, yMax, xMax }) => {
76
+ const Regions: React.FC<RegionsProps> = ({
77
+ xScale,
78
+ barWidth = 0,
79
+ totalBarsInGroup = 1,
80
+ yMax,
81
+ xMax,
82
+ yAxisWidth = 0
83
+ }) => {
74
84
  const { parseDate, config } = useContext<ChartContext>(ConfigContext)
75
85
 
76
86
  const { regions, visualizationType, orientation, xAxis } = config
@@ -122,8 +132,8 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
122
132
  } else {
123
133
  from = xScale(region.from)
124
134
  }
125
- // Add left padding (yAxis.size) + half bandwidth to center on the category
126
- let scalePadding = Number(config.yAxis.size)
135
+ // Add left padding (yAxisWidth) + half bandwidth to center on the category
136
+ let scalePadding = yAxisWidth
127
137
  if (xScale.bandwidth) {
128
138
  scalePadding += xScale.bandwidth() / 2
129
139
  }
@@ -135,8 +145,8 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
135
145
  return calculateLineLastDatePosition_Categorical()
136
146
  }
137
147
  let to = xScale(region.to)
138
- // Add left padding (yAxis.size) + half bandwidth
139
- let scalePadding = Number(config.yAxis.size)
148
+ // Add left padding (yAxisWidth) + half bandwidth
149
+ let scalePadding = yAxisWidth
140
150
  if (xScale.bandwidth) {
141
151
  scalePadding += xScale.bandwidth() / 2
142
152
  }
@@ -158,8 +168,8 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
158
168
  const closestDate = findClosestDate(parsedDate, domain, d => d)
159
169
  from = xScale(closestDate)
160
170
  }
161
- // Add left padding (yAxis.size) + half bandwidth
162
- let scalePadding = Number(config.yAxis.size)
171
+ // Add left padding (yAxisWidth) + half bandwidth
172
+ let scalePadding = yAxisWidth
163
173
  if (xScale.bandwidth) {
164
174
  scalePadding += xScale.bandwidth() / 2
165
175
  }
@@ -179,8 +189,8 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
179
189
  const closestDate = findClosestDate(parsedDate, domain, d => d)
180
190
  let to = xScale(closestDate)
181
191
 
182
- // Add left padding (yAxis.size) + half bandwidth
183
- let scalePadding = Number(config.yAxis.size)
192
+ // Add left padding (yAxisWidth) + half bandwidth
193
+ let scalePadding = yAxisWidth
184
194
  if (xScale.bandwidth) {
185
195
  scalePadding += xScale.bandwidth() / 2
186
196
  }
@@ -190,13 +200,13 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
190
200
  const getLineFromValue_DateTime = (region: Region): number => {
191
201
  if (region.fromType === 'Previous Days') {
192
202
  const from = calculatePreviousDaysFrom(region, 'date-time')
193
- return from + Number(config.yAxis.size)
203
+ return from + yAxisWidth
194
204
  }
195
205
  const date = new Date(region.from)
196
206
  const parsedDate = parseDate(formatDate(config.xAxis.dateParseFormat, date, config.locale)).getTime()
197
207
  let from = xScale(parsedDate)
198
208
  // For date-time, xScale returns correct position (no bandwidth), just add left padding
199
- return from + Number(config.yAxis.size)
209
+ return from + yAxisWidth
200
210
  }
201
211
 
202
212
  const getLineToValue_DateTime = (region: Region): number => {
@@ -204,17 +214,17 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
204
214
  return calculateLineLastDatePosition_DateTime()
205
215
  }
206
216
  let to = xScale(parseDate(region.to).getTime())
207
- return to + Number(config.yAxis.size)
217
+ return to + yAxisWidth
208
218
  }
209
219
 
210
220
  const calculateLineLastDatePosition_Categorical = (): number => {
211
- const chartStart = Number(config.yAxis.size || 0)
221
+ const chartStart = yAxisWidth
212
222
  // Extend to the right edge of the chart
213
223
  return chartStart + (xMax || 0)
214
224
  }
215
225
 
216
226
  const calculateLineLastDatePosition_Date = (): number => {
217
- const chartStart = Number(config.yAxis.size || 0)
227
+ const chartStart = yAxisWidth
218
228
  // For date scale line charts with Last Date, extend to the right edge of the chart
219
229
  return chartStart + (xMax || 0)
220
230
  }
@@ -223,8 +233,8 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
223
233
  const domain = xScale.domain()
224
234
  const lastDate = domain[domain.length - 1]
225
235
  const lastDatePosition = xScale(lastDate)
226
- // Match the non-Last Date logic: just add yAxis.size
227
- return Number(lastDatePosition + Number(config.yAxis.size))
236
+ // Match the non-Last Date logic: just add yAxisWidth
237
+ return Number(lastDatePosition + yAxisWidth)
228
238
  }
229
239
 
230
240
  // ============================================
@@ -389,7 +399,7 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
389
399
 
390
400
  if (!regions || orientation !== 'vertical') return null
391
401
 
392
- const chartStart = Number(config.yAxis.size || 0)
402
+ const chartStart = yAxisWidth
393
403
  const chartEnd = xMax !== undefined ? chartStart + xMax : chartStart + 1000
394
404
 
395
405
  return regions.map((region: Region) => {
@@ -4,22 +4,21 @@ import { Group } from '@visx/group'
4
4
  import { formatNumber as formatColNumber } from '@cdc/core/helpers/cove/number'
5
5
  import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
6
6
  import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
7
+ import { buildSeriesTooltipListHtml } from '../../helpers/tooltipHelpers'
7
8
 
8
- const ScatterPlot = ({ xScale, yScale }) => {
9
+ const ScatterPlot = ({ xScale, yScale, yAxisWidth }) => {
9
10
  const {
10
11
  transformedData: data,
11
12
  config,
12
13
  tableData,
13
14
  formatNumber,
14
15
  seriesHighlight,
15
- colorPalettes,
16
16
  colorScale,
17
17
  interactionLabel
18
18
  } = useContext(ConfigContext)
19
19
 
20
20
  // TODO: copied from line chart should probably be a constant somewhere.
21
21
  const circleRadii = 4.5
22
- const hasMultipleSeries = Object.keys(config.runtime.seriesLabels).length > 1
23
22
 
24
23
  // Track current hover for analytics
25
24
  const [currentHover, setCurrentHover] = useState({ dataIndex: null, seriesKey: null })
@@ -37,23 +36,26 @@ const ScatterPlot = ({ xScale, yScale }) => {
37
36
  }
38
37
  ])
39
38
  const handleTooltip = (item, s, dataIndex) => `<div>
40
- ${
41
- config.legend.showLegendValuesTooltip && config.runtime.seriesLabels && hasMultipleSeries
42
- ? `${config.runtime.seriesLabels[s] || ''}<br/>`
43
- : ''
44
- }
45
- ${config.runtime?.xAxis?.label || config.xAxis.label}: ${formatNumber(item[config.xAxis.dataKey], 'bottom')} <br/>
46
- ${config.runtime?.yAxis?.label || config.yAxis.label}: ${formatNumber(item[s], 'left')}<br/>
47
- ${additionalColumns
48
- .map(
49
- ([label, name, options]) =>
50
- `${label} : ${formatColNumber(tableData[dataIndex][name], 'left', false, config, options)}<br/>`
51
- )
52
- .join('')}
39
+ ${buildSeriesTooltipListHtml({
40
+ config,
41
+ colorScale,
42
+ heading: `${config.runtime?.xAxis?.label || config.xAxis.label}: ${formatNumber(
43
+ item[config.xAxis.dataKey],
44
+ 'bottom'
45
+ )}`,
46
+ seriesKey: s,
47
+ seriesText: `${config.runtime.seriesLabels[s] || s}: ${formatNumber(item[s], 'left')}`,
48
+ extraRows: additionalColumns
49
+ .map(
50
+ ([label, name, options]) =>
51
+ `${label} : ${formatColNumber(tableData[dataIndex][name], 'left', false, config, options)}`
52
+ )
53
+ .filter(Boolean)
54
+ })}
53
55
  </div>`
54
56
 
55
57
  return (
56
- <Group className='scatter-plot' left={config.yAxis.size}>
58
+ <Group className='scatter-plot' left={yAxisWidth}>
57
59
  {data.map((item, dataIndex) => {
58
60
  // prettier-ignore
59
61
  return config.runtime.seriesKeys.map((s, index) => {
@@ -114,6 +114,7 @@ const SmallMultipleTile: React.FC<SmallMultipleTileProps> = ({
114
114
  hideYAxisLabel: !isFirstInRow,
115
115
  legend: {
116
116
  ...tileConfig.legend,
117
+ tooltipLegendVisible: !config.legend?.hide,
117
118
  hide: true // Hide legends for all small multiple tiles
118
119
  },
119
120
  xAxis: {
@@ -34,7 +34,7 @@ const SparkLine: React.FC<SparkLineProps> = props => {
34
34
  const margin = { top: 5, right: 20, bottom: 10, left: 10 }
35
35
  const height = Number(parentHeight)
36
36
 
37
- const xMax = width - config.runtime.yAxis.size
37
+ const xMax = width - 50
38
38
  const yMax = height - margin.top - 20
39
39
 
40
40
  const getXAxisData = d =>
@@ -15,6 +15,7 @@ type WarmingStripesProps = {
15
15
  yScale: any
16
16
  xMax: number
17
17
  yMax: number
18
+ yAxisWidth: number
18
19
  synchronizedXValue?: any
19
20
  showTooltip: (args: any) => void
20
21
  handleTooltipMouseOff: () => void
@@ -23,6 +24,7 @@ type WarmingStripesProps = {
23
24
  const WarmingStripes = ({
24
25
  xMax,
25
26
  yMax,
27
+ yAxisWidth,
26
28
  synchronizedXValue,
27
29
  showTooltip,
28
30
  handleTooltipMouseOff
@@ -119,7 +121,7 @@ const WarmingStripes = ({
119
121
  [valueKey, formattedValue, 'left']
120
122
  ]
121
123
 
122
- const dataXPosition = index * stripeWidth + stripeWidth / 2 + Number(config.yAxis.size)
124
+ const dataXPosition = index * stripeWidth + stripeWidth / 2 + yAxisWidth
123
125
  const dataYPosition = mouseY ?? yMax / 2
124
126
 
125
127
  showTooltip({
@@ -132,7 +134,7 @@ const WarmingStripes = ({
132
134
  }
133
135
  })
134
136
  },
135
- [valueKey, xAxisDataKey, stripeWidth, config.yAxis.size, yMax, formatNumber, showTooltip]
137
+ [valueKey, xAxisDataKey, stripeWidth, yAxisWidth, yMax, formatNumber, showTooltip]
136
138
  )
137
139
 
138
140
  // Handle incoming synchronized tooltip from sibling small multiple tiles
@@ -156,7 +158,7 @@ const WarmingStripes = ({
156
158
  }
157
159
 
158
160
  return (
159
- <Group className='warming-stripes' left={config.yAxis.size}>
161
+ <Group className='warming-stripes' left={yAxisWidth}>
160
162
  {displayData.map((item, index) => {
161
163
  const value = Number(item[valueKey])
162
164
  if (isNaN(value)) return null
@@ -156,6 +156,7 @@ const createInitialState = () => {
156
156
  height: '',
157
157
  caption: '',
158
158
  showDownloadUrl: false,
159
+ downloadUrlLabel: '',
159
160
  showDataTableLink: true,
160
161
  showDownloadLinkBelow: true,
161
162
  indexLabel: '',