@cdc/chart 4.22.11 → 4.23.1

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 (38) hide show
  1. package/dist/495.js +3 -0
  2. package/dist/703.js +1 -0
  3. package/dist/cdcchart.js +723 -6
  4. package/examples/box-plot-data.json +71 -0
  5. package/examples/box-plot.csv +5 -0
  6. package/examples/{private/yaxis-test.json → box-plot.json} +47 -56
  7. package/examples/gallery/bar-chart-vertical/combo-line-chart.json +3 -1
  8. package/examples/gallery/bar-chart-vertical/vertical-bar-chart.json +85 -16
  9. package/examples/new-data.csv +17 -0
  10. package/examples/newdata.json +90 -0
  11. package/package.json +3 -2
  12. package/src/CdcChart.tsx +150 -94
  13. package/src/components/BarChart.tsx +156 -226
  14. package/src/components/BoxPlot.js +92 -0
  15. package/src/components/DataTable.tsx +28 -12
  16. package/src/components/EditorPanel.js +151 -104
  17. package/src/components/Filters.js +131 -0
  18. package/src/components/Legend.js +8 -1
  19. package/src/components/LineChart.tsx +64 -13
  20. package/src/components/LinearChart.tsx +120 -81
  21. package/src/components/PairedBarChart.tsx +1 -1
  22. package/src/components/PieChart.tsx +12 -2
  23. package/src/components/useIntersectionObserver.tsx +9 -7
  24. package/src/data/initial-state.js +14 -8
  25. package/src/hooks/useReduceData.ts +8 -5
  26. package/src/index.html +51 -51
  27. package/src/scss/DataTable.scss +1 -1
  28. package/src/scss/main.scss +53 -22
  29. package/examples/private/filters.json +0 -170
  30. package/examples/private/line-test-data.json +0 -22
  31. package/examples/private/line-test-two.json +0 -210
  32. package/examples/private/line-test.json +0 -102
  33. package/examples/private/new.json +0 -48800
  34. package/examples/private/newtest.csv +0 -101
  35. package/examples/private/shawn.json +0 -1106
  36. package/examples/private/test.json +0 -10124
  37. package/examples/private/yaxis-testing.csv +0 -27
  38. package/examples/private/yaxis.json +0 -28
package/src/CdcChart.tsx CHANGED
@@ -4,13 +4,16 @@ import React, { useState, useEffect, useCallback } from 'react'
4
4
  import 'core-js/stable'
5
5
  import ResizeObserver from 'resize-observer-polyfill'
6
6
  import 'whatwg-fetch'
7
+ import * as d3 from 'd3-array'
7
8
 
8
9
  // External Libraries
9
10
  import { scaleOrdinal } from '@visx/scale'
10
11
  import ParentSize from '@visx/responsive/lib/components/ParentSize'
11
12
  import { timeParse, timeFormat } from 'd3-time-format'
13
+ import { format } from 'd3-format'
12
14
  import Papa from 'papaparse'
13
15
  import parse from 'html-react-parser'
16
+ import { Base64 } from 'js-base64'
14
17
 
15
18
  // Primary Components
16
19
  import Context from './context'
@@ -29,6 +32,8 @@ import DataTable from './components/DataTable'
29
32
  import defaults from './data/initial-state'
30
33
  import EditorPanel from './components/EditorPanel'
31
34
  import Loading from '@cdc/core/components/Loading'
35
+ import Filters from './components/Filters'
36
+ import CoveMediaControls from '@cdc/core/helpers/CoveMediaControls'
32
37
 
33
38
  // helpers
34
39
  import numberFromString from '@cdc/core/helpers/numberFromString'
@@ -40,7 +45,6 @@ import './scss/main.scss'
40
45
 
