@cdc/chart 4.26.1 → 4.26.2

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 (132) hide show
  1. package/CLAUDE.local.md +79 -0
  2. package/dist/{cdcchart-dgT_1dIT.es.js → cdcchart-DQ00cQCm.es.js} +1 -20
  3. package/dist/cdcchart.js +45357 -43655
  4. package/examples/default.json +378 -0
  5. package/examples/feature/__data__/horizon-chart-data.json +373 -0
  6. package/examples/feature/annotations/index.json +3 -6
  7. package/examples/feature/horizon/horizon-chart.json +395 -0
  8. package/examples/line-chart-states.json +1085 -0
  9. package/examples/private/123.json +694 -0
  10. package/examples/private/anchor-issue.json +4094 -0
  11. package/examples/private/backwards-slider.json +10430 -0
  12. package/examples/private/georgia.csv +160 -0
  13. package/examples/private/timeline-data.json +1 -0
  14. package/examples/private/timeline.json +389 -0
  15. package/examples/radar-chart-simple.json +133 -0
  16. package/examples/radar-chart.json +148 -0
  17. package/index.html +1 -31
  18. package/package.json +57 -59
  19. package/src/CdcChartComponent.tsx +99 -18
  20. package/src/_stories/Chart.Anchors.stories.tsx +10 -0
  21. package/src/_stories/Chart.BoxPlot.stories.tsx +7 -0
  22. package/src/_stories/Chart.CI.stories.tsx +13 -0
  23. package/src/_stories/Chart.Combo.stories.tsx +17 -0
  24. package/src/_stories/Chart.CustomColors.stories.tsx +4 -0
  25. package/src/_stories/Chart.DynamicSeries.stories.tsx +19 -0
  26. package/src/_stories/Chart.Filters.stories.tsx +4 -0
  27. package/src/_stories/Chart.Forecast.stories.tsx +4 -0
  28. package/src/_stories/Chart.HTMLInDataTable.stories.tsx +22 -0
  29. package/src/_stories/Chart.Legend.Gradient.stories.tsx +28 -0
  30. package/src/_stories/Chart.Patterns.stories.tsx +4 -0
  31. package/src/_stories/Chart.PreserveDecimals.stories.tsx +25 -0
  32. package/src/_stories/Chart.Regions.Categorical.stories.tsx +13 -0
  33. package/src/_stories/Chart.Regions.DateScale.stories.tsx +19 -0
  34. package/src/_stories/Chart.Regions.DateTimeScale.stories.tsx +25 -10
  35. package/src/_stories/Chart.ScatterPlot.stories.tsx +4 -0
  36. package/src/_stories/Chart.SmallMultiples.stories.tsx +16 -0
  37. package/src/_stories/Chart.stories.tsx +37 -0
  38. package/src/_stories/Chart.tooltip.stories.tsx +7 -0
  39. package/src/_stories/ChartAnnotation.stories.tsx +10 -0
  40. package/src/_stories/ChartAxisLabels.stories.tsx +4 -0
  41. package/src/_stories/ChartAxisTitles.stories.tsx +10 -0
  42. package/src/_stories/ChartBrush.Matrix.Continuous.stories.tsx +41 -0
  43. package/src/_stories/ChartBrush.Matrix.Date.stories.tsx +114 -0
  44. package/src/_stories/ChartBrush.Matrix.DateTime.stories.tsx +78 -0
  45. package/src/_stories/ChartBrush.stories.tsx +7 -0
  46. package/src/_stories/ChartEditor.stories.tsx +7 -0
  47. package/src/_stories/ChartLine.QuadrantAngles.stories.tsx +89 -0
  48. package/src/_stories/ChartLine.Suppression.stories.tsx +7 -0
  49. package/src/_stories/ChartLine.Symbols.stories.tsx +4 -0
  50. package/src/_stories/ChartPrefixSuffix.stories.tsx +46 -1
  51. package/src/_stories/TechAdoptionWithLinks.stories.tsx +7 -0
  52. package/src/_stories/_mock/brush_continuous.json +86 -0
  53. package/src/_stories/_mock/brush_date_large.json +176 -0
  54. package/src/_stories/_mock/line_chart_angle_near_zero_fall.json +195 -0
  55. package/src/_stories/_mock/line_chart_angle_near_zero_rise.json +195 -0
  56. package/src/_stories/_mock/line_chart_angle_q1_steep_upward.json +195 -0
  57. package/src/_stories/_mock/line_chart_angle_q2_gentle_downward.json +195 -0
  58. package/src/_stories/_mock/line_chart_angle_q3_steep_downward.json +195 -0
  59. package/src/_stories/_mock/line_chart_angle_q4_gentle_upward.json +195 -0
  60. package/src/_stories/_mock/line_chart_quadrant_angles.json +264 -0
  61. package/src/components/Annotations/components/AnnotationDraggable.styles.css +11 -17
  62. package/src/components/Annotations/components/AnnotationDraggable.tsx +240 -116
  63. package/src/components/Annotations/components/AnnotationDropdown.styles.css +1 -2
  64. package/src/components/Annotations/components/AnnotationDropdown.tsx +8 -12
  65. package/src/components/Annotations/components/AnnotationList.styles.css +4 -10
  66. package/src/components/Annotations/components/AnnotationList.tsx +5 -4
  67. package/src/components/Annotations/components/findNearestDatum.ts +75 -85
  68. package/src/components/Annotations/helpers/getVisibleAnnotations.ts +38 -0
  69. package/src/components/Axis/BottomAxis.tsx +270 -0
  70. package/src/components/Axis/LeftAxis.tsx +404 -0
  71. package/src/components/Axis/LeftAxisGridlines.tsx +77 -0
  72. package/src/components/Axis/PairedBarAxis.tsx +186 -0
  73. package/src/components/Axis/README.md +94 -0
  74. package/src/components/Axis/RightAxis.tsx +108 -0
  75. package/src/components/Axis/axis.constants.ts +21 -0
  76. package/src/components/Axis/index.ts +7 -0
  77. package/src/components/BarChart/components/BarChart.tsx +7 -1
  78. package/src/components/Brush/BrushSelector.tsx +154 -22
  79. package/src/components/Brush/MiniChartPreview.tsx +138 -21
  80. package/src/components/EditorPanel/EditorPanel.tsx +25 -11
  81. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +60 -22
  82. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +81 -1
  83. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +1 -1
  84. package/src/components/EditorPanel/components/Panels/Panel.Radar.tsx +353 -0
  85. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +0 -1
  86. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +21 -1
  87. package/src/components/EditorPanel/components/Panels/index.tsx +2 -0
  88. package/src/components/EditorPanel/useEditorPermissions.ts +55 -26
  89. package/src/components/HorizonChart/HorizonChart.tsx +131 -0
  90. package/src/components/HorizonChart/components/HorizonBand.tsx +160 -0
  91. package/src/components/HorizonChart/helpers/calculateHorizonBands.ts +27 -0
  92. package/src/components/HorizonChart/helpers/getHorizonLayerColors.ts +40 -0
  93. package/src/components/HorizonChart/index.tsx +3 -0
  94. package/src/components/Legend/Legend.Component.tsx +52 -4
  95. package/src/components/Legend/Legend.tsx +1 -1
  96. package/src/components/Legend/LegendValueRange.tsx +77 -0
  97. package/src/components/Legend/helpers/createFormatLabels.tsx +13 -0
  98. package/src/components/Legend/helpers/generateValueRanges.ts +92 -0
  99. package/src/components/LineChart/helpers/README.md +292 -0
  100. package/src/components/LineChart/helpers/labelPositioning.test.ts +245 -0
  101. package/src/components/LineChart/helpers/labelPositioning.ts +304 -0
  102. package/src/components/LineChart/index.tsx +44 -8
  103. package/src/components/LinearChart/README.md +109 -0
  104. package/src/components/LinearChart/VisualizationRenderer.tsx +267 -0
  105. package/src/components/LinearChart/linearChart.constants.ts +84 -0
  106. package/src/components/LinearChart/tests/LinearChart.test.tsx +201 -0
  107. package/src/components/LinearChart/tests/mockConfigContext.ts +129 -0
  108. package/src/components/LinearChart/utils/tickFormatting.ts +146 -0
  109. package/src/components/LinearChart.tsx +250 -1059
  110. package/src/components/PieChart/PieChart.tsx +1 -1
  111. package/src/components/RadarChart/RadarAxis.tsx +78 -0
  112. package/src/components/RadarChart/RadarChart.tsx +298 -0
  113. package/src/components/RadarChart/RadarGrid.tsx +64 -0
  114. package/src/components/RadarChart/RadarPolygon.tsx +91 -0
  115. package/src/components/RadarChart/helpers.ts +83 -0
  116. package/src/components/RadarChart/index.tsx +3 -0
  117. package/src/components/WarmingStripes/WarmingStripes.tsx +95 -25
  118. package/src/data/initial-state.js +14 -1
  119. package/src/helpers/getExcludedData.ts +4 -0
  120. package/src/helpers/handleChartAriaLabels.ts +19 -19
  121. package/src/helpers/handleLineType.ts +22 -18
  122. package/src/hooks/useProgrammaticTooltip.ts +23 -2
  123. package/src/hooks/useScales.ts +7 -0
  124. package/src/hooks/useTooltip.tsx +3 -0
  125. package/src/scss/main.scss +5 -0
  126. package/src/selectors/README.md +68 -0
  127. package/src/store/chart.reducer.ts +2 -0
  128. package/src/types/ChartConfig.ts +18 -0
  129. package/src/types/ChartContext.ts +1 -0
  130. package/src/types/Horizon.ts +64 -0
  131. package/preview.html +0 -1616
  132. 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,7 +11,7 @@ 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'
