@dou-ui/chart 0.0.0-stage → 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +10 -2
- package/dist/chart.js +6 -0
- package/dist/chart.js.map +1 -0
- package/dist/chart.vue_vue_type_script_setup_true_lang.js +4245 -0
- package/dist/chart.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/area.js +6 -0
- package/dist/components/area.js.map +1 -0
- package/dist/components/area.vue_vue_type_script_setup_true_lang.js +55 -0
- package/dist/components/area.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/axis.js +6 -0
- package/dist/components/axis.js.map +1 -0
- package/dist/components/axis.vue_vue_type_script_setup_true_lang.js +46 -0
- package/dist/components/axis.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/bar.js +6 -0
- package/dist/components/bar.js.map +1 -0
- package/dist/components/bar.vue_vue_type_script_setup_true_lang.js +46 -0
- package/dist/components/bar.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/boxplot.js +6 -0
- package/dist/components/boxplot.js.map +1 -0
- package/dist/components/boxplot.vue_vue_type_script_setup_true_lang.js +42 -0
- package/dist/components/boxplot.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/brush.js +6 -0
- package/dist/components/brush.js.map +1 -0
- package/dist/components/brush.vue_vue_type_script_setup_true_lang.js +42 -0
- package/dist/components/brush.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/funnel.js +6 -0
- package/dist/components/funnel.js.map +1 -0
- package/dist/components/funnel.vue_vue_type_script_setup_true_lang.js +41 -0
- package/dist/components/funnel.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/gauge.js +6 -0
- package/dist/components/gauge.js.map +1 -0
- package/dist/components/gauge.vue_vue_type_script_setup_true_lang.js +91 -0
- package/dist/components/gauge.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/heatmap.js +6 -0
- package/dist/components/heatmap.js.map +1 -0
- package/dist/components/heatmap.vue_vue_type_script_setup_true_lang.js +45 -0
- package/dist/components/heatmap.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/histogram.js +6 -0
- package/dist/components/histogram.js.map +1 -0
- package/dist/components/histogram.vue_vue_type_script_setup_true_lang.js +61 -0
- package/dist/components/histogram.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/line.js +6 -0
- package/dist/components/line.js.map +1 -0
- package/dist/components/line.vue_vue_type_script_setup_true_lang.js +52 -0
- package/dist/components/line.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/marker.js +6 -0
- package/dist/components/marker.js.map +1 -0
- package/dist/components/marker.vue_vue_type_script_setup_true_lang.js +31 -0
- package/dist/components/marker.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/pie.js +6 -0
- package/dist/components/pie.js.map +1 -0
- package/dist/components/pie.vue_vue_type_script_setup_true_lang.js +42 -0
- package/dist/components/pie.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/radar.js +6 -0
- package/dist/components/radar.js.map +1 -0
- package/dist/components/radar.vue_vue_type_script_setup_true_lang.js +45 -0
- package/dist/components/radar.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/range.js +6 -0
- package/dist/components/range.js.map +1 -0
- package/dist/components/range.vue_vue_type_script_setup_true_lang.js +27 -0
- package/dist/components/range.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/scatter.js +6 -0
- package/dist/components/scatter.js.map +1 -0
- package/dist/components/scatter.vue_vue_type_script_setup_true_lang.js +45 -0
- package/dist/components/scatter.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/sunburst.js +6 -0
- package/dist/components/sunburst.js.map +1 -0
- package/dist/components/sunburst.vue_vue_type_script_setup_true_lang.js +36 -0
- package/dist/components/sunburst.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/tooltip.js +6 -0
- package/dist/components/tooltip.js.map +1 -0
- package/dist/components/tooltip.vue_vue_type_script_setup_true_lang.js +31 -0
- package/dist/components/tooltip.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/treemap.js +6 -0
- package/dist/components/treemap.js.map +1 -0
- package/dist/components/treemap.vue_vue_type_script_setup_true_lang.js +33 -0
- package/dist/components/treemap.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/waterfall.js +6 -0
- package/dist/components/waterfall.js.map +1 -0
- package/dist/components/waterfall.vue_vue_type_script_setup_true_lang.js +49 -0
- package/dist/components/waterfall.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/zoom.js +6 -0
- package/dist/components/zoom.js.map +1 -0
- package/dist/components/zoom.vue_vue_type_script_setup_true_lang.js +37 -0
- package/dist/components/zoom.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/context.js +30 -0
- package/dist/context.js.map +1 -0
- package/dist/core/bar.js +223 -0
- package/dist/core/bar.js.map +1 -0
- package/dist/core/box.js +150 -0
- package/dist/core/box.js.map +1 -0
- package/dist/core/colors.js +58 -0
- package/dist/core/colors.js.map +1 -0
- package/dist/core/data.js +257 -0
- package/dist/core/data.js.map +1 -0
- package/dist/core/decimate.js +211 -0
- package/dist/core/decimate.js.map +1 -0
- package/dist/core/format.js +151 -0
- package/dist/core/format.js.map +1 -0
- package/dist/core/funnel.js +68 -0
- package/dist/core/funnel.js.map +1 -0
- package/dist/core/grid.js +87 -0
- package/dist/core/grid.js.map +1 -0
- package/dist/core/heatmap.js +187 -0
- package/dist/core/heatmap.js.map +1 -0
- package/dist/core/histogram.js +81 -0
- package/dist/core/histogram.js.map +1 -0
- package/dist/core/hit.js +115 -0
- package/dist/core/hit.js.map +1 -0
- package/dist/core/labels.js +141 -0
- package/dist/core/labels.js.map +1 -0
- package/dist/core/layout.js +1002 -0
- package/dist/core/layout.js.map +1 -0
- package/dist/core/line.js +230 -0
- package/dist/core/line.js.map +1 -0
- package/dist/core/marker.js +205 -0
- package/dist/core/marker.js.map +1 -0
- package/dist/core/model.js +391 -0
- package/dist/core/model.js.map +1 -0
- package/dist/core/painter.js +79 -0
- package/dist/core/painter.js.map +1 -0
- package/dist/core/pattern.js +102 -0
- package/dist/core/pattern.js.map +1 -0
- package/dist/core/pie.js +119 -0
- package/dist/core/pie.js.map +1 -0
- package/dist/core/radar.js +83 -0
- package/dist/core/radar.js.map +1 -0
- package/dist/core/scale.js +149 -0
- package/dist/core/scale.js.map +1 -0
- package/dist/core/scatter.js +263 -0
- package/dist/core/scatter.js.map +1 -0
- package/dist/core/svg.js +209 -0
- package/dist/core/svg.js.map +1 -0
- package/dist/core/sync.js +21 -0
- package/dist/core/sync.js.map +1 -0
- package/dist/core/time.js +217 -0
- package/dist/core/time.js.map +1 -0
- package/dist/core/tree.js +348 -0
- package/dist/core/tree.js.map +1 -0
- package/dist/core/warn.js +11 -0
- package/dist/core/warn.js.map +1 -0
- package/dist/core/waterfall.js +134 -0
- package/dist/core/waterfall.js.map +1 -0
- package/dist/core/window.js +94 -0
- package/dist/core/window.js.map +1 -0
- package/dist/define.js +62 -0
- package/dist/define.js.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/locale.js +170 -0
- package/dist/locale.js.map +1 -0
- package/dist/overlay.js +6 -0
- package/dist/overlay.js.map +1 -0
- package/dist/overlay.vue_vue_type_script_setup_true_lang.js +207 -0
- package/dist/overlay.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/pointer.js +6 -0
- package/dist/pointer.js.map +1 -0
- package/dist/pointer.vue_vue_type_script_setup_true_lang.js +136 -0
- package/dist/pointer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/static.js +6 -0
- package/dist/static.js.map +1 -0
- package/dist/static.vue_vue_type_script_setup_true_lang.js +34 -0
- package/dist/static.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/style.css +1 -0
- package/dist/style.d.ts +1 -0
- package/dist/style.js +3 -0
- package/dist/table.js +6 -0
- package/dist/table.js.map +1 -0
- package/dist/table.vue_vue_type_script_setup_true_lang.js +52 -0
- package/dist/table.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/types/chart.vue.d.ts +24 -0
- package/dist/types/components/area.vue.d.ts +16 -0
- package/dist/types/components/axis.vue.d.ts +8 -0
- package/dist/types/components/bar.vue.d.ts +14 -0
- package/dist/types/components/boxplot.vue.d.ts +14 -0
- package/dist/types/components/brush.vue.d.ts +22 -0
- package/dist/types/components/funnel.vue.d.ts +14 -0
- package/dist/types/components/gauge.vue.d.ts +17 -0
- package/dist/types/components/heatmap.vue.d.ts +14 -0
- package/dist/types/components/histogram.vue.d.ts +14 -0
- package/dist/types/components/line.vue.d.ts +16 -0
- package/dist/types/components/marker.vue.d.ts +6 -0
- package/dist/types/components/pie.vue.d.ts +23 -0
- package/dist/types/components/radar.vue.d.ts +16 -0
- package/dist/types/components/range.vue.d.ts +12 -0
- package/dist/types/components/scatter.vue.d.ts +14 -0
- package/dist/types/components/sunburst.vue.d.ts +17 -0
- package/dist/types/components/tooltip.vue.d.ts +13 -0
- package/dist/types/components/treemap.vue.d.ts +8 -0
- package/dist/types/components/waterfall.vue.d.ts +16 -0
- package/dist/types/components/zoom.vue.d.ts +18 -0
- package/dist/types/context.d.ts +87 -0
- package/dist/types/core/bar.d.ts +38 -0
- package/dist/types/core/box.d.ts +25 -0
- package/dist/types/core/colors.d.ts +16 -0
- package/dist/types/core/data.d.ts +72 -0
- package/dist/types/core/decimate.d.ts +39 -0
- package/dist/types/core/format.d.ts +33 -0
- package/dist/types/core/funnel.d.ts +15 -0
- package/dist/types/core/grid.d.ts +18 -0
- package/dist/types/core/heatmap.d.ts +17 -0
- package/dist/types/core/histogram.d.ts +23 -0
- package/dist/types/core/hit.d.ts +31 -0
- package/dist/types/core/labels.d.ts +45 -0
- package/dist/types/core/layout.d.ts +259 -0
- package/dist/types/core/line.d.ts +36 -0
- package/dist/types/core/marker.d.ts +67 -0
- package/dist/types/core/model.d.ts +86 -0
- package/dist/types/core/painter.d.ts +34 -0
- package/dist/types/core/pattern.d.ts +14 -0
- package/dist/types/core/pie.d.ts +66 -0
- package/dist/types/core/radar.d.ts +12 -0
- package/dist/types/core/scale.d.ts +60 -0
- package/dist/types/core/scatter.d.ts +16 -0
- package/dist/types/core/series.d.ts +228 -0
- package/dist/types/core/svg.d.ts +63 -0
- package/dist/types/core/sync.d.ts +8 -0
- package/dist/types/core/time.d.ts +20 -0
- package/dist/types/core/tree.d.ts +84 -0
- package/dist/types/core/warn.d.ts +4 -0
- package/dist/types/core/waterfall.d.ts +24 -0
- package/dist/types/core/window.d.ts +36 -0
- package/dist/types/define.d.ts +115 -0
- package/dist/types/index.d.ts +363 -0
- package/dist/types/locale.d.ts +100 -0
- package/dist/types/overlay.vue.d.ts +95 -0
- package/dist/types/pointer.vue.d.ts +53 -0
- package/dist/types/static.vue.d.ts +24 -0
- package/dist/types/table.vue.d.ts +19 -0
- package/dist/types/types.d.ts +555 -0
- package/dist/types/zoom-layer.vue.d.ts +41 -0
- package/dist/zoom-layer.js +6 -0
- package/dist/zoom-layer.js.map +1 -0
- package/dist/zoom-layer.vue_vue_type_script_setup_true_lang.js +210 -0
- package/dist/zoom-layer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/package.json +53 -4
|
@@ -0,0 +1,1002 @@
|
|
|
1
|
+
import { categoryScale, categoryTicks, linearDomain, linearScale, linearTicks, logDomain, logScale, logTicks, nextNiceStep, niceStep, stepDecimals } from "./scale.js";
|
|
2
|
+
import { toTime } from "./data.js";
|
|
3
|
+
import { timeInterval, timeTicks } from "./time.js";
|
|
4
|
+
import { numberTickFormat, timeTickFormat } from "./format.js";
|
|
5
|
+
import { unitsOf } from "./window.js";
|
|
6
|
+
// src/core/layout.ts
|
|
7
|
+
/** `range` as the axis runs: turned round when it is reversed. */
|
|
8
|
+
var runs = (config, range) => config?.reverse ? [range[1], range[0]] : range;
|
|
9
|
+
var nameText = (name) => typeof name === "string" ? name : name.text;
|
|
10
|
+
var swatchOf = (name) => typeof name === "string" ? SWATCH : name.swatch;
|
|
11
|
+
/** A legend entry: the swatch, the room after it, and the room to the next entry. */
|
|
12
|
+
var SWATCH = 12;
|
|
13
|
+
var SWATCH_GAP = 6;
|
|
14
|
+
var LEGEND_GAP = 16;
|
|
15
|
+
var LEGEND_ROW_GAP = 4;
|
|
16
|
+
/** Rows 40px apart on a touch screen: each entry's hit area reaches 12px past its text, above and below. */
|
|
17
|
+
var LEGEND_ROW_GAP_COARSE = 24;
|
|
18
|
+
/** The most of the chart's width a legend down the side may take. */
|
|
19
|
+
var LEGEND_SIDE_SHARE = .3;
|
|
20
|
+
var STRIP_GAP = 12;
|
|
21
|
+
/** Wanted room between ticks, in px. */
|
|
22
|
+
var X_TICK_SPACE = 64;
|
|
23
|
+
var Y_TICK_SPACE = 48;
|
|
24
|
+
/** The most of the chart's width that category names down the side may take. */
|
|
25
|
+
var SIDE_LABEL_SHARE = .4;
|
|
26
|
+
var asNumber = (value, kind) => value === void 0 ? void 0 : kind === "time" ? toTime(value) : Number(value);
|
|
27
|
+
var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
28
|
+
/** Widens `[lo, hi]` so `inset` px stay free at both ends of an axis `length` px long. */
|
|
29
|
+
function widen(lo, hi, inset, length) {
|
|
30
|
+
if (!(inset > 0) || !(hi > lo)) return [lo, hi];
|
|
31
|
+
const pad = (hi - lo) * inset / Math.max(1, length - inset * 2);
|
|
32
|
+
return [lo - pad, hi + pad];
|
|
33
|
+
}
|
|
34
|
+
/** Places the axes and works out the plot area, the scales and the tick labels. */
|
|
35
|
+
function layoutChart(options) {
|
|
36
|
+
const { width, height, measure } = options;
|
|
37
|
+
const placed = placeLegendIn(options.legend ?? [], options.legendPlacement ?? "bottom", width, height, measure);
|
|
38
|
+
const { legend, box: legendBox } = placed;
|
|
39
|
+
const { bottom: legendHeight, top: legendTop, right: legendRight } = placed.room;
|
|
40
|
+
const room = options.strip ? 44 : 0;
|
|
41
|
+
if (options.horizontal) {
|
|
42
|
+
const inner = width - room - legendRight;
|
|
43
|
+
const layout = layoutAcross({
|
|
44
|
+
...options,
|
|
45
|
+
width: inner
|
|
46
|
+
}, legendHeight, legendTop);
|
|
47
|
+
const { plot } = layout;
|
|
48
|
+
const strip = room ? {
|
|
49
|
+
x: inner + STRIP_GAP,
|
|
50
|
+
y: plot.y,
|
|
51
|
+
width: 32,
|
|
52
|
+
height: plot.height
|
|
53
|
+
} : void 0;
|
|
54
|
+
return {
|
|
55
|
+
...layout,
|
|
56
|
+
width,
|
|
57
|
+
legend,
|
|
58
|
+
legendBox,
|
|
59
|
+
strip
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const layout = layoutUp({
|
|
63
|
+
...options,
|
|
64
|
+
width: width - legendRight
|
|
65
|
+
}, legendHeight + room, legendTop);
|
|
66
|
+
const { plot } = layout;
|
|
67
|
+
const strip = room ? {
|
|
68
|
+
x: plot.x,
|
|
69
|
+
y: height - legendHeight - 32,
|
|
70
|
+
width: plot.width,
|
|
71
|
+
height: 32
|
|
72
|
+
} : void 0;
|
|
73
|
+
return {
|
|
74
|
+
...layout,
|
|
75
|
+
width,
|
|
76
|
+
legend,
|
|
77
|
+
legendBox,
|
|
78
|
+
strip
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Places a legend's entries where it goes: in rows under or above the
|
|
83
|
+
* chart, or one under another down its right side. Gives the room it
|
|
84
|
+
* takes from the chart on each side, and the box it shows in: past three
|
|
85
|
+
* rows (or the chart's height, down the side), its entries scroll.
|
|
86
|
+
*/
|
|
87
|
+
function placeLegendIn(names, at, width, height, measure, coarse = false) {
|
|
88
|
+
const rowGap = coarse ? LEGEND_ROW_GAP_COARSE : LEGEND_ROW_GAP;
|
|
89
|
+
const room = {
|
|
90
|
+
top: 0,
|
|
91
|
+
bottom: 0,
|
|
92
|
+
right: 0
|
|
93
|
+
};
|
|
94
|
+
if (!names.length) return {
|
|
95
|
+
legend: [],
|
|
96
|
+
room
|
|
97
|
+
};
|
|
98
|
+
if (at === "right") {
|
|
99
|
+
const widest = Math.max(42, Math.floor(width * LEGEND_SIDE_SHARE) - LEGEND_GAP);
|
|
100
|
+
const step = 16 + Math.max(rowGap, 8);
|
|
101
|
+
const legend = names.map((name, index) => {
|
|
102
|
+
const text = nameText(name);
|
|
103
|
+
const full = swatchOf(name) + SWATCH_GAP + measure(text);
|
|
104
|
+
const item = {
|
|
105
|
+
index,
|
|
106
|
+
text,
|
|
107
|
+
left: 0,
|
|
108
|
+
top: index * step,
|
|
109
|
+
width: Math.min(full, widest)
|
|
110
|
+
};
|
|
111
|
+
if (full > widest) item.clipped = true;
|
|
112
|
+
if (typeof name !== "string") item.swatch = name.swatch;
|
|
113
|
+
return item;
|
|
114
|
+
});
|
|
115
|
+
const boxWidth = Math.max(...legend.map((item) => item.width));
|
|
116
|
+
const content = (legend.length - 1) * step + 16;
|
|
117
|
+
const boxHeight = Math.min(content, height - 16);
|
|
118
|
+
const box = {
|
|
119
|
+
x: width - 4 - boxWidth,
|
|
120
|
+
y: 8,
|
|
121
|
+
width: boxWidth,
|
|
122
|
+
height: boxHeight,
|
|
123
|
+
scroll: content > boxHeight + .5
|
|
124
|
+
};
|
|
125
|
+
for (const item of legend) {
|
|
126
|
+
item.left += box.x;
|
|
127
|
+
item.top += box.y;
|
|
128
|
+
}
|
|
129
|
+
room.right = boxWidth + LEGEND_GAP;
|
|
130
|
+
return {
|
|
131
|
+
legend,
|
|
132
|
+
box,
|
|
133
|
+
room
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const legend = placeLegend(names, width, measure, coarse);
|
|
137
|
+
const rowStep = 16 + rowGap;
|
|
138
|
+
const content = legend[legend.length - 1].top + 16;
|
|
139
|
+
const boxHeight = Math.min(content, 2 * rowStep + 16);
|
|
140
|
+
const gap = 12;
|
|
141
|
+
const y = at === "top" ? 4 : height - boxHeight;
|
|
142
|
+
for (const item of legend) item.top += y;
|
|
143
|
+
const box = {
|
|
144
|
+
x: 0,
|
|
145
|
+
y,
|
|
146
|
+
width,
|
|
147
|
+
height: boxHeight,
|
|
148
|
+
scroll: content > boxHeight + .5
|
|
149
|
+
};
|
|
150
|
+
if (at === "top") room.top = 4 + boxHeight + gap - 8;
|
|
151
|
+
else room.bottom = boxHeight + gap;
|
|
152
|
+
return {
|
|
153
|
+
legend,
|
|
154
|
+
box,
|
|
155
|
+
room
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
var shown = (config) => !!config && !config.hidden;
|
|
159
|
+
/**
|
|
160
|
+
* The value axes for about `count` ticks; the second one gets as many
|
|
161
|
+
* steps as the first, so both share one set of grid lines.
|
|
162
|
+
*/
|
|
163
|
+
function valueAxes(axes, model, count, locale, length, zoom, across = false, most = Infinity) {
|
|
164
|
+
let y = axes.y ? valueAxis("y", axes.y, model, count, locale, length, across, most) : null;
|
|
165
|
+
let y2 = axes.y2 ? valueAxis("y2", axes.y2, model, count, locale, length, across, most) : null;
|
|
166
|
+
if (y && y2 && !axes.y2.ticks && !y2.fixed && y.ticks.length > 1) {
|
|
167
|
+
const both = alignZero(y, y2, axes.y, axes.y2, model, locale, length);
|
|
168
|
+
if (both) ({y, y2} = both);
|
|
169
|
+
else y2 = alignTo(y2, y.ticks.length - 1, axes.y2, model, locale, length);
|
|
170
|
+
}
|
|
171
|
+
const primary = y ?? y2;
|
|
172
|
+
if (!zoom || !primary) return {
|
|
173
|
+
y,
|
|
174
|
+
y2
|
|
175
|
+
};
|
|
176
|
+
const units = unitsOf({ kind: primary.kind });
|
|
177
|
+
const [d0, d1] = primary.domain.map(units.to);
|
|
178
|
+
const part = [(units.to(zoom[0]) - d0) / (d1 - d0 || 1), (units.to(zoom[1]) - d0) / (d1 - d0 || 1)];
|
|
179
|
+
const zoomed = (axis, config) => axis && config ? zoomAxis(axis, part, count, config, locale, most) : null;
|
|
180
|
+
return {
|
|
181
|
+
y: zoomed(y, axes.y),
|
|
182
|
+
y2: zoomed(y2, axes.y2)
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/** `axis` zoomed in to `part` of it (0 to 1 from its start), with ticks of its own. */
|
|
186
|
+
function zoomAxis(axis, part, count, config, locale, most = Infinity) {
|
|
187
|
+
const units = unitsOf({ kind: axis.kind });
|
|
188
|
+
const [d0, d1] = axis.domain.map(units.to);
|
|
189
|
+
const domain = [units.from(d0 + (d1 - d0) * part[0]), units.from(d0 + (d1 - d0) * part[1])];
|
|
190
|
+
count = tickCount(config, count, most);
|
|
191
|
+
const given = explicitTicks(config, axis.kind);
|
|
192
|
+
const ticks = given ? given.filter((v) => v >= domain[0] && v <= domain[1]) : axis.kind === "log" ? logTicks(domain[0], domain[1], count) : linearTicks(domain[0], domain[1], count);
|
|
193
|
+
return {
|
|
194
|
+
...axis,
|
|
195
|
+
full: axis.domain,
|
|
196
|
+
domain,
|
|
197
|
+
ticks,
|
|
198
|
+
texts: valueTexts(ticks, config, locale)
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/** The data span an axis has to show, with 0 when it starts at 0. */
|
|
202
|
+
function extent(role, config, model, length) {
|
|
203
|
+
const data = model.y[role];
|
|
204
|
+
let [lo, hi] = data ? widen(data[0], data[1], model.inset, length) : [0, 1];
|
|
205
|
+
if (config.zero ?? model.zero[role]) {
|
|
206
|
+
lo = Math.min(lo, 0);
|
|
207
|
+
hi = Math.max(hi, 0);
|
|
208
|
+
}
|
|
209
|
+
return [lo, hi];
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Two linear axes that both reach below 0: 0 on one line, so bars on
|
|
213
|
+
* both start from the same baseline. Each gets nice steps of its own, the
|
|
214
|
+
* same number of them below 0 and above, as few as cover both. `null`
|
|
215
|
+
* when it doesn't apply (an end or the ticks given, a log axis, all data
|
|
216
|
+
* on one side of 0, where aligning the steps already lines up 0).
|
|
217
|
+
*/
|
|
218
|
+
function alignZero(y, y2, yConfig, y2Config, model, locale, length) {
|
|
219
|
+
if (y.kind === "log" || y2.kind === "log") return null;
|
|
220
|
+
const given = (c) => c.min !== void 0 || c.max !== void 0 || !!c.ticks;
|
|
221
|
+
if (given(yConfig) || given(y2Config)) return null;
|
|
222
|
+
const spans = [extent("y", yConfig, model, length), extent("y2", y2Config, model, length)];
|
|
223
|
+
if (!spans.some(([lo, hi]) => lo < 0 && hi > 0) || spans.some(([lo, hi]) => lo > 0 || hi < 0)) return null;
|
|
224
|
+
const intervals = y.ticks.length - 1;
|
|
225
|
+
const steps = spans.map(([lo, hi]) => {
|
|
226
|
+
const choices = [];
|
|
227
|
+
let step = niceStep(hi - lo || 1, intervals * 2);
|
|
228
|
+
for (let i = 0; i < 6; i++, step = nextNiceStep(step)) choices.push(step);
|
|
229
|
+
return choices;
|
|
230
|
+
});
|
|
231
|
+
let best = null;
|
|
232
|
+
for (const s1 of steps[0]) for (const s2 of steps[1]) {
|
|
233
|
+
const s = [s1, s2];
|
|
234
|
+
const below = Math.max(...spans.map(([lo], i) => Math.ceil(-lo / s[i] - 1e-9)));
|
|
235
|
+
const above = Math.max(...spans.map(([, hi], i) => Math.ceil(hi / s[i] - 1e-9)));
|
|
236
|
+
const total = below + above;
|
|
237
|
+
if (total < Math.max(2, intervals - 1) || total > intervals + 1) continue;
|
|
238
|
+
const fill = Math.min(...spans.map(([lo, hi], i) => (hi - lo) / (s[i] * total)));
|
|
239
|
+
if (!best || fill > best.fill + 1e-9) best = {
|
|
240
|
+
s,
|
|
241
|
+
below,
|
|
242
|
+
above,
|
|
243
|
+
fill
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
if (!best) return null;
|
|
247
|
+
const { s, below, above } = best;
|
|
248
|
+
const make = (axis, config, step) => {
|
|
249
|
+
const decimals = stepDecimals(step);
|
|
250
|
+
const ticks = Array.from({ length: below + above + 1 }, (_, i) => Number(((i - below) * step).toFixed(decimals)));
|
|
251
|
+
return {
|
|
252
|
+
...axis,
|
|
253
|
+
domain: [ticks[0], ticks[ticks.length - 1]],
|
|
254
|
+
ticks,
|
|
255
|
+
texts: valueTexts(ticks, config, locale)
|
|
256
|
+
};
|
|
257
|
+
};
|
|
258
|
+
return {
|
|
259
|
+
y: make(y, yConfig, s[0]),
|
|
260
|
+
y2: make(y2, y2Config, s[1])
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
/** x along the bottom, values up the left side (and the right). */
|
|
264
|
+
function layoutUp({ model, axes, width, height, locale, measure, zoom, trailing = 0 }, legendHeight, topRoom = 0) {
|
|
265
|
+
const xAxis = axes.x;
|
|
266
|
+
const xShown = !xAxis.hidden;
|
|
267
|
+
const top = (shown(axes.y) && axes.y.title || shown(axes.y2) && axes.y2.title ? 32 : 8) + topRoom;
|
|
268
|
+
const bottom = (xShown ? 24 + (xAxis.title ? 20 : 0) : 8) + legendHeight;
|
|
269
|
+
const plotTop = top;
|
|
270
|
+
const plotHeight = Math.max(plotTop + 1, height - bottom) - plotTop;
|
|
271
|
+
const yCount = Math.max(2, Math.floor(plotHeight / Y_TICK_SPACE));
|
|
272
|
+
const roomy = (axis) => {
|
|
273
|
+
if (!axis || axis.ticks.length < 2 || model.yCategories?.[axis.role]) return true;
|
|
274
|
+
const scale = valueScale(axis, [plotHeight, 0]);
|
|
275
|
+
const at = axis.ticks.map((t) => scale.map(t));
|
|
276
|
+
return at.every((v, i) => i === 0 || Math.abs(v - at[i - 1]) >= 19.5);
|
|
277
|
+
};
|
|
278
|
+
const asked = [axes.y?.ticks, axes.y2?.ticks].filter((t) => typeof t === "number");
|
|
279
|
+
const rows = Math.max(2, Math.floor(plotHeight / 20) + 1);
|
|
280
|
+
let values = valueAxes(axes, model, yCount, locale, plotHeight, zoom?.y, false, rows);
|
|
281
|
+
for (let most = Math.min(Math.max(yCount, ...asked), rows); most >= 1; most--) {
|
|
282
|
+
values = valueAxes(axes, model, yCount, locale, plotHeight, zoom?.y, false, most);
|
|
283
|
+
if (roomy(values.y) && roomy(values.y2)) break;
|
|
284
|
+
}
|
|
285
|
+
const topOnly = (axis) => {
|
|
286
|
+
if (!axis || roomy(axis)) return axis;
|
|
287
|
+
const last = axis.ticks.length - 1;
|
|
288
|
+
return {
|
|
289
|
+
...axis,
|
|
290
|
+
ticks: [axis.ticks[last]],
|
|
291
|
+
texts: [axis.texts[last]]
|
|
292
|
+
};
|
|
293
|
+
};
|
|
294
|
+
const y = topOnly(values.y);
|
|
295
|
+
const y2 = topOnly(values.y2);
|
|
296
|
+
const side = (axis, config) => axis && shown(config) ? 4 + (config.labelWidth ?? labelWidth(axis.texts, measure)) + 8 : 8;
|
|
297
|
+
const leftWidth = side(y, axes.y);
|
|
298
|
+
const rightWidth = side(y2, axes.y2) + trailing;
|
|
299
|
+
const plot = {
|
|
300
|
+
x: leftWidth,
|
|
301
|
+
y: plotTop,
|
|
302
|
+
width: Math.max(1, width - leftWidth - rightWidth),
|
|
303
|
+
height: plotHeight
|
|
304
|
+
};
|
|
305
|
+
const yLayout = {};
|
|
306
|
+
for (const axis of [y, y2]) {
|
|
307
|
+
if (!axis) continue;
|
|
308
|
+
const config = axes[axis.role];
|
|
309
|
+
const scale = valueScale(axis, runs(config, [plot.y + plot.height, plot.y]));
|
|
310
|
+
const labels = config.hidden ? [] : placeDown(axis.ticks, axis.texts, scale, height, measure);
|
|
311
|
+
for (const label of labels) if (axis.role === "y") alignEnd(label, plot.x);
|
|
312
|
+
else label.left = plot.x + plot.width + trailing + 8;
|
|
313
|
+
yLayout[axis.role] = {
|
|
314
|
+
position: config.position,
|
|
315
|
+
scale,
|
|
316
|
+
ticks: axis.ticks,
|
|
317
|
+
labels,
|
|
318
|
+
grid: config.grid,
|
|
319
|
+
full: axis.full ?? axis.domain,
|
|
320
|
+
title: config.title && !config.hidden ? topTitle(config.title, axis.role === "y2", width, topRoom) : void 0
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
const along = [plot.x, plot.x + plot.width];
|
|
324
|
+
const count = Math.max(2, Math.floor(plot.width / X_TICK_SPACE));
|
|
325
|
+
const { scale, full } = xScale(xAxis, model, runs(xAxis, along), count, zoom?.x);
|
|
326
|
+
const labelTop = plot.y + plot.height + 8;
|
|
327
|
+
let x;
|
|
328
|
+
for (const { ticks, texts } of xTickChoices(xAxis, model, scale, plot.width, X_TICK_SPACE, locale)) {
|
|
329
|
+
const labels = placeAcross(ticks, texts, scale, labelTop, width, measure);
|
|
330
|
+
x = {
|
|
331
|
+
position: xAxis.position,
|
|
332
|
+
scale,
|
|
333
|
+
ticks,
|
|
334
|
+
labels,
|
|
335
|
+
grid: xAxis.grid,
|
|
336
|
+
full
|
|
337
|
+
};
|
|
338
|
+
if (fitAcross(labels)) break;
|
|
339
|
+
}
|
|
340
|
+
x ??= {
|
|
341
|
+
position: xAxis.position,
|
|
342
|
+
scale,
|
|
343
|
+
ticks: [],
|
|
344
|
+
labels: [],
|
|
345
|
+
grid: xAxis.grid,
|
|
346
|
+
full
|
|
347
|
+
};
|
|
348
|
+
if (!xShown) x.labels = [];
|
|
349
|
+
else if (xAxis.title) x.title = bottomTitle(xAxis.title, width, height, legendHeight);
|
|
350
|
+
return {
|
|
351
|
+
width,
|
|
352
|
+
height,
|
|
353
|
+
plot,
|
|
354
|
+
x,
|
|
355
|
+
y: yLayout
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* x down the left side, values along the bottom (and a second kind along
|
|
360
|
+
* the top): bars with long category names.
|
|
361
|
+
*/
|
|
362
|
+
function layoutAcross({ model, axes, width, height, locale, measure, zoom }, legendHeight, topRoom = 0) {
|
|
363
|
+
const xAxis = axes.x;
|
|
364
|
+
const xShown = !xAxis.hidden;
|
|
365
|
+
const yShown = shown(axes.y);
|
|
366
|
+
const y2Shown = shown(axes.y2);
|
|
367
|
+
const titleRow = xShown && xAxis.title || y2Shown && axes.y2.title ? 24 : 0;
|
|
368
|
+
const top = Math.max(8, titleRow + (y2Shown ? 24 : 0)) + topRoom;
|
|
369
|
+
const bottom = (yShown ? 24 + (axes.y.title ? 20 : 0) : 8) + legendHeight;
|
|
370
|
+
const plotTop = top;
|
|
371
|
+
const plotBottom = Math.max(plotTop + 1, height - bottom);
|
|
372
|
+
const plotHeight = plotBottom - plotTop;
|
|
373
|
+
const { scale, full } = xScale(xAxis, model, runs(xAxis, [plotTop, plotBottom]), Math.max(2, Math.floor(plotHeight / Y_TICK_SPACE)), zoom?.x);
|
|
374
|
+
let x;
|
|
375
|
+
for (const { ticks, texts } of xTickChoices(xAxis, model, scale, plotHeight, Y_TICK_SPACE, locale)) {
|
|
376
|
+
const labels = placeDown(ticks, texts, scale, height, measure);
|
|
377
|
+
x = {
|
|
378
|
+
position: xAxis.position,
|
|
379
|
+
scale,
|
|
380
|
+
ticks,
|
|
381
|
+
labels,
|
|
382
|
+
grid: xAxis.grid,
|
|
383
|
+
full
|
|
384
|
+
};
|
|
385
|
+
if (fitDown(labels)) break;
|
|
386
|
+
}
|
|
387
|
+
x ??= {
|
|
388
|
+
position: xAxis.position,
|
|
389
|
+
scale,
|
|
390
|
+
ticks: [],
|
|
391
|
+
labels: [],
|
|
392
|
+
grid: xAxis.grid,
|
|
393
|
+
full
|
|
394
|
+
};
|
|
395
|
+
if (!xShown) x.labels = [];
|
|
396
|
+
const widest = xAxis.labelWidth ?? Math.floor(width * SIDE_LABEL_SHARE);
|
|
397
|
+
for (const label of x.labels) if (label.width > widest) {
|
|
398
|
+
label.width = widest;
|
|
399
|
+
label.clipped = true;
|
|
400
|
+
}
|
|
401
|
+
const leftWidth = !xShown ? 8 : xAxis.labelWidth !== void 0 ? 4 + xAxis.labelWidth + 8 : x.labels.length ? 4 + labelWidth(x.labels.map((l) => l.width)) + 8 : 8;
|
|
402
|
+
let rightWidth = 8;
|
|
403
|
+
const plot = {
|
|
404
|
+
x: leftWidth,
|
|
405
|
+
y: plotTop,
|
|
406
|
+
width: 1,
|
|
407
|
+
height: plotHeight
|
|
408
|
+
};
|
|
409
|
+
const across = width - leftWidth - 8;
|
|
410
|
+
const place = (axis, labelTop, visible) => {
|
|
411
|
+
if (!axis) return null;
|
|
412
|
+
const valueScaleAcross = valueScale(axis, runs(axes[axis.role], [plot.x, plot.x + plot.width]));
|
|
413
|
+
return {
|
|
414
|
+
axis,
|
|
415
|
+
scale: valueScaleAcross,
|
|
416
|
+
labels: visible ? placeAcross(axis.ticks, axis.texts, valueScaleAcross, labelTop, width, measure) : []
|
|
417
|
+
};
|
|
418
|
+
};
|
|
419
|
+
const placeAll = (axes2) => {
|
|
420
|
+
plot.width = Math.max(1, width - leftWidth - rightWidth);
|
|
421
|
+
return {
|
|
422
|
+
y: place(axes2.y, plotBottom + 8, yShown),
|
|
423
|
+
y2: place(axes2.y2, plotTop - 8 - 16, y2Shown)
|
|
424
|
+
};
|
|
425
|
+
};
|
|
426
|
+
let values = valueAxes(axes, model, 2, locale, across, zoom?.y, true, 2);
|
|
427
|
+
let placed = placeAll(values);
|
|
428
|
+
const asked = [axes.y?.ticks, axes.y2?.ticks].filter((t) => typeof t === "number");
|
|
429
|
+
const start = Math.min(Math.max(2, Math.floor(across / X_TICK_SPACE), ...asked), Math.max(2, Math.floor(across / 16)));
|
|
430
|
+
const placeWithRoom = () => {
|
|
431
|
+
rightWidth = 8;
|
|
432
|
+
let out = placeAll(values);
|
|
433
|
+
let excess = 0;
|
|
434
|
+
for (const p of [out.y, out.y2]) {
|
|
435
|
+
const last = p?.labels[p.labels.length - 1];
|
|
436
|
+
if (p && last) excess = Math.max(excess, p.scale.map(p.axis.ticks[p.axis.ticks.length - 1]) + last.width / 2 + 4 - width);
|
|
437
|
+
}
|
|
438
|
+
if (excess > 0) {
|
|
439
|
+
rightWidth += Math.ceil(excess);
|
|
440
|
+
out = placeAll(values);
|
|
441
|
+
}
|
|
442
|
+
return out;
|
|
443
|
+
};
|
|
444
|
+
for (let count = start; count >= 2; count--) {
|
|
445
|
+
values = valueAxes(axes, model, count, locale, across, zoom?.y, true, count);
|
|
446
|
+
placed = placeWithRoom();
|
|
447
|
+
if ((!placed.y || fitAcross(placed.y.labels)) && (!placed.y2 || fitAcross(placed.y2.labels))) break;
|
|
448
|
+
}
|
|
449
|
+
for (const label of x.labels) alignEnd(label, plot.x);
|
|
450
|
+
x.title = xShown && xAxis.title ? {
|
|
451
|
+
text: xAxis.title,
|
|
452
|
+
left: 4,
|
|
453
|
+
top: topRoom,
|
|
454
|
+
align: "start"
|
|
455
|
+
} : void 0;
|
|
456
|
+
const yLayout = {};
|
|
457
|
+
for (const role of ["y", "y2"]) {
|
|
458
|
+
const p = placed[role];
|
|
459
|
+
const config = axes[role];
|
|
460
|
+
if (!p || !config) continue;
|
|
461
|
+
yLayout[role] = {
|
|
462
|
+
position: config.position,
|
|
463
|
+
scale: p.scale,
|
|
464
|
+
ticks: p.axis.ticks,
|
|
465
|
+
labels: p.labels,
|
|
466
|
+
grid: config.grid,
|
|
467
|
+
full: p.axis.full ?? p.axis.domain,
|
|
468
|
+
title: config.title && !config.hidden ? role === "y" ? bottomTitle(config.title, width, height, legendHeight) : topTitle(config.title, true, width, topRoom) : void 0
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
return {
|
|
472
|
+
width,
|
|
473
|
+
height,
|
|
474
|
+
plot,
|
|
475
|
+
x,
|
|
476
|
+
y: yLayout
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
/** Puts a label's right edge a gap before `edge`. */
|
|
480
|
+
function alignEnd(label, edge) {
|
|
481
|
+
label.left = edge - 8 - label.width;
|
|
482
|
+
}
|
|
483
|
+
/** A title at the top: on the left, or (`end`) on the right. */
|
|
484
|
+
function topTitle(text, end, width, top = 0) {
|
|
485
|
+
return {
|
|
486
|
+
text,
|
|
487
|
+
top,
|
|
488
|
+
left: end ? width - 4 : 4,
|
|
489
|
+
align: end ? "end" : "start"
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
function bottomTitle(text, width, height, legendHeight) {
|
|
493
|
+
return {
|
|
494
|
+
text,
|
|
495
|
+
top: height - legendHeight - 16,
|
|
496
|
+
left: width - 4,
|
|
497
|
+
align: "end"
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
/** Labels centered under their ticks, kept inside the chart. */
|
|
501
|
+
function placeAcross(ticks, texts, scale, top, width, measure) {
|
|
502
|
+
return ticks.map((value, i) => {
|
|
503
|
+
const text = texts[i];
|
|
504
|
+
const w = measure(text);
|
|
505
|
+
return {
|
|
506
|
+
text,
|
|
507
|
+
width: w,
|
|
508
|
+
left: clamp(scale.map(value) - w / 2, 4, width - 4 - w),
|
|
509
|
+
top
|
|
510
|
+
};
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
/** Labels centered beside their ticks, kept inside the chart; `left` is set by the caller. */
|
|
514
|
+
function placeDown(ticks, texts, scale, height, measure) {
|
|
515
|
+
return ticks.map((value, i) => {
|
|
516
|
+
const text = texts[i];
|
|
517
|
+
return {
|
|
518
|
+
text,
|
|
519
|
+
width: measure(text),
|
|
520
|
+
left: 0,
|
|
521
|
+
top: clamp(scale.map(value) - 8, 0, height - 16)
|
|
522
|
+
};
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
var fitAcross = (labels) => {
|
|
526
|
+
const list = [...labels];
|
|
527
|
+
list.sort((a, b) => a.left - b.left);
|
|
528
|
+
for (let i = 1; i < list.length; i++) if (list[i].left < list[i - 1].left + list[i - 1].width + 8) return false;
|
|
529
|
+
return true;
|
|
530
|
+
};
|
|
531
|
+
var fitDown = (labels) => {
|
|
532
|
+
for (let i = 1; i < labels.length; i++) if (Math.abs(labels[i].top - labels[i - 1].top) < 20) return false;
|
|
533
|
+
return true;
|
|
534
|
+
};
|
|
535
|
+
/** Entries in rows that fit `width`, each row centered; `top` counts from the first row. */
|
|
536
|
+
function placeLegend(names, width, measure, coarse = false) {
|
|
537
|
+
const items = [];
|
|
538
|
+
let row = [];
|
|
539
|
+
let rowWidth = 0;
|
|
540
|
+
let top = 0;
|
|
541
|
+
const finish = () => {
|
|
542
|
+
let left = Math.max(4, (width - rowWidth) / 2);
|
|
543
|
+
for (const item of row) {
|
|
544
|
+
item.left = left;
|
|
545
|
+
left += item.width + LEGEND_GAP;
|
|
546
|
+
}
|
|
547
|
+
row = [];
|
|
548
|
+
rowWidth = 0;
|
|
549
|
+
};
|
|
550
|
+
names.forEach((name, index) => {
|
|
551
|
+
const text = nameText(name);
|
|
552
|
+
const w = swatchOf(name) + SWATCH_GAP + measure(text);
|
|
553
|
+
if (row.length && rowWidth + LEGEND_GAP + w > width - 8) {
|
|
554
|
+
finish();
|
|
555
|
+
top += 16 + (coarse ? LEGEND_ROW_GAP_COARSE : LEGEND_ROW_GAP);
|
|
556
|
+
}
|
|
557
|
+
const item = {
|
|
558
|
+
index,
|
|
559
|
+
text,
|
|
560
|
+
left: 0,
|
|
561
|
+
top,
|
|
562
|
+
width: w
|
|
563
|
+
};
|
|
564
|
+
if (typeof name !== "string") item.swatch = name.swatch;
|
|
565
|
+
rowWidth += (row.length ? LEGEND_GAP : 0) + w;
|
|
566
|
+
row.push(item);
|
|
567
|
+
items.push(item);
|
|
568
|
+
});
|
|
569
|
+
if (row.length) finish();
|
|
570
|
+
return items;
|
|
571
|
+
}
|
|
572
|
+
function labelWidth(items, measure) {
|
|
573
|
+
let widest = 0;
|
|
574
|
+
for (const item of items) widest = Math.max(widest, typeof item === "number" ? item : measure(item));
|
|
575
|
+
return Math.ceil(widest);
|
|
576
|
+
}
|
|
577
|
+
function valueScale(axis, range) {
|
|
578
|
+
return axis.kind === "log" ? logScale(axis.domain, range) : linearScale(axis.domain, range);
|
|
579
|
+
}
|
|
580
|
+
function valueAxis(role, config, model, count, locale, length, across = false, most = Infinity) {
|
|
581
|
+
const listed = model.yCategories?.[role];
|
|
582
|
+
if (listed) {
|
|
583
|
+
const ticks = listed.map((_, i) => i);
|
|
584
|
+
return {
|
|
585
|
+
role,
|
|
586
|
+
kind: "linear",
|
|
587
|
+
domain: across ? [-.5, listed.length - .5] : [listed.length - .5, -.5],
|
|
588
|
+
ticks,
|
|
589
|
+
texts: listed.map((label) => config.format ? config.format(label) : label),
|
|
590
|
+
fixed: true
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
count = tickCount(config, count, most);
|
|
594
|
+
const kind = config.type === "log" ? "log" : "linear";
|
|
595
|
+
const data = model.y[role];
|
|
596
|
+
const zero = config.zero ?? model.zero[role];
|
|
597
|
+
let [lo, hi] = data ? kind === "linear" ? widen(data[0], data[1], model.inset, length) : data : [0, 1];
|
|
598
|
+
if (zero && kind === "linear") {
|
|
599
|
+
lo = Math.min(lo, 0);
|
|
600
|
+
hi = Math.max(hi, 0);
|
|
601
|
+
}
|
|
602
|
+
const min = asNumber(config.min, kind);
|
|
603
|
+
const max = asNumber(config.max, kind);
|
|
604
|
+
if (min !== void 0) lo = min;
|
|
605
|
+
if (max !== void 0) hi = max;
|
|
606
|
+
let domain;
|
|
607
|
+
if (kind === "log") domain = min !== void 0 && max !== void 0 ? [lo, hi] : logDomain(lo, hi);
|
|
608
|
+
else {
|
|
609
|
+
domain = linearDomain(lo, hi, {
|
|
610
|
+
count,
|
|
611
|
+
nice: config.nice ?? true
|
|
612
|
+
});
|
|
613
|
+
if (min !== void 0) domain[0] = min;
|
|
614
|
+
if (max !== void 0) domain[1] = max;
|
|
615
|
+
}
|
|
616
|
+
const given = explicitTicks(config, kind);
|
|
617
|
+
let ticks = given ?? (kind === "log" ? logTicks(domain[0], domain[1], count) : linearTicks(domain[0], domain[1], count));
|
|
618
|
+
if (!given && kind === "linear" && (config.nice ?? true) && ticks.length > 1) {
|
|
619
|
+
const step = ticks[1] - ticks[0];
|
|
620
|
+
const decimals = stepDecimals(step);
|
|
621
|
+
const from = min ?? Number((Math.floor(domain[0] / step + 1e-9) * step).toFixed(decimals));
|
|
622
|
+
const to = max ?? Number((Math.ceil(domain[1] / step - 1e-9) * step).toFixed(decimals));
|
|
623
|
+
if (from !== domain[0] || to !== domain[1]) {
|
|
624
|
+
domain = [from, to];
|
|
625
|
+
ticks = linearTicks(from, to, count);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return {
|
|
629
|
+
role,
|
|
630
|
+
kind,
|
|
631
|
+
domain,
|
|
632
|
+
ticks,
|
|
633
|
+
texts: valueTexts(ticks, config, locale),
|
|
634
|
+
fixed: min !== void 0 && max !== void 0
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
/** Gives `axis` exactly `intervals` steps of a nice size, still covering its data. */
|
|
638
|
+
function alignTo(axis, intervals, config, model, locale, length) {
|
|
639
|
+
if (axis.kind === "log") return axis;
|
|
640
|
+
let [lo, hi] = extent(axis.role, config, model, length);
|
|
641
|
+
if (config.min !== void 0) lo = Number(config.min);
|
|
642
|
+
if (lo === hi) hi = lo + 1;
|
|
643
|
+
let step = niceStep(hi - lo, intervals);
|
|
644
|
+
for (let guard = 0; guard < 8; guard++) {
|
|
645
|
+
const start = config.min !== void 0 ? lo : Math.floor(lo / step) * step;
|
|
646
|
+
if (start + step * intervals >= hi - 1e-9) {
|
|
647
|
+
const decimals = stepDecimals(step);
|
|
648
|
+
const ticks = Array.from({ length: intervals + 1 }, (_, i) => Number((start + step * i).toFixed(decimals)));
|
|
649
|
+
return {
|
|
650
|
+
...axis,
|
|
651
|
+
domain: [ticks[0], ticks[intervals]],
|
|
652
|
+
ticks,
|
|
653
|
+
texts: valueTexts(ticks, config, locale)
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
step = nextNiceStep(step);
|
|
657
|
+
}
|
|
658
|
+
return axis;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* How many ticks to aim for: the number given as `ticks`, else `count`
|
|
662
|
+
* (what fits); never more than `most`, so asking for many on a narrow
|
|
663
|
+
* chart doesn't pile the labels on one another.
|
|
664
|
+
*/
|
|
665
|
+
function tickCount(config, count, most = Infinity) {
|
|
666
|
+
const asked = typeof config.ticks === "number" && config.ticks >= 1 ? Math.round(config.ticks) : count;
|
|
667
|
+
return Math.max(1, Math.min(asked, most));
|
|
668
|
+
}
|
|
669
|
+
function explicitTicks(config, kind) {
|
|
670
|
+
if (!Array.isArray(config.ticks)) return null;
|
|
671
|
+
return config.ticks.map((value) => asNumber(value, kind)).filter((v) => Number.isFinite(v));
|
|
672
|
+
}
|
|
673
|
+
function valueTexts(ticks, config, locale) {
|
|
674
|
+
if (config.format) return ticks.map((value) => config.format(value));
|
|
675
|
+
const format = numberTickFormat(locale, ticks);
|
|
676
|
+
return ticks.map((value) => format(value));
|
|
677
|
+
}
|
|
678
|
+
/** The x scale over `range`; `count` is about how many ticks a nice number domain is rounded to. */
|
|
679
|
+
function xScale(config, model, range, count, zoom) {
|
|
680
|
+
const scale = wholeXScale(config, model, range, count);
|
|
681
|
+
const full = scale.domain;
|
|
682
|
+
if (!zoom) return {
|
|
683
|
+
scale,
|
|
684
|
+
full
|
|
685
|
+
};
|
|
686
|
+
const lo = Math.max(full[0], Math.min(zoom[0], zoom[1]));
|
|
687
|
+
const hi = Math.min(full[1], Math.max(zoom[0], zoom[1]));
|
|
688
|
+
if (!(hi > lo)) return {
|
|
689
|
+
scale,
|
|
690
|
+
full
|
|
691
|
+
};
|
|
692
|
+
const part = linearScale([lo, hi], range, scale.kind === "time" ? "time" : "linear");
|
|
693
|
+
if (scale.kind !== "category") return {
|
|
694
|
+
scale: part,
|
|
695
|
+
full
|
|
696
|
+
};
|
|
697
|
+
const step = Math.abs(range[1] - range[0]) / (hi - lo);
|
|
698
|
+
return {
|
|
699
|
+
scale: {
|
|
700
|
+
...part,
|
|
701
|
+
kind: "category",
|
|
702
|
+
step,
|
|
703
|
+
categories: scale.categories
|
|
704
|
+
},
|
|
705
|
+
full
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
function wholeXScale(config, model, range, count) {
|
|
709
|
+
const kind = model.xKind;
|
|
710
|
+
if (kind === "category") return categoryScale(model.categories, range);
|
|
711
|
+
const data = model.x ?? [0, 1];
|
|
712
|
+
const pad = model.xStep > 0 ? model.xStep / 2 : 0;
|
|
713
|
+
const [from, to] = widen(data[0] - pad, data[1] + pad, model.inset, Math.abs(range[1] - range[0]));
|
|
714
|
+
let lo = asNumber(config.min, kind) ?? from;
|
|
715
|
+
let hi = asNumber(config.max, kind) ?? to;
|
|
716
|
+
if (kind === "time") {
|
|
717
|
+
if (lo === hi) {
|
|
718
|
+
lo -= 432e5;
|
|
719
|
+
hi += 432e5;
|
|
720
|
+
}
|
|
721
|
+
return linearScale([lo, hi], range, "time");
|
|
722
|
+
}
|
|
723
|
+
const domain = linearDomain(lo, hi, {
|
|
724
|
+
count: tickCount(config, count),
|
|
725
|
+
nice: config.nice ?? false
|
|
726
|
+
});
|
|
727
|
+
if (config.min !== void 0) domain[0] = lo;
|
|
728
|
+
if (config.max !== void 0) domain[1] = hi;
|
|
729
|
+
return linearScale(domain, range);
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* The ticks to try on the x axis with their texts, most first: the
|
|
733
|
+
* caller takes the first whose labels fit. `length` is the axis length
|
|
734
|
+
* in px and `space` the wanted room per tick.
|
|
735
|
+
*/
|
|
736
|
+
function* xTickChoices(config, model, scale, length, space, locale) {
|
|
737
|
+
const kind = model.xKind;
|
|
738
|
+
if (kind === "category") {
|
|
739
|
+
const text = (value) => config.format ? config.format(model.categories[value]) : model.categories[value] ?? "";
|
|
740
|
+
const count = model.categories.length;
|
|
741
|
+
const [lo, hi] = scale.domain;
|
|
742
|
+
const shownTicks = (ticks) => lo <= -.5 && hi >= count - .5 ? ticks : ticks.filter((i) => i >= lo && i <= hi);
|
|
743
|
+
for (let every = 1; every <= Math.max(1, count); every++) {
|
|
744
|
+
const ticks = shownTicks(categoryTicks(count, every));
|
|
745
|
+
yield {
|
|
746
|
+
ticks,
|
|
747
|
+
texts: ticks.map(text)
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
const explicit = explicitTicks(config, kind);
|
|
753
|
+
const [lo, hi] = scale.domain;
|
|
754
|
+
const wanted = tickCount(config, Math.max(2, Math.floor(length / space)));
|
|
755
|
+
if (kind === "time") {
|
|
756
|
+
for (let count = wanted; count >= 1; count--) {
|
|
757
|
+
const interval = timeInterval(hi - lo, count);
|
|
758
|
+
const ticks = explicit ?? timeTicks(lo, hi, interval);
|
|
759
|
+
const format = timeTickFormat(locale, interval);
|
|
760
|
+
yield {
|
|
761
|
+
ticks,
|
|
762
|
+
texts: ticks.map((value, i) => config.format ? config.format(new Date(value)) : format(value, ticks[i - 1]))
|
|
763
|
+
};
|
|
764
|
+
if (explicit) return;
|
|
765
|
+
}
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
const most = model.xIntegers ? Math.max(1, Math.floor(hi - lo)) : wanted;
|
|
769
|
+
for (let count = Math.min(wanted, most); count >= 1; count--) {
|
|
770
|
+
const ticks = explicit ?? linearTicks(lo, hi, count);
|
|
771
|
+
const format = numberTickFormat(locale, ticks, { years: true });
|
|
772
|
+
yield {
|
|
773
|
+
ticks,
|
|
774
|
+
texts: ticks.map((value) => config.format ? config.format(value) : format(value))
|
|
775
|
+
};
|
|
776
|
+
if (explicit) return;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* The box round the part of a circle of radius 1 from `start` to `end`
|
|
781
|
+
* (radians clockwise from 12 o'clock), its center included, in units of
|
|
782
|
+
* the radius from the center: a half circle is half as tall as a whole.
|
|
783
|
+
*/
|
|
784
|
+
function arcBox(start, end) {
|
|
785
|
+
let [left, right, top, bottom] = [
|
|
786
|
+
0,
|
|
787
|
+
0,
|
|
788
|
+
0,
|
|
789
|
+
0
|
|
790
|
+
];
|
|
791
|
+
const take = (angle) => {
|
|
792
|
+
const x = Math.sin(angle);
|
|
793
|
+
const y = -Math.cos(angle);
|
|
794
|
+
left = Math.min(left, x);
|
|
795
|
+
right = Math.max(right, x);
|
|
796
|
+
top = Math.min(top, y);
|
|
797
|
+
bottom = Math.max(bottom, y);
|
|
798
|
+
};
|
|
799
|
+
take(start);
|
|
800
|
+
take(end);
|
|
801
|
+
for (let k = Math.ceil(start / (Math.PI / 2)); k * (Math.PI / 2) <= end; k++) take(k * (Math.PI / 2));
|
|
802
|
+
return {
|
|
803
|
+
left,
|
|
804
|
+
right,
|
|
805
|
+
top,
|
|
806
|
+
bottom
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* A pie as large as fits above its legend; with labels outside, as large
|
|
811
|
+
* as leaves room for the widest of them on each side, moved over towards
|
|
812
|
+
* the side with the narrower labels.
|
|
813
|
+
*/
|
|
814
|
+
function layoutPie({ width, height, measure, legend: names = [], coarse, legendPlacement = "bottom", sideLabels = {
|
|
815
|
+
left: 0,
|
|
816
|
+
right: 0
|
|
817
|
+
}, arc = [0, Math.PI * 2] }) {
|
|
818
|
+
const placed = placeLegendIn(names, legendPlacement, width, height, measure, coarse);
|
|
819
|
+
const { top, bottom, right: legendRight } = placed.room;
|
|
820
|
+
const room = height - top - bottom - 16;
|
|
821
|
+
const inner = width - legendRight;
|
|
822
|
+
const left = sideLabels.left > 0 ? sideLabels.left + 24 : 0;
|
|
823
|
+
const right = sideLabels.right > 0 ? sideLabels.right + 24 : 0;
|
|
824
|
+
const across = inner - left - right;
|
|
825
|
+
const down = left || right ? room - 16 : room;
|
|
826
|
+
const box = arcBox(arc[0], arc[1]);
|
|
827
|
+
const radius = Math.max(1, Math.min((across - 16) / (box.right - box.left || 1), down / (box.bottom - box.top || 1)));
|
|
828
|
+
return {
|
|
829
|
+
width,
|
|
830
|
+
height,
|
|
831
|
+
cx: left + across / 2 - (box.left + box.right) / 2 * radius,
|
|
832
|
+
cy: top + 8 + room / 2 - (box.top + box.bottom) / 2 * radius,
|
|
833
|
+
radius,
|
|
834
|
+
bottom: height - bottom - 8,
|
|
835
|
+
legend: placed.legend,
|
|
836
|
+
legendBox: placed.box
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
/** Room between the rim and a spoke's name. */
|
|
840
|
+
var SPOKE_GAP = 8;
|
|
841
|
+
/** The most of the chart's width a spoke's name may take, each side. */
|
|
842
|
+
var SPOKE_SHARE = .22;
|
|
843
|
+
/** Wanted room between rings, in px. */
|
|
844
|
+
var RING_SPACE = 24;
|
|
845
|
+
/**
|
|
846
|
+
* Lays out a radar: the legend first, then the largest circle that leaves
|
|
847
|
+
* room round it for the spokes' names (the categories), with rings at
|
|
848
|
+
* nice values from the center (0, or the smallest value below it) out.
|
|
849
|
+
*/
|
|
850
|
+
function layoutRadar({ model, axes, width, height, locale, measure, legend: names = [], coarse, legendPlacement = "bottom", spokes }) {
|
|
851
|
+
const placed = placeLegendIn(names, legendPlacement, width, height, measure, coarse);
|
|
852
|
+
const { top, bottom, right: legendRight } = placed.room;
|
|
853
|
+
const across = width - legendRight;
|
|
854
|
+
const down = height - top - bottom;
|
|
855
|
+
const count = spokes.length;
|
|
856
|
+
const most = Math.max(24, Math.floor(across * SPOKE_SHARE));
|
|
857
|
+
const widths = spokes.map((text) => measure(text));
|
|
858
|
+
const angleOf = (i) => i / Math.max(1, count) * Math.PI * 2;
|
|
859
|
+
let side = 0;
|
|
860
|
+
widths.forEach((w, i) => {
|
|
861
|
+
if (Math.abs(Math.sin(angleOf(i))) >= .1) side = Math.max(side, w);
|
|
862
|
+
});
|
|
863
|
+
side = Math.min(side, most);
|
|
864
|
+
const radius = Math.max(1, Math.min((across - (side + SPOKE_GAP) * 2 - 16) / 2, (down - 48 - 16) / 2));
|
|
865
|
+
const cx = across / 2;
|
|
866
|
+
const cy = top + down / 2;
|
|
867
|
+
const config = axes.y ?? {
|
|
868
|
+
position: "left",
|
|
869
|
+
grid: true,
|
|
870
|
+
hidden: false
|
|
871
|
+
};
|
|
872
|
+
const axis = valueAxis("y", config, model, Math.max(2, Math.floor(radius / RING_SPACE)), locale, radius);
|
|
873
|
+
const scale = linearScale(axis.domain, [0, radius]);
|
|
874
|
+
const labels = [];
|
|
875
|
+
if (!axes.x.hidden) spokes.forEach((text, i) => {
|
|
876
|
+
const room = Math.min(widths[i], most);
|
|
877
|
+
const sin = Math.sin(angleOf(i));
|
|
878
|
+
const cos = Math.cos(angleOf(i));
|
|
879
|
+
const x = cx + sin * (radius + SPOKE_GAP);
|
|
880
|
+
const y = cy - cos * (radius + SPOKE_GAP);
|
|
881
|
+
const where = Math.abs(sin) < .1 ? 0 : sin > 0 ? 1 : -1;
|
|
882
|
+
const lift = where === 0 ? cos > 0 ? 8 : -8 : 0;
|
|
883
|
+
const label = {
|
|
884
|
+
text,
|
|
885
|
+
left: where === 0 ? x - room / 2 : where > 0 ? x : x - room,
|
|
886
|
+
top: y - 8 - lift,
|
|
887
|
+
width: room
|
|
888
|
+
};
|
|
889
|
+
if (widths[i] > most) label.clipped = true;
|
|
890
|
+
labels.push(label);
|
|
891
|
+
});
|
|
892
|
+
const values = [];
|
|
893
|
+
if (!config.hidden) axis.ticks.forEach((tick, i) => {
|
|
894
|
+
const r = scale.map(tick);
|
|
895
|
+
if (r < 1) return;
|
|
896
|
+
const text = axis.texts[i];
|
|
897
|
+
const half = Math.PI / Math.max(2, count);
|
|
898
|
+
const out = r * Math.cos(half);
|
|
899
|
+
const x = cx + Math.sin(half) * out;
|
|
900
|
+
const y = cy - Math.cos(half) * out;
|
|
901
|
+
const label = {
|
|
902
|
+
text,
|
|
903
|
+
left: x + 2,
|
|
904
|
+
top: y - 16,
|
|
905
|
+
width: measure(text)
|
|
906
|
+
};
|
|
907
|
+
if (!labels.some((other) => label.left < other.left + other.width && other.left < label.left + label.width && label.top < other.top + 16 && other.top < label.top + 16)) values.push(label);
|
|
908
|
+
});
|
|
909
|
+
return {
|
|
910
|
+
width,
|
|
911
|
+
height,
|
|
912
|
+
cx,
|
|
913
|
+
cy,
|
|
914
|
+
radius,
|
|
915
|
+
count,
|
|
916
|
+
scale,
|
|
917
|
+
ticks: axis.ticks,
|
|
918
|
+
labels,
|
|
919
|
+
values,
|
|
920
|
+
legend: placed.legend,
|
|
921
|
+
legendBox: placed.box
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
/** Room between a funnel's bars and the text beside them, and between a value and its rate. */
|
|
925
|
+
var FUNNEL_GAP = 12;
|
|
926
|
+
var RATE_GAP = 8;
|
|
927
|
+
/** The most of the chart's width the names may take. */
|
|
928
|
+
var FUNNEL_NAME_SHARE = .3;
|
|
929
|
+
/** The tallest a stage's row grows, in px: a few stages don't fill a tall chart with bars. */
|
|
930
|
+
var FUNNEL_STEP = 64;
|
|
931
|
+
/**
|
|
932
|
+
* Lays out a funnel: the legend, then the names in a column on the left
|
|
933
|
+
* and the values (with rates) on the right, and the bars between them.
|
|
934
|
+
*/
|
|
935
|
+
function layoutFunnel({ width, height, measure, legend: names = [], coarse, legendPlacement = "bottom", stages, values: written, rates: rateTexts, max }) {
|
|
936
|
+
const placed = placeLegendIn(names, legendPlacement, width, height, measure, coarse);
|
|
937
|
+
const { top, bottom, right: legendRight } = placed.room;
|
|
938
|
+
const across = width - legendRight;
|
|
939
|
+
const count = stages.length;
|
|
940
|
+
const most = Math.floor(across * FUNNEL_NAME_SHARE);
|
|
941
|
+
const nameWidths = stages.map((text) => Math.min(measure(text), most));
|
|
942
|
+
const valueWidths = written.map((text) => measure(text));
|
|
943
|
+
const rateWidths = rateTexts.map((text) => text ? measure(text) : 0);
|
|
944
|
+
const valueColumn = Math.max(0, ...valueWidths);
|
|
945
|
+
const rateColumn = Math.max(0, ...rateWidths);
|
|
946
|
+
const left = 4 + Math.max(0, ...nameWidths) + FUNNEL_GAP;
|
|
947
|
+
const right = FUNNEL_GAP + valueColumn + (rateColumn ? RATE_GAP + rateColumn : 0) + 4;
|
|
948
|
+
const room = Math.max(1, height - top - bottom - 16);
|
|
949
|
+
const step = count ? Math.min(FUNNEL_STEP, room / count) : room;
|
|
950
|
+
const plotHeight = step * count;
|
|
951
|
+
const plot = {
|
|
952
|
+
x: left,
|
|
953
|
+
y: top + 8 + (room - plotHeight) / 2,
|
|
954
|
+
width: Math.max(1, across - left - right),
|
|
955
|
+
height: plotHeight
|
|
956
|
+
};
|
|
957
|
+
const scale = linearScale([0, max > 0 ? max : 1], [0, plot.width / 2]);
|
|
958
|
+
const middle = (i) => plot.y + step * i + step / 2 - 8;
|
|
959
|
+
const labels = stages.map((text, i) => {
|
|
960
|
+
const label = {
|
|
961
|
+
text,
|
|
962
|
+
left: plot.x - FUNNEL_GAP - nameWidths[i],
|
|
963
|
+
top: middle(i),
|
|
964
|
+
width: nameWidths[i]
|
|
965
|
+
};
|
|
966
|
+
if (measure(text) > most) label.clipped = true;
|
|
967
|
+
return label;
|
|
968
|
+
});
|
|
969
|
+
const valueLeft = plot.x + plot.width + FUNNEL_GAP;
|
|
970
|
+
const values = written.map((text, i) => ({
|
|
971
|
+
text,
|
|
972
|
+
left: valueLeft + valueColumn - valueWidths[i],
|
|
973
|
+
top: middle(i),
|
|
974
|
+
width: valueWidths[i]
|
|
975
|
+
}));
|
|
976
|
+
const rates = [];
|
|
977
|
+
rateTexts.forEach((text, i) => {
|
|
978
|
+
if (!text) return;
|
|
979
|
+
rates.push({
|
|
980
|
+
text,
|
|
981
|
+
left: valueLeft + valueColumn + RATE_GAP + rateColumn - rateWidths[i],
|
|
982
|
+
top: middle(i),
|
|
983
|
+
width: rateWidths[i]
|
|
984
|
+
});
|
|
985
|
+
});
|
|
986
|
+
return {
|
|
987
|
+
width,
|
|
988
|
+
height,
|
|
989
|
+
plot,
|
|
990
|
+
count,
|
|
991
|
+
step,
|
|
992
|
+
scale,
|
|
993
|
+
names: labels,
|
|
994
|
+
values,
|
|
995
|
+
rates,
|
|
996
|
+
legend: placed.legend,
|
|
997
|
+
legendBox: placed.box
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
export { arcBox, layoutChart, layoutFunnel, layoutPie, layoutRadar, placeLegend, placeLegendIn };
|
|
1001
|
+
|
|
1002
|
+
//# sourceMappingURL=layout.js.map
|