d3graph 2.9.2__tar.gz → 2.9.4__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.4}/PKG-INFO +9 -1
- {d3graph-2.9.2 → d3graph-2.9.4}/README.md +8 -0
- {d3graph-2.9.2 → d3graph-2.9.4}/d3graph/__init__.py +1 -1
- {d3graph-2.9.2 → d3graph-2.9.4}/d3graph/d3graph.py +52 -3
- {d3graph-2.9.2 → d3graph-2.9.4}/d3graph/d3js/d3graphscript.js +311 -53
- {d3graph-2.9.2 → d3graph-2.9.4}/d3graph/d3js/index.html.j2 +37 -24
- {d3graph-2.9.2 → d3graph-2.9.4}/d3graph/d3js/style.css +7 -0
- {d3graph-2.9.2 → d3graph-2.9.4}/d3graph/examples.py +36 -5
- d3graph-2.9.4/d3graph/examples_rankings.py +308 -0
- d3graph-2.9.4/d3graph/examples_rankings_Pvalue.py +378 -0
- {d3graph-2.9.2 → d3graph-2.9.4/d3graph.egg-info}/PKG-INFO +9 -1
- {d3graph-2.9.2 → d3graph-2.9.4}/d3graph.egg-info/SOURCES.txt +2 -0
- {d3graph-2.9.2 → d3graph-2.9.4}/LICENSE +0 -0
- {d3graph-2.9.2 → d3graph-2.9.4}/MANIFEST.in +0 -0
- {d3graph-2.9.2 → d3graph-2.9.4}/d3graph/d3js/d3.v3.js +0 -0
- {d3graph-2.9.2 → d3graph-2.9.4}/d3graph/examples_docs.py +0 -0
- {d3graph-2.9.2 → d3graph-2.9.4}/d3graph.egg-info/dependency_links.txt +0 -0
- {d3graph-2.9.2 → d3graph-2.9.4}/d3graph.egg-info/requires.txt +0 -0
- {d3graph-2.9.2 → d3graph-2.9.4}/d3graph.egg-info/top_level.txt +0 -0
- {d3graph-2.9.2 → d3graph-2.9.4}/pyproject.toml +0 -0
- {d3graph-2.9.2 → d3graph-2.9.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: d3graph
|
|
3
|
-
Version: 2.9.
|
|
3
|
+
Version: 2.9.4
|
|
4
4
|
Summary: Python package to create interactive network based on d3js.
|
|
5
5
|
Author-email: Erdogan Taskesen <erdogant@gmail.com>
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -103,6 +103,14 @@ from d3graph import d3graph
|
|
|
103
103
|
</a>
|
|
104
104
|
</p>
|
|
105
105
|
|
|
106
|
+
|
|
107
|
+
<p align="left">
|
|
108
|
+
<a href="https://erdogant.github.io/docs/d3graph/titanic_example/index.html">
|
|
109
|
+
<img src="https://github.com/d3blocks/d3blocks/blob/main/docs/figs/d3graph/socialmedia.gif" width="1000"/>
|
|
110
|
+
</a>
|
|
111
|
+
</p>
|
|
112
|
+
|
|
113
|
+
|
|
106
114
|
<hr>
|
|
107
115
|
|
|
108
116
|
### Contributors
|
|
@@ -72,6 +72,14 @@ from d3graph import d3graph
|
|
|
72
72
|
</a>
|
|
73
73
|
</p>
|
|
74
74
|
|
|
75
|
+
|
|
76
|
+
<p align="left">
|
|
77
|
+
<a href="https://erdogant.github.io/docs/d3graph/titanic_example/index.html">
|
|
78
|
+
<img src="https://github.com/d3blocks/d3blocks/blob/main/docs/figs/d3graph/socialmedia.gif" width="1000"/>
|
|
79
|
+
</a>
|
|
80
|
+
</p>
|
|
81
|
+
|
|
82
|
+
|
|
75
83
|
<hr>
|
|
76
84
|
|
|
77
85
|
### Contributors
|
|
@@ -123,6 +123,12 @@ 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,
|
|
131
|
+
show_controls: bool = True,
|
|
126
132
|
) -> None:
|
|
127
133
|
"""Build and show the graph.
|
|
128
134
|
|
|
@@ -177,6 +183,39 @@ class d3graph:
|
|
|
177
183
|
Node and edge labels are hidden below this zoom scale (unreadable anyway, and
|
|
178
184
|
costly to keep rendering for large graphs) and reappear once zoomed back in
|
|
179
185
|
past it. Uses a single CSS class toggle, not per-label work. 0: never hide.
|
|
186
|
+
canvas_edge_threshold : int, (default: 2000)
|
|
187
|
+
Above this many visible edges, edges are drawn on a <canvas> layer instead of
|
|
188
|
+
as individual SVG <line> elements. SVG's per-element DOM overhead is what makes
|
|
189
|
+
tens of thousands of edges freeze the page; canvas draw calls stay cheap
|
|
190
|
+
regardless of edge count. Nodes always stay SVG (drag/click/tooltips). Only
|
|
191
|
+
applies once edges exceed this count, so small/medium graphs are unaffected -
|
|
192
|
+
note that in canvas mode, the "Save as SVG" export won't include edges, since
|
|
193
|
+
they no longer live in the SVG DOM.
|
|
194
|
+
show_density : bool, (default: False)
|
|
195
|
+
Adds a node-clustering heatmap layer (grid-binned density of node positions),
|
|
196
|
+
drawn on its own canvas beneath the edges and nodes, with a toggle button in
|
|
197
|
+
the UI ("Show/Hide Density") to turn it on/off regardless of this default.
|
|
198
|
+
Recomputed from live node positions each frame it's visible, so it tracks the
|
|
199
|
+
force layout as nodes settle, and it responds to the weight/component sliders
|
|
200
|
+
since it's based on whichever nodes are currently on screen. Color scheme is
|
|
201
|
+
a yellow-to-red heat gradient in light mode, single-hue blue in dark mode
|
|
202
|
+
(updates live when dark mode is toggled).
|
|
203
|
+
density_grid_size : int, (default: 40)
|
|
204
|
+
Grid resolution for the density heatmap (cells along the longer axis of the
|
|
205
|
+
node bounding box). Higher = finer detail on tight clusters, more cells to draw.
|
|
206
|
+
density_blur : int, (default: 8)
|
|
207
|
+
Blur radius (px) applied to the heatmap for a smooth look instead of a
|
|
208
|
+
blocky grid.
|
|
209
|
+
density_opacity : float, (default: 0.6)
|
|
210
|
+
Maximum heatmap opacity, reached at the highest-density grid cell.
|
|
211
|
+
show_controls : bool, (default: True)
|
|
212
|
+
Whether to render the top-panel buttons (Dark Mode, Hide Edges, Show Density,
|
|
213
|
+
Save) at all. Set to False when embedding the generated HTML into an existing
|
|
214
|
+
page/app that provides its own UI chrome and doesn't need d3graph's built-in
|
|
215
|
+
controls - the buttons (and their JS wiring) are omitted entirely rather than
|
|
216
|
+
just hidden, so there's no extra DOM/clutter in the embed. The weight/component
|
|
217
|
+
sliders are controlled separately via show_slider; the Save button specifically
|
|
218
|
+
is also controlled via save_button, independent of this.
|
|
180
219
|
|
|
181
220
|
Returns
|
|
182
221
|
-------
|
|
@@ -201,6 +240,12 @@ class d3graph:
|
|
|
201
240
|
self.config['node_text_inside'] = node_text_inside
|
|
202
241
|
self.config['max_ticks'] = max_ticks
|
|
203
242
|
self.config['label_zoom_threshold'] = label_zoom_threshold
|
|
243
|
+
self.config['canvas_edge_threshold'] = canvas_edge_threshold
|
|
244
|
+
self.config['show_density'] = show_density
|
|
245
|
+
self.config['density_grid_size'] = density_grid_size
|
|
246
|
+
self.config['density_blur'] = density_blur
|
|
247
|
+
self.config['density_opacity'] = density_opacity
|
|
248
|
+
self.config['show_controls'] = show_controls
|
|
204
249
|
|
|
205
250
|
# Allow show() to override the link_tension set at __init__ time
|
|
206
251
|
if link_tension is not None:
|
|
@@ -802,7 +847,6 @@ class d3graph:
|
|
|
802
847
|
|
|
803
848
|
# Hide slider
|
|
804
849
|
show_slider = ['', ''] if self.config['show_slider'] else ['<!--', '-->']
|
|
805
|
-
show_save_button = ['', ''] if self.config['save_button'] else ['<!--', '-->']
|
|
806
850
|
# Set width and height to screen resolution if None.
|
|
807
851
|
width = 'window.screen.width' if self.config['figsize'][0] is None else self.config['figsize'][0]
|
|
808
852
|
height = 'window.screen.height' if self.config['figsize'][1] is None else self.config['figsize'][1]
|
|
@@ -824,6 +868,13 @@ class d3graph:
|
|
|
824
868
|
'sticky': self.config.get('sticky', False),
|
|
825
869
|
'max_ticks': self.config.get('max_ticks', 300),
|
|
826
870
|
'label_zoom_threshold': self.config.get('label_zoom_threshold', 0.6),
|
|
871
|
+
'canvas_edge_threshold': self.config.get('canvas_edge_threshold', 2000),
|
|
872
|
+
'show_density': self.config.get('show_density', False),
|
|
873
|
+
'density_grid_size': self.config.get('density_grid_size', 40),
|
|
874
|
+
'density_blur': self.config.get('density_blur', 8),
|
|
875
|
+
'density_opacity': self.config.get('density_opacity', 0.6),
|
|
876
|
+
'show_controls': self.config.get('show_controls', True),
|
|
877
|
+
'save_button': self.config['save_button'],
|
|
827
878
|
'node_text_inside': self.config.get('node_text_inside', False),
|
|
828
879
|
'CLICK_COMMENT': CLICK_COMMENT,
|
|
829
880
|
'CLICK_FILL': click_properties['fill'],
|
|
@@ -832,8 +883,6 @@ class d3graph:
|
|
|
832
883
|
'CLICK_STROKEW': click_properties['stroke-width'],
|
|
833
884
|
'slider_comment_start': show_slider[0],
|
|
834
885
|
'slider_comment_stop': show_slider[1],
|
|
835
|
-
'save_button_comment_start': show_save_button[0],
|
|
836
|
-
'save_button_comment_stop': show_save_button[1],
|
|
837
886
|
'SET_SLIDER': self.config['set_slider'],
|
|
838
887
|
'SUPPORT': support,
|
|
839
888
|
'background_color': self.config['background_color'],
|
|
@@ -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);
|