@cdc/chart 4.25.10 → 4.26.1

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 (135) hide show
  1. package/dist/{cdcchart-1a1724a1.es.js → cdcchart-dgT_1dIT.es.js} +136 -151
  2. package/dist/cdcchart.js +44003 -43518
  3. package/examples/feature/__data__/planet-example-data.json +1 -1
  4. package/examples/feature/boxplot/valid-boxplot.csv +38 -17
  5. package/examples/feature/pie/planet-pie-example-config.json +48 -2
  6. package/examples/private/DEV-11825.json +573 -0
  7. package/examples/private/DEV-12100.json +1303 -0
  8. package/examples/private/cat-y.json +1235 -0
  9. package/examples/private/data-points.json +228 -0
  10. package/examples/private/height.json +3915 -0
  11. package/examples/private/links.json +569 -0
  12. package/examples/private/na.json +913 -0
  13. package/examples/private/quadrant.txt +30 -0
  14. package/examples/private/test-data.csv +28 -0
  15. package/examples/private/test-forecast.json +5510 -0
  16. package/examples/private/warming-stripe-test.json +2578 -0
  17. package/examples/private/warming-stripes.json +4763 -0
  18. package/examples/tech-adoption-with-links.json +560 -0
  19. package/index.html +16 -140
  20. package/package.json +6 -5
  21. package/preview.html +1616 -0
  22. package/src/CdcChart.tsx +8 -11
  23. package/src/CdcChartComponent.tsx +329 -124
  24. package/src/_stories/Chart.Combo.stories.tsx +18 -0
  25. package/src/_stories/Chart.Forecast.stories.tsx +36 -0
  26. package/src/_stories/Chart.HTMLInDataTable.stories.tsx +520 -0
  27. package/src/_stories/Chart.Patterns.stories.tsx +2 -1
  28. package/src/_stories/Chart.PreserveDecimals.stories.tsx +220 -0
  29. package/src/_stories/Chart.Regions.Categorical.stories.tsx +148 -0
  30. package/src/_stories/Chart.Regions.DateScale.stories.tsx +197 -0
  31. package/src/_stories/Chart.Regions.DateTimeScale.stories.tsx +297 -0
  32. package/src/_stories/Chart.SmallMultiples.stories.tsx +47 -0
  33. package/src/_stories/Chart.stories.tsx +8 -0
  34. package/src/_stories/ChartAnnotation.stories.tsx +6 -3
  35. package/src/_stories/ChartBar.Editor.stories.tsx +3585 -0
  36. package/src/_stories/ChartBrush.Editor.stories.tsx +295 -0
  37. package/src/_stories/ChartBrush.stories.tsx +50 -0
  38. package/src/_stories/ChartEditor.Editor.stories.tsx +656 -0
  39. package/src/_stories/ChartEditor.stories.tsx +1 -2
  40. package/src/_stories/TechAdoptionWithLinks.stories.tsx +27 -0
  41. package/src/_stories/_mock/brush_enabled.json +326 -0
  42. package/src/_stories/_mock/brush_mock.json +2 -69
  43. package/src/_stories/_mock/combo.json +451 -0
  44. package/src/_stories/_mock/editor-test-configs.json +376 -0
  45. package/src/_stories/_mock/editor-test-datasets.json +477 -0
  46. package/src/_stories/_mock/editor-tests/bar-chart-editor-test.json +255 -0
  47. package/src/_stories/_mock/editor-tests/bar-chart-general-test.json +267 -0
  48. package/src/_stories/_mock/editor-tests/bar-chart-test.json +237 -0
  49. package/src/_stories/_mock/forecast_combo_with_gaps.json +913 -0
  50. package/src/_stories/_mock/horizontal-bars-dynamic-y-axis.json +413 -0
  51. package/src/_stories/_mock/pie_config.json +257 -62
  52. package/src/_stories/_mock/small_multiples/small_multiples_bars.json +1944 -0
  53. package/src/_stories/_mock/small_multiples/small_multiples_big_data_bars.json +1114 -0
  54. package/src/_stories/_mock/small_multiples/small_multiples_lines.json +2646 -0
  55. package/src/_stories/_mock/small_multiples/small_multiples_lines_colors.json +1305 -0
  56. package/src/_stories/_mock/small_multiples/small_multiples_stacked_bars.json +1936 -0
  57. package/src/components/Annotations/components/findNearestDatum.ts +6 -41
  58. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +10 -7
  59. package/src/components/AreaChart/index.tsx +1 -2
  60. package/src/components/Axis/Categorical.Axis.tsx +6 -7
  61. package/src/components/BarChart/components/BarChart.Horizontal.tsx +181 -27
  62. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +3 -1
  63. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +1 -0
  64. package/src/components/BarChart/components/BarChart.Vertical.tsx +8 -9
  65. package/src/components/BarChart/components/context.tsx +1 -0
  66. package/src/components/BarChart/helpers/useBarChart.ts +14 -2
  67. package/src/components/BoxPlot/helpers/index.ts +3 -3
  68. package/src/components/Brush/BrushSelector.tsx +1258 -0
  69. package/src/components/Brush/MiniChartPreview.tsx +283 -0
  70. package/src/components/DeviationBar.jsx +9 -7
  71. package/src/components/EditorPanel/EditorPanel.tsx +2720 -2586
  72. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +96 -111
  73. package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +56 -34
  74. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +76 -31
  75. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +104 -55
  76. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +54 -49
  77. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +427 -0
  78. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +96 -48
  79. package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
  80. package/src/components/EditorPanel/editor-panel.scss +0 -20
  81. package/src/components/EditorPanel/useEditorPermissions.ts +36 -31
  82. package/src/components/Forecasting/Forecasting.tsx +139 -21
  83. package/src/components/Legend/Legend.Component.tsx +16 -9
  84. package/src/components/Legend/Legend.tsx +3 -2
  85. package/src/components/Legend/helpers/createFormatLabels.tsx +325 -176
  86. package/src/components/Legend/helpers/getLegendClasses.ts +0 -1
  87. package/src/components/Legend/helpers/index.ts +10 -6
  88. package/src/components/LineChart/LineChartProps.ts +0 -3
  89. package/src/components/LineChart/helpers.ts +1 -1
  90. package/src/components/LineChart/index.tsx +36 -13
  91. package/src/components/LinearChart.tsx +559 -499
  92. package/src/components/PairedBarChart.jsx +20 -3
  93. package/src/components/Regions/components/Regions.tsx +366 -144
  94. package/src/components/Sankey/types/index.ts +1 -1
  95. package/src/components/ScatterPlot/ScatterPlot.jsx +2 -2
  96. package/src/components/SmallMultiples/SmallMultipleTile.tsx +202 -0
  97. package/src/components/SmallMultiples/SmallMultiples.css +32 -0
  98. package/src/components/SmallMultiples/SmallMultiples.tsx +271 -0
  99. package/src/components/SmallMultiples/index.ts +2 -0
  100. package/src/components/WarmingStripes/WarmingStripes.tsx +160 -0
  101. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +35 -0
  102. package/src/components/WarmingStripes/WarmingStripesGradientLegend.tsx +104 -0
  103. package/src/components/WarmingStripes/index.tsx +3 -0
  104. package/src/data/initial-state.js +16 -2
  105. package/src/helpers/buildForecastPaletteOptions.ts +0 -38
  106. package/src/helpers/calculateHorizontalBarCategoryLabelWidth.ts +57 -0
  107. package/src/helpers/getColorScale.ts +10 -0
  108. package/src/{hooks/useMinMax.ts → helpers/getMinMax.ts} +26 -14
  109. package/src/helpers/getYAxisAutoPadding.ts +53 -0
  110. package/src/helpers/sizeHelpers.ts +0 -20
  111. package/src/helpers/smallMultiplesHelpers.ts +529 -0
  112. package/src/hooks/useChartHoverAnalytics.tsx +10 -9
  113. package/src/hooks/useProgrammaticTooltip.ts +96 -0
  114. package/src/hooks/useScales.ts +98 -34
  115. package/src/hooks/useSmallMultipleSynchronization.ts +59 -0
  116. package/src/hooks/useTooltip.tsx +91 -25
  117. package/src/scss/DataTable.scss +0 -4
  118. package/src/scss/main.scss +18 -83
  119. package/src/store/chart.actions.ts +2 -0
  120. package/src/store/chart.reducer.ts +4 -0
  121. package/src/test/CdcChart.test.jsx +1 -1
  122. package/src/types/ChartConfig.ts +27 -6
  123. package/src/types/ChartContext.ts +3 -0
  124. package/src/types/Label.ts +1 -0
  125. package/src/utils/analyticsTracking.ts +19 -0
  126. package/LICENSE +0 -201
  127. package/src/_stories/_mock/pie_data.json +0 -218
  128. package/src/components/AreaChart/components/AreaChart.jsx +0 -109
  129. package/src/components/Brush/BrushChart.tsx +0 -128
  130. package/src/components/Brush/BrushController.tsx +0 -71
  131. package/src/components/Brush/types.tsx +0 -8
  132. package/src/components/BrushChart.tsx +0 -223
  133. package/src/helpers/sort.ts +0 -7
  134. package/src/hooks/useActiveElement.js +0 -19
  135. package/src/hooks/useChartClasses.js +0 -41
