@cdc/chart 4.26.1 → 4.26.2
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/CLAUDE.local.md +79 -0
- package/dist/{cdcchart-dgT_1dIT.es.js → cdcchart-DQ00cQCm.es.js} +1 -20
- package/dist/cdcchart.js +45357 -43655
- package/examples/default.json +378 -0
- package/examples/feature/__data__/horizon-chart-data.json +373 -0
- package/examples/feature/annotations/index.json +3 -6
- package/examples/feature/horizon/horizon-chart.json +395 -0
- package/examples/line-chart-states.json +1085 -0
- package/examples/private/123.json +694 -0
- package/examples/private/anchor-issue.json +4094 -0
- package/examples/private/backwards-slider.json +10430 -0
- package/examples/private/georgia.csv +160 -0
- package/examples/private/timeline-data.json +1 -0
- package/examples/private/timeline.json +389 -0
- package/examples/radar-chart-simple.json +133 -0
- package/examples/radar-chart.json +148 -0
- package/index.html +1 -31
- package/package.json +57 -59
- package/src/CdcChartComponent.tsx +99 -18
- package/src/_stories/Chart.Anchors.stories.tsx +10 -0
- package/src/_stories/Chart.BoxPlot.stories.tsx +7 -0
- package/src/_stories/Chart.CI.stories.tsx +13 -0
- package/src/_stories/Chart.Combo.stories.tsx +17 -0
- package/src/_stories/Chart.CustomColors.stories.tsx +4 -0
- package/src/_stories/Chart.DynamicSeries.stories.tsx +19 -0
- package/src/_stories/Chart.Filters.stories.tsx +4 -0
- package/src/_stories/Chart.Forecast.stories.tsx +4 -0
- package/src/_stories/Chart.HTMLInDataTable.stories.tsx +22 -0
- package/src/_stories/Chart.Legend.Gradient.stories.tsx +28 -0
- package/src/_stories/Chart.Patterns.stories.tsx +4 -0
- package/src/_stories/Chart.PreserveDecimals.stories.tsx +25 -0
- package/src/_stories/Chart.Regions.Categorical.stories.tsx +13 -0
- package/src/_stories/Chart.Regions.DateScale.stories.tsx +19 -0
- package/src/_stories/Chart.Regions.DateTimeScale.stories.tsx +25 -10
- package/src/_stories/Chart.ScatterPlot.stories.tsx +4 -0
- package/src/_stories/Chart.SmallMultiples.stories.tsx +16 -0
- package/src/_stories/Chart.stories.tsx +37 -0
- package/src/_stories/Chart.tooltip.stories.tsx +7 -0
- package/src/_stories/ChartAnnotation.stories.tsx +10 -0
- package/src/_stories/ChartAxisLabels.stories.tsx +4 -0
- package/src/_stories/ChartAxisTitles.stories.tsx +10 -0
- package/src/_stories/ChartBrush.Matrix.Continuous.stories.tsx +41 -0
- package/src/_stories/ChartBrush.Matrix.Date.stories.tsx +114 -0
- package/src/_stories/ChartBrush.Matrix.DateTime.stories.tsx +78 -0
- package/src/_stories/ChartBrush.stories.tsx +7 -0
- package/src/_stories/ChartEditor.stories.tsx +7 -0
- package/src/_stories/ChartLine.QuadrantAngles.stories.tsx +89 -0
- package/src/_stories/ChartLine.Suppression.stories.tsx +7 -0
- package/src/_stories/ChartLine.Symbols.stories.tsx +4 -0
- package/src/_stories/ChartPrefixSuffix.stories.tsx +46 -1
- package/src/_stories/TechAdoptionWithLinks.stories.tsx +7 -0
- package/src/_stories/_mock/brush_continuous.json +86 -0
- package/src/_stories/_mock/brush_date_large.json +176 -0
- package/src/_stories/_mock/line_chart_angle_near_zero_fall.json +195 -0
- package/src/_stories/_mock/line_chart_angle_near_zero_rise.json +195 -0
- package/src/_stories/_mock/line_chart_angle_q1_steep_upward.json +195 -0
- package/src/_stories/_mock/line_chart_angle_q2_gentle_downward.json +195 -0
- package/src/_stories/_mock/line_chart_angle_q3_steep_downward.json +195 -0
- package/src/_stories/_mock/line_chart_angle_q4_gentle_upward.json +195 -0
- package/src/_stories/_mock/line_chart_quadrant_angles.json +264 -0
- package/src/components/Annotations/components/AnnotationDraggable.styles.css +11 -17
- package/src/components/Annotations/components/AnnotationDraggable.tsx +240 -116
- package/src/components/Annotations/components/AnnotationDropdown.styles.css +1 -2
- package/src/components/Annotations/components/AnnotationDropdown.tsx +8 -12
- package/src/components/Annotations/components/AnnotationList.styles.css +4 -10
- package/src/components/Annotations/components/AnnotationList.tsx +5 -4
- package/src/components/Annotations/components/findNearestDatum.ts +75 -85
- package/src/components/Annotations/helpers/getVisibleAnnotations.ts +38 -0
- package/src/components/Axis/BottomAxis.tsx +270 -0
- package/src/components/Axis/LeftAxis.tsx +404 -0
- package/src/components/Axis/LeftAxisGridlines.tsx +77 -0
- package/src/components/Axis/PairedBarAxis.tsx +186 -0
- package/src/components/Axis/README.md +94 -0
- package/src/components/Axis/RightAxis.tsx +108 -0
- package/src/components/Axis/axis.constants.ts +21 -0
- package/src/components/Axis/index.ts +7 -0
- package/src/components/BarChart/components/BarChart.tsx +7 -1
- package/src/components/Brush/BrushSelector.tsx +154 -22
- package/src/components/Brush/MiniChartPreview.tsx +138 -21
- package/src/components/EditorPanel/EditorPanel.tsx +25 -11
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +60 -22
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +81 -1
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +1 -1
- package/src/components/EditorPanel/components/Panels/Panel.Radar.tsx +353 -0
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +0 -1
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +21 -1
- package/src/components/EditorPanel/components/Panels/index.tsx +2 -0
- package/src/components/EditorPanel/useEditorPermissions.ts +55 -26
- package/src/components/HorizonChart/HorizonChart.tsx +131 -0
- package/src/components/HorizonChart/components/HorizonBand.tsx +160 -0
- package/src/components/HorizonChart/helpers/calculateHorizonBands.ts +27 -0
- package/src/components/HorizonChart/helpers/getHorizonLayerColors.ts +40 -0
- package/src/components/HorizonChart/index.tsx +3 -0
- package/src/components/Legend/Legend.Component.tsx +52 -4
- package/src/components/Legend/Legend.tsx +1 -1
- package/src/components/Legend/LegendValueRange.tsx +77 -0
- package/src/components/Legend/helpers/createFormatLabels.tsx +13 -0
- package/src/components/Legend/helpers/generateValueRanges.ts +92 -0
- package/src/components/LineChart/helpers/README.md +292 -0
- package/src/components/LineChart/helpers/labelPositioning.test.ts +245 -0
- package/src/components/LineChart/helpers/labelPositioning.ts +304 -0
- package/src/components/LineChart/index.tsx +44 -8
- package/src/components/LinearChart/README.md +109 -0
- package/src/components/LinearChart/VisualizationRenderer.tsx +267 -0
- package/src/components/LinearChart/linearChart.constants.ts +84 -0
- package/src/components/LinearChart/tests/LinearChart.test.tsx +201 -0
- package/src/components/LinearChart/tests/mockConfigContext.ts +129 -0
- package/src/components/LinearChart/utils/tickFormatting.ts +146 -0
- package/src/components/LinearChart.tsx +250 -1059
- package/src/components/PieChart/PieChart.tsx +1 -1
- package/src/components/RadarChart/RadarAxis.tsx +78 -0
- package/src/components/RadarChart/RadarChart.tsx +298 -0
- package/src/components/RadarChart/RadarGrid.tsx +64 -0
- package/src/components/RadarChart/RadarPolygon.tsx +91 -0
- package/src/components/RadarChart/helpers.ts +83 -0
- package/src/components/RadarChart/index.tsx +3 -0
- package/src/components/WarmingStripes/WarmingStripes.tsx +95 -25
- package/src/data/initial-state.js +14 -1
- package/src/helpers/getExcludedData.ts +4 -0
- package/src/helpers/handleChartAriaLabels.ts +19 -19
- package/src/helpers/handleLineType.ts +22 -18
- package/src/hooks/useProgrammaticTooltip.ts +23 -2
- package/src/hooks/useScales.ts +7 -0
- package/src/hooks/useTooltip.tsx +3 -0
- package/src/scss/main.scss +5 -0
- package/src/selectors/README.md +68 -0
- package/src/store/chart.reducer.ts +2 -0
- package/src/types/ChartConfig.ts +18 -0
- package/src/types/ChartContext.ts +1 -0
- package/src/types/Horizon.ts +64 -0
- package/preview.html +0 -1616
- package/src/components/Annotations/components/helpers/index.tsx +0 -46
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useContext, useState } from 'react'
|
|
1
|
+
import { useContext, useState, useCallback, useEffect } from 'react'
|
|
2
2
|
import ConfigContext from '../../ConfigContext'
|
|
3
3
|
import { Group } from '@visx/group'
|
|
4
4
|
import { scaleSequential } from 'd3-scale'
|
|
@@ -6,7 +6,6 @@ import { interpolateRgbBasis } from 'd3-interpolate'
|
|
|
6
6
|
import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
|
|
7
7
|
import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
|
|
8
8
|
import { filterChartColorPalettes } from '@cdc/core/helpers/filterColorPalettes'
|
|
9
|
-
import { getColorPaletteVersion } from '@cdc/core/helpers/getColorPaletteVersion'
|
|
10
9
|
import { getFallbackColorPalette, migratePaletteWithMap } from '@cdc/core/helpers/palettes/utils'
|
|
11
10
|
import { paletteMigrationMap } from '@cdc/core/helpers/palettes/migratePaletteName'
|
|
12
11
|
import { hasTrackedHover, markHoverTracked } from '../../utils/analyticsTracking'
|
|
@@ -16,10 +15,26 @@ type WarmingStripesProps = {
|
|
|
16
15
|
yScale: any
|
|
17
16
|
xMax: number
|
|
18
17
|
yMax: number
|
|
18
|
+
synchronizedXValue?: any
|
|
19
|
+
showTooltip: (args: any) => void
|
|
20
|
+
handleTooltipMouseOff: () => void
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
const WarmingStripes = ({
|
|
22
|
-
|
|
23
|
+
const WarmingStripes = ({
|
|
24
|
+
xMax,
|
|
25
|
+
yMax,
|
|
26
|
+
synchronizedXValue,
|
|
27
|
+
showTooltip,
|
|
28
|
+
handleTooltipMouseOff
|
|
29
|
+
}: WarmingStripesProps) => {
|
|
30
|
+
const {
|
|
31
|
+
transformedData: data,
|
|
32
|
+
config,
|
|
33
|
+
formatNumber,
|
|
34
|
+
interactionLabel,
|
|
35
|
+
currentViewport,
|
|
36
|
+
handleSmallMultipleHover
|
|
37
|
+
} = useContext(ConfigContext)
|
|
23
38
|
|
|
24
39
|
const [currentHover, setCurrentHover] = useState<number | null>(null)
|
|
25
40
|
|
|
@@ -28,18 +43,14 @@ const WarmingStripes = ({ xMax, yMax }: WarmingStripesProps) => {
|
|
|
28
43
|
const valueKey = config.runtime.seriesKeys?.[0]
|
|
29
44
|
const xAxisDataKey = config.runtime.originalXAxis?.dataKey || config.xAxis?.dataKey
|
|
30
45
|
|
|
31
|
-
if (!valueKey || !xAxisDataKey || !data || data.length === 0) {
|
|
32
|
-
return null
|
|
33
|
-
}
|
|
34
|
-
|
|
35
46
|
// Determine max stripes based on viewport
|
|
36
47
|
const isMobile = ['xxs', 'xs', 'sm', 'md'].includes(currentViewport)
|
|
37
48
|
const maxStripes = isMobile ? 60 : 200
|
|
38
49
|
|
|
39
50
|
// Sample data if we have more than the max allowed stripes
|
|
40
|
-
let displayData = data
|
|
41
|
-
if (
|
|
42
|
-
const step =
|
|
51
|
+
let displayData = data || []
|
|
52
|
+
if (displayData.length > maxStripes) {
|
|
53
|
+
const step = displayData.length / maxStripes
|
|
43
54
|
displayData = []
|
|
44
55
|
for (let i = 0; i < maxStripes; i++) {
|
|
45
56
|
const index = Math.floor(i * step)
|
|
@@ -48,7 +59,7 @@ const WarmingStripes = ({ xMax, yMax }: WarmingStripesProps) => {
|
|
|
48
59
|
}
|
|
49
60
|
|
|
50
61
|
// Calculate the min and max values for the color scale
|
|
51
|
-
const values =
|
|
62
|
+
const values = displayData.map(d => Number(d[valueKey])).filter(v => !isNaN(v))
|
|
52
63
|
const minValue = Math.min(...values)
|
|
53
64
|
const maxValue = Math.max(...values)
|
|
54
65
|
|
|
@@ -94,15 +105,54 @@ const WarmingStripes = ({ xMax, yMax }: WarmingStripesProps) => {
|
|
|
94
105
|
// Calculate stripe width based on available space and display data
|
|
95
106
|
const stripeWidth = xMax / displayData.length
|
|
96
107
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
108
|
+
// Build tooltip data in COVE format and trigger showTooltip
|
|
109
|
+
const showStripeTooltip = useCallback(
|
|
110
|
+
(item: any, index: number, mouseY?: number) => {
|
|
111
|
+
const value = Number(item[valueKey])
|
|
112
|
+
if (isNaN(value)) return
|
|
113
|
+
|
|
114
|
+
const formattedValue = formatNumber(value, 'left')
|
|
115
|
+
|
|
116
|
+
// Pass raw x-axis value — TooltipListItem handles date formatting
|
|
117
|
+
const tooltipItems = [
|
|
118
|
+
[xAxisDataKey, item[xAxisDataKey]],
|
|
119
|
+
[valueKey, formattedValue, 'left']
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
const dataXPosition = index * stripeWidth + stripeWidth / 2 + Number(config.yAxis.size)
|
|
123
|
+
const dataYPosition = mouseY ?? yMax / 2
|
|
124
|
+
|
|
125
|
+
showTooltip({
|
|
126
|
+
tooltipLeft: dataXPosition + 10,
|
|
127
|
+
tooltipTop: dataYPosition,
|
|
128
|
+
tooltipData: {
|
|
129
|
+
data: tooltipItems,
|
|
130
|
+
dataXPosition: dataXPosition + 10,
|
|
131
|
+
dataYPosition
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
},
|
|
135
|
+
[valueKey, xAxisDataKey, stripeWidth, config.yAxis.size, yMax, formatNumber, showTooltip]
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
// Handle incoming synchronized tooltip from sibling small multiple tiles
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (synchronizedXValue === null || synchronizedXValue === undefined) {
|
|
141
|
+
setCurrentHover(null)
|
|
142
|
+
handleTooltipMouseOff()
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const matchIndex = displayData.findIndex(item => String(item[xAxisDataKey]) === String(synchronizedXValue))
|
|
147
|
+
|
|
148
|
+
if (matchIndex >= 0) {
|
|
149
|
+
setCurrentHover(matchIndex)
|
|
150
|
+
showStripeTooltip(displayData[matchIndex], matchIndex)
|
|
151
|
+
}
|
|
152
|
+
}, [synchronizedXValue])
|
|
101
153
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
<strong>${config.runtime.seriesLabels?.[valueKey] || valueKey}:</strong> ${formattedValue}
|
|
105
|
-
</div>`
|
|
154
|
+
if (!valueKey || !xAxisDataKey || !data || data.length === 0) {
|
|
155
|
+
return null
|
|
106
156
|
}
|
|
107
157
|
|
|
108
158
|
return (
|
|
@@ -126,13 +176,10 @@ const WarmingStripes = ({ xMax, yMax }: WarmingStripesProps) => {
|
|
|
126
176
|
fill={fillColor}
|
|
127
177
|
fillOpacity={isMuted ? 0.5 : 1}
|
|
128
178
|
stroke='none'
|
|
129
|
-
data-tooltip-html={handleTooltip(item)}
|
|
130
|
-
data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
|
|
131
179
|
tabIndex={-1}
|
|
132
180
|
style={{ cursor: 'pointer', transition: 'fill-opacity 0.2s ease' }}
|
|
133
|
-
onMouseEnter={
|
|
181
|
+
onMouseEnter={e => {
|
|
134
182
|
if (currentHover !== index) {
|
|
135
|
-
// Only publish analytics event once per visualization (shared tracking)
|
|
136
183
|
const vizId = String(config.runtime.uniqueId)
|
|
137
184
|
if (!hasTrackedHover(vizId)) {
|
|
138
185
|
publishAnalyticsEvent({
|
|
@@ -148,8 +195,31 @@ const WarmingStripes = ({ xMax, yMax }: WarmingStripesProps) => {
|
|
|
148
195
|
}
|
|
149
196
|
setCurrentHover(index)
|
|
150
197
|
}
|
|
198
|
+
|
|
199
|
+
// Show COVE tooltip using mouse Y position
|
|
200
|
+
const svgEl = (e.currentTarget as SVGRectElement).ownerSVGElement
|
|
201
|
+
const svgRect = svgEl?.getBoundingClientRect()
|
|
202
|
+
const mouseY = svgRect ? e.clientY - svgRect.top : yMax / 2
|
|
203
|
+
showStripeTooltip(item, index, mouseY)
|
|
204
|
+
|
|
205
|
+
if (handleSmallMultipleHover) {
|
|
206
|
+
handleSmallMultipleHover(item[xAxisDataKey], yMax / 2)
|
|
207
|
+
}
|
|
208
|
+
}}
|
|
209
|
+
onMouseMove={e => {
|
|
210
|
+
// Update tooltip Y position as mouse moves within the stripe
|
|
211
|
+
const svgEl = (e.currentTarget as SVGRectElement).ownerSVGElement
|
|
212
|
+
const svgRect = svgEl?.getBoundingClientRect()
|
|
213
|
+
const mouseY = svgRect ? e.clientY - svgRect.top : yMax / 2
|
|
214
|
+
showStripeTooltip(item, index, mouseY)
|
|
215
|
+
}}
|
|
216
|
+
onMouseLeave={() => {
|
|
217
|
+
setCurrentHover(null)
|
|
218
|
+
handleTooltipMouseOff()
|
|
219
|
+
if (handleSmallMultipleHover) {
|
|
220
|
+
handleSmallMultipleHover(null, null)
|
|
221
|
+
}
|
|
151
222
|
}}
|
|
152
|
-
onMouseLeave={() => setCurrentHover(null)}
|
|
153
223
|
/>
|
|
154
224
|
)
|
|
155
225
|
})}
|
|
@@ -45,7 +45,8 @@ const createInitialState = () => {
|
|
|
45
45
|
showSuppressedSymbol: true,
|
|
46
46
|
showZeroValueData: true,
|
|
47
47
|
hideNullValue: true,
|
|
48
|
-
palette: paletteDefaults
|
|
48
|
+
palette: paletteDefaults,
|
|
49
|
+
useIntelligentLineChartLabels: false
|
|
49
50
|
},
|
|
50
51
|
padding: {
|
|
51
52
|
left: 5,
|
|
@@ -298,6 +299,18 @@ const createInitialState = () => {
|
|
|
298
299
|
area: {
|
|
299
300
|
isStacked: false
|
|
300
301
|
},
|
|
302
|
+
radar: {
|
|
303
|
+
gridRings: 5,
|
|
304
|
+
showGridRings: true,
|
|
305
|
+
gridRingStyle: 'polygons',
|
|
306
|
+
scaleMin: 0,
|
|
307
|
+
scaleMax: '',
|
|
308
|
+
fillOpacity: 0.3,
|
|
309
|
+
showPoints: true,
|
|
310
|
+
pointRadius: 4,
|
|
311
|
+
strokeWidth: 2,
|
|
312
|
+
axisLabelOffset: 15
|
|
313
|
+
},
|
|
301
314
|
sankey: {
|
|
302
315
|
title: {
|
|
303
316
|
defaultColor: 'black'
|
|
@@ -2,6 +2,10 @@ import { isDateScale } from '@cdc/core/helpers/cove/date'
|
|
|
2
2
|
import _ from 'lodash'
|
|
3
3
|
import { ChartConfig } from '../types/ChartConfig'
|
|
4
4
|
export const getExcludedData = (newConfig: ChartConfig, data: object[]) => {
|
|
5
|
+
if (!Array.isArray(data)) {
|
|
6
|
+
console.warn('COVE: getExcludedData received non-array data:', typeof data)
|
|
7
|
+
return []
|
|
8
|
+
}
|
|
5
9
|
let newExcludedData = data
|
|
6
10
|
if (newConfig.exclusions && newConfig.exclusions.active) {
|
|
7
11
|
if (newConfig.xAxis.type === 'categorical' && newConfig.exclusions.keys?.length > 0) {
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
export const handleChartAriaLabels =
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
}
|
|
1
|
+
export const handleChartAriaLabels = state => {
|
|
2
|
+
try {
|
|
3
|
+
if (!state.visualizationType) throw Error('handleChartAriaLabels: no visualization type found in state')
|
|
4
|
+
let ariaLabel = ''
|
|
5
|
+
|
|
6
|
+
if (state.visualizationType) {
|
|
7
|
+
ariaLabel += `${state.visualizationType} chart`
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (state.title && state.visualizationType) {
|
|
11
|
+
ariaLabel += ` with the title: ${state.title}`
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return ariaLabel
|
|
15
|
+
} catch (e) {
|
|
16
|
+
console.error('COVE: ', e.message) // eslint-disable-line
|
|
17
|
+
return 'Data visualization container'
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -1,18 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
case '
|
|
12
|
-
return
|
|
13
|
-
case '
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
const DASH_PATTERNS = {
|
|
2
|
+
SMALL: '5 5',
|
|
3
|
+
MEDIUM: '10 5',
|
|
4
|
+
LARGE: '15 5',
|
|
5
|
+
SOLID: 0
|
|
6
|
+
} as const
|
|
7
|
+
|
|
8
|
+
export const handleLineType = (lineType: string): string | number => {
|
|
9
|
+
switch (lineType) {
|
|
10
|
+
case 'dashed-sm':
|
|
11
|
+
case 'Dashed Small':
|
|
12
|
+
return DASH_PATTERNS.SMALL
|
|
13
|
+
case 'dashed-md':
|
|
14
|
+
case 'Dashed Medium':
|
|
15
|
+
return DASH_PATTERNS.MEDIUM
|
|
16
|
+
case 'dashed-lg':
|
|
17
|
+
case 'Dashed Large':
|
|
18
|
+
return DASH_PATTERNS.LARGE
|
|
19
|
+
default:
|
|
20
|
+
return DASH_PATTERNS.SOLID
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -8,6 +8,7 @@ interface UseProgrammaticTooltipProps {
|
|
|
8
8
|
setShowHoverLine: (show: boolean) => void
|
|
9
9
|
handleTooltipMouseOver: (event: MouseEvent, additionalChartData?: any) => void
|
|
10
10
|
hideTooltip: () => void
|
|
11
|
+
setSynchronizedXValue?: (value: any) => void
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -21,7 +22,8 @@ export const useProgrammaticTooltip = ({
|
|
|
21
22
|
setPoint,
|
|
22
23
|
setShowHoverLine,
|
|
23
24
|
handleTooltipMouseOver,
|
|
24
|
-
hideTooltip
|
|
25
|
+
hideTooltip,
|
|
26
|
+
setSynchronizedXValue
|
|
25
27
|
}: UseProgrammaticTooltipProps) => {
|
|
26
28
|
// Internal SVG ref for DOM manipulation
|
|
27
29
|
const internalSvgRef = useRef<SVGSVGElement>(null)
|
|
@@ -50,6 +52,15 @@ export const useProgrammaticTooltip = ({
|
|
|
50
52
|
* @param {number} yCoordinate - Exact Y coordinate to use
|
|
51
53
|
*/
|
|
52
54
|
triggerTooltipAtDataValue: (xAxisValue: any, yCoordinate: number) => {
|
|
55
|
+
// Warming Stripes positions rects by index (with data sampling), not via xScale,
|
|
56
|
+
// so synthetic mouse events won't map to the correct data points.
|
|
57
|
+
// Route through synchronizedXValue state instead, which WarmingStripes
|
|
58
|
+
// resolves to the matching stripe and calls showTooltip directly.
|
|
59
|
+
if (config.visualizationType === 'Warming Stripes') {
|
|
60
|
+
setSynchronizedXValue?.(xAxisValue)
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
53
64
|
const pixelX = getCoordinateFromXValue(xAxisValue)
|
|
54
65
|
const adjustedX = pixelX + Number(config.yAxis.size || 0)
|
|
55
66
|
|
|
@@ -86,10 +97,20 @@ export const useProgrammaticTooltip = ({
|
|
|
86
97
|
hideTooltip: () => {
|
|
87
98
|
hideTooltip()
|
|
88
99
|
setShowHoverLine(false)
|
|
100
|
+
setSynchronizedXValue?.(null)
|
|
89
101
|
}
|
|
90
102
|
})
|
|
91
103
|
},
|
|
92
|
-
[
|
|
104
|
+
[
|
|
105
|
+
getCoordinateFromXValue,
|
|
106
|
+
config.yAxis.size,
|
|
107
|
+
config.visualizationType,
|
|
108
|
+
setPoint,
|
|
109
|
+
setShowHoverLine,
|
|
110
|
+
handleTooltipMouseOver,
|
|
111
|
+
hideTooltip,
|
|
112
|
+
setSynchronizedXValue
|
|
113
|
+
]
|
|
93
114
|
)
|
|
94
115
|
|
|
95
116
|
return internalSvgRef
|
package/src/hooks/useScales.ts
CHANGED
|
@@ -121,6 +121,11 @@ const useScales = (properties: useScaleProps) => {
|
|
|
121
121
|
range: [0, xMax]
|
|
122
122
|
})
|
|
123
123
|
|
|
124
|
+
let yScaleAnnotation = scaleLinear({
|
|
125
|
+
domain: [0, 100],
|
|
126
|
+
range: [0, yMax]
|
|
127
|
+
})
|
|
128
|
+
|
|
124
129
|
// handle Horizontal bars
|
|
125
130
|
if (isHorizontal) {
|
|
126
131
|
xScale = composeXScale({ min: min * 1.03, max, xMax, config })
|
|
@@ -372,6 +377,7 @@ const useScales = (properties: useScaleProps) => {
|
|
|
372
377
|
g2xScale,
|
|
373
378
|
xScaleNoPadding,
|
|
374
379
|
xScaleAnnotation,
|
|
380
|
+
yScaleAnnotation,
|
|
375
381
|
min,
|
|
376
382
|
max,
|
|
377
383
|
leftMax,
|
|
@@ -387,6 +393,7 @@ const getFirstDayOfMonth = ms => {
|
|
|
387
393
|
}
|
|
388
394
|
|
|
389
395
|
const dateFormatHasMonthButNoDays = dateFormat => {
|
|
396
|
+
if (!dateFormat) return false
|
|
390
397
|
return (
|
|
391
398
|
(dateFormat.includes('%b') ||
|
|
392
399
|
dateFormat.includes('%B') ||
|
package/src/hooks/useTooltip.tsx
CHANGED
|
@@ -592,6 +592,7 @@ export const useTooltip = props => {
|
|
|
592
592
|
case 'Line':
|
|
593
593
|
case 'Area Chart':
|
|
594
594
|
case 'Pie':
|
|
595
|
+
case 'Horizon Chart':
|
|
595
596
|
return common
|
|
596
597
|
case 'Combo':
|
|
597
598
|
return [...common, ...ciItems]
|
|
@@ -600,6 +601,8 @@ export const useTooltip = props => {
|
|
|
600
601
|
|
|
601
602
|
case 'Bar':
|
|
602
603
|
return orientation === 'vertical' ? common : [runtime.yAxis.dataKey, ...runtime?.seriesKeys]
|
|
604
|
+
case 'Warming Stripes':
|
|
605
|
+
return common
|
|
603
606
|
default:
|
|
604
607
|
throw new Error('No visualization type found in handleTooltipMouseOver')
|
|
605
608
|
}
|
package/src/scss/main.scss
CHANGED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Config Selectors
|
|
2
|
+
|
|
3
|
+
Typed selector functions and hooks for extracting memoizable slices of chart configuration.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
These selectors help:
|
|
8
|
+
1. **Reduce coupling** - Components depend on specific config slices, not the entire config object
|
|
9
|
+
2. **Improve memoization** - Hooks use specific dependencies instead of the full config
|
|
10
|
+
3. **Enhance testability** - Selectors can be easily mocked in tests
|
|
11
|
+
4. **Document config usage** - Selectors serve as documentation for which config properties are used together
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### Direct Selectors
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { selectAxisConfig, selectVisualizationConfig } from '../selectors'
|
|
19
|
+
|
|
20
|
+
// In a component
|
|
21
|
+
const axisConfig = selectAxisConfig(config)
|
|
22
|
+
// Returns: { xAxis, yAxis, runtime: { xAxis, yAxis, originalXAxis } }
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Hook Versions (Memoized)
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { useAxisConfig, useVisualizationConfig } from '../selectors'
|
|
29
|
+
|
|
30
|
+
// In a component - automatically memoized
|
|
31
|
+
const axisConfig = useAxisConfig(config)
|
|
32
|
+
const vizConfig = useVisualizationConfig(config)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Available Selectors
|
|
36
|
+
|
|
37
|
+
| Selector | Hook | Description |
|
|
38
|
+
|----------|------|-------------|
|
|
39
|
+
| `selectAxisConfig` | `useAxisConfig` | X/Y axis configuration |
|
|
40
|
+
| `selectVisualizationConfig` | `useVisualizationConfig` | Chart type and orientation |
|
|
41
|
+
| `selectDisplayConfig` | `useDisplayConfig` | Animation, debug flags |
|
|
42
|
+
| `selectTooltipConfig` | `useTooltipConfig` | Tooltip settings |
|
|
43
|
+
| `selectLegendConfig` | - | Legend position and visibility |
|
|
44
|
+
| `selectDataFormatConfig` | - | Number formatting options |
|
|
45
|
+
| `selectChartMessages` | - | UI message strings |
|
|
46
|
+
| `selectBrushConfig` | - | Brush/filter settings |
|
|
47
|
+
| `selectForestPlotConfig` | - | Forest plot specific options |
|
|
48
|
+
| `selectSmallMultiplesConfig` | - | Small multiples mode |
|
|
49
|
+
|
|
50
|
+
## Type Exports
|
|
51
|
+
|
|
52
|
+
Each selector has a corresponding type export:
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import type { AxisConfig, VisualizationConfig } from '../selectors'
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Adding New Selectors
|
|
59
|
+
|
|
60
|
+
1. Add the selector function that extracts config properties
|
|
61
|
+
2. Optionally add a hook version with proper dependencies
|
|
62
|
+
3. Export the type using `ReturnType<typeof selectXxx>`
|
|
63
|
+
4. Update this README
|
|
64
|
+
|
|
65
|
+
## Notes
|
|
66
|
+
|
|
67
|
+
- Selectors are created but integration into LinearChart is deferred
|
|
68
|
+
- Future work: gradually replace direct `config.x.y` accesses with selector usage
|
package/src/types/ChartConfig.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Axis } from '@cdc/core/types/Axis'
|
|
2
2
|
import { MarkupConfig } from '@cdc/core/types/MarkupVariable'
|
|
3
3
|
import { type ForestPlotConfigSettings } from './ForestPlot'
|
|
4
|
+
import { type HorizonConfigSettings } from './Horizon'
|
|
4
5
|
import { type Column } from '@cdc/core/types/Column'
|
|
5
6
|
import { type Series } from '@cdc/core/types/Series'
|
|
6
7
|
import { Runtime } from '@cdc/core/types/Runtime'
|
|
@@ -18,6 +19,7 @@ type General = CoreGeneral & {
|
|
|
18
19
|
customColors?: string[]
|
|
19
20
|
customColorsOrdered?: string[]
|
|
20
21
|
}
|
|
22
|
+
useIntelligentLineChartLabels?: boolean
|
|
21
23
|
}
|
|
22
24
|
import { type Link } from './../components/Sankey/types'
|
|
23
25
|
import { type DataDescription } from '@cdc/core/types/DataDescription'
|
|
@@ -39,9 +41,11 @@ export type VisualizationType =
|
|
|
39
41
|
| 'Box Plot'
|
|
40
42
|
| 'Deviation Bar'
|
|
41
43
|
| 'Forest Plot'
|
|
44
|
+
| 'Horizon Chart'
|
|
42
45
|
| 'Line'
|
|
43
46
|
| 'Paired Bar'
|
|
44
47
|
| 'Pie'
|
|
48
|
+
| 'Radar'
|
|
45
49
|
| 'Scatter Plot'
|
|
46
50
|
| 'Spark Line'
|
|
47
51
|
| 'Combo'
|
|
@@ -172,6 +176,7 @@ export type AllChartsConfig = {
|
|
|
172
176
|
mobileVertical: number
|
|
173
177
|
}
|
|
174
178
|
highlightedBarValues: { value: any; color: string; borderWidth: number; legendLabel: string }[]
|
|
179
|
+
horizon?: HorizonConfigSettings
|
|
175
180
|
introText: string
|
|
176
181
|
isLollipopChart: boolean
|
|
177
182
|
isLegendValue: boolean
|
|
@@ -267,6 +272,19 @@ export type AllChartsConfig = {
|
|
|
267
272
|
default: string
|
|
268
273
|
}
|
|
269
274
|
}
|
|
275
|
+
radar?: {
|
|
276
|
+
gridRings: number
|
|
277
|
+
showGridRings: boolean
|
|
278
|
+
gridRingStyle: 'polygons' | 'circles'
|
|
279
|
+
scaleMin: number
|
|
280
|
+
scaleMax: number | string
|
|
281
|
+
showFill: boolean
|
|
282
|
+
fillOpacity: number
|
|
283
|
+
showPoints: boolean
|
|
284
|
+
pointRadius: number
|
|
285
|
+
strokeWidth: number
|
|
286
|
+
axisLabelOffset: number
|
|
287
|
+
}
|
|
270
288
|
} & MarkupConfig
|
|
271
289
|
|
|
272
290
|
type ForestPlotConfig = {
|
|
@@ -36,6 +36,7 @@ type SharedChartContext = {
|
|
|
36
36
|
setLegendIsolateValues?: Function
|
|
37
37
|
svgRef?: React.RefObject<SVGSVGElement>
|
|
38
38
|
handleSmallMultipleHover?: (xAxisValue: any, yCoordinate: number) => void
|
|
39
|
+
visibleAnnotations?: Annotation[]
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
// Line Chart Specific Context
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration settings for Horizon Chart visualization
|
|
3
|
+
*
|
|
4
|
+
* Horizon charts display multiple time series as stacked rows,
|
|
5
|
+
* with each series shown as layered bands to compress vertical space.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Palette configuration structure (matches general.palette structure)
|
|
10
|
+
*/
|
|
11
|
+
export type HorizonPalette = {
|
|
12
|
+
name?: string
|
|
13
|
+
version?: '1.0' | '2.0'
|
|
14
|
+
isReversed?: boolean
|
|
15
|
+
customColors?: string[]
|
|
16
|
+
customColorsOrdered?: string[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type HorizonConfigSettings = {
|
|
20
|
+
/**
|
|
21
|
+
* Number of horizon layers/bands per series
|
|
22
|
+
* More layers = finer granularity, but denser visual
|
|
23
|
+
* @default 4
|
|
24
|
+
*/
|
|
25
|
+
numLayers: number
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Rendering mode for data values
|
|
29
|
+
* - 'offset': All values treated as positive (absolute values)
|
|
30
|
+
* - 'mirror': Negative values mirrored with contrasting colors. Not currently used.
|
|
31
|
+
* @default 'offset'
|
|
32
|
+
*/
|
|
33
|
+
mode: 'offset' | 'mirror'
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Gap in pixels between series bands
|
|
37
|
+
* Bands will fill available chart height minus gaps
|
|
38
|
+
* @default 15
|
|
39
|
+
*/
|
|
40
|
+
bandGap: number
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Padding in pixels below the bottom band (above x-axis)
|
|
44
|
+
* @default 15
|
|
45
|
+
*/
|
|
46
|
+
bottomPadding: number
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Optional secondary palette for negative values (mirror mode only)
|
|
50
|
+
* Uses same structure as general.palette for consistency
|
|
51
|
+
* Not exposed in UI initially - scaffolded for future use
|
|
52
|
+
*/
|
|
53
|
+
negativePalette?: HorizonPalette
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Default configuration for Horizon charts
|
|
58
|
+
*/
|
|
59
|
+
export const HORIZON_DEFAULTS: HorizonConfigSettings = {
|
|
60
|
+
numLayers: 4,
|
|
61
|
+
mode: 'offset',
|
|
62
|
+
bandGap: 15,
|
|
63
|
+
bottomPadding: 15
|
|
64
|
+
}
|