@cdc/chart 4.24.9-1 → 4.24.10

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 (62) hide show
  1. package/dist/cdcchart.js +37673 -36530
  2. package/index.html +1 -1
  3. package/package.json +2 -2
  4. package/src/CdcChart.tsx +128 -106
  5. package/src/_stories/Chart.Legend.Gradient.stories.tsx +33 -0
  6. package/src/_stories/Chart.stories.tsx +28 -0
  7. package/src/_stories/ChartAxisLabels.stories.tsx +20 -0
  8. package/src/_stories/ChartAxisTitles.stories.tsx +53 -0
  9. package/src/_stories/ChartPrefixSuffix.stories.tsx +151 -0
  10. package/src/_stories/_mock/horizontal_bar.json +257 -0
  11. package/src/_stories/_mock/large_x_axis_labels.json +261 -0
  12. package/src/_stories/_mock/paired-bar.json +262 -0
  13. package/src/_stories/_mock/pie_with_data.json +255 -0
  14. package/src/_stories/_mock/simplified_line.json +1510 -0
  15. package/src/components/Annotations/components/AnnotationDraggable.tsx +0 -3
  16. package/src/components/Annotations/components/AnnotationDropdown.tsx +1 -1
  17. package/src/components/Axis/Categorical.Axis.tsx +22 -4
  18. package/src/components/BarChart/components/BarChart.Horizontal.tsx +95 -16
  19. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +41 -17
  20. package/src/components/BarChart/components/BarChart.Vertical.tsx +78 -20
  21. package/src/components/BarChart/helpers/index.ts +23 -4
  22. package/src/components/BrushChart.tsx +3 -2
  23. package/src/components/DeviationBar.jsx +58 -8
  24. package/src/components/EditorPanel/EditorPanel.tsx +62 -39
  25. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +6 -23
  26. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +21 -4
  27. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +297 -35
  28. package/src/components/EditorPanel/components/panels.scss +4 -6
  29. package/src/components/EditorPanel/editor-panel.scss +0 -8
  30. package/src/components/EditorPanel/helpers/tests/updateFieldRankByValue.test.ts +38 -0
  31. package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +42 -0
  32. package/src/components/EditorPanel/useEditorPermissions.ts +1 -0
  33. package/src/components/ForestPlot/ForestPlot.tsx +2 -3
  34. package/src/components/ForestPlot/ForestPlotProps.ts +2 -0
  35. package/src/components/Legend/Legend.Component.tsx +16 -16
  36. package/src/components/Legend/Legend.Suppression.tsx +25 -20
  37. package/src/components/Legend/Legend.tsx +0 -2
  38. package/src/components/Legend/helpers/index.ts +16 -19
  39. package/src/components/LegendWrapper.tsx +3 -1
  40. package/src/components/LinearChart.tsx +740 -562
  41. package/src/components/PairedBarChart.jsx +50 -10
  42. package/src/components/PieChart/PieChart.tsx +1 -6
  43. package/src/components/Regions/components/Regions.tsx +33 -19
  44. package/src/components/ZoomBrush.tsx +25 -6
  45. package/src/coreStyles_chart.scss +3 -0
  46. package/src/data/initial-state.js +6 -2
  47. package/src/helpers/configHelpers.ts +28 -0
  48. package/src/helpers/handleRankByValue.ts +15 -0
  49. package/src/helpers/sizeHelpers.ts +25 -0
  50. package/src/helpers/tests/handleRankByValue.test.ts +37 -0
  51. package/src/helpers/tests/sizeHelpers.test.ts +80 -0
  52. package/src/hooks/useColorPalette.js +10 -2
  53. package/src/hooks/useLegendClasses.ts +4 -0
  54. package/src/hooks/useScales.ts +31 -3
  55. package/src/hooks/useTooltip.tsx +9 -5
  56. package/src/index.jsx +1 -0
  57. package/src/scss/DataTable.scss +5 -4
  58. package/src/scss/main.scss +57 -52
  59. package/src/types/ChartConfig.ts +38 -16
  60. package/src/types/ChartContext.ts +18 -14
  61. package/src/_stories/Chart.Legend.Gradient.tsx +0 -19
  62. package/src/_stories/ChartBrush.stories.tsx +0 -19
@@ -1,4 +1,4 @@
1
- import React, { useContext, useEffect, useRef, useState } from 'react'
1
+ import React, { forwardRef, useContext, useEffect, useMemo, useRef, useState } from 'react'
2
2
 
3
3
  // Libraries
4
4
  import { AxisLeft, AxisBottom, AxisRight, AxisTop } from '@visx/axis'
@@ -27,6 +27,9 @@ import CategoricalYAxis from './Axis/Categorical.Axis'
27
27
 
28
28
  // Helpers
29
29
  import { isConvertLineToBarGraph } from '../helpers/isConvertLineToBarGraph'
30
+ import { isLegendWrapViewport } from '@cdc/core/helpers/viewports'
31
+ import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
32
+ import { calcInitialHeight } from '../helpers/sizeHelpers'
30
33
 
31
34
  // Hooks
32
35
  import useMinMax from '../hooks/useMinMax'
@@ -37,97 +40,141 @@ import useTopAxis from '../hooks/useTopAxis'
37
40
  import { useTooltip as useCoveTooltip } from '../hooks/useTooltip'
38
41
  import { useEditorPermissions } from './EditorPanel/useEditorPermissions'
39
42
  import Annotation from './Annotations'
43
+ import { BlurStrokeText } from '@cdc/core/components/BlurStrokeText'
40
44
 
41
45
  type LinearChartProps = {
42
46
  parentWidth: number
43
47
  parentHeight: number
44
48
  }
45
49
 
