@cdc/chart 4.25.11 → 4.26.2
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/CLAUDE.local.md +79 -0
- package/dist/{cdcchart-dgT_1dIT.es.js → cdcchart-DQ00cQCm.es.js} +1 -20
- package/dist/cdcchart.js +51401 -50814
- package/examples/default.json +378 -0
- package/examples/feature/__data__/horizon-chart-data.json +373 -0
- package/examples/feature/annotations/index.json +3 -6
- package/examples/feature/horizon/horizon-chart.json +395 -0
- package/examples/feature/pie/planet-pie-example-config.json +48 -2
- package/examples/line-chart-states.json +1085 -0
- package/examples/private/123.json +694 -0
- package/examples/private/DEV-12100.json +1303 -0
- package/examples/private/anchor-issue.json +4094 -0
- package/examples/private/backwards-slider.json +10430 -0
- package/examples/private/cat-y.json +1235 -0
- package/examples/private/data-points.json +228 -0
- package/examples/private/georgia.csv +160 -0
- package/examples/private/height.json +3915 -0
- package/examples/private/links.json +569 -0
- package/examples/private/quadrant.txt +30 -0
- package/examples/private/test-forecast.json +5510 -0
- package/examples/private/timeline-data.json +1 -0
- package/examples/private/timeline.json +389 -0
- package/examples/private/warming-stripe-test.json +2578 -0
- package/examples/private/warming-stripes.json +4763 -0
- package/examples/radar-chart-simple.json +133 -0
- package/examples/radar-chart.json +148 -0
- package/examples/tech-adoption-with-links.json +560 -0
- package/index.html +1 -36
- package/package.json +59 -60
- package/src/CdcChartComponent.tsx +206 -89
- package/src/_stories/Chart.Anchors.stories.tsx +10 -0
- package/src/_stories/Chart.BoxPlot.stories.tsx +7 -0
- package/src/_stories/Chart.CI.stories.tsx +13 -0
- package/src/_stories/Chart.Combo.stories.tsx +17 -0
- package/src/_stories/Chart.CustomColors.stories.tsx +4 -0
- package/src/_stories/Chart.DynamicSeries.stories.tsx +19 -0
- package/src/_stories/Chart.Filters.stories.tsx +4 -0
- package/src/_stories/Chart.Forecast.stories.tsx +4 -0
- package/src/_stories/Chart.HTMLInDataTable.stories.tsx +22 -0
- package/src/_stories/Chart.Legend.Gradient.stories.tsx +28 -0
- package/src/_stories/Chart.Patterns.stories.tsx +4 -0
- package/src/_stories/Chart.PreserveDecimals.stories.tsx +25 -0
- package/src/_stories/Chart.Regions.Categorical.stories.tsx +161 -0
- package/src/_stories/Chart.Regions.DateScale.stories.tsx +216 -0
- package/src/_stories/Chart.Regions.DateTimeScale.stories.tsx +312 -0
- package/src/_stories/Chart.ScatterPlot.stories.tsx +4 -0
- package/src/_stories/Chart.SmallMultiples.stories.tsx +16 -0
- package/src/_stories/Chart.stories.tsx +45 -0
- package/src/_stories/Chart.tooltip.stories.tsx +7 -0
- package/src/_stories/ChartAnnotation.stories.tsx +10 -0
- package/src/_stories/ChartAxisLabels.stories.tsx +4 -0
- package/src/_stories/ChartAxisTitles.stories.tsx +10 -0
- package/src/_stories/ChartBar.Editor.stories.tsx +11 -6
- package/src/_stories/ChartBrush.Editor.stories.tsx +295 -0
- package/src/_stories/ChartBrush.Matrix.Continuous.stories.tsx +41 -0
- package/src/_stories/ChartBrush.Matrix.Date.stories.tsx +114 -0
- package/src/_stories/ChartBrush.Matrix.DateTime.stories.tsx +78 -0
- package/src/_stories/ChartBrush.stories.tsx +57 -0
- package/src/_stories/ChartEditor.Editor.stories.tsx +3 -5
- package/src/_stories/ChartEditor.stories.tsx +7 -0
- package/src/_stories/ChartLine.QuadrantAngles.stories.tsx +89 -0
- package/src/_stories/ChartLine.Suppression.stories.tsx +7 -0
- package/src/_stories/ChartLine.Symbols.stories.tsx +4 -0
- package/src/_stories/ChartPrefixSuffix.stories.tsx +46 -1
- package/src/_stories/TechAdoptionWithLinks.stories.tsx +34 -0
- package/src/_stories/_mock/brush_continuous.json +86 -0
- package/src/_stories/_mock/brush_date_large.json +176 -0
- package/src/_stories/_mock/brush_enabled.json +326 -0
- package/src/_stories/_mock/brush_mock.json +2 -69
- package/src/_stories/_mock/horizontal-bars-dynamic-y-axis.json +413 -0
- package/src/_stories/_mock/line_chart_angle_near_zero_fall.json +195 -0
- package/src/_stories/_mock/line_chart_angle_near_zero_rise.json +195 -0
- package/src/_stories/_mock/line_chart_angle_q1_steep_upward.json +195 -0
- package/src/_stories/_mock/line_chart_angle_q2_gentle_downward.json +195 -0
- package/src/_stories/_mock/line_chart_angle_q3_steep_downward.json +195 -0
- package/src/_stories/_mock/line_chart_angle_q4_gentle_upward.json +195 -0
- package/src/_stories/_mock/line_chart_quadrant_angles.json +264 -0
- package/src/components/Annotations/components/AnnotationDraggable.styles.css +11 -17
- package/src/components/Annotations/components/AnnotationDraggable.tsx +240 -116
- package/src/components/Annotations/components/AnnotationDropdown.styles.css +1 -2
- package/src/components/Annotations/components/AnnotationDropdown.tsx +8 -12
- package/src/components/Annotations/components/AnnotationList.styles.css +4 -10
- package/src/components/Annotations/components/AnnotationList.tsx +5 -4
- package/src/components/Annotations/components/findNearestDatum.ts +75 -85
- package/src/components/Annotations/helpers/getVisibleAnnotations.ts +38 -0
- package/src/components/AreaChart/components/AreaChart.Stacked.jsx +1 -2
- package/src/components/Axis/BottomAxis.tsx +270 -0
- package/src/components/Axis/Categorical.Axis.tsx +6 -7
- package/src/components/Axis/LeftAxis.tsx +404 -0
- package/src/components/Axis/LeftAxisGridlines.tsx +77 -0
- package/src/components/Axis/PairedBarAxis.tsx +186 -0
- package/src/components/Axis/README.md +94 -0
- package/src/components/Axis/RightAxis.tsx +108 -0
- package/src/components/Axis/axis.constants.ts +21 -0
- package/src/components/Axis/index.ts +7 -0
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +178 -24
- 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 +6 -8
- package/src/components/BarChart/components/BarChart.tsx +7 -1
- package/src/components/BarChart/components/context.tsx +1 -0
- package/src/components/BarChart/helpers/useBarChart.ts +14 -2
- package/src/components/Brush/BrushSelector.tsx +1390 -0
- package/src/components/Brush/MiniChartPreview.tsx +400 -0
- package/src/components/DeviationBar.jsx +9 -7
- package/src/components/EditorPanel/EditorPanel.tsx +2734 -2595
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +60 -22
- package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +56 -34
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +137 -30
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +2 -0
- package/src/components/EditorPanel/components/Panels/Panel.Radar.tsx +353 -0
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +0 -1
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +30 -25
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +42 -28
- package/src/components/EditorPanel/components/Panels/index.tsx +2 -0
- package/src/components/EditorPanel/useEditorPermissions.ts +81 -39
- package/src/components/HorizonChart/HorizonChart.tsx +131 -0
- package/src/components/HorizonChart/components/HorizonBand.tsx +160 -0
- package/src/components/HorizonChart/helpers/calculateHorizonBands.ts +27 -0
- package/src/components/HorizonChart/helpers/getHorizonLayerColors.ts +40 -0
- package/src/components/HorizonChart/index.tsx +3 -0
- package/src/components/Legend/Legend.Component.tsx +52 -4
- package/src/components/Legend/Legend.tsx +4 -3
- package/src/components/Legend/LegendValueRange.tsx +77 -0
- package/src/components/Legend/helpers/createFormatLabels.tsx +164 -2
- package/src/components/Legend/helpers/generateValueRanges.ts +92 -0
- package/src/components/Legend/helpers/index.ts +10 -6
- package/src/components/LineChart/helpers/README.md +292 -0
- package/src/components/LineChart/helpers/labelPositioning.test.ts +245 -0
- package/src/components/LineChart/helpers/labelPositioning.ts +304 -0
- package/src/components/LineChart/index.tsx +44 -8
- package/src/components/LinearChart/README.md +109 -0
- package/src/components/LinearChart/VisualizationRenderer.tsx +267 -0
- package/src/components/LinearChart/linearChart.constants.ts +84 -0
- package/src/components/LinearChart/tests/LinearChart.test.tsx +201 -0
- package/src/components/LinearChart/tests/mockConfigContext.ts +129 -0
- package/src/components/LinearChart/utils/tickFormatting.ts +146 -0
- package/src/components/LinearChart.tsx +338 -1082
- package/src/components/PairedBarChart.jsx +20 -3
- package/src/components/PieChart/PieChart.tsx +1 -1
- package/src/components/RadarChart/RadarAxis.tsx +78 -0
- package/src/components/RadarChart/RadarChart.tsx +298 -0
- package/src/components/RadarChart/RadarGrid.tsx +64 -0
- package/src/components/RadarChart/RadarPolygon.tsx +91 -0
- package/src/components/RadarChart/helpers.ts +83 -0
- package/src/components/RadarChart/index.tsx +3 -0
- package/src/components/Regions/components/Regions.tsx +365 -122
- package/src/components/ScatterPlot/ScatterPlot.jsx +2 -2
- package/src/components/SmallMultiples/SmallMultipleTile.tsx +5 -1
- package/src/components/WarmingStripes/WarmingStripes.tsx +230 -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 +17 -2
- package/src/helpers/calculateHorizontalBarCategoryLabelWidth.ts +57 -0
- package/src/helpers/getExcludedData.ts +4 -0
- package/src/helpers/getMinMax.ts +12 -7
- package/src/helpers/handleChartAriaLabels.ts +19 -19
- package/src/helpers/handleLineType.ts +22 -18
- package/src/helpers/sizeHelpers.ts +0 -20
- package/src/helpers/smallMultiplesHelpers.ts +1 -1
- package/src/hooks/useChartHoverAnalytics.tsx +10 -9
- package/src/hooks/useProgrammaticTooltip.ts +23 -2
- package/src/hooks/useScales.ts +18 -1
- package/src/hooks/useTooltip.tsx +34 -10
- package/src/scss/DataTable.scss +0 -4
- package/src/scss/main.scss +22 -3
- package/src/selectors/README.md +68 -0
- package/src/store/chart.reducer.ts +2 -0
- package/src/test/CdcChart.test.jsx +1 -1
- package/src/types/ChartConfig.ts +21 -0
- package/src/types/ChartContext.ts +1 -0
- package/src/types/Horizon.ts +64 -0
- package/src/types/Label.ts +1 -0
- package/src/utils/analyticsTracking.ts +19 -0
- package/LICENSE +0 -201
- package/src/components/Annotations/components/helpers/index.tsx +0 -46
- 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
|
@@ -1,7 +1,17 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useContext,
|
|
4
|
+
useEffect,
|
|
5
|
+
useLayoutEffect,
|
|
6
|
+
useImperativeHandle,
|
|
7
|
+
useMemo,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
useCallback
|
|
11
|
+
} from 'react'
|
|
2
12
|
|
|
3
13
|
// Libraries
|
|
4
|
-
import {
|
|
14
|
+
import { AxisTop } from '@visx/axis'
|
|
5
15
|
import { Group } from '@visx/group'
|
|
6
16
|
import { Line, Bar } from '@visx/shape'
|
|
7
17
|
import { Tooltip as ReactTooltip } from 'react-tooltip'
|
|
@@ -12,33 +22,25 @@ import _ from 'lodash'
|
|
|
12
22
|
|
|
13
23
|
// CDC Components
|
|
14
24
|
import { isDateScale } from '@cdc/core/helpers/cove/date'
|
|
15
|
-
import { AreaChartStacked } from './AreaChart'
|
|
16
|
-
import BarChart from './BarChart'
|
|
17
25
|
import ConfigContext from '../ConfigContext'
|
|
18
|
-
import BoxPlotVertical from './BoxPlot/BoxPlot.Vertical'
|
|
19
|
-
import BoxPlotHorizontal from './BoxPlot/BoxPlot.Horizontal'
|
|
20
|
-
import ScatterPlot from './ScatterPlot'
|
|
21
|
-
import DeviationBar from './DeviationBar'
|
|
22
26
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
23
|
-
import Forecasting from './Forecasting'
|
|
24
|
-
import LineChart from './LineChart'
|
|
25
|
-
import ForestPlot from './ForestPlot'
|
|
26
|
-
import PairedBarChart from './PairedBarChart'
|
|
27
27
|
import useIntersectionObserver from '../hooks/useIntersectionObserver'
|
|
28
28
|
import Regions from './Regions'
|
|
29
|
-
import CategoricalYAxis from './Axis
|
|
30
|
-
import
|
|
29
|
+
import { CategoricalYAxis, LeftAxis, LeftAxisGridlines, BottomAxis, PairedBarAxis, RightAxis } from './Axis'
|
|
30
|
+
import BrushSelector from './Brush/BrushSelector'
|
|
31
|
+
import VisualizationRenderer from './LinearChart/VisualizationRenderer'
|
|
32
|
+
import { TYPES_WITHOUT_GRID, TYPES_WITH_TOOLTIP_GUIDES } from './LinearChart/linearChart.constants'
|
|
33
|
+
import { useTickFormatters } from './LinearChart/utils/tickFormatting'
|
|
31
34
|
|
|
32
35
|
// Helpers
|
|
33
36
|
import { isLegendWrapViewport, isMobileFontViewport } from '@cdc/core/helpers/viewports'
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import { filterAndShiftLinearDateTicks } from '../helpers/filterAndShiftLinearDateTicks'
|
|
37
|
+
import { calcInitialHeight } from '../helpers/sizeHelpers'
|
|
38
|
+
import { calculateHorizontalBarCategoryLabelWidth } from '../helpers/calculateHorizontalBarCategoryLabelWidth'
|
|
37
39
|
|
|
38
40
|
// Hooks
|
|
39
41
|
import useReduceData from '../hooks/useReduceData'
|
|
40
42
|
import useRightAxis from '../hooks/useRightAxis'
|
|
41
|
-
import useScales
|
|
43
|
+
import useScales from '../hooks/useScales'
|
|
42
44
|
import { useProgrammaticTooltip } from '../hooks/useProgrammaticTooltip'
|
|
43
45
|
import { useSmallMultipleSynchronization } from '../hooks/useSmallMultipleSynchronization'
|
|
44
46
|
|
|
@@ -47,7 +49,6 @@ import { useTooltip as useCoveTooltip } from '../hooks/useTooltip'
|
|
|
47
49
|
import { useChartHoverAnalytics } from '../hooks/useChartHoverAnalytics'
|
|
48
50
|
import { useEditorPermissions } from './EditorPanel/useEditorPermissions'
|
|
49
51
|
import Annotation from './Annotations'
|
|
50
|
-
import { BlurStrokeText } from '@cdc/core/components/BlurStrokeText'
|
|
51
52
|
import { countNumOfTicks } from '../helpers/countNumOfTicks'
|
|
52
53
|
import HoverLine from './HoverLine/HoverLine'
|
|
53
54
|
import { SmallMultiples } from './SmallMultiples'
|
|
@@ -57,15 +58,36 @@ type LinearChartProps = {
|
|
|
57
58
|
parentHeight: number
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
// Axis and tick constants
|
|
60
62
|
const BOTTOM_LABEL_PADDING = 9
|
|
61
63
|
const X_TICK_LABEL_PADDING = 4.5
|
|
62
64
|
const DEFAULT_TICK_LENGTH = 8
|
|
63
|
-
const
|
|
65
|
+
const DEFAULT_MAX_TICK_ROTATION = 90
|
|
66
|
+
|
|
67
|
+
// Font sizes
|
|
64
68
|
const TICK_LABEL_FONT_SIZE = 16
|
|
65
69
|
const TICK_LABEL_FONT_SIZE_SMALL = 13
|
|
66
70
|
const AXIS_LABEL_FONT_SIZE = 18
|
|
67
71
|
const AXIS_LABEL_FONT_SIZE_SMALL = 14
|
|
68
|
-
|
|
72
|
+
|
|
73
|
+
// Label positioning constants
|
|
74
|
+
const BELOW_BAR_TEXT_OFFSET = -6.5
|
|
75
|
+
const LABEL_PADDING_OFFSET = 8
|
|
76
|
+
|
|
77
|
+
// Brush constants
|
|
78
|
+
const BRUSH_HEIGHT = 70
|
|
79
|
+
const BRUSH_MARGIN = 10
|
|
80
|
+
const BRUSH_MIN_WIDTH = 100
|
|
81
|
+
|
|
82
|
+
// Tooltip constants
|
|
83
|
+
const TOOLTIP_EDGE_BUFFER = 10
|
|
84
|
+
const TOOLTIP_OFFSET = 6
|
|
85
|
+
|
|
86
|
+
// Chart-specific constants
|
|
87
|
+
const WARMING_STRIPES_HEIGHT = 78
|
|
88
|
+
|
|
89
|
+
// Time constants
|
|
90
|
+
const MONTH_AS_MS = 1000 * 60 * 60 * 24 * 30
|
|
69
91
|
|
|
70
92
|
type TooltipData = {
|
|
71
93
|
dataXPosition?: number
|
|
@@ -84,12 +106,9 @@ type UseTooltipReturn<T = TooltipData> = {
|
|
|
84
106
|
const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, parentWidth }, svgRef) => {
|
|
85
107
|
// prettier-ignore
|
|
86
108
|
const {
|
|
87
|
-
colorScale,
|
|
88
109
|
config,
|
|
89
|
-
convertLineToBarGraph,
|
|
90
110
|
currentViewport,
|
|
91
111
|
vizViewport,
|
|
92
|
-
dimensions,
|
|
93
112
|
formatDate,
|
|
94
113
|
formatNumber,
|
|
95
114
|
handleChartAriaLabels,
|
|
@@ -102,36 +121,38 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
102
121
|
parentRef,
|
|
103
122
|
tableData,
|
|
104
123
|
transformedData: data,
|
|
105
|
-
seriesHighlight
|
|
106
124
|
} = useContext(ConfigContext)
|
|
107
125
|
|
|
108
126
|
// CONFIG
|
|
109
127
|
// todo: start destructuring this file for conciseness
|
|
110
|
-
const {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
visualizationSubType,
|
|
114
|
-
orientation,
|
|
115
|
-
xAxis,
|
|
116
|
-
yAxis,
|
|
117
|
-
runtime,
|
|
118
|
-
legend,
|
|
119
|
-
forestPlot,
|
|
120
|
-
brush,
|
|
121
|
-
dataFormat,
|
|
122
|
-
debugSvg
|
|
123
|
-
} = config
|
|
124
|
-
|
|
125
|
-
const { labelsAboveGridlines, hideAxis, inlineLabel } = config.yAxis
|
|
128
|
+
const { visualizationType, orientation, xAxis, yAxis, runtime, legend, forestPlot, debugSvg } = config
|
|
129
|
+
|
|
130
|
+
const { inlineLabel } = config.yAxis
|
|
126
131
|
|
|
127
132
|
// HOOKS % STATES
|
|
128
|
-
|
|
129
|
-
|
|
133
|
+
// When brush is active, use tableData (full dataset) for min/max calculation
|
|
134
|
+
// so the y-axis shows the full range, but still use filtered data for rendering
|
|
135
|
+
const dataForMinMax = config.xAxis.brushActive && tableData && tableData.length > 0 ? tableData : data
|
|
136
|
+
const { minValue, maxValue, existPositiveValue, isAllLine } = useReduceData(config, dataForMinMax)
|
|
137
|
+
|
|
138
|
+
const { visSupportsSmallMultiples } = useEditorPermissions()
|
|
130
139
|
const { hasTopAxis } = getTopAxis(config)
|
|
140
|
+
|
|
141
|
+
// Increment on tableData change to force BrushSelector remount when filters change
|
|
142
|
+
const brushKeyRef = useRef(0)
|
|
143
|
+
const prevTableDataRef = useRef(tableData)
|
|
144
|
+
if (prevTableDataRef.current !== tableData) {
|
|
145
|
+
prevTableDataRef.current = tableData
|
|
146
|
+
brushKeyRef.current += 1
|
|
147
|
+
}
|
|
148
|
+
|
|
131
149
|
const [animatedChart, setAnimatedChart] = useState(false)
|
|
132
150
|
const [showHoverLine, setShowHoverLine] = useState(false)
|
|
133
151
|
const [point, setPoint] = useState({ x: 0, y: 0 })
|
|
134
152
|
const [suffixWidth, setSuffixWidth] = useState(0)
|
|
153
|
+
const [calculatedSvgHeight, setCalculatedSvgHeight] = useState<number | null>(null)
|
|
154
|
+
const [axisUpdateKey, setAxisUpdateKey] = useState(0)
|
|
155
|
+
const [synchronizedXValue, setSynchronizedXValue] = useState<any>(null)
|
|
135
156
|
|
|
136
157
|
// REFS
|
|
137
158
|
const axisBottomRef = useRef(null)
|
|
@@ -141,7 +162,6 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
141
162
|
const triggerRef = useRef()
|
|
142
163
|
const xAxisLabelRefs = useRef([])
|
|
143
164
|
const xAxisTitleRef = useRef(null)
|
|
144
|
-
const gridLineRefs = useRef([])
|
|
145
165
|
const tooltipRef = useRef(null)
|
|
146
166
|
|
|
147
167
|
const dataRef = useIntersectionObserver(triggerRef, {
|
|
@@ -167,57 +187,75 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
167
187
|
|
|
168
188
|
// height before bottom axis
|
|
169
189
|
const initialHeight = useMemo(
|
|
170
|
-
() =>
|
|
171
|
-
|
|
190
|
+
() =>
|
|
191
|
+
visualizationType === 'Warming Stripes' ? WARMING_STRIPES_HEIGHT : calcInitialHeight(config, currentViewport),
|
|
192
|
+
[
|
|
193
|
+
visualizationType,
|
|
194
|
+
currentViewport,
|
|
195
|
+
config.heights?.vertical,
|
|
196
|
+
config.heights?.horizontal,
|
|
197
|
+
config.heights?.mobileVertical,
|
|
198
|
+
config.orientation
|
|
199
|
+
]
|
|
172
200
|
)
|
|
173
201
|
const forestHeight = useMemo(() => initialHeight + forestRowsHeight, [initialHeight, forestRowsHeight])
|
|
174
202
|
|
|
175
|
-
// width
|
|
176
|
-
const width = useMemo(() => {
|
|
177
|
-
const initialWidth = dimensions[0]
|
|
178
|
-
const legendHidden = legend?.hide
|
|
179
|
-
const legendOnTopOrBottom = ['bottom', 'top'].includes(config.legend?.position)
|
|
180
|
-
const legendWrapped = isLegendWrapViewport(currentViewport)
|
|
181
|
-
|
|
182
|
-
const legendShowingLeftOrRight = !isForestPlot && !legendHidden && !legendOnTopOrBottom && !legendWrapped
|
|
183
|
-
|
|
184
|
-
if (!legendShowingLeftOrRight) return initialWidth
|
|
185
|
-
|
|
186
|
-
if (legendRef.current) {
|
|
187
|
-
const legendStyle = getComputedStyle(legendRef.current)
|
|
188
|
-
return (
|
|
189
|
-
initialWidth -
|
|
190
|
-
legendRef.current.getBoundingClientRect().width -
|
|
191
|
-
parseInt(legendStyle.marginLeft) -
|
|
192
|
-
parseInt(legendStyle.marginRight)
|
|
193
|
-
)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return initialWidth * 0.73
|
|
197
|
-
}, [dimensions[0], config.legend, currentViewport, legendRef.current])
|
|
198
|
-
|
|
199
203
|
// Used to calculate the y position of the x-axis title
|
|
204
|
+
// Dependencies trigger recalc when axis config changes (affects label rendering/size)
|
|
200
205
|
const bottomLabelStart = useMemo(() => {
|
|
201
206
|
xAxisLabelRefs.current = xAxisLabelRefs.current?.filter(label => label)
|
|
202
207
|
if (!xAxisLabelRefs.current.length) return
|
|
203
208
|
const tallestLabel = Math.max(...xAxisLabelRefs.current.map(label => label.getBBox().height))
|
|
204
209
|
return tallestLabel + X_TICK_LABEL_PADDING + DEFAULT_TICK_LENGTH
|
|
205
|
-
}, [
|
|
210
|
+
}, [parentWidth, config.xAxis.tickRotation, config.xAxis.hideLabel, xAxisLabelRefs.current, axisUpdateKey])
|
|
206
211
|
|
|
207
|
-
// xMax and yMax
|
|
208
|
-
const xMax = width - runtime.yAxis.size - (visualizationType === 'Combo' ? config.yAxis.rightAxisSize : 0)
|
|
209
212
|
const yMax = initialHeight + forestRowsHeight
|
|
210
213
|
|
|
211
214
|
const isNoDataAvailable = config.filters?.length > 0 && data.length === 0
|
|
212
215
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
216
|
+
// Memoized data accessors to prevent unnecessary re-renders
|
|
217
|
+
const getXAxisData = useCallback(
|
|
218
|
+
d =>
|
|
219
|
+
isDateScale(config.runtime.xAxis)
|
|
220
|
+
? parseDate(d[config.runtime.originalXAxis.dataKey]).getTime()
|
|
221
|
+
: d[config.runtime.originalXAxis.dataKey],
|
|
222
|
+
[config.runtime.xAxis, config.runtime.originalXAxis.dataKey, parseDate]
|
|
223
|
+
)
|
|
224
|
+
const getYAxisData = useCallback((d, seriesKey) => d[seriesKey], [])
|
|
225
|
+
|
|
226
|
+
const xAxisDataMapped = useMemo(() => data.map(d => getXAxisData(d)), [data, getXAxisData])
|
|
227
|
+
|
|
228
|
+
// Get unique x-axis values (for cases where multiple series share the same x-axis value)
|
|
229
|
+
// This is important for brush filtering where we want to count unique dates, not total data points
|
|
230
|
+
const uniqueXAxisDataMapped = useMemo(() => {
|
|
231
|
+
const unique = new Set()
|
|
232
|
+
const result: any[] = []
|
|
233
|
+
for (const value of xAxisDataMapped) {
|
|
234
|
+
const key = value instanceof Date ? value.getTime() : typeof value === 'number' ? value : String(value)
|
|
235
|
+
if (!unique.has(key)) {
|
|
236
|
+
unique.add(key)
|
|
237
|
+
result.push(value)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return result
|
|
241
|
+
}, [xAxisDataMapped])
|
|
242
|
+
|
|
243
|
+
// Force update x axis ticks when filtering
|
|
244
|
+
useLayoutEffect(() => {
|
|
245
|
+
setAxisUpdateKey(prev => prev + 1)
|
|
246
|
+
}, [data.length, xAxisDataMapped?.[0], xAxisDataMapped?.[xAxisDataMapped.length - 1]])
|
|
247
|
+
|
|
219
248
|
const { yScaleRight, hasRightAxis } = useRightAxis({ config, yMax, data })
|
|
220
249
|
|
|
250
|
+
// State for computed y-axis width - allows re-render when horizontal bar label space is calculated
|
|
251
|
+
const [computedYAxisWidth, setComputedYAxisWidth] = useState<number | null>(null)
|
|
252
|
+
|
|
253
|
+
// Use computed width if available, otherwise fall back to config value
|
|
254
|
+
const yAxisWidth = computedYAxisWidth ?? Number(runtime.yAxis.size)
|
|
255
|
+
|
|
256
|
+
// Chart width calculation using the current y-axis width
|
|
257
|
+
const xMax = parentWidth - yAxisWidth - (hasRightAxis ? config.yAxis.rightAxisSize : 0)
|
|
258
|
+
|
|
221
259
|
const {
|
|
222
260
|
xScale,
|
|
223
261
|
yScale,
|
|
@@ -226,10 +264,9 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
226
264
|
g2xScale,
|
|
227
265
|
xScaleNoPadding,
|
|
228
266
|
xScaleAnnotation,
|
|
267
|
+
yScaleAnnotation,
|
|
229
268
|
min,
|
|
230
|
-
max
|
|
231
|
-
leftMax,
|
|
232
|
-
rightMax
|
|
269
|
+
max
|
|
233
270
|
} = useScales({
|
|
234
271
|
data,
|
|
235
272
|
tableData,
|
|
@@ -240,15 +277,49 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
240
277
|
existPositiveValue,
|
|
241
278
|
xAxisDataMapped,
|
|
242
279
|
yMax,
|
|
243
|
-
|
|
244
|
-
xMax:
|
|
245
|
-
parentWidth -
|
|
246
|
-
Number(config.orientation === 'horizontal' ? config.xAxis.size : config.yAxis.size) -
|
|
247
|
-
(hasRightAxis ? config.yAxis.rightAxisSize : 0),
|
|
280
|
+
xMax,
|
|
248
281
|
needsYAxisAutoPadding,
|
|
249
282
|
currentViewport
|
|
250
283
|
})
|
|
251
284
|
|
|
285
|
+
// Consolidated tick formatters
|
|
286
|
+
const { handleLeftTickFormatting, handleBottomTickFormatting } = useTickFormatters({
|
|
287
|
+
isLogarithmicAxis,
|
|
288
|
+
orientation,
|
|
289
|
+
visualizationType,
|
|
290
|
+
min,
|
|
291
|
+
max,
|
|
292
|
+
shouldAbbreviate
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
// Calculate category label space for horizontal bar charts
|
|
296
|
+
const categoryLabelSpace = useMemo(() => {
|
|
297
|
+
return calculateHorizontalBarCategoryLabelWidth({
|
|
298
|
+
yScale,
|
|
299
|
+
chartWidth: parentWidth,
|
|
300
|
+
formatDate,
|
|
301
|
+
parseDate,
|
|
302
|
+
tickLabelFont: GET_TEXT_WIDTH_FONT,
|
|
303
|
+
xAxisType: config.runtime.xAxis?.type,
|
|
304
|
+
labelPlacement: config.yAxis.labelPlacement
|
|
305
|
+
})
|
|
306
|
+
}, [isHorizontal, config.visualizationType, config.yAxis.labelPlacement, yScale, parentWidth])
|
|
307
|
+
|
|
308
|
+
const horizontalYAxisLabelSpace = runtime.yAxis.label && !config.hideYAxisLabel ? 30 : 0
|
|
309
|
+
|
|
310
|
+
// Update y-axis width state when computed value changes (for horizontal bar charts)
|
|
311
|
+
useEffect(() => {
|
|
312
|
+
if (isHorizontal && config.visualizationType === 'Bar') {
|
|
313
|
+
const newWidth = categoryLabelSpace + horizontalYAxisLabelSpace
|
|
314
|
+
if (newWidth !== computedYAxisWidth) {
|
|
315
|
+
setComputedYAxisWidth(newWidth)
|
|
316
|
+
}
|
|
317
|
+
} else if (computedYAxisWidth !== null) {
|
|
318
|
+
// Reset to null for non-horizontal bar charts so we use config value
|
|
319
|
+
setComputedYAxisWidth(null)
|
|
320
|
+
}
|
|
321
|
+
}, [isHorizontal, config.visualizationType, categoryLabelSpace, horizontalYAxisLabelSpace, computedYAxisWidth])
|
|
322
|
+
|
|
252
323
|
const [yTickCount, xTickCount] = ['yAxis', 'xAxis'].map(axis =>
|
|
253
324
|
countNumOfTicks({ axis, max, runtime, currentViewport, isHorizontal, data, config, min })
|
|
254
325
|
)
|
|
@@ -291,65 +362,19 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
291
362
|
const useDateSpanMonths = isDateTime && dateSpanMonths > xTickCount && !config.runtime.xAxis.manual
|
|
292
363
|
|
|
293
364
|
// GETTERS & FUNCTIONS
|
|
294
|
-
const handleLeftTickFormatting = (tick, index, ticks) => {
|
|
295
|
-
if (isLogarithmicAxis && tick === 0.1) {
|
|
296
|
-
//when logarithmic scale applied change value of first tick
|
|
297
|
-
tick = 0
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (config.data && !config.data[index] && visualizationType === 'Forest Plot') return
|
|
301
|
-
if (config.visualizationType === 'Forest Plot') return config.data[index][config.xAxis.dataKey]
|
|
302
|
-
if (isDateScale(runtime.yAxis)) return formatDate(parseDate(tick))
|
|
303
|
-
if (orientation === 'vertical' && max - min < 3 && !config.dataFormat?.roundTo)
|
|
304
|
-
return formatNumber(tick, 'left', shouldAbbreviate, false, false, '1', { index, length: ticks.length })
|
|
305
|
-
if (orientation === 'vertical') {
|
|
306
|
-
// TODO suggestion: pass all options as object key/values to allow for more flexibility
|
|
307
|
-
return formatNumber(tick, 'left', shouldAbbreviate, false, false, undefined, { index, length: ticks.length })
|
|
308
|
-
}
|
|
309
|
-
return tick
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const handleBottomTickFormatting = (tick, i, ticks) => {
|
|
313
|
-
if (isLogarithmicAxis && tick === 0.1) {
|
|
314
|
-
// when logarithmic scale applied change value FIRST of tick
|
|
315
|
-
tick = 0
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (isDateScale(runtime.xAxis) && config.visualizationType !== 'Forest Plot') {
|
|
319
|
-
return formatDate(tick, i, ticks)
|
|
320
|
-
}
|
|
321
|
-
if (orientation === 'horizontal' && config.visualizationType !== 'Forest Plot')
|
|
322
|
-
return formatNumber(tick, 'left', shouldAbbreviate)
|
|
323
|
-
if (config.xAxis.type === 'continuous' && config.visualizationType !== 'Forest Plot')
|
|
324
|
-
return formatNumber(tick, 'bottom', shouldAbbreviate)
|
|
325
|
-
if (config.visualizationType === 'Forest Plot')
|
|
326
|
-
return formatNumber(
|
|
327
|
-
tick,
|
|
328
|
-
'left',
|
|
329
|
-
config.dataFormat.abbreviated,
|
|
330
|
-
config.runtime.xAxis.prefix,
|
|
331
|
-
config.runtime.xAxis.suffix,
|
|
332
|
-
Number(config.dataFormat.roundTo)
|
|
333
|
-
)
|
|
334
|
-
return tick
|
|
335
|
-
}
|
|
336
|
-
|
|
337
365
|
const chartHasTooltipGuides = () => {
|
|
338
366
|
const { visualizationType } = config
|
|
339
367
|
if (visualizationType === 'Combo' && runtime.forecastingSeriesKeys > 0) return true
|
|
340
|
-
|
|
341
|
-
if (visualizationType === 'Line') return true
|
|
342
|
-
if (visualizationType === 'Bar') return true
|
|
343
|
-
return false
|
|
368
|
+
return TYPES_WITH_TOOLTIP_GUIDES.includes(visualizationType as any)
|
|
344
369
|
}
|
|
345
370
|
|
|
346
|
-
const getManualStep = () => {
|
|
371
|
+
const getManualStep = useCallback(() => {
|
|
347
372
|
let manualStep = config.xAxis.manualStep
|
|
348
373
|
if (config.xAxis.viewportStepCount && config.xAxis.viewportStepCount[currentViewport]) {
|
|
349
374
|
manualStep = config.xAxis.viewportStepCount[currentViewport]
|
|
350
375
|
}
|
|
351
376
|
return manualStep
|
|
352
|
-
}
|
|
377
|
+
}, [config.xAxis.manualStep, config.xAxis.viewportStepCount, currentViewport])
|
|
353
378
|
|
|
354
379
|
const smallMultiplesSync = useSmallMultipleSynchronization(xMax, yMax, getXValueFromCoordinate)
|
|
355
380
|
|
|
@@ -363,11 +388,17 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
363
388
|
y
|
|
364
389
|
})
|
|
365
390
|
|
|
366
|
-
|
|
391
|
+
// Warming Stripes handles its own sync at the rect level since
|
|
392
|
+
// getXValueFromCoordinate won't map correctly due to data sampling
|
|
393
|
+
if (visualizationType !== 'Warming Stripes') {
|
|
394
|
+
smallMultiplesSync.onMouseMove?.(event)
|
|
395
|
+
}
|
|
367
396
|
}
|
|
368
397
|
|
|
369
398
|
const onMouseLeave = () => {
|
|
370
|
-
|
|
399
|
+
if (visualizationType !== 'Warming Stripes') {
|
|
400
|
+
smallMultiplesSync.onMouseLeave?.()
|
|
401
|
+
}
|
|
371
402
|
}
|
|
372
403
|
|
|
373
404
|
// Use custom hook to provide programmatic tooltip control for small multiples
|
|
@@ -378,41 +409,23 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
378
409
|
setPoint,
|
|
379
410
|
setShowHoverLine,
|
|
380
411
|
handleTooltipMouseOver,
|
|
381
|
-
hideTooltip
|
|
412
|
+
hideTooltip,
|
|
413
|
+
setSynchronizedXValue
|
|
382
414
|
})
|
|
383
415
|
|
|
384
|
-
// EFFECTS
|
|
385
|
-
// Adjust padding on the right side of the chart to accommodate for overflow
|
|
386
|
-
useEffect(() => {
|
|
387
|
-
if (!parentRef.current || !parentWidth || !gridLineRefs.current.length) return
|
|
388
|
-
|
|
389
|
-
const [updatePadding, paddingToAdd] = handleAutoPaddingRight(parentRef, xAxisLabelRefs, parentWidth)
|
|
390
|
-
|
|
391
|
-
if (!updatePadding) return
|
|
392
|
-
|
|
393
|
-
parentRef.current.style.paddingRight = `${paddingToAdd}px`
|
|
394
|
-
// subtract padding from grid line's x1 value
|
|
395
|
-
gridLineRefs.current.forEach(gridLine => {
|
|
396
|
-
if (!gridLine) return
|
|
397
|
-
gridLine.setAttribute('x1', xMax - paddingToAdd)
|
|
398
|
-
})
|
|
399
|
-
}, [parentWidth, parentHeight, data])
|
|
400
|
-
|
|
401
416
|
// Make sure the chart is visible if in the editor
|
|
402
|
-
/* eslint-disable react-hooks/exhaustive-deps */
|
|
403
417
|
useEffect(() => {
|
|
404
418
|
const element = document.querySelector('.isEditor')
|
|
405
419
|
if (element) {
|
|
406
|
-
|
|
407
|
-
setAnimatedChart(prevState => true)
|
|
420
|
+
setAnimatedChart(true)
|
|
408
421
|
}
|
|
409
|
-
})
|
|
422
|
+
}, [])
|
|
410
423
|
|
|
411
424
|
// If the chart is in view, set to animate if it has not already played
|
|
412
425
|
useEffect(() => {
|
|
413
426
|
if (dataRef?.isIntersecting === true && config.animate) {
|
|
414
427
|
setTimeout(() => {
|
|
415
|
-
setAnimatedChart(
|
|
428
|
+
setAnimatedChart(true)
|
|
416
429
|
}, 500)
|
|
417
430
|
}
|
|
418
431
|
}, [dataRef?.isIntersecting, config.animate])
|
|
@@ -449,16 +462,22 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
449
462
|
const topLabelOnGridline = topYLabelRef.current && yAxis.labelsAboveGridlines
|
|
450
463
|
|
|
451
464
|
// Heights to add
|
|
452
|
-
|
|
453
|
-
const brushHeight = 25
|
|
454
|
-
const brushHeightWithMargin = config.xAxis.brushActive ? brushHeight + brushHeight : 0
|
|
455
465
|
const forestRowsHeight = isForestPlot ? config.data.length * forestPlot.rowHeight : 0
|
|
456
466
|
const topLabelOnGridlineHeight = topLabelOnGridline ? topYLabelRef.current.getBBox().height : 0
|
|
457
|
-
|
|
458
|
-
|
|
467
|
+
|
|
468
|
+
// SVG height (without brush)
|
|
469
|
+
const svgAdditionalHeight = axisBottomHeight + forestRowsHeight + topLabelOnGridlineHeight
|
|
470
|
+
const svgHeight = initialHeight + svgAdditionalHeight
|
|
471
|
+
|
|
472
|
+
// Parent container height (includes brush if active)
|
|
473
|
+
const brushHeightWithMargin = config.xAxis.brushActive ? BRUSH_HEIGHT + BRUSH_MARGIN : 0
|
|
474
|
+
const parentHeight = svgHeight + brushHeightWithMargin
|
|
475
|
+
|
|
459
476
|
if (!parentRef.current) return
|
|
477
|
+
parentRef.current.style.height = `${parentHeight}px`
|
|
460
478
|
|
|
461
|
-
|
|
479
|
+
// Set the calculated SVG height via state to ensure it's used on render
|
|
480
|
+
setCalculatedSvgHeight(svgHeight)
|
|
462
481
|
|
|
463
482
|
/* Adding text above the top gridline overflows the bounds of the svg.
|
|
464
483
|
To accommodate for this we need to...
|
|
@@ -472,22 +491,14 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
472
491
|
const svg = internalSvgRef.current
|
|
473
492
|
if (!svg) return
|
|
474
493
|
const parentWidthFromRef = parentRef.current.getBoundingClientRect().width
|
|
475
|
-
svg.setAttribute('viewBox', `0 ${-topLabelOnGridlineHeight} ${parentWidthFromRef} ${
|
|
494
|
+
svg.setAttribute('viewBox', `0 ${-topLabelOnGridlineHeight} ${parentWidthFromRef} ${svgHeight}`)
|
|
476
495
|
|
|
477
496
|
// translate legend match viewBox-adjusted height
|
|
478
497
|
if (!legendRef.current) return
|
|
479
498
|
const legendIsLeftOrRight =
|
|
480
499
|
legend?.position !== 'top' && legend?.position !== 'bottom' && !isLegendWrapViewport(currentViewport)
|
|
481
500
|
legendRef.current.style.transform = legendIsLeftOrRight ? `translateY(${topLabelOnGridlineHeight}px)` : 'none'
|
|
482
|
-
}, [
|
|
483
|
-
axisBottomRef.current,
|
|
484
|
-
config,
|
|
485
|
-
bottomLabelStart,
|
|
486
|
-
config.xAxis.brushActive,
|
|
487
|
-
currentViewport,
|
|
488
|
-
topYLabelRef.current,
|
|
489
|
-
initialHeight
|
|
490
|
-
])
|
|
501
|
+
}, [axisBottomRef.current, config, config.xAxis.brushActive, currentViewport, topYLabelRef.current, initialHeight])
|
|
491
502
|
|
|
492
503
|
useEffect(() => {
|
|
493
504
|
if (!tooltipOpen) return
|
|
@@ -501,13 +512,13 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
501
512
|
|
|
502
513
|
const rightSideRemainingSpace = parentWidth - dataXPosition
|
|
503
514
|
|
|
504
|
-
const rightSide = rightSideRemainingSpace <= tooltipWidth && dataXPosition > parentWidth / 2 -
|
|
505
|
-
const maxWidth = rightSide ? dataXPosition -
|
|
515
|
+
const rightSide = rightSideRemainingSpace <= tooltipWidth && dataXPosition > parentWidth / 2 - TOOLTIP_EDGE_BUFFER
|
|
516
|
+
const maxWidth = rightSide ? dataXPosition - TOOLTIP_EDGE_BUFFER : parentWidth - (dataXPosition + TOOLTIP_OFFSET)
|
|
506
517
|
tooltipRef.current.node.style.maxWidth = `${maxWidth}px`
|
|
507
518
|
}, [tooltipOpen, tooltipData])
|
|
508
519
|
|
|
509
|
-
// Check if small multiples are enabled - if so, render SmallMultiples instead
|
|
510
|
-
if (config.smallMultiples?.mode) {
|
|
520
|
+
// Check if small multiples are enabled and supported - if so, render SmallMultiples instead
|
|
521
|
+
if (config.smallMultiples?.mode && visSupportsSmallMultiples()) {
|
|
511
522
|
return (
|
|
512
523
|
<SmallMultiples
|
|
513
524
|
config={config}
|
|
@@ -519,151 +530,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
519
530
|
)
|
|
520
531
|
}
|
|
521
532
|
|
|
522
|
-
|
|
523
|
-
const generatePairedBarAxis = () => {
|
|
524
|
-
const axisMaxHeight = bottomLabelStart + BOTTOM_LABEL_PADDING
|
|
525
|
-
|
|
526
|
-
const getTickPositions = (ticks, xScale) => {
|
|
527
|
-
if (!ticks.length) return false
|
|
528
|
-
// filter out first index
|
|
529
|
-
const filteredTicks = ticks.filter(tick => tick.index !== 0)
|
|
530
|
-
const numberOfTicks = filteredTicks?.length
|
|
531
|
-
const xMaxHalf = xScale.range()[0] || xMax / 2
|
|
532
|
-
const tickWidthAll = filteredTicks.map(tick =>
|
|
533
|
-
getTextWidth(formatNumber(tick.value, 'left'), GET_TEXT_WIDTH_FONT)
|
|
534
|
-
)
|
|
535
|
-
const accumulator = 100
|
|
536
|
-
const sumOfTickWidth = tickWidthAll.reduce((a, b) => a + b, accumulator)
|
|
537
|
-
const spaceBetweenEachTick = (xMaxHalf - sumOfTickWidth) / numberOfTicks
|
|
538
|
-
// Determine the position of each tick
|
|
539
|
-
let positions = [0]
|
|
540
|
-
for (let i = 1; i < tickWidthAll.length; i++) {
|
|
541
|
-
positions[i] = positions[i - 1] + tickWidthAll[i - 1] + spaceBetweenEachTick
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
// Check if ticks are overlapping
|
|
545
|
-
let isTicksOverlapping = false
|
|
546
|
-
tickWidthAll.forEach((_, i) => {
|
|
547
|
-
if (positions[i] + tickWidthAll[i] > positions[i + 1]) {
|
|
548
|
-
isTicksOverlapping = true
|
|
549
|
-
return
|
|
550
|
-
}
|
|
551
|
-
})
|
|
552
|
-
return isTicksOverlapping
|
|
553
|
-
}
|
|
554
|
-
return (
|
|
555
|
-
<>
|
|
556
|
-
<AxisBottom
|
|
557
|
-
top={yMax}
|
|
558
|
-
left={Number(runtime.yAxis.size)}
|
|
559
|
-
label={runtime.xAxis.label}
|
|
560
|
-
tickFormat={isDateScale(runtime.xAxis) ? formatDate : formatNumber}
|
|
561
|
-
scale={g1xScale}
|
|
562
|
-
stroke='#333'
|
|
563
|
-
tickStroke='#333'
|
|
564
|
-
numTicks={runtime.xAxis.numTicks || undefined}
|
|
565
|
-
>
|
|
566
|
-
{props => {
|
|
567
|
-
return (
|
|
568
|
-
<Group className='bottom-axis'>
|
|
569
|
-
{props.ticks.map((tick, i) => {
|
|
570
|
-
const isTicksOverlapping = getTickPositions(props.ticks, g1xScale)
|
|
571
|
-
const maxTickRotation = Number(config.xAxis.maxTickRotation) || 90
|
|
572
|
-
const isResponsiveTicks = config.isResponsiveTicks && isTicksOverlapping
|
|
573
|
-
const angle =
|
|
574
|
-
tick.index !== 0 && (isResponsiveTicks ? maxTickRotation : Number(config.yAxis.tickRotation))
|
|
575
|
-
const textAnchor = angle && tick.index !== 0 ? 'end' : 'middle'
|
|
576
|
-
|
|
577
|
-
return (
|
|
578
|
-
<Group key={`vx-tick-${tick.value}-${i}`} className={'vx-axis-tick'}>
|
|
579
|
-
{!runtime.yAxis.hideTicks && <Line from={tick.from} to={tick.to} stroke='#333' />}
|
|
580
|
-
{!runtime.yAxis.hideLabel && (
|
|
581
|
-
<Text // prettier-ignore
|
|
582
|
-
innerRef={el => (xAxisLabelRefs.current[i] = el)}
|
|
583
|
-
x={tick.to.x}
|
|
584
|
-
y={tick.to.y}
|
|
585
|
-
angle={-angle}
|
|
586
|
-
verticalAnchor={angle ? 'middle' : 'start'}
|
|
587
|
-
textAnchor={textAnchor}
|
|
588
|
-
fontSize={tickLabelFontSize}
|
|
589
|
-
>
|
|
590
|
-
{formatNumber(tick.value, 'left')}
|
|
591
|
-
</Text>
|
|
592
|
-
)}
|
|
593
|
-
</Group>
|
|
594
|
-
)
|
|
595
|
-
})}
|
|
596
|
-
{!runtime.yAxis.hideAxis && <Line from={props.axisFromPoint} to={props.axisToPoint} stroke='#333' />}
|
|
597
|
-
</Group>
|
|
598
|
-
)
|
|
599
|
-
}}
|
|
600
|
-
</AxisBottom>
|
|
601
|
-
<AxisBottom
|
|
602
|
-
innerRef={axisBottomRef}
|
|
603
|
-
top={yMax}
|
|
604
|
-
left={Number(runtime.yAxis.size)}
|
|
605
|
-
label={runtime.xAxis.label}
|
|
606
|
-
tickFormat={
|
|
607
|
-
isDateScale(runtime.xAxis) ? formatDate : runtime.xAxis.dataKey !== 'Year' ? formatNumber : tick => tick
|
|
608
|
-
}
|
|
609
|
-
scale={g2xScale}
|
|
610
|
-
stroke='#333'
|
|
611
|
-
tickStroke='#333'
|
|
612
|
-
numTicks={runtime.xAxis.numTicks || undefined}
|
|
613
|
-
>
|
|
614
|
-
{props => {
|
|
615
|
-
return (
|
|
616
|
-
<>
|
|
617
|
-
<Group className='bottom-axis'>
|
|
618
|
-
{props.ticks.map((tick, i) => {
|
|
619
|
-
const isTicksOverlapping = getTickPositions(props.ticks, g2xScale)
|
|
620
|
-
const maxTickRotation = Number(config.xAxis.maxTickRotation) || 90
|
|
621
|
-
const isResponsiveTicks = config.isResponsiveTicks && isTicksOverlapping
|
|
622
|
-
const angle =
|
|
623
|
-
tick.index !== 0 && (isResponsiveTicks ? maxTickRotation : Number(config.yAxis.tickRotation))
|
|
624
|
-
const textAnchor = angle && tick.index !== 0 ? 'end' : 'middle'
|
|
625
|
-
if (!i) return <></> // skip first tick to avoid overlapping 0's
|
|
626
|
-
return (
|
|
627
|
-
<Group key={`vx-tick-${tick.value}-${i}`} className={'vx-axis-tick'}>
|
|
628
|
-
{!runtime.yAxis.hideTicks && <Line from={tick.from} to={tick.to} stroke='#333' />}
|
|
629
|
-
{!runtime.yAxis.hideLabel && (
|
|
630
|
-
<Text // prettier-ignore
|
|
631
|
-
x={tick.to.x}
|
|
632
|
-
y={tick.to.y + X_TICK_LABEL_PADDING}
|
|
633
|
-
angle={-angle}
|
|
634
|
-
verticalAnchor={angle ? 'middle' : 'start'}
|
|
635
|
-
textAnchor={textAnchor}
|
|
636
|
-
fontSize={tickLabelFontSize}
|
|
637
|
-
>
|
|
638
|
-
{formatNumber(tick.value, 'left')}
|
|
639
|
-
</Text>
|
|
640
|
-
)}
|
|
641
|
-
</Group>
|
|
642
|
-
)
|
|
643
|
-
})}
|
|
644
|
-
{!runtime.yAxis.hideAxis && <Line from={props.axisFromPoint} to={props.axisToPoint} stroke='#333' />}
|
|
645
|
-
</Group>
|
|
646
|
-
<Group>
|
|
647
|
-
<Text
|
|
648
|
-
className='x-axis-title-label'
|
|
649
|
-
x={xMax / 2}
|
|
650
|
-
y={axisMaxHeight}
|
|
651
|
-
stroke='#333'
|
|
652
|
-
textAnchor={'middle'}
|
|
653
|
-
verticalAnchor='start'
|
|
654
|
-
fontSize={axisLabelFontSize}
|
|
655
|
-
>
|
|
656
|
-
{!config.hideXAxisLabel ? runtime.xAxis.label : null}
|
|
657
|
-
</Text>
|
|
658
|
-
</Group>
|
|
659
|
-
</>
|
|
660
|
-
)
|
|
661
|
-
}}
|
|
662
|
-
</AxisBottom>
|
|
663
|
-
</>
|
|
664
|
-
)
|
|
665
|
-
}
|
|
666
|
-
return isNaN(width) ? (
|
|
533
|
+
return isNaN(parentWidth) ? (
|
|
667
534
|
<React.Fragment></React.Fragment>
|
|
668
535
|
) : (
|
|
669
536
|
<ErrorBoundary component='LinearChart'>
|
|
@@ -676,7 +543,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
676
543
|
ref={internalSvgRef}
|
|
677
544
|
onMouseMove={onMouseMove}
|
|
678
545
|
width={parentWidth + config.yAxis.rightAxisSize}
|
|
679
|
-
height={isNoDataAvailable ? 1 : parentHeight}
|
|
546
|
+
height={isNoDataAvailable ? 1 : calculatedSvgHeight ?? parentHeight}
|
|
680
547
|
className={`linear ${config.animate ? 'animated' : ''} ${animatedChart && config.animate ? 'animate' : ''} ${
|
|
681
548
|
debugSvg && 'debug'
|
|
682
549
|
} ${isDraggingAnnotation && 'dragging-annotation'}`}
|
|
@@ -695,229 +562,74 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
695
562
|
>
|
|
696
563
|
{!isDraggingAnnotation && <Bar width={parentWidth} height={initialHeight} fill={'transparent'}></Bar>}{' '}
|
|
697
564
|
{/* GRID LINES */}
|
|
698
|
-
{/* Actual
|
|
699
|
-
{!
|
|
700
|
-
<
|
|
701
|
-
scale={yScale}
|
|
702
|
-
left={Number(runtime.yAxis.size) - config.yAxis.axisPadding}
|
|
703
|
-
numTicks={handleNumTicks}
|
|
704
|
-
>
|
|
705
|
-
{props => {
|
|
706
|
-
const axisCenter =
|
|
707
|
-
config.orientation === 'horizontal'
|
|
708
|
-
? Math.abs(props.axisToPoint.y - props.axisFromPoint.y) / 2
|
|
709
|
-
: (props.axisFromPoint.y - props.axisToPoint.y) / 2
|
|
710
|
-
return (
|
|
711
|
-
<Group className='left-axis'>
|
|
712
|
-
{props.ticks.map((tick, i) => {
|
|
713
|
-
const showTicks = String(tick.value).startsWith('1') || tick.value === 0.1 ? 'block' : 'none'
|
|
714
|
-
const hideFirstGridLine = tick.index === 0 && tick.value === 0 && config.xAxis.hideAxis
|
|
715
|
-
|
|
716
|
-
return (
|
|
717
|
-
<Group key={`vx-tick-${tick.value}-${i}`} className={'vx-axis-tick'}>
|
|
718
|
-
{runtime.yAxis.gridLines && !hideFirstGridLine ? (
|
|
719
|
-
<Line
|
|
720
|
-
innerRef={el => (gridLineRefs.current[i] = el)}
|
|
721
|
-
key={`${tick.value}--hide-hideGridLines`}
|
|
722
|
-
display={(isLogarithmicAxis && showTicks).toString()}
|
|
723
|
-
from={{ x: tick.from.x + xMax, y: tick.from.y }}
|
|
724
|
-
to={tick.from}
|
|
725
|
-
stroke='#d6d6d6'
|
|
726
|
-
/>
|
|
727
|
-
) : (
|
|
728
|
-
''
|
|
729
|
-
)}
|
|
730
|
-
</Group>
|
|
731
|
-
)
|
|
732
|
-
})}
|
|
733
|
-
<Text
|
|
734
|
-
className='y-label'
|
|
735
|
-
textAnchor='middle'
|
|
736
|
-
verticalAnchor='start'
|
|
737
|
-
transform={`translate(${-1 * runtime.yAxis.size + yLabelOffset}, ${axisCenter}) rotate(-90)`}
|
|
738
|
-
fontWeight='bold'
|
|
739
|
-
fill={config.yAxis.labelColor}
|
|
740
|
-
fontSize={axisLabelFontSize}
|
|
741
|
-
>
|
|
742
|
-
{!config.hideYAxisLabel ? props.label : null}
|
|
743
|
-
</Text>
|
|
744
|
-
</Group>
|
|
745
|
-
)
|
|
746
|
-
}}
|
|
747
|
-
</AxisLeft>
|
|
748
|
-
)}
|
|
749
|
-
{visualizationType === 'Paired Bar' && generatePairedBarAxis()}
|
|
750
|
-
{visualizationType === 'Deviation Bar' && config.runtime.series?.length === 1 && (
|
|
751
|
-
<DeviationBar animatedChart={animatedChart} xScale={xScale} yScale={yScale} width={xMax} height={yMax} />
|
|
752
|
-
)}
|
|
753
|
-
{visualizationType === 'Paired Bar' && <PairedBarChart originalWidth={width} width={xMax} height={yMax} />}
|
|
754
|
-
{visualizationType === 'Scatter Plot' && (
|
|
755
|
-
<ScatterPlot
|
|
756
|
-
xScale={xScale}
|
|
565
|
+
{/* Actual LeftAxis is drawn after visualization */}
|
|
566
|
+
{!TYPES_WITHOUT_GRID.includes(visualizationType as any) && config.yAxis.type !== 'categorical' && (
|
|
567
|
+
<LeftAxisGridlines
|
|
757
568
|
yScale={yScale}
|
|
758
|
-
getXAxisData={getXAxisData}
|
|
759
|
-
getYAxisData={getYAxisData}
|
|
760
|
-
xMax={xMax}
|
|
761
|
-
yMax={yMax}
|
|
762
|
-
handleTooltipMouseOver={handleTooltipMouseOver}
|
|
763
|
-
handleTooltipMouseOff={handleTooltipMouseOff}
|
|
764
|
-
handleTooltipClick={handleTooltipClick}
|
|
765
|
-
tooltipData={tooltipData}
|
|
766
|
-
showTooltip={showTooltip}
|
|
767
|
-
/>
|
|
768
|
-
)}
|
|
769
|
-
{visualizationType === 'Box Plot' && config.orientation === 'vertical' && (
|
|
770
|
-
<BoxPlotVertical
|
|
771
|
-
seriesScale={seriesScale}
|
|
772
569
|
xMax={xMax}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
yScale={yScale}
|
|
778
|
-
/>
|
|
779
|
-
)}
|
|
780
|
-
{visualizationType === 'Box Plot' && config.orientation === 'horizontal' && (
|
|
781
|
-
<BoxPlotHorizontal
|
|
782
|
-
seriesScale={seriesScale}
|
|
783
|
-
xMax={xMax}
|
|
784
|
-
yMax={yMax}
|
|
785
|
-
min={min}
|
|
786
|
-
max={max}
|
|
787
|
-
xScale={xScale}
|
|
788
|
-
yScale={yScale}
|
|
570
|
+
yAxisWidth={yAxisWidth}
|
|
571
|
+
numTicks={handleNumTicks}
|
|
572
|
+
yLabelOffset={yLabelOffset}
|
|
573
|
+
axisLabelFontSize={axisLabelFontSize}
|
|
789
574
|
/>
|
|
790
575
|
)}
|
|
791
|
-
{
|
|
792
|
-
|
|
793
|
-
<
|
|
794
|
-
xScale
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
576
|
+
{/* Horizontal chart grid lines */}
|
|
577
|
+
{runtime.xAxis.gridLines && orientation === 'horizontal' && (
|
|
578
|
+
<Group left={yAxisWidth}>
|
|
579
|
+
{xScale.ticks(xTickCount).map((tickValue, i) => {
|
|
580
|
+
const tickPosition = xScale(tickValue)
|
|
581
|
+
return (
|
|
582
|
+
<Line
|
|
583
|
+
key={`horizontal-gridline-${tickValue}-${i}`}
|
|
584
|
+
from={{ x: tickPosition, y: 0 }}
|
|
585
|
+
to={{ x: tickPosition, y: yMax }}
|
|
586
|
+
stroke='#d6d6d6'
|
|
587
|
+
/>
|
|
588
|
+
)
|
|
589
|
+
})}
|
|
590
|
+
</Group>
|
|
806
591
|
)}
|
|
807
|
-
{
|
|
808
|
-
<
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
seriesScale={seriesScale}
|
|
812
|
-
xMax={xMax}
|
|
592
|
+
{visualizationType === 'Paired Bar' && (
|
|
593
|
+
<PairedBarAxis
|
|
594
|
+
g1xScale={g1xScale}
|
|
595
|
+
g2xScale={g2xScale}
|
|
813
596
|
yMax={yMax}
|
|
814
|
-
getXAxisData={getXAxisData}
|
|
815
|
-
getYAxisData={getYAxisData}
|
|
816
|
-
animatedChart={animatedChart}
|
|
817
|
-
visible={animatedChart}
|
|
818
|
-
handleTooltipMouseOver={handleTooltipMouseOver}
|
|
819
|
-
handleTooltipMouseOff={handleTooltipMouseOff}
|
|
820
|
-
handleTooltipClick={handleTooltipClick}
|
|
821
|
-
tooltipData={tooltipData}
|
|
822
|
-
showTooltip={showTooltip}
|
|
823
|
-
chartRef={svgRef}
|
|
824
|
-
/>
|
|
825
|
-
)}
|
|
826
|
-
{(visualizationType === 'Combo' || visualizationType === 'Bump Chart') && (
|
|
827
|
-
<LineChart
|
|
828
|
-
xScale={xScale}
|
|
829
|
-
yScale={yScale}
|
|
830
|
-
getXAxisData={getXAxisData}
|
|
831
|
-
getYAxisData={getYAxisData}
|
|
832
597
|
xMax={xMax}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
/>
|
|
841
|
-
)}
|
|
842
|
-
{/* Line chart */}
|
|
843
|
-
{/* TODO: Make this just line or combo? */}
|
|
844
|
-
{!['Paired Bar', 'Box Plot', 'Area Chart', 'Scatter Plot', 'Deviation Bar', 'Forecasting', 'Bar'].includes(
|
|
845
|
-
visualizationType
|
|
846
|
-
) &&
|
|
847
|
-
!convertLineToBarGraph && (
|
|
848
|
-
<>
|
|
849
|
-
<LineChart
|
|
850
|
-
xScale={xScale}
|
|
851
|
-
yScale={yScale}
|
|
852
|
-
getXAxisData={getXAxisData}
|
|
853
|
-
getYAxisData={getYAxisData}
|
|
854
|
-
xMax={xMax}
|
|
855
|
-
yMax={yMax}
|
|
856
|
-
seriesStyle={config.runtime.series}
|
|
857
|
-
tooltipData={tooltipData}
|
|
858
|
-
handleTooltipMouseOver={handleTooltipMouseOver}
|
|
859
|
-
handleTooltipMouseOff={handleTooltipMouseOff}
|
|
860
|
-
/>
|
|
861
|
-
</>
|
|
862
|
-
)}
|
|
863
|
-
{(visualizationType === 'Forecasting' || visualizationType === 'Combo') && (
|
|
864
|
-
<Forecasting
|
|
865
|
-
showTooltip={showTooltip}
|
|
866
|
-
tooltipData={tooltipData}
|
|
867
|
-
xScale={xScale}
|
|
868
|
-
yScale={yScale}
|
|
869
|
-
width={xMax}
|
|
870
|
-
height={yMax}
|
|
871
|
-
xScaleNoPadding={xScaleNoPadding}
|
|
872
|
-
chartRef={svgRef}
|
|
873
|
-
handleTooltipMouseOver={handleTooltipMouseOver}
|
|
874
|
-
handleTooltipMouseOff={handleTooltipMouseOff}
|
|
875
|
-
/>
|
|
876
|
-
)}
|
|
877
|
-
{visualizationType === 'Forest Plot' && (
|
|
878
|
-
<ForestPlot
|
|
879
|
-
xScale={xScale}
|
|
880
|
-
yScale={yScale}
|
|
881
|
-
seriesScale={seriesScale}
|
|
882
|
-
width={width}
|
|
883
|
-
height={forestHeight}
|
|
884
|
-
getXAxisData={getXAxisData}
|
|
885
|
-
getYAxisData={getYAxisData}
|
|
886
|
-
animatedChart={animatedChart}
|
|
887
|
-
visible={animatedChart}
|
|
888
|
-
handleTooltipMouseOver={handleTooltipMouseOver}
|
|
889
|
-
handleTooltipMouseOff={handleTooltipMouseOff}
|
|
890
|
-
handleTooltipClick={handleTooltipClick}
|
|
891
|
-
tooltipData={tooltipData}
|
|
892
|
-
showTooltip={showTooltip}
|
|
893
|
-
chartRef={svgRef}
|
|
894
|
-
config={config}
|
|
895
|
-
forestPlotRightLabelRef={forestPlotRightLabelRef}
|
|
598
|
+
yAxisWidth={yAxisWidth}
|
|
599
|
+
bottomLabelStart={bottomLabelStart}
|
|
600
|
+
tickLabelFontSize={tickLabelFontSize}
|
|
601
|
+
axisLabelFontSize={axisLabelFontSize}
|
|
602
|
+
axisBottomRef={axisBottomRef}
|
|
603
|
+
xAxisLabelRefs={xAxisLabelRefs}
|
|
604
|
+
tickLabelFont={GET_TEXT_WIDTH_FONT}
|
|
896
605
|
/>
|
|
897
606
|
)}
|
|
898
|
-
{/*
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
607
|
+
{/* Visualization Renderer - handles all chart type rendering */}
|
|
608
|
+
<VisualizationRenderer
|
|
609
|
+
xScale={xScale}
|
|
610
|
+
yScale={yScale}
|
|
611
|
+
xMax={xMax}
|
|
612
|
+
yMax={yMax}
|
|
613
|
+
seriesScale={seriesScale}
|
|
614
|
+
xScaleNoPadding={xScaleNoPadding}
|
|
615
|
+
min={min}
|
|
616
|
+
max={max}
|
|
617
|
+
parentWidth={parentWidth}
|
|
618
|
+
yAxisWidth={yAxisWidth}
|
|
619
|
+
forestHeight={forestHeight}
|
|
620
|
+
animatedChart={animatedChart}
|
|
621
|
+
tooltipData={tooltipData}
|
|
622
|
+
showTooltip={showTooltip}
|
|
623
|
+
handleTooltipMouseOver={handleTooltipMouseOver}
|
|
624
|
+
handleTooltipMouseOff={handleTooltipMouseOff}
|
|
625
|
+
handleTooltipClick={handleTooltipClick}
|
|
626
|
+
getXAxisData={getXAxisData}
|
|
627
|
+
getYAxisData={getYAxisData}
|
|
628
|
+
svgRef={svgRef}
|
|
629
|
+
forestPlotRightLabelRef={forestPlotRightLabelRef}
|
|
630
|
+
synchronizedXValue={synchronizedXValue}
|
|
631
|
+
/>
|
|
632
|
+
{/* Brush moved to separate overlay - no longer in main SVG */}
|
|
921
633
|
{/* y anchors */}
|
|
922
634
|
{config.yAxis.anchors &&
|
|
923
635
|
config.yAxis.anchors.map((anchor, index) => {
|
|
@@ -926,11 +638,10 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
926
638
|
|
|
927
639
|
if (!anchor.value) return
|
|
928
640
|
if (config.yAxis.labelPlacement === 'Below Bar') {
|
|
929
|
-
|
|
930
|
-
|
|
641
|
+
middleOffset =
|
|
642
|
+
BELOW_BAR_TEXT_OFFSET + Number(config.series.length * config.barHeight) / config.series.length
|
|
931
643
|
} else {
|
|
932
|
-
|
|
933
|
-
middleOffset = paddingOffset
|
|
644
|
+
middleOffset = LABEL_PADDING_OFFSET
|
|
934
645
|
}
|
|
935
646
|
|
|
936
647
|
if (!position) return
|
|
@@ -942,8 +653,8 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
942
653
|
strokeDasharray={handleLineType(anchor.lineStyle)}
|
|
943
654
|
stroke={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
|
|
944
655
|
className='anchor-y'
|
|
945
|
-
from={{ x:
|
|
946
|
-
to={{ x:
|
|
656
|
+
from={{ x: Number(runtime.yAxis.size), y: position - middleOffset }}
|
|
657
|
+
to={{ x: Number(runtime.yAxis.size) + Number(xMax), y: position - middleOffset }}
|
|
947
658
|
/>
|
|
948
659
|
)
|
|
949
660
|
})}
|
|
@@ -996,12 +707,12 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
996
707
|
hideTooltip={hideTooltip}
|
|
997
708
|
tooltipData={tooltipData}
|
|
998
709
|
yMax={yMax}
|
|
999
|
-
|
|
710
|
+
xMax={xMax}
|
|
1000
711
|
/>
|
|
1001
712
|
)}
|
|
1002
713
|
{isNoDataAvailable && (
|
|
1003
714
|
<Text
|
|
1004
|
-
x={
|
|
715
|
+
x={yAxisWidth + Number(xMax / 2)}
|
|
1005
716
|
y={initialHeight / 2 - (config.xAxis.padding || 0) / 2}
|
|
1006
717
|
textAnchor='middle'
|
|
1007
718
|
>
|
|
@@ -1014,387 +725,65 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
1014
725
|
<HoverLine xMax={xMax} yMax={yMax} point={point} tooltipData={tooltipData} orientation='vertical' />
|
|
1015
726
|
</>
|
|
1016
727
|
)}
|
|
1017
|
-
<Group left={
|
|
728
|
+
<Group left={yAxisWidth}>
|
|
1018
729
|
<Annotation.Draggable
|
|
1019
730
|
xScale={xScale}
|
|
1020
731
|
yScale={yScale}
|
|
1021
732
|
xScaleAnnotation={xScaleAnnotation}
|
|
733
|
+
yScaleAnnotation={yScaleAnnotation}
|
|
1022
734
|
xMax={xMax}
|
|
735
|
+
yMax={yMax}
|
|
736
|
+
seriesScale={seriesScale}
|
|
1023
737
|
svgRef={svgRef}
|
|
1024
738
|
onDragStateChange={handleDragStateChange}
|
|
1025
739
|
/>
|
|
1026
740
|
</Group>
|
|
1027
741
|
{/* Highlighted regions */}
|
|
1028
742
|
{/* Y axis */}
|
|
1029
|
-
{
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
from={props.axisFromPoint}
|
|
1051
|
-
to={
|
|
1052
|
-
runtime.horizontal
|
|
1053
|
-
? {
|
|
1054
|
-
x: 0,
|
|
1055
|
-
y:
|
|
1056
|
-
config.visualizationType === 'Forest Plot' ? parentHeight : Number(heights.horizontal)
|
|
1057
|
-
}
|
|
1058
|
-
: props.axisToPoint
|
|
1059
|
-
}
|
|
1060
|
-
stroke='#000'
|
|
1061
|
-
/>
|
|
1062
|
-
)}
|
|
1063
|
-
{orientation === 'vertical' && yScale.domain()[0] < 0 && (
|
|
1064
|
-
// draw from the Left of the chart …
|
|
1065
|
-
<Line
|
|
1066
|
-
from={{ x: props.axisFromPoint.x, y: yScale(0) }}
|
|
1067
|
-
to={{ x: xMax, y: yScale(0) }}
|
|
1068
|
-
stroke='#333'
|
|
1069
|
-
/>
|
|
1070
|
-
)}
|
|
1071
|
-
{orientation === 'horizontal' && xScale.domain()[0] < 0 && (
|
|
1072
|
-
<Line
|
|
1073
|
-
// draw from the top of the char
|
|
1074
|
-
from={{ x: xScale(0), y: 0 }}
|
|
1075
|
-
to={{ x: xScale(0), y: yMax }}
|
|
1076
|
-
stroke='#333'
|
|
1077
|
-
/>
|
|
1078
|
-
)}
|
|
1079
|
-
{visualizationType === 'Bar' && orientation === 'horizontal' && xScale.domain()[0] < 0 && (
|
|
1080
|
-
<Line
|
|
1081
|
-
from={{ x: xScale(0), y: 0 }}
|
|
1082
|
-
to={{ x: xScale(0), y: yMax }}
|
|
1083
|
-
stroke='#333'
|
|
1084
|
-
strokeWidth={2}
|
|
1085
|
-
/>
|
|
1086
|
-
)}
|
|
1087
|
-
{props.ticks.map((tick, i) => {
|
|
1088
|
-
const minY = props.ticks[0].to.y
|
|
1089
|
-
const barMinHeight = 15 // 15 is the min height for bars by default
|
|
1090
|
-
const showTicks = String(tick.value).startsWith('1') || tick.value === 0.1 ? 'block' : 'none'
|
|
1091
|
-
const tickLength = showTicks === 'block' ? 7 : 0
|
|
1092
|
-
const to = { x: tick.to.x - tickLength, y: tick.to.y }
|
|
1093
|
-
|
|
1094
|
-
// Vertical value/suffix vars
|
|
1095
|
-
const lastTick = props.ticks.length - 1 === i
|
|
1096
|
-
const useInlineLabel = lastTick && inlineLabel
|
|
1097
|
-
const hideTopTick = lastTick && inlineLabel && !inlineLabelHasNoSpace
|
|
1098
|
-
const valueOnLinePadding = hideAxis ? -8 : -12
|
|
1099
|
-
const labelXPadding = labelsAboveGridlines ? valueOnLinePadding : TICK_LABEL_MARGIN_RIGHT
|
|
1100
|
-
const labelYPadding = labelsAboveGridlines ? 4 : 0
|
|
1101
|
-
const labelX = tick.to.x - labelXPadding
|
|
1102
|
-
const labelY = tick.to.y - labelYPadding
|
|
1103
|
-
const labelVerticalAnchor = labelsAboveGridlines ? 'end' : 'middle'
|
|
1104
|
-
const combineDomInlineLabelWithValue = inlineLabel && labelsAboveGridlines && lastTick
|
|
1105
|
-
const formattedValue = useInlineLabel
|
|
1106
|
-
? String(tick?.formattedValue || '').replace(config.dataFormat.suffix, '')
|
|
1107
|
-
: tick?.formattedValue
|
|
1108
|
-
|
|
1109
|
-
return (
|
|
1110
|
-
<Group key={`vx-tick-${tick.value}-${i}`} className={'vx-axis-tick'}>
|
|
1111
|
-
{!runtime.yAxis.hideTicks && !labelsAboveGridlines && !hideTopTick && (
|
|
1112
|
-
<Line
|
|
1113
|
-
key={`${tick.value}--hide-hideTicks`}
|
|
1114
|
-
from={tick.from}
|
|
1115
|
-
to={isLogarithmicAxis ? to : tick.to}
|
|
1116
|
-
stroke={config.yAxis.tickColor}
|
|
1117
|
-
display={orientation === 'horizontal' ? 'none' : 'block'}
|
|
1118
|
-
fontSize={tickLabelFontSize}
|
|
1119
|
-
/>
|
|
1120
|
-
)}
|
|
1121
|
-
|
|
1122
|
-
{orientation === 'horizontal' &&
|
|
1123
|
-
visualizationType === 'Box Plot' &&
|
|
1124
|
-
config.yAxis.labelPlacement === 'On Date/Category Axis' &&
|
|
1125
|
-
!config.yAxis.hideLabel && (
|
|
1126
|
-
<Text
|
|
1127
|
-
x={tick.to.x}
|
|
1128
|
-
y={yScale(tick.value) + yScale.bandwidth() / 2}
|
|
1129
|
-
transform={`rotate(${
|
|
1130
|
-
config.orientation === 'horizontal' ? config.runtime.yAxis.tickRotation || 0 : 0
|
|
1131
|
-
}, ${tick.to.x}, ${tick.to.y})`}
|
|
1132
|
-
verticalAnchor={'middle'}
|
|
1133
|
-
textAnchor={'end'}
|
|
1134
|
-
fontSize={tickLabelFontSize}
|
|
1135
|
-
>
|
|
1136
|
-
{tick.formattedValue}
|
|
1137
|
-
</Text>
|
|
1138
|
-
)}
|
|
1139
|
-
|
|
1140
|
-
{orientation === 'horizontal' &&
|
|
1141
|
-
visualizationType !== 'Box Plot' &&
|
|
1142
|
-
visualizationSubType !== 'stacked' &&
|
|
1143
|
-
config.yAxis.labelPlacement === 'On Date/Category Axis' &&
|
|
1144
|
-
!config.yAxis.hideLabel && (
|
|
1145
|
-
<Text
|
|
1146
|
-
transform={`translate(${tick.to.x - 5}, ${
|
|
1147
|
-
config.isLollipopChart
|
|
1148
|
-
? tick.to.y - minY
|
|
1149
|
-
: tick.to.y -
|
|
1150
|
-
minY +
|
|
1151
|
-
(Number(config.barHeight * config.runtime.series.length) - barMinHeight) / 2
|
|
1152
|
-
}) rotate(-${config.runtime.horizontal ? config.runtime.yAxis.tickRotation || 0 : 0})`}
|
|
1153
|
-
verticalAnchor={'start'}
|
|
1154
|
-
textAnchor={'end'}
|
|
1155
|
-
fontSize={tickLabelFontSize}
|
|
1156
|
-
>
|
|
1157
|
-
{tick.formattedValue}
|
|
1158
|
-
</Text>
|
|
1159
|
-
)}
|
|
1160
|
-
|
|
1161
|
-
{orientation === 'horizontal' &&
|
|
1162
|
-
visualizationSubType === 'stacked' &&
|
|
1163
|
-
config.yAxis.labelPlacement === 'On Date/Category Axis' &&
|
|
1164
|
-
!config.yAxis.hideLabel && (
|
|
1165
|
-
<Text
|
|
1166
|
-
transform={`translate(${tick.to.x - 5}, ${
|
|
1167
|
-
tick.to.y - minY + (Number(config.barHeight) - barMinHeight) / 2
|
|
1168
|
-
}) rotate(-${runtime.horizontal ? runtime.yAxis.tickRotation : 0})`}
|
|
1169
|
-
verticalAnchor={'start'}
|
|
1170
|
-
textAnchor={'end'}
|
|
1171
|
-
fontSize={tickLabelFontSize}
|
|
1172
|
-
>
|
|
1173
|
-
{tick.formattedValue}
|
|
1174
|
-
</Text>
|
|
1175
|
-
)}
|
|
1176
|
-
|
|
1177
|
-
{orientation === 'horizontal' &&
|
|
1178
|
-
visualizationType === 'Paired Bar' &&
|
|
1179
|
-
!config.yAxis.hideLabel && (
|
|
1180
|
-
<Text
|
|
1181
|
-
transform={`translate(${tick.to.x - 5}, ${
|
|
1182
|
-
tick.to.y - minY + Number(config.barHeight) / 2
|
|
1183
|
-
}) rotate(-${runtime.horizontal ? runtime.yAxis.tickRotation : 0})`}
|
|
1184
|
-
textAnchor={'end'}
|
|
1185
|
-
verticalAnchor='middle'
|
|
1186
|
-
fontSize={tickLabelFontSize}
|
|
1187
|
-
>
|
|
1188
|
-
{tick.formattedValue}
|
|
1189
|
-
</Text>
|
|
1190
|
-
)}
|
|
1191
|
-
{orientation === 'horizontal' &&
|
|
1192
|
-
visualizationType === 'Deviation Bar' &&
|
|
1193
|
-
!config.yAxis.hideLabel && (
|
|
1194
|
-
<Text
|
|
1195
|
-
transform={`translate(${tick.to.x - 5}, ${
|
|
1196
|
-
config.isLollipopChart
|
|
1197
|
-
? tick.to.y - minY + 2
|
|
1198
|
-
: tick.to.y - minY + Number(config.barHeight) / 2
|
|
1199
|
-
}) rotate(-${runtime.horizontal ? runtime.yAxis.tickRotation : 0})`}
|
|
1200
|
-
textAnchor={'end'}
|
|
1201
|
-
verticalAnchor='middle'
|
|
1202
|
-
fontSize={tickLabelFontSize}
|
|
1203
|
-
>
|
|
1204
|
-
{tick.formattedValue}
|
|
1205
|
-
</Text>
|
|
1206
|
-
)}
|
|
1207
|
-
|
|
1208
|
-
{orientation === 'vertical' &&
|
|
1209
|
-
visualizationType === 'Bump Chart' &&
|
|
1210
|
-
!config.yAxis.hideLabel && (
|
|
1211
|
-
<>
|
|
1212
|
-
<Text
|
|
1213
|
-
display={config.useLogScale ? showTicks : 'block'}
|
|
1214
|
-
dx={config.useLogScale ? -6 : 0}
|
|
1215
|
-
x={config.runtime.horizontal ? tick.from.x + 2 : tick.to.x - 8.5}
|
|
1216
|
-
y={tick.to.y - 13 + (config.runtime.horizontal ? horizontalTickOffset : 0)}
|
|
1217
|
-
angle={-Number(config.yAxis.tickRotation) || 0}
|
|
1218
|
-
verticalAnchor={config.runtime.horizontal ? 'start' : 'middle'}
|
|
1219
|
-
textAnchor={config.runtime.horizontal ? 'start' : 'end'}
|
|
1220
|
-
fill={config.yAxis.tickLabelColor}
|
|
1221
|
-
fontSize={tickLabelFontSize}
|
|
1222
|
-
>
|
|
1223
|
-
{config.runtime.seriesLabelsAll[tick.formattedValue - 1]}
|
|
1224
|
-
</Text>
|
|
1225
|
-
|
|
1226
|
-
{(seriesHighlight.length === 0 ||
|
|
1227
|
-
seriesHighlight.includes(
|
|
1228
|
-
config.runtime.seriesLabelsAll[tick.formattedValue - 1]
|
|
1229
|
-
)) && (
|
|
1230
|
-
<rect
|
|
1231
|
-
x={0 - Number(config.yAxis.size)}
|
|
1232
|
-
y={tick.to.y - 8 + (config.runtime.horizontal ? horizontalTickOffset : 7)}
|
|
1233
|
-
width={Number(config.yAxis.size) + xScale(xScale.domain()[0])}
|
|
1234
|
-
height='2'
|
|
1235
|
-
fill={colorScale(config.runtime.seriesLabelsAll[tick.formattedValue - 1])}
|
|
1236
|
-
/>
|
|
1237
|
-
)}
|
|
1238
|
-
</>
|
|
1239
|
-
)}
|
|
1240
|
-
{orientation === 'vertical' &&
|
|
1241
|
-
visualizationType !== 'Paired Bar' &&
|
|
1242
|
-
visualizationType !== 'Bump Chart' &&
|
|
1243
|
-
!config.yAxis.hideLabel && (
|
|
1244
|
-
<>
|
|
1245
|
-
{/* INLINE LABEL BEHAVIOR: Dom suffix for 'inlineLabel' behavior */}
|
|
1246
|
-
{/* inline label is shown alone and is allowed to 'overflow' to the right */}
|
|
1247
|
-
{/* SPECIAL ONE CHAR CASE: a one character inlineLabel does not overflow */}
|
|
1248
|
-
{/* IF VALUES ON LINE: suffix is combined with value to avoid having to calculate varying (now left-aligned) value widths */}
|
|
1249
|
-
{inlineLabel && lastTick && !labelsAboveGridlines && (
|
|
1250
|
-
<BlurStrokeText
|
|
1251
|
-
innerRef={suffixRef}
|
|
1252
|
-
display={isLogarithmicAxis ? showTicks : 'block'}
|
|
1253
|
-
dx={isLogarithmicAxis ? -6 : 0}
|
|
1254
|
-
x={labelX}
|
|
1255
|
-
y={labelY}
|
|
1256
|
-
angle={-Number(config.yAxis.tickRotation) || 0}
|
|
1257
|
-
verticalAnchor={labelVerticalAnchor}
|
|
1258
|
-
textAnchor={inlineLabelHasNoSpace ? 'end' : 'start'}
|
|
1259
|
-
fill={config.yAxis.tickLabelColor}
|
|
1260
|
-
stroke={'#fff'}
|
|
1261
|
-
paintOrder={'stroke'} // keeps stroke under fill
|
|
1262
|
-
strokeLinejoin='round'
|
|
1263
|
-
style={{ whiteSpace: 'pre-wrap' }} // prevents leading spaces from being trimmed
|
|
1264
|
-
fontSize={tickLabelFontSize}
|
|
1265
|
-
>
|
|
1266
|
-
{inlineLabel}
|
|
1267
|
-
</BlurStrokeText>
|
|
1268
|
-
)}
|
|
1269
|
-
|
|
1270
|
-
{/* VALUE */}
|
|
1271
|
-
<BlurStrokeText
|
|
1272
|
-
innerRef={el => lastTick && (topYLabelRef.current = el)}
|
|
1273
|
-
display={isLogarithmicAxis ? showTicks : 'block'}
|
|
1274
|
-
dx={isLogarithmicAxis ? -6 : 0}
|
|
1275
|
-
x={inlineLabelHasNoSpace ? labelX - suffixWidth : labelX}
|
|
1276
|
-
y={labelY + (config.runtime.horizontal ? horizontalTickOffset : 0)}
|
|
1277
|
-
angle={-Number(config.yAxis.tickRotation) || 0}
|
|
1278
|
-
verticalAnchor={config.runtime.horizontal ? 'start' : labelVerticalAnchor}
|
|
1279
|
-
textAnchor={config.runtime.horizontal || labelsAboveGridlines ? 'start' : 'end'}
|
|
1280
|
-
fill={config.yAxis.tickLabelColor}
|
|
1281
|
-
stroke={'#fff'}
|
|
1282
|
-
disableStroke={!labelsAboveGridlines}
|
|
1283
|
-
strokeLinejoin='round'
|
|
1284
|
-
paintOrder={'stroke'} // keeps stroke under fill
|
|
1285
|
-
style={{ whiteSpace: 'pre-wrap' }} // prevents leading spaces from being trimmed
|
|
1286
|
-
fontSize={tickLabelFontSize}
|
|
1287
|
-
>
|
|
1288
|
-
{`${formattedValue}${combineDomInlineLabelWithValue ? inlineLabel : ''}`}
|
|
1289
|
-
</BlurStrokeText>
|
|
1290
|
-
</>
|
|
1291
|
-
)}
|
|
1292
|
-
</Group>
|
|
1293
|
-
)
|
|
1294
|
-
})}
|
|
1295
|
-
<Text
|
|
1296
|
-
className='y-label'
|
|
1297
|
-
textAnchor='middle'
|
|
1298
|
-
verticalAnchor='start'
|
|
1299
|
-
transform={`translate(${-1 * runtime.yAxis.size + yLabelOffset}, ${axisCenter}) rotate(-90)`}
|
|
1300
|
-
fontWeight='bold'
|
|
1301
|
-
fill={config.yAxis.labelColor}
|
|
1302
|
-
fontSize={axisLabelFontSize}
|
|
1303
|
-
>
|
|
1304
|
-
{!config.hideYAxisLabel ? props.label : null}
|
|
1305
|
-
</Text>
|
|
1306
|
-
</Group>
|
|
1307
|
-
)
|
|
1308
|
-
}}
|
|
1309
|
-
</AxisLeft>
|
|
1310
|
-
)}
|
|
743
|
+
{/* Horizon charts don't have a grid but should be rendered with a left axis */}
|
|
744
|
+
{(!TYPES_WITHOUT_GRID.includes(visualizationType as any) || visualizationType === 'Horizon Chart') &&
|
|
745
|
+
config.yAxis.type !== 'categorical' && (
|
|
746
|
+
<LeftAxis
|
|
747
|
+
yScale={yScale}
|
|
748
|
+
xScale={xScale}
|
|
749
|
+
yMax={yMax}
|
|
750
|
+
xMax={xMax}
|
|
751
|
+
yAxisWidth={yAxisWidth}
|
|
752
|
+
numTicks={handleNumTicks}
|
|
753
|
+
tickLabelFontSize={tickLabelFontSize}
|
|
754
|
+
axisLabelFontSize={axisLabelFontSize}
|
|
755
|
+
handleLeftTickFormatting={handleLeftTickFormatting}
|
|
756
|
+
topYLabelRef={topYLabelRef}
|
|
757
|
+
suffixRef={suffixRef}
|
|
758
|
+
suffixWidth={suffixWidth}
|
|
759
|
+
horizontalYAxisLabelSpace={horizontalYAxisLabelSpace}
|
|
760
|
+
categoryLabelSpace={categoryLabelSpace}
|
|
761
|
+
yLabelOffset={yLabelOffset}
|
|
762
|
+
/>
|
|
763
|
+
)}
|
|
1311
764
|
{config.yAxis.type === 'categorical' && config.orientation === 'vertical' && (
|
|
1312
765
|
<CategoricalYAxis
|
|
1313
|
-
|
|
1314
|
-
maxValue={maxValue}
|
|
1315
|
-
height={initialHeight}
|
|
766
|
+
yScale={yScale}
|
|
1316
767
|
xMax={xMax}
|
|
1317
768
|
yMax={yMax}
|
|
1318
|
-
leftSize={
|
|
769
|
+
leftSize={yAxisWidth - config.yAxis.axisPadding}
|
|
1319
770
|
/>
|
|
1320
771
|
)}
|
|
1321
772
|
{/* Right Axis */}
|
|
1322
773
|
{hasRightAxis && (
|
|
1323
|
-
<
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
{props => {
|
|
1332
|
-
const axisCenter =
|
|
1333
|
-
config.orientation === 'horizontal'
|
|
1334
|
-
? (props.axisToPoint.y - props.axisFromPoint.y) / 2
|
|
1335
|
-
: (props.axisFromPoint.y - props.axisToPoint.y) / 2
|
|
1336
|
-
const horizontalTickOffset =
|
|
1337
|
-
yMax / props.ticks.length / 2 - (yMax / props.ticks.length) * (1 - config.barThickness) + 5
|
|
1338
|
-
return (
|
|
1339
|
-
<Group className='right-axis'>
|
|
1340
|
-
{props.ticks.map((tick, i) => {
|
|
1341
|
-
return (
|
|
1342
|
-
<Group key={`vx-tick-${tick.value}-${i}`} className='vx-axis-tick'>
|
|
1343
|
-
{!runtime.yAxis.rightHideTicks && (
|
|
1344
|
-
<Line
|
|
1345
|
-
from={tick.from}
|
|
1346
|
-
to={tick.to}
|
|
1347
|
-
display={runtime.horizontal ? 'none' : 'block'}
|
|
1348
|
-
stroke={config.yAxis.rightAxisTickColor}
|
|
1349
|
-
/>
|
|
1350
|
-
)}
|
|
1351
|
-
|
|
1352
|
-
{runtime.yAxis.rightGridLines ? (
|
|
1353
|
-
<Line from={{ x: tick.from.x + xMax, y: tick.from.y }} to={tick.from} stroke='#d6d6d6' />
|
|
1354
|
-
) : (
|
|
1355
|
-
''
|
|
1356
|
-
)}
|
|
1357
|
-
|
|
1358
|
-
{!config.yAxis.rightHideLabel && (
|
|
1359
|
-
<Text
|
|
1360
|
-
x={tick.to.x}
|
|
1361
|
-
y={tick.to.y + (runtime.horizontal ? horizontalTickOffset : 0)}
|
|
1362
|
-
verticalAnchor={runtime.horizontal ? 'start' : 'middle'}
|
|
1363
|
-
textAnchor={'start'}
|
|
1364
|
-
fill={config.yAxis.rightAxisTickLabelColor}
|
|
1365
|
-
fontSize={tickLabelFontSize}
|
|
1366
|
-
>
|
|
1367
|
-
{tick.formattedValue}
|
|
1368
|
-
</Text>
|
|
1369
|
-
)}
|
|
1370
|
-
</Group>
|
|
1371
|
-
)
|
|
1372
|
-
})}
|
|
1373
|
-
{!config.yAxis.rightHideAxis && (
|
|
1374
|
-
<Line from={props.axisFromPoint} to={props.axisToPoint} stroke='#333' />
|
|
1375
|
-
)}
|
|
1376
|
-
<Text
|
|
1377
|
-
className='y-label'
|
|
1378
|
-
textAnchor='middle'
|
|
1379
|
-
verticalAnchor='start'
|
|
1380
|
-
transform={`translate(${
|
|
1381
|
-
config.yAxis.rightLabelOffsetSize ? config.yAxis.rightLabelOffsetSize : 0
|
|
1382
|
-
}, ${axisCenter}) rotate(-90)`}
|
|
1383
|
-
fontWeight='bold'
|
|
1384
|
-
fill={config.yAxis.rightAxisLabelColor}
|
|
1385
|
-
fontSize={axisLabelFontSize}
|
|
1386
|
-
>
|
|
1387
|
-
{props.label}
|
|
1388
|
-
</Text>
|
|
1389
|
-
</Group>
|
|
1390
|
-
)
|
|
1391
|
-
}}
|
|
1392
|
-
</AxisRight>
|
|
774
|
+
<RightAxis
|
|
775
|
+
yScaleRight={yScaleRight}
|
|
776
|
+
yMax={yMax}
|
|
777
|
+
xMax={xMax}
|
|
778
|
+
yAxisWidth={yAxisWidth}
|
|
779
|
+
tickLabelFontSize={tickLabelFontSize}
|
|
780
|
+
axisLabelFontSize={axisLabelFontSize}
|
|
781
|
+
/>
|
|
1393
782
|
)}
|
|
1394
783
|
{hasTopAxis && config.topAxis.hasLine && (
|
|
1395
784
|
<AxisTop
|
|
1396
785
|
stroke='#333'
|
|
1397
|
-
left={
|
|
786
|
+
left={yAxisWidth}
|
|
1398
787
|
scale={xScale}
|
|
1399
788
|
hideTicks
|
|
1400
789
|
hideZero
|
|
@@ -1405,181 +794,27 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
1405
794
|
)}
|
|
1406
795
|
{/* X axis */}
|
|
1407
796
|
{visualizationType !== 'Paired Bar' && visualizationType !== 'Spark Line' && (
|
|
1408
|
-
<
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
: undefined
|
|
1430
|
-
}
|
|
1431
|
-
>
|
|
1432
|
-
{props => {
|
|
1433
|
-
const hasDynamicCategory = config.series.some(s => s.dynamicCategory)
|
|
1434
|
-
// For these charts, we generated all ticks in tickValues above, and now need to filter/shift them
|
|
1435
|
-
// so the last tick is always labeled
|
|
1436
|
-
if (config.runtime.xAxis.type === 'date' && !config.runtime.xAxis.manual && !hasDynamicCategory) {
|
|
1437
|
-
props.ticks = filterAndShiftLinearDateTicks(config, props, xAxisDataMapped, formatDate)
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
const distanceBetweenTicks =
|
|
1441
|
-
useDateSpanMonths &&
|
|
1442
|
-
xScale
|
|
1443
|
-
.ticks(xTickCount)
|
|
1444
|
-
.map(t =>
|
|
1445
|
-
props.ticks.findIndex(
|
|
1446
|
-
tick => (typeof tick.value === 'number' ? tick.value : tick.value.getTime()) === t.getTime()
|
|
1447
|
-
)
|
|
1448
|
-
)
|
|
1449
|
-
.slice(0, 2)
|
|
1450
|
-
.reduce((acc, curr) => curr - acc)
|
|
1451
|
-
|
|
1452
|
-
// filter out every [distanceBetweenTicks] tick starting from the end, so the last tick is always labeled
|
|
1453
|
-
const filteredTicks = useDateSpanMonths
|
|
1454
|
-
? [...props.ticks]
|
|
1455
|
-
.reverse()
|
|
1456
|
-
.filter((_, i) => i % distanceBetweenTicks === 0)
|
|
1457
|
-
.reverse()
|
|
1458
|
-
.map((tick, i, arr) => ({
|
|
1459
|
-
...tick,
|
|
1460
|
-
// reformat in case showYearsOnce, since first month of year may have changed
|
|
1461
|
-
formattedValue: handleBottomTickFormatting(tick.value, i, arr)
|
|
1462
|
-
}))
|
|
1463
|
-
: props.ticks
|
|
1464
|
-
|
|
1465
|
-
const axisMaxHeight = bottomLabelStart + BOTTOM_LABEL_PADDING
|
|
1466
|
-
|
|
1467
|
-
const containsMultipleWords = inputString => /\s/.test(inputString)
|
|
1468
|
-
const isMultiLabel = filteredTicks.some(tick => containsMultipleWords(tick.value))
|
|
1469
|
-
|
|
1470
|
-
// Calculate sumOfTickWidth here, before map function
|
|
1471
|
-
const longestTickLength = Math.max(
|
|
1472
|
-
...filteredTicks.map(tick => getTextWidth(tick.formattedValue, GET_TEXT_WIDTH_FONT))
|
|
1473
|
-
)
|
|
1474
|
-
// const marginTop = 20 // moved to top bc need for yMax calcs
|
|
1475
|
-
const accumulator = isMultiLabel ? 180 : 100
|
|
1476
|
-
|
|
1477
|
-
const textWidths = filteredTicks.map(tick => getTextWidth(tick.formattedValue, GET_TEXT_WIDTH_FONT))
|
|
1478
|
-
const sumOfTickWidth = textWidths.reduce((a, b) => a + b, accumulator)
|
|
1479
|
-
const spaceBetweenEachTick = (xMax - sumOfTickWidth) / (filteredTicks.length - 1)
|
|
1480
|
-
const bufferBetweenTicks = 40
|
|
1481
|
-
const maxLengthOfTick = width / filteredTicks.length - X_TICK_LABEL_PADDING * 2 - bufferBetweenTicks
|
|
1482
|
-
|
|
1483
|
-
// Determine the position of each tick
|
|
1484
|
-
let positions = [0] // The first tick is at position 0
|
|
1485
|
-
for (let i = 1; i < textWidths.length; i++) {
|
|
1486
|
-
// The position of each subsequent tick is the position of the previous tick
|
|
1487
|
-
// plus the width of the previous tick and the space
|
|
1488
|
-
positions[i] = positions[i - 1] + textWidths[i - 1] + spaceBetweenEachTick
|
|
1489
|
-
}
|
|
1490
|
-
// calculate the end of x axis box
|
|
1491
|
-
const axisBBox = axisBottomRef?.current?.getBBox().height
|
|
1492
|
-
config.xAxis.axisBBox = axisBBox
|
|
1493
|
-
|
|
1494
|
-
// force wrap it last tick is close to the end of the axis
|
|
1495
|
-
const lastTickWidth = textWidths[textWidths.length - 1]
|
|
1496
|
-
const lastTickPosition = positions[positions.length - 1] + lastTickWidth
|
|
1497
|
-
const lastTickEnd = lastTickPosition + lastTickWidth / 2
|
|
1498
|
-
const lastTickEndThreshold = xMax - lastTickWidth
|
|
1499
|
-
|
|
1500
|
-
const areTicksTouching =
|
|
1501
|
-
textWidths.some(textWidth => textWidth > maxLengthOfTick) || // Force wrap if any tick is too long
|
|
1502
|
-
config.xAxis.showYearsOnce || // Force wrap when showing years once so it's easier to read
|
|
1503
|
-
lastTickEnd > lastTickEndThreshold // Force wrap it last tick is close to the end of the axis
|
|
1504
|
-
|
|
1505
|
-
const dynamicMarginTop =
|
|
1506
|
-
areTicksTouching && config.isResponsiveTicks ? longestTickLength + DEFAULT_TICK_LENGTH + 20 : 0
|
|
1507
|
-
|
|
1508
|
-
config.dynamicMarginTop = dynamicMarginTop
|
|
1509
|
-
config.xAxis.tickWidthMax = longestTickLength
|
|
1510
|
-
|
|
1511
|
-
return (
|
|
1512
|
-
<Group className='bottom-axis' width={dimensions[0]}>
|
|
1513
|
-
{filteredTicks.map((tick, i, propsTicks) => {
|
|
1514
|
-
// when using LogScale show major ticks values only
|
|
1515
|
-
const showTick = String(tick.value).startsWith('1') || tick.value === 0.1 ? 'block' : 'none'
|
|
1516
|
-
const tickLength = showTick === 'block' ? 16 : DEFAULT_TICK_LENGTH
|
|
1517
|
-
const to = { x: tick.to.x, y: tickLength }
|
|
1518
|
-
const limitedWidth = 100 / propsTicks.length
|
|
1519
|
-
//reset rotations by updating config
|
|
1520
|
-
config.yAxis.tickRotation =
|
|
1521
|
-
config.isResponsiveTicks && config.orientation === 'horizontal' ? 0 : config.yAxis.tickRotation
|
|
1522
|
-
config.xAxis.tickRotation =
|
|
1523
|
-
config.isResponsiveTicks && config.orientation === 'vertical' ? 0 : config.xAxis.tickRotation
|
|
1524
|
-
//configure rotation
|
|
1525
|
-
|
|
1526
|
-
const tickRotation =
|
|
1527
|
-
config.isResponsiveTicks && areTicksTouching
|
|
1528
|
-
? -Number(config.xAxis.maxTickRotation) || -90
|
|
1529
|
-
: -Number(config.runtime.xAxis.tickRotation)
|
|
1530
|
-
|
|
1531
|
-
return (
|
|
1532
|
-
<Group key={`vx-tick-${tick.value}-${i}`} className={'vx-axis-tick'}>
|
|
1533
|
-
{!config.xAxis.hideTicks && (
|
|
1534
|
-
<Line
|
|
1535
|
-
from={tick.from}
|
|
1536
|
-
to={orientation === 'horizontal' && isLogarithmicAxis ? to : tick.to}
|
|
1537
|
-
stroke={config.xAxis.tickColor}
|
|
1538
|
-
strokeWidth={showTick === 'block' && isLogarithmicAxis ? 1.3 : 1}
|
|
1539
|
-
/>
|
|
1540
|
-
)}
|
|
1541
|
-
{!config.xAxis.hideLabel && (
|
|
1542
|
-
<Text
|
|
1543
|
-
innerRef={el => (xAxisLabelRefs.current[i] = el)}
|
|
1544
|
-
dy={config.orientation === 'horizontal' && isLogarithmicAxis ? 8 : 0}
|
|
1545
|
-
display={config.orientation === 'horizontal' && isLogarithmicAxis ? showTick : 'block'}
|
|
1546
|
-
x={tick.to.x}
|
|
1547
|
-
y={tick.to.y + X_TICK_LABEL_PADDING}
|
|
1548
|
-
angle={tickRotation}
|
|
1549
|
-
verticalAnchor={tickRotation < -50 ? 'middle' : 'start'}
|
|
1550
|
-
textAnchor={tickRotation ? 'end' : 'middle'}
|
|
1551
|
-
width={
|
|
1552
|
-
areTicksTouching && !config.isResponsiveTicks && !Number(config.xAxis.tickRotation)
|
|
1553
|
-
? limitedWidth
|
|
1554
|
-
: undefined
|
|
1555
|
-
}
|
|
1556
|
-
fill={config.xAxis.tickLabelColor}
|
|
1557
|
-
fontSize={tickLabelFontSize}
|
|
1558
|
-
>
|
|
1559
|
-
{tick.formattedValue}
|
|
1560
|
-
</Text>
|
|
1561
|
-
)}
|
|
1562
|
-
</Group>
|
|
1563
|
-
)
|
|
1564
|
-
})}
|
|
1565
|
-
{!config.xAxis.hideAxis && <Line from={props.axisFromPoint} to={props.axisToPoint} stroke='#333' />}
|
|
1566
|
-
<Text
|
|
1567
|
-
innerRef={xAxisTitleRef}
|
|
1568
|
-
className='x-axis-title-label'
|
|
1569
|
-
x={xMax / 2}
|
|
1570
|
-
y={isForestPlot ? 0 /* set via ref */ : axisMaxHeight}
|
|
1571
|
-
textAnchor='middle'
|
|
1572
|
-
verticalAnchor='start'
|
|
1573
|
-
fontWeight='bold'
|
|
1574
|
-
fill={config.xAxis.labelColor}
|
|
1575
|
-
fontSize={axisLabelFontSize}
|
|
1576
|
-
>
|
|
1577
|
-
{!config.hideXAxisLabel ? props.label : null}
|
|
1578
|
-
</Text>
|
|
1579
|
-
</Group>
|
|
1580
|
-
)
|
|
1581
|
-
}}
|
|
1582
|
-
</AxisBottom>
|
|
797
|
+
<BottomAxis
|
|
798
|
+
axisBottomRef={axisBottomRef}
|
|
799
|
+
xScale={xScale}
|
|
800
|
+
yMax={yMax}
|
|
801
|
+
xMax={xMax}
|
|
802
|
+
yAxisWidth={yAxisWidth}
|
|
803
|
+
xTickCount={xTickCount}
|
|
804
|
+
tickLabelFontSize={tickLabelFontSize}
|
|
805
|
+
axisLabelFontSize={axisLabelFontSize}
|
|
806
|
+
handleBottomTickFormatting={handleBottomTickFormatting}
|
|
807
|
+
useDateSpanMonths={useDateSpanMonths}
|
|
808
|
+
dateSpanMonths={dateSpanMonths}
|
|
809
|
+
xAxisDataMapped={xAxisDataMapped}
|
|
810
|
+
uniqueXAxisDataMapped={uniqueXAxisDataMapped}
|
|
811
|
+
isDateTime={isDateTime}
|
|
812
|
+
bottomLabelStart={bottomLabelStart}
|
|
813
|
+
parentWidth={parentWidth}
|
|
814
|
+
xAxisLabelRefs={xAxisLabelRefs}
|
|
815
|
+
xAxisTitleRef={xAxisTitleRef}
|
|
816
|
+
getManualStep={getManualStep}
|
|
817
|
+
/>
|
|
1583
818
|
)}
|
|
1584
819
|
</svg>
|
|
1585
820
|
{!isDraggingAnnotation &&
|
|
@@ -1631,6 +866,27 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
1631
866
|
/>
|
|
1632
867
|
)}
|
|
1633
868
|
<div className='animation-trigger' ref={triggerRef} />
|
|
869
|
+
{/* SEPARATED BRUSH - Independent SVG overlay */}
|
|
870
|
+
{config.xAxis.brushActive && config.xAxis.type !== 'categorical' && xMax > 0 && (
|
|
871
|
+
<div
|
|
872
|
+
style={{
|
|
873
|
+
position: 'relative',
|
|
874
|
+
marginTop: `${BRUSH_MARGIN}px`,
|
|
875
|
+
left: `${yAxisWidth}px`,
|
|
876
|
+
width: `${Math.max(xMax, BRUSH_MIN_WIDTH)}px`,
|
|
877
|
+
height: `${BRUSH_HEIGHT}px`,
|
|
878
|
+
pointerEvents: 'auto',
|
|
879
|
+
zIndex: 15,
|
|
880
|
+
touchAction: 'none', // Enable touch interactions for brush
|
|
881
|
+
WebkitTouchCallout: 'none',
|
|
882
|
+
WebkitUserSelect: 'none',
|
|
883
|
+
userSelect: 'none'
|
|
884
|
+
}}
|
|
885
|
+
className='brush-overlay'
|
|
886
|
+
>
|
|
887
|
+
<BrushSelector key={brushKeyRef.current} xMax={Math.max(xMax, BRUSH_MIN_WIDTH)} yMax={BRUSH_HEIGHT} />
|
|
888
|
+
</div>
|
|
889
|
+
)}
|
|
1634
890
|
</div>
|
|
1635
891
|
</ErrorBoundary>
|
|
1636
892
|
)
|