@cdc/chart 4.24.1 → 4.24.3

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 (82) hide show
  1. package/dist/cdcchart.js +48948 -37923
  2. package/examples/{private/combo.json → chart-regression-1.json} +40 -31
  3. package/examples/chart-regression-2.json +2360 -0
  4. package/examples/feature/filters/url-filter.json +1076 -0
  5. package/examples/feature/line/line-chart-preliminary.json +84 -37
  6. package/examples/feature/line/line-chart.json +2 -1
  7. package/examples/feature/regions/index.json +55 -5
  8. package/examples/feature/sankey/sankey-example-data.json +1364 -0
  9. package/examples/feature/sankey/sankey_chart_data.csv +20 -0
  10. package/examples/gallery/bar-chart-vertical/vertical-bar-chart-stacked.json +306 -19
  11. package/examples/sparkline.json +868 -0
  12. package/index.html +128 -121
  13. package/package.json +4 -2
  14. package/src/CdcChart.tsx +73 -38
  15. package/src/_stories/ChartEditor.stories.tsx +15 -4
  16. package/src/_stories/_mock/pie_config.json +4 -3
  17. package/src/_stories/_mock/url_filter.json +1076 -0
  18. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +2 -1
  19. package/src/components/AreaChart/components/AreaChart.jsx +2 -25
  20. package/src/components/BarChart/components/BarChart.Horizontal.tsx +39 -49
  21. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +36 -56
  22. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +36 -41
  23. package/src/components/BarChart/components/BarChart.Vertical.tsx +48 -64
  24. package/src/components/BoxPlot/BoxPlot.jsx +11 -9
  25. package/src/components/DeviationBar.jsx +3 -3
  26. package/src/components/EditorPanel/EditorPanel.tsx +1717 -1961
  27. package/src/components/EditorPanel/EditorPanelContext.ts +40 -0
  28. package/src/components/EditorPanel/components/Panels/Panel.BoxPlot.tsx +148 -0
  29. package/src/components/EditorPanel/components/{Panel.ForestPlotSettings.tsx → Panels/Panel.ForestPlotSettings.tsx} +16 -7
  30. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +160 -0
  31. package/src/components/EditorPanel/components/{Panel.Regions.tsx → Panels/Panel.Regions.tsx} +6 -6
  32. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +108 -0
  33. package/src/components/EditorPanel/components/{Panel.Series.tsx → Panels/Panel.Series.tsx} +50 -6
  34. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +338 -0
  35. package/src/components/EditorPanel/components/Panels/index.tsx +19 -0
  36. package/src/components/EditorPanel/components/panels.scss +11 -0
  37. package/src/components/EditorPanel/editor-panel.scss +1 -13
  38. package/src/components/EditorPanel/useEditorPermissions.js +44 -13
  39. package/src/components/Legend/Legend.Component.tsx +207 -0
  40. package/src/components/Legend/Legend.tsx +8 -327
  41. package/src/components/Legend/helpers/createFormatLabels.tsx +140 -0
  42. package/src/components/LineChart/LineChartProps.ts +2 -1
  43. package/src/components/LineChart/components/LineChart.Circle.tsx +85 -52
  44. package/src/components/LineChart/helpers.ts +3 -3
  45. package/src/components/LineChart/index.tsx +99 -23
  46. package/src/components/LinearChart.jsx +12 -33
  47. package/src/components/PairedBarChart.jsx +10 -12
  48. package/src/components/PieChart/PieChart.tsx +80 -27
  49. package/src/components/Regions/components/Regions.tsx +120 -69
  50. package/src/components/Sankey/index.tsx +434 -0
  51. package/src/components/Sankey/sankey.scss +153 -0
  52. package/src/components/Sankey/types/index.ts +16 -0
  53. package/src/components/ScatterPlot/ScatterPlot.jsx +1 -0
  54. package/src/components/Sparkline/{SparkLine.jsx → components/SparkLine.tsx} +14 -30
  55. package/src/components/Sparkline/index.scss +3 -0
  56. package/src/components/Sparkline/index.tsx +1 -1
  57. package/src/components/ZoomBrush.tsx +2 -1
  58. package/src/data/initial-state.js +51 -4
  59. package/src/helpers/computeMarginBottom.ts +4 -3
  60. package/src/helpers/tests/computeMarginBottom.test.ts +2 -1
  61. package/src/hooks/useBarChart.js +5 -2
  62. package/src/hooks/useHighlightedBars.js +1 -1
  63. package/src/hooks/useMinMax.ts +3 -3
  64. package/src/hooks/useScales.ts +28 -18
  65. package/src/hooks/useTooltip.tsx +19 -14
  66. package/src/scss/main.scss +8 -96
  67. package/src/types/ChartConfig.ts +47 -20
  68. package/src/types/ChartContext.ts +17 -4
  69. package/src/types/Label.ts +7 -0
  70. package/examples/private/chart-t.json +0 -3740
  71. package/examples/private/epi-data.csv +0 -13
  72. package/examples/private/epi-data.json +0 -62
  73. package/examples/private/epi.json +0 -403
  74. package/examples/private/occupancy.json +0 -109283
  75. package/examples/private/prod-line-config.json +0 -401
  76. package/examples/private/region-data.json +0 -822
  77. package/examples/private/region-testing.json +0 -312
  78. package/examples/private/scaling.json +0 -45325
  79. package/examples/private/testing-data.json +0 -1739
  80. package/examples/private/testing.json +0 -816
  81. package/src/components/EditorPanel/components/Panel.DateHighlighting.tsx +0 -109
  82. package/src/components/EditorPanel/components/Panels.tsx +0 -13