@@ -21,35 +22,25 @@ import _ from 'lodash'
21
22
 
22
23
  // CDC Components
23
24
  import { isDateScale } from '@cdc/core/helpers/cove/date'
24
- import { AreaChartStacked } from './AreaChart'
25
- import BarChart from './BarChart'
26
25
  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
26
  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
27
  import useIntersectionObserver from '../hooks/useIntersectionObserver'
37
28
  import Regions from './Regions'
38
- import CategoricalYAxis from './Axis/Categorical.Axis'
29
+ import { CategoricalYAxis, LeftAxis, LeftAxisGridlines, BottomAxis, PairedBarAxis, RightAxis } from './Axis'
39
30
  import BrushSelector from './Brush/BrushSelector'
40
- import WarmingStripes from './WarmingStripes'
31
+ import VisualizationRenderer from './LinearChart/VisualizationRenderer'
32
+ import { TYPES_WITHOUT_GRID, TYPES_WITH_TOOLTIP_GUIDES } from './LinearChart/linearChart.constants'
33
+ import { useTickFormatters } from './LinearChart/utils/tickFormatting'
41
34
 
42
35
  // Helpers
43
36
  import { isLegendWrapViewport, isMobileFontViewport } from '@cdc/core/helpers/viewports'
44
- import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
45
37
  import { calcInitialHeight } from '../helpers/sizeHelpers'
