d3graph 2.9.4__tar.gz → 2.9.6__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.6
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.6'
21
21
 
22
22
  # Setup root logger
23
23
  _logger = logging.getLogger('d3graph')
@@ -65,9 +65,9 @@ class d3graph:
65
65
  def __init__(self,
66
66
  collision: float = 0.5,
67
67
  charge: int = 600,
68
+ link_tension: float = 1,
68
69
  slider=None,
69
70
  support: str = 'text',
70
- link_tension: float = 1,
71
71
  sticky: bool = True,
72
72
  verbose: int = 20) -> None:
73
73
  """Initialize d3graph.
@@ -106,7 +106,7 @@ class d3graph:
106
106
  if clean_config and hasattr(self, 'config'): del self.config
107
107
 
108
108
  def show(self,
109
- figsize=[1500, 800],
109
+ figsize=[None, None],
110
110
  title: str = 'd3graph',
111
111
  filepath: str = None,
112
112
  showfig: bool = True,
@@ -115,7 +115,7 @@ class d3graph:
115
115
  set_slider: bool = 0,
116
116
  click={'fill': None, 'stroke': 'black', 'size': 1.3, 'stroke-width': 3},
117
117
  background_color = '#FFFFFF',
118
- dark_mode = False,
118
+ dark_mode = True,
119
119
  notebook: bool = False,
120
120
  save_button: bool = True,
121
121
  link_tension: float = None,
@@ -126,15 +126,15 @@ class d3graph:
126
126
  canvas_edge_threshold: int = 2000,
127
127
  show_density: bool = False,
128
128
  density_grid_size: int = 60,
129
- density_blur: int = 10,
130
- density_opacity: float = 0.6,
129
+ density_blur: int = 15,
130
+ density_opacity: float = 0.8,
131
131
  show_controls: bool = True,
132
132
  ) -> None:
133
133
  """Build and show the graph.
134
134
 
135
135
  Parameters
136
136
  ----------
137
- figsize : tuple, (default: (1500, 800))
137
+ figsize : tuple, (default: (None, None))
138
138
  Size of the figure in the browser, (width, height).
139
139
  (None, None): Use the screen resolution.
140
140
  title : String, (default: None)
@@ -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
@@ -848,8 +848,8 @@ class d3graph:
848
848
  # Hide slider
849
849
  show_slider = ['', ''] if self.config['show_slider'] else ['<!--', '-->']
850
850
  # Set width and height to screen resolution if None.
851
- width = 'window.screen.width' if self.config['figsize'][0] is None else self.config['figsize'][0]
852
- height = 'window.screen.height' if self.config['figsize'][1] is None else self.config['figsize'][1]
851
+ width = 'window.innerWidth' if self.config['figsize'][0] is None else self.config['figsize'][0]
852
+ height = 'null' if self.config['figsize'][1] is None else self.config['figsize'][1]
853
853
  # Get support
854
854
  support = self.config.get('support')
855
855
  if support is None: support = get_support('text')
@@ -897,7 +897,6 @@ class d3graph:
897
897
  html = index_template.render(content)
898
898
 
899
899
  index_file = self.config['filepath']
900
- # index_file.write_text(index_template.render(content))
901
900
  if overwrite and index_file:
902
901
  logger.info(f'Write to path: [{index_file.absolute()}]')
903
902
  logger.info(f'File already exists and will be overwritten: [{index_file}]')
@@ -1049,13 +1048,20 @@ def check_logger(verbose: [str, int] = 'info'):
1049
1048
 
1050
1049
 
1051
1050
  # %% Write network in json file
