@gravity-ui/charts 1.44.0 → 1.46.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 (99) hide show
  1. package/dist/cjs/components/ChartInner/index.js +2 -2
  2. package/dist/cjs/components/ChartInner/styles.css +2 -2
  3. package/dist/cjs/components/ChartInner/useChartInnerProps.js +1 -1
  4. package/dist/cjs/components/ChartInner/utils/title.d.ts +2 -1
  5. package/dist/cjs/components/ChartInner/utils/title.js +51 -14
  6. package/dist/cjs/components/Title/index.d.ts +4 -2
  7. package/dist/cjs/components/Title/index.js +9 -2
  8. package/dist/cjs/core/constants/defaults/annotation.d.ts +12 -0
  9. package/dist/cjs/core/constants/defaults/annotation.js +12 -0
  10. package/dist/cjs/core/constants/defaults/index.d.ts +1 -0
  11. package/dist/cjs/core/constants/defaults/index.js +1 -0
  12. package/dist/cjs/core/series/constants.d.ts +1 -1
  13. package/dist/cjs/core/series/constants.js +1 -1
  14. package/dist/cjs/core/series/prepare-annotation.d.ts +12 -0
  15. package/dist/cjs/core/series/prepare-annotation.js +31 -0
  16. package/dist/cjs/core/series/types.d.ts +16 -0
  17. package/dist/cjs/core/types/chart/annotation.d.ts +45 -0
  18. package/dist/cjs/core/types/chart/annotation.js +1 -0
  19. package/dist/cjs/core/types/chart/area.d.ts +8 -0
  20. package/dist/cjs/core/types/chart/bar-x.d.ts +6 -0
  21. package/dist/cjs/core/types/chart/line.d.ts +8 -0
  22. package/dist/cjs/core/types/chart/marker.d.ts +6 -4
  23. package/dist/cjs/core/types/chart/series.d.ts +7 -0
  24. package/dist/cjs/core/types/chart/title.d.ts +18 -0
  25. package/dist/cjs/core/types/chart/tooltip.d.ts +1 -0
  26. package/dist/cjs/core/types/index.d.ts +1 -0
  27. package/dist/cjs/core/types/index.js +1 -0
  28. package/dist/cjs/core/utils/text.d.ts +8 -0
  29. package/dist/cjs/core/utils/text.js +9 -1
  30. package/dist/cjs/hooks/types.d.ts +6 -3
  31. package/dist/cjs/hooks/useShapes/HtmlLayer.js +4 -3
  32. package/dist/cjs/hooks/useShapes/annotation/index.d.ts +14 -0
  33. package/dist/cjs/hooks/useShapes/annotation/index.js +200 -0
  34. package/dist/cjs/hooks/useShapes/area/index.d.ts +2 -0
  35. package/dist/cjs/hooks/useShapes/area/index.js +21 -2
  36. package/dist/cjs/hooks/useShapes/area/prepare-data.d.ts +2 -1
  37. package/dist/cjs/hooks/useShapes/area/prepare-data.js +38 -20
  38. package/dist/cjs/hooks/useShapes/area/types.d.ts +4 -0
  39. package/dist/cjs/hooks/useShapes/bar-x/index.d.ts +2 -0
  40. package/dist/cjs/hooks/useShapes/bar-x/index.js +30 -2
  41. package/dist/cjs/hooks/useShapes/bar-x/prepare-data.js +10 -2
  42. package/dist/cjs/hooks/useShapes/bar-x/types.d.ts +2 -0
  43. package/dist/cjs/hooks/useShapes/index.js +5 -3
  44. package/dist/cjs/hooks/useShapes/line/index.d.ts +2 -0
  45. package/dist/cjs/hooks/useShapes/line/index.js +18 -4
  46. package/dist/cjs/hooks/useShapes/line/prepare-data.d.ts +2 -1
  47. package/dist/cjs/hooks/useShapes/line/prepare-data.js +28 -10
  48. package/dist/cjs/hooks/useShapes/line/types.d.ts +4 -0
  49. package/dist/cjs/types/chart-ui.d.ts +2 -0
  50. package/dist/esm/components/ChartInner/index.js +2 -2
  51. package/dist/esm/components/ChartInner/styles.css +2 -2
  52. package/dist/esm/components/ChartInner/useChartInnerProps.js +1 -1
  53. package/dist/esm/components/ChartInner/utils/title.d.ts +2 -1
  54. package/dist/esm/components/ChartInner/utils/title.js +51 -14
  55. package/dist/esm/components/Title/index.d.ts +4 -2
  56. package/dist/esm/components/Title/index.js +9 -2
  57. package/dist/esm/core/constants/defaults/annotation.d.ts +12 -0
  58. package/dist/esm/core/constants/defaults/annotation.js +12 -0
  59. package/dist/esm/core/constants/defaults/index.d.ts +1 -0
  60. package/dist/esm/core/constants/defaults/index.js +1 -0
  61. package/dist/esm/core/series/constants.d.ts +1 -1
  62. package/dist/esm/core/series/constants.js +1 -1
  63. package/dist/esm/core/series/prepare-annotation.d.ts +12 -0
  64. package/dist/esm/core/series/prepare-annotation.js +31 -0
  65. package/dist/esm/core/series/types.d.ts +16 -0
  66. package/dist/esm/core/types/chart/annotation.d.ts +45 -0
  67. package/dist/esm/core/types/chart/annotation.js +1 -0
  68. package/dist/esm/core/types/chart/area.d.ts +8 -0
  69. package/dist/esm/core/types/chart/bar-x.d.ts +6 -0
  70. package/dist/esm/core/types/chart/line.d.ts +8 -0
  71. package/dist/esm/core/types/chart/marker.d.ts +6 -4
  72. package/dist/esm/core/types/chart/series.d.ts +7 -0
  73. package/dist/esm/core/types/chart/title.d.ts +18 -0
  74. package/dist/esm/core/types/chart/tooltip.d.ts +1 -0
  75. package/dist/esm/core/types/index.d.ts +1 -0
  76. package/dist/esm/core/types/index.js +1 -0
  77. package/dist/esm/core/utils/text.d.ts +8 -0
  78. package/dist/esm/core/utils/text.js +9 -1
  79. package/dist/esm/hooks/types.d.ts +6 -3
  80. package/dist/esm/hooks/useShapes/HtmlLayer.js +4 -3
  81. package/dist/esm/hooks/useShapes/annotation/index.d.ts +14 -0
  82. package/dist/esm/hooks/useShapes/annotation/index.js +200 -0
  83. package/dist/esm/hooks/useShapes/area/index.d.ts +2 -0
  84. package/dist/esm/hooks/useShapes/area/index.js +21 -2
  85. package/dist/esm/hooks/useShapes/area/prepare-data.d.ts +2 -1
  86. package/dist/esm/hooks/useShapes/area/prepare-data.js +38 -20
  87. package/dist/esm/hooks/useShapes/area/types.d.ts +4 -0
  88. package/dist/esm/hooks/useShapes/bar-x/index.d.ts +2 -0
  89. package/dist/esm/hooks/useShapes/bar-x/index.js +30 -2
  90. package/dist/esm/hooks/useShapes/bar-x/prepare-data.js +10 -2
  91. package/dist/esm/hooks/useShapes/bar-x/types.d.ts +2 -0
  92. package/dist/esm/hooks/useShapes/index.js +5 -3
  93. package/dist/esm/hooks/useShapes/line/index.d.ts +2 -0
  94. package/dist/esm/hooks/useShapes/line/index.js +18 -4
  95. package/dist/esm/hooks/useShapes/line/prepare-data.d.ts +2 -1
  96. package/dist/esm/hooks/useShapes/line/prepare-data.js +28 -10
  97. package/dist/esm/hooks/useShapes/line/types.d.ts +4 -0
  98. package/dist/esm/types/chart-ui.d.ts +2 -0
  99. package/package.json +2 -2
