@cdc/chart 4.26.3 → 4.26.5

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 (192) hide show
  1. package/CONFIG.md +489 -0
  2. package/README.md +42 -17
  3. package/dist/cdcchart.js +39024 -36587
  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/heatmap/average-age-categorical.json +313 -0
  7. package/examples/feature/heatmap/calendar-heatmap.json +134 -0
  8. package/examples/feature/heatmap/cell-values-bottom-axis.json +181 -0
  9. package/examples/feature/heatmap/sparse-aggregation.json +158 -0
  10. package/examples/feature/paired-bar/paired-bar-example.json +2 -2
  11. package/examples/feature/tests-case-rate/case-rate-example-config.json +2 -2
  12. package/examples/feature/tests-covid/covid-confidence-example-config.json +2 -2
  13. package/examples/feature/tests-covid/covid-example-config.json +2 -2
  14. package/examples/feature/tests-cutoff/cutoff-example-config.json +2 -2
  15. package/examples/feature/tests-non-numerics/planet-pie-example-config-nonnumeric.json +2 -2
  16. package/examples/metadata-variables.json +1 -1
  17. package/examples/minimal-example.json +24 -0
  18. package/examples/private/bump-chart.json +1116 -0
  19. package/examples/private/test-123.json +624 -0
  20. package/examples/private/test2.json +35012 -0
  21. package/package.json +5 -4
  22. package/src/CdcChartComponent.tsx +144 -60
  23. package/src/_stories/Chart.AltText.stories.tsx +136 -0
  24. package/src/_stories/{Chart.HTMLInDataTable.stories.tsx → Chart.HTMLInDataTable.smoke.stories.tsx} +10 -0
  25. package/src/_stories/Chart.HeatMap.stories.tsx +187 -0
  26. package/src/_stories/Chart.Minimal.smoke.stories.tsx +25 -0
  27. package/src/_stories/Chart.Sankey.smoke.stories.tsx +62 -0
  28. package/src/_stories/Chart.SmallMultiples.smoke.stories.tsx +108 -0
  29. package/src/_stories/{Chart.stories.tsx → Chart.smoke.stories.tsx} +12 -0
  30. package/src/_stories/{Chart.tooltip.stories.tsx → Chart.tooltip.smoke.stories.tsx} +68 -0
  31. package/src/_stories/{ChartAxisTitles.stories.tsx → ChartAxisTitles.smoke.stories.tsx} +13 -0
  32. package/src/_stories/ChartBar.Editor.stories.tsx +151 -85
  33. package/src/_stories/ChartBrush.Interaction.stories.tsx +112 -0
  34. package/src/_stories/{ChartBrush.stories.tsx → ChartBrush.smoke.stories.tsx} +13 -4
  35. package/src/_stories/ChartEditor.Editor.stories.tsx +23 -0
  36. package/src/_stories/ChartLine.DashedDates.smoke.stories.tsx +23 -0
  37. package/src/_stories/_mock/alt_text_metadata.json +38 -0
  38. package/src/_stories/_mock/heatmap-average-age-categorical.json +315 -0
  39. package/src/_stories/_mock/heatmap.json +156 -0
  40. package/src/_stories/_mock/line_chart_dashed_dates.json +276 -0
  41. package/src/_stories/_mock/paired-bar-abbr.json +1 -1
  42. package/src/_stories/_mock/paired-bar.json +1 -1
  43. package/src/_stories/_mock/reported_pertussis_cases.json +1643 -0
  44. package/src/_stories/_mock/sankey_mock.json +78 -0
  45. package/src/_stories/_mock/small_multiples/small_multiples_lines.json +1 -1
  46. package/src/components/Annotations/components/AnnotationDraggable.tsx +2 -2
  47. package/src/components/Annotations/components/AnnotationDropdown.tsx +5 -2
  48. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +10 -2
  49. package/src/components/Axis/BottomAxis.tsx +1 -1
  50. package/src/components/Axis/LeftAxis.tsx +15 -14
  51. package/src/components/Axis/LeftAxisGridlines.tsx +14 -13
  52. package/src/components/BarChart/components/BarChart.Horizontal.tsx +9 -5
  53. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +9 -5
  54. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +9 -5
  55. package/src/components/BarChart/components/BarChart.Vertical.tsx +9 -6
  56. package/src/components/BarChart/components/BarChart.tsx +2 -6
  57. package/src/components/BarChart/helpers/useBarChart.ts +1 -1
  58. package/src/components/BoxPlot/BoxPlot.Vertical.tsx +3 -3
  59. package/src/components/Brush/BrushSelector.tsx +88 -187
  60. package/src/components/Brush/brushInteraction.ts +22 -0
  61. package/src/components/Brush/tests/brushInteraction.test.ts +38 -0
  62. package/src/components/EditorPanel/EditorPanel.tsx +946 -960
  63. package/src/components/EditorPanel/EditorPanelContext.ts +1 -1
  64. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +229 -165
  65. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +105 -13
  66. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +169 -116
  67. package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +159 -109
  68. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +3 -3
  69. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +14 -28
  70. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +1 -1
  71. package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +57 -0
  72. package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +56 -0
  73. package/src/components/EditorPanel/useEditorPermissions.test.tsx +70 -0
  74. package/src/components/EditorPanel/useEditorPermissions.ts +32 -21
  75. package/src/components/Forecasting/Forecasting.tsx +2 -2
  76. package/src/components/HeatMap/components/HeatMap.tsx +762 -0
  77. package/src/components/HeatMap/components/HeatMapGradientLegend.tsx +114 -0
  78. package/src/components/HeatMap/heatmap.constants.ts +13 -0
  79. package/src/components/HeatMap/heatmap.css +71 -0
  80. package/src/components/HeatMap/helpers/index.ts +261 -0
  81. package/src/components/HeatMap/index.tsx +5 -0
  82. package/src/components/HeatMap/tests/HeatMap.test.tsx +1040 -0
  83. package/src/components/HeatMap/tests/helpers.test.ts +167 -0
  84. package/src/components/HorizonChart/HorizonChart.tsx +10 -2
  85. package/src/components/HoverLine/HoverLine.tsx +6 -8
  86. package/src/components/Legend/Legend.Component.tsx +4 -4
  87. package/src/components/Legend/LegendGroup/LegendGroup.styles.css +5 -0
  88. package/src/components/Legend/LegendGroup/LegendGroup.tsx +3 -1
  89. package/src/components/Legend/helpers/index.ts +0 -2
  90. package/src/components/LineChart/LineChartProps.ts +1 -0
  91. package/src/components/LineChart/components/LineChart.BumpCircle.tsx +22 -24
  92. package/src/components/LineChart/helpers.test.ts +345 -0
  93. package/src/components/LineChart/helpers.ts +15 -5
  94. package/src/components/LineChart/index.tsx +18 -5
  95. package/src/components/LinearChart/VisualizationRenderer.tsx +11 -1
  96. package/src/components/LinearChart/tests/LinearChart.test.tsx +228 -7
  97. package/src/components/LinearChart/tests/mockConfigContext.ts +1 -0
  98. package/src/components/LinearChart.tsx +155 -77
  99. package/src/components/PairedBarChart.jsx +2 -2
  100. package/src/components/PieChart/PieChart.tsx +15 -10
  101. package/src/components/RadarChart/RadarChart.tsx +3 -1
  102. package/src/components/Regions/components/Regions.tsx +199 -35
  103. package/src/components/Sankey/components/Sankey.tsx +7 -3
  104. package/src/components/Sankey/helpers/getSankeyTooltip.tsx +7 -6
  105. package/src/components/ScatterPlot/ScatterPlot.jsx +19 -17
  106. package/src/components/SmallMultiples/SmallMultipleTile.tsx +31 -3
  107. package/src/components/SmallMultiples/SmallMultiples.css +12 -0
  108. package/src/components/SmallMultiples/SmallMultiples.tsx +15 -9
  109. package/src/components/SmallMultiples/tests/SmallMultipleTile.test.tsx +152 -0
  110. package/src/components/Sparkline/components/SparkLine.tsx +5 -10
  111. package/src/components/WarmingStripes/WarmingStripes.tsx +5 -3
  112. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +1 -1
  113. package/src/components/WarmingStripes/WarmingStripesGradientLegend.tsx +2 -2
  114. package/src/data/initial-state.js +12 -0
  115. package/src/helpers/axisLabelFontSize.ts +8 -0
  116. package/src/helpers/bumpChartTooltip.ts +54 -0
  117. package/src/helpers/calculateLeftYAxisWidth.ts +197 -0
  118. package/src/helpers/ensureSpecialChartAxisTypes.ts +11 -0
  119. package/src/helpers/getMinMax.ts +2 -3
  120. package/src/helpers/getYAxisAutoPadding.ts +8 -6
  121. package/src/helpers/getYAxisAutoPaddingMode.ts +17 -0
  122. package/src/helpers/handleChartAriaLabels.ts +23 -10
  123. package/src/helpers/seriesHighlight.ts +26 -0
  124. package/src/helpers/tests/axisLabelFontSize.test.ts +15 -0
  125. package/src/helpers/tests/bumpChartTooltip.test.ts +75 -0
  126. package/src/helpers/tests/ensureSpecialChartAxisTypes.test.ts +30 -0
  127. package/src/helpers/tests/getYAxisAutoPadding.test.ts +37 -0
  128. package/src/helpers/tests/getYAxisAutoPaddingMode.test.ts +90 -0
  129. package/src/helpers/tests/handleChartAriaLabels.test.ts +71 -0
  130. package/src/helpers/tests/seriesHighlight.test.ts +61 -0
  131. package/src/helpers/tests/tooltipHelpers.test.ts +151 -0
  132. package/src/helpers/tooltipHelpers.ts +176 -0
  133. package/src/hooks/tests/useScales.test.tsx +105 -0
  134. package/src/hooks/useHighlightedBars.ts +9 -16
  135. package/src/hooks/useProgrammaticTooltip.ts +16 -2
  136. package/src/hooks/useScales.ts +19 -9
  137. package/src/hooks/useSmallMultipleSynchronization.ts +3 -4
  138. package/src/hooks/useTooltip.tsx +167 -38
  139. package/src/index.jsx +5 -2
  140. package/src/scss/main.scss +32 -49
  141. package/src/test/CdcChart.test.jsx +24 -0
  142. package/src/types/ChartConfig.ts +19 -0
  143. package/src/types/ChartContext.ts +2 -0
  144. package/examples/chart-regression-2.json +0 -2360
  145. package/examples/feature/__data__/age-adjusted-rates.json +0 -1486
  146. package/examples/feature/__data__/city-temperature.json +0 -2198
  147. package/examples/feature/bar/Barchart_with_negative.json +0 -34
  148. package/examples/feature/boxplot/box-plot-data.json +0 -71
  149. package/examples/feature/boxplot/boxplot-data.json +0 -156
  150. package/examples/feature/combo/combochart-categories_are_numbers .json +0 -18
  151. package/examples/feature/forest-plot/log.json +0 -26
  152. package/examples/feature/tests-date-exclusions/date-exclusions-data.json +0 -162
  153. package/examples/region-issue.json +0 -2065
  154. package/examples/xaxis.json +0 -493
  155. package/src/_stories/Chart.SmallMultiples.stories.tsx +0 -63
  156. /package/examples/{data → __data__}/data-with-metadata.json +0 -0
  157. /package/examples/feature/bar/{tall-data.json → __data__/tall-data.json} +0 -0
  158. /package/examples/feature/deviation/{planet-deviation-data.json → __data__/planet-deviation-data.json} +0 -0
  159. /package/examples/feature/paired-bar/{paired-bar-data.json → __data__/paired-bar-data.json} +0 -0
  160. /package/examples/feature/paired-bar/{paired-bar-formatted.json → __data__/paired-bar-formatted.json} +0 -0
  161. /package/examples/feature/tests-case-rate/{case-rate-example-data.json → __data__/case-rate-example-data.json} +0 -0
  162. /package/examples/feature/tests-covid/{covid-example-data-confidence.json → __data__/covid-example-data-confidence.json} +0 -0
  163. /package/examples/feature/tests-covid/{covid-example-data.json → __data__/covid-example-data.json} +0 -0
  164. /package/examples/feature/tests-cutoff/{cutoff-example-data.json → __data__/cutoff-example-data.json} +0 -0
  165. /package/examples/feature/tests-non-numerics/{planet-example-data-nonnumeric.json → __data__/planet-example-data-nonnumeric.json} +0 -0
  166. /package/src/_stories/{Chart.Anchors.stories.tsx → Chart.Anchors.smoke.stories.tsx} +0 -0
  167. /package/src/_stories/{Chart.BoxPlot.stories.tsx → Chart.BoxPlot.smoke.stories.tsx} +0 -0
  168. /package/src/_stories/{Chart.CI.stories.tsx → Chart.CI.smoke.stories.tsx} +0 -0
  169. /package/src/_stories/{Chart.Combo.stories.tsx → Chart.Combo.smoke.stories.tsx} +0 -0
  170. /package/src/_stories/{Chart.CustomColors.stories.tsx → Chart.CustomColors.smoke.stories.tsx} +0 -0
  171. /package/src/_stories/{Chart.Defaults.stories.tsx → Chart.Defaults.smoke.stories.tsx} +0 -0
  172. /package/src/_stories/{Chart.DynamicSeries.stories.tsx → Chart.DynamicSeries.smoke.stories.tsx} +0 -0
  173. /package/src/_stories/{Chart.Filters.stories.tsx → Chart.Filters.smoke.stories.tsx} +0 -0
  174. /package/src/_stories/{Chart.Forecast.stories.tsx → Chart.Forecast.smoke.stories.tsx} +0 -0
  175. /package/src/_stories/{Chart.Legend.Gradient.stories.tsx → Chart.Legend.Gradient.smoke.stories.tsx} +0 -0
  176. /package/src/_stories/{Chart.Patterns.stories.tsx → Chart.Patterns.smoke.stories.tsx} +0 -0
  177. /package/src/_stories/{Chart.PreserveDecimals.stories.tsx → Chart.PreserveDecimals.smoke.stories.tsx} +0 -0
  178. /package/src/_stories/{Chart.Regions.Categorical.stories.tsx → Chart.Regions.Categorical.smoke.stories.tsx} +0 -0
  179. /package/src/_stories/{Chart.Regions.DateScale.stories.tsx → Chart.Regions.DateScale.smoke.stories.tsx} +0 -0
  180. /package/src/_stories/{Chart.Regions.DateTimeScale.stories.tsx → Chart.Regions.DateTimeScale.smoke.stories.tsx} +0 -0
  181. /package/src/_stories/{Chart.ScatterPlot.stories.tsx → Chart.ScatterPlot.smoke.stories.tsx} +0 -0
  182. /package/src/_stories/{Chart.SmallestLeftAxisMax.stories.tsx → Chart.SmallestLeftAxisMax.smoke.stories.tsx} +0 -0
  183. /package/src/_stories/{ChartAnnotation.stories.tsx → ChartAnnotation.smoke.stories.tsx} +0 -0
  184. /package/src/_stories/{ChartAxisLabels.stories.tsx → ChartAxisLabels.smoke.stories.tsx} +0 -0
  185. /package/src/_stories/{ChartBrush.Matrix.Continuous.stories.tsx → ChartBrush.Matrix.Continuous.smoke.stories.tsx} +0 -0
  186. /package/src/_stories/{ChartBrush.Matrix.Date.stories.tsx → ChartBrush.Matrix.Date.smoke.stories.tsx} +0 -0
  187. /package/src/_stories/{ChartBrush.Matrix.DateTime.stories.tsx → ChartBrush.Matrix.DateTime.smoke.stories.tsx} +0 -0
  188. /package/src/_stories/{ChartLine.QuadrantAngles.stories.tsx → ChartLine.QuadrantAngles.smoke.stories.tsx} +0 -0
  189. /package/src/_stories/{ChartLine.Suppression.stories.tsx → ChartLine.Suppression.smoke.stories.tsx} +0 -0
  190. /package/src/_stories/{ChartLine.Symbols.stories.tsx → ChartLine.Symbols.smoke.stories.tsx} +0 -0
  191. /package/src/_stories/{ChartPrefixSuffix.stories.tsx → ChartPrefixSuffix.smoke.stories.tsx} +0 -0
  192. /package/src/_stories/{TechAdoptionWithLinks.stories.tsx → TechAdoptionWithLinks.smoke.stories.tsx} +0 -0
