@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.
Files changed (95) hide show
  1. package/LICENSE +201 -0
  2. package/dist/cdcchart.js +45911 -41739
  3. package/examples/feature/boxplot/boxplot-data.json +88 -22
  4. package/examples/feature/boxplot/boxplot.json +540 -16
  5. package/examples/feature/boxplot/testing.csv +7 -7
  6. package/examples/feature/sankey/sankey-example-data.json +0 -1
  7. package/examples/private/test.json +20092 -0
  8. package/index.html +4 -4
  9. package/package.json +2 -2
  10. package/src/CdcChart.tsx +209 -188
  11. package/src/_stories/Chart.CustomColors.stories.tsx +19 -0
  12. package/src/_stories/Chart.DynamicSeries.stories.tsx +27 -0
  13. package/src/_stories/Chart.Legend.Gradient.stories.tsx +74 -0
  14. package/src/_stories/Chart.stories.tsx +30 -3
  15. package/src/_stories/ChartAxisLabels.stories.tsx +20 -0
  16. package/src/_stories/ChartAxisTitles.stories.tsx +53 -0
  17. package/src/_stories/ChartEditor.stories.tsx +27 -0
  18. package/src/_stories/ChartLine.Suppression.stories.tsx +25 -0
  19. package/src/_stories/ChartPrefixSuffix.stories.tsx +159 -0
  20. package/src/_stories/_mock/boxplot_multiseries.json +647 -0
  21. package/src/_stories/_mock/dynamic_series_bar_config.json +723 -0
  22. package/src/_stories/_mock/dynamic_series_config.json +979 -0
  23. package/src/_stories/_mock/horizontal_bar.json +257 -0
  24. package/src/_stories/_mock/large_x_axis_labels.json +261 -0
  25. package/src/_stories/_mock/paired-bar.json +262 -0
  26. package/src/_stories/_mock/pie_with_data.json +255 -0
  27. package/{examples/feature/scatterplot/scatterplot.json → src/_stories/_mock/scatterplot_mock.json} +62 -92
  28. package/src/_stories/_mock/simplified_line.json +1510 -0
  29. package/src/_stories/_mock/suppression_mock.json +1549 -0
  30. package/src/components/Annotations/components/AnnotationDraggable.tsx +0 -3
  31. package/src/components/Annotations/components/AnnotationDropdown.tsx +1 -1
  32. package/src/components/Axis/Categorical.Axis.tsx +22 -4
  33. package/src/components/BarChart/components/BarChart.Horizontal.tsx +95 -16
  34. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +41 -17
  35. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +43 -9
  36. package/src/components/BarChart/components/BarChart.Vertical.tsx +123 -47
  37. package/src/components/BarChart/helpers/index.ts +23 -5
  38. package/src/components/BoxPlot/BoxPlot.tsx +189 -0
  39. package/src/components/BrushChart.tsx +3 -2
  40. package/src/components/DeviationBar.jsx +58 -8
  41. package/src/components/EditorPanel/EditorPanel.tsx +127 -102
  42. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +11 -28
  43. package/src/components/EditorPanel/components/Panels/Panel.BoxPlot.tsx +51 -6
  44. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +21 -4
  45. package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +40 -9
  46. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +3 -3
  47. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +121 -56
  48. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +296 -35
  49. package/src/components/EditorPanel/components/panels.scss +4 -6
  50. package/src/components/EditorPanel/editor-panel.scss +0 -8
  51. package/src/components/EditorPanel/helpers/tests/updateFieldRankByValue.test.ts +38 -0
  52. package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +42 -0
  53. package/src/components/EditorPanel/useEditorPermissions.ts +16 -1
  54. package/src/components/ForestPlot/ForestPlot.tsx +2 -3
  55. package/src/components/ForestPlot/ForestPlotProps.ts +2 -0
  56. package/src/components/Legend/Legend.Component.tsx +23 -24
  57. package/src/components/Legend/Legend.Suppression.tsx +25 -20
  58. package/src/components/Legend/Legend.tsx +16 -18
  59. package/src/components/Legend/helpers/index.ts +16 -19
  60. package/src/components/LegendWrapper.tsx +3 -1
  61. package/src/components/LineChart/components/LineChart.Circle.tsx +10 -0
  62. package/src/components/LineChart/helpers.ts +48 -43
  63. package/src/components/LineChart/index.tsx +88 -82
  64. package/src/components/LinearChart.tsx +747 -562
  65. package/src/components/PairedBarChart.jsx +50 -10
  66. package/src/components/PieChart/PieChart.tsx +1 -6
  67. package/src/components/Regions/components/Regions.tsx +33 -19
  68. package/src/components/Sankey/index.tsx +50 -32
  69. package/src/components/Sankey/sankey.scss +6 -5
  70. package/src/components/Sankey/useSankeyAlert.tsx +60 -0
  71. package/src/components/ScatterPlot/ScatterPlot.jsx +20 -4
  72. package/src/components/ZoomBrush.tsx +25 -6
  73. package/src/coreStyles_chart.scss +3 -0
  74. package/src/data/initial-state.js +8 -10
  75. package/src/helpers/configHelpers.ts +28 -0
  76. package/src/helpers/handleRankByValue.ts +15 -0
  77. package/src/helpers/sizeHelpers.ts +25 -0
  78. package/src/helpers/tests/handleRankByValue.test.ts +37 -0
  79. package/src/helpers/tests/sizeHelpers.test.ts +80 -0
  80. package/src/hooks/useColorPalette.js +10 -2
  81. package/src/hooks/useLegendClasses.ts +13 -22
  82. package/src/hooks/useMinMax.ts +27 -13
  83. package/src/hooks/useReduceData.ts +43 -10
  84. package/src/hooks/useScales.ts +87 -38
  85. package/src/hooks/useTooltip.tsx +62 -53
  86. package/src/index.jsx +1 -0
  87. package/src/scss/DataTable.scss +5 -4
  88. package/src/scss/main.scss +57 -70
  89. package/src/types/ChartConfig.ts +43 -34
  90. package/src/types/ChartContext.ts +22 -15
  91. package/src/types/ForestPlot.ts +8 -0
  92. package/src/_stories/Chart.Legend.Gradient.tsx +0 -19
  93. package/src/_stories/ChartBrush.stories.tsx +0 -19
  94. package/src/components/BoxPlot/BoxPlot.jsx +0 -111
  95. 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, isNumber, parseDate, seriesHighlight, setSharedFilter, transformedData, brushConfig, getTextWidth } = useContext<ChartContext>(ConfigContext)
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' || config.visualizationType === 'Combo' || isConvertLineToBarGraph(config.visualizationType, data, config.allowLineToBarGraph)) &&
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={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.map(item => item.value).filter(item => item !== ('' || undefined))
90
- highlightedBarValues = config.xAxis.type === 'date' ? HighLightedBarUtils.formatDates(highlightedBarValues) : highlightedBarValues
91
- const transparentBar = config.legend.behavior === 'highlight' && seriesHighlight.length > 0 && seriesHighlight.indexOf(bar.key) === -1
92
- const displayBar = config.legend.behavior === 'highlight' || seriesHighlight.length === 0 || seriesHighlight.indexOf(bar.key) !== -1
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 = bar.x + (config.isLollipopChart ? (barGroupWidth / barGroup.bars.length - lollipopBarWidth) / 2 : 0) - (config.xAxis.type === 'date-time' ? barGroupWidth / 2 : 0)
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(data[barGroup.index][config.runtime.originalXAxis.dataKey])) : data[barGroup.index][config.runtime.originalXAxis.dataKey]
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, data[barGroup.index][config.runtime.originalXAxis.dataKey])
109
- let xAxisTooltip = config.runtime.xAxis.label ? `${config.runtime.xAxis.label}: ${xAxisValue}` : xAxisValue
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 ? highlightedBarColor : config.barHasBorder === 'true' ? '#000' : 'transparent'
128
- const borderWidth = isHighlightedBar ? highlightedBar.borderWidth : config.isLollipopChart ? 0 : config.barHasBorder === 'true' ? barBorderWidth : 0
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({ bar, defaultBarHeight, config, isNumber, getTextWidth, barWidth, isVertical: true, yAxisValue })
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) return colorScale(config.runtime.seriesLabels[bar.key])
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]) return colorScale(config.runtime.seriesLabels[bar.key])
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) _barColor = assignColorsToValues(barGroups.length, barGroup.index, barColor)
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) _barColor = assignColorsToValues(barGroups.length, barGroup.index, barColor)
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 (!isSuppressed || barWidth < 10 || !config.general.showSuppressedSymbol || pd.hideBarSymbol) {
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 = pd.symbol === 'Asterisk' ? barWidth * 1.2 : pd.symbol === 'Double Asterisk' ? barWidth : barWidth / 1.5
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 = ({ bar, defaultBarHeight, defaultBarWidth, config, isNumber, getTextWidth, barWidth, isVertical }: BarConfigProps) => {
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, getTextWidth, dashboardConfig, formatDate } = useContext(ConfigContext)
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 ?? []