@cdc/chart 4.24.7 → 4.24.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/cdcchart.js +40313 -37543
  2. package/examples/cases-year.json +13379 -0
  3. package/examples/gallery/bar-chart-vertical/combo-line-chart.json +76 -15
  4. package/examples/gallery/bar-chart-vertical/vertical-bar-chart-stacked.json +5 -5
  5. package/index.html +17 -8
  6. package/package.json +2 -2
  7. package/src/CdcChart.tsx +383 -133
  8. package/src/_stories/Chart.Legend.Gradient.tsx +19 -0
  9. package/src/_stories/_mock/legend.gradient_mock.json +236 -0
  10. package/src/components/Annotations/components/AnnotationDraggable.tsx +64 -11
  11. package/src/components/Axis/Categorical.Axis.tsx +145 -0
  12. package/src/components/BarChart/components/BarChart.Horizontal.tsx +4 -3
  13. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +1 -1
  14. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +2 -5
  15. package/src/components/BarChart/components/BarChart.Vertical.tsx +17 -8
  16. package/src/components/BarChart/helpers/index.ts +5 -16
  17. package/src/components/BrushChart.tsx +205 -0
  18. package/src/components/EditorPanel/EditorPanel.tsx +1766 -509
  19. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +19 -5
  20. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +190 -37
  21. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +43 -7
  22. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +4 -4
  23. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +1 -11
  24. package/src/components/EditorPanel/editor-panel.scss +16 -3
  25. package/src/components/EditorPanel/{useEditorPermissions.js → useEditorPermissions.ts} +90 -19
  26. package/src/components/Legend/Legend.Component.tsx +185 -193
  27. package/src/components/Legend/Legend.Suppression.tsx +146 -0
  28. package/src/components/Legend/Legend.tsx +21 -5
  29. package/src/components/Legend/helpers/index.ts +33 -3
  30. package/src/components/LegendWrapper.tsx +26 -0
  31. package/src/components/LineChart/LineChartProps.ts +1 -18
  32. package/src/components/LineChart/components/LineChart.BumpCircle.tsx +103 -0
  33. package/src/components/LineChart/components/LineChart.Circle.tsx +47 -8
  34. package/src/components/LineChart/helpers.ts +55 -11
  35. package/src/components/LineChart/index.tsx +113 -38
  36. package/src/components/LinearChart.tsx +1366 -0
  37. package/src/components/PieChart/PieChart.tsx +74 -17
  38. package/src/components/Sankey/index.tsx +22 -16
  39. package/src/components/Sparkline/components/SparkLine.tsx +2 -2
  40. package/src/data/initial-state.js +13 -3
  41. package/src/hooks/useLegendClasses.ts +52 -15
  42. package/src/hooks/useMinMax.ts +4 -4
  43. package/src/hooks/useScales.ts +34 -24
  44. package/src/hooks/useTooltip.tsx +85 -22
  45. package/src/scss/DataTable.scss +2 -1
  46. package/src/scss/main.scss +107 -14
  47. package/src/types/ChartConfig.ts +34 -8
  48. package/src/types/ChartContext.ts +5 -4
  49. package/examples/feature/line/line-chart.json +0 -449
  50. package/src/components/BrushHandle.jsx +0 -17
  51. package/src/components/LineChart/index.scss +0 -1
