@cdc/chart 4.25.11 → 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 (77) hide show
  1. package/dist/cdcchart.js +38898 -40013
  2. package/examples/feature/pie/planet-pie-example-config.json +48 -2
  3. package/examples/private/DEV-12100.json +1303 -0
  4. package/examples/private/cat-y.json +1235 -0
  5. package/examples/private/data-points.json +228 -0
  6. package/examples/private/height.json +3915 -0
  7. package/examples/private/links.json +569 -0
  8. package/examples/private/quadrant.txt +30 -0
  9. package/examples/private/test-forecast.json +5510 -0
  10. package/examples/private/warming-stripe-test.json +2578 -0
  11. package/examples/private/warming-stripes.json +4763 -0
  12. package/examples/tech-adoption-with-links.json +560 -0
  13. package/index.html +15 -20
  14. package/package.json +5 -4
  15. package/preview.html +1616 -0
  16. package/src/CdcChartComponent.tsx +111 -75
  17. package/src/_stories/Chart.Regions.Categorical.stories.tsx +148 -0
  18. package/src/_stories/Chart.Regions.DateScale.stories.tsx +197 -0
  19. package/src/_stories/Chart.Regions.DateTimeScale.stories.tsx +297 -0
  20. package/src/_stories/Chart.stories.tsx +8 -0
  21. package/src/_stories/ChartBar.Editor.stories.tsx +11 -6
  22. package/src/_stories/ChartBrush.Editor.stories.tsx +295 -0
  23. package/src/_stories/ChartBrush.stories.tsx +50 -0
  24. package/src/_stories/ChartEditor.Editor.stories.tsx +3 -5
  25. package/src/_stories/TechAdoptionWithLinks.stories.tsx +27 -0
  26. package/src/_stories/_mock/brush_enabled.json +326 -0
  27. package/src/_stories/_mock/brush_mock.json +2 -69
  28. package/src/_stories/_mock/horizontal-bars-dynamic-y-axis.json +413 -0
  29. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +1 -2
  30. package/src/components/Axis/Categorical.Axis.tsx +6 -7
  31. package/src/components/BarChart/components/BarChart.Horizontal.tsx +178 -24
  32. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +3 -1
  33. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +1 -0
  34. package/src/components/BarChart/components/BarChart.Vertical.tsx +6 -8
  35. package/src/components/BarChart/components/context.tsx +1 -0
  36. package/src/components/BarChart/helpers/useBarChart.ts +14 -2
  37. package/src/components/Brush/BrushSelector.tsx +1258 -0
  38. package/src/components/Brush/MiniChartPreview.tsx +283 -0
  39. package/src/components/DeviationBar.jsx +9 -7
  40. package/src/components/EditorPanel/EditorPanel.tsx +2711 -2586
  41. package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +56 -34
  42. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +57 -30
  43. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +2 -0
  44. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +30 -25
  45. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +21 -27
  46. package/src/components/EditorPanel/useEditorPermissions.ts +31 -18
  47. package/src/components/Legend/Legend.tsx +3 -2
  48. package/src/components/Legend/helpers/createFormatLabels.tsx +151 -2
  49. package/src/components/Legend/helpers/index.ts +10 -6
  50. package/src/components/LinearChart.tsx +495 -430
  51. package/src/components/PairedBarChart.jsx +20 -3
  52. package/src/components/Regions/components/Regions.tsx +365 -122
  53. package/src/components/ScatterPlot/ScatterPlot.jsx +2 -2
  54. package/src/components/SmallMultiples/SmallMultipleTile.tsx +5 -1
  55. package/src/components/WarmingStripes/WarmingStripes.tsx +160 -0
  56. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +35 -0
  57. package/src/components/WarmingStripes/WarmingStripesGradientLegend.tsx +104 -0
  58. package/src/components/WarmingStripes/index.tsx +3 -0
  59. package/src/data/initial-state.js +3 -1
  60. package/src/helpers/calculateHorizontalBarCategoryLabelWidth.ts +57 -0
  61. package/src/helpers/getMinMax.ts +12 -7
  62. package/src/helpers/sizeHelpers.ts +0 -20
  63. package/src/helpers/smallMultiplesHelpers.ts +1 -1
  64. package/src/hooks/useChartHoverAnalytics.tsx +10 -9
  65. package/src/hooks/useScales.ts +11 -1
  66. package/src/hooks/useTooltip.tsx +31 -10
  67. package/src/scss/DataTable.scss +0 -4
  68. package/src/scss/main.scss +17 -3
  69. package/src/test/CdcChart.test.jsx +1 -1
  70. package/src/types/ChartConfig.ts +3 -0
  71. package/src/types/Label.ts +1 -0
  72. package/src/utils/analyticsTracking.ts +19 -0
  73. package/LICENSE +0 -201
  74. package/src/components/Brush/BrushChart.tsx +0 -128
  75. package/src/components/Brush/BrushController.tsx +0 -71
  76. package/src/components/Brush/types.tsx +0 -8
  77. package/src/components/BrushChart.tsx +0 -223
