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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: d3graph
3
- Version: 2.9.4
3
+ Version: 2.9.5
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.4'
20
+ __version__ = '2.9.5'
21
21
 
22
22
  # Setup root logger
23
23
  _logger = logging.getLogger('d3graph')
@@ -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
- ctx.globalAlpha = (d.edge_opacity !== undefined && d.edge_opacity !== null) ? d.edge_opacity : 0.6;
332
- ctx.lineWidth = d.edge_width || 1;
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:1000]
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: d3graph
3
- Version: 2.9.4
3
+ Version: 2.9.5
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
@@ -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