46
- import { filterAndShiftLinearDateTicks } from '../helpers/filterAndShiftLinearDateTicks'
47
38
  import { calculateHorizontalBarCategoryLabelWidth } from '../helpers/calculateHorizontalBarCategoryLabelWidth'
48
39
 
49
40
  // Hooks
50
41
  import useReduceData from '../hooks/useReduceData'
51
42
  import useRightAxis from '../hooks/useRightAxis'
52
- import useScales, { getTickValues } from '../hooks/useScales'
43
+ import useScales from '../hooks/useScales'
53
44
  import { useProgrammaticTooltip } from '../hooks/useProgrammaticTooltip'
54
45
  import { useSmallMultipleSynchronization } from '../hooks/useSmallMultipleSynchronization'
55
46
 
@@ -58,7 +49,6 @@ import { useTooltip as useCoveTooltip } from '../hooks/useTooltip'
58
49
  import { useChartHoverAnalytics } from '../hooks/useChartHoverAnalytics'
59
50
  import { useEditorPermissions } from './EditorPanel/useEditorPermissions'
60
51
  import Annotation from './Annotations'
61
- import { BlurStrokeText } from '@cdc/core/components/BlurStrokeText'
62
52
  import { countNumOfTicks } from '../helpers/countNumOfTicks'
63
53
  import HoverLine from './HoverLine/HoverLine'
64
54
  import { SmallMultiples } from './SmallMultiples'
@@ -68,15 +58,36 @@ type LinearChartProps = {
68
58
  parentHeight: number
69
59
  }
70
60
 
61
+ // Axis and tick constants
71
62
  const BOTTOM_LABEL_PADDING = 9
72
63
  const X_TICK_LABEL_PADDING = 4.5
73
64
  const DEFAULT_TICK_LENGTH = 8
74
- const MONTH_AS_MS = 1000 * 60 * 60 * 24 * 30
65
+ const DEFAULT_MAX_TICK_ROTATION = 90
66
+
67
+ // Font sizes
75
68
  const TICK_LABEL_FONT_SIZE = 16
76
69
  const TICK_LABEL_FONT_SIZE_SMALL = 13
77
70
  const AXIS_LABEL_FONT_SIZE = 18
78
71
  const AXIS_LABEL_FONT_SIZE_SMALL = 14
