@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.
- package/dist/{cdcchart-1a1724a1.es.js → cdcchart-dgT_1dIT.es.js} +136 -151
- package/dist/cdcchart.js +36258 -34658
- package/examples/feature/__data__/planet-example-data.json +1 -1
- package/examples/feature/boxplot/valid-boxplot.csv +38 -17
- package/examples/private/DEV-11825.json +573 -0
- package/examples/private/na.json +913 -0
- package/examples/private/test-data.csv +28 -0
- package/index.html +2 -121
- package/package.json +4 -4
- package/src/CdcChart.tsx +8 -11
- package/src/CdcChartComponent.tsx +256 -87
- package/src/_stories/Chart.Combo.stories.tsx +18 -0
- package/src/_stories/Chart.Forecast.stories.tsx +36 -0
- package/src/_stories/Chart.HTMLInDataTable.stories.tsx +520 -0
- package/src/_stories/Chart.Patterns.stories.tsx +2 -1
- package/src/_stories/Chart.PreserveDecimals.stories.tsx +220 -0
- package/src/_stories/Chart.SmallMultiples.stories.tsx +47 -0
- package/src/_stories/ChartAnnotation.stories.tsx +6 -3
- package/src/_stories/ChartBar.Editor.stories.tsx +3580 -0
- package/src/_stories/ChartEditor.Editor.stories.tsx +658 -0
- package/src/_stories/ChartEditor.stories.tsx +1 -2
- package/src/_stories/_mock/combo.json +451 -0
- package/src/_stories/_mock/editor-test-configs.json +376 -0
- package/src/_stories/_mock/editor-test-datasets.json +477 -0
- package/src/_stories/_mock/editor-tests/bar-chart-editor-test.json +255 -0
- package/src/_stories/_mock/editor-tests/bar-chart-general-test.json +267 -0
- package/src/_stories/_mock/editor-tests/bar-chart-test.json +237 -0
- package/src/_stories/_mock/forecast_combo_with_gaps.json +913 -0
- package/src/_stories/_mock/pie_config.json +257 -62
- package/src/_stories/_mock/small_multiples/small_multiples_bars.json +1944 -0
- package/src/_stories/_mock/small_multiples/small_multiples_big_data_bars.json +1114 -0
- package/src/_stories/_mock/small_multiples/small_multiples_lines.json +2646 -0
- package/src/_stories/_mock/small_multiples/small_multiples_lines_colors.json +1305 -0
- package/src/_stories/_mock/small_multiples/small_multiples_stacked_bars.json +1936 -0
- package/src/components/Annotations/components/findNearestDatum.ts +6 -41
- package/src/components/AreaChart/components/AreaChart.Stacked.jsx +10 -6
- package/src/components/AreaChart/index.tsx +1 -2
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +4 -4
- package/src/components/BarChart/components/BarChart.Vertical.tsx +3 -2
- package/src/components/BoxPlot/helpers/index.ts +3 -3
- package/src/components/Brush/BrushChart.tsx +1 -1
- package/src/components/EditorPanel/EditorPanel.tsx +199 -190
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +96 -111
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +19 -1
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +102 -55
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +54 -49
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +422 -0
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +75 -21
- package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
- package/src/components/EditorPanel/editor-panel.scss +0 -20
- package/src/components/EditorPanel/useEditorPermissions.ts +7 -15
- package/src/components/Forecasting/Forecasting.tsx +139 -21
- package/src/components/Legend/Legend.Component.tsx +16 -9
- package/src/components/Legend/helpers/createFormatLabels.tsx +181 -181
- package/src/components/Legend/helpers/getLegendClasses.ts +0 -1
- package/src/components/LineChart/LineChartProps.ts +0 -3
- package/src/components/LineChart/helpers.ts +1 -1
- package/src/components/LineChart/index.tsx +36 -13
- package/src/components/LinearChart.tsx +75 -80
- package/src/components/Regions/components/Regions.tsx +3 -24
- package/src/components/Sankey/types/index.ts +1 -1
- package/src/components/SmallMultiples/SmallMultipleTile.tsx +198 -0
- package/src/components/SmallMultiples/SmallMultiples.css +32 -0
- package/src/components/SmallMultiples/SmallMultiples.tsx +271 -0
- package/src/components/SmallMultiples/index.ts +2 -0
- package/src/data/initial-state.js +13 -1
- package/src/helpers/buildForecastPaletteOptions.ts +0 -38
- package/src/helpers/getColorScale.ts +10 -0
- package/src/{hooks/useMinMax.ts → helpers/getMinMax.ts} +14 -7
- package/src/helpers/getYAxisAutoPadding.ts +53 -0
- package/src/helpers/smallMultiplesHelpers.ts +529 -0
- package/src/hooks/useProgrammaticTooltip.ts +96 -0
- package/src/hooks/useScales.ts +88 -34
- package/src/hooks/useSmallMultipleSynchronization.ts +59 -0
- package/src/hooks/useTooltip.tsx +60 -15
- package/src/scss/main.scss +1 -80
- package/src/store/chart.actions.ts +2 -0
- package/src/store/chart.reducer.ts +4 -0
- package/src/types/ChartConfig.ts +24 -6
- package/src/types/ChartContext.ts +3 -0
- package/src/_stories/_mock/pie_data.json +0 -218
- package/src/components/AreaChart/components/AreaChart.jsx +0 -109
- package/src/helpers/sort.ts +0 -7
- package/src/hooks/useActiveElement.js +0 -19
- 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
|
|
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(
|
|
161
|
-
const axisLabelFontSize = isMobileFontViewport(
|
|
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
|
|
219
|
-
|
|
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
|
-
|
|
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 =
|
|
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={
|
|
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
|
|
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
|
|
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, {
|
|
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
|
-
|
|
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'>
|
|
@@ -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
|
+
}
|