d3graph 2.8.1__tar.gz → 2.9.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: d3graph
3
- Version: 2.8.1
3
+ Version: 2.9.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
@@ -17,7 +17,7 @@ from d3graph.d3graph import (
17
17
 
18
18
  __author__ = 'Erdogan Tasksen'
19
19
  __email__ = 'erdogant@gmail.com'
20
- __version__ = '2.8.1'
20
+ __version__ = '2.9.0'
21
21
 
22
22
  # Setup root logger
23
23
  _logger = logging.getLogger('d3graph')
@@ -121,6 +121,8 @@ class d3graph:
121
121
  link_tension: float = None,
122
122
  sticky: bool = None,
123
123
  node_text_inside: bool = False,
124
+ max_ticks: int = 300,
125
+ label_zoom_threshold: float = 0.4,
124
126
  ) -> None:
125
127
  """Build and show the graph.
126
128
 
@@ -166,6 +168,15 @@ class d3graph:
166
168
  When False, nodes are released after dragging (default simulation behaviour).
167
169
  When None, the value set in __init__ is used.
168
170
  Right-click a fixed node to release it back into the simulation.
171
+ max_ticks : int, (default: 300)
172
+ Caps how many simulation ticks run before the force layout auto-stops, instead
173
+ of letting it cool down naturally (which can take thousands of ticks on large
174
+ graphs, each re-running collision detection over every node).
175
+ 0 or None: disable the cap and run to natural cooldown.
176
+ label_zoom_threshold : float, (default: 0.6)
177
+ Node and edge labels are hidden below this zoom scale (unreadable anyway, and
178
+ costly to keep rendering for large graphs) and reappear once zoomed back in
179
+ past it. Uses a single CSS class toggle, not per-label work. 0: never hide.
169
180
 
170
181
  Returns
171
182
  -------
@@ -188,6 +199,8 @@ class d3graph:
188
199
  self.config['background_color'] = background_color
189
200
  self.config['dark_mode'] = dark_mode
190
201
  self.config['node_text_inside'] = node_text_inside
202
+ self.config['max_ticks'] = max_ticks
203
+ self.config['label_zoom_threshold'] = label_zoom_threshold
191
204
 
192
205
  # Allow show() to override the link_tension set at __init__ time
193
206
  if link_tension is not None:
@@ -485,8 +498,10 @@ class d3graph:
485
498
  node_names = self.adjmat.columns.astype(str)
486
499
  nodecount = self.adjmat.shape[0]
487
500
  group = np.zeros_like(node_names).astype(int)
501
+
488
502
  # Check validity of color.
489
503
  color = _check_hex_color(color, nodecount, cmap=cmap)
504
+
490
505
  # Store in config
491
506
  self.config['cmap'] = 'Paired' if cmap is None else cmap
492
507
  self.config['node_scaler'] = scaler
@@ -799,6 +814,8 @@ class d3graph:
799
814
  'collision': self.config['collision'],
800
815
  'link_tension': self.config.get('link_tension', 1.0),
801
816
  'sticky': self.config.get('sticky', False),
817
+ 'max_ticks': self.config.get('max_ticks', 300),
818
+ 'label_zoom_threshold': self.config.get('label_zoom_threshold', 0.6),
802
819
  'node_text_inside': self.config.get('node_text_inside', False),
803
820
  'CLICK_COMMENT': CLICK_COMMENT,
804
821
  'CLICK_FILL': click_properties['fill'],
@@ -1621,18 +1638,21 @@ def adjmat2vec(adjmat, min_weight: float = 1.0) -> pd.DataFrame:
1621
1638
  def _check_hex_color(color, n=None, cmap='Set1'):
1622
1639
  seq_types = (list, np.ndarray, pd.Series, StringArray)
1623
1640
 
1641
+ if isinstance(color, str) and color=='cluster':
1642
+ return 'cluster'
1643
+
1624
1644
  # Single string
1625
1645
  if isinstance(color, str):
1626
1646
  if len(color) != 7 or not color.startswith('#'):
1627
- logger.warning('Invalid [color] format. Expected "#000000" <auto-fixing>')
1628
- return get_hex_color(color, cmap=cmap)[0]
1647
+ logger.warning('1. Invalid [color] format. Expected "#000000" <auto-fixing>')
1648
+ return get_hex_color([color]*n, cmap=cmap)[0]
1629
1649
  return color
1630
1650
 
1631
1651
  # Sequence input
1632
1652
  if isinstance(color, seq_types):
1633
1653
 
1634
1654
  if len(color) == 0:
1635
- logger.warning('Invalid [color] sequence. Expected ["#000000", ...] <auto-fixing>')
1655
+ logger.warning('2. Invalid [color] sequence. Expected ["#000000", ...] <auto-fixing>')
1636
1656
  return get_hex_color(color, cmap=cmap)[0]
1637
1657
 
1638
1658
  valid = np.all([
@@ -1641,11 +1661,11 @@ def _check_hex_color(color, n=None, cmap='Set1'):
1641
1661
  ])
1642
1662
 
1643
1663
  if not valid:
1644
- logger.warning('Invalid hex colors in [color]. Expected ["#000000", ...] <auto-fixing>')
1664
+ logger.warning('3. Invalid hex colors in [color]. Expected ["#000000", ...] <auto-fixing>')
1645
1665
  return get_hex_color(color, cmap=cmap)[0]
1646
1666
 
1647
1667
  if (n is not None) and (len(color) != n):
1648
- logger.warning(f'Invalid [color] length. Expected length={n} <auto-fixing>')
1668
+ logger.warning(f'4. Invalid [color] length. Expected length={n} <auto-fixing>')
1649
1669
  return get_hex_color(color, cmap=cmap)[0]
1650
1670
 
1651
1671
  return color
@@ -10,6 +10,8 @@ function d3graphscript(config = {
10
10
  sticky: false,
11
11
  background_color: '#FFFFFF',
12
12
  node_text_inside: false,
13
+ max_ticks: 300,
14
+ label_zoom_threshold: 0.6,
13
15
  }) {
14
16
 
15
17
  //Constants for the SVG
@@ -17,6 +19,17 @@ function d3graphscript(config = {
17
19
  var height = config.height;
18
20
  var background_color = config.background_color || '#FFFFFF';
19
21
  var sticky = config.sticky || false;
22
+ // Cap how many simulation ticks run before auto-stopping, instead of
23
+ // letting a large graph's force layout cool down naturally over
24
+ // thousands of ticks (each one re-running collision detection over
25
+ // every node). Restarting the simulation (drag, slider changes) resets
26
+ // this counter so it still settles again after each change.
27
+ var maxTicks = (config.max_ticks !== undefined && config.max_ticks !== null) ? config.max_ticks : 300;
28
+ var tickCount = 0;
29
+ // Below this zoom scale, labels are unreadable anyway — hide them (via a
30
+ // single CSS class, not per-element work) instead of rendering thousands
31
+ // of illegible <text> nodes. They reappear once zoomed back in past it.
32
+ var labelZoomThreshold = (config.label_zoom_threshold !== undefined && config.label_zoom_threshold !== null) ? config.label_zoom_threshold : 0.6;
20
33
 
21
34
  // Set the body background color
22
35
  document.body.style.backgroundColor = background_color;
@@ -42,6 +55,7 @@ function d3graphscript(config = {
42
55
  d3.select(this).classed("dragging", true);
43
56
  if (sticky) {
44
57
  d.fixed = true;
58
+ tickCount = 0;
45
59
  force.start();
46
60
  }
47
61
  }
@@ -214,12 +228,26 @@ function d3graphscript(config = {
214
228
  .attr("width", width)
215
229
  .attr("height", height)
216
230
  .style("background-color", background_color)
217
- .call(d3.behavior.zoom().on("zoom", function () { svg.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")") }))
231
+ .call(d3.behavior.zoom().on("zoom", function () {
232
+ svg.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")");
233
+ // Single class toggle (cheap) rather than iterating every label on
234
+ // every zoom/pan event — CSS handles hiding all descendant <text>.
235
+ svg.classed("labels-hidden", d3.event.scale < labelZoomThreshold);
236
+ }))
218
237
  .on("dblclick.zoom", null)
219
238
  .append("g")
220
239
 
221
- graphRec = JSON.parse(JSON.stringify(graph));
222
-
240
+ graphRec = JSON.parse(JSON.stringify(graph)); // Full, unfiltered copy — used by the slider to restore edges later
241
+
242
+ // Apply the initial slider threshold BEFORE building any DOM elements.
243
+ // Without this, the browser would create a <line>/<title>/<text> for every
244
+ // single edge (e.g. 100,000+) and immediately delete most of them once the
245
+ // slider filter ran — doubling the work and freezing the page in the meantime.
246
+ (function applyInitialThreshold() {
247
+ var initialThresh = {{ SET_SLIDER }};
248
+ graph.links = graph.links.filter(function(d) { return d.edge_weight > initialThresh; });
249
+ })();
250
+
223
251
  //Creates the graph data structure out of the json data
224
252
  force.nodes(graph.nodes)
225
253
  .links(graph.links)
@@ -268,6 +296,7 @@ function d3graphscript(config = {
268
296
  d3.select(this).select(".node-shape")
269
297
  .style("stroke-dasharray", null)
270
298
  .style("stroke-width", function(d) { return d.node_size_edge; });
299
+ tickCount = 0;
271
300
  force.resume();
272
301
  });
273
302
  }
@@ -309,6 +338,14 @@ function d3graphscript(config = {
309
338
 
310
339
  //Now we are giving the SVGs co-ordinates - the force layout is generating the co-ordinates which this code is using to update the attributes of the SVG elements
311
340
  force.on("tick", function() {
341
+ // Auto-stop after maxTicks so a large graph doesn't keep re-running
342
+ // collision detection / layout math for thousands of frames while
343
+ // settling. maxTicks <= 0 disables the cap (run to natural cooldown).
344
+ if (maxTicks > 0 && ++tickCount > maxTicks) {
345
+ force.stop();
346
+ return;
347
+ }
348
+
312
349
  link.attr("x1", function(d) { return d.source.x; })
313
350
  .attr("y1", function(d) { return d.source.y; })
314
351
  .attr("x2", function(d) { return d.target.x; })
@@ -317,7 +354,9 @@ function d3graphscript(config = {
317
354
  // Position each shape according to its SVG element type:
318
355
  // circle / ellipse → cx / cy attributes
319
356
  // path → transform translate(x, y)
320
- d3.selectAll(".node-shape").each(function(d) {
357
+ // Scoped to the bound `node` selection instead of re-querying the
358
+ // entire DOM every tick (was: d3.selectAll(".node-shape")).
359
+ node.select(".node-shape").each(function(d) {
321
360
  var el = d3.select(this);
322
361
  var tag = this.tagName.toLowerCase();
323
362
  if (tag === 'circle' || tag === 'ellipse') {
@@ -327,7 +366,9 @@ function d3graphscript(config = {
327
366
  }
328
367
  });
329
368
 
330
- d3.selectAll("text").attr("x", function(d) { return d.x; })
369
+ // Scoped to node labels only (was: d3.selectAll("text"), which also
370
+ // re-matched every link-text element on every tick).
371
+ node.select("text").attr("x", function(d) { return d.x; })
331
372
  .attr("y", function(d) { return d.y; })
332
373
  linkText.attr("x", function(d) { return (d.source.x + d.target.x) / 2; }) // ADD TEXT ON THE EDGES (PART 2/2)
333
374
  .attr("y", function(d) { return (d.source.y + d.target.y) / 2; })
@@ -534,6 +575,7 @@ function d3graphscript(config = {
534
575
 
535
576
  node = node.data(graph.nodes);
536
577
  node.enter().insert("circle", ".cursor").attr("class", "node").attr("r", 5).call(force.drag);
578
+ tickCount = 0;
537
579
  force.start();
538
580
  }
539
581
 
@@ -130,6 +130,8 @@
130
130
  sticky: {{ sticky | lower }},
131
131
  background_color: '{{ background_color }}',
132
132
  node_text_inside: {{ node_text_inside | lower }},
133
+ max_ticks: {{ max_ticks }},
134
+ label_zoom_threshold: {{ label_zoom_threshold }},
133
135
  })
134
136
  });
135
137
 
@@ -27,4 +27,12 @@ h3 {
27
27
  /* Shared pointer cursor for all node shapes */
28
28
  .node .node-shape {
29
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;
30
38
  }
@@ -1,3 +1,42 @@
1
+ # %%
2
+ from d3graph import d3graph, vec2adjmat, import_example
3
+ import pandas as pd
4
+
5
+ df = pd.read_csv('https://github.com/d3blocks/d3blocks/files/11995798/Df.csv', sep=',', index_col=False)
6
+ del df['Unnamed: 0']
7
+ df = df[0:5000]
8
+ adjmat = vec2adjmat(source=df['source'], target=df['target'], weight=df['weight'])
9
+
10
+ # sticky=False — classic spring-back behaviour
11
+ d3 = d3graph()
12
+ d3.graph(adjmat)
13
+ d3.show()
14
+
15
+ # %% SET PATH ISSUE https://github.com/erdogant/d3graph/issues/42
16
+ from pathlib import Path
17
+ from d3graph import d3graph, vec2adjmat, import_example
18
+ output_path = Path.cwd() / 'd3graph.html'
19
+
20
+ # Load example data
21
+ df = import_example('stormofswords')
22
+ adjmat = vec2adjmat(source=df['source'], target=df['target'], weight=df['weight'])
23
+
24
+
25
+ # sticky=True (default) — drag to pin, right-click to release
26
+ d3 = d3graph(sticky=True)
27
+ d3.set_path(output_path)
28
+ d3.config['filepath']
29
+
30
+ d3.graph(adjmat)
31
+ d3.show()
32
+
33
+ # sticky=False — classic spring-back behaviour
34
+ d3 = d3graph(adjmat, sticky=False)
35
+ d3.show()
36
+
37
+ # %%
38
+
39
+
1
40
  from d3graph import d3graph, import_example, vec2adjmat
2
41
 
3
42
  d3 = d3graph()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: d3graph
3
- Version: 2.8.1
3
+ Version: 2.9.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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes