@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
@@ -2,6 +2,7 @@ import React, {
2
2
  forwardRef,
3
3
  useContext,
4
4
  useEffect,
5
+ useLayoutEffect,
5
6
  useImperativeHandle,
6
7
  useMemo,
7
8
  useRef,
@@ -10,46 +11,35 @@ import React, {
10
11
  } from 'react'
11
12
 
12
13
  // Libraries
13
- import { AxisLeft, AxisBottom, AxisRight, AxisTop } from '@visx/axis'
14
+ import { AxisTop } from '@visx/axis'
14
15
  import { Group } from '@visx/group'
15
16
  import { Line, Bar } from '@visx/shape'
16
17
  import { Tooltip as ReactTooltip } from 'react-tooltip'
17
18
  import 'react-tooltip/dist/react-tooltip.css'
18
19
  import { Text } from '@visx/text'
19
20
  import { useTooltip, TooltipWithBounds } from '@visx/tooltip'
20
- import _ from 'lodash'
21
-
21
+ import ResizeObserver from 'resize-observer-polyfill'
22
22
  // CDC Components
23
23
  import { isDateScale } from '@cdc/core/helpers/cove/date'
24
- import { AreaChartStacked } from './AreaChart'
25
- import BarChart from './BarChart'
26
24
  import ConfigContext from '../ConfigContext'
27
- import BoxPlotVertical from './BoxPlot/BoxPlot.Vertical'
28
- import BoxPlotHorizontal from './BoxPlot/BoxPlot.Horizontal'
29
- import ScatterPlot from './ScatterPlot'
30
- import DeviationBar from './DeviationBar'
31
25
  import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
32
- import Forecasting from './Forecasting'
33
- import LineChart from './LineChart'
34
- import ForestPlot from './ForestPlot'
35
- import PairedBarChart from './PairedBarChart'
36
26
  import useIntersectionObserver from '../hooks/useIntersectionObserver'
37
27
  import Regions from './Regions'
38
- import CategoricalYAxis from './Axis/Categorical.Axis'
28
+ import { CategoricalYAxis, LeftAxis, LeftAxisGridlines, BottomAxis, PairedBarAxis, RightAxis } from './Axis'
39
29
  import BrushSelector from './Brush/BrushSelector'
40
- import WarmingStripes from './WarmingStripes'
30
+ import VisualizationRenderer from './LinearChart/VisualizationRenderer'
31
+ import { TYPES_WITHOUT_GRID, TYPES_WITH_TOOLTIP_GUIDES } from './LinearChart/linearChart.constants'
32
+ import { useTickFormatters } from './LinearChart/utils/tickFormatting'
41
33
 
42
34
  // Helpers
43
35
  import { isLegendWrapViewport, isMobileFontViewport } from '@cdc/core/helpers/viewports'
44
- import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
45
36
  import { calcInitialHeight } from '../helpers/sizeHelpers'
46
- import { filterAndShiftLinearDateTicks } from '../helpers/filterAndShiftLinearDateTicks'
47
37
  import { calculateHorizontalBarCategoryLabelWidth } from '../helpers/calculateHorizontalBarCategoryLabelWidth'
48
38
 
49
39
  // Hooks
50
40
  import useReduceData from '../hooks/useReduceData'
51
41
  import useRightAxis from '../hooks/useRightAxis'
52
- import useScales, { getTickValues } from '../hooks/useScales'
42
+ import useScales from '../hooks/useScales'
53
43
  import { useProgrammaticTooltip } from '../hooks/useProgrammaticTooltip'
54
44
  import { useSmallMultipleSynchronization } from '../hooks/useSmallMultipleSynchronization'
55
45
 
@@ -58,7 +48,6 @@ import { useTooltip as useCoveTooltip } from '../hooks/useTooltip'
58
48
  import { useChartHoverAnalytics } from '../hooks/useChartHoverAnalytics'
59
49
  import { useEditorPermissions } from './EditorPanel/useEditorPermissions'
60
50
  import Annotation from './Annotations'
61
- import { BlurStrokeText } from '@cdc/core/components/BlurStrokeText'
62
51
  import { countNumOfTicks } from '../helpers/countNumOfTicks'
63
52
  import HoverLine from './HoverLine/HoverLine'
64
53
  import { SmallMultiples } from './SmallMultiples'
@@ -68,15 +57,36 @@ type LinearChartProps = {
68
57
  parentHeight: number
69
58
  }
70
59
 
60
+ // Axis and tick constants
71
61
  const BOTTOM_LABEL_PADDING = 9
72
62
  const X_TICK_LABEL_PADDING = 4.5
73
63
  const DEFAULT_TICK_LENGTH = 8
74
- const MONTH_AS_MS = 1000 * 60 * 60 * 24 * 30
64
+ const DEFAULT_MAX_TICK_ROTATION = 90
65
+
66
+ // Font sizes
75
67
  const TICK_LABEL_FONT_SIZE = 16
76
68
  const TICK_LABEL_FONT_SIZE_SMALL = 13
77
69
  const AXIS_LABEL_FONT_SIZE = 18
78
70
  const AXIS_LABEL_FONT_SIZE_SMALL = 14
79
- const TICK_LABEL_MARGIN_RIGHT = 4.5
71
+
72
+ // Label positioning constants
73
+ const BELOW_BAR_TEXT_OFFSET = -6.5
74
+ const LABEL_PADDING_OFFSET = 8
75
+
76
+ // Brush constants
77
+ const BRUSH_HEIGHT = 70
78
+ const BRUSH_MARGIN = 10
79
+ const BRUSH_MIN_WIDTH = 100
80
+
81
+ // Tooltip constants
82
+ const TOOLTIP_EDGE_BUFFER = 10
83
+ const TOOLTIP_OFFSET = 6
84
+
85
+ // Chart-specific constants
86
+ const WARMING_STRIPES_HEIGHT = 78
87
+
88
+ // Time constants
89
+ const MONTH_AS_MS = 1000 * 60 * 60 * 24 * 30
80
90
 
81
91
  type TooltipData = {
82
92
  dataXPosition?: number
@@ -95,12 +105,9 @@ type UseTooltipReturn<T = TooltipData> = {
95
105
  const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, parentWidth }, svgRef) => {
96
106
  // prettier-ignore
97
107
  const {
98
- colorScale,
99
108
  config,
100
- convertLineToBarGraph,
101
109
  currentViewport,
102
110
  vizViewport,
103
- dimensions,
104
111
  formatDate,
105
112
  formatNumber,
106
113
  handleChartAriaLabels,
@@ -108,33 +115,18 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
108
115
  handleDragStateChange,
109
116
  interactionLabel,
110
117
  isDraggingAnnotation,
111
- isEditor,
112
118
  legendRef,
113
119
  parseDate,
114
120
  parentRef,
115
121
  tableData,
116
122
  transformedData: data,
117
- seriesHighlight
118
123
  } = useContext(ConfigContext)
119
124
 
120
125
  // CONFIG
121
126
  // todo: start destructuring this file for conciseness
122
- const {
123
- heights,
124
- visualizationType,
125
- visualizationSubType,
126
- orientation,
127
- xAxis,
128
- yAxis,
129
- runtime,
130
- legend,
131
- forestPlot,
132
- brush,
133
- dataFormat,
134
- debugSvg
135
- } = config
136
-
137
- const { labelsAboveGridlines, hideAxis, inlineLabel } = config.yAxis
127
+ const { visualizationType, orientation, xAxis, yAxis, runtime, legend, forestPlot, debugSvg } = config
128
+
129
+ const { inlineLabel } = config.yAxis
138
130
 
139
131
  // HOOKS % STATES
140
132
  // When brush is active, use tableData (full dataset) for min/max calculation
@@ -144,11 +136,23 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
144
136
 
145
137
  const { visSupportsSmallMultiples } = useEditorPermissions()
146
138
  const { hasTopAxis } = getTopAxis(config)
139
+
140
+ // Increment on tableData change to force BrushSelector remount when filters change
141
+ const brushKeyRef = useRef(0)
142
+ const prevTableDataRef = useRef(tableData)
143
+ if (prevTableDataRef.current !== tableData) {
144
+ prevTableDataRef.current = tableData
145
+ brushKeyRef.current += 1
146
+ }
147
+
147
148
  const [animatedChart, setAnimatedChart] = useState(false)
148
149
  const [showHoverLine, setShowHoverLine] = useState(false)
149
150
  const [point, setPoint] = useState({ x: 0, y: 0 })
150
151
  const [suffixWidth, setSuffixWidth] = useState(0)
151
152
  const [calculatedSvgHeight, setCalculatedSvgHeight] = useState<number | null>(null)
153
+ const [axisUpdateKey, setAxisUpdateKey] = useState(0)
154
+ const [axisBottomSizeKey, setAxisBottomSizeKey] = useState(0)
155
+ const [synchronizedXValue, setSynchronizedXValue] = useState<any>(null)
152
156
 
153
157
  // REFS
154
158
  const axisBottomRef = useRef(null)
@@ -183,29 +187,43 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
183
187
 
184
188
  // height before bottom axis
185
189
  const initialHeight = useMemo(
186
- () => (visualizationType === 'Warming Stripes' ? 78 : calcInitialHeight(config, currentViewport)),
187
- [config, currentViewport, parentHeight, config.heights?.vertical, config.heights?.horizontal, visualizationType]
190
+ () =>
191
+ visualizationType === 'Warming Stripes' ? WARMING_STRIPES_HEIGHT : calcInitialHeight(config, currentViewport),
192
+ [
193
+ visualizationType,
194
+ currentViewport,
195
+ config.heights?.vertical,
196
+ config.heights?.horizontal,
197
+ config.heights?.mobileVertical,
198
+ config.orientation
199
+ ]
188
200
  )
189
201
  const forestHeight = useMemo(() => initialHeight + forestRowsHeight, [initialHeight, forestRowsHeight])
190
202
 
191
203
  // Used to calculate the y position of the x-axis title
204
+ // Dependencies trigger recalc when axis config changes (affects label rendering/size)
192
205
  const bottomLabelStart = useMemo(() => {
193
206
  xAxisLabelRefs.current = xAxisLabelRefs.current?.filter(label => label)
194
207
  if (!xAxisLabelRefs.current.length) return
195
208
  const tallestLabel = Math.max(...xAxisLabelRefs.current.map(label => label.getBBox().height))
196
209
  return tallestLabel + X_TICK_LABEL_PADDING + DEFAULT_TICK_LENGTH
197
- }, [parentWidth, config.xAxis, xAxisLabelRefs.current, config.xAxis.tickRotation])
210
+ }, [parentWidth, config.xAxis.tickRotation, config.xAxis.hideLabel, xAxisLabelRefs.current, axisUpdateKey])
198
211
 
