@cdc/chart 4.24.10 → 4.24.12-2

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 (94) hide show
  1. package/dist/cdcchart.js +35019 -34301
  2. package/examples/feature/boxplot/boxplot-data.json +88 -22
  3. package/examples/feature/boxplot/boxplot.json +540 -16
  4. package/examples/feature/boxplot/testing.csv +7 -7
  5. package/examples/feature/sankey/sankey-example-data.json +126 -14
  6. package/examples/feature/tests-date-exclusions/date-exclusions-config.json +372 -12
  7. package/examples/private/DEV-8850-2.json +493 -0
  8. package/examples/private/DEV-9822.json +574 -0
  9. package/examples/private/DEV-9840.json +553 -0
  10. package/examples/private/DEV-9850-3.json +461 -0
  11. package/examples/private/chart.json +1084 -0
  12. package/examples/private/ci_formatted.json +202 -0
  13. package/examples/private/ci_issue.json +3016 -0
  14. package/examples/private/completed.json +634 -0
  15. package/examples/private/dem-data-long.csv +20 -0
  16. package/examples/private/dem-data-long.json +36 -0
  17. package/examples/private/demographic_data.csv +157 -0
  18. package/examples/private/demographic_data.json +2654 -0
  19. package/examples/private/demographic_dynamic.json +443 -0
  20. package/examples/private/demographic_standard.json +560 -0
  21. package/examples/private/ehdi.json +29939 -0
  22. package/examples/private/test.json +493 -0
  23. package/index.html +10 -7
  24. package/package.json +2 -2
  25. package/src/CdcChart.tsx +132 -152
  26. package/src/_stories/Chart.Anchors.stories.tsx +31 -0
  27. package/src/_stories/Chart.CustomColors.stories.tsx +19 -0
  28. package/src/_stories/Chart.DynamicSeries.stories.tsx +34 -0
  29. package/src/_stories/Chart.Legend.Gradient.stories.tsx +42 -1
  30. package/src/_stories/Chart.stories.tsx +37 -6
  31. package/src/_stories/ChartAxisLabels.stories.tsx +4 -1
  32. package/src/_stories/ChartEditor.stories.tsx +27 -0
  33. package/src/_stories/ChartLine.Suppression.stories.tsx +25 -0
  34. package/src/_stories/ChartPrefixSuffix.stories.tsx +8 -0
  35. package/{examples/feature/area/area-chart-date-city-temperature.json → src/_stories/_mock/area_chart_stacked.json} +125 -27
  36. package/src/_stories/_mock/boxplot_multiseries.json +647 -0
  37. package/src/_stories/_mock/dynamic_series_bar_config.json +723 -0
  38. package/src/_stories/_mock/dynamic_series_config.json +979 -0
  39. package/src/_stories/_mock/line_chart_dynamic_ci.json +493 -0
  40. package/src/_stories/_mock/line_chart_non_dynamic_ci.json +522 -0
  41. package/{examples/feature/scatterplot/scatterplot.json → src/_stories/_mock/scatterplot_mock.json} +62 -92
  42. package/src/_stories/_mock/short_dates.json +288 -0
  43. package/src/_stories/_mock/suppression_mock.json +1549 -0
  44. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +15 -3
  45. package/src/components/Axis/Categorical.Axis.tsx +2 -2
  46. package/src/components/BarChart/components/BarChart.Horizontal.tsx +46 -37
  47. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +43 -9
  48. package/src/components/BarChart/components/BarChart.Vertical.tsx +53 -47
  49. package/src/components/BarChart/helpers/getBarData.ts +28 -0
  50. package/src/components/BarChart/helpers/index.ts +1 -2
  51. package/src/components/BarChart/helpers/tests/getBarData.test.ts +74 -0
  52. package/src/components/BoxPlot/BoxPlot.tsx +131 -0
  53. package/src/components/BoxPlot/helpers/index.ts +54 -0
  54. package/src/components/BrushChart.tsx +23 -26
  55. package/src/components/EditorPanel/EditorPanel.tsx +117 -139
  56. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +3 -3
  57. package/src/components/EditorPanel/components/Panels/Panel.BoxPlot.tsx +51 -6
  58. package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +40 -9
  59. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +3 -3
  60. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +122 -56
  61. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +1 -2
  62. package/src/components/EditorPanel/useEditorPermissions.ts +20 -2
  63. package/src/components/Legend/Legend.Component.tsx +11 -12
  64. package/src/components/Legend/Legend.tsx +16 -16
  65. package/src/components/Legend/helpers/getLegendClasses.ts +59 -0
  66. package/src/components/Legend/helpers/index.ts +2 -1
  67. package/src/components/Legend/tests/getLegendClasses.test.ts +115 -0
  68. package/src/components/LineChart/components/LineChart.Circle.tsx +1 -1
  69. package/src/components/LineChart/helpers.ts +49 -43
  70. package/src/components/LineChart/index.tsx +135 -83
  71. package/src/components/LinearChart.tsx +196 -181
  72. package/src/components/PieChart/PieChart.tsx +7 -1
  73. package/src/components/Sankey/components/ColumnList.tsx +19 -0
  74. package/src/components/Sankey/components/Sankey.tsx +479 -0
  75. package/src/components/Sankey/helpers/getSankeyTooltip.tsx +33 -0
  76. package/src/components/Sankey/index.tsx +1 -492
  77. package/src/components/Sankey/sankey.scss +22 -21
  78. package/src/components/Sankey/types/index.ts +1 -1
  79. package/src/components/Sankey/useSankeyAlert.tsx +60 -0
  80. package/src/components/ScatterPlot/ScatterPlot.jsx +20 -4
  81. package/src/data/initial-state.js +7 -12
  82. package/src/helpers/countNumOfTicks.ts +57 -0
  83. package/src/helpers/getQuartiles.ts +15 -18
  84. package/src/hooks/useMinMax.ts +44 -16
  85. package/src/hooks/useReduceData.ts +43 -10
  86. package/src/hooks/useScales.ts +90 -35
  87. package/src/hooks/useTooltip.tsx +59 -50
  88. package/src/scss/DataTable.scss +5 -0
  89. package/src/scss/main.scss +6 -20
  90. package/src/types/ChartConfig.ts +6 -19
  91. package/src/types/ChartContext.ts +4 -1
  92. package/src/types/ForestPlot.ts +8 -0
  93. package/src/components/BoxPlot/BoxPlot.jsx +0 -111
  94. package/src/hooks/useLegendClasses.ts +0 -72
