@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.
Files changed (39) hide show
  1. package/dist/cdcchart.js +34377 -33726
  2. package/examples/feature/line/line-chart.json +361 -37
  3. package/examples/region-issue.json +2065 -0
  4. package/examples/test.json +5409 -0
  5. package/index.html +13 -11
  6. package/package.json +2 -2
  7. package/src/CdcChart.tsx +159 -89
  8. package/src/_stories/Chart.stories.tsx +8 -0
  9. package/src/_stories/_mock/bar-chart-suppressed.json +474 -0
  10. package/src/components/AreaChart/components/AreaChart.jsx +2 -2
  11. package/src/components/BarChart/components/BarChart.Horizontal.tsx +61 -63
  12. package/src/components/BarChart/components/BarChart.Vertical.tsx +79 -94
  13. package/src/components/DeviationBar.jsx +4 -2
  14. package/src/components/EditorPanel/EditorPanel.tsx +1580 -1924
  15. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +19 -2
  16. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +0 -1
  17. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +4 -5
  18. package/src/components/EditorPanel/editor-panel.scss +0 -724
  19. package/src/components/EditorPanel/useEditorPermissions.js +4 -1
  20. package/src/components/Legend/Legend.Component.tsx +82 -58
  21. package/src/components/Legend/Legend.tsx +5 -1
  22. package/src/components/LineChart/LineChartProps.ts +13 -6
  23. package/src/components/LineChart/components/LineChart.Circle.tsx +22 -11
  24. package/src/components/LineChart/helpers.ts +134 -10
  25. package/src/components/LineChart/index.tsx +69 -42
  26. package/src/components/LinearChart.jsx +156 -139
  27. package/src/components/ZoomBrush.tsx +40 -21
  28. package/src/data/initial-state.js +4 -4
  29. package/src/hooks/useBarChart.js +47 -22
  30. package/src/hooks/useMinMax.ts +21 -2
  31. package/src/hooks/useScales.ts +33 -1
  32. package/src/hooks/useTooltip.tsx +11 -11
  33. package/src/scss/main.scss +80 -5
  34. package/src/types/ChartConfig.ts +3 -13
  35. package/src/types/ChartContext.ts +4 -0
  36. package/src/_stories/ChartLine.preliminary.tsx +0 -19
  37. package/src/_stories/ChartSuppress.stories.tsx +0 -19
  38. package/src/_stories/_mock/suppress_mock.json +0 -911
  39. 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
- seriesHighlight: string[]
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 && orientation === 'horizontal' ? `${config.yAxis.label && config.isResponsiveTicks ? config.dynamicMarginTop : config.runtime.xAxis.size}px` : `${isBottomOrSmallViewport ? config.dynamicMarginTop + 15 : 0}px`
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
- {config.visualizationType === 'Line' && config.legend.lineMode ? (
120
- <svg width={40} height={20}>
121
- <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 : '')} />
122
- </svg>
123
- ) : (
124
- <div style={{ display: 'flex', flexDirection: 'column' }}>
125
- <LegendCircle margin='0' fill={label.value} display={displayScale(label.datum)} />
126
- <div style={{ marginTop: '2px', marginRight: '6px' }}>{label.icon}</div>
127
- </div>
128
- )}
129
-
130
- <LegendLabel align='left' margin='0 0 0 4px'>
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 style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
181
- <svg style={{ width: '50px' }} key={index} height={'23px'}>
182
- {pd.style.includes('Dashed') ? <Line from={{ x: 10, y: 10 }} to={{ x: 40, y: 10 }} stroke={'#000'} strokeWidth={2} strokeDasharray={handleLineType(pd.style)} /> : <circle r={6} strokeWidth={2} stroke={'#000'} cx={22} cy={10} fill='transparent' />}
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 !['Box Plot', 'Pie'].includes(config.visualizationType) && <LegendComponent ref={ref} config={config} colorScale={colorScale} seriesHighlight={seriesHighlight} highlight={highlight} highlightReset={highlightReset} currentViewport={currentViewport} formatLabels={createLegendLabels} />
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
- value: string
24
- seriesKey: string
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 = data?.find(d => {
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
- if (currentIndex === 0 && !nextPoint[seriesKey]) {
125
- return true
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
- if (currentIndex === data.length - 1 && !previousPoint[seriesKey]) {
128
- return true
132
+ // Handle the last point in the array
133
+ if (currentIndex === data.length - 1 && previousPoint && !previousPoint[seriesKey]) {
134
+ res = true
129
135
  }
130
- if (currentIndex !== 0 && currentPoint[seriesKey] && !previousPoint[seriesKey] && !nextPoint[seriesKey]) {
131
- return true
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.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])} />
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.filter(item => item.style === 'Open Circles' && item.type === 'effect').map(item => ({ column: item.column, value: item.value, seriesKey: item.seriesKey }))
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
- if (circlesFiltered.some(d => item[d.column] === d.value && d.seriesKey === seriesKey)) {
39
- // Add current item
40
- filteredData.push(item)
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 filteredData
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
  }