@cdc/chart 4.26.2 → 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 (165) hide show
  1. package/CONFIG.md +169 -0
  2. package/README.md +42 -17
  3. package/dist/cdcchart.js +39419 -35180
  4. package/examples/__data__/data-with-metadata.json +10 -0
  5. package/examples/feature/bar/additional-column-tooltip.json +3 -3
  6. package/examples/feature/deviation/planet-deviation-config.json +2 -2
  7. package/examples/feature/paired-bar/paired-bar-example.json +2 -2
  8. package/examples/feature/pie/planet-pie-example-config.json +2 -1
  9. package/examples/feature/tests-case-rate/case-rate-example-config.json +2 -2
  10. package/examples/feature/tests-covid/covid-confidence-example-config.json +2 -2
  11. package/examples/feature/tests-covid/covid-example-config.json +2 -2
  12. package/examples/feature/tests-cutoff/cutoff-example-config.json +2 -2
  13. package/examples/feature/tests-non-numerics/planet-pie-example-config-nonnumeric.json +2 -2
  14. package/examples/metadata-variables.json +58 -0
  15. package/examples/minimal-example.json +24 -0
  16. package/examples/private/test2.json +35012 -0
  17. package/package.json +4 -4
  18. package/src/CdcChart.tsx +8 -4
  19. package/src/CdcChartComponent.tsx +326 -290
  20. package/src/_stories/Chart.CustomColors.smoke.stories.tsx +97 -0
  21. package/src/_stories/Chart.Defaults.smoke.stories.tsx +95 -0
  22. package/src/_stories/{Chart.HTMLInDataTable.stories.tsx → Chart.HTMLInDataTable.smoke.stories.tsx} +10 -0
  23. package/src/_stories/Chart.Minimal.smoke.stories.tsx +25 -0
  24. package/src/_stories/{Chart.SmallMultiples.stories.tsx → Chart.SmallMultiples.smoke.stories.tsx} +17 -1
  25. package/src/_stories/Chart.SmallestLeftAxisMax.smoke.stories.tsx +64 -0
  26. package/src/_stories/{Chart.stories.tsx → Chart.smoke.stories.tsx} +48 -2
  27. package/src/_stories/{Chart.tooltip.stories.tsx → Chart.tooltip.smoke.stories.tsx} +68 -0
  28. package/src/_stories/ChartBar.Editor.stories.tsx +177 -115
  29. package/src/_stories/ChartBrush.Editor.stories.tsx +11 -25
  30. package/src/_stories/ChartEditor.Editor.stories.tsx +24 -1
  31. package/src/_stories/{Chart.CustomColors.stories.tsx → ChartLine.DashedDates.smoke.stories.tsx} +4 -4
  32. package/src/_stories/_mock/line_chart_dashed_dates.json +276 -0
  33. package/src/_stories/_mock/paired-bar-abbr.json +421 -0
  34. package/src/_stories/_mock/paired-bar.json +1 -1
  35. package/src/_stories/_mock/pie_custom_colors.json +268 -0
  36. package/src/_stories/_mock/smallest_left_axis_max.json +104 -0
  37. package/src/components/Annotations/components/AnnotationDraggable.styles.css +10 -10
  38. package/src/components/Annotations/components/AnnotationDraggable.tsx +1 -1
  39. package/src/components/Annotations/components/AnnotationDropdown.styles.css +1 -1
  40. package/src/components/Annotations/components/AnnotationList.styles.css +11 -11
  41. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +10 -2
  42. package/src/components/Axis/BottomAxis.tsx +10 -3
  43. package/src/components/Axis/LeftAxis.tsx +2 -4
  44. package/src/components/Axis/LeftAxisGridlines.tsx +1 -3
  45. package/src/components/Axis/PairedBarAxis.tsx +10 -4
  46. package/src/components/BarChart/components/BarChart.Horizontal.tsx +21 -33
  47. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +21 -35
  48. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +21 -36
  49. package/src/components/BarChart/components/BarChart.Vertical.tsx +21 -34
  50. package/src/components/BarChart/components/BarChart.tsx +2 -6
  51. package/src/components/BarChart/helpers/getPatternUrl.ts +94 -0
  52. package/src/components/BarChart/helpers/tests/getPatternUrl.test.ts +134 -0
  53. package/src/components/BarChart/helpers/useBarChart.ts +4 -1
  54. package/src/components/BoxPlot/BoxPlot.Vertical.tsx +3 -3
  55. package/src/components/Brush/BrushSelector.tsx +2 -1
  56. package/src/components/Brush/MiniChartPreview.tsx +21 -26
  57. package/src/components/EditorPanel/EditorPanel.tsx +692 -954
  58. package/src/components/EditorPanel/EditorPanelContext.ts +1 -1
  59. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +218 -167
  60. package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +0 -78
  61. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +39 -1
  62. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +169 -135
  63. package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +154 -109
  64. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +3 -3
  65. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +95 -28
  66. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +45 -42
  67. package/src/components/EditorPanel/editor-panel.scss +1 -1
  68. package/src/components/EditorPanel/useEditorPermissions.ts +1 -10
  69. package/src/components/Forecasting/Forecasting.tsx +2 -2
  70. package/src/components/ForestPlot/ForestPlot.tsx +26 -22
  71. package/src/components/HorizonChart/HorizonChart.tsx +10 -2
  72. package/src/components/HoverLine/HoverLine.tsx +6 -8
  73. package/src/components/Legend/Legend.Component.tsx +4 -4
  74. package/src/components/Legend/LegendGroup/LegendGroup.styles.css +9 -4
  75. package/src/components/Legend/LegendGroup/LegendGroup.tsx +3 -1
  76. package/src/components/Legend/helpers/createFormatLabels.tsx +3 -2
  77. package/src/components/Legend/helpers/index.ts +0 -2
  78. package/src/components/LineChart/LineChartProps.ts +1 -0
  79. package/src/components/LineChart/components/LineChart.BumpCircle.tsx +3 -2
  80. package/src/components/LineChart/index.tsx +11 -4
  81. package/src/components/LinearChart/VisualizationRenderer.tsx +11 -1
  82. package/src/components/LinearChart/tests/LinearChart.test.tsx +224 -0
  83. package/src/components/LinearChart/tests/mockConfigContext.ts +2 -0
  84. package/src/components/LinearChart.tsx +124 -47
  85. package/src/components/PairedBarChart.jsx +2 -2
  86. package/src/components/PieChart/PieChart.tsx +30 -13
  87. package/src/components/RadarChart/RadarChart.tsx +1 -1
  88. package/src/components/Regions/components/Regions.tsx +36 -26
  89. package/src/components/Sankey/components/Sankey.tsx +3 -3
  90. package/src/components/Sankey/sankey.scss +1 -1
  91. package/src/components/ScatterPlot/ScatterPlot.jsx +19 -17
  92. package/src/components/SmallMultiples/SmallMultipleTile.tsx +1 -0
  93. package/src/components/SmallMultiples/SmallMultiples.css +5 -5
  94. package/src/components/Sparkline/components/SparkLine.tsx +1 -1
  95. package/src/components/Sparkline/index.scss +4 -2
  96. package/src/components/WarmingStripes/WarmingStripes.tsx +5 -3
  97. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +8 -8
  98. package/src/data/initial-state.js +24 -14
  99. package/src/data/legacy-defaults.ts +18 -0
  100. package/src/helpers/abbreviateNumber.ts +24 -17
  101. package/src/helpers/calculateLeftYAxisWidth.ts +193 -0
  102. package/src/helpers/getChartPatternId.ts +17 -0
  103. package/src/helpers/getMinMax.ts +16 -2
  104. package/src/helpers/seriesColumnSettings.ts +114 -0
  105. package/src/helpers/tests/countNumOfTicks.test.ts +77 -0
  106. package/src/helpers/tests/seriesColumnSettings.test.ts +84 -0
  107. package/src/helpers/tests/tooltipHelpers.test.ts +151 -0
  108. package/src/helpers/tooltipHelpers.ts +176 -0
  109. package/src/hooks/useHighlightedBars.ts +9 -16
  110. package/src/hooks/useProgrammaticTooltip.ts +16 -2
  111. package/src/hooks/useRightAxis.ts +14 -0
  112. package/src/hooks/useScales.ts +92 -56
  113. package/src/hooks/useSmallMultipleSynchronization.ts +3 -4
  114. package/src/hooks/useTooltip.tsx +186 -40
  115. package/src/scss/main.scss +152 -79
  116. package/src/test/CdcChart.test.jsx +26 -2
  117. package/src/types/ChartConfig.ts +5 -0
  118. package/tests/fixtures/chart-config-with-metadata.json +29 -0
  119. package/tests/fixtures/data-with-metadata.json +10 -0
  120. package/examples/chart-regression-2.json +0 -2360
  121. package/examples/feature/__data__/age-adjusted-rates.json +0 -1486
  122. package/examples/feature/__data__/city-temperature.json +0 -2198
  123. package/examples/feature/bar/Barchart_with_negative.json +0 -34
  124. package/examples/feature/boxplot/box-plot-data.json +0 -71
  125. package/examples/feature/boxplot/boxplot-data.json +0 -156
  126. package/examples/feature/combo/combochart-categories_are_numbers .json +0 -18
  127. package/examples/feature/forest-plot/log.json +0 -26
  128. package/examples/feature/tests-date-exclusions/date-exclusions-data.json +0 -162
  129. package/examples/region-issue.json +0 -2065
  130. package/examples/xaxis.json +0 -493
  131. /package/examples/feature/bar/{tall-data.json → __data__/tall-data.json} +0 -0
  132. /package/examples/feature/deviation/{planet-deviation-data.json → __data__/planet-deviation-data.json} +0 -0
  133. /package/examples/feature/paired-bar/{paired-bar-data.json → __data__/paired-bar-data.json} +0 -0
  134. /package/examples/feature/paired-bar/{paired-bar-formatted.json → __data__/paired-bar-formatted.json} +0 -0
  135. /package/examples/feature/tests-case-rate/{case-rate-example-data.json → __data__/case-rate-example-data.json} +0 -0
  136. /package/examples/feature/tests-covid/{covid-example-data-confidence.json → __data__/covid-example-data-confidence.json} +0 -0
  137. /package/examples/feature/tests-covid/{covid-example-data.json → __data__/covid-example-data.json} +0 -0
  138. /package/examples/feature/tests-cutoff/{cutoff-example-data.json → __data__/cutoff-example-data.json} +0 -0
  139. /package/examples/feature/tests-non-numerics/{planet-example-data-nonnumeric.json → __data__/planet-example-data-nonnumeric.json} +0 -0
  140. /package/src/_stories/{Chart.Anchors.stories.tsx → Chart.Anchors.smoke.stories.tsx} +0 -0
  141. /package/src/_stories/{Chart.BoxPlot.stories.tsx → Chart.BoxPlot.smoke.stories.tsx} +0 -0
  142. /package/src/_stories/{Chart.CI.stories.tsx → Chart.CI.smoke.stories.tsx} +0 -0
  143. /package/src/_stories/{Chart.Combo.stories.tsx → Chart.Combo.smoke.stories.tsx} +0 -0
  144. /package/src/_stories/{Chart.DynamicSeries.stories.tsx → Chart.DynamicSeries.smoke.stories.tsx} +0 -0
  145. /package/src/_stories/{Chart.Filters.stories.tsx → Chart.Filters.smoke.stories.tsx} +0 -0
  146. /package/src/_stories/{Chart.Forecast.stories.tsx → Chart.Forecast.smoke.stories.tsx} +0 -0
  147. /package/src/_stories/{Chart.Legend.Gradient.stories.tsx → Chart.Legend.Gradient.smoke.stories.tsx} +0 -0
  148. /package/src/_stories/{Chart.Patterns.stories.tsx → Chart.Patterns.smoke.stories.tsx} +0 -0
  149. /package/src/_stories/{Chart.PreserveDecimals.stories.tsx → Chart.PreserveDecimals.smoke.stories.tsx} +0 -0
  150. /package/src/_stories/{Chart.Regions.Categorical.stories.tsx → Chart.Regions.Categorical.smoke.stories.tsx} +0 -0
  151. /package/src/_stories/{Chart.Regions.DateScale.stories.tsx → Chart.Regions.DateScale.smoke.stories.tsx} +0 -0
  152. /package/src/_stories/{Chart.Regions.DateTimeScale.stories.tsx → Chart.Regions.DateTimeScale.smoke.stories.tsx} +0 -0
  153. /package/src/_stories/{Chart.ScatterPlot.stories.tsx → Chart.ScatterPlot.smoke.stories.tsx} +0 -0
  154. /package/src/_stories/{ChartAnnotation.stories.tsx → ChartAnnotation.smoke.stories.tsx} +0 -0
  155. /package/src/_stories/{ChartAxisLabels.stories.tsx → ChartAxisLabels.smoke.stories.tsx} +0 -0
  156. /package/src/_stories/{ChartAxisTitles.stories.tsx → ChartAxisTitles.smoke.stories.tsx} +0 -0
  157. /package/src/_stories/{ChartBrush.Matrix.Continuous.stories.tsx → ChartBrush.Matrix.Continuous.smoke.stories.tsx} +0 -0
  158. /package/src/_stories/{ChartBrush.Matrix.Date.stories.tsx → ChartBrush.Matrix.Date.smoke.stories.tsx} +0 -0
  159. /package/src/_stories/{ChartBrush.Matrix.DateTime.stories.tsx → ChartBrush.Matrix.DateTime.smoke.stories.tsx} +0 -0
  160. /package/src/_stories/{ChartBrush.stories.tsx → ChartBrush.smoke.stories.tsx} +0 -0
  161. /package/src/_stories/{ChartLine.QuadrantAngles.stories.tsx → ChartLine.QuadrantAngles.smoke.stories.tsx} +0 -0
  162. /package/src/_stories/{ChartLine.Suppression.stories.tsx → ChartLine.Suppression.smoke.stories.tsx} +0 -0
  163. /package/src/_stories/{ChartLine.Symbols.stories.tsx → ChartLine.Symbols.smoke.stories.tsx} +0 -0
  164. /package/src/_stories/{ChartPrefixSuffix.stories.tsx → ChartPrefixSuffix.smoke.stories.tsx} +0 -0
  165. /package/src/_stories/{TechAdoptionWithLinks.stories.tsx → TechAdoptionWithLinks.smoke.stories.tsx} +0 -0
