@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.
Files changed (76) hide show
  1. package/dist/cdcchart.js +39611 -36038
  2. package/examples/feature/annotations/index.json +542 -0
  3. package/examples/xaxis.json +493 -0
  4. package/index.html +9 -8
  5. package/package.json +5 -4
  6. package/src/CdcChart.tsx +115 -71
  7. package/src/_stories/Chart.stories.tsx +26 -171
  8. package/src/_stories/ChartAnnotation.stories.tsx +32 -0
  9. package/src/_stories/_mock/annotation_category_mock.json +473 -0
  10. package/src/_stories/_mock/annotation_date-linear_mock.json +530 -0
  11. package/src/_stories/_mock/annotation_date-time_mock.json +530 -0
  12. package/src/_stories/_mock/bar-chart-suppressed.json +474 -0
  13. package/src/_stories/_mock/line_chart_two_points_new_chart.json +128 -0
  14. package/src/_stories/_mock/line_chart_two_points_regression_test.json +127 -0
  15. package/src/_stories/_mock/lollipop.json +171 -0
  16. package/src/components/Annotations/components/AnnotationDraggable.styles.css +31 -0
  17. package/src/components/Annotations/components/AnnotationDraggable.tsx +154 -0
  18. package/src/components/Annotations/components/AnnotationDropdown.styles.css +14 -0
  19. package/src/components/Annotations/components/AnnotationDropdown.tsx +72 -0
  20. package/src/components/Annotations/components/AnnotationList.styles.css +45 -0
  21. package/src/components/Annotations/components/AnnotationList.tsx +42 -0
  22. package/src/components/Annotations/components/findNearestDatum.ts +138 -0
  23. package/src/components/Annotations/components/helpers/index.tsx +46 -0
  24. package/src/components/Annotations/index.tsx +13 -0
  25. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +1 -1
  26. package/src/components/AreaChart/components/AreaChart.jsx +2 -2
  27. package/src/components/BarChart/components/BarChart.Horizontal.tsx +78 -71
  28. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +1 -2
  29. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +11 -11
  30. package/src/components/BarChart/components/BarChart.Vertical.tsx +100 -87
  31. package/src/components/BarChart/helpers/index.ts +102 -0
  32. package/src/components/DeviationBar.jsx +4 -2
  33. package/src/components/EditorPanel/EditorPanel.tsx +435 -613
  34. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +306 -0
  35. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +135 -7
  36. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +2 -3
  37. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +4 -5
  38. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +3 -2
  39. package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
  40. package/src/components/EditorPanel/components/panels.scss +4 -0
  41. package/src/components/EditorPanel/editor-panel.scss +19 -0
  42. package/src/components/EditorPanel/useEditorPermissions.js +23 -3
  43. package/src/components/Legend/Legend.Component.tsx +66 -15
  44. package/src/components/Legend/helpers/createFormatLabels.tsx +1 -1
  45. package/src/components/Legend/helpers/index.ts +5 -0
  46. package/src/components/LineChart/LineChartProps.ts +16 -6
  47. package/src/components/LineChart/components/LineChart.Circle.tsx +22 -11
  48. package/src/components/LineChart/helpers.ts +148 -10
  49. package/src/components/LineChart/index.tsx +71 -44
  50. package/src/components/LinearChart.jsx +184 -125
  51. package/src/components/PairedBarChart.jsx +9 -9
  52. package/src/components/PieChart/PieChart.tsx +4 -4
  53. package/src/components/Sankey/index.tsx +73 -20
  54. package/src/components/ScatterPlot/ScatterPlot.jsx +22 -8
  55. package/src/components/ZoomBrush.tsx +120 -55
  56. package/src/data/initial-state.js +14 -6
  57. package/src/helpers/handleChartTabbing.ts +8 -0
  58. package/src/helpers/isConvertLineToBarGraph.ts +4 -0
  59. package/src/hooks/{useBarChart.js → useBarChart.ts} +9 -22
  60. package/src/hooks/useColorScale.ts +1 -1
  61. package/src/hooks/useMinMax.ts +29 -5
  62. package/src/hooks/useScales.ts +48 -26
  63. package/src/hooks/useTooltip.tsx +62 -15
  64. package/src/scss/main.scss +69 -12
  65. package/src/types/ChartConfig.ts +53 -16
  66. package/src/types/ChartContext.ts +13 -0
  67. package/tests-examples/helpers/testZeroValue.test.ts +30 -0
  68. package/LICENSE +0 -201
  69. package/src/_stories/ChartLine.preliminary.tsx +0 -19
  70. package/src/_stories/ChartSuppress.stories.tsx +0 -19
  71. package/src/_stories/_mock/suppress_mock.json +0 -911
  72. package/src/helpers/computeMarginBottom.ts +0 -56
  73. package/src/helpers/filterData.ts +0 -18
  74. package/src/helpers/tests/computeMarginBottom.test.ts +0 -21
  75. /package/src/hooks/{useLegendClasses.js → useLegendClasses.ts} +0 -0
  76. /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
