d3graph 2.9.4__tar.gz → 2.9.5__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.4/d3graph.egg-info → d3graph-2.9.5}/PKG-INFO +1 -1
- {d3graph-2.9.4 → d3graph-2.9.5}/d3graph/__init__.py +1 -1
- {d3graph-2.9.4 → d3graph-2.9.5}/d3graph/d3graph.py +70 -2
- {d3graph-2.9.4 → d3graph-2.9.5}/d3graph/d3js/d3graphscript.js +171 -44
- {d3graph-2.9.4 → d3graph-2.9.5}/d3graph/d3js/index.html.j2 +13 -87
- d3graph-2.9.5/d3graph/d3js/style.css +173 -0
- {d3graph-2.9.4 → d3graph-2.9.5}/d3graph/examples.py +1 -1
- {d3graph-2.9.4 → d3graph-2.9.5/d3graph.egg-info}/PKG-INFO +1 -1
- d3graph-2.9.4/d3graph/d3js/style.css +0 -45
- {d3graph-2.9.4 → d3graph-2.9.5}/LICENSE +0 -0
- {d3graph-2.9.4 → d3graph-2.9.5}/MANIFEST.in +0 -0
- {d3graph-2.9.4 → d3graph-2.9.5}/README.md +0 -0
- {d3graph-2.9.4 → d3graph-2.9.5}/d3graph/d3js/d3.v3.js +0 -0
- {d3graph-2.9.4 → d3graph-2.9.5}/d3graph/examples_docs.py +0 -0
- {d3graph-2.9.4 → d3graph-2.9.5}/d3graph/examples_rankings.py +0 -0
- {d3graph-2.9.4 → d3graph-2.9.5}/d3graph/examples_rankings_Pvalue.py +0 -0
- {d3graph-2.9.4 → d3graph-2.9.5}/d3graph.egg-info/SOURCES.txt +0 -0
- {d3graph-2.9.4 → d3graph-2.9.5}/d3graph.egg-info/dependency_links.txt +0 -0
- {d3graph-2.9.4 → d3graph-2.9.5}/d3graph.egg-info/requires.txt +0 -0
- {d3graph-2.9.4 → d3graph-2.9.5}/d3graph.egg-info/top_level.txt +0 -0
- {d3graph-2.9.4 → d3graph-2.9.5}/pyproject.toml +0 -0
- {d3graph-2.9.4 → d3graph-2.9.5}/setup.cfg +0 -0
|
@@ -262,7 +262,7 @@ class d3graph:
|
|
|
262
262
|
# Make slider
|
|
263
263
|
self.setup_slider()
|
|
264
264
|
# Create json
|
|
265
|
-
json_data = json_create(self.G)
|
|
265
|
+
json_data = json_create(self.G, compute_stats=self.config.get('show_controls', True))
|
|
266
266
|
# Create html with json file embedded
|
|
267
267
|
html = self.write_html(json_data, overwrite=overwrite)
|
|
268
268
|
# Display the chart
|
|
@@ -1049,13 +1049,20 @@ def check_logger(verbose: [str, int] = 'info'):
|
|
|
1049
1049
|
|
|
1050
1050
|
|
|
1051
1051
|
# %% Write network in json file
|
|
1052
|
-
def json_create(G: nx.Graph) -> str:
|
|
1052
|
+
def json_create(G: nx.Graph, compute_stats: bool = True) -> str:
|
|
1053
1053
|
"""Create json from Graph.
|
|
1054
1054
|
|
|
1055
1055
|
Parameters
|
|
1056
1056
|
----------
|
|
1057
1057
|
G : Networkx object
|
|
1058
1058
|
Graph G
|
|
1059
|
+
compute_stats : bool, (default: True)
|
|
1060
|
+
Compute PageRank, HITS (hub/authority), degree/closeness/betweenness centrality
|
|
1061
|
+
per node and embed them as node_pagerank / node_hits_hub / node_hits_authority /
|
|
1062
|
+
node_degree_centrality / node_closeness_centrality / node_betweenness_centrality,
|
|
1063
|
+
normalized to [0, 1], for the interactive stats side panel. Skipped when the
|
|
1064
|
+
panel won't be shown (show_controls=False) to avoid the wasted computation.
|
|
1065
|
+
Betweenness is approximated (sampled) above 500 nodes to bound generation time.
|
|
1059
1066
|
|
|
1060
1067
|
Returns
|
|
1061
1068
|
-------
|
|
@@ -1072,6 +1079,59 @@ def json_create(G: nx.Graph) -> str:
|
|
|
1072
1079
|
source.append(node_id[edge[0] == node_ui][0])
|
|
1073
1080
|
target.append(node_id[edge[1] == node_ui][0])
|
|
1074
1081
|
|
|
1082
|
+
# Common network statistics for the optional interactive stats panel
|
|
1083
|
+
# (recolors nodes by the selected metric). Computed once here, server-side,
|
|
1084
|
+
# rather than in the browser, since networkx already has these and it keeps
|
|
1085
|
+
# the JS side simple (just reads a precomputed per-node number).
|
|
1086
|
+
if compute_stats and len(node_ui) > 0:
|
|
1087
|
+
def _normalize_dict(d):
|
|
1088
|
+
vals = np.array(list(d.values()), dtype=float)
|
|
1089
|
+
vmin, vmax = vals.min(), vals.max()
|
|
1090
|
+
if (vmax - vmin) < 1e-12:
|
|
1091
|
+
return {k: 0.0 for k in d}
|
|
1092
|
+
return {k: float((v - vmin) / (vmax - vmin)) for k, v in d.items()}
|
|
1093
|
+
|
|
1094
|
+
try:
|
|
1095
|
+
pagerank_norm = _normalize_dict(nx.pagerank(G, weight='weight'))
|
|
1096
|
+
except Exception:
|
|
1097
|
+
pagerank_norm = {n: 0.0 for n in G.nodes()}
|
|
1098
|
+
|
|
1099
|
+
try:
|
|
1100
|
+
hubs, authorities = nx.hits(G, max_iter=1000)
|
|
1101
|
+
hubs_norm = _normalize_dict(hubs)
|
|
1102
|
+
authorities_norm = _normalize_dict(authorities)
|
|
1103
|
+
except Exception:
|
|
1104
|
+
hubs_norm = {n: 0.0 for n in G.nodes()}
|
|
1105
|
+
authorities_norm = {n: 0.0 for n in G.nodes()}
|
|
1106
|
+
|
|
1107
|
+
try:
|
|
1108
|
+
degree_norm = _normalize_dict(nx.degree_centrality(G))
|
|
1109
|
+
except Exception:
|
|
1110
|
+
degree_norm = {n: 0.0 for n in G.nodes()}
|
|
1111
|
+
|
|
1112
|
+
try:
|
|
1113
|
+
closeness_norm = _normalize_dict(nx.closeness_centrality(G))
|
|
1114
|
+
except Exception:
|
|
1115
|
+
closeness_norm = {n: 0.0 for n in G.nodes()}
|
|
1116
|
+
|
|
1117
|
+
try:
|
|
1118
|
+
# Betweenness is O(V*E) (Brandes) - exact for smaller graphs, but that
|
|
1119
|
+
# gets slow fast on larger ones. Above 500 nodes, approximate via a
|
|
1120
|
+
# fixed-seed sample of source nodes so generation time stays bounded.
|
|
1121
|
+
n_nodes = G.number_of_nodes()
|
|
1122
|
+
k = min(100, n_nodes) if n_nodes > 500 else None
|
|
1123
|
+
betweenness_norm = _normalize_dict(
|
|
1124
|
+
nx.betweenness_centrality(G, k=k, weight='weight', seed=42 if k else None))
|
|
1125
|
+
except Exception:
|
|
1126
|
+
betweenness_norm = {n: 0.0 for n in G.nodes()}
|
|
1127
|
+
else:
|
|
1128
|
+
pagerank_norm = {n: 0.0 for n in G.nodes()}
|
|
1129
|
+
hubs_norm = {n: 0.0 for n in G.nodes()}
|
|
1130
|
+
authorities_norm = {n: 0.0 for n in G.nodes()}
|
|
1131
|
+
degree_norm = {n: 0.0 for n in G.nodes()}
|
|
1132
|
+
closeness_norm = {n: 0.0 for n in G.nodes()}
|
|
1133
|
+
betweenness_norm = {n: 0.0 for n in G.nodes()}
|
|
1134
|
+
|
|
1075
1135
|
# Set edge properties
|
|
1076
1136
|
links = pd.DataFrame([*G.edges.values()]).T.to_dict()
|
|
1077
1137
|
links_new = []
|
|
@@ -1108,6 +1168,14 @@ def json_create(G: nx.Graph) -> str:
|
|
|
1108
1168
|
nodes[i]['node_color_edge'] = nodes[i].pop('edge_color')
|
|
1109
1169
|
nodes[i]['node_fontcolor'] = nodes[i].pop('fontcolor')
|
|
1110
1170
|
nodes[i]['node_fontsize'] = nodes[i].pop('fontsize')
|
|
1171
|
+
# Stats for the interactive side panel (see compute_stats above)
|
|
1172
|
+
nid = node_ui[i]
|
|
1173
|
+
nodes[i]['node_pagerank'] = pagerank_norm.get(nid, 0.0)
|
|
1174
|
+
nodes[i]['node_hits_hub'] = hubs_norm.get(nid, 0.0)
|
|
1175
|
+
nodes[i]['node_hits_authority'] = authorities_norm.get(nid, 0.0)
|
|
1176
|
+
nodes[i]['node_degree_centrality'] = degree_norm.get(nid, 0.0)
|
|
1177
|
+
nodes[i]['node_closeness_centrality'] = closeness_norm.get(nid, 0.0)
|
|
1178
|
+
nodes[i]['node_betweenness_centrality'] = betweenness_norm.get(nid, 0.0)
|
|
1111
1179
|
# Combine all information into new list
|
|
1112
1180
|
nodes_new[i] = nodes[i]
|
|
1113
1181
|
data = {'links': links_new, 'nodes': nodes_new}
|
|
@@ -51,6 +51,23 @@ function d3graphscript(config = {
|
|
|
51
51
|
// node structure/clustering without redrawing or refiltering anything.
|
|
52
52
|
var edgesVisible = true;
|
|
53
53
|
|
|
54
|
+
// ---- STATS PANEL (recolor nodes by a network statistic) ----
|
|
55
|
+
// node_pagerank / node_hits_hub / node_hits_authority are precomputed
|
|
56
|
+
// server-side (networkx) and normalized to [0, 1]; the panel just picks
|
|
57
|
+
// which one (if any) drives node fill color. null = original node colors.
|
|
58
|
+
var currentStatKey = null;
|
|
59
|
+
|
|
60
|
+
// COLOR SCHEME
|
|
61
|
+
var statColorScale = d3.scale.linear()
|
|
62
|
+
.domain([0, 0.5, 1])
|
|
63
|
+
.range(["#2166ac", "#f7f7f7", "#b2182b"]);
|
|
64
|
+
|
|
65
|
+
// While a stat is active, nodes/edges get a visibility boost (bigger nodes,
|
|
66
|
+
// thicker/more opaque edges) so the highlighted structure reads clearly.
|
|
67
|
+
var statHighlightActive = false;
|
|
68
|
+
var EDGE_HIGHLIGHT_WIDTH_MULT = 1.8;
|
|
69
|
+
var EDGE_HIGHLIGHT_OPACITY_MULT = 1.6;
|
|
70
|
+
|
|
54
71
|
// ---- DENSITY (clustering heatmap) LAYER ----
|
|
55
72
|
// Grid-binned node density, drawn on its own canvas beneath the edges and
|
|
56
73
|
// nodes. Recomputed from live node positions every tick it's visible, so
|
|
@@ -328,8 +345,10 @@ function d3graphscript(config = {
|
|
|
328
345
|
ctx.moveTo(d.source.x, d.source.y);
|
|
329
346
|
ctx.lineTo(d.target.x, d.target.y);
|
|
330
347
|
ctx.strokeStyle = d.edge_color || '#999';
|
|
331
|
-
|
|
332
|
-
|
|
348
|
+
var baseOpacity = (d.edge_opacity !== undefined && d.edge_opacity !== null) ? d.edge_opacity : 0.6;
|
|
349
|
+
var baseWidth = d.edge_width || 1;
|
|
350
|
+
ctx.globalAlpha = statHighlightActive ? Math.min(1, baseOpacity * EDGE_HIGHLIGHT_OPACITY_MULT) : baseOpacity;
|
|
351
|
+
ctx.lineWidth = statHighlightActive ? baseWidth * EDGE_HIGHLIGHT_WIDTH_MULT : baseWidth;
|
|
333
352
|
ctx.setLineDash(d.edge_style === 'dashed' ? [6, 3] : d.edge_style === 'dotted' ? [1.5, 3] : []);
|
|
334
353
|
ctx.stroke();
|
|
335
354
|
}
|
|
@@ -702,49 +721,64 @@ function d3graphscript(config = {
|
|
|
702
721
|
|
|
703
722
|
// COLOR ON CLICK
|
|
704
723
|
function color_on_click() {
|
|
705
|
-
// Reset all nodes to their original styles
|
|
706
|
-
d3.selectAll(".node")
|
|
707
|
-
.select(".node-shape")
|
|
708
|
-
.style("fill", function(d) {return d.node_color;})
|
|
709
|
-
.style("opacity", function(d) {return d.node_opacity;})
|
|
710
|
-
.style("stroke", function(d) {return d.node_color_edge;})
|
|
711
|
-
.style("stroke-width", function(d) {return d.node_size_edge;})
|
|
712
|
-
// Restore pinned cue on still-fixed nodes
|
|
713
|
-
.style("stroke-dasharray", function(d) { return (sticky && d.fixed) ? "4,2" : null; });
|
|
714
|
-
|
|
715
|
-
// Reset circle radii
|
|
716
|
-
d3.selectAll(".node").select("circle.node-shape")
|
|
717
|
-
.attr("r", function(d) { return d.node_size; });
|
|
718
|
-
|
|
719
|
-
// Apply click styling to the selected node's shape
|
|
720
|
-
var clickedShape = d3.select(this).select(".node-shape");
|
|
721
|
-
clickedShape
|
|
722
|
-
.style("fill", {{ CLICK_FILL }})
|
|
723
|
-
.style("stroke", "{{ CLICK_STROKE }}")
|
|
724
|
-
.style("stroke-width", {{ CLICK_STROKEW }});
|
|
725
|
-
|
|
726
|
-
// Scale up the shape — strategy depends on element type
|
|
727
|
-
var shapeNode = clickedShape.node();
|
|
728
|
-
if (!shapeNode) return;
|
|
729
|
-
var tag = shapeNode.tagName.toLowerCase();
|
|
730
|
-
if (tag === 'circle') {
|
|
731
|
-
clickedShape.attr("r", function(d) { return d.node_size * {{ CLICK_SIZE }}; });
|
|
732
|
-
} else if (tag === 'ellipse') {
|
|
733
|
-
clickedShape.each(function(d) {
|
|
734
|
-
var r = parseFloat(d.node_size) || 8;
|
|
735
|
-
d3.select(this)
|
|
736
|
-
.attr("rx", r * 1.6 * {{ CLICK_SIZE }})
|
|
737
|
-
.attr("ry", r * {{ CLICK_SIZE }});
|
|
738
|
-
});
|
|
739
|
-
} else {
|
|
740
|
-
// <path>: re-compute path with a scaled radius
|
|
741
|
-
clickedShape.each(function(d) {
|
|
742
|
-
var r = (parseFloat(d.node_size) || 8) * {{ CLICK_SIZE }};
|
|
743
|
-
d3.select(this).attr("d", shapePathD(d.node_marker, r));
|
|
744
|
-
});
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
724
|
|
|
725
|
+
// First restore the currently active base styling:
|
|
726
|
+
// - statistic colors/sizes when a radio metric is active
|
|
727
|
+
// - original node styling when no statistic is active
|
|
728
|
+
applyStatStyling();
|
|
729
|
+
|
|
730
|
+
// Restore pinned cue after applyStatStyling()
|
|
731
|
+
node.select(".node-shape")
|
|
732
|
+
.style("stroke-dasharray", function(d) {
|
|
733
|
+
return (sticky && d.fixed) ? "4,2" : null;
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
// Apply click styling only to the selected node
|
|
737
|
+
var clickedShape = d3.select(this).select(".node-shape");
|
|
738
|
+
|
|
739
|
+
clickedShape
|
|
740
|
+
.style("fill", {{ CLICK_FILL }})
|
|
741
|
+
.style("stroke", "{{ CLICK_STROKE }}")
|
|
742
|
+
.style("stroke-width", {{ CLICK_STROKEW }});
|
|
743
|
+
|
|
744
|
+
var shapeNode = clickedShape.node();
|
|
745
|
+
if (!shapeNode) return;
|
|
746
|
+
|
|
747
|
+
var tag = shapeNode.tagName.toLowerCase();
|
|
748
|
+
|
|
749
|
+
clickedShape.each(function(d) {
|
|
750
|
+
var el = d3.select(this);
|
|
751
|
+
var baseR = parseFloat(d.node_size) || 8;
|
|
752
|
+
|
|
753
|
+
// Preserve the statistic-driven size before applying click enlargement
|
|
754
|
+
var statValue = currentStatKey ? d[currentStatKey] : null;
|
|
755
|
+
var hasStatValue =
|
|
756
|
+
currentStatKey &&
|
|
757
|
+
typeof statValue === "number" &&
|
|
758
|
+
!isNaN(statValue);
|
|
759
|
+
|
|
760
|
+
var statScale = hasStatValue
|
|
761
|
+
? 1.2 + statValue * 1.3
|
|
762
|
+
: 1;
|
|
763
|
+
|
|
764
|
+
var clickScale = statScale * {{ CLICK_SIZE }};
|
|
765
|
+
|
|
766
|
+
if (tag === "circle") {
|
|
767
|
+
el.attr("r", baseR * clickScale);
|
|
768
|
+
|
|
769
|
+
} else if (tag === "ellipse") {
|
|
770
|
+
el
|
|
771
|
+
.attr("rx", baseR * 1.6 * clickScale)
|
|
772
|
+
.attr("ry", baseR * clickScale);
|
|
773
|
+
|
|
774
|
+
} else if (tag === "path") {
|
|
775
|
+
el.attr(
|
|
776
|
+
"d",
|
|
777
|
+
shapePathD(d.node_marker, baseR * clickScale)
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
}
|
|
748
782
|
|
|
749
783
|
|
|
750
784
|
function connectedNodes() {
|
|
@@ -826,6 +860,98 @@ function d3graphscript(config = {
|
|
|
826
860
|
if (densityVisible) drawDensityLayer();
|
|
827
861
|
};
|
|
828
862
|
|
|
863
|
+
// Recolors AND resizes node shapes by the currently selected statistic
|
|
864
|
+
// (reverting to each node's original color/size/opacity when none is
|
|
865
|
+
// selected), and boosts edge width/opacity for readability against the
|
|
866
|
+
// now-emphasized nodes. Handles circle/ellipse/path shapes generically,
|
|
867
|
+
// same approach as the click-to-highlight scaling. Safe to call even if a
|
|
868
|
+
// node is missing the stat (e.g. older data) — falls back to defaults.
|
|
869
|
+
function applyStatStyling() {
|
|
870
|
+
statHighlightActive = !!currentStatKey;
|
|
871
|
+
|
|
872
|
+
node.select(".node-shape").each(function(d) {
|
|
873
|
+
var el = d3.select(this);
|
|
874
|
+
var tag = this.tagName.toLowerCase();
|
|
875
|
+
var baseR = parseFloat(d.node_size) || 8;
|
|
876
|
+
var v = statHighlightActive ? d[currentStatKey] : null;
|
|
877
|
+
var hasVal = (typeof v === 'number' && !isNaN(v));
|
|
878
|
+
// 1.2x–2.5x range: even low scorers get a visibility bump, high scorers stand out more.
|
|
879
|
+
var scale = (statHighlightActive && hasVal) ? (1.2 + v * 1.3) : 1;
|
|
880
|
+
var fillColor = (statHighlightActive && hasVal) ? statColorScale(v) : d.node_color;
|
|
881
|
+
|
|
882
|
+
if (tag === 'circle') {
|
|
883
|
+
el.attr('r', baseR * scale);
|
|
884
|
+
} else if (tag === 'ellipse') {
|
|
885
|
+
el.attr('rx', baseR * 1.6 * scale).attr('ry', baseR * scale);
|
|
886
|
+
} else if (tag === 'path') {
|
|
887
|
+
el.attr('d', shapePathD(d.node_marker, baseR * scale));
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
el.style('fill', fillColor)
|
|
891
|
+
.style('opacity', statHighlightActive ? 1 : d.node_opacity)
|
|
892
|
+
.style('stroke-width', statHighlightActive ? (parseFloat(d.node_size_edge) || 1) * 1.5 : d.node_size_edge);
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
// SVG-mode edges: bump width/opacity directly on the current selection.
|
|
896
|
+
// Canvas-mode edges read statHighlightActive inside drawCanvasEdges().
|
|
897
|
+
link.style("stroke-width", function(d) {
|
|
898
|
+
var w = parseFloat(d.edge_width) || 1;
|
|
899
|
+
return statHighlightActive ? w * EDGE_HIGHLIGHT_WIDTH_MULT : w;
|
|
900
|
+
});
|
|
901
|
+
link.style("opacity", function(d) {
|
|
902
|
+
var o = (d.edge_opacity !== undefined && d.edge_opacity !== null) ? d.edge_opacity : 0.6;
|
|
903
|
+
return statHighlightActive ? Math.min(1, o * EDGE_HIGHLIGHT_OPACITY_MULT) : o;
|
|
904
|
+
});
|
|
905
|
+
if (useCanvasEdges) drawCanvasEdges();
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
var statRadios = document.querySelectorAll('input[name="statMetric"]');
|
|
909
|
+
if (statRadios.length) {
|
|
910
|
+
statRadios.forEach(function(radio) {
|
|
911
|
+
radio.addEventListener('change', function() {
|
|
912
|
+
currentStatKey = (this.value === 'none') ? null : this.value;
|
|
913
|
+
applyStatStyling();
|
|
914
|
+
});
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Exports the currently filtered nodes/edges (respecting the weight and
|
|
919
|
+
// component sliders — whatever's "in scope" right now, not necessarily
|
|
920
|
+
// what's on screen if edges are toggled off) together with their
|
|
921
|
+
// precomputed stats, as a single JSON file.
|
|
922
|
+
var exportDataBtn = document.getElementById('exportDataButton');
|
|
923
|
+
if (exportDataBtn) {
|
|
924
|
+
exportDataBtn.addEventListener('click', function() {
|
|
925
|
+
var nodesOut = graph.nodes.map(function(d) {
|
|
926
|
+
return {
|
|
927
|
+
id: d.index,
|
|
928
|
+
name: d.node_name,
|
|
929
|
+
pagerank: d.node_pagerank,
|
|
930
|
+
hits_hub: d.node_hits_hub,
|
|
931
|
+
hits_authority: d.node_hits_authority,
|
|
932
|
+
degree_centrality: d.node_degree_centrality,
|
|
933
|
+
closeness_centrality: d.node_closeness_centrality,
|
|
934
|
+
betweenness_centrality: d.node_betweenness_centrality
|
|
935
|
+
};
|
|
936
|
+
});
|
|
937
|
+
var edgesOut = graph.links.map(function(d) {
|
|
938
|
+
return {
|
|
939
|
+
source: (d.source && d.source.node_name !== undefined) ? d.source.node_name : d.source,
|
|
940
|
+
target: (d.target && d.target.node_name !== undefined) ? d.target.node_name : d.target,
|
|
941
|
+
weight: d.edge_weight
|
|
942
|
+
};
|
|
943
|
+
});
|
|
944
|
+
var payload = { nodes: nodesOut, edges: edgesOut };
|
|
945
|
+
var blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json;charset=utf-8" });
|
|
946
|
+
var url = URL.createObjectURL(blob);
|
|
947
|
+
var a = document.createElement('a');
|
|
948
|
+
a.href = url;
|
|
949
|
+
a.download = '{{ title }}_export.json';
|
|
950
|
+
a.click();
|
|
951
|
+
URL.revokeObjectURL(url);
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
|
|
829
955
|
//Restart the visualisation after any node and link changes
|
|
830
956
|
function restart() {
|
|
831
957
|
|
|
@@ -835,6 +961,7 @@ function d3graphscript(config = {
|
|
|
835
961
|
node.enter().insert("circle", ".cursor").attr("class", "node").attr("r", 5).call(force.drag);
|
|
836
962
|
tickCount = 0;
|
|
837
963
|
force.start();
|
|
964
|
+
applyStatStyling();
|
|
838
965
|
}
|
|
839
966
|
|
|
840
967
|
}
|
|
@@ -9,93 +9,6 @@
|
|
|
9
9
|
|
|
10
10
|
<style type="text/css">
|
|
11
11
|
{% include "style.css" %}
|
|
12
|
-
.dark-mode {
|
|
13
|
-
background-color: #222 !important;
|
|
14
|
-
color: #eee !important;
|
|
15
|
-
}
|
|
16
|
-
.dark-mode #graphContainer {
|
|
17
|
-
background-color: #222 !important;
|
|
18
|
-
}
|
|
19
|
-
.dark-mode .button-switch {
|
|
20
|
-
background: #333;
|
|
21
|
-
color: #eee;
|
|
22
|
-
border: 1px solid #555;
|
|
23
|
-
}
|
|
24
|
-
.button-switch {
|
|
25
|
-
display: inline-block;
|
|
26
|
-
margin: 8px 4px;
|
|
27
|
-
padding: 6px 16px;
|
|
28
|
-
border-radius: 6px;
|
|
29
|
-
border: 1px solid #ccc;
|
|
30
|
-
background: #fff;
|
|
31
|
-
color: #222;
|
|
32
|
-
cursor: pointer;
|
|
33
|
-
font-size: 1em;
|
|
34
|
-
font-family: inherit, Arial, sans-serif;
|
|
35
|
-
transition: background 0.2s, color 0.2s;
|
|
36
|
-
}
|
|
37
|
-
.top-panel {
|
|
38
|
-
display: flex;
|
|
39
|
-
flex-wrap: wrap;
|
|
40
|
-
justify-content: flex-end;
|
|
41
|
-
gap: 8px;
|
|
42
|
-
padding: 10px;
|
|
43
|
-
}
|
|
44
|
-
.slider-container {
|
|
45
|
-
display: flex;
|
|
46
|
-
align-items: center;
|
|
47
|
-
justify-content: center;
|
|
48
|
-
margin: 12px 0;
|
|
49
|
-
}
|
|
50
|
-
.slider-label {
|
|
51
|
-
margin: 0 8px;
|
|
52
|
-
font-size: 1em;
|
|
53
|
-
font-family: inherit, Arial, sans-serif;
|
|
54
|
-
}
|
|
55
|
-
.styled-slider {
|
|
56
|
-
-webkit-appearance: none;
|
|
57
|
-
width: 180px;
|
|
58
|
-
height: 8px;
|
|
59
|
-
background: #eee;
|
|
60
|
-
border-radius: 5px;
|
|
61
|
-
outline: none;
|
|
62
|
-
transition: background 0.2s;
|
|
63
|
-
margin: 0 8px;
|
|
64
|
-
}
|
|
65
|
-
.styled-slider::-webkit-slider-thumb {
|
|
66
|
-
-webkit-appearance: none;
|
|
67
|
-
appearance: none;
|
|
68
|
-
width: 22px;
|
|
69
|
-
height: 22px;
|
|
70
|
-
border-radius: 50%;
|
|
71
|
-
background: #fff;
|
|
72
|
-
border: 2px solid #888;
|
|
73
|
-
cursor: pointer;
|
|
74
|
-
box-shadow: 0 0 2px #888;
|
|
75
|
-
}
|
|
76
|
-
.styled-slider::-moz-range-thumb {
|
|
77
|
-
width: 22px;
|
|
78
|
-
height: 22px;
|
|
79
|
-
border-radius: 50%;
|
|
80
|
-
background: #fff;
|
|
81
|
-
border: 2px solid #888;
|
|
82
|
-
cursor: pointer;
|
|
83
|
-
box-shadow: 0 0 2px #888;
|
|
84
|
-
}
|
|
85
|
-
.styled-slider:focus {
|
|
86
|
-
background: #ddd;
|
|
87
|
-
}
|
|
88
|
-
.dark-mode .styled-slider {
|
|
89
|
-
background: #444;
|
|
90
|
-
}
|
|
91
|
-
.dark-mode .styled-slider::-webkit-slider-thumb {
|
|
92
|
-
background: #222;
|
|
93
|
-
border: 2px solid #eee;
|
|
94
|
-
}
|
|
95
|
-
.dark-mode .styled-slider::-moz-range-thumb {
|
|
96
|
-
background: #222;
|
|
97
|
-
border: 2px solid #eee;
|
|
98
|
-
}
|
|
99
12
|
</style>
|
|
100
13
|
</head>
|
|
101
14
|
<body style="background-color: {{ background_color }};{% if dark_mode %} color: #eee;{% endif %}"{% if dark_mode %} class="dark-mode"{% endif %}>
|
|
@@ -106,8 +19,21 @@
|
|
|
106
19
|
<button id="darkModeSwitch" class="button-switch">{% if dark_mode %}☀ Light Mode{% else %}🌙 Dark Mode{% endif %}</button>
|
|
107
20
|
<button id="edgeToggleButton" class="button-switch">Hide Edges</button>
|
|
108
21
|
<button id="densityToggleButton" class="button-switch">{% if show_density %}Hide Density{% else %}Show Density{% endif %}</button>
|
|
22
|
+
<button id="exportDataButton" class="button-switch">Export Data</button>
|
|
109
23
|
{% if save_button %}<button id="saveButton" class="button-switch">Save</button>{% endif %}
|
|
110
24
|
</div>
|
|
25
|
+
1
|
|
26
|
+
<!-- Side panel: interactively recolor/resize nodes by common network statistics -->
|
|
27
|
+
<div class="stats-panel" id="statsPanel">
|
|
28
|
+
<div class="stats-panel-title">Network Statistic</div>
|
|
29
|
+
<label><input type="radio" name="statMetric" value="none" checked> Default</label>
|
|
30
|
+
<label><input type="radio" name="statMetric" value="node_pagerank"> PageRank</label>
|
|
31
|
+
<label><input type="radio" name="statMetric" value="node_hits_hub"> HITS (Hub)</label>
|
|
32
|
+
<label><input type="radio" name="statMetric" value="node_hits_authority"> HITS (Authority)</label>
|
|
33
|
+
<label><input type="radio" name="statMetric" value="node_degree_centrality"> Degree Centrality</label>
|
|
34
|
+
<label><input type="radio" name="statMetric" value="node_closeness_centrality"> Closeness Centrality</label>
|
|
35
|
+
<label><input type="radio" name="statMetric" value="node_betweenness_centrality"> Betweenness Centrality</label>
|
|
36
|
+
</div>
|
|
111
37
|
{% endif %}
|
|
112
38
|
|
|
113
39
|
<script>
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
.node text {
|
|
2
|
+
font-size: 10px;
|
|
3
|
+
font-family: "Helvetica";
|
|
4
|
+
stroke: none;
|
|
5
|
+
fill: grey;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.link {
|
|
9
|
+
stroke: #999;
|
|
10
|
+
stroke-opacity: .6;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
h3 {
|
|
14
|
+
color: #1ABC9C;
|
|
15
|
+
text-align: center;
|
|
16
|
+
font-style: italic;
|
|
17
|
+
font-size: 14px;
|
|
18
|
+
font-family: "Helvetica";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* Sticky / fixed node indicator: dashed outline ring — works for all shape types */
|
|
22
|
+
.node .node-shape.node-fixed {
|
|
23
|
+
stroke-dasharray: 4, 2;
|
|
24
|
+
stroke-width: 2.5px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* Shared pointer cursor for all node shapes */
|
|
28
|
+
.node .node-shape {
|
|
29
|
+
cursor: pointer;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* Zoom-linked label visibility (performance): when zoomed far out, node and
|
|
33
|
+
edge labels are unreadable anyway, so they're hidden via this single class
|
|
34
|
+
toggle instead of being permanently removed. Placed after .node text so it
|
|
35
|
+
wins on specificity ties. */
|
|
36
|
+
.labels-hidden text {
|
|
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;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* Stats side panel (PageRank / HITS / centrality toggles). Deliberately NOT
|
|
48
|
+
position:fixed/absolute — this codebase has twice hit bugs from
|
|
49
|
+
floating-overlay panels landing behind or on top of other content in ways
|
|
50
|
+
that depend on the exact height of the (wrapping) top-panel row. Normal
|
|
51
|
+
document flow sidesteps that whole category of bug: it always renders
|
|
52
|
+
directly below the top-panel, full stop. */
|
|
53
|
+
.stats-panel {
|
|
54
|
+
position: absolute;
|
|
55
|
+
top: 70px; /* Adjust to place it below the top controls */
|
|
56
|
+
left: 10px;
|
|
57
|
+
z-index: 1000;
|
|
58
|
+
|
|
59
|
+
background: rgba(255, 255, 255, 0.92);
|
|
60
|
+
border-radius: 8px;
|
|
61
|
+
padding: 10px 14px;
|
|
62
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
|
63
|
+
font-family: "Helvetica", Arial, sans-serif;
|
|
64
|
+
font-size: 13px;
|
|
65
|
+
width: 220px;
|
|
66
|
+
max-width: calc(100vw - 40px);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.dark-mode .stats-panel {
|
|
70
|
+
background: rgba(34, 34, 34, 0.92);
|
|
71
|
+
color: #eee;
|
|
72
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.stats-panel .stats-panel-title {
|
|
76
|
+
font-weight: bold;
|
|
77
|
+
margin-bottom: 6px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.stats-panel label {
|
|
81
|
+
display: block;
|
|
82
|
+
margin: 4px 0;
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
.dark-mode {
|
|
88
|
+
background-color: #222 !important;
|
|
89
|
+
color: #eee !important;
|
|
90
|
+
}
|
|
91
|
+
.dark-mode #graphContainer {
|
|
92
|
+
background-color: #222 !important;
|
|
93
|
+
}
|
|
94
|
+
.dark-mode .button-switch {
|
|
95
|
+
background: #333;
|
|
96
|
+
color: #eee;
|
|
97
|
+
border: 1px solid #555;
|
|
98
|
+
}
|
|
99
|
+
.button-switch {
|
|
100
|
+
display: inline-block;
|
|
101
|
+
margin: 8px 4px;
|
|
102
|
+
padding: 6px 16px;
|
|
103
|
+
border-radius: 6px;
|
|
104
|
+
border: 1px solid #ccc;
|
|
105
|
+
background: #fff;
|
|
106
|
+
color: #222;
|
|
107
|
+
cursor: pointer;
|
|
108
|
+
font-size: 1em;
|
|
109
|
+
font-family: inherit, Arial, sans-serif;
|
|
110
|
+
transition: background 0.2s, color 0.2s;
|
|
111
|
+
}
|
|
112
|
+
.top-panel {
|
|
113
|
+
display: flex;
|
|
114
|
+
flex-wrap: wrap;
|
|
115
|
+
justify-content: flex-end;
|
|
116
|
+
gap: 8px;
|
|
117
|
+
padding: 10px;
|
|
118
|
+
}
|
|
119
|
+
.slider-container {
|
|
120
|
+
display: flex;
|
|
121
|
+
align-items: center;
|
|
122
|
+
justify-content: center;
|
|
123
|
+
margin: 12px 0;
|
|
124
|
+
}
|
|
125
|
+
.slider-label {
|
|
126
|
+
margin: 0 8px;
|
|
127
|
+
font-size: 1em;
|
|
128
|
+
font-family: inherit, Arial, sans-serif;
|
|
129
|
+
}
|
|
130
|
+
.styled-slider {
|
|
131
|
+
-webkit-appearance: none;
|
|
132
|
+
width: 180px;
|
|
133
|
+
height: 8px;
|
|
134
|
+
background: #eee;
|
|
135
|
+
border-radius: 5px;
|
|
136
|
+
outline: none;
|
|
137
|
+
transition: background 0.2s;
|
|
138
|
+
margin: 0 8px;
|
|
139
|
+
}
|
|
140
|
+
.styled-slider::-webkit-slider-thumb {
|
|
141
|
+
-webkit-appearance: none;
|
|
142
|
+
appearance: none;
|
|
143
|
+
width: 22px;
|
|
144
|
+
height: 22px;
|
|
145
|
+
border-radius: 50%;
|
|
146
|
+
background: #fff;
|
|
147
|
+
border: 2px solid #888;
|
|
148
|
+
cursor: pointer;
|
|
149
|
+
box-shadow: 0 0 2px #888;
|
|
150
|
+
}
|
|
151
|
+
.styled-slider::-moz-range-thumb {
|
|
152
|
+
width: 22px;
|
|
153
|
+
height: 22px;
|
|
154
|
+
border-radius: 50%;
|
|
155
|
+
background: #fff;
|
|
156
|
+
border: 2px solid #888;
|
|
157
|
+
cursor: pointer;
|
|
158
|
+
box-shadow: 0 0 2px #888;
|
|
159
|
+
}
|
|
160
|
+
.styled-slider:focus {
|
|
161
|
+
background: #ddd;
|
|
162
|
+
}
|
|
163
|
+
.dark-mode .styled-slider {
|
|
164
|
+
background: #444;
|
|
165
|
+
}
|
|
166
|
+
.dark-mode .styled-slider::-webkit-slider-thumb {
|
|
167
|
+
background: #222;
|
|
168
|
+
border: 2px solid #eee;
|
|
169
|
+
}
|
|
170
|
+
.dark-mode .styled-slider::-moz-range-thumb {
|
|
171
|
+
background: #222;
|
|
172
|
+
border: 2px solid #eee;
|
|
173
|
+
}
|
|
@@ -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:10000]
|
|
10
10
|
# Create adjmat
|
|
11
11
|
adjmat = vec2adjmat(source=df['source'], target=df['target'], weight=df['weight'])
|
|
12
12
|
# Update matrix with random weights
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
.node text {
|
|
2
|
-
font-size: 10px;
|
|
3
|
-
font-family: "Helvetica";
|
|
4
|
-
stroke: none;
|
|
5
|
-
fill: grey;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
.link {
|
|
9
|
-
stroke: #999;
|
|
10
|
-
stroke-opacity: .6;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
h3 {
|
|
14
|
-
color: #1ABC9C;
|
|
15
|
-
text-align: center;
|
|
16
|
-
font-style: italic;
|
|
17
|
-
font-size: 14px;
|
|
18
|
-
font-family: "Helvetica";
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/* Sticky / fixed node indicator: dashed outline ring — works for all shape types */
|
|
22
|
-
.node .node-shape.node-fixed {
|
|
23
|
-
stroke-dasharray: 4, 2;
|
|
24
|
-
stroke-width: 2.5px;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/* Shared pointer cursor for all node shapes */
|
|
28
|
-
.node .node-shape {
|
|
29
|
-
cursor: pointer;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/* Zoom-linked label visibility (performance): when zoomed far out, node and
|
|
33
|
-
edge labels are unreadable anyway, so they're hidden via this single class
|
|
34
|
-
toggle instead of being permanently removed. Placed after .node text so it
|
|
35
|
-
wins on specificity ties. */
|
|
36
|
-
.labels-hidden text {
|
|
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;
|
|
45
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|