199
212
  const yMax = initialHeight + forestRowsHeight
200
213
 
201
214
  const isNoDataAvailable = config.filters?.length > 0 && data.length === 0
202
215
 
203
- const getXAxisData = d =>
204
- isDateScale(config.runtime.xAxis)
205
- ? parseDate(d[config.runtime.originalXAxis.dataKey]).getTime()
206
- : d[config.runtime.originalXAxis.dataKey]
207
- const getYAxisData = (d, seriesKey) => d[seriesKey]
208
- const xAxisDataMapped = data.map(d => getXAxisData(d))
216
+ // Memoized data accessors to prevent unnecessary re-renders
217
+ const getXAxisData = useCallback(
218
+ d =>
219
+ isDateScale(config.runtime.xAxis)
220
+ ? parseDate(d[config.runtime.originalXAxis.dataKey]).getTime()
221
+ : d[config.runtime.originalXAxis.dataKey],
222
+ [config.runtime.xAxis, config.runtime.originalXAxis.dataKey, parseDate]
223
+ )
224
+ const getYAxisData = useCallback((d, seriesKey) => d[seriesKey], [])
225
+
226
+ const xAxisDataMapped = useMemo(() => data.map(d => getXAxisData(d)), [data, getXAxisData])
209
227
 
210
228
  // Get unique x-axis values (for cases where multiple series share the same x-axis value)
211
229
  // This is important for brush filtering where we want to count unique dates, not total data points
@@ -221,9 +239,33 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
221
239
  }
222
240
  return result
223
241
  }, [xAxisDataMapped])
242
+
243
+ // Force update x axis ticks when filtering
244
+ useLayoutEffect(() => {
245
+ setAxisUpdateKey(prev => prev + 1)
246
+ }, [data.length, xAxisDataMapped?.[0], xAxisDataMapped?.[xAxisDataMapped.length - 1]])
247
+
248
+ // Recompute heights when bottom axis size changes (e.g., font load or wrap).
249
+ useEffect(() => {
250
+ const axisBottomEl = axisBottomRef.current
251
+ if (!axisBottomEl) return
252
+ const observer = new ResizeObserver(() => {
253
+ setAxisBottomSizeKey(prev => prev + 1)
254
+ })
255
+ observer.observe(axisBottomEl)
256
+ return () => observer.disconnect()
257
+ }, [axisBottomRef.current])
258
+
224
259
  const { yScaleRight, hasRightAxis } = useRightAxis({ config, yMax, data })
225
260
 
226
- const xMax = parentWidth - Number(runtime.yAxis.size) - (hasRightAxis ? config.yAxis.rightAxisSize : 0)
261
+ // State for computed y-axis width - allows re-render when horizontal bar label space is calculated
262
+ const [computedYAxisWidth, setComputedYAxisWidth] = useState<number | null>(null)
263
+
264
+ // Use computed width if available, otherwise fall back to config value
265
+ const yAxisWidth = computedYAxisWidth ?? Number(runtime.yAxis.size)
266
+
267
+ // Chart width calculation using the current y-axis width
268
+ const xMax = parentWidth - yAxisWidth - (hasRightAxis ? config.yAxis.rightAxisSize : 0)
227
269
 
228
270
  const {
229
271
  xScale,
@@ -233,10 +275,9 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
233
275
  g2xScale,
234
276
  xScaleNoPadding,
235
277
  xScaleAnnotation,
278
+ yScaleAnnotation,
236
279
  min,
237
- max,
238
- leftMax,
239
- rightMax
280
+ max
240
281
  } = useScales({
241
282
  data,
242
283
  tableData,
@@ -252,6 +293,16 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
252
293
  currentViewport
253
294
  })
254
295
 
296
+ // Consolidated tick formatters
297
+ const { handleLeftTickFormatting, handleBottomTickFormatting } = useTickFormatters({
298
+ isLogarithmicAxis,
299
+ orientation,
300
+ visualizationType,
301
+ min,
302
+ max,
303
+ shouldAbbreviate
304
+ })
305
+
255
306
  // Calculate category label space for horizontal bar charts
256
307
  const categoryLabelSpace = useMemo(() => {
257
308
  return calculateHorizontalBarCategoryLabelWidth({
@@ -266,9 +317,19 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
266
317
  }, [isHorizontal, config.visualizationType, config.yAxis.labelPlacement, yScale, parentWidth])
267
318
 
268
319
  const horizontalYAxisLabelSpace = runtime.yAxis.label && !config.hideYAxisLabel ? 30 : 0
269
- if (isHorizontal && config.visualizationType === 'Bar') {
270
- runtime.yAxis.size = categoryLabelSpace + horizontalYAxisLabelSpace
271
- }
320
+
321
+ // Update y-axis width state when computed value changes (for horizontal bar charts)
322
+ useEffect(() => {
323
+ if (isHorizontal && config.visualizationType === 'Bar') {
324
+ const newWidth = categoryLabelSpace + horizontalYAxisLabelSpace
325
+ if (newWidth !== computedYAxisWidth) {
326
+ setComputedYAxisWidth(newWidth)
327
+ }
328
+ } else if (computedYAxisWidth !== null) {
329
+ // Reset to null for non-horizontal bar charts so we use config value
330
+ setComputedYAxisWidth(null)
331
+ }
332
+ }, [isHorizontal, config.visualizationType, categoryLabelSpace, horizontalYAxisLabelSpace, computedYAxisWidth])
272
333
 
273
334
  const [yTickCount, xTickCount] = ['yAxis', 'xAxis'].map(axis =>
274
335
  countNumOfTicks({ axis, max, runtime, currentViewport, isHorizontal, data, config, min })
@@ -312,65 +373,19 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
312
373
  const useDateSpanMonths = isDateTime && dateSpanMonths > xTickCount && !config.runtime.xAxis.manual
313
374
 
314
375
  // GETTERS & FUNCTIONS
315
- const handleLeftTickFormatting = (tick, index, ticks) => {
316
- if (isLogarithmicAxis && tick === 0.1) {
317
- //when logarithmic scale applied change value of first tick
318
- tick = 0
319
- }
320
-
321
- if (config.data && !config.data[index] && visualizationType === 'Forest Plot') return
322
- if (config.visualizationType === 'Forest Plot') return config.data[index][config.xAxis.dataKey]
323
- if (isDateScale(runtime.yAxis)) return formatDate(parseDate(tick))
324
- if (orientation === 'vertical' && max - min < 3 && !config.dataFormat?.roundTo)
325
- return formatNumber(tick, 'left', shouldAbbreviate, false, false, '1', { index, length: ticks.length })
326
- if (orientation === 'vertical') {
327
- // TODO suggestion: pass all options as object key/values to allow for more flexibility
328
- return formatNumber(tick, 'left', shouldAbbreviate, false, false, undefined, { index, length: ticks.length })
329
- }
330
- return tick
331
- }
332
-
333
- const handleBottomTickFormatting = (tick, i, ticks) => {
334
- if (isLogarithmicAxis && tick === 0.1) {
335
- // when logarithmic scale applied change value FIRST of tick
336
- tick = 0
337
- }
338
-
339
- if (isDateScale(runtime.xAxis) && config.visualizationType !== 'Forest Plot') {
340
- return formatDate(tick, i, ticks)
341
- }
342
- if (orientation === 'horizontal' && config.visualizationType !== 'Forest Plot')
343
- return formatNumber(tick, 'left', shouldAbbreviate)
344
- if (config.xAxis.type === 'continuous' && config.visualizationType !== 'Forest Plot')
345
- return formatNumber(tick, 'bottom', shouldAbbreviate)
346
- if (config.visualizationType === 'Forest Plot')
347
- return formatNumber(
348
- tick,
349
- 'left',
350
- config.dataFormat.abbreviated,
351
- config.runtime.xAxis.prefix,
352
- config.runtime.xAxis.suffix,
353
- Number(config.dataFormat.roundTo)
354
- )
355
- return tick
356
- }
357
-
358
376
  const chartHasTooltipGuides = () => {
359
377
  const { visualizationType } = config
360
378
  if (visualizationType === 'Combo' && runtime.forecastingSeriesKeys > 0) return true
361
- if (visualizationType === 'Area Chart') return true
362
- if (visualizationType === 'Line') return true
363
- if (visualizationType === 'Bar') return true
364
- return false
379
+ return TYPES_WITH_TOOLTIP_GUIDES.includes(visualizationType as any)
365
380
  }
366
381
 
367
- const getManualStep = () => {
382
+ const getManualStep = useCallback(() => {
368
383
  let manualStep = config.xAxis.manualStep
369
384
  if (config.xAxis.viewportStepCount && config.xAxis.viewportStepCount[currentViewport]) {
370
385
  manualStep = config.xAxis.viewportStepCount[currentViewport]
371
386
  }
372
387
  return manualStep
373
- }
388
+ }, [config.xAxis.manualStep, config.xAxis.viewportStepCount, currentViewport])
374
389
 
375
390
  const smallMultiplesSync = useSmallMultipleSynchronization(xMax, yMax, getXValueFromCoordinate)
376
391
 
@@ -384,11 +399,17 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
384
399
  y
385
400
  })
386
401
 
387
- smallMultiplesSync.onMouseMove?.(event)
402
+ // Warming Stripes handles its own sync at the rect level since
403
+ // getXValueFromCoordinate won't map correctly due to data sampling
404
+ if (visualizationType !== 'Warming Stripes') {
405
+ smallMultiplesSync.onMouseMove?.(event)
406
+ }
388
407
  }
