@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
@@ -848,6 +848,61 @@ export const BarLeftValueAxisTests: Story = {
848
848
  }
849
849
  )
850
850
 
851
+ const getAxisTitlePlacementVisualization = () => {
852
+ const chartContainer = canvasElement.querySelector('.cove-visualization__body, .chart-container, .visualization')
853
+ const svg = chartContainer?.querySelector('svg') || canvasElement.querySelector('svg:not(.icon)')
854
+ const sideLabelCandidates = svg ? Array.from(svg.querySelectorAll('text.y-label')) : []
855
+ const sideLabel = sideLabelCandidates.find(el => (el.textContent?.trim() || '').length > 0) || null
856
+ const topLabel = chartContainer?.querySelector('.y-axis-top-title')
857
+
858
+ return {
859
+ hasSideLabel: !!sideLabel,
860
+ sideLabelTransform: sideLabel?.getAttribute('transform') || '',
861
+ sideLabelText: sideLabel?.textContent?.trim() || '',
862
+ hasTopLabel: !!topLabel,
863
+ topLabelText: topLabel?.textContent?.trim() || ''
864
+ }
865
+ }
866
+
867
+ const titlePlacementSelect = canvas.getByLabelText(/label placement/i) as HTMLSelectElement
868
+
869
+ expect(yAxisLabelInput.getAttribute('maxlength')).toBe('35')
870
+
871
+ await performAndAssert(
872
+ 'Switch Title Placement to Top',
873
+ getAxisTitlePlacementVisualization,
874
+ async () => {
875
+ await userEvent.selectOptions(titlePlacementSelect, 'top')
876
+ },
877
+ (before, after) => {
878
+ expect(titlePlacementSelect.value).toBe('top')
879
+ expect(yAxisLabelInput.getAttribute('maxlength')).toBeNull()
880
+ expect(after.hasTopLabel).toBe(true)
881
+ expect(after.topLabelText).toBe('Y-Axis')
882
+ expect(after.hasSideLabel).toBe(false)
883
+
884
+ return true
885
+ }
886
+ )
887
+
888
+ await performAndAssert(
889
+ 'Switch Title Placement back to Side',
890
+ getAxisTitlePlacementVisualization,
891
+ async () => {
892
+ await userEvent.selectOptions(titlePlacementSelect, 'side')
893
+ },
894
+ (before, after) => {
895
+ expect(titlePlacementSelect.value).toBe('side')
896
+ expect(yAxisLabelInput.getAttribute('maxlength')).toBe('35')
897
+ expect(after.hasTopLabel).toBe(false)
898
+ expect(after.hasSideLabel).toBe(true)
899
+ expect(after.sideLabelText).toBe('Y-Axis')
900
+ expect(after.sideLabelTransform).toContain('rotate(-90)')
901
+
902
+ return true
903
+ }
904
+ )
905
+
851
906
  // ============================================================================
852
907
  // TEST: Inline Label Field
853
908
  // Tests visualization output changes when entering custom inline label text
