@cdc/chart 1.3.4 → 4.22.10

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 (50) hide show
  1. package/LICENSE +201 -0
  2. package/dist/cdcchart.js +6 -6
  3. package/examples/cutoff-example-config.json +2 -0
  4. package/examples/cutoff-example-data.json +1 -1
  5. package/examples/dynamic-legends.json +125 -0
  6. package/examples/gallery/bar-chart-horizontal/horizontal-bar-chart-with-numbers-on-bar.json +198 -0
  7. package/examples/gallery/bar-chart-horizontal/horizontal-bar-chart.json +241 -0
  8. package/examples/gallery/bar-chart-horizontal/horizontal-stacked.json +248 -0
  9. package/examples/gallery/bar-chart-vertical/combo-line-chart.json +137 -0
  10. package/examples/gallery/bar-chart-vertical/vertical-bar-chart-categorical.json +80 -0
  11. package/examples/gallery/bar-chart-vertical/vertical-bar-chart-stacked.json +81 -0
  12. package/examples/gallery/bar-chart-vertical/vertical-bar-chart-with-confidence.json +68 -0
  13. package/examples/gallery/bar-chart-vertical/vertical-bar-chart.json +111 -0
  14. package/examples/gallery/lollipop/lollipop-style-horizontal.json +220 -0
  15. package/examples/gallery/paired-bar/paired-bar-chart.json +196 -0
  16. package/examples/horizontal-chart.json +3 -0
  17. package/examples/paired-bar-data.json +1 -1
  18. package/examples/paired-bar-example.json +2 -0
  19. package/examples/planet-combo-example-config.json +2 -0
  20. package/examples/planet-example-config.json +2 -2
  21. package/examples/planet-example-data.json +1 -1
  22. package/examples/planet-pie-example-config.json +2 -0
  23. package/examples/private/line-test-data.json +22 -0
  24. package/examples/private/line-test-two.json +216 -0
  25. package/examples/private/line-test.json +102 -0
  26. package/examples/private/shawn.json +1296 -0
  27. package/examples/private/yaxis-test.json +132 -0
  28. package/examples/private/yaxis-testing.csv +27 -0
  29. package/examples/private/yaxis.json +28 -0
  30. package/examples/stacked-vertical-bar-example.json +228 -0
  31. package/package.json +2 -2
  32. package/src/CdcChart.tsx +121 -168
  33. package/src/components/BarChart.tsx +92 -40
  34. package/src/components/DataTable.tsx +28 -13
  35. package/src/components/EditorPanel.js +286 -182
  36. package/src/components/Legend.js +334 -0
  37. package/src/components/LineChart.tsx +57 -17
  38. package/src/components/LinearChart.tsx +171 -77
  39. package/src/components/PairedBarChart.tsx +139 -42
  40. package/src/components/PieChart.tsx +33 -6
  41. package/src/components/SparkLine.js +28 -27
  42. package/src/components/useIntersectionObserver.tsx +30 -0
  43. package/src/data/initial-state.js +23 -7
  44. package/src/hooks/useChartClasses.js +35 -0
  45. package/src/hooks/useLegendClasses.js +20 -0
  46. package/src/hooks/useReduceData.ts +72 -24
  47. package/src/index.html +29 -30
  48. package/src/scss/editor-panel.scss +34 -4
  49. package/src/scss/main.scss +201 -5
  50. package/src/components/BarStackVertical.js +0 -0
