@cdc/chart 4.24.3 → 4.24.5
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 +34377 -33726
- package/examples/feature/line/line-chart.json +361 -37
- package/examples/region-issue.json +2065 -0
- package/examples/test.json +5409 -0
- package/index.html +13 -11
- package/package.json +2 -2
- package/src/CdcChart.tsx +159 -89
- package/src/_stories/Chart.stories.tsx +8 -0
- package/src/_stories/_mock/bar-chart-suppressed.json +474 -0
- package/src/components/AreaChart/components/AreaChart.jsx +2 -2
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +61 -63
- package/src/components/BarChart/components/BarChart.Vertical.tsx +79 -94
- package/src/components/DeviationBar.jsx +4 -2
- package/src/components/EditorPanel/EditorPanel.tsx +1580 -1924
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +19 -2
- package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +0 -1
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +4 -5
- package/src/components/EditorPanel/editor-panel.scss +0 -724
- package/src/components/EditorPanel/useEditorPermissions.js +4 -1
- package/src/components/Legend/Legend.Component.tsx +82 -58
- package/src/components/Legend/Legend.tsx +5 -1
- package/src/components/LineChart/LineChartProps.ts +13 -6
- package/src/components/LineChart/components/LineChart.Circle.tsx +22 -11
- package/src/components/LineChart/helpers.ts +134 -10
- package/src/components/LineChart/index.tsx +69 -42
- package/src/components/LinearChart.jsx +156 -139
- package/src/components/ZoomBrush.tsx +40 -21
- package/src/data/initial-state.js +4 -4
- package/src/hooks/useBarChart.js +47 -22
- package/src/hooks/useMinMax.ts +21 -2
- package/src/hooks/useScales.ts +33 -1
- package/src/hooks/useTooltip.tsx +11 -11
- package/src/scss/main.scss +80 -5
- package/src/types/ChartConfig.ts +3 -13
- package/src/types/ChartContext.ts +4 -0
- package/src/_stories/ChartLine.preliminary.tsx +0 -19
- package/src/_stories/ChartSuppress.stories.tsx +0 -19
- package/src/_stories/_mock/suppress_mock.json +0 -911
- package/src/helpers/computeMarginBottom.ts +0 -56
|
@@ -2,76 +2,51 @@ import parse from 'html-react-parser'
|
|
|
2
2
|
import { LegendOrdinal, LegendItem, LegendLabel } from '@visx/legend'
|
|
3
3
|
import LegendCircle from '@cdc/core/components/LegendCircle'
|
|
4
4
|
import Button from '@cdc/core/components/elements/Button'
|
|
5
|
-
|
|
6
5
|
import useLegendClasses from '../../hooks/useLegendClasses'
|
|
7
6
|
import { useHighlightedBars } from '../../hooks/useHighlightedBars'
|
|
8
7
|
import { handleLineType } from '../../helpers/handleLineType'
|
|
8
|
+
import { useBarChart } from '../../hooks/useBarChart'
|
|
9
9
|
import { Line } from '@visx/shape'
|
|
10
|
-
import { scaleOrdinal } from '@visx/scale'
|
|
11
10
|
import { Label } from '../../types/Label'
|
|
12
11
|
import { ChartConfig } from '../../types/ChartConfig'
|
|
13
12
|
import { ColorScale } from '../../types/ChartContext'
|
|
14
|
-
import { Group } from '@visx/group'
|
|
15
13
|
import { forwardRef } from 'react'
|
|
16
14
|
|
|
17
|
-
interface LegendProps {
|
|
18
|
-
config: ChartConfig
|
|
15
|
+
export interface LegendProps {
|
|
19
16
|
colorScale: ColorScale
|
|
20
|
-
|
|
17
|
+
config: ChartConfig
|
|
18
|
+
currentViewport: 'lg' | 'md' | 'sm' | 'xs' | 'xxs'
|
|
19
|
+
formatLabels: (labels: Label[]) => Label[]
|
|
21
20
|
highlight: Function
|
|
22
21
|
highlightReset: Function
|
|
23
|
-
currentViewport: string
|
|
24
|
-
formatLabels: (labels: Label[]) => Label[]
|
|
25
22
|
ref: React.Ref<() => void>
|
|
23
|
+
seriesHighlight: string[]
|
|
24
|
+
skipId: string
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
/* eslint-disable jsx-a11y/no-noninteractive-tabindex, jsx-a11y/no-static-element-interactions */
|
|
29
|
-
const Legend: React.FC<LegendProps> = forwardRef(({ config, colorScale, seriesHighlight, highlight, highlightReset, currentViewport, formatLabels }, ref) => {
|
|
28
|
+
const Legend: React.FC<LegendProps> = forwardRef(({ config, colorScale, seriesHighlight, highlight, highlightReset, currentViewport, formatLabels, skipId = 'legend' }, ref) => {
|
|
30
29
|
const { innerClasses, containerClasses } = useLegendClasses(config)
|
|
31
30
|
const { runtime, orientation, legend } = config
|
|
32
|
-
if (!legend) return null
|
|
33
|
-
// create fn to reverse labels while legend is Bottom. Legend-right , legend-left works by default.
|
|
34
|
-
const displayScale = scaleOrdinal({
|
|
35
|
-
domain: config.suppressedData?.map(d => d.label),
|
|
36
|
-
range: ['none'],
|
|
37
|
-
unknown: 'block'
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
const renderDashes = style => {
|
|
41
|
-
const dashCount = style === 'Dashed Small' ? 3 : 2
|
|
42
|
-
const dashClass = `dashes ${style.toLowerCase().replace(' ', '-')}`
|
|
43
|
-
|
|
44
|
-
return (
|
|
45
|
-
<div className={dashClass}>
|
|
46
|
-
{Array.from({ length: dashCount }, (_, i) => (
|
|
47
|
-
<span key={i}>-</span>
|
|
48
|
-
))}
|
|
49
|
-
</div>
|
|
50
|
-
)
|
|
51
|
-
}
|
|
52
|
-
const renderDashesOrCircle = style => {
|
|
53
|
-
if (['Dashed Small', 'Dashed Medium', 'Dashed Large'].includes(style)) {
|
|
54
|
-
return renderDashes(style)
|
|
55
|
-
} else if (style === 'Open Circles') {
|
|
56
|
-
return <div className='dashes open-circles'></div>
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
31
|
|
|
32
|
+
if (!legend) return null
|
|
60
33
|
const isBottomOrSmallViewport = legend.position === 'bottom' || ['sm', 'xs', 'xxs'].includes(currentViewport)
|
|
61
34
|
|
|
62
35
|
const legendClasses = {
|
|
63
36
|
marginBottom: isBottomOrSmallViewport ? '15px' : '0px',
|
|
64
|
-
marginTop: isBottomOrSmallViewport
|
|
37
|
+
marginTop: isBottomOrSmallViewport ? '15px' : '0px'
|
|
65
38
|
}
|
|
66
39
|
|
|
67
40
|
const { HighLightedBarUtils } = useHighlightedBars(config)
|
|
68
41
|
|
|
69
42
|
let highLightedLegendItems = HighLightedBarUtils.findDuplicates(config.highlightedBarValues)
|
|
43
|
+
const fontSize = ['sm', 'xs', 'xxs'].includes(currentViewport) ? { fontSize: '11px' } : null
|
|
70
44
|
|
|
71
45
|
return (
|
|
72
|
-
<aside ref={ref} style={legendClasses} id='legend' className={containerClasses.join(' ')} role='region' aria-label='legend' tabIndex={0}>
|
|
46
|
+
<aside ref={ref} style={legendClasses} id={skipId || 'legend'} className={containerClasses.join(' ')} role='region' aria-label='legend' tabIndex={0}>
|
|
73
47
|
{legend.label && <h3>{parse(legend.label)}</h3>}
|
|
74
|
-
{legend.description && <p>{parse(legend.description)}</p>}
|
|
48
|
+
{legend.description && <p style={fontSize}>{parse(legend.description)}</p>}
|
|
49
|
+
|
|
75
50
|
<LegendOrdinal scale={colorScale} itemDirection='row' labelMargin='0 20px 0 0' shapeMargin='0 10px 0'>
|
|
76
51
|
{labels => {
|
|
77
52
|
return (
|
|
@@ -116,18 +91,19 @@ const Legend: React.FC<LegendProps> = forwardRef(({ config, colorScale, seriesHi
|
|
|
116
91
|
}}
|
|
117
92
|
role='button'
|
|
118
93
|
>
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
<
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
94
|
+
<div>
|
|
95
|
+
{config.visualizationType === 'Line' && config.legend.lineMode ? (
|
|
96
|
+
<svg width={40} height={20}>
|
|
97
|
+
<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 : '')} />
|
|
98
|
+
</svg>
|
|
99
|
+
) : (
|
|
100
|
+
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
|
101
|
+
<LegendCircle viewport={currentViewport} margin='0' fill={label.value} display={true} />
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<LegendLabel style={fontSize} align='left' margin='0 0 0 4px'>
|
|
131
107
|
{label.text}
|
|
132
108
|
</LegendLabel>
|
|
133
109
|
</LegendItem>
|
|
@@ -169,19 +145,17 @@ const Legend: React.FC<LegendProps> = forwardRef(({ config, colorScale, seriesHi
|
|
|
169
145
|
</div>
|
|
170
146
|
|
|
171
147
|
<>
|
|
172
|
-
{config?.preliminaryData?.some(pd => pd.label) && ['Line', 'Combo'].includes(config.visualizationType) && (
|
|
148
|
+
{config?.preliminaryData?.some(pd => pd.label && pd.type === 'effect' && pd.style) && ['Line', 'Combo'].includes(config.visualizationType) && (
|
|
173
149
|
<>
|
|
174
150
|
<hr></hr>
|
|
175
151
|
<div className={config.legend.singleRow && isBottomOrSmallViewport ? 'legend-container__inner bottom single-row' : ''}>
|
|
176
152
|
{config?.preliminaryData?.map((pd, index) => {
|
|
177
153
|
return (
|
|
178
154
|
<>
|
|
179
|
-
{pd.label && (
|
|
180
|
-
<div
|
|
181
|
-
<
|
|
182
|
-
|
|
183
|
-
</svg>
|
|
184
|
-
<span> {pd.label}</span>
|
|
155
|
+
{pd.label && pd.type === 'effect' && pd.style && (
|
|
156
|
+
<div key={index} className='legend-preliminary'>
|
|
157
|
+
<span className={pd.symbol}>{pd.lineCode}</span>
|
|
158
|
+
<p> {pd.label}</p>
|
|
185
159
|
</div>
|
|
186
160
|
)}
|
|
187
161
|
</>
|
|
@@ -190,6 +164,56 @@ const Legend: React.FC<LegendProps> = forwardRef(({ config, colorScale, seriesHi
|
|
|
190
164
|
</div>
|
|
191
165
|
</>
|
|
192
166
|
)}
|
|
167
|
+
{!config.legend.hideSuppressedLabels &&
|
|
168
|
+
config?.preliminaryData?.some(pd => pd.label && pd.displayLegend && pd.type === 'suppression' && pd.value && (pd?.style || pd.symbol)) &&
|
|
169
|
+
((config.visualizationType === 'Bar' && config.visualizationSubType === 'regular') || config.visualizationType === 'Line' || config.visualizationType === 'Combo') && (
|
|
170
|
+
<>
|
|
171
|
+
<hr></hr>
|
|
172
|
+
<div className={config.legend.singleRow && isBottomOrSmallViewport ? 'legend-container__inner bottom single-row' : ''}>
|
|
173
|
+
{config?.preliminaryData?.map(
|
|
174
|
+
(pd, index) =>
|
|
175
|
+
pd.displayLegend &&
|
|
176
|
+
pd.type === 'suppression' && (
|
|
177
|
+
<>
|
|
178
|
+
{config.visualizationType === 'Bar' && (
|
|
179
|
+
<>
|
|
180
|
+
<div key={index + 'Bar'} className={`legend-preliminary ${pd.symbol}`}>
|
|
181
|
+
<span className={pd.symbol}>{pd.iconCode}</span>
|
|
182
|
+
<p className={pd.type}>{pd.label}</p>
|
|
183
|
+
</div>
|
|
184
|
+
</>
|
|
185
|
+
)}
|
|
186
|
+
{config.visualizationType === 'Line' && (
|
|
187
|
+
<>
|
|
188
|
+
<div key={index + 'Line'} className={`legend-preliminary `}>
|
|
189
|
+
<span>{pd.lineCode}</span>
|
|
190
|
+
<p className={pd.type}>{pd.label}</p>
|
|
191
|
+
</div>
|
|
192
|
+
</>
|
|
193
|
+
)}
|
|
194
|
+
{config.visualizationType === 'Combo' && (
|
|
195
|
+
<>
|
|
196
|
+
{pd.symbol && pd.iconCode && (
|
|
197
|
+
<div key={index + 'Combo'} className={`legend-preliminary ${pd.symbol}`}>
|
|
198
|
+
<span className={pd.symbol}>{pd.iconCode}</span>
|
|
199
|
+
<p className={pd.type}>{pd.label}</p>
|
|
200
|
+
</div>
|
|
201
|
+
)}
|
|
202
|
+
|
|
203
|
+
{pd.style && pd.lineCode && (
|
|
204
|
+
<div key={index + 'Combo'} className='legend-preliminary'>
|
|
205
|
+
<span>{pd.lineCode}</span>
|
|
206
|
+
<p>{pd.label}</p>
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
209
|
+
</>
|
|
210
|
+
)}
|
|
211
|
+
</>
|
|
212
|
+
)
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
</>
|
|
216
|
+
)}
|
|
193
217
|
</>
|
|
194
218
|
</>
|
|
195
219
|
)
|
|
@@ -22,7 +22,11 @@ const Legend = forwardRef((props, ref) => {
|
|
|
22
22
|
|
|
23
23
|
const createLegendLabels = createFormatLabels(config, tableData, data, colorScale)
|
|
24
24
|
|
|
25
|
-
return
|
|
25
|
+
return (
|
|
26
|
+
!['Box Plot', 'Pie'].includes(config.visualizationType) && (
|
|
27
|
+
<LegendComponent ref={ref} skipId={props.skipId || 'legend'} config={config} colorScale={colorScale} seriesHighlight={seriesHighlight} highlight={highlight} highlightReset={highlightReset} currentViewport={currentViewport} formatLabels={createLegendLabels} />
|
|
28
|
+
)
|
|
29
|
+
)
|
|
26
30
|
})
|
|
27
31
|
|
|
28
32
|
export default Legend
|
|
@@ -17,12 +17,18 @@ export type LineChartProps = {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export interface PreliminaryDataItem {
|
|
20
|
-
style: string
|
|
21
|
-
type: string
|
|
22
20
|
column: string
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
displayLegend: boolean
|
|
22
|
+
displayTable: boolean
|
|
23
|
+
displayTooltip: boolean
|
|
24
|
+
iconCode: string
|
|
25
25
|
label: string
|
|
26
|
+
lineCode: string
|
|
27
|
+
seriesKey: string
|
|
28
|
+
style: string
|
|
29
|
+
symbol: string
|
|
30
|
+
type: 'effect' | 'suppression'
|
|
31
|
+
value: string
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
export interface DataItem {
|
|
@@ -33,12 +39,13 @@ export interface Config {
|
|
|
33
39
|
preliminaryData: PreliminaryDataItem[] | []
|
|
34
40
|
}
|
|
35
41
|
export interface StyleProps {
|
|
36
|
-
preliminaryData: PreliminaryDataItem[]
|
|
37
42
|
data: DataItem[]
|
|
38
|
-
stroke: string
|
|
39
43
|
handleLineType: Function
|
|
40
44
|
lineType: string
|
|
45
|
+
preliminaryData: PreliminaryDataItem[]
|
|
41
46
|
seriesKey: 'string'
|
|
47
|
+
stroke: string
|
|
48
|
+
strokeWidth: number
|
|
42
49
|
}
|
|
43
50
|
export interface Style {
|
|
44
51
|
stroke: string
|
|
@@ -7,6 +7,7 @@ type LineChartCircleProps = {
|
|
|
7
7
|
circleData: object[]
|
|
8
8
|
config: ChartConfig
|
|
9
9
|
data: object[]
|
|
10
|
+
tableData: object[]
|
|
10
11
|
d?: Object
|
|
11
12
|
displayArea: boolean
|
|
12
13
|
seriesKey: string
|
|
@@ -26,7 +27,7 @@ type LineChartCircleProps = {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
const LineChartCircle = (props: LineChartCircleProps) => {
|
|
29
|
-
const { config, d, displayArea, seriesKey, tooltipData, xScale, yScale, colorScale, parseDate, yScaleRight, data, circleData, dataIndex, mode } = props
|
|
30
|
+
const { config, d, tableData, displayArea, seriesKey, tooltipData, xScale, yScale, colorScale, parseDate, yScaleRight, data, circleData, dataIndex, mode } = props
|
|
30
31
|
const { lineDatapointStyle } = config
|
|
31
32
|
const filtered = config?.series.filter(s => s.dataKey === seriesKey)?.[0]
|
|
32
33
|
// If we're not showing the circle, simply return
|
|
@@ -86,7 +87,7 @@ const LineChartCircle = (props: LineChartCircleProps) => {
|
|
|
86
87
|
let hoveredSeriesAxis = hoveredSeriesData?.[0]?.[2]
|
|
87
88
|
if (!hoveredSeriesKey) return
|
|
88
89
|
hoveredSeriesIndex = tooltipData?.data.indexOf(hoveredSeriesKey)
|
|
89
|
-
hoveredSeriesValue =
|
|
90
|
+
hoveredSeriesValue = tableData?.find(d => {
|
|
90
91
|
return d[config?.xAxis.dataKey] === hoveredXValue
|
|
91
92
|
})?.[seriesKey]
|
|
92
93
|
|
|
@@ -100,6 +101,7 @@ const LineChartCircle = (props: LineChartCircleProps) => {
|
|
|
100
101
|
if (isMatch) {
|
|
101
102
|
return <></>
|
|
102
103
|
}
|
|
104
|
+
|
|
103
105
|
return (
|
|
104
106
|
<circle
|
|
105
107
|
cx={getXPos(hoveredXValue)}
|
|
@@ -119,23 +121,32 @@ const LineChartCircle = (props: LineChartCircleProps) => {
|
|
|
119
121
|
if (mode === 'ISOLATED_POINTS') {
|
|
120
122
|
const drawIsolatedPoints = (currentIndex, seriesKey) => {
|
|
121
123
|
const currentPoint = data[currentIndex]
|
|
122
|
-
const previousPoint = data[currentIndex - 1]
|
|
123
|
-
const nextPoint = data[currentIndex + 1]
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
const previousPoint = currentIndex > 0 ? data[currentIndex - 1] : null
|
|
125
|
+
const nextPoint = currentIndex < data.length - 1 ? data[currentIndex + 1] : null
|
|
126
|
+
let res = false
|
|
127
|
+
|
|
128
|
+
// Handle the first point in the array
|
|
129
|
+
if (currentIndex === 0 && nextPoint && !nextPoint[seriesKey]) {
|
|
130
|
+
res = true
|
|
126
131
|
}
|
|
127
|
-
|
|
128
|
-
|
|
132
|
+
// Handle the last point in the array
|
|
133
|
+
if (currentIndex === data.length - 1 && previousPoint && !previousPoint[seriesKey]) {
|
|
134
|
+
res = true
|
|
129
135
|
}
|
|
130
|
-
|
|
131
|
-
|
|
136
|
+
// Handle points in the middle
|
|
137
|
+
if (currentIndex > 0 && currentIndex < data.length - 1) {
|
|
138
|
+
if (currentPoint && currentPoint[seriesKey] && (!previousPoint || !previousPoint[seriesKey]) && (!nextPoint || !nextPoint[seriesKey])) {
|
|
139
|
+
res = true
|
|
140
|
+
}
|
|
132
141
|
}
|
|
142
|
+
|
|
143
|
+
return res
|
|
133
144
|
}
|
|
134
145
|
|
|
135
146
|
if (mode) {
|
|
136
147
|
if (drawIsolatedPoints(dataIndex, seriesKey)) {
|
|
137
148
|
return (
|
|
138
|
-
<circle cx={getXPos(d[config.xAxis
|
|
149
|
+
<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
150
|
)
|
|
140
151
|
}
|
|
141
152
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { type PreliminaryDataItem, DataItem, StyleProps, Style } from './LineChartProps'
|
|
2
|
-
|
|
2
|
+
import _ from 'lodash'
|
|
3
3
|
export const createStyles = (props: StyleProps): Style[] => {
|
|
4
4
|
const { preliminaryData, data, stroke, strokeWidth, handleLineType, lineType, seriesKey } = props
|
|
5
5
|
|
|
6
|
-
const validPreliminaryData: PreliminaryDataItem[] = preliminaryData.filter(pd => pd.seriesKey && pd.column && pd.value && pd.type && pd.style)
|
|
6
|
+
const validPreliminaryData: PreliminaryDataItem[] = preliminaryData.filter(pd => pd.seriesKey && pd.column && pd.value && pd.type && pd.style && pd.type === 'effect')
|
|
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')
|
|
8
8
|
|
|
9
9
|
let styles: Style[] = []
|
|
@@ -29,17 +29,141 @@ export const createStyles = (props: StyleProps): Style[] => {
|
|
|
29
29
|
|
|
30
30
|
export const filterCircles = (preliminaryData: PreliminaryDataItem[], data: DataItem[], seriesKey: string): DataItem[] => {
|
|
31
31
|
// Filter and map preliminaryData to get circlesFiltered
|
|
32
|
-
const circlesFiltered = preliminaryData
|
|
33
|
-
|
|
34
|
-
let filteredData: DataItem[] = []
|
|
35
|
-
|
|
32
|
+
const circlesFiltered = preliminaryData?.filter(item => item.style === 'Open Circles' && item.type === 'effect').map(item => ({ column: item.column, value: item.value, seriesKey: item.seriesKey }))
|
|
33
|
+
const filteredData: DataItem[] = []
|
|
36
34
|
// Process data to find matching items
|
|
37
35
|
data.forEach(item => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
circlesFiltered.forEach(fc => {
|
|
37
|
+
if (item[fc.column] === fc.value && fc.seriesKey === seriesKey) {
|
|
38
|
+
filteredData.push(item)
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
return filteredData
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const isCalculable = value => !isNaN(parseFloat(value)) && isFinite(value)
|
|
46
|
+
const handleFirstIndex = (data, seriesKey, preliminaryData) => {
|
|
47
|
+
const result = {
|
|
48
|
+
data: [],
|
|
49
|
+
style: ''
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// If data is empty, return the empty result
|
|
53
|
+
if (!data.length) return result
|
|
54
|
+
|
|
55
|
+
const firstIndexDataItem = data[0]
|
|
56
|
+
|
|
57
|
+
// Function to check if a data item matches the suppression criteria
|
|
58
|
+
const isSuppressed = pd => {
|
|
59
|
+
if (pd.type === 'effect') return
|
|
60
|
+
return pd.type == 'suppression' && pd.value === firstIndexDataItem[seriesKey] && (!pd.column || pd.column === seriesKey)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Find applicable suppression data for the first item
|
|
64
|
+
const suppressionData = preliminaryData.find(isSuppressed)
|
|
65
|
+
|
|
66
|
+
if (suppressionData && suppressionData.style) {
|
|
67
|
+
// Modify first item and add to result
|
|
68
|
+
const modifiedItem = { ...firstIndexDataItem, [seriesKey]: 0 }
|
|
69
|
+
result.data.push(modifiedItem)
|
|
70
|
+
result.style = suppressionData.style
|
|
71
|
+
|
|
72
|
+
// Find the next calculable item index
|
|
73
|
+
let nextIndex = 1
|
|
74
|
+
while (nextIndex < data.length && !isCalculable(data[nextIndex][seriesKey])) {
|
|
75
|
+
nextIndex++
|
|
76
|
+
}
|
|
77
|
+
if (nextIndex < data.length) {
|
|
78
|
+
result.data.push(data[nextIndex])
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
// If no suppression, just add the first item
|
|
82
|
+
result.data.push(firstIndexDataItem)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return result
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const handleLastIndex = (data, seriesKey, preliminaryData) => {
|
|
89
|
+
const result = {
|
|
90
|
+
data: [],
|
|
91
|
+
style: ''
|
|
92
|
+
}
|
|
93
|
+
let lastAddedIndex = -1 // Tracks the last index added to the result
|
|
94
|
+
preliminaryData?.forEach(pd => {
|
|
95
|
+
if (pd.type === 'effect') return
|
|
96
|
+
if (data[data.length - 1][seriesKey] === pd.value && pd.style && (!pd.column || pd.column === seriesKey) && pd.type == 'suppression') {
|
|
97
|
+
const lastIndex = data.length - 1
|
|
98
|
+
const modifiedItem = { ...data[lastIndex], [seriesKey]: 0 }
|
|
99
|
+
result.data.push(modifiedItem)
|
|
100
|
+
|
|
101
|
+
// Find previous calculable item
|
|
102
|
+
let prevIndex = lastIndex - 1
|
|
103
|
+
while (prevIndex >= 0 && !isCalculable(data[prevIndex][seriesKey])) {
|
|
104
|
+
prevIndex--
|
|
105
|
+
}
|
|
106
|
+
if (prevIndex >= 0 && lastAddedIndex !== prevIndex) {
|
|
107
|
+
result.data.push(data[prevIndex])
|
|
108
|
+
lastAddedIndex = prevIndex
|
|
109
|
+
}
|
|
110
|
+
result.style = pd.style
|
|
41
111
|
}
|
|
42
112
|
})
|
|
43
113
|
|
|
44
|
-
return
|
|
114
|
+
return result
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function handleMiddleIndices(data, seriesKey, dataKey, preliminaryData) {
|
|
118
|
+
const result = {
|
|
119
|
+
data: [],
|
|
120
|
+
style: ''
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const isValidMiddleIndex = index => index > 0 && index < data.length - 1
|
|
124
|
+
|
|
125
|
+
preliminaryData?.forEach(pd => {
|
|
126
|
+
if (pd.type === 'effect') return
|
|
127
|
+
const targetValue = pd.value
|
|
128
|
+
|
|
129
|
+
// Find all indices
|
|
130
|
+
const matchingIndices = data.reduce((indices, item, index) => {
|
|
131
|
+
if (item[seriesKey] === targetValue && isValidMiddleIndex(index) && (!pd.column || pd.column === seriesKey)) {
|
|
132
|
+
indices.push(index)
|
|
133
|
+
}
|
|
134
|
+
return indices
|
|
135
|
+
}, [])
|
|
136
|
+
|
|
137
|
+
// Process each valid index
|
|
138
|
+
matchingIndices.forEach(i => {
|
|
139
|
+
result.style = pd.style
|
|
140
|
+
// Add previous object if calculable
|
|
141
|
+
if (isCalculable(data[i - 1][seriesKey])) {
|
|
142
|
+
result.data.push(data[i - 1])
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Find and add the next calculable object
|
|
146
|
+
const nextIndex = data.slice(i + 1).findIndex(item => item[seriesKey] !== targetValue && isCalculable(item[seriesKey]))
|
|
147
|
+
if (nextIndex !== -1) {
|
|
148
|
+
result.data.push(data[i + 1 + nextIndex])
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// Deduplicate entries
|
|
154
|
+
result.data = _.uniqWith(result.data, (a, b) => a[dataKey] === b[dataKey] && a[seriesKey] === b[seriesKey])
|
|
155
|
+
|
|
156
|
+
return result
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// create segments (array of arrays) for building suppressed Lines
|
|
160
|
+
export const createDataSegments = (data, seriesKey, preliminaryData, dataKey) => {
|
|
161
|
+
// Process the first index if necessary
|
|
162
|
+
const firstSegment = handleFirstIndex(data, seriesKey, preliminaryData)
|
|
163
|
+
// Process the last index if necessary
|
|
164
|
+
const lastSegment = handleLastIndex(data, seriesKey, preliminaryData)
|
|
165
|
+
// Process the middle segment
|
|
166
|
+
const middleSegments = handleMiddleIndices(data, seriesKey, dataKey, preliminaryData)
|
|
167
|
+
// Combine all segments into a single array
|
|
168
|
+
return [firstSegment, middleSegments, lastSegment].filter(segment => segment.data.length > 0 && segment.style !== '')
|
|
45
169
|
}
|