389
408
 
390
409
  const onMouseLeave = () => {
391
- smallMultiplesSync.onMouseLeave?.()
410
+ if (visualizationType !== 'Warming Stripes') {
411
+ smallMultiplesSync.onMouseLeave?.()
412
+ }
392
413
  }
393
414
 
394
415
  // Use custom hook to provide programmatic tooltip control for small multiples
@@ -399,24 +420,23 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
399
420
  setPoint,
400
421
  setShowHoverLine,
401
422
  handleTooltipMouseOver,
402
- hideTooltip
423
+ hideTooltip,
424
+ setSynchronizedXValue
403
425
  })
404
426
 
405
427
  // Make sure the chart is visible if in the editor
406
- /* eslint-disable react-hooks/exhaustive-deps */
407
428
  useEffect(() => {
408
- const element = document.querySelector('.isEditor')
429
+ const element = document.querySelector('.is-editor')
409
430
  if (element) {
410
- // parent element is visible
411
- setAnimatedChart(prevState => true)
431
+ setAnimatedChart(true)
412
432
  }
413
- }) /* eslint-disable-line */
433
+ }, [])
414
434
 
415
435
  // If the chart is in view, set to animate if it has not already played
416
436
  useEffect(() => {
417
437
  if (dataRef?.isIntersecting === true && config.animate) {
418
438
  setTimeout(() => {
419
- setAnimatedChart(prevState => true)
439
+ setAnimatedChart(true)
420
440
  }, 500)
421
441
  }
422
442
  }, [dataRef?.isIntersecting, config.animate])
@@ -461,9 +481,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
461
481
  const svgHeight = initialHeight + svgAdditionalHeight
462
482
 
463
483
  // Parent container height (includes brush if active)
464
- const brushHeight = 70
465
- const brushMargin = 10
466
- const brushHeightWithMargin = config.xAxis.brushActive ? brushHeight + brushMargin : 0
484
+ const brushHeightWithMargin = config.xAxis.brushActive ? BRUSH_HEIGHT + BRUSH_MARGIN : 0
467
485
  const parentHeight = svgHeight + brushHeightWithMargin
468
486
 
469
487
  if (!parentRef.current) return
@@ -494,11 +512,12 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
494
512
  }, [
495
513
  axisBottomRef.current,
496
514
  config,
497
- bottomLabelStart,
498
515
  config.xAxis.brushActive,
499
516
  currentViewport,
500
517
  topYLabelRef.current,
501
- initialHeight
518
+ initialHeight,
519
+ parentWidth,
520
+ axisBottomSizeKey
502
521
  ])
503
522
 
504
523
  useEffect(() => {
@@ -513,8 +532,8 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
513
532
 
514
533
  const rightSideRemainingSpace = parentWidth - dataXPosition
515
534
 
516
- const rightSide = rightSideRemainingSpace <= tooltipWidth && dataXPosition > parentWidth / 2 - 10
517
- const maxWidth = rightSide ? dataXPosition - 10 : parentWidth - (dataXPosition + 6)
535
+ const rightSide = rightSideRemainingSpace <= tooltipWidth && dataXPosition > parentWidth / 2 - TOOLTIP_EDGE_BUFFER
536
+ const maxWidth = rightSide ? dataXPosition - TOOLTIP_EDGE_BUFFER : parentWidth - (dataXPosition + TOOLTIP_OFFSET)
518
537
  tooltipRef.current.node.style.maxWidth = `${maxWidth}px`
519
538
  }, [tooltipOpen, tooltipData])
520
539
 
@@ -531,150 +550,6 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
531
550
  )
532
551
  }
533
552
 
