@cdc/chart 4.26.1 → 4.26.3
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/LICENSE +201 -0
- package/dist/{cdcchart-dgT_1dIT.es.js → cdcchart-DQ00cQCm.es.js} +1 -20
- package/dist/cdcchart.js +54742 -49796
- package/examples/data/data-with-metadata.json +10 -0
- 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 +2 -1
- package/examples/line-chart-states.json +1085 -0
- package/examples/metadata-variables.json +58 -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/CdcChart.tsx +8 -4
- package/src/CdcChartComponent.tsx +398 -284
- 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 +78 -0
- package/src/_stories/Chart.Defaults.stories.tsx +95 -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.SmallestLeftAxisMax.stories.tsx +64 -0
- package/src/_stories/Chart.stories.tsx +72 -1
- 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 +97 -38
- package/src/_stories/ChartBrush.Editor.stories.tsx +11 -25
- 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.Editor.stories.tsx +1 -1
- 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/_stories/_mock/paired-bar-abbr.json +421 -0
- package/src/_stories/_mock/pie_custom_colors.json +268 -0
- package/src/_stories/_mock/smallest_left_axis_max.json +104 -0
- package/src/components/Annotations/components/AnnotationDraggable.styles.css +14 -20
- 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 +12 -18
- 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 +277 -0
- package/src/components/Axis/LeftAxis.tsx +404 -0
- package/src/components/Axis/LeftAxisGridlines.tsx +77 -0
- package/src/components/Axis/PairedBarAxis.tsx +192 -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 +12 -28
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +12 -30
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +12 -31
- package/src/components/BarChart/components/BarChart.Vertical.tsx +12 -28
- package/src/components/BarChart/components/BarChart.tsx +7 -1
- package/src/components/BarChart/helpers/getPatternUrl.ts +94 -0
- package/src/components/BarChart/helpers/tests/getPatternUrl.test.ts +134 -0
- package/src/components/BarChart/helpers/useBarChart.ts +3 -0
- package/src/components/Brush/BrushSelector.tsx +155 -22
- package/src/components/Brush/MiniChartPreview.tsx +133 -21
- package/src/components/EditorPanel/EditorPanel.tsx +81 -54
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +67 -29
- package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +0 -78
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +120 -2
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +25 -43
- package/src/components/EditorPanel/components/Panels/Panel.Radar.tsx +353 -0
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +83 -3
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +66 -43
- package/src/components/EditorPanel/components/Panels/index.tsx +2 -0
- package/src/components/EditorPanel/editor-panel.scss +1 -1
- package/src/components/EditorPanel/useEditorPermissions.ts +55 -26
- package/src/components/ForestPlot/ForestPlot.tsx +26 -22
- 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/LegendGroup/LegendGroup.styles.css +4 -4
- package/src/components/Legend/LegendValueRange.tsx +77 -0
- package/src/components/Legend/helpers/createFormatLabels.tsx +16 -2
- 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 +278 -0
- package/src/components/LinearChart/tests/mockConfigContext.ts +131 -0
- package/src/components/LinearChart/utils/tickFormatting.ts +146 -0
- package/src/components/LinearChart.tsx +268 -1057
- package/src/components/PieChart/PieChart.tsx +20 -5
- 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 +6 -6
- package/src/components/Sankey/components/Sankey.tsx +3 -3
- package/src/components/Sankey/sankey.scss +1 -1
- package/src/components/SmallMultiples/SmallMultiples.css +5 -5
- package/src/components/Sparkline/index.scss +4 -2
- package/src/components/WarmingStripes/WarmingStripes.tsx +95 -25
- package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +8 -8
- package/src/data/initial-state.js +37 -15
- package/src/data/legacy-defaults.ts +18 -0
- package/src/helpers/abbreviateNumber.ts +24 -17
- package/src/helpers/getChartPatternId.ts +17 -0
- package/src/helpers/getExcludedData.ts +4 -0
- package/src/helpers/getMinMax.ts +16 -2
- package/src/helpers/handleChartAriaLabels.ts +19 -19
- package/src/helpers/handleLineType.ts +22 -18
- package/src/helpers/seriesColumnSettings.ts +114 -0
- package/src/helpers/tests/countNumOfTicks.test.ts +77 -0
- package/src/helpers/tests/seriesColumnSettings.test.ts +84 -0
- package/src/hooks/useProgrammaticTooltip.ts +23 -2
- package/src/hooks/useRightAxis.ts +14 -0
- package/src/hooks/useScales.ts +99 -56
- package/src/hooks/useTooltip.tsx +23 -3
- package/src/scss/main.scss +157 -79
- package/src/selectors/README.md +68 -0
- package/src/store/chart.reducer.ts +2 -0
- package/src/test/CdcChart.test.jsx +2 -2
- package/src/types/ChartConfig.ts +22 -0
- package/src/types/ChartContext.ts +1 -0
- package/src/types/Horizon.ts +64 -0
- package/tests/fixtures/chart-config-with-metadata.json +29 -0
- package/tests/fixtures/data-with-metadata.json +10 -0
- package/preview.html +0 -1616
- package/src/components/Annotations/components/helpers/index.tsx +0 -46
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { getPatternUrl } from '../getPatternUrl'
|
|
2
|
+
import { getChartPatternId } from '../../../../helpers/getChartPatternId'
|
|
3
|
+
|
|
4
|
+
describe('getPatternUrl', () => {
|
|
5
|
+
const pattern1Url = `url(#${getChartPatternId('Pattern1')})`
|
|
6
|
+
const pattern2Url = `url(#${getChartPatternId('Pattern2')})`
|
|
7
|
+
|
|
8
|
+
it('matches specific series patterns by series key and value', () => {
|
|
9
|
+
const patternUrl = getPatternUrl({
|
|
10
|
+
patterns: {
|
|
11
|
+
Pattern1: { dataKey: 'y1', dataValue: '19000' }
|
|
12
|
+
},
|
|
13
|
+
datum: { category: 'Q1', y1: 19000, y2: 47000 },
|
|
14
|
+
seriesKey: 'y1',
|
|
15
|
+
seriesValue: 19000,
|
|
16
|
+
seriesLabels: { y1: 'Series 1', y2: 'Series 2' }
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
expect(patternUrl).toBe(pattern1Url)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('matches specific non-series field patterns', () => {
|
|
23
|
+
const patternUrl = getPatternUrl({
|
|
24
|
+
patterns: {
|
|
25
|
+
Pattern1: { dataKey: 'category', dataValue: 'Q1' }
|
|
26
|
+
},
|
|
27
|
+
datum: { category: 'Q1', y1: 19000, y2: 47000 },
|
|
28
|
+
seriesKey: 'y2',
|
|
29
|
+
seriesValue: 47000,
|
|
30
|
+
seriesLabels: { y1: 'Series 1', y2: 'Series 2' }
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
expect(patternUrl).toBe(pattern1Url)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('does not match non-series field patterns when non-series matching is disabled', () => {
|
|
37
|
+
const patternUrl = getPatternUrl({
|
|
38
|
+
patterns: {
|
|
39
|
+
Pattern1: { dataKey: 'category', dataValue: 'Q1' }
|
|
40
|
+
},
|
|
41
|
+
datum: { category: 'Q1', y1: 19000, y2: 47000 },
|
|
42
|
+
seriesKey: 'y2',
|
|
43
|
+
seriesValue: 47000,
|
|
44
|
+
seriesLabels: { y1: 'Series 1', y2: 'Series 2' },
|
|
45
|
+
allowNonSeriesFieldMatch: false
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
expect(patternUrl).toBeNull()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('matches blank dataKey pattern by value across series', () => {
|
|
52
|
+
const patternUrl = getPatternUrl({
|
|
53
|
+
patterns: {
|
|
54
|
+
Pattern1: { dataKey: '', dataValue: '47000' }
|
|
55
|
+
},
|
|
56
|
+
datum: { category: 'Q1', y1: 19000, y2: 47000 },
|
|
57
|
+
seriesKey: 'y2',
|
|
58
|
+
seriesValue: 47000,
|
|
59
|
+
seriesLabels: { y1: 'Series 1', y2: 'Series 2' }
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
expect(patternUrl).toBe(pattern1Url)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('does not match blank dataKey pattern with empty dataValue', () => {
|
|
66
|
+
const patternUrl = getPatternUrl({
|
|
67
|
+
patterns: {
|
|
68
|
+
Pattern1: { dataKey: '', dataValue: '' }
|
|
69
|
+
},
|
|
70
|
+
datum: { category: 'Q1', y1: 19000, y2: 47000 },
|
|
71
|
+
seriesKey: 'y2',
|
|
72
|
+
seriesValue: 47000,
|
|
73
|
+
seriesLabels: { y1: 'Series 1', y2: 'Series 2' }
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
expect(patternUrl).toBeNull()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('prioritizes specific match over broad match', () => {
|
|
80
|
+
const patternUrl = getPatternUrl({
|
|
81
|
+
patterns: {
|
|
82
|
+
Pattern1: { dataKey: '', dataValue: '19000' },
|
|
83
|
+
Pattern2: { dataKey: 'y1', dataValue: '19000' }
|
|
84
|
+
},
|
|
85
|
+
datum: { category: 'Q1', y1: 19000, y2: 47000 },
|
|
86
|
+
seriesKey: 'y1',
|
|
87
|
+
seriesValue: 19000,
|
|
88
|
+
seriesLabels: { y1: 'Series 1', y2: 'Series 2' }
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
expect(patternUrl).toBe(pattern2Url)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('uses seriesKeys as fallback to identify series keys when seriesLabels are missing', () => {
|
|
95
|
+
const patternUrl = getPatternUrl({
|
|
96
|
+
patterns: {
|
|
97
|
+
Pattern1: { dataKey: 'y1', dataValue: '19000' }
|
|
98
|
+
},
|
|
99
|
+
datum: { category: 'Q1', y1: 19000, y2: 19000 },
|
|
100
|
+
seriesKey: 'y2',
|
|
101
|
+
seriesValue: 19000,
|
|
102
|
+
seriesKeys: ['y1', 'y2']
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
expect(patternUrl).toBeNull()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('sanitizes special-character pattern keys in returned url fragments', () => {
|
|
109
|
+
const patternKey = 'Pattern 1 / @ value'
|
|
110
|
+
const patternUrl = getPatternUrl({
|
|
111
|
+
patterns: {
|
|
112
|
+
[patternKey]: { dataKey: 'y1', dataValue: '19000' }
|
|
113
|
+
},
|
|
114
|
+
datum: { category: 'Q1', y1: 19000, y2: 47000 },
|
|
115
|
+
seriesKey: 'y1',
|
|
116
|
+
seriesValue: 19000,
|
|
117
|
+
seriesLabels: { y1: 'Series 1', y2: 'Series 2' }
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
expect(patternUrl).toBe(`url(#${getChartPatternId(patternKey)})`)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('creates distinct ids for keys that sanitize to the same base id', () => {
|
|
124
|
+
const keyA = 'A B'
|
|
125
|
+
const keyB = 'A@B'
|
|
126
|
+
|
|
127
|
+
const idA = getChartPatternId(keyA)
|
|
128
|
+
const idB = getChartPatternId(keyB)
|
|
129
|
+
|
|
130
|
+
expect(idA).not.toBe(idB)
|
|
131
|
+
expect(idA.startsWith('chart-pattern-A-B-')).toBe(true)
|
|
132
|
+
expect(idB.startsWith('chart-pattern-A-B-')).toBe(true)
|
|
133
|
+
})
|
|
134
|
+
})
|
|
@@ -6,6 +6,7 @@ import { getPaletteColors } from '@cdc/core/helpers/palettes/utils'
|
|
|
6
6
|
import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
|
|
7
7
|
import { getVizSubType, getVizTitle } from '@cdc/core/helpers/metrics/utils'
|
|
8
8
|
import { isMobileFontViewport } from '@cdc/core/helpers/viewports'
|
|
9
|
+
import { getSeriesOwnedColumnNames } from '../../../helpers/seriesColumnSettings'
|
|
9
10
|
|
|
10
11
|
export const useBarChart = (handleTooltipMouseOver, handleTooltipMouseOff, configContext) => {
|
|
11
12
|
const {
|
|
@@ -51,6 +52,7 @@ export const useBarChart = (handleTooltipMouseOver, handleTooltipMouseOff, confi
|
|
|
51
52
|
isBarAndLegendIsolate && seriesHighlight?.length
|
|
52
53
|
? seriesHighlight
|
|
53
54
|
: config.runtime.barSeriesKeys || config.runtime.seriesKeys
|
|
55
|
+
const seriesOwnedColumnNames = getSeriesOwnedColumnNames(config.series)
|
|
54
56
|
|
|
55
57
|
useEffect(() => {
|
|
56
58
|
if (orientation === 'horizontal' && !config.yAxis.labelPlacement) {
|
|
@@ -179,6 +181,7 @@ export const useBarChart = (handleTooltipMouseOver, handleTooltipMouseOff, confi
|
|
|
179
181
|
}) || {}
|
|
180
182
|
Object.keys(columns).forEach(colKeys => {
|
|
181
183
|
const colConfig = config.columns[colKeys]
|
|
184
|
+
if (seriesOwnedColumnNames.includes(colConfig.name || colKeys)) return
|
|
182
185
|
if (series && colConfig.series && colConfig.series !== series && !colConfig.tooltips) return
|
|
183
186
|
const formattingParams = {
|
|
184
187
|
addColPrefix: config.columns[colKeys].prefix,
|
|
@@ -2,10 +2,13 @@ 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'
|
|
11
|
+
import { getChartPatternId } from '../../helpers/getChartPatternId'
|
|
9
12
|
import MiniChartPreview from './MiniChartPreview'
|
|
10
13
|
|
|
11
14
|
interface BrushSelectorProps {
|
|
@@ -110,11 +113,77 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
|
|
|
110
113
|
const selectionRef = useRef<HTMLButtonElement>(null)
|
|
111
114
|
const rightHandleRef = useRef<HTMLButtonElement>(null)
|
|
112
115
|
|
|
113
|
-
const { tableData, config, colorScale } = useContext(ConfigContext)
|
|
116
|
+
const { tableData, config, colorScale, parseDate } = useContext(ConfigContext)
|
|
114
117
|
const dispatch = useContext(ChartDispatchContext)
|
|
115
118
|
const dataKey = config.xAxis.dataKey
|
|
116
119
|
const series = config.series || []
|
|
117
120
|
|
|
121
|
+
const renderPatternDefs = () => {
|
|
122
|
+
if (!config.legend?.patterns || Object.keys(config.legend.patterns).length === 0) {
|
|
123
|
+
return null
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<>
|
|
128
|
+
{Object.entries(config.legend.patterns).map(([key, pattern]) => {
|
|
129
|
+
const patternId = getChartPatternId(key)
|
|
130
|
+
const size = pattern.patternSize || 8
|
|
131
|
+
|
|
132
|
+
switch (pattern.shape) {
|
|
133
|
+
case 'circles':
|
|
134
|
+
return (
|
|
135
|
+
<PatternCircles
|
|
136
|
+
key={patternId}
|
|
137
|
+
id={patternId}
|
|
138
|
+
height={size}
|
|
139
|
+
width={size}
|
|
140
|
+
fill={pattern.color}
|
|
141
|
+
radius={1.25}
|
|
142
|
+
/>
|
|
143
|
+
)
|
|
144
|
+
case 'lines':
|
|
145
|
+
return (
|
|
146
|
+
<PatternLines
|
|
147
|
+
key={patternId}
|
|
148
|
+
id={patternId}
|
|
149
|
+
height={size}
|
|
150
|
+
width={size}
|
|
151
|
+
stroke={pattern.color}
|
|
152
|
+
strokeWidth={0.75}
|
|
153
|
+
orientation={['horizontal']}
|
|
154
|
+
/>
|
|
155
|
+
)
|
|
156
|
+
case 'diagonalLines':
|
|
157
|
+
return (
|
|
158
|
+
<PatternLines
|
|
159
|
+
key={patternId}
|
|
160
|
+
id={patternId}
|
|
161
|
+
height={size}
|
|
162
|
+
width={size}
|
|
163
|
+
stroke={pattern.color}
|
|
164
|
+
strokeWidth={0.75}
|
|
165
|
+
orientation={['diagonalRightToLeft']}
|
|
166
|
+
/>
|
|
167
|
+
)
|
|
168
|
+
case 'waves':
|
|
169
|
+
return (
|
|
170
|
+
<PatternWaves
|
|
171
|
+
key={patternId}
|
|
172
|
+
id={patternId}
|
|
173
|
+
height={size}
|
|
174
|
+
width={size}
|
|
175
|
+
fill={pattern.color}
|
|
176
|
+
strokeWidth={0.25}
|
|
177
|
+
/>
|
|
178
|
+
)
|
|
179
|
+
default:
|
|
180
|
+
return null
|
|
181
|
+
}
|
|
182
|
+
})}
|
|
183
|
+
</>
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
118
187
|
// Capture initial brush extent after mount and sync accessible extent
|
|
119
188
|
useEffect(() => {
|
|
120
189
|
if (brushRef.current && brushRef.current.state.extent.x0 !== -1) {
|
|
@@ -133,8 +202,22 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
|
|
|
133
202
|
return scaleBand<string>({ domain: [], range: [0, Math.max(xMax, 0)] })
|
|
134
203
|
}
|
|
135
204
|
|
|
136
|
-
const
|
|
137
|
-
|
|
205
|
+
const mappedValues = tableData.map(row => row[dataKey])
|
|
206
|
+
|
|
207
|
+
// Sort domain chronologically for date types, matching the main chart's sort behavior.
|
|
208
|
+
// Without this, data arriving in reverse chronological order renders the brush backwards.
|
|
209
|
+
const xAxisType = config?.xAxis?.type
|
|
210
|
+
let domain: string[]
|
|
211
|
+
if (xAxisType === 'date' || xAxisType === 'date-time') {
|
|
212
|
+
const sorted = [...mappedValues].sort((a, b) => {
|
|
213
|
+
const dateA = parseDate ? parseDate(a) : new Date(a)
|
|
214
|
+
const dateB = parseDate ? parseDate(b) : new Date(b)
|
|
215
|
+
return dateA - dateB
|
|
216
|
+
})
|
|
217
|
+
domain = config?.xAxis?.sortByRecentDate ? sorted.reverse() : sorted
|
|
218
|
+
} else {
|
|
219
|
+
domain = config?.xAxis?.sortByRecentDate ? [...mappedValues].reverse() : mappedValues
|
|
220
|
+
}
|
|
138
221
|
|
|
139
222
|
return scaleBand<string>({
|
|
140
223
|
domain,
|
|
@@ -142,30 +225,82 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
|
|
|
142
225
|
paddingInner: 0.1,
|
|
143
226
|
paddingOuter: 0.1
|
|
144
227
|
})
|
|
145
|
-
}, [tableData, dataKey, config?.xAxis?.sortByRecentDate, xMax])
|
|
228
|
+
}, [tableData, dataKey, config?.xAxis?.sortByRecentDate, config?.xAxis?.type, parseDate, xMax])
|
|
146
229
|
|
|
147
230
|
// Simple Y scale for brush (identity mapping)
|
|
148
231
|
const yScale = useMemo(() => scaleLinear<number>({ domain: [0, BRUSH_HEIGHT], range: [BRUSH_HEIGHT, 0] }), [])
|
|
149
232
|
|
|
150
|
-
//
|
|
233
|
+
// Helper to build a mini Y scale from a subset of series
|
|
234
|
+
const buildMiniYScale = useCallback(
|
|
235
|
+
(seriesSubset: typeof series, includeZero: boolean) => {
|
|
236
|
+
const defaultScale = scaleLinear({ domain: [0, 100], range: [BRUSH_HEIGHT - 4, 2] })
|
|
237
|
+
if (!seriesSubset.length || !tableData.length) return defaultScale
|
|
238
|
+
|
|
239
|
+
let minValue = Number.POSITIVE_INFINITY
|
|
240
|
+
let maxValue = Number.NEGATIVE_INFINITY
|
|
241
|
+
let hasValidValues = false
|
|
242
|
+
|
|
243
|
+
for (const s of seriesSubset) {
|
|
244
|
+
if (!s.dataKey) continue
|
|
245
|
+
for (const row of tableData) {
|
|
246
|
+
const value = parseFloat(row[s.dataKey])
|
|
247
|
+
if (!isNaN(value) && isFinite(value)) {
|
|
248
|
+
hasValidValues = true
|
|
249
|
+
minValue = Math.min(minValue, value)
|
|
250
|
+
maxValue = Math.max(maxValue, value)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (!hasValidValues) return defaultScale
|
|
256
|
+
|
|
257
|
+
if (includeZero) minValue = Math.min(0, minValue)
|
|
258
|
+
|
|
259
|
+
if (minValue === maxValue) {
|
|
260
|
+
const padding = Math.abs(minValue) * 0.1 || 10
|
|
261
|
+
minValue = minValue - padding
|
|
262
|
+
maxValue = maxValue + padding
|
|
263
|
+
if (minValue > 0) minValue = 0
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const domain = [minValue, maxValue]
|
|
267
|
+
return scaleLinear({ domain, range: [BRUSH_HEIGHT - 4, 2], nice: true })
|
|
268
|
+
},
|
|
269
|
+
[tableData]
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
// Determine if we have a right-axis series (dual-axis combo)
|
|
273
|
+
const hasRightAxis = useMemo(
|
|
274
|
+
() => config.visualizationType === 'Combo' && series.some(s => s.axis === 'Right'),
|
|
275
|
+
[series, config.visualizationType]
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
// Mini chart Y scale — left-axis (or all series when there's no right axis)
|
|
151
279
|
const miniYScale = useMemo(() => {
|
|
152
280
|
if (!series.length || !tableData.length) {
|
|
153
281
|
return scaleLinear({ domain: [0, 100], range: [BRUSH_HEIGHT - 4, 2] })
|
|
154
282
|
}
|
|
155
283
|
|
|
284
|
+
const barSeriesTypes = new Set(['Bar', 'Paired Bar', 'Deviation Bar', 'Combo'])
|
|
285
|
+
const hasBarSeries =
|
|
286
|
+
config.visualizationType === 'Bar' ||
|
|
287
|
+
(config.visualizationType === 'Combo' && series.some(s => barSeriesTypes.has(s.type)))
|
|
156
288
|
const isStacked =
|
|
157
289
|
config.visualizationSubType === 'stacked' &&
|
|
158
290
|
(config.visualizationType === 'Bar' || config.visualizationType === 'Area Chart')
|
|
291
|
+
|
|
292
|
+
// When dual-axis, only use left-axis series for this scale
|
|
293
|
+
const leftSeries = hasRightAxis ? series.filter(s => s.axis !== 'Right') : series
|
|
294
|
+
|
|
159
295
|
let minValue = Number.POSITIVE_INFINITY
|
|
160
296
|
let maxValue = Number.NEGATIVE_INFINITY
|
|
161
297
|
let hasValidValues = false
|
|
162
298
|
|
|
163
299
|
if (isStacked) {
|
|
164
|
-
// For stacked bars, calculate the sum of all series for each row
|
|
165
300
|
for (const row of tableData) {
|
|
166
301
|
let rowSum = 0
|
|
167
302
|
let hasRowValue = false
|
|
168
|
-
for (const s of
|
|
303
|
+
for (const s of leftSeries) {
|
|
169
304
|
if (!s.dataKey) continue
|
|
170
305
|
const value = parseFloat(row[s.dataKey])
|
|
171
306
|
if (!isNaN(value) && isFinite(value)) {
|
|
@@ -179,11 +314,9 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
|
|
|
179
314
|
maxValue = Math.max(maxValue, rowSum)
|
|
180
315
|
}
|
|
181
316
|
}
|
|
182
|
-
// For stacked charts, ensure domain starts at 0
|
|
183
317
|
minValue = Math.min(0, minValue)
|
|
184
318
|
} else {
|
|
185
|
-
|
|
186
|
-
for (const s of series) {
|
|
319
|
+
for (const s of leftSeries) {
|
|
187
320
|
if (!s.dataKey) continue
|
|
188
321
|
for (const row of tableData) {
|
|
189
322
|
const value = parseFloat(row[s.dataKey])
|
|
@@ -194,19 +327,13 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
|
|
|
194
327
|
}
|
|
195
328
|
}
|
|
196
329
|
}
|
|
197
|
-
|
|
198
|
-
if (config.visualizationType === 'Bar') {
|
|
199
|
-
minValue = Math.min(0, minValue)
|
|
200
|
-
}
|
|
330
|
+
if (hasBarSeries) minValue = Math.min(0, minValue)
|
|
201
331
|
}
|
|
202
332
|
|
|
203
|
-
// Handle edge case where all values are the same
|
|
204
333
|
if (hasValidValues && minValue === maxValue) {
|
|
205
|
-
// Create a domain with some padding around the single value
|
|
206
334
|
const padding = Math.abs(minValue) * 0.1 || 10
|
|
207
335
|
minValue = minValue - padding
|
|
208
336
|
maxValue = maxValue + padding
|
|
209
|
-
// Ensure 0 is included if we're near it
|
|
210
337
|
if (minValue > 0) minValue = 0
|
|
211
338
|
}
|
|
212
339
|
|
|
@@ -214,14 +341,18 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
|
|
|
214
341
|
return scaleLinear({ domain: [0, 100], range: [BRUSH_HEIGHT - 4, 2] })
|
|
215
342
|
}
|
|
216
343
|
|
|
217
|
-
|
|
218
|
-
if (config.visualizationType === 'Bar') {
|
|
219
|
-
minValue = Math.min(0, minValue)
|
|
220
|
-
}
|
|
344
|
+
if (hasBarSeries) minValue = Math.min(0, minValue)
|
|
221
345
|
|
|
222
346
|
const domain = minValue === maxValue ? [minValue - 1, maxValue + 1] : [minValue, maxValue]
|
|
223
347
|
return scaleLinear({ domain, range: [BRUSH_HEIGHT - 4, 2], nice: true })
|
|
224
|
-
}, [series, tableData, config.visualizationSubType, config.visualizationType])
|
|
348
|
+
}, [series, tableData, config.visualizationSubType, config.visualizationType, hasRightAxis])
|
|
349
|
+
|
|
350
|
+
// Mini chart Y scale for right-axis series (dual-axis combo charts)
|
|
351
|
+
const miniRightYScale = useMemo(() => {
|
|
352
|
+
if (!hasRightAxis) return undefined
|
|
353
|
+
const rightSeries = series.filter(s => s.axis === 'Right')
|
|
354
|
+
return buildMiniYScale(rightSeries, false)
|
|
355
|
+
}, [hasRightAxis, series, buildMiniYScale])
|
|
225
356
|
|
|
226
357
|
// Fallback: Window mouseup listener to prevent stuck drag states
|
|
227
358
|
useEffect(() => {
|
|
@@ -1107,6 +1238,7 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
|
|
|
1107
1238
|
shapeRendering='auto'
|
|
1108
1239
|
/>
|
|
1109
1240
|
</pattern>
|
|
1241
|
+
{renderPatternDefs()}
|
|
1110
1242
|
</defs>
|
|
1111
1243
|
|
|
1112
1244
|
{/* Mini chart preview */}
|
|
@@ -1118,6 +1250,7 @@ const BrushSelector: FC<BrushSelectorProps> = ({ xMax, yMax }) => {
|
|
|
1118
1250
|
dataKey={dataKey}
|
|
1119
1251
|
xScale={xScale}
|
|
1120
1252
|
miniYScale={miniYScale}
|
|
1253
|
+
miniRightYScale={miniRightYScale}
|
|
1121
1254
|
colorScale={colorScale}
|
|
1122
1255
|
config={config}
|
|
1123
1256
|
xMax={safeXMax}
|