@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
package/src/CdcChart.tsx CHANGED
@@ -1,42 +1,47 @@
1
1
  import React, { useState, useEffect, useCallback } from 'react';
2
2
 
3
3
  // IE11
4
- import 'core-js/stable'
5
- import ResizeObserver from 'resize-observer-polyfill'
6
- import 'whatwg-fetch'
4
+ import 'core-js/stable';
5
+ import ResizeObserver from 'resize-observer-polyfill';
6
+ import 'whatwg-fetch';
7
7
 
8
- import { LegendOrdinal, LegendItem, LegendLabel } from '@visx/legend';
8
+ // External Libraries
9
9
  import { scaleOrdinal } from '@visx/scale';
10
10
  import ParentSize from '@visx/responsive/lib/components/ParentSize';
11
-
12
11
  import { timeParse, timeFormat } from 'd3-time-format';
13
12
  import Papa from 'papaparse';
14
13
  import parse from 'html-react-parser';
15
14
 
16
- import Loading from '@cdc/core/components/Loading';
17
- import DataTransform from '@cdc/core/components/DataTransform';
18
- import getViewport from '@cdc/core/helpers/getViewport';
19
15
 
16
+ // Primary Components
17
+ import Context from './context';
20
18
  import PieChart from './components/PieChart';
21
19
  import LinearChart from './components/LinearChart';
22
- import DataTable from './components/DataTable';
23
- import Context from './context';
24
- import defaults from './data/initial-state';
25
20
 
26
- import EditorPanel from './components/EditorPanel';
27
- import numberFromString from '@cdc/core/helpers/numberFromString'
28
- import LegendCircle from '@cdc/core/components/LegendCircle';
29
21
  import {colorPalettesChart as colorPalettes} from '../../core/data/colorPalettes';
30
22
 
31
23
  import { publish, subscribe, unsubscribe } from '@cdc/core/helpers/events';
32
24
 
25
+ import useDataVizClasses from '@cdc/core/helpers/useDataVizClasses';
26
+
33
27
  import SparkLine from './components/SparkLine';
28
+ import Legend from './components/Legend';
29
+ import DataTable from './components/DataTable';
30
+ import defaults from './data/initial-state';
31
+ import EditorPanel from './components/EditorPanel';
32
+ import Loading from '@cdc/core/components/Loading';
33
+
34
+ // helpers
35
+ import numberFromString from '@cdc/core/helpers/numberFromString'
36
+ import getViewport from '@cdc/core/helpers/getViewport';
37
+ import { DataTransform } from '@cdc/core/helpers/DataTransform';
38
+ import cacheBustingString from '@cdc/core/helpers/cacheBustingString';
34
39
 
35
40
  import './scss/main.scss';
36
41
 
