@cdc/chart 4.24.12 → 4.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cdcchart.js +79611 -78971
- package/examples/feature/boxplot/boxplot.json +2 -157
- package/examples/feature/boxplot/testing.csv +23 -38
- package/examples/feature/tests-non-numerics/example-combo-bar-nonnumeric.json +394 -30
- package/examples/private/ehdi.json +29939 -0
- package/examples/private/not-loading.json +360 -0
- package/index.html +7 -14
- package/package.json +2 -2
- package/src/CdcChart.tsx +92 -1512
- package/src/CdcChartComponent.tsx +1105 -0
- package/src/_stories/Chart.Anchors.stories.tsx +1 -1
- package/src/_stories/Chart.CustomColors.stories.tsx +1 -1
- package/src/_stories/Chart.DynamicSeries.stories.tsx +1 -1
- package/src/_stories/Chart.Legend.Gradient.stories.tsx +2 -2
- package/src/_stories/Chart.ScatterPlot.stories.tsx +19 -0
- package/src/_stories/Chart.tooltip.stories.tsx +1 -2
- package/src/_stories/ChartAnnotation.stories.tsx +1 -1
- package/src/_stories/ChartAxisLabels.stories.tsx +1 -1
- package/src/_stories/ChartAxisTitles.stories.tsx +1 -1
- package/src/_stories/ChartEditor.stories.tsx +1 -1
- package/src/_stories/ChartLine.Suppression.stories.tsx +1 -1
- package/src/_stories/ChartLine.Symbols.stories.tsx +18 -0
- package/src/_stories/ChartPrefixSuffix.stories.tsx +1 -1
- package/src/_stories/_mock/line_chart_symbols.json +437 -0
- package/src/_stories/_mock/scatterplot-image-download.json +1244 -0
- package/src/components/Annotations/components/AnnotationDraggable.tsx +3 -11
- package/src/components/Annotations/components/AnnotationDropdown.tsx +3 -3
- package/src/components/Axis/Categorical.Axis.tsx +3 -4
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +14 -5
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +10 -4
- package/src/components/BarChart/components/BarChart.Vertical.tsx +2 -2
- package/src/components/BoxPlot/BoxPlot.tsx +34 -32
- package/src/components/BoxPlot/helpers/index.ts +108 -18
- package/src/components/DeviationBar.jsx +2 -6
- package/src/components/EditorPanel/EditorPanel.tsx +62 -6
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +4 -0
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +44 -7
- package/src/components/ForestPlot/ForestPlot.tsx +176 -26
- package/src/components/Legend/Legend.Component.tsx +29 -38
- package/src/components/Legend/Legend.Suppression.tsx +3 -5
- package/src/components/Legend/Legend.tsx +2 -2
- package/src/components/Legend/LegendLine.Shape.tsx +51 -0
- package/src/components/Legend/helpers/createFormatLabels.tsx +29 -26
- package/src/components/Legend/helpers/getLegendClasses.ts +20 -38
- package/src/components/Legend/helpers/index.ts +14 -7
- package/src/components/Legend/tests/getLegendClasses.test.ts +3 -20
- package/src/components/LineChart/components/LineChart.Circle.tsx +90 -88
- package/src/components/LineChart/index.tsx +4 -0
- package/src/components/LinearChart.tsx +65 -31
- package/src/components/PairedBarChart.jsx +2 -9
- package/src/components/ZoomBrush.tsx +5 -7
- package/src/data/initial-state.js +6 -3
- package/src/helpers/getBoxPlotConfig.ts +68 -0
- package/src/helpers/getColorScale.ts +28 -0
- package/src/helpers/getComboChartConfig.ts +42 -0
- package/src/helpers/getExcludedData.ts +37 -0
- package/src/helpers/getTopAxis.ts +7 -0
- package/src/hooks/useBarChart.ts +28 -9
- package/src/hooks/{useHighlightedBars.js → useHighlightedBars.ts} +2 -1
- package/src/hooks/useIntersectionObserver.ts +37 -0
- package/src/hooks/useMinMax.ts +4 -0
- package/src/hooks/useReduceData.ts +1 -1
- package/src/hooks/useTooltip.tsx +9 -1
- package/src/index.jsx +1 -0
- package/src/scss/DataTable.scss +0 -5
- package/src/scss/main.scss +30 -115
- package/src/types/ChartConfig.ts +6 -3
- package/src/types/ChartContext.ts +1 -3
- package/src/helpers/getQuartiles.ts +0 -27
- package/src/hooks/useColorScale.ts +0 -50
- package/src/hooks/useIntersectionObserver.jsx +0 -29
- package/src/hooks/useTopAxis.js +0 -6
|
@@ -4,10 +4,10 @@ import Icon from '@cdc/core/components/ui/Icon'
|
|
|
4
4
|
import { Tooltip as ReactTooltip } from 'react-tooltip'
|
|
5
5
|
interface LegendProps {
|
|
6
6
|
config: ChartConfig
|
|
7
|
-
|
|
7
|
+
isLegendBottom: boolean
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
const LegendSuppression: React.FC<LegendProps> = ({ config,
|
|
10
|
+
const LegendSuppression: React.FC<LegendProps> = ({ config, isLegendBottom }) => {
|
|
11
11
|
const { preliminaryData, visualizationType, visualizationSubType, legend } = config
|
|
12
12
|
|
|
13
13
|
const hasOpenCircleEffects = () =>
|
|
@@ -98,15 +98,13 @@ const LegendSuppression: React.FC<LegendProps> = ({ config, isBottomOrSmallViewp
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
const getLegendContainerClass = () =>
|
|
101
|
-
legend.singleRow &&
|
|
101
|
+
legend.singleRow && isLegendBottom ? 'legend-container__inner bottom single-row' : ''
|
|
102
102
|
|
|
103
103
|
const shouldShowSuppressedInfo = () =>
|
|
104
104
|
!config.legend.hideSuppressionLink &&
|
|
105
105
|
config.visualizationSubType !== 'stacked' &&
|
|
106
106
|
preliminaryData?.some(pd => pd.label && pd.type === 'suppression' && pd.value && (pd?.style || pd.symbol))
|
|
107
107
|
|
|
108
|
-
setHasSuppression(shouldShowSuppressedInfo())
|
|
109
|
-
|
|
110
108
|
return (
|
|
111
109
|
<React.Fragment>
|
|
112
110
|
{hasOpenCircleEffects() && (
|
|
@@ -13,7 +13,7 @@ const Legend = forwardRef((props, ref) => {
|
|
|
13
13
|
seriesHighlight,
|
|
14
14
|
highlight,
|
|
15
15
|
tableData,
|
|
16
|
-
|
|
16
|
+
handleShowAll,
|
|
17
17
|
transformedData: data,
|
|
18
18
|
currentViewport,
|
|
19
19
|
dimensions,
|
|
@@ -35,7 +35,7 @@ const Legend = forwardRef((props, ref) => {
|
|
|
35
35
|
colorScale={colorScale}
|
|
36
36
|
seriesHighlight={seriesHighlight}
|
|
37
37
|
highlight={highlight}
|
|
38
|
-
|
|
38
|
+
handleShowAll={handleShowAll}
|
|
39
39
|
currentViewport={currentViewport}
|
|
40
40
|
formatLabels={createLegendLabels}
|
|
41
41
|
/>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { handleLineType } from '../../helpers/handleLineType'
|
|
3
|
+
import { Line } from '@visx/shape'
|
|
4
|
+
import { GlyphDiamond, GlyphCircle, GlyphSquare, GlyphTriangle, GlyphCross, Glyph as CustomGlyph } from '@visx/glyph'
|
|
5
|
+
import { Text } from '@visx/text'
|
|
6
|
+
|
|
7
|
+
// Define glyph shapes
|
|
8
|
+
const Glyphs = [
|
|
9
|
+
GlyphCircle,
|
|
10
|
+
GlyphSquare,
|
|
11
|
+
GlyphTriangle,
|
|
12
|
+
GlyphDiamond,
|
|
13
|
+
GlyphTriangle,
|
|
14
|
+
GlyphCross,
|
|
15
|
+
({ fill }: { fill: string }) => (
|
|
16
|
+
<CustomGlyph>
|
|
17
|
+
{/* Render Filled Pentagon */}
|
|
18
|
+
<Text fill={fill} fontSize={14} textAnchor='middle' verticalAnchor='middle'>
|
|
19
|
+
⬟
|
|
20
|
+
</Text>
|
|
21
|
+
</CustomGlyph>
|
|
22
|
+
)
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
const LegendLineShape = props => {
|
|
26
|
+
const { config, label, index } = props
|
|
27
|
+
const isReversedTriangle = index === 4
|
|
28
|
+
|
|
29
|
+
const Shape =
|
|
30
|
+
Glyphs[config.visual.lineDatapointSymbol === 'standard' && index < config.visual.maximumShapeAmount ? index : 0]
|
|
31
|
+
const transform = `translate(${15}, ${3}) ${isReversedTriangle ? 'rotate(180)' : ''}`
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<svg width={30} height={10} style={{ overflow: 'visible' }} className='me-2'>
|
|
35
|
+
{/* Render line */}
|
|
36
|
+
<Line
|
|
37
|
+
from={{ x: 0, y: 3 }}
|
|
38
|
+
to={{ x: 30, y: 3 }}
|
|
39
|
+
stroke={label.value}
|
|
40
|
+
strokeWidth={2}
|
|
41
|
+
strokeDasharray={handleLineType(config.series[index]?.type || '')}
|
|
42
|
+
/>
|
|
43
|
+
|
|
44
|
+
<g display={config.legend.hasShape ? 'block' : 'none'} transform={transform}>
|
|
45
|
+
<Shape fillOpacity={1} fill={label.value} />
|
|
46
|
+
</g>
|
|
47
|
+
</svg>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default LegendLineShape
|
|
@@ -3,13 +3,22 @@ import { FaStar } from 'react-icons/fa'
|
|
|
3
3
|
import { Label } from '../../../types/Label'
|
|
4
4
|
import { ColorScale, TransformedData } from '../../../types/ChartContext'
|
|
5
5
|
import { ChartConfig } from '../../../types/ChartConfig'
|
|
6
|
+
import _ from 'lodash'
|
|
6
7
|
|
|
7
8
|
export const createFormatLabels =
|
|
8
9
|
(config: ChartConfig, tableData: Object[], data: TransformedData[], colorScale: ColorScale) =>
|
|
9
10
|
(defaultLabels: Label[]): Label[] => {
|
|
10
|
-
const { visualizationType, visualizationSubType, series, runtime } = config
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
const { visualizationType, visualizationSubType, series, runtime, legend } = config
|
|
12
|
+
const sortVertical = labels =>
|
|
13
|
+
legend.verticalSorted
|
|
14
|
+
? _.sortBy(_.cloneDeep(labels), label => {
|
|
15
|
+
const match = label.datum?.match(/-?\d+(\.\d+)?/)
|
|
16
|
+
return match ? parseFloat(match[0]) : Number.MAX_SAFE_INTEGER
|
|
17
|
+
})
|
|
18
|
+
: labels
|
|
19
|
+
const reverseLabels = labels => {
|
|
20
|
+
return config.legend.reverseLabelOrder ? sortVertical(labels).reverse() : sortVertical(labels)
|
|
21
|
+
}
|
|
13
22
|
const colorCode = config.legend?.colorCode
|
|
14
23
|
if (visualizationType === 'Deviation Bar') {
|
|
15
24
|
const [belowColor, aboveColor] = twoColorPalette[config.twoColor.palette]
|
|
@@ -62,7 +71,11 @@ export const createFormatLabels =
|
|
|
62
71
|
// loop through each stage/group/area on the chart and create a label
|
|
63
72
|
config.runtime?.forecastingSeriesKeys?.map((outerGroup, index) => {
|
|
64
73
|
return outerGroup?.stages?.map((stage, index) => {
|
|
65
|
-
let colorValue = sequentialPalettes[stage.color]?.[2]
|
|
74
|
+
let colorValue = sequentialPalettes[stage.color]?.[2]
|
|
75
|
+
? sequentialPalettes[stage.color]?.[2]
|
|
76
|
+
: colorPalettes[stage.color]?.[2]
|
|
77
|
+
? colorPalettes[stage.color]?.[2]
|
|
78
|
+
: '#ccc'
|
|
66
79
|
|
|
67
80
|
const newLabel = {
|
|
68
81
|
datum: stage.key,
|
|
@@ -93,31 +106,21 @@ export const createFormatLabels =
|
|
|
93
106
|
return reverseLabels(seriesLabels)
|
|
94
107
|
}
|
|
95
108
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
109
|
+
if (config.series.some(item => item.name)) {
|
|
110
|
+
const uniqueLabels = Array.from(new Set(config.series.map(d => d.name || d.dataKey))).map((val, i) => ({
|
|
111
|
+
datum: val,
|
|
112
|
+
index: i,
|
|
113
|
+
text: val,
|
|
114
|
+
value: colorScale(val)
|
|
115
|
+
}))
|
|
117
116
|
return reverseLabels(uniqueLabels)
|
|
118
117
|
}
|
|
119
118
|
|
|
120
|
-
if (
|
|
119
|
+
if (
|
|
120
|
+
(config.visualizationType === 'Bar' || config.visualizationType === 'Combo') &&
|
|
121
|
+
config.visualizationSubType === 'regular' &&
|
|
122
|
+
config.suppressedData
|
|
123
|
+
) {
|
|
121
124
|
const lastIndex = defaultLabels.length - 1
|
|
122
125
|
let newLabels = []
|
|
123
126
|
|
|
@@ -1,54 +1,36 @@
|
|
|
1
1
|
import { ChartConfig } from './../../../types/ChartConfig'
|
|
2
2
|
|
|
3
3
|
export const getLegendClasses = (config: ChartConfig) => {
|
|
4
|
-
const { position, singleRow,
|
|
5
|
-
const containerClasses = ['legend-container']
|
|
6
|
-
const innerClasses = ['legend-container__inner']
|
|
7
|
-
|
|
8
|
-
// Handle legend positioning
|
|
9
|
-
switch (position) {
|
|
10
|
-
case 'left':
|
|
11
|
-
containerClasses.push('left')
|
|
12
|
-
break
|
|
13
|
-
case 'right':
|
|
14
|
-
containerClasses.push('right')
|
|
15
|
-
break
|
|
16
|
-
case 'bottom':
|
|
17
|
-
containerClasses.push('bottom')
|
|
18
|
-
innerClasses.push('double-column', 'bottom')
|
|
19
|
-
break
|
|
20
|
-
case 'top':
|
|
21
|
-
containerClasses.push('top')
|
|
22
|
-
innerClasses.push('double-column', 'top')
|
|
23
|
-
break
|
|
24
|
-
}
|
|
4
|
+
const { position, singleRow, verticalSorted, hideBorder } = config.legend
|
|
25
5
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
6
|
+
const containerClassMap = {
|
|
7
|
+
left: 'left',
|
|
8
|
+
right: 'right',
|
|
9
|
+
bottom: 'bottom',
|
|
10
|
+
top: 'top'
|
|
29
11
|
}
|
|
30
12
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
13
|
+
const innerClassMap = {
|
|
14
|
+
bottom: singleRow ? ['single-row', 'bottom'] : ['double-column', 'bottom'],
|
|
15
|
+
top: singleRow ? ['single-row', 'top'] : ['double-column', 'top']
|
|
34
16
|
}
|
|
35
17
|
|
|
36
|
-
|
|
18
|
+
const containerClasses = ['legend-container', containerClassMap[position]].filter(Boolean)
|
|
19
|
+
const innerClasses = ['legend-container__inner', ...(innerClassMap[position] || [])]
|
|
20
|
+
|
|
21
|
+
// Add vertical sorting class for 'bottom' and 'top' positions
|
|
37
22
|
if (['bottom', 'top'].includes(position) && verticalSorted) {
|
|
38
23
|
innerClasses.push('vertical-sorted')
|
|
39
24
|
}
|
|
40
25
|
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (hideBorder.topBottom && ['top', 'bottom'].includes(position)) {
|
|
47
|
-
containerClasses.push('border-0')
|
|
48
|
-
}
|
|
26
|
+
// Add border and padding classes based on position and hideBorder
|
|
27
|
+
const shouldHideBorder = (['right', 'left'].includes(position) || !position) && hideBorder.side
|
|
28
|
+
const shouldHideTopBottomBorder = ['top', 'bottom'].includes(position) && hideBorder.topBottom
|
|
49
29
|
|
|
50
|
-
if (
|
|
51
|
-
containerClasses.push('p-0')
|
|
30
|
+
if (shouldHideBorder || shouldHideTopBottomBorder) {
|
|
31
|
+
containerClasses.push('border-0', 'p-0')
|
|
32
|
+
} else {
|
|
33
|
+
containerClasses.push('p-3')
|
|
52
34
|
}
|
|
53
35
|
|
|
54
36
|
return {
|
|
@@ -10,22 +10,29 @@ export const getGradientConfig = (config, formatLabels, colorScale) => {
|
|
|
10
10
|
return { colors, labels }
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export const getMarginTop = (
|
|
14
|
-
if (!
|
|
13
|
+
export const getMarginTop = (isLegendBottom, config) => {
|
|
14
|
+
if (!isLegendBottom) {
|
|
15
15
|
return '0px'
|
|
16
16
|
}
|
|
17
|
-
if (
|
|
17
|
+
if (isLegendBottom && config.brush?.active) {
|
|
18
18
|
const BRUSH_HEIGHT_MULTIPLIER = 1.5
|
|
19
19
|
return `${config.brush.height * BRUSH_HEIGHT_MULTIPLIER}px`
|
|
20
20
|
}
|
|
21
|
-
return '
|
|
21
|
+
return '27px'
|
|
22
22
|
}
|
|
23
|
-
export const getMarginBottom = (
|
|
23
|
+
export const getMarginBottom = (isLegendBottom, config) => {
|
|
24
24
|
const isLegendTop = config.legend?.position === 'top' && !config.legend.hide
|
|
25
|
-
|
|
25
|
+
const hasSuppression =
|
|
26
|
+
!config.legend.hideSuppressionLink &&
|
|
27
|
+
config.visualizationSubType !== 'stacked' &&
|
|
28
|
+
config.preliminaryData?.some(pd => pd.label && pd.type === 'suppression' && pd.value && (pd?.style || pd.symbol))
|
|
26
29
|
let marginBottom = 0
|
|
27
30
|
|
|
28
|
-
if (isLegendTop) marginBottom =
|
|
31
|
+
if (isLegendTop) marginBottom = 27
|
|
32
|
+
|
|
33
|
+
if (isLegendTop && config.dataFormat?.onlyShowTopPrefixSuffix) marginBottom += 9
|
|
34
|
+
|
|
35
|
+
if (isLegendBottom) marginBottom += 9
|
|
29
36
|
|
|
30
37
|
if (hasSuppression) marginBottom += 40
|
|
31
38
|
|
|
@@ -47,41 +47,24 @@ describe('getLegendClasses', () => {
|
|
|
47
47
|
}
|
|
48
48
|
const result = getLegendClasses(config)
|
|
49
49
|
expect(result.containerClasses).toContain('bottom')
|
|
50
|
-
expect(result.innerClasses).toContain('double-column')
|
|
51
50
|
expect(result.innerClasses).toContain('bottom')
|
|
52
51
|
expect(result.innerClasses).toContain('single-row')
|
|
53
52
|
})
|
|
54
53
|
|
|
55
|
-
it('should return correct classes for
|
|
54
|
+
it('should return correct classes for TOP position with double column', () => {
|
|
56
55
|
const config: ChartConfig = {
|
|
57
56
|
legend: {
|
|
58
57
|
position: 'top',
|
|
59
58
|
singleRow: false,
|
|
60
59
|
reverseLabelOrder: false,
|
|
61
|
-
verticalSorted:
|
|
60
|
+
verticalSorted: false,
|
|
62
61
|
hideBorder: { side: false, topBottom: false }
|
|
63
62
|
}
|
|
64
63
|
}
|
|
65
64
|
const result = getLegendClasses(config)
|
|
66
65
|
expect(result.containerClasses).toContain('top')
|
|
67
|
-
expect(result.innerClasses).toContain('double-column')
|
|
68
66
|
expect(result.innerClasses).toContain('top')
|
|
69
|
-
expect(result.innerClasses).toContain('
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
it('should return correct classes for reverse label order', () => {
|
|
73
|
-
const config: ChartConfig = {
|
|
74
|
-
legend: {
|
|
75
|
-
position: 'bottom',
|
|
76
|
-
singleRow: false,
|
|
77
|
-
reverseLabelOrder: true,
|
|
78
|
-
verticalSorted: false,
|
|
79
|
-
hideBorder: { side: false, topBottom: false }
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
const result = getLegendClasses(config)
|
|
83
|
-
expect(result.innerClasses).toContain('d-flex')
|
|
84
|
-
expect(result.innerClasses).toContain('flex-column-reverse')
|
|
67
|
+
expect(result.innerClasses).toContain('double-column')
|
|
85
68
|
})
|
|
86
69
|
|
|
87
70
|
it('should return correct classes for hide border side', () => {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import chroma from 'chroma-js'
|
|
3
3
|
import { type ChartConfig } from '../../../types/ChartConfig'
|
|
4
|
+
import { GlyphDiamond, GlyphCircle, GlyphSquare, GlyphTriangle, GlyphCross, Glyph as CustomGlyph } from '@visx/glyph'
|
|
5
|
+
import { Text } from '@visx/text'
|
|
4
6
|
|
|
5
|
-
// todo: change this config obj to ChartConfig once its created
|
|
6
7
|
type LineChartCircleProps = {
|
|
7
8
|
circleData: object[]
|
|
8
9
|
config: ChartConfig
|
|
@@ -23,9 +24,25 @@ type LineChartCircleProps = {
|
|
|
23
24
|
parseDate: any
|
|
24
25
|
seriesAxis: string
|
|
25
26
|
dataIndex: number
|
|
27
|
+
seriesIndex: number
|
|
26
28
|
mode: 'ISOLATED_POINTS' | 'HOVER_POINTS' | 'ALWAYS_SHOW_POINTS'
|
|
27
29
|
}
|
|
28
|
-
|
|
30
|
+
const Glyphs = [
|
|
31
|
+
GlyphCircle,
|
|
32
|
+
GlyphSquare,
|
|
33
|
+
GlyphTriangle,
|
|
34
|
+
GlyphDiamond,
|
|
35
|
+
GlyphTriangle,
|
|
36
|
+
GlyphCross,
|
|
37
|
+
({ fill }: { fill: string }) => (
|
|
38
|
+
<CustomGlyph>
|
|
39
|
+
{/* Render Filled Pentagon */}
|
|
40
|
+
<Text fill={fill} fontSize={14} textAnchor='middle' verticalAnchor='middle'>
|
|
41
|
+
⬟
|
|
42
|
+
</Text>
|
|
43
|
+
</CustomGlyph>
|
|
44
|
+
)
|
|
45
|
+
]
|
|
29
46
|
const LineChartCircle = (props: LineChartCircleProps) => {
|
|
30
47
|
const {
|
|
31
48
|
config,
|
|
@@ -42,10 +59,18 @@ const LineChartCircle = (props: LineChartCircleProps) => {
|
|
|
42
59
|
data,
|
|
43
60
|
circleData,
|
|
44
61
|
dataIndex,
|
|
45
|
-
mode
|
|
62
|
+
mode,
|
|
63
|
+
seriesIndex
|
|
46
64
|
} = props
|
|
47
|
-
const { lineDatapointStyle } = config
|
|
65
|
+
const { lineDatapointStyle, visual } = config
|
|
48
66
|
const filtered = config?.runtime?.series.filter(s => s.dataKey === seriesKey)?.[0]
|
|
67
|
+
const Shape =
|
|
68
|
+
Glyphs[
|
|
69
|
+
config.visual.lineDatapointSymbol === 'standard' && seriesIndex < visual.maximumShapeAmount ? seriesIndex : 0
|
|
70
|
+
]
|
|
71
|
+
const isReversedTriangle = seriesIndex === 4
|
|
72
|
+
const transformShape = (top, left) => `translate(${left}, ${top})${isReversedTriangle ? ' rotate(180)' : ''}`
|
|
73
|
+
|
|
49
74
|
// If we're not showing the circle, simply return
|
|
50
75
|
const getColor = (
|
|
51
76
|
displayArea: boolean,
|
|
@@ -55,13 +80,7 @@ const LineChartCircle = (props: LineChartCircleProps) => {
|
|
|
55
80
|
seriesKey: string
|
|
56
81
|
) => {
|
|
57
82
|
const seriesLabels = config.runtime.seriesLabels || []
|
|
58
|
-
let color
|
|
59
|
-
|
|
60
|
-
if (displayArea) {
|
|
61
|
-
color = colorScale(seriesLabels[hoveredKey] || seriesKey)
|
|
62
|
-
} else {
|
|
63
|
-
color = 'transparent'
|
|
64
|
-
}
|
|
83
|
+
let color = displayArea ? colorScale(seriesLabels[hoveredKey] || seriesKey) : ' transparent'
|
|
65
84
|
|
|
66
85
|
if (config.lineDatapointColor === 'Lighter than Line' && color !== 'transparent' && color) {
|
|
67
86
|
color = chroma(color).brighten(1)
|
|
@@ -74,27 +93,30 @@ const LineChartCircle = (props: LineChartCircleProps) => {
|
|
|
74
93
|
(xScale.bandwidth ? xScale.bandwidth() / 2 : 0)
|
|
75
94
|
)
|
|
76
95
|
}
|
|
77
|
-
if (mode === 'ALWAYS_SHOW_POINTS') {
|
|
78
|
-
if (lineDatapointStyle === 'hidden') return <></>
|
|
79
|
-
const getIndex = seriesKey => config.runtime.seriesLabelsAll.indexOf(seriesKey)
|
|
80
|
-
|
|
96
|
+
if (mode === 'ALWAYS_SHOW_POINTS' && lineDatapointStyle !== 'hidden') {
|
|
81
97
|
if (lineDatapointStyle === 'always show') {
|
|
82
98
|
const isMatch = circleData?.some(
|
|
83
99
|
cd => cd[config.xAxis.dataKey] === d[config.xAxis.dataKey] && cd[seriesKey] === d[seriesKey]
|
|
84
100
|
)
|
|
85
|
-
|
|
101
|
+
|
|
102
|
+
if (
|
|
103
|
+
isMatch ||
|
|
104
|
+
!filtered ||
|
|
105
|
+
(visual.maximumShapeAmount === seriesIndex && visual.lineDatapointSymbol === 'standard')
|
|
106
|
+
)
|
|
86
107
|
return <></>
|
|
87
|
-
|
|
108
|
+
const positionLeft = getXPos(d[config.xAxis.dataKey])
|
|
109
|
+
const positionTop = filtered.axis === 'Right' ? yScaleRight(d[filtered.dataKey]) : yScale(d[filtered.dataKey])
|
|
110
|
+
|
|
88
111
|
return (
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
/>
|
|
112
|
+
<g transform={transformShape(positionTop, positionLeft)}>
|
|
113
|
+
<Shape
|
|
114
|
+
opacity={d[seriesKey] ? 1 : 0}
|
|
115
|
+
fillOpacity={1}
|
|
116
|
+
fill={getColor(displayArea, colorScale, config, seriesKey, seriesKey)}
|
|
117
|
+
style={{ filter: 'unset', opacity: 1 }}
|
|
118
|
+
/>
|
|
119
|
+
</g>
|
|
98
120
|
)
|
|
99
121
|
}
|
|
100
122
|
}
|
|
@@ -120,84 +142,64 @@ const LineChartCircle = (props: LineChartCircleProps) => {
|
|
|
120
142
|
|
|
121
143
|
// hoveredSeriesValue = extractNumber(hoveredSeriesValue)
|
|
122
144
|
return tooltipData?.data.map((tooltipItem, index) => {
|
|
123
|
-
let seriesIndex = config.runtime.seriesLabelsAll.indexOf(hoveredXValue)
|
|
124
|
-
|
|
125
145
|
if (isNaN(hoveredSeriesValue)) return <></>
|
|
126
146
|
const isMatch = circleData?.some(cd => cd[config.xAxis.dataKey] === hoveredXValue)
|
|
127
147
|
|
|
128
|
-
if (
|
|
148
|
+
if (
|
|
149
|
+
isMatch ||
|
|
150
|
+
!hoveredSeriesValue ||
|
|
151
|
+
(visual.maximumShapeAmount === seriesIndex && visual.lineDatapointSymbol === 'standard')
|
|
152
|
+
) {
|
|
129
153
|
return <></>
|
|
130
154
|
}
|
|
131
155
|
|
|
156
|
+
const positionTop = hoveredSeriesAxis === 'right' ? yScaleRight(hoveredSeriesValue) : yScale(hoveredSeriesValue)
|
|
157
|
+
const positionLeft = getXPos(hoveredXValue)
|
|
132
158
|
return (
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
/>
|
|
159
|
+
<g transform={transformShape(positionTop, positionLeft)}>
|
|
160
|
+
<Shape
|
|
161
|
+
size={55}
|
|
162
|
+
opacity={1}
|
|
163
|
+
fillOpacity={1}
|
|
164
|
+
fill={getColor(displayArea, colorScale, config, hoveredSeriesKey, seriesKey)}
|
|
165
|
+
style={{ filter: 'unset', opacity: 1 }}
|
|
166
|
+
/>
|
|
167
|
+
</g>
|
|
143
168
|
)
|
|
144
169
|
})
|
|
145
170
|
}
|
|
146
171
|
}
|
|
147
|
-
|
|
148
172
|
if (mode === 'ISOLATED_POINTS') {
|
|
149
173
|
const drawIsolatedPoints = (currentIndex, seriesKey) => {
|
|
150
|
-
let isMatch = false
|
|
151
174
|
const currentPoint = data[currentIndex]
|
|
152
|
-
const previousPoint =
|
|
153
|
-
const nextPoint =
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
res = true
|
|
169
|
-
}
|
|
170
|
-
// Handle points in the middle
|
|
171
|
-
if (currentIndex > 0 && currentIndex < data.length - 1) {
|
|
172
|
-
if (
|
|
173
|
-
currentPoint &&
|
|
174
|
-
currentPoint[seriesKey] &&
|
|
175
|
-
(!previousPoint || !previousPoint[seriesKey]) &&
|
|
176
|
-
(!nextPoint || !nextPoint[seriesKey])
|
|
177
|
-
) {
|
|
178
|
-
res = true
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
if (isMatch) {
|
|
182
|
-
res = false
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return res
|
|
175
|
+
const previousPoint = data[currentIndex - 1] || {}
|
|
176
|
+
const nextPoint = data[currentIndex + 1] || {}
|
|
177
|
+
|
|
178
|
+
const isMatch = circleData.some(item => item?.data[seriesKey] === currentPoint[seriesKey])
|
|
179
|
+
if (isMatch) return false
|
|
180
|
+
|
|
181
|
+
const isFirstPoint = currentIndex === 0 && !nextPoint[seriesKey]
|
|
182
|
+
const isLastPoint = currentIndex === data.length - 1 && !previousPoint[seriesKey]
|
|
183
|
+
const isMiddlePoint =
|
|
184
|
+
currentIndex > 0 &&
|
|
185
|
+
currentIndex < data.length - 1 &&
|
|
186
|
+
currentPoint[seriesKey] &&
|
|
187
|
+
!previousPoint[seriesKey] &&
|
|
188
|
+
!nextPoint[seriesKey]
|
|
189
|
+
|
|
190
|
+
return isFirstPoint || isLastPoint || isMiddlePoint
|
|
186
191
|
}
|
|
187
192
|
|
|
188
|
-
if (
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
/>
|
|
199
|
-
)
|
|
200
|
-
}
|
|
193
|
+
if (drawIsolatedPoints(dataIndex, seriesKey) && !config.series.some(s => s.dynamicCategory)) {
|
|
194
|
+
const positionTop = filtered?.axis === 'Right' ? yScaleRight(d[filtered?.dataKey]) : yScale(d[filtered?.dataKey])
|
|
195
|
+
const positionLeft = getXPos(d[config.xAxis?.dataKey])
|
|
196
|
+
const color = colorScale(config.runtime.seriesLabels[seriesKey])
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<g transform={transformShape(positionTop, positionLeft)}>
|
|
200
|
+
<Shape size={124} stroke={color} fill={color} />
|
|
201
|
+
</g>
|
|
202
|
+
)
|
|
201
203
|
}
|
|
202
204
|
}
|
|
203
205
|
|
|
@@ -150,12 +150,14 @@ const LineChart = (props: LineChartProps) => {
|
|
|
150
150
|
parseDate={parseDate}
|
|
151
151
|
yScaleRight={yScaleRight}
|
|
152
152
|
seriesAxis={seriesAxis}
|
|
153
|
+
seriesIndex={index}
|
|
153
154
|
key={`line-circle--${dataIndex}`}
|
|
154
155
|
/>
|
|
155
156
|
)}
|
|
156
157
|
|
|
157
158
|
<LineChartCircle
|
|
158
159
|
mode='ISOLATED_POINTS'
|
|
160
|
+
seriesIndex={index}
|
|
159
161
|
dataIndex={dataIndex}
|
|
160
162
|
tableData={tableData}
|
|
161
163
|
circleData={circleData}
|
|
@@ -180,6 +182,7 @@ const LineChart = (props: LineChartProps) => {
|
|
|
180
182
|
<>
|
|
181
183
|
{lineDatapointStyle === 'hover' && (
|
|
182
184
|
<LineChartCircle
|
|
185
|
+
seriesIndex={index}
|
|
183
186
|
tableData={tableData}
|
|
184
187
|
dataIndex={0}
|
|
185
188
|
mode='HOVER_POINTS'
|
|
@@ -198,6 +201,7 @@ const LineChart = (props: LineChartProps) => {
|
|
|
198
201
|
/>
|
|
199
202
|
)}
|
|
200
203
|
</>
|
|
204
|
+
|
|
201
205
|
{/* SPLIT LINE */}
|
|
202
206
|
{isSplitLine ? (
|
|
203
207
|
<>
|