@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,245 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { calculateLabelPosition, calculateAngle, isLowValue } from './labelPositioning'
|
|
3
|
+
|
|
4
|
+
describe('labelPositioning', () => {
|
|
5
|
+
const X_AXIS_Y = 450 // Bottom of chart
|
|
6
|
+
const VERTICAL_OFFSET = 9
|
|
7
|
+
const HORIZONTAL_OFFSET_SMALL = 4.5
|
|
8
|
+
const HORIZONTAL_OFFSET_LARGE = 9
|
|
9
|
+
|
|
10
|
+
describe('calculateAngle', () => {
|
|
11
|
+
it('should calculate 0° for horizontal right', () => {
|
|
12
|
+
const from = { x: 0, y: 100 }
|
|
13
|
+
const to = { x: 100, y: 100 }
|
|
14
|
+
expect(calculateAngle(from, to)).toBeCloseTo(0, 1)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('should calculate 90° for vertical up', () => {
|
|
18
|
+
const from = { x: 100, y: 100 }
|
|
19
|
+
const to = { x: 100, y: 0 }
|
|
20
|
+
expect(calculateAngle(from, to)).toBeCloseTo(90, 1)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('should calculate 180° for horizontal left', () => {
|
|
24
|
+
const from = { x: 100, y: 100 }
|
|
25
|
+
const to = { x: 0, y: 100 }
|
|
26
|
+
expect(calculateAngle(from, to)).toBeCloseTo(180, 1)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('should calculate 270° for vertical down', () => {
|
|
30
|
+
const from = { x: 100, y: 0 }
|
|
31
|
+
const to = { x: 100, y: 100 }
|
|
32
|
+
expect(calculateAngle(from, to)).toBeCloseTo(270, 1)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('should calculate 45° for diagonal up-right', () => {
|
|
36
|
+
const from = { x: 0, y: 100 }
|
|
37
|
+
const to = { x: 100, y: 0 }
|
|
38
|
+
expect(calculateAngle(from, to)).toBeCloseTo(45, 1)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should calculate 135° for diagonal up-left', () => {
|
|
42
|
+
const from = { x: 100, y: 100 }
|
|
43
|
+
const to = { x: 0, y: 0 }
|
|
44
|
+
expect(calculateAngle(from, to)).toBeCloseTo(135, 1)
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('isLowValue', () => {
|
|
49
|
+
it('should return true when point is ≤20px above x-axis', () => {
|
|
50
|
+
expect(isLowValue(430, 450)).toBe(true) // 20px
|
|
51
|
+
expect(isLowValue(440, 450)).toBe(true) // 10px
|
|
52
|
+
expect(isLowValue(450, 450)).toBe(true) // 0px
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('should return false when point is >20px above x-axis', () => {
|
|
56
|
+
expect(isLowValue(429, 450)).toBe(false) // 21px
|
|
57
|
+
expect(isLowValue(400, 450)).toBe(false) // 50px
|
|
58
|
+
expect(isLowValue(350, 450)).toBe(false) // 100px
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe('calculateLabelPosition - First Point', () => {
|
|
63
|
+
it('should position above when ending segment is in Q3', () => {
|
|
64
|
+
const pointY = 100
|
|
65
|
+
const endingAngle = 225 // Q3: downward-left
|
|
66
|
+
const result = calculateLabelPosition(null, endingAngle, pointY, X_AXIS_Y)
|
|
67
|
+
|
|
68
|
+
expect(result.offsetX).toBe(0)
|
|
69
|
+
expect(result.offsetY).toBe(-VERTICAL_OFFSET)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should position below when ending segment is in Q1 and point is high', () => {
|
|
73
|
+
const pointY = 100 // Far from x-axis
|
|
74
|
+
const endingAngle = 45 // Q1: upward-right
|
|
75
|
+
const result = calculateLabelPosition(null, endingAngle, pointY, X_AXIS_Y)
|
|
76
|
+
|
|
77
|
+
expect(result.offsetX).toBe(0)
|
|
78
|
+
expect(result.offsetY).toBe(VERTICAL_OFFSET)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('should position above+left when ending segment is in Q1 and point is low', () => {
|
|
82
|
+
const pointY = 440 // Near x-axis (10px)
|
|
83
|
+
const endingAngle = 45 // Q1: upward-right
|
|
84
|
+
const result = calculateLabelPosition(null, endingAngle, pointY, X_AXIS_Y)
|
|
85
|
+
|
|
86
|
+
expect(result.offsetX).toBe(-HORIZONTAL_OFFSET_SMALL)
|
|
87
|
+
expect(result.offsetY).toBe(-VERTICAL_OFFSET)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('should position above when ending segment is in Q2 or Q4 (default)', () => {
|
|
91
|
+
const pointY = 100
|
|
92
|
+
const endingAngleQ2 = 135 // Q2
|
|
93
|
+
const endingAngleQ4 = 315 // Q4
|
|
94
|
+
|
|
95
|
+
const resultQ2 = calculateLabelPosition(null, endingAngleQ2, pointY, X_AXIS_Y)
|
|
96
|
+
const resultQ4 = calculateLabelPosition(null, endingAngleQ4, pointY, X_AXIS_Y)
|
|
97
|
+
|
|
98
|
+
expect(resultQ2.offsetX).toBe(0)
|
|
99
|
+
expect(resultQ2.offsetY).toBe(-VERTICAL_OFFSET)
|
|
100
|
+
expect(resultQ4.offsetX).toBe(0)
|
|
101
|
+
expect(resultQ4.offsetY).toBe(-VERTICAL_OFFSET)
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe('calculateLabelPosition - Last Point', () => {
|
|
106
|
+
it('should position above when starting segment is in Q4', () => {
|
|
107
|
+
const pointY = 100
|
|
108
|
+
const startingAngle = 315 // Q4: downward-right (looking back)
|
|
109
|
+
const result = calculateLabelPosition(startingAngle, null, pointY, X_AXIS_Y)
|
|
110
|
+
|
|
111
|
+
expect(result.offsetX).toBe(0)
|
|
112
|
+
expect(result.offsetY).toBe(-VERTICAL_OFFSET)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('should position below when starting segment is in Q2 and point is high', () => {
|
|
116
|
+
const pointY = 100 // Far from x-axis
|
|
117
|
+
const startingAngle = 135 // Q2: upward-left (looking back)
|
|
118
|
+
const result = calculateLabelPosition(startingAngle, null, pointY, X_AXIS_Y)
|
|
119
|
+
|
|
120
|
+
expect(result.offsetX).toBe(0)
|
|
121
|
+
expect(result.offsetY).toBe(VERTICAL_OFFSET)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('should position above+right when starting segment is in Q2 and point is low', () => {
|
|
125
|
+
const pointY = 440 // Near x-axis (10px)
|
|
126
|
+
const startingAngle = 135 // Q2: upward-left (looking back)
|
|
127
|
+
const result = calculateLabelPosition(startingAngle, null, pointY, X_AXIS_Y)
|
|
128
|
+
|
|
129
|
+
expect(result.offsetX).toBe(HORIZONTAL_OFFSET_SMALL)
|
|
130
|
+
expect(result.offsetY).toBe(-VERTICAL_OFFSET)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('should position below when starting segment is in Q1 and point is high', () => {
|
|
134
|
+
const pointY = 100 // Far from x-axis
|
|
135
|
+
const startingAngle = 45 // Q1: upward-right (looking back)
|
|
136
|
+
const result = calculateLabelPosition(startingAngle, null, pointY, X_AXIS_Y)
|
|
137
|
+
|
|
138
|
+
expect(result.offsetX).toBe(0)
|
|
139
|
+
expect(result.offsetY).toBe(VERTICAL_OFFSET)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('should position above+right when starting segment is in Q1 and point is low', () => {
|
|
143
|
+
const pointY = 440 // Near x-axis (10px)
|
|
144
|
+
const startingAngle = 45 // Q1: upward-right (looking back)
|
|
145
|
+
const result = calculateLabelPosition(startingAngle, null, pointY, X_AXIS_Y)
|
|
146
|
+
|
|
147
|
+
expect(result.offsetX).toBe(HORIZONTAL_OFFSET_SMALL)
|
|
148
|
+
expect(result.offsetY).toBe(-VERTICAL_OFFSET)
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
describe('calculateLabelPosition - Middle Point', () => {
|
|
153
|
+
it('should position above when start=Q3 and end=Q4 (valley bottom)', () => {
|
|
154
|
+
const pointY = 100
|
|
155
|
+
const startingAngle = 225 // Q3: looking back to upper-left
|
|
156
|
+
const endingAngle = 315 // Q4: looking forward to lower-right
|
|
157
|
+
const result = calculateLabelPosition(startingAngle, endingAngle, pointY, X_AXIS_Y)
|
|
158
|
+
|
|
159
|
+
expect(result.offsetX).toBe(0)
|
|
160
|
+
expect(result.offsetY).toBe(-VERTICAL_OFFSET)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('should position below when start=Q2 and end=Q1 and point is high', () => {
|
|
164
|
+
const pointY = 100 // Far from x-axis
|
|
165
|
+
const startingAngle = 135 // Q2: looking back to upper-left
|
|
166
|
+
const endingAngle = 45 // Q1: looking forward to upper-right
|
|
167
|
+
const result = calculateLabelPosition(startingAngle, endingAngle, pointY, X_AXIS_Y)
|
|
168
|
+
|
|
169
|
+
expect(result.offsetX).toBe(0)
|
|
170
|
+
expect(result.offsetY).toBe(VERTICAL_OFFSET)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('should position above when start=Q2, end=Q1, low-value, angle ≥135°', () => {
|
|
174
|
+
const pointY = 440 // Near x-axis (10px)
|
|
175
|
+
const startingAngle = 160 // Q2
|
|
176
|
+
const endingAngle = 20 // Q1
|
|
177
|
+
// Angle between = 140° (≥135°)
|
|
178
|
+
const result = calculateLabelPosition(startingAngle, endingAngle, pointY, X_AXIS_Y)
|
|
179
|
+
|
|
180
|
+
expect(result.offsetX).toBe(0)
|
|
181
|
+
expect(result.offsetY).toBe(-VERTICAL_OFFSET)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('should position above+right when start=Q2, end=Q1, low-value, angle <135°, ending ≥68°', () => {
|
|
185
|
+
const pointY = 440 // Near x-axis (10px)
|
|
186
|
+
const startingAngle = 100 // Q2
|
|
187
|
+
const endingAngle = 70 // Q1 and ≥68°
|
|
188
|
+
const result = calculateLabelPosition(startingAngle, endingAngle, pointY, X_AXIS_Y)
|
|
189
|
+
|
|
190
|
+
expect(result.offsetX).toBe(HORIZONTAL_OFFSET_LARGE)
|
|
191
|
+
expect(result.offsetY).toBe(-VERTICAL_OFFSET)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('should position above+left when start=Q2, end=Q1, low-value, angle <135°, ending <68°', () => {
|
|
195
|
+
const pointY = 440 // Near x-axis (10px)
|
|
196
|
+
const startingAngle = 100 // Q2
|
|
197
|
+
const endingAngle = 50 // Q1 and <68°
|
|
198
|
+
const result = calculateLabelPosition(startingAngle, endingAngle, pointY, X_AXIS_Y)
|
|
199
|
+
|
|
200
|
+
expect(result.offsetX).toBe(-HORIZONTAL_OFFSET_LARGE)
|
|
201
|
+
expect(result.offsetY).toBe(-VERTICAL_OFFSET)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('should position above+left when start=Q3 and end=Q1', () => {
|
|
205
|
+
const pointY = 100
|
|
206
|
+
const startingAngle = 225 // Q3
|
|
207
|
+
const endingAngle = 45 // Q1
|
|
208
|
+
const result = calculateLabelPosition(startingAngle, endingAngle, pointY, X_AXIS_Y)
|
|
209
|
+
|
|
210
|
+
expect(result.offsetX).toBe(-HORIZONTAL_OFFSET_LARGE)
|
|
211
|
+
expect(result.offsetY).toBe(-VERTICAL_OFFSET)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('should position above+right when start=Q2 and end=Q4', () => {
|
|
215
|
+
const pointY = 100
|
|
216
|
+
const startingAngle = 135 // Q2
|
|
217
|
+
const endingAngle = 315 // Q4
|
|
218
|
+
const result = calculateLabelPosition(startingAngle, endingAngle, pointY, X_AXIS_Y)
|
|
219
|
+
|
|
220
|
+
expect(result.offsetX).toBe(HORIZONTAL_OFFSET_LARGE)
|
|
221
|
+
expect(result.offsetY).toBe(-VERTICAL_OFFSET)
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
describe('Edge cases', () => {
|
|
226
|
+
it('should handle point exactly at x-axis (value=0)', () => {
|
|
227
|
+
const pointY = 450 // Exactly at x-axis
|
|
228
|
+
const startingAngle = 135 // Q2
|
|
229
|
+
const endingAngle = 45 // Q1
|
|
230
|
+
const result = calculateLabelPosition(startingAngle, endingAngle, pointY, X_AXIS_Y)
|
|
231
|
+
|
|
232
|
+
// Should still follow low-value rules
|
|
233
|
+
expect(result.offsetY).toBe(-VERTICAL_OFFSET)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('should return default position above when no specific rule matches', () => {
|
|
237
|
+
const pointY = 100
|
|
238
|
+
const startingAngle = 180 // Q3
|
|
239
|
+
const endingAngle = 180 // Q3 (unusual but possible)
|
|
240
|
+
const result = calculateLabelPosition(startingAngle, endingAngle, pointY, X_AXIS_Y)
|
|
241
|
+
|
|
242
|
+
expect(result.offsetY).toBe(-VERTICAL_OFFSET)
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
})
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line Chart Label Positioning Algorithm
|
|
3
|
+
*
|
|
4
|
+
* Implements quadrant-based label positioning to prevent overlaps with line segments.
|
|
5
|
+
* Uses segment direction analysis and vertical position to determine optimal label placement.
|
|
6
|
+
*
|
|
7
|
+
* Quadrant System (Standard Cartesian):
|
|
8
|
+
* - Q1 (0°-90°): upper-right
|
|
9
|
+
* - Q2 (90°-180°): upper-left
|
|
10
|
+
* - Q3 (180°-270°): lower-left
|
|
11
|
+
* - Q4 (270°-360°): lower-right
|
|
12
|
+
*
|
|
13
|
+
* Angle System: Standard polar coordinates (0° = right/east, counterclockwise)
|
|
14
|
+
* Note: In SVG coordinate space, Y increases downward, so angles are inverted
|
|
15
|
+
*
|
|
16
|
+
* Segment Constraints:
|
|
17
|
+
* - Starting segments (left quadrants) should be in Q2 or Q3
|
|
18
|
+
* - Ending segments (right quadrants) should be in Q1 or Q4
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// Constants
|
|
22
|
+
const VERTICAL_OFFSET = 9 // 0.5rem in pixels
|
|
23
|
+
const HORIZONTAL_OFFSET_SMALL = 4.5 // 0.25rem in pixels
|
|
24
|
+
const HORIZONTAL_OFFSET_LARGE = 9 // 0.5rem in pixels
|
|
25
|
+
const LOW_VALUE_THRESHOLD = 20 // pixels from x-axis
|
|
26
|
+
|
|
27
|
+
interface Point {
|
|
28
|
+
x: number
|
|
29
|
+
y: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface LabelOffset {
|
|
33
|
+
dx: number // horizontal offset
|
|
34
|
+
dy: number // vertical offset (negative = up, positive = down)
|
|
35
|
+
textAnchor?: 'start' | 'middle' | 'end'
|
|
36
|
+
verticalAnchor?: 'start' | 'middle' | 'end'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Determine quadrant for an angle (1-4) using standard Cartesian plane
|
|
41
|
+
* Q1 (0°-90°): upper-right
|
|
42
|
+
* Q2 (90°-180°): upper-left
|
|
43
|
+
* Q3 (180°-270°): lower-left
|
|
44
|
+
* Q4 (270°-360°): lower-right
|
|
45
|
+
*/
|
|
46
|
+
function getQuadrant(angle: number): number {
|
|
47
|
+
const normalized = ((angle % 360) + 360) % 360
|
|
48
|
+
if (normalized >= 0 && normalized < 90) return 1
|
|
49
|
+
if (normalized >= 90 && normalized < 180) return 2
|
|
50
|
+
if (normalized >= 180 && normalized < 270) return 3
|
|
51
|
+
if (normalized >= 270 && normalized < 360) return 4
|
|
52
|
+
return 1 // Edge case: 360° maps to 0° → Q1
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Calculate the angle between two segments
|
|
57
|
+
* Returns the absolute angular difference, normalized to 0°-180°
|
|
58
|
+
*/
|
|
59
|
+
function getAngleBetweenSegments(startAngle: number, endAngle: number): number {
|
|
60
|
+
let diff = Math.abs(endAngle - startAngle)
|
|
61
|
+
if (diff > 180) {
|
|
62
|
+
diff = 360 - diff
|
|
63
|
+
}
|
|
64
|
+
return diff
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if a point is low-value (≤20px above x-axis)
|
|
69
|
+
*/
|
|
70
|
+
export function isLowValue(yPosition: number, xAxisY: number): boolean {
|
|
71
|
+
const distanceFromXAxis = xAxisY - yPosition
|
|
72
|
+
return distanceFromXAxis <= LOW_VALUE_THRESHOLD
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Calculate the angle in degrees between two points
|
|
77
|
+
* Returns angle in range [0, 360) using standard polar coordinates:
|
|
78
|
+
* - 0° = right (east)
|
|
79
|
+
* - 90° = up (north in Cartesian, but note SVG y-axis is inverted)
|
|
80
|
+
* - 180° = left (west)
|
|
81
|
+
* - 270° = down (south in Cartesian)
|
|
82
|
+
*/
|
|
83
|
+
export function calculateAngle(fromPoint: Point, toPoint: Point): number {
|
|
84
|
+
const dx = toPoint.x - fromPoint.x
|
|
85
|
+
const dy = toPoint.y - fromPoint.y
|
|
86
|
+
|
|
87
|
+
// atan2 returns angle in radians from -PI to PI
|
|
88
|
+
// Negative dy because SVG y-axis is inverted (y increases downward)
|
|
89
|
+
let angleRad = Math.atan2(-dy, dx)
|
|
90
|
+
let angleDeg = angleRad * (180 / Math.PI)
|
|
91
|
+
|
|
92
|
+
// Normalize to [0, 360)
|
|
93
|
+
if (angleDeg < 0) {
|
|
94
|
+
angleDeg += 360
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return angleDeg
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Main label positioning function
|
|
102
|
+
*
|
|
103
|
+
* Calculates optimal label position based on segment directions and point position
|
|
104
|
+
*
|
|
105
|
+
* @param startingSegmentAngle - Angle from previous point to current (null for first point)
|
|
106
|
+
* @param endingSegmentAngle - Angle from current point to next (null for last point)
|
|
107
|
+
* @param pointY - Y coordinate of current point
|
|
108
|
+
* @param xAxisY - Y coordinate of the x-axis
|
|
109
|
+
* @param value - Optional data value for debugging purposes
|
|
110
|
+
*/
|
|
111
|
+
export function calculateLabelPosition(
|
|
112
|
+
startingSegmentAngle: number | null,
|
|
113
|
+
endingSegmentAngle: number | null,
|
|
114
|
+
pointY: number,
|
|
115
|
+
xAxisY: number,
|
|
116
|
+
value?: string | number
|
|
117
|
+
): { offsetX: number; offsetY: number } {
|
|
118
|
+
const isNearXAxis = isLowValue(pointY, xAxisY)
|
|
119
|
+
|
|
120
|
+
// ===== FIRST POINT (only ending segment: current → next) =====
|
|
121
|
+
if (startingSegmentAngle === null && endingSegmentAngle !== null) {
|
|
122
|
+
const quad = getQuadrant(endingSegmentAngle)
|
|
123
|
+
|
|
124
|
+
// Position label 0.5rem / 9px above point when segment is in Quadrant 3
|
|
125
|
+
if (quad === 3) {
|
|
126
|
+
return { offsetX: 0, offsetY: -VERTICAL_OFFSET }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// If the point isn't near the x-axis, position label 0.5rem / 9px below point when segment is in Quadrant 1
|
|
130
|
+
if (quad === 1 && !isNearXAxis) {
|
|
131
|
+
return { offsetX: 0, offsetY: VERTICAL_OFFSET }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// If point is 20 pixels or less above x-axis (i.e., it's a low value and close to 0)
|
|
135
|
+
// position label 0.5rem / 9px above and 0.25rem / 4.5px to the left of point
|
|
136
|
+
if (quad === 1 && isNearXAxis) {
|
|
137
|
+
return { offsetX: -HORIZONTAL_OFFSET_SMALL, offsetY: -VERTICAL_OFFSET }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Default for Q2 and Q4: position above
|
|
141
|
+
return { offsetX: 0, offsetY: -VERTICAL_OFFSET }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ===== LAST POINT (only starting segment: previous → current) =====
|
|
145
|
+
// Mirrors the first point logic
|
|
146
|
+
if (startingSegmentAngle !== null && endingSegmentAngle === null) {
|
|
147
|
+
const quad = getQuadrant(startingSegmentAngle)
|
|
148
|
+
|
|
149
|
+
// Q4 mirrors first point Q3: position above
|
|
150
|
+
// This also works for when last data point is near x-axis
|
|
151
|
+
if (quad === 4) {
|
|
152
|
+
return { offsetX: 0, offsetY: -VERTICAL_OFFSET }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Q2 matches the quadrant.txt rules: below if high, above+right if low
|
|
156
|
+
if (quad === 2) {
|
|
157
|
+
if (!isNearXAxis) {
|
|
158
|
+
// Point >20px above x-axis: position below
|
|
159
|
+
return { offsetX: 0, offsetY: VERTICAL_OFFSET }
|
|
160
|
+
} else {
|
|
161
|
+
// Point ≤20px above x-axis: position above + 0.25rem right
|
|
162
|
+
return { offsetX: HORIZONTAL_OFFSET_SMALL, offsetY: -VERTICAL_OFFSET }
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Q1: mirror first point Q1 logic for symmetry in Q1 stories
|
|
167
|
+
if (quad === 1) {
|
|
168
|
+
if (!isNearXAxis) {
|
|
169
|
+
// Point >20px above x-axis: position below
|
|
170
|
+
return { offsetX: 0, offsetY: VERTICAL_OFFSET }
|
|
171
|
+
} else {
|
|
172
|
+
// Point ≤20px above x-axis: position above + 0.25rem right
|
|
173
|
+
return { offsetX: HORIZONTAL_OFFSET_SMALL, offsetY: -VERTICAL_OFFSET }
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Default for Q3: position above
|
|
178
|
+
return { offsetX: 0, offsetY: -VERTICAL_OFFSET }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ===== MIDDLE POINT (both segments) =====
|
|
182
|
+
if (startingSegmentAngle !== null && endingSegmentAngle !== null) {
|
|
183
|
+
const startQ = getQuadrant(startingSegmentAngle)
|
|
184
|
+
const endQ = getQuadrant(endingSegmentAngle)
|
|
185
|
+
const angleBetween = getAngleBetweenSegments(startingSegmentAngle, endingSegmentAngle)
|
|
186
|
+
|
|
187
|
+
// Position label 0.5rem / 9px above point when starting segment is in Quadrant 3
|
|
188
|
+
// and ending segment is in Quadrant 4 and the angle created is 0°–180°
|
|
189
|
+
if (startQ === 3 && endQ === 4 && angleBetween >= 0 && angleBetween <= 180) {
|
|
190
|
+
return { offsetX: 0, offsetY: -VERTICAL_OFFSET }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// If the point isn't near the x-axis, position label 0.5rem / 9px below it
|
|
194
|
+
// when starting segment is in Quadrant 2 and ending segment is in Quadrant 1
|
|
195
|
+
// and the angle created is 1°–179°
|
|
196
|
+
if (startQ === 2 && endQ === 1 && angleBetween >= 1 && angleBetween <= 179 && !isNearXAxis) {
|
|
197
|
+
return { offsetX: 0, offsetY: VERTICAL_OFFSET }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// If the point is 20 pixels or less above x-axis (i.e., it's a low value and close to 0)
|
|
201
|
+
// and starting segment is in Quadrant 2 and ending segment is in Quadrant 1
|
|
202
|
+
// then calculate the angle between the 2 segments:
|
|
203
|
+
if (startQ === 2 && endQ === 1 && angleBetween >= 1 && angleBetween <= 179 && isNearXAxis) {
|
|
204
|
+
// If it's 135°–180° then position the label 0.5rem / 9px above the point
|
|
205
|
+
if (angleBetween >= 135 && angleBetween <= 180) {
|
|
206
|
+
return { offsetX: 0, offsetY: -VERTICAL_OFFSET }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// If it's less than 135°, calculate the angle created by the ending segment
|
|
210
|
+
if (angleBetween < 135) {
|
|
211
|
+
// If it's 68° or more then position the label 0.5rem / 9px above and 0.5rem / 9px to the right of the point
|
|
212
|
+
if (endingSegmentAngle >= 68) {
|
|
213
|
+
return { offsetX: HORIZONTAL_OFFSET_LARGE, offsetY: -VERTICAL_OFFSET }
|
|
214
|
+
}
|
|
215
|
+
// Otherwise, position the label 0.5rem / 9px above and 0.5rem / 9px to the left of the point
|
|
216
|
+
else {
|
|
217
|
+
return { offsetX: -HORIZONTAL_OFFSET_LARGE, offsetY: -VERTICAL_OFFSET }
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Position the label 0.5rem / 9px above and 0.5rem / 9px to the left of the point
|
|
223
|
+
// if starting segment is in Quadrant 3 and ending segment is in Quadrant 1
|
|
224
|
+
// and the angle created is 92°–269°
|
|
225
|
+
if (startQ === 3 && endQ === 1 && angleBetween >= 92 && angleBetween <= 269) {
|
|
226
|
+
return { offsetX: -HORIZONTAL_OFFSET_LARGE, offsetY: -VERTICAL_OFFSET }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Position the label 0.5rem / 9px above and 0.5rem / 9px to the right of the point
|
|
230
|
+
// if starting segment is in Quadrant 2 and ending segment is in Quadrant 4
|
|
231
|
+
// and the angle created is 92°–269°
|
|
232
|
+
if (startQ === 2 && endQ === 4 && angleBetween >= 92 && angleBetween <= 269) {
|
|
233
|
+
return { offsetX: HORIZONTAL_OFFSET_LARGE, offsetY: -VERTICAL_OFFSET }
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Default fallback: position above
|
|
238
|
+
return { offsetX: 0, offsetY: -VERTICAL_OFFSET }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Get text anchor based on horizontal offset
|
|
243
|
+
*/
|
|
244
|
+
function getTextAnchor(offsetX: number): 'start' | 'middle' | 'end' {
|
|
245
|
+
if (offsetX > 0) return 'start'
|
|
246
|
+
if (offsetX < 0) return 'end'
|
|
247
|
+
return 'middle'
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Get vertical anchor based on vertical offset
|
|
252
|
+
*/
|
|
253
|
+
function getVerticalAnchor(offsetY: number): 'start' | 'middle' | 'end' {
|
|
254
|
+
if (offsetY > 0) return 'start' // Below point
|
|
255
|
+
if (offsetY < 0) return 'end' // Above point
|
|
256
|
+
return 'middle'
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Helper function to get label position for a data point in a series
|
|
261
|
+
*/
|
|
262
|
+
export function getLabelPositionForDataPoint(
|
|
263
|
+
dataPoints: Point[],
|
|
264
|
+
dataIndex: number,
|
|
265
|
+
xAxisY: number,
|
|
266
|
+
value?: string | number
|
|
267
|
+
): LabelOffset {
|
|
268
|
+
const currentPoint = dataPoints[dataIndex]
|
|
269
|
+
const prevPoint = dataPoints[dataIndex - 1]
|
|
270
|
+
const nextPoint = dataPoints[dataIndex + 1]
|
|
271
|
+
|
|
272
|
+
// Calculate angles for segments connected to this point
|
|
273
|
+
// Starting segment: angle looking BACK at where we came from (current → previous)
|
|
274
|
+
// This ensures starting segments are in left quadrants (Q2/Q3)
|
|
275
|
+
const startingSegmentAngle = prevPoint ? calculateAngle(currentPoint, prevPoint) : null
|
|
276
|
+
|
|
277
|
+
// Ending segment: angle looking FORWARD to where we're going (current → next)
|
|
278
|
+
// This ensures ending segments are in right quadrants (Q1/Q4)
|
|
279
|
+
const endingSegmentAngle = nextPoint ? calculateAngle(currentPoint, nextPoint) : null
|
|
280
|
+
|
|
281
|
+
const position = calculateLabelPosition(startingSegmentAngle, endingSegmentAngle, currentPoint.y, xAxisY, value)
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
dx: position.offsetX,
|
|
285
|
+
dy: position.offsetY,
|
|
286
|
+
textAnchor: getTextAnchor(position.offsetX),
|
|
287
|
+
verticalAnchor: getVerticalAnchor(position.offsetY)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Legacy compatibility
|
|
293
|
+
* @deprecated Use calculateLabelPosition instead
|
|
294
|
+
*/
|
|
295
|
+
export function calculateLabelOffset(
|
|
296
|
+
pointIndex: number,
|
|
297
|
+
pointY: number,
|
|
298
|
+
startingSegmentAngle: number | null,
|
|
299
|
+
endingSegmentAngle: number | null,
|
|
300
|
+
xAxisY: number,
|
|
301
|
+
value?: string | number
|
|
302
|
+
): { offsetX: number; offsetY: number } {
|
|
303
|
+
return calculateLabelPosition(startingSegmentAngle, endingSegmentAngle, pointY, xAxisY, value)
|
|
304
|
+
}
|
|
@@ -15,6 +15,7 @@ import useRightAxis from '../../hooks/useRightAxis'
|
|
|
15
15
|
|
|
16
16
|
// Local helpers and components
|
|
17
17
|
import { filterCircles, createStyles, createDataSegments } from './helpers'
|
|
18
|
+
import { getLabelPositionForDataPoint } from './helpers/labelPositioning'
|
|
18
19
|
import LineChartCircle from './components/LineChart.Circle'
|
|
19
20
|
import LineChartBumpCircle from './components/LineChart.BumpCircle'
|
|
20
21
|
import isNumber from '@cdc/core/helpers/isNumber'
|
|
@@ -128,20 +129,55 @@ const LineChart = (props: LineChartProps) => {
|
|
|
128
129
|
xValue: d[config.xAxis.dataKey],
|
|
129
130
|
yValue: d[_seriesKey]
|
|
130
131
|
})
|
|
132
|
+
|
|
133
|
+
// Build array of point coordinates for intelligent label positioning
|
|
134
|
+
const dataPointsForSeries = _data
|
|
135
|
+
.filter(item => isNumber(item[_seriesKey]))
|
|
136
|
+
.map(item => ({
|
|
137
|
+
x: xPos(item),
|
|
138
|
+
y:
|
|
139
|
+
seriesAxis === 'Right'
|
|
140
|
+
? yScaleRight(getYAxisData(item, _seriesKey))
|
|
141
|
+
: yScale(getYAxisData(item, _seriesKey))
|
|
142
|
+
}))
|
|
143
|
+
|
|
144
|
+
// Find the index in the filtered array (points with valid data only)
|
|
145
|
+
const filteredIndex = dataPointsForSeries.findIndex(
|
|
146
|
+
point =>
|
|
147
|
+
point.x === xPos(d) &&
|
|
148
|
+
point.y ===
|
|
149
|
+
(seriesAxis === 'Right'
|
|
150
|
+
? yScaleRight(getYAxisData(d, _seriesKey))
|
|
151
|
+
: yScale(getYAxisData(d, _seriesKey)))
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
// Calculate label position
|
|
155
|
+
const labelValue = d[_seriesKey]
|
|
156
|
+
const xAxisY = seriesAxis === 'Right' ? yScaleRight(0) : yScale(0)
|
|
157
|
+
|
|
158
|
+
// Use intelligent label positioning if enabled, otherwise use simple default
|
|
159
|
+
const labelOffset =
|
|
160
|
+
config.general?.useIntelligentLineChartLabels && filteredIndex >= 0
|
|
161
|
+
? getLabelPositionForDataPoint(dataPointsForSeries, filteredIndex, xAxisY, labelValue)
|
|
162
|
+
: { dx: 0, dy: -9, textAnchor: 'middle' as const, verticalAnchor: 'end' as const }
|
|
163
|
+
|
|
164
|
+
const baseX = xPos(d)
|
|
165
|
+
const baseY =
|
|
166
|
+
seriesAxis === 'Right'
|
|
167
|
+
? yScaleRight(getYAxisData(d, _seriesKey))
|
|
168
|
+
: yScale(getYAxisData(d, _seriesKey))
|
|
169
|
+
|
|
131
170
|
return (
|
|
132
171
|
isNumber(d[_seriesKey]) && (
|
|
133
172
|
<React.Fragment key={`series-${seriesKey}-point-${dataIndex}`}>
|
|
134
|
-
{/* Render label */}
|
|
173
|
+
{/* Render label with intelligent positioning */}
|
|
135
174
|
{config.labels && (
|
|
136
175
|
<Text
|
|
137
|
-
x={
|
|
138
|
-
y={
|
|
139
|
-
seriesAxis === 'Right'
|
|
140
|
-
? yScaleRight(getYAxisData(d, _seriesKey))
|
|
141
|
-
: yScale(getYAxisData(d, _seriesKey))
|
|
142
|
-
}
|
|
176
|
+
x={baseX + labelOffset.dx}
|
|
177
|
+
y={baseY + labelOffset.dy}
|
|
143
178
|
fill={'#000'}
|
|
144
|
-
textAnchor='middle'
|
|
179
|
+
textAnchor={labelOffset.textAnchor || 'middle'}
|
|
180
|
+
verticalAnchor={labelOffset.verticalAnchor || 'middle'}
|
|
145
181
|
>
|
|
146
182
|
{formatNumber(d[_seriesKey], 'left')}
|
|
147
183
|
</Text>
|