@cdc/chart 4.24.9 → 4.24.11

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 (95) hide show
  1. package/LICENSE +201 -0
  2. package/dist/cdcchart.js +45911 -41739
  3. package/examples/feature/boxplot/boxplot-data.json +88 -22
  4. package/examples/feature/boxplot/boxplot.json +540 -16
  5. package/examples/feature/boxplot/testing.csv +7 -7
  6. package/examples/feature/sankey/sankey-example-data.json +0 -1
  7. package/examples/private/test.json +20092 -0
  8. package/index.html +4 -4
  9. package/package.json +2 -2
  10. package/src/CdcChart.tsx +209 -188
  11. package/src/_stories/Chart.CustomColors.stories.tsx +19 -0
  12. package/src/_stories/Chart.DynamicSeries.stories.tsx +27 -0
  13. package/src/_stories/Chart.Legend.Gradient.stories.tsx +74 -0
  14. package/src/_stories/Chart.stories.tsx +30 -3
  15. package/src/_stories/ChartAxisLabels.stories.tsx +20 -0
  16. package/src/_stories/ChartAxisTitles.stories.tsx +53 -0
  17. package/src/_stories/ChartEditor.stories.tsx +27 -0
  18. package/src/_stories/ChartLine.Suppression.stories.tsx +25 -0
  19. package/src/_stories/ChartPrefixSuffix.stories.tsx +159 -0
  20. package/src/_stories/_mock/boxplot_multiseries.json +647 -0
  21. package/src/_stories/_mock/dynamic_series_bar_config.json +723 -0
  22. package/src/_stories/_mock/dynamic_series_config.json +979 -0
  23. package/src/_stories/_mock/horizontal_bar.json +257 -0
  24. package/src/_stories/_mock/large_x_axis_labels.json +261 -0
  25. package/src/_stories/_mock/paired-bar.json +262 -0
  26. package/src/_stories/_mock/pie_with_data.json +255 -0
  27. package/{examples/feature/scatterplot/scatterplot.json → src/_stories/_mock/scatterplot_mock.json} +62 -92
  28. package/src/_stories/_mock/simplified_line.json +1510 -0
  29. package/src/_stories/_mock/suppression_mock.json +1549 -0
  30. package/src/components/Annotations/components/AnnotationDraggable.tsx +0 -3
  31. package/src/components/Annotations/components/AnnotationDropdown.tsx +1 -1
  32. package/src/components/Axis/Categorical.Axis.tsx +22 -4
  33. package/src/components/BarChart/components/BarChart.Horizontal.tsx +95 -16
  34. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +41 -17
  35. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +43 -9
  36. package/src/components/BarChart/components/BarChart.Vertical.tsx +123 -47
  37. package/src/components/BarChart/helpers/index.ts +23 -5
  38. package/src/components/BoxPlot/BoxPlot.tsx +189 -0
  39. package/src/components/BrushChart.tsx +3 -2
  40. package/src/components/DeviationBar.jsx +58 -8
  41. package/src/components/EditorPanel/EditorPanel.tsx +127 -102
  42. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +11 -28
  43. package/src/components/EditorPanel/components/Panels/Panel.BoxPlot.tsx +51 -6
  44. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +21 -4
  45. package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +40 -9
  46. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +3 -3
  47. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +121 -56
  48. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +296 -35
  49. package/src/components/EditorPanel/components/panels.scss +4 -6
  50. package/src/components/EditorPanel/editor-panel.scss +0 -8
  51. package/src/components/EditorPanel/helpers/tests/updateFieldRankByValue.test.ts +38 -0
  52. package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +42 -0
  53. package/src/components/EditorPanel/useEditorPermissions.ts +16 -1
  54. package/src/components/ForestPlot/ForestPlot.tsx +2 -3
  55. package/src/components/ForestPlot/ForestPlotProps.ts +2 -0
  56. package/src/components/Legend/Legend.Component.tsx +23 -24
  57. package/src/components/Legend/Legend.Suppression.tsx +25 -20
  58. package/src/components/Legend/Legend.tsx +16 -18
  59. package/src/components/Legend/helpers/index.ts +16 -19
  60. package/src/components/LegendWrapper.tsx +3 -1
  61. package/src/components/LineChart/components/LineChart.Circle.tsx +10 -0
  62. package/src/components/LineChart/helpers.ts +48 -43
  63. package/src/components/LineChart/index.tsx +88 -82
  64. package/src/components/LinearChart.tsx +747 -562
  65. package/src/components/PairedBarChart.jsx +50 -10
  66. package/src/components/PieChart/PieChart.tsx +1 -6
  67. package/src/components/Regions/components/Regions.tsx +33 -19
  68. package/src/components/Sankey/index.tsx +50 -32
  69. package/src/components/Sankey/sankey.scss +6 -5
  70. package/src/components/Sankey/useSankeyAlert.tsx +60 -0
  71. package/src/components/ScatterPlot/ScatterPlot.jsx +20 -4
  72. package/src/components/ZoomBrush.tsx +25 -6
  73. package/src/coreStyles_chart.scss +3 -0
  74. package/src/data/initial-state.js +8 -10
  75. package/src/helpers/configHelpers.ts +28 -0
  76. package/src/helpers/handleRankByValue.ts +15 -0
  77. package/src/helpers/sizeHelpers.ts +25 -0
  78. package/src/helpers/tests/handleRankByValue.test.ts +37 -0
  79. package/src/helpers/tests/sizeHelpers.test.ts +80 -0
  80. package/src/hooks/useColorPalette.js +10 -2
  81. package/src/hooks/useLegendClasses.ts +13 -22
  82. package/src/hooks/useMinMax.ts +27 -13
  83. package/src/hooks/useReduceData.ts +43 -10
  84. package/src/hooks/useScales.ts +87 -38
  85. package/src/hooks/useTooltip.tsx +62 -53
  86. package/src/index.jsx +1 -0
  87. package/src/scss/DataTable.scss +5 -4
  88. package/src/scss/main.scss +57 -70
  89. package/src/types/ChartConfig.ts +43 -34
  90. package/src/types/ChartContext.ts +22 -15
  91. package/src/types/ForestPlot.ts +8 -0
  92. package/src/_stories/Chart.Legend.Gradient.tsx +0 -19
  93. package/src/_stories/ChartBrush.stories.tsx +0 -19
  94. package/src/components/BoxPlot/BoxPlot.jsx +0 -111
  95. package/src/components/LinearChart.jsx +0 -817
