@cdc/chart 4.25.11 → 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 (181) 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 +51401 -50814
  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/feature/pie/planet-pie-example-config.json +48 -2
  9. package/examples/line-chart-states.json +1085 -0
  10. package/examples/private/123.json +694 -0
  11. package/examples/private/DEV-12100.json +1303 -0
  12. package/examples/private/anchor-issue.json +4094 -0
  13. package/examples/private/backwards-slider.json +10430 -0
  14. package/examples/private/cat-y.json +1235 -0
  15. package/examples/private/data-points.json +228 -0
  16. package/examples/private/georgia.csv +160 -0
  17. package/examples/private/height.json +3915 -0
  18. package/examples/private/links.json +569 -0
  19. package/examples/private/quadrant.txt +30 -0
  20. package/examples/private/test-forecast.json +5510 -0
  21. package/examples/private/timeline-data.json +1 -0
  22. package/examples/private/timeline.json +389 -0
  23. package/examples/private/warming-stripe-test.json +2578 -0
  24. package/examples/private/warming-stripes.json +4763 -0
  25. package/examples/radar-chart-simple.json +133 -0
  26. package/examples/radar-chart.json +148 -0
  27. package/examples/tech-adoption-with-links.json +560 -0
  28. package/index.html +1 -36
  29. package/package.json +59 -60
  30. package/src/CdcChartComponent.tsx +206 -89
  31. package/src/_stories/Chart.Anchors.stories.tsx +10 -0
  32. package/src/_stories/Chart.BoxPlot.stories.tsx +7 -0
  33. package/src/_stories/Chart.CI.stories.tsx +13 -0
  34. package/src/_stories/Chart.Combo.stories.tsx +17 -0
  35. package/src/_stories/Chart.CustomColors.stories.tsx +4 -0
  36. package/src/_stories/Chart.DynamicSeries.stories.tsx +19 -0
  37. package/src/_stories/Chart.Filters.stories.tsx +4 -0
  38. package/src/_stories/Chart.Forecast.stories.tsx +4 -0
  39. package/src/_stories/Chart.HTMLInDataTable.stories.tsx +22 -0
  40. package/src/_stories/Chart.Legend.Gradient.stories.tsx +28 -0
  41. package/src/_stories/Chart.Patterns.stories.tsx +4 -0
  42. package/src/_stories/Chart.PreserveDecimals.stories.tsx +25 -0
  43. package/src/_stories/Chart.Regions.Categorical.stories.tsx +161 -0
  44. package/src/_stories/Chart.Regions.DateScale.stories.tsx +216 -0
  45. package/src/_stories/Chart.Regions.DateTimeScale.stories.tsx +312 -0
  46. package/src/_stories/Chart.ScatterPlot.stories.tsx +4 -0
  47. package/src/_stories/Chart.SmallMultiples.stories.tsx +16 -0
  48. package/src/_stories/Chart.stories.tsx +45 -0
  49. package/src/_stories/Chart.tooltip.stories.tsx +7 -0
  50. package/src/_stories/ChartAnnotation.stories.tsx +10 -0
  51. package/src/_stories/ChartAxisLabels.stories.tsx +4 -0
  52. package/src/_stories/ChartAxisTitles.stories.tsx +10 -0
  53. package/src/_stories/ChartBar.Editor.stories.tsx +11 -6
  54. package/src/_stories/ChartBrush.Editor.stories.tsx +295 -0
  55. package/src/_stories/ChartBrush.Matrix.Continuous.stories.tsx +41 -0
  56. package/src/_stories/ChartBrush.Matrix.Date.stories.tsx +114 -0
  57. package/src/_stories/ChartBrush.Matrix.DateTime.stories.tsx +78 -0
  58. package/src/_stories/ChartBrush.stories.tsx +57 -0
  59. package/src/_stories/ChartEditor.Editor.stories.tsx +3 -5
  60. package/src/_stories/ChartEditor.stories.tsx +7 -0
  61. package/src/_stories/ChartLine.QuadrantAngles.stories.tsx +89 -0
  62. package/src/_stories/ChartLine.Suppression.stories.tsx +7 -0
  63. package/src/_stories/ChartLine.Symbols.stories.tsx +4 -0
  64. package/src/_stories/ChartPrefixSuffix.stories.tsx +46 -1
  65. package/src/_stories/TechAdoptionWithLinks.stories.tsx +34 -0
  66. package/src/_stories/_mock/brush_continuous.json +86 -0
  67. package/src/_stories/_mock/brush_date_large.json +176 -0
  68. package/src/_stories/_mock/brush_enabled.json +326 -0
  69. package/src/_stories/_mock/brush_mock.json +2 -69
  70. package/src/_stories/_mock/horizontal-bars-dynamic-y-axis.json +413 -0
  71. package/src/_stories/_mock/line_chart_angle_near_zero_fall.json +195 -0
  72. package/src/_stories/_mock/line_chart_angle_near_zero_rise.json +195 -0
  73. package/src/_stories/_mock/line_chart_angle_q1_steep_upward.json +195 -0
  74. package/src/_stories/_mock/line_chart_angle_q2_gentle_downward.json +195 -0
  75. package/src/_stories/_mock/line_chart_angle_q3_steep_downward.json +195 -0
  76. package/src/_stories/_mock/line_chart_angle_q4_gentle_upward.json +195 -0
  77. package/src/_stories/_mock/line_chart_quadrant_angles.json +264 -0
  78. package/src/components/Annotations/components/AnnotationDraggable.styles.css +11 -17
  79. package/src/components/Annotations/components/AnnotationDraggable.tsx +240 -116
  80. package/src/components/Annotations/components/AnnotationDropdown.styles.css +1 -2
  81. package/src/components/Annotations/components/AnnotationDropdown.tsx +8 -12
  82. package/src/components/Annotations/components/AnnotationList.styles.css +4 -10
  83. package/src/components/Annotations/components/AnnotationList.tsx +5 -4
  84. package/src/components/Annotations/components/findNearestDatum.ts +75 -85
  85. package/src/components/Annotations/helpers/getVisibleAnnotations.ts +38 -0
  86. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +1 -2
  87. package/src/components/Axis/BottomAxis.tsx +270 -0
  88. package/src/components/Axis/Categorical.Axis.tsx +6 -7
  89. package/src/components/Axis/LeftAxis.tsx +404 -0
  90. package/src/components/Axis/LeftAxisGridlines.tsx +77 -0
  91. package/src/components/Axis/PairedBarAxis.tsx +186 -0
  92. package/src/components/Axis/README.md +94 -0
  93. package/src/components/Axis/RightAxis.tsx +108 -0
  94. package/src/components/Axis/axis.constants.ts +21 -0
  95. package/src/components/Axis/index.ts +7 -0
  96. package/src/components/BarChart/components/BarChart.Horizontal.tsx +178 -24
  97. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +3 -1
  98. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +1 -0
  99. package/src/components/BarChart/components/BarChart.Vertical.tsx +6 -8
  100. package/src/components/BarChart/components/BarChart.tsx +7 -1
  101. package/src/components/BarChart/components/context.tsx +1 -0
  102. package/src/components/BarChart/helpers/useBarChart.ts +14 -2
  103. package/src/components/Brush/BrushSelector.tsx +1390 -0
  104. package/src/components/Brush/MiniChartPreview.tsx +400 -0
  105. package/src/components/DeviationBar.jsx +9 -7
  106. package/src/components/EditorPanel/EditorPanel.tsx +2734 -2595
  107. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +60 -22
  108. package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +56 -34
  109. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +137 -30
  110. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +2 -0
  111. package/src/components/EditorPanel/components/Panels/Panel.Radar.tsx +353 -0
  112. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +0 -1
  113. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +30 -25
  114. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +42 -28
  115. package/src/components/EditorPanel/components/Panels/index.tsx +2 -0
  116. package/src/components/EditorPanel/useEditorPermissions.ts +81 -39
  117. package/src/components/HorizonChart/HorizonChart.tsx +131 -0
  118. package/src/components/HorizonChart/components/HorizonBand.tsx +160 -0
  119. package/src/components/HorizonChart/helpers/calculateHorizonBands.ts +27 -0
  120. package/src/components/HorizonChart/helpers/getHorizonLayerColors.ts +40 -0
  121. package/src/components/HorizonChart/index.tsx +3 -0
  122. package/src/components/Legend/Legend.Component.tsx +52 -4
  123. package/src/components/Legend/Legend.tsx +4 -3
  124. package/src/components/Legend/LegendValueRange.tsx +77 -0
  125. package/src/components/Legend/helpers/createFormatLabels.tsx +164 -2
  126. package/src/components/Legend/helpers/generateValueRanges.ts +92 -0
  127. package/src/components/Legend/helpers/index.ts +10 -6
  128. package/src/components/LineChart/helpers/README.md +292 -0
  129. package/src/components/LineChart/helpers/labelPositioning.test.ts +245 -0
  130. package/src/components/LineChart/helpers/labelPositioning.ts +304 -0
  131. package/src/components/LineChart/index.tsx +44 -8
  132. package/src/components/LinearChart/README.md +109 -0
  133. package/src/components/LinearChart/VisualizationRenderer.tsx +267 -0
  134. package/src/components/LinearChart/linearChart.constants.ts +84 -0
  135. package/src/components/LinearChart/tests/LinearChart.test.tsx +201 -0
  136. package/src/components/LinearChart/tests/mockConfigContext.ts +129 -0
  137. package/src/components/LinearChart/utils/tickFormatting.ts +146 -0
  138. package/src/components/LinearChart.tsx +338 -1082
  139. package/src/components/PairedBarChart.jsx +20 -3
  140. package/src/components/PieChart/PieChart.tsx +1 -1
  141. package/src/components/RadarChart/RadarAxis.tsx +78 -0
  142. package/src/components/RadarChart/RadarChart.tsx +298 -0
  143. package/src/components/RadarChart/RadarGrid.tsx +64 -0
  144. package/src/components/RadarChart/RadarPolygon.tsx +91 -0
  145. package/src/components/RadarChart/helpers.ts +83 -0
  146. package/src/components/RadarChart/index.tsx +3 -0
  147. package/src/components/Regions/components/Regions.tsx +365 -122
  148. package/src/components/ScatterPlot/ScatterPlot.jsx +2 -2
  149. package/src/components/SmallMultiples/SmallMultipleTile.tsx +5 -1
  150. package/src/components/WarmingStripes/WarmingStripes.tsx +230 -0
  151. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +35 -0
  152. package/src/components/WarmingStripes/WarmingStripesGradientLegend.tsx +104 -0
  153. package/src/components/WarmingStripes/index.tsx +3 -0
  154. package/src/data/initial-state.js +17 -2
  155. package/src/helpers/calculateHorizontalBarCategoryLabelWidth.ts +57 -0
  156. package/src/helpers/getExcludedData.ts +4 -0
  157. package/src/helpers/getMinMax.ts +12 -7
  158. package/src/helpers/handleChartAriaLabels.ts +19 -19
  159. package/src/helpers/handleLineType.ts +22 -18
  160. package/src/helpers/sizeHelpers.ts +0 -20
  161. package/src/helpers/smallMultiplesHelpers.ts +1 -1
  162. package/src/hooks/useChartHoverAnalytics.tsx +10 -9
  163. package/src/hooks/useProgrammaticTooltip.ts +23 -2
  164. package/src/hooks/useScales.ts +18 -1
  165. package/src/hooks/useTooltip.tsx +34 -10
  166. package/src/scss/DataTable.scss +0 -4
  167. package/src/scss/main.scss +22 -3
  168. package/src/selectors/README.md +68 -0
  169. package/src/store/chart.reducer.ts +2 -0
  170. package/src/test/CdcChart.test.jsx +1 -1
  171. package/src/types/ChartConfig.ts +21 -0
  172. package/src/types/ChartContext.ts +1 -0
  173. package/src/types/Horizon.ts +64 -0
  174. package/src/types/Label.ts +1 -0
  175. package/src/utils/analyticsTracking.ts +19 -0
  176. package/LICENSE +0 -201
  177. package/src/components/Annotations/components/helpers/index.tsx +0 -46
  178. package/src/components/Brush/BrushChart.tsx +0 -128
  179. package/src/components/Brush/BrushController.tsx +0 -71
  180. package/src/components/Brush/types.tsx +0 -8
  181. package/src/components/BrushChart.tsx +0 -223