@@ -1,4 +1,13 @@
1
- import React, { forwardRef, useContext, useEffect, useMemo, useRef, useState } from 'react'
1
+ import React, {
2
+ forwardRef,
3
+ useContext,
4
+ useEffect,
5
+ useImperativeHandle,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ useCallback
10
+ } from 'react'
2
11
 
3
12
  // Libraries
4
13
  import { AxisLeft, AxisBottom, AxisRight, AxisTop } from '@visx/axis'
@@ -27,19 +36,22 @@ import PairedBarChart from './PairedBarChart'
27
36
  import useIntersectionObserver from '../hooks/useIntersectionObserver'
28
37
  import Regions from './Regions'
29
38
  import CategoricalYAxis from './Axis/Categorical.Axis'
30
- import BrushChart from './Brush/BrushController'
39
+ import BrushSelector from './Brush/BrushSelector'
40
+ import WarmingStripes from './WarmingStripes'
31
41
 
32
42
  // Helpers
33
43
  import { isLegendWrapViewport, isMobileFontViewport } from '@cdc/core/helpers/viewports'
34
44
  import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
35
- import { calcInitialHeight, handleAutoPaddingRight } from '../helpers/sizeHelpers'
45
+ import { calcInitialHeight } from '../helpers/sizeHelpers'
36
46
  import { filterAndShiftLinearDateTicks } from '../helpers/filterAndShiftLinearDateTicks'
47
+ import { calculateHorizontalBarCategoryLabelWidth } from '../helpers/calculateHorizontalBarCategoryLabelWidth'
37
48
 
38
49
  // Hooks
39
- import useMinMax from '../hooks/useMinMax'
40
50
  import useReduceData from '../hooks/useReduceData'
41
51
  import useRightAxis from '../hooks/useRightAxis'
42
52
  import useScales, { getTickValues } from '../hooks/useScales'
53
+ import { useProgrammaticTooltip } from '../hooks/useProgrammaticTooltip'
54
+ import { useSmallMultipleSynchronization } from '../hooks/useSmallMultipleSynchronization'
43
55
 
44
56
  import getTopAxis from '../helpers/getTopAxis'
45
57
  import { useTooltip as useCoveTooltip } from '../hooks/useTooltip'
@@ -49,6 +61,7 @@ import Annotation from './Annotations'
49
61
  import { BlurStrokeText } from '@cdc/core/components/BlurStrokeText'
50
62
  import { countNumOfTicks } from '../helpers/countNumOfTicks'
51
63
  import HoverLine from './HoverLine/HoverLine'
64
+ import { SmallMultiples } from './SmallMultiples'
52
65
 
53
66
  type LinearChartProps = {
54
67
  parentWidth: number
@@ -86,6 +99,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
86
99
  config,
87
100
  convertLineToBarGraph,
88
101
  currentViewport,
102
+ vizViewport,
89
103
  dimensions,
90
104
  formatDate,
91
105
  formatNumber,
@@ -94,13 +108,13 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
94
108
  handleDragStateChange,
95
109
  interactionLabel,
96
110
  isDraggingAnnotation,
111
+ isEditor,
97
112
  legendRef,
98
113
  parseDate,
99
114
  parentRef,
100
115
  tableData,
101
116
  transformedData: data,
102
- seriesHighlight,
103
-
117
+ seriesHighlight
104
118
  } = useContext(ConfigContext)
105
119
 
106
120
  // CONFIG
@@ -119,17 +133,22 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
119
133
  dataFormat,
120
134
  debugSvg
121
135
  } = config
136
+
122
137
  const { labelsAboveGridlines, hideAxis, inlineLabel } = config.yAxis
123
138
 
124
139
  // HOOKS % STATES
125
- const { minValue, maxValue, existPositiveValue, isAllLine } = useReduceData(config, data)
126
- const { visSupportsReactTooltip } = useEditorPermissions()
140
+ // When brush is active, use tableData (full dataset) for min/max calculation
141
+ // so the y-axis shows the full range, but still use filtered data for rendering
142
+ const dataForMinMax = config.xAxis.brushActive && tableData && tableData.length > 0 ? tableData : data
143
+ const { minValue, maxValue, existPositiveValue, isAllLine } = useReduceData(config, dataForMinMax)
144
+
145
+ const { visSupportsSmallMultiples } = useEditorPermissions()
127
146
  const { hasTopAxis } = getTopAxis(config)
128
147
  const [animatedChart, setAnimatedChart] = useState(false)
129
148
  const [showHoverLine, setShowHoverLine] = useState(false)
130
149
  const [point, setPoint] = useState({ x: 0, y: 0 })
131
150
  const [suffixWidth, setSuffixWidth] = useState(0)
132
- const [yAxisAutoPadding, setYAxisAutoPadding] = useState(0)
151
+ const [calculatedSvgHeight, setCalculatedSvgHeight] = useState<number | null>(null)
133
152
 
134
153
  // REFS
135
154
  const axisBottomRef = useRef(null)
@@ -139,8 +158,6 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
139
158
  const triggerRef = useRef()
140
159
  const xAxisLabelRefs = useRef([])
141
160
  const xAxisTitleRef = useRef(null)
142
- const lastMaxValue = useRef(maxValue)
143
- const gridLineRefs = useRef([])
144
161
  const tooltipRef = useRef(null)
145
162
 
