@cdc/chart 4.23.5 → 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.
@@ -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
@@ -7,84 +7,36 @@ import LegendCircle from '@cdc/core/components/LegendCircle'
7
7
  import useLegendClasses from './../hooks/useLegendClasses'
8
8
  import { useHighlightedBars } from '../hooks/useHighlightedBars'
9
9
 
10
- const Legend = () => {
11
- const { config, legend, colorScale, seriesHighlight, highlight, twoColorPalette, tableData, highlightReset, setSeriesHighlight, dynamicLegendItems, setDynamicLegendItems, transformedData: data, colorPalettes, rawData, setConfig, currentViewport } = useContext(ConfigContext)
12
-
13
- const { innerClasses, containerClasses } = useLegendClasses(config)
14
-
15
- useEffect(() => {
16
- if (dynamicLegendItems.length === 0) return
17
-
18
- let itemsToHighlight = dynamicLegendItems.map(item => item.text)
19
-
20
- setSeriesHighlight(itemsToHighlight)
10
+ // * FILE REVIEW *
11
+ // TODO: fix eslint-disable jsxa11y issues
21
12
 
22
- let colsToKeep = [...itemsToHighlight]
23
- let tmpLabels = []
24
-
25
- rawData.map(dataItem => {
26
- let tmp = {}
27
- colsToKeep.map(col => {
28
- tmp[col] = isNaN(dataItem[col]) ? dataItem[col] : dataItem[col]
29
- return null
30
- })
31
- return tmp
32
- })
33
-
34
- colsToKeep.map(col => {
35
- tmpLabels[col] = col
36
- return null
37
- })
38
-
39
- if (dynamicLegendItems.length > 0) {
40
- setConfig({
41
- ...config,
42
- runtime: {
43
- ...config.runtime,
44
- seriesKeys: colsToKeep,
45
- seriesLabels: tmpLabels
46
- }
47
- })
48
- }
49
- }, [dynamicLegendItems]) // eslint-disable-line
50
-
51
- useEffect(() => {
52
- if (dynamicLegendItems.length === 0) {
53
- // loop through all labels and add keys
54
- let resetSeriesNames = [...config.runtime.seriesLabelsAll]
55
- let tmpLabels = []
56
- config.runtime.seriesLabelsAll.map(item => {
57
- resetSeriesNames.map(col => {
58
- tmpLabels[col] = col
59
- return null
60
- })
61
- return null
62
- })
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
63
16
 
64
- setConfig({
65
- ...config,
66
- runtime: {
67
- ...config.runtime,
68
- seriesKeys: config.runtime.seriesLabelsAll,
69
- seriesLabels: tmpLabels
70
- }
71
- })
72
- }
73
- }, [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)
74
33
 
75
- const removeDynamicLegendItem = label => {
76
- let newLegendItems = dynamicLegendItems.filter(item => item.text !== label.text)
77
- let newLegendItemsText = newLegendItems.map(item => item.text)
78
- setDynamicLegendItems(newLegendItems)
79
- setSeriesHighlight(newLegendItemsText)
80
- }
81
- const handleDynamicLegendChange = e => {
82
- setDynamicLegendItems([...dynamicLegendItems, JSON.parse(e.target.value)])
83
- }
34
+ const { innerClasses, containerClasses } = useLegendClasses(config)
35
+ const { visualizationType, visualizationSubType, series, runtime, orientation } = config
84
36
 
85
37
  const createLegendLabels = defaultLabels => {
86
38
  const colorCode = config.legend?.colorCode
87
- if (config.visualizationType === 'Deviation Bar') {
39
+ if (visualizationType === 'Deviation Bar') {
88
40
  const [belowColor, aboveColor] = twoColorPalette[config.twoColor.palette]
89
41
  const labelBelow = {
90
42
  datum: 'X',
@@ -101,20 +53,20 @@ const Legend = () => {
101
53
 
102
54
  return [labelBelow, labelAbove]
103
55
  }
104
- if (config.visualizationType === 'Bar' && config.visualizationSubType === 'regular' && colorCode && config.series?.length === 1) {
56
+ if (visualizationType === 'Bar' && visualizationSubType === 'regular' && colorCode && series?.length === 1) {
105
57
  let palette = colorPalettes[config.palette]
106
58
 
107
59
  while (tableData.length > palette.length) {
108
60
  palette = palette.concat(palette)
109
61
  }
110
62
  palette = palette.slice(0, data.length)
111
- //store uniq values to Set by colorCode
63
+ //store unique values to Set by colorCode
112
64
  const set = new Set()
113
65
 
114
66
  tableData.forEach(d => set.add(d[colorCode]))
115
67
 
116
- // create labels with uniq values
117
- const uniqeLabels = Array.from(set).map((val, i) => {
68
+ // create labels with unique values
69
+ const uniqueLabels = Array.from(set).map((val, i) => {
118
70
  const newLabel = {
119
71
  datum: val,
120
72
  index: i,
@@ -124,226 +76,188 @@ const Legend = () => {
124
76
  return newLabel
125
77
  })
126
78
 
127
- return uniqeLabels
79
+ return uniqueLabels
128
80
  }
129
- return defaultLabels
130
- }
131
81
 
132
- const isBottomOrSmallViewport = config.legend.position === 'bottom' || currentViewport === 'sm' || currentViewport === 'xs' || currentViewport === 'xxs'
133
- const isHorizontal = config.orientation === 'horizontal'
134
- const marginTop = isBottomOrSmallViewport && isHorizontal ? `${config.runtime.xAxis.size}px` : '0px'
135
- const marginBottom = isBottomOrSmallViewport ? '15px' : '0px'
82
+ // get forecasting items inside of combo
83
+ if (runtime?.forecastingSeriesKeys?.length > 0) {
84
+ let seriesLabels = []
136
85
 
137
- const { HighLightedBarUtils } = useHighlightedBars(config)
86
+ //store unique values to Set by colorCode
138
87
 
139
- let highLightedLegendItems = HighLightedBarUtils.findDuplicates(config.highlightedBarValues)
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'
140
92
 
141
- if (!legend) return null
93
+ const newLabel = {
94
+ datum: stage.key,
95
+ index: index,
96
+ text: stage.key,
97
+ value: colorValue
98
+ }
142
99
 
143
- if (!legend.dynamicLegend)
144
- return config.visualizationType !== 'Box Plot' ? (
145
- <aside style={{ marginTop, marginBottom }} id='legend' className={containerClasses.join(' ')} role='region' aria-label='legend' tabIndex={0}>
146
- {legend.label && <h2>{parse(legend.label)}</h2>}
147
- {legend.description && <p>{parse(legend.description)}</p>}
148
- <LegendOrdinal scale={colorScale} itemDirection='row' labelMargin='0 20px 0 0' shapeMargin='0 10px 0'>
149
- {labels => (
150
- <div className={innerClasses.join(' ')}>
151
- {createLegendLabels(labels).map((label, i) => {
152
- let className = 'legend-item'
153
- let itemName = label.datum
154
-
155
- // Filter excluded data keys from legend
156
- if (config.exclusions.active && config.exclusions.keys?.includes(itemName)) {
157
- return null
158
- }
159
-
160
- if (config.runtime.seriesLabels) {
161
- let index = config.runtime.seriesLabelsAll.indexOf(itemName)
162
- itemName = config.runtime.seriesKeys[index]
163
- }
164
-
165
- if (seriesHighlight.length > 0 && false === seriesHighlight.includes(itemName)) {
166
- className += ' inactive'
167
- }
168
-
169
- return (
170
- <LegendItem
171
- className={className}
172
- tabIndex={0}
173
- key={`legend-quantile-${i}`}
174
- onKeyPress={e => {
175
- if (e.key === 'Enter') {
176
- highlight(label)
177
- }
178
- }}
179
- onClick={() => {
180
- highlight(label)
181
- }}
182
- >
183
- <LegendCircle fill={label.value} />
184
- <LegendLabel align='left' margin='0 0 0 4px'>
185
- {label.text}
186
- </LegendLabel>
187
- </LegendItem>
188
- )
189
- })}
190
-
191
- {highLightedLegendItems.map((bar, i) => {
192
- // if duplicates only return first item
193
- let className = 'legend-item'
194
- let itemName = bar.legendLabel
195
-
196
- if (!itemName) return
197
- if (seriesHighlight.length > 0 && false === seriesHighlight.includes(itemName)) {
198
- className += ' inactive'
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(bar.legendLabel)
208
- }
209
- }}
210
- onClick={() => {
211
- highlight(bar.legendLabel)
212
- }}
213
- >
214
- <LegendCircle fill='transparent' borderColor={bar.color ? bar.color : `rgba(255, 102, 1)`} />{' '}
215
- <LegendLabel align='left' margin='0 0 0 4px'>
216
- {bar.legendLabel ? bar.legendLabel : bar.value}
217
- </LegendLabel>
218
- </LegendItem>
219
- )
220
- })}
221
- {seriesHighlight.length > 0 && (
222
- <button className={`legend-reset ${config.theme}`} onClick={labels => highlightReset(labels)} tabIndex={0}>
223
- Reset
224
- </button>
225
- )}
226
- </div>
227
- )}
228
- </LegendOrdinal>
229
- </aside>
230
- ) : (
231
- <aside id='legend' className={containerClasses.join(' ')} role='region' aria-label='legend' tabIndex={0}>
232
- {config.boxplot.legend.displayHowToReadText && <h3>{config.boxplot.legend.howToReadText}</h3>}
233
- </aside>
234
- )
235
- return (
236
- config.visualizationType !== 'Box Plot' && (
237
- <aside id='legend' className={containerClasses.join(' ')} role='region' aria-label='legend' tabIndex={0}>
238
- {legend.label && <h2>{parse(legend.label)}</h2>}
239
- {legend.description && <p>{parse(legend.description)}</p>}
100
+ seriesLabels.push(newLabel)
101
+ })
102
+ })
240
103
 
241
- <LegendOrdinal scale={colorScale} itemDirection='row' labelMargin='0 20px 0 0' shapeMargin='0 10px 0'>
242
- {labels => {
243
- if (
244
- Number(config.legend.dynamicLegendItemLimit) > dynamicLegendItems.length && // legend items are less than limit
245
- dynamicLegendItems.length !== config.runtime.seriesLabelsAll.length
246
- ) {
247
- // legend items are equal to series length
248
- return (
249
- <select className='dynamic-legend-dropdown' onChange={e => handleDynamicLegendChange(e)}>
250
- <option className={'all'} tabIndex={0} value={JSON.stringify({ text: config.legend.dynamicLegendDefaultText })}>
251
- {config.legend.dynamicLegendDefaultText}
252
- </option>
253
- {labels.map((label, i) => {
254
- let className = 'legend-item'
255
- let itemName = label.datum
256
- let inDynamicList = false
257
-
258
- // Filter excluded data keys from legend
259
- if (config.exclusions.active && config.exclusions.keys?.includes(itemName)) {
260
- return null
261
- }
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'
262
108
 
263
- if (config.runtime.seriesLabels) {
264
- let index = config.runtime.seriesLabelsAll.indexOf(itemName)
265
- itemName = config.runtime.seriesKeys[index]
266
- }
109
+ const newLabel = {
110
+ datum: bar,
111
+ index: index,
112
+ text: bar,
113
+ value: colorValue
114
+ }
267
115
 
268
- if (seriesHighlight.length > 0 && false === seriesHighlight.includes(itemName)) {
269
- className += ' inactive'
270
- }
116
+ seriesLabels.push(newLabel)
117
+ })
271
118
 
272
- dynamicLegendItems.map(listItem => {
273
- if (listItem.text === label.text) {
274
- inDynamicList = true
275
- }
276
- return null
277
- })
119
+ return seriesLabels
120
+ }
278
121
 
279
- if (inDynamicList) return true
280
- let palette = colorPalettes[config.palette]
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]
281
126
 
282
- label.value = palette[dynamicLegendItems.length]
127
+ while (tableData.length > palette.length) {
128
+ palette = palette.concat(palette)
129
+ }
283
130
 
284
- return (
285
- <option className={className} tabIndex={0} value={JSON.stringify(label)}>
286
- {label.text}
287
- </option>
288
- )
289
- })}
290
- </select>
291
- )
292
- } else {
293
- return config.legend.dynamicLegendItemLimitMessage
294
- }
295
- }}
296
- </LegendOrdinal>
131
+ palette = palette.slice(0, data.length)
132
+ //store unique values to Set by colorCode
133
+ const set = new Set()
134
+
135
+ config.series.forEach(d => {
136
+ set.add(d['name'] ? d['name'] : d['dataKey'])
137
+ })
138
+
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
+ })
297
149
 
