@cdc/chart 4.24.7 → 4.24.9
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 +40313 -37543
- package/examples/cases-year.json +13379 -0
- package/examples/gallery/bar-chart-vertical/combo-line-chart.json +76 -15
- package/examples/gallery/bar-chart-vertical/vertical-bar-chart-stacked.json +5 -5
- package/index.html +17 -8
- package/package.json +2 -2
- package/src/CdcChart.tsx +383 -133
- package/src/_stories/Chart.Legend.Gradient.tsx +19 -0
- package/src/_stories/_mock/legend.gradient_mock.json +236 -0
- package/src/components/Annotations/components/AnnotationDraggable.tsx +64 -11
- package/src/components/Axis/Categorical.Axis.tsx +145 -0
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +4 -3
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +1 -1
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +2 -5
- package/src/components/BarChart/components/BarChart.Vertical.tsx +17 -8
- package/src/components/BarChart/helpers/index.ts +5 -16
- package/src/components/BrushChart.tsx +205 -0
- package/src/components/EditorPanel/EditorPanel.tsx +1766 -509
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +19 -5
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +190 -37
- package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +43 -7
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +4 -4
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +1 -11
- package/src/components/EditorPanel/editor-panel.scss +16 -3
- package/src/components/EditorPanel/{useEditorPermissions.js → useEditorPermissions.ts} +90 -19
- package/src/components/Legend/Legend.Component.tsx +185 -193
- package/src/components/Legend/Legend.Suppression.tsx +146 -0
- package/src/components/Legend/Legend.tsx +21 -5
- package/src/components/Legend/helpers/index.ts +33 -3
- package/src/components/LegendWrapper.tsx +26 -0
- package/src/components/LineChart/LineChartProps.ts +1 -18
- package/src/components/LineChart/components/LineChart.BumpCircle.tsx +103 -0
- package/src/components/LineChart/components/LineChart.Circle.tsx +47 -8
- package/src/components/LineChart/helpers.ts +55 -11
- package/src/components/LineChart/index.tsx +113 -38
- package/src/components/LinearChart.tsx +1366 -0
- package/src/components/PieChart/PieChart.tsx +74 -17
- package/src/components/Sankey/index.tsx +22 -16
- package/src/components/Sparkline/components/SparkLine.tsx +2 -2
- package/src/data/initial-state.js +13 -3
- package/src/hooks/useLegendClasses.ts +52 -15
- package/src/hooks/useMinMax.ts +4 -4
- package/src/hooks/useScales.ts +34 -24
- package/src/hooks/useTooltip.tsx +85 -22
- package/src/scss/DataTable.scss +2 -1
- package/src/scss/main.scss +107 -14
- package/src/types/ChartConfig.ts +34 -8
- package/src/types/ChartContext.ts +5 -4
- package/examples/feature/line/line-chart.json +0 -449
- package/src/components/BrushHandle.jsx +0 -17
- package/src/components/LineChart/index.scss +0 -1
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { ChartConfig } from '../../types/ChartConfig'
|
|
3
|
+
import Icon from '@cdc/core/components/ui/Icon'
|
|
4
|
+
import { Tooltip as ReactTooltip } from 'react-tooltip'
|
|
5
|
+
interface LegendProps {
|
|
6
|
+
config: ChartConfig
|
|
7
|
+
isBottomOrSmallViewport: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const LegendSuppression: React.FC<LegendProps> = ({ config, isBottomOrSmallViewport }) => {
|
|
11
|
+
const { preliminaryData, visualizationType, visualizationSubType, legend } = config
|
|
12
|
+
|
|
13
|
+
const hasOpenCircleEffects = () =>
|
|
14
|
+
preliminaryData?.some(pd => pd.label && pd.type === 'effect' && pd.style === 'Open Circles') &&
|
|
15
|
+
['Line', 'Combo'].includes(visualizationType)
|
|
16
|
+
|
|
17
|
+
const shouldShowSuppressedLabels = () =>
|
|
18
|
+
!legend.hideSuppressedLabels &&
|
|
19
|
+
preliminaryData?.some(
|
|
20
|
+
pd => pd.label && pd.displayLegend && pd.type === 'suppression' && pd.value && (pd?.style || pd.symbol)
|
|
21
|
+
) &&
|
|
22
|
+
((visualizationType === 'Bar' && visualizationSubType === 'regular') ||
|
|
23
|
+
visualizationType === 'Line' ||
|
|
24
|
+
visualizationType === 'Combo')
|
|
25
|
+
|
|
26
|
+
const renderEffectItems = () =>
|
|
27
|
+
preliminaryData?.map(
|
|
28
|
+
(pd, index) =>
|
|
29
|
+
pd.label &&
|
|
30
|
+
pd.type === 'effect' &&
|
|
31
|
+
pd.style && (
|
|
32
|
+
<div key={index} className='legend-preliminary'>
|
|
33
|
+
<span className={pd.symbol}>{pd.lineCode}</span>
|
|
34
|
+
<p>{pd.label}</p>
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
const handleLinkClick = event => {
|
|
39
|
+
// prevent defintion link to change URl
|
|
40
|
+
event.preventDefault()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const renderSuppressedItems = () => {
|
|
44
|
+
const getStyle = displayGray => {
|
|
45
|
+
if (displayGray) {
|
|
46
|
+
return {
|
|
47
|
+
color: '#777772'
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
return preliminaryData?.map((pd, index) => {
|
|
53
|
+
if (!pd.displayLegend || pd.type !== 'suppression') return null
|
|
54
|
+
|
|
55
|
+
const baseClass = 'legend-preliminary'
|
|
56
|
+
const itemKey = index + visualizationType
|
|
57
|
+
|
|
58
|
+
if (visualizationType === 'Bar') {
|
|
59
|
+
return (
|
|
60
|
+
<div style={getStyle(pd.displayGray)} key={itemKey} className={`${baseClass} ${pd.symbol}`}>
|
|
61
|
+
<span className={pd.symbol}>{pd.iconCode}</span>
|
|
62
|
+
<p className={pd.type}>{pd.label}</p>
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (visualizationType === 'Line') {
|
|
68
|
+
return (
|
|
69
|
+
<div style={getStyle(pd.displayGray)} key={itemKey} className={baseClass}>
|
|
70
|
+
<span>{pd.lineCode}</span>
|
|
71
|
+
<p className={pd.type}>{pd.label}</p>
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (visualizationType === 'Combo') {
|
|
77
|
+
return (
|
|
78
|
+
<React.Fragment>
|
|
79
|
+
{pd.symbol && pd.iconCode && (
|
|
80
|
+
<div style={getStyle(pd.displayGray)} key={itemKey} className={`${baseClass} ${pd.symbol}`}>
|
|
81
|
+
<span className={pd.symbol}>{pd.iconCode}</span>
|
|
82
|
+
<p className={pd.type}>{pd.label}</p>
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
|
|
86
|
+
{pd.style && pd.lineCode && (
|
|
87
|
+
<div style={getStyle(pd.displayGray)} key={itemKey} className={baseClass}>
|
|
88
|
+
<span>{pd.lineCode}</span>
|
|
89
|
+
<p>{pd.label}</p>
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
</React.Fragment>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return null
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const getLegendContainerClass = () =>
|
|
101
|
+
legend.singleRow && isBottomOrSmallViewport ? 'legend-container__inner bottom single-row' : ''
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<React.Fragment>
|
|
105
|
+
{hasOpenCircleEffects() && (
|
|
106
|
+
<React.Fragment>
|
|
107
|
+
<hr />
|
|
108
|
+
<div className={getLegendContainerClass()}>{renderEffectItems()}</div>
|
|
109
|
+
</React.Fragment>
|
|
110
|
+
)}
|
|
111
|
+
|
|
112
|
+
{shouldShowSuppressedLabels() && (
|
|
113
|
+
<React.Fragment>
|
|
114
|
+
<hr />
|
|
115
|
+
<div className={getLegendContainerClass()}>{renderSuppressedItems()}</div>
|
|
116
|
+
</React.Fragment>
|
|
117
|
+
)}
|
|
118
|
+
{!config.legend.hideSuppressionLink &&
|
|
119
|
+
config.visualizationSubType !== 'stacked' &&
|
|
120
|
+
preliminaryData?.some(pd => pd.label && pd.type === 'suppression' && pd.value && (pd?.style || pd.symbol)) && (
|
|
121
|
+
<div className='legend-container__outer definition-link'>
|
|
122
|
+
<Icon alt='info-icon' display='info' />
|
|
123
|
+
<p>
|
|
124
|
+
This chart contains
|
|
125
|
+
<a // prettier-ignore
|
|
126
|
+
onClick={handleLinkClick}
|
|
127
|
+
data-tooltip-content='Data is suppressed to maintain statistical reliability. This occurs when the number of respondents or reported values does not meet the minimum reporting threshold.'
|
|
128
|
+
data-tooltip-id='my-tooltip'
|
|
129
|
+
href='no-router-link'
|
|
130
|
+
>
|
|
131
|
+
suppressed data
|
|
132
|
+
</a>
|
|
133
|
+
</p>
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
|
|
137
|
+
<ReactTooltip // prettier-ignore
|
|
138
|
+
id='my-tooltip'
|
|
139
|
+
variant='light'
|
|
140
|
+
style={{ background: `rgba(255,255,255, ${config.tooltips.opacity / 100})`, color: 'black', maxWidth: '100%' }}
|
|
141
|
+
/>
|
|
142
|
+
</React.Fragment>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export default LegendSuppression
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useContext, forwardRef } from 'react'
|
|
1
|
+
import { useContext, forwardRef, Fragment } from 'react'
|
|
2
2
|
import ConfigContext from '../../ConfigContext'
|
|
3
3
|
import LegendComponent from './Legend.Component'
|
|
4
4
|
import { createFormatLabels } from './helpers/createFormatLabels'
|
|
@@ -7,6 +7,7 @@ import { createFormatLabels } from './helpers/createFormatLabels'
|
|
|
7
7
|
const Legend = forwardRef((props, ref) => {
|
|
8
8
|
// prettier-ignore
|
|
9
9
|
const {
|
|
10
|
+
// prettier-ignore
|
|
10
11
|
config,
|
|
11
12
|
colorScale,
|
|
12
13
|
seriesHighlight,
|
|
@@ -14,17 +15,32 @@ const Legend = forwardRef((props, ref) => {
|
|
|
14
15
|
tableData,
|
|
15
16
|
highlightReset,
|
|
16
17
|
transformedData: data,
|
|
17
|
-
currentViewport
|
|
18
|
+
currentViewport,
|
|
19
|
+
dimensions,
|
|
20
|
+
getTextWidth,
|
|
18
21
|
} = useContext(ConfigContext)
|
|
19
|
-
|
|
20
22
|
if (!config.legend) return null
|
|
21
23
|
// create fn to reverse labels while legend is Bottom. Legend-right , legend-left works by default.
|
|
22
24
|
|
|
23
25
|
const createLegendLabels = createFormatLabels(config, tableData, data, colorScale)
|
|
24
26
|
|
|
25
27
|
return (
|
|
26
|
-
!['Box Plot'
|
|
27
|
-
<
|
|
28
|
+
!['Box Plot'].includes(config.visualizationType) && (
|
|
29
|
+
<Fragment>
|
|
30
|
+
<LegendComponent
|
|
31
|
+
getTextWidth={getTextWidth}
|
|
32
|
+
dimensions={dimensions}
|
|
33
|
+
ref={ref}
|
|
34
|
+
skipId={props.skipId || 'legend'}
|
|
35
|
+
config={config}
|
|
36
|
+
colorScale={colorScale}
|
|
37
|
+
seriesHighlight={seriesHighlight}
|
|
38
|
+
highlight={highlight}
|
|
39
|
+
highlightReset={highlightReset}
|
|
40
|
+
currentViewport={currentViewport}
|
|
41
|
+
formatLabels={createLegendLabels}
|
|
42
|
+
/>
|
|
43
|
+
</Fragment>
|
|
28
44
|
)
|
|
29
45
|
)
|
|
30
46
|
})
|
|
@@ -1,5 +1,35 @@
|
|
|
1
|
-
export const getMarginTop = (isBottomOrSmallViewport, isBrushActive) => {
|
|
1
|
+
export const getMarginTop = (isBottomOrSmallViewport, isBrushActive, legend) => {
|
|
2
2
|
if (!isBottomOrSmallViewport) return '0px'
|
|
3
|
-
if (isBrushActive) return '35px'
|
|
4
|
-
|
|
3
|
+
if (isBrushActive && legend.position === 'bottom') return '35px'
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export const getGradientConfig = (config, formatLabels, colorScale) => {
|
|
7
|
+
const defaultValue = [{ datum: '', index: 0, text: '', value: '' }]
|
|
8
|
+
|
|
9
|
+
const formatted = formatLabels(defaultValue)
|
|
10
|
+
const colors = config.legend.colorCode ? formatted.map(label => label?.value) : colorScale?.range() ?? []
|
|
11
|
+
const labels = config.legend.colorCode
|
|
12
|
+
? formatted.map(label => label?.text || label?.datum)
|
|
13
|
+
: colorScale?.domain() ?? []
|
|
14
|
+
|
|
15
|
+
return { colors, labels }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const getMarginBottom = (isBottomOrSmallViewport, config) => {
|
|
19
|
+
const isSuppressedActive = config.preliminaryData.some(pd => pd.label) && !config.legend.hideSuppressionLink
|
|
20
|
+
|
|
21
|
+
const isLegendTop = config.legend?.position === 'top' && !config.legend.hide
|
|
22
|
+
|
|
23
|
+
let marginBottom = '0px'
|
|
24
|
+
if (isLegendTop && !isSuppressedActive) {
|
|
25
|
+
marginBottom = config.legend.hideBorder.topBottom ? '15px' : '25px'
|
|
26
|
+
}
|
|
27
|
+
if (isLegendTop && isSuppressedActive) {
|
|
28
|
+
marginBottom = '75px'
|
|
29
|
+
}
|
|
30
|
+
if (isBottomOrSmallViewport && isSuppressedActive) {
|
|
31
|
+
marginBottom = '45px'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return marginBottom
|
|
5
35
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React, { useContext } from 'react'
|
|
2
|
+
import ConfigContext from '../ConfigContext'
|
|
3
|
+
|
|
4
|
+
type LegendWrapperProps = {
|
|
5
|
+
children: React.ReactNode
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const LegendWrapper: React.FC<LegendWrapperProps> = props => {
|
|
9
|
+
const { children } = props
|
|
10
|
+
|
|
11
|
+
const { config, currentViewport } = useContext(ConfigContext)
|
|
12
|
+
|
|
13
|
+
const getLegendWrappingClasses = () => {
|
|
14
|
+
let classes = ['legend-wrapper', 'd-flex', 'flex-nowrap', 'w-100']
|
|
15
|
+
const { legend } = config
|
|
16
|
+
if (legend.position === 'bottom' || legend.position === 'top' || ['xxs', 'xs', 'sm'].includes(currentViewport)) {
|
|
17
|
+
classes = classes.filter(item => item !== 'flex-nowrap')
|
|
18
|
+
classes.push('flex-wrap')
|
|
19
|
+
}
|
|
20
|
+
return classes.join(' ')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return <div className={getLegendWrappingClasses()}>{...children}</div>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default LegendWrapper
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// todo: review tooltipData type
|
|
2
2
|
// todo: review svgRef type
|
|
3
|
+
import { type PreliminaryDataItem } from '../../types/ChartConfig'
|
|
3
4
|
export type LineChartProps = {
|
|
4
5
|
xScale: Function
|
|
5
6
|
yScale: Function
|
|
@@ -16,24 +17,6 @@ export type LineChartProps = {
|
|
|
16
17
|
tooltipData: any
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
export interface PreliminaryDataItem {
|
|
20
|
-
column: string
|
|
21
|
-
displayLegend: boolean
|
|
22
|
-
displayTable: boolean
|
|
23
|
-
displayTooltip: boolean
|
|
24
|
-
iconCode: string
|
|
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
|
|
35
|
-
}
|
|
36
|
-
|
|
37
20
|
export interface DataItem {
|
|
38
21
|
[key: string]: any
|
|
39
22
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { Group } from '@visx/group'
|
|
3
|
+
import { type Column } from '@cdc/core/types/Column'
|
|
4
|
+
import React from 'react'
|
|
5
|
+
import { type ChartConfig } from '../../../types/ChartConfig'
|
|
6
|
+
|
|
7
|
+
type LineChartBumpCircleProp = {
|
|
8
|
+
config: ChartConfig,
|
|
9
|
+
xScale: any,
|
|
10
|
+
yScale: any,
|
|
11
|
+
parseDate: any
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const LineChartBumpCircle = (props: LineChartBumpCircleProp) => {
|
|
15
|
+
const { config, xScale, yScale, parseDate } = props
|
|
16
|
+
|
|
17
|
+
// get xScale and yScale...
|
|
18
|
+
if (!config?.runtime?.series) return
|
|
19
|
+
|
|
20
|
+
const handleX = xValue => {
|
|
21
|
+
if (config.xAxis.type === 'date') {
|
|
22
|
+
return parseDate(xValue).getTime()
|
|
23
|
+
}
|
|
24
|
+
if (config.xAxis.type === 'date-time') {
|
|
25
|
+
return new Date(xValue)
|
|
26
|
+
}
|
|
27
|
+
if (config.xAxis.type === 'categorical') {
|
|
28
|
+
return xValue
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const checkBandScale = xValue => {
|
|
33
|
+
return xScale.bandwidth ? xScale.bandwidth() / 2 + Number(xValue) : Number(xValue)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
const getListItems = dataRow => {
|
|
38
|
+
return Object.values(config.columns)
|
|
39
|
+
?.filter(column => column.tooltips).map(column => {
|
|
40
|
+
const label = column.label || column.name;
|
|
41
|
+
return `
|
|
42
|
+
<li className='tooltip-body'>
|
|
43
|
+
<strong>${label}</strong>: ${dataRow[column.name]}
|
|
44
|
+
</li>`;
|
|
45
|
+
})
|
|
46
|
+
.join(' ');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const getTooltip = dataRow => `<ul> ${getListItems(dataRow)} </ul>`
|
|
50
|
+
|
|
51
|
+
const circles = config.runtime?.series.map((series) => {
|
|
52
|
+
return config.data.map((d, dataIndex) => {
|
|
53
|
+
let series_dataKey = d[series.dataKey]
|
|
54
|
+
let axis_dataKey = d[config.xAxis.dataKey]
|
|
55
|
+
return (
|
|
56
|
+
<React.Fragment key={`bump-circle-${series_dataKey}-${dataIndex}`}>
|
|
57
|
+
<Group left={Number(config.runtime.yAxis.size)}>
|
|
58
|
+
{series_dataKey && (
|
|
59
|
+
<>
|
|
60
|
+
<circle
|
|
61
|
+
key={`bump-circle-${series_dataKey}-${dataIndex}`}
|
|
62
|
+
data-tooltip-html={getTooltip(d)}
|
|
63
|
+
data-tooltip-id={`bump-chart`}
|
|
64
|
+
r={10}
|
|
65
|
+
cx={Number(checkBandScale(xScale(handleX(axis_dataKey))))}
|
|
66
|
+
cy={Number(yScale(series_dataKey))}
|
|
67
|
+
stroke='#CACACA'
|
|
68
|
+
strokeWidth={1}
|
|
69
|
+
fill='#E5E4E2'
|
|
70
|
+
/>
|
|
71
|
+
{series_dataKey.toString().length === 2 ? (
|
|
72
|
+
// prettier-ignore
|
|
73
|
+
<text
|
|
74
|
+
x={Number(checkBandScale(xScale(handleX(axis_dataKey)))) - 7}
|
|
75
|
+
y={Number(yScale(series_dataKey)) + 4}
|
|
76
|
+
fill='#000000'
|
|
77
|
+
fontSize={11.5}
|
|
78
|
+
>
|
|
79
|
+
{series_dataKey}
|
|
80
|
+
</text>
|
|
81
|
+
) : (
|
|
82
|
+
// prettier-ignore
|
|
83
|
+
<text
|
|
84
|
+
x={Number(checkBandScale(xScale(handleX(axis_dataKey)))) - 4}
|
|
85
|
+
y={Number(yScale(series_dataKey)) + 4}
|
|
86
|
+
fill='#000000'
|
|
87
|
+
fontSize={11.5}
|
|
88
|
+
>
|
|
89
|
+
{series_dataKey}
|
|
90
|
+
</text>
|
|
91
|
+
)}
|
|
92
|
+
</>
|
|
93
|
+
)}
|
|
94
|
+
</Group>
|
|
95
|
+
</React.Fragment>
|
|
96
|
+
)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
return <>{circles}</>
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export default LineChartBumpCircle
|
|
@@ -27,11 +27,33 @@ type LineChartCircleProps = {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const LineChartCircle = (props: LineChartCircleProps) => {
|
|
30
|
-
const {
|
|
30
|
+
const {
|
|
31
|
+
config,
|
|
32
|
+
d,
|
|
33
|
+
tableData,
|
|
34
|
+
displayArea,
|
|
35
|
+
seriesKey,
|
|
36
|
+
tooltipData,
|
|
37
|
+
xScale,
|
|
38
|
+
yScale,
|
|
39
|
+
colorScale,
|
|
40
|
+
parseDate,
|
|
41
|
+
yScaleRight,
|
|
42
|
+
data,
|
|
43
|
+
circleData,
|
|
44
|
+
dataIndex,
|
|
45
|
+
mode
|
|
46
|
+
} = props
|
|
31
47
|
const { lineDatapointStyle } = config
|
|
32
|
-
const filtered = config?.series.filter(s => s.dataKey === seriesKey)?.[0]
|
|
48
|
+
const filtered = config?.runtime?.series.filter(s => s.dataKey === seriesKey)?.[0]
|
|
33
49
|
// If we're not showing the circle, simply return
|
|
34
|
-
const getColor = (
|
|
50
|
+
const getColor = (
|
|
51
|
+
displayArea: boolean,
|
|
52
|
+
colorScale: Function,
|
|
53
|
+
config: ChartConfig,
|
|
54
|
+
hoveredKey: string,
|
|
55
|
+
seriesKey: string
|
|
56
|
+
) => {
|
|
35
57
|
const seriesLabels = config.runtime.seriesLabels || []
|
|
36
58
|
let color
|
|
37
59
|
|
|
@@ -47,14 +69,19 @@ const LineChartCircle = (props: LineChartCircleProps) => {
|
|
|
47
69
|
return color
|
|
48
70
|
}
|
|
49
71
|
const getXPos = hoveredXValue => {
|
|
50
|
-
return (
|
|
72
|
+
return (
|
|
73
|
+
(config.xAxis.type === 'categorical' ? xScale(hoveredXValue) : xScale(parseDate(hoveredXValue))) +
|
|
74
|
+
(xScale.bandwidth ? xScale.bandwidth() / 2 : 0)
|
|
75
|
+
)
|
|
51
76
|
}
|
|
52
77
|
if (mode === 'ALWAYS_SHOW_POINTS') {
|
|
53
78
|
if (lineDatapointStyle === 'hidden') return <></>
|
|
54
79
|
const getIndex = seriesKey => config.runtime.seriesLabelsAll.indexOf(seriesKey)
|
|
55
80
|
|
|
56
81
|
if (lineDatapointStyle === 'always show') {
|
|
57
|
-
const isMatch = circleData?.some(
|
|
82
|
+
const isMatch = circleData?.some(
|
|
83
|
+
cd => cd[config.xAxis.dataKey] === d[config.xAxis.dataKey] && cd[seriesKey] === d[seriesKey]
|
|
84
|
+
)
|
|
58
85
|
if (isMatch) {
|
|
59
86
|
return <></>
|
|
60
87
|
}
|
|
@@ -98,7 +125,7 @@ const LineChartCircle = (props: LineChartCircleProps) => {
|
|
|
98
125
|
if (isNaN(hoveredSeriesValue)) return <></>
|
|
99
126
|
const isMatch = circleData?.some(cd => cd[config.xAxis.dataKey] === hoveredXValue)
|
|
100
127
|
|
|
101
|
-
if (isMatch) {
|
|
128
|
+
if (isMatch || !hoveredSeriesValue) {
|
|
102
129
|
return <></>
|
|
103
130
|
}
|
|
104
131
|
|
|
@@ -135,7 +162,12 @@ const LineChartCircle = (props: LineChartCircleProps) => {
|
|
|
135
162
|
}
|
|
136
163
|
// Handle points in the middle
|
|
137
164
|
if (currentIndex > 0 && currentIndex < data.length - 1) {
|
|
138
|
-
if (
|
|
165
|
+
if (
|
|
166
|
+
currentPoint &&
|
|
167
|
+
currentPoint[seriesKey] &&
|
|
168
|
+
(!previousPoint || !previousPoint[seriesKey]) &&
|
|
169
|
+
(!nextPoint || !nextPoint[seriesKey])
|
|
170
|
+
) {
|
|
139
171
|
res = true
|
|
140
172
|
}
|
|
141
173
|
}
|
|
@@ -146,7 +178,14 @@ const LineChartCircle = (props: LineChartCircleProps) => {
|
|
|
146
178
|
if (mode) {
|
|
147
179
|
if (drawIsolatedPoints(dataIndex, seriesKey)) {
|
|
148
180
|
return (
|
|
149
|
-
<circle
|
|
181
|
+
<circle
|
|
182
|
+
cx={getXPos(d[config.xAxis?.dataKey])}
|
|
183
|
+
cy={filtered?.axis === 'Right' ? yScaleRight(d[filtered?.dataKey]) : yScale(d[filtered?.dataKey])}
|
|
184
|
+
r={5.3}
|
|
185
|
+
strokeWidth={2}
|
|
186
|
+
stroke={colorScale(config.runtime.seriesLabels[seriesKey])}
|
|
187
|
+
fill={colorScale(config.runtime?.seriesLabels[seriesKey])}
|
|
188
|
+
/>
|
|
150
189
|
)
|
|
151
190
|
}
|
|
152
191
|
}
|
|
@@ -1,10 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DataItem, StyleProps, Style } from './LineChartProps'
|
|
2
|
+
import { PreliminaryDataItem } from '../../types/ChartConfig'
|
|
2
3
|
import _ from 'lodash'
|
|
3
4
|
export const createStyles = (props: StyleProps): Style[] => {
|
|
4
5
|
const { preliminaryData, data, stroke, strokeWidth, handleLineType, lineType, seriesKey } = props
|
|
5
6
|
|
|
6
|
-
const validPreliminaryData: PreliminaryDataItem[] = preliminaryData.filter(
|
|
7
|
-
|
|
7
|
+
const validPreliminaryData: PreliminaryDataItem[] = preliminaryData.filter(
|
|
8
|
+
pd => pd.seriesKey && pd.column && pd.value && pd.type && pd.style && pd.type === 'effect'
|
|
9
|
+
)
|
|
10
|
+
const getMatchingPd = (point: DataItem): PreliminaryDataItem =>
|
|
11
|
+
validPreliminaryData.find(
|
|
12
|
+
pd =>
|
|
13
|
+
pd.seriesKey === seriesKey &&
|
|
14
|
+
point[pd.column] === pd.value &&
|
|
15
|
+
pd.type === 'effect' &&
|
|
16
|
+
pd.style !== 'Open Circles'
|
|
17
|
+
)
|
|
8
18
|
|
|
9
19
|
let styles: Style[] = []
|
|
10
20
|
const createStyle = (lineStyle): Style => ({
|
|
@@ -15,7 +25,9 @@ export const createStyles = (props: StyleProps): Style[] => {
|
|
|
15
25
|
|
|
16
26
|
data.forEach((d, index) => {
|
|
17
27
|
let matchingPd: PreliminaryDataItem = getMatchingPd(d)
|
|
18
|
-
let style: Style = matchingPd
|
|
28
|
+
let style: Style = matchingPd
|
|
29
|
+
? createStyle(handleLineType(matchingPd.style))
|
|
30
|
+
: createStyle(handleLineType(lineType))
|
|
19
31
|
|
|
20
32
|
styles.push(style)
|
|
21
33
|
|
|
@@ -27,14 +39,31 @@ export const createStyles = (props: StyleProps): Style[] => {
|
|
|
27
39
|
return styles as Style[]
|
|
28
40
|
}
|
|
29
41
|
|
|
30
|
-
export const filterCircles = (
|
|
42
|
+
export const filterCircles = (
|
|
43
|
+
preliminaryData: PreliminaryDataItem[],
|
|
44
|
+
data: DataItem[],
|
|
45
|
+
seriesKey: string
|
|
46
|
+
): DataItem[] => {
|
|
31
47
|
// Filter and map preliminaryData to get circlesFiltered
|
|
32
|
-
const circlesFiltered = preliminaryData
|
|
48
|
+
const circlesFiltered = preliminaryData
|
|
49
|
+
?.filter(item => item.style.includes('Circles') && item.type === 'effect')
|
|
50
|
+
.map(item => ({
|
|
51
|
+
column: item.column,
|
|
52
|
+
value: item.value,
|
|
53
|
+
seriesKey: item.seriesKey,
|
|
54
|
+
circleSize: item.circleSize,
|
|
55
|
+
style: item.style
|
|
56
|
+
}))
|
|
33
57
|
const filteredData = []
|
|
34
58
|
// Process data to find matching items
|
|
35
59
|
data.forEach(item => {
|
|
36
60
|
circlesFiltered.forEach(fc => {
|
|
37
|
-
if (
|
|
61
|
+
if (
|
|
62
|
+
item[fc.column] === fc.value &&
|
|
63
|
+
fc.seriesKey === seriesKey &&
|
|
64
|
+
item[seriesKey] &&
|
|
65
|
+
fc.style === 'Open Circles'
|
|
66
|
+
) {
|
|
38
67
|
const result = {
|
|
39
68
|
data: item,
|
|
40
69
|
size: fc.circleSize,
|
|
@@ -42,7 +71,12 @@ export const filterCircles = (preliminaryData: PreliminaryDataItem[], data: Data
|
|
|
42
71
|
}
|
|
43
72
|
filteredData.push(result)
|
|
44
73
|
}
|
|
45
|
-
if (
|
|
74
|
+
if (
|
|
75
|
+
(!fc.value || item[fc.column] === fc.value) &&
|
|
76
|
+
fc.seriesKey === seriesKey &&
|
|
77
|
+
item[seriesKey] &&
|
|
78
|
+
fc.style === 'Filled Circles'
|
|
79
|
+
) {
|
|
46
80
|
const result = {
|
|
47
81
|
data: item,
|
|
48
82
|
size: fc.circleSize,
|
|
@@ -71,7 +105,9 @@ const handleFirstIndex = (data, seriesKey, preliminaryData) => {
|
|
|
71
105
|
// Function to check if a data item matches the suppression criteria
|
|
72
106
|
const isSuppressed = pd => {
|
|
73
107
|
if (pd.type === 'effect' || pd.hideLineStyle) return
|
|
74
|
-
return
|
|
108
|
+
return (
|
|
109
|
+
pd.type == 'suppression' && pd.value === firstIndexDataItem[seriesKey] && (!pd.column || pd.column === seriesKey)
|
|
110
|
+
)
|
|
75
111
|
}
|
|
76
112
|
|
|
77
113
|
// Find applicable suppression data for the first item
|
|
@@ -107,7 +143,13 @@ const handleLastIndex = (data, seriesKey, preliminaryData) => {
|
|
|
107
143
|
let lastAddedIndex = -1 // Tracks the last index added to the result
|
|
108
144
|
preliminaryData?.forEach(pd => {
|
|
109
145
|
if (pd.type === 'effect') return
|
|
110
|
-
if (
|
|
146
|
+
if (
|
|
147
|
+
data[data.length - 1][seriesKey] === pd.value &&
|
|
148
|
+
pd.style &&
|
|
149
|
+
(!pd.column || pd.column === seriesKey) &&
|
|
150
|
+
pd.type == 'suppression' &&
|
|
151
|
+
!pd.hideLineStyle
|
|
152
|
+
) {
|
|
111
153
|
const lastIndex = data.length - 1
|
|
112
154
|
const modifiedItem = { ...data[lastIndex], [seriesKey]: 0 }
|
|
113
155
|
result.data.push(modifiedItem)
|
|
@@ -157,7 +199,9 @@ function handleMiddleIndices(data, seriesKey, dataKey, preliminaryData) {
|
|
|
157
199
|
}
|
|
158
200
|
|
|
159
201
|
// Find and add the next calculable object
|
|
160
|
-
const nextIndex = data
|
|
202
|
+
const nextIndex = data
|
|
203
|
+
.slice(i + 1)
|
|
204
|
+
.findIndex(item => item[seriesKey] !== targetValue && isCalculable(item[seriesKey]))
|
|
161
205
|
if (nextIndex !== -1) {
|
|
162
206
|
result.data.push(data[i + 1 + nextIndex])
|
|
163
207
|
}
|