@cdc/chart 4.23.4 → 4.23.5
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 +52384 -50875
- package/examples/feature/__data__/planet-example-data.json +2 -19
- 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/planet-chart-logaritmic-config.json +170 -0
- package/examples/feature/boxplot/valid-boxplot.csv +17 -0
- package/examples/feature/filters/filter-testing.json +37 -3
- package/examples/feature/forecasting/random_data.csv +366 -0
- package/examples/feature/line/line-chart.json +2 -2
- 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/index.html +8 -8
- package/package.json +2 -2
- package/src/CdcChart.jsx +294 -14
- package/src/components/AreaChart.jsx +27 -20
- package/src/components/BarChart.jsx +85 -25
- package/src/components/DeviationBar.jsx +32 -32
- package/src/components/EditorPanel.jsx +1105 -184
- package/src/components/Legend.jsx +39 -3
- package/src/components/LineChart.jsx +1 -8
- package/src/components/LinearChart.jsx +121 -270
- package/src/data/initial-state.js +18 -3
- package/src/hooks/useHighlightedBars.js +154 -0
- package/src/hooks/useMinMax.js +92 -0
- package/src/hooks/useReduceData.js +31 -57
- package/src/hooks/useScales.js +202 -0
- /package/examples/feature/area/{area-chart.json → area-chart-date.json} +0 -0
|
@@ -5,9 +5,10 @@ import { LegendOrdinal, LegendItem, LegendLabel } from '@visx/legend'
|
|
|
5
5
|
import LegendCircle from '@cdc/core/components/LegendCircle'
|
|
6
6
|
|
|
7
7
|
import useLegendClasses from './../hooks/useLegendClasses'
|
|
8
|
+
import { useHighlightedBars } from '../hooks/useHighlightedBars'
|
|
8
9
|
|
|
9
10
|
const Legend = () => {
|
|
10
|
-
const { config, legend, colorScale, seriesHighlight, highlight, twoColorPalette, highlightReset, setSeriesHighlight, dynamicLegendItems, setDynamicLegendItems, transformedData: data, colorPalettes, rawData, setConfig, currentViewport } = useContext(ConfigContext)
|
|
11
|
+
const { config, legend, colorScale, seriesHighlight, highlight, twoColorPalette, tableData, highlightReset, setSeriesHighlight, dynamicLegendItems, setDynamicLegendItems, transformedData: data, colorPalettes, rawData, setConfig, currentViewport } = useContext(ConfigContext)
|
|
11
12
|
|
|
12
13
|
const { innerClasses, containerClasses } = useLegendClasses(config)
|
|
13
14
|
|
|
@@ -103,14 +104,14 @@ const Legend = () => {
|
|
|
103
104
|
if (config.visualizationType === 'Bar' && config.visualizationSubType === 'regular' && colorCode && config.series?.length === 1) {
|
|
104
105
|
let palette = colorPalettes[config.palette]
|
|
105
106
|
|
|
106
|
-
while (
|
|
107
|
+
while (tableData.length > palette.length) {
|
|
107
108
|
palette = palette.concat(palette)
|
|
108
109
|
}
|
|
109
110
|
palette = palette.slice(0, data.length)
|
|
110
111
|
//store uniq values to Set by colorCode
|
|
111
112
|
const set = new Set()
|
|
112
113
|
|
|
113
|
-
|
|
114
|
+
tableData.forEach(d => set.add(d[colorCode]))
|
|
114
115
|
|
|
115
116
|
// create labels with uniq values
|
|
116
117
|
const uniqeLabels = Array.from(set).map((val, i) => {
|
|
@@ -133,6 +134,10 @@ const Legend = () => {
|
|
|
133
134
|
const marginTop = isBottomOrSmallViewport && isHorizontal ? `${config.runtime.xAxis.size}px` : '0px'
|
|
134
135
|
const marginBottom = isBottomOrSmallViewport ? '15px' : '0px'
|
|
135
136
|
|
|
137
|
+
const { HighLightedBarUtils } = useHighlightedBars(config)
|
|
138
|
+
|
|
139
|
+
let highLightedLegendItems = HighLightedBarUtils.findDuplicates(config.highlightedBarValues)
|
|
140
|
+
|
|
136
141
|
if (!legend) return null
|
|
137
142
|
|
|
138
143
|
if (!legend.dynamicLegend)
|
|
@@ -182,6 +187,37 @@ const Legend = () => {
|
|
|
182
187
|
</LegendItem>
|
|
183
188
|
)
|
|
184
189
|
})}
|
|
190
|
+
|
|
191
|
+
{highLightedLegendItems.map((bar, i) => {
|
|
192
|
+
// if duplicates only return first item
|
|
193
|
+
let className = 'legend-item'
|
|
194
|
+
let itemName = bar.legendLabel
|
|
195
|
+
|
|
196
|
+
if (!itemName) return
|
|
197
|
+
if (seriesHighlight.length > 0 && false === seriesHighlight.includes(itemName)) {
|
|
198
|
+
className += ' inactive'
|
|
199
|
+
}
|
|
200
|
+
return (
|
|
201
|
+
<LegendItem
|
|
202
|
+
className={className}
|
|
203
|
+
tabIndex={0}
|
|
204
|
+
key={`legend-quantile-${i}`}
|
|
205
|
+
onKeyPress={e => {
|
|
206
|
+
if (e.key === 'Enter') {
|
|
207
|
+
highlight(bar.legendLabel)
|
|
208
|
+
}
|
|
209
|
+
}}
|
|
210
|
+
onClick={() => {
|
|
211
|
+
highlight(bar.legendLabel)
|
|
212
|
+
}}
|
|
213
|
+
>
|
|
214
|
+
<LegendCircle fill='transparent' borderColor={bar.color ? bar.color : `rgba(255, 102, 1)`} />{' '}
|
|
215
|
+
<LegendLabel align='left' margin='0 0 0 4px'>
|
|
216
|
+
{bar.legendLabel ? bar.legendLabel : bar.value}
|
|
217
|
+
</LegendLabel>
|
|
218
|
+
</LegendItem>
|
|
219
|
+
)
|
|
220
|
+
})}
|
|
185
221
|
{seriesHighlight.length > 0 && (
|
|
186
222
|
<button className={`legend-reset ${config.theme}`} onClick={labels => highlightReset(labels)} tabIndex={0}>
|
|
187
223
|
Reset
|
|
@@ -8,7 +8,6 @@ import { Text } from '@visx/text'
|
|
|
8
8
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
9
9
|
import ConfigContext from '../ConfigContext'
|
|
10
10
|
import useRightAxis from '../hooks/useRightAxis'
|
|
11
|
-
import isNumber from '@cdc/core/helpers/isNumber'
|
|
12
11
|
|
|
13
12
|
export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData, xMax, yMax, seriesStyle = 'Line' }) {
|
|
14
13
|
const { colorPalettes, transformedData: data, colorScale, seriesHighlight, config, formatNumber, formatDate, parseDate, isNumber, updateConfig, handleLineType } = useContext(ConfigContext)
|
|
@@ -72,13 +71,7 @@ export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData,
|
|
|
72
71
|
isNumber(d[seriesKey]) && (
|
|
73
72
|
<Group key={`series-${seriesKey}-point-${dataIndex}`}>
|
|
74
73
|
{/* Render legend */}
|
|
75
|
-
<Text
|
|
76
|
-
display={config.labels ? 'block' : 'none'}
|
|
77
|
-
x={xScale(getXAxisData(d))}
|
|
78
|
-
y={seriesAxis === 'Right' ? yScaleRight(getYAxisData(d, seriesKey)) : yScale(getYAxisData(d, seriesKey))}
|
|
79
|
-
fill={colorScale ? colorScale(config.runtime.seriesLabels ? config.runtime.seriesLabels[seriesKey] : seriesKey) : '#000'}
|
|
80
|
-
textAnchor='middle'
|
|
81
|
-
>
|
|
74
|
+
<Text display={config.labels ? 'block' : 'none'} x={xScale(getXAxisData(d))} y={seriesAxis === 'Right' ? yScaleRight(getYAxisData(d, seriesKey)) : yScale(getYAxisData(d, seriesKey))} fill={'#000'} textAnchor='middle'>
|
|
82
75
|
{formatNumber(d[seriesKey], 'left')}
|
|
83
76
|
</Text>
|
|
84
77
|
|
|
@@ -4,302 +4,78 @@ 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'
|
|
9
8
|
|
|
10
|
-
import CoveScatterPlot from './ScatterPlot'
|
|
11
9
|
import BarChart from './BarChart'
|
|
12
|
-
import LineChart from './LineChart'
|
|
13
10
|
import ConfigContext from '../ConfigContext'
|
|
11
|
+
import CoveAreaChart from './AreaChart'
|
|
12
|
+
import CoveBoxPlot from './BoxPlot'
|
|
13
|
+
import CoveScatterPlot from './ScatterPlot'
|
|
14
|
+
import DeviationBar from './DeviationBar'
|
|
15
|
+
import LineChart from './LineChart'
|
|
14
16
|
import PairedBarChart from './PairedBarChart'
|
|
15
17
|
import useIntersectionObserver from './useIntersectionObserver'
|
|
16
|
-
import CoveBoxPlot from './BoxPlot'
|
|
17
|
-
import CoveAreaChart from './AreaChart'
|
|
18
18
|
|
|
19
19
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
20
20
|
import '../scss/LinearChart.scss'
|
|
21
21
|
import useReduceData from '../hooks/useReduceData'
|
|
22
|
+
import useScales from '../hooks/useScales'
|
|
23
|
+
import useMinMax from '../hooks/useMinMax'
|
|
22
24
|
import useRightAxis from '../hooks/useRightAxis'
|
|
23
25
|
import useTopAxis from '../hooks/useTopAxis'
|
|
24
|
-
import { DeviationBar } from './DeviationBar'
|
|
25
26
|
|
|
26
|
-
// TODO: Move scaling functions into hooks to manage complexity
|
|
27
27
|
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
|
-
})
|
|
38
|
-
|
|
39
|
-
// Make sure the chart is visible if in the editor
|
|
40
|
-
/* eslint-disable react-hooks/exhaustive-deps */
|
|
41
|
-
useEffect(() => {
|
|
42
|
-
const element = document.querySelector('.isEditor')
|
|
43
|
-
if (element) {
|
|
44
|
-
// parent element is visible
|
|
45
|
-
setAnimatedChart(prevState => true)
|
|
46
|
-
}
|
|
47
|
-
}) /* eslint-disable-line */
|
|
28
|
+
const { transformedData: data, dimensions, config, parseDate, formatDate, currentViewport, formatNumber, handleChartAriaLabels, updateConfig, handleLineType } = useContext(ConfigContext)
|
|
48
29
|
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
setAnimatedChart(prevState => true)
|
|
54
|
-
}, 500)
|
|
55
|
-
}
|
|
56
|
-
}, [dataRef?.isIntersecting, config.animate])
|
|
30
|
+
// getters & functions
|
|
31
|
+
const getXAxisData = d => (config.runtime.xAxis.type === 'date' ? parseDate(d[config.runtime.originalXAxis.dataKey]).getTime() : d[config.runtime.originalXAxis.dataKey])
|
|
32
|
+
const getYAxisData = (d, seriesKey) => d[seriesKey]
|
|
33
|
+
const xAxisDataMapped = data.map(d => getXAxisData(d))
|
|
57
34
|
|
|
58
|
-
|
|
35
|
+
// configure width
|
|
36
|
+
let [width] = dimensions
|
|
37
|
+
if (config && config.legend && !config.legend.hide && config.legend.position !== 'bottom' && ['lg', 'md'].includes(currentViewport)) {
|
|
59
38
|
width = width * 0.73
|
|
60
39
|
}
|
|
40
|
+
// configure height , yMax, xMAx
|
|
61
41
|
const { horizontal: heightHorizontal } = config.heights
|
|
42
|
+
const isHorizontal = config.orientation === 'horizontal'
|
|
43
|
+
const shouldAbbreviate = true
|
|
62
44
|
const height = config.aspectRatio ? width * config.aspectRatio : config.heights[config.orientation]
|
|
63
45
|
const xMax = width - config.runtime.yAxis.size - (config.visualizationType === 'Combo' ? config.yAxis.rightAxisSize : 0)
|
|
64
46
|
const yMax = height - (config.orientation === 'horizontal' ? 0 : config.runtime.xAxis.size)
|
|
65
47
|
|
|
48
|
+
// hooks % states
|
|
49
|
+
const { minValue, maxValue, existPositiveValue, isAllLine } = useReduceData(config, data)
|
|
66
50
|
const { yScaleRight, hasRightAxis } = useRightAxis({ config, yMax, data, updateConfig })
|
|
67
51
|
const { hasTopAxis } = useTopAxis(config)
|
|
52
|
+
const [animatedChart, setAnimatedChart] = useState(false)
|
|
53
|
+
const properties = { data, config, minValue, maxValue, isAllLine, existPositiveValue, xAxisDataMapped, xMax, yMax }
|
|
54
|
+
const { min, max } = useMinMax(properties)
|
|
55
|
+
const { xScale, yScale, seriesScale, g1xScale, g2xScale } = useScales({ ...properties, min, max })
|
|
68
56
|
|
|
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
|
|
57
|
+
// refs
|
|
58
|
+
const triggerRef = useRef()
|
|
59
|
+
const svgRef = useRef()
|
|
60
|
+
const dataRef = useIntersectionObserver(triggerRef, {
|
|
61
|
+
freezeOnceVisible: false
|
|
62
|
+
})
|
|
295
63
|
|
|
296
64
|
const handleLeftTickFormatting = tick => {
|
|
65
|
+
if (config.useLogScale && tick === 0.1) {
|
|
66
|
+
//when logaritmic scale applyed change value of FIRST tick
|
|
67
|
+
tick = 0
|
|
68
|
+
}
|
|
297
69
|
if (config.runtime.yAxis.type === 'date') return formatDate(parseDate(tick))
|
|
298
70
|
if (config.orientation === 'vertical') return formatNumber(tick, 'left', shouldAbbreviate)
|
|
299
71
|
return tick
|
|
300
72
|
}
|
|
301
73
|
|
|
302
74
|
const handleBottomTickFormatting = tick => {
|
|
75
|
+
if (config.useLogScale && tick === 0.1) {
|
|
76
|
+
// when logaritmic scale applyed change value FIRST of tick
|
|
77
|
+
tick = 0
|
|
78
|
+
}
|
|
303
79
|
if (config.runtime.xAxis.type === 'date') return formatDate(tick)
|
|
304
80
|
if (config.orientation === 'horizontal') return formatNumber(tick, 'left', shouldAbbreviate)
|
|
305
81
|
if (config.xAxis.type === 'continuous') return formatNumber(tick, 'bottom', shouldAbbreviate)
|
|
@@ -307,8 +83,6 @@ export default function LinearChart() {
|
|
|
307
83
|
}
|
|
308
84
|
|
|
309
85
|
const countNumOfTicks = axis => {
|
|
310
|
-
// function get number of ticks based on bar type & users value
|
|
311
|
-
const isHorizontal = config.orientation === 'horizontal'
|
|
312
86
|
const { numTicks } = config.runtime[axis]
|
|
313
87
|
let tickCount = undefined
|
|
314
88
|
|
|
@@ -344,7 +118,26 @@ export default function LinearChart() {
|
|
|
344
118
|
return tickCount
|
|
345
119
|
}
|
|
346
120
|
|
|
347
|
-
|
|
121
|
+
// Make sure the chart is visible if in the editor
|
|
122
|
+
/* eslint-disable react-hooks/exhaustive-deps */
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
const element = document.querySelector('.isEditor')
|
|
125
|
+
if (element) {
|
|
126
|
+
// parent element is visible
|
|
127
|
+
setAnimatedChart(prevState => true)
|
|
128
|
+
}
|
|
129
|
+
}) /* eslint-disable-line */
|
|
130
|
+
|
|
131
|
+
// If the chart is in view, set to animate if it has not already played
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (dataRef?.isIntersecting === true && config.animate) {
|
|
134
|
+
setTimeout(() => {
|
|
135
|
+
setAnimatedChart(prevState => true)
|
|
136
|
+
}, 500)
|
|
137
|
+
}
|
|
138
|
+
}, [dataRef?.isIntersecting, config.animate])
|
|
139
|
+
|
|
140
|
+
const { orientation, xAxis, yAxis } = config
|
|
348
141
|
|
|
349
142
|
return isNaN(width) ? (
|
|
350
143
|
<></>
|
|
@@ -397,7 +190,7 @@ export default function LinearChart() {
|
|
|
397
190
|
|
|
398
191
|
{/* Y axis */}
|
|
399
192
|
{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')}>
|
|
193
|
+
<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
194
|
{props => {
|
|
402
195
|
const axisCenter = config.runtime.horizontal ? (props.axisToPoint.y - props.axisFromPoint.y) / 2 : (props.axisFromPoint.y - props.axisToPoint.y) / 2
|
|
403
196
|
const horizontalTickOffset = yMax / props.ticks.length / 2 - (yMax / props.ticks.length) * (1 - config.barThickness) + 5
|
|
@@ -406,12 +199,15 @@ export default function LinearChart() {
|
|
|
406
199
|
{props.ticks.map((tick, i) => {
|
|
407
200
|
const minY = props.ticks[0].to.y
|
|
408
201
|
const barMinHeight = 15 // 15 is the min height for bars by default
|
|
202
|
+
const showTicks = String(tick.value).startsWith('1') || tick.value === 0.1 ? 'block' : 'none'
|
|
203
|
+
const tickLength = showTicks === 'block' ? 7 : 0
|
|
204
|
+
const to = { x: tick.to.x - tickLength, y: tick.to.y }
|
|
409
205
|
|
|
410
206
|
return (
|
|
411
207
|
<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'} />}
|
|
208
|
+
{!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
209
|
|
|
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)' /> : ''}
|
|
210
|
+
{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
211
|
|
|
416
212
|
{config.orientation === 'horizontal' && config.visualizationSubType !== 'stacked' && config.yAxis.labelPlacement === 'On Date/Category Axis' && !config.yAxis.hideLabel && (
|
|
417
213
|
<Text
|
|
@@ -440,8 +236,10 @@ export default function LinearChart() {
|
|
|
440
236
|
</Text>
|
|
441
237
|
)}
|
|
442
238
|
|
|
443
|
-
{config.orientation
|
|
239
|
+
{config.orientation === 'vertical' && config.visualizationType !== 'Paired Bar' && !config.yAxis.hideLabel && (
|
|
444
240
|
<Text
|
|
241
|
+
display={config.useLogScale ? showTicks : 'block'}
|
|
242
|
+
dx={config.useLogScale ? -6 : 0}
|
|
445
243
|
x={config.runtime.horizontal ? tick.from.x + 2 : tick.to.x}
|
|
446
244
|
y={tick.to.y + (config.runtime.horizontal ? horizontalTickOffset : 0)}
|
|
447
245
|
verticalAnchor={config.runtime.horizontal ? 'start' : 'middle'}
|
|
@@ -529,12 +327,19 @@ export default function LinearChart() {
|
|
|
529
327
|
return (
|
|
530
328
|
<Group className='bottom-axis'>
|
|
531
329
|
{props.ticks.map((tick, i) => {
|
|
330
|
+
// when using LogScale show major ticks values only
|
|
331
|
+
const showTick = String(tick.value).startsWith('1') || tick.value === 0.1 ? 'block' : 'none'
|
|
532
332
|
const tickWidth = xMax / props.ticks.length
|
|
333
|
+
const tickLength = showTick === 'block' ? 16 : 8
|
|
334
|
+
const to = { x: tick.to.x, y: tickLength }
|
|
335
|
+
|
|
533
336
|
return (
|
|
534
337
|
<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} />}
|
|
338
|
+
{!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
339
|
{!config.xAxis.hideLabel && (
|
|
537
340
|
<Text
|
|
341
|
+
dy={config.orientation === 'horizontal' && config.useLogScale ? 8 : 0}
|
|
342
|
+
display={config.orientation === 'horizontal' && config.useLogScale ? showTick : 'block'}
|
|
538
343
|
transform={`translate(${tick.to.x}, ${tick.to.y}) rotate(-${!config.runtime.horizontal ? config.runtime.xAxis.tickRotation : 0})`}
|
|
539
344
|
verticalAnchor='start'
|
|
540
345
|
textAnchor={config.runtime.xAxis.tickRotation && config.runtime.xAxis.tickRotation !== '0' ? 'end' : 'middle'}
|
|
@@ -645,6 +450,52 @@ export default function LinearChart() {
|
|
|
645
450
|
<LineChart xScale={xScale} yScale={yScale} getXAxisData={getXAxisData} getYAxisData={getYAxisData} xMax={xMax} yMax={yMax} seriesStyle={config.series} />
|
|
646
451
|
</>
|
|
647
452
|
)}
|
|
453
|
+
|
|
454
|
+
{/* y anchors */}
|
|
455
|
+
{config.yAxis.anchors &&
|
|
456
|
+
config.yAxis.anchors.map(anchor => {
|
|
457
|
+
let anchorPosition = yScale(anchor.value)
|
|
458
|
+
const padding = config.orientation === 'horizontal' ? Number(config.xAxis.size) : Number(config.yAxis.size)
|
|
459
|
+
const middleOffset = config.orientation === 'horizontal' && config.visualizationType === 'Bar' ? config.barHeight / 4 : 0
|
|
460
|
+
|
|
461
|
+
return (
|
|
462
|
+
// prettier-ignore
|
|
463
|
+
<Line
|
|
464
|
+
key={anchor.value}
|
|
465
|
+
strokeDasharray={handleLineType(anchor.lineStyle)}
|
|
466
|
+
stroke={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
|
|
467
|
+
className='anchor-y'
|
|
468
|
+
from={{ x: 0 + padding, y: anchorPosition - middleOffset}}
|
|
469
|
+
to={{ x: width, y: anchorPosition - middleOffset }}
|
|
470
|
+
/>
|
|
471
|
+
)
|
|
472
|
+
})}
|
|
473
|
+
|
|
474
|
+
{/* x anchors */}
|
|
475
|
+
{config.xAxis.anchors &&
|
|
476
|
+
config.xAxis.anchors.map(anchor => {
|
|
477
|
+
let newX = xAxis
|
|
478
|
+
if (orientation === 'horizontal') {
|
|
479
|
+
newX = yAxis
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
let anchorPosition = newX.type === 'date' ? xScale(parseDate(anchor.value, false)) : xScale(anchor.value)
|
|
483
|
+
|
|
484
|
+
const padding = orientation === 'horizontal' ? Number(config.xAxis.size) : Number(config.yAxis.size)
|
|
485
|
+
|
|
486
|
+
return (
|
|
487
|
+
// prettier-ignore
|
|
488
|
+
<Line
|
|
489
|
+
key={anchor.value}
|
|
490
|
+
strokeDasharray={handleLineType(anchor.lineStyle)}
|
|
491
|
+
stroke={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
|
|
492
|
+
fill={anchor.color ? anchor.color : 'rgba(0,0,0,1)'}
|
|
493
|
+
className='anchor-x'
|
|
494
|
+
from={{ x: Number(anchorPosition) + Number(padding), y: 0 }}
|
|
495
|
+
to={{ x: Number(anchorPosition) + Number(padding), y: yMax }}
|
|
496
|
+
/>
|
|
497
|
+
)
|
|
498
|
+
})}
|
|
648
499
|
</svg>
|
|
649
500
|
<ReactTooltip id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`} variant='light' arrowColor='rgba(0,0,0,0)' className='tooltip' />
|
|
650
501
|
<div className='animation-trigger' ref={triggerRef} />
|
|
@@ -2,6 +2,7 @@ export default {
|
|
|
2
2
|
type: 'chart',
|
|
3
3
|
title: '',
|
|
4
4
|
showTitle: true,
|
|
5
|
+
showDownloadMediaButton: false,
|
|
5
6
|
theme: 'theme-blue',
|
|
6
7
|
animate: false,
|
|
7
8
|
fontSize: 'medium',
|
|
@@ -14,6 +15,9 @@ export default {
|
|
|
14
15
|
barStyle: '',
|
|
15
16
|
roundingStyle: 'standard',
|
|
16
17
|
tipRounding: 'top',
|
|
18
|
+
general: {
|
|
19
|
+
showDownloadButton: false
|
|
20
|
+
},
|
|
17
21
|
padding: {
|
|
18
22
|
left: 5,
|
|
19
23
|
right: 5
|
|
@@ -40,7 +44,8 @@ export default {
|
|
|
40
44
|
rightAxisTickColor: '#333',
|
|
41
45
|
numTicks: '',
|
|
42
46
|
axisPadding: 0,
|
|
43
|
-
tickRotation: 0
|
|
47
|
+
tickRotation: 0,
|
|
48
|
+
anchors: []
|
|
44
49
|
},
|
|
45
50
|
boxplot: {
|
|
46
51
|
plots: [],
|
|
@@ -84,6 +89,7 @@ export default {
|
|
|
84
89
|
horizontal: 750
|
|
85
90
|
},
|
|
86
91
|
xAxis: {
|
|
92
|
+
anchors: [],
|
|
87
93
|
type: 'categorical',
|
|
88
94
|
showTargetLabel: true,
|
|
89
95
|
targetLabel: 'Target',
|
|
@@ -109,9 +115,15 @@ export default {
|
|
|
109
115
|
height: '',
|
|
110
116
|
caption: '',
|
|
111
117
|
showDownloadUrl: false,
|
|
112
|
-
showDataTableLink: true
|
|
118
|
+
showDataTableLink: true,
|
|
119
|
+
indexLabel: '',
|
|
120
|
+
download: false,
|
|
121
|
+
showVertical: true
|
|
113
122
|
},
|
|
114
123
|
orientation: 'vertical',
|
|
124
|
+
color: 'pinkpurple',
|
|
125
|
+
columns: { // start with a blank list
|
|
126
|
+
},
|
|
115
127
|
legend: {
|
|
116
128
|
behavior: 'isolate',
|
|
117
129
|
singleRow: false,
|
|
@@ -150,5 +162,8 @@ export default {
|
|
|
150
162
|
accent: true,
|
|
151
163
|
background: true
|
|
152
164
|
},
|
|
153
|
-
|
|
165
|
+
useLogScale: false,
|
|
166
|
+
filterBehavior: 'Filter Change',
|
|
167
|
+
highlightedBarValues: [],
|
|
168
|
+
series: []
|
|
154
169
|
}
|