@cdc/chart 4.24.9 → 4.24.11

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