@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import parse from 'html-react-parser'
|
|
2
|
-
import React from 'react'
|
|
2
|
+
import React, { useMemo } from 'react'
|
|
3
3
|
import { LegendOrdinal, LegendItem, LegendLabel } from '@visx/legend'
|
|
4
4
|
import { PatternLines, PatternCircles, PatternWaves } from '@visx/pattern'
|
|
5
5
|
import LegendShape from '@cdc/core/components/LegendShape'
|
|
@@ -17,6 +17,8 @@ import { DimensionsType } from '@cdc/core/types/Dimensions'
|
|
|
17
17
|
import { isLegendWrapViewport } from '@cdc/core/helpers/viewports'
|
|
18
18
|
import LegendLineShape from './LegendLine.Shape'
|
|
19
19
|
import LegendGroup from './LegendGroup'
|
|
20
|
+
import LegendValueRange from './LegendValueRange'
|
|
21
|
+
import { getHorizonLayerColors, getHorizonMaxValue } from '../../components/HorizonChart/helpers/getHorizonLayerColors'
|
|
20
22
|
import { getSeriesWithData } from '../../helpers/dataHelpers'
|
|
21
23
|
import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
|
|
22
24
|
import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
|
|
@@ -28,6 +30,7 @@ interface LegendProps {
|
|
|
28
30
|
config: ChartConfig
|
|
29
31
|
currentViewport: ViewportSize
|
|
30
32
|
formatLabels: (labels: Label[]) => Label[]
|
|
33
|
+
formatNumber?: (value: number, axis?: string) => string
|
|
31
34
|
highlight: Function
|
|
32
35
|
handleShowAll: Function
|
|
33
36
|
ref: React.Ref<() => void>
|
|
@@ -48,6 +51,7 @@ const Legend: React.FC<LegendProps> = forwardRef(
|
|
|
48
51
|
handleShowAll,
|
|
49
52
|
currentViewport,
|
|
50
53
|
formatLabels,
|
|
54
|
+
formatNumber,
|
|
51
55
|
skipId = 'legend',
|
|
52
56
|
dimensions,
|
|
53
57
|
transformedData: data,
|
|
@@ -60,7 +64,10 @@ const Legend: React.FC<LegendProps> = forwardRef(
|
|
|
60
64
|
const { series } = runtime
|
|
61
65
|
|
|
62
66
|
const seriesWithData = getSeriesWithData(config)
|
|
63
|
-
|
|
67
|
+
// For Radar charts, seriesWithData contains dimension keys but legend shows entity names
|
|
68
|
+
// so we skip the series filter for radar charts
|
|
69
|
+
const isRadarChart = config.visualizationType === 'Radar'
|
|
70
|
+
const dontFilterLegendItems = !series.length || legend.unified || !seriesWithData.length || isRadarChart
|
|
64
71
|
|
|
65
72
|
const isLegendBottom =
|
|
66
73
|
legend?.position === 'bottom' ||
|
|
@@ -74,6 +81,29 @@ const Legend: React.FC<LegendProps> = forwardRef(
|
|
|
74
81
|
const { HighLightedBarUtils } = useHighlightedBars(config)
|
|
75
82
|
let highLightedLegendItems = HighLightedBarUtils.findDuplicates(config.highlightedBarValues)
|
|
76
83
|
|
|
84
|
+
const horizonLegendData = useMemo(() => {
|
|
85
|
+
if (config.visualizationType !== 'Horizon Chart') {
|
|
86
|
+
return null
|
|
87
|
+
}
|
|
88
|
+
const numLayers = config.horizon?.numLayers || 4
|
|
89
|
+
const runtimeSeriesKeys = config.runtime?.seriesKeys
|
|
90
|
+
const seriesKeys =
|
|
91
|
+
(Array.isArray(runtimeSeriesKeys) && runtimeSeriesKeys.length > 0
|
|
92
|
+
? runtimeSeriesKeys
|
|
93
|
+
: config.series?.map(s => s.dataKey)) || []
|
|
94
|
+
const maxValue = getHorizonMaxValue(data, seriesKeys)
|
|
95
|
+
const layerColors = getHorizonLayerColors(config, numLayers)
|
|
96
|
+
|
|
97
|
+
return { numLayers, maxValue, layerColors }
|
|
98
|
+
}, [
|
|
99
|
+
config.visualizationType,
|
|
100
|
+
config.horizon?.numLayers,
|
|
101
|
+
config.runtime?.seriesKeys,
|
|
102
|
+
config.series,
|
|
103
|
+
config.general?.palette?.name,
|
|
104
|
+
data
|
|
105
|
+
])
|
|
106
|
+
|
|
77
107
|
if (!legend) return null
|
|
78
108
|
return (
|
|
79
109
|
<aside
|
|
@@ -99,6 +129,20 @@ const Legend: React.FC<LegendProps> = forwardRef(
|
|
|
99
129
|
/>
|
|
100
130
|
<LegendGroup formatLabels={formatLabels} />
|
|
101
131
|
|
|
132
|
+
{/* Value Range Legend for Horizon Chart (and future chart types) */}
|
|
133
|
+
{horizonLegendData && (
|
|
134
|
+
<LegendValueRange
|
|
135
|
+
maxValue={horizonLegendData.maxValue}
|
|
136
|
+
numRanges={horizonLegendData.numLayers}
|
|
137
|
+
colors={horizonLegendData.layerColors}
|
|
138
|
+
formatNumber={formatNumber}
|
|
139
|
+
innerClasses={innerClasses}
|
|
140
|
+
shape={config.legend.style === 'boxes' ? 'square' : 'circle'}
|
|
141
|
+
onClick={undefined}
|
|
142
|
+
reverseLabelOrder={config.legend.reverseLabelOrder}
|
|
143
|
+
/>
|
|
144
|
+
)}
|
|
145
|
+
|
|
102
146
|
<LegendOrdinal scale={colorScale} itemDirection='row' labelMargin='0 20px 0 0' shapeMargin='0 10px 0'>
|
|
103
147
|
{labels => {
|
|
104
148
|
return (
|
|
@@ -130,8 +174,12 @@ const Legend: React.FC<LegendProps> = forwardRef(
|
|
|
130
174
|
} else className.push('highlighted')
|
|
131
175
|
}
|
|
132
176
|
|
|
133
|
-
if (
|
|
134
|
-
|
|
177
|
+
if (
|
|
178
|
+
config.legend.style === 'gradient' ||
|
|
179
|
+
config.legend.groupBy ||
|
|
180
|
+
config.visualizationType === 'Horizon Chart'
|
|
181
|
+
) {
|
|
182
|
+
return null
|
|
135
183
|
}
|
|
136
184
|
|
|
137
185
|
return (
|
|
@@ -3,7 +3,6 @@ import ConfigContext from '../../ConfigContext'
|
|
|
3
3
|
import LegendComponent from './Legend.Component'
|
|
4
4
|
import { createFormatLabels } from './helpers/createFormatLabels'
|
|
5
5
|
|
|
6
|
-
/* eslint-disable jsx-a11y/no-noninteractive-tabindex, jsx-a11y/no-static-element-interactions */
|
|
7
6
|
const Legend = forwardRef((props, ref) => {
|
|
8
7
|
// prettier-ignore
|
|
9
8
|
const {
|
|
@@ -18,13 +17,14 @@ const Legend = forwardRef((props, ref) => {
|
|
|
18
17
|
currentViewport,
|
|
19
18
|
dimensions,
|
|
20
19
|
getTextWidth,
|
|
21
|
-
transformedData
|
|
20
|
+
transformedData,
|
|
21
|
+
formatNumber
|
|
22
22
|
} = useContext(ConfigContext)
|
|
23
23
|
if (!config.legend) return null
|
|
24
24
|
// create fn to reverse labels while legend is Bottom. Legend-right , legend-left works by default
|
|
25
25
|
const { interactionLabel } = props
|
|
26
26
|
|
|
27
|
-
const createLegendLabels = createFormatLabels(config, tableData, data, colorScale)
|
|
27
|
+
const createLegendLabels = createFormatLabels(config, tableData, data, colorScale, formatNumber)
|
|
28
28
|
|
|
29
29
|
return (
|
|
30
30
|
<Fragment>
|
|
@@ -41,6 +41,7 @@ const Legend = forwardRef((props, ref) => {
|
|
|
41
41
|
handleShowAll={handleShowAll}
|
|
42
42
|
currentViewport={currentViewport}
|
|
43
43
|
formatLabels={createLegendLabels}
|
|
44
|
+
formatNumber={formatNumber}
|
|
44
45
|
interactionLabel={interactionLabel}
|
|
45
46
|
/>
|
|
46
47
|
</Fragment>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { LegendItem, LegendLabel } from '@visx/legend'
|
|
3
|
+
import LegendShape from '@cdc/core/components/LegendShape'
|
|
4
|
+
import { generateValueRanges, GenerateValueRangesOptions, ValueRange } from './helpers/generateValueRanges'
|
|
5
|
+
|
|
6
|
+
export type LegendValueRangeProps = GenerateValueRangesOptions & {
|
|
7
|
+
colors: string[]
|
|
8
|
+
shape?: 'circle' | 'square'
|
|
9
|
+
onClick?: (index: number, range: ValueRange) => void
|
|
10
|
+
innerClasses?: string[]
|
|
11
|
+
reverseLabelOrder?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* LegendValueRange - Generic component for displaying value range legends
|
|
16
|
+
*
|
|
17
|
+
* Used by Horizon Charts to show layer intensity ranges (e.g., 1-100, 101-200).
|
|
18
|
+
* Can be reused by other chart types that need binned value legends.
|
|
19
|
+
*/
|
|
20
|
+
const LegendValueRange: React.FC<LegendValueRangeProps> = ({
|
|
21
|
+
minValue = 0,
|
|
22
|
+
maxValue,
|
|
23
|
+
numRanges,
|
|
24
|
+
distribution = 'equal',
|
|
25
|
+
formatNumber,
|
|
26
|
+
colors,
|
|
27
|
+
shape = 'square',
|
|
28
|
+
onClick,
|
|
29
|
+
innerClasses = ['legend-container__inner'],
|
|
30
|
+
reverseLabelOrder = false
|
|
31
|
+
}) => {
|
|
32
|
+
const ranges = generateValueRanges({ minValue, maxValue, numRanges, distribution, formatNumber })
|
|
33
|
+
|
|
34
|
+
if (ranges.length === 0) return null
|
|
35
|
+
|
|
36
|
+
// Reverse both ranges and colors when reverseLabelOrder is true
|
|
37
|
+
const displayRanges = reverseLabelOrder ? [...ranges].reverse() : ranges
|
|
38
|
+
const displayColors = reverseLabelOrder ? [...colors].reverse() : colors
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className={innerClasses.join(' ')}>
|
|
42
|
+
{displayRanges.map((range, i) => {
|
|
43
|
+
const color = displayColors[i % displayColors.length]
|
|
44
|
+
const isClickable = typeof onClick === 'function'
|
|
45
|
+
const className = ['legend-item', `legend-text--range-${i}`, !isClickable && 'not-clickable'].filter(Boolean)
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<LegendItem
|
|
49
|
+
className={className.join(' ')}
|
|
50
|
+
tabIndex={isClickable ? 0 : -1}
|
|
51
|
+
key={`legend-range-${i}`}
|
|
52
|
+
onKeyDown={e => {
|
|
53
|
+
if (isClickable && e.key === 'Enter') {
|
|
54
|
+
e.preventDefault()
|
|
55
|
+
onClick(i, range)
|
|
56
|
+
}
|
|
57
|
+
}}
|
|
58
|
+
onClick={e => {
|
|
59
|
+
if (isClickable) {
|
|
60
|
+
e.preventDefault()
|
|
61
|
+
onClick(i, range)
|
|
62
|
+
}
|
|
63
|
+
}}
|
|
64
|
+
role={isClickable ? 'button' : undefined}
|
|
65
|
+
>
|
|
66
|
+
<LegendShape shape={shape} fill={color} />
|
|
67
|
+
<LegendLabel align='left' className='m-0'>
|
|
68
|
+
{range.label}
|
|
69
|
+
</LegendLabel>
|
|
70
|
+
</LegendItem>
|
|
71
|
+
)
|
|
72
|
+
})}
|
|
73
|
+
</div>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export default LegendValueRange
|
|
@@ -5,23 +5,172 @@ import {
|
|
|
5
5
|
twoColorPalette
|
|
6
6
|
} from '@cdc/core/data/colorPalettes'
|
|
7
7
|
import { getCurrentPaletteName, getFallbackColorPalette, migratePaletteWithMap } from '@cdc/core/helpers/palettes/utils'
|
|
8
|
-
import { chartPaletteMigrationMap } from '@cdc/core/helpers/palettes/migratePaletteName'
|
|
8
|
+
import { chartPaletteMigrationMap, paletteMigrationMap } from '@cdc/core/helpers/palettes/migratePaletteName'
|
|
9
9
|
import { getPaletteAccessor } from '@cdc/core/helpers/getPaletteAccessor'
|
|
10
10
|
import { getColorPaletteVersion } from '@cdc/core/helpers/getColorPaletteVersion'
|
|
11
11
|
import { isV1Palette } from '@cdc/core/helpers/palettes/utils'
|
|
12
12
|
import { v2ColorDistribution } from '@cdc/core/helpers/palettes/colorDistributions'
|
|
13
13
|
import { updatePaletteNames } from '@cdc/core/helpers/updatePaletteNames'
|
|
14
14
|
import { buildForecastPaletteMappings } from '../../../helpers/buildForecastPaletteMappings'
|
|
15
|
+
import { getFullColorPalette } from '../../../helpers/smallMultiplesHelpers'
|
|
15
16
|
import { FaStar } from 'react-icons/fa'
|
|
16
17
|
import { Label } from '../../../types/Label'
|
|
17
18
|
import { ColorScale, TransformedData } from '../../../types/ChartContext'
|
|
18
19
|
import { ChartConfig } from '../../../types/ChartConfig'
|
|
19
20
|
import _ from 'lodash'
|
|
21
|
+
import { scaleSequential } from 'd3-scale'
|
|
22
|
+
import { interpolateRgbBasis } from 'd3-interpolate'
|
|
23
|
+
import { filterChartColorPalettes } from '@cdc/core/helpers/filterColorPalettes'
|
|
20
24
|
|
|
21
25
|
export const createFormatLabels =
|
|
22
|
-
(
|
|
26
|
+
(
|
|
27
|
+
config: ChartConfig,
|
|
28
|
+
tableData: Object[],
|
|
29
|
+
data: TransformedData[],
|
|
30
|
+
colorScale: ColorScale,
|
|
31
|
+
formatNumber: (value: any, axis: string) => string
|
|
32
|
+
) =>
|
|
23
33
|
(defaultLabels: Label[]): Label[] => {
|
|
24
34
|
const { visualizationType, visualizationSubType, series, runtime, legend } = config
|
|
35
|
+
|
|
36
|
+
// Handle small multiples legend adjustments
|
|
37
|
+
// by-series + same: all tiles use same color, legend should show one color
|
|
38
|
+
if (config.smallMultiples?.mode === 'by-series' && config.smallMultiples?.colorMode === 'same') {
|
|
39
|
+
const baseColor = colorScale?.range()?.[0]
|
|
40
|
+
return defaultLabels.map((label, i) => ({
|
|
41
|
+
...label,
|
|
42
|
+
value: baseColor
|
|
43
|
+
}))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// by-column + different: each tile gets different color, legend should show tile values with their colors
|
|
47
|
+
if (config.smallMultiples?.mode === 'by-column' && config.smallMultiples?.colorMode === 'different') {
|
|
48
|
+
const tileColumn = config.smallMultiples.tileColumn
|
|
49
|
+
const tileValues = Array.from(new Set(data.map(d => d[tileColumn])))
|
|
50
|
+
.filter(Boolean)
|
|
51
|
+
.sort()
|
|
52
|
+
const tilePalette = getFullColorPalette(config, tileValues.length)
|
|
53
|
+
|
|
54
|
+
return tileValues.map((value, index) => ({
|
|
55
|
+
datum: value,
|
|
56
|
+
index,
|
|
57
|
+
text: config.smallMultiples.tileTitles?.[value] || String(value),
|
|
58
|
+
value: tilePalette[index]
|
|
59
|
+
}))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Handle Warming Stripes legend
|
|
63
|
+
if (visualizationType === 'Warming Stripes') {
|
|
64
|
+
const valueKey = runtime.seriesKeys?.[0]
|
|
65
|
+
if (!valueKey || !data || data.length === 0) {
|
|
66
|
+
return []
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Calculate min and max values
|
|
70
|
+
const values = data.map(d => Number(d[valueKey])).filter(v => !isNaN(v))
|
|
71
|
+
const minValue = Math.min(...values)
|
|
72
|
+
const maxValue = Math.max(...values)
|
|
73
|
+
|
|
74
|
+
// Get the color palette from config (same logic as WarmingStripes component)
|
|
75
|
+
const colorPalettesFiltered = filterChartColorPalettes(config)
|
|
76
|
+
const configPalette = config.general?.palette?.name
|
|
77
|
+
const migratedPaletteName = configPalette ? configPalette : getFallbackColorPalette(config)
|
|
78
|
+
|
|
79
|
+
const isReversedPalette = migratedPaletteName?.endsWith('reverse')
|
|
80
|
+
const basePaletteName = isReversedPalette ? migratedPaletteName.slice(0, -7) : migratedPaletteName
|
|
81
|
+
|
|
82
|
+
let palette =
|
|
83
|
+
colorPalettesFiltered[migratePaletteWithMap(basePaletteName, paletteMigrationMap, false)] ||
|
|
84
|
+
colorPalettesFiltered[basePaletteName] ||
|
|
85
|
+
colorPalettesFiltered[configPalette]
|
|
86
|
+
|
|
87
|
+
if (!palette || palette.length < 2) {
|
|
88
|
+
palette = [
|
|
89
|
+
'#053061',
|
|
90
|
+
'#2166ac',
|
|
91
|
+
'#4393c3',
|
|
92
|
+
'#92c5de',
|
|
93
|
+
'#d1e5f0',
|
|
94
|
+
'#f7f7f7',
|
|
95
|
+
'#fddbc7',
|
|
96
|
+
'#f4a582',
|
|
97
|
+
'#d6604d',
|
|
98
|
+
'#b2182b',
|
|
99
|
+
'#67001f'
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const shouldReverse = config.general?.palette?.isReversed || isReversedPalette
|
|
104
|
+
const finalPalette = shouldReverse ? [...palette].reverse() : palette
|
|
105
|
+
const warmingColorScale = scaleSequential(interpolateRgbBasis(finalPalette)).domain([minValue, maxValue])
|
|
106
|
+
|
|
107
|
+
// For gradient style, create smooth gradient with min/max labels only
|
|
108
|
+
if (legend?.style === 'gradient') {
|
|
109
|
+
// Create many color stops for smooth gradient (these are used for the gradient fill)
|
|
110
|
+
const numColorStops = 20
|
|
111
|
+
const colorStops = []
|
|
112
|
+
for (let i = 0; i < numColorStops; i++) {
|
|
113
|
+
const t = i / (numColorStops - 1)
|
|
114
|
+
const value = minValue + t * (maxValue - minValue)
|
|
115
|
+
colorStops.push(warmingColorScale(value))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Create multiple stops for proper spacing, but only show labels at first and last
|
|
119
|
+
// This ensures the first label appears at the left edge and last at the right edge
|
|
120
|
+
const numPositions = 5 // Number of tick positions for spacing
|
|
121
|
+
const labels = []
|
|
122
|
+
for (let i = 0; i < numPositions; i++) {
|
|
123
|
+
const t = i / (numPositions - 1)
|
|
124
|
+
const value = minValue + t * (maxValue - minValue)
|
|
125
|
+
const isFirstOrLast = i === 0 || i === numPositions - 1
|
|
126
|
+
|
|
127
|
+
labels.push({
|
|
128
|
+
datum: String(value),
|
|
129
|
+
index: i,
|
|
130
|
+
text: isFirstOrLast ? formatNumber(value, 'left') : '',
|
|
131
|
+
value: colorStops[Math.floor(t * (numColorStops - 1))],
|
|
132
|
+
colors: colorStops
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return labels
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// For interval style, create ranges
|
|
140
|
+
const numIntervals = legend?.warmingStripesIntervals || 5
|
|
141
|
+
const range = maxValue - minValue
|
|
142
|
+
const intervalSize = range / numIntervals
|
|
143
|
+
|
|
144
|
+
const intervalLabels = []
|
|
145
|
+
|
|
146
|
+
for (let i = 0; i < numIntervals; i++) {
|
|
147
|
+
// Calculate interval boundaries
|
|
148
|
+
// Each interval after the first starts at the exact boundary point
|
|
149
|
+
const start = minValue + i * intervalSize
|
|
150
|
+
const end = i === numIntervals - 1 ? maxValue : minValue + (i + 1) * intervalSize
|
|
151
|
+
const midPoint = (start + end) / 2
|
|
152
|
+
|
|
153
|
+
// For display, show the actual start for first interval
|
|
154
|
+
// For subsequent intervals, format the boundary point
|
|
155
|
+
const displayStart = start
|
|
156
|
+
const displayEnd = i === numIntervals - 1 ? end : end
|
|
157
|
+
|
|
158
|
+
intervalLabels.push({
|
|
159
|
+
datum: String(midPoint),
|
|
160
|
+
index: i,
|
|
161
|
+
text:
|
|
162
|
+
i === 0
|
|
163
|
+
? `${formatNumber(displayStart, 'left')} - < ${formatNumber(displayEnd, 'left')}`
|
|
164
|
+
: i === numIntervals - 1
|
|
165
|
+
? `${formatNumber(displayStart, 'left')} - ${formatNumber(displayEnd, 'left')}`
|
|
166
|
+
: `${formatNumber(displayStart, 'left')} - < ${formatNumber(displayEnd, 'left')}`,
|
|
167
|
+
value: warmingColorScale(midPoint)
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return intervalLabels
|
|
172
|
+
}
|
|
173
|
+
|
|
25
174
|
const sortVertical = labels =>
|
|
26
175
|
legend.verticalSorted
|
|
27
176
|
? _.sortBy(_.cloneDeep(labels), label => {
|
|
@@ -193,6 +342,19 @@ export const createFormatLabels =
|
|
|
193
342
|
return reverseLabels(seriesLabels)
|
|
194
343
|
}
|
|
195
344
|
|
|
345
|
+
// For Radar charts, use the entity names from runtime.seriesKeys (set from xAxis.dataKey values)
|
|
346
|
+
// not the series names (which are the dimension keys)
|
|
347
|
+
if (visualizationType === 'Radar') {
|
|
348
|
+
const entityNames = runtime.seriesKeys || []
|
|
349
|
+
const radarLabels = entityNames.map((val, i) => ({
|
|
350
|
+
datum: val,
|
|
351
|
+
index: i,
|
|
352
|
+
text: String(val),
|
|
353
|
+
value: colorScale(val)
|
|
354
|
+
}))
|
|
355
|
+
return reverseLabels(radarLabels)
|
|
356
|
+
}
|
|
357
|
+
|
|
196
358
|
if (config.series.some(item => item.name)) {
|
|
197
359
|
const uniqueLabels = Array.from(new Set(config.series.map(d => d.name || d.dataKey))).map((val, i) => ({
|
|
198
360
|
datum: val,
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* generateValueRanges - Creates value range bins for legend display
|
|
3
|
+
*
|
|
4
|
+
* Supports equal interval distribution with scaffolding for future quantile support.
|
|
5
|
+
* Ranges are inclusive with no overlap, starting at minValue or 0 (e.g., 1-100, 101-200).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type RangeDistribution = 'equal' | 'quantile'
|
|
9
|
+
|
|
10
|
+
export type ValueRange = {
|
|
11
|
+
min: number
|
|
12
|
+
max: number
|
|
13
|
+
label: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type GenerateValueRangesOptions = {
|
|
17
|
+
minValue?: number
|
|
18
|
+
maxValue: number
|
|
19
|
+
numRanges: number
|
|
20
|
+
distribution?: RangeDistribution
|
|
21
|
+
formatNumber?: (value: number, axis?: string) => string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Generates an array of value ranges for legend display
|
|
26
|
+
*/
|
|
27
|
+
export const generateValueRanges = ({
|
|
28
|
+
minValue = 0,
|
|
29
|
+
maxValue,
|
|
30
|
+
numRanges,
|
|
31
|
+
distribution = 'equal',
|
|
32
|
+
formatNumber
|
|
33
|
+
}: GenerateValueRangesOptions): ValueRange[] => {
|
|
34
|
+
if (numRanges <= 0 || maxValue <= minValue) {
|
|
35
|
+
return []
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const ranges: ValueRange[] = []
|
|
39
|
+
|
|
40
|
+
if (distribution === 'equal') {
|
|
41
|
+
const rangeSize = (maxValue - minValue) / numRanges
|
|
42
|
+
|
|
43
|
+
for (let i = 0; i < numRanges; i++) {
|
|
44
|
+
// Calculate raw boundaries
|
|
45
|
+
const rawMin = minValue + i * rangeSize
|
|
46
|
+
const rawMax = minValue + (i + 1) * rangeSize
|
|
47
|
+
|
|
48
|
+
// For display:
|
|
49
|
+
// - First range starts at floor of minValue
|
|
50
|
+
// - Subsequent ranges start at previous max + 1 (monotonic constraint)
|
|
51
|
+
// - Last range ends exactly at ceil of maxValue
|
|
52
|
+
let displayMin: number
|
|
53
|
+
let displayMax: number
|
|
54
|
+
|
|
55
|
+
if (i === 0) {
|
|
56
|
+
displayMin = Math.floor(rawMin)
|
|
57
|
+
} else {
|
|
58
|
+
// Start at previous range's max + 1 to avoid overlap
|
|
59
|
+
displayMin = ranges[i - 1].max + 1
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (i === numRanges - 1) {
|
|
63
|
+
// Last range ends exactly at maxValue
|
|
64
|
+
displayMax = Math.ceil(maxValue)
|
|
65
|
+
} else {
|
|
66
|
+
displayMax = Math.floor(rawMax)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Ensure min <= max (can happen with very small ranges)
|
|
70
|
+
if (displayMin > displayMax) {
|
|
71
|
+
displayMax = displayMin
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Format numbers if formatter provided
|
|
75
|
+
const formattedMin = formatNumber ? formatNumber(displayMin, 'left') : displayMin.toLocaleString()
|
|
76
|
+
const formattedMax = formatNumber ? formatNumber(displayMax, 'left') : displayMax.toLocaleString()
|
|
77
|
+
|
|
78
|
+
ranges.push({
|
|
79
|
+
min: displayMin,
|
|
80
|
+
max: displayMax,
|
|
81
|
+
label: displayMin === displayMax ? formattedMin : `${formattedMin}–${formattedMax}`
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
} else if (distribution === 'quantile') {
|
|
85
|
+
// Scaffolding for future quantile-based distribution
|
|
86
|
+
// Would require passing in the actual data values to calculate percentiles
|
|
87
|
+
// Currently, silently fall back to equal interval distribution
|
|
88
|
+
return generateValueRanges({ minValue, maxValue, numRanges, distribution: 'equal', formatNumber })
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return ranges
|
|
92
|
+
}
|
|
@@ -2,6 +2,15 @@ export const getGradientConfig = (config, formatLabels, colorScale) => {
|
|
|
2
2
|
const defaultValue = [{ datum: '', index: 0, text: '', value: '' }]
|
|
3
3
|
|
|
4
4
|
const formatted = formatLabels(defaultValue)
|
|
5
|
+
|
|
6
|
+
// For Warming Stripes, always use the formatted labels which contain the gradient stops
|
|
7
|
+
if (config.visualizationType === 'Warming Stripes' && config.legend.style === 'gradient') {
|
|
8
|
+
// Extract the colors array from the first item (all items have the same color stops)
|
|
9
|
+
const colors = formatted[0]?.colors || formatted.map(label => label?.value).filter(Boolean)
|
|
10
|
+
const labels = formatted.map(label => label?.text || label?.datum)
|
|
11
|
+
return { colors, labels }
|
|
12
|
+
}
|
|
13
|
+
|
|
5
14
|
const colors = config.legend.colorCode ? formatted.map(label => label?.value) : colorScale?.range() ?? []
|
|
6
15
|
const labels = config.legend.colorCode
|
|
7
16
|
? formatted.map(label => label?.text || label?.datum)
|
|
@@ -19,12 +28,7 @@ export const getMarginTop = (isLegendBottom, config) => {
|
|
|
19
28
|
if (!isLegendBottom) {
|
|
20
29
|
return '0px'
|
|
21
30
|
}
|
|
22
|
-
|
|
23
|
-
const additiolMargin = 25
|
|
24
|
-
return `${DEFAULT_MARGIN_TOP + config.brush?.height + additiolMargin}px`
|
|
25
|
-
} else {
|
|
26
|
-
return `${DEFAULT_MARGIN_TOP}px`
|
|
27
|
-
}
|
|
31
|
+
return `${DEFAULT_MARGIN_TOP}px`
|
|
28
32
|
}
|
|
29
33
|
export const getMarginBottom = (isLegendBottom, config) => {
|
|
30
34
|
const isLegendTop = config.legend?.position === 'top' && !config.legend.hide
|