@@ -1,18 +1,31 @@
1
1
  import React, { useContext } from 'react'
2
2
  import ConfigContext from '../../../ConfigContext'
3
3
  import { ChartContext } from '../../../types/ChartContext'
4
- import { Text } from '@visx/text'
4
+ import { Text, useText } from '@visx/text'
5
5
  import { Group } from '@visx/group'
6
- import { formatDate, isDateScale } from '@cdc/core/helpers/cove/date.js'
6
+ import { formatDate } from '@cdc/core/helpers/cove/date.js'
7
+ import { APP_FONT_COLOR } from '@cdc/core/helpers/constants'
8
+ import { isMobileFontViewport } from '@cdc/core/helpers/viewports'
7
9
 
8
10
  // Constants for visualization types
9
11
  const VIZ_TYPES = {
10
12
  BAR: 'Bar',
11
13
  LINE: 'Line',
12
14
  AREA: 'Area Chart',
13
- COMBO: 'Combo'
15
+ COMBO: 'Combo',
16
+ HORIZON: 'Horizon Chart'
14
17
  } as const
15
18
 
19
+ const DEFAULT_REGION_BACKGROUND = 'var(--cool-gray-50, #71767a)'
20
+ const TICK_LABEL_FONT_SIZE = 16
21
+ const TICK_LABEL_FONT_SIZE_SMALL = 13
22
+ const REGION_LABEL_HORIZONTAL_PADDING = 8
23
+ const REGION_LABEL_TOP_PADDING = 6
24
+ const REGION_LABEL_MAX_PLOT_WIDTH_RATIO = 1 / 3
25
+ const REGION_LABEL_COMFORTABLE_MIN_WIDTH = 120
26
+ const REGION_LABEL_COMFORTABLE_MIN_WIDTH_SMALL = 96
27
+ const REGION_LABEL_FONT_FAMILY = 'Nunito, sans-serif'
28
+
16
29
  type Region = {
17
30
  from: string
18
31
  to: string
@@ -44,10 +57,111 @@ type HighlightedAreaProps = {
44
57
  background: string
45
58
  }
46
59
 
60
+ type RegionLabelLayout = {
61
+ x: number
62
+ width: number
63
+ }
64
+
65
+ type RegionLabelProps = {
66
+ label: string
67
+ color: string
68
+ clippedFrom: number
69
+ regionWidth: number
70
+ plotWidth: number
71
+ fontSize: number
72
+ isMobileViewport: boolean
73
+ }
74
+
47
75
  const HighlightedArea: React.FC<HighlightedAreaProps> = ({ x, width, yMax, background }) => (
48
- <rect x={x} y={0} width={width} height={yMax} fill={background} opacity={0.3} />
76
+ <rect x={x} y={0} width={width} height={yMax} fill={background || DEFAULT_REGION_BACKGROUND} opacity={0.3} />
49
77
  )
50
78
 
79
+ const clamp = (value: number, min: number, max: number): number => Math.min(Math.max(value, min), max)
80
+
81
+ const getRegionLabelY = (fontSize: number): number => REGION_LABEL_TOP_PADDING + fontSize * 0.25
82
+
83
+ const getComfortableRegionLabelWidth = (plotWidth: number, isMobileViewport: boolean): number => {
84
+ const maxPlotLabelWidth = Math.max(1, plotWidth * REGION_LABEL_MAX_PLOT_WIDTH_RATIO)
85
+ const comfortableMinWidth = isMobileViewport
86
+ ? REGION_LABEL_COMFORTABLE_MIN_WIDTH_SMALL
87
+ : REGION_LABEL_COMFORTABLE_MIN_WIDTH
88
+ return Math.min(comfortableMinWidth, maxPlotLabelWidth, plotWidth)
89
+ }
90
+
91
+ // Region labels prefer to sit centered inside their highlighted region when the measured text
92
+ // fits with padding. If the visible region is too narrow, keep a readable fixed wrapping width
93
+ // and pin the measured text near the region's right edge, clamped so the text stays inside the plot.
94
+ const getRegionLabelLayout = (
95
+ clippedFrom: number,
96
+ regionWidth: number,
97
+ plotWidth: number,
98
+ comfortableLabelWidth: number,
99
+ estimatedWrappedWidth: number
100
+ ): RegionLabelLayout => {
101
+ const maxPlotLabelWidth = Math.max(1, plotWidth * REGION_LABEL_MAX_PLOT_WIDTH_RATIO)
102
+ const regionInnerWidth = Math.max(0, regionWidth - REGION_LABEL_HORIZONTAL_PADDING * 2)
103
+ const insideThreshold = Math.min(comfortableLabelWidth, estimatedWrappedWidth || comfortableLabelWidth)
104
+
105
+ const isInsideRegion = regionInnerWidth >= insideThreshold
106
+ const labelWidth = isInsideRegion ? Math.min(regionInnerWidth, maxPlotLabelWidth) : comfortableLabelWidth
107
+
108
+ const regionCenter = clippedFrom + regionWidth / 2
109
+ const halfLabelWidth = labelWidth / 2
110
+ const measuredTextWidth = Math.min(estimatedWrappedWidth || labelWidth, labelWidth)
111
+ const halfMeasuredTextWidth = measuredTextWidth / 2
112
+ const overflowLabelX = clamp(
113
+ clippedFrom + regionWidth - REGION_LABEL_HORIZONTAL_PADDING - halfMeasuredTextWidth,
114
+ halfMeasuredTextWidth,
115
+ plotWidth - halfMeasuredTextWidth
116
+ )
117
+ const insideLabelX =
118
+ labelWidth >= plotWidth ? plotWidth / 2 : clamp(regionCenter, halfLabelWidth, plotWidth - halfLabelWidth)
119
+
120
+ return { x: isInsideRegion ? insideLabelX : overflowLabelX, width: labelWidth }
121
+ }
122
+
123
+ const RegionLabel: React.FC<RegionLabelProps> = ({
124
+ label,
125
+ color,
126
+ clippedFrom,
127
+ regionWidth,
128
+ plotWidth,
129
+ fontSize,
130
+ isMobileViewport
131
+ }) => {
132
+ const style = { fontFamily: REGION_LABEL_FONT_FAMILY, fontSize }
133
+ const comfortableLabelWidth = getComfortableRegionLabelWidth(plotWidth, isMobileViewport)
134
+ const { wordsByLines } = useText({
135
+ children: label,
136
+ width: comfortableLabelWidth,
137
+ style
138
+ })
139
+ const estimatedWrappedWidth = Math.max(...wordsByLines.map(line => line.width || 0), 0)
140
+ const labelLayout = getRegionLabelLayout(
141
+ clippedFrom,
142
+ regionWidth,
143
+ plotWidth,
144
+ comfortableLabelWidth,
145
+ estimatedWrappedWidth
146
+ )
147
+
148
+ return (
149
+ <Text
150
+ x={labelLayout.x}
151
+ y={getRegionLabelY(fontSize)}
152
+ width={labelLayout.width}
153
+ fill={color || APP_FONT_COLOR}
154
+ fontSize={fontSize}
155
+ style={style}
156
+ lineHeight='1.1em'
157
+ verticalAnchor='start'
158
+ textAnchor='middle'
159
+ >
160
+ {label}
161
+ </Text>
162
+ )
163
+ }
164
+
51
165
  /** Find the closest date in domain to a target date */
52
166
  const findClosestDate = <T,>(targetTime: number, domain: T[], getTime: (d: T) => number): T => {
53
167
  let closest = domain[0]
@@ -63,17 +177,20 @@ const findClosestDate = <T,>(targetTime: number, domain: T[], getTime: (d: T) =>
63
177
  return closest
64
178
  }
65
179
 
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
180
+ /** Check if visualization type is line-like (Line, Area Chart, or Horizon Chart) */
181
+ const isLineLike = (type: string): boolean =>
182
+ type === VIZ_TYPES.LINE || type === VIZ_TYPES.AREA || type === VIZ_TYPES.HORIZON
68
183
 
69
184
  /** Check if visualization type is bar-like (Bar or Combo) */
70
185
  const isBarLike = (type: string): boolean => type === VIZ_TYPES.BAR || type === VIZ_TYPES.COMBO
71
186
 
72
187
  // TODO: should regions be removed on categorical axis?
73
188
  const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGroup = 1, yMax, xMax }) => {
74
- const { parseDate, config } = useContext<ChartContext>(ConfigContext)
189
+ const { parseDate, config, tableData, vizViewport } = useContext<ChartContext>(ConfigContext)
75
190
 
76
191
  const { regions, visualizationType, orientation, xAxis } = config
192
+ const isMobileViewport = isMobileFontViewport(vizViewport || 'lg')
193
+ const regionLabelFontSize = isMobileViewport ? TICK_LABEL_FONT_SIZE_SMALL : TICK_LABEL_FONT_SIZE
77
194
 
78
195
  const getBarOffset = (): number => (barWidth * totalBarsInGroup) / 2
79
196
 
@@ -81,15 +198,32 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
81
198
  // HELPER FUNCTIONS FOR PREVIOUS DAYS
82
199
  // ============================================
83
200
 
201
+ const getDataLastTime = (): number => {
202
+ const dataKey = config.xAxis.dataKey
203
+ if (!Array.isArray(tableData) || tableData.length === 0 || !dataKey) return NaN
204
+ let last = NaN
205
+ for (const row of tableData) {
206
+ const value = (row as Record<string, unknown>)[dataKey]
207
+ const t = parseDate(String(value))?.getTime()
208
+ if (typeof t === 'number' && !isNaN(t) && (isNaN(last) || t > last)) {
209
+ last = t
210
+ }
211
+ }
212
+ return last
213
+ }
214
+
84
215
  const calculatePreviousDaysFrom = (region: Region, axisType: string): number => {
85
216
  const previousDays = Number(region.from) || 0
86
217
  const domain = xScale.domain()
87
218
 
88
219
  // Determine the "to" reference date
89
- const toRefDate =
90
- region.toType === 'Last Date'
91
- ? new Date(domain[domain.length - 1] as string | number).getTime()
92
- : new Date(region.to)
220
+ let toRefDate: number | Date
221
+ if (region.toType === 'Last Date') {
222
+ const dataLastTime = getDataLastTime()
223
+ toRefDate = !isNaN(dataLastTime) ? dataLastTime : new Date(domain[domain.length - 1] as string | number).getTime()
224
+ } else {
225
+ toRefDate = new Date(region.to)
226
+ }
93
227
 
94
228
  const toFormatted = formatDate(config.xAxis.dateParseFormat, toRefDate, config.locale)
95
229
  const toDate = new Date(toFormatted)
@@ -122,8 +256,8 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
122
256
  } else {
123
257
  from = xScale(region.from)
124
258
  }
125
- // Add left padding (yAxis.size) + half bandwidth to center on the category
126
- let scalePadding = Number(config.yAxis.size)
259
+ // Add half bandwidth to center on the category
260
+ let scalePadding = 0
127
261
  if (xScale.bandwidth) {
128
262
  scalePadding += xScale.bandwidth() / 2
129
263
  }
@@ -135,8 +269,8 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
135
269
  return calculateLineLastDatePosition_Categorical()
136
270
  }
