@cdc/chart 4.24.9 → 4.24.11
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/LICENSE +201 -0
- package/dist/cdcchart.js +45911 -41739
- package/examples/feature/boxplot/boxplot-data.json +88 -22
- package/examples/feature/boxplot/boxplot.json +540 -16
- package/examples/feature/boxplot/testing.csv +7 -7
- package/examples/feature/sankey/sankey-example-data.json +0 -1
- package/examples/private/test.json +20092 -0
- package/index.html +4 -4
- package/package.json +2 -2
- package/src/CdcChart.tsx +209 -188
- package/src/_stories/Chart.CustomColors.stories.tsx +19 -0
- package/src/_stories/Chart.DynamicSeries.stories.tsx +27 -0
- package/src/_stories/Chart.Legend.Gradient.stories.tsx +74 -0
- package/src/_stories/Chart.stories.tsx +30 -3
- package/src/_stories/ChartAxisLabels.stories.tsx +20 -0
- package/src/_stories/ChartAxisTitles.stories.tsx +53 -0
- package/src/_stories/ChartEditor.stories.tsx +27 -0
- package/src/_stories/ChartLine.Suppression.stories.tsx +25 -0
- package/src/_stories/ChartPrefixSuffix.stories.tsx +159 -0
- package/src/_stories/_mock/boxplot_multiseries.json +647 -0
- package/src/_stories/_mock/dynamic_series_bar_config.json +723 -0
- package/src/_stories/_mock/dynamic_series_config.json +979 -0
- package/src/_stories/_mock/horizontal_bar.json +257 -0
- package/src/_stories/_mock/large_x_axis_labels.json +261 -0
- package/src/_stories/_mock/paired-bar.json +262 -0
- package/src/_stories/_mock/pie_with_data.json +255 -0
- package/{examples/feature/scatterplot/scatterplot.json → src/_stories/_mock/scatterplot_mock.json} +62 -92
- package/src/_stories/_mock/simplified_line.json +1510 -0
- package/src/_stories/_mock/suppression_mock.json +1549 -0
- package/src/components/Annotations/components/AnnotationDraggable.tsx +0 -3
- package/src/components/Annotations/components/AnnotationDropdown.tsx +1 -1
- package/src/components/Axis/Categorical.Axis.tsx +22 -4
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +95 -16
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +41 -17
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +43 -9
- package/src/components/BarChart/components/BarChart.Vertical.tsx +123 -47
- package/src/components/BarChart/helpers/index.ts +23 -5
- package/src/components/BoxPlot/BoxPlot.tsx +189 -0
- package/src/components/BrushChart.tsx +3 -2
- package/src/components/DeviationBar.jsx +58 -8
- package/src/components/EditorPanel/EditorPanel.tsx +127 -102
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +11 -28
- package/src/components/EditorPanel/components/Panels/Panel.BoxPlot.tsx +51 -6
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +21 -4
- package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +40 -9
- package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +3 -3
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +121 -56
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +296 -35
- package/src/components/EditorPanel/components/panels.scss +4 -6
- package/src/components/EditorPanel/editor-panel.scss +0 -8
- package/src/components/EditorPanel/helpers/tests/updateFieldRankByValue.test.ts +38 -0
- package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +42 -0
- package/src/components/EditorPanel/useEditorPermissions.ts +16 -1
- package/src/components/ForestPlot/ForestPlot.tsx +2 -3
- package/src/components/ForestPlot/ForestPlotProps.ts +2 -0
- package/src/components/Legend/Legend.Component.tsx +23 -24
- package/src/components/Legend/Legend.Suppression.tsx +25 -20
- package/src/components/Legend/Legend.tsx +16 -18
- package/src/components/Legend/helpers/index.ts +16 -19
- package/src/components/LegendWrapper.tsx +3 -1
- package/src/components/LineChart/components/LineChart.Circle.tsx +10 -0
- package/src/components/LineChart/helpers.ts +48 -43
- package/src/components/LineChart/index.tsx +88 -82
- package/src/components/LinearChart.tsx +747 -562
- package/src/components/PairedBarChart.jsx +50 -10
- package/src/components/PieChart/PieChart.tsx +1 -6
- package/src/components/Regions/components/Regions.tsx +33 -19
- package/src/components/Sankey/index.tsx +50 -32
- package/src/components/Sankey/sankey.scss +6 -5
- package/src/components/Sankey/useSankeyAlert.tsx +60 -0
- package/src/components/ScatterPlot/ScatterPlot.jsx +20 -4
- package/src/components/ZoomBrush.tsx +25 -6
- package/src/coreStyles_chart.scss +3 -0
- package/src/data/initial-state.js +8 -10
- package/src/helpers/configHelpers.ts +28 -0
- package/src/helpers/handleRankByValue.ts +15 -0
- package/src/helpers/sizeHelpers.ts +25 -0
- package/src/helpers/tests/handleRankByValue.test.ts +37 -0
- package/src/helpers/tests/sizeHelpers.test.ts +80 -0
- package/src/hooks/useColorPalette.js +10 -2
- package/src/hooks/useLegendClasses.ts +13 -22
- package/src/hooks/useMinMax.ts +27 -13
- package/src/hooks/useReduceData.ts +43 -10
- package/src/hooks/useScales.ts +87 -38
- package/src/hooks/useTooltip.tsx +62 -53
- package/src/index.jsx +1 -0
- package/src/scss/DataTable.scss +5 -4
- package/src/scss/main.scss +57 -70
- package/src/types/ChartConfig.ts +43 -34
- package/src/types/ChartContext.ts +22 -15
- package/src/types/ForestPlot.ts +8 -0
- package/src/_stories/Chart.Legend.Gradient.tsx +0 -19
- package/src/_stories/ChartBrush.stories.tsx +0 -19
- package/src/components/BoxPlot/BoxPlot.jsx +0 -111
- package/src/components/LinearChart.jsx +0 -817
|
@@ -15,11 +15,13 @@ import { BarGroup } from '@visx/shape'
|
|
|
15
15
|
import Regions from '../../Regions'
|
|
16
16
|
// CDC core components and helpers
|
|
17
17
|
import { isDateScale } from '@cdc/core/helpers/cove/date'
|
|
18
|
+
import isNumber from '@cdc/core/helpers/isNumber'
|
|
18
19
|
import createBarElement from '@cdc/core/components/createBarElement'
|
|
19
20
|
// Third party libraries
|
|
20
21
|
import chroma from 'chroma-js'
|
|
21
22
|
// Types
|
|
22
23
|
import { type ChartContext } from '../../../types/ChartContext'
|
|
24
|
+
import _, { has } from 'lodash'
|
|
23
25
|
|
|
24
26
|
export const BarChartVertical = () => {
|
|
25
27
|
const { xScale, yScale, xMax, yMax, seriesScale } = useContext<BarChartContextValues>(BarChartContext)
|
|
@@ -42,7 +44,7 @@ export const BarChartVertical = () => {
|
|
|
42
44
|
} = useBarChart()
|
|
43
45
|
|
|
44
46
|
// prettier-ignore
|
|
45
|
-
const { colorScale, config, dashboardConfig, tableData, formatDate, formatNumber, getXAxisData, getYAxisData,
|
|
47
|
+
const { colorScale, config, dashboardConfig, tableData, formatDate, formatNumber, getXAxisData, getYAxisData, parseDate, seriesHighlight, setSharedFilter, transformedData, brushConfig } = useContext<ChartContext>(ConfigContext)
|
|
46
48
|
const { HighLightedBarUtils } = useHighlightedBars(config)
|
|
47
49
|
let data = transformedData
|
|
48
50
|
// check if user add suppression
|
|
@@ -56,13 +58,43 @@ export const BarChartVertical = () => {
|
|
|
56
58
|
data = brushConfig.data
|
|
57
59
|
}
|
|
58
60
|
|
|
61
|
+
const hasConfidenceInterval = Object.keys(config.confidenceKeys).length > 0
|
|
62
|
+
|
|
63
|
+
const getData = () => {
|
|
64
|
+
const dynamicSeries = config.series.find(s => s.dynamicCategory)
|
|
65
|
+
if (!dynamicSeries) return data
|
|
66
|
+
const { dynamicCategory, dataKey } = dynamicSeries
|
|
67
|
+
const xAxisKey = config.runtime.originalXAxis.dataKey
|
|
68
|
+
const xAxisGroupDataLookup = _.groupBy(data, xAxisKey)
|
|
69
|
+
return Object.values(xAxisGroupDataLookup).map(group => {
|
|
70
|
+
return group.reduce((acc, datum) => {
|
|
71
|
+
const dataValue = datum[dataKey]
|
|
72
|
+
const dataCategory = datum[dynamicCategory]
|
|
73
|
+
if (hasConfidenceInterval) {
|
|
74
|
+
const { lower, upper } = config.confidenceKeys
|
|
75
|
+
if (!acc.CI) acc.CI = {}
|
|
76
|
+
const lowerValue = datum[lower]
|
|
77
|
+
const upperValue = datum[upper]
|
|
78
|
+
acc.CI[dataCategory] = { lower: lowerValue, upper: upperValue }
|
|
79
|
+
}
|
|
80
|
+
acc[dataCategory] = dataValue
|
|
81
|
+
acc[xAxisKey] = datum[xAxisKey]
|
|
82
|
+
acc.dynamicData = true
|
|
83
|
+
return acc
|
|
84
|
+
}, {})
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const _data = getData()
|
|
59
89
|
return (
|
|
60
90
|
config.visualizationSubType !== 'stacked' &&
|
|
61
|
-
(config.visualizationType === 'Bar' ||
|
|
91
|
+
(config.visualizationType === 'Bar' ||
|
|
92
|
+
config.visualizationType === 'Combo' ||
|
|
93
|
+
isConvertLineToBarGraph(config.visualizationType, data, config.allowLineToBarGraph)) &&
|
|
62
94
|
config.orientation === 'vertical' && (
|
|
63
95
|
<Group>
|
|
64
96
|
<BarGroup
|
|
65
|
-
data={
|
|
97
|
+
data={_data}
|
|
66
98
|
keys={config.runtime.barSeriesKeys || config.runtime.seriesKeys}
|
|
67
99
|
height={yMax}
|
|
68
100
|
x0={d => {
|
|
@@ -85,28 +117,46 @@ export const BarChartVertical = () => {
|
|
|
85
117
|
left={barGroup.x0}
|
|
86
118
|
>
|
|
87
119
|
{barGroup.bars.map((bar, index) => {
|
|
120
|
+
const datum = _data[barGroup.index]
|
|
121
|
+
const dataValue = datum[config.runtime.originalXAxis.dataKey]
|
|
88
122
|
const scaleVal = config.yAxis.type === 'logarithmic' ? 0.1 : 0
|
|
89
|
-
let highlightedBarValues = config.highlightedBarValues
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
123
|
+
let highlightedBarValues = config.highlightedBarValues
|
|
124
|
+
.map(item => item.value)
|
|
125
|
+
.filter(item => item !== ('' || undefined))
|
|
126
|
+
highlightedBarValues =
|
|
127
|
+
config.xAxis.type === 'date'
|
|
128
|
+
? HighLightedBarUtils.formatDates(highlightedBarValues)
|
|
129
|
+
: highlightedBarValues
|
|
130
|
+
const transparentBar =
|
|
131
|
+
config.legend.behavior === 'highlight' &&
|
|
132
|
+
seriesHighlight.length > 0 &&
|
|
133
|
+
seriesHighlight.indexOf(bar.key) === -1
|
|
134
|
+
const displayBar =
|
|
135
|
+
config.legend.behavior === 'highlight' ||
|
|
136
|
+
seriesHighlight.length === 0 ||
|
|
137
|
+
seriesHighlight.indexOf(bar.key) !== -1
|
|
93
138
|
|
|
94
139
|
let barGroupWidth = seriesScale.range()[1] - seriesScale.range()[0]
|
|
95
140
|
const defaultBarHeight = Math.abs(yScale(bar.value) - yScale(scaleVal))
|
|
96
141
|
const defaultBarY = bar.value >= 0 && isNumber(bar.value) ? bar.y : yScale(0)
|
|
97
142
|
let barWidth = config.isLollipopChart ? lollipopBarWidth : seriesScale.bandwidth()
|
|
98
|
-
let barX =
|
|
143
|
+
let barX =
|
|
144
|
+
bar.x +
|
|
145
|
+
(config.isLollipopChart ? (barGroupWidth / barGroup.bars.length - lollipopBarWidth) / 2 : 0) -
|
|
146
|
+
(config.xAxis.type === 'date-time' ? barGroupWidth / 2 : 0)
|
|
99
147
|
setBarWidth(barWidth)
|
|
100
148
|
setTotalBarsInGroup(barGroup.bars.length)
|
|
101
149
|
const yAxisValue = formatNumber(/[a-zA-Z]/.test(String(bar.value)) ? '' : bar.value, 'left')
|
|
102
150
|
const xAxisValue =
|
|
103
|
-
config.runtime[section].type === 'date' ? formatDate(parseDate(
|
|
151
|
+
config.runtime[section].type === 'date' ? formatDate(parseDate(dataValue)) : dataValue
|
|
104
152
|
|
|
105
153
|
// create new Index for bars with negative values
|
|
106
154
|
const newIndex = bar.value < 0 ? -1 : index
|
|
107
155
|
// tooltips
|
|
108
|
-
const additionalColTooltip = getAdditionalColumn(bar.key,
|
|
109
|
-
let xAxisTooltip = config.runtime.xAxis.label
|
|
156
|
+
const additionalColTooltip = getAdditionalColumn(bar.key, dataValue)
|
|
157
|
+
let xAxisTooltip = config.runtime.xAxis.label
|
|
158
|
+
? `${config.runtime.xAxis.label}: ${xAxisValue}`
|
|
159
|
+
: xAxisValue
|
|
110
160
|
const tooltipBody = `${config.runtime.seriesLabels[bar.key]}: ${yAxisValue}`
|
|
111
161
|
|
|
112
162
|
const tooltip = `<ul>
|
|
@@ -118,16 +168,31 @@ export const BarChartVertical = () => {
|
|
|
118
168
|
// configure colors
|
|
119
169
|
let labelColor = '#000000'
|
|
120
170
|
labelColor = HighLightedBarUtils.checkFontColor(yAxisValue, highlightedBarValues, labelColor) // Set if background is transparent'
|
|
121
|
-
let barColor = config.runtime.seriesLabels && config.runtime.seriesLabels[bar.key] ? colorScale(config.runtime.seriesLabels[bar.key]) : colorScale(bar.key)
|
|
122
171
|
const isRegularLollipopColor = config.isLollipopChart && config.lollipopColorStyle === 'regular'
|
|
123
172
|
const isTwoToneLollipopColor = config.isLollipopChart && config.lollipopColorStyle === 'two-tone'
|
|
124
173
|
const isHighlightedBar = highlightedBarValues?.includes(xAxisValue)
|
|
125
174
|
const highlightedBarColor = getHighlightedBarColorByValue(xAxisValue)
|
|
126
175
|
const highlightedBar = getHighlightedBarByValue(xAxisValue)
|
|
127
|
-
const borderColor = isHighlightedBar
|
|
128
|
-
|
|
176
|
+
const borderColor = isHighlightedBar
|
|
177
|
+
? highlightedBarColor
|
|
178
|
+
: config.barHasBorder === 'true'
|
|
179
|
+
? '#000'
|
|
180
|
+
: 'transparent'
|
|
181
|
+
const borderWidth = isHighlightedBar
|
|
182
|
+
? highlightedBar.borderWidth
|
|
183
|
+
: config.isLollipopChart
|
|
184
|
+
? 0
|
|
185
|
+
: config.barHasBorder === 'true'
|
|
186
|
+
? barBorderWidth
|
|
187
|
+
: 0
|
|
129
188
|
|
|
130
|
-
const { barHeight, isSuppressed, getBarY, getAbsentDataLabel } = getBarConfig({
|
|
189
|
+
const { barHeight, isSuppressed, getBarY, getAbsentDataLabel } = getBarConfig({
|
|
190
|
+
bar,
|
|
191
|
+
defaultBarHeight,
|
|
192
|
+
config,
|
|
193
|
+
barWidth,
|
|
194
|
+
isVertical: true
|
|
195
|
+
})
|
|
131
196
|
|
|
132
197
|
const absentDataLabel = getAbsentDataLabel(yAxisValue)
|
|
133
198
|
const barDefaultLabel = isSuppressed || !config.labels ? '' : yAxisValue
|
|
@@ -149,9 +214,11 @@ export const BarChartVertical = () => {
|
|
|
149
214
|
? sharedFilters.map(_sharedFilter => {
|
|
150
215
|
if (_sharedFilter.setBy === config.uid) {
|
|
151
216
|
// If the current filter is the reset filter item.
|
|
152
|
-
if (_sharedFilter.resetLabel === _sharedFilter.active)
|
|
217
|
+
if (_sharedFilter.resetLabel === _sharedFilter.active)
|
|
218
|
+
return colorScale(config.runtime.seriesLabels[bar.key])
|
|
153
219
|
// If the current filter is the bars
|
|
154
|
-
if (_sharedFilter.active === transformedData[barGroup.index][config.xAxis.dataKey])
|
|
220
|
+
if (_sharedFilter.active === transformedData[barGroup.index][config.xAxis.dataKey])
|
|
221
|
+
return colorScale(config.runtime.seriesLabels[bar.key])
|
|
155
222
|
return _filteredOutColor
|
|
156
223
|
} else {
|
|
157
224
|
// If the setBy isn't the config.uid return the original barColor
|
|
@@ -163,20 +230,32 @@ export const BarChartVertical = () => {
|
|
|
163
230
|
if (isRegularLollipopColor) _barColor = barColor
|
|
164
231
|
|
|
165
232
|
if (isHighlightedBar) _barColor = 'transparent'
|
|
166
|
-
if (config.legend.colorCode)
|
|
233
|
+
if (config.legend.colorCode)
|
|
234
|
+
_barColor = assignColorsToValues(barGroups.length, barGroup.index, barColor)
|
|
167
235
|
if (isTwoToneLollipopColor) _barColor = chroma(barColor).brighten(1)
|
|
168
236
|
return _barColor
|
|
169
237
|
}
|
|
170
238
|
|
|
171
239
|
// if this is a two tone lollipop slightly lighten the bar.
|
|
172
240
|
if (isTwoToneLollipopColor) _barColor = chroma(barColor).brighten(1)
|
|
173
|
-
if (config.legend.colorCode)
|
|
241
|
+
if (config.legend.colorCode)
|
|
242
|
+
_barColor = assignColorsToValues(barGroups.length, barGroup.index, barColor)
|
|
174
243
|
|
|
175
244
|
// if we're highlighting a bar make it invisible since it gets a border
|
|
176
245
|
if (isHighlightedBar) _barColor = 'transparent'
|
|
177
246
|
return _barColor
|
|
178
247
|
}
|
|
179
248
|
|
|
249
|
+
// Confidence Interval Variables
|
|
250
|
+
const tickWidth = 5
|
|
251
|
+
const xPos = barX + (config.xAxis.type !== 'date-time' ? barWidth / 2 : 0)
|
|
252
|
+
const [upperPos, lowerPos] = ['upper', 'lower'].map(position => {
|
|
253
|
+
if (!hasConfidenceInterval) return
|
|
254
|
+
const d = datum.dynamicData ? datum.CI[bar.key][position] : datum[config.confidenceKeys[position]]
|
|
255
|
+
return yScale(d)
|
|
256
|
+
})
|
|
257
|
+
// End Confidence Interval Variables
|
|
258
|
+
|
|
180
259
|
return (
|
|
181
260
|
<Group key={`${barGroup.index}--${index}`}>
|
|
182
261
|
<Group key={`bar-sub-group-${barGroup.index}-${barGroup.x0}-${barY}--${index}`}>
|
|
@@ -217,13 +296,23 @@ export const BarChartVertical = () => {
|
|
|
217
296
|
const isValueMatch = String(pd.value) === String(bar.value) && pd.value !== ''
|
|
218
297
|
const isSuppressed = isValueMatch && selectedSuppressionColumn
|
|
219
298
|
|
|
220
|
-
if (
|
|
299
|
+
if (
|
|
300
|
+
!isSuppressed ||
|
|
301
|
+
barWidth < 10 ||
|
|
302
|
+
!config.general.showSuppressedSymbol ||
|
|
303
|
+
pd.hideBarSymbol
|
|
304
|
+
) {
|
|
221
305
|
return
|
|
222
306
|
}
|
|
223
307
|
const hasAsterisk = String(pd.symbol).includes('Asterisk')
|
|
224
308
|
const yPadding = hasAsterisk ? -5 : -8
|
|
225
309
|
const verticalAnchor = hasAsterisk ? 'middle' : 'end'
|
|
226
|
-
const iconSize =
|
|
310
|
+
const iconSize =
|
|
311
|
+
pd.symbol === 'Asterisk'
|
|
312
|
+
? barWidth * 1.2
|
|
313
|
+
: pd.symbol === 'Double Asterisk'
|
|
314
|
+
? barWidth
|
|
315
|
+
: barWidth / 1.5
|
|
227
316
|
const fillColor = pd.displayGray ? '#8b8b8a' : '#000'
|
|
228
317
|
|
|
229
318
|
return (
|
|
@@ -295,6 +384,19 @@ export const BarChartVertical = () => {
|
|
|
295
384
|
<animate attributeName='height' values={`0, ${lollipopShapeSize}`} dur='2.5s' />
|
|
296
385
|
</rect>
|
|
297
386
|
)}
|
|
387
|
+
{hasConfidenceInterval && (
|
|
388
|
+
<path
|
|
389
|
+
key={`confidence-interval-v-${datum[config.runtime.originalXAxis.dataKey]}`}
|
|
390
|
+
stroke='#333'
|
|
391
|
+
strokeWidth='px'
|
|
392
|
+
d={`M${xPos - tickWidth} ${upperPos}
|
|
393
|
+
L${xPos + tickWidth} ${upperPos}
|
|
394
|
+
M${xPos} ${upperPos}
|
|
395
|
+
L${xPos} ${lowerPos}
|
|
396
|
+
M${xPos - tickWidth} ${lowerPos}
|
|
397
|
+
L${xPos + tickWidth} ${lowerPos}`}
|
|
398
|
+
/>
|
|
399
|
+
)}
|
|
298
400
|
</Group>
|
|
299
401
|
</Group>
|
|
300
402
|
)
|
|
@@ -304,32 +406,6 @@ export const BarChartVertical = () => {
|
|
|
304
406
|
}}
|
|
305
407
|
</BarGroup>
|
|
306
408
|
|
|
307
|
-
{Object.keys(config.confidenceKeys).length > 0
|
|
308
|
-
? data.map(d => {
|
|
309
|
-
let xPos, yPos
|
|
310
|
-
let upperPos
|
|
311
|
-
let lowerPos
|
|
312
|
-
let tickWidth = 5
|
|
313
|
-
xPos = xScale(getXAxisData(d)) + (config.xAxis.type !== 'date-time' ? seriesScale.range()[1] / 2 : 0)
|
|
314
|
-
upperPos = yScale(getYAxisData(d, config.confidenceKeys.lower))
|
|
315
|
-
lowerPos = yScale(getYAxisData(d, config.confidenceKeys.upper))
|
|
316
|
-
return (
|
|
317
|
-
<path
|
|
318
|
-
key={`confidence-interval-v-${yPos}-${d[config.runtime.originalXAxis.dataKey]}`}
|
|
319
|
-
stroke='#333'
|
|
320
|
-
strokeWidth='px'
|
|
321
|
-
d={`
|
|
322
|
-
M${xPos - tickWidth} ${upperPos}
|
|
323
|
-
L${xPos + tickWidth} ${upperPos}
|
|
324
|
-
M${xPos} ${upperPos}
|
|
325
|
-
L${xPos} ${lowerPos}
|
|
326
|
-
M${xPos - tickWidth} ${lowerPos}
|
|
327
|
-
L${xPos + tickWidth} ${lowerPos}`}
|
|
328
|
-
/>
|
|
329
|
-
)
|
|
330
|
-
})
|
|
331
|
-
: ''}
|
|
332
|
-
|
|
333
409
|
<Regions xScale={xScale} yMax={yMax} barWidth={barWidth} totalBarsInGroup={totalBarsInGroup} />
|
|
334
410
|
</Group>
|
|
335
411
|
)
|
|
@@ -1,17 +1,25 @@
|
|
|
1
|
+
import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
|
|
2
|
+
import isNumber from '@cdc/core/helpers/isNumber'
|
|
3
|
+
|
|
1
4
|
// Define an interface for the function's parameter
|
|
2
5
|
interface BarConfigProps {
|
|
3
6
|
defaultBarWidth?: number
|
|
4
7
|
defaultBarHeight?: number
|
|
5
8
|
bar?: { [key: string]: any }
|
|
6
|
-
isNumber?: Function
|
|
7
9
|
config: { [key: string]: any }
|
|
8
|
-
getTextWidth: (a: string, b: string) => string
|
|
9
10
|
barWidth: number
|
|
10
11
|
isVertical: boolean
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
// Function to create bar width based on suppression status and missing data label
|
|
14
|
-
export const getBarConfig = ({
|
|
15
|
+
export const getBarConfig = ({
|
|
16
|
+
bar,
|
|
17
|
+
defaultBarHeight,
|
|
18
|
+
defaultBarWidth,
|
|
19
|
+
config,
|
|
20
|
+
barWidth,
|
|
21
|
+
isVertical
|
|
22
|
+
}: BarConfigProps) => {
|
|
15
23
|
const heightMini = 3 /// height of small bars aka suppressed/NA/Zero valued
|
|
16
24
|
let barHeight = defaultBarHeight
|
|
17
25
|
|
|
@@ -20,6 +28,7 @@ export const getBarConfig = ({ bar, defaultBarHeight, defaultBarWidth, config, i
|
|
|
20
28
|
let barLabel = ''
|
|
21
29
|
let isSuppressed = false
|
|
22
30
|
let showMissingDataLabel = false
|
|
31
|
+
let showZeroValueData = false
|
|
23
32
|
const showSuppressedSymbol = config.general.showSuppressedSymbol
|
|
24
33
|
|
|
25
34
|
config.preliminaryData.forEach(pd => {
|
|
@@ -45,10 +54,19 @@ export const getBarConfig = ({ bar, defaultBarHeight, defaultBarWidth, config, i
|
|
|
45
54
|
barHeight = labelFits ? heightMini : 0
|
|
46
55
|
barWidthHorizontal = heightMini
|
|
47
56
|
}
|
|
57
|
+
// Handle undefined, null, or non-calculable bar.value
|
|
58
|
+
|
|
59
|
+
if (!isSuppressed && bar.value === '0' && config.general.showZeroValueData) {
|
|
60
|
+
const labelWidth = getTextWidth('0', `normal ${barWidth / 2}px sans-serif`)
|
|
61
|
+
const labelFits = Number(labelWidth) < barWidth && barWidth > 10
|
|
62
|
+
showZeroValueData = true
|
|
63
|
+
barHeight = labelFits ? heightMini : 0
|
|
64
|
+
barWidthHorizontal = heightMini
|
|
65
|
+
}
|
|
48
66
|
|
|
49
67
|
const getBarY = (defaultBarY, yScale) => {
|
|
50
68
|
// calculate Y position of small bars (suppressed,N/A,Zero valued) bars
|
|
51
|
-
if (isSuppressed || showMissingDataLabel) {
|
|
69
|
+
if (isSuppressed || showMissingDataLabel || showZeroValueData) {
|
|
52
70
|
if (config.isLollipopChart) {
|
|
53
71
|
return yScale - heightMini * 2
|
|
54
72
|
} else {
|
|
@@ -68,6 +86,7 @@ export const getBarConfig = ({ bar, defaultBarHeight, defaultBarWidth, config, i
|
|
|
68
86
|
if (isSuppressed) label = ''
|
|
69
87
|
// If the config is set to show a label for missing data, display 'N/A'
|
|
70
88
|
if (showMissingDataLabel) label = 'N/A'
|
|
89
|
+
if (showZeroValueData) label = '0'
|
|
71
90
|
|
|
72
91
|
// determine label width in pixels & check if it fits to the bar width
|
|
73
92
|
const labelWidth = getTextWidth(barLabel, `normal ${barWidth / 2}px sans-serif`)
|
|
@@ -78,7 +97,6 @@ export const getBarConfig = ({ bar, defaultBarHeight, defaultBarWidth, config, i
|
|
|
78
97
|
return labelFits && isVertical ? label : !isVertical ? label : ''
|
|
79
98
|
}
|
|
80
99
|
}
|
|
81
|
-
|
|
82
100
|
return { barWidthHorizontal, barHeight, isSuppressed, showMissingDataLabel, getBarY, getAbsentDataLabel }
|
|
83
101
|
}
|
|
84
102
|
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import React, { useContext } from 'react'
|
|
2
|
+
import { BoxPlot } from '@visx/stats'
|
|
3
|
+
import { Group } from '@visx/group'
|
|
4
|
+
import ConfigContext from '../../ConfigContext'
|
|
5
|
+
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
6
|
+
import { colorPalettesChart } from '@cdc/core/data/colorPalettes'
|
|
7
|
+
import { scaleBand } from '@visx/scale'
|
|
8
|
+
import _ from 'lodash'
|
|
9
|
+
import { max, min, median, quantile } from 'd3-array'
|
|
10
|
+
const CoveBoxPlot = ({ xScale, yScale }) => {
|
|
11
|
+
const { config, colorScale, seriesHighlight, transformedData: data } = useContext(ConfigContext)
|
|
12
|
+
const { boxplot } = config
|
|
13
|
+
|
|
14
|
+
// tooltips
|
|
15
|
+
const tooltip_id = `cdc-open-viz-tooltip-${config.runtime.uniqueId}`
|
|
16
|
+
|
|
17
|
+
const handleTooltip = (d, key, q1, q3, median, iqr) => {
|
|
18
|
+
return `
|
|
19
|
+
<strong>${d.columnCategory}</strong></br>
|
|
20
|
+
<strong>Key:${key}</strong></br>
|
|
21
|
+
${boxplot.labels.q1}: ${q1}<br/>
|
|
22
|
+
${boxplot.labels.q3}: ${q3}<br/>
|
|
23
|
+
${boxplot.labels.iqr}: ${iqr}<br/>
|
|
24
|
+
${boxplot.labels.median}: ${median}
|
|
25
|
+
`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const boxWidth = xScale.bandwidth()
|
|
29
|
+
const constrainedWidth = Math.min(40, boxWidth)
|
|
30
|
+
const color_0 = colorPalettesChart[config?.palette][0] ? colorPalettesChart[config?.palette][0] : '#000'
|
|
31
|
+
const seriesScale = scaleBand({
|
|
32
|
+
range: [0, config.barThickness * 100 || 1],
|
|
33
|
+
domain: config.series?.map(item => item?.dataKey)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const calculateBoxPlotStats = values => {
|
|
37
|
+
if (!values || !values.length) return {}
|
|
38
|
+
const sortedValues = values.sort((a, b) => a - b)
|
|
39
|
+
return {
|
|
40
|
+
min: min(values),
|
|
41
|
+
max: max(values),
|
|
42
|
+
median: median(values),
|
|
43
|
+
firstQuartile: quantile(sortedValues, 0.25),
|
|
44
|
+
thirdQuartile: quantile(sortedValues, 0.75)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const getValuesBySeriesKey = (group: string) => {
|
|
49
|
+
const allSeriesKeys = config.series.map(item => item?.dataKey)
|
|
50
|
+
const result = {}
|
|
51
|
+
const filteredData = data.filter(item => item[config.xAxis.dataKey] === group)
|
|
52
|
+
allSeriesKeys.forEach(key => {
|
|
53
|
+
result[key] = filteredData.map(item => item[key])
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
return result
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface Plot {
|
|
60
|
+
columnCategory: string
|
|
61
|
+
keyValues: { [key: string]: number[] }
|
|
62
|
+
}
|
|
63
|
+
const createPlots = data => {
|
|
64
|
+
const dataKeys = data.map(d => d[config.xAxis.dataKey])
|
|
65
|
+
const plots: Plot[] = []
|
|
66
|
+
const groups: string[] = _.uniq(dataKeys)
|
|
67
|
+
if (groups && groups.length > 0) {
|
|
68
|
+
groups.forEach(group => {
|
|
69
|
+
plots.push({
|
|
70
|
+
columnCategory: group,
|
|
71
|
+
keyValues: getValuesBySeriesKey(group)
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
return plots
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<ErrorBoundary component='BoxPlot'>
|
|
80
|
+
<Group left={Number(config.yAxis.size)} className='boxplot' key={`boxplot-group`}>
|
|
81
|
+
{createPlots(data).map((d, i) => {
|
|
82
|
+
const offset = boxWidth - constrainedWidth
|
|
83
|
+
const radius = 4
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<Group
|
|
87
|
+
key={`boxplotplot-${d.columnCategory}`}
|
|
88
|
+
left={xScale(d.columnCategory) + (xScale.bandwidth() - seriesScale.bandwidth()) / 2}
|
|
89
|
+
>
|
|
90
|
+
{config.series.map(item => {
|
|
91
|
+
const valuesByKey = d.keyValues[item.dataKey]
|
|
92
|
+
const { min, max, median, firstQuartile, thirdQuartile } = calculateBoxPlotStats(valuesByKey)
|
|
93
|
+
let iqr = Number(thirdQuartile - firstQuartile).toFixed(config.dataFormat.roundTo)
|
|
94
|
+
|
|
95
|
+
const transparentPlot =
|
|
96
|
+
config.legend.behavior === 'highlight' &&
|
|
97
|
+
seriesHighlight.length > 0 &&
|
|
98
|
+
seriesHighlight.indexOf(item.dataKey) === -1
|
|
99
|
+
const displayPlot =
|
|
100
|
+
config.legend.behavior === 'highlight' ||
|
|
101
|
+
seriesHighlight.length === 0 ||
|
|
102
|
+
seriesHighlight.indexOf(item.dataKey) !== -1
|
|
103
|
+
const fillOpacity = transparentPlot ? 0.3 : 0.5
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<Group key={`boxplotplot-${item}`}>
|
|
107
|
+
{boxplot.plotNonOutlierValues &&
|
|
108
|
+
valuesByKey.map((value, index) => {
|
|
109
|
+
return (
|
|
110
|
+
<circle
|
|
111
|
+
display={displayPlot ? 'block' : 'none'}
|
|
112
|
+
cx={seriesScale(item.dataKey) + seriesScale.bandwidth() / 2}
|
|
113
|
+
cy={yScale(value)}
|
|
114
|
+
r={radius}
|
|
115
|
+
fill={'#ccc'}
|
|
116
|
+
style={{ opacity: fillOpacity, fillOpacity: 1, stroke: 'black' }}
|
|
117
|
+
key={`boxplot-${i}--circle-${index}`}
|
|
118
|
+
/>
|
|
119
|
+
)
|
|
120
|
+
})}
|
|
121
|
+
{displayPlot && (
|
|
122
|
+
<BoxPlot
|
|
123
|
+
display={displayPlot ? 'block' : 'none'}
|
|
124
|
+
data-left={xScale(d.columnCategory) + config.yAxis.size + offset / 2 + 0.5}
|
|
125
|
+
key={`box-plot-${i}-${item}`}
|
|
126
|
+
min={min}
|
|
127
|
+
max={max}
|
|
128
|
+
left={seriesScale(item.dataKey)}
|
|
129
|
+
firstQuartile={firstQuartile}
|
|
130
|
+
thirdQuartile={thirdQuartile}
|
|
131
|
+
median={median}
|
|
132
|
+
boxWidth={seriesScale.bandwidth()}
|
|
133
|
+
fill={colorScale(item.dataKey)}
|
|
134
|
+
fillOpacity={fillOpacity}
|
|
135
|
+
stroke={fillOpacity ? 'rgba(0,0,0,0.2)' : 'rgba(0,0,0,0.1)'}
|
|
136
|
+
valueScale={yScale}
|
|
137
|
+
outliers={boxplot.plotOutlierValues ? d.columnOutliers : []}
|
|
138
|
+
outlierProps={{
|
|
139
|
+
style: {
|
|
140
|
+
fill: `${color_0}`,
|
|
141
|
+
opacity: fillOpacity
|
|
142
|
+
}
|
|
143
|
+
}}
|
|
144
|
+
medianProps={{
|
|
145
|
+
style: {
|
|
146
|
+
stroke: 'black',
|
|
147
|
+
opacity: fillOpacity
|
|
148
|
+
}
|
|
149
|
+
}}
|
|
150
|
+
boxProps={{
|
|
151
|
+
style: {
|
|
152
|
+
stroke: 'black',
|
|
153
|
+
strokeWidth: boxplot.borders === 'true' ? 1 : 0,
|
|
154
|
+
opacity: fillOpacity
|
|
155
|
+
}
|
|
156
|
+
}}
|
|
157
|
+
maxProps={{
|
|
158
|
+
style: {
|
|
159
|
+
stroke: 'black',
|
|
160
|
+
opacity: fillOpacity
|
|
161
|
+
}
|
|
162
|
+
}}
|
|
163
|
+
container
|
|
164
|
+
containerProps={{
|
|
165
|
+
'data-tooltip-html': handleTooltip(
|
|
166
|
+
d,
|
|
167
|
+
item.dataKey,
|
|
168
|
+
firstQuartile,
|
|
169
|
+
thirdQuartile,
|
|
170
|
+
median,
|
|
171
|
+
iqr
|
|
172
|
+
),
|
|
173
|
+
'data-tooltip-id': tooltip_id,
|
|
174
|
+
tabIndex: -1
|
|
175
|
+
}}
|
|
176
|
+
/>
|
|
177
|
+
)}
|
|
178
|
+
</Group>
|
|
179
|
+
)
|
|
180
|
+
})}
|
|
181
|
+
</Group>
|
|
182
|
+
)
|
|
183
|
+
})}
|
|
184
|
+
</Group>
|
|
185
|
+
</ErrorBoundary>
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export default CoveBoxPlot
|
|
@@ -2,15 +2,16 @@ import { Group } from '@visx/group'
|
|
|
2
2
|
import { useContext, useEffect, useRef, useState } from 'react'
|
|
3
3
|
import ConfigContext from '../ConfigContext'
|
|
4
4
|
import * as d3 from 'd3'
|
|
5
|
-
import { invertValue } from '@cdc/core/helpers/scaling'
|
|
6
5
|
import { Text } from '@visx/text'
|
|
6
|
+
import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
|
|
7
|
+
|
|
7
8
|
interface BrushChartProps {
|
|
8
9
|
xMax: number
|
|
9
10
|
yMax: number
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
const BrushChart = ({ xMax, yMax }: BrushChartProps) => {
|
|
13
|
-
const { tableData, config, setBrushConfig,
|
|
14
|
+
const { tableData, config, setBrushConfig, dashboardConfig, formatDate } = useContext(ConfigContext)
|
|
14
15
|
const [brushState, setBrushState] = useState({ isBrushing: false, selection: [] })
|
|
15
16
|
const [brushKey, setBrushKey] = useState(0)
|
|
16
17
|
const sharedFilters = dashboardConfig?.dashboard?.sharedFilters ?? []
|