ggaction 0.0.6 → 0.0.7
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/CHANGELOG.md +23 -0
- package/README.md +77 -14
- package/package.json +19 -2
- package/src/BasicChartProgram.js +10 -0
- package/src/actions/basic.js +21 -0
- package/src/actions/canvas/actions.js +42 -0
- package/src/actions/canvas/index.js +5 -1
- package/src/actions/charts/basic.js +13 -0
- package/src/actions/data/basic.js +11 -0
- package/src/actions/data/basicBin2d.js +43 -0
- package/src/actions/data/bin2d.js +1 -26
- package/src/actions/data/bin2dMaterialize.js +29 -0
- package/src/actions/encodings/appearance.js +6 -2
- package/src/actions/encodings/basic.js +21 -0
- package/src/actions/encodings/position/index.js +5 -1
- package/src/actions/encodings/ranged.js +6 -2
- package/src/actions/guides/axes/index.js +10 -2
- package/src/actions/guides/basic.js +19 -0
- package/src/actions/guides/grids/grid.js +7 -3
- package/src/actions/guides/grids/index.js +1 -1
- package/src/actions/guides/legends/categorical/index.js +6 -2
- package/src/actions/guides/legends/continuous/index.js +6 -2
- package/src/actions/marks/bar/index.js +5 -1
- package/src/actions/marks/basic.js +11 -0
- package/src/actions/marks/line/actions.js +5 -1
- package/src/actions/marks/line/index.js +4 -1
- package/src/actions/marks/point/index.js +5 -1
- package/src/actions/marks/rect/actions.js +5 -1
- package/src/actions/marks/rect/index.js +4 -1
- package/src/actions/scales/index.js +5 -1
- package/src/basic.js +2 -0
- package/src/renderers/canvas/index.js +96 -60
- package/src/renderers/canvas/text.js +3 -14
- package/src/renderers/pdf.js +112 -0
- package/src/renderers/svg.js +559 -0
- package/src/renderers/text.js +11 -0
- package/types/basic.d.ts +107 -0
- package/types/pdf.d.ts +26 -0
- package/types/svg.d.ts +11 -0
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
import {
|
|
2
|
+
requireSingleOrderedGraphicByType,
|
|
3
|
+
walkGraphicTreeEvents
|
|
4
|
+
} from "../grammar/schemas/graphicTree.js";
|
|
5
|
+
import {
|
|
6
|
+
validateConcreteGraphicProperties
|
|
7
|
+
} from "../grammar/schemas/concreteGraphic.js";
|
|
8
|
+
import { resolvePathCommandBounds } from
|
|
9
|
+
"../grammar/schemas/graphicBounds.js";
|
|
10
|
+
import {
|
|
11
|
+
isLinearGradientPaint,
|
|
12
|
+
resolveLinearGradientCoordinates,
|
|
13
|
+
validateFillPaint
|
|
14
|
+
} from "../grammar/paint.js";
|
|
15
|
+
import {
|
|
16
|
+
requireFiniteProperty,
|
|
17
|
+
requireStringProperty
|
|
18
|
+
} from "./canvas/validation.js";
|
|
19
|
+
import { normalizeRendererFontWeight } from "./text.js";
|
|
20
|
+
|
|
21
|
+
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
|
22
|
+
const SVG_OPTIONS = new Set(["title", "description"]);
|
|
23
|
+
const TEXT_ANCHORS = Object.freeze({
|
|
24
|
+
left: "start",
|
|
25
|
+
start: "start",
|
|
26
|
+
center: "middle",
|
|
27
|
+
right: "end",
|
|
28
|
+
end: "end"
|
|
29
|
+
});
|
|
30
|
+
const DOMINANT_BASELINES = Object.freeze({
|
|
31
|
+
top: "text-before-edge",
|
|
32
|
+
hanging: "hanging",
|
|
33
|
+
middle: "middle",
|
|
34
|
+
alphabetic: "alphabetic",
|
|
35
|
+
ideographic: "ideographic",
|
|
36
|
+
bottom: "text-after-edge"
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
function escapeText(value) {
|
|
40
|
+
return value
|
|
41
|
+
.replaceAll("&", "&")
|
|
42
|
+
.replaceAll("<", "<")
|
|
43
|
+
.replaceAll(">", ">");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function escapeAttribute(value) {
|
|
47
|
+
return escapeText(String(value))
|
|
48
|
+
.replaceAll("\"", """)
|
|
49
|
+
.replaceAll("'", "'");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function formatNumber(value) {
|
|
53
|
+
if (!Number.isFinite(value)) {
|
|
54
|
+
throw new TypeError("SVG renderer requires finite numeric output.");
|
|
55
|
+
}
|
|
56
|
+
return Object.is(value, -0) ? "0" : String(value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function attributes(entries) {
|
|
60
|
+
return entries
|
|
61
|
+
.filter(([, value]) => value !== undefined)
|
|
62
|
+
.map(([name, value]) => `${name}="${escapeAttribute(value)}"`)
|
|
63
|
+
.join(" ");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function element(name, entries, content) {
|
|
67
|
+
const serialized = attributes(entries);
|
|
68
|
+
const opening = serialized.length === 0 ? `<${name}` : `<${name} ${serialized}`;
|
|
69
|
+
if (content === undefined) return `${opening}/>`;
|
|
70
|
+
return `${opening}>${content}</${name}>`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function requireOpacity(properties, graphicId) {
|
|
74
|
+
const opacity = properties.opacity ?? 1;
|
|
75
|
+
if (!Number.isFinite(opacity) || opacity < 0 || opacity > 1) {
|
|
76
|
+
throw new Error(`Graphic "${graphicId}" requires opacity from 0 to 1.`);
|
|
77
|
+
}
|
|
78
|
+
return opacity;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function requireStrokeDash(properties, graphicId) {
|
|
82
|
+
const strokeDash = properties.strokeDash ?? [];
|
|
83
|
+
if (
|
|
84
|
+
!Array.isArray(strokeDash) ||
|
|
85
|
+
!strokeDash.every(value => Number.isFinite(value) && value >= 0)
|
|
86
|
+
) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Graphic "${graphicId}" requires a non-negative finite strokeDash array.`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return strokeDash;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function requireCanvasGeometry(id, canvas, { nested }) {
|
|
95
|
+
const properties = canvas.properties ?? {};
|
|
96
|
+
const width = requireFiniteProperty(properties, "width", id);
|
|
97
|
+
const height = requireFiniteProperty(properties, "height", id);
|
|
98
|
+
if (width < 0 || height < 0) {
|
|
99
|
+
const label = nested ? `Nested Canvas "${id}"` : "Canvas";
|
|
100
|
+
throw new Error(`${label} width and height must not be negative.`);
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
...(nested ? {
|
|
104
|
+
x: requireFiniteProperty(properties, "x", id),
|
|
105
|
+
y: requireFiniteProperty(properties, "y", id)
|
|
106
|
+
} : {}),
|
|
107
|
+
width,
|
|
108
|
+
height
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function requireCanvasBackground(id, canvas) {
|
|
113
|
+
const background = canvas.properties?.background;
|
|
114
|
+
if (background === undefined) return undefined;
|
|
115
|
+
if (typeof background !== "string") {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Graphic "${id}" requires a string background property.`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
return background;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function addLinearGradient(state, fill, bounds, graphicId) {
|
|
124
|
+
const coordinates = resolveLinearGradientCoordinates(fill, bounds);
|
|
125
|
+
const id = `ggaction-gradient-${state.gradientCount}`;
|
|
126
|
+
state.gradientCount += 1;
|
|
127
|
+
const stops = fill.stops.map(stop => element("stop", [
|
|
128
|
+
["offset", formatNumber(stop.offset)],
|
|
129
|
+
["stop-color", stop.color]
|
|
130
|
+
])).join("");
|
|
131
|
+
state.definitions.push(element("linearGradient", [
|
|
132
|
+
["id", id],
|
|
133
|
+
["gradientUnits", "userSpaceOnUse"],
|
|
134
|
+
["x1", formatNumber(coordinates.from.x)],
|
|
135
|
+
["y1", formatNumber(coordinates.from.y)],
|
|
136
|
+
["x2", formatNumber(coordinates.to.x)],
|
|
137
|
+
["y2", formatNumber(coordinates.to.y)]
|
|
138
|
+
], stops));
|
|
139
|
+
return `url(#${id})`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function serializeFill(state, fill, bounds, graphicId) {
|
|
143
|
+
validateFillPaint(fill, `${graphicId}.fill`);
|
|
144
|
+
if (!isLinearGradientPaint(fill)) return fill;
|
|
145
|
+
return addLinearGradient(state, fill, bounds, graphicId);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function serializeCircle(state, graphicId, properties) {
|
|
149
|
+
validateConcreteGraphicProperties("circle", properties);
|
|
150
|
+
const x = requireFiniteProperty(properties, "x", graphicId);
|
|
151
|
+
const y = requireFiniteProperty(properties, "y", graphicId);
|
|
152
|
+
const radius = requireFiniteProperty(properties, "radius", graphicId);
|
|
153
|
+
if (radius < 0) {
|
|
154
|
+
throw new Error(`Graphic "${graphicId}" requires a non-negative radius.`);
|
|
155
|
+
}
|
|
156
|
+
if (typeof properties.fill !== "string") {
|
|
157
|
+
throw new Error(`Graphic "${graphicId}" requires a string fill property.`);
|
|
158
|
+
}
|
|
159
|
+
let strokeWidth;
|
|
160
|
+
if (properties.stroke !== undefined) {
|
|
161
|
+
if (typeof properties.stroke !== "string") {
|
|
162
|
+
throw new Error(`Graphic "${graphicId}" requires a string stroke property.`);
|
|
163
|
+
}
|
|
164
|
+
strokeWidth = requireFiniteProperty(properties, "strokeWidth", graphicId);
|
|
165
|
+
if (strokeWidth < 0) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`Graphic "${graphicId}" requires a non-negative strokeWidth.`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return element("circle", [
|
|
172
|
+
["cx", formatNumber(x)],
|
|
173
|
+
["cy", formatNumber(y)],
|
|
174
|
+
["r", formatNumber(radius)],
|
|
175
|
+
["fill", properties.fill],
|
|
176
|
+
["stroke", properties.stroke],
|
|
177
|
+
["stroke-width", strokeWidth === undefined
|
|
178
|
+
? undefined
|
|
179
|
+
: formatNumber(strokeWidth)],
|
|
180
|
+
["opacity", formatNumber(requireOpacity(properties, graphicId))]
|
|
181
|
+
]);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function serializeRect(state, graphicId, properties) {
|
|
185
|
+
validateConcreteGraphicProperties("rect", properties);
|
|
186
|
+
const x = requireFiniteProperty(properties, "x", graphicId);
|
|
187
|
+
const y = requireFiniteProperty(properties, "y", graphicId);
|
|
188
|
+
const width = requireFiniteProperty(properties, "width", graphicId);
|
|
189
|
+
const height = requireFiniteProperty(properties, "height", graphicId);
|
|
190
|
+
const strokeWidth = requireFiniteProperty(
|
|
191
|
+
properties,
|
|
192
|
+
"strokeWidth",
|
|
193
|
+
graphicId
|
|
194
|
+
);
|
|
195
|
+
if (width < 0 || height < 0 || strokeWidth < 0) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`Graphic "${graphicId}" requires non-negative rect dimensions and strokeWidth.`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
if (properties.fill === undefined) {
|
|
201
|
+
throw new Error(`Graphic "${graphicId}" requires a fill property.`);
|
|
202
|
+
}
|
|
203
|
+
if (typeof properties.stroke !== "string") {
|
|
204
|
+
throw new Error(`Graphic "${graphicId}" requires a string stroke property.`);
|
|
205
|
+
}
|
|
206
|
+
const fill = serializeFill(state, properties.fill, {
|
|
207
|
+
left: x,
|
|
208
|
+
right: x + width,
|
|
209
|
+
top: y,
|
|
210
|
+
bottom: y + height
|
|
211
|
+
}, graphicId);
|
|
212
|
+
return element("rect", [
|
|
213
|
+
["x", formatNumber(x)],
|
|
214
|
+
["y", formatNumber(y)],
|
|
215
|
+
["width", formatNumber(width)],
|
|
216
|
+
["height", formatNumber(height)],
|
|
217
|
+
["fill", fill],
|
|
218
|
+
["stroke", properties.stroke],
|
|
219
|
+
["stroke-width", formatNumber(strokeWidth)],
|
|
220
|
+
["opacity", formatNumber(requireOpacity(properties, graphicId))]
|
|
221
|
+
]);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function serializeLine(state, graphicId, properties) {
|
|
225
|
+
validateConcreteGraphicProperties("line", properties);
|
|
226
|
+
const strokeWidth = requireFiniteProperty(
|
|
227
|
+
properties,
|
|
228
|
+
"strokeWidth",
|
|
229
|
+
graphicId
|
|
230
|
+
);
|
|
231
|
+
if (strokeWidth < 0) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`Graphic "${graphicId}" requires a non-negative strokeWidth.`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
if (typeof properties.stroke !== "string") {
|
|
237
|
+
throw new Error(`Graphic "${graphicId}" requires a string stroke property.`);
|
|
238
|
+
}
|
|
239
|
+
const strokeDash = requireStrokeDash(properties, graphicId);
|
|
240
|
+
return element("line", [
|
|
241
|
+
["x1", formatNumber(requireFiniteProperty(properties, "x1", graphicId))],
|
|
242
|
+
["y1", formatNumber(requireFiniteProperty(properties, "y1", graphicId))],
|
|
243
|
+
["x2", formatNumber(requireFiniteProperty(properties, "x2", graphicId))],
|
|
244
|
+
["y2", formatNumber(requireFiniteProperty(properties, "y2", graphicId))],
|
|
245
|
+
["fill", "none"],
|
|
246
|
+
["stroke", properties.stroke],
|
|
247
|
+
["stroke-width", formatNumber(strokeWidth)],
|
|
248
|
+
["stroke-dasharray", strokeDash.length === 0
|
|
249
|
+
? undefined
|
|
250
|
+
: strokeDash.map(formatNumber).join(" ")],
|
|
251
|
+
["opacity", formatNumber(requireOpacity(properties, graphicId))]
|
|
252
|
+
]);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function serializePathData(commands) {
|
|
256
|
+
return commands.map(command => {
|
|
257
|
+
if (command.op === "M" || command.op === "L") {
|
|
258
|
+
return `${command.op}${formatNumber(command.x)} ${formatNumber(command.y)}`;
|
|
259
|
+
}
|
|
260
|
+
if (command.op === "C") {
|
|
261
|
+
return `C${formatNumber(command.x1)} ${formatNumber(command.y1)} ` +
|
|
262
|
+
`${formatNumber(command.x2)} ${formatNumber(command.y2)} ` +
|
|
263
|
+
`${formatNumber(command.x)} ${formatNumber(command.y)}`;
|
|
264
|
+
}
|
|
265
|
+
return "Z";
|
|
266
|
+
}).join(" ");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function serializePath(state, graphicId, properties) {
|
|
270
|
+
validateConcreteGraphicProperties("path", properties);
|
|
271
|
+
const commands = properties.commands;
|
|
272
|
+
if (!Array.isArray(commands)) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
`Graphic "${graphicId}" requires concrete path commands.`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
const hasFill = properties.fill !== undefined;
|
|
278
|
+
const hasStroke = properties.stroke !== undefined;
|
|
279
|
+
if (!hasFill && !hasStroke) {
|
|
280
|
+
throw new Error(`Graphic "${graphicId}" requires a fill or stroke property.`);
|
|
281
|
+
}
|
|
282
|
+
if (hasFill && commands.at(-1).op !== "Z") {
|
|
283
|
+
throw new Error(`Graphic "${graphicId}" requires a final Z command when filled.`);
|
|
284
|
+
}
|
|
285
|
+
if (hasStroke && typeof properties.stroke !== "string") {
|
|
286
|
+
throw new Error(`Graphic "${graphicId}" requires a string stroke property.`);
|
|
287
|
+
}
|
|
288
|
+
const strokeWidth = hasStroke
|
|
289
|
+
? requireFiniteProperty(properties, "strokeWidth", graphicId)
|
|
290
|
+
: undefined;
|
|
291
|
+
if (hasStroke && strokeWidth < 0) {
|
|
292
|
+
throw new Error(
|
|
293
|
+
`Graphic "${graphicId}" requires a non-negative strokeWidth.`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
const strokeDash = requireStrokeDash(properties, graphicId);
|
|
297
|
+
let fill = "none";
|
|
298
|
+
if (hasFill) {
|
|
299
|
+
const bounds = resolvePathCommandBounds(commands);
|
|
300
|
+
if (bounds === undefined) {
|
|
301
|
+
throw new Error(`Graphic "${graphicId}" requires finite path fill bounds.`);
|
|
302
|
+
}
|
|
303
|
+
fill = serializeFill(state, properties.fill, bounds, graphicId);
|
|
304
|
+
}
|
|
305
|
+
return element("path", [
|
|
306
|
+
["d", serializePathData(commands)],
|
|
307
|
+
["fill", fill],
|
|
308
|
+
["stroke", properties.stroke],
|
|
309
|
+
["stroke-width", strokeWidth === undefined
|
|
310
|
+
? undefined
|
|
311
|
+
: formatNumber(strokeWidth)],
|
|
312
|
+
["stroke-dasharray", strokeDash.length === 0
|
|
313
|
+
? undefined
|
|
314
|
+
: strokeDash.map(formatNumber).join(" ")],
|
|
315
|
+
["opacity", formatNumber(requireOpacity(properties, graphicId))]
|
|
316
|
+
]);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function serializeText(state, graphicId, properties) {
|
|
320
|
+
validateConcreteGraphicProperties("text", properties);
|
|
321
|
+
const x = requireFiniteProperty(properties, "x", graphicId);
|
|
322
|
+
const y = requireFiniteProperty(properties, "y", graphicId);
|
|
323
|
+
const fontSize = requireFiniteProperty(properties, "fontSize", graphicId);
|
|
324
|
+
const rotation = properties.rotation ?? 0;
|
|
325
|
+
const opacity = requireOpacity(properties, graphicId);
|
|
326
|
+
const text = requireStringProperty(properties, "text", graphicId);
|
|
327
|
+
const fill = requireStringProperty(properties, "fill", graphicId);
|
|
328
|
+
const fontFamily = requireStringProperty(
|
|
329
|
+
properties,
|
|
330
|
+
"fontFamily",
|
|
331
|
+
graphicId
|
|
332
|
+
);
|
|
333
|
+
const textAlign = requireStringProperty(
|
|
334
|
+
properties,
|
|
335
|
+
"textAlign",
|
|
336
|
+
graphicId
|
|
337
|
+
);
|
|
338
|
+
const textBaseline = requireStringProperty(
|
|
339
|
+
properties,
|
|
340
|
+
"textBaseline",
|
|
341
|
+
graphicId
|
|
342
|
+
);
|
|
343
|
+
const fontWeight = properties.fontWeight ?? "normal";
|
|
344
|
+
if (fontSize <= 0) {
|
|
345
|
+
throw new Error(`Graphic "${graphicId}" requires a positive fontSize.`);
|
|
346
|
+
}
|
|
347
|
+
if (!Number.isFinite(rotation)) {
|
|
348
|
+
throw new Error(`Graphic "${graphicId}" requires a finite rotation.`);
|
|
349
|
+
}
|
|
350
|
+
if (
|
|
351
|
+
!(
|
|
352
|
+
(typeof fontWeight === "string" && fontWeight.length > 0) ||
|
|
353
|
+
Number.isFinite(fontWeight)
|
|
354
|
+
)
|
|
355
|
+
) {
|
|
356
|
+
throw new Error(`Graphic "${graphicId}" requires a valid fontWeight.`);
|
|
357
|
+
}
|
|
358
|
+
if (TEXT_ANCHORS[textAlign] === undefined) {
|
|
359
|
+
throw new Error(`Graphic "${graphicId}" has invalid textAlign.`);
|
|
360
|
+
}
|
|
361
|
+
if (DOMINANT_BASELINES[textBaseline] === undefined) {
|
|
362
|
+
throw new Error(`Graphic "${graphicId}" has invalid textBaseline.`);
|
|
363
|
+
}
|
|
364
|
+
const rotationDegrees = rotation * 180 / Math.PI;
|
|
365
|
+
return element("text", [
|
|
366
|
+
["x", formatNumber(x)],
|
|
367
|
+
["y", formatNumber(y)],
|
|
368
|
+
["fill", fill],
|
|
369
|
+
["font-family", fontFamily],
|
|
370
|
+
["font-size", formatNumber(fontSize)],
|
|
371
|
+
["font-weight", normalizeRendererFontWeight(fontWeight)],
|
|
372
|
+
["text-anchor", TEXT_ANCHORS[textAlign]],
|
|
373
|
+
["dominant-baseline", DOMINANT_BASELINES[textBaseline]],
|
|
374
|
+
["opacity", formatNumber(opacity)],
|
|
375
|
+
["transform", rotation === 0
|
|
376
|
+
? undefined
|
|
377
|
+
: `rotate(${formatNumber(rotationDegrees)} ${formatNumber(x)} ${formatNumber(y)})`]
|
|
378
|
+
], escapeText(text));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function serializeConcreteGraphic(state, id, graphic) {
|
|
382
|
+
if (graphic.type === "collection") {
|
|
383
|
+
const items = (graphic.items ?? []).map(item =>
|
|
384
|
+
serializeConcreteGraphic(state, item.id ?? id, item)
|
|
385
|
+
).join("");
|
|
386
|
+
return element("g", [], items);
|
|
387
|
+
}
|
|
388
|
+
const properties = graphic.properties ?? {};
|
|
389
|
+
if (graphic.type === "circle") {
|
|
390
|
+
return serializeCircle(state, id, properties);
|
|
391
|
+
}
|
|
392
|
+
if (graphic.type === "rect") {
|
|
393
|
+
return serializeRect(state, id, properties);
|
|
394
|
+
}
|
|
395
|
+
if (graphic.type === "line") {
|
|
396
|
+
return serializeLine(state, id, properties);
|
|
397
|
+
}
|
|
398
|
+
if (graphic.type === "path") {
|
|
399
|
+
return serializePath(state, id, properties);
|
|
400
|
+
}
|
|
401
|
+
if (graphic.type === "text") {
|
|
402
|
+
return serializeText(state, id, properties);
|
|
403
|
+
}
|
|
404
|
+
throw new Error(`SVG renderer does not support "${graphic.type}" yet.`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function openNestedCanvas(state, id, canvas) {
|
|
408
|
+
const { x, y, width, height } = requireCanvasGeometry(
|
|
409
|
+
id,
|
|
410
|
+
canvas,
|
|
411
|
+
{ nested: true }
|
|
412
|
+
);
|
|
413
|
+
const clipId = `ggaction-clip-${state.clipCount}`;
|
|
414
|
+
state.clipCount += 1;
|
|
415
|
+
state.definitions.push(element("clipPath", [["id", clipId]], element(
|
|
416
|
+
"rect",
|
|
417
|
+
[
|
|
418
|
+
["x", "0"],
|
|
419
|
+
["y", "0"],
|
|
420
|
+
["width", formatNumber(width)],
|
|
421
|
+
["height", formatNumber(height)]
|
|
422
|
+
]
|
|
423
|
+
)));
|
|
424
|
+
const background = requireCanvasBackground(id, canvas);
|
|
425
|
+
const backgroundRect = background === undefined
|
|
426
|
+
? ""
|
|
427
|
+
: element("rect", [
|
|
428
|
+
["x", "0"],
|
|
429
|
+
["y", "0"],
|
|
430
|
+
["width", formatNumber(width)],
|
|
431
|
+
["height", formatNumber(height)],
|
|
432
|
+
["fill", background]
|
|
433
|
+
]);
|
|
434
|
+
return `<g ${attributes([
|
|
435
|
+
["transform", `translate(${formatNumber(x)} ${formatNumber(y)})`],
|
|
436
|
+
["clip-path", `url(#${clipId})`]
|
|
437
|
+
])}>${backgroundRect}`;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function serializeBody(graphicSpec, rootId, rootCanvas, width, height, state) {
|
|
441
|
+
const output = [];
|
|
442
|
+
const background = requireCanvasBackground(rootId, rootCanvas);
|
|
443
|
+
if (background !== undefined) {
|
|
444
|
+
output.push(element("rect", [
|
|
445
|
+
["x", "0"],
|
|
446
|
+
["y", "0"],
|
|
447
|
+
["width", formatNumber(width)],
|
|
448
|
+
["height", formatNumber(height)],
|
|
449
|
+
["fill", background]
|
|
450
|
+
]));
|
|
451
|
+
}
|
|
452
|
+
walkGraphicTreeEvents(graphicSpec, {
|
|
453
|
+
enter({ id, object }) {
|
|
454
|
+
if (id === rootId) return;
|
|
455
|
+
if (object.type === "canvas") {
|
|
456
|
+
output.push(openNestedCanvas(state, id, object));
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (object.type === "collection") {
|
|
460
|
+
output.push("<g>");
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (!Array.isArray(object.items)) {
|
|
464
|
+
output.push(serializeConcreteGraphic(state, id, object));
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
item({ id, object, owner }) {
|
|
468
|
+
output.push(serializeConcreteGraphic(state, id, {
|
|
469
|
+
...object,
|
|
470
|
+
type: object.type ?? owner.type
|
|
471
|
+
}));
|
|
472
|
+
},
|
|
473
|
+
exit({ id, object }) {
|
|
474
|
+
if (
|
|
475
|
+
id !== rootId &&
|
|
476
|
+
(object.type === "collection" || object.type === "canvas")
|
|
477
|
+
) {
|
|
478
|
+
output.push("</g>");
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
return output.join("");
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function requireSVGOptions(options) {
|
|
486
|
+
if (
|
|
487
|
+
options === null ||
|
|
488
|
+
typeof options !== "object" ||
|
|
489
|
+
Array.isArray(options) ||
|
|
490
|
+
Object.getPrototypeOf(options) !== Object.prototype
|
|
491
|
+
) {
|
|
492
|
+
throw new TypeError("renderToSVG options must be a plain object.");
|
|
493
|
+
}
|
|
494
|
+
for (const key of Object.keys(options)) {
|
|
495
|
+
if (!SVG_OPTIONS.has(key)) {
|
|
496
|
+
throw new TypeError(`renderToSVG does not support option "${key}".`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
for (const key of SVG_OPTIONS) {
|
|
500
|
+
if (
|
|
501
|
+
options[key] !== undefined &&
|
|
502
|
+
(typeof options[key] !== "string" || options[key].length === 0)
|
|
503
|
+
) {
|
|
504
|
+
throw new TypeError(`renderToSVG ${key} must be a non-empty string.`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return options;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export function renderToSVG(program, options = {}) {
|
|
511
|
+
const resolvedOptions = requireSVGOptions(options);
|
|
512
|
+
const graphicSpec = program?.graphicSpec;
|
|
513
|
+
if (
|
|
514
|
+
graphicSpec === null ||
|
|
515
|
+
typeof graphicSpec !== "object" ||
|
|
516
|
+
graphicSpec.objects === null ||
|
|
517
|
+
typeof graphicSpec.objects !== "object" ||
|
|
518
|
+
!Array.isArray(graphicSpec.order)
|
|
519
|
+
) {
|
|
520
|
+
throw new TypeError("renderToSVG requires a program with a graphicSpec.");
|
|
521
|
+
}
|
|
522
|
+
const { id: rootId, object: rootCanvas } =
|
|
523
|
+
requireSingleOrderedGraphicByType(graphicSpec, "canvas");
|
|
524
|
+
const { width, height } = requireCanvasGeometry(
|
|
525
|
+
rootId,
|
|
526
|
+
rootCanvas,
|
|
527
|
+
{ nested: false }
|
|
528
|
+
);
|
|
529
|
+
const state = {
|
|
530
|
+
clipCount: 1,
|
|
531
|
+
definitions: [],
|
|
532
|
+
gradientCount: 1
|
|
533
|
+
};
|
|
534
|
+
const body = serializeBody(
|
|
535
|
+
graphicSpec,
|
|
536
|
+
rootId,
|
|
537
|
+
rootCanvas,
|
|
538
|
+
width,
|
|
539
|
+
height,
|
|
540
|
+
state
|
|
541
|
+
);
|
|
542
|
+
const accessible = [
|
|
543
|
+
resolvedOptions.title === undefined
|
|
544
|
+
? ""
|
|
545
|
+
: element("title", [], escapeText(resolvedOptions.title)),
|
|
546
|
+
resolvedOptions.description === undefined
|
|
547
|
+
? ""
|
|
548
|
+
: element("desc", [], escapeText(resolvedOptions.description))
|
|
549
|
+
].join("");
|
|
550
|
+
const definitions = state.definitions.length === 0
|
|
551
|
+
? ""
|
|
552
|
+
: element("defs", [], state.definitions.join(""));
|
|
553
|
+
return element("svg", [
|
|
554
|
+
["xmlns", SVG_NAMESPACE],
|
|
555
|
+
["width", formatNumber(width)],
|
|
556
|
+
["height", formatNumber(height)],
|
|
557
|
+
["viewBox", `0 0 ${formatNumber(width)} ${formatNumber(height)}`]
|
|
558
|
+
], `${accessible}${definitions}${body}`);
|
|
559
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const MIN_RENDERER_NUMERIC_FONT_WEIGHT = 100;
|
|
2
|
+
const MAX_RENDERER_NUMERIC_FONT_WEIGHT = 900;
|
|
3
|
+
|
|
4
|
+
export function normalizeRendererFontWeight(fontWeight) {
|
|
5
|
+
if (typeof fontWeight !== "number") return fontWeight;
|
|
6
|
+
const rounded = Math.round(fontWeight / 100) * 100;
|
|
7
|
+
return Math.min(
|
|
8
|
+
MAX_RENDERER_NUMERIC_FONT_WEIGHT,
|
|
9
|
+
Math.max(MIN_RENDERER_NUMERIC_FONT_WEIGHT, rounded)
|
|
10
|
+
);
|
|
11
|
+
}
|
package/types/basic.d.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { ChartProgram } from "./program.js";
|
|
2
|
+
|
|
3
|
+
export type {
|
|
4
|
+
AxisLabelOptions,
|
|
5
|
+
AxisLineStyleOptions,
|
|
6
|
+
AxisTickOptions,
|
|
7
|
+
AxisTicksAndLabelsOptions,
|
|
8
|
+
AxisTitleOptions,
|
|
9
|
+
BarWidthOptions,
|
|
10
|
+
Bin2DDataOptions,
|
|
11
|
+
CanvasOptions,
|
|
12
|
+
ColorEncodingOptions,
|
|
13
|
+
CompleteAxisOptions,
|
|
14
|
+
CreateAxesOptions,
|
|
15
|
+
CreateBarPlotOptions,
|
|
16
|
+
CreateGridOptions,
|
|
17
|
+
CreateGuidesOptions,
|
|
18
|
+
CreateHeatmapOptions,
|
|
19
|
+
CreateHistogramOptions,
|
|
20
|
+
CreateLinePlotOptions,
|
|
21
|
+
CreateScatterPlotOptions,
|
|
22
|
+
CreateScaleOptions,
|
|
23
|
+
GraphicSpec,
|
|
24
|
+
GridDirectionOptions,
|
|
25
|
+
HistogramEncodingOptions,
|
|
26
|
+
LegendOptions,
|
|
27
|
+
PositionEncodingOptions,
|
|
28
|
+
RectMarkOptions,
|
|
29
|
+
ScaleOptions,
|
|
30
|
+
SecondaryPositionEncodingOptions,
|
|
31
|
+
SemanticSpec,
|
|
32
|
+
StrokeDashEncodingOptions,
|
|
33
|
+
TraceNode,
|
|
34
|
+
XAxisPosition,
|
|
35
|
+
XOffsetEncodingOptions,
|
|
36
|
+
YAxisPosition
|
|
37
|
+
} from "./program.js";
|
|
38
|
+
|
|
39
|
+
type BasicStateKey =
|
|
40
|
+
| "semanticSpec"
|
|
41
|
+
| "graphicSpec"
|
|
42
|
+
| "resolvedScales"
|
|
43
|
+
| "materializationConfigs"
|
|
44
|
+
| "context"
|
|
45
|
+
| "trace"
|
|
46
|
+
| "actionStack";
|
|
47
|
+
|
|
48
|
+
type BasicMethodKey =
|
|
49
|
+
| "createCanvas"
|
|
50
|
+
| "createData"
|
|
51
|
+
| "createPointMark"
|
|
52
|
+
| "createLineMark"
|
|
53
|
+
| "createBarMark"
|
|
54
|
+
| "createRectMark"
|
|
55
|
+
| "encodeX"
|
|
56
|
+
| "encodeY"
|
|
57
|
+
| "encodeX2"
|
|
58
|
+
| "encodeY2"
|
|
59
|
+
| "encodeGroup"
|
|
60
|
+
| "encodeColor"
|
|
61
|
+
| "encodeStrokeDash"
|
|
62
|
+
| "encodeSize"
|
|
63
|
+
| "encodeShape"
|
|
64
|
+
| "encodeXOffset"
|
|
65
|
+
| "encodeYOffset"
|
|
66
|
+
| "encodeHistogram"
|
|
67
|
+
| "encodeBarWidth"
|
|
68
|
+
| "createCoordinate"
|
|
69
|
+
| "createScale"
|
|
70
|
+
| "createScatterPlot"
|
|
71
|
+
| "createLinePlot"
|
|
72
|
+
| "createBarPlot"
|
|
73
|
+
| "createHistogram"
|
|
74
|
+
| "createHeatmap"
|
|
75
|
+
| "createAxes"
|
|
76
|
+
| "createXAxis"
|
|
77
|
+
| "createYAxis"
|
|
78
|
+
| "createXAxisLine"
|
|
79
|
+
| "createYAxisLine"
|
|
80
|
+
| "createXAxisTicks"
|
|
81
|
+
| "createYAxisTicks"
|
|
82
|
+
| "createXAxisLabels"
|
|
83
|
+
| "createYAxisLabels"
|
|
84
|
+
| "createXAxisTicksAndLabels"
|
|
85
|
+
| "createYAxisTicksAndLabels"
|
|
86
|
+
| "createXAxisTitle"
|
|
87
|
+
| "createYAxisTitle"
|
|
88
|
+
| "createGrid"
|
|
89
|
+
| "createHorizontalGrid"
|
|
90
|
+
| "createVerticalGrid"
|
|
91
|
+
| "createLegend"
|
|
92
|
+
| "createGuides";
|
|
93
|
+
|
|
94
|
+
type RebindMethod<T> = T extends (...args: infer Args) => ChartProgram
|
|
95
|
+
? (...args: Args) => BasicChartProgram
|
|
96
|
+
: never;
|
|
97
|
+
|
|
98
|
+
export type BasicChartProgram =
|
|
99
|
+
Pick<ChartProgram, BasicStateKey> &
|
|
100
|
+
{ readonly [Key in BasicMethodKey]: RebindMethod<ChartProgram[Key]> };
|
|
101
|
+
|
|
102
|
+
export function chart(): BasicChartProgram;
|
|
103
|
+
export function render(
|
|
104
|
+
program: Pick<BasicChartProgram, "graphicSpec">,
|
|
105
|
+
context: CanvasRenderingContext2D,
|
|
106
|
+
options?: { pixelRatio?: number }
|
|
107
|
+
): void;
|
package/types/pdf.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ChartProgram } from "./program.js";
|
|
2
|
+
|
|
3
|
+
export interface PDFMetadata {
|
|
4
|
+
readonly title?: string;
|
|
5
|
+
readonly author?: string;
|
|
6
|
+
readonly subject?: string;
|
|
7
|
+
readonly keywords?: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface PDFRenderOptions {
|
|
11
|
+
readonly output: string;
|
|
12
|
+
readonly metadata?: PDFMetadata;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface PDFRenderResult {
|
|
16
|
+
readonly output: string;
|
|
17
|
+
readonly width: number;
|
|
18
|
+
readonly height: number;
|
|
19
|
+
readonly pages: 1;
|
|
20
|
+
readonly bytes: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function renderToPDF(
|
|
24
|
+
program: Pick<ChartProgram, "graphicSpec">,
|
|
25
|
+
options: PDFRenderOptions
|
|
26
|
+
): Promise<PDFRenderResult>;
|