- seriesHighlight: string[]
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, orientation, legend } = config
30
+ const { runtime, legend } = config
31
+
30
32
  if (!legend) return null
31
- const isBottomOrSmallViewport = legend.position === 'bottom' || ['sm', 'xs', 'xxs'].includes(currentViewport)
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` : `${isBottomOrSmallViewport ? config.dynamicMarginTop + 15 : 0}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 style={fontSize}>{parse(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 style={fontSize} align='left' margin='0 0 0 4px'>
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
- <svg>{pd.style.includes('Dashed') ? <Line from={{ x: 10, y: 10 }} to={{ x: 40, y: 10 }} stroke={'#000'} strokeWidth={2} strokeDasharray={handleLineType(pd.style)} /> : <circle r={6} strokeWidth={2} stroke={'#000'} cx={22} cy={10} fill='transparent' />}</svg>
156
- <span> {pd.label}</span>
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.position === 'bottom' ? labels.reverse() : labels)
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]
@@ -0,0 +1,5 @@
1
+ export const getMarginTop = (isBottomOrSmallViewport, isBrushActive) => {
2
+ if (!isBottomOrSmallViewport) return '0px'
3
+ if (isBrushActive) return '35px'
4
+ return '15px'
5
+ }
@@ -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
- value: string
24
- seriesKey: string
21
+ displayLegend: boolean
22
+ displayTable: boolean
23
+ displayTooltip: boolean
24
+ iconCode: string
25
25
  label: string
