@cdc/chart 4.26.4 → 4.26.6

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 (141) hide show
  1. package/CONFIG.md +353 -29
  2. package/LICENSE +201 -0
  3. package/dist/{cdcchart-DQ00cQCm.es.js → cdcchart-BWVIUcAF.es.js} +5 -55
  4. package/dist/cdcchart.js +37885 -35117
  5. package/examples/feature/heatmap/average-age-categorical.json +313 -0
  6. package/examples/feature/heatmap/calendar-heatmap.json +134 -0
  7. package/examples/feature/heatmap/cell-values-bottom-axis.json +181 -0
  8. package/examples/feature/heatmap/sparse-aggregation.json +158 -0
  9. package/examples/private/bad-y-axis.json +543 -0
  10. package/examples/private/bar-chart-negative-value.json +455 -0
  11. package/examples/private/bump-chart.json +1116 -0
  12. package/examples/private/test-123.json +624 -0
  13. package/package.json +4 -3
  14. package/src/CdcChart.tsx +5 -4
  15. package/src/CdcChartComponent.tsx +166 -62
  16. package/src/_stories/Chart.AltText.stories.tsx +136 -0
  17. package/src/_stories/Chart.HeatMap.stories.tsx +187 -0
  18. package/src/_stories/Chart.Sankey.smoke.stories.tsx +62 -0
  19. package/src/_stories/Chart.SeriesColorAssignments.stories.tsx +183 -0
  20. package/src/_stories/Chart.SmallMultiples.smoke.stories.tsx +29 -0
  21. package/src/_stories/Chart.YAxisAutoMaxStrategy.stories.tsx +52 -0
  22. package/src/_stories/Chart.YAxisFilterDomain.stories.tsx +90 -0
  23. package/src/_stories/ChartAnnotation.smoke.stories.tsx +10 -0
  24. package/src/_stories/ChartAxisTitles.smoke.stories.tsx +13 -0
  25. package/src/_stories/ChartBar.Editor.stories.tsx +63 -0
  26. package/src/_stories/ChartBrush.Interaction.stories.tsx +112 -0
  27. package/src/_stories/ChartBrush.smoke.stories.tsx +13 -4
  28. package/src/_stories/_mock/alt_text_metadata.json +38 -0
  29. package/src/_stories/_mock/annotation_event-line_mock.json +1064 -0
  30. package/src/_stories/_mock/heatmap-average-age-categorical.json +315 -0
  31. package/src/_stories/_mock/heatmap.json +156 -0
  32. package/src/_stories/_mock/reported_pertussis_cases.json +1643 -0
  33. package/src/_stories/_mock/sankey_mock.json +78 -0
  34. package/src/_stories/_mock/small_multiples/small_multiples_lines.json +1 -1
  35. package/src/components/Annotations/components/AnnotationDraggable.styles.css +22 -1
  36. package/src/components/Annotations/components/AnnotationDraggable.tsx +220 -120
  37. package/src/components/Annotations/components/AnnotationDropdown.tsx +5 -2
  38. package/src/components/Annotations/tests/AnnotationDraggable.test.tsx +198 -0
  39. package/src/components/Axis/LeftAxis.tsx +17 -11
  40. package/src/components/Axis/LeftAxisGridlines.tsx +22 -12
  41. package/src/components/Axis/RightAxis.tsx +3 -0
  42. package/src/components/Axis/tests/AxisTickValues.test.tsx +86 -0
  43. package/src/components/Brush/BrushSelector.tsx +88 -187
  44. package/src/components/Brush/brushInteraction.ts +22 -0
  45. package/src/components/Brush/tests/brushInteraction.test.ts +38 -0
  46. package/src/components/EditorPanel/EditorPanel.tsx +677 -185
  47. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +114 -52
  48. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +105 -13
  49. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +1 -0
  50. package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +5 -0
  51. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +2 -2
  52. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.test.tsx +98 -0
  53. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +10 -0
  54. package/src/components/EditorPanel/components/Panels/Panel.Visual.test.tsx +484 -0
  55. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +4 -1
  56. package/src/components/EditorPanel/components/Panels/SeriesColorAssignments.tsx +262 -0
  57. package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +32 -0
  58. package/src/components/EditorPanel/components/panels.scss +10 -0
  59. package/src/components/EditorPanel/editor-panel.scss +47 -0
  60. package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +61 -0
  61. package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +74 -0
  62. package/src/components/EditorPanel/useEditorPermissions.test.tsx +315 -0
  63. package/src/components/EditorPanel/useEditorPermissions.ts +77 -21
  64. package/src/components/HeatMap/components/HeatMap.tsx +767 -0
  65. package/src/components/HeatMap/components/HeatMapGradientLegend.tsx +128 -0
  66. package/src/components/HeatMap/heatmap.constants.ts +13 -0
  67. package/src/components/HeatMap/heatmap.css +85 -0
  68. package/src/components/HeatMap/helpers/index.ts +261 -0
  69. package/src/components/HeatMap/index.tsx +5 -0
  70. package/src/components/HeatMap/tests/HeatMap.test.tsx +1072 -0
  71. package/src/components/HeatMap/tests/helpers.test.ts +167 -0
  72. package/src/components/Legend/Legend.Component.tsx +2 -2
  73. package/src/components/Legend/helpers/createFormatLabels.test.ts +88 -0
  74. package/src/components/Legend/helpers/createFormatLabels.tsx +6 -1
  75. package/src/components/LineChart/components/LineChart.BumpCircle.tsx +19 -22
  76. package/src/components/LineChart/helpers.test.ts +574 -0
  77. package/src/components/LineChart/helpers.ts +39 -16
  78. package/src/components/LineChart/index.tsx +28 -5
  79. package/src/components/LinearChart/tests/LinearChart.test.tsx +137 -33
  80. package/src/components/LinearChart/tests/mockConfigContext.ts +1 -0
  81. package/src/components/LinearChart.tsx +103 -47
  82. package/src/components/PieChart/PieChart.tsx +4 -1
  83. package/src/components/RadarChart/RadarChart.tsx +3 -1
  84. package/src/components/Regions/components/Regions.tsx +195 -41
  85. package/src/components/Sankey/components/Sankey.tsx +7 -3
  86. package/src/components/Sankey/helpers/getSankeyTooltip.tsx +7 -6
  87. package/src/components/ScatterPlot/ScatterPlot.jsx +3 -3
  88. package/src/components/SmallMultiples/SmallMultipleTile.tsx +33 -3
  89. package/src/components/SmallMultiples/SmallMultiples.css +12 -0
  90. package/src/components/SmallMultiples/SmallMultiples.tsx +50 -15
  91. package/src/components/SmallMultiples/tests/SmallMultipleTile.test.tsx +152 -0
  92. package/src/components/SmallMultiples/tests/SmallMultiples.test.tsx +170 -0
  93. package/src/components/Sparkline/components/SparkLine.tsx +4 -9
  94. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +1 -1
  95. package/src/components/WarmingStripes/WarmingStripesGradientLegend.tsx +2 -2
  96. package/src/data/initial-state.js +12 -0
  97. package/src/helpers/axisLabelFontSize.ts +8 -0
  98. package/src/helpers/bumpChartTooltip.ts +54 -0
  99. package/src/helpers/calculateLeftYAxisWidth.ts +13 -3
  100. package/src/helpers/colorAssignmentHelpers.ts +102 -0
  101. package/src/helpers/dataHelpers.ts +11 -1
  102. package/src/helpers/ensureSpecialChartAxisTypes.ts +11 -0
  103. package/src/helpers/getAxisMaxOverride.ts +16 -0
  104. package/src/helpers/getCleanTopTickMax.ts +64 -0
  105. package/src/helpers/getColorScale.ts +18 -9
  106. package/src/helpers/getHasBoundarySuppression.ts +26 -0
  107. package/src/helpers/getMinMax.ts +26 -25
  108. package/src/helpers/getYAxisDomainData.ts +38 -0
  109. package/src/helpers/getYAxisFinalizationEligibility.ts +40 -0
  110. package/src/helpers/handleChartAriaLabels.ts +23 -10
  111. package/src/helpers/hasSpacedInlineLabel.ts +10 -0
  112. package/src/helpers/seriesColumnSettings.ts +5 -4
  113. package/src/helpers/seriesHighlight.ts +26 -0
  114. package/src/helpers/smallMultiplesHelpers.ts +5 -0
  115. package/src/helpers/tests/axisLabelFontSize.test.ts +15 -0
  116. package/src/helpers/tests/bumpChartTooltip.test.ts +75 -0
  117. package/src/helpers/tests/calculateLeftYAxisWidth.test.ts +38 -0
  118. package/src/helpers/tests/dataHelpers.test.ts +135 -0
  119. package/src/helpers/tests/ensureSpecialChartAxisTypes.test.ts +30 -0
  120. package/src/helpers/tests/getAxisMaxOverride.test.ts +36 -0
  121. package/src/helpers/tests/getCleanTopTickMax.test.ts +165 -0
  122. package/src/helpers/tests/getColorScale.test.ts +197 -0
  123. package/src/helpers/tests/getMinMax.test.ts +211 -0
  124. package/src/helpers/tests/getYAxisDomainData.test.ts +119 -0
  125. package/src/helpers/tests/getYAxisFinalizationEligibility.test.ts +185 -0
  126. package/src/helpers/tests/handleChartAriaLabels.test.ts +71 -0
  127. package/src/helpers/tests/hasSpacedInlineLabel.test.ts +75 -0
  128. package/src/helpers/tests/seriesColumnSettings.test.ts +16 -2
  129. package/src/helpers/tests/seriesHighlight.test.ts +61 -0
  130. package/src/helpers/tests/smallMultiplesHelpers.test.ts +99 -0
  131. package/src/hooks/tests/useRightAxis.test.tsx +127 -0
  132. package/src/hooks/tests/useScales.test.tsx +383 -0
  133. package/src/hooks/useRightAxis.ts +30 -13
  134. package/src/hooks/useScales.ts +48 -21
  135. package/src/hooks/useTooltip.tsx +3 -1
  136. package/src/index.jsx +5 -2
  137. package/src/scss/main.scss +32 -49
  138. package/src/test/CdcChart.dataTable.test.tsx +122 -0
  139. package/src/types/ChartConfig.ts +24 -1
  140. package/src/types/ChartContext.ts +5 -0
  141. package/src/helpers/getYAxisAutoPadding.ts +0 -53
