@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.
- package/dist/cdcchart.js +37673 -36530
- package/index.html +1 -1
- package/package.json +2 -2
- package/src/CdcChart.tsx +128 -106
- package/src/_stories/Chart.Legend.Gradient.stories.tsx +33 -0
- package/src/_stories/Chart.stories.tsx +28 -0
- package/src/_stories/ChartAxisLabels.stories.tsx +20 -0
- package/src/_stories/ChartAxisTitles.stories.tsx +53 -0
- package/src/_stories/ChartPrefixSuffix.stories.tsx +151 -0
- package/src/_stories/_mock/horizontal_bar.json +257 -0
- package/src/_stories/_mock/large_x_axis_labels.json +261 -0
- package/src/_stories/_mock/paired-bar.json +262 -0
- package/src/_stories/_mock/pie_with_data.json +255 -0
- package/src/_stories/_mock/simplified_line.json +1510 -0
- package/src/components/Annotations/components/AnnotationDraggable.tsx +0 -3
- package/src/components/Annotations/components/AnnotationDropdown.tsx +1 -1
- package/src/components/Axis/Categorical.Axis.tsx +22 -4
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +95 -16
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +41 -17
- package/src/components/BarChart/components/BarChart.Vertical.tsx +78 -20
- package/src/components/BarChart/helpers/index.ts +23 -4
- package/src/components/BrushChart.tsx +3 -2
- package/src/components/DeviationBar.jsx +58 -8
- package/src/components/EditorPanel/EditorPanel.tsx +62 -39
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +6 -23
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +21 -4
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +297 -35
- package/src/components/EditorPanel/components/panels.scss +4 -6
- package/src/components/EditorPanel/editor-panel.scss +0 -8
- package/src/components/EditorPanel/helpers/tests/updateFieldRankByValue.test.ts +38 -0
- package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +42 -0
- package/src/components/EditorPanel/useEditorPermissions.ts +1 -0
- package/src/components/ForestPlot/ForestPlot.tsx +2 -3
- package/src/components/ForestPlot/ForestPlotProps.ts +2 -0
- package/src/components/Legend/Legend.Component.tsx +16 -16
- package/src/components/Legend/Legend.Suppression.tsx +25 -20
- package/src/components/Legend/Legend.tsx +0 -2
- package/src/components/Legend/helpers/index.ts +16 -19
- package/src/components/LegendWrapper.tsx +3 -1
- package/src/components/LinearChart.tsx +740 -562
- package/src/components/PairedBarChart.jsx +50 -10
- package/src/components/PieChart/PieChart.tsx +1 -6
- package/src/components/Regions/components/Regions.tsx +33 -19
- package/src/components/ZoomBrush.tsx +25 -6
- package/src/coreStyles_chart.scss +3 -0
- package/src/data/initial-state.js +6 -2
- package/src/helpers/configHelpers.ts +28 -0
- package/src/helpers/handleRankByValue.ts +15 -0
- package/src/helpers/sizeHelpers.ts +25 -0
- package/src/helpers/tests/handleRankByValue.test.ts +37 -0
- package/src/helpers/tests/sizeHelpers.test.ts +80 -0
- package/src/hooks/useColorPalette.js +10 -2
- package/src/hooks/useLegendClasses.ts +4 -0
- package/src/hooks/useScales.ts +31 -3
- package/src/hooks/useTooltip.tsx +9 -5
- package/src/index.jsx +1 -0
- package/src/scss/DataTable.scss +5 -4
- package/src/scss/main.scss +57 -52
- package/src/types/ChartConfig.ts +38 -16
- package/src/types/ChartContext.ts +18 -14
- package/src/_stories/Chart.Legend.Gradient.tsx +0 -19
- 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
|
|
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 {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
//
|
|
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
|
|
102
|
+
const [suffixWidth, setSuffixWidth] = useState(0)
|
|
121
103
|
|
|
122
|
-
//
|
|
123
|
-
const triggerRef = useRef()
|
|
104
|
+
// REFS
|
|
124
105
|
const axisBottomRef = useRef(null)
|
|
125
|
-
const
|
|
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
|
-
//
|
|
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:
|
|
209
|
+
xMax: parentWidth - Number(config.orientation === 'horizontal' ? config.xAxis.size : config.yAxis.size)
|
|
163
210
|
})
|
|
164
211
|
|
|
165
|
-
|
|
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')
|
|
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')
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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={
|
|
515
|
-
height={
|
|
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
|
-
|
|
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='
|
|
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
|
-
{
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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={
|
|
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
|
|
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={
|
|
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
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
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
|