79
- const TICK_LABEL_MARGIN_RIGHT = 4.5
72
+
73
+ // Label positioning constants
74
+ const BELOW_BAR_TEXT_OFFSET = -6.5
75
+ const LABEL_PADDING_OFFSET = 8
76
+
77
+ // Brush constants
78
+ const BRUSH_HEIGHT = 70
79
+ const BRUSH_MARGIN = 10
80
+ const BRUSH_MIN_WIDTH = 100
81
+
82
+ // Tooltip constants
83
+ const TOOLTIP_EDGE_BUFFER = 10
84
+ const TOOLTIP_OFFSET = 6
85
+
86
+ // Chart-specific constants
87
+ const WARMING_STRIPES_HEIGHT = 78
88
+
89
+ // Time constants
90
+ const MONTH_AS_MS = 1000 * 60 * 60 * 24 * 30
80
91
 
81
92
  type TooltipData = {
82
93
  dataXPosition?: number
@@ -95,12 +106,9 @@ type UseTooltipReturn<T = TooltipData> = {
95
106
  const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, parentWidth }, svgRef) => {
96
107
  // prettier-ignore
97
108
  const {
98
- colorScale,
99
109
  config,
100
- convertLineToBarGraph,
101
110
  currentViewport,
102
111
  vizViewport,
103
- dimensions,
104
112
  formatDate,
105
113
  formatNumber,
106
114
  handleChartAriaLabels,
@@ -108,33 +116,18 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
108
116
  handleDragStateChange,
109
117
  interactionLabel,
110
118
  isDraggingAnnotation,
111
- isEditor,
112
119
  legendRef,
113
120
  parseDate,
114
121
  parentRef,
115
122
  tableData,
116
123
  transformedData: data,
117
- seriesHighlight
118
124
  } = useContext(ConfigContext)
119
125
 
120
126
  // CONFIG
121
127
  // 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
128
+ const { visualizationType, orientation, xAxis, yAxis, runtime, legend, forestPlot, debugSvg } = config
129
+
130
+ const { inlineLabel } = config.yAxis
138
131
 
139
132
  // HOOKS % STATES
140
133
  // When brush is active, use tableData (full dataset) for min/max calculation
@@ -144,11 +137,22 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
144
137
 
145
138
  const { visSupportsSmallMultiples } = useEditorPermissions()
146
139
  const { hasTopAxis } = getTopAxis(config)
140
+
141
+ // Increment on tableData change to force BrushSelector remount when filters change
142
+ const brushKeyRef = useRef(0)
143
+ const prevTableDataRef = useRef(tableData)
144
+ if (prevTableDataRef.current !== tableData) {
145
+ prevTableDataRef.current = tableData
146
+ brushKeyRef.current += 1
147
+ }
148
+
147
149
  const [animatedChart, setAnimatedChart] = useState(false)
148
150
  const [showHoverLine, setShowHoverLine] = useState(false)
149
151
  const [point, setPoint] = useState({ x: 0, y: 0 })
150
152
  const [suffixWidth, setSuffixWidth] = useState(0)
151
153
  const [calculatedSvgHeight, setCalculatedSvgHeight] = useState<number | null>(null)
154
+ const [axisUpdateKey, setAxisUpdateKey] = 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,22 @@ 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
+
224
248
  const { yScaleRight, hasRightAxis } = useRightAxis({ config, yMax, data })
225
249
 
226
- const xMax = parentWidth - Number(runtime.yAxis.size) - (hasRightAxis ? config.yAxis.rightAxisSize : 0)
250
+ // State for computed y-axis width - allows re-render when horizontal bar label space is calculated
251
+ const [computedYAxisWidth, setComputedYAxisWidth] = useState<number | null>(null)
252
+
253
+ // Use computed width if available, otherwise fall back to config value
254
+ const yAxisWidth = computedYAxisWidth ?? Number(runtime.yAxis.size)
255
+
256
+ // Chart width calculation using the current y-axis width
257
+ const xMax = parentWidth - yAxisWidth - (hasRightAxis ? config.yAxis.rightAxisSize : 0)
227
258
 
228
259
  const {
229
260
  xScale,
@@ -233,10 +264,9 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
233
264
  g2xScale,
234
265
  xScaleNoPadding,
235
266
  xScaleAnnotation,
267
+ yScaleAnnotation,
236
268
  min,
237
- max,
238
- leftMax,
239
- rightMax
269
+ max
240
270
  } = useScales({
241
271
  data,
242
272
  tableData,
@@ -252,6 +282,16 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
252
282
  currentViewport
253
283
  })
254
284
 
285
+ // Consolidated tick formatters
286
+ const { handleLeftTickFormatting, handleBottomTickFormatting } = useTickFormatters({
287
+ isLogarithmicAxis,
288
+ orientation,
289
+ visualizationType,
290
+ min,
291
+ max,
292
+ shouldAbbreviate
293
+ })
294
+
255
295
  // Calculate category label space for horizontal bar charts
256
296
  const categoryLabelSpace = useMemo(() => {
257
297
  return calculateHorizontalBarCategoryLabelWidth({
@@ -266,9 +306,19 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
266
306
  }, [isHorizontal, config.visualizationType, config.yAxis.labelPlacement, yScale, parentWidth])
267
307
 
268
308
  const horizontalYAxisLabelSpace = runtime.yAxis.label && !config.hideYAxisLabel ? 30 : 0
269
- if (isHorizontal && config.visualizationType === 'Bar') {
270
- runtime.yAxis.size = categoryLabelSpace + horizontalYAxisLabelSpace
271
- }
309
+
310
+ // Update y-axis width state when computed value changes (for horizontal bar charts)
311
+ useEffect(() => {
312
+ if (isHorizontal && config.visualizationType === 'Bar') {
313
+ const newWidth = categoryLabelSpace + horizontalYAxisLabelSpace
314
+ if (newWidth !== computedYAxisWidth) {
315
+ setComputedYAxisWidth(newWidth)
316
+ }
317
+ } else if (computedYAxisWidth !== null) {
318
+ // Reset to null for non-horizontal bar charts so we use config value
319
+ setComputedYAxisWidth(null)
320
+ }
321
+ }, [isHorizontal, config.visualizationType, categoryLabelSpace, horizontalYAxisLabelSpace, computedYAxisWidth])
272
322
 
273
323
  const [yTickCount, xTickCount] = ['yAxis', 'xAxis'].map(axis =>
274
324
  countNumOfTicks({ axis, max, runtime, currentViewport, isHorizontal, data, config, min })
@@ -312,65 +362,19 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
312
362
  const useDateSpanMonths = isDateTime && dateSpanMonths > xTickCount && !config.runtime.xAxis.manual
313
363
 
314
364
  // 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
365
  const chartHasTooltipGuides = () => {
359
366
  const { visualizationType } = config
360
367
  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
368
+ return TYPES_WITH_TOOLTIP_GUIDES.includes(visualizationType as any)
365
369
  }
366
370
 
367
- const getManualStep = () => {
371
+ const getManualStep = useCallback(() => {
368
372
  let manualStep = config.xAxis.manualStep
369
373
  if (config.xAxis.viewportStepCount && config.xAxis.viewportStepCount[currentViewport]) {
370
374
  manualStep = config.xAxis.viewportStepCount[currentViewport]
371
375
  }
372
376
  return manualStep
373
- }
377
+ }, [config.xAxis.manualStep, config.xAxis.viewportStepCount, currentViewport])
374
378
 
375
379
  const smallMultiplesSync = useSmallMultipleSynchronization(xMax, yMax, getXValueFromCoordinate)
376
380
 
@@ -384,11 +388,17 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
384
388
  y
385
389
  })
386
390
 
387
- smallMultiplesSync.onMouseMove?.(event)
391
+ // Warming Stripes handles its own sync at the rect level since
392
+ // getXValueFromCoordinate won't map correctly due to data sampling
393
+ if (visualizationType !== 'Warming Stripes') {
394
+ smallMultiplesSync.onMouseMove?.(event)
395
+ }
388
396
  }
389
397
 
390
398
  const onMouseLeave = () => {
391
- smallMultiplesSync.onMouseLeave?.()
399
+ if (visualizationType !== 'Warming Stripes') {
400
+ smallMultiplesSync.onMouseLeave?.()
401
+ }
392
402
  }
393
403
 
394
404
  // Use custom hook to provide programmatic tooltip control for small multiples
@@ -399,24 +409,23 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
399
409
  setPoint,
400
410
  setShowHoverLine,
401
411
  handleTooltipMouseOver,
402
- hideTooltip
412
+ hideTooltip,
413
+ setSynchronizedXValue
403
414
  })
