@cdc/chart 4.25.6 → 4.25.8

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 (47) hide show
  1. package/dist/cdcchart.js +53500 -32825
  2. package/package.json +3 -2
  3. package/src/CdcChart.tsx +9 -2
  4. package/src/CdcChartComponent.tsx +30 -12
  5. package/src/_stories/Chart.BoxPlot.stories.tsx +35 -0
  6. package/src/_stories/Chart.stories.tsx +0 -7
  7. package/src/_stories/Chart.tooltip.stories.tsx +35 -275
  8. package/src/_stories/_mock/bar-chart-suppressed.json +2 -80
  9. package/src/_stories/_mock/boxplot_multiseries.json +252 -166
  10. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +1 -1
  11. package/src/components/AreaChart/components/AreaChart.jsx +4 -8
  12. package/src/components/BarChart/components/BarChart.Horizontal.tsx +45 -7
  13. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +1 -1
  14. package/src/components/BarChart/components/BarChart.Vertical.tsx +36 -4
  15. package/src/components/BoxPlot/BoxPlot.Horizontal.tsx +131 -0
  16. package/src/components/BoxPlot/{BoxPlot.tsx → BoxPlot.Vertical.tsx} +4 -4
  17. package/src/components/BoxPlot/helpers/index.ts +32 -12
  18. package/src/components/Brush/BrushChart.tsx +65 -10
  19. package/src/components/Brush/BrushController.tsx +71 -0
  20. package/src/components/Brush/types.tsx +8 -0
  21. package/src/components/BrushChart.tsx +1 -1
  22. package/src/components/EditorPanel/EditorPanel.tsx +19 -14
  23. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +2 -2
  24. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +2 -2
  25. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +2 -34
  26. package/src/components/Forecasting/{Forecasting.jsx → Forecasting.tsx} +32 -12
  27. package/src/components/Legend/Legend.Component.tsx +16 -1
  28. package/src/components/Legend/Legend.tsx +3 -1
  29. package/src/components/Legend/LegendGroup/LegendGroup.tsx +1 -0
  30. package/src/components/Legend/helpers/index.ts +2 -2
  31. package/src/components/LineChart/components/LineChart.BumpCircle.tsx +27 -26
  32. package/src/components/LineChart/helpers.ts +7 -7
  33. package/src/components/LinearChart.tsx +130 -75
  34. package/src/data/initial-state.js +12 -15
  35. package/src/helpers/countNumOfTicks.ts +4 -19
  36. package/src/helpers/filterAndShiftLinearDateTicks.ts +58 -0
  37. package/src/helpers/getBridgedData.ts +13 -0
  38. package/src/helpers/tests/getBridgedData.test.ts +64 -0
  39. package/src/hooks/useScales.ts +42 -42
  40. package/src/hooks/useTooltip.tsx +3 -2
  41. package/src/index.jsx +6 -1
  42. package/src/scss/main.scss +2 -4
  43. package/src/store/chart.actions.ts +2 -2
  44. package/src/store/chart.reducer.ts +4 -12
  45. package/src/types/ChartConfig.ts +1 -6
  46. package/src/components/BoxPlot/index.tsx +0 -3
  47. package/src/components/Brush/BrushController..tsx +0 -39
@@ -11,7 +11,7 @@ import { Bar, AreaStack } from '@visx/shape'
11
11
  import { Group } from '@visx/group'
12
12
  import { approvedCurveTypes } from '@cdc/core/helpers/lineChartHelpers'
13
13
 