package/src/CdcChart.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect, useCallback, useRef, useId } from 'react'
1
+ import React, { useState, useEffect, useCallback, useRef, useId, useMemo } from 'react'
2
2
 
3
3
  // IE11
4
4
  import ResizeObserver from 'resize-observer-polyfill'
@@ -9,6 +9,7 @@ import Button from '@cdc/core/components/elements/Button'
9
9
 
10
10
  //types
11
11
  import { DimensionsType } from '@cdc/core/types/Dimensions'
12
+ import { type DashboardConfig } from '@cdc/dashboard/src/types/DashboardConfig'
12
13
 
13
14
  // External Libraries
14
15
  import { scaleOrdinal } from '@visx/scale'
@@ -34,10 +35,10 @@ import EditorPanel from './components/EditorPanel'
34
35
  import { abbreviateNumber } from './helpers/abbreviateNumber'
35
36
  import { handleChartTabbing } from './helpers/handleChartTabbing'
36
37
  import { getQuartiles } from './helpers/getQuartiles'
37
- import { sortAsc, sortDesc } from './helpers/sort'
38
38
  import { handleChartAriaLabels } from './helpers/handleChartAriaLabels'
39
39
  import { lineOptions } from './helpers/lineOptions'
40
40
  import { handleLineType } from './helpers/handleLineType'
41
+ import { handleRankByValue } from './helpers/handleRankByValue'
41
42
  import { generateColorsArray } from './helpers/generateColorsArray'
42
43
  import Loading from '@cdc/core/components/Loading'
43
44
  import Filters from '@cdc/core/components/Filters'
@@ -45,7 +46,6 @@ import MediaControls from '@cdc/core/components/MediaControls'
45
46
  import Annotation from './components/Annotations'
46
47
 
47
48
  // Helpers
48
- import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
49
49
  import { publish, subscribe, unsubscribe } from '@cdc/core/helpers/events'
50
50
  import useDataVizClasses from '@cdc/core/helpers/useDataVizClasses'
51
51
  import numberFromString from '@cdc/core/helpers/numberFromString'
@@ -54,23 +54,42 @@ import { DataTransform } from '@cdc/core/helpers/DataTransform'
54
54
  import cacheBustingString from '@cdc/core/helpers/cacheBustingString'
55
55
  import isNumber from '@cdc/core/helpers/isNumber'
56
56
  import coveUpdateWorker from '@cdc/core/helpers/coveUpdateWorker'
57
- import { getQueryStringFilterValue } from '@cdc/core/helpers/queryStringUtils'
58
57
  import { isConvertLineToBarGraph } from './helpers/isConvertLineToBarGraph'
58
+ import { isLegendWrapViewport } from '@cdc/core/helpers/viewports'
59
59
 
60
60
  import './scss/main.scss'
61
61
  // load both then config below determines which to use
62
62
  import DataTable from '@cdc/core/components/DataTable'
63
+ import type { TableConfig } from '@cdc/core/components/DataTable/types/TableConfig'
63
64
  import { getFileExtension } from '@cdc/core/helpers/getFileExtension'
64
65
  import Title from '@cdc/core/components/ui/Title'
65
- import { ChartConfig } from './types/ChartConfig'
66
+ import { AllChartsConfig, ChartConfig } from './types/ChartConfig'
66
67
  import { Label } from './types/Label'
67
68
  import { type ViewportSize } from './types/ChartConfig'
68
69
  import { isSolrCsv, isSolrJson } from '@cdc/core/helpers/isSolr'
69
70
  import SkipTo from '@cdc/core/components/elements/SkipTo'
70
71
  import { filterVizData } from '@cdc/core/helpers/filterVizData'
71
72
  import LegendWrapper from './components/LegendWrapper'