@@ -1,7 +1,17 @@
1
- import React, { forwardRef, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
1
+ import React, {
2
+ forwardRef,
3
+ useContext,
4
+ useEffect,
5
+ useLayoutEffect,
6
+ useImperativeHandle,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ useCallback
11
+ } from 'react'
2
12
 
3
13
  // Libraries
4
- import { AxisLeft, AxisBottom, AxisRight, AxisTop } from '@visx/axis'
14
+ import { AxisTop } from '@visx/axis'
5
15
  import { Group } from '@visx/group'
6
16
  import { Line, Bar } from '@visx/shape'
7
17
  import { Tooltip as ReactTooltip } from 'react-tooltip'
@@ -12,33 +22,25 @@ import _ from 'lodash'
12
22
 
13
23
  // CDC Components
14
24
  import { isDateScale } from '@cdc/core/helpers/cove/date'
15
- import { AreaChartStacked } from './AreaChart'
16
- import BarChart from './BarChart'
17
25
  import ConfigContext from '../ConfigContext'
18
- import BoxPlotVertical from './BoxPlot/BoxPlot.Vertical'
19
- import BoxPlotHorizontal from './BoxPlot/BoxPlot.Horizontal'
20
- import ScatterPlot from './ScatterPlot'
21
- import DeviationBar from './DeviationBar'
22
26
  import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
23
- import Forecasting from './Forecasting'
24
- import LineChart from './LineChart'
25
- import ForestPlot from './ForestPlot'
26
- import PairedBarChart from './PairedBarChart'
27
27
  import useIntersectionObserver from '../hooks/useIntersectionObserver'
28
28
  import Regions from './Regions'
29
- import CategoricalYAxis from './Axis/Categorical.Axis'
30
- import BrushChart from './Brush/BrushController'
29
+ import { CategoricalYAxis, LeftAxis, LeftAxisGridlines, BottomAxis, PairedBarAxis, RightAxis } from './Axis'
30
+ import BrushSelector from './Brush/BrushSelector'
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'
31
34
 
32
35
  // Helpers
33
36
  import { isLegendWrapViewport, isMobileFontViewport } from '@cdc/core/helpers/viewports'
34
- import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
35
- import { calcInitialHeight, handleAutoPaddingRight } from '../helpers/sizeHelpers'
36
- import { filterAndShiftLinearDateTicks } from '../helpers/filterAndShiftLinearDateTicks'
37
+ import { calcInitialHeight } from '../helpers/sizeHelpers'
38
+ import { calculateHorizontalBarCategoryLabelWidth } from '../helpers/calculateHorizontalBarCategoryLabelWidth'
37
39
 
38
40
  // Hooks
39
41
  import useReduceData from '../hooks/useReduceData'
40
42
  import useRightAxis from '../hooks/useRightAxis'
41
- import useScales, { getTickValues } from '../hooks/useScales'
43
+ import useScales from '../hooks/useScales'
42
44
  import { useProgrammaticTooltip } from '../hooks/useProgrammaticTooltip'
43
45
  import { useSmallMultipleSynchronization } from '../hooks/useSmallMultipleSynchronization'
44
46
 
@@ -47,7 +49,6 @@ import { useTooltip as useCoveTooltip } from '../hooks/useTooltip'
47
49
  import { useChartHoverAnalytics } from '../hooks/useChartHoverAnalytics'
48
50
  import { useEditorPermissions } from './EditorPanel/useEditorPermissions'
49
51
  import Annotation from './Annotations'
50
- import { BlurStrokeText } from '@cdc/core/components/BlurStrokeText'
51
52
  import { countNumOfTicks } from '../helpers/countNumOfTicks'
52
53
  import HoverLine from './HoverLine/HoverLine'
53
54
  import { SmallMultiples } from './SmallMultiples'
@@ -57,15 +58,36 @@ type LinearChartProps = {
57
58
  parentHeight: number
58
59
  }
59
60
 
61
+ // Axis and tick constants
60
62
  const BOTTOM_LABEL_PADDING = 9
61
63
  const X_TICK_LABEL_PADDING = 4.5
62
64
  const DEFAULT_TICK_LENGTH = 8
63
- const MONTH_AS_MS = 1000 * 60 * 60 * 24 * 30
65
+ const DEFAULT_MAX_TICK_ROTATION = 90
66
+
67
+ // Font sizes
64
68
  const TICK_LABEL_FONT_SIZE = 16
65
69
  const TICK_LABEL_FONT_SIZE_SMALL = 13
66
70
  const AXIS_LABEL_FONT_SIZE = 18
67
71
  const AXIS_LABEL_FONT_SIZE_SMALL = 14
68
- 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
69
91
 
70
92
  type TooltipData = {
71
93
  dataXPosition?: number
@@ -84,12 +106,9 @@ type UseTooltipReturn<T = TooltipData> = {
84
106
  const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, parentWidth }, svgRef) => {
85
107
  // prettier-ignore
86
108
  const {
87
- colorScale,
88
109
  config,
89
- convertLineToBarGraph,
90
110
  currentViewport,
91
111
  vizViewport,
92
- dimensions,
93
112
  formatDate,
94
113
  formatNumber,
95
114
  handleChartAriaLabels,
@@ -102,36 +121,38 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
102
121
  parentRef,
103
122
  tableData,
104
123
  transformedData: data,
105
- seriesHighlight
106
124
  } = useContext(ConfigContext)
107
125
 
108
126
  // CONFIG
109
127
  // todo: start destructuring this file for conciseness
110
- const {
111
- heights,
112
- visualizationType,
113
- visualizationSubType,
114
- orientation,
115
- xAxis,
116
- yAxis,
117
- runtime,
118
- legend,
119
- forestPlot,
120
- brush,
121
- dataFormat,
122
- debugSvg
123
- } = config
124
-
125
- const { labelsAboveGridlines, hideAxis, inlineLabel } = config.yAxis
128
+ const { visualizationType, orientation, xAxis, yAxis, runtime, legend, forestPlot, debugSvg } = config
129
+
130
+ const { inlineLabel } = config.yAxis
126
131
 
127
132
  // HOOKS % STATES
128
- const { minValue, maxValue, existPositiveValue, isAllLine } = useReduceData(config, data)
129
- const { visSupportsReactTooltip } = useEditorPermissions()
133
+ // When brush is active, use tableData (full dataset) for min/max calculation
134
+ // so the y-axis shows the full range, but still use filtered data for rendering
135
+ const dataForMinMax = config.xAxis.brushActive && tableData && tableData.length > 0 ? tableData : data
136
+ const { minValue, maxValue, existPositiveValue, isAllLine } = useReduceData(config, dataForMinMax)
137
+
138
+ const { visSupportsSmallMultiples } = useEditorPermissions()
130
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
+
131
149
  const [animatedChart, setAnimatedChart] = useState(false)
132
150
  const [showHoverLine, setShowHoverLine] = useState(false)
133
151
  const [point, setPoint] = useState({ x: 0, y: 0 })
134
152
  const [suffixWidth, setSuffixWidth] = useState(0)
153
+ const [calculatedSvgHeight, setCalculatedSvgHeight] = useState<number | null>(null)
154
+ const [axisUpdateKey, setAxisUpdateKey] = useState(0)
155
+ const [synchronizedXValue, setSynchronizedXValue] = useState<any>(null)
135
156
 
136
157
  // REFS
137
158
  const axisBottomRef = useRef(null)
@@ -141,7 +162,6 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
141
162
  const triggerRef = useRef()
142
163
  const xAxisLabelRefs = useRef([])
143
164
  const xAxisTitleRef = useRef(null)
144
- const gridLineRefs = useRef([])
145
165
  const tooltipRef = useRef(null)
146
166
 
147
167
  const dataRef = useIntersectionObserver(triggerRef, {
@@ -167,57 +187,75 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
167
187
 
168
188
  // height before bottom axis
169
189
  const initialHeight = useMemo(
170
- () => calcInitialHeight(config, currentViewport),
171
- [config, currentViewport, parentHeight, config.heights?.vertical, config.heights?.horizontal]
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
+ ]
172
200
  )
173
201
  const forestHeight = useMemo(() => initialHeight + forestRowsHeight, [initialHeight, forestRowsHeight])
174
202
 
175
- // width
176
- const width = useMemo(() => {
177
- const initialWidth = dimensions[0]
178
- const legendHidden = legend?.hide
179
- const legendOnTopOrBottom = ['bottom', 'top'].includes(config.legend?.position)
180
- const legendWrapped = isLegendWrapViewport(currentViewport)
181
-
182
- const legendShowingLeftOrRight = !isForestPlot && !legendHidden && !legendOnTopOrBottom && !legendWrapped
183
-
184
- if (!legendShowingLeftOrRight) return initialWidth
185
-
186
- if (legendRef.current) {
187
- const legendStyle = getComputedStyle(legendRef.current)
188
- return (
189
- initialWidth -
190
- legendRef.current.getBoundingClientRect().width -
191
- parseInt(legendStyle.marginLeft) -
192
- parseInt(legendStyle.marginRight)
193
- )
194
- }
195
-
196
- return initialWidth * 0.73
197
- }, [dimensions[0], config.legend, currentViewport, legendRef.current])
198
-
199
203
  // Used to calculate the y position of the x-axis title
204
+ // Dependencies trigger recalc when axis config changes (affects label rendering/size)
200
205
  const bottomLabelStart = useMemo(() => {
201
206
  xAxisLabelRefs.current = xAxisLabelRefs.current?.filter(label => label)
202
207
  if (!xAxisLabelRefs.current.length) return
203
208
  const tallestLabel = Math.max(...xAxisLabelRefs.current.map(label => label.getBBox().height))
204
209
  return tallestLabel + X_TICK_LABEL_PADDING + DEFAULT_TICK_LENGTH
205
- }, [dimensions[0], config.xAxis, xAxisLabelRefs.current, config.xAxis.tickRotation])
210
+ }, [parentWidth, config.xAxis.tickRotation, config.xAxis.hideLabel, xAxisLabelRefs.current, axisUpdateKey])
206
211
 
207
- // xMax and yMax
208
- const xMax = width - runtime.yAxis.size - (visualizationType === 'Combo' ? config.yAxis.rightAxisSize : 0)
209
212
  const yMax = initialHeight + forestRowsHeight
210
213
 
211
214
  const isNoDataAvailable = config.filters?.length > 0 && data.length === 0
212
215
 
213
- const getXAxisData = d =>
214
- isDateScale(config.runtime.xAxis)
215
- ? parseDate(d[config.runtime.originalXAxis.dataKey]).getTime()
216
- : d[config.runtime.originalXAxis.dataKey]
217
- const getYAxisData = (d, seriesKey) => d[seriesKey]
218
- 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])
227
+
228
+ // Get unique x-axis values (for cases where multiple series share the same x-axis value)
229
+ // This is important for brush filtering where we want to count unique dates, not total data points
230
+ const uniqueXAxisDataMapped = useMemo(() => {
231
+ const unique = new Set()
232
+ const result: any[] = []
233
+ for (const value of xAxisDataMapped) {
234
+ const key = value instanceof Date ? value.getTime() : typeof value === 'number' ? value : String(value)
235
+ if (!unique.has(key)) {
236
+ unique.add(key)
237
+ result.push(value)
238
+ }
239
+ }
240
+ return result
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
+
219
248
  const { yScaleRight, hasRightAxis } = useRightAxis({ config, yMax, data })
220
249
 
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)
258
+
221
259
  const {
222
260
  xScale,
223
261
  yScale,
@@ -226,10 +264,9 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
226
264
  g2xScale,
227
265
  xScaleNoPadding,
228
266
  xScaleAnnotation,
267
+ yScaleAnnotation,
229
268
  min,
230
- max,
231
- leftMax,
232
- rightMax
269
+ max
233
270
  } = useScales({
234
271
  data,
235
272
  tableData,
@@ -240,15 +277,49 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
240
277
  existPositiveValue,
241
278
  xAxisDataMapped,
242
279
  yMax,
243
- dimensions,
244
- xMax:
245
- parentWidth -
246
- Number(config.orientation === 'horizontal' ? config.xAxis.size : config.yAxis.size) -
247
- (hasRightAxis ? config.yAxis.rightAxisSize : 0),
280
+ xMax,
248
281
  needsYAxisAutoPadding,
249
282
  currentViewport
250
283
  })
251
284
 
285
+ // Consolidated tick formatters
286
+ const { handleLeftTickFormatting, handleBottomTickFormatting } = useTickFormatters({
287
+ isLogarithmicAxis,
288
+ orientation,
289
+ visualizationType,
290
+ min,
291
+ max,
292
+ shouldAbbreviate
293
+ })
294
+
295
+ // Calculate category label space for horizontal bar charts
296
+ const categoryLabelSpace = useMemo(() => {
297
+ return calculateHorizontalBarCategoryLabelWidth({
298
+ yScale,
299
+ chartWidth: parentWidth,
300
+ formatDate,
301
+ parseDate,
302
+ tickLabelFont: GET_TEXT_WIDTH_FONT,
303
+ xAxisType: config.runtime.xAxis?.type,
304
+ labelPlacement: config.yAxis.labelPlacement
305
+ })
306
+ }, [isHorizontal, config.visualizationType, config.yAxis.labelPlacement, yScale, parentWidth])
307
+
308
+ const horizontalYAxisLabelSpace = runtime.yAxis.label && !config.hideYAxisLabel ? 30 : 0
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])
322
+
252
323
  const [yTickCount, xTickCount] = ['yAxis', 'xAxis'].map(axis =>
253
324
  countNumOfTicks({ axis, max, runtime, currentViewport, isHorizontal, data, config, min })
254
325
  )
