@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.
Files changed (53) hide show
  1. package/dist/cdcchart.js +34618 -33995
  2. package/examples/feature/boxplot/boxplot-data.json +88 -22
  3. package/examples/feature/boxplot/boxplot.json +540 -16
  4. package/examples/feature/boxplot/testing.csv +7 -7
  5. package/examples/feature/sankey/sankey-example-data.json +0 -1
  6. package/examples/private/test.json +20092 -0
  7. package/index.html +3 -3
  8. package/package.json +2 -2
  9. package/src/CdcChart.tsx +86 -86
  10. package/src/_stories/Chart.CustomColors.stories.tsx +19 -0
  11. package/src/_stories/Chart.DynamicSeries.stories.tsx +27 -0
  12. package/src/_stories/Chart.Legend.Gradient.stories.tsx +42 -1
  13. package/src/_stories/Chart.stories.tsx +7 -8
  14. package/src/_stories/ChartEditor.stories.tsx +27 -0
  15. package/src/_stories/ChartLine.Suppression.stories.tsx +25 -0
  16. package/src/_stories/ChartPrefixSuffix.stories.tsx +8 -0
  17. package/src/_stories/_mock/boxplot_multiseries.json +647 -0
  18. package/src/_stories/_mock/dynamic_series_bar_config.json +723 -0
  19. package/src/_stories/_mock/dynamic_series_config.json +979 -0
  20. package/{examples/feature/scatterplot/scatterplot.json → src/_stories/_mock/scatterplot_mock.json} +62 -92
  21. package/src/_stories/_mock/suppression_mock.json +1549 -0
  22. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +43 -9
  23. package/src/components/BarChart/components/BarChart.Vertical.tsx +60 -42
  24. package/src/components/BarChart/helpers/index.ts +1 -2
  25. package/src/components/BoxPlot/BoxPlot.tsx +189 -0
  26. package/src/components/EditorPanel/EditorPanel.tsx +64 -62
  27. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +3 -3
  28. package/src/components/EditorPanel/components/Panels/Panel.BoxPlot.tsx +51 -6
  29. package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +40 -9
  30. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +3 -3
  31. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +121 -56
  32. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +1 -2
  33. package/src/components/EditorPanel/useEditorPermissions.ts +15 -1
  34. package/src/components/Legend/Legend.Component.tsx +9 -10
  35. package/src/components/Legend/Legend.tsx +16 -16
  36. package/src/components/LineChart/helpers.ts +48 -43
  37. package/src/components/LineChart/index.tsx +88 -82
  38. package/src/components/LinearChart.tsx +17 -10
  39. package/src/components/Sankey/index.tsx +50 -32
  40. package/src/components/Sankey/sankey.scss +6 -5
  41. package/src/components/Sankey/useSankeyAlert.tsx +60 -0
  42. package/src/components/ScatterPlot/ScatterPlot.jsx +20 -4
  43. package/src/data/initial-state.js +3 -9
  44. package/src/hooks/useLegendClasses.ts +10 -23
  45. package/src/hooks/useMinMax.ts +27 -13
  46. package/src/hooks/useReduceData.ts +43 -10
  47. package/src/hooks/useScales.ts +56 -35
  48. package/src/hooks/useTooltip.tsx +54 -49
  49. package/src/scss/main.scss +0 -18
  50. package/src/types/ChartConfig.ts +6 -19
  51. package/src/types/ChartContext.ts +4 -1
  52. package/src/types/ForestPlot.ts +8 -0
  53. 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, isNumber, parseDate, seriesHighlight, tableData, transformedData, updateConfig, brushConfig,clean } = useContext<ChartContext>(ConfigContext)
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
- let lineType = config.runtime.series.filter(item => item.dataKey === seriesKey)[0].type
61
- const seriesData = config.runtime.series.filter(item => item.dataKey === seriesKey)
62
- const seriesAxis = seriesData[0].axis ? seriesData[0].axis : 'left'
63
- let displayArea =
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
- const circleData = filterCircles(config?.preliminaryData, tableD, seriesKey)
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 splittedData = config?.preliminaryData?.filter(pd => pd.style && !pd.style.includes('Circles'))
83
- let xPos = d => {
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(seriesKey) === -1
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(seriesKey) !== -1
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
- {data.map((d, dataIndex) => {
115
+ {_data.map((d, dataIndex) => {
117
116
  return (
118
- d[seriesKey] !== undefined &&
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, seriesKey))
130
- : yScale(getYAxisData(d, seriesKey))
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[seriesKey], 'left')}
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={data}
141
+ data={_data}
146
142
  d={d}
147
143
  config={config}
148
- seriesKey={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={data}
162
+ data={_data}
167
163
  d={d}
168
164
  config={config}
169
- seriesKey={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={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
- {splittedData.length > 0 ? (
202
+ {isSplitLine ? (
207
203
  <>
208
204
  <SplitLinePath
209
- curve={allCurves[seriesData[0].lineType]}
210
- segments={data.map(d => [d])}
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, seriesKey))
216
- : yScale(Number(getYAxisData(d, seriesKey)))
211
+ ? yScaleRight(getYAxisData(d, _seriesKey))
212
+ : yScale(Number(getYAxisData(d, _seriesKey)))
217
213
  }
218
- styles={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
- <LinePath
227
- key={index}
228
- data={segment.data}
229
- x={d => xPos(d)}
230
- y={d =>
231
- seriesAxis === 'Right'
232
- ? yScaleRight(getYAxisData(d, seriesKey))
233
- : yScale(Number(getYAxisData(d, seriesKey)))
234
- }
235
- stroke={colorScale(config.runtime.seriesLabels[seriesKey])}
236
- strokeWidth={seriesData[0].weight || 2}
237
- strokeOpacity={1}
238
- shapeRendering='geometricPrecision'
239
- strokeDasharray={handleLineType(segment.style)}
240
- defined={(item, i) => {
241
- return item[seriesKey] !== '' && item[seriesKey] !== null && item[seriesKey] !== undefined
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[0].lineType]}
257
+ curve={allCurves[seriesData.lineType]}
252
258
  data={
253
259
  config.visualizationType == 'Bump Chart'
254
- ? data
260
+ ? _data
255
261
  : config.xAxis.type === 'date-time' || config.xAxis.type === 'date'
256
- ? data.sort((d1, d2) => {
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
- : data
269
+ : _data
264
270
  }
265
271
  x={d => xPos(d)}
266
272
  y={d =>
267
273
  seriesAxis === 'Right'
268
- ? yScaleRight(getYAxisData(d, seriesKey))
269
- : yScale(Number(getYAxisData(d, seriesKey)))
274
+ ? yScaleRight(getYAxisData(d, _seriesKey))
275
+ : yScale(Number(getYAxisData(d, _seriesKey)))
270
276
  }
271
277
  stroke={colorScale(config.runtime.seriesLabels[seriesKey])}
272
- strokeWidth={seriesData[0].weight || 2}
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[seriesKey] !== '' && item[seriesKey] !== null && item[seriesKey] !== undefined
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, seriesKey))
292
- : yScale(Number(getYAxisData(item.data, seriesKey)))
297
+ ? yScaleRight(getYAxisData(item.data, _seriesKey))
298
+ : yScale(Number(getYAxisData(item.data, _seriesKey)))
293
299
  }
294
300
  r={item.size}
295
- strokeWidth={seriesData[0].weight || 2}
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[0].lineType]}
313
- data={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, seriesKey))
318
- : yScale(Number(getYAxisData(d, seriesKey)))
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 = data.length - 1; i >= 0; i--) {
335
- if (data[i][seriesKey]) {
336
- lastDatum = data[i]
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
- <text
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
- </text>
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
- const formattedDate = formatDate(tick, prevTickRef.current)
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' && <BoxPlot xScale={xScale} yScale={yScale} />}
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={axisCenter}
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
- //Mobile Pop Up
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}>{sankeyConfig.nodeValueStyle.textBefore + (typeof node.value === 'number' ? node.value.toLocaleString() : node.value) + sankeyConfig.nodeValueStyle.textAfter}</tspan>
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 verticalAnchor='end' className={classStyle} x={node.x0! + textPositionHorizontal} y={(node.y1! + node.y0! + 25) / 2} fill={sankeyConfig.storyNodeFontColor || sankeyConfig.nodeFontColor} fontWeight='bold' textAnchor='start' style={{ pointerEvents: 'none' }}>
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 x={node.x0! + textPositionHorizontal} y={(node.y1! + node.y0!) / 2 + textPositionVertical} dominantBaseline='text-before-edge' fill={sankeyConfig.nodeFontColor} fontWeight='bold' textAnchor='start' style={{ pointerEvents: 'none' }}>
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 + (typeof node.value === 'number' ? node.value.toLocaleString() : node.value) + sankeyConfig.nodeValueStyle.textAfter}
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 className='sankey-chart__diagram' width={width} height={Number(config.heights.vertical)} style={{ overflow: 'visible' }}>
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 id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`} afterHide={() => setTooltipID('')} events={['click']} place={'bottom'} style={{ backgroundColor: `rgba(238, 238, 238, 1)`, color: 'black', boxShadow: `0 3px 10px rgb(0 0 0 / 0.2)` }} />
477
- {showPopup && (
478
- <div className='popup'>
479
- <div className='popup-content'>
480
- <button className='visually-hidden' onClick={closePopUp}>
481
- Select for accessible version.
482
- </button>
483
- <p>
484
- <strong>Please change the orientation of your screen or increase the size of your browser to view the diagram better.</strong>
485
- </p>
486
- </div>
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'>&times;</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