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.
@@ -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
+ })();