137
271
  let to = xScale(region.to)
138
- // Add left padding (yAxis.size) + half bandwidth
139
- let scalePadding = Number(config.yAxis.size)
272
+ // Add half bandwidth to center on the category
273
+ let scalePadding = 0
140
274
  if (xScale.bandwidth) {
141
275
  scalePadding += xScale.bandwidth() / 2
142
276
  }
@@ -158,8 +292,8 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
158
292
  const closestDate = findClosestDate(parsedDate, domain, d => d)
159
293
  from = xScale(closestDate)
160
294
  }
161
- // Add left padding (yAxis.size) + half bandwidth
162
- let scalePadding = Number(config.yAxis.size)
295
+ // Add half bandwidth to center on the date band
296
+ let scalePadding = 0
163
297
  if (xScale.bandwidth) {
164
298
  scalePadding += xScale.bandwidth() / 2
165
299
  }
@@ -179,8 +313,8 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
179
313
  const closestDate = findClosestDate(parsedDate, domain, d => d)
180
314
  let to = xScale(closestDate)
181
315
 
182
- // Add left padding (yAxis.size) + half bandwidth
183
- let scalePadding = Number(config.yAxis.size)
316
+ // Add half bandwidth to center on the date band
317
+ let scalePadding = 0
184
318
  if (xScale.bandwidth) {
185
319
  scalePadding += xScale.bandwidth() / 2
186
320
  }
