@gravity-ui/charts 1.44.0 → 1.45.0

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.
Files changed (77) hide show
  1. package/dist/cjs/core/constants/defaults/annotation.d.ts +12 -0
  2. package/dist/cjs/core/constants/defaults/annotation.js +12 -0
  3. package/dist/cjs/core/constants/defaults/index.d.ts +1 -0
  4. package/dist/cjs/core/constants/defaults/index.js +1 -0
  5. package/dist/cjs/core/series/constants.d.ts +1 -1
  6. package/dist/cjs/core/series/constants.js +1 -1
  7. package/dist/cjs/core/series/prepare-annotation.d.ts +12 -0
  8. package/dist/cjs/core/series/prepare-annotation.js +31 -0
  9. package/dist/cjs/core/series/types.d.ts +16 -0
  10. package/dist/cjs/core/types/chart/annotation.d.ts +45 -0
  11. package/dist/cjs/core/types/chart/annotation.js +1 -0
  12. package/dist/cjs/core/types/chart/area.d.ts +8 -0
  13. package/dist/cjs/core/types/chart/bar-x.d.ts +6 -0
  14. package/dist/cjs/core/types/chart/line.d.ts +8 -0
  15. package/dist/cjs/core/types/chart/marker.d.ts +6 -4
  16. package/dist/cjs/core/types/chart/series.d.ts +7 -0
  17. package/dist/cjs/core/types/chart/tooltip.d.ts +1 -0
  18. package/dist/cjs/core/types/index.d.ts +1 -0
  19. package/dist/cjs/core/types/index.js +1 -0
  20. package/dist/cjs/core/utils/text.d.ts +8 -0
  21. package/dist/cjs/core/utils/text.js +9 -1
  22. package/dist/cjs/hooks/useShapes/annotation/index.d.ts +14 -0
  23. package/dist/cjs/hooks/useShapes/annotation/index.js +200 -0
  24. package/dist/cjs/hooks/useShapes/area/index.d.ts +2 -0
  25. package/dist/cjs/hooks/useShapes/area/index.js +21 -2
  26. package/dist/cjs/hooks/useShapes/area/prepare-data.d.ts +2 -1
  27. package/dist/cjs/hooks/useShapes/area/prepare-data.js +38 -20
  28. package/dist/cjs/hooks/useShapes/area/types.d.ts +4 -0
  29. package/dist/cjs/hooks/useShapes/bar-x/index.d.ts +2 -0
  30. package/dist/cjs/hooks/useShapes/bar-x/index.js +30 -2
  31. package/dist/cjs/hooks/useShapes/bar-x/prepare-data.js +10 -2
  32. package/dist/cjs/hooks/useShapes/bar-x/types.d.ts +2 -0
  33. package/dist/cjs/hooks/useShapes/index.js +5 -3
  34. package/dist/cjs/hooks/useShapes/line/index.d.ts +2 -0
  35. package/dist/cjs/hooks/useShapes/line/index.js +21 -7
  36. package/dist/cjs/hooks/useShapes/line/prepare-data.d.ts +2 -1
  37. package/dist/cjs/hooks/useShapes/line/prepare-data.js +28 -10
  38. package/dist/cjs/hooks/useShapes/line/types.d.ts +4 -0
  39. package/dist/esm/core/constants/defaults/annotation.d.ts +12 -0
  40. package/dist/esm/core/constants/defaults/annotation.js +12 -0
  41. package/dist/esm/core/constants/defaults/index.d.ts +1 -0
  42. package/dist/esm/core/constants/defaults/index.js +1 -0
  43. package/dist/esm/core/series/constants.d.ts +1 -1
  44. package/dist/esm/core/series/constants.js +1 -1
  45. package/dist/esm/core/series/prepare-annotation.d.ts +12 -0
  46. package/dist/esm/core/series/prepare-annotation.js +31 -0
  47. package/dist/esm/core/series/types.d.ts +16 -0
  48. package/dist/esm/core/types/chart/annotation.d.ts +45 -0
  49. package/dist/esm/core/types/chart/annotation.js +1 -0
  50. package/dist/esm/core/types/chart/area.d.ts +8 -0
  51. package/dist/esm/core/types/chart/bar-x.d.ts +6 -0
  52. package/dist/esm/core/types/chart/line.d.ts +8 -0
  53. package/dist/esm/core/types/chart/marker.d.ts +6 -4
  54. package/dist/esm/core/types/chart/series.d.ts +7 -0
  55. package/dist/esm/core/types/chart/tooltip.d.ts +1 -0
  56. package/dist/esm/core/types/index.d.ts +1 -0
  57. package/dist/esm/core/types/index.js +1 -0
  58. package/dist/esm/core/utils/text.d.ts +8 -0
  59. package/dist/esm/core/utils/text.js +9 -1
  60. package/dist/esm/hooks/useShapes/annotation/index.d.ts +14 -0
  61. package/dist/esm/hooks/useShapes/annotation/index.js +200 -0
  62. package/dist/esm/hooks/useShapes/area/index.d.ts +2 -0
  63. package/dist/esm/hooks/useShapes/area/index.js +21 -2
  64. package/dist/esm/hooks/useShapes/area/prepare-data.d.ts +2 -1
  65. package/dist/esm/hooks/useShapes/area/prepare-data.js +38 -20
  66. package/dist/esm/hooks/useShapes/area/types.d.ts +4 -0
  67. package/dist/esm/hooks/useShapes/bar-x/index.d.ts +2 -0
  68. package/dist/esm/hooks/useShapes/bar-x/index.js +30 -2
  69. package/dist/esm/hooks/useShapes/bar-x/prepare-data.js +10 -2
  70. package/dist/esm/hooks/useShapes/bar-x/types.d.ts +2 -0
  71. package/dist/esm/hooks/useShapes/index.js +5 -3
  72. package/dist/esm/hooks/useShapes/line/index.d.ts +2 -0
  73. package/dist/esm/hooks/useShapes/line/index.js +21 -7
  74. package/dist/esm/hooks/useShapes/line/prepare-data.d.ts +2 -1
  75. package/dist/esm/hooks/useShapes/line/prepare-data.js +28 -10
  76. package/dist/esm/hooks/useShapes/line/types.d.ts +4 -0
  77. package/package.json +2 -2
