@cdc/chart 4.23.11 → 4.24.2

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 (104) hide show
  1. package/dist/cdcchart.js +35740 -35027
  2. package/examples/feature/bar/additional-column-tooltip.json +446 -0
  3. package/examples/feature/bar/tall-data.json +98 -0
  4. package/examples/feature/forest-plot/forest-plot.json +63 -19
  5. package/examples/feature/forest-plot/linear.json +52 -3
  6. package/examples/feature/forest-plot/log.json +26 -0
  7. package/examples/feature/forest-plot/logarithmic.json +0 -35
  8. package/examples/feature/line/line-chart-preliminary.json +393 -0
  9. package/examples/feature/regions/index.json +9 -5
  10. package/examples/feature/scatterplot/scatterplot.json +272 -33
  11. package/index.html +10 -8
  12. package/package.json +2 -2
  13. package/src/CdcChart.tsx +70 -234
  14. package/src/ConfigContext.tsx +6 -0
  15. package/src/_stories/ChartEditor.stories.tsx +22 -0
  16. package/src/_stories/ChartLine.preliminary.tsx +19 -0
  17. package/src/_stories/_mock/pie_config.json +192 -0
  18. package/src/_stories/_mock/pie_data.json +218 -0
  19. package/src/_stories/_mock/preliminary_mock.json +346 -0
  20. package/src/components/{AreaChart.Stacked.jsx → AreaChart/components/AreaChart.Stacked.jsx} +2 -2
  21. package/src/components/{AreaChart.jsx → AreaChart/components/AreaChart.jsx} +2 -26
  22. package/src/components/AreaChart/index.tsx +4 -0
  23. package/src/components/{BarChart.Horizontal.tsx → BarChart/components/BarChart.Horizontal.tsx} +8 -8
  24. package/src/components/{BarChart.StackedHorizontal.tsx → BarChart/components/BarChart.StackedHorizontal.tsx} +37 -7
  25. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +108 -0
  26. package/src/components/{BarChart.Vertical.tsx → BarChart/components/BarChart.Vertical.tsx} +53 -70
  27. package/src/components/BarChart/components/BarChart.jsx +39 -0
  28. package/src/components/{BarChartType.jsx → BarChart/components/BarChartType.jsx} +0 -2
  29. package/src/components/BarChart/components/context.tsx +13 -0
  30. package/src/components/BarChart/index.tsx +3 -0
  31. package/src/components/{BoxPlot.jsx → BoxPlot/BoxPlot.jsx} +10 -9
  32. package/src/components/BoxPlot/index.tsx +3 -0
  33. package/src/components/EditorPanel/EditorPanel.tsx +2776 -0
  34. package/src/components/EditorPanel/EditorPanelContext.ts +40 -0
  35. package/src/components/EditorPanel/components/PanelProps.ts +3 -0
  36. package/src/components/EditorPanel/components/Panels/Panel.BoxPlot.tsx +148 -0
  37. package/src/components/{ForestPlotSettings.jsx → EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx} +97 -167
  38. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +160 -0
  39. package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +168 -0
  40. package/src/components/{Series.jsx → EditorPanel/components/Panels/Panel.Series.tsx} +4 -4
  41. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +297 -0
  42. package/src/components/EditorPanel/components/Panels/index.tsx +17 -0
  43. package/src/components/EditorPanel/components/panels.scss +72 -0
  44. package/src/components/EditorPanel/editor-panel.scss +739 -0
  45. package/src/components/EditorPanel/index.tsx +3 -0
  46. package/src/{hooks → components/EditorPanel}/useEditorPermissions.js +34 -2
  47. package/src/components/{Forecasting.jsx → Forecasting/Forecasting.jsx} +1 -1
  48. package/src/components/Forecasting/index.tsx +3 -0
  49. package/src/components/ForestPlot/ForestPlot.tsx +254 -0
  50. package/src/components/ForestPlot/ForestPlotProps.ts +7 -0
  51. package/src/components/ForestPlot/index.tsx +1 -209
  52. package/src/components/Legend/Legend.Component.tsx +199 -0
  53. package/src/components/Legend/Legend.tsx +28 -0
  54. package/src/components/Legend/helpers/createFormatLabels.tsx +140 -0
  55. package/src/components/Legend/index.tsx +3 -0
  56. package/src/components/LineChart/LineChartProps.ts +29 -0
  57. package/src/components/LineChart/components/LineChart.Circle.tsx +147 -0
  58. package/src/components/LineChart/helpers.ts +45 -0
  59. package/src/components/LineChart/index.tsx +111 -23
  60. package/src/components/LinearChart.jsx +55 -72
  61. package/src/components/PairedBarChart.jsx +4 -2
  62. package/src/components/{PieChart.jsx → PieChart/PieChart.tsx} +93 -31
  63. package/src/components/PieChart/index.tsx +3 -0
  64. package/src/components/Regions/components/Regions.tsx +144 -0
  65. package/src/components/Regions/index.tsx +3 -0
  66. package/src/components/{ScatterPlot.jsx → ScatterPlot/ScatterPlot.jsx} +3 -3
  67. package/src/components/ScatterPlot/index.tsx +3 -0
  68. package/src/components/{SparkLine.jsx → Sparkline/SparkLine.jsx} +2 -2
  69. package/src/components/Sparkline/index.tsx +3 -0
  70. package/src/data/initial-state.js +10 -8
  71. package/src/helpers/abbreviateNumber.ts +17 -0
  72. package/src/helpers/computeMarginBottom.ts +55 -0
  73. package/src/helpers/filterData.ts +18 -0
  74. package/src/helpers/generateColorsArray.ts +8 -0
  75. package/src/helpers/getQuartiles.ts +30 -0
  76. package/src/helpers/handleChartAriaLabels.ts +19 -0
  77. package/src/helpers/handleLineType.ts +18 -0
  78. package/src/helpers/lineOptions.ts +18 -0
  79. package/src/helpers/sort.ts +7 -0
  80. package/src/helpers/tests/computeMarginBottom.test.ts +20 -0
  81. package/src/hooks/useBarChart.js +7 -6
  82. package/src/hooks/useHighlightedBars.js +1 -1
  83. package/src/hooks/useMinMax.ts +3 -3
  84. package/src/hooks/useScales.ts +19 -6
  85. package/src/hooks/{useTooltip.jsx → useTooltip.tsx} +31 -25
  86. package/src/scss/main.scss +0 -3
  87. package/src/types/ChartConfig.ts +167 -23
  88. package/src/types/ChartContext.ts +34 -12
  89. package/src/types/ForestPlot.ts +7 -14
  90. package/src/types/Label.ts +7 -0
  91. package/examples/feature/scatterplot/scatterplot-continuous.csv +0 -17
  92. package/src/ConfigContext.jsx +0 -5
  93. package/src/components/BarChart.StackedVertical.tsx +0 -91
  94. package/src/components/BarChart.jsx +0 -30
  95. package/src/components/EditorPanel.jsx +0 -3356
  96. package/src/components/ForestPlot/Readme.md +0 -0
  97. package/src/components/Legend.jsx +0 -310
  98. package/src/components/LineChart/LineChart.Circle.tsx +0 -105
  99. package/src/scss/LinearChart.scss +0 -0
  100. package/src/scss/editor-panel.scss +0 -745
  101. package/src/scss/legend.scss +0 -206
  102. package/src/scss/mixins.scss +0 -0
  103. package/src/scss/variables.scss +0 -1
  104. package/src/types/ChartProps.ts +0 -7
