@cdc/chart 4.26.5 → 4.26.6
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/CONFIG.md +12 -8
- package/dist/{cdcchart-DQ00cQCm.es.js → cdcchart-BWVIUcAF.es.js} +5 -55
- package/dist/cdcchart.js +34093 -32767
- package/examples/feature/heatmap/average-age-categorical.json +1 -1
- package/examples/feature/heatmap/calendar-heatmap.json +1 -1
- package/examples/feature/heatmap/sparse-aggregation.json +1 -1
- package/examples/private/bad-y-axis.json +543 -0
- package/examples/private/bar-chart-negative-value.json +455 -0
- package/package.json +3 -3
- package/src/CdcChart.tsx +5 -4
- package/src/CdcChartComponent.tsx +32 -9
- package/src/_stories/Chart.SeriesColorAssignments.stories.tsx +183 -0
- package/src/_stories/Chart.YAxisAutoMaxStrategy.stories.tsx +52 -0
- package/src/_stories/Chart.YAxisFilterDomain.stories.tsx +90 -0
- package/src/_stories/ChartAnnotation.smoke.stories.tsx +10 -0
- package/src/_stories/_mock/annotation_event-line_mock.json +1064 -0
- package/src/_stories/_mock/heatmap-average-age-categorical.json +1 -1
- package/src/_stories/_mock/heatmap.json +1 -1
- package/src/components/Annotations/components/AnnotationDraggable.styles.css +22 -1
- package/src/components/Annotations/components/AnnotationDraggable.tsx +219 -119
- package/src/components/Annotations/tests/AnnotationDraggable.test.tsx +198 -0
- package/src/components/Axis/LeftAxis.tsx +3 -0
- package/src/components/Axis/LeftAxisGridlines.tsx +8 -1
- package/src/components/Axis/RightAxis.tsx +3 -0
- package/src/components/Axis/tests/AxisTickValues.test.tsx +86 -0
- package/src/components/EditorPanel/EditorPanel.tsx +367 -136
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +101 -52
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.test.tsx +98 -0
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +10 -0
- package/src/components/EditorPanel/components/Panels/Panel.Visual.test.tsx +484 -0
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +3 -0
- package/src/components/EditorPanel/components/Panels/SeriesColorAssignments.tsx +262 -0
- package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +32 -0
- package/src/components/EditorPanel/components/panels.scss +10 -0
- package/src/components/EditorPanel/editor-panel.scss +47 -0
- package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +6 -2
- package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +20 -2
- package/src/components/EditorPanel/useEditorPermissions.test.tsx +256 -11
- package/src/components/EditorPanel/useEditorPermissions.ts +41 -5
- package/src/components/HeatMap/components/HeatMap.tsx +22 -17
- package/src/components/HeatMap/components/HeatMapGradientLegend.tsx +29 -15
- package/src/components/HeatMap/heatmap.css +15 -1
- package/src/components/HeatMap/tests/HeatMap.test.tsx +39 -7
- package/src/components/Legend/Legend.Component.tsx +2 -2
- package/src/components/Legend/helpers/createFormatLabels.test.ts +88 -0
- package/src/components/Legend/helpers/createFormatLabels.tsx +6 -1
- package/src/components/LineChart/helpers.test.ts +230 -1
- package/src/components/LineChart/helpers.ts +26 -13
- package/src/components/LineChart/index.tsx +21 -4
- package/src/components/LinearChart/tests/LinearChart.test.tsx +30 -0
- package/src/components/LinearChart.tsx +50 -15
- package/src/components/ScatterPlot/ScatterPlot.jsx +3 -3
- package/src/components/SmallMultiples/SmallMultipleTile.tsx +3 -0
- package/src/components/SmallMultiples/SmallMultiples.tsx +38 -9
- package/src/components/SmallMultiples/tests/SmallMultiples.test.tsx +170 -0
- package/src/data/initial-state.js +1 -0
- package/src/helpers/calculateLeftYAxisWidth.ts +9 -3
- package/src/helpers/colorAssignmentHelpers.ts +102 -0
- package/src/helpers/dataHelpers.ts +11 -1
- package/src/helpers/getAxisMaxOverride.ts +16 -0
- package/src/helpers/getCleanTopTickMax.ts +64 -0
- package/src/helpers/getColorScale.ts +18 -9
- package/src/helpers/getHasBoundarySuppression.ts +26 -0
- package/src/helpers/getMinMax.ts +24 -22
- package/src/helpers/getYAxisDomainData.ts +38 -0
- package/src/helpers/getYAxisFinalizationEligibility.ts +40 -0
- package/src/helpers/hasSpacedInlineLabel.ts +10 -0
- package/src/helpers/seriesColumnSettings.ts +5 -4
- package/src/helpers/smallMultiplesHelpers.ts +5 -0
- package/src/helpers/tests/calculateLeftYAxisWidth.test.ts +38 -0
- package/src/helpers/tests/dataHelpers.test.ts +135 -0
- package/src/helpers/tests/getAxisMaxOverride.test.ts +36 -0
- package/src/helpers/tests/getCleanTopTickMax.test.ts +165 -0
- package/src/helpers/tests/getColorScale.test.ts +197 -0
- package/src/helpers/tests/getMinMax.test.ts +211 -0
- package/src/helpers/tests/getYAxisDomainData.test.ts +119 -0
- package/src/helpers/tests/getYAxisFinalizationEligibility.test.ts +185 -0
- package/src/helpers/tests/{getYAxisAutoPaddingMode.test.ts → hasSpacedInlineLabel.test.ts} +17 -32
- package/src/helpers/tests/seriesColumnSettings.test.ts +16 -2
- package/src/helpers/tests/smallMultiplesHelpers.test.ts +99 -0
- package/src/hooks/tests/useRightAxis.test.tsx +127 -0
- package/src/hooks/tests/useScales.test.tsx +307 -29
- package/src/hooks/useRightAxis.ts +30 -13
- package/src/hooks/useScales.ts +45 -28
- package/src/hooks/useTooltip.tsx +3 -1
- package/src/test/CdcChart.dataTable.test.tsx +122 -0
- package/src/types/ChartConfig.ts +6 -1
- package/src/types/ChartContext.ts +3 -0
- package/src/helpers/getYAxisAutoPadding.ts +0 -55
- package/src/helpers/getYAxisAutoPaddingMode.ts +0 -17
- package/src/helpers/tests/getYAxisAutoPadding.test.ts +0 -37
|
@@ -5,6 +5,7 @@ import LinearChart from '../../LinearChart'
|
|
|
5
5
|
import ConfigContext from '../../../ConfigContext'
|
|
6
6
|
import { createMockChartContext } from './mockConfigContext'
|
|
7
7
|
import forestPlotConfig from '../../../../examples/feature/forest-plot/forest-plot.json'
|
|
8
|
+
import * as suppressionHelpers from '../../../helpers/getHasBoundarySuppression'
|
|
8
9
|
|
|
9
10
|
// Mock ResizeObserver
|
|
10
11
|
vi.stubGlobal(
|
|
@@ -88,6 +89,35 @@ const sidePlacementYAxis = {
|
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
describe('LinearChart', () => {
|
|
92
|
+
describe('suppression domain data source', () => {
|
|
93
|
+
it('uses excludedData fallback for suppression detection when Y-axis domain is stable and yAxisDomainData is not provided', () => {
|
|
94
|
+
const suppressionSpy = vi.spyOn(suppressionHelpers, 'getHasBoundarySuppression').mockReturnValue(false)
|
|
95
|
+
const excludedRows = [{ Date: '2020-01-01', Cases: 'SUPP' }]
|
|
96
|
+
const tableRows = [{ Date: '2020-01-01', Cases: 12 }]
|
|
97
|
+
|
|
98
|
+
renderLinearChart(
|
|
99
|
+
{
|
|
100
|
+
visualizationType: 'Line',
|
|
101
|
+
yAxis: {
|
|
102
|
+
...createMockChartContext().config.yAxis,
|
|
103
|
+
filterDomainBehavior: 'stable'
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
transformedData: tableRows as any,
|
|
108
|
+
tableData: tableRows as any,
|
|
109
|
+
excludedData: excludedRows as any,
|
|
110
|
+
yAxisDomainData: undefined
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
expect(suppressionSpy).toHaveBeenCalled()
|
|
115
|
+
expect(suppressionSpy.mock.calls[0][0].rows).toBe(excludedRows)
|
|
116
|
+
|
|
117
|
+
suppressionSpy.mockRestore()
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
91
121
|
describe('rendering', () => {
|
|
92
122
|
it('renders without crashing', () => {
|
|
93
123
|
const { container } = renderLinearChart()
|
|
@@ -37,7 +37,10 @@ import { calcInitialHeight } from '../helpers/sizeHelpers'
|
|
|
37
37
|
import { calculateHorizontalBarCategoryLabelWidth } from '../helpers/calculateHorizontalBarCategoryLabelWidth'
|
|
38
38
|
import { calculateLeftYAxisWidth } from '../helpers/calculateLeftYAxisWidth'
|
|
39
39
|
import { getAxisLabelFontSize } from '../helpers/axisLabelFontSize'
|
|
40
|
-
import {
|
|
40
|
+
import { hasSpacedInlineLabel } from '../helpers/hasSpacedInlineLabel'
|
|
41
|
+
import { getYAxisDomainData, getYAxisFilterDomainBehavior } from '../helpers/getYAxisDomainData'
|
|
42
|
+
import { getHasBoundarySuppression } from '../helpers/getHasBoundarySuppression'
|
|
43
|
+
import { getExcludedData } from '../helpers/getExcludedData'
|
|
41
44
|
|
|
42
45
|
// Hooks
|
|
43
46
|
import useReduceData from '../hooks/useReduceData'
|
|
@@ -117,8 +120,11 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
117
120
|
legendRef,
|
|
118
121
|
parseDate,
|
|
119
122
|
parentRef,
|
|
123
|
+
excludedData,
|
|
120
124
|
tableData,
|
|
121
125
|
transformedData: data,
|
|
126
|
+
yAxisTickValues: sharedYAxisTickValues,
|
|
127
|
+
yAxisDomainData
|
|
122
128
|
} = useContext(ConfigContext)
|
|
123
129
|
|
|
124
130
|
// SVG accessibility: title/desc pattern
|
|
@@ -131,8 +137,28 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
131
137
|
const { inlineLabel } = config.yAxis
|
|
132
138
|
|
|
133
139
|
// HOOKS % STATES
|
|
134
|
-
const
|
|
135
|
-
|
|
140
|
+
const dataForMinMax = getYAxisDomainData({
|
|
141
|
+
config,
|
|
142
|
+
data,
|
|
143
|
+
tableData,
|
|
144
|
+
fullEligibleDomainData: yAxisDomainData
|
|
145
|
+
})
|
|
146
|
+
const isStableYAxisDomain = getYAxisFilterDomainBehavior(config) === 'stable'
|
|
147
|
+
const rawEligibleYAxisDomainData = Array.isArray(config.yAxisDomainData) && config.yAxisDomainData.length > 0
|
|
148
|
+
? getExcludedData(config, config.yAxisDomainData)
|
|
149
|
+
: isStableYAxisDomain && Array.isArray(excludedData)
|
|
150
|
+
? excludedData
|
|
151
|
+
: tableData
|
|
152
|
+
const suppressionDomainData = getYAxisDomainData({
|
|
153
|
+
config,
|
|
154
|
+
data: tableData,
|
|
155
|
+
tableData,
|
|
156
|
+
fullEligibleDomainData: rawEligibleYAxisDomainData
|
|
157
|
+
})
|
|
158
|
+
const hasBoundarySuppression = getHasBoundarySuppression({
|
|
159
|
+
rows: suppressionDomainData,
|
|
160
|
+
config
|
|
161
|
+
})
|
|
136
162
|
const { minValue, maxValue, existPositiveValue, isAllLine } = useReduceData(config, dataForMinMax)
|
|
137
163
|
|
|
138
164
|
const { visSupportsSmallMultiples } = useEditorPermissions()
|
|
@@ -177,7 +203,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
177
203
|
const isLogarithmicAxis = config.yAxis.type === 'logarithmic'
|
|
178
204
|
const isForestPlot = visualizationType === 'Forest Plot'
|
|
179
205
|
const isDateTime = config.xAxis.type === 'date-time'
|
|
180
|
-
const
|
|
206
|
+
const usesSpacedInlineLabel = hasSpacedInlineLabel(config)
|
|
181
207
|
const tickLabelFontSize = isMobileFontViewport(vizViewport) ? TICK_LABEL_FONT_SIZE_SMALL : TICK_LABEL_FONT_SIZE
|
|
182
208
|
const axisLabelFontSize = getAxisLabelFontSize(vizViewport)
|
|
183
209
|
const GET_TEXT_WIDTH_FONT = `normal ${tickLabelFontSize}px Nunito, sans-serif`
|
|
@@ -255,14 +281,14 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
255
281
|
return () => observer.disconnect()
|
|
256
282
|
}, [axisBottomRef.current])
|
|
257
283
|
|
|
258
|
-
const { yScaleRight, hasRightAxis } = useRightAxis({ config, yMax, data })
|
|
284
|
+
const { yScaleRight, hasRightAxis, rightTickValues } = useRightAxis({ config, yMax, data: dataForMinMax })
|
|
259
285
|
|
|
260
286
|
// State for computed left-axis width - shared across all linear-chart types.
|
|
261
287
|
const [currentYAxisWidth, setCurrentYAxisWidth] = useState<number>(DEFAULT_LEFT_Y_AXIS_WIDTH)
|
|
262
288
|
const yAxisWidth = currentYAxisWidth
|
|
263
289
|
|
|
264
290
|
// Chart width calculation using the current y-axis width
|
|
265
|
-
const xMax = parentWidth - yAxisWidth - (hasRightAxis ? config.yAxis.rightAxisSize : 0)
|
|
291
|
+
const xMax = Math.max(0, parentWidth - yAxisWidth - (hasRightAxis ? config.yAxis.rightAxisSize : 0))
|
|
266
292
|
|
|
267
293
|
// Stabilize brush container dimensions when brushDynamicYAxis is enabled.
|
|
268
294
|
// Without this, y-axis rescaling on each brush change creates a feedback loop:
|
|
@@ -289,10 +315,13 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
289
315
|
xScaleAnnotation,
|
|
290
316
|
yScaleAnnotation,
|
|
291
317
|
min,
|
|
292
|
-
max
|
|
318
|
+
max,
|
|
319
|
+
yTickValues
|
|
293
320
|
} = useScales({
|
|
294
321
|
data,
|
|
295
|
-
tableData,
|
|
322
|
+
tableData: dataForMinMax,
|
|
323
|
+
yAxisDomainData: dataForMinMax,
|
|
324
|
+
hasBoundarySuppression,
|
|
296
325
|
config,
|
|
297
326
|
minValue,
|
|
298
327
|
maxValue,
|
|
@@ -301,9 +330,10 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
301
330
|
xAxisDataMapped,
|
|
302
331
|
yMax,
|
|
303
332
|
xMax,
|
|
304
|
-
|
|
333
|
+
hasSpacedInlineLabel: usesSpacedInlineLabel,
|
|
305
334
|
currentViewport: vizViewport
|
|
306
335
|
})
|
|
336
|
+
const effectiveYTickValues = sharedYAxisTickValues ?? yTickValues
|
|
307
337
|
|
|
308
338
|
// Consolidated tick formatters
|
|
309
339
|
const { handleLeftTickFormatting, handleBottomTickFormatting } = useTickFormatters({
|
|
@@ -358,6 +388,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
358
388
|
data,
|
|
359
389
|
yScale,
|
|
360
390
|
numTicks: handleNumTicks,
|
|
391
|
+
tickValues: effectiveYTickValues,
|
|
361
392
|
parentWidth,
|
|
362
393
|
tickLabelFont: GET_TEXT_WIDTH_FONT,
|
|
363
394
|
axisLabelFontSize,
|
|
@@ -369,6 +400,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
369
400
|
config,
|
|
370
401
|
data,
|
|
371
402
|
yScale,
|
|
403
|
+
effectiveYTickValues,
|
|
372
404
|
handleNumTicks,
|
|
373
405
|
parentWidth,
|
|
374
406
|
isHorizontal,
|
|
@@ -596,6 +628,8 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
596
628
|
<SmallMultiples
|
|
597
629
|
config={config}
|
|
598
630
|
data={data}
|
|
631
|
+
yAxisDomainData={dataForMinMax}
|
|
632
|
+
hasBoundarySuppression={hasBoundarySuppression}
|
|
599
633
|
svgRef={svgRef}
|
|
600
634
|
parentWidth={parentWidth}
|
|
601
635
|
parentHeight={parentHeight}
|
|
@@ -617,9 +651,8 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
617
651
|
onMouseMove={onMouseMove}
|
|
618
652
|
width={parentWidth}
|
|
619
653
|
height={isNoDataAvailable ? 1 : calculatedSvgHeight ?? parentHeight}
|
|
620
|
-
className={`linear ${config.animate ? 'animated' : ''} ${animatedChart && config.animate ? 'animate' : ''} ${
|
|
621
|
-
|
|
622
|
-
} ${isDraggingAnnotation && 'dragging-annotation'}`}
|
|
654
|
+
className={`linear ${config.animate ? 'animated' : ''} ${animatedChart && config.animate ? 'animate' : ''} ${debugSvg && 'debug'
|
|
655
|
+
} ${isDraggingAnnotation && 'dragging-annotation'}`}
|
|
623
656
|
role='img'
|
|
624
657
|
aria-label={a11y}
|
|
625
658
|
style={{ overflow: 'visible' }}
|
|
@@ -642,6 +675,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
642
675
|
xMax={xMax}
|
|
643
676
|
yAxisWidth={yAxisWidth}
|
|
644
677
|
numTicks={handleNumTicks}
|
|
678
|
+
tickValues={effectiveYTickValues}
|
|
645
679
|
axisLabelFontSize={axisLabelFontSize}
|
|
646
680
|
/>
|
|
647
681
|
)}
|
|
@@ -838,6 +872,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
838
872
|
xMax={xMax}
|
|
839
873
|
yAxisWidth={yAxisWidth}
|
|
840
874
|
numTicks={handleNumTicks}
|
|
875
|
+
tickValues={effectiveYTickValues}
|
|
841
876
|
tickLabelFontSize={tickLabelFontSize}
|
|
842
877
|
axisLabelFontSize={axisLabelFontSize}
|
|
843
878
|
handleLeftTickFormatting={handleLeftTickFormatting}
|
|
@@ -863,6 +898,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
863
898
|
yMax={yMax}
|
|
864
899
|
xMax={xMax}
|
|
865
900
|
yAxisWidth={yAxisWidth}
|
|
901
|
+
tickValues={rightTickValues}
|
|
866
902
|
tickLabelFontSize={tickLabelFontSize}
|
|
867
903
|
axisLabelFontSize={axisLabelFontSize}
|
|
868
904
|
/>
|
|
@@ -914,9 +950,8 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
914
950
|
tooltipData.dataXPosition &&
|
|
915
951
|
!config.tooltips.singleSeries && (
|
|
916
952
|
<>
|
|
917
|
-
<style>{`.tooltip {background-color: rgba(255,255,255, ${
|
|
918
|
-
|
|
919
|
-
}) !important;`}</style>
|
|
953
|
+
<style>{`.tooltip {background-color: rgba(255,255,255, ${config.tooltips.opacity / 100
|
|
954
|
+
}) !important;`}</style>
|
|
920
955
|
<style>{`.tooltip {max-width:300px} !important; word-wrap: break-word; `}</style>
|
|
921
956
|
<TooltipWithBounds
|
|
922
957
|
ref={tooltipRef}
|
|
@@ -6,7 +6,7 @@ import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
|
|
|
6
6
|
import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
|
|
7
7
|
import { buildSeriesTooltipListHtml } from '../../helpers/tooltipHelpers'
|
|
8
8
|
|
|
9
|
-
const ScatterPlot = ({ xScale, yScale, yAxisWidth }) => {
|
|
9
|
+
const ScatterPlot = ({ xScale, yScale, yAxisWidth, getXAxisData }) => {
|
|
10
10
|
const {
|
|
11
11
|
transformedData: data,
|
|
12
12
|
config,
|
|
@@ -61,7 +61,7 @@ const ScatterPlot = ({ xScale, yScale, yAxisWidth }) => {
|
|
|
61
61
|
return config.runtime.seriesKeys.map((s, index) => {
|
|
62
62
|
const transparentArea = config.legend.behavior === 'highlight' && seriesHighlight.length > 0 && seriesHighlight.indexOf(s) === -1
|
|
63
63
|
const displayArea = config.legend.behavior === 'highlight' || seriesHighlight.length === 0 || seriesHighlight.indexOf(s) !== -1
|
|
64
|
-
const seriesColor =
|
|
64
|
+
const seriesColor = colorScale(config.runtime.seriesLabels?.[s] || s)
|
|
65
65
|
|
|
66
66
|
let pointStyles = {
|
|
67
67
|
filter: 'unset',
|
|
@@ -78,7 +78,7 @@ const ScatterPlot = ({ xScale, yScale, yAxisWidth }) => {
|
|
|
78
78
|
<circle
|
|
79
79
|
key={`${dataIndex}-${index}`}
|
|
80
80
|
r={circleRadii}
|
|
81
|
-
cx={xScale(item
|
|
81
|
+
cx={xScale(getXAxisData(item))}
|
|
82
82
|
cy={yScale(item[s])}
|
|
83
83
|
fill={displayArea ? seriesColor : 'transparent'}
|
|
84
84
|
fillOpacity={transparentArea ? 0.25 : 1}
|
|
@@ -23,6 +23,7 @@ interface SmallMultipleTileProps {
|
|
|
23
23
|
parentHeight?: number
|
|
24
24
|
globalYAxisMax?: number
|
|
25
25
|
globalYAxisMin?: number
|
|
26
|
+
globalYAxisTickValues?: number[]
|
|
26
27
|
isFirstInRow?: boolean
|
|
27
28
|
onHeightChange?: (tileKey: string, height: number) => void
|
|
28
29
|
onChartRef?: (ref: any) => void
|
|
@@ -43,6 +44,7 @@ const SmallMultipleTile: React.FC<SmallMultipleTileProps> = ({
|
|
|
43
44
|
parentWidth,
|
|
44
45
|
globalYAxisMax,
|
|
45
46
|
globalYAxisMin,
|
|
47
|
+
globalYAxisTickValues,
|
|
46
48
|
isFirstInRow,
|
|
47
49
|
onHeightChange,
|
|
48
50
|
onChartRef,
|
|
@@ -152,6 +154,7 @@ const SmallMultipleTile: React.FC<SmallMultipleTileProps> = ({
|
|
|
152
154
|
tableData: tileData, // Override with tile-specific filtered data (important for tooltip data lookup)
|
|
153
155
|
parentRef: tileParentRef, // Override with tile-specific parentRef
|
|
154
156
|
updateConfig: () => {}, // Prevent tile hooks from modifying global config
|
|
157
|
+
yAxisTickValues: globalYAxisTickValues,
|
|
155
158
|
...(customColorScale && { colorScale: customColorScale }) // Override colorScale if provided
|
|
156
159
|
}
|
|
157
160
|
|
|
@@ -3,7 +3,7 @@ import SmallMultipleTile from './SmallMultipleTile'
|
|
|
3
3
|
import ConfigContext from '../../ConfigContext'
|
|
4
4
|
import useReduceData from '../../hooks/useReduceData'
|
|
5
5
|
import useScales from '../../hooks/useScales'
|
|
6
|
-
import {
|
|
6
|
+
import { hasSpacedInlineLabel } from '../../helpers/hasSpacedInlineLabel'
|
|
7
7
|
import { createCombinedDataForYAxis, applyTileOrder, createTileColorScale } from '../../helpers/smallMultiplesHelpers'
|
|
8
8
|
import { isMobileSmallMultiplesViewport } from '@cdc/core/helpers/viewports'
|
|
9
9
|
import './SmallMultiples.css'
|
|
@@ -12,6 +12,8 @@ import { ChartConfig } from '../../types/ChartConfig'
|
|
|
12
12
|
interface SmallMultiplesProps {
|
|
13
13
|
config: ChartConfig
|
|
14
14
|
data: object[]
|
|
15
|
+
yAxisDomainData?: object[]
|
|
16
|
+
hasBoundarySuppression?: boolean
|
|
15
17
|
svgRef?: React.RefObject<SVGAElement>
|
|
16
18
|
parentWidth?: number
|
|
17
19
|
parentHeight?: number
|
|
@@ -34,12 +36,21 @@ type TileHeaderRows = Array<Array<HTMLDivElement>>
|
|
|
34
36
|
|
|
35
37
|
type TileHeaderEntries = Array<[string, HTMLDivElement]>
|
|
36
38
|
|
|
37
|
-
const SmallMultiples: React.FC<SmallMultiplesProps> = ({
|
|
39
|
+
const SmallMultiples: React.FC<SmallMultiplesProps> = ({
|
|
40
|
+
config,
|
|
41
|
+
data,
|
|
42
|
+
yAxisDomainData,
|
|
43
|
+
hasBoundarySuppression = false,
|
|
44
|
+
svgRef,
|
|
45
|
+
parentWidth,
|
|
46
|
+
parentHeight
|
|
47
|
+
}) => {
|
|
38
48
|
const { currentViewport, colorScale, parentRef } = useContext(ConfigContext)
|
|
39
49
|
const { mode, tileColumn, tilesPerRowDesktop, tilesPerRowMobile } = config.smallMultiples || {}
|
|
40
50
|
|
|
41
51
|
const isMobile = isMobileSmallMultiplesViewport(currentViewport)
|
|
42
52
|
const tilesPerRow = isMobile ? tilesPerRowMobile || 1 : tilesPerRowDesktop || 3
|
|
53
|
+
const dataForSharedYAxis = Array.isArray(yAxisDomainData) && yAxisDomainData.length > 0 ? yAxisDomainData : data
|
|
43
54
|
|
|
44
55
|
// Figure out what objects to iterate over based on mode - memoized to prevent recalculation
|
|
45
56
|
const tileItems = useMemo<Array<TileItem>>(() => {
|
|
@@ -80,6 +91,22 @@ const SmallMultiples: React.FC<SmallMultiplesProps> = ({ config, data, svgRef, p
|
|
|
80
91
|
config.smallMultiples?.tileTitles
|
|
81
92
|
])
|
|
82
93
|
|
|
94
|
+
const sharedYAxisTileItems = useMemo<Array<TileItem>>(
|
|
95
|
+
() =>
|
|
96
|
+
mode === 'by-column'
|
|
97
|
+
? Array.from(new Set(dataForSharedYAxis.map(row => row[tileColumn])))
|
|
98
|
+
.filter(val => val != null)
|
|
99
|
+
.sort()
|
|
100
|
+
.map(value => ({
|
|
101
|
+
key: value,
|
|
102
|
+
mode: 'by-column' as const,
|
|
103
|
+
tileValue: value,
|
|
104
|
+
tileColumn: tileColumn
|
|
105
|
+
}))
|
|
106
|
+
: tileItems,
|
|
107
|
+
[mode, dataForSharedYAxis, tileColumn, tileItems]
|
|
108
|
+
)
|
|
109
|
+
|
|
83
110
|
// Calculate the grid styling based on tiles per row
|
|
84
111
|
const gridGap = isMobile ? '1rem' : '2rem'
|
|
85
112
|
const gridStyle = {
|
|
@@ -97,8 +124,8 @@ const SmallMultiples: React.FC<SmallMultiplesProps> = ({ config, data, svgRef, p
|
|
|
97
124
|
|
|
98
125
|
// Create combined data and config for consistent Y-axis calculation
|
|
99
126
|
const combinedDataForYAxis = useMemo(
|
|
100
|
-
() => createCombinedDataForYAxis(config,
|
|
101
|
-
[config,
|
|
127
|
+
() => createCombinedDataForYAxis(config, dataForSharedYAxis, sharedYAxisTileItems),
|
|
128
|
+
[config, dataForSharedYAxis, sharedYAxisTileItems]
|
|
102
129
|
)
|
|
103
130
|
|
|
104
131
|
const { minValue, maxValue, existPositiveValue, isAllLine } = useReduceData(
|
|
@@ -106,12 +133,13 @@ const SmallMultiples: React.FC<SmallMultiplesProps> = ({ config, data, svgRef, p
|
|
|
106
133
|
combinedDataForYAxis.data
|
|
107
134
|
)
|
|
108
135
|
|
|
109
|
-
const
|
|
136
|
+
const usesSpacedInlineLabel = hasSpacedInlineLabel(config)
|
|
110
137
|
|
|
111
|
-
const { min, max } = useScales({
|
|
138
|
+
const { min, max, yTickValues } = useScales({
|
|
112
139
|
config: combinedDataForYAxis.config,
|
|
113
140
|
data: combinedDataForYAxis.data,
|
|
114
141
|
tableData: combinedDataForYAxis.data,
|
|
142
|
+
hasBoundarySuppression,
|
|
115
143
|
minValue,
|
|
116
144
|
maxValue,
|
|
117
145
|
existPositiveValue,
|
|
@@ -119,7 +147,7 @@ const SmallMultiples: React.FC<SmallMultiplesProps> = ({ config, data, svgRef, p
|
|
|
119
147
|
xAxisDataMapped: [],
|
|
120
148
|
xMax: parentWidth,
|
|
121
149
|
yMax: parentHeight,
|
|
122
|
-
|
|
150
|
+
hasSpacedInlineLabel: usesSpacedInlineLabel,
|
|
123
151
|
currentViewport
|
|
124
152
|
})
|
|
125
153
|
|
|
@@ -129,8 +157,8 @@ const SmallMultiples: React.FC<SmallMultiplesProps> = ({ config, data, svgRef, p
|
|
|
129
157
|
if (typeof min !== 'number' || typeof max !== 'number') return null
|
|
130
158
|
if (combinedDataForYAxis.data.length === 0) return null
|
|
131
159
|
|
|
132
|
-
return { min, max }
|
|
133
|
-
}, [config.smallMultiples?.independentYAxis, min, max, combinedDataForYAxis.data.length])
|
|
160
|
+
return { min, max, tickValues: yTickValues }
|
|
161
|
+
}, [config.smallMultiples?.independentYAxis, min, max, yTickValues, combinedDataForYAxis.data.length])
|
|
134
162
|
|
|
135
163
|
const numberOfRows = useMemo(() => Math.ceil(tileItems.length / tilesPerRow), [tileItems.length, tilesPerRow])
|
|
136
164
|
|
|
@@ -257,6 +285,7 @@ const SmallMultiples: React.FC<SmallMultiplesProps> = ({ config, data, svgRef, p
|
|
|
257
285
|
parentHeight={parentHeight}
|
|
258
286
|
globalYAxisMax={globalYAxisValues?.max}
|
|
259
287
|
globalYAxisMin={globalYAxisValues?.min}
|
|
288
|
+
globalYAxisTickValues={globalYAxisValues?.tickValues}
|
|
260
289
|
isFirstInRow={index % tilesPerRow === 0}
|
|
261
290
|
onHeightChange={handleTileHeightChange}
|
|
262
291
|
onChartRef={ref => {
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { render } from '@testing-library/react'
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
import ConfigContext from '../../../ConfigContext'
|
|
5
|
+
import SmallMultiples from '../SmallMultiples'
|
|
6
|
+
import { createMockChartContext, createMockConfig } from '../../LinearChart/tests/mockConfigContext'
|
|
7
|
+
|
|
8
|
+
const tileProps = vi.hoisted(() => [] as any[])
|
|
9
|
+
const mockUseReduceData = vi.hoisted(() => vi.fn())
|
|
10
|
+
const mockUseScales = vi.hoisted(() => vi.fn())
|
|
11
|
+
|
|
12
|
+
vi.mock('../SmallMultipleTile', () => ({
|
|
13
|
+
default: props => {
|
|
14
|
+
tileProps.push(props)
|
|
15
|
+
return <div data-testid={`tile-${props.tileKey}`} />
|
|
16
|
+
}
|
|
17
|
+
}))
|
|
18
|
+
|
|
19
|
+
vi.mock('../../../hooks/useReduceData', () => ({
|
|
20
|
+
default: (...args: any[]) => mockUseReduceData(...args)
|
|
21
|
+
}))
|
|
22
|
+
|
|
23
|
+
vi.mock('../../../hooks/useScales', () => ({
|
|
24
|
+
default: (...args: any[]) => mockUseScales(...args)
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
describe('SmallMultiples', () => {
|
|
28
|
+
const createSmallMultiplesConfig = () =>
|
|
29
|
+
createMockConfig({
|
|
30
|
+
smallMultiples: {
|
|
31
|
+
mode: 'by-column',
|
|
32
|
+
tileColumn: 'Sex',
|
|
33
|
+
independentYAxis: false,
|
|
34
|
+
tilesPerRowDesktop: 2,
|
|
35
|
+
tilesPerRowMobile: 1
|
|
36
|
+
},
|
|
37
|
+
series: [{ dataKey: 'Cases', type: 'Bar' }] as any,
|
|
38
|
+
runtime: {
|
|
39
|
+
...createMockConfig().runtime,
|
|
40
|
+
series: [{ dataKey: 'Cases', type: 'Bar' }],
|
|
41
|
+
seriesKeys: ['Cases'],
|
|
42
|
+
seriesLabelsAll: ['Cases']
|
|
43
|
+
} as any
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
tileProps.length = 0
|
|
48
|
+
mockUseReduceData.mockReset()
|
|
49
|
+
mockUseScales.mockReset()
|
|
50
|
+
mockUseReduceData.mockReturnValue({
|
|
51
|
+
minValue: 0,
|
|
52
|
+
maxValue: 100,
|
|
53
|
+
existPositiveValue: true,
|
|
54
|
+
isAllLine: false
|
|
55
|
+
})
|
|
56
|
+
mockUseScales.mockReturnValue({
|
|
57
|
+
min: 0,
|
|
58
|
+
max: 100,
|
|
59
|
+
yTickValues: [0, 50, 100]
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('uses stable domain data for the shared Y-axis while rendering filtered tile data', () => {
|
|
64
|
+
const filteredRows = [
|
|
65
|
+
{ Sex: 'Male', Year: '2019', Cases: 10 },
|
|
66
|
+
{ Sex: 'Female', Year: '2019', Cases: 20 }
|
|
67
|
+
]
|
|
68
|
+
const stableDomainRows = [
|
|
69
|
+
...filteredRows,
|
|
70
|
+
{ Sex: 'Male', Year: '2020', Cases: 100 },
|
|
71
|
+
{ Sex: 'Female', Year: '2020', Cases: 80 }
|
|
72
|
+
]
|
|
73
|
+
const config = createSmallMultiplesConfig()
|
|
74
|
+
const context = createMockChartContext(config, {
|
|
75
|
+
colorScale: vi.fn(),
|
|
76
|
+
parentRef: { current: document.createElement('div') }
|
|
77
|
+
} as any)
|
|
78
|
+
|
|
79
|
+
render(
|
|
80
|
+
<ConfigContext.Provider value={context}>
|
|
81
|
+
<SmallMultiples
|
|
82
|
+
config={config}
|
|
83
|
+
data={filteredRows}
|
|
84
|
+
yAxisDomainData={stableDomainRows}
|
|
85
|
+
parentWidth={600}
|
|
86
|
+
parentHeight={300}
|
|
87
|
+
/>
|
|
88
|
+
</ConfigContext.Provider>
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
const expectedSharedAxisRows = [
|
|
92
|
+
{ Sex: 'Female', Year: '2019', Cases: 20 },
|
|
93
|
+
{ Sex: 'Female', Year: '2020', Cases: 80 },
|
|
94
|
+
{ Sex: 'Male', Year: '2019', Cases: 10 },
|
|
95
|
+
{ Sex: 'Male', Year: '2020', Cases: 100 }
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
expect(mockUseReduceData.mock.calls[0][1]).toEqual(expectedSharedAxisRows)
|
|
99
|
+
expect(mockUseScales.mock.calls[0][0].data).toEqual(expectedSharedAxisRows)
|
|
100
|
+
expect(mockUseScales.mock.calls[0][0].hasBoundarySuppression).toBe(false)
|
|
101
|
+
expect(tileProps).toHaveLength(2)
|
|
102
|
+
expect(tileProps[0].data).toBe(filteredRows)
|
|
103
|
+
expect(tileProps[0].globalYAxisMax).toBe(100)
|
|
104
|
+
expect(tileProps[0].globalYAxisTickValues).toEqual([0, 50, 100])
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('includes filtered-out tile values when calculating a stable shared Y-axis', () => {
|
|
108
|
+
const filteredRows = [{ Sex: 'Male', Year: '2019', Cases: 10 }]
|
|
109
|
+
const stableDomainRows = [...filteredRows, { Sex: 'Female', Year: '2019', Cases: 200 }]
|
|
110
|
+
const config = createSmallMultiplesConfig()
|
|
111
|
+
const context = createMockChartContext(config, {
|
|
112
|
+
colorScale: vi.fn(),
|
|
113
|
+
parentRef: { current: document.createElement('div') }
|
|
114
|
+
} as any)
|
|
115
|
+
|
|
116
|
+
render(
|
|
117
|
+
<ConfigContext.Provider value={context}>
|
|
118
|
+
<SmallMultiples
|
|
119
|
+
config={config}
|
|
120
|
+
data={filteredRows}
|
|
121
|
+
yAxisDomainData={stableDomainRows}
|
|
122
|
+
parentWidth={600}
|
|
123
|
+
parentHeight={300}
|
|
124
|
+
/>
|
|
125
|
+
</ConfigContext.Provider>
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
expect(mockUseReduceData.mock.calls[0][1]).toEqual([
|
|
129
|
+
{ Sex: 'Female', Year: '2019', Cases: 200 },
|
|
130
|
+
{ Sex: 'Male', Year: '2019', Cases: 10 }
|
|
131
|
+
])
|
|
132
|
+
expect(mockUseScales.mock.calls[0][0].data).toEqual([
|
|
133
|
+
{ Sex: 'Female', Year: '2019', Cases: 200 },
|
|
134
|
+
{ Sex: 'Male', Year: '2019', Cases: 10 }
|
|
135
|
+
])
|
|
136
|
+
expect(mockUseScales.mock.calls[0][0].hasBoundarySuppression).toBe(false)
|
|
137
|
+
expect(tileProps).toHaveLength(1)
|
|
138
|
+
expect(tileProps[0].tileValue).toBe('Male')
|
|
139
|
+
expect(tileProps[0].data).toBe(filteredRows)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('passes hasBoundarySuppression from shared stable-domain boundary rows', () => {
|
|
143
|
+
const filteredRows = [{ Sex: 'Male', Year: '2019', Cases: 10 }]
|
|
144
|
+
const stableDomainRows = [
|
|
145
|
+
{ Sex: 'Female', Year: '2018', Cases: 'ABC' },
|
|
146
|
+
{ Sex: 'Male', Year: '2019', Cases: 10 }
|
|
147
|
+
]
|
|
148
|
+
const config = createSmallMultiplesConfig()
|
|
149
|
+
config.preliminaryData = [{ type: 'suppression', style: 'Dashed Small', column: '', value: 'ABC' }] as any
|
|
150
|
+
const context = createMockChartContext(config, {
|
|
151
|
+
colorScale: vi.fn(),
|
|
152
|
+
parentRef: { current: document.createElement('div') }
|
|
153
|
+
} as any)
|
|
154
|
+
|
|
155
|
+
render(
|
|
156
|
+
<ConfigContext.Provider value={context}>
|
|
157
|
+
<SmallMultiples
|
|
158
|
+
config={config}
|
|
159
|
+
data={filteredRows}
|
|
160
|
+
yAxisDomainData={stableDomainRows as any}
|
|
161
|
+
hasBoundarySuppression={true}
|
|
162
|
+
parentWidth={600}
|
|
163
|
+
parentHeight={300}
|
|
164
|
+
/>
|
|
165
|
+
</ConfigContext.Provider>
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
expect(mockUseScales.mock.calls[0][0].hasBoundarySuppression).toBe(true)
|
|
169
|
+
})
|
|
170
|
+
})
|
|
@@ -11,6 +11,7 @@ type CalculateLeftYAxisWidthProps = {
|
|
|
11
11
|
data: any[]
|
|
12
12
|
yScale: any
|
|
13
13
|
numTicks: number
|
|
14
|
+
tickValues?: number[]
|
|
14
15
|
parentWidth: number
|
|
15
16
|
tickLabelFont: string
|
|
16
17
|
axisLabelFontSize: number
|
|
@@ -28,7 +29,11 @@ type TickValueProps = Omit<
|
|
|
28
29
|
'parentWidth' | 'tickLabelFont' | 'axisLabelFontSize' | 'handleLeftTickFormatting'
|
|
29
30
|
>
|
|
30
31
|
|
|
31
|
-
const getTickValues = ({ config, data, yScale, numTicks }: TickValueProps) => {
|
|
32
|
+
const getTickValues = ({ config, data, yScale, numTicks, tickValues }: TickValueProps) => {
|
|
33
|
+
if (tickValues) {
|
|
34
|
+
return tickValues
|
|
35
|
+
}
|
|
36
|
+
|
|
32
37
|
if (config.visualizationType === 'Forest Plot') {
|
|
33
38
|
return data.map((_, index) => index)
|
|
34
39
|
}
|
|
@@ -142,6 +147,7 @@ export const calculateLeftYAxisWidth = ({
|
|
|
142
147
|
data,
|
|
143
148
|
yScale,
|
|
144
149
|
numTicks,
|
|
150
|
+
tickValues,
|
|
145
151
|
parentWidth,
|
|
146
152
|
tickLabelFont,
|
|
147
153
|
axisLabelFontSize,
|
|
@@ -153,10 +159,10 @@ export const calculateLeftYAxisWidth = ({
|
|
|
153
159
|
return getHorizonChartWidth({ config, parentWidth, tickLabelFont, axisLabelFontSize })
|
|
154
160
|
}
|
|
155
161
|
|
|
156
|
-
const
|
|
162
|
+
const measuredTickValues = getTickValues({ config, data, yScale, numTicks, tickValues })
|
|
157
163
|
const { hideAxis, inlineLabel, labelsAboveGridlines } = config.yAxis
|
|
158
164
|
const formattedTicks = formatMeasuredTickLabels({
|
|
159
|
-
tickValues,
|
|
165
|
+
tickValues: measuredTickValues,
|
|
160
166
|
handleLeftTickFormatting,
|
|
161
167
|
inlineLabel,
|
|
162
168
|
labelsAboveGridlines
|