404
415
 
405
416
  // Make sure the chart is visible if in the editor
406
- /* eslint-disable react-hooks/exhaustive-deps */
407
417
  useEffect(() => {
408
418
  const element = document.querySelector('.isEditor')
409
419
  if (element) {
410
- // parent element is visible
411
- setAnimatedChart(prevState => true)
420
+ setAnimatedChart(true)
412
421
  }
413
- }) /* eslint-disable-line */
422
+ }, [])
414
423
 
415
424
  // If the chart is in view, set to animate if it has not already played
416
425
  useEffect(() => {
417
426
  if (dataRef?.isIntersecting === true && config.animate) {
418
427
  setTimeout(() => {
419
- setAnimatedChart(prevState => true)
428
+ setAnimatedChart(true)
420
429
  }, 500)
421
430
  }
422
431
  }, [dataRef?.isIntersecting, config.animate])
@@ -461,9 +470,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
461
470
  const svgHeight = initialHeight + svgAdditionalHeight
462
471
 
463
472
  // Parent container height (includes brush if active)
464
- const brushHeight = 70
465
- const brushMargin = 10
466
- const brushHeightWithMargin = config.xAxis.brushActive ? brushHeight + brushMargin : 0
473
+ const brushHeightWithMargin = config.xAxis.brushActive ? BRUSH_HEIGHT + BRUSH_MARGIN : 0
467
474
  const parentHeight = svgHeight + brushHeightWithMargin