@@ -0,0 +1,199 @@
1
+ import parse from 'html-react-parser'
2
+ import { LegendOrdinal, LegendItem, LegendLabel } from '@visx/legend'
3
+ import LegendCircle from '@cdc/core/components/LegendCircle'
4
+
5
+ import useLegendClasses from '../../hooks/useLegendClasses'
6
+ import { useHighlightedBars } from '../../hooks/useHighlightedBars'
7
+ import { handleLineType } from '../../helpers/handleLineType'
8
+ import { Line } from '@visx/shape'
9
+ import { scaleOrdinal } from '@visx/scale'
10
+ import { Label } from '../../types/Label'
11
+ import { ChartConfig } from '../../types/ChartConfig'
12
+ import { ColorScale } from '../../types/ChartContext'
13
+ import { Group } from '@visx/group'
14
+
15
+ interface LegendProps {
16
+ config: ChartConfig
17
+ colorScale: ColorScale
18
+ seriesHighlight: string[]
19
+ highlight: Function
20
+ highlightReset: Function
21
+ currentViewport: string
22
+ formatLabels: (labels: Label[]) => Label[]
23
+ }
24
+
25
+ /* eslint-disable jsx-a11y/no-noninteractive-tabindex, jsx-a11y/no-static-element-interactions */
26
+ const Legend: React.FC<LegendProps> = ({ config, colorScale, seriesHighlight, highlight, highlightReset, currentViewport, formatLabels }) => {
27
+ const { innerClasses, containerClasses } = useLegendClasses(config)
28
+ const { runtime, orientation, legend } = config
29
+ if (!legend) return null
30
+ // create fn to reverse labels while legend is Bottom. Legend-right , legend-left works by default.
31
+ const displayScale = scaleOrdinal({
32
+ domain: config.suppressedData?.map(d => d.label),
33
+ range: ['none'],
34
+ unknown: 'block'
35
+ })
36
+
37
+ const renderDashes = style => {
38
+ const dashCount = style === 'Dashed Small' ? 3 : 2
39
+ const dashClass = `dashes ${style.toLowerCase().replace(' ', '-')}`
40
+
41
+ return (
42
+ <div className={dashClass}>
43
+ {Array.from({ length: dashCount }, (_, i) => (
44
+ <span key={i}>-</span>
45
+ ))}
46
+ </div>
47
+ )
48
+ }
49
+ const renderDashesOrCircle = style => {
50
+ if (['Dashed Small', 'Dashed Medium', 'Dashed Large'].includes(style)) {
51
+ return renderDashes(style)
52
+ } else if (style === 'Open Circles') {
53
+ return <div className='dashes open-circles'></div>
54
+ }
55
+ }
56
+
57
+ const isBottomOrSmallViewport = legend.position === 'bottom' || ['sm', 'xs', 'xxs'].includes(currentViewport)
58
+
59
+ const legendClasses = {
60
+ marginBottom: isBottomOrSmallViewport ? '15px' : '0px',
61
+ marginTop: isBottomOrSmallViewport && orientation === 'horizontal' ? `${config.yAxis.label && config.isResponsiveTicks ? config.dynamicMarginTop : config.runtime.xAxis.size}px` : `${isBottomOrSmallViewport ? config.dynamicMarginTop + 15 : 0}px`
62
+ }
63
+
64
+ const { HighLightedBarUtils } = useHighlightedBars(config)
65
+
66
+ let highLightedLegendItems = HighLightedBarUtils.findDuplicates(config.highlightedBarValues)
67
+
68
+ return (
69
+ <aside style={legendClasses} id='legend' className={containerClasses.join(' ')} role='region' aria-label='legend' tabIndex={0}>
70
+ {legend.label && <h2>{parse(legend.label)}</h2>}
71
+ {legend.description && <p>{parse(legend.description)}</p>}
72
+ <LegendOrdinal scale={colorScale} itemDirection='row' labelMargin='0 20px 0 0' shapeMargin='0 10px 0'>
73
+ {labels => {
74
+ return (
75
+ <>
76
+ <div className={innerClasses.join(' ')}>
77
+ {formatLabels(labels as Label[]).map((label, i) => {
78
+ let className = ['legend-item', `legend-text--${label.text.replace(' ', '').toLowerCase()}`]
79
+ let itemName = label.datum
80
+
81
+ // Filter excluded data keys from legend
82
+ if (config.exclusions.active && config.exclusions.keys?.includes(itemName)) {
83
+ return null
84
+ }
85
+
86
+ if (runtime.seriesLabels) {
87
+ let index = config.runtime.seriesLabelsAll.indexOf(itemName)
88
+ itemName = config.runtime.seriesKeys[index]
89
+
90
+ if (runtime?.forecastingSeriesKeys?.length > 0) {
91
+ itemName = label.text
92
+ }
93
+ }
94
+
95
+ if (seriesHighlight.length > 0 && false === seriesHighlight.includes(itemName)) {
96
+ className.push('inactive')
97
+ }
98
+
99
+ return (
100
+ <LegendItem
101
+ className={className.join(' ')}
102
+ tabIndex={0}
103
+ key={`legend-quantile-${i}`}
104
+ onKeyPress={e => {
105
+ if (e.key === 'Enter') {
106
+ highlight(label)
107
+ }
108
+ }}
109
+ onClick={() => {
110
+ highlight(label)
111
+ }}
112
+ >
113
+ {config.visualizationType === 'Line' && config.legend.lineMode ? (
114
+ <svg width={40} height={20}>
115
+ <Line from={{ x: 10, y: 10 }} to={{ x: 40, y: 10 }} stroke={label.value} strokeWidth={2} strokeDasharray={handleLineType(config.series[i]?.type ? config.series[i]?.type : '')} />
116
+ </svg>
117
+ ) : (
118
+ <div style={{ display: 'flex', flexDirection: 'column' }}>
119
+ <LegendCircle margin='0' fill={label.value} display={displayScale(label.datum)} />
120
+ <div style={{ marginTop: '2px', marginRight: '6px' }}>{label.icon}</div>
121
+ </div>
122
+ )}
123
+
124
+ <LegendLabel align='left' margin='0 0 0 4px'>
125
+ {label.text}
126
+ </LegendLabel>
127
+ </LegendItem>
128
+ )
129
+ })}
130
+
131
+ {highLightedLegendItems.map((bar, i) => {
132
+ // if duplicates only return first item
133
+ let className = 'legend-item'
134
+ let itemName = bar.legendLabel
135
+
136
+ if (!itemName) return false
137
+ if (seriesHighlight.length > 0 && false === seriesHighlight.includes(itemName)) {
138
+ className += ' inactive'
139
+ }
140
+ return (
141
+ <LegendItem
142
+ className={className}
143
+ tabIndex={0}
144
+ key={`legend-quantile-${i}`}
145
+ onKeyPress={e => {
146
+ if (e.key === 'Enter') {
147
+ highlight(bar.legendLabel)
148
+ }
149
+ }}
150
+ onClick={() => {
151
+ highlight(bar.legendLabel)
152
+ }}
153
+ >
154
+ <LegendCircle fill='transparent' borderColor={bar.color ? bar.color : `rgba(255, 102, 1)`} />{' '}
155
+ <LegendLabel align='left' margin='0 0 0 4px'>
156
+ {bar.legendLabel ? bar.legendLabel : bar.value}
157
+ </LegendLabel>
158
+ </LegendItem>
159
+ )
160
+ })}
161
+ {seriesHighlight.length > 0 && (
162
+ <button className={`legend-reset ${config.theme}`} onClick={labels => highlightReset(labels)} tabIndex={0}>
163
+ Reset
164
+ </button>
165
+ )}
166
+ </div>
167
+
168
+ <>
169
+ {config?.preliminaryData?.some(pd => pd.label) && config.visualizationType === 'Line' && (
170
+ <>
171
+ <hr></hr>
172
+ <div className={config.legend.singleRow && isBottomOrSmallViewport ? 'legend-container__inner bottom single-row' : ''}>
173
+ {config?.preliminaryData?.map((pd, index) => {
174
+ return (
175
+ <>
176
+ {pd.label && (
177
+ <div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
178
+ <svg style={{ width: '50px' }} key={index} height={'23px'}>
179
+ {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' />}
180
+ </svg>
181
+ <span style={{}}> {pd.label}</span>
182
+ </div>
183
+ )}
184
+ </>
185
+ )
186
+ })}
187
+ </div>
188
+ </>
189
+ )}
190
+ </>
191
+ </>
192
+ )
193
+ }}
194
+ </LegendOrdinal>
195
+ </aside>
196
+ )
197
+ }
198
+
199
+ export default Legend
@@ -0,0 +1,28 @@
1
+ import { useContext } from 'react'
2
+ import ConfigContext from '../../ConfigContext'
3
+ import LegendComponent from './Legend.Component'
4
+ import { createFormatLabels } from './helpers/createFormatLabels'
5
+
6
+ /* eslint-disable jsx-a11y/no-noninteractive-tabindex, jsx-a11y/no-static-element-interactions */
7
+ const Legend = () => {
8
+ // prettier-ignore
9
+ const {
10
+ config,
11
+ colorScale,
12
+ seriesHighlight,
13
+ highlight,
14
+ tableData,
15
+ highlightReset,
16
+ transformedData: data,
17
+ currentViewport
18
+ } = useContext(ConfigContext)
19
+
20
+ if (!config.legend) return null
21
+ // create fn to reverse labels while legend is Bottom. Legend-right , legend-left works by default.
22
+
23
+ const createLegendLabels = createFormatLabels(config, tableData, data, colorScale)
24
+
25
+ return !['Box Plot', 'Pie'].includes(config.visualizationType) && <LegendComponent config={config} colorScale={colorScale} seriesHighlight={seriesHighlight} highlight={highlight} highlightReset={highlightReset} currentViewport={currentViewport} formatLabels={createLegendLabels} />
26
+ }
27
+
28
+ export default Legend
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ import Legend from './Legend'
2
+
3
+ export default Legend
@@ -15,3 +15,32 @@ export type LineChartProps = {
15
15
  handleTooltipClick: Function
16
16
  tooltipData: any
17
17
  }
18
+
19
+ export interface PreliminaryDataItem {
20
+ style: string
21
+ type: string
22
+ column: string
23
+ value: string
24
+ seriesKey: string
25
+ }
26
+
27
+ export interface DataItem {
28
+ [key: string]: any
29
+ }
30
+
31
+ export interface Config {
32
+ preliminaryData: PreliminaryDataItem[] | []
33
+ }
34
+ export interface StyleProps {
35
+ preliminaryData: PreliminaryDataItem[]
36
+ data: DataItem[]
37
+ stroke: string
38
+ handleLineType: Function
39
+ lineType: string
40
+ seriesKey: 'string'
41
+ }
42
+ export interface Style {
43
+ stroke: string
44
+ strokeWidth: number
45
+ strokeDasharray: string
46
+ }
@@ -0,0 +1,147 @@
1
+ import React from 'react'
2
+ import chroma from 'chroma-js'
3
+ import { type ChartConfig } from '../../../types/ChartConfig'
4
+
5
+ // todo: change this config obj to ChartConfig once its created
6
+ type LineChartCircleProps = {
7
+ circleData: object[]
8
+ config: ChartConfig
9
+ data: object[]
10
+ d?: Object
11
+ displayArea: boolean
12
+ seriesKey: string
13
+ tooltipData: {
14
+ data: []
15
+ tooltipDataX: number
16
+ tooltipDataY: number
17
+ }
18
+ xScale: any
19
+ yScale: any
20
+ yScaleRight: any
21
+ colorScale: any
22
+ parseDate: any
23
+ seriesAxis: string
24
+ dataIndex: number
25
+ mode: 'ISOLATED_POINTS' | 'HOVER_POINTS' | 'ALWAYS_SHOW_POINTS'
26
+ }
27
+
28
+ const LineChartCircle = (props: LineChartCircleProps) => {
29
+ const { config, d, displayArea, seriesKey, tooltipData, xScale, yScale, colorScale, parseDate, yScaleRight, data, circleData, dataIndex, mode } = props
30
+ const { lineDatapointStyle } = config
31
+ const filtered = config?.series.filter(s => s.dataKey === seriesKey)?.[0]
32
+ // If we're not showing the circle, simply return
33
+ const getColor = (displayArea: boolean, colorScale: Function, config: ChartConfig, hoveredKey: string, seriesKey: string) => {
34
+ const seriesLabels = config.runtime.seriesLabels || []
35
+ let color
36
+
37
+ if (displayArea) {
38
+ color = colorScale(seriesLabels[hoveredKey] || seriesKey)
39
+ } else {
40
+ color = 'transparent'
41
+ }
42
+
43
+ if (config.lineDatapointColor === 'Lighter than Line' && color !== 'transparent' && color) {
44
+ color = chroma(color).brighten(1)
45
+ }
46
+ return color
47
+ }
48
+ const getXPos = hoveredXValue => {
49
+ return (config.xAxis.type === 'categorical' ? xScale(hoveredXValue) : xScale(parseDate(hoveredXValue))) + (xScale.bandwidth ? xScale.bandwidth() / 2 : 0)
50
+ }
51
+ if (mode === 'ALWAYS_SHOW_POINTS') {
52
+ if (lineDatapointStyle === 'hidden') return <></>
53
+ const getIndex = seriesKey => config.runtime.seriesLabelsAll.indexOf(seriesKey)
54
+
55
+ if (lineDatapointStyle === 'always show') {
56
+ const isMatch = circleData?.some(cd => cd[config.xAxis.dataKey] === d[config.xAxis.dataKey] && cd[seriesKey] === d[seriesKey])
57
+ if (isMatch) {
58
+ return <></>
59
+ }
60
+ return (
61
+ <circle
62
+ cx={getXPos(d[config.xAxis.dataKey])}
63
+ cy={filtered.axis === 'Right' ? yScaleRight(d[filtered.dataKey]) : yScale(d[filtered.dataKey])}
64
+ r={4.5}
65
+ opacity={d[seriesKey] ? 1 : 0}
66
+ fillOpacity={1}
67
+ fill={getColor(displayArea, colorScale, config, seriesKey, seriesKey)}
68
+ style={{ filter: 'unset', opacity: 1 }}
69
+ />
70
+ )
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
+ }
142
+ }
143
+
144
+ return null
145
+ }
146
+
147
+ export default LineChartCircle
@@ -0,0 +1,45 @@
1
+ import { type PreliminaryDataItem, DataItem, StyleProps, Style } from './LineChartProps'
2
+
3
+ export const createStyles = (props: StyleProps): Style[] => {
4
+ const { preliminaryData, data, stroke, handleLineType, lineType, seriesKey } = props
5
+
6
+ const validPreliminaryData: PreliminaryDataItem[] = preliminaryData.filter(pd => pd.seriesKey && pd.column && pd.value && pd.type && pd.style)
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
+
9
+ let styles: Style[] = []
10
+ const createStyle = (lineStyle): Style => ({
11
+ stroke: stroke,
12
+ strokeWidth: 2,
13
+ strokeDasharray: lineStyle
14
+ })
15
+
16
+ data.forEach((d, index) => {
17
+ let matchingPd: PreliminaryDataItem = getMatchingPd(d)
18
+ let style: Style = matchingPd ? createStyle(handleLineType(matchingPd.style)) : createStyle(handleLineType(lineType))
19
+
20
+ styles.push(style)
21
+
22
+ // If matchingPd exists, update the previous style if there is a previous element
23
+ if (matchingPd && index > 0) {
24
+ styles[index - 1] = createStyle(handleLineType(matchingPd.style))
25
+ }
26
+ })
27
+ return styles as Style[]
28
+ }
29
+
30
+ export const filterCircles = (preliminaryData: PreliminaryDataItem[], data: DataItem[], seriesKey: string): DataItem[] => {
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
+
36
+ // Process data to find matching items
37
+ 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
+ }
42
+ })
43
+
44
+ return filteredData
45
+ }