@cdc/chart 4.24.10 → 4.24.11
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 +34618 -33995
- package/examples/feature/boxplot/boxplot-data.json +88 -22
- package/examples/feature/boxplot/boxplot.json +540 -16
- package/examples/feature/boxplot/testing.csv +7 -7
- package/examples/feature/sankey/sankey-example-data.json +0 -1
- package/examples/private/test.json +20092 -0
- package/index.html +3 -3
- package/package.json +2 -2
- package/src/CdcChart.tsx +86 -86
- package/src/_stories/Chart.CustomColors.stories.tsx +19 -0
- package/src/_stories/Chart.DynamicSeries.stories.tsx +27 -0
- package/src/_stories/Chart.Legend.Gradient.stories.tsx +42 -1
- package/src/_stories/Chart.stories.tsx +7 -8
- package/src/_stories/ChartEditor.stories.tsx +27 -0
- package/src/_stories/ChartLine.Suppression.stories.tsx +25 -0
- package/src/_stories/ChartPrefixSuffix.stories.tsx +8 -0
- package/src/_stories/_mock/boxplot_multiseries.json +647 -0
- package/src/_stories/_mock/dynamic_series_bar_config.json +723 -0
- package/src/_stories/_mock/dynamic_series_config.json +979 -0
- package/{examples/feature/scatterplot/scatterplot.json → src/_stories/_mock/scatterplot_mock.json} +62 -92
- package/src/_stories/_mock/suppression_mock.json +1549 -0
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +43 -9
- package/src/components/BarChart/components/BarChart.Vertical.tsx +60 -42
- package/src/components/BarChart/helpers/index.ts +1 -2
- package/src/components/BoxPlot/BoxPlot.tsx +189 -0
- package/src/components/EditorPanel/EditorPanel.tsx +64 -62
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +3 -3
- package/src/components/EditorPanel/components/Panels/Panel.BoxPlot.tsx +51 -6
- package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +40 -9
- package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +3 -3
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +121 -56
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +1 -2
- package/src/components/EditorPanel/useEditorPermissions.ts +15 -1
- package/src/components/Legend/Legend.Component.tsx +9 -10
- package/src/components/Legend/Legend.tsx +16 -16
- package/src/components/LineChart/helpers.ts +48 -43
- package/src/components/LineChart/index.tsx +88 -82
- package/src/components/LinearChart.tsx +17 -10
- package/src/components/Sankey/index.tsx +50 -32
- package/src/components/Sankey/sankey.scss +6 -5
- package/src/components/Sankey/useSankeyAlert.tsx +60 -0
- package/src/components/ScatterPlot/ScatterPlot.jsx +20 -4
- package/src/data/initial-state.js +3 -9
- package/src/hooks/useLegendClasses.ts +10 -23
- package/src/hooks/useMinMax.ts +27 -13
- package/src/hooks/useReduceData.ts +43 -10
- package/src/hooks/useScales.ts +56 -35
- package/src/hooks/useTooltip.tsx +54 -49
- package/src/scss/main.scss +0 -18
- package/src/types/ChartConfig.ts +6 -19
- package/src/types/ChartContext.ts +4 -1
- package/src/types/ForestPlot.ts +8 -0
- package/src/components/BoxPlot/BoxPlot.jsx +0 -111
|
@@ -17,6 +17,7 @@ import useRightAxis from '../../hooks/useRightAxis'
|
|
|
17
17
|
import { filterCircles, createStyles, createDataSegments } from './helpers'
|
|
18
18
|
import LineChartCircle from './components/LineChart.Circle'
|
|
19
19
|
import LineChartBumpCircle from './components/LineChart.BumpCircle'
|
|
20
|
+
import isNumber from '@cdc/core/helpers/isNumber'
|
|
20
21
|
|
|
21
22
|
// Types
|
|
22
23
|
import { type ChartContext } from '../../types/ChartContext'
|
|
@@ -38,7 +39,7 @@ const LineChart = (props: LineChartProps) => {
|
|
|
38
39
|
} = props
|
|
39
40
|
|
|
40
41
|
// prettier-ignore
|
|
41
|
-
const { colorScale, config, formatNumber, handleLineType,
|
|
42
|
+
const { colorScale, config, formatNumber, handleLineType, parseDate, seriesHighlight, tableData, transformedData, updateConfig, brushConfig,clean } = useContext<ChartContext>(ConfigContext)
|
|
42
43
|
const { yScaleRight } = useRightAxis({ config, yMax, data: transformedData, updateConfig })
|
|
43
44
|
if (!handleTooltipMouseOver) return
|
|
44
45
|
|
|
@@ -51,53 +52,51 @@ const LineChart = (props: LineChartProps) => {
|
|
|
51
52
|
data = clean(brushConfig.data)
|
|
52
53
|
tableD = clean(brushConfig.data)
|
|
53
54
|
}
|
|
55
|
+
|
|
56
|
+
const xPos = d => {
|
|
57
|
+
return xScale(getXAxisData(d)) + (xScale.bandwidth ? xScale.bandwidth() / 2 : 0)
|
|
58
|
+
}
|
|
59
|
+
|
|
54
60
|
return (
|
|
55
61
|
<ErrorBoundary component='LineChart'>
|
|
56
62
|
<Group left={Number(config.runtime.yAxis.size)}>
|
|
57
63
|
{' '}
|
|
58
64
|
{/* left - expects a number not a string */}
|
|
59
65
|
{(config.runtime.lineSeriesKeys || config.runtime.seriesKeys).map((seriesKey, index) => {
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
const seriesAxis = seriesData
|
|
63
|
-
|
|
66
|
+
const seriesData = config.runtime.series.find(item => item.dataKey === seriesKey)
|
|
67
|
+
const lineType = seriesData.type
|
|
68
|
+
const seriesAxis = seriesData.axis || 'left'
|
|
69
|
+
const displayArea =
|
|
64
70
|
legend.behavior === 'highlight' || seriesHighlight.length === 0 || seriesHighlight.indexOf(seriesKey) !== -1
|
|
65
|
-
|
|
66
|
-
// styles for preliminary Data items
|
|
67
|
-
let styles = createStyles({
|
|
68
|
-
preliminaryData: config.preliminaryData,
|
|
69
|
-
data: tableD,
|
|
70
|
-
stroke: colorScale(config.runtime.seriesLabels[seriesKey]),
|
|
71
|
-
strokeWidth: seriesData[0].weight || 2,
|
|
72
|
-
handleLineType,
|
|
73
|
-
lineType,
|
|
74
|
-
seriesKey
|
|
75
|
-
})
|
|
71
|
+
|
|
76
72
|
const suppressedSegments = createDataSegments(
|
|
77
73
|
tableData,
|
|
78
74
|
seriesKey,
|
|
79
75
|
config.preliminaryData,
|
|
80
76
|
config.xAxis.dataKey
|
|
81
77
|
)
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
return xScale(getXAxisData(d)) + (xScale.bandwidth ? xScale.bandwidth() / 2 : 0)
|
|
85
|
-
}
|
|
78
|
+
const isSplitLine =
|
|
79
|
+
config?.preliminaryData?.filter(pd => pd.style && !pd.style.includes('Circles')).length > 0
|
|
86
80
|
|
|
81
|
+
const _data = seriesData.dynamicCategory
|
|
82
|
+
? data.filter(d => d[seriesData.dynamicCategory] === seriesKey)
|
|
83
|
+
: data
|
|
84
|
+
const _seriesKey = seriesData.dynamicCategory ? seriesData.originalDataKey : seriesKey
|
|
85
|
+
const circleData = filterCircles(config?.preliminaryData, tableD, _seriesKey)
|
|
87
86
|
return (
|
|
88
87
|
<Group
|
|
89
|
-
key={`series-${seriesKey}`}
|
|
88
|
+
key={`series-${seriesKey}-${index}`}
|
|
90
89
|
opacity={
|
|
91
90
|
legend.behavior === 'highlight' &&
|
|
92
91
|
seriesHighlight.length > 0 &&
|
|
93
|
-
seriesHighlight.indexOf(
|
|
92
|
+
seriesHighlight.indexOf(_seriesKey) === -1
|
|
94
93
|
? 0.5
|
|
95
94
|
: 1
|
|
96
95
|
}
|
|
97
96
|
display={
|
|
98
97
|
legend.behavior === 'highlight' ||
|
|
99
98
|
(seriesHighlight.length === 0 && !legend.dynamicLegend) ||
|
|
100
|
-
seriesHighlight.indexOf(
|
|
99
|
+
seriesHighlight.indexOf(_seriesKey) !== -1
|
|
101
100
|
? 'block'
|
|
102
101
|
: 'none'
|
|
103
102
|
}
|
|
@@ -113,12 +112,9 @@ const LineChart = (props: LineChartProps) => {
|
|
|
113
112
|
onMouseOut={handleTooltipMouseOff}
|
|
114
113
|
onClick={e => handleTooltipClick(e, data)}
|
|
115
114
|
/>
|
|
116
|
-
{
|
|
115
|
+
{_data.map((d, dataIndex) => {
|
|
117
116
|
return (
|
|
118
|
-
d[
|
|
119
|
-
d[seriesKey] !== '' &&
|
|
120
|
-
d[seriesKey] !== null &&
|
|
121
|
-
isNumber(d[seriesKey]) && (
|
|
117
|
+
isNumber(d[_seriesKey]) && (
|
|
122
118
|
<React.Fragment key={`series-${seriesKey}-point-${dataIndex}`}>
|
|
123
119
|
{/* Render label */}
|
|
124
120
|
{config.labels && (
|
|
@@ -126,13 +122,13 @@ const LineChart = (props: LineChartProps) => {
|
|
|
126
122
|
x={xPos(d)}
|
|
127
123
|
y={
|
|
128
124
|
seriesAxis === 'Right'
|
|
129
|
-
? yScaleRight(getYAxisData(d,
|
|
130
|
-
: yScale(getYAxisData(d,
|
|
125
|
+
? yScaleRight(getYAxisData(d, _seriesKey))
|
|
126
|
+
: yScale(getYAxisData(d, _seriesKey))
|
|
131
127
|
}
|
|
132
128
|
fill={'#000'}
|
|
133
129
|
textAnchor='middle'
|
|
134
130
|
>
|
|
135
|
-
{formatNumber(d[
|
|
131
|
+
{formatNumber(d[_seriesKey], 'left')}
|
|
136
132
|
</Text>
|
|
137
133
|
)}
|
|
138
134
|
|
|
@@ -142,10 +138,10 @@ const LineChart = (props: LineChartProps) => {
|
|
|
142
138
|
dataIndex={dataIndex}
|
|
143
139
|
circleData={circleData}
|
|
144
140
|
tableData={tableData}
|
|
145
|
-
data={
|
|
141
|
+
data={_data}
|
|
146
142
|
d={d}
|
|
147
143
|
config={config}
|
|
148
|
-
seriesKey={
|
|
144
|
+
seriesKey={_seriesKey}
|
|
149
145
|
displayArea={displayArea}
|
|
150
146
|
tooltipData={tooltipData}
|
|
151
147
|
xScale={xScale}
|
|
@@ -163,10 +159,10 @@ const LineChart = (props: LineChartProps) => {
|
|
|
163
159
|
dataIndex={dataIndex}
|
|
164
160
|
tableData={tableData}
|
|
165
161
|
circleData={circleData}
|
|
166
|
-
data={
|
|
162
|
+
data={_data}
|
|
167
163
|
d={d}
|
|
168
164
|
config={config}
|
|
169
|
-
seriesKey={
|
|
165
|
+
seriesKey={_seriesKey}
|
|
170
166
|
displayArea={displayArea}
|
|
171
167
|
tooltipData={tooltipData}
|
|
172
168
|
xScale={xScale}
|
|
@@ -188,7 +184,7 @@ const LineChart = (props: LineChartProps) => {
|
|
|
188
184
|
dataIndex={0}
|
|
189
185
|
mode='HOVER_POINTS'
|
|
190
186
|
circleData={circleData}
|
|
191
|
-
data={
|
|
187
|
+
data={_data}
|
|
192
188
|
config={config}
|
|
193
189
|
seriesKey={seriesKey}
|
|
194
190
|
displayArea={displayArea}
|
|
@@ -203,78 +199,88 @@ const LineChart = (props: LineChartProps) => {
|
|
|
203
199
|
)}
|
|
204
200
|
</>
|
|
205
201
|
{/* SPLIT LINE */}
|
|
206
|
-
{
|
|
202
|
+
{isSplitLine ? (
|
|
207
203
|
<>
|
|
208
204
|
<SplitLinePath
|
|
209
|
-
curve={allCurves[seriesData
|
|
210
|
-
segments={
|
|
205
|
+
curve={allCurves[seriesData.lineType]}
|
|
206
|
+
segments={_data.map(d => [d])}
|
|
211
207
|
segmentation='x'
|
|
212
208
|
x={d => xPos(d)}
|
|
213
209
|
y={d =>
|
|
214
210
|
seriesAxis === 'Right'
|
|
215
|
-
? yScaleRight(getYAxisData(d,
|
|
216
|
-
: yScale(Number(getYAxisData(d,
|
|
211
|
+
? yScaleRight(getYAxisData(d, _seriesKey))
|
|
212
|
+
: yScale(Number(getYAxisData(d, _seriesKey)))
|
|
217
213
|
}
|
|
218
|
-
styles={
|
|
214
|
+
styles={createStyles({
|
|
215
|
+
preliminaryData: config.preliminaryData,
|
|
216
|
+
data: tableD,
|
|
217
|
+
stroke: colorScale(config.runtime.seriesLabels[seriesKey]),
|
|
218
|
+
strokeWidth: seriesData.weight || 2,
|
|
219
|
+
handleLineType,
|
|
220
|
+
lineType,
|
|
221
|
+
seriesKey
|
|
222
|
+
})}
|
|
219
223
|
defined={(item, i) => {
|
|
220
224
|
return item[seriesKey] !== '' && item[seriesKey] !== null && item[seriesKey] !== undefined
|
|
221
225
|
}}
|
|
222
226
|
/>
|
|
223
227
|
|
|
224
228
|
{suppressedSegments.map((segment, index) => {
|
|
225
|
-
return (
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
229
|
+
return Object.entries(segment.data).map(([key, value]) => {
|
|
230
|
+
return (
|
|
231
|
+
<LinePath
|
|
232
|
+
key={index}
|
|
233
|
+
data={value}
|
|
234
|
+
x={d => xPos(d)}
|
|
235
|
+
y={d =>
|
|
236
|
+
seriesAxis === 'Right'
|
|
237
|
+
? yScaleRight(getYAxisData(d, seriesKey))
|
|
238
|
+
: yScale(Number(getYAxisData(d, seriesKey)))
|
|
239
|
+
}
|
|
240
|
+
stroke={colorScale(config.runtime.seriesLabels[seriesKey])}
|
|
241
|
+
strokeWidth={seriesData[0]?.weight || 2}
|
|
242
|
+
strokeOpacity={1}
|
|
243
|
+
shapeRendering='geometricPrecision'
|
|
244
|
+
strokeDasharray={handleLineType(segment.style)}
|
|
245
|
+
defined={(item, i) => {
|
|
246
|
+
return item[seriesKey] !== '' && item[seriesKey] !== null && item[seriesKey] !== undefined
|
|
247
|
+
}}
|
|
248
|
+
/>
|
|
249
|
+
)
|
|
250
|
+
})
|
|
245
251
|
})}
|
|
246
252
|
</>
|
|
247
253
|
) : (
|
|
248
254
|
<>
|
|
249
255
|
{/* STANDARD LINE */}
|
|
250
256
|
<LinePath
|
|
251
|
-
curve={allCurves[seriesData
|
|
257
|
+
curve={allCurves[seriesData.lineType]}
|
|
252
258
|
data={
|
|
253
259
|
config.visualizationType == 'Bump Chart'
|
|
254
|
-
?
|
|
260
|
+
? _data
|
|
255
261
|
: config.xAxis.type === 'date-time' || config.xAxis.type === 'date'
|
|
256
|
-
?
|
|
262
|
+
? _data.sort((d1, d2) => {
|
|
257
263
|
let x1 = getXAxisData(d1)
|
|
258
264
|
let x2 = getXAxisData(d2)
|
|
259
265
|
if (x1 < x2) return -1
|
|
260
266
|
if (x2 < x1) return 1
|
|
261
267
|
return 0
|
|
262
268
|
})
|
|
263
|
-
:
|
|
269
|
+
: _data
|
|
264
270
|
}
|
|
265
271
|
x={d => xPos(d)}
|
|
266
272
|
y={d =>
|
|
267
273
|
seriesAxis === 'Right'
|
|
268
|
-
? yScaleRight(getYAxisData(d,
|
|
269
|
-
: yScale(Number(getYAxisData(d,
|
|
274
|
+
? yScaleRight(getYAxisData(d, _seriesKey))
|
|
275
|
+
: yScale(Number(getYAxisData(d, _seriesKey)))
|
|
270
276
|
}
|
|
271
277
|
stroke={colorScale(config.runtime.seriesLabels[seriesKey])}
|
|
272
|
-
strokeWidth={seriesData
|
|
278
|
+
strokeWidth={seriesData.weight || 2}
|
|
273
279
|
strokeOpacity={1}
|
|
274
280
|
shapeRendering='geometricPrecision'
|
|
275
281
|
strokeDasharray={lineType ? handleLineType(lineType) : 0}
|
|
276
282
|
defined={(item, i) => {
|
|
277
|
-
return item[
|
|
283
|
+
return item[_seriesKey] !== '' && item[_seriesKey] !== null && item[_seriesKey] !== undefined
|
|
278
284
|
}}
|
|
279
285
|
/>
|
|
280
286
|
</>
|
|
@@ -288,11 +294,11 @@ const LineChart = (props: LineChartProps) => {
|
|
|
288
294
|
cx={xPos(item.data)}
|
|
289
295
|
cy={
|
|
290
296
|
seriesAxis === 'Right'
|
|
291
|
-
? yScaleRight(getYAxisData(item.data,
|
|
292
|
-
: yScale(Number(getYAxisData(item.data,
|
|
297
|
+
? yScaleRight(getYAxisData(item.data, _seriesKey))
|
|
298
|
+
: yScale(Number(getYAxisData(item.data, _seriesKey)))
|
|
293
299
|
}
|
|
294
300
|
r={item.size}
|
|
295
|
-
strokeWidth={seriesData
|
|
301
|
+
strokeWidth={seriesData.weight || 2}
|
|
296
302
|
stroke={colorScale ? colorScale(config.runtime.seriesLabels[seriesKey]) : '#000'}
|
|
297
303
|
fill={
|
|
298
304
|
item.isFilled
|
|
@@ -309,13 +315,13 @@ const LineChart = (props: LineChartProps) => {
|
|
|
309
315
|
{config.animate && (
|
|
310
316
|
<LinePath
|
|
311
317
|
className='animation'
|
|
312
|
-
curve={allCurves[seriesData
|
|
313
|
-
data={
|
|
318
|
+
curve={allCurves[seriesData.lineType]}
|
|
319
|
+
data={_data}
|
|
314
320
|
x={d => xPos(d)}
|
|
315
321
|
y={d =>
|
|
316
322
|
seriesAxis === 'Right'
|
|
317
|
-
? yScaleRight(getYAxisData(d,
|
|
318
|
-
: yScale(Number(getYAxisData(d,
|
|
323
|
+
? yScaleRight(getYAxisData(d, _seriesKey))
|
|
324
|
+
: yScale(Number(getYAxisData(d, _seriesKey)))
|
|
319
325
|
}
|
|
320
326
|
stroke='#fff'
|
|
321
327
|
strokeWidth={3}
|
|
@@ -331,9 +337,9 @@ const LineChart = (props: LineChartProps) => {
|
|
|
331
337
|
{showLineSeriesLabels &&
|
|
332
338
|
(config.runtime.lineSeriesKeys || config.runtime.seriesKeys).map(seriesKey => {
|
|
333
339
|
let lastDatum
|
|
334
|
-
for (let i =
|
|
335
|
-
if (
|
|
336
|
-
lastDatum =
|
|
340
|
+
for (let i = _data.length - 1; i >= 0; i--) {
|
|
341
|
+
if (_data[i][seriesKey]) {
|
|
342
|
+
lastDatum = _data[i]
|
|
337
343
|
break
|
|
338
344
|
}
|
|
339
345
|
}
|
|
@@ -341,7 +347,7 @@ const LineChart = (props: LineChartProps) => {
|
|
|
341
347
|
return <></>
|
|
342
348
|
}
|
|
343
349
|
return (
|
|
344
|
-
<
|
|
350
|
+
<Text
|
|
345
351
|
x={xPos(lastDatum) + 5}
|
|
346
352
|
y={yScale(getYAxisData(lastDatum, seriesKey))}
|
|
347
353
|
alignmentBaseline='middle'
|
|
@@ -352,7 +358,7 @@ const LineChart = (props: LineChartProps) => {
|
|
|
352
358
|
}
|
|
353
359
|
>
|
|
354
360
|
{config.runtime.seriesLabels[seriesKey] || seriesKey}
|
|
355
|
-
</
|
|
361
|
+
</Text>
|
|
356
362
|
)
|
|
357
363
|
})}
|
|
358
364
|
</Group>
|
|
@@ -109,7 +109,6 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
109
109
|
const triggerRef = useRef()
|
|
110
110
|
const xAxisLabelRefs = useRef([])
|
|
111
111
|
const xAxisTitleRef = useRef(null)
|
|
112
|
-
const prevTickRef = useRef(null)
|
|
113
112
|
|
|
114
113
|
const dataRef = useIntersectionObserver(triggerRef, {
|
|
115
114
|
freezeOnceVisible: false
|
|
@@ -130,7 +129,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
130
129
|
// height before bottom axis
|
|
131
130
|
const initialHeight = useMemo(
|
|
132
131
|
() => calcInitialHeight(config, currentViewport),
|
|
133
|
-
[config, currentViewport, parentHeight]
|
|
132
|
+
[config, currentViewport, parentHeight, config.heights?.vertical, config.heights?.horizontal]
|
|
134
133
|
)
|
|
135
134
|
const forestHeight = useMemo(() => initialHeight + forestRowsHeight, [initialHeight, forestRowsHeight])
|
|
136
135
|
|
|
@@ -227,16 +226,14 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
227
226
|
return tick
|
|
228
227
|
}
|
|
229
228
|
|
|
230
|
-
const handleBottomTickFormatting = tick => {
|
|
229
|
+
const handleBottomTickFormatting = (tick, i, ticks) => {
|
|
231
230
|
if (isLogarithmicAxis && tick === 0.1) {
|
|
232
231
|
// when logarithmic scale applied change value FIRST of tick
|
|
233
232
|
tick = 0
|
|
234
233
|
}
|
|
235
234
|
|
|
236
235
|
if (isDateScale(runtime.xAxis) && config.visualizationType !== 'Forest Plot') {
|
|
237
|
-
|
|
238
|
-
prevTickRef.current = tick
|
|
239
|
-
return formattedDate
|
|
236
|
+
return formatDate(tick, i, ticks)
|
|
240
237
|
}
|
|
241
238
|
if (orientation === 'horizontal' && config.visualizationType !== 'Forest Plot')
|
|
242
239
|
return formatNumber(tick, 'left', shouldAbbreviate)
|
|
@@ -279,7 +276,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
279
276
|
tickCount = 4 // same default as standalone components
|
|
280
277
|
}
|
|
281
278
|
}
|
|
282
|
-
if (Number(tickCount) > Number(max)) {
|
|
279
|
+
if (Number(tickCount) > Number(max) && !isHorizontal) {
|
|
283
280
|
// cap it and round it so its an integer
|
|
284
281
|
tickCount = Number(min) < 0 ? Math.round(max) * 2 : Math.round(max)
|
|
285
282
|
}
|
|
@@ -410,7 +407,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
410
407
|
const legendIsLeftOrRight =
|
|
411
408
|
legend?.position !== 'top' && legend?.position !== 'bottom' && !isLegendWrapViewport(currentViewport)
|
|
412
409
|
legendRef.current.style.transform = legendIsLeftOrRight ? `translateY(${topLabelOnGridlineHeight}px)` : 'none'
|
|
413
|
-
}, [axisBottomRef.current, config, bottomLabelStart, brush, currentViewport, topYLabelRef.current])
|
|
410
|
+
}, [axisBottomRef.current, config, bottomLabelStart, brush, currentViewport, topYLabelRef.current, initialHeight])
|
|
414
411
|
|
|
415
412
|
const chartHasTooltipGuides = () => {
|
|
416
413
|
const { visualizationType } = config
|
|
@@ -690,7 +687,17 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
690
687
|
showTooltip={showTooltip}
|
|
691
688
|
/>
|
|
692
689
|
)}
|
|
693
|
-
{visualizationType === 'Box Plot' &&
|
|
690
|
+
{visualizationType === 'Box Plot' && (
|
|
691
|
+
<BoxPlot
|
|
692
|
+
seriesScale={seriesScale}
|
|
693
|
+
xMax={xMax}
|
|
694
|
+
yMax={yMax}
|
|
695
|
+
min={min}
|
|
696
|
+
max={max}
|
|
697
|
+
xScale={xScale}
|
|
698
|
+
yScale={yScale}
|
|
699
|
+
/>
|
|
700
|
+
)}
|
|
694
701
|
{((visualizationType === 'Area Chart' && config.visualizationSubType === 'regular') ||
|
|
695
702
|
visualizationType === 'Combo') && (
|
|
696
703
|
<AreaChart
|
|
@@ -1476,7 +1483,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
1476
1483
|
<Text
|
|
1477
1484
|
innerRef={xAxisTitleRef}
|
|
1478
1485
|
className='x-axis-title-label'
|
|
1479
|
-
x={
|
|
1486
|
+
x={xMax / 2}
|
|
1480
1487
|
y={isForestPlot ? 0 /* set via ref */ : axisMaxHeight}
|
|
1481
1488
|
textAnchor='middle'
|
|
1482
1489
|
verticalAnchor='start'
|
|
@@ -14,9 +14,10 @@ import ConfigContext from '@cdc/chart/src/ConfigContext'
|
|
|
14
14
|
import { ChartContext } from '../../types/ChartContext'
|
|
15
15
|
import type { SankeyNode, SankeyProps } from './types'
|
|
16
16
|
import { SankeyChartConfig, AllChartsConfig } from '../../types/ChartConfig'
|
|
17
|
+
import useSankeyAlert from './useSankeyAlert'
|
|
17
18
|
|
|
18
19
|
const Sankey = ({ width, height, runtime }: SankeyProps) => {
|
|
19
|
-
const { config } = useContext<ChartContext>(ConfigContext)
|
|
20
|
+
const { config, handleChartTabbing, legendId } = useContext<ChartContext>(ConfigContext)
|
|
20
21
|
const { sankey: sankeyConfig } = config
|
|
21
22
|
|
|
22
23
|
const isSankeyChartConfig = (config: AllChartsConfig | SankeyChartConfig): config is SankeyChartConfig => {
|
|
@@ -28,8 +29,7 @@ const Sankey = ({ width, height, runtime }: SankeyProps) => {
|
|
|
28
29
|
|
|
29
30
|
//Tooltip
|
|
30
31
|
const [tooltipID, setTooltipID] = useState<string>('')
|
|
31
|
-
|
|
32
|
-
const [showPopup, setShowPopup] = useState(false)
|
|
32
|
+
const { showAlert, alert } = useSankeyAlert()
|
|
33
33
|
|
|
34
34
|
const handleNodeClick = (nodeId: string) => {
|
|
35
35
|
// Store the previous tooltipID
|
|
@@ -46,16 +46,6 @@ const Sankey = ({ width, height, runtime }: SankeyProps) => {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
if (window.innerWidth < 768 && window.innerHeight > window.innerWidth) {
|
|
51
|
-
setShowPopup(true)
|
|
52
|
-
}
|
|
53
|
-
}, [window.innerWidth])
|
|
54
|
-
|
|
55
|
-
const closePopUp = () => {
|
|
56
|
-
setShowPopup(false)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
49
|
// Uses Visx Groups innerRef to get all Group elements that are mapped.
|
|
60
50
|
// Sets the largest group width in state and subtracts that group the svg width to calculate overall width.
|
|
61
51
|
useEffect(() => {
|
|
@@ -313,7 +303,11 @@ const Sankey = ({ width, height, runtime }: SankeyProps) => {
|
|
|
313
303
|
data-tooltip-html={data.tooltips && config.enableTooltips && tooltipID !== '' ? sankeyToolTip : null}
|
|
314
304
|
data-tooltip-id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`}
|
|
315
305
|
>
|
|
316
|
-
<tspan className={classStyle}>
|
|
306
|
+
<tspan className={classStyle}>
|
|
307
|
+
{sankeyConfig.nodeValueStyle.textBefore +
|
|
308
|
+
(typeof node.value === 'number' ? node.value.toLocaleString() : node.value) +
|
|
309
|
+
sankeyConfig.nodeValueStyle.textAfter}
|
|
310
|
+
</tspan>
|
|
317
311
|
</text>
|
|
318
312
|
</>
|
|
319
313
|
)}
|
|
@@ -415,7 +409,16 @@ const Sankey = ({ width, height, runtime }: SankeyProps) => {
|
|
|
415
409
|
>
|
|
416
410
|
{(data?.storyNodeText?.find(storyNode => storyNode.StoryNode === node.id) || {}).segmentTextBefore}
|
|
417
411
|
</Text>
|
|
418
|
-
<Text
|
|
412
|
+
<Text
|
|
413
|
+
verticalAnchor='end'
|
|
414
|
+
className={classStyle}
|
|
415
|
+
x={node.x0! + textPositionHorizontal}
|
|
416
|
+
y={(node.y1! + node.y0! + 25) / 2}
|
|
417
|
+
fill={sankeyConfig.storyNodeFontColor || sankeyConfig.nodeFontColor}
|
|
418
|
+
fontWeight='bold'
|
|
419
|
+
textAnchor='start'
|
|
420
|
+
style={{ pointerEvents: 'none' }}
|
|
421
|
+
>
|
|
419
422
|
{typeof node.value === 'number' ? node.value.toLocaleString() : node.value}
|
|
420
423
|
</Text>
|
|
421
424
|
<Text
|
|
@@ -434,7 +437,15 @@ const Sankey = ({ width, height, runtime }: SankeyProps) => {
|
|
|
434
437
|
</>
|
|
435
438
|
) : (
|
|
436
439
|
<>
|
|
437
|
-
<text
|
|
440
|
+
<text
|
|
441
|
+
x={node.x0! + textPositionHorizontal}
|
|
442
|
+
y={(node.y1! + node.y0!) / 2 + textPositionVertical}
|
|
443
|
+
dominantBaseline='text-before-edge'
|
|
444
|
+
fill={sankeyConfig.nodeFontColor}
|
|
445
|
+
fontWeight='bold'
|
|
446
|
+
textAnchor='start'
|
|
447
|
+
style={{ pointerEvents: 'none' }}
|
|
448
|
+
>
|
|
438
449
|
<tspan id={node.id} className='node-id'>
|
|
439
450
|
{node.id}
|
|
440
451
|
</tspan>
|
|
@@ -451,7 +462,9 @@ const Sankey = ({ width, height, runtime }: SankeyProps) => {
|
|
|
451
462
|
style={{ pointerEvents: 'none' }}
|
|
452
463
|
>
|
|
453
464
|
<tspan onClick={() => handleNodeClick(node.id)} className={classStyle}>
|
|
454
|
-
{sankeyConfig.nodeValueStyle.textBefore +
|
|
465
|
+
{sankeyConfig.nodeValueStyle.textBefore +
|
|
466
|
+
(typeof node.value === 'number' ? node.value.toLocaleString() : node.value) +
|
|
467
|
+
sankeyConfig.nodeValueStyle.textAfter}
|
|
455
468
|
</tspan>
|
|
456
469
|
</text>
|
|
457
470
|
</>
|
|
@@ -460,10 +473,15 @@ const Sankey = ({ width, height, runtime }: SankeyProps) => {
|
|
|
460
473
|
)
|
|
461
474
|
})
|
|
462
475
|
|
|
463
|
-
return (
|
|
476
|
+
return !showAlert ? (
|
|
464
477
|
<>
|
|
465
478
|
<div className='sankey-chart'>
|
|
466
|
-
<svg
|
|
479
|
+
<svg
|
|
480
|
+
className='sankey-chart__diagram'
|
|
481
|
+
width={width}
|
|
482
|
+
height={Number(config.heights.vertical)}
|
|
483
|
+
style={{ overflow: 'visible' }}
|
|
484
|
+
>
|
|
467
485
|
<Group className='links'>{allLinks}</Group>
|
|
468
486
|
<Group className='nodes'>{allNodes}</Group>
|
|
469
487
|
<Group className='finalNodes' style={{ display: 'none' }}>
|
|
@@ -473,21 +491,21 @@ const Sankey = ({ width, height, runtime }: SankeyProps) => {
|
|
|
473
491
|
|
|
474
492
|
{/* ReactTooltip needs to remain even if tooltips are disabled -- it handles when a user clicks off of the node and resets
|
|
475
493
|
the sankey diagram. When tooltips are disabled this will nothing */}
|
|
476
|
-
<ReactTooltip
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
</div>
|
|
488
|
-
)}
|
|
494
|
+
<ReactTooltip
|
|
495
|
+
id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`}
|
|
496
|
+
afterHide={() => setTooltipID('')}
|
|
497
|
+
events={['click']}
|
|
498
|
+
place={'bottom'}
|
|
499
|
+
style={{
|
|
500
|
+
backgroundColor: `rgba(238, 238, 238, 1)`,
|
|
501
|
+
color: 'black',
|
|
502
|
+
boxShadow: `0 3px 10px rgb(0 0 0 / 0.2)`
|
|
503
|
+
}}
|
|
504
|
+
/>
|
|
489
505
|
</div>
|
|
490
506
|
</>
|
|
507
|
+
) : (
|
|
508
|
+
alert
|
|
491
509
|
)
|
|
492
510
|
}
|
|
493
511
|
export default Sankey
|
|
@@ -25,8 +25,12 @@
|
|
|
25
25
|
margin: 10px 0;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
.alert-dismissible {
|
|
29
|
+
padding-right: 4rem !important;
|
|
30
|
+
}
|
|
31
|
+
|
|
28
32
|
svg.sankey-chart__diagram {
|
|
29
|
-
position:relative;
|
|
33
|
+
position: relative;
|
|
30
34
|
font-family: 'Roboto', sans-serif;
|
|
31
35
|
height: auto;
|
|
32
36
|
width: 100%;
|
|
@@ -124,9 +128,6 @@
|
|
|
124
128
|
.popup {
|
|
125
129
|
display: block; /* Show the popup on smaller screens */
|
|
126
130
|
}
|
|
127
|
-
.sankey-chart__diagram {
|
|
128
|
-
opacity: .1;
|
|
129
|
-
}
|
|
130
131
|
}
|
|
131
132
|
}
|
|
132
133
|
|
|
@@ -150,4 +151,4 @@
|
|
|
150
151
|
font-size: 30px;
|
|
151
152
|
padding: 10px;
|
|
152
153
|
text-align: center;
|
|
153
|
-
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useState, useEffect, useContext } from 'react'
|
|
2
|
+
import { ChartContext } from '../../types/ChartContext'
|
|
3
|
+
import ConfigContext from '../../ConfigContext'
|
|
4
|
+
|
|
5
|
+
const useSankeyAlert = () => {
|
|
6
|
+
const { config, handleChartTabbing, legendId } = useContext<ChartContext>(ConfigContext)
|
|
7
|
+
|
|
8
|
+
//Mobile Pop Up
|
|
9
|
+
const [showAlert, setShowAlert] = useState(false)
|
|
10
|
+
const alertMessage = (
|
|
11
|
+
<>
|
|
12
|
+
For best viewing we recommend portrait mode. If you are unable to put your device in portrait mode, please review
|
|
13
|
+
the <a href={`#${handleChartTabbing(config, legendId)}`}>data table</a> below.{' '}
|
|
14
|
+
<a onClick={() => setShowAlert(false)} href={'#!'}>
|
|
15
|
+
Close this alert
|
|
16
|
+
</a>{' '}
|
|
17
|
+
to continue viewing the chart.
|
|
18
|
+
</>
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
const handleCloseModal = () => {
|
|
22
|
+
setShowAlert(false)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const alert = showAlert ? (
|
|
26
|
+
<div className='alert alert-warning alert-dismissible' role='alert'>
|
|
27
|
+
<p style={{ padding: '35px' }}>{alertMessage}</p>
|
|
28
|
+
<button type='button' className='close' data-dismiss='alert' aria-label='Close' onClick={handleCloseModal}>
|
|
29
|
+
<span aria-hidden='true'>×</span>
|
|
30
|
+
</button>
|
|
31
|
+
</div>
|
|
32
|
+
) : null
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const handleResize = () => {
|
|
36
|
+
if (window.innerWidth < 768 && window.innerHeight > window.innerWidth) {
|
|
37
|
+
setShowAlert(true)
|
|
38
|
+
} else {
|
|
39
|
+
setShowAlert(false)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
window.addEventListener('resize', handleResize)
|
|
44
|
+
handleResize() // Call the function initially to set the state based on the initial window size
|
|
45
|
+
|
|
46
|
+
return () => {
|
|
47
|
+
window.removeEventListener('resize', handleResize)
|
|
48
|
+
}
|
|
49
|
+
}, [])
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
setShowAlert,
|
|
53
|
+
showAlert,
|
|
54
|
+
handleCloseModal,
|
|
55
|
+
alertMessage,
|
|
56
|
+
alert
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default useSankeyAlert
|