@cdc/chart 4.24.4 → 4.24.7
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 +39611 -36038
- package/examples/feature/annotations/index.json +542 -0
- package/examples/xaxis.json +493 -0
- package/index.html +9 -8
- package/package.json +5 -4
- package/src/CdcChart.tsx +115 -71
- package/src/_stories/Chart.stories.tsx +26 -171
- package/src/_stories/ChartAnnotation.stories.tsx +32 -0
- package/src/_stories/_mock/annotation_category_mock.json +473 -0
- package/src/_stories/_mock/annotation_date-linear_mock.json +530 -0
- package/src/_stories/_mock/annotation_date-time_mock.json +530 -0
- package/src/_stories/_mock/bar-chart-suppressed.json +474 -0
- package/src/_stories/_mock/line_chart_two_points_new_chart.json +128 -0
- package/src/_stories/_mock/line_chart_two_points_regression_test.json +127 -0
- package/src/_stories/_mock/lollipop.json +171 -0
- package/src/components/Annotations/components/AnnotationDraggable.styles.css +31 -0
- package/src/components/Annotations/components/AnnotationDraggable.tsx +154 -0
- package/src/components/Annotations/components/AnnotationDropdown.styles.css +14 -0
- package/src/components/Annotations/components/AnnotationDropdown.tsx +72 -0
- package/src/components/Annotations/components/AnnotationList.styles.css +45 -0
- package/src/components/Annotations/components/AnnotationList.tsx +42 -0
- package/src/components/Annotations/components/findNearestDatum.ts +138 -0
- package/src/components/Annotations/components/helpers/index.tsx +46 -0
- package/src/components/Annotations/index.tsx +13 -0
- package/src/components/AreaChart/components/AreaChart.Stacked.jsx +1 -1
- package/src/components/AreaChart/components/AreaChart.jsx +2 -2
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +78 -71
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +1 -2
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +11 -11
- package/src/components/BarChart/components/BarChart.Vertical.tsx +100 -87
- package/src/components/BarChart/helpers/index.ts +102 -0
- package/src/components/DeviationBar.jsx +4 -2
- package/src/components/EditorPanel/EditorPanel.tsx +435 -613
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +306 -0
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +135 -7
- package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +2 -3
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +4 -5
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +3 -2
- package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
- package/src/components/EditorPanel/components/panels.scss +4 -0
- package/src/components/EditorPanel/editor-panel.scss +19 -0
- package/src/components/EditorPanel/useEditorPermissions.js +23 -3
- package/src/components/Legend/Legend.Component.tsx +66 -15
- package/src/components/Legend/helpers/createFormatLabels.tsx +1 -1
- package/src/components/Legend/helpers/index.ts +5 -0
- package/src/components/LineChart/LineChartProps.ts +16 -6
- package/src/components/LineChart/components/LineChart.Circle.tsx +22 -11
- package/src/components/LineChart/helpers.ts +148 -10
- package/src/components/LineChart/index.tsx +71 -44
- package/src/components/LinearChart.jsx +184 -125
- package/src/components/PairedBarChart.jsx +9 -9
- package/src/components/PieChart/PieChart.tsx +4 -4
- package/src/components/Sankey/index.tsx +73 -20
- package/src/components/ScatterPlot/ScatterPlot.jsx +22 -8
- package/src/components/ZoomBrush.tsx +120 -55
- package/src/data/initial-state.js +14 -6
- package/src/helpers/handleChartTabbing.ts +8 -0
- package/src/helpers/isConvertLineToBarGraph.ts +4 -0
- package/src/hooks/{useBarChart.js → useBarChart.ts} +9 -22
- package/src/hooks/useColorScale.ts +1 -1
- package/src/hooks/useMinMax.ts +29 -5
- package/src/hooks/useScales.ts +48 -26
- package/src/hooks/useTooltip.tsx +62 -15
- package/src/scss/main.scss +69 -12
- package/src/types/ChartConfig.ts +53 -16
- package/src/types/ChartContext.ts +13 -0
- package/tests-examples/helpers/testZeroValue.test.ts +30 -0
- package/LICENSE +0 -201
- package/src/_stories/ChartLine.preliminary.tsx +0 -19
- package/src/_stories/ChartSuppress.stories.tsx +0 -19
- package/src/_stories/_mock/suppress_mock.json +0 -911
- package/src/helpers/computeMarginBottom.ts +0 -56
- package/src/helpers/filterData.ts +0 -18
- package/src/helpers/tests/computeMarginBottom.test.ts +0 -21
- /package/src/hooks/{useLegendClasses.js → useLegendClasses.ts} +0 -0
- /package/src/hooks/{useReduceData.js → useReduceData.ts} +0 -0
|
@@ -5,45 +5,46 @@ import Button from '@cdc/core/components/elements/Button'
|
|
|
5
5
|
import useLegendClasses from '../../hooks/useLegendClasses'
|
|
6
6
|
import { useHighlightedBars } from '../../hooks/useHighlightedBars'
|
|
7
7
|
import { handleLineType } from '../../helpers/handleLineType'
|
|
8
|
+
import { getMarginTop } from './helpers/index'
|
|
8
9
|
import { Line } from '@visx/shape'
|
|
9
10
|
import { Label } from '../../types/Label'
|
|
10
11
|
import { ChartConfig } from '../../types/ChartConfig'
|
|
11
12
|
import { ColorScale } from '../../types/ChartContext'
|
|
12
13
|
import { forwardRef } from 'react'
|
|
13
14
|
|
|
14
|
-
interface LegendProps {
|
|
15
|
-
config: ChartConfig
|
|
15
|
+
export interface LegendProps {
|
|
16
16
|
colorScale: ColorScale
|
|
17
|
-
|
|
18
|
-
highlight: Function
|
|
19
|
-
highlightReset: Function
|
|
17
|
+
config: ChartConfig
|
|
20
18
|
currentViewport: 'lg' | 'md' | 'sm' | 'xs' | 'xxs'
|
|
21
19
|
formatLabels: (labels: Label[]) => Label[]
|
|
20
|
+
highlight: Function
|
|
21
|
+
highlightReset: Function
|
|
22
22
|
ref: React.Ref<() => void>
|
|
23
|
+
seriesHighlight: string[]
|
|
23
24
|
skipId: string
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
/* eslint-disable jsx-a11y/no-noninteractive-tabindex, jsx-a11y/no-static-element-interactions */
|
|
27
28
|
const Legend: React.FC<LegendProps> = forwardRef(({ config, colorScale, seriesHighlight, highlight, highlightReset, currentViewport, formatLabels, skipId = 'legend' }, ref) => {
|
|
28
29
|
const { innerClasses, containerClasses } = useLegendClasses(config)
|
|
29
|
-
const { runtime,
|
|
30
|
+
const { runtime, legend } = config
|
|
31
|
+
|
|
30
32
|
if (!legend) return null
|
|
31
|
-
const isBottomOrSmallViewport = legend
|
|
33
|
+
const isBottomOrSmallViewport = legend?.position === 'bottom' || (['sm', 'xs', 'xxs'].includes(currentViewport) && !legend.hide)
|
|
32
34
|
|
|
33
35
|
const legendClasses = {
|
|
34
36
|
marginBottom: isBottomOrSmallViewport ? '15px' : '0px',
|
|
35
|
-
marginTop: isBottomOrSmallViewport && orientation === 'horizontal' ? `${config.yAxis.label && config.isResponsiveTicks ? config.dynamicMarginTop : config.runtime.xAxis.size}px` :
|
|
37
|
+
marginTop: isBottomOrSmallViewport && config.orientation === 'horizontal' ? `${config.yAxis.label && config.isResponsiveTicks ? config.dynamicMarginTop : config.runtime.xAxis.size}px` : getMarginTop(isBottomOrSmallViewport, config.brush.active)
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
const { HighLightedBarUtils } = useHighlightedBars(config)
|
|
39
41
|
|
|
40
42
|
let highLightedLegendItems = HighLightedBarUtils.findDuplicates(config.highlightedBarValues)
|
|
41
|
-
const fontSize = ['sm', 'xs', 'xxs'].includes(currentViewport) ? { fontSize: '11px' } : null
|
|
42
43
|
|
|
43
44
|
return (
|
|
44
45
|
<aside ref={ref} style={legendClasses} id={skipId || 'legend'} className={containerClasses.join(' ')} role='region' aria-label='legend' tabIndex={0}>
|
|
45
46
|
{legend.label && <h3>{parse(legend.label)}</h3>}
|
|
46
|
-
{legend.description && <p
|
|
47
|
+
{legend.description && <p>{parse(legend.description)}</p>}
|
|
47
48
|
|
|
48
49
|
<LegendOrdinal scale={colorScale} itemDirection='row' labelMargin='0 20px 0 0' shapeMargin='0 10px 0'>
|
|
49
50
|
{labels => {
|
|
@@ -101,7 +102,7 @@ const Legend: React.FC<LegendProps> = forwardRef(({ config, colorScale, seriesHi
|
|
|
101
102
|
)}
|
|
102
103
|
</div>
|
|
103
104
|
|
|
104
|
-
<LegendLabel
|
|
105
|
+
<LegendLabel align='left' margin='0 0 0 4px'>
|
|
105
106
|
{label.text}
|
|
106
107
|
</LegendLabel>
|
|
107
108
|
</LegendItem>
|
|
@@ -143,17 +144,17 @@ const Legend: React.FC<LegendProps> = forwardRef(({ config, colorScale, seriesHi
|
|
|
143
144
|
</div>
|
|
144
145
|
|
|
145
146
|
<>
|
|
146
|
-
{config?.preliminaryData?.some(pd => pd.label) && ['Line', 'Combo'].includes(config.visualizationType) && (
|
|
147
|
+
{config?.preliminaryData?.some(pd => pd.label && pd.type === 'effect' && pd.style === 'Open Circles') && ['Line', 'Combo'].includes(config.visualizationType) && (
|
|
147
148
|
<>
|
|
148
149
|
<hr></hr>
|
|
149
150
|
<div className={config.legend.singleRow && isBottomOrSmallViewport ? 'legend-container__inner bottom single-row' : ''}>
|
|
150
151
|
{config?.preliminaryData?.map((pd, index) => {
|
|
151
152
|
return (
|
|
152
153
|
<>
|
|
153
|
-
{pd.label && (
|
|
154
|
+
{pd.label && pd.type === 'effect' && pd.style && (
|
|
154
155
|
<div key={index} className='legend-preliminary'>
|
|
155
|
-
<
|
|
156
|
-
<
|
|
156
|
+
<span className={pd.symbol}>{pd.lineCode}</span>
|
|
157
|
+
<p> {pd.label}</p>
|
|
157
158
|
</div>
|
|
158
159
|
)}
|
|
159
160
|
</>
|
|
@@ -162,6 +163,56 @@ const Legend: React.FC<LegendProps> = forwardRef(({ config, colorScale, seriesHi
|
|
|
162
163
|
</div>
|
|
163
164
|
</>
|
|
164
165
|
)}
|
|
166
|
+
{!config.legend.hideSuppressedLabels &&
|
|
167
|
+
config?.preliminaryData?.some(pd => pd.label && pd.displayLegend && pd.type === 'suppression' && pd.value && (pd?.style || pd.symbol)) &&
|
|
168
|
+
((config.visualizationType === 'Bar' && config.visualizationSubType === 'regular') || config.visualizationType === 'Line' || config.visualizationType === 'Combo') && (
|
|
169
|
+
<>
|
|
170
|
+
<hr></hr>
|
|
171
|
+
<div className={config.legend.singleRow && isBottomOrSmallViewport ? 'legend-container__inner bottom single-row' : ''}>
|
|
172
|
+
{config?.preliminaryData?.map(
|
|
173
|
+
(pd, index) =>
|
|
174
|
+
pd.displayLegend &&
|
|
175
|
+
pd.type === 'suppression' && (
|
|
176
|
+
<>
|
|
177
|
+
{config.visualizationType === 'Bar' && (
|
|
178
|
+
<>
|
|
179
|
+
<div key={index + 'Bar'} className={`legend-preliminary ${pd.symbol}`}>
|
|
180
|
+
<span className={pd.symbol}>{pd.iconCode}</span>
|
|
181
|
+
<p className={pd.type}>{pd.label}</p>
|
|
182
|
+
</div>
|
|
183
|
+
</>
|
|
184
|
+
)}
|
|
185
|
+
{config.visualizationType === 'Line' && (
|
|
186
|
+
<>
|
|
187
|
+
<div key={index + 'Line'} className={`legend-preliminary `}>
|
|
188
|
+
<span>{pd.lineCode}</span>
|
|
189
|
+
<p className={pd.type}>{pd.label}</p>
|
|
190
|
+
</div>
|
|
191
|
+
</>
|
|
192
|
+
)}
|
|
193
|
+
{config.visualizationType === 'Combo' && (
|
|
194
|
+
<>
|
|
195
|
+
{pd.symbol && pd.iconCode && (
|
|
196
|
+
<div key={index + 'Combo'} className={`legend-preliminary ${pd.symbol}`}>
|
|
197
|
+
<span className={pd.symbol}>{pd.iconCode}</span>
|
|
198
|
+
<p className={pd.type}>{pd.label}</p>
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
|
|
202
|
+
{pd.style && pd.lineCode && (
|
|
203
|
+
<div key={index + 'Combo'} className='legend-preliminary'>
|
|
204
|
+
<span>{pd.lineCode}</span>
|
|
205
|
+
<p>{pd.label}</p>
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
</>
|
|
209
|
+
)}
|
|
210
|
+
</>
|
|
211
|
+
)
|
|
212
|
+
)}
|
|
213
|
+
</div>
|
|
214
|
+
</>
|
|
215
|
+
)}
|
|
165
216
|
</>
|
|
166
217
|
</>
|
|
167
218
|
)
|
|
@@ -9,7 +9,7 @@ export const createFormatLabels =
|
|
|
9
9
|
(defaultLabels: Label[]): Label[] => {
|
|
10
10
|
const { visualizationType, visualizationSubType, series, runtime } = config
|
|
11
11
|
|
|
12
|
-
const reverseLabels = labels => (config.legend.reverseLabelOrder && config.legend
|
|
12
|
+
const reverseLabels = labels => (config.legend.reverseLabelOrder && config.legend?.position === 'bottom' ? labels.reverse() : labels)
|
|
13
13
|
const colorCode = config.legend?.colorCode
|
|
14
14
|
if (visualizationType === 'Deviation Bar') {
|
|
15
15
|
const [belowColor, aboveColor] = twoColorPalette[config.twoColor.palette]
|
|
@@ -17,12 +17,21 @@ export type LineChartProps = {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export interface PreliminaryDataItem {
|
|
20
|
-
style: string
|
|
21
|
-
type: string
|
|
22
20
|
column: string
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
displayLegend: boolean
|
|
22
|
+
displayTable: boolean
|
|
23
|
+
displayTooltip: boolean
|
|
24
|
+
iconCode: string
|
|
25
25
|
label: string
|
|
26
|
+
lineCode: string
|
|
27
|
+
seriesKey: string
|
|
28
|
+
style: string
|
|
29
|
+
symbol: string
|
|
30
|
+
type: 'effect' | 'suppression'
|
|
31
|
+
value: string
|
|
32
|
+
hideBarSymbol: boolean
|
|
33
|
+
hideLineStyle: boolean
|
|
34
|
+
circleSize: number
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
export interface DataItem {
|
|
@@ -33,12 +42,13 @@ export interface Config {
|
|
|
33
42
|
preliminaryData: PreliminaryDataItem[] | []
|
|
34
43
|
}
|
|
35
44
|
export interface StyleProps {
|
|
36
|
-
preliminaryData: PreliminaryDataItem[]
|
|
37
45
|
data: DataItem[]
|
|
38
|
-
stroke: string
|
|
39
46
|
handleLineType: Function
|
|
40
47
|
lineType: string
|
|
48
|
+
preliminaryData: PreliminaryDataItem[]
|
|
41
49
|
seriesKey: 'string'
|
|
50
|
+
stroke: string
|
|
51
|
+
strokeWidth: number
|
|
42
52
|
}
|
|
43
53
|
export interface Style {
|
|
44
54
|
stroke: string
|
|
@@ -7,6 +7,7 @@ type LineChartCircleProps = {
|
|
|
7
7
|
circleData: object[]
|
|
8
8
|
config: ChartConfig
|
|
9
9
|
data: object[]
|
|
10
|
+
tableData: object[]
|
|
10
11
|
d?: Object
|
|
11
12
|
displayArea: boolean
|
|
12
13
|
seriesKey: string
|
|
@@ -26,7 +27,7 @@ type LineChartCircleProps = {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
const LineChartCircle = (props: LineChartCircleProps) => {
|
|
29
|
-
const { config, d, displayArea, seriesKey, tooltipData, xScale, yScale, colorScale, parseDate, yScaleRight, data, circleData, dataIndex, mode } = props
|
|
30
|
+
const { config, d, tableData, displayArea, seriesKey, tooltipData, xScale, yScale, colorScale, parseDate, yScaleRight, data, circleData, dataIndex, mode } = props
|
|
30
31
|
const { lineDatapointStyle } = config
|
|
31
32
|
const filtered = config?.series.filter(s => s.dataKey === seriesKey)?.[0]
|
|
32
33
|
// If we're not showing the circle, simply return
|
|
@@ -86,7 +87,7 @@ const LineChartCircle = (props: LineChartCircleProps) => {
|
|
|
86
87
|
let hoveredSeriesAxis = hoveredSeriesData?.[0]?.[2]
|
|
87
88
|
if (!hoveredSeriesKey) return
|
|
88
89
|
hoveredSeriesIndex = tooltipData?.data.indexOf(hoveredSeriesKey)
|
|
89
|
-
hoveredSeriesValue =
|
|
90
|
+
hoveredSeriesValue = tableData?.find(d => {
|
|
90
91
|
return d[config?.xAxis.dataKey] === hoveredXValue
|
|
91
92
|
})?.[seriesKey]
|
|
92
93
|
|
|
@@ -100,6 +101,7 @@ const LineChartCircle = (props: LineChartCircleProps) => {
|
|
|
100
101
|
if (isMatch) {
|
|
101
102
|
return <></>
|
|
102
103
|
}
|
|
104
|
+
|
|
103
105
|
return (
|
|
104
106
|
<circle
|
|
105
107
|
cx={getXPos(hoveredXValue)}
|
|
@@ -119,23 +121,32 @@ const LineChartCircle = (props: LineChartCircleProps) => {
|
|
|
119
121
|
if (mode === 'ISOLATED_POINTS') {
|
|
120
122
|
const drawIsolatedPoints = (currentIndex, seriesKey) => {
|
|
121
123
|
const currentPoint = data[currentIndex]
|
|
122
|
-
const previousPoint = data[currentIndex - 1]
|
|
123
|
-
const nextPoint = data[currentIndex + 1]
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
const previousPoint = currentIndex > 0 ? data[currentIndex - 1] : null
|
|
125
|
+
const nextPoint = currentIndex < data.length - 1 ? data[currentIndex + 1] : null
|
|
126
|
+
let res = false
|
|
127
|
+
|
|
128
|
+
// Handle the first point in the array
|
|
129
|
+
if (currentIndex === 0 && nextPoint && !nextPoint[seriesKey]) {
|
|
130
|
+
res = true
|
|
126
131
|
}
|
|
127
|
-
|
|
128
|
-
|
|
132
|
+
// Handle the last point in the array
|
|
133
|
+
if (currentIndex === data.length - 1 && previousPoint && !previousPoint[seriesKey]) {
|
|
134
|
+
res = true
|
|
129
135
|
}
|
|
130
|
-
|
|
131
|
-
|
|
136
|
+
// Handle points in the middle
|
|
137
|
+
if (currentIndex > 0 && currentIndex < data.length - 1) {
|
|
138
|
+
if (currentPoint && currentPoint[seriesKey] && (!previousPoint || !previousPoint[seriesKey]) && (!nextPoint || !nextPoint[seriesKey])) {
|
|
139
|
+
res = true
|
|
140
|
+
}
|
|
132
141
|
}
|
|
142
|
+
|
|
143
|
+
return res
|
|
133
144
|
}
|
|
134
145
|
|
|
135
146
|
if (mode) {
|
|
136
147
|
if (drawIsolatedPoints(dataIndex, seriesKey)) {
|
|
137
148
|
return (
|
|
138
|
-
<circle cx={getXPos(d[config.xAxis
|
|
149
|
+
<circle cx={getXPos(d[config.xAxis?.dataKey])} cy={filtered.axis === 'Right' ? yScaleRight(d[filtered.dataKey]) : yScale(d[filtered?.dataKey])} r={5.3} strokeWidth={2} stroke={colorScale(config.runtime.seriesLabels[seriesKey])} fill={colorScale(config.runtime?.seriesLabels[seriesKey])} />
|
|
139
150
|
)
|
|
140
151
|
}
|
|
141
152
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { type PreliminaryDataItem, DataItem, StyleProps, Style } from './LineChartProps'
|
|
2
|
-
|
|
2
|
+
import _ from 'lodash'
|
|
3
3
|
export const createStyles = (props: StyleProps): Style[] => {
|
|
4
4
|
const { preliminaryData, data, stroke, strokeWidth, handleLineType, lineType, seriesKey } = props
|
|
5
5
|
|
|
6
|
-
const validPreliminaryData: PreliminaryDataItem[] = preliminaryData.filter(pd => pd.seriesKey && pd.column && pd.value && pd.type && pd.style)
|
|
6
|
+
const validPreliminaryData: PreliminaryDataItem[] = preliminaryData.filter(pd => pd.seriesKey && pd.column && pd.value && pd.type && pd.style && pd.type === 'effect')
|
|
7
7
|
const getMatchingPd = (point: DataItem): PreliminaryDataItem => validPreliminaryData.find(pd => pd.seriesKey === seriesKey && point[pd.column] === pd.value && pd.type === 'effect' && pd.style !== 'Open Circles')
|
|
8
8
|
|
|
9
9
|
let styles: Style[] = []
|
|
@@ -29,17 +29,155 @@ export const createStyles = (props: StyleProps): Style[] => {
|
|
|
29
29
|
|
|
30
30
|
export const filterCircles = (preliminaryData: PreliminaryDataItem[], data: DataItem[], seriesKey: string): DataItem[] => {
|
|
31
31
|
// Filter and map preliminaryData to get circlesFiltered
|
|
32
|
-
const circlesFiltered = preliminaryData
|
|
33
|
-
|
|
34
|
-
let filteredData: DataItem[] = []
|
|
35
|
-
|
|
32
|
+
const circlesFiltered = preliminaryData?.filter(item => item.style.includes('Circles') && item.type === 'effect').map(item => ({ column: item.column, value: item.value, seriesKey: item.seriesKey, circleSize: item.circleSize, style: item.style }))
|
|
33
|
+
const filteredData = []
|
|
36
34
|
// Process data to find matching items
|
|
37
35
|
data.forEach(item => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
circlesFiltered.forEach(fc => {
|
|
37
|
+
if (item[fc.column] === fc.value && fc.seriesKey === seriesKey && item[seriesKey] && fc.style === 'Open Circles') {
|
|
38
|
+
const result = {
|
|
39
|
+
data: item,
|
|
40
|
+
size: fc.circleSize,
|
|
41
|
+
isFilled: false
|
|
42
|
+
}
|
|
43
|
+
filteredData.push(result)
|
|
44
|
+
}
|
|
45
|
+
if ((!fc.value || item[fc.column] === fc.value) && fc.seriesKey === seriesKey && item[seriesKey] && fc.style === 'Filled Circles') {
|
|
46
|
+
const result = {
|
|
47
|
+
data: item,
|
|
48
|
+
size: fc.circleSize,
|
|
49
|
+
isFilled: true
|
|
50
|
+
}
|
|
51
|
+
filteredData.push(result)
|
|
52
|
+
}
|
|
53
|
+
})
|
|
42
54
|
})
|
|
43
55
|
|
|
44
56
|
return filteredData
|
|
45
57
|
}
|
|
58
|
+
|
|
59
|
+
const isCalculable = value => !isNaN(parseFloat(value)) && isFinite(value)
|
|
60
|
+
const handleFirstIndex = (data, seriesKey, preliminaryData) => {
|
|
61
|
+
const result = {
|
|
62
|
+
data: [],
|
|
63
|
+
style: ''
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// If data is empty, return the empty result
|
|
67
|
+
if (!data.length) return result
|
|
68
|
+
|
|
69
|
+
const firstIndexDataItem = data[0]
|
|
70
|
+
|
|
71
|
+
// Function to check if a data item matches the suppression criteria
|
|
72
|
+
const isSuppressed = pd => {
|
|
73
|
+
if (pd.type === 'effect' || pd.hideLineStyle) return
|
|
74
|
+
return pd.type == 'suppression' && pd.value === firstIndexDataItem[seriesKey] && (!pd.column || pd.column === seriesKey)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Find applicable suppression data for the first item
|
|
78
|
+
const suppressionData = preliminaryData.find(isSuppressed)
|
|
79
|
+
|
|
80
|
+
if (suppressionData && suppressionData.style) {
|
|
81
|
+
// Modify first item and add to result
|
|
82
|
+
const modifiedItem = { ...firstIndexDataItem, [seriesKey]: 0 }
|
|
83
|
+
result.data.push(modifiedItem)
|
|
84
|
+
result.style = suppressionData.style
|
|
85
|
+
|
|
86
|
+
// Find the next calculable item index
|
|
87
|
+
let nextIndex = 1
|
|
88
|
+
while (nextIndex < data.length && !isCalculable(data[nextIndex][seriesKey])) {
|
|
89
|
+
nextIndex++
|
|
90
|
+
}
|
|
91
|
+
if (nextIndex < data.length) {
|
|
92
|
+
result.data.push(data[nextIndex])
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
// If no suppression, just add the first item
|
|
96
|
+
result.data.push(firstIndexDataItem)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return result
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const handleLastIndex = (data, seriesKey, preliminaryData) => {
|
|
103
|
+
const result = {
|
|
104
|
+
data: [],
|
|
105
|
+
style: ''
|
|
106
|
+
}
|
|
107
|
+
let lastAddedIndex = -1 // Tracks the last index added to the result
|
|
108
|
+
preliminaryData?.forEach(pd => {
|
|
109
|
+
if (pd.type === 'effect') return
|
|
110
|
+
if (data[data.length - 1][seriesKey] === pd.value && pd.style && (!pd.column || pd.column === seriesKey) && pd.type == 'suppression' && !pd.hideLineStyle) {
|
|
111
|
+
const lastIndex = data.length - 1
|
|
112
|
+
const modifiedItem = { ...data[lastIndex], [seriesKey]: 0 }
|
|
113
|
+
result.data.push(modifiedItem)
|
|
114
|
+
|
|
115
|
+
// Find previous calculable item
|
|
116
|
+
let prevIndex = lastIndex - 1
|
|
117
|
+
while (prevIndex >= 0 && !isCalculable(data[prevIndex][seriesKey])) {
|
|
118
|
+
prevIndex--
|
|
119
|
+
}
|
|
120
|
+
if (prevIndex >= 0 && lastAddedIndex !== prevIndex) {
|
|
121
|
+
result.data.push(data[prevIndex])
|
|
122
|
+
lastAddedIndex = prevIndex
|
|
123
|
+
}
|
|
124
|
+
result.style = pd.style
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
return result
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function handleMiddleIndices(data, seriesKey, dataKey, preliminaryData) {
|
|
132
|
+
const result = {
|
|
133
|
+
data: [],
|
|
134
|
+
style: ''
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const isValidMiddleIndex = index => index > 0 && index < data.length - 1
|
|
138
|
+
|
|
139
|
+
preliminaryData?.forEach(pd => {
|
|
140
|
+
if (pd.type === 'effect' || pd.hideLineStyle) return
|
|
141
|
+
const targetValue = pd.value
|
|
142
|
+
|
|
143
|
+
// Find all indices
|
|
144
|
+
const matchingIndices = data.reduce((indices, item, index) => {
|
|
145
|
+
if (item[seriesKey] === targetValue && isValidMiddleIndex(index) && (!pd.column || pd.column === seriesKey)) {
|
|
146
|
+
indices.push(index)
|
|
147
|
+
}
|
|
148
|
+
return indices
|
|
149
|
+
}, [])
|
|
150
|
+
|
|
151
|
+
// Process each valid index
|
|
152
|
+
matchingIndices.forEach(i => {
|
|
153
|
+
result.style = pd.style
|
|
154
|
+
// Add previous object if calculable
|
|
155
|
+
if (isCalculable(data[i - 1][seriesKey])) {
|
|
156
|
+
result.data.push(data[i - 1])
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Find and add the next calculable object
|
|
160
|
+
const nextIndex = data.slice(i + 1).findIndex(item => item[seriesKey] !== targetValue && isCalculable(item[seriesKey]))
|
|
161
|
+
if (nextIndex !== -1) {
|
|
162
|
+
result.data.push(data[i + 1 + nextIndex])
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
// Deduplicate entries
|
|
168
|
+
result.data = _.uniqWith(result.data, (a, b) => a[dataKey] === b[dataKey] && a[seriesKey] === b[seriesKey])
|
|
169
|
+
|
|
170
|
+
return result
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// create segments (array of arrays) for building suppressed Lines
|
|
174
|
+
export const createDataSegments = (data, seriesKey, preliminaryData, dataKey) => {
|
|
175
|
+
// Process the first index if necessary
|
|
176
|
+
const firstSegment = handleFirstIndex(data, seriesKey, preliminaryData)
|
|
177
|
+
// Process the last index if necessary
|
|
178
|
+
const lastSegment = handleLastIndex(data, seriesKey, preliminaryData)
|
|
179
|
+
// Process the middle segment
|
|
180
|
+
const middleSegments = handleMiddleIndices(data, seriesKey, dataKey, preliminaryData)
|
|
181
|
+
// Combine all segments into a single array
|
|
182
|
+
return [firstSegment, middleSegments, lastSegment].filter(segment => segment.data.length > 0 && segment.style !== '')
|
|
183
|
+
}
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import React, { useContext } from 'react'
|
|
2
2
|
|
|
3
|
+
// VisX library imports
|
|
3
4
|
import * as allCurves from '@visx/curve'
|
|
4
5
|
import { Group } from '@visx/group'
|
|
5
6
|
import { LinePath, Bar, SplitLinePath } from '@visx/shape'
|
|
6
7
|
import { Text } from '@visx/text'
|
|
7
8
|
|
|
9
|
+
// CDC core components
|
|
8
10
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
11
|
+
|
|
12
|
+
// Local context and hooks
|
|
9
13
|
import ConfigContext from '../../ConfigContext'
|
|
10
14
|
import useRightAxis from '../../hooks/useRightAxis'
|
|
11
|
-
|
|
15
|
+
|
|
16
|
+
// Local helpers and components
|
|
17
|
+
import { filterCircles, createStyles, createDataSegments } from './helpers'
|
|
12
18
|
import LineChartCircle from './components/LineChart.Circle'
|
|
13
19
|
|
|
14
|
-
//
|
|
20
|
+
// Types
|
|
15
21
|
import { type ChartContext } from '../../types/ChartContext'
|
|
16
22
|
import { type LineChartProps } from './LineChartProps'
|
|
17
23
|
|
|
@@ -31,28 +37,22 @@ const LineChart = (props: LineChartProps) => {
|
|
|
31
37
|
} = props
|
|
32
38
|
|
|
33
39
|
// prettier-ignore
|
|
34
|
-
const {
|
|
35
|
-
|
|
36
|
-
config,
|
|
37
|
-
formatNumber,
|
|
38
|
-
handleLineType,
|
|
39
|
-
isNumber,
|
|
40
|
-
parseDate,
|
|
41
|
-
seriesHighlight,
|
|
42
|
-
tableData,
|
|
43
|
-
transformedData: data,
|
|
44
|
-
updateConfig,
|
|
45
|
-
rawData
|
|
46
|
-
} = useContext<ChartContext>(ConfigContext)
|
|
47
|
-
const { yScaleRight } = useRightAxis({ config, yMax, data, updateConfig })
|
|
40
|
+
const { colorScale, config, formatNumber, handleLineType, isNumber, parseDate, seriesHighlight, tableData, transformedData, updateConfig, brushConfig,clean } = useContext<ChartContext>(ConfigContext)
|
|
41
|
+
const { yScaleRight } = useRightAxis({ config, yMax, data: transformedData, updateConfig })
|
|
48
42
|
if (!handleTooltipMouseOver) return
|
|
49
43
|
|
|
50
44
|
const DEBUG = false
|
|
51
45
|
const { lineDatapointStyle, showLineSeriesLabels, legend } = config
|
|
52
|
-
|
|
46
|
+
let data = transformedData
|
|
47
|
+
let tableD = tableData
|
|
48
|
+
// if brush on use brush data and clean
|
|
49
|
+
if (brushConfig.data.length > 0 && config.brush?.active) {
|
|
50
|
+
data = clean(brushConfig.data)
|
|
51
|
+
tableD = clean(brushConfig.data)
|
|
52
|
+
}
|
|
53
53
|
return (
|
|
54
54
|
<ErrorBoundary component='LineChart'>
|
|
55
|
-
<Group left={config.runtime.yAxis.size}>
|
|
55
|
+
<Group left={Number(config.runtime.yAxis.size)}>
|
|
56
56
|
{' '}
|
|
57
57
|
{/* left - expects a number not a string */}
|
|
58
58
|
{(config.runtime.lineSeriesKeys || config.runtime.seriesKeys).map((seriesKey, index) => {
|
|
@@ -60,9 +60,10 @@ const LineChart = (props: LineChartProps) => {
|
|
|
60
60
|
const seriesData = config.series.filter(item => item.dataKey === seriesKey)
|
|
61
61
|
const seriesAxis = seriesData[0].axis ? seriesData[0].axis : 'left'
|
|
62
62
|
let displayArea = legend.behavior === 'highlight' || seriesHighlight.length === 0 || seriesHighlight.indexOf(seriesKey) !== -1
|
|
63
|
-
const circleData = filterCircles(config
|
|
63
|
+
const circleData = filterCircles(config?.preliminaryData, tableD, seriesKey)
|
|
64
64
|
// styles for preliminary Data items
|
|
65
|
-
let styles = createStyles({ preliminaryData: config.preliminaryData, data:
|
|
65
|
+
let styles = createStyles({ preliminaryData: config.preliminaryData, data: tableD, stroke: colorScale(config.runtime.seriesLabels[seriesKey]), strokeWidth: seriesData[0].weight || 2, handleLineType, lineType, seriesKey })
|
|
66
|
+
const suppressedSegments = createDataSegments(tableData, seriesKey, config.preliminaryData, config.xAxis.dataKey)
|
|
66
67
|
|
|
67
68
|
let xPos = d => {
|
|
68
69
|
return xScale(getXAxisData(d)) + (xScale.bandwidth ? xScale.bandwidth() / 2 : 0)
|
|
@@ -82,6 +83,7 @@ const LineChart = (props: LineChartProps) => {
|
|
|
82
83
|
const hasMultipleSeries = Object.keys(config.runtime.seriesLabels).length > 1
|
|
83
84
|
const labeltype = axis === 'Right' ? 'rightLabel' : 'label'
|
|
84
85
|
let label = config.runtime.yAxis[labeltype]
|
|
86
|
+
|
|
85
87
|
// if has muiltiple series dont show legend value on tooltip
|
|
86
88
|
if (!hasMultipleSeries) label = config.isLegendValue ? config.runtime.seriesLabels[seriesKey] : label
|
|
87
89
|
|
|
@@ -104,6 +106,7 @@ const LineChart = (props: LineChartProps) => {
|
|
|
104
106
|
mode='ALWAYS_SHOW_POINTS'
|
|
105
107
|
dataIndex={dataIndex}
|
|
106
108
|
circleData={circleData}
|
|
109
|
+
tableData={tableData}
|
|
107
110
|
data={data}
|
|
108
111
|
d={d}
|
|
109
112
|
config={config}
|
|
@@ -123,6 +126,7 @@ const LineChart = (props: LineChartProps) => {
|
|
|
123
126
|
<LineChartCircle
|
|
124
127
|
mode='ISOLATED_POINTS'
|
|
125
128
|
dataIndex={dataIndex}
|
|
129
|
+
tableData={tableData}
|
|
126
130
|
circleData={circleData}
|
|
127
131
|
data={data}
|
|
128
132
|
d={d}
|
|
@@ -145,6 +149,7 @@ const LineChart = (props: LineChartProps) => {
|
|
|
145
149
|
<>
|
|
146
150
|
{lineDatapointStyle === 'hover' && (
|
|
147
151
|
<LineChartCircle
|
|
152
|
+
tableData={tableData}
|
|
148
153
|
dataIndex={0}
|
|
149
154
|
mode='HOVER_POINTS'
|
|
150
155
|
circleData={circleData}
|
|
@@ -163,34 +168,46 @@ const LineChart = (props: LineChartProps) => {
|
|
|
163
168
|
)}
|
|
164
169
|
</>
|
|
165
170
|
{/* SPLIT LINE */}
|
|
166
|
-
{config?.preliminaryData?.some(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
171
|
+
{config?.preliminaryData?.some(pd => pd.value && pd.type) ? (
|
|
172
|
+
<>
|
|
173
|
+
<SplitLinePath
|
|
174
|
+
curve={allCurves[seriesData[0].lineType]}
|
|
175
|
+
segments={data.map(d => [d])}
|
|
176
|
+
segmentation='x'
|
|
177
|
+
x={d => xPos(d)}
|
|
178
|
+
y={d => (seriesAxis === 'Right' ? yScaleRight(getYAxisData(d, seriesKey)) : yScale(Number(getYAxisData(d, seriesKey))))}
|
|
179
|
+
styles={styles}
|
|
180
|
+
defined={(item, i) => {
|
|
181
|
+
return item[seriesKey] !== '' && item[seriesKey] !== null && item[seriesKey] !== undefined
|
|
182
|
+
}}
|
|
183
|
+
/>
|
|
184
|
+
|
|
185
|
+
{suppressedSegments.map((segment, index) => {
|
|
186
|
+
return (
|
|
187
|
+
<LinePath
|
|
188
|
+
key={index}
|
|
189
|
+
data={segment.data}
|
|
190
|
+
x={d => xPos(d)}
|
|
191
|
+
y={d => (seriesAxis === 'Right' ? yScaleRight(getYAxisData(d, seriesKey)) : yScale(Number(getYAxisData(d, seriesKey))))}
|
|
192
|
+
stroke={colorScale(config.runtime.seriesLabels[seriesKey])}
|
|
193
|
+
strokeWidth={seriesData[0].weight || 2}
|
|
194
|
+
strokeOpacity={1}
|
|
195
|
+
shapeRendering='geometricPrecision'
|
|
196
|
+
strokeDasharray={handleLineType(segment.style)}
|
|
197
|
+
defined={(item, i) => {
|
|
198
|
+
return item[seriesKey] !== '' && item[seriesKey] !== null && item[seriesKey] !== undefined
|
|
199
|
+
}}
|
|
200
|
+
/>
|
|
201
|
+
)
|
|
202
|
+
})}
|
|
203
|
+
</>
|
|
187
204
|
) : (
|
|
188
205
|
<>
|
|
189
206
|
{/* STANDARD LINE */}
|
|
190
207
|
<LinePath
|
|
191
208
|
curve={allCurves[seriesData[0].lineType]}
|
|
192
209
|
data={
|
|
193
|
-
config.xAxis.type === 'date-time'
|
|
210
|
+
config.xAxis.type === 'date-time' || config.xAxis.type === 'date'
|
|
194
211
|
? data.sort((d1, d2) => {
|
|
195
212
|
let x1 = getXAxisData(d1)
|
|
196
213
|
let x2 = getXAxisData(d2)
|
|
@@ -215,8 +232,18 @@ const LineChart = (props: LineChartProps) => {
|
|
|
215
232
|
)}
|
|
216
233
|
|
|
217
234
|
{/* circles for preliminaryData data */}
|
|
218
|
-
{circleData.map((
|
|
219
|
-
return
|
|
235
|
+
{circleData.map((item, i) => {
|
|
236
|
+
return (
|
|
237
|
+
<circle
|
|
238
|
+
key={i}
|
|
239
|
+
cx={xPos(item.data)}
|
|
240
|
+
cy={seriesAxis === 'Right' ? yScaleRight(getYAxisData(item.data, seriesKey)) : yScale(Number(getYAxisData(item.data, seriesKey)))}
|
|
241
|
+
r={item.size}
|
|
242
|
+
strokeWidth={seriesData[0].weight || 2}
|
|
243
|
+
stroke={colorScale ? colorScale(config.runtime.seriesLabels[seriesKey]) : '#000'}
|
|
244
|
+
fill={item.isFilled ? (colorScale ? colorScale(config.runtime.seriesLabels[seriesKey]) : '#000') : '#fff'}
|
|
245
|
+
/>
|
|
246
|
+
)
|
|
220
247
|
})}
|
|
221
248
|
|
|
222
249
|
{/* ANIMATED LINE */}
|