@cdc/chart 4.25.11 → 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 +51401 -50814
- 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/feature/pie/planet-pie-example-config.json +48 -2
- package/examples/line-chart-states.json +1085 -0
- package/examples/private/123.json +694 -0
- package/examples/private/DEV-12100.json +1303 -0
- package/examples/private/anchor-issue.json +4094 -0
- package/examples/private/backwards-slider.json +10430 -0
- package/examples/private/cat-y.json +1235 -0
- package/examples/private/data-points.json +228 -0
- package/examples/private/georgia.csv +160 -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/timeline-data.json +1 -0
- package/examples/private/timeline.json +389 -0
- package/examples/private/warming-stripe-test.json +2578 -0
- package/examples/private/warming-stripes.json +4763 -0
- package/examples/radar-chart-simple.json +133 -0
- package/examples/radar-chart.json +148 -0
- package/examples/tech-adoption-with-links.json +560 -0
- package/index.html +1 -36
- package/package.json +59 -60
- package/src/CdcChartComponent.tsx +206 -89
- 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 +161 -0
- package/src/_stories/Chart.Regions.DateScale.stories.tsx +216 -0
- package/src/_stories/Chart.Regions.DateTimeScale.stories.tsx +312 -0
- package/src/_stories/Chart.ScatterPlot.stories.tsx +4 -0
- package/src/_stories/Chart.SmallMultiples.stories.tsx +16 -0
- package/src/_stories/Chart.stories.tsx +45 -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/ChartBar.Editor.stories.tsx +11 -6
- package/src/_stories/ChartBrush.Editor.stories.tsx +295 -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 +57 -0
- package/src/_stories/ChartEditor.Editor.stories.tsx +3 -5
- 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 +34 -0
- package/src/_stories/_mock/brush_continuous.json +86 -0
- package/src/_stories/_mock/brush_date_large.json +176 -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/_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/AreaChart/components/AreaChart.Stacked.jsx +1 -2
- package/src/components/Axis/BottomAxis.tsx +270 -0
- package/src/components/Axis/Categorical.Axis.tsx +6 -7
- 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.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/BarChart.tsx +7 -1
- package/src/components/BarChart/components/context.tsx +1 -0
- package/src/components/BarChart/helpers/useBarChart.ts +14 -2
- package/src/components/Brush/BrushSelector.tsx +1390 -0
- package/src/components/Brush/MiniChartPreview.tsx +400 -0
- package/src/components/DeviationBar.jsx +9 -7
- package/src/components/EditorPanel/EditorPanel.tsx +2734 -2595
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +60 -22
- package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +56 -34
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +137 -30
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +2 -0
- 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.SmallMultiples.tsx +30 -25
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +42 -28
- package/src/components/EditorPanel/components/Panels/index.tsx +2 -0
- package/src/components/EditorPanel/useEditorPermissions.ts +81 -39
- 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 +4 -3
- package/src/components/Legend/LegendValueRange.tsx +77 -0
- package/src/components/Legend/helpers/createFormatLabels.tsx +164 -2
- package/src/components/Legend/helpers/generateValueRanges.ts +92 -0
- package/src/components/Legend/helpers/index.ts +10 -6
- 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 +338 -1082
- package/src/components/PairedBarChart.jsx +20 -3
- 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/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 +230 -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 +17 -2
- package/src/helpers/calculateHorizontalBarCategoryLabelWidth.ts +57 -0
- package/src/helpers/getExcludedData.ts +4 -0
- package/src/helpers/getMinMax.ts +12 -7
- package/src/helpers/handleChartAriaLabels.ts +19 -19
- package/src/helpers/handleLineType.ts +22 -18
- package/src/helpers/sizeHelpers.ts +0 -20
- package/src/helpers/smallMultiplesHelpers.ts +1 -1
- package/src/hooks/useChartHoverAnalytics.tsx +10 -9
- package/src/hooks/useProgrammaticTooltip.ts +23 -2
- package/src/hooks/useScales.ts +18 -1
- package/src/hooks/useTooltip.tsx +34 -10
- package/src/scss/DataTable.scss +0 -4
- package/src/scss/main.scss +22 -3
- package/src/selectors/README.md +68 -0
- package/src/store/chart.reducer.ts +2 -0
- package/src/test/CdcChart.test.jsx +1 -1
- package/src/types/ChartConfig.ts +21 -0
- package/src/types/ChartContext.ts +1 -0
- package/src/types/Horizon.ts +64 -0
- package/src/types/Label.ts +1 -0
- package/src/utils/analyticsTracking.ts +19 -0
- package/LICENSE +0 -201
- package/src/components/Annotations/components/helpers/index.tsx +0 -46
- 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,230 @@
|
|
|
1
|
+
import { useContext, useState, useCallback, useEffect } 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 { getFallbackColorPalette, migratePaletteWithMap } from '@cdc/core/helpers/palettes/utils'
|
|
10
|
+
import { paletteMigrationMap } from '@cdc/core/helpers/palettes/migratePaletteName'
|
|
11
|
+
import { hasTrackedHover, markHoverTracked } from '../../utils/analyticsTracking'
|
|
12
|
+
|
|
13
|
+
type WarmingStripesProps = {
|
|
14
|
+
xScale: any
|
|
15
|
+
yScale: any
|
|
16
|
+
xMax: number
|
|
17
|
+
yMax: number
|
|
18
|
+
synchronizedXValue?: any
|
|
19
|
+
showTooltip: (args: any) => void
|
|
20
|
+
handleTooltipMouseOff: () => void
|
|
21
|
+
}
|
|
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)
|
|
38
|
+
|
|
39
|
+
const [currentHover, setCurrentHover] = useState<number | null>(null)
|
|
40
|
+
|
|
41
|
+
// Get the data key for the temperature/anomaly values
|
|
42
|
+
// Use the first series key as the value column
|
|
43
|
+
const valueKey = config.runtime.seriesKeys?.[0]
|
|
44
|
+
const xAxisDataKey = config.runtime.originalXAxis?.dataKey || config.xAxis?.dataKey
|
|
45
|
+
|
|
46
|
+
// Determine max stripes based on viewport
|
|
47
|
+
const isMobile = ['xxs', 'xs', 'sm', 'md'].includes(currentViewport)
|
|
48
|
+
const maxStripes = isMobile ? 60 : 200
|
|
49
|
+
|
|
50
|
+
// Sample data if we have more than the max allowed stripes
|
|
51
|
+
let displayData = data || []
|
|
52
|
+
if (displayData.length > maxStripes) {
|
|
53
|
+
const step = displayData.length / maxStripes
|
|
54
|
+
displayData = []
|
|
55
|
+
for (let i = 0; i < maxStripes; i++) {
|
|
56
|
+
const index = Math.floor(i * step)
|
|
57
|
+
displayData.push(data[index])
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Calculate the min and max values for the color scale
|
|
62
|
+
const values = displayData.map(d => Number(d[valueKey])).filter(v => !isNaN(v))
|
|
63
|
+
const minValue = Math.min(...values)
|
|
64
|
+
const maxValue = Math.max(...values)
|
|
65
|
+
|
|
66
|
+
// Get the color palette from config
|
|
67
|
+
const colorPalettes = filterChartColorPalettes(config)
|
|
68
|
+
const configPalette = config.general?.palette?.name
|
|
69
|
+
const migratedPaletteName = configPalette ? configPalette : getFallbackColorPalette(config)
|
|
70
|
+
|
|
71
|
+
// Check if the palette name ends with 'reverse' and get the base palette
|
|
72
|
+
const isReversedPalette = migratedPaletteName?.endsWith('reverse')
|
|
73
|
+
const basePaletteName = isReversedPalette ? migratedPaletteName.slice(0, -7) : migratedPaletteName
|
|
74
|
+
|
|
75
|
+
let palette =
|
|
76
|
+
colorPalettes[migratePaletteWithMap(basePaletteName, paletteMigrationMap, false)] ||
|
|
77
|
+
colorPalettes[basePaletteName] ||
|
|
78
|
+
colorPalettes[configPalette]
|
|
79
|
+
|
|
80
|
+
// Fallback to a default diverging palette if none found
|
|
81
|
+
if (!palette || palette.length < 2) {
|
|
82
|
+
console.warn(`Palette "${configPalette}" not found or invalid, falling back to default`)
|
|
83
|
+
// Use a default blue to red palette
|
|
84
|
+
palette = [
|
|
85
|
+
'#053061',
|
|
86
|
+
'#2166ac',
|
|
87
|
+
'#4393c3',
|
|
88
|
+
'#92c5de',
|
|
89
|
+
'#d1e5f0',
|
|
90
|
+
'#f7f7f7',
|
|
91
|
+
'#fddbc7',
|
|
92
|
+
'#f4a582',
|
|
93
|
+
'#d6604d',
|
|
94
|
+
'#b2182b',
|
|
95
|
+
'#67001f'
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Create a sequential color scale using the palette colors
|
|
100
|
+
// Apply reverse if configured (either via isReversed flag or 'reverse' suffix in name)
|
|
101
|
+
const shouldReverse = config.general?.palette?.isReversed || isReversedPalette
|
|
102
|
+
const finalPalette = shouldReverse ? [...palette].reverse() : palette
|
|
103
|
+
const colorScale = scaleSequential(interpolateRgbBasis(finalPalette)).domain([minValue, maxValue])
|
|
104
|
+
|
|
105
|
+
// Calculate stripe width based on available space and display data
|
|
106
|
+
const stripeWidth = xMax / displayData.length
|
|
107
|
+
|
|
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])
|
|
153
|
+
|
|
154
|
+
if (!valueKey || !xAxisDataKey || !data || data.length === 0) {
|
|
155
|
+
return null
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<Group className='warming-stripes' left={config.yAxis.size}>
|
|
160
|
+
{displayData.map((item, index) => {
|
|
161
|
+
const value = Number(item[valueKey])
|
|
162
|
+
if (isNaN(value)) return null
|
|
163
|
+
|
|
164
|
+
const xPosition = index * stripeWidth
|
|
165
|
+
const fillColor = colorScale(value) as unknown as string
|
|
166
|
+
const isHovered = currentHover === index
|
|
167
|
+
const isMuted = currentHover !== null && !isHovered
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<rect
|
|
171
|
+
key={`stripe-${index}`}
|
|
172
|
+
x={xPosition}
|
|
173
|
+
y={0}
|
|
174
|
+
width={stripeWidth}
|
|
175
|
+
height={yMax}
|
|
176
|
+
fill={fillColor}
|
|
177
|
+
fillOpacity={isMuted ? 0.5 : 1}
|
|
178
|
+
stroke='none'
|
|
179
|
+
tabIndex={-1}
|
|
180
|
+
style={{ cursor: 'pointer', transition: 'fill-opacity 0.2s ease' }}
|
|
181
|
+
onMouseEnter={e => {
|
|
182
|
+
if (currentHover !== index) {
|
|
183
|
+
const vizId = String(config.runtime.uniqueId)
|
|
184
|
+
if (!hasTrackedHover(vizId)) {
|
|
185
|
+
publishAnalyticsEvent({
|
|
186
|
+
vizType: config?.type,
|
|
187
|
+
vizSubType: getVizSubType(config),
|
|
188
|
+
eventType: 'chart_hover',
|
|
189
|
+
eventAction: 'hover',
|
|
190
|
+
eventLabel: interactionLabel || 'unknown',
|
|
191
|
+
vizTitle: getVizTitle(config),
|
|
192
|
+
series: valueKey
|
|
193
|
+
})
|
|
194
|
+
markHoverTracked(vizId)
|
|
195
|
+
}
|
|
196
|
+
setCurrentHover(index)
|
|
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
|
+
}
|
|
222
|
+
}}
|
|
223
|
+
/>
|
|
224
|
+
)
|
|
225
|
+
})}
|
|
226
|
+
</Group>
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
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',
|
|
@@ -44,7 +45,8 @@ const createInitialState = () => {
|
|
|
44
45
|
showSuppressedSymbol: true,
|
|
45
46
|
showZeroValueData: true,
|
|
46
47
|
hideNullValue: true,
|
|
47
|
-
palette: paletteDefaults
|
|
48
|
+
palette: paletteDefaults,
|
|
49
|
+
useIntelligentLineChartLabels: false
|
|
48
50
|
},
|
|
49
51
|
padding: {
|
|
50
52
|
left: 5,
|
|
@@ -139,7 +141,8 @@ const createInitialState = () => {
|
|
|
139
141
|
padding: 5,
|
|
140
142
|
showYearsOnce: false,
|
|
141
143
|
sortByRecentDate: false,
|
|
142
|
-
brushActive: false
|
|
144
|
+
brushActive: false,
|
|
145
|
+
brushDefaultRecentDateCount: undefined
|
|
143
146
|
},
|
|
144
147
|
table: {
|
|
145
148
|
label: 'Data Table',
|
|
@@ -296,6 +299,18 @@ const createInitialState = () => {
|
|
|
296
299
|
area: {
|
|
297
300
|
isStacked: false
|
|
298
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
|
+
},
|
|
299
314
|
sankey: {
|
|
300
315
|
title: {
|
|
301
316
|
defaultColor: 'black'
|
|
@@ -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
|
+
}
|
|
@@ -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) {
|
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) {
|
|
@@ -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
|
+
}
|
|
@@ -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)
|