@cdc/chart 4.23.7 → 4.23.9
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.
- package/dist/cdcchart.js +28341 -27278
- package/examples/feature/__data__/area-chart-date-apple.json +5122 -0
- package/examples/feature/__data__/city-temperature.json +2198 -0
- package/examples/feature/area/area-chart-category.json +45 -45
- package/examples/feature/area/area-chart-date-apple.json +10376 -0
- package/examples/feature/area/area-chart-date-city-temperature.json +4528 -0
- package/examples/feature/area/area-chart-date.json +111 -3
- package/examples/feature/forest-plot/broken.json +700 -0
- package/examples/feature/forest-plot/data.csv +24 -0
- package/examples/feature/forest-plot/forest-plot.json +717 -0
- package/examples/feature/pie/planet-pie-example-config.json +1 -1
- package/index.html +28 -8
- package/package.json +4 -3
- package/src/CdcChart.jsx +24 -14
- package/src/components/AreaChart.jsx +84 -59
- package/src/components/BarChart.Horizontal.jsx +251 -0
- package/src/components/BarChart.StackedHorizontal.jsx +118 -0
- package/src/components/BarChart.StackedVertical.jsx +95 -0
- package/src/components/BarChart.Vertical.jsx +204 -0
- package/src/components/BarChart.jsx +14 -674
- package/src/components/BarChartType.jsx +15 -0
- package/src/components/BrushHandle.jsx +17 -0
- package/src/components/DataTable.jsx +185 -23
- package/src/components/EditorPanel.jsx +361 -305
- package/src/components/ForestPlot.jsx +191 -0
- package/src/components/ForestPlotSettings.jsx +508 -0
- package/src/components/Legend.jsx +11 -3
- package/src/components/LineChart.jsx +2 -2
- package/src/components/LinearChart.jsx +115 -310
- package/src/data/initial-state.js +47 -1
- package/src/hooks/useBarChart.js +186 -0
- package/src/hooks/useEditorPermissions.js +218 -0
- package/src/hooks/useLegendClasses.js +14 -11
- package/src/hooks/useMinMax.js +15 -3
- package/src/hooks/useReduceData.js +2 -2
- package/src/hooks/useScales.js +46 -3
- package/src/hooks/useTooltip.jsx +407 -0
- package/src/scss/legend.scss +206 -0
- package/src/scss/main.scss +26 -12
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import BarChartStackedVertical from './BarChart.StackedVertical'
|
|
4
|
+
import BarChartStackedHorizontal from './BarChart.StackedHorizontal'
|
|
5
|
+
import BarChartVertical from './BarChart.Vertical'
|
|
6
|
+
import BarChartHorizontal from './BarChart.Horizontal'
|
|
7
|
+
|
|
8
|
+
const BarChartType = {
|
|
9
|
+
Vertical: BarChartVertical,
|
|
10
|
+
Horizontal: BarChartHorizontal,
|
|
11
|
+
StackedVertical: BarChartStackedVertical,
|
|
12
|
+
StackedHorizontal: BarChartStackedHorizontal
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default BarChartType
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Group } from '@visx/group'
|
|
2
|
+
|
|
3
|
+
const BrushHandle = props => {
|
|
4
|
+
const { x, height, isBrushActive } = props
|
|
5
|
+
const pathWidth = 8
|
|
6
|
+
const pathHeight = 15
|
|
7
|
+
if (!isBrushActive) {
|
|
8
|
+
return null
|
|
9
|
+
}
|
|
10
|
+
return (
|
|
11
|
+
<Group left={x + pathWidth / 2} top={(height - pathHeight) / 2}>
|
|
12
|
+
<path fill='#f2f2f2' d='M -4.5 0.5 L 3.5 0.5 L 3.5 15.5 L -4.5 15.5 L -4.5 0.5 M -1.5 4 L -1.5 12 M 0.5 4 L 0.5 12' stroke='#999999' strokeWidth='1' style={{ cursor: 'ew-resize' }} />
|
|
13
|
+
</Group>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default BrushHandle
|
|
@@ -7,18 +7,23 @@ import { colorPalettesChart } from '@cdc/core/data/colorPalettes'
|
|
|
7
7
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
8
8
|
import LegendCircle from '@cdc/core/components/LegendCircle'
|
|
9
9
|
import Icon from '@cdc/core/components/ui/Icon'
|
|
10
|
+
import { DataTransform } from '@cdc/core/helpers/DataTransform'
|
|
10
11
|
|
|
11
12
|
import ConfigContext from '../ConfigContext'
|
|
12
13
|
|
|
13
14
|
import MediaControls from '@cdc/core/components/MediaControls'
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
const DataTable = props => {
|
|
17
|
+
// had to pass in runtimeData as prop to get the raw prop names in the inbound data (TT)
|
|
18
|
+
const { runtimeData, isDebug } = props
|
|
19
|
+
|
|
16
20
|
const { rawData, tableData: data, config, colorScale, parseDate, formatDate, formatNumber: numberFormatter, colorPalettes, currentViewport } = useContext(ConfigContext)
|
|
17
21
|
|
|
18
22
|
const section = config.orientation === 'horizontal' ? 'yAxis' : 'xAxis'
|
|
19
23
|
const [tableExpanded, setTableExpanded] = useState(config.table.expanded)
|
|
20
24
|
const [accessibilityLabel, setAccessibilityLabel] = useState('')
|
|
21
25
|
const isLegendBottom = ['sm', 'xs', 'xxs'].includes(currentViewport)
|
|
26
|
+
const transform = new DataTransform()
|
|
22
27
|
|
|
23
28
|
const DownloadButton = ({ data }, type) => {
|
|
24
29
|
const fileName = `${config.title.substring(0, 50)}.csv`
|
|
@@ -34,19 +39,13 @@ export default function DataTable() {
|
|
|
34
39
|
}
|
|
35
40
|
}
|
|
36
41
|
|
|
42
|
+
// - trying to eliminate console error that occurs if formatted with prettier
|
|
43
|
+
// prettier-ignore
|
|
37
44
|
switch (type) {
|
|
38
45
|
case 'download':
|
|
39
|
-
return (
|
|
40
|
-
<a download={fileName} onClick={saveBlob} href={`data:text/csv;base64,${Base64.encode(csvData)}`} aria-label='Download this data in a CSV file format.' className={`btn btn-download no-border margin-sm`}>
|
|
41
|
-
Download Data (CSV)
|
|
42
|
-
</a>
|
|
43
|
-
)
|
|
46
|
+
return (<a download={fileName} onClick={saveBlob} href={`data:text/csv;base64,${Base64.encode(csvData)}`} aria-label='Download this data in a CSV file format.' className={`btn btn-download no-border margin-sm`}>Download Data (CSV)</a>)
|
|
44
47
|
default:
|
|
45
|
-
return (
|
|
46
|
-
<a download={fileName} onClick={saveBlob} href={`data:text/csv;base64,${Base64.encode(csvData)}`} aria-label='Download this data in a CSV file format.' className={`no-border`}>
|
|
47
|
-
Download Data (CSV)
|
|
48
|
-
</a>
|
|
49
|
-
)
|
|
48
|
+
return (<a download={fileName} onClick={saveBlob} href={`data:text/csv;base64,${Base64.encode(csvData)}`} aria-label='Download this data in a CSV file format.' className={`no-border`}>Download Data (CSV)</a>)
|
|
50
49
|
}
|
|
51
50
|
}
|
|
52
51
|
|
|
@@ -93,7 +92,7 @@ export default function DataTable() {
|
|
|
93
92
|
]
|
|
94
93
|
: [
|
|
95
94
|
{
|
|
96
|
-
Header: '',
|
|
95
|
+
Header: ' ',
|
|
97
96
|
Cell: ({ row }) => {
|
|
98
97
|
const getSeriesLabel = () => {
|
|
99
98
|
let userUpdatedSeriesName = config.series.filter(series => series.dataKey === row.original)?.[0]?.name
|
|
@@ -121,7 +120,9 @@ export default function DataTable() {
|
|
|
121
120
|
</>
|
|
122
121
|
)
|
|
123
122
|
},
|
|
124
|
-
id: 'series-label'
|
|
123
|
+
id: 'series-label',
|
|
124
|
+
sortType: 'custom',
|
|
125
|
+
canSort: true
|
|
125
126
|
}
|
|
126
127
|
]
|
|
127
128
|
if (config.visualizationType !== 'Box Plot') {
|
|
@@ -151,7 +152,9 @@ export default function DataTable() {
|
|
|
151
152
|
return <>{numberFormatter(d[row.original], resolvedAxis)}</>
|
|
152
153
|
},
|
|
153
154
|
id: `${d[config.runtime.originalXAxis.dataKey]}--${index}`,
|
|
154
|
-
|
|
155
|
+
sortType: 'custom',
|
|
156
|
+
canSort: true,
|
|
157
|
+
defaultCanSort: true
|
|
155
158
|
}
|
|
156
159
|
|
|
157
160
|
newTableColumns.push(newCol)
|
|
@@ -222,7 +225,166 @@ export default function DataTable() {
|
|
|
222
225
|
}),
|
|
223
226
|
[]
|
|
224
227
|
)
|
|
225
|
-
const
|
|
228
|
+
const upIcon = (
|
|
229
|
+
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 5'>
|
|
230
|
+
<path d='M0 5l5-5 5 5z' />
|
|
231
|
+
</svg>
|
|
232
|
+
)
|
|
233
|
+
const downIcon = (
|
|
234
|
+
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 5'>
|
|
235
|
+
<path d='M0 0l5 5 5-5z' />
|
|
236
|
+
</svg>
|
|
237
|
+
)
|
|
238
|
+
const getSpecificCellData = (array, value) => {
|
|
239
|
+
return array.filter(data => JSON.stringify(data).toLowerCase().indexOf(value.toLowerCase()) !== -1)
|
|
240
|
+
}
|
|
241
|
+
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable(
|
|
242
|
+
{
|
|
243
|
+
columns: tableColumns,
|
|
244
|
+
data: tableData,
|
|
245
|
+
defaultColumn,
|
|
246
|
+
disableSortRemove: true, // otherwise 3rd click on header removes sorting entirely
|
|
247
|
+
sortTypes: {
|
|
248
|
+
custom: (rowA, rowB, columnId) => {
|
|
249
|
+
// NOTE:
|
|
250
|
+
// 1) Main issue causing all this code:
|
|
251
|
+
// rowA and rowB are coming in with all values undefined
|
|
252
|
+
// - if it passed the values we could just use the columnId to get the correct sort value
|
|
253
|
+
// but since it's not there we have to go through a bunch of code to get it because
|
|
254
|
+
// we also do not know the Y axis data key (TT)
|
|
255
|
+
// 2). if formattedData did not truncate the strings we could get it from there
|
|
256
|
+
// but Hispanic or Latino is truncated to just Hispanic as the key
|
|
257
|
+
// and 'White, Non-Hispanic/Latino' gets truncated to remove the /Latino
|
|
258
|
+
|
|
259
|
+
// rowA.original - is the row data field name to access the value
|
|
260
|
+
// columnId = the column indicator typically date or date--index
|
|
261
|
+
let a, b
|
|
262
|
+
if (columnId === 'series-label') {
|
|
263
|
+
// comparing strings
|
|
264
|
+
a = rowA.original
|
|
265
|
+
b = rowB.original
|
|
266
|
+
return a.localeCompare(b)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
let dataKey = config.xAxis.dataKey
|
|
270
|
+
let columnIdIndexRemoved = columnId.split('--')[0] // have to remove index for compare
|
|
271
|
+
//get all the data from that column
|
|
272
|
+
let colData = runtimeData.filter(obj => {
|
|
273
|
+
// problem is dates can be in different formats
|
|
274
|
+
if (config.xAxis.type === 'date' && !isNaN(Date.parse(obj[dataKey])) && !isNaN(Date.parse(columnIdIndexRemoved))) {
|
|
275
|
+
// must convert to datetime number to compare
|
|
276
|
+
return parseDate(obj[dataKey]).getTime() === parseDate(columnIdIndexRemoved).getTime()
|
|
277
|
+
} else {
|
|
278
|
+
return obj[dataKey] === columnIdIndexRemoved // have to remove index
|
|
279
|
+
}
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
if (colData === undefined || colData[0] === undefined) {
|
|
283
|
+
return -1
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
let rowA_cellObj = getSpecificCellData(colData, rowA.original)
|
|
287
|
+
let rowB_cellObj = getSpecificCellData(colData, rowB.original)
|
|
288
|
+
|
|
289
|
+
// - ** REMOVE any data points NOT selected in the data series ***
|
|
290
|
+
// I dont understand why not selected data series are still sent down in the data
|
|
291
|
+
// - would be better to scrub outside of here (TT)
|
|
292
|
+
let newRowA_cellObj = []
|
|
293
|
+
let newRowB_cellObj = []
|
|
294
|
+
if (config.runtime.seriesKeys) {
|
|
295
|
+
config.runtime.seriesKeys.forEach(seriesKey => {
|
|
296
|
+
if (seriesKey in rowA_cellObj[0]) newRowA_cellObj.push(rowA_cellObj[0][seriesKey])
|
|
297
|
+
if (seriesKey in rowB_cellObj[0]) newRowB_cellObj.push(rowB_cellObj[0][seriesKey])
|
|
298
|
+
})
|
|
299
|
+
// copy back over
|
|
300
|
+
rowA_cellObj[0] = newRowA_cellObj
|
|
301
|
+
rowB_cellObj[0] = newRowB_cellObj
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// REMOVE the following:
|
|
305
|
+
// - value equal to column date
|
|
306
|
+
// - value that is the .original
|
|
307
|
+
// - any data still in that's not really a number
|
|
308
|
+
let rowA_valueObj = Object.values(rowA_cellObj[0]).filter(value => value !== columnIdIndexRemoved && value !== rowA.original && !isNaN(value))
|
|
309
|
+
let rowB_valueObj = Object.values(rowB_cellObj[0]).filter(value => value !== columnIdIndexRemoved && value !== rowB.original && !isNaN(value))
|
|
310
|
+
|
|
311
|
+
// NOW we can get the sort values from the cell object
|
|
312
|
+
a = rowA_valueObj.length > 1 ? rowA_valueObj[rowA.id] : rowA_valueObj[0]
|
|
313
|
+
b = rowB_valueObj.length > 1 ? rowB_valueObj[rowB.id] : rowB_valueObj[0]
|
|
314
|
+
|
|
315
|
+
// force null and undefined to the bottom
|
|
316
|
+
a = a === null || a === undefined ? '' : transform.cleanDataPoint(a)
|
|
317
|
+
b = b === null || b === undefined ? '' : transform.cleanDataPoint(b)
|
|
318
|
+
if (a === '' || a === null) {
|
|
319
|
+
if (b === '' || b === null) {
|
|
320
|
+
return 0 // Both empty/null
|
|
321
|
+
}
|
|
322
|
+
return -1 // Sort a to an index lower than b
|
|
323
|
+
}
|
|
324
|
+
if (b === '' || b === null) {
|
|
325
|
+
if (a === '' || a === null) {
|
|
326
|
+
return 0 // Both empty/null
|
|
327
|
+
}
|
|
328
|
+
return 1 // Sort b to an index lower than a
|
|
329
|
+
}
|
|
330
|
+
// End code for forcing NULLS to bottom
|
|
331
|
+
|
|
332
|
+
// convert any strings that are actually numbers to proper data type
|
|
333
|
+
const aNum = Number(a)
|
|
334
|
+
|
|
335
|
+
if (!Number.isNaN(aNum)) {
|
|
336
|
+
a = aNum
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const bNum = Number(b)
|
|
340
|
+
|
|
341
|
+
if (!Number.isNaN(bNum)) {
|
|
342
|
+
b = bNum
|
|
343
|
+
}
|
|
344
|
+
// remove iso code prefixes
|
|
345
|
+
if (typeof a === 'string') {
|
|
346
|
+
a = a.replace('us-', '')
|
|
347
|
+
a = displayGeoName(a)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (typeof b === 'string') {
|
|
351
|
+
b = b.replace('us-', '')
|
|
352
|
+
b = displayGeoName(b)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// force any string values to lowercase
|
|
356
|
+
a = typeof a === 'string' ? a.toLowerCase() : a
|
|
357
|
+
b = typeof b === 'string' ? b.toLowerCase() : b
|
|
358
|
+
|
|
359
|
+
// When comparing a number to a string, always send string to bottom
|
|
360
|
+
if (typeof a === 'number' && typeof b === 'string') {
|
|
361
|
+
return 1
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (typeof b === 'number' && typeof a === 'string') {
|
|
365
|
+
return -1
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Return either 1 or -1 to indicate a sort priority
|
|
369
|
+
if (a > b) {
|
|
370
|
+
return 1
|
|
371
|
+
}
|
|
372
|
+
if (a < b) {
|
|
373
|
+
return -1
|
|
374
|
+
}
|
|
375
|
+
// returning 0, undefined or any falsey value will use subsequent sorts or
|
|
376
|
+
// the index as a tiebreaker
|
|
377
|
+
return 0
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
initialState: {
|
|
381
|
+
sortBy: [{ id: 'series-label', desc: false }] // default sort on 1st column -
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
useSortBy,
|
|
385
|
+
useBlockLayout,
|
|
386
|
+
useResizeColumns
|
|
387
|
+
)
|
|
226
388
|
|
|
227
389
|
// sort continuous x axis scaling for data tables, ie. xAxis should read 1,2,3,4,5
|
|
228
390
|
if (config.xAxis.type === 'continuous' && headerGroups) {
|
|
@@ -236,7 +398,7 @@ export default function DataTable() {
|
|
|
236
398
|
{config.table.download && <DownloadButton data={rawData} type='link' />}
|
|
237
399
|
</MediaControls.Section>
|
|
238
400
|
|
|
239
|
-
<section style={{ marginTop: !isLegendBottom ? config.dynamicMarginTop + 'px' : '0px' }} id={config?.title ? `dataTableSection__${config?.title.replace(/\s/g, '')}` : `dataTableSection`} className={`data-table-container`} aria-label={accessibilityLabel}>
|
|
401
|
+
<section style={{ marginTop: !isLegendBottom ? config.dynamicMarginTop / 4 + 'px' : '0px' }} id={config?.title ? `dataTableSection__${config?.title.replace(/\s/g, '')}` : `dataTableSection`} className={`data-table-container`} aria-label={accessibilityLabel}>
|
|
240
402
|
<div
|
|
241
403
|
role='button'
|
|
242
404
|
className={tableExpanded ? 'data-table-heading' : 'collapsed data-table-heading'}
|
|
@@ -259,6 +421,7 @@ export default function DataTable() {
|
|
|
259
421
|
<thead>
|
|
260
422
|
{headerGroups.map((headerGroup, index) => (
|
|
261
423
|
<tr {...headerGroup.getHeaderGroupProps()} key={`headerGroups--${index}`}>
|
|
424
|
+
{' '}
|
|
262
425
|
{headerGroup.headers.map((column, index) => (
|
|
263
426
|
<th
|
|
264
427
|
tabIndex='0'
|
|
@@ -267,14 +430,11 @@ export default function DataTable() {
|
|
|
267
430
|
role='columnheader'
|
|
268
431
|
scope='col'
|
|
269
432
|
{...column.getHeaderProps(column.getSortByToggleProps())}
|
|
270
|
-
className={column.isSorted
|
|
271
|
-
{...(column.isSorted
|
|
433
|
+
className={column.isSorted && column.isSortedDesc ? 'sort sort-desc' : 'sort sort-asc'}
|
|
434
|
+
{...(column.isSorted && column.isSortedDesc ? { 'aria-sort': 'descending' } : { 'aria-sort': 'ascending' })}
|
|
272
435
|
>
|
|
273
|
-
{
|
|
274
|
-
<
|
|
275
|
-
<span className='cdcdataviz-sr-only'>{`Sort by ${typeof column.render('Header') === 'string' ? column.render('Header').toLowerCase() : column.render('Header')} in ${column.isSorted ? (column.isSortedDesc ? 'descending' : 'ascending') : 'no'} `} order</span>
|
|
276
|
-
</button>
|
|
277
|
-
<div {...column.getResizerProps()} className='resizer' />
|
|
436
|
+
{column.render('Header')}
|
|
437
|
+
{column.isSorted && <span className={'sort-icon'}>{column.isSortedDesc ? downIcon : upIcon}</span>}
|
|
278
438
|
</th>
|
|
279
439
|
))}
|
|
280
440
|
</tr>
|
|
@@ -330,3 +490,5 @@ export default function DataTable() {
|
|
|
330
490
|
</ErrorBoundary>
|
|
331
491
|
)
|
|
332
492
|
}
|
|
493
|
+
|
|
494
|
+
export default DataTable
|