@@ -0,0 +1,187 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { expect } from 'storybook/test'
3
+ import Chart from '../CdcChart'
4
+ import heatMapAverageAgeCategoricalConfig from './_mock/heatmap-average-age-categorical.json'
5
+ import heatMapSparseAggregationConfig from '../../examples/feature/heatmap/sparse-aggregation.json'
6
+ import heatMapCellValuesBottomAxisConfig from '../../examples/feature/heatmap/cell-values-bottom-axis.json'
7
+ import heatMapCalendarConfig from '../../examples/feature/heatmap/calendar-heatmap.json'
8
+ import { assertVisualizationRendered, waitForPresence } from '@cdc/core/helpers/testing'
9
+
10
+ const meta: Meta<typeof Chart> = {
11
+ title: 'Components/Templates/Chart/HeatMap',
12
+ component: Chart
13
+ }
14
+
15
+ type Story = StoryObj<typeof Chart>
16
+
17
+ const heatMapTopYAxisLabelConfig = {
18
+ ...heatMapAverageAgeCategoricalConfig,
19
+ yAxis: {
20
+ ...heatMapAverageAgeCategoricalConfig.yAxis,
21
+ titlePlacement: 'top'
22
+ }
23
+ }
24
+
25
+ const getTooltipHtmlValues = (canvasElement: HTMLElement) =>
26
+ Array.from(canvasElement.querySelectorAll('[data-tooltip-html]')).map(
27
+ element => element.getAttribute('data-tooltip-html') || ''
28
+ )
29
+
30
+ const getSvgTextBox = (svg: SVGSVGElement, value: string) =>
31
+ Array.from(svg.querySelectorAll('text'))
32
+ .find(text => text.textContent === value)
33
+ ?.getBoundingClientRect()
34
+
35
+ const getSvgText = (svg: SVGSVGElement, value: string) =>
36
+ Array.from(svg.querySelectorAll('text')).find(text => text.textContent === value)
37
+
38
+ const getTranslateY = (element: Element | null | undefined) => {
39
+ const transform = element?.getAttribute('transform') || ''
40
+ const match = transform.match(/translate\([^,]+,\s*([^)]+)\)/)
41
+ return match ? Number(match[1]) : 0
42
+ }
43
+
44
+ const getElementBox = (canvasElement: HTMLElement, selector: string) =>
45
+ canvasElement.querySelector(selector)?.getBoundingClientRect()
46
+
47
+ export const HeatMap_Demo: Story = {
48
+ args: {
49
+ config: heatMapSparseAggregationConfig,
50
+ isEditor: false
51
+ },
52
+ play: async ({ canvasElement }) => {
53
+ await assertVisualizationRendered(canvasElement)
54
+ await waitForPresence('.visx-heatmap-rect', canvasElement)
55
+
56
+ const cells = canvasElement.querySelectorAll('.visx-heatmap-rect')
57
+ const tooltipHtmlValues = getTooltipHtmlValues(canvasElement)
58
+ const firstCell = cells[0]
59
+ const legendBox = getElementBox(canvasElement, '.cdc-heatmap__legend')
60
+ const heatMapBox = getElementBox(canvasElement, '.cdc-heatmap')
61
+ const dataTableBox = getElementBox(canvasElement, '.data-table-container')
62
+ const tableGap = (dataTableBox?.top || 0) - (heatMapBox?.bottom || 0)
63
+
64
+ expect(cells.length).toBe(9)
65
+ expect(firstCell).toHaveAttribute('tabindex', '0')
66
+ expect(firstCell).toHaveAttribute('aria-label')
67
+ expect(legendBox?.bottom).toBeLessThanOrEqual((heatMapBox?.top || 0) + 2)
68
+ expect(tableGap).toBeGreaterThanOrEqual(0)
69
+ expect(tableGap).toBeLessThan(80)
70
+ expect(tooltipHtmlValues.some(html => html.includes('tooltip-heading'))).toBe(true)
71
+ expect(tooltipHtmlValues.some(html => html.includes('Aggregated Rows: 2'))).toBe(true)
72
+ expect(tooltipHtmlValues.some(html => html.includes('Notes: Multiple values'))).toBe(true)
73
+ expect(tooltipHtmlValues.some(html => html.includes('No data'))).toBe(true)
74
+ }
75
+ }
76
+
77
+ export const HeatMap_Average_Age_Categorical_Demo: Story = {
78
+ args: {
79
+ config: heatMapAverageAgeCategoricalConfig,
80
+ isEditor: false
81
+ },
82
+ play: async ({ canvasElement }) => {
83
+ await assertVisualizationRendered(canvasElement)
84
+ await waitForPresence('.visx-heatmap-rect', canvasElement)
85
+
86
+ const cells = canvasElement.querySelectorAll('.visx-heatmap-rect')
87
+ const tooltipHtmlValues = getTooltipHtmlValues(canvasElement)
88
+ const firstCell = cells[0]
89
+ const firstCellAriaLabel = firstCell.getAttribute('aria-label') || ''
90
+ const chartSvg = firstCell.ownerSVGElement as SVGSVGElement
91
+ const firstCellBox = firstCell.getBoundingClientRect()
92
+ const lastCellBox = cells[cells.length - 1].getBoundingClientRect()
93
+ const cityLabelBox = getSvgTextBox(chartSvg, 'Atlanta')
94
+ const rowAxisTitle = getSvgText(chartSvg, 'City')
95
+ const columnLabelBox = getSvgTextBox(chartSvg, 'Urban Core')
96
+ const svgBox = chartSvg.getBoundingClientRect()
97
+ const blockLeft = Math.min(cityLabelBox?.left || firstCellBox.left, firstCellBox.left)
98
+ const blockRight = lastCellBox.right
99
+ const blockCenterDelta = Math.abs((blockLeft + blockRight) / 2 - (svgBox.left + svgBox.width / 2))
100
+ const rowLabelGap = cityLabelBox ? firstCellBox.left - cityLabelBox.right : 0
101
+ const columnLabelGap = columnLabelBox ? firstCellBox.top - columnLabelBox.bottom : 0
102
+
103
+ expect(cells.length).toBe(60)
104
+ expect(firstCellAriaLabel).toContain('Community Type: Urban Core')
105
+ expect(firstCellAriaLabel).toContain('City: Atlanta')
106
+ expect(rowLabelGap).toBeGreaterThan(16)
107
+ expect(rowLabelGap).toBeLessThan(80)
108
+ expect(columnLabelGap).toBeGreaterThan(0)
109
+ expect(rowAxisTitle?.getAttribute('transform')).toContain('rotate(-90)')
110
+ expect(columnLabelBox?.top).toBeGreaterThanOrEqual(svgBox.top - 1)
111
+ expect(blockCenterDelta).toBeLessThan(140)
112
+ expect(tooltipHtmlValues.some(html => html.includes('Community Type: Urban Core'))).toBe(true)
113
+ expect(tooltipHtmlValues.some(html => html.includes('Average age: 34'))).toBe(true)
114
+ expect(tooltipHtmlValues.some(html => html.includes('Notes: Dense city center'))).toBe(true)
115
+ }
116
+ }
117
+
118
+ export const HeatMap_Top_Y_Axis_Label_Demo: Story = {
119
+ args: {
120
+ config: heatMapTopYAxisLabelConfig,
121
+ isEditor: false
122
+ },
123
+ play: async ({ canvasElement }) => {
124
+ await assertVisualizationRendered(canvasElement)
125
+ await waitForPresence('.visx-heatmap-rect', canvasElement)
126
+
127
+ const chartSvg = canvasElement.querySelector('.cdc-heatmap__svg') as SVGSVGElement
128
+ const rowAxisTitle = getSvgText(chartSvg, 'City')
129
+ const columnAxisTitle = getSvgText(chartSvg, 'Community Type')
130
+
131
+ expect(rowAxisTitle?.getAttribute('transform')).toBeNull()
132
+ expect(rowAxisTitle?.getAttribute('y')).toBe(columnAxisTitle?.getAttribute('y'))
133
+ }
134
+ }
135
+
136
+ export const HeatMap_Cell_Values_And_Binned_Legend: Story = {
137
+ args: {
138
+ config: heatMapCellValuesBottomAxisConfig,
139
+ isEditor: false
140
+ },
141
+ play: async ({ canvasElement }) => {
142
+ await assertVisualizationRendered(canvasElement)
143
+ await waitForPresence('.visx-heatmap-rect', canvasElement)
144
+ await waitForPresence('.cdc-heatmap__cell-value', canvasElement)
145
+
146
+ const cellValues = canvasElement.querySelectorAll('.cdc-heatmap__cell-value')
147
+ const legendItems = canvasElement.querySelectorAll('.cdc-heatmap__legend .legend-item')
148
+ const legendBox = getElementBox(canvasElement, '.cdc-heatmap__legend')
149
+ const heatMapBox = getElementBox(canvasElement, '.cdc-heatmap')
150
+ const dataTableBox = getElementBox(canvasElement, '.data-table-container')
151
+ const tableGap = (dataTableBox?.top || 0) - (legendBox?.bottom || 0)
152
+
153
+ expect(canvasElement.querySelectorAll('.visx-heatmap-rect').length).toBe(20)
154
+ expect(canvasElement.querySelector('.visx-axis-bottom')).toBeTruthy()
155
+ expect(heatMapBox?.bottom).toBeLessThanOrEqual((legendBox?.top || 0) + 2)
156
+ expect(tableGap).toBeGreaterThanOrEqual(0)
157
+ expect(tableGap).toBeLessThan(80)
158
+ expect(cellValues.length).toBeGreaterThan(0)
159
+ expect(legendItems.length).toBeGreaterThan(0)
160
+ }
161
+ }
162
+
163
+ export const HeatMap_Calendar_Demo: Story = {
164
+ args: {
165
+ config: heatMapCalendarConfig,
166
+ isEditor: false
167
+ },
168
+ play: async ({ canvasElement }) => {
169
+ await assertVisualizationRendered(canvasElement)
170
+ await waitForPresence('.visx-heatmap-rect', canvasElement)
171
+
172
+ const cells = Array.from(canvasElement.querySelectorAll('.visx-heatmap-rect'))
173
+ const chartSvg = cells[0].ownerSVGElement as SVGSVGElement
174
+ const rowAxisTitle = getSvgText(chartSvg, 'Month')
175
+ const gridTop = Math.min(...cells.map(cell => Number(cell.getAttribute('y'))))
176
+ const gridBottom = Math.max(
177
+ ...cells.map(cell => Number(cell.getAttribute('y')) + Number(cell.getAttribute('height')))
178
+ )
179
+ const gridCenter = (gridTop + gridBottom) / 2
180
+
181
+ expect(cells.length).toBe(372)
182
+ expect(rowAxisTitle?.getAttribute('transform')).toContain('rotate(-90)')
183
+ expect(Math.abs(getTranslateY(rowAxisTitle) - gridCenter)).toBeLessThanOrEqual(1)
184
+ }
185
+ }
186
+
187
+ export default meta
@@ -0,0 +1,62 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import Chart from '../CdcChartComponent'
3
+ import sankeyConfig from './_mock/sankey_mock.json'
4
+ import { assertVisualizationRendered } from '@cdc/core/helpers/testing'
5
+
6
+ const meta: Meta<typeof Chart> = {
7
+ title: 'Components/Templates/Chart/Sankey',
8
+ component: Chart
9
+ }
10
+
11
+ export default meta
12
+ type Story = StoryObj<typeof Chart>
13
+
14
+ export const Sankey_Basic: Story = {
15
+ args: {
16
+ config: {
17
+ ...sankeyConfig,
18
+ title: 'Patient Care Pathway'
19
+ },
20
+ isEditor: false
21
+ },
22
+ play: async ({ canvasElement }) => {
23
+ await assertVisualizationRendered(canvasElement)
24
+ }
25
+ }
26
+
27
+ export const Sankey_WithStoryNodes: Story = {
28
+ args: {
29
+ config: {
30
+ ...sankeyConfig,
31
+ title: 'Care Pathway with Story Nodes',
32
+ data: [
33
+ {
34
+ links: sankeyConfig.data[0].links,
35
+ tooltips: true,
36
+ storyNodeText: [
37
+ { StoryNode: 'Screened', segmentTextBefore: 'Total Screened', segmentTextAfter: 'participants' },
38
+ { StoryNode: 'Recovered', segmentTextBefore: 'Fully', segmentTextAfter: 'patients' }
39
+ ]
40
+ }
41
+ ]
42
+ },
43
+ isEditor: false
44
+ },
45
+ play: async ({ canvasElement }) => {
46
+ await assertVisualizationRendered(canvasElement)
47
+ }
48
+ }
49
+
50
+ export const Sankey_TooltipsDisabled: Story = {
51
+ args: {
52
+ config: {
53
+ ...sankeyConfig,
54
+ title: 'Sankey — Tooltips Disabled',
55
+ enableTooltips: false
56
+ },
57
+ isEditor: false
58
+ },
59
+ play: async ({ canvasElement }) => {
60
+ await assertVisualizationRendered(canvasElement)
61
+ }
62
+ }
@@ -0,0 +1,183 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import Chart from '../CdcChartComponent'
3
+ import { assertVisualizationRendered } from '@cdc/core/helpers/testing'
4
+ import { expect, userEvent, waitFor, within } from 'storybook/test'
5
+
6
+ const meta: Meta<typeof Chart> = {
7
+ title: 'Components/Templates/Chart/Series Color Assignments',
8
+ component: Chart
9
+ }
10
+
11
+ type Story = StoryObj<typeof Chart>
12
+
13
+ const assignedColors = {
14
+ alpha: '#0057B7',
15
+ bravo: '#D94E5F',
16
+ charlie: '#5A8E3F'
17
+ }
18
+
19
+ const normalizeColor = (color: string) => {
20
+ const swatch = document.createElement('div')
21
+ swatch.style.color = color
22
+ document.body.appendChild(swatch)
23
+ const normalized = getComputedStyle(swatch).color
24
+ swatch.remove()
25
+ return normalized
26
+ }
27
+
28
+ const getVisiblePointColorsBySeries = (canvasElement: HTMLElement) => {
29
+ const groups = Array.from(canvasElement.querySelectorAll('.line-chart-group .visx-glyph-group')) as SVGGElement[]
30
+ const colorsBySeries = new Map<string, Set<string>>()
31
+
32
+ groups.forEach(group => {
33
+ const seriesIndex = group.getAttribute('data-seriesindex')
34
+ const glyph = group.querySelector('[fill]') as SVGElement | null
35
+ const fill = glyph?.getAttribute('fill')
36
+
37
+ if (!seriesIndex || !fill || fill === 'transparent') return
38
+
39
+ const seriesColors = colorsBySeries.get(seriesIndex) || new Set<string>()
40
+ seriesColors.add(normalizeColor(fill))
41
+ colorsBySeries.set(seriesIndex, seriesColors)
42
+ })
43
+
44
+ return colorsBySeries
45
+ }
46
+
47
+ const expectVisibleSeriesColors = async (
48
+ canvasElement: HTMLElement,
49
+ expected: Array<[seriesIndex: string, color: string]>
50
+ ) => {
51
+ await waitFor(() => {
52
+ const colorsBySeries = getVisiblePointColorsBySeries(canvasElement)
53
+ const expectedSeries = expected.map(([seriesIndex]) => seriesIndex)
54
+
55
+ expect(Array.from(colorsBySeries.keys()).sort()).toEqual(expectedSeries.sort())
56
+
57
+ expected.forEach(([seriesIndex, color]) => {
58
+ expect(Array.from(colorsBySeries.get(seriesIndex) || [])).toEqual([normalizeColor(color)])
59
+ })
60
+ })
61
+ }
62
+
63
+ const expectLegendLabels = async (canvasElement: HTMLElement, expectedLabels: string[]) => {
64
+ await waitFor(() => {
65
+ const labels = Array.from(canvasElement.querySelectorAll('.legend-container [role="button"]')).map(item =>
66
+ item.textContent?.trim()
67
+ )
68
+
69
+ expect(labels).toEqual(expectedLabels)
70
+ })
71
+ }
72
+
73
+ const config = {
74
+ type: 'chart',
75
+ version: '4.26.5',
76
+ title: 'Series Color Assignments With Filtered Series',
77
+ visualizationType: 'Line',
78
+ visualizationSubType: 'regular',
79
+ orientation: 'vertical',
80
+ filterBehavior: 'Filter Change',
81
+ lineDatapointStyle: 'always show',
82
+ lineDatapointColor: 'Same as Line',
83
+ general: {
84
+ palette: {
85
+ name: 'qualitative_standard',
86
+ version: '2.0',
87
+ colorAssignmentMode: 'by-value',
88
+ colorAssignments: [
89
+ { key: 'alpha', color: assignedColors.alpha },
90
+ { key: 'bravo', color: assignedColors.bravo },
91
+ { key: 'charlie', color: assignedColors.charlie }
92
+ ]
93
+ }
94
+ },
95
+ legend: {
96
+ hide: false,
97
+ behavior: 'isolate',
98
+ dynamicLegend: false,
99
+ position: 'top',
100
+ style: 'circles',
101
+ unified: false
102
+ },
103
+ xAxis: {
104
+ dataKey: 'week',
105
+ type: 'date-time',
106
+ dateParseFormat: '%Y-%m-%d',
107
+ dateDisplayFormat: '%b. %-d',
108
+ label: 'Week',
109
+ sortDates: false
110
+ },
111
+ yAxis: {
112
+ label: 'Rate',
113
+ min: '0',
114
+ max: '50',
115
+ numTicks: '5',
116
+ gridLines: true
117
+ },
118
+ visual: {
119
+ lineDatapointSymbol: 'none',
120
+ verticalHoverLine: false,
121
+ horizontalHoverLine: false
122
+ },
123
+ tooltips: {
124
+ singleSeries: false
125
+ },
126
+ series: [
127
+ { dataKey: 'alpha', name: 'Alpha', type: 'Line', axis: 'Left', tooltip: true },
128
+ { dataKey: 'bravo', name: 'Bravo', type: 'Line', axis: 'Left', tooltip: true },
129
+ { dataKey: 'charlie', name: 'Charlie', type: 'Line', axis: 'Left', tooltip: true }
130
+ ],
131
+ filters: [
132
+ {
133
+ values: ['Alpha + Bravo', 'Bravo + Charlie'],
134
+ filterStyle: 'dropdown',
135
+ id: 1726688167657,
136
+ columnName: 'view',
137
+ active: 'Alpha + Bravo',
138
+ label: 'Visible series'
139
+ }
140
+ ],
141
+ data: [
142
+ { week: '2024-01-01', view: 'Alpha + Bravo', alpha: 12, bravo: 20, charlie: null },
143
+ { week: '2024-01-08', view: 'Alpha + Bravo', alpha: 16, bravo: 23, charlie: null },
144
+ { week: '2024-01-15', view: 'Alpha + Bravo', alpha: 21, bravo: 28, charlie: null },
145
+ { week: '2024-01-22', view: 'Alpha + Bravo', alpha: 26, bravo: 31, charlie: null },
146
+ { week: '2024-01-01', view: 'Bravo + Charlie', alpha: null, bravo: 20, charlie: 30 },
147
+ { week: '2024-01-08', view: 'Bravo + Charlie', alpha: null, bravo: 23, charlie: 34 },
148
+ { week: '2024-01-15', view: 'Bravo + Charlie', alpha: null, bravo: 28, charlie: 39 },
149
+ { week: '2024-01-22', view: 'Bravo + Charlie', alpha: null, bravo: 31, charlie: 43 }
150
+ ],
151
+ dataDescription: {
152
+ horizontal: false,
153
+ series: false
154
+ }
155
+ }
156
+
157
+ export const FilteredSeriesKeepAssignedColors: Story = {
158
+ args: {
159
+ config,
160
+ isEditor: false
161
+ },
162
+ play: async ({ canvasElement }) => {
163
+ await assertVisualizationRendered(canvasElement)
164
+
165
+ await expectVisibleSeriesColors(canvasElement, [
166
+ ['0', assignedColors.alpha],
167
+ ['1', assignedColors.bravo]
168
+ ])
169
+ await expectLegendLabels(canvasElement, ['Alpha', 'Bravo'])
170
+
171
+ const canvas = within(canvasElement)
172
+ const filter = canvas.getByLabelText(/filter by visible series/i) as HTMLSelectElement
173
+ await userEvent.selectOptions(filter, 'Bravo + Charlie')
174
+
175
+ await expectVisibleSeriesColors(canvasElement, [
176
+ ['1', assignedColors.bravo],
177
+ ['2', assignedColors.charlie]
178
+ ])
179
+ await expectLegendLabels(canvasElement, ['Bravo', 'Charlie'])
180
+ }
181
+ }
182
+
183
+ export default meta
@@ -5,6 +5,7 @@ import smallMultiplesBigDataBars from './_mock/small_multiples/small_multiples_b
5
5
  import smallMultiplesLinesColors from './_mock/small_multiples/small_multiples_lines_colors.json'
