@cdc/chart 4.23.4 → 4.23.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cdcchart.js +54845 -51755
- package/examples/feature/__data__/planet-example-data.json +14 -32
- package/examples/feature/__data__/planet-logaritmic-data.json +56 -0
- package/examples/feature/area/area-chart-category.json +240 -0
- package/examples/feature/bar/example-bar-chart.json +544 -22
- package/examples/feature/bar/new.json +561 -0
- package/examples/feature/bar/planet-chart-logaritmic-config.json +170 -0
- package/examples/feature/boxplot/valid-boxplot.csv +17 -0
- package/examples/feature/combo/right-issues.json +190 -0
- package/examples/feature/filters/filter-testing.json +37 -3
- package/examples/feature/forecasting/combo-forecasting.json +245 -0
- package/examples/feature/forecasting/forecasting.json +5325 -0
- package/examples/feature/forecasting/index.json +203 -0
- package/examples/feature/forecasting/random_data.csv +366 -0
- package/examples/feature/line/line-chart.json +3 -3
- package/examples/feature/test-highlight/test-highlight-2.json +789 -0
- package/examples/feature/test-highlight/test-highlight-vertical.json +561 -0
- package/examples/feature/test-highlight/test-highlight.json +100 -0
- package/examples/feature/tests-non-numerics/stacked-vertical-bar-example-nonnumerics.json +1 -2
- package/examples/gallery/bar-chart-horizontal/horizontal-highlight.json +345 -0
- package/examples/gallery/line/line.json +173 -1
- package/index.html +14 -8
- package/package.json +2 -2
- package/src/CdcChart.jsx +342 -25
- package/src/components/AreaChart.jsx +32 -40
- package/src/components/BarChart.jsx +147 -25
- package/src/components/DataTable.jsx +30 -12
- package/src/components/DeviationBar.jsx +32 -32
- package/src/components/EditorPanel.jsx +1902 -1126
- package/src/components/Forecasting.jsx +147 -0
- package/src/components/Legend.jsx +193 -243
- package/src/components/LineChart.jsx +4 -9
- package/src/components/LinearChart.jsx +263 -285
- package/src/components/Series.jsx +518 -0
- package/src/components/SparkLine.jsx +3 -3
- package/src/data/initial-state.js +24 -5
- package/src/hooks/useHighlightedBars.js +154 -0
- package/src/hooks/useMinMax.js +128 -0
- package/src/hooks/useReduceData.js +31 -57
- package/src/hooks/useRightAxis.js +8 -2
- package/src/hooks/useScales.js +196 -0
- /package/examples/feature/area/{area-chart.json → area-chart-date.json} +0 -0
|
@@ -4,302 +4,81 @@ import { Tooltip as ReactTooltip } from 'react-tooltip'
|
|
|
4
4
|
import { Group } from '@visx/group'
|
|
5
5
|
import { Line } from '@visx/shape'
|
|
6
6
|
import { Text } from '@visx/text'
|
|
7
|
-
import { scaleLinear, scalePoint, scaleBand, scaleTime } from '@visx/scale'
|
|
8
7
|
import { AxisLeft, AxisBottom, AxisRight, AxisTop } from '@visx/axis'
|
|
8
|
+
import { localPoint } from '@visx/event'
|
|
9
|
+
import { useTooltip } from '@visx/tooltip'
|
|
9
10
|
|
|
10
|
-
import CoveScatterPlot from './ScatterPlot'
|
|
11
11
|
import BarChart from './BarChart'
|
|
12
|
-
import LineChart from './LineChart'
|
|
13
12
|
import ConfigContext from '../ConfigContext'
|
|
13
|
+
import CoveAreaChart from './AreaChart'
|
|
14
|
+
import CoveBoxPlot from './BoxPlot'
|
|
15
|
+
import CoveScatterPlot from './ScatterPlot'
|
|
16
|
+
import DeviationBar from './DeviationBar'
|
|
17
|
+
import LineChart from './LineChart'
|
|
14
18
|
import PairedBarChart from './PairedBarChart'
|
|
15
19
|
import useIntersectionObserver from './useIntersectionObserver'
|
|
16
|
-
import CoveBoxPlot from './BoxPlot'
|
|
17
|
-
import CoveAreaChart from './AreaChart'
|
|
18
20
|
|
|
19
21
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
20
22
|
import '../scss/LinearChart.scss'
|
|
21
23
|
import useReduceData from '../hooks/useReduceData'
|
|
24
|
+
import useScales from '../hooks/useScales'
|
|
25
|
+
import useMinMax from '../hooks/useMinMax'
|
|
22
26
|
import useRightAxis from '../hooks/useRightAxis'
|
|
23
27
|
import useTopAxis from '../hooks/useTopAxis'
|
|
24
|
-
import
|
|
28
|
+
import Forecasting from './Forecasting'
|
|
25
29
|
|
|
26
|
-
// TODO: Move scaling functions into hooks to manage complexity
|
|
27
30
|
export default function LinearChart() {
|
|
28
|
-
const { transformedData: data, dimensions, config, parseDate, formatDate, currentViewport, formatNumber, handleChartAriaLabels, updateConfig } = useContext(ConfigContext)
|
|
29
|
-
|
|
30
|
-
let [width] = dimensions
|
|
31
|
-
const { minValue, maxValue, existPositiveValue, isAllLine } = useReduceData(config, data)
|
|
32
|
-
const [animatedChart, setAnimatedChart] = useState(false)
|
|
33
|
-
|
|
34
|
-
const triggerRef = useRef()
|
|
35
|
-
const dataRef = useIntersectionObserver(triggerRef, {
|
|
36
|
-
freezeOnceVisible: false
|
|
37
|
-
})
|
|
31
|
+
const { transformedData: data, dimensions, config, parseDate, formatDate, currentViewport, formatNumber, handleChartAriaLabels, updateConfig, handleLineType, rawData } = useContext(ConfigContext)
|
|
38
32
|
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (element) {
|
|
44
|
-
// parent element is visible
|
|
45
|
-
setAnimatedChart(prevState => true)
|
|
46
|
-
}
|
|
47
|
-
}) /* eslint-disable-line */
|
|
48
|
-
|
|
49
|
-
// If the chart is in view, set to animate if it has not already played
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
if (dataRef?.isIntersecting === true && config.animate) {
|
|
52
|
-
setTimeout(() => {
|
|
53
|
-
setAnimatedChart(prevState => true)
|
|
54
|
-
}, 500)
|
|
55
|
-
}
|
|
56
|
-
}, [dataRef?.isIntersecting, config.animate])
|
|
33
|
+
// getters & functions
|
|
34
|
+
const getXAxisData = d => (config.runtime.xAxis.type === 'date' ? parseDate(d[config.runtime.originalXAxis.dataKey]).getTime() : d[config.runtime.originalXAxis.dataKey])
|
|
35
|
+
const getYAxisData = (d, seriesKey) => d[seriesKey]
|
|
36
|
+
const xAxisDataMapped = data.map(d => getXAxisData(d))
|
|
57
37
|
|
|
58
|
-
|
|
38
|
+
// configure width
|
|
39
|
+
let [width] = dimensions
|
|
40
|
+
if (config && config.legend && !config.legend.hide && config.legend.position !== 'bottom' && ['lg', 'md'].includes(currentViewport)) {
|
|
59
41
|
width = width * 0.73
|
|
60
42
|
}
|
|
43
|
+
// configure height , yMax, xMAx
|
|
61
44
|
const { horizontal: heightHorizontal } = config.heights
|
|
45
|
+
const isHorizontal = config.orientation === 'horizontal'
|
|
46
|
+
const shouldAbbreviate = true
|
|
62
47
|
const height = config.aspectRatio ? width * config.aspectRatio : config.heights[config.orientation]
|
|
63
48
|
const xMax = width - config.runtime.yAxis.size - (config.visualizationType === 'Combo' ? config.yAxis.rightAxisSize : 0)
|
|
64
49
|
const yMax = height - (config.orientation === 'horizontal' ? 0 : config.runtime.xAxis.size)
|
|
65
50
|
|
|
51
|
+
// hooks % states
|
|
52
|
+
const { minValue, maxValue, existPositiveValue, isAllLine } = useReduceData(config, data)
|
|
66
53
|
const { yScaleRight, hasRightAxis } = useRightAxis({ config, yMax, data, updateConfig })
|
|
67
54
|
const { hasTopAxis } = useTopAxis(config)
|
|
55
|
+
const [animatedChart, setAnimatedChart] = useState(false)
|
|
56
|
+
const properties = { data, config, minValue, maxValue, isAllLine, existPositiveValue, xAxisDataMapped, xMax, yMax }
|
|
57
|
+
const { min, max } = useMinMax(properties)
|
|
58
|
+
const { xScale, yScale, seriesScale, g1xScale, g2xScale, xScaleNoPadding } = useScales({ ...properties, min, max })
|
|
68
59
|
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const { max: enteredMaxValue, min: enteredMinValue } = config.runtime.yAxis
|
|
77
|
-
const isMaxValid = existPositiveValue ? enteredMaxValue >= maxValue : enteredMaxValue >= 0
|
|
78
|
-
const isMinValid = (enteredMinValue <= 0 && minValue >= 0) || (enteredMinValue <= minValue && minValue < 0)
|
|
79
|
-
|
|
80
|
-
let max = 0 // need outside the if statement
|
|
81
|
-
let min = 0
|
|
82
|
-
if (data) {
|
|
83
|
-
min = enteredMinValue && isMinValid ? enteredMinValue : minValue
|
|
84
|
-
max = enteredMaxValue && isMaxValid ? enteredMaxValue : Number.MIN_VALUE
|
|
85
|
-
|
|
86
|
-
// If Confidence Intervals in data, then need to account for increased height in max for YScale
|
|
87
|
-
if (config.visualizationType === 'Bar' || config.visualizationType === 'Combo' || config.visualizationType === 'Deviation Bar') {
|
|
88
|
-
let ciYMax = 0
|
|
89
|
-
if (config.hasOwnProperty('confidenceKeys')) {
|
|
90
|
-
let upperCIValues = data.map(function (d) {
|
|
91
|
-
return d[config.confidenceKeys.upper]
|
|
92
|
-
})
|
|
93
|
-
ciYMax = Math.max.apply(Math, upperCIValues)
|
|
94
|
-
if (ciYMax > max) max = ciYMax // bump up the max
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if ((config.visualizationType === 'Bar' || (config.visualizationType === 'Combo' && !isAllLine)) && min > 0) {
|
|
99
|
-
min = 0
|
|
100
|
-
}
|
|
101
|
-
if (config.visualizationType === 'Combo' && isAllLine) {
|
|
102
|
-
if ((enteredMinValue === undefined || enteredMinValue === null || enteredMinValue === '') && min > 0) {
|
|
103
|
-
min = 0
|
|
104
|
-
}
|
|
105
|
-
if (enteredMinValue) {
|
|
106
|
-
const isMinValid = +enteredMinValue < minValue
|
|
107
|
-
min = +enteredMinValue && isMinValid ? enteredMinValue : minValue
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (config.visualizationType === 'Deviation Bar' && min > 0) {
|
|
112
|
-
const isMinValid = Number(enteredMinValue) < Math.min(minValue, Number(config.xAxis.target))
|
|
113
|
-
min = enteredMinValue && isMinValid ? enteredMinValue : 0
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (config.visualizationType === 'Line') {
|
|
117
|
-
const isMinValid = enteredMinValue < minValue
|
|
118
|
-
min = enteredMinValue && isMinValid ? enteredMinValue : minValue
|
|
119
|
-
}
|
|
120
|
-
//If data value max wasn't provided, calculate it
|
|
121
|
-
if (max === Number.MIN_VALUE) {
|
|
122
|
-
// if all values in data are negative set max = 0
|
|
123
|
-
max = existPositiveValue ? maxValue : 0
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
//Adds Y Axis data padding if applicable
|
|
127
|
-
if (config.runtime.yAxis.paddingPercent) {
|
|
128
|
-
let paddingValue = (max - min) * config.runtime.yAxis.paddingPercent
|
|
129
|
-
min -= paddingValue
|
|
130
|
-
max += paddingValue
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
let xAxisDataMapped = data.map(d => getXAxisData(d))
|
|
134
|
-
|
|
135
|
-
if (config.isLollipopChart && config.yAxis.displayNumbersOnBar) {
|
|
136
|
-
const dataKey = data.map(item => item[config.series[0].dataKey])
|
|
137
|
-
const maxDataVal = Math.max(...dataKey).toString().length
|
|
138
|
-
|
|
139
|
-
switch (true) {
|
|
140
|
-
case maxDataVal > 8 && maxDataVal <= 12:
|
|
141
|
-
max = max * 1.3
|
|
142
|
-
break
|
|
143
|
-
case maxDataVal > 4 && maxDataVal <= 7:
|
|
144
|
-
max = max * 1.1
|
|
145
|
-
break
|
|
146
|
-
default:
|
|
147
|
-
break
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// DEV-3219 - bc some values are going above YScale - adding 10% or 20% factor onto Max
|
|
152
|
-
// - put the statement up here and it works for both vert and horiz charts of all types
|
|
153
|
-
if (config.yAxis.enablePadding) {
|
|
154
|
-
if (min < 0) {
|
|
155
|
-
// sets with negative data need more padding on the max
|
|
156
|
-
max *= 1.2
|
|
157
|
-
min *= 1.2
|
|
158
|
-
} else {
|
|
159
|
-
max *= 1.1
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (config.runtime.horizontal) {
|
|
164
|
-
xScale = scaleLinear({
|
|
165
|
-
domain: [min * 1.03, max],
|
|
166
|
-
range: [0, xMax]
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
yScale =
|
|
170
|
-
config.runtime.xAxis.type === 'date'
|
|
171
|
-
? scaleLinear({
|
|
172
|
-
domain: [Math.min(...xAxisDataMapped), Math.max(...xAxisDataMapped)]
|
|
173
|
-
})
|
|
174
|
-
: scalePoint({ domain: xAxisDataMapped, padding: 0.5 })
|
|
175
|
-
|
|
176
|
-
seriesScale = scalePoint({
|
|
177
|
-
domain: config.runtime.barSeriesKeys || config.runtime.seriesKeys,
|
|
178
|
-
range: [0, yMax]
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
yScale.rangeRound([0, yMax])
|
|
182
|
-
} else {
|
|
183
|
-
min = min < 0 ? min * 1.11 : min
|
|
184
|
-
|
|
185
|
-
yScale = scaleLinear({
|
|
186
|
-
domain: [min, max],
|
|
187
|
-
range: [yMax, 0]
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
xScale = scalePoint({
|
|
191
|
-
domain: xAxisDataMapped,
|
|
192
|
-
range: [0, xMax],
|
|
193
|
-
padding: 0.5
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
seriesScale = scalePoint({
|
|
197
|
-
domain: config.runtime.barSeriesKeys || config.runtime.seriesKeys,
|
|
198
|
-
range: [0, xMax]
|
|
199
|
-
})
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (config.visualizationType === 'Area Chart' && config.xAxis.type === 'date') {
|
|
203
|
-
xScale = scaleTime({
|
|
204
|
-
domain: [Math.min(...xAxisDataMapped), Math.max(...xAxisDataMapped)],
|
|
205
|
-
range: [0, xMax]
|
|
206
|
-
})
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (config.visualizationType === 'Paired Bar') {
|
|
210
|
-
const offset = 1.02 // Offset of the ticks/values from the Axis
|
|
211
|
-
let groupOneMax = Math.max.apply(
|
|
212
|
-
Math,
|
|
213
|
-
data.map(d => d[config.series[0].dataKey])
|
|
214
|
-
)
|
|
215
|
-
let groupTwoMax = Math.max.apply(
|
|
216
|
-
Math,
|
|
217
|
-
data.map(d => d[config.series[1].dataKey])
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
-
// group one
|
|
221
|
-
var g1xScale = scaleLinear({
|
|
222
|
-
domain: [0, Math.max(groupOneMax, groupTwoMax) * offset],
|
|
223
|
-
range: [xMax / 2, 0]
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
// group 2
|
|
227
|
-
var g2xScale = scaleLinear({
|
|
228
|
-
domain: g1xScale.domain(),
|
|
229
|
-
range: [xMax / 2, xMax],
|
|
230
|
-
nice: true
|
|
231
|
-
})
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (config.visualizationType === 'Scatter Plot') {
|
|
235
|
-
if (config.xAxis.type === 'continuous') {
|
|
236
|
-
xScale = scaleLinear({
|
|
237
|
-
domain: [0, Math.max.apply(null, xScale.domain())],
|
|
238
|
-
range: [0, xMax]
|
|
239
|
-
})
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (config.visualizationType === 'Deviation Bar') {
|
|
244
|
-
const leftOffset = config.isLollipopChart ? 1.05 : 1.03
|
|
245
|
-
yScale = scaleBand({
|
|
246
|
-
domain: xAxisDataMapped,
|
|
247
|
-
range: [0, yMax]
|
|
248
|
-
})
|
|
249
|
-
xScale = scaleLinear({
|
|
250
|
-
domain: [min * leftOffset, Math.max(Number(config.xAxis.target), max)],
|
|
251
|
-
range: [0, xMax],
|
|
252
|
-
round: true,
|
|
253
|
-
nice: true
|
|
254
|
-
})
|
|
255
|
-
}
|
|
256
|
-
// Handle Box Plots
|
|
257
|
-
if (config.visualizationType === 'Box Plot') {
|
|
258
|
-
const allOutliers = []
|
|
259
|
-
const hasOutliers = config.boxplot.plots.map(b => b.columnOutliers.map(outlier => allOutliers.push(outlier))) && !config.boxplot.hideOutliers
|
|
260
|
-
|
|
261
|
-
// check if outliers are lower
|
|
262
|
-
if (hasOutliers) {
|
|
263
|
-
let outlierMin = Math.min(...allOutliers)
|
|
264
|
-
let outlierMax = Math.max(...allOutliers)
|
|
265
|
-
|
|
266
|
-
// check if outliers exceed standard bounds
|
|
267
|
-
if (outlierMin < min) min = outlierMin
|
|
268
|
-
if (outlierMax > max) max = outlierMax
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// check fences for max/min
|
|
272
|
-
let lowestFence = Math.min(...config.boxplot.plots.map(item => item.columnLowerBounds))
|
|
273
|
-
let highestFence = Math.max(...config.boxplot.plots.map(item => item.columnUpperBounds))
|
|
274
|
-
|
|
275
|
-
if (lowestFence < min) min = lowestFence
|
|
276
|
-
if (highestFence > max) max = highestFence
|
|
277
|
-
|
|
278
|
-
// Set Scales
|
|
279
|
-
yScale = scaleLinear({
|
|
280
|
-
range: [yMax, 0],
|
|
281
|
-
round: true,
|
|
282
|
-
domain: [min, max]
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
xScale = scaleBand({
|
|
286
|
-
range: [0, xMax],
|
|
287
|
-
round: true,
|
|
288
|
-
domain: config.boxplot.categories,
|
|
289
|
-
padding: 0.4
|
|
290
|
-
})
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const shouldAbbreviate = true
|
|
60
|
+
// refs
|
|
61
|
+
const triggerRef = useRef()
|
|
62
|
+
const svgRef = useRef()
|
|
63
|
+
const dataRef = useIntersectionObserver(triggerRef, {
|
|
64
|
+
freezeOnceVisible: false
|
|
65
|
+
})
|
|
295
66
|
|
|
296
67
|
const handleLeftTickFormatting = tick => {
|
|
68
|
+
if (config.useLogScale && tick === 0.1) {
|
|
69
|
+
//when logarithmic scale applied change value of first tick
|
|
70
|
+
tick = 0
|
|
71
|
+
}
|
|
297
72
|
if (config.runtime.yAxis.type === 'date') return formatDate(parseDate(tick))
|
|
298
73
|
if (config.orientation === 'vertical') return formatNumber(tick, 'left', shouldAbbreviate)
|
|
299
74
|
return tick
|
|
300
75
|
}
|
|
301
76
|
|
|
302
77
|
const handleBottomTickFormatting = tick => {
|
|
78
|
+
if (config.useLogScale && tick === 0.1) {
|
|
79
|
+
// when logaritmic scale applyed change value FIRST of tick
|
|
80
|
+
tick = 0
|
|
81
|
+
}
|
|
303
82
|
if (config.runtime.xAxis.type === 'date') return formatDate(tick)
|
|
304
83
|
if (config.orientation === 'horizontal') return formatNumber(tick, 'left', shouldAbbreviate)
|
|
305
84
|
if (config.xAxis.type === 'continuous') return formatNumber(tick, 'bottom', shouldAbbreviate)
|
|
@@ -307,8 +86,6 @@ export default function LinearChart() {
|
|
|
307
86
|
}
|
|
308
87
|
|
|
309
88
|
const countNumOfTicks = axis => {
|
|
310
|
-
// function get number of ticks based on bar type & users value
|
|
311
|
-
const isHorizontal = config.orientation === 'horizontal'
|
|
312
89
|
const { numTicks } = config.runtime[axis]
|
|
313
90
|
let tickCount = undefined
|
|
314
91
|
|
|
@@ -344,14 +121,136 @@ export default function LinearChart() {
|
|
|
344
121
|
return tickCount
|
|
345
122
|
}
|
|
346
123
|
|
|
347
|
-
|
|
124
|
+
// Tooltip helper for getting data to the closest date/category hovered.
|
|
125
|
+
const getXValueFromCoordinate = x => {
|
|
126
|
+
if (xScale.type === 'point') {
|
|
127
|
+
// Find the closest x value by calculating the minimum distance
|
|
128
|
+
let closestX = null
|
|
129
|
+
let minDistance = Number.MAX_VALUE
|
|
130
|
+
let offset = x - yAxis.size
|
|
131
|
+
|
|
132
|
+
data.forEach(d => {
|
|
133
|
+
const xPosition = xAxis.type === 'date' ? xScale(parseDate(d[xAxis.dataKey])) : xScale(d[xAxis.dataKey])
|
|
134
|
+
const distance = Math.abs(Number(xPosition - offset))
|
|
135
|
+
|
|
136
|
+
if (distance < minDistance) {
|
|
137
|
+
minDistance = distance
|
|
138
|
+
closestX = xAxis.type === 'date' ? parseDate(d[xAxis.dataKey]) : d[xAxis.dataKey]
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
return closestX
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// import tooltip helpers
|
|
146
|
+
const { tooltipData, showTooltip, hideTooltip } = useTooltip()
|
|
147
|
+
|
|
148
|
+
const handleTooltipMouseOver = (e, data) => {
|
|
149
|
+
// get the svg coordinates of the mouse
|
|
150
|
+
// and get the closest values
|
|
151
|
+
const eventSvgCoords = localPoint(e)
|
|
152
|
+
const { x, y } = eventSvgCoords
|
|
153
|
+
|
|
154
|
+
const { runtime } = config
|
|
155
|
+
|
|
156
|
+
let closestXScaleValue = getXValueFromCoordinate(x)
|
|
157
|
+
let formattedDate = formatDate(closestXScaleValue)
|
|
158
|
+
|
|
159
|
+
let yScaleValues
|
|
160
|
+
if (xAxis.type === 'categorical') {
|
|
161
|
+
yScaleValues = data.filter(d => d[xAxis.dataKey] === closestXScaleValue)
|
|
162
|
+
} else {
|
|
163
|
+
yScaleValues = rawData.filter(d => formatDate(parseDate(d[xAxis.dataKey])) === formattedDate)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let seriesToInclude = []
|
|
167
|
+
let stageColumns = []
|
|
168
|
+
let ciItems = []
|
|
169
|
+
|
|
170
|
+
// loop through series for items to add to tooltip.
|
|
171
|
+
// there is probably a better way of doing this.
|
|
172
|
+
config.series?.map(s => {
|
|
173
|
+
if (s.type === 'Forecasting') {
|
|
174
|
+
stageColumns.push(s.stageColumn)
|
|
175
|
+
|
|
176
|
+
// greedy fn 😭
|
|
177
|
+
s?.confidenceIntervals.map(ci => {
|
|
178
|
+
if (ci.showInTooltip === true) {
|
|
179
|
+
ciItems.push(ci.low)
|
|
180
|
+
ciItems.push(ci.high)
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
let standardLoopItems = []
|
|
187
|
+
|
|
188
|
+
if (config.visualizationType === 'Combo') {
|
|
189
|
+
standardLoopItems = [runtime.xAxis.dataKey, ...runtime?.barSeriesKeys, ...stageColumns, ...ciItems]
|
|
190
|
+
} else {
|
|
191
|
+
standardLoopItems = [runtime.xAxis.dataKey, ...stageColumns, ...ciItems]
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
standardLoopItems.map(seriesKey => {
|
|
195
|
+
if (!seriesKey) return false
|
|
196
|
+
if (!yScaleValues[0]) return false
|
|
197
|
+
for (const item of Object.entries(yScaleValues[0])) {
|
|
198
|
+
if (item[0] === seriesKey) {
|
|
199
|
+
seriesToInclude.push(item)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
// filter out the series that aren't added to the map.
|
|
205
|
+
if (!seriesToInclude) return
|
|
206
|
+
let initialTooltipData = Object.fromEntries(seriesToInclude) ? Object.fromEntries(seriesToInclude) : {}
|
|
207
|
+
|
|
208
|
+
let tooltipData = {}
|
|
209
|
+
tooltipData.data = initialTooltipData
|
|
210
|
+
tooltipData.dataXPosition = x + 10
|
|
211
|
+
tooltipData.dataYPosition = y
|
|
212
|
+
|
|
213
|
+
let tooltipInformation = {
|
|
214
|
+
tooltipData: tooltipData,
|
|
215
|
+
tooltipTop: 0,
|
|
216
|
+
tooltipValues: yScaleValues,
|
|
217
|
+
tooltipLeft: x
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
showTooltip(tooltipInformation)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const handleTooltipMouseOff = () => {
|
|
224
|
+
hideTooltip()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Make sure the chart is visible if in the editor
|
|
228
|
+
/* eslint-disable react-hooks/exhaustive-deps */
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
const element = document.querySelector('.isEditor')
|
|
231
|
+
if (element) {
|
|
232
|
+
// parent element is visible
|
|
233
|
+
setAnimatedChart(prevState => true)
|
|
234
|
+
}
|
|
235
|
+
}) /* eslint-disable-line */
|
|
236
|
+
|
|
237
|
+
// If the chart is in view, set to animate if it has not already played
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
if (dataRef?.isIntersecting === true && config.animate) {
|
|
240
|
+
setTimeout(() => {
|
|
241
|
+
setAnimatedChart(prevState => true)
|
|
242
|
+
}, 500)
|
|
243
|
+
}
|
|
244
|
+
}, [dataRef?.isIntersecting, config.animate])
|
|
245
|
+
|
|
246
|
+
const { orientation, xAxis, yAxis } = config
|
|
348
247
|
|
|
349
248
|
return isNaN(width) ? (
|
|
350
249
|
<></>
|
|
351
250
|
) : (
|
|
352
251
|
<ErrorBoundary component='LinearChart'>
|
|
353
252
|
<svg width={width} height={height} className={`linear ${config.animate ? 'animated' : ''} ${animatedChart && config.animate ? 'animate' : ''}`} role='img' aria-label={handleChartAriaLabels(config)} tabIndex={0} ref={svgRef}>
|
|
354
|
-
{/*
|
|
253
|
+
{/* Highlighted regions */}
|
|
355
254
|
{config.regions
|
|
356
255
|
? config.regions.map(region => {
|
|
357
256
|
if (!Object.keys(region).includes('from') || !Object.keys(region).includes('to')) return null
|
|
@@ -397,7 +296,7 @@ export default function LinearChart() {
|
|
|
397
296
|
|
|
398
297
|
{/* Y axis */}
|
|
399
298
|
{config.visualizationType !== 'Spark Line' && (
|
|
400
|
-
<AxisLeft scale={yScale} left={Number(config.runtime.yAxis.size) - config.yAxis.axisPadding} label={config.runtime.yAxis.label} stroke='#333' tickFormat={tick => handleLeftTickFormatting(tick)} numTicks={countNumOfTicks('yAxis')}>
|
|
299
|
+
<AxisLeft scale={yScale} tickLength={config.useLogScale ? 6 : 8} left={Number(config.runtime.yAxis.size) - config.yAxis.axisPadding} label={config.runtime.yAxis.label} stroke='#333' tickFormat={tick => handleLeftTickFormatting(tick)} numTicks={countNumOfTicks('yAxis')}>
|
|
401
300
|
{props => {
|
|
402
301
|
const axisCenter = config.runtime.horizontal ? (props.axisToPoint.y - props.axisFromPoint.y) / 2 : (props.axisFromPoint.y - props.axisToPoint.y) / 2
|
|
403
302
|
const horizontalTickOffset = yMax / props.ticks.length / 2 - (yMax / props.ticks.length) * (1 - config.barThickness) + 5
|
|
@@ -406,12 +305,15 @@ export default function LinearChart() {
|
|
|
406
305
|
{props.ticks.map((tick, i) => {
|
|
407
306
|
const minY = props.ticks[0].to.y
|
|
408
307
|
const barMinHeight = 15 // 15 is the min height for bars by default
|
|
308
|
+
const showTicks = String(tick.value).startsWith('1') || tick.value === 0.1 ? 'block' : 'none'
|
|
309
|
+
const tickLength = showTicks === 'block' ? 7 : 0
|
|
310
|
+
const to = { x: tick.to.x - tickLength, y: tick.to.y }
|
|
409
311
|
|
|
410
312
|
return (
|
|
411
313
|
<Group key={`vx-tick-${tick.value}-${i}`} className={'vx-axis-tick'}>
|
|
412
|
-
{!config.runtime.yAxis.hideTicks && <Line from={tick.from} to={tick.to} stroke={config.yAxis.tickColor} display={config.runtime.horizontal ? 'none' : 'block'} />}
|
|
314
|
+
{!config.runtime.yAxis.hideTicks && <Line from={tick.from} to={config.useLogScale ? to : tick.to} stroke={config.yAxis.tickColor} display={config.runtime.horizontal ? 'none' : 'block'} />}
|
|
413
315
|
|
|
414
|
-
{config.runtime.yAxis.gridLines ? <Line from={{ x: tick.from.x + xMax, y: tick.from.y }} to={tick.from} stroke='rgba(0,0,0,0.3)' /> : ''}
|
|
316
|
+
{config.runtime.yAxis.gridLines ? <Line display={config.useLogScale && showTicks} from={{ x: tick.from.x + xMax, y: tick.from.y }} to={tick.from} stroke='rgba(0,0,0,0.3)' /> : ''}
|
|
415
317
|
|
|
416
318
|
{config.orientation === 'horizontal' && config.visualizationSubType !== 'stacked' && config.yAxis.labelPlacement === 'On Date/Category Axis' && !config.yAxis.hideLabel && (
|
|
417
319
|
<Text
|
|
@@ -440,8 +342,10 @@ export default function LinearChart() {
|
|
|
440
342
|
</Text>
|
|
441
343
|
)}
|
|
442
344
|
|
|
443
|
-
{config.orientation
|
|
345
|
+
{config.orientation === 'vertical' && config.visualizationType !== 'Paired Bar' && !config.yAxis.hideLabel && (
|
|
444
346
|
<Text
|
|
347
|
+
display={config.useLogScale ? showTicks : 'block'}
|
|
348
|
+
dx={config.useLogScale ? -6 : 0}
|
|
445
349
|
x={config.runtime.horizontal ? tick.from.x + 2 : tick.to.x}
|
|
446
350
|
y={tick.to.y + (config.runtime.horizontal ? horizontalTickOffset : 0)}
|
|
447
351
|
verticalAnchor={config.runtime.horizontal ? 'start' : 'middle'}
|
|
@@ -490,7 +394,7 @@ export default function LinearChart() {
|
|
|
490
394
|
)
|
|
491
395
|
})}
|
|
492
396
|
{!config.yAxis.rightHideAxis && <Line from={props.axisFromPoint} to={props.axisToPoint} stroke='#333' />}
|
|
493
|
-
<Text className='y-label' textAnchor='middle' verticalAnchor='start' transform={`translate(${config.yAxis.rightLabelOffsetSize ? config.yAxis.rightLabelOffsetSize : 0}, ${axisCenter}) rotate(90)`} fontWeight='bold' fill={config.yAxis.rightAxisLabelColor}>
|
|
397
|
+
<Text className='y-label' textAnchor='middle' verticalAnchor='start' transform={`translate(${config.yAxis.rightLabelOffsetSize ? config.yAxis.rightLabelOffsetSize : 0}, ${axisCenter}) rotate(-90)`} fontWeight='bold' fill={config.yAxis.rightAxisLabelColor}>
|
|
494
398
|
{props.label}
|
|
495
399
|
</Text>
|
|
496
400
|
</Group>
|
|
@@ -529,12 +433,19 @@ export default function LinearChart() {
|
|
|
529
433
|
return (
|
|
530
434
|
<Group className='bottom-axis'>
|
|
531
435
|
{props.ticks.map((tick, i) => {
|
|
436
|
+
// when using LogScale show major ticks values only
|
|
437
|
+
const showTick = String(tick.value).startsWith('1') || tick.value === 0.1 ? 'block' : 'none'
|
|
532
438
|
const tickWidth = xMax / props.ticks.length
|
|
439
|
+
const tickLength = showTick === 'block' ? 16 : 8
|
|
440
|
+
const to = { x: tick.to.x, y: tickLength }
|
|
441
|
+
|
|
533
442
|
return (
|
|
534
443
|
<Group key={`vx-tick-${tick.value}-${i}`} className={'vx-axis-tick'}>
|
|
535
|
-
{!config.xAxis.hideTicks && <Line from={tick.from} to={tick.to} stroke={config.xAxis.tickColor} />}
|
|
444
|
+
{!config.xAxis.hideTicks && <Line from={tick.from} to={config.orientation === 'horizontal' && config.useLogScale ? to : tick.to} stroke={config.xAxis.tickColor} strokeWidth={showTick === 'block' ? 1.3 : 1} />}
|
|
536
445
|
{!config.xAxis.hideLabel && (
|
|
537
446
|
<Text
|
|
447
|
+
dy={config.orientation === 'horizontal' && config.useLogScale ? 8 : 0}
|
|
448
|
+
display={config.orientation === 'horizontal' && config.useLogScale ? showTick : 'block'}
|
|
538
449
|
transform={`translate(${tick.to.x}, ${tick.to.y}) rotate(-${!config.runtime.horizontal ? config.runtime.xAxis.tickRotation : 0})`}
|
|
539
450
|
verticalAnchor='start'
|
|
540
451
|
textAnchor={config.runtime.xAxis.tickRotation && config.runtime.xAxis.tickRotation !== '0' ? 'end' : 'middle'}
|
|
@@ -623,28 +534,95 @@ export default function LinearChart() {
|
|
|
623
534
|
</AxisBottom>
|
|
624
535
|
</>
|
|
625
536
|
)}
|
|
626
|
-
|
|
627
537
|
{config.visualizationType === 'Deviation Bar' && <DeviationBar xScale={xScale} yScale={yScale} width={xMax} height={yMax} />}
|
|
628
538
|
{config.visualizationType === 'Paired Bar' && <PairedBarChart originalWidth={width} width={xMax} height={yMax} />}
|
|
629
539
|
{config.visualizationType === 'Scatter Plot' && <CoveScatterPlot xScale={xScale} yScale={yScale} getXAxisData={getXAxisData} getYAxisData={getYAxisData} />}
|
|
630
540
|
{config.visualizationType === 'Box Plot' && <CoveBoxPlot xScale={xScale} yScale={yScale} />}
|
|
631
541
|
{(config.visualizationType === 'Area Chart' || config.visualizationType === 'Combo') && <CoveAreaChart xScale={xScale} yScale={yScale} yMax={yMax} xMax={xMax} chartRef={svgRef} />}
|
|
632
|
-
|
|
633
|
-
{
|
|
634
|
-
{
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
542
|
+
{(config.visualizationType === 'Bar' || config.visualizationType === 'Combo') && <BarChart xScale={xScale} yScale={yScale} seriesScale={seriesScale} xMax={xMax} yMax={yMax} getXAxisData={getXAxisData} getYAxisData={getYAxisData} animatedChart={animatedChart} visible={animatedChart} />}
|
|
543
|
+
{(config.visualizationType === 'Line' || config.visualizationType === 'Combo') && <LineChart xScale={xScale} yScale={yScale} getXAxisData={getXAxisData} getYAxisData={getYAxisData} xMax={xMax} yMax={yMax} seriesStyle={config.series} />}
|
|
544
|
+
{(config.visualizationType === 'Forecasting' || config.visualizationType === 'Combo') && (
|
|
545
|
+
<Forecasting
|
|
546
|
+
hideTooltip={hideTooltip}
|
|
547
|
+
showTooltip={showTooltip}
|
|
548
|
+
tooltipData={tooltipData}
|
|
549
|
+
xScale={xScale}
|
|
550
|
+
yScale={yScale}
|
|
551
|
+
width={xMax}
|
|
552
|
+
height={yMax}
|
|
553
|
+
xScaleNoPadding={xScaleNoPadding}
|
|
554
|
+
chartRef={svgRef}
|
|
555
|
+
getXValueFromCoordinate={getXValueFromCoordinate}
|
|
556
|
+
handleTooltipMouseOver={handleTooltipMouseOver}
|
|
557
|
+
handleTooltipMouseOff={handleTooltipMouseOff}
|
|
558
|
+
/>
|
|
639
559
|
)}
|
|
640
560
|
|
|
561
|
+
{/* y anchors */}
|
|
562
|
+
{config.yAxis.anchors &&
|
|
563
|
+
config.yAxis.anchors.map(anchor => {
|
|
564
|
+
return <Line strokeDasharray={handleLineType(anchor.lineStyle)} stroke='rgba(0,0,0,1)' className='customAnchor' from={{ x: 0 + config.yAxis.size, y: yScale(anchor.value) }} to={{ x: xMax, y: yScale(anchor.value) }} display={config.runtime.horizontal ? 'none' : 'block'} />
|
|
565
|
+
})}
|
|
566
|
+
|
|
641
567
|
{/* Line chart */}
|
|
642
568
|
{/* TODO: Make this just line or combo? */}
|
|
643
|
-
{config.visualizationType !== 'Bar' &&
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
569
|
+
{config.visualizationType !== 'Bar' &&
|
|
570
|
+
config.visualizationType !== 'Paired Bar' &&
|
|
571
|
+
config.visualizationType !== 'Box Plot' &&
|
|
572
|
+
config.visualizationType !== 'Area Chart' &&
|
|
573
|
+
config.visualizationType !== 'Scatter Plot' &&
|
|
574
|
+
config.visualizationType !== 'Deviation Bar' &&
|
|
575
|
+
config.visualizationType !== 'Forecasting' && (
|
|
576
|
+
<>
|
|
577
|
+
<LineChart xScale={xScale} yScale={yScale} getXAxisData={getXAxisData} getYAxisData={getYAxisData} xMax={xMax} yMax={yMax} seriesStyle={config.series} />
|
|
578
|
+
</>
|
|
579
|
+
)}
|
|
580
|
+
|
|
581
|
+
{/* y anchors */}
|
|
582
|
+
{config.yAxis.anchors &&
|
|
583
|
+
config.yAxis.anchors.map(anchor => {
|
|
584
|
+
let anchorPosition = yScale(anchor.value)
|
|
585
|
+
const padding = config.orientation === 'horizontal' ? Number(config.xAxis.size) : Number(config.yAxis.size)
|
|
586
|
+
const middleOffset = config.orientation === 'horizontal' && config.visualizationType === 'Bar' ? config.barHeight / 4 : 0
|
|
587
|
+
|
|
588
|
+
return (
|
|
589
|
+
// prettier-ignore
|
|
590
|
+
<Line
|
|
591
|
+
key={anchor.value}
|
|
592
|
+
strokeDasharray={handleLineType(anchor.lineStyle)}
|
|
593
|
+
stroke={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
|
|
594
|
+
className='anchor-y'
|
|
595
|
+
from={{ x: 0 + padding, y: anchorPosition - middleOffset}}
|
|
596
|
+
to={{ x: width, y: anchorPosition - middleOffset }}
|
|
597
|
+
/>
|
|
598
|
+
)
|
|
599
|
+
})}
|
|
600
|
+
|
|
601
|
+
{/* x anchors */}
|
|
602
|
+
{config.xAxis.anchors &&
|
|
603
|
+
config.xAxis.anchors.map(anchor => {
|
|
604
|
+
let newX = xAxis
|
|
605
|
+
if (orientation === 'horizontal') {
|
|
606
|
+
newX = yAxis
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
let anchorPosition = newX.type === 'date' ? xScale(parseDate(anchor.value, false)) : xScale(anchor.value)
|
|
610
|
+
|
|
611
|
+
const padding = orientation === 'horizontal' ? Number(config.xAxis.size) : Number(config.yAxis.size)
|
|
612
|
+
|
|
613
|
+
return (
|
|
614
|
+
// prettier-ignore
|
|
615
|
+
<Line
|
|
616
|
+
key={anchor.value}
|
|
617
|
+
strokeDasharray={handleLineType(anchor.lineStyle)}
|
|
618
|
+
stroke={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
|
|
619
|
+
fill={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
|
|
620
|
+
className='anchor-x'
|
|
621
|
+
from={{ x: Number(anchorPosition) + Number(padding), y: 0 }}
|
|
622
|
+
to={{ x: Number(anchorPosition) + Number(padding), y: yMax }}
|
|
623
|
+
/>
|
|
624
|
+
)
|
|
625
|
+
})}
|
|
648
626
|
</svg>
|
|
649
627
|
<ReactTooltip id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`} variant='light' arrowColor='rgba(0,0,0,0)' className='tooltip' />
|
|
650
628
|
<div className='animation-trigger' ref={triggerRef} />
|