@cdc/chart 4.23.2 → 4.23.3

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 (48) hide show
  1. package/dist/cdcchart.js +41198 -39447
  2. package/examples/area-chart.json +187 -0
  3. package/examples/big-small-test-bar.json +328 -0
  4. package/examples/big-small-test-line.json +328 -0
  5. package/examples/big-small-test-negative.json +328 -0
  6. package/examples/box-plot.json +0 -1
  7. package/examples/example-bar-chart.json +4 -1
  8. package/examples/example-sparkline.json +76 -0
  9. package/examples/gallery/bar-chart-horizontal/horizontal-bar-chart.json +31 -172
  10. package/examples/gallery/bar-chart-vertical/vertical-bar-chart-confidence.json +1 -0
  11. package/examples/gallery/bar-chart-vertical/vertical-bar-chart-with-confidence.json +96 -14
  12. package/examples/gallery/line/line.json +1 -0
  13. package/examples/horizontal-chart-max-increase.json +38 -0
  14. package/examples/line-chart-max-increase.json +32 -0
  15. package/examples/line-chart-nonnumeric.json +5 -5
  16. package/examples/line-chart.json +6 -6
  17. package/examples/planet-deviation-config.json +168 -0
  18. package/examples/planet-deviation-data.json +38 -0
  19. package/examples/planet-example-config.json +139 -20
  20. package/examples/planet-example-data-max-increase.json +56 -0
  21. package/examples/planet-example-data.json +10 -10
  22. package/examples/scatterplot-continuous.csv +3 -3
  23. package/examples/scatterplot.json +2 -2
  24. package/examples/sparkline-chart-nonnumeric.json +3 -3
  25. package/index.html +26 -9
  26. package/package.json +6 -3
  27. package/src/CdcChart.jsx +146 -92
  28. package/src/components/AreaChart.jsx +198 -0
  29. package/src/components/BarChart.jsx +58 -34
  30. package/src/components/BoxPlot.jsx +28 -15
  31. package/src/components/DataTable.jsx +21 -17
  32. package/src/components/DeviationBar.jsx +191 -0
  33. package/src/components/EditorPanel.jsx +473 -168
  34. package/src/components/Filters.jsx +3 -2
  35. package/src/components/Legend.jsx +59 -46
  36. package/src/components/LineChart.jsx +3 -21
  37. package/src/components/LinearChart.jsx +158 -55
  38. package/src/components/PairedBarChart.jsx +0 -1
  39. package/src/components/PieChart.jsx +11 -14
  40. package/src/components/ScatterPlot.jsx +19 -16
  41. package/src/components/SparkLine.jsx +87 -85
  42. package/src/components/useIntersectionObserver.jsx +1 -1
  43. package/src/data/initial-state.js +20 -4
  44. package/src/hooks/useColorPalette.js +58 -48
  45. package/src/hooks/useReduceData.js +3 -4
  46. package/src/index.jsx +1 -1
  47. package/src/scss/editor-panel.scss +5 -0
  48. package/src/test/CdcChart.test.jsx +6 -0
@@ -84,7 +84,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
84
84
 
85
85
  // return new updated bars/groupes
86
86
  return barsArr.map((bar, i) => {
87
- // set bars Y dynamycly to handle space between bars
87
+ // set bars Y dynamically to handle space between bars
88
88
  let y = 0
89
89
  bar.index !== 0 && (y = (barHeight + barSpace + labelHeight) * i)
90
90
 
@@ -105,13 +105,13 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
105
105
  }
106
106
  })
107
107
  }
108
- }, [config, updateConfig])
108
+ }, [config, updateConfig]) // eslint-disable-line
109
109
 
110
110
  useEffect(() => {
111
111
  if (config.isLollipopChart === false && config.barHeight < 25) {
112
112
  updateConfig({ ...config, barHeight: 25 })
113
113
  }
114
- }, [config.isLollipopChart])
114
+ }, [config.isLollipopChart]) // eslint-disable-line
115
115
 
