@gravity-ui/charts 1.4.0 → 1.5.1
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/hooks/useSeries/prepare-treemap.js +3 -2
- package/dist/cjs/hooks/useSeries/types.d.ts +2 -1
- package/dist/cjs/hooks/useShapes/pie/prepare-data.js +79 -46
- package/dist/cjs/hooks/useShapes/pie/utils.d.ts +10 -0
- package/dist/cjs/hooks/useShapes/pie/utils.js +18 -0
- package/dist/cjs/hooks/useShapes/treemap/prepare-data.js +27 -15
- package/dist/cjs/types/chart/treemap.d.ts +11 -0
- package/dist/cjs/utils/chart/text.js +10 -5
- package/dist/esm/hooks/useSeries/prepare-treemap.js +3 -2
- package/dist/esm/hooks/useSeries/types.d.ts +2 -1
- package/dist/esm/hooks/useShapes/pie/prepare-data.js +79 -46
- package/dist/esm/hooks/useShapes/pie/utils.d.ts +10 -0
- package/dist/esm/hooks/useShapes/pie/utils.js +18 -0
- package/dist/esm/hooks/useShapes/treemap/prepare-data.js +27 -15
- package/dist/esm/types/chart/treemap.d.ts +11 -0
- package/dist/esm/utils/chart/text.js +10 -5
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ import { prepareLegendSymbol } from './utils';
|
|
|
6
6
|
export function prepareTreemap(args) {
|
|
7
7
|
const { colorScale, legend, series } = args;
|
|
8
8
|
return series.map((s) => {
|
|
9
|
-
var _a, _b;
|
|
9
|
+
var _a, _b, _c;
|
|
10
10
|
const id = getUniqId();
|
|
11
11
|
const name = s.name || '';
|
|
12
12
|
const color = s.color || colorScale(name);
|
|
@@ -30,9 +30,10 @@ export function prepareTreemap(args) {
|
|
|
30
30
|
enabled: get(s, 'legend.enabled', legend.enabled),
|
|
31
31
|
symbol: prepareLegendSymbol(s),
|
|
32
32
|
},
|
|
33
|
-
levels: s.levels,
|
|
33
|
+
levels: (_c = s.levels) !== null && _c !== void 0 ? _c : [],
|
|
34
34
|
layoutAlgorithm: get(s, 'layoutAlgorithm', LayoutAlgorithm.Binary),
|
|
35
35
|
cursor: get(s, 'cursor', null),
|
|
36
|
+
sorting: Object.assign({ enabled: false, direction: 'desc' }, s.sorting),
|
|
36
37
|
};
|
|
37
38
|
return preparedSeries;
|
|
38
39
|
});
|
|
@@ -242,7 +242,8 @@ export type PreparedTreemapSeries = {
|
|
|
242
242
|
format?: ValueFormat;
|
|
243
243
|
};
|
|
244
244
|
layoutAlgorithm: `${LayoutAlgorithm}`;
|
|
245
|
-
|
|
245
|
+
sorting: Required<TreemapSeries['sorting']>;
|
|
246
|
+
} & BasePreparedSeries & Required<Omit<TreemapSeries, keyof BasePreparedSeries>>;
|
|
246
247
|
export type PreparedWaterfallSeriesData = WaterfallSeriesData & {
|
|
247
248
|
index: number;
|
|
248
249
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { arc, group, line as lineGenerator } from 'd3';
|
|
2
2
|
import { calculateNumericProperty, getLabelsSize, getLeftPosition, isLabelsOverlapping, } from '../../../utils';
|
|
3
3
|
import { getFormattedValue } from '../../../utils/chart/format';
|
|
4
|
-
import { getCurveFactory, pieGenerator } from './utils';
|
|
4
|
+
import { getCurveFactory, getInscribedAngle, pieGenerator } from './utils';
|
|
5
5
|
const FULL_CIRCLE = Math.PI * 2;
|
|
6
6
|
const getCenter = (boundsWidth, boundsHeight, center) => {
|
|
7
7
|
var _a, _b;
|
|
@@ -23,7 +23,6 @@ export function preparePieData(args) {
|
|
|
23
23
|
const maxRadius = Math.min(boundsWidth, boundsHeight) / 2 - haloSize;
|
|
24
24
|
const minRadius = maxRadius * 0.3;
|
|
25
25
|
const groupedPieSeries = group(preparedSeries, (pieSeries) => pieSeries.stackId);
|
|
26
|
-
let maxMissingWidth = 0;
|
|
27
26
|
const prepareItem = (stackId, items) => {
|
|
28
27
|
var _a;
|
|
29
28
|
const series = items[0];
|
|
@@ -77,7 +76,7 @@ export function preparePieData(args) {
|
|
|
77
76
|
return data;
|
|
78
77
|
};
|
|
79
78
|
const prepareLabels = (prepareLabelsArgs) => {
|
|
80
|
-
const { data, series } = prepareLabelsArgs;
|
|
79
|
+
const { data, series, allowOverlow = true } = prepareLabelsArgs;
|
|
81
80
|
const { dataLabels } = series[0];
|
|
82
81
|
const labels = [];
|
|
83
82
|
const htmlLabels = [];
|
|
@@ -104,6 +103,7 @@ export function preparePieData(args) {
|
|
|
104
103
|
const labelArcGenerator = arc()
|
|
105
104
|
.innerRadius((d) => d.data.radius + distance + connectorPadding)
|
|
106
105
|
.outerRadius((d) => d.data.radius + distance + connectorPadding);
|
|
106
|
+
let shouldStopLabelPlacement = false;
|
|
107
107
|
series.forEach((d, index) => {
|
|
108
108
|
const prevLabel = labels[labels.length - 1];
|
|
109
109
|
const text = getFormattedValue(Object.assign({ value: d.data.label || d.data.value }, d.dataLabels));
|
|
@@ -120,7 +120,6 @@ export function preparePieData(args) {
|
|
|
120
120
|
else {
|
|
121
121
|
y = y < 0 ? y - labelHeight : y;
|
|
122
122
|
}
|
|
123
|
-
x = Math.max(-boundsWidth / 2, x);
|
|
124
123
|
return [x, y];
|
|
125
124
|
};
|
|
126
125
|
const getConnectorPoints = (angle) => {
|
|
@@ -148,11 +147,25 @@ export function preparePieData(args) {
|
|
|
148
147
|
segment: relatedSegment.data,
|
|
149
148
|
angle: midAngle,
|
|
150
149
|
};
|
|
150
|
+
if (!allowOverlow) {
|
|
151
|
+
const labelLeftPosition = getLeftPosition(label);
|
|
152
|
+
const newMaxWidth = labelLeftPosition > 0
|
|
153
|
+
? Math.min(boundsWidth / 2 - labelLeftPosition, labelWidth)
|
|
154
|
+
: Math.min(labelWidth - (-labelLeftPosition - boundsWidth / 2), labelWidth);
|
|
155
|
+
if (newMaxWidth !== label.maxWidth) {
|
|
156
|
+
label.maxWidth = Math.max(0, newMaxWidth);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
151
159
|
let overlap = false;
|
|
152
160
|
if (prevLabel) {
|
|
153
161
|
overlap = isLabelsOverlapping(prevLabel, label, dataLabels.padding);
|
|
162
|
+
const startAngle = relatedSegment.startAngle +
|
|
163
|
+
(relatedSegment.endAngle - relatedSegment.startAngle) / 2;
|
|
154
164
|
if (overlap) {
|
|
155
|
-
let shouldAdjustAngle =
|
|
165
|
+
let shouldAdjustAngle = !shouldStopLabelPlacement;
|
|
166
|
+
const connectorPoints = getConnectorPoints(startAngle);
|
|
167
|
+
const pointA = connectorPoints[0];
|
|
168
|
+
const pointB = connectorPoints[connectorPoints.length - 1];
|
|
156
169
|
const step = Math.PI / 180;
|
|
157
170
|
while (shouldAdjustAngle) {
|
|
158
171
|
const newAngle = label.angle + step;
|
|
@@ -164,6 +177,11 @@ export function preparePieData(args) {
|
|
|
164
177
|
const [newX, newY] = getLabelPosition(newAngle);
|
|
165
178
|
label.x = newX;
|
|
166
179
|
label.y = newY;
|
|
180
|
+
const inscribedAngle = getInscribedAngle(pointA, pointB, [newX, newY]);
|
|
181
|
+
if (inscribedAngle > 90) {
|
|
182
|
+
shouldAdjustAngle = false;
|
|
183
|
+
shouldStopLabelPlacement = true;
|
|
184
|
+
}
|
|
167
185
|
if (!isLabelsOverlapping(prevLabel, label, dataLabels.padding)) {
|
|
168
186
|
shouldAdjustAngle = false;
|
|
169
187
|
overlap = false;
|
|
@@ -172,21 +190,8 @@ export function preparePieData(args) {
|
|
|
172
190
|
}
|
|
173
191
|
}
|
|
174
192
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (Math.abs(left) > boundsWidth / 2) {
|
|
178
|
-
const overflow = Math.abs(left) - boundsWidth / 2;
|
|
179
|
-
label.maxWidth = label.size.width - overflow;
|
|
180
|
-
maxMissingWidth = Math.max(maxMissingWidth, overflow);
|
|
181
|
-
}
|
|
182
|
-
else {
|
|
183
|
-
const right = left + label.size.width;
|
|
184
|
-
if (right > boundsWidth / 2) {
|
|
185
|
-
const overflow = right - boundsWidth / 2;
|
|
186
|
-
label.maxWidth = label.size.width - overflow;
|
|
187
|
-
maxMissingWidth = Math.max(maxMissingWidth, overflow);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
193
|
+
const isLabelOverlapped = !dataLabels.allowOverlap && overlap;
|
|
194
|
+
if (!isLabelOverlapped && label.maxWidth > 0 && !shouldStopLabelPlacement) {
|
|
190
195
|
if (shouldUseHtml) {
|
|
191
196
|
htmlLabels.push({
|
|
192
197
|
x: data.center[0] + label.x,
|
|
@@ -200,7 +205,7 @@ export function preparePieData(args) {
|
|
|
200
205
|
labels.push(label);
|
|
201
206
|
}
|
|
202
207
|
const connector = {
|
|
203
|
-
path: line(getConnectorPoints(
|
|
208
|
+
path: line(getConnectorPoints(label.angle)),
|
|
204
209
|
color: relatedSegment.data.color,
|
|
205
210
|
};
|
|
206
211
|
connectors.push(connector);
|
|
@@ -218,43 +223,71 @@ export function preparePieData(args) {
|
|
|
218
223
|
data,
|
|
219
224
|
series: items,
|
|
220
225
|
});
|
|
226
|
+
let maxLeftRightFreeSpace = Infinity;
|
|
227
|
+
let labelsOverflow = 0;
|
|
228
|
+
preparedLabels.labels.forEach((label) => {
|
|
229
|
+
const left = getLeftPosition(label);
|
|
230
|
+
let freeSpace = 0;
|
|
231
|
+
if (left < 0) {
|
|
232
|
+
freeSpace = boundsWidth / 2 - Math.abs(left);
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
freeSpace = boundsWidth / 2 - (left + label.size.width);
|
|
236
|
+
}
|
|
237
|
+
maxLeftRightFreeSpace = Math.max(0, Math.min(maxLeftRightFreeSpace, freeSpace));
|
|
238
|
+
labelsOverflow = freeSpace < 0 ? Math.max(labelsOverflow, -freeSpace) : labelsOverflow;
|
|
239
|
+
});
|
|
221
240
|
const segmentMaxRadius = Math.max(...data.segments.map((s) => s.data.radius));
|
|
222
|
-
|
|
223
|
-
const bottom = Math.max(data.center[1] + segmentMaxRadius, ...preparedLabels.labels.map((l) => l.y + data.center[1] + l.size.height), ...preparedLabels.htmlLabels.map((l) => l.y + l.size.height));
|
|
224
|
-
if (topAdjustment > 0) {
|
|
241
|
+
if (labelsOverflow) {
|
|
225
242
|
data.segments.forEach((s) => {
|
|
226
|
-
const
|
|
227
|
-
s.data.radius =
|
|
243
|
+
const neeSegmentRadius = Math.max(minRadius, s.data.radius - labelsOverflow);
|
|
244
|
+
s.data.radius = neeSegmentRadius;
|
|
228
245
|
});
|
|
229
|
-
data.center[1] -= topAdjustment / 2;
|
|
230
246
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
|
|
247
|
+
else {
|
|
248
|
+
let topFreeSpace = data.center[1] - segmentMaxRadius - haloSize;
|
|
249
|
+
if (preparedLabels.labels.length) {
|
|
250
|
+
const topSvgLabel = Math.max(0, ...preparedLabels.labels.map((l) => -l.y));
|
|
251
|
+
topFreeSpace = Math.min(topFreeSpace, data.center[1] - topSvgLabel);
|
|
252
|
+
}
|
|
253
|
+
if (preparedLabels.htmlLabels.length) {
|
|
254
|
+
const topHtmlLabel = Math.max(0, ...preparedLabels.htmlLabels.map((l) => l.y));
|
|
255
|
+
topFreeSpace = Math.min(topFreeSpace, topHtmlLabel);
|
|
256
|
+
}
|
|
257
|
+
let bottomFreeSpace = data.center[1] - segmentMaxRadius - haloSize;
|
|
258
|
+
if (preparedLabels.labels.length) {
|
|
259
|
+
const bottomSvgLabel = Math.max(0, ...preparedLabels.labels.map((l) => l.y + l.size.height));
|
|
260
|
+
bottomFreeSpace = Math.min(bottomFreeSpace, data.center[1] - bottomSvgLabel);
|
|
261
|
+
}
|
|
262
|
+
if (preparedLabels.htmlLabels.length) {
|
|
263
|
+
const bottomHtmlLabel = Math.max(0, ...preparedLabels.htmlLabels.map((l) => l.y + l.size.height));
|
|
264
|
+
bottomFreeSpace = Math.min(bottomFreeSpace, data.center[1] * 2 - bottomHtmlLabel);
|
|
265
|
+
}
|
|
266
|
+
const topAdjustment = Math.max(0, Math.min(topFreeSpace, maxLeftRightFreeSpace));
|
|
267
|
+
const bottomAdjustment = Math.max(0, Math.min(bottomFreeSpace, maxLeftRightFreeSpace));
|
|
268
|
+
if (topAdjustment && topAdjustment >= bottomAdjustment) {
|
|
269
|
+
data.segments.forEach((s) => {
|
|
270
|
+
const nextPossibleRadius = s.data.radius + (topAdjustment + bottomAdjustment) / 2;
|
|
271
|
+
s.data.radius = Math.min(nextPossibleRadius, maxRadius);
|
|
272
|
+
});
|
|
273
|
+
data.center[1] -= (topAdjustment - bottomAdjustment) / 2;
|
|
274
|
+
}
|
|
275
|
+
else if (bottomAdjustment) {
|
|
276
|
+
data.segments.forEach((s) => {
|
|
277
|
+
const nextPossibleRadius = s.data.radius + (topAdjustment + bottomAdjustment) / 2;
|
|
278
|
+
s.data.radius = Math.min(nextPossibleRadius, maxRadius);
|
|
279
|
+
});
|
|
280
|
+
data.center[1] += (bottomAdjustment - topAdjustment) / 2;
|
|
281
|
+
}
|
|
238
282
|
}
|
|
239
283
|
const { labels, htmlLabels, connectors } = prepareLabels({
|
|
240
284
|
data,
|
|
241
285
|
series: items,
|
|
286
|
+
allowOverlow: false,
|
|
242
287
|
});
|
|
243
288
|
data.labels = labels;
|
|
244
289
|
data.htmlLabels = htmlLabels;
|
|
245
290
|
data.connectors = connectors;
|
|
246
|
-
if (maxMissingWidth > 0) {
|
|
247
|
-
const { dataLabels } = items[0];
|
|
248
|
-
if (dataLabels.enabled) {
|
|
249
|
-
data.segments.forEach((s) => {
|
|
250
|
-
s.data.radius = Math.max(minRadius, s.data.radius - maxMissingWidth);
|
|
251
|
-
});
|
|
252
|
-
const finalLabels = prepareLabels({ data, series: items });
|
|
253
|
-
data.labels = finalLabels.labels;
|
|
254
|
-
data.htmlLabels = finalLabels.htmlLabels;
|
|
255
|
-
data.connectors = finalLabels.connectors;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
291
|
return data;
|
|
259
292
|
});
|
|
260
293
|
}
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import type { CurveFactory } from 'd3';
|
|
2
|
+
import type { PointPosition } from '../../../types';
|
|
2
3
|
import type { PreparedPieData, SegmentData } from './types';
|
|
3
4
|
export declare const pieGenerator: import("d3-shape").Pie<any, SegmentData>;
|
|
4
5
|
export declare function getCurveFactory(data: PreparedPieData): CurveFactory | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* Inscribed angle at vertex A (opposite side/chord BC): the angle between rays AB and AC.
|
|
8
|
+
*
|
|
9
|
+
* The order of B and C does not affect the result.
|
|
10
|
+
*
|
|
11
|
+
* @see: https://en.wikipedia.org/wiki/Inscribed_angle
|
|
12
|
+
* @returns The angle in degrees, in the range [0, 180].
|
|
13
|
+
*/
|
|
14
|
+
export declare function getInscribedAngle(a: PointPosition, b: PointPosition, c: PointPosition): number;
|
|
@@ -13,3 +13,21 @@ export function getCurveFactory(data) {
|
|
|
13
13
|
}
|
|
14
14
|
return undefined;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Inscribed angle at vertex A (opposite side/chord BC): the angle between rays AB and AC.
|
|
18
|
+
*
|
|
19
|
+
* The order of B and C does not affect the result.
|
|
20
|
+
*
|
|
21
|
+
* @see: https://en.wikipedia.org/wiki/Inscribed_angle
|
|
22
|
+
* @returns The angle in degrees, in the range [0, 180].
|
|
23
|
+
*/
|
|
24
|
+
export function getInscribedAngle(a, b, c) {
|
|
25
|
+
const ux = b[0] - a[0];
|
|
26
|
+
const uy = b[1] - a[1];
|
|
27
|
+
const vx = c[0] - a[0];
|
|
28
|
+
const vy = c[1] - a[1];
|
|
29
|
+
const dot = ux * vx + uy * vy;
|
|
30
|
+
const cross = ux * vy - uy * vx;
|
|
31
|
+
const radians = Math.atan2(Math.abs(cross), dot);
|
|
32
|
+
return (radians * 180) / Math.PI;
|
|
33
|
+
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { stratify, treemap, treemapBinary, treemapDice, treemapSlice, treemapSliceDice, treemapSquarify, } from 'd3';
|
|
1
|
+
import { ascending, descending, sort, stratify, treemap, treemapBinary, treemapDice, treemapSlice, treemapSliceDice, treemapSquarify, } from 'd3';
|
|
2
2
|
import { LayoutAlgorithm } from '../../../constants';
|
|
3
3
|
import { getLabelsSize } from '../../../utils';
|
|
4
4
|
import { getFormattedValue } from '../../../utils/chart/format';
|
|
5
5
|
const DEFAULT_PADDING = 1;
|
|
6
6
|
function getLabels(args) {
|
|
7
|
-
const { data, options: { html, padding, align }, } = args;
|
|
7
|
+
const { data, options: { html, padding, align, style }, } = args;
|
|
8
8
|
return data.reduce((acc, d) => {
|
|
9
9
|
const texts = Array.isArray(d.data.name) ? d.data.name : [d.data.name];
|
|
10
10
|
texts.forEach((text, index) => {
|
|
11
11
|
var _a;
|
|
12
12
|
const label = getFormattedValue(Object.assign({ value: text }, args.options));
|
|
13
|
-
const { maxHeight: lineHeight, maxWidth: labelMaxWidth } = (_a = getLabelsSize({ labels: [label], html })) !== null && _a !== void 0 ? _a : {};
|
|
13
|
+
const { maxHeight: lineHeight, maxWidth: labelMaxWidth } = (_a = getLabelsSize({ labels: [label], style, html })) !== null && _a !== void 0 ? _a : {};
|
|
14
14
|
const left = d.x0 + padding;
|
|
15
15
|
const right = d.x1 - padding;
|
|
16
16
|
const spaceWidth = Math.max(0, right - left);
|
|
@@ -35,6 +35,10 @@ function getLabels(args) {
|
|
|
35
35
|
break;
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
+
const bottom = y + lineHeight;
|
|
39
|
+
if (bottom > d.y1) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
38
42
|
const item = html
|
|
39
43
|
? {
|
|
40
44
|
content: label,
|
|
@@ -57,7 +61,25 @@ function getLabels(args) {
|
|
|
57
61
|
export function prepareTreemapData(args) {
|
|
58
62
|
var _a;
|
|
59
63
|
const { series, width, height } = args;
|
|
60
|
-
const
|
|
64
|
+
const parentNodeValues = {};
|
|
65
|
+
let dataWithRootNode = series.data.reduce((acc, d) => {
|
|
66
|
+
var _a, _b;
|
|
67
|
+
const dataChunk = Object.assign({}, d);
|
|
68
|
+
if (!dataChunk.parentId) {
|
|
69
|
+
dataChunk.parentId = series.id;
|
|
70
|
+
}
|
|
71
|
+
if (dataChunk.parentId) {
|
|
72
|
+
parentNodeValues[dataChunk.parentId] =
|
|
73
|
+
((_a = parentNodeValues[dataChunk.parentId]) !== null && _a !== void 0 ? _a : 0) + ((_b = dataChunk.value) !== null && _b !== void 0 ? _b : 0);
|
|
74
|
+
}
|
|
75
|
+
acc.push(dataChunk);
|
|
76
|
+
return acc;
|
|
77
|
+
}, [{ name: series.name, id: series.id }]);
|
|
78
|
+
if (series.sorting.enabled) {
|
|
79
|
+
const getSortingValue = (d) => { var _a, _b; return (_a = d.value) !== null && _a !== void 0 ? _a : parentNodeValues[(_b = d.id) !== null && _b !== void 0 ? _b : '']; };
|
|
80
|
+
const comparator = series.sorting.direction === 'desc' ? descending : ascending;
|
|
81
|
+
dataWithRootNode = sort(dataWithRootNode, (a, b) => comparator(getSortingValue(a), getSortingValue(b)));
|
|
82
|
+
}
|
|
61
83
|
const hierarchy = stratify()
|
|
62
84
|
.id((d) => {
|
|
63
85
|
if (d.id) {
|
|
@@ -102,7 +124,7 @@ export function prepareTreemapData(args) {
|
|
|
102
124
|
const { html, style: dataLabelsStyle } = series.dataLabels;
|
|
103
125
|
const labels = getLabels({ data: leaves, options: series.dataLabels });
|
|
104
126
|
if (html) {
|
|
105
|
-
const htmlItems = labels.map((l) => (Object.assign({ style: dataLabelsStyle }, l)));
|
|
127
|
+
const htmlItems = labels.map((l) => (Object.assign({ style: Object.assign(Object.assign({}, dataLabelsStyle), { maxWidth: l.size.width, maxHeight: l.size.height, overflow: 'hidden' }) }, l)));
|
|
106
128
|
htmlElements.push(...htmlItems);
|
|
107
129
|
}
|
|
108
130
|
else {
|
|
@@ -111,13 +133,3 @@ export function prepareTreemapData(args) {
|
|
|
111
133
|
}
|
|
112
134
|
return { labelData, leaves, series, htmlElements };
|
|
113
135
|
}
|
|
114
|
-
function getSeriesDataWithRootNode(series) {
|
|
115
|
-
return series.data.reduce((acc, d) => {
|
|
116
|
-
const dataChunk = Object.assign({}, d);
|
|
117
|
-
if (!dataChunk.parentId) {
|
|
118
|
-
dataChunk.parentId = series.id;
|
|
119
|
-
}
|
|
120
|
-
acc.push(dataChunk);
|
|
121
|
-
return acc;
|
|
122
|
-
}, [{ name: series.name, id: series.id }]);
|
|
123
|
-
}
|
|
@@ -43,4 +43,15 @@ export interface TreemapSeries<T = MeaningfulAny> extends BaseSeries {
|
|
|
43
43
|
/** Horizontal alignment of the data label inside the tile. */
|
|
44
44
|
align?: 'left' | 'center' | 'right';
|
|
45
45
|
};
|
|
46
|
+
/** Data sorting settings (affects the order in which blocks are displayed inside the chart).
|
|
47
|
+
* If the option is not specified, the data is displayed in the order defined by the user. */
|
|
48
|
+
sorting?: {
|
|
49
|
+
/** Enable or disable sorting. */
|
|
50
|
+
enabled?: boolean;
|
|
51
|
+
/** The sorting direction.
|
|
52
|
+
*
|
|
53
|
+
* @default: 'desc'
|
|
54
|
+
*/
|
|
55
|
+
direction?: 'asc' | 'desc';
|
|
56
|
+
};
|
|
46
57
|
}
|
|
@@ -64,7 +64,7 @@ function renderLabels(selection, { labels, style = {}, attrs = {}, }) {
|
|
|
64
64
|
return text;
|
|
65
65
|
}
|
|
66
66
|
export function getLabelsSize({ labels, style, rotation, html, }) {
|
|
67
|
-
var _a, _b, _c;
|
|
67
|
+
var _a, _b, _c, _d, _e;
|
|
68
68
|
if (!labels.filter(Boolean).length) {
|
|
69
69
|
return { maxHeight: 0, maxWidth: 0 };
|
|
70
70
|
}
|
|
@@ -74,7 +74,12 @@ export function getLabelsSize({ labels, style, rotation, html, }) {
|
|
|
74
74
|
const result = { maxHeight: 0, maxWidth: 0 };
|
|
75
75
|
let labelWrapper;
|
|
76
76
|
if (html) {
|
|
77
|
-
labelWrapper = container
|
|
77
|
+
labelWrapper = container
|
|
78
|
+
.append('div')
|
|
79
|
+
.style('position', 'absolute')
|
|
80
|
+
.style('font-size', (_a = style === null || style === void 0 ? void 0 : style.fontSize) !== null && _a !== void 0 ? _a : '')
|
|
81
|
+
.style('font-weight', (_b = style === null || style === void 0 ? void 0 : style.fontWeight) !== null && _b !== void 0 ? _b : '')
|
|
82
|
+
.node();
|
|
78
83
|
const { height, width } = labels.reduce((acc, l) => {
|
|
79
84
|
var _a, _b;
|
|
80
85
|
if (labelWrapper) {
|
|
@@ -97,9 +102,9 @@ export function getLabelsSize({ labels, style, rotation, html, }) {
|
|
|
97
102
|
.attr('text-anchor', rotation > 0 ? 'start' : 'end')
|
|
98
103
|
.style('transform', `rotate(${rotation}deg)`);
|
|
99
104
|
}
|
|
100
|
-
const rect = (
|
|
101
|
-
result.maxWidth = (
|
|
102
|
-
result.maxHeight = (
|
|
105
|
+
const rect = (_c = svg.select('g').node()) === null || _c === void 0 ? void 0 : _c.getBoundingClientRect();
|
|
106
|
+
result.maxWidth = (_d = rect === null || rect === void 0 ? void 0 : rect.width) !== null && _d !== void 0 ? _d : 0;
|
|
107
|
+
result.maxHeight = (_e = rect === null || rect === void 0 ? void 0 : rect.height) !== null && _e !== void 0 ? _e : 0;
|
|
103
108
|
}
|
|
104
109
|
container.remove();
|
|
105
110
|
return result;
|
|
@@ -6,7 +6,7 @@ import { prepareLegendSymbol } from './utils';
|
|
|
6
6
|
export function prepareTreemap(args) {
|
|
7
7
|
const { colorScale, legend, series } = args;
|
|
8
8
|
return series.map((s) => {
|
|
9
|
-
var _a, _b;
|
|
9
|
+
var _a, _b, _c;
|
|
10
10
|
const id = getUniqId();
|
|
11
11
|
const name = s.name || '';
|
|
12
12
|
const color = s.color || colorScale(name);
|
|
@@ -30,9 +30,10 @@ export function prepareTreemap(args) {
|
|
|
30
30
|
enabled: get(s, 'legend.enabled', legend.enabled),
|
|
31
31
|
symbol: prepareLegendSymbol(s),
|
|
32
32
|
},
|
|
33
|
-
levels: s.levels,
|
|
33
|
+
levels: (_c = s.levels) !== null && _c !== void 0 ? _c : [],
|
|
34
34
|
layoutAlgorithm: get(s, 'layoutAlgorithm', LayoutAlgorithm.Binary),
|
|
35
35
|
cursor: get(s, 'cursor', null),
|
|
36
|
+
sorting: Object.assign({ enabled: false, direction: 'desc' }, s.sorting),
|
|
36
37
|
};
|
|
37
38
|
return preparedSeries;
|
|
38
39
|
});
|
|
@@ -242,7 +242,8 @@ export type PreparedTreemapSeries = {
|
|
|
242
242
|
format?: ValueFormat;
|
|
243
243
|
};
|
|
244
244
|
layoutAlgorithm: `${LayoutAlgorithm}`;
|
|
245
|
-
|
|
245
|
+
sorting: Required<TreemapSeries['sorting']>;
|
|
246
|
+
} & BasePreparedSeries & Required<Omit<TreemapSeries, keyof BasePreparedSeries>>;
|
|
246
247
|
export type PreparedWaterfallSeriesData = WaterfallSeriesData & {
|
|
247
248
|
index: number;
|
|
248
249
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { arc, group, line as lineGenerator } from 'd3';
|
|
2
2
|
import { calculateNumericProperty, getLabelsSize, getLeftPosition, isLabelsOverlapping, } from '../../../utils';
|
|
3
3
|
import { getFormattedValue } from '../../../utils/chart/format';
|
|
4
|
-
import { getCurveFactory, pieGenerator } from './utils';
|
|
4
|
+
import { getCurveFactory, getInscribedAngle, pieGenerator } from './utils';
|
|
5
5
|
const FULL_CIRCLE = Math.PI * 2;
|
|
6
6
|
const getCenter = (boundsWidth, boundsHeight, center) => {
|
|
7
7
|
var _a, _b;
|
|
@@ -23,7 +23,6 @@ export function preparePieData(args) {
|
|
|
23
23
|
const maxRadius = Math.min(boundsWidth, boundsHeight) / 2 - haloSize;
|
|
24
24
|
const minRadius = maxRadius * 0.3;
|
|
25
25
|
const groupedPieSeries = group(preparedSeries, (pieSeries) => pieSeries.stackId);
|
|
26
|
-
let maxMissingWidth = 0;
|
|
27
26
|
const prepareItem = (stackId, items) => {
|
|
28
27
|
var _a;
|
|
29
28
|
const series = items[0];
|
|
@@ -77,7 +76,7 @@ export function preparePieData(args) {
|
|
|
77
76
|
return data;
|
|
78
77
|
};
|
|
79
78
|
const prepareLabels = (prepareLabelsArgs) => {
|
|
80
|
-
const { data, series } = prepareLabelsArgs;
|
|
79
|
+
const { data, series, allowOverlow = true } = prepareLabelsArgs;
|
|
81
80
|
const { dataLabels } = series[0];
|
|
82
81
|
const labels = [];
|
|
83
82
|
const htmlLabels = [];
|
|
@@ -104,6 +103,7 @@ export function preparePieData(args) {
|
|
|
104
103
|
const labelArcGenerator = arc()
|
|
105
104
|
.innerRadius((d) => d.data.radius + distance + connectorPadding)
|
|
106
105
|
.outerRadius((d) => d.data.radius + distance + connectorPadding);
|
|
106
|
+
let shouldStopLabelPlacement = false;
|
|
107
107
|
series.forEach((d, index) => {
|
|
108
108
|
const prevLabel = labels[labels.length - 1];
|
|
109
109
|
const text = getFormattedValue(Object.assign({ value: d.data.label || d.data.value }, d.dataLabels));
|
|
@@ -120,7 +120,6 @@ export function preparePieData(args) {
|
|
|
120
120
|
else {
|
|
121
121
|
y = y < 0 ? y - labelHeight : y;
|
|
122
122
|
}
|
|
123
|
-
x = Math.max(-boundsWidth / 2, x);
|
|
124
123
|
return [x, y];
|
|
125
124
|
};
|
|
126
125
|
const getConnectorPoints = (angle) => {
|
|
@@ -148,11 +147,25 @@ export function preparePieData(args) {
|
|
|
148
147
|
segment: relatedSegment.data,
|
|
149
148
|
angle: midAngle,
|
|
150
149
|
};
|
|
150
|
+
if (!allowOverlow) {
|
|
151
|
+
const labelLeftPosition = getLeftPosition(label);
|
|
152
|
+
const newMaxWidth = labelLeftPosition > 0
|
|
153
|
+
? Math.min(boundsWidth / 2 - labelLeftPosition, labelWidth)
|
|
154
|
+
: Math.min(labelWidth - (-labelLeftPosition - boundsWidth / 2), labelWidth);
|
|
155
|
+
if (newMaxWidth !== label.maxWidth) {
|
|
156
|
+
label.maxWidth = Math.max(0, newMaxWidth);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
151
159
|
let overlap = false;
|
|
152
160
|
if (prevLabel) {
|
|
153
161
|
overlap = isLabelsOverlapping(prevLabel, label, dataLabels.padding);
|
|
162
|
+
const startAngle = relatedSegment.startAngle +
|
|
163
|
+
(relatedSegment.endAngle - relatedSegment.startAngle) / 2;
|
|
154
164
|
if (overlap) {
|
|
155
|
-
let shouldAdjustAngle =
|
|
165
|
+
let shouldAdjustAngle = !shouldStopLabelPlacement;
|
|
166
|
+
const connectorPoints = getConnectorPoints(startAngle);
|
|
167
|
+
const pointA = connectorPoints[0];
|
|
168
|
+
const pointB = connectorPoints[connectorPoints.length - 1];
|
|
156
169
|
const step = Math.PI / 180;
|
|
157
170
|
while (shouldAdjustAngle) {
|
|
158
171
|
const newAngle = label.angle + step;
|
|
@@ -164,6 +177,11 @@ export function preparePieData(args) {
|
|
|
164
177
|
const [newX, newY] = getLabelPosition(newAngle);
|
|
165
178
|
label.x = newX;
|
|
166
179
|
label.y = newY;
|
|
180
|
+
const inscribedAngle = getInscribedAngle(pointA, pointB, [newX, newY]);
|
|
181
|
+
if (inscribedAngle > 90) {
|
|
182
|
+
shouldAdjustAngle = false;
|
|
183
|
+
shouldStopLabelPlacement = true;
|
|
184
|
+
}
|
|
167
185
|
if (!isLabelsOverlapping(prevLabel, label, dataLabels.padding)) {
|
|
168
186
|
shouldAdjustAngle = false;
|
|
169
187
|
overlap = false;
|
|
@@ -172,21 +190,8 @@ export function preparePieData(args) {
|
|
|
172
190
|
}
|
|
173
191
|
}
|
|
174
192
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (Math.abs(left) > boundsWidth / 2) {
|
|
178
|
-
const overflow = Math.abs(left) - boundsWidth / 2;
|
|
179
|
-
label.maxWidth = label.size.width - overflow;
|
|
180
|
-
maxMissingWidth = Math.max(maxMissingWidth, overflow);
|
|
181
|
-
}
|
|
182
|
-
else {
|
|
183
|
-
const right = left + label.size.width;
|
|
184
|
-
if (right > boundsWidth / 2) {
|
|
185
|
-
const overflow = right - boundsWidth / 2;
|
|
186
|
-
label.maxWidth = label.size.width - overflow;
|
|
187
|
-
maxMissingWidth = Math.max(maxMissingWidth, overflow);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
193
|
+
const isLabelOverlapped = !dataLabels.allowOverlap && overlap;
|
|
194
|
+
if (!isLabelOverlapped && label.maxWidth > 0 && !shouldStopLabelPlacement) {
|
|
190
195
|
if (shouldUseHtml) {
|
|
191
196
|
htmlLabels.push({
|
|
192
197
|
x: data.center[0] + label.x,
|
|
@@ -200,7 +205,7 @@ export function preparePieData(args) {
|
|
|
200
205
|
labels.push(label);
|
|
201
206
|
}
|
|
202
207
|
const connector = {
|
|
203
|
-
path: line(getConnectorPoints(
|
|
208
|
+
path: line(getConnectorPoints(label.angle)),
|
|
204
209
|
color: relatedSegment.data.color,
|
|
205
210
|
};
|
|
206
211
|
connectors.push(connector);
|
|
@@ -218,43 +223,71 @@ export function preparePieData(args) {
|
|
|
218
223
|
data,
|
|
219
224
|
series: items,
|
|
220
225
|
});
|
|
226
|
+
let maxLeftRightFreeSpace = Infinity;
|
|
227
|
+
let labelsOverflow = 0;
|
|
228
|
+
preparedLabels.labels.forEach((label) => {
|
|
229
|
+
const left = getLeftPosition(label);
|
|
230
|
+
let freeSpace = 0;
|
|
231
|
+
if (left < 0) {
|
|
232
|
+
freeSpace = boundsWidth / 2 - Math.abs(left);
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
freeSpace = boundsWidth / 2 - (left + label.size.width);
|
|
236
|
+
}
|
|
237
|
+
maxLeftRightFreeSpace = Math.max(0, Math.min(maxLeftRightFreeSpace, freeSpace));
|
|
238
|
+
labelsOverflow = freeSpace < 0 ? Math.max(labelsOverflow, -freeSpace) : labelsOverflow;
|
|
239
|
+
});
|
|
221
240
|
const segmentMaxRadius = Math.max(...data.segments.map((s) => s.data.radius));
|
|
222
|
-
|
|
223
|
-
const bottom = Math.max(data.center[1] + segmentMaxRadius, ...preparedLabels.labels.map((l) => l.y + data.center[1] + l.size.height), ...preparedLabels.htmlLabels.map((l) => l.y + l.size.height));
|
|
224
|
-
if (topAdjustment > 0) {
|
|
241
|
+
if (labelsOverflow) {
|
|
225
242
|
data.segments.forEach((s) => {
|
|
226
|
-
const
|
|
227
|
-
s.data.radius =
|
|
243
|
+
const neeSegmentRadius = Math.max(minRadius, s.data.radius - labelsOverflow);
|
|
244
|
+
s.data.radius = neeSegmentRadius;
|
|
228
245
|
});
|
|
229
|
-
data.center[1] -= topAdjustment / 2;
|
|
230
246
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
|
|
247
|
+
else {
|
|
248
|
+
let topFreeSpace = data.center[1] - segmentMaxRadius - haloSize;
|
|
249
|
+
if (preparedLabels.labels.length) {
|
|
250
|
+
const topSvgLabel = Math.max(0, ...preparedLabels.labels.map((l) => -l.y));
|
|
251
|
+
topFreeSpace = Math.min(topFreeSpace, data.center[1] - topSvgLabel);
|
|
252
|
+
}
|
|
253
|
+
if (preparedLabels.htmlLabels.length) {
|
|
254
|
+
const topHtmlLabel = Math.max(0, ...preparedLabels.htmlLabels.map((l) => l.y));
|
|
255
|
+
topFreeSpace = Math.min(topFreeSpace, topHtmlLabel);
|
|
256
|
+
}
|
|
257
|
+
let bottomFreeSpace = data.center[1] - segmentMaxRadius - haloSize;
|
|
258
|
+
if (preparedLabels.labels.length) {
|
|
259
|
+
const bottomSvgLabel = Math.max(0, ...preparedLabels.labels.map((l) => l.y + l.size.height));
|
|
260
|
+
bottomFreeSpace = Math.min(bottomFreeSpace, data.center[1] - bottomSvgLabel);
|
|
261
|
+
}
|
|
262
|
+
if (preparedLabels.htmlLabels.length) {
|
|
263
|
+
const bottomHtmlLabel = Math.max(0, ...preparedLabels.htmlLabels.map((l) => l.y + l.size.height));
|
|
264
|
+
bottomFreeSpace = Math.min(bottomFreeSpace, data.center[1] * 2 - bottomHtmlLabel);
|
|
265
|
+
}
|
|
266
|
+
const topAdjustment = Math.max(0, Math.min(topFreeSpace, maxLeftRightFreeSpace));
|
|
267
|
+
const bottomAdjustment = Math.max(0, Math.min(bottomFreeSpace, maxLeftRightFreeSpace));
|
|
268
|
+
if (topAdjustment && topAdjustment >= bottomAdjustment) {
|
|
269
|
+
data.segments.forEach((s) => {
|
|
270
|
+
const nextPossibleRadius = s.data.radius + (topAdjustment + bottomAdjustment) / 2;
|
|
271
|
+
s.data.radius = Math.min(nextPossibleRadius, maxRadius);
|
|
272
|
+
});
|
|
273
|
+
data.center[1] -= (topAdjustment - bottomAdjustment) / 2;
|
|
274
|
+
}
|
|
275
|
+
else if (bottomAdjustment) {
|
|
276
|
+
data.segments.forEach((s) => {
|
|
277
|
+
const nextPossibleRadius = s.data.radius + (topAdjustment + bottomAdjustment) / 2;
|
|
278
|
+
s.data.radius = Math.min(nextPossibleRadius, maxRadius);
|
|
279
|
+
});
|
|
280
|
+
data.center[1] += (bottomAdjustment - topAdjustment) / 2;
|
|
281
|
+
}
|
|
238
282
|
}
|
|
239
283
|
const { labels, htmlLabels, connectors } = prepareLabels({
|
|
240
284
|
data,
|
|
241
285
|
series: items,
|
|
286
|
+
allowOverlow: false,
|
|
242
287
|
});
|
|
243
288
|
data.labels = labels;
|
|
244
289
|
data.htmlLabels = htmlLabels;
|
|
245
290
|
data.connectors = connectors;
|
|
246
|
-
if (maxMissingWidth > 0) {
|
|
247
|
-
const { dataLabels } = items[0];
|
|
248
|
-
if (dataLabels.enabled) {
|
|
249
|
-
data.segments.forEach((s) => {
|
|
250
|
-
s.data.radius = Math.max(minRadius, s.data.radius - maxMissingWidth);
|
|
251
|
-
});
|
|
252
|
-
const finalLabels = prepareLabels({ data, series: items });
|
|
253
|
-
data.labels = finalLabels.labels;
|
|
254
|
-
data.htmlLabels = finalLabels.htmlLabels;
|
|
255
|
-
data.connectors = finalLabels.connectors;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
291
|
return data;
|
|
259
292
|
});
|
|
260
293
|
}
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import type { CurveFactory } from 'd3';
|
|
2
|
+
import type { PointPosition } from '../../../types';
|
|
2
3
|
import type { PreparedPieData, SegmentData } from './types';
|
|
3
4
|
export declare const pieGenerator: import("d3-shape").Pie<any, SegmentData>;
|
|
4
5
|
export declare function getCurveFactory(data: PreparedPieData): CurveFactory | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* Inscribed angle at vertex A (opposite side/chord BC): the angle between rays AB and AC.
|
|
8
|
+
*
|
|
9
|
+
* The order of B and C does not affect the result.
|
|
10
|
+
*
|
|
11
|
+
* @see: https://en.wikipedia.org/wiki/Inscribed_angle
|
|
12
|
+
* @returns The angle in degrees, in the range [0, 180].
|
|
13
|
+
*/
|
|
14
|
+
export declare function getInscribedAngle(a: PointPosition, b: PointPosition, c: PointPosition): number;
|
|
@@ -13,3 +13,21 @@ export function getCurveFactory(data) {
|
|
|
13
13
|
}
|
|
14
14
|
return undefined;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Inscribed angle at vertex A (opposite side/chord BC): the angle between rays AB and AC.
|
|
18
|
+
*
|
|
19
|
+
* The order of B and C does not affect the result.
|
|
20
|
+
*
|
|
21
|
+
* @see: https://en.wikipedia.org/wiki/Inscribed_angle
|
|
22
|
+
* @returns The angle in degrees, in the range [0, 180].
|
|
23
|
+
*/
|
|
24
|
+
export function getInscribedAngle(a, b, c) {
|
|
25
|
+
const ux = b[0] - a[0];
|
|
26
|
+
const uy = b[1] - a[1];
|
|
27
|
+
const vx = c[0] - a[0];
|
|
28
|
+
const vy = c[1] - a[1];
|
|
29
|
+
const dot = ux * vx + uy * vy;
|
|
30
|
+
const cross = ux * vy - uy * vx;
|
|
31
|
+
const radians = Math.atan2(Math.abs(cross), dot);
|
|
32
|
+
return (radians * 180) / Math.PI;
|
|
33
|
+
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { stratify, treemap, treemapBinary, treemapDice, treemapSlice, treemapSliceDice, treemapSquarify, } from 'd3';
|
|
1
|
+
import { ascending, descending, sort, stratify, treemap, treemapBinary, treemapDice, treemapSlice, treemapSliceDice, treemapSquarify, } from 'd3';
|
|
2
2
|
import { LayoutAlgorithm } from '../../../constants';
|
|
3
3
|
import { getLabelsSize } from '../../../utils';
|
|
4
4
|
import { getFormattedValue } from '../../../utils/chart/format';
|
|
5
5
|
const DEFAULT_PADDING = 1;
|
|
6
6
|
function getLabels(args) {
|
|
7
|
-
const { data, options: { html, padding, align }, } = args;
|
|
7
|
+
const { data, options: { html, padding, align, style }, } = args;
|
|
8
8
|
return data.reduce((acc, d) => {
|
|
9
9
|
const texts = Array.isArray(d.data.name) ? d.data.name : [d.data.name];
|
|
10
10
|
texts.forEach((text, index) => {
|
|
11
11
|
var _a;
|
|
12
12
|
const label = getFormattedValue(Object.assign({ value: text }, args.options));
|
|
13
|
-
const { maxHeight: lineHeight, maxWidth: labelMaxWidth } = (_a = getLabelsSize({ labels: [label], html })) !== null && _a !== void 0 ? _a : {};
|
|
13
|
+
const { maxHeight: lineHeight, maxWidth: labelMaxWidth } = (_a = getLabelsSize({ labels: [label], style, html })) !== null && _a !== void 0 ? _a : {};
|
|
14
14
|
const left = d.x0 + padding;
|
|
15
15
|
const right = d.x1 - padding;
|
|
16
16
|
const spaceWidth = Math.max(0, right - left);
|
|
@@ -35,6 +35,10 @@ function getLabels(args) {
|
|
|
35
35
|
break;
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
+
const bottom = y + lineHeight;
|
|
39
|
+
if (bottom > d.y1) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
38
42
|
const item = html
|
|
39
43
|
? {
|
|
40
44
|
content: label,
|
|
@@ -57,7 +61,25 @@ function getLabels(args) {
|
|
|
57
61
|
export function prepareTreemapData(args) {
|
|
58
62
|
var _a;
|
|
59
63
|
const { series, width, height } = args;
|
|
60
|
-
const
|
|
64
|
+
const parentNodeValues = {};
|
|
65
|
+
let dataWithRootNode = series.data.reduce((acc, d) => {
|
|
66
|
+
var _a, _b;
|
|
67
|
+
const dataChunk = Object.assign({}, d);
|
|
68
|
+
if (!dataChunk.parentId) {
|
|
69
|
+
dataChunk.parentId = series.id;
|
|
70
|
+
}
|
|
71
|
+
if (dataChunk.parentId) {
|
|
72
|
+
parentNodeValues[dataChunk.parentId] =
|
|
73
|
+
((_a = parentNodeValues[dataChunk.parentId]) !== null && _a !== void 0 ? _a : 0) + ((_b = dataChunk.value) !== null && _b !== void 0 ? _b : 0);
|
|
74
|
+
}
|
|
75
|
+
acc.push(dataChunk);
|
|
76
|
+
return acc;
|
|
77
|
+
}, [{ name: series.name, id: series.id }]);
|
|
78
|
+
if (series.sorting.enabled) {
|
|
79
|
+
const getSortingValue = (d) => { var _a, _b; return (_a = d.value) !== null && _a !== void 0 ? _a : parentNodeValues[(_b = d.id) !== null && _b !== void 0 ? _b : '']; };
|
|
80
|
+
const comparator = series.sorting.direction === 'desc' ? descending : ascending;
|
|
81
|
+
dataWithRootNode = sort(dataWithRootNode, (a, b) => comparator(getSortingValue(a), getSortingValue(b)));
|
|
82
|
+
}
|
|
61
83
|
const hierarchy = stratify()
|
|
62
84
|
.id((d) => {
|
|
63
85
|
if (d.id) {
|
|
@@ -102,7 +124,7 @@ export function prepareTreemapData(args) {
|
|
|
102
124
|
const { html, style: dataLabelsStyle } = series.dataLabels;
|
|
103
125
|
const labels = getLabels({ data: leaves, options: series.dataLabels });
|
|
104
126
|
if (html) {
|
|
105
|
-
const htmlItems = labels.map((l) => (Object.assign({ style: dataLabelsStyle }, l)));
|
|
127
|
+
const htmlItems = labels.map((l) => (Object.assign({ style: Object.assign(Object.assign({}, dataLabelsStyle), { maxWidth: l.size.width, maxHeight: l.size.height, overflow: 'hidden' }) }, l)));
|
|
106
128
|
htmlElements.push(...htmlItems);
|
|
107
129
|
}
|
|
108
130
|
else {
|
|
@@ -111,13 +133,3 @@ export function prepareTreemapData(args) {
|
|
|
111
133
|
}
|
|
112
134
|
return { labelData, leaves, series, htmlElements };
|
|
113
135
|
}
|
|
114
|
-
function getSeriesDataWithRootNode(series) {
|
|
115
|
-
return series.data.reduce((acc, d) => {
|
|
116
|
-
const dataChunk = Object.assign({}, d);
|
|
117
|
-
if (!dataChunk.parentId) {
|
|
118
|
-
dataChunk.parentId = series.id;
|
|
119
|
-
}
|
|
120
|
-
acc.push(dataChunk);
|
|
121
|
-
return acc;
|
|
122
|
-
}, [{ name: series.name, id: series.id }]);
|
|
123
|
-
}
|
|
@@ -43,4 +43,15 @@ export interface TreemapSeries<T = MeaningfulAny> extends BaseSeries {
|
|
|
43
43
|
/** Horizontal alignment of the data label inside the tile. */
|
|
44
44
|
align?: 'left' | 'center' | 'right';
|
|
45
45
|
};
|
|
46
|
+
/** Data sorting settings (affects the order in which blocks are displayed inside the chart).
|
|
47
|
+
* If the option is not specified, the data is displayed in the order defined by the user. */
|
|
48
|
+
sorting?: {
|
|
49
|
+
/** Enable or disable sorting. */
|
|
50
|
+
enabled?: boolean;
|
|
51
|
+
/** The sorting direction.
|
|
52
|
+
*
|
|
53
|
+
* @default: 'desc'
|
|
54
|
+
*/
|
|
55
|
+
direction?: 'asc' | 'desc';
|
|
56
|
+
};
|
|
46
57
|
}
|
|
@@ -64,7 +64,7 @@ function renderLabels(selection, { labels, style = {}, attrs = {}, }) {
|
|
|
64
64
|
return text;
|
|
65
65
|
}
|
|
66
66
|
export function getLabelsSize({ labels, style, rotation, html, }) {
|
|
67
|
-
var _a, _b, _c;
|
|
67
|
+
var _a, _b, _c, _d, _e;
|
|
68
68
|
if (!labels.filter(Boolean).length) {
|
|
69
69
|
return { maxHeight: 0, maxWidth: 0 };
|
|
70
70
|
}
|
|
@@ -74,7 +74,12 @@ export function getLabelsSize({ labels, style, rotation, html, }) {
|
|
|
74
74
|
const result = { maxHeight: 0, maxWidth: 0 };
|
|
75
75
|
let labelWrapper;
|
|
76
76
|
if (html) {
|
|
77
|
-
labelWrapper = container
|
|
77
|
+
labelWrapper = container
|
|
78
|
+
.append('div')
|
|
79
|
+
.style('position', 'absolute')
|
|
80
|
+
.style('font-size', (_a = style === null || style === void 0 ? void 0 : style.fontSize) !== null && _a !== void 0 ? _a : '')
|
|
81
|
+
.style('font-weight', (_b = style === null || style === void 0 ? void 0 : style.fontWeight) !== null && _b !== void 0 ? _b : '')
|
|
82
|
+
.node();
|
|
78
83
|
const { height, width } = labels.reduce((acc, l) => {
|
|
79
84
|
var _a, _b;
|
|
80
85
|
if (labelWrapper) {
|
|
@@ -97,9 +102,9 @@ export function getLabelsSize({ labels, style, rotation, html, }) {
|
|
|
97
102
|
.attr('text-anchor', rotation > 0 ? 'start' : 'end')
|
|
98
103
|
.style('transform', `rotate(${rotation}deg)`);
|
|
99
104
|
}
|
|
100
|
-
const rect = (
|
|
101
|
-
result.maxWidth = (
|
|
102
|
-
result.maxHeight = (
|
|
105
|
+
const rect = (_c = svg.select('g').node()) === null || _c === void 0 ? void 0 : _c.getBoundingClientRect();
|
|
106
|
+
result.maxWidth = (_d = rect === null || rect === void 0 ? void 0 : rect.width) !== null && _d !== void 0 ? _d : 0;
|
|
107
|
+
result.maxHeight = (_e = rect === null || rect === void 0 ? void 0 : rect.height) !== null && _e !== void 0 ? _e : 0;
|
|
103
108
|
}
|
|
104
109
|
container.remove();
|
|
105
110
|
return result;
|