@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,404 @@
1
+ import React, { useContext } from 'react'
2
+ import { AxisLeft as VisxAxisLeft } from '@visx/axis'
3
+ import { Group } from '@visx/group'
4
+ import { Line } from '@visx/shape'
5
+ import { Text } from '@visx/text'
6
+ import ConfigContext from '../../ConfigContext'
7
+ import { BlurStrokeText } from '@cdc/core/components/BlurStrokeText'
8
+ import { HORIZON_DEFAULTS } from '../../types/Horizon'
9
+ import { calculateHorizonBands } from '../HorizonChart/helpers/calculateHorizonBands'
10
+ import {
11
+ DEFAULT_TICK_LENGTH,
12
+ LOGARITHMIC_TICK_LENGTH,
13
+ MAJOR_LOG_TICK_LENGTH,
14
+ TICK_LABEL_MARGIN_RIGHT,
15
+ VALUE_ON_LINE_PADDING_NO_AXIS,
16
+ VALUE_ON_LINE_PADDING_WITH_AXIS,
17
+ LABEL_Y_PADDING_ABOVE_GRIDLINES,
18
+ HORIZONTAL_TICK_OFFSET_ADJUSTMENT,
19
+ ZERO_LINE_STROKE_WIDTH,
20
+ BAR_MIN_HEIGHT,
21
+ LOLLIPOP_SIZES
22
+ } from './axis.constants'
23
+
24
+ interface LeftAxisProps {
25
+ yScale: any
26
+ xScale: any
27
+ yMax: number
28
+ xMax: number
29
+ yAxisWidth: number
30
+ numTicks: number
31
+ tickLabelFontSize: number
32
+ axisLabelFontSize: number
33
+ handleLeftTickFormatting: (tick: any, index: number, ticks: any[]) => string
34
+ topYLabelRef?: React.RefObject<SVGTextElement>
35
+ suffixRef?: React.RefObject<SVGTextElement>
36
+ suffixWidth?: number
37
+ horizontalYAxisLabelSpace?: number
38
+ categoryLabelSpace?: number
39
+ yLabelOffset?: number
40
+ }
41
+
42
+ const LeftAxis: React.FC<LeftAxisProps> = ({
43
+ yScale,
44
+ xScale,
45
+ yMax,
46
+ xMax,
47
+ yAxisWidth,
48
+ numTicks,
49
+ tickLabelFontSize,
50
+ axisLabelFontSize,
51
+ handleLeftTickFormatting,
52
+ topYLabelRef,
53
+ suffixRef,
54
+ suffixWidth = 0,
55
+ horizontalYAxisLabelSpace = 0,
56
+ categoryLabelSpace = 0,
57
+ yLabelOffset = 0
58
+ }) => {
59
+ const { config, colorScale, seriesHighlight } = useContext(ConfigContext)
60
+ const { runtime, orientation, visualizationType, visualizationSubType, heights } = config
61
+
62
+ const isLogarithmicAxis = config.yAxis.type === 'logarithmic'
63
+ const { labelsAboveGridlines, hideAxis, inlineLabel } = config.yAxis
64
+ const inlineLabelHasNoSpace = !inlineLabel?.includes(' ')
65
+
66
+ return (
67
+ <VisxAxisLeft
68
+ scale={yScale}
69
+ tickLength={isLogarithmicAxis ? LOGARITHMIC_TICK_LENGTH : DEFAULT_TICK_LENGTH}
70
+ left={yAxisWidth - config.yAxis.axisPadding}
71
+ label={runtime.yAxis.label || runtime.yAxis.label}
72
+ stroke='#333'
73
+ tickFormat={handleLeftTickFormatting}
74
+ numTicks={numTicks}
75
+ >
76
+ {props => {
77
+ const axisCenter =
78
+ config.orientation === 'horizontal'
79
+ ? Math.abs(props.axisToPoint.y - props.axisFromPoint.y) / 2
80
+ : (props.axisFromPoint.y - props.axisToPoint.y) / 2
81
+ const horizontalTickOffset =
82
+ yMax / props.ticks.length / 2 -
83
+ (yMax / props.ticks.length) * (1 - config.barThickness) +
84
+ HORIZONTAL_TICK_OFFSET_ADJUSTMENT
85
+ return (
86
+ <Group className='left-axis'>
87
+ {!config.yAxis.hideAxis && (
88
+ <Line
89
+ from={props.axisFromPoint}
90
+ to={
91
+ runtime.horizontal
92
+ ? {
93
+ x: 0,
94
+ y:
95
+ config.visualizationType === 'Forest Plot'
96
+ ? yMax // Use yMax for forest plot since parentHeight is not available
97
+ : Number(heights.horizontal)
98
+ }
99
+ : props.axisToPoint
100
+ }
101
+ stroke='#000'
102
+ />
103
+ )}
104
+ {orientation === 'vertical' && yScale.domain()[0] < 0 && (
105
+ <Line from={{ x: props.axisFromPoint.x, y: yScale(0) }} to={{ x: xMax, y: yScale(0) }} stroke='#333' />
106
+ )}
107
+ {orientation === 'horizontal' && xScale.domain()[0] < 0 && (
108
+ <Line from={{ x: xScale(0), y: 0 }} to={{ x: xScale(0), y: yMax }} stroke='#333' />
109
+ )}
110
+ {visualizationType === 'Bar' && orientation === 'horizontal' && xScale.domain()[0] < 0 && (
111
+ <Line
112
+ from={{ x: xScale(0), y: 0 }}
113
+ to={{ x: xScale(0), y: yMax }}
114
+ stroke='#333'
115
+ strokeWidth={ZERO_LINE_STROKE_WIDTH}
116
+ />
117
+ )}
118
+ {visualizationType !== 'Horizon Chart' &&
119
+ props.ticks.map((tick, i) => {
120
+ const minY = props.ticks[0].to.y
121
+ const showTicks = String(tick.value).startsWith('1') || tick.value === 0.1 ? 'block' : 'none'
122
+ const tickLength = showTicks === 'block' ? MAJOR_LOG_TICK_LENGTH : 0
123
+ const to = { x: tick.to.x - tickLength, y: tick.to.y }
124
+
125
+ // Vertical value/suffix vars
126
+ const lastTick = props.ticks.length - 1 === i
127
+ const useInlineLabel = lastTick && inlineLabel
128
+ const hideTopTick = lastTick && inlineLabel && !inlineLabelHasNoSpace
129
+ const valueOnLinePadding = hideAxis ? VALUE_ON_LINE_PADDING_NO_AXIS : VALUE_ON_LINE_PADDING_WITH_AXIS
130
+ const labelXPadding = labelsAboveGridlines ? valueOnLinePadding : TICK_LABEL_MARGIN_RIGHT
131
+ const labelYPadding = labelsAboveGridlines ? LABEL_Y_PADDING_ABOVE_GRIDLINES : 0
132
+ const labelX = tick.to.x - labelXPadding
133
+ const labelY = tick.to.y - labelYPadding
134
+ const labelVerticalAnchor = labelsAboveGridlines ? 'end' : 'middle'
135
+ const combineDomInlineLabelWithValue = inlineLabel && labelsAboveGridlines && lastTick
136
+ const formattedValue = useInlineLabel
137
+ ? String(tick?.formattedValue || '').replace(config.dataFormat.suffix, '')
138
+ : tick?.formattedValue
139
+
140
+ return (
141
+ <Group key={`vx-tick-${tick.value}-${i}`} className={'vx-axis-tick'}>
142
+ {!runtime.yAxis.hideTicks && !labelsAboveGridlines && !hideTopTick && (
143
+ <Line
144
+ key={`${tick.value}--hide-hideTicks`}
145
+ from={tick.from}
146
+ to={isLogarithmicAxis ? to : tick.to}
147
+ stroke={config.yAxis.tickColor}
148
+ display={orientation === 'horizontal' ? 'none' : 'block'}
149
+ fontSize={tickLabelFontSize}
150
+ />
151
+ )}
152
+
153
+ {orientation === 'horizontal' &&
154
+ visualizationType === 'Box Plot' &&
155
+ config.yAxis.labelPlacement === 'On Date/Category Axis' &&
156
+ !config.yAxis.hideLabel && (
157
+ <Text
158
+ x={tick.to.x}
159
+ y={yScale(tick.value) + yScale.bandwidth() / 2}
160
+ transform={`rotate(${
161
+ config.orientation === 'horizontal' ? config.runtime.yAxis.tickRotation || 0 : 0
162
+ }, ${tick.to.x}, ${tick.to.y})`}
163
+ verticalAnchor={'middle'}
164
+ textAnchor={'end'}
165
+ fontSize={tickLabelFontSize}
166
+ >
167
+ {tick.formattedValue}
168
+ </Text>
169
+ )}
170
+
171
+ {orientation === 'horizontal' &&
172
+ visualizationType === 'Bar' &&
173
+ config.yAxis.labelPlacement === 'On Date/Category Axis' &&
174
+ !config.yAxis.hideLabel &&
175
+ (() => {
176
+ const barGroupCount =
177
+ config.visualizationSubType === 'stacked' ? 1 : config.runtime.seriesKeys.length
178
+
179
+ // Calculate barHeight based on chart type (regular bar vs lollipop)
180
+ let barHeight
181
+ if (config.isLollipopChart) {
182
+ const lollipopBarWidth = LOLLIPOP_SIZES[config.lollipopSize] || LOLLIPOP_SIZES.small
183
+ barHeight = lollipopBarWidth * barGroupCount
184
+ } else {
185
+ barHeight = Number(config.barHeight) * barGroupCount
186
+ }
187
+
188
+ const totalBarHeight = barHeight + Number(config.barSpace)
189
+ const barGroupY = i === 0 ? 0 : totalBarHeight * i
190
+ const labelCenterY = barGroupY + barHeight / 2
191
+
192
+ return (
193
+ <Text
194
+ x={tick.from.x - yAxisWidth + horizontalYAxisLabelSpace}
195
+ y={labelCenterY}
196
+ verticalAnchor={'middle'}
197
+ textAnchor={'start'}
198
+ fontSize={tickLabelFontSize}
199
+ width={categoryLabelSpace}
200
+ lineHeight={'1.2em'}
201
+ >
202
+ {tick.formattedValue}
203
+ </Text>
204
+ )
205
+ })()}
206
+
207
+ {orientation === 'horizontal' &&
208
+ visualizationType !== 'Bar' &&
209
+ visualizationSubType === 'stacked' &&
210
+ config.yAxis.labelPlacement === 'On Date/Category Axis' &&
211
+ !config.yAxis.hideLabel && (
212
+ <Text
213
+ transform={`translate(${tick.to.x - 5}, ${
214
+ tick.to.y - minY + (Number(config.barHeight) - BAR_MIN_HEIGHT) / 2
215
+ }) rotate(-${runtime.horizontal ? runtime.yAxis.tickRotation : 0})`}
216
+ verticalAnchor={'start'}
217
+ textAnchor={'end'}
218
+ fontSize={tickLabelFontSize}
219
+ >
220
+ {tick.formattedValue}
221
+ </Text>
222
+ )}
223
+
224
+ {orientation === 'horizontal' && visualizationType === 'Paired Bar' && !config.yAxis.hideLabel && (
225
+ <Text
226
+ transform={`translate(${tick.to.x - 5}, ${
227
+ tick.to.y - minY + Number(config.barHeight) / 2
228
+ }) rotate(-${runtime.horizontal ? runtime.yAxis.tickRotation : 0})`}
229
+ textAnchor={'end'}
230
+ verticalAnchor='middle'
231
+ fontSize={tickLabelFontSize}
232
+ >
233
+ {tick.formattedValue}
234
+ </Text>
235
+ )}
236
+ {orientation === 'horizontal' &&
237
+ visualizationType === 'Deviation Bar' &&
238
+ !config.yAxis.hideLabel && (
239
+ <Text
240
+ transform={`translate(${tick.to.x - 5}, ${
241
+ config.isLollipopChart
242
+ ? tick.to.y - minY + 2
243
+ : tick.to.y - minY + Number(config.barHeight) / 2
244
+ }) rotate(-${runtime.horizontal ? runtime.yAxis.tickRotation : 0})`}
245
+ textAnchor={'end'}
246
+ verticalAnchor='middle'
247
+ fontSize={tickLabelFontSize}
248
+ >
249
+ {tick.formattedValue}
250
+ </Text>
251
+ )}
252
+
253
+ {orientation === 'vertical' && visualizationType === 'Bump Chart' && !config.yAxis.hideLabel && (
254
+ <>
255
+ <Text
256
+ display={config.useLogScale ? showTicks : 'block'}
257
+ dx={config.useLogScale ? -6 : 0}
258
+ x={config.runtime.horizontal ? tick.from.x + 2 : tick.to.x - 8.5}
259
+ y={tick.to.y - 13 + (config.runtime.horizontal ? horizontalTickOffset : 0)}
260
+ angle={-Number(config.yAxis.tickRotation) || 0}
261
+ verticalAnchor={config.runtime.horizontal ? 'start' : 'middle'}
262
+ textAnchor={config.runtime.horizontal ? 'start' : 'end'}
263
+ fill={config.yAxis.tickLabelColor}
264
+ fontSize={tickLabelFontSize}
265
+ >
266
+ {config.runtime.seriesLabelsAll[tick.formattedValue - 1]}
267
+ </Text>
268
+
269
+ {(seriesHighlight.length === 0 ||
270
+ seriesHighlight.includes(config.runtime.seriesLabelsAll[tick.formattedValue - 1])) && (
271
+ <rect
272
+ x={0 - yAxisWidth}
273
+ y={tick.to.y - 8 + (config.runtime.horizontal ? horizontalTickOffset : 7)}
274
+ width={yAxisWidth + xScale(xScale.domain()[0])}
275
+ height='2'
276
+ fill={colorScale(config.runtime.seriesLabelsAll[tick.formattedValue - 1])}
277
+ />
278
+ )}
279
+ </>
280
+ )}
281
+ {orientation === 'vertical' &&
282
+ visualizationType !== 'Paired Bar' &&
283
+ visualizationType !== 'Bump Chart' &&
284
+ !config.yAxis.hideLabel && (
285
+ <>
286
+ {/* INLINE LABEL BEHAVIOR: Dom suffix for 'inlineLabel' behavior */}
287
+ {/* inline label is shown alone and is allowed to 'overflow' to the right */}
288
+ {/* SPECIAL ONE CHAR CASE: a one character inlineLabel does not overflow */}
289
+ {/* IF VALUES ON LINE: suffix is combined with value to avoid having to calculate varying (now left-aligned) value widths */}
290
+ {inlineLabel && lastTick && !labelsAboveGridlines && (
291
+ <BlurStrokeText
292
+ innerRef={suffixRef}
293
+ display={isLogarithmicAxis ? showTicks : 'block'}
294
+ dx={isLogarithmicAxis ? -6 : 0}
295
+ x={labelX}
296
+ y={labelY}
297
+ angle={-Number(config.yAxis.tickRotation) || 0}
298
+ verticalAnchor={labelVerticalAnchor}
299
+ textAnchor={inlineLabelHasNoSpace ? 'end' : 'start'}
300
+ fill={config.yAxis.tickLabelColor}
301
+ stroke={'#fff'}
302
+ paintOrder={'stroke'} // keeps stroke under fill
303
+ strokeLinejoin='round'
304
+ style={{ whiteSpace: 'pre-wrap' }} // prevents leading spaces from being trimmed
305
+ fontSize={tickLabelFontSize}
306
+ >
307
+ {inlineLabel}
308
+ </BlurStrokeText>
309
+ )}
310
+
311
+ {/* VALUE */}
312
+ <BlurStrokeText
313
+ innerRef={el => lastTick && topYLabelRef && (topYLabelRef.current = el)}
314
+ display={isLogarithmicAxis ? showTicks : 'block'}
315
+ dx={isLogarithmicAxis ? -6 : 0}
316
+ x={inlineLabelHasNoSpace ? labelX - suffixWidth : labelX}
317
+ y={labelY + (config.runtime.horizontal ? horizontalTickOffset : 0)}
318
+ angle={-Number(config.yAxis.tickRotation) || 0}
319
+ verticalAnchor={config.runtime.horizontal ? 'start' : labelVerticalAnchor}
320
+ textAnchor={config.runtime.horizontal || labelsAboveGridlines ? 'start' : 'end'}
321
+ fill={config.yAxis.tickLabelColor}
322
+ stroke={'#fff'}
323
+ disableStroke={!labelsAboveGridlines}
324
+ strokeLinejoin='round'
325
+ paintOrder={'stroke'} // keeps stroke under fill
326
+ style={{ whiteSpace: 'pre-wrap' }} // prevents leading spaces from being trimmed
327
+ fontSize={tickLabelFontSize}
328
+ >
329
+ {`${formattedValue}${combineDomInlineLabelWithValue ? inlineLabel : ''}`}
330
+ </BlurStrokeText>
331
+ </>
332
+ )}
333
+ </Group>
334
+ )
335
+ })}
336
+
337
+ {/* Horizon Chart series labels - rendered outside ticks loop since it uses custom band positioning */}
338
+ {visualizationType === 'Horizon Chart' &&
339
+ (() => {
340
+ const horizonConfig = { ...HORIZON_DEFAULTS, ...config.horizon }
341
+
342
+ const seriesKeys =
343
+ runtime?.seriesKeys && runtime.seriesKeys.length > 0
344
+ ? runtime.seriesKeys
345
+ : config.series?.map((s: any) => s.dataKey) || []
346
+ if (seriesKeys.length === 0) return null
347
+
348
+ const { bandHeight, getRowY } = calculateHorizonBands(
349
+ seriesKeys.length,
350
+ yMax,
351
+ horizonConfig.bandGap,
352
+ horizonConfig.bottomPadding
353
+ )
354
+
355
+ return seriesKeys.map((seriesKey, index) => {
356
+ const labelY = getRowY(index) + bandHeight / 2
357
+ const labelX = -DEFAULT_TICK_LENGTH - TICK_LABEL_MARGIN_RIGHT
358
+ return (
359
+ <Group key={`horizon-tick-${seriesKey}`} className='horizon-axis-tick'>
360
+ {/* Tick mark */}
361
+ {!runtime.yAxis.hideTicks && (
362
+ <Line
363
+ from={{ x: 0, y: labelY }}
364
+ to={{ x: -DEFAULT_TICK_LENGTH, y: labelY }}
365
+ stroke={(config.yAxis as any).tickColor || '#333'}
366
+ />
367
+ )}
368
+ {/* Series label */}
369
+ {!config.yAxis.hideLabel && (
370
+ <Text
371
+ x={labelX}
372
+ y={labelY}
373
+ textAnchor='end'
374
+ verticalAnchor='middle'
375
+ fontSize={tickLabelFontSize}
376
+ fill={(config.yAxis as any).tickLabelColor || '#1c1d1f'}
377
+ >
378
+ {runtime?.seriesLabels?.[seriesKey] || seriesKey}
379
+ </Text>
380
+ )}
381
+ </Group>
382
+ )
383
+ })
384
+ })()}
385
+
386
+ <Text
387
+ className='y-label'
388
+ textAnchor='middle'
389
+ verticalAnchor='start'
390
+ transform={`translate(${-1 * yAxisWidth + yLabelOffset}, ${axisCenter}) rotate(-90)`}
391
+ fontWeight='bold'
392
+ fill={config.yAxis.labelColor}
393
+ fontSize={axisLabelFontSize}
394
+ >
395
+ {!config.hideYAxisLabel ? props.label : null}
396
+ </Text>
397
+ </Group>
398
+ )
399
+ }}
400
+ </VisxAxisLeft>
401
+ )
402
+ }
403
+
404
+ export default LeftAxis
@@ -0,0 +1,77 @@
1
+ import React, { useContext } from 'react'
2
+ import { AxisLeft } from '@visx/axis'
3
+ import { Group } from '@visx/group'
4
+ import { Line } from '@visx/shape'
5
+ import { Text } from '@visx/text'
6
+ import ConfigContext from '../../ConfigContext'
7
+
8
+ interface LeftAxisGridlinesProps {
9
+ yScale: any
10
+ xMax: number
11
+ yAxisWidth: number
12
+ numTicks: number
13
+ yLabelOffset: number
14
+ axisLabelFontSize: number
15
+ }
16
+
17
+ const LeftAxisGridlines: React.FC<LeftAxisGridlinesProps> = ({
18
+ yScale,
19
+ xMax,
20
+ yAxisWidth,
21
+ numTicks,
22
+ yLabelOffset,
23
+ axisLabelFontSize
24
+ }) => {
25
+ const { config } = useContext(ConfigContext)
26
+ const { runtime } = config
27
+
28
+ const isLogarithmicAxis = config.yAxis.type === 'logarithmic'
29
+
30
+ return (
31
+ <AxisLeft scale={yScale} left={yAxisWidth - config.yAxis.axisPadding} numTicks={numTicks}>
32
+ {props => {
33
+ const axisCenter =
34
+ config.orientation === 'horizontal'
35
+ ? Math.abs(props.axisToPoint.y - props.axisFromPoint.y) / 2
36
+ : (props.axisFromPoint.y - props.axisToPoint.y) / 2
37
+ return (
38
+ <Group className='left-axis'>
39
+ {props.ticks.map((tick, i) => {
40
+ const showTicks = String(tick.value).startsWith('1') || tick.value === 0.1 ? 'block' : 'none'
41
+ const hideFirstGridLine = tick.index === 0 && tick.value === 0 && config.xAxis.hideAxis
42
+
43
+ return (
44
+ <Group key={`vx-tick-${tick.value}-${i}`} className={'vx-axis-tick'}>
45
+ {runtime.yAxis.gridLines && !hideFirstGridLine ? (
46
+ <Line
47
+ key={`${tick.value}--hide-hideGridLines`}
48
+ display={(isLogarithmicAxis && showTicks).toString()}
49
+ from={{ x: tick.from.x + xMax, y: tick.from.y }}
50
+ to={tick.from}
51
+ stroke='#d6d6d6'
52
+ />
53
+ ) : (
54
+ ''
55
+ )}
56
+ </Group>
57
+ )
58
+ })}
59
+ <Text
60
+ className='y-label'
61
+ textAnchor='middle'
62
+ verticalAnchor='start'
63
+ transform={`translate(${-1 * yAxisWidth + yLabelOffset}, ${axisCenter}) rotate(-90)`}
64
+ fontWeight='bold'
65
+ fill={config.yAxis.labelColor}
66
+ fontSize={axisLabelFontSize}
67
+ >
68
+ {!config.hideYAxisLabel ? props.label : null}
69
+ </Text>
70
+ </Group>
71
+ )
72
+ }}
73
+ </AxisLeft>
74
+ )
75
+ }
76
+
77
+ export default LeftAxisGridlines
@@ -0,0 +1,192 @@
1
+ import React, { useContext, MutableRefObject } from 'react'
2
+ import { AxisBottom } from '@visx/axis'
3
+ import { Group } from '@visx/group'
4
+ import { Line } from '@visx/shape'
5
+ import { Text } from '@visx/text'
6
+ import { ScaleLinear } from 'd3-scale'
7
+
8
+ import ConfigContext from '../../ConfigContext'
9
+ import { isDateScale } from '@cdc/core/helpers/cove/date'
10
+ import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
11
+
12
+ // Constants
13
+ const BOTTOM_LABEL_PADDING = 9
14
+ const X_TICK_LABEL_PADDING = 4.5
15
+ const DEFAULT_MAX_TICK_ROTATION = 90
16
+ const BASE_TICK_WIDTH_ACCUMULATOR = 100
17
+
18
+ type PairedBarAxisProps = {
19
+ g1xScale: ScaleLinear<number, number>
20
+ g2xScale: ScaleLinear<number, number>
21
+ yMax: number
22
+ xMax: number
23
+ yAxisWidth: number
24
+ bottomLabelStart: number
25
+ tickLabelFontSize: number
26
+ axisLabelFontSize: number
27
+ axisBottomRef: MutableRefObject<SVGGElement | null>
28
+ xAxisLabelRefs: MutableRefObject<(SVGTextElement | null)[]>
29
+ tickLabelFont: string
30
+ }
31
+
32
+ /**
33
+ * Specialized axis component for Paired Bar charts.
34
+ * Renders two mirrored AxisBottom components with shared tick formatting logic.
35
+ * Extracted from LinearChart.tsx
36
+ */
37
+ export const PairedBarAxis: React.FC<PairedBarAxisProps> = ({
38
+ g1xScale,
39
+ g2xScale,
40
+ yMax,
41
+ xMax,
42
+ yAxisWidth,
43
+ bottomLabelStart,
44
+ tickLabelFontSize,
45
+ axisLabelFontSize,
46
+ axisBottomRef,
47
+ xAxisLabelRefs,
48
+ tickLabelFont
49
+ }) => {
50
+ const { config, formatDate, formatNumber } = useContext(ConfigContext)
51
+ const { runtime } = config
52
+
53
+ const axisMaxHeight = bottomLabelStart + BOTTOM_LABEL_PADDING
54
+
55
+ /**
56
+ * Determines if ticks are overlapping to trigger responsive rotation
57
+ */
58
+ const getTickPositions = (ticks: any[], xScale: ScaleLinear<number, number>): boolean => {
59
+ if (!ticks.length) return false
60
+
61
+ // Filter out first index
62
+ const filteredTicks = ticks.filter(tick => tick.index !== 0)
63
+ const numberOfTicks = filteredTicks?.length
64
+ const xMaxHalf = xScale.range()[0] || xMax / 2
65
+
66
+ const tickWidthAll = filteredTicks.map(tick =>
67
+ getTextWidth(tick.formattedValue ?? formatNumber(tick.value, 'left', true), tickLabelFont)
68
+ )
69
+ const sumOfTickWidth = tickWidthAll.reduce((a, b) => a + b, BASE_TICK_WIDTH_ACCUMULATOR)
70
+ const spaceBetweenEachTick = (xMaxHalf - sumOfTickWidth) / numberOfTicks
71
+
72
+ // Determine the position of each tick
73
+ const positions = [0]
74
+ for (let i = 1; i < tickWidthAll.length; i++) {
75
+ positions[i] = positions[i - 1] + tickWidthAll[i - 1] + spaceBetweenEachTick
76
+ }
77
+
78
+ // Check if ticks are overlapping
79
+ let isTicksOverlapping = false
80
+ tickWidthAll.forEach((_, i) => {
81
+ if (positions[i] + tickWidthAll[i] > positions[i + 1]) {
82
+ isTicksOverlapping = true
83
+ }
84
+ })
85
+ return isTicksOverlapping
86
+ }
87
+
88
+ /**
89
+ * Renders tick elements for either the left or right paired bar section
90
+ */
91
+ const renderTickGroup = (
92
+ tick: any,
93
+ index: number,
94
+ ticks: any[],
95
+ xScale: ScaleLinear<number, number>,
96
+ isRightSection: boolean
97
+ ) => {
98
+ const isTicksOverlapping = getTickPositions(ticks, xScale)
99
+ const maxTickRotation = Number(config.xAxis.maxTickRotation) || DEFAULT_MAX_TICK_ROTATION
100
+ const isResponsiveTicks = config.isResponsiveTicks && isTicksOverlapping
101
+ const angle = tick.index !== 0 && (isResponsiveTicks ? maxTickRotation : Number(config.yAxis.tickRotation))
102
+ const textAnchor = angle && tick.index !== 0 ? 'end' : 'middle'
103
+
104
+ // Skip first tick on right section to avoid overlapping 0's
105
+ if (isRightSection && !index) return <React.Fragment key={`vx-tick-empty-${index}`} />
106
+
107
+ return (
108
+ <Group key={`vx-tick-${tick.value}-${index}`} className='vx-axis-tick'>
109
+ {!runtime.yAxis.hideTicks && <Line from={tick.from} to={tick.to} stroke='#333' />}
110
+ {!runtime.yAxis.hideLabel && (
111
+ <Text
112
+ innerRef={!isRightSection ? el => (xAxisLabelRefs.current[index] = el) : undefined}
113
+ x={tick.to.x}
114
+ y={tick.to.y + (isRightSection ? X_TICK_LABEL_PADDING : 0)}
115
+ angle={-angle}
116
+ verticalAnchor={angle ? 'middle' : 'start'}
117
+ textAnchor={textAnchor}
118
+ fontSize={tickLabelFontSize}
119
+ >
120
+ {tick.formattedValue ?? formatNumber(tick.value, 'left', true)}
121
+ </Text>
122
+ )}
123
+ </Group>
124
+ )
125
+ }
126
+
127
+ return (
128
+ <>
129
+ {/* Left section axis */}
130
+ <AxisBottom
131
+ top={yMax}
132
+ left={yAxisWidth}
133
+ label={runtime.xAxis.label}
134
+ tickFormat={isDateScale(runtime.xAxis) ? formatDate : tick => formatNumber(tick, 'left', true)}
135
+ scale={g1xScale}
136
+ stroke='#333'
137
+ tickStroke='#333'
138
+ numTicks={runtime.xAxis.numTicks || undefined}
139
+ >
140
+ {props => (
141
+ <Group className='bottom-axis'>
142
+ {props.ticks.map((tick, i) => renderTickGroup(tick, i, props.ticks, g1xScale, false))}
143
+ {!runtime.yAxis.hideAxis && <Line from={props.axisFromPoint} to={props.axisToPoint} stroke='#333' />}
144
+ </Group>
145
+ )}
146
+ </AxisBottom>
147
+
148
+ {/* Right section axis */}
149
+ <AxisBottom
150
+ innerRef={axisBottomRef}
151
+ top={yMax}
152
+ left={yAxisWidth}
153
+ label={runtime.xAxis.label}
154
+ tickFormat={
155
+ isDateScale(runtime.xAxis)
156
+ ? formatDate
157
+ : runtime.xAxis.dataKey !== 'Year'
158
+ ? tick => formatNumber(tick, 'left', true)
159
+ : tick => tick
160
+ }
161
+ scale={g2xScale}
162
+ stroke='#333'
163
+ tickStroke='#333'
164
+ numTicks={runtime.xAxis.numTicks || undefined}
165
+ >
166
+ {props => (
167
+ <>
168
+ <Group className='bottom-axis'>
169
+ {props.ticks.map((tick, i) => renderTickGroup(tick, i, props.ticks, g2xScale, true))}
170
+ {!runtime.yAxis.hideAxis && <Line from={props.axisFromPoint} to={props.axisToPoint} stroke='#333' />}
171
+ </Group>
172
+ <Group>
173
+ <Text
174
+ className='x-axis-title-label'
175
+ x={xMax / 2}
176
+ y={axisMaxHeight}
177
+ stroke='#333'
178
+ textAnchor='middle'
179
+ verticalAnchor='start'
180
+ fontSize={axisLabelFontSize}
181
+ >
182
+ {!config.hideXAxisLabel ? runtime.xAxis.label : null}
183
+ </Text>
184
+ </Group>
185
+ </>
186
+ )}
187
+ </AxisBottom>
188
+ </>
189
+ )
190
+ }
191
+
192
+ export default PairedBarAxis