116
116
  useEffect(() => {
117
117
  if (config.visualizationSubType === 'horizontal') {
@@ -120,7 +120,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
120
120
  orientation: 'horizontal'
121
121
  })
122
122
  }
123
- }, [])
123
+ }, []) // eslint-disable-line
124
124
 
125
125
  useEffect(() => {
126
126
  if (config.barStyle === 'lollipop' && !config.isLollipopChart) {
@@ -129,7 +129,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
129
129
  if (isRounded || config.barStyle === 'flat') {
130
130
  updateConfig({ ...config, isLollipopChart: false })
131
131
  }
132
- }, [config.barStyle])
132
+ }, [config.barStyle]) // eslint-disable-line
133
133
 
134
134
  return (
135
135
  <ErrorBoundary component='BarChart'>
@@ -147,7 +147,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
147
147
  let offset = (barThickness * (1 - (config.barThickness || 0.8))) / 2
148
148
  // tooltips
149
149
  const xAxisValue = config.runtime.xAxis.type === 'date' ? formatDate(parseDate(data[bar.index][config.runtime.xAxis.dataKey])) : data[bar.index][config.runtime.xAxis.dataKey]
150
- const yAxisValue = formatNumber(bar.bar ? bar.bar.data[bar.key] : 0)
150
+ const yAxisValue = formatNumber(bar.bar ? bar.bar.data[bar.key] : 0, 'left')
151
151
  const style = applyRadius(barStack.index, yAxisValue < 0)
152
152
  let yAxisTooltip = config.runtime.yAxis.label ? `${config.runtime.yAxis.label}: ${yAxisValue}` : yAxisValue
153
153
  const xAxisTooltip = config.runtime.xAxis.label ? `${config.runtime.xAxis.label}: ${xAxisValue}` : xAxisValue
@@ -161,7 +161,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
161
161
  ${xAxisTooltip}
162
162
  </div>`
163
163
  return (
164
- <>
164
+ <Group key={`${barStack.index}--${bar.index}--${orientation}`}>
165
165
  <style>
166
166
  {`
167
167
  #barStack${barStack.index}-${bar.index} rect,
@@ -173,7 +173,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
173
173
  </style>
174
174
  <Group key={`bar-stack-${barStack.index}-${bar.index}`} id={`barStack${barStack.index}-${bar.index}`} className='stack vertical'>
175
175
  <Text display={config.labels && displayBar ? 'block' : 'none'} opacity={transparentBar ? 0.5 : 1} x={barThickness * bar.index + offset} y={bar.y - 5} fill={bar.color} textAnchor='middle'>
176
- {formatNumber(bar.bar ? bar.bar.data[bar.key] : 0)}
176
+ {yAxisValue}
177
177
  </Text>
178
178
  <foreignObject
179
179
  key={`bar-stack-${barStack.index}-${bar.index}`}
@@ -188,7 +188,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
188
188
  data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
189
189
  ></foreignObject>
190
190
  </Group>
191
- </>
191
+ </Group>
192
192
  )
193
193
  })
194
194
  )
@@ -208,7 +208,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
208
208
  config.barHeight = Number(config.barHeight)
209
209
  let labelColor = '#000000'
210
210
  // tooltips
211
- const xAxisValue = formatNumber(data[bar.index][bar.key])
211
+ const xAxisValue = formatNumber(data[bar.index][bar.key], 'left')
212
212
  const yAxisValue = config.runtime.yAxis.type === 'date' ? formatDate(parseDate(data[bar.index][config.runtime.originalXAxis.dataKey])) : data[bar.index][config.runtime.originalXAxis.dataKey]
213
213
  const style = applyRadius(barStack.index, yAxisValue < 0)
214
214
  let yAxisTooltip = config.runtime.yAxis.label ? `${config.runtime.yAxis.label}: ${yAxisValue}` : yAxisValue
