d3graph 2.9.2__tar.gz → 2.9.3__tar.gz
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.
- {d3graph-2.9.2/d3graph.egg-info → d3graph-2.9.3}/PKG-INFO +1 -1
- {d3graph-2.9.2 → d3graph-2.9.3}/d3graph/__init__.py +1 -1
- {d3graph-2.9.2 → d3graph-2.9.3}/d3graph/d3graph.py +40 -0
- {d3graph-2.9.2 → d3graph-2.9.3}/d3graph/d3js/d3graphscript.js +311 -53
- {d3graph-2.9.2 → d3graph-2.9.3}/d3graph/d3js/index.html.j2 +22 -11
- {d3graph-2.9.2 → d3graph-2.9.3}/d3graph/d3js/style.css +7 -0
- {d3graph-2.9.2 → d3graph-2.9.3}/d3graph/examples.py +28 -4
- {d3graph-2.9.2 → d3graph-2.9.3/d3graph.egg-info}/PKG-INFO +1 -1
- {d3graph-2.9.2 → d3graph-2.9.3}/LICENSE +0 -0
- {d3graph-2.9.2 → d3graph-2.9.3}/MANIFEST.in +0 -0
- {d3graph-2.9.2 → d3graph-2.9.3}/README.md +0 -0
- {d3graph-2.9.2 → d3graph-2.9.3}/d3graph/d3js/d3.v3.js +0 -0
- {d3graph-2.9.2 → d3graph-2.9.3}/d3graph/examples_docs.py +0 -0
- {d3graph-2.9.2 → d3graph-2.9.3}/d3graph.egg-info/SOURCES.txt +0 -0
- {d3graph-2.9.2 → d3graph-2.9.3}/d3graph.egg-info/dependency_links.txt +0 -0
- {d3graph-2.9.2 → d3graph-2.9.3}/d3graph.egg-info/requires.txt +0 -0
- {d3graph-2.9.2 → d3graph-2.9.3}/d3graph.egg-info/top_level.txt +0 -0
- {d3graph-2.9.2 → d3graph-2.9.3}/pyproject.toml +0 -0
- {d3graph-2.9.2 → d3graph-2.9.3}/setup.cfg +0 -0
|
@@ -123,6 +123,11 @@ class d3graph:
|
|
|
123
123
|
node_text_inside: bool = False,
|
|
124
124
|
max_ticks: int = 300,
|
|
125
125
|
label_zoom_threshold: float = 0.4,
|
|
126
|
+
canvas_edge_threshold: int = 2000,
|
|
127
|
+
show_density: bool = False,
|
|
128
|
+
density_grid_size: int = 60,
|
|
129
|
+
density_blur: int = 10,
|
|
130
|
+
density_opacity: float = 0.6,
|
|
126
131
|
) -> None:
|
|
127
132
|
"""Build and show the graph.
|
|
128
133
|
|
|
@@ -177,6 +182,31 @@ class d3graph:
|
|
|
177
182
|
Node and edge labels are hidden below this zoom scale (unreadable anyway, and
|
|
178
183
|
costly to keep rendering for large graphs) and reappear once zoomed back in
|
|
179
184
|
past it. Uses a single CSS class toggle, not per-label work. 0: never hide.
|
|
185
|
+
canvas_edge_threshold : int, (default: 2000)
|
|
186
|
+
Above this many visible edges, edges are drawn on a <canvas> layer instead of
|
|
187
|
+
as individual SVG <line> elements. SVG's per-element DOM overhead is what makes
|
|
188
|
+
tens of thousands of edges freeze the page; canvas draw calls stay cheap
|
|
189
|
+
regardless of edge count. Nodes always stay SVG (drag/click/tooltips). Only
|
|
190
|
+
applies once edges exceed this count, so small/medium graphs are unaffected -
|
|
191
|
+
note that in canvas mode, the "Save as SVG" export won't include edges, since
|
|
192
|
+
they no longer live in the SVG DOM.
|
|
193
|
+
show_density : bool, (default: False)
|
|
194
|
+
Adds a node-clustering heatmap layer (grid-binned density of node positions),
|
|
195
|
+
drawn on its own canvas beneath the edges and nodes, with a toggle button in
|
|
196
|
+
the UI ("Show/Hide Density") to turn it on/off regardless of this default.
|
|
197
|
+
Recomputed from live node positions each frame it's visible, so it tracks the
|
|
198
|
+
force layout as nodes settle, and it responds to the weight/component sliders
|
|
199
|
+
since it's based on whichever nodes are currently on screen. Color scheme is
|
|
200
|
+
a yellow-to-red heat gradient in light mode, single-hue blue in dark mode
|
|
201
|
+
(updates live when dark mode is toggled).
|
|
202
|
+
density_grid_size : int, (default: 40)
|
|
203
|
+
Grid resolution for the density heatmap (cells along the longer axis of the
|
|
204
|
+
node bounding box). Higher = finer detail on tight clusters, more cells to draw.
|
|
205
|
+
density_blur : int, (default: 8)
|
|
206
|
+
Blur radius (px) applied to the heatmap for a smooth look instead of a
|
|
207
|
+
blocky grid.
|
|
208
|
+
density_opacity : float, (default: 0.6)
|
|
209
|
+
Maximum heatmap opacity, reached at the highest-density grid cell.
|
|
180
210
|
|
|
181
211
|
Returns
|
|
182
212
|
-------
|
|
@@ -201,6 +231,11 @@ class d3graph:
|
|
|
201
231
|
self.config['node_text_inside'] = node_text_inside
|
|
202
232
|
self.config['max_ticks'] = max_ticks
|
|
203
233
|
self.config['label_zoom_threshold'] = label_zoom_threshold
|
|
234
|
+
self.config['canvas_edge_threshold'] = canvas_edge_threshold
|
|
235
|
+
self.config['show_density'] = show_density
|
|
236
|
+
self.config['density_grid_size'] = density_grid_size
|
|
237
|
+
self.config['density_blur'] = density_blur
|
|
238
|
+
self.config['density_opacity'] = density_opacity
|
|
204
239
|
|
|
205
240
|
# Allow show() to override the link_tension set at __init__ time
|
|
206
241
|
if link_tension is not None:
|
|
@@ -824,6 +859,11 @@ class d3graph:
|
|
|
824
859
|
'sticky': self.config.get('sticky', False),
|
|
825
860
|
'max_ticks': self.config.get('max_ticks', 300),
|
|
826
861
|
'label_zoom_threshold': self.config.get('label_zoom_threshold', 0.6),
|
|
862
|
+
'canvas_edge_threshold': self.config.get('canvas_edge_threshold', 2000),
|
|
863
|
+
'show_density': self.config.get('show_density', False),
|
|
864
|
+
'density_grid_size': self.config.get('density_grid_size', 40),
|
|
865
|
+
'density_blur': self.config.get('density_blur', 8),
|
|
866
|
+
'density_opacity': self.config.get('density_opacity', 0.6),
|
|
827
867
|
'node_text_inside': self.config.get('node_text_inside', False),
|
|
828
868
|
'CLICK_COMMENT': CLICK_COMMENT,
|
|
829
869
|
'CLICK_FILL': click_properties['fill'],
|
|
@@ -12,6 +12,12 @@ function d3graphscript(config = {
|
|
|
12
12
|
node_text_inside: false,
|
|
13
13
|
max_ticks: 300,
|
|
14
14
|
label_zoom_threshold: 0.6,
|
|
15
|
+
canvas_edge_threshold: 2000,
|
|
16
|
+
density_grid_size: 40,
|
|
17
|
+
density_blur: 8,
|
|
18
|
+
density_opacity: 0.6,
|
|
19
|
+
show_density: false,
|
|
20
|
+
dark_mode: false,
|
|
15
21
|
}) {
|
|
16
22
|
|
|
17
23
|
//Constants for the SVG
|
|
@@ -30,6 +36,34 @@ function d3graphscript(config = {
|
|
|
30
36
|
// single CSS class, not per-element work) instead of rendering thousands
|
|
31
37
|
// of illegible <text> nodes. They reappear once zoomed back in past it.
|
|
32
38
|
var labelZoomThreshold = (config.label_zoom_threshold !== undefined && config.label_zoom_threshold !== null) ? config.label_zoom_threshold : 0.6;
|
|
39
|
+
// Above this many visible edges, draw links on a <canvas> instead of as
|
|
40
|
+
// SVG <line> elements. SVG per-element overhead (DOM node creation,
|
|
41
|
+
// layout, GC) is what makes tens of thousands of edges freeze the page;
|
|
42
|
+
// canvas just issues draw calls into a single bitmap each frame, so cost
|
|
43
|
+
// stays flat regardless of edge count. Nodes stay SVG either way (far
|
|
44
|
+
// fewer of them, and it keeps drag/click/tooltip interactivity simple).
|
|
45
|
+
var canvasEdgeThreshold = (config.canvas_edge_threshold !== undefined && config.canvas_edge_threshold !== null) ? config.canvas_edge_threshold : 2000;
|
|
46
|
+
var useCanvasEdges = false;
|
|
47
|
+
var canvasEl, ctx;
|
|
48
|
+
var currentTransform = { scale: 1, translate: [0, 0] };
|
|
49
|
+
// Master on/off switch for edges (independent of the weight/component
|
|
50
|
+
// sliders) — lets the user clear visual clutter on large graphs to see
|
|
51
|
+
// node structure/clustering without redrawing or refiltering anything.
|
|
52
|
+
var edgesVisible = true;
|
|
53
|
+
|
|
54
|
+
// ---- DENSITY (clustering heatmap) LAYER ----
|
|
55
|
+
// Grid-binned node density, drawn on its own canvas beneath the edges and
|
|
56
|
+
// nodes. Recomputed from live node positions every tick it's visible, so
|
|
57
|
+
// it tracks the force layout as nodes settle/move — cheap since it's a
|
|
58
|
+
// single O(nodes) pass, unlike the edge count this was never the
|
|
59
|
+
// bottleneck. Color scheme adapts to dark mode (updated live via
|
|
60
|
+
// window.d3graphSetDarkMode, called from the dark-mode toggle).
|
|
61
|
+
var densityGridSize = (config.density_grid_size !== undefined && config.density_grid_size !== null) ? config.density_grid_size : 40;
|
|
62
|
+
var densityBlur = (config.density_blur !== undefined && config.density_blur !== null) ? config.density_blur : 8;
|
|
63
|
+
var densityOpacity = (config.density_opacity !== undefined && config.density_opacity !== null) ? config.density_opacity : 0.6;
|
|
64
|
+
var densityVisible = config.show_density || false;
|
|
65
|
+
var darkMode = config.dark_mode || false;
|
|
66
|
+
var densityCanvasEl, densityCtx, densityOffscreen;
|
|
33
67
|
|
|
34
68
|
// Set the body background color
|
|
35
69
|
document.body.style.backgroundColor = background_color;
|
|
@@ -67,6 +101,7 @@ function d3graphscript(config = {
|
|
|
67
101
|
} else {
|
|
68
102
|
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
|
|
69
103
|
}
|
|
104
|
+
if (densityVisible) drawDensityLayer();
|
|
70
105
|
}
|
|
71
106
|
|
|
72
107
|
function dragended(d) {
|
|
@@ -223,19 +258,239 @@ function d3graphscript(config = {
|
|
|
223
258
|
|
|
224
259
|
// ---- END SHAPE RENDERING ----
|
|
225
260
|
|
|
226
|
-
//
|
|
227
|
-
|
|
261
|
+
// Layered container: canvas (background + edges, used only above
|
|
262
|
+
// canvasEdgeThreshold) sits under a transparent SVG (nodes always render
|
|
263
|
+
// here, for drag/click/tooltip interactivity). The background color lives
|
|
264
|
+
// on the container so it shows through the transparent SVG either way.
|
|
265
|
+
var container = d3.select("body").append("div")
|
|
266
|
+
.attr("id", "graphContainer")
|
|
267
|
+
.style("position", "relative")
|
|
268
|
+
.style("width", width + "px")
|
|
269
|
+
.style("height", height + "px")
|
|
270
|
+
.style("background-color", background_color);
|
|
271
|
+
|
|
272
|
+
// Density (clustering heatmap) layer — created first so it stacks
|
|
273
|
+
// beneath both the edge canvas and the SVG node layer.
|
|
274
|
+
densityCanvasEl = container.append("canvas")
|
|
228
275
|
.attr("width", width)
|
|
229
276
|
.attr("height", height)
|
|
230
|
-
.style("
|
|
277
|
+
.style("position", "absolute")
|
|
278
|
+
.style("top", 0)
|
|
279
|
+
.style("left", 0)
|
|
280
|
+
.style("pointer-events", "none")
|
|
281
|
+
.style("display", densityVisible ? null : "none")
|
|
282
|
+
.node();
|
|
283
|
+
densityCtx = densityCanvasEl.getContext("2d");
|
|
284
|
+
|
|
285
|
+
canvasEl = container.append("canvas")
|
|
286
|
+
.attr("width", width)
|
|
287
|
+
.attr("height", height)
|
|
288
|
+
.style("position", "absolute")
|
|
289
|
+
.style("top", 0)
|
|
290
|
+
.style("left", 0)
|
|
291
|
+
.style("pointer-events", "none")
|
|
292
|
+
.node();
|
|
293
|
+
ctx = canvasEl.getContext("2d");
|
|
294
|
+
|
|
295
|
+
//Append a SVG to the container. Assign this SVG as an object to svg
|
|
296
|
+
var svg = container.append("svg")
|
|
297
|
+
.attr("width", width)
|
|
298
|
+
.attr("height", height)
|
|
299
|
+
.style("position", "absolute")
|
|
300
|
+
.style("top", 0)
|
|
301
|
+
.style("left", 0)
|
|
302
|
+
.style("background-color", "transparent")
|
|
231
303
|
.call(d3.behavior.zoom().on("zoom", function () {
|
|
232
304
|
svg.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")");
|
|
233
305
|
// Single class toggle (cheap) rather than iterating every label on
|
|
234
306
|
// every zoom/pan event — CSS handles hiding all descendant <text>.
|
|
235
307
|
svg.classed("labels-hidden", d3.event.scale < labelZoomThreshold);
|
|
308
|
+
currentTransform.scale = d3.event.scale;
|
|
309
|
+
currentTransform.translate = d3.event.translate;
|
|
310
|
+
drawCanvasEdges();
|
|
311
|
+
drawDensityLayer();
|
|
236
312
|
}))
|
|
237
313
|
.on("dblclick.zoom", null)
|
|
238
314
|
.append("g")
|
|
315
|
+
|
|
316
|
+
// Draws all links onto the canvas layer, matching the SVG group's current
|
|
317
|
+
// pan/zoom transform. No-ops when the graph is small enough to stay on SVG.
|
|
318
|
+
function drawCanvasEdges() {
|
|
319
|
+
if (!useCanvasEdges || !ctx || !edgesVisible) return;
|
|
320
|
+
ctx.save();
|
|
321
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
322
|
+
ctx.clearRect(0, 0, canvasEl.width, canvasEl.height);
|
|
323
|
+
ctx.setTransform(currentTransform.scale, 0, 0, currentTransform.scale, currentTransform.translate[0], currentTransform.translate[1]);
|
|
324
|
+
for (var i = 0; i < graph.links.length; i++) {
|
|
325
|
+
var d = graph.links[i];
|
|
326
|
+
if (!d.source || !d.target || typeof d.source.x !== 'number') continue;
|
|
327
|
+
ctx.beginPath();
|
|
328
|
+
ctx.moveTo(d.source.x, d.source.y);
|
|
329
|
+
ctx.lineTo(d.target.x, d.target.y);
|
|
330
|
+
ctx.strokeStyle = d.edge_color || '#999';
|
|
331
|
+
ctx.globalAlpha = (d.edge_opacity !== undefined && d.edge_opacity !== null) ? d.edge_opacity : 0.6;
|
|
332
|
+
ctx.lineWidth = d.edge_width || 1;
|
|
333
|
+
ctx.setLineDash(d.edge_style === 'dashed' ? [6, 3] : d.edge_style === 'dotted' ? [1.5, 3] : []);
|
|
334
|
+
ctx.stroke();
|
|
335
|
+
}
|
|
336
|
+
ctx.restore();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Bins the currently visible nodes' live (x, y) positions into a coarse
|
|
340
|
+
// grid over their bounding box. O(nodes) — cheap enough to recompute
|
|
341
|
+
// every tick, unlike the edge count this was never the bottleneck.
|
|
342
|
+
function computeDensityGrid() {
|
|
343
|
+
var nodesData = node.data();
|
|
344
|
+
if (!nodesData.length) return null;
|
|
345
|
+
|
|
346
|
+
var minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
347
|
+
for (var i = 0; i < nodesData.length; i++) {
|
|
348
|
+
var nx = nodesData[i].x, ny = nodesData[i].y;
|
|
349
|
+
if (typeof nx !== 'number' || typeof ny !== 'number') continue;
|
|
350
|
+
if (nx < minX) minX = nx;
|
|
351
|
+
if (nx > maxX) maxX = nx;
|
|
352
|
+
if (ny < minY) minY = ny;
|
|
353
|
+
if (ny > maxY) maxY = ny;
|
|
354
|
+
}
|
|
355
|
+
if (minX === Infinity) return null;
|
|
356
|
+
|
|
357
|
+
var padX = (maxX - minX) * 0.05 || 20;
|
|
358
|
+
var padY = (maxY - minY) * 0.05 || 20;
|
|
359
|
+
minX -= padX; maxX += padX; minY -= padY; maxY += padY;
|
|
360
|
+
|
|
361
|
+
var w = (maxX - minX) || 1;
|
|
362
|
+
var h = (maxY - minY) || 1;
|
|
363
|
+
var cellsX, cellsY;
|
|
364
|
+
if (w >= h) {
|
|
365
|
+
cellsX = densityGridSize;
|
|
366
|
+
cellsY = Math.max(1, Math.round(densityGridSize * h / w));
|
|
367
|
+
} else {
|
|
368
|
+
cellsY = densityGridSize;
|
|
369
|
+
cellsX = Math.max(1, Math.round(densityGridSize * w / h));
|
|
370
|
+
}
|
|
371
|
+
var cellW = w / cellsX;
|
|
372
|
+
var cellH = h / cellsY;
|
|
373
|
+
|
|
374
|
+
var grid = new Float32Array(cellsX * cellsY);
|
|
375
|
+
var maxCount = 0;
|
|
376
|
+
for (var j = 0; j < nodesData.length; j++) {
|
|
377
|
+
var d = nodesData[j];
|
|
378
|
+
if (typeof d.x !== 'number' || typeof d.y !== 'number') continue;
|
|
379
|
+
var cx = Math.min(cellsX - 1, Math.max(0, Math.floor((d.x - minX) / cellW)));
|
|
380
|
+
var cy = Math.min(cellsY - 1, Math.max(0, Math.floor((d.y - minY) / cellH)));
|
|
381
|
+
var idx = cy * cellsX + cx;
|
|
382
|
+
grid[idx] += 1;
|
|
383
|
+
if (grid[idx] > maxCount) maxCount = grid[idx];
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return { grid: grid, cellsX: cellsX, cellsY: cellsY, cellW: cellW, cellH: cellH, minX: minX, minY: minY, maxCount: maxCount };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// t in [0, 1] (relative density) -> fill color. Light mode: yellow -> red
|
|
390
|
+
// heat gradient. Dark mode: single-hue blue, so it reads well against a
|
|
391
|
+
// dark background instead of clashing with it.
|
|
392
|
+
function densityColor(t) {
|
|
393
|
+
if (darkMode) {
|
|
394
|
+
var l = 25 + t * 45; // 25%..70% lightness
|
|
395
|
+
return 'hsl(210, 90%, ' + l + '%)';
|
|
396
|
+
}
|
|
397
|
+
var hue = 60 - t * 60; // 60=yellow -> 0=red
|
|
398
|
+
return 'hsl(' + hue + ', 100%, 50%)';
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Draws the heatmap: unblurred cells go to an offscreen buffer first,
|
|
402
|
+
// then get composited onto the visible canvas with a single blurred
|
|
403
|
+
// drawImage — much cheaper than blurring each cell individually.
|
|
404
|
+
function drawDensityLayer() {
|
|
405
|
+
if (!densityVisible || !densityCtx) return;
|
|
406
|
+
densityCtx.save();
|
|
407
|
+
densityCtx.setTransform(1, 0, 0, 1, 0, 0);
|
|
408
|
+
densityCtx.clearRect(0, 0, densityCanvasEl.width, densityCanvasEl.height);
|
|
409
|
+
|
|
410
|
+
var data = computeDensityGrid();
|
|
411
|
+
if (!data || data.maxCount <= 0) { densityCtx.restore(); return; }
|
|
412
|
+
|
|
413
|
+
if (!densityOffscreen) densityOffscreen = document.createElement('canvas');
|
|
414
|
+
densityOffscreen.width = densityCanvasEl.width;
|
|
415
|
+
densityOffscreen.height = densityCanvasEl.height;
|
|
416
|
+
var offCtx = densityOffscreen.getContext('2d');
|
|
417
|
+
offCtx.clearRect(0, 0, densityOffscreen.width, densityOffscreen.height);
|
|
418
|
+
offCtx.setTransform(currentTransform.scale, 0, 0, currentTransform.scale, currentTransform.translate[0], currentTransform.translate[1]);
|
|
419
|
+
|
|
420
|
+
for (var cy = 0; cy < data.cellsY; cy++) {
|
|
421
|
+
for (var cx = 0; cx < data.cellsX; cx++) {
|
|
422
|
+
var count = data.grid[cy * data.cellsX + cx];
|
|
423
|
+
if (count <= 0) continue;
|
|
424
|
+
var t = count / data.maxCount;
|
|
425
|
+
offCtx.fillStyle = densityColor(t);
|
|
426
|
+
offCtx.globalAlpha = densityOpacity * t;
|
|
427
|
+
offCtx.fillRect(data.minX + cx * data.cellW, data.minY + cy * data.cellH, data.cellW, data.cellH);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
densityCtx.filter = 'blur(' + densityBlur + 'px)';
|
|
432
|
+
densityCtx.drawImage(densityOffscreen, 0, 0);
|
|
433
|
+
densityCtx.filter = 'none';
|
|
434
|
+
densityCtx.restore();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
function applyEdgeVisibility() {
|
|
439
|
+
svg.classed("edges-hidden", !edgesVisible);
|
|
440
|
+
if (useCanvasEdges) {
|
|
441
|
+
if (edgesVisible) {
|
|
442
|
+
d3.select(canvasEl).style("display", null);
|
|
443
|
+
drawCanvasEdges();
|
|
444
|
+
} else {
|
|
445
|
+
if (ctx) ctx.clearRect(0, 0, canvasEl.width, canvasEl.height);
|
|
446
|
+
d3.select(canvasEl).style("display", "none");
|
|
447
|
+
}
|
|
448
|
+
} else {
|
|
449
|
+
d3.select(canvasEl).style("display", "none");
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Builds (or tears down) the SVG <line>/<text> elements for links, or
|
|
454
|
+
// switches to canvas rendering, based on the current edge count. Shared
|
|
455
|
+
// by the initial render and by restart() (slider-driven updates), so the
|
|
456
|
+
// threshold is re-evaluated every time the visible edge set changes.
|
|
457
|
+
function renderLinks() {
|
|
458
|
+
useCanvasEdges = graph.links.length > canvasEdgeThreshold;
|
|
459
|
+
|
|
460
|
+
if (useCanvasEdges) {
|
|
461
|
+
// Canvas takes over — drop any SVG link/link-text DOM entirely.
|
|
462
|
+
link = link.data([]);
|
|
463
|
+
link.exit().remove();
|
|
464
|
+
linkText = linkText.data([]);
|
|
465
|
+
linkText.exit().remove();
|
|
466
|
+
} else {
|
|
467
|
+
if (ctx) ctx.clearRect(0, 0, canvasEl.width, canvasEl.height);
|
|
468
|
+
|
|
469
|
+
link = link.data(graph.links);
|
|
470
|
+
link.exit().remove();
|
|
471
|
+
var linkEnter = link.enter().insert("line", ".node")
|
|
472
|
+
.attr("class", "link")
|
|
473
|
+
.attr('marker-start', function(d) { return 'url(#marker_' + d.marker_start + ')' });
|
|
474
|
+
linkEnter.append("title").text(function(d) { return d.tooltip; });
|
|
475
|
+
link.attr("marker-end", function(d) {
|
|
476
|
+
if (config.directed) { return 'url(#marker_' + d.marker_end + ')' } });
|
|
477
|
+
link.style("stroke-width", function(d) { return d.edge_width; });
|
|
478
|
+
link.style("stroke", function(d) { return d.edge_color; });
|
|
479
|
+
link.style("stroke-dasharray", function(d) { return d.edge_style; });
|
|
480
|
+
link.style("opacity", function(d) { return d.edge_opacity; });
|
|
481
|
+
|
|
482
|
+
linkText = linkText.data(graph.links);
|
|
483
|
+
linkText.exit().remove();
|
|
484
|
+
linkText.enter().append("text")
|
|
485
|
+
.attr("class", "link-text")
|
|
486
|
+
.attr("font-size", function(d) { return d.label_fontsize + "px"; })
|
|
487
|
+
.style("fill", function(d) { return d.label_color; })
|
|
488
|
+
.style("font-family", "Arial")
|
|
489
|
+
.text(function(d) { return d.label; });
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
applyEdgeVisibility();
|
|
493
|
+
}
|
|
239
494
|
|
|
240
495
|
graphRec = JSON.parse(JSON.stringify(graph)); // Full, unfiltered copy — used by the slider to restore edges later
|
|
241
496
|
|
|
@@ -253,32 +508,13 @@ function d3graphscript(config = {
|
|
|
253
508
|
.links(graph.links)
|
|
254
509
|
.start();
|
|
255
510
|
|
|
256
|
-
// Create
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (config.directed) {return 'url(#marker_' + d.marker_end + ')' }})
|
|
264
|
-
.style("stroke-width", function(d) {return d.edge_width;}) // LINK-WIDTH
|
|
265
|
-
.style("stroke", function(d) {return d.edge_color;}) // EDGE-COLORS
|
|
266
|
-
.style("stroke-dasharray", function(d) {return d.edge_style;}) // EDGE-STYLE
|
|
267
|
-
.style("opacity", function(d) {return d.edge_opacity;}) // EDGE-OPACITY
|
|
268
|
-
;
|
|
269
|
-
|
|
270
|
-
link.append("title").text(function(d) { return d.tooltip; });
|
|
271
|
-
|
|
272
|
-
// ADD TEXT ON THE EDGES (PART 1/2)
|
|
273
|
-
var linkText = svg.selectAll(".link-text")
|
|
274
|
-
.data(graph.links)
|
|
275
|
-
.enter().append("text")
|
|
276
|
-
.attr("class", "link-text")
|
|
277
|
-
.attr("font-size", function(d) {return d.label_fontsize + "px";})
|
|
278
|
-
.style("fill", function(d) {return d.label_color;})
|
|
279
|
-
.style("font-family", "Arial")
|
|
280
|
-
.text(function(d) { return d.label; });
|
|
281
|
-
|
|
511
|
+
// Create empty node/link/link-text selections. renderLinks(), called
|
|
512
|
+
// below, decides whether links go to SVG or canvas and populates
|
|
513
|
+
// link/linkText accordingly.
|
|
514
|
+
var link = svg.selectAll(".link").data([]);
|
|
515
|
+
var linkText = svg.selectAll(".link-text").data([]);
|
|
516
|
+
renderLinks();
|
|
517
|
+
|
|
282
518
|
//Do the same with the circles for the nodes
|
|
283
519
|
var node = svg.selectAll(".node")
|
|
284
520
|
.data(graph.nodes)
|
|
@@ -346,10 +582,14 @@ function d3graphscript(config = {
|
|
|
346
582
|
return;
|
|
347
583
|
}
|
|
348
584
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
.attr("
|
|
585
|
+
if (useCanvasEdges) {
|
|
586
|
+
drawCanvasEdges();
|
|
587
|
+
} else {
|
|
588
|
+
link.attr("x1", function(d) { return d.source.x; })
|
|
589
|
+
.attr("y1", function(d) { return d.source.y; })
|
|
590
|
+
.attr("x2", function(d) { return d.target.x; })
|
|
591
|
+
.attr("y2", function(d) { return d.target.y; });
|
|
592
|
+
}
|
|
353
593
|
|
|
354
594
|
// Position each shape according to its SVG element type:
|
|
355
595
|
// circle / ellipse → cx / cy attributes
|
|
@@ -376,6 +616,8 @@ function d3graphscript(config = {
|
|
|
376
616
|
|
|
377
617
|
node.each(collide(config.collision)); //COLLISION DETECTION. High means a big fight to get untouchable nodes (default=0.5)
|
|
378
618
|
|
|
619
|
+
if (densityVisible) drawDensityLayer();
|
|
620
|
+
|
|
379
621
|
});
|
|
380
622
|
|
|
381
623
|
// --------- MARKER FOR EDGE ENDINGS -----------
|
|
@@ -549,29 +791,45 @@ function d3graphscript(config = {
|
|
|
549
791
|
|
|
550
792
|
d3.select("#thresholdSlider").on("change", threshold);
|
|
551
793
|
|
|
794
|
+
// Master edges on/off toggle — independent of the weight slider, purely
|
|
795
|
+
// visual, doesn't touch the underlying filtered data.
|
|
796
|
+
var edgeToggleBtn = document.getElementById('edgeToggleButton');
|
|
797
|
+
if (edgeToggleBtn) {
|
|
798
|
+
edgeToggleBtn.addEventListener('click', function() {
|
|
799
|
+
edgesVisible = !edgesVisible;
|
|
800
|
+
edgeToggleBtn.textContent = edgesVisible ? 'Hide Edges' : 'Show Edges';
|
|
801
|
+
applyEdgeVisibility();
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Density (clustering heatmap) layer toggle.
|
|
806
|
+
var densityToggleBtn = document.getElementById('densityToggleButton');
|
|
807
|
+
if (densityToggleBtn) {
|
|
808
|
+
densityToggleBtn.addEventListener('click', function() {
|
|
809
|
+
densityVisible = !densityVisible;
|
|
810
|
+
densityToggleBtn.textContent = densityVisible ? 'Hide Density' : 'Show Density';
|
|
811
|
+
d3.select(densityCanvasEl).style("display", densityVisible ? null : "none");
|
|
812
|
+
if (densityVisible) {
|
|
813
|
+
drawDensityLayer();
|
|
814
|
+
} else {
|
|
815
|
+
densityCtx.setTransform(1, 0, 0, 1, 0, 0);
|
|
816
|
+
densityCtx.clearRect(0, 0, densityCanvasEl.width, densityCanvasEl.height);
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Called from the dark-mode toggle (outside this function's scope, in the
|
|
822
|
+
// page's own script) so the density color scheme switches live instead of
|
|
823
|
+
// only reflecting whatever mode the page loaded in.
|
|
824
|
+
window.d3graphSetDarkMode = function(isDark) {
|
|
825
|
+
darkMode = !!isDark;
|
|
826
|
+
if (densityVisible) drawDensityLayer();
|
|
827
|
+
};
|
|
828
|
+
|
|
552
829
|
//Restart the visualisation after any node and link changes
|
|
553
830
|
function restart() {
|
|
554
831
|
|
|
555
|
-
|
|
556
|
-
link = link.data(graph.links);
|
|
557
|
-
link.exit().remove();
|
|
558
|
-
link.enter().insert("line", ".node").attr("class", "link");
|
|
559
|
-
link.style("stroke-width", function(d) {return d.edge_width;}); // LINK-WIDTH AFTER BREAKING WITH SLIDER
|
|
560
|
-
link.style("marker-end", function(d) { // Include the markers.
|
|
561
|
-
if (config.directed) {return 'url(#marker_' + d.marker_end + ')' }})
|
|
562
|
-
link.style("stroke", function(d) {return d.edge_color;}); // EDGE-COLOR AFTER BREAKING WITH SLIDER
|
|
563
|
-
link.style("stroke-dasharray", function(d) {return d.edge_style;}) // EDGE-STYLE
|
|
564
|
-
link.style("opacity", function(d) {return d.edge_opacity;}); // EDGE-OPACITY AFTER BREAKING WITH SLIDER
|
|
565
|
-
|
|
566
|
-
// Update EDGE-LABELS
|
|
567
|
-
linkText = linkText.data(graph.links);
|
|
568
|
-
linkText.exit().remove();
|
|
569
|
-
linkText.enter().append("text")
|
|
570
|
-
.attr("class", "link-text")
|
|
571
|
-
.attr("font-size", function(d) {return d.label_fontsize + "px";})
|
|
572
|
-
.style("fill", function(d) {return d.label_color;})
|
|
573
|
-
.style("font-family", "Arial")
|
|
574
|
-
.text(function(d) { return d.label; });
|
|
832
|
+
renderLinks();
|
|
575
833
|
|
|
576
834
|
node = node.data(graph.nodes);
|
|
577
835
|
node.enter().insert("circle", ".cursor").attr("class", "node").attr("r", 5).call(force.drag);
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
background-color: #222 !important;
|
|
14
14
|
color: #eee !important;
|
|
15
15
|
}
|
|
16
|
-
.dark-mode
|
|
16
|
+
.dark-mode #graphContainer {
|
|
17
17
|
background-color: #222 !important;
|
|
18
18
|
}
|
|
19
19
|
.dark-mode .button-switch {
|
|
@@ -35,12 +35,11 @@
|
|
|
35
35
|
transition: background 0.2s, color 0.2s;
|
|
36
36
|
}
|
|
37
37
|
.top-panel {
|
|
38
|
-
position: absolute;
|
|
39
|
-
top: 10px;
|
|
40
|
-
right: 10px;
|
|
41
|
-
z-index: 1000;
|
|
42
38
|
display: flex;
|
|
39
|
+
flex-wrap: wrap;
|
|
40
|
+
justify-content: flex-end;
|
|
43
41
|
gap: 8px;
|
|
42
|
+
padding: 10px;
|
|
44
43
|
}
|
|
45
44
|
.slider-container {
|
|
46
45
|
display: flex;
|
|
@@ -103,7 +102,9 @@
|
|
|
103
102
|
|
|
104
103
|
<!-- Top right panel with dark mode and save buttons -->
|
|
105
104
|
<div class="top-panel">
|
|
106
|
-
<button id="darkModeSwitch" class="button-switch">{% if dark_mode %}
|
|
105
|
+
<button id="darkModeSwitch" class="button-switch">{% if dark_mode %}☀ Light Mode{% else %}🌙 Dark Mode{% endif %}</button>
|
|
106
|
+
<button id="edgeToggleButton" class="button-switch">Hide Edges</button>
|
|
107
|
+
<button id="densityToggleButton" class="button-switch">{% if show_density %}Hide Density{% else %}Show Density{% endif %}</button>
|
|
107
108
|
<button id="saveButton" class="button-switch">Save</button>
|
|
108
109
|
</div>
|
|
109
110
|
|
|
@@ -131,7 +132,13 @@
|
|
|
131
132
|
background_color: '{{ background_color }}',
|
|
132
133
|
node_text_inside: {{ node_text_inside | lower }},
|
|
133
134
|
max_ticks: {{ max_ticks }},
|
|
135
|
+
canvas_edge_threshold: {{ canvas_edge_threshold }},
|
|
134
136
|
label_zoom_threshold: {{ label_zoom_threshold }},
|
|
137
|
+
density_grid_size: {{ density_grid_size }},
|
|
138
|
+
density_blur: {{ density_blur }},
|
|
139
|
+
density_opacity: {{ density_opacity }},
|
|
140
|
+
show_density: {{ show_density | lower }},
|
|
141
|
+
dark_mode: {{ 'true' if dark_mode else 'false' }},
|
|
135
142
|
})
|
|
136
143
|
});
|
|
137
144
|
|
|
@@ -144,17 +151,18 @@
|
|
|
144
151
|
document.body.classList.add('dark-mode');
|
|
145
152
|
document.body.style.backgroundColor = '#222';
|
|
146
153
|
document.body.style.color = '#eee';
|
|
147
|
-
let
|
|
148
|
-
if (
|
|
149
|
-
darkSwitch.textContent = '
|
|
154
|
+
let graphBg = document.getElementById('graphContainer');
|
|
155
|
+
if (graphBg) graphBg.style.backgroundColor = '#222';
|
|
156
|
+
darkSwitch.textContent = '☀ Light Mode';
|
|
150
157
|
} else {
|
|
151
158
|
document.body.classList.remove('dark-mode');
|
|
152
159
|
document.body.style.backgroundColor = originalBg;
|
|
153
160
|
document.body.style.color = '';
|
|
154
|
-
let
|
|
155
|
-
if (
|
|
161
|
+
let graphBg = document.getElementById('graphContainer');
|
|
162
|
+
if (graphBg) graphBg.style.backgroundColor = originalBg;
|
|
156
163
|
darkSwitch.textContent = '🌙 Dark Mode';
|
|
157
164
|
}
|
|
165
|
+
if (window.d3graphSetDarkMode) window.d3graphSetDarkMode(on);
|
|
158
166
|
}
|
|
159
167
|
// Set initial mode
|
|
160
168
|
setDarkMode(darkMode);
|
|
@@ -164,6 +172,9 @@
|
|
|
164
172
|
});
|
|
165
173
|
|
|
166
174
|
// Save image to svg
|
|
175
|
+
// Note: when the graph is large enough to switch to canvas-rendered edges
|
|
176
|
+
// (see canvas_edge_threshold), this export will include nodes but not edges,
|
|
177
|
+
// since edges live on a separate <canvas> element, not in the SVG DOM.
|
|
167
178
|
document.getElementById('saveButton').addEventListener('click', function () {
|
|
168
179
|
var svgData = document.querySelector('svg').outerHTML;
|
|
169
180
|
var blob = new Blob([svgData], {type: "image/svg+xml;charset=utf-8"});
|
|
@@ -35,4 +35,11 @@ h3 {
|
|
|
35
35
|
wins on specificity ties. */
|
|
36
36
|
.labels-hidden text {
|
|
37
37
|
display: none;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Master edges on/off switch (SVG mode). Canvas mode is handled purely in JS
|
|
41
|
+
(skips drawing / clears the canvas) since there's no DOM to toggle. */
|
|
42
|
+
.edges-hidden .link,
|
|
43
|
+
.edges-hidden .link-text {
|
|
44
|
+
display: none;
|
|
38
45
|
}
|
|
@@ -1,21 +1,45 @@
|
|
|
1
1
|
# %%
|
|
2
|
-
from d3graph import d3graph, vec2adjmat
|
|
2
|
+
from d3graph import d3graph, vec2adjmat
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
d3 = d3graph()
|
|
6
|
+
# Load example data
|
|
7
|
+
df = d3.import_example('socialmedia')
|
|
8
|
+
# Slice first 10000 rows
|
|
9
|
+
df = df[0:10000]
|
|
10
|
+
# Create adjmat
|
|
11
|
+
adjmat = vec2adjmat(source=df['source'], target=df['target'], weight=df['weight'])
|
|
12
|
+
# Update matrix with random weights
|
|
13
|
+
tmpadjmat = np.random.randint(1, 10, size=adjmat.shape)
|
|
14
|
+
adjmat = adjmat*tmpadjmat
|
|
15
|
+
|
|
16
|
+
# Create graph
|
|
17
|
+
d3.graph(adjmat)
|
|
18
|
+
|
|
19
|
+
# Show graph with default settings
|
|
20
|
+
# d3.show()
|
|
21
|
+
|
|
22
|
+
# Show graph with custom specific settings
|
|
23
|
+
d3.show(density_grid_size=60, density_blur=10, density_opacity=0.6, dark_mode=True, show_density=True)
|
|
24
|
+
|
|
25
|
+
# %%
|
|
26
|
+
from d3graph import d3graph, vec2adjmat
|
|
3
27
|
import pandas as pd
|
|
4
28
|
import time
|
|
5
29
|
|
|
6
30
|
|
|
7
31
|
df = pd.read_csv('https://github.com/d3blocks/d3blocks/files/11995798/Df.csv', sep=',', index_col=False)
|
|
8
32
|
del df['Unnamed: 0']
|
|
9
|
-
df = df[0:
|
|
33
|
+
df = df[0:10000]
|
|
10
34
|
adjmat = vec2adjmat(source=df['source'], target=df['target'], weight=df['weight'])
|
|
11
35
|
|
|
12
36
|
d3 = d3graph()
|
|
13
37
|
|
|
14
38
|
start = time.perf_counter()
|
|
15
|
-
d3.graph(adjmat)
|
|
39
|
+
d3.graph(adjmat, min_weight=1)
|
|
16
40
|
end = time.perf_counter()
|
|
17
41
|
|
|
18
|
-
d3.show()
|
|
42
|
+
d3.show(density_grid_size=80)
|
|
19
43
|
|
|
20
44
|
print(f"Elapsed: {end - start:.6f} seconds")
|
|
21
45
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|