@@ -4,6 +4,7 @@ import { describe, expect, it, vi, beforeAll } from 'vitest'
4
4
  import LinearChart from '../../LinearChart'
5
5
  import ConfigContext from '../../../ConfigContext'
6
6
  import { createMockChartContext } from './mockConfigContext'
7
+ import forestPlotConfig from '../../../../examples/feature/forest-plot/forest-plot.json'
7
8
 
8
9
  // Mock ResizeObserver
9
10
  vi.stubGlobal(
@@ -66,6 +67,11 @@ const renderLinearChart = (
66
67
  )
67
68
  }
68
69
 
70
+ const getLeftAxisLabelTransforms = container =>
71
+ Array.from(container.querySelectorAll('.left-axis text.y-label'))
72
+ .map(label => label.getAttribute('transform'))
73
+ .filter(Boolean)
74
+
69
75
  describe('LinearChart', () => {
70
76
  describe('rendering', () => {
71
77
  it('renders without crashing', () => {
@@ -147,6 +153,82 @@ describe('LinearChart', () => {
147
153
  })
148
154
  expect(container).toBeTruthy()
149
155
  })
156
+
157
+ it('keeps forest plot lines inside the computed plot bounds at narrow and wide widths', () => {
158
+ const forestContextOverrides = {
159
+ transformedData: forestPlotConfig.data,
160
+ rawData: forestPlotConfig.data
161
+ }
162
+
163
+ const narrowRender = renderLinearChart(forestPlotConfig as any, forestContextOverrides, {
164
+ parentWidth: 320,
165
+ parentHeight: 500
166
+ })
167
+ const wideRender = renderLinearChart(forestPlotConfig as any, forestContextOverrides, {
168
+ parentWidth: 960,
169
+ parentHeight: 500
170
+ })
171
+
172
+ const narrowTopLine = narrowRender.container.querySelector('.forestplot__top-line')
173
+ const wideTopLine = wideRender.container.querySelector('.forestplot__top-line')
174
+ const narrowCiLine = narrowRender.container.querySelector('line[class^="line-"]')
175
+ const wideCiLine = wideRender.container.querySelector('line[class^="line-"]')
176
+
177
+ expect(narrowTopLine).toBeTruthy()
178
+ expect(wideTopLine).toBeTruthy()
179
+ expect(narrowCiLine).toBeTruthy()
180
+ expect(wideCiLine).toBeTruthy()
181
+
182
+ const narrowStart = Number(narrowTopLine?.getAttribute('x1'))
183
+ const narrowEnd = Number(narrowTopLine?.getAttribute('x2'))
184
+ const wideStart = Number(wideTopLine?.getAttribute('x1'))
185
+ const wideEnd = Number(wideTopLine?.getAttribute('x2'))
186
+
187
+ expect(narrowStart).toBe(0)
188
+ expect(narrowEnd).toBeLessThanOrEqual(320)
189
+ expect(wideStart).toBe(0)
190
+ expect(wideEnd).toBeLessThanOrEqual(960)
191
+ expect(wideEnd - wideStart).toBeGreaterThan(narrowEnd - narrowStart)
192
+
193
+ expect(Number(narrowCiLine?.getAttribute('x1'))).toBeGreaterThan(narrowStart)
194
+ expect(Number(narrowCiLine?.getAttribute('x2'))).toBeLessThanOrEqual(narrowEnd)
195
+ expect(Number(wideCiLine?.getAttribute('x1'))).toBeGreaterThan(wideStart)
196
+ expect(Number(wideCiLine?.getAttribute('x2'))).toBeLessThanOrEqual(wideEnd)
197
+ })
198
+
199
+ it('avoids rendering a duplicate manual bottom border when the forest plot x-axis is visible', () => {
200
+ const { container } = renderLinearChart(
201
+ forestPlotConfig as any,
202
+ {
203
+ transformedData: forestPlotConfig.data,
204
+ rawData: forestPlotConfig.data
205
+ },
206
+ { parentWidth: 800, parentHeight: 500 }
207
+ )
208
+
209
+ expect(container.querySelector('.forestplot__top-line')).toBeTruthy()
210
+ expect(container.querySelector('.forestplot__bottom-line')).toBeFalsy()
211
+ const bottomAxisLine = container.querySelector('.bottom-axis > line[stroke="#333"]')
212
+ expect(bottomAxisLine?.getAttribute('x1')).toBe('0')
213
+ })
214
+
215
+ it('renders forest plot rows from transformedData instead of rawData', () => {
216
+ const filteredData = forestPlotConfig.data.slice(0, 2)
217
+ const { container } = renderLinearChart(
218
+ forestPlotConfig as any,
219
+ {
220
+ transformedData: filteredData,
221
+ rawData: forestPlotConfig.data
222
+ },
223
+ { parentWidth: 800, parentHeight: 500 }
224
+ )
225
+
226
+ expect(container.querySelectorAll('.lower-ci')).toHaveLength(filteredData.length)
227
+ expect(container.querySelectorAll('line[class^="line-"]')).toHaveLength(filteredData.length)
228
+ expect(container.textContent).not.toContain(
229
+ String(forestPlotConfig.data[forestPlotConfig.data.length - 1]['Author(s) and Year'])
230
+ )
231
+ })
150
232
  })
151
233
 
152
234
  describe('axis rendering', () => {
@@ -181,6 +263,148 @@ describe('LinearChart', () => {
181
263
  // The axis line should be hidden, but grid lines may still render
182
264
  expect(container.querySelector('svg')).toBeTruthy()
183
265
  })
266
+
267
+ it('ignores yAxis.labelOffset when positioning the vertical left-axis title', () => {
268
+ const baseRuntime = {
269
+ xAxis: {
270
+ type: 'date',
271
+ dataKey: 'Date',
272
+ label: 'X-Axis'
273
+ },
274
+ yAxis: {
275
+ size: 50,
276
+ label: 'Y-Axis',
277
+ gridLines: true,
278
+ labelOffset: 0
279
+ },
280
+ originalXAxis: {
281
+ dataKey: 'Date'
282
+ },
283
+ series: [],
284
+ seriesKeys: [],
285
+ seriesLabelsAll: [],
286
+ uniqueId: 'test-chart'
287
+ }
288
+
289
+ const baseRender = renderLinearChart({
290
+ yAxis: {
291
+ hideAxis: false,
292
+ hideLabel: false,
293
+ hideTicks: false,
294
+ size: '50',
295
+ gridLines: true,
296
+ label: 'Y-Axis',
297
+ tickRotation: 0,
298
+ anchors: [],
299
+ axisPadding: 0,
300
+ labelPlacement: 'On Date/Category Axis',
301
+ rightAxisSize: 0,
302
+ labelOffset: 0
303
+ },
304
+ runtime: baseRuntime
305
+ })
306
+ const offsetRender = renderLinearChart({
307
+ yAxis: {
308
+ hideAxis: false,
309
+ hideLabel: false,
310
+ hideTicks: false,
311
+ size: '50',
312
+ gridLines: true,
313
+ label: 'Y-Axis',
314
+ tickRotation: 0,
315
+ anchors: [],
316
+ axisPadding: 0,
317
+ labelPlacement: 'On Date/Category Axis',
318
+ rightAxisSize: 0,
319
+ labelOffset: 240
320
+ },
321
+ runtime: {
322
+ ...baseRuntime,
323
+ yAxis: {
324
+ ...baseRuntime.yAxis,
325
+ labelOffset: 240
326
+ }
327
+ }
328
+ })
329
+
330
+ const baseTransforms = getLeftAxisLabelTransforms(baseRender.container)
331
+ const offsetTransforms = getLeftAxisLabelTransforms(offsetRender.container)
332
+
333
+ expect(baseTransforms.length).toBeGreaterThan(0)
334
+ expect(offsetTransforms).toEqual(baseTransforms)
335
+ })
336
+
337
+ it('ignores horizontal xAxis.labelOffset when positioning the left-side axis title', () => {
338
+ const baseRuntime = {
339
+ xAxis: {
340
+ type: 'linear',
341
+ dataKey: 'Value',
342
+ label: 'X-Axis'
343
+ },
344
+ yAxis: {
345
+ size: 50,
346
+ label: 'Y-Axis',
347
+ gridLines: true,
348
+ labelOffset: 0
349
+ },
350
+ originalXAxis: {
351
+ dataKey: 'Date'
352
+ },
353
+ series: [],
354
+ seriesKeys: [],
355
+ seriesLabelsAll: [],
356
+ uniqueId: 'test-chart'
357
+ }
358
+
359
+ const baseRender = renderLinearChart({
360
+ orientation: 'horizontal',
361
+ xAxis: {
362
+ type: 'linear',
363
+ dataKey: 'Value',
364
+ label: 'X-Axis',
365
+ hideAxis: false,
366
+ hideLabel: false,
367
+ hideTicks: false,
368
+ size: '50',
369
+ tickRotation: 0,
370
+ maxTickRotation: 90,
371
+ anchors: [],
372
+ axisPadding: 0,
373
+ labelOffset: 0
374
+ },
375
+ runtime: baseRuntime
376
+ })
377
+ const offsetRender = renderLinearChart({
378
+ orientation: 'horizontal',
379
+ xAxis: {
380
+ type: 'linear',
381
+ dataKey: 'Value',
382
+ label: 'X-Axis',
383
+ hideAxis: false,
384
+ hideLabel: false,
385
+ hideTicks: false,
386
+ size: '50',
387
+ tickRotation: 0,
388
+ maxTickRotation: 90,
389
+ anchors: [],
390
+ axisPadding: 0,
391
+ labelOffset: 240
392
+ },
393
+ runtime: {
394
+ ...baseRuntime,
395
+ yAxis: {
396
+ ...baseRuntime.yAxis,
397
+ labelOffset: 240
398
+ }
399
+ }
400
+ })
401
+
402
+ const baseTransforms = getLeftAxisLabelTransforms(baseRender.container)
403
+ const offsetTransforms = getLeftAxisLabelTransforms(offsetRender.container)
404
+
405
+ expect(baseTransforms.length).toBeGreaterThan(0)
406
+ expect(offsetTransforms).toEqual(baseTransforms)
407
+ })
184
408
  })
185
409
 
186
410
  describe('SVG dimensions', () => {
@@ -124,6 +124,8 @@ export const createMockChartContext = (
124
124
  clean: (s: any) => s,
125
125
  formatTooltipsDate: (date: any) => String(date),
126
126
  legendId: 'test-legend',
127
+ rawData: config.data,
128
+ updateConfig: () => {},
127
129
  ...contextOverrides
128
130
  } as ChartContext
129
131
  }
@@ -18,8 +18,7 @@ import { Tooltip as ReactTooltip } from 'react-tooltip'
18
18
  import 'react-tooltip/dist/react-tooltip.css'
19
19
  import { Text } from '@visx/text'
20
20
  import { useTooltip, TooltipWithBounds } from '@visx/tooltip'
21
- import _ from 'lodash'
22
-
21
+ import ResizeObserver from 'resize-observer-polyfill'
23
22
  // CDC Components
24
23
  import { isDateScale } from '@cdc/core/helpers/cove/date'
25
24
  import ConfigContext from '../ConfigContext'
@@ -36,6 +35,7 @@ import { useTickFormatters } from './LinearChart/utils/tickFormatting'
36
35
  import { isLegendWrapViewport, isMobileFontViewport } from '@cdc/core/helpers/viewports'
37
36
  import { calcInitialHeight } from '../helpers/sizeHelpers'
38
37
  import { calculateHorizontalBarCategoryLabelWidth } from '../helpers/calculateHorizontalBarCategoryLabelWidth'
38
+ import { calculateLeftYAxisWidth } from '../helpers/calculateLeftYAxisWidth'
39
39
 
40
40
  // Hooks
41
41
  import useReduceData from '../hooks/useReduceData'
@@ -52,6 +52,7 @@ import Annotation from './Annotations'
52
52
  import { countNumOfTicks } from '../helpers/countNumOfTicks'
53
53
  import HoverLine from './HoverLine/HoverLine'
54
54
  import { SmallMultiples } from './SmallMultiples'
55
+ import { type TooltipDisplayData } from '../helpers/tooltipHelpers'
55
56
 
56
57
  type LinearChartProps = {
57
58
  parentWidth: number
@@ -63,6 +64,7 @@ const BOTTOM_LABEL_PADDING = 9
63
64
  const X_TICK_LABEL_PADDING = 4.5
64
65
  const DEFAULT_TICK_LENGTH = 8
65
66
  const DEFAULT_MAX_TICK_ROTATION = 90
67
+ const DEFAULT_LEFT_Y_AXIS_WIDTH = 50
66
68
 
67
69
  // Font sizes
68
70
  const TICK_LABEL_FONT_SIZE = 16
@@ -89,10 +91,7 @@ const WARMING_STRIPES_HEIGHT = 78
89
91
  // Time constants
90
92
  const MONTH_AS_MS = 1000 * 60 * 60 * 24 * 30
91
93
 
92
- type TooltipData = {
93
- dataXPosition?: number
94
- dataYPosition?: number
95
- }
94
+ type TooltipData = Partial<TooltipDisplayData>
96
95
 
97
96
  type UseTooltipReturn<T = TooltipData> = {
98
97
  tooltipData: T | null
@@ -152,6 +151,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
152
151
  const [suffixWidth, setSuffixWidth] = useState(0)
153
152
  const [calculatedSvgHeight, setCalculatedSvgHeight] = useState<number | null>(null)
154
153
  const [axisUpdateKey, setAxisUpdateKey] = useState(0)
154
+ const [axisBottomSizeKey, setAxisBottomSizeKey] = useState(0)
155
155
  const [synchronizedXValue, setSynchronizedXValue] = useState<any>(null)
156
156
 
157
157
  // REFS
@@ -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`
@@ -245,13 +243,22 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
245
243
  setAxisUpdateKey(prev => prev + 1)
246
244
  }, [data.length, xAxisDataMapped?.[0], xAxisDataMapped?.[xAxisDataMapped.length - 1]])
247
245
 
248
- const { yScaleRight, hasRightAxis } = useRightAxis({ config, yMax, data })
246
+ // Recompute heights when bottom axis size changes (e.g., font load or wrap).
247
+ useEffect(() => {
248
+ const axisBottomEl = axisBottomRef.current
249
+ if (!axisBottomEl) return
250
+ const observer = new ResizeObserver(() => {
251
+ setAxisBottomSizeKey(prev => prev + 1)
252
+ })
253
+ observer.observe(axisBottomEl)
254
+ return () => observer.disconnect()
255
+ }, [axisBottomRef.current])
249
256
 
250
- // State for computed y-axis width - allows re-render when horizontal bar label space is calculated
251
- const [computedYAxisWidth, setComputedYAxisWidth] = useState<number | null>(null)
257
+ const { yScaleRight, hasRightAxis } = useRightAxis({ config, yMax, data })
252
258
 
253
- // Use computed width if available, otherwise fall back to config value
254
- 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
255
262
 
256
263
  // Chart width calculation using the current y-axis width
257
264
  const xMax = parentWidth - yAxisWidth - (hasRightAxis ? config.yAxis.rightAxisSize : 0)
@@ -292,39 +299,77 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
292
299
  shouldAbbreviate
293
300
  })
294
301
 
295
- // 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.
296
304
  const categoryLabelSpace = useMemo(() => {
305
+ if (config.yAxis.hideLabel) return 0
306
+
297
307
  return calculateHorizontalBarCategoryLabelWidth({
298
308
  yScale,
299
309
  chartWidth: parentWidth,
300
310
  formatDate,
301
311
  parseDate,
302
312
  tickLabelFont: GET_TEXT_WIDTH_FONT,
303
- xAxisType: config.runtime.xAxis?.type,
313
+ xAxisType: runtime.xAxis?.type,
304
314
  labelPlacement: config.yAxis.labelPlacement
305
315
  })
306
- }, [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
+ ])
307
326
 
308
327
  const horizontalYAxisLabelSpace = runtime.yAxis.label && !config.hideYAxisLabel ? 30 : 0
309
328
 
310
- // Update y-axis width state when computed value changes (for horizontal bar charts)
311
- useEffect(() => {
312
- if (isHorizontal && config.visualizationType === 'Bar') {
313
- const newWidth = categoryLabelSpace + horizontalYAxisLabelSpace
314
- if (newWidth !== computedYAxisWidth) {
315
- setComputedYAxisWidth(newWidth)
316
- }
317
- } else if (computedYAxisWidth !== null) {
318
- // Reset to null for non-horizontal bar charts so we use config value
319
- setComputedYAxisWidth(null)
320
- }
321
- }, [isHorizontal, config.visualizationType, categoryLabelSpace, horizontalYAxisLabelSpace, computedYAxisWidth])
322
-
323
329
  const [yTickCount, xTickCount] = ['yAxis', 'xAxis'].map(axis =>
324
330
  countNumOfTicks({ axis, max, runtime, currentViewport, isHorizontal, data, config, min })
325
331
  )
326
332
  const handleNumTicks = isForestPlot ? config.data.length : yTickCount
327
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
+
328
373
  // Tooltip Helpers
329
374
 
330
375
  const { tooltipData, showTooltip, hideTooltip, tooltipOpen, tooltipLeft, tooltipTop } =
@@ -342,6 +387,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
342
387
  xScale,
343
388
  yScale,
344
389
  seriesScale,
390
+ yAxisWidth,
345
391
  showTooltip,
346
392
  hideTooltip,
347
393
  interactionLabel
@@ -376,7 +422,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
376
422
  return manualStep
377
423
  }, [config.xAxis.manualStep, config.xAxis.viewportStepCount, currentViewport])
378
424
 
379
- const smallMultiplesSync = useSmallMultipleSynchronization(xMax, yMax, getXValueFromCoordinate)
425
+ const smallMultiplesSync = useSmallMultipleSynchronization(xMax, yMax, yAxisWidth, getXValueFromCoordinate)
380
426
 
381
427
  const onMouseMove = event => {
382
428
  const svgRect = event.currentTarget.getBoundingClientRect()
@@ -405,7 +451,9 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
405
451
  const internalSvgRef = useProgrammaticTooltip({
406
452
  svgRef,
407
453
  getCoordinateFromXValue,
454
+ getXValueFromCoordinate,
408
455
  config,
456
+ yAxisWidth,
409
457
  setPoint,
410
458
  setShowHoverLine,
411
459
  handleTooltipMouseOver,
@@ -415,7 +463,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
415
463
 
416
464
  // Make sure the chart is visible if in the editor
417
465
  useEffect(() => {
418
- const element = document.querySelector('.isEditor')
466
+ const element = document.querySelector('.is-editor')
419
467
  if (element) {
420
468
  setAnimatedChart(true)
421
469
  }
@@ -498,7 +546,16 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
498
546
  const legendIsLeftOrRight =
499
547
  legend?.position !== 'top' && legend?.position !== 'bottom' && !isLegendWrapViewport(currentViewport)
500
548
  legendRef.current.style.transform = legendIsLeftOrRight ? `translateY(${topLabelOnGridlineHeight}px)` : 'none'
501
- }, [axisBottomRef.current, config, config.xAxis.brushActive, currentViewport, topYLabelRef.current, initialHeight])
549
+ }, [
550
+ axisBottomRef.current,
551
+ config,
552
+ config.xAxis.brushActive,
553
+ currentViewport,
554
+ topYLabelRef.current,
555
+ initialHeight,
556
+ parentWidth,
557
+ axisBottomSizeKey
558
+ ])
502
559
 
503
560
  useEffect(() => {
504
561
  if (!tooltipOpen) return
@@ -542,7 +599,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
542
599
  <svg
543
600
  ref={internalSvgRef}
544
601
  onMouseMove={onMouseMove}
545
- width={parentWidth + config.yAxis.rightAxisSize}
602
+ width={parentWidth}
546
603
  height={isNoDataAvailable ? 1 : calculatedSvgHeight ?? parentHeight}
547
604
  className={`linear ${config.animate ? 'animated' : ''} ${animatedChart && config.animate ? 'animate' : ''} ${
548
605
  debugSvg && 'debug'
@@ -569,7 +626,6 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
569
626
  xMax={xMax}
570
627
  yAxisWidth={yAxisWidth}
571
628
  numTicks={handleNumTicks}
572
- yLabelOffset={yLabelOffset}
573
629
  axisLabelFontSize={axisLabelFontSize}
574
630
  />
575
631
  )}
@@ -653,8 +709,8 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
653
709
  strokeDasharray={handleLineType(anchor.lineStyle)}
654
710
  stroke={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
655
711
  className='anchor-y'
656
- from={{ x: Number(runtime.yAxis.size), y: position - middleOffset }}
657
- 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 }}
658
714
  />
659
715
  )
660
716
  })}