@@ -291,65 +362,19 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
291
362
  const useDateSpanMonths = isDateTime && dateSpanMonths > xTickCount && !config.runtime.xAxis.manual
292
363
 
293
364
  // GETTERS & FUNCTIONS
294
- const handleLeftTickFormatting = (tick, index, ticks) => {
295
- if (isLogarithmicAxis && tick === 0.1) {
296
- //when logarithmic scale applied change value of first tick
297
- tick = 0
298
- }
299
-
300
- if (config.data && !config.data[index] && visualizationType === 'Forest Plot') return
301
- if (config.visualizationType === 'Forest Plot') return config.data[index][config.xAxis.dataKey]
302
- if (isDateScale(runtime.yAxis)) return formatDate(parseDate(tick))
303
- if (orientation === 'vertical' && max - min < 3 && !config.dataFormat?.roundTo)
304
- return formatNumber(tick, 'left', shouldAbbreviate, false, false, '1', { index, length: ticks.length })
305
- if (orientation === 'vertical') {
306
- // TODO suggestion: pass all options as object key/values to allow for more flexibility
307
- return formatNumber(tick, 'left', shouldAbbreviate, false, false, undefined, { index, length: ticks.length })
308
- }
309
- return tick
310
- }
311
-
312
- const handleBottomTickFormatting = (tick, i, ticks) => {
313
- if (isLogarithmicAxis && tick === 0.1) {
314
- // when logarithmic scale applied change value FIRST of tick
315
- tick = 0
316
- }
317
-
318
- if (isDateScale(runtime.xAxis) && config.visualizationType !== 'Forest Plot') {
319
- return formatDate(tick, i, ticks)
320
- }
321
- if (orientation === 'horizontal' && config.visualizationType !== 'Forest Plot')
322
- return formatNumber(tick, 'left', shouldAbbreviate)
323
- if (config.xAxis.type === 'continuous' && config.visualizationType !== 'Forest Plot')
324
- return formatNumber(tick, 'bottom', shouldAbbreviate)
325
- if (config.visualizationType === 'Forest Plot')
326
- return formatNumber(
327
- tick,
328
- 'left',
329
- config.dataFormat.abbreviated,
330
- config.runtime.xAxis.prefix,
331
- config.runtime.xAxis.suffix,
332
- Number(config.dataFormat.roundTo)
333
- )
334
- return tick
335
- }
336
-
337
365
  const chartHasTooltipGuides = () => {
338
366
  const { visualizationType } = config
339
367
  if (visualizationType === 'Combo' && runtime.forecastingSeriesKeys > 0) return true
340
- if (visualizationType === 'Area Chart') return true
341
- if (visualizationType === 'Line') return true
342
- if (visualizationType === 'Bar') return true
343
- return false
368
+ return TYPES_WITH_TOOLTIP_GUIDES.includes(visualizationType as any)
344
369
  }
345
370
 
346
- const getManualStep = () => {
371
+ const getManualStep = useCallback(() => {
347
372
  let manualStep = config.xAxis.manualStep
348
373
  if (config.xAxis.viewportStepCount && config.xAxis.viewportStepCount[currentViewport]) {
349
374
  manualStep = config.xAxis.viewportStepCount[currentViewport]
350
375
  }
351
376
  return manualStep
352
- }
377
+ }, [config.xAxis.manualStep, config.xAxis.viewportStepCount, currentViewport])
353
378
 
354
379
  const smallMultiplesSync = useSmallMultipleSynchronization(xMax, yMax, getXValueFromCoordinate)
355
380
 
@@ -363,11 +388,17 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
363
388
  y
364
389
  })
365
390
 
366
- 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
+ }
367
396
  }
368
397
 
369
398
  const onMouseLeave = () => {
370
- smallMultiplesSync.onMouseLeave?.()
399
+ if (visualizationType !== 'Warming Stripes') {
400
+ smallMultiplesSync.onMouseLeave?.()
401
+ }
371
402
  }
372
403
 
373
404
  // Use custom hook to provide programmatic tooltip control for small multiples
@@ -378,41 +409,23 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
378
409
  setPoint,
379
410
  setShowHoverLine,
380
411
  handleTooltipMouseOver,
381
- hideTooltip
412
+ hideTooltip,
413
+ setSynchronizedXValue
382
414
  })
383
415
 
384
- // EFFECTS
385
- // Adjust padding on the right side of the chart to accommodate for overflow
386
- useEffect(() => {
387
- if (!parentRef.current || !parentWidth || !gridLineRefs.current.length) return
388
-
389
- const [updatePadding, paddingToAdd] = handleAutoPaddingRight(parentRef, xAxisLabelRefs, parentWidth)
390
-
391
- if (!updatePadding) return
392
-
393
- parentRef.current.style.paddingRight = `${paddingToAdd}px`
394
- // subtract padding from grid line's x1 value
395
- gridLineRefs.current.forEach(gridLine => {
396
- if (!gridLine) return
397
- gridLine.setAttribute('x1', xMax - paddingToAdd)
398
- })
399
- }, [parentWidth, parentHeight, data])
400
-
401
416
  // Make sure the chart is visible if in the editor
402
- /* eslint-disable react-hooks/exhaustive-deps */
403
417
  useEffect(() => {
404
418
  const element = document.querySelector('.isEditor')
405
419
  if (element) {
406
- // parent element is visible
407
- setAnimatedChart(prevState => true)
420
+ setAnimatedChart(true)
408
421
  }
409
- }) /* eslint-disable-line */
422
+ }, [])
410
423
 
411
424
  // If the chart is in view, set to animate if it has not already played
412
425
  useEffect(() => {
413
426
  if (dataRef?.isIntersecting === true && config.animate) {
414
427
  setTimeout(() => {
415
- setAnimatedChart(prevState => true)
428
+ setAnimatedChart(true)
416
429
  }, 500)
417
430
  }
418
431
  }, [dataRef?.isIntersecting, config.animate])
@@ -449,16 +462,22 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
449
462
  const topLabelOnGridline = topYLabelRef.current && yAxis.labelsAboveGridlines
450
463
 
451
464
  // Heights to add
