@gravity-ui/charts 1.49.1 → 1.51.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 (59) hide show
  1. package/dist/cjs/components/AxisY/types.d.ts +2 -1
  2. package/dist/cjs/components/ChartInner/utils/zoom.js +1 -1
  3. package/dist/cjs/core/scales/y-scale.js +6 -0
  4. package/dist/cjs/core/series/prepare-area.js +1 -0
  5. package/dist/cjs/core/series/prepare-bar-x.js +1 -0
  6. package/dist/cjs/core/series/prepare-bar-y.d.ts +1 -0
  7. package/dist/cjs/core/series/prepare-bar-y.js +1 -0
  8. package/dist/cjs/core/series/prepare-funnel.js +5 -2
  9. package/dist/cjs/core/series/types.d.ts +4 -0
  10. package/dist/cjs/core/shapes/bar-y/prepare-data.js +11 -9
  11. package/dist/cjs/core/shapes/funnel/prepare-data.js +57 -23
  12. package/dist/cjs/core/shapes/funnel/renderer.js +3 -6
  13. package/dist/cjs/core/shapes/funnel/types.d.ts +3 -1
  14. package/dist/cjs/core/shapes/utils.d.ts +6 -0
  15. package/dist/cjs/core/shapes/utils.js +9 -0
  16. package/dist/cjs/core/types/chart/funnel.d.ts +18 -1
  17. package/dist/cjs/core/types/chart/series.d.ts +2 -2
  18. package/dist/cjs/core/types/chart/tooltip.d.ts +3 -2
  19. package/dist/cjs/core/types/chart/zoom.d.ts +1 -1
  20. package/dist/cjs/core/types/css.d.ts +2 -0
  21. package/dist/cjs/core/types/css.js +1 -0
  22. package/dist/cjs/core/types/renderer.d.ts +15 -0
  23. package/dist/cjs/core/types/renderer.js +1 -0
  24. package/dist/cjs/core/utils/text.d.ts +2 -1
  25. package/dist/cjs/core/zoom/zoom.js +10 -2
  26. package/dist/cjs/hooks/useShapes/funnel/index.js +5 -2
  27. package/dist/cjs/hooks/useShapes/utils.d.ts +0 -7
  28. package/dist/cjs/hooks/useShapes/utils.js +0 -11
  29. package/dist/cjs/types/chart-ui.d.ts +2 -1
  30. package/dist/esm/components/AxisY/types.d.ts +2 -1
  31. package/dist/esm/components/ChartInner/utils/zoom.js +1 -1
  32. package/dist/esm/core/scales/y-scale.js +6 -0
  33. package/dist/esm/core/series/prepare-area.js +1 -0
  34. package/dist/esm/core/series/prepare-bar-x.js +1 -0
  35. package/dist/esm/core/series/prepare-bar-y.d.ts +1 -0
  36. package/dist/esm/core/series/prepare-bar-y.js +1 -0
  37. package/dist/esm/core/series/prepare-funnel.js +5 -2
  38. package/dist/esm/core/series/types.d.ts +4 -0
  39. package/dist/esm/core/shapes/bar-y/prepare-data.js +11 -9
  40. package/dist/esm/core/shapes/funnel/prepare-data.js +57 -23
  41. package/dist/esm/core/shapes/funnel/renderer.js +3 -6
  42. package/dist/esm/core/shapes/funnel/types.d.ts +3 -1
  43. package/dist/esm/core/shapes/utils.d.ts +6 -0
  44. package/dist/esm/core/shapes/utils.js +9 -0
  45. package/dist/esm/core/types/chart/funnel.d.ts +18 -1
  46. package/dist/esm/core/types/chart/series.d.ts +2 -2
  47. package/dist/esm/core/types/chart/tooltip.d.ts +3 -2
  48. package/dist/esm/core/types/chart/zoom.d.ts +1 -1
  49. package/dist/esm/core/types/css.d.ts +2 -0
  50. package/dist/esm/core/types/css.js +1 -0
  51. package/dist/esm/core/types/renderer.d.ts +15 -0
  52. package/dist/esm/core/types/renderer.js +1 -0
  53. package/dist/esm/core/utils/text.d.ts +2 -1
  54. package/dist/esm/core/zoom/zoom.js +10 -2
  55. package/dist/esm/hooks/useShapes/funnel/index.js +5 -2
  56. package/dist/esm/hooks/useShapes/utils.d.ts +0 -7
  57. package/dist/esm/hooks/useShapes/utils.js +0 -11
  58. package/dist/esm/types/chart-ui.d.ts +2 -1
  59. package/package.json +2 -1
@@ -1,5 +1,6 @@
1
1
  import type { DashStyle } from 'src/core/constants';