6
6
  import smallMultiplesLines from './_mock/small_multiples/small_multiples_lines.json'
7
7
  import smallMultiplesStackedBars from './_mock/small_multiples/small_multiples_stacked_bars.json'
8
+ import { editConfigKeys } from '@cdc/core/helpers/configHelpers'
8
9
  import { assertVisualizationRendered, waitForPresence } from '@cdc/core/helpers/testing'
9
10
  import { expect } from 'storybook/test'
10
11
 
@@ -62,6 +63,34 @@ export const smallMultiples_Lines: Story = {
62
63
  await assertVisualizationRendered(canvasElement)
63
64
  }
64
65
  }
66
+ export const smallMultiples_TopYAxisTitle: Story = {
67
+ args: {
68
+ config: editConfigKeys(smallMultiplesLines, [
69
+ { path: ['yAxis', 'label'], value: 'Percentage of emergency department visits' },
70
+ { path: ['runtime', 'yAxis', 'label'], value: 'Percentage of emergency department visits' },
71
+ { path: ['yAxis', 'inlineLabel'], value: ' percent' },
72
+ { path: ['yAxis', 'titlePlacement'], value: 'top' }
73
+ ]),
74
+ isEditor: false
75
+ },
76
+ play: async ({ canvasElement }) => {
77
+ await assertVisualizationRendered(canvasElement)
78
+ }
79
+ }
80
+ export const smallMultiples_TopYAxisTitle2: Story = {
81
+ args: {
82
+ config: editConfigKeys(smallMultiplesLines, [
83
+ { path: ['yAxis', 'label'], value: 'Percentage of emergency department visits' },
84
+ { path: ['runtime', 'yAxis', 'label'], value: 'Percentage of emergency department visits' },
85
+ { path: ['yAxis', 'inlineLabel'], value: '%' },
86
+ { path: ['yAxis', 'titlePlacement'], value: 'top' }
87
+ ]),
88
+ isEditor: false
89
+ },
90
+ play: async ({ canvasElement }) => {
91
+ await assertVisualizationRendered(canvasElement)
92
+ }
93
+ }
65
94
  export const smallMultiples_StackedBars: Story = {
66
95
  args: {
67
96
  config: smallMultiplesStackedBars,
@@ -0,0 +1,52 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import Chart from '../CdcChartComponent'
3
+ import { editConfigKeys } from '@cdc/core/helpers/configHelpers'
4
+ import smallestLeftAxisMaxConfig from './_mock/smallest_left_axis_max.json'
5
+ import { assertVisualizationRendered } from '@cdc/core/helpers/testing'
6
+
7
+ const meta: Meta<typeof Chart> = {
8
+ title: 'Components/Templates/Chart/Y Axis Auto Max Strategy',
9
+ component: Chart
10
+ }
11
+
12
+ type Story = StoryObj<typeof Chart>
13
+
14
+ const baseConfig = editConfigKeys(smallestLeftAxisMaxConfig, [
15
+ { path: ['yAxis', 'smallestLeftAxisMax'], value: undefined },
16
+ { path: ['yAxis', 'filterDomainBehavior'], value: 'dynamic' },
17
+ { path: ['yAxis', 'numTicks'], value: 4 },
18
+ { path: ['filters', '0', 'active'], value: 'Region A' }
19
+ ])
20
+
21
+ export const Clean_Top_Tick_Strategy: Story = {
22
+ args: {
23
+ config: editConfigKeys(baseConfig, [{ path: ['yAxis', 'autoMaxStrategy'], value: 'clean-top-tick' }])
24
+ },
25
+ play: async ({ canvasElement }) => {
26
+ await assertVisualizationRendered(canvasElement)
27
+ }
28
+ }
29
+
30
+ export const Default_Strategy: Story = {
31
+ args: {
32
+ config: editConfigKeys(baseConfig, [{ path: ['yAxis', 'autoMaxStrategy'], value: 'default' }])
33
+ },
34
+ play: async ({ canvasElement }) => {
35
+ await assertVisualizationRendered(canvasElement)
36
+ }
37
+ }
38
+
39
+ export const Stable_Filter_Domain_With_Clean_Top_Tick: Story = {
40
+ args: {
41
+ config: editConfigKeys(baseConfig, [
42
+ { path: ['yAxis', 'filterDomainBehavior'], value: 'stable' },
43
+ { path: ['yAxis', 'autoMaxStrategy'], value: 'clean-top-tick' },
44
+ { path: ['filters', '0', 'active'], value: 'Region B' }
45
+ ])
46
+ },
47
+ play: async ({ canvasElement }) => {
48
+ await assertVisualizationRendered(canvasElement)
49
+ }
50
+ }
51
+
52
+ export default meta
@@ -0,0 +1,90 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { expect, within } from 'storybook/test'
3
+ import Chart from '../CdcChartComponent'
4
+ import { editConfigKeys } from '@cdc/core/helpers/configHelpers'
5
+ import smallestLeftAxisMaxConfig from './_mock/smallest_left_axis_max.json'
6
+ import horizontalBarConfig from './_mock/horizontal_bar.json'
7
+ import { assertVisualizationRendered, openAccordion, waitForEditor } from '@cdc/core/helpers/testing'
8
+
9
+ const meta: Meta<typeof Chart> = {
10
+ title: 'Components/Templates/Chart/Y Axis Filter Domain',
11
+ component: Chart
12
+ }
13
+
14
+ type Story = StoryObj<typeof Chart>
15
+
16
+ const baseConfig = editConfigKeys(smallestLeftAxisMaxConfig, [
17
+ { path: ['yAxis', 'smallestLeftAxisMax'], value: undefined },
18
+ { path: ['yAxis', 'numTicks'], value: 4 }
19
+ ])
20
+
21
+ export const Dynamic_Filter_Domain: Story = {
22
+ args: {
23
+ config: editConfigKeys(baseConfig, [{ path: ['yAxis', 'filterDomainBehavior'], value: 'dynamic' }])
24
+ },
25
+ play: async ({ canvasElement }) => {
26
+ await assertVisualizationRendered(canvasElement)
27
+ }
28
+ }
29
+
30
+ export const Stable_Filter_Domain: Story = {
31
+ args: {
32
+ config: editConfigKeys(baseConfig, [{ path: ['yAxis', 'filterDomainBehavior'], value: 'stable' }])
33
+ },
34
+ play: async ({ canvasElement }) => {
35
+ await assertVisualizationRendered(canvasElement)
36
+ }
37
+ }
38
+
39
+ export const Editor_Control: Story = {
40
+ args: {
41
+ config: editConfigKeys(baseConfig, [
42
+ { path: ['yAxis', 'filterDomainBehavior'], value: 'stable' },
43
+ { path: ['yAxis', 'autoMaxStrategy'], value: 'clean-top-tick' }
44
+ ]),
45
+ isEditor: true
46
+ },
47
+ play: async ({ canvasElement }) => {
48
+ const canvas = within(canvasElement)
49
+ await waitForEditor(canvas)
50
+ await openAccordion(canvas, 'Left Value Axis')
51
+
52
+ const filterDomainSelect = (await canvas.findByLabelText(/Filter Domain Behavior/i)) as HTMLSelectElement
53
+ expect(filterDomainSelect.value).toBe('stable')
54
+ expect(Array.from(filterDomainSelect.options).map(option => option.text)).toEqual(['Dynamic', 'Stable'])
55
+
56
+ const autoMaxStrategySelect = (await canvas.findByLabelText(/Automatic max strategy/i)) as HTMLSelectElement
57
+ expect(autoMaxStrategySelect.value).toBe('clean-top-tick')
58
+ expect(Array.from(autoMaxStrategySelect.options).map(option => option.text)).toEqual(['Default', 'Clean top tick'])
59
+ }
60
+ }
61
+
62
+ export const Horizontal_Editor_Control: Story = {
63
+ args: {
64
+ config: editConfigKeys(horizontalBarConfig, [
65
+ { path: ['filters'], value: [{ columnName: 'Race', active: 'AI/AN, Non-Hispanic', showDropdown: true }] },
66
+ { path: ['xAxis', 'max'], value: '' },
67
+ { path: ['yAxis', 'type'], value: 'linear' },
68
+ { path: ['yAxis', 'smallestLeftAxisMax'], value: 5 },
69
+ { path: ['yAxis', 'filterDomainBehavior'], value: 'stable' },
70
+ { path: ['yAxis', 'autoMaxStrategy'], value: 'clean-top-tick' }
71
+ ]),
72
+ isEditor: true
73
+ },
74
+ play: async ({ canvasElement }) => {
75
+ const canvas = within(canvasElement)
76
+ await waitForEditor(canvas)
77
+ await openAccordion(canvas, 'Value Axis')
78
+
79
+ const smallestAxisMaxInput = (await canvas.findByLabelText(/Smallest axis maximum/i)) as HTMLInputElement
80
+ expect(smallestAxisMaxInput.value).toBe('5')
81
+
82
+ const filterDomainSelect = (await canvas.findByLabelText(/Filter Domain Behavior/i)) as HTMLSelectElement
83
+ expect(filterDomainSelect.value).toBe('stable')
84
+
85
+ const autoMaxStrategySelect = (await canvas.findByLabelText(/Automatic max strategy/i)) as HTMLSelectElement
86
+ expect(autoMaxStrategySelect.value).toBe('clean-top-tick')
87
+ }
88
+ }
89
+
90
+ export default meta
@@ -2,6 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
2
2
  import annotationConfig from './_mock/annotation_category_mock.json'
3
3
  import annotationConfigDateLinear from './_mock/annotation_date-linear_mock.json'
4
4
  import annotationConfigDateTime from './_mock/annotation_date-time_mock.json'
5
+ import annotationConfigEventLine from './_mock/annotation_event-line_mock.json'
5
6
  import Chart from '../CdcChartComponent'
6
7
  import { assertVisualizationRendered } from '@cdc/core/helpers/testing'
7
8
 
@@ -42,4 +43,13 @@ export const Chart_Annotation_Date_Time: Story = {
42
43
  }
43
44
  }
44
45
 
46
+ export const Chart_Annotation_Event_Line: Story = {
47
+ args: {
48
+ config: annotationConfigEventLine
49
+ },
50
+ play: async ({ canvasElement }) => {
51
+ await assertVisualizationRendered(canvasElement)
52
+ }
53
+ }
54
+
45
55
  export default meta
@@ -61,3 +61,16 @@ export const Paired_Bar: Story = {
61
61
  await assertVisualizationRendered(canvasElement)
62
62
  }
63
63
  }
64
+
65
+ export const Top_Y_Axis_Title: Story = {
66
+ args: {
67
+ config: editConfigKeys(longXLabelsConfig, [
68
+ { path: ['yAxis', 'label'], value: 'Number of emergency department visits' },
69
+ { path: ['runtime', 'yAxis', 'label'], value: 'Number of emergency department visits' },
70
+ { path: ['yAxis', 'titlePlacement'], value: 'top' }
71
+ ])
72
+ },
73
+ play: async ({ canvasElement }) => {
74
+ await assertVisualizationRendered(canvasElement)
75
+ }
76
+ }