@cdc/chart 4.25.3-6 → 4.25.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.
- package/dist/cdcchart-1a1724a1.es.js +4886 -0
- package/dist/cdcchart.js +51921 -76151
- package/index.html +1 -1
- package/package.json +2 -2
- package/src/CdcChart.tsx +1 -22
- package/src/CdcChartComponent.tsx +27 -13
- package/src/_stories/Chart.CI.stories.tsx +33 -0
- package/src/_stories/Chart.Legend.Gradient.stories.tsx +6 -0
- package/src/_stories/Chart.stories.tsx +0 -16
- package/src/_stories/_mock/bar_chart_ci_labels.json +620 -0
- package/src/_stories/_mock/legend_groupBy_mock.json +474 -0
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +1 -1
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +1 -1
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +1 -1
- package/src/components/BarChart/components/BarChart.Vertical.tsx +2 -2
- package/src/components/EditorPanel/EditorPanel.tsx +60 -24
- package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +4 -3
- package/src/components/Legend/Legend.Component.tsx +69 -58
- package/src/components/Legend/Legend.tsx +3 -1
- package/src/components/Legend/LegendGroup/LegendGroup.styles.css +40 -0
- package/src/components/Legend/LegendGroup/LegendGroup.tsx +103 -0
- package/src/components/Legend/LegendGroup/index.tsx +3 -0
- package/src/components/LineChart/components/LineChart.Circle.tsx +14 -9
- package/src/components/LineChart/index.tsx +18 -7
- package/src/components/LinearChart.tsx +38 -30
- package/src/data/initial-state.js +1 -1
- package/src/helpers/dataHelpers.ts +10 -0
- package/src/helpers/sizeHelpers.ts +23 -0
- package/src/hooks/useBarChart.ts +2 -1
- package/src/hooks/useScales.ts +2 -8
- package/src/store/chart.actions.ts +1 -1
- package/src/types/ChartConfig.ts +2 -1
|
@@ -15,6 +15,8 @@ import LegendGradient from '@cdc/core/components/Legend/Legend.Gradient'
|
|
|
15
15
|
import { DimensionsType } from '@cdc/core/types/Dimensions'
|
|
16
16
|
import { isLegendWrapViewport } from '@cdc/core/helpers/viewports'
|
|
17
17
|
import LegendLineShape from './LegendLine.Shape'
|
|
18
|
+
import LegendGroup from './LegendGroup'
|
|
19
|
+
import { getSeriesWithData } from '../../helpers/dataHelpers'
|
|
18
20
|
|
|
19
21
|
const LEGEND_PADDING = 36
|
|
20
22
|
|
|
@@ -29,6 +31,7 @@ export interface LegendProps {
|
|
|
29
31
|
seriesHighlight: string[]
|
|
30
32
|
skipId: string
|
|
31
33
|
dimensions: DimensionsType // for responsive width legend
|
|
34
|
+
transformedData: any
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
/* eslint-disable jsx-a11y/no-noninteractive-tabindex, jsx-a11y/no-static-element-interactions */
|
|
@@ -43,12 +46,17 @@ const Legend: React.FC<LegendProps> = forwardRef(
|
|
|
43
46
|
currentViewport,
|
|
44
47
|
formatLabels,
|
|
45
48
|
skipId = 'legend',
|
|
46
|
-
dimensions
|
|
49
|
+
dimensions,
|
|
50
|
+
transformedData: data
|
|
47
51
|
},
|
|
48
52
|
ref
|
|
49
53
|
) => {
|
|
50
54
|
const { innerClasses, containerClasses } = getLegendClasses(config)
|
|
51
55
|
const { runtime, legend } = config
|
|
56
|
+
const { series } = runtime
|
|
57
|
+
|
|
58
|
+
const seriesWithData = getSeriesWithData(config)
|
|
59
|
+
const dontFilterLegendItems = !series.length || legend.unified
|
|
52
60
|
|
|
53
61
|
const isLegendBottom =
|
|
54
62
|
legend?.position === 'bottom' ||
|
|
@@ -84,77 +92,80 @@ const Legend: React.FC<LegendProps> = forwardRef(
|
|
|
84
92
|
dimensions={dimensions}
|
|
85
93
|
parentPaddingToSubtract={legend.hideBorder ? 0 : LEGEND_PADDING}
|
|
86
94
|
/>
|
|
95
|
+
<LegendGroup formatLabels={formatLabels} />
|
|
87
96
|
|
|
88
97
|
<LegendOrdinal scale={colorScale} itemDirection='row' labelMargin='0 20px 0 0' shapeMargin='0 10px 0'>
|
|
89
98
|
{labels => {
|
|
90
99
|
return (
|
|
91
100
|
<>
|
|
92
101
|
<div className={innerClasses.join(' ')}>
|
|
93
|
-
{formatLabels(labels as Label[])
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
{formatLabels(labels as Label[])
|
|
103
|
+
.filter(label => dontFilterLegendItems || seriesWithData.includes(label.datum))
|
|
104
|
+
.map((label, i) => {
|
|
105
|
+
let className = ['legend-item', `legend-text--${label.text.replace(' ', '').toLowerCase()}`]
|
|
106
|
+
let itemName = label.datum
|
|
107
|
+
|
|
108
|
+
// Filter excluded data keys from legend
|
|
109
|
+
if (config.exclusions.active && config.exclusions.keys?.includes(itemName)) {
|
|
110
|
+
return null
|
|
111
|
+
}
|
|
101
112
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
113
|
+
if (runtime.seriesLabels) {
|
|
114
|
+
let index = config.runtime.seriesLabelsAll.indexOf(itemName)
|
|
115
|
+
itemName = config.runtime.seriesKeys[index]
|
|
105
116
|
|
|
106
|
-
|
|
107
|
-
|
|
117
|
+
if (runtime?.forecastingSeriesKeys?.length > 0) {
|
|
118
|
+
itemName = label.text
|
|
119
|
+
}
|
|
108
120
|
}
|
|
109
|
-
}
|
|
110
121
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
122
|
+
if (seriesHighlight.length) {
|
|
123
|
+
if (!seriesHighlight.includes(itemName)) {
|
|
124
|
+
className.push('inactive')
|
|
125
|
+
} else className.push('highlighted')
|
|
126
|
+
}
|
|
116
127
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
128
|
+
if (config.legend.style === 'gradient' || config.legend.groupBy) {
|
|
129
|
+
return <></>
|
|
130
|
+
}
|
|
120
131
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
132
|
+
return (
|
|
133
|
+
<LegendItem
|
|
134
|
+
className={className.join(' ')}
|
|
135
|
+
tabIndex={0}
|
|
136
|
+
key={`legend-quantile-${i}`}
|
|
137
|
+
onKeyDown={e => {
|
|
138
|
+
if (e.key === 'Enter') {
|
|
139
|
+
e.preventDefault()
|
|
140
|
+
highlight(label)
|
|
141
|
+
}
|
|
142
|
+
}}
|
|
143
|
+
onClick={e => {
|
|
128
144
|
e.preventDefault()
|
|
129
145
|
highlight(label)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
{label.text}
|
|
154
|
-
</LegendLabel>
|
|
155
|
-
</LegendItem>
|
|
156
|
-
)
|
|
157
|
-
})}
|
|
146
|
+
}}
|
|
147
|
+
role='button'
|
|
148
|
+
>
|
|
149
|
+
<>
|
|
150
|
+
{config.visualizationType === 'Line' && config.legend.style === 'lines' ? (
|
|
151
|
+
<React.Fragment>
|
|
152
|
+
<LegendLineShape index={i} label={label} config={config} />
|
|
153
|
+
</React.Fragment>
|
|
154
|
+
) : (
|
|
155
|
+
<>
|
|
156
|
+
<LegendShape
|
|
157
|
+
shape={config.legend.style === 'boxes' ? 'square' : 'circle'}
|
|
158
|
+
fill={label.value}
|
|
159
|
+
/>
|
|
160
|
+
</>
|
|
161
|
+
)}
|
|
162
|
+
</>
|
|
163
|
+
<LegendLabel align='left' className='m-0'>
|
|
164
|
+
{parse(label.text)}
|
|
165
|
+
</LegendLabel>
|
|
166
|
+
</LegendItem>
|
|
167
|
+
)
|
|
168
|
+
})}
|
|
158
169
|
|
|
159
170
|
{highLightedLegendItems.map((bar, i) => {
|
|
160
171
|
// if duplicates only return first item
|
|
@@ -17,7 +17,8 @@ const Legend = forwardRef((props, ref) => {
|
|
|
17
17
|
transformedData: data,
|
|
18
18
|
currentViewport,
|
|
19
19
|
dimensions,
|
|
20
|
-
getTextWidth
|
|
20
|
+
getTextWidth,
|
|
21
|
+
transformedData
|
|
21
22
|
} = useContext(ConfigContext)
|
|
22
23
|
if (!config.legend) return null
|
|
23
24
|
// create fn to reverse labels while legend is Bottom. Legend-right , legend-left works by default.
|
|
@@ -30,6 +31,7 @@ const Legend = forwardRef((props, ref) => {
|
|
|
30
31
|
getTextWidth={getTextWidth}
|
|
31
32
|
dimensions={dimensions}
|
|
32
33
|
ref={ref}
|
|
34
|
+
transformedData={transformedData}
|
|
33
35
|
skipId={props.skipId || 'legend'}
|
|
34
36
|
config={config}
|
|
35
37
|
colorScale={colorScale}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
.legend-container {
|
|
2
|
+
.legend-group {
|
|
3
|
+
font-size: 1rem;
|
|
4
|
+
|
|
5
|
+
line-height: 18px;
|
|
6
|
+
margin-bottom: 0.5rem;
|
|
7
|
+
|
|
8
|
+
.group-item .visx-legend-label {
|
|
9
|
+
font-weight: 400;
|
|
10
|
+
font-size: 0.889rem;
|
|
11
|
+
margin-bottom: 0.5rem;
|
|
12
|
+
}
|
|
13
|
+
.group-label {
|
|
14
|
+
font-weight: 500;
|
|
15
|
+
font-family: Nunito, sans-serif;
|
|
16
|
+
font-size: 1rem;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.legend-group.top,
|
|
21
|
+
.legend-group.bottom {
|
|
22
|
+
grid-gap: 10px;
|
|
23
|
+
|
|
24
|
+
&.group-item {
|
|
25
|
+
display: flex;
|
|
26
|
+
flex-direction: column;
|
|
27
|
+
align-items: flex-start;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
& .inactive {
|
|
31
|
+
opacity: 0.5;
|
|
32
|
+
transition: 0.2s all;
|
|
33
|
+
}
|
|
34
|
+
& .highlighted {
|
|
35
|
+
outline: 1px solid #005ea2;
|
|
36
|
+
outline-offset: 5px;
|
|
37
|
+
border-radius: 1px;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import React, { useContext } from 'react'
|
|
2
|
+
import { LegendOrdinal, LegendItem, LegendLabel } from '@visx/legend'
|
|
3
|
+
import LegendShape from '@cdc/core/components/LegendShape'
|
|
4
|
+
import ConfigContext from '../../../ConfigContext'
|
|
5
|
+
import _ from 'lodash'
|
|
6
|
+
import './LegendGroup.styles.css'
|
|
7
|
+
|
|
8
|
+
interface LegendGroup {
|
|
9
|
+
formatLabels: Function
|
|
10
|
+
}
|
|
11
|
+
const LegendGroup = ({ formatLabels }) => {
|
|
12
|
+
const {
|
|
13
|
+
highlight,
|
|
14
|
+
seriesHighlight,
|
|
15
|
+
colorScale,
|
|
16
|
+
transformedData: data,
|
|
17
|
+
config,
|
|
18
|
+
currentViewport
|
|
19
|
+
} = useContext(ConfigContext)
|
|
20
|
+
|
|
21
|
+
const getSubGroups = (data, key: string | undefined) => {
|
|
22
|
+
const uniqueGroups = new Set()
|
|
23
|
+
data.forEach(d => {
|
|
24
|
+
config.series.forEach(series => {
|
|
25
|
+
if (d[key] && d[series.dataKey]) {
|
|
26
|
+
uniqueGroups.add(d[key])
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
return Array.from(uniqueGroups) as string[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const groups: string[] = getSubGroups(data, config.legend.groupBy)
|
|
34
|
+
|
|
35
|
+
const classNames = ['legend-group', 'container', config.legend.position, currentViewport, 'row']
|
|
36
|
+
|
|
37
|
+
const gridCol =
|
|
38
|
+
currentViewport === 'xs'
|
|
39
|
+
? 'col-12'
|
|
40
|
+
: currentViewport === 'sm'
|
|
41
|
+
? 'col-6'
|
|
42
|
+
: currentViewport === 'md'
|
|
43
|
+
? 'col-4'
|
|
44
|
+
: 'col-3'
|
|
45
|
+
|
|
46
|
+
const isSigleCol = config.legend.position === 'bottom' || config.legend.position === 'top' ? gridCol : 'col-12'
|
|
47
|
+
|
|
48
|
+
let classNameItem = ['legend-group group-item', isSigleCol]
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className={classNames.join(' ')}>
|
|
52
|
+
{groups.map(group => {
|
|
53
|
+
return (
|
|
54
|
+
<div className={classNameItem.join(' ')} key={group}>
|
|
55
|
+
<div>
|
|
56
|
+
<p className='legend-group group-label'>{group}</p>
|
|
57
|
+
</div>
|
|
58
|
+
<LegendOrdinal scale={colorScale} itemDirection='row' labelMargin='0 20px 0 0' shapeMargin='0 10px 0'>
|
|
59
|
+
{labels =>
|
|
60
|
+
formatLabels(labels)
|
|
61
|
+
.filter(label => {
|
|
62
|
+
const groupBy = config.legend.groupBy || ''
|
|
63
|
+
return data.some(d => d[groupBy] === group && d[label.text] !== undefined && d[label.text] !== 'NA')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
.map((label, i) => {
|
|
67
|
+
let className = ['legend-group', 'group-item']
|
|
68
|
+
if (seriesHighlight.length) {
|
|
69
|
+
if (!seriesHighlight.includes(label.datum)) {
|
|
70
|
+
className.push('inactive')
|
|
71
|
+
} else {
|
|
72
|
+
className.push('highlighted')
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<LegendItem
|
|
78
|
+
alignItems='start'
|
|
79
|
+
className={className.join(' ')}
|
|
80
|
+
onClick={e => {
|
|
81
|
+
e.preventDefault()
|
|
82
|
+
highlight(label)
|
|
83
|
+
}}
|
|
84
|
+
key={`legend-item-${i}`}
|
|
85
|
+
tabIndex={0}
|
|
86
|
+
>
|
|
87
|
+
<LegendShape shape={config.legend.style === 'boxes' ? 'square' : 'circle'} fill={label.value} />
|
|
88
|
+
<LegendLabel align='left' margin='0'>
|
|
89
|
+
{label.text}
|
|
90
|
+
</LegendLabel>
|
|
91
|
+
</LegendItem>
|
|
92
|
+
)
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
</LegendOrdinal>
|
|
96
|
+
</div>
|
|
97
|
+
)
|
|
98
|
+
})}
|
|
99
|
+
</div>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export default LegendGroup
|
|
@@ -46,7 +46,7 @@ const Glyphs = [
|
|
|
46
46
|
const LineChartCircle = (props: LineChartCircleProps) => {
|
|
47
47
|
const {
|
|
48
48
|
config,
|
|
49
|
-
d,
|
|
49
|
+
d: pointData,
|
|
50
50
|
tableData,
|
|
51
51
|
displayArea,
|
|
52
52
|
seriesKey,
|
|
@@ -96,7 +96,7 @@ const LineChartCircle = (props: LineChartCircleProps) => {
|
|
|
96
96
|
if (mode === 'ALWAYS_SHOW_POINTS' && lineDatapointStyle !== 'hidden') {
|
|
97
97
|
if (lineDatapointStyle === 'always show') {
|
|
98
98
|
const isMatch = circleData?.some(
|
|
99
|
-
cd => cd[config.xAxis.dataKey] ===
|
|
99
|
+
cd => cd[config.xAxis.dataKey] === pointData[config.xAxis.dataKey] && cd[seriesKey] === pointData[seriesKey]
|
|
100
100
|
)
|
|
101
101
|
|
|
102
102
|
if (
|
|
@@ -105,13 +105,14 @@ const LineChartCircle = (props: LineChartCircleProps) => {
|
|
|
105
105
|
(visual.maximumShapeAmount === seriesIndex && visual.lineDatapointSymbol === 'standard')
|
|
106
106
|
)
|
|
107
107
|
return <></>
|
|
108
|
-
const positionLeft = getXPos(
|
|
109
|
-
const positionTop =
|
|
108
|
+
const positionLeft = getXPos(pointData[config.xAxis.dataKey])
|
|
109
|
+
const positionTop =
|
|
110
|
+
filtered.axis === 'Right' ? yScaleRight(pointData[filtered.dataKey]) : yScale(pointData[filtered.dataKey])
|
|
110
111
|
|
|
111
112
|
return (
|
|
112
113
|
<g transform={transformShape(positionTop, positionLeft)}>
|
|
113
114
|
<Shape
|
|
114
|
-
opacity={
|
|
115
|
+
opacity={pointData[seriesKey] ? 1 : 0}
|
|
115
116
|
fillOpacity={1}
|
|
116
117
|
fill={getColor(displayArea, colorScale, config, seriesKey, seriesKey)}
|
|
117
118
|
style={{ filter: 'unset', opacity: 1 }}
|
|
@@ -197,10 +198,14 @@ const LineChartCircle = (props: LineChartCircleProps) => {
|
|
|
197
198
|
|
|
198
199
|
return isFirstPoint || isLastPoint || isMiddlePoint
|
|
199
200
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
201
|
+
const _dataIndex = pointData
|
|
202
|
+
? data.findIndex(item => item[config.xAxis.dataKey] === pointData[config.xAxis.dataKey])
|
|
203
|
+
: dataIndex
|
|
204
|
+
|
|
205
|
+
if (drawIsolatedPoints(_dataIndex, seriesKey)) {
|
|
206
|
+
const positionTop =
|
|
207
|
+
filtered?.axis === 'Right' ? yScaleRight(pointData[filtered?.dataKey]) : yScale(pointData[filtered?.dataKey])
|
|
208
|
+
const positionLeft = getXPos(pointData[config.xAxis?.dataKey])
|
|
204
209
|
const color = colorScale(config.runtime.seriesLabelsAll[seriesIndex])
|
|
205
210
|
|
|
206
211
|
return (
|
|
@@ -393,21 +393,32 @@ const LineChart = (props: LineChartProps) => {
|
|
|
393
393
|
break
|
|
394
394
|
}
|
|
395
395
|
}
|
|
396
|
-
if (!lastDatum) {
|
|
396
|
+
if (!lastDatum || legend.position === 'right') {
|
|
397
397
|
return <></>
|
|
398
398
|
}
|
|
399
|
+
|
|
400
|
+
let labelText = config.runtime.seriesLabels[seriesKey] || seriesKey
|
|
401
|
+
// truncate labels longer that 10 chars
|
|
402
|
+
const ellipsis = '...'
|
|
403
|
+
if (labelText.length > 10) {
|
|
404
|
+
labelText = labelText.substring(0, 10) + ellipsis
|
|
405
|
+
}
|
|
406
|
+
|
|
399
407
|
return (
|
|
400
408
|
<Text
|
|
409
|
+
display={
|
|
410
|
+
legend.behavior === 'highlight' ||
|
|
411
|
+
(seriesHighlight.length === 0 && !legend.dynamicLegend) ||
|
|
412
|
+
seriesHighlight.indexOf(seriesKey) !== -1
|
|
413
|
+
? 'block'
|
|
414
|
+
: 'none'
|
|
415
|
+
}
|
|
401
416
|
x={xPos(lastDatum) + 5}
|
|
402
417
|
y={yScale(getYAxisData(lastDatum, seriesKey))}
|
|
403
418
|
alignmentBaseline='middle'
|
|
404
|
-
fill={
|
|
405
|
-
config.colorMatchLineSeriesLabels && colorScale
|
|
406
|
-
? colorScale(config.runtime.seriesLabels[seriesKey] || seriesKey)
|
|
407
|
-
: 'black'
|
|
408
|
-
}
|
|
419
|
+
fill={colorScale(config.runtime.seriesLabels[seriesKey] || seriesKey)}
|
|
409
420
|
>
|
|
410
|
-
{
|
|
421
|
+
{labelText}
|
|
411
422
|
</Text>
|
|
412
423
|
)
|
|
413
424
|
})}
|
|
@@ -7,9 +7,11 @@ 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 _ from 'lodash'
|
|
11
|
+
|
|
12
|
+
// CDC Components
|
|
10
13
|
import { isDateScale } from '@cdc/core/helpers/cove/date'
|
|
11
14
|
import BrushChart from './BrushChart'
|
|
12
|
-
// CDC Components
|
|
13
15
|
import { AreaChart, AreaChartStacked } from './AreaChart'
|
|
14
16
|
import BarChart from './BarChart'
|
|
15
17
|
import ConfigContext from '../ConfigContext'
|
|
@@ -28,7 +30,7 @@ import CategoricalYAxis from './Axis/Categorical.Axis'
|
|
|
28
30
|
// Helpers
|
|
29
31
|
import { isLegendWrapViewport, isMobileHeightViewport } from '@cdc/core/helpers/viewports'
|
|
30
32
|
import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
|
|
31
|
-
import { calcInitialHeight } from '../helpers/sizeHelpers'
|
|
33
|
+
import { calcInitialHeight, handleAutoPaddingRight } from '../helpers/sizeHelpers'
|
|
32
34
|
|
|
33
35
|
// Hooks
|
|
34
36
|
import useMinMax from '../hooks/useMinMax'
|
|
@@ -41,7 +43,6 @@ import { useEditorPermissions } from './EditorPanel/useEditorPermissions'
|
|
|
41
43
|
import Annotation from './Annotations'
|
|
42
44
|
import { BlurStrokeText } from '@cdc/core/components/BlurStrokeText'
|
|
43
45
|
import { countNumOfTicks } from '../helpers/countNumOfTicks'
|
|
44
|
-
import _ from 'lodash'
|
|
45
46
|
|
|
46
47
|
type LinearChartProps = {
|
|
47
48
|
parentWidth: number
|
|
@@ -119,6 +120,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
119
120
|
const xAxisLabelRefs = useRef([])
|
|
120
121
|
const xAxisTitleRef = useRef(null)
|
|
121
122
|
const lastMaxValue = useRef(maxValue)
|
|
123
|
+
const gridLineRefs = useRef([])
|
|
122
124
|
|
|
123
125
|
const dataRef = useIntersectionObserver(triggerRef, {
|
|
124
126
|
freezeOnceVisible: false
|
|
@@ -329,6 +331,21 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
329
331
|
}
|
|
330
332
|
|
|
331
333
|
// EFFECTS
|
|
334
|
+
// Adjust padding on the right side of the chart to accommodate for overflow
|
|
335
|
+
useEffect(() => {
|
|
336
|
+
if (!parentRef.current || !parentWidth || !gridLineRefs.current.length) return
|
|
337
|
+
|
|
338
|
+
const [updatePadding, paddingToAdd] = handleAutoPaddingRight(parentRef, xAxisLabelRefs, parentWidth)
|
|
339
|
+
|
|
340
|
+
if (!updatePadding) return
|
|
341
|
+
|
|
342
|
+
parentRef.current.style.paddingRight = `${paddingToAdd}px`
|
|
343
|
+
// subtract padding from grid line's x1 value
|
|
344
|
+
gridLineRefs.current.forEach(gridLine => {
|
|
345
|
+
if (!gridLine) return
|
|
346
|
+
gridLine.setAttribute('x1', xMax - paddingToAdd)
|
|
347
|
+
})
|
|
348
|
+
}, [parentWidth, parentHeight, data])
|
|
332
349
|
|
|
333
350
|
// Make sure the chart is visible if in the editor
|
|
334
351
|
/* eslint-disable react-hooks/exhaustive-deps */
|
|
@@ -421,8 +438,10 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
421
438
|
}, [maxValue])
|
|
422
439
|
|
|
423
440
|
useEffect(() => {
|
|
424
|
-
if (orientation === 'horizontal')
|
|
425
|
-
|
|
441
|
+
if (orientation === 'horizontal' || !labelsOverflow || config.yAxis?.max) {
|
|
442
|
+
setYAxisAutoPadding(0)
|
|
443
|
+
return
|
|
444
|
+
}
|
|
426
445
|
|
|
427
446
|
// minimum percentage of the max value that the distance should be from the top grid line
|
|
428
447
|
const MINIMUM_DISTANCE_PERCENTAGE = 0.025
|
|
@@ -638,6 +657,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
638
657
|
<Group key={`vx-tick-${tick.value}-${i}`} className={'vx-axis-tick'}>
|
|
639
658
|
{runtime.yAxis.gridLines && !hideFirstGridLine ? (
|
|
640
659
|
<Line
|
|
660
|
+
innerRef={el => (gridLineRefs.current[i] = el)}
|
|
641
661
|
key={`${tick.value}--hide-hideGridLines`}
|
|
642
662
|
display={(isLogarithmicAxis && showTicks).toString()}
|
|
643
663
|
from={{ x: tick.from.x + xMax, y: tick.from.y }}
|
|
@@ -698,7 +718,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
698
718
|
/>
|
|
699
719
|
)}
|
|
700
720
|
{((visualizationType === 'Area Chart' && config.visualizationSubType === 'regular') ||
|
|
701
|
-
visualizationType === 'Combo') && (
|
|
721
|
+
(visualizationType === 'Combo' && config.visualizationSubType === 'regular')) && (
|
|
702
722
|
<AreaChart
|
|
703
723
|
xScale={xScale}
|
|
704
724
|
yScale={yScale}
|
|
@@ -714,7 +734,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
714
734
|
/>
|
|
715
735
|
)}
|
|
716
736
|
{((visualizationType === 'Area Chart' && config.visualizationSubType === 'stacked') ||
|
|
717
|
-
visualizationType === 'Combo') && (
|
|
737
|
+
(visualizationType === 'Combo' && config.visualizationSubType === 'stacked')) && (
|
|
718
738
|
<AreaChartStacked
|
|
719
739
|
xScale={xScale}
|
|
720
740
|
yScale={yScale}
|
|
@@ -1399,20 +1419,21 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
1399
1419
|
const axisMaxHeight = bottomLabelStart + BOTTOM_LABEL_PADDING
|
|
1400
1420
|
|
|
1401
1421
|
const containsMultipleWords = inputString => /\s/.test(inputString)
|
|
1402
|
-
const
|
|
1422
|
+
const isMultiLabel = filteredTicks.some(tick => containsMultipleWords(tick.value))
|
|
1403
1423
|
|
|
1404
1424
|
// Calculate sumOfTickWidth here, before map function
|
|
1405
|
-
const
|
|
1425
|
+
const longestTickLength = Math.max(
|
|
1406
1426
|
...filteredTicks.map(tick => getTextWidth(tick.formattedValue, GET_TEXT_WIDTH_FONT))
|
|
1407
1427
|
)
|
|
1408
1428
|
// const marginTop = 20 // moved to top bc need for yMax calcs
|
|
1409
|
-
const accumulator =
|
|
1429
|
+
const accumulator = isMultiLabel ? 180 : 100
|
|
1410
1430
|
|
|
1411
1431
|
const textWidths = filteredTicks.map(tick => getTextWidth(tick.formattedValue, GET_TEXT_WIDTH_FONT))
|
|
1412
1432
|
const sumOfTickWidth = textWidths.reduce((a, b) => a + b, accumulator)
|
|
1413
1433
|
const spaceBetweenEachTick = (xMax - sumOfTickWidth) / (filteredTicks.length - 1)
|
|
1434
|
+
const bufferBetweenTicks = 40
|
|
1435
|
+
const maxLengthOfTick = width / filteredTicks.length - X_TICK_LABEL_PADDING * 2 - bufferBetweenTicks
|
|
1414
1436
|
|
|
1415
|
-
// Check if ticks are overlapping
|
|
1416
1437
|
// Determine the position of each tick
|
|
1417
1438
|
let positions = [0] // The first tick is at position 0
|
|
1418
1439
|
for (let i = 1; i < textWidths.length; i++) {
|
|
@@ -1424,35 +1445,22 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
1424
1445
|
const axisBBox = axisBottomRef?.current?.getBBox().height
|
|
1425
1446
|
config.xAxis.axisBBox = axisBBox
|
|
1426
1447
|
|
|
1427
|
-
// Check if ticks are overlapping
|
|
1428
|
-
let areTicksTouching = false
|
|
1429
|
-
textWidths.forEach((_, i) => {
|
|
1430
|
-
if (positions[i] + textWidths[i] > positions[i + 1]) {
|
|
1431
|
-
areTicksTouching = true
|
|
1432
|
-
return
|
|
1433
|
-
}
|
|
1434
|
-
})
|
|
1435
|
-
|
|
1436
|
-
// Force wrap when showing years once so it's easier to read
|
|
1437
|
-
if (config.xAxis.showYearsOnce) {
|
|
1438
|
-
areTicksTouching = true
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
1448
|
// force wrap it last tick is close to the end of the axis
|
|
1442
1449
|
const lastTickWidth = textWidths[textWidths.length - 1]
|
|
1443
1450
|
const lastTickPosition = positions[positions.length - 1] + lastTickWidth
|
|
1444
1451
|
const lastTickEnd = lastTickPosition + lastTickWidth / 2
|
|
1445
1452
|
const lastTickEndThreshold = xMax - lastTickWidth
|
|
1446
1453
|
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1454
|
+
const areTicksTouching =
|
|
1455
|
+
textWidths.some(textWidth => textWidth > maxLengthOfTick) || // Force wrap if any tick is too long
|
|
1456
|
+
config.xAxis.showYearsOnce || // Force wrap when showing years once so it's easier to read
|
|
1457
|
+
lastTickEnd > lastTickEndThreshold // Force wrap it last tick is close to the end of the axis
|
|
1450
1458
|
|
|
1451
1459
|
const dynamicMarginTop =
|
|
1452
|
-
areTicksTouching && config.isResponsiveTicks ?
|
|
1460
|
+
areTicksTouching && config.isResponsiveTicks ? longestTickLength + DEFAULT_TICK_LENGTH + 20 : 0
|
|
1453
1461
|
|
|
1454
1462
|
config.dynamicMarginTop = dynamicMarginTop
|
|
1455
|
-
config.xAxis.tickWidthMax =
|
|
1463
|
+
config.xAxis.tickWidthMax = longestTickLength
|
|
1456
1464
|
|
|
1457
1465
|
return (
|
|
1458
1466
|
<Group className='bottom-axis' width={dimensions[0]}>
|
|
@@ -24,7 +24,6 @@ export default {
|
|
|
24
24
|
isResponsiveTicks: false,
|
|
25
25
|
general: {
|
|
26
26
|
annotationDropdownText: 'Annotations',
|
|
27
|
-
showDownloadButton: false,
|
|
28
27
|
showMissingDataLabel: true,
|
|
29
28
|
showSuppressedSymbol: true,
|
|
30
29
|
showZeroValueData: true,
|
|
@@ -166,6 +165,7 @@ export default {
|
|
|
166
165
|
seriesHighlight: [],
|
|
167
166
|
style: 'circles',
|
|
168
167
|
subStyle: 'linear blocks',
|
|
168
|
+
groupBy: '',
|
|
169
169
|
shape: 'circle',
|
|
170
170
|
tickRotation: '',
|
|
171
171
|
hideBorder: {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ChartConfig } from '../types/ChartConfig'
|
|
2
|
+
|
|
3
|
+
export const getSeriesWithData = (config: ChartConfig) => {
|
|
4
|
+
const { filters, data, runtime } = config
|
|
5
|
+
const { series } = runtime
|
|
6
|
+
|
|
7
|
+
const filteredData = data.filter(d => filters.every(f => d[f.columnName] === f.active))
|
|
8
|
+
|
|
9
|
+
return series.filter(s => filteredData.some(d => d[s.dynamicCategory || s.dataKey])).map(s => s.name || s.dataKey)
|
|
10
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { clamp } from 'lodash'
|
|
2
|
+
|
|
1
3
|
import { isMobileHeightViewport } from '@cdc/core/helpers/viewports'
|
|
2
4
|
import { ChartConfig, ViewportSize } from '../types/ChartConfig'
|
|
5
|
+
import { EDITOR_WIDTH } from '../CdcChartComponent'
|
|
3
6
|
|
|
4
7
|
export function getOrientation(
|
|
5
8
|
{ orientation, heights, visualizationType }: Pick<ChartConfig, 'orientation' | 'heights' | 'visualizationType'>,
|
|
@@ -23,3 +26,23 @@ export function calcInitialHeight(
|
|
|
23
26
|
const height = Number(heights?.[renderedOrientation])
|
|
24
27
|
return isNaN(height) ? 0 : height
|
|
25
28
|
}
|
|
29
|
+
|
|
30
|
+
export function handleAutoPaddingRight(parentRef, xAxisLabelRefs, parentWidth): [boolean, number] {
|
|
31
|
+
const parentX = parentRef.current.getBoundingClientRect().x
|
|
32
|
+
const editorIsOpen = !!document.querySelector('.editor-panel:not(.hidden)')
|
|
33
|
+
const lastTickRect = xAxisLabelRefs.current?.[xAxisLabelRefs.current.length - 1]?.getBoundingClientRect()
|
|
34
|
+
const lastBottomTickEnd = lastTickRect ? lastTickRect.x + lastTickRect.width : 0
|
|
35
|
+
const editorWidth = editorIsOpen ? EDITOR_WIDTH : 0
|
|
36
|
+
const calculatedOverhang = lastBottomTickEnd - parentX - editorWidth - parentWidth
|
|
37
|
+
|
|
38
|
+
const paddingToAdd = clamp(calculatedOverhang, 0, 20)
|
|
39
|
+
const currentPadding = Number(parentRef.current.style.paddingRight.replace('px', ''))
|
|
40
|
+
const paddingDiff = Math.abs(currentPadding - paddingToAdd)
|
|
41
|
+
const DIFF_THRESHOLD = 5
|
|
42
|
+
|
|
43
|
+
const noChange = currentPadding === calculatedOverhang
|
|
44
|
+
const insufficientDiff = (paddingDiff < DIFF_THRESHOLD && calculatedOverhang > 0) || Math.abs(calculatedOverhang) < 1
|
|
45
|
+
const updatePadding = !noChange && !insufficientDiff
|
|
46
|
+
|
|
47
|
+
return [updatePadding, paddingToAdd]
|
|
48
|
+
}
|