@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.
Files changed (39) hide show
  1. package/dist/cdcchart.js +28341 -27278
  2. package/examples/feature/__data__/area-chart-date-apple.json +5122 -0
  3. package/examples/feature/__data__/city-temperature.json +2198 -0
  4. package/examples/feature/area/area-chart-category.json +45 -45
  5. package/examples/feature/area/area-chart-date-apple.json +10376 -0
  6. package/examples/feature/area/area-chart-date-city-temperature.json +4528 -0
  7. package/examples/feature/area/area-chart-date.json +111 -3
  8. package/examples/feature/forest-plot/broken.json +700 -0
  9. package/examples/feature/forest-plot/data.csv +24 -0
  10. package/examples/feature/forest-plot/forest-plot.json +717 -0
  11. package/examples/feature/pie/planet-pie-example-config.json +1 -1
  12. package/index.html +28 -8
  13. package/package.json +4 -3
  14. package/src/CdcChart.jsx +24 -14
  15. package/src/components/AreaChart.jsx +84 -59
  16. package/src/components/BarChart.Horizontal.jsx +251 -0
  17. package/src/components/BarChart.StackedHorizontal.jsx +118 -0
  18. package/src/components/BarChart.StackedVertical.jsx +95 -0
  19. package/src/components/BarChart.Vertical.jsx +204 -0
  20. package/src/components/BarChart.jsx +14 -674
  21. package/src/components/BarChartType.jsx +15 -0
  22. package/src/components/BrushHandle.jsx +17 -0
  23. package/src/components/DataTable.jsx +185 -23
  24. package/src/components/EditorPanel.jsx +361 -305
  25. package/src/components/ForestPlot.jsx +191 -0
  26. package/src/components/ForestPlotSettings.jsx +508 -0
  27. package/src/components/Legend.jsx +11 -3
  28. package/src/components/LineChart.jsx +2 -2
  29. package/src/components/LinearChart.jsx +115 -310
  30. package/src/data/initial-state.js +47 -1
  31. package/src/hooks/useBarChart.js +186 -0
  32. package/src/hooks/useEditorPermissions.js +218 -0
  33. package/src/hooks/useLegendClasses.js +14 -11
  34. package/src/hooks/useMinMax.js +15 -3
  35. package/src/hooks/useReduceData.js +2 -2
  36. package/src/hooks/useScales.js +46 -3
  37. package/src/hooks/useTooltip.jsx +407 -0
  38. package/src/scss/legend.scss +206 -0
  39. 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
- export default function DataTable() {
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
- canSort: true
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 { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable({ columns: tableColumns, data: tableData, defaultColumn }, useSortBy, useBlockLayout, useResizeColumns)
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 ? (column.isSortedDesc ? 'sort sort-desc' : 'sort sort-asc') : 'sort'}
271
- {...(column.isSorted ? (column.isSortedDesc ? { 'aria-sort': 'descending' } : { 'aria-sort': 'ascending' }) : null)}
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
- {index === 0 ? (config.table.indexLabel ? config.table.indexLabel : column.render('Header')) : column.render('Header')}
274
- <button>
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