72
-
73
- export default function CdcChart({
73
+ import _ from 'lodash'
74
+ import { addValuesToFilters } from '@cdc/core/helpers/addValuesToFilters'
75
+ import { Runtime } from '@cdc/core/types/Runtime'
76
+ import { Pivot } from '@cdc/core/types/Table'
77
+
78
+ interface CdcChartProps {
79
+ configUrl?: string
80
+ config?: ChartConfig
81
+ isEditor?: boolean
82
+ isDebug?: boolean
83
+ isDashboard?: boolean
84
+ setConfig?: (config: ChartConfig) => void
85
+ setEditing?: (editing: boolean) => void
86
+ hostname?: string
87
+ link?: string
88
+ setSharedFilter?: (filter: any) => void
89
+ setSharedFilterValue?: (value: any) => void
90
+ dashboardConfig?: DashboardConfig
91
+ }
92
+ const CdcChart = ({
74
93
  configUrl,
75
94
  config: configObj,
76
95
  isEditor = false,
@@ -83,12 +102,13 @@ export default function CdcChart({
83
102
  setSharedFilter,
84
103
  setSharedFilterValue,
85
104
  dashboardConfig
86
- }) {
105
+ }: CdcChartProps) => {
87
106
  const transform = new DataTransform()
88
107
  const [loading, setLoading] = useState(true)
108
+ const svgRef = useRef(null)
89
109
  const [colorScale, setColorScale] = useState(null)
90
110
  const [config, setConfig] = useState<ChartConfig>({} as ChartConfig)
91
- const [stateData, setStateData] = useState(config.data || [])
111
+ const [stateData, setStateData] = useState(_.cloneDeep(configObj?.data) || [])
92
112
  const [excludedData, setExcludedData] = useState<Record<string, number>[] | undefined>(undefined)
93
113
  const [filteredData, setFilteredData] = useState<Record<string, any>[] | undefined>(undefined)
94
114
  const [seriesHighlight, setSeriesHighlight] = useState<string[]>(
@@ -108,19 +128,10 @@ export default function CdcChart({
108
128
  isBrushing: false
109
129
  })
110
130
 
111
- let [width] = dimensions
112
- const useVertical = config.orientation === 'vertical'
113
- const useMobileVertical = config.heights?.mobileVertical && ['xs', 'xxs'].includes(currentViewport)
114
- const responsiveVertical = useMobileVertical ? 'mobileVertical' : 'vertical'
115
- const renderedOrientation = useVertical ? responsiveVertical : 'horizontal'
116
- let height = config.aspectRatio ? width * config.aspectRatio : config?.heights?.[renderedOrientation]
117
- if (config.visualizationType === 'Pie') height = config?.heights?.[renderedOrientation]
118
- height = height + Number(config?.xAxis?.size) + 45
119
-
120
- type Config = typeof config
121
- let legendMemo = useRef(new Map()) // map collection
122
- let innerContainerRef = useRef()
131
+ const { description, visualizationType } = config
132
+
123
133
  const legendRef = useRef(null)
134
+ const parentRef = useRef(null)
124
135
 
125
136
  const handleDragStateChange = isDragging => {
126
137
  setIsDraggingAnnotation(isDragging)
@@ -129,7 +140,7 @@ export default function CdcChart({
129
140
  if (isDebug) console.log('Chart config, isEditor', config, isEditor)
130
141
 
131
142
  // Destructure items from config for more readable JSX
132
- let { legend, title, description, visualizationType } = config
143
+ let { legend, title } = config
133
144
 
134
145
  // set defaults on titles if blank AND only in editor
135
146
  if (isEditor) {
@@ -138,7 +149,7 @@ export default function CdcChart({
138
149
 
139
150
  if (config.table && (!config.table?.label || config.table?.label === '')) config.table.label = 'Data Table'
140
151
 
141
- const { barBorderClass, lineDatapointClass, contentClasses, sparkLineStyles } = useDataVizClasses(config)
152
+ const { lineDatapointClass, contentClasses, sparkLineStyles } = useDataVizClasses(config)
142
153
  const legendId = useId()
143
154
 
144
155
  const checkLineToBarGraph = () => {
@@ -201,6 +212,8 @@ export default function CdcChart({
201
212
 
202
213
  Object.assign(data, { urlFiltered: true })
203
214
 
215
+ data = handleRankByValue(data, config)
216
+
204
217
  updateConfig({ ...config, runtimeDataUrl: dataUrlFinal, data, formattedData: data })
205
218
 
206
219
  if (data) {
@@ -212,7 +225,7 @@ export default function CdcChart({
212
225
  }
213
226
 
214
227
  const loadConfig = async () => {
215
- let response = configObj || (await (await fetch(configUrl)).json())
228
+ const response = _.cloneDeep(configObj) || (await (await fetch(configUrl)).json())
216
229
 
217
230
  // If data is included through a URL, fetch that and store
218
231
  let data: any[] = response.data || []
@@ -263,6 +276,8 @@ export default function CdcChart({
263
276
  data = transform.developerStandardize(data, response.dataDescription)
264
277
  }
265
278
 
279
+ data = handleRankByValue(data, response)
280
+
266
281
  if (data) {
267
282
  setStateData(data)
268
283
  setExcludedData(data)
@@ -276,18 +291,7 @@ export default function CdcChart({
276
291
  }
277
292
  }
278
293
  let newConfig = { ...defaults, ...response }
279
- if (newConfig.filters) {
280
- newConfig.filters.forEach((filter, index) => {
281
- const queryStringFilterValue = getQueryStringFilterValue(filter)
282
- if (queryStringFilterValue) {
283
- newConfig.filters[index].active = queryStringFilterValue
284
- }
285
- })
286
- }
287
294
 
288
- if (newConfig.visualizationType === 'Box Plot') {
289
- newConfig.legend.hide = true
290
- }
291
295
  if (undefined === newConfig.table.show) newConfig.table.show = !isDashboard
292
296
 
293
297
  newConfig.series.forEach(series => {
@@ -306,9 +310,12 @@ export default function CdcChart({
306
310
  updateConfig(processedConfig, data)
307
311
  }
308
312
 
309
- const updateConfig = (newConfig, dataOverride?: any[]) => {
313
+ const updateConfig = (_config: AllChartsConfig, dataOverride?: any[]) => {
314
+ const newConfig = _.cloneDeep(_config)
310
315
  let data = dataOverride || stateData
311
316
 
317
+ data = handleRankByValue(data, newConfig)
318
+
312
319
  // Deeper copy
313
320
  Object.keys(defaults).forEach(key => {
314
321
  if (newConfig[key] && 'object' === typeof newConfig[key] && !Array.isArray(newConfig[key])) {
@@ -356,28 +363,8 @@ export default function CdcChart({
356
363
  // After data is grabbed, loop through and generate filter column values if there are any
357
364
  let currentData: any[] = []
358
365
  if (newConfig.filters) {
359
- newConfig.filters.forEach((filter, index) => {
360
- const filterValues =
361
- filter.filterStyle === 'nested-dropdown'
362
- ? filter.values
363
- : filter.orderedValues ||
364
- generateValuesForFilter(filter.columnName, newExcludedData).sort(
365
- filter.order === 'desc' ? sortDesc : sortAsc
366
- )
367
-
368
- newConfig.filters[index].values = filterValues
369
- // Initial filter should be active
370
-
371
- const includes = (arr: any[], val: any): boolean => (arr || []).map(val => String(val)).includes(String(val))
372
- newConfig.filters[index].active =
373
- !newConfig.filters[index].active || !includes(filterValues, newConfig.filters[index].active)
374
- ? filterValues[0]
375
- : newConfig.filters[index].active
376
- newConfig.filters[index].filterStyle = newConfig.filters[index].filterStyle
377
- ? newConfig.filters[index].filterStyle
378
- : 'dropdown'
379
- })
380
- currentData = filterVizData(newConfig.filters, newExcludedData)
366
+ const filtersWithValues = addValuesToFilters(newConfig.filters, newExcludedData)
367
+ currentData = filterVizData(filtersWithValues, newExcludedData)
381
368
  setFilteredData(currentData)
382
369
  }
383
370
 
@@ -386,19 +373,18 @@ export default function CdcChart({
386
373
  }
387
374
 
388
375
  //Enforce default values that need to be calculated at runtime
389
- newConfig.runtime = {}
390
- newConfig.runtime.series = newConfig.dynamicSeries ? [] : newConfig.series
376
+ newConfig.runtime = {} as Runtime
377
+ newConfig.runtime.series = newConfig.dynamicSeries ? [] : _.cloneDeep(newConfig.series)
391
378
  newConfig.runtime.seriesLabels = {}
392
379
  newConfig.runtime.seriesLabelsAll = []
393
380
  newConfig.runtime.originalXAxis = newConfig.xAxis
394
381
 
395
382
  if (newConfig.dynamicSeries) {
396
383
  let finalData = dataOverride || newConfig.formattedData || newConfig.data
397
- if (finalData && finalData.length && finalData.length > 0) {
384
+ if (finalData?.length) {
398
385
  Object.keys(finalData[0]).forEach(seriesKey => {
399
386
  if (
400
387
  seriesKey !== newConfig.xAxis.dataKey &&
401
- finalData[0][seriesKey] &&
402
388
  (!newConfig.filters || newConfig.filters.filter(filter => filter.columnName === seriesKey).length === 0) &&
403
389
  (!newConfig.columns || Object.keys(newConfig.columns).indexOf(seriesKey) === -1)
404
390
  ) {
@@ -417,30 +403,42 @@ export default function CdcChart({
417
403
  newConfig.runtime.seriesKeys = (dataOverride || data).map(d => d[newConfig.xAxis.dataKey])
418
404
  newConfig.runtime.seriesLabelsAll = newConfig.runtime.seriesKeys
419
405
  } else {
420
- newConfig.runtime.seriesKeys = newConfig.runtime.series
421
- ? newConfig.runtime.series.map(series => {
422
- newConfig.runtime.seriesLabels[series.dataKey] = series.name || series.label || series.dataKey
423
- newConfig.runtime.seriesLabelsAll.push(series.name || series.dataKey)
424
- return series.dataKey
406
+ const finalData = dataOverride || newConfig.formattedData || newConfig.data
407
+ newConfig.runtime.seriesKeys = (newConfig.runtime.series || []).flatMap(series => {
408
+ if (series.dynamicCategory) {
409
+ _.remove(newConfig.runtime.seriesLabelsAll, label => label === series.dataKey)
410
+ _.remove(newConfig.runtime.series, s => s.dataKey === series.dataKey)
411
+ // grab the dynamic series keys from the data
412
+ const seriesKeys: string[] = _.uniq(finalData.map(d => d[series.dynamicCategory]))
413
+ // for each of those keys perform side effects
414
+ seriesKeys.forEach(dataKey => {
415
+ newConfig.runtime.seriesLabels[dataKey] = dataKey
416
+ newConfig.runtime.seriesLabelsAll.push(dataKey)
417
+ newConfig.runtime.series.push({
418
+ dataKey,
419
+ type: series.type,
420
+ lineType: series.lineType,
421
+ originalDataKey: series.dataKey,
422
+ dynamicCategory: series.dynamicCategory,
423
+ tooltip: true
424
+ })
425
425
  })
426
- : []
426
+ // return the series keys
427
+ return seriesKeys
428
+ } else {
429
+ newConfig.runtime.seriesLabels[series.dataKey] = series.name || series.label || series.dataKey
430
+ newConfig.runtime.seriesLabelsAll.push(series.name || series.dataKey)
431
+ // return the series keys
432
+ return [series.dataKey]
433
+ }
434
+ })
427
435
  }
428
436
 
429
437
  if (newConfig.visualizationType === 'Box Plot' && newConfig.series) {
430
- let allKeys = newExcludedData
431
- ? newExcludedData.map(d => d[newConfig.xAxis.dataKey])
432
- : data.map(d => d[newConfig.xAxis.dataKey])
433
- let allValues = newExcludedData
434
- ? newExcludedData.map(d => Number(d[newConfig?.series[0]?.dataKey]))
435
- : data.map(d => Number(d[newConfig?.series[0]?.dataKey]))
436
-
437
- const uniqueArray = function (arrArg) {
438
- return arrArg.filter(function (elem, pos, arr) {
439
- return arr.indexOf(elem) === pos
440
- })
441
- }
438
+ const combinedData = filteredData || data
439
+ let allKeys = combinedData.map(d => d[newConfig.xAxis.dataKey])
440
+ const groups = _.uniq(allKeys)
442
441
 
443
- const groups = uniqueArray(allKeys)
444
442
  let tableData: any[] = []
445
443
  const plots: any[] = []
446
444
 
@@ -452,9 +450,8 @@ export default function CdcChart({
452
450
  if (!g) throw new Error('No groups resolved in box plots')
453
451
 
454
452
  // filter data by group
455
- let filteredData = newExcludedData
456
- ? newExcludedData.filter(item => item[newConfig.xAxis.dataKey] === g)
457
- : data.filter(item => item[newConfig.xAxis.dataKey] === g)
453
+ let filteredData = combinedData.filter(item => item[newConfig.xAxis.dataKey] === g)
454
+
458
455
  let filteredDataValues: number[] = filteredData.map(item => Number(item[newConfig?.series[0]?.dataKey]))
459
456
 
460
457
  // Sort the data for upcoming functions.
@@ -463,16 +460,18 @@ export default function CdcChart({
463
460
  // ! - Notice d3.quantile doesn't work here, and we had to take a custom route.
464
461
  const quartiles = getQuartiles(sortedData)
465
462
 
466
- if (!filteredData) throw new Error('boxplots dont have data yet')
467
- if (!plots) throw new Error('boxplots dont have plots yet')
463
+ const getValuesBySeriesKey = () => {
464
+ const allSeriesKeys = newConfig.series.map(item => item?.dataKey)
465
+ const result = {}
466
+ allSeriesKeys.forEach(key => {
467
+ result[key] = filteredData.map(item => item[key])
468
+ })
468
469
 
469
- if (newConfig.boxplot.firstQuartilePercentage === '') {
470
- newConfig.boxplot.firstQuartilePercentage = 0
470
+ return result
471
471
  }
472
472
 
473
- if (newConfig.boxplot.thirdQuartilePercentage === '') {
474
- newConfig.boxplot.thirdQuartilePercentage = 0
475
- }
473
+ if (!filteredData) throw new Error('boxplots dont have data yet')
474
+ if (!plots) throw new Error('boxplots dont have plots yet')
476
475
 
477
476
  const q1 = quartiles.q1
478
477
  const q3 = quartiles.q3
@@ -481,9 +480,7 @@ export default function CdcChart({
481
480
  const upperBounds = q3 + (q3 - q1) * 1.5
482
481
 
483
482
  const outliers = sortedData.filter(v => v < lowerBounds || v > upperBounds)
484
- let nonOutliers = filteredDataValues
485
483
 
486
- nonOutliers = nonOutliers.filter(item => !outliers.includes(item))
487
484
  const minValue: number = d3.min<number>(filteredDataValues) || 0
488
485
  const _colMin = d3.max<number>([minValue, q1 - 1.5 * iqr])
489
486
  plots.push({
@@ -493,7 +490,7 @@ export default function CdcChart({
493
490
  columnMedian: Number(d3.median(filteredDataValues)).toFixed(newConfig.dataFormat.roundTo),
494
491
  columnFirstQuartile: q1.toFixed(newConfig.dataFormat.roundTo),
495
492
  columnMin: _colMin,
496
- columnTotal: filteredDataValues.reduce((partialSum, a) => partialSum + a, 0),
493
+ columnCount: filteredData.length,
497
494
  columnSd: Number(d3.deviation(filteredDataValues)).toFixed(newConfig.dataFormat.roundTo),
498
495
  columnMean: Number(d3.mean(filteredDataValues)).toFixed(newConfig.dataFormat.roundTo),
499
496
  columnIqr: Number(iqr).toFixed(newConfig.dataFormat.roundTo),
@@ -501,7 +498,7 @@ export default function CdcChart({
501
498
  columnUpperBounds: d3.min([d3.max(sortedData), q1 + 1.5 * iqr]),
502
499
  columnOutliers: outliers,
503
500
  values: filteredDataValues,
504
- nonOutlierValues: nonOutliers
501
+ keyValues: getValuesBySeriesKey()
505
502
  })
506
503
  } catch (e) {
507
504
  console.error('COVE: ', e.message) // eslint-disable-line
@@ -519,8 +516,6 @@ export default function CdcChart({
519
516
  return null // resolve eslint
520
517
  })
521
518
 
522
- // any other data we can add to boxplots
523
- newConfig.boxplot['allValues'] = allValues
524
519
  newConfig.boxplot['categories'] = groups
525
520
  newConfig.boxplot.plots = plots
526
521
  newConfig.boxplot.tableData = tableData
@@ -580,6 +575,7 @@ export default function CdcChart({
580
575
  ) {
581
576
  newConfig.runtime.xAxis = newConfig.yAxis['yAxis'] ? newConfig.yAxis['yAxis'] : newConfig.yAxis
582
577
  newConfig.runtime.yAxis = newConfig.xAxis['xAxis'] ? newConfig.xAxis['xAxis'] : newConfig.xAxis
578
+ newConfig.runtime.yAxis.labelOffset *= -1
583
579
 
584
580
  newConfig.runtime.horizontal = false
585
581
  newConfig.orientation = 'horizontal'
@@ -618,21 +614,6 @@ export default function CdcChart({
618
614
  setConfig(newConfig)
619
615
  }
620
616
 
621
- // Gets filter values from dataset
622
- const generateValuesForFilter = (columnName, data = this.state.data) => {
623
- const values: any[] = []
624
-
625
- data.forEach(row => {
626
- const value = row[columnName]
627
- //@ts-ignore
628
- if (value && false === values.includes(value)) {
629
- values.push(value)
630
- }
631
- })
632
-
633
- return values
634
- }
635
-
636
617
  // Sorts data series for horizontal bar charts
637
618
  const sortData = (a, b) => {
638
619
  let sortKey =
@@ -655,7 +636,6 @@ export default function CdcChart({
655
636
  const resizeObserver = new ResizeObserver(entries => {
656
637
  for (let entry of entries) {
657
638
  let { width, height } = entry.contentRect
658
- let svgMarginWidth = 32
659
639
  let editorWidth = 350
660
640
 
661
641
  width = isEditor ? width - editorWidth : width
@@ -668,7 +648,7 @@ export default function CdcChart({
668
648
  width = width - 2.5
669
649
  }
670
650
 
671
- width = width - svgMarginWidth
651
+ width = width
672
652
 
673
653
  setDimensions([width, height])
674
654
  }
@@ -682,14 +662,14 @@ export default function CdcChart({
682
662
  setContainer(node)
683
663
  }, []) // eslint-disable-line
684
664
 
685
- function isEmpty(obj) {
665
+ const isEmpty = obj => {
686
666
  return Object.keys(obj).length === 0
687
667
  }
688
668
 
689
669
  // Load data when component first mounts
690
670
  useEffect(() => {
691
671
  loadConfig()
692
- }, []) // eslint-disable-line
672
+ }, [configObj?.data?.length ? configObj.data : null]) // eslint-disable-line
693
673
 
694
674
  useEffect(() => {
695
675
  reloadURLData()
@@ -753,14 +733,6 @@ export default function CdcChart({
753
733
  }
754
734
  }, [externalFilters]) // eslint-disable-line
755
735
 
756
- // Load data when configObj data changes
757
- if (configObj) {
758
- // eslint-disable-next-line react-hooks/rules-of-hooks
759
- useEffect(() => {
760
- loadConfig()
761
- }, [configObj.data]) // eslint-disable-line
762
- }
763
-
764
736
  // This will set the bump chart's default scaling type to date-time
765
737
  useEffect(() => {
766
738
  if (['Bump Chart'].includes(config.visualizationType)) {
@@ -875,8 +847,23 @@ export default function CdcChart({
875
847
  }
876
848
  }
877
849
 
878
- const formatDate = date => {
879
- return timeFormat(config.runtime[section].dateDisplayFormat)(date)
850
+ const formatDate = (date, i, ticks) => {
851
+ let formattedDate = timeFormat(config.runtime[section].dateDisplayFormat)(date)
852
+ // Handle the case where all months work with '%b.' except for May
853
+ if (config.runtime[section].dateDisplayFormat?.includes('%b.') && formattedDate.includes('May.')) {
854
+ formattedDate = formattedDate.replace(/May\./g, 'May')
855
+ }
856
+ // Show years only once
857
+ if (config.xAxis.showYearsOnce && config.runtime[section].dateDisplayFormat?.includes('%Y') && ticks) {
858
+ const prevDate = ticks[i - 1] ? ticks[i - 1].value : null
859
+ const prevFormattedDate = timeFormat(config.runtime[section].dateDisplayFormat)(prevDate)
860
+ const year = formattedDate.match(/\d{4}/)
861
+ const prevYear = prevFormattedDate.match(/\d{4}/)
862
+ if (year && prevYear && year[0] === prevYear[0]) {
863
+ formattedDate = formattedDate.replace(year, '')
864
+ }
865
+ }
866
+ return formattedDate
880
867
  }
881
868
 
882
869
  const formatTooltipsDate = date => {
@@ -886,7 +873,16 @@ export default function CdcChart({
886
873
  // Format numeric data based on settings in config OR from passed in settings for Additional Columns
887
874
  // - use only for old horizontal data - newer formatNumber is in helper/formatNumber
888
875
  // TODO: we should combine various formatNumber functions across this project.
889
- const formatNumber = (num, axis, shouldAbbreviate = false, addColPrefix, addColSuffix, addColRoundTo) => {
876
+ // TODO suggestion: pass all options as object key/values to allow for more flexibility
877
+ const formatNumber = (
878
+ num,
879
+ axis,
880
+ shouldAbbreviate = false,
881
+ addColPrefix,
882
+ addColSuffix,
883
+ addColRoundTo,
884
+ { index, length } = { index: null, length: null }
885
+ ) => {
890
886
  // if num is NaN return num
891
887
  if (isNaN(num) || !num) return num
892
888
  // Check if the input number is negative
@@ -913,7 +909,8 @@ export default function CdcChart({
913
909
  rightSuffix,
914
910
  bottomPrefix,
915
911
  bottomSuffix,
916
- bottomAbbreviated
912
+ bottomAbbreviated,
913
+ onlyShowTopPrefixSuffix
917
914
  }
918
915
  } = config
919
916
 
@@ -1006,7 +1003,9 @@ export default function CdcChart({
1006
1003
  if (addColPrefix && axis === 'left') {
1007
1004
  result = addColPrefix + result
1008
1005
  } else {
1009
- if (prefix && axis === 'left') {
1006
+ // if onlyShowTopPrefixSuffix only show top prefix
1007
+ const suppressAllButLast = onlyShowTopPrefixSuffix && length - 1 !== index
1008
+ if (prefix && axis === 'left' && !suppressAllButLast) {
1010
1009
  result += prefix
1011
1010
  }
1012
1011
  }
@@ -1025,7 +1024,7 @@ export default function CdcChart({
1025
1024
  if (addColSuffix && axis === 'left') {
1026
1025
  result += addColSuffix
1027
1026
  } else {
1028
- if (suffix && axis === 'left') {
1027
+ if (suffix && axis === 'left' && !onlyShowTopPrefixSuffix) {
1029
1028
  result += suffix
1030
1029
  }
1031
1030
  }
@@ -1161,7 +1160,7 @@ export default function CdcChart({
1161
1160
  <h3>Finish Configuring</h3>
1162
1161
  <p>Set all required options to the left and confirm below to display a preview of the chart.</p>
1163
1162
  <Button
1164
- className='btn'
1163
+ className='btn btn-primary'
1165
1164
  style={{ margin: '1em auto' }}
1166
1165
  disabled={missingRequiredSections()}
1167
1166
  onClick={e => confirmDone(e)}
@@ -1218,12 +1217,31 @@ export default function CdcChart({
1218
1217
  // cleaning is deleting data we need in forecasting charts.
1219
1218
  if (!Array.isArray(data)) return []
1220
1219
  if (config.visualizationType === 'Forecasting') return data
1220
+ if (config.series?.some(series => !!series.dynamicCategory)) return data
1221
1221
  return config?.xAxis?.dataKey ? transform.cleanData(data, config.xAxis.dataKey) : data
1222
1222
  }
1223
1223
 
1224
- // required for DataTable
1225
- const displayGeoName = key => {
1226
- return key
1224
+ const getTableRuntimeData = () => {
1225
+ if (visualizationType === 'Sankey') return config?.data?.[0]?.tableData
1226
+ const data = filteredData || excludedData
1227
+ const dynamicSeries = config.series.find(series => !!series.dynamicCategory)
1228
+ if (!dynamicSeries) return data
1229
+ const usedColumns = Object.values(config.columns)
1230
+ .filter(col => col.dataTable)
1231
+ .map(col => col.name)
1232
+ .concat([dynamicSeries.dynamicCategory, dynamicSeries.dataKey])
1233
+ if (config.xAxis?.dataKey) usedColumns.push(config.xAxis.dataKey)
1234
+ return data.map(d => _.pick(d, usedColumns))
1235
+ }
1236
+
1237
+ const pivotDynamicSeries = (config: ChartConfig): TableConfig => {
1238
+ const tableConfig: TableConfig = _.cloneDeep(config)
1239
+ const dynamicSeries = tableConfig.series.find(series => !!series.dynamicCategory)
1240
+ if (dynamicSeries) {
1241
+ const pivot: Pivot = { columnName: dynamicSeries.dynamicCategory, valueColumns: [dynamicSeries.dataKey] }
1242
+ tableConfig.table.pivot = pivot
1243
+ }
1244
+ return tableConfig
1227
1245
  }
1228
1246
 
1229
1247
  // Prevent render if loading
@@ -1235,10 +1253,10 @@ export default function CdcChart({
1235
1253
  }
1236
1254
 
1237
1255
  const getChartWrapperClasses = () => {
1238
- const isLegendOnBottom = legend?.position === 'bottom' || ['sm', 'xs', 'xxs'].includes(currentViewport)
1256
+ const isLegendOnBottom = legend?.position === 'bottom' || isLegendWrapViewport(currentViewport)
1239
1257
  const classes = ['chart-container', 'p-relative']
1240
1258
  if (legend?.position) {
1241
- if (['sm', 'xs', 'xxs'].includes(currentViewport) && legend?.position !== 'top') {
1259
+ if (isLegendWrapViewport(currentViewport) && legend?.position !== 'top') {
1242
1260
  classes.push('legend-bottom')
1243
1261
  } else {
1244
1262
  classes.push(`legend-${legend.position}`)
@@ -1255,7 +1273,7 @@ export default function CdcChart({
1255
1273
 
1256
1274
  const getChartSubTextClasses = () => {
1257
1275
  const classes = ['subtext ']
1258
- const isLegendOnBottom = legend?.position === 'bottom' || ['sm', 'xs', 'xxs'].includes(currentViewport)
1276
+ const isLegendOnBottom = legend?.position === 'bottom' || isLegendWrapViewport(currentViewport)
1259
1277
 
1260
1278
  if (config.isResponsiveTicks) classes.push('subtext--responsive-ticks ')
1261
1279
  if (config.brush?.active && !isLegendOnBottom) classes.push('subtext--brush-active ')
@@ -1291,42 +1309,38 @@ export default function CdcChart({
1291
1309
  classes={['chart-title', `${config.theme}`, 'cove-component__header']}
1292
1310
  style={undefined}
1293
1311
  />
1294
- {/* Intro Text/Message */}
1295
- {config?.introText && config.visualizationType !== 'Spark Line' && (
1296
- <section
1297
- className={`introText legend_${config.legend.hide ? 'hidden' : 'visible'}_${config.legend.position} `}
1298
- >
1299
- {parse(config.introText)}
1300
- </section>
1301
- )}
1302
-
1303
- {/* Filters */}
1304
- {config.filters && !externalFilters && config.visualizationType !== 'Spark Line' && (
1305
- <Filters
1306
- config={config}
1307
- setConfig={setConfig}
1308
- setFilteredData={setFilteredData}
1309
- filteredData={filteredData}
1310
- excludedData={excludedData}
1311
- filterData={filterVizData}
1312
- dimensions={dimensions}
1313
- />
1314
- )}
1315
- <SkipTo skipId={handleChartTabbing(config, legendId)} skipMessage='Skip Over Chart Container' />
1316
- {config.annotations?.length > 0 && (
1317
- <SkipTo
1318
- skipId={handleChartTabbing(config, legendId)}
1319
- skipMessage={`Skip over annotations`}
1320
- key={`skip-annotations`}
1321
- />
1322
- )}
1323
1312
 
1324
1313
  {/* Visualization Wrapper */}
1325
1314
  <div className={getChartWrapperClasses().join(' ')}>
1315
+ {/* Intro Text/Message */}
1316
+ {config?.introText && config.visualizationType !== 'Spark Line' && (
1317
+ <section className={`introText `}>{parse(config.introText)}</section>
1318
+ )}
1319
+
1320
+ {/* Filters */}
1321
+ {config.filters && !externalFilters && config.visualizationType !== 'Spark Line' && (
1322
+ <Filters
1323
+ config={config}
1324
+ setConfig={setConfig}
1325
+ setFilteredData={setFilteredData}
1326
+ filteredData={filteredData}
1327
+ excludedData={excludedData}
1328
+ filterData={filterVizData}
1329
+ dimensions={dimensions}
1330
+ />
1331
+ )}
1332
+ <SkipTo skipId={handleChartTabbing(config, legendId)} skipMessage='Skip Over Chart Container' />
1333
+ {config.annotations?.length > 0 && (
1334
+ <SkipTo
1335
+ skipId={handleChartTabbing(config, legendId)}
1336
+ skipMessage={`Skip over annotations`}
1337
+ key={`skip-annotations`}
1338
+ />
1339
+ )}
1326
1340
  <LegendWrapper>
1327
1341
  <div
1328
1342
  className={
1329
- legend.hide || ['xxs', 'xs', 'sm'].includes(currentViewport)
1343
+ legend.hide || isLegendWrapViewport(currentViewport)
1330
1344
  ? 'w-100'
1331
1345
  : legend.position === 'bottom' || legend.position === 'top' || visualizationType === 'Sankey'
1332
1346
  ? 'w-100'
@@ -1335,30 +1349,36 @@ export default function CdcChart({
1335
1349
  >
1336
1350
  {/* All charts with LinearChart */}
1337
1351
  {!['Spark Line', 'Line', 'Sankey', 'Pie', 'Sankey'].includes(config.visualizationType) && (
1338
- <div style={{ height, width: `100%` }}>
1352
+ <div ref={parentRef} style={{ width: `100%` }}>
1339
1353
  <ParentSize>
1340
- {parent => <LinearChart parentWidth={parent.width} parentHeight={parent.height} />}
1354
+ {parent => (
1355
+ <LinearChart ref={svgRef} parentWidth={parent.width} parentHeight={parent.height} />
1356
+ )}
1341
1357
  </ParentSize>
1342
1358
  </div>
1343
1359
  )}
1344
1360
 
1345
1361
  {config.visualizationType === 'Pie' && (
1346
- <ParentSize className='justify-content-center d-flex' style={{ height, width: `100%` }}>
1347
- {parent => <PieChart parentWidth={parent.width} parentHeight={parent.height} />}
1362
+ <ParentSize className='justify-content-center d-flex' style={{ width: `100%` }}>
1363
+ {parent => <PieChart ref={svgRef} parentWidth={parent.width} parentHeight={parent.height} />}
1348
1364
  </ParentSize>
1349
1365
  )}
1350
1366
  {/* Line Chart */}
1351
1367
  {config.visualizationType === 'Line' &&
1352
1368
  (checkLineToBarGraph() ? (
1353
- <div style={{ height: config?.heights?.vertical, width: `100%` }}>
1369
+ <div ref={parentRef} style={{ width: `100%` }}>
1354
1370
  <ParentSize>
1355
- {parent => <LinearChart parentWidth={parent.width} parentHeight={parent.height} />}
1371
+ {parent => (
1372
+ <LinearChart ref={svgRef} parentWidth={parent.width} parentHeight={parent.height} />
1373
+ )}
1356
1374
  </ParentSize>
1357
1375
  </div>
1358
1376
  ) : (
1359
- <div style={{ height, width: `100%` }}>
1377
+ <div ref={parentRef} style={{ width: `100%` }}>
1360
1378
  <ParentSize>
1361
- {parent => <LinearChart parentWidth={parent.width} parentHeight={parent.height} />}
1379
+ {parent => (
1380
+ <LinearChart ref={svgRef} parentWidth={parent.width} parentHeight={parent.height} />
1381
+ )}
1362
1382
  </ParentSize>
1363
1383
  </div>
1364
1384
  ))}
@@ -1412,7 +1432,6 @@ export default function CdcChart({
1412
1432
  {description && config.visualizationType !== 'Spark Line' && (
1413
1433
  <div className={getChartSubTextClasses().join('')}>{parse(description)}</div>
1414
1434
  )}
1415
- {false && <Annotation.List />}
1416
1435
 
1417
1436
  {/* buttons */}
1418
1437
  <MediaControls.Section classes={['download-buttons']}>
@@ -1442,7 +1461,7 @@ export default function CdcChart({
1442
1461
  config.visualizationType !== 'Sankey') ||
1443
1462
  (config.visualizationType === 'Sankey' && config.table.show)) && (
1444
1463
  <DataTable
1445
- config={config}
1464
+ config={pivotDynamicSeries(config)}
1446
1465
  rawData={
1447
1466
  config.visualizationType === 'Sankey'
1448
1467
  ? config?.data?.[0]?.tableData
@@ -1450,15 +1469,11 @@ export default function CdcChart({
1450
1469
  ? filterVizData(config.filters, config.data)
1451
1470
  : config.data
1452
1471
  }
1453
- runtimeData={
1454
- config.visualizationType === 'Sankey'
1455
- ? config?.data?.[0]?.tableData
1456
- : filteredData || excludedData
1457
- }
1472
+ runtimeData={getTableRuntimeData()}
1458
1473
  expandDataTable={config.table.expanded}
1459
1474
  columns={config.columns}
1460
1475
  displayDataAsText={displayDataAsText}
1461
- displayGeoName={displayGeoName}
1476
+ displayGeoName={name => name}
1462
1477
  applyLegendToRow={applyLegendToRow}
1463
1478
  tableTitle={config.table.label}
1464
1479
  indexTitle={config.table.indexLabel}
@@ -1501,32 +1516,35 @@ export default function CdcChart({
1501
1516
  debugSvg: isDebug,
1502
1517
  dimensions,
1503
1518
  dynamicLegendItems,
1504
- excludedData: excludedData,
1519
+ excludedData,
1505
1520
  formatDate,
1506
1521
  formatNumber,
1507
1522
  formatTooltipsDate,
1508
- getTextWidth,
1509
1523
  getXAxisData,
1510
1524
  getYAxisData,
1511
1525
  handleChartAriaLabels,
1512
1526
  handleLineType,
1527
+ handleChartTabbing,
1513
1528
  highlight,
1514
1529
  highlightReset,
1515
1530
  imageId,
1516
1531
  isDashboard,
1517
- isLegendBottom: legend?.position === 'bottom' || ['sm', 'xs', 'xxs'].includes(currentViewport),
1532
+ isLegendBottom: legend?.position === 'bottom' || isLegendWrapViewport(currentViewport),
1518
1533
  isDebug,
1519
1534
  isDraggingAnnotation,
1520
1535
  handleDragStateChange,
1521
1536
  isEditor,
1522
1537
  isNumber,
1523
1538
  legend,
1539
+ legendId,
1540
+ legendRef,
1524
1541
  lineOptions,
1525
1542
  loading,
1526
1543
  missingRequiredSections,
1527
1544
  outerContainerRef,
1545
+ parentRef,
1528
1546
  parseDate,
1529
- rawData: stateData ?? {},
1547
+ rawData: _.cloneDeep(stateData) ?? {},
1530
1548
  seriesHighlight,
1531
1549
  setBrushConfig,
1532
1550
  setConfig,
@@ -1537,10 +1555,11 @@ export default function CdcChart({
1537
1555
  setSeriesHighlight,
1538
1556
  setSharedFilter,
1539
1557
  setSharedFilterValue,
1558
+ svgRef,
1540
1559
  tableData: filteredData || excludedData, // do not clean table data
1541
1560
  transformedData: clean(filteredData || excludedData), // do this right before passing to components
1542
1561
  twoColorPalette,
1543
- unfilteredData: stateData,
1562
+ unfilteredData: _.cloneDeep(stateData),
1544
1563
  updateConfig
1545
1564
  }
1546
1565
 
@@ -1559,3 +1578,5 @@ export default function CdcChart({
1559
1578
  </ConfigContext.Provider>
1560
1579
  )
1561
1580
  }
1581
+
1582
+ export default CdcChart