@@ -190,13 +324,13 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
190
324
  const getLineFromValue_DateTime = (region: Region): number => {
191
325
  if (region.fromType === 'Previous Days') {
192
326
  const from = calculatePreviousDaysFrom(region, 'date-time')
193
- return from + Number(config.yAxis.size)
327
+ return from
194
328
  }
195
329
  const date = new Date(region.from)
196
330
  const parsedDate = parseDate(formatDate(config.xAxis.dateParseFormat, date, config.locale)).getTime()
197
331
  let from = xScale(parsedDate)
198
- // For date-time, xScale returns correct position (no bandwidth), just add left padding
199
- return from + Number(config.yAxis.size)
332
+ // For date-time, xScale returns the plot-local position (no bandwidth)
333
+ return from
200
334
  }
201
335
 
202
336
  const getLineToValue_DateTime = (region: Region): number => {
@@ -204,27 +338,24 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
204
338
  return calculateLineLastDatePosition_DateTime()
205
339
  }
206
340
  let to = xScale(parseDate(region.to).getTime())
207
- return to + Number(config.yAxis.size)
341
+ return to
208
342
  }
209
343
 
210
344
  const calculateLineLastDatePosition_Categorical = (): number => {
211
- const chartStart = Number(config.yAxis.size || 0)
212
345
  // Extend to the right edge of the chart
213
- return chartStart + (xMax || 0)
346
+ return xMax || 0
214
347
  }
