@cdc/chart 4.25.10 → 4.25.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 (85) hide show
  1. package/dist/{cdcchart-1a1724a1.es.js → cdcchart-dgT_1dIT.es.js} +136 -151
  2. package/dist/cdcchart.js +36258 -34658
  3. package/examples/feature/__data__/planet-example-data.json +1 -1
  4. package/examples/feature/boxplot/valid-boxplot.csv +38 -17
  5. package/examples/private/DEV-11825.json +573 -0
  6. package/examples/private/na.json +913 -0
  7. package/examples/private/test-data.csv +28 -0
  8. package/index.html +2 -121
  9. package/package.json +4 -4
  10. package/src/CdcChart.tsx +8 -11
  11. package/src/CdcChartComponent.tsx +256 -87
  12. package/src/_stories/Chart.Combo.stories.tsx +18 -0
  13. package/src/_stories/Chart.Forecast.stories.tsx +36 -0
  14. package/src/_stories/Chart.HTMLInDataTable.stories.tsx +520 -0
  15. package/src/_stories/Chart.Patterns.stories.tsx +2 -1
  16. package/src/_stories/Chart.PreserveDecimals.stories.tsx +220 -0
  17. package/src/_stories/Chart.SmallMultiples.stories.tsx +47 -0
  18. package/src/_stories/ChartAnnotation.stories.tsx +6 -3
  19. package/src/_stories/ChartBar.Editor.stories.tsx +3580 -0
  20. package/src/_stories/ChartEditor.Editor.stories.tsx +658 -0
  21. package/src/_stories/ChartEditor.stories.tsx +1 -2
  22. package/src/_stories/_mock/combo.json +451 -0
  23. package/src/_stories/_mock/editor-test-configs.json +376 -0
  24. package/src/_stories/_mock/editor-test-datasets.json +477 -0
  25. package/src/_stories/_mock/editor-tests/bar-chart-editor-test.json +255 -0
  26. package/src/_stories/_mock/editor-tests/bar-chart-general-test.json +267 -0
  27. package/src/_stories/_mock/editor-tests/bar-chart-test.json +237 -0
  28. package/src/_stories/_mock/forecast_combo_with_gaps.json +913 -0
  29. package/src/_stories/_mock/pie_config.json +257 -62
  30. package/src/_stories/_mock/small_multiples/small_multiples_bars.json +1944 -0
  31. package/src/_stories/_mock/small_multiples/small_multiples_big_data_bars.json +1114 -0
  32. package/src/_stories/_mock/small_multiples/small_multiples_lines.json +2646 -0
  33. package/src/_stories/_mock/small_multiples/small_multiples_lines_colors.json +1305 -0
  34. package/src/_stories/_mock/small_multiples/small_multiples_stacked_bars.json +1936 -0
  35. package/src/components/Annotations/components/findNearestDatum.ts +6 -41
  36. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +10 -6
  37. package/src/components/AreaChart/index.tsx +1 -2
  38. package/src/components/BarChart/components/BarChart.Horizontal.tsx +4 -4
  39. package/src/components/BarChart/components/BarChart.Vertical.tsx +3 -2
  40. package/src/components/BoxPlot/helpers/index.ts +3 -3
  41. package/src/components/Brush/BrushChart.tsx +1 -1
  42. package/src/components/EditorPanel/EditorPanel.tsx +199 -190
  43. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +96 -111
  44. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +19 -1
  45. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +102 -55
  46. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +54 -49
  47. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +422 -0
  48. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +75 -21
  49. package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
  50. package/src/components/EditorPanel/editor-panel.scss +0 -20
  51. package/src/components/EditorPanel/useEditorPermissions.ts +7 -15
  52. package/src/components/Forecasting/Forecasting.tsx +139 -21
  53. package/src/components/Legend/Legend.Component.tsx +16 -9
  54. package/src/components/Legend/helpers/createFormatLabels.tsx +181 -181
  55. package/src/components/Legend/helpers/getLegendClasses.ts +0 -1
  56. package/src/components/LineChart/LineChartProps.ts +0 -3
  57. package/src/components/LineChart/helpers.ts +1 -1
  58. package/src/components/LineChart/index.tsx +36 -13
  59. package/src/components/LinearChart.tsx +75 -80
  60. package/src/components/Regions/components/Regions.tsx +3 -24
  61. package/src/components/Sankey/types/index.ts +1 -1
  62. package/src/components/SmallMultiples/SmallMultipleTile.tsx +198 -0
  63. package/src/components/SmallMultiples/SmallMultiples.css +32 -0
  64. package/src/components/SmallMultiples/SmallMultiples.tsx +271 -0
  65. package/src/components/SmallMultiples/index.ts +2 -0
  66. package/src/data/initial-state.js +13 -1
  67. package/src/helpers/buildForecastPaletteOptions.ts +0 -38
  68. package/src/helpers/getColorScale.ts +10 -0
  69. package/src/{hooks/useMinMax.ts → helpers/getMinMax.ts} +14 -7
  70. package/src/helpers/getYAxisAutoPadding.ts +53 -0
  71. package/src/helpers/smallMultiplesHelpers.ts +529 -0
  72. package/src/hooks/useProgrammaticTooltip.ts +96 -0
  73. package/src/hooks/useScales.ts +88 -34
  74. package/src/hooks/useSmallMultipleSynchronization.ts +59 -0
  75. package/src/hooks/useTooltip.tsx +60 -15
  76. package/src/scss/main.scss +1 -80
  77. package/src/store/chart.actions.ts +2 -0
  78. package/src/store/chart.reducer.ts +4 -0
  79. package/src/types/ChartConfig.ts +24 -6
  80. package/src/types/ChartContext.ts +3 -0
  81. package/src/_stories/_mock/pie_data.json +0 -218
  82. package/src/components/AreaChart/components/AreaChart.jsx +0 -109
  83. package/src/helpers/sort.ts +0 -7
  84. package/src/hooks/useActiveElement.js +0 -19
  85. package/src/hooks/useChartClasses.js +0 -41