146
163
  const dataRef = useIntersectionObserver(triggerRef, {
@@ -154,11 +171,11 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
154
171
  const isForestPlot = visualizationType === 'Forest Plot'
155
172
  const isDateTime = config.xAxis.type === 'date-time'
156
173
  const inlineLabelHasNoSpace = !inlineLabel?.includes(' ')
157
- const labelsOverflow = inlineLabel && !inlineLabelHasNoSpace
174
+ const needsYAxisAutoPadding = inlineLabel && !inlineLabelHasNoSpace
158
175
  const padding = orientation === 'horizontal' ? Number(config.xAxis.size) : Number(config.yAxis.size)
159
176
  const yLabelOffset = isNaN(parseInt(`${runtime.yAxis.labelOffset}`)) ? 0 : parseInt(`${runtime.yAxis.labelOffset}`)
160
- const tickLabelFontSize = isMobileFontViewport(currentViewport) ? TICK_LABEL_FONT_SIZE_SMALL : TICK_LABEL_FONT_SIZE
161
- const axisLabelFontSize = isMobileFontViewport(currentViewport) ? AXIS_LABEL_FONT_SIZE_SMALL : AXIS_LABEL_FONT_SIZE
177
+ const tickLabelFontSize = isMobileFontViewport(vizViewport) ? TICK_LABEL_FONT_SIZE_SMALL : TICK_LABEL_FONT_SIZE
178
+ const axisLabelFontSize = isMobileFontViewport(vizViewport) ? AXIS_LABEL_FONT_SIZE_SMALL : AXIS_LABEL_FONT_SIZE
162
179
  const GET_TEXT_WIDTH_FONT = `normal ${tickLabelFontSize}px Nunito, sans-serif`
163
180
 
164
181
  // zero if not forest plot
@@ -166,45 +183,19 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
166
183
 
167
184
  // height before bottom axis
168
185
  const initialHeight = useMemo(
169
- () => calcInitialHeight(config, currentViewport),
170
- [config, currentViewport, parentHeight, config.heights?.vertical, config.heights?.horizontal]
186
+ () => (visualizationType === 'Warming Stripes' ? 78 : calcInitialHeight(config, currentViewport)),
187
+ [config, currentViewport, parentHeight, config.heights?.vertical, config.heights?.horizontal, visualizationType]
171
188
  )
172
189
  const forestHeight = useMemo(() => initialHeight + forestRowsHeight, [initialHeight, forestRowsHeight])
173
190
 
174
- // width
175
- const width = useMemo(() => {
176
- const initialWidth = dimensions[0]
177
- const legendHidden = legend?.hide
178
- const legendOnTopOrBottom = ['bottom', 'top'].includes(config.legend?.position)
179
- const legendWrapped = isLegendWrapViewport(currentViewport)
180
-
181
- const legendShowingLeftOrRight = !isForestPlot && !legendHidden && !legendOnTopOrBottom && !legendWrapped
182
-
183
- if (!legendShowingLeftOrRight) return initialWidth
184
-
185
- if (legendRef.current) {
186
- const legendStyle = getComputedStyle(legendRef.current)
187
- return (
188
- initialWidth -
189
- legendRef.current.getBoundingClientRect().width -
190
- parseInt(legendStyle.marginLeft) -
191
- parseInt(legendStyle.marginRight)
192
- )
193
- }
194
-
195
- return initialWidth * 0.73
196
- }, [dimensions[0], config.legend, currentViewport, legendRef.current])
197
-
198
191
  // Used to calculate the y position of the x-axis title
199
192
  const bottomLabelStart = useMemo(() => {
200
193
  xAxisLabelRefs.current = xAxisLabelRefs.current?.filter(label => label)
201
194
  if (!xAxisLabelRefs.current.length) return
202
195
  const tallestLabel = Math.max(...xAxisLabelRefs.current.map(label => label.getBBox().height))
203
196
  return tallestLabel + X_TICK_LABEL_PADDING + DEFAULT_TICK_LENGTH
204
- }, [dimensions[0], config.xAxis, xAxisLabelRefs.current, config.xAxis.tickRotation])
197
+ }, [parentWidth, config.xAxis, xAxisLabelRefs.current, config.xAxis.tickRotation])
205
198
 
206
- // xMax and yMax
207
- const xMax = width - runtime.yAxis.size - (visualizationType === 'Combo' ? config.yAxis.rightAxisSize : 0)
208
199
  const yMax = initialHeight + forestRowsHeight
209
200
 
210
201
  const isNoDataAvailable = config.filters?.length > 0 && data.length === 0
@@ -215,41 +206,70 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
215
206
  : d[config.runtime.originalXAxis.dataKey]
216
207
  const getYAxisData = (d, seriesKey) => d[seriesKey]
217
208
  const xAxisDataMapped = data.map(d => getXAxisData(d))
218
- const section = config.orientation === 'horizontal' || config.visualizationType === 'Forest Plot' ? 'yAxis' : 'xAxis'
219
- const properties = {
209
+
210
+ // Get unique x-axis values (for cases where multiple series share the same x-axis value)
211
+ // This is important for brush filtering where we want to count unique dates, not total data points
212
+ const uniqueXAxisDataMapped = useMemo(() => {
213
+ const unique = new Set()
214
+ const result: any[] = []
215
+ for (const value of xAxisDataMapped) {
216
+ const key = value instanceof Date ? value.getTime() : typeof value === 'number' ? value : String(value)
217
+ if (!unique.has(key)) {
218
+ unique.add(key)
219
+ result.push(value)
220
+ }
221
+ }
222
+ return result
223
+ }, [xAxisDataMapped])
224
+ const { yScaleRight, hasRightAxis } = useRightAxis({ config, yMax, data })
225
+
226
+ const xMax = parentWidth - Number(runtime.yAxis.size) - (hasRightAxis ? config.yAxis.rightAxisSize : 0)
227
+
228
+ const {
229
+ xScale,
230
+ yScale,
231
+ seriesScale,
232
+ g1xScale,
233
+ g2xScale,
234
+ xScaleNoPadding,
235
+ xScaleAnnotation,
236
+ min,
237
+ max,
238
+ leftMax,
239
+ rightMax
240
+ } = useScales({
220
241
  data,
221
242
  tableData,
222
- config: {
223
- ...config,
224
- yAxis: {
225
- ...config.yAxis,
226
- scalePadding: labelsOverflow ? yAxisAutoPadding : config.yAxis.scalePadding,
227
- enablePadding: labelsOverflow || config.yAxis.enablePadding
228
- }
229
- },
243
+ config,
230
244
  minValue,
231
245
  maxValue,
232
246
  isAllLine,
233
247
  existPositiveValue,
234
248
  xAxisDataMapped,
249
+ yMax,
235
250
  xMax,
236
- yMax
237
- }
238
- const { min, max, leftMax, rightMax } = useMinMax(properties)
239
- const { yScaleRight, hasRightAxis } = useRightAxis({ config, yMax, data })
240
- const { xScale, yScale, seriesScale, g1xScale, g2xScale, xScaleNoPadding, xScaleAnnotation } = useScales({
241
- ...properties,
242
- min,
243
- max,
244
- leftMax,
245
- rightMax,
246
- dimensions,
247
- xMax:
248
- parentWidth -
249
- Number(config.orientation === 'horizontal' ? config.xAxis.size : config.yAxis.size) -
250
- (hasRightAxis ? config.yAxis.rightAxisSize : 0)
251
+ needsYAxisAutoPadding,
252
+ currentViewport
251
253
  })
252
254
 
255
+ // Calculate category label space for horizontal bar charts
256
+ const categoryLabelSpace = useMemo(() => {
257
+ return calculateHorizontalBarCategoryLabelWidth({
258
+ yScale,
259
+ chartWidth: parentWidth,
260
+ formatDate,
261
+ parseDate,
262
+ tickLabelFont: GET_TEXT_WIDTH_FONT,
263
+ xAxisType: config.runtime.xAxis?.type,
264
+ labelPlacement: config.yAxis.labelPlacement
265
+ })
266
+ }, [isHorizontal, config.visualizationType, config.yAxis.labelPlacement, yScale, parentWidth])
267
+
268
+ const horizontalYAxisLabelSpace = runtime.yAxis.label && !config.hideYAxisLabel ? 30 : 0
269
+ if (isHorizontal && config.visualizationType === 'Bar') {
270
+ runtime.yAxis.size = categoryLabelSpace + horizontalYAxisLabelSpace
271
+ }
272
+
253
273
  const [yTickCount, xTickCount] = ['yAxis', 'xAxis'].map(axis =>
254
274
  countNumOfTicks({ axis, max, runtime, currentViewport, isHorizontal, data, config, min })
255
275
  )
@@ -266,6 +286,8 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
266
286
  handleTooltipClick,
267
287
  handleTooltipMouseOff,
268
288
  TooltipListItem,
289
+ getXValueFromCoordinate,
290
+ getCoordinateFromXValue,
269
291
  } = useCoveTooltip({
270
292
  xScale,
271
293
  yScale,
@@ -350,6 +372,8 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
350
372
  return manualStep
351
373
  }
352
374
 
375
+ const smallMultiplesSync = useSmallMultipleSynchronization(xMax, yMax, getXValueFromCoordinate)
376
+
353
377
  const onMouseMove = event => {
354
378
  const svgRect = event.currentTarget.getBoundingClientRect()
355
379
  const x = event.clientX - svgRect.left
@@ -359,24 +383,24 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
359
383
  x,
360
384
  y
361
385
  })
362
- }
363
386
 