452
-
453
- const brushHeight = 25
454
- const brushHeightWithMargin = config.xAxis.brushActive ? brushHeight + brushHeight : 0
455
465
  const forestRowsHeight = isForestPlot ? config.data.length * forestPlot.rowHeight : 0
456
466
  const topLabelOnGridlineHeight = topLabelOnGridline ? topYLabelRef.current.getBBox().height : 0
457
- const additionalHeight = axisBottomHeight + brushHeightWithMargin + forestRowsHeight + topLabelOnGridlineHeight
458
- const newHeight = initialHeight + additionalHeight
467
+
468
+ // SVG height (without brush)
469
+ const svgAdditionalHeight = axisBottomHeight + forestRowsHeight + topLabelOnGridlineHeight
470
+ const svgHeight = initialHeight + svgAdditionalHeight
471
+
472
+ // Parent container height (includes brush if active)
473
+ const brushHeightWithMargin = config.xAxis.brushActive ? BRUSH_HEIGHT + BRUSH_MARGIN : 0
474
+ const parentHeight = svgHeight + brushHeightWithMargin
475
+
459
476
  if (!parentRef.current) return
477
+ parentRef.current.style.height = `${parentHeight}px`
460
478
 
461
- parentRef.current.style.height = `${newHeight}px`
479
+ // Set the calculated SVG height via state to ensure it's used on render
480
+ setCalculatedSvgHeight(svgHeight)
462
481
 
463
482
  /* Adding text above the top gridline overflows the bounds of the svg.
464
483
  To accommodate for this we need to...
@@ -472,22 +491,14 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
472
491
  const svg = internalSvgRef.current
473
492
  if (!svg) return
474
493
  const parentWidthFromRef = parentRef.current.getBoundingClientRect().width
475
- svg.setAttribute('viewBox', `0 ${-topLabelOnGridlineHeight} ${parentWidthFromRef} ${newHeight}`)
494
+ svg.setAttribute('viewBox', `0 ${-topLabelOnGridlineHeight} ${parentWidthFromRef} ${svgHeight}`)
476
495
 
477
496
  // translate legend match viewBox-adjusted height
478
497
  if (!legendRef.current) return
479
498
  const legendIsLeftOrRight =
480
499
  legend?.position !== 'top' && legend?.position !== 'bottom' && !isLegendWrapViewport(currentViewport)
481
500
  legendRef.current.style.transform = legendIsLeftOrRight ? `translateY(${topLabelOnGridlineHeight}px)` : 'none'
482
- }, [
483
- axisBottomRef.current,
484
- config,
485
- bottomLabelStart,
486
- config.xAxis.brushActive,
487
- currentViewport,
488
- topYLabelRef.current,
489
- initialHeight
490
- ])
501
+ }, [axisBottomRef.current, config, config.xAxis.brushActive, currentViewport, topYLabelRef.current, initialHeight])
491
502
 
492
503
  useEffect(() => {
493
504
  if (!tooltipOpen) return
@@ -501,13 +512,13 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
501
512
 
502
513
  const rightSideRemainingSpace = parentWidth - dataXPosition
503
514
 
504
- const rightSide = rightSideRemainingSpace <= tooltipWidth && dataXPosition > parentWidth / 2 - 10
505
- 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)
506
517
  tooltipRef.current.node.style.maxWidth = `${maxWidth}px`
507
518
  }, [tooltipOpen, tooltipData])
508
519
 
509
- // Check if small multiples are enabled - if so, render SmallMultiples instead
510
- if (config.smallMultiples?.mode) {
520
+ // Check if small multiples are enabled and supported - if so, render SmallMultiples instead
521
+ if (config.smallMultiples?.mode && visSupportsSmallMultiples()) {
511
522
  return (
512
523
  <SmallMultiples
513
524
  config={config}
@@ -519,151 +530,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
519
530
  )
520
531
  }
521
532
 
522
- // Render Functions
523
- const generatePairedBarAxis = () => {
524
- const axisMaxHeight = bottomLabelStart + BOTTOM_LABEL_PADDING
525
-
526
- const getTickPositions = (ticks, xScale) => {
527
- if (!ticks.length) return false
528
- // filter out first index
529
- const filteredTicks = ticks.filter(tick => tick.index !== 0)
530
- const numberOfTicks = filteredTicks?.length
531
- const xMaxHalf = xScale.range()[0] || xMax / 2
532
- const tickWidthAll = filteredTicks.map(tick =>
533
- getTextWidth(formatNumber(tick.value, 'left'), GET_TEXT_WIDTH_FONT)
534
- )
535
- const accumulator = 100
536
- const sumOfTickWidth = tickWidthAll.reduce((a, b) => a + b, accumulator)
537
- const spaceBetweenEachTick = (xMaxHalf - sumOfTickWidth) / numberOfTicks
538
- // Determine the position of each tick
539
- let positions = [0]
540
- for (let i = 1; i < tickWidthAll.length; i++) {
541
- positions[i] = positions[i - 1] + tickWidthAll[i - 1] + spaceBetweenEachTick
542
- }
543
-
544
- // Check if ticks are overlapping
545
- let isTicksOverlapping = false
546
- tickWidthAll.forEach((_, i) => {
547
- if (positions[i] + tickWidthAll[i] > positions[i + 1]) {
548
- isTicksOverlapping = true
549
- return
550
- }
551
- })
552
- return isTicksOverlapping
553
- }
554
- return (
555
- <>
556
- <AxisBottom
557
- top={yMax}
558
- left={Number(runtime.yAxis.size)}
559
- label={runtime.xAxis.label}
560
- tickFormat={isDateScale(runtime.xAxis) ? formatDate : formatNumber}
561
- scale={g1xScale}
562
- stroke='#333'
563
- tickStroke='#333'
564
- numTicks={runtime.xAxis.numTicks || undefined}
565
- >
566
- {props => {
567
- return (
568
- <Group className='bottom-axis'>
569
- {props.ticks.map((tick, i) => {
570
- const isTicksOverlapping = getTickPositions(props.ticks, g1xScale)
571
- const maxTickRotation = Number(config.xAxis.maxTickRotation) || 90
572
- const isResponsiveTicks = config.isResponsiveTicks && isTicksOverlapping
573
- const angle =
574
- tick.index !== 0 && (isResponsiveTicks ? maxTickRotation : Number(config.yAxis.tickRotation))
575
- const textAnchor = angle && tick.index !== 0 ? 'end' : 'middle'
576
-
577
- return (
578
- <Group key={`vx-tick-${tick.value}-${i}`} className={'vx-axis-tick'}>
579
- {!runtime.yAxis.hideTicks && <Line from={tick.from} to={tick.to} stroke='#333' />}
580
- {!runtime.yAxis.hideLabel && (
581
- <Text // prettier-ignore
582
- innerRef={el => (xAxisLabelRefs.current[i] = el)}
583
- x={tick.to.x}
584
- y={tick.to.y}
585
- angle={-angle}
586
- verticalAnchor={angle ? 'middle' : 'start'}
587
- textAnchor={textAnchor}
588
- fontSize={tickLabelFontSize}
589
- >
590
- {formatNumber(tick.value, 'left')}
591
- </Text>
592
- )}
593
- </Group>
594
- )
595
- })}
596
- {!runtime.yAxis.hideAxis && <Line from={props.axisFromPoint} to={props.axisToPoint} stroke='#333' />}
597
- </Group>
598
- )
599
- }}
600
- </AxisBottom>
601
- <AxisBottom
602
- innerRef={axisBottomRef}
603
- top={yMax}
604
- left={Number(runtime.yAxis.size)}
605
- label={runtime.xAxis.label}
606
- tickFormat={
607
- isDateScale(runtime.xAxis) ? formatDate : runtime.xAxis.dataKey !== 'Year' ? formatNumber : tick => tick
608
- }
609
- scale={g2xScale}
610
- stroke='#333'
611
- tickStroke='#333'
612
- numTicks={runtime.xAxis.numTicks || undefined}
613
- >
614
- {props => {
615
- return (
616
- <>
617
- <Group className='bottom-axis'>
618
- {props.ticks.map((tick, i) => {
619
- const isTicksOverlapping = getTickPositions(props.ticks, g2xScale)
620
- const maxTickRotation = Number(config.xAxis.maxTickRotation) || 90
621
- const isResponsiveTicks = config.isResponsiveTicks && isTicksOverlapping
622
- const angle =
623
- tick.index !== 0 && (isResponsiveTicks ? maxTickRotation : Number(config.yAxis.tickRotation))
624
- const textAnchor = angle && tick.index !== 0 ? 'end' : 'middle'
625
- if (!i) return <></> // skip first tick to avoid overlapping 0's
626
- return (
627
- <Group key={`vx-tick-${tick.value}-${i}`} className={'vx-axis-tick'}>
628
- {!runtime.yAxis.hideTicks && <Line from={tick.from} to={tick.to} stroke='#333' />}
629
- {!runtime.yAxis.hideLabel && (
630
- <Text // prettier-ignore
631
- x={tick.to.x}
632
- y={tick.to.y + X_TICK_LABEL_PADDING}
633
- angle={-angle}
634
- verticalAnchor={angle ? 'middle' : 'start'}
635
- textAnchor={textAnchor}
636
- fontSize={tickLabelFontSize}
637
- >
638
- {formatNumber(tick.value, 'left')}
639
- </Text>
640
- )}
641
- </Group>
642
- )
643
- })}
644
- {!runtime.yAxis.hideAxis && <Line from={props.axisFromPoint} to={props.axisToPoint} stroke='#333' />}
645
- </Group>
646
- <Group>
647
- <Text
648
- className='x-axis-title-label'
649
- x={xMax / 2}
650
- y={axisMaxHeight}
651
- stroke='#333'
652
- textAnchor={'middle'}
653
- verticalAnchor='start'
654
- fontSize={axisLabelFontSize}
655
- >
656
- {!config.hideXAxisLabel ? runtime.xAxis.label : null}
657
- </Text>
658
- </Group>
659
- </>
660
- )
661
- }}
662
- </AxisBottom>
663
- </>
664
- )
665
- }
666
- return isNaN(width) ? (
533
+ return isNaN(parentWidth) ? (
667
534
  <React.Fragment></React.Fragment>
668
535
  ) : (
669
536
  <ErrorBoundary component='LinearChart'>
@@ -676,7 +543,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
676
543
  ref={internalSvgRef}
677
544
  onMouseMove={onMouseMove}
678
545
  width={parentWidth + config.yAxis.rightAxisSize}
679
- height={isNoDataAvailable ? 1 : parentHeight}
546
+ height={isNoDataAvailable ? 1 : calculatedSvgHeight ?? parentHeight}
680
547
  className={`linear ${config.animate ? 'animated' : ''} ${animatedChart && config.animate ? 'animate' : ''} ${
681
548
  debugSvg && 'debug'
682
549
  } ${isDraggingAnnotation && 'dragging-annotation'}`}
@@ -695,229 +562,74 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
695
562
  >
696
563
  {!isDraggingAnnotation && <Bar width={parentWidth} height={initialHeight} fill={'transparent'}></Bar>}{' '}
697
564
  {/* GRID LINES */}
