@cdc/chart 4.24.9 → 4.24.11

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 (95) hide show
  1. package/LICENSE +201 -0
  2. package/dist/cdcchart.js +45911 -41739
  3. package/examples/feature/boxplot/boxplot-data.json +88 -22
  4. package/examples/feature/boxplot/boxplot.json +540 -16
  5. package/examples/feature/boxplot/testing.csv +7 -7
  6. package/examples/feature/sankey/sankey-example-data.json +0 -1
  7. package/examples/private/test.json +20092 -0
  8. package/index.html +4 -4
  9. package/package.json +2 -2
  10. package/src/CdcChart.tsx +209 -188
  11. package/src/_stories/Chart.CustomColors.stories.tsx +19 -0
  12. package/src/_stories/Chart.DynamicSeries.stories.tsx +27 -0
  13. package/src/_stories/Chart.Legend.Gradient.stories.tsx +74 -0
  14. package/src/_stories/Chart.stories.tsx +30 -3
  15. package/src/_stories/ChartAxisLabels.stories.tsx +20 -0
  16. package/src/_stories/ChartAxisTitles.stories.tsx +53 -0
  17. package/src/_stories/ChartEditor.stories.tsx +27 -0
  18. package/src/_stories/ChartLine.Suppression.stories.tsx +25 -0
  19. package/src/_stories/ChartPrefixSuffix.stories.tsx +159 -0
  20. package/src/_stories/_mock/boxplot_multiseries.json +647 -0
  21. package/src/_stories/_mock/dynamic_series_bar_config.json +723 -0
  22. package/src/_stories/_mock/dynamic_series_config.json +979 -0
  23. package/src/_stories/_mock/horizontal_bar.json +257 -0
  24. package/src/_stories/_mock/large_x_axis_labels.json +261 -0
  25. package/src/_stories/_mock/paired-bar.json +262 -0
  26. package/src/_stories/_mock/pie_with_data.json +255 -0
  27. package/{examples/feature/scatterplot/scatterplot.json → src/_stories/_mock/scatterplot_mock.json} +62 -92
  28. package/src/_stories/_mock/simplified_line.json +1510 -0
  29. package/src/_stories/_mock/suppression_mock.json +1549 -0
  30. package/src/components/Annotations/components/AnnotationDraggable.tsx +0 -3
  31. package/src/components/Annotations/components/AnnotationDropdown.tsx +1 -1
  32. package/src/components/Axis/Categorical.Axis.tsx +22 -4
  33. package/src/components/BarChart/components/BarChart.Horizontal.tsx +95 -16
  34. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +41 -17
  35. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +43 -9
  36. package/src/components/BarChart/components/BarChart.Vertical.tsx +123 -47
  37. package/src/components/BarChart/helpers/index.ts +23 -5
  38. package/src/components/BoxPlot/BoxPlot.tsx +189 -0
  39. package/src/components/BrushChart.tsx +3 -2
  40. package/src/components/DeviationBar.jsx +58 -8
  41. package/src/components/EditorPanel/EditorPanel.tsx +127 -102
  42. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +11 -28
  43. package/src/components/EditorPanel/components/Panels/Panel.BoxPlot.tsx +51 -6
  44. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +21 -4
  45. package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +40 -9
  46. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +3 -3
  47. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +121 -56
  48. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +296 -35
  49. package/src/components/EditorPanel/components/panels.scss +4 -6
  50. package/src/components/EditorPanel/editor-panel.scss +0 -8
  51. package/src/components/EditorPanel/helpers/tests/updateFieldRankByValue.test.ts +38 -0
  52. package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +42 -0
  53. package/src/components/EditorPanel/useEditorPermissions.ts +16 -1
  54. package/src/components/ForestPlot/ForestPlot.tsx +2 -3
  55. package/src/components/ForestPlot/ForestPlotProps.ts +2 -0
  56. package/src/components/Legend/Legend.Component.tsx +23 -24
  57. package/src/components/Legend/Legend.Suppression.tsx +25 -20
  58. package/src/components/Legend/Legend.tsx +16 -18
  59. package/src/components/Legend/helpers/index.ts +16 -19
  60. package/src/components/LegendWrapper.tsx +3 -1
  61. package/src/components/LineChart/components/LineChart.Circle.tsx +10 -0
  62. package/src/components/LineChart/helpers.ts +48 -43
  63. package/src/components/LineChart/index.tsx +88 -82
  64. package/src/components/LinearChart.tsx +747 -562
  65. package/src/components/PairedBarChart.jsx +50 -10
  66. package/src/components/PieChart/PieChart.tsx +1 -6
  67. package/src/components/Regions/components/Regions.tsx +33 -19
  68. package/src/components/Sankey/index.tsx +50 -32
  69. package/src/components/Sankey/sankey.scss +6 -5
  70. package/src/components/Sankey/useSankeyAlert.tsx +60 -0
  71. package/src/components/ScatterPlot/ScatterPlot.jsx +20 -4
  72. package/src/components/ZoomBrush.tsx +25 -6
  73. package/src/coreStyles_chart.scss +3 -0
  74. package/src/data/initial-state.js +8 -10
  75. package/src/helpers/configHelpers.ts +28 -0
  76. package/src/helpers/handleRankByValue.ts +15 -0
  77. package/src/helpers/sizeHelpers.ts +25 -0
  78. package/src/helpers/tests/handleRankByValue.test.ts +37 -0
  79. package/src/helpers/tests/sizeHelpers.test.ts +80 -0
  80. package/src/hooks/useColorPalette.js +10 -2
  81. package/src/hooks/useLegendClasses.ts +13 -22
  82. package/src/hooks/useMinMax.ts +27 -13
  83. package/src/hooks/useReduceData.ts +43 -10
  84. package/src/hooks/useScales.ts +87 -38
  85. package/src/hooks/useTooltip.tsx +62 -53
  86. package/src/index.jsx +1 -0
  87. package/src/scss/DataTable.scss +5 -4
  88. package/src/scss/main.scss +57 -70
  89. package/src/types/ChartConfig.ts +43 -34
  90. package/src/types/ChartContext.ts +22 -15
  91. package/src/types/ForestPlot.ts +8 -0
  92. package/src/_stories/Chart.Legend.Gradient.tsx +0 -19
  93. package/src/_stories/ChartBrush.stories.tsx +0 -19
  94. package/src/components/BoxPlot/BoxPlot.jsx +0 -111
  95. package/src/components/LinearChart.jsx +0 -817