@@ -1,4 +1,13 @@
1
- import React, { forwardRef, useContext, useEffect, useImperativeHandle, 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,13 +36,15 @@ 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
50
  import useReduceData from '../hooks/useReduceData'
@@ -97,6 +108,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
97
108
  handleDragStateChange,
98
109
  interactionLabel,
99
110
  isDraggingAnnotation,
111
+ isEditor,
100
112
  legendRef,
101
113
  parseDate,
102
114
  parentRef,
@@ -125,13 +137,18 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
125
137
  const { labelsAboveGridlines, hideAxis, inlineLabel } = config.yAxis
126
138
 
127
139
  // HOOKS % STATES
128
- const { minValue, maxValue, existPositiveValue, isAllLine } = useReduceData(config, data)
129
- 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()
130
146
  const { hasTopAxis } = getTopAxis(config)
131
147
  const [animatedChart, setAnimatedChart] = useState(false)
132
148
  const [showHoverLine, setShowHoverLine] = useState(false)
133
149
  const [point, setPoint] = useState({ x: 0, y: 0 })
134
150
  const [suffixWidth, setSuffixWidth] = useState(0)
151
+ const [calculatedSvgHeight, setCalculatedSvgHeight] = useState<number | null>(null)
135
152
 
136
153
  // REFS
137
154
  const axisBottomRef = useRef(null)
@@ -141,7 +158,6 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
141
158
  const triggerRef = useRef()
142
159
  const xAxisLabelRefs = useRef([])
143
160
  const xAxisTitleRef = useRef(null)
144
- const gridLineRefs = useRef([])
145
161
  const tooltipRef = useRef(null)
146
162
 
147
163
  const dataRef = useIntersectionObserver(triggerRef, {
@@ -167,45 +183,19 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
167
183
 
168
184
  // height before bottom axis
169
185
  const initialHeight = useMemo(
170
- () => calcInitialHeight(config, currentViewport),
171
- [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]
172
188
  )
173
189
  const forestHeight = useMemo(() => initialHeight + forestRowsHeight, [initialHeight, forestRowsHeight])
174
190
 
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
191
  // Used to calculate the y position of the x-axis title
200
192
  const bottomLabelStart = useMemo(() => {
201
193
  xAxisLabelRefs.current = xAxisLabelRefs.current?.filter(label => label)
202
194
  if (!xAxisLabelRefs.current.length) return
203
195
  const tallestLabel = Math.max(...xAxisLabelRefs.current.map(label => label.getBBox().height))
204
196
  return tallestLabel + X_TICK_LABEL_PADDING + DEFAULT_TICK_LENGTH
205
- }, [dimensions[0], config.xAxis, xAxisLabelRefs.current, config.xAxis.tickRotation])
197
+ }, [parentWidth, config.xAxis, xAxisLabelRefs.current, config.xAxis.tickRotation])
206
198
 
207
- // xMax and yMax
208
- const xMax = width - runtime.yAxis.size - (visualizationType === 'Combo' ? config.yAxis.rightAxisSize : 0)
209
199
  const yMax = initialHeight + forestRowsHeight
210
200
 
211
201
  const isNoDataAvailable = config.filters?.length > 0 && data.length === 0
@@ -216,8 +206,25 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
216
206
  : d[config.runtime.originalXAxis.dataKey]
217
207
  const getYAxisData = (d, seriesKey) => d[seriesKey]
218
208
  const xAxisDataMapped = data.map(d => getXAxisData(d))
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])
219
224
  const { yScaleRight, hasRightAxis } = useRightAxis({ config, yMax, data })