41
46
  export default function CdcChart({ configUrl, config: configObj, isEditor = false, isDashboard = false, setConfig: setParentConfig, setEditing, hostname, link }: { configUrl?: string; config?: any; isEditor?: boolean; isDashboard?: boolean; setConfig?; setEditing?; hostname?; link?: any }) {
42
47
  const transform = new DataTransform()
43
-
44
48
  interface keyable {
45
49
  [key: string]: any
46
50
  }
@@ -58,10 +62,13 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
58
62
  const [container, setContainer] = useState()
59
63
  const [coveLoadedEventRan, setCoveLoadedEventRan] = useState(false)
60
64
  const [dynamicLegendItems, setDynamicLegendItems] = useState([])
65
+ const [imageId, setImageId] = useState(`cove-${Math.random().toString(16).slice(-4)}`)
61
66
 
62
67
  const legendGlyphSize = 15
63
68
  const legendGlyphSizeHalf = legendGlyphSize / 2
64
69
 
70
+ // Destructure items from config for more readable JSX
71
+ const { legend, title, description, visualizationType } = config
65
72
  const { barBorderClass, lineDatapointClass, contentClasses, innerContainerClasses, sparkLineStyles } = useDataVizClasses(config)
66
73
 
67
74
  const handleChartTabbing = config.showSidebar ? `#legend` : config?.title ? `#dataTableSection__${config.title.replace(/\s/g, '')}` : `#dataTableSection`
@@ -211,6 +218,66 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
211
218
  : []
212
219
  }
213
220
 
221
+ if (newConfig.visualizationType === 'Box Plot' && newConfig.series) {
222
+ console.log('hit', newConfig)
223
+
224
+ // stats
225
+ let allKeys = data.map(d => d[newConfig.xAxis.dataKey])
226
+ let allValues = data.map(d => Number(d[newConfig?.series[0]?.dataKey]))
227
+
228
+ const uniqueArray = function (arrArg) {
229
+ return arrArg.filter(function (elem, pos, arr) {
230
+ return arr.indexOf(elem) === pos
231
+ })
232
+ }
233
+
234
+ const groups = uniqueArray(allKeys)
235
+ const plots = []
236
+
237
+ console.log('d', data)
238
+ console.log('newConfig', newConfig)
239
+ console.log('groups', groups)
240
+ console.log('allKeys', allKeys)
241
+ console.log('allValues', allValues)
242
+
243
+ // group specific statistics
244
+ // prevent re-renders
245
+ groups.map((g, index) => {
246
+ if (!g) return
247
+ // filter data by group
248
+ let filteredData = data.filter(item => item[newConfig.xAxis.dataKey] === g)
249
+ let filteredDataValues = filteredData.map(item => Number(item[newConfig?.series[0]?.dataKey]))
250
+ console.log('g', g)
251
+ console.log('item', filteredData)
252
+ console.log('item', newConfig)
253
+ // let filteredDataValues = filteredData.map(item => Number(item[newConfig.yAxis.dataKey]))
254
+
255
+ const q1 = d3.quantile(filteredDataValues, 0.25)
256
+ const q3 = d3.quantile(filteredDataValues, 0.75)
257
+ const iqr = q3 - q1
258
+ const lowerBounds = q1 - (q3 - q1) * 1.5
259
+ const upperBounds = q3 + (q3 - q1) * 1.5
260
+ const outliers = filteredDataValues.filter(v => v < lowerBounds || v > upperBounds)
261
+ plots.push({
262
+ columnCategory: g,
263
+ columnMean: d3.mean(filteredDataValues),
264
+ columnMedian: d3.median(filteredDataValues),
265
+ columnFirstQuartile: q1,
266
+ columnThirdQuartile: q3,
267
+ columnMin: q1 - 1.5 * iqr,
268
+ columnMax: q3 + 1.5 * iqr,
269
+ columnIqr: iqr,
270
+ columnOutliers: outliers,
271
+ values: filteredDataValues
272
+ })
273
+ })
274
+
275
+ // any other data we can add to boxplots
276
+ newConfig.boxplot['allValues'] = allValues
277
+ newConfig.boxplot['categories'] = groups
278
+ newConfig.boxplot.push(...plots)
279
+ }
280
+
214
281
  if (newConfig.visualizationType === 'Combo' && newConfig.series) {
215
282
  newConfig.runtime.barSeriesKeys = []
216
283
  newConfig.runtime.lineSeriesKeys = []
@@ -470,28 +537,63 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
470
537
  return timeFormat(config.runtime[section].dateDisplayFormat)(date)
471
538
  }
