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