@@ -264,7 +264,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
264
264
  </Text>
265
265
  )}
266
266
 
267
- {displayNumbersOnBar && textWidth + 50 < bar.width && (
267
+ {displayNumbersOnBar && textWidth < bar.width && (
268
268
  <Text
269
269
  display={displayBar ? 'block' : 'none'}
270
270
  x={bar.x + barStack.bars[bar.index].width / 2} // padding
@@ -280,7 +280,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
280
280
  }
281
281
  }}
282
282
  >
283
- {formatNumber(data[bar.index][bar.key])}
283
+ {xAxisValue}
284
284
  </Text>
285
285
  )}
286
286
  </Group>
@@ -313,6 +313,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
313
313
  <Group
314
314
  className={`bar-group-${barGroup.index}-${barGroup.x0}--${index} ${config.orientation}`}
315
315
  key={`bar-group-${barGroup.index}-${barGroup.x0}--${index}`}
316
+ id={`bar-group-${barGroup.index}-${barGroup.x0}--${index}`}
316
317
  top={config.runtime.horizontal ? barGroup.y : 0}
317
318
  left={config.runtime.horizontal ? 0 : (xMax / barGroups.length) * barGroup.index}
318
319
  >
@@ -341,7 +342,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
341
342
  }
342
343
  if (config.legend.colorCode && config.series.length === 1) barColor = palette[barGroup.index]
343
344
 
344
- let yAxisValue = formatNumber(bar.value)
345
+ let yAxisValue = formatNumber(bar.value, 'left')
345
346
  let xAxisValue = config.runtime[section].type === 'date' ? formatDate(parseDate(data[barGroup.index][config.runtime.originalXAxis.dataKey])) : data[barGroup.index][config.runtime.originalXAxis.dataKey]
346
347
 
347
348
  if (config.runtime.horizontal) {
@@ -379,7 +380,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
379
380
  </div>`
380
381
 
381
382
  return (
382
- <>
383
+ <Group key={`${barGroup.index}--${index}--${orientation}`}>
383
384
  {/* This feels gross but inline transition was not working well*/}
384
385
  <style>
385
386
  {`
@@ -446,7 +447,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
446
447
  ;
447
448
  {orientation === 'vertical' && (
448
449
  <Text display={config.labels && displayBar ? 'block' : 'none'} opacity={transparentBar ? 0.5 : 1} x={barWidth * (bar.index + 0.5) + offset} y={barY - 5} fill={barColor} textAnchor='middle'>
449
- {formatNumber(bar.value)}
450
+ {yAxisValue}
450
451
  </Text>
451
452
  )}
452
453
  ;
@@ -478,7 +479,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
478
479
  </rect>
479
480
  )}
480
481
  </Group>
481
- </>
482
+ </Group>
482
483
  )
483
484
  })}
484
485
  </Group>
@@ -488,25 +489,48 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
488
489
 
