@skdx/angular-charts 0.37.0 → 0.39.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.
|
@@ -457,10 +457,16 @@ const size = (input, w = 600, h = 300) => ({
|
|
|
457
457
|
width: (input.width ?? w) - legendSide(input),
|
|
458
458
|
height: (input.height ?? h) - (legendOptions(input)?.position === 'bottom' ? LEGEND_HEIGHT : 0),
|
|
459
459
|
});
|
|
460
|
-
/**
|
|
461
|
-
|
|
460
|
+
/**
|
|
461
|
+
* Vertical room the legend rows take above the plot, to shift the plot down by. Pass the legend
|
|
462
|
+
* items and the layout width to reserve every wrapped row; without them one row is assumed.
|
|
463
|
+
*/
|
|
464
|
+
const legendTop = (input, items, width) => {
|
|
462
465
|
const l = legendOptions(input);
|
|
463
|
-
|
|
466
|
+
if (!l || !(l.position === undefined || l.position === 'top'))
|
|
467
|
+
return 0;
|
|
468
|
+
const rows = items ? Math.max(1, legendRows(items, width ?? size(input).width, l)) : 1;
|
|
469
|
+
return rows * LEGEND_HEIGHT;
|
|
464
470
|
};
|
|
465
471
|
/** Average glyph width at the chart font size, for text-width estimates. */
|
|
466
472
|
const CHAR = 6.5;
|
|
@@ -735,6 +741,12 @@ function axis([min, max], range, options = false, chartFormat) {
|
|
|
735
741
|
zero: px(scale(Math.min(Math.max(0, domain[0]), domain[1]))),
|
|
736
742
|
};
|
|
737
743
|
}
|
|
744
|
+
/**
|
|
745
|
+
* Left (or right) margin wide enough for the widest tick label a value axis over `[lo, hi]` will
|
|
746
|
+
* draw, never below the default. Charts measure it before laying out so a long label such as
|
|
747
|
+
* "$180.00" is not clipped by the svg edge.
|
|
748
|
+
*/
|
|
749
|
+
const valueAxisRoom = ([lo, hi], format) => Math.max(MARGIN$1.left, niceTicks(lo, hi).reduce((w, v) => Math.max(w, format(v).length), 0) * CHAR + 14);
|
|
738
750
|
/** Whether an axis draws its grid lines. */
|
|
739
751
|
const gridOn = (opts) => opts?.grid !== false;
|
|
740
752
|
/** Keeps every n-th tick so labels of the given widths do not overlap along `width`. */
|
|
@@ -791,6 +803,19 @@ function stateCanvas(input, empty, w, h) {
|
|
|
791
803
|
* This file is part of the SkandaDX organization.
|
|
792
804
|
* Licensed under the MIT License. See LICENSE in the repository root.
|
|
793
805
|
*/
|
|
806
|
+
/**
|
|
807
|
+
* Anchor for a tick label centred at `pos`: `end` when half of it would run past `width`, `start`
|
|
808
|
+
* when it would run past 0, else `middle`. The outermost tick sits on the plot edge, where a long
|
|
809
|
+
* date or currency label would otherwise be clipped by the svg.
|
|
810
|
+
*/
|
|
811
|
+
const tickAnchor = (pos, text, width) => {
|
|
812
|
+
const half = (text.length * CHAR) / 2;
|
|
813
|
+
if (pos + half > width)
|
|
814
|
+
return 'end';
|
|
815
|
+
if (pos - half < 0)
|
|
816
|
+
return 'start';
|
|
817
|
+
return 'middle';
|
|
818
|
+
};
|
|
794
819
|
/** Grid, axis line and tick labels of an axis frame, as shapes. */
|
|
795
820
|
function frameShapes(frame, options = {}) {
|
|
796
821
|
const { plot, xTicks, yTicks, baseline } = frame;
|
|
@@ -825,7 +850,7 @@ function frameShapes(frame, options = {}) {
|
|
|
825
850
|
baseline: 'middle',
|
|
826
851
|
rotate: rot,
|
|
827
852
|
})
|
|
828
|
-
: label(x, y, text, { part, anchor:
|
|
853
|
+
: label(x, y, text, { part, anchor: tickAnchor(x, text, frame.width) });
|
|
829
854
|
// Short marks across an axis line at the ticks.
|
|
830
855
|
const marks = (ticks, along, at, dir, size) => {
|
|
831
856
|
for (const t of ticks)
|
|
@@ -890,7 +915,10 @@ function frameShapes(frame, options = {}) {
|
|
|
890
915
|
const ly = px(top ? y - 8 : y + 20);
|
|
891
916
|
for (const t of a.ticks)
|
|
892
917
|
shapes.push(top
|
|
893
|
-
? label(t.pos, ly, t.label, {
|
|
918
|
+
? label(t.pos, ly, t.label, {
|
|
919
|
+
part: 'x-label-top',
|
|
920
|
+
anchor: tickAnchor(t.pos, t.label, frame.width),
|
|
921
|
+
})
|
|
894
922
|
: rotated(t.pos, ly, t.label, 'x-label'));
|
|
895
923
|
// The first axis keeps its title at the top left; stacked axes title their own label row.
|
|
896
924
|
if (a.label)
|
|
@@ -2343,8 +2371,8 @@ function layoutCartesian(input, defaultType) {
|
|
|
2343
2371
|
return MARGIN$1.left;
|
|
2344
2372
|
const [lo, hi] = extentOf(opts.id);
|
|
2345
2373
|
const f = makeFormatter(opts.format ?? percentFormat ?? input.format);
|
|
2346
|
-
|
|
2347
|
-
|
|
2374
|
+
return (valueAxisRoom([opts.min ?? lo, opts.max ?? hi], f) +
|
|
2375
|
+
(opts.showTicks ? (opts.tickSize ?? 4) : 0));
|
|
2348
2376
|
};
|
|
2349
2377
|
// Horizontal charts run the value axes along the bottom (near) and top (far) edges. Under RTL
|
|
2350
2378
|
// the vertical sides swap so the first axis sits at the right, next to the reader.
|
|
@@ -3988,7 +4016,12 @@ function layoutHeatmap(input) {
|
|
|
3988
4016
|
part: 'cell-label',
|
|
3989
4017
|
anchor: 'middle',
|
|
3990
4018
|
baseline: 'middle',
|
|
3991
|
-
fill:
|
|
4019
|
+
// The cell composites its fill over the surface at the ramp's opacity: a faint cell
|
|
4020
|
+
// is mostly surface and reads with the default text token, a strong one is the fill
|
|
4021
|
+
// and needs the token meant to sit on it. A fixed token fails one end or the other.
|
|
4022
|
+
fill: fill.opacity >= 0.5
|
|
4023
|
+
? 'var(--skdx-color-text-on-accent)'
|
|
4024
|
+
: 'var(--skdx-color-text-default)',
|
|
3992
4025
|
}));
|
|
3993
4026
|
});
|
|
3994
4027
|
});
|
|
@@ -4765,6 +4798,12 @@ function layoutRangeChart(input) {
|
|
|
4765
4798
|
* This file is part of the SkandaDX organization.
|
|
4766
4799
|
* Licensed under the MIT License. See LICENSE in the repository root.
|
|
4767
4800
|
*/
|
|
4801
|
+
/**
|
|
4802
|
+
* Trigonometry rounded to 1e-6: `Math.cos`/`Math.sin` are implementation-defined, so their
|
|
4803
|
+
* last-bit differences between a Node render and a browser render would otherwise reach the
|
|
4804
|
+
* markup and break hydration.
|
|
4805
|
+
*/
|
|
4806
|
+
const q = (v) => Math.round(v * 1e6) / 1e6;
|
|
4768
4807
|
/** Node labels are drawn by default up to this many nodes. */
|
|
4769
4808
|
const NETWORK_LABEL_LIMIT = 60;
|
|
4770
4809
|
/** Per-edge titles and keyboard stops are emitted up to this many edges. */
|
|
@@ -4834,8 +4873,8 @@ function layoutNetworkChart(input) {
|
|
|
4834
4873
|
ys = nodes.map((node) => node.y);
|
|
4835
4874
|
}
|
|
4836
4875
|
else if (mode === 'circular' || mode === 'manual' || n < 3) {
|
|
4837
|
-
xs = nodes.map((_, i) => Math.cos((2 * Math.PI * i) / n - Math.PI / 2));
|
|
4838
|
-
ys = nodes.map((_, i) => Math.sin((2 * Math.PI * i) / n - Math.PI / 2));
|
|
4876
|
+
xs = nodes.map((_, i) => q(Math.cos((2 * Math.PI * i) / n - Math.PI / 2)));
|
|
4877
|
+
ys = nodes.map((_, i) => q(Math.sin((2 * Math.PI * i) / n - Math.PI / 2)));
|
|
4839
4878
|
}
|
|
4840
4879
|
else {
|
|
4841
4880
|
({ xs, ys } = force(n, edges.map((e) => [at.get(e.source), at.get(e.target)]), input.iterations ?? Math.max(50, Math.min(300, Math.floor(30_000 / n)))));
|
|
@@ -4908,7 +4947,7 @@ function layoutNetworkChart(input) {
|
|
|
4908
4947
|
item,
|
|
4909
4948
|
};
|
|
4910
4949
|
}
|
|
4911
|
-
const len = Math.
|
|
4950
|
+
const len = Math.sqrt((b.x - a.x) * (b.x - a.x) + (b.y - a.y) * (b.y - a.y)) || 1;
|
|
4912
4951
|
const ux = (b.x - a.x) / len;
|
|
4913
4952
|
const uy = (b.y - a.y) / len;
|
|
4914
4953
|
if (input.directed) {
|
|
@@ -4999,11 +5038,15 @@ function layoutNetworkChart(input) {
|
|
|
4999
5038
|
}
|
|
5000
5039
|
/**
|
|
5001
5040
|
* Fruchterman–Reingold in the unit square with linear cooling. Deterministic: nodes start on a
|
|
5002
|
-
* circle in input order
|
|
5041
|
+
* circle in input order, the seed is quantised and every step afterwards uses only IEEE-exact
|
|
5042
|
+
* arithmetic (`+ - * /` and `sqrt`), so a server render and a browser render agree bit for bit —
|
|
5043
|
+
* `Math.cos`/`Math.sin`/`Math.hypot` are implementation-defined and their last-bit differences
|
|
5044
|
+
* would otherwise amplify over the iterations into a hydration mismatch.
|
|
5045
|
+
* ponytail: O(n²) per iteration; a Barnes–Hut tree if graphs exceed ~2000 nodes.
|
|
5003
5046
|
*/
|
|
5004
5047
|
function force(n, edges, iterations) {
|
|
5005
|
-
const xs = Array.from({ length: n }, (_, i) => Math.cos((2 * Math.PI * i) / n));
|
|
5006
|
-
const ys = Array.from({ length: n }, (_, i) => Math.sin((2 * Math.PI * i) / n));
|
|
5048
|
+
const xs = Array.from({ length: n }, (_, i) => q(Math.cos((2 * Math.PI * i) / n)));
|
|
5049
|
+
const ys = Array.from({ length: n }, (_, i) => q(Math.sin((2 * Math.PI * i) / n)));
|
|
5007
5050
|
const k = Math.sqrt(4 / n);
|
|
5008
5051
|
const dx = new Array(n).fill(0);
|
|
5009
5052
|
const dy = new Array(n).fill(0);
|
|
@@ -5032,7 +5075,7 @@ function force(n, edges, iterations) {
|
|
|
5032
5075
|
continue;
|
|
5033
5076
|
const ex = xs[a] - xs[b];
|
|
5034
5077
|
const ey = ys[a] - ys[b];
|
|
5035
|
-
const d = Math.
|
|
5078
|
+
const d = Math.sqrt(ex * ex + ey * ey) || 1e-6;
|
|
5036
5079
|
const f = (d * d) / k / d;
|
|
5037
5080
|
dx[a] = dx[a] - ex * f;
|
|
5038
5081
|
dy[a] = dy[a] - ey * f;
|
|
@@ -5040,7 +5083,8 @@ function force(n, edges, iterations) {
|
|
|
5040
5083
|
dy[b] = dy[b] + ey * f;
|
|
5041
5084
|
}
|
|
5042
5085
|
for (let i = 0; i < n; i++) {
|
|
5043
|
-
const d = Math.
|
|
5086
|
+
const d = Math.sqrt(dx[i] * dx[i] + dy[i] * dy[i]) ||
|
|
5087
|
+
1e-6;
|
|
5044
5088
|
const step = Math.min(d, t);
|
|
5045
5089
|
xs[i] = xs[i] + (dx[i] / d) * step;
|
|
5046
5090
|
ys[i] = ys[i] + (dy[i] / d) * step;
|
|
@@ -5917,6 +5961,11 @@ function seriesCsv(series, categories = []) {
|
|
|
5917
5961
|
*/
|
|
5918
5962
|
/** A node's size: its own value, else the sum of its children's. */
|
|
5919
5963
|
const nodeValue = (n) => isFinite(n.value) ? n.value : (n.children ?? []).reduce((s, c) => s + nodeValue(c), 0);
|
|
5964
|
+
/**
|
|
5965
|
+
* Whether this node or anything under it carries a value. A branch in a tree where nobody sets
|
|
5966
|
+
* `value` sums to 0, which is not data and must not be printed as one.
|
|
5967
|
+
*/
|
|
5968
|
+
const hasValue = (n) => isFinite(n.value) || (n.children ?? []).some(hasValue);
|
|
5920
5969
|
/** Levels below the given nodes (0 for none). */
|
|
5921
5970
|
const depthOf = (nodes) => nodes.length === 0 ? 0 : 1 + Math.max(...nodes.map((n) => depthOf(n.children ?? [])));
|
|
5922
5971
|
/** A node's identity: its id, else its name. */
|
|
@@ -6507,19 +6556,12 @@ function layoutCandlestick(input) {
|
|
|
6507
6556
|
const { width, height } = size(input);
|
|
6508
6557
|
const brushRoom = input.brush ? BRUSH_HEIGHT + BRUSH_GAP : 0;
|
|
6509
6558
|
const selector = input.rangeSelector ?? [];
|
|
6510
|
-
const outer = plotBox(width, height, {
|
|
6511
|
-
...MARGIN$1,
|
|
6512
|
-
top: MARGIN$1.top + legendTop(input) + (selector.length ? SELECTOR_HEIGHT : 0),
|
|
6513
|
-
bottom: MARGIN$1.bottom + brushRoom,
|
|
6514
|
-
});
|
|
6515
6559
|
// `compare` plots the change from the first visible close: y values (and the axis format)
|
|
6516
6560
|
// become fractions while tooltips keep the prices and add the percentage.
|
|
6517
6561
|
const base = input.compare ? data[0].close : undefined;
|
|
6518
6562
|
const cmp = (v) => (base !== undefined && base !== 0 ? v / base - 1 : v);
|
|
6519
6563
|
const pctFormat = base !== undefined ? { style: 'percent', digits: 1 } : undefined;
|
|
6520
6564
|
const pct = makeFormatter({ style: 'percent', digits: 1 });
|
|
6521
|
-
const volumeH = hasVolume ? outer.height * VOLUME_SHARE : 0;
|
|
6522
|
-
const plot = { ...outer, height: Math.max(0, outer.height - volumeH - (hasVolume ? 8 : 0)) };
|
|
6523
6565
|
// Moving averages run over every close, so zooming never shortens the window; the visible
|
|
6524
6566
|
// slice is drawn and read in the crosshair.
|
|
6525
6567
|
const closes = all.map((c) => c.close);
|
|
@@ -6535,12 +6577,25 @@ function layoutCandlestick(input) {
|
|
|
6535
6577
|
hidden: isHidden(input, name),
|
|
6536
6578
|
};
|
|
6537
6579
|
});
|
|
6538
|
-
|
|
6580
|
+
// The price extent is known before the plot box, so the left margin can hold the widest tick
|
|
6581
|
+
// label: a currency price such as "$180.00" overflows the default 48.
|
|
6582
|
+
const prices = extent([
|
|
6539
6583
|
...data.flatMap((c) => [cmp(c.low), cmp(c.high)]),
|
|
6540
6584
|
...overlays
|
|
6541
6585
|
.filter((o) => !o.hidden)
|
|
6542
6586
|
.flatMap((o) => o.values.map((v) => (isFinite(v) ? cmp(v) : v))),
|
|
6543
|
-
], false)
|
|
6587
|
+
], false);
|
|
6588
|
+
const outer = plotBox(width, height, {
|
|
6589
|
+
...MARGIN$1,
|
|
6590
|
+
top: MARGIN$1.top + legendTop(input) + (selector.length ? SELECTOR_HEIGHT : 0),
|
|
6591
|
+
bottom: MARGIN$1.bottom + brushRoom,
|
|
6592
|
+
left: input.scale === 'log'
|
|
6593
|
+
? MARGIN$1.left
|
|
6594
|
+
: valueAxisRoom(prices, makeFormatter(pctFormat ?? input.format)),
|
|
6595
|
+
});
|
|
6596
|
+
const volumeH = hasVolume ? outer.height * VOLUME_SHARE : 0;
|
|
6597
|
+
const plot = { ...outer, height: Math.max(0, outer.height - volumeH - (hasVolume ? 8 : 0)) };
|
|
6598
|
+
const y = axis(prices, [plot.y + plot.height, plot.y], { scale: input.scale === 'log' ? 'log' : 'linear', format: pctFormat }, input.format);
|
|
6544
6599
|
// A time axis places candles by their label's time, so missing sessions leave gaps; the bar
|
|
6545
6600
|
// width is 80% of the smallest gap. Otherwise every candle takes an equal slot.
|
|
6546
6601
|
const times = data.map((c) => categoryValue(c.label));
|
|
@@ -7167,7 +7222,10 @@ function layoutGantt(input) {
|
|
|
7167
7222
|
y2: plot.y + plot.height,
|
|
7168
7223
|
stroke: CHART_COLORS.grid,
|
|
7169
7224
|
});
|
|
7170
|
-
shapes.push(label(t.pos, plot.y + plot.height + 20, t.label, {
|
|
7225
|
+
shapes.push(label(t.pos, plot.y + plot.height + 20, t.label, {
|
|
7226
|
+
part: 'x-label',
|
|
7227
|
+
anchor: tickAnchor(t.pos, t.label, width),
|
|
7228
|
+
}));
|
|
7171
7229
|
}
|
|
7172
7230
|
rowNames.forEach((name, i) => {
|
|
7173
7231
|
const owner = visible.find((t) => (t.group ?? t.name) === name);
|
|
@@ -7850,7 +7908,11 @@ function layoutChord(input) {
|
|
|
7850
7908
|
const top = legendTop(input);
|
|
7851
7909
|
const cx = width / 2;
|
|
7852
7910
|
const cy = top + (height - top) / 2;
|
|
7853
|
-
|
|
7911
|
+
// Group names sit outside the rim, so the circle keeps room for the widest one on either side;
|
|
7912
|
+
// without it the left-hand label runs off the svg.
|
|
7913
|
+
const nameGap = input.ticks ? 8 + TICK + 14 : 8;
|
|
7914
|
+
const nameRoom = nameGap + Math.max(0, ...given.map((s) => s.length)) * CHAR + 4;
|
|
7915
|
+
const r = Math.max(0, Math.min(width / 2 - nameRoom, (height - top) / 2 - 36));
|
|
7854
7916
|
const r0 = Math.max(0, r - RING);
|
|
7855
7917
|
const PAD = ((input.padding ?? 2.3) * Math.PI) / 180;
|
|
7856
7918
|
// Groups keep their input order (and color) but can be arranged by total flow.
|
|
@@ -7943,7 +8005,7 @@ function layoutChord(input) {
|
|
|
7943
8005
|
const mid = (g.a0 + g.a1) / 2;
|
|
7944
8006
|
const sin = Math.sin(mid);
|
|
7945
8007
|
// Ticks sit on the rim; the group name moves out past their labels.
|
|
7946
|
-
const nameR = r +
|
|
8008
|
+
const nameR = r + nameGap;
|
|
7947
8009
|
shapes.push(label(px(cx + nameR * sin), px(cy - nameR * Math.cos(mid)), names[i], {
|
|
7948
8010
|
anchor: sin > 0.1 ? 'start' : sin < -0.1 ? 'end' : 'middle',
|
|
7949
8011
|
baseline: 'middle',
|
|
@@ -8512,6 +8574,10 @@ function layoutDumbbellChart(input) {
|
|
|
8512
8574
|
const vertical = input.orientation === 'vertical';
|
|
8513
8575
|
const startLabel = input.startLabel ?? 'Start';
|
|
8514
8576
|
const endLabel = input.endLabel ?? 'End';
|
|
8577
|
+
// The legend toggles the two ends like any other series: a hidden end drops its markers, its
|
|
8578
|
+
// value labels and the connector that would dangle, and leaves the value axis to the rest.
|
|
8579
|
+
const hideStart = isHidden(input, startLabel);
|
|
8580
|
+
const hideEnd = isHidden(input, endLabel);
|
|
8515
8581
|
const sorted = [...data];
|
|
8516
8582
|
if (input.sort) {
|
|
8517
8583
|
const key = (d) => input.sort === 'change'
|
|
@@ -8529,7 +8595,7 @@ function layoutDumbbellChart(input) {
|
|
|
8529
8595
|
bottom: MARGIN$1.bottom + (!vertical && input.xAxis?.label ? 14 : 0),
|
|
8530
8596
|
left: vertical ? MARGIN$1.left : Math.max(MARGIN$1.left, widest + 16),
|
|
8531
8597
|
});
|
|
8532
|
-
const value = axis(extent(sorted.flatMap((d) => [d.start, d.end]), false), vertical ? [plot.y + plot.height, plot.y] : [plot.x, plot.x + plot.width], input.xAxis ?? {}, input.format);
|
|
8598
|
+
const value = axis(extent(sorted.flatMap((d) => [hideStart ? undefined : d.start, hideEnd ? undefined : d.end]), false), vertical ? [plot.y + plot.height, plot.y] : [plot.x, plot.x + plot.width], input.xAxis ?? {}, input.format);
|
|
8533
8599
|
const band = bandScale(sorted.length, vertical ? [plot.x, plot.x + plot.width] : [plot.y, plot.y + plot.height]);
|
|
8534
8600
|
const startColor = colorOf(input, 0);
|
|
8535
8601
|
const endColor = colorOf(input, 1);
|
|
@@ -8575,7 +8641,7 @@ function layoutDumbbellChart(input) {
|
|
|
8575
8641
|
index: i,
|
|
8576
8642
|
value: isFinite(d.end) ? d.end : undefined,
|
|
8577
8643
|
};
|
|
8578
|
-
if (isFinite(d.start) && isFinite(d.end)) {
|
|
8644
|
+
if (!hideStart && !hideEnd && isFinite(d.start) && isFinite(d.end)) {
|
|
8579
8645
|
const a = at(d.start, mid);
|
|
8580
8646
|
const b = at(d.end, mid);
|
|
8581
8647
|
connectors.push({
|
|
@@ -8591,11 +8657,11 @@ function layoutDumbbellChart(input) {
|
|
|
8591
8657
|
item,
|
|
8592
8658
|
});
|
|
8593
8659
|
}
|
|
8594
|
-
for (const [v, part, name, fill] of [
|
|
8595
|
-
[d.start, 'start', startLabel, startColor],
|
|
8596
|
-
[d.end, 'end', endLabel, endColor],
|
|
8660
|
+
for (const [v, part, name, fill, hidden] of [
|
|
8661
|
+
[d.start, 'start', startLabel, startColor, hideStart],
|
|
8662
|
+
[d.end, 'end', endLabel, endColor, hideEnd],
|
|
8597
8663
|
]) {
|
|
8598
|
-
if (!isFinite(v))
|
|
8664
|
+
if (hidden || !isFinite(v))
|
|
8599
8665
|
continue;
|
|
8600
8666
|
const p = at(v, mid);
|
|
8601
8667
|
const text = `${d.name} · ${name}: ${format(v)}`;
|
|
@@ -8801,7 +8867,7 @@ function layoutTreeChart(input) {
|
|
|
8801
8867
|
});
|
|
8802
8868
|
}
|
|
8803
8869
|
const value = nodeValue(p.node);
|
|
8804
|
-
const title = `${p.node.name}${p.node.subtitle ? ` · ${p.node.subtitle}` : ''}${
|
|
8870
|
+
const title = `${p.node.name}${p.node.subtitle ? ` · ${p.node.subtitle}` : ''}${hasValue(p.node) ? `: ${format(value)}` : ''}${p.collapsed ? ' (collapsed)' : ''}`;
|
|
8805
8871
|
const item = { label: title, id: nodeId(p.node), name: p.node.name, value };
|
|
8806
8872
|
if (box) {
|
|
8807
8873
|
marks.push({
|
|
@@ -8942,7 +9008,11 @@ function packCircles(radii, padding) {
|
|
|
8942
9008
|
function layoutCirclePacking(input) {
|
|
8943
9009
|
const issues = [];
|
|
8944
9010
|
const { nodes: roots } = drill(input.data, input.rootId);
|
|
8945
|
-
|
|
9011
|
+
// The legend lists every top-level branch, hidden ones included, so a hide can be undone; and
|
|
9012
|
+
// a branch keeps the colour of its place in that full list whatever is hidden.
|
|
9013
|
+
const branches = cutDepth(roots, input.maxDepth);
|
|
9014
|
+
const branchColor = new Map(branches.map((n, i) => [nodeId(n), colorOf(input, i, n.color)]));
|
|
9015
|
+
const nodes = branches.filter((n) => !isHidden(input, n.name));
|
|
8946
9016
|
const state = stateCanvas(input, !nodes.some((n) => nodeValue(n) > 0), 300, 300);
|
|
8947
9017
|
if (state)
|
|
8948
9018
|
return { ...state, issues };
|
|
@@ -8987,7 +9057,7 @@ function layoutCirclePacking(input) {
|
|
|
8987
9057
|
const r = Math.max(0, c.r * k - pad / 2);
|
|
8988
9058
|
const value = nodeValue(n);
|
|
8989
9059
|
const branch = (n.children?.length ?? 0) > 0;
|
|
8990
|
-
const color = n.color ?? inherited ?? colorOf(input, i);
|
|
9060
|
+
const color = n.color ?? inherited ?? branchColor.get(nodeId(n)) ?? colorOf(input, i);
|
|
8991
9061
|
const fill = branch ? color : input.colorScale ? (colors.color(value)?.fill ?? color) : color;
|
|
8992
9062
|
const title = `${n.name}: ${format(value)}`;
|
|
8993
9063
|
shapes.push({
|
|
@@ -9025,7 +9095,7 @@ function layoutCirclePacking(input) {
|
|
|
9025
9095
|
};
|
|
9026
9096
|
const radius = Math.max(0, Math.min(width, height - top) / 2 - 8);
|
|
9027
9097
|
pack(nodes, width / 2, top + (height - top) / 2, radius, undefined);
|
|
9028
|
-
const c = canvas(input, width, height,
|
|
9098
|
+
const c = canvas(input, width, height, branches.map((n) => ({ name: n.name, color: branchColor.get(nodeId(n)) })));
|
|
9029
9099
|
return { ...c, shapes: [...shapes, ...texts, ...c.shapes], issues };
|
|
9030
9100
|
}
|
|
9031
9101
|
|
|
@@ -9540,7 +9610,23 @@ function layoutWaffleChart(input) {
|
|
|
9540
9610
|
const share = (v) => (total > 0 ? v / total : 0);
|
|
9541
9611
|
const counts = waffleCounts(data.map((d) => (isHidden(input, d.name) ? 0 : Math.min(1, share(d.value)) * cells)));
|
|
9542
9612
|
const { width, height } = size(input, 300, 300);
|
|
9543
|
-
|
|
9613
|
+
// `labels` appends the share to each legend entry; the entry still toggles by the datum's name.
|
|
9614
|
+
const shares = new Map(data.map((d) => [d.name, `${Math.round(share(d.value) * 100)}%`]));
|
|
9615
|
+
const legend = legendOptions(input);
|
|
9616
|
+
const legendItems = input.data.map((d, i) => ({
|
|
9617
|
+
name: d.name,
|
|
9618
|
+
color: colorOf(input, i, d.color),
|
|
9619
|
+
}));
|
|
9620
|
+
const legendInput = input.labels && legend
|
|
9621
|
+
? {
|
|
9622
|
+
...input,
|
|
9623
|
+
legend: {
|
|
9624
|
+
...legend,
|
|
9625
|
+
formatter: (n) => `${legend.formatter?.(n) ?? n} ${shares.get(n) ?? ''}`,
|
|
9626
|
+
},
|
|
9627
|
+
}
|
|
9628
|
+
: input;
|
|
9629
|
+
const top = legendTop(legendInput, legendItems, width);
|
|
9544
9630
|
const gap = Math.max(0, input.cellGap ?? 2);
|
|
9545
9631
|
const cell = Math.max(1, Math.min((width - 2 * PAD) / columns, (height - top - 2 * PAD) / rows));
|
|
9546
9632
|
const x0 = (width - cell * columns) / 2;
|
|
@@ -9579,18 +9665,7 @@ function layoutWaffleChart(input) {
|
|
|
9579
9665
|
const restTitle = rest > 0 ? `Other: ${format(rest)} (${Math.round(share(rest) * 100)}%)` : 'Empty';
|
|
9580
9666
|
for (; k < cells; k++)
|
|
9581
9667
|
shapes.push(rect(k, CHART_COLORS.grid, restTitle, undefined, 0.6));
|
|
9582
|
-
|
|
9583
|
-
const shares = new Map(data.map((d) => [d.name, `${Math.round(share(d.value) * 100)}%`]));
|
|
9584
|
-
const legend = legendOptions(input);
|
|
9585
|
-
const c = canvas(input.labels && legend
|
|
9586
|
-
? {
|
|
9587
|
-
...input,
|
|
9588
|
-
legend: {
|
|
9589
|
-
...legend,
|
|
9590
|
-
formatter: (n) => `${legend.formatter?.(n) ?? n} ${shares.get(n) ?? ''}`,
|
|
9591
|
-
},
|
|
9592
|
-
}
|
|
9593
|
-
: input, width, height, input.data.map((d, i) => ({ name: d.name, color: colorOf(input, i, d.color) })));
|
|
9668
|
+
const c = canvas(legendInput, width, height, legendItems);
|
|
9594
9669
|
return { ...c, shapes: [...shapes, ...c.shapes], issues };
|
|
9595
9670
|
}
|
|
9596
9671
|
|