@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.
- package/dist/cdcchart.js +29056 -27475
- package/examples/feature/__data__/planet-example-data.json +15 -16
- package/examples/feature/bar/new.json +561 -0
- package/examples/feature/combo/right-issues.json +190 -0
- package/examples/feature/forecasting/combo-forecasting.json +245 -0
- package/examples/feature/forecasting/forecasting.json +5325 -0
- package/examples/feature/forecasting/index.json +203 -0
- package/examples/feature/line/line-chart.json +1 -1
- package/examples/gallery/line/line.json +173 -1
- package/index.html +12 -6
- package/package.json +2 -2
- package/src/CdcChart.jsx +48 -11
- package/src/components/AreaChart.jsx +8 -23
- package/src/components/BarChart.jsx +65 -3
- package/src/components/DataTable.jsx +30 -12
- package/src/components/EditorPanel.jsx +1803 -1948
- package/src/components/Forecasting.jsx +147 -0
- package/src/components/Legend.jsx +188 -274
- package/src/components/LineChart.jsx +3 -1
- package/src/components/LinearChart.jsx +145 -18
- package/src/components/Series.jsx +518 -0
- package/src/components/SparkLine.jsx +3 -3
- package/src/data/initial-state.js +7 -3
- package/src/hooks/useMinMax.js +36 -0
- package/src/hooks/useRightAxis.js +8 -2
- package/src/hooks/useScales.js +7 -13
|
@@ -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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
76
|
-
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
|
117
|
-
const
|
|
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
|
|
79
|
+
return uniqueLabels
|
|
128
80
|
}
|
|
129
|
-
return defaultLabels
|
|
130
|
-
}
|
|
131
81
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
86
|
+
//store unique values to Set by colorCode
|
|
138
87
|
|
|
139
|
-
|
|
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
|
-
|
|
93
|
+
const newLabel = {
|
|
94
|
+
datum: stage.key,
|
|
95
|
+
index: index,
|
|
96
|
+
text: stage.key,
|
|
97
|
+
value: colorValue
|
|
98
|
+
}
|
|
142
99
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
109
|
+
const newLabel = {
|
|
110
|
+
datum: bar,
|
|
111
|
+
index: index,
|
|
112
|
+
text: bar,
|
|
113
|
+
value: colorValue
|
|
114
|
+
}
|
|
267
115
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
116
|
+
seriesLabels.push(newLabel)
|
|
117
|
+
})
|
|
271
118
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
inDynamicList = true
|
|
275
|
-
}
|
|
276
|
-
return null
|
|
277
|
-
})
|
|
119
|
+
return seriesLabels
|
|
120
|
+
}
|
|
278
121
|
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
127
|
+
while (tableData.length > palette.length) {
|
|
128
|
+
palette = palette.concat(palette)
|
|
129
|
+
}
|
|
283
130
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
let className = ['legend-item']
|
|
301
|
-
let itemName = label.text
|
|
302
|
-
let palette = colorPalettes[config.palette]
|
|
150
|
+
return uniqueLabels
|
|
151
|
+
}
|
|
303
152
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
return null
|
|
307
|
-
}
|
|
153
|
+
return defaultLabels
|
|
154
|
+
}
|
|
308
155
|
|
|
309
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
158
|
+
const legendClasses = {
|
|
159
|
+
marginBottom: isBottomOrSmallViewport ? '15px' : '0px',
|
|
160
|
+
marginTop: isBottomOrSmallViewport && orientation === 'horizontal' ? `${config.runtime.xAxis.size}px` : '0px'
|
|
161
|
+
}
|
|
317
162
|
|
|
318
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
</>
|
|
256
|
+
)}
|
|
257
|
+
</div>
|
|
339
258
|
)
|
|
340
|
-
}
|
|
341
|
-
</
|
|
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
|
)
|