@cdc/chart 4.23.1 → 4.23.2

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 (52) hide show
  1. package/dist/cdcchart.js +54532 -696
  2. package/examples/Barchart_with_negative.json +34 -0
  3. package/examples/box-plot.json +2 -2
  4. package/examples/dynamic-legends.json +1 -1
  5. package/examples/example-bar-chart-nonnumeric.json +36 -0
  6. package/examples/example-bar-chart.json +33 -0
  7. package/examples/example-combo-bar-nonnumeric.json +105 -0
  8. package/examples/gallery/bar-chart-vertical/vertical-bar-chart-categorical.json +1 -1
  9. package/examples/gallery/bar-chart-vertical/vertical-bar-chart.json +2 -2
  10. package/examples/gallery/paired-bar/paired-bar-chart.json +65 -13
  11. package/examples/line-chart-nonnumeric.json +32 -0
  12. package/examples/line-chart.json +21 -63
  13. package/examples/newdata.json +1 -1
  14. package/examples/planet-combo-example-config.json +143 -20
  15. package/examples/planet-example-data-nonnumeric.json +56 -0
  16. package/examples/planet-example-data.json +2 -2
  17. package/examples/planet-pie-example-config-nonnumeric.json +30 -0
  18. package/examples/scatterplot-continuous.csv +17 -0
  19. package/examples/scatterplot.json +136 -0
  20. package/examples/sparkline-chart-nonnumeric.json +76 -0
  21. package/examples/stacked-vertical-bar-example-negative.json +154 -0
  22. package/examples/stacked-vertical-bar-example-nonnumerics.json +154 -0
  23. package/index.html +74 -0
  24. package/package.json +29 -23
  25. package/src/{CdcChart.tsx → CdcChart.jsx} +74 -56
  26. package/src/components/{BarChart.tsx → BarChart.jsx} +99 -91
  27. package/src/components/BoxPlot.jsx +88 -0
  28. package/src/components/{DataTable.tsx → DataTable.jsx} +102 -25
  29. package/src/components/{EditorPanel.js → EditorPanel.jsx} +228 -14
  30. package/src/components/{Filters.js → Filters.jsx} +6 -12
  31. package/src/components/{Legend.js → Legend.jsx} +120 -108
  32. package/src/components/{LineChart.tsx → LineChart.jsx} +26 -12
  33. package/src/components/{LinearChart.tsx → LinearChart.jsx} +67 -47
  34. package/src/components/{PairedBarChart.tsx → PairedBarChart.jsx} +45 -78
  35. package/src/components/{PieChart.tsx → PieChart.jsx} +17 -32
  36. package/src/components/ScatterPlot.jsx +48 -0
  37. package/src/components/{SparkLine.js → SparkLine.jsx} +49 -18
  38. package/src/components/{useIntersectionObserver.tsx → useIntersectionObserver.jsx} +1 -1
  39. package/src/data/initial-state.js +33 -3
  40. package/src/hooks/{useColorPalette.ts → useColorPalette.js} +10 -28
  41. package/src/hooks/{useReduceData.ts → useReduceData.js} +25 -14
  42. package/src/hooks/useRightAxis.js +3 -1
  43. package/src/index.jsx +16 -0
  44. package/src/scss/DataTable.scss +22 -0
  45. package/src/scss/main.scss +30 -10
  46. package/vite.config.js +4 -0
  47. package/dist/495.js +0 -3
  48. package/dist/703.js +0 -1
  49. package/src/components/BoxPlot.js +0 -92
  50. package/src/index.html +0 -67
  51. package/src/index.tsx +0 -18
  52. /package/src/{context.tsx → ConfigContext.jsx} +0 -0
@@ -1,5 +1,5 @@
1
1
  import React, { useContext, useEffect } from 'react'
2
- import Context from '../context'
2
+ import ConfigContext from '../ConfigContext'
3
3
  import parse from 'html-react-parser'
4
4
  import { LegendOrdinal, LegendItem, LegendLabel } from '@visx/legend'
5
5
  import LegendCircle from '@cdc/core/components/LegendCircle'
@@ -7,7 +7,7 @@ import LegendCircle from '@cdc/core/components/LegendCircle'
7
7
  import useLegendClasses from './../hooks/useLegendClasses'
8
8
 
