@cdc/chart 4.24.4 → 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 +32130 -31726
- package/index.html +7 -7
- package/package.json +2 -2
- package/src/CdcChart.tsx +17 -13
- 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 +52 -47
- package/src/components/BarChart/components/BarChart.Vertical.tsx +77 -92
- package/src/components/DeviationBar.jsx +4 -2
- package/src/components/EditorPanel/EditorPanel.tsx +289 -601
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +19 -2
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +4 -5
- package/src/components/EditorPanel/useEditorPermissions.js +4 -1
- package/src/components/Legend/Legend.Component.tsx +62 -10
- 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 +155 -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 +23 -23
- package/src/hooks/useTooltip.tsx +11 -11
- package/src/scss/main.scss +56 -6
- 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
|
@@ -49,7 +49,24 @@ const PanelGeneral: FC<PanelProps> = props => {
|
|
|
49
49
|
{visHasNumbersOnBars() ? (
|
|
50
50
|
<CheckBox value={config.yAxis.displayNumbersOnBar} section='yAxis' fieldName='displayNumbersOnBar' label={config.isLollipopChart ? 'Display Numbers after Bar' : 'Display Numbers on Bar'} updateField={updateField} />
|
|
51
51
|
) : (
|
|
52
|
-
visHasLabelOnData() &&
|
|
52
|
+
visHasLabelOnData() && (
|
|
53
|
+
<CheckBox
|
|
54
|
+
value={config.labels}
|
|
55
|
+
fieldName='labels'
|
|
56
|
+
label='Display label on data'
|
|
57
|
+
updateField={updateField}
|
|
58
|
+
tooltip={
|
|
59
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
60
|
+
<Tooltip.Target>
|
|
61
|
+
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
62
|
+
</Tooltip.Target>
|
|
63
|
+
<Tooltip.Content>
|
|
64
|
+
<p>Selecting this option will not hide the display of "zero value", "suppressed data", or "no data" indicators on the chart (if applicable).</p>
|
|
65
|
+
</Tooltip.Content>
|
|
66
|
+
</Tooltip>
|
|
67
|
+
}
|
|
68
|
+
/>
|
|
69
|
+
)
|
|
53
70
|
)}
|
|
54
71
|
{visualizationType === 'Pie' && <Select fieldName='pieType' label='Pie Chart Type' updateField={updateField} options={['Regular', 'Donut']} />}
|
|
55
72
|
|
|
@@ -144,7 +161,7 @@ const PanelGeneral: FC<PanelProps> = props => {
|
|
|
144
161
|
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
145
162
|
</Tooltip.Target>
|
|
146
163
|
<Tooltip.Content>
|
|
147
|
-
<p>
|
|
164
|
+
<p>Consider adding footnotes when displaying 'suppressed,' 'no data,' and 'zero values' to ensure accurate interpretation of the data.</p>
|
|
148
165
|
</Tooltip.Content>
|
|
149
166
|
</Tooltip>
|
|
150
167
|
}
|
|
@@ -5,15 +5,14 @@ import ConfigContext from '../../../../ConfigContext'
|
|
|
5
5
|
import InputSelect from '@cdc/core/components/inputs/InputSelect'
|
|
6
6
|
import Check from '@cdc/core/assets/icon-check.svg'
|
|
7
7
|
import { approvedCurveTypes } from '@cdc/core/helpers/lineChartHelpers'
|
|
8
|
-
|
|
8
|
+
import { sequentialPalettes } from '@cdc/core/data/colorPalettes'
|
|
9
9
|
import Icon from '@cdc/core/components/ui/Icon'
|
|
10
10
|
|
|
11
11
|
// Third Party
|
|
12
12
|
import { Accordion, AccordionItem, AccordionItemHeading, AccordionItemPanel, AccordionItemButton } from 'react-accessible-accordion'
|
|
13
13
|
import { Draggable } from '@hello-pangea/dnd'
|
|
14
|
-
import { colorPalettesChart, sequentialPalettes } from '@cdc/core/data/colorPalettes'
|
|
15
14
|
|
|
16
|
-
const SeriesContext = React.createContext()
|
|
15
|
+
const SeriesContext = React.createContext({})
|
|
17
16
|
|
|
18
17
|
const SeriesWrapper = props => {
|
|
19
18
|
const { updateConfig, config, rawData } = useContext(ConfigContext)
|
|
@@ -456,8 +455,8 @@ const SeriesInputWeight = props => {
|
|
|
456
455
|
type='number'
|
|
457
456
|
key={`series-weight-${i}`}
|
|
458
457
|
value={series.weight ? series.weight : ''}
|
|
459
|
-
min=
|
|
460
|
-
max=
|
|
458
|
+
min='1'
|
|
459
|
+
max='9'
|
|
461
460
|
onChange={event => {
|
|
462
461
|
changeSeriesWeight(i, event.target.value, event.target.min, event.target.max)
|
|
463
462
|
}}
|
|
@@ -301,8 +301,11 @@ export const useEditorPermissions = () => {
|
|
|
301
301
|
if (visualizationType === 'Line') {
|
|
302
302
|
return true
|
|
303
303
|
}
|
|
304
|
+
if (visualizationType === 'Bar' && visualizationSubType === 'regular') {
|
|
305
|
+
return true
|
|
306
|
+
}
|
|
304
307
|
|
|
305
|
-
if (visualizationType === 'Combo'
|
|
308
|
+
if (visualizationType === 'Combo') {
|
|
306
309
|
return true
|
|
307
310
|
}
|
|
308
311
|
return false
|
|
@@ -5,21 +5,22 @@ import Button from '@cdc/core/components/elements/Button'
|
|
|
5
5
|
import useLegendClasses from '../../hooks/useLegendClasses'
|
|
6
6
|
import { useHighlightedBars } from '../../hooks/useHighlightedBars'
|
|
7
7
|
import { handleLineType } from '../../helpers/handleLineType'
|
|
8
|
+
import { useBarChart } from '../../hooks/useBarChart'
|
|
8
9
|
import { Line } from '@visx/shape'
|
|
9
10
|
import { Label } from '../../types/Label'
|
|
10
11
|
import { ChartConfig } from '../../types/ChartConfig'
|
|
11
12
|
import { ColorScale } from '../../types/ChartContext'
|
|
12
13
|
import { forwardRef } from 'react'
|
|
13
14
|
|
|
14
|
-
interface LegendProps {
|
|
15
|
-
config: ChartConfig
|
|
15
|
+
export interface LegendProps {
|
|
16
16
|
colorScale: ColorScale
|
|
17
|
-
|
|
18
|
-
highlight: Function
|
|
19
|
-
highlightReset: Function
|
|
17
|
+
config: ChartConfig
|
|
20
18
|
currentViewport: 'lg' | 'md' | 'sm' | 'xs' | 'xxs'
|
|
21
19
|
formatLabels: (labels: Label[]) => Label[]
|
|
20
|
+
highlight: Function
|
|
21
|
+
highlightReset: Function
|
|
22
22
|
ref: React.Ref<() => void>
|
|
23
|
+
seriesHighlight: string[]
|
|
23
24
|
skipId: string
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -27,12 +28,13 @@ interface LegendProps {
|
|
|
27
28
|
const Legend: React.FC<LegendProps> = forwardRef(({ config, colorScale, seriesHighlight, highlight, highlightReset, currentViewport, formatLabels, skipId = 'legend' }, ref) => {
|
|
28
29
|
const { innerClasses, containerClasses } = useLegendClasses(config)
|
|
29
30
|
const { runtime, orientation, legend } = config
|
|
31
|
+
|
|
30
32
|
if (!legend) return null
|
|
31
33
|
const isBottomOrSmallViewport = legend.position === 'bottom' || ['sm', 'xs', 'xxs'].includes(currentViewport)
|
|
32
34
|
|
|
33
35
|
const legendClasses = {
|
|
34
36
|
marginBottom: isBottomOrSmallViewport ? '15px' : '0px',
|
|
35
|
-
marginTop: isBottomOrSmallViewport
|
|
37
|
+
marginTop: isBottomOrSmallViewport ? '15px' : '0px'
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
const { HighLightedBarUtils } = useHighlightedBars(config)
|
|
@@ -143,17 +145,17 @@ const Legend: React.FC<LegendProps> = forwardRef(({ config, colorScale, seriesHi
|
|
|
143
145
|
</div>
|
|
144
146
|
|
|
145
147
|
<>
|
|
146
|
-
{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) && (
|
|
147
149
|
<>
|
|
148
150
|
<hr></hr>
|
|
149
151
|
<div className={config.legend.singleRow && isBottomOrSmallViewport ? 'legend-container__inner bottom single-row' : ''}>
|
|
150
152
|
{config?.preliminaryData?.map((pd, index) => {
|
|
151
153
|
return (
|
|
152
154
|
<>
|
|
153
|
-
{pd.label && (
|
|
155
|
+
{pd.label && pd.type === 'effect' && pd.style && (
|
|
154
156
|
<div key={index} className='legend-preliminary'>
|
|
155
|
-
<
|
|
156
|
-
<
|
|
157
|
+
<span className={pd.symbol}>{pd.lineCode}</span>
|
|
158
|
+
<p> {pd.label}</p>
|
|
157
159
|
</div>
|
|
158
160
|
)}
|
|
159
161
|
</>
|
|
@@ -162,6 +164,56 @@ const Legend: React.FC<LegendProps> = forwardRef(({ config, colorScale, seriesHi
|
|
|
162
164
|
</div>
|
|
163
165
|
</>
|
|
164
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
|
+
)}
|
|
165
217
|
</>
|
|
166
218
|
</>
|
|
167
219
|
)
|
|
@@ -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
|
}
|