698
- {/* Actual AxisLeft is drawn after visualization */}
699
- {!['Spark Line', 'Forest Plot'].includes(visualizationType) && config.yAxis.type !== 'categorical' && (
700
- <AxisLeft
701
- scale={yScale}
702
- left={Number(runtime.yAxis.size) - config.yAxis.axisPadding}
703
- numTicks={handleNumTicks}
704
- >
705
- {props => {
706
- const axisCenter =
707
- config.orientation === 'horizontal'
708
- ? Math.abs(props.axisToPoint.y - props.axisFromPoint.y) / 2
709
- : (props.axisFromPoint.y - props.axisToPoint.y) / 2
710
- return (
711
- <Group className='left-axis'>
712
- {props.ticks.map((tick, i) => {
713
- const showTicks = String(tick.value).startsWith('1') || tick.value === 0.1 ? 'block' : 'none'
714
- const hideFirstGridLine = tick.index === 0 && tick.value === 0 && config.xAxis.hideAxis
715
-
716
- return (
717
- <Group key={`vx-tick-${tick.value}-${i}`} className={'vx-axis-tick'}>
718
- {runtime.yAxis.gridLines && !hideFirstGridLine ? (
719
- <Line
720
- innerRef={el => (gridLineRefs.current[i] = el)}
721
- key={`${tick.value}--hide-hideGridLines`}
722
- display={(isLogarithmicAxis && showTicks).toString()}
723
- from={{ x: tick.from.x + xMax, y: tick.from.y }}
724
- to={tick.from}
725
- stroke='#d6d6d6'
726
- />
727
- ) : (
728
- ''
729
- )}
730
- </Group>
731
- )
732
- })}
733
- <Text
734
- className='y-label'
735
- textAnchor='middle'
736
- verticalAnchor='start'
737
- transform={`translate(${-1 * runtime.yAxis.size + yLabelOffset}, ${axisCenter}) rotate(-90)`}
738
- fontWeight='bold'
739
- fill={config.yAxis.labelColor}
740
- fontSize={axisLabelFontSize}
741
- >
742
- {!config.hideYAxisLabel ? props.label : null}
743
- </Text>
744
- </Group>
745
- )
746
- }}
747
- </AxisLeft>
748
- )}
749
- {visualizationType === 'Paired Bar' && generatePairedBarAxis()}
750
- {visualizationType === 'Deviation Bar' && config.runtime.series?.length === 1 && (
751
- <DeviationBar animatedChart={animatedChart} xScale={xScale} yScale={yScale} width={xMax} height={yMax} />
752
- )}
753
- {visualizationType === 'Paired Bar' && <PairedBarChart originalWidth={width} width={xMax} height={yMax} />}
754
- {visualizationType === 'Scatter Plot' && (
755
- <ScatterPlot
756
- xScale={xScale}
565
+ {/* Actual LeftAxis is drawn after visualization */}
566
+ {!TYPES_WITHOUT_GRID.includes(visualizationType as any) && config.yAxis.type !== 'categorical' && (
567
+ <LeftAxisGridlines
757
568
  yScale={yScale}
758
- getXAxisData={getXAxisData}
759
- getYAxisData={getYAxisData}
760
- xMax={xMax}
761
- yMax={yMax}
762
- handleTooltipMouseOver={handleTooltipMouseOver}
763
- handleTooltipMouseOff={handleTooltipMouseOff}
764
- handleTooltipClick={handleTooltipClick}
765
- tooltipData={tooltipData}
766
- showTooltip={showTooltip}
767
- />
768
- )}
769
- {visualizationType === 'Box Plot' && config.orientation === 'vertical' && (
770
- <BoxPlotVertical
771
- seriesScale={seriesScale}
772
569
  xMax={xMax}
773
- yMax={yMax}
774
- min={min}
775
- max={max}
776
- xScale={xScale}
777
- yScale={yScale}
778
- />
779
- )}
780
- {visualizationType === 'Box Plot' && config.orientation === 'horizontal' && (
781
- <BoxPlotHorizontal
782
- seriesScale={seriesScale}
783
- xMax={xMax}
784
- yMax={yMax}
785
- min={min}
786
- max={max}
787
- xScale={xScale}
788
- yScale={yScale}
570
+ yAxisWidth={yAxisWidth}
571
+ numTicks={handleNumTicks}
572
+ yLabelOffset={yLabelOffset}
573
+ axisLabelFontSize={axisLabelFontSize}
789
574
  />
790
575
  )}
791
- {((visualizationType === 'Area Chart' && config.visualizationSubType === 'stacked') ||
792
- visualizationType === 'Combo') && (
793
- <AreaChartStacked
794
- xScale={xScale}
795
- yScale={yScale}
796
- yMax={yMax}
797
- xMax={xMax}
798
- chartRef={svgRef}
799
- width={xMax}
800
- height={yMax}
801
- handleTooltipMouseOver={handleTooltipMouseOver}
802
- handleTooltipMouseOff={handleTooltipMouseOff}
803
- tooltipData={tooltipData}
804
- showTooltip={showTooltip}
805
- />
576
+ {/* Horizontal chart grid lines */}
577
+ {runtime.xAxis.gridLines && orientation === 'horizontal' && (
578
+ <Group left={yAxisWidth}>
579
+ {xScale.ticks(xTickCount).map((tickValue, i) => {
580
+ const tickPosition = xScale(tickValue)
581
+ return (
582
+ <Line
583
+ key={`horizontal-gridline-${tickValue}-${i}`}
584
+ from={{ x: tickPosition, y: 0 }}
585
+ to={{ x: tickPosition, y: yMax }}
586
+ stroke='#d6d6d6'
587
+ />
588
+ )
589
+ })}
590
+ </Group>
806
591
  )}
807
- {(visualizationType === 'Bar' || visualizationType === 'Combo' || convertLineToBarGraph) && (
808
- <BarChart
809
- xScale={xScale}
810
- yScale={yScale}
811
- seriesScale={seriesScale}
812
- xMax={xMax}
592
+ {visualizationType === 'Paired Bar' && (
593
+ <PairedBarAxis
594
+ g1xScale={g1xScale}
595
+ g2xScale={g2xScale}
813
596
  yMax={yMax}
814
- getXAxisData={getXAxisData}
815
- getYAxisData={getYAxisData}
816
- animatedChart={animatedChart}
817
- visible={animatedChart}
818
- handleTooltipMouseOver={handleTooltipMouseOver}
819
- handleTooltipMouseOff={handleTooltipMouseOff}
820
- handleTooltipClick={handleTooltipClick}
821
- tooltipData={tooltipData}
822
- showTooltip={showTooltip}
823
- chartRef={svgRef}
824
- />
825
- )}
826
- {(visualizationType === 'Combo' || visualizationType === 'Bump Chart') && (
827
- <LineChart
828
- xScale={xScale}
829
- yScale={yScale}
830
- getXAxisData={getXAxisData}
831
- getYAxisData={getYAxisData}
832
597
  xMax={xMax}
833
- yMax={yMax}
834
- seriesStyle={config.runtime.series}
835
- handleTooltipMouseOver={handleTooltipMouseOver}
836
- handleTooltipMouseOff={handleTooltipMouseOff}
837
- handleTooltipClick={handleTooltipClick}
838
- tooltipData={tooltipData}
839
- showTooltip={showTooltip}
840
- />
841
- )}
842
- {/* Line chart */}
843
- {/* TODO: Make this just line or combo? */}
844
- {!['Paired Bar', 'Box Plot', 'Area Chart', 'Scatter Plot', 'Deviation Bar', 'Forecasting', 'Bar'].includes(
845
- visualizationType
846
- ) &&
847
- !convertLineToBarGraph && (
848
- <>
849
- <LineChart
850
- xScale={xScale}
851
- yScale={yScale}
852
- getXAxisData={getXAxisData}
853
- getYAxisData={getYAxisData}
854
- xMax={xMax}
855
- yMax={yMax}
856
- seriesStyle={config.runtime.series}
857
- tooltipData={tooltipData}
858
- handleTooltipMouseOver={handleTooltipMouseOver}
859
- handleTooltipMouseOff={handleTooltipMouseOff}
860
- />
861
- </>
862
- )}
863
- {(visualizationType === 'Forecasting' || visualizationType === 'Combo') && (
864
- <Forecasting
865
- showTooltip={showTooltip}
866
- tooltipData={tooltipData}
867
- xScale={xScale}
868
- yScale={yScale}
869
- width={xMax}
870
- height={yMax}
871
- xScaleNoPadding={xScaleNoPadding}
872
- chartRef={svgRef}
873
- handleTooltipMouseOver={handleTooltipMouseOver}
874
- handleTooltipMouseOff={handleTooltipMouseOff}
875
- />
876
- )}
877
- {visualizationType === 'Forest Plot' && (
878
- <ForestPlot
879
- xScale={xScale}
880
- yScale={yScale}
881
- seriesScale={seriesScale}
882
- width={width}
883
- height={forestHeight}
884
- getXAxisData={getXAxisData}
885
- getYAxisData={getYAxisData}
886
- animatedChart={animatedChart}
887
- visible={animatedChart}
888
- handleTooltipMouseOver={handleTooltipMouseOver}
889
- handleTooltipMouseOff={handleTooltipMouseOff}
890
- handleTooltipClick={handleTooltipClick}
891
- tooltipData={tooltipData}
892
- showTooltip={showTooltip}
893
- chartRef={svgRef}
894
- config={config}
895
- 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}
896
605
  />
897
606
  )}
898
- {/*Brush chart */}
899
- {config.xAxis.brushActive && config.xAxis.type !== 'categorical' && <BrushChart xMax={xMax} yMax={yMax} />}
900
- {/* Line chart */}
901
- {/* TODO: Make this just line or combo? */}
902
- {!['Paired Bar', 'Box Plot', 'Area Chart', 'Scatter Plot', 'Deviation Bar', 'Forecasting', 'Bar'].includes(
903
- visualizationType
904
- ) &&
905
- !convertLineToBarGraph && (
906
- <>
907
- <LineChart
908
- xScale={xScale}
909
- yScale={yScale}
910
- getXAxisData={getXAxisData}
911
- getYAxisData={getYAxisData}
912
- xMax={xMax}
913
- yMax={yMax}
914
- seriesStyle={config.runtime.series}
915
- tooltipData={tooltipData}
916
- handleTooltipMouseOver={handleTooltipMouseOver}
917
- handleTooltipMouseOff={handleTooltipMouseOff}
918
- />
919
- </>
920
- )}
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
+ />
632
+ {/* Brush moved to separate overlay - no longer in main SVG */}
921
633
  {/* y anchors */}
922
634
  {config.yAxis.anchors &&
923
635
  config.yAxis.anchors.map((anchor, index) => {
@@ -926,11 +638,10 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
926
638
 
927
639
  if (!anchor.value) return
928
640
  if (config.yAxis.labelPlacement === 'Below Bar') {
929
- const textOffset = -6.5
930
- 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
931
643
  } else {
932
- const paddingOffset = 8
933
- middleOffset = paddingOffset
644
+ middleOffset = LABEL_PADDING_OFFSET
934
645
  }
935
646
 
936
647
  if (!position) return
@@ -942,8 +653,8 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
942
653
  strokeDasharray={handleLineType(anchor.lineStyle)}
943
654
  stroke={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
944
655
  className='anchor-y'
945
- from={{ x: 0 + padding, y: position - middleOffset }}
946
- to={{ x: width - config.yAxis.rightAxisSize, y: position - middleOffset }}
656
+ from={{ x: Number(runtime.yAxis.size), y: position - middleOffset }}
657
+ to={{ x: Number(runtime.yAxis.size) + Number(xMax), y: position - middleOffset }}
947
658
  />
948
659
  )
949
660
  })}
@@ -996,12 +707,12 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
996
707
  hideTooltip={hideTooltip}
997
708
  tooltipData={tooltipData}
998
709
  yMax={yMax}
999
- width={width}
710
+ xMax={xMax}
1000
711
  />
1001
712
  )}
1002
713
  {isNoDataAvailable && (
1003
714
  <Text
1004
- x={Number(config.yAxis.size) + Number(xMax / 2)}
715
+ x={yAxisWidth + Number(xMax / 2)}
1005
716
  y={initialHeight / 2 - (config.xAxis.padding || 0) / 2}
1006
717
  textAnchor='middle'
1007
718
  >
@@ -1014,387 +725,65 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1014
725
  <HoverLine xMax={xMax} yMax={yMax} point={point} tooltipData={tooltipData} orientation='vertical' />
1015
726
  </>
1016
727
  )}
1017
- <Group left={Number(config.runtime.yAxis.size)}>
728
+ <Group left={yAxisWidth}>
1018
729
  <Annotation.Draggable
1019
730
  xScale={xScale}
1020
731
  yScale={yScale}
1021
732
  xScaleAnnotation={xScaleAnnotation}
733
+ yScaleAnnotation={yScaleAnnotation}
1022
734
  xMax={xMax}
735
+ yMax={yMax}
736
+ seriesScale={seriesScale}
1023
737
  svgRef={svgRef}
1024
738
  onDragStateChange={handleDragStateChange}
1025
739
  />
1026
740
  </Group>
1027
741
  {/* Highlighted regions */}
1028
742
  {/* Y axis */}
1029
- {!['Spark Line', 'Forest Plot'].includes(visualizationType) && config.yAxis.type !== 'categorical' && (
1030
- <AxisLeft
1031
- scale={yScale}
1032
- tickLength={isLogarithmicAxis ? 6 : 8}
1033
- left={Number(runtime.yAxis.size) - config.yAxis.axisPadding}
1034
- label={runtime.yAxis.label || runtime.yAxis.label}
1035
- stroke='#333'
1036
- tickFormat={handleLeftTickFormatting}
1037
- numTicks={handleNumTicks}
1038
- >
1039
- {props => {
1040
- const axisCenter =
1041
- config.orientation === 'horizontal'
1042
- ? Math.abs(props.axisToPoint.y - props.axisFromPoint.y) / 2
1043
- : (props.axisFromPoint.y - props.axisToPoint.y) / 2
1044
- const horizontalTickOffset =
1045
- yMax / props.ticks.length / 2 - (yMax / props.ticks.length) * (1 - config.barThickness) + 5
1046
- return (
1047
- <Group className='left-axis'>
1048
- {!config.yAxis.hideAxis && (
1049
- <Line
1050
- from={props.axisFromPoint}
1051
- to={
1052
- runtime.horizontal
1053
- ? {
1054
- x: 0,
1055
- y:
1056
- config.visualizationType === 'Forest Plot' ? parentHeight : Number(heights.horizontal)
1057
- }
1058
- : props.axisToPoint
1059
- }
1060
- stroke='#000'
1061
- />
1062
- )}
1063
- {orientation === 'vertical' && yScale.domain()[0] < 0 && (
1064
- // draw from the Left of the chart …
1065
- <Line
1066
- from={{ x: props.axisFromPoint.x, y: yScale(0) }}
1067
- to={{ x: xMax, y: yScale(0) }}
1068
- stroke='#333'
1069
- />
1070
- )}
1071
- {orientation === 'horizontal' && xScale.domain()[0] < 0 && (
1072
- <Line
1073
- // draw from the top of the char
1074
- from={{ x: xScale(0), y: 0 }}
1075
- to={{ x: xScale(0), y: yMax }}
1076
- stroke='#333'
1077
- />
1078
- )}
1079
- {visualizationType === 'Bar' && orientation === 'horizontal' && xScale.domain()[0] < 0 && (
1080
- <Line
1081
- from={{ x: xScale(0), y: 0 }}
1082
- to={{ x: xScale(0), y: yMax }}
1083
- stroke='#333'
1084
- strokeWidth={2}
1085
- />
1086
- )}
1087
- {props.ticks.map((tick, i) => {
1088
- const minY = props.ticks[0].to.y
1089
- const barMinHeight = 15 // 15 is the min height for bars by default
1090
- const showTicks = String(tick.value).startsWith('1') || tick.value === 0.1 ? 'block' : 'none'
1091
- const tickLength = showTicks === 'block' ? 7 : 0
1092
- const to = { x: tick.to.x - tickLength, y: tick.to.y }
1093
-
1094
- // Vertical value/suffix vars
1095
- const lastTick = props.ticks.length - 1 === i
1096
- const useInlineLabel = lastTick && inlineLabel
1097
- const hideTopTick = lastTick && inlineLabel && !inlineLabelHasNoSpace
1098
- const valueOnLinePadding = hideAxis ? -8 : -12
1099
- const labelXPadding = labelsAboveGridlines ? valueOnLinePadding : TICK_LABEL_MARGIN_RIGHT
1100
- const labelYPadding = labelsAboveGridlines ? 4 : 0
1101
- const labelX = tick.to.x - labelXPadding
1102
- const labelY = tick.to.y - labelYPadding
1103
- const labelVerticalAnchor = labelsAboveGridlines ? 'end' : 'middle'
1104
- const combineDomInlineLabelWithValue = inlineLabel && labelsAboveGridlines && lastTick
1105
- const formattedValue = useInlineLabel
1106
- ? String(tick?.formattedValue || '').replace(config.dataFormat.suffix, '')
1107
- : tick?.formattedValue
1108
-
1109
- return (
1110
- <Group key={`vx-tick-${tick.value}-${i}`} className={'vx-axis-tick'}>
1111
- {!runtime.yAxis.hideTicks && !labelsAboveGridlines && !hideTopTick && (
1112
- <Line
1113
- key={`${tick.value}--hide-hideTicks`}
1114
- from={tick.from}
1115
- to={isLogarithmicAxis ? to : tick.to}
1116
- stroke={config.yAxis.tickColor}
1117
- display={orientation === 'horizontal' ? 'none' : 'block'}
1118
- fontSize={tickLabelFontSize}
1119
- />
1120
- )}
1121
-
1122
- {orientation === 'horizontal' &&
1123
- visualizationType === 'Box Plot' &&
1124
- config.yAxis.labelPlacement === 'On Date/Category Axis' &&
1125
- !config.yAxis.hideLabel && (
1126
- <Text
1127
- x={tick.to.x}
1128
- y={yScale(tick.value) + yScale.bandwidth() / 2}
1129
- transform={`rotate(${
1130
- config.orientation === 'horizontal' ? config.runtime.yAxis.tickRotation || 0 : 0
1131
- }, ${tick.to.x}, ${tick.to.y})`}
1132
- verticalAnchor={'middle'}
1133
- textAnchor={'end'}
1134
- fontSize={tickLabelFontSize}
1135
- >
1136
- {tick.formattedValue}
1137
- </Text>
1138
- )}
1139
-
1140
- {orientation === 'horizontal' &&
1141
- visualizationType !== 'Box Plot' &&
1142
- visualizationSubType !== 'stacked' &&
1143
- config.yAxis.labelPlacement === 'On Date/Category Axis' &&
1144
- !config.yAxis.hideLabel && (
1145
- <Text
1146
- transform={`translate(${tick.to.x - 5}, ${
1147
- config.isLollipopChart
1148
- ? tick.to.y - minY
1149
- : tick.to.y -
1150
- minY +
1151
- (Number(config.barHeight * config.runtime.series.length) - barMinHeight) / 2
1152
- }) rotate(-${config.runtime.horizontal ? config.runtime.yAxis.tickRotation || 0 : 0})`}
1153
- verticalAnchor={'start'}
1154
- textAnchor={'end'}
1155
- fontSize={tickLabelFontSize}
1156
- >
1157
- {tick.formattedValue}
1158
- </Text>
1159
- )}
1160
-
1161
- {orientation === 'horizontal' &&
1162
- visualizationSubType === 'stacked' &&
1163
- config.yAxis.labelPlacement === 'On Date/Category Axis' &&
1164
- !config.yAxis.hideLabel && (
1165
- <Text
1166
- transform={`translate(${tick.to.x - 5}, ${
1167
- tick.to.y - minY + (Number(config.barHeight) - barMinHeight) / 2
1168
- }) rotate(-${runtime.horizontal ? runtime.yAxis.tickRotation : 0})`}
1169
- verticalAnchor={'start'}
1170
- textAnchor={'end'}
1171
- fontSize={tickLabelFontSize}
1172
- >
1173
- {tick.formattedValue}
1174
- </Text>
1175
- )}
1176
-
1177
- {orientation === 'horizontal' &&
1178
- visualizationType === 'Paired Bar' &&
1179
- !config.yAxis.hideLabel && (
1180
- <Text
1181
- transform={`translate(${tick.to.x - 5}, ${
1182
- tick.to.y - minY + Number(config.barHeight) / 2
1183
- }) rotate(-${runtime.horizontal ? runtime.yAxis.tickRotation : 0})`}
1184
- textAnchor={'end'}
1185
- verticalAnchor='middle'
1186
- fontSize={tickLabelFontSize}
1187
- >
1188
- {tick.formattedValue}
1189
- </Text>
1190
- )}
1191
- {orientation === 'horizontal' &&
1192
- visualizationType === 'Deviation Bar' &&
1193
- !config.yAxis.hideLabel && (
1194
- <Text
1195
- transform={`translate(${tick.to.x - 5}, ${
1196
- config.isLollipopChart
1197
- ? tick.to.y - minY + 2
1198
- : tick.to.y - minY + Number(config.barHeight) / 2
1199
- }) rotate(-${runtime.horizontal ? runtime.yAxis.tickRotation : 0})`}
1200
- textAnchor={'end'}
1201
- verticalAnchor='middle'
1202
- fontSize={tickLabelFontSize}
1203
- >
1204
- {tick.formattedValue}
1205
- </Text>
1206
- )}
1207
-
1208
- {orientation === 'vertical' &&
1209
- visualizationType === 'Bump Chart' &&
1210
- !config.yAxis.hideLabel && (
1211
- <>
1212
- <Text
1213
- display={config.useLogScale ? showTicks : 'block'}
1214
- dx={config.useLogScale ? -6 : 0}
1215
- x={config.runtime.horizontal ? tick.from.x + 2 : tick.to.x - 8.5}
1216
- y={tick.to.y - 13 + (config.runtime.horizontal ? horizontalTickOffset : 0)}
1217
- angle={-Number(config.yAxis.tickRotation) || 0}
1218
- verticalAnchor={config.runtime.horizontal ? 'start' : 'middle'}
1219
- textAnchor={config.runtime.horizontal ? 'start' : 'end'}
1220
- fill={config.yAxis.tickLabelColor}
1221
- fontSize={tickLabelFontSize}
1222
- >
1223
- {config.runtime.seriesLabelsAll[tick.formattedValue - 1]}
1224
- </Text>
1225
-
1226
- {(seriesHighlight.length === 0 ||
1227
- seriesHighlight.includes(
1228
- config.runtime.seriesLabelsAll[tick.formattedValue - 1]
1229
- )) && (
1230
- <rect
1231
- x={0 - Number(config.yAxis.size)}
1232
- y={tick.to.y - 8 + (config.runtime.horizontal ? horizontalTickOffset : 7)}
1233
- width={Number(config.yAxis.size) + xScale(xScale.domain()[0])}
1234
- height='2'
1235
- fill={colorScale(config.runtime.seriesLabelsAll[tick.formattedValue - 1])}
1236
- />
1237
- )}
1238
- </>
1239
- )}
1240
- {orientation === 'vertical' &&
1241
- visualizationType !== 'Paired Bar' &&
1242
- visualizationType !== 'Bump Chart' &&
1243
- !config.yAxis.hideLabel && (
1244
- <>
1245
- {/* INLINE LABEL BEHAVIOR: Dom suffix for 'inlineLabel' behavior */}
1246
- {/* inline label is shown alone and is allowed to 'overflow' to the right */}
1247
- {/* SPECIAL ONE CHAR CASE: a one character inlineLabel does not overflow */}
1248
- {/* IF VALUES ON LINE: suffix is combined with value to avoid having to calculate varying (now left-aligned) value widths */}
1249
- {inlineLabel && lastTick && !labelsAboveGridlines && (
1250
- <BlurStrokeText
1251
- innerRef={suffixRef}
1252
- display={isLogarithmicAxis ? showTicks : 'block'}
1253
- dx={isLogarithmicAxis ? -6 : 0}
1254
- x={labelX}
1255
- y={labelY}
1256
- angle={-Number(config.yAxis.tickRotation) || 0}
1257
- verticalAnchor={labelVerticalAnchor}
1258
- textAnchor={inlineLabelHasNoSpace ? 'end' : 'start'}
1259
- fill={config.yAxis.tickLabelColor}
1260
- stroke={'#fff'}
1261
- paintOrder={'stroke'} // keeps stroke under fill
1262
- strokeLinejoin='round'
1263
- style={{ whiteSpace: 'pre-wrap' }} // prevents leading spaces from being trimmed
1264
- fontSize={tickLabelFontSize}
1265
- >
1266
- {inlineLabel}
1267
- </BlurStrokeText>
1268
- )}
1269
-
1270
- {/* VALUE */}
1271
- <BlurStrokeText
1272
- innerRef={el => lastTick && (topYLabelRef.current = el)}
1273
- display={isLogarithmicAxis ? showTicks : 'block'}
1274
- dx={isLogarithmicAxis ? -6 : 0}
1275
- x={inlineLabelHasNoSpace ? labelX - suffixWidth : labelX}
1276
- y={labelY + (config.runtime.horizontal ? horizontalTickOffset : 0)}
1277
- angle={-Number(config.yAxis.tickRotation) || 0}
1278
- verticalAnchor={config.runtime.horizontal ? 'start' : labelVerticalAnchor}
1279
- textAnchor={config.runtime.horizontal || labelsAboveGridlines ? 'start' : 'end'}
1280
- fill={config.yAxis.tickLabelColor}
1281
- stroke={'#fff'}
1282
- disableStroke={!labelsAboveGridlines}
1283
- strokeLinejoin='round'
1284
- paintOrder={'stroke'} // keeps stroke under fill
1285
- style={{ whiteSpace: 'pre-wrap' }} // prevents leading spaces from being trimmed
1286
- fontSize={tickLabelFontSize}
1287
- >
1288
- {`${formattedValue}${combineDomInlineLabelWithValue ? inlineLabel : ''}`}
1289
- </BlurStrokeText>
1290
- </>
1291
- )}
1292
- </Group>
1293
- )
1294
- })}
1295
- <Text
1296
- className='y-label'
1297
- textAnchor='middle'
1298
- verticalAnchor='start'
1299
- transform={`translate(${-1 * runtime.yAxis.size + yLabelOffset}, ${axisCenter}) rotate(-90)`}
1300
- fontWeight='bold'
1301
- fill={config.yAxis.labelColor}
1302
- fontSize={axisLabelFontSize}
1303
- >
1304
- {!config.hideYAxisLabel ? props.label : null}
1305
- </Text>
1306
- </Group>
1307
- )
1308
- }}
1309
- </AxisLeft>
1310
- )}
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') &&
745
+ config.yAxis.type !== 'categorical' && (
746
+ <LeftAxis
747
+ yScale={yScale}
748
+ xScale={xScale}
749
+ yMax={yMax}
750
+ xMax={xMax}
751
+ yAxisWidth={yAxisWidth}
752
+ numTicks={handleNumTicks}
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
+ />
763
+ )}
1311
764
  {config.yAxis.type === 'categorical' && config.orientation === 'vertical' && (
1312
765
  <CategoricalYAxis
1313
- max={max}
1314
- maxValue={maxValue}
1315
- height={initialHeight}
766
+ yScale={yScale}
1316
767
  xMax={xMax}
1317
768
  yMax={yMax}
1318
- leftSize={Number(runtime.yAxis.size) - config.yAxis.axisPadding}
769
+ leftSize={yAxisWidth - config.yAxis.axisPadding}
1319
770
  />
1320
771
  )}
1321
772
  {/* Right Axis */}
1322
773
  {hasRightAxis && (
1323
- <AxisRight
1324
- scale={yScaleRight}
1325
- left={Number(width - config.yAxis.rightAxisSize)}
1326
- label={config.yAxis.rightLabel}
1327
- tickFormat={tick => formatNumber(tick, 'right')}
1328
- numTicks={runtime.yAxis.rightNumTicks || undefined}
1329
- labelOffset={45}
1330
- >
1331
- {props => {
1332
- const axisCenter =
1333
- config.orientation === 'horizontal'
1334
- ? (props.axisToPoint.y - props.axisFromPoint.y) / 2
1335
- : (props.axisFromPoint.y - props.axisToPoint.y) / 2
1336
- const horizontalTickOffset =
1337
- yMax / props.ticks.length / 2 - (yMax / props.ticks.length) * (1 - config.barThickness) + 5
1338
- return (
1339
- <Group className='right-axis'>
1340
- {props.ticks.map((tick, i) => {
1341
- return (
1342
- <Group key={`vx-tick-${tick.value}-${i}`} className='vx-axis-tick'>
1343
- {!runtime.yAxis.rightHideTicks && (
1344
- <Line
1345
- from={tick.from}
1346
- to={tick.to}
1347
- display={runtime.horizontal ? 'none' : 'block'}
1348
- stroke={config.yAxis.rightAxisTickColor}
1349
- />
1350
- )}
1351
-
1352
- {runtime.yAxis.rightGridLines ? (
1353
- <Line from={{ x: tick.from.x + xMax, y: tick.from.y }} to={tick.from} stroke='#d6d6d6' />
1354
- ) : (
1355
- ''
1356
- )}
1357
-
1358
- {!config.yAxis.rightHideLabel && (
1359
- <Text
1360
- x={tick.to.x}
1361
- y={tick.to.y + (runtime.horizontal ? horizontalTickOffset : 0)}
1362
- verticalAnchor={runtime.horizontal ? 'start' : 'middle'}
1363
- textAnchor={'start'}
1364
- fill={config.yAxis.rightAxisTickLabelColor}
1365
- fontSize={tickLabelFontSize}
1366
- >
1367
- {tick.formattedValue}
1368
- </Text>
1369
- )}
1370
- </Group>
1371
- )
1372
- })}
1373
- {!config.yAxis.rightHideAxis && (
1374
- <Line from={props.axisFromPoint} to={props.axisToPoint} stroke='#333' />
1375
- )}
1376
- <Text
1377
- className='y-label'
1378
- textAnchor='middle'
1379
- verticalAnchor='start'
1380
- transform={`translate(${
1381
- config.yAxis.rightLabelOffsetSize ? config.yAxis.rightLabelOffsetSize : 0
1382
- }, ${axisCenter}) rotate(-90)`}
1383
- fontWeight='bold'
1384
- fill={config.yAxis.rightAxisLabelColor}
1385
- fontSize={axisLabelFontSize}
1386
- >
1387
- {props.label}
1388
- </Text>
1389
- </Group>
1390
- )
1391
- }}
1392
- </AxisRight>
774
+ <RightAxis
775
+ yScaleRight={yScaleRight}
776
+ yMax={yMax}
777
+ xMax={xMax}
778
+ yAxisWidth={yAxisWidth}
779
+ tickLabelFontSize={tickLabelFontSize}
780
+ axisLabelFontSize={axisLabelFontSize}
781
+ />
1393
782
  )}
1394
783
  {hasTopAxis && config.topAxis.hasLine && (
1395
784
  <AxisTop
1396
785
  stroke='#333'
1397
- left={Number(runtime.yAxis.size)}
786
+ left={yAxisWidth}
1398
787
  scale={xScale}
1399
788
  hideTicks
1400
789
  hideZero
@@ -1405,181 +794,27 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1405
794
  )}
1406
795
  {/* X axis */}
1407
796
  {visualizationType !== 'Paired Bar' && visualizationType !== 'Spark Line' && (
1408
- <AxisBottom
1409
- innerRef={axisBottomRef}
1410
- top={
1411
- runtime.horizontal && config.visualizationType !== 'Forest Plot'
1412
- ? Number(heights.horizontal) + Number(config.xAxis.axisPadding)
1413
- : config.visualizationType === 'Forest Plot'
1414
- ? yMax + Number(config.xAxis.axisPadding)
1415
- : yMax
1416
- }
1417
- left={config.visualizationType !== 'Forest Plot' ? Number(runtime.yAxis.size) : 0}
1418
- label={runtime.xAxis.label}
1419
- tickFormat={handleBottomTickFormatting}
1420
- scale={xScale}
1421
- stroke='#333'
1422
- numTicks={useDateSpanMonths ? dateSpanMonths : xTickCount}
1423
- tickStroke='#333'
1424
- tickValues={
1425
- config.runtime.xAxis.manual
1426
- ? getTickValues(xAxisDataMapped, xScale, isDateTime ? xTickCount : getManualStep(), config)
1427
- : config.runtime.xAxis.type === 'date'
1428
- ? xAxisDataMapped
1429
- : undefined
1430
- }
1431
- >
1432
- {props => {
1433
- const hasDynamicCategory = config.series.some(s => s.dynamicCategory)
1434
- // For these charts, we generated all ticks in tickValues above, and now need to filter/shift them
1435
- // so the last tick is always labeled
1436
- if (config.runtime.xAxis.type === 'date' && !config.runtime.xAxis.manual && !hasDynamicCategory) {
1437
- props.ticks = filterAndShiftLinearDateTicks(config, props, xAxisDataMapped, formatDate)
1438
- }
1439
-
1440
- const distanceBetweenTicks =
1441
- useDateSpanMonths &&
1442
- xScale
1443
- .ticks(xTickCount)
1444
- .map(t =>
1445
- props.ticks.findIndex(
1446
- tick => (typeof tick.value === 'number' ? tick.value : tick.value.getTime()) === t.getTime()
1447
- )
1448
- )
1449
- .slice(0, 2)
1450
- .reduce((acc, curr) => curr - acc)
1451
-
1452
- // filter out every [distanceBetweenTicks] tick starting from the end, so the last tick is always labeled
1453
- const filteredTicks = useDateSpanMonths
1454
- ? [...props.ticks]
1455
- .reverse()
1456
- .filter((_, i) => i % distanceBetweenTicks === 0)
1457
- .reverse()
1458
- .map((tick, i, arr) => ({
1459
- ...tick,
1460
- // reformat in case showYearsOnce, since first month of year may have changed
1461
- formattedValue: handleBottomTickFormatting(tick.value, i, arr)
1462
- }))
1463
- : props.ticks
1464
-
1465
- const axisMaxHeight = bottomLabelStart + BOTTOM_LABEL_PADDING
1466
-
1467
- const containsMultipleWords = inputString => /\s/.test(inputString)
1468
- const isMultiLabel = filteredTicks.some(tick => containsMultipleWords(tick.value))
1469
-
1470
- // Calculate sumOfTickWidth here, before map function
1471
- const longestTickLength = Math.max(
1472
- ...filteredTicks.map(tick => getTextWidth(tick.formattedValue, GET_TEXT_WIDTH_FONT))
1473
- )
1474
- // const marginTop = 20 // moved to top bc need for yMax calcs
1475
- const accumulator = isMultiLabel ? 180 : 100
1476
-
1477
- const textWidths = filteredTicks.map(tick => getTextWidth(tick.formattedValue, GET_TEXT_WIDTH_FONT))
1478
- const sumOfTickWidth = textWidths.reduce((a, b) => a + b, accumulator)
1479
- const spaceBetweenEachTick = (xMax - sumOfTickWidth) / (filteredTicks.length - 1)
1480
- const bufferBetweenTicks = 40
1481
- const maxLengthOfTick = width / filteredTicks.length - X_TICK_LABEL_PADDING * 2 - bufferBetweenTicks
1482
-
1483
- // Determine the position of each tick
1484
- let positions = [0] // The first tick is at position 0
1485
- for (let i = 1; i < textWidths.length; i++) {
1486
- // The position of each subsequent tick is the position of the previous tick
1487
- // plus the width of the previous tick and the space
1488
- positions[i] = positions[i - 1] + textWidths[i - 1] + spaceBetweenEachTick
1489
- }
1490
- // calculate the end of x axis box
1491
- const axisBBox = axisBottomRef?.current?.getBBox().height
1492
- config.xAxis.axisBBox = axisBBox
1493
-
1494
- // force wrap it last tick is close to the end of the axis
1495
- const lastTickWidth = textWidths[textWidths.length - 1]
1496
- const lastTickPosition = positions[positions.length - 1] + lastTickWidth
1497
- const lastTickEnd = lastTickPosition + lastTickWidth / 2
1498
- const lastTickEndThreshold = xMax - lastTickWidth
1499
-
1500
- const areTicksTouching =
1501
- textWidths.some(textWidth => textWidth > maxLengthOfTick) || // Force wrap if any tick is too long
1502
- config.xAxis.showYearsOnce || // Force wrap when showing years once so it's easier to read
1503
- lastTickEnd > lastTickEndThreshold // Force wrap it last tick is close to the end of the axis
1504
-
1505
- const dynamicMarginTop =
1506
- areTicksTouching && config.isResponsiveTicks ? longestTickLength + DEFAULT_TICK_LENGTH + 20 : 0
1507
-
1508
- config.dynamicMarginTop = dynamicMarginTop
1509
- config.xAxis.tickWidthMax = longestTickLength
1510
-
1511
- return (
1512
- <Group className='bottom-axis' width={dimensions[0]}>
1513
- {filteredTicks.map((tick, i, propsTicks) => {
1514
- // when using LogScale show major ticks values only
1515
- const showTick = String(tick.value).startsWith('1') || tick.value === 0.1 ? 'block' : 'none'
1516
- const tickLength = showTick === 'block' ? 16 : DEFAULT_TICK_LENGTH
1517
- const to = { x: tick.to.x, y: tickLength }
1518
- const limitedWidth = 100 / propsTicks.length
1519
- //reset rotations by updating config
1520
- config.yAxis.tickRotation =
1521
- config.isResponsiveTicks && config.orientation === 'horizontal' ? 0 : config.yAxis.tickRotation
1522
- config.xAxis.tickRotation =
1523
- config.isResponsiveTicks && config.orientation === 'vertical' ? 0 : config.xAxis.tickRotation
1524
- //configure rotation
1525
-
1526
- const tickRotation =
1527
- config.isResponsiveTicks && areTicksTouching
1528
- ? -Number(config.xAxis.maxTickRotation) || -90
1529
- : -Number(config.runtime.xAxis.tickRotation)
1530
-
1531
- return (
1532
- <Group key={`vx-tick-${tick.value}-${i}`} className={'vx-axis-tick'}>
1533
- {!config.xAxis.hideTicks && (
1534
- <Line
1535
- from={tick.from}
1536
- to={orientation === 'horizontal' && isLogarithmicAxis ? to : tick.to}
1537
- stroke={config.xAxis.tickColor}
1538
- strokeWidth={showTick === 'block' && isLogarithmicAxis ? 1.3 : 1}
1539
- />
1540
- )}
1541
- {!config.xAxis.hideLabel && (
1542
- <Text
1543
- innerRef={el => (xAxisLabelRefs.current[i] = el)}
1544
- dy={config.orientation === 'horizontal' && isLogarithmicAxis ? 8 : 0}
1545
- display={config.orientation === 'horizontal' && isLogarithmicAxis ? showTick : 'block'}
1546
- x={tick.to.x}
1547
- y={tick.to.y + X_TICK_LABEL_PADDING}
1548
- angle={tickRotation}
1549
- verticalAnchor={tickRotation < -50 ? 'middle' : 'start'}
1550
- textAnchor={tickRotation ? 'end' : 'middle'}
1551
- width={
1552
- areTicksTouching && !config.isResponsiveTicks && !Number(config.xAxis.tickRotation)
1553
- ? limitedWidth
1554
- : undefined
1555
- }
1556
- fill={config.xAxis.tickLabelColor}
1557
- fontSize={tickLabelFontSize}
1558
- >
1559
- {tick.formattedValue}
1560
- </Text>
1561
- )}
1562
- </Group>
1563
- )
1564
- })}
1565
- {!config.xAxis.hideAxis && <Line from={props.axisFromPoint} to={props.axisToPoint} stroke='#333' />}
1566
- <Text
1567
- innerRef={xAxisTitleRef}
1568
- className='x-axis-title-label'
1569
- x={xMax / 2}
1570
- y={isForestPlot ? 0 /* set via ref */ : axisMaxHeight}
1571
- textAnchor='middle'
1572
- verticalAnchor='start'
1573
- fontWeight='bold'
1574
- fill={config.xAxis.labelColor}
1575
- fontSize={axisLabelFontSize}
1576
- >
1577
- {!config.hideXAxisLabel ? props.label : null}
1578
- </Text>
1579
- </Group>
1580
- )
1581
- }}
1582
- </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
+ />
1583
818
  )}
1584
819
  </svg>
1585
820
  {!isDraggingAnnotation &&
@@ -1631,6 +866,27 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1631
866
  />
1632
867
  )}
1633
868
  <div className='animation-trigger' ref={triggerRef} />
869
+ {/* SEPARATED BRUSH - Independent SVG overlay */}
870
+ {config.xAxis.brushActive && config.xAxis.type !== 'categorical' && xMax > 0 && (
871
+ <div
872
+ style={{
873
+ position: 'relative',
874
+ marginTop: `${BRUSH_MARGIN}px`,
875
+ left: `${yAxisWidth}px`,
876
+ width: `${Math.max(xMax, BRUSH_MIN_WIDTH)}px`,
877
+ height: `${BRUSH_HEIGHT}px`,
878
+ pointerEvents: 'auto',
879
+ zIndex: 15,
880
+ touchAction: 'none', // Enable touch interactions for brush
881
+ WebkitTouchCallout: 'none',
882
+ WebkitUserSelect: 'none',
883
+ userSelect: 'none'
884
+ }}
885
+ className='brush-overlay'
886
+ >
887
+ <BrushSelector key={brushKeyRef.current} xMax={Math.max(xMax, BRUSH_MIN_WIDTH)} yMax={BRUSH_HEIGHT} />
888
+ </div>
889
+ )}
1634
890
  </div>
1635
891
  </ErrorBoundary>
1636
892
  )