@@ -0,0 +1,334 @@
1
+ import React, { useContext, useEffect } from 'react';
2
+ import Context from '../context';
3
+ import parse from 'html-react-parser';
4
+ import { LegendOrdinal, LegendItem, LegendLabel } from '@visx/legend';
5
+ import LegendCircle from '@cdc/core/components/LegendCircle';
6
+
7
+ import useLegendClasses from './../hooks/useLegendClasses';
8
+
9
+
10
+ const Legend = () => {
11
+
12
+ const {
13
+ config,
14
+ legend,
15
+ colorScale,
16
+ seriesHighlight,
17
+ highlight,
18
+ highlightReset,
19
+ setSeriesHighlight,
20
+ dynamicLegendItems,
21
+ setDynamicLegendItems,
22
+ transformedData: data,
23
+ setFilteredData,
24
+ colorPalettes,
25
+ rawData,
26
+ setConfig
27
+ } = useContext(Context);
28
+
29
+ const {innerClasses, containerClasses} = useLegendClasses(config)
30
+
31
+ useEffect(() => {
32
+ if(dynamicLegendItems.length === 0) return;
33
+
34
+ let itemsToHighlight = dynamicLegendItems.map( item => item.text);
35
+
36
+ setSeriesHighlight( itemsToHighlight )
37
+
38
+ let colsToKeep = [...itemsToHighlight]
39
+ let tmpLabels = [];
40
+
41
+ rawData.map( dataItem => {
42
+ let tmp = {}
43
+ colsToKeep.map( col => {
44
+ tmp[col] = isNaN(dataItem[col]) ? dataItem[col] : dataItem[col]
45
+ })
46
+ return tmp
47
+ })
48
+
49
+ colsToKeep.map( col => {
50
+ tmpLabels[col] = col
51
+ })
52
+
53
+ if(dynamicLegendItems.length > 0) {
54
+ setConfig({
55
+ ...config,
56
+ runtime: {
57
+ ...config.runtime,
58
+ seriesKeys: colsToKeep,
59
+ seriesLabels: tmpLabels
60
+ }
61
+ })
62
+ }
63
+
64
+
65
+
66
+ }, [dynamicLegendItems]);
67
+
68
+
69
+ useEffect(() => {
70
+ if(dynamicLegendItems.length === 0 ) {
71
+
72
+ // loop through all labels and add keys
73
+ let resetSeriesNames = [...config.runtime.seriesLabelsAll]
74
+ let tmpLabels = [];
75
+ config.runtime.seriesLabelsAll.map( item => {
76
+ resetSeriesNames.map( col => {
77
+ tmpLabels[col] = col
78
+ })
79
+ })
80
+
81
+ setConfig({
82
+ ...config,
83
+ runtime: {
84
+ ...config.runtime,
85
+ seriesKeys: config.runtime.seriesLabelsAll,
86
+ seriesLabels: tmpLabels
87
+ }
88
+ })
89
+
90
+ }
91
+ }, [dynamicLegendItems]);
92
+
93
+ const removeDynamicLegendItem = (label) => {
94
+ let newLegendItems = dynamicLegendItems.filter((item) => item.text !== label.text);
95
+ let newLegendItemsText = newLegendItems.map(item => item.text )
96
+ setDynamicLegendItems( newLegendItems )
97
+ setSeriesHighlight( newLegendItemsText)
98
+ }
99
+ const handleDynamicLegendChange = (e) => {
100
+ setDynamicLegendItems([...dynamicLegendItems, JSON.parse(e.target.value)])
101
+ }
102
+
103
+ const createLegendLabels = (data,defaultLabels) => {
104
+ const colorCode = config.legend?.colorCode;
105
+ if( config.visualizationType !=='Bar' || config.visualizationSubType !=="regular" || !colorCode || config.series?.length > 1){
106
+ return defaultLabels;
107
+ };
108
+ let palette = colorPalettes[config.palette];
109
+
110
+ while(data.length > palette.length) {
111
+ palette = palette.concat(palette);
112
+ }
113
+ palette = palette.slice(0, data.length);
114
+ //store uniq values to Set by colorCode
115
+ const set = new Set();
116
+
117
+ data.forEach(d=>set.add(d[colorCode]));
118
+
119
+ // create labels with uniq values
120
+ const uniqeLabels = Array.from(set).map((val,i)=>{
121
+ const newLabel = {
122
+ datum :val,
123
+ index:i,
124
+ text:val,
125
+ value:palette[i]
126
+ };
127
+ return newLabel;
128
+ });
129
+
130
+ return uniqeLabels;
131
+ };
132
+
133
+
134
+
135
+ if (!legend) return;
136
+
137
+ if (!legend.dynamicLegend) return (
138
+ <aside id="legend" className={containerClasses.join(' ')} role="region" aria-label="legend" tabIndex={0}>
139
+ {legend.label && <h2>{parse(legend.label)}</h2>}
140
+ {legend.description && <p>{parse(legend.description)}</p>}
141
+ <LegendOrdinal
142
+ scale={colorScale}
143
+ itemDirection="row"
144
+ labelMargin="0 20px 0 0"
145
+ shapeMargin="0 10px 0"
146
+ >
147
+
148
+ {labels => (
149
+ <div className={innerClasses.join(' ')}>
150
+ {createLegendLabels(data,labels).map((label, i) => {
151
+
152
+ let className = 'legend-item'
153
+ let itemName = label.datum
154
+
155
+ // Filter excluded data keys from legend
156
+ if (config.exclusions.active && config.exclusions.keys?.includes(itemName)) {
157
+ return
158
+ }
159
+
160
+ if (config.runtime.seriesLabels) {
161
+ let index = config.runtime.seriesLabelsAll.indexOf(itemName)
162
+ itemName = config.runtime.seriesKeys[index]
163
+ }
164
+
165
+ if (seriesHighlight.length > 0 && false === seriesHighlight.includes(itemName)) {
166
+ className += ' inactive'
167
+ }
168
+
169
+
170
+ return (
171
+ <LegendItem
172
+ className={className}
173
+ tabIndex={0}
174
+ key={`legend-quantile-${i}`}
175
+ onKeyPress={(e) => {
176
+ if (e.key === 'Enter') {
177
+ highlight(label);
178
+ }
179
+ }}
180
+ onClick={() => {
181
+ highlight(label);
182
+ }}
183
+ >
184
+ <LegendCircle fill={label.value} />
185
+ <LegendLabel align="left" margin="0 0 0 4px">
186
+ {label.text}
187
+ </LegendLabel>
188
+ </LegendItem>
189
+ )
190
+ })}
191
+ {seriesHighlight.length > 0 && <button className={`legend-reset ${config.theme}`} onClick={ (labels) => highlightReset(labels) } tabIndex={0}>Reset</button>}
192
+ </div>
193
+ )}
194
+ </LegendOrdinal>
195
+ </aside>
196
+ )
197
+
198
+ return (
199
+ <aside id="legend" className={containerClasses.join(' ')} role="region" aria-label="legend" tabIndex={0}>
200
+ {legend.label && <h2>{parse(legend.label)}</h2>}
201
+ {legend.description && <p>{parse(legend.description)}</p>}
202
+
203
+ <LegendOrdinal
204
+ scale={colorScale}
205
+ itemDirection="row"
206
+ labelMargin="0 20px 0 0"
207
+ shapeMargin="0 10px 0"
208
+ >
209
+
210
+ {labels => {
211
+ if (
212
+ (Number(config.legend.dynamicLegendItemLimit) > dynamicLegendItems.length) // legend items are less than limit
213
+ && (dynamicLegendItems.length !== config.runtime.seriesLabelsAll.length) ) // legend items are equal to series length
214
+ {
215
+ return (
216
+ <select
217
+ className='dynamic-legend-dropdown'
218
+ onChange={(e) => handleDynamicLegendChange(e) }
219
+ >
220
+ <option
221
+ className={'all'}
222
+ tabIndex={0}
223
+ value={JSON.stringify({ text: config.legend.dynamicLegendDefaultText })}
224
+
225
+ >
226
+ {config.legend.dynamicLegendDefaultText}
227
+ </option>
228
+ {labels.map((label, i) => {
229
+ let className = 'legend-item'
230
+ let itemName = label.datum
231
+ let inDynamicList = false;
232
+
233
+ // Filter excluded data keys from legend
234
+ if (config.exclusions.active && config.exclusions.keys?.includes(itemName)) {
235
+ return
236
+ }
237
+
238
+ if (config.runtime.seriesLabels) {
239
+ let index = config.runtime.seriesLabelsAll.indexOf(itemName)
240
+ itemName = config.runtime.seriesKeys[index]
241
+ }
242
+
243
+ if (seriesHighlight.length > 0 && false === seriesHighlight.includes(itemName)) {
244
+ className += ' inactive'
245
+ }
246
+
247
+ dynamicLegendItems.map(listItem => {
248
+ if(listItem.text === label.text) {
249
+ inDynamicList = true;
250
+ }
251
+ })
252
+
253
+ if(inDynamicList) return true;
254
+ let palette = colorPalettes[config.palette];
255
+
256
+ label.value = palette[dynamicLegendItems.length]
257
+
258
+ return (
259
+ <option
260
+ className={className}
261
+ tabIndex={0}
262
+ value={JSON.stringify(label)}
263
+
264
+ >
265
+ {label.text}
266
+ </option>
267
+ )
268
+ })}
269
+ </select>
270
+ )
271
+ } else {
272
+ return config.legend.dynamicLegendItemLimitMessage
273
+ }
274
+ }}
275
+ </LegendOrdinal>
276
+
277
+
278
+ <div className="dynamic-legend-list">
279
+ {dynamicLegendItems.map((label, i) => {
280
+
281
+ let className = ['legend-item']
282
+ let itemName = label.text
283
+ let palette = colorPalettes[config.palette];
284
+
285
+ // Filter excluded data keys from legend
286
+ if (config.exclusions.active && config.exclusions.keys?.includes(itemName)) {
287
+ return
288
+ }
289
+
290
+ if (config.runtime.seriesLabels && !config.legend.dynamicLegend) {
291
+ let index = config.runtime.seriesLabelsAll.indexOf(itemName)
292
+ itemName = config.runtime.seriesKeys[index]
293
+ }
294
+
295
+ if (seriesHighlight.length > 0 && !seriesHighlight.includes(itemName)) {
296
+ className.push('inactive')
297
+ }
298
+
299
+ if(seriesHighlight.length === 0 && config.legend.dynamicLegend) {
300
+ className.push('inactive')
301
+ }
302
+
303
+ return (
304
+ <>
305
+ <LegendItem
306
+ className={className.join(' ')}
307
+ tabIndex={0}
308
+ key={`dynamic-legend-item-${i}`}
309
+ alignItems="center"
310
+
311
+ >
312
+ <button
313
+ className="btn-wrapper"
314
+ onClick={() => {
315
+ highlight(label);
316
+ }}
317
+ >
318
+ <LegendCircle fill={palette[i]} config={config} />
319
+ <LegendLabel align="space-between" margin="4px 0 0 4px">
320
+ {label.text}
321
+ </LegendLabel>
322
+ </button>
323
+ <button onClick={() => removeDynamicLegendItem(label)}>x</button>
324
+ </LegendItem>
325
+ </>
326
+ )
327
+ })}
328
+ </div>
329
+ {seriesHighlight.length < dynamicLegendItems.length && <button className={`legend-reset legend-reset--dynamic ${config.theme}`} onClick={highlightReset} tabIndex={0}>Reset</button>}
330
+ </aside>
331
+ )
332
+ }
333
+
334
+ export default Legend;
@@ -9,17 +9,34 @@ import ErrorBoundary from '@cdc/core/components/ErrorBoundary';
9
9
 