2
2
  import type { AxisPlotShape } from '../../core/types/chart/axis';
3
+ import type { CSSProperties } from '../../core/types/css';
3
4
  import type { BaseTextStyle, HtmlItem, PlotLayerPlacement, PointPosition } from '../../types';
4
5
  import type { TextRowData } from '../types';
5
6
  export type AxisSvgLabelData = {
@@ -42,7 +43,7 @@ export type SvgAxisTitleData = {
42
43
  export type HtmlAxisTitleData = {
43
44
  html: true;
44
45
  content: string;
45
- style: BaseTextStyle & React.CSSProperties;
46
+ style: BaseTextStyle & CSSProperties;
46
47
  size: {
47
48
  width: number;
48
49
  height: number;
@@ -13,7 +13,7 @@ function mapSeriesTypeToZoomType(seriesType) {
13
13
  return [ZOOM_TYPE.X];
14
14
  }
15
15
  case SERIES_TYPE.BarY: {
16
- return [ZOOM_TYPE.Y];
16
+ return [ZOOM_TYPE.Y, ZOOM_TYPE.XY];
17
17
  }
18
18
  case SERIES_TYPE.Line: {
19
19
  return [ZOOM_TYPE.X, ZOOM_TYPE.XY, ZOOM_TYPE.Y];
@@ -127,6 +127,9 @@ function getDomainMinAlignedToStartTick(args) {
127
127
  else {
128
128
  step = tickStep(dMin, dMax, 1);
129
129
  }
130
+ if (step === 0) {
131
+ return dMin;
132
+ }
130
133
  dNewMin = tickValues[0].value - step;
131
134
  }
132
135
  }
@@ -161,6 +164,9 @@ function getDomainMaxAlignedToEndTick(args) {
161
164
  else {
162
165
  step = tickStep(dMin, dMax, 1);
163
166
  }
167
+ if (step === 0) {
168
+ return dMax;
169
+ }
164
170
  dNewMax = Math.floor(dMax / step + 1) * step;
165
171
  }
166
172
  }
@@ -63,6 +63,7 @@ export function prepareArea(args) {
63
63
  data: prepareSeriesData(series),
64
64
  stacking: series.stacking,
65
65
  stackId: getSeriesStackId(series),
66
+ valueAxis: 'y',
66
67
  dataLabels: {
67
68
  enabled: ((_e = series.dataLabels) === null || _e === void 0 ? void 0 : _e.enabled) || false,
68
69
  style: Object.assign({}, DEFAULT_DATALABELS_STYLE, (_f = series.dataLabels) === null || _f === void 0 ? void 0 : _f.style),
@@ -37,6 +37,7 @@ export function prepareBarXSeries(args) {
37
37
  data: prepareSeriesData(series),
38
38
  stacking: series.stacking,
39
39
  stackId: getSeriesStackId(series),
40
+ valueAxis: 'y',
40
41
  dataLabels: {
41
42
  enabled: ((_e = series.dataLabels) === null || _e === void 0 ? void 0 : _e.enabled) || false,
42
43
  inside: dataLabelsInside,
@@ -12,6 +12,7 @@ export declare function prepareBarYSeries(args: PrepareBarYSeriesArgs): Promise<
12
12
  data: BarYSeriesData[];
13
13
  stackId: string;
14
14
  stacking: BarYSeries["stacking"];
15
+ valueAxis: "x";
15
16
  dataLabels: {
16
17
  padding: number;
17
18
  enabled: boolean;
@@ -64,6 +64,7 @@ export function prepareBarYSeries(args) {
64
64
  data: prepareSeriesData(series),
65
65
  stacking: series.stacking,
66
66
  stackId: getSeriesStackId(series),
67
+ valueAxis: 'x',
67
68
  dataLabels: await prepareDataLabels(series),
68
69
  cursor: get(series, 'cursor', null),
69
70
  borderRadius: (_g = (_e = series.borderRadius) !== null && _e !== void 0 ? _e : (_f = seriesOptions === null || seriesOptions === void 0 ? void 0 : seriesOptions['bar-y']) === null || _f === void 0 ? void 0 : _f.borderRadius) !== null && _g !== void 0 ? _g : 0,
@@ -4,17 +4,20 @@ import { DEFAULT_DATALABELS_STYLE } from '../constants';
4
4
  import { getUniqId } from '../utils';
5
5
  import { prepareLegendSymbol } from './utils';
6
6
  export function prepareFunnelSeries(args) {
7
- var _a, _b;
7
+ var _a, _b, _c;
8
8
  const { series, legend, colors } = args;
9
9
  const dataNames = series.data.map((d) => d.name);
10
10
  const colorScale = scaleOrdinal(dataNames, colors);
11
- const isConnectorsEnabled = (_b = (_a = series.connectors) === null || _a === void 0 ? void 0 : _a.enabled) !== null && _b !== void 0 ? _b : true;
11
+ const shape = (_a = series.shape) !== null && _a !== void 0 ? _a : 'rectangle';
12
+ const isTrapezoid = shape === 'trapezoid';
13
+ const isConnectorsEnabled = (_c = (_b = series.connectors) === null || _b === void 0 ? void 0 : _b.enabled) !== null && _c !== void 0 ? _c : !isTrapezoid;
12
14
  const preparedSeries = series.data.map((dataItem) => {
13
15
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2;
14
16
  const id = getUniqId();
15
17
  const color = dataItem.color || colorScale(dataItem.name);
16
18
  const result = {
17
19
  type: 'funnel',
20
+ shape,
18
21
  data: dataItem,
19
22
  dataLabels: {
20
23
  enabled: get(series, 'dataLabels.enabled', true),
@@ -134,6 +134,7 @@ export type PreparedBarXSeries = {
134
134
  data: BarXSeriesData[];
135
135
  stackId: string;
136
136
  stacking: BarXSeries['stacking'];
137
+ valueAxis: 'y';
137
138
  dataLabels: {
138
139
  enabled: boolean;
139
140
  inside: boolean;
@@ -151,6 +152,7 @@ export type PreparedBarYSeries = {
151
152
  data: BarYSeriesData[];
152
153
  stackId: string;
153
154
  stacking: BarYSeries['stacking'];
155
+ valueAxis: 'x';
154
156
  dataLabels: {
155
157
  padding: number;
156
158
  enabled: boolean;
@@ -252,6 +254,7 @@ export type PreparedAreaSeries = {
252
254
  data: AreaSeriesData[];
253
255
  stacking: AreaSeries['stacking'];
254
256
  stackId: string;
257
+ valueAxis: 'y';
255
258
  lineWidth: number;
256
259
  opacity: number;
257
260
  nullMode: AreaSeries['nullMode'];
@@ -359,6 +362,7 @@ export type PreparedRadarSeries = {
359
362
  export type PreparedFunnelSeries = {
360
363
  type: FunnelSeries['type'];
361
364
  data: FunnelSeriesData;
365
+ shape: Required<FunnelSeries>['shape'];
362
366
  dataLabels: {
363
367
  enabled: boolean;
364
368
  style: BaseTextStyle;
@@ -9,7 +9,6 @@ export async function prepareBarYData(args) {
9
9
  const stackGap = seriesOptions['bar-y'].stackGap;
10
10
  const xLinearScale = xScale;
11
11
  const yLinearScale = yScale;
12
- const [baseRangeValue] = xLinearScale.range();
13
12
  if (!yLinearScale) {
14
13
  return {
15
14
  shapes: [],
@@ -92,19 +91,22 @@ export async function prepareBarYData(args) {
92
91
  const borderWidth = barSize > s.borderWidth * 2 ? s.borderWidth : 0;
93
92
  const isFirstInStack = xValueIndex === 0;
94
93
  const isLastStackItem = xValueIndex === sortedData.length - 1;
94
+ const extendsRight = xLinearScale(xValue) > baseValue;
95
95
  // Calculate position with border compensation
96
96
  // Border extends halfBorder outward from the shape, so we need to adjust position
97
- let itemX = xValue > baseRangeValue ? positiveStack : negativeStack - width;
97
+ let itemX = extendsRight ? positiveStack : negativeStack - width;
98
98
  itemX += itemStackGap;
99
99
  const halfBorder = borderWidth / 2;
100
- if (isFirstInStack && xValue > 0) {
101
- // Positive bar: border extends left, so shift position left by halfBorder
102
- // to keep the visual left edge at the zero line
100
+ if (isFirstInStack && extendsRight) {
101
+ // Bar extends right from base, border extends outward to the
102
+ // left shift left by halfBorder to keep the visual left
103
+ // edge at the zero line.
103
104
  itemX -= halfBorder;
104
105
  }
105
- else if (isFirstInStack && xValue < 0) {
106
- // Negative bar: border extends right, so shift position right by halfBorder
107
- // to keep the visual right edge at the zero line
106
+ else if (isFirstInStack && !extendsRight && xValue !== 0) {
107
+ // Bar extends left from base, border extends outward to the
108
+ // right shift right by halfBorder to keep the visual
109
+ // right edge at the zero line.
108
110
  itemX += halfBorder;
109
111
  }
110
112
  const item = {
@@ -121,7 +123,7 @@ export async function prepareBarYData(args) {
121
123
  isLastStackItem,
122
124
  };
123
125
  stackItems.push(item);
124
- if (xValue > baseRangeValue) {
126
+ if (extendsRight) {
125
127
  positiveStack += width;
126
128
  }
127
129
  else {
@@ -1,5 +1,5 @@
1
1
  import { path } from 'd3-path';
2
- import { calculateNumericProperty, getFormattedValue, getTextSizeFn } from '../../utils';
2
+ import { calculateNumericProperty, getFormattedValue, getLabelsSize, getTextSizeFn, } from '../../utils';
3
3
  function getLineConnectorPaths(args) {
4
4
  const { points } = args;
5
5
  const leftPath = path();
@@ -19,10 +19,11 @@ function getAreaConnectorPath(args) {
19
19
  return p;
20
20
  }
21
21
  export async function prepareFunnelData(args) {
22
- var _a, _b, _c, _d;
22
+ var _a, _b, _c, _d, _e;
23
23
  const { series, boundsWidth, boundsHeight } = args;
24
24
  const items = [];
25
25
  const svgLabels = [];
26
+ const htmlLabels = [];
26
27
  const connectors = [];
27
28
  const maxValue = Math.max(...series.map((s) => s.data.value));
28
29
  const itemBandSpace = boundsHeight / series.length;
@@ -42,48 +43,80 @@ export async function prepareFunnelData(args) {
42
43
  if (s.dataLabels.enabled) {
43
44
  const d = s.data;
44
45
  const labelContent = (_c = d.label) !== null && _c !== void 0 ? _c : getFormattedValue({ value: d.value, format: s.dataLabels.format });
45
- const labelSize = await getTextSize(labelContent);
46
+ const { width, height, hangingOffset } = s.dataLabels.html
47
+ ? await getLabelsSize({
48
+ labels: [labelContent],
49
+ style: s.dataLabels.style,
50
+ html: true,
51
+ }).then((size) => ({
52
+ width: size.maxWidth,
53
+ height: size.maxHeight,
54
+ hangingOffset: 0,
55
+ }))
56
+ : await getTextSize(labelContent);
46
57
  let x;
47
58
  switch (s.dataLabels.align) {
48
59
  case 'left': {
49
60
  x = 0;
50
- segmentLeftOffset = Math.max(segmentLeftOffset, labelSize.width);
61
+ segmentLeftOffset = Math.max(segmentLeftOffset, width);
51
62
  break;
52
63
  }
53
64
  case 'right': {
54
- x = boundsWidth - labelSize.width;
55
- segmentRightOffset = Math.max(segmentRightOffset, labelSize.width);
65
+ x = boundsWidth - width;
66
+ segmentRightOffset = Math.max(segmentRightOffset, width);
56
67
  break;
57
68
  }
58
69
  case 'center': {
59
- x = boundsWidth / 2 - labelSize.width / 2;
70
+ x = boundsWidth / 2 - width / 2;
60
71
  break;
61
72
  }
62
73
  }
63
- svgLabels.push({
64
- x,
65
- y: getSegmentY(index) +
66
- itemHeight / 2 -
67
- labelSize.height / 2 +
68
- labelSize.hangingOffset,
69
- text: labelContent,
70
- style: s.dataLabels.style,
71
- size: labelSize,
72
- textAnchor: 'start',
73
- series: s,
74
- });
74
+ const y = getSegmentY(index) + itemHeight / 2 - height / 2 + hangingOffset;
75
+ if (s.dataLabels.html) {
76
+ htmlLabels.push({
77
+ x,
78
+ y,
79
+ content: labelContent,
80
+ size: { width, height },
81
+ style: s.dataLabels.style,
82
+ });
83
+ }
84
+ else {
85
+ svgLabels.push({
86
+ x,
87
+ y,
88
+ text: labelContent,
89
+ style: s.dataLabels.style,
90
+ size: { width, height, hangingOffset },
91
+ textAnchor: 'start',
92
+ series: s,
93
+ });
94
+ }
75
95
  }
76
96
  }
77
97
  const segmentMaxWidth = boundsWidth - segmentLeftOffset - segmentRightOffset;
98
+ const isTrapezoid = ((_d = series[0]) === null || _d === void 0 ? void 0 : _d.shape) === 'trapezoid';
99
+ const getItemWidth = (index) => (segmentMaxWidth * series[index].data.value) / maxValue;
78
100
  for (let index = 0; index < series.length; index++) {
79
101
  const s = series[index];
80
102
  const d = s.data;
81
- const itemWidth = (segmentMaxWidth * d.value) / maxValue;
103
+ const itemWidth = getItemWidth(index);
104
+ const centerX = segmentLeftOffset + segmentMaxWidth / 2;
105
+ const segmentY = getSegmentY(index);
106
+ const isLastSegment = index === series.length - 1;
107
+ const bottomWidth = isTrapezoid && !isLastSegment ? getItemWidth(index + 1) : itemWidth;
108
+ const points = [
109
+ [centerX - itemWidth / 2, segmentY],
110
+ [centerX + itemWidth / 2, segmentY],
111
+ [centerX + bottomWidth / 2, segmentY + itemHeight],
112
+ [centerX - bottomWidth / 2, segmentY + itemHeight],
113
+ ];
82
114
  const funnelSegment = {
83
- x: segmentLeftOffset + segmentMaxWidth / 2 - itemWidth / 2,
84
- y: getSegmentY(index),
115
+ x: centerX - itemWidth / 2,
116
+ y: segmentY,
85
117
  width: itemWidth,
86
118
  height: itemHeight,
119
+ points,
87
120
  color: s.color,
88
121
  series: s,
89
122
  data: d,
@@ -94,7 +127,7 @@ export async function prepareFunnelData(args) {
94
127
  items.push(funnelSegment);
95
128
  const prevSeries = series[index - 1];
96
129
  const prevItem = items[index - 1];
97
- if (prevSeries && prevItem && ((_d = prevSeries.connectors) === null || _d === void 0 ? void 0 : _d.enabled)) {
130
+ if (prevSeries && prevItem && ((_e = prevSeries.connectors) === null || _e === void 0 ? void 0 : _e.enabled)) {
98
131
  const connectorPoints = [
99
132
  [prevItem.x, prevItem.y + prevItem.height],
100
133
  [prevItem.x + prevItem.width, prevItem.y + prevItem.height],
@@ -117,6 +150,7 @@ export async function prepareFunnelData(args) {
117
150
  type: 'funnel',
118
151
  items,
119
152
  svgLabels,
153
+ htmlLabels,
120
154
  connectors,
121
155
  };
122
156
  return data;
@@ -11,13 +11,10 @@ export function renderFunnel(elements, preparedData, seriesOptions, dispatcher)
11
11
  svgElement.selectAll('*').remove();
12
12
  // funnel levels
13
13
  const cellsSelection = svgElement
14
- .selectAll('rect')
14
+ .selectAll('polygon')
15
15
  .data(preparedData.items)
16
- .join('rect')
17
- .attr('x', (d) => d.x)
18
- .attr('y', (d) => d.y)
19
- .attr('height', (d) => d.height)
20
- .attr('width', (d) => d.width)
16
+ .join('polygon')
17
+ .attr('points', (d) => d.points.map((p) => p.join(',')).join(' '))
21
18
  .attr('fill', (d) => d.color)
22
19
  .attr('stroke', (d) => d.borderColor)
23
20
  .attr('stroke-width', (d) => d.borderWidth);
@@ -1,5 +1,5 @@
1
1
  import type { Path } from 'd3-path';
2
- import type { FunnelSeriesData, LabelData } from '../../../types';
2
+ import type { FunnelSeriesData, HtmlItem, LabelData } from '../../../types';
3
3
  import type { DashStyle } from '../../constants';
4
4
  import type { PreparedFunnelSeries } from '../../series/types';
5
5
  export type FunnelItemData = {
@@ -7,6 +7,7 @@ export type FunnelItemData = {
7
7
  y: number;
8
8
  width: number;
9
9
  height: number;
10
+ points: [number, number][];
10
11
  color: string;
11
12
  series: PreparedFunnelSeries;
12
13
  data: FunnelSeriesData;
@@ -29,4 +30,5 @@ export type PreparedFunnelData = {
29
30
  items: FunnelItemData[];
30
31
  connectors: FunnelConnectorData[];
31
32
  svgLabels: LabelData[];
33
+ htmlLabels: HtmlItem[];
32
34
  };
@@ -2,6 +2,7 @@ import type { BaseType } from 'd3-selection';
2
2
  import type { PreparedXAxis, PreparedYAxis } from '../axes/types';
3
3
  import type { ChartScale } from '../scales/types';
4
4
  import type { BasicInactiveState } from '../types';
5
+ import type { ZoomState } from '../zoom/types';
5
6
  export declare function getXValue(args: {
6
7
  point: {
7
8
  x?: number | string | null;
@@ -74,3 +75,8 @@ export declare function getClipPathIdByBounds(args: {
74
75
  clipPathId: string;
75
76
  bounds?: 'horizontal';
76
77
  }): string;
78
+ export declare function getSeriesClipPathId(args: {
79
+ clipPathId: string;
80
+ yAxis: PreparedYAxis[];
81
+ zoomState?: Partial<ZoomState>;
82
+ }): string;
@@ -184,3 +184,12 @@ export function getClipPathIdByBounds(args) {
184
184
  const { bounds, clipPathId } = args;
185
185
  return bounds ? `${clipPathId}-${bounds}` : clipPathId;
186
186
  }
187
+ export function getSeriesClipPathId(args) {
188
+ const { clipPathId, yAxis, zoomState } = args;
189
+ const hasMinOrMax = yAxis.some((axis) => typeof (axis === null || axis === void 0 ? void 0 : axis.min) === 'number' || typeof (axis === null || axis === void 0 ? void 0 : axis.max) === 'number');
190
+ const hasZoom = zoomState && Object.keys(zoomState).length > 0;
191
+ if (!hasZoom && !hasMinOrMax) {
192
+ return `${clipPathId}-horizontal`;
193
+ }
194
+ return clipPathId;
195
+ }
@@ -21,6 +21,23 @@ export interface FunnelSeries<T = MeaningfulAny> extends Omit<BaseSeries, 'dataL
21
21
  name?: string;
22
22
  /** The color of the funnel series. */
23
23
  color?: string;
24
+ /**
25
+ * The visual shape of funnel segments.
26
+ *
27
+ * - `'rectangle'` (**recommended**): each segment is an independent rectangle whose
28
+ * width is directly proportional to its value. The human eye reads width as a linear
29
+ * scale, making comparisons between segments accurate and effortless.
30
+ *
31
+ * - `'trapezoid'`: adjacent segments are drawn as connected trapezoids, giving the chart
32
+ * a classic "funnel" silhouette. However, this shape distorts perception: the slanted
33
+ * sides cause viewers to judge area (which grows as the square of width) rather than
34
+ * width alone, exaggerating differences between large and small values. Use only for
35
+ * decorative purposes or when visual familiarity with the funnel metaphor is more
36
+ * important than analytical precision.
37
+ *
38
+ * @default 'rectangle'
39
+ */
40
+ shape?: 'rectangle' | 'trapezoid';
24
41
  /** Lines or areas connecting the funnel segments. */
25
42
  connectors?: {
26
43
  enabled?: boolean;
@@ -39,7 +56,7 @@ export interface FunnelSeries<T = MeaningfulAny> extends Omit<BaseSeries, 'dataL
39
56
  /** Opacity for the connector area. */
40
57
  areaOpacity?: number;
41
58
  };
42
- dataLabels?: Omit<BaseDataLabels, 'html' | 'allowOverlap'> & {
59
+ dataLabels?: Omit<BaseDataLabels, 'allowOverlap'> & {
43
60
  /** Horizontal alignment of the data labels. */
44
61
  align?: 'left' | 'center' | 'right';
45
62
  };
@@ -1,6 +1,6 @@
1
- import type React from 'react';
2
1
  import type { DashStyle, LineCap, LineJoin } from '../../constants';
3
2
  import type { MeaningfulAny } from '../misc';
3
+ import type { SVGTextAttributes } from '../renderer';
4
4
  import type { ChartAnnotationSeriesOptions } from './annotation';
5
5
  import type { AreaSeries, AreaSeriesData } from './area';
6
6
  import type { BarXSeries, BarXSeriesData } from './bar-x';
@@ -55,7 +55,7 @@ export interface ChartSeriesOptions {
55
55
  /** Enable or disable the data labels */
56
56
  enabled?: boolean;
57
57
  /** Callback function to render the data label */
58
- renderer?: (args: DataLabelRendererData) => React.SVGTextElementAttributes<SVGTextElement>;
58
+ renderer?: (args: DataLabelRendererData) => SVGTextAttributes;
59
59
  };
60
60
  'bar-x'?: {
61
61
  /**
@@ -1,6 +1,7 @@
1
1
  import type { TOOLTIP_TOTALS_BUILT_IN_AGGREGATION } from '../../constants';
2
2
  import type { DateTimeLabelFormats } from '../../utils/time';
3
3
  import type { MeaningfulAny } from '../misc';
4
+ import type { RendererElement } from '../renderer';
4
5
  import type { AreaSeries, AreaSeriesData } from './area';
5
6
  import type { AxisPlotBand, AxisPlotLine, AxisPlotShape, ChartXAxis, ChartYAxis } from './axis';
6
7
  import type { BarXSeries, BarXSeriesData } from './bar-x';
@@ -132,7 +133,7 @@ export type ChartTooltipSortComparator<T = MeaningfulAny> = (a: TooltipDataChunk
132
133
  export interface ChartTooltip<T = MeaningfulAny> {
133
134
  enabled?: boolean;
134
135
  /** Specifies the renderer for the tooltip. If returned null default tooltip renderer will be used. */
135
- renderer?: (args: ChartTooltipRendererArgs<T>) => React.ReactElement | null;
136
+ renderer?: (args: ChartTooltipRendererArgs<T>) => RendererElement | null;
136
137
  /**
137
138
  * Defines the way a single data/series is displayed (corresponding to a separate selected point/ruler/shape on the chart).
138
139
  * It is useful in cases where you need to display additional information, but keep the general format of the tooltip.
@@ -157,7 +158,7 @@ export interface ChartTooltip<T = MeaningfulAny> {
157
158
  * `<tr class="${className}"><td>${name}</td><td>${value}</td></tr>`
158
159
  * ```
159
160
  */
160
- rowRenderer?: ((args: ChartTooltipRowRendererArgs) => React.ReactElement | string) | null;
161
+ rowRenderer?: ((args: ChartTooltipRowRendererArgs) => RendererElement | string) | null;
161
162
  pin?: {
162
163
  enabled?: boolean;
163
164
  modifierKey?: 'altKey' | 'metaKey';
@@ -19,8 +19,8 @@ export interface ChartZoom {
19
19
  * Supported zoom types by series type:
20
20
  * - `Area`, `Line`, `Scatter`: `x`, `y`, `xy`
21
21
  * - `BarX`: `x`, `xy`
22
+ * - `BarY`: `y`, `xy`
22
23
  * - `XRange`: `x`
23
- * - `BarY`: `y`
24
24
  *
25
25
  * Default zoom type by series type:
26
26
  * - `BarY`: `y`
@@ -0,0 +1,2 @@
1
+ import type { Properties } from 'csstype';
2
+ export type CSSProperties = Properties;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Framework-agnostic renderer return type.
3
+ * Structurally compatible with React.ReactElement — callers in React contexts
4
+ * may narrow to it without changes.
5
+ */
6
+ export interface RendererElement {
7
+ type: unknown;
8
+ props: unknown;
9
+ key: unknown;
10
+ }
11
+ /**
12
+ * SVG text element attributes map.
13
+ * Replaces React.SVGTextElementAttributes<SVGTextElement> in core types.
14
+ */
15
+ export type SVGTextAttributes = object;
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,6 @@
1
1
  import type { Selection } from 'd3-selection';
2
2
  import type { BaseTextStyle, MeaningfulAny } from '../../types';
3
+ import type { CSSProperties } from '../types/css';
3
4
  /**
4
5
  * Approximate ratio of descenders relative to the full font em height.
5
6
  * Based on the Chromium hanging baseline algorithm where hanging offset ≈ ascent × 0.2.
@@ -12,7 +13,7 @@ export declare function setEllipsisForOverflowText<T>(selection: Selection<SVGTe
12
13
  export declare function setEllipsisForOverflowTexts<T>(selection: Selection<SVGTextElement, T, MeaningfulAny, unknown>, maxWidth: ((datum: T) => number) | number, currentWidth?: (datum: T) => number): void;
13
14
  export declare function getLabelsSize({ labels, style, rotation, html, }: {
14
15
  labels: string[];
15
- style?: BaseTextStyle & React.CSSProperties;
16
+ style?: BaseTextStyle & CSSProperties;
16
17
  rotation?: number;
17
18
  html?: boolean;
18
19
  }): Promise<{
@@ -60,6 +60,14 @@ export function getZoomedSeriesData(args) {
60
60
  if (!isPreparedZoomableSeries(seriesItem)) {
61
61
  return;
62
62
  }
63
+ // For stacked series the chart-space position of each point is the
64
+ // cumulative stack sum, not its individual value, so the value-axis
65
+ // filter would drop segments that still need to participate in the
66
+ // stack. Skip it and let the axis range / clip handle visibility.
67
+ const isStacked = 'stacking' in seriesItem && Boolean(seriesItem.stacking);
68
+ const stackedValueAxis = isStacked && 'valueAxis' in seriesItem ? seriesItem.valueAxis : undefined;
69
+ const skipXFilter = stackedValueAxis === 'x';
70
+ const skipYFilter = stackedValueAxis === 'y';
63
71
  seriesItem.data.forEach((point, i) => {
64
72
  var _a, _b;
65
73
  const prevPoint = seriesItem.data[i - 1];
@@ -67,7 +75,7 @@ export function getZoomedSeriesData(args) {
67
75
  let inXRange = true;
68
76
  let inYRange = true;
69
77
  prevPointInRange = currentPointInRange;
70
- if (zoomState.x) {
78
+ if (zoomState.x && !skipXFilter) {
71
79
  const [xMin, xMax] = zoomState.x;
72
80
  if ('x0' in point && 'x1' in point) {
73
81
  const isStartInRange = isValueInRange({
@@ -94,7 +102,7 @@ export function getZoomedSeriesData(args) {
94
102
  });
95
103
  }
96
104
  }
97
- if (zoomState.y) {
105
+ if (zoomState.y && !skipYFilter) {
98
106
  const yAxisIndex = 'yAxis' in seriesItem && typeof seriesItem.yAxis === 'number'
99
107
  ? seriesItem.yAxis
100
108
  : 0;
@@ -1,11 +1,12 @@
1
1
  import React from 'react';
2
2
  import { renderFunnel } from '../../../core/shapes/funnel/renderer';
3
3
  import { block } from '../../../utils';
4
+ import { HtmlLayer } from '../HtmlLayer';
4
5
  export { prepareFunnelData } from '../../../core/shapes/funnel/prepare-data';
5
6
  export * from '../../../core/shapes/funnel/types';
6
7
  const b = block('funnel');
7
8
  export const FunnelSeriesShapes = (args) => {
8
- const { dispatcher, preparedData, seriesOptions } = args;
9
+ const { dispatcher, htmlLayout, preparedData, seriesOptions } = args;
9
10
  const ref = React.useRef(null);
10
11
  React.useEffect(() => {
11
12
  if (!ref.current) {
@@ -13,6 +14,8 @@ export const FunnelSeriesShapes = (args) => {
13
14
  }
14
15
  return renderFunnel({ plot: ref.current }, preparedData, seriesOptions, dispatcher);
15
16
  }, [dispatcher, preparedData, seriesOptions]);
17
+ const htmlLayerData = React.useMemo(() => ({ htmlElements: preparedData.htmlLabels }), [preparedData.htmlLabels]);
16
18
  return (React.createElement(React.Fragment, null,
17
- React.createElement("g", { ref: ref, className: b() })));
19
+ React.createElement("g", { ref: ref, className: b() }),
20
+ React.createElement(HtmlLayer, { preparedData: htmlLayerData, htmlLayout: htmlLayout })));
18
21
  };
@@ -1,8 +1 @@
1
- import type { ZoomState } from '../../core/zoom/types';
2
- import type { PreparedYAxis } from '../useAxis/types';
3
1
  export * from '../../core/shapes/utils';
4
- export declare function getSeriesClipPathId(args: {
5
- clipPathId: string;
6
- yAxis: PreparedYAxis[];
7
- zoomState?: Partial<ZoomState>;
8
- }): string;
@@ -1,12 +1 @@
1
1
  export * from '../../core/shapes/utils';
2
- export function getSeriesClipPathId(args) {
3
- const { clipPathId, yAxis, zoomState } = args;
4
- const hasMinOrMax = yAxis.some((axis) => {
5
- return typeof (axis === null || axis === void 0 ? void 0 : axis.min) === 'number' || typeof (axis === null || axis === void 0 ? void 0 : axis.max) === 'number';
6
- });
7
- const hasZoom = zoomState && Object.keys(zoomState).length > 0;
8
- if (!hasZoom && !hasMinOrMax) {
9
- return `${clipPathId}-horizontal`;
10
- }
11
- return clipPathId;
12
- }
@@ -1,4 +1,5 @@
1
1
  import type { BaseTextStyle } from '../core/types/chart/base';
2
+ import type { CSSProperties } from '../core/types/css';
2
3
  export interface LabelData {
3
4
  text: string;
4
5
  x: number;
@@ -23,7 +24,7 @@ export interface HtmlItem {
23
24
  width: number;
24
25
  height: number;
25
26
  };
26
- style?: BaseTextStyle & React.CSSProperties;
27
+ style?: BaseTextStyle & CSSProperties;
27
28
  /** Coordinate space for positioning: 'plot' uses the plot area origin, 'chart' uses the full chart origin. Defaults to 'plot'. */
28
29
  scope?: 'plot' | 'chart';
29
30
  }
@@ -1,5 +1,6 @@
1
1
  import type { DashStyle } from 'src/core/constants';
2
2
  import type { AxisPlotShape } from '../../core/types/chart/axis';
3
+ import type { CSSProperties } from '../../core/types/css';
3
4
  import type { BaseTextStyle, HtmlItem, PlotLayerPlacement, PointPosition } from '../../types';
4
5
  import type { TextRowData } from '../types';
5
6
  export type AxisSvgLabelData = {
@@ -42,7 +43,7 @@ export type SvgAxisTitleData = {
42
43
  export type HtmlAxisTitleData = {
43
44
  html: true;
44
45
  content: string;
45
- style: BaseTextStyle & React.CSSProperties;
46
+ style: BaseTextStyle & CSSProperties;
46
47
  size: {
47
48
  width: number;
48
49
  height: number;