298
- <div className='dynamic-legend-list'>
299
- {dynamicLegendItems.map((label, i) => {
300
- let className = ['legend-item']
301
- let itemName = label.text
302
- let palette = colorPalettes[config.palette]
150
+ return uniqueLabels
151
+ }
303
152
 
304
- // Filter excluded data keys from legend
305
- if (config.exclusions.active && config.exclusions.keys?.includes(itemName)) {
306
- return null
307
- }
153
+ return defaultLabels
154
+ }
308
155
 
309
- if (config.runtime.seriesLabels && !config.legend.dynamicLegend) {
310
- let index = config.runtime.seriesLabelsAll.indexOf(itemName)
311
- itemName = config.runtime.seriesKeys[index]
312
- }
156
+ const isBottomOrSmallViewport = legend.position === 'bottom' || currentViewport === 'sm' || currentViewport === 'xs' || currentViewport === 'xxs'
313
157
 
314
- if (seriesHighlight.length > 0 && !seriesHighlight.includes(itemName)) {
315
- className.push('inactive')
316
- }
158
+ const legendClasses = {
159
+ marginBottom: isBottomOrSmallViewport ? '15px' : '0px',
160
+ marginTop: isBottomOrSmallViewport && orientation === 'horizontal' ? `${config.runtime.xAxis.size}px` : '0px'
161
+ }
317
162
 
318
- if (seriesHighlight.length === 0 && config.legend.dynamicLegend) {
319
- className.push('inactive')
320
- }
163
+ const { HighLightedBarUtils } = useHighlightedBars(config)
321
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 => {
322
176
  return (
323
- <>
324
- <LegendItem className={className.join(' ')} tabIndex={0} key={`dynamic-legend-item-${i}`} alignItems='center'>
325
- <button
326
- className='btn-wrapper'
327
- onClick={() => {
328
- highlight(label)
329
- }}
330
- >
331
- <LegendCircle fill={palette[i]} config={config} />
332
- <LegendLabel align='space-between' margin='4px 0 0 4px'>
333
- {label.text}
334
- </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
335
255
  </button>
336
- <button onClick={() => removeDynamicLegendItem(label)}>x</button>
337
- </LegendItem>
338
- </>
256
+ )}
257
+ </div>
339
258
  )
340
- })}
341
- </div>
342
- {seriesHighlight.length < dynamicLegendItems.length && (
343
- <button className={`legend-reset legend-reset--dynamic ${config.theme}`} onClick={highlightReset} tabIndex={0}>
344
- Reset
345
- </button>
346
- )}
259
+ }}
260
+ </LegendOrdinal>
347
261
  </aside>
348
262
  )
349
263
  )