468
475
 
469
476
  if (!parentRef.current) return
@@ -491,15 +498,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
491
498
  const legendIsLeftOrRight =
492
499
  legend?.position !== 'top' && legend?.position !== 'bottom' && !isLegendWrapViewport(currentViewport)
493
500
  legendRef.current.style.transform = legendIsLeftOrRight ? `translateY(${topLabelOnGridlineHeight}px)` : 'none'
494
- }, [
495
- axisBottomRef.current,
496
- config,
497
- bottomLabelStart,
498
- config.xAxis.brushActive,
499
- currentViewport,
500
- topYLabelRef.current,
501
- initialHeight
502
- ])
501
+ }, [axisBottomRef.current, config, config.xAxis.brushActive, currentViewport, topYLabelRef.current, initialHeight])
503
502
 
504
503
  useEffect(() => {
505
504
  if (!tooltipOpen) return
@@ -513,8 +512,8 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
513
512
 
514
513
  const rightSideRemainingSpace = parentWidth - dataXPosition
515
514
 
516
- const rightSide = rightSideRemainingSpace <= tooltipWidth && dataXPosition > parentWidth / 2 - 10
517
- const maxWidth = rightSide ? dataXPosition - 10 : parentWidth - (dataXPosition + 6)
515
+ const rightSide = rightSideRemainingSpace <= tooltipWidth && dataXPosition > parentWidth / 2 - TOOLTIP_EDGE_BUFFER
516
+ const maxWidth = rightSide ? dataXPosition - TOOLTIP_EDGE_BUFFER : parentWidth - (dataXPosition + TOOLTIP_OFFSET)
518
517
  tooltipRef.current.node.style.maxWidth = `${maxWidth}px`
519
518
  }, [tooltipOpen, tooltipData])
520
519
 
@@ -531,150 +530,6 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
531
530
  )
532
531
  }
533
532
 
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
533
  return isNaN(parentWidth) ? (
679
534
  <React.Fragment></React.Fragment>
680
535
  ) : (
@@ -707,60 +562,20 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
707
562
  >
708
563
  {!isDraggingAnnotation && <Bar width={parentWidth} height={initialHeight} fill={'transparent'}></Bar>}{' '}
709
564
  {/* 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
- )}
565
+ {/* Actual LeftAxis is drawn after visualization */}
566
+ {!TYPES_WITHOUT_GRID.includes(visualizationType as any) && config.yAxis.type !== 'categorical' && (
567
+ <LeftAxisGridlines
568
+ yScale={yScale}
569
+ xMax={xMax}
570
+ yAxisWidth={yAxisWidth}
571
+ numTicks={handleNumTicks}
572
+ yLabelOffset={yLabelOffset}
573
+ axisLabelFontSize={axisLabelFontSize}
574
+ />
575
+ )}
761
576
  {/* Horizontal chart grid lines */}
762
577
  {runtime.xAxis.gridLines && orientation === 'horizontal' && (
763
- <Group left={Number(runtime.yAxis.size)}>
578
+ <Group left={yAxisWidth}>
764
579
  {xScale.ticks(xTickCount).map((tickValue, i) => {
765
580
  const tickPosition = xScale(tickValue)
766
581
  return (
@@ -774,167 +589,46 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
774
589
  })}
775
590
  </Group>
776
591
  )}
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
592
  {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}
593
+ <PairedBarAxis
594
+ g1xScale={g1xScale}
595
+ g2xScale={g2xScale}
791
596
  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
597
  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}
598
+ yAxisWidth={yAxisWidth}
599
+ bottomLabelStart={bottomLabelStart}
600
+ tickLabelFontSize={tickLabelFontSize}
601
+ axisLabelFontSize={axisLabelFontSize}
602
+ axisBottomRef={axisBottomRef}
603
+ xAxisLabelRefs={xAxisLabelRefs}
604
+ tickLabelFont={GET_TEXT_WIDTH_FONT}
936
605
  />
937
606
  )}