489
490
  {Object.keys(config.confidenceKeys).length > 0
490
491
  ? data.map(d => {
491
- let xPos = xScale(getXAxisData(d))
492
- let upperPos = yScale(getYAxisData(d, config.confidenceKeys.lower))
493
- let lowerPos = yScale(getYAxisData(d, config.confidenceKeys.upper))
492
+ let xPos, yPos
493
+ let upperPos
494
+ let lowerPos
494
495
  let tickWidth = 5
495
-
496
- return (
497
- <path
498
- key={`confidence-interval-${d[config.runtime.originalXAxis.dataKey]}`}
499
- stroke='#333'
500
- strokeWidth='2px'
501
- d={`
502
- M${xPos - tickWidth} ${upperPos}
503
- L${xPos + tickWidth} ${upperPos}
504
- M${xPos} ${upperPos}
505
- L${xPos} ${lowerPos}
506
- M${xPos - tickWidth} ${lowerPos}
507
- L${xPos + tickWidth} ${lowerPos}`}
508
- />
509
- )
496
+ // DEV-3264 Make Confidence Intervals work on horizontal bar charts
497
+ if (orientation === 'horizontal') {
498
+ yPos = yScale(getXAxisData(d)) - 0.75 * config.barHeight
499
+ upperPos = xScale(getYAxisData(d, config.confidenceKeys.upper))
500
+ lowerPos = xScale(getYAxisData(d, config.confidenceKeys.lower))
501
+ return (
502
+ <path
503
+ key={`confidence-interval-h-${yPos}-${d[config.runtime.originalXAxis.dataKey]}`}
504
+ stroke='#333'
505
+ strokeWidth='px'
506
+ d={`
507
+ M${lowerPos} ${yPos - tickWidth}
508
+ L${lowerPos} ${yPos + tickWidth}
509
+ M${lowerPos} ${yPos}
510
+ L${upperPos} ${yPos}
511
+ M${upperPos} ${yPos - tickWidth}
512
+ L${upperPos} ${yPos + tickWidth} `}
513
+ />
514
+ )
515
+ } else {
516
+ xPos = xScale(getXAxisData(d))
517
+ upperPos = yScale(getYAxisData(d, config.confidenceKeys.lower))
518
+ lowerPos = yScale(getYAxisData(d, config.confidenceKeys.upper))
519
+ return (
520
+ <path
521
+ key={`confidence-interval-v-${yPos}-${d[config.runtime.originalXAxis.dataKey]}`}
522
+ stroke='#333'
523
+ strokeWidth='px'
524
+ d={`
525
+ M${xPos - tickWidth} ${upperPos}
526
+ L${xPos + tickWidth} ${upperPos}
527
+ M${xPos} ${upperPos}
528
+ L${xPos} ${lowerPos}
529
+ M${xPos - tickWidth} ${lowerPos}
530
+ L${xPos + tickWidth} ${lowerPos}`}
531
+ />
532
+ )
533
+ }
510
534
  })
511
535
  : ''}
512
536
  </Group>
@@ -1,14 +1,12 @@
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 { transformedData: data, config } = useContext(ConfigContext)
11
-
9
+ const { config, setConfig } = useContext(ConfigContext)
12
10
  const boxWidth = xScale.bandwidth()
13
11
  const constrainedWidth = Math.min(40, boxWidth)
14
12
  const color_0 = colorPalettesChart[config?.palette][0] ? colorPalettesChart[config?.palette][0] : '#000'
@@ -24,6 +22,19 @@ const CoveBoxPlot = ({ xScale, yScale }) => {
24
22
  ${config.boxplot.labels.median}: ${d.columnMedian}
25
23
  `
26
24
  }
25
+
26
+ useEffect(() => {
27
+ if (config.legend.hide === false) {
28
+ setConfig({
29
+ ...config,
30
+ legend: {
31
+ ...config.legend,
32
+ hide: true
33
+ }
34
+ })
35
+ }
36
+ }, []) // eslint-disable-line
37
+
27
38
  return (
28
39
  <ErrorBoundary component='BoxPlot'>
29
40
  <Group className='boxplot' key={`boxplot-group`}>
@@ -31,17 +42,20 @@ const CoveBoxPlot = ({ xScale, yScale }) => {
31
42
  const offset = boxWidth - constrainedWidth
32
43
  const radius = 4
33
44
  return (
34
- <>
35
- {config.boxplot.plotNonOutlierValues && d.nonOutlierValues.map(value => <circle cx={xScale(d.columnCategory) + config.yAxis.size + boxWidth / 2} cy={yScale(value)} r={radius} fill={'#ccc'} style={{ opacity: 1, fillOpacity: 1, stroke: 'black' }} />)}
45
+ <Group key={`boxplotplot-${i}`}>
46
+ {config.boxplot.plotNonOutlierValues &&
47
+ d.nonOutlierValues.map((value, index) => {
48
+ 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}`} />
49
+ })}
36
50
  <BoxPlot
