@cdc/chart 4.25.3 → 4.25.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/cdcchart.js +46641 -42561
  2. package/index.html +130 -129
  3. package/package.json +22 -27
  4. package/src/CdcChartComponent.tsx +75 -35
  5. package/src/_stories/Chart.CI.stories.tsx +10 -0
  6. package/src/_stories/Chart.DynamicSeries.stories.tsx +68 -49
  7. package/src/_stories/Chart.stories.tsx +99 -86
  8. package/src/_stories/ChartPrefixSuffix.stories.tsx +29 -32
  9. package/{examples/private/line-issue.json → src/_stories/_mock/barchart_labels.mock.json} +150 -35
  10. package/src/_stories/_mock/dynamic_series_bar_config.json +1 -1
  11. package/src/_stories/_mock/dynamic_series_suppression_mock.json +610 -0
  12. package/{examples/private/not-loading.json → src/_stories/_mock/pie_calculated_area.json} +152 -95
  13. package/src/components/Annotations/components/AnnotationDropdown.tsx +2 -2
  14. package/src/components/AreaChart/components/AreaChart.jsx +33 -5
  15. package/src/components/Axis/Categorical.Axis.tsx +2 -2
  16. package/src/components/BarChart/components/BarChart.Horizontal.tsx +38 -37
  17. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +18 -8
  18. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +8 -8
  19. package/src/components/BarChart/components/BarChart.Vertical.tsx +47 -36
  20. package/src/components/BarChart/components/{BarChart.jsx → BarChart.tsx} +23 -5
  21. package/src/components/BarChart/components/context.tsx +20 -2
  22. package/src/components/BarChart/helpers/getBarHeights.ts +47 -0
  23. package/src/components/BarChart/helpers/index.ts +5 -2
  24. package/src/components/BarChart/helpers/tests/getBarHeights.test.ts +83 -0
  25. package/src/{hooks → components/BarChart/helpers}/useBarChart.ts +9 -46
  26. package/src/components/BoxPlot/BoxPlot.tsx +2 -1
  27. package/src/components/Brush/BrushChart.tsx +73 -0
  28. package/src/components/Brush/BrushController..tsx +39 -0
  29. package/src/components/DeviationBar.jsx +2 -2
  30. package/src/components/EditorPanel/EditorPanel.tsx +232 -147
  31. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +36 -36
  32. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +52 -25
  33. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +12 -4
  34. package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +8 -0
  35. package/src/components/EditorPanel/useEditorPermissions.ts +5 -5
  36. package/src/components/ForestPlot/ForestPlot.tsx +2 -2
  37. package/src/components/HoverLine/HoverLine.tsx +74 -0
  38. package/src/components/Legend/Legend.Component.tsx +1 -1
  39. package/src/components/Legend/Legend.Suppression.tsx +59 -25
  40. package/src/components/Legend/helpers/createFormatLabels.tsx +28 -0
  41. package/src/components/Legend/helpers/index.ts +1 -1
  42. package/src/components/LineChart/LineChartProps.ts +3 -1
  43. package/src/components/LineChart/components/LineChart.Circle.tsx +72 -119
  44. package/src/components/LineChart/helpers.ts +133 -56
  45. package/src/components/LineChart/index.tsx +106 -55
  46. package/src/components/LinearChart.tsx +178 -198
  47. package/src/components/PairedBarChart.jsx +3 -2
  48. package/src/components/PieChart/PieChart.tsx +127 -102
  49. package/src/components/ScatterPlot/ScatterPlot.jsx +5 -0
  50. package/src/components/Sparkline/components/SparkLine.tsx +80 -18
  51. package/src/data/initial-state.js +11 -6
  52. package/src/helpers/countNumOfTicks.ts +1 -1
  53. package/src/helpers/dataHelpers.ts +23 -2
  54. package/src/helpers/getNewRuntime.ts +35 -0
  55. package/src/helpers/getPiePercent.ts +22 -0
  56. package/src/helpers/getTransformedData.ts +22 -0
  57. package/src/helpers/sizeHelpers.ts +1 -1
  58. package/src/helpers/tests/getNewRuntime.test.ts +82 -0
  59. package/src/helpers/tests/getPiePercent.test.ts +38 -0
  60. package/src/hooks/useMinMax.ts +21 -28
  61. package/src/hooks/useRightAxis.ts +5 -3
  62. package/src/hooks/useScales.ts +15 -6
  63. package/src/hooks/useTooltip.tsx +218 -203
  64. package/src/index.jsx +2 -2
  65. package/src/scss/main.scss +13 -6
  66. package/src/store/chart.actions.ts +2 -6
  67. package/src/store/chart.reducer.ts +23 -23
  68. package/src/types/ChartConfig.ts +11 -3
  69. package/src/types/ChartContext.ts +0 -2
  70. package/examples/private/DEV-8850-2.json +0 -493
  71. package/examples/private/DEV-9822.json +0 -574
  72. package/examples/private/DEV-9840.json +0 -553
  73. package/examples/private/DEV-9850-3.json +0 -461
  74. package/examples/private/chart.json +0 -1084
  75. package/examples/private/ci_formatted.json +0 -202
  76. package/examples/private/ci_issue.json +0 -3016
  77. package/examples/private/completed.json +0 -634
  78. package/examples/private/dem-data-long.csv +0 -20
  79. package/examples/private/dem-data-long.json +0 -36
  80. package/examples/private/demographic_data.csv +0 -157
  81. package/examples/private/demographic_data.json +0 -2654
  82. package/examples/private/demographic_dynamic.json +0 -443
  83. package/examples/private/demographic_standard.json +0 -560
  84. package/examples/private/ehdi.json +0 -29939
  85. package/examples/private/test.json +0 -493
  86. package/src/components/ZoomBrush.tsx +0 -251
