@cdc/chart 4.23.4 → 4.23.6

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 (42) hide show
  1. package/dist/cdcchart.js +54845 -51755
  2. package/examples/feature/__data__/planet-example-data.json +14 -32
  3. package/examples/feature/__data__/planet-logaritmic-data.json +56 -0
  4. package/examples/feature/area/area-chart-category.json +240 -0
  5. package/examples/feature/bar/example-bar-chart.json +544 -22
  6. package/examples/feature/bar/new.json +561 -0
  7. package/examples/feature/bar/planet-chart-logaritmic-config.json +170 -0
  8. package/examples/feature/boxplot/valid-boxplot.csv +17 -0
  9. package/examples/feature/combo/right-issues.json +190 -0
  10. package/examples/feature/filters/filter-testing.json +37 -3
  11. package/examples/feature/forecasting/combo-forecasting.json +245 -0
  12. package/examples/feature/forecasting/forecasting.json +5325 -0
  13. package/examples/feature/forecasting/index.json +203 -0
  14. package/examples/feature/forecasting/random_data.csv +366 -0
  15. package/examples/feature/line/line-chart.json +3 -3
  16. package/examples/feature/test-highlight/test-highlight-2.json +789 -0
  17. package/examples/feature/test-highlight/test-highlight-vertical.json +561 -0
  18. package/examples/feature/test-highlight/test-highlight.json +100 -0
  19. package/examples/feature/tests-non-numerics/stacked-vertical-bar-example-nonnumerics.json +1 -2
  20. package/examples/gallery/bar-chart-horizontal/horizontal-highlight.json +345 -0
  21. package/examples/gallery/line/line.json +173 -1
  22. package/index.html +14 -8
  23. package/package.json +2 -2
  24. package/src/CdcChart.jsx +342 -25
  25. package/src/components/AreaChart.jsx +32 -40
  26. package/src/components/BarChart.jsx +147 -25
  27. package/src/components/DataTable.jsx +30 -12
  28. package/src/components/DeviationBar.jsx +32 -32
  29. package/src/components/EditorPanel.jsx +1902 -1126
  30. package/src/components/Forecasting.jsx +147 -0
  31. package/src/components/Legend.jsx +193 -243
  32. package/src/components/LineChart.jsx +4 -9
  33. package/src/components/LinearChart.jsx +263 -285
  34. package/src/components/Series.jsx +518 -0
  35. package/src/components/SparkLine.jsx +3 -3
  36. package/src/data/initial-state.js +24 -5
  37. package/src/hooks/useHighlightedBars.js +154 -0
  38. package/src/hooks/useMinMax.js +128 -0
  39. package/src/hooks/useReduceData.js +31 -57
  40. package/src/hooks/useRightAxis.js +8 -2
  41. package/src/hooks/useScales.js +196 -0
  42. /package/examples/feature/area/{area-chart.json → area-chart-date.json} +0 -0