364
- // EFFECTS
365
- // Adjust padding on the right side of the chart to accommodate for overflow
366
- useEffect(() => {
367
- if (!parentRef.current || !parentWidth || !gridLineRefs.current.length) return
368
-
369
- const [updatePadding, paddingToAdd] = handleAutoPaddingRight(parentRef, xAxisLabelRefs, parentWidth)
387
+ smallMultiplesSync.onMouseMove?.(event)
388
+ }
370
389
 
371
- if (!updatePadding) return
390
+ const onMouseLeave = () => {
391
+ smallMultiplesSync.onMouseLeave?.()
392
+ }
372
393
 
373
- parentRef.current.style.paddingRight = `${paddingToAdd}px`
374
- // subtract padding from grid line's x1 value
375
- gridLineRefs.current.forEach(gridLine => {
376
- if (!gridLine) return
377
- gridLine.setAttribute('x1', xMax - paddingToAdd)
378
- })
379
- }, [parentWidth, parentHeight, data])
394
+ // Use custom hook to provide programmatic tooltip control for small multiples
395
+ const internalSvgRef = useProgrammaticTooltip({
396
+ svgRef,
397
+ getCoordinateFromXValue,
398
+ config,
399
+ setPoint,
400
+ setShowHoverLine,
401
+ handleTooltipMouseOver,
402
+ hideTooltip
403
+ })
380
404
 
381
405
  // Make sure the chart is visible if in the editor
382
406
  /* eslint-disable react-hooks/exhaustive-deps */
@@ -429,16 +453,24 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
429
453
  const topLabelOnGridline = topYLabelRef.current && yAxis.labelsAboveGridlines
430
454
 
431
455
  // Heights to add
432
-
433
- const brushHeight = 25
434
- const brushHeightWithMargin = config.xAxis.brushActive ? brushHeight + brushHeight : 0
435
456
  const forestRowsHeight = isForestPlot ? config.data.length * forestPlot.rowHeight : 0
436
457
  const topLabelOnGridlineHeight = topLabelOnGridline ? topYLabelRef.current.getBBox().height : 0
437
- const additionalHeight = axisBottomHeight + brushHeightWithMargin + forestRowsHeight + topLabelOnGridlineHeight
438
- const newHeight = initialHeight + additionalHeight
458
+
459
+ // SVG height (without brush)
460
+ const svgAdditionalHeight = axisBottomHeight + forestRowsHeight + topLabelOnGridlineHeight
461
+ const svgHeight = initialHeight + svgAdditionalHeight
462
+
463
+ // Parent container height (includes brush if active)
464
+ const brushHeight = 70
465
+ const brushMargin = 10
466
+ const brushHeightWithMargin = config.xAxis.brushActive ? brushHeight + brushMargin : 0
467
+ const parentHeight = svgHeight + brushHeightWithMargin
468
+
439
469
  if (!parentRef.current) return
470
+ parentRef.current.style.height = `${parentHeight}px`
440
471
 
441
- parentRef.current.style.height = `${newHeight}px`
472
+ // Set the calculated SVG height via state to ensure it's used on render
473
+ setCalculatedSvgHeight(svgHeight)
442
474
 
443
475
  /* Adding text above the top gridline overflows the bounds of the svg.
444
476
  To accommodate for this we need to...
@@ -449,10 +481,10 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
449
481
  if (!topLabelOnGridlineHeight) return
450
482
 
451
483
  // Adjust the viewBox for the svg
452
- const svg = svgRef.current
484
+ const svg = internalSvgRef.current
453
485
  if (!svg) return
454
486
  const parentWidthFromRef = parentRef.current.getBoundingClientRect().width
455
- svg.setAttribute('viewBox', `0 ${-topLabelOnGridlineHeight} ${parentWidthFromRef} ${newHeight}`)
487
+ svg.setAttribute('viewBox', `0 ${-topLabelOnGridlineHeight} ${parentWidthFromRef} ${svgHeight}`)
456
488
 
457
489
  // translate legend match viewBox-adjusted height
458
490
  if (!legendRef.current) return
@@ -469,45 +501,6 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
469
501
  initialHeight
470
502
  ])
471
503
 
472
- useEffect(() => {
473
- if (lastMaxValue.current === maxValue) return
474
- lastMaxValue.current = maxValue
475
-
476
- if (!yAxisAutoPadding) return
477
- setYAxisAutoPadding(0)
478
- }, [maxValue])
479
-
480
- useEffect(() => {
481
- if (!yScale?.ticks) return
482
- const ticks = yScale.ticks(handleNumTicks)
483
- if (orientation === 'horizontal' || !labelsOverflow || config.yAxis?.max || ticks.length === 0) {
484
- setYAxisAutoPadding(0)
485
- return
486
- }
487
-
488
- // minimum percentage of the max value that the distance should be from the top grid line
489
- const MINIMUM_DISTANCE_PERCENTAGE = 0.025
490
-
491
- const topGridLine = Math.max(...ticks)
492
- const needsPaddingThreshold = topGridLine - maxValue * MINIMUM_DISTANCE_PERCENTAGE
493
- const maxValueIsGreaterThanThreshold = maxValue > needsPaddingThreshold
494
-
495
- if (!maxValueIsGreaterThanThreshold) return
496
-
497
- const tickGap = ticks.length === 1 ? ticks[0] : ticks[1] - ticks[0]
498
- const nextTick = Math.max(...yScale.ticks(handleNumTicks)) + tickGap
499
- const divideBy = minValue < 0 ? maxValue / 2 : maxValue
500
- const calculatedPadding = (nextTick - maxValue) / divideBy
501
-
502
- // if auto padding is too close to next tick, add one more ticks worth of padding
503
- const newPadding =
504
- calculatedPadding > MINIMUM_DISTANCE_PERCENTAGE ? calculatedPadding : calculatedPadding + tickGap / divideBy
505
-
506
- /* sometimes even though the padding is getting to the next tick exactly,
507
- d3 still doesn't show the tick. we add 0.1 to ensure to tip it over the edge */
508
- setYAxisAutoPadding(newPadding * 100 + 0.1)
509
- }, [maxValue, labelsOverflow, yScale, handleNumTicks])
510
-
511
504
  useEffect(() => {
512
505
  if (!tooltipOpen) return
513
506
  if (!tooltipRef.current) return
@@ -525,6 +518,19 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
525
518
  tooltipRef.current.node.style.maxWidth = `${maxWidth}px`
526
519
  }, [tooltipOpen, tooltipData])
527
520
 
521
+ // Check if small multiples are enabled and supported - if so, render SmallMultiples instead
522
+ if (config.smallMultiples?.mode && visSupportsSmallMultiples()) {
523
+ return (
524
+ <SmallMultiples
525
+ config={config}
526
+ data={data}
527
+ svgRef={svgRef}
528
+ parentWidth={parentWidth}
529
+ parentHeight={parentHeight}
530
+ />
531
+ )
532
+ }
533
+
528
534
  // Render Functions
