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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: d3graph
3
- Version: 2.9.2
3
+ Version: 2.9.3
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
@@ -17,7 +17,7 @@ from d3graph.d3graph import (
17
17
 
18
18
  __author__ = 'Erdogan Tasksen'
19
19
  __email__ = 'erdogant@gmail.com'
20
- __version__ = '2.9.2'
20
+ __version__ = '2.9.3'
21
21
 
22
22
  # Setup root logger
23
23
  _logger = logging.getLogger('d3graph')
@@ -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
- //Append a SVG to the body of the html page. Assign this SVG as an object to svg
227
- var svg = d3.select("body").append("svg")
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("background-color", background_color)
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 all the line svgs but without locations yet
257
- var link = svg.selectAll(".link")
258
- .data(graph.links)
259
- .enter().append("line")
260
- .attr("class", "link")
261
- .attr('marker-start', function(d){ return 'url(#marker_' + d.marker_start + ')' })
262
- .attr("marker-end", function(d) {
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
- link.attr("x1", function(d) { return d.source.x; })
350
- .attr("y1", function(d) { return d.source.y; })
351
- .attr("x2", function(d) { return d.target.x; })
352
- .attr("y2", function(d) { return d.target.y; });
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
- // Update EDGE-LINKS
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 svg {
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 %}☀️ Light Mode{% else %}🌙 Dark Mode{% endif %}</button>
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 svg = document.querySelector('svg');
148
- if (svg) svg.style.backgroundColor = '#222';
149
- darkSwitch.textContent = '☀️ Light Mode';
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 svg = document.querySelector('svg');
155
- if (svg) svg.style.backgroundColor = originalBg;
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, import_example
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:5000]
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: d3graph
3
- Version: 2.9.2
3
+ Version: 2.9.3
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes