@gravity-ui/charts 1.42.3 → 1.43.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 (127) hide show
  1. package/dist/cjs/components/AxisX/AxisX.js +31 -4
  2. package/dist/cjs/components/AxisX/prepare-axis-data.js +58 -13
  3. package/dist/cjs/components/AxisX/types.d.ts +18 -1
  4. package/dist/cjs/components/AxisY/AxisY.js +31 -4
  5. package/dist/cjs/components/AxisY/prepare-axis-data.js +68 -21
  6. package/dist/cjs/components/AxisY/prepare-axis-title.js +8 -3
  7. package/dist/cjs/components/AxisY/styles.css +1 -1
  8. package/dist/cjs/components/AxisY/types.d.ts +18 -1
  9. package/dist/cjs/components/ChartInner/index.js +21 -15
  10. package/dist/cjs/components/ChartInner/useChartInnerHandlers.js +6 -5
  11. package/dist/cjs/components/ChartInner/useChartInnerProps.d.ts +3 -2
  12. package/dist/cjs/components/ChartInner/useChartInnerProps.js +22 -11
  13. package/dist/cjs/components/ChartInner/useDefaultState.js +4 -3
  14. package/dist/cjs/components/ChartInner/utils/chart.js +1 -1
  15. package/dist/cjs/components/ChartInner/utils/normalized-original-data.d.ts +1 -0
  16. package/dist/cjs/components/ChartInner/utils/title.d.ts +3 -2
  17. package/dist/cjs/components/ChartInner/utils/title.js +69 -11
  18. package/dist/cjs/components/Legend/index.js +8 -11
  19. package/dist/cjs/components/Legend/styles.css +1 -1
  20. package/dist/cjs/components/Title/index.js +3 -5
  21. package/dist/cjs/components/Tooltip/ChartTooltipContent.d.ts +2 -1
  22. package/dist/cjs/components/Tooltip/ChartTooltipContent.js +3 -2
  23. package/dist/cjs/components/Tooltip/index.js +2 -2
  24. package/dist/cjs/components/utils/axis-title.js +1 -1
  25. package/dist/cjs/core/axes/types.d.ts +26 -9
  26. package/dist/cjs/core/axes/x-axis.js +16 -3
  27. package/dist/cjs/core/axes/y-axis.js +21 -8
  28. package/dist/cjs/core/constants/defaults/axis.d.ts +1 -0
  29. package/dist/cjs/core/constants/defaults/axis.js +1 -0
  30. package/dist/cjs/core/layout/split.d.ts +2 -2
  31. package/dist/cjs/core/layout/split.js +22 -19
  32. package/dist/cjs/core/scales/y-scale.js +37 -13
  33. package/dist/cjs/core/series/prepare-legend.js +7 -7
  34. package/dist/cjs/core/series/types.d.ts +2 -0
  35. package/dist/cjs/core/types/chart/axis.d.ts +43 -1
  36. package/dist/cjs/core/types/chart/title.d.ts +10 -0
  37. package/dist/cjs/core/types/chart/tooltip.d.ts +3 -1
  38. package/dist/cjs/core/utils/axis-generators/bottom.js +6 -16
  39. package/dist/cjs/core/utils/common.d.ts +0 -4
  40. package/dist/cjs/core/utils/common.js +1 -14
  41. package/dist/cjs/core/utils/get-hovered-plots.d.ts +3 -2
  42. package/dist/cjs/core/utils/get-hovered-plots.js +28 -4
  43. package/dist/cjs/core/utils/labels.d.ts +1 -0
  44. package/dist/cjs/core/utils/labels.js +5 -5
  45. package/dist/cjs/core/utils/text.d.ts +1 -0
  46. package/dist/cjs/core/utils/text.js +16 -2
  47. package/dist/cjs/hooks/types.d.ts +5 -2
  48. package/dist/cjs/hooks/useShapes/area/prepare-data.js +12 -7
  49. package/dist/cjs/hooks/useShapes/bar-x/prepare-data.js +12 -4
  50. package/dist/cjs/hooks/useShapes/bar-y/prepare-data.js +3 -2
  51. package/dist/cjs/hooks/useShapes/funnel/prepare-data.js +4 -1
  52. package/dist/cjs/hooks/useShapes/heatmap/prepare-data.js +1 -1
  53. package/dist/cjs/hooks/useShapes/line/prepare-data.js +4 -1
  54. package/dist/cjs/hooks/useShapes/pie/prepare-data.js +9 -2
  55. package/dist/cjs/hooks/useShapes/radar/prepare-data.js +17 -7
  56. package/dist/cjs/hooks/useShapes/sankey/prepare-data.js +1 -1
  57. package/dist/cjs/hooks/useShapes/sankey/sankey-layout.d.ts +49 -0
  58. package/dist/cjs/hooks/useShapes/sankey/sankey-layout.js +362 -0
  59. package/dist/cjs/hooks/useShapes/styles.css +4 -4
  60. package/dist/cjs/hooks/useShapes/treemap/prepare-data.js +3 -1
  61. package/dist/cjs/hooks/useTooltip/index.d.ts +3 -2
  62. package/dist/cjs/hooks/useTooltip/index.js +5 -3
  63. package/dist/cjs/types/chart-ui.d.ts +1 -0
  64. package/dist/esm/components/AxisX/AxisX.js +31 -4
  65. package/dist/esm/components/AxisX/prepare-axis-data.js +58 -13
  66. package/dist/esm/components/AxisX/types.d.ts +18 -1
  67. package/dist/esm/components/AxisY/AxisY.js +31 -4
  68. package/dist/esm/components/AxisY/prepare-axis-data.js +68 -21
  69. package/dist/esm/components/AxisY/prepare-axis-title.js +8 -3
  70. package/dist/esm/components/AxisY/styles.css +1 -1
  71. package/dist/esm/components/AxisY/types.d.ts +18 -1
  72. package/dist/esm/components/ChartInner/index.js +21 -15
  73. package/dist/esm/components/ChartInner/useChartInnerHandlers.js +6 -5
  74. package/dist/esm/components/ChartInner/useChartInnerProps.d.ts +3 -2
  75. package/dist/esm/components/ChartInner/useChartInnerProps.js +22 -11
  76. package/dist/esm/components/ChartInner/useDefaultState.js +4 -3
  77. package/dist/esm/components/ChartInner/utils/chart.js +1 -1
  78. package/dist/esm/components/ChartInner/utils/normalized-original-data.d.ts +1 -0
  79. package/dist/esm/components/ChartInner/utils/title.d.ts +3 -2
  80. package/dist/esm/components/ChartInner/utils/title.js +69 -11
  81. package/dist/esm/components/Legend/index.js +8 -11
  82. package/dist/esm/components/Legend/styles.css +1 -1
  83. package/dist/esm/components/Title/index.js +3 -5
  84. package/dist/esm/components/Tooltip/ChartTooltipContent.d.ts +2 -1
  85. package/dist/esm/components/Tooltip/ChartTooltipContent.js +3 -2
  86. package/dist/esm/components/Tooltip/index.js +2 -2
  87. package/dist/esm/components/utils/axis-title.js +1 -1
  88. package/dist/esm/core/axes/types.d.ts +26 -9
  89. package/dist/esm/core/axes/x-axis.js +16 -3
  90. package/dist/esm/core/axes/y-axis.js +21 -8
  91. package/dist/esm/core/constants/defaults/axis.d.ts +1 -0
  92. package/dist/esm/core/constants/defaults/axis.js +1 -0
  93. package/dist/esm/core/layout/split.d.ts +2 -2
  94. package/dist/esm/core/layout/split.js +22 -19
  95. package/dist/esm/core/scales/y-scale.js +37 -13
  96. package/dist/esm/core/series/prepare-legend.js +7 -7
  97. package/dist/esm/core/series/types.d.ts +2 -0
  98. package/dist/esm/core/types/chart/axis.d.ts +43 -1
  99. package/dist/esm/core/types/chart/title.d.ts +10 -0
  100. package/dist/esm/core/types/chart/tooltip.d.ts +3 -1
  101. package/dist/esm/core/utils/axis-generators/bottom.js +6 -16
  102. package/dist/esm/core/utils/common.d.ts +0 -4
  103. package/dist/esm/core/utils/common.js +1 -14
  104. package/dist/esm/core/utils/get-hovered-plots.d.ts +3 -2
  105. package/dist/esm/core/utils/get-hovered-plots.js +28 -4
  106. package/dist/esm/core/utils/labels.d.ts +1 -0
  107. package/dist/esm/core/utils/labels.js +5 -5
  108. package/dist/esm/core/utils/text.d.ts +1 -0
  109. package/dist/esm/core/utils/text.js +16 -2
  110. package/dist/esm/hooks/types.d.ts +5 -2
  111. package/dist/esm/hooks/useShapes/area/prepare-data.js +12 -7
  112. package/dist/esm/hooks/useShapes/bar-x/prepare-data.js +12 -4
  113. package/dist/esm/hooks/useShapes/bar-y/prepare-data.js +3 -2
  114. package/dist/esm/hooks/useShapes/funnel/prepare-data.js +4 -1
  115. package/dist/esm/hooks/useShapes/heatmap/prepare-data.js +1 -1
  116. package/dist/esm/hooks/useShapes/line/prepare-data.js +4 -1
  117. package/dist/esm/hooks/useShapes/pie/prepare-data.js +9 -2
  118. package/dist/esm/hooks/useShapes/radar/prepare-data.js +17 -7
  119. package/dist/esm/hooks/useShapes/sankey/prepare-data.js +1 -1
  120. package/dist/esm/hooks/useShapes/sankey/sankey-layout.d.ts +49 -0
  121. package/dist/esm/hooks/useShapes/sankey/sankey-layout.js +362 -0
  122. package/dist/esm/hooks/useShapes/styles.css +4 -4
  123. package/dist/esm/hooks/useShapes/treemap/prepare-data.js +3 -1
  124. package/dist/esm/hooks/useTooltip/index.d.ts +3 -2
  125. package/dist/esm/hooks/useTooltip/index.js +5 -3
  126. package/dist/esm/types/chart-ui.d.ts +1 -0
  127. package/package.json +1 -3