10
10
  import Context from '../context';
11
11
 
12
- export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData }) {
13
- const { transformedData: data, colorScale, seriesHighlight, config, formatNumber,formatDate,parseDate } = useContext<any>(Context);
12
+ export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData, xMax, yMax, seriesStyle = 'Line' }) {
13
+ const { colorPalettes, transformedData: data, colorScale, seriesHighlight, config, formatNumber,formatDate,parseDate } = useContext<any>(Context);
14
+
15
+ const handleLineType = (lineType) => {
16
+ switch(lineType) {
17
+ case 'dashed-sm':
18
+ return '5 5'
19
+ case 'dashed-md':
20
+ return '10 5'
21
+ case 'dashed-lg':
22
+ return '15 5'
23
+ default:
24
+ return 0;
25
+ }
26
+ }
27
+
28
+ console.log('seriesStyle', seriesStyle)
14
29
 
15
30
  return (
16
31
  <ErrorBoundary component="LineChart">
17
32
  <Group left={config.runtime.yAxis.size}>
18
- { (config.runtime.lineSeriesKeys || config.runtime.seriesKeys).map((seriesKey, index) => (
33
+ { (config.runtime.lineSeriesKeys || config.runtime.seriesKeys).map((seriesKey, index) => {
34
+ let lineType = config.series.filter(item => item.dataKey === seriesKey)[0].type
35
+ return (
19
36
  <Group
20
37
  key={`series-${seriesKey}`}
21
38
  opacity={config.legend.behavior === "highlight" && seriesHighlight.length > 0 && seriesHighlight.indexOf(seriesKey) === -1 ? 0.5 : 1}
22
- display={config.legend.behavior === "highlight" || seriesHighlight.length === 0 || seriesHighlight.indexOf(seriesKey) !== -1 ? 'block' : 'none'}
39
+ display={config.legend.behavior === "highlight" || (seriesHighlight.length === 0 && !config.legend.dynamicLegend) || seriesHighlight.indexOf(seriesKey) !== -1 ? 'block' : 'none'}
23
40
  >
24
41
  { data.map((d, dataIndex) => {
25
42
  const xAxisValue = config.runtime.xAxis.type==='date' ? formatDate(parseDate(d[config.runtime.xAxis.dataKey])) : d[config.runtime.xAxis.dataKey];
@@ -33,9 +50,9 @@ export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData }
33
50
  </div>`
34
51
 
35
52
  let circleRadii = 4.5
36
-
37
- return (
53
+ return (d[seriesKey] !== undefined && d[seriesKey] !== "") && (
38
54
  <Group key={`series-${seriesKey}-point-${dataIndex}`}>
55
+
39
56
  <Text
40
57
  display={config.labels ? 'block' : 'none'}
41
58
  x={xScale(getXAxisData(d))}
@@ -44,6 +61,7 @@ export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData }
44
61
  textAnchor="middle">
45
62
  {formatNumber(d[seriesKey])}
46
63
  </Text>
64
+
47
65
  <circle
48
66
  key={`${seriesKey}-${dataIndex}`}
49
67
  r={circleRadii}
@@ -57,18 +75,40 @@ export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData }
57
75
  </Group>
58
76
  )
59
77
  })}
60
- <LinePath
61
- curve={allCurves.curveLinear}
62
- data={data}
63
- x={(d) => xScale(getXAxisData(d))}
64
- y={(d) => yScale(getYAxisData(d, seriesKey))}
65
- stroke={colorScale ? colorScale(config.runtime.seriesLabels ? config.runtime.seriesLabels[seriesKey] : seriesKey) : '#000'}
66
- strokeWidth={2}
67
- strokeOpacity={1}
68
- shapeRendering="geometricPrecision"
69
- />
78
+ <LinePath
79
+ curve={allCurves.curveLinear}
80
+ data={data}
81
+ x={(d) => xScale(getXAxisData(d))}
82
+ y={(d) => yScale(getYAxisData(d, seriesKey))}
83
+ stroke={colorScale && !config.legend.dynamicLegend ? colorScale(config.runtime.seriesLabels ? config.runtime.seriesLabels[seriesKey] : seriesKey) :
84
+ // is dynamic legend
85
+ config.legend.dynamicLegend ? colorPalettes[config.palette][index] :
86
+ // fallback
87
+ '#000'
88
+ }
89
+ strokeWidth={2}
90
+ strokeOpacity={1}
91
+ shapeRendering="geometricPrecision"
92
+ strokeDasharray={lineType ? handleLineType(lineType) : 0}
93
+ defined={(item,i) => {
94
+ return item[config.runtime.seriesLabels[seriesKey]] !== "";
95
+ }}
96
+ />
70
97
  </Group>
71
- ))
98
+ )})
99
+ }
100
+
101
+ {/* Message when dynamic legend and nothing has been picked */}
102
+ { (config.legend.dynamicLegend && seriesHighlight.length === 0) &&
103
+ <Text
104
+ x={ xMax / 2 }
105
+ y={yMax / 2}
106
+ fill="black"
107
+ textAnchor="middle"
108
+ color='black'
109
+ >
110
+ {config.legend.dynamicLegendChartMessage}
111
+ </Text>
72
112
  }
73
113
  </Group>
74
114
  </ErrorBoundary>