472
539
 
540
+ const DownloadButton = ({ data }: any, type = 'link') => {
541
+ const fileName = `${config.title.substring(0, 50)}.csv`
542
+
543
+ const csvData = Papa.unparse(data)
544
+
545
+ const saveBlob = () => {
546
+ //@ts-ignore
547
+ if (typeof window.navigator.msSaveBlob === 'function') {
548
+ const dataBlob = new Blob([csvData], { type: 'text/csv;charset=utf-8;' })
549
+ //@ts-ignore
550
+ window.navigator.msSaveBlob(dataBlob, fileName)
551
+ }
552
+ }
553
+
554
+ if (type === 'download') {
555
+ return (
556
+ <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`}>
557
+ Download Data (CSV)
558
+ </a>
559
+ )
560
+ } else {
561
+ return (
562
+ <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 no-border`}>
563
+ Download Data (CSV)
564
+ </a>
565
+ )
566
+ }
567
+ }
568
+
473
569
  // Format numeric data based on settings in config
474
570
  const formatNumber = (num, axis) => {
475
- // check if value contains comma and remove it. later will add comma below.
476
- if (String(num).indexOf(',') !== -1) num = num.replaceAll(',', '')
477
571
  // if num is NaN return num
478
572
  if (isNaN(num) || !num) return num
479
573
 
574
+ // destructure dataFormat values
575
+ let {
576
+ dataFormat: { commas, abbreviated, roundTo, prefix, suffix, rightRoundTo, rightPrefix, rightSuffix }
577
+ } = config
578
+ let formatSuffix = format('.2s')
579
+
580
+ // check if value contains comma and remove it. later will add comma below.
581
+ if (String(num).indexOf(',') !== -1) num = num.replaceAll(',', '')
582
+
480
583
  let original = num
481
- let prefix = config.dataFormat.prefix
482
584
  let stringFormattingOptions
483
585
 
484
586
  if (axis !== 'right') {
485
587
  stringFormattingOptions = {
486
588
  useGrouping: config.dataFormat.commas ? true : false,
487
- minimumFractionDigits: config.dataFormat.roundTo ? Number(config.dataFormat.roundTo) : 0,
488
- maximumFractionDigits: config.dataFormat.roundTo ? Number(config.dataFormat.roundTo) : 0
589
+ minimumFractionDigits: roundTo ? Number(roundTo) : 0,
590
+ maximumFractionDigits: roundTo ? Number(roundTo) : 0
489
591
  }
490
592
  } else {
491
593
  stringFormattingOptions = {
492
594
  useGrouping: config.dataFormat.rightCommas ? true : false,
493
- minimumFractionDigits: config.dataFormat.rightRoundTo ? Number(config.dataFormat.rightRoundTo) : 0,
494
- maximumFractionDigits: config.dataFormat.rightRoundTo ? Number(config.dataFormat.rightRoundTo) : 0
595
+ minimumFractionDigits: rightRoundTo ? Number(rightRoundTo) : 0,
596
+ maximumFractionDigits: rightRoundTo ? Number(rightRoundTo) : 0
495
597
  }
496
598
  }
497
599
 
@@ -510,109 +612,55 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
510
612
  num = cutoff
511
613
  }
512
614
  }
513
- num = num.toLocaleString('en-US', stringFormattingOptions)
514
615
 