@@ -0,0 +1,12 @@
1
+ import type { ChartAnnotationLabel, ChartAnnotationPopup } from '../../types';
2
+ import type { PreparedAnnotation } from './types';
3
+ type AnnotationOptionsLabel = Omit<ChartAnnotationLabel, 'text'> | undefined;
4
+ export declare function prepareAnnotation(args: {
5
+ annotation: {
6
+ label: ChartAnnotationLabel;
7
+ popup?: ChartAnnotationPopup;
8
+ };
9
+ optionsLabel?: AnnotationOptionsLabel;
10
+ optionsPopup?: ChartAnnotationPopup;
11
+ }): Promise<PreparedAnnotation>;
12
+ export {};
@@ -0,0 +1,31 @@
1
+ import { annotationLabelDefaults, annotationPopupDefaults } from '../constants';
2
+ import { getTextSizeFn } from '../utils';
3
+ function resolvePadding(padding) {
4
+ if (padding === undefined) {
5
+ return annotationPopupDefaults.padding;
6
+ }
7
+ if (typeof padding === 'number') {
8
+ return [padding, padding];
9
+ }
10
+ return padding;
11
+ }
12
+ export async function prepareAnnotation(args) {
13
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
14
+ const { annotation, optionsLabel, optionsPopup } = args;
15
+ const style = Object.assign(Object.assign(Object.assign({}, annotationLabelDefaults.style), optionsLabel === null || optionsLabel === void 0 ? void 0 : optionsLabel.style), annotation.label.style);
16
+ const getTextSize = getTextSizeFn({ style });
17
+ const textSize = await getTextSize(annotation.label.text);
18
+ return {
19
+ label: {
20
+ style,
21
+ text: annotation.label.text,
22
+ size: { height: textSize.height, width: textSize.width },
23
+ },
24
+ popup: {
25
+ backgroundColor: (_c = (_b = (_a = annotation.popup) === null || _a === void 0 ? void 0 : _a.backgroundColor) !== null && _b !== void 0 ? _b : optionsPopup === null || optionsPopup === void 0 ? void 0 : optionsPopup.backgroundColor) !== null && _c !== void 0 ? _c : annotationPopupDefaults.backgroundColor,
26
+ borderRadius: (_f = (_e = (_d = annotation.popup) === null || _d === void 0 ? void 0 : _d.borderRadius) !== null && _e !== void 0 ? _e : optionsPopup === null || optionsPopup === void 0 ? void 0 : optionsPopup.borderRadius) !== null && _f !== void 0 ? _f : annotationPopupDefaults.borderRadius,
27
+ offset: (_j = (_h = (_g = annotation.popup) === null || _g === void 0 ? void 0 : _g.offset) !== null && _h !== void 0 ? _h : optionsPopup === null || optionsPopup === void 0 ? void 0 : optionsPopup.offset) !== null && _j !== void 0 ? _j : annotationPopupDefaults.offset,
28
+ padding: resolvePadding((_l = (_k = annotation.popup) === null || _k === void 0 ? void 0 : _k.padding) !== null && _l !== void 0 ? _l : optionsPopup === null || optionsPopup === void 0 ? void 0 : optionsPopup.padding),
29
+ },
30
+ };
31
+ }
@@ -1,5 +1,21 @@
1
1
  import type { AreaSeries, AreaSeriesData, BarXSeries, BarXSeriesData, BarYSeries, BarYSeriesData, BaseTextStyle, ChartLegend, ChartSeries, ChartSeriesRangeSliderOptions, ConnectorCurve, ConnectorShape, FunnelSeries, FunnelSeriesData, HeatmapSeries, HeatmapSeriesData, LineSeries, LineSeriesData, LineSeriesLineBaseStyle, PathLegendSymbolOptions, PieSeries, PieSeriesData, RadarSeries, RadarSeriesCategory, RadarSeriesData, RectLegendSymbolOptions, SankeySeries, SankeySeriesData, ScatterSeries, ScatterSeriesData, SymbolLegendSymbolOptions, TreemapSeries, TreemapSeriesData, ValueFormat, WaterfallSeries, WaterfallSeriesData, XRangeSeries, XRangeSeriesData } from '../../types';