@@ -78,9 +78,14 @@ export const BarChartVertical = () => {
78
78
  >
79
79
  {barGroups => {
80
80
  return barGroups.map((barGroup, index) => (
81
- <Group className={`bar-group-${barGroup.index}-${barGroup.x0}--${index} ${config.orientation}`} key={`bar-group-${barGroup.index}-${barGroup.x0}--${index}`} id={`bar-group-${barGroup.index}-${barGroup.x0}--${index}`} left={barGroup.x0}>
81
+ <Group
82
+ className={`bar-group-${barGroup.index}-${barGroup.x0}--${index} ${config.orientation}`}
83
+ key={`bar-group-${barGroup.index}-${barGroup.x0}--${index}`}
84
+ id={`bar-group-${barGroup.index}-${barGroup.x0}--${index}`}
85
+ left={barGroup.x0}
86
+ >
82
87
  {barGroup.bars.map((bar, index) => {
83
- const scaleVal = config.useLogScale ? 0.1 : 0
88
+ const scaleVal = config.yAxis.type === 'logarithmic' ? 0.1 : 0
84
89
  let highlightedBarValues = config.highlightedBarValues.map(item => item.value).filter(item => item !== ('' || undefined))
85
90
  highlightedBarValues = config.xAxis.type === 'date' ? HighLightedBarUtils.formatDates(highlightedBarValues) : highlightedBarValues
86
91
  const transparentBar = config.legend.behavior === 'highlight' && seriesHighlight.length > 0 && seriesHighlight.indexOf(bar.key) === -1
@@ -94,7 +99,8 @@ export const BarChartVertical = () => {
94
99
  setBarWidth(barWidth)
95
100
  setTotalBarsInGroup(barGroup.bars.length)
96
101
  const yAxisValue = formatNumber(/[a-zA-Z]/.test(String(bar.value)) ? '' : bar.value, 'left')
97
- const xAxisValue = config.runtime[section].type === 'date' ? formatDate(parseDate(data[barGroup.index][config.runtime.originalXAxis.dataKey])) : data[barGroup.index][config.runtime.originalXAxis.dataKey]
102
+ const xAxisValue =
103
+ config.runtime[section].type === 'date' ? formatDate(parseDate(data[barGroup.index][config.runtime.originalXAxis.dataKey])) : data[barGroup.index][config.runtime.originalXAxis.dataKey]
98
104
 
99
105
  // create new Index for bars with negative values
100
106
  const newIndex = bar.value < 0 ? -1 : index
@@ -113,7 +119,6 @@ export const BarChartVertical = () => {
113
119
  let labelColor = '#000000'
114
120
  labelColor = HighLightedBarUtils.checkFontColor(yAxisValue, highlightedBarValues, labelColor) // Set if background is transparent'
115
121
  let barColor = config.runtime.seriesLabels && config.runtime.seriesLabels[bar.key] ? colorScale(config.runtime.seriesLabels[bar.key]) : colorScale(bar.key)
116
- barColor = assignColorsToValues(barGroups.length, barGroup.index, barColor) // Color code by category
117
122
  const isRegularLollipopColor = config.isLollipopChart && config.lollipopColorStyle === 'regular'
118
123
  const isTwoToneLollipopColor = config.isLollipopChart && config.lollipopColorStyle === 'two-tone'
119
124
  const isHighlightedBar = highlightedBarValues?.includes(xAxisValue)
@@ -156,13 +161,16 @@ export const BarChartVertical = () => {
156
161
  : colorScale(config.runtime.seriesLabels[bar.key])
157
162
 
158
163
  if (isRegularLollipopColor) _barColor = barColor
159
- if (isTwoToneLollipopColor) _barColor = chroma(barColor).brighten(1)
164
+
160
165
  if (isHighlightedBar) _barColor = 'transparent'
166
+ if (config.legend.colorCode) _barColor = assignColorsToValues(barGroups.length, barGroup.index, barColor)
167
+ if (isTwoToneLollipopColor) _barColor = chroma(barColor).brighten(1)
161
168
  return _barColor
162
169
  }
163
170
 
164
171
  // if this is a two tone lollipop slightly lighten the bar.
165
172
  if (isTwoToneLollipopColor) _barColor = chroma(barColor).brighten(1)
173
+ if (config.legend.colorCode) _barColor = assignColorsToValues(barGroups.length, barGroup.index, barColor)
166
174
 
167
175
  // if we're highlighting a bar make it invisible since it gets a border
168
176
  if (isHighlightedBar) _barColor = 'transparent'
@@ -216,6 +224,7 @@ export const BarChartVertical = () => {
216
224
  const yPadding = hasAsterisk ? -5 : -8
217
225
  const verticalAnchor = hasAsterisk ? 'middle' : 'end'
218
226
  const iconSize = pd.symbol === 'Asterisk' ? barWidth * 1.2 : pd.symbol === 'Double Asterisk' ? barWidth : barWidth / 1.5
227
+ const fillColor = pd.displayGray ? '#8b8b8a' : '#000'
219
228
 
220
229
  return (
221
230
  <Text // prettier-ignore
@@ -226,7 +235,7 @@ export const BarChartVertical = () => {
226
235
  x={barX + barWidth / 2}
227
236
  y={barY}
228
237
  verticalAnchor={verticalAnchor}
229
- fill={labelColor}
238
+ fill={fillColor}
230
239
  textAnchor='middle'
231
240
  fontSize={`${iconSize}px`}
232
241
  >
@@ -263,7 +272,7 @@ export const BarChartVertical = () => {
263
272
  cx={barX + lollipopShapeSize / 3.5}
264
273
  cy={bar.y}
265
274
  r={lollipopShapeSize / 2}
266
- fill={barColor}
275
+ fill={getBarBackgroundColor(colorScale(config.runtime.seriesLabels[bar.key]))}
267
276
  key={`circle--${bar.index}`}
268
277
  data-tooltip-html={tooltip}
269
278
  data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
@@ -277,7 +286,7 @@ export const BarChartVertical = () => {
277
286
  y={barY}
278
287
  width={lollipopShapeSize}
279
288
  height={lollipopShapeSize}
280
- fill={barColor}
289
+ fill={getBarBackgroundColor(colorScale(config.runtime.seriesLabels[bar.key]))}
281
290
  key={`circle--${bar.index}`}
282
291
  data-tooltip-html={tooltip}
283
292
  data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
@@ -5,7 +5,7 @@ interface BarConfigProps {
5
5
  bar?: { [key: string]: any }
6
6
  isNumber?: Function
7
7
  config: { [key: string]: any }
8
- getTextWidth: Function
8
+ getTextWidth: (a: string, b: string) => string
9
9
  barWidth: number
10
10
  isVertical: boolean
11
11
  }
@@ -20,7 +20,6 @@ export const getBarConfig = ({ bar, defaultBarHeight, defaultBarWidth, config, i
20
20
  let barLabel = ''
21
21
  let isSuppressed = false
22
22
  let showMissingDataLabel = false
23
- let showZeroValueDataLabel = false
24
23
  const showSuppressedSymbol = config.general.showSuppressedSymbol
25
24
 
26
25
  config.preliminaryData.forEach(pd => {
@@ -41,23 +40,15 @@ export const getBarConfig = ({ bar, defaultBarHeight, defaultBarWidth, config, i
41
40
  // Handle undefined, null, or non-calculable bar.value
42
41
  if (!isSuppressed && !isNumber(bar.value) && config.general.showMissingDataLabel) {
43
42
  const labelWidth = getTextWidth(barLabel, `normal ${barWidth / 2}px sans-serif`)
44
- const labelFits = labelWidth < barWidth && barWidth > 10
43
+ const labelFits = Number(labelWidth) < barWidth && barWidth > 10
45
44
  showMissingDataLabel = true
46
45
  barHeight = labelFits ? heightMini : 0
47
46
  barWidthHorizontal = heightMini
48
47
  }
49
- // handle zero values
50
- if (!isSuppressed && String(bar.value) === '0' && config.general.showZeroValueDataLabel) {
51
- const labelWidth = getTextWidth(barLabel, `normal ${barWidth / 2}px sans-serif`)
52
- const labelFits = labelWidth < barWidth && barWidth > 10
53
- barHeight = config.isLollipopChart ? heightMini * 2 : !config.isLollipopChart && labelFits ? heightMini : 0
54
- barWidthHorizontal = heightMini
55
- showZeroValueDataLabel = true
56
- }
57
48
 
58
49
  const getBarY = (defaultBarY, yScale) => {
59
50
  // calculate Y position of small bars (suppressed,N/A,Zero valued) bars
60
- if (isSuppressed || showMissingDataLabel || showZeroValueDataLabel) {
51
+ if (isSuppressed || showMissingDataLabel) {
61
52
  if (config.isLollipopChart) {
62
53
  return yScale - heightMini * 2
63
54
  } else {
@@ -77,12 +68,10 @@ export const getBarConfig = ({ bar, defaultBarHeight, defaultBarWidth, config, i
77
68
  if (isSuppressed) label = ''
78
69
  // If the config is set to show a label for missing data, display 'N/A'
79
70
  if (showMissingDataLabel) label = 'N/A'
80
- // If the config is set to specifically show zero values, set the label to '0'
81
- if (showZeroValueDataLabel) label = '0'
82
71
 
83
72
  // determine label width in pixels & check if it fits to the bar width
84
73
  const labelWidth = getTextWidth(barLabel, `normal ${barWidth / 2}px sans-serif`)
85
- const labelFits = labelWidth < barWidth && barWidth > 10
74
+ const labelFits = Number(labelWidth) < barWidth && barWidth > 10
86
75
  if (config.isLollipopChart) {
87
76
  return label
88
77
  } else {
@@ -90,7 +79,7 @@ export const getBarConfig = ({ bar, defaultBarHeight, defaultBarWidth, config, i
90
79
  }
91
80
  }
92
81
 
93
- return { barWidthHorizontal, barHeight, isSuppressed, showMissingDataLabel, showZeroValueDataLabel, getBarY, getAbsentDataLabel }
82
+ return { barWidthHorizontal, barHeight, isSuppressed, showMissingDataLabel, getBarY, getAbsentDataLabel }
94
83
  }
95
84
 
96
85
  export const testZeroValue = value => {
@@ -0,0 +1,205 @@
1
+ import { Group } from '@visx/group'
2
+ import { useContext, useEffect, useRef, useState } from 'react'
3
+ import ConfigContext from '../ConfigContext'
4
+ import * as d3 from 'd3'
5
+ import { invertValue } from '@cdc/core/helpers/scaling'
6
+ import { Text } from '@visx/text'
7
+ interface BrushChartProps {
8
+ xMax: number
9
+ yMax: number
10
+ }
11
+
12
+ const BrushChart = ({ xMax, yMax }: BrushChartProps) => {
13
+ const { tableData, config, setBrushConfig, getTextWidth, dashboardConfig, formatDate } = useContext(ConfigContext)
14
+ const [brushState, setBrushState] = useState({ isBrushing: false, selection: [] })
15
+ const [brushKey, setBrushKey] = useState(0)
16
+ const sharedFilters = dashboardConfig?.dashboard?.sharedFilters ?? []
17
+ const isDashboardFilters = sharedFilters?.length > 0
18
+ const [tooltip, showTooltip] = useState(false)
19
+ const svgRef = useRef()
20
+ const brushheight = 25
21
+ const borderRadius = 15
22
+ const xDomain = d3.extent(tableData, d => new Date(d[config.runtime.originalXAxis.dataKey]))
23
+
24
+ const xScale = d3.scaleTime().domain(xDomain).range([0, xMax])
25
+
26
+ const tooltipText = 'Drag edges to focus on a specific segment '
27
+ const textWidth = getTextWidth(tooltipText, `normal ${16 / 1.1}px sans-serif`)
28
+
29
+ const calculateGroupTop = (): number => {
30
+ return Number(yMax) + config.xAxis.axisBBox + brushheight * 1.5
31
+ }
32
+
33
+ const handleMouseOver = () => {
34
+ // show tooltip text only once before brush triggered
35
+ if (brushState.selection[0] === 0 && xMax === brushState.selection[1]) {
36
+ showTooltip(true)
37
+ }
38
+ }
39
+ const handleMouseLeave = () => {
40
+ // hide tooltip text if brush was triggered
41
+ if (brushState.selection[0] !== 0 || brushState.selection[1] !== xMax) {
42
+ showTooltip(false)
43
+ }
44
+ showTooltip(false)
45
+ }
46
+
47
+ const brushHandle = (g, selection, firstDate, lastDate) => {
48
+ const textWidth = getTextWidth(firstDate, `normal ${16 / 1.1}px sans-serif`)
49
+ return g
50
+ .selectAll('.handle--custom')
51
+ .data([{ side: 'left' }, { side: 'right' }])
52
+ .join(enter => {
53
+ const handleGroup = enter.append('g').attr('class', 'handle--custom')
54
+ handleGroup
55
+ .append('text')
56
+ .attr('x', d => (d.side === 'left' ? 0 : -textWidth))
57
+ .attr('y', 30)
58
+ .text(d => (d.side === 'left' ? firstDate : lastDate))
59
+ .attr('font-size', '13px')
60
+ return handleGroup
61
+ })
62
+
63
+ .attr('display', 'block')
64
+ .attr('transform', selection === null ? null : (_, i) => `translate(${selection[i]},${'10'})`)
65
+ }
66
+
67
+ const initializeBrush = () => {
68
+ const svg = d3.select(svgRef.current).attr('overflow', 'visible')
69
+
70
+ svg
71
+ .append('rect') // prettier-ignore
72
+ .attr('fill', '#949494')
73
+ .attr('stroke', '#c5c5c5')
74
+ .attr('stroke-width', 2)
75
+ .attr('ry', borderRadius)
76
+ .attr('rx', borderRadius)
77
+ .attr('height', brushheight)
78
+ .attr('width', xMax)
79
+
80
+ const brushHandler = event => {
81
+ const selection = event?.selection
82
+ //if (!selection) return
83
+ let isUserBrushing = event.type === 'brush' && selection && selection.length > 0
84
+
85
+ const [x0, x1] = selection.map(value => xScale.invert(value))
86
+
87
+ // filter and update brush state directly
88
+ const newFilteredData = tableData.filter(d => {
89
+ const dateValue = d[config.runtime.originalXAxis.dataKey]
90
+ // Check if the date value exists and is valid
91
+ if (!dateValue) {
92
+ return false
93
+ }
94
+
95
+ const parsedDate = new Date(dateValue)
96
+
97
+ // Check if parsedDate is a valid date
98
+ if (isNaN(parsedDate.getTime())) {
99
+ return false
100
+ }
101
+
102
+ // Check if the date falls within the selection range
103
+ if (parsedDate >= x0 && parsedDate <= x1) {
104
+ return true
105
+ }
106
+ })
107
+
108
+ const firstDate = (newFilteredData.length && newFilteredData[0][config?.runtime?.originalXAxis?.dataKey]) ?? ''
109
+ const lastDate =
110
+ (newFilteredData.length &&
111
+ newFilteredData[newFilteredData.length - 1][config?.runtime?.originalXAxis?.dataKey]) ??
112
+ ''
113
+ // add custom blue colored handlers to each corners of brush
114
+ svg.selectAll('.handle--custom').remove()
115
+ // append handler
116
+ svg.call(brushHandle, selection, firstDate, lastDate)
117
+
118
+ setBrushConfig({
119
+ active: config.brush.active,
120
+ isBrushing: isUserBrushing,
121
+ data: newFilteredData
122
+ })
123
+ setBrushState({
124
+ isBrushing: true,
125
+ selection
126
+ })
127
+ }
128
+
129
+ const brush = d3
130
+ .brushX()
131
+ .extent([
132
+ [0, 0],
133
+ [xMax, 25]
134
+ ]) // brush extent
135
+ .on('start brush end', brushHandler)
136
+
137
+ const defaultSelection = [0, xMax]
138
+ let brushGroup = svg.append('g').call(brush).call(brush.move, defaultSelection)
139
+ brushGroup.select('.overlay').style('pointer-events', 'none')
140
+
141
+ brushGroup
142
+ .selectAll('.selection')
143
+ .attr('fill', '#474747')
144
+ .attr('fill-opacity', 1)
145
+ .attr('rx', borderRadius)
146
+ .attr('ry', borderRadius)
147
+ }
148
+
149
+ useEffect(() => {
150
+ const isFiltersActive = config.filters?.some(filter => filter.active)
151
+ const isExclusionsActive = config.exclusions?.active
152
+
153
+ if ((isFiltersActive || isExclusionsActive || isDashboardFilters) && config.brush?.active) {
154
+ setBrushKey(prevKey => prevKey + 1)
155
+ setBrushConfig(prev => {
156
+ return {
157
+ ...prev,
158
+ data: tableData
159
+ }
160
+ })
161
+ }
162
+ return () =>
163
+ setBrushConfig(prev => {
164
+ return {
165
+ ...prev,
166
+ data: []
167
+ }
168
+ })
169
+ }, [config.filters, config.exclusions, config.brush?.active, isDashboardFilters])
170
+ // Initialize brush when component is first rendered
171
+
172
+ // reset brush on keychange
173
+ useEffect(() => {
174
+ if (brushKey) {
175
+ initializeBrush()
176
+ }
177
+ }, [brushKey])
178
+
179
+ if (!brushState.isBrushing) {
180
+ initializeBrush()
181
+ }
182
+
183
+ return (
184
+ <Group
185
+ onMouseLeave={handleMouseLeave}
186
+ onMouseOver={handleMouseOver}
187
+ className='brush-container'
188
+ left={Number(config.runtime.yAxis.size)}
189
+ top={calculateGroupTop()}
190
+ >
191
+ <Text
192
+ pointerEvents='visiblePainted'
193
+ display={tooltip ? 'block' : 'none'}
194
+ fontSize={16}
195
+ x={(Number(xMax) - Number(textWidth)) / 2}
196
+ y={-10}
197
+ >
198
+ Drag edges to focus on a specific segment
199
+ </Text>
200
+ <svg width={'100%'} height={brushheight * 3} ref={svgRef}></svg>
201
+ </Group>
202
+ )
203
+ }
204
+
205
+ export default BrushChart