@octane-xplat/charts 0.0.0-stage → 0.10.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/dist/macos/Chart.macos.tsrx.js +351 -0
- package/dist/macos/core/axes.js +146 -0
- package/dist/macos/core/hit.js +131 -0
- package/dist/macos/core/marks.js +262 -0
- package/dist/macos/core/scales.js +99 -0
- package/dist/macos/core/scene.js +120 -0
- package/dist/macos/core/svg.js +40 -0
- package/dist/macos/index.js +2 -0
- package/dist/macos/octane-client-build.json +8 -0
- package/dist/macos/svg-image.macos.js +14 -0
- package/dist/native/Chart.tsrx.js +340 -0
- package/dist/native/core/axes.js +146 -0
- package/dist/native/core/hit.js +131 -0
- package/dist/native/core/marks.js +262 -0
- package/dist/native/core/scales.js +99 -0
- package/dist/native/core/scene.js +120 -0
- package/dist/native/core/svg.js +40 -0
- package/dist/native/index.js +2 -0
- package/dist/native/octane-client-build.json +8 -0
- package/dist/web/Chart.web.tsrx.js +488 -0
- package/dist/web/core/axes.js +146 -0
- package/dist/web/core/hit.js +131 -0
- package/dist/web/core/marks.js +262 -0
- package/dist/web/core/scales.js +99 -0
- package/dist/web/core/scene.js +120 -0
- package/dist/web/core/svg.js +40 -0
- package/dist/web/index.js +2 -0
- package/dist/web/octane-client-build.json +8 -0
- package/package.json +91 -3
- package/src/Chart.macos.tsrx +134 -0
- package/src/Chart.tsrx +174 -0
- package/src/Chart.web.tsrx +215 -0
- package/src/core/axes.ts +211 -0
- package/src/core/hit.ts +201 -0
- package/src/core/marks.ts +380 -0
- package/src/core/scales.ts +168 -0
- package/src/core/scene.ts +145 -0
- package/src/core/svg.ts +84 -0
- package/src/index.macos.ts +12 -0
- package/src/index.ts +12 -0
- package/src/index.web.ts +12 -0
- package/src/props.ts +90 -0
- package/src/svg-image.macos.ts +34 -0
- package/types/index.d.ts +17 -0
- package/types/props.d.ts +83 -0
- package/README.md +0 -3
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { categoryKey, xCenter } from "./scales.js";
|
|
2
|
+
import { scaleBand } from "d3-scale";
|
|
3
|
+
import { arc, area, curveMonotoneX, line, pie } from "d3-shape";
|
|
4
|
+
//#region src/core/marks.ts
|
|
5
|
+
function buildMarks(type, layout, series, options) {
|
|
6
|
+
switch (type) {
|
|
7
|
+
case "line": return lineMarks(layout, series);
|
|
8
|
+
case "area": return areaMarks(layout, series);
|
|
9
|
+
case "scatter": return scatterMarks(layout, series);
|
|
10
|
+
case "bar": return options.stacked ? stackedBarMarks(layout, series) : groupedBarMarks(layout, series);
|
|
11
|
+
case "pie": return pieMarks(layout, series, options);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function lineMarks(layout, series) {
|
|
15
|
+
const { xKind, x, y } = layout;
|
|
16
|
+
const gen = line().defined((d) => Number.isFinite(d.y)).x((d) => xCenter(xKind, x, d.x)).y((d) => y(d.y)).curve(curveMonotoneX);
|
|
17
|
+
const marks = [];
|
|
18
|
+
const points = [];
|
|
19
|
+
series.forEach((s, si) => {
|
|
20
|
+
marks.push({
|
|
21
|
+
kind: "path",
|
|
22
|
+
d: gen(s.values) ?? "",
|
|
23
|
+
fill: "none",
|
|
24
|
+
stroke: s.color,
|
|
25
|
+
strokeWidth: 2,
|
|
26
|
+
strokeLinecap: "round",
|
|
27
|
+
series: si
|
|
28
|
+
});
|
|
29
|
+
s.values.forEach((d, i) => {
|
|
30
|
+
points.push({
|
|
31
|
+
series: si,
|
|
32
|
+
index: i,
|
|
33
|
+
key: categoryKey(d.x),
|
|
34
|
+
cx: xCenter(xKind, x, d.x),
|
|
35
|
+
cy: y(d.y),
|
|
36
|
+
x: d.x,
|
|
37
|
+
y: d.y
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
return {
|
|
42
|
+
marks,
|
|
43
|
+
points
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function areaMarks(layout, series) {
|
|
47
|
+
const { xKind, x, y } = layout;
|
|
48
|
+
const zero = y(Math.max(y.domain()[0], Math.min(0, y.domain()[1])));
|
|
49
|
+
const gen = area().defined((d) => Number.isFinite(d.y)).x((d) => xCenter(xKind, x, d.x)).y0(zero).y1((d) => y(d.y)).curve(curveMonotoneX);
|
|
50
|
+
const stroke = line().defined((d) => Number.isFinite(d.y)).x((d) => xCenter(xKind, x, d.x)).y((d) => y(d.y)).curve(curveMonotoneX);
|
|
51
|
+
const marks = [];
|
|
52
|
+
const points = [];
|
|
53
|
+
series.forEach((s, si) => {
|
|
54
|
+
marks.push({
|
|
55
|
+
kind: "path",
|
|
56
|
+
d: gen(s.values) ?? "",
|
|
57
|
+
fill: s.color,
|
|
58
|
+
fillOpacity: .25,
|
|
59
|
+
series: si
|
|
60
|
+
});
|
|
61
|
+
marks.push({
|
|
62
|
+
kind: "path",
|
|
63
|
+
d: stroke(s.values) ?? "",
|
|
64
|
+
fill: "none",
|
|
65
|
+
stroke: s.color,
|
|
66
|
+
strokeWidth: 2,
|
|
67
|
+
series: si
|
|
68
|
+
});
|
|
69
|
+
s.values.forEach((d, i) => {
|
|
70
|
+
points.push({
|
|
71
|
+
series: si,
|
|
72
|
+
index: i,
|
|
73
|
+
key: categoryKey(d.x),
|
|
74
|
+
cx: xCenter(xKind, x, d.x),
|
|
75
|
+
cy: y(d.y),
|
|
76
|
+
x: d.x,
|
|
77
|
+
y: d.y
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
marks,
|
|
83
|
+
points
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function scatterMarks(layout, series) {
|
|
87
|
+
const { xKind, x, y } = layout;
|
|
88
|
+
const marks = [];
|
|
89
|
+
const points = [];
|
|
90
|
+
series.forEach((s, si) => {
|
|
91
|
+
s.values.forEach((d, i) => {
|
|
92
|
+
const cx = xCenter(xKind, x, d.x);
|
|
93
|
+
const cy = y(d.y);
|
|
94
|
+
marks.push({
|
|
95
|
+
kind: "circle",
|
|
96
|
+
cx,
|
|
97
|
+
cy,
|
|
98
|
+
r: 3.5,
|
|
99
|
+
fill: d.color ?? s.color,
|
|
100
|
+
fillOpacity: .85,
|
|
101
|
+
series: si,
|
|
102
|
+
index: i
|
|
103
|
+
});
|
|
104
|
+
points.push({
|
|
105
|
+
series: si,
|
|
106
|
+
index: i,
|
|
107
|
+
key: categoryKey(d.x),
|
|
108
|
+
cx,
|
|
109
|
+
cy,
|
|
110
|
+
x: d.x,
|
|
111
|
+
y: d.y
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
return {
|
|
116
|
+
marks,
|
|
117
|
+
points
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function groupedBarMarks(layout, series) {
|
|
121
|
+
const { x, y } = layout;
|
|
122
|
+
const outer = x;
|
|
123
|
+
const inner = scaleBand().domain(series.map((_, i) => String(i))).range([0, outer.bandwidth()]).padding(.08);
|
|
124
|
+
const marks = [];
|
|
125
|
+
const points = [];
|
|
126
|
+
series.forEach((s, si) => {
|
|
127
|
+
const offset = inner(String(si)) ?? 0;
|
|
128
|
+
const width = inner.bandwidth();
|
|
129
|
+
s.values.forEach((d, i) => {
|
|
130
|
+
const band = outer(String(d.x));
|
|
131
|
+
if (band === void 0 || !Number.isFinite(d.y)) return;
|
|
132
|
+
const top = y(Math.max(0, d.y));
|
|
133
|
+
const bottom = y(Math.min(0, d.y));
|
|
134
|
+
marks.push({
|
|
135
|
+
kind: "rect",
|
|
136
|
+
x: band + offset,
|
|
137
|
+
y: top,
|
|
138
|
+
width,
|
|
139
|
+
height: Math.max(0, bottom - top),
|
|
140
|
+
fill: d.color ?? s.color,
|
|
141
|
+
series: si,
|
|
142
|
+
index: i
|
|
143
|
+
});
|
|
144
|
+
points.push({
|
|
145
|
+
series: si,
|
|
146
|
+
index: i,
|
|
147
|
+
key: categoryKey(d.x),
|
|
148
|
+
cx: band + offset + width / 2,
|
|
149
|
+
cy: y(d.y),
|
|
150
|
+
x: d.x,
|
|
151
|
+
y: d.y
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
return {
|
|
156
|
+
marks,
|
|
157
|
+
points
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/** Stacked bars accumulate per category — positive values stack up from the
|
|
161
|
+
* baseline, negatives down, so mixed-sign data stays correct. */
|
|
162
|
+
function stackedBarMarks(layout, series) {
|
|
163
|
+
const { x, y } = layout;
|
|
164
|
+
const outer = x;
|
|
165
|
+
const posBase = /* @__PURE__ */ new Map();
|
|
166
|
+
const negBase = /* @__PURE__ */ new Map();
|
|
167
|
+
const marks = [];
|
|
168
|
+
const points = [];
|
|
169
|
+
series.forEach((s, si) => {
|
|
170
|
+
s.values.forEach((d, i) => {
|
|
171
|
+
const cat = String(d.x);
|
|
172
|
+
const band = outer(cat);
|
|
173
|
+
if (band === void 0 || !Number.isFinite(d.y)) return;
|
|
174
|
+
const v0 = d.y >= 0 ? posBase.get(cat) ?? 0 : negBase.get(cat) ?? 0;
|
|
175
|
+
const v1 = v0 + d.y;
|
|
176
|
+
if (d.y >= 0) posBase.set(cat, v1);
|
|
177
|
+
else negBase.set(cat, v1);
|
|
178
|
+
const top = y(Math.max(v0, v1));
|
|
179
|
+
const bottom = y(Math.min(v0, v1));
|
|
180
|
+
marks.push({
|
|
181
|
+
kind: "rect",
|
|
182
|
+
x: band,
|
|
183
|
+
y: top,
|
|
184
|
+
width: outer.bandwidth(),
|
|
185
|
+
height: Math.max(0, bottom - top),
|
|
186
|
+
fill: d.color ?? s.color,
|
|
187
|
+
series: si,
|
|
188
|
+
index: i
|
|
189
|
+
});
|
|
190
|
+
points.push({
|
|
191
|
+
series: si,
|
|
192
|
+
index: i,
|
|
193
|
+
key: cat,
|
|
194
|
+
cx: band + outer.bandwidth() / 2,
|
|
195
|
+
cy: y(v1),
|
|
196
|
+
x: d.x,
|
|
197
|
+
y: d.y
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
return {
|
|
202
|
+
marks,
|
|
203
|
+
points
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/** Pie consumes every datum across series — each {x: label, y: value} is a
|
|
207
|
+
* slice; slice colors are categorical (palette by order) with datum.color
|
|
208
|
+
* overrides. A series color is ignored on pie — it would be monochrome. */
|
|
209
|
+
function pieMarks(layout, series, options) {
|
|
210
|
+
const { plot } = layout;
|
|
211
|
+
const flat = [];
|
|
212
|
+
series.forEach((s, si) => s.values.forEach((d, i) => flat.push({
|
|
213
|
+
d,
|
|
214
|
+
si,
|
|
215
|
+
i
|
|
216
|
+
})));
|
|
217
|
+
const cx = plot.width / 2;
|
|
218
|
+
const cy = plot.height / 2;
|
|
219
|
+
const rOuter = Math.max(0, Math.min(plot.width, plot.height) / 2);
|
|
220
|
+
const ratio = options.innerRadiusRatio ?? 0;
|
|
221
|
+
const rInner = rOuter * Math.min(Math.max(ratio, 0), .95);
|
|
222
|
+
const arcs = pie().value((v) => Math.max(0, v.d.y)).sort(null)(flat);
|
|
223
|
+
const gen = arc().innerRadius(rInner).outerRadius(rOuter);
|
|
224
|
+
const marks = [];
|
|
225
|
+
const slices = [];
|
|
226
|
+
arcs.forEach((a, order) => {
|
|
227
|
+
const color = a.data.d.color ?? options.palette[order % options.palette.length];
|
|
228
|
+
const centroid = gen.centroid(a);
|
|
229
|
+
marks.push({
|
|
230
|
+
kind: "path",
|
|
231
|
+
d: gen(a) ?? "",
|
|
232
|
+
transform: `translate(${cx} ${cy})`,
|
|
233
|
+
fill: color,
|
|
234
|
+
series: a.data.si,
|
|
235
|
+
index: a.data.i
|
|
236
|
+
});
|
|
237
|
+
slices.push({
|
|
238
|
+
series: a.data.si,
|
|
239
|
+
index: a.data.i,
|
|
240
|
+
label: String(a.data.d.x),
|
|
241
|
+
value: a.data.d.y,
|
|
242
|
+
color,
|
|
243
|
+
startAngle: a.startAngle,
|
|
244
|
+
endAngle: a.endAngle,
|
|
245
|
+
cx: centroid[0],
|
|
246
|
+
cy: centroid[1]
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
return {
|
|
250
|
+
marks,
|
|
251
|
+
points: [],
|
|
252
|
+
pie: {
|
|
253
|
+
cx,
|
|
254
|
+
cy,
|
|
255
|
+
rOuter,
|
|
256
|
+
rInner,
|
|
257
|
+
slices
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
//#endregion
|
|
262
|
+
export { buildMarks };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { extent } from "d3-array";
|
|
2
|
+
import { scaleBand, scaleLinear, scalePoint, scaleTime } from "d3-scale";
|
|
3
|
+
//#region src/core/scales.ts
|
|
4
|
+
/** Categorical x values across all series, first-seen order — the band/point
|
|
5
|
+
* domain. */
|
|
6
|
+
function categoryDomain(data) {
|
|
7
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8
|
+
const out = [];
|
|
9
|
+
for (const series of data) for (const d of series.values) {
|
|
10
|
+
const key = String(d.x);
|
|
11
|
+
if (!seen.has(key)) {
|
|
12
|
+
seen.add(key);
|
|
13
|
+
out.push(key);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
/** A categorical x lookup — band/point domains key on String(x). */
|
|
19
|
+
function categoryKey(x) {
|
|
20
|
+
return String(x);
|
|
21
|
+
}
|
|
22
|
+
/** Pick the x scale family: bar is always band, other cartesian types infer
|
|
23
|
+
* from the first datum — Date → time, string → point, number → linear. */
|
|
24
|
+
function inferXKind(type, data) {
|
|
25
|
+
if (type === "bar") return "band";
|
|
26
|
+
const first = data[0]?.values[0]?.x;
|
|
27
|
+
if (first instanceof Date) return "time";
|
|
28
|
+
if (typeof first === "string") return "point";
|
|
29
|
+
return "linear";
|
|
30
|
+
}
|
|
31
|
+
function buildXScale(kind, data, width) {
|
|
32
|
+
switch (kind) {
|
|
33
|
+
case "band": return scaleBand().domain(categoryDomain(data)).range([0, width]).paddingInner(.25).paddingOuter(.15);
|
|
34
|
+
case "point": {
|
|
35
|
+
const domain = categoryDomain(data);
|
|
36
|
+
return scalePoint().domain(domain).range([0, width]).padding(domain.length <= 1 ? 0 : .5);
|
|
37
|
+
}
|
|
38
|
+
case "time": {
|
|
39
|
+
const values = data.flatMap((s) => s.values.map((d) => d.x));
|
|
40
|
+
const [lo, hi] = extent(values);
|
|
41
|
+
if (lo === void 0 || hi === void 0) {
|
|
42
|
+
const now = /* @__PURE__ */ new Date();
|
|
43
|
+
return scaleTime().domain([now, now]).range([0, width]);
|
|
44
|
+
}
|
|
45
|
+
return scaleTime().domain(padDomain(+lo, +hi).map((v) => new Date(v))).range([0, width]).nice();
|
|
46
|
+
}
|
|
47
|
+
case "linear": {
|
|
48
|
+
const values = data.flatMap((s) => s.values.map((d) => Number(d.x)));
|
|
49
|
+
const [lo, hi] = extent(values);
|
|
50
|
+
return scaleLinear().domain(padDomain(lo ?? 0, hi ?? 1)).range([0, width]).nice();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Degenerate domains (one value) get a unit pad so the scale still maps
|
|
55
|
+
* somewhere sensible. */
|
|
56
|
+
function padDomain(lo, hi) {
|
|
57
|
+
return lo === hi ? [lo - .5, hi + .5] : [lo, hi];
|
|
58
|
+
}
|
|
59
|
+
/** Y domain: bar/area pin the zero baseline; line/scatter take the data
|
|
60
|
+
* extent; stacked bars sum per category (positive stack only — negative
|
|
61
|
+
* values in stacked mode stack below zero independently). */
|
|
62
|
+
function yDomain(data, type, stacked) {
|
|
63
|
+
if (stacked && type === "bar") {
|
|
64
|
+
const pos = /* @__PURE__ */ new Map();
|
|
65
|
+
const neg = /* @__PURE__ */ new Map();
|
|
66
|
+
for (const series of data) for (const d of series.values) {
|
|
67
|
+
const key = String(d.x);
|
|
68
|
+
if (d.y >= 0) pos.set(key, (pos.get(key) ?? 0) + d.y);
|
|
69
|
+
else neg.set(key, (neg.get(key) ?? 0) + d.y);
|
|
70
|
+
}
|
|
71
|
+
const hi = Math.max(0, ...pos.values());
|
|
72
|
+
const lo = Math.min(0, ...neg.values());
|
|
73
|
+
return lo === hi ? [0, 1] : [lo, hi];
|
|
74
|
+
}
|
|
75
|
+
const values = data.flatMap((s) => s.values.map((d) => d.y));
|
|
76
|
+
const [lo, hi] = extent(values);
|
|
77
|
+
const min = lo ?? 0;
|
|
78
|
+
const max = hi ?? 1;
|
|
79
|
+
if (type === "bar" || type === "area") {
|
|
80
|
+
const domain = [Math.min(0, min), Math.max(0, max)];
|
|
81
|
+
return domain[0] === domain[1] ? [0, 1] : domain;
|
|
82
|
+
}
|
|
83
|
+
return padDomain(min, max);
|
|
84
|
+
}
|
|
85
|
+
function buildYScale(domain, height) {
|
|
86
|
+
return scaleLinear().domain(domain).range([height, 0]).nice();
|
|
87
|
+
}
|
|
88
|
+
/** Pixel center of an x value — band offsets by half the band, point and
|
|
89
|
+
* continuous scales map directly. */
|
|
90
|
+
function xCenter(kind, scale, value) {
|
|
91
|
+
if (kind === "band") {
|
|
92
|
+
const band = scale;
|
|
93
|
+
return (band(String(value)) ?? NaN) + band.bandwidth() / 2;
|
|
94
|
+
}
|
|
95
|
+
if (kind === "point") return scale(String(value)) ?? NaN;
|
|
96
|
+
return scale(value);
|
|
97
|
+
}
|
|
98
|
+
//#endregion
|
|
99
|
+
export { buildXScale, buildYScale, categoryDomain, categoryKey, inferXKind, xCenter, yDomain };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { buildXScale, buildYScale, inferXKind, yDomain } from "./scales.js";
|
|
2
|
+
import { xAxisModel, yAxisModel } from "./axes.js";
|
|
3
|
+
import { buildMarks } from "./marks.js";
|
|
4
|
+
import { svgDocument } from "./svg.js";
|
|
5
|
+
//#region src/core/scene.ts
|
|
6
|
+
/** Default categorical palette — Tableau 10, embedded so the leaf stays free
|
|
7
|
+
* of d3-scale-chromatic. */
|
|
8
|
+
var PALETTE = [
|
|
9
|
+
"#4e79a7",
|
|
10
|
+
"#f28e2b",
|
|
11
|
+
"#e15759",
|
|
12
|
+
"#76b7b2",
|
|
13
|
+
"#59a14f",
|
|
14
|
+
"#edc948",
|
|
15
|
+
"#b07aa1",
|
|
16
|
+
"#ff9da7",
|
|
17
|
+
"#9c755f",
|
|
18
|
+
"#bab0ac"
|
|
19
|
+
];
|
|
20
|
+
function computeScene(props, width, height) {
|
|
21
|
+
if (!(width > 0) || !(height > 0) || !props.data?.length) return null;
|
|
22
|
+
const pie = props.type === "pie";
|
|
23
|
+
const xLabels = !pie && props.xAxis?.labels !== false;
|
|
24
|
+
const yLabels = !pie && props.yAxis?.labels !== false;
|
|
25
|
+
const margin = {
|
|
26
|
+
top: 8,
|
|
27
|
+
right: 10,
|
|
28
|
+
bottom: pie ? 8 : xLabels ? 26 : 10,
|
|
29
|
+
left: pie ? 8 : yLabels ? 46 : 10,
|
|
30
|
+
...props.margin
|
|
31
|
+
};
|
|
32
|
+
const plot = {
|
|
33
|
+
x: margin.left,
|
|
34
|
+
y: margin.top,
|
|
35
|
+
width: Math.max(0, width - margin.left - margin.right),
|
|
36
|
+
height: Math.max(0, height - margin.top - margin.bottom)
|
|
37
|
+
};
|
|
38
|
+
if (!(plot.width > 0) || !(plot.height > 0)) return null;
|
|
39
|
+
const series = props.data.map((s, i) => ({
|
|
40
|
+
name: s.name,
|
|
41
|
+
color: s.color ?? PALETTE[i % PALETTE.length],
|
|
42
|
+
values: s.values
|
|
43
|
+
}));
|
|
44
|
+
let marks;
|
|
45
|
+
let points;
|
|
46
|
+
let pieScene = null;
|
|
47
|
+
let labels = [];
|
|
48
|
+
let grid = [];
|
|
49
|
+
let frame = [];
|
|
50
|
+
if (pie) {
|
|
51
|
+
const built = buildMarks("pie", {
|
|
52
|
+
plot,
|
|
53
|
+
xKind: "band",
|
|
54
|
+
x: buildXScale("band", [], 0),
|
|
55
|
+
y: buildYScale([0, 1], 1)
|
|
56
|
+
}, series, {
|
|
57
|
+
innerRadiusRatio: props.innerRadiusRatio,
|
|
58
|
+
palette: PALETTE
|
|
59
|
+
});
|
|
60
|
+
marks = built.marks;
|
|
61
|
+
points = [];
|
|
62
|
+
const center = {
|
|
63
|
+
cx: built.pie.cx + plot.x,
|
|
64
|
+
cy: built.pie.cy + plot.y
|
|
65
|
+
};
|
|
66
|
+
pieScene = {
|
|
67
|
+
...built.pie,
|
|
68
|
+
...center,
|
|
69
|
+
slices: built.pie.slices.map((s) => ({
|
|
70
|
+
...s,
|
|
71
|
+
cx: s.cx + center.cx,
|
|
72
|
+
cy: s.cy + center.cy
|
|
73
|
+
}))
|
|
74
|
+
};
|
|
75
|
+
} else {
|
|
76
|
+
const xKind = inferXKind(props.type, props.data);
|
|
77
|
+
const layout = {
|
|
78
|
+
plot,
|
|
79
|
+
xKind,
|
|
80
|
+
x: buildXScale(xKind, props.data, plot.width),
|
|
81
|
+
y: buildYScale(yDomain(props.data, props.type, props.stacked === true), plot.height)
|
|
82
|
+
};
|
|
83
|
+
const built = buildMarks(props.type, layout, series, {
|
|
84
|
+
stacked: props.stacked,
|
|
85
|
+
palette: PALETTE
|
|
86
|
+
});
|
|
87
|
+
marks = built.marks;
|
|
88
|
+
points = built.points.map((p) => ({
|
|
89
|
+
...p,
|
|
90
|
+
cx: p.cx + plot.x,
|
|
91
|
+
cy: p.cy + plot.y
|
|
92
|
+
}));
|
|
93
|
+
const xAxis = xAxisModel(xKind, layout.x, props.xAxis, plot);
|
|
94
|
+
const yAxis = yAxisModel(layout.y, props.yAxis, plot);
|
|
95
|
+
labels = [...xAxis.labels, ...yAxis.labels];
|
|
96
|
+
grid = [...xAxis.grid, ...yAxis.grid];
|
|
97
|
+
frame = [...xAxis.frame, ...yAxis.frame];
|
|
98
|
+
}
|
|
99
|
+
const legend = pieScene ? pieScene.slices.map((s) => ({
|
|
100
|
+
color: s.color,
|
|
101
|
+
text: s.label
|
|
102
|
+
})) : series.map((s) => ({
|
|
103
|
+
color: s.color,
|
|
104
|
+
text: s.name
|
|
105
|
+
}));
|
|
106
|
+
return {
|
|
107
|
+
type: props.type,
|
|
108
|
+
width,
|
|
109
|
+
height,
|
|
110
|
+
plot,
|
|
111
|
+
markup: svgDocument(marks, grid, frame, plot, width, height),
|
|
112
|
+
labels,
|
|
113
|
+
legend,
|
|
114
|
+
series,
|
|
115
|
+
points,
|
|
116
|
+
pie: pieScene
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
export { PALETTE, computeScene };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//#region src/core/svg.ts
|
|
2
|
+
/** Mark[] → one `<svg>` markup string. Emits only the androidsvg ∩ SVGKit
|
|
3
|
+
* subset: `<svg>`/`<g>` structure, path/rect/circle/line shapes, solid fills
|
|
4
|
+
* and strokes, `translate` transforms, opacity attributes. Never `<text>`,
|
|
5
|
+
* filters, masks, patterns, or gradients — labels are real elements in the
|
|
6
|
+
* leaf overlay (docs/notes/primitive-notes.md). */
|
|
7
|
+
function num(v) {
|
|
8
|
+
if (!Number.isFinite(v)) return "0";
|
|
9
|
+
return Number.isInteger(v) ? String(v) : String(+v.toFixed(2));
|
|
10
|
+
}
|
|
11
|
+
function esc(v) {
|
|
12
|
+
return v.replaceAll("&", "&").replaceAll("\"", """).replaceAll("<", "<").replaceAll(">", ">");
|
|
13
|
+
}
|
|
14
|
+
function paintAttrs(m) {
|
|
15
|
+
let out = "";
|
|
16
|
+
if (m.fill !== void 0) out += ` fill="${esc(m.fill)}"`;
|
|
17
|
+
if (m.fillOpacity !== void 0) out += ` fill-opacity="${num(m.fillOpacity)}"`;
|
|
18
|
+
if (m.stroke !== void 0) out += ` stroke="${esc(m.stroke)}"`;
|
|
19
|
+
if (m.strokeOpacity !== void 0) out += ` stroke-opacity="${num(m.strokeOpacity)}"`;
|
|
20
|
+
if (m.strokeWidth !== void 0) out += ` stroke-width="${num(m.strokeWidth)}"`;
|
|
21
|
+
if (m.strokeLinecap !== void 0) out += ` stroke-linecap="${m.strokeLinecap}"`;
|
|
22
|
+
if (m.transform !== void 0) out += ` transform="${esc(m.transform)}"`;
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
function markXml(m) {
|
|
26
|
+
switch (m.kind) {
|
|
27
|
+
case "path": return `<path d="${esc(m.d)}"${paintAttrs(m)}/>`;
|
|
28
|
+
case "rect": return `<rect x="${num(m.x)}" y="${num(m.y)}" width="${num(m.width)}" height="${num(m.height)}"${paintAttrs(m)}/>`;
|
|
29
|
+
case "circle": return `<circle cx="${num(m.cx)}" cy="${num(m.cy)}" r="${num(m.r)}"${paintAttrs(m)}/>`;
|
|
30
|
+
case "line": return `<line x1="${num(m.x1)}" y1="${num(m.y1)}" x2="${num(m.x2)}" y2="${num(m.y2)}"${paintAttrs(m)}/>`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Chart marks are plot-space; axis rules are chart-space. Paint order:
|
|
34
|
+
* gridlines under the translated marks group, frame (spine + stubs) over. */
|
|
35
|
+
function svgDocument(marks, grid, frame, plot, width, height) {
|
|
36
|
+
const body = `${grid.map(markXml).join("")}<g transform="translate(${num(plot.x)} ${num(plot.y)})">${marks.map(markXml).join("")}</g>${frame.map(markXml).join("")}`;
|
|
37
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${num(width)} ${num(height)}" width="${num(width)}" height="${num(height)}">${body}</svg>`;
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
export { svgDocument };
|