@@ -0,0 +1,140 @@
1
+ import { colorPalettesChart as colorPalettes, sequentialPalettes, twoColorPalette } from '@cdc/core/data/colorPalettes'
2
+ import { FaStar } from 'react-icons/fa'
3
+ import { Label } from '../../../types/Label'
4
+ import { ColorScale, TransformedData } from '../../../types/ChartContext'
5
+ import { ChartConfig } from '../../../types/ChartConfig'
6
+
7
+ export const createFormatLabels =
8
+ (config: ChartConfig, tableData: Object[], data: TransformedData[], colorScale: ColorScale) =>
9
+ (defaultLabels: Label[]): Label[] => {
10
+ const { visualizationType, visualizationSubType, series, runtime } = config
11
+
12
+ const reverseLabels = labels => (config.legend.reverseLabelOrder && config.legend.position === 'bottom' ? labels.reverse() : labels)
13
+ const colorCode = config.legend?.colorCode
14
+ if (visualizationType === 'Deviation Bar') {
15
+ const [belowColor, aboveColor] = twoColorPalette[config.twoColor.palette]
16
+ const labelBelow = {
17
+ datum: 'X',
18
+ index: 0,
19
+ text: `Below ${config.xAxis.targetLabel}`,
20
+ value: belowColor
21
+ }
22
+ const labelAbove = {
23
+ datum: 'X',
24
+ index: 1,
25
+ text: `Above ${config.xAxis.targetLabel}`,
26
+ value: aboveColor
27
+ }
28
+
29
+ return reverseLabels([labelBelow, labelAbove])
30
+ }
31
+ if (visualizationType === 'Bar' && visualizationSubType === 'regular' && colorCode && series?.length === 1) {
32
+ let palette = colorPalettes[config.palette]
33
+
34
+ while (tableData.length > palette.length) {
35
+ palette = palette.concat(palette)
36
+ }
37
+ palette = palette.slice(0, data.length)
38
+ //store unique values to Set by colorCode
39
+ const set = new Set()
40
+
41
+ tableData.forEach(d => set.add(d[colorCode]))
42
+
43
+ // create labels with unique values
44
+ const uniqueLabels = Array.from(set).map((val, i) => {
45
+ const newLabel = {
46
+ datum: val,
47
+ index: i,
48
+ text: val,
49
+ value: palette[i]
50
+ }
51
+ return newLabel
52
+ })
53
+
54
+ return reverseLabels(uniqueLabels)
55
+ }
56
+
57
+ // get forecasting items inside of combo
58
+ if (runtime?.forecastingSeriesKeys?.length > 0) {
59
+ let seriesLabels = []
60
+
61
+ //store unique values to Set by colorCode
62
+ // loop through each stage/group/area on the chart and create a label
63
+ config.runtime?.forecastingSeriesKeys?.map((outerGroup, index) => {
64
+ return outerGroup?.stages?.map((stage, index) => {
65
+ let colorValue = sequentialPalettes[stage.color]?.[2] ? sequentialPalettes[stage.color]?.[2] : colorPalettes[stage.color]?.[2] ? colorPalettes[stage.color]?.[2] : '#ccc'
66
+
67
+ const newLabel = {
68
+ datum: stage.key,
69
+ index: index,
70
+ text: stage.key,
71
+ value: colorValue
72
+ }
73
+
74
+ seriesLabels.push(newLabel)
75
+ })
76
+ })
77
+
78
+ // loop through bars for now to meet requirements.
79
+ config.runtime.barSeriesKeys &&
80
+ config.runtime.barSeriesKeys.forEach((bar, index) => {
81
+ let colorValue = colorPalettes[config.palette][index] ? colorPalettes[config.palette][index] : '#ccc'
82
+
83
+ const newLabel = {
84
+ datum: bar,
85
+ index: index,
86
+ text: bar,
87
+ value: colorValue
88
+ }
89
+
90
+ seriesLabels.push(newLabel)
91
+ })
92
+
93
+ return reverseLabels(seriesLabels)
94
+ }
95
+
96
+ // DEV-4161: replaceable series name in the legend
97
+ const hasNewSeriesName = config.series.filter(item => !!item.name).length > 0
98
+ if (hasNewSeriesName) {
99
+ //store unique values to Set by colorCode
100
+ const set = new Set()
101
+
102
+ config.series.forEach(d => {
103
+ set.add(d.name || d.dataKey)
104
+ })
105
+
106
+ // create labels with unique values
107
+ const uniqueLabels = Array.from(set).map((val, i) => {
108
+ const newLabel = {
109
+ datum: val,
110
+ index: i,
111
+ text: val,
112
+ value: colorScale(val)
113
+ }
114
+ return newLabel
115
+ })
116
+
117
+ return reverseLabels(uniqueLabels)
118
+ }
119
+
120
+ if ((config.visualizationType === 'Bar' || config.visualizationType === 'Combo') && config.visualizationSubType === 'regular' && config.suppressedData) {
121
+ const lastIndex = defaultLabels.length - 1
122
+ let newLabels = []
123
+
124
+ config.suppressedData?.forEach(({ label, icon }, index) => {
125
+ if (label && icon) {
126
+ const newLabel = {
127
+ datum: label,
128
+ index: lastIndex + index,
129
+ text: label,
130
+ icon: <FaStar color='#000' size={15} />
131
+ }
132
+ newLabels.push(newLabel)
133
+ }
134
+ })
135
+
136
+ return [...defaultLabels, ...newLabels]
137
+ }
138
+
139
+ return reverseLabels(defaultLabels)
140
+ }
@@ -22,6 +22,7 @@ export interface PreliminaryDataItem {
22
22
  column: string
23
23
  value: string
24
24
  seriesKey: string
25
+ label: string
25
26
  }