@@ -0,0 +1,147 @@
1
+ import React, { useContext, useEffect, useState } from 'react'
2
+
3
+ // cdc
4
+ import ConfigContext from '../ConfigContext'
5
+ import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
6
+ import { colorPalettesChart } from '@cdc/core/data/colorPalettes'
7
+
8
+ // visx & d3
9
+ import { useTooltipInPortal, defaultStyles } from '@visx/tooltip'
10
+ import { curveMonotoneX } from '@visx/curve'
11
+ import { Bar, Area, LinePath, Line } from '@visx/shape'
12
+ import { Group } from '@visx/group'
13
+
14
+ const Forecasting = ({ xScale, yScale, height, width, chartRef, handleTooltipMouseOver, tooltipData, showTooltip, handleTooltipMouseOff }) => {
15
+ const { transformedData: data, rawData, config, seriesHighlight, formatNumber } = useContext(ConfigContext)
16
+ const { xAxis, yAxis, legend, runtime } = config
17
+ const DEBUG = false
18
+
19
+ // sets the portal x/y for where tooltips should appear on the page.
20
+ const [chartPosition, setChartPosition] = useState(null)
21
+ useEffect(() => {
22
+ setChartPosition(chartRef.current.getBoundingClientRect())
23
+ }, [chartRef])
24
+
25
+ // a unique id is needed for tooltips.
26
+ const tooltip_id = `cdc-open-viz-tooltip-${config.runtime.uniqueId}`
27
+
28
+ // it appears we need to use TooltipInPortal.
29
+ const { TooltipInPortal } = useTooltipInPortal({
30
+ detectBounds: true,
31
+ // when tooltip containers are scrolled, this will correctly update the Tooltip position
32
+ scroll: true
33
+ })
34
+
35
+ const TooltipListItem = ({ item }) => {
36
+ const [label, value] = item
37
+ return label === config.xAxis.dataKey ? `${label}: ${value}` : `${label}: ${formatNumber(value, 'left')}`
38
+ }
39
+
40
+ return (
41
+ data && (
42
+ <ErrorBoundary component='ForecastingChart'>
43
+ <Group className='forecasting-items' key='forecasting-items-wrapper' left={yAxis.size}>
44
+ {runtime.forecastingSeriesKeys?.map((group, index) => {
45
+ if (!group || !group.stages) return
46
+ return group.stages.map((stage, stageIndex) => {
47
+ const { behavior } = legend
48
+ const groupData = rawData.filter(d => d[group.stageColumn] === stage.key)
49
+ let transparentArea = behavior === 'highlight' && seriesHighlight.length > 0 && seriesHighlight.indexOf(stage.key) === -1
50
+ let displayArea = behavior === 'highlight' || seriesHighlight.length === 0 || seriesHighlight.indexOf(stage.key) !== -1
51
+
52
+ return (
53
+ <Group className={`forecasting-areas-combo-${index}`} key={`forecasting-areas--stage-${stage.key.replaceAll(' ', '-')}-${index}`}>
54
+ {group.confidenceIntervals?.map((ciGroup, ciGroupIndex) => {
55
+ const palette = colorPalettesChart[stage.color]
56
+
57
+ return (
58
+ <Group key={`forecasting-areas--stage-${stage.key.replaceAll(' ', '-')}--group-${stageIndex}-${ciGroupIndex}`}>
59
+ {/* prettier-ignore */}
60
+ <Area
61
+ curve={curveMonotoneX}
62
+ data={groupData}
63
+ fill={displayArea ? palette[ciGroupIndex] : 'transparent'}
64
+ opacity={transparentArea ? 0.1 : 0.5}
65
+ x={d => xScale(Date.parse(d[xAxis.dataKey]))}
66
+ y0={d => yScale(d[ciGroup.low])}
67
+ y1={d => yScale(d[ciGroup.high])}
68
+ />
69
+
70
+ {ciGroupIndex === 0 && (
71
+ <>
72
+ {/* prettier-ignore */}
73
+ <LinePath
74
+ data={groupData}
75
+ x={ d => xScale(Date.parse(d[xAxis.dataKey])) }
76
+ y={ d => yScale(d[ciGroup.high])}
77
+ curve={curveMonotoneX}
78
+ stroke={displayArea ? palette[2] : 'transparent'}
79
+ strokeWidth={1}
80
+ strokeOpacity={1}
81
+ />
82
+
83
+ {/* prettier-ignore */}
84
+ <LinePath
85
+ data={groupData}
86
+ x={ d => xScale(Date.parse(d[xAxis.dataKey])) }
87
+ y={ d => yScale(d[ciGroup.low])}
88
+ curve={curveMonotoneX}
89
+ stroke={displayArea ? palette[2] : 'transparent'}
90
+ strokeWidth={1}
91
+ strokeOpacity={1}
92
+ />
93
+ </>
94
+ )}
95
+ </Group>
96
+ )
97
+ })}
98
+ </Group>
99
+ )
100
+ })
101
+ })}
102
+
103
+ {tooltipData && Object.entries(tooltipData.data).length > 0 && config?.runtime?.forecastingSeriesKeys?.length > 0 && (config.visualizationType === 'Combo' || config.visualizationType === 'Forecasting') && (
104
+ <TooltipInPortal key={Math.random()} top={tooltipData.dataYPosition + chartPosition?.top} left={tooltipData.dataXPosition + chartPosition?.left} style={defaultStyles}>
105
+ <ul
106
+ style={{
107
+ listStyle: 'none',
108
+ paddingLeft: 'unset',
109
+ fontFamily: 'sans-serif',
110
+ margin: 'auto',
111
+ lineHeight: '1rem'
112
+ }}
113
+ data-tooltip-id={tooltip_id}
114
+ >
115
+ {typeof tooltipData === 'object' &&
116
+ Object.entries(tooltipData.data).map((item, index) => (
117
+ <li style={{ padding: '2.5px 0' }} key={`li-${index}`}>
118
+ <TooltipListItem item={item} />
119
+ </li>
120
+ ))}
121
+ </ul>
122
+ </TooltipInPortal>
123
+ )}
124
+ {config?.runtime?.forecastingSeriesKeys?.length > 0 && (config.visualizationType === 'Combo' || config.visualizationType === 'Forecasting') && (
125
+ <Group key='tooltip-hover-section'>
126
+ <Bar key={'bars'} width={Number(width)} height={Number(height)} fill={DEBUG ? 'red' : 'transparent'} fillOpacity={0.05} onMouseMove={e => handleTooltipMouseOver(e, data)} onMouseOut={handleTooltipMouseOff} />
127
+ </Group>
128
+ )}
129
+ </Group>
130
+
131
+ {showTooltip && tooltipData && config.visual.verticalHoverLine && (
132
+ <Group key='tooltipLine-vertical' className='vertical-tooltip-line'>
133
+ <Line from={{ x: tooltipData.dataXPosition - 10, y: 0 }} to={{ x: tooltipData.dataXPosition - 10, y: height }} stroke={'black'} strokeWidth={1} pointerEvents='none' strokeDasharray='5,5' className='vertical-tooltip-line' />
134
+ </Group>
135
+ )}
136
+
137
+ {showTooltip && tooltipData && config.visual.horizontalHoverLine && (
138
+ <Group key='tooltipLine-horizontal' className='horizontal-tooltip-line' left={config.yAxis.size ? config.yAxis.size : 0}>
139
+ <Line from={{ x: 0, y: tooltipData.dataYPosition }} to={{ x: width, y: tooltipData.dataYPosition }} stroke={'black'} strokeWidth={1} pointerEvents='none' strokeDasharray='5,5' className='horizontal-tooltip-line' />
140
+ </Group>
141
+ )}
142
+ </ErrorBoundary>
143
+ )
144
+ )
145
+ }
146
+
147
+ export default Forecasting
@@ -5,85 +5,38 @@ import { LegendOrdinal, LegendItem, LegendLabel } from '@visx/legend'
5
5
  import LegendCircle from '@cdc/core/components/LegendCircle'