1052
- def json_create(G: nx.Graph) -> str:
1051
+ def json_create(G: nx.Graph, compute_stats: bool = True) -> str:
1053
1052
  """Create json from Graph.
1054
1053
 
1055
1054
  Parameters
1056
1055
  ----------
1057
1056
  G : Networkx object
1058
1057
  Graph G
1058
+ compute_stats : bool, (default: True)
1059
+ Compute PageRank, HITS (hub/authority), degree/closeness/betweenness centrality
1060
+ per node and embed them as node_pagerank / node_hits_hub / node_hits_authority /
1061
+ node_degree_centrality / node_closeness_centrality / node_betweenness_centrality,
1062
+ normalized to [0, 1], for the interactive stats side panel. Skipped when the
1063
+ panel won't be shown (show_controls=False) to avoid the wasted computation.
1064
+ Betweenness is approximated (sampled) above 500 nodes to bound generation time.
1059
1065
 
1060
1066
  Returns
1061
1067
  -------
@@ -1072,6 +1078,59 @@ def json_create(G: nx.Graph) -> str:
1072
1078
  source.append(node_id[edge[0] == node_ui][0])
1073
1079
  target.append(node_id[edge[1] == node_ui][0])
1074
1080
 
1081
+ # Common network statistics for the optional interactive stats panel
1082
+ # (recolors nodes by the selected metric). Computed once here, server-side,
1083
+ # rather than in the browser, since networkx already has these and it keeps
1084
+ # the JS side simple (just reads a precomputed per-node number).
1085
+ if compute_stats and len(node_ui) > 0:
1086
+ def _normalize_dict(d):
1087
+ vals = np.array(list(d.values()), dtype=float)
1088
+ vmin, vmax = vals.min(), vals.max()
1089
+ if (vmax - vmin) < 1e-12:
1090
+ return {k: 0.0 for k in d}
1091
+ return {k: float((v - vmin) / (vmax - vmin)) for k, v in d.items()}
1092
+
1093
+ try:
1094
+ pagerank_norm = _normalize_dict(nx.pagerank(G, weight='weight'))
1095
+ except Exception:
1096
+ pagerank_norm = {n: 0.0 for n in G.nodes()}
1097
+
1098
+ try:
1099
+ hubs, authorities = nx.hits(G, max_iter=1000)
1100
+ hubs_norm = _normalize_dict(hubs)
1101
+ authorities_norm = _normalize_dict(authorities)
1102
+ except Exception:
1103
+ hubs_norm = {n: 0.0 for n in G.nodes()}
1104
+ authorities_norm = {n: 0.0 for n in G.nodes()}
1105
+
1106
+ try:
1107
+ degree_norm = _normalize_dict(nx.degree_centrality(G))
1108
+ except Exception:
1109
+ degree_norm = {n: 0.0 for n in G.nodes()}
1110
+
1111
+ try:
1112
+ closeness_norm = _normalize_dict(nx.closeness_centrality(G))
1113
+ except Exception:
1114
+ closeness_norm = {n: 0.0 for n in G.nodes()}
1115
+
1116
+ try:
1117
+ # Betweenness is O(V*E) (Brandes) - exact for smaller graphs, but that
1118
+ # gets slow fast on larger ones. Above 500 nodes, approximate via a
1119
+ # fixed-seed sample of source nodes so generation time stays bounded.
1120
+ n_nodes = G.number_of_nodes()
1121
+ k = min(100, n_nodes) if n_nodes > 500 else None
1122
+ betweenness_norm = _normalize_dict(
1123
+ nx.betweenness_centrality(G, k=k, weight='weight', seed=42 if k else None))
1124
+ except Exception:
1125
+ betweenness_norm = {n: 0.0 for n in G.nodes()}
1126
+ else:
1127
+ pagerank_norm = {n: 0.0 for n in G.nodes()}
1128
+ hubs_norm = {n: 0.0 for n in G.nodes()}
1129
+ authorities_norm = {n: 0.0 for n in G.nodes()}
1130
+ degree_norm = {n: 0.0 for n in G.nodes()}
1131
+ closeness_norm = {n: 0.0 for n in G.nodes()}
1132
+ betweenness_norm = {n: 0.0 for n in G.nodes()}
1133
+
1075
1134
  # Set edge properties
1076
1135
  links = pd.DataFrame([*G.edges.values()]).T.to_dict()
1077
1136
  links_new = []
@@ -1108,6 +1167,14 @@ def json_create(G: nx.Graph) -> str:
1108
1167
  nodes[i]['node_color_edge'] = nodes[i].pop('edge_color')
1109
1168
  nodes[i]['node_fontcolor'] = nodes[i].pop('fontcolor')
1110
1169
  nodes[i]['node_fontsize'] = nodes[i].pop('fontsize')
1170
+ # Stats for the interactive side panel (see compute_stats above)
1171
+ nid = node_ui[i]
1172
+ nodes[i]['node_pagerank'] = pagerank_norm.get(nid, 0.0)
1173
+ nodes[i]['node_hits_hub'] = hubs_norm.get(nid, 0.0)
1174
+ nodes[i]['node_hits_authority'] = authorities_norm.get(nid, 0.0)
1175
+ nodes[i]['node_degree_centrality'] = degree_norm.get(nid, 0.0)
1176
+ nodes[i]['node_closeness_centrality'] = closeness_norm.get(nid, 0.0)
1177
+ nodes[i]['node_betweenness_centrality'] = betweenness_norm.get(nid, 0.0)
1111
1178
  # Combine all information into new list
1112
1179
  nodes_new[i] = nodes[i]
1113
1180
  data = {'links': links_new, 'nodes': nodes_new}