@@ -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. */
@@ -5,6 +5,7 @@ export interface ChartTitle {
5
5
  /**
6
6
  * Maximum number of text rows. If the text exceeds this limit, it is truncated with an ellipsis.
7
7
  * Default: 1
8
+ * Not applicable when `html: true` — HTML content manages its own layout.
8
9
  */
9
10
  maxRowCount?: number;
10
11
  /**
@@ -17,4 +18,21 @@ export interface ChartTitle {
17
18
  * It is assigned as a data-qa attribute to an element.
18
19
  */
19
20
  qa?: string;
21
+ /**
22
+ * Enables HTML rendering for the chart title.
23
+ * When true, the title is rendered as an HTML element on top of the SVG
24
+ * instead of an SVG text node. This allows using arbitrary HTML tags
25
+ * (e.g. links, styled spans) that cannot be embedded in SVG.
26
+ * The element will be displayed outside the box of the SVG element.
27
+ */
28
+ html?: boolean;
29
+ /**
30
+ * Maximum height of the title area. Accepts a pixel value (`number` or `"100px"`)
31
+ * or a percentage string (`"50%"`) relative to the full chart height.
32
+ * When the title content exceeds this height, it is clipped.
33
+ * Only applicable when `html: true`.
34
+ *
35
+ * Default: 50%
36
+ */
37
+ maxHeight?: string | number;
20
38
  }
