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.
Files changed (39) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +77 -14
  3. package/package.json +19 -2
  4. package/src/BasicChartProgram.js +10 -0
  5. package/src/actions/basic.js +21 -0
  6. package/src/actions/canvas/actions.js +42 -0
  7. package/src/actions/canvas/index.js +5 -1
  8. package/src/actions/charts/basic.js +13 -0
  9. package/src/actions/data/basic.js +11 -0
  10. package/src/actions/data/basicBin2d.js +43 -0
  11. package/src/actions/data/bin2d.js +1 -26
  12. package/src/actions/data/bin2dMaterialize.js +29 -0
  13. package/src/actions/encodings/appearance.js +6 -2
  14. package/src/actions/encodings/basic.js +21 -0
  15. package/src/actions/encodings/position/index.js +5 -1
  16. package/src/actions/encodings/ranged.js +6 -2
  17. package/src/actions/guides/axes/index.js +10 -2
  18. package/src/actions/guides/basic.js +19 -0
  19. package/src/actions/guides/grids/grid.js +7 -3
  20. package/src/actions/guides/grids/index.js +1 -1
  21. package/src/actions/guides/legends/categorical/index.js +6 -2
  22. package/src/actions/guides/legends/continuous/index.js +6 -2
  23. package/src/actions/marks/bar/index.js +5 -1
  24. package/src/actions/marks/basic.js +11 -0
  25. package/src/actions/marks/line/actions.js +5 -1
  26. package/src/actions/marks/line/index.js +4 -1
  27. package/src/actions/marks/point/index.js +5 -1
  28. package/src/actions/marks/rect/actions.js +5 -1
  29. package/src/actions/marks/rect/index.js +4 -1
  30. package/src/actions/scales/index.js +5 -1
  31. package/src/basic.js +2 -0
  32. package/src/renderers/canvas/index.js +96 -60
  33. package/src/renderers/canvas/text.js +3 -14
  34. package/src/renderers/pdf.js +112 -0
  35. package/src/renderers/svg.js +559 -0
  36. package/src/renderers/text.js +11 -0
  37. package/types/basic.d.ts +107 -0
  38. package/types/pdf.d.ts +26 -0
  39. package/types/svg.d.ts +11 -0
@@ -25,9 +25,8 @@ import {
25
25
  } from "./symbols.js";
26
26
  import { editLegend } from "../edit.js";
27
27
 
