@cdc/chart 4.26.1 → 4.26.3

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 (173) hide show
  1. package/CLAUDE.local.md +79 -0
  2. package/LICENSE +201 -0
  3. package/dist/{cdcchart-dgT_1dIT.es.js → cdcchart-DQ00cQCm.es.js} +1 -20
  4. package/dist/cdcchart.js +54742 -49796
  5. package/examples/data/data-with-metadata.json +10 -0
  6. package/examples/default.json +378 -0
  7. package/examples/feature/__data__/horizon-chart-data.json +373 -0
  8. package/examples/feature/annotations/index.json +3 -6
  9. package/examples/feature/horizon/horizon-chart.json +395 -0
  10. package/examples/feature/pie/planet-pie-example-config.json +2 -1
  11. package/examples/line-chart-states.json +1085 -0
  12. package/examples/metadata-variables.json +58 -0
  13. package/examples/private/123.json +694 -0
  14. package/examples/private/anchor-issue.json +4094 -0
  15. package/examples/private/backwards-slider.json +10430 -0
  16. package/examples/private/georgia.csv +160 -0
  17. package/examples/private/timeline-data.json +1 -0
  18. package/examples/private/timeline.json +389 -0
  19. package/examples/radar-chart-simple.json +133 -0
  20. package/examples/radar-chart.json +148 -0
  21. package/index.html +1 -31
  22. package/package.json +57 -59
  23. package/src/CdcChart.tsx +8 -4
  24. package/src/CdcChartComponent.tsx +398 -284
  25. package/src/_stories/Chart.Anchors.stories.tsx +10 -0
  26. package/src/_stories/Chart.BoxPlot.stories.tsx +7 -0
  27. package/src/_stories/Chart.CI.stories.tsx +13 -0
  28. package/src/_stories/Chart.Combo.stories.tsx +17 -0
  29. package/src/_stories/Chart.CustomColors.stories.tsx +78 -0
  30. package/src/_stories/Chart.Defaults.stories.tsx +95 -0
  31. package/src/_stories/Chart.DynamicSeries.stories.tsx +19 -0
  32. package/src/_stories/Chart.Filters.stories.tsx +4 -0
  33. package/src/_stories/Chart.Forecast.stories.tsx +4 -0
  34. package/src/_stories/Chart.HTMLInDataTable.stories.tsx +22 -0
  35. package/src/_stories/Chart.Legend.Gradient.stories.tsx +28 -0
  36. package/src/_stories/Chart.Patterns.stories.tsx +4 -0
  37. package/src/_stories/Chart.PreserveDecimals.stories.tsx +25 -0
  38. package/src/_stories/Chart.Regions.Categorical.stories.tsx +13 -0
  39. package/src/_stories/Chart.Regions.DateScale.stories.tsx +19 -0
  40. package/src/_stories/Chart.Regions.DateTimeScale.stories.tsx +25 -10
  41. package/src/_stories/Chart.ScatterPlot.stories.tsx +4 -0
  42. package/src/_stories/Chart.SmallMultiples.stories.tsx +16 -0
  43. package/src/_stories/Chart.SmallestLeftAxisMax.stories.tsx +64 -0
  44. package/src/_stories/Chart.stories.tsx +72 -1
  45. package/src/_stories/Chart.tooltip.stories.tsx +7 -0
  46. package/src/_stories/ChartAnnotation.stories.tsx +10 -0
  47. package/src/_stories/ChartAxisLabels.stories.tsx +4 -0
  48. package/src/_stories/ChartAxisTitles.stories.tsx +10 -0
  49. package/src/_stories/ChartBar.Editor.stories.tsx +97 -38
  50. package/src/_stories/ChartBrush.Editor.stories.tsx +11 -25
  51. package/src/_stories/ChartBrush.Matrix.Continuous.stories.tsx +41 -0
  52. package/src/_stories/ChartBrush.Matrix.Date.stories.tsx +114 -0
  53. package/src/_stories/ChartBrush.Matrix.DateTime.stories.tsx +78 -0
  54. package/src/_stories/ChartBrush.stories.tsx +7 -0
  55. package/src/_stories/ChartEditor.Editor.stories.tsx +1 -1
  56. package/src/_stories/ChartEditor.stories.tsx +7 -0
  57. package/src/_stories/ChartLine.QuadrantAngles.stories.tsx +89 -0
  58. package/src/_stories/ChartLine.Suppression.stories.tsx +7 -0
  59. package/src/_stories/ChartLine.Symbols.stories.tsx +4 -0
  60. package/src/_stories/ChartPrefixSuffix.stories.tsx +46 -1
  61. package/src/_stories/TechAdoptionWithLinks.stories.tsx +7 -0
  62. package/src/_stories/_mock/brush_continuous.json +86 -0
  63. package/src/_stories/_mock/brush_date_large.json +176 -0
  64. package/src/_stories/_mock/line_chart_angle_near_zero_fall.json +195 -0
  65. package/src/_stories/_mock/line_chart_angle_near_zero_rise.json +195 -0
  66. package/src/_stories/_mock/line_chart_angle_q1_steep_upward.json +195 -0
  67. package/src/_stories/_mock/line_chart_angle_q2_gentle_downward.json +195 -0
  68. package/src/_stories/_mock/line_chart_angle_q3_steep_downward.json +195 -0
  69. package/src/_stories/_mock/line_chart_angle_q4_gentle_upward.json +195 -0
  70. package/src/_stories/_mock/line_chart_quadrant_angles.json +264 -0
  71. package/src/_stories/_mock/paired-bar-abbr.json +421 -0
  72. package/src/_stories/_mock/pie_custom_colors.json +268 -0
  73. package/src/_stories/_mock/smallest_left_axis_max.json +104 -0
  74. package/src/components/Annotations/components/AnnotationDraggable.styles.css +14 -20
  75. package/src/components/Annotations/components/AnnotationDraggable.tsx +240 -116
  76. package/src/components/Annotations/components/AnnotationDropdown.styles.css +1 -2
  77. package/src/components/Annotations/components/AnnotationDropdown.tsx +8 -12
  78. package/src/components/Annotations/components/AnnotationList.styles.css +12 -18
  79. package/src/components/Annotations/components/AnnotationList.tsx +5 -4
  80. package/src/components/Annotations/components/findNearestDatum.ts +75 -85
  81. package/src/components/Annotations/helpers/getVisibleAnnotations.ts +38 -0
  82. package/src/components/Axis/BottomAxis.tsx +277 -0
  83. package/src/components/Axis/LeftAxis.tsx +404 -0
  84. package/src/components/Axis/LeftAxisGridlines.tsx +77 -0
  85. package/src/components/Axis/PairedBarAxis.tsx +192 -0
  86. package/src/components/Axis/README.md +94 -0
  87. package/src/components/Axis/RightAxis.tsx +108 -0
  88. package/src/components/Axis/axis.constants.ts +21 -0
  89. package/src/components/Axis/index.ts +7 -0
  90. package/src/components/BarChart/components/BarChart.Horizontal.tsx +12 -28
  91. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +12 -30
  92. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +12 -31
  93. package/src/components/BarChart/components/BarChart.Vertical.tsx +12 -28
  94. package/src/components/BarChart/components/BarChart.tsx +7 -1
  95. package/src/components/BarChart/helpers/getPatternUrl.ts +94 -0
  96. package/src/components/BarChart/helpers/tests/getPatternUrl.test.ts +134 -0
  97. package/src/components/BarChart/helpers/useBarChart.ts +3 -0
  98. package/src/components/Brush/BrushSelector.tsx +155 -22
  99. package/src/components/Brush/MiniChartPreview.tsx +133 -21
  100. package/src/components/EditorPanel/EditorPanel.tsx +81 -54
  101. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +67 -29
  102. package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +0 -78
  103. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +120 -2
  104. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +25 -43
  105. package/src/components/EditorPanel/components/Panels/Panel.Radar.tsx +353 -0
  106. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +83 -3
  107. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +66 -43
  108. package/src/components/EditorPanel/components/Panels/index.tsx +2 -0
  109. package/src/components/EditorPanel/editor-panel.scss +1 -1
  110. package/src/components/EditorPanel/useEditorPermissions.ts +55 -26
  111. package/src/components/ForestPlot/ForestPlot.tsx +26 -22
  112. package/src/components/HorizonChart/HorizonChart.tsx +131 -0
  113. package/src/components/HorizonChart/components/HorizonBand.tsx +160 -0
  114. package/src/components/HorizonChart/helpers/calculateHorizonBands.ts +27 -0
  115. package/src/components/HorizonChart/helpers/getHorizonLayerColors.ts +40 -0
  116. package/src/components/HorizonChart/index.tsx +3 -0
  117. package/src/components/Legend/Legend.Component.tsx +52 -4
  118. package/src/components/Legend/Legend.tsx +1 -1
  119. package/src/components/Legend/LegendGroup/LegendGroup.styles.css +4 -4
  120. package/src/components/Legend/LegendValueRange.tsx +77 -0
  121. package/src/components/Legend/helpers/createFormatLabels.tsx +16 -2
  122. package/src/components/Legend/helpers/generateValueRanges.ts +92 -0
  123. package/src/components/LineChart/helpers/README.md +292 -0
  124. package/src/components/LineChart/helpers/labelPositioning.test.ts +245 -0
  125. package/src/components/LineChart/helpers/labelPositioning.ts +304 -0
  126. package/src/components/LineChart/index.tsx +44 -8
  127. package/src/components/LinearChart/README.md +109 -0
  128. package/src/components/LinearChart/VisualizationRenderer.tsx +267 -0
  129. package/src/components/LinearChart/linearChart.constants.ts +84 -0
  130. package/src/components/LinearChart/tests/LinearChart.test.tsx +278 -0
  131. package/src/components/LinearChart/tests/mockConfigContext.ts +131 -0
  132. package/src/components/LinearChart/utils/tickFormatting.ts +146 -0
  133. package/src/components/LinearChart.tsx +268 -1057
  134. package/src/components/PieChart/PieChart.tsx +20 -5
  135. package/src/components/RadarChart/RadarAxis.tsx +78 -0
  136. package/src/components/RadarChart/RadarChart.tsx +298 -0
  137. package/src/components/RadarChart/RadarGrid.tsx +64 -0
  138. package/src/components/RadarChart/RadarPolygon.tsx +91 -0
  139. package/src/components/RadarChart/helpers.ts +83 -0
  140. package/src/components/RadarChart/index.tsx +3 -0
  141. package/src/components/Regions/components/Regions.tsx +6 -6
  142. package/src/components/Sankey/components/Sankey.tsx +3 -3
  143. package/src/components/Sankey/sankey.scss +1 -1
  144. package/src/components/SmallMultiples/SmallMultiples.css +5 -5
  145. package/src/components/Sparkline/index.scss +4 -2
  146. package/src/components/WarmingStripes/WarmingStripes.tsx +95 -25
  147. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +8 -8
  148. package/src/data/initial-state.js +37 -15
  149. package/src/data/legacy-defaults.ts +18 -0
  150. package/src/helpers/abbreviateNumber.ts +24 -17
  151. package/src/helpers/getChartPatternId.ts +17 -0
  152. package/src/helpers/getExcludedData.ts +4 -0
  153. package/src/helpers/getMinMax.ts +16 -2
  154. package/src/helpers/handleChartAriaLabels.ts +19 -19
  155. package/src/helpers/handleLineType.ts +22 -18
  156. package/src/helpers/seriesColumnSettings.ts +114 -0
  157. package/src/helpers/tests/countNumOfTicks.test.ts +77 -0
  158. package/src/helpers/tests/seriesColumnSettings.test.ts +84 -0
  159. package/src/hooks/useProgrammaticTooltip.ts +23 -2
  160. package/src/hooks/useRightAxis.ts +14 -0
  161. package/src/hooks/useScales.ts +99 -56
  162. package/src/hooks/useTooltip.tsx +23 -3
  163. package/src/scss/main.scss +157 -79
  164. package/src/selectors/README.md +68 -0
  165. package/src/store/chart.reducer.ts +2 -0
  166. package/src/test/CdcChart.test.jsx +2 -2
  167. package/src/types/ChartConfig.ts +22 -0
  168. package/src/types/ChartContext.ts +1 -0
  169. package/src/types/Horizon.ts +64 -0
  170. package/tests/fixtures/chart-config-with-metadata.json +29 -0
  171. package/tests/fixtures/data-with-metadata.json +10 -0
  172. package/preview.html +0 -1616
  173. package/src/components/Annotations/components/helpers/index.tsx +0 -46