@@ -1,4 +1,4 @@
1
- import React, { forwardRef, useContext, useEffect, useMemo, useRef, useState } from 'react'
1
+ import React, { forwardRef, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
2
2
 
3
3
  // Libraries
4
4
  import { AxisLeft, AxisBottom, AxisRight, AxisTop } from '@visx/axis'
@@ -36,10 +36,11 @@ import { calcInitialHeight, handleAutoPaddingRight } from '../helpers/sizeHelper
36
36
  import { filterAndShiftLinearDateTicks } from '../helpers/filterAndShiftLinearDateTicks'
37
37
 
38
38
  // Hooks
39
- import useMinMax from '../hooks/useMinMax'
40
39
  import useReduceData from '../hooks/useReduceData'
41
40
  import useRightAxis from '../hooks/useRightAxis'
42
41
  import useScales, { getTickValues } from '../hooks/useScales'
42
+ import { useProgrammaticTooltip } from '../hooks/useProgrammaticTooltip'
43
+ import { useSmallMultipleSynchronization } from '../hooks/useSmallMultipleSynchronization'
43
44
 
44
45
  import getTopAxis from '../helpers/getTopAxis'
45
46
  import { useTooltip as useCoveTooltip } from '../hooks/useTooltip'
@@ -49,6 +50,7 @@ import Annotation from './Annotations'
49
50
  import { BlurStrokeText } from '@cdc/core/components/BlurStrokeText'
50
51
  import { countNumOfTicks } from '../helpers/countNumOfTicks'
51
52
  import HoverLine from './HoverLine/HoverLine'
53
+ import { SmallMultiples } from './SmallMultiples'
52
54
 
53
55
  type LinearChartProps = {
54
56
  parentWidth: number
@@ -86,6 +88,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
86
88
  config,
87
89
  convertLineToBarGraph,
88
90
  currentViewport,
91
+ vizViewport,
89
92
  dimensions,
90
93
  formatDate,
91
94
  formatNumber,
@@ -99,8 +102,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
99
102
  parentRef,
100
103
  tableData,
101
104
  transformedData: data,
102
- seriesHighlight,
103
-
105
+ seriesHighlight
104
106
  } = useContext(ConfigContext)
105
107
 
106
108
  // CONFIG
@@ -119,6 +121,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
119
121
  dataFormat,
120
122
  debugSvg
121
123
  } = config
124
+
122
125
  const { labelsAboveGridlines, hideAxis, inlineLabel } = config.yAxis
123
126
 
124
127
  // HOOKS % STATES