@@ -1,5 +1,6 @@
1
1
  import { getUniqId } from '@gravity-ui/uikit';
2
- import { calculateSin, formatAxisTickLabel, getBandsPosition, getLabelsSize, getMinSpaceBetween, getTextSizeFn, getTextWithElipsis, } from '../../core/utils';
2
+ import { select } from 'd3-selection';
3
+ import { calculateCos, calculateSin, formatAxisTickLabel, getBandsPosition, getLabelsSize, getMinSpaceBetween, getTextSizeFn, getTextWithElipsis, } from '../../core/utils';
3
4
  import { getXAxisTickValues } from '../../core/utils/axis/x-axis';
4
5
  import { getMultilineTitleContentRows } from '../utils/axis-title';
5
6
  async function getSvgAxisLabel({ getTextSize, text, axis, top, left, labelMaxWidth, axisWidth, boundsOffsetLeft, boundsOffsetRight, }) {
@@ -56,7 +57,9 @@ async function getSvgAxisLabel({ getTextSize, text, axis, top, left, labelMaxWid
56
57
  x = left + actualTextWidth / 2 - xOffset;
57
58
  }
58
59
  const yOffset = rotation <= 0 ? textSize.width * calculateSin(a) : 0;
59
- const y = top + yOffset + axis.labels.margin;
60
+ const hOffset = textSize.hangingOffset;
61
+ const y = top + yOffset + axis.labels.margin + hOffset * calculateCos(rotation);
62
+ x -= hOffset * calculateSin(rotation);
60
63
  const svgLabel = {
61
64
  title: ((_a = content[0]) === null || _a === void 0 ? void 0 : _a.text) === text ? undefined : text,
62
65
  content,
@@ -69,7 +72,7 @@ async function getSvgAxisLabel({ getTextSize, text, axis, top, left, labelMaxWid
69
72
  return svgLabel;
70
73
  }
71
74
  export async function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRight, boundsWidth, height, scale, series, split, yAxis, }) {
72
- var _a, _b, _c, _d, _e, _f, _g;
75
+ var _a, _b, _c, _d, _e, _f, _g, _h;
73
76
  const xAxisItems = [];
74
77
  const splitPlots = (_a = split === null || split === void 0 ? void 0 : split.plots) !== null && _a !== void 0 ? _a : [];
75
78
  for (let plotIndex = 0; plotIndex < splitPlots.length; plotIndex++) {
@@ -201,7 +204,8 @@ export async function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRig
201
204
  const titleContent = [];
202
205
  const titleMaxWidth = axisWidth;
203
206
  if (axis.title.maxRowCount > 1) {
204
- titleContent.push(...(await getMultilineTitleContentRows({ axis, titleMaxWidth })));
207
+ const rows = await getMultilineTitleContentRows({ axis, titleMaxWidth });
208
+ titleContent.push(...rows);
205
209
  }
206
210
  else {
207
211
  const text = await getTextWithElipsis({
@@ -209,11 +213,12 @@ export async function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRig
209
213
  maxWidth: titleMaxWidth,
210
214
  getTextWidth: async (s) => (await getTitleTextSize(s)).width,
211
215
  });
216
+ const titleSize = await getTitleTextSize(text);
212
217
  titleContent.push({
213
218
  text,
214
219
  x: 0,
215
- y: 0,
216
- size: await getTitleTextSize(text),
220
+ y: titleSize.hangingOffset,
221
+ size: titleSize,
217
222
  });
218
223
  }
219
224
  const titleTextSize = titleContent.reduce((acc, item) => {
@@ -258,14 +263,14 @@ export async function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRig
258
263
  const halfBandwidth = ((_f = (_e = axisScale.bandwidth) === null || _e === void 0 ? void 0 : _e.call(axisScale)) !== null && _f !== void 0 ? _f : 0) / 2;
259
264
  const startPos = halfBandwidth + Math.min(from, to);
260
265
  const endPos = Math.min(Math.abs(to - from), axisWidth - Math.min(from, to));
261
- const getPlotLabelSize = getTextSizeFn({ style: plotBand.label.style });
262
- const labelSize = plotBand.label.text
263
- ? await getPlotLabelSize(plotBand.label.text)
264
- : null;
265
266
  const plotBandWidth = Math.min(endPos, axisWidth);
266
267
  if (plotBandWidth < 0) {
267
268
  continue;
268
269
  }
270
+ const getPlotLabelSize = getTextSizeFn({ style: plotBand.label.style });
271
+ const labelSize = plotBand.label.text
272
+ ? await getPlotLabelSize(plotBand.label.text)
273
+ : null;
269
274
  plotBands.push({
270
275
  layerPlacement: plotBand.layerPlacement,
271
276
  x: Math.max(0, startPos),
@@ -278,8 +283,8 @@ export async function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRig
278
283
  ? {
279
284
  text: plotBand.label.text,
280
285
  style: plotBand.label.style,
281
- x: plotBand.label.padding,
282
- y: plotBand.label.padding + ((_g = labelSize === null || labelSize === void 0 ? void 0 : labelSize.width) !== null && _g !== void 0 ? _g : 0),
286
+ x: plotBand.label.padding + ((_g = labelSize === null || labelSize === void 0 ? void 0 : labelSize.hangingOffset) !== null && _g !== void 0 ? _g : 0),
287
+ y: plotBand.label.padding + ((_h = labelSize === null || labelSize === void 0 ? void 0 : labelSize.width) !== null && _h !== void 0 ? _h : 0),
283
288
  rotate: -90,
284
289
  qa: plotBand.label.qa,
285
290
  }
@@ -305,7 +310,7 @@ export async function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRig
305
310
  label = {
306
311
  text: plotLine.label.text,
307
312
  style: plotLine.label.style,
308
- x: plotLineValue - plotLine.label.padding - size.height,
313
+ x: plotLineValue - plotLine.label.padding - size.height + size.hangingOffset,
309
314
  y: plotLine.label.padding + size.width,
310
315
  rotate: -90,
311
316
  qa: plotLine.label.qa,
@@ -324,6 +329,45 @@ export async function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRig
324
329
  dashStyle: plotLine.dashStyle,
325
330
  });
326
331
  }
332
+ const plotShapes = [];
333
+ const measureContainer = axis.plotShapes.length
334
+ ? select(document.body)
335
+ .append('svg')
336
+ .style('visibility', 'hidden')
337
+ .style('position', 'absolute')
338
+ .style('top', '-200vw')
339
+ : null;
340
+ for (let i = 0; i < axis.plotShapes.length; i++) {
341
+ if (!measureContainer) {
342
+ break;
343
+ }
344
+ const plotShape = axis.plotShapes[i];
345
+ const axisScale = scale;
346
+ const shapeX = Number(axisScale(plotShape.value));
347
+ if (shapeX < 0 || shapeX > boundsWidth) {
348
+ continue;
349
+ }
350
+ const markup = plotShape.renderer({
351
+ x: shapeX,
352
+ y: 0,
353
+ plotHeight: axisHeight,
354
+ plotWidth: axisWidth,
355
+ });
356
+ const wrapper = measureContainer.append('g').html(markup);
357
+ const bbox = wrapper.node().getBBox();
358
+ wrapper.remove();
359
+ plotShapes.push({
360
+ hitbox: { x: bbox.x, y: bbox.y, width: bbox.width, height: bbox.height },
361
+ layerPlacement: plotShape.layerPlacement,
362
+ opacity: plotShape.opacity,
363
+ plotHeight: axisHeight,
364
+ plotWidth: axisWidth,
365
+ renderer: plotShape.renderer,
366
+ x: shapeX,
367
+ y: axisTop,
368
+ });
369
+ }
370
+ measureContainer === null || measureContainer === void 0 ? void 0 : measureContainer.remove();
327
371
  xAxisItems.push({
328
372
  id: getUniqId(),
329
373
  gridEnabled: axis.grid.enabled,
@@ -332,6 +376,7 @@ export async function prepareXAxisData({ axis, boundsOffsetLeft, boundsOffsetRig
332
376
  domain,
333
377
  plotBands,
334
378
  plotLines,
379
+ plotShapes,
335
380
  });
336
381
  }
337
382
  return xAxisItems;
@@ -1,4 +1,5 @@
1
1
  import type { DashStyle } from 'src/core/constants';
2
+ import type { AxisPlotShape } from '../../core/types/chart/axis';
2
3
  import type { BaseTextStyle, HtmlItem, PlotLayerPlacement, PointPosition } from '../../types';
3
4
  import type { TextRowData } from '../types';
4
5
  export type AxisSvgLabelData = {
@@ -67,6 +68,21 @@ export type AxisPlotBandData = {
67
68
  opacity: number;
68
69
  label: AxisPlotLineLabel | null;
69
70
  };
71
+ export type AxisPlotShapeData = {
72
+ hitbox: {
73
+ x: number;
74
+ y: number;
75
+ width: number;
76
+ height: number;
77
+ };
78
+ layerPlacement: PlotLayerPlacement;
79
+ opacity: number;
80
+ plotHeight: number;
81
+ plotWidth: number;
82
+ renderer: AxisPlotShape['renderer'];
83
+ x: number;
84
+ y: number;
85
+ };
70
86
  export type AxisDomainData = {
71
87
  start: [number, number];
72
88
  end: [number, number];
@@ -78,6 +94,7 @@ export type AxisXData = {
78
94
  title: AxisTitleData | null;
79
95
  domain: AxisDomainData | null;
80
96
  ticks: AxisTickData[];
81
- plotLines: AxisPlotLineData[];
82
97
  plotBands: AxisPlotBandData[];
98
+ plotLines: AxisPlotLineData[];
99
+ plotShapes: AxisPlotShapeData[];
83
100
  };
@@ -28,6 +28,7 @@ export const AxisY = (props) => {
28
28
  const plotDataAttr = 'data-plot-y';
29
29
  const plotBandDataAttr = `data-plot-y-band-${preparedAxisData.id}`;
30
30
  const plotLineDataAttr = `data-plot-y-line-${preparedAxisData.id}`;
31
+ const plotShapeDataAttr = `data-plot-y-shape-${preparedAxisData.id}`;
31
32
  if (plotBeforeRef === null || plotBeforeRef === void 0 ? void 0 : plotBeforeRef.current) {
32
33
  plotBeforeContainer = select(plotBeforeRef.current);
33
34
  }
@@ -50,7 +51,7 @@ export const AxisY = (props) => {
50
51
  .html((d) => d.text)
51
52
  .attr('x', (d) => d.x)
52
53
  .attr('y', (d) => d.y)
53
- .attr('dominant-baseline', 'text-after-edge')
54
+ .attr('dominant-baseline', 'hanging')
54
55
  .attr('text-anchor', 'start');
55
56
  }
56
57
  if (preparedAxisData.domain) {
@@ -103,7 +104,7 @@ export const AxisY = (props) => {
103
104
  .attr('y', (d) => d.y)
104
105
  .attr('text-anchor', 'start')
105
106
  .attr('class', labelClassName)
106
- .style('dominant-baseline', 'text-before-edge')
107
+ .style('dominant-baseline', 'hanging')
107
108
  .style('font-size', label.style.fontSize)
108
109
  .style('fill', (_a = label.style.fontColor) !== null && _a !== void 0 ? _a : '');
109
110
  }
@@ -139,7 +140,7 @@ export const AxisY = (props) => {
139
140
  .style('fill', (_a = label.style.fontColor) !== null && _a !== void 0 ? _a : '')
140
141
  .style('font-size', label.style.fontSize)
141
142
  .style('font-weight', (_b = label.style.fontWeight) !== null && _b !== void 0 ? _b : '')
142
- .style('dominant-baseline', 'text-before-edge')
143
+ .style('dominant-baseline', 'hanging')
143
144
  .attr('data-qa', (_c = label.qa) !== null && _c !== void 0 ? _c : null)
144
145
  .attr('x', label.x)
145
146
  .attr('y', label.y);
@@ -181,7 +182,7 @@ export const AxisY = (props) => {
181
182
  .style('fill', (_a = label.style.fontColor) !== null && _a !== void 0 ? _a : '')
182
183
  .style('font-size', label.style.fontSize)
183
184
  .style('font-weight', (_b = label.style.fontWeight) !== null && _b !== void 0 ? _b : '')
184
- .style('dominant-baseline', 'text-before-edge')
185
+ .style('dominant-baseline', 'hanging')
185
186
  .attr('data-qa', (_c = label.qa) !== null && _c !== void 0 ? _c : null)
186
187
  .attr('x', label.x)
187
188
  .attr('y', label.y);
@@ -191,14 +192,40 @@ export const AxisY = (props) => {
191
192
  setPlotLines(plotBeforeContainer, preparedAxisData.plotLines.filter((item) => item.layerPlacement === 'before'));
192
193
  setPlotLines(plotAfterContainer, preparedAxisData.plotLines.filter((item) => item.layerPlacement === 'after'));
193
194
  }
195
+ if (preparedAxisData.plotShapes.length > 0) {
196
+ const setPlotShapes = (plotContainer, plotShapes) => {
197
+ if (!plotContainer || !plotShapes.length) {
198
+ return;
199
+ }
200
+ plotContainer
201
+ .selectAll(`[${plotShapeDataAttr}]`)
202
+ .remove()
203
+ .data(plotShapes)
204
+ .join('g')
205
+ .attr(plotDataAttr, 1)
206
+ .attr(plotShapeDataAttr, 1)
207
+ .attr('transform', (d) => `translate(${d.x}, ${d.y})`)
208
+ .attr('opacity', (d) => d.opacity)
209
+ .html((d) => d.renderer({
210
+ x: 0,
211
+ y: d.y,
212
+ plotHeight: d.plotHeight,
213
+ plotWidth: d.plotWidth,
214
+ }));
215
+ };
216
+ setPlotShapes(plotBeforeContainer, preparedAxisData.plotShapes.filter((item) => item.layerPlacement === 'before'));
217
+ setPlotShapes(plotAfterContainer, preparedAxisData.plotShapes.filter((item) => item.layerPlacement === 'after'));
218
+ }
194
219
  return () => {
195
220
  if (plotBeforeContainer) {
196
221
  plotBeforeContainer.selectAll(`[${plotBandDataAttr}]`).remove();
197
222
  plotBeforeContainer.selectAll(`[${plotLineDataAttr}]`).remove();
223
+ plotBeforeContainer.selectAll(`[${plotShapeDataAttr}]`).remove();
198
224
  }
199
225
  if (plotAfterContainer) {
200
226
  plotAfterContainer.selectAll(`[${plotBandDataAttr}]`).remove();
201
227
  plotAfterContainer.selectAll(`[${plotLineDataAttr}]`).remove();
228
+ plotAfterContainer.selectAll(`[${plotShapeDataAttr}]`).remove();
202
229
  }
203
230
  };
204
231
  }, [lineGenerator, plotAfterRef, plotBeforeRef, preparedAxisData]);
@@ -1,9 +1,10 @@
1
1
  import { getUniqId } from '@gravity-ui/uikit';
2
+ import { select } from 'd3-selection';
2
3
  import { calculateCos, calculateSin, formatAxisTickLabel, getBandsPosition, getLabelsSize, getMinSpaceBetween, getTextSizeFn, getTextWithElipsis, wrapText, } from '../../core/utils';
3
4
  import { prepareHtmlYAxisTitle, prepareSvgYAxisTitle } from './prepare-axis-title';
4
5
  import { getTickValues } from './utils';
5
6
  async function getSvgAxisLabel({ getTextSize, text, axis, top, left, labelMaxHeight, topOffset, }) {
6
- var _a;
7
+ var _a, _b;
7
8
  const originalTextSize = await getTextSize(text);
8
9
  // Currently, a preliminary label calculation is used to build the chart - we cannot exceed it here.
9
10
  // Therefore, we rely on a pre-calculated number instead of the current maximum label width.
@@ -59,7 +60,9 @@ async function getSvgAxisLabel({ getTextSize, text, axis, top, left, labelMaxHei
59
60
  }
60
61
  }
61
62
  content.forEach((row) => {
63
+ var _a;
62
64
  row.y -= newLabelHeight / 2;
65
+ row.y += (_a = originalTextSize.hangingOffset) !== null && _a !== void 0 ? _a : 0;
63
66
  });
64
67
  size.width = newLabelWidth;
65
68
  size.height = newLabelHeight;
@@ -87,7 +90,8 @@ async function getSvgAxisLabel({ getTextSize, text, axis, top, left, labelMaxHei
87
90
  ? textSize.height / calculateSin(axis.labels.rotation)
88
91
  : textSize.height;
89
92
  const x = axis.position === 'left' ? -textSize.width : 0;
90
- const y = Math.max(-topOffset - top, -actualTextHeight / 2);
93
+ const y = Math.max(-topOffset - top, -actualTextHeight / 2) +
94
+ ((_a = originalTextSize.hangingOffset) !== null && _a !== void 0 ? _a : 0);
91
95
  content.push({
92
96
  text: rowText,
93
97
  x,
@@ -98,7 +102,7 @@ async function getSvgAxisLabel({ getTextSize, text, axis, top, left, labelMaxHei
98
102
  const x = axis.position === 'left' ? left - axis.labels.margin : left + axis.labels.margin;
99
103
  const y = top;
100
104
  const svgLabel = {
101
- title: content.length > 1 || ((_a = content[0]) === null || _a === void 0 ? void 0 : _a.text) !== text ? text : undefined,
105
+ title: content.length > 1 || ((_b = content[0]) === null || _b === void 0 ? void 0 : _b.text) !== text ? text : undefined,
102
106
  content: content,
103
107
  style: axis.labels.style,
104
108
  size: size,
@@ -109,7 +113,7 @@ async function getSvgAxisLabel({ getTextSize, text, axis, top, left, labelMaxHei
109
113
  return svgLabel;
110
114
  }
111
115
  export async function prepareYAxisData({ axis, split, scale, top: topOffset, width, height, series, }) {
112
- var _a, _b, _c;
116
+ var _a, _b, _c, _d, _e;
113
117
  const axisPlotTopPosition = ((_a = split === null || split === void 0 ? void 0 : split.plots[axis.plotIndex]) === null || _a === void 0 ? void 0 : _a.top) || 0;
114
118
  const axisHeight = ((_b = split === null || split === void 0 ? void 0 : split.plots[axis.plotIndex]) === null || _b === void 0 ? void 0 : _b.height) || height;
115
119
  const domainX = axis.position === 'left' ? 0 : width;
@@ -212,23 +216,22 @@ export async function prepareYAxisData({ axis, split, scale, top: topOffset, wid
212
216
  axisLabelsWidth: labelsWidth,
213
217
  });
214
218
  const plotBands = [];
215
- axis.plotBands.forEach((plotBand) => {
216
- var _a, _b;
219
+ for (const plotBand of axis.plotBands) {
217
220
  const axisScale = scale;
218
221
  const { from, to } = getBandsPosition({
219
222
  band: plotBand,
220
223
  axisScale,
221
224
  axis: 'y',
222
225
  });
223
- const halfBandwidth = ((_b = (_a = axisScale.bandwidth) === null || _a === void 0 ? void 0 : _a.call(axisScale)) !== null && _b !== void 0 ? _b : 0) / 2;
226
+ const halfBandwidth = ((_e = (_d = axisScale.bandwidth) === null || _d === void 0 ? void 0 : _d.call(axisScale)) !== null && _e !== void 0 ? _e : 0) / 2;
224
227
  const startPos = halfBandwidth + Math.min(from, to);
225
228
  const endPos = Math.min(Math.abs(to - from), axisHeight - Math.min(from, to));
226
229
  const top = Math.max(0, startPos);
227
230
  const plotBandHeight = Math.min(endPos, axisHeight);
228
231
  if (plotBandHeight < 0) {
229
- return;
232
+ continue;
230
233
  }
231
- plotBands.push({
234
+ const plotBandItem = {
232
235
  layerPlacement: plotBand.layerPlacement,
233
236
  x: 0,
234
237
  y: axisPlotTopPosition + top,
@@ -236,17 +239,21 @@ export async function prepareYAxisData({ axis, split, scale, top: topOffset, wid
236
239
  height: plotBandHeight,
237
240
  color: plotBand.color,
238
241
  opacity: plotBand.opacity,
239
- label: plotBand.label.text
240
- ? {
241
- text: plotBand.label.text,
242
- style: plotBand.label.style,
243
- x: plotBand.label.padding,
244
- y: plotBand.label.padding,
245
- qa: plotBand.label.qa,
246
- }
247
- : null,
248
- });
249
- });
242
+ label: null,
243
+ };
244
+ if (plotBand.label.text) {
245
+ const getPlotLabelSize = getTextSizeFn({ style: plotBand.label.style });
246
+ const labelSize = await getPlotLabelSize(plotBand.label.text);
247
+ plotBandItem.label = {
248
+ text: plotBand.label.text,
249
+ style: plotBand.label.style,
250
+ x: plotBand.label.padding,
251
+ y: plotBand.label.padding + labelSize.hangingOffset,
252
+ qa: plotBand.label.qa,
253
+ };
254
+ }
255
+ plotBands.push(plotBandItem);
256
+ }
250
257
  const plotLines = [];
251
258
  for (let i = 0; i < axis.plotLines.length; i++) {
252
259
  const plotLine = axis.plotLines[i];
@@ -267,7 +274,7 @@ export async function prepareYAxisData({ axis, split, scale, top: topOffset, wid
267
274
  text: plotLine.label.text,
268
275
  style: plotLine.label.style,
269
276
  x: plotLine.label.padding,
270
- y: Math.max(0, plotLineValue - size.height - plotLine.label.padding),
277
+ y: Math.max(0, plotLineValue - size.height - plotLine.label.padding + size.hangingOffset),
271
278
  qa: plotLine.label.qa,
272
279
  };
273
280
  }
@@ -284,6 +291,45 @@ export async function prepareYAxisData({ axis, split, scale, top: topOffset, wid
284
291
  dashStyle: plotLine.dashStyle,
285
292
  });
286
293
  }
294
+ const plotShapes = [];
295
+ const measureContainer = axis.plotShapes.length
296
+ ? select(document.body)
297
+ .append('svg')
298
+ .style('visibility', 'hidden')
299
+ .style('position', 'absolute')
300
+ .style('top', '-200vw')
301
+ : null;
302
+ for (let i = 0; i < axis.plotShapes.length; i++) {
303
+ if (!measureContainer) {
304
+ break;
305
+ }
306
+ const plotShape = axis.plotShapes[i];
307
+ const axisScale = scale;
308
+ const shapeY = Number(axisScale(plotShape.value));
309
+ if (shapeY < 0 || shapeY > axisHeight) {
310
+ continue;
311
+ }
312
+ const markup = plotShape.renderer({
313
+ x: 0,
314
+ y: shapeY,
315
+ plotHeight: axisHeight,
316
+ plotWidth: width,
317
+ });
318
+ const wrapper = measureContainer.append('g').html(markup);
319
+ const bbox = wrapper.node().getBBox();
320
+ wrapper.remove();
321
+ plotShapes.push({
322
+ hitbox: { x: bbox.x, y: bbox.y, width: bbox.width, height: bbox.height },
323
+ layerPlacement: plotShape.layerPlacement,
324
+ opacity: plotShape.opacity,
325
+ plotHeight: axisHeight,
326
+ plotWidth: width,
327
+ renderer: plotShape.renderer,
328
+ x: 0,
329
+ y: axisPlotTopPosition + shapeY,
330
+ });
331
+ }
332
+ measureContainer === null || measureContainer === void 0 ? void 0 : measureContainer.remove();
287
333
  return {
288
334
  id: getUniqId(),
289
335
  gridEnabled: axis.grid.enabled,
@@ -292,5 +338,6 @@ export async function prepareYAxisData({ axis, split, scale, top: topOffset, wid
292
338
  domain,
293
339
  plotBands,
294
340
  plotLines,
341
+ plotShapes,
295
342
  };
296
343
  }
@@ -11,7 +11,11 @@ export async function prepareSvgYAxisTitle({ axis, axisTop, axisHeight, axisWidt
11
11
  const titleContent = [];
12
12
  const titleMaxWidth = rotateAngle === 0 ? axis.title.maxWidth : sin * axisHeight;
13
13
  if (axis.title.maxRowCount > 1) {
14
- titleContent.push(...(await getMultilineTitleContentRows({ axis, titleMaxWidth })));
14
+ const rows = await getMultilineTitleContentRows({ axis, titleMaxWidth });
15
+ rows.forEach((r) => {
16
+ r.y -= r.size.height;
17
+ });
18
+ titleContent.push(...rows);
15
19
  }
16
20
  else {
17
21
  const text = await getTextWithElipsis({
@@ -19,11 +23,12 @@ export async function prepareSvgYAxisTitle({ axis, axisTop, axisHeight, axisWidt
19
23
  maxWidth: titleMaxWidth,
20
24
  getTextWidth: async (s) => (await getTitleTextSize(s)).width,
21
25
  });
26
+ const titleSize = await getTitleTextSize(text);
22
27
  titleContent.push({
23
28
  text,
24
29
  x: 0,
25
- y: 0,
26
- size: await getTitleTextSize(text),
30
+ y: titleSize.hangingOffset - titleSize.height,
31
+ size: titleSize,
27
32
  });
28
33
  }
29
34
  const originalTextSize = titleContent.reduce((acc, item) => {
@@ -4,7 +4,7 @@
4
4
  .gcharts-y-axis__label {
5
5
  fill: var(--g-color-text-secondary);
6
6
  stroke: none;
7
- dominant-baseline: text-after-edge;
7
+ dominant-baseline: text-bottom;
8
8
  }
9
9
  .gcharts-y-axis__tick {
10
10
  stroke: var(--gcharts-axis-tick-color);
@@ -1,4 +1,5 @@
1
1
  import type { DashStyle } from 'src/core/constants';
2
+ import type { AxisPlotShape } from '../../core/types/chart/axis';
2
3
  import type { BaseTextStyle, HtmlItem, PlotLayerPlacement, PointPosition } from '../../types';
3
4
  import type { TextRowData } from '../types';
4
5
  export type AxisSvgLabelData = {
@@ -78,6 +79,21 @@ export type AxisPlotBandData = {
78
79
  opacity: number;
79
80
  label: AxisPlotLineLabel | null;
80
81
  };
82
+ export type AxisPlotShapeData = {
83
+ hitbox: {
84
+ x: number;
85
+ y: number;
86
+ width: number;
87
+ height: number;
88
+ };
89
+ layerPlacement: PlotLayerPlacement;
90
+ opacity: number;
91
+ plotHeight: number;
92
+ plotWidth: number;
93
+ renderer: AxisPlotShape['renderer'];
94
+ x: number;
95
+ y: number;
96
+ };
81
97
  export type AxisDomainData = {
82
98
  start: [number, number];
83
99
  end: [number, number];
@@ -89,6 +105,7 @@ export type AxisYData = {
89
105
  title: HtmlAxisTitleData | SvgAxisTitleData | null;
90
106
  domain: AxisDomainData | null;
91
107
  ticks: AxisTickData[];
92
- plotLines: AxisPlotLineData[];
93
108
  plotBands: AxisPlotBandData[];
109
+ plotLines: AxisPlotLineData[];
110
+ plotShapes: AxisPlotShapeData[];
94
111
  };
@@ -20,7 +20,7 @@ import { useChartInnerHandlers } from './useChartInnerHandlers';
20
20
  import { useChartInnerProps } from './useChartInnerProps';
21
21
  import { useChartInnerState } from './useChartInnerState';
22
22
  import { useDefaultState } from './useDefaultState';
23
- import { getPreparedChart, getPreparedTitle, getPreparedTooltip, getResetZoomButtonStyle, useAsyncState, useDebouncedValue, } from './utils';
23
+ import { getPreparedTooltip, getResetZoomButtonStyle, useAsyncState, useDebouncedValue, } from './utils';
24
24
  import './styles.css';
25
25
  const b = block('chart');
26
26
  const DEBOUNCED_VALUE_DELAY = 10;
@@ -36,16 +36,6 @@ export const ChartInner = (props) => {
36
36
  const rangeSliderRef = React.useRef(null);
37
37
  const dispatcher = React.useMemo(() => getDispatcher(), []);
38
38
  const clipPathId = useUniqId();
39
- const preparedTitle = React.useMemo(() => {
40
- return getPreparedTitle({ title: data.title });
41
- }, [data.title]);
42
- const preparedChart = React.useMemo(() => {
43
- return getPreparedChart({
44
- chart: data.chart,
45
- seriesData: data.series.data,
46
- preparedTitle,
47
- });
48
- }, [data.chart, data.series.data, preparedTitle]);
49
39
  const preparedTooltip = React.useMemo(() => {
50
40
  return getPreparedTooltip({
51
41
  tooltip: data.tooltip,
@@ -62,10 +52,9 @@ export const ChartInner = (props) => {
62
52
  preparedRangeSlider,
63
53
  tooltip: preparedTooltip,
64
54
  });
65
- const { activeLegendItems, allPreparedSeries, boundsHeight, boundsOffsetLeft, boundsOffsetTop, boundsWidth, handleLegendItemClick, legendConfig, legendItems, preparedLegend, preparedSeries, preparedSeriesOptions, preparedSplit, shapes, shapesData, shapesReady, xAxis, xScale, yAxis, yScale, } = useChartInnerProps(Object.assign(Object.assign({}, props), { clipPathId,
55
+ const { activeLegendItems, allPreparedSeries, boundsHeight, boundsOffsetLeft, boundsOffsetTop, boundsWidth, handleLegendItemClick, legendConfig, legendItems, preparedLegend, preparedSeries, preparedSeriesOptions, preparedSplit, shapes, shapesData, shapesReady, xAxis, xScale, yAxis, yScale, preparedTitle, preparedChart, } = useChartInnerProps(Object.assign(Object.assign({}, props), { clipPathId,
66
56
  dispatcher,
67
- htmlLayout, plotNode: plotRef.current, preparedChart,
68
- rangeSliderState,
57
+ htmlLayout, plotNode: plotRef.current, rangeSliderState,
69
58
  updateZoomState,
70
59
  zoomState }));
71
60
  const prevWidth = usePrevious(width);
@@ -165,6 +154,13 @@ export const ChartInner = (props) => {
165
154
  split: preparedSplit,
166
155
  series: preparedSeries.filter((s) => s.visible),
167
156
  });
157
+ axisData.plotShapes.forEach((shapeData, j) => {
158
+ if (axis.plotShapes[j]) {
159
+ axis.plotShapes[j].hitbox = shapeData.hitbox;
160
+ axis.plotShapes[j].x = shapeData.x;
161
+ axis.plotShapes[j].y = shapeData.y;
162
+ }
163
+ });
168
164
  items.push(axisData);
169
165
  }
170
166
  }
@@ -191,6 +187,15 @@ export const ChartInner = (props) => {
191
187
  split: preparedSplit,
192
188
  yAxis,
193
189
  });
190
+ axisData.forEach((data) => {
191
+ data.plotShapes.forEach((shapeData, i) => {
192
+ if (axis.plotShapes[i]) {
193
+ axis.plotShapes[i].hitbox = shapeData.hitbox;
194
+ axis.plotShapes[i].x = shapeData.x;
195
+ axis.plotShapes[i].y = shapeData.y;
196
+ }
197
+ });
198
+ });
194
199
  items.push(...axisData);
195
200
  }
196
201
  return items;
@@ -269,6 +274,7 @@ export const ChartInner = (props) => {
269
274
  shapes,
270
275
  React.createElement("g", { ref: plotAfterRef })),
271
276
  ((_e = xAxis === null || xAxis === void 0 ? void 0 : xAxis.rangeSlider) === null || _e === void 0 ? void 0 : _e.enabled) &&
277
+ preparedChart &&
272
278
  preparedLegend &&
273
279
  debouncedAllPreparedSeries &&
274
280
  preparedSeriesOptions && (React.createElement(RangeSlider, { activeLegendItems: activeLegendItems !== null && activeLegendItems !== void 0 ? activeLegendItems : [], boundsOffsetLeft: debouncedOffsetLeft, boundsWidth: debouncedBoundsWidth, height: height, htmlLayout: htmlLayout, onUpdate: updateRangeSliderState, preparedChart: preparedChart, preparedLegend: preparedLegend, preparedSeries: debouncedAllPreparedSeries, preparedSeriesOptions: preparedSeriesOptions, preparedRangeSlider: xAxis.rangeSlider, rangeSliderState: rangeSliderState, ref: rangeSliderRef, width: width, xAxis: data.xAxis, yAxis: data.yAxis, legendConfig: legendConfig })),
@@ -281,7 +287,7 @@ export const ChartInner = (props) => {
281
287
  React.createElement("div", { className: b('html-layer'), ref: setHtmlLayout, style: {
282
288
  '--g-html-layout-transform': `translate(${boundsOffsetLeft}px, ${boundsOffsetTop}px)`,
283
289
  } }),
284
- Object.keys(zoomState).length > 0 && preparedChart.zoom && (React.createElement(Button, { className: b('reset-zoom-button'), onClick: () => {
290
+ Object.keys(zoomState).length > 0 && (preparedChart === null || preparedChart === void 0 ? void 0 : preparedChart.zoom) && (React.createElement(Button, { className: b('reset-zoom-button'), onClick: () => {
285
291
  var _a;
286
292
  updateZoomState({});
287
293
  (_a = rangeSliderRef.current) === null || _a === void 0 ? void 0 : _a.resetState();
@@ -29,7 +29,7 @@ export function useChartInnerHandlers(props) {
29
29
  boundsHeight,
30
30
  boundsWidth,
31
31
  });
32
- const { plotLines, plotBands } = getHoveredPlots({
32
+ const { plotBands, plotLines, plotShapes } = getHoveredPlots({
33
33
  pointerX: x,
34
34
  pointerY: y,
35
35
  xAxis,
@@ -37,7 +37,7 @@ export function useChartInnerHandlers(props) {
37
37
  xScale,
38
38
  yScale,
39
39
  });
40
- const hoveredPlotsArg = { lines: plotLines, bands: plotBands };
40
+ const hoveredPlotsArg = { bands: plotBands, lines: plotLines, shapes: plotShapes };
41
41
  dispatcher.call(EventType.HOVER_SHAPE, event.target, closest, [pointerX, pointerY], hoveredPlotsArg);
42
42
  dispatcher.call(EventType.POINTERMOVE_CHART, {}, {
43
43
  hovered: closest,
@@ -93,7 +93,7 @@ export function useChartInnerHandlers(props) {
93
93
  dispatcher.call(EventType.CLICK_CHART, undefined, { point: selected.data, series: selected.series }, event);
94
94
  const nextTooltipFixed = !tooltipPinned;
95
95
  if (!nextTooltipFixed) {
96
- const { plotLines, plotBands } = getHoveredPlots({
96
+ const { plotBands, plotLines, plotShapes } = getHoveredPlots({
97
97
  pointerX: x,
98
98
  pointerY: y,
99
99
  xAxis,
@@ -101,14 +101,15 @@ export function useChartInnerHandlers(props) {
101
101
  xScale,
102
102
  yScale,
103
103
  });
104
- const hoveredPlotsArg = { lines: plotLines, bands: plotBands };
104
+ const hoveredPlotsArg = { bands: plotBands, lines: plotLines, shapes: plotShapes };
105
105
  dispatcher.call(EventType.HOVER_SHAPE, event.target, items, [pointerX, pointerY], hoveredPlotsArg);
106
106
  dispatcher.call(EventType.POINTERMOVE_CHART, {}, {
107
107
  hovered: items,
108
108
  xAxis,
109
109
  yAxis: yAxis[0],
110
- hoveredPlotLines: plotLines,
111
110
  hoveredPlotBands: plotBands,
111
+ hoveredPlotLines: plotLines,
112
+ hoveredPlotShapes: plotShapes,
112
113
  }, event);
113
114
  }
114
115
  togglePinTooltip === null || togglePinTooltip === void 0 ? void 0 : togglePinTooltip(nextTooltipFixed, event);
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import type { Dispatch } from 'd3-dispatch';
3
3
  import type { ChartScale, LegendItem, OnLegendItemClick, PreparedLegend, PreparedSeries, PreparedSplit, PreparedXAxis, PreparedYAxis, RangeSliderState, ShapeData, ZoomState } from '../../hooks';
4
- import type { PreparedChart } from '../../hooks/types';
4
+ import type { PreparedChart, PreparedTitle } from '../../hooks/types';
5
5
  import type { LegendConfig } from '../../types';
6
6
  import type { ChartInnerProps } from './types';
7
7
  type Props = ChartInnerProps & {
@@ -9,7 +9,6 @@ type Props = ChartInnerProps & {
9
9
  dispatcher: Dispatch<object>;
10
10
  htmlLayout: HTMLElement | null;
11
11
  plotNode: SVGGElement | null;
12
- preparedChart: PreparedChart;
13
12
  updateZoomState: (nextZoomState: Partial<ZoomState>) => void;
14
13
  zoomState: Partial<ZoomState>;
15
14
  rangeSliderState?: RangeSliderState;
@@ -25,6 +24,8 @@ export declare function useChartInnerProps(props: Props): {
25
24
  shapesData: ShapeData[];
26
25
  shapesReady: boolean;
27
26
  handleLegendItemClick: OnLegendItemClick;
27
+ preparedTitle: PreparedTitle | undefined;
28
+ preparedChart: PreparedChart | undefined;
28
29
  allPreparedSeries?: PreparedSeries[] | undefined;
29
30
  legendConfig?: LegendConfig | undefined;
30
31
  legendItems?: LegendItem[][] | undefined;