220
225
 
226
+ const xMax = parentWidth - Number(runtime.yAxis.size) - (hasRightAxis ? config.yAxis.rightAxisSize : 0)
227
+
221
228
  const {
222
229
  xScale,
223
230
  yScale,
@@ -240,15 +247,29 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
240
247
  existPositiveValue,
241
248
  xAxisDataMapped,
242
249
  yMax,
243
- dimensions,
244
- xMax:
245
- parentWidth -
246
- Number(config.orientation === 'horizontal' ? config.xAxis.size : config.yAxis.size) -
247
- (hasRightAxis ? config.yAxis.rightAxisSize : 0),
250
+ xMax,
248
251
  needsYAxisAutoPadding,
249
252
  currentViewport
250
253
  })
251
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
+
252
273
  const [yTickCount, xTickCount] = ['yAxis', 'xAxis'].map(axis =>
253
274
  countNumOfTicks({ axis, max, runtime, currentViewport, isHorizontal, data, config, min })
254
275
  )
@@ -381,23 +402,6 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
381
402
  hideTooltip
382
403
  })
383
404
 
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
405
  // Make sure the chart is visible if in the editor
402
406
  /* eslint-disable react-hooks/exhaustive-deps */
403
407
  useEffect(() => {
@@ -449,16 +453,24 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
449
453
  const topLabelOnGridline = topYLabelRef.current && yAxis.labelsAboveGridlines
450
454
 
451
455
  // Heights to add
452
-
453
- const brushHeight = 25
454
- const brushHeightWithMargin = config.xAxis.brushActive ? brushHeight + brushHeight : 0
455
456
  const forestRowsHeight = isForestPlot ? config.data.length * forestPlot.rowHeight : 0
456
457
  const topLabelOnGridlineHeight = topLabelOnGridline ? topYLabelRef.current.getBBox().height : 0
457
- const additionalHeight = axisBottomHeight + brushHeightWithMargin + forestRowsHeight + topLabelOnGridlineHeight
458
- 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
+
459
469
  if (!parentRef.current) return
470
+ parentRef.current.style.height = `${parentHeight}px`
460
471
 
461
- parentRef.current.style.height = `${newHeight}px`
472
+ // Set the calculated SVG height via state to ensure it's used on render
473
+ setCalculatedSvgHeight(svgHeight)
462
474
 
463
475
  /* Adding text above the top gridline overflows the bounds of the svg.
464
476
  To accommodate for this we need to...
@@ -472,7 +484,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
472
484
  const svg = internalSvgRef.current
473
485
  if (!svg) return
474
486
  const parentWidthFromRef = parentRef.current.getBoundingClientRect().width
475
- svg.setAttribute('viewBox', `0 ${-topLabelOnGridlineHeight} ${parentWidthFromRef} ${newHeight}`)
487
+ svg.setAttribute('viewBox', `0 ${-topLabelOnGridlineHeight} ${parentWidthFromRef} ${svgHeight}`)
476
488
 
477
489
  // translate legend match viewBox-adjusted height
478
490
  if (!legendRef.current) return
@@ -506,8 +518,8 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
506
518
  tooltipRef.current.node.style.maxWidth = `${maxWidth}px`
507
519
  }, [tooltipOpen, tooltipData])
508
520
 
509
- // Check if small multiples are enabled - if so, render SmallMultiples instead
510
- if (config.smallMultiples?.mode) {
521
+ // Check if small multiples are enabled and supported - if so, render SmallMultiples instead
522
+ if (config.smallMultiples?.mode && visSupportsSmallMultiples()) {
511
523
  return (
512
524
  <SmallMultiples
513
525
  config={config}
@@ -663,7 +675,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
663
675
  </>
664
676
  )
665
677
  }
666
- return isNaN(width) ? (
678
+ return isNaN(parentWidth) ? (
667
679
  <React.Fragment></React.Fragment>
668
680
  ) : (
669
681
  <ErrorBoundary component='LinearChart'>
@@ -676,7 +688,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
676
688
  ref={internalSvgRef}
677
689
  onMouseMove={onMouseMove}
678
690
  width={parentWidth + config.yAxis.rightAxisSize}
679
- height={isNoDataAvailable ? 1 : parentHeight}
691
+ height={isNoDataAvailable ? 1 : calculatedSvgHeight ?? parentHeight}
680
692
  className={`linear ${config.animate ? 'animated' : ''} ${animatedChart && config.animate ? 'animate' : ''} ${
681
693
  debugSvg && 'debug'
682
694
  } ${isDraggingAnnotation && 'dragging-annotation'}`}
@@ -696,61 +708,79 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
696
708
  {!isDraggingAnnotation && <Bar width={parentWidth} height={initialHeight} fill={'transparent'}></Bar>}{' '}
697
709
  {/* GRID LINES */}
698
710
  {/* 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
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)
710
766
  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>
767
+ <Line
768
+ key={`horizontal-gridline-${tickValue}-${i}`}
769
+ from={{ x: tickPosition, y: 0 }}
770
+ to={{ x: tickPosition, y: yMax }}
771
+ stroke='#d6d6d6'
772
+ />
745
773
  )
746
- }}
747
- </AxisLeft>
774
+ })}
775
+ </Group>
748
776
  )}
749
777
  {visualizationType === 'Paired Bar' && generatePairedBarAxis()}
750
778
  {visualizationType === 'Deviation Bar' && config.runtime.series?.length === 1 && (
751
779
  <DeviationBar animatedChart={animatedChart} xScale={xScale} yScale={yScale} width={xMax} height={yMax} />
752
780
  )}
753
- {visualizationType === 'Paired Bar' && <PairedBarChart originalWidth={width} width={xMax} height={yMax} />}
781
+ {visualizationType === 'Paired Bar' && (
782
+ <PairedBarChart originalWidth={parentWidth} width={xMax} height={yMax} />
783
+ )}
754
784
  {visualizationType === 'Scatter Plot' && (
755
785
  <ScatterPlot
756
786
  xScale={xScale}
@@ -766,6 +796,9 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
766
796
  showTooltip={showTooltip}
767
797
  />
768
798
  )}
799
+ {visualizationType === 'Warming Stripes' && (
800
+ <WarmingStripes xScale={xScale} yScale={yScale} xMax={xMax} yMax={yMax} />
801
+ )}
769
802
  {visualizationType === 'Box Plot' && config.orientation === 'vertical' && (
770
803
  <BoxPlotVertical
771
804
  seriesScale={seriesScale}
@@ -841,9 +874,16 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
841
874
  )}
842
875
  {/* Line chart */}
843
876
  {/* TODO: Make this just line or combo? */}
844
- {!['Paired Bar', 'Box Plot', 'Area Chart', 'Scatter Plot', 'Deviation Bar', 'Forecasting', 'Bar'].includes(
845
- visualizationType
846
- ) &&
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) &&
847
887
  !convertLineToBarGraph && (
848
888
  <>
849
889
  <LineChart
@@ -879,7 +919,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
879
919
  xScale={xScale}
880
920
  yScale={yScale}
881
921
  seriesScale={seriesScale}
882
- width={width}
922
+ width={parentWidth}
883
923
  height={forestHeight}
884
924
  getXAxisData={getXAxisData}
885
925
  getYAxisData={getYAxisData}
@@ -895,29 +935,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
895
935
  forestPlotRightLabelRef={forestPlotRightLabelRef}
896
936
  />
897
937
  )}
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
- )}
938
+ {/* Brush moved to separate overlay - no longer in main SVG */}
921
939
  {/* y anchors */}
