@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.
- package/dist/cdcchart.js +46641 -42561
- package/index.html +130 -129
- package/package.json +22 -27
- package/src/CdcChartComponent.tsx +75 -35
- package/src/_stories/Chart.CI.stories.tsx +10 -0
- package/src/_stories/Chart.DynamicSeries.stories.tsx +68 -49
- package/src/_stories/Chart.stories.tsx +99 -86
- package/src/_stories/ChartPrefixSuffix.stories.tsx +29 -32
- package/{examples/private/line-issue.json → src/_stories/_mock/barchart_labels.mock.json} +150 -35
- package/src/_stories/_mock/dynamic_series_bar_config.json +1 -1
- package/src/_stories/_mock/dynamic_series_suppression_mock.json +610 -0
- package/{examples/private/not-loading.json → src/_stories/_mock/pie_calculated_area.json} +152 -95
- package/src/components/Annotations/components/AnnotationDropdown.tsx +2 -2
- package/src/components/AreaChart/components/AreaChart.jsx +33 -5
- package/src/components/Axis/Categorical.Axis.tsx +2 -2
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +38 -37
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +18 -8
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +8 -8
- package/src/components/BarChart/components/BarChart.Vertical.tsx +47 -36
- package/src/components/BarChart/components/{BarChart.jsx → BarChart.tsx} +23 -5
- package/src/components/BarChart/components/context.tsx +20 -2
- package/src/components/BarChart/helpers/getBarHeights.ts +47 -0
- package/src/components/BarChart/helpers/index.ts +5 -2
- package/src/components/BarChart/helpers/tests/getBarHeights.test.ts +83 -0
- package/src/{hooks → components/BarChart/helpers}/useBarChart.ts +9 -46
- package/src/components/BoxPlot/BoxPlot.tsx +2 -1
- package/src/components/Brush/BrushChart.tsx +73 -0
- package/src/components/Brush/BrushController..tsx +39 -0
- package/src/components/DeviationBar.jsx +2 -2
- package/src/components/EditorPanel/EditorPanel.tsx +232 -147
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +36 -36
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +52 -25
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +12 -4
- package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +8 -0
- package/src/components/EditorPanel/useEditorPermissions.ts +5 -5
- package/src/components/ForestPlot/ForestPlot.tsx +2 -2
- package/src/components/HoverLine/HoverLine.tsx +74 -0
- package/src/components/Legend/Legend.Component.tsx +1 -1
- package/src/components/Legend/Legend.Suppression.tsx +59 -25
- package/src/components/Legend/helpers/createFormatLabels.tsx +28 -0
- package/src/components/Legend/helpers/index.ts +1 -1
- package/src/components/LineChart/LineChartProps.ts +3 -1
- package/src/components/LineChart/components/LineChart.Circle.tsx +72 -119
- package/src/components/LineChart/helpers.ts +133 -56
- package/src/components/LineChart/index.tsx +106 -55
- package/src/components/LinearChart.tsx +178 -198
- package/src/components/PairedBarChart.jsx +3 -2
- package/src/components/PieChart/PieChart.tsx +127 -102
- package/src/components/ScatterPlot/ScatterPlot.jsx +5 -0
- package/src/components/Sparkline/components/SparkLine.tsx +80 -18
- package/src/data/initial-state.js +11 -6
- package/src/helpers/countNumOfTicks.ts +1 -1
- package/src/helpers/dataHelpers.ts +23 -2
- package/src/helpers/getNewRuntime.ts +35 -0
- package/src/helpers/getPiePercent.ts +22 -0
- package/src/helpers/getTransformedData.ts +22 -0
- package/src/helpers/sizeHelpers.ts +1 -1
- package/src/helpers/tests/getNewRuntime.test.ts +82 -0
- package/src/helpers/tests/getPiePercent.test.ts +38 -0
- package/src/hooks/useMinMax.ts +21 -28
- package/src/hooks/useRightAxis.ts +5 -3
- package/src/hooks/useScales.ts +15 -6
- package/src/hooks/useTooltip.tsx +218 -203
- package/src/index.jsx +2 -2
- package/src/scss/main.scss +13 -6
- package/src/store/chart.actions.ts +2 -6
- package/src/store/chart.reducer.ts +23 -23
- package/src/types/ChartConfig.ts +11 -3
- package/src/types/ChartContext.ts +0 -2
- package/examples/private/DEV-8850-2.json +0 -493
- package/examples/private/DEV-9822.json +0 -574
- package/examples/private/DEV-9840.json +0 -553
- package/examples/private/DEV-9850-3.json +0 -461
- package/examples/private/chart.json +0 -1084
- package/examples/private/ci_formatted.json +0 -202
- package/examples/private/ci_issue.json +0 -3016
- package/examples/private/completed.json +0 -634
- package/examples/private/dem-data-long.csv +0 -20
- package/examples/private/dem-data-long.json +0 -36
- package/examples/private/demographic_data.csv +0 -157
- package/examples/private/demographic_data.json +0 -2654
- package/examples/private/demographic_dynamic.json +0 -443
- package/examples/private/demographic_standard.json +0 -560
- package/examples/private/ehdi.json +0 -29939
- package/examples/private/test.json +0 -493
- 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
|
-
|
|
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
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
213
|
-
|
|
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
|
-
|
|
3
|
+
|
|
4
4
|
import _ from 'lodash'
|
|
5
5
|
export const createStyles = (props: StyleProps): Style[] => {
|
|
6
|
-
const {
|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = (
|
|
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(
|
|
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
|
|
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][
|
|
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 = (
|
|
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
|
-
|
|
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], [
|
|
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][
|
|
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
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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][
|
|
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][
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
}
|