2
2
  import type { DashStyle, LayoutAlgorithm, LineCap, LineJoin, SeriesOptionsDefaults, SymbolType } from '../constants';
3
+ export type PreparedAnnotation = {
4
+ label: {
5
+ size: {
6
+ height: number;
7
+ width: number;
8
+ };
9
+ style: BaseTextStyle;
10
+ text: string;
11
+ };
12
+ popup: {
13
+ backgroundColor: string;
14
+ borderRadius: number;
15
+ offset: number;
16
+ padding: [number, number];
17
+ };
18
+ };
3
19
  export type RectLegendSymbol = {
4
20
  shape: 'rect';
5
21
  } & Required<RectLegendSymbolOptions>;
@@ -0,0 +1,45 @@
1
+ import type { BaseTextStyle } from './base';
2
+ export interface ChartAnnotationLabel {
3
+ /** Annotation text */
4
+ text: string;
5
+ /** Text style. fontColor defaults to 'var(--g-color-text-light-primary)' */
6
+ style?: Partial<BaseTextStyle>;
7
+ }
8
+ export interface ChartAnnotationPopup {
9
+ /**
10
+ * Background color of the popup.
11
+ * @default 'var(--g-color-base-float-heavy)'
12
+ */
13
+ backgroundColor?: string;
14
+ /**
15
+ * Border radius of the popup.
16
+ * @default 4
17
+ */
18
+ borderRadius?: number;
19
+ /**
20
+ * Distance in pixels between the anchor point and the popup edge along the main axis.
21
+ * The main axis depends on the automatically chosen placement:
22
+ * for top/bottom — vertical distance, for left/right — horizontal distance.
23
+ * @default 5
24
+ */
25
+ offset?: number;
26
+ /**
27
+ * Popup padding. Number or [vertical, horizontal].
28
+ * @default [4, 8]
29
+ */
30
+ padding?: number | [number, number];
31
+ }
32
+ /** Default annotation settings applied to all data points in a series */
33
+ export interface ChartAnnotationSeriesOptions {
34
+ /** Default label style for annotations */
35
+ label?: Omit<ChartAnnotationLabel, 'text'>;
36
+ /** Default popup settings for annotations */
37
+ popup?: ChartAnnotationPopup;
38
+ }
39
+ /** Annotation for a specific data point. Renders as a popup with text label near the data point. */
40
+ export interface ChartPointAnnotation {
41
+ /** Text content and style of the annotation */
42
+ label: ChartAnnotationLabel;
43
+ /** Visual settings for the annotation popup container (background, padding, etc.) */
44
+ popup?: ChartAnnotationPopup;
45
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,6 @@
1
1
  import type { SERIES_TYPE } from '../../constants';
2
2
  import type { MeaningfulAny } from '../misc';
3
+ import type { ChartPointAnnotation } from './annotation';
3
4
  import type { BaseSeries, BaseSeriesData, BaseSeriesLegend } from './base';
4
5
  import type { RectLegendSymbolOptions } from './legend';
5
6
  import type { PointMarkerOptions } from './marker';
@@ -23,6 +24,8 @@ export interface AreaSeriesData<T = MeaningfulAny> extends BaseSeriesData<T> {
23
24
  label?: string | number;
24
25
  /** Individual marker options for the point. */
25
26
  marker?: {
27
+ /** Fill color of the marker for this point */
28
+ color?: string;
26
29
  /** States for a single point marker. */
27
30
  states?: {
28
31
  /** The normal state of a single point marker. */
@@ -35,6 +38,11 @@ export interface AreaSeriesData<T = MeaningfulAny> extends BaseSeriesData<T> {
35
38
  };
36
39
  };
37
40
  };
41
+ /**
42
+ * Annotation displayed near this data point as a bubble with text label and optional marker.
43
+ * Useful for highlighting specific values, events, or adding contextual notes.
44
+ */
45
+ annotation?: ChartPointAnnotation;
38
46
  }
39
47
  export type AreaMarkerSymbol = 'circle' | 'square';
40
48
  export interface AreaMarkerOptions extends PointMarkerOptions {
@@ -1,5 +1,6 @@
1
1
  import type { SERIES_TYPE } from '../../constants';
2
2
  import type { MeaningfulAny } from '../misc';
3
+ import type { ChartPointAnnotation } from './annotation';
3
4
  import type { BaseSeries, BaseSeriesData, BaseSeriesLegend } from './base';
4
5
  import type { RectLegendSymbolOptions } from './legend';
5
6
  import type { ChartSeriesOptions, ChartSeriesRangeSliderOptions } from './series';
@@ -27,6 +28,11 @@ export interface BarXSeriesData<T = MeaningfulAny> extends BaseSeriesData<T> {
27
28
  label?: string | number;
28
29
  /** Individual opacity for the bar-x column. */
29
30
  opacity?: number;
31
+ /**
32
+ * Annotation displayed near this data point as a bubble with text label.
33
+ * Useful for highlighting specific values, events, or adding contextual notes.
34
+ */
35
+ annotation?: ChartPointAnnotation;
30
36
  }
31
37
  export interface BarXSeries<T = MeaningfulAny> extends BaseSeries {
32
38
  type: typeof SERIES_TYPE.BarX;
@@ -1,5 +1,6 @@
1
1
  import type { DashStyle, LineCap, LineJoin, SERIES_TYPE } from '../../constants';
2
2
  import type { MeaningfulAny } from '../misc';
3
+ import type { ChartPointAnnotation } from './annotation';
3
4
  import type { BaseSeries, BaseSeriesData, BaseSeriesLegend } from './base';
4
5
  import type { RectLegendSymbolOptions } from './legend';
5
6
  import type { PointMarkerOptions } from './marker';
@@ -22,12 +23,19 @@ export interface LineSeriesData<T = MeaningfulAny> extends BaseSeriesData<T> {
22
23
  /** Data label value of the point. If not specified, the y value is used. */
23
24
  label?: string | number;
24
25
  marker?: {
26
+ /** Fill color of the marker for this point */
27
+ color?: string;
25
28
  states?: {
26
29
  normal?: {
27
30
  enabled: boolean;
28
31
  };
29
32
  };
30
33
  };
34
+ /**
35
+ * Annotation displayed near this data point as a bubble with text label and optional marker.
36
+ * Useful for highlighting specific values, events, or adding contextual notes.
37
+ */
38
+ annotation?: ChartPointAnnotation;
31
39
  }
32
40
  export interface LineSeriesLineBaseStyle {
33
41
  /**
@@ -1,12 +1,14 @@
1
1
  import type { SymbolType } from '../../constants';
2
2
  export interface PointMarkerOptions {
3
- /** Enable or disable the point marker */
4
- enabled?: boolean;
5
- /** The radius of the point marker */
6
- radius?: number;
7
3
  /** The color of the point marker's border */
8
4
  borderColor?: string;
9
5
  /** The width of the point marker's border */
10
6
  borderWidth?: number;
7
+ /** Fill color of the marker */
8
+ color?: string;
9
+ /** Enable or disable the point marker */
10
+ enabled?: boolean;
11
+ /** The radius of the point marker */
12
+ radius?: number;
11
13
  symbol?: `${SymbolType}`;
12
14
  }
@@ -1,6 +1,7 @@
1
1
  import type React from 'react';
2
2
  import type { DashStyle, LineCap, LineJoin } from '../../constants';
3
3
  import type { MeaningfulAny } from '../misc';
4
+ import type { ChartAnnotationSeriesOptions } from './annotation';
4
5
  import type { AreaSeries, AreaSeriesData } from './area';
5
6
  import type { BarXSeries, BarXSeriesData } from './bar-x';
6
7
  import type { BarYSeries, BarYSeriesData } from './bar-y';
@@ -101,6 +102,8 @@ export interface ChartSeriesOptions {
101
102
  hover?: BasicHoverState;
102
103
  inactive?: BasicInactiveState;
103
104
  };
105
+ /** Default annotation settings for all bar-x data points */
106
+ annotation?: ChartAnnotationSeriesOptions;
104
107
  };
105
108
  'bar-y'?: {
106
109
  /**
@@ -212,6 +215,8 @@ export interface ChartSeriesOptions {
212
215
  * @default 'round' when dashStyle is not 'solid', 'unset' when dashStyle is not 'solid'
213
216
  */
214
217
  linejoin?: `${LineJoin}`;
218
+ /** Default annotation settings for all line data points */
219
+ annotation?: ChartAnnotationSeriesOptions;
215
220
  };
216
221
  area?: {
217
222
  /**
@@ -231,6 +236,8 @@ export interface ChartSeriesOptions {
231
236
  };
232
237
  /** Options for the point markers of line series */
233
238
  marker?: PointMarkerOptions;
239
+ /** Default annotation settings for all area data points */
240
+ annotation?: ChartAnnotationSeriesOptions;
234
241
  };
235
242
  treemap?: {
236
243
  /** Options for the series states that provide additional styling information to the series. */
@@ -47,6 +47,7 @@ export interface TooltipDataChunkLine<T = MeaningfulAny> {
47
47
  id: string;
48
48
  name: string;
49
49
  };
50
+ closest?: boolean;
50
51
  }
51
52
  export interface TooltipDataChunkArea<T = MeaningfulAny> {
52
53
  data: AreaSeriesData<T>;
@@ -7,6 +7,7 @@ import type { ChartTitle } from './chart/title';
7
7
  import type { ChartTooltip } from './chart/tooltip';
8
8
  import type { MeaningfulAny } from './misc';
9
9
  export * from './misc';
10
+ export * from './chart/annotation';
10
11
  export * from './chart/axis';
11
12
  export * from './chart/base';
12
13
  export * from './chart/chart';
@@ -1,4 +1,5 @@
1
1
  export * from './misc';
2
+ export * from './chart/annotation';
2
3
  export * from './chart/axis';
3
4
  export * from './chart/base';
4
5
  export * from './chart/chart';
@@ -1,5 +1,13 @@
1
1
  import type { Selection } from 'd3-selection';
2
2
  import type { BaseTextStyle, MeaningfulAny } from '../../types';
3
+ /**
4
+ * Approximate ratio of descenders relative to the full font em height.
5
+ * Based on the Chromium hanging baseline algorithm where hanging offset ≈ ascent × 0.2.
6
+ * This means ascent ≈ 80% of em height, descenders ≈ 20%.
7
+ *
8
+ * @see https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/html/canvas/text_metrics.cc;l=32
9
+ */
10
+ export declare const DESCENDER_RATIO = 0.2;
3
11
  export declare function handleOverflowingText(tSpan: SVGTSpanElement | null, maxWidth: number, textWidth?: number): void;
4
12
  export declare function setEllipsisForOverflowText<T>(selection: Selection<SVGTextElement, T, null, unknown>, maxWidth: number, textWidth?: number): void;
5
13
  export declare function setEllipsisForOverflowTexts<T>(selection: Selection<SVGTextElement, T, MeaningfulAny, unknown>, maxWidth: ((datum: T) => number) | number, currentWidth?: (datum: T) => number): void;
@@ -1,6 +1,14 @@
1
1
  import { select } from 'd3-selection';
2
2
  import { block } from '../../utils/cn';
3
3
  const b = block('chart');
4
+ /**
5
+ * Approximate ratio of descenders relative to the full font em height.
6
+ * Based on the Chromium hanging baseline algorithm where hanging offset ≈ ascent × 0.2.
7
+ * This means ascent ≈ 80% of em height, descenders ≈ 20%.
8
+ *
9
+ * @see https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/html/canvas/text_metrics.cc;l=32
10
+ */
11
+ export const DESCENDER_RATIO = 0.2;
4
12
  export function handleOverflowingText(tSpan, maxWidth, textWidth) {
5
13
  var _a, _b, _c;
6
14
  if (!tSpan) {
@@ -210,7 +218,7 @@ export function getTextSizeFn({ style }) {
210
218
  return {
211
219
  width: textMetric.width,
212
220
  height: textMetric.fontBoundingBoxDescent + textMetric.fontBoundingBoxAscent,
213
- hangingOffset: textMetric.fontBoundingBoxAscent * 0.2,
221
+ hangingOffset: textMetric.fontBoundingBoxAscent * DESCENDER_RATIO,
214
222
  };
215
223
  };
216
224
  }
@@ -0,0 +1,14 @@
1
+ import type { Selection } from 'd3-selection';
2
+ import type { PreparedAnnotation } from '../../../core/series/types';
3
+ type AnnotationAnchor = {
4
+ annotation: PreparedAnnotation;
5
+ x: number;
6
+ y: number;
7
+ };
8
+ export { type AnnotationAnchor };
9
+ export declare function renderAnnotations(args: {
10
+ anchors: AnnotationAnchor[];
11
+ container: Selection<SVGGElement, unknown, null, undefined>;
12
+ plotHeight: number;
13
+ plotWidth: number;
14
+ }): void;
@@ -0,0 +1,200 @@
1
+ import { select } from 'd3-selection';
2
+ import { DESCENDER_RATIO } from '../../../core/utils/text';
3
+ import { block } from '../../../utils';
4
+ const b = block('annotation');
5
+ const ARROW_WIDTH = 18;
6
+ const ARROW_HEIGHT = 9;
7
+ // Base arrow path pointing downward (for "top" placement).
8
+ // Elliptical arc matching gravity-ui/uikit Popup arrow geometry.
9
+ // uikit builds the arrow from two 28×30 circles ($arrow-circle-width/height) with
10
+ // a 5px inset box-shadow ($arrow-border), clipped in 9×9 wrappers.
11
+ // The visible curve follows the inner edge of the ring:
12
+ // rx = circle_width/2 - (arrow_border - border_width) = 14 - 4 = 10
13
+ // ry = circle_height/2 - (arrow_border - border_width) = 15 - 4 = 11
14
+ const ARROW_RX = 10;
15
+ const ARROW_RY = 11;
16
+ const ARROW_PATH = (() => {
17
+ const hw = ARROW_WIDTH / 2;
18
+ const h = ARROW_HEIGHT;
19
+ return `M ${-hw},0 A ${ARROW_RX} ${ARROW_RY} 0 0 1 0,${h} A ${ARROW_RX} ${ARROW_RY} 0 0 1 ${hw},0 Z`;
20
+ })();
21
+ function getArrowRotation(placement) {
22
+ switch (placement) {
23
+ case 'top':
24
+ return 0;
25
+ case 'bottom':
26
+ return 180;
27
+ case 'right':
28
+ return 90;
29
+ case 'left':
30
+ default:
31
+ return -90;
32
+ }
33
+ }
34
+ function clampX(x, width, plotWidth) {
35
+ return Math.max(0, Math.min(x, plotWidth - width));
36
+ }
37
+ function clampY(y, height, plotHeight) {
38
+ return Math.max(0, Math.min(y, plotHeight - height));
39
+ }
40
+ function calculateLayout(args) {
41
+ const { anchorX, anchorY, popupWidth, popupHeight, offset, plotWidth, plotHeight } = args;
42
+ // Minimum distance from popup edge to arrow center (arrow half-width + border radius clearance)
43
+ const arrowEdgePadding = ARROW_WIDTH / 2;
44
+ // Check if anchor falls within popup's horizontal span (for top/bottom placement)
45
+ function isAnchorInPopupX(popupX) {
46
+ return (anchorX >= popupX + arrowEdgePadding &&
47
+ anchorX <= popupX + popupWidth - arrowEdgePadding);
48
+ }
49
+ // Check if anchor falls within popup's vertical span (for right/left placement)
50
+ function isAnchorInPopupY(popupY) {
51
+ return (anchorY >= popupY + arrowEdgePadding &&
52
+ anchorY <= popupY + popupHeight - arrowEdgePadding);
53
+ }
54
+ // Try top
55
+ const topY = anchorY - offset - ARROW_HEIGHT - popupHeight;
56
+ if (topY >= 0) {
57
+ const popupX = clampX(anchorX - popupWidth / 2, popupWidth, plotWidth);
58
+ if (isAnchorInPopupX(popupX)) {
59
+ return {
60
+ arrowX: anchorX,
61
+ arrowY: anchorY,
62
+ popupX,
63
+ popupY: topY,
64
+ placement: 'top',
65
+ showArrow: true,
66
+ };
67
+ }
68
+ }
69
+ // Try bottom
70
+ const bottomY = anchorY + offset + ARROW_HEIGHT;
71
+ if (bottomY + popupHeight <= plotHeight) {
72
+ const popupX = clampX(anchorX - popupWidth / 2, popupWidth, plotWidth);
73
+ if (isAnchorInPopupX(popupX)) {
74
+ return {
75
+ arrowX: anchorX,
76
+ arrowY: anchorY,
77
+ popupX,
78
+ popupY: bottomY,
79
+ placement: 'bottom',
80
+ showArrow: true,
81
+ };
82
+ }
83
+ }
84
+ // Try right
85
+ const rightX = anchorX + offset + ARROW_HEIGHT;
86
+ if (rightX + popupWidth <= plotWidth) {
87
+ const popupY = clampY(anchorY - popupHeight / 2, popupHeight, plotHeight);
88
+ if (isAnchorInPopupY(popupY)) {
89
+ return {
90
+ arrowX: anchorX,
91
+ arrowY: anchorY,
92
+ popupX: rightX,
93
+ popupY,
94
+ placement: 'right',
95
+ showArrow: true,
96
+ };
97
+ }
98
+ }
99
+ // Try left
100
+ const leftX = anchorX - offset - ARROW_HEIGHT - popupWidth;
101
+ if (leftX >= 0) {
102
+ const popupY = clampY(anchorY - popupHeight / 2, popupHeight, plotHeight);
103
+ if (isAnchorInPopupY(popupY)) {
104
+ return {
105
+ arrowX: anchorX,
106
+ arrowY: anchorY,
107
+ popupX: leftX,
108
+ popupY,
109
+ placement: 'left',
110
+ showArrow: true,
111
+ };
112
+ }
113
+ }
114
+ // Fallback: no arrow, popup near anchor (prefer above, then below)
115
+ const popupX = clampX(anchorX - popupWidth / 2, popupWidth, plotWidth);
116
+ const fallbackTopY = anchorY - offset - popupHeight;
117
+ const popupY = fallbackTopY >= 0 ? fallbackTopY : Math.min(plotHeight - popupHeight, anchorY + offset);
118
+ return {
119
+ arrowX: anchorX,
120
+ arrowY: anchorY,
121
+ popupX,
122
+ popupY,
123
+ placement: 'top',
124
+ showArrow: false,
125
+ };
126
+ }
127
+ function getArrowTranslate(layout, popupWidth, popupHeight) {
128
+ const { arrowX, arrowY, popupX, popupY, placement } = layout;
129
+ // Overlap by 0.5px to avoid subpixel gap between arrow and popup rect
130
+ const overlap = 0.5;
131
+ switch (placement) {
132
+ case 'top':
133
+ return `translate(${arrowX}, ${popupY + popupHeight - overlap})`;
134
+ case 'bottom':
135
+ return `translate(${arrowX}, ${popupY + overlap})`;
136
+ case 'right':
137
+ return `translate(${popupX + overlap}, ${arrowY})`;
138
+ case 'left':
139
+ default:
140
+ return `translate(${popupX + popupWidth - overlap}, ${arrowY})`;
141
+ }
142
+ }
143
+ export function renderAnnotations(args) {
144
+ const { container, anchors, plotWidth, plotHeight } = args;
145
+ container.selectAll(`.${b()}`).remove();
146
+ if (!anchors.length) {
147
+ return;
148
+ }
149
+ const groups = container
150
+ .selectAll(`.${b()}`)
151
+ .data(anchors)
152
+ .join('g')
153
+ .attr('class', b());
154
+ groups.each(function (d) {
155
+ const g = select(this);
156
+ const { annotation, x: anchorX, y: anchorY } = d;
157
+ const { label, popup } = annotation;
158
+ const [paddingV, paddingH] = popup.padding;
159
+ const popupWidth = label.size.width + paddingH * 2;
160
+ const popupHeight = label.size.height + paddingV * 2;
161
+ const layout = calculateLayout({
162
+ anchorX,
163
+ anchorY,
164
+ popupWidth,
165
+ popupHeight,
166
+ offset: popup.offset,
167
+ plotWidth,
168
+ plotHeight,
169
+ });
170
+ // Popup background
171
+ g.append('rect')
172
+ .attr('class', b('popup'))
173
+ .attr('x', layout.popupX)
174
+ .attr('y', layout.popupY)
175
+ .attr('width', popupWidth)
176
+ .attr('height', popupHeight)
177
+ .attr('rx', popup.borderRadius)
178
+ .attr('ry', popup.borderRadius)
179
+ .attr('fill', popup.backgroundColor);
180
+ // Arrow
181
+ if (layout.showArrow) {
182
+ const arrowTranslate = getArrowTranslate(layout, popupWidth, popupHeight);
183
+ const arrowRotation = getArrowRotation(layout.placement);
184
+ g.append('path')
185
+ .attr('class', b('arrow'))
186
+ .attr('d', ARROW_PATH)
187
+ .attr('fill', popup.backgroundColor)
188
+ .attr('transform', `${arrowTranslate} rotate(${arrowRotation})`);
189
+ }
190
+ // Text
191
+ g.append('text')
192
+ .attr('class', b('text'))
193
+ .text(label.text)
194
+ .attr('x', layout.popupX + paddingH)
195
+ .attr('y', layout.popupY + paddingV + label.size.height * (1 - DESCENDER_RATIO))
196
+ .style('font-size', label.style.fontSize)
197
+ .style('font-weight', label.style.fontWeight || '')
198
+ .style('fill', label.style.fontColor || '');
199
+ });
200
+ }
@@ -3,6 +3,8 @@ import type { Dispatch } from 'd3-dispatch';
3
3
  import type { PreparedSeriesOptions } from '../../useSeries/types';
4
4
  import type { PreparedAreaData } from './types';
5
5
  type Args = {
6
+ boundsHeight: number;
7
+ boundsWidth: number;
6
8
  clipPathId: string;
7
9
  htmlLayout: HTMLElement | null;
8
10
  preparedData: PreparedAreaData[];
@@ -6,15 +6,17 @@ import get from 'lodash/get';
6
6
  import { filterOverlappingLabels } from '../../../core/utils';
7
7
  import { block } from '../../../utils';
8
8
  import { HtmlLayer } from '../HtmlLayer';
9
+ import { renderAnnotations } from '../annotation';
9
10
  import { getMarkerHaloVisibility, getMarkerVisibility, renderMarker, selectMarkerHalo, selectMarkerSymbol, setMarker, } from '../marker';
10
11
  import { setActiveState } from '../utils';
11
12
  const b = block('area');
12
13
  export const AreaSeriesShapes = (args) => {
13
- const { dispatcher, preparedData, seriesOptions, htmlLayout, clipPathId } = args;
14
+ const { boundsHeight, boundsWidth, dispatcher, preparedData, seriesOptions, htmlLayout, clipPathId, } = args;
14
15
  const hoveredDataRef = React.useRef(null);
15
16
  const plotRef = React.useRef(null);
16
17
  const markersRef = React.useRef(null);
17
18
  const hoverMarkersRef = React.useRef(null);
19
+ const annotationsRef = React.useRef(null);
18
20
  const allowOverlapDataLabels = React.useMemo(() => {
19
21
  return preparedData.some((d) => d === null || d === void 0 ? void 0 : d.series.dataLabels.allowOverlap);
20
22
  }, [preparedData]);
@@ -83,6 +85,15 @@ export const AreaSeriesShapes = (args) => {
83
85
  .data(markers)
84
86
  .join('g')
85
87
  .call(renderMarker);
88
+ if (annotationsRef.current) {
89
+ const anchors = preparedData.flatMap((d) => d.annotations);
90
+ renderAnnotations({
91
+ anchors,
92
+ container: select(annotationsRef.current),
93
+ plotHeight: boundsHeight,
94
+ plotWidth: boundsWidth,
95
+ });
96
+ }
86
97
  const hoverEnabled = hoverOptions === null || hoverOptions === void 0 ? void 0 : hoverOptions.enabled;
87
98
  const inactiveEnabled = inactiveOptions === null || inactiveOptions === void 0 ? void 0 : inactiveOptions.enabled;
88
99
  function handleShapeHover(data) {
@@ -189,7 +200,14 @@ export const AreaSeriesShapes = (args) => {
189
200
  return () => {
190
201
  dispatcher === null || dispatcher === void 0 ? void 0 : dispatcher.on('hover-shape.area', null);
191
202
  };
192
- }, [allowOverlapDataLabels, dispatcher, preparedData, seriesOptions]);
203
+ }, [
204
+ allowOverlapDataLabels,
205
+ boundsHeight,
206
+ boundsWidth,
207
+ dispatcher,
208
+ preparedData,
209
+ seriesOptions,
210
+ ]);
193
211
  const htmlLayerData = React.useMemo(() => {
194
212
  const items = preparedData.map((d) => d === null || d === void 0 ? void 0 : d.htmlLabels).flat();
195
213
  if (allowOverlapDataLabels) {
@@ -201,5 +219,6 @@ export const AreaSeriesShapes = (args) => {
201
219
  React.createElement("g", { ref: plotRef, className: b(), clipPath: `url(#${clipPathId})` }),
202
220
  React.createElement("g", { ref: markersRef }),
203
221
  React.createElement("g", { ref: hoverMarkersRef }),
222
+ React.createElement("g", { ref: annotationsRef }),
204
223
  React.createElement(HtmlLayer, { preparedData: htmlLayerData, htmlLayout: htmlLayout })));
205
224
  };
@@ -1,10 +1,11 @@
1
1
  import type { PreparedSplit } from '../../../core/layout/split-types';
2
2
  import type { ChartScale } from '../../../core/scales/types';
3
3
  import type { PreparedXAxis, PreparedYAxis } from '../../useAxis/types';
4
- import type { PreparedAreaSeries } from '../../useSeries/types';
4
+ import type { PreparedAreaSeries, PreparedSeriesOptions } from '../../useSeries/types';
5
5
  import type { PreparedAreaData } from './types';
6
6
  export declare const prepareAreaData: (args: {
7
7
  series: PreparedAreaSeries[];
8
+ seriesOptions?: PreparedSeriesOptions;
8
9
  xAxis: PreparedXAxis;
9
10
  xScale: ChartScale;
10
11
  yAxis: PreparedYAxis[];