@@ -0,0 +1,131 @@
1
+ import React, { useContext } from 'react'
2
+ import { BoxPlot } from '@visx/stats'
3
+ import { Group } from '@visx/group'
4
+ import ConfigContext from '../../ConfigContext'
5
+ import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
6
+ import { colorPalettesChart } from '@cdc/core/data/colorPalettes'
7
+ import { handleTooltip, calculateBoxPlotStats, createPlots } from './helpers/index'
8
+ import _ from 'lodash'
9
+
10
+ const CoveBoxPlot = ({ xScale, yScale, seriesScale }) => {
11
+ const { config, colorScale, seriesHighlight, transformedData: data } = useContext(ConfigContext)
12
+ const { boxplot } = config
13
+
14
+ const tooltip_id = `cdc-open-viz-tooltip-${config.runtime.uniqueId}`
15
+ const boxWidth = xScale.bandwidth()
16
+ const constrainedWidth = Math.min(40, boxWidth)
17
+ const color_0 = _.get(colorPalettesChart, [config.palette, 0], '#000')
18
+
19
+ return (
20
+ <ErrorBoundary component='BoxPlot'>
21
+ <Group left={Number(config.yAxis.size)} className='boxplot' key={`boxplot-group`}>
22
+ {createPlots(data, config).map((d, i) => {
23
+ const offset = boxWidth - constrainedWidth
24
+ const radius = 4
25
+
26
+ return (
27
+ <Group
28
+ key={`boxplotplot-${d.columnCategory}`}
29
+ left={xScale(d.columnCategory) + (xScale.bandwidth() - seriesScale.bandwidth()) / 2}
30
+ >
31
+ {config.series.map((item, index) => {
32
+ const valuesByKey = d.keyValues[item.dataKey]
33
+ const { min, max, median, firstQuartile, thirdQuartile } = calculateBoxPlotStats(valuesByKey)
34
+ let iqr = Number(thirdQuartile - firstQuartile).toFixed(config.dataFormat.roundTo)
35
+
36
+ const isTransparent =
37
+ config.legend.behavior === 'highlight' &&
38
+ seriesHighlight.length > 0 &&
39
+ seriesHighlight.indexOf(item.dataKey) === -1
40
+ const displayPlot =
41
+ config.legend.behavior === 'highlight' ||
42
+ seriesHighlight.length === 0 ||
43
+ seriesHighlight.indexOf(item.dataKey) !== -1
44
+ const fillOpacity = isTransparent ? 0.3 : 0.5
45
+
46
+ return (
47
+ <Group key={`boxplotplot-${item.dataKey}-${index}`}>
48
+ {boxplot.plotNonOutlierValues &&
49
+ valuesByKey.map((value, index) => {
50
+ return (
51
+ <circle
52
+ display={displayPlot ? 'block' : 'none'}
53
+ cx={seriesScale(item.dataKey) + seriesScale.bandwidth() / 2}
54
+ cy={yScale(value)}
55
+ r={radius}
56
+ fill={'#ccc'}
57
+ style={{ opacity: fillOpacity, fillOpacity: 1, stroke: 'black' }}
58
+ key={`boxplot-${i}--circle-${index}`}
59
+ />
60
+ )
61
+ })}
62
+ {displayPlot && (
63
+ <BoxPlot
64
+ display={displayPlot ? 'block' : 'none'}
65
+ data-left={xScale(d.columnCategory) + config.yAxis.size + offset / 2 + 0.5}
66
+ key={`box-plot-${i}-${item}`}
67
+ min={Number(min)}
68
+ max={Number(max)}
69
+ left={seriesScale(item.dataKey)}
70
+ firstQuartile={firstQuartile}
71
+ thirdQuartile={thirdQuartile}
72
+ median={median}
73
+ boxWidth={seriesScale.bandwidth()}
74
+ fill={colorScale(item.dataKey)}
75
+ fillOpacity={fillOpacity}
76
+ stroke={fillOpacity ? 'rgba(0,0,0,0.2)' : 'rgba(0,0,0,0.1)'}
77
+ valueScale={yScale}
78
+ outliers={boxplot.plotOutlierValues ? d.columnOutliers : []}
79
+ outlierProps={{
80
+ style: {
81
+ fill: `${color_0}`,
82
+ opacity: fillOpacity
83
+ }
84
+ }}
85
+ medianProps={{
86
+ style: {
87
+ stroke: 'black',
88
+ opacity: fillOpacity
89
+ }
90
+ }}
91
+ boxProps={{
92
+ style: {
93
+ stroke: 'black',
94
+ strokeWidth: boxplot.borders === 'true' ? 1 : 0,
95
+ opacity: fillOpacity
96
+ }
97
+ }}
98
+ maxProps={{
99
+ style: {
100
+ stroke: 'black',
101
+ opacity: fillOpacity
102
+ }
103
+ }}
104
+ container
105
+ containerProps={{
106
+ 'data-tooltip-html': handleTooltip(
107
+ boxplot,
108
+ d,
109
+ item.dataKey,
110
+ firstQuartile,
111
+ thirdQuartile,
112
+ median,
113
+ iqr
114
+ ),
115
+ 'data-tooltip-id': tooltip_id,
116
+ tabIndex: -1
117
+ }}
118
+ />
119
+ )}
120
+ </Group>
121
+ )
122
+ })}
123
+ </Group>
124
+ )
125
+ })}
126
+ </Group>
127
+ </ErrorBoundary>
128
+ )
129
+ }
130
+
131
+ export default CoveBoxPlot
@@ -0,0 +1,54 @@
1
+ import { max, min, median, quantile } from 'd3-array'
2
+ import _ from 'lodash'
3
+
4
+ interface Plot {
5
+ columnCategory: string
6
+ keyValues: { [key: string]: number[] }
7
+ }
8
+ export const handleTooltip = (boxplot, d, key, q1, q3, median, iqr) => {
9
+ return `
10
+ <strong>${d.columnCategory}</strong></br>
11
+ <strong>Key:${key}</strong></br>
12
+ ${boxplot.labels.q1}: ${q1}<br/>
13
+ ${boxplot.labels.q3}: ${q3}<br/>
14
+ ${boxplot.labels.iqr}: ${iqr}<br/>
15
+ ${boxplot.labels.median}: ${median}
16
+ `
17
+ }
18
+
19
+ export const calculateBoxPlotStats = values => {
20
+ if (!values || !values.length) return {}
21
+ const sortedValues = _.sortBy(values)
22
+ return {
23
+ min: min(values),
24
+ max: max(values),
25
+ median: median(values),
26
+ firstQuartile: quantile(sortedValues, 0.25),
27
+ thirdQuartile: quantile(sortedValues, 0.75)
28
+ }
29
+ }
30
+ const getValuesBySeriesKey = (group: string, config, data) => {
31
+ const allSeriesKeys = config.series.map(item => item?.dataKey)
32
+ const result = {}
33
+ const filteredData = data.filter(item => item[config.xAxis.dataKey] === group)
34
+ allSeriesKeys.forEach(key => {
35
+ result[key] = filteredData.map(item => item[key])
36
+ })
37
+
38
+ return result
39
+ }
40
+
41
+ export const createPlots = (data, config) => {
42
+ const dataKeys = data.map(d => d[config.xAxis.dataKey])
43
+ const plots: Plot[] = []
44
+ const groups: string[] = _.uniq(dataKeys)
45
+ if (groups && groups.length > 0) {
46
+ groups.forEach(group => {
47
+ plots.push({
48
+ columnCategory: group,
49
+ keyValues: getValuesBySeriesKey(group, config, data)
50
+ })
51
+ })
52
+ }
53
+ return plots
54
+ }
@@ -4,6 +4,7 @@ import ConfigContext from '../ConfigContext'
4
4
  import * as d3 from 'd3'