26
27
 
27
28
  export interface DataItem {
@@ -33,7 +34,7 @@ export interface Config {
33
34
  }
34
35
  export interface StyleProps {
35
36
  preliminaryData: PreliminaryDataItem[]
36
- rawData: DataItem[]
37
+ data: DataItem[]
37
38
  stroke: string
38
39
  handleLineType: Function
39
40
  lineType: string
@@ -21,17 +21,15 @@ type LineChartCircleProps = {
21
21
  colorScale: any
22
22
  parseDate: any
23
23
  seriesAxis: string
24
+ dataIndex: number
25
+ mode: 'ISOLATED_POINTS' | 'HOVER_POINTS' | 'ALWAYS_SHOW_POINTS'
24
26
  }
25
27
 
26
28
  const LineChartCircle = (props: LineChartCircleProps) => {
27
- const { config, d, displayArea, seriesKey, tooltipData, xScale, yScale, colorScale, parseDate, yScaleRight, data, circleData } = props
29
+ const { config, d, displayArea, seriesKey, tooltipData, xScale, yScale, colorScale, parseDate, yScaleRight, data, circleData, dataIndex, mode } = props
28
30
  const { lineDatapointStyle } = config
29
31
  const filtered = config?.series.filter(s => s.dataKey === seriesKey)?.[0]
30
32
  // If we're not showing the circle, simply return
31
- if (lineDatapointStyle === 'hidden') return <></>
32
-
33
- const getIndex = seriesKey => config.runtime.seriesLabelsAll.indexOf(seriesKey)
34
-
35
33
  const getColor = (displayArea: boolean, colorScale: Function, config: ChartConfig, hoveredKey: string, seriesKey: string) => {
36
34
  const seriesLabels = config.runtime.seriesLabels || []
37
35
  let color
@@ -47,65 +45,100 @@ const LineChartCircle = (props: LineChartCircleProps) => {
47
45
  }
48
46
  return color
49
47
  }
50
- if (lineDatapointStyle === 'always show') {
51
- const isMatch = circleData?.some(cd => cd[config.xAxis.dataKey] === d[config.xAxis.dataKey] && cd[seriesKey] === d[seriesKey])
52
- if (isMatch) {
53
- return <></>
54
- }
55
- return (
56
- <circle
57
- cx={config.xAxis.type === 'categorical' ? xScale(d[config.xAxis.dataKey]) : xScale(parseDate(d[config.xAxis.dataKey]))}
58
- cy={filtered.axis === 'Right' ? yScaleRight(d[filtered.dataKey]) : yScale(d[filtered.dataKey])}
59
- r={4.5}
60
- opacity={d[seriesKey] ? 1 : 0}
61
- fillOpacity={1}
62
- fill={getColor(displayArea, colorScale, config, seriesKey, seriesKey)}
63
- style={{ filter: 'unset', opacity: 1 }}
64
- />
65
- )
48
+ const getXPos = hoveredXValue => {
49
+ return (config.xAxis.type === 'categorical' ? xScale(hoveredXValue) : xScale(parseDate(hoveredXValue))) + (xScale.bandwidth ? xScale.bandwidth() / 2 : 0)
66
50
  }
51
+ if (mode === 'ALWAYS_SHOW_POINTS') {
52
+ if (lineDatapointStyle === 'hidden') return <></>
53
+ const getIndex = seriesKey => config.runtime.seriesLabelsAll.indexOf(seriesKey)
67
54
 
68
- if (lineDatapointStyle === 'hover') {
69
- if (!tooltipData) return
70
- if (!seriesKey) return
71
- if (!tooltipData.data) return
72
- let hoveredXValue = tooltipData?.data?.[0]?.[1]
73
- if (!hoveredXValue) return
74
-
75
- let hoveredSeriesValue
76
- let hoveredSeriesIndex
77
- let hoveredSeriesData = tooltipData.data.filter(d => d[0] === seriesKey)
78
- let hoveredSeriesKey = hoveredSeriesData?.[0]?.[0]
79
- let hoveredSeriesAxis = hoveredSeriesData?.[0]?.[2]
80
- if (!hoveredSeriesKey) return
81
- hoveredSeriesIndex = tooltipData?.data.indexOf(hoveredSeriesKey)
82
- hoveredSeriesValue = data?.find(d => {
83
- return d[config?.xAxis.dataKey] === hoveredXValue
84
- })?.[seriesKey]
85
-
86
- // hoveredSeriesValue = extractNumber(hoveredSeriesValue)
87
- return tooltipData?.data.map((tooltipItem, index) => {
88
- let seriesIndex = config.runtime.seriesLabelsAll.indexOf(hoveredXValue)
89
-
90
- if (isNaN(hoveredSeriesValue)) return <></>
91
- const isMatch = circleData?.some(cd => cd[config.xAxis.dataKey] === hoveredXValue)
92
-
55
+ if (lineDatapointStyle === 'always show') {
56
+ const isMatch = circleData?.some(cd => cd[config.xAxis.dataKey] === d[config.xAxis.dataKey] && cd[seriesKey] === d[seriesKey])
93
57
  if (isMatch) {
94
58
  return <></>
95
59
  }
96
60
  return (
97
61
  <circle
98
- cx={config.xAxis.type === 'categorical' ? xScale(hoveredXValue) : xScale(parseDate(hoveredXValue))}
99
- cy={hoveredSeriesAxis === 'right' ? yScaleRight(hoveredSeriesValue) : yScale(hoveredSeriesValue)}
62
+ cx={getXPos(d[config.xAxis.dataKey])}
63
+ cy={filtered.axis === 'Right' ? yScaleRight(d[filtered.dataKey]) : yScale(d[filtered.dataKey])}
100
64
  r={4.5}
101
- opacity={1}
65
+ opacity={d[seriesKey] ? 1 : 0}
102
66
  fillOpacity={1}
103
- fill={getColor(displayArea, colorScale, config, hoveredSeriesKey, seriesKey)}
67
+ fill={getColor(displayArea, colorScale, config, seriesKey, seriesKey)}
104
68
  style={{ filter: 'unset', opacity: 1 }}
105
- key={`line-chart-circle--${JSON.stringify(tooltipItem)}--${index}`}
106
69
  />
107
70
  )
108
- })
71
+ }
72
+ }
73
+
74
+ if (mode === 'HOVER_POINTS') {
75
+ if (lineDatapointStyle === 'hover') {
76
+ if (!tooltipData) return
77
+ if (!seriesKey) return
78
+ if (!tooltipData.data) return
79
+ let hoveredXValue = tooltipData?.data?.[0]?.[1]
80
+ if (!hoveredXValue) return
81
+
82
+ let hoveredSeriesValue
83
+ let hoveredSeriesIndex
84
+ let hoveredSeriesData = tooltipData.data.filter(d => d[0] === seriesKey)
85
+ let hoveredSeriesKey = hoveredSeriesData?.[0]?.[0]
86
+ let hoveredSeriesAxis = hoveredSeriesData?.[0]?.[2]
87
+ if (!hoveredSeriesKey) return
88
+ hoveredSeriesIndex = tooltipData?.data.indexOf(hoveredSeriesKey)
89
+ hoveredSeriesValue = data?.find(d => {
90
+ return d[config?.xAxis.dataKey] === hoveredXValue
91
+ })?.[seriesKey]
92
+
93
+ // hoveredSeriesValue = extractNumber(hoveredSeriesValue)
94
+ return tooltipData?.data.map((tooltipItem, index) => {
95
+ let seriesIndex = config.runtime.seriesLabelsAll.indexOf(hoveredXValue)
96
+
97
+ if (isNaN(hoveredSeriesValue)) return <></>
98
+ const isMatch = circleData?.some(cd => cd[config.xAxis.dataKey] === hoveredXValue)
99
+
100
+ if (isMatch) {
101
+ return <></>
102
+ }
103
+ return (
104
+ <circle
105
+ cx={getXPos(hoveredXValue)}
106
+ cy={hoveredSeriesAxis === 'right' ? yScaleRight(hoveredSeriesValue) : yScale(hoveredSeriesValue)}
107
+ r={4.5}
108
+ opacity={1}
109
+ fillOpacity={1}
110
+ fill={getColor(displayArea, colorScale, config, hoveredSeriesKey, seriesKey)}
111
+ style={{ filter: 'unset', opacity: 1 }}
112
+ key={`line-chart-circle--${JSON.stringify(tooltipItem)}--${index}`}
113
+ />
114
+ )
115
+ })
116
+ }
117
+ }
118
+
119
+ if (mode === 'ISOLATED_POINTS') {
120
+ const drawIsolatedPoints = (currentIndex, seriesKey) => {
121
+ 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
126
+ }
127
+ if (currentIndex === data.length - 1 && !previousPoint[seriesKey]) {
128
+ return true
129
+ }
130
+ if (currentIndex !== 0 && currentPoint[seriesKey] && !previousPoint[seriesKey] && !nextPoint[seriesKey]) {
131
+ return true
132
+ }
133
+ }
134
+
135
+ if (mode) {
136
+ if (drawIsolatedPoints(dataIndex, seriesKey)) {
137
+ 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])} />
139
+ )
140
+ }
141
+ }
109
142
  }
