@cdc/chart 4.24.9-1 → 4.24.10
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 +37673 -36530
- package/index.html +1 -1
- package/package.json +2 -2
- package/src/CdcChart.tsx +128 -106
- package/src/_stories/Chart.Legend.Gradient.stories.tsx +33 -0
- package/src/_stories/Chart.stories.tsx +28 -0
- package/src/_stories/ChartAxisLabels.stories.tsx +20 -0
- package/src/_stories/ChartAxisTitles.stories.tsx +53 -0
- package/src/_stories/ChartPrefixSuffix.stories.tsx +151 -0
- package/src/_stories/_mock/horizontal_bar.json +257 -0
- package/src/_stories/_mock/large_x_axis_labels.json +261 -0
- package/src/_stories/_mock/paired-bar.json +262 -0
- package/src/_stories/_mock/pie_with_data.json +255 -0
- package/src/_stories/_mock/simplified_line.json +1510 -0
- package/src/components/Annotations/components/AnnotationDraggable.tsx +0 -3
- package/src/components/Annotations/components/AnnotationDropdown.tsx +1 -1
- package/src/components/Axis/Categorical.Axis.tsx +22 -4
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +95 -16
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +41 -17
- package/src/components/BarChart/components/BarChart.Vertical.tsx +78 -20
- package/src/components/BarChart/helpers/index.ts +23 -4
- package/src/components/BrushChart.tsx +3 -2
- package/src/components/DeviationBar.jsx +58 -8
- package/src/components/EditorPanel/EditorPanel.tsx +62 -39
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +6 -23
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +21 -4
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +297 -35
- package/src/components/EditorPanel/components/panels.scss +4 -6
- package/src/components/EditorPanel/editor-panel.scss +0 -8
- package/src/components/EditorPanel/helpers/tests/updateFieldRankByValue.test.ts +38 -0
- package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +42 -0
- package/src/components/EditorPanel/useEditorPermissions.ts +1 -0
- package/src/components/ForestPlot/ForestPlot.tsx +2 -3
- package/src/components/ForestPlot/ForestPlotProps.ts +2 -0
- package/src/components/Legend/Legend.Component.tsx +16 -16
- package/src/components/Legend/Legend.Suppression.tsx +25 -20
- package/src/components/Legend/Legend.tsx +0 -2
- package/src/components/Legend/helpers/index.ts +16 -19
- package/src/components/LegendWrapper.tsx +3 -1
- package/src/components/LinearChart.tsx +740 -562
- package/src/components/PairedBarChart.jsx +50 -10
- package/src/components/PieChart/PieChart.tsx +1 -6
- package/src/components/Regions/components/Regions.tsx +33 -19
- package/src/components/ZoomBrush.tsx +25 -6
- package/src/coreStyles_chart.scss +3 -0
- package/src/data/initial-state.js +6 -2
- package/src/helpers/configHelpers.ts +28 -0
- package/src/helpers/handleRankByValue.ts +15 -0
- package/src/helpers/sizeHelpers.ts +25 -0
- package/src/helpers/tests/handleRankByValue.test.ts +37 -0
- package/src/helpers/tests/sizeHelpers.test.ts +80 -0
- package/src/hooks/useColorPalette.js +10 -2
- package/src/hooks/useLegendClasses.ts +4 -0
- package/src/hooks/useScales.ts +31 -3
- package/src/hooks/useTooltip.tsx +9 -5
- package/src/index.jsx +1 -0
- package/src/scss/DataTable.scss +5 -4
- package/src/scss/main.scss +57 -52
- package/src/types/ChartConfig.ts +38 -16
- package/src/types/ChartContext.ts +18 -14
- package/src/_stories/Chart.Legend.Gradient.tsx +0 -19
- package/src/_stories/ChartBrush.stories.tsx +0 -19
|
@@ -6,9 +6,10 @@ import { Text } from '@visx/text'
|
|
|
6
6
|
|
|
7
7
|
import ConfigContext from '../ConfigContext'
|
|
8
8
|
import { getContrastColor } from '@cdc/core/helpers/cove/accessibility'
|
|
9
|
+
import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
|
|
9
10
|
|
|
10
11
|
const PairedBarChart = ({ width, height, originalWidth }) => {
|
|
11
|
-
const { config, colorScale, transformedData: data, formatNumber, seriesHighlight
|
|
12
|
+
const { config, colorScale, transformedData: data, formatNumber, seriesHighlight } = useContext(ConfigContext)
|
|
12
13
|
|
|
13
14
|
if (!config || config?.series?.length < 2) return
|
|
14
15
|
|
|
@@ -79,14 +80,27 @@ const PairedBarChart = ({ width, height, originalWidth }) => {
|
|
|
79
80
|
}
|
|
80
81
|
`}
|
|
81
82
|
</style>
|
|
82
|
-
<svg
|
|
83
|
+
<svg
|
|
84
|
+
id='cdc-visualization__paired-bar-chart'
|
|
85
|
+
width={originalWidth}
|
|
86
|
+
height={height}
|
|
87
|
+
viewBox={`0 0 ${width + Number(config.runtime.yAxis.size)} ${height}`}
|
|
88
|
+
role='img'
|
|
89
|
+
tabIndex={0}
|
|
90
|
+
>
|
|
83
91
|
<title>{`Paired bar chart graphic with the title ${config.title ? config.title : 'No Title Found'}`}</title>
|
|
84
92
|
<Group top={0} left={Number(config.xAxis.size)}>
|
|
85
93
|
{data
|
|
86
94
|
.filter(item => config.series[0].dataKey === groupOne.dataKey)
|
|
87
95
|
.map((d, index) => {
|
|
88
|
-
let transparentBar =
|
|
89
|
-
|
|
96
|
+
let transparentBar =
|
|
97
|
+
config.legend.behavior === 'highlight' &&
|
|
98
|
+
seriesHighlight.length > 0 &&
|
|
99
|
+
seriesHighlight.indexOf(config.series[0].dataKey) === -1
|
|
100
|
+
let displayBar =
|
|
101
|
+
config.legend.behavior === 'highlight' ||
|
|
102
|
+
seriesHighlight.length === 0 ||
|
|
103
|
+
seriesHighlight.indexOf(config.series[0].dataKey) !== -1
|
|
90
104
|
let barWidth = xScale(d[config.series[0].dataKey])
|
|
91
105
|
let barHeight = Number(config.barHeight) ? Number(config.barHeight) : 25
|
|
92
106
|
// update bar Y to give dynamic Y when user applyes BarSpace
|
|
@@ -95,7 +109,10 @@ const PairedBarChart = ({ width, height, originalWidth }) => {
|
|
|
95
109
|
const totalheight = (Number(config.barSpace) + barHeight + borderWidth) * data.length
|
|
96
110
|
config.heights.horizontal = totalheight
|
|
97
111
|
// check if text fits inside of the bar including suffix/prefix,comma,fontSize ..etc
|
|
98
|
-
const textWidth = getTextWidth(
|
|
112
|
+
const textWidth = getTextWidth(
|
|
113
|
+
formatNumber(d[groupOne.dataKey], 'left'),
|
|
114
|
+
`normal ${fontSize[config.fontSize]}px sans-serif`
|
|
115
|
+
)
|
|
99
116
|
const textFits = textWidth < barWidth - 5 // minus padding dx(5)
|
|
100
117
|
|
|
101
118
|
return (
|
|
@@ -119,7 +136,14 @@ const PairedBarChart = ({ width, height, originalWidth }) => {
|
|
|
119
136
|
tabIndex={-1}
|
|
120
137
|
/>
|
|
121
138
|
{config.yAxis.displayNumbersOnBar && displayBar && (
|
|
122
|
-
<Text
|
|
139
|
+
<Text
|
|
140
|
+
textAnchor={textFits ? 'start' : 'end'}
|
|
141
|
+
dx={textFits ? 5 : -5}
|
|
142
|
+
verticalAnchor='middle'
|
|
143
|
+
x={halfWidth - barWidth}
|
|
144
|
+
y={y + config.barHeight / 2}
|
|
145
|
+
fill={textFits ? groupOne.labelColor : '#000'}
|
|
146
|
+
>
|
|
123
147
|
{formatNumber(d[groupOne.dataKey], 'left')}
|
|
124
148
|
</Text>
|
|
125
149
|
)}
|
|
@@ -131,8 +155,14 @@ const PairedBarChart = ({ width, height, originalWidth }) => {
|
|
|
131
155
|
.filter(item => config.series[1].dataKey === groupTwo.dataKey)
|
|
132
156
|
.map((d, index) => {
|
|
133
157
|
let barWidth = xScale(d[config.series[1].dataKey])
|
|
134
|
-
let transparentBar =
|
|
135
|
-
|
|
158
|
+
let transparentBar =
|
|
159
|
+
config.legend.behavior === 'highlight' &&
|
|
160
|
+
seriesHighlight.length > 0 &&
|
|
161
|
+
seriesHighlight.indexOf(config.series[1].dataKey) === -1
|
|
162
|
+
let displayBar =
|
|
163
|
+
config.legend.behavior === 'highlight' ||
|
|
164
|
+
seriesHighlight.length === 0 ||
|
|
165
|
+
seriesHighlight.indexOf(config.series[1].dataKey) !== -1
|
|
136
166
|
let barHeight = config.barHeight ? Number(config.barHeight) : 25
|
|
137
167
|
// update bar Y to give dynamic Y when user applyes BarSpace
|
|
138
168
|
let y = 0
|
|
@@ -140,7 +170,10 @@ const PairedBarChart = ({ width, height, originalWidth }) => {
|
|
|
140
170
|
const totalheight = (Number(config.barSpace) + barHeight + borderWidth) * data.length
|
|
141
171
|
config.heights.horizontal = totalheight
|
|
142
172
|
// check if text fits inside of the bar including suffix/prefix,comma,fontSize ..etc
|
|
143
|
-
const textWidth = getTextWidth(
|
|
173
|
+
const textWidth = getTextWidth(
|
|
174
|
+
formatNumber(d[groupTwo.dataKey], 'left'),
|
|
175
|
+
`normal ${fontSize[config.fontSize]}px sans-serif`
|
|
176
|
+
)
|
|
144
177
|
const isTextFits = textWidth < barWidth - 5 // minus padding dx(5)
|
|
145
178
|
|
|
146
179
|
return (
|
|
@@ -171,7 +204,14 @@ const PairedBarChart = ({ width, height, originalWidth }) => {
|
|
|
171
204
|
tabIndex={-1}
|
|
172
205
|
/>
|
|
173
206
|
{config.yAxis.displayNumbersOnBar && displayBar && (
|
|
174
|
-
<Text
|
|
207
|
+
<Text
|
|
208
|
+
textAnchor={isTextFits ? 'end' : 'start'}
|
|
209
|
+
dx={isTextFits ? -5 : 5}
|
|
210
|
+
verticalAnchor='middle'
|
|
211
|
+
x={halfWidth + barWidth}
|
|
212
|
+
y={y + config.barHeight / 2}
|
|
213
|
+
fill={isTextFits ? groupTwo.labelColor : '#000'}
|
|
214
|
+
>
|
|
175
215
|
{formatNumber(d[groupTwo.dataKey], 'left')}
|
|
176
216
|
</Text>
|
|
177
217
|
)}
|
|
@@ -38,9 +38,6 @@ const PieChart = props => {
|
|
|
38
38
|
config,
|
|
39
39
|
colorScale,
|
|
40
40
|
currentViewport,
|
|
41
|
-
dimensions,
|
|
42
|
-
highlight,
|
|
43
|
-
highlightReset,
|
|
44
41
|
seriesHighlight,
|
|
45
42
|
isDraggingAnnotation
|
|
46
43
|
} = useContext(ConfigContext)
|
|
@@ -232,8 +229,6 @@ const PieChart = props => {
|
|
|
232
229
|
}
|
|
233
230
|
}, [seriesHighlight]) // eslint-disable-line
|
|
234
231
|
|
|
235
|
-
const createLegendLabels = createFormatLabels(config, [], _data, _colorScale)
|
|
236
|
-
|
|
237
232
|
const getSvgClasses = () => {
|
|
238
233
|
let classes = ['animated-pie', 'group']
|
|
239
234
|
if (config.animate === false || animatedPie) {
|
|
@@ -280,7 +275,7 @@ const PieChart = props => {
|
|
|
280
275
|
<TooltipWithBounds
|
|
281
276
|
key={Math.random()}
|
|
282
277
|
className={'tooltip cdc-open-viz-module'}
|
|
283
|
-
left={tooltipLeft}
|
|
278
|
+
left={tooltipLeft + centerX - radius}
|
|
284
279
|
top={tooltipTop}
|
|
285
280
|
>
|
|
286
281
|
<ul>
|
|
@@ -19,7 +19,18 @@ type RegionsProps = {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
// TODO: should regions be removed on categorical axis?
|
|
22
|
-
const Regions: React.FC<RegionsProps> = ({
|
|
22
|
+
const Regions: React.FC<RegionsProps> = ({
|
|
23
|
+
xScale,
|
|
24
|
+
barWidth = 0,
|
|
25
|
+
totalBarsInGroup = 1,
|
|
26
|
+
yMax,
|
|
27
|
+
handleTooltipMouseOff,
|
|
28
|
+
handleTooltipMouseOver,
|
|
29
|
+
handleTooltipClick,
|
|
30
|
+
tooltipData,
|
|
31
|
+
showTooltip,
|
|
32
|
+
hideTooltip
|
|
33
|
+
}) => {
|
|
23
34
|
const { parseDate, config } = useContext<ChartContext>(ConfigContext)
|
|
24
35
|
|
|
25
36
|
const { runtime, regions, visualizationType, orientation, xAxis } = config
|
|
@@ -44,7 +55,10 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
|
|
|
44
55
|
const previousDays = Number(region.from) || 0
|
|
45
56
|
const categoricalDomain = domain.map(d => formatDate(config.xAxis.dateParseFormat, new Date(d)))
|
|
46
57
|
const d = region.toType === 'Last Date' ? new Date(domain[domain.length - 1]).getTime() : new Date(region.to) // on categorical charts force leading zero 03/15/2016 vs 3/15/2016 for valid date format
|
|
47
|
-
const to =
|
|
58
|
+
const to =
|
|
59
|
+
config.xAxis.type === 'categorical'
|
|
60
|
+
? formatDate(config.xAxis.dateParseFormat, d)
|
|
61
|
+
: formatDate(config.xAxis.dateParseFormat, d)
|
|
48
62
|
const toDate = new Date(to)
|
|
49
63
|
from = new Date(toDate.setDate(toDate.getDate() - Number(previousDays)))
|
|
50
64
|
|
|
@@ -120,7 +134,12 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
|
|
|
120
134
|
}
|
|
121
135
|
if (region.toType === 'Last Date') {
|
|
122
136
|
const lastDate = domain[domain.length - 1]
|
|
123
|
-
to = Number(
|
|
137
|
+
to = Number(
|
|
138
|
+
xScale(lastDate) +
|
|
139
|
+
((visualizationType === 'Bar' || visualizationType === 'Combo') && config.xAxis.type === 'date'
|
|
140
|
+
? barWidth * totalBarsInGroup
|
|
141
|
+
: 0)
|
|
142
|
+
)
|
|
124
143
|
}
|
|
125
144
|
|
|
126
145
|
if (visualizationType === 'Line' || visualizationType === 'Area Chart') {
|
|
@@ -152,27 +171,22 @@ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGrou
|
|
|
152
171
|
if (!from) return null
|
|
153
172
|
if (!to) return null
|
|
154
173
|
|
|
155
|
-
const TopRegionBorderShape = () => {
|
|
156
|
-
return (
|
|
157
|
-
<path
|
|
158
|
-
stroke='#333'
|
|
159
|
-
d={`M${from} -5
|
|
160
|
-
L${from} 5
|
|
161
|
-
M${from} 0
|
|
162
|
-
L${to} 0
|
|
163
|
-
M${to} -5
|
|
164
|
-
L${to} 5`}
|
|
165
|
-
/>
|
|
166
|
-
)
|
|
167
|
-
}
|
|
168
|
-
|
|
169
174
|
const HighlightedArea = () => {
|
|
170
175
|
return <rect x={from} y={0} width={width} height={yMax} fill={region.background} opacity={0.3} />
|
|
171
176
|
}
|
|
172
177
|
|
|
173
178
|
return (
|
|
174
|
-
<Group
|
|
175
|
-
|
|
179
|
+
<Group
|
|
180
|
+
height={100}
|
|
181
|
+
fill='red'
|
|
182
|
+
className='regions regions-group--line zzz'
|
|
183
|
+
key={region.label}
|
|
184
|
+
onMouseMove={handleTooltipMouseOver}
|
|
185
|
+
onMouseLeave={handleTooltipMouseOff}
|
|
186
|
+
handleTooltipClick={handleTooltipClick}
|
|
187
|
+
tooltipData={JSON.stringify(tooltipData)}
|
|
188
|
+
showTooltip={showTooltip}
|
|
189
|
+
>
|
|
176
190
|
<HighlightedArea />
|
|
177
191
|
<Text x={from + width / 2} y={5} fill={region.color} verticalAnchor='start' textAnchor='middle'>
|
|
178
192
|
{region.label}
|
|
@@ -7,6 +7,7 @@ import ConfigContext from '../ConfigContext'
|
|
|
7
7
|
import { ScaleLinear, ScaleBand } from 'd3-scale'
|
|
8
8
|
import { isDateScale } from '@cdc/core/helpers/cove/date'
|
|
9
9
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
10
|
+
import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
|
|
10
11
|
|
|
11
12
|
interface Props {
|
|
12
13
|
xScaleBrush: ScaleLinear<number, number>
|
|
@@ -15,7 +16,7 @@ interface Props {
|
|
|
15
16
|
yMax: number
|
|
16
17
|
}
|
|
17
18
|
const ZoomBrush: FC<Props> = props => {
|
|
18
|
-
const { tableData, config, parseDate, formatDate, setBrushConfig,
|
|
19
|
+
const { tableData, config, parseDate, formatDate, setBrushConfig, dashboardConfig } = useContext(ConfigContext)
|
|
19
20
|
const sharedFilters = dashboardConfig?.dashboard?.sharedFilters ?? []
|
|
20
21
|
const isDashboardFilters = sharedFilters?.length > 0
|
|
21
22
|
const { fontSize } = useBarChart()
|
|
@@ -175,7 +176,6 @@ const ZoomBrush: FC<Props> = props => {
|
|
|
175
176
|
<BrushHandle
|
|
176
177
|
left={Number(config.runtime.yAxis.size)}
|
|
177
178
|
showTooltip={showTooltip}
|
|
178
|
-
getTextWidth={getTextWidth}
|
|
179
179
|
pixelDistance={textProps.endPosition - textProps.startPosition}
|
|
180
180
|
textProps={textProps}
|
|
181
181
|
fontSize={fontSize[config.fontSize]}
|
|
@@ -202,7 +202,7 @@ const ZoomBrush: FC<Props> = props => {
|
|
|
202
202
|
}
|
|
203
203
|
|
|
204
204
|
const BrushHandle = props => {
|
|
205
|
-
const { x, isBrushActive, isBrushing, className, textProps, fontSize, showTooltip, left
|
|
205
|
+
const { x, isBrushActive, isBrushing, className, textProps, fontSize, showTooltip, left } = props
|
|
206
206
|
const pathWidth = 8
|
|
207
207
|
if (!isBrushActive) {
|
|
208
208
|
return null
|
|
@@ -217,15 +217,34 @@ const BrushHandle = props => {
|
|
|
217
217
|
return (
|
|
218
218
|
<>
|
|
219
219
|
{showTooltip && (
|
|
220
|
-
<Text
|
|
220
|
+
<Text
|
|
221
|
+
x={(Number(textProps.xMax) - textWidth) / 2}
|
|
222
|
+
dy={-12}
|
|
223
|
+
pointerEvents='visiblePainted'
|
|
224
|
+
fontSize={fontSize / 1.1}
|
|
225
|
+
>
|
|
221
226
|
{tooltipText}
|
|
222
227
|
</Text>
|
|
223
228
|
)}
|
|
224
229
|
<Group left={x + pathWidth / 2} top={-2}>
|
|
225
|
-
<Text
|
|
230
|
+
<Text
|
|
231
|
+
pointerEvents='visiblePainted'
|
|
232
|
+
dominantBaseline='hanging'
|
|
233
|
+
x={isLeft ? 55 : -50}
|
|
234
|
+
y={25}
|
|
235
|
+
verticalAnchor='start'
|
|
236
|
+
textAnchor={textAnchor}
|
|
237
|
+
fontSize={fontSize / 1.4}
|
|
238
|
+
>
|
|
226
239
|
{isLeft ? textProps.startValue : textProps.endValue}
|
|
227
240
|
</Text>
|
|
228
|
-
<path
|
|
241
|
+
<path
|
|
242
|
+
cursor='ew-resize'
|
|
243
|
+
d='M0.5,10A6,6 0 0 1 6.5,16V14A6,6 0 0 1 0.5,20ZM2.5,18V12M4.5,18V12'
|
|
244
|
+
fill={'#297EF1'}
|
|
245
|
+
strokeWidth='1'
|
|
246
|
+
transform={transform}
|
|
247
|
+
></path>
|
|
229
248
|
</Group>
|
|
230
249
|
</>
|
|
231
250
|
)
|
|
@@ -28,6 +28,7 @@ export default {
|
|
|
28
28
|
showDownloadButton: false,
|
|
29
29
|
showMissingDataLabel: true,
|
|
30
30
|
showSuppressedSymbol: true,
|
|
31
|
+
showZeroValueData: true,
|
|
31
32
|
hideNullValue: true
|
|
32
33
|
},
|
|
33
34
|
padding: {
|
|
@@ -122,10 +123,12 @@ export default {
|
|
|
122
123
|
tickLabelColor: '#333',
|
|
123
124
|
tickColor: '#333',
|
|
124
125
|
numTicks: '',
|
|
125
|
-
labelOffset:
|
|
126
|
+
labelOffset: 0,
|
|
126
127
|
axisPadding: 200,
|
|
127
128
|
target: 0,
|
|
128
|
-
maxTickRotation: 0
|
|
129
|
+
maxTickRotation: 0,
|
|
130
|
+
padding: 5,
|
|
131
|
+
showYearsOnce: false
|
|
129
132
|
},
|
|
130
133
|
table: {
|
|
131
134
|
label: 'Data Table',
|
|
@@ -135,6 +138,7 @@ export default {
|
|
|
135
138
|
caption: '',
|
|
136
139
|
showDownloadUrl: false,
|
|
137
140
|
showDataTableLink: true,
|
|
141
|
+
showDownloadLinkBelow: true,
|
|
138
142
|
indexLabel: '',
|
|
139
143
|
download: false,
|
|
140
144
|
showVertical: true,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { cloneDeep } from 'lodash'
|
|
2
|
+
import { ChartConfig } from '../types/ChartConfig'
|
|
3
|
+
|
|
4
|
+
/* editConfigKeys
|
|
5
|
+
* Add edit or update config keys
|
|
6
|
+
* keyUpdates: { path: string[], value: any }[]
|
|
7
|
+
* path is the array of keys needed to reach the value to be updated
|
|
8
|
+
* value is the new value to be set
|
|
9
|
+
* if the key does not exist, it will be created
|
|
10
|
+
*/
|
|
11
|
+
export function editConfigKeys(config: ChartConfig, keyUpdates: { path: string[]; value: any }[]): ChartConfig {
|
|
12
|
+
const configDeepCopy = cloneDeep(config)
|
|
13
|
+
|
|
14
|
+
const newConfig = keyUpdates.reduce((acc, { path, value }) => {
|
|
15
|
+
const pathCopy = [...path]
|
|
16
|
+
const lastKey = pathCopy.pop()
|
|
17
|
+
const target = pathCopy.reduce((target, key) => {
|
|
18
|
+
if (!target[key]) {
|
|
19
|
+
target[key] = {}
|
|
20
|
+
}
|
|
21
|
+
return target[key]
|
|
22
|
+
}, acc)
|
|
23
|
+
target[lastKey] = value
|
|
24
|
+
return acc
|
|
25
|
+
}, configDeepCopy)
|
|
26
|
+
|
|
27
|
+
return newConfig
|
|
28
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ChartConfig } from '../types/ChartConfig'
|
|
2
|
+
|
|
3
|
+
const getNumericValue = number => {
|
|
4
|
+
if (typeof number === 'string') return parseFloat(number.replace(/,/g, ''))
|
|
5
|
+
return Number(number)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const handleRankByValue = (data, passedConfig: ChartConfig) => {
|
|
9
|
+
if (passedConfig.rankByValue) {
|
|
10
|
+
const series = passedConfig.series[0].dataKey
|
|
11
|
+
const sorted = data.sort((a, b) => getNumericValue(a[series]) - getNumericValue(b[series]))
|
|
12
|
+
return passedConfig.rankByValue === 'asc' ? sorted : sorted.reverse()
|
|
13
|
+
}
|
|
14
|
+
return data
|
|
15
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { isMobileHeightViewport } from '@cdc/core/helpers/viewports'
|
|
2
|
+
import { ChartConfig, ViewportSize } from '../types/ChartConfig'
|
|
3
|
+
|
|
4
|
+
export function getOrientation(
|
|
5
|
+
{ orientation, heights, visualizationType }: Pick<ChartConfig, 'orientation' | 'heights' | 'visualizationType'>,
|
|
6
|
+
currentViewport: ViewportSize
|
|
7
|
+
): 'vertical' | 'horizontal' | 'mobileVertical' {
|
|
8
|
+
const isForestPlot = visualizationType === 'Forest Plot'
|
|
9
|
+
const useVertical = orientation === 'vertical' || isForestPlot
|
|
10
|
+
const useMobileVertical = heights?.mobileVertical && isMobileHeightViewport(currentViewport)
|
|
11
|
+
const responsiveVertical = useMobileVertical ? 'mobileVertical' : 'vertical'
|
|
12
|
+
|
|
13
|
+
return useVertical ? responsiveVertical : 'horizontal'
|
|
14
|
+
}
|
|
15
|
+
export function calcInitialHeight(
|
|
16
|
+
{ heights, orientation, visualizationType }: Pick<ChartConfig, 'heights' | 'orientation' | 'visualizationType'>,
|
|
17
|
+
currentViewport: ViewportSize
|
|
18
|
+
): number {
|
|
19
|
+
// if no heights are provided assume config has not been loaded
|
|
20
|
+
if (!heights) return 0
|
|
21
|
+
|
|
22
|
+
const renderedOrientation = getOrientation({ orientation, heights, visualizationType }, currentViewport)
|
|
23
|
+
const height = Number(heights?.[renderedOrientation])
|
|
24
|
+
return isNaN(height) ? 0 : height
|
|
25
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { handleRankByValue } from '../handleRankByValue'
|
|
2
|
+
import { ChartConfig } from '../../types/ChartConfig'
|
|
3
|
+
|
|
4
|
+
describe('handleRankByValue', () => {
|
|
5
|
+
it('should sort the data in ascending order when rankByValue is "asc"', () => {
|
|
6
|
+
const data = [{ value: 3 }, { value: 1 }, { value: 2 }]
|
|
7
|
+
const config: ChartConfig = {
|
|
8
|
+
rankByValue: 'asc',
|
|
9
|
+
series: [{ dataKey: 'value' }]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const result = handleRankByValue(data, config)
|
|
13
|
+
expect(result).toEqual([{ value: 1 }, { value: 2 }, { value: 3 }])
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('should sort the data in descending order when rankByValue is "desc"', () => {
|
|
17
|
+
const data = [{ value: 3 }, { value: 1 }, { value: 2 }]
|
|
18
|
+
const config: ChartConfig = {
|
|
19
|
+
rankByValue: 'desc',
|
|
20
|
+
series: [{ dataKey: 'value' }]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const result = handleRankByValue(data, config)
|
|
24
|
+
expect(result).toEqual([{ value: 3 }, { value: 2 }, { value: 1 }])
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should handle numeric strings correctly', () => {
|
|
28
|
+
const data = [{ value: '3' }, { value: '1' }, { value: '2' }]
|
|
29
|
+
const config: ChartConfig = {
|
|
30
|
+
rankByValue: 'asc',
|
|
31
|
+
series: [{ dataKey: 'value' }]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const result = handleRankByValue(data, config)
|
|
35
|
+
expect(result).toEqual([{ value: '1' }, { value: '2' }, { value: '3' }])
|
|
36
|
+
})
|
|
37
|
+
})
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { calcInitialHeight, getOrientation } from '../sizeHelpers'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { ChartOrientation, VisualizationType } from '../../types/ChartConfig'
|
|
4
|
+
|
|
5
|
+
describe('sizeHelpers', () => {
|
|
6
|
+
describe('getOrientation', () => {
|
|
7
|
+
it("should return 'vertical' when orientation is vertical", () => {
|
|
8
|
+
const config = {
|
|
9
|
+
orientation: 'vertical' as ChartOrientation,
|
|
10
|
+
heights: { mobileVertical: 0, vertical: 0, horizontal: 0 },
|
|
11
|
+
visualizationType: 'Bar' as VisualizationType
|
|
12
|
+
}
|
|
13
|
+
expect(getOrientation(config, 'md')).toBe('vertical')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it("should return 'horizontal' when orientation is horizontal", () => {
|
|
17
|
+
const config = {
|
|
18
|
+
orientation: 'horizontal' as ChartOrientation,
|
|
19
|
+
heights: { mobileVertical: 0, vertical: 0, horizontal: 0 },
|
|
20
|
+
visualizationType: 'Bar' as VisualizationType
|
|
21
|
+
}
|
|
22
|
+
expect(getOrientation(config, 'md')).toBe('horizontal')
|
|
23
|
+
})
|
|
24
|
+
it("should return 'vertical' when orientation is horizontal but visualizationType is 'Forest Plot'", () => {
|
|
25
|
+
const config = {
|
|
26
|
+
orientation: 'horizontal' as ChartOrientation,
|
|
27
|
+
heights: { mobileVertical: 0, vertical: 0, horizontal: 0 },
|
|
28
|
+
visualizationType: 'Forest Plot' as VisualizationType
|
|
29
|
+
}
|
|
30
|
+
expect(getOrientation(config, 'md')).toBe('vertical')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should return mobileVertical when viewport is mobile height', () => {
|
|
34
|
+
const config = {
|
|
35
|
+
orientation: 'vertical' as ChartOrientation,
|
|
36
|
+
heights: { mobileVertical: 100, vertical: 0, horizontal: 0 },
|
|
37
|
+
visualizationType: 'Bar' as VisualizationType
|
|
38
|
+
}
|
|
39
|
+
expect(getOrientation(config, 'xxs')).toBe('mobileVertical')
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('calcInitialHeight', () => {
|
|
45
|
+
it('should return 0 when no heights are provided', () => {
|
|
46
|
+
const config = {
|
|
47
|
+
heights: undefined,
|
|
48
|
+
orientation: 'vertical' as ChartOrientation,
|
|
49
|
+
visualizationType: 'Bar' as VisualizationType
|
|
50
|
+
}
|
|
51
|
+
expect(calcInitialHeight(config, 'md')).toBe(0)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should return vertical height when orientation is vertical', () => {
|
|
55
|
+
const config = {
|
|
56
|
+
orientation: 'vertical' as ChartOrientation,
|
|
57
|
+
heights: { mobileVertical: 0, vertical: 100, horizontal: 0 },
|
|
58
|
+
visualizationType: 'Bar' as VisualizationType
|
|
59
|
+
}
|
|
60
|
+
expect(calcInitialHeight(config, 'md')).toBe(100)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('should return horizontal height when orientation is horizontal', () => {
|
|
64
|
+
const config = {
|
|
65
|
+
orientation: 'horizontal' as ChartOrientation,
|
|
66
|
+
heights: { mobileVertical: 0, vertical: 0, horizontal: 100 },
|
|
67
|
+
visualizationType: 'Bar' as VisualizationType
|
|
68
|
+
}
|
|
69
|
+
expect(calcInitialHeight(config, 'md')).toBe(100)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should return mobileVertical height when viewport is mobile height', () => {
|
|
73
|
+
const config = {
|
|
74
|
+
heights: { mobileVertical: 100, vertical: 0, horizontal: 0 },
|
|
75
|
+
orientation: 'vertical' as ChartOrientation,
|
|
76
|
+
visualizationType: 'Bar' as VisualizationType
|
|
77
|
+
}
|
|
78
|
+
expect(calcInitialHeight(config, 'xxs')).toBe(100)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -5,11 +5,14 @@ export const useColorPalette = (config, updateConfig) => {
|
|
|
5
5
|
let twoColorPalettes = []
|
|
6
6
|
let sequential = []
|
|
7
7
|
let nonSequential = []
|
|
8
|
+
const accessibleColors = []
|
|
8
9
|
|
|
9
10
|
// Get two color palettes if visualization type is Paired Bar
|
|
10
11
|
if (config.visualizationType === 'Paired Bar' || config.visualizationType === 'Deviation Bar') {
|
|
11
12
|
const isReversed = config.twoColor.isPaletteReversed
|
|
12
|
-
twoColorPalettes = Object.keys(twoColorPalette).filter(name =>
|
|
13
|
+
twoColorPalettes = Object.keys(twoColorPalette).filter(name =>
|
|
14
|
+
isReversed ? name.endsWith('reverse') : !name.endsWith('reverse')
|
|
15
|
+
)
|
|
13
16
|
} else {
|
|
14
17
|
// Get sequential and non-sequential palettes for other visualization types
|
|
15
18
|
const seqPalettes = []
|
|
@@ -18,6 +21,7 @@ export const useColorPalette = (config, updateConfig) => {
|
|
|
18
21
|
for (const paletteName in colorPalettesChart) {
|
|
19
22
|
const isSequential = paletteName.startsWith('sequential')
|
|
20
23
|
const isQualitative = paletteName.startsWith('qualitative')
|
|
24
|
+
const colorblindsafe = paletteName.startsWith('colorblindsafe')
|
|
21
25
|
const isReversed = paletteName.endsWith('reverse')
|
|
22
26
|
|
|
23
27
|
if (isSequential && ((!config.isPaletteReversed && !isReversed) || (config.isPaletteReversed && isReversed))) {
|
|
@@ -27,6 +31,9 @@ export const useColorPalette = (config, updateConfig) => {
|
|
|
27
31
|
if (isQualitative && ((!config.isPaletteReversed && !isReversed) || (config.isPaletteReversed && isReversed))) {
|
|
28
32
|
nonSeqPalettes.push(paletteName)
|
|
29
33
|
}
|
|
34
|
+
if (colorblindsafe && ((!config.isPaletteReversed && !isReversed) || (config.isPaletteReversed && isReversed))) {
|
|
35
|
+
accessibleColors.push(paletteName)
|
|
36
|
+
}
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
sequential = seqPalettes
|
|
@@ -64,5 +71,6 @@ export const useColorPalette = (config, updateConfig) => {
|
|
|
64
71
|
}, [config.isPaletteReversed])
|
|
65
72
|
|
|
66
73
|
// Return all palettes
|
|
67
|
-
|
|
74
|
+
|
|
75
|
+
return { twoColorPalettes, sequential, nonSequential, accessibleColors }
|
|
68
76
|
}
|
|
@@ -60,6 +60,10 @@ const useLegendClasses = (config: ConfigType) => {
|
|
|
60
60
|
containerClasses.push('no-border')
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
if (config.legend.hideBorder.topBottom && ['top'].includes(config.legend.position)) {
|
|
64
|
+
containerClasses.push('p-0')
|
|
65
|
+
}
|
|
66
|
+
|
|
63
67
|
return {
|
|
64
68
|
containerClasses,
|
|
65
69
|
innerClasses
|
package/src/hooks/useScales.ts
CHANGED
|
@@ -72,7 +72,10 @@ const useScales = (properties: useScaleProps) => {
|
|
|
72
72
|
let xAxisMin = Math.min(...xAxisDataMapped.map(Number))
|
|
73
73
|
let xAxisMax = Math.max(...xAxisDataMapped.map(Number))
|
|
74
74
|
xAxisMin -= (config.xAxis.padding ? config.xAxis.padding * 0.01 : 0) * (xAxisMax - xAxisMin)
|
|
75
|
-
xAxisMax +=
|
|
75
|
+
xAxisMax +=
|
|
76
|
+
visualizationType === 'Line'
|
|
77
|
+
? 0
|
|
78
|
+
: (config.xAxis.padding ? config.xAxis.padding * 0.01 : 0) * (xAxisMax - xAxisMin)
|
|
76
79
|
xScale = scaleTime({
|
|
77
80
|
domain: [xAxisMin, xAxisMax],
|
|
78
81
|
range: [0, xMax]
|
|
@@ -264,14 +267,34 @@ const useScales = (properties: useScaleProps) => {
|
|
|
264
267
|
|
|
265
268
|
export default useScales
|
|
266
269
|
|
|
267
|
-
export const
|
|
270
|
+
export const getFirstDayOfMonth = ms => {
|
|
271
|
+
const date = new Date(ms)
|
|
272
|
+
return new Date(date.getFullYear(), date.getMonth(), 1).getTime()
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export const dateFormatHasMonthButNoDays = dateFormat => {
|
|
276
|
+
return (
|
|
277
|
+
(dateFormat.includes('%b') ||
|
|
278
|
+
dateFormat.includes('%B') ||
|
|
279
|
+
dateFormat.includes('%m') ||
|
|
280
|
+
dateFormat.includes('%-m') ||
|
|
281
|
+
dateFormat.includes('%_m')) &&
|
|
282
|
+
!dateFormat.includes('%d') &&
|
|
283
|
+
!dateFormat.includes('%-d') &&
|
|
284
|
+
!dateFormat.includes('%_d') &&
|
|
285
|
+
!dateFormat.includes('%e')
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export const getTickValues = (xAxisDataMapped, xScale, num, config) => {
|
|
268
290
|
const xDomain = xScale.domain()
|
|
269
291
|
|
|
270
292
|
if (xScale.type === 'time') {
|
|
271
293
|
const xDomainMax = xAxisDataMapped[xAxisDataMapped.length - 1]
|
|
272
294
|
const xDomainMin = xAxisDataMapped[0]
|
|
295
|
+
|
|
273
296
|
const step = (xDomainMax - xDomainMin) / (num - 1)
|
|
274
|
-
|
|
297
|
+
let tickValues = []
|
|
275
298
|
for (let i = xDomainMax; i >= xDomainMin; i -= step) {
|
|
276
299
|
tickValues.push(i)
|
|
277
300
|
}
|
|
@@ -280,6 +303,11 @@ export const getTickValues = (xAxisDataMapped, xScale, num) => {
|
|
|
280
303
|
}
|
|
281
304
|
tickValues.reverse()
|
|
282
305
|
|
|
306
|
+
// Use first days of months when showing months without days
|
|
307
|
+
if (dateFormatHasMonthButNoDays(config.xAxis.dateDisplayFormat)) {
|
|
308
|
+
tickValues = tickValues.map(tv => getFirstDayOfMonth(tv))
|
|
309
|
+
}
|
|
310
|
+
|
|
283
311
|
return tickValues
|
|
284
312
|
}
|
|
285
313
|
|
package/src/hooks/useTooltip.tsx
CHANGED
|
@@ -58,10 +58,7 @@ export const useTooltip = props => {
|
|
|
58
58
|
const showMissingDataValue = config.general.showMissingDataLabel && (!value || value === 'null')
|
|
59
59
|
let formattedValue = seriesKey === config.xAxis.dataKey ? value : formatNumber(value, getAxisPosition(seriesKey))
|
|
60
60
|
|
|
61
|
-
formattedValue =
|
|
62
|
-
showMissingDataValue && (config.visualizationSubType === 'stacked' ? !config.general.hideNullValue : true)
|
|
63
|
-
? 'N/A'
|
|
64
|
-
: formattedValue
|
|
61
|
+
formattedValue = showMissingDataValue ? 'N/A' : formattedValue
|
|
65
62
|
|
|
66
63
|
return formattedValue
|
|
67
64
|
}
|
|
@@ -199,7 +196,14 @@ export const useTooltip = props => {
|
|
|
199
196
|
?.flatMap(seriesKey => {
|
|
200
197
|
const value = resolvedScaleValues[0]?.[seriesKey]
|
|
201
198
|
const formattedValue = getFormattedValue(seriesKey, value, config, getAxisPosition)
|
|
202
|
-
|
|
199
|
+
if (
|
|
200
|
+
(value === null || value === undefined || value === '' || formattedValue === 'N/A') &&
|
|
201
|
+
config.general.hideNullValue
|
|
202
|
+
) {
|
|
203
|
+
return []
|
|
204
|
+
} else {
|
|
205
|
+
return [[seriesKey, formattedValue, getAxisPosition(seriesKey)]]
|
|
206
|
+
}
|
|
203
207
|
})
|
|
204
208
|
)
|
|
205
209
|
}
|