9
9
  const Legend = () => {
10
- const { config, legend, colorScale, seriesHighlight, highlight, highlightReset, setSeriesHighlight, dynamicLegendItems, setDynamicLegendItems, transformedData: data, setFilteredData, colorPalettes, rawData, setConfig } = useContext(Context)
10
+ const { config, legend, colorScale, seriesHighlight, highlight, highlightReset, setSeriesHighlight, dynamicLegendItems, setDynamicLegendItems, transformedData: data, colorPalettes, rawData, setConfig, currentViewport } = useContext(ConfigContext)
11
11
 
12
12
  const { innerClasses, containerClasses } = useLegendClasses(config)
13
13
 
@@ -106,11 +106,18 @@ const Legend = () => {
106
106
 
107
107
  return uniqeLabels
108
108
  }
109
+ // in small screens update config legend position.
110
+ useEffect(() => {
111
+ if (currentViewport === 'sm' || currentViewport === 'xs' || config.legend.position === 'left') {
112
+ setConfig({ ...config, legend: { ...config.legend, position: 'bottom' } })
113
+ }
114
+ setConfig({ ...config, legend: { ...config.legend, position: 'right' } })
115
+ }, [currentViewport])
109
116
 
110
117
  if (!legend) return
111
118
 
112
119
  if (!legend.dynamicLegend)
113
- return (
120
+ return config.visualizationType !== 'Box Plot' ? (
114
121
  <aside
115
122
  style={{ marginTop: config.legend.position === 'bottom' && config.orientation === 'horizontal' ? `${config.runtime.xAxis.size}px` : '0px', marginBottom: config.legend.position === 'bottom' ? '15px' : '0px' }}
116
123
  id='legend'
@@ -172,119 +179,124 @@ const Legend = () => {
172
179
  )}
173
180
  </LegendOrdinal>
174
181
  </aside>
182
+ ) : (
183
+ <aside id='legend' className={containerClasses.join(' ')} role='region' aria-label='legend' tabIndex={0}>
184
+ {config.boxplot.legend.displayHowToReadText && <h3>{config.boxplot.legend.howToReadText}</h3>}
185
+ </aside>
175
186
  )
176
-
177
187
  return (
178
- <aside id='legend' className={containerClasses.join(' ')} role='region' aria-label='legend' tabIndex={0}>
179
- {legend.label && <h2>{parse(legend.label)}</h2>}
180
- {legend.description && <p>{parse(legend.description)}</p>}
181
-
182
- <LegendOrdinal scale={colorScale} itemDirection='row' labelMargin='0 20px 0 0' shapeMargin='0 10px 0'>
183
- {labels => {
184
- if (
185
- Number(config.legend.dynamicLegendItemLimit) > dynamicLegendItems.length && // legend items are less than limit
186
- dynamicLegendItems.length !== config.runtime.seriesLabelsAll.length
187
- ) {
188
- // legend items are equal to series length
189
- return (
190
- <select className='dynamic-legend-dropdown' onChange={e => handleDynamicLegendChange(e)}>
191
- <option className={'all'} tabIndex={0} value={JSON.stringify({ text: config.legend.dynamicLegendDefaultText })}>
192
- {config.legend.dynamicLegendDefaultText}
193
- </option>
194
- {labels.map((label, i) => {
195
- let className = 'legend-item'
196
- let itemName = label.datum
197
- let inDynamicList = false
198
-
199
- // Filter excluded data keys from legend
200
- if (config.exclusions.active && config.exclusions.keys?.includes(itemName)) {
201
- return
202
- }
203
-
204
- if (config.runtime.seriesLabels) {
205
- let index = config.runtime.seriesLabelsAll.indexOf(itemName)
206
- itemName = config.runtime.seriesKeys[index]
207
- }
208
-
209
- if (seriesHighlight.length > 0 && false === seriesHighlight.includes(itemName)) {
210
- className += ' inactive'
211
- }
212
-
213
- dynamicLegendItems.map(listItem => {
214
- if (listItem.text === label.text) {
215
- inDynamicList = true
188
+ config.visualizationType !== 'Box Plot' && (
189
+ <aside id='legend' className={containerClasses.join(' ')} role='region' aria-label='legend' tabIndex={0}>
190
+ {legend.label && <h2>{parse(legend.label)}</h2>}
191
+ {legend.description && <p>{parse(legend.description)}</p>}
192
+
193
+ <LegendOrdinal scale={colorScale} itemDirection='row' labelMargin='0 20px 0 0' shapeMargin='0 10px 0'>
194
+ {labels => {
195
+ if (
196
+ Number(config.legend.dynamicLegendItemLimit) > dynamicLegendItems.length && // legend items are less than limit
197
+ dynamicLegendItems.length !== config.runtime.seriesLabelsAll.length
198
+ ) {
199
+ // legend items are equal to series length
200
+ return (
201
+ <select className='dynamic-legend-dropdown' onChange={e => handleDynamicLegendChange(e)}>
202
+ <option className={'all'} tabIndex={0} value={JSON.stringify({ text: config.legend.dynamicLegendDefaultText })}>
203
+ {config.legend.dynamicLegendDefaultText}
204
+ </option>
205
+ {labels.map((label, i) => {
206
+ let className = 'legend-item'
207
+ let itemName = label.datum
208
+ let inDynamicList = false
209
+
210
+ // Filter excluded data keys from legend
211
+ if (config.exclusions.active && config.exclusions.keys?.includes(itemName)) {
212
+ return
213
+ }
214
+
215
+ if (config.runtime.seriesLabels) {
216
+ let index = config.runtime.seriesLabelsAll.indexOf(itemName)
217
+ itemName = config.runtime.seriesKeys[index]
216
218
  }
217
- })
218
219
 
219
- if (inDynamicList) return true
220
- let palette = colorPalettes[config.palette]
220
+ if (seriesHighlight.length > 0 && false === seriesHighlight.includes(itemName)) {
221
+ className += ' inactive'
222
+ }
223
+
224
+ dynamicLegendItems.map(listItem => {
225
+ if (listItem.text === label.text) {
226
+ inDynamicList = true
227
+ }
228
+ })
229
+
230
+ if (inDynamicList) return true
231
+ let palette = colorPalettes[config.palette]
232
+
233
+ label.value = palette[dynamicLegendItems.length]
234
+
235
+ return (
236
+ <option className={className} tabIndex={0} value={JSON.stringify(label)}>
237
+ {label.text}
238
+ </option>
239
+ )
240
+ })}
241
+ </select>
242
+ )
243
+ } else {
244
+ return config.legend.dynamicLegendItemLimitMessage
245
+ }
246
+ }}
247
+ </LegendOrdinal>
248
+
249
+ <div className='dynamic-legend-list'>
250
+ {dynamicLegendItems.map((label, i) => {
251
+ let className = ['legend-item']
252
+ let itemName = label.text
253
+ let palette = colorPalettes[config.palette]
254
+
255
+ // Filter excluded data keys from legend
256
+ if (config.exclusions.active && config.exclusions.keys?.includes(itemName)) {
257
+ return
258
+ }
259
+
260
+ if (config.runtime.seriesLabels && !config.legend.dynamicLegend) {
261
+ let index = config.runtime.seriesLabelsAll.indexOf(itemName)
262
+ itemName = config.runtime.seriesKeys[index]
263
+ }
221
264
 
222
- label.value = palette[dynamicLegendItems.length]
265
+ if (seriesHighlight.length > 0 && !seriesHighlight.includes(itemName)) {
266
+ className.push('inactive')
267
+ }
223
268
 
224
- return (
225
- <option className={className} tabIndex={0} value={JSON.stringify(label)}>
269
+ if (seriesHighlight.length === 0 && config.legend.dynamicLegend) {
270
+ className.push('inactive')
271
+ }
272
+
273
+ return (
274
+ <>
275
+ <LegendItem className={className.join(' ')} tabIndex={0} key={`dynamic-legend-item-${i}`} alignItems='center'>
276
+ <button
277
+ className='btn-wrapper'
278
+ onClick={() => {
279
+ highlight(label)
280
+ }}
281
+ >
282
+ <LegendCircle fill={palette[i]} config={config} />
283
+ <LegendLabel align='space-between' margin='4px 0 0 4px'>
226
284
  {label.text}
227
- </option>
228
- )
229
- })}
230
- </select>
285
+ </LegendLabel>
286
+ </button>
287
+ <button onClick={() => removeDynamicLegendItem(label)}>x</button>
288
+ </LegendItem>
289
+ </>
231
290
  )
232
- } else {
233
- return config.legend.dynamicLegendItemLimitMessage
234
- }
235
- }}
236
- </LegendOrdinal>
237
-
238
- <div className='dynamic-legend-list'>
239
- {dynamicLegendItems.map((label, i) => {
240
- let className = ['legend-item']
241
- let itemName = label.text
242
- let palette = colorPalettes[config.palette]
243
-
244
- // Filter excluded data keys from legend
245
- if (config.exclusions.active && config.exclusions.keys?.includes(itemName)) {
246
- return
247
- }
248
-
249
- if (config.runtime.seriesLabels && !config.legend.dynamicLegend) {
250
- let index = config.runtime.seriesLabelsAll.indexOf(itemName)
251
- itemName = config.runtime.seriesKeys[index]
252
- }
253
-
254
- if (seriesHighlight.length > 0 && !seriesHighlight.includes(itemName)) {
255
- className.push('inactive')
256
- }
257
-
258
- if (seriesHighlight.length === 0 && config.legend.dynamicLegend) {
259
- className.push('inactive')
260
- }
261
-
262
- return (
263
- <>
264
- <LegendItem className={className.join(' ')} tabIndex={0} key={`dynamic-legend-item-${i}`} alignItems='center'>
265
- <button
266
- className='btn-wrapper'
267
- onClick={() => {
268
- highlight(label)
269
- }}
270
- >
271
- <LegendCircle fill={palette[i]} config={config} />
272
- <LegendLabel align='space-between' margin='4px 0 0 4px'>
273
- {label.text}
274
- </LegendLabel>
275
- </button>
276
- <button onClick={() => removeDynamicLegendItem(label)}>x</button>
277
- </LegendItem>
278
- </>
279
- )
280
- })}
281
- </div>
282
- {seriesHighlight.length < dynamicLegendItems.length && (
283
- <button className={`legend-reset legend-reset--dynamic ${config.theme}`} onClick={highlightReset} tabIndex={0}>
284
- Reset
285
- </button>
286
- )}
287
- </aside>
291
+ })}
292
+ </div>
293
+ {seriesHighlight.length < dynamicLegendItems.length && (
294
+ <button className={`legend-reset legend-reset--dynamic ${config.theme}`} onClick={highlightReset} tabIndex={0}>
295
+ Reset
296
+ </button>
297
+ )}
298
+ </aside>
299
+ )
288
300
  )
289
301
  }
290
302
 
@@ -7,12 +7,15 @@ import { Text } from '@visx/text'
7
7
 
8
8
  import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
9
9
 
10
- import Context from '../context'
10
+ import ConfigContext from '../ConfigContext'
11
11
 
12
12
  import useRightAxis from '../hooks/useRightAxis'
13
13
 
14
14
  export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData, xMax, yMax, seriesStyle = 'Line' }) {
15
- const { colorPalettes, transformedData: data, colorScale, seriesHighlight, config, formatNumber, formatDate, parseDate, updateConfig } = useContext<any>(Context)
15
+ const { colorPalettes, transformedData: data, colorScale, seriesHighlight, config, formatNumber, formatDate, parseDate, isNumber, cleanData, updateConfig } = useContext(ConfigContext)
16
+ // Just do this once up front otherwise we end up
17
+ // calling clean several times on same set of data (TT)
18
+ const cleanedData = cleanData(data, config.xAxis.dataKey)
16
19
  const { yScaleRight } = useRightAxis({ config, yMax, data, updateConfig })
17
20
 
18
21
  const handleLineType = lineType => {
@@ -29,6 +32,9 @@ export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData,
29
32
  }
30
33
 
31
34
  const handleAxisFormating = (axis = 'left', label, value) => {
35
+ // if this is an x axis category/date value return without doing any formatting.
36
+ if (label === config.runtime.xAxis.label) return value
37
+
32
38
  axis = String(axis).toLocaleLowerCase()
33
39
  if (label) {
34
40
  return `${label}: ${formatNumber(value, axis)}`
@@ -38,7 +44,9 @@ export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData,
38
44
 
39
45
  return (
40
46
  <ErrorBoundary component='LineChart'>
41
- <Group left={config.runtime.yAxis.size}>
47
+ <Group left={config.runtime.yAxis.size ? parseInt(config.runtime.yAxis.size) : 66}>
48
+ {' '}
49
+ {/* left - expects a number not a string */}
42
50
  {(config.runtime.lineSeriesKeys || config.runtime.seriesKeys).map((seriesKey, index) => {
43
51
  let lineType = config.series.filter(item => item.dataKey === seriesKey)[0].type
44
52
  const seriesData = config.series.filter(item => item.dataKey === seriesKey)
@@ -50,7 +58,7 @@ export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData,
50
58
  opacity={config.legend.behavior === 'highlight' && seriesHighlight.length > 0 && seriesHighlight.indexOf(seriesKey) === -1 ? 0.5 : 1}
51
59
  display={config.legend.behavior === 'highlight' || (seriesHighlight.length === 0 && !config.legend.dynamicLegend) || seriesHighlight.indexOf(seriesKey) !== -1 ? 'block' : 'none'}
52
60
  >
53
- {data.map((d, dataIndex) => {
61
+ {cleanedData.map((d, dataIndex) => {
54
62
  // Find the series object from the config.series array that has a dataKey matching the seriesKey variable.
55
63
  const series = config.series.find(({ dataKey }) => dataKey === seriesKey)
56
64
  const { axis } = series
@@ -73,11 +81,18 @@ export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData,
73
81
  ${xAxisTooltip}
74
82
  </div>`
75
83
  let circleRadii = 4.5
84
+
76
85
  return (
77
86
  d[seriesKey] !== undefined &&
78
87
  d[seriesKey] !== '' &&
79
88
  d[seriesKey] !== null && (
89
+ // isNumber(d[seriesKey]) &&
90
+ // isNumber(getYAxisData(d, seriesKey)) &&
91
+ // isNumber(getXAxisData(d)) &&
92
+ // isNumber(yScaleRight(getXAxisData(d))) &&
93
+ // isNumber(yScale(getXAxisData(d))) &&
80
94
  <Group key={`series-${seriesKey}-point-${dataIndex}`}>
95
+ {/* Render legend */}
81
96
  <Text
82
97
  display={config.labels ? 'block' : 'none'}
83
98
  x={xScale(getXAxisData(d))}
@@ -91,12 +106,12 @@ export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData,
91
106
  <circle
92
107
  key={`${seriesKey}-${dataIndex}`}
93
108
  r={circleRadii}
94
- cx={xScale(getXAxisData(d))}
109
+ cx={Number(xScale(getXAxisData(d)))}
95
110
  cy={seriesAxis === 'Right' ? yScaleRight(getYAxisData(d, seriesKey)) : yScale(getYAxisData(d, seriesKey))}
96
111
  fill={colorScale ? colorScale(config.runtime.seriesLabels ? config.runtime.seriesLabels[seriesKey] : seriesKey) : '#000'}
97
112
  style={{ fill: colorScale ? colorScale(config.runtime.seriesLabels ? config.runtime.seriesLabels[seriesKey] : seriesKey) : '#000' }}
98
- data-tip={tooltip}
99
- data-for={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
113
+ data-tooltip-html={tooltip}
114
+ data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
100
115
  />
101
116
  </Group>
102
117
  )
@@ -105,7 +120,7 @@ export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData,
105
120
 
106
121
  <LinePath
107
122
  curve={allCurves.curveLinear}
108
- data={data}
123
+ data={cleanedData}
109
124
  x={d => xScale(getXAxisData(d))}
110
125
  y={d => (seriesAxis === 'Right' ? yScaleRight(getYAxisData(d, seriesKey)) : yScale(getYAxisData(d, seriesKey)))}
111
126
  stroke={
@@ -122,14 +137,14 @@ export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData,
122
137
  shapeRendering='geometricPrecision'
123
138
  strokeDasharray={lineType ? handleLineType(lineType) : 0}
124
139
  defined={(item, i) => {
125
- return item[config.runtime.seriesLabels[seriesKey]] !== '' && item[config.runtime.seriesLabels[seriesKey]] !== null
140
+ return item[config.runtime.seriesLabels[seriesKey]] !== '' && item[config.runtime.seriesLabels[seriesKey]] !== null && item[config.runtime.seriesLabels[seriesKey]] !== undefined
126
141
  }}
127
142
  />
128
143
  {config.animate && (
129
144
  <LinePath
130
145
  className='animation'
131
146
  curve={allCurves.curveLinear}
132
- data={data}
147
+ data={cleanedData}
133
148
  x={d => xScale(getXAxisData(d))}
134
149
  y={d => (seriesAxis === 'Right' ? yScaleRight(getYAxisData(d, seriesKey)) : yScale(getYAxisData(d, seriesKey)))}
135
150
  stroke='#fff'
@@ -138,7 +153,7 @@ export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData,
138
153
  shapeRendering='geometricPrecision'
139
154
  strokeDasharray={lineType ? handleLineType(lineType) : 0}
140
155
  defined={(item, i) => {
141
- return item[config.runtime.seriesLabels[seriesKey]] !== '' && item[config.runtime.seriesLabels[seriesKey]] !== null
156
+ return isNumber(item[config.runtime.seriesLabels[seriesKey]])
142
157
  }}
143
158
  />
144
159
  )}
@@ -165,7 +180,6 @@ export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData,
165
180
  </Group>
166
181
  )
167
182
  })}
168
-
169
183
  {/* Message when dynamic legend and nothing has been picked */}
170
184
  {config.legend.dynamicLegend && seriesHighlight.length === 0 && (
171
185
  <Text x={xMax / 2} y={yMax / 2} fill='black' textAnchor='middle' color='black'>