@@ -9,17 +9,20 @@ import { handleLineType } from '../../helpers/handleLineType'
9
9
  import { getMarginTop, getGradientConfig, getMarginBottom } from './helpers/index'
10
10
  import { Line } from '@visx/shape'
11
11
  import { Label } from '../../types/Label'
12
- import { ChartConfig } from '../../types/ChartConfig'
12
+ import { ChartConfig, ViewportSize } from '../../types/ChartConfig'
13
13
  import { ColorScale } from '../../types/ChartContext'
14
- import { forwardRef } from 'react'
14
+ import { forwardRef, useState } from 'react'
15
15
  import LegendSuppression from './Legend.Suppression'
16
16
  import LegendGradient from '@cdc/core/components/Legend/Legend.Gradient'
17
17
  import { DimensionsType } from '@cdc/core/types/Dimensions'
18
+ import { isLegendWrapViewport } from '@cdc/core/helpers/viewports'
19
+
20
+ const LEGEND_PADDING = 30
18
21
 
19
22
  export interface LegendProps {
20
23
  colorScale: ColorScale
21
24
  config: ChartConfig
22
- currentViewport: 'lg' | 'md' | 'sm' | 'xs' | 'xxs'
25
+ currentViewport: ViewportSize
23
26
  formatLabels: (labels: Label[]) => Label[]
24
27
  highlight: Function
25
28
  highlightReset: Function
@@ -27,7 +30,6 @@ export interface LegendProps {
27
30
  seriesHighlight: string[]
28
31
  skipId: string
29
32
  dimensions: DimensionsType // for responsive width legend
30
- getTextWidth: (text: string, font: string) => string
31
33
  }
32
34
 
33
35
  /* eslint-disable jsx-a11y/no-noninteractive-tabindex, jsx-a11y/no-static-element-interactions */
@@ -42,24 +44,21 @@ const Legend: React.FC<LegendProps> = forwardRef(
42
44
  currentViewport,
43
45
  formatLabels,
44
46
  skipId = 'legend',
45
- dimensions,
46
- getTextWidth
47
+ dimensions
47
48
  },
48
49
  ref
49
50
  ) => {
50
51
  const { innerClasses, containerClasses } = useLegendClasses(config)
51
52
  const { runtime, legend } = config
52
53
 
54
+ const [hasSuppression, setHasSuppression] = useState(false)
55
+
53
56
  const isBottomOrSmallViewport =
54
- legend?.position === 'bottom' || (['sm', 'xs', 'xxs'].includes(currentViewport) && !legend.hide)
57
+ legend?.position === 'bottom' || (isLegendWrapViewport(currentViewport) && !legend.hide)
55
58
 
56
59
  const legendClasses = {
57
- marginBottom: getMarginBottom(isBottomOrSmallViewport, config),
58
-
59
- marginTop:
60
- isBottomOrSmallViewport && config.orientation === 'horizontal'
61
- ? `${config.yAxis.label && config.isResponsiveTicks ? config.dynamicMarginTop : config.runtime.xAxis.size}px`
62
- : getMarginTop(isBottomOrSmallViewport, config.brush.active, legend)
60
+ marginBottom: getMarginBottom(config, hasSuppression),
61
+ marginTop: getMarginTop(isBottomOrSmallViewport, config)
63
62
  }
64
63
 
65
64
  const { HighLightedBarUtils } = useHighlightedBars(config)
@@ -78,11 +77,10 @@ const Legend: React.FC<LegendProps> = forwardRef(
78
77
  {legend.label && <h3>{parse(legend.label)}</h3>}
79
78
  {legend.description && <p>{parse(legend.description)}</p>}
80
79
  <LegendGradient
81
- getTextWidth={getTextWidth}
82
80
  config={config}
83
81
  {...getGradientConfig(config, formatLabels, colorScale)}
84
82
  dimensions={dimensions}
85
- currentViewport={currentViewport}
83
+ parentPaddingToSubtract={legend.hideBorder ? 0 : LEGEND_PADDING}
86
84
  />
87
85
 
88
86
  <LegendOrdinal scale={colorScale} itemDirection='row' labelMargin='0 20px 0 0' shapeMargin='0 10px 0'>
@@ -133,9 +131,9 @@ const Legend: React.FC<LegendProps> = forwardRef(
133
131
  }}
134
132
  role='button'
135
133
  >
136
- <div>
134
+ <>
137
135
  {config.visualizationType === 'Line' && config.legend.style === 'lines' ? (
138
- <svg width={40} height={20}>
136
+ <svg width={40} height={25}>
139
137
  <Line
140
138
  from={{ x: 10, y: 10 }}
141
139
  to={{ x: 40, y: 10 }}
@@ -145,17 +143,14 @@ const Legend: React.FC<LegendProps> = forwardRef(
145
143
  />
146
144
  </svg>
147
145
  ) : (
148
- <div style={{ display: 'flex', flexDirection: 'column' }}>
146
+ <>
149
147
  <LegendShape
150
148
  shape={config.legend.style === 'boxes' ? 'square' : 'circle'}
151
- viewport={currentViewport}
152
- margin='0'
153
149
  fill={label.value}
154
- display={true}
155
150
  />
156
- </div>
151
+ </>
157
152
  )}
158
- </div>
153
+ </>
159
154
 
160
155
  <LegendLabel align='left' margin='0 0 0 4px'>
161
156
  {label.text}
@@ -204,7 +199,11 @@ const Legend: React.FC<LegendProps> = forwardRef(
204
199
  })}