529
535
  const generatePairedBarAxis = () => {
530
536
  const axisMaxHeight = bottomLabelStart + BOTTOM_LABEL_PADDING
@@ -659,7 +665,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
659
665
  verticalAnchor='start'
660
666
  fontSize={axisLabelFontSize}
661
667
  >
662
- {runtime.xAxis.label}
668
+ {!config.hideXAxisLabel ? runtime.xAxis.label : null}
663
669
  </Text>
664
670
  </Group>
665
671
  </>
@@ -669,7 +675,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
669
675
  </>
670
676
  )
671
677
  }
672
- return isNaN(width) ? (
678
+ return isNaN(parentWidth) ? (
673
679
  <React.Fragment></React.Fragment>
674
680
  ) : (
675
681
  <ErrorBoundary component='LinearChart'>
@@ -679,10 +685,10 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
679
685
  className='tooltip-boundary'
680
686
  >
681
687
  <svg
682
- ref={svgRef}
688
+ ref={internalSvgRef}
683
689
  onMouseMove={onMouseMove}
684
690
  width={parentWidth + config.yAxis.rightAxisSize}
685
- height={isNoDataAvailable ? 1 : parentHeight}
691
+ height={isNoDataAvailable ? 1 : calculatedSvgHeight ?? parentHeight}
686
692
  className={`linear ${config.animate ? 'animated' : ''} ${animatedChart && config.animate ? 'animate' : ''} ${
687
693
  debugSvg && 'debug'
688
694
  } ${isDraggingAnnotation && 'dragging-annotation'}`}
@@ -692,6 +698,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
692
698
  onMouseLeave={() => {
693
699
  setShowHoverLine(false)
694
700
  handleChartMouseLeave()
701
+ onMouseLeave()
695
702
  }}
696
703
  onMouseEnter={() => {
697
704
  setShowHoverLine(true)
@@ -701,61 +708,79 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
701
708
  {!isDraggingAnnotation && <Bar width={parentWidth} height={initialHeight} fill={'transparent'}></Bar>}{' '}
702
709
  {/* GRID LINES */}
703
710
  {/* Actual AxisLeft is drawn after visualization */}
704
- {!['Spark Line', 'Forest Plot'].includes(visualizationType) && config.yAxis.type !== 'categorical' && (
705
- <AxisLeft
706
- scale={yScale}
707
- left={Number(runtime.yAxis.size) - config.yAxis.axisPadding}
708
- numTicks={handleNumTicks}
709
- >
710
- {props => {
711
- const axisCenter =
712
- config.orientation === 'horizontal'
713
- ? (props.axisToPoint.y - props.axisFromPoint.y) / 2
714
- : (props.axisFromPoint.y - props.axisToPoint.y) / 2
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
+ )}
761
+ {/* Horizontal chart grid lines */}
762
+ {runtime.xAxis.gridLines && orientation === 'horizontal' && (
763
+ <Group left={Number(runtime.yAxis.size)}>
764
+ {xScale.ticks(xTickCount).map((tickValue, i) => {
765
+ const tickPosition = xScale(tickValue)
715
766
  return (
716
- <Group className='left-axis'>
717
- {props.ticks.map((tick, i) => {
718
- const showTicks = String(tick.value).startsWith('1') || tick.value === 0.1 ? 'block' : 'none'
719
- const hideFirstGridLine = tick.index === 0 && tick.value === 0 && config.xAxis.hideAxis
720
-
721
- return (
722
- <Group key={`vx-tick-${tick.value}-${i}`} className={'vx-axis-tick'}>
723
- {runtime.yAxis.gridLines && !hideFirstGridLine ? (
724
- <Line
725
- innerRef={el => (gridLineRefs.current[i] = el)}
726
- key={`${tick.value}--hide-hideGridLines`}
727
- display={(isLogarithmicAxis && showTicks).toString()}
728
- from={{ x: tick.from.x + xMax, y: tick.from.y }}
729
- to={tick.from}
730
- stroke='#d6d6d6'
731
- />
732
- ) : (
733
- ''
734
- )}
735
- </Group>
736
- )
737
- })}
738
- <Text
739
- className='y-label'
740
- textAnchor='middle'
741
- verticalAnchor='start'
742
- transform={`translate(${-1 * runtime.yAxis.size + yLabelOffset}, ${axisCenter}) rotate(-90)`}
743
- fontWeight='bold'
744
- fill={config.yAxis.labelColor}
745
- fontSize={axisLabelFontSize}
746
- >
747
- {props.label}
748
- </Text>
749
- </Group>
767
+ <Line
768
+ key={`horizontal-gridline-${tickValue}-${i}`}
769
+ from={{ x: tickPosition, y: 0 }}
770
+ to={{ x: tickPosition, y: yMax }}
771
+ stroke='#d6d6d6'
772
+ />
750
773
  )
751
- }}
752
- </AxisLeft>
774
+ })}
775
+ </Group>
753
776
  )}
754
777
  {visualizationType === 'Paired Bar' && generatePairedBarAxis()}
755
778
  {visualizationType === 'Deviation Bar' && config.runtime.series?.length === 1 && (
756
779
  <DeviationBar animatedChart={animatedChart} xScale={xScale} yScale={yScale} width={xMax} height={yMax} />
757
780
  )}
758
- {visualizationType === 'Paired Bar' && <PairedBarChart originalWidth={width} width={xMax} height={yMax} />}
781
+ {visualizationType === 'Paired Bar' && (
782
+ <PairedBarChart originalWidth={parentWidth} width={xMax} height={yMax} />
783
+ )}
759
784
  {visualizationType === 'Scatter Plot' && (
760
785
  <ScatterPlot
761
786
  xScale={xScale}
@@ -771,6 +796,9 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
771
796
  showTooltip={showTooltip}
772
797
  />
773
798
  )}
799
+ {visualizationType === 'Warming Stripes' && (
800
+ <WarmingStripes xScale={xScale} yScale={yScale} xMax={xMax} yMax={yMax} />
801
+ )}
774
802
  {visualizationType === 'Box Plot' && config.orientation === 'vertical' && (
775
803
  <BoxPlotVertical
776
804
  seriesScale={seriesScale}
@@ -846,9 +874,16 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
846
874
  )}
847
875
  {/* Line chart */}
848
876
  {/* TODO: Make this just line or combo? */}
849
- {!['Paired Bar', 'Box Plot', 'Area Chart', 'Scatter Plot', 'Deviation Bar', 'Forecasting', 'Bar'].includes(
850
- visualizationType
851
- ) &&
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) &&
852
887
  !convertLineToBarGraph && (
853
888
  <>
854
889
  <LineChart
@@ -884,7 +919,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
884
919
  xScale={xScale}
885
920
  yScale={yScale}
886
921
  seriesScale={seriesScale}
887
- width={width}
922
+ width={parentWidth}
888
923
  height={forestHeight}
889
924
  getXAxisData={getXAxisData}
890
925
  getYAxisData={getYAxisData}
@@ -900,29 +935,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
900
935
  forestPlotRightLabelRef={forestPlotRightLabelRef}
901
936
  />
902
937
  )}
903
- {/*Brush chart */}
904
- {config.xAxis.brushActive && config.xAxis.type !== 'categorical' && <BrushChart xMax={xMax} yMax={yMax} />}
905
- {/* Line chart */}
906
- {/* TODO: Make this just line or combo? */}
907
- {!['Paired Bar', 'Box Plot', 'Area Chart', 'Scatter Plot', 'Deviation Bar', 'Forecasting', 'Bar'].includes(
908
- visualizationType
909
- ) &&
910
- !convertLineToBarGraph && (
911
- <>
912
- <LineChart
913
- xScale={xScale}
914
- yScale={yScale}
915
- getXAxisData={getXAxisData}
916
- getYAxisData={getYAxisData}
917
- xMax={xMax}
918
- yMax={yMax}
919
- seriesStyle={config.runtime.series}
920
- tooltipData={tooltipData}
921
- handleTooltipMouseOver={handleTooltipMouseOver}
922
- handleTooltipMouseOff={handleTooltipMouseOff}
923
- />
924
- </>
925
- )}
938
+ {/* Brush moved to separate overlay - no longer in main SVG */}
926
939
  {/* y anchors */}
927
940
  {config.yAxis.anchors &&
928
941
  config.yAxis.anchors.map((anchor, index) => {
@@ -943,13 +956,13 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
943
956
  return (
944
957
  // prettier-ignore
945
958
  <Line
946
- key={`yAxis-${anchor.value}--${index}`}
947
- strokeDasharray={handleLineType(anchor.lineStyle)}
948
- stroke={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
949
- className='anchor-y'
950
- from={{ x: 0 + padding, y: position - middleOffset }}
951
- to={{ x: width - config.yAxis.rightAxisSize, y: position - middleOffset }}
952
- />
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
+ />
953
966
  )
954
967
  })}
955
968
  {/* x anchors */}
@@ -979,14 +992,14 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
979
992
  return (
980
993
  // prettier-ignore
981
994
  <Line
982
- key={`xAxis-${anchor.value}--${index}`}
983
- strokeDasharray={handleLineType(anchor.lineStyle)}
984
- stroke={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
985
- fill={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
986
- className='anchor-x'
987
- from={{ x: Number(anchorPosition) + Number(padding), y: 0 }}
988
- to={{ x: Number(anchorPosition) + Number(padding), y: yMax }}
989
- />
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
+ />
990
1003
  )
991
1004
  })}
992
1005
  {/* we are handling regions in bar charts differently, so that we can calculate the bar group into the region space. */}
@@ -1001,12 +1014,12 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1001
1014
  hideTooltip={hideTooltip}
1002
1015
  tooltipData={tooltipData}
1003
1016
  yMax={yMax}
1004
- width={width}
1017
+ xMax={xMax}
1005
1018
  />
1006
1019
  )}
1007
1020
  {isNoDataAvailable && (
1008
1021
  <Text
1009
- x={Number(config.yAxis.size) + Number(xMax / 2)}
1022
+ x={Number(runtime.yAxis.size) + Number(xMax / 2)}
1010
1023
  y={initialHeight / 2 - (config.xAxis.padding || 0) / 2}
1011
1024
  textAnchor='middle'
1012
1025
  >
@@ -1031,293 +1044,311 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1031
1044
  </Group>
1032
1045
  {/* Highlighted regions */}
1033
1046
  {/* Y axis */}
1034
- {!['Spark Line', 'Forest Plot'].includes(visualizationType) && config.yAxis.type !== 'categorical' && (
1035
- <AxisLeft
1036
- scale={yScale}
1037
- tickLength={isLogarithmicAxis ? 6 : 8}
1038
- left={Number(runtime.yAxis.size) - config.yAxis.axisPadding}
1039
- label={runtime.yAxis.label || runtime.yAxis.label}
1040
- stroke='#333'
1041
- tickFormat={handleLeftTickFormatting}
1042
- numTicks={handleNumTicks}
1043
- >
1044
- {props => {
1045
- const axisCenter =
1046
- config.orientation === 'horizontal'
1047
- ? (props.axisToPoint.y - props.axisFromPoint.y) / 2
1048
- : (props.axisFromPoint.y - props.axisToPoint.y) / 2
1049
- const horizontalTickOffset =
1050
- yMax / props.ticks.length / 2 - (yMax / props.ticks.length) * (1 - config.barThickness) + 5
1051
- return (
1052
- <Group className='left-axis'>
1053
- {!config.yAxis.hideAxis && (
1054
- <Line
1055
- from={props.axisFromPoint}
1056
- to={
1057
- runtime.horizontal
1058
- ? {
1059
- x: 0,
1060
- y:
1061
- config.visualizationType === 'Forest Plot' ? parentHeight : Number(heights.horizontal)
1062
- }
1063
- : props.axisToPoint
1064
- }
1065
- stroke='#000'
1066
- />
1067
- )}
1068
- {orientation === 'vertical' && yScale.domain()[0] < 0 && (
1069
- // draw from the Left of the chart …
1070
- <Line
1071
- from={{ x: props.axisFromPoint.x, y: yScale(0) }}
1072
- to={{ x: xMax, y: yScale(0) }}
1073
- stroke='#333'
1074
- />
1075
- )}
1076
- {orientation === 'horizontal' && xScale.domain()[0] < 0 && (
1077
- <Line
1078
- // draw from the top of the char
1079
- from={{ x: xScale(0), y: 0 }}
1080
- to={{ x: xScale(0), y: yMax }}
1081
- stroke='#333'
1082
- />
1083
- )}
1084
- {visualizationType === 'Bar' && orientation === 'horizontal' && xScale.domain()[0] < 0 && (
1085
- <Line
1086
- from={{ x: xScale(0), y: 0 }}
1087
- to={{ x: xScale(0), y: yMax }}
1088
- stroke='#333'
1089
- strokeWidth={2}
1090
- />
1091
- )}
1092
- {props.ticks.map((tick, i) => {
1093
- const minY = props.ticks[0].to.y
1094
- const barMinHeight = 15 // 15 is the min height for bars by default
1095
- const showTicks = String(tick.value).startsWith('1') || tick.value === 0.1 ? 'block' : 'none'
1096
- const tickLength = showTicks === 'block' ? 7 : 0
1097
- const to = { x: tick.to.x - tickLength, y: tick.to.y }
1098
-
1099
- // Vertical value/suffix vars
1100
- const lastTick = props.ticks.length - 1 === i
1101
- const useInlineLabel = lastTick && inlineLabel
1102
- const hideTopTick = lastTick && inlineLabel && !inlineLabelHasNoSpace
1103
- const valueOnLinePadding = hideAxis ? -8 : -12
1104
- const labelXPadding = labelsAboveGridlines ? valueOnLinePadding : TICK_LABEL_MARGIN_RIGHT
1105
- const labelYPadding = labelsAboveGridlines ? 4 : 0
1106
- const labelX = tick.to.x - labelXPadding
1107
- const labelY = tick.to.y - labelYPadding
1108
- const labelVerticalAnchor = labelsAboveGridlines ? 'end' : 'middle'
1109
- const combineDomInlineLabelWithValue = inlineLabel && labelsAboveGridlines && lastTick
1110
- const formattedValue = useInlineLabel
1111
- ? String(tick?.formattedValue || '').replace(config.dataFormat.suffix, '')
1112
- : tick?.formattedValue
1113
-
1114
- return (
1115
- <Group key={`vx-tick-${tick.value}-${i}`} className={'vx-axis-tick'}>
1116
- {!runtime.yAxis.hideTicks && !labelsAboveGridlines && !hideTopTick && (
1117
- <Line
1118
- key={`${tick.value}--hide-hideTicks`}
1119
- from={tick.from}
1120
- to={isLogarithmicAxis ? to : tick.to}
1121
- stroke={config.yAxis.tickColor}
1122
- display={orientation === 'horizontal' ? 'none' : 'block'}
1123
- fontSize={tickLabelFontSize}
1124
- />
1125
- )}
1126
-
1127
- {orientation === 'horizontal' &&
1128
- visualizationType === 'Box Plot' &&
1129
- config.yAxis.labelPlacement === 'On Date/Category Axis' &&
1130
- !config.yAxis.hideLabel && (
1131
- <Text
1132
- x={tick.to.x}
1133
- y={yScale(tick.value) + yScale.bandwidth() / 2}
1134
- transform={`rotate(${
1135
- config.orientation === 'horizontal' ? config.runtime.yAxis.tickRotation || 0 : 0
1136
- }, ${tick.to.x}, ${tick.to.y})`}
1137
- verticalAnchor={'middle'}
1138
- textAnchor={'end'}
1139
- fontSize={tickLabelFontSize}
1140
- >
1141
- {tick.formattedValue}
1142
- </Text>
1143
- )}
1144
-
1145
- {orientation === 'horizontal' &&
1146
- visualizationType !== 'Box Plot' &&
1147
- visualizationSubType !== 'stacked' &&
1148
- config.yAxis.labelPlacement === 'On Date/Category Axis' &&
1149
- !config.yAxis.hideLabel && (
1150
- <Text
1151
- transform={`translate(${tick.to.x - 5}, ${
1152
- config.isLollipopChart
1153
- ? tick.to.y - minY
1154
- : tick.to.y -
1155
- minY +
1156
- (Number(config.barHeight * config.runtime.series.length) - barMinHeight) / 2
1157
- }) rotate(-${config.runtime.horizontal ? config.runtime.yAxis.tickRotation || 0 : 0})`}
1158
- verticalAnchor={'start'}
1159
- textAnchor={'end'}
1160
- fontSize={tickLabelFontSize}
1161
- >
1162
- {tick.formattedValue}
1163
- </Text>
1164
- )}
1165
-
1166
- {orientation === 'horizontal' &&
1167
- visualizationSubType === 'stacked' &&
1168
- config.yAxis.labelPlacement === 'On Date/Category Axis' &&
1169
- !config.yAxis.hideLabel && (
1170
- <Text
1171
- transform={`translate(${tick.to.x - 5}, ${
1172
- tick.to.y - minY + (Number(config.barHeight) - barMinHeight) / 2
1173
- }) rotate(-${runtime.horizontal ? runtime.yAxis.tickRotation : 0})`}
1174
- verticalAnchor={'start'}
1175
- textAnchor={'end'}
1176
- fontSize={tickLabelFontSize}
1177
- >
1178
- {tick.formattedValue}
1179
- </Text>
1180
- )}
1181
-
1182
- {orientation === 'horizontal' &&
1183
- visualizationType === 'Paired Bar' &&
1184
- !config.yAxis.hideLabel && (
1185
- <Text
1186
- transform={`translate(${tick.to.x - 5}, ${
1187
- tick.to.y - minY + Number(config.barHeight) / 2
1188
- }) rotate(-${runtime.horizontal ? runtime.yAxis.tickRotation : 0})`}
1189
- textAnchor={'end'}
1190
- verticalAnchor='middle'
1191
- fontSize={tickLabelFontSize}
1192
- >
1193
- {tick.formattedValue}
1194
- </Text>
1195
- )}
1196
- {orientation === 'horizontal' &&
1197
- visualizationType === 'Deviation Bar' &&
1198
- !config.yAxis.hideLabel && (
1199
- <Text
1200
- transform={`translate(${tick.to.x - 5}, ${
1201
- config.isLollipopChart
1202
- ? tick.to.y - minY + 2
1203
- : tick.to.y - minY + Number(config.barHeight) / 2
1204
- }) rotate(-${runtime.horizontal ? runtime.yAxis.tickRotation : 0})`}
1205
- textAnchor={'end'}
1206
- verticalAnchor='middle'
1047
+ {!['Spark Line', 'Forest Plot', 'Warming Stripes'].includes(visualizationType) &&
1048
+ 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}
1056
+ 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'}
1207
1139
  fontSize={tickLabelFontSize}
1208
- >
1209
- {tick.formattedValue}
1210
- </Text>
1140
+ />
1211
1141
  )}
1212
1142
 
1213
- {orientation === 'vertical' &&
1214
- visualizationType === 'Bump Chart' &&
1215
- !config.yAxis.hideLabel && (
1216
- <>
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 && (
1217
1203
  <Text
1218
- display={config.useLogScale ? showTicks : 'block'}
1219
- dx={config.useLogScale ? -6 : 0}
1220
- x={config.runtime.horizontal ? tick.from.x + 2 : tick.to.x - 8.5}
1221
- y={tick.to.y - 13 + (config.runtime.horizontal ? horizontalTickOffset : 0)}
1222
- angle={-Number(config.yAxis.tickRotation) || 0}
1223
- verticalAnchor={config.runtime.horizontal ? 'start' : 'middle'}
1224
- textAnchor={config.runtime.horizontal ? 'start' : 'end'}
1225
- fill={config.yAxis.tickLabelColor}
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'}
1226
1209
  fontSize={tickLabelFontSize}
1227
1210
  >
1228
- {config.runtime.seriesLabelsAll[tick.formattedValue - 1]}
1211
+ {tick.formattedValue}
1229
1212
  </Text>
1213
+ )}
1230
1214
 
1231
- {(seriesHighlight.length === 0 ||
1232
- seriesHighlight.includes(
1233
- config.runtime.seriesLabelsAll[tick.formattedValue - 1]
1234
- )) && (
1235
- <rect
1236
- x={0 - Number(config.yAxis.size)}
1237
- y={tick.to.y - 8 + (config.runtime.horizontal ? horizontalTickOffset : 7)}
1238
- width={Number(config.yAxis.size) + xScale(xScale.domain()[0])}
1239
- height='2'
1240
- fill={colorScale(config.runtime.seriesLabelsAll[tick.formattedValue - 1])}
1241
- />
1242
- )}
1243
- </>
1244
- )}
1245
- {orientation === 'vertical' &&
1246
- visualizationType !== 'Paired Bar' &&
1247
- visualizationType !== 'Bump Chart' &&
1248
- !config.yAxis.hideLabel && (
1249
- <>
1250
- {/* INLINE LABEL BEHAVIOR: Dom suffix for 'inlineLabel' behavior */}
1251
- {/* inline label is shown alone and is allowed to 'overflow' to the right */}
1252
- {/* SPECIAL ONE CHAR CASE: a one character inlineLabel does not overflow */}
1253
- {/* IF VALUES ON LINE: suffix is combined with value to avoid having to calculate varying (now left-aligned) value widths */}
1254
- {inlineLabel && lastTick && !labelsAboveGridlines && (
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 */}
1255
1309
  <BlurStrokeText
1256
- innerRef={suffixRef}
1310
+ innerRef={el => lastTick && (topYLabelRef.current = el)}
1257
1311
  display={isLogarithmicAxis ? showTicks : 'block'}
1258
1312
  dx={isLogarithmicAxis ? -6 : 0}
1259
- x={labelX}
1260
- y={labelY}
1313
+ x={inlineLabelHasNoSpace ? labelX - suffixWidth : labelX}
1314
+ y={labelY + (config.runtime.horizontal ? horizontalTickOffset : 0)}
1261
1315
  angle={-Number(config.yAxis.tickRotation) || 0}
1262
- verticalAnchor={labelVerticalAnchor}
1263
- textAnchor={inlineLabelHasNoSpace ? 'end' : 'start'}
1316
+ verticalAnchor={config.runtime.horizontal ? 'start' : labelVerticalAnchor}
1317
+ textAnchor={config.runtime.horizontal || labelsAboveGridlines ? 'start' : 'end'}
1264
1318
  fill={config.yAxis.tickLabelColor}
1265
1319
  stroke={'#fff'}
1266
- paintOrder={'stroke'} // keeps stroke under fill
1320
+ disableStroke={!labelsAboveGridlines}
1267
1321
  strokeLinejoin='round'
1322
+ paintOrder={'stroke'} // keeps stroke under fill
1268
1323
  style={{ whiteSpace: 'pre-wrap' }} // prevents leading spaces from being trimmed
1269
1324
  fontSize={tickLabelFontSize}
1270
1325
  >
1271
- {inlineLabel}
1326
+ {`${formattedValue}${combineDomInlineLabelWithValue ? inlineLabel : ''}`}
1272
1327
  </BlurStrokeText>
1273
- )}
1274
-
1275
- {/* VALUE */}
1276
- <BlurStrokeText
1277
- innerRef={el => lastTick && (topYLabelRef.current = el)}
1278
- display={isLogarithmicAxis ? showTicks : 'block'}
1279
- dx={isLogarithmicAxis ? -6 : 0}
1280
- x={inlineLabelHasNoSpace ? labelX - suffixWidth : labelX}
1281
- y={labelY + (config.runtime.horizontal ? horizontalTickOffset : 0)}
1282
- angle={-Number(config.yAxis.tickRotation) || 0}
1283
- verticalAnchor={config.runtime.horizontal ? 'start' : labelVerticalAnchor}
1284
- textAnchor={config.runtime.horizontal || labelsAboveGridlines ? 'start' : 'end'}
1285
- fill={config.yAxis.tickLabelColor}
1286
- stroke={'#fff'}
1287
- disableStroke={!labelsAboveGridlines}
1288
- strokeLinejoin='round'
1289
- paintOrder={'stroke'} // keeps stroke under fill
1290
- style={{ whiteSpace: 'pre-wrap' }} // prevents leading spaces from being trimmed
1291
- fontSize={tickLabelFontSize}
1292
- >
1293
- {`${formattedValue}${combineDomInlineLabelWithValue ? inlineLabel : ''}`}
1294
- </BlurStrokeText>
1295
- </>
1296
- )}
1297
- </Group>
1298
- )
1299
- })}
1300
- <Text
1301
- className='y-label'
1302
- textAnchor='middle'
1303
- verticalAnchor='start'
1304
- transform={`translate(${-1 * runtime.yAxis.size + yLabelOffset}, ${axisCenter}) rotate(-90)`}
1305
- fontWeight='bold'
1306
- fill={config.yAxis.labelColor}
1307
- fontSize={axisLabelFontSize}
1308
- >
1309
- {props.label}
1310
- </Text>
1311
- </Group>
1312
- )
1313
- }}
1314
- </AxisLeft>
1315
- )}
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>
1348
+ )}
1316
1349
  {config.yAxis.type === 'categorical' && config.orientation === 'vertical' && (
1317
1350
  <CategoricalYAxis
1318
- max={max}
1319
- maxValue={maxValue}
1320
- height={initialHeight}
1351
+ yScale={yScale}
1321
1352
  xMax={xMax}
1322
1353
  yMax={yMax}
1323
1354
  leftSize={Number(runtime.yAxis.size) - config.yAxis.axisPadding}
@@ -1327,7 +1358,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1327
1358
  {hasRightAxis && (
1328
1359
  <AxisRight
1329
1360
  scale={yScaleRight}
1330
- left={Number(width - config.yAxis.rightAxisSize)}
1361
+ left={Number(runtime.yAxis.size + xMax)}
1331
1362
  label={config.yAxis.rightLabel}
1332
1363
  tickFormat={tick => formatNumber(tick, 'right')}
1333
1364
  numTicks={runtime.yAxis.rightNumTicks || undefined}
@@ -1420,7 +1451,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1420
1451
  : yMax
1421
1452
  }
1422
1453
  left={config.visualizationType !== 'Forest Plot' ? Number(runtime.yAxis.size) : 0}
1423
- label={runtime[section].label}
1454
+ label={runtime.xAxis.label}
1424
1455
  tickFormat={handleBottomTickFormatting}
1425
1456
  scale={xScale}
1426
1457
  stroke='#333'
@@ -1431,15 +1462,22 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1431
1462
  ? getTickValues(xAxisDataMapped, xScale, isDateTime ? xTickCount : getManualStep(), config)
1432
1463
  : config.runtime.xAxis.type === 'date'
1433
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
1434
1470
  : undefined
1435
1471
  }
1436
1472
  >
1437
1473
  {props => {
1438
1474
  const hasDynamicCategory = config.series.some(s => s.dynamicCategory)
1475
+
1439
1476
  // For these charts, we generated all ticks in tickValues above, and now need to filter/shift them
1440
1477
  // so the last tick is always labeled
1478
+ // Use uniqueXAxisDataMapped for date filtering to match the tickValues we set
1441
1479
  if (config.runtime.xAxis.type === 'date' && !config.runtime.xAxis.manual && !hasDynamicCategory) {
1442
- props.ticks = filterAndShiftLinearDateTicks(config, props, xAxisDataMapped, formatDate)
1480
+ props.ticks = filterAndShiftLinearDateTicks(config, props, uniqueXAxisDataMapped, formatDate)
1443
1481
  }
1444
1482
 
1445
1483
  const distanceBetweenTicks =
@@ -1483,7 +1521,8 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1483
1521
  const sumOfTickWidth = textWidths.reduce((a, b) => a + b, accumulator)
1484
1522
  const spaceBetweenEachTick = (xMax - sumOfTickWidth) / (filteredTicks.length - 1)
1485
1523
  const bufferBetweenTicks = 40
1486
- const maxLengthOfTick = width / filteredTicks.length - X_TICK_LABEL_PADDING * 2 - bufferBetweenTicks
1524
+ const maxLengthOfTick =
1525
+ parentWidth / filteredTicks.length - X_TICK_LABEL_PADDING * 2 - bufferBetweenTicks
1487
1526
 
1488
1527
  // Determine the position of each tick
1489
1528
  let positions = [0] // The first tick is at position 0
@@ -1514,7 +1553,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1514
1553
  config.xAxis.tickWidthMax = longestTickLength
1515
1554
 
1516
1555
  return (
1517
- <Group className='bottom-axis' width={dimensions[0]}>
1556
+ <Group className='bottom-axis' width={parentWidth}>
1518
1557
  {filteredTicks.map((tick, i, propsTicks) => {
1519
1558
  // when using LogScale show major ticks values only
1520
1559
  const showTick = String(tick.value).startsWith('1') || tick.value === 0.1 ? 'block' : 'none'
@@ -1554,7 +1593,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1554
1593
  verticalAnchor={tickRotation < -50 ? 'middle' : 'start'}
1555
1594
  textAnchor={tickRotation ? 'end' : 'middle'}
1556
1595
  width={
1557
- areTicksTouching && !config.isResponsiveTicks && !Number(config[section].tickRotation)
1596
+ areTicksTouching && !config.isResponsiveTicks && !Number(config.xAxis.tickRotation)
1558
1597
  ? limitedWidth
1559
1598
  : undefined
1560
1599
  }
@@ -1579,7 +1618,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1579
1618
  fill={config.xAxis.labelColor}
1580
1619
  fontSize={axisLabelFontSize}
1581
1620
  >
1582
- {props.label}
1621
+ {!config.hideXAxisLabel ? props.label : null}
1583
1622
  </Text>
1584
1623
  </Group>
1585
1624
  )
@@ -1636,6 +1675,27 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1636
1675
  />
1637
1676
  )}
1638
1677
  <div className='animation-trigger' ref={triggerRef} />
1678
+ {/* SEPARATED BRUSH - Independent SVG overlay */}
1679
+ {config.xAxis.brushActive && config.xAxis.type !== 'categorical' && xMax > 0 && (
1680
+ <div
1681
+ style={{
1682
+ position: 'relative',
1683
+ marginTop: '10px',
1684
+ left: `${runtime.yAxis.size || 0}px`,
1685
+ width: `${Math.max(xMax, 100)}px`,
1686
+ height: '70px',
1687
+ pointerEvents: 'auto',
1688
+ zIndex: 15,
1689
+ touchAction: 'none', // Enable touch interactions for brush
1690
+ WebkitTouchCallout: 'none',
1691
+ WebkitUserSelect: 'none',
1692
+ userSelect: 'none'
1693
+ }}
1694
+ className='brush-overlay'
1695
+ >
1696
+ <BrushSelector xMax={Math.max(xMax, 100)} yMax={70} />
1697
+ </div>
1698
+ )}
1639
1699
  </div>
1640
1700
  </ErrorBoundary>
1641
1701
  )