922
940
  {config.yAxis.anchors &&
923
941
  config.yAxis.anchors.map((anchor, index) => {
@@ -938,13 +956,13 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
938
956
  return (
939
957
  // prettier-ignore
940
958
  <Line
941
- key={`yAxis-${anchor.value}--${index}`}
942
- strokeDasharray={handleLineType(anchor.lineStyle)}
943
- stroke={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
944
- className='anchor-y'
945
- from={{ x: 0 + padding, y: position - middleOffset }}
946
- to={{ x: width - config.yAxis.rightAxisSize, y: position - middleOffset }}
947
- />
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
+ />
948
966
  )
949
967
  })}
950
968
  {/* x anchors */}
@@ -974,14 +992,14 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
974
992
  return (
975
993
  // prettier-ignore
976
994
  <Line
977
- key={`xAxis-${anchor.value}--${index}`}
978
- strokeDasharray={handleLineType(anchor.lineStyle)}
979
- stroke={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
980
- fill={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
981
- className='anchor-x'
982
- from={{ x: Number(anchorPosition) + Number(padding), y: 0 }}
983
- to={{ x: Number(anchorPosition) + Number(padding), y: yMax }}
984
- />
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
+ />
985
1003
  )
986
1004
  })}
987
1005
  {/* we are handling regions in bar charts differently, so that we can calculate the bar group into the region space. */}
