rgrid-python 4.5.3__py3-none-any.whl
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.
- grid_py/__init__.py +340 -0
- grid_py/_arrow.py +331 -0
- grid_py/_clippath.py +170 -0
- grid_py/_colour.py +815 -0
- grid_py/_coords.py +1534 -0
- grid_py/_curve.py +1668 -0
- grid_py/_display_list.py +507 -0
- grid_py/_draw.py +1397 -0
- grid_py/_edit.py +756 -0
- grid_py/_font_metrics.py +319 -0
- grid_py/_gpar.py +572 -0
- grid_py/_grab.py +501 -0
- grid_py/_grob.py +1377 -0
- grid_py/_group.py +798 -0
- grid_py/_highlevel.py +2176 -0
- grid_py/_just.py +361 -0
- grid_py/_layout.py +593 -0
- grid_py/_ls.py +895 -0
- grid_py/_mask.py +196 -0
- grid_py/_path.py +414 -0
- grid_py/_patterns.py +1049 -0
- grid_py/_primitives.py +2198 -0
- grid_py/_renderer_base.py +1184 -0
- grid_py/_scene_graph.py +248 -0
- grid_py/_size.py +1352 -0
- grid_py/_state.py +683 -0
- grid_py/_transforms.py +448 -0
- grid_py/_typeset.py +384 -0
- grid_py/_units.py +1924 -0
- grid_py/_utils.py +310 -0
- grid_py/_viewport.py +1649 -0
- grid_py/_vp_calc.py +970 -0
- grid_py/py.typed +0 -0
- grid_py/renderer.py +1762 -0
- grid_py/renderer_web.py +764 -0
- grid_py/resources/d3.v7.min.js +2 -0
- grid_py/resources/gridpy.css +80 -0
- grid_py/resources/gridpy.js +813 -0
- rgrid_python-4.5.3.dist-info/METADATA +489 -0
- rgrid_python-4.5.3.dist-info/RECORD +42 -0
- rgrid_python-4.5.3.dist-info/WHEEL +4 -0
- rgrid_python-4.5.3.dist-info/licenses/LICENSE +3 -0
|
@@ -0,0 +1,813 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gridpy.js — Browser-side rendering runtime for grid_py WebRenderer.
|
|
3
|
+
*
|
|
4
|
+
* Reads a scene graph JSON and renders it to layered SVG + Canvas.
|
|
5
|
+
* Uses D3.js v7 for SVG manipulation and interactions (zoom, brush, tooltip).
|
|
6
|
+
*
|
|
7
|
+
* @version 0.1.0
|
|
8
|
+
* @license MIT
|
|
9
|
+
*/
|
|
10
|
+
var gridpy = (function () {
|
|
11
|
+
"use strict";
|
|
12
|
+
|
|
13
|
+
// ---- Configuration ---------------------------------------------------
|
|
14
|
+
|
|
15
|
+
var CANVAS_THRESHOLD = 2000; // Elements above this route to Canvas
|
|
16
|
+
var TOOLTIP_SEARCH_RADIUS = 10; // px
|
|
17
|
+
|
|
18
|
+
// ---- Gpar → CSS/Canvas style helpers ----------------------------------
|
|
19
|
+
|
|
20
|
+
function parseColour(c) {
|
|
21
|
+
if (!c || c === "transparent" || c === "NA" || c === "none") return null;
|
|
22
|
+
return c; // CSS can handle hex, named colours, rgba()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function applyGparSvg(sel, gpar) {
|
|
26
|
+
if (!gpar) return;
|
|
27
|
+
var fill = parseColour(gpar.fill);
|
|
28
|
+
var col = parseColour(gpar.col);
|
|
29
|
+
if (fill !== undefined) sel.attr("fill", fill || "none");
|
|
30
|
+
if (col !== undefined) sel.attr("stroke", col || "none");
|
|
31
|
+
if (gpar.lwd !== undefined) sel.attr("stroke-width", gpar.lwd);
|
|
32
|
+
if (gpar.alpha !== undefined) sel.attr("opacity", gpar.alpha);
|
|
33
|
+
if (gpar.lty) sel.attr("stroke-dasharray", ltyToDash(gpar.lty));
|
|
34
|
+
if (gpar.lineend) sel.attr("stroke-linecap", gpar.lineend);
|
|
35
|
+
if (gpar.linejoin) sel.attr("stroke-linejoin",
|
|
36
|
+
gpar.linejoin === "mitre" ? "miter" : gpar.linejoin);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function applyTextGparSvg(sel, gpar) {
|
|
40
|
+
if (!gpar) return;
|
|
41
|
+
var col = parseColour(gpar.col);
|
|
42
|
+
if (col) sel.attr("fill", col);
|
|
43
|
+
if (gpar.fontsize) sel.attr("font-size", gpar.fontsize + "px");
|
|
44
|
+
if (gpar.fontfamily) sel.attr("font-family", gpar.fontfamily);
|
|
45
|
+
if (gpar.fontface) {
|
|
46
|
+
var face = gpar.fontface;
|
|
47
|
+
if (face === "bold" || face === 2) sel.attr("font-weight", "bold");
|
|
48
|
+
else if (face === "italic" || face === 3) sel.attr("font-style", "italic");
|
|
49
|
+
else if (face === "bold.italic" || face === 4) {
|
|
50
|
+
sel.attr("font-weight", "bold").attr("font-style", "italic");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (gpar.alpha !== undefined) sel.attr("opacity", gpar.alpha);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function applyGparCanvas(ctx, gpar) {
|
|
57
|
+
if (!gpar) return;
|
|
58
|
+
var fill = parseColour(gpar.fill);
|
|
59
|
+
var col = parseColour(gpar.col);
|
|
60
|
+
ctx.fillStyle = fill || "rgba(0,0,0,0)";
|
|
61
|
+
ctx.strokeStyle = col || "rgba(0,0,0,0)";
|
|
62
|
+
ctx.lineWidth = gpar.lwd || 1;
|
|
63
|
+
ctx.globalAlpha = gpar.alpha !== undefined ? gpar.alpha : 1.0;
|
|
64
|
+
if (gpar.lineend) ctx.lineCap = gpar.lineend;
|
|
65
|
+
if (gpar.linejoin) ctx.lineJoin =
|
|
66
|
+
gpar.linejoin === "mitre" ? "miter" : (gpar.linejoin || "round");
|
|
67
|
+
if (gpar.lty) {
|
|
68
|
+
var dash = ltyToDashArray(gpar.lty);
|
|
69
|
+
ctx.setLineDash(dash || []);
|
|
70
|
+
} else {
|
|
71
|
+
ctx.setLineDash([]);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function ltyToDash(lty) {
|
|
76
|
+
var map = {
|
|
77
|
+
"dashed": "6,4", "dotted": "2,2",
|
|
78
|
+
"dotdash": "2,2,6,2", "longdash": "10,3",
|
|
79
|
+
"twodash": "5,2,10,2", "blank": "0,100"
|
|
80
|
+
};
|
|
81
|
+
return map[lty] || null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function ltyToDashArray(lty) {
|
|
85
|
+
var map = {
|
|
86
|
+
"dashed": [6, 4], "dotted": [2, 2],
|
|
87
|
+
"dotdash": [2, 2, 6, 2], "longdash": [10, 3],
|
|
88
|
+
"twodash": [5, 2, 10, 2], "blank": [0, 100]
|
|
89
|
+
};
|
|
90
|
+
return map[lty] || [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function hjustToAnchor(hj) {
|
|
94
|
+
if (hj <= 0.2) return "start";
|
|
95
|
+
if (hj >= 0.8) return "end";
|
|
96
|
+
return "middle";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function vjustToBaseline(vj) {
|
|
100
|
+
if (vj <= 0.2) return "text-after-edge";
|
|
101
|
+
if (vj >= 0.8) return "text-before-edge";
|
|
102
|
+
return "central";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---- Gradient/pattern → SVG defs --------------------------------------
|
|
106
|
+
|
|
107
|
+
function createDefs(defsSel, defsData) {
|
|
108
|
+
if (!defsData) return;
|
|
109
|
+
|
|
110
|
+
// Clip paths
|
|
111
|
+
(defsData.clip_paths || []).forEach(function (cp) {
|
|
112
|
+
defsSel.append("clipPath").attr("id", cp.id)
|
|
113
|
+
.append("rect")
|
|
114
|
+
.attr("x", cp.x).attr("y", cp.y)
|
|
115
|
+
.attr("width", cp.w).attr("height", cp.h);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Linear / radial gradients
|
|
119
|
+
(defsData.gradients || []).forEach(function (g) {
|
|
120
|
+
var el;
|
|
121
|
+
if (g.type === "linear") {
|
|
122
|
+
el = defsSel.append("linearGradient").attr("id", g.id)
|
|
123
|
+
.attr("x1", g.x1).attr("y1", g.y1)
|
|
124
|
+
.attr("x2", g.x2).attr("y2", g.y2)
|
|
125
|
+
.attr("gradientUnits", "userSpaceOnUse");
|
|
126
|
+
} else {
|
|
127
|
+
el = defsSel.append("radialGradient").attr("id", g.id)
|
|
128
|
+
.attr("cx", g.cx2).attr("cy", g.cy2).attr("r", g.r2)
|
|
129
|
+
.attr("fx", g.cx1).attr("fy", g.cy1).attr("fr", g.r1)
|
|
130
|
+
.attr("gradientUnits", "userSpaceOnUse");
|
|
131
|
+
}
|
|
132
|
+
if (g.colours && g.stops) {
|
|
133
|
+
for (var i = 0; i < g.colours.length; i++) {
|
|
134
|
+
el.append("stop")
|
|
135
|
+
.attr("offset", g.stops[i])
|
|
136
|
+
.attr("stop-color", g.colours[i]);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Patterns
|
|
142
|
+
(defsData.patterns || []).forEach(function (p) {
|
|
143
|
+
defsSel.append("pattern").attr("id", p.id)
|
|
144
|
+
.attr("x", p.x || 0).attr("y", p.y || 0)
|
|
145
|
+
.attr("width", p.width).attr("height", p.height)
|
|
146
|
+
.attr("patternUnits", "userSpaceOnUse")
|
|
147
|
+
.html(p.content || "");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Masks
|
|
151
|
+
(defsData.masks || []).forEach(function (m) {
|
|
152
|
+
var mask = defsSel.append("mask").attr("id", m.id);
|
|
153
|
+
if (m.content && m.content.children) {
|
|
154
|
+
m.content.children.forEach(function (child) {
|
|
155
|
+
renderSvgNode(child, mask);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---- Routing ----------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
function routeToLayer(node) {
|
|
164
|
+
if (node.render_hint === "svg") return "svg";
|
|
165
|
+
if (node.render_hint === "canvas") return "canvas";
|
|
166
|
+
// Auto-routing
|
|
167
|
+
if (node.type === "text") return "svg";
|
|
168
|
+
if (node.type === "raster") return "svg";
|
|
169
|
+
if (node.type === "points") {
|
|
170
|
+
var n = node.props.x ? node.props.x.length : 0;
|
|
171
|
+
return n > CANVAS_THRESHOLD ? "canvas" : "svg";
|
|
172
|
+
}
|
|
173
|
+
return "svg";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ---- SVG rendering ----------------------------------------------------
|
|
177
|
+
|
|
178
|
+
function renderSvgNode(node, parentG, state) {
|
|
179
|
+
if (!node) return;
|
|
180
|
+
if (node.type === "viewport") {
|
|
181
|
+
renderSvgViewport(node, parentG, state);
|
|
182
|
+
} else {
|
|
183
|
+
drawSvgGrub(node, parentG, state);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function renderSvgViewport(vpNode, parentG, state) {
|
|
188
|
+
var g = parentG.append("g")
|
|
189
|
+
.attr("class", "vp-" + (vpNode.name || ""))
|
|
190
|
+
.attr("data-viewport", vpNode.name || "");
|
|
191
|
+
if (vpNode.clip_id) {
|
|
192
|
+
g.attr("clip-path", "url(#" + vpNode.clip_id + ")");
|
|
193
|
+
}
|
|
194
|
+
if (vpNode.mask_id) {
|
|
195
|
+
g.attr("mask", "url(#" + vpNode.mask_id + ")");
|
|
196
|
+
}
|
|
197
|
+
(vpNode.children || []).forEach(function (child) {
|
|
198
|
+
renderSvgNode(child, g, state);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function drawSvgGrub(node, parentG, state) {
|
|
203
|
+
var p = node.props || {};
|
|
204
|
+
switch (node.type) {
|
|
205
|
+
case "rect":
|
|
206
|
+
parentG.append("rect")
|
|
207
|
+
.attr("x", p.x).attr("y", p.y)
|
|
208
|
+
.attr("width", p.w).attr("height", p.h)
|
|
209
|
+
.attr("data-id", node.id)
|
|
210
|
+
.call(applyGparSvg, node.gpar);
|
|
211
|
+
break;
|
|
212
|
+
|
|
213
|
+
case "roundrect":
|
|
214
|
+
parentG.append("rect")
|
|
215
|
+
.attr("x", p.x).attr("y", p.y)
|
|
216
|
+
.attr("width", p.w).attr("height", p.h)
|
|
217
|
+
.attr("rx", p.r || 0).attr("ry", p.r || 0)
|
|
218
|
+
.attr("data-id", node.id)
|
|
219
|
+
.call(applyGparSvg, node.gpar);
|
|
220
|
+
break;
|
|
221
|
+
|
|
222
|
+
case "circle":
|
|
223
|
+
parentG.append("circle")
|
|
224
|
+
.attr("cx", p.x).attr("cy", p.y).attr("r", p.r)
|
|
225
|
+
.attr("data-id", node.id)
|
|
226
|
+
.call(applyGparSvg, node.gpar);
|
|
227
|
+
break;
|
|
228
|
+
|
|
229
|
+
case "text":
|
|
230
|
+
var txt = parentG.append("text")
|
|
231
|
+
.attr("x", p.x).attr("y", p.y)
|
|
232
|
+
.attr("text-anchor", hjustToAnchor(p.hjust || 0.5))
|
|
233
|
+
.attr("dominant-baseline", vjustToBaseline(p.vjust || 0.5))
|
|
234
|
+
.text(p.label || "")
|
|
235
|
+
.attr("data-id", node.id)
|
|
236
|
+
.call(applyTextGparSvg, node.gpar);
|
|
237
|
+
if (p.rot) {
|
|
238
|
+
txt.attr("transform",
|
|
239
|
+
"rotate(" + (-p.rot) + "," + p.x + "," + p.y + ")");
|
|
240
|
+
}
|
|
241
|
+
break;
|
|
242
|
+
|
|
243
|
+
case "points":
|
|
244
|
+
drawSvgPoints(node, parentG, state);
|
|
245
|
+
break;
|
|
246
|
+
|
|
247
|
+
case "polyline":
|
|
248
|
+
case "lines":
|
|
249
|
+
drawSvgPolyline(node, parentG);
|
|
250
|
+
break;
|
|
251
|
+
|
|
252
|
+
case "segments":
|
|
253
|
+
drawSvgSegments(node, parentG);
|
|
254
|
+
break;
|
|
255
|
+
|
|
256
|
+
case "polygon":
|
|
257
|
+
drawSvgPolygon(node, parentG);
|
|
258
|
+
break;
|
|
259
|
+
|
|
260
|
+
case "path":
|
|
261
|
+
drawSvgPath(node, parentG);
|
|
262
|
+
break;
|
|
263
|
+
|
|
264
|
+
case "raster":
|
|
265
|
+
parentG.append("image")
|
|
266
|
+
.attr("x", p.x).attr("y", p.y)
|
|
267
|
+
.attr("width", p.w).attr("height", p.h)
|
|
268
|
+
.attr("href", p.src)
|
|
269
|
+
.attr("preserveAspectRatio", "none")
|
|
270
|
+
.attr("data-id", node.id);
|
|
271
|
+
break;
|
|
272
|
+
|
|
273
|
+
case "compound_stroke":
|
|
274
|
+
case "compound_fill":
|
|
275
|
+
case "compound_fill_stroke":
|
|
276
|
+
drawSvgCompound(node, parentG, state);
|
|
277
|
+
break;
|
|
278
|
+
|
|
279
|
+
default:
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function drawSvgPoints(node, parentG, state) {
|
|
285
|
+
var p = node.props;
|
|
286
|
+
var xs = p.x || [], ys = p.y || [];
|
|
287
|
+
var r = (p.size || 1) * 2;
|
|
288
|
+
var gpar = node.gpar || {};
|
|
289
|
+
var colArr = Array.isArray(gpar.col) ? gpar.col : null;
|
|
290
|
+
var fillArr = Array.isArray(gpar.fill) ? gpar.fill : null;
|
|
291
|
+
var lwdArr = Array.isArray(gpar.lwd) ? gpar.lwd : null;
|
|
292
|
+
var g = parentG.append("g").attr("class", "grob-points");
|
|
293
|
+
for (var i = 0; i < xs.length; i++) {
|
|
294
|
+
var c = g.append("circle")
|
|
295
|
+
.attr("cx", xs[i]).attr("cy", ys[i]).attr("r", r)
|
|
296
|
+
.attr("data-id", node.id)
|
|
297
|
+
.attr("data-index", i);
|
|
298
|
+
// Apply base gpar, then override per-point if arrays
|
|
299
|
+
applyGparSvg(c, gpar);
|
|
300
|
+
if (colArr) c.attr("stroke", colArr[i % colArr.length] || "none");
|
|
301
|
+
if (fillArr) c.attr("fill", fillArr[i % fillArr.length] || "none");
|
|
302
|
+
if (lwdArr) c.attr("stroke-width", lwdArr[i % lwdArr.length]);
|
|
303
|
+
// Register in spatial index for tooltip hit-testing
|
|
304
|
+
if (state) {
|
|
305
|
+
state.hitItems.push({
|
|
306
|
+
x: xs[i], y: ys[i], r: r,
|
|
307
|
+
id: node.id, index: i, layer: "svg",
|
|
308
|
+
data: node.data ? extractRow(node.data, i) : null
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function pointsToSvg(xs, ys) {
|
|
315
|
+
var parts = [];
|
|
316
|
+
for (var i = 0; i < xs.length; i++) {
|
|
317
|
+
parts.push(xs[i] + "," + ys[i]);
|
|
318
|
+
}
|
|
319
|
+
return parts.join(" ");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function drawSvgPolyline(node, parentG) {
|
|
323
|
+
var p = node.props;
|
|
324
|
+
if (p.groups) {
|
|
325
|
+
p.groups.forEach(function (grp) {
|
|
326
|
+
parentG.append("polyline")
|
|
327
|
+
.attr("points", pointsToSvg(grp.x, grp.y))
|
|
328
|
+
.attr("fill", "none")
|
|
329
|
+
.call(applyGparSvg, node.gpar);
|
|
330
|
+
});
|
|
331
|
+
} else {
|
|
332
|
+
parentG.append("polyline")
|
|
333
|
+
.attr("points", pointsToSvg(p.x || [], p.y || []))
|
|
334
|
+
.attr("fill", "none")
|
|
335
|
+
.call(applyGparSvg, node.gpar);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function drawSvgSegments(node, parentG) {
|
|
340
|
+
var p = node.props;
|
|
341
|
+
var x0s = p.x0 || [], y0s = p.y0 || [];
|
|
342
|
+
var x1s = p.x1 || [], y1s = p.y1 || [];
|
|
343
|
+
for (var i = 0; i < x0s.length; i++) {
|
|
344
|
+
parentG.append("line")
|
|
345
|
+
.attr("x1", x0s[i]).attr("y1", y0s[i])
|
|
346
|
+
.attr("x2", x1s[i]).attr("y2", y1s[i])
|
|
347
|
+
.call(applyGparSvg, node.gpar);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function drawSvgPolygon(node, parentG) {
|
|
352
|
+
var p = node.props;
|
|
353
|
+
parentG.append("polygon")
|
|
354
|
+
.attr("points", pointsToSvg(p.x || [], p.y || []))
|
|
355
|
+
.call(applyGparSvg, node.gpar);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function drawSvgPath(node, parentG) {
|
|
359
|
+
var p = node.props;
|
|
360
|
+
if (p.d) {
|
|
361
|
+
parentG.append("path").attr("d", p.d)
|
|
362
|
+
.attr("fill-rule", p.rule === "evenodd" ? "evenodd" : "nonzero")
|
|
363
|
+
.call(applyGparSvg, node.gpar);
|
|
364
|
+
} else if (p.groups) {
|
|
365
|
+
var d = "";
|
|
366
|
+
p.groups.forEach(function (grp) {
|
|
367
|
+
for (var i = 0; i < grp.x.length; i++) {
|
|
368
|
+
d += (i === 0 ? "M" : "L") + grp.x[i] + "," + grp.y[i];
|
|
369
|
+
}
|
|
370
|
+
d += "Z";
|
|
371
|
+
});
|
|
372
|
+
parentG.append("path").attr("d", d)
|
|
373
|
+
.attr("fill-rule", p.rule === "evenodd" ? "evenodd" : "nonzero")
|
|
374
|
+
.call(applyGparSvg, node.gpar);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function drawSvgCompound(node, parentG, state) {
|
|
379
|
+
// compound_stroke / compound_fill / compound_fill_stroke
|
|
380
|
+
// Render sub-paths as a combined <path>
|
|
381
|
+
var subPaths = (node.props || {}).sub_paths || [];
|
|
382
|
+
var g = parentG.append("g").attr("class", "compound-path");
|
|
383
|
+
subPaths.forEach(function (sub) {
|
|
384
|
+
drawSvgGrub(sub, g, state);
|
|
385
|
+
});
|
|
386
|
+
// Apply overall stroke/fill from the compound node's gpar
|
|
387
|
+
applyGparSvg(g, node.gpar);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ---- Canvas rendering -------------------------------------------------
|
|
391
|
+
|
|
392
|
+
function drawCanvasNode(node, state) {
|
|
393
|
+
if (!node) return;
|
|
394
|
+
var ctx = state.ctx;
|
|
395
|
+
if (node.type === "viewport") {
|
|
396
|
+
ctx.save();
|
|
397
|
+
if (node.clip) {
|
|
398
|
+
var t = node.transform || {};
|
|
399
|
+
ctx.beginPath();
|
|
400
|
+
ctx.rect(t.x0 || 0, t.y0 || 0, t.w || 0, t.h || 0);
|
|
401
|
+
ctx.clip();
|
|
402
|
+
}
|
|
403
|
+
(node.children || []).forEach(function (child) {
|
|
404
|
+
drawCanvasNodeRouted(child, state);
|
|
405
|
+
});
|
|
406
|
+
ctx.restore();
|
|
407
|
+
} else {
|
|
408
|
+
drawCanvasPrimitive(node, state);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function drawCanvasNodeRouted(node, state) {
|
|
413
|
+
if (!node) return;
|
|
414
|
+
if (node.type === "viewport") {
|
|
415
|
+
drawCanvasNode(node, state);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
var layer = routeToLayer(node);
|
|
419
|
+
if (layer === "canvas") {
|
|
420
|
+
drawCanvasPrimitive(node, state);
|
|
421
|
+
}
|
|
422
|
+
// SVG items are handled separately in the SVG pass
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function drawCanvasPrimitive(node, state) {
|
|
426
|
+
var ctx = state.ctx;
|
|
427
|
+
var p = node.props || {};
|
|
428
|
+
ctx.save();
|
|
429
|
+
applyGparCanvas(ctx, node.gpar);
|
|
430
|
+
|
|
431
|
+
switch (node.type) {
|
|
432
|
+
case "points":
|
|
433
|
+
drawCanvasPoints(node, state);
|
|
434
|
+
break;
|
|
435
|
+
case "rect":
|
|
436
|
+
ctx.beginPath();
|
|
437
|
+
ctx.rect(p.x, p.y, p.w, p.h);
|
|
438
|
+
if (ctx.fillStyle !== "rgba(0,0,0,0)") ctx.fill();
|
|
439
|
+
if (ctx.strokeStyle !== "rgba(0,0,0,0)") ctx.stroke();
|
|
440
|
+
break;
|
|
441
|
+
case "circle":
|
|
442
|
+
ctx.beginPath();
|
|
443
|
+
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
|
|
444
|
+
ctx.fill();
|
|
445
|
+
ctx.stroke();
|
|
446
|
+
break;
|
|
447
|
+
case "polygon":
|
|
448
|
+
if (p.x && p.x.length >= 3) {
|
|
449
|
+
ctx.beginPath();
|
|
450
|
+
ctx.moveTo(p.x[0], p.y[0]);
|
|
451
|
+
for (var i = 1; i < p.x.length; i++) {
|
|
452
|
+
ctx.lineTo(p.x[i], p.y[i]);
|
|
453
|
+
}
|
|
454
|
+
ctx.closePath();
|
|
455
|
+
ctx.fill();
|
|
456
|
+
ctx.stroke();
|
|
457
|
+
}
|
|
458
|
+
break;
|
|
459
|
+
default:
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
ctx.restore();
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function drawCanvasPoints(node, state) {
|
|
466
|
+
var ctx = state.ctx;
|
|
467
|
+
var xs = node.props.x || [], ys = node.props.y || [];
|
|
468
|
+
var r = (node.props.size || 1) * 2;
|
|
469
|
+
var gpar = node.gpar || {};
|
|
470
|
+
var colArr = Array.isArray(gpar.col) ? gpar.col : null;
|
|
471
|
+
var fillArr = Array.isArray(gpar.fill) ? gpar.fill : null;
|
|
472
|
+
var hasPerPointStyle = colArr || fillArr;
|
|
473
|
+
|
|
474
|
+
if (hasPerPointStyle) {
|
|
475
|
+
// Per-point rendering: each point gets its own fill/stroke
|
|
476
|
+
for (var i = 0; i < xs.length; i++) {
|
|
477
|
+
ctx.beginPath();
|
|
478
|
+
ctx.arc(xs[i], ys[i], r, 0, Math.PI * 2);
|
|
479
|
+
if (fillArr) ctx.fillStyle = fillArr[i % fillArr.length] || "rgba(0,0,0,0)";
|
|
480
|
+
ctx.fill();
|
|
481
|
+
if (colArr) {
|
|
482
|
+
ctx.strokeStyle = colArr[i % colArr.length] || "rgba(0,0,0,0)";
|
|
483
|
+
ctx.stroke();
|
|
484
|
+
}
|
|
485
|
+
state.hitItems.push({
|
|
486
|
+
x: xs[i], y: ys[i], r: r,
|
|
487
|
+
id: node.id, index: i, layer: "canvas",
|
|
488
|
+
data: node.data ? extractRow(node.data, i) : null
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
} else {
|
|
492
|
+
// Batch rendering: all points share one style
|
|
493
|
+
ctx.beginPath();
|
|
494
|
+
for (var j = 0; j < xs.length; j++) {
|
|
495
|
+
ctx.moveTo(xs[j] + r, ys[j]);
|
|
496
|
+
ctx.arc(xs[j], ys[j], r, 0, Math.PI * 2);
|
|
497
|
+
state.hitItems.push({
|
|
498
|
+
x: xs[j], y: ys[j], r: r,
|
|
499
|
+
id: node.id, index: j, layer: "canvas",
|
|
500
|
+
data: node.data ? extractRow(node.data, j) : null
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
ctx.fill();
|
|
504
|
+
if (gpar.col && parseColour(gpar.col)) {
|
|
505
|
+
ctx.stroke();
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function extractRow(data, i) {
|
|
511
|
+
var row = {};
|
|
512
|
+
for (var key in data) {
|
|
513
|
+
row[key] = Array.isArray(data[key]) ? data[key][i] : data[key];
|
|
514
|
+
}
|
|
515
|
+
return row;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// ---- Quadtree (simple implementation, no D3 dependency) ---------------
|
|
519
|
+
|
|
520
|
+
function SimpleQuadtree(items) {
|
|
521
|
+
this.items = items;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
SimpleQuadtree.prototype.find = function (x, y, radius) {
|
|
525
|
+
var best = null, bestDist = radius * radius;
|
|
526
|
+
for (var i = 0; i < this.items.length; i++) {
|
|
527
|
+
var it = this.items[i];
|
|
528
|
+
var dx = it.x - x, dy = it.y - y;
|
|
529
|
+
var d2 = dx * dx + dy * dy;
|
|
530
|
+
if (d2 < bestDist) {
|
|
531
|
+
bestDist = d2;
|
|
532
|
+
best = it;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return best;
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
// If D3 quadtree is available, use it for better performance
|
|
539
|
+
function buildQuadtree(items) {
|
|
540
|
+
if (typeof d3 !== "undefined" && d3.quadtree) {
|
|
541
|
+
return d3.quadtree()
|
|
542
|
+
.x(function (d) { return d.x; })
|
|
543
|
+
.y(function (d) { return d.y; })
|
|
544
|
+
.addAll(items);
|
|
545
|
+
}
|
|
546
|
+
return new SimpleQuadtree(items);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// ---- Interactions -----------------------------------------------------
|
|
550
|
+
|
|
551
|
+
function bindInteractions(state, options) {
|
|
552
|
+
if (!options.interactive) return;
|
|
553
|
+
|
|
554
|
+
// Tooltip
|
|
555
|
+
if (options.tooltip !== false) {
|
|
556
|
+
bindTooltip(state);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Zoom / Pan (requires D3)
|
|
560
|
+
if (options.zoom !== false && typeof d3 !== "undefined" && d3.zoom) {
|
|
561
|
+
bindZoom(state, options);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Brush selection (requires D3)
|
|
565
|
+
if (options.brush && typeof d3 !== "undefined" && d3.brush) {
|
|
566
|
+
bindBrush(state, options);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function bindTooltip(state) {
|
|
571
|
+
var tooltip = state.tooltip;
|
|
572
|
+
var container = state.container;
|
|
573
|
+
|
|
574
|
+
container.addEventListener("mousemove", function (event) {
|
|
575
|
+
var rect = container.getBoundingClientRect();
|
|
576
|
+
var mx = event.clientX - rect.left;
|
|
577
|
+
var my = event.clientY - rect.top;
|
|
578
|
+
|
|
579
|
+
// All interactive items (SVG and Canvas) are registered in a
|
|
580
|
+
// unified spatial index. Query by proximity — no DOM hit-testing
|
|
581
|
+
// needed, so the overlay z-order is irrelevant.
|
|
582
|
+
if (state.quadtree) {
|
|
583
|
+
var nearest = state.quadtree.find(mx, my, TOOLTIP_SEARCH_RADIUS);
|
|
584
|
+
if (nearest && nearest.data) {
|
|
585
|
+
showTooltip(tooltip, nearest.data, mx, my, container);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
hideTooltip(tooltip);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
container.addEventListener("mouseleave", function () {
|
|
594
|
+
hideTooltip(tooltip);
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function showTooltip(tooltip, data, mx, my, container) {
|
|
599
|
+
var lines = [];
|
|
600
|
+
for (var key in data) {
|
|
601
|
+
lines.push("<b>" + key + "</b>: " + data[key]);
|
|
602
|
+
}
|
|
603
|
+
tooltip.innerHTML = lines.join("<br>");
|
|
604
|
+
tooltip.classList.add("visible");
|
|
605
|
+
// Position: offset from cursor
|
|
606
|
+
var tw = tooltip.offsetWidth || 100;
|
|
607
|
+
var th = tooltip.offsetHeight || 30;
|
|
608
|
+
var cw = container.offsetWidth;
|
|
609
|
+
var left = mx + 12;
|
|
610
|
+
var top = my - th - 8;
|
|
611
|
+
if (left + tw > cw) left = mx - tw - 12;
|
|
612
|
+
if (top < 0) top = my + 12;
|
|
613
|
+
tooltip.style.left = left + "px";
|
|
614
|
+
tooltip.style.top = top + "px";
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function hideTooltip(tooltip) {
|
|
618
|
+
tooltip.classList.remove("visible");
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function bindZoom(state, options) {
|
|
622
|
+
var zoomExtent = options.zoomExtent || [0.5, 20];
|
|
623
|
+
var zoom = d3.zoom()
|
|
624
|
+
.scaleExtent(zoomExtent)
|
|
625
|
+
.on("zoom", function (event) {
|
|
626
|
+
// SVG layer
|
|
627
|
+
state.svgRoot.attr("transform", event.transform);
|
|
628
|
+
// Canvas layer: redraw
|
|
629
|
+
redrawCanvas(state, event.transform);
|
|
630
|
+
});
|
|
631
|
+
d3.select(state.overlay).call(zoom);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function redrawCanvas(state, transform) {
|
|
635
|
+
var ctx = state.ctx;
|
|
636
|
+
var w = state.width, h = state.height;
|
|
637
|
+
ctx.save();
|
|
638
|
+
ctx.clearRect(0, 0, w, h);
|
|
639
|
+
if (transform) {
|
|
640
|
+
ctx.translate(transform.x, transform.y);
|
|
641
|
+
ctx.scale(transform.k, transform.k);
|
|
642
|
+
}
|
|
643
|
+
// Re-draw only canvas-layer items (not SVG-registered ones)
|
|
644
|
+
state.hitItems.forEach(function (item) {
|
|
645
|
+
if (item.layer !== "canvas") return;
|
|
646
|
+
ctx.beginPath();
|
|
647
|
+
ctx.arc(item.x, item.y, item.r, 0, Math.PI * 2);
|
|
648
|
+
ctx.fill();
|
|
649
|
+
});
|
|
650
|
+
ctx.restore();
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function bindBrush(state, options) {
|
|
654
|
+
var brush = d3.brush()
|
|
655
|
+
.extent([[0, 0], [state.width, state.height]])
|
|
656
|
+
.on("end", function (event) {
|
|
657
|
+
if (!event.selection) {
|
|
658
|
+
emitEvent(state, "brush", { selected: [] });
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
var sel = event.selection;
|
|
662
|
+
var x0 = sel[0][0], y0 = sel[0][1];
|
|
663
|
+
var x1 = sel[1][0], y1 = sel[1][1];
|
|
664
|
+
var selected = queryRegion(state, x0, y0, x1, y1);
|
|
665
|
+
emitEvent(state, "brush", { selected: selected });
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
d3.select(state.overlay).append("g")
|
|
669
|
+
.attr("class", "brush")
|
|
670
|
+
.call(brush);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function queryRegion(state, x0, y0, x1, y1) {
|
|
674
|
+
var result = [];
|
|
675
|
+
state.hitItems.forEach(function (item) {
|
|
676
|
+
if (item.x >= x0 && item.x <= x1 && item.y >= y0 && item.y <= y1) {
|
|
677
|
+
result.push(item);
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
return result;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function emitEvent(state, name, detail) {
|
|
684
|
+
var event = new CustomEvent("gridpy:" + name, { detail: detail });
|
|
685
|
+
state.container.dispatchEvent(event);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// ---- Main render entry point ------------------------------------------
|
|
689
|
+
|
|
690
|
+
function render(container, sceneGraph, options) {
|
|
691
|
+
options = options || {};
|
|
692
|
+
if (options.interactive === undefined) options.interactive = true;
|
|
693
|
+
var theme = options.theme || "light";
|
|
694
|
+
|
|
695
|
+
var sg = (typeof sceneGraph === "string")
|
|
696
|
+
? JSON.parse(sceneGraph) : sceneGraph;
|
|
697
|
+
var w = sg.width, h = sg.height;
|
|
698
|
+
|
|
699
|
+
// Setup container
|
|
700
|
+
if (typeof container === "string") {
|
|
701
|
+
container = document.getElementById(container)
|
|
702
|
+
|| document.querySelector(container);
|
|
703
|
+
}
|
|
704
|
+
container.innerHTML = "";
|
|
705
|
+
container.classList.add("gridpy-container", "gridpy-theme-" + theme);
|
|
706
|
+
container.style.width = w + "px";
|
|
707
|
+
container.style.height = h + "px";
|
|
708
|
+
|
|
709
|
+
// Canvas layer
|
|
710
|
+
var canvas = document.createElement("canvas");
|
|
711
|
+
canvas.width = w;
|
|
712
|
+
canvas.height = h;
|
|
713
|
+
canvas.className = "gridpy-canvas";
|
|
714
|
+
container.appendChild(canvas);
|
|
715
|
+
var ctx = canvas.getContext("2d");
|
|
716
|
+
|
|
717
|
+
// SVG layer
|
|
718
|
+
var svgNS = "http://www.w3.org/2000/svg";
|
|
719
|
+
var svgEl = document.createElementNS(svgNS, "svg");
|
|
720
|
+
svgEl.setAttribute("width", w);
|
|
721
|
+
svgEl.setAttribute("height", h);
|
|
722
|
+
svgEl.setAttribute("class", "gridpy-svg");
|
|
723
|
+
container.appendChild(svgEl);
|
|
724
|
+
|
|
725
|
+
var svg = (typeof d3 !== "undefined") ? d3.select(svgEl) : null;
|
|
726
|
+
var defs = svg ? svg.append("defs") : null;
|
|
727
|
+
var svgRoot = svg ? svg.append("g").attr("class", "gridpy-root") : null;
|
|
728
|
+
|
|
729
|
+
// Create defs
|
|
730
|
+
if (defs) createDefs(defs, sg.defs);
|
|
731
|
+
|
|
732
|
+
// Interaction overlay (transparent SVG)
|
|
733
|
+
var overlayEl = document.createElementNS(svgNS, "svg");
|
|
734
|
+
overlayEl.setAttribute("width", w);
|
|
735
|
+
overlayEl.setAttribute("height", h);
|
|
736
|
+
overlayEl.setAttribute("class", "gridpy-overlay");
|
|
737
|
+
container.appendChild(overlayEl);
|
|
738
|
+
|
|
739
|
+
// Tooltip
|
|
740
|
+
var tooltip = document.createElement("div");
|
|
741
|
+
tooltip.className = "gridpy-tooltip";
|
|
742
|
+
container.appendChild(tooltip);
|
|
743
|
+
|
|
744
|
+
var state = {
|
|
745
|
+
container: container,
|
|
746
|
+
canvas: canvas,
|
|
747
|
+
ctx: ctx,
|
|
748
|
+
svg: svg,
|
|
749
|
+
svgRoot: svgRoot,
|
|
750
|
+
overlay: overlayEl,
|
|
751
|
+
tooltip: tooltip,
|
|
752
|
+
width: w,
|
|
753
|
+
height: h,
|
|
754
|
+
dpi: sg.dpi || 150,
|
|
755
|
+
hitItems: [],
|
|
756
|
+
quadtree: null,
|
|
757
|
+
sceneGraph: sg
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
// Render SVG pass
|
|
761
|
+
if (svgRoot && sg.root) {
|
|
762
|
+
renderTree(sg.root, state, svgRoot);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Render Canvas pass (only canvas-routed items)
|
|
766
|
+
if (sg.root) {
|
|
767
|
+
drawCanvasNode(sg.root, state);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Build spatial index for tooltip hit-testing (SVG + Canvas items)
|
|
771
|
+
if (state.hitItems.length > 0) {
|
|
772
|
+
state.quadtree = buildQuadtree(state.hitItems);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Bind interactions
|
|
776
|
+
bindInteractions(state, options);
|
|
777
|
+
|
|
778
|
+
return state;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function renderTree(node, state, parentG) {
|
|
782
|
+
if (!node) return;
|
|
783
|
+
if (node.type === "viewport") {
|
|
784
|
+
var g = parentG.append("g")
|
|
785
|
+
.attr("class", "vp-" + (node.name || ""))
|
|
786
|
+
.attr("data-viewport", node.name || "");
|
|
787
|
+
if (node.clip_id) {
|
|
788
|
+
g.attr("clip-path", "url(#" + node.clip_id + ")");
|
|
789
|
+
}
|
|
790
|
+
if (node.mask_id) {
|
|
791
|
+
g.attr("mask", "url(#" + node.mask_id + ")");
|
|
792
|
+
}
|
|
793
|
+
(node.children || []).forEach(function (child) {
|
|
794
|
+
renderTree(child, state, g);
|
|
795
|
+
});
|
|
796
|
+
} else {
|
|
797
|
+
var layer = routeToLayer(node);
|
|
798
|
+
if (layer === "svg") {
|
|
799
|
+
drawSvgGrub(node, parentG, state);
|
|
800
|
+
}
|
|
801
|
+
// Canvas items are drawn in a separate pass
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// ---- Public API -------------------------------------------------------
|
|
806
|
+
|
|
807
|
+
return {
|
|
808
|
+
render: render,
|
|
809
|
+
CANVAS_THRESHOLD: CANVAS_THRESHOLD,
|
|
810
|
+
version: "0.1.0"
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
})();
|