37
51
  data-left={xScale(d.columnCategory) + config.yAxis.size + offset / 2 + 0.5}
38
52
  key={`box-plot-${i}`}
39
- min={d.columnMin}
40
- max={d.columnMax}
41
- left={xScale(d.columnCategory) + config.yAxis.size + offset / 2 + 0.5}
42
- firstQuartile={d.columnFirstQuartile}
43
- thirdQuartile={d.columnThirdQuartile}
44
- median={d.columnMedian}
53
+ min={Number(d.columnMin)}
54
+ max={Number(d.columnMax)}
55
+ left={Number(xScale(d.columnCategory)) + Number(config.yAxis.size) + offset / 2 + 0.5}
56
+ firstQuartile={Number(d.columnFirstQuartile)}
57
+ thirdQuartile={Number(d.columnThirdQuartile)}
58
+ median={Number(d.columnMedian)}
45
59
  boxWidth={constrainedWidth}
46
60
  fill={color_0}
47
61
  fillOpacity={0.5}
@@ -63,8 +77,7 @@ const CoveBoxPlot = ({ xScale, yScale }) => {
63
77
  style: {
64
78
  stroke: 'black',
65
79
  strokeWidth: config.boxplot.borders === 'true' ? 1 : 0
66
- },
67
- 'data-tooltip-html': 'cool'
80
+ }
68
81
  }}
69
82
  maxProps={{
70
83
  style: {
@@ -77,7 +90,7 @@ const CoveBoxPlot = ({ xScale, yScale }) => {
77
90
  'data-tooltip-id': tooltip_id
78
91
  }}
79
92
  />
80
- </>
93
+ </Group>
81
94
  )
82
95
  })}
83
96
  </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, transformedData: data, config, colorScale, parseDate, formatDate, formatNumber: numberFormatter, colorPalettes, imageId } = useContext(ConfigContext)
16
-
17
- // Debugging.
18
- // if (config.visualizationType === 'Box Plot') return null
15
+ const { rawData, transformedData: 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)
@@ -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,7 +73,7 @@ export default function DataTable() {
76
73
  columnThirdQuartile: labels.q3,
77
74
  columnOutliers: labels.outliers,
78
75
  values: labels.values,
79
- columnCount: labels.count,
76
+ columnTotal: labels.total,
80
77
  columnSd: 'Standard Deviation',
81
78
  nonOutlierValues: 'Non Outliers'
82
79
  }
@@ -90,7 +87,7 @@ export default function DataTable() {
90
87
  }
91
88
  }
92
89
  ]
