@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.
- package/dist/cdcchart.js +54845 -51755
- package/examples/feature/__data__/planet-example-data.json +14 -32
- package/examples/feature/__data__/planet-logaritmic-data.json +56 -0
- package/examples/feature/area/area-chart-category.json +240 -0
- package/examples/feature/bar/example-bar-chart.json +544 -22
- package/examples/feature/bar/new.json +561 -0
- package/examples/feature/bar/planet-chart-logaritmic-config.json +170 -0
- package/examples/feature/boxplot/valid-boxplot.csv +17 -0
- package/examples/feature/combo/right-issues.json +190 -0
- package/examples/feature/filters/filter-testing.json +37 -3
- 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/forecasting/random_data.csv +366 -0
- package/examples/feature/line/line-chart.json +3 -3
- package/examples/feature/test-highlight/test-highlight-2.json +789 -0
- package/examples/feature/test-highlight/test-highlight-vertical.json +561 -0
- package/examples/feature/test-highlight/test-highlight.json +100 -0
- package/examples/feature/tests-non-numerics/stacked-vertical-bar-example-nonnumerics.json +1 -2
- package/examples/gallery/bar-chart-horizontal/horizontal-highlight.json +345 -0
- package/examples/gallery/line/line.json +173 -1
- package/index.html +14 -8
- package/package.json +2 -2
- package/src/CdcChart.jsx +342 -25
- package/src/components/AreaChart.jsx +32 -40
- package/src/components/BarChart.jsx +147 -25
- package/src/components/DataTable.jsx +30 -12
- package/src/components/DeviationBar.jsx +32 -32
- package/src/components/EditorPanel.jsx +1902 -1126
- package/src/components/Forecasting.jsx +147 -0
- package/src/components/Legend.jsx +193 -243
- package/src/components/LineChart.jsx +4 -9
- package/src/components/LinearChart.jsx +263 -285
- package/src/components/Series.jsx +518 -0
- package/src/components/SparkLine.jsx +3 -3
- package/src/data/initial-state.js +24 -5
- package/src/hooks/useHighlightedBars.js +154 -0
- package/src/hooks/useMinMax.js +128 -0
- package/src/hooks/useReduceData.js +31 -57
- package/src/hooks/useRightAxis.js +8 -2
- package/src/hooks/useScales.js +196 -0
- /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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
75
|
-
|
|
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 (
|
|
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 (
|
|
56
|
+
if (visualizationType === 'Bar' && visualizationSubType === 'regular' && colorCode && series?.length === 1) {
|
|
104
57
|
let palette = colorPalettes[config.palette]
|
|
105
58
|
|
|
106
|
-
while (
|
|
59
|
+
while (tableData.length > palette.length) {
|
|
107
60
|
palette = palette.concat(palette)
|
|
108
61
|
}
|
|
109
62
|
palette = palette.slice(0, data.length)
|
|
110
|
-
//store
|
|
63
|
+
//store unique values to Set by colorCode
|
|
111
64
|
const set = new Set()
|
|
112
65
|
|
|
113
|
-
|
|
66
|
+
tableData.forEach(d => set.add(d[colorCode]))
|
|
114
67
|
|
|
115
|
-
// create labels with
|
|
116
|
-
const
|
|
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
|
|
79
|
+
return uniqueLabels
|
|
127
80
|
}
|
|
128
|
-
return defaultLabels
|
|
129
|
-
}
|
|
130
81
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
86
|
+
//store unique values to Set by colorCode
|
|
137
87
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
100
|
+
seriesLabels.push(newLabel)
|
|
101
|
+
})
|
|
102
|
+
})
|
|
231
103
|
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
109
|
+
const newLabel = {
|
|
110
|
+
datum: bar,
|
|
111
|
+
index: index,
|
|
112
|
+
text: bar,
|
|
113
|
+
value: colorValue
|
|
114
|
+
}
|
|
242
115
|
|
|
243
|
-
|
|
244
|
-
|
|
116
|
+
seriesLabels.push(newLabel)
|
|
117
|
+
})
|
|
245
118
|
|
|
246
|
-
|
|
119
|
+
return seriesLabels
|
|
120
|
+
}
|
|
247
121
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
131
|
+
palette = palette.slice(0, data.length)
|
|
132
|
+
//store unique values to Set by colorCode
|
|
133
|
+
const set = new Set()
|
|
272
134
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
135
|
+
config.series.forEach(d => {
|
|
136
|
+
set.add(d['name'] ? d['name'] : d['dataKey'])
|
|
137
|
+
})
|
|
277
138
|
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
283
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
</>
|
|
256
|
+
)}
|
|
257
|
+
</div>
|
|
303
258
|
)
|
|
304
|
-
}
|
|
305
|
-
</
|
|
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
|
)
|