@@ -129,7 +132,6 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
129
132
  const [showHoverLine, setShowHoverLine] = useState(false)
130
133
  const [point, setPoint] = useState({ x: 0, y: 0 })
131
134
  const [suffixWidth, setSuffixWidth] = useState(0)
132
- const [yAxisAutoPadding, setYAxisAutoPadding] = useState(0)
133
135
 
134
136
  // REFS
135
137
  const axisBottomRef = useRef(null)
@@ -139,7 +141,6 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
139
141
  const triggerRef = useRef()
140
142
  const xAxisLabelRefs = useRef([])
141
143
  const xAxisTitleRef = useRef(null)
142
- const lastMaxValue = useRef(maxValue)
143
144
  const gridLineRefs = useRef([])
144
145
  const tooltipRef = useRef(null)
145
146
 
@@ -154,11 +155,11 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
154
155
  const isForestPlot = visualizationType === 'Forest Plot'
155
156
  const isDateTime = config.xAxis.type === 'date-time'
156
157
  const inlineLabelHasNoSpace = !inlineLabel?.includes(' ')
157
- const labelsOverflow = inlineLabel && !inlineLabelHasNoSpace
158
+ const needsYAxisAutoPadding = inlineLabel && !inlineLabelHasNoSpace
158
159
  const padding = orientation === 'horizontal' ? Number(config.xAxis.size) : Number(config.yAxis.size)
159
160
  const yLabelOffset = isNaN(parseInt(`${runtime.yAxis.labelOffset}`)) ? 0 : parseInt(`${runtime.yAxis.labelOffset}`)
160
- const tickLabelFontSize = isMobileFontViewport(currentViewport) ? TICK_LABEL_FONT_SIZE_SMALL : TICK_LABEL_FONT_SIZE
161
- const axisLabelFontSize = isMobileFontViewport(currentViewport) ? AXIS_LABEL_FONT_SIZE_SMALL : AXIS_LABEL_FONT_SIZE
161
+ const tickLabelFontSize = isMobileFontViewport(vizViewport) ? TICK_LABEL_FONT_SIZE_SMALL : TICK_LABEL_FONT_SIZE
162
+ const axisLabelFontSize = isMobileFontViewport(vizViewport) ? AXIS_LABEL_FONT_SIZE_SMALL : AXIS_LABEL_FONT_SIZE
162
163
  const GET_TEXT_WIDTH_FONT = `normal ${tickLabelFontSize}px Nunito, sans-serif`
163
164
 
164
165
  // zero if not forest plot
@@ -215,39 +216,37 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
215
216
  : d[config.runtime.originalXAxis.dataKey]
216
217
  const getYAxisData = (d, seriesKey) => d[seriesKey]
217
218
  const xAxisDataMapped = data.map(d => getXAxisData(d))