26
+ lineCode: string
27
+ seriesKey: string
28
+ style: string
29
+ symbol: string
30
+ type: 'effect' | 'suppression'
31
+ value: string
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 = data?.find(d => {
90
+ hoveredSeriesValue = tableData?.find(d => {
90
91
  return d[config?.xAxis.dataKey] === hoveredXValue
91
92
  })?.[seriesKey]
92
93
 
@@ -100,6 +101,7 @@ const LineChartCircle = (props: LineChartCircleProps) => {
100
101
  if (isMatch) {
101
102
  return <></>
102
103
  }
104
+
103
105
  return (
104
106
  <circle
105
107
  cx={getXPos(hoveredXValue)}
@@ -119,23 +121,32 @@ const LineChartCircle = (props: LineChartCircleProps) => {
119
121
  if (mode === 'ISOLATED_POINTS') {
120
122
  const drawIsolatedPoints = (currentIndex, seriesKey) => {
121
123
  const currentPoint = data[currentIndex]
122
- const previousPoint = data[currentIndex - 1]
123
- const nextPoint = data[currentIndex + 1]
124
- if (currentIndex === 0 && !nextPoint[seriesKey]) {
125
- return true
124
+ const previousPoint = currentIndex > 0 ? data[currentIndex - 1] : null
125
+ const nextPoint = currentIndex < data.length - 1 ? data[currentIndex + 1] : null
126
+ let res = false
127
+
128
+ // Handle the first point in the array
129
+ if (currentIndex === 0 && nextPoint && !nextPoint[seriesKey]) {
130
+ res = true
126
131
  }
127
- if (currentIndex === data.length - 1 && !previousPoint[seriesKey]) {
128
- return true
132
+ // Handle the last point in the array
133
+ if (currentIndex === data.length - 1 && previousPoint && !previousPoint[seriesKey]) {
134
+ res = true
129
135
  }
130
- if (currentIndex !== 0 && currentPoint[seriesKey] && !previousPoint[seriesKey] && !nextPoint[seriesKey]) {
131
- return true
136
+ // Handle points in the middle
137
+ if (currentIndex > 0 && currentIndex < data.length - 1) {
138
+ if (currentPoint && currentPoint[seriesKey] && (!previousPoint || !previousPoint[seriesKey]) && (!nextPoint || !nextPoint[seriesKey])) {
139
+ res = true
140
+ }
132
141
  }
142
+
143
+ return res
133
144
  }
134
145
 
135
146
  if (mode) {
136
147
  if (drawIsolatedPoints(dataIndex, seriesKey)) {
137
148
  return (
138
- <circle cx={getXPos(d[config.xAxis.dataKey])} cy={filtered.axis === 'Right' ? yScaleRight(d[filtered.dataKey]) : yScale(d[filtered.dataKey])} r={5.3} strokeWidth={2} stroke={colorScale(config.runtime.seriesLabels[seriesKey])} fill={colorScale(config.runtime.seriesLabels[seriesKey])} />
149
+ <circle cx={getXPos(d[config.xAxis?.dataKey])} cy={filtered.axis === 'Right' ? yScaleRight(d[filtered.dataKey]) : yScale(d[filtered?.dataKey])} r={5.3} strokeWidth={2} stroke={colorScale(config.runtime.seriesLabels[seriesKey])} fill={colorScale(config.runtime?.seriesLabels[seriesKey])} />
139
150
  )
140
151
  }
141
152
  }
@@ -1,9 +1,9 @@
1
1
  import { type PreliminaryDataItem, DataItem, StyleProps, Style } from './LineChartProps'
2
-
2
+ import _ from 'lodash'
3
3
  export const createStyles = (props: StyleProps): Style[] => {
4
4
  const { preliminaryData, data, stroke, strokeWidth, handleLineType, lineType, seriesKey } = props
5
5
 
6
- const validPreliminaryData: PreliminaryDataItem[] = preliminaryData.filter(pd => pd.seriesKey && pd.column && pd.value && pd.type && pd.style)
6
+ const validPreliminaryData: PreliminaryDataItem[] = preliminaryData.filter(pd => pd.seriesKey && pd.column && pd.value && pd.type && pd.style && pd.type === 'effect')
7
7
  const getMatchingPd = (point: DataItem): PreliminaryDataItem => validPreliminaryData.find(pd => pd.seriesKey === seriesKey && point[pd.column] === pd.value && pd.type === 'effect' && pd.style !== 'Open Circles')
8
8
 
9
9
  let styles: Style[] = []
@@ -29,17 +29,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.filter(item => item.style === 'Open Circles' && item.type === 'effect').map(item => ({ column: item.column, value: item.value, seriesKey: item.seriesKey }))
33
-
34
- let filteredData: DataItem[] = []
35
-
32
+ const circlesFiltered = preliminaryData?.filter(item => item.style.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
- if (circlesFiltered.some(d => item[d.column] === d.value && d.seriesKey === seriesKey)) {
39
- // Add current item
40
- filteredData.push(item)
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
- import { filterCircles, createStyles } from './helpers'
15
+
16
+ // Local helpers and components
17
+ import { filterCircles, createStyles, createDataSegments } from './helpers'
12
18
  import LineChartCircle from './components/LineChart.Circle'
13
19
 
14
- // types
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
- colorScale,
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.preliminaryData, rawData, seriesKey)
63
+ const circleData = filterCircles(config?.preliminaryData, tableD, seriesKey)
64
64
  // styles for preliminary Data items
65
- let styles = createStyles({ preliminaryData: config.preliminaryData, data: tableData, stroke: colorScale(config.runtime.seriesLabels[seriesKey]), strokeWidth: seriesData[0].weight || 2, handleLineType, lineType, seriesKey })
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(d => d.value && d.column) ? (
167
- <SplitLinePath
168
- curve={allCurves[seriesData[0].lineType]}
169
- segments={(config.xAxis.type === 'date-time'
170
- ? data.sort((d1, d2) => {
171
- let x1 = getXAxisData(d1)
172
- let x2 = getXAxisData(d2)
173
- if (x1 < x2) return -1
174
- if (x2 < x1) return 1
175
- return 0
176
- })
177
- : data
178
- ).map(d => [d])}
179
- segmentation='x'
180
- x={d => xPos(d)}
181
- y={d => (seriesAxis === 'Right' ? yScaleRight(getYAxisData(d, seriesKey)) : yScale(Number(getYAxisData(d, seriesKey))))}
182
- styles={styles}
183
- defined={(item, i) => {
184
- return item[seriesKey] !== '' && item[seriesKey] !== null && item[seriesKey] !== undefined
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((d, i) => {
219
- return <circle key={i} cx={xPos(d)} cy={seriesAxis === 'Right' ? yScaleRight(getYAxisData(d, seriesKey)) : yScale(Number(getYAxisData(d, seriesKey)))} r={6} strokeWidth={seriesData[0].weight || 2} stroke={colorScale ? colorScale(config.runtime.seriesLabels[seriesKey]) : '#000'} fill='#fff' />
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 */}