37
42
  export default function CdcChart(
38
- { configUrl, config: configObj, isEditor = false, isDashboard = false, setConfig: setParentConfig, setEditing, hostname} :
39
- { configUrl?: string, config?: any, isEditor?: boolean, isDashboard?: boolean, setConfig?, setEditing?, hostname? }
43
+ { configUrl, config: configObj, isEditor = false, isDashboard = false, setConfig: setParentConfig, setEditing, hostname,link} :
44
+ { configUrl?: string, config?: any, isEditor?: boolean, isDashboard?: boolean, setConfig?, setEditing?, hostname?,link?:any }
40
45
  ) {
41
46
 
42
47
  const transform = new DataTransform();
@@ -52,21 +57,47 @@ export default function CdcChart(
52
57
  const [seriesHighlight, setSeriesHighlight] = useState<Array<String>>([]);
53
58
  const [currentViewport, setCurrentViewport] = useState<String>('lg');
54
59
  const [dimensions, setDimensions] = useState<Array<Number>>([]);
55
- const [parentElement, setParentElement] = useState(false)
56
- const [externalFilters, setExternalFilters] = useState([]);
60
+ const [externalFilters, setExternalFilters] = useState(null);
57
61
  const [container, setContainer] = useState()
58
62
  const [coveLoadedEventRan, setCoveLoadedEventRan] = useState(false)
63
+ const [dynamicLegendItems, setDynamicLegendItems] = useState([])
59
64
 
60
65
  const legendGlyphSize = 15;
61
66
  const legendGlyphSizeHalf = legendGlyphSize / 2;
62
67
 
68
+ const {
69
+ barBorderClass,
70
+ lineDatapointClass,
71
+ contentClasses,
72
+ innerContainerClasses,
73
+ sparkLineStyles
74
+ } = useDataVizClasses(config)
75
+
63
76
  const handleChartTabbing = config.showSidebar ? `#legend` : config?.title ? `#dataTableSection__${config.title.replace(/\s/g, '')}` : `#dataTableSection`
64
77
 
65
- const cacheBustingString = () => {
66
- const round = 1000 * 60 * 15;
67
- const date = new Date();
68
- return new Date(date.getTime() - (date.getTime() % round)).toISOString();
78
+
79
+
80
+
81
+ const handleChartAriaLabels = (state, testing = false) => {
82
+ if(testing) console.log(`handleChartAriaLabels Testing On:`, state);
83
+ try {
84
+ if(!state.visualizationType) throw Error('handleChartAriaLabels: no visualization type found in state');
85
+ let ariaLabel = '';
86
+
87
+ if(state.visualizationType) {
88
+ ariaLabel += `${state.visualizationType} chart`
89
+ }
90
+
91
+ if(state.title && state.visualizationType) {
92
+ ariaLabel += ` with the title: ${state.title}`
93
+ }
94
+
95
+ return ariaLabel;
96
+ } catch(e) {
97
+ console.error(e.message)
98
+ }
69
99
  }
100
+
70
101
  const loadConfig = async () => {
71
102
  let response = configObj || await (await fetch(configUrl)).json();
72
103
 
@@ -117,6 +148,7 @@ export default function CdcChart(
117
148
  updateConfig(newConfig, data);
118
149
  }
119
150
 
151
+
120
152
  const updateConfig = (newConfig, dataOverride = undefined) => {
121
153
 
122
154
  let data = dataOverride || stateData
@@ -185,7 +217,6 @@ export default function CdcChart(
185
217
  newConfig.filters[index].active = filterValues[0];
186
218
 
187
219
  });
188
-
189
220
  currentData = filterData(newConfig.filters, newExcludedData);
190
221
  setFilteredData(currentData);
191
222
  }
@@ -196,6 +227,7 @@ export default function CdcChart(
196
227
  newConfig.runtime.seriesLabelsAll = [];
197
228
  newConfig.runtime.originalXAxis = newConfig.xAxis;
198
229
 
230
+
199
231
  if (newConfig.visualizationType === 'Pie') {
200
232
  newConfig.runtime.seriesKeys = (dataOverride || data).map(d => d[newConfig.xAxis.dataKey]);
201
233
  newConfig.runtime.seriesLabelsAll = newConfig.runtime.seriesKeys;
@@ -214,7 +246,7 @@ export default function CdcChart(
214
246
  if(series.type === 'Bar'){
215
247
  newConfig.runtime.barSeriesKeys.push(series.dataKey);
216
248
  }
217
- if(series.type === 'Line'){
249
+ if(series.type === 'Line' || series.type === 'dashed-sm' || series.type === 'dashed-md' || series.type === 'dashed-lg'){
218
250
  newConfig.runtime.lineSeriesKeys.push(series.dataKey);
219
251
  }
220
252
  });
@@ -232,20 +264,6 @@ export default function CdcChart(
232
264
  newConfig.runtime.uniqueId = Date.now();
233
265
  newConfig.runtime.editorErrorMessage = newConfig.visualizationType === 'Pie' && !newConfig.yAxis.dataKey ? 'Data Key property in Y Axis section must be set for pie charts.' : '';
234
266
 
235
- // Check for duplicate x axis values in data
236
- if(!currentData) currentData = newExcludedData;
237
-
238
- let uniqueXValues = {};
239
-
240
- if(newConfig.visualizationType !== 'Paired Bar') {
241
- for(let i = 0; i < currentData.length; i++) {
242
- if(uniqueXValues[currentData[i][newConfig.xAxis.dataKey]]){
243
- newConfig.runtime.editorErrorMessage = 'Duplicate keys in data. Try adding a filter.';
244
- } else {
245
- uniqueXValues[currentData[i][newConfig.xAxis.dataKey]] = true;
246
- }
247
- }
248
- }
249
267
  setConfig(newConfig);
250
268
  };
251
269
 
@@ -259,6 +277,7 @@ export default function CdcChart(
259
277
  add = false;
260
278
  }
261
279
  });
280
+
262
281
  if(add) filteredData.push(row);
263
282
  });
264
283
  return filteredData;
@@ -375,7 +394,7 @@ export default function CdcChart(
375
394
  */
376
395
  useEffect(() => {
377
396
 
378
- if(externalFilters[0]) {
397
+ if(externalFilters && externalFilters[0]) {
379
398
  const hasActiveProperty = externalFilters[0].hasOwnProperty('active')
380
399
 
381
400
  if(!hasActiveProperty) {
@@ -386,7 +405,7 @@ export default function CdcChart(
386
405
  }
387
406
  }
388
407
 
389
- if(externalFilters.length > 0 && externalFilters.length > 0 && externalFilters[0].hasOwnProperty('active')) {
408
+ if(externalFilters && externalFilters.length > 0 && externalFilters.length > 0 && externalFilters[0].hasOwnProperty('active')) {
390
409
  let newConfigHere = {...config, filters: externalFilters }
391
410
  setConfig(newConfigHere)
392
411
  setFilteredData(filterData(externalFilters, excludedData));
@@ -407,6 +426,7 @@ export default function CdcChart(
407
426
  if(stateData && config.xAxis && config.runtime.seriesKeys) {
408
427
  let palette = config.customColors || colorPalettes[config.palette]
409
428
  let numberOfKeys = config.runtime.seriesKeys.length
429
+ let newColorScale;
410
430
 
411
431
  while(numberOfKeys > palette.length) {
412
432
  palette = palette.concat(palette);
@@ -414,7 +434,7 @@ export default function CdcChart(
414
434
 
415
435
  palette = palette.slice(0, numberOfKeys);
416
436
 
417
- const newColorScale = () => scaleOrdinal({
437
+ newColorScale = () => scaleOrdinal({
418
438
  domain: config.runtime.seriesLabelsAll,
419
439
  range: palette,
420
440
  });
@@ -433,7 +453,7 @@ export default function CdcChart(
433
453
  const newSeriesHighlight = [];
434
454
 
435
455
  // If we're highlighting all the series, reset them
436
- if(seriesHighlight.length + 1 === config.runtime.seriesKeys.length) {
456
+ if( (seriesHighlight.length + 1 === config.runtime.seriesKeys.length) && !config.legend.dynamicLegend) {
437
457
  highlightReset()
438
458
  return
439
459
  }
@@ -457,14 +477,18 @@ export default function CdcChart(
457
477
  } else {
458
478
  newSeriesHighlight.push(newHighlight);
459
479
  }
460
-
461
480
  setSeriesHighlight(newSeriesHighlight);
462
481
  };
463
482
 
464
483
  // Called on reset button click, unhighlights all data series
465
484
  const highlightReset = () => {
466
- setSeriesHighlight([]);
485
+ if(config.legend.dynamicLegend && dynamicLegendItems) {
486
+ setSeriesHighlight(dynamicLegendItems.map( item => item.text ))
487
+ } else {
488
+ setSeriesHighlight([]);
489
+ }
467
490
  }
491
+
468
492
  const section = config.orientation ==='horizontal' ? 'yAxis' :'xAxis';
469
493
 
470
494
  const parseDate = (dateString: string) => {
@@ -484,11 +508,10 @@ export default function CdcChart(
484
508
 
485
509
  // Format numeric data based on settings in config
486
510
  const formatNumber = (num) => {
487
- if(num === undefined || num ===null) return "";
488
511
  // check if value contains comma and remove it. later will add comma below.
489
512
  if(String(num).indexOf(',') !== -1) num = num.replaceAll(',', '');
490
513
  // if num is NaN return num
491
- if(isNaN(num)) return num ;
514
+ if(isNaN(num)|| !num) return num ;
492
515
 
493
516
  let original = num;
494
517
  let prefix = config.dataFormat.prefix;
@@ -500,8 +523,8 @@ export default function CdcChart(
500
523
  };
501
524
 
502
525
  num = numberFromString(num);
503
-
504
- if(isNaN(num)) {
526
+
527
+ if (isNaN(num)) {
505
528
  config.runtime.editorErrorMessage = `Unable to parse number from data ${original}. Try reviewing your data and selections in the Data Series section.`;
506
529
  return original
507
530
  }
@@ -510,8 +533,7 @@ export default function CdcChart(
510
533
  if (config.dataCutoff){
511
534
  let cutoff = numberFromString(config.dataCutoff)
512
535
 
513
- if(num < cutoff) {
514
- prefix = '< ' + (prefix || '');
536
+ if(num < cutoff) {
515
537
  num = cutoff;
516
538
  }
517
539
  }
@@ -543,79 +565,6 @@ export default function CdcChart(
543
565
  'Pie' : <PieChart />,
544
566
  }
545
567
 
546
- // JSX for Legend
547
- const Legend = () => {
548
-
549
- let containerClasses = ['legend-container']
550
- let innerClasses = ['legend-container__inner'];
551
-
552
- if(config.legend.position === "left") {
553
- containerClasses.push('left')
554
- }
555
-
556
- if(config.legend.reverseLabelOrder) {
557
- innerClasses.push('d-flex')
558
- innerClasses.push('flex-column-reverse')
559
- }
560
-
561
- return (
562
- <aside id="legend" className={containerClasses.join(' ')} role="region" aria-label="legend" tabIndex={0}>
563
- {legend.label && <h2>{legend.label}</h2>}
564
- <LegendOrdinal
565
- scale={colorScale}
566
- itemDirection="row"
567
- labelMargin="0 20px 0 0"
568
- shapeMargin="0 10px 0"
569
- >
570
- {labels => (
571
- <div className={innerClasses.join(' ')}>
572
- {labels.map((label, i) => {
573
- let className = 'legend-item'
574
- let itemName:any = label.datum
575
-
576
- // Filter excluded data keys from legend
577
- if (config.exclusions.active && config.exclusions.keys?.includes(itemName)) {
578
- return
579
- }
580
-
581
- if(config.runtime.seriesLabels){
582
- let index = config.runtime.seriesLabelsAll.indexOf(itemName)
583
- itemName = config.runtime.seriesKeys[index]
584
- }
585
-
586
- if( seriesHighlight.length > 0 && false === seriesHighlight.includes( itemName ) ) {
587
- className += ' inactive'
588
- }
589
-
590
- return (
591
- <LegendItem
592
- className={className}
593
- tabIndex={0}
594
- key={`legend-quantile-${i}`}
595
- onKeyPress={(e) => {
596
- if (e.key === 'Enter') {
597
- highlight(label);
598
- }
599
- }}
600
- onClick={() => {
601
- highlight(label);
602
- }}
603
- >
604
- <LegendCircle fill={label.value} />
605
- <LegendLabel align="left" margin="0 0 0 4px">
606
- {label.text}
607
- </LegendLabel>
608
- </LegendItem>
609
- )
610
- })}
611
- {seriesHighlight.length > 0 && <button className={`legend-reset ${config.theme}`} onClick={highlightReset}>Reset</button>}
612
- </div>
613
- )}
614
- </LegendOrdinal>
615
- </aside>
616
- )
617
- }
618
-
619
568
  const Filters = () => {
620
569
  const changeFilterActive = (index, value) => {
621
570
  let newFilters = config.filters;
@@ -703,52 +652,48 @@ export default function CdcChart(
703
652
  return false;
704
653
  };
705
654
 
706
- let innerContainerClasses = ['cove-component__inner']
707
- config.title && innerContainerClasses.push('component--has-title')
708
- config.subtext && innerContainerClasses.push('component--has-subtext')
709
- config.biteStyle && innerContainerClasses.push(`bite__style--${config.biteStyle}`)
710
- config.general?.isCompactStyle && innerContainerClasses.push(`component--isCompactStyle`)
711
-
712
- let contentClasses = ['cove-component__content'];
713
- config.visualizationType === 'Spark Line' && contentClasses.push('sparkline')
714
- !config.visual?.border && contentClasses.push('no-borders');
715
- config.visual?.borderColorTheme && contentClasses.push('component--has-borderColorTheme');
716
- config.visual?.accent && contentClasses.push('component--has-accent');
717
- config.visual?.background && contentClasses.push('component--has-background');
718
- config.visual?.hideBackgroundColor && contentClasses.push('component--hideBackgroundColor');
719
-
720
-
721
-
722
655
  // Prevent render if loading
723
656
  let body = (<Loading />)
724
- let lineDatapointClass = ''
725
- let barBorderClass = ''
726
657
 
727
- let sparkLineStyles = {
728
- width: '100%',
729
- height: '100px',
730
- }
731
658
 
732
- if(false === loading) {
733
- if (config.lineDatapointStyle === "hover") { lineDatapointClass = ' chart-line--hover' }
734
- if (config.lineDatapointStyle === "always show") { lineDatapointClass = ' chart-line--always' }
735
- if (config.barHasBorder === "false") { barBorderClass = ' chart-bar--no-border' }
659
+ if(!loading) {
660
+
736
661
 
737
662
  body = (
738
663
  <>
739
664
  {isEditor && <EditorPanel />}
740
- {!missingRequiredSections() && !config.newViz && <div className={`cdc-chart-inner-container`}>
741
- <>
665
+ {!missingRequiredSections() && !config.newViz && (
666
+ <div className="cdc-chart-inner-container">
742
667
  {/* Title */}
743
- {title && <div role="heading" className={`chart-title ${config.theme} cove-component__header`} aria-level={2}>{parse(title)}</div>}
744
- <a id='skip-chart-container' className='cdcdataviz-sr-only-focusable' href={handleChartTabbing}>
668
+
669
+ {title && (
670
+ <div
671
+ role="heading"
672
+ className={`chart-title ${config.theme} cove-component__header`}
673
+ aria-level={2}
674
+ >
675
+ {config && (
676
+ <sup className="superTitle">{parse(config.superTitle || '')}</sup>
677
+ )}
678
+ <div>{parse(title)}</div>
679
+ </div>
680
+ )}
681
+ <a
682
+ id="skip-chart-container"
683
+ className="cdcdataviz-sr-only-focusable"
684
+ href={handleChartTabbing}
685
+ >
745
686
  Skip Over Chart Container
746
687
  </a>
747
688
  {/* Filters */}
748
- { (config.filters && !externalFilters ) && <Filters />}
689
+ {config.filters && !externalFilters && <Filters />}
749
690
  {/* Visualization */}
750
- <div className={`chart-container${config.legend.hide ? ' legend-hidden' : ''}${lineDatapointClass}${barBorderClass} ${contentClasses.join(' ')}`}>
751
-
691
+ {config?.introText && <section className="introText">{parse(config.introText)}</section>}
692
+ <div
693
+ className={`chart-container${
694
+ config.legend.hide ? " legend-hidden" : ""
695
+ }${lineDatapointClass}${barBorderClass} ${contentClasses.join(' ')}`}
696
+ >
752
697
  {/* All charts except sparkline */}
753
698
  {config.visualizationType !== "Spark Line" &&
754
699
  chartComponents[visualizationType]
@@ -770,20 +715,20 @@ export default function CdcChart(
770
715
  </>
771
716
  )
772
717
  }
773
-
774
- {/* Legend */}
775
- {(!config.legend.hide && config.visualizationType !== "Spark Line") && <Legend />}
776
-
718
+ {!config.legend.hide && config.visualizationType !== "Spark Line" && <Legend />}
777
719
  </div>
778
- </>
779
- {/* Description */}
780
- { (description && config.visualizationType !== "Spark Line") && <div className="subtext">{parse(description)}</div>}
781
- {/* Data Table */}
782
- { (config.xAxis.dataKey && config.table.show && config.visualizationType !== "Spark Line") && <DataTable />}
783
- </div>
784
- }
720
+ {/* Link */}
721
+ {link && link}
722
+ {/* Description */}
723
+ {description && config.visualizationType !== "Spark Line" && <div className="subtext">{parse(description)}</div>}
724
+ {/* Data Table */}
725
+
726
+ {config.xAxis.dataKey && config.table.show && config.visualizationType !== "Spark Line" && <DataTable />}
727
+ {config?.footnotes && <section className="footnotes">{parse(config.footnotes)}</section>}
728
+ </div>
729
+ )}
785
730
  </>
786
- )
731
+ );
787
732
  }
788
733
 
789
734
  const getXAxisData = (d: any) => config.runtime.xAxis.type === 'date' ? (parseDate(d[config.runtime.originalXAxis.dataKey])).getTime() : d[config.runtime.originalXAxis.dataKey];
@@ -793,6 +738,7 @@ export default function CdcChart(
793
738
  getXAxisData,
794
739
  getYAxisData,
795
740
  config,
741
+ setConfig,
796
742
  rawData: stateData ?? {},
797
743
  excludedData: excludedData,
798
744
  transformedData: filteredData || excludedData,
@@ -812,6 +758,13 @@ export default function CdcChart(
812
758
  missingRequiredSections,
813
759
  setEditing,
814
760
  setFilteredData,
761
+ handleChartAriaLabels,
762
+ highlight,
763
+ highlightReset,
764
+ legend,
765
+ setSeriesHighlight,
766
+ dynamicLegendItems,
767
+ setDynamicLegendItems
815
768
  }
816
769
 
817
770
  const classes = [