@@ -1,22 +1,15 @@
1
- import React from 'react'
2
1
  import chroma from 'chroma-js'
3
- import { type ChartConfig } from '../../../types/ChartConfig'
2
+ import { LineChartConfig, type ChartConfig } from '../../../types/ChartConfig'
4
3
  import { GlyphDiamond, GlyphCircle, GlyphSquare, GlyphTriangle, GlyphCross, Glyph as CustomGlyph } from '@visx/glyph'
5
4
  import { Text } from '@visx/text'
6
5
 
7
6
  type LineChartCircleProps = {
8
- circleData: object[]
9
7
  config: ChartConfig
10
8
  data: object[]
11
9
  tableData: object[]
12
10
  d?: Object
13
11
  displayArea: boolean
14
12
  seriesKey: string
15
- tooltipData: {
16
- data: []
17
- tooltipDataX: number
18
- tooltipDataY: number
19
- }
20
13
  xScale: any
21
14
  yScale: any
22
15
  yScaleRight: any
@@ -25,7 +18,14 @@ type LineChartCircleProps = {
25
18
  seriesAxis: string
26
19
  dataIndex: number
27
20
  seriesIndex: number
28
- mode: 'ISOLATED_POINTS' | 'HOVER_POINTS' | 'ALWAYS_SHOW_POINTS'
21
+ mode: 'ISOLATED_POINTS' | 'HOVER_POINTS' | 'ALWAYS_SHOW_POINTS' | 'TOOLTIP_POINTS'
22
+ tooltipPoint: {
23
+ xValue: string | number
24
+ yValue: string | number
25
+ color: string
26
+ }
27
+ handleTooltipMouseOver?: Function
28
+ handleTooltipMouseOff?: Function
29
29
  }
30
30
  const Glyphs = [
31
31
  GlyphCircle,
@@ -47,146 +47,79 @@ const LineChartCircle = (props: LineChartCircleProps) => {
47
47
  const {
48
48
  config,
49
49
  d: pointData,
50
- tableData,
51
50
  displayArea,
52
51
  seriesKey,
53
- tooltipData,
54
52
  xScale,
55
53
  yScale,
56
54
  colorScale,
57
55
  parseDate,
58
56
  yScaleRight,
59
57
  data,
60
- circleData,
58
+ tooltipPoint,
61
59
  dataIndex,
62
60
  mode,
63
- seriesIndex
61
+ seriesIndex,
62
+ handleTooltipMouseOver,
63
+ handleTooltipMouseOff
64
64
  } = props
65
- const { lineDatapointStyle, visual } = config
65
+ const { isolatedDotsSameSize, lineDatapointStyle, visual } = config as LineChartConfig
66
66
  const filtered = config?.series.filter(s => s.dataKey === seriesKey)?.[0]
67
67
  const Shape =
68
68
  Glyphs[
69
69
  config.visual.lineDatapointSymbol === 'standard' && seriesIndex < visual.maximumShapeAmount ? seriesIndex : 0
70
70
  ]
71
71
  const isReversedTriangle = seriesIndex === 4
72
- const transformShape = (top, left) => `translate(${left}, ${top})${isReversedTriangle ? ' rotate(180)' : ''}`
72
+ const LARGE_DOT_SIZE = 124
73
+ const REGULAR_DOT_SIZE = 55
74
+ const dotSize = isolatedDotsSameSize ? REGULAR_DOT_SIZE : LARGE_DOT_SIZE
73
75
 
74
- // If we're not showing the circle, simply return
75
- const getColor = (
76
- displayArea: boolean,
77
- colorScale: Function,
78
- config: ChartConfig,
79
- hoveredKey: string,
80
- seriesKey: string
81
- ) => {
82
- const seriesLabels = config.runtime.seriesLabels || []
83
- const seriesLabelsAll = config.runtime.seriesLabelsAll || []
84
- let color = displayArea ? colorScale(seriesLabels[hoveredKey] || seriesLabelsAll[seriesIndex]) : ' transparent'
85
- if (config.lineDatapointColor === 'Lighter than Line' && color !== 'transparent' && color) {
86
- color = chroma(color).brighten(1)
87
- }
88
- return color
89
- }
90
76
  const getXPos = hoveredXValue => {
91
77
  return (
92
78
  (config.xAxis.type === 'categorical' ? xScale(hoveredXValue) : xScale(parseDate(hoveredXValue))) +
93
79
  (xScale.bandwidth ? xScale.bandwidth() / 2 : 0)
94
80
  )
95
81
  }
96
- if (mode === 'ALWAYS_SHOW_POINTS' && lineDatapointStyle !== 'hidden') {
97
- if (lineDatapointStyle === 'always show') {
98
- const isMatch = circleData?.some(
99
- cd => cd[config.xAxis.dataKey] === pointData[config.xAxis.dataKey] && cd[seriesKey] === pointData[seriesKey]
100
- )
101
82
 
102
- if (
103
- isMatch ||
104
- !filtered ||
105
- (visual.maximumShapeAmount === seriesIndex && visual.lineDatapointSymbol === 'standard')
106
- )
107
- return <></>
108
- const positionLeft = getXPos(pointData[config.xAxis.dataKey])
109
- const positionTop =
110
- filtered.axis === 'Right' ? yScaleRight(pointData[filtered.dataKey]) : yScale(pointData[filtered.dataKey])
83
+ const transformShape = (xValue, yValue) => {
84
+ const positionLeft = getXPos(xValue)
85
+ const positionTop = filtered?.axis === 'Right' ? yScaleRight(yValue) : yScale(yValue)
111
86
 
112
- return (
113
- <g transform={transformShape(positionTop, positionLeft)}>
114
- <Shape
115
- opacity={pointData[seriesKey] ? 1 : 0}
116
- fillOpacity={1}
117
- fill={getColor(displayArea, colorScale, config, seriesKey, seriesKey)}
118
- style={{ filter: 'unset', opacity: 1 }}
119
- />
120
- </g>
121
- )
122
- }
87
+ return `translate(${positionLeft}, ${positionTop})${isReversedTriangle ? ' rotate(180)' : ''}`
123
88
  }
124
89
 
125
- if (mode === 'HOVER_POINTS') {
126
- if (lineDatapointStyle === 'hover') {
127
- if (!tooltipData) return
128
- if (!seriesKey) return
129
- if (!tooltipData.data) return
130
- let hoveredXValue = tooltipData?.data?.[0]?.[1]
131
- if (!hoveredXValue) return
132
-
133
- let hoveredSeriesValue
134
- let hoveredSeriesData = tooltipData.data.filter(d => d[0] === seriesKey)
135
- let hoveredSeriesKey = hoveredSeriesData?.[0]?.[0]
136
- let hoveredSeriesAxis = hoveredSeriesData?.[0]?.[2]
137
- const dynamicSeriesConfig = config.runtime.series.find(s => s.dynamicCategory)
138
- const originalDataKey = dynamicSeriesConfig?.originalDataKey ?? seriesKey
139
-
140
- if (!hoveredSeriesKey) return
141
- hoveredSeriesValue = tableData?.find(d => {
142
- const dynamicCategory = dynamicSeriesConfig?.dynamicCategory
143
- const matchingXValue = d[config.xAxis.dataKey] === hoveredXValue
144
- if (!matchingXValue) return false
145
- if (dynamicCategory) {
146
- const match = d[dynamicCategory] === hoveredSeriesKey
147
- return match
148
- }
149
- return true
150
- })?.[originalDataKey]
151
-
152
- // hoveredSeriesValue = extractNumber(hoveredSeriesValue)
153
- return tooltipData?.data.map((tooltipItem, index) => {
154
- if (isNaN(hoveredSeriesValue)) return <></>
155
- const isMatch = circleData?.some(cd => cd[config.xAxis.dataKey] === hoveredXValue)
156
-
157
- if (
158
- isMatch ||
159
- !hoveredSeriesValue ||
160
- (visual.maximumShapeAmount === seriesIndex && visual.lineDatapointSymbol === 'standard')
161
- ) {
162
- return <></>
163
- }
164
-
165
- const positionTop = hoveredSeriesAxis === 'right' ? yScaleRight(hoveredSeriesValue) : yScale(hoveredSeriesValue)
166
- const positionLeft = getXPos(hoveredXValue)
167
- return (
168
- <g transform={transformShape(positionTop, positionLeft)}>
169
- <Shape
170
- size={55}
171
- opacity={1}
172
- fillOpacity={1}
173
- fill={getColor(displayArea, colorScale, config, hoveredSeriesKey, seriesKey)}
174
- style={{ filter: 'unset', opacity: 1 }}
175
- />
176
- </g>
177
- )
178
- })
90
+ // If we're not showing the circle, simply return
91
+ const getColor = (displayArea: boolean, colorScale: Function, config: ChartConfig, hoveredKey: string) => {
92
+ const seriesLabels = config.runtime.seriesLabels || []
93
+ const seriesLabelsAll = config.runtime.seriesLabelsAll || []
94
+ let color = displayArea ? colorScale(seriesLabels[hoveredKey] || seriesLabelsAll[seriesIndex]) : 'transparent'
95
+ if (config.lineDatapointColor === 'Lighter than Line' && color !== 'transparent' && color) {
96
+ color = chroma(color).brighten(1)
179
97
  }
98
+ return color
180
99
  }
100
+
101
+ if (['ALWAYS_SHOW_POINTS', 'HOVER_POINTS'].includes(mode)) {
102
+ if (!filtered || (visual.maximumShapeAmount === seriesIndex && visual.lineDatapointSymbol === 'standard'))
103
+ return <></>
104
+ return (
105
+ <g
106
+ transform={transformShape(pointData[config.xAxis.dataKey], pointData[filtered?.dataKey])}
107
+ className={`visx-glyph-group${displayArea ? '' : '-hidden'}`}
108
+ data-seriesIndex={seriesIndex}
109
+ >
110
+ <Shape
111
+ fillOpacity={mode === 'ALWAYS_SHOW_POINTS' ? 1 : 0}
112
+ fill={getColor(displayArea, colorScale, config, seriesKey)}
113
+ />
114
+ </g>
115
+ )
116
+ }
117
+
181
118
  if (mode === 'ISOLATED_POINTS') {
182
119
  const drawIsolatedPoints = (currentIndex, seriesKey) => {
183
120
  const currentPoint = data[currentIndex]
184
121
  const previousPoint = data[currentIndex - 1] || {}
185
122
  const nextPoint = data[currentIndex + 1] || {}
186
-
187
- const isMatch = circleData.some(item => item?.data[seriesKey] === currentPoint[seriesKey])
188
- if (isMatch) return false
189
-
190
123
  const isFirstPoint = currentIndex === 0 && !nextPoint[seriesKey]
191
124
  const isLastPoint = currentIndex === data.length - 1 && !previousPoint[seriesKey]
192
125
  const isMiddlePoint =
@@ -203,19 +136,39 @@ const LineChartCircle = (props: LineChartCircleProps) => {
203
136
  : dataIndex
204
137
 
205
138
  if (drawIsolatedPoints(_dataIndex, seriesKey)) {
206
- const positionTop =
207
- filtered?.axis === 'Right' ? yScaleRight(pointData[filtered?.dataKey]) : yScale(pointData[filtered?.dataKey])
208
- const positionLeft = getXPos(pointData[config.xAxis?.dataKey])
209
139
  const color = colorScale(config.runtime.seriesLabelsAll[seriesIndex])
210
140
 
211
141
  return (
212
- <g transform={transformShape(positionTop, positionLeft)}>
213
- <Shape size={124} stroke={color} fill={color} />
142
+ <g
143
+ transform={transformShape(pointData[config.xAxis?.dataKey], pointData[filtered?.dataKey])}
144
+ className={`visx-glyph-group${displayArea ? '' : '-hidden'}`}
145
+ data-seriesIndex={seriesIndex}
146
+ >
147
+ <Shape size={dotSize} stroke={color} fill={color} />
214
148
  </g>
215
149
  )
216
150
  }
217
151
  }
218
152
 
153
+ if (mode === 'TOOLTIP_POINTS' && displayArea === true) {
154
+ return (
155
+ <g
156
+ transform={transformShape(tooltipPoint.xValue, tooltipPoint.yValue)}
157
+ className='visx-glyph-circle'
158
+ onMouseOver={e => {
159
+ handleTooltipMouseOver(e)
160
+ if (lineDatapointStyle == 'hover') (e.target as HTMLElement).style.fillOpacity = '1'
161
+ }}
162
+ onMouseOut={e => {
163
+ handleTooltipMouseOff()
164
+ if (lineDatapointStyle == 'hover') (e.target as HTMLElement).style.fillOpacity = '0'
165
+ }}
166
+ >
167
+ <Shape size={55} fill={tooltipPoint.color} fillOpacity={'0'} />
168
+ </g>
169
+ )
170
+ }
171
+
219
172
  return null
220
173
  }
221
174
 
@@ -1,23 +1,46 @@
1
1
  import { DataItem, StyleProps, Style } from './LineChartProps'
2
2
  import { PreliminaryDataItem } from '../../types/ChartConfig'
3
- import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
3
+
4
4
  import _ from 'lodash'
5
5
  export const createStyles = (props: StyleProps): Style[] => {
6
- const { preliminaryData, data, stroke, strokeWidth, handleLineType, lineType, seriesKey } = props
6
+ const {
7
+ preliminaryData,
8
+ data,
9
+ stroke,
10
+ strokeWidth,
11
+ handleLineType,
12
+ lineType,
13
+ seriesKey,
14
+ dynamicCategory,
15
+ originalSeriesKey
16
+ } = props
7
17
 
18
+ const dynamicSeriesKey = dynamicCategory ? originalSeriesKey : seriesKey
8
19
  const validPreliminaryData: PreliminaryDataItem[] = preliminaryData.filter(
9
20
  pd => pd.seriesKey && pd.column && pd.value && pd.type && pd.style && pd.type === 'effect'
10
21
  )
11
- const getMatchingPd = (point: DataItem): PreliminaryDataItem =>
12
- validPreliminaryData.find(
13
- pd =>
22
+ const isEffectLine = (pd, dataPoint) => {
23
+ if (dynamicCategory) {
24
+ return (
25
+ pd.type === 'effect' &&
26
+ pd.style !== 'Open Circles' &&
14
27
  pd.seriesKey === seriesKey &&
15
- point[pd.column] === pd.value &&
28
+ String(dataPoint[dynamicSeriesKey]) === String(pd.value)
29
+ )
30
+ } else {
31
+ return (
32
+ pd.seriesKey === seriesKey &&
33
+ dataPoint[pd.column] === pd.value &&
16
34
  pd.type === 'effect' &&
17
35
  pd.style !== 'Open Circles'
18
- )
36
+ )
37
+ }
38
+ }
39
+
40
+ const getMatchingPd = (point: DataItem): PreliminaryDataItem =>
41
+ validPreliminaryData.find(pd => isEffectLine(pd, point))
19
42
 
20
- let styles: Style[] = []
43
+ const styles: Style[] = []
21
44
  const createStyle = (lineStyle): Style => ({
22
45
  stroke: stroke,
23
46
  strokeWidth: strokeWidth,
@@ -25,7 +48,8 @@ export const createStyles = (props: StyleProps): Style[] => {
25
48
  })
26
49
 
27
50
  data.forEach((d, index) => {
28
- let matchingPd: PreliminaryDataItem = getMatchingPd(d)
51
+ const matchingPd: PreliminaryDataItem = getMatchingPd(d)
52
+
29
53
  let style: Style = matchingPd
30
54
  ? createStyle(handleLineType(matchingPd.style))
31
55
  : createStyle(handleLineType(lineType))
@@ -92,11 +116,20 @@ export const filterCircles = (
92
116
  }
93
117
 
94
118
  const isCalculable = value => !isNaN(parseFloat(value)) && isFinite(value)
95
- const handleFirstIndex = (data, seriesKey, preliminaryData) => {
119
+ const handleFirstIndex = ({
120
+ data,
121
+ seriesKey,
122
+ preliminaryData,
123
+ dynamicCategory,
124
+ originalSeriesKey,
125
+ colorScale,
126
+ isSuppressed
127
+ }) => {
96
128
  let pairCount = '0'
97
129
  const result = {
98
130
  data: { '0': [] },
99
- style: ''
131
+ style: '',
132
+ color: ''
100
133
  }
101
134
 
102
135
  // If data is empty, return the empty result
@@ -104,26 +137,24 @@ const handleFirstIndex = (data, seriesKey, preliminaryData) => {
104
137
 
105
138
  const firstIndexDataItem = data[0]
106
139
 
107
- // Function to check if a data item matches the suppression criteria
108
- const isSuppressed = pd => {
109
- if (pd.type === 'effect' || pd.hideLineStyle) return
110
- return (
111
- pd.type == 'suppression' && pd.value === firstIndexDataItem[seriesKey] && (!pd.column || pd.column === seriesKey)
112
- )
113
- }
114
-
115
140
  // Find applicable suppression data for the first item
116
- const suppressionData = preliminaryData.find(isSuppressed)
141
+ const suppressionData = (preliminaryData ?? []).find(
142
+ item => item && firstIndexDataItem && isSuppressed(item, firstIndexDataItem)
143
+ )
117
144
 
118
145
  if (suppressionData && suppressionData.style) {
119
146
  // Modify first item and add to result
120
- const modifiedItem = { ...firstIndexDataItem, [seriesKey]: 0 }
147
+ const dynamicSeriesKey = dynamicCategory ? originalSeriesKey : seriesKey
148
+
149
+ const modifiedItem = { ...firstIndexDataItem, [dynamicSeriesKey]: 0 }
150
+
121
151
  result.data[pairCount].push(modifiedItem)
122
152
  result.style = suppressionData.style
153
+ result.color = dynamicCategory && modifiedItem ? colorScale(modifiedItem[dynamicCategory]) : ''
123
154
 
124
155
  // Find the next calculable item index
125
156
  let nextIndex = 1
126
- while (nextIndex < data.length && !isCalculable(data[nextIndex][seriesKey])) {
157
+ while (nextIndex < data.length && !isCalculable(data[nextIndex][dynamicSeriesKey])) {
127
158
  nextIndex++
128
159
  }
129
160
  if (nextIndex < data.length) {
@@ -133,33 +164,38 @@ const handleFirstIndex = (data, seriesKey, preliminaryData) => {
133
164
  // If no suppression, just add the first item
134
165
  result.data[pairCount].push(firstIndexDataItem)
135
166
  }
136
-
137
167
  return result
138
168
  }
139
169
 
140
- const handleLastIndex = (data, seriesKey, preliminaryData) => {
170
+ const handleLastIndex = ({
171
+ data,
172
+ seriesKey,
173
+ preliminaryData,
174
+ dynamicCategory,
175
+ originalSeriesKey,
176
+ colorScale,
177
+ isSuppressed
178
+ }) => {
141
179
  let pairCount = '0'
142
180
  const result = {
143
181
  data: { '0': [] },
144
- style: ''
182
+ style: '',
183
+ color: ''
145
184
  }
146
- let lastAddedIndex = -1 // Tracks the last index added to the result
185
+ const lastIndexDataItem = data[data.length - 1]
186
+
187
+ const dynamicSeriesKey = dynamicCategory ? originalSeriesKey : seriesKey
188
+ let lastAddedIndex = -1
147
189
  preliminaryData?.forEach(pd => {
148
- if (pd.type === 'effect') return
149
- if (
150
- data[data.length - 1][seriesKey] === pd.value &&
151
- pd.style &&
152
- (!pd.column || pd.column === seriesKey) &&
153
- pd.type == 'suppression' &&
154
- !pd.hideLineStyle
155
- ) {
190
+ if (pd.type === 'effect') return []
191
+ if (isSuppressed(pd, lastIndexDataItem)) {
156
192
  const lastIndex = data.length - 1
157
- const modifiedItem = { ...data[lastIndex], [seriesKey]: 0 }
193
+ const modifiedItem = { ...data[lastIndex], [dynamicSeriesKey]: 0 }
158
194
  result.data[pairCount].push(modifiedItem)
159
195
 
160
196
  // Find previous calculable item
161
197
  let prevIndex = lastIndex - 1
162
- while (prevIndex >= 0 && !isCalculable(data[prevIndex][seriesKey])) {
198
+ while (prevIndex >= 0 && !isCalculable(data[prevIndex][dynamicSeriesKey])) {
163
199
  prevIndex--
164
200
  }
165
201
  if (prevIndex >= 0 && lastAddedIndex !== prevIndex) {
@@ -167,33 +203,47 @@ const handleLastIndex = (data, seriesKey, preliminaryData) => {
167
203
  lastAddedIndex = prevIndex
168
204
  }
169
205
  result.style = pd.style
206
+ result.color = colorScale(modifiedItem[dynamicCategory])
170
207
  }
171
208
  })
172
209
 
173
210
  return result
174
211
  }
175
212
 
176
- function handleMiddleIndices(data, seriesKey, preliminaryData) {
177
- // slice data to remove first and last object these no need for handleMiddleIndices
178
-
213
+ export const handleMiddleIndices = ({
214
+ data,
215
+ seriesKey,
216
+ preliminaryData,
217
+ dynamicCategory,
218
+ originalSeriesKey,
219
+ colorScale,
220
+ isSuppressed
221
+ }) => {
179
222
  let result = {
180
223
  data: {},
181
- style: ''
224
+ style: '',
225
+ color: 'red'
182
226
  }
227
+
228
+ //skip processing if data or preliminaryData is not an array
229
+ if (!Array.isArray(data) || !Array.isArray(preliminaryData)) {
230
+ return result
231
+ }
232
+
183
233
  // Variable to count the number of sibling pairs found
184
234
  let pairCount = 1
235
+ const dynamicSeriesKey = dynamicCategory ? originalSeriesKey : seriesKey
185
236
 
186
237
  // Loop through the data array to find each occurrence of the target value
187
238
  data.forEach((item, index) => {
188
239
  preliminaryData.forEach(pd => {
189
- const targetValue = pd.value
190
- if (item[seriesKey] === targetValue) {
240
+ if (isSuppressed(pd, item)) {
191
241
  let siblingBefore = null
192
242
  let siblingAfter = null
193
243
 
194
244
  // Find the nearest numeric sibling before the current index
195
245
  for (let i = index - 1; i >= 0; i--) {
196
- if (isCalculable(data[i][seriesKey])) {
246
+ if (isCalculable(data[i][dynamicSeriesKey])) {
197
247
  siblingBefore = data[i]
198
248
  break // Stop searching once a valid sibling is found
199
249
  }
@@ -201,7 +251,7 @@ function handleMiddleIndices(data, seriesKey, preliminaryData) {
201
251
 
202
252
  // Find the nearest numeric sibling after the current index
203
253
  for (let j = index + 1; j < data.length; j++) {
204
- if (isCalculable(data[j][seriesKey])) {
254
+ if (isCalculable(data[j][dynamicSeriesKey])) {
205
255
  siblingAfter = data[j]
206
256
  break // Stop searching once a valid sibling is found
207
257
  }
@@ -210,24 +260,51 @@ function handleMiddleIndices(data, seriesKey, preliminaryData) {
210
260
  // Only add siblings to results if both siblings are found
211
261
  if (siblingBefore && siblingAfter) {
212
262
  result.style = pd.style
263
+ result.color = colorScale(item[dynamicCategory])
213
264
  result.data[pairCount++] = [siblingBefore, siblingAfter]
214
265
  }
215
266
  }
216
267
  })
217
268
  })
269
+
218
270
  return result
219
271
  }
220
272
 
221
- // create segments (array of arrays) for building suppressed Lines
222
- export const createDataSegments = (data, seriesKey, preliminaryData, dataKey) => {
223
- // Process the first index if necessary
224
- const firstSegment = handleFirstIndex(data, seriesKey, preliminaryData)
225
- // Process the last index if necessary
226
- const lastSegment = handleLastIndex(data, seriesKey, preliminaryData)
227
- // Process the middle segment
228
- const middleSegments = handleMiddleIndices(data, seriesKey, preliminaryData)
229
-
230
- // Combine all segments into a single array
231
- return [firstSegment, middleSegments, lastSegment]
232
- // return [firstSegment, middleSegments, lastSegment].filter(segment => segment.data.length > 0 && segment.style !== '')
273
+ export const createDataSegments = props => {
274
+ const dynamicData = (props.data ?? []).filter(d => {
275
+ if (!props?.dynamicCategory) return true
276
+
277
+ return (props.preliminaryData ?? []).some(pd => d?.[props.dynamicCategory] === props?.seriesKey)
278
+ })
279
+ const isSuppressed = (pd, dataItem) => {
280
+ if (pd.type === 'effect' || pd.hideLineStyle) return false
281
+
282
+ if (props.dynamicCategory) {
283
+ return (
284
+ pd.type === 'suppression' &&
285
+ (!pd.column || pd.column === dataItem[props.dynamicCategory]) &&
286
+ pd.value === dataItem[props.originalSeriesKey]
287
+ )
288
+ }
289
+ return (
290
+ pd.type === 'suppression' &&
291
+ pd.value === dataItem[props.seriesKey] &&
292
+ (!pd.column || pd.column === props.seriesKey)
293
+ )
294
+ }
295
+ const firstSegment = handleFirstIndex({ ...props, data: dynamicData, isSuppressed })
296
+ const lastSegment = handleLastIndex({ ...props, data: dynamicData, isSuppressed })
297
+ const middleSegments = handleMiddleIndices({ ...props, data: dynamicData, isSuppressed })
298
+
299
+ const segments = [firstSegment, middleSegments, lastSegment]
300
+
301
+ // ✅ Filter: keep only segments with real data
302
+ return segments.filter(segment => {
303
+ if (!segment || !segment.data) return false
304
+
305
+ // Check if at least one non-empty array exists in `data`
306
+ const hasData = Object.values(segment.data).some(arr => Array.isArray(arr) && arr.length > 0)
307
+
308
+ return hasData
309
+ })
233
310
  }