14
- const AreaChartStacked = ({ xScale, yScale, yMax, xMax, handleTooltipMouseOver, handleTooltipMouseOff, isDebug }) => {
14
+ const AreaChartStacked = ({ xScale, yScale, yMax, xMax, handleTooltipMouseOver, handleTooltipMouseOff }) => {
15
15
  // import data from context
16
16
  let { transformedData, config, seriesHighlight, colorScale, rawData } = useContext(ConfigContext)
17
17
  const data = config.brush?.active && config.brush.data?.length ? config.brush.data : transformedData
@@ -9,24 +9,20 @@ import { isDateScale } from '@cdc/core/helpers/cove/date'
9
9
  import * as allCurves from '@visx/curve'
10
10
  import { AreaClosed, LinePath, Bar } from '@visx/shape'
11
11
  import { Group } from '@visx/group'
12
- import { bisector } from 'd3-array'
13
12
 
14
13
  const AreaChart = props => {
15
- const { xScale, yScale, yMax, xMax, handleTooltipMouseOver, handleTooltipMouseOff, isDebug, children } = props
14
+ const { xScale, yScale, yMax, xMax, handleTooltipMouseOver, handleTooltipMouseOff } = props
16
15
  // import data from context
17
16
  let {
18
- transformedData,
17
+ transformedData: data,
19
18
  config,
20
19
  handleLineType,
21
20
  parseDate,
22
- formatDate,
23
- formatNumber,
21
+
24
22
  seriesHighlight,
25
23
  colorScale,
26
- rawData,
27
- brushConfig
24
+ rawData
28
25
  } = useContext(ConfigContext)
29
- const data = config.brush?.active && brushConfig.data?.length ? brushConfig.data : transformedData
30
26
 
31
27
  if (!data) return
32
28
 
@@ -59,7 +59,9 @@ export const BarChartHorizontal = () => {
59
59
 
60
60
  const { HighLightedBarUtils } = useHighlightedBars(config)
61
61
 
62
- const hasConfidenceInterval = Object.keys(config.confidenceKeys).length > 0
62
+ const hasConfidenceInterval = [config.confidenceKeys?.upper, config.confidenceKeys?.lower].every(
63
+ v => v != null && String(v).trim() !== ''
64
+ )
63
65
 
64
66
  const _data = getBarData(config, data, hasConfidenceInterval)
65
67
 
@@ -113,9 +115,15 @@ export const BarChartHorizontal = () => {
113
115
  numbericBarHeight = 25
114
116
  }
115
117
  let barY = bar.value >= 0 && isNumber(bar.value) ? bar.y : yScale(scaleVal)
116
- const defaultBarWidth = Math.abs(xScale(bar.value) - xScale(scaleVal))
118
+ let defaultBarWidth = Math.abs(xScale(bar.value) - xScale(scaleVal))
117
119
  const isPositiveBar = bar.value >= 0 && isNumber(bar.value)
118
120
 
121
+ const MINIMUM_BAR_HEIGHT = 3
122
+ if (isPositiveBar && barGroup.bars.length === 1 && defaultBarWidth < MINIMUM_BAR_HEIGHT) {
123
+ defaultBarWidth = MINIMUM_BAR_HEIGHT
124
+ barY = yScale(0) - MINIMUM_BAR_HEIGHT
125
+ }
126
+
119
127
  const barX = bar.value < 0 ? Math.abs(xScale(bar.value)) : xScale(scaleVal)
120
128
  const yAxisValue = formatNumber(bar.value, 'left')
121
129
  const xAxisValue =
@@ -164,7 +172,7 @@ export const BarChartHorizontal = () => {
164
172
  </li></ul>`
165
173
 
166
174
  // configure colors
167
- let labelColor = '#000000'
175
+ let labelColor = APP_FONT_COLOR
168
176
  labelColor = HighLightedBarUtils.checkFontColor(yAxisValue, highlightedBarValues, labelColor) // Set if background is transparent'
169
177
  let barColor =
170
178
  config.runtime.seriesLabels && config.runtime.seriesLabels[bar.key]
@@ -182,7 +190,7 @@ export const BarChartHorizontal = () => {
182
190
  const borderColor = isHighlightedBar
183
191
  ? highlightedBarColor
184
192
  : config.barHasBorder === 'true'
185
- ? '#000'
193
+ ? APP_FONT_COLOR
186
194
  : 'transparent'
187
195
  const borderWidth = isHighlightedBar
188
196
  ? highlightedBar.borderWidth
@@ -262,6 +270,21 @@ export const BarChartHorizontal = () => {
262
270
  }
263
271
  })}
264
272
 
273
+ {(absentDataLabel || isSuppressed) && (
274
+ <rect
275
+ x={barX}
276
+ y={0}
277
+ width={yMax}
278
+ height={numbericBarHeight}
279
+ fill='transparent'
280
+ data-tooltip-place='top'
281
+ data-tooltip-offset='{"top":3}'
282
+ style={{ pointerEvents: 'all', cursor: 'pointer' }}
283
+ data-tooltip-html={tooltip}
284
+ data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
285
+ />
286
+ )}
287
+
265
288
  {config.preliminaryData?.map((pd, index) => {
266
289
  // check if user selected column
267
290
  const selectedSuppressionColumn = !pd.column || pd.column === bar.key
@@ -281,7 +304,7 @@ export const BarChartHorizontal = () => {
281
304
  : pd.symbol === 'Double Asterisk'
282
305
  ? barHeight
283
306
  : barHeight / 1.5
284
- const fillColor = pd.displayGray ? '#8b8b8a' : '#000'
307
+ const fillColor = pd.displayGray ? '#8b8b8a' : APP_FONT_COLOR
285
308
  return (
286
309
  <Text // prettier-ignore
287
310
  key={index}
@@ -301,7 +324,7 @@ export const BarChartHorizontal = () => {
301
324
  )
302
325
  })}
303
326
 
304
- {!config.isLollipopChart && (
327
+ {!config.isLollipopChart && !hasConfidenceInterval && (
305
328
  <Text // prettier-ignore
306
329
  display={displayBar ? 'block' : 'none'}
307
330
  x={bar.y}
@@ -315,6 +338,21 @@ export const BarChartHorizontal = () => {
315
338
  {testZeroValue(bar.value) ? '' : barDefaultLabel}
316
339
  </Text>
317
340
  )}
341
+
342
+ {!config.isLollipopChart && hasConfidenceInterval && (
343
+ <Text // prettier-ignore
344
+ display={displayBar ? 'block' : 'none'}
345
+ x={bar.value < 0 ? bar.y + barWidth : bar.y - barWidth}
346
+ opacity={transparentBar ? 0.5 : 1}
347
+ y={config.barHeight / 2 + config.barHeight * bar.index}
348
+ fill={labelColor}
349
+ dx={-textPadding}
350
+ verticalAnchor='middle'
351
+ textAnchor={bar.value < 0 ? 'end' : 'start'}
352
+ >
353
+ {testZeroValue(bar.value) ? '' : barDefaultLabel}
354
+ </Text>
355
+ )}
318
356
  <Text // prettier-ignore
319
357
  display={displayBar ? 'block' : 'none'}
320
358
  x={bar.y}
@@ -334,7 +372,7 @@ export const BarChartHorizontal = () => {
334
372
  display={displayBar ? 'block' : 'none'}
335
373
  x={bar.y}
336
374
  y={0}
337
- fill={'#000000'}
375
+ fill={APP_FONT_COLOR}
338
376
  dx={textPaddingLollipop}
339
377
  textAnchor={textAnchorLollipop}
340
378
  verticalAnchor='middle'
@@ -137,7 +137,7 @@ const BarChartStackedHorizontal = () => {
137
137
  <Text
138
138
  x={`${bar.x + (config.isLollipopChart ? 15 : 5)}`} // padding
139
139
  y={bar.y + bar.height * 1.2}
140
- fill={'#000000'}
140
+ fill={APP_FONT_COLOR}
141
141
  textAnchor='start'
142
142
  verticalAnchor='start'
143
143
  >
@@ -16,6 +16,7 @@ import { isDateScale } from '@cdc/core/helpers/cove/date'
16
16
  import isNumber from '@cdc/core/helpers/isNumber'
17
17
  import createBarElement from '@cdc/core/components/createBarElement'
18
18
  import { APP_FONT_COLOR } from '@cdc/core/helpers/constants'
19
+ import { isMobileFontViewport } from '@cdc/core/helpers/viewports'
19
20
  // Third party libraries
20
21
  import chroma from 'chroma-js'
21
22
  // Types
@@ -45,6 +46,7 @@ export const BarChartVertical = () => {
45
46
  const {
46
47
  colorScale,
47
48
  config,
49
+ currentViewport,
48
50
  dashboardConfig,
49
51
  tableData,
50
52
  formatDate,
@@ -123,8 +125,20 @@ export const BarChartVertical = () => {
123
125
  seriesHighlight.indexOf(bar.key) !== -1
124
126
 
125
127
  let barGroupWidth = seriesScale.range()[1] - seriesScale.range()[0]
126
- const defaultBarHeight = Math.abs(yScale(bar.value) - yScale(scaleVal))
127
- const defaultBarY = bar.value >= 0 && isNumber(bar.value) ? bar.y : yScale(0)
128
+ let defaultBarHeight = Math.abs(yScale(bar.value) - yScale(scaleVal))
129
+ let defaultBarY = bar.value >= 0 && isNumber(bar.value) ? bar.y : yScale(0)
130
+
131
+ const MINIMUM_BAR_HEIGHT = 3
132
+ if (
133
+ bar.value >= 0 &&
134
+ isNumber(bar.value) &&
135
+ barGroup.bars.length === 1 &&
136
+ defaultBarHeight < MINIMUM_BAR_HEIGHT
137
+ ) {
138
+ defaultBarHeight = MINIMUM_BAR_HEIGHT
139
+ defaultBarY = yScale(0) - MINIMUM_BAR_HEIGHT
140
+ }
141
+
128
142
  let barWidth = config.isLollipopChart ? lollipopBarWidth : seriesScale.bandwidth()
129
143
  let barX =
130
144
  bar.x +
@@ -159,7 +173,7 @@ export const BarChartVertical = () => {
159
173
  yAxisValue
160
174
  })
161
175
  // configure colors
162
- let labelColor = '#000000'
176
+ let labelColor = APP_FONT_COLOR
163
177
  labelColor = HighLightedBarUtils.checkFontColor(yAxisValue, highlightedBarValues, labelColor) // Set if background is transparent'
164
178
  const isRegularLollipopColor = config.isLollipopChart && config.lollipopColorStyle === 'regular'
165
179
  const isTwoToneLollipopColor = config.isLollipopChart && config.lollipopColorStyle === 'two-tone'
@@ -254,6 +268,8 @@ export const BarChartVertical = () => {
254
268
 
255
269
  const BAR_LABEL_PADDING = 10
256
270
 
271
+ const LABEL_FONT_SIZE = isMobileFontViewport(currentViewport) ? 13 : 16
272
+
257
273
  return (
258
274
  <Group display={hideGroup} key={`${barGroup.index}--${index}`}>
259
275
  <Group key={`bar-sub-group-${barGroup.index}-${barGroup.x0}-${barY}--${index}`}>
@@ -288,6 +304,21 @@ export const BarChartVertical = () => {
288
304
  }
289
305
  })}
290
306
 
307
+ {(absentDataLabel || isSuppressed) && (
308
+ <rect
309
+ x={barX}
310
+ y={0}
311
+ width={barWidth}
312
+ height={yMax}
313
+ fill='transparent'
314
+ data-tooltip-place='top'
315
+ data-tooltip-offset='{"top":3}'
316
+ style={{ pointerEvents: 'all', cursor: 'pointer' }}
317
+ data-tooltip-html={tooltip}
318
+ data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
319
+ />
320
+ )}
321
+
291
322
  {config.preliminaryData.map((pd, index) => {
292
323
  // check if user selected column
293
324
  const selectedSuppressionColumn = !pd.column || pd.column === bar.key
@@ -338,6 +369,7 @@ export const BarChartVertical = () => {
338
369
  y={barY - BAR_LABEL_PADDING}
339
370
  fill={labelColor}
340
371
  textAnchor='middle'
372
+ fontSize={LABEL_FONT_SIZE}
341
373
  >
342
374
  {testZeroValue(bar.value) ? '' : barDefaultLabel}
343
375
  </Text>
@@ -348,7 +380,7 @@ export const BarChartVertical = () => {
348
380
  y={barY - BAR_LABEL_PADDING}
349
381
  fill={labelColor}
350
382
  textAnchor='middle'
351
- fontSize={config.isLollipopChart ? null : barWidth / 2}
383
+ fontSize={config.isLollipopChart ? null : LABEL_FONT_SIZE}
352
384
  >
353
385
  {absentDataLabel}
354
386
  </Text>
@@ -0,0 +1,131 @@
1
+ import React, { useContext } from 'react'
2
+ import { Group } from '@visx/group'
3
+ import { BoxPlot } from '@visx/stats'
4
+ import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
5
+ import ConfigContext from '../../ConfigContext'
6
+ import { handleTooltip, createPlots } from './helpers/index'
7
+ import { APP_FONT_COLOR } from '@cdc/core/helpers/constants'
8
+ import _ from 'lodash'
9
+
10
+ const BoxPlotHorizontal = ({ xScale, yScale, seriesScale }) => {
11
+ const { config, transformedData: data, colorScale, seriesHighlight } = useContext(ConfigContext)
12
+ const yOffset = Number(config.xAxis.size)
13
+ const defaultColor = APP_FONT_COLOR
14
+ const tooltip_id = `cdc-open-viz-tooltip-${config.runtime.uniqueId}`
15
+ // generate summary stats
16
+ const plots = createPlots(data, config)
17
+ const { plotNonOutlierValues, plotOutlierValues } = config.boxplot
18
+
19
+ return (
20
+ <ErrorBoundary component='BoxPlot Horizontal'>
21
+ <Group left={yOffset} top={0} className='boxplot'>
22
+ {plots.map(plot => {
23
+ const category = plot.columnCategory
24
+
25
+ return config.series.map(item => {
26
+ const y0 = yScale(category)
27
+ const offset = seriesScale(item.dataKey)
28
+ const isTransparent =
29
+ config.legend.behavior === 'highlight' &&
30
+ seriesHighlight.length > 0 &&
31
+ seriesHighlight.indexOf(item.dataKey) === -1
32
+ const displayPlot =
33
+ config.legend.behavior === 'highlight' ||
34
+ seriesHighlight.length === 0 ||
35
+ seriesHighlight.indexOf(item.dataKey) !== -1
36
+ const fillOpacity = isTransparent ? 0.3 : 0.7
37
+ // outlier & non-outlier arrays
38
+ const nonOut = plot.columnNonOutliers?.[item.dataKey] || []
39
+ const out = plot.columnOutliers?.[item.dataKey] || []
40
+
41
+ return (
42
+ <Group key={`${category}-${item.dataKey}`} top={y0 + offset}>
43
+ {displayPlot && (
44
+ <BoxPlot
45
+ min={Number(plot.min[item.dataKey])}
46
+ max={Number(plot.max[item.dataKey])}
47
+ thirdQuartile={plot.q3[item.dataKey]}
48
+ firstQuartile={plot.q1[item.dataKey]}
49
+ median={plot.median[item.dataKey]}
50
+ horizontal={true}
51
+ valueScale={xScale}
52
+ boxWidth={seriesScale.bandwidth()}
53
+ fill={colorScale(item.dataKey)}
54
+ fillOpacity={1}
55
+ stroke={defaultColor}
56
+ boxProps={{
57
+ fill: colorScale(item.dataKey),
58
+ strokeWidth: config.boxplot.borders === 'true' ? 1.5 : 0,
59
+ stroke: defaultColor,
60
+ fillOpacity: fillOpacity
61
+ }}
62
+ minProps={{ stroke: defaultColor, strokeWidth: 1, opacity: fillOpacity }}
63
+ maxProps={{ stroke: defaultColor, strokeWidth: 1, opacity: fillOpacity }}
64
+ medianProps={{ stroke: defaultColor, strokeWidth: 1, opacity: fillOpacity }}
65
+ outliers={
66
+ config.boxplot.plotOutlierValues ? _.map(plot.columnOutliers[item.dataKey], item => item) : []
67
+ }
68
+ outlierProps={{
69
+ style: {
70
+ fill: colorScale(item.dataKey),
71
+ opacity: fillOpacity,
72
+ stroke: defaultColor
73
+ }
74
+ }}
75
+ container
76
+ containerProps={{
77
+ 'data-tooltip-html': handleTooltip(
78
+ config.boxplot,
79
+ plot.columnCategory,
80
+ item.dataKey,
81
+ _.round(plot.q1[item.dataKey], config.dataFormat.roundTo),
82
+ _.round(plot.q3[item.dataKey], config.dataFormat.roundTo),
83
+ _.round(plot.median[item.dataKey], config.dataFormat.roundTo),
84
+ _.round(plot.iqr[item.dataKey], config.dataFormat.roundTo),
85
+ config.xAxis.label,
86
+ defaultColor
87
+ ),
88
+ 'data-tooltip-id': tooltip_id,
89
+ tabIndex: -1
90
+ }}
91
+ />
92
+ )}
93
+
94
+ {/* non-outlier points */}
95
+ {plotNonOutlierValues &&
96
+ nonOut.map((value, idx) => (
97
+ <circle
98
+ display={displayPlot ? 'block' : 'none'}
99
+ key={`non-${category}-${item.dataKey}-${idx}`}
100
+ cx={xScale(value)}
101
+ cy={seriesScale.bandwidth() / 2}
102
+ r={4}
103
+ opacity={fillOpacity}
104
+ fill={defaultColor}
105
+ style={{ stroke: defaultColor }}
106
+ />
107
+ ))}
108
+
109
+ {/* outlier points */}
110
+ {plotOutlierValues &&
111
+ out.map((value, idx) => (
112
+ <circle
113
+ display={displayPlot ? 'block' : 'none'}
114
+ key={`out-${category}-${item.dataKey}-${idx}`}
115
+ cx={xScale(value)}
116
+ cy={seriesScale.bandwidth() / 2}
117
+ r={4}
118
+ opacity={fillOpacity}
119
+ fill={defaultColor}
120
+ style={{ stroke: defaultColor }}
121
+ />
122
+ ))}
123
+ </Group>
124
+ )
125
+ })
126
+ })}
127
+ </Group>
128
+ </ErrorBoundary>
129
+ )
130
+ }
131
+ export default BoxPlotHorizontal
@@ -8,7 +8,7 @@ import { handleTooltip, createPlots } from './helpers/index'
8
8
  import { APP_FONT_COLOR } from '@cdc/core/helpers/constants'
9
9
  import _ from 'lodash'
10
10
 
11
- const CoveBoxPlot = ({ xScale, yScale, seriesScale }) => {
11
+ const BoxPlotVertical = ({ xScale, yScale, seriesScale }) => {
12
12
  const { config, colorScale, seriesHighlight, transformedData: data } = useContext(ConfigContext)
13
13
  const { boxplot } = config
14
14
 
@@ -21,7 +21,7 @@ const CoveBoxPlot = ({ xScale, yScale, seriesScale }) => {
21
21
  const color_0 = _.get(colorPalettesChart, [config.palette, 0], '#000')
22
22
  const plots = createPlots(data, config)
23
23
  return (
24
- <ErrorBoundary component='BoxPlot'>
24
+ <ErrorBoundary component='BoxPlot Vertical'>
25
25
  <Group left={Number(config.yAxis.size)} className='boxplot' key={`boxplot-group`}>
26
26
  {plots.map((d, i) => {
27
27
  const offset = boxWidth - constrainedWidth
@@ -41,7 +41,7 @@ const CoveBoxPlot = ({ xScale, yScale, seriesScale }) => {
41
41
  config.legend.behavior === 'highlight' ||
42
42
  seriesHighlight.length === 0 ||
43
43
  seriesHighlight.indexOf(item.dataKey) !== -1
44
- const fillOpacity = isTransparent ? 0.3 : 0.5
44
+ const fillOpacity = isTransparent ? 0.3 : 0.7
45
45
  return (
46
46
  <Group key={`boxplotplot-${item.dataKey}-${index}`}>
47
47
  {boxplot.plotNonOutlierValues &&
@@ -131,4 +131,4 @@ const CoveBoxPlot = ({ xScale, yScale, seriesScale }) => {
131
131
  )
132
132
  }
133
133
 
134
- export default CoveBoxPlot
134
+ export default BoxPlotVertical
@@ -33,7 +33,7 @@ export const calculateBoxPlotStats = (values: number[]) => {
33
33
  if (!values || values.length === 0) return {}
34
34
 
35
35
  // Sort the values
36
- const sortedValues = _.sortBy(values)
36
+ const sortedValues = _.sortBy(values.map(v => Number(v)))
37
37
 
38
38
  // Quartiles
39
39
  const firstQuartile = d3.quantile(sortedValues, 0.25) ?? 0
@@ -45,18 +45,28 @@ export const calculateBoxPlotStats = (values: number[]) => {
45
45
  // Outlier Bounds
46
46
  const lowerBound = firstQuartile - 1.5 * iqr
47
47
  const upperBound = thirdQuartile + 1.5 * iqr
48
+ // const lowerFence = q1 - 1.5 * iqr
49
+ // const upperFence = q3 + 1.5 * iqr
48
50
 
49
51
  // Non-Outlier Values
50
52
  const nonOutliers = sortedValues.filter(value => value >= lowerBound && value <= upperBound)
51
-
53
+ // **Outliers** =
54
+ const outliers = sortedValues.filter(v => v < lowerBound || v > upperBound)
55
+ const whiskerMax =
56
+ sortedValues
57
+ .slice()
58
+ .reverse()
59
+ .find(v => v <= upperBound) ?? thirdQuartile
60
+ const whiskerMin = nonOutliers.length > 0 ? nonOutliers[0] : firstQuartile
52
61
  // Calculate Box Plot Stats
53
62
  return {
54
- min: d3.min(nonOutliers), // Smallest non-outlier value
55
- max: d3.max(nonOutliers), // Largest non-outlier value
56
- median: d3.median(sortedValues), // Median of all values
63
+ min: whiskerMin,
64
+ max: whiskerMax,
65
+ median: d3.median(sortedValues),
57
66
  firstQuartile,
58
67
  thirdQuartile,
59
- iqr
68
+ iqr,
69
+ outliers
60
70
  }
61
71
  }
62
72
 
@@ -109,16 +119,26 @@ export const createPlots = (data, config) => {
109
119
 
110
120
  // Calculate outliers and non-outliers for each series key
111
121
  Object.keys(keyValues).forEach(key => {
112
- const values = keyValues[key]
122
+ const raw = keyValues[key] ?? []
123
+
124
+ // 2) normalize → trim, drop empties/non-numbers, coerce to Number
125
+ const cleaned: number[] = raw
126
+ .map(v => (typeof v === 'string' ? v.trim() : v)) // trim strings
127
+ .filter(v => v != null && v !== '' && !isNaN(+v)) // drop null/''/non-nums
128
+ .map(v => +v)
129
+
130
+ if (cleaned.length === 0) {
131
+ return
132
+ }
113
133
 
114
134
  // Calculate box plot statistics
115
- const { firstQuartile, thirdQuartile, min, max, median, iqr } = calculateBoxPlotStats(values)
135
+ const { firstQuartile, thirdQuartile, min, max, median, iqr, outliers } = calculateBoxPlotStats(cleaned)
116
136
  // Calculate outliers and non-outliers
117
- columnOutliers[key] = calculateOutliers(values, firstQuartile, thirdQuartile).map(Number)
118
- columnNonOutliers[key] = calculateNonOutliers(values, firstQuartile, thirdQuartile).map(Number)
137
+ columnOutliers[key] = calculateOutliers(cleaned, firstQuartile, thirdQuartile).map(Number)
138
+ columnNonOutliers[key] = calculateNonOutliers(cleaned, firstQuartile, thirdQuartile).map(Number)
119
139
  columnMedian[key] = median
120
- columnMin[key] = min
121
- columnMax[key] = max
140
+ columnMin[key] = Number(min)
141
+ columnMax[key] = Number(max)
122
142
  columnQ1[key] = firstQuartile
123
143
  columnQ3[key] = thirdQuartile
124
144
  columnIqr[key] = iqr
@@ -6,28 +6,36 @@ import ConfigContext from '../../ConfigContext'
6
6
  import { Text } from '@visx/text'
7
7
  import { APP_FONT_SIZE } from '@cdc/core/helpers/constants'
8
8
  import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
9
- import { isDateScale } from '@cdc/core/helpers/cove/date'
10
- export interface ZoomBrushProps {
9
+ export interface BrushChartProps {
11
10
  xMax: number
12
11
  yMax: number
13
12
  brushPosition: { start: { x: number }; end: { x: number } }
14
13
  onBrushChange: (bounds: any) => void
15
14
  brushKey: number
15
+ brushHandleProps: { startValue: string; endValue: string; endPos: number; startPos: number }
16
+ brushRef: React.RefObject<BrushRef>
16
17
  }
17
18
 
18
- const ZoomBrush: FC<ZoomBrushProps> = ({ xMax, yMax, brushPosition, onBrushChange, brushKey }) => {
19
- const { tableData, config, parseDate, dashboardConfig } = useContext(ConfigContext)
20
- const brushRef = useRef(null)
19
+ const BrushChart: FC<BrushChartProps> = ({
20
+ xMax,
21
+ yMax,
22
+ brushPosition,
23
+ onBrushChange,
24
+ brushKey,
25
+ brushHandleProps,
26
+ brushRef
27
+ }) => {
28
+ const { tableData, config, dashboardConfig } = useContext(ConfigContext)
21
29
  const dataKey = config.xAxis.dataKey
22
30
  const borderRadius = 15
23
31
  const mappedDates: string[] = tableData.map(row => row[dataKey])
24
32
  const brushheight = 25
25
33
  const DASHBOARD_MARGIN = 50
26
34
  const BRUSH_HEIGHT_MULTIPLIER = 1.5
27
-
35
+ const range = config?.xAxis?.sortByRecentDate ? [xMax, 0] : [0, xMax]
28
36
  const xScale = scaleBand<string>({
29
- domain: mappedDates,
30
- range: [0, xMax],
37
+ domain: config?.xAxis?.sortByRecentDate ? mappedDates.reverse() : mappedDates,
38
+ range: range,
31
39
  paddingInner: 0.1,
32
40
  paddingOuter: 0.1
33
41
  })
@@ -52,6 +60,16 @@ const ZoomBrush: FC<ZoomBrushProps> = ({ xMax, yMax, brushPosition, onBrushChang
52
60
  <Group left={config.yAxis.size} top={calculateGroupTop()}>
53
61
  <rect fill='#949494' width={xMax} height={25} rx={borderRadius} pointerEvents='none' />
54
62
  <Brush
63
+ disableDraggingOverlay={false}
64
+ renderBrushHandle={props => (
65
+ <BrushHandle
66
+ left={Number(config.runtime.yAxis.size)}
67
+ pixelDistance={brushHandleProps.endPos - brushHandleProps.startPos}
68
+ textProps={brushHandleProps}
69
+ isBrushing={brushRef.current?.state.isBrushing}
70
+ {...props}
71
+ />
72
+ )}
55
73
  innerRef={brushRef}
56
74
  key={brushKey}
57
75
  xScale={xScale}
@@ -64,10 +82,47 @@ const ZoomBrush: FC<ZoomBrushProps> = ({ xMax, yMax, brushPosition, onBrushChang
64
82
  initialBrushPosition={brushPosition}
65
83
  selectedBoxStyle={style}
66
84
  onChange={onBrushChange}
67
- disableDraggingOverlay={true}
85
+ useWindowMoveEvents={true}
68
86
  />
69
87
  </Group>
70
88
  )
71
89
  }
72
90
 
73
- export default ZoomBrush
91
+ export default BrushChart
92
+
93
+ const BrushHandle = props => {
94
+ const { x, y, isBrushing, className, textProps } = props
95
+ const pathWidth = 8
96
+
97
+ // Flip the SVG path horizontally for the left handle
98
+ const isLeft = className.includes('left')
99
+ const transform = isLeft ? 'scale(-1, 1)' : 'translate(0,0)'
100
+ const textAnchor = isLeft ? 'end' : 'start'
101
+ const tooltipText = isLeft ? ` Drag edges to focus on a specific segment ` : ''
102
+ const textFontSize = APP_FONT_SIZE / 1.4
103
+ const textWidth = getTextWidth(textProps.startValue, `${textFontSize}px`)
104
+ const textPosLeft = x > 0 ? 0 : 55
105
+ const textPosRight = y < textProps.xMax ? 0 : -50
106
+ return (
107
+ <Group left={x + pathWidth / 2} top={-2}>
108
+ <Text
109
+ pointerEvents='visiblePainted'
110
+ dominantBaseline='hanging'
111
+ x={isLeft ? textPosLeft : textPosRight}
112
+ y={25}
113
+ verticalAnchor='start'
114
+ textAnchor={textAnchor}
115
+ fontSize={textFontSize}
116
+ >
117
+ {isLeft ? textProps.startValue : textProps.endValue}
118
+ </Text>
119
+ <path
120
+ cursor='ew-resize'
121
+ d='M0.5,10A6,6 0 0 1 6.5,16V14A6,6 0 0 1 0.5,20ZM2.5,18V12M4.5,18V12'
122
+ fill='#297EF1'
123
+ strokeWidth='1'
124
+ transform={transform}
125
+ />
126
+ </Group>
127
+ )
128
+ }