215
348
 
216
349
  const calculateLineLastDatePosition_Date = (): number => {
217
- const chartStart = Number(config.yAxis.size || 0)
218
350
  // For date scale line charts with Last Date, extend to the right edge of the chart
219
- return chartStart + (xMax || 0)
351
+ return xMax || 0
220
352
  }
221
353
 
222
354
  const calculateLineLastDatePosition_DateTime = (): number => {
223
355
  const domain = xScale.domain()
224
356
  const lastDate = domain[domain.length - 1]
225
357
  const lastDatePosition = xScale(lastDate)
226
- // Match the non-Last Date logic: just add yAxis.size
227
- return Number(lastDatePosition + Number(config.yAxis.size))
358
+ return Number(lastDatePosition)
228
359
  }
229
360
 
230
361
  // ============================================
@@ -323,6 +454,31 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
323
454
  return Number(lastDatePosition)
324
455
  }
325
456
 
457
+ const isRegionInVisibleDomain = (region: Region): boolean => {
458
+ if (region.toType !== 'Last Date') return true
459
+ if (xAxis.type !== 'date' && xAxis.type !== 'date-time') return true
460
+
461
+ const domain = xScale.domain()
462
+ if (!domain || domain.length === 0) return true
463
+ const visEnd = Number(domain[domain.length - 1])
464
+ if (isNaN(visEnd)) return true
465
+
466
+ let fromTime: number | undefined
467
+ if (region.fromType === 'Previous Days') {
468
+ const dataLastTime = getDataLastTime()
469
+ if (isNaN(dataLastTime)) return true
470
+ const previousDays = Number(region.from) || 0
471
+ const fromDate = new Date(dataLastTime)
472
+ fromDate.setDate(fromDate.getDate() - previousDays)
473
+ fromTime = fromDate.getTime()
474
+ } else {
475
+ fromTime = parseDate(formatDate(config.xAxis.dateParseFormat, new Date(region.from), config.locale))?.getTime()
476
+ }
477
+
478
+ if (fromTime === undefined || isNaN(fromTime)) return true
479
+ return fromTime < visEnd
480
+ }
481
+
326
482
  // ============================================
