@cdc/chart 4.23.2 → 4.23.4
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 +42292 -40337
- package/examples/feature/__data__/area-chart.json +56 -0
- package/examples/{planet-example-data.json → feature/__data__/planet-example-data-max-increase.json} +4 -4
- package/examples/feature/__data__/planet-example-data.json +68 -0
- package/examples/feature/area/area-chart.json +244 -0
- package/examples/{example-bar-chart.json → feature/bar/example-bar-chart.json} +4 -1
- package/examples/feature/bar/horizontal-chart-max-increase.json +44 -0
- package/examples/{horizontal-chart.json → feature/bar/horizontal-chart.json} +10 -4
- package/examples/{horizontal-stacked-bar-chart.json → feature/bar/horizontal-stacked-bar-chart.json} +7 -3
- package/examples/{planet-chart-horizontal-example-config.json → feature/bar/planet-chart-horizontal-example-config.json} +8 -3
- package/examples/feature/bar/planet-example-config.json +156 -0
- package/examples/{box-plot.json → feature/boxplot/boxplot.json} +7 -8
- package/examples/feature/boxplot/testing.csv +38 -0
- package/examples/feature/combo/combochart-categories_are_numbers .json +18 -0
- package/examples/{planet-combo-example-config.json → feature/combo/planet-combo-example-config.json} +1 -1
- package/examples/feature/deviation/planet-deviation-config.json +168 -0
- package/examples/feature/deviation/planet-deviation-data.json +38 -0
- package/examples/feature/filters/filter-testing.json +178 -0
- package/examples/feature/forecasting/case_date_example.csv +130 -0
- package/examples/feature/forecasting/effective_reproduction.json +202 -0
- package/examples/feature/forecasting/r_data.csv +130 -0
- package/examples/feature/line/line-chart-max-increase.json +32 -0
- package/examples/feature/line/line-chart.json +124 -0
- package/examples/{paired-bar-example.json → feature/paired-bar/paired-bar-example.json} +10 -4
- package/examples/{planet-pie-example-config.json → feature/pie/planet-pie-example-config.json} +2 -2
- package/examples/{scatterplot-continuous.csv → feature/scatterplot/scatterplot-continuous.csv} +3 -3
- package/examples/{scatterplot.json → feature/scatterplot/scatterplot.json} +3 -3
- package/examples/feature/sparkline/example-sparkline.json +76 -0
- package/examples/feature/tests-big-small/big-small-test-bar.json +328 -0
- package/examples/feature/tests-big-small/big-small-test-line.json +328 -0
- package/examples/feature/tests-big-small/big-small-test-negative.json +328 -0
- package/examples/{case-rate-example-config.json → feature/tests-case-rate/case-rate-example-config.json} +2 -2
- package/examples/{covid-confidence-example-config.json → feature/tests-covid/covid-confidence-example-config.json} +8 -3
- package/examples/{covid-example-config.json → feature/tests-covid/covid-example-config.json} +7 -3
- package/examples/{cutoff-example-config.json → feature/tests-cutoff/cutoff-example-config.json} +7 -3
- package/examples/{date-exclusions-config.json → feature/tests-date-exclusions/date-exclusions-config.json} +2 -2
- package/examples/{example-bar-chart-nonnumeric.json → feature/tests-non-numerics/example-bar-chart-nonnumeric.json} +1 -1
- package/examples/{line-chart-nonnumeric.json → feature/tests-non-numerics/line-chart-nonnumeric.json} +5 -5
- package/examples/{planet-pie-example-config-nonnumeric.json → feature/tests-non-numerics/planet-pie-example-config-nonnumeric.json} +2 -2
- package/examples/{sparkline-chart-nonnumeric.json → feature/tests-non-numerics/sparkline-chart-nonnumeric.json} +2 -2
- package/examples/gallery/bar-chart-horizontal/horizontal-bar-chart.json +31 -172
- package/examples/gallery/bar-chart-vertical/combo-line-chart.json +145 -7
- package/examples/gallery/bar-chart-vertical/vertical-bar-chart-confidence.json +1 -0
- package/examples/gallery/bar-chart-vertical/vertical-bar-chart-with-confidence.json +96 -14
- package/examples/gallery/line/line.json +1 -0
- package/examples/gallery/paired-bar/paired-bar-chart.json +1 -0
- package/index.html +76 -35
- package/package.json +6 -3
- package/src/CdcChart.jsx +245 -106
- package/src/components/AreaChart.jsx +233 -0
- package/src/components/BarChart.jsx +103 -62
- package/src/components/BoxPlot.jsx +39 -18
- package/src/components/DataTable.jsx +26 -21
- package/src/components/DeviationBar.jsx +191 -0
- package/src/components/EditorPanel.jsx +662 -298
- package/src/components/Legend.jsx +59 -46
- package/src/components/LineChart.jsx +12 -36
- package/src/components/LinearChart.jsx +163 -64
- package/src/components/PairedBarChart.jsx +6 -7
- package/src/components/PieChart.jsx +12 -17
- package/src/components/ScatterPlot.jsx +19 -16
- package/src/components/SparkLine.jsx +84 -118
- package/src/components/useIntersectionObserver.jsx +1 -1
- package/src/data/initial-state.js +27 -7
- package/src/hooks/useColorPalette.js +58 -48
- package/src/hooks/useReduceData.js +3 -4
- package/src/index.jsx +3 -2
- package/src/scss/editor-panel.scss +20 -0
- package/src/scss/main.scss +8 -6
- package/src/test/CdcChart.test.jsx +6 -0
- package/examples/box-plot.csv +0 -5
- package/examples/dynamic-legends.json +0 -125
- package/examples/line-chart.json +0 -34
- package/examples/planet-example-config.json +0 -37
- package/examples/temp-example-config.json +0 -64
- package/examples/temp-example-data.json +0 -130
- package/src/components/Filters.jsx +0 -125
- /package/examples/{age-adjusted-rates.json → feature/__data__/age-adjusted-rates.json} +0 -0
- /package/examples/{new-data.csv → feature/__data__/new-data.csv} +0 -0
- /package/examples/{Barchart_with_negative.json → feature/bar/Barchart_with_negative.json} +0 -0
- /package/examples/{stacked-vertical-bar-example-negative.json → feature/bar/stacked-vertical-bar-example-negative.json} +0 -0
- /package/examples/{stacked-vertical-bar-example.json → feature/bar/stacked-vertical-bar-example.json} +0 -0
- /package/examples/{box-plot-data.json → feature/boxplot/box-plot-data.json} +0 -0
- /package/examples/{newdata.json → feature/boxplot/boxplot-data.json} +0 -0
- /package/examples/{paired-bar-data.json → feature/paired-bar/paired-bar-data.json} +0 -0
- /package/examples/{paired-bar-formatted.json → feature/paired-bar/paired-bar-formatted.json} +0 -0
- /package/examples/{case-rate-example-data.json → feature/tests-case-rate/case-rate-example-data.json} +0 -0
- /package/examples/{covid-example-data-confidence.json → feature/tests-covid/covid-example-data-confidence.json} +0 -0
- /package/examples/{covid-example-data.json → feature/tests-covid/covid-example-data.json} +0 -0
- /package/examples/{cutoff-example-data.json → feature/tests-cutoff/cutoff-example-data.json} +0 -0
- /package/examples/{date-exclusions-data.json → feature/tests-date-exclusions/date-exclusions-data.json} +0 -0
- /package/examples/{example-combo-bar-nonnumeric.json → feature/tests-non-numerics/example-combo-bar-nonnumeric.json} +0 -0
- /package/examples/{planet-example-data-nonnumeric.json → feature/tests-non-numerics/planet-example-data-nonnumeric.json} +0 -0
- /package/examples/{stacked-vertical-bar-example-nonnumerics.json → feature/tests-non-numerics/stacked-vertical-bar-example-nonnumerics.json} +0 -0
|
@@ -1,17 +1,24 @@
|
|
|
1
|
-
import React, { useContext } from 'react'
|
|
1
|
+
import React, { useContext, useEffect } from 'react'
|
|
2
2
|
import { BoxPlot } from '@visx/stats'
|
|
3
3
|
import { Group } from '@visx/group'
|
|
4
|
-
import { scaleBand, scaleLinear } from '@visx/scale'
|
|
5
4
|
import ConfigContext from '../ConfigContext'
|
|
6
5
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
7
6
|
import { colorPalettesChart } from '@cdc/core/data/colorPalettes'
|
|
8
7
|
|
|
9
8
|
const CoveBoxPlot = ({ xScale, yScale }) => {
|
|
10
|
-
const {
|
|
9
|
+
const { config, setConfig } = useContext(ConfigContext)
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (config.legend.hide === false) {
|
|
13
|
+
setConfig({
|
|
14
|
+
...config,
|
|
15
|
+
legend: {
|
|
16
|
+
...config.legend,
|
|
17
|
+
hide: true
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
}, []) // eslint-disable-line
|
|
15
22
|
|
|
16
23
|
// tooltips
|
|
17
24
|
const tooltip_id = `cdc-open-viz-tooltip-${config.runtime.uniqueId}`
|
|
@@ -24,6 +31,18 @@ const CoveBoxPlot = ({ xScale, yScale }) => {
|
|
|
24
31
|
${config.boxplot.labels.median}: ${d.columnMedian}
|
|
25
32
|
`
|
|
26
33
|
}
|
|
34
|
+
|
|
35
|
+
// accessors & constants
|
|
36
|
+
const max = d => Number(d.columnMax)
|
|
37
|
+
const min = d => Number(d.columnMin)
|
|
38
|
+
const median = d => Number(d.columnMedian)
|
|
39
|
+
const thirdQuartile = d => Number(d.columnThirdQuartile)
|
|
40
|
+
const firstQuartile = d => Number(d.columnFirstQuartile)
|
|
41
|
+
const fillOpacity = 0.5
|
|
42
|
+
const boxWidth = xScale.bandwidth()
|
|
43
|
+
const constrainedWidth = Math.min(40, boxWidth)
|
|
44
|
+
const color_0 = colorPalettesChart[config?.palette][0] ? colorPalettesChart[config?.palette][0] : '#000'
|
|
45
|
+
|
|
27
46
|
return (
|
|
28
47
|
<ErrorBoundary component='BoxPlot'>
|
|
29
48
|
<Group className='boxplot' key={`boxplot-group`}>
|
|
@@ -31,20 +50,23 @@ const CoveBoxPlot = ({ xScale, yScale }) => {
|
|
|
31
50
|
const offset = boxWidth - constrainedWidth
|
|
32
51
|
const radius = 4
|
|
33
52
|
return (
|
|
34
|
-
|
|
35
|
-
{config.boxplot.plotNonOutlierValues &&
|
|
53
|
+
<Group key={`boxplotplot-${i}`}>
|
|
54
|
+
{config.boxplot.plotNonOutlierValues &&
|
|
55
|
+
d.nonOutlierValues.map((value, index) => {
|
|
56
|
+
return <circle cx={xScale(d.columnCategory) + Number(config.yAxis.size) + boxWidth / 2} cy={yScale(value)} r={radius} fill={'#ccc'} style={{ opacity: 1, fillOpacity: 1, stroke: 'black' }} key={`boxplot-${i}--circle-${index}`} />
|
|
57
|
+
})}
|
|
36
58
|
<BoxPlot
|
|
37
59
|
data-left={xScale(d.columnCategory) + config.yAxis.size + offset / 2 + 0.5}
|
|
38
60
|
key={`box-plot-${i}`}
|
|
39
|
-
min={d
|
|
40
|
-
max={d
|
|
41
|
-
left={xScale(d.columnCategory) + config.yAxis.size + offset / 2 + 0.5}
|
|
42
|
-
firstQuartile={d
|
|
43
|
-
thirdQuartile={d
|
|
44
|
-
median={d
|
|
61
|
+
min={min(d)}
|
|
62
|
+
max={max(d)}
|
|
63
|
+
left={Number(xScale(d.columnCategory)) + Number(config.yAxis.size) + offset / 2 + 0.5}
|
|
64
|
+
firstQuartile={firstQuartile(d)}
|
|
65
|
+
thirdQuartile={thirdQuartile(d)}
|
|
66
|
+
median={median(d)}
|
|
45
67
|
boxWidth={constrainedWidth}
|
|
46
68
|
fill={color_0}
|
|
47
|
-
fillOpacity={
|
|
69
|
+
fillOpacity={fillOpacity}
|
|
48
70
|
stroke='black'
|
|
49
71
|
valueScale={yScale}
|
|
50
72
|
outliers={config.boxplot.plotOutlierValues ? d.columnOutliers : []}
|
|
@@ -63,8 +85,7 @@ const CoveBoxPlot = ({ xScale, yScale }) => {
|
|
|
63
85
|
style: {
|
|
64
86
|
stroke: 'black',
|
|
65
87
|
strokeWidth: config.boxplot.borders === 'true' ? 1 : 0
|
|
66
|
-
}
|
|
67
|
-
'data-tooltip-html': 'cool'
|
|
88
|
+
}
|
|
68
89
|
}}
|
|
69
90
|
maxProps={{
|
|
70
91
|
style: {
|
|
@@ -77,7 +98,7 @@ const CoveBoxPlot = ({ xScale, yScale }) => {
|
|
|
77
98
|
'data-tooltip-id': tooltip_id
|
|
78
99
|
}}
|
|
79
100
|
/>
|
|
80
|
-
|
|
101
|
+
</Group>
|
|
81
102
|
)
|
|
82
103
|
})}
|
|
83
104
|
</Group>
|
|
@@ -12,10 +12,7 @@ import ConfigContext from '../ConfigContext'
|
|
|
12
12
|
import CoveMediaControls from '@cdc/core/components/CoveMediaControls'
|
|
13
13
|
|
|
14
14
|
export default function DataTable() {
|
|
15
|
-
const { rawData,
|
|
16
|
-
|
|
17
|
-
// Debugging.
|
|
18
|
-
// if (config.visualizationType === 'Box Plot') return null
|
|
15
|
+
const { rawData, tableData: data, config, colorScale, parseDate, formatDate, formatNumber: numberFormatter, colorPalettes } = useContext(ConfigContext)
|
|
19
16
|
|
|
20
17
|
const section = config.orientation === 'horizontal' ? 'yAxis' : 'xAxis'
|
|
21
18
|
const [tableExpanded, setTableExpanded] = useState(config.table.expanded)
|
|
@@ -38,7 +35,7 @@ export default function DataTable() {
|
|
|
38
35
|
switch (type) {
|
|
39
36
|
case 'download':
|
|
40
37
|
return (
|
|
41
|
-
<a download={fileName} onClick={saveBlob} href={`data:text/csv;base64,${Base64.encode(csvData)}`} aria-label='Download this data in a CSV file format.' className={`btn btn-download no-border`}>
|
|
38
|
+
<a download={fileName} onClick={saveBlob} href={`data:text/csv;base64,${Base64.encode(csvData)}`} aria-label='Download this data in a CSV file format.' className={`btn btn-download no-border margin-sm`}>
|
|
42
39
|
Download Data (CSV)
|
|
43
40
|
</a>
|
|
44
41
|
)
|
|
@@ -57,7 +54,7 @@ export default function DataTable() {
|
|
|
57
54
|
config.visualizationType === 'Pie'
|
|
58
55
|
? []
|
|
59
56
|
: config.visualizationType === 'Box Plot'
|
|
60
|
-
|
|
57
|
+
? [
|
|
61
58
|
{
|
|
62
59
|
Header: 'Measures',
|
|
63
60
|
Cell: props => {
|
|
@@ -76,9 +73,11 @@ export default function DataTable() {
|
|
|
76
73
|
columnThirdQuartile: labels.q3,
|
|
77
74
|
columnOutliers: labels.outliers,
|
|
78
75
|
values: labels.values,
|
|
79
|
-
|
|
76
|
+
columnTotal: labels.total,
|
|
80
77
|
columnSd: 'Standard Deviation',
|
|
81
|
-
nonOutlierValues: 'Non Outliers'
|
|
78
|
+
nonOutlierValues: 'Non Outliers',
|
|
79
|
+
columnLowerBounds: labels.lowerBounds,
|
|
80
|
+
columnUpperBounds: labels.upperBounds
|
|
82
81
|
}
|
|
83
82
|
|
|
84
83
|
let resolvedName = columnLookup[props.row.original[0]]
|
|
@@ -90,7 +89,7 @@ export default function DataTable() {
|
|
|
90
89
|
}
|
|
91
90
|
}
|
|
92
91
|
]
|
|
93
|
-
|
|
92
|
+
: [
|
|
94
93
|
{
|
|
95
94
|
Header: '',
|
|
96
95
|
Cell: ({ row }) => {
|
|
@@ -105,8 +104,8 @@ export default function DataTable() {
|
|
|
105
104
|
? colorScale(seriesLabel)
|
|
106
105
|
: // dynamic legend
|
|
107
106
|
config.legend.dynamicLegend
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
? colorPalettes[config.palette][row.index]
|
|
108
|
+
: // fallback
|
|
110
109
|
'#000'
|
|
111
110
|
}
|
|
112
111
|
/>
|
|
@@ -122,12 +121,13 @@ export default function DataTable() {
|
|
|
122
121
|
data.forEach((d, index) => {
|
|
123
122
|
const resolveTableHeader = () => {
|
|
124
123
|
if (config.runtime[section].type === 'date') return formatDate(parseDate(d[config.runtime.originalXAxis.dataKey]))
|
|
124
|
+
if (config.runtime[section].type === 'continuous') return numberFormatter(d[config.runtime.originalXAxis.dataKey], 'bottom')
|
|
125
125
|
return d[config.runtime.originalXAxis.dataKey]
|
|
126
126
|
}
|
|
127
127
|
const newCol = {
|
|
128
128
|
Header: resolveTableHeader(),
|
|
129
129
|
Cell: ({ row }) => {
|
|
130
|
-
return <>{numberFormatter(d[row.original])}</>
|
|
130
|
+
return <>{numberFormatter(d[row.original], 'left')}</>
|
|
131
131
|
},
|
|
132
132
|
id: `${d[config.runtime.originalXAxis.dataKey]}--${index}`,
|
|
133
133
|
canSort: true
|
|
@@ -149,7 +149,7 @@ export default function DataTable() {
|
|
|
149
149
|
if (Number(props.row.id) === 3) return plot.columnMedian
|
|
150
150
|
if (Number(props.row.id) === 4) return plot.columnFirstQuartile
|
|
151
151
|
if (Number(props.row.id) === 5) return plot.columnMin
|
|
152
|
-
if (Number(props.row.id) === 6) return plot.
|
|
152
|
+
if (Number(props.row.id) === 6) return plot.columnTotal
|
|
153
153
|
if (Number(props.row.id) === 7) return plot.columnSd
|
|
154
154
|
if (Number(props.row.id) === 8) return plot.columnMean
|
|
155
155
|
if (Number(props.row.id) === 9) return plot.columnOutliers.length > 0 ? plot.columnOutliers.toString() : '-'
|
|
@@ -162,12 +162,12 @@ export default function DataTable() {
|
|
|
162
162
|
canSort: false
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
newTableColumns.push(newCol)
|
|
165
|
+
return newTableColumns.push(newCol)
|
|
166
166
|
})
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
return newTableColumns
|
|
170
|
-
}, [config, colorScale])
|
|
170
|
+
}, [config, colorScale]) // eslint-disable-line
|
|
171
171
|
|
|
172
172
|
// prettier-ignore
|
|
173
173
|
const tableData = useMemo(() => (
|
|
@@ -176,7 +176,7 @@ export default function DataTable() {
|
|
|
176
176
|
: config.visualizationType === 'Box Plot'
|
|
177
177
|
? Object.entries(config.boxplot.tableData[0])
|
|
178
178
|
: config.runtime.seriesKeys),
|
|
179
|
-
[config.runtime.seriesKeys])
|
|
179
|
+
[config.runtime.seriesKeys]) // eslint-disable-line
|
|
180
180
|
|
|
181
181
|
// Change accessibility label depending on expanded status
|
|
182
182
|
useEffect(() => {
|
|
@@ -201,8 +201,13 @@ export default function DataTable() {
|
|
|
201
201
|
}),
|
|
202
202
|
[]
|
|
203
203
|
)
|
|
204
|
-
|
|
205
204
|
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable({ columns: tableColumns, data: tableData, defaultColumn }, useSortBy, useBlockLayout, useResizeColumns)
|
|
205
|
+
|
|
206
|
+
// sort continuous x axis scaling for data tables, ie. xAxis should read 1,2,3,4,5
|
|
207
|
+
if (config.xAxis.type === 'continuous' && headerGroups) {
|
|
208
|
+
data.sort((a, b) => a[config.xAxis.dataKey] - b[config.xAxis.dataKey])
|
|
209
|
+
}
|
|
210
|
+
|
|
206
211
|
return (
|
|
207
212
|
<ErrorBoundary component='DataTable'>
|
|
208
213
|
<CoveMediaControls.Section classes={['download-links']}>
|
|
@@ -224,13 +229,12 @@ export default function DataTable() {
|
|
|
224
229
|
}
|
|
225
230
|
}}
|
|
226
231
|
>
|
|
227
|
-
<Icon display={tableExpanded ? 'minus' : 'plus'} base/>
|
|
232
|
+
<Icon display={tableExpanded ? 'minus' : 'plus'} base />
|
|
228
233
|
{config.table.label}
|
|
229
234
|
</div>
|
|
230
235
|
<div className='table-container' hidden={!tableExpanded} style={{ maxHeight: config.table.limitHeight && `${config.table.height}px`, overflowY: 'scroll' }}>
|
|
231
236
|
<table className={tableExpanded ? 'data-table' : 'data-table cdcdataviz-sr-only'} {...getTableProps()} aria-rowcount={config?.series?.length ? config?.series?.length : '-1'}>
|
|
232
|
-
<caption className='cdcdataviz-sr-only'>{config.table.caption ? config.table.caption : ''}</caption>
|
|
233
|
-
<caption className='visually-hidden'>{config.table.label}</caption>
|
|
237
|
+
<caption className='cdcdataviz-sr-only visually-hidden'>{config.table.caption ? config.table.caption : config.table.label ? config.table.label : 'Data Table'}</caption>
|
|
234
238
|
<thead>
|
|
235
239
|
{headerGroups.map((headerGroup, index) => (
|
|
236
240
|
<tr {...headerGroup.getHeaderGroupProps()} key={`headerGroups--${index}`}>
|
|
@@ -272,7 +276,7 @@ export default function DataTable() {
|
|
|
272
276
|
})}
|
|
273
277
|
</tbody>
|
|
274
278
|
</table>
|
|
275
|
-
{config.regions && config.regions.length > 0 ? (
|
|
279
|
+
{config.regions && config.regions.length > 0 && config.visualizationType !== 'Box Plot' ? (
|
|
276
280
|
<table className='region-table data-table'>
|
|
277
281
|
<caption className='visually-hidden'>Table of the highlighted regions in the visualization</caption>
|
|
278
282
|
<thead>
|
|
@@ -284,6 +288,7 @@ export default function DataTable() {
|
|
|
284
288
|
</thead>
|
|
285
289
|
<tbody>
|
|
286
290
|
{config.regions.map((region, index) => {
|
|
291
|
+
if (config.visualizationType === 'Box Plot') return false
|
|
287
292
|
if (!Object.keys(region).includes('from') || !Object.keys(region).includes('to')) return null
|
|
288
293
|
|
|
289
294
|
return (
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { Line } from '@visx/shape'
|
|
2
|
+
import { Group } from '@visx/group'
|
|
3
|
+
import { useContext, useEffect } from 'react'
|
|
4
|
+
import ConfigContext from '../ConfigContext'
|
|
5
|
+
import { Text } from '@visx/text'
|
|
6
|
+
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
7
|
+
import chroma from 'chroma-js'
|
|
8
|
+
|
|
9
|
+
// create function to position text based where bar is located left/or right
|
|
10
|
+
function getTextProps(isLollipopChart, textFits, lollipopShapeSize, fill) {
|
|
11
|
+
if (isLollipopChart) {
|
|
12
|
+
return {
|
|
13
|
+
right: {
|
|
14
|
+
textAnchor: 'start',
|
|
15
|
+
dx: lollipopShapeSize + 6,
|
|
16
|
+
fill: '#000000'
|
|
17
|
+
},
|
|
18
|
+
left: {
|
|
19
|
+
textAnchor: 'end',
|
|
20
|
+
dx: -lollipopShapeSize,
|
|
21
|
+
fill: '#000000'
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
return {
|
|
26
|
+
right: {
|
|
27
|
+
textAnchor: textFits ? 'end' : 'start',
|
|
28
|
+
dx: textFits ? -6 : 6,
|
|
29
|
+
fill: textFits ? fill : '#000000'
|
|
30
|
+
},
|
|
31
|
+
left: {
|
|
32
|
+
textAnchor: textFits ? 'start' : 'end',
|
|
33
|
+
dx: textFits ? 6 : -6,
|
|
34
|
+
fill: textFits ? fill : '#000000'
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function DeviationBar({ height, xScale }) {
|
|
41
|
+
const { transformedData: data, config, formatNumber, twoColorPalette, getTextWidth, updateConfig, parseDate, formatDate } = useContext(ConfigContext)
|
|
42
|
+
|
|
43
|
+
if (!config || config?.series?.length !== 1 || config.orientation !== 'horizontal') return
|
|
44
|
+
|
|
45
|
+
const { barStyle, tipRounding, roundingStyle, twoColor } = config
|
|
46
|
+
|
|
47
|
+
const radius = roundingStyle === 'standard' ? '8px' : roundingStyle === 'shallow' ? '5px' : roundingStyle === 'finger' ? '15px' : '0px'
|
|
48
|
+
const fontSize = { small: 16, medium: 18, large: 20 }
|
|
49
|
+
const isRounded = config.barStyle === 'rounded'
|
|
50
|
+
const target = Number(config.xAxis.target)
|
|
51
|
+
const seriesKey = config.series[0].dataKey
|
|
52
|
+
const maxVal = Number(xScale.domain()[1])
|
|
53
|
+
const hasNegativeValues = data.some(d => d[seriesKey] < 0)
|
|
54
|
+
const shouldShowTargetLine = hasNegativeValues || target > 0 || xScale.domain()[0] < 0
|
|
55
|
+
const borderWidth = config.barHasBorder === 'true' ? 1 : 0
|
|
56
|
+
const lollipopBarHeight = config.lollipopSize === 'large' ? 7 : config.lollipopSize === 'medium' ? 6 : 5
|
|
57
|
+
const lollipopShapeSize = config.lollipopSize === 'large' ? 14 : config.lollipopSize === 'medium' ? 12 : 10
|
|
58
|
+
|
|
59
|
+
const targetX = Math.max(xScale(0), Math.min(xScale(target), xScale(maxVal)))
|
|
60
|
+
|
|
61
|
+
const applyRadius = barPosition => {
|
|
62
|
+
if (barPosition === undefined || barPosition === null || barStyle !== 'rounded') return
|
|
63
|
+
let style = {}
|
|
64
|
+
if (barPosition === 'left') {
|
|
65
|
+
style = { borderRadius: `${radius} 0 0 ${radius}` }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (barPosition === 'right') {
|
|
69
|
+
style = { borderRadius: `0 ${radius} ${radius} 0` }
|
|
70
|
+
}
|
|
71
|
+
if (tipRounding === 'full') {
|
|
72
|
+
style = { borderRadius: radius }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return style
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const targetLabel = {
|
|
79
|
+
calculate: function () {
|
|
80
|
+
const firstBarValue = data[0][seriesKey]
|
|
81
|
+
const barPosition = firstBarValue < target ? 'left' : 'right'
|
|
82
|
+
const label = `${config.xAxis.targetLabel} ${formatNumber(config.xAxis.target || 0, 'left')}`
|
|
83
|
+
const labelWidth = getTextWidth(label, `bold ${fontSize[config.fontSize]}px sans-serif`)
|
|
84
|
+
let labelY = config.isLollipopChart ? lollipopBarHeight / 2 : Number(config.barHeight) / 2
|
|
85
|
+
let paddingX = 0
|
|
86
|
+
let labelX = 0
|
|
87
|
+
let showLabel = false
|
|
88
|
+
|
|
89
|
+
if (barPosition === 'right') {
|
|
90
|
+
paddingX = -10
|
|
91
|
+
showLabel = labelWidth - paddingX < targetX
|
|
92
|
+
labelX = targetX - labelWidth
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (barPosition === 'left') {
|
|
96
|
+
paddingX = 10
|
|
97
|
+
showLabel = xScale(maxVal) - targetX > labelWidth + paddingX
|
|
98
|
+
labelX = targetX
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this.text = label
|
|
102
|
+
this.y = labelY
|
|
103
|
+
this.x = labelX
|
|
104
|
+
this.padding = paddingX
|
|
105
|
+
this.showLabel = config.xAxis.showTargetLabel ? showLabel : false
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
targetLabel.calculate()
|
|
109
|
+
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (config.barStyle === 'lollipop' && !config.isLollipopChart) {
|
|
112
|
+
updateConfig({ ...config, isLollipopChart: true })
|
|
113
|
+
}
|
|
114
|
+
if (isRounded || config.barStyle === 'flat') {
|
|
115
|
+
updateConfig({ ...config, isLollipopChart: false })
|
|
116
|
+
}
|
|
117
|
+
}, [config.barStyle])
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<ErrorBoundary component='Deviation Bar'>
|
|
121
|
+
<Group left={Number(config.xAxis.size)}>
|
|
122
|
+
{data.map((d, index) => {
|
|
123
|
+
const barValue = Number(d[seriesKey])
|
|
124
|
+
const barHeight = config.isLollipopChart ? lollipopBarHeight : Number(config.barHeight)
|
|
125
|
+
const barSpace = Number(config.barSpace)
|
|
126
|
+
const barWidth = Math.abs(xScale(barValue) - targetX)
|
|
127
|
+
const barBaseX = xScale(barValue)
|
|
128
|
+
const barX = barValue > target ? targetX : barBaseX
|
|
129
|
+
const barPosition = barValue < target ? 'left' : 'right'
|
|
130
|
+
|
|
131
|
+
// update bar Y to give dynamic Y when user applyes BarSpace
|
|
132
|
+
let barY = 0
|
|
133
|
+
barY = index !== 0 ? (barSpace + barHeight + borderWidth) * index : barY
|
|
134
|
+
const totalheight = (barSpace + barHeight + borderWidth) * data.length
|
|
135
|
+
config.heights.horizontal = totalheight
|
|
136
|
+
|
|
137
|
+
// text,labels postiions
|
|
138
|
+
const textWidth = getTextWidth(formatNumber(barValue, 'left'), `normal ${fontSize[config.fontSize]}px sans-serif`)
|
|
139
|
+
const textFits = textWidth < barWidth - 6
|
|
140
|
+
const textX = barBaseX
|
|
141
|
+
const textY = barY + barHeight / 2
|
|
142
|
+
|
|
143
|
+
// lollipop shapes
|
|
144
|
+
const circleX = barBaseX
|
|
145
|
+
const circleY = barY + barHeight / 2
|
|
146
|
+
const squareX = barBaseX
|
|
147
|
+
const squareY = barY - barHeight / 2
|
|
148
|
+
const borderRadius = applyRadius(barPosition)
|
|
149
|
+
// colors
|
|
150
|
+
const [leftColor, rightColor] = twoColorPalette[twoColor.palette]
|
|
151
|
+
const barColor = { left: leftColor, right: rightColor }
|
|
152
|
+
const isBarColorDark = chroma.contrast('#000000', barColor[barPosition]) < 4.9
|
|
153
|
+
const fill = isBarColorDark ? '#FFFFFF' : '#000000'
|
|
154
|
+
|
|
155
|
+
let textProps = getTextProps(config.isLollipopChart, textFits, lollipopShapeSize, fill)
|
|
156
|
+
|
|
157
|
+
// tooltips
|
|
158
|
+
const xAxisValue = formatNumber(barValue, 'left')
|
|
159
|
+
const yAxisValue = config.runtime.yAxis.type === 'date' ? formatDate(parseDate(data[index][config.runtime.originalXAxis.dataKey])) : data[index][config.runtime.originalXAxis.dataKey]
|
|
160
|
+
let yAxisTooltip = config.runtime.yAxis.label ? `${config.runtime.yAxis.label}: ${yAxisValue}` : yAxisValue
|
|
161
|
+
let xAxisTooltip = config.runtime.xAxis.label ? `${config.runtime.xAxis.label}: ${xAxisValue}` : xAxisValue
|
|
162
|
+
const tooltip = `<div>
|
|
163
|
+
${yAxisTooltip}<br />
|
|
164
|
+
${xAxisTooltip}
|
|
165
|
+
</div>`
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<Group key={`deviation-bar-${config.orientation}-${seriesKey}-${index}`}>
|
|
169
|
+
<foreignObject x={barX} y={barY} width={barWidth} height={barHeight} style={{ border: `${borderWidth}px solid #333`, backgroundColor: barColor[barPosition], ...borderRadius }} data-tooltip-html={tooltip} data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`} />
|
|
170
|
+
{config.yAxis.displayNumbersOnBar && (
|
|
171
|
+
<Text verticalAnchor='middle' x={textX} y={textY} {...textProps[barPosition]}>
|
|
172
|
+
{formatNumber(d[seriesKey], 'left')}
|
|
173
|
+
</Text>
|
|
174
|
+
)}
|
|
175
|
+
|
|
176
|
+
{config.isLollipopChart && config.lollipopShape === 'circle' && <circle cx={circleX} cy={circleY} r={lollipopShapeSize / 2} fill={barColor[barPosition]} style={{ filter: 'unset', opacity: 1 }} />}
|
|
177
|
+
{config.isLollipopChart && config.lollipopShape === 'square' && <rect x={squareX} y={squareY} width={lollipopShapeSize} height={lollipopShapeSize} fill={barColor[barPosition]} style={{ opacity: 1, filter: 'unset' }}></rect>}
|
|
178
|
+
</Group>
|
|
179
|
+
)
|
|
180
|
+
})}
|
|
181
|
+
{targetLabel.showLabel && (
|
|
182
|
+
<Text fontWeight='bold' dx={targetLabel.padding} verticalAnchor='middle' x={targetLabel.x} y={targetLabel.y}>
|
|
183
|
+
{targetLabel.text}
|
|
184
|
+
</Text>
|
|
185
|
+
)}
|
|
186
|
+
|
|
187
|
+
{shouldShowTargetLine && <Line from={{ x: targetX, y: 0 }} to={{ x: targetX, y: height }} stroke='#333' strokeWidth={2} />}
|
|
188
|
+
</Group>
|
|
189
|
+
</ErrorBoundary>
|
|
190
|
+
)
|
|
191
|
+
}
|