@@ -996,12 +1014,12 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
996
1014
  hideTooltip={hideTooltip}
997
1015
  tooltipData={tooltipData}
998
1016
  yMax={yMax}
999
- width={width}
1017
+ xMax={xMax}
1000
1018
  />
1001
1019
  )}
1002
1020
  {isNoDataAvailable && (
1003
1021
  <Text
1004
- x={Number(config.yAxis.size) + Number(xMax / 2)}
1022
+ x={Number(runtime.yAxis.size) + Number(xMax / 2)}
1005
1023
  y={initialHeight / 2 - (config.xAxis.padding || 0) / 2}
1006
1024
  textAnchor='middle'
1007
1025
  >
@@ -1026,293 +1044,311 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1026
1044
  </Group>
1027
1045
  {/* Highlighted regions */}
1028
1046
  {/* 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'
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'}
1202
1139
  fontSize={tickLabelFontSize}
1203
- >
1204
- {tick.formattedValue}
1205
- </Text>
1140
+ />
1206
1141
  )}
1207
1142
 
1208
- {orientation === 'vertical' &&
1209
- visualizationType === 'Bump Chart' &&
1210
- !config.yAxis.hideLabel && (
1211
- <>
1143
+ {orientation === 'horizontal' &&
1144
+ visualizationType === 'Box Plot' &&
1145
+ config.yAxis.labelPlacement === 'On Date/Category Axis' &&
1146
+ !config.yAxis.hideLabel && (
1212
1147
  <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}
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'}
1221
1155
  fontSize={tickLabelFontSize}
1222
1156
  >
1223
- {config.runtime.seriesLabelsAll[tick.formattedValue - 1]}
1157
+ {tick.formattedValue}
1224
1158
  </Text>
1159
+ )}
1160
+
1161
+ {orientation === 'horizontal' &&
1162
+ visualizationType === 'Bar' &&
1163
+ config.yAxis.labelPlacement === 'On Date/Category Axis' &&
1164
+ !config.yAxis.hideLabel &&
1165
+ (() => {
1166
+ const barGroupCount =
1167
+ config.visualizationSubType === 'stacked' ? 1 : config.runtime.seriesKeys.length
1168
+
1169
+ // Calculate barHeight based on chart type (regular bar vs lollipop)
1170
+ let barHeight
1171
+ if (config.isLollipopChart) {
1172
+ const lollipopSizes = { large: 7, medium: 6, small: 5 }
1173
+ const lollipopBarWidth = lollipopSizes[config.lollipopSize] || 5
1174
+ barHeight = lollipopBarWidth * barGroupCount
1175
+ } else {
1176
+ barHeight = Number(config.barHeight) * barGroupCount
1177
+ }
1178
+
1179
+ const totalBarHeight = barHeight + Number(config.barSpace)
1180
+ const barGroupY = i === 0 ? 0 : totalBarHeight * i
1181
+ const labelCenterY = barGroupY + barHeight / 2
1182
+
1183
+ return (
1184
+ <Text
1185
+ x={tick.from.x - Number(runtime.yAxis.size) + horizontalYAxisLabelSpace}
1186
+ y={labelCenterY}
1187
+ verticalAnchor={'middle'}
1188
+ textAnchor={'start'}
1189
+ fontSize={tickLabelFontSize}
1190
+ width={categoryLabelSpace}
1191
+ lineHeight={'1.2em'}
1192
+ >
1193
+ {tick.formattedValue}
1194
+ </Text>
1195
+ )
1196
+ })()}
1197
+
1198
+ {orientation === 'horizontal' &&
1199
+ visualizationType !== 'Bar' &&
1200
+ visualizationSubType === 'stacked' &&
1201
+ config.yAxis.labelPlacement === 'On Date/Category Axis' &&
1202
+ !config.yAxis.hideLabel && (
1203
+ <Text
1204
+ transform={`translate(${tick.to.x - 5}, ${
1205
+ tick.to.y - minY + (Number(config.barHeight) - barMinHeight) / 2
1206
+ }) rotate(-${runtime.horizontal ? runtime.yAxis.tickRotation : 0})`}
1207
+ verticalAnchor={'start'}
1208
+ textAnchor={'end'}
1209
+ fontSize={tickLabelFontSize}
1210
+ >
1211
+ {tick.formattedValue}
1212
+ </Text>
1213
+ )}
1225
1214
 
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 && (
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 */}
1250
1309
  <BlurStrokeText
1251
- innerRef={suffixRef}
1310
+ innerRef={el => lastTick && (topYLabelRef.current = el)}
1252
1311
  display={isLogarithmicAxis ? showTicks : 'block'}
1253
1312
  dx={isLogarithmicAxis ? -6 : 0}
1254
- x={labelX}
1255
- y={labelY}
1313
+ x={inlineLabelHasNoSpace ? labelX - suffixWidth : labelX}
1314
+ y={labelY + (config.runtime.horizontal ? horizontalTickOffset : 0)}
1256
1315
  angle={-Number(config.yAxis.tickRotation) || 0}
1257
- verticalAnchor={labelVerticalAnchor}
1258
- textAnchor={inlineLabelHasNoSpace ? 'end' : 'start'}
1316
+ verticalAnchor={config.runtime.horizontal ? 'start' : labelVerticalAnchor}
1317
+ textAnchor={config.runtime.horizontal || labelsAboveGridlines ? 'start' : 'end'}
1259
1318
  fill={config.yAxis.tickLabelColor}
1260
1319
  stroke={'#fff'}
1261
- paintOrder={'stroke'} // keeps stroke under fill
1320
+ disableStroke={!labelsAboveGridlines}
1262
1321
  strokeLinejoin='round'
1322
+ paintOrder={'stroke'} // keeps stroke under fill
1263
1323
  style={{ whiteSpace: 'pre-wrap' }} // prevents leading spaces from being trimmed
1264
1324
  fontSize={tickLabelFontSize}
1265
1325
  >
1266
- {inlineLabel}
1326
+ {`${formattedValue}${combineDomInlineLabelWithValue ? inlineLabel : ''}`}
1267
1327
  </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
- )}
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
+ )}
1311
1349
  {config.yAxis.type === 'categorical' && config.orientation === 'vertical' && (
1312
1350
  <CategoricalYAxis
1313
- max={max}
1314
- maxValue={maxValue}
1315
- height={initialHeight}
1351
+ yScale={yScale}
1316
1352
  xMax={xMax}
1317
1353
  yMax={yMax}
1318
1354
  leftSize={Number(runtime.yAxis.size) - config.yAxis.axisPadding}
@@ -1322,7 +1358,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1322
1358
  {hasRightAxis && (
1323
1359
  <AxisRight
1324
1360
  scale={yScaleRight}
1325
- left={Number(width - config.yAxis.rightAxisSize)}
1361
+ left={Number(runtime.yAxis.size + xMax)}
1326
1362
  label={config.yAxis.rightLabel}
1327
1363
  tickFormat={tick => formatNumber(tick, 'right')}
1328
1364
  numTicks={runtime.yAxis.rightNumTicks || undefined}
@@ -1426,15 +1462,22 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1426
1462
  ? getTickValues(xAxisDataMapped, xScale, isDateTime ? xTickCount : getManualStep(), config)
1427
1463
  : config.runtime.xAxis.type === 'date'
1428
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
1429
1470
  : undefined
1430
1471
  }
1431
1472
  >
1432
1473
  {props => {
1433
1474
  const hasDynamicCategory = config.series.some(s => s.dynamicCategory)
1475
+
1434
1476
  // For these charts, we generated all ticks in tickValues above, and now need to filter/shift them
1435
1477
  // so the last tick is always labeled
1478
+ // Use uniqueXAxisDataMapped for date filtering to match the tickValues we set
1436
1479
  if (config.runtime.xAxis.type === 'date' && !config.runtime.xAxis.manual && !hasDynamicCategory) {
1437
- props.ticks = filterAndShiftLinearDateTicks(config, props, xAxisDataMapped, formatDate)
1480
+ props.ticks = filterAndShiftLinearDateTicks(config, props, uniqueXAxisDataMapped, formatDate)
1438
1481
  }
1439
1482
 
1440
1483
  const distanceBetweenTicks =
@@ -1478,7 +1521,8 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1478
1521
  const sumOfTickWidth = textWidths.reduce((a, b) => a + b, accumulator)
1479
1522
  const spaceBetweenEachTick = (xMax - sumOfTickWidth) / (filteredTicks.length - 1)
1480
1523
  const bufferBetweenTicks = 40
1481
- const maxLengthOfTick = width / filteredTicks.length - X_TICK_LABEL_PADDING * 2 - bufferBetweenTicks
1524
+ const maxLengthOfTick =
1525
+ parentWidth / filteredTicks.length - X_TICK_LABEL_PADDING * 2 - bufferBetweenTicks
1482
1526
 
1483
1527
  // Determine the position of each tick
1484
1528
  let positions = [0] // The first tick is at position 0
@@ -1509,7 +1553,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1509
1553
  config.xAxis.tickWidthMax = longestTickLength
1510
1554
 
1511
1555
  return (
1512
- <Group className='bottom-axis' width={dimensions[0]}>
1556
+ <Group className='bottom-axis' width={parentWidth}>
1513
1557
  {filteredTicks.map((tick, i, propsTicks) => {
1514
1558
  // when using LogScale show major ticks values only
1515
1559
  const showTick = String(tick.value).startsWith('1') || tick.value === 0.1 ? 'block' : 'none'
@@ -1631,6 +1675,27 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1631
1675
  />
1632
1676
  )}
1633
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
+ )}
1634
1699
  </div>
1635
1700
  </ErrorBoundary>
1636
1701
  )