@@ -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
  }
@@ -1,5 +1,6 @@
1
1
  import type { TextRowData } from '../components/types';
2
- import type { ChartBrush, ChartData, ChartMargin, ChartTitle, ChartZoom, DeepRequired } from '../types';
2
+ import type { BaseTextStyle, ChartBrush, ChartData, ChartMargin, ChartTitle, ChartZoom, DeepRequired } from '../types';
3
+ import type { HtmlItem } from '../types/chart-ui';
3
4
  export type PreparedZoom = DeepRequired<Omit<ChartZoom, 'enabled' | 'brush'>> & DeepRequired<{
4
5
  brush: ChartBrush;
5
6
  }>;
@@ -7,10 +8,12 @@ export type PreparedChart = {
7
8
  margin: ChartMargin;
8
9
  zoom: PreparedZoom | null;
9
10
  };
10
- export type PreparedTitle = Omit<ChartTitle, 'margin'> & {
11
+ export type PreparedTitle = Omit<ChartTitle, 'margin' | 'style'> & {
11
12
  height: number;
12
13
  margin: number;
13
- contentRows: TextRowData[];
14
+ style: BaseTextStyle;
15
+ contentRows?: TextRowData[];
16
+ htmlElements?: HtmlItem[];
14
17
  };
15
18
  export type PreparedTooltip = ChartData['tooltip'] & {
16
19
  enabled: boolean;
@@ -19,8 +19,9 @@ export const HtmlLayer = (props) => {
19
19
  return null;
20
20
  }
21
21
  return (React.createElement(Portal, { container: htmlLayout }, items.map((item, index) => {
22
- var _a, _b, _c;
23
- const style = Object.assign(Object.assign({}, item.style), { color: (_b = (_a = item.style) === null || _a === void 0 ? void 0 : _a.color) !== null && _b !== void 0 ? _b : (_c = item.style) === null || _c === void 0 ? void 0 : _c.fontColor, position: 'absolute', left: item.x, top: item.y });
24
- return (React.createElement("div", { className: b('html-layer-item'), key: index, dangerouslySetInnerHTML: { __html: item.content }, style: style }));
22
+ var _a, _b, _c, _d;
23
+ const scope = (_a = item.scope) !== null && _a !== void 0 ? _a : 'plot';
24
+ const style = Object.assign(Object.assign({}, item.style), { color: (_c = (_b = item.style) === null || _b === void 0 ? void 0 : _b.color) !== null && _c !== void 0 ? _c : (_d = item.style) === null || _d === void 0 ? void 0 : _d.fontColor, position: 'absolute', left: item.x, top: item.y });
25
+ return (React.createElement("div", { className: b('html-layer-item', { plot: scope === 'plot' }), key: index, dangerouslySetInnerHTML: { __html: item.content }, style: style }));
25
26
  })));
26
27
  };
@@ -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[];
@@ -1,6 +1,7 @@
1
1
  import { group, min, sort } from 'd3-array';
2
2
  import isNil from 'lodash/isNil';
3
3
  import round from 'lodash/round';
4
+ import { prepareAnnotation } from '../../../core/series/prepare-annotation';
4
5
  import { getDataCategoryValue, getLabelsSize, getTextSizeFn } from '../../../core/utils';
5
6
  import { getFormattedValue } from '../../../core/utils/format';
6
7
  import { getXValue, getYValue } from '../utils';
@@ -76,8 +77,8 @@ async function prepareDataLabels({ series, points, xMax, yAxisTop, isOutsideBoun
76
77
  return { svgLabels, htmlLabels };
77
78
  }
78
79
  export const prepareAreaData = async (args) => {
79
- var _a, _b, _c, _d;
80
- const { series, xAxis, xScale, yAxis, yScale, split, isOutsideBounds, isRangeSlider } = args;
80
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
81
+ const { series, seriesOptions, xAxis, xScale, yAxis, yScale, split, isOutsideBounds, isRangeSlider, } = args;
81
82
  const [_xMin, xRangeMax] = xScale.range();
82
83
  const xMax = xRangeMax;
83
84
  const result = [];
@@ -165,16 +166,25 @@ export const prepareAreaData = async (args) => {
165
166
  : d.x);
166
167
  return m.set(key, d);
167
168
  }, new Map());
168
- const points = xValues.reduce((pointsAcc, [x, xValue], index) => {
169
- var _a, _b, _c, _d, _e, _f, _g;
169
+ const annotationOpts = (_d = seriesOptions === null || seriesOptions === void 0 ? void 0 : seriesOptions.area) === null || _d === void 0 ? void 0 : _d.annotation;
170
+ const points = [];
171
+ for (let xIdx = 0; xIdx < xValues.length; xIdx++) {
172
+ const [x, xValue] = xValues[xIdx];
170
173
  const rawData = seriesData.get(x);
171
174
  const d = rawData !== null && rawData !== void 0 ? rawData : {
172
175
  x,
173
176
  y: 0,
174
177
  };
175
- let yDataValue = (_a = d.y) !== null && _a !== void 0 ? _a : null;
178
+ let yDataValue = (_e = d.y) !== null && _e !== void 0 ? _e : null;
179
+ const pointAnnotation = d.annotation && !isRangeSlider
180
+ ? await prepareAnnotation({
181
+ annotation: d.annotation,
182
+ optionsLabel: annotationOpts === null || annotationOpts === void 0 ? void 0 : annotationOpts.label,
183
+ optionsPopup: annotationOpts === null || annotationOpts === void 0 ? void 0 : annotationOpts.popup,
184
+ })
185
+ : undefined;
176
186
  if (s.nullMode === 'connect' && (yDataValue === null || !rawData)) {
177
- return pointsAcc;
187
+ continue;
178
188
  }
179
189
  if (yDataValue && isPercentStacking) {
180
190
  yDataValue = Number(yDataValue) * ratio[x];
@@ -188,21 +198,23 @@ export const prepareAreaData = async (args) => {
188
198
  });
189
199
  if (typeof yDataValue === 'number' && yValue !== null) {
190
200
  yValue = round(yValue, 2);
191
- const prevPoint = seriesData.get((_b = xValues[index - 1]) === null || _b === void 0 ? void 0 : _b[0]);
192
- const nextPoint = seriesData.get((_c = xValues[index + 1]) === null || _c === void 0 ? void 0 : _c[0]);
201
+ const prevPoint = seriesData.get((_f = xValues[xIdx - 1]) === null || _f === void 0 ? void 0 : _f[0]);
202
+ const nextPoint = seriesData.get((_g = xValues[xIdx + 1]) === null || _g === void 0 ? void 0 : _g[0]);
193
203
  const currentPointStackHeight = Math.abs(yMin - yValue);
194
204
  if (yDataValue >= 0) {
195
205
  const positiveStackHeights = positiveStackValues.get(x);
196
- let prevSectionStackHeight = (_d = positiveStackHeights === null || positiveStackHeights === void 0 ? void 0 : positiveStackHeights.prev) !== null && _d !== void 0 ? _d : 0;
197
- let nextSectionStackHeight = (_e = positiveStackHeights === null || positiveStackHeights === void 0 ? void 0 : positiveStackHeights.next) !== null && _e !== void 0 ? _e : 0;
206
+ let prevSectionStackHeight = (_h = positiveStackHeights === null || positiveStackHeights === void 0 ? void 0 : positiveStackHeights.prev) !== null && _h !== void 0 ? _h : 0;
207
+ let nextSectionStackHeight = (_j = positiveStackHeights === null || positiveStackHeights === void 0 ? void 0 : positiveStackHeights.next) !== null && _j !== void 0 ? _j : 0;
198
208
  const point = {
199
209
  y0: yAxisTop + yMin - prevSectionStackHeight,
200
210
  x: xValue,
201
211
  y: yAxisTop + yValue - prevSectionStackHeight,
212
+ color: (_l = (_k = d.marker) === null || _k === void 0 ? void 0 : _k.color) !== null && _l !== void 0 ? _l : d.color,
202
213
  data: d,
203
214
  series: s,
215
+ annotation: pointAnnotation,
204
216
  };
205
- pointsAcc.push(point);
217
+ points.push(point);
206
218
  if (prevSectionStackHeight !== nextSectionStackHeight) {
207
219
  const point2 = {
208
220
  y0: yAxisTop + yMin - nextSectionStackHeight,
@@ -211,7 +223,7 @@ export const prepareAreaData = async (args) => {
211
223
  data: d,
212
224
  series: s,
213
225
  };
214
- pointsAcc.push(point2);
226
+ points.push(point2);
215
227
  if (isPercentStacking) {
216
228
  const newYValue = yAxisTop +
217
229
  yValue -
@@ -235,9 +247,9 @@ export const prepareAreaData = async (args) => {
235
247
  }
236
248
  else {
237
249
  const negativeStackHeights = negativeStackValues.get(x);
238
- let prevSectionStackHeight = (_f = negativeStackHeights === null || negativeStackHeights === void 0 ? void 0 : negativeStackHeights.prev) !== null && _f !== void 0 ? _f : 0;
239
- let nextSectionStackHeight = (_g = negativeStackHeights === null || negativeStackHeights === void 0 ? void 0 : negativeStackHeights.next) !== null && _g !== void 0 ? _g : 0;
240
- pointsAcc.push({
250
+ let prevSectionStackHeight = (_m = negativeStackHeights === null || negativeStackHeights === void 0 ? void 0 : negativeStackHeights.prev) !== null && _m !== void 0 ? _m : 0;
251
+ let nextSectionStackHeight = (_o = negativeStackHeights === null || negativeStackHeights === void 0 ? void 0 : negativeStackHeights.next) !== null && _o !== void 0 ? _o : 0;
252
+ points.push({
241
253
  y0: yAxisTop + yMin + prevSectionStackHeight,
242
254
  x: xValue,
243
255
  y: yAxisTop + yValue + prevSectionStackHeight,
@@ -245,7 +257,7 @@ export const prepareAreaData = async (args) => {
245
257
  series: s,
246
258
  });
247
259
  if (prevSectionStackHeight !== nextSectionStackHeight) {
248
- pointsAcc.push({
260
+ points.push({
249
261
  y0: yAxisTop + yMin + nextSectionStackHeight,
250
262
  x: xValue,
251
263
  y: yAxisTop + yValue + nextSectionStackHeight,
@@ -268,7 +280,7 @@ export const prepareAreaData = async (args) => {
268
280
  }
269
281
  }
270
282
  else {
271
- pointsAcc.push({
283
+ points.push({
272
284
  y0: yAxisTop + yMin,
273
285
  x: xValue,
274
286
  y: null,
@@ -276,8 +288,7 @@ export const prepareAreaData = async (args) => {
276
288
  series: s,
277
289
  });
278
290
  }
279
- return pointsAcc;
280
- }, []);
291
+ }
281
292
  let markers = [];
282
293
  const hasPerPointNormalMarkers = s.data.some((d) => { var _a, _b, _c; return (_c = (_b = (_a = d.marker) === null || _a === void 0 ? void 0 : _a.states) === null || _b === void 0 ? void 0 : _b.normal) === null || _c === void 0 ? void 0 : _c.enabled; });
283
294
  if (s.marker.states.normal.enabled || hasPerPointNormalMarkers) {
@@ -298,7 +309,14 @@ export const prepareAreaData = async (args) => {
298
309
  return markersAcc;
299
310
  }, []);
300
311
  }
312
+ const annotations = points.reduce((result, p) => {
313
+ if (p.annotation && p.y !== null) {
314
+ result.push({ annotation: p.annotation, x: p.x, y: p.y });
315
+ }
316
+ return result;
317
+ }, []);
301
318
  seriesStackData.push({
319
+ annotations,
302
320
  points,
303
321
  markers,
304
322
  svgLabels: [],
@@ -315,7 +333,7 @@ export const prepareAreaData = async (args) => {
315
333
  for (let itemIndex = 0; itemIndex < seriesStackData.length; itemIndex++) {
316
334
  const item = seriesStackData[itemIndex];
317
335
  const currentYAxis = yAxis[item.series.yAxis];
318
- const itemYAxisTop = ((_d = split.plots[currentYAxis.plotIndex]) === null || _d === void 0 ? void 0 : _d.top) || 0;
336
+ const itemYAxisTop = ((_p = split.plots[currentYAxis.plotIndex]) === null || _p === void 0 ? void 0 : _p.top) || 0;
319
337
  if (item.series.dataLabels.enabled && !isRangeSlider) {
320
338
  const labelsData = await prepareDataLabels({
321
339
  series: item.series,
@@ -1,11 +1,14 @@
1
+ import type { PreparedAnnotation } from '../../../core/series/types';
1
2
  import type { AreaSeriesData, HtmlItem, LabelData } from '../../../types';
2
3
  import type { PreparedAreaSeries } from '../../useSeries/types';
4
+ import type { AnnotationAnchor } from '../annotation';
3
5
  export type PointData = {
4
6
  y0: number;
5
7
  x: number;
6
8
  y: number | null;
7
9
  data: AreaSeriesData;
8
10
  series: PreparedAreaSeries;
11
+ annotation?: PreparedAnnotation;
9
12
  color?: string;
10
13
  };
11
14
  export type MarkerPointData = PointData & {
@@ -18,6 +21,7 @@ export type MarkerData = {
18
21
  clipped: boolean;
19
22
  };
20
23
  export type PreparedAreaData = {
24
+ annotations: AnnotationAnchor[];
21
25
  id: string;
22
26
  points: PointData[];
23
27
  markers: MarkerData[];