28
- export function registerCategoricalLegendActions(ProgramClass) {
28
+ export function registerBasicCategoricalLegendActions(ProgramClass) {
29
29
  ProgramClass.prototype.createLegend = createLegend;
30
- ProgramClass.prototype.editLegend = editLegend;
31
30
  ProgramClass.prototype.createCategoricalLegend = createCategoricalLegend;
32
31
  ProgramClass.prototype.removeCategoricalLegend = removeCategoricalLegend;
33
32
  ProgramClass.prototype.createLegendBackground = createLegendBackground;
@@ -47,3 +46,8 @@ export function registerCategoricalLegendActions(ProgramClass) {
47
46
  ProgramClass.prototype.rematerializeLegendTitle = rematerializeLegendTitle;
48
47
  ProgramClass.prototype.rematerializeLegend = rematerializeLegend;
49
48
  }
49
+
50
+ export function registerCategoricalLegendActions(ProgramClass) {
51
+ registerBasicCategoricalLegendActions(ProgramClass);
52
+ ProgramClass.prototype.editLegend = editLegend;
53
+ }
@@ -15,11 +15,15 @@ export {
15
15
  export { createIntervalLegend, rematerializeIntervalLegend } from "./interval.js";
16
16
 
17
17
  export function registerContinuousLegendActions(ProgramClass) {
18
- ProgramClass.prototype.createGradientLegend = createGradientLegend;
19
- ProgramClass.prototype.rematerializeGradientLegend = rematerializeGradientLegend;
18
+ registerGradientLegendActions(ProgramClass);
20
19
  ProgramClass.prototype.createOpacityLegend = createOpacityLegend;
21
20
  ProgramClass.prototype.rematerializeOpacityLegend = rematerializeOpacityLegend;
22
21
  ProgramClass.prototype.removeOpacityLegend = removeOpacityLegend;
23
22
  ProgramClass.prototype.createIntervalLegend = createIntervalLegend;
24
23
  ProgramClass.prototype.rematerializeIntervalLegend = rematerializeIntervalLegend;
25
24
  }
25
+
26
+ export function registerGradientLegendActions(ProgramClass) {
27
+ ProgramClass.prototype.createGradientLegend = createGradientLegend;
28
+ ProgramClass.prototype.rematerializeGradientLegend = rematerializeGradientLegend;
29
+ }
@@ -3,7 +3,11 @@ import { editBarMark } from "./edit.js";
3
3
  import { rematerializeBarMark } from "./materialize.js";
4
4
 
5
5
  export function registerBarMarkActions(ProgramClass) {
6
- ProgramClass.prototype.createBarMark = createBarMark;
7
6
  ProgramClass.prototype.editBarMark = editBarMark;
7
+ registerBasicBarMarkActions(ProgramClass);
8
+ }
9
+
10
+ export function registerBasicBarMarkActions(ProgramClass) {
11
+ ProgramClass.prototype.createBarMark = createBarMark;
8
12
  ProgramClass.prototype.rematerializeBarMark = rematerializeBarMark;
9
13
  }
@@ -0,0 +1,11 @@
1
+ import { registerBarMarkActions } from "./bar/index.js";
2
+ import { registerLineMarkActions } from "./line/index.js";
3
+ import { registerPointMarkActions } from "./point/index.js";
4
+ import { registerRectMarkActions } from "./rect/index.js";
5
+
6
+ export function registerBasicMarkActions(ProgramClass) {
7
+ registerPointMarkActions(ProgramClass);
8
+ registerLineMarkActions(ProgramClass);
9
+ registerBarMarkActions(ProgramClass);
10
+ registerRectMarkActions(ProgramClass);
11
+ }
@@ -386,7 +386,11 @@ const editLineMark = action(
386
386
  );
387
387
 
388
388
  export function registerLineMarkActions(ProgramClass) {
389
- ProgramClass.prototype.createLineMark = createLineMark;
390
389
  ProgramClass.prototype.editLineMark = editLineMark;
390
+ registerBasicLineMarkActions(ProgramClass);
391
+ }
392
+
393
+ export function registerBasicLineMarkActions(ProgramClass) {
394
+ ProgramClass.prototype.createLineMark = createLineMark;
391
395
  ProgramClass.prototype.rematerializeLineMark = rematerializeLineMark;
392
396
  }
@@ -1 +1,4 @@
1
- export { registerLineMarkActions } from "./actions.js";
1
+ export {
2
+ registerBasicLineMarkActions,
3
+ registerLineMarkActions
4
+ } from "./actions.js";
@@ -5,7 +5,11 @@ import { rematerializePointMark } from "./materialize.js";
5
5
  export { registerPointJitterActions } from "./jitter.js";
6
6
 
7
7
  export function registerPointMarkActions(ProgramClass) {
8
- ProgramClass.prototype.createPointMark = createPointMark;
9
8
  ProgramClass.prototype.editPointMark = editPointMark;
9
+ registerBasicPointMarkActions(ProgramClass);
10
+ }
11
+
12
+ export function registerBasicPointMarkActions(ProgramClass) {
13
+ ProgramClass.prototype.createPointMark = createPointMark;
10
14
  ProgramClass.prototype.rematerializePointMark = rematerializePointMark;
11
15
  }
@@ -162,7 +162,11 @@ const editRectMark = action(
162
162
  );
163
163
 
164
164
  export function registerRectMarkActions(ProgramClass) {
165
- ProgramClass.prototype.createRectMark = createRectMark;
166
165
  ProgramClass.prototype.editRectMark = editRectMark;
166
+ registerBasicRectMarkActions(ProgramClass);
167
+ }
168
+
169
+ export function registerBasicRectMarkActions(ProgramClass) {
170
+ ProgramClass.prototype.createRectMark = createRectMark;
167
171
  ProgramClass.prototype.rematerializeRectMark = rematerializeRectMark;
168
172
  }
@@ -1 +1,4 @@
1
- export { registerRectMarkActions } from "./actions.js";
1
+ export {
2
+ registerBasicRectMarkActions,
3
+ registerRectMarkActions
4
+ } from "./actions.js";
@@ -4,8 +4,12 @@ import { rematerializeScale } from "./materialize.js";
4
4
  import { setQuantitativeColorScale } from "./quantitativeColor.js";
5
5
 
6
6
  export function registerScaleActions(ProgramClass) {
7
- ProgramClass.prototype.createScale = createScale;
8
7
  ProgramClass.prototype.editScale = editScale;
8
+ registerBasicScaleActions(ProgramClass);
9
+ }
10
+
11
+ export function registerBasicScaleActions(ProgramClass) {
12
+ ProgramClass.prototype.createScale = createScale;
9
13
  ProgramClass.prototype.rematerializeScale = rematerializeScale;
10
14
  ProgramClass.prototype.setQuantitativeColorScale = setQuantitativeColorScale;
11
15
  }
package/src/basic.js ADDED
@@ -0,0 +1,2 @@
1
+ export { chart } from "./BasicChartProgram.js";
2
+ export { render } from "./renderers/canvas/index.js";
@@ -17,32 +17,43 @@ const DRAWERS = Object.freeze({
17
17
  path: drawPathGraphic
18
18
  });
19
19
 
20
- function requireCanvasContext(context) {
21
- const methods = [
22
- "save",
23
- "restore",
24
- "clearRect",
25
- "fillRect",
26
- "beginPath",
27
- "closePath",
28
- "arc",
29
- "fill",
30
- "moveTo",
31
- "lineTo",
32
- "bezierCurveTo",
33
- "setLineDash",
34
- "stroke",
35
- "translate",
36
- "rotate",
37
- "fillText",
38
- "scale"
39
- ];
20
+ const DRAWING_CONTEXT_METHODS = Object.freeze([
21
+ "save",
22
+ "restore",
23
+ "fillRect",
24
+ "beginPath",
25
+ "closePath",
26
+ "arc",
27
+ "fill",
28
+ "moveTo",
29
+ "lineTo",
30
+ "bezierCurveTo",
31
+ "setLineDash",
32
+ "stroke",
33
+ "translate",
34
+ "rotate",
35
+ "fillText"
36
+ ]);
37
+
38
+ function requireDrawingContext(context) {
39
+ if (context === null || typeof context !== "object") {
40
+ throw new TypeError("Canvas drawing requires a 2D context.");
41
+ }
42
+
43
+ for (const method of DRAWING_CONTEXT_METHODS) {
44
+ if (typeof context[method] !== "function") {
45
+ throw new TypeError(`Canvas context is missing ${method}().`);
46
+ }
47
+ }
48
+ }
40
49
 
50
+ function requireCanvasContext(context) {
41
51
  if (context === null || typeof context !== "object" || !context.canvas) {
42
52
  throw new TypeError("render requires a Canvas 2D context.");
43
53
  }
44
54
 
45
- for (const method of methods) {
55
+ requireDrawingContext(context);
56
+ for (const method of ["clearRect", "scale"]) {
46
57
  if (typeof context[method] !== "function") {
47
58
  throw new TypeError(`Canvas context is missing ${method}().`);
48
59
  }
@@ -86,7 +97,7 @@ function enterNestedCanvas(context, id, canvas) {
86
97
  fillCanvasBackground(context, id, canvas, width, height);
87
98
  }
88
99
 
89
- export function render(program, context, { pixelRatio = 1 } = {}) {
100
+ export function requireProgramGraphicSpec(program) {
90
101
  const graphicSpec = program?.graphicSpec;
91
102
 
92
103
  if (
@@ -99,12 +110,10 @@ export function render(program, context, { pixelRatio = 1 } = {}) {
99
110
  throw new TypeError("render requires a program with a graphicSpec.");
100
111
  }
101
112
 
102
- requireCanvasContext(context);
103
-
104
- if (!Number.isFinite(pixelRatio) || pixelRatio <= 0) {
105
- throw new RangeError("render pixelRatio must be a positive finite number.");
106
- }
113
+ return graphicSpec;
114
+ }
107
115
 
116
+ export function resolveGraphicRenderTarget(graphicSpec) {
108
117
  const { id: canvasId, object: canvas } =
109
118
  requireSingleOrderedGraphicByType(graphicSpec, "canvas");
110
119
  const width = requireFiniteProperty(canvas.properties ?? {}, "width", canvasId);
@@ -118,6 +127,65 @@ export function render(program, context, { pixelRatio = 1 } = {}) {
118
127
  throw new Error("Canvas width and height must not be negative.");
119
128
  }
120
129
 
130
+ return Object.freeze({
131
+ graphicSpec,
132
+ canvasId,
133
+ canvas,
134
+ width,
135
+ height
136
+ });
137
+ }
138
+
139
+ export function drawResolvedGraphicSpec(target, context) {
140
+ requireDrawingContext(context);
141
+ const { graphicSpec, canvasId, canvas, width, height } = target;
142
+
143
+ fillCanvasBackground(context, canvasId, canvas, width, height);
144
+
145
+ walkGraphicTreeEvents(graphicSpec, {
146
+ enter({ id, object }) {
147
+ if (id === canvasId) return;
148
+ if (object.type === "canvas") {
149
+ enterNestedCanvas(context, id, object);
150
+ return;
151
+ }
152
+ if (object.type === "collection") {
153
+ context.save();
154
+ return;
155
+ }
156
+ if (!Array.isArray(object.items)) {
157
+ drawConcreteGraphic(context, id, object);
158
+ }
159
+ },
160
+ item({ id, object, owner }) {
161
+ drawConcreteGraphic(context, id, {
162
+ ...object,
163
+ type: object.type ?? owner.type
164
+ });
165
+ },
166
+ exit({ id, object }) {
167
+ if (
168
+ id !== canvasId &&
169
+ (object.type === "collection" || object.type === "canvas")
170
+ ) {
171
+ context.restore();
172
+ }
173
+ }
174
+ });
175
+ }
176
+
177
+ export function render(program, context, { pixelRatio = 1 } = {}) {
178
+ const graphicSpec = requireProgramGraphicSpec(program);
179
+
180
+ requireCanvasContext(context);
181
+
182
+ if (!Number.isFinite(pixelRatio) || pixelRatio <= 0) {
183
+ throw new RangeError("render pixelRatio must be a positive finite number.");
184
+ }
185
+
186
+ const target = resolveGraphicRenderTarget(graphicSpec);
187
+ const { width, height } = target;
188
+
121
189
  context.canvas.width = Math.round(width * pixelRatio);
122
190
  context.canvas.height = Math.round(height * pixelRatio);
123
191
  if (
@@ -132,39 +200,7 @@ export function render(program, context, { pixelRatio = 1 } = {}) {
132
200
  try {
133
201
  context.scale(pixelRatio, pixelRatio);
134
202
  context.clearRect(0, 0, width, height);
135
-
136
- fillCanvasBackground(context, canvasId, canvas, width, height);
137
-
138
- walkGraphicTreeEvents(graphicSpec, {
139
- enter({ id, object }) {
140
- if (id === canvasId) return;
141
- if (object.type === "canvas") {
142
- enterNestedCanvas(context, id, object);
143
- return;
144
- }
145
- if (object.type === "collection") {
146
- context.save();
147
- return;
148
- }
149
- if (!Array.isArray(object.items)) {
150
- drawConcreteGraphic(context, id, object);
151
- }
152
- },
153
- item({ id, object, owner }) {
154
- drawConcreteGraphic(context, id, {
155
- ...object,
156
- type: object.type ?? owner.type
157
- });
158
- },
159
- exit({ id, object }) {
160
- if (
161
- id !== canvasId &&
162
- (object.type === "collection" || object.type === "canvas")
163
- ) {
164
- context.restore();
165
- }
166
- }
167
- });
203
+ drawResolvedGraphicSpec(target, context);
168
204
  } finally {
169
205
  context.restore();
170
206
  }
@@ -1,18 +1,7 @@
1
1
  import { requireFiniteProperty, requireStringProperty } from "./validation.js";
2
2
  import { validateConcreteGraphicProperties } from
3
3
  "../../grammar/schemas/concreteGraphic.js";
4
-
5
- const MIN_CANVAS_NUMERIC_FONT_WEIGHT = 100;
6
- const MAX_CANVAS_NUMERIC_FONT_WEIGHT = 900;
7
-
8
- export function normalizeCanvasFontWeight(fontWeight) {
9
- if (typeof fontWeight !== "number") return fontWeight;
10
- const rounded = Math.round(fontWeight / 100) * 100;
11
- return Math.min(
12
- MAX_CANVAS_NUMERIC_FONT_WEIGHT,
13
- Math.max(MIN_CANVAS_NUMERIC_FONT_WEIGHT, rounded)
14
- );
15
- }
4
+ import { normalizeRendererFontWeight } from "../text.js";
16
5
 
17
6
  function drawText(context, child, collectionId) {
18
7
  const properties = child.properties ?? {};
@@ -85,8 +74,8 @@ function drawText(context, child, collectionId) {
85
74
  try {
86
75
  context.fillStyle = fill;
87
76
  context.globalAlpha = opacity;
88
- const canvasFontWeight = normalizeCanvasFontWeight(fontWeight);
89
- context.font = `${canvasFontWeight} ${fontSize}px ${fontFamily}`;
77
+ const rendererFontWeight = normalizeRendererFontWeight(fontWeight);
78
+ context.font = `${rendererFontWeight} ${fontSize}px ${fontFamily}`;
90
79
  context.textAlign = textAlign;
91
80
  context.textBaseline = textBaseline;
92
81
  context.translate(x, y);
@@ -0,0 +1,112 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { dirname, resolve } from "node:path";
3
+
4
+ import { PDFDocument } from "@napi-rs/canvas";
5
+
6
+ import {
7
+ drawResolvedGraphicSpec,
8
+ requireProgramGraphicSpec,
9
+ resolveGraphicRenderTarget
10
+ } from "./canvas/index.js";
11
+
12
+ const PDF_OPTIONS = new Set(["output", "metadata"]);
13
+ const PDF_METADATA = new Set([
14
+ "title",
15
+ "author",
16
+ "subject",
17
+ "keywords"
18
+ ]);
19
+
20
+ function requirePlainObject(value, label) {
21
+ if (
22
+ value === null ||
23
+ typeof value !== "object" ||
24
+ Array.isArray(value) ||
25
+ Object.getPrototypeOf(value) !== Object.prototype
26
+ ) {
27
+ throw new TypeError(`${label} must be a plain object.`);
28
+ }
29
+ return value;
30
+ }
31
+
32
+ function requireClosedKeys(value, allowed, label) {
33
+ for (const key of Object.keys(value)) {
34
+ if (!allowed.has(key)) {
35
+ throw new TypeError(`${label} does not support option "${key}".`);
36
+ }
37
+ }
38
+ }
39
+
40
+ function requireMetadata(metadata) {
41
+ if (metadata === undefined) return undefined;
42
+ requirePlainObject(metadata, "renderToPDF metadata");
43
+ requireClosedKeys(metadata, PDF_METADATA, "renderToPDF metadata");
44
+
45
+ const resolved = {};
46
+ for (const key of ["title", "author", "subject"]) {
47
+ const value = metadata[key];
48
+ if (
49
+ value !== undefined &&
50
+ (typeof value !== "string" || value.length === 0)
51
+ ) {
52
+ throw new TypeError(
53
+ `renderToPDF metadata ${key} must be a non-empty string.`
54
+ );
55
+ }
56
+ if (value !== undefined) resolved[key] = value;
57
+ }
58
+
59
+ if (metadata.keywords !== undefined) {
60
+ if (
61
+ !Array.isArray(metadata.keywords) ||
62
+ !metadata.keywords.every(keyword =>
63
+ typeof keyword === "string" && keyword.length > 0
64
+ )
65
+ ) {
66
+ throw new TypeError(
67
+ "renderToPDF metadata keywords must be an array of non-empty strings."
68
+ );
69
+ }
70
+ resolved.keywords = metadata.keywords.join(", ");
71
+ }
72
+
73
+ return resolved;
74
+ }
75
+
76
+ function requirePDFOptions(options) {
77
+ requirePlainObject(options, "renderToPDF options");
78
+ requireClosedKeys(options, PDF_OPTIONS, "renderToPDF");
79
+ if (typeof options.output !== "string" || options.output.length === 0) {
80
+ throw new TypeError("renderToPDF requires a non-empty output path.");
81
+ }
82
+ return {
83
+ output: resolve(options.output),
84
+ metadata: requireMetadata(options.metadata)
85
+ };
86
+ }
87
+
88
+ function renderPDFBuffer(target, metadata) {
89
+ const document = new PDFDocument(metadata);
90
+ const context = document.beginPage(target.width, target.height);
91
+ drawResolvedGraphicSpec(target, context);
92
+ document.endPage();
93
+ return document.close();
94
+ }
95
+
96
+ export async function renderToPDF(program, options = {}) {
97
+ const { output, metadata } = requirePDFOptions(options);
98
+ const graphicSpec = requireProgramGraphicSpec(program);
99
+ const target = resolveGraphicRenderTarget(graphicSpec);
100
+ const buffer = renderPDFBuffer(target, metadata);
101
+
102
+ await mkdir(dirname(output), { recursive: true });
103
+ await writeFile(output, buffer);
104
+
105
+ return Object.freeze({
106
+ output,
107
+ width: target.width,
108
+ height: target.height,
109
+ pages: 1,
110
+ bytes: buffer.length
111
+ });
112
+ }