6
6
 
7
7
  import useLegendClasses from './../hooks/useLegendClasses'
8
+ import { useHighlightedBars } from '../hooks/useHighlightedBars'
8
9
 
9
- const Legend = () => {
10
- const { config, legend, colorScale, seriesHighlight, highlight, twoColorPalette, highlightReset, setSeriesHighlight, dynamicLegendItems, setDynamicLegendItems, transformedData: data, colorPalettes, rawData, setConfig, currentViewport } = useContext(ConfigContext)
11
-
12
- const { innerClasses, containerClasses } = useLegendClasses(config)
13
-
14
- useEffect(() => {
15
- if (dynamicLegendItems.length === 0) return
16
-
17
- let itemsToHighlight = dynamicLegendItems.map(item => item.text)
18
-
19
- setSeriesHighlight(itemsToHighlight)
10
+ // * FILE REVIEW *
11
+ // TODO: fix eslint-disable jsxa11y issues
20
12
 
21
- let colsToKeep = [...itemsToHighlight]
22
- let tmpLabels = []
13
+ // * ADDITIONAL NOTES *
14
+ // > recently removed dynamic legend items as they weren't used
15
+ // > recently removed boxplots, they don't provide any legend settings
23
16
 
24
- rawData.map(dataItem => {
25
- let tmp = {}
26
- colsToKeep.map(col => {
27
- tmp[col] = isNaN(dataItem[col]) ? dataItem[col] : dataItem[col]
28
- return null
29
- })
30
- return tmp
31
- })
32
-
33
- colsToKeep.map(col => {
34
- tmpLabels[col] = col
35
- return null
36
- })
37
-
38
- if (dynamicLegendItems.length > 0) {
39
- setConfig({
40
- ...config,
41
- runtime: {
42
- ...config.runtime,
43
- seriesKeys: colsToKeep,
44
- seriesLabels: tmpLabels
45
- }
46
- })
47
- }
48
- }, [dynamicLegendItems]) // eslint-disable-line
49
-
50
- useEffect(() => {
51
- if (dynamicLegendItems.length === 0) {
52
- // loop through all labels and add keys
53
- let resetSeriesNames = [...config.runtime.seriesLabelsAll]
54
- let tmpLabels = []
55
- config.runtime.seriesLabelsAll.map(item => {
56
- resetSeriesNames.map(col => {
57
- tmpLabels[col] = col
58
- return null
59
- })
60
- return null
61
- })
62
-
63
- setConfig({
64
- ...config,
65
- runtime: {
66
- ...config.runtime,
67
- seriesKeys: config.runtime.seriesLabelsAll,
68
- seriesLabels: tmpLabels
69
- }
70
- })
71
- }
72
- }, [dynamicLegendItems]) // eslint-disable-line
17
+ /* eslint-disable jsx-a11y/no-noninteractive-tabindex, jsx-a11y/no-static-element-interactions */
18
+ const Legend = () => {
19
+ // prettier-ignore
20
+ const {
21
+ config,
22
+ legend,
23
+ colorScale,
24
+ seriesHighlight,
25
+ highlight,
26
+ twoColorPalette,
27
+ tableData,
28
+ highlightReset,
29
+ transformedData: data,
30
+ colorPalettes,
31
+ currentViewport
32
+ } = useContext(ConfigContext)
73
33
 
74
- const removeDynamicLegendItem = label => {
75
- let newLegendItems = dynamicLegendItems.filter(item => item.text !== label.text)
76
- let newLegendItemsText = newLegendItems.map(item => item.text)
77
- setDynamicLegendItems(newLegendItems)
78
- setSeriesHighlight(newLegendItemsText)
79
- }
80
- const handleDynamicLegendChange = e => {
81
- setDynamicLegendItems([...dynamicLegendItems, JSON.parse(e.target.value)])
82
- }
34
+ const { innerClasses, containerClasses } = useLegendClasses(config)
35
+ const { visualizationType, visualizationSubType, series, runtime, orientation } = config
83
36
 
84
37
  const createLegendLabels = defaultLabels => {
85
38
  const colorCode = config.legend?.colorCode
86
- if (config.visualizationType === 'Deviation Bar') {
39
+ if (visualizationType === 'Deviation Bar') {
87
40
  const [belowColor, aboveColor] = twoColorPalette[config.twoColor.palette]
88
41
  const labelBelow = {
89
42
  datum: 'X',
@@ -100,20 +53,20 @@ const Legend = () => {
100
53
 
101
54
  return [labelBelow, labelAbove]
102
55
  }
103
- if (config.visualizationType === 'Bar' && config.visualizationSubType === 'regular' && colorCode && config.series?.length === 1) {
56
+ if (visualizationType === 'Bar' && visualizationSubType === 'regular' && colorCode && series?.length === 1) {
104
57
  let palette = colorPalettes[config.palette]
105
58
 
106
- while (data.length > palette.length) {
59
+ while (tableData.length > palette.length) {
107
60
  palette = palette.concat(palette)
108
61
  }
109
62
  palette = palette.slice(0, data.length)
110
- //store uniq values to Set by colorCode
63
+ //store unique values to Set by colorCode
111
64
  const set = new Set()
112
65
 
113
- data.forEach(d => set.add(d[colorCode]))
66
+ tableData.forEach(d => set.add(d[colorCode]))
114
67
 
115
- // create labels with uniq values
116
- const uniqeLabels = Array.from(set).map((val, i) => {
68
+ // create labels with unique values
69
+ const uniqueLabels = Array.from(set).map((val, i) => {
117
70
  const newLabel = {
118
71
  datum: val,
119
72
  index: i,
@@ -123,191 +76,188 @@ const Legend = () => {
123
76
  return newLabel
124
77
  })
125
78
 
126
- return uniqeLabels
79
+ return uniqueLabels
127
80
  }
128
- return defaultLabels
129
- }
130
81
 
131
- const isBottomOrSmallViewport = config.legend.position === 'bottom' || currentViewport === 'sm' || currentViewport === 'xs' || currentViewport === 'xxs'
132
- const isHorizontal = config.orientation === 'horizontal'
133
- const marginTop = isBottomOrSmallViewport && isHorizontal ? `${config.runtime.xAxis.size}px` : '0px'
134
- const marginBottom = isBottomOrSmallViewport ? '15px' : '0px'
82
+ // get forecasting items inside of combo
83
+ if (runtime?.forecastingSeriesKeys?.length > 0) {
84
+ let seriesLabels = []
135
85
 
136
- if (!legend) return null
86
+ //store unique values to Set by colorCode
137
87
 
138
- if (!legend.dynamicLegend)
139
- return config.visualizationType !== 'Box Plot' ? (
140
- <aside style={{ marginTop, marginBottom }} id='legend' className={containerClasses.join(' ')} role='region' aria-label='legend' tabIndex={0}>
141
- {legend.label && <h2>{parse(legend.label)}</h2>}
142
- {legend.description && <p>{parse(legend.description)}</p>}
143
- <LegendOrdinal scale={colorScale} itemDirection='row' labelMargin='0 20px 0 0' shapeMargin='0 10px 0'>
144
- {labels => (
145
- <div className={innerClasses.join(' ')}>
146
- {createLegendLabels(labels).map((label, i) => {
147
- let className = 'legend-item'
148
- let itemName = label.datum
149
-
150
- // Filter excluded data keys from legend
151
- if (config.exclusions.active && config.exclusions.keys?.includes(itemName)) {
152
- return null
153
- }
154
-
155
- if (config.runtime.seriesLabels) {
156
- let index = config.runtime.seriesLabelsAll.indexOf(itemName)
157
- itemName = config.runtime.seriesKeys[index]
158
- }
159
-
160
- if (seriesHighlight.length > 0 && false === seriesHighlight.includes(itemName)) {
161
- className += ' inactive'
162
- }
163
-
164
- return (
165
- <LegendItem
166
- className={className}
167
- tabIndex={0}
168
- key={`legend-quantile-${i}`}
169
- onKeyPress={e => {
170
- if (e.key === 'Enter') {
171
- highlight(label)
172
- }
173
- }}
174
- onClick={() => {
175
- highlight(label)
176
- }}
177
- >
178
- <LegendCircle fill={label.value} />
179
- <LegendLabel align='left' margin='0 0 0 4px'>
180
- {label.text}
181
- </LegendLabel>
182
- </LegendItem>
183
- )
184
- })}
185
- {seriesHighlight.length > 0 && (
186
- <button className={`legend-reset ${config.theme}`} onClick={labels => highlightReset(labels)} tabIndex={0}>
187
- Reset
188
- </button>
189
- )}
190
- </div>
191
- )}
192
- </LegendOrdinal>
193
- </aside>
194
- ) : (
195
- <aside id='legend' className={containerClasses.join(' ')} role='region' aria-label='legend' tabIndex={0}>
196
- {config.boxplot.legend.displayHowToReadText && <h3>{config.boxplot.legend.howToReadText}</h3>}
197
- </aside>
198
- )
199
- return (
200
- config.visualizationType !== 'Box Plot' && (
201
- <aside id='legend' className={containerClasses.join(' ')} role='region' aria-label='legend' tabIndex={0}>
202
- {legend.label && <h2>{parse(legend.label)}</h2>}
203
- {legend.description && <p>{parse(legend.description)}</p>}
88
+ // loop through each stage/group/area on the chart and create a label
89
+ config.runtime?.forecastingSeriesKeys?.map((outerGroup, index) => {
90
+ return outerGroup?.stages?.map((stage, index) => {
91
+ let colorValue = colorPalettes[stage.color]?.[2] ? colorPalettes[stage.color]?.[2] : '#ccc'
204
92
 
205
- <LegendOrdinal scale={colorScale} itemDirection='row' labelMargin='0 20px 0 0' shapeMargin='0 10px 0'>
206
- {labels => {
207
- if (
208
- Number(config.legend.dynamicLegendItemLimit) > dynamicLegendItems.length && // legend items are less than limit
209
- dynamicLegendItems.length !== config.runtime.seriesLabelsAll.length
210
- ) {
211
- // legend items are equal to series length
212
- return (
213
- <select className='dynamic-legend-dropdown' onChange={e => handleDynamicLegendChange(e)}>
214
- <option className={'all'} tabIndex={0} value={JSON.stringify({ text: config.legend.dynamicLegendDefaultText })}>
215
- {config.legend.dynamicLegendDefaultText}
216
- </option>
217
- {labels.map((label, i) => {
218
- let className = 'legend-item'
219
- let itemName = label.datum
220
- let inDynamicList = false
221
-
222
- // Filter excluded data keys from legend
223
- if (config.exclusions.active && config.exclusions.keys?.includes(itemName)) {
224
- return null
225
- }
93
+ const newLabel = {
94
+ datum: stage.key,
95
+ index: index,
96
+ text: stage.key,
97
+ value: colorValue
98
+ }
226
99
 
227
- if (config.runtime.seriesLabels) {
228
- let index = config.runtime.seriesLabelsAll.indexOf(itemName)
229
- itemName = config.runtime.seriesKeys[index]
230
- }
100
+ seriesLabels.push(newLabel)
101
+ })
102
+ })
231
103
 
232
- if (seriesHighlight.length > 0 && false === seriesHighlight.includes(itemName)) {
233
- className += ' inactive'
234
- }
104
+ // loop through bars for now to meet requirements.
105
+ config.runtime.barSeriesKeys &&
106
+ config.runtime.barSeriesKeys.map((bar, index) => {
107
+ let colorValue = colorPalettes[config.palette][index] ? colorPalettes[config.palette][index] : '#ccc'
235
108
 
236
- dynamicLegendItems.map(listItem => {
237
- if (listItem.text === label.text) {
238
- inDynamicList = true
239
- }
240
- return null
241
- })
109
+ const newLabel = {
110
+ datum: bar,
111
+ index: index,
112
+ text: bar,
113
+ value: colorValue
114
+ }
242
115
 
243
- if (inDynamicList) return true
244
- let palette = colorPalettes[config.palette]
116
+ seriesLabels.push(newLabel)
117
+ })
245
118
 
246
- label.value = palette[dynamicLegendItems.length]
119
+ return seriesLabels
120
+ }
247
121
 
248
- return (
249
- <option className={className} tabIndex={0} value={JSON.stringify(label)}>
250
- {label.text}
251
- </option>
252
- )
253
- })}
254
- </select>
255
- )
256
- } else {
257
- return config.legend.dynamicLegendItemLimitMessage
258
- }
259
- }}
260
- </LegendOrdinal>
122
+ // DEV-4161: replaceable series name in the legend
123
+ const hasNewSeriesName = config.series.map(s => s.name).filter(item => item).length > 0
124
+ if (hasNewSeriesName) {
125
+ let palette = colorPalettes[config.palette]
261
126
 
262
- <div className='dynamic-legend-list'>
263
- {dynamicLegendItems.map((label, i) => {
264
- let className = ['legend-item']
265
- let itemName = label.text
266
- let palette = colorPalettes[config.palette]
127
+ while (tableData.length > palette.length) {
128
+ palette = palette.concat(palette)
129
+ }
267
130
 
268
- // Filter excluded data keys from legend
269
- if (config.exclusions.active && config.exclusions.keys?.includes(itemName)) {
270
- return null
271
- }
131
+ palette = palette.slice(0, data.length)
132
+ //store unique values to Set by colorCode
133
+ const set = new Set()
272
134
 
273
- if (config.runtime.seriesLabels && !config.legend.dynamicLegend) {
274
- let index = config.runtime.seriesLabelsAll.indexOf(itemName)
275
- itemName = config.runtime.seriesKeys[index]
276
- }
135
+ config.series.forEach(d => {
136
+ set.add(d['name'] ? d['name'] : d['dataKey'])
137
+ })
277
138
 
278
- if (seriesHighlight.length > 0 && !seriesHighlight.includes(itemName)) {
279
- className.push('inactive')
280
- }
139
+ // create labels with unique values
140
+ const uniqueLabels = Array.from(set).map((val, i) => {
141
+ const newLabel = {
142
+ datum: val,
143
+ index: i,
144
+ text: val,
145
+ value: palette[i]
146
+ }
147
+ return newLabel
148
+ })
281
149
 
282
- if (seriesHighlight.length === 0 && config.legend.dynamicLegend) {
283
- className.push('inactive')
284
- }
150
+ return uniqueLabels
151
+ }
285
152
 
153
+ return defaultLabels
154
+ }
155
+
156
+ const isBottomOrSmallViewport = legend.position === 'bottom' || currentViewport === 'sm' || currentViewport === 'xs' || currentViewport === 'xxs'
157
+
158
+ const legendClasses = {
159
+ marginBottom: isBottomOrSmallViewport ? '15px' : '0px',
160
+ marginTop: isBottomOrSmallViewport && orientation === 'horizontal' ? `${config.runtime.xAxis.size}px` : '0px'
161
+ }
162
+
163
+ const { HighLightedBarUtils } = useHighlightedBars(config)
164
+
165
+ let highLightedLegendItems = HighLightedBarUtils.findDuplicates(config.highlightedBarValues)
166
+
167
+ if (!legend) return null
168
+
169
+ return (
170
+ config.visualizationType !== 'Box Plot' && (
171
+ <aside style={legendClasses} id='legend' className={containerClasses.join(' ')} role='region' aria-label='legend' tabIndex={0}>
172
+ {legend.label && <h2>{parse(legend.label)}</h2>}
173
+ {legend.description && <p>{parse(legend.description)}</p>}
174
+ <LegendOrdinal scale={colorScale} itemDirection='row' labelMargin='0 20px 0 0' shapeMargin='0 10px 0'>
175
+ {labels => {
286
176
  return (
287
- <>
288
- <LegendItem className={className.join(' ')} tabIndex={0} key={`dynamic-legend-item-${i}`} alignItems='center'>
289
- <button
290
- className='btn-wrapper'
291
- onClick={() => {
292
- highlight(label)
293
- }}
294
- >
295
- <LegendCircle fill={palette[i]} config={config} />
296
- <LegendLabel align='space-between' margin='4px 0 0 4px'>
297
- {label.text}
298
- </LegendLabel>
177
+ <div className={innerClasses.join(' ')}>
178
+ {createLegendLabels(labels).map((label, i) => {
179
+ let className = 'legend-item'
180
+ let itemName = label.datum
181
+
182
+ // Filter excluded data keys from legend
183
+ if (config.exclusions.active && config.exclusions.keys?.includes(itemName)) {
184
+ return null
185
+ }
186
+
187
+ if (runtime.seriesLabels) {
188
+ let index = config.runtime.seriesLabelsAll.indexOf(itemName)
189
+ itemName = config.runtime.seriesKeys[index]
190
+
191
+ if (runtime?.forecastingSeriesKeys?.length > 0) {
192
+ itemName = label.text
193
+ }
194
+ }
195
+
196
+ if (seriesHighlight.length > 0 && false === seriesHighlight.includes(itemName)) {
197
+ className += ' inactive'
198
+ }
199
+
200
+ return (
201
+ <LegendItem
202
+ className={className}
203
+ tabIndex={0}
204
+ key={`legend-quantile-${i}`}
205
+ onKeyPress={e => {
206
+ if (e.key === 'Enter') {
207
+ highlight(label)
208
+ }
209
+ }}
210
+ onClick={() => {
211
+ highlight(label)
212
+ }}
213
+ >
214
+ <LegendCircle fill={label.value} />
215
+ <LegendLabel align='left' margin='0 0 0 4px'>
216
+ {label.text}
217
+ </LegendLabel>
218
+ </LegendItem>
219
+ )
220
+ })}
221
+
222
+ {highLightedLegendItems.map((bar, i) => {
223
+ // if duplicates only return first item
224
+ let className = 'legend-item'
225
+ let itemName = bar.legendLabel
226
+
227
+ if (!itemName) return false
228
+ if (seriesHighlight.length > 0 && false === seriesHighlight.includes(itemName)) {
229
+ className += ' inactive'
230
+ }
231
+ return (
232
+ <LegendItem
233
+ className={className}
234
+ tabIndex={0}
235
+ key={`legend-quantile-${i}`}
236
+ onKeyPress={e => {
237
+ if (e.key === 'Enter') {
238
+ highlight(bar.legendLabel)
239
+ }
240
+ }}
241
+ onClick={() => {
242
+ highlight(bar.legendLabel)
243
+ }}
244
+ >
245
+ <LegendCircle fill='transparent' borderColor={bar.color ? bar.color : `rgba(255, 102, 1)`} />{' '}
246
+ <LegendLabel align='left' margin='0 0 0 4px'>
247
+ {bar.legendLabel ? bar.legendLabel : bar.value}
248
+ </LegendLabel>
249
+ </LegendItem>
250
+ )
251
+ })}
252
+ {seriesHighlight.length > 0 && (
253
+ <button className={`legend-reset ${config.theme}`} onClick={labels => highlightReset(labels)} tabIndex={0}>
254
+ Reset
299
255
  </button>
300
- <button onClick={() => removeDynamicLegendItem(label)}>x</button>
301
- </LegendItem>
302
- </>
256
+ )}
257
+ </div>
303
258
  )
304
- })}
305
- </div>
306
- {seriesHighlight.length < dynamicLegendItems.length && (
307
- <button className={`legend-reset legend-reset--dynamic ${config.theme}`} onClick={highlightReset} tabIndex={0}>
308
- Reset
309
- </button>
310
- )}
259
+ }}
260
+ </LegendOrdinal>
311
261
  </aside>
312
262
  )
313
263
  )