93
- : [
90
+ : [
94
91
  {
95
92
  Header: '',
96
93
  Cell: ({ row }) => {
@@ -105,8 +102,8 @@ export default function DataTable() {
105
102
  ? colorScale(seriesLabel)
106
103
  : // dynamic legend
107
104
  config.legend.dynamicLegend
108
- ? colorPalettes[config.palette][row.index]
109
- : // fallback
105
+ ? colorPalettes[config.palette][row.index]
106
+ : // fallback
110
107
  '#000'
111
108
  }
112
109
  />
@@ -122,12 +119,13 @@ export default function DataTable() {
122
119
  data.forEach((d, index) => {
123
120
  const resolveTableHeader = () => {
124
121
  if (config.runtime[section].type === 'date') return formatDate(parseDate(d[config.runtime.originalXAxis.dataKey]))
122
+ if (config.runtime[section].type === 'continuous') return numberFormatter(d[config.runtime.originalXAxis.dataKey], 'bottom')
125
123
  return d[config.runtime.originalXAxis.dataKey]
126
124
  }
127
125
  const newCol = {
128
126
  Header: resolveTableHeader(),
129
127
  Cell: ({ row }) => {
130
- return <>{numberFormatter(d[row.original])}</>
128
+ return <>{numberFormatter(d[row.original], 'left')}</>
131
129
  },
132
130
  id: `${d[config.runtime.originalXAxis.dataKey]}--${index}`,
133
131
  canSort: true
@@ -149,7 +147,7 @@ export default function DataTable() {
149
147
  if (Number(props.row.id) === 3) return plot.columnMedian
150
148
  if (Number(props.row.id) === 4) return plot.columnFirstQuartile
151
149
  if (Number(props.row.id) === 5) return plot.columnMin
152
- if (Number(props.row.id) === 6) return plot.columnCount
150
+ if (Number(props.row.id) === 6) return plot.columnTotal
153
151
  if (Number(props.row.id) === 7) return plot.columnSd
154
152
  if (Number(props.row.id) === 8) return plot.columnMean
155
153
  if (Number(props.row.id) === 9) return plot.columnOutliers.length > 0 ? plot.columnOutliers.toString() : '-'
@@ -162,12 +160,12 @@ export default function DataTable() {
162
160
  canSort: false
163
161
  }
164
162
 
165
- newTableColumns.push(newCol)
163
+ return newTableColumns.push(newCol)
166
164
  })
167
165
  }
168
166
 
169
167
  return newTableColumns
170
- }, [config, colorScale])
168
+ }, [config, colorScale]) // eslint-disable-line
171
169
 
172
170
  // prettier-ignore
173
171
  const tableData = useMemo(() => (
@@ -176,7 +174,7 @@ export default function DataTable() {
176
174
  : config.visualizationType === 'Box Plot'
177
175
  ? Object.entries(config.boxplot.tableData[0])
178
176
  : config.runtime.seriesKeys),
179
- [config.runtime.seriesKeys])
177
+ [config.runtime.seriesKeys]) // eslint-disable-line
180
178
 
181
179
  // Change accessibility label depending on expanded status
182
180
  useEffect(() => {
@@ -201,8 +199,13 @@ export default function DataTable() {
201
199
  }),
202
200
  []
203
201
  )
204
-
205
202
  const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable({ columns: tableColumns, data: tableData, defaultColumn }, useSortBy, useBlockLayout, useResizeColumns)
203
+
204
+ // sort continuous x axis scaling for data tables, ie. xAxis should read 1,2,3,4,5
205
+ if (config.xAxis.type === 'continuous' && headerGroups) {
206
+ data.sort((a, b) => a[config.xAxis.dataKey] - b[config.xAxis.dataKey])
207
+ }
208
+
206
209
  return (
207
210
  <ErrorBoundary component='DataTable'>
208
211
  <CoveMediaControls.Section classes={['download-links']}>
@@ -224,7 +227,7 @@ export default function DataTable() {
224
227
  }
225
228
  }}
226
229
  >
227
- <Icon display={tableExpanded ? 'minus' : 'plus'} base/>
230
+ <Icon display={tableExpanded ? 'minus' : 'plus'} base />
228
231
  {config.table.label}
229
232
  </div>
230
233
  <div className='table-container' hidden={!tableExpanded} style={{ maxHeight: config.table.limitHeight && `${config.table.height}px`, overflowY: 'scroll' }}>
@@ -272,7 +275,7 @@ export default function DataTable() {
272
275
  })}
273
276
  </tbody>
274
277
  </table>
275
- {config.regions && config.regions.length > 0 ? (
278
+ {config.regions && config.regions.length > 0 && !config.visualizationType === 'Box Plot' ? (
276
279
  <table className='region-table data-table'>
277
280
  <caption className='visually-hidden'>Table of the highlighted regions in the visualization</caption>
278
281
  <thead>
@@ -284,6 +287,7 @@ export default function DataTable() {
284
287
  </thead>
285
288
  <tbody>
286
289
  {config.regions.map((region, index) => {
290
+ if (config.visualizationType === 'Box Plot') return false
287
291
  if (!Object.keys(region).includes('from') || !Object.keys(region).includes('to')) return null
288
292
 
289
293
  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 * 1.05)))
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) || 15
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
+ }