@cdc/chart 4.24.1 → 4.24.2
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 +30014 -29757
- package/examples/feature/line/line-chart-preliminary.json +84 -37
- package/examples/feature/regions/index.json +9 -5
- package/index.html +4 -2
- package/package.json +2 -2
- package/src/CdcChart.tsx +41 -24
- package/src/_stories/ChartEditor.stories.tsx +1 -1
- package/src/_stories/_mock/pie_config.json +4 -3
- package/src/components/AreaChart/components/AreaChart.jsx +1 -25
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +7 -5
- package/src/components/BarChart/components/BarChart.Vertical.tsx +12 -13
- package/src/components/BoxPlot/BoxPlot.jsx +9 -8
- package/src/components/EditorPanel/EditorPanel.tsx +1563 -1959
- package/src/components/EditorPanel/EditorPanelContext.ts +40 -0
- package/src/components/EditorPanel/components/Panels/Panel.BoxPlot.tsx +148 -0
- package/src/components/EditorPanel/components/{Panel.ForestPlotSettings.tsx → Panels/Panel.ForestPlotSettings.tsx} +16 -7
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +160 -0
- package/src/components/EditorPanel/components/{Panel.Regions.tsx → Panels/Panel.Regions.tsx} +5 -5
- package/src/components/EditorPanel/components/{Panel.Series.tsx → Panels/Panel.Series.tsx} +4 -4
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +297 -0
- package/src/components/EditorPanel/components/Panels/index.tsx +17 -0
- package/src/components/EditorPanel/editor-panel.scss +1 -13
- package/src/components/EditorPanel/useEditorPermissions.js +5 -0
- package/src/components/Legend/Legend.Component.tsx +199 -0
- package/src/components/Legend/Legend.tsx +5 -324
- package/src/components/Legend/helpers/createFormatLabels.tsx +140 -0
- package/src/components/LineChart/LineChartProps.ts +1 -1
- package/src/components/LineChart/components/LineChart.Circle.tsx +85 -52
- package/src/components/LineChart/helpers.ts +2 -2
- package/src/components/LineChart/index.tsx +97 -21
- package/src/components/LinearChart.jsx +3 -3
- package/src/components/PairedBarChart.jsx +4 -2
- package/src/components/PieChart/PieChart.tsx +78 -25
- package/src/components/Regions/components/Regions.tsx +14 -5
- package/src/data/initial-state.js +5 -2
- package/src/helpers/computeMarginBottom.ts +2 -2
- package/src/hooks/useHighlightedBars.js +1 -1
- package/src/hooks/useMinMax.ts +3 -3
- package/src/hooks/useScales.ts +18 -5
- package/src/hooks/useTooltip.tsx +11 -7
- package/src/scss/main.scss +0 -67
- package/src/types/ChartConfig.ts +17 -8
- package/src/types/ChartContext.ts +10 -4
- package/src/types/Label.ts +7 -0
- package/examples/private/chart-t.json +0 -3740
- package/examples/private/combo.json +0 -369
- package/examples/private/epi-data.csv +0 -13
- package/examples/private/epi-data.json +0 -62
- package/examples/private/epi.json +0 -403
- package/examples/private/occupancy.json +0 -109283
- package/examples/private/prod-line-config.json +0 -401
- package/examples/private/region-data.json +0 -822
- package/examples/private/region-testing.json +0 -312
- package/examples/private/scaling.json +0 -45325
- package/examples/private/testing-data.json +0 -1739
- package/examples/private/testing.json +0 -816
- package/src/components/EditorPanel/components/Panel.DateHighlighting.tsx +0 -109
- package/src/components/EditorPanel/components/Panels.tsx +0 -13
|
@@ -1,26 +1,7 @@
|
|
|
1
1
|
import { useContext } from 'react'
|
|
2
2
|
import ConfigContext from '../../ConfigContext'
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import LegendCircle from '@cdc/core/components/LegendCircle'
|
|
6
|
-
|
|
7
|
-
import useLegendClasses from '../../hooks/useLegendClasses'
|
|
8
|
-
import { useHighlightedBars } from '../../hooks/useHighlightedBars'
|
|
9
|
-
import { handleLineType } from '../../helpers/handleLineType'
|
|
10
|
-
import { Line } from '@visx/shape'
|
|
11
|
-
import { colorPalettesChart as colorPalettes, sequentialPalettes, twoColorPalette } from '@cdc/core/data/colorPalettes'
|
|
12
|
-
import { scaleOrdinal } from '@visx/scale'
|
|
13
|
-
import { FaStar } from 'react-icons/fa'
|
|
14
|
-
import { Text } from '@visx/text'
|
|
15
|
-
import { Group } from '@visx/group'
|
|
16
|
-
|
|
17
|
-
type Label = {
|
|
18
|
-
datum: string
|
|
19
|
-
index: number
|
|
20
|
-
text: string
|
|
21
|
-
value: string
|
|
22
|
-
icon?: any
|
|
23
|
-
}
|
|
3
|
+
import LegendComponent from './Legend.Component'
|
|
4
|
+
import { createFormatLabels } from './helpers/createFormatLabels'
|
|
24
5
|
|
|
25
6
|
/* eslint-disable jsx-a11y/no-noninteractive-tabindex, jsx-a11y/no-static-element-interactions */
|
|
26
7
|
const Legend = () => {
|
|
@@ -36,312 +17,12 @@ const Legend = () => {
|
|
|
36
17
|
currentViewport
|
|
37
18
|
} = useContext(ConfigContext)
|
|
38
19
|
|
|
39
|
-
|
|
40
|
-
const { visualizationType, visualizationSubType, series, runtime, orientation, legend } = config
|
|
20
|
+
if (!config.legend) return null
|
|
41
21
|
// create fn to reverse labels while legend is Bottom. Legend-right , legend-left works by default.
|
|
42
|
-
const reverseLabels = labels => (config.legend.reverseLabelOrder && config.legend.position === 'bottom' ? labels.reverse() : labels)
|
|
43
|
-
const displayScale = scaleOrdinal({
|
|
44
|
-
domain: config.suppressedData?.map(d => d.label),
|
|
45
|
-
range: ['none'],
|
|
46
|
-
unknown: 'block'
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
const renderDashes = style => {
|
|
50
|
-
const dashCount = style === 'Dashed Small' ? 3 : 2
|
|
51
|
-
const dashClass = `dashes ${style.toLowerCase().replace(' ', '-')}`
|
|
52
|
-
|
|
53
|
-
return (
|
|
54
|
-
<div className={dashClass}>
|
|
55
|
-
{Array.from({ length: dashCount }, (_, i) => (
|
|
56
|
-
<span key={i}>-</span>
|
|
57
|
-
))}
|
|
58
|
-
</div>
|
|
59
|
-
)
|
|
60
|
-
}
|
|
61
|
-
const renderDashesOrCircle = style => {
|
|
62
|
-
if (['Dashed Small', 'Dashed Medium', 'Dashed Large'].includes(style)) {
|
|
63
|
-
return renderDashes(style)
|
|
64
|
-
} else if (style === 'Open Circles') {
|
|
65
|
-
return <div className='dashes open-circles'></div>
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
const createLegendLabels = (defaultLabels): Label[] => {
|
|
69
|
-
const colorCode = config.legend?.colorCode
|
|
70
|
-
if (visualizationType === 'Deviation Bar') {
|
|
71
|
-
const [belowColor, aboveColor] = twoColorPalette[config.twoColor.palette]
|
|
72
|
-
const labelBelow = {
|
|
73
|
-
datum: 'X',
|
|
74
|
-
index: 0,
|
|
75
|
-
text: `Below ${config.xAxis.targetLabel}`,
|
|
76
|
-
value: belowColor
|
|
77
|
-
}
|
|
78
|
-
const labelAbove = {
|
|
79
|
-
datum: 'X',
|
|
80
|
-
index: 1,
|
|
81
|
-
text: `Above ${config.xAxis.targetLabel}`,
|
|
82
|
-
value: aboveColor
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return reverseLabels([labelBelow, labelAbove])
|
|
86
|
-
}
|
|
87
|
-
if (visualizationType === 'Bar' && visualizationSubType === 'regular' && colorCode && series?.length === 1) {
|
|
88
|
-
let palette = colorPalettes[config.palette]
|
|
89
|
-
|
|
90
|
-
while (tableData.length > palette.length) {
|
|
91
|
-
palette = palette.concat(palette)
|
|
92
|
-
}
|
|
93
|
-
palette = palette.slice(0, data.length)
|
|
94
|
-
//store unique values to Set by colorCode
|
|
95
|
-
const set = new Set()
|
|
96
|
-
|
|
97
|
-
tableData.forEach(d => set.add(d[colorCode]))
|
|
98
|
-
|
|
99
|
-
// create labels with unique values
|
|
100
|
-
const uniqueLabels = Array.from(set).map((val, i) => {
|
|
101
|
-
const newLabel = {
|
|
102
|
-
datum: val,
|
|
103
|
-
index: i,
|
|
104
|
-
text: val,
|
|
105
|
-
value: palette[i]
|
|
106
|
-
}
|
|
107
|
-
return newLabel
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
return reverseLabels(uniqueLabels)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// get forecasting items inside of combo
|
|
114
|
-
if (runtime?.forecastingSeriesKeys?.length > 0) {
|
|
115
|
-
let seriesLabels = []
|
|
116
|
-
|
|
117
|
-
//store unique values to Set by colorCode
|
|
118
|
-
// loop through each stage/group/area on the chart and create a label
|
|
119
|
-
config.runtime?.forecastingSeriesKeys?.map((outerGroup, index) => {
|
|
120
|
-
return outerGroup?.stages?.map((stage, index) => {
|
|
121
|
-
let colorValue = sequentialPalettes[stage.color]?.[2] ? sequentialPalettes[stage.color]?.[2] : colorPalettes[stage.color]?.[2] ? colorPalettes[stage.color]?.[2] : '#ccc'
|
|
122
|
-
|
|
123
|
-
const newLabel = {
|
|
124
|
-
datum: stage.key,
|
|
125
|
-
index: index,
|
|
126
|
-
text: stage.key,
|
|
127
|
-
value: colorValue
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
seriesLabels.push(newLabel)
|
|
131
|
-
})
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
// loop through bars for now to meet requirements.
|
|
135
|
-
config.runtime.barSeriesKeys &&
|
|
136
|
-
config.runtime.barSeriesKeys.forEach((bar, index) => {
|
|
137
|
-
let colorValue = colorPalettes[config.palette][index] ? colorPalettes[config.palette][index] : '#ccc'
|
|
138
|
-
|
|
139
|
-
const newLabel = {
|
|
140
|
-
datum: bar,
|
|
141
|
-
index: index,
|
|
142
|
-
text: bar,
|
|
143
|
-
value: colorValue
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
seriesLabels.push(newLabel)
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
return reverseLabels(seriesLabels)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// DEV-4161: replaceable series name in the legend
|
|
153
|
-
const hasNewSeriesName = config.series.map(s => s.name).filter(item => item).length > 0
|
|
154
|
-
if (hasNewSeriesName) {
|
|
155
|
-
let palette = colorPalettes[config.palette]
|
|
156
|
-
|
|
157
|
-
while (tableData.length > palette.length) {
|
|
158
|
-
palette = palette.concat(palette)
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
palette = palette.slice(0, data.length)
|
|
162
|
-
//store unique values to Set by colorCode
|
|
163
|
-
const set = new Set()
|
|
164
|
-
|
|
165
|
-
config.series.forEach(d => {
|
|
166
|
-
set.add(d['name'] ? d['name'] : d['dataKey'])
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
// create labels with unique values
|
|
170
|
-
const uniqueLabels = Array.from(set).map((val, i) => {
|
|
171
|
-
const newLabel = {
|
|
172
|
-
datum: val,
|
|
173
|
-
index: i,
|
|
174
|
-
text: val,
|
|
175
|
-
value: colorScale(val)
|
|
176
|
-
}
|
|
177
|
-
return newLabel
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
return reverseLabels(uniqueLabels)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if ((config.visualizationType === 'Bar' || config.visualizationType === 'Combo') && config.visualizationSubType === 'regular' && config.suppressedData) {
|
|
184
|
-
const lastIndex = defaultLabels.length - 1
|
|
185
|
-
let newLabels = []
|
|
186
|
-
|
|
187
|
-
config.suppressedData?.forEach(({ label, icon }, index) => {
|
|
188
|
-
if (label && icon) {
|
|
189
|
-
const newLabel = {
|
|
190
|
-
datum: label,
|
|
191
|
-
index: lastIndex + index,
|
|
192
|
-
text: label,
|
|
193
|
-
icon: <FaStar color='#000' size={15} />
|
|
194
|
-
}
|
|
195
|
-
newLabels.push(newLabel)
|
|
196
|
-
}
|
|
197
|
-
})
|
|
198
|
-
|
|
199
|
-
return [...defaultLabels, ...newLabels]
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return reverseLabels(defaultLabels)
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const isBottomOrSmallViewport = legend.position === 'bottom' || ['sm', 'xs', 'xxs'].includes(currentViewport)
|
|
206
|
-
|
|
207
|
-
const legendClasses = {
|
|
208
|
-
marginBottom: isBottomOrSmallViewport ? '15px' : '0px',
|
|
209
|
-
marginTop: isBottomOrSmallViewport && orientation === 'horizontal' ? `${config.yAxis.label && config.isResponsiveTicks ? config.dynamicMarginTop : config.runtime.xAxis.size}px` : `${isBottomOrSmallViewport ? config.dynamicMarginTop + 15 : 0}px`
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const { HighLightedBarUtils } = useHighlightedBars(config)
|
|
213
|
-
|
|
214
|
-
let highLightedLegendItems = HighLightedBarUtils.findDuplicates(config.highlightedBarValues)
|
|
215
|
-
if (!legend) return null
|
|
216
|
-
|
|
217
|
-
return (
|
|
218
|
-
config.visualizationType !== 'Box Plot' && (
|
|
219
|
-
<aside style={legendClasses} id='legend' className={containerClasses.join(' ')} role='region' aria-label='legend' tabIndex={0}>
|
|
220
|
-
{legend.label && <h2>{parse(legend.label)}</h2>}
|
|
221
|
-
{legend.description && <p>{parse(legend.description)}</p>}
|
|
222
|
-
<LegendOrdinal scale={colorScale} itemDirection='row' labelMargin='0 20px 0 0' shapeMargin='0 10px 0'>
|
|
223
|
-
{labels => {
|
|
224
|
-
return (
|
|
225
|
-
<>
|
|
226
|
-
<div className={innerClasses.join(' ')}>
|
|
227
|
-
{createLegendLabels(labels).map((label, i) => {
|
|
228
|
-
let className = ['legend-item', `legend-text--${label.text.replace(' ', '').toLowerCase()}`]
|
|
229
|
-
let itemName = label.datum
|
|
230
|
-
|
|
231
|
-
// Filter excluded data keys from legend
|
|
232
|
-
if (config.exclusions.active && config.exclusions.keys?.includes(itemName)) {
|
|
233
|
-
return null
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if (runtime.seriesLabels) {
|
|
237
|
-
let index = config.runtime.seriesLabelsAll.indexOf(itemName)
|
|
238
|
-
itemName = config.runtime.seriesKeys[index]
|
|
239
|
-
|
|
240
|
-
if (runtime?.forecastingSeriesKeys?.length > 0) {
|
|
241
|
-
itemName = label.text
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (seriesHighlight.length > 0 && false === seriesHighlight.includes(itemName)) {
|
|
246
|
-
className.push('inactive')
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return (
|
|
250
|
-
<LegendItem
|
|
251
|
-
className={className.join(' ')}
|
|
252
|
-
tabIndex={0}
|
|
253
|
-
key={`legend-quantile-${i}`}
|
|
254
|
-
onKeyPress={e => {
|
|
255
|
-
if (e.key === 'Enter') {
|
|
256
|
-
highlight(label)
|
|
257
|
-
}
|
|
258
|
-
}}
|
|
259
|
-
onClick={() => {
|
|
260
|
-
highlight(label)
|
|
261
|
-
}}
|
|
262
|
-
>
|
|
263
|
-
{config.visualizationType === 'Line' && config.legend.lineMode ? (
|
|
264
|
-
<svg width={40} height={20}>
|
|
265
|
-
<Line from={{ x: 10, y: 10 }} to={{ x: 40, y: 10 }} stroke={label.value} strokeWidth={2} strokeDasharray={handleLineType(config.series[i]?.type ? config.series[i]?.type : '')} />
|
|
266
|
-
</svg>
|
|
267
|
-
) : (
|
|
268
|
-
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
|
269
|
-
<LegendCircle margin='0' fill={label.value} display={displayScale(label.datum)} />
|
|
270
|
-
<div style={{ marginTop: '2px', marginRight: '6px' }}>{label.icon}</div>
|
|
271
|
-
</div>
|
|
272
|
-
)}
|
|
273
|
-
|
|
274
|
-
<LegendLabel align='left' margin='0 0 0 4px'>
|
|
275
|
-
{label.text}
|
|
276
|
-
</LegendLabel>
|
|
277
|
-
</LegendItem>
|
|
278
|
-
)
|
|
279
|
-
})}
|
|
280
22
|
|
|
281
|
-
|
|
282
|
-
// if duplicates only return first item
|
|
283
|
-
let className = 'legend-item'
|
|
284
|
-
let itemName = bar.legendLabel
|
|
23
|
+
const createLegendLabels = createFormatLabels(config, tableData, data, colorScale)
|
|
285
24
|
|
|
286
|
-
|
|
287
|
-
if (seriesHighlight.length > 0 && false === seriesHighlight.includes(itemName)) {
|
|
288
|
-
className += ' inactive'
|
|
289
|
-
}
|
|
290
|
-
return (
|
|
291
|
-
<LegendItem
|
|
292
|
-
className={className}
|
|
293
|
-
tabIndex={0}
|
|
294
|
-
key={`legend-quantile-${i}`}
|
|
295
|
-
onKeyPress={e => {
|
|
296
|
-
if (e.key === 'Enter') {
|
|
297
|
-
highlight(bar.legendLabel)
|
|
298
|
-
}
|
|
299
|
-
}}
|
|
300
|
-
onClick={() => {
|
|
301
|
-
highlight(bar.legendLabel)
|
|
302
|
-
}}
|
|
303
|
-
>
|
|
304
|
-
<LegendCircle fill='transparent' borderColor={bar.color ? bar.color : `rgba(255, 102, 1)`} />{' '}
|
|
305
|
-
<LegendLabel align='left' margin='0 0 0 4px'>
|
|
306
|
-
{bar.legendLabel ? bar.legendLabel : bar.value}
|
|
307
|
-
</LegendLabel>
|
|
308
|
-
</LegendItem>
|
|
309
|
-
)
|
|
310
|
-
})}
|
|
311
|
-
{seriesHighlight.length > 0 && (
|
|
312
|
-
<button className={`legend-reset ${config.theme}`} onClick={labels => highlightReset(labels)} tabIndex={0}>
|
|
313
|
-
Reset
|
|
314
|
-
</button>
|
|
315
|
-
)}
|
|
316
|
-
</div>
|
|
317
|
-
<>
|
|
318
|
-
{config.preliminaryData.some(pd => pd.label) && (
|
|
319
|
-
<>
|
|
320
|
-
<hr></hr>
|
|
321
|
-
<div className={config.legend.singleRow && isBottomOrSmallViewport ? 'legend-container__inner bottom single-row' : 'dash-left'}>
|
|
322
|
-
{config.preliminaryData.map((pd, index) => {
|
|
323
|
-
return (
|
|
324
|
-
<div className='dash-container' key={index}>
|
|
325
|
-
{pd.label && (
|
|
326
|
-
<>
|
|
327
|
-
<div className='dash-inner'>{renderDashesOrCircle(pd.style)}</div>
|
|
328
|
-
<div style={{ marginLeft: '7px' }}>{pd.label}</div>
|
|
329
|
-
</>
|
|
330
|
-
)}
|
|
331
|
-
</div>
|
|
332
|
-
)
|
|
333
|
-
})}
|
|
334
|
-
</div>
|
|
335
|
-
</>
|
|
336
|
-
)}
|
|
337
|
-
</>
|
|
338
|
-
</>
|
|
339
|
-
)
|
|
340
|
-
}}
|
|
341
|
-
</LegendOrdinal>
|
|
342
|
-
</aside>
|
|
343
|
-
)
|
|
344
|
-
)
|
|
25
|
+
return !['Box Plot', 'Pie'].includes(config.visualizationType) && <LegendComponent config={config} colorScale={colorScale} seriesHighlight={seriesHighlight} highlight={highlight} highlightReset={highlightReset} currentViewport={currentViewport} formatLabels={createLegendLabels} />
|
|
345
26
|
}
|
|
346
27
|
|
|
347
28
|
export default Legend
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { colorPalettesChart as colorPalettes, sequentialPalettes, twoColorPalette } from '@cdc/core/data/colorPalettes'
|
|
2
|
+
import { FaStar } from 'react-icons/fa'
|
|
3
|
+
import { Label } from '../../../types/Label'
|
|
4
|
+
import { ColorScale, TransformedData } from '../../../types/ChartContext'
|
|
5
|
+
import { ChartConfig } from '../../../types/ChartConfig'
|
|
6
|
+
|
|
7
|
+
export const createFormatLabels =
|
|
8
|
+
(config: ChartConfig, tableData: Object[], data: TransformedData[], colorScale: ColorScale) =>
|
|
9
|
+
(defaultLabels: Label[]): Label[] => {
|
|
10
|
+
const { visualizationType, visualizationSubType, series, runtime } = config
|
|
11
|
+
|
|
12
|
+
const reverseLabels = labels => (config.legend.reverseLabelOrder && config.legend.position === 'bottom' ? labels.reverse() : labels)
|
|
13
|
+
const colorCode = config.legend?.colorCode
|
|
14
|
+
if (visualizationType === 'Deviation Bar') {
|
|
15
|
+
const [belowColor, aboveColor] = twoColorPalette[config.twoColor.palette]
|
|
16
|
+
const labelBelow = {
|
|
17
|
+
datum: 'X',
|
|
18
|
+
index: 0,
|
|
19
|
+
text: `Below ${config.xAxis.targetLabel}`,
|
|
20
|
+
value: belowColor
|
|
21
|
+
}
|
|
22
|
+
const labelAbove = {
|
|
23
|
+
datum: 'X',
|
|
24
|
+
index: 1,
|
|
25
|
+
text: `Above ${config.xAxis.targetLabel}`,
|
|
26
|
+
value: aboveColor
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return reverseLabels([labelBelow, labelAbove])
|
|
30
|
+
}
|
|
31
|
+
if (visualizationType === 'Bar' && visualizationSubType === 'regular' && colorCode && series?.length === 1) {
|
|
32
|
+
let palette = colorPalettes[config.palette]
|
|
33
|
+
|
|
34
|
+
while (tableData.length > palette.length) {
|
|
35
|
+
palette = palette.concat(palette)
|
|
36
|
+
}
|
|
37
|
+
palette = palette.slice(0, data.length)
|
|
38
|
+
//store unique values to Set by colorCode
|
|
39
|
+
const set = new Set()
|
|
40
|
+
|
|
41
|
+
tableData.forEach(d => set.add(d[colorCode]))
|
|
42
|
+
|
|
43
|
+
// create labels with unique values
|
|
44
|
+
const uniqueLabels = Array.from(set).map((val, i) => {
|
|
45
|
+
const newLabel = {
|
|
46
|
+
datum: val,
|
|
47
|
+
index: i,
|
|
48
|
+
text: val,
|
|
49
|
+
value: palette[i]
|
|
50
|
+
}
|
|
51
|
+
return newLabel
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
return reverseLabels(uniqueLabels)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// get forecasting items inside of combo
|
|
58
|
+
if (runtime?.forecastingSeriesKeys?.length > 0) {
|
|
59
|
+
let seriesLabels = []
|
|
60
|
+
|
|
61
|
+
//store unique values to Set by colorCode
|
|
62
|
+
// loop through each stage/group/area on the chart and create a label
|
|
63
|
+
config.runtime?.forecastingSeriesKeys?.map((outerGroup, index) => {
|
|
64
|
+
return outerGroup?.stages?.map((stage, index) => {
|
|
65
|
+
let colorValue = sequentialPalettes[stage.color]?.[2] ? sequentialPalettes[stage.color]?.[2] : colorPalettes[stage.color]?.[2] ? colorPalettes[stage.color]?.[2] : '#ccc'
|
|
66
|
+
|
|
67
|
+
const newLabel = {
|
|
68
|
+
datum: stage.key,
|
|
69
|
+
index: index,
|
|
70
|
+
text: stage.key,
|
|
71
|
+
value: colorValue
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
seriesLabels.push(newLabel)
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// loop through bars for now to meet requirements.
|
|
79
|
+
config.runtime.barSeriesKeys &&
|
|
80
|
+
config.runtime.barSeriesKeys.forEach((bar, index) => {
|
|
81
|
+
let colorValue = colorPalettes[config.palette][index] ? colorPalettes[config.palette][index] : '#ccc'
|
|
82
|
+
|
|
83
|
+
const newLabel = {
|
|
84
|
+
datum: bar,
|
|
85
|
+
index: index,
|
|
86
|
+
text: bar,
|
|
87
|
+
value: colorValue
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
seriesLabels.push(newLabel)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
return reverseLabels(seriesLabels)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// DEV-4161: replaceable series name in the legend
|
|
97
|
+
const hasNewSeriesName = config.series.filter(item => !!item.name).length > 0
|
|
98
|
+
if (hasNewSeriesName) {
|
|
99
|
+
//store unique values to Set by colorCode
|
|
100
|
+
const set = new Set()
|
|
101
|
+
|
|
102
|
+
config.series.forEach(d => {
|
|
103
|
+
set.add(d.name || d.dataKey)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
// create labels with unique values
|
|
107
|
+
const uniqueLabels = Array.from(set).map((val, i) => {
|
|
108
|
+
const newLabel = {
|
|
109
|
+
datum: val,
|
|
110
|
+
index: i,
|
|
111
|
+
text: val,
|
|
112
|
+
value: colorScale(val)
|
|
113
|
+
}
|
|
114
|
+
return newLabel
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
return reverseLabels(uniqueLabels)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if ((config.visualizationType === 'Bar' || config.visualizationType === 'Combo') && config.visualizationSubType === 'regular' && config.suppressedData) {
|
|
121
|
+
const lastIndex = defaultLabels.length - 1
|
|
122
|
+
let newLabels = []
|
|
123
|
+
|
|
124
|
+
config.suppressedData?.forEach(({ label, icon }, index) => {
|
|
125
|
+
if (label && icon) {
|
|
126
|
+
const newLabel = {
|
|
127
|
+
datum: label,
|
|
128
|
+
index: lastIndex + index,
|
|
129
|
+
text: label,
|
|
130
|
+
icon: <FaStar color='#000' size={15} />
|
|
131
|
+
}
|
|
132
|
+
newLabels.push(newLabel)
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
return [...defaultLabels, ...newLabels]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return reverseLabels(defaultLabels)
|
|
140
|
+
}
|
|
@@ -21,17 +21,15 @@ type LineChartCircleProps = {
|
|
|
21
21
|
colorScale: any
|
|
22
22
|
parseDate: any
|
|
23
23
|
seriesAxis: string
|
|
24
|
+
dataIndex: number
|
|
25
|
+
mode: 'ISOLATED_POINTS' | 'HOVER_POINTS' | 'ALWAYS_SHOW_POINTS'
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
const LineChartCircle = (props: LineChartCircleProps) => {
|
|
27
|
-
const { config, d, displayArea, seriesKey, tooltipData, xScale, yScale, colorScale, parseDate, yScaleRight, data, circleData } = props
|
|
29
|
+
const { config, d, displayArea, seriesKey, tooltipData, xScale, yScale, colorScale, parseDate, yScaleRight, data, circleData, dataIndex, mode } = props
|
|
28
30
|
const { lineDatapointStyle } = config
|
|
29
31
|
const filtered = config?.series.filter(s => s.dataKey === seriesKey)?.[0]
|
|
30
32
|
// If we're not showing the circle, simply return
|
|
31
|
-
if (lineDatapointStyle === 'hidden') return <></>
|
|
32
|
-
|
|
33
|
-
const getIndex = seriesKey => config.runtime.seriesLabelsAll.indexOf(seriesKey)
|
|
34
|
-
|
|
35
33
|
const getColor = (displayArea: boolean, colorScale: Function, config: ChartConfig, hoveredKey: string, seriesKey: string) => {
|
|
36
34
|
const seriesLabels = config.runtime.seriesLabels || []
|
|
37
35
|
let color
|
|
@@ -47,65 +45,100 @@ const LineChartCircle = (props: LineChartCircleProps) => {
|
|
|
47
45
|
}
|
|
48
46
|
return color
|
|
49
47
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (isMatch) {
|
|
53
|
-
return <></>
|
|
54
|
-
}
|
|
55
|
-
return (
|
|
56
|
-
<circle
|
|
57
|
-
cx={config.xAxis.type === 'categorical' ? xScale(d[config.xAxis.dataKey]) : xScale(parseDate(d[config.xAxis.dataKey]))}
|
|
58
|
-
cy={filtered.axis === 'Right' ? yScaleRight(d[filtered.dataKey]) : yScale(d[filtered.dataKey])}
|
|
59
|
-
r={4.5}
|
|
60
|
-
opacity={d[seriesKey] ? 1 : 0}
|
|
61
|
-
fillOpacity={1}
|
|
62
|
-
fill={getColor(displayArea, colorScale, config, seriesKey, seriesKey)}
|
|
63
|
-
style={{ filter: 'unset', opacity: 1 }}
|
|
64
|
-
/>
|
|
65
|
-
)
|
|
48
|
+
const getXPos = hoveredXValue => {
|
|
49
|
+
return (config.xAxis.type === 'categorical' ? xScale(hoveredXValue) : xScale(parseDate(hoveredXValue))) + (xScale.bandwidth ? xScale.bandwidth() / 2 : 0)
|
|
66
50
|
}
|
|
51
|
+
if (mode === 'ALWAYS_SHOW_POINTS') {
|
|
52
|
+
if (lineDatapointStyle === 'hidden') return <></>
|
|
53
|
+
const getIndex = seriesKey => config.runtime.seriesLabelsAll.indexOf(seriesKey)
|
|
67
54
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (!seriesKey) return
|
|
71
|
-
if (!tooltipData.data) return
|
|
72
|
-
let hoveredXValue = tooltipData?.data?.[0]?.[1]
|
|
73
|
-
if (!hoveredXValue) return
|
|
74
|
-
|
|
75
|
-
let hoveredSeriesValue
|
|
76
|
-
let hoveredSeriesIndex
|
|
77
|
-
let hoveredSeriesData = tooltipData.data.filter(d => d[0] === seriesKey)
|
|
78
|
-
let hoveredSeriesKey = hoveredSeriesData?.[0]?.[0]
|
|
79
|
-
let hoveredSeriesAxis = hoveredSeriesData?.[0]?.[2]
|
|
80
|
-
if (!hoveredSeriesKey) return
|
|
81
|
-
hoveredSeriesIndex = tooltipData?.data.indexOf(hoveredSeriesKey)
|
|
82
|
-
hoveredSeriesValue = data?.find(d => {
|
|
83
|
-
return d[config?.xAxis.dataKey] === hoveredXValue
|
|
84
|
-
})?.[seriesKey]
|
|
85
|
-
|
|
86
|
-
// hoveredSeriesValue = extractNumber(hoveredSeriesValue)
|
|
87
|
-
return tooltipData?.data.map((tooltipItem, index) => {
|
|
88
|
-
let seriesIndex = config.runtime.seriesLabelsAll.indexOf(hoveredXValue)
|
|
89
|
-
|
|
90
|
-
if (isNaN(hoveredSeriesValue)) return <></>
|
|
91
|
-
const isMatch = circleData?.some(cd => cd[config.xAxis.dataKey] === hoveredXValue)
|
|
92
|
-
|
|
55
|
+
if (lineDatapointStyle === 'always show') {
|
|
56
|
+
const isMatch = circleData?.some(cd => cd[config.xAxis.dataKey] === d[config.xAxis.dataKey] && cd[seriesKey] === d[seriesKey])
|
|
93
57
|
if (isMatch) {
|
|
94
58
|
return <></>
|
|
95
59
|
}
|
|
96
60
|
return (
|
|
97
61
|
<circle
|
|
98
|
-
cx={config.xAxis.
|
|
99
|
-
cy={
|
|
62
|
+
cx={getXPos(d[config.xAxis.dataKey])}
|
|
63
|
+
cy={filtered.axis === 'Right' ? yScaleRight(d[filtered.dataKey]) : yScale(d[filtered.dataKey])}
|
|
100
64
|
r={4.5}
|
|
101
|
-
opacity={1}
|
|
65
|
+
opacity={d[seriesKey] ? 1 : 0}
|
|
102
66
|
fillOpacity={1}
|
|
103
|
-
fill={getColor(displayArea, colorScale, config,
|
|
67
|
+
fill={getColor(displayArea, colorScale, config, seriesKey, seriesKey)}
|
|
104
68
|
style={{ filter: 'unset', opacity: 1 }}
|
|
105
|
-
key={`line-chart-circle--${JSON.stringify(tooltipItem)}--${index}`}
|
|
106
69
|
/>
|
|
107
70
|
)
|
|
108
|
-
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (mode === 'HOVER_POINTS') {
|
|
75
|
+
if (lineDatapointStyle === 'hover') {
|
|
76
|
+
if (!tooltipData) return
|
|
77
|
+
if (!seriesKey) return
|
|
78
|
+
if (!tooltipData.data) return
|
|
79
|
+
let hoveredXValue = tooltipData?.data?.[0]?.[1]
|
|
80
|
+
if (!hoveredXValue) return
|
|
81
|
+
|
|
82
|
+
let hoveredSeriesValue
|
|
83
|
+
let hoveredSeriesIndex
|
|
84
|
+
let hoveredSeriesData = tooltipData.data.filter(d => d[0] === seriesKey)
|
|
85
|
+
let hoveredSeriesKey = hoveredSeriesData?.[0]?.[0]
|
|
86
|
+
let hoveredSeriesAxis = hoveredSeriesData?.[0]?.[2]
|
|
87
|
+
if (!hoveredSeriesKey) return
|
|
88
|
+
hoveredSeriesIndex = tooltipData?.data.indexOf(hoveredSeriesKey)
|
|
89
|
+
hoveredSeriesValue = data?.find(d => {
|
|
90
|
+
return d[config?.xAxis.dataKey] === hoveredXValue
|
|
91
|
+
})?.[seriesKey]
|
|
92
|
+
|
|
93
|
+
// hoveredSeriesValue = extractNumber(hoveredSeriesValue)
|
|
94
|
+
return tooltipData?.data.map((tooltipItem, index) => {
|
|
95
|
+
let seriesIndex = config.runtime.seriesLabelsAll.indexOf(hoveredXValue)
|
|
96
|
+
|
|
97
|
+
if (isNaN(hoveredSeriesValue)) return <></>
|
|
98
|
+
const isMatch = circleData?.some(cd => cd[config.xAxis.dataKey] === hoveredXValue)
|
|
99
|
+
|
|
100
|
+
if (isMatch) {
|
|
101
|
+
return <></>
|
|
102
|
+
}
|
|
103
|
+
return (
|
|
104
|
+
<circle
|
|
105
|
+
cx={getXPos(hoveredXValue)}
|
|
106
|
+
cy={hoveredSeriesAxis === 'right' ? yScaleRight(hoveredSeriesValue) : yScale(hoveredSeriesValue)}
|
|
107
|
+
r={4.5}
|
|
108
|
+
opacity={1}
|
|
109
|
+
fillOpacity={1}
|
|
110
|
+
fill={getColor(displayArea, colorScale, config, hoveredSeriesKey, seriesKey)}
|
|
111
|
+
style={{ filter: 'unset', opacity: 1 }}
|
|
112
|
+
key={`line-chart-circle--${JSON.stringify(tooltipItem)}--${index}`}
|
|
113
|
+
/>
|
|
114
|
+
)
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (mode === 'ISOLATED_POINTS') {
|
|
120
|
+
const drawIsolatedPoints = (currentIndex, seriesKey) => {
|
|
121
|
+
const currentPoint = data[currentIndex]
|
|
122
|
+
const previousPoint = data[currentIndex - 1]
|
|
123
|
+
const nextPoint = data[currentIndex + 1]
|
|
124
|
+
if (currentIndex === 0 && !nextPoint[seriesKey]) {
|
|
125
|
+
return true
|
|
126
|
+
}
|
|
127
|
+
if (currentIndex === data.length - 1 && !previousPoint[seriesKey]) {
|
|
128
|
+
return true
|
|
129
|
+
}
|
|
130
|
+
if (currentIndex !== 0 && currentPoint[seriesKey] && !previousPoint[seriesKey] && !nextPoint[seriesKey]) {
|
|
131
|
+
return true
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (mode) {
|
|
136
|
+
if (drawIsolatedPoints(dataIndex, seriesKey)) {
|
|
137
|
+
return (
|
|
138
|
+
<circle cx={getXPos(d[config.xAxis.dataKey])} cy={filtered.axis === 'Right' ? yScaleRight(d[filtered.dataKey]) : yScale(d[filtered.dataKey])} r={5.3} strokeWidth={2} stroke={colorScale(config.runtime.seriesLabels[seriesKey])} fill={colorScale(config.runtime.seriesLabels[seriesKey])} />
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
109
142
|
}
|
|
110
143
|
|
|
111
144
|
return null
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type PreliminaryDataItem, DataItem, StyleProps, Style } from './LineChartProps'
|
|
2
2
|
|
|
3
3
|
export const createStyles = (props: StyleProps): Style[] => {
|
|
4
|
-
const { preliminaryData,
|
|
4
|
+
const { preliminaryData, data, stroke, handleLineType, lineType, seriesKey } = props
|
|
5
5
|
|
|
6
6
|
const validPreliminaryData: PreliminaryDataItem[] = preliminaryData.filter(pd => pd.seriesKey && pd.column && pd.value && pd.type && pd.style)
|
|
7
7
|
const getMatchingPd = (point: DataItem): PreliminaryDataItem => validPreliminaryData.find(pd => pd.seriesKey === seriesKey && point[pd.column] === pd.value && pd.type === 'effect' && pd.style !== 'Open Circles')
|
|
@@ -13,7 +13,7 @@ export const createStyles = (props: StyleProps): Style[] => {
|
|
|
13
13
|
strokeDasharray: lineStyle
|
|
14
14
|
})
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
data.forEach((d, index) => {
|
|
17
17
|
let matchingPd: PreliminaryDataItem = getMatchingPd(d)
|
|
18
18
|
let style: Style = matchingPd ? createStyle(handleLineType(matchingPd.style)) : createStyle(handleLineType(lineType))
|
|
19
19
|
|