607
+ {/* Visualization Renderer - handles all chart type rendering */}
608
+ <VisualizationRenderer
609
+ xScale={xScale}
610
+ yScale={yScale}
611
+ xMax={xMax}
612
+ yMax={yMax}
613
+ seriesScale={seriesScale}
614
+ xScaleNoPadding={xScaleNoPadding}
615
+ min={min}
616
+ max={max}
617
+ parentWidth={parentWidth}
618
+ yAxisWidth={yAxisWidth}
619
+ forestHeight={forestHeight}
620
+ animatedChart={animatedChart}
621
+ tooltipData={tooltipData}
622
+ showTooltip={showTooltip}
623
+ handleTooltipMouseOver={handleTooltipMouseOver}
624
+ handleTooltipMouseOff={handleTooltipMouseOff}
625
+ handleTooltipClick={handleTooltipClick}
626
+ getXAxisData={getXAxisData}
627
+ getYAxisData={getYAxisData}
628
+ svgRef={svgRef}
629
+ forestPlotRightLabelRef={forestPlotRightLabelRef}
630
+ synchronizedXValue={synchronizedXValue}
631
+ />
938
632
  {/* Brush moved to separate overlay - no longer in main SVG */}
939
633
  {/* y anchors */}
940
634
  {config.yAxis.anchors &&
@@ -944,11 +638,10 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
944
638
 
945
639
  if (!anchor.value) return
946
640
  if (config.yAxis.labelPlacement === 'Below Bar') {
947
- const textOffset = -6.5
948
- middleOffset = textOffset + Number(config.series.length * config.barHeight) / config.series.length
641
+ middleOffset =
642
+ BELOW_BAR_TEXT_OFFSET + Number(config.series.length * config.barHeight) / config.series.length
949
643
  } else {
950
- const paddingOffset = 8
951
- middleOffset = paddingOffset
644
+ middleOffset = LABEL_PADDING_OFFSET
952
645
  }
953
646
 
954
647
  if (!position) return
@@ -956,13 +649,13 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
956
649
  return (
957
650
  // prettier-ignore
958
651
  <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
- />
652
+ key={`yAxis-${anchor.value}--${index}`}
653
+ strokeDasharray={handleLineType(anchor.lineStyle)}
654
+ stroke={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
655
+ className='anchor-y'
656
+ from={{ x: Number(runtime.yAxis.size), y: position - middleOffset }}
657
+ to={{ x: Number(runtime.yAxis.size) + Number(xMax), y: position - middleOffset }}
658
+ />
966
659
  )
967
660
  })}
968
661
  {/* x anchors */}
@@ -992,14 +685,14 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
992
685
  return (
993
686
  // prettier-ignore
994
687
  <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
- />
688
+ key={`xAxis-${anchor.value}--${index}`}
689
+ strokeDasharray={handleLineType(anchor.lineStyle)}
690
+ stroke={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
691
+ fill={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
692
+ className='anchor-x'
693
+ from={{ x: Number(anchorPosition) + Number(padding), y: 0 }}
694
+ to={{ x: Number(anchorPosition) + Number(padding), y: yMax }}
695
+ />
1003
696
  )
1004
697
  })}
1005
698
  {/* we are handling regions in bar charts differently, so that we can calculate the bar group into the region space. */}
@@ -1019,7 +712,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1019
712
  )}
1020
713
  {isNoDataAvailable && (
1021
714
  <Text
1022
- x={Number(runtime.yAxis.size) + Number(xMax / 2)}
715
+ x={yAxisWidth + Number(xMax / 2)}
1023
716
  y={initialHeight / 2 - (config.xAxis.padding || 0) / 2}
1024
717
  textAnchor='middle'
1025
718
  >
@@ -1032,405 +725,65 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1032
725
  <HoverLine xMax={xMax} yMax={yMax} point={point} tooltipData={tooltipData} orientation='vertical' />
1033
726
  </>
1034
727
  )}
1035
- <Group left={Number(config.runtime.yAxis.size)}>
728
+ <Group left={yAxisWidth}>
1036
729
  <Annotation.Draggable
1037
730
  xScale={xScale}
1038
731
  yScale={yScale}
1039
732
  xScaleAnnotation={xScaleAnnotation}
733
+ yScaleAnnotation={yScaleAnnotation}
1040
734
  xMax={xMax}
735
+ yMax={yMax}
736
+ seriesScale={seriesScale}
1041
737
  svgRef={svgRef}
1042
738
  onDragStateChange={handleDragStateChange}
1043
739
  />
1044
740
  </Group>
1045
741
  {/* Highlighted regions */}
1046
742
  {/* Y axis */}