205
200
  </div>
206
201
 
207
- <LegendSuppression config={config} isBottomOrSmallViewport={isBottomOrSmallViewport} />
202
+ <LegendSuppression
203
+ config={config}
204
+ isBottomOrSmallViewport={isBottomOrSmallViewport}
205
+ setHasSuppression={setHasSuppression}
206
+ />
208
207
  </>
209
208
  )
210
209
  }}
@@ -7,11 +7,11 @@ interface LegendProps {
7
7
  isBottomOrSmallViewport: boolean
8
8
  }
9
9
 
10
- const LegendSuppression: React.FC<LegendProps> = ({ config, isBottomOrSmallViewport }) => {
10
+ const LegendSuppression: React.FC<LegendProps> = ({ config, isBottomOrSmallViewport, setHasSuppression }) => {
11
11
  const { preliminaryData, visualizationType, visualizationSubType, legend } = config
12
12
 
13
13
  const hasOpenCircleEffects = () =>
14
- preliminaryData?.some(pd => pd.label && pd.type === 'effect' && pd.style === 'Open Circles') &&
14
+ preliminaryData?.some(pd => pd.label && pd.type === 'effect' && pd.style !== 'Filled Circles') &&
15
15
  ['Line', 'Combo'].includes(visualizationType)
16
16
 
17
17
  const shouldShowSuppressedLabels = () =>
@@ -100,6 +100,13 @@ const LegendSuppression: React.FC<LegendProps> = ({ config, isBottomOrSmallViewp
100
100
  const getLegendContainerClass = () =>
101
101
  legend.singleRow && isBottomOrSmallViewport ? 'legend-container__inner bottom single-row' : ''
102
102
 
103
+ const shouldShowSuppressedInfo = () =>
104
+ !config.legend.hideSuppressionLink &&
105
+ config.visualizationSubType !== 'stacked' &&
106
+ preliminaryData?.some(pd => pd.label && pd.type === 'suppression' && pd.value && (pd?.style || pd.symbol))
107
+
108
+ setHasSuppression(shouldShowSuppressedInfo())
109
+
103
110
  return (
104
111
  <React.Fragment>
105
112
  {hasOpenCircleEffects() && (
@@ -115,24 +122,22 @@ const LegendSuppression: React.FC<LegendProps> = ({ config, isBottomOrSmallViewp
115
122
  <div className={getLegendContainerClass()}>{renderSuppressedItems()}</div>
116
123
  </React.Fragment>
117
124
  )}
118
- {!config.legend.hideSuppressionLink &&
119
- config.visualizationSubType !== 'stacked' &&
120
- preliminaryData?.some(pd => pd.label && pd.type === 'suppression' && pd.value && (pd?.style || pd.symbol)) && (
121
- <div className='legend-container__outer definition-link'>
122
- <Icon alt='info-icon' display='info' />
123
- <p>
124
- This chart contains
125
- <a // prettier-ignore
126
- onClick={handleLinkClick}
127
- data-tooltip-content='Data is suppressed to maintain statistical reliability. This occurs when the number of respondents or reported values does not meet the minimum reporting threshold.'
128
- data-tooltip-id='my-tooltip'
129
- href='no-router-link'
130
- >
131
- suppressed data
132
- </a>
133
- </p>
134
- </div>
135
- )}
125
+ {shouldShowSuppressedInfo() && (
126
+ <div className='legend-container__outer definition-link'>
127
+ <Icon alt='info-icon' display='info' />
128
+ <p>
129
+ This chart contains
130
+ <a // prettier-ignore
131
+ onClick={handleLinkClick}
132
+ data-tooltip-content='Data is suppressed to maintain statistical reliability. This occurs when the number of respondents or reported values does not meet the minimum reporting threshold.'
133
+ data-tooltip-id='my-tooltip'
134
+ href='no-router-link'
135
+ >
136
+ suppressed data
137
+ </a>
138
+ </p>
139
+ </div>
140
+ )}
136
141
 
137
142
  <ReactTooltip // prettier-ignore
138
143
  id='my-tooltip'
@@ -17,7 +17,7 @@ const Legend = forwardRef((props, ref) => {
17
17
  transformedData: data,
18
18
  currentViewport,
19
19
  dimensions,
20
- getTextWidth,
20
+ getTextWidth
21
21
  } = useContext(ConfigContext)
22
22
  if (!config.legend) return null
23
23
  // create fn to reverse labels while legend is Bottom. Legend-right , legend-left works by default.
@@ -25,23 +25,21 @@ const Legend = forwardRef((props, ref) => {
25
25
  const createLegendLabels = createFormatLabels(config, tableData, data, colorScale)
26
26
 
27
27
  return (
28
- !['Box Plot'].includes(config.visualizationType) && (
29
- <Fragment>
30
- <LegendComponent
31
- getTextWidth={getTextWidth}
32
- dimensions={dimensions}
33
- ref={ref}
34
- skipId={props.skipId || 'legend'}
35
- config={config}
36
- colorScale={colorScale}
37
- seriesHighlight={seriesHighlight}
38
- highlight={highlight}
39
- highlightReset={highlightReset}
40
- currentViewport={currentViewport}
41
- formatLabels={createLegendLabels}
42
- />
43
- </Fragment>
44
- )
28
+ <Fragment>
29
+ <LegendComponent
30
+ getTextWidth={getTextWidth}
31
+ dimensions={dimensions}
32
+ ref={ref}
33
+ skipId={props.skipId || 'legend'}
34
+ config={config}
35
+ colorScale={colorScale}
36
+ seriesHighlight={seriesHighlight}
37
+ highlight={highlight}
38
+ highlightReset={highlightReset}
39
+ currentViewport={currentViewport}
40
+ formatLabels={createLegendLabels}
41
+ />
42
+ </Fragment>
45
43
  )
46
44
  })
47
45
 
@@ -1,8 +1,3 @@
1
- export const getMarginTop = (isBottomOrSmallViewport, isBrushActive, legend) => {
2
- if (!isBottomOrSmallViewport) return '0px'
3
- if (isBrushActive && legend.position === 'bottom') return '35px'
4
- }
5
-
6
1
  export const getGradientConfig = (config, formatLabels, colorScale) => {
7
2
  const defaultValue = [{ datum: '', index: 0, text: '', value: '' }]
8
3
 
@@ -15,21 +10,23 @@ export const getGradientConfig = (config, formatLabels, colorScale) => {
15
10
  return { colors, labels }
16
11
  }
17
12
 
18
- export const getMarginBottom = (isBottomOrSmallViewport, config) => {
19
- const isSuppressedActive = config.preliminaryData.some(pd => pd.label) && !config.legend.hideSuppressionLink
20
-
21
- const isLegendTop = config.legend?.position === 'top' && !config.legend.hide
22
-
23
- let marginBottom = '0px'
24
- if (isLegendTop && !isSuppressedActive) {
25
- marginBottom = config.legend.hideBorder.topBottom ? '15px' : '25px'
26
- }
27
- if (isLegendTop && isSuppressedActive) {
28
- marginBottom = '75px'
13
+ export const getMarginTop = (isBottomOrSmallViewport, config) => {
14
+ if (!isBottomOrSmallViewport) {
15
+ return '0px'
29
16
  }
30
- if (isBottomOrSmallViewport && isSuppressedActive) {
31
- marginBottom = '45px'
17
+ if (isBottomOrSmallViewport && config.brush?.active) {
18
+ return '35px'
32
19
  }
20
+ return '20px'
21
+ }
22
+ export const getMarginBottom = (config, hasSuppression) => {
23
+ const isLegendTop = config.legend?.position === 'top' && !config.legend.hide
24
+
25
+ let marginBottom = 0
26
+
27
+ if (isLegendTop) marginBottom = config.legend.hideBorder.topBottom ? 15 : 25
28
+
29
+ if (hasSuppression) marginBottom += 40
33
30
 
34
- return marginBottom
31
+ return `${marginBottom}px`
35
32
  }
@@ -1,6 +1,8 @@
1
1
  import React, { useContext } from 'react'
2
2
  import ConfigContext from '../ConfigContext'
3
3
 
4
+ import { isLegendWrapViewport } from '@cdc/core/helpers/viewports'
5
+
4
6
  type LegendWrapperProps = {
5
7
  children: React.ReactNode
6
8
  }
@@ -13,7 +15,7 @@ const LegendWrapper: React.FC<LegendWrapperProps> = props => {
13
15
  const getLegendWrappingClasses = () => {
14
16
  let classes = ['legend-wrapper', 'd-flex', 'flex-nowrap', 'w-100']
15
17
  const { legend } = config
16
- if (legend.position === 'bottom' || legend.position === 'top' || ['xxs', 'xs', 'sm'].includes(currentViewport)) {
18
+ if (legend.position === 'bottom' || legend.position === 'top' || isLegendWrapViewport(currentViewport)) {
17
19
  classes = classes.filter(item => item !== 'flex-nowrap')
18
20
  classes.push('flex-wrap')
19
21
  }
@@ -147,10 +147,17 @@ const LineChartCircle = (props: LineChartCircleProps) => {
147
147
 
148
148
  if (mode === 'ISOLATED_POINTS') {
149
149
  const drawIsolatedPoints = (currentIndex, seriesKey) => {
150
+ let isMatch = false
150
151
  const currentPoint = data[currentIndex]
151
152
  const previousPoint = currentIndex > 0 ? data[currentIndex - 1] : null
152
153
  const nextPoint = currentIndex < data.length - 1 ? data[currentIndex + 1] : null
153
154
  let res = false
155
+ // check if isolated points has overlap with circle effect
156
+ circleData.forEach(item => {
157
+ if (item?.data[seriesKey] === currentPoint[seriesKey]) {
158
+ isMatch = true
159
+ }
160
+ })
154
161
 
155
162
  // Handle the first point in the array
156
163
  if (currentIndex === 0 && nextPoint && !nextPoint[seriesKey]) {
@@ -171,6 +178,9 @@ const LineChartCircle = (props: LineChartCircleProps) => {
171
178
  res = true
172
179
  }
173
180
  }
181
+ if (isMatch) {
182
+ res = false
183
+ }
174
184
 
175
185
  return res
176
186
  }
@@ -92,8 +92,9 @@ export const filterCircles = (
92
92
 
93
93
  const isCalculable = value => !isNaN(parseFloat(value)) && isFinite(value)
94
94
  const handleFirstIndex = (data, seriesKey, preliminaryData) => {
95
+ let pairCount = '0'
95
96
  const result = {
96
- data: [],
97
+ data: { '0': [] },
97
98
  style: ''
98
99
  }
99
100
 
@@ -116,7 +117,7 @@ const handleFirstIndex = (data, seriesKey, preliminaryData) => {
116
117
  if (suppressionData && suppressionData.style) {
117
118
  // Modify first item and add to result
118
119
  const modifiedItem = { ...firstIndexDataItem, [seriesKey]: 0 }
119
- result.data.push(modifiedItem)
120
+ result.data[pairCount].push(modifiedItem)
120
121
  result.style = suppressionData.style
121
122
 
122
123
  // Find the next calculable item index
@@ -125,19 +126,20 @@ const handleFirstIndex = (data, seriesKey, preliminaryData) => {
125
126
  nextIndex++
126
127
  }
127
128
  if (nextIndex < data.length) {
128
- result.data.push(data[nextIndex])
129
+ result.data[pairCount].push(data[nextIndex])
129
130
  }
130
131
  } else {
131
132
  // If no suppression, just add the first item
132
- result.data.push(firstIndexDataItem)
133
+ result.data[pairCount].push(firstIndexDataItem)
133
134
  }
134
135
 
135
136
  return result
136
137
  }
137
138
 
138
139
  const handleLastIndex = (data, seriesKey, preliminaryData) => {
140
+ let pairCount = '0'
139
141
  const result = {
140
- data: [],
142
+ data: { '0': [] },
141
143
  style: ''
142
144
  }
143
145
  let lastAddedIndex = -1 // Tracks the last index added to the result
@@ -152,7 +154,7 @@ const handleLastIndex = (data, seriesKey, preliminaryData) => {
152
154
  ) {
153
155
  const lastIndex = data.length - 1
154
156
  const modifiedItem = { ...data[lastIndex], [seriesKey]: 0 }
155
- result.data.push(modifiedItem)
157
+ result.data[pairCount].push(modifiedItem)
156
158
 
157
159
  // Find previous calculable item
158
160
  let prevIndex = lastIndex - 1
@@ -160,7 +162,7 @@ const handleLastIndex = (data, seriesKey, preliminaryData) => {
160
162
  prevIndex--
161
163
  }
162
164
  if (prevIndex >= 0 && lastAddedIndex !== prevIndex) {
163
- result.data.push(data[prevIndex])
165
+ result.data[pairCount].push(data[prevIndex])
164
166
  lastAddedIndex = prevIndex
165
167
  }
166
168
  result.style = pd.style
@@ -170,47 +172,48 @@ const handleLastIndex = (data, seriesKey, preliminaryData) => {
170
172
  return result
171
173
  }
172
174
 
173
- function handleMiddleIndices(data, seriesKey, dataKey, preliminaryData) {
174
- const result = {
175
- data: [],
175
+ function handleMiddleIndices(data, seriesKey, preliminaryData) {
176
+ // slice data to remove first and last object these no need for handleMiddleIndices
177
+
178
+ let result = {
179
+ data: {},
176
180
  style: ''
177
181
  }
182
+ // Variable to count the number of sibling pairs found
183
+ let pairCount = 1
184
+
185
+ // Loop through the data array to find each occurrence of the target value
186
+ data.forEach((item, index) => {
187
+ preliminaryData.forEach(pd => {
188
+ const targetValue = pd.value
189
+ if (item[seriesKey] === targetValue) {
190
+ let siblingBefore = null
191
+ let siblingAfter = null
192
+
193
+ // Find the nearest numeric sibling before the current index
194
+ for (let i = index - 1; i >= 0; i--) {
195
+ if (isCalculable(data[i][seriesKey])) {
196
+ siblingBefore = data[i]
197
+ break // Stop searching once a valid sibling is found
198
+ }
199
+ }
178
200
 
179
- const isValidMiddleIndex = index => index > 0 && index < data.length - 1
180
-
181
- preliminaryData?.forEach(pd => {
182
- if (pd.type === 'effect' || pd.hideLineStyle) return
183
- const targetValue = pd.value
184
-
185
- // Find all indices
186
- const matchingIndices = data.reduce((indices, item, index) => {
187
- if (item[seriesKey] === targetValue && isValidMiddleIndex(index) && (!pd.column || pd.column === seriesKey)) {
188
- indices.push(index)
189
- }
190
- return indices
191
- }, [])
192
-
193
- // Process each valid index
194
- matchingIndices.forEach(i => {
195
- result.style = pd.style
196
- // Add previous object if calculable
197
- if (isCalculable(data[i - 1][seriesKey])) {
198
- result.data.push(data[i - 1])
199
- }
201
+ // Find the nearest numeric sibling after the current index
202
+ for (let j = index + 1; j < data.length; j++) {
203
+ if (isCalculable(data[j][seriesKey])) {
204
+ siblingAfter = data[j]
205
+ break // Stop searching once a valid sibling is found
206
+ }
207
+ }
200
208
 
201
- // Find and add the next calculable object
202
- const nextIndex = data
203
- .slice(i + 1)
204
- .findIndex(item => item[seriesKey] !== targetValue && isCalculable(item[seriesKey]))
205
- if (nextIndex !== -1) {
206
- result.data.push(data[i + 1 + nextIndex])
209
+ // Only add siblings to results if both siblings are found
210
+ if (siblingBefore && siblingAfter) {
211
+ result.style = pd.style
212
+ result.data[pairCount++] = [siblingBefore, siblingAfter]
213
+ }
207
214
  }
208
215
  })
209
216
  })
210
-
211
- // Deduplicate entries
212
- result.data = _.uniqWith(result.data, (a, b) => a[dataKey] === b[dataKey] && a[seriesKey] === b[seriesKey])
213
-
214
217
  return result
215
218
  }
216
219
 
@@ -221,7 +224,9 @@ export const createDataSegments = (data, seriesKey, preliminaryData, dataKey) =>
221
224
  // Process the last index if necessary
222
225
  const lastSegment = handleLastIndex(data, seriesKey, preliminaryData)
223
226
  // Process the middle segment
224
- const middleSegments = handleMiddleIndices(data, seriesKey, dataKey, preliminaryData)
227
+ const middleSegments = handleMiddleIndices(data, seriesKey, preliminaryData)
228
+
225
229
  // Combine all segments into a single array
226
- return [firstSegment, middleSegments, lastSegment].filter(segment => segment.data.length > 0 && segment.style !== '')
230
+ return [firstSegment, middleSegments, lastSegment]
231
+ // return [firstSegment, middleSegments, lastSegment].filter(segment => segment.data.length > 0 && segment.style !== '')
227
232
  }