@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.
- package/dist/cjs/core/scales/y-scale.js +6 -0
- package/dist/cjs/core/series/prepare-funnel.js +5 -2
- package/dist/cjs/core/series/types.d.ts +1 -0
- package/dist/cjs/core/shapes/funnel/prepare-data.js +18 -5
- package/dist/cjs/core/shapes/funnel/renderer.js +3 -6
- package/dist/cjs/core/shapes/funnel/types.d.ts +1 -0
- package/dist/cjs/core/types/chart/funnel.d.ts +17 -0
- package/dist/esm/core/scales/y-scale.js +6 -0
- package/dist/esm/core/series/prepare-funnel.js +5 -2
- package/dist/esm/core/series/types.d.ts +1 -0
- package/dist/esm/core/shapes/funnel/prepare-data.js +18 -5
- package/dist/esm/core/shapes/funnel/renderer.js +3 -6
- package/dist/esm/core/shapes/funnel/types.d.ts +1 -0
- package/dist/esm/core/types/chart/funnel.d.ts +17 -0
- package/package.json +1 -1
|
@@ -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
|
|
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),
|
|
@@ -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 = (
|
|
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:
|
|
104
|
-
y:
|
|
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 && ((
|
|
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('
|
|
14
|
+
.selectAll('polygon')
|
|
15
15
|
.data(preparedData.items)
|
|
16
|
-
.join('
|
|
17
|
-
.attr('
|
|
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);
|
|
@@ -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
|
|
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),
|
|
@@ -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 = (
|
|
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:
|
|
104
|
-
y:
|
|
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 && ((
|
|
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('
|
|
14
|
+
.selectAll('polygon')
|
|
15
15
|
.data(preparedData.items)
|
|
16
|
-
.join('
|
|
17
|
-
.attr('
|
|
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);
|
|
@@ -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;
|