@gravity-ui/charts 1.50.0 → 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.
@@ -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
  }
@@ -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),
@@ -362,6 +362,7 @@ export type PreparedRadarSeries = {
362
362
  export type PreparedFunnelSeries = {
363
363
  type: FunnelSeries['type'];
364
364
  data: FunnelSeriesData;
365
+ shape: Required<FunnelSeries>['shape'];
365
366
  dataLabels: {
366
367
  enabled: boolean;
367
368
  style: BaseTextStyle;
@@ -19,7 +19,7 @@ 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 = [];
@@ -95,15 +95,28 @@ export async function prepareFunnelData(args) {
95
95
  }
96
96
  }
97
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;
98
100
  for (let index = 0; index < series.length; index++) {
99
101
  const s = series[index];
100
102
  const d = s.data;
101
- 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
+ ];
102
114
  const funnelSegment = {
103
- x: segmentLeftOffset + segmentMaxWidth / 2 - itemWidth / 2,
104
- y: getSegmentY(index),
115
+ x: centerX - itemWidth / 2,
116
+ y: segmentY,
105
117
  width: itemWidth,
106
118
  height: itemHeight,
119
+ points,
107
120
  color: s.color,
108
121
  series: s,
109
122
  data: d,
@@ -114,7 +127,7 @@ export async function prepareFunnelData(args) {
114
127
  items.push(funnelSegment);
115
128
  const prevSeries = series[index - 1];
116
129
  const prevItem = items[index - 1];
117
- 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)) {
118
131
  const connectorPoints = [
119
132
  [prevItem.x, prevItem.y + prevItem.height],
120
133
  [prevItem.x + prevItem.width, prevItem.y + prevItem.height],
@@ -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);
@@ -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;
@@ -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;
@@ -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
  }
@@ -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),
@@ -362,6 +362,7 @@ export type PreparedRadarSeries = {
362
362
  export type PreparedFunnelSeries = {
363
363
  type: FunnelSeries['type'];
364
364
  data: FunnelSeriesData;
365
+ shape: Required<FunnelSeries>['shape'];
365
366
  dataLabels: {
366
367
  enabled: boolean;
367
368
  style: BaseTextStyle;
@@ -19,7 +19,7 @@ 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 = [];
@@ -95,15 +95,28 @@ export async function prepareFunnelData(args) {
95
95
  }
96
96
  }
97
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;
98
100
  for (let index = 0; index < series.length; index++) {
99
101
  const s = series[index];
100
102
  const d = s.data;
101
- 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
+ ];
102
114
  const funnelSegment = {
103
- x: segmentLeftOffset + segmentMaxWidth / 2 - itemWidth / 2,
104
- y: getSegmentY(index),
115
+ x: centerX - itemWidth / 2,
116
+ y: segmentY,
105
117
  width: itemWidth,
106
118
  height: itemHeight,
119
+ points,
107
120
  color: s.color,
108
121
  series: s,
109
122
  data: d,
@@ -114,7 +127,7 @@ export async function prepareFunnelData(args) {
114
127
  items.push(funnelSegment);
115
128
  const prevSeries = series[index - 1];
116
129
  const prevItem = items[index - 1];
117
- 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)) {
118
131
  const connectorPoints = [
119
132
  [prevItem.x, prevItem.y + prevItem.height],
120
133
  [prevItem.x + prevItem.width, prevItem.y + prevItem.height],
@@ -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);
@@ -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;
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/charts",
3
- "version": "1.50.0",
3
+ "version": "1.51.0",
4
4
  "description": "A flexible JavaScript library for data visualization and chart rendering using React",
5
5
  "license": "MIT",
6
6
  "main": "dist/cjs/index.js",