@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.
- package/CLAUDE.local.md +79 -0
- package/LICENSE +201 -0
- package/dist/{cdcchart-dgT_1dIT.es.js → cdcchart-DQ00cQCm.es.js} +1 -20
- package/dist/cdcchart.js +54742 -49796
- package/examples/data/data-with-metadata.json +10 -0
- package/examples/default.json +378 -0
- package/examples/feature/__data__/horizon-chart-data.json +373 -0
- package/examples/feature/annotations/index.json +3 -6
- package/examples/feature/horizon/horizon-chart.json +395 -0
- package/examples/feature/pie/planet-pie-example-config.json +2 -1
- package/examples/line-chart-states.json +1085 -0
- package/examples/metadata-variables.json +58 -0
- package/examples/private/123.json +694 -0
- package/examples/private/anchor-issue.json +4094 -0
- package/examples/private/backwards-slider.json +10430 -0
- package/examples/private/georgia.csv +160 -0
- package/examples/private/timeline-data.json +1 -0
- package/examples/private/timeline.json +389 -0
- package/examples/radar-chart-simple.json +133 -0
- package/examples/radar-chart.json +148 -0
- package/index.html +1 -31
- package/package.json +57 -59
- package/src/CdcChart.tsx +8 -4
- package/src/CdcChartComponent.tsx +398 -284
- package/src/_stories/Chart.Anchors.stories.tsx +10 -0
- package/src/_stories/Chart.BoxPlot.stories.tsx +7 -0
- package/src/_stories/Chart.CI.stories.tsx +13 -0
- package/src/_stories/Chart.Combo.stories.tsx +17 -0
- package/src/_stories/Chart.CustomColors.stories.tsx +78 -0
- package/src/_stories/Chart.Defaults.stories.tsx +95 -0
- package/src/_stories/Chart.DynamicSeries.stories.tsx +19 -0
- package/src/_stories/Chart.Filters.stories.tsx +4 -0
- package/src/_stories/Chart.Forecast.stories.tsx +4 -0
- package/src/_stories/Chart.HTMLInDataTable.stories.tsx +22 -0
- package/src/_stories/Chart.Legend.Gradient.stories.tsx +28 -0
- package/src/_stories/Chart.Patterns.stories.tsx +4 -0
- package/src/_stories/Chart.PreserveDecimals.stories.tsx +25 -0
- package/src/_stories/Chart.Regions.Categorical.stories.tsx +13 -0
- package/src/_stories/Chart.Regions.DateScale.stories.tsx +19 -0
- package/src/_stories/Chart.Regions.DateTimeScale.stories.tsx +25 -10
- package/src/_stories/Chart.ScatterPlot.stories.tsx +4 -0
- package/src/_stories/Chart.SmallMultiples.stories.tsx +16 -0
- package/src/_stories/Chart.SmallestLeftAxisMax.stories.tsx +64 -0
- package/src/_stories/Chart.stories.tsx +72 -1
- package/src/_stories/Chart.tooltip.stories.tsx +7 -0
- package/src/_stories/ChartAnnotation.stories.tsx +10 -0
- package/src/_stories/ChartAxisLabels.stories.tsx +4 -0
- package/src/_stories/ChartAxisTitles.stories.tsx +10 -0
- package/src/_stories/ChartBar.Editor.stories.tsx +97 -38
- package/src/_stories/ChartBrush.Editor.stories.tsx +11 -25
- package/src/_stories/ChartBrush.Matrix.Continuous.stories.tsx +41 -0
- package/src/_stories/ChartBrush.Matrix.Date.stories.tsx +114 -0
- package/src/_stories/ChartBrush.Matrix.DateTime.stories.tsx +78 -0
- package/src/_stories/ChartBrush.stories.tsx +7 -0
- package/src/_stories/ChartEditor.Editor.stories.tsx +1 -1
- package/src/_stories/ChartEditor.stories.tsx +7 -0
- package/src/_stories/ChartLine.QuadrantAngles.stories.tsx +89 -0
- package/src/_stories/ChartLine.Suppression.stories.tsx +7 -0
- package/src/_stories/ChartLine.Symbols.stories.tsx +4 -0
- package/src/_stories/ChartPrefixSuffix.stories.tsx +46 -1
- package/src/_stories/TechAdoptionWithLinks.stories.tsx +7 -0
- package/src/_stories/_mock/brush_continuous.json +86 -0
- package/src/_stories/_mock/brush_date_large.json +176 -0
- package/src/_stories/_mock/line_chart_angle_near_zero_fall.json +195 -0
- package/src/_stories/_mock/line_chart_angle_near_zero_rise.json +195 -0
- package/src/_stories/_mock/line_chart_angle_q1_steep_upward.json +195 -0
- package/src/_stories/_mock/line_chart_angle_q2_gentle_downward.json +195 -0
- package/src/_stories/_mock/line_chart_angle_q3_steep_downward.json +195 -0
- package/src/_stories/_mock/line_chart_angle_q4_gentle_upward.json +195 -0
- package/src/_stories/_mock/line_chart_quadrant_angles.json +264 -0
- package/src/_stories/_mock/paired-bar-abbr.json +421 -0
- package/src/_stories/_mock/pie_custom_colors.json +268 -0
- package/src/_stories/_mock/smallest_left_axis_max.json +104 -0
- package/src/components/Annotations/components/AnnotationDraggable.styles.css +14 -20
- package/src/components/Annotations/components/AnnotationDraggable.tsx +240 -116
- package/src/components/Annotations/components/AnnotationDropdown.styles.css +1 -2
- package/src/components/Annotations/components/AnnotationDropdown.tsx +8 -12
- package/src/components/Annotations/components/AnnotationList.styles.css +12 -18
- package/src/components/Annotations/components/AnnotationList.tsx +5 -4
- package/src/components/Annotations/components/findNearestDatum.ts +75 -85
- package/src/components/Annotations/helpers/getVisibleAnnotations.ts +38 -0
- package/src/components/Axis/BottomAxis.tsx +277 -0
- package/src/components/Axis/LeftAxis.tsx +404 -0
- package/src/components/Axis/LeftAxisGridlines.tsx +77 -0
- package/src/components/Axis/PairedBarAxis.tsx +192 -0
- package/src/components/Axis/README.md +94 -0
- package/src/components/Axis/RightAxis.tsx +108 -0
- package/src/components/Axis/axis.constants.ts +21 -0
- package/src/components/Axis/index.ts +7 -0
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +12 -28
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +12 -30
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +12 -31
- package/src/components/BarChart/components/BarChart.Vertical.tsx +12 -28
- package/src/components/BarChart/components/BarChart.tsx +7 -1
- package/src/components/BarChart/helpers/getPatternUrl.ts +94 -0
- package/src/components/BarChart/helpers/tests/getPatternUrl.test.ts +134 -0
- package/src/components/BarChart/helpers/useBarChart.ts +3 -0
- package/src/components/Brush/BrushSelector.tsx +155 -22
- package/src/components/Brush/MiniChartPreview.tsx +133 -21
- package/src/components/EditorPanel/EditorPanel.tsx +81 -54
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +67 -29
- package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +0 -78
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +120 -2
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +25 -43
- package/src/components/EditorPanel/components/Panels/Panel.Radar.tsx +353 -0
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +83 -3
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +66 -43
- package/src/components/EditorPanel/components/Panels/index.tsx +2 -0
- package/src/components/EditorPanel/editor-panel.scss +1 -1
- package/src/components/EditorPanel/useEditorPermissions.ts +55 -26
- package/src/components/ForestPlot/ForestPlot.tsx +26 -22
- package/src/components/HorizonChart/HorizonChart.tsx +131 -0
- package/src/components/HorizonChart/components/HorizonBand.tsx +160 -0
- package/src/components/HorizonChart/helpers/calculateHorizonBands.ts +27 -0
- package/src/components/HorizonChart/helpers/getHorizonLayerColors.ts +40 -0
- package/src/components/HorizonChart/index.tsx +3 -0
- package/src/components/Legend/Legend.Component.tsx +52 -4
- package/src/components/Legend/Legend.tsx +1 -1
- package/src/components/Legend/LegendGroup/LegendGroup.styles.css +4 -4
- package/src/components/Legend/LegendValueRange.tsx +77 -0
- package/src/components/Legend/helpers/createFormatLabels.tsx +16 -2
- package/src/components/Legend/helpers/generateValueRanges.ts +92 -0
- package/src/components/LineChart/helpers/README.md +292 -0
- package/src/components/LineChart/helpers/labelPositioning.test.ts +245 -0
- package/src/components/LineChart/helpers/labelPositioning.ts +304 -0
- package/src/components/LineChart/index.tsx +44 -8
- package/src/components/LinearChart/README.md +109 -0
- package/src/components/LinearChart/VisualizationRenderer.tsx +267 -0
- package/src/components/LinearChart/linearChart.constants.ts +84 -0
- package/src/components/LinearChart/tests/LinearChart.test.tsx +278 -0
- package/src/components/LinearChart/tests/mockConfigContext.ts +131 -0
- package/src/components/LinearChart/utils/tickFormatting.ts +146 -0
- package/src/components/LinearChart.tsx +268 -1057
- package/src/components/PieChart/PieChart.tsx +20 -5
- package/src/components/RadarChart/RadarAxis.tsx +78 -0
- package/src/components/RadarChart/RadarChart.tsx +298 -0
- package/src/components/RadarChart/RadarGrid.tsx +64 -0
- package/src/components/RadarChart/RadarPolygon.tsx +91 -0
- package/src/components/RadarChart/helpers.ts +83 -0
- package/src/components/RadarChart/index.tsx +3 -0
- package/src/components/Regions/components/Regions.tsx +6 -6
- package/src/components/Sankey/components/Sankey.tsx +3 -3
- package/src/components/Sankey/sankey.scss +1 -1
- package/src/components/SmallMultiples/SmallMultiples.css +5 -5
- package/src/components/Sparkline/index.scss +4 -2
- package/src/components/WarmingStripes/WarmingStripes.tsx +95 -25
- package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +8 -8
- package/src/data/initial-state.js +37 -15
- package/src/data/legacy-defaults.ts +18 -0
- package/src/helpers/abbreviateNumber.ts +24 -17
- package/src/helpers/getChartPatternId.ts +17 -0
- package/src/helpers/getExcludedData.ts +4 -0
- package/src/helpers/getMinMax.ts +16 -2
- package/src/helpers/handleChartAriaLabels.ts +19 -19
- package/src/helpers/handleLineType.ts +22 -18
- package/src/helpers/seriesColumnSettings.ts +114 -0
- package/src/helpers/tests/countNumOfTicks.test.ts +77 -0
- package/src/helpers/tests/seriesColumnSettings.test.ts +84 -0
- package/src/hooks/useProgrammaticTooltip.ts +23 -2
- package/src/hooks/useRightAxis.ts +14 -0
- package/src/hooks/useScales.ts +99 -56
- package/src/hooks/useTooltip.tsx +23 -3
- package/src/scss/main.scss +157 -79
- package/src/selectors/README.md +68 -0
- package/src/store/chart.reducer.ts +2 -0
- package/src/test/CdcChart.test.jsx +2 -2
- package/src/types/ChartConfig.ts +22 -0
- package/src/types/ChartContext.ts +1 -0
- package/src/types/Horizon.ts +64 -0
- package/tests/fixtures/chart-config-with-metadata.json +29 -0
- package/tests/fixtures/data-with-metadata.json +10 -0
- package/preview.html +0 -1616
- 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
|