@@ -0,0 +1,278 @@
1
+ import React from 'react'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { describe, expect, it, vi, beforeAll } from 'vitest'
4
+ import LinearChart from '../../LinearChart'
5
+ import ConfigContext from '../../../ConfigContext'
6
+ import { createMockChartContext } from './mockConfigContext'
7
+ import forestPlotConfig from '../../../../examples/feature/forest-plot/forest-plot.json'
8
+
9
+ // Mock ResizeObserver
10
+ vi.stubGlobal(
11
+ 'ResizeObserver',
12
+ vi.fn(() => ({
13
+ observe: vi.fn(),
14
+ unobserve: vi.fn(),
15
+ disconnect: vi.fn()
16
+ }))
17
+ )
18
+
19
+ // Mock IntersectionObserver
20
+ vi.stubGlobal(
21
+ 'IntersectionObserver',
22
+ vi.fn(() => ({
23
+ observe: vi.fn(),
24
+ unobserve: vi.fn(),
25
+ disconnect: vi.fn()
26
+ }))
27
+ )
28
+
29
+ // Mock canvas for text measurement
30
+ beforeAll(() => {
31
+ HTMLCanvasElement.prototype.getContext = vi.fn(() => ({
32
+ measureText: vi.fn(() => ({ width: 50 })),
33
+ fillText: vi.fn(),
34
+ fillRect: vi.fn(),
35
+ clearRect: vi.fn()
36
+ })) as any
37
+
38
+ // Mock SVG getBBox for axis measurements
39
+ const mockBBox = { x: 0, y: 0, width: 100, height: 50 }
40
+ // @ts-expect-error - mocking SVG method
41
+ SVGElement.prototype.getBBox = vi.fn(() => mockBBox)
42
+ // @ts-expect-error - mocking SVG method
43
+ SVGElement.prototype.getBoundingClientRect = vi.fn(() => ({
44
+ x: 0,
45
+ y: 0,
46
+ width: 100,
47
+ height: 50,
48
+ top: 0,
49
+ left: 0,
50
+ right: 100,
51
+ bottom: 50
52
+ }))
53
+ })
54
+
55
+ // Helper to render LinearChart with context
56
+ const renderLinearChart = (
57
+ configOverrides = {},
58
+ contextOverrides = {},
59
+ props = { parentWidth: 800, parentHeight: 400 }
60
+ ) => {
61
+ const context = createMockChartContext(configOverrides, contextOverrides)
62
+
63
+ return render(
64
+ <ConfigContext.Provider value={context}>
65
+ <LinearChart {...props} />
66
+ </ConfigContext.Provider>
67
+ )
68
+ }
69
+
70
+ describe('LinearChart', () => {
71
+ describe('rendering', () => {
72
+ it('renders without crashing', () => {
73
+ const { container } = renderLinearChart()
74
+ expect(container).toBeTruthy()
75
+ })
76
+
77
+ it('renders an SVG element', () => {
78
+ const { container } = renderLinearChart()
79
+ const svg = container.querySelector('svg')
80
+ expect(svg).toBeTruthy()
81
+ })
82
+
83
+ it('renders with correct aria-label', () => {
84
+ const { container } = renderLinearChart()
85
+ const svg = container.querySelector('svg')
86
+ expect(svg?.getAttribute('aria-label')).toBe('Chart')
87
+ })
88
+
89
+ it('applies animated class when config.animate is true', () => {
90
+ const { container } = renderLinearChart({ animate: true })
91
+ const svg = container.querySelector('svg')
92
+ expect(svg?.classList.contains('animated')).toBe(true)
93
+ })
94
+
95
+ it('does not apply animated class when config.animate is false', () => {
96
+ const { container } = renderLinearChart({ animate: false })
97
+ const svg = container.querySelector('svg')
98
+ expect(svg?.classList.contains('animated')).toBe(false)
99
+ })
100
+ })
101
+
102
+ describe('empty data handling', () => {
103
+ it('renders no data message when filters result in empty data', () => {
104
+ const context = createMockChartContext(
105
+ { filters: [{ columnName: 'test', active: 'test' }] },
106
+ { transformedData: [] }
107
+ )
108
+
109
+ render(
110
+ <ConfigContext.Provider value={context}>
111
+ <LinearChart parentWidth={800} parentHeight={400} />
112
+ </ConfigContext.Provider>
113
+ )
114
+
115
+ expect(screen.getByText('No data available')).toBeTruthy()
116
+ })
117
+ })
118
+
119
+ describe('visualization types', () => {
120
+ it('renders Line chart type without crashing', () => {
121
+ const { container } = renderLinearChart({ visualizationType: 'Line' })
122
+ expect(container.querySelector('svg')).toBeTruthy()
123
+ })
124
+
125
+ it('handles Bar chart type without uncaught exceptions', () => {
126
+ // Bar charts require additional data/series setup - verify it renders without throwing
127
+ const { container } = renderLinearChart({
128
+ visualizationType: 'Bar',
129
+ orientation: 'vertical'
130
+ })
131
+ // ErrorBoundary will catch errors, so container should exist
132
+ expect(container).toBeTruthy()
133
+ })
134
+
135
+ it('handles horizontal Bar chart without uncaught exceptions', () => {
136
+ const { container } = renderLinearChart({
137
+ visualizationType: 'Bar',
138
+ orientation: 'horizontal'
139
+ })
140
+ expect(container).toBeTruthy()
141
+ })
142
+
143
+ it('handles Area Chart type without uncaught exceptions', () => {
144
+ // Area charts require stacked data setup - verify it renders without throwing
145
+ const { container } = renderLinearChart({
146
+ visualizationType: 'Area Chart',
147
+ visualizationSubType: 'stacked'
148
+ })
149
+ expect(container).toBeTruthy()
150
+ })
151
+
152
+ it('keeps forest plot lines inside the computed plot bounds at narrow and wide widths', () => {
153
+ const forestContextOverrides = {
154
+ transformedData: forestPlotConfig.data,
155
+ rawData: forestPlotConfig.data
156
+ }
157
+
158
+ const narrowRender = renderLinearChart(forestPlotConfig as any, forestContextOverrides, {
159
+ parentWidth: 320,
160
+ parentHeight: 500
161
+ })
162
+ const wideRender = renderLinearChart(forestPlotConfig as any, forestContextOverrides, {
163
+ parentWidth: 960,
164
+ parentHeight: 500
165
+ })
166
+
167
+ const narrowTopLine = narrowRender.container.querySelector('.forestplot__top-line')
168
+ const wideTopLine = wideRender.container.querySelector('.forestplot__top-line')
169
+ const narrowCiLine = narrowRender.container.querySelector('line[class^="line-"]')
170
+ const wideCiLine = wideRender.container.querySelector('line[class^="line-"]')
171
+
172
+ expect(narrowTopLine).toBeTruthy()
173
+ expect(wideTopLine).toBeTruthy()
174
+ expect(narrowCiLine).toBeTruthy()
175
+ expect(wideCiLine).toBeTruthy()
176
+
177
+ const narrowStart = Number(narrowTopLine?.getAttribute('x1'))
178
+ const narrowEnd = Number(narrowTopLine?.getAttribute('x2'))
179
+ const wideStart = Number(wideTopLine?.getAttribute('x1'))
180
+ const wideEnd = Number(wideTopLine?.getAttribute('x2'))
181
+
182
+ expect(narrowStart).toBe(0)
183
+ expect(narrowEnd).toBeLessThanOrEqual(320)
184
+ expect(wideStart).toBe(0)
185
+ expect(wideEnd).toBeLessThanOrEqual(960)
186
+ expect(wideEnd - wideStart).toBeGreaterThan(narrowEnd - narrowStart)
187
+
188
+ expect(Number(narrowCiLine?.getAttribute('x1'))).toBeGreaterThan(narrowStart)
189
+ expect(Number(narrowCiLine?.getAttribute('x2'))).toBeLessThanOrEqual(narrowEnd)
190
+ expect(Number(wideCiLine?.getAttribute('x1'))).toBeGreaterThan(wideStart)
191
+ expect(Number(wideCiLine?.getAttribute('x2'))).toBeLessThanOrEqual(wideEnd)
192
+ })
193
+
194
+ it('avoids rendering a duplicate manual bottom border when the forest plot x-axis is visible', () => {
195
+ const { container } = renderLinearChart(
196
+ forestPlotConfig as any,
197
+ {
198
+ transformedData: forestPlotConfig.data,
199
+ rawData: forestPlotConfig.data
200
+ },
201
+ { parentWidth: 800, parentHeight: 500 }
202
+ )
203
+
204
+ expect(container.querySelector('.forestplot__top-line')).toBeTruthy()
205
+ expect(container.querySelector('.forestplot__bottom-line')).toBeFalsy()
206
+ const bottomAxisLine = container.querySelector('.bottom-axis > line[stroke="#333"]')
207
+ expect(bottomAxisLine?.getAttribute('x1')).toBe('0')
208
+ })
209
+
210
+ it('renders forest plot rows from transformedData instead of rawData', () => {
211
+ const filteredData = forestPlotConfig.data.slice(0, 2)
212
+ const { container } = renderLinearChart(
213
+ forestPlotConfig as any,
214
+ {
215
+ transformedData: filteredData,
216
+ rawData: forestPlotConfig.data
217
+ },
218
+ { parentWidth: 800, parentHeight: 500 }
219
+ )
220
+
221
+ expect(container.querySelectorAll('.lower-ci')).toHaveLength(filteredData.length)
222
+ expect(container.querySelectorAll('line[class^="line-"]')).toHaveLength(filteredData.length)
223
+ expect(container.textContent).not.toContain(
224
+ String(forestPlotConfig.data[forestPlotConfig.data.length - 1]['Author(s) and Year'])
225
+ )
226
+ })
227
+ })
228
+
229
+ describe('axis rendering', () => {
230
+ it('renders left axis group', () => {
231
+ const { container } = renderLinearChart()
232
+ const leftAxis = container.querySelector('.left-axis')
233
+ expect(leftAxis).toBeTruthy()
234
+ })
235
+
236
+ it('renders bottom axis group', () => {
237
+ const { container } = renderLinearChart()
238
+ const bottomAxis = container.querySelector('.bottom-axis')
239
+ expect(bottomAxis).toBeTruthy()
240
+ })
241
+
242
+ it('hides Y axis when hideAxis is true', () => {
243
+ const { container } = renderLinearChart({
244
+ yAxis: {
245
+ hideAxis: true,
246
+ hideLabel: false,
247
+ hideTicks: false,
248
+ size: '50',
249
+ gridLines: true,
250
+ label: 'Y-Axis',
251
+ tickRotation: 0,
252
+ anchors: [],
253
+ axisPadding: 0,
254
+ labelPlacement: 'On Date/Category Axis',
255
+ rightAxisSize: 0
256
+ }
257
+ })
258
+ // The axis line should be hidden, but grid lines may still render
259
+ expect(container.querySelector('svg')).toBeTruthy()
260
+ })
261
+ })
262
+
263
+ describe('SVG dimensions', () => {
264
+ it('sets correct width based on parentWidth prop', () => {
265
+ const { container } = renderLinearChart({}, {}, { parentWidth: 600, parentHeight: 400 })
266
+ const svg = container.querySelector('svg')
267
+ // Width should include rightAxisSize (default 0)
268
+ expect(svg?.getAttribute('width')).toBe('600')
269
+ })
270
+
271
+ it('returns empty fragment when parentWidth is NaN', () => {
272
+ const { container } = renderLinearChart({}, {}, { parentWidth: NaN, parentHeight: 400 })
273
+ // Should render an empty React.Fragment
274
+ const svg = container.querySelector('svg')
275
+ expect(svg).toBeFalsy()
276
+ })
277
+ })
278
+ })
@@ -0,0 +1,131 @@
1
+ import { ChartContext } from '../../../types/ChartContext'
2
+ import { ChartConfig } from '../../../types/ChartConfig'
3
+
4
+ // Minimal config for testing LinearChart
5
+ export const createMockConfig = (overrides: Partial<ChartConfig> = {}): ChartConfig =>
6
+ ({
7
+ type: 'chart',
8
+ visualizationType: 'Line',
9
+ visualizationSubType: 'regular',
10
+ orientation: 'vertical',
11
+ animate: false,
12
+ heights: {
13
+ vertical: 300,
14
+ horizontal: 300,
15
+ mobileVertical: 200
16
+ },
17
+ xAxis: {
18
+ type: 'date',
19
+ dataKey: 'Date',
20
+ label: 'X-Axis',
21
+ hideAxis: false,
22
+ hideLabel: false,
23
+ hideTicks: false,
24
+ size: '50',
25
+ tickRotation: 0,
26
+ maxTickRotation: 90,
27
+ anchors: [],
28
+ axisPadding: 0
29
+ },
30
+ yAxis: {
31
+ hideAxis: false,
32
+ hideLabel: false,
33
+ hideTicks: false,
34
+ size: '50',
35
+ gridLines: true,
36
+ label: 'Y-Axis',
37
+ tickRotation: 0,
38
+ anchors: [],
39
+ axisPadding: 0,
40
+ labelPlacement: 'On Date/Category Axis',
41
+ rightAxisSize: 0
42
+ },
43
+ runtime: {
44
+ xAxis: {
45
+ type: 'date',
46
+ dataKey: 'Date',
47
+ label: 'X-Axis'
48
+ },
49
+ yAxis: {
50
+ size: 50,
51
+ label: 'Y-Axis',
52
+ gridLines: true
53
+ },
54
+ originalXAxis: {
55
+ dataKey: 'Date'
56
+ },
57
+ series: [],
58
+ seriesKeys: [],
59
+ seriesLabelsAll: [],
60
+ uniqueId: 'test-chart'
61
+ },
62
+ series: [],
63
+ data: [],
64
+ dataFormat: {
65
+ abbreviated: false,
66
+ roundTo: 0
67
+ },
68
+ legend: {
69
+ position: 'bottom'
70
+ },
71
+ tooltips: {
72
+ opacity: 90,
73
+ singleSeries: false
74
+ },
75
+ chartMessage: {
76
+ noData: 'No data available'
77
+ },
78
+ barThickness: 0.8,
79
+ barHeight: 25,
80
+ barSpace: 15,
81
+ isResponsiveTicks: true,
82
+ debugSvg: false,
83
+ filters: [],
84
+ topAxis: {
85
+ hasLine: false
86
+ },
87
+ hideXAxisLabel: false,
88
+ hideYAxisLabel: false,
89
+ ...overrides
90
+ } as ChartConfig)
91
+
92
+ // Minimal chart context for testing
93
+ export const createMockChartContext = (
94
+ configOverrides: Partial<ChartConfig> = {},
95
+ contextOverrides: Partial<ChartContext> = {}
96
+ ): ChartContext => {
97
+ const config = createMockConfig(configOverrides)
98
+
99
+ return {
100
+ config,
101
+ colorScale: undefined,
102
+ convertLineToBarGraph: false,
103
+ currentViewport: 'lg',
104
+ vizViewport: 'lg',
105
+ dimensions: [800, 400],
106
+ formatDate: (date: any) => String(date),
107
+ formatNumber: (num: any) => String(num),
108
+ handleChartAriaLabels: () => 'Chart',
109
+ handleLineType: () => '',
110
+ handleDragStateChange: () => {},
111
+ interactionLabel: '',
112
+ isEditor: false,
113
+ isDraggingAnnotation: false,
114
+ legendRef: { current: null },
115
+ parentRef: { current: null },
116
+ parseDate: (date: any) => new Date(date),
117
+ seriesHighlight: [],
118
+ tableData: [],
119
+ transformedData: [],
120
+ annotations: [],
121
+ colorPalettes: {},
122
+ twoColorPalette: {},
123
+ capitalize: (s: string) => s,
124
+ clean: (s: any) => s,
125
+ formatTooltipsDate: (date: any) => String(date),
126
+ legendId: 'test-legend',
127
+ rawData: config.data,
128
+ updateConfig: () => {},
129
+ ...contextOverrides
130
+ } as ChartContext
131
+ }
@@ -0,0 +1,146 @@
1
+ import { useCallback, useContext } from 'react'
2
+ import ConfigContext from '../../../ConfigContext'
3
+ import { isDateScale } from '@cdc/core/helpers/cove/date'
4
+
5
+ type TickFormattingOptions = {
6
+ isLogarithmicAxis: boolean
7
+ orientation: 'horizontal' | 'vertical'
8
+ visualizationType: string
9
+ min: number
10
+ max: number
11
+ shouldAbbreviate: boolean
12
+ }
13
+
14
+ /**
15
+ * Shared logarithmic tick handling
16
+ * Both left and bottom formatters convert 0.1 to 0 for logarithmic scales
17
+ */
18
+ const handleLogarithmicTick = (tick: number, isLogarithmic: boolean): number => {
19
+ if (isLogarithmic && tick === 0.1) {
20
+ return 0
21
+ }
22
+ return tick
23
+ }
24
+
25
+ /**
26
+ * Hook that provides consolidated tick formatting functions for both axes.
27
+ * Consolidates handleLeftTickFormatting and handleBottomTickFormatting
28
+ * from LinearChart.tsx
29
+ */
30
+ export const useTickFormatters = (options: TickFormattingOptions) => {
31
+ const { config, formatDate, formatNumber, parseDate } = useContext(ConfigContext)
32
+ const { runtime, data, xAxis } = config
33
+ const { isLogarithmicAxis, orientation, visualizationType, min, max, shouldAbbreviate } = options
34
+
35
+ /**
36
+ * Format ticks for the left (Y) axis
37
+ */
38
+ const handleLeftTickFormatting = useCallback(
39
+ (tick: number | string, index: number, ticks: any[]) => {
40
+ // Handle logarithmic scale
41
+ let processedTick = typeof tick === 'number' ? handleLogarithmicTick(tick, isLogarithmicAxis) : tick
42
+
43
+ // Forest Plot special case - return data key value
44
+ if (visualizationType === 'Forest Plot') {
45
+ if (data && !data[index]) return ''
46
+ return data[index][xAxis.dataKey]
47
+ }
48
+
49
+ // Date scale on y-axis
50
+ if (isDateScale(runtime.yAxis)) {
51
+ return formatDate(parseDate(processedTick))
52
+ }
53
+
54
+ // Vertical orientation with small range (needs more precision)
55
+ if (orientation === 'vertical' && max - min < 3 && !config.dataFormat?.roundTo) {
56
+ return formatNumber(processedTick, 'left', shouldAbbreviate, false, false, '1', {
57
+ index,
58
+ length: ticks.length
59
+ })
60
+ }
61
+
62
+ // Standard vertical orientation formatting
63
+ if (orientation === 'vertical') {
64
+ return formatNumber(processedTick, 'left', shouldAbbreviate, false, false, undefined, {
65
+ index,
66
+ length: ticks.length
67
+ })
68
+ }
69
+
70
+ return processedTick
71
+ },
72
+ [
73
+ isLogarithmicAxis,
74
+ visualizationType,
75
+ data,
76
+ xAxis.dataKey,
77
+ runtime.yAxis,
78
+ orientation,
79
+ min,
80
+ max,
81
+ config.dataFormat?.roundTo,
82
+ shouldAbbreviate,
83
+ formatDate,
84
+ formatNumber,
85
+ parseDate
86
+ ]
87
+ )
88
+
89
+ /**
90
+ * Format ticks for the bottom (X) axis
91
+ */
92
+ const handleBottomTickFormatting = useCallback(
93
+ (tick: number | string | Date, index: number, ticks: any[]) => {
94
+ // Handle logarithmic scale
95
+ let processedTick = typeof tick === 'number' ? handleLogarithmicTick(tick, isLogarithmicAxis) : tick
96
+
97
+ // Date scale formatting (most common case for x-axis)
98
+ if (isDateScale(runtime.xAxis) && visualizationType !== 'Forest Plot') {
99
+ return formatDate(processedTick, index, ticks)
100
+ }
101
+
102
+ // Horizontal orientation (bars)
103
+ if (orientation === 'horizontal' && visualizationType !== 'Forest Plot') {
104
+ return formatNumber(processedTick, 'left', shouldAbbreviate)
105
+ }
106
+
107
+ // Continuous x-axis type
108
+ if (xAxis.type === 'continuous' && visualizationType !== 'Forest Plot') {
109
+ return formatNumber(processedTick, 'bottom', shouldAbbreviate)
110
+ }
111
+
112
+ // Forest Plot special case with prefix/suffix
113
+ if (visualizationType === 'Forest Plot') {
114
+ return formatNumber(
115
+ processedTick,
116
+ 'left',
117
+ config.dataFormat.abbreviated,
118
+ runtime.xAxis.prefix,
119
+ runtime.xAxis.suffix,
120
+ Number(config.dataFormat.roundTo)
121
+ )
122
+ }
123
+
124
+ return processedTick
125
+ },
126
+ [
127
+ isLogarithmicAxis,
128
+ runtime.xAxis,
129
+ visualizationType,
130
+ orientation,
131
+ xAxis.type,
132
+ shouldAbbreviate,
133
+ config.dataFormat.abbreviated,
134
+ config.dataFormat.roundTo,
135
+ formatDate,
136
+ formatNumber
137
+ ]
138
+ )
139
+
140
+ return {
141
+ handleLeftTickFormatting,
142
+ handleBottomTickFormatting
143
+ }
144
+ }
145
+
146
+ export default useTickFormatters