1047
- {!['Spark Line', 'Forest Plot', 'Warming Stripes'].includes(visualizationType) &&
743
+ {/* Horizon charts don't have a grid but should be rendered with a left axis */}
744
+ {(!TYPES_WITHOUT_GRID.includes(visualizationType as any) || visualizationType === 'Horizon Chart') &&
1048
745
  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}
746
+ <LeftAxis
747
+ yScale={yScale}
748
+ xScale={xScale}
749
+ yMax={yMax}
750
+ xMax={xMax}
751
+ yAxisWidth={yAxisWidth}
1056
752
  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>
753
+ tickLabelFontSize={tickLabelFontSize}
754
+ axisLabelFontSize={axisLabelFontSize}
755
+ handleLeftTickFormatting={handleLeftTickFormatting}
756
+ topYLabelRef={topYLabelRef}
757
+ suffixRef={suffixRef}
758
+ suffixWidth={suffixWidth}
759
+ horizontalYAxisLabelSpace={horizontalYAxisLabelSpace}
760
+ categoryLabelSpace={categoryLabelSpace}
761
+ yLabelOffset={yLabelOffset}
762
+ />
1348
763
  )}
1349
764
  {config.yAxis.type === 'categorical' && config.orientation === 'vertical' && (
1350
765
  <CategoricalYAxis
1351
766
  yScale={yScale}
1352
767
  xMax={xMax}
1353
768
  yMax={yMax}
1354
- leftSize={Number(runtime.yAxis.size) - config.yAxis.axisPadding}
769
+ leftSize={yAxisWidth - config.yAxis.axisPadding}
1355
770
  />
1356
771
  )}
1357
772
  {/* Right Axis */}
1358
773
  {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>
774
+ <RightAxis
775
+ yScaleRight={yScaleRight}
776
+ yMax={yMax}
777
+ xMax={xMax}
778
+ yAxisWidth={yAxisWidth}
779
+ tickLabelFontSize={tickLabelFontSize}
780
+ axisLabelFontSize={axisLabelFontSize}
781
+ />
1429
782
  )}
1430
783
  {hasTopAxis && config.topAxis.hasLine && (
1431
784
  <AxisTop
1432
785
  stroke='#333'
1433
- left={Number(runtime.yAxis.size)}
786
+ left={yAxisWidth}
1434
787
  scale={xScale}
1435
788
  hideTicks
1436
789
  hideZero
@@ -1441,189 +794,27 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1441
794
  )}
1442
795
  {/* X axis */}
1443
796
  {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>
797
+ <BottomAxis
798
+ axisBottomRef={axisBottomRef}
799
+ xScale={xScale}
800
+ yMax={yMax}
801
+ xMax={xMax}
802
+ yAxisWidth={yAxisWidth}
803
+ xTickCount={xTickCount}
804
+ tickLabelFontSize={tickLabelFontSize}
805
+ axisLabelFontSize={axisLabelFontSize}
806
+ handleBottomTickFormatting={handleBottomTickFormatting}
807
+ useDateSpanMonths={useDateSpanMonths}
808
+ dateSpanMonths={dateSpanMonths}
809
+ xAxisDataMapped={xAxisDataMapped}
810
+ uniqueXAxisDataMapped={uniqueXAxisDataMapped}
811
+ isDateTime={isDateTime}
812
+ bottomLabelStart={bottomLabelStart}
813
+ parentWidth={parentWidth}
814
+ xAxisLabelRefs={xAxisLabelRefs}
815
+ xAxisTitleRef={xAxisTitleRef}
816
+ getManualStep={getManualStep}
817
+ />
1627
818
  )}
1628
819
  </svg>
1629
820
  {!isDraggingAnnotation &&
@@ -1680,10 +871,10 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1680
871
  <div
1681
872
  style={{
1682
873
  position: 'relative',
1683
- marginTop: '10px',
1684
- left: `${runtime.yAxis.size || 0}px`,
1685
- width: `${Math.max(xMax, 100)}px`,
1686
- height: '70px',
874
+ marginTop: `${BRUSH_MARGIN}px`,
875
+ left: `${yAxisWidth}px`,
876
+ width: `${Math.max(xMax, BRUSH_MIN_WIDTH)}px`,
877
+ height: `${BRUSH_HEIGHT}px`,
1687
878
  pointerEvents: 'auto',
1688
879
  zIndex: 15,
1689
880
  touchAction: 'none', // Enable touch interactions for brush
@@ -1693,7 +884,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1693
884
  }}
1694
885
  className='brush-overlay'
1695
886
  >
1696
- <BrushSelector xMax={Math.max(xMax, 100)} yMax={70} />
887
+ <BrushSelector key={brushKeyRef.current} xMax={Math.max(xMax, BRUSH_MIN_WIDTH)} yMax={BRUSH_HEIGHT} />
1697
888
  </div>
1698
889
  )}
1699
890
  </div>