218
- const section = config.orientation === 'horizontal' || config.visualizationType === 'Forest Plot' ? 'yAxis' : 'xAxis'
219
- const properties = {
219
+ const { yScaleRight, hasRightAxis } = useRightAxis({ config, yMax, data })
220
+
221
+ const {
222
+ xScale,
223
+ yScale,
224
+ seriesScale,
225
+ g1xScale,
226
+ g2xScale,
227
+ xScaleNoPadding,
228
+ xScaleAnnotation,
229
+ min,
230
+ max,
231
+ leftMax,
232
+ rightMax
233
+ } = useScales({
220
234
  data,
221
235
  tableData,
222
- config: {
223
- ...config,
224
- yAxis: {
225
- ...config.yAxis,
226
- scalePadding: labelsOverflow ? yAxisAutoPadding : config.yAxis.scalePadding,
227
- enablePadding: labelsOverflow || config.yAxis.enablePadding
228
- }
229
- },
236
+ config,
230
237
  minValue,
231
238
  maxValue,
232
239
  isAllLine,
233
240
  existPositiveValue,
234
241
  xAxisDataMapped,
235
- xMax,
236
- yMax
237
- }
238
- const { min, max, leftMax, rightMax } = useMinMax(properties)
239
- const { yScaleRight, hasRightAxis } = useRightAxis({ config, yMax, data })
240
- const { xScale, yScale, seriesScale, g1xScale, g2xScale, xScaleNoPadding, xScaleAnnotation } = useScales({
241
- ...properties,
242
- min,
243
- max,
244
- leftMax,
245
- rightMax,
242
+ yMax,
246
243
  dimensions,
247
244
  xMax:
248
245
  parentWidth -
249
246
  Number(config.orientation === 'horizontal' ? config.xAxis.size : config.yAxis.size) -
250
- (hasRightAxis ? config.yAxis.rightAxisSize : 0)
247
+ (hasRightAxis ? config.yAxis.rightAxisSize : 0),
248
+ needsYAxisAutoPadding,
249
+ currentViewport
251
250
  })
252
251
 
253
252
  const [yTickCount, xTickCount] = ['yAxis', 'xAxis'].map(axis =>
@@ -266,6 +265,8 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
266
265
  handleTooltipClick,
267
266
  handleTooltipMouseOff,
268
267
  TooltipListItem,
268
+ getXValueFromCoordinate,
269
+ getCoordinateFromXValue,
269
270
  } = useCoveTooltip({
270
271
  xScale,
271
272
  yScale,
@@ -350,6 +351,8 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
350
351
  return manualStep
351
352
  }
352
353
 
354
+ const smallMultiplesSync = useSmallMultipleSynchronization(xMax, yMax, getXValueFromCoordinate)
355
+
353
356
  const onMouseMove = event => {
354
357
  const svgRect = event.currentTarget.getBoundingClientRect()
355
358
  const x = event.clientX - svgRect.left
@@ -359,8 +362,25 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
359
362
  x,
360
363
  y
361
364
  })
365
+
366
+ smallMultiplesSync.onMouseMove?.(event)
367
+ }
368
+
369
+ const onMouseLeave = () => {
370
+ smallMultiplesSync.onMouseLeave?.()
362
371
  }
363
372
 
373
+ // Use custom hook to provide programmatic tooltip control for small multiples
374
+ const internalSvgRef = useProgrammaticTooltip({
375
+ svgRef,
376
+ getCoordinateFromXValue,
377
+ config,
378
+ setPoint,
379
+ setShowHoverLine,
380
+ handleTooltipMouseOver,
381
+ hideTooltip
382
+ })
383
+
364
384
  // EFFECTS
365
385
  // Adjust padding on the right side of the chart to accommodate for overflow
366
386
  useEffect(() => {
@@ -449,7 +469,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
449
469
  if (!topLabelOnGridlineHeight) return
450
470
 
451
471
  // Adjust the viewBox for the svg
452
- const svg = svgRef.current
472
+ const svg = internalSvgRef.current
453
473
  if (!svg) return
454
474
  const parentWidthFromRef = parentRef.current.getBoundingClientRect().width
455
475
  svg.setAttribute('viewBox', `0 ${-topLabelOnGridlineHeight} ${parentWidthFromRef} ${newHeight}`)
@@ -469,45 +489,6 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
469
489
  initialHeight
470
490
  ])
471
491
 
472
- useEffect(() => {
473
- if (lastMaxValue.current === maxValue) return
474
- lastMaxValue.current = maxValue
475
-
476
- if (!yAxisAutoPadding) return
477
- setYAxisAutoPadding(0)
478
- }, [maxValue])
479
-
480
- useEffect(() => {
481
- if (!yScale?.ticks) return
482
- const ticks = yScale.ticks(handleNumTicks)
483
- if (orientation === 'horizontal' || !labelsOverflow || config.yAxis?.max || ticks.length === 0) {
484
- setYAxisAutoPadding(0)
485
- return
486
- }
487
-
488
- // minimum percentage of the max value that the distance should be from the top grid line
489
- const MINIMUM_DISTANCE_PERCENTAGE = 0.025
490
-
491
- const topGridLine = Math.max(...ticks)
492
- const needsPaddingThreshold = topGridLine - maxValue * MINIMUM_DISTANCE_PERCENTAGE
493
- const maxValueIsGreaterThanThreshold = maxValue > needsPaddingThreshold
494
-
495
- if (!maxValueIsGreaterThanThreshold) return
496
-
497
- const tickGap = ticks.length === 1 ? ticks[0] : ticks[1] - ticks[0]
498
- const nextTick = Math.max(...yScale.ticks(handleNumTicks)) + tickGap
499
- const divideBy = minValue < 0 ? maxValue / 2 : maxValue
500
- const calculatedPadding = (nextTick - maxValue) / divideBy
501
-
502
- // if auto padding is too close to next tick, add one more ticks worth of padding
503
- const newPadding =
504
- calculatedPadding > MINIMUM_DISTANCE_PERCENTAGE ? calculatedPadding : calculatedPadding + tickGap / divideBy
505
-
506
- /* sometimes even though the padding is getting to the next tick exactly,
507
- d3 still doesn't show the tick. we add 0.1 to ensure to tip it over the edge */
508
- setYAxisAutoPadding(newPadding * 100 + 0.1)
509
- }, [maxValue, labelsOverflow, yScale, handleNumTicks])
510
-
511
492
  useEffect(() => {
512
493
  if (!tooltipOpen) return
513
494
  if (!tooltipRef.current) return
@@ -525,6 +506,19 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
525
506
  tooltipRef.current.node.style.maxWidth = `${maxWidth}px`
526
507
  }, [tooltipOpen, tooltipData])
527
508
 
509
+ // Check if small multiples are enabled - if so, render SmallMultiples instead
510
+ if (config.smallMultiples?.mode) {
511
+ return (
512
+ <SmallMultiples
513
+ config={config}
514
+ data={data}
515
+ svgRef={svgRef}
516
+ parentWidth={parentWidth}
517
+ parentHeight={parentHeight}
518
+ />
519
+ )
520
+ }
521
+
528
522
  // Render Functions
529
523
  const generatePairedBarAxis = () => {
530
524
  const axisMaxHeight = bottomLabelStart + BOTTOM_LABEL_PADDING
@@ -659,7 +653,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
659
653
  verticalAnchor='start'
660
654
  fontSize={axisLabelFontSize}
661
655
  >
662
- {runtime.xAxis.label}
656
+ {!config.hideXAxisLabel ? runtime.xAxis.label : null}
663
657
  </Text>
664
658
  </Group>
665
659
  </>
@@ -679,7 +673,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
679
673
  className='tooltip-boundary'
680
674
  >
681
675
  <svg
682
- ref={svgRef}
676
+ ref={internalSvgRef}
683
677
  onMouseMove={onMouseMove}
684
678
  width={parentWidth + config.yAxis.rightAxisSize}
685
679
  height={isNoDataAvailable ? 1 : parentHeight}
@@ -692,6 +686,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
692
686
  onMouseLeave={() => {
693
687
  setShowHoverLine(false)
694
688
  handleChartMouseLeave()
689
+ onMouseLeave()
695
690
  }}
696
691
  onMouseEnter={() => {
697
692
  setShowHoverLine(true)
@@ -710,7 +705,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
710
705
  {props => {
711
706
  const axisCenter =
712
707
  config.orientation === 'horizontal'
713
- ? (props.axisToPoint.y - props.axisFromPoint.y) / 2
708
+ ? Math.abs(props.axisToPoint.y - props.axisFromPoint.y) / 2
714
709
  : (props.axisFromPoint.y - props.axisToPoint.y) / 2
715
710
  return (
716
711
  <Group className='left-axis'>
@@ -744,7 +739,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
744
739
  fill={config.yAxis.labelColor}
745
740
  fontSize={axisLabelFontSize}
746
741
  >
747
- {props.label}
742
+ {!config.hideYAxisLabel ? props.label : null}
748
743
  </Text>
749
744
  </Group>
750
745
  )
@@ -1044,7 +1039,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1044
1039
  {props => {
1045
1040
  const axisCenter =
1046
1041
  config.orientation === 'horizontal'
1047
- ? (props.axisToPoint.y - props.axisFromPoint.y) / 2
1042
+ ? Math.abs(props.axisToPoint.y - props.axisFromPoint.y) / 2
1048
1043
  : (props.axisFromPoint.y - props.axisToPoint.y) / 2
1049
1044
  const horizontalTickOffset =
1050
1045
  yMax / props.ticks.length / 2 - (yMax / props.ticks.length) * (1 - config.barThickness) + 5
@@ -1306,7 +1301,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1306
1301
  fill={config.yAxis.labelColor}
1307
1302
  fontSize={axisLabelFontSize}
1308
1303
  >
1309
- {props.label}
1304
+ {!config.hideYAxisLabel ? props.label : null}
1310
1305
  </Text>
1311
1306
  </Group>
1312
1307
  )
@@ -1420,7 +1415,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1420
1415
  : yMax
1421
1416
  }
1422
1417
  left={config.visualizationType !== 'Forest Plot' ? Number(runtime.yAxis.size) : 0}
1423
- label={runtime[section].label}
1418
+ label={runtime.xAxis.label}
1424
1419
  tickFormat={handleBottomTickFormatting}
1425
1420
  scale={xScale}
1426
1421
  stroke='#333'
@@ -1554,7 +1549,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1554
1549
  verticalAnchor={tickRotation < -50 ? 'middle' : 'start'}
1555
1550
  textAnchor={tickRotation ? 'end' : 'middle'}
1556
1551
  width={
1557
- areTicksTouching && !config.isResponsiveTicks && !Number(config[section].tickRotation)
1552
+ areTicksTouching && !config.isResponsiveTicks && !Number(config.xAxis.tickRotation)
1558
1553
  ? limitedWidth
1559
1554
  : undefined
1560
1555
  }
@@ -1579,7 +1574,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
1579
1574
  fill={config.xAxis.labelColor}
1580
1575
  fontSize={axisLabelFontSize}
1581
1576
  >
1582
- {props.label}
1577
+ {!config.hideXAxisLabel ? props.label : null}
1583
1578
  </Text>
1584
1579
  </Group>
1585
1580
  )
@@ -1,4 +1,4 @@
1
- import React, { MouseEventHandler, useContext } from 'react'
1
+ import React, { useContext } from 'react'
2
2
  import ConfigContext from '../../../ConfigContext'
3
3
  import { ChartContext } from '../../../types/ChartContext'
4
4
  import { Text } from '@visx/text'
@@ -10,27 +10,10 @@ type RegionsProps = {
10
10
  yMax: number
11
11
  barWidth?: number
12
12
  totalBarsInGroup?: number
13
- handleTooltipMouseOff: MouseEventHandler<SVGElement>
14
- handleTooltipMouseOver: MouseEventHandler<SVGElement>
15
- handleTooltipClick: MouseEventHandler<SVGElement>
16
- tooltipData: unknown
17
- showTooltip: Function
18
- hideTooltip: Function
19
13
  }
20
14
 
21
15
  // TODO: should regions be removed on categorical axis?
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
- }) => {
16
+ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGroup = 1, yMax }) => {
34
17
  const { parseDate, config } = useContext<ChartContext>(ConfigContext)
35
18
 
36
19
  const { runtime, regions, visualizationType, orientation, xAxis } = config
@@ -181,11 +164,7 @@ const Regions: React.FC<RegionsProps> = ({
181
164
  fill='red'
182
165
  className='regions regions-group--line zzz'
183
166
  key={region.label}
184
- onMouseMove={handleTooltipMouseOver}
185
- onMouseLeave={handleTooltipMouseOff}
186
- handleTooltipClick={handleTooltipClick}
187
- tooltipData={JSON.stringify(tooltipData)}
188
- showTooltip={showTooltip}
167
+ pointerEvents='none'
189
168
  >
190
169
  <HighlightedArea />
191
170
  <Text x={from + width / 2} y={5} fill={region.color} verticalAnchor='start' textAnchor='middle'>
@@ -1,6 +1,6 @@
1
1
  export type Link = { source: string; target: string; value: number; id: string }
2
2
 
3
- export type Data = {
3
+ type Data = {
4
4
  links: Link[]
5
5
  }
6
6
 
@@ -0,0 +1,198 @@
1
+ import React, { useContext, useRef, useEffect } from 'react'
2
+ import LinearChart from '../LinearChart'
3
+ import ParentSize from '@visx/responsive/lib/components/ParentSize'
4
+ import ConfigContext from '../../ConfigContext'
5
+ import { ColorScale } from '../../types/ChartContext'
6
+ import cloneConfig from '@cdc/core/helpers/cloneConfig'
7
+ import { getTileDisplayTitle } from '../../helpers/smallMultiplesHelpers'
8
+ import getViewport from '@cdc/core/helpers/getViewport'
9
+ import { ChartConfig } from '../../types/ChartConfig'
10
+
11
+ interface SmallMultipleTileProps {
12
+ mode: 'by-series' | 'by-column'
13
+ config: ChartConfig
14
+ data: object[]
15
+ tileKey: string
16
+ seriesKey?: string
17
+ tileValue?: string | number
18
+ tileColumn?: string
19
+ customColorScale?: ColorScale
20
+ svgRef?: React.RefObject<SVGAElement>
21
+ parentWidth?: number
22
+ parentHeight?: number
23
+ globalYAxisMax?: number
24
+ globalYAxisMin?: number
25
+ isFirstInRow?: boolean
26
+ onHeightChange?: (tileKey: string, height: number) => void
27
+ onChartRef?: (ref: any) => void
28
+ onHeaderRef?: (ref: HTMLDivElement | null) => void
29
+ onChartHover?: (xAxisValue: any, yCoordinate: number) => void
30
+ }
31
+
32
+ const SmallMultipleTile: React.FC<SmallMultipleTileProps> = ({
33
+ mode,
34
+ config,
35
+ data,
36
+ tileKey,
37
+ seriesKey,
38
+ tileValue,
39
+ tileColumn,
40
+ customColorScale,
41
+ svgRef,
42
+ parentWidth,
43
+ globalYAxisMax,
44
+ globalYAxisMin,
45
+ isFirstInRow,
46
+ onHeightChange,
47
+ onChartRef,
48
+ onHeaderRef,
49
+ onChartHover
50
+ }) => {
51
+ let tileConfig = cloneConfig(config)
52
+ let tileData = data
53
+
54
+ if (mode === 'by-series') {
55
+ // BY-SERIES: One series per tile, all data
56
+ const singleSeries = tileConfig.series.find(s => s.dataKey === seriesKey)
57
+ tileConfig = {
58
+ ...tileConfig,
59
+ series: [singleSeries], // Single series
60
+ runtime: {
61
+ ...tileConfig.runtime,
62
+ series: tileConfig.runtime.series.filter(s => s.dataKey === seriesKey),
63
+ seriesKeys: [seriesKey],
64
+ seriesLabels: { [seriesKey]: tileConfig.runtime.seriesLabels?.[seriesKey] || seriesKey },
65
+ seriesLabelsAll: [tileConfig.runtime.seriesLabels?.[seriesKey] || seriesKey],
66
+ // Filter area chart specific series keys for proper rendering
67
+ ...(tileConfig.runtime.areaSeriesKeys && {
68
+ areaSeriesKeys: tileConfig.runtime.areaSeriesKeys.filter(s => s.dataKey === seriesKey)
69
+ }),
70
+ // Filter line chart specific series keys for proper rendering
71
+ ...(tileConfig.runtime.lineSeriesKeys && {
72
+ lineSeriesKeys: tileConfig.runtime.lineSeriesKeys.filter(key => key === seriesKey)
73
+ })
74
+ },
75
+ showTitle: false, // Individual tiles don't need the main title
76
+ smallMultiples: undefined // Remove smallMultiples to prevent infinite loop
77
+ }
78
+ tileData = data // All data, but only one series will render
79
+ } else if (mode === 'by-column') {
80
+ // BY-COLUMN: All series, filtered data by tile column value
81
+ tileConfig = {
82
+ ...tileConfig,
83
+ showTitle: false, // Individual tiles don't need the main title
84
+ smallMultiples: undefined // Remove smallMultiples to prevent infinite loop
85
+ }
86
+ tileData = data.filter(row => row[tileColumn] === tileValue) // Filtered data
87
+ }
88
+
89
+ // Apply global Y-axis values for consistent scaling if provided
90
+ if (globalYAxisMax !== undefined) {
91
+ tileConfig = {
92
+ ...tileConfig,
93
+ yAxis: {
94
+ ...tileConfig.yAxis,
95
+ max: globalYAxisMax,
96
+ min: globalYAxisMin
97
+ },
98
+ // Also update runtime properties since LinearChart checks runtime.yAxis.max
99
+ runtime: {
100
+ ...tileConfig.runtime,
101
+ yAxis: {
102
+ ...tileConfig.runtime?.yAxis,
103
+ max: globalYAxisMax,
104
+ min: globalYAxisMin
105
+ }
106
+ }
107
+ }
108
+ }
109
+
110
+ // Small multiples-specific modifications
111
+ tileConfig = {
112
+ ...tileConfig,
113
+ hideXAxisLabel: !isFirstInRow,
114
+ hideYAxisLabel: !isFirstInRow,
115
+ legend: {
116
+ ...tileConfig.legend,
117
+ hide: true
118
+ },
119
+ showAreaUnderLine: config.smallMultiples?.showAreaUnderLine || false
120
+ }
121
+
122
+ const displayTitle = getTileDisplayTitle(mode, seriesKey, tileValue, tileKey, config)
123
+
124
+ // Get the original context values to merge with our filtered config
125
+ const originalContextValues = useContext(ConfigContext)
126
+
127
+ // Create a tile-specific parentRef for this tile's chart
128
+ const tileParentRef = useRef<HTMLDivElement>(null)
129
+
130
+ // Create a ref for the entire tile (including header) for height measurement
131
+ const fullTileRef = useRef<HTMLDivElement>(null)
132
+
133
+ // Create a ref for the LinearChart instance for tooltip coordination
134
+ const linearChartRef = useRef<any>(null)
135
+
136
+ // Create new context values with our filtered config
137
+ const tileContextValues = {
138
+ ...originalContextValues,
139
+ config: tileConfig,
140
+ transformedData: tileData,
141
+ tableData: tileData, // Override with tile-specific filtered data (important for tooltip data lookup)
142
+ parentRef: tileParentRef, // Override with tile-specific parentRef
143
+ updateConfig: () => {}, // Prevent tile hooks from modifying global config
144
+ ...(customColorScale && { colorScale: customColorScale }) // Override colorScale if provided
145
+ }
146
+
147
+ // Use ResizeObserver to capture actual full tile height changes (including header)
148
+ useEffect(() => {
149
+ if (!fullTileRef.current || !onHeightChange) return
150
+
151
+ const resizeObserver = new ResizeObserver(entries => {
152
+ entries.forEach(entry => {
153
+ const { height } = entry.contentRect
154
+ onHeightChange(tileKey, height)
155
+ })
156
+ })
157
+
158
+ resizeObserver.observe(fullTileRef.current)
159
+ return () => resizeObserver.disconnect()
160
+ }, [tileKey, onHeightChange])
161
+
162
+ // Pass chart ref to parent SmallMultiples component for tooltip coordination
163
+ useEffect(() => {
164
+ if (onChartRef && linearChartRef.current) {
165
+ onChartRef(linearChartRef.current)
166
+ }
167
+ }, [onChartRef])
168
+
169
+ return (
170
+ <div ref={fullTileRef} className='small-multiple-tile'>
171
+ <div ref={onHeaderRef} className='tile-header'>
172
+ <div className='tile-title'>{displayTitle}</div>
173
+ </div>
174
+ <div ref={tileParentRef} className='tile-chart'>
175
+ <ParentSize
176
+ key={`${mode}-${seriesKey || tileValue}-${config.smallMultiples?.tilesPerRowDesktop}-${
177
+ config.smallMultiples?.tilesPerRowMobile
178
+ }-${parentWidth}`}
179
+ >
180
+ {parent => (
181
+ <ConfigContext.Provider
182
+ value={{
183
+ ...tileContextValues,
184
+ dimensions: [parent.width, parent.height], // Override with tile-specific dimensions
185
+ vizViewport: getViewport(parent.width), // Override with tile-specific viewport
186
+ handleSmallMultipleHover: onChartHover
187
+ }}
188
+ >
189
+ <LinearChart ref={linearChartRef} parentWidth={parent.width} parentHeight={parent.height} />
190
+ </ConfigContext.Provider>
191
+ )}
192
+ </ParentSize>
193
+ </div>
194
+ </div>
195
+ )
196
+ }
197
+
198
+ export default SmallMultipleTile
@@ -0,0 +1,32 @@
1
+ .small-multiples-container {
2
+ width: 100%;
3
+ display: flex;
4
+ flex-direction: column;
5
+ }
6
+
7
+ .small-multiples-grid {
8
+ display: grid;
9
+ width: 100%;
10
+ flex: 1;
11
+ }
12
+
13
+ .small-multiple-tile {
14
+ display: flex;
15
+ flex-direction: column;
16
+ }
17
+
18
+ .tile-header {
19
+ margin-bottom: 0.5rem;
20
+ }
21
+
22
+ .tile-title {
23
+ margin: 0;
24
+ font-weight: 700;
25
+ text-align: left;
26
+ line-height: 1.3;
27
+ }
28
+
29
+ .tile-chart {
30
+ width: 100%;
31
+ flex-shrink: 0;
32
+ }