616
+ // When we're formatting the left axis
617
+ // Use commas also updates bars and the data table
618
+ // We can't use commas when we're formatting the dataFormatted number
619
+ // Example: commas -> 12,000; abbreviated -> 12k (correct); abbreviated & commas -> 12 (incorrect)
620
+ if (axis === 'left' && commas && abbreviated) {
621
+ num = num
622
+ } else {
623
+ num = num.toLocaleString('en-US', stringFormattingOptions)
624
+ }
515
625
  let result = ''
516
626
 
627
+ if (abbreviated && axis === 'left') {
628
+ num = formatSuffix(parseFloat(num)).replace('G', 'B')
629
+ }
630
+
631
+ if (abbreviated && axis === 'bottom') {
632
+ num = formatSuffix(parseFloat(num)).replace('G', 'B')
633
+ }
634
+
517
635
  if (prefix && axis !== 'right') {
518
636
  result += prefix
519
637
  }
520
638
 
521
- if (config.dataFormat.rightPrefix && axis === 'right') {
522
- result += config.dataFormat.rightPrefix
639
+ if (rightPrefix && axis === 'right') {
640
+ result += rightPrefix
523
641
  }
524
642
 
525
643
  result += num
526
644
 
527
- if (config.dataFormat.suffix && axis !== 'right') {
528
- result += config.dataFormat.suffix
645
+ if (suffix && axis !== 'right') {
646
+ result += suffix
529
647
  }
530
648
 
531
- if (config.dataFormat.rightSuffix && axis === 'right') {
532
- result += config.dataFormat.rightSuffix
649
+ if (rightSuffix && axis === 'right') {
650
+ result += rightSuffix
533
651
  }
534
652
 
535
653
  return String(result)
536
654
  }
537
655
 
538
- // Destructure items from config for more readable JSX
539
- const { legend, title, description, visualizationType } = config
540
-
541
656
  // Select appropriate chart type
542
657
  const chartComponents = {
543
658
  'Paired Bar': <LinearChart />,
544
659
  Bar: <LinearChart />,
545
660
  Line: <LinearChart />,
546
661
  Combo: <LinearChart />,
547
- Pie: <PieChart />
548
- }
549
-
550
- const Filters = () => {
551
- const changeFilterActive = (index, value) => {
552
- let newFilters = config.filters
553
-
554
- newFilters[index].active = value
555
-
556
- setConfig({ ...config, filters: newFilters })
557
-
558
- setFilteredData(filterData(newFilters, excludedData))
559
- }
560
-
561
- const announceChange = text => {}
562
-
563
- let filterList = ''
564
- if (config.filters) {
565
- filterList = config.filters.map((singleFilter, index) => {
566
- const values = []
567
- const sortAsc = (a, b) => {
568
- return a.toString().localeCompare(b.toString(), 'en', { numeric: true })
569
- }
570
-
571
- const sortDesc = (a, b) => {
572
- return b.toString().localeCompare(a.toString(), 'en', { numeric: true })
573
- }
574
-
575
- if (!singleFilter.order || singleFilter.order === '') {
576
- singleFilter.order = 'asc'
577
- }
578
-
579
- if (singleFilter.order === 'desc') {
580
- singleFilter.values = singleFilter.values.sort(sortDesc)
581
- }
582
-
583
- if (singleFilter.order === 'asc') {
584
- singleFilter.values = singleFilter.values.sort(sortAsc)
585
- }
586
-
587
- singleFilter.values.forEach((filterOption, index) => {
588
- values.push(
589
- <option key={index} value={filterOption}>
590
- {filterOption}
591
- </option>
592
- )
593
- })
594
-
595
- return (
596
- <div className='single-filter' key={index}>
597
- <label htmlFor={`filter-${index}`}>{singleFilter.label}</label>
598
- <select
599
- id={`filter-${index}`}
600
- className='filter-select'
601
- data-index='0'
602
- value={singleFilter.active}
603
- onChange={val => {
604
- changeFilterActive(index, val.target.value)
605
- announceChange(`Filter ${singleFilter.label} value has been changed to ${val.target.value}, please reference the data table to see updated values.`)
606
- }}
607
- >
608
- {values}
609
- </select>
610
- </div>
611
- )
612
- })
613
- }
614
-
615
- return <section className='filters-section'>{filterList}</section>
662
+ Pie: <PieChart />,
663
+ 'Box Plot': <LinearChart />
616
664
  }
617
665
 
618
666
  const missingRequiredSections = () => {
@@ -656,11 +704,10 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
656
704
  {/* Filters */}
657
705
  {config.filters && !externalFilters && <Filters />}
658
706
  {/* Visualization */}
659
- {config?.introText && <section className="introText">{parse(config.introText)}</section>}
707
+ {config?.introText && <section className='introText'>{parse(config.introText)}</section>}
660
708
  <div
661
- className={`chart-container ${config.legend.position==='bottom'? "bottom":""
662
- }${config.legend.hide ? " legend-hidden" : ""
663
- }${lineDatapointClass}${barBorderClass} ${contentClasses.join(' ')}`}
709
+ style={{ marginBottom: config.legend.position !== 'bottom' && config.orientation === 'horizontal' ? `${config.runtime.xAxis.size}px` : '0px' }}
710
+ className={`chart-container ${config.legend.position === 'bottom' ? 'bottom' : ''}${config.legend.hide ? ' legend-hidden' : ''}${lineDatapointClass}${barBorderClass} ${contentClasses.join(' ')}`}
664
711
  >
665
712
  {/* All charts except sparkline */}
666
713
  {config.visualizationType !== 'Spark Line' && chartComponents[visualizationType]}
@@ -686,10 +733,17 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
686
733
  {link && link}
687
734
  {/* Description */}
688
735
  {description && config.visualizationType !== 'Spark Line' && <div className='subtext'>{parse(description)}</div>}
689
- {/* Data Table */}
690
736
 
737
+ {/* buttons */}
738
+ <CoveMediaControls.Section classes={['download-buttons']}>
739
+ {config.table.showDownloadImgButton && <CoveMediaControls.Button text='Download Image' title='Download Chart as Image' type='image' state={config} elementToCapture={imageId} />}
740
+ {config.table.showDownloadPdfButton && <CoveMediaControls.Button text='Download PDF' title='Download Chart as PDF' type='pdf' state={config} elementToCapture={imageId} />}
741
+ </CoveMediaControls.Section>
742
+
743
+ {/* Data Table */}
691
744
  {config.xAxis.dataKey && config.table.show && config.visualizationType !== 'Spark Line' && <DataTable />}
692
745
  {config?.footnotes && <section className='footnotes'>{parse(config.footnotes)}</section>}
746
+ {/* show pdf or image button */}
693
747
  </div>
694
748
  )}
695
749
  </>
@@ -729,7 +783,9 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
729
783
  legend,
730
784
  setSeriesHighlight,
731
785
  dynamicLegendItems,
732
- setDynamicLegendItems
786
+ setDynamicLegendItems,
787
+ filterData,
788
+ imageId
733
789
  }
734
790
 
735
791
  const classes = ['cdc-open-viz-module', 'type-chart', `${currentViewport}`, `font-${config.fontSize}`, `${config.theme}`]
@@ -740,7 +796,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
740
796
 
741
797
  return (
742
798
  <Context.Provider value={contextValues}>
743
- <div className={`${classes.join(' ')}`} ref={outerContainerRef} data-lollipop={config.isLollipopChart}>
799
+ <div className={`${classes.join(' ')}`} ref={outerContainerRef} data-lollipop={config.isLollipopChart} data-download-id={imageId}>
744
800
  {body}
745
801
  </div>
746
802
  </Context.Provider>