5
5
  import { Text } from '@visx/text'
6
6
  import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
7
+ import _ from 'lodash'
7
8
 
8
9
  interface BrushChartProps {
9
10
  xMax: number
@@ -11,7 +12,7 @@ interface BrushChartProps {
11
12
  }
12
13
 
13
14
  const BrushChart = ({ xMax, yMax }: BrushChartProps) => {
14
- const { tableData, config, setBrushConfig, dashboardConfig, formatDate } = useContext(ConfigContext)
15
+ const { tableData, config, setBrushConfig, dashboardConfig, formatDate, parseDate } = useContext(ConfigContext)
15
16
  const [brushState, setBrushState] = useState({ isBrushing: false, selection: [] })
16
17
  const [brushKey, setBrushKey] = useState(0)
17
18
  const sharedFilters = dashboardConfig?.dashboard?.sharedFilters ?? []
@@ -26,9 +27,15 @@ const BrushChart = ({ xMax, yMax }: BrushChartProps) => {
26
27
 
27
28
  const tooltipText = 'Drag edges to focus on a specific segment '
28
29
  const textWidth = getTextWidth(tooltipText, `normal ${16 / 1.1}px sans-serif`)
30
+ const DASHBOARD_MARGIN = 50
31
+ const BRUSH_HEIGHT_MULTIPLIER = 1.5
29
32
 
30
33
  const calculateGroupTop = (): number => {
31
- return Number(yMax) + config.xAxis.axisBBox + brushheight * 1.5
34
+ if (dashboardConfig?.type === 'dashboard') {
35
+ return Number(yMax) + config.xAxis.axisBBox + brushheight * BRUSH_HEIGHT_MULTIPLIER + DASHBOARD_MARGIN
36
+ } else {
37
+ return Number(yMax) + config.xAxis.axisBBox + brushheight * BRUSH_HEIGHT_MULTIPLIER
38
+ }
32
39
  }
33
40
 
34
41
  const handleMouseOver = () => {
@@ -85,41 +92,31 @@ const BrushChart = ({ xMax, yMax }: BrushChartProps) => {
85
92
 
86
93
  const [x0, x1] = selection.map(value => xScale.invert(value))
87
94
 
88
- // filter and update brush state directly
89
- const newFilteredData = tableData.filter(d => {
90
- const dateValue = d[config.runtime.originalXAxis.dataKey]
91
- // Check if the date value exists and is valid
92
- if (!dateValue) {
93
- return false
94
- }
95
+ const newFilteredData = _.filter(tableData, d => {
96
+ const parsedDate = new Date(d[config.xAxis.dataKey])
97
+ return parsedDate && !isNaN(parsedDate.getTime()) && parsedDate >= x0 && parsedDate <= x1
98
+ })
95
99
 
96
- const parsedDate = new Date(dateValue)
100
+ const sortByRecentDate = config.xAxis.sortByRecentDate
97
101
 
98
- // Check if parsedDate is a valid date
99
- if (isNaN(parsedDate.getTime())) {
100
- return false
101
- }
102
+ const sortedData = _.sortBy(newFilteredData, item => new Date(item[config.xAxis.dataKey]))
102
103
 
103
- // Check if the date falls within the selection range
104
- if (parsedDate >= x0 && parsedDate <= x1) {
105
- return true
106
- }
107
- })
104
+ // If ascending is false, reverse the sorted array
105
+ const finalData = !sortByRecentDate ? sortedData : sortedData.reverse()
108
106
 
109
- const firstDate = (newFilteredData.length && newFilteredData[0][config?.runtime?.originalXAxis?.dataKey]) ?? ''
110
- const lastDate =
111
- (newFilteredData.length &&
112
- newFilteredData[newFilteredData.length - 1][config?.runtime?.originalXAxis?.dataKey]) ??
113
- ''
107
+ // Retrieve the start and end dates based on the sorted data array
108
+ const startDate = _.get(_.first(finalData), config.xAxis.dataKey, '')
109
+ const endDate = _.get(_.last(finalData), config.xAxis.dataKey, '')
114
110
  // add custom blue colored handlers to each corners of brush
115
111
  svg.selectAll('.handle--custom').remove()
116
112
  // append handler
117
- svg.call(brushHandle, selection, firstDate, lastDate)
113
+ const [formattedStartDate, formattedEndDate] = [startDate, endDate].map(date => formatDate(parseDate(date)))
114
+ svg.call(brushHandle, selection, formattedStartDate, formattedEndDate)
118
115
 
119
116
  setBrushConfig({
120
117
  active: config.brush.active,
121
118
  isBrushing: isUserBrushing,
122
- data: newFilteredData
119
+ data: finalData
123
120
  })
124
121
  setBrushState({
125
122
  isBrushing: true,