46
- const LinearChart: React.FC<LinearChartProps> = props => {
50
+ const BOTTOM_LABEL_PADDING = 9
51
+ const X_TICK_LABEL_PADDING = 3
52
+ const DEFAULT_TICK_LENGTH = 8
53
+
54
+ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, parentWidth }, svgRef) => {
47
55
  // prettier-ignore
48
56
  const {
49
57
  brushConfig,
58
+ colorScale,
50
59
  config,
51
60
  currentViewport,
52
61
  dimensions,
53
62
  formatDate,
54
63
  formatNumber,
55
- getTextWidth,
56
64
  handleChartAriaLabels,
57
65
  handleLineType,
58
66
  handleDragStateChange,
67
+ isDraggingAnnotation,
68
+ legendRef,
59
69
  parseDate,
70
+ parentRef,
60
71
  tableData,
61
72
  transformedData: data,
62
73
  updateConfig,
63
- isDraggingAnnotation,
64
74
  seriesHighlight,
65
- colorScale
66
75
  } = useContext(ConfigContext)
67
76
 
77
+ // CONFIG
68
78
  // todo: start destructuring this file for conciseness
69
- const { visualizationType, visualizationSubType, orientation, xAxis, yAxis, runtime, debugSvg } = config
70
-
71
- const checkLineToBarGraph = () => {
72
- return isConvertLineToBarGraph(config.visualizationType, data, config.allowLineToBarGraph)
73
- }
74
-
75
- // configure width
76
- let [width] = dimensions
77
- if (
78
- config &&
79
- config.legend &&
80
- !config.legend.hide &&
81
- !['bottom', 'top'].includes(config.legend?.position) &&
82
- ['lg', 'md'].includes(currentViewport)
83
- ) {
84
- width = width * 0.73
85
- }
86
- // configure height , yMax, xMax
87
- const { horizontal: heightHorizontal, mobileVertical } = config.heights
88
- const isHorizontal = orientation === 'horizontal' || config.visualizationType === 'Forest Plot'
89
- const shouldAbbreviate = true
90
- const isLogarithmicAxis = config.yAxis.type === 'logarithmic'
91
- const xLabelOffset = isNaN(parseInt(runtime.xAxis.labelOffset)) ? 0 : parseInt(runtime.xAxis.labelOffset)
92
- const yLabelOffset = isNaN(parseInt(runtime.yAxis.labelOffset)) ? 0 : parseInt(runtime.yAxis.labelOffset)
93
- const xAxisSize = isNaN(parseInt(runtime.xAxis.size)) ? 0 : parseInt(runtime.xAxis.size)
94
- const isForestPlot = visualizationType === 'Forest Plot'
95
- const useVertical = orientation === 'vertical' || isForestPlot
96
- const useMobileVertical = mobileVertical && ['xs', 'xxs'].includes(currentViewport)
97
- const responsiveVertical = useMobileVertical ? 'mobileVertical' : 'vertical'
98
- const renderedOrientation = useVertical ? responsiveVertical : 'horizontal'
99
- let height = config.aspectRatio ? width * config.aspectRatio : config.heights[renderedOrientation]
100
- height = Number(height)
101
- const xMax = width - runtime.yAxis.size - (visualizationType === 'Combo' ? config.yAxis.rightAxisSize : 0)
102
- let yMax = height - (orientation === 'horizontal' ? 0 : xAxisSize)
103
- height += orientation === 'horizontal' ? xAxisSize : 0
104
-
105
- if (config.visualizationType === 'Forest Plot') {
106
- height = height + config.data.length * config.forestPlot.rowHeight
107
- yMax = yMax + config.data.length * config.forestPlot.rowHeight
108
- width = dimensions[0]
109
- }
110
- if (config.brush?.active) {
111
- height = height + config.brush?.height
112
- }
113
-
114
- // hooks % states
79
+ const {
80
+ heights,
81
+ visualizationType,
82
+ visualizationSubType,
83
+ orientation,
84
+ xAxis,
85
+ yAxis,
86
+ runtime,
87
+ legend,
88
+ forestPlot,
89
+ brush,
90
+ dataFormat,
91
+ debugSvg
92
+ } = config
93
+ const { suffix, onlyShowTopPrefixSuffix } = dataFormat
94
+ const { labelsAboveGridlines, hideAxis } = config.yAxis
95
+
96
+ // HOOKS % STATES
115
97
  const { minValue, maxValue, existPositiveValue, isAllLine } = useReduceData(config, data)
116
98
  const { visSupportsReactTooltip } = useEditorPermissions()
117
99
  const { hasTopAxis } = useTopAxis(config)
118
100
  const [animatedChart, setAnimatedChart] = useState(false)
119
101
  const [point, setPoint] = useState({ x: 0, y: 0 })
120
- const annotationRefs = useRef(null)
102
+ const [suffixWidth, setSuffixWidth] = useState(0)
121
103
 
122
- // refs
123
- const triggerRef = useRef()
104
+ // REFS
124
105
  const axisBottomRef = useRef(null)
125
- const svgRef = useRef()
106
+ const forestPlotRightLabelRef = useRef(null)
107
+ const suffixRef = useRef(null)
108
+ const topYLabelRef = useRef(null)
109
+ const triggerRef = useRef()
110
+ const xAxisLabelRefs = useRef([])
111
+ const xAxisTitleRef = useRef(null)
112
+ const prevTickRef = useRef(null)
113
+
126
114
  const dataRef = useIntersectionObserver(triggerRef, {
127
115
  freezeOnceVisible: false
128
116
  })
129
117
 
130
- // getters & functions
118
+ // VARS/MEMOS
119
+ const shouldAbbreviate = true
120
+ const isHorizontal = orientation === 'horizontal' || config.visualizationType === 'Forest Plot'
121
+ const isLogarithmicAxis = config.yAxis.type === 'logarithmic'
122
+ const isForestPlot = visualizationType === 'Forest Plot'
123
+ const suffixHasNoSpace = !suffix.includes(' ')
124
+
125
+ const yLabelOffset = isNaN(parseInt(`${runtime.yAxis.labelOffset}`)) ? 0 : parseInt(`${runtime.yAxis.labelOffset}`)
126
+
127
+ // zero if not forest plot
128
+ const forestRowsHeight = isForestPlot ? config.data.length * config.forestPlot.rowHeight : 0
129
+
130
+ // height before bottom axis
131
+ const initialHeight = useMemo(
132
+ () => calcInitialHeight(config, currentViewport),
133
+ [config, currentViewport, parentHeight]
134
+ )
135
+ const forestHeight = useMemo(() => initialHeight + forestRowsHeight, [initialHeight, forestRowsHeight])
136
+
137
+ // width
138
+ const width = useMemo(() => {
139
+ const initialWidth = dimensions[0]
140
+ const legendHidden = legend?.hide
141
+ const legendOnTopOrBottom = ['bottom', 'top'].includes(config.legend?.position)
142
+ const legendWrapped = isLegendWrapViewport(currentViewport)
143
+
144
+ const legendShowingLeftOrRight = !isForestPlot && !legendHidden && !legendOnTopOrBottom && !legendWrapped
145
+
146
+ if (!legendShowingLeftOrRight) return initialWidth
147
+
148
+ if (legendRef.current) {
149
+ const legendStyle = getComputedStyle(legendRef.current)
150
+ return (
151
+ initialWidth -
152
+ legendRef.current.getBoundingClientRect().width -
153
+ parseInt(legendStyle.marginLeft) -
154
+ parseInt(legendStyle.marginRight)
155
+ )
156
+ }
157
+
158
+ return initialWidth * 0.73
159
+ }, [dimensions[0], config.legend, currentViewport, legendRef.current])
160
+
161
+ // Used to calculate the y position of the x-axis title
162
+ const bottomLabelStart = useMemo(() => {
163
+ xAxisLabelRefs.current = xAxisLabelRefs.current?.filter(label => label)
164
+ if (!xAxisLabelRefs.current.length) return
165
+ const tallestLabel = Math.max(...xAxisLabelRefs.current.map(label => label.getBBox().height))
166
+ return tallestLabel + X_TICK_LABEL_PADDING + DEFAULT_TICK_LENGTH
167
+ }, [dimensions[0], config.xAxis, xAxisLabelRefs.current, config.xAxis.tickRotation])
168
+
169
+ // xMax and yMax
170
+ const xMax = width - runtime.yAxis.size - (visualizationType === 'Combo' ? config.yAxis.rightAxisSize : 0)
171
+ const yMax = initialHeight + forestRowsHeight
172
+
173
+ const checkLineToBarGraph = () => {
174
+ return isConvertLineToBarGraph(config.visualizationType, data, config.allowLineToBarGraph)
175
+ }
176
+
177
+ // GETTERS & FUNCTIONS
131
178
  const getXAxisData = d =>
132
179
  isDateScale(config.runtime.xAxis)
133
180
  ? parseDate(d[config.runtime.originalXAxis.dataKey]).getTime()
@@ -159,16 +206,10 @@ const LinearChart: React.FC<LinearChartProps> = props => {
159
206
  leftMax,
160
207
  rightMax,
161
208
  dimensions,
162
- xMax: props.parentWidth - Number(config.orientation === 'horizontal' ? config.xAxis.size : config.yAxis.size)
209
+ xMax: parentWidth - Number(config.orientation === 'horizontal' ? config.xAxis.size : config.yAxis.size)
163
210
  })
164
211
 