327
483
  // MAIN ROUTING FUNCTIONS
328
484
  // ============================================
@@ -389,10 +545,12 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
389
545
 
390
546
  if (!regions || orientation !== 'vertical') return null
391
547
 
392
- const chartStart = Number(config.yAxis.size || 0)
393
- const chartEnd = xMax !== undefined ? chartStart + xMax : chartStart + 1000
548
+ const chartStart = 0
549
+ const chartEnd = xMax !== undefined ? xMax : 1000
394
550
 
395
551
  return regions.map((region: Region) => {
552
+ if (!isRegionInVisibleDomain(region)) return null
553
+
396
554
  const from = getFromValue(region)
397
555
  const to = getToValue(region)
398
556
 
@@ -409,11 +567,17 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
409
567
  if (width <= 0) return null
410
568
 
411
569
  return (
412
- <Group height={100} fill='red' className='regions regions-group--line' key={region.label} pointerEvents='none'>
570
+ <Group height={100} className='regions regions-group--line' key={region.label} pointerEvents='none'>
413
571
  <HighlightedArea x={clippedFrom} width={width} yMax={yMax} background={region.background} />
414
- <Text x={clippedFrom + width / 2} y={5} fill={region.color} verticalAnchor='start' textAnchor='middle'>
415
- {region.label}
416
- </Text>
572
+ <RegionLabel
573
+ label={region.label}
574
+ color={region.color}
575
+ clippedFrom={clippedFrom}
576
+ regionWidth={width}
577
+ plotWidth={chartEnd}
578
+ fontSize={regionLabelFontSize}
579
+ isMobileViewport={isMobileViewport}
580
+ />
417
581
  </Group>
418
582
  )
419
583
  })
@@ -16,8 +16,10 @@ import useSankeyAlert from '../useSankeyAlert'
16
16
  import { getSankeyTooltip } from '../helpers/getSankeyTooltip'
17
17
 
