@cdc/chart 4.26.1 → 4.26.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.local.md +79 -0
- package/dist/{cdcchart-dgT_1dIT.es.js → cdcchart-DQ00cQCm.es.js} +1 -20
- package/dist/cdcchart.js +45357 -43655
- package/examples/default.json +378 -0
- package/examples/feature/__data__/horizon-chart-data.json +373 -0
- package/examples/feature/annotations/index.json +3 -6
- package/examples/feature/horizon/horizon-chart.json +395 -0
- package/examples/line-chart-states.json +1085 -0
- package/examples/private/123.json +694 -0
- package/examples/private/anchor-issue.json +4094 -0
- package/examples/private/backwards-slider.json +10430 -0
- package/examples/private/georgia.csv +160 -0
- package/examples/private/timeline-data.json +1 -0
- package/examples/private/timeline.json +389 -0
- package/examples/radar-chart-simple.json +133 -0
- package/examples/radar-chart.json +148 -0
- package/index.html +1 -31
- package/package.json +57 -59
- package/src/CdcChartComponent.tsx +99 -18
- package/src/_stories/Chart.Anchors.stories.tsx +10 -0
- package/src/_stories/Chart.BoxPlot.stories.tsx +7 -0
- package/src/_stories/Chart.CI.stories.tsx +13 -0
- package/src/_stories/Chart.Combo.stories.tsx +17 -0
- package/src/_stories/Chart.CustomColors.stories.tsx +4 -0
- package/src/_stories/Chart.DynamicSeries.stories.tsx +19 -0
- package/src/_stories/Chart.Filters.stories.tsx +4 -0
- package/src/_stories/Chart.Forecast.stories.tsx +4 -0
- package/src/_stories/Chart.HTMLInDataTable.stories.tsx +22 -0
- package/src/_stories/Chart.Legend.Gradient.stories.tsx +28 -0
- package/src/_stories/Chart.Patterns.stories.tsx +4 -0
- package/src/_stories/Chart.PreserveDecimals.stories.tsx +25 -0
- package/src/_stories/Chart.Regions.Categorical.stories.tsx +13 -0
- package/src/_stories/Chart.Regions.DateScale.stories.tsx +19 -0
- package/src/_stories/Chart.Regions.DateTimeScale.stories.tsx +25 -10
- package/src/_stories/Chart.ScatterPlot.stories.tsx +4 -0
- package/src/_stories/Chart.SmallMultiples.stories.tsx +16 -0
- package/src/_stories/Chart.stories.tsx +37 -0
- package/src/_stories/Chart.tooltip.stories.tsx +7 -0
- package/src/_stories/ChartAnnotation.stories.tsx +10 -0
- package/src/_stories/ChartAxisLabels.stories.tsx +4 -0
- package/src/_stories/ChartAxisTitles.stories.tsx +10 -0
- package/src/_stories/ChartBrush.Matrix.Continuous.stories.tsx +41 -0
- package/src/_stories/ChartBrush.Matrix.Date.stories.tsx +114 -0
- package/src/_stories/ChartBrush.Matrix.DateTime.stories.tsx +78 -0
- package/src/_stories/ChartBrush.stories.tsx +7 -0
- package/src/_stories/ChartEditor.stories.tsx +7 -0
- package/src/_stories/ChartLine.QuadrantAngles.stories.tsx +89 -0
- package/src/_stories/ChartLine.Suppression.stories.tsx +7 -0
- package/src/_stories/ChartLine.Symbols.stories.tsx +4 -0
- package/src/_stories/ChartPrefixSuffix.stories.tsx +46 -1
- package/src/_stories/TechAdoptionWithLinks.stories.tsx +7 -0
- package/src/_stories/_mock/brush_continuous.json +86 -0
- package/src/_stories/_mock/brush_date_large.json +176 -0
- package/src/_stories/_mock/line_chart_angle_near_zero_fall.json +195 -0
- package/src/_stories/_mock/line_chart_angle_near_zero_rise.json +195 -0
- package/src/_stories/_mock/line_chart_angle_q1_steep_upward.json +195 -0
- package/src/_stories/_mock/line_chart_angle_q2_gentle_downward.json +195 -0
- package/src/_stories/_mock/line_chart_angle_q3_steep_downward.json +195 -0
- package/src/_stories/_mock/line_chart_angle_q4_gentle_upward.json +195 -0
- package/src/_stories/_mock/line_chart_quadrant_angles.json +264 -0
- package/src/components/Annotations/components/AnnotationDraggable.styles.css +11 -17
- package/src/components/Annotations/components/AnnotationDraggable.tsx +240 -116
- package/src/components/Annotations/components/AnnotationDropdown.styles.css +1 -2
- package/src/components/Annotations/components/AnnotationDropdown.tsx +8 -12
- package/src/components/Annotations/components/AnnotationList.styles.css +4 -10
- package/src/components/Annotations/components/AnnotationList.tsx +5 -4
- package/src/components/Annotations/components/findNearestDatum.ts +75 -85
- package/src/components/Annotations/helpers/getVisibleAnnotations.ts +38 -0
- package/src/components/Axis/BottomAxis.tsx +270 -0
- package/src/components/Axis/LeftAxis.tsx +404 -0
- package/src/components/Axis/LeftAxisGridlines.tsx +77 -0
- package/src/components/Axis/PairedBarAxis.tsx +186 -0
- package/src/components/Axis/README.md +94 -0
- package/src/components/Axis/RightAxis.tsx +108 -0
- package/src/components/Axis/axis.constants.ts +21 -0
- package/src/components/Axis/index.ts +7 -0
- package/src/components/BarChart/components/BarChart.tsx +7 -1
- package/src/components/Brush/BrushSelector.tsx +154 -22
- package/src/components/Brush/MiniChartPreview.tsx +138 -21
- package/src/components/EditorPanel/EditorPanel.tsx +25 -11
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +60 -22
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +81 -1
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +1 -1
- package/src/components/EditorPanel/components/Panels/Panel.Radar.tsx +353 -0
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +0 -1
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +21 -1
- package/src/components/EditorPanel/components/Panels/index.tsx +2 -0
- package/src/components/EditorPanel/useEditorPermissions.ts +55 -26
- package/src/components/HorizonChart/HorizonChart.tsx +131 -0
- package/src/components/HorizonChart/components/HorizonBand.tsx +160 -0
- package/src/components/HorizonChart/helpers/calculateHorizonBands.ts +27 -0
- package/src/components/HorizonChart/helpers/getHorizonLayerColors.ts +40 -0
- package/src/components/HorizonChart/index.tsx +3 -0
- package/src/components/Legend/Legend.Component.tsx +52 -4
- package/src/components/Legend/Legend.tsx +1 -1
- package/src/components/Legend/LegendValueRange.tsx +77 -0
- package/src/components/Legend/helpers/createFormatLabels.tsx +13 -0
- package/src/components/Legend/helpers/generateValueRanges.ts +92 -0
- package/src/components/LineChart/helpers/README.md +292 -0
- package/src/components/LineChart/helpers/labelPositioning.test.ts +245 -0
- package/src/components/LineChart/helpers/labelPositioning.ts +304 -0
- package/src/components/LineChart/index.tsx +44 -8
- package/src/components/LinearChart/README.md +109 -0
- package/src/components/LinearChart/VisualizationRenderer.tsx +267 -0
- package/src/components/LinearChart/linearChart.constants.ts +84 -0
- package/src/components/LinearChart/tests/LinearChart.test.tsx +201 -0
- package/src/components/LinearChart/tests/mockConfigContext.ts +129 -0
- package/src/components/LinearChart/utils/tickFormatting.ts +146 -0
- package/src/components/LinearChart.tsx +250 -1059
- package/src/components/PieChart/PieChart.tsx +1 -1
- package/src/components/RadarChart/RadarAxis.tsx +78 -0
- package/src/components/RadarChart/RadarChart.tsx +298 -0
- package/src/components/RadarChart/RadarGrid.tsx +64 -0
- package/src/components/RadarChart/RadarPolygon.tsx +91 -0
- package/src/components/RadarChart/helpers.ts +83 -0
- package/src/components/RadarChart/index.tsx +3 -0
- package/src/components/WarmingStripes/WarmingStripes.tsx +95 -25
- package/src/data/initial-state.js +14 -1
- package/src/helpers/getExcludedData.ts +4 -0
- package/src/helpers/handleChartAriaLabels.ts +19 -19
- package/src/helpers/handleLineType.ts +22 -18
- package/src/hooks/useProgrammaticTooltip.ts +23 -2
- package/src/hooks/useScales.ts +7 -0
- package/src/hooks/useTooltip.tsx +3 -0
- package/src/scss/main.scss +5 -0
- package/src/selectors/README.md +68 -0
- package/src/store/chart.reducer.ts +2 -0
- package/src/types/ChartConfig.ts +18 -0
- package/src/types/ChartContext.ts +1 -0
- package/src/types/Horizon.ts +64 -0
- package/preview.html +0 -1616
- package/src/components/Annotations/components/helpers/index.tsx +0 -46
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Axis Components
|
|
2
|
+
|
|
3
|
+
This folder contains extracted axis components from `LinearChart.tsx` to improve maintainability and reduce complexity.
|
|
4
|
+
|
|
5
|
+
## Components
|
|
6
|
+
|
|
7
|
+
### LeftAxis
|
|
8
|
+
**File:** `LeftAxis.tsx`
|
|
9
|
+
|
|
10
|
+
Renders the left (Y) axis for vertical charts. Handles:
|
|
11
|
+
- Tick formatting and positioning
|
|
12
|
+
- Labels above gridlines option
|
|
13
|
+
- Inline label (suffix) display
|
|
14
|
+
- Forest plot special rendering
|
|
15
|
+
|
|
16
|
+
**Props:**
|
|
17
|
+
- `yScale` - D3 scale for Y axis
|
|
18
|
+
- `xScale` - D3 scale for X axis
|
|
19
|
+
- `yMax`, `xMax` - Chart dimensions
|
|
20
|
+
- `yAxisWidth` - Width allocated for Y axis
|
|
21
|
+
- `numTicks` - Number of ticks to display
|
|
22
|
+
- `handleLeftTickFormatting` - Tick format function
|
|
23
|
+
|
|
24
|
+
### LeftAxisGridlines
|
|
25
|
+
**File:** `LeftAxisGridlines.tsx`
|
|
26
|
+
|
|
27
|
+
Renders horizontal gridlines separately from the axis itself. This separation allows gridlines to be drawn behind the visualization while the axis is drawn on top.
|
|
28
|
+
|
|
29
|
+
### BottomAxis
|
|
30
|
+
**File:** `BottomAxis.tsx`
|
|
31
|
+
|
|
32
|
+
Renders the bottom (X) axis. Handles:
|
|
33
|
+
- Date/time formatting
|
|
34
|
+
- Tick rotation for responsive layouts
|
|
35
|
+
- Manual step configuration
|
|
36
|
+
- Brush integration
|
|
37
|
+
|
|
38
|
+
**Props:**
|
|
39
|
+
- `xScale` - D3 scale for X axis
|
|
40
|
+
- `yMax` - Chart height
|
|
41
|
+
- `xTickCount` - Number of ticks
|
|
42
|
+
- `handleBottomTickFormatting` - Tick format function
|
|
43
|
+
- `useDateSpanMonths` - Date range calculation flag
|
|
44
|
+
|
|
45
|
+
### PairedBarAxis
|
|
46
|
+
**File:** `PairedBarAxis.tsx`
|
|
47
|
+
|
|
48
|
+
Specialized axis for Paired Bar charts with two mirrored AxisBottom components. Handles:
|
|
49
|
+
- Dual scale rendering (g1xScale, g2xScale)
|
|
50
|
+
- Responsive tick rotation
|
|
51
|
+
- Tick overlap detection
|
|
52
|
+
|
|
53
|
+
### RightAxis
|
|
54
|
+
**File:** `RightAxis.tsx`
|
|
55
|
+
|
|
56
|
+
Renders the right (Y) axis for dual-axis charts. Handles:
|
|
57
|
+
- Secondary Y scale
|
|
58
|
+
- Configurable tick/label colors
|
|
59
|
+
- Optional gridlines from right axis
|
|
60
|
+
|
|
61
|
+
### CategoricalYAxis
|
|
62
|
+
**File:** `Categorical.Axis.tsx`
|
|
63
|
+
|
|
64
|
+
Specialized Y axis for categorical data types.
|
|
65
|
+
|
|
66
|
+
## Constants
|
|
67
|
+
**File:** `axis.constants.ts`
|
|
68
|
+
|
|
69
|
+
Shared constants used across axis components.
|
|
70
|
+
|
|
71
|
+
## Usage
|
|
72
|
+
|
|
73
|
+
All components are exported from `index.ts`:
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
import {
|
|
77
|
+
LeftAxis,
|
|
78
|
+
LeftAxisGridlines,
|
|
79
|
+
BottomAxis,
|
|
80
|
+
PairedBarAxis,
|
|
81
|
+
RightAxis,
|
|
82
|
+
CategoricalYAxis
|
|
83
|
+
} from './Axis'
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Architecture Notes
|
|
87
|
+
|
|
88
|
+
These components were extracted from `LinearChart.tsx` as part of a refactoring effort to:
|
|
89
|
+
1. Reduce the main component from 1,704 to ~845 lines
|
|
90
|
+
2. Improve testability through smaller, focused components
|
|
91
|
+
3. Enable reuse across different chart types
|
|
92
|
+
4. Simplify maintenance and debugging
|
|
93
|
+
|
|
94
|
+
Each component accesses the `ConfigContext` for chart configuration rather than receiving all config as props, keeping the prop interfaces focused on rendering-specific data.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import React, { useContext } from 'react'
|
|
2
|
+
import { AxisRight as VisxAxisRight } from '@visx/axis'
|
|
3
|
+
import { Group } from '@visx/group'
|
|
4
|
+
import { Line } from '@visx/shape'
|
|
5
|
+
import { Text } from '@visx/text'
|
|
6
|
+
import { ScaleLinear } from 'd3-scale'
|
|
7
|
+
|
|
8
|
+
import ConfigContext from '../../ConfigContext'
|
|
9
|
+
|
|
10
|
+
// Constants
|
|
11
|
+
const HORIZONTAL_TICK_OFFSET_ADJUSTMENT = 5
|
|
12
|
+
|
|
13
|
+
type RightAxisProps = {
|
|
14
|
+
yScaleRight: ScaleLinear<number, number>
|
|
15
|
+
yMax: number
|
|
16
|
+
xMax: number
|
|
17
|
+
yAxisWidth: number
|
|
18
|
+
tickLabelFontSize: number
|
|
19
|
+
axisLabelFontSize: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Right Y-axis component for dual-axis charts.
|
|
24
|
+
* Renders a secondary y-axis on the right side with configurable styling.
|
|
25
|
+
* Extracted from LinearChart.tsx
|
|
26
|
+
*/
|
|
27
|
+
export const RightAxis: React.FC<RightAxisProps> = ({
|
|
28
|
+
yScaleRight,
|
|
29
|
+
yMax,
|
|
30
|
+
xMax,
|
|
31
|
+
yAxisWidth,
|
|
32
|
+
tickLabelFontSize,
|
|
33
|
+
axisLabelFontSize
|
|
34
|
+
}) => {
|
|
35
|
+
const { config, formatNumber } = useContext(ConfigContext)
|
|
36
|
+
const { runtime } = config
|
|
37
|
+
|
|
38
|
+
const horizontalTickOffset = (ticks: any[]) =>
|
|
39
|
+
yMax / ticks.length / 2 - (yMax / ticks.length) * (1 - config.barThickness) + HORIZONTAL_TICK_OFFSET_ADJUSTMENT
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<VisxAxisRight
|
|
43
|
+
scale={yScaleRight}
|
|
44
|
+
left={yAxisWidth + xMax}
|
|
45
|
+
label={config.yAxis.rightLabel}
|
|
46
|
+
tickFormat={tick => formatNumber(tick, 'right')}
|
|
47
|
+
numTicks={runtime.yAxis.rightNumTicks || undefined}
|
|
48
|
+
labelOffset={45}
|
|
49
|
+
>
|
|
50
|
+
{props => {
|
|
51
|
+
const axisCenter =
|
|
52
|
+
config.orientation === 'horizontal'
|
|
53
|
+
? (props.axisToPoint.y - props.axisFromPoint.y) / 2
|
|
54
|
+
: (props.axisFromPoint.y - props.axisToPoint.y) / 2
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<Group className='right-axis'>
|
|
58
|
+
{props.ticks.map((tick, i) => (
|
|
59
|
+
<Group key={`vx-tick-${tick.value}-${i}`} className='vx-axis-tick'>
|
|
60
|
+
{!runtime.yAxis.rightHideTicks && (
|
|
61
|
+
<Line
|
|
62
|
+
from={tick.from}
|
|
63
|
+
to={tick.to}
|
|
64
|
+
display={runtime.horizontal ? 'none' : 'block'}
|
|
65
|
+
stroke={config.yAxis.rightAxisTickColor}
|
|
66
|
+
/>
|
|
67
|
+
)}
|
|
68
|
+
|
|
69
|
+
{runtime.yAxis.rightGridLines && (
|
|
70
|
+
<Line from={{ x: tick.from.x + xMax, y: tick.from.y }} to={tick.from} stroke='#d6d6d6' />
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
{!config.yAxis.rightHideLabel && (
|
|
74
|
+
<Text
|
|
75
|
+
x={tick.to.x}
|
|
76
|
+
y={tick.to.y + (runtime.horizontal ? horizontalTickOffset(props.ticks) : 0)}
|
|
77
|
+
verticalAnchor={runtime.horizontal ? 'start' : 'middle'}
|
|
78
|
+
textAnchor='start'
|
|
79
|
+
fill={config.yAxis.rightAxisTickLabelColor}
|
|
80
|
+
fontSize={tickLabelFontSize}
|
|
81
|
+
>
|
|
82
|
+
{tick.formattedValue}
|
|
83
|
+
</Text>
|
|
84
|
+
)}
|
|
85
|
+
</Group>
|
|
86
|
+
))}
|
|
87
|
+
|
|
88
|
+
{!config.yAxis.rightHideAxis && <Line from={props.axisFromPoint} to={props.axisToPoint} stroke='#333' />}
|
|
89
|
+
|
|
90
|
+
<Text
|
|
91
|
+
className='y-label'
|
|
92
|
+
textAnchor='middle'
|
|
93
|
+
verticalAnchor='start'
|
|
94
|
+
transform={`translate(${config.yAxis.rightLabelOffsetSize || 0}, ${axisCenter}) rotate(-90)`}
|
|
95
|
+
fontWeight='bold'
|
|
96
|
+
fill={config.yAxis.rightAxisLabelColor}
|
|
97
|
+
fontSize={axisLabelFontSize}
|
|
98
|
+
>
|
|
99
|
+
{props.label}
|
|
100
|
+
</Text>
|
|
101
|
+
</Group>
|
|
102
|
+
)
|
|
103
|
+
}}
|
|
104
|
+
</VisxAxisRight>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default RightAxis
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Tick length constants
|
|
2
|
+
export const DEFAULT_TICK_LENGTH = 8
|
|
3
|
+
export const LOGARITHMIC_TICK_LENGTH = 6
|
|
4
|
+
export const MAJOR_LOG_TICK_LENGTH = 7
|
|
5
|
+
|
|
6
|
+
// Tick styling constants
|
|
7
|
+
export const TICK_LABEL_MARGIN_RIGHT = 4.5
|
|
8
|
+
export const MAJOR_LOG_TICK_STROKE_WIDTH = 1.3
|
|
9
|
+
|
|
10
|
+
// Label positioning constants
|
|
11
|
+
export const VALUE_ON_LINE_PADDING_NO_AXIS = -8
|
|
12
|
+
export const VALUE_ON_LINE_PADDING_WITH_AXIS = -12
|
|
13
|
+
export const LABEL_Y_PADDING_ABOVE_GRIDLINES = 4
|
|
14
|
+
export const HORIZONTAL_TICK_OFFSET_ADJUSTMENT = 5
|
|
15
|
+
|
|
16
|
+
// Chart-specific constants
|
|
17
|
+
export const ZERO_LINE_STROKE_WIDTH = 2
|
|
18
|
+
export const BAR_MIN_HEIGHT = 15
|
|
19
|
+
|
|
20
|
+
// Lollipop chart sizes
|
|
21
|
+
export const LOLLIPOP_SIZES = { large: 7, medium: 6, small: 5 } as const
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { default as CategoricalYAxis } from './Categorical.Axis'
|
|
2
|
+
export { default as LeftAxis } from './LeftAxis'
|
|
3
|
+
export { default as LeftAxisGridlines } from './LeftAxisGridlines'
|
|
4
|
+
export { default as BottomAxis } from './BottomAxis'
|
|
5
|
+
export { default as PairedBarAxis } from './PairedBarAxis'
|
|
6
|
+
export { default as RightAxis } from './RightAxis'
|
|
7
|
+
export * from './axis.constants'
|
|
@@ -18,6 +18,7 @@ type BarChartProps = {
|
|
|
18
18
|
seriesScale: PositionScale
|
|
19
19
|
xMax: number
|
|
20
20
|
yMax: number
|
|
21
|
+
yAxisWidth?: number
|
|
21
22
|
handleTooltipMouseOver: MouseEventHandler<SVGRectElement>
|
|
22
23
|
handleTooltipMouseOff: MouseEventHandler<SVGRectElement>
|
|
23
24
|
handleTooltipClick: MouseEventHandler<SVGRectElement>
|
|
@@ -29,6 +30,7 @@ const BarChart: React.FC<BarChartProps> = ({
|
|
|
29
30
|
seriesScale,
|
|
30
31
|
xMax,
|
|
31
32
|
yMax,
|
|
33
|
+
yAxisWidth,
|
|
32
34
|
handleTooltipMouseOver,
|
|
33
35
|
handleTooltipMouseOff,
|
|
34
36
|
handleTooltipClick
|
|
@@ -47,10 +49,14 @@ const BarChart: React.FC<BarChartProps> = ({
|
|
|
47
49
|
barChart
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
// Use yAxisWidth prop if provided (for horizontal bar charts with dynamic labels)
|
|
53
|
+
// otherwise fall back to config value
|
|
54
|
+
const leftOffset = yAxisWidth ?? parseFloat(config.runtime.yAxis.size)
|
|
55
|
+
|
|
50
56
|
return (
|
|
51
57
|
<ErrorBoundary component='BarChart'>
|
|
52
58
|
<BarChartContext.Provider value={contextValues}>
|
|
53
|
-
<Group left={
|
|
59
|
+
<Group left={leftOffset}>
|
|
54
60
|
<BarChartType.StackedVertical />
|
|
55
61
|
<BarChartType.StackedHorizontal />
|
|
56
62
|
<BarChartType.Vertical />
|
|
@@ -2,9 +2,11 @@ import React, { FC, useContext, useMemo, memo, useRef, useEffect, useState, useC
|
|
|
2
2
|
import { Brush } from '@visx/brush'
|
|
3
3
|
import BaseBrush from '@visx/brush/lib/BaseBrush'
|
|
4
4
|
import { Group } from '@visx/group'
|
|
5
|
+
import { PatternLines, PatternCircles, PatternWaves } from '@visx/pattern'
|
|
5
6
|
import { scaleBand, scaleLinear } from '@visx/scale'
|
|
6
7
|
import { Bounds } from '@visx/brush/lib/types'
|
|
7
8
|
import type { BrushHandleRenderProps } from '@visx/brush/lib/BrushHandle'
|
|
9
|
+
import { isDateScale } from '@cdc/core/helpers/cove/date'
|
|
8
10
|
import ConfigContext, { ChartDispatchContext } from '../../ConfigContext'
|
|
9
11
|
import MiniChartPreview from './MiniChartPreview'
|
|
10
12
|
|
|
@@ -110,11 +112,77 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
|
|
|
110
112
|
const selectionRef = useRef<HTMLButtonElement>(null)
|
|
111
113
|
const rightHandleRef = useRef<HTMLButtonElement>(null)
|
|
112
114
|
|
|
113
|
-
const { tableData, config, colorScale } = useContext(ConfigContext)
|
|
115
|
+
const { tableData, config, colorScale, parseDate } = useContext(ConfigContext)
|
|
114
116
|
const dispatch = useContext(ChartDispatchContext)
|
|
115
117
|
const dataKey = config.xAxis.dataKey
|
|
116
118
|
const series = config.series || []
|
|
117
119
|
|
|
120
|
+
const renderPatternDefs = () => {
|
|
121
|
+
if (!config.legend?.patterns || Object.keys(config.legend.patterns).length === 0) {
|
|
122
|
+
return null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<>
|
|
127
|
+
{Object.entries(config.legend.patterns).map(([key, pattern]) => {
|
|
128
|
+
const patternId = `chart-pattern-${key}`
|
|
129
|
+
const size = pattern.patternSize || 8
|
|
130
|
+
|
|
131
|
+
switch (pattern.shape) {
|
|
132
|
+
case 'circles':
|
|
133
|
+
return (
|
|
134
|
+
<PatternCircles
|
|
135
|
+
key={patternId}
|
|
136
|
+
id={patternId}
|
|
137
|
+
height={size}
|
|
138
|
+
width={size}
|
|
139
|
+
fill={pattern.color}
|
|
140
|
+
radius={1.25}
|
|
141
|
+
/>
|
|
142
|
+
)
|
|
143
|
+
case 'lines':
|
|
144
|
+
return (
|
|
145
|
+
<PatternLines
|
|
146
|
+
key={patternId}
|
|
147
|
+
id={patternId}
|
|
148
|
+
height={size}
|
|
149
|
+
width={size}
|
|
150
|
+
stroke={pattern.color}
|
|
151
|
+
strokeWidth={0.75}
|
|
152
|
+
orientation={['horizontal']}
|
|
153
|
+
/>
|
|
154
|
+
)
|
|
155
|
+
case 'diagonalLines':
|
|
156
|
+
return (
|
|
157
|
+
<PatternLines
|
|
158
|
+
key={patternId}
|
|
159
|
+
id={patternId}
|
|
160
|
+
height={size}
|
|
161
|
+
width={size}
|
|
162
|
+
stroke={pattern.color}
|
|
163
|
+
strokeWidth={0.75}
|
|
164
|
+
orientation={['diagonalRightToLeft']}
|
|
165
|
+
/>
|
|
166
|
+
)
|
|
167
|
+
case 'waves':
|
|
168
|
+
return (
|
|
169
|
+
<PatternWaves
|
|
170
|
+
key={patternId}
|
|
171
|
+
id={patternId}
|
|
172
|
+
height={size}
|
|
173
|
+
width={size}
|
|
174
|
+
fill={pattern.color}
|
|
175
|
+
strokeWidth={0.25}
|
|
176
|
+
/>
|
|
177
|
+
)
|
|
178
|
+
default:
|
|
179
|
+
return null
|
|
180
|
+
}
|
|
181
|
+
})}
|
|
182
|
+
</>
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
118
186
|
// Capture initial brush extent after mount and sync accessible extent
|
|
119
187
|
useEffect(() => {
|
|
120
188
|
if (brushRef.current && brushRef.current.state.extent.x0 !== -1) {
|
|
@@ -133,8 +201,22 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
|
|
|
133
201
|
return scaleBand<string>({ domain: [], range: [0, Math.max(xMax, 0)] })
|
|
134
202
|
}
|
|
135
203
|
|
|
136
|
-
const
|
|
137
|
-
|
|
204
|
+
const mappedValues = tableData.map(row => row[dataKey])
|
|
205
|
+
|
|
206
|
+
// Sort domain chronologically for date types, matching the main chart's sort behavior.
|
|
207
|
+
// Without this, data arriving in reverse chronological order renders the brush backwards.
|
|
208
|
+
const xAxisType = config?.xAxis?.type
|
|
209
|
+
let domain: string[]
|
|
210
|
+
if (xAxisType === 'date' || xAxisType === 'date-time') {
|
|
211
|
+
const sorted = [...mappedValues].sort((a, b) => {
|
|
212
|
+
const dateA = parseDate ? parseDate(a) : new Date(a)
|
|
213
|
+
const dateB = parseDate ? parseDate(b) : new Date(b)
|
|
214
|
+
return dateA - dateB
|
|
215
|
+
})
|
|
216
|
+
domain = config?.xAxis?.sortByRecentDate ? sorted.reverse() : sorted
|
|
217
|
+
} else {
|
|
218
|
+
domain = config?.xAxis?.sortByRecentDate ? [...mappedValues].reverse() : mappedValues
|
|
219
|
+
}
|
|
138
220
|
|
|
139
221
|
return scaleBand<string>({
|
|
140
222
|
domain,
|
|
@@ -142,30 +224,82 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
|
|
|
142
224
|
paddingInner: 0.1,
|
|
143
225
|
paddingOuter: 0.1
|
|
144
226
|
})
|
|
145
|
-
}, [tableData, dataKey, config?.xAxis?.sortByRecentDate, xMax])
|
|
227
|
+
}, [tableData, dataKey, config?.xAxis?.sortByRecentDate, config?.xAxis?.type, parseDate, xMax])
|
|
146
228
|
|
|
147
229
|
// Simple Y scale for brush (identity mapping)
|
|
148
230
|
const yScale = useMemo(() => scaleLinear<number>({ domain: [0, BRUSH_HEIGHT], range: [BRUSH_HEIGHT, 0] }), [])
|
|
149
231
|
|
|
150
|
-
//
|
|
232
|
+
// Helper to build a mini Y scale from a subset of series
|
|
233
|
+
const buildMiniYScale = useCallback(
|
|
234
|
+
(seriesSubset: typeof series, includeZero: boolean) => {
|
|
235
|
+
const defaultScale = scaleLinear({ domain: [0, 100], range: [BRUSH_HEIGHT - 4, 2] })
|
|
236
|
+
if (!seriesSubset.length || !tableData.length) return defaultScale
|
|
237
|
+
|
|
238
|
+
let minValue = Number.POSITIVE_INFINITY
|
|
239
|
+
let maxValue = Number.NEGATIVE_INFINITY
|
|
240
|
+
let hasValidValues = false
|
|
241
|
+
|
|
242
|
+
for (const s of seriesSubset) {
|
|
243
|
+
if (!s.dataKey) continue
|
|
244
|
+
for (const row of tableData) {
|
|
245
|
+
const value = parseFloat(row[s.dataKey])
|
|
246
|
+
if (!isNaN(value) && isFinite(value)) {
|
|
247
|
+
hasValidValues = true
|
|
248
|
+
minValue = Math.min(minValue, value)
|
|
249
|
+
maxValue = Math.max(maxValue, value)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!hasValidValues) return defaultScale
|
|
255
|
+
|
|
256
|
+
if (includeZero) minValue = Math.min(0, minValue)
|
|
257
|
+
|
|
258
|
+
if (minValue === maxValue) {
|
|
259
|
+
const padding = Math.abs(minValue) * 0.1 || 10
|
|
260
|
+
minValue = minValue - padding
|
|
261
|
+
maxValue = maxValue + padding
|
|
262
|
+
if (minValue > 0) minValue = 0
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const domain = [minValue, maxValue]
|
|
266
|
+
return scaleLinear({ domain, range: [BRUSH_HEIGHT - 4, 2], nice: true })
|
|
267
|
+
},
|
|
268
|
+
[tableData]
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
// Determine if we have a right-axis series (dual-axis combo)
|
|
272
|
+
const hasRightAxis = useMemo(
|
|
273
|
+
() => config.visualizationType === 'Combo' && series.some(s => s.axis === 'Right'),
|
|
274
|
+
[series, config.visualizationType]
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
// Mini chart Y scale — left-axis (or all series when there's no right axis)
|
|
151
278
|
const miniYScale = useMemo(() => {
|
|
152
279
|
if (!series.length || !tableData.length) {
|
|
153
280
|
return scaleLinear({ domain: [0, 100], range: [BRUSH_HEIGHT - 4, 2] })
|
|
154
281
|
}
|
|
155
282
|
|
|
283
|
+
const barSeriesTypes = new Set(['Bar', 'Paired Bar', 'Deviation Bar', 'Combo'])
|
|
284
|
+
const hasBarSeries =
|
|
285
|
+
config.visualizationType === 'Bar' ||
|
|
286
|
+
(config.visualizationType === 'Combo' && series.some(s => barSeriesTypes.has(s.type)))
|
|
156
287
|
const isStacked =
|
|
157
288
|
config.visualizationSubType === 'stacked' &&
|
|
158
289
|
(config.visualizationType === 'Bar' || config.visualizationType === 'Area Chart')
|
|
290
|
+
|
|
291
|
+
// When dual-axis, only use left-axis series for this scale
|
|
292
|
+
const leftSeries = hasRightAxis ? series.filter(s => s.axis !== 'Right') : series
|
|
293
|
+
|
|
159
294
|
let minValue = Number.POSITIVE_INFINITY
|
|
160
295
|
let maxValue = Number.NEGATIVE_INFINITY
|
|
161
296
|
let hasValidValues = false
|
|
162
297
|
|
|
163
298
|
if (isStacked) {
|
|
164
|
-
// For stacked bars, calculate the sum of all series for each row
|
|
165
299
|
for (const row of tableData) {
|
|
166
300
|
let rowSum = 0
|
|
167
301
|
let hasRowValue = false
|
|
168
|
-
for (const s of
|
|
302
|
+
for (const s of leftSeries) {
|
|
169
303
|
if (!s.dataKey) continue
|
|
170
304
|
const value = parseFloat(row[s.dataKey])
|
|
171
305
|
if (!isNaN(value) && isFinite(value)) {
|
|
@@ -179,11 +313,9 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
|
|
|
179
313
|
maxValue = Math.max(maxValue, rowSum)
|
|
180
314
|
}
|
|
181
315
|
}
|
|
182
|
-
// For stacked charts, ensure domain starts at 0
|
|
183
316
|
minValue = Math.min(0, minValue)
|
|
184
317
|
} else {
|
|
185
|
-
|
|
186
|
-
for (const s of series) {
|
|
318
|
+
for (const s of leftSeries) {
|
|
187
319
|
if (!s.dataKey) continue
|
|
188
320
|
for (const row of tableData) {
|
|
189
321
|
const value = parseFloat(row[s.dataKey])
|
|
@@ -194,19 +326,13 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
|
|
|
194
326
|
}
|
|
195
327
|
}
|
|
196
328
|
}
|
|
197
|
-
|
|
198
|
-
if (config.visualizationType === 'Bar') {
|
|
199
|
-
minValue = Math.min(0, minValue)
|
|
200
|
-
}
|
|
329
|
+
if (hasBarSeries) minValue = Math.min(0, minValue)
|
|
201
330
|
}
|
|
202
331
|
|
|
203
|
-
// Handle edge case where all values are the same
|
|
204
332
|
if (hasValidValues && minValue === maxValue) {
|
|
205
|
-
// Create a domain with some padding around the single value
|
|
206
333
|
const padding = Math.abs(minValue) * 0.1 || 10
|
|
207
334
|
minValue = minValue - padding
|
|
208
335
|
maxValue = maxValue + padding
|
|
209
|
-
// Ensure 0 is included if we're near it
|
|
210
336
|
if (minValue > 0) minValue = 0
|
|
211
337
|
}
|
|
212
338
|
|
|
@@ -214,14 +340,18 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
|
|
|
214
340
|
return scaleLinear({ domain: [0, 100], range: [BRUSH_HEIGHT - 4, 2] })
|
|
215
341
|
}
|
|
216
342
|
|
|
217
|
-
|
|
218
|
-
if (config.visualizationType === 'Bar') {
|
|
219
|
-
minValue = Math.min(0, minValue)
|
|
220
|
-
}
|
|
343
|
+
if (hasBarSeries) minValue = Math.min(0, minValue)
|
|
221
344
|
|
|
222
345
|
const domain = minValue === maxValue ? [minValue - 1, maxValue + 1] : [minValue, maxValue]
|
|
223
346
|
return scaleLinear({ domain, range: [BRUSH_HEIGHT - 4, 2], nice: true })
|
|
224
|
-
}, [series, tableData, config.visualizationSubType, config.visualizationType])
|
|
347
|
+
}, [series, tableData, config.visualizationSubType, config.visualizationType, hasRightAxis])
|
|
348
|
+
|
|
349
|
+
// Mini chart Y scale for right-axis series (dual-axis combo charts)
|
|
350
|
+
const miniRightYScale = useMemo(() => {
|
|
351
|
+
if (!hasRightAxis) return undefined
|
|
352
|
+
const rightSeries = series.filter(s => s.axis === 'Right')
|
|
353
|
+
return buildMiniYScale(rightSeries, false)
|
|
354
|
+
}, [hasRightAxis, series, buildMiniYScale])
|
|
225
355
|
|
|
226
356
|
// Fallback: Window mouseup listener to prevent stuck drag states
|
|
227
357
|
useEffect(() => {
|
|
@@ -1107,6 +1237,7 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
|
|
|
1107
1237
|
shapeRendering='auto'
|
|
1108
1238
|
/>
|
|
1109
1239
|
</pattern>
|
|
1240
|
+
{renderPatternDefs()}
|
|
1110
1241
|
</defs>
|
|
1111
1242
|
|
|
1112
1243
|
{/* Mini chart preview */}
|
|
@@ -1118,6 +1249,7 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
|
|
|
1118
1249
|
dataKey={dataKey}
|
|
1119
1250
|
xScale={xScale}
|
|
1120
1251
|
miniYScale={miniYScale}
|
|
1252
|
+
miniRightYScale={miniRightYScale}
|
|
1121
1253
|
colorScale={colorScale}
|
|
1122
1254
|
config={config}
|
|
1123
1255
|
xMax={safeXMax}
|