165
- // sets the portal x/y for where tooltips should appear on the page.
166
- const [chartPosition, setChartPosition] = useState(null)
167
- useEffect(() => {
168
- setChartPosition(svgRef?.current?.getBoundingClientRect())
169
- }, [svgRef, config.legend])
170
-
171
- const handleLeftTickFormatting = (tick, index) => {
212
+ const handleLeftTickFormatting = (tick, index, ticks) => {
172
213
  if (isLogarithmicAxis && tick === 0.1) {
173
214
  //when logarithmic scale applied change value of first tick
174
215
  tick = 0
@@ -178,8 +219,11 @@ const LinearChart: React.FC<LinearChartProps> = props => {
178
219
  if (config.visualizationType === 'Forest Plot') return config.data[index][config.xAxis.dataKey]
179
220
  if (isDateScale(runtime.yAxis)) return formatDate(parseDate(tick))
180
221
  if (orientation === 'vertical' && max - min < 3)
181
- return formatNumber(tick, 'left', shouldAbbreviate, false, false, '1')
182
- if (orientation === 'vertical') return formatNumber(tick, 'left', shouldAbbreviate)
222
+ return formatNumber(tick, 'left', shouldAbbreviate, false, false, '1', { index, length: ticks.length })
223
+ if (orientation === 'vertical') {
224
+ // TODO suggestion: pass all options as object key/values to allow for more flexibility
225
+ return formatNumber(tick, 'left', shouldAbbreviate, false, false, undefined, { index, length: ticks.length })
226
+ }
183
227
  return tick
184
228
  }
185
229
 
@@ -189,7 +233,11 @@ const LinearChart: React.FC<LinearChartProps> = props => {
189
233
  tick = 0
190
234
  }
191
235
 
192
- if (isDateScale(runtime.xAxis) && config.visualizationType !== 'Forest Plot') return formatDate(tick)
236
+ if (isDateScale(runtime.xAxis) && config.visualizationType !== 'Forest Plot') {
237
+ const formattedDate = formatDate(tick, prevTickRef.current)
238
+ prevTickRef.current = tick
239
+ return formattedDate
240
+ }
193
241
  if (orientation === 'horizontal' && config.visualizationType !== 'Forest Plot')
194
242
  return formatNumber(tick, 'left', shouldAbbreviate)
195
243
  if (config.xAxis.type === 'continuous' && config.visualizationType !== 'Forest Plot')
@@ -272,9 +320,7 @@ const LinearChart: React.FC<LinearChartProps> = props => {
272
320
  handleTooltipMouseOver,
273
321
  handleTooltipClick,
274
322
  handleTooltipMouseOff,
275
- tooltipStyles,
276
323
  TooltipListItem,
277
- getXValueFromCoordinateDate,
278
324
  getXValueFromCoordinate
279
325
  } = useCoveTooltip({
280
326
  xScale,
@@ -283,6 +329,8 @@ const LinearChart: React.FC<LinearChartProps> = props => {
283
329
  hideTooltip
284
330
  })
285
331
 
332
+ // EFFECTS
333
+
286
334
  // Make sure the chart is visible if in the editor
287
335
  /* eslint-disable react-hooks/exhaustive-deps */
288
336
  useEffect(() => {
@@ -303,10 +351,66 @@ const LinearChart: React.FC<LinearChartProps> = props => {
303
351
  }, [dataRef?.isIntersecting, config.animate])
304
352
 
305
353
  useEffect(() => {
306
- if (max) {
307
- updateConfig({ ...config, yAxis: { ...config.yAxis, maxValue: max } })
308
- }
309
- }, [max])
354
+ const suffixEl = suffixRef.current
355
+ if (!suffixEl && !suffixWidth) return
356
+ if (!suffixEl) return setSuffixWidth(0)
357
+ const suffixElWidth = suffixEl.getBBox().width
358
+ setSuffixWidth(suffixElWidth)
359
+ }, [config.dataFormat.suffix, config.dataFormat.onlyShowTopPrefixSuffix])
360
+
361
+ // forest plot x-axis label positioning
362
+ useEffect(() => {
363
+ if (!isForestPlot || xAxis.hideLabel) return
364
+
365
+ const rightLabel = forestPlotRightLabelRef.current
366
+
367
+ if (!rightLabel) return
368
+
369
+ const axisBottomY = yMax + Number(config.xAxis.axisPadding)
370
+ const labelRelativeY = rightLabel.getBBox().y - axisBottomY
371
+ const xLabelY = labelRelativeY + rightLabel.getBBox().height + BOTTOM_LABEL_PADDING
372
+ if (!xAxisTitleRef.current) return
373
+ xAxisTitleRef.current.setAttribute('y', xLabelY)
374
+ }, [config?.data?.length, forestRowsHeight])
375
+
376
+ // Parent height adjustments
377
+ useEffect(() => {
378
+ if (!axisBottomRef.current) return
379
+ const axisBottomHeight = axisBottomRef.current.getBBox().height
380
+
381
+ const isForestPlot = visualizationType === 'Forest Plot'
382
+ const topLabelOnGridline = topYLabelRef.current && yAxis.labelsAboveGridlines
383
+
384
+ // Heights to add
385
+ const brushHeight = brush?.active ? brush?.height : 0
386
+ const forestRowsHeight = isForestPlot ? config.data.length * forestPlot.rowHeight : 0
387
+ const topLabelOnGridlineHeight = topLabelOnGridline ? topYLabelRef.current.getBBox().height : 0
388
+ const additionalHeight = axisBottomHeight + brushHeight + forestRowsHeight + topLabelOnGridlineHeight
389
+ const newHeight = initialHeight + additionalHeight
390
+ if (!parentRef.current) return
391
+
392
+ parentRef.current.style.height = `${newHeight}px`
393
+
394
+ /* Adding text above the top gridline overflows the bounds of the svg.
395
+ To accommodate for this we need to...
396
+ 1. Add the extra height to the svg (done above)
397
+ 2. Adjust the viewBox to move the intended top height into focus
398
+ 3. if the legend is on the left or right, translate it by
399
+ the label height so it is aligned with the top border */
400
+ if (!topLabelOnGridlineHeight) return
401
+
402
+ // Adjust the viewBox for the svg
403
+ const svg = svgRef.current
404
+ if (!svg) return
405
+ const parentWidthFromRef = parentRef.current.getBoundingClientRect().width
406
+ svg.setAttribute('viewBox', `0 ${-topLabelOnGridlineHeight} ${parentWidthFromRef} ${newHeight}`)
407
+
408
+ // translate legend match viewBox-adjusted height
409
+ if (!legendRef.current) return
410
+ const legendIsLeftOrRight =
411
+ legend?.position !== 'top' && legend?.position !== 'bottom' && !isLegendWrapViewport(currentViewport)
412
+ legendRef.current.style.transform = legendIsLeftOrRight ? `translateY(${topLabelOnGridlineHeight}px)` : 'none'
413
+ }, [axisBottomRef.current, config, bottomLabelStart, brush, currentViewport, topYLabelRef.current])
310
414
 
311
415
  const chartHasTooltipGuides = () => {
312
416
  const { visualizationType } = config
@@ -346,7 +450,7 @@ const LinearChart: React.FC<LinearChartProps> = props => {
346
450
  }
347
451
 
348
452
  const generatePairedBarAxis = () => {
349
- let axisMaxHeight = 40
453
+ const axisMaxHeight = bottomLabelStart + BOTTOM_LABEL_PADDING
350
454
 
351
455
  const getTickPositions = (ticks, xScale) => {
352
456
  if (!ticks.length) return false
@@ -401,16 +505,14 @@ const LinearChart: React.FC<LinearChartProps> = props => {
401
505
  const isResponsiveTicks = config.isResponsiveTicks && isTicksOverlapping
402
506
  const angle =
403
507
  tick.index !== 0 && (isResponsiveTicks ? maxTickRotation : Number(config.yAxis.tickRotation))
404
- const axisHeight = textWidth * Math.sin(angle * (Math.PI / 180)) + 25
405
508
  const textAnchor = angle && tick.index !== 0 ? 'end' : 'middle'
406
509
 
407
- if (axisHeight > axisMaxHeight) axisMaxHeight = axisHeight
408
-
409
510
  return (
410
511
  <Group key={`vx-tick-${tick.value}-${i}`} className={'vx-axis-tick'}>
411
512
  {!runtime.yAxis.hideTicks && <Line from={tick.from} to={tick.to} stroke='#333' />}
412
513
  {!runtime.yAxis.hideLabel && (
413
514
  <Text // prettier-ignore
515
+ innerRef={el => (xAxisLabelRefs.current[i] = el)}
414
516
  x={tick.to.x}
415
517
  y={tick.to.y}
416
518
  angle={-angle}
@@ -429,6 +531,7 @@ const LinearChart: React.FC<LinearChartProps> = props => {
429
531
  }}
430
532
  </AxisBottom>
431
533
  <AxisBottom
534
+ innerRef={axisBottomRef}
432
535
  top={yMax}
433
536
  left={Number(runtime.yAxis.size)}
434
537
  label={runtime.xAxis.label}
@@ -454,18 +557,15 @@ const LinearChart: React.FC<LinearChartProps> = props => {
454
557
  const isResponsiveTicks = config.isResponsiveTicks && isTicksOverlapping
455
558
  const angle =
456
559
  tick.index !== 0 && (isResponsiveTicks ? maxTickRotation : Number(config.yAxis.tickRotation))
457
- const axisHeight = textWidth * Math.sin(angle * (Math.PI / 180)) + 25
458
560
  const textAnchor = angle && tick.index !== 0 ? 'end' : 'middle'
459
-
460
- if (axisHeight > axisMaxHeight) axisMaxHeight = axisHeight
461
-
561
+ if (!i) return <></> // skip first tick to avoid overlapping 0's
462
562
  return (
463
563
  <Group key={`vx-tick-${tick.value}-${i}`} className={'vx-axis-tick'}>
464
564
  {!runtime.yAxis.hideTicks && <Line from={tick.from} to={tick.to} stroke='#333' />}
465
565
  {!runtime.yAxis.hideLabel && (
466
566
  <Text // prettier-ignore
467
567
  x={tick.to.x}
468
- y={tick.to.y}
568
+ y={tick.to.y + X_TICK_LABEL_PADDING}
469
569
  angle={-angle}
470
570
  verticalAnchor={angle ? 'middle' : 'start'}
471
571
  textAnchor={textAnchor}
@@ -480,8 +580,9 @@ const LinearChart: React.FC<LinearChartProps> = props => {
480
580
  </Group>
481
581
  <Group>
482
582
  <Text
583
+ className='x-axis-title-label'
483
584
  x={xMax / 2}
484
- y={axisMaxHeight + 20 + xLabelOffset}
585
+ y={axisMaxHeight}
485
586
  stroke='#333'
486
587
  textAnchor={'middle'}
487
588
  verticalAnchor='start'
@@ -489,12 +590,6 @@ const LinearChart: React.FC<LinearChartProps> = props => {
489
590
  {runtime.xAxis.label}
490
591
  </Text>
491
592
  </Group>
492
- {svgRef.current
493
- ? svgRef.current.setAttribute(
494
- 'height',
495
- Number(height) + Number(axisMaxHeight) + (runtime.xAxis.label ? 50 : 0) + 'px'
496
- )
497
- : ''}
498
593
  </>
499
594
  )
500
595
  }}
@@ -508,32 +603,29 @@ const LinearChart: React.FC<LinearChartProps> = props => {
508
603
  ) : (
509
604
  <ErrorBoundary component='LinearChart'>
510
605
  {/* ! Notice - div needed for tooltip boundaries (flip/flop) */}
511
- <div style={{ width: `${props.parentWidth}px`, overflow: 'visible' }} className='tooltip-boundary'>
606
+ <div
607
+ style={{ width: `${parentWidth}px`, overflow: 'visible', position: 'relative' }}
608
+ className='tooltip-boundary'
609
+ >
512
610
  <svg
611
+ ref={svgRef}
513
612
  onMouseMove={onMouseMove}
514
- width={props.parentWidth}
515
- height={props.parentHeight}
613
+ width={parentWidth}
614
+ height={parentHeight}
516
615
  className={`linear ${config.animate ? 'animated' : ''} ${animatedChart && config.animate ? 'animate' : ''} ${
517
616
  debugSvg && 'debug'
518
617
  } ${isDraggingAnnotation && 'dragging-annotation'}`}
519
618
  role='img'
520
619
  aria-label={handleChartAriaLabels(config)}
521
- ref={svgRef}
522
620
  style={{ overflow: 'visible' }}
523
621
  >
524
- {!isDraggingAnnotation && (
525
- <Bar width={props.parentWidth} height={props.parentHeight} fill={'transparent'}></Bar>
526
- )}{' '}
527
- {/* Highlighted regions */}
528
- {/* Y axis */}
622
+ {!isDraggingAnnotation && <Bar width={parentWidth} height={initialHeight} fill={'transparent'}></Bar>}{' '}
623
+ {/* GRID LINES */}
624
+ {/* Actual AxisLeft is drawn after visualization */}
529
625
  {!['Spark Line', 'Forest Plot'].includes(visualizationType) && config.yAxis.type !== 'categorical' && (
530
626
  <AxisLeft
531
627
  scale={yScale}
532
- tickLength={isLogarithmicAxis ? 6 : 8}
533
628
  left={Number(runtime.yAxis.size) - config.yAxis.axisPadding}
534
- label={runtime.yAxis.label || runtime.yAxis.label}
535
- stroke='#333'
536
- tickFormat={(tick, i) => handleLeftTickFormatting(tick, i)}
537
629
  numTicks={handleNumTicks()}
538
630
  >
539
631
  {props => {
@@ -541,185 +633,28 @@ const LinearChart: React.FC<LinearChartProps> = props => {
541
633
  config.orientation === 'horizontal'
542
634
  ? (props.axisToPoint.y - props.axisFromPoint.y) / 2
543
635
  : (props.axisFromPoint.y - props.axisToPoint.y) / 2
544
- const horizontalTickOffset =
545
- yMax / props.ticks.length / 2 - (yMax / props.ticks.length) * (1 - config.barThickness) + 5
546
636
  return (
547
637
  <Group className='left-axis'>
548
638
  {props.ticks.map((tick, i) => {
549
- const minY = props.ticks[0].to.y
550
- const barMinHeight = 15 // 15 is the min height for bars by default
551
639
  const showTicks = String(tick.value).startsWith('1') || tick.value === 0.1 ? 'block' : 'none'
552
- const tickLength = showTicks === 'block' ? 7 : 0
553
- const to = { x: tick.to.x - tickLength, y: tick.to.y }
554
640
  const hideFirstGridLine = tick.index === 0 && tick.value === 0 && config.xAxis.hideAxis
555
641
 
556
642
  return (
557
643
  <Group key={`vx-tick-${tick.value}-${i}`} className={'vx-axis-tick'}>
558
- {!runtime.yAxis.hideTicks && (
559
- <Line
560
- key={`${tick.value}--hide-hideTicks`}
561
- from={tick.from}
562
- to={isLogarithmicAxis ? to : tick.to}
563
- stroke={config.yAxis.tickColor}
564
- display={orientation === 'horizontal' ? 'none' : 'block'}
565
- />
566
- )}
567
-
568
644
  {runtime.yAxis.gridLines && !hideFirstGridLine ? (
569
645
  <Line
570
646
  key={`${tick.value}--hide-hideGridLines`}
571
647
  display={(isLogarithmicAxis && showTicks).toString()}
572
648
  from={{ x: tick.from.x + xMax, y: tick.from.y }}
573
649
  to={tick.from}
574
- stroke='rgba(0,0,0,0.3)'
650
+ stroke='#d6d6d6'
575
651
  />
576
652
  ) : (
577
653
  ''
578
654
  )}
579
-
580
- {orientation === 'horizontal' &&
581
- visualizationSubType !== 'stacked' &&
582
- config.yAxis.labelPlacement === 'On Date/Category Axis' &&
583
- !config.yAxis.hideLabel && (
584
- <Text
585
- transform={`translate(${tick.to.x - 5}, ${
586
- config.isLollipopChart
587
- ? tick.to.y - minY
588
- : tick.to.y -
589
- minY +
590
- (Number(config.barHeight * config.runtime.series.length) - barMinHeight) / 2
591
- }) rotate(-${config.runtime.horizontal ? config.runtime.yAxis.tickRotation || 0 : 0})`}
592
- verticalAnchor={'start'}
593
- textAnchor={'end'}
594
- >
595
- {tick.formattedValue}
596
- </Text>
597
- )}
598
-
599
- {orientation === 'horizontal' &&
600
- visualizationSubType === 'stacked' &&
601
- config.yAxis.labelPlacement === 'On Date/Category Axis' &&
602
- !config.yAxis.hideLabel && (
603
- <Text
604
- transform={`translate(${tick.to.x - 5}, ${
605
- tick.to.y - minY + (Number(config.barHeight) - barMinHeight) / 2
606
- }) rotate(-${runtime.horizontal ? runtime.yAxis.tickRotation : 0})`}
607
- verticalAnchor={'start'}
608
- textAnchor={'end'}
609
- >
610
- {tick.formattedValue}
611
- </Text>
612
- )}
613
-
614
- {orientation === 'horizontal' &&
615
- visualizationType === 'Paired Bar' &&
616
- !config.yAxis.hideLabel && (
617
- <Text
618
- transform={`translate(${tick.to.x - 5}, ${
619
- tick.to.y - minY + Number(config.barHeight) / 2
620
- }) rotate(-${runtime.horizontal ? runtime.yAxis.tickRotation : 0})`}
621
- textAnchor={'end'}
622
- verticalAnchor='middle'
623
- >
624
- {tick.formattedValue}
625
- </Text>
626
- )}
627
- {orientation === 'horizontal' &&
628
- visualizationType === 'Deviation Bar' &&
629
- !config.yAxis.hideLabel && (
630
- <Text
631
- transform={`translate(${tick.to.x - 5}, ${
632
- config.isLollipopChart
633
- ? tick.to.y - minY + 2
634
- : tick.to.y - minY + Number(config.barHeight) / 2
635
- }) rotate(-${runtime.horizontal ? runtime.yAxis.tickRotation : 0})`}
636
- textAnchor={'end'}
637
- verticalAnchor='middle'
638
- >
639
- {tick.formattedValue}
640
- </Text>
641
- )}
642
-
643
- {orientation === 'vertical' &&
644
- visualizationType === 'Bump Chart' &&
645
- !config.yAxis.hideLabel && (
646
- <>
647
- <Text
648
- display={config.useLogScale ? showTicks : 'block'}
649
- dx={config.useLogScale ? -6 : 0}
650
- x={config.runtime.horizontal ? tick.from.x + 2 : tick.to.x - 8.5}
651
- y={tick.to.y - 13 + (config.runtime.horizontal ? horizontalTickOffset : 0)}
652
- angle={-Number(config.yAxis.tickRotation) || 0}
653
- verticalAnchor={config.runtime.horizontal ? 'start' : 'middle'}
654
- textAnchor={config.runtime.horizontal ? 'start' : 'end'}
655
- fill={config.yAxis.tickLabelColor}
656
- >
657
- {config.runtime.seriesLabelsAll[tick.formattedValue - 1]}
658
- </Text>
659
-
660
- {(seriesHighlight.length === 0 ||
661
- seriesHighlight.includes(
662
- config.runtime.seriesLabelsAll[tick.formattedValue - 1]
663
- )) && (
664
- <rect
665
- x={0 - Number(config.yAxis.size)}
666
- y={tick.to.y - 8 + (config.runtime.horizontal ? horizontalTickOffset : 7)}
667
- width={Number(config.yAxis.size) + xScale(xScale.domain()[0])}
668
- height='2'
669
- fill={colorScale(config.runtime.seriesLabelsAll[tick.formattedValue - 1])}
670
- />
671
- )}
672
- </>
673
- )}
674
- {orientation === 'vertical' &&
675
- visualizationType !== 'Paired Bar' &&
676
- visualizationType !== 'Bump Chart' &&
677
- !config.yAxis.hideLabel && (
678
- <Text
679
- display={isLogarithmicAxis ? showTicks : 'block'}
680
- dx={isLogarithmicAxis ? -6 : 0}
681
- x={config.runtime.horizontal ? tick.from.x + 2 : tick.to.x}
682
- y={tick.to.y + (config.runtime.horizontal ? horizontalTickOffset : 0)}
683
- angle={-Number(config.yAxis.tickRotation) || 0}
684
- verticalAnchor={config.runtime.horizontal ? 'start' : 'middle'}
685
- textAnchor={config.runtime.horizontal ? 'start' : 'end'}
686
- fill={config.yAxis.tickLabelColor}
687
- >
688
- {tick.formattedValue}
689
- </Text>
690
- )}
691
655
  </Group>
692
656
  )
693
657
  })}
694
- {!config.yAxis.hideAxis && (
695
- <Line
696
- from={props.axisFromPoint}
697
- to={
698
- runtime.horizontal
699
- ? {
700
- x: 0,
701
- y: config.visualizationType === 'Forest Plot' ? height : Number(heightHorizontal)
702
- }
703
- : props.axisToPoint
704
- }
705
- stroke='#000'
706
- />
707
- )}
708
- {yScale.domain()[0] < 0 && (
709
- <Line
710
- from={{ x: props.axisFromPoint.x, y: yScale(0) }}
711
- to={{ x: xMax, y: yScale(0) }}
712
- stroke='#333'
713
- />
714
- )}
715
- {visualizationType === 'Bar' && orientation === 'horizontal' && xScale.domain()[0] < 0 && (
716
- <Line
717
- from={{ x: xScale(0), y: 0 }}
718
- to={{ x: xScale(0), y: yMax }}
719
- stroke='#333'
720
- strokeWidth={2}
721
- />
722
- )}
723
658
  <Text
724
659
  className='y-label'
725
660
  textAnchor='middle'
@@ -735,287 +670,24 @@ const LinearChart: React.FC<LinearChartProps> = props => {
735
670
  }}
736
671
  </AxisLeft>
737
672
  )}
738
- {config.yAxis.type === 'categorical' && config.orientation === 'vertical' && (
739
- <CategoricalYAxis
740
- max={max}
741
- maxValue={maxValue}
742
- height={height}
673
+ {visualizationType === 'Paired Bar' && generatePairedBarAxis()}
674
+ {visualizationType === 'Deviation Bar' && config.runtime.series?.length === 1 && (
675
+ <DeviationBar animatedChart={animatedChart} xScale={xScale} yScale={yScale} width={xMax} height={yMax} />
676
+ )}
677
+ {visualizationType === 'Paired Bar' && <PairedBarChart originalWidth={width} width={xMax} height={yMax} />}
678
+ {visualizationType === 'Scatter Plot' && (
679
+ <ScatterPlot
680
+ xScale={xScale}
681
+ yScale={yScale}
682
+ getXAxisData={getXAxisData}
683
+ getYAxisData={getYAxisData}
743
684
  xMax={xMax}
744
685
  yMax={yMax}
745
- leftSize={Number(runtime.yAxis.size) - config.yAxis.axisPadding}
746
- />
747
- )}
748
- {/* Right Axis */}
749
- {hasRightAxis && (
750
- <AxisRight
751
- scale={yScaleRight}
752
- left={Number(width - config.yAxis.rightAxisSize)}
753
- label={config.yAxis.rightLabel}
754
- tickFormat={tick => formatNumber(tick, 'right')}
755
- numTicks={runtime.yAxis.rightNumTicks || undefined}
756
- labelOffset={45}
757
- >
758
- {props => {
759
- const axisCenter =
760
- config.orientation === 'horizontal'
761
- ? (props.axisToPoint.y - props.axisFromPoint.y) / 2
762
- : (props.axisFromPoint.y - props.axisToPoint.y) / 2
763
- const horizontalTickOffset =
764
- yMax / props.ticks.length / 2 - (yMax / props.ticks.length) * (1 - config.barThickness) + 5
765
- return (
766
- <Group className='right-axis'>
767
- {props.ticks.map((tick, i) => {
768
- return (
769
- <Group key={`vx-tick-${tick.value}-${i}`} className='vx-axis-tick'>
770
- {!runtime.yAxis.rightHideTicks && (
771
- <Line
772
- from={tick.from}
773
- to={tick.to}
774
- display={runtime.horizontal ? 'none' : 'block'}
775
- stroke={config.yAxis.rightAxisTickColor}
776
- />
777
- )}
778
-
779
- {runtime.yAxis.rightGridLines ? (
780
- <Line
781
- from={{ x: tick.from.x + xMax, y: tick.from.y }}
782
- to={tick.from}
783
- stroke='rgba(0,0,0,0.3)'
784
- />
785
- ) : (
786
- ''
787
- )}
788
-
789
- {!config.yAxis.rightHideLabel && (
790
- <Text
791
- x={tick.to.x}
792
- y={tick.to.y + (runtime.horizontal ? horizontalTickOffset : 0)}
793
- verticalAnchor={runtime.horizontal ? 'start' : 'middle'}
794
- textAnchor={'start'}
795
- fill={config.yAxis.rightAxisTickLabelColor}
796
- >
797
- {tick.formattedValue}
798
- </Text>
799
- )}
800
- </Group>
801
- )
802
- })}
803
- {!config.yAxis.rightHideAxis && (
804
- <Line from={props.axisFromPoint} to={props.axisToPoint} stroke='#333' />
805
- )}
806
- <Text
807
- className='y-label'
808
- textAnchor='middle'
809
- verticalAnchor='start'
810
- transform={`translate(${
811
- config.yAxis.rightLabelOffsetSize ? config.yAxis.rightLabelOffsetSize : 0
812
- }, ${axisCenter}) rotate(-90)`}
813
- fontWeight='bold'
814
- fill={config.yAxis.rightAxisLabelColor}
815
- >
816
- {props.label}
817
- </Text>
818
- </Group>
819
- )
820
- }}
821
- </AxisRight>
822
- )}
823
- {hasTopAxis && config.topAxis.hasLine && (
824
- <AxisTop
825
- stroke='#333'
826
- left={Number(runtime.yAxis.size)}
827
- scale={xScale}
828
- hideTicks
829
- hideZero
830
- tickLabelProps={() => ({
831
- fill: 'transparent'
832
- })}
833
- />
834
- )}
835
- {/* X axis */}
836
- {visualizationType !== 'Paired Bar' && visualizationType !== 'Spark Line' && (
837
- <AxisBottom
838
- innerRef={axisBottomRef}
839
- top={
840
- runtime.horizontal && config.visualizationType !== 'Forest Plot'
841
- ? Number(heightHorizontal) + Number(config.xAxis.axisPadding)
842
- : config.visualizationType === 'Forest Plot'
843
- ? yMax + Number(config.xAxis.axisPadding)
844
- : yMax
845
- }
846
- left={config.visualizationType !== 'Forest Plot' ? Number(runtime.yAxis.size) : 0}
847
- label={config[section].label}
848
- tickFormat={handleBottomTickFormatting}
849
- scale={xScale}
850
- stroke='#333'
851
- numTicks={countNumOfTicks('xAxis')}
852
- tickStroke='#333'
853
- tickValues={
854
- config.xAxis.manual
855
- ? getTickValues(
856
- xAxisDataMapped,
857
- xScale,
858
- config.xAxis.type === 'date-time' ? countNumOfTicks('xAxis') : getManualStep()
859
- )
860
- : undefined
861
- }
862
- >
863
- {props => {
864
- const axisCenter =
865
- config.visualizationType !== 'Forest Plot'
866
- ? (props.axisToPoint.x - props.axisFromPoint.x) / 2
867
- : dimensions[0] / 2
868
- const containsMultipleWords = inputString => /\s/.test(inputString)
869
- const ismultiLabel = props.ticks.some(tick => containsMultipleWords(tick.value))
870
-
871
- // Calculate sumOfTickWidth here, before map function
872
- const defaultTickLength = 8
873
- const tickWidthMax = Math.max(
874
- ...props.ticks.map(tick =>
875
- getTextWidth(tick.formattedValue, `normal ${fontSize[config.fontSize]}px sans-serif`)
876
- )
877
- )
878
- // const marginTop = 20 // moved to top bc need for yMax calcs
879
- const accumulator = ismultiLabel ? 180 : 100
880
-
881
- const textWidths = props.ticks.map(tick =>
882
- getTextWidth(tick.formattedValue, `normal ${fontSize[config.fontSize]}px sans-serif`)
883
- )
884
- const sumOfTickWidth = textWidths.reduce((a, b) => a + b, accumulator)
885
- const spaceBetweenEachTick = (xMax - sumOfTickWidth) / (props.ticks.length - 1)
886
-
887
- // Check if ticks are overlapping
888
- // Determine the position of each tick
889
- let positions = [0] // The first tick is at position 0
890
- for (let i = 1; i < textWidths.length; i++) {
891
- // The position of each subsequent tick is the position of the previous tick
892
- // plus the width of the previous tick and the space
893
- positions[i] = positions[i - 1] + textWidths[i - 1] + spaceBetweenEachTick
894
- }
895
- // calculate the end of x axis box
896
- const axisBBox = axisBottomRef?.current?.getBBox().height
897
- config.xAxis.axisBBox = axisBBox
898
-
899
- // Check if ticks are overlapping
900
- let areTicksTouching = false
901
- textWidths.forEach((_, i) => {
902
- if (positions[i] + textWidths[i] > positions[i + 1]) {
903
- areTicksTouching = true
904
- return
905
- }
906
- })
907
-
908
- const dynamicMarginTop =
909
- areTicksTouching && config.isResponsiveTicks ? tickWidthMax + defaultTickLength + 20 : 0
910
- const rotation = Number(config.xAxis.tickRotation) > 0 ? Number(config.xAxis.tickRotation) : 0
911
-
912
- config.dynamicMarginTop = dynamicMarginTop
913
- config.xAxis.tickWidthMax = tickWidthMax
914
-
915
- let axisMaxHeight = 40
916
-
917
- const axisContents = (
918
- <Group className='bottom-axis' width={dimensions[0]}>
919
- {props.ticks.map((tick, i, propsTicks) => {
920
- // when using LogScale show major ticks values only
921
- const showTick = String(tick.value).startsWith('1') || tick.value === 0.1 ? 'block' : 'none'
922
- const tickLength = showTick === 'block' ? 16 : defaultTickLength
923
- const to = { x: tick.to.x, y: tickLength }
924
- const textWidth = getTextWidth(
925
- tick.formattedValue,
926
- `normal ${fontSize[config.fontSize]}px sans-serif`
927
- )
928
- const limitedWidth = 100 / propsTicks.length
929
- //reset rotations by updating config
930
- config.yAxis.tickRotation =
931
- config.isResponsiveTicks && config.orientation === 'horizontal' ? 0 : config.yAxis.tickRotation
932
- config.xAxis.tickRotation =
933
- config.isResponsiveTicks && config.orientation === 'vertical' ? 0 : config.xAxis.tickRotation
934
- //configure rotation
935
-
936
- const tickRotation =
937
- config.isResponsiveTicks && areTicksTouching
938
- ? -Number(config.xAxis.maxTickRotation) || -90
939
- : -Number(config.runtime.xAxis.tickRotation)
940
-
941
- const axisHeight = textWidth * Math.sin(tickRotation * -1 * (Math.PI / 180)) + 25
942
-
943
- if (axisHeight > axisMaxHeight) axisMaxHeight = axisHeight
944
-
945
- return (
946
- <Group key={`vx-tick-${tick.value}-${i}`} className={'vx-axis-tick'}>
947
- {!config.xAxis.hideTicks && (
948
- <Line
949
- from={tick.from}
950
- to={orientation === 'horizontal' && isLogarithmicAxis ? to : tick.to}
951
- stroke={config.xAxis.tickColor}
952
- strokeWidth={showTick === 'block' && isLogarithmicAxis ? 1.3 : 1}
953
- />
954
- )}
955
- {!config.xAxis.hideLabel && (
956
- <Text
957
- dy={config.orientation === 'horizontal' && isLogarithmicAxis ? 8 : 0}
958
- display={config.orientation === 'horizontal' && isLogarithmicAxis ? showTick : 'block'}
959
- x={tick.to.x}
960
- y={tick.to.y}
961
- angle={tickRotation}
962
- verticalAnchor={tickRotation < -50 ? 'middle' : 'start'}
963
- textAnchor={tickRotation ? 'end' : 'middle'}
964
- width={
965
- areTicksTouching && !config.isResponsiveTicks && !Number(config[section].tickRotation)
966
- ? limitedWidth
967
- : undefined
968
- }
969
- fill={config.xAxis.tickLabelColor}
970
- >
971
- {tick.formattedValue}
972
- </Text>
973
- )}
974
- </Group>
975
- )
976
- })}
977
- {!config.xAxis.hideAxis && <Line from={props.axisFromPoint} to={props.axisToPoint} stroke='#333' />}
978
- <Text
979
- x={axisCenter}
980
- y={axisMaxHeight + 20 + xLabelOffset}
981
- textAnchor='middle'
982
- verticalAnchor='start'
983
- fontWeight='bold'
984
- fill={config.xAxis.labelColor}
985
- >
986
- {props.label}
987
- </Text>
988
- </Group>
989
- )
990
-
991
- if (svgRef.current)
992
- svgRef.current.setAttribute(
993
- 'height',
994
- Number(height) + Number(axisMaxHeight) + (runtime.xAxis.label ? 50 : 0) + 'px'
995
- )
996
-
997
- return axisContents
998
- }}
999
- </AxisBottom>
1000
- )}
1001
- {visualizationType === 'Paired Bar' && generatePairedBarAxis()}
1002
- {visualizationType === 'Deviation Bar' && config.runtime.series?.length === 1 && (
1003
- <DeviationBar animatedChart={animatedChart} xScale={xScale} yScale={yScale} width={xMax} height={yMax} />
1004
- )}
1005
- {visualizationType === 'Paired Bar' && <PairedBarChart originalWidth={width} width={xMax} height={yMax} />}
1006
- {visualizationType === 'Scatter Plot' && (
1007
- <ScatterPlot
1008
- xScale={xScale}
1009
- yScale={yScale}
1010
- getXAxisData={getXAxisData}
1011
- getYAxisData={getYAxisData}
1012
- xMax={xMax}
1013
- yMax={yMax}
1014
- handleTooltipMouseOver={handleTooltipMouseOver}
1015
- handleTooltipMouseOff={handleTooltipMouseOff}
1016
- handleTooltipClick={handleTooltipClick}
1017
- tooltipData={tooltipData}
1018
- showTooltip={showTooltip}
686
+ handleTooltipMouseOver={handleTooltipMouseOver}
687
+ handleTooltipMouseOff={handleTooltipMouseOff}
688
+ handleTooltipClick={handleTooltipClick}
689
+ tooltipData={tooltipData}
690
+ showTooltip={showTooltip}
1019
691
  />
1020
692
  )}
1021
693
  {visualizationType === 'Box Plot' && <BoxPlot xScale={xScale} yScale={yScale} />}
@@ -1126,7 +798,7 @@ const LinearChart: React.FC<LinearChartProps> = props => {
1126
798
  yScale={yScale}
1127
799
  seriesScale={seriesScale}
1128
800
  width={width}
1129
- height={height}
801
+ height={forestHeight}
1130
802
  getXAxisData={getXAxisData}
1131
803
  getYAxisData={getYAxisData}
1132
804
  animatedChart={animatedChart}
@@ -1138,11 +810,19 @@ const LinearChart: React.FC<LinearChartProps> = props => {
1138
810
  showTooltip={showTooltip}
1139
811
  chartRef={svgRef}
1140
812
  config={config}
813
+ forestPlotRightLabelRef={forestPlotRightLabelRef}
1141
814
  />
1142
815
  )}
1143
816
  {/*Brush chart */}
1144
817
  {config.brush.active && config.xAxis.type !== 'categorical' && (
1145
- <BrushChart yScale={yScale} xMax={xMax} yMax={yMax} xScale={xScale} seriesScale={seriesScale} />
818
+ <BrushChart
819
+ xScaleBrush={xScaleBrush}
820
+ yScale={yScale}
821
+ xMax={xMax}
822
+ yMax={yMax}
823
+ xScale={xScale}
824
+ seriesScale={seriesScale}
825
+ />
1146
826
  )}
1147
827
  {/* Line chart */}
1148
828
  {/* TODO: Make this just line or combo? */}
@@ -1262,7 +942,7 @@ const LinearChart: React.FC<LinearChartProps> = props => {
1262
942
  {config.filters && config.filters.values.length === 0 && data.length === 0 && (
1263
943
  <Text
1264
944
  x={Number(config.yAxis.size) + Number(xMax / 2)}
1265
- y={height / 2 - config.xAxis.padding / 2}
945
+ y={initialHeight / 2 - (config.xAxis.padding || 0) / 2}
1266
946
  textAnchor='middle'
1267
947
  >
1268
948
  {config.chartMessage.noData}
@@ -1312,25 +992,523 @@ const LinearChart: React.FC<LinearChartProps> = props => {
1312
992
  onDragStateChange={handleDragStateChange}
1313
993
  />
1314
994
  </Group>
1315
- </svg>
1316
- {!isDraggingAnnotation &&
1317
- tooltipData &&
1318
- Object.entries(tooltipData.data).length > 0 &&
1319
- tooltipOpen &&
1320
- showTooltip &&
1321
- tooltipData.dataYPosition &&
1322
- tooltipData.dataXPosition && (
1323
- <>
1324
- <style>{`.tooltip {background-color: rgba(255,255,255, ${
1325
- config.tooltips.opacity / 100
1326
- }) !important;`}</style>
1327
- <style>{`.tooltip {max-width:300px} !important; word-wrap: break-word; `}</style>
1328
- <TooltipWithBounds
1329
- key={Math.random()}
1330
- className={'tooltip cdc-open-viz-module'}
1331
- left={tooltipLeft}
1332
- top={tooltipTop}
1333
- >
995
+ {/* Highlighted regions */}
996
+ {/* Y axis */}
997
+ {!['Spark Line', 'Forest Plot'].includes(visualizationType) && config.yAxis.type !== 'categorical' && (
998
+ <AxisLeft
999
+ scale={yScale}
1000
+ tickLength={isLogarithmicAxis ? 6 : 8}
1001
+ left={Number(runtime.yAxis.size) - config.yAxis.axisPadding}
1002
+ label={runtime.yAxis.label || runtime.yAxis.label}
1003
+ stroke='#333'
1004
+ tickFormat={handleLeftTickFormatting}
1005
+ numTicks={handleNumTicks()}
1006
+ >
1007
+ {props => {
1008
+ const axisCenter =
1009
+ config.orientation === 'horizontal'
1010
+ ? (props.axisToPoint.y - props.axisFromPoint.y) / 2
1011
+ : (props.axisFromPoint.y - props.axisToPoint.y) / 2
1012
+ const horizontalTickOffset =
1013
+ yMax / props.ticks.length / 2 - (yMax / props.ticks.length) * (1 - config.barThickness) + 5
1014
+ return (
1015
+ <Group className='left-axis'>
1016
+ {!config.yAxis.hideAxis && (
1017
+ <Line
1018
+ from={props.axisFromPoint}
1019
+ to={
1020
+ runtime.horizontal
1021
+ ? {
1022
+ x: 0,
1023
+ y:
1024
+ config.visualizationType === 'Forest Plot' ? parentHeight : Number(heights.horizontal)
1025
+ }
1026
+ : props.axisToPoint
1027
+ }
1028
+ stroke='#000'
1029
+ />
1030
+ )}
1031
+ {yScale.domain()[0] < 0 && (
1032
+ <Line
1033
+ from={{ x: props.axisFromPoint.x, y: yScale(0) }}
1034
+ to={{ x: xMax, y: yScale(0) }}
1035
+ stroke='#333'
1036
+ />
1037
+ )}
1038
+ {visualizationType === 'Bar' && orientation === 'horizontal' && xScale.domain()[0] < 0 && (
1039
+ <Line
1040
+ from={{ x: xScale(0), y: 0 }}
1041
+ to={{ x: xScale(0), y: yMax }}
1042
+ stroke='#333'
1043
+ strokeWidth={2}
1044
+ />
1045
+ )}
1046
+ {props.ticks.map((tick, i) => {
1047
+ const minY = props.ticks[0].to.y
1048
+ const barMinHeight = 15 // 15 is the min height for bars by default
1049
+ const showTicks = String(tick.value).startsWith('1') || tick.value === 0.1 ? 'block' : 'none'
1050
+ const tickLength = showTicks === 'block' ? 7 : 0
1051
+ const to = { x: tick.to.x - tickLength, y: tick.to.y }
1052
+
1053
+ // Vertical value/suffix vars
1054
+ const lastTick = props.ticks.length - 1 === i
1055
+ const hideTopTick = lastTick && onlyShowTopPrefixSuffix && suffix && !suffixHasNoSpace
1056
+ const valueOnLinePadding = hideAxis ? -8 : -12
1057
+ const labelXPadding = labelsAboveGridlines ? valueOnLinePadding : 2
1058
+ const labelYPadding = labelsAboveGridlines ? 4 : 0
1059
+ const labelX = tick.to.x - labelXPadding
1060
+ const labelY = tick.to.y - labelYPadding
1061
+ const labelVerticalAnchor = labelsAboveGridlines ? 'end' : 'middle'
1062
+ const combineDomSuffixWithValue =
1063
+ onlyShowTopPrefixSuffix && labelsAboveGridlines && suffix && lastTick
1064
+
1065
+ return (
1066
+ <Group key={`vx-tick-${tick.value}-${i}`} className={'vx-axis-tick'}>
1067
+ {!runtime.yAxis.hideTicks && !labelsAboveGridlines && !hideTopTick && (
1068
+ <Line
1069
+ key={`${tick.value}--hide-hideTicks`}
1070
+ from={tick.from}
1071
+ to={isLogarithmicAxis ? to : tick.to}
1072
+ stroke={config.yAxis.tickColor}
1073
+ display={orientation === 'horizontal' ? 'none' : 'block'}
1074
+ />
1075
+ )}
1076
+
1077
+ {orientation === 'horizontal' &&
1078
+ visualizationSubType !== 'stacked' &&
1079
+ config.yAxis.labelPlacement === 'On Date/Category Axis' &&
1080
+ !config.yAxis.hideLabel && (
1081
+ <Text
1082
+ transform={`translate(${tick.to.x - 5}, ${
1083
+ config.isLollipopChart
1084
+ ? tick.to.y - minY
1085
+ : tick.to.y -
1086
+ minY +
1087
+ (Number(config.barHeight * config.runtime.series.length) - barMinHeight) / 2
1088
+ }) rotate(-${config.runtime.horizontal ? config.runtime.yAxis.tickRotation || 0 : 0})`}
1089
+ verticalAnchor={'start'}
1090
+ textAnchor={'end'}
1091
+ >
1092
+ {tick.formattedValue}
1093
+ </Text>
1094
+ )}
1095
+
1096
+ {orientation === 'horizontal' &&
1097
+ visualizationSubType === 'stacked' &&
1098
+ config.yAxis.labelPlacement === 'On Date/Category Axis' &&
1099
+ !config.yAxis.hideLabel && (
1100
+ <Text
1101
+ transform={`translate(${tick.to.x - 5}, ${
1102
+ tick.to.y - minY + (Number(config.barHeight) - barMinHeight) / 2
1103
+ }) rotate(-${runtime.horizontal ? runtime.yAxis.tickRotation : 0})`}
1104
+ verticalAnchor={'start'}
1105
+ textAnchor={'end'}
1106
+ >
1107
+ {tick.formattedValue}
1108
+ </Text>
1109
+ )}
1110
+
1111
+ {orientation === 'horizontal' &&
1112
+ visualizationType === 'Paired Bar' &&
1113
+ !config.yAxis.hideLabel && (
1114
+ <Text
1115
+ transform={`translate(${tick.to.x - 5}, ${
1116
+ tick.to.y - minY + Number(config.barHeight) / 2
1117
+ }) rotate(-${runtime.horizontal ? runtime.yAxis.tickRotation : 0})`}
1118
+ textAnchor={'end'}
1119
+ verticalAnchor='middle'
1120
+ >
1121
+ {tick.formattedValue}
1122
+ </Text>
1123
+ )}
1124
+ {orientation === 'horizontal' &&
1125
+ visualizationType === 'Deviation Bar' &&
1126
+ !config.yAxis.hideLabel && (
1127
+ <Text
1128
+ transform={`translate(${tick.to.x - 5}, ${
1129
+ config.isLollipopChart
1130
+ ? tick.to.y - minY + 2
1131
+ : tick.to.y - minY + Number(config.barHeight) / 2
1132
+ }) rotate(-${runtime.horizontal ? runtime.yAxis.tickRotation : 0})`}
1133
+ textAnchor={'end'}
1134
+ verticalAnchor='middle'
1135
+ >
1136
+ {tick.formattedValue}
1137
+ </Text>
1138
+ )}
1139
+
1140
+ {orientation === 'vertical' &&
1141
+ visualizationType === 'Bump Chart' &&
1142
+ !config.yAxis.hideLabel && (
1143
+ <>
1144
+ <Text
1145
+ display={config.useLogScale ? showTicks : 'block'}
1146
+ dx={config.useLogScale ? -6 : 0}
1147
+ x={config.runtime.horizontal ? tick.from.x + 2 : tick.to.x - 8.5}
1148
+ y={tick.to.y - 13 + (config.runtime.horizontal ? horizontalTickOffset : 0)}
1149
+ angle={-Number(config.yAxis.tickRotation) || 0}
1150
+ verticalAnchor={config.runtime.horizontal ? 'start' : 'middle'}
1151
+ textAnchor={config.runtime.horizontal ? 'start' : 'end'}
1152
+ fill={config.yAxis.tickLabelColor}
1153
+ >
1154
+ {config.runtime.seriesLabelsAll[tick.formattedValue - 1]}
1155
+ </Text>
1156
+
1157
+ {(seriesHighlight.length === 0 ||
1158
+ seriesHighlight.includes(
1159
+ config.runtime.seriesLabelsAll[tick.formattedValue - 1]
1160
+ )) && (
1161
+ <rect
1162
+ x={0 - Number(config.yAxis.size)}
1163
+ y={tick.to.y - 8 + (config.runtime.horizontal ? horizontalTickOffset : 7)}
1164
+ width={Number(config.yAxis.size) + xScale(xScale.domain()[0])}
1165
+ height='2'
1166
+ fill={colorScale(config.runtime.seriesLabelsAll[tick.formattedValue - 1])}
1167
+ />
1168
+ )}
1169
+ </>
1170
+ )}
1171
+ {orientation === 'vertical' &&
1172
+ visualizationType !== 'Paired Bar' &&
1173
+ visualizationType !== 'Bump Chart' &&
1174
+ !config.yAxis.hideLabel && (
1175
+ <>
1176
+ {/* TOP ONLY SUFFIX: Dom suffix for 'show only top suffix' behavior */}
1177
+ {/* top suffix is shown alone and is allowed to 'overflow' to the right */}
1178
+ {/* SPECIAL ONE CHAR CASE: a one character top-only suffix does not overflow */}
1179
+ {/* IF VALUES ON LINE: suffix is combined with value to avoid having to calculate varying (now left-aligned) value widths */}
1180
+ {onlyShowTopPrefixSuffix && lastTick && !labelsAboveGridlines && (
1181
+ <BlurStrokeText
1182
+ innerRef={suffixRef}
1183
+ display={isLogarithmicAxis ? showTicks : 'block'}
1184
+ dx={isLogarithmicAxis ? -6 : 0}
1185
+ x={labelX}
1186
+ y={labelY}
1187
+ angle={-Number(config.yAxis.tickRotation) || 0}
1188
+ verticalAnchor={labelVerticalAnchor}
1189
+ textAnchor={suffixHasNoSpace ? 'end' : 'start'}
1190
+ fill={config.yAxis.tickLabelColor}
1191
+ stroke={'#fff'}
1192
+ paintOrder={'stroke'} // keeps stroke under fill
1193
+ strokeLinejoin='round'
1194
+ style={{ whiteSpace: 'pre-wrap' }} // prevents leading spaces from being trimmed
1195
+ >
1196
+ {suffix}
1197
+ </BlurStrokeText>
1198
+ )}
1199
+
1200
+ {/* VALUE */}
1201
+ <BlurStrokeText
1202
+ innerRef={el => lastTick && (topYLabelRef.current = el)}
1203
+ display={isLogarithmicAxis ? showTicks : 'block'}
1204
+ dx={isLogarithmicAxis ? -6 : 0}
1205
+ x={suffixHasNoSpace ? labelX - suffixWidth : labelX}
1206
+ y={labelY + (config.runtime.horizontal ? horizontalTickOffset : 0)}
1207
+ angle={-Number(config.yAxis.tickRotation) || 0}
1208
+ verticalAnchor={config.runtime.horizontal ? 'start' : labelVerticalAnchor}
1209
+ textAnchor={config.runtime.horizontal || labelsAboveGridlines ? 'start' : 'end'}
1210
+ fill={config.yAxis.tickLabelColor}
1211
+ stroke={'#fff'}
1212
+ disableStroke={!labelsAboveGridlines}
1213
+ strokeLinejoin='round'
1214
+ paintOrder={'stroke'} // keeps stroke under fill
1215
+ style={{ whiteSpace: 'pre-wrap' }} // prevents leading spaces from being trimmed
1216
+ >
1217
+ {`${tick.formattedValue}${combineDomSuffixWithValue ? suffix : ''}`}
1218
+ </BlurStrokeText>
1219
+ </>
1220
+ )}
1221
+ </Group>
1222
+ )
1223
+ })}
1224
+ <Text
1225
+ className='y-label'
1226
+ textAnchor='middle'
1227
+ verticalAnchor='start'
1228
+ transform={`translate(${-1 * runtime.yAxis.size + yLabelOffset}, ${axisCenter}) rotate(-90)`}
1229
+ fontWeight='bold'
1230
+ fill={config.yAxis.labelColor}
1231
+ >
1232
+ {props.label}
1233
+ </Text>
1234
+ </Group>
1235
+ )
1236
+ }}
1237
+ </AxisLeft>
1238
+ )}
1239
+ {config.yAxis.type === 'categorical' && config.orientation === 'vertical' && (
1240
+ <CategoricalYAxis
1241
+ max={max}
1242
+ maxValue={maxValue}
1243
+ height={initialHeight}
1244
+ xMax={xMax}
1245
+ yMax={yMax}
1246
+ leftSize={Number(runtime.yAxis.size) - config.yAxis.axisPadding}
1247
+ />
1248
+ )}
1249
+ {/* Right Axis */}
1250
+ {hasRightAxis && (
1251
+ <AxisRight
1252
+ scale={yScaleRight}
1253
+ left={Number(width - config.yAxis.rightAxisSize)}
1254
+ label={config.yAxis.rightLabel}
1255
+ tickFormat={tick => formatNumber(tick, 'right')}
1256
+ numTicks={runtime.yAxis.rightNumTicks || undefined}
1257
+ labelOffset={45}
1258
+ >
1259
+ {props => {
1260
+ const axisCenter =
1261
+ config.orientation === 'horizontal'
1262
+ ? (props.axisToPoint.y - props.axisFromPoint.y) / 2
1263
+ : (props.axisFromPoint.y - props.axisToPoint.y) / 2
1264
+ const horizontalTickOffset =
1265
+ yMax / props.ticks.length / 2 - (yMax / props.ticks.length) * (1 - config.barThickness) + 5
1266
+ return (
1267
+ <Group className='right-axis'>
1268
+ {props.ticks.map((tick, i) => {
1269
+ return (
1270
+ <Group key={`vx-tick-${tick.value}-${i}`} className='vx-axis-tick'>
1271
+ {!runtime.yAxis.rightHideTicks && (
1272
+ <Line
1273
+ from={tick.from}
1274
+ to={tick.to}
1275
+ display={runtime.horizontal ? 'none' : 'block'}
1276
+ stroke={config.yAxis.rightAxisTickColor}
1277
+ />
1278
+ )}
1279
+
1280
+ {runtime.yAxis.rightGridLines ? (
1281
+ <Line from={{ x: tick.from.x + xMax, y: tick.from.y }} to={tick.from} stroke='#d6d6d6' />
1282
+ ) : (
1283
+ ''
1284
+ )}
1285
+
1286
+ {!config.yAxis.rightHideLabel && (
1287
+ <Text
1288
+ x={tick.to.x}
1289
+ y={tick.to.y + (runtime.horizontal ? horizontalTickOffset : 0)}
1290
+ verticalAnchor={runtime.horizontal ? 'start' : 'middle'}
1291
+ textAnchor={'start'}
1292
+ fill={config.yAxis.rightAxisTickLabelColor}
1293
+ >
1294
+ {tick.formattedValue}
1295
+ </Text>
1296
+ )}
1297
+ </Group>
1298
+ )
1299
+ })}
1300
+ {!config.yAxis.rightHideAxis && (
1301
+ <Line from={props.axisFromPoint} to={props.axisToPoint} stroke='#333' />
1302
+ )}
1303
+ <Text
1304
+ className='y-label'
1305
+ textAnchor='middle'
1306
+ verticalAnchor='start'
1307
+ transform={`translate(${
1308
+ config.yAxis.rightLabelOffsetSize ? config.yAxis.rightLabelOffsetSize : 0
1309
+ }, ${axisCenter}) rotate(-90)`}
1310
+ fontWeight='bold'
1311
+ fill={config.yAxis.rightAxisLabelColor}
1312
+ >
1313
+ {props.label}
1314
+ </Text>
1315
+ </Group>
1316
+ )
1317
+ }}
1318
+ </AxisRight>
1319
+ )}
1320
+ {hasTopAxis && config.topAxis.hasLine && (
1321
+ <AxisTop
1322
+ stroke='#333'
1323
+ left={Number(runtime.yAxis.size)}
1324
+ scale={xScale}
1325
+ hideTicks
1326
+ hideZero
1327
+ tickLabelProps={() => ({
1328
+ fill: 'transparent'
1329
+ })}
1330
+ />
1331
+ )}
1332
+ {/* X axis */}
1333
+ {visualizationType !== 'Paired Bar' && visualizationType !== 'Spark Line' && (
1334
+ <AxisBottom
1335
+ innerRef={axisBottomRef}
1336
+ top={
1337
+ runtime.horizontal && config.visualizationType !== 'Forest Plot'
1338
+ ? Number(heights.horizontal) + Number(config.xAxis.axisPadding)
1339
+ : config.visualizationType === 'Forest Plot'
1340
+ ? yMax + Number(config.xAxis.axisPadding)
1341
+ : yMax
1342
+ }
1343
+ left={config.visualizationType !== 'Forest Plot' ? Number(runtime.yAxis.size) : 0}
1344
+ label={config[section].label}
1345
+ tickFormat={handleBottomTickFormatting}
1346
+ scale={xScale}
1347
+ stroke='#333'
1348
+ numTicks={countNumOfTicks('xAxis')}
1349
+ tickStroke='#333'
1350
+ tickValues={
1351
+ config.xAxis.manual
1352
+ ? getTickValues(
1353
+ xAxisDataMapped,
1354
+ xScale,
1355
+ config.xAxis.type === 'date-time' ? countNumOfTicks('xAxis') : getManualStep(),
1356
+ config
1357
+ )
1358
+ : undefined
1359
+ }
1360
+ >
1361
+ {props => {
1362
+ const axisMaxHeight = bottomLabelStart + BOTTOM_LABEL_PADDING
1363
+
1364
+ const axisCenter =
1365
+ config.visualizationType !== 'Forest Plot'
1366
+ ? (props.axisToPoint.x - props.axisFromPoint.x) / 2
1367
+ : dimensions[0] / 2
1368
+ const containsMultipleWords = inputString => /\s/.test(inputString)
1369
+ const ismultiLabel = props.ticks.some(tick => containsMultipleWords(tick.value))
1370
+
1371
+ // Calculate sumOfTickWidth here, before map function
1372
+ const tickWidthMax = Math.max(
1373
+ ...props.ticks.map(tick =>
1374
+ getTextWidth(tick.formattedValue, `normal ${fontSize[config.fontSize]}px sans-serif`)
1375
+ )
1376
+ )
1377
+ // const marginTop = 20 // moved to top bc need for yMax calcs
1378
+ const accumulator = ismultiLabel ? 180 : 100
1379
+
1380
+ const textWidths = props.ticks.map(tick =>
1381
+ getTextWidth(tick.formattedValue, `normal ${fontSize[config.fontSize]}px sans-serif`)
1382
+ )
1383
+ const sumOfTickWidth = textWidths.reduce((a, b) => a + b, accumulator)
1384
+ const spaceBetweenEachTick = (xMax - sumOfTickWidth) / (props.ticks.length - 1)
1385
+
1386
+ // Check if ticks are overlapping
1387
+ // Determine the position of each tick
1388
+ let positions = [0] // The first tick is at position 0
1389
+ for (let i = 1; i < textWidths.length; i++) {
1390
+ // The position of each subsequent tick is the position of the previous tick
1391
+ // plus the width of the previous tick and the space
1392
+ positions[i] = positions[i - 1] + textWidths[i - 1] + spaceBetweenEachTick
1393
+ }
1394
+ // calculate the end of x axis box
1395
+ const axisBBox = axisBottomRef?.current?.getBBox().height
1396
+ config.xAxis.axisBBox = axisBBox
1397
+
1398
+ // Check if ticks are overlapping
1399
+ let areTicksTouching = false
1400
+ textWidths.forEach((_, i) => {
1401
+ if (positions[i] + textWidths[i] > positions[i + 1]) {
1402
+ areTicksTouching = true
1403
+ return
1404
+ }
1405
+ })
1406
+
1407
+ // Force wrap when showing years once so it's easier to read
1408
+ if (config.xAxis.showYearsOnce) {
1409
+ areTicksTouching = true
1410
+ }
1411
+
1412
+ const dynamicMarginTop =
1413
+ areTicksTouching && config.isResponsiveTicks ? tickWidthMax + DEFAULT_TICK_LENGTH + 20 : 0
1414
+
1415
+ config.dynamicMarginTop = dynamicMarginTop
1416
+ config.xAxis.tickWidthMax = tickWidthMax
1417
+
1418
+ return (
1419
+ <Group className='bottom-axis' width={dimensions[0]}>
1420
+ {props.ticks.map((tick, i, propsTicks) => {
1421
+ // when using LogScale show major ticks values only
1422
+ const showTick = String(tick.value).startsWith('1') || tick.value === 0.1 ? 'block' : 'none'
1423
+ const tickLength = showTick === 'block' ? 16 : DEFAULT_TICK_LENGTH
1424
+ const to = { x: tick.to.x, y: tickLength }
1425
+ const textWidth = getTextWidth(
1426
+ tick.formattedValue,
1427
+ `normal ${fontSize[config.fontSize]}px sans-serif`
1428
+ )
1429
+ const limitedWidth = 100 / propsTicks.length
1430
+ //reset rotations by updating config
1431
+ config.yAxis.tickRotation =
1432
+ config.isResponsiveTicks && config.orientation === 'horizontal' ? 0 : config.yAxis.tickRotation
1433
+ config.xAxis.tickRotation =
1434
+ config.isResponsiveTicks && config.orientation === 'vertical' ? 0 : config.xAxis.tickRotation
1435
+ //configure rotation
1436
+
1437
+ const tickRotation =
1438
+ config.isResponsiveTicks && areTicksTouching
1439
+ ? -Number(config.xAxis.maxTickRotation) || -90
1440
+ : -Number(config.runtime.xAxis.tickRotation)
1441
+
1442
+ return (
1443
+ <Group key={`vx-tick-${tick.value}-${i}`} className={'vx-axis-tick'}>
1444
+ {!config.xAxis.hideTicks && (
1445
+ <Line
1446
+ from={tick.from}
1447
+ to={orientation === 'horizontal' && isLogarithmicAxis ? to : tick.to}
1448
+ stroke={config.xAxis.tickColor}
1449
+ strokeWidth={showTick === 'block' && isLogarithmicAxis ? 1.3 : 1}
1450
+ />
1451
+ )}
1452
+ {!config.xAxis.hideLabel && (
1453
+ <Text
1454
+ innerRef={el => (xAxisLabelRefs.current[i] = el)}
1455
+ dy={config.orientation === 'horizontal' && isLogarithmicAxis ? 8 : 0}
1456
+ display={config.orientation === 'horizontal' && isLogarithmicAxis ? showTick : 'block'}
1457
+ x={tick.to.x}
1458
+ y={tick.to.y + X_TICK_LABEL_PADDING}
1459
+ angle={tickRotation}
1460
+ verticalAnchor={tickRotation < -50 ? 'middle' : 'start'}
1461
+ textAnchor={tickRotation ? 'end' : 'middle'}
1462
+ width={
1463
+ areTicksTouching && !config.isResponsiveTicks && !Number(config[section].tickRotation)
1464
+ ? limitedWidth
1465
+ : undefined
1466
+ }
1467
+ fill={config.xAxis.tickLabelColor}
1468
+ >
1469
+ {tick.formattedValue}
1470
+ </Text>
1471
+ )}
1472
+ </Group>
1473
+ )
1474
+ })}
1475
+ {!config.xAxis.hideAxis && <Line from={props.axisFromPoint} to={props.axisToPoint} stroke='#333' />}
1476
+ <Text
1477
+ innerRef={xAxisTitleRef}
1478
+ className='x-axis-title-label'
1479
+ x={axisCenter}
1480
+ y={isForestPlot ? 0 /* set via ref */ : axisMaxHeight}
1481
+ textAnchor='middle'
1482
+ verticalAnchor='start'
1483
+ fontWeight='bold'
1484
+ fill={config.xAxis.labelColor}
1485
+ >
1486
+ {props.label}
1487
+ </Text>
1488
+ </Group>
1489
+ )
1490
+ }}
1491
+ </AxisBottom>
1492
+ )}
1493
+ </svg>
1494
+ {!isDraggingAnnotation &&
1495
+ tooltipData &&
1496
+ Object.entries(tooltipData.data).length > 0 &&
1497
+ tooltipOpen &&
1498
+ showTooltip &&
1499
+ tooltipData.dataYPosition &&
1500
+ tooltipData.dataXPosition && (
1501
+ <>
1502
+ <style>{`.tooltip {background-color: rgba(255,255,255, ${
1503
+ config.tooltips.opacity / 100
1504
+ }) !important;`}</style>
1505
+ <style>{`.tooltip {max-width:300px} !important; word-wrap: break-word; `}</style>
1506
+ <TooltipWithBounds
1507
+ key={Math.random()}
1508
+ className={'tooltip cdc-open-viz-module'}
1509
+ left={tooltipLeft}
1510
+ top={tooltipTop}
1511
+ >
1334
1512
  <ul>
1335
1513
  {typeof tooltipData === 'object' &&
1336
1514
  Object.entries(tooltipData.data).map((item, index) => <TooltipListItem item={item} key={index} />)}
@@ -1361,6 +1539,6 @@ const LinearChart: React.FC<LinearChartProps> = props => {
1361
1539
  </div>
1362
1540
  </ErrorBoundary>
1363
1541
  )
1364
- }
1542
+ })
1365
1543
 
1366
1544
  export default LinearChart