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.
- {d3graph-2.8.1/d3graph.egg-info → d3graph-2.9.0}/PKG-INFO +1 -1
- {d3graph-2.8.1 → d3graph-2.9.0}/d3graph/__init__.py +1 -1
- {d3graph-2.8.1 → d3graph-2.9.0}/d3graph/d3graph.py +25 -5
- {d3graph-2.8.1 → d3graph-2.9.0}/d3graph/d3js/d3graphscript.js +47 -5
- {d3graph-2.8.1 → d3graph-2.9.0}/d3graph/d3js/index.html.j2 +2 -0
- {d3graph-2.8.1 → d3graph-2.9.0}/d3graph/d3js/style.css +8 -0
- {d3graph-2.8.1 → d3graph-2.9.0}/d3graph/examples.py +39 -0
- {d3graph-2.8.1 → d3graph-2.9.0/d3graph.egg-info}/PKG-INFO +1 -1
- {d3graph-2.8.1 → d3graph-2.9.0}/LICENSE +0 -0
- {d3graph-2.8.1 → d3graph-2.9.0}/MANIFEST.in +0 -0
- {d3graph-2.8.1 → d3graph-2.9.0}/README.md +0 -0
- {d3graph-2.8.1 → d3graph-2.9.0}/d3graph/d3js/d3.v3.js +0 -0
- {d3graph-2.8.1 → d3graph-2.9.0}/d3graph/examples_docs.py +0 -0
- {d3graph-2.8.1 → d3graph-2.9.0}/d3graph.egg-info/SOURCES.txt +0 -0
- {d3graph-2.8.1 → d3graph-2.9.0}/d3graph.egg-info/dependency_links.txt +0 -0
- {d3graph-2.8.1 → d3graph-2.9.0}/d3graph.egg-info/requires.txt +0 -0
- {d3graph-2.8.1 → d3graph-2.9.0}/d3graph.egg-info/top_level.txt +0 -0
- {d3graph-2.8.1 → d3graph-2.9.0}/pyproject.toml +0 -0
- {d3graph-2.8.1 → d3graph-2.9.0}/setup.cfg +0 -0
|
@@ -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 () {
|
|
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
|
-
|
|
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")
|
|
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
|
|
|
@@ -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()
|
|
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
|