@@ -690,8 +746,8 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
690
746
  stroke={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
691
747
  fill={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
692
748
  className='anchor-x'
693
- from={{ x: Number(anchorPosition) + Number(padding), y: 0 }}
694
- 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 }}
695
751
  />
696
752
  )
697
753
  })}
@@ -708,6 +764,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
708
764
  tooltipData={tooltipData}
709
765
  yMax={yMax}
710
766
  xMax={xMax}
767
+ yAxisWidth={yAxisWidth}
711
768
  />
712
769
  )}
713
770
  {isNoDataAvailable && (
@@ -721,8 +778,22 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
721
778
  )}
722
779
  {showHoverLine && (
723
780
  <>
724
- <HoverLine xMax={xMax} yMax={yMax} point={point} tooltipData={tooltipData} orientation='horizontal' />
725
- <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
+ />
726
797
  </>
727
798
  )}
728
799
  <Group left={yAxisWidth}>
@@ -758,7 +829,6 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
758
829
  suffixWidth={suffixWidth}
759
830
  horizontalYAxisLabelSpace={horizontalYAxisLabelSpace}
760
831
  categoryLabelSpace={categoryLabelSpace}
761
- yLabelOffset={yLabelOffset}
762
832
  />
763
833
  )}
764
834
  {config.yAxis.type === 'categorical' && config.orientation === 'vertical' && (
@@ -819,10 +889,10 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
819
889
  </svg>
820
890
  {!isDraggingAnnotation &&
821
891
  tooltipData &&
822
- Object.entries(tooltipData.data).length > 0 &&
892
+ tooltipData.data?.length > 0 &&
823
893
  tooltipOpen &&
824
894
  showTooltip &&
825
- !tooltipData?.data?.some(subArray => subArray.some(item => item === undefined)) &&
895
+ !tooltipData?.data?.some(row => row?.value === undefined) &&
826
896
  tooltipData.dataYPosition &&
827
897
  tooltipData.dataXPosition &&
828
898
  !config.tooltips.singleSeries && (
@@ -834,15 +904,22 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
834
904
  <TooltipWithBounds
835
905
  ref={tooltipRef}
836
906
  key={Math.random()}
837
- className={'tooltip cdc-open-viz-module'}
907
+ className={'tooltip cove-visualization'}
838
908
  left={tooltipLeft}
839
909
  top={tooltipTop}
840
910
  >
841
911
  <ul>
842
912
  {typeof tooltipData === 'object' &&
843
- Object.entries(tooltipData.data)
844
- .filter(([_, values]) => Array.isArray(values) && !values.includes(undefined))
845
- .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
+ ))}
846
923
  </ul>
847
924
  </TooltipWithBounds>
848
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
  >