110
143
 
111
144
  return null
@@ -1,7 +1,7 @@
1
1
  import { type PreliminaryDataItem, DataItem, StyleProps, Style } from './LineChartProps'
2
2
 
3
3
  export const createStyles = (props: StyleProps): Style[] => {
4
- const { preliminaryData, rawData, stroke, handleLineType, lineType, seriesKey } = props
4
+ const { preliminaryData, data, stroke, strokeWidth, handleLineType, lineType, seriesKey } = props
5
5
 
6
6
  const validPreliminaryData: PreliminaryDataItem[] = preliminaryData.filter(pd => pd.seriesKey && pd.column && pd.value && pd.type && pd.style)
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')
@@ -9,11 +9,11 @@ export const createStyles = (props: StyleProps): Style[] => {
9
9
  let styles: Style[] = []
10
10
  const createStyle = (lineStyle): Style => ({
11
11
  stroke: stroke,
12
- strokeWidth: 2,
12
+ strokeWidth: strokeWidth,
13
13
  strokeDasharray: lineStyle
14
14
  })
15
15
 
16
- rawData.forEach((d, index) => {
16
+ data.forEach((d, index) => {
17
17
  let matchingPd: PreliminaryDataItem = getMatchingPd(d)
18
18
  let style: Style = matchingPd ? createStyle(handleLineType(matchingPd.style)) : createStyle(handleLineType(lineType))
19
19
 
@@ -52,7 +52,7 @@ const LineChart = (props: LineChartProps) => {
52
52
 
53
53
  return (
54
54
  <ErrorBoundary component='LineChart'>
55
- <Group left={config.runtime.yAxis.size ? parseInt(config.runtime.yAxis.size) : 66}>
55
+ <Group left={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) => {
@@ -62,7 +62,11 @@ const LineChart = (props: LineChartProps) => {
62
62
  let displayArea = legend.behavior === 'highlight' || seriesHighlight.length === 0 || seriesHighlight.indexOf(seriesKey) !== -1
63
63
  const circleData = filterCircles(config.preliminaryData, rawData, seriesKey)
64
64
  // styles for preliminary Data items
65
- let styles = createStyles({ preliminaryData: config.preliminaryData, rawData, stroke: colorScale(config.runtime.seriesLabels[seriesKey]), handleLineType, lineType, seriesKey })
65
+ let styles = createStyles({ preliminaryData: config.preliminaryData, data: tableData, stroke: colorScale(config.runtime.seriesLabels[seriesKey]), strokeWidth: seriesData[0].weight || 2, handleLineType, lineType, seriesKey })
66
+
67
+ let xPos = d => {
68
+ return xScale(getXAxisData(d)) + (xScale.bandwidth ? xScale.bandwidth() / 2 : 0)
69
+ }
66
70
 
67
71
  return (
68
72
  <Group
@@ -91,12 +95,14 @@ const LineChart = (props: LineChartProps) => {
91
95
  <Bar key={'bars'} width={Number(xMax)} height={Number(yMax)} fill={DEBUG ? 'red' : 'transparent'} fillOpacity={0.05} onMouseMove={e => handleTooltipMouseOver(e, tableData)} onMouseOut={handleTooltipMouseOff} onClick={e => handleTooltipClick(e, data)} />
92
96
 
93
97
  {/* Render legend */}
94
- <Text display={config.labels ? 'block' : 'none'} x={xScale(getXAxisData(d))} y={seriesAxis === 'Right' ? yScaleRight(getYAxisData(d, seriesKey)) : yScale(getYAxisData(d, seriesKey))} fill={'#000'} textAnchor='middle'>
98
+ <Text display={config.labels ? 'block' : 'none'} x={xPos(d)} y={seriesAxis === 'Right' ? yScaleRight(getYAxisData(d, seriesKey)) : yScale(getYAxisData(d, seriesKey))} fill={'#000'} textAnchor='middle'>
95
99
  {formatNumber(d[seriesKey], 'left')}
96
100
  </Text>
97
101
 
98
102
  {(lineDatapointStyle === 'hidden' || lineDatapointStyle === 'always show') && (
99
103
  <LineChartCircle
104
+ mode='ALWAYS_SHOW_POINTS'
105
+ dataIndex={dataIndex}
100
106
  circleData={circleData}
101
107
  data={data}
102
108
  d={d}
@@ -113,43 +119,113 @@ const LineChart = (props: LineChartProps) => {
113
119
  key={`line-circle--${dataIndex}`}
114
120
  />
115
121
  )}
122
+
123
+ <LineChartCircle
124
+ mode='ISOLATED_POINTS'
125
+ dataIndex={dataIndex}
126
+ circleData={circleData}
127
+ data={data}
128
+ d={d}
129
+ config={config}
130
+ seriesKey={seriesKey}
131
+ displayArea={displayArea}
132
+ tooltipData={tooltipData}
133
+ xScale={xScale}
134
+ yScale={yScale}
135
+ colorScale={colorScale}
136
+ parseDate={parseDate}
137
+ yScaleRight={yScaleRight}
138
+ seriesAxis={seriesAxis}
139
+ key={`isolated-circle-${dataIndex}`}
140
+ />
116
141
  </Group>
117
142
  )
118
143
  )
119
144
  })}
120
145
  <>
121
146
  {lineDatapointStyle === 'hover' && (
122
- <LineChartCircle circleData={circleData} data={data} config={config} seriesKey={seriesKey} displayArea={displayArea} tooltipData={tooltipData} xScale={xScale} yScale={yScale} colorScale={colorScale} parseDate={parseDate} yScaleRight={yScaleRight} seriesAxis={seriesAxis} />
147
+ <LineChartCircle
148
+ dataIndex={0}
149
+ mode='HOVER_POINTS'
150
+ circleData={circleData}
151
+ data={data}
152
+ config={config}
153
+ seriesKey={seriesKey}
154
+ displayArea={displayArea}
155
+ tooltipData={tooltipData}
156
+ xScale={xScale}
157
+ yScale={yScale}
158
+ colorScale={colorScale}
159
+ parseDate={parseDate}
160
+ yScaleRight={yScaleRight}
161
+ seriesAxis={seriesAxis}
162
+ />
123
163
  )}
124
164
  </>
125
- {/* STANDARD LINE */}
126
- <LinePath
127
- curve={allCurves[seriesData[0].lineType]}
128
- data={data}
129
- x={d => xScale(getXAxisData(d))}
130
- y={d => (seriesAxis === 'Right' ? yScaleRight(getYAxisData(d, seriesKey)) : yScale(Number(getYAxisData(d, seriesKey))))}
131
- stroke={colorScale(config.runtime.seriesLabels[seriesKey])}
132
- strokeWidth={2}
133
- strokeOpacity={1}
134
- shapeRendering='geometricPrecision'
135
- strokeDasharray={lineType ? handleLineType(lineType) : 0}
136
- defined={(item, i) => {
137
- return item[seriesKey] !== '' && item[seriesKey] !== null && item[seriesKey] !== undefined
138
- }}
139
- />
165
+ {/* 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
+ />
187
+ ) : (
188
+ <>
189
+ {/* STANDARD LINE */}
190
+ <LinePath
191
+ curve={allCurves[seriesData[0].lineType]}
192
+ data={
193
+ config.xAxis.type === 'date-time'
194
+ ? data.sort((d1, d2) => {
195
+ let x1 = getXAxisData(d1)
196
+ let x2 = getXAxisData(d2)
197
+ if (x1 < x2) return -1
198
+ if (x2 < x1) return 1
199
+ return 0
200
+ })
201
+ : data
202
+ }
203
+ x={d => xPos(d)}
204
+ y={d => (seriesAxis === 'Right' ? yScaleRight(getYAxisData(d, seriesKey)) : yScale(Number(getYAxisData(d, seriesKey))))}
205
+ stroke={colorScale(config.runtime.seriesLabels[seriesKey])}
206
+ strokeWidth={seriesData[0].weight || 2}
207
+ strokeOpacity={1}
208
+ shapeRendering='geometricPrecision'
209
+ strokeDasharray={lineType ? handleLineType(lineType) : 0}
210
+ defined={(item, i) => {
211
+ return item[seriesKey] !== '' && item[seriesKey] !== null && item[seriesKey] !== undefined
212
+ }}
213
+ />
214
+ </>
215
+ )}
140
216
 
141
217
  {/* circles for preliminaryData data */}
142
218
  {circleData.map((d, i) => {
143
- return <circle key={i} cx={xScale(getXAxisData(d))} cy={yScale(Number(getYAxisData(d, seriesKey)))} r={6} strokeWidth={2} stroke={colorScale ? colorScale(config.runtime.seriesLabels[seriesKey]) : '#000'} fill='#fff' />
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' />
144
220
  })}
145
221
 
146
222
  {/* ANIMATED LINE */}
147
223
  {config.animate && (
148
224
  <LinePath
149
225
  className='animation'
150
- curve={seriesData.lineType}
226
+ curve={allCurves[seriesData[0].lineType]}
151
227
  data={data}
152
- x={d => xScale(getXAxisData(d))}
228
+ x={d => xPos(d)}
153
229
  y={d => (seriesAxis === 'Right' ? yScaleRight(getYAxisData(d, seriesKey)) : yScale(Number(getYAxisData(d, seriesKey))))}
154
230
  stroke='#fff'
155
231
  strokeWidth={3}
@@ -175,7 +251,7 @@ const LineChart = (props: LineChartProps) => {
175
251
  return <></>
176
252
  }
177
253
  return (
178
- <text x={xScale(getXAxisData(lastDatum)) + 5} y={yScale(getYAxisData(lastDatum, seriesKey))} alignmentBaseline='middle' fill={config.colorMatchLineSeriesLabels && colorScale ? colorScale(config.runtime.seriesLabels[seriesKey] || seriesKey) : 'black'}>
254
+ <text x={xPos(lastDatum) + 5} y={yScale(getYAxisData(lastDatum, seriesKey))} alignmentBaseline='middle' fill={config.colorMatchLineSeriesLabels && colorScale ? colorScale(config.runtime.seriesLabels[seriesKey] || seriesKey) : 'black'}>
179
255
  {config.runtime.seriesLabels[seriesKey] || seriesKey}
180
256
  </text>
181
257
  )
@@ -7,6 +7,7 @@ import { Line, Bar } from '@visx/shape'
7
7
  import { Text } from '@visx/text'
8
8
  import { Tooltip as ReactTooltip } from 'react-tooltip'
9
9
  import { useTooltip, TooltipWithBounds } from '@visx/tooltip'
10
+ import { isDateScale } from '@cdc/core/helpers/cove/date'
10
11
 
11
12
  // CDC Components
12
13
  import { AreaChart, AreaChartStacked } from './AreaChart'
@@ -36,32 +37,10 @@ import { useEditorPermissions } from './EditorPanel/useEditorPermissions'
36
37
  import ZoomBrush from './ZoomBrush'
37
38
 
38
39
  const LinearChart = props => {
39
- const {
40
- isEditor,
41
- isDashboard,
42
- computeMarginBottom,
43
- transformedData: data,
44
- dimensions,
45
- config,
46
- parseDate,
47
- formatDate,
48
- currentViewport,
49
- formatNumber,
50
- handleChartAriaLabels,
51
- updateConfig,
52
- handleLineType,
53
- rawData,
54
- capitalize,
55
- setSharedFilter,
56
- setSharedFilterValue,
57
- getTextWidth,
58
- isDebug
59
- } = useContext(ConfigContext)
40
+ const { transformedData: data, dimensions, config, parseDate, formatDate, currentViewport, formatNumber, handleChartAriaLabels, updateConfig, handleLineType, getTextWidth } = useContext(ConfigContext)
60
41
  // todo: start destructuring this file for conciseness
61
42
  const { visualizationType, visualizationSubType, orientation, xAxis, yAxis, runtime, debugSvg } = config
62
43
 
63
- const getDate = d => new Date(d[config.xAxis.dataKey])
64
-
65
44
  // configure width
66
45
  let [width] = dimensions
67
46
  if (config && config.legend && !config.legend.hide && config.legend.position !== 'bottom' && ['lg', 'md'].includes(currentViewport)) {
@@ -80,8 +59,8 @@ const LinearChart = props => {
80
59
  yMax = yMax + config.data.length * config.forestPlot.rowHeight
81
60
  width = dimensions[0]
82
61
  }
83
- if (config.brush.active) {
84
- height = height + config.brush.height
62
+ if (config.brush?.active) {
63
+ height = height + config.brush?.height
85
64
  }
86
65
 
87
66
  // hooks % states
@@ -99,7 +78,7 @@ const LinearChart = props => {
99
78
  })
100
79
 
101
80
  // getters & functions
102
- const getXAxisData = d => (config.runtime.xAxis.type === 'date' ? parseDate(d[config.runtime.originalXAxis.dataKey]).getTime() : d[config.runtime.originalXAxis.dataKey])
81
+ const getXAxisData = d => (isDateScale(config.runtime.xAxis) ? parseDate(d[config.runtime.originalXAxis.dataKey]).getTime() : d[config.runtime.originalXAxis.dataKey])
103
82
  const getYAxisData = (d, seriesKey) => d[seriesKey]
104
83
  const xAxisDataMapped = config.brush.active && config.brush.data?.length ? config.brush.data.map(d => getXAxisData(d)) : data.map(d => getXAxisData(d))
105
84
  const section = config.orientation === 'horizontal' || config.visualizationType === 'Forest Plot' ? 'yAxis' : 'xAxis'
@@ -122,7 +101,7 @@ const LinearChart = props => {
122
101
 
123
102
  if (config.data && !config.data[index] && visualizationType === 'Forest Plot') return
124
103
  if (config.visualizationType === 'Forest Plot') return config.data[index][config.xAxis.dataKey]
125
- if (runtime.yAxis.type === 'date') return formatDate(parseDate(tick))
104
+ if (isDateScale(runtime.yAxis)) return formatDate(parseDate(tick))
126
105
  if (orientation === 'vertical') return formatNumber(tick, 'left', shouldAbbreviate)
127
106
  return tick
128
107
  }
@@ -133,7 +112,7 @@ const LinearChart = props => {
133
112
  tick = 0
134
113
  }
135
114
 
136
- if (runtime.xAxis.type === 'date' && config.visualizationType !== 'Forest Plot') return formatDate(tick)
115
+ if (isDateScale(runtime.xAxis) && config.visualizationType !== 'Forest Plot') return formatDate(tick)
137
116
  if (orientation === 'horizontal' && config.visualizationType !== 'Forest Plot') return formatNumber(tick, 'left', shouldAbbreviate)
138
117
  if (config.xAxis.type === 'continuous' && config.visualizationType !== 'Forest Plot') return formatNumber(tick, 'bottom', shouldAbbreviate)
139
118
  if (config.visualizationType === 'Forest Plot') return formatNumber(tick, 'left', config.dataFormat.abbreviated, config.runtime.xAxis.prefix, config.runtime.xAxis.suffix, Number(config.dataFormat.roundTo))
@@ -267,7 +246,7 @@ const LinearChart = props => {
267
246
  <Bar width={width} height={height} fill={'transparent'}></Bar> {/* Highlighted regions */}
268
247
  {/* Y axis */}
269
248
  {!['Spark Line', 'Forest Plot'].includes(visualizationType) && (
270
- <AxisLeft scale={yScale} tickLength={config.useLogScale ? 6 : 8} left={Number(runtime.yAxis.size) - config.yAxis.axisPadding} label={runtime.yAxis.label} stroke='#333' tickFormat={(tick, i) => handleLeftTickFormatting(tick, i)} numTicks={handleNumTicks()}>
249
+ <AxisLeft scale={yScale} tickLength={config.useLogScale ? 6 : 8} left={Number(runtime.yAxis.size) - config.yAxis.axisPadding} label={runtime.yAxis.yAxis?.label || runtime.yAxis.label} stroke='#333' tickFormat={(tick, i) => handleLeftTickFormatting(tick, i)} numTicks={handleNumTicks()}>
271
250
  {props => {
272
251
  const axisCenter = config.orientation === 'horizontal' ? (props.axisToPoint.y - props.axisFromPoint.y) / 2 : (props.axisFromPoint.y - props.axisToPoint.y) / 2
273
252
  const horizontalTickOffset = yMax / props.ticks.length / 2 - (yMax / props.ticks.length) * (1 - config.barThickness) + 5
@@ -388,7 +367,7 @@ const LinearChart = props => {
388
367
  {/* X axis */}
389
368
  {visualizationType !== 'Paired Bar' && visualizationType !== 'Spark Line' && (
390
369
  <AxisBottom
391
- top={runtime.horizontal && config.visualizationType !== 'Forest Plot' ? Number(heightHorizontal) + Number(config.xAxis.axisPadding) : config.visualizationType === 'Forest Plot' ? yMax + Number(config.xAxis.axisPadding) : yMax + Number(config.xAxis.axisPadding)}
370
+ top={runtime.horizontal && config.visualizationType !== 'Forest Plot' ? Number(heightHorizontal) + Number(config.xAxis.axisPadding) : config.visualizationType === 'Forest Plot' ? yMax + Number(config.xAxis.axisPadding) : yMax}
392
371
  left={config.visualizationType !== 'Forest Plot' ? Number(runtime.yAxis.size) : 0}
393
372
  label={runtime.xAxis.label}
394
373
  tickFormat={handleBottomTickFormatting}
@@ -502,7 +481,7 @@ const LinearChart = props => {
502
481
  )}
503
482
  {visualizationType === 'Paired Bar' && (
504
483
  <>
505
- <AxisBottom top={yMax} left={Number(runtime.yAxis.size)} label={runtime.xAxis.label} tickFormat={runtime.xAxis.type === 'date' ? formatDate : formatNumber} scale={g1xScale} stroke='#333' tickStroke='#333' numTicks={runtime.xAxis.numTicks || undefined}>
484
+ <AxisBottom top={yMax} left={Number(runtime.yAxis.size)} label={runtime.xAxis.label} tickFormat={isDateScale(runtime.xAxis) ? formatDate : formatNumber} scale={g1xScale} stroke='#333' tickStroke='#333' numTicks={runtime.xAxis.numTicks || undefined}>
506
485
  {props => {
507
486
  return (
508
487
  <Group className='bottom-axis'>
@@ -529,7 +508,7 @@ const LinearChart = props => {
529
508
  top={yMax}
530
509
  left={Number(runtime.yAxis.size)}
531
510
  label={runtime.xAxis.label}
532
- tickFormat={runtime.xAxis.type === 'date' ? formatDate : runtime.xAxis.dataKey !== 'Year' ? formatNumber : tick => tick}
511
+ tickFormat={isDateScale(runtime.xAxis) ? formatDate : runtime.xAxis.dataKey !== 'Year' ? formatNumber : tick => tick}
533
512
  scale={g2xScale}
534
513
  stroke='#333'
535
514
  tickStroke='#333'
@@ -708,7 +687,7 @@ const LinearChart = props => {
708
687
  newX = yAxis
709
688
  }
710
689
 
711
- let anchorPosition = newX.type === 'date' ? xScale(parseDate(anchor.value, false)) : xScale(anchor.value)
690
+ let anchorPosition = isDateScale(newX) ? xScale(parseDate(anchor.value, false)) : xScale(anchor.value)
712
691
 
713
692
  // have to move up
714
693
  // const padding = orientation === 'horizontal' ? Number(config.xAxis.size) : Number(config.yAxis.size)