@cdc/chart 4.26.5 → 4.26.7
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 +20 -14
- package/dist/{cdcchart-DQ00cQCm.es.js → cdcchart-BWVIUcAF.es.js} +5 -55
- package/dist/cdcchart.js +36554 -34320
- 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/examples/private/flueview.json +22051 -0
- package/package.json +3 -11
- package/src/CdcChart.tsx +7 -6
- package/src/CdcChartComponent.tsx +71 -18
- package/src/_stories/Chart.PieLabels.stories.tsx +47 -0
- 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/Chart.smoke.stories.tsx +14 -1
- package/src/_stories/ChartAnnotation.smoke.stories.tsx +10 -0
- package/src/_stories/ChartPrefixSuffix.smoke.stories.tsx +28 -1
- 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/BarChart/components/BarChart.Horizontal.tsx +1 -2
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +1 -1
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +1 -1
- package/src/components/BarChart/components/BarChart.Vertical.tsx +1 -2
- package/src/components/BarChart/helpers/getBarData.ts +28 -28
- package/src/components/BarChart/helpers/getBarHeights.ts +2 -2
- package/src/components/BarChart/helpers/tests/useBarChart.test.tsx +98 -0
- package/src/components/BarChart/helpers/useBarChart.ts +4 -5
- package/src/components/BoxPlot/BoxPlot.Horizontal.tsx +7 -6
- package/src/components/BoxPlot/BoxPlot.Vertical.tsx +9 -7
- package/src/components/BoxPlot/helpers/index.ts +4 -3
- package/src/components/Brush/BrushSelector.tsx +1 -1
- package/src/components/EditorPanel/EditorPanel.tsx +457 -165
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +101 -52
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +16 -7
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +3 -3
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +19 -4
- 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/helpers/updateFieldRankByValue.ts +3 -3
- package/src/components/EditorPanel/useEditorPermissions.test.tsx +288 -11
- package/src/components/EditorPanel/useEditorPermissions.ts +46 -6
- package/src/components/Forecasting/Forecasting.tsx +1 -1
- 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 +56 -61
- package/src/components/Legend/Legend.Suppression.tsx +3 -3
- package/src/components/Legend/LegendGroup/LegendGroup.tsx +0 -1
- 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 +25 -14
- package/src/components/LineChart/index.tsx +23 -7
- package/src/components/LinearChart/tests/LinearChart.test.tsx +30 -0
- package/src/components/LinearChart.tsx +51 -15
- package/src/components/PieChart/PieChart.tsx +117 -10
- package/src/components/PieChart/helpers/labelPlacement.test.ts +88 -0
- package/src/components/PieChart/helpers/labelPlacement.ts +95 -0
- package/src/components/ScatterPlot/ScatterPlot.jsx +5 -13
- 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 +3 -0
- package/src/helpers/calculateLeftYAxisWidth.ts +9 -3
- package/src/helpers/categoryOrder.ts +60 -0
- package/src/helpers/colorAssignmentHelpers.ts +102 -0
- package/src/helpers/dataHelpers.ts +11 -1
- package/src/helpers/getAxisMaxOverride.ts +16 -0
- package/src/helpers/getBoxPlotConfig.ts +19 -12
- package/src/helpers/getCleanTopTickMax.ts +64 -0
- package/src/helpers/getColorScale.ts +18 -9
- package/src/helpers/getComboChartConfig.ts +0 -1
- package/src/helpers/getExcludedData.ts +0 -1
- package/src/helpers/getHasBoundarySuppression.ts +27 -0
- package/src/helpers/getMinMax.ts +25 -22
- package/src/helpers/getNewRuntime.ts +0 -2
- package/src/helpers/getPiePercent.ts +3 -3
- package/src/helpers/getYAxisDomainData.ts +38 -0
- package/src/helpers/getYAxisFinalizationEligibility.ts +40 -0
- package/src/helpers/hasSpacedInlineLabel.ts +10 -0
- package/src/helpers/isConvertLineToBarGraph.ts +2 -2
- package/src/helpers/seriesColumnSettings.ts +26 -7
- package/src/helpers/sizeHelpers.ts +0 -2
- package/src/helpers/smallMultiplesHelpers.ts +5 -0
- package/src/helpers/tests/calculateLeftYAxisWidth.test.ts +38 -0
- package/src/helpers/tests/categoryOrder.test.ts +57 -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 +76 -2
- package/src/helpers/tests/smallMultiplesHelpers.test.ts +99 -0
- package/src/hooks/tests/useRightAxis.test.tsx +268 -0
- package/src/hooks/tests/useScales.test.tsx +302 -27
- package/src/hooks/tests/useTooltip.test.tsx +132 -0
- package/src/hooks/useRightAxis.ts +35 -25
- package/src/hooks/useScales.ts +45 -28
- package/src/hooks/useTooltip.tsx +17 -11
- package/src/store/chart.reducer.ts +0 -1
- package/src/test/CdcChart.dataTable.test.tsx +216 -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
package/src/hooks/useScales.ts
CHANGED
|
@@ -13,11 +13,11 @@ import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
|
|
|
13
13
|
import ConfigContext from '../ConfigContext'
|
|
14
14
|
import { ChartConfig } from '../types/ChartConfig'
|
|
15
15
|
import { ChartContext } from '../types/ChartContext'
|
|
16
|
-
import _ from 'lodash'
|
|
17
|
-
import { getYAxisAutoPadding } from '../helpers/getYAxisAutoPadding'
|
|
18
|
-
import { YAxisAutoPaddingMode } from '../helpers/getYAxisAutoPaddingMode'
|
|
19
16
|
import getMinMax from '../helpers/getMinMax'
|
|
20
17
|
import { countNumOfTicks } from '../helpers/countNumOfTicks'
|
|
18
|
+
import { getFinalTopTickMax } from '../helpers/getCleanTopTickMax'
|
|
19
|
+
import { getYAxisFinalizationEligibility } from '../helpers/getYAxisFinalizationEligibility'
|
|
20
|
+
import { getAxisMaxOverride } from '../helpers/getAxisMaxOverride'
|
|
21
21
|
|
|
22
22
|
const scaleTypes = {
|
|
23
23
|
TIME: 'time',
|
|
@@ -33,6 +33,8 @@ type useScaleProps = {
|
|
|
33
33
|
config: ChartConfig // standard chart config
|
|
34
34
|
data: Object[] // standard data array
|
|
35
35
|
tableData: Object[] // table data for getMinMax
|
|
36
|
+
yAxisDomainData?: Object[] // data used to calculate Y-axis min/max
|
|
37
|
+
hasBoundarySuppression?: boolean // whether selected-domain boundaries include suppression markers
|
|
36
38
|
minValue: number // raw minimum value from data
|
|
37
39
|
maxValue: number // raw maximum value from data
|
|
38
40
|
existPositiveValue: boolean // whether data contains positive values
|
|
@@ -40,7 +42,7 @@ type useScaleProps = {
|
|
|
40
42
|
xAxisDataMapped: Object[] // array of x axis date/category items
|
|
41
43
|
xMax: number // chart svg width
|
|
42
44
|
yMax: number // chart svg height
|
|
43
|
-
|
|
45
|
+
hasSpacedInlineLabel?: boolean
|
|
44
46
|
currentViewport?: string // current viewport for tick calculation
|
|
45
47
|
}
|
|
46
48
|
|
|
@@ -52,11 +54,13 @@ const useScales = (properties: useScaleProps) => {
|
|
|
52
54
|
config,
|
|
53
55
|
data,
|
|
54
56
|
tableData,
|
|
57
|
+
yAxisDomainData,
|
|
58
|
+
hasBoundarySuppression = false,
|
|
55
59
|
minValue,
|
|
56
60
|
maxValue,
|
|
57
61
|
existPositiveValue,
|
|
58
62
|
isAllLine,
|
|
59
|
-
|
|
63
|
+
hasSpacedInlineLabel = false,
|
|
60
64
|
currentViewport
|
|
61
65
|
} = properties
|
|
62
66
|
|
|
@@ -66,8 +70,18 @@ const useScales = (properties: useScaleProps) => {
|
|
|
66
70
|
const isHorizontal = config.orientation === 'horizontal'
|
|
67
71
|
const { visualizationType, xAxis, forestPlot, runtime } = config
|
|
68
72
|
const isForestPlot = visualizationType === 'Forest Plot'
|
|
69
|
-
|
|
70
|
-
const
|
|
73
|
+
|
|
74
|
+
const { hasValidMax: hasValidExplicitLeftMax } = getAxisMaxOverride({
|
|
75
|
+
value: config.runtime?.yAxis?.max,
|
|
76
|
+
minimumValidMax: existPositiveValue ? maxValue : 0
|
|
77
|
+
})
|
|
78
|
+
const yAxisFinalizationEligibility = getYAxisFinalizationEligibility({
|
|
79
|
+
config,
|
|
80
|
+
hasSpacedInlineLabel,
|
|
81
|
+
hasValidExplicitLeftMax,
|
|
82
|
+
isHorizontal
|
|
83
|
+
})
|
|
84
|
+
const minMaxConfig = yAxisFinalizationEligibility.shouldUseInlineLabelHeadroom
|
|
71
85
|
? { ...config, yAxis: { ...config.yAxis, enablePadding: false, scalePadding: 0 } }
|
|
72
86
|
: config
|
|
73
87
|
|
|
@@ -76,10 +90,11 @@ const useScales = (properties: useScaleProps) => {
|
|
|
76
90
|
minValue,
|
|
77
91
|
maxValue,
|
|
78
92
|
existPositiveValue,
|
|
79
|
-
data,
|
|
93
|
+
data: yAxisDomainData || data,
|
|
80
94
|
isAllLine,
|
|
81
95
|
tableData,
|
|
82
|
-
convertLineToBarGraph
|
|
96
|
+
convertLineToBarGraph,
|
|
97
|
+
hasBoundarySuppression
|
|
83
98
|
}
|
|
84
99
|
let { min, max, leftMax, rightMax } = getMinMax(minMaxProps)
|
|
85
100
|
|
|
@@ -95,24 +110,25 @@ const useScales = (properties: useScaleProps) => {
|
|
|
95
110
|
})
|
|
96
111
|
const handleNumTicks = isForestPlot ? config.data.length : yTickCount
|
|
97
112
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
let yTickValues: number[] | undefined
|
|
114
|
+
|
|
115
|
+
if (yAxisFinalizationEligibility.shouldFinalizePrimaryYAxis) {
|
|
116
|
+
const yScaleForTicks = composeYScale({ min, max, yMax, config: minMaxConfig, leftMax })
|
|
117
|
+
const ticks = yScaleForTicks?.ticks ? yScaleForTicks.ticks(handleNumTicks) : []
|
|
118
|
+
const currentMax = minMaxConfig.visualizationType === 'Combo' ? leftMax : max
|
|
119
|
+
const { max: finalizedMax, tickValues } = getFinalTopTickMax({
|
|
120
|
+
currentMax,
|
|
121
|
+
rawMax: minMaxConfig.visualizationType === 'Combo' ? currentMax : maxValue,
|
|
122
|
+
ticks,
|
|
123
|
+
shouldUseCleanTopTick: yAxisFinalizationEligibility.shouldUseCleanTopTick,
|
|
124
|
+
headroomMode: yAxisFinalizationEligibility.shouldUseInlineLabelHeadroom ? 'inline-label' : 'none'
|
|
125
|
+
})
|
|
126
|
+
yTickValues = tickValues
|
|
127
|
+
|
|
128
|
+
if (minMaxConfig.visualizationType === 'Combo') {
|
|
129
|
+
leftMax = finalizedMax
|
|
130
|
+
} else {
|
|
131
|
+
max = finalizedMax
|
|
116
132
|
}
|
|
117
133
|
}
|
|
118
134
|
|
|
@@ -364,7 +380,8 @@ const useScales = (properties: useScaleProps) => {
|
|
|
364
380
|
min,
|
|
365
381
|
max,
|
|
366
382
|
leftMax,
|
|
367
|
-
rightMax
|
|
383
|
+
rightMax,
|
|
384
|
+
yTickValues
|
|
368
385
|
}
|
|
369
386
|
}
|
|
370
387
|
|
package/src/hooks/useTooltip.tsx
CHANGED
|
@@ -8,7 +8,11 @@ import { isDateScale } from '@cdc/core/helpers/cove/date'
|
|
|
8
8
|
// Third-party library imports
|
|
9
9
|
import { localPoint } from '@visx/event'
|
|
10
10
|
import { bisector } from 'd3-array'
|
|
11
|
-
import
|
|
11
|
+
import find from 'lodash/find'
|
|
12
|
+
import omit from 'lodash/omit'
|
|
13
|
+
import pick from 'lodash/pick'
|
|
14
|
+
import round from 'lodash/round'
|
|
15
|
+
import uniq from 'lodash/uniq'
|
|
12
16
|
import { getHorizontalBarHeights } from '../components/BarChart/helpers/getBarHeights'
|
|
13
17
|
import {
|
|
14
18
|
findColumnConfigByName,
|
|
@@ -148,11 +152,11 @@ export const useTooltip = props => {
|
|
|
148
152
|
const columnName = column.name || colKey
|
|
149
153
|
if (seriesOwnedColumnNames.includes(columnName)) continue
|
|
150
154
|
|
|
155
|
+
// Extra columns own their affixes; explicit blanks prevent fallback to the value-axis units.
|
|
151
156
|
const formattingParams = {
|
|
152
|
-
addColPrefix:
|
|
153
|
-
addColSuffix:
|
|
154
|
-
|
|
155
|
-
addColCommas: column.commas
|
|
157
|
+
addColPrefix: '',
|
|
158
|
+
addColSuffix: '',
|
|
159
|
+
...getSeriesColumnFormattingParams(column)
|
|
156
160
|
}
|
|
157
161
|
|
|
158
162
|
const pieColumnData = additionalChartData?.data?.[column.name]
|
|
@@ -328,7 +332,7 @@ export const useTooltip = props => {
|
|
|
328
332
|
|
|
329
333
|
const runtimeSeries =
|
|
330
334
|
config.tooltips.singleSeries && visualizationType === 'Line'
|
|
331
|
-
? [
|
|
335
|
+
? [find(config.runtime.series, d => d.dataKey === singleSeriesValue)]
|
|
332
336
|
: config.runtime.series
|
|
333
337
|
|
|
334
338
|
runtimeSeries?.forEach(series => {
|
|
@@ -569,7 +573,7 @@ export const useTooltip = props => {
|
|
|
569
573
|
const barsWithHeights = getHorizontalBarHeights<{ group }>(config, barGroups)
|
|
570
574
|
|
|
571
575
|
const barGroup = findClosest(
|
|
572
|
-
barsWithHeights.map(d => [d,
|
|
576
|
+
barsWithHeights.map(d => [d, round(d.y)]),
|
|
573
577
|
mouseY
|
|
574
578
|
)
|
|
575
579
|
|
|
@@ -580,7 +584,7 @@ export const useTooltip = props => {
|
|
|
580
584
|
const seriesWithY = config.series.map((c, i) => [c, config.barHeight * i]) as [Object, number][]
|
|
581
585
|
const hoveredSeries = findClosest(seriesWithY, subGroupMouseY)
|
|
582
586
|
const exludeColumns = config.series.filter(s => s.dataKey !== hoveredSeries.dataKey).map(s => s.dataKey)
|
|
583
|
-
const dataColumn =
|
|
587
|
+
const dataColumn = omit(columns[0], exludeColumns)
|
|
584
588
|
return dataColumn
|
|
585
589
|
} else {
|
|
586
590
|
const columnsWithY = columns.map((c, i) => [c, config.barHeight * i]) as [Object, number][]
|
|
@@ -661,7 +665,7 @@ export const useTooltip = props => {
|
|
|
661
665
|
const includedSeries = runtimeSeries.map(item => item.dataKey)
|
|
662
666
|
includedSeries.push(config.xAxis.dataKey)
|
|
663
667
|
// get dynamic category series
|
|
664
|
-
const dynamicDataCategories =
|
|
668
|
+
const dynamicDataCategories = uniq(
|
|
665
669
|
config.runtime.series.flatMap(series => {
|
|
666
670
|
if (series.dynamicCategory) {
|
|
667
671
|
return [series.dynamicCategory, series.originalDataKey]
|
|
@@ -701,7 +705,7 @@ export const useTooltip = props => {
|
|
|
701
705
|
}
|
|
702
706
|
}
|
|
703
707
|
|
|
704
|
-
return dataToSearch.map(d =>
|
|
708
|
+
return dataToSearch.map(d => pick(d, includedSeries))
|
|
705
709
|
}
|
|
706
710
|
|
|
707
711
|
/**
|
|
@@ -847,7 +851,9 @@ export const useTooltip = props => {
|
|
|
847
851
|
return (
|
|
848
852
|
<li
|
|
849
853
|
style={style}
|
|
850
|
-
className={
|
|
854
|
+
className={['tooltip-body', 'mb-1', shouldRenderMarkerSlot ? 'tooltip-body--marker-layout' : '']
|
|
855
|
+
.filter(Boolean)
|
|
856
|
+
.join(' ')}
|
|
851
857
|
>
|
|
852
858
|
{markerSlot}
|
|
853
859
|
{shouldRenderMarkerSlot ? <span className='tooltip-body-content'>{content}</span> : content}
|
|
@@ -2,7 +2,6 @@ import ChartActions from './chart.actions'
|
|
|
2
2
|
import defaults from '../data/initial-state.js'
|
|
3
3
|
import { ChartConfig, type ViewportSize } from '../types/ChartConfig'
|
|
4
4
|
import { DimensionsType } from '@cdc/core/types/Dimensions'
|
|
5
|
-
import _ from 'lodash'
|
|
6
5
|
|
|
7
6
|
type ChartState = {
|
|
8
7
|
isLoading: boolean
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { render, screen, waitFor } from '@testing-library/react'
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
import CdcChart from '../CdcChartComponent'
|
|
5
|
+
|
|
6
|
+
const dataTableProps = vi.hoisted(() => {
|
|
7
|
+
Object.defineProperty((globalThis as any).HTMLCanvasElement.prototype, 'getContext', {
|
|
8
|
+
configurable: true,
|
|
9
|
+
value: () => ({
|
|
10
|
+
measureText: (text = '') => ({ width: String(text).length * 8 })
|
|
11
|
+
})
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
return [] as any[]
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
vi.mock('@cdc/core/components/DataTable', async () => {
|
|
18
|
+
const React = await vi.importActual<typeof import('react')>('react')
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
default: props => {
|
|
22
|
+
dataTableProps.push(props)
|
|
23
|
+
return React.createElement('div', { 'data-testid': 'data-table-probe' })
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
vi.mock('@visx/responsive/lib/components/ParentSize', () => ({
|
|
29
|
+
default: ({ children }) => children({ width: 640, height: 360 })
|
|
30
|
+
}))
|
|
31
|
+
|
|
32
|
+
vi.mock('../components/LinearChart', async () => {
|
|
33
|
+
const React = await vi.importActual<typeof import('react')>('react')
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
default: React.forwardRef(() => React.createElement('div', { 'data-testid': 'mock-linear-chart' }))
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('CdcChart data table dataset wiring', () => {
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
dataTableProps.length = 0
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('passes the selected dashboard dataset metadata to DataTable', async () => {
|
|
46
|
+
const dataset = {
|
|
47
|
+
data: [{ category: 'A', value: 1 }],
|
|
48
|
+
dataFileName: 'stale-chart-source.json',
|
|
49
|
+
dataFileSourceType: 'url',
|
|
50
|
+
dataUrl: '/wcms/vizdata/chart-source.json',
|
|
51
|
+
runtimeDataUrl: '/wcms/vizdata/chart-runtime.json'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
render(
|
|
55
|
+
<CdcChart
|
|
56
|
+
config={
|
|
57
|
+
{
|
|
58
|
+
type: 'chart',
|
|
59
|
+
visualizationType: 'Bar',
|
|
60
|
+
title: 'Dataset-backed Chart',
|
|
61
|
+
dataKey: 'chartDataset',
|
|
62
|
+
data: dataset.data,
|
|
63
|
+
xAxis: { dataKey: 'category' },
|
|
64
|
+
series: [{ dataKey: 'value' }],
|
|
65
|
+
table: {
|
|
66
|
+
show: true,
|
|
67
|
+
expanded: true,
|
|
68
|
+
download: true,
|
|
69
|
+
label: 'Data Table',
|
|
70
|
+
indexLabel: ''
|
|
71
|
+
}
|
|
72
|
+
} as any
|
|
73
|
+
}
|
|
74
|
+
datasets={{ chartDataset: dataset } as any}
|
|
75
|
+
isDashboard={true}
|
|
76
|
+
interactionLabel='chart-dataset-test'
|
|
77
|
+
/>
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
await waitFor(() => expect(dataTableProps.length).toBeGreaterThan(0))
|
|
81
|
+
|
|
82
|
+
expect(dataTableProps.at(-1).dataConfig).toMatchObject({
|
|
83
|
+
runtimeDataUrl: '/wcms/vizdata/chart-runtime.json'
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('keeps chart footnotes visible when the data table is collapsed by default', async () => {
|
|
88
|
+
render(
|
|
89
|
+
<CdcChart
|
|
90
|
+
config={
|
|
91
|
+
{
|
|
92
|
+
type: 'chart',
|
|
93
|
+
visualizationType: 'Bar',
|
|
94
|
+
title: 'Footnote-backed Chart',
|
|
95
|
+
data: [{ category: 'A', value: 1 }],
|
|
96
|
+
xAxis: { dataKey: 'category' },
|
|
97
|
+
series: [{ dataKey: 'value' }],
|
|
98
|
+
legacyFootnotes: 'Legacy chart footnote',
|
|
99
|
+
footnotes: {
|
|
100
|
+
staticFootnotes: [{ text: 'Structured chart footnote' }]
|
|
101
|
+
},
|
|
102
|
+
table: {
|
|
103
|
+
show: true,
|
|
104
|
+
expanded: false,
|
|
105
|
+
download: true,
|
|
106
|
+
label: 'Data Table',
|
|
107
|
+
indexLabel: ''
|
|
108
|
+
}
|
|
109
|
+
} as any
|
|
110
|
+
}
|
|
111
|
+
interactionLabel='chart-footnote-collapse-test'
|
|
112
|
+
/>
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
await waitFor(() => {
|
|
116
|
+
expect(dataTableProps.at(-1)?.expandDataTable).toBe(false)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
expect(await screen.findByText('Legacy chart footnote')).toBeInTheDocument()
|
|
120
|
+
expect(await screen.findByText('Structured chart footnote')).toBeInTheDocument()
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('passes custom category order to the chart-owned data table runtime data', async () => {
|
|
124
|
+
render(
|
|
125
|
+
<CdcChart
|
|
126
|
+
config={
|
|
127
|
+
{
|
|
128
|
+
type: 'chart',
|
|
129
|
+
visualizationType: 'Bar',
|
|
130
|
+
title: 'Custom Category Order Chart',
|
|
131
|
+
data: [
|
|
132
|
+
{ category: 'A', value: 1 },
|
|
133
|
+
{ category: 'D', value: 4 },
|
|
134
|
+
{ category: 'C', value: 3 },
|
|
135
|
+
{ category: 'L', value: 12 }
|
|
136
|
+
],
|
|
137
|
+
xAxis: {
|
|
138
|
+
type: 'categorical',
|
|
139
|
+
dataKey: 'category',
|
|
140
|
+
categoryOrderType: 'custom',
|
|
141
|
+
categoryOrder: ['A', 'C', 'D', 'L']
|
|
142
|
+
},
|
|
143
|
+
series: [{ dataKey: 'value' }],
|
|
144
|
+
table: {
|
|
145
|
+
show: true,
|
|
146
|
+
expanded: true,
|
|
147
|
+
download: true,
|
|
148
|
+
label: 'Data Table',
|
|
149
|
+
indexLabel: ''
|
|
150
|
+
}
|
|
151
|
+
} as any
|
|
152
|
+
}
|
|
153
|
+
interactionLabel='chart-category-order-table-test'
|
|
154
|
+
/>
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
await waitFor(() => expect(dataTableProps.length).toBeGreaterThan(0))
|
|
158
|
+
|
|
159
|
+
expect(dataTableProps.at(-1).runtimeData.map(row => row.category)).toEqual(['A', 'C', 'D', 'L'])
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('updates metadata-backed chart title and text when dataMetadata changes and data does not', async () => {
|
|
163
|
+
const data = [{ category: 'A', value: 1 }]
|
|
164
|
+
const config = {
|
|
165
|
+
type: 'chart',
|
|
166
|
+
visualizationType: 'Bar',
|
|
167
|
+
title: 'Chart {{source}}',
|
|
168
|
+
introText: 'Intro {{source}}',
|
|
169
|
+
legend: {
|
|
170
|
+
label: 'Legend <strong>{{source}}</strong>',
|
|
171
|
+
description: 'Legend description <strong>{{source}}</strong>'
|
|
172
|
+
},
|
|
173
|
+
data,
|
|
174
|
+
dataMetadata: {},
|
|
175
|
+
enableMarkupVariables: true,
|
|
176
|
+
markupVariables: [
|
|
177
|
+
{
|
|
178
|
+
sourceType: 'metadata',
|
|
179
|
+
name: 'Source',
|
|
180
|
+
tag: '{{source}}',
|
|
181
|
+
metadataKey: 'source',
|
|
182
|
+
conditions: [],
|
|
183
|
+
addCommas: false
|
|
184
|
+
}
|
|
185
|
+
],
|
|
186
|
+
xAxis: { dataKey: 'category' },
|
|
187
|
+
series: [{ dataKey: 'value' }],
|
|
188
|
+
table: {
|
|
189
|
+
show: false,
|
|
190
|
+
expanded: false,
|
|
191
|
+
label: 'Data Table',
|
|
192
|
+
indexLabel: ''
|
|
193
|
+
}
|
|
194
|
+
} as any
|
|
195
|
+
|
|
196
|
+
const { container, rerender } = render(<CdcChart config={config} interactionLabel='chart-metadata-test' />)
|
|
197
|
+
|
|
198
|
+
expect(await screen.findByText('Chart')).toBeInTheDocument()
|
|
199
|
+
|
|
200
|
+
rerender(
|
|
201
|
+
<CdcChart
|
|
202
|
+
config={{
|
|
203
|
+
...config,
|
|
204
|
+
dataMetadata: { source: 'June file' }
|
|
205
|
+
}}
|
|
206
|
+
interactionLabel='chart-metadata-test'
|
|
207
|
+
/>
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
expect(await screen.findByText('Chart June file')).toBeInTheDocument()
|
|
211
|
+
expect(screen.getByText('Intro June file')).toBeInTheDocument()
|
|
212
|
+
expect(container.textContent).toContain('Legend June file')
|
|
213
|
+
expect(container.textContent).toContain('Legend description June file')
|
|
214
|
+
expect(screen.getAllByText('June file', { selector: 'strong' }).length).toBeGreaterThanOrEqual(2)
|
|
215
|
+
})
|
|
216
|
+
})
|
package/src/types/ChartConfig.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Axis } from '@cdc/core/types/Axis'
|
|
1
|
+
import { type Axis } from '@cdc/core/types/Axis'
|
|
2
2
|
import { MarkupConfig } from '@cdc/core/types/MarkupVariable'
|
|
3
3
|
import { type ForestPlotConfigSettings } from './ForestPlot'
|
|
4
4
|
import { type HorizonConfigSettings } from './Horizon'
|
|
@@ -18,6 +18,8 @@ type General = CoreGeneral & {
|
|
|
18
18
|
isReversed?: boolean
|
|
19
19
|
customColors?: string[]
|
|
20
20
|
customColorsOrdered?: string[]
|
|
21
|
+
colorAssignmentMode?: 'ordered' | 'by-value'
|
|
22
|
+
colorAssignments?: { key: string; color: string }[]
|
|
21
23
|
}
|
|
22
24
|
useIntelligentLineChartLabels?: boolean
|
|
23
25
|
}
|
|
@@ -83,6 +85,7 @@ export interface PreliminaryDataItem {
|
|
|
83
85
|
hideLineStyle: boolean
|
|
84
86
|
circleSize: number
|
|
85
87
|
displayGray: boolean
|
|
88
|
+
weight?: number
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
type DataFormat = {
|
|
@@ -191,6 +194,8 @@ export type AllChartsConfig = {
|
|
|
191
194
|
footnotes: Footnotes
|
|
192
195
|
forestPlot: ForestPlotConfigSettings
|
|
193
196
|
formattedData: Object[] & { urlFiltered: boolean }
|
|
197
|
+
originalFormattedData?: Object[]
|
|
198
|
+
yAxisDomainData?: Object[]
|
|
194
199
|
heatmap?: HeatMapConfig
|
|
195
200
|
heights: {
|
|
196
201
|
vertical: number
|
|
@@ -39,6 +39,7 @@ type SharedChartContext = {
|
|
|
39
39
|
svgRef?: React.RefObject<SVGSVGElement>
|
|
40
40
|
handleSmallMultipleHover?: (xAxisValue: any, yCoordinate: number) => void
|
|
41
41
|
visibleAnnotations?: Annotation[]
|
|
42
|
+
yAxisTickValues?: number[]
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
// Line Chart Specific Context
|
|
@@ -58,6 +59,7 @@ type LineChartContext = SharedChartContext & {
|
|
|
58
59
|
transformedData: TransformedData[]
|
|
59
60
|
updateConfig: Function
|
|
60
61
|
visualizationType: 'Line'
|
|
62
|
+
yAxisDomainData?: Object[]
|
|
61
63
|
colorPalettes: Record<string, string[]>
|
|
62
64
|
twoColorPalette: Record<string, string[]>
|
|
63
65
|
}
|
|
@@ -88,4 +90,5 @@ export type ChartContext =
|
|
|
88
90
|
transformedData?: TransformedData[]
|
|
89
91
|
twoColorPalette: any
|
|
90
92
|
updateConfig?: Function
|
|
93
|
+
yAxisDomainData?: Object[]
|
|
91
94
|
})
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { ChartConfig } from '../types/ChartConfig'
|
|
2
|
-
import { YAxisAutoPaddingMode } from './getYAxisAutoPaddingMode'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Calculates the Y-axis auto padding to prevent data labels from overlapping with axis tick labels.
|
|
6
|
-
* This is used when inline labels are enabled and there's potential for overlap.
|
|
7
|
-
*
|
|
8
|
-
* @param yScale - The D3 scale object for the Y-axis (must have .ticks() method)
|
|
9
|
-
* @param handleNumTicks - The number of ticks to display on the axis
|
|
10
|
-
* @param maxValue - The maximum data value (from useReduceData)
|
|
11
|
-
* @param minValue - The minimum data value (from useReduceData)
|
|
12
|
-
* @param config - The chart configuration object
|
|
13
|
-
* @returns The calculated auto padding percentage (0-100+), or 0 if no padding needed
|
|
14
|
-
*/
|
|
15
|
-
export const getYAxisAutoPadding = (
|
|
16
|
-
yScale: any,
|
|
17
|
-
handleNumTicks: number,
|
|
18
|
-
maxValue: number,
|
|
19
|
-
minValue: number,
|
|
20
|
-
config: ChartConfig,
|
|
21
|
-
mode: Exclude<YAxisAutoPaddingMode, 'none'>
|
|
22
|
-
): number => {
|
|
23
|
-
// Early returns for cases where auto padding is not needed
|
|
24
|
-
if (!yScale?.ticks || config.orientation === 'horizontal' || config.yAxis?.max) {
|
|
25
|
-
return 0
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const ticks = yScale.ticks(handleNumTicks)
|
|
29
|
-
|
|
30
|
-
if (!Array.isArray(ticks) || ticks.length === 0) {
|
|
31
|
-
return 0
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// minimum percentage of the max value that the distance should be from the top grid line
|
|
35
|
-
const MINIMUM_DISTANCE_PERCENTAGE = 0.025
|
|
36
|
-
|
|
37
|
-
const topGridLine = Math.max(...ticks)
|
|
38
|
-
const needsPaddingThreshold = topGridLine - maxValue * MINIMUM_DISTANCE_PERCENTAGE
|
|
39
|
-
const maxValueIsGreaterThanThreshold = maxValue > needsPaddingThreshold
|
|
40
|
-
|
|
41
|
-
if (!maxValueIsGreaterThanThreshold) return 0
|
|
42
|
-
|
|
43
|
-
const tickGap = ticks.length === 1 ? ticks[0] : ticks[1] - ticks[0]
|
|
44
|
-
const nextTick = Math.max(...ticks) + tickGap
|
|
45
|
-
const divideBy = minValue < 0 ? maxValue / 2 : maxValue
|
|
46
|
-
let calculatedPadding = (nextTick - maxValue) / divideBy
|
|
47
|
-
|
|
48
|
-
if (mode === 'inline-label' && calculatedPadding <= MINIMUM_DISTANCE_PERCENTAGE) {
|
|
49
|
-
calculatedPadding += tickGap / divideBy
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/* sometimes even though the padding is getting to the next tick exactly,
|
|
53
|
-
d3 still doesn't show the tick. we add 0.1 to ensure to tip it over the edge */
|
|
54
|
-
return calculatedPadding * 100 + 0.1
|
|
55
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { ChartConfig } from '../types/ChartConfig'
|
|
2
|
-
|
|
3
|
-
export type YAxisAutoPaddingMode = 'none' | 'inline-label' | 'top-title'
|
|
4
|
-
|
|
5
|
-
export const getYAxisAutoPaddingMode = (config: ChartConfig): YAxisAutoPaddingMode => {
|
|
6
|
-
const inlineLabel = config.yAxis?.inlineLabel
|
|
7
|
-
if (inlineLabel && inlineLabel.includes(' ')) {
|
|
8
|
-
return 'inline-label'
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const yAxisTitle = String(config.yAxis?.label || config.runtime?.yAxis?.label || '').trim()
|
|
12
|
-
if (config.yAxis?.titlePlacement === 'top' && yAxisTitle && !config.hideYAxisLabel) {
|
|
13
|
-
return 'top-title'
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
return 'none'
|
|
17
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { getYAxisAutoPadding } from '../getYAxisAutoPadding'
|
|
3
|
-
import { createMockConfig } from '../../components/LinearChart/tests/mockConfigContext'
|
|
4
|
-
|
|
5
|
-
const baseConfig = createMockConfig({
|
|
6
|
-
orientation: 'vertical'
|
|
7
|
-
})
|
|
8
|
-
|
|
9
|
-
describe('getYAxisAutoPadding', () => {
|
|
10
|
-
it('preserves the extra-tick escalation for inline-label mode', () => {
|
|
11
|
-
const yScale = {
|
|
12
|
-
ticks: () => [0, 1, 2]
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const padding = getYAxisAutoPadding(yScale, 3, 2.95, 0, baseConfig, 'inline-label')
|
|
16
|
-
|
|
17
|
-
expect(padding).toBeCloseTo(35.69, 2)
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
it('returns the single-pass padding for top-title mode', () => {
|
|
21
|
-
const yScale = {
|
|
22
|
-
ticks: () => [0, 1, 2]
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const padding = getYAxisAutoPadding(yScale, 3, 2.95, 0, baseConfig, 'top-title')
|
|
26
|
-
|
|
27
|
-
expect(padding).toBeCloseTo(1.79, 2)
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('returns 0 when the data is not close enough to the top tick', () => {
|
|
31
|
-
const yScale = {
|
|
32
|
-
ticks: () => [0, 50, 100]
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
expect(getYAxisAutoPadding(yScale, 3, 80, 0, baseConfig, 'top-title')).toBe(0)
|
|
36
|
-
})
|
|
37
|
-
})
|