534
- // Render Functions
535
- const generatePairedBarAxis = () => {
536
- const axisMaxHeight = bottomLabelStart + BOTTOM_LABEL_PADDING
537
-
538
- const getTickPositions = (ticks, xScale) => {
539
- if (!ticks.length) return false
540
- // filter out first index
541
- const filteredTicks = ticks.filter(tick => tick.index !== 0)
542
- const numberOfTicks = filteredTicks?.length
543
- const xMaxHalf = xScale.range()[0] || xMax / 2
544
- const tickWidthAll = filteredTicks.map(tick =>
545
- getTextWidth(formatNumber(tick.value, 'left'), GET_TEXT_WIDTH_FONT)
546
- )
547
- const accumulator = 100
548
- const sumOfTickWidth = tickWidthAll.reduce((a, b) => a + b, accumulator)
549
- const spaceBetweenEachTick = (xMaxHalf - sumOfTickWidth) / numberOfTicks
550
- // Determine the position of each tick
551
- let positions = [0]
552
- for (let i = 1; i < tickWidthAll.length; i++) {
553
- positions[i] = positions[i - 1] + tickWidthAll[i - 1] + spaceBetweenEachTick
554
- }
555
-
556
- // Check if ticks are overlapping
557
- let isTicksOverlapping = false
558
- tickWidthAll.forEach((_, i) => {
559
- if (positions[i] + tickWidthAll[i] > positions[i + 1]) {
560
- isTicksOverlapping = true
561
- return
562
- }
563
- })
564
- return isTicksOverlapping
565
- }
566
- return (
567
- <>
568
- <AxisBottom
569
- top={yMax}
570
- left={Number(runtime.yAxis.size)}
571
- label={runtime.xAxis.label}
572
- tickFormat={isDateScale(runtime.xAxis) ? formatDate : formatNumber}
573
- scale={g1xScale}
574
- stroke='#333'
575
- tickStroke='#333'
576
- numTicks={runtime.xAxis.numTicks || undefined}
577
- >
578
- {props => {
579
- return (
580
- <Group className='bottom-axis'>
581
- {props.ticks.map((tick, i) => {
582
- const isTicksOverlapping = getTickPositions(props.ticks, g1xScale)
583
- const maxTickRotation = Number(config.xAxis.maxTickRotation) || 90
584
- const isResponsiveTicks = config.isResponsiveTicks && isTicksOverlapping
585
- const angle =
586
- tick.index !== 0 && (isResponsiveTicks ? maxTickRotation : Number(config.yAxis.tickRotation))
587
- const textAnchor = angle && tick.index !== 0 ? 'end' : 'middle'
588
-
589
- return (
590
- <Group key={`vx-tick-${tick.value}-${i}`} className={'vx-axis-tick'}>
591
- {!runtime.yAxis.hideTicks && <Line from={tick.from} to={tick.to} stroke='#333' />}
592
- {!runtime.yAxis.hideLabel && (
593
- <Text // prettier-ignore
594
- innerRef={el => (xAxisLabelRefs.current[i] = el)}
595
- x={tick.to.x}
596
- y={tick.to.y}
597
- angle={-angle}
598
- verticalAnchor={angle ? 'middle' : 'start'}
599
- textAnchor={textAnchor}
600
- fontSize={tickLabelFontSize}
601
- >
602
- {formatNumber(tick.value, 'left')}
603
- </Text>
604
- )}
605
- </Group>
606
- )
607
- })}
608
- {!runtime.yAxis.hideAxis && <Line from={props.axisFromPoint} to={props.axisToPoint} stroke='#333' />}
609
- </Group>
610
- )
611
- }}
612
- </AxisBottom>
613
- <AxisBottom
614
- innerRef={axisBottomRef}
615
- top={yMax}
616
- left={Number(runtime.yAxis.size)}
617
- label={runtime.xAxis.label}
618
- tickFormat={
619
- isDateScale(runtime.xAxis) ? formatDate : runtime.xAxis.dataKey !== 'Year' ? formatNumber : tick => tick
620
- }
621
- scale={g2xScale}
622
- stroke='#333'
623
- tickStroke='#333'
624
- numTicks={runtime.xAxis.numTicks || undefined}
625
- >
626
- {props => {
627
- return (
628
- <>
629
- <Group className='bottom-axis'>
630
- {props.ticks.map((tick, i) => {
631
- const isTicksOverlapping = getTickPositions(props.ticks, g2xScale)
632
- const maxTickRotation = Number(config.xAxis.maxTickRotation) || 90
633
- const isResponsiveTicks = config.isResponsiveTicks && isTicksOverlapping
634
- const angle =
635
- tick.index !== 0 && (isResponsiveTicks ? maxTickRotation : Number(config.yAxis.tickRotation))
636
- const textAnchor = angle && tick.index !== 0 ? 'end' : 'middle'
637
- if (!i) return <></> // skip first tick to avoid overlapping 0's
638
- return (
639
- <Group key={`vx-tick-${tick.value}-${i}`} className={'vx-axis-tick'}>
640
- {!runtime.yAxis.hideTicks && <Line from={tick.from} to={tick.to} stroke='#333' />}
641
- {!runtime.yAxis.hideLabel && (
642
- <Text // prettier-ignore
643
- x={tick.to.x}
644
- y={tick.to.y + X_TICK_LABEL_PADDING}
645
- angle={-angle}
646
- verticalAnchor={angle ? 'middle' : 'start'}
647
- textAnchor={textAnchor}
648
- fontSize={tickLabelFontSize}
649
- >
650
- {formatNumber(tick.value, 'left')}
651
- </Text>
652
- )}
653
- </Group>
654
- )
655
- })}
656
- {!runtime.yAxis.hideAxis && <Line from={props.axisFromPoint} to={props.axisToPoint} stroke='#333' />}
657
- </Group>
658
- <Group>
659
- <Text
660
- className='x-axis-title-label'
661
- x={xMax / 2}
662
- y={axisMaxHeight}
663
- stroke='#333'
664
- textAnchor={'middle'}
665
- verticalAnchor='start'
666
- fontSize={axisLabelFontSize}
667
- >
668
- {!config.hideXAxisLabel ? runtime.xAxis.label : null}
669
- </Text>
670
- </Group>
671
- </>
672
- )
673
- }}
674
- </AxisBottom>
675
- </>
676
- )
677
- }
678
553
  return isNaN(parentWidth) ? (
679
554
  <React.Fragment></React.Fragment>
680
555
  ) : (
@@ -687,7 +562,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
687
562
  <svg
688
563
  ref={internalSvgRef}
689
564
  onMouseMove={onMouseMove}
690
- width={parentWidth + config.yAxis.rightAxisSize}
565
+ width={parentWidth}
691
566
  height={isNoDataAvailable ? 1 : calculatedSvgHeight ?? parentHeight}
692
567
  className={`linear ${config.animate ? 'animated' : ''} ${animatedChart && config.animate ? 'animate' : ''} ${
693
568
  debugSvg && 'debug'
@@ -707,60 +582,20 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
707
582
  >
708
583
  {!isDraggingAnnotation && <Bar width={parentWidth} height={initialHeight} fill={'transparent'}></Bar>}{' '}
709
584
  {/* GRID LINES */}
710
- {/* Actual AxisLeft is drawn after visualization */}
711
- {!['Spark Line', 'Forest Plot', 'Warming Stripes'].includes(visualizationType) &&
712
- config.yAxis.type !== 'categorical' && (
713
- <AxisLeft
714
- scale={yScale}
715
- left={Number(runtime.yAxis.size) - config.yAxis.axisPadding}
716
- numTicks={handleNumTicks}
717
- >
718
- {props => {
719
- const axisCenter =
720
- config.orientation === 'horizontal'
721
- ? Math.abs(props.axisToPoint.y - props.axisFromPoint.y) / 2
722
- : (props.axisFromPoint.y - props.axisToPoint.y) / 2
723
- return (
724
- <Group className='left-axis'>
725
- {props.ticks.map((tick, i) => {
726
- const showTicks = String(tick.value).startsWith('1') || tick.value === 0.1 ? 'block' : 'none'
727
- const hideFirstGridLine = tick.index === 0 && tick.value === 0 && config.xAxis.hideAxis
728
-
729
- return (
730
- <Group key={`vx-tick-${tick.value}-${i}`} className={'vx-axis-tick'}>
731
- {runtime.yAxis.gridLines && !hideFirstGridLine ? (
732
- <Line
733
- key={`${tick.value}--hide-hideGridLines`}
734
- display={(isLogarithmicAxis && showTicks).toString()}
735
- from={{ x: tick.from.x + xMax, y: tick.from.y }}
736
- to={tick.from}
737
- stroke='#d6d6d6'
738
- />
739
- ) : (
740
- ''
741
- )}
742
- </Group>
743
- )
744
- })}
745
- <Text
746
- className='y-label'
747
- textAnchor='middle'
748
- verticalAnchor='start'
749
- transform={`translate(${-1 * runtime.yAxis.size + yLabelOffset}, ${axisCenter}) rotate(-90)`}
750
- fontWeight='bold'
751
- fill={config.yAxis.labelColor}
752
- fontSize={axisLabelFontSize}
753
- >
754
- {!config.hideYAxisLabel ? props.label : null}
755
- </Text>
756
- </Group>
757
- )
758
- }}
759
- </AxisLeft>
760
- )}
585
+ {/* Actual LeftAxis is drawn after visualization */}
586
+ {!TYPES_WITHOUT_GRID.includes(visualizationType as any) && config.yAxis.type !== 'categorical' && (
587
+ <LeftAxisGridlines
588
+ yScale={yScale}
589
+ xMax={xMax}
590
+ yAxisWidth={yAxisWidth}
591
+ numTicks={handleNumTicks}
592
+ yLabelOffset={yLabelOffset}
593
+ axisLabelFontSize={axisLabelFontSize}
594
+ />
595
+ )}
761
596
  {/* Horizontal chart grid lines */}
762
597
  {runtime.xAxis.gridLines && orientation === 'horizontal' && (
763
- <Group left={Number(runtime.yAxis.size)}>
598
+ <Group left={yAxisWidth}>
764
599
  {xScale.ticks(xTickCount).map((tickValue, i) => {
765
600
  const tickPosition = xScale(tickValue)
766
601
  return (
@@ -774,167 +609,46 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
774
609
  })}
775
610
  </Group>
776
611
  )}
777
- {visualizationType === 'Paired Bar' && generatePairedBarAxis()}
778
- {visualizationType === 'Deviation Bar' && config.runtime.series?.length === 1 && (
779
- <DeviationBar animatedChart={animatedChart} xScale={xScale} yScale={yScale} width={xMax} height={yMax} />
780
- )}
781
612
  {visualizationType === 'Paired Bar' && (
782
- <PairedBarChart originalWidth={parentWidth} width={xMax} height={yMax} />
783
- )}
784
- {visualizationType === 'Scatter Plot' && (
785
- <ScatterPlot
786
- xScale={xScale}
787
- yScale={yScale}
788
- getXAxisData={getXAxisData}
789
- getYAxisData={getYAxisData}
790
- xMax={xMax}
613
+ <PairedBarAxis
614
+ g1xScale={g1xScale}
615
+ g2xScale={g2xScale}
791
616
  yMax={yMax}
792
- handleTooltipMouseOver={handleTooltipMouseOver}
793
- handleTooltipMouseOff={handleTooltipMouseOff}
794
- handleTooltipClick={handleTooltipClick}
795
- tooltipData={tooltipData}
796
- showTooltip={showTooltip}
797
- />
798
- )}
799
- {visualizationType === 'Warming Stripes' && (
800
- <WarmingStripes xScale={xScale} yScale={yScale} xMax={xMax} yMax={yMax} />
801
- )}
802
- {visualizationType === 'Box Plot' && config.orientation === 'vertical' && (
803
- <BoxPlotVertical
804
- seriesScale={seriesScale}
805
617
  xMax={xMax}
806
- yMax={yMax}
807
- min={min}
808
- max={max}
809
- xScale={xScale}
810
- yScale={yScale}
811
- />
812
- )}
813
- {visualizationType === 'Box Plot' && config.orientation === 'horizontal' && (
814
- <BoxPlotHorizontal
815
- seriesScale={seriesScale}
816
- xMax={xMax}
817
- yMax={yMax}
818
- min={min}
819
- max={max}
820
- xScale={xScale}
821
- yScale={yScale}
822
- />
823
- )}
824
- {((visualizationType === 'Area Chart' && config.visualizationSubType === 'stacked') ||
825
- visualizationType === 'Combo') && (
826
- <AreaChartStacked
827
- xScale={xScale}
828
- yScale={yScale}
829
- yMax={yMax}
830
- xMax={xMax}
831
- chartRef={svgRef}
832
- width={xMax}
833
- height={yMax}
834
- handleTooltipMouseOver={handleTooltipMouseOver}
835
- handleTooltipMouseOff={handleTooltipMouseOff}
836
- tooltipData={tooltipData}
837
- showTooltip={showTooltip}
838
- />
839
- )}
840
- {(visualizationType === 'Bar' || visualizationType === 'Combo' || convertLineToBarGraph) && (
841
- <BarChart
842
- xScale={xScale}
843
- yScale={yScale}
844
- seriesScale={seriesScale}
845
- xMax={xMax}
846
- yMax={yMax}
847
- getXAxisData={getXAxisData}
848
- getYAxisData={getYAxisData}
849
- animatedChart={animatedChart}
850
- visible={animatedChart}
851
- handleTooltipMouseOver={handleTooltipMouseOver}
852
- handleTooltipMouseOff={handleTooltipMouseOff}
853
- handleTooltipClick={handleTooltipClick}
854
- tooltipData={tooltipData}
855
- showTooltip={showTooltip}
856
- chartRef={svgRef}
857
- />
858
- )}
859
- {(visualizationType === 'Combo' || visualizationType === 'Bump Chart') && (
860
- <LineChart
861
- xScale={xScale}
862
- yScale={yScale}
863
- getXAxisData={getXAxisData}
864
- getYAxisData={getYAxisData}
865
- xMax={xMax}
866
- yMax={yMax}
867
- seriesStyle={config.runtime.series}
868
- handleTooltipMouseOver={handleTooltipMouseOver}
869
- handleTooltipMouseOff={handleTooltipMouseOff}
870
- handleTooltipClick={handleTooltipClick}
871
- tooltipData={tooltipData}
872
- showTooltip={showTooltip}
873
- />
874
- )}
875
- {/* Line chart */}
876
- {/* TODO: Make this just line or combo? */}
877
- {![
878
- 'Paired Bar',
879
- 'Box Plot',
880
- 'Area Chart',
881
- 'Scatter Plot',
882
- 'Deviation Bar',
883
- 'Forecasting',
884
- 'Bar',
885
- 'Warming Stripes'
886
- ].includes(visualizationType) &&
887
- !convertLineToBarGraph && (
888
- <>
889
- <LineChart
890
- xScale={xScale}
891
- yScale={yScale}
892
- getXAxisData={getXAxisData}
893
- getYAxisData={getYAxisData}
894
- xMax={xMax}
895
- yMax={yMax}
896
- seriesStyle={config.runtime.series}
897
- tooltipData={tooltipData}
898
- handleTooltipMouseOver={handleTooltipMouseOver}
899
- handleTooltipMouseOff={handleTooltipMouseOff}
900
- />
901
- </>
902
- )}
903
- {(visualizationType === 'Forecasting' || visualizationType === 'Combo') && (
904
- <Forecasting
905
- showTooltip={showTooltip}
906
- tooltipData={tooltipData}
907
- xScale={xScale}
908
- yScale={yScale}
909
- width={xMax}
910
- height={yMax}
911
- xScaleNoPadding={xScaleNoPadding}
912
- chartRef={svgRef}
913
- handleTooltipMouseOver={handleTooltipMouseOver}
914
- handleTooltipMouseOff={handleTooltipMouseOff}
915
- />
916
- )}
917
- {visualizationType === 'Forest Plot' && (
918
- <ForestPlot
919
- xScale={xScale}
920
- yScale={yScale}
921
- seriesScale={seriesScale}
922
- width={parentWidth}
923
- height={forestHeight}
924
- getXAxisData={getXAxisData}
925
- getYAxisData={getYAxisData}
926
- animatedChart={animatedChart}
927
- visible={animatedChart}
928
- handleTooltipMouseOver={handleTooltipMouseOver}
929
- handleTooltipMouseOff={handleTooltipMouseOff}
930
- handleTooltipClick={handleTooltipClick}
931
- tooltipData={tooltipData}
932
- showTooltip={showTooltip}
933
- chartRef={svgRef}
934
- config={config}
935
- forestPlotRightLabelRef={forestPlotRightLabelRef}
618
+ yAxisWidth={yAxisWidth}
619
+ bottomLabelStart={bottomLabelStart}
620
+ tickLabelFontSize={tickLabelFontSize}
621
+ axisLabelFontSize={axisLabelFontSize}
622
+ axisBottomRef={axisBottomRef}
623
+ xAxisLabelRefs={xAxisLabelRefs}
624
+ tickLabelFont={GET_TEXT_WIDTH_FONT}
936
625
  />
937
626
  )}
627
+ {/* Visualization Renderer - handles all chart type rendering */}
628
+ <VisualizationRenderer
629
+ xScale={xScale}
630
+ yScale={yScale}
631
+ xMax={xMax}
632
+ yMax={yMax}
633
+ seriesScale={seriesScale}
634
+ xScaleNoPadding={xScaleNoPadding}
635
+ min={min}
636
+ max={max}
637
+ parentWidth={parentWidth}
638
+ yAxisWidth={yAxisWidth}
639
+ forestHeight={forestHeight}
640
+ animatedChart={animatedChart}
641
+ tooltipData={tooltipData}
642
+ showTooltip={showTooltip}
643
+ handleTooltipMouseOver={handleTooltipMouseOver}
644
+ handleTooltipMouseOff={handleTooltipMouseOff}
645
+ handleTooltipClick={handleTooltipClick}
646
+ getXAxisData={getXAxisData}
647
+ getYAxisData={getYAxisData}
648
+ svgRef={svgRef}
649
+ forestPlotRightLabelRef={forestPlotRightLabelRef}
650
+ synchronizedXValue={synchronizedXValue}
651
+ />
938
652
  {/* Brush moved to separate overlay - no longer in main SVG */}
939
653
  {/* y anchors */}
940
654
  {config.yAxis.anchors &&
@@ -944,11 +658,10 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
944
658
 
945
659
  if (!anchor.value) return
946
660
  if (config.yAxis.labelPlacement === 'Below Bar') {
947
- const textOffset = -6.5
948
- middleOffset = textOffset + Number(config.series.length * config.barHeight) / config.series.length
661
+ middleOffset =
662
+ BELOW_BAR_TEXT_OFFSET + Number(config.series.length * config.barHeight) / config.series.length
949
663
  } else {
950
- const paddingOffset = 8
951
- middleOffset = paddingOffset
664
+ middleOffset = LABEL_PADDING_OFFSET
952
665
  }
953
666
 
954
667
  if (!position) return
@@ -956,13 +669,13 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
956
669
  return (
957
670
  // prettier-ignore
958
671
  <Line
959
- key={`yAxis-${anchor.value}--${index}`}
960
- strokeDasharray={handleLineType(anchor.lineStyle)}
961
- stroke={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
962
- className='anchor-y'
963
- from={{ x: runtime.yAxis.size, y: position - middleOffset }}
964
- to={{ x: runtime.yAxis.size + xMax, y: position - middleOffset }}
965
- />
672
+ key={`yAxis-${anchor.value}--${index}`}
673
+ strokeDasharray={handleLineType(anchor.lineStyle)}
674
+ stroke={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
675
+ className='anchor-y'
676
+ from={{ x: Number(runtime.yAxis.size), y: position - middleOffset }}
677
+ to={{ x: Number(runtime.yAxis.size) + Number(xMax), y: position - middleOffset }}
678
+ />
966
679
  )
967
680
  })}
968
681
  {/* x anchors */}
@@ -992,14 +705,14 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
992
705
  return (
993
706
  // prettier-ignore
994
707
  <Line
995
- key={`xAxis-${anchor.value}--${index}`}
996
- strokeDasharray={handleLineType(anchor.lineStyle)}
997
- stroke={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
998
- fill={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
999
- className='anchor-x'
1000
- from={{ x: Number(anchorPosition) + Number(padding), y: 0 }}
1001
- to={{ x: Number(anchorPosition) + Number(padding), y: yMax }}
1002
- />
708
+ key={`xAxis-${anchor.value}--${index}`}
709
+ strokeDasharray={handleLineType(anchor.lineStyle)}
710
+ stroke={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
711
+ fill={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
712
+ className='anchor-x'
713
+ from={{ x: Number(anchorPosition) + Number(padding), y: 0 }}
714
+ to={{ x: Number(anchorPosition) + Number(padding), y: yMax }}
715
+ />
1003
716
  )
1004
717
  })}
1005
718
  {/* we are handling regions in bar charts differently, so that we can calculate the bar group into the region space. */}
@@ -1019,7 +732,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1019
732
  )}
1020
733
  {isNoDataAvailable && (
1021
734
  <Text
1022
- x={Number(runtime.yAxis.size) + Number(xMax / 2)}
735
+ x={yAxisWidth + Number(xMax / 2)}
1023
736
  y={initialHeight / 2 - (config.xAxis.padding || 0) / 2}
1024
737
  textAnchor='middle'
1025
738
  >
@@ -1032,405 +745,65 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1032
745
  <HoverLine xMax={xMax} yMax={yMax} point={point} tooltipData={tooltipData} orientation='vertical' />
1033
746
  </>
1034
747
  )}
1035
- <Group left={Number(config.runtime.yAxis.size)}>
748
+ <Group left={yAxisWidth}>
1036
749
  <Annotation.Draggable
1037
750
  xScale={xScale}
1038
751
  yScale={yScale}
1039
752
  xScaleAnnotation={xScaleAnnotation}
753
+ yScaleAnnotation={yScaleAnnotation}
1040
754
  xMax={xMax}
755
+ yMax={yMax}
756
+ seriesScale={seriesScale}
1041
757
  svgRef={svgRef}
1042
758
  onDragStateChange={handleDragStateChange}
1043
759
  />
1044
760
  </Group>
1045
761
  {/* Highlighted regions */}
1046
762
  {/* Y axis */}
1047
- {!['Spark Line', 'Forest Plot', 'Warming Stripes'].includes(visualizationType) &&
763
+ {/* Horizon charts don't have a grid but should be rendered with a left axis */}
764
+ {(!TYPES_WITHOUT_GRID.includes(visualizationType as any) || visualizationType === 'Horizon Chart') &&
1048
765
  config.yAxis.type !== 'categorical' && (
1049
- <AxisLeft
1050
- scale={yScale}
1051
- tickLength={isLogarithmicAxis ? 6 : 8}
1052
- left={Number(runtime.yAxis.size) - config.yAxis.axisPadding}
1053
- label={runtime.yAxis.label || runtime.yAxis.label}
1054
- stroke='#333'
1055
- tickFormat={handleLeftTickFormatting}
766
+ <LeftAxis
767
+ yScale={yScale}
768
+ xScale={xScale}
769
+ yMax={yMax}
770
+ xMax={xMax}
771
+ yAxisWidth={yAxisWidth}
1056
772
  numTicks={handleNumTicks}
1057
- >
1058
- {props => {
1059
- const axisCenter =
1060
- config.orientation === 'horizontal'
1061
- ? Math.abs(props.axisToPoint.y - props.axisFromPoint.y) / 2
1062
- : (props.axisFromPoint.y - props.axisToPoint.y) / 2
1063
- const horizontalTickOffset =
1064
- yMax / props.ticks.length / 2 - (yMax / props.ticks.length) * (1 - config.barThickness) + 5
1065
- return (
1066
- <Group className='left-axis'>
1067
- {!config.yAxis.hideAxis && (
1068
- <Line
1069
- from={props.axisFromPoint}
1070
- to={
1071
- runtime.horizontal
1072
- ? {
1073
- x: 0,
1074
- y:
1075
- config.visualizationType === 'Forest Plot'
1076
- ? parentHeight
1077
- : Number(heights.horizontal)
1078
- }
1079
- : props.axisToPoint
1080
- }
1081
- stroke='#000'
1082
- />
1083
- )}
1084
- {orientation === 'vertical' && yScale.domain()[0] < 0 && (
1085
- // draw from the Left of the chart …
1086
- <Line
1087
- from={{ x: props.axisFromPoint.x, y: yScale(0) }}
1088
- to={{ x: xMax, y: yScale(0) }}
1089
- stroke='#333'
1090
- />
1091
- )}
1092
- {orientation === 'horizontal' && xScale.domain()[0] < 0 && (
1093
- <Line
1094
- // draw from the top of the char
1095
- from={{ x: xScale(0), y: 0 }}
1096
- to={{ x: xScale(0), y: yMax }}
1097
- stroke='#333'
1098
- />
1099
- )}
1100
- {visualizationType === 'Bar' && orientation === 'horizontal' && xScale.domain()[0] < 0 && (
1101
- <Line
1102
- from={{ x: xScale(0), y: 0 }}
1103
- to={{ x: xScale(0), y: yMax }}
1104
- stroke='#333'
1105
- strokeWidth={2}
1106
- />
1107
- )}
1108
- {props.ticks.map((tick, i) => {
1109
- const minY = props.ticks[0].to.y
1110
- const barMinHeight = 15 // 15 is the min height for bars by default
1111
- const showTicks = String(tick.value).startsWith('1') || tick.value === 0.1 ? 'block' : 'none'
1112
- const tickLength = showTicks === 'block' ? 7 : 0
1113
- const to = { x: tick.to.x - tickLength, y: tick.to.y }
1114
-
1115
- // Vertical value/suffix vars
1116
- const lastTick = props.ticks.length - 1 === i
1117
- const useInlineLabel = lastTick && inlineLabel
1118
- const hideTopTick = lastTick && inlineLabel && !inlineLabelHasNoSpace
1119
- const valueOnLinePadding = hideAxis ? -8 : -12
1120
- const labelXPadding = labelsAboveGridlines ? valueOnLinePadding : TICK_LABEL_MARGIN_RIGHT
1121
- const labelYPadding = labelsAboveGridlines ? 4 : 0
1122
- const labelX = tick.to.x - labelXPadding
1123
- const labelY = tick.to.y - labelYPadding
1124
- const labelVerticalAnchor = labelsAboveGridlines ? 'end' : 'middle'
1125
- const combineDomInlineLabelWithValue = inlineLabel && labelsAboveGridlines && lastTick
1126
- const formattedValue = useInlineLabel
1127
- ? String(tick?.formattedValue || '').replace(config.dataFormat.suffix, '')
1128
- : tick?.formattedValue
1129
-
1130
- return (
1131
- <Group key={`vx-tick-${tick.value}-${i}`} className={'vx-axis-tick'}>
1132
- {!runtime.yAxis.hideTicks && !labelsAboveGridlines && !hideTopTick && (
1133
- <Line
1134
- key={`${tick.value}--hide-hideTicks`}
1135
- from={tick.from}
1136
- to={isLogarithmicAxis ? to : tick.to}
1137
- stroke={config.yAxis.tickColor}
1138
- display={orientation === 'horizontal' ? 'none' : 'block'}
1139
- fontSize={tickLabelFontSize}
1140
- />
1141
- )}
1142
-
1143
- {orientation === 'horizontal' &&
1144
- visualizationType === 'Box Plot' &&
1145
- config.yAxis.labelPlacement === 'On Date/Category Axis' &&
1146
- !config.yAxis.hideLabel && (
1147
- <Text
1148
- x={tick.to.x}
1149
- y={yScale(tick.value) + yScale.bandwidth() / 2}
1150
- transform={`rotate(${
1151
- config.orientation === 'horizontal' ? config.runtime.yAxis.tickRotation || 0 : 0
1152
- }, ${tick.to.x}, ${tick.to.y})`}
1153
- verticalAnchor={'middle'}
1154
- textAnchor={'end'}
1155
- fontSize={tickLabelFontSize}
1156
- >
1157
- {tick.formattedValue}
1158
- </Text>
1159
- )}
1160
-
1161
- {orientation === 'horizontal' &&
1162
- visualizationType === 'Bar' &&
1163
- config.yAxis.labelPlacement === 'On Date/Category Axis' &&
1164
- !config.yAxis.hideLabel &&
1165
- (() => {
1166
- const barGroupCount =
1167
- config.visualizationSubType === 'stacked' ? 1 : config.runtime.seriesKeys.length
1168
-
1169
- // Calculate barHeight based on chart type (regular bar vs lollipop)
1170
- let barHeight
1171
- if (config.isLollipopChart) {
1172
- const lollipopSizes = { large: 7, medium: 6, small: 5 }
1173
- const lollipopBarWidth = lollipopSizes[config.lollipopSize] || 5
1174
- barHeight = lollipopBarWidth * barGroupCount
1175
- } else {
1176
- barHeight = Number(config.barHeight) * barGroupCount
1177
- }
1178
-
1179
- const totalBarHeight = barHeight + Number(config.barSpace)
1180
- const barGroupY = i === 0 ? 0 : totalBarHeight * i
1181
- const labelCenterY = barGroupY + barHeight / 2
1182
-
1183
- return (
1184
- <Text
1185
- x={tick.from.x - Number(runtime.yAxis.size) + horizontalYAxisLabelSpace}
1186
- y={labelCenterY}
1187
- verticalAnchor={'middle'}
1188
- textAnchor={'start'}
1189
- fontSize={tickLabelFontSize}
1190
- width={categoryLabelSpace}
1191
- lineHeight={'1.2em'}
1192
- >
1193
- {tick.formattedValue}
1194
- </Text>
1195
- )
1196
- })()}
1197
-
1198
- {orientation === 'horizontal' &&
1199
- visualizationType !== 'Bar' &&
1200
- visualizationSubType === 'stacked' &&
1201
- config.yAxis.labelPlacement === 'On Date/Category Axis' &&
1202
- !config.yAxis.hideLabel && (
1203
- <Text
1204
- transform={`translate(${tick.to.x - 5}, ${
1205
- tick.to.y - minY + (Number(config.barHeight) - barMinHeight) / 2
1206
- }) rotate(-${runtime.horizontal ? runtime.yAxis.tickRotation : 0})`}
1207
- verticalAnchor={'start'}
1208
- textAnchor={'end'}
1209
- fontSize={tickLabelFontSize}
1210
- >
1211
- {tick.formattedValue}
1212
- </Text>
1213
- )}
1214
-
1215
- {orientation === 'horizontal' &&
1216
- visualizationType === 'Paired Bar' &&
1217
- !config.yAxis.hideLabel && (
1218
- <Text
1219
- transform={`translate(${tick.to.x - 5}, ${
1220
- tick.to.y - minY + Number(config.barHeight) / 2
1221
- }) rotate(-${runtime.horizontal ? runtime.yAxis.tickRotation : 0})`}
1222
- textAnchor={'end'}
1223
- verticalAnchor='middle'
1224
- fontSize={tickLabelFontSize}
1225
- >
1226
- {tick.formattedValue}
1227
- </Text>
1228
- )}
1229
- {orientation === 'horizontal' &&
1230
- visualizationType === 'Deviation Bar' &&
1231
- !config.yAxis.hideLabel && (
1232
- <Text
1233
- transform={`translate(${tick.to.x - 5}, ${
1234
- config.isLollipopChart
1235
- ? tick.to.y - minY + 2
1236
- : tick.to.y - minY + Number(config.barHeight) / 2
1237
- }) rotate(-${runtime.horizontal ? runtime.yAxis.tickRotation : 0})`}
1238
- textAnchor={'end'}
1239
- verticalAnchor='middle'
1240
- fontSize={tickLabelFontSize}
1241
- >
1242
- {tick.formattedValue}
1243
- </Text>
1244
- )}
1245
-
1246
- {orientation === 'vertical' &&
1247
- visualizationType === 'Bump Chart' &&
1248
- !config.yAxis.hideLabel && (
1249
- <>
1250
- <Text
1251
- display={config.useLogScale ? showTicks : 'block'}
1252
- dx={config.useLogScale ? -6 : 0}
1253
- x={config.runtime.horizontal ? tick.from.x + 2 : tick.to.x - 8.5}
1254
- y={tick.to.y - 13 + (config.runtime.horizontal ? horizontalTickOffset : 0)}
1255
- angle={-Number(config.yAxis.tickRotation) || 0}
1256
- verticalAnchor={config.runtime.horizontal ? 'start' : 'middle'}
1257
- textAnchor={config.runtime.horizontal ? 'start' : 'end'}
1258
- fill={config.yAxis.tickLabelColor}
1259
- fontSize={tickLabelFontSize}
1260
- >
1261
- {config.runtime.seriesLabelsAll[tick.formattedValue - 1]}
1262
- </Text>
1263
-
1264
- {(seriesHighlight.length === 0 ||
1265
- seriesHighlight.includes(
1266
- config.runtime.seriesLabelsAll[tick.formattedValue - 1]
1267
- )) && (
1268
- <rect
1269
- x={0 - Number(runtime.yAxis.size)}
1270
- y={tick.to.y - 8 + (config.runtime.horizontal ? horizontalTickOffset : 7)}
1271
- width={Number(runtime.yAxis.size) + xScale(xScale.domain()[0])}
1272
- height='2'
1273
- fill={colorScale(config.runtime.seriesLabelsAll[tick.formattedValue - 1])}
1274
- />
1275
- )}
1276
- </>
1277
- )}
1278
- {orientation === 'vertical' &&
1279
- visualizationType !== 'Paired Bar' &&
1280
- visualizationType !== 'Bump Chart' &&
1281
- !config.yAxis.hideLabel && (
1282
- <>
1283
- {/* INLINE LABEL BEHAVIOR: Dom suffix for 'inlineLabel' behavior */}
1284
- {/* inline label is shown alone and is allowed to 'overflow' to the right */}
1285
- {/* SPECIAL ONE CHAR CASE: a one character inlineLabel does not overflow */}
1286
- {/* IF VALUES ON LINE: suffix is combined with value to avoid having to calculate varying (now left-aligned) value widths */}
1287
- {inlineLabel && lastTick && !labelsAboveGridlines && (
1288
- <BlurStrokeText
1289
- innerRef={suffixRef}
1290
- display={isLogarithmicAxis ? showTicks : 'block'}
1291
- dx={isLogarithmicAxis ? -6 : 0}
1292
- x={labelX}
1293
- y={labelY}
1294
- angle={-Number(config.yAxis.tickRotation) || 0}
1295
- verticalAnchor={labelVerticalAnchor}
1296
- textAnchor={inlineLabelHasNoSpace ? 'end' : 'start'}
1297
- fill={config.yAxis.tickLabelColor}
1298
- stroke={'#fff'}
1299
- paintOrder={'stroke'} // keeps stroke under fill
1300
- strokeLinejoin='round'
1301
- style={{ whiteSpace: 'pre-wrap' }} // prevents leading spaces from being trimmed
1302
- fontSize={tickLabelFontSize}
1303
- >
1304
- {inlineLabel}
1305
- </BlurStrokeText>
1306
- )}
1307
-
1308
- {/* VALUE */}
1309
- <BlurStrokeText
1310
- innerRef={el => lastTick && (topYLabelRef.current = el)}
1311
- display={isLogarithmicAxis ? showTicks : 'block'}
1312
- dx={isLogarithmicAxis ? -6 : 0}
1313
- x={inlineLabelHasNoSpace ? labelX - suffixWidth : labelX}
1314
- y={labelY + (config.runtime.horizontal ? horizontalTickOffset : 0)}
1315
- angle={-Number(config.yAxis.tickRotation) || 0}
1316
- verticalAnchor={config.runtime.horizontal ? 'start' : labelVerticalAnchor}
1317
- textAnchor={config.runtime.horizontal || labelsAboveGridlines ? 'start' : 'end'}
1318
- fill={config.yAxis.tickLabelColor}
1319
- stroke={'#fff'}
1320
- disableStroke={!labelsAboveGridlines}
1321
- strokeLinejoin='round'
1322
- paintOrder={'stroke'} // keeps stroke under fill
1323
- style={{ whiteSpace: 'pre-wrap' }} // prevents leading spaces from being trimmed
1324
- fontSize={tickLabelFontSize}
1325
- >
1326
- {`${formattedValue}${combineDomInlineLabelWithValue ? inlineLabel : ''}`}
1327
- </BlurStrokeText>
1328
- </>
1329
- )}
1330
- </Group>
1331
- )
1332
- })}
1333
- <Text
1334
- className='y-label'
1335
- textAnchor='middle'
1336
- verticalAnchor='start'
1337
- transform={`translate(${-1 * runtime.yAxis.size + yLabelOffset}, ${axisCenter}) rotate(-90)`}
1338
- fontWeight='bold'
1339
- fill={config.yAxis.labelColor}
1340
- fontSize={axisLabelFontSize}
1341
- >
1342
- {!config.hideYAxisLabel ? props.label : null}
1343
- </Text>
1344
- </Group>
1345
- )
1346
- }}
1347
- </AxisLeft>
773
+ tickLabelFontSize={tickLabelFontSize}
774
+ axisLabelFontSize={axisLabelFontSize}
775
+ handleLeftTickFormatting={handleLeftTickFormatting}
776
+ topYLabelRef={topYLabelRef}
777
+ suffixRef={suffixRef}
778
+ suffixWidth={suffixWidth}
779
+ horizontalYAxisLabelSpace={horizontalYAxisLabelSpace}
780
+ categoryLabelSpace={categoryLabelSpace}
781
+ yLabelOffset={yLabelOffset}
782
+ />
1348
783
  )}
1349
784
  {config.yAxis.type === 'categorical' && config.orientation === 'vertical' && (
1350
785
  <CategoricalYAxis
1351
786
  yScale={yScale}
1352
787
  xMax={xMax}
1353
788
  yMax={yMax}
1354
- leftSize={Number(runtime.yAxis.size) - config.yAxis.axisPadding}
789
+ leftSize={yAxisWidth - config.yAxis.axisPadding}
1355
790
  />
1356
791
  )}
1357
792
  {/* Right Axis */}
1358
793
  {hasRightAxis && (
1359
- <AxisRight
1360
- scale={yScaleRight}
1361
- left={Number(runtime.yAxis.size + xMax)}
1362
- label={config.yAxis.rightLabel}
1363
- tickFormat={tick => formatNumber(tick, 'right')}
1364
- numTicks={runtime.yAxis.rightNumTicks || undefined}
1365
- labelOffset={45}
1366
- >
1367
- {props => {
1368
- const axisCenter =
1369
- config.orientation === 'horizontal'
1370
- ? (props.axisToPoint.y - props.axisFromPoint.y) / 2
1371
- : (props.axisFromPoint.y - props.axisToPoint.y) / 2
1372
- const horizontalTickOffset =
1373
- yMax / props.ticks.length / 2 - (yMax / props.ticks.length) * (1 - config.barThickness) + 5
1374
- return (
1375
- <Group className='right-axis'>
1376
- {props.ticks.map((tick, i) => {
1377
- return (
1378
- <Group key={`vx-tick-${tick.value}-${i}`} className='vx-axis-tick'>
1379
- {!runtime.yAxis.rightHideTicks && (
1380
- <Line
1381
- from={tick.from}
1382
- to={tick.to}
1383
- display={runtime.horizontal ? 'none' : 'block'}
1384
- stroke={config.yAxis.rightAxisTickColor}
1385
- />
1386
- )}
1387
-
1388
- {runtime.yAxis.rightGridLines ? (
1389
- <Line from={{ x: tick.from.x + xMax, y: tick.from.y }} to={tick.from} stroke='#d6d6d6' />
1390
- ) : (
1391
- ''
1392
- )}
1393
-
1394
- {!config.yAxis.rightHideLabel && (
1395
- <Text
1396
- x={tick.to.x}
1397
- y={tick.to.y + (runtime.horizontal ? horizontalTickOffset : 0)}
1398
- verticalAnchor={runtime.horizontal ? 'start' : 'middle'}
1399
- textAnchor={'start'}
1400
- fill={config.yAxis.rightAxisTickLabelColor}
1401
- fontSize={tickLabelFontSize}
1402
- >
1403
- {tick.formattedValue}
1404
- </Text>
1405
- )}
1406
- </Group>
1407
- )
1408
- })}
1409
- {!config.yAxis.rightHideAxis && (
1410
- <Line from={props.axisFromPoint} to={props.axisToPoint} stroke='#333' />
1411
- )}
1412
- <Text
1413
- className='y-label'
1414
- textAnchor='middle'
1415
- verticalAnchor='start'
1416
- transform={`translate(${
1417
- config.yAxis.rightLabelOffsetSize ? config.yAxis.rightLabelOffsetSize : 0
1418
- }, ${axisCenter}) rotate(-90)`}
1419
- fontWeight='bold'
1420
- fill={config.yAxis.rightAxisLabelColor}
1421
- fontSize={axisLabelFontSize}
1422
- >
1423
- {props.label}
1424
- </Text>
1425
- </Group>
1426
- )
1427
- }}
1428
- </AxisRight>
794
+ <RightAxis
795
+ yScaleRight={yScaleRight}
796
+ yMax={yMax}
797
+ xMax={xMax}
798
+ yAxisWidth={yAxisWidth}
799
+ tickLabelFontSize={tickLabelFontSize}
800
+ axisLabelFontSize={axisLabelFontSize}
801
+ />
1429
802
  )}
1430
803
  {hasTopAxis && config.topAxis.hasLine && (
1431
804
  <AxisTop
1432
805
  stroke='#333'
1433
- left={Number(runtime.yAxis.size)}
806
+ left={yAxisWidth}
1434
807
  scale={xScale}
1435
808
  hideTicks
1436
809
  hideZero
@@ -1441,189 +814,27 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1441
814
  )}
1442
815
  {/* X axis */}
1443
816
  {visualizationType !== 'Paired Bar' && visualizationType !== 'Spark Line' && (
1444
- <AxisBottom
1445
- innerRef={axisBottomRef}
1446
- top={
1447
- runtime.horizontal && config.visualizationType !== 'Forest Plot'
1448
- ? Number(heights.horizontal) + Number(config.xAxis.axisPadding)
1449
- : config.visualizationType === 'Forest Plot'
1450
- ? yMax + Number(config.xAxis.axisPadding)
1451
- : yMax
1452
- }
1453
- left={config.visualizationType !== 'Forest Plot' ? Number(runtime.yAxis.size) : 0}
1454
- label={runtime.xAxis.label}
1455
- tickFormat={handleBottomTickFormatting}
1456
- scale={xScale}
1457
- stroke='#333'
1458
- numTicks={useDateSpanMonths ? dateSpanMonths : xTickCount}
1459
- tickStroke='#333'
1460
- tickValues={
1461
- config.runtime.xAxis.manual
1462
- ? getTickValues(xAxisDataMapped, xScale, isDateTime ? xTickCount : getManualStep(), config)
1463
- : config.runtime.xAxis.type === 'date'
1464
- ? xAxisDataMapped
1465
- : // For date-time with small datasets (e.g., brush-filtered), use explicit tick values
1466
- // to ensure each data point can have a tick. Otherwise, visx may generate too few.
1467
- // Use uniqueXAxisDataMapped to handle cases where multiple series share x-axis values
1468
- isDateTime && uniqueXAxisDataMapped.length > 0 && uniqueXAxisDataMapped.length <= (xTickCount || 15)
1469
- ? uniqueXAxisDataMapped
1470
- : undefined
1471
- }
1472
- >
1473
- {props => {
1474
- const hasDynamicCategory = config.series.some(s => s.dynamicCategory)
1475
-
1476
- // For these charts, we generated all ticks in tickValues above, and now need to filter/shift them
1477
- // so the last tick is always labeled
1478
- // Use uniqueXAxisDataMapped for date filtering to match the tickValues we set
1479
- if (config.runtime.xAxis.type === 'date' && !config.runtime.xAxis.manual && !hasDynamicCategory) {
1480
- props.ticks = filterAndShiftLinearDateTicks(config, props, uniqueXAxisDataMapped, formatDate)
1481
- }
1482
-
1483
- const distanceBetweenTicks =
1484
- useDateSpanMonths &&
1485
- xScale
1486
- .ticks(xTickCount)
1487
- .map(t =>
1488
- props.ticks.findIndex(
1489
- tick => (typeof tick.value === 'number' ? tick.value : tick.value.getTime()) === t.getTime()
1490
- )
1491
- )
1492
- .slice(0, 2)
1493
- .reduce((acc, curr) => curr - acc)
1494
-
1495
- // filter out every [distanceBetweenTicks] tick starting from the end, so the last tick is always labeled
1496
- const filteredTicks = useDateSpanMonths
1497
- ? [...props.ticks]
1498
- .reverse()
1499
- .filter((_, i) => i % distanceBetweenTicks === 0)
1500
- .reverse()
1501
- .map((tick, i, arr) => ({
1502
- ...tick,
1503
- // reformat in case showYearsOnce, since first month of year may have changed
1504
- formattedValue: handleBottomTickFormatting(tick.value, i, arr)
1505
- }))
1506
- : props.ticks
1507
-
1508
- const axisMaxHeight = bottomLabelStart + BOTTOM_LABEL_PADDING
1509
-
1510
- const containsMultipleWords = inputString => /\s/.test(inputString)
1511
- const isMultiLabel = filteredTicks.some(tick => containsMultipleWords(tick.value))
1512
-
1513
- // Calculate sumOfTickWidth here, before map function
1514
- const longestTickLength = Math.max(
1515
- ...filteredTicks.map(tick => getTextWidth(tick.formattedValue, GET_TEXT_WIDTH_FONT))
1516
- )
1517
- // const marginTop = 20 // moved to top bc need for yMax calcs
1518
- const accumulator = isMultiLabel ? 180 : 100
1519
-
1520
- const textWidths = filteredTicks.map(tick => getTextWidth(tick.formattedValue, GET_TEXT_WIDTH_FONT))
1521
- const sumOfTickWidth = textWidths.reduce((a, b) => a + b, accumulator)
1522
- const spaceBetweenEachTick = (xMax - sumOfTickWidth) / (filteredTicks.length - 1)
1523
- const bufferBetweenTicks = 40
1524
- const maxLengthOfTick =
1525
- parentWidth / filteredTicks.length - X_TICK_LABEL_PADDING * 2 - bufferBetweenTicks
1526
-
1527
- // Determine the position of each tick
1528
- let positions = [0] // The first tick is at position 0
1529
- for (let i = 1; i < textWidths.length; i++) {
1530
- // The position of each subsequent tick is the position of the previous tick
1531
- // plus the width of the previous tick and the space
1532
- positions[i] = positions[i - 1] + textWidths[i - 1] + spaceBetweenEachTick
1533
- }
1534
- // calculate the end of x axis box
1535
- const axisBBox = axisBottomRef?.current?.getBBox().height
1536
- config.xAxis.axisBBox = axisBBox
1537
-
1538
- // force wrap it last tick is close to the end of the axis
1539
- const lastTickWidth = textWidths[textWidths.length - 1]
1540
- const lastTickPosition = positions[positions.length - 1] + lastTickWidth
1541
- const lastTickEnd = lastTickPosition + lastTickWidth / 2
1542
- const lastTickEndThreshold = xMax - lastTickWidth
1543
-
1544
- const areTicksTouching =
1545
- textWidths.some(textWidth => textWidth > maxLengthOfTick) || // Force wrap if any tick is too long
1546
- config.xAxis.showYearsOnce || // Force wrap when showing years once so it's easier to read
1547
- lastTickEnd > lastTickEndThreshold // Force wrap it last tick is close to the end of the axis
1548
-
1549
- const dynamicMarginTop =
1550
- areTicksTouching && config.isResponsiveTicks ? longestTickLength + DEFAULT_TICK_LENGTH + 20 : 0
1551
-
1552
- config.dynamicMarginTop = dynamicMarginTop
1553
- config.xAxis.tickWidthMax = longestTickLength
1554
-
1555
- return (
1556
- <Group className='bottom-axis' width={parentWidth}>
1557
- {filteredTicks.map((tick, i, propsTicks) => {
1558
- // when using LogScale show major ticks values only
1559
- const showTick = String(tick.value).startsWith('1') || tick.value === 0.1 ? 'block' : 'none'
1560
- const tickLength = showTick === 'block' ? 16 : DEFAULT_TICK_LENGTH
1561
- const to = { x: tick.to.x, y: tickLength }
1562
- const limitedWidth = 100 / propsTicks.length
1563
- //reset rotations by updating config
1564
- config.yAxis.tickRotation =
1565
- config.isResponsiveTicks && config.orientation === 'horizontal' ? 0 : config.yAxis.tickRotation
1566
- config.xAxis.tickRotation =
1567
- config.isResponsiveTicks && config.orientation === 'vertical' ? 0 : config.xAxis.tickRotation
1568
- //configure rotation
1569
-
1570
- const tickRotation =
1571
- config.isResponsiveTicks && areTicksTouching
1572
- ? -Number(config.xAxis.maxTickRotation) || -90
1573
- : -Number(config.runtime.xAxis.tickRotation)
1574
-
1575
- return (
1576
- <Group key={`vx-tick-${tick.value}-${i}`} className={'vx-axis-tick'}>
1577
- {!config.xAxis.hideTicks && (
1578
- <Line
1579
- from={tick.from}
1580
- to={orientation === 'horizontal' && isLogarithmicAxis ? to : tick.to}
1581
- stroke={config.xAxis.tickColor}
1582
- strokeWidth={showTick === 'block' && isLogarithmicAxis ? 1.3 : 1}
1583
- />
1584
- )}
1585
- {!config.xAxis.hideLabel && (
1586
- <Text
1587
- innerRef={el => (xAxisLabelRefs.current[i] = el)}
1588
- dy={config.orientation === 'horizontal' && isLogarithmicAxis ? 8 : 0}
1589
- display={config.orientation === 'horizontal' && isLogarithmicAxis ? showTick : 'block'}
1590
- x={tick.to.x}
1591
- y={tick.to.y + X_TICK_LABEL_PADDING}
1592
- angle={tickRotation}
1593
- verticalAnchor={tickRotation < -50 ? 'middle' : 'start'}
1594
- textAnchor={tickRotation ? 'end' : 'middle'}
1595
- width={
1596
- areTicksTouching && !config.isResponsiveTicks && !Number(config.xAxis.tickRotation)
1597
- ? limitedWidth
1598
- : undefined
1599
- }
1600
- fill={config.xAxis.tickLabelColor}
1601
- fontSize={tickLabelFontSize}
1602
- >
1603
- {tick.formattedValue}
1604
- </Text>
1605
- )}
1606
- </Group>
1607
- )
1608
- })}
1609
- {!config.xAxis.hideAxis && <Line from={props.axisFromPoint} to={props.axisToPoint} stroke='#333' />}
1610
- <Text
1611
- innerRef={xAxisTitleRef}
1612
- className='x-axis-title-label'
1613
- x={xMax / 2}
1614
- y={isForestPlot ? 0 /* set via ref */ : axisMaxHeight}
1615
- textAnchor='middle'
1616
- verticalAnchor='start'
1617
- fontWeight='bold'
1618
- fill={config.xAxis.labelColor}
1619
- fontSize={axisLabelFontSize}
1620
- >
1621
- {!config.hideXAxisLabel ? props.label : null}
1622
- </Text>
1623
- </Group>
1624
- )
1625
- }}
1626
- </AxisBottom>
817
+ <BottomAxis
818
+ axisBottomRef={axisBottomRef}
819
+ xScale={xScale}
820
+ yMax={yMax}
821
+ xMax={xMax}
822
+ yAxisWidth={yAxisWidth}
823
+ xTickCount={xTickCount}
824
+ tickLabelFontSize={tickLabelFontSize}
825
+ axisLabelFontSize={axisLabelFontSize}
826
+ handleBottomTickFormatting={handleBottomTickFormatting}
827
+ useDateSpanMonths={useDateSpanMonths}
828
+ dateSpanMonths={dateSpanMonths}
829
+ xAxisDataMapped={xAxisDataMapped}
830
+ uniqueXAxisDataMapped={uniqueXAxisDataMapped}
831
+ isDateTime={isDateTime}
832
+ bottomLabelStart={bottomLabelStart}
833
+ parentWidth={parentWidth}
834
+ xAxisLabelRefs={xAxisLabelRefs}
835
+ xAxisTitleRef={xAxisTitleRef}
836
+ getManualStep={getManualStep}
837
+ />
1627
838
  )}
1628
839
  </svg>
1629
840
  {!isDraggingAnnotation &&
@@ -1643,7 +854,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1643
854
  <TooltipWithBounds
1644
855
  ref={tooltipRef}
1645
856
  key={Math.random()}
1646
- className={'tooltip cdc-open-viz-module'}
857
+ className={'tooltip cove-visualization'}
1647
858
  left={tooltipLeft}
1648
859
  top={tooltipTop}
1649
860
  >
@@ -1680,10 +891,10 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1680
891
  <div
1681
892
  style={{
1682
893
  position: 'relative',
1683
- marginTop: '10px',
1684
- left: `${runtime.yAxis.size || 0}px`,
1685
- width: `${Math.max(xMax, 100)}px`,
1686
- height: '70px',
894
+ marginTop: `${BRUSH_MARGIN}px`,
895
+ left: `${yAxisWidth}px`,
896
+ width: `${Math.max(xMax, BRUSH_MIN_WIDTH)}px`,
897
+ height: `${BRUSH_HEIGHT}px`,
1687
898
  pointerEvents: 'auto',
1688
899
  zIndex: 15,
1689
900
  touchAction: 'none', // Enable touch interactions for brush
@@ -1693,7 +904,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1693
904
  }}
1694
905
  className='brush-overlay'
1695
906
  >
1696
- <BrushSelector xMax={Math.max(xMax, 100)} yMax={70} />
907
+ <BrushSelector key={brushKeyRef.current} xMax={Math.max(xMax, BRUSH_MIN_WIDTH)} yMax={BRUSH_HEIGHT} />
1697
908
  </div>
1698
909
  )}
1699
910
  </div>