d3graph 3.0.1__tar.gz → 3.1.0__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-3.0.1/d3graph.egg-info → d3graph-3.1.0}/PKG-INFO +2 -1
- {d3graph-3.0.1 → d3graph-3.1.0}/d3graph/__init__.py +1 -1
- {d3graph-3.0.1 → d3graph-3.1.0}/d3graph/d3js/d3graphscript.js +401 -53
- {d3graph-3.0.1 → d3graph-3.1.0}/d3graph/d3js/index.html.j2 +13 -1
- {d3graph-3.0.1 → d3graph-3.1.0}/d3graph/d3js/style.css +95 -1
- {d3graph-3.0.1 → d3graph-3.1.0}/d3graph/examples.py +2 -2
- {d3graph-3.0.1 → d3graph-3.1.0/d3graph.egg-info}/PKG-INFO +2 -1
- {d3graph-3.0.1 → d3graph-3.1.0}/d3graph.egg-info/requires.txt +1 -0
- {d3graph-3.0.1 → d3graph-3.1.0}/pyproject.toml +1 -0
- {d3graph-3.0.1 → d3graph-3.1.0}/LICENSE +0 -0
- {d3graph-3.0.1 → d3graph-3.1.0}/MANIFEST.in +0 -0
- {d3graph-3.0.1 → d3graph-3.1.0}/README.md +0 -0
- {d3graph-3.0.1 → d3graph-3.1.0}/d3graph/d3graph.py +0 -0
- {d3graph-3.0.1 → d3graph-3.1.0}/d3graph/d3js/d3.v3.js +0 -0
- {d3graph-3.0.1 → d3graph-3.1.0}/d3graph/examples_docs.py +0 -0
- {d3graph-3.0.1 → d3graph-3.1.0}/d3graph/examples_rankings.py +0 -0
- {d3graph-3.0.1 → d3graph-3.1.0}/d3graph/examples_rankings_Pvalue.py +0 -0
- {d3graph-3.0.1 → d3graph-3.1.0}/d3graph.egg-info/SOURCES.txt +0 -0
- {d3graph-3.0.1 → d3graph-3.1.0}/d3graph.egg-info/dependency_links.txt +0 -0
- {d3graph-3.0.1 → d3graph-3.1.0}/d3graph.egg-info/top_level.txt +0 -0
- {d3graph-3.0.1 → d3graph-3.1.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: d3graph
|
|
3
|
-
Version: 3.0
|
|
3
|
+
Version: 3.1.0
|
|
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
|
|
@@ -28,6 +28,7 @@ Requires-Dist: markupsafe
|
|
|
28
28
|
Requires-Dist: python-louvain
|
|
29
29
|
Requires-Dist: datazets
|
|
30
30
|
Requires-Dist: distfit
|
|
31
|
+
Requires-Dist: tqdm
|
|
31
32
|
Dynamic: license-file
|
|
32
33
|
|
|
33
34
|
# D3graph: Interactive force-directed networks
|
|
@@ -58,11 +58,26 @@ function d3graphscript(config = {
|
|
|
58
58
|
var useCanvasEdges = false;
|
|
59
59
|
var canvasEl, ctx;
|
|
60
60
|
var currentTransform = { scale: 1, translate: [0, 0] };
|
|
61
|
+
var zoomBehavior = null; // set when SVG is created; used by _jumpToNode to sync state
|
|
61
62
|
// Master on/off switch for edges (independent of the weight/component
|
|
62
63
|
// sliders) — lets the user clear visual clutter on large graphs to see
|
|
63
64
|
// node structure/clustering without redrawing or refiltering anything.
|
|
64
65
|
var edgesVisible = true;
|
|
65
66
|
|
|
67
|
+
// ---- VISUAL SCALE MULTIPLIERS (physics panel sliders) ----
|
|
68
|
+
// Independent of the per-node data values; multiplied in at render time so
|
|
69
|
+
// changing them never mutates the underlying data and is instantly reversible.
|
|
70
|
+
var edgeWidthMult = 1.0; // 0.1 – 5.0, driven by the "Edge Width" slider
|
|
71
|
+
var nodeSizeMult = 1.0; // 0.1 – 5.0, driven by the "Node Size" slider
|
|
72
|
+
|
|
73
|
+
// ---- PERFORMANCE: cached quadtree for collision detection ----
|
|
74
|
+
// Rebuilding a quadtree from scratch every single tick is O(N log N) and
|
|
75
|
+
// dominates frame time on graphs with thousands of nodes. We reuse one
|
|
76
|
+
// quadtree across the whole tick instead — the quadtree is still built once
|
|
77
|
+
// per tick (nodes moved since last tick), but NOT once per node per tick.
|
|
78
|
+
var _collideQuadtree = null;
|
|
79
|
+
var _collideTickId = -1; // matches tickCount so we rebuild once per tick
|
|
80
|
+
|
|
66
81
|
// ---- STATS PANEL (recolor nodes by a network statistic) ----
|
|
67
82
|
// node_pagerank / node_hits_hub / node_hits_authority are precomputed
|
|
68
83
|
// server-side (networkx) and normalized to [0, 1]; the panel just picks
|
|
@@ -126,6 +141,9 @@ function d3graphscript(config = {
|
|
|
126
141
|
// structure pops. highlightedNodeIndex is null when nothing is
|
|
127
142
|
// highlighted, or the .index of the currently-highlighted node.
|
|
128
143
|
var highlightedNodeIndex = null;
|
|
144
|
+
|
|
145
|
+
// ---- SEARCH ----
|
|
146
|
+
var searchHighlightedIndex = null; // node .index of the current search hit
|
|
129
147
|
// True (default): clicking a node highlights every node/edge in its whole
|
|
130
148
|
// connected component, however many hops away. False: only its directly-
|
|
131
149
|
// connected (one-hop) neighbors/edges light up, the old behavior.
|
|
@@ -400,17 +418,15 @@ function d3graphscript(config = {
|
|
|
400
418
|
.style("top", 0)
|
|
401
419
|
.style("left", 0)
|
|
402
420
|
.style("background-color", "transparent")
|
|
403
|
-
.call(d3.behavior.zoom().on("zoom", function () {
|
|
421
|
+
.call((zoomBehavior = d3.behavior.zoom().on("zoom", function () {
|
|
404
422
|
panOrZoomOccurred = true;
|
|
405
423
|
svg.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")");
|
|
406
|
-
// Single class toggle (cheap) rather than iterating every label on
|
|
407
|
-
// every zoom/pan event — CSS handles hiding all descendant <text>.
|
|
408
424
|
svg.classed("labels-hidden", d3.event.scale < labelZoomThreshold);
|
|
409
425
|
currentTransform.scale = d3.event.scale;
|
|
410
426
|
currentTransform.translate = d3.event.translate;
|
|
411
427
|
drawCanvasEdges();
|
|
412
428
|
drawDensityLayer();
|
|
413
|
-
}))
|
|
429
|
+
})))
|
|
414
430
|
.on("dblclick.zoom", null)
|
|
415
431
|
.append("g")
|
|
416
432
|
|
|
@@ -418,48 +434,104 @@ function d3graphscript(config = {
|
|
|
418
434
|
// pan/zoom transform. No-ops when the graph is small enough to stay on SVG.
|
|
419
435
|
function drawCanvasEdges() {
|
|
420
436
|
if (!useCanvasEdges || !ctx || !edgesVisible) return;
|
|
437
|
+
|
|
438
|
+
var sc = currentTransform.scale;
|
|
439
|
+
var tx = currentTransform.translate[0];
|
|
440
|
+
var ty = currentTransform.translate[1];
|
|
441
|
+
|
|
442
|
+
// Compute viewport bounds in graph-space so we can skip edges whose
|
|
443
|
+
// both endpoints are well outside the visible area (cheap early-out).
|
|
444
|
+
var vpMargin = 20 / sc; // a small buffer in graph units
|
|
445
|
+
var vpMinX = (-tx / sc) - vpMargin;
|
|
446
|
+
var vpMaxX = ((canvasEl.width - tx) / sc) + vpMargin;
|
|
447
|
+
var vpMinY = (-ty / sc) - vpMargin;
|
|
448
|
+
var vpMaxY = ((canvasEl.height - ty) / sc) + vpMargin;
|
|
449
|
+
|
|
421
450
|
ctx.save();
|
|
422
451
|
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
423
452
|
ctx.clearRect(0, 0, canvasEl.width, canvasEl.height);
|
|
424
|
-
ctx.setTransform(
|
|
425
|
-
|
|
426
|
-
|
|
453
|
+
ctx.setTransform(sc, 0, 0, sc, tx, ty);
|
|
454
|
+
|
|
455
|
+
// Batch edges by (strokeStyle + lineWidth + globalAlpha + shadowBlur +
|
|
456
|
+
// dash pattern) to minimise expensive canvas state changes. Instead of
|
|
457
|
+
// setting style then stroking once per edge, we bucket all edges that
|
|
458
|
+
// share the same style, then set the style once and stroke the whole
|
|
459
|
+
// bucket in a single path. On a 20 000-edge graph this typically
|
|
460
|
+
// reduces unique state-change sequences from 20 000 to a handful.
|
|
461
|
+
|
|
462
|
+
// Bucket map: key → { path points, ctx state }
|
|
463
|
+
// We collect lines per bucket, then replay with a single ctx state set.
|
|
464
|
+
var buckets = {}; // key → { a, lw, sw, sb, sc_str, dash, lines: [[x1,y1,x2,y2],...] }
|
|
465
|
+
|
|
466
|
+
var links = graph.links;
|
|
467
|
+
var nLinks = links.length;
|
|
468
|
+
|
|
469
|
+
for (var i = 0; i < nLinks; i++) {
|
|
470
|
+
var d = links[i];
|
|
427
471
|
if (!d.source || !d.target || typeof d.source.x !== 'number') continue;
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
472
|
+
|
|
473
|
+
var sx = d.source.x, sy = d.source.y;
|
|
474
|
+
var ex = d.target.x, ey = d.target.y;
|
|
475
|
+
|
|
476
|
+
// Viewport cull: skip if both endpoints are outside on the same side.
|
|
477
|
+
if ((sx < vpMinX && ex < vpMinX) || (sx > vpMaxX && ex > vpMaxX) ||
|
|
478
|
+
(sy < vpMinY && ey < vpMinY) || (sy > vpMaxY && ey > vpMaxY)) continue;
|
|
479
|
+
|
|
432
480
|
var baseOpacity = (d.edge_opacity !== undefined && d.edge_opacity !== null) ? d.edge_opacity : 0.6;
|
|
433
|
-
var baseWidth
|
|
434
|
-
|
|
481
|
+
var baseWidth = (d.edge_width || 1) * edgeWidthMult;
|
|
482
|
+
var col = d.edge_color || '#999';
|
|
483
|
+
var dashStyle = d.edge_style;
|
|
484
|
+
|
|
485
|
+
var alpha, lw, shadowBlur, shadowColor;
|
|
435
486
|
|
|
436
487
|
if (highlightedNodeIndex !== null) {
|
|
437
|
-
// Click-highlight takes priority over the stat-driven boost below —
|
|
438
|
-
// same precedence SVG-mode edges get in applyNeighborHighlightStyling().
|
|
439
|
-
// When highlightedComponentSet is set (highlightFullNetwork), an edge
|
|
440
|
-
// counts as connected if BOTH its ends are anywhere in the clicked
|
|
441
|
-
// node's connected component, not just directly touching it.
|
|
442
488
|
var isConnected = highlightedComponentSet
|
|
443
489
|
? (highlightedComponentSet[d.source.index] && highlightedComponentSet[d.target.index])
|
|
444
490
|
: (d.source.index === highlightedNodeIndex || d.target.index === highlightedNodeIndex);
|
|
445
491
|
if (isConnected) {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
ctx.shadowColor = EDGE_GLOW_COLOR;
|
|
449
|
-
ctx.shadowBlur = EDGE_GLOW_CANVAS_BLUR;
|
|
492
|
+
alpha = 1; lw = baseWidth * EDGE_GLOW_WIDTH_MULT;
|
|
493
|
+
shadowBlur = EDGE_GLOW_CANVAS_BLUR; shadowColor = EDGE_GLOW_COLOR;
|
|
450
494
|
} else {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
ctx.shadowBlur = 0;
|
|
495
|
+
alpha = NEIGHBOR_DIM_OPACITY; lw = baseWidth;
|
|
496
|
+
shadowBlur = 0; shadowColor = '';
|
|
454
497
|
}
|
|
455
498
|
} else {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
499
|
+
alpha = statHighlightActive ? Math.min(1, baseOpacity * EDGE_HIGHLIGHT_OPACITY_MULT) : baseOpacity;
|
|
500
|
+
lw = statHighlightActive ? baseWidth * EDGE_HIGHLIGHT_WIDTH_MULT : baseWidth;
|
|
501
|
+
shadowBlur = 0; shadowColor = '';
|
|
459
502
|
}
|
|
460
503
|
|
|
504
|
+
// Round to 2 decimal places for bucketing to avoid float key explosion.
|
|
505
|
+
var key = col + '|' + (Math.round(alpha * 100)) + '|' + (Math.round(lw * 100)) +
|
|
506
|
+
'|' + shadowBlur + '|' + (shadowColor || '') + '|' + (dashStyle || '');
|
|
507
|
+
if (!buckets[key]) {
|
|
508
|
+
buckets[key] = { col: col, alpha: alpha, lw: lw,
|
|
509
|
+
shadowBlur: shadowBlur, shadowColor: shadowColor,
|
|
510
|
+
dash: dashStyle === 'dashed' ? [6, 3] : dashStyle === 'dotted' ? [1.5, 3] : [],
|
|
511
|
+
lines: [] };
|
|
512
|
+
}
|
|
513
|
+
buckets[key].lines.push(sx, sy, ex, ey);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Replay each bucket as a single compound path — one state setup, one stroke.
|
|
517
|
+
for (var key in buckets) {
|
|
518
|
+
var b = buckets[key];
|
|
519
|
+
ctx.strokeStyle = b.col;
|
|
520
|
+
ctx.globalAlpha = b.alpha;
|
|
521
|
+
ctx.lineWidth = b.lw;
|
|
522
|
+
ctx.shadowBlur = b.shadowBlur;
|
|
523
|
+
ctx.shadowColor = b.shadowColor || '';
|
|
524
|
+
ctx.setLineDash(b.dash);
|
|
525
|
+
|
|
526
|
+
ctx.beginPath();
|
|
527
|
+
var pts = b.lines;
|
|
528
|
+
for (var j = 0, jn = pts.length; j < jn; j += 4) {
|
|
529
|
+
ctx.moveTo(pts[j], pts[j + 1]);
|
|
530
|
+
ctx.lineTo(pts[j + 2], pts[j + 3]);
|
|
531
|
+
}
|
|
461
532
|
ctx.stroke();
|
|
462
533
|
}
|
|
534
|
+
|
|
463
535
|
ctx.restore();
|
|
464
536
|
}
|
|
465
537
|
|
|
@@ -601,7 +673,7 @@ function d3graphscript(config = {
|
|
|
601
673
|
linkEnter.append("title").text(function(d) { return d.tooltip; });
|
|
602
674
|
link.attr("marker-end", function(d) {
|
|
603
675
|
if (config.directed) { return 'url(#marker_' + d.marker_end + ')' } });
|
|
604
|
-
link.style("stroke-width", function(d) { return d.edge_width; });
|
|
676
|
+
link.style("stroke-width", function(d) { return (parseFloat(d.edge_width) || 1) * edgeWidthMult; });
|
|
605
677
|
link.style("stroke", function(d) { return d.edge_color; });
|
|
606
678
|
link.style("stroke-dasharray", function(d) { return d.edge_style; });
|
|
607
679
|
link.style("opacity", function(d) { return d.edge_opacity; });
|
|
@@ -694,6 +766,17 @@ function d3graphscript(config = {
|
|
|
694
766
|
// Append the correct shape per node (circle, ellipse, rect, triangle, etc.)
|
|
695
767
|
appendShape(node);
|
|
696
768
|
|
|
769
|
+
// Search highlight rings — two concentric circles behind each node,
|
|
770
|
+
// made visible via the .search-hit CSS class on the parent <g>.
|
|
771
|
+
// They live in graph-space (same SVG coordinate system as nodes) and
|
|
772
|
+
// are repositioned every tick alongside node shapes in updatePositions().
|
|
773
|
+
node.each(function() {
|
|
774
|
+
var g = d3.select(this);
|
|
775
|
+
var rings = g.insert('g', ':first-child').attr('class', 'search-rings');
|
|
776
|
+
rings.append('circle').attr('class', 'search-ring search-ring-border');
|
|
777
|
+
rings.append('circle').attr('class', 'search-ring search-ring-yellow');
|
|
778
|
+
});
|
|
779
|
+
|
|
697
780
|
// Text in nodes
|
|
698
781
|
node.append("text")
|
|
699
782
|
.attr("dx", function(d) { return config.node_text_inside ? 0 : 10; })
|
|
@@ -744,6 +827,8 @@ function d3graphscript(config = {
|
|
|
744
827
|
// path → transform translate(x, y)
|
|
745
828
|
// Scoped to the bound `node` selection instead of re-querying the
|
|
746
829
|
// entire DOM every tick (was: d3.selectAll(".node-shape")).
|
|
830
|
+
// nodeSizeMult is applied here for position-independent shapes (path),
|
|
831
|
+
// and through renderStatStyling / applyNodeSizeScale for radius attrs.
|
|
747
832
|
node.select(".node-shape").each(function(d) {
|
|
748
833
|
var el = d3.select(this);
|
|
749
834
|
var tag = this.tagName.toLowerCase();
|
|
@@ -754,6 +839,12 @@ function d3graphscript(config = {
|
|
|
754
839
|
}
|
|
755
840
|
});
|
|
756
841
|
|
|
842
|
+
// Search ring positions — cheap: two attr sets per node per tick.
|
|
843
|
+
node.select('.search-rings').each(function(d) {
|
|
844
|
+
var r = (parseFloat(d.node_size) || 8) * nodeSizeMult + 5;
|
|
845
|
+
d3.select(this).selectAll('.search-ring').attr('cx', d.x).attr('cy', d.y).attr('r', r);
|
|
846
|
+
});
|
|
847
|
+
|
|
757
848
|
// Scoped to node labels only (was: d3.selectAll("text"), which also
|
|
758
849
|
// re-matched every link-text element on every tick).
|
|
759
850
|
node.select("text").attr("x", function(d) { return d.x; })
|
|
@@ -765,6 +856,54 @@ function d3graphscript(config = {
|
|
|
765
856
|
if (densityVisible) drawDensityLayer();
|
|
766
857
|
}
|
|
767
858
|
|
|
859
|
+
// One-shot pass to apply the visual scale multipliers to all SVG nodes and
|
|
860
|
+
// edges without touching positions. Called when a slider changes, not
|
|
861
|
+
// every tick — so the cost is negligible compared to the tick loop.
|
|
862
|
+
function applyVisualScaleMultipliers() {
|
|
863
|
+
// Node sizes
|
|
864
|
+
node.select(".node-shape").each(function(d) {
|
|
865
|
+
var el = d3.select(this);
|
|
866
|
+
var tag = this.tagName.toLowerCase();
|
|
867
|
+
var baseR = parseFloat(d.node_size) || 8;
|
|
868
|
+
// Also factor in any stat-driven scaling that renderStatStyling applied.
|
|
869
|
+
var statScale = 1;
|
|
870
|
+
if (currentStatKey && currentStatKey !== 'network_clustering') {
|
|
871
|
+
var v = d[currentStatKey + '_colorNorm'];
|
|
872
|
+
if (typeof v === 'number' && !isNaN(v)) {
|
|
873
|
+
var isSignificance = (currentStatKey === 'node_proba');
|
|
874
|
+
var sizeVal = isSignificance ? (1 - (d[currentStatKey] || 0)) : v;
|
|
875
|
+
statScale = 1.2 + sizeVal * 1.3;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
var r = baseR * nodeSizeMult * statScale;
|
|
879
|
+
if (tag === 'circle') {
|
|
880
|
+
el.attr('r', r);
|
|
881
|
+
} else if (tag === 'ellipse') {
|
|
882
|
+
el.attr('rx', r * 1.6).attr('ry', r);
|
|
883
|
+
} else if (tag === 'path') {
|
|
884
|
+
el.attr('d', shapePathD(d.node_marker, r));
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
// Search ring radii
|
|
889
|
+
node.select('.search-rings').each(function(d) {
|
|
890
|
+
var r = (parseFloat(d.node_size) || 8) * nodeSizeMult + 5;
|
|
891
|
+
d3.select(this).selectAll('.search-ring').attr('r', r);
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
// SVG edge widths
|
|
895
|
+
if (!useCanvasEdges) {
|
|
896
|
+
link.style("stroke-width", function(d) {
|
|
897
|
+
var w = parseFloat(d.edge_width) || 1;
|
|
898
|
+
w *= edgeWidthMult;
|
|
899
|
+
if (statHighlightActive) w *= EDGE_HIGHLIGHT_WIDTH_MULT;
|
|
900
|
+
return w;
|
|
901
|
+
});
|
|
902
|
+
} else {
|
|
903
|
+
drawCanvasEdges();
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
768
907
|
force.on("tick", function() {
|
|
769
908
|
// Auto-stop after maxTicks so a large graph doesn't keep re-running
|
|
770
909
|
// collision detection / layout math for thousands of frames while
|
|
@@ -811,23 +950,31 @@ function d3graphscript(config = {
|
|
|
811
950
|
|
|
812
951
|
|
|
813
952
|
// collision detection
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
953
|
+
// Optimisation: build the quadtree ONCE per tick (stored in _collideQuadtree)
|
|
954
|
+
// rather than once per visited node. On a 5 000-node graph this alone cuts
|
|
955
|
+
// the per-tick cost of collision from O(N² log N) rebuilds to one O(N log N)
|
|
956
|
+
// build + O(N log N) queries.
|
|
957
|
+
var padding = 1; // separation between circles
|
|
817
958
|
|
|
818
959
|
function collide(alpha) {
|
|
819
|
-
|
|
960
|
+
// Rebuild the shared quadtree only when we have advanced to a new tick.
|
|
961
|
+
if (_collideTickId !== tickCount) {
|
|
962
|
+
_collideQuadtree = d3.geom.quadtree(graph.nodes);
|
|
963
|
+
_collideTickId = tickCount;
|
|
964
|
+
}
|
|
965
|
+
var quadtree = _collideQuadtree;
|
|
820
966
|
return function(d) {
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
967
|
+
// Use the node's actual rendered radius (scaled by nodeSizeMult) so
|
|
968
|
+
// collision boundaries match what the user sees on screen.
|
|
969
|
+
var r = (parseFloat(d.node_size) || 8) * nodeSizeMult;
|
|
970
|
+
var rb = r * 2 + padding;
|
|
971
|
+
var nx1 = d.x - rb, nx2 = d.x + rb,
|
|
972
|
+
ny1 = d.y - rb, ny2 = d.y + rb;
|
|
826
973
|
quadtree.visit(function(quad, x1, y1, x2, y2) {
|
|
827
974
|
if (quad.point && (quad.point !== d)) {
|
|
828
975
|
var x = d.x - quad.point.x,
|
|
829
|
-
|
|
830
|
-
|
|
976
|
+
y = d.y - quad.point.y,
|
|
977
|
+
l = Math.sqrt(x * x + y * y);
|
|
831
978
|
if (l < rb) {
|
|
832
979
|
l = (l - rb) / l * alpha;
|
|
833
980
|
d.x -= x *= l;
|
|
@@ -1007,7 +1154,7 @@ function d3graphscript(config = {
|
|
|
1007
1154
|
var statValue = currentStatKey ? d[currentStatKey] : null;
|
|
1008
1155
|
var hasStatValue = currentStatKey && typeof statValue === "number" && !isNaN(statValue);
|
|
1009
1156
|
var statScale = hasStatValue ? 1.2 + statValue * 1.3 : 1;
|
|
1010
|
-
var clickScale = statScale * {{ CLICK_SIZE }};
|
|
1157
|
+
var clickScale = statScale * {{ CLICK_SIZE }} * nodeSizeMult;
|
|
1011
1158
|
|
|
1012
1159
|
if (tag === "circle") {
|
|
1013
1160
|
el.attr("r", baseR * clickScale);
|
|
@@ -1656,12 +1803,13 @@ function d3graphscript(config = {
|
|
|
1656
1803
|
? SIGNIFICANCE_BORDER_COLOR
|
|
1657
1804
|
: d.node_color_edge;
|
|
1658
1805
|
|
|
1806
|
+
var scaledR = baseR * scale * nodeSizeMult;
|
|
1659
1807
|
if (tag === 'circle') {
|
|
1660
|
-
el.attr('r',
|
|
1808
|
+
el.attr('r', scaledR);
|
|
1661
1809
|
} else if (tag === 'ellipse') {
|
|
1662
|
-
el.attr('rx',
|
|
1810
|
+
el.attr('rx', scaledR * 1.6).attr('ry', scaledR);
|
|
1663
1811
|
} else if (tag === 'path') {
|
|
1664
|
-
el.attr('d', shapePathD(d.node_marker,
|
|
1812
|
+
el.attr('d', shapePathD(d.node_marker, scaledR));
|
|
1665
1813
|
}
|
|
1666
1814
|
|
|
1667
1815
|
el.style('fill', fillColor)
|
|
@@ -1673,7 +1821,7 @@ function d3graphscript(config = {
|
|
|
1673
1821
|
// SVG-mode edges: bump width/opacity directly on the current selection.
|
|
1674
1822
|
// Canvas-mode edges read statHighlightActive inside drawCanvasEdges().
|
|
1675
1823
|
link.style("stroke-width", function(d) {
|
|
1676
|
-
var w = parseFloat(d.edge_width) || 1;
|
|
1824
|
+
var w = (parseFloat(d.edge_width) || 1) * edgeWidthMult;
|
|
1677
1825
|
return statHighlightActive ? w * EDGE_HIGHLIGHT_WIDTH_MULT : w;
|
|
1678
1826
|
});
|
|
1679
1827
|
link.style("opacity", function(d) {
|
|
@@ -1863,26 +2011,53 @@ function d3graphscript(config = {
|
|
|
1863
2011
|
wirePhysicsSlider('collisionSlider', 'collisionValue', setCollision);
|
|
1864
2012
|
wirePhysicsSlider('linkTensionSlider', 'linkTensionValue', setLinkTension);
|
|
1865
2013
|
|
|
2014
|
+
// ---- Edge Width multiplier slider ----
|
|
2015
|
+
function setEdgeWidthMult(value) {
|
|
2016
|
+
edgeWidthMult = value;
|
|
2017
|
+
// Cheap visual-only update; no need to reheat the simulation.
|
|
2018
|
+
applyVisualScaleMultipliers();
|
|
2019
|
+
}
|
|
2020
|
+
wirePhysicsSlider('edgeWidthMultSlider', 'edgeWidthMultValue', setEdgeWidthMult);
|
|
2021
|
+
|
|
2022
|
+
// ---- Node Size multiplier slider ----
|
|
2023
|
+
function setNodeSizeMult(value) {
|
|
2024
|
+
nodeSizeMult = value;
|
|
2025
|
+
// Invalidate cached quadtree so collision detection uses updated radii.
|
|
2026
|
+
_collideTickId = -1;
|
|
2027
|
+
applyVisualScaleMultipliers();
|
|
2028
|
+
}
|
|
2029
|
+
wirePhysicsSlider('nodeSizeMultSlider', 'nodeSizeMultValue', setNodeSizeMult);
|
|
2030
|
+
|
|
1866
2031
|
var physicsResetBtn = document.getElementById('physicsResetButton');
|
|
1867
2032
|
if (physicsResetBtn) {
|
|
1868
2033
|
physicsResetBtn.addEventListener('click', function() {
|
|
1869
2034
|
setCharge(DEFAULT_CHARGE);
|
|
1870
2035
|
setCollision(DEFAULT_COLLISION);
|
|
1871
2036
|
setLinkTension(DEFAULT_LINK_TENSION);
|
|
2037
|
+
setEdgeWidthMult(1.0);
|
|
2038
|
+
setNodeSizeMult(1.0);
|
|
1872
2039
|
|
|
1873
|
-
var chargeSlider
|
|
2040
|
+
var chargeSlider = document.getElementById('chargeSlider');
|
|
1874
2041
|
var collisionSlider = document.getElementById('collisionSlider');
|
|
1875
|
-
var tensionSlider
|
|
1876
|
-
|
|
2042
|
+
var tensionSlider = document.getElementById('linkTensionSlider');
|
|
2043
|
+
var ewSlider = document.getElementById('edgeWidthMultSlider');
|
|
2044
|
+
var nsSlider = document.getElementById('nodeSizeMultSlider');
|
|
2045
|
+
if (chargeSlider) chargeSlider.value = DEFAULT_CHARGE;
|
|
1877
2046
|
if (collisionSlider) collisionSlider.value = DEFAULT_COLLISION;
|
|
1878
|
-
if (tensionSlider)
|
|
2047
|
+
if (tensionSlider) tensionSlider.value = DEFAULT_LINK_TENSION;
|
|
2048
|
+
if (ewSlider) ewSlider.value = 1.0;
|
|
2049
|
+
if (nsSlider) nsSlider.value = 1.0;
|
|
1879
2050
|
|
|
1880
|
-
var chargeVal
|
|
2051
|
+
var chargeVal = document.getElementById('chargeValue');
|
|
1881
2052
|
var collisionVal = document.getElementById('collisionValue');
|
|
1882
|
-
var tensionVal
|
|
1883
|
-
|
|
2053
|
+
var tensionVal = document.getElementById('linkTensionValue');
|
|
2054
|
+
var ewVal = document.getElementById('edgeWidthMultValue');
|
|
2055
|
+
var nsVal = document.getElementById('nodeSizeMultValue');
|
|
2056
|
+
if (chargeVal) chargeVal.textContent = DEFAULT_CHARGE;
|
|
1884
2057
|
if (collisionVal) collisionVal.textContent = DEFAULT_COLLISION;
|
|
1885
|
-
if (tensionVal)
|
|
2058
|
+
if (tensionVal) tensionVal.textContent = DEFAULT_LINK_TENSION;
|
|
2059
|
+
if (ewVal) ewVal.textContent = '1';
|
|
2060
|
+
if (nsVal) nsVal.textContent = '1';
|
|
1886
2061
|
});
|
|
1887
2062
|
}
|
|
1888
2063
|
|
|
@@ -2036,4 +2211,177 @@ function d3graphscript(config = {
|
|
|
2036
2211
|
|
|
2037
2212
|
window.addEventListener("resize", resizeGraph);
|
|
2038
2213
|
|
|
2214
|
+
// =========================================================================
|
|
2215
|
+
// SEARCH
|
|
2216
|
+
// Matches node_name (label) and name (id) fields. Results are ranked:
|
|
2217
|
+
// prefix matches first, then substring; ties broken by shorter name.
|
|
2218
|
+
// Selecting a result highlights the node with a yellow ring AND selects it
|
|
2219
|
+
// into the Node Info panel (same as clicking it directly).
|
|
2220
|
+
// =========================================================================
|
|
2221
|
+
var searchInputEl = document.getElementById('searchInput');
|
|
2222
|
+
var searchResultsEl = document.getElementById('searchResults');
|
|
2223
|
+
if (searchInputEl && searchResultsEl) {
|
|
2224
|
+
var _searchActiveIdx = -1;
|
|
2225
|
+
var _searchMatches = []; // array of node datum objects
|
|
2226
|
+
|
|
2227
|
+
function _clearSearchHighlight() {
|
|
2228
|
+
searchHighlightedIndex = null;
|
|
2229
|
+
node.classed('search-hit', false);
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
function _highlightSearchNode(datum) {
|
|
2233
|
+
searchHighlightedIndex = datum.index;
|
|
2234
|
+
node.classed('search-hit', function(d) { return d.index === datum.index; });
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
function _jumpToNode(datum) {
|
|
2238
|
+
// 1. Highlight ring
|
|
2239
|
+
_highlightSearchNode(datum);
|
|
2240
|
+
|
|
2241
|
+
// 2. Pan the SVG so the node is centred in the viewport at the current scale.
|
|
2242
|
+
var s = currentTransform.scale || 1;
|
|
2243
|
+
var cx = (width || window.innerWidth) / 2;
|
|
2244
|
+
var cy = (height || window.innerHeight) / 2;
|
|
2245
|
+
var tx = cx - datum.x * s;
|
|
2246
|
+
var ty = cy - datum.y * s;
|
|
2247
|
+
|
|
2248
|
+
// KEY FIX: tell the zoom behaviour about the new translate/scale BEFORE
|
|
2249
|
+
// the transition fires. Without this, the next user scroll/drag fires
|
|
2250
|
+
// the zoom handler with the behaviour's stale internal state, snapping
|
|
2251
|
+
// the view back to wherever it was before the jump.
|
|
2252
|
+
if (zoomBehavior) {
|
|
2253
|
+
zoomBehavior.translate([tx, ty]).scale(s);
|
|
2254
|
+
}
|
|
2255
|
+
currentTransform.scale = s;
|
|
2256
|
+
currentTransform.translate = [tx, ty];
|
|
2257
|
+
|
|
2258
|
+
// Animate the SVG <g> to the new position.
|
|
2259
|
+
var svgG = d3.select(svg.node().parentNode).select('svg g');
|
|
2260
|
+
svg.transition().duration(500)
|
|
2261
|
+
.attr('transform', 'translate(' + tx + ',' + ty + ') scale(' + s + ')')
|
|
2262
|
+
.each('end', function() {
|
|
2263
|
+
// Redraw canvas overlays at the final position once animation settles.
|
|
2264
|
+
drawCanvasEdges();
|
|
2265
|
+
drawDensityLayer();
|
|
2266
|
+
});
|
|
2267
|
+
svg.classed('labels-hidden', s < labelZoomThreshold);
|
|
2268
|
+
// Immediate redraw for canvas edges (they stay correct during animation
|
|
2269
|
+
// because currentTransform is already updated above).
|
|
2270
|
+
drawCanvasEdges();
|
|
2271
|
+
|
|
2272
|
+
// 3. Select the node into the click-highlight / Node Info panel —
|
|
2273
|
+
// identical to the user clicking it directly.
|
|
2274
|
+
if (highlightedNodeIndex !== datum.index) {
|
|
2275
|
+
navigateNodeInfo(datum, null, false);
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
// 4. Close dropdown
|
|
2279
|
+
searchResultsEl.classList.remove('open');
|
|
2280
|
+
searchInputEl.value = datum.node_name || datum.name || '';
|
|
2281
|
+
searchInputEl.blur();
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
function _renderSearchResults(query) {
|
|
2285
|
+
var q = query.trim().toLowerCase();
|
|
2286
|
+
_searchActiveIdx = -1;
|
|
2287
|
+
if (!q) {
|
|
2288
|
+
_searchMatches = [];
|
|
2289
|
+
searchResultsEl.innerHTML = '';
|
|
2290
|
+
searchResultsEl.classList.remove('open');
|
|
2291
|
+
return;
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
// Build candidate list from live graph.nodes (respects current slider state)
|
|
2295
|
+
var all = graph.nodes;
|
|
2296
|
+
_searchMatches = all.filter(function(d) {
|
|
2297
|
+
var label = (d.node_name || d.name || '').toLowerCase();
|
|
2298
|
+
return label.indexOf(q) !== -1;
|
|
2299
|
+
}).sort(function(a, b) {
|
|
2300
|
+
var al = (a.node_name || a.name || '').toLowerCase();
|
|
2301
|
+
var bl = (b.node_name || b.name || '').toLowerCase();
|
|
2302
|
+
var ap = al.indexOf(q) === 0 ? 0 : 1;
|
|
2303
|
+
var bp = bl.indexOf(q) === 0 ? 0 : 1;
|
|
2304
|
+
if (ap !== bp) return ap - bp;
|
|
2305
|
+
return al.length - bl.length || al.localeCompare(bl);
|
|
2306
|
+
}).slice(0, 12);
|
|
2307
|
+
|
|
2308
|
+
if (!_searchMatches.length) {
|
|
2309
|
+
searchResultsEl.innerHTML = '<div class="search-item" style="opacity:0.5;cursor:default">No matches</div>';
|
|
2310
|
+
searchResultsEl.classList.add('open');
|
|
2311
|
+
return;
|
|
2312
|
+
}
|
|
2313
|
+
searchResultsEl.innerHTML = _searchMatches.map(function(d, i) {
|
|
2314
|
+
var label = d.node_name || d.name || ('Node ' + d.index);
|
|
2315
|
+
var meta = 'idx ' + d.index;
|
|
2316
|
+
if (d.group !== undefined && d.group !== null && String(d.group) !== '-1' && String(d.group) !== '') {
|
|
2317
|
+
meta += ' · g' + d.group;
|
|
2318
|
+
}
|
|
2319
|
+
return '<div class="search-item" data-idx="' + i + '">' +
|
|
2320
|
+
escapeHtml(label) +
|
|
2321
|
+
'<div class="meta">' + escapeHtml(meta) + '</div></div>';
|
|
2322
|
+
}).join('');
|
|
2323
|
+
searchResultsEl.classList.add('open');
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
searchInputEl.addEventListener('input', function() {
|
|
2327
|
+
_renderSearchResults(searchInputEl.value);
|
|
2328
|
+
});
|
|
2329
|
+
searchInputEl.addEventListener('focus', function() {
|
|
2330
|
+
if (searchInputEl.value.trim()) _renderSearchResults(searchInputEl.value);
|
|
2331
|
+
});
|
|
2332
|
+
searchInputEl.addEventListener('keydown', function(e) {
|
|
2333
|
+
var open = searchResultsEl.classList.contains('open') && _searchMatches.length;
|
|
2334
|
+
if (!open) {
|
|
2335
|
+
if (e.key === 'Enter' && searchInputEl.value.trim()) {
|
|
2336
|
+
if (_searchMatches.length) _jumpToNode(_searchMatches[0]);
|
|
2337
|
+
}
|
|
2338
|
+
return;
|
|
2339
|
+
}
|
|
2340
|
+
if (e.key === 'ArrowDown') {
|
|
2341
|
+
e.preventDefault();
|
|
2342
|
+
_searchActiveIdx = Math.min(_searchMatches.length - 1, _searchActiveIdx + 1);
|
|
2343
|
+
} else if (e.key === 'ArrowUp') {
|
|
2344
|
+
e.preventDefault();
|
|
2345
|
+
_searchActiveIdx = Math.max(0, _searchActiveIdx - 1);
|
|
2346
|
+
} else if (e.key === 'Enter') {
|
|
2347
|
+
e.preventDefault();
|
|
2348
|
+
var d = _searchMatches[_searchActiveIdx >= 0 ? _searchActiveIdx : 0];
|
|
2349
|
+
if (d) _jumpToNode(d);
|
|
2350
|
+
return;
|
|
2351
|
+
} else if (e.key === 'Escape') {
|
|
2352
|
+
searchResultsEl.classList.remove('open');
|
|
2353
|
+
searchInputEl.blur();
|
|
2354
|
+
return;
|
|
2355
|
+
} else { return; }
|
|
2356
|
+
|
|
2357
|
+
// Highlight active item
|
|
2358
|
+
searchResultsEl.querySelectorAll('.search-item').forEach(function(el, i) {
|
|
2359
|
+
el.classList.toggle('active', i === _searchActiveIdx);
|
|
2360
|
+
if (i === _searchActiveIdx) el.scrollIntoView({ block: 'nearest' });
|
|
2361
|
+
});
|
|
2362
|
+
});
|
|
2363
|
+
|
|
2364
|
+
searchResultsEl.addEventListener('mousedown', function(e) {
|
|
2365
|
+
// mousedown fires before blur so the list stays open long enough to register
|
|
2366
|
+
var item = e.target.closest('.search-item');
|
|
2367
|
+
if (!item) return;
|
|
2368
|
+
e.preventDefault();
|
|
2369
|
+
var idx = parseInt(item.getAttribute('data-idx'), 10);
|
|
2370
|
+
if (!isNaN(idx) && _searchMatches[idx]) _jumpToNode(_searchMatches[idx]);
|
|
2371
|
+
});
|
|
2372
|
+
|
|
2373
|
+
// Close dropdown when clicking outside
|
|
2374
|
+
document.addEventListener('click', function(e) {
|
|
2375
|
+
if (!e.target.closest('.search-wrap')) searchResultsEl.classList.remove('open');
|
|
2376
|
+
});
|
|
2377
|
+
|
|
2378
|
+
// Clear search highlight when the user clicks empty background
|
|
2379
|
+
var _origBgClick = container.on('click');
|
|
2380
|
+
container.on('click', function() {
|
|
2381
|
+
if (panOrZoomOccurred) { panOrZoomOccurred = false; return; }
|
|
2382
|
+
_clearSearchHighlight();
|
|
2383
|
+
clearNeighborHighlight();
|
|
2384
|
+
});
|
|
2385
|
+
} // end search block
|
|
2386
|
+
|
|
2039
2387
|
}
|
|
@@ -19,7 +19,11 @@
|
|
|
19
19
|
HTML into a page that supplies its own button chrome but still wants
|
|
20
20
|
the slider). -->
|
|
21
21
|
{% if show_controls or max_slider > min_slider %}
|
|
22
|
-
<div class="header-row">
|
|
22
|
+
<div class="header-row">
|
|
23
|
+
<div class="search-wrap">
|
|
24
|
+
<input class="search-input" id="searchInput" type="search" placeholder="Find a node…" autocomplete="off" spellcheck="false" />
|
|
25
|
+
<div class="search-results" id="searchResults"></div>
|
|
26
|
+
</div>
|
|
23
27
|
{% if max_slider > min_slider %}
|
|
24
28
|
{{ slider_comment_start }}
|
|
25
29
|
<div class="slider-container">
|
|
@@ -92,6 +96,14 @@
|
|
|
92
96
|
<label for="linkTensionSlider">Edge Tension <span id="linkTensionValue">{{ link_tension }}</span></label>
|
|
93
97
|
<input type="range" id="linkTensionSlider" min="0" max="5" step="0.1" value="{{ link_tension }}">
|
|
94
98
|
</div>
|
|
99
|
+
<div class="physics-control">
|
|
100
|
+
<label for="edgeWidthMultSlider">Edge Width <span id="edgeWidthMultValue">1</span>×</label>
|
|
101
|
+
<input type="range" id="edgeWidthMultSlider" min="0.1" max="5" step="0.1" value="1">
|
|
102
|
+
</div>
|
|
103
|
+
<div class="physics-control">
|
|
104
|
+
<label for="nodeSizeMultSlider">Node Size <span id="nodeSizeMultValue">1</span>×</label>
|
|
105
|
+
<input type="range" id="nodeSizeMultSlider" min="0.1" max="5" step="0.1" value="1">
|
|
106
|
+
</div>
|
|
95
107
|
<button id="physicsResetButton" class="button-switch">Reset to Default</button>
|
|
96
108
|
</div>
|
|
97
109
|
</div>
|
|
@@ -434,4 +434,98 @@ h3 {
|
|
|
434
434
|
.dark-mode .styled-slider::-moz-range-thumb {
|
|
435
435
|
background: #222;
|
|
436
436
|
border: 2px solid #eee;
|
|
437
|
-
}
|
|
437
|
+
}
|
|
438
|
+
/* ---- Search bar ---- */
|
|
439
|
+
.search-wrap {
|
|
440
|
+
position: relative;
|
|
441
|
+
display: inline-flex;
|
|
442
|
+
align-items: center;
|
|
443
|
+
grid-column: 1; /* left slot of .header-row grid */
|
|
444
|
+
justify-self: start;
|
|
445
|
+
margin: 0;
|
|
446
|
+
}
|
|
447
|
+
.search-input {
|
|
448
|
+
width: 200px;
|
|
449
|
+
padding: 5px 10px;
|
|
450
|
+
border-radius: 6px;
|
|
451
|
+
border: 1px solid #ccc;
|
|
452
|
+
background: #fff;
|
|
453
|
+
color: #222;
|
|
454
|
+
font-size: 13px;
|
|
455
|
+
font-family: "Helvetica", Arial, sans-serif;
|
|
456
|
+
outline: none;
|
|
457
|
+
transition: border-color 0.15s, width 0.2s;
|
|
458
|
+
}
|
|
459
|
+
.search-input:focus {
|
|
460
|
+
border-color: #1ABC9C;
|
|
461
|
+
width: 260px;
|
|
462
|
+
}
|
|
463
|
+
.dark-mode .search-input {
|
|
464
|
+
background: #333;
|
|
465
|
+
color: #eee;
|
|
466
|
+
border-color: #555;
|
|
467
|
+
}
|
|
468
|
+
.dark-mode .search-input:focus {
|
|
469
|
+
border-color: #1ABC9C;
|
|
470
|
+
}
|
|
471
|
+
.search-results {
|
|
472
|
+
display: none;
|
|
473
|
+
position: absolute;
|
|
474
|
+
top: calc(100% + 4px);
|
|
475
|
+
left: 0;
|
|
476
|
+
min-width: 200px;
|
|
477
|
+
max-height: 260px;
|
|
478
|
+
overflow-y: auto;
|
|
479
|
+
background: #fff;
|
|
480
|
+
border: 1px solid #ccc;
|
|
481
|
+
border-radius: 6px;
|
|
482
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
483
|
+
z-index: 2000;
|
|
484
|
+
font-family: "Helvetica", Arial, sans-serif;
|
|
485
|
+
font-size: 13px;
|
|
486
|
+
}
|
|
487
|
+
.search-results.open { display: block; }
|
|
488
|
+
.dark-mode .search-results {
|
|
489
|
+
background: #2a2a2a;
|
|
490
|
+
border-color: #555;
|
|
491
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
|
|
492
|
+
}
|
|
493
|
+
.search-item {
|
|
494
|
+
padding: 7px 12px;
|
|
495
|
+
cursor: pointer;
|
|
496
|
+
color: #222;
|
|
497
|
+
border-bottom: 1px solid rgba(0,0,0,0.05);
|
|
498
|
+
line-height: 1.3;
|
|
499
|
+
}
|
|
500
|
+
.search-item:last-child { border-bottom: none; }
|
|
501
|
+
.search-item:hover,
|
|
502
|
+
.search-item.active { background: #e8f8f5; color: #111; }
|
|
503
|
+
.dark-mode .search-item { color: #eee; border-bottom-color: rgba(255,255,255,0.07); }
|
|
504
|
+
.dark-mode .search-item:hover,
|
|
505
|
+
.dark-mode .search-item.active { background: #1e4d42; }
|
|
506
|
+
.search-item .meta {
|
|
507
|
+
font-size: 11px;
|
|
508
|
+
color: #888;
|
|
509
|
+
margin-top: 1px;
|
|
510
|
+
}
|
|
511
|
+
.dark-mode .search-item .meta { color: #aaa; }
|
|
512
|
+
|
|
513
|
+
/* Search-hit highlight ring on the found node (SVG overlay circle) */
|
|
514
|
+
.search-ring-yellow {
|
|
515
|
+
fill: none;
|
|
516
|
+
stroke: #ffe600;
|
|
517
|
+
stroke-width: 3px;
|
|
518
|
+
pointer-events: none;
|
|
519
|
+
opacity: 0;
|
|
520
|
+
transition: opacity 0.2s;
|
|
521
|
+
}
|
|
522
|
+
.search-ring-border {
|
|
523
|
+
fill: none;
|
|
524
|
+
stroke: rgba(0,0,0,0.25);
|
|
525
|
+
stroke-width: 5px;
|
|
526
|
+
pointer-events: none;
|
|
527
|
+
opacity: 0;
|
|
528
|
+
transition: opacity 0.2s;
|
|
529
|
+
}
|
|
530
|
+
.search-hit .search-ring-yellow,
|
|
531
|
+
.search-hit .search-ring-border { opacity: 1; }
|
|
@@ -6,7 +6,7 @@ d3 = d3graph()
|
|
|
6
6
|
# Load example data
|
|
7
7
|
df = d3.import_example('socialmedia')
|
|
8
8
|
# Slice first 10000 rows
|
|
9
|
-
df = df[0:
|
|
9
|
+
df = df[0:5000]
|
|
10
10
|
# Create adjmat
|
|
11
11
|
adjmat = vec2adjmat(source=df['source'], target=df['target'], weight=df['weight'])
|
|
12
12
|
# Update matrix with random weights
|
|
@@ -17,7 +17,7 @@ adjmat = vec2adjmat(source=df['source'], target=df['target'], weight=df['weight'
|
|
|
17
17
|
d3.graph(adjmat)
|
|
18
18
|
|
|
19
19
|
# Compute node significance for specified network statistic
|
|
20
|
-
d3.network_significance(adjmat, 'pagerank', n_top=100, n_random=100)
|
|
20
|
+
# d3.network_significance(adjmat, 'pagerank', n_top=100, n_random=100)
|
|
21
21
|
|
|
22
22
|
# Show graph with custom specific settings
|
|
23
23
|
d3.show(density_grid_size=60,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: d3graph
|
|
3
|
-
Version: 3.0
|
|
3
|
+
Version: 3.1.0
|
|
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
|
|
@@ -28,6 +28,7 @@ Requires-Dist: markupsafe
|
|
|
28
28
|
Requires-Dist: python-louvain
|
|
29
29
|
Requires-Dist: datazets
|
|
30
30
|
Requires-Dist: distfit
|
|
31
|
+
Requires-Dist: tqdm
|
|
31
32
|
Dynamic: license-file
|
|
32
33
|
|
|
33
34
|
# D3graph: Interactive force-directed networks
|
|
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
|
|
File without changes
|