@@ -1529,9 +1584,13 @@ export const BarRegionsSectionTests: Story = {
1529
1584
  const textColors = canvas.queryAllByLabelText(/text color/i)
1530
1585
  const backgrounds = canvas.queryAllByLabelText(/background/i)
1531
1586
  const minRegionTypes = canvas.queryAllByLabelText(/minimum region type/i)
1587
+ const textColor = textColors[0] as HTMLInputElement | undefined
1588
+ const background = backgrounds[0] as HTMLInputElement | undefined
1532
1589
 
1533
1590
  return {
1534
1591
  regionCount: regionLabels.length,
1592
+ textColor: textColor?.placeholder,
1593
+ background: background?.placeholder,
1535
1594
  hasFields:
1536
1595
  regionLabels.length > 0 && textColors.length > 0 && backgrounds.length > 0 && minRegionTypes.length > 0
1537
1596
  }
@@ -1544,6 +1603,10 @@ export const BarRegionsSectionTests: Story = {
1544
1603
  // Should now have 1 region with all fields
1545
1604
  expect(after.regionCount).toBe(1)
1546
1605
  expect(after.hasFields).toBe(true)
1606
+ expect(after.textColor).toBe(
1607
+ getComputedStyle(document.documentElement).getPropertyValue('--cool-gray-90').trim()
1608
+ )
1609
+ expect(after.background).toBe('#71767a')
1547
1610
 
1548
1611
  return true
1549
1612
  }
@@ -0,0 +1,112 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { expect, waitFor } from 'storybook/test'
3
+ import Chart from '../CdcChartComponent'
4
+ import brushEnabledConfig from './_mock/brush_enabled.json'
5
+ import { assertVisualizationRendered } from '@cdc/core/helpers/testing'
6
+
7
+ const meta: Meta<typeof Chart> = {
8
+ title: 'Components/Templates/Chart/BrushSlider/Interactions',
9
+ component: Chart
10
+ }
11
+
12
+ export default meta
13
+
14
+ type Story = StoryObj<typeof Chart>
15
+
16
+ type BrushSelectionState = {
17
+ x: string | null
18
+ width: string | null
19
+ }
20
+
21
+ const getBrushSelection = (canvasElement: HTMLElement) => {
22
+ const selection = canvasElement.querySelector('.visx-brush-selection') as SVGRectElement | null
23
+ expect(selection).toBeTruthy()
24
+ return selection!
25
+ }
26
+
27
+ const readBrushSelectionState = (canvasElement: HTMLElement): BrushSelectionState => {
28
+ const selection = getBrushSelection(canvasElement)
29
+ return {
30
+ x: selection.getAttribute('x'),
31
+ width: selection.getAttribute('width')
32
+ }
33
+ }
34
+
35
+ const getBrushPoint = (canvasElement: HTMLElement, x: number) => {
36
+ const overlay = canvasElement.querySelector('.visx-brush-overlay') as SVGRectElement | null
37
+ expect(overlay).toBeTruthy()
38
+
39
+ const rect = overlay!.getBoundingClientRect()
40
+ return {
41
+ target: overlay!,
42
+ clientX: rect.left + x,
43
+ clientY: rect.top + rect.height / 2
44
+ }
45
+ }
46
+
47
+ const dispatchPointer = (target: Element, type: string, clientX: number, clientY: number, buttons = 1) => {
48
+ target.dispatchEvent(
49
+ new PointerEvent(type, {
50
+ bubbles: true,
51
+ cancelable: true,
52
+ clientX,
53
+ clientY,
54
+ buttons,
55
+ pointerId: 1,
56
+ pointerType: 'mouse'
57
+ })
58
+ )
59
+ }
60
+
61
+ const settleInteraction = async () => {
62
+ await new Promise<void>(resolve => {
63
+ requestAnimationFrame(() => requestAnimationFrame(() => resolve()))
64
+ })
65
+ }
66
+
67
+ export const OutsideSelectionInteractionsBlocked: Story = {
68
+ args: {
69
+ config: brushEnabledConfig,
70
+ isEditor: false
71
+ },
72
+ parameters: {
73
+ docs: {
74
+ description: {
75
+ story: 'Outside-selection clicks and drags are blocked before they reach the visx brush overlay.'
76
+ }
77
+ }
78
+ },
79
+ play: async ({ canvasElement }) => {
80
+ await assertVisualizationRendered(canvasElement)
81
+
82
+ await waitFor(() => {
83
+ expect(canvasElement.querySelector('.visx-brush-selection')).toBeTruthy()
84
+ expect(canvasElement.querySelector('.visx-brush-overlay')).toBeTruthy()
85
+ })
86
+
87
+ const initial = readBrushSelectionState(canvasElement)
88
+ const selectionX = Number(initial.x)
89
+ const selectionWidth = Number(initial.width)
90
+
91
+ const outsideLeft = getBrushPoint(canvasElement, Math.max(1, selectionX - 40))
92
+ dispatchPointer(outsideLeft.target, 'pointerdown', outsideLeft.clientX, outsideLeft.clientY)
93
+ dispatchPointer(outsideLeft.target, 'pointerup', outsideLeft.clientX, outsideLeft.clientY, 0)
94
+ outsideLeft.target.dispatchEvent(
95
+ new MouseEvent('click', {
96
+ bubbles: true,
97
+ cancelable: true,
98
+ clientX: outsideLeft.clientX,
99
+ clientY: outsideLeft.clientY
100
+ })
101
+ )
102
+ await settleInteraction()
103
+ expect(readBrushSelectionState(canvasElement)).toEqual(initial)
104
+
105
+ const outsideRight = getBrushPoint(canvasElement, selectionX + selectionWidth + 40)
106
+ dispatchPointer(outsideRight.target, 'pointerdown', outsideRight.clientX, outsideRight.clientY)
107
+ dispatchPointer(outsideRight.target, 'pointermove', outsideRight.clientX + 80, outsideRight.clientY)
108
+ dispatchPointer(outsideRight.target, 'pointerup', outsideRight.clientX + 80, outsideRight.clientY, 0)
109
+ await settleInteraction()
110
+ expect(readBrushSelectionState(canvasElement)).toEqual(initial)
111
+ }
112
+ }
@@ -1,6 +1,7 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite'
2
2
  import Chart from '../CdcChartComponent'
3
3
  import brushEnabledConfig from './_mock/brush_enabled.json'
4
+ import pertussisConfig from './_mock/reported_pertussis_cases.json'
4
5
  import { assertVisualizationRendered } from '@cdc/core/helpers/testing'
5
6
 
6
7
  const meta: Meta<typeof Chart> = {
@@ -36,16 +37,24 @@ export const BrushSliderEnabled: Story = {
36
37
  }
37
38
  }
38
39
 
39
- export const BrushSliderInEditor: Story = {
40
+ export const BrushDynamicYAxis: Story = {
40
41
  args: {
41
- config: brushEnabledConfig,
42
- isEditor: true
42
+ config: {
43
+ ...pertussisConfig,
44
+ xAxis: {
45
+ ...pertussisConfig.xAxis,
46
+ brushActive: true,
47
+ brushDynamicYAxis: true,
48
+ brushDefaultRecentDateCount: undefined
49
+ }
50
+ },
51
+ isEditor: false
43
52
  },
44
53
  parameters: {
45
54
  docs: {
46
55
  description: {
47
56
  story:
48
- 'Brush slider in editor mode. You can toggle the "Brush Slider" checkbox in the X-Axis section to enable/disable it.'
57
+ 'Reported pertussis cases (1922-2025) with Dynamic Y-Axis enabled. The Y-axis rescales to fit only the data visible in the current brush selection. Drag the brush to the 1970s-1990s low period (~1K-7K cases) then to the 1920s-1940s peak (~100K-265K cases) to see the Y-axis adjust dramatically.'
49
58
  }
50
59
  }
51
60
  },
@@ -0,0 +1,38 @@
1
+ {
2
+ "version": "4.26.4",
3
+ "type": "chart",
4
+ "visualizationType": "Line",
5
+ "title": "COVID-19 Test Positivity",
6
+ "showTitle": true,
7
+ "animate": false,
8
+ "orientation": "vertical",
9
+ "dataMetadata": {
10
+ "altDescription": "Line chart showing COVID-19 test positivity rising from 4% to 12% over the last eight weeks, which may indicate growing community spread.",
11
+ "lastUpdated": "2024-09-21",
12
+ "source": "CDC NREVSS"
13
+ },
14
+ "data": [
15
+ { "week": "Aug 3", "positivity": 4.2 },
16
+ { "week": "Aug 10", "positivity": 4.8 },
17
+ { "week": "Aug 17", "positivity": 5.5 },
18
+ { "week": "Aug 24", "positivity": 6.3 },
19
+ { "week": "Aug 31", "positivity": 7.4 },
20
+ { "week": "Sep 7", "positivity": 8.9 },
21
+ { "week": "Sep 14", "positivity": 10.6 },
22
+ { "week": "Sep 21", "positivity": 12.0 }
23
+ ],
24
+ "xAxis": {
25
+ "dataKey": "week",
26
+ "type": "categorical",
27
+ "label": "Week Ending"
28
+ },
29
+ "yAxis": {
30
+ "label": "% Positive",
31
+ "numTicks": "5"
32
+ },
33
+ "series": [
34
+ { "dataKey": "positivity", "type": "Line", "axis": "Left", "tooltip": true }
35
+ ],
36
+ "legend": { "hide": true },
37
+ "table": { "show": true, "label": "Data Table" }
38
+ }