18
18
  const Sankey = ({ width, height, runtime }: SankeyProps) => {
19
- const { config } = useContext<ChartContext>(ConfigContext)
19
+ const { config, handleChartAriaLabels } = useContext<ChartContext>(ConfigContext)
20
20
  const { sankey: sankeyConfig } = config
21
+
22
+ const a11y = handleChartAriaLabels(config)
21
23
  const [largestGroupWidth, setLargestGroupWidth] = useState(0)
22
24
  const [tooltipID, setTooltipID] = useState<string>('')
23
25
  const { showAlert, alert } = useSankeyAlert()
@@ -43,7 +45,7 @@ const Sankey = ({ width, height, runtime }: SankeyProps) => {
43
45
  useEffect(() => {
44
46
  let largest = 0
45
47
  groupRefs?.current?.map(g => {
46
- const groupWidth = g?.getBoundingClientRect().width
48
+ const groupWidth = g?.getBBox?.().width || g?.getBoundingClientRect?.().width || 0
47
49
  if (groupWidth > largest) {
48
50
  largest = groupWidth
49
51
  }
@@ -455,10 +457,12 @@ const Sankey = ({ width, height, runtime }: SankeyProps) => {
455
457
  width={width}
456
458
  height={Number(config.heights.vertical)}
457
459
  style={{ overflow: 'visible' }}
460
+ role='img'
461
+ aria-label={a11y}
458
462
  >
459
463
  <Group className='links'>{allLinks}</Group>
460
464
  <Group className='nodes'>{allNodes}</Group>
461
- <Group className='finalNodes' style={{ display: 'none' }}>
465
+ <Group className='finalNodes' style={{ visibility: 'hidden', pointerEvents: 'none' }} aria-hidden='true'>
462
466
  {finalNodes}
463
467
  </Group>
464
468
  </svg>
@@ -2,12 +2,13 @@ import ReactDOMServer from 'react-dom/server'
2
2
  import ColumnList from '../components/ColumnList'
3
3
 
4
4
  export const getSankeyTooltip = (data: Object, tooltipID: string) => {
5
- const tooltipVal = `${(data?.tooltips?.find(item => item.node === tooltipID) || {}).value}`
6
- const tooltipSummary = `${(data?.tooltips?.find(item => item.node === tooltipID) || {}).summary}`
7
- const tooltipColumn1Label = (data?.tooltips?.find(item => item.node === tooltipID) || {}).column1Label
8
- const tooltipColumn2Label = (data?.tooltips?.find(item => item.node === tooltipID) || {}).column2Label
9
- const tooltipColumn1 = (data?.tooltips?.find(item => item.node === tooltipID) || {}).column1
10
- const tooltipColumn2 = (data?.tooltips?.find(item => item.node === tooltipID) || {}).column2
5
+ const tooltips = Array.isArray(data?.tooltips) ? data.tooltips : []
6
+ const tooltipVal = `${(tooltips.find(item => item.node === tooltipID) || {}).value}`
7
+ const tooltipSummary = `${(tooltips.find(item => item.node === tooltipID) || {}).summary}`
8
+ const tooltipColumn1Label = (tooltips.find(item => item.node === tooltipID) || {}).column1Label
9
+ const tooltipColumn2Label = (tooltips.find(item => item.node === tooltipID) || {}).column2Label
10
+ const tooltipColumn1 = (tooltips.find(item => item.node === tooltipID) || {}).column1
11
+ const tooltipColumn2 = (tooltips.find(item => item.node === tooltipID) || {}).column2
11
12
 
12
13
  const tooltipColumn1Data = ReactDOMServer.renderToString(<ColumnList columnData={tooltipColumn1} />)
13
14
  const tooltipColumn2Data = ReactDOMServer.renderToString(<ColumnList columnData={tooltipColumn2} />)
@@ -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) => {
@@ -1,4 +1,4 @@
1
- import React, { useContext, useRef, useEffect } from 'react'
1
+ import React, { useContext, useRef, useEffect, useState } from 'react'
2
2
  import LinearChart from '../LinearChart'
3
3
  import ParentSize from '@visx/responsive/lib/components/ParentSize'
4
4
  import ConfigContext from '../../ConfigContext'
@@ -7,6 +7,7 @@ import cloneConfig from '@cdc/core/helpers/cloneConfig'
7
7
  import { getTileDisplayTitle } from '../../helpers/smallMultiplesHelpers'
8
8
  import getViewport from '@cdc/core/helpers/getViewport'
9
9
  import { ChartConfig } from '../../types/ChartConfig'
10
+ import { getAxisLabelFontSize } from '../../helpers/axisLabelFontSize'
10
11
 
11
12
  interface SmallMultipleTileProps {
12
13
  mode: 'by-series' | 'by-column'
@@ -107,13 +108,16 @@ const SmallMultipleTile: React.FC<SmallMultipleTileProps> = ({
107
108
  }
108
109
  }
109
110
 
111
+ const titlePlacementIsTop = tileConfig.yAxis?.titlePlacement === 'top'
112
+
110
113
  // Small multiples-specific modifications
111
114
  tileConfig = {
112
115
  ...tileConfig,
113
116
  hideXAxisLabel: !isFirstInRow,
114
- hideYAxisLabel: !isFirstInRow,
117
+ hideYAxisLabel: config.hideYAxisLabel || (!isFirstInRow && !titlePlacementIsTop),
115
118
  legend: {
116
119
  ...tileConfig.legend,
120
+ tooltipLegendVisible: !config.legend?.hide,
117
121
  hide: true // Hide legends for all small multiple tiles
118
122
  },
119
123
  xAxis: {
@@ -124,6 +128,8 @@ const SmallMultipleTile: React.FC<SmallMultipleTileProps> = ({
124
128
  }
125
129
 
126
130
  const displayTitle = getTileDisplayTitle(mode, seriesKey, tileValue, tileKey, config)
131
+ const showTopYAxisTitle =
132
+ titlePlacementIsTop && isFirstInRow && !config.hideYAxisLabel && Boolean(tileConfig.runtime?.yAxis?.label)
127
133
 
128
134
  // Get the original context values to merge with our filtered config
129
135
  const originalContextValues = useContext(ConfigContext)
@@ -133,6 +139,7 @@ const SmallMultipleTile: React.FC<SmallMultipleTileProps> = ({
133
139
 
134
140
  // Create a ref for the entire tile (including header) for height measurement
135
141
  const fullTileRef = useRef<HTMLDivElement>(null)
142
+ const [tileChartWidth, setTileChartWidth] = useState(0)
136
143
 
137
144
  // Create a ref for the LinearChart instance for tooltip coordination
138
145
  const linearChartRef = useRef<any>(null)
@@ -163,6 +170,22 @@ const SmallMultipleTile: React.FC<SmallMultipleTileProps> = ({
163
170
  return () => resizeObserver.disconnect()
164
171
  }, [tileKey, onHeightChange])
165
172
 
173
+ useEffect(() => {
174
+ if (!tileParentRef.current) return
175
+
176
+ const resizeObserver = new ResizeObserver(entries => {
177
+ entries.forEach(entry => {
178
+ setTileChartWidth(entry.contentRect.width)
179
+ })
180
+ })
181
+
182
+ resizeObserver.observe(tileParentRef.current)
183
+ return () => resizeObserver.disconnect()
184
+ }, [])
185
+
186
+ const tileVizViewport = tileChartWidth > 0 ? getViewport(tileChartWidth) : originalContextValues.vizViewport || 'lg'
187
+ const topYAxisTitleFontSize = getAxisLabelFontSize(tileVizViewport)
188
+
166
189
  // Pass chart ref to parent SmallMultiples component for tooltip coordination
167
190
  useEffect(() => {
168
191
  if (onChartRef && linearChartRef.current) {
@@ -172,8 +195,13 @@ const SmallMultipleTile: React.FC<SmallMultipleTileProps> = ({
172
195
 
173
196
  return (
174
197
  <div ref={fullTileRef} className='small-multiple-tile'>
175
- <div ref={onHeaderRef} className='tile-header'>
198
+ <div ref={onHeaderRef} className={`tile-header${showTopYAxisTitle ? ' tile-header--with-top-y-axis-title' : ''}`}>
176
199
  <div className='tile-title'>{displayTitle}</div>
200
+ {showTopYAxisTitle && (
201
+ <div className='y-axis-top-title' style={{ fontSize: `${topYAxisTitleFontSize}px` }}>
202
+ {tileConfig.runtime.yAxis.label}
203
+ </div>
204
+ )}
177
205
  </div>
178
206
  <div ref={tileParentRef} className='tile-chart'>
179
207
  <ParentSize
@@ -19,6 +19,18 @@
19
19
  margin-bottom: 0.5rem;
20
20
  }
21
21
 
22
+ .small-multiples-container--with-top-y-axis-title .tile-header {
23
+ margin-bottom: 0;
24
+ }
25
+
26
+ .tile-header--with-top-y-axis-title {
27
+ margin-bottom: 0;
28
+ }
29
+
30
+ .tile-header--with-top-y-axis-title .tile-title {
31
+ margin-bottom: 0.5rem;
32
+ }
33
+
22
34
  .tile-title {
23
35
  font-weight: 700;
24
36
  line-height: 1.3;
@@ -1,8 +1,9 @@
1
- import React, { useContext, useEffect, useRef, useState, useMemo, useCallback } from 'react'
1
+ import React, { useContext, useRef, useState, useMemo, useCallback, useLayoutEffect } from 'react'
2
2
  import SmallMultipleTile from './SmallMultipleTile'
3
3
  import ConfigContext from '../../ConfigContext'
4
4
  import useReduceData from '../../hooks/useReduceData'
5
5
  import useScales from '../../hooks/useScales'
6
+ import { getYAxisAutoPaddingMode } from '../../helpers/getYAxisAutoPaddingMode'
6
7
  import { createCombinedDataForYAxis, applyTileOrder, createTileColorScale } from '../../helpers/smallMultiplesHelpers'
7
8
  import { isMobileSmallMultiplesViewport } from '@cdc/core/helpers/viewports'
8
9
  import './SmallMultiples.css'
@@ -105,9 +106,7 @@ const SmallMultiples: React.FC<SmallMultiplesProps> = ({ config, data, svgRef, p
105
106
  combinedDataForYAxis.data
106
107
  )
107
108
 
108
- const inlineLabel = config.yAxis?.inlineLabel
109
- const inlineLabelHasNoSpace = !inlineLabel?.includes(' ')
110
- const needsYAxisAutoPadding = inlineLabel && !inlineLabelHasNoSpace
109
+ const yAxisAutoPaddingMode = getYAxisAutoPaddingMode(config)
111
110
 
112
111
  const { min, max } = useScales({
113
112
  config: combinedDataForYAxis.config,
@@ -120,7 +119,7 @@ const SmallMultiples: React.FC<SmallMultiplesProps> = ({ config, data, svgRef, p
120
119
  xAxisDataMapped: [],
121
120
  xMax: parentWidth,
122
121
  yMax: parentHeight,
123
- needsYAxisAutoPadding,
122
+ yAxisAutoPaddingMode,
124
123
  currentViewport
125
124
  })
126
125
 
@@ -168,7 +167,7 @@ const SmallMultiples: React.FC<SmallMultiplesProps> = ({ config, data, svgRef, p
168
167
  )
169
168
 
170
169
  // Align tile header heights per row
171
- useEffect(() => {
170
+ useLayoutEffect(() => {
172
171
  const headerEntries = Object.entries(headerRefs.current).filter(([_, ref]) => ref) as TileHeaderEntries
173
172
  if (headerEntries.length === 0) return
174
173
 
@@ -209,7 +208,7 @@ const SmallMultiples: React.FC<SmallMultiplesProps> = ({ config, data, svgRef, p
209
208
  }, [tileItems, tilesPerRow])
210
209
 
211
210
  // Calculate container height from measured tile heights
212
- useEffect(() => {
211
+ useLayoutEffect(() => {
213
212
  if (!parentRef.current) return
214
213
 
215
214
  const measuredHeights = Object.values(tileHeights)
@@ -223,14 +222,21 @@ const SmallMultiples: React.FC<SmallMultiplesProps> = ({ config, data, svgRef, p
223
222
  const totalHeight = numberOfRows * maxTileHeight + totalGapsHeight
224
223
 
225
224
  parentRef.current.style.height = `${totalHeight}px`
226
- }, [tileHeights, numberOfRows, isMobile, parentRef, tilesPerRow])
225
+ }, [tileHeights, numberOfRows, isMobile])
227
226
 
228
227
  if (tileItems.length === 0) {
229
228
  return null
230
229
  }
231
230
 
231
+ const hasTopYAxisTitle =
232
+ config.yAxis?.titlePlacement === 'top' && !config.hideYAxisLabel && Boolean(config.runtime?.yAxis?.label)
233
+
232
234
  return (
233
- <div className='small-multiples-container'>
235
+ <div
236
+ className={`small-multiples-container${
237
+ hasTopYAxisTitle ? ' small-multiples-container--with-top-y-axis-title' : ''
238
+ }`}
239
+ >
234
240
  <div className='small-multiples-grid' style={gridStyle}>
235
241
  {tileItems.map((item, index) => {
236
242
  const customColorScale = createTileColorScale(item, config, colorScale, index, tileItems.length)