@cdc/chart 4.25.11 → 4.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cdcchart.js +38898 -40013
- package/examples/feature/pie/planet-pie-example-config.json +48 -2
- package/examples/private/DEV-12100.json +1303 -0
- package/examples/private/cat-y.json +1235 -0
- package/examples/private/data-points.json +228 -0
- package/examples/private/height.json +3915 -0
- package/examples/private/links.json +569 -0
- package/examples/private/quadrant.txt +30 -0
- package/examples/private/test-forecast.json +5510 -0
- package/examples/private/warming-stripe-test.json +2578 -0
- package/examples/private/warming-stripes.json +4763 -0
- package/examples/tech-adoption-with-links.json +560 -0
- package/index.html +15 -20
- package/package.json +5 -4
- package/preview.html +1616 -0
- package/src/CdcChartComponent.tsx +111 -75
- package/src/_stories/Chart.Regions.Categorical.stories.tsx +148 -0
- package/src/_stories/Chart.Regions.DateScale.stories.tsx +197 -0
- package/src/_stories/Chart.Regions.DateTimeScale.stories.tsx +297 -0
- package/src/_stories/Chart.stories.tsx +8 -0
- package/src/_stories/ChartBar.Editor.stories.tsx +11 -6
- package/src/_stories/ChartBrush.Editor.stories.tsx +295 -0
- package/src/_stories/ChartBrush.stories.tsx +50 -0
- package/src/_stories/ChartEditor.Editor.stories.tsx +3 -5
- package/src/_stories/TechAdoptionWithLinks.stories.tsx +27 -0
- package/src/_stories/_mock/brush_enabled.json +326 -0
- package/src/_stories/_mock/brush_mock.json +2 -69
- package/src/_stories/_mock/horizontal-bars-dynamic-y-axis.json +413 -0
- package/src/components/AreaChart/components/AreaChart.Stacked.jsx +1 -2
- package/src/components/Axis/Categorical.Axis.tsx +6 -7
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +178 -24
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +3 -1
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +1 -0
- package/src/components/BarChart/components/BarChart.Vertical.tsx +6 -8
- package/src/components/BarChart/components/context.tsx +1 -0
- package/src/components/BarChart/helpers/useBarChart.ts +14 -2
- package/src/components/Brush/BrushSelector.tsx +1258 -0
- package/src/components/Brush/MiniChartPreview.tsx +283 -0
- package/src/components/DeviationBar.jsx +9 -7
- package/src/components/EditorPanel/EditorPanel.tsx +2711 -2586
- package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +56 -34
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +57 -30
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +2 -0
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +30 -25
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +21 -27
- package/src/components/EditorPanel/useEditorPermissions.ts +31 -18
- package/src/components/Legend/Legend.tsx +3 -2
- package/src/components/Legend/helpers/createFormatLabels.tsx +151 -2
- package/src/components/Legend/helpers/index.ts +10 -6
- package/src/components/LinearChart.tsx +495 -430
- package/src/components/PairedBarChart.jsx +20 -3
- package/src/components/Regions/components/Regions.tsx +365 -122
- package/src/components/ScatterPlot/ScatterPlot.jsx +2 -2
- package/src/components/SmallMultiples/SmallMultipleTile.tsx +5 -1
- package/src/components/WarmingStripes/WarmingStripes.tsx +160 -0
- package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +35 -0
- package/src/components/WarmingStripes/WarmingStripesGradientLegend.tsx +104 -0
- package/src/components/WarmingStripes/index.tsx +3 -0
- package/src/data/initial-state.js +3 -1
- package/src/helpers/calculateHorizontalBarCategoryLabelWidth.ts +57 -0
- package/src/helpers/getMinMax.ts +12 -7
- package/src/helpers/sizeHelpers.ts +0 -20
- package/src/helpers/smallMultiplesHelpers.ts +1 -1
- package/src/hooks/useChartHoverAnalytics.tsx +10 -9
- package/src/hooks/useScales.ts +11 -1
- package/src/hooks/useTooltip.tsx +31 -10
- package/src/scss/DataTable.scss +0 -4
- package/src/scss/main.scss +17 -3
- package/src/test/CdcChart.test.jsx +1 -1
- package/src/types/ChartConfig.ts +3 -0
- package/src/types/Label.ts +1 -0
- package/src/utils/analyticsTracking.ts +19 -0
- package/LICENSE +0 -201
- package/src/components/Brush/BrushChart.tsx +0 -128
- package/src/components/Brush/BrushController.tsx +0 -71
- package/src/components/Brush/types.tsx +0 -8
- package/src/components/BrushChart.tsx +0 -223
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { useContext, useState } from 'react'
|
|
2
|
+
import ConfigContext from '../../ConfigContext'
|
|
3
|
+
import { Group } from '@visx/group'
|
|
4
|
+
import { scaleSequential } from 'd3-scale'
|
|
5
|
+
import { interpolateRgbBasis } from 'd3-interpolate'
|
|
6
|
+
import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
|
|
7
|
+
import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
|
|
8
|
+
import { filterChartColorPalettes } from '@cdc/core/helpers/filterColorPalettes'
|
|
9
|
+
import { getColorPaletteVersion } from '@cdc/core/helpers/getColorPaletteVersion'
|
|
10
|
+
import { getFallbackColorPalette, migratePaletteWithMap } from '@cdc/core/helpers/palettes/utils'
|
|
11
|
+
import { paletteMigrationMap } from '@cdc/core/helpers/palettes/migratePaletteName'
|
|
12
|
+
import { hasTrackedHover, markHoverTracked } from '../../utils/analyticsTracking'
|
|
13
|
+
|
|
14
|
+
type WarmingStripesProps = {
|
|
15
|
+
xScale: any
|
|
16
|
+
yScale: any
|
|
17
|
+
xMax: number
|
|
18
|
+
yMax: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const WarmingStripes = ({ xMax, yMax }: WarmingStripesProps) => {
|
|
22
|
+
const { transformedData: data, config, formatNumber, interactionLabel, currentViewport } = useContext(ConfigContext)
|
|
23
|
+
|
|
24
|
+
const [currentHover, setCurrentHover] = useState<number | null>(null)
|
|
25
|
+
|
|
26
|
+
// Get the data key for the temperature/anomaly values
|
|
27
|
+
// Use the first series key as the value column
|
|
28
|
+
const valueKey = config.runtime.seriesKeys?.[0]
|
|
29
|
+
const xAxisDataKey = config.runtime.originalXAxis?.dataKey || config.xAxis?.dataKey
|
|
30
|
+
|
|
31
|
+
if (!valueKey || !xAxisDataKey || !data || data.length === 0) {
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Determine max stripes based on viewport
|
|
36
|
+
const isMobile = ['xxs', 'xs', 'sm', 'md'].includes(currentViewport)
|
|
37
|
+
const maxStripes = isMobile ? 60 : 200
|
|
38
|
+
|
|
39
|
+
// Sample data if we have more than the max allowed stripes
|
|
40
|
+
let displayData = data
|
|
41
|
+
if (data.length > maxStripes) {
|
|
42
|
+
const step = data.length / maxStripes
|
|
43
|
+
displayData = []
|
|
44
|
+
for (let i = 0; i < maxStripes; i++) {
|
|
45
|
+
const index = Math.floor(i * step)
|
|
46
|
+
displayData.push(data[index])
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Calculate the min and max values for the color scale
|
|
51
|
+
const values = data.map(d => Number(d[valueKey])).filter(v => !isNaN(v))
|
|
52
|
+
const minValue = Math.min(...values)
|
|
53
|
+
const maxValue = Math.max(...values)
|
|
54
|
+
|
|
55
|
+
// Get the color palette from config
|
|
56
|
+
const colorPalettes = filterChartColorPalettes(config)
|
|
57
|
+
const configPalette = config.general?.palette?.name
|
|
58
|
+
const migratedPaletteName = configPalette ? configPalette : getFallbackColorPalette(config)
|
|
59
|
+
|
|
60
|
+
// Check if the palette name ends with 'reverse' and get the base palette
|
|
61
|
+
const isReversedPalette = migratedPaletteName?.endsWith('reverse')
|
|
62
|
+
const basePaletteName = isReversedPalette ? migratedPaletteName.slice(0, -7) : migratedPaletteName
|
|
63
|
+
|
|
64
|
+
let palette =
|
|
65
|
+
colorPalettes[migratePaletteWithMap(basePaletteName, paletteMigrationMap, false)] ||
|
|
66
|
+
colorPalettes[basePaletteName] ||
|
|
67
|
+
colorPalettes[configPalette]
|
|
68
|
+
|
|
69
|
+
// Fallback to a default diverging palette if none found
|
|
70
|
+
if (!palette || palette.length < 2) {
|
|
71
|
+
console.warn(`Palette "${configPalette}" not found or invalid, falling back to default`)
|
|
72
|
+
// Use a default blue to red palette
|
|
73
|
+
palette = [
|
|
74
|
+
'#053061',
|
|
75
|
+
'#2166ac',
|
|
76
|
+
'#4393c3',
|
|
77
|
+
'#92c5de',
|
|
78
|
+
'#d1e5f0',
|
|
79
|
+
'#f7f7f7',
|
|
80
|
+
'#fddbc7',
|
|
81
|
+
'#f4a582',
|
|
82
|
+
'#d6604d',
|
|
83
|
+
'#b2182b',
|
|
84
|
+
'#67001f'
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Create a sequential color scale using the palette colors
|
|
89
|
+
// Apply reverse if configured (either via isReversed flag or 'reverse' suffix in name)
|
|
90
|
+
const shouldReverse = config.general?.palette?.isReversed || isReversedPalette
|
|
91
|
+
const finalPalette = shouldReverse ? [...palette].reverse() : palette
|
|
92
|
+
const colorScale = scaleSequential(interpolateRgbBasis(finalPalette)).domain([minValue, maxValue])
|
|
93
|
+
|
|
94
|
+
// Calculate stripe width based on available space and display data
|
|
95
|
+
const stripeWidth = xMax / displayData.length
|
|
96
|
+
|
|
97
|
+
const handleTooltip = (item: any) => {
|
|
98
|
+
const xValue = item[xAxisDataKey]
|
|
99
|
+
const yValue = item[valueKey]
|
|
100
|
+
const formattedValue = formatNumber(yValue, 'left')
|
|
101
|
+
|
|
102
|
+
return `<div>
|
|
103
|
+
<strong>${config.xAxis.label || xAxisDataKey}:</strong> ${xValue}<br/>
|
|
104
|
+
<strong>${config.runtime.seriesLabels?.[valueKey] || valueKey}:</strong> ${formattedValue}
|
|
105
|
+
</div>`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<Group className='warming-stripes' left={config.yAxis.size}>
|
|
110
|
+
{displayData.map((item, index) => {
|
|
111
|
+
const value = Number(item[valueKey])
|
|
112
|
+
if (isNaN(value)) return null
|
|
113
|
+
|
|
114
|
+
const xPosition = index * stripeWidth
|
|
115
|
+
const fillColor = colorScale(value) as unknown as string
|
|
116
|
+
const isHovered = currentHover === index
|
|
117
|
+
const isMuted = currentHover !== null && !isHovered
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<rect
|
|
121
|
+
key={`stripe-${index}`}
|
|
122
|
+
x={xPosition}
|
|
123
|
+
y={0}
|
|
124
|
+
width={stripeWidth}
|
|
125
|
+
height={yMax}
|
|
126
|
+
fill={fillColor}
|
|
127
|
+
fillOpacity={isMuted ? 0.5 : 1}
|
|
128
|
+
stroke='none'
|
|
129
|
+
data-tooltip-html={handleTooltip(item)}
|
|
130
|
+
data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
|
|
131
|
+
tabIndex={-1}
|
|
132
|
+
style={{ cursor: 'pointer', transition: 'fill-opacity 0.2s ease' }}
|
|
133
|
+
onMouseEnter={() => {
|
|
134
|
+
if (currentHover !== index) {
|
|
135
|
+
// Only publish analytics event once per visualization (shared tracking)
|
|
136
|
+
const vizId = String(config.runtime.uniqueId)
|
|
137
|
+
if (!hasTrackedHover(vizId)) {
|
|
138
|
+
publishAnalyticsEvent({
|
|
139
|
+
vizType: config?.type,
|
|
140
|
+
vizSubType: getVizSubType(config),
|
|
141
|
+
eventType: 'chart_hover',
|
|
142
|
+
eventAction: 'hover',
|
|
143
|
+
eventLabel: interactionLabel || 'unknown',
|
|
144
|
+
vizTitle: getVizTitle(config),
|
|
145
|
+
series: valueKey
|
|
146
|
+
})
|
|
147
|
+
markHoverTracked(vizId)
|
|
148
|
+
}
|
|
149
|
+
setCurrentHover(index)
|
|
150
|
+
}
|
|
151
|
+
}}
|
|
152
|
+
onMouseLeave={() => setCurrentHover(null)}
|
|
153
|
+
/>
|
|
154
|
+
)
|
|
155
|
+
})}
|
|
156
|
+
</Group>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export default WarmingStripes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
.warming-stripes-gradient-legend {
|
|
2
|
+
margin: 1rem 0;
|
|
3
|
+
width: 100%;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.warming-stripes-gradient-legend__title {
|
|
7
|
+
font-weight: 600;
|
|
8
|
+
margin-bottom: 0.5rem;
|
|
9
|
+
font-size: 16px;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.warming-stripes-gradient-legend__description {
|
|
13
|
+
margin-top: 0.5rem;
|
|
14
|
+
margin-bottom: 1rem;
|
|
15
|
+
font-size: 14px;
|
|
16
|
+
color: #555;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.warming-stripes-gradient-legend__container {
|
|
20
|
+
width: 100%;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.warming-stripes-gradient-legend__svg {
|
|
24
|
+
display: block;
|
|
25
|
+
width: 100%;
|
|
26
|
+
overflow: visible;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.warming-stripes-gradient-legend__series-label {
|
|
30
|
+
text-align: center;
|
|
31
|
+
margin-top: 0.5rem;
|
|
32
|
+
font-size: 14px;
|
|
33
|
+
font-weight: 500;
|
|
34
|
+
color: #333;
|
|
35
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { useContext } from 'react'
|
|
2
|
+
import ConfigContext from '../../ConfigContext'
|
|
3
|
+
import { filterChartColorPalettes } from '@cdc/core/helpers/filterColorPalettes'
|
|
4
|
+
import { getFallbackColorPalette, migratePaletteWithMap } from '@cdc/core/helpers/palettes/utils'
|
|
5
|
+
import { paletteMigrationMap } from '@cdc/core/helpers/palettes/migratePaletteName'
|
|
6
|
+
import './WarmingStripesGradientLegend.css'
|
|
7
|
+
|
|
8
|
+
const WarmingStripesGradientLegend = () => {
|
|
9
|
+
const { transformedData: data, config, formatNumber } = useContext(ConfigContext)
|
|
10
|
+
|
|
11
|
+
const valueKey = config.runtime.seriesKeys?.[0]
|
|
12
|
+
|
|
13
|
+
if (!valueKey || !data || data.length === 0 || config.legend?.hide) {
|
|
14
|
+
return null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Calculate min and max values
|
|
18
|
+
const values = data.map(d => Number(d[valueKey])).filter(v => !isNaN(v))
|
|
19
|
+
const minValue = Math.min(...values)
|
|
20
|
+
const maxValue = Math.max(...values)
|
|
21
|
+
|
|
22
|
+
// Get the color palette from config (same logic as WarmingStripes component)
|
|
23
|
+
const colorPalettes = filterChartColorPalettes(config)
|
|
24
|
+
const configPalette = config.general?.palette?.name
|
|
25
|
+
const migratedPaletteName = configPalette ? configPalette : getFallbackColorPalette(config)
|
|
26
|
+
|
|
27
|
+
const isReversedPalette = migratedPaletteName?.endsWith('reverse')
|
|
28
|
+
const basePaletteName = isReversedPalette ? migratedPaletteName.slice(0, -7) : migratedPaletteName
|
|
29
|
+
|
|
30
|
+
let palette =
|
|
31
|
+
colorPalettes[migratePaletteWithMap(basePaletteName, paletteMigrationMap, false)] ||
|
|
32
|
+
colorPalettes[basePaletteName] ||
|
|
33
|
+
colorPalettes[configPalette]
|
|
34
|
+
|
|
35
|
+
if (!palette || palette.length < 2) {
|
|
36
|
+
palette = [
|
|
37
|
+
'#053061',
|
|
38
|
+
'#2166ac',
|
|
39
|
+
'#4393c3',
|
|
40
|
+
'#92c5de',
|
|
41
|
+
'#d1e5f0',
|
|
42
|
+
'#f7f7f7',
|
|
43
|
+
'#fddbc7',
|
|
44
|
+
'#f4a582',
|
|
45
|
+
'#d6604d',
|
|
46
|
+
'#b2182b',
|
|
47
|
+
'#67001f'
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const shouldReverse = config.general?.palette?.isReversed || isReversedPalette
|
|
52
|
+
const finalPalette = shouldReverse ? [...palette].reverse() : palette
|
|
53
|
+
|
|
54
|
+
// Create gradient stops for SVG
|
|
55
|
+
const gradientStops = finalPalette.map((color, index) => {
|
|
56
|
+
const offset = (index / (finalPalette.length - 1)) * 100
|
|
57
|
+
return { offset: `${offset}%`, color }
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const seriesLabel = config.runtime.seriesLabels?.[valueKey] || valueKey
|
|
61
|
+
const uniqueId = `warming-stripes-gradient-${config.runtime.uniqueId}`
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className='warming-stripes-gradient-legend'>
|
|
65
|
+
{config.legend?.label && <h3 className='warming-stripes-gradient-legend__title'>{config.legend.label}</h3>}
|
|
66
|
+
{config.legend?.description && (
|
|
67
|
+
<p className='warming-stripes-gradient-legend__description'>{config.legend.description}</p>
|
|
68
|
+
)}
|
|
69
|
+
|
|
70
|
+
<div className='warming-stripes-gradient-legend__container'>
|
|
71
|
+
<svg className='warming-stripes-gradient-legend__svg' height='50' width='100%'>
|
|
72
|
+
<defs>
|
|
73
|
+
<linearGradient id={uniqueId} x1='0%' y1='0%' x2='100%' y2='0%'>
|
|
74
|
+
{gradientStops.map((stop, index) => (
|
|
75
|
+
<stop key={index} offset={stop.offset} stopColor={stop.color} />
|
|
76
|
+
))}
|
|
77
|
+
</linearGradient>
|
|
78
|
+
</defs>
|
|
79
|
+
|
|
80
|
+
{/* Border */}
|
|
81
|
+
<rect x='0' y='0' width='100%' height='25' fill='#d3d3d3' />
|
|
82
|
+
|
|
83
|
+
{/* Gradient bar */}
|
|
84
|
+
<rect x='1' y='1' width='calc(100% - 2px)' height='23' fill={`url(#${uniqueId})`} />
|
|
85
|
+
|
|
86
|
+
{/* Min label */}
|
|
87
|
+
<text x='0' y='40' fontSize='14' textAnchor='start' fill='#333'>
|
|
88
|
+
{formatNumber(minValue, 'left')}
|
|
89
|
+
</text>
|
|
90
|
+
|
|
91
|
+
{/* Max label */}
|
|
92
|
+
<text x='100%' y='40' fontSize='14' textAnchor='end' fill='#333'>
|
|
93
|
+
{formatNumber(maxValue, 'left')}
|
|
94
|
+
</text>
|
|
95
|
+
</svg>
|
|
96
|
+
|
|
97
|
+
{/* Series name centered below gradient */}
|
|
98
|
+
<div className='warming-stripes-gradient-legend__series-label'>{seriesLabel}</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export default WarmingStripesGradientLegend
|
|
@@ -23,6 +23,7 @@ const createInitialState = () => {
|
|
|
23
23
|
noData: 'No Data Available'
|
|
24
24
|
},
|
|
25
25
|
title: '',
|
|
26
|
+
titleStyle: 'small',
|
|
26
27
|
showTitle: true,
|
|
27
28
|
showDownloadMediaButton: false,
|
|
28
29
|
theme: 'theme-blue',
|
|
@@ -139,7 +140,8 @@ const createInitialState = () => {
|
|
|
139
140
|
padding: 5,
|
|
140
141
|
showYearsOnce: false,
|
|
141
142
|
sortByRecentDate: false,
|
|
142
|
-
brushActive: false
|
|
143
|
+
brushActive: false,
|
|
144
|
+
brushDefaultRecentDateCount: undefined
|
|
143
145
|
},
|
|
144
146
|
table: {
|
|
145
147
|
label: 'Data Table',
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
|
|
2
|
+
|
|
3
|
+
interface CalculateHorizontalBarCategoryLabelWidthProps {
|
|
4
|
+
yScale: any
|
|
5
|
+
chartWidth: number
|
|
6
|
+
formatDate: Function
|
|
7
|
+
parseDate: Function
|
|
8
|
+
tickLabelFont: string
|
|
9
|
+
xAxisType?: string
|
|
10
|
+
labelPlacement?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Helper function to calculate category label space for horizontal bar charts
|
|
15
|
+
*
|
|
16
|
+
* @param props Configuration object with chart properties
|
|
17
|
+
* @returns Calculated category label space, capped at 30% of parent width
|
|
18
|
+
*/
|
|
19
|
+
export const calculateHorizontalBarCategoryLabelWidth = ({
|
|
20
|
+
yScale,
|
|
21
|
+
chartWidth,
|
|
22
|
+
formatDate,
|
|
23
|
+
parseDate,
|
|
24
|
+
tickLabelFont,
|
|
25
|
+
xAxisType,
|
|
26
|
+
labelPlacement
|
|
27
|
+
}: CalculateHorizontalBarCategoryLabelWidthProps): number => {
|
|
28
|
+
if (labelPlacement !== 'On Date/Category Axis') return 0
|
|
29
|
+
|
|
30
|
+
const categoryValues = yScale.domain()
|
|
31
|
+
|
|
32
|
+
if (!categoryValues || categoryValues.length === 0) {
|
|
33
|
+
return chartWidth * 0.3
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const formattedLabels = categoryValues.map(value => {
|
|
37
|
+
if (xAxisType === 'date') {
|
|
38
|
+
try {
|
|
39
|
+
return formatDate(parseDate(value))
|
|
40
|
+
} catch (e) {
|
|
41
|
+
return String(value)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return String(value)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const labelWidths = formattedLabels.map(label => getTextWidth(label, tickLabelFont))
|
|
48
|
+
const maxLabelWidth = Math.max(...labelWidths)
|
|
49
|
+
|
|
50
|
+
// We need some extra padding or visx will wrap labels too early
|
|
51
|
+
const paddedWidth = maxLabelWidth + Math.ceil(maxLabelWidth * 0.15)
|
|
52
|
+
|
|
53
|
+
// Allocate at most 30% of chart width to category labels
|
|
54
|
+
const maxAllowedWidth = chartWidth * 0.3
|
|
55
|
+
|
|
56
|
+
return Math.min(paddedWidth, maxAllowedWidth)
|
|
57
|
+
}
|
package/src/helpers/getMinMax.ts
CHANGED
|
@@ -55,10 +55,15 @@ const getMinMax = ({
|
|
|
55
55
|
max = enteredMaxValue && isMaxValid ? Number(enteredMaxValue) : Number.MIN_VALUE
|
|
56
56
|
const { lower, upper } = config?.confidenceKeys || {}
|
|
57
57
|
|
|
58
|
+
// When brush is active, use tableData (full dataset) for min/max calculations
|
|
59
|
+
// so the y-axis shows the full range, but still use filtered data for rendering
|
|
60
|
+
const dataForMinMax = config.xAxis.brushActive && tableData && tableData.length > 0 ? tableData : data
|
|
61
|
+
|
|
58
62
|
if (lower && upper && config.visualizationType === 'Bar') {
|
|
59
63
|
const buffer = min < 0 ? 1.1 : 0
|
|
60
|
-
const maxValueWithCI = Math.max(...
|
|
61
|
-
const minValueWithCIPlusBuffer =
|
|
64
|
+
const maxValueWithCI = Math.max(...dataForMinMax.flatMap(d => [d[upper], d[lower]])) * paddingAddedToAxis
|
|
65
|
+
const minValueWithCIPlusBuffer =
|
|
66
|
+
Math.min(...dataForMinMax.flatMap(d => [d[upper], d[lower]])) * paddingAddedToAxis * buffer
|
|
62
67
|
max = max > maxValueWithCI ? max : maxValueWithCI
|
|
63
68
|
min = min < minValueWithCIPlusBuffer ? min : minValueWithCIPlusBuffer
|
|
64
69
|
}
|
|
@@ -79,7 +84,7 @@ const getMinMax = ({
|
|
|
79
84
|
})
|
|
80
85
|
|
|
81
86
|
// Using the columnNames or "keys" get the returned result
|
|
82
|
-
const result =
|
|
87
|
+
const result = dataForMinMax.map(obj => columnNames.map(key => obj[key]))
|
|
83
88
|
|
|
84
89
|
const highCIGroup = Math.max.apply(
|
|
85
90
|
null,
|
|
@@ -102,7 +107,7 @@ const getMinMax = ({
|
|
|
102
107
|
|
|
103
108
|
if (visualizationType === 'Combo') {
|
|
104
109
|
try {
|
|
105
|
-
if (!
|
|
110
|
+
if (!dataForMinMax) throw new Error('COVE: missing data while getting min/max for combo chart.')
|
|
106
111
|
// seperate the left and right axis items & get each sides series keys
|
|
107
112
|
let leftAxisSeriesItems = series.filter(s => s.axis === 'Left')
|
|
108
113
|
let rightAxisSeriesItems = series.filter(s => s.axis === 'Right')
|
|
@@ -128,8 +133,8 @@ const getMinMax = ({
|
|
|
128
133
|
})
|
|
129
134
|
return max
|
|
130
135
|
}
|
|
131
|
-
leftMax = findMaxFromSeriesKeys(
|
|
132
|
-
rightMax = findMaxFromSeriesKeys(
|
|
136
|
+
leftMax = findMaxFromSeriesKeys(dataForMinMax, leftAxisSeriesItems, leftMax, 'left')
|
|
137
|
+
rightMax = findMaxFromSeriesKeys(dataForMinMax, rightAxisSeriesItems, rightMax, 'right')
|
|
133
138
|
|
|
134
139
|
if (leftMax < Number(enteredMaxValue)) {
|
|
135
140
|
leftMax = Number(enteredMaxValue)
|
|
@@ -209,7 +214,7 @@ const getMinMax = ({
|
|
|
209
214
|
}
|
|
210
215
|
|
|
211
216
|
if (config.isLollipopChart && config.yAxis.displayNumbersOnBar) {
|
|
212
|
-
const dataKey =
|
|
217
|
+
const dataKey = dataForMinMax.map(item => item[config.series[0].dataKey])
|
|
213
218
|
const maxDataVal = Math.max(...dataKey).toString().length
|
|
214
219
|
|
|
215
220
|
switch (true) {
|
|
@@ -26,23 +26,3 @@ export function calcInitialHeight(
|
|
|
26
26
|
const height = Number(heights?.[renderedOrientation])
|
|
27
27
|
return isNaN(height) ? 0 : height
|
|
28
28
|
}
|
|
29
|
-
|
|
30
|
-
export function handleAutoPaddingRight(parentRef, xAxisLabelRefs, parentWidth): [boolean, number] {
|
|
31
|
-
const parentX = parentRef.current.getBoundingClientRect().x
|
|
32
|
-
const editorIsOpen = !!document.querySelector('.editor-panel:not(.hidden)')
|
|
33
|
-
const lastTickRect = xAxisLabelRefs.current?.[xAxisLabelRefs.current.length - 1]?.getBoundingClientRect()
|
|
34
|
-
const lastBottomTickEnd = lastTickRect ? lastTickRect.x + lastTickRect.width : 0
|
|
35
|
-
const editorWidth = editorIsOpen ? EDITOR_WIDTH : 0
|
|
36
|
-
const calculatedOverhang = lastBottomTickEnd - parentX - editorWidth - parentWidth
|
|
37
|
-
|
|
38
|
-
const paddingToAdd = clamp(calculatedOverhang, 0, 20)
|
|
39
|
-
const currentPadding = Number(parentRef.current.style.paddingRight.replace('px', ''))
|
|
40
|
-
const paddingDiff = Math.abs(currentPadding - paddingToAdd)
|
|
41
|
-
const DIFF_THRESHOLD = 5
|
|
42
|
-
|
|
43
|
-
const noChange = currentPadding === calculatedOverhang
|
|
44
|
-
const insufficientDiff = (paddingDiff < DIFF_THRESHOLD && calculatedOverhang > 0) || Math.abs(calculatedOverhang) < 1
|
|
45
|
-
const updatePadding = !noChange && !insufficientDiff
|
|
46
|
-
|
|
47
|
-
return [updatePadding, paddingToAdd]
|
|
48
|
-
}
|
|
@@ -181,7 +181,7 @@ export const getTileDisplayTitle = (mode, seriesKey, tileValue, tileKey, config)
|
|
|
181
181
|
* Get the full color palette from config with exactly the number of colors needed
|
|
182
182
|
* This creates a temporary colorScale with the right number of series to get the needed colors
|
|
183
183
|
*/
|
|
184
|
-
const getFullColorPalette = (config, numberOfTiles) => {
|
|
184
|
+
export const getFullColorPalette = (config, numberOfTiles) => {
|
|
185
185
|
// Create fake series keys for exactly the number of tiles needed
|
|
186
186
|
const tempSeriesKeys = Array(numberOfTiles)
|
|
187
187
|
.fill(null)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { useRef } from 'react'
|
|
2
1
|
import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
|
|
3
2
|
import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
|
|
3
|
+
import { hasTrackedHover, markHoverTracked } from '../utils/analyticsTracking'
|
|
4
4
|
|
|
5
5
|
type UseChartHoverAnalyticsParams = {
|
|
6
6
|
config: any
|
|
@@ -9,14 +9,16 @@ type UseChartHoverAnalyticsParams = {
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Hook to track analytics when user enters the chart area
|
|
12
|
-
* Fires once per
|
|
12
|
+
* Fires once per visualization, persists across component remounts
|
|
13
13
|
*/
|
|
14
14
|
export const useChartHoverAnalytics = ({ config, interactionLabel = '' }: UseChartHoverAnalyticsParams) => {
|
|
15
|
-
const hasTrackedRef = useRef(false)
|
|
16
|
-
|
|
17
15
|
const handleChartMouseEnter = () => {
|
|
18
|
-
// Only track if we have an interaction label
|
|
19
|
-
if (!interactionLabel
|
|
16
|
+
// Only track if we have an interaction label
|
|
17
|
+
if (!interactionLabel) return
|
|
18
|
+
|
|
19
|
+
// Use unique ID to track per visualization
|
|
20
|
+
const vizId = String(config.runtime.uniqueId)
|
|
21
|
+
if (hasTrackedHover(vizId)) return
|
|
20
22
|
|
|
21
23
|
// Publish the analytics event
|
|
22
24
|
publishAnalyticsEvent({
|
|
@@ -29,12 +31,11 @@ export const useChartHoverAnalytics = ({ config, interactionLabel = '' }: UseCha
|
|
|
29
31
|
})
|
|
30
32
|
|
|
31
33
|
// Mark as tracked so we don't fire again
|
|
32
|
-
|
|
34
|
+
markHoverTracked(vizId)
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
const handleChartMouseLeave = () => {
|
|
36
|
-
//
|
|
37
|
-
hasTrackedRef.current = false
|
|
38
|
+
// No-op: We no longer reset tracking on mouse leave
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
return {
|
package/src/hooks/useScales.ts
CHANGED
|
@@ -133,7 +133,17 @@ const useScales = (properties: useScaleProps) => {
|
|
|
133
133
|
// handle Vertical bars
|
|
134
134
|
if (!isHorizontal) {
|
|
135
135
|
xScale = composeScaleBand(xAxisDataMapped, [0, xMax], paddingRange)
|
|
136
|
-
|
|
136
|
+
// For categorical y-axis, use [0, max] domain and [yMax, 0] range to match CategoricalYAxis
|
|
137
|
+
// This ensures line data aligns with categorical bars and bars go to 100% height
|
|
138
|
+
if (config.yAxis.type === 'categorical') {
|
|
139
|
+
yScale = scaleLinear({
|
|
140
|
+
domain: [0, max],
|
|
141
|
+
range: [yMax, 0],
|
|
142
|
+
clamp: true
|
|
143
|
+
})
|
|
144
|
+
} else {
|
|
145
|
+
yScale = composeYScale({ min, max, yMax, config, leftMax })
|
|
146
|
+
}
|
|
137
147
|
seriesScale = composeScaleBand(seriesDomain, [0, xScale.bandwidth()], 0)
|
|
138
148
|
}
|
|
139
149
|
|
package/src/hooks/useTooltip.tsx
CHANGED
|
@@ -105,7 +105,7 @@ export const useTooltip = props => {
|
|
|
105
105
|
addColCommas: column.commas
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
const pieColumnData = additionalChartData?.
|
|
108
|
+
const pieColumnData = additionalChartData?.data?.[column.name]
|
|
109
109
|
const columnData =
|
|
110
110
|
config.tooltips.singleSeries && visualizationType === 'Line'
|
|
111
111
|
? resolvedScaleValues.filter(
|
|
@@ -145,7 +145,7 @@ export const useTooltip = props => {
|
|
|
145
145
|
tooltipItems.push(
|
|
146
146
|
[config.xAxis.dataKey, pieData[config.xAxis.dataKey]],
|
|
147
147
|
[
|
|
148
|
-
config.runtime.yAxis.dataKey,
|
|
148
|
+
config.runtime.yAxis.label || config.runtime.yAxis.dataKey,
|
|
149
149
|
showPiePercent ? pctString(actualPieValue) : formatNumber(pieData[config.runtime.yAxis.dataKey])
|
|
150
150
|
],
|
|
151
151
|
showPiePercent ? [] : ['Percent', pctString(pctOf360)]
|
|
@@ -260,11 +260,30 @@ export const useTooltip = props => {
|
|
|
260
260
|
const dataXPosition = eventSvgCoords.x + 10
|
|
261
261
|
const dataYPosition = eventSvgCoords.y
|
|
262
262
|
|
|
263
|
+
// Helper to strip <a> tags and only show link text
|
|
264
|
+
function stripLinkTags(str) {
|
|
265
|
+
if (typeof str !== 'string') return str
|
|
266
|
+
// Remove HTML <a> tags, keep inner text
|
|
267
|
+
return str.replace(/<a [^>]*>(.*?)<\/a>/gi, '$1')
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Strip link tags from all tooltip values
|
|
271
|
+
const cleanTooltipItems = [...tooltipItems, ...additionalTooltipItems].map(item => {
|
|
272
|
+
// item can be [key, value] or [key, value, axisPosition]
|
|
273
|
+
if (Array.isArray(item)) {
|
|
274
|
+
// Only strip from value (item[1])
|
|
275
|
+
const newItem = [...item]
|
|
276
|
+
newItem[1] = stripLinkTags(newItem[1])
|
|
277
|
+
return newItem
|
|
278
|
+
}
|
|
279
|
+
return item
|
|
280
|
+
})
|
|
281
|
+
|
|
263
282
|
const tooltipInformation = {
|
|
264
283
|
tooltipLeft: dataXPosition,
|
|
265
284
|
tooltipTop: dataYPosition,
|
|
266
285
|
tooltipData: {
|
|
267
|
-
data:
|
|
286
|
+
data: cleanTooltipItems,
|
|
268
287
|
dataXPosition,
|
|
269
288
|
dataYPosition
|
|
270
289
|
}
|
|
@@ -523,14 +542,16 @@ export const useTooltip = props => {
|
|
|
523
542
|
includedSeries.push(...dynamicDataCategories)
|
|
524
543
|
|
|
525
544
|
if (config.visualizationType === 'Forecasting') {
|
|
526
|
-
config.runtime.series
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
545
|
+
config.runtime.series
|
|
546
|
+
.filter(s => s.type === 'Forecasting' && s.confidenceIntervals)
|
|
547
|
+
.forEach(s => {
|
|
548
|
+
s.confidenceIntervals.forEach(c => {
|
|
549
|
+
if (c.showInTooltip) {
|
|
550
|
+
includedSeries.push(c.high)
|
|
551
|
+
includedSeries.push(c.low)
|
|
552
|
+
}
|
|
553
|
+
})
|
|
532
554
|
})
|
|
533
|
-
})
|
|
534
555
|
}
|
|
535
556
|
|
|
536
557
|
const colNames = Object.values(config.columns).map(column => column.name)
|
package/src/scss/DataTable.scss
CHANGED
package/src/scss/main.scss
CHANGED
|
@@ -50,9 +50,6 @@
|
|
|
50
50
|
.subtext,
|
|
51
51
|
.subtext--responsive-ticks,
|
|
52
52
|
.section-subtext {
|
|
53
|
-
&--brush-active {
|
|
54
|
-
margin-top: 3rem !important;
|
|
55
|
-
}
|
|
56
53
|
}
|
|
57
54
|
|
|
58
55
|
.type-pie {
|
|
@@ -325,6 +322,23 @@
|
|
|
325
322
|
margin-bottom: 2.5em;
|
|
326
323
|
}
|
|
327
324
|
|
|
325
|
+
// Brush touch support
|
|
326
|
+
.brush-overlay {
|
|
327
|
+
touch-action: none;
|
|
328
|
+
-webkit-touch-callout: none;
|
|
329
|
+
-webkit-user-select: none;
|
|
330
|
+
user-select: none;
|
|
331
|
+
|
|
332
|
+
.visx-brush,
|
|
333
|
+
.visx-brush-overlay,
|
|
334
|
+
.visx-brush-selection,
|
|
335
|
+
.visx-brush-handle-left,
|
|
336
|
+
.visx-brush-handle-right {
|
|
337
|
+
touch-action: none;
|
|
338
|
+
cursor: pointer;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
328
342
|
svg.dragging-annotation * {
|
|
329
343
|
user-select: none;
|
|
330
344
|
}
|