d3graph 2.8.2__tar.gz → 2.9.1__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.2/d3graph.egg-info → d3graph-2.9.1}/PKG-INFO +1 -1
- {d3graph-2.8.2 → d3graph-2.9.1}/d3graph/__init__.py +1 -1
- {d3graph-2.8.2 → d3graph-2.9.1}/d3graph/d3graph.py +138 -73
- {d3graph-2.8.2 → d3graph-2.9.1}/d3graph/d3js/d3graphscript.js +47 -5
- {d3graph-2.8.2 → d3graph-2.9.1}/d3graph/d3js/index.html.j2 +2 -0
- {d3graph-2.8.2 → d3graph-2.9.1}/d3graph/d3js/style.css +8 -0
- {d3graph-2.8.2 → d3graph-2.9.1}/d3graph/examples.py +28 -3
- {d3graph-2.8.2 → d3graph-2.9.1/d3graph.egg-info}/PKG-INFO +1 -1
- {d3graph-2.8.2 → d3graph-2.9.1}/LICENSE +0 -0
- {d3graph-2.8.2 → d3graph-2.9.1}/MANIFEST.in +0 -0
- {d3graph-2.8.2 → d3graph-2.9.1}/README.md +0 -0
- {d3graph-2.8.2 → d3graph-2.9.1}/d3graph/d3js/d3.v3.js +0 -0
- {d3graph-2.8.2 → d3graph-2.9.1}/d3graph/examples_docs.py +0 -0
- {d3graph-2.8.2 → d3graph-2.9.1}/d3graph.egg-info/SOURCES.txt +0 -0
- {d3graph-2.8.2 → d3graph-2.9.1}/d3graph.egg-info/dependency_links.txt +0 -0
- {d3graph-2.8.2 → d3graph-2.9.1}/d3graph.egg-info/requires.txt +0 -0
- {d3graph-2.8.2 → d3graph-2.9.1}/d3graph.egg-info/top_level.txt +0 -0
- {d3graph-2.8.2 → d3graph-2.9.1}/pyproject.toml +0 -0
- {d3graph-2.8.2 → d3graph-2.9.1}/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:
|
|
@@ -339,7 +352,7 @@ class d3graph:
|
|
|
339
352
|
return
|
|
340
353
|
|
|
341
354
|
if (not directed) and (marker_end is not None) or (marker_start is not None):
|
|
342
|
-
logger.info('Set directed=True to see the markers
|
|
355
|
+
logger.info('Markers not shown. Set directed=True to see the markers.')
|
|
343
356
|
|
|
344
357
|
# Get node color properties which is required to set the edge color
|
|
345
358
|
node_color_map = None
|
|
@@ -351,6 +364,7 @@ class d3graph:
|
|
|
351
364
|
self.config['edge_color'] = '#808080'
|
|
352
365
|
|
|
353
366
|
# Set the edge properties
|
|
367
|
+
logger.info('Set the edge properties')
|
|
354
368
|
self.edge_properties, self.adjmat = adjmat2dict(
|
|
355
369
|
self.adjmat,
|
|
356
370
|
filter_weight=0,
|
|
@@ -372,7 +386,7 @@ class d3graph:
|
|
|
372
386
|
node_color_map=node_color_map,
|
|
373
387
|
)
|
|
374
388
|
|
|
375
|
-
logger.
|
|
389
|
+
# logger.info(f'Number of edges: {len(self.edge_properties.keys())}')
|
|
376
390
|
|
|
377
391
|
def set_node_properties(self,
|
|
378
392
|
label=None,
|
|
@@ -697,7 +711,7 @@ class d3graph:
|
|
|
697
711
|
Parameters
|
|
698
712
|
----------
|
|
699
713
|
adjmat : pd.DataFrame()
|
|
700
|
-
Adjacency matrix (symmetric
|
|
714
|
+
Adjacency matrix (symmetric and Values > 0 are edges).
|
|
701
715
|
color : list of strings (default: 'cluster')
|
|
702
716
|
Coloring of the nodes.
|
|
703
717
|
* 'cluster' or None : Colours are based on the community distance clusters.
|
|
@@ -801,6 +815,8 @@ class d3graph:
|
|
|
801
815
|
'collision': self.config['collision'],
|
|
802
816
|
'link_tension': self.config.get('link_tension', 1.0),
|
|
803
817
|
'sticky': self.config.get('sticky', False),
|
|
818
|
+
'max_ticks': self.config.get('max_ticks', 300),
|
|
819
|
+
'label_zoom_threshold': self.config.get('label_zoom_threshold', 0.6),
|
|
804
820
|
'node_text_inside': self.config.get('node_text_inside', False),
|
|
805
821
|
'CLICK_COMMENT': CLICK_COMMENT,
|
|
806
822
|
'CLICK_FILL': click_properties['fill'],
|
|
@@ -1138,21 +1154,23 @@ def adjmat2dict(adjmat: pd.DataFrame,
|
|
|
1138
1154
|
'tooltip': Text that is shown when hovering over the edge.
|
|
1139
1155
|
|
|
1140
1156
|
"""
|
|
1141
|
-
# Convert adjacency matrix into vector
|
|
1142
|
-
df = adjmat.stack().reset_index()
|
|
1143
|
-
# Set columns
|
|
1144
|
-
df.columns = ['source', 'target', 'weight']
|
|
1145
|
-
# Combine source-target values with weights
|
|
1146
|
-
df = create_unique_dataframe(df)
|
|
1147
|
-
# Remove self loops and no-connected edges
|
|
1148
|
-
Iloc = df['source'] != df['target']
|
|
1149
|
-
# Keep only edges with a minimum edge strength
|
|
1150
|
-
if filter_weight is not None:
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
df = df.loc[Iloc, :]
|
|
1155
|
-
df.reset_index(drop=True, inplace=True)
|
|
1157
|
+
# # Convert adjacency matrix into vector
|
|
1158
|
+
# df = adjmat.stack().reset_index()
|
|
1159
|
+
# # Set columns
|
|
1160
|
+
# df.columns = ['source', 'target', 'weight']
|
|
1161
|
+
# # Combine source-target values with weights
|
|
1162
|
+
# df = create_unique_dataframe(df)
|
|
1163
|
+
# # Remove self loops and no-connected edges
|
|
1164
|
+
# Iloc = df['source'] != df['target']
|
|
1165
|
+
# # Keep only edges with a minimum edge strength
|
|
1166
|
+
# if filter_weight is not None:
|
|
1167
|
+
# logger.info(f"Keep only edges with weight>{filter_weight}")
|
|
1168
|
+
# Iloc2 = df['weight'] > filter_weight
|
|
1169
|
+
# Iloc = Iloc & Iloc2
|
|
1170
|
+
# df = df.loc[Iloc, :]
|
|
1171
|
+
# df.reset_index(drop=True, inplace=True)
|
|
1172
|
+
|
|
1173
|
+
df = adjmat2vec(adjmat)
|
|
1156
1174
|
|
|
1157
1175
|
# Scale the weights for visualization purposes
|
|
1158
1176
|
if minmax_distance is not None:
|
|
@@ -1210,6 +1228,7 @@ def adjmat2dict(adjmat: pd.DataFrame,
|
|
|
1210
1228
|
source_target = list(zip(df['source'], df['target']))
|
|
1211
1229
|
if len(source_target)==0:
|
|
1212
1230
|
raise Exception('There are no links in the input data set that have unique source-target value with weight > 0')
|
|
1231
|
+
|
|
1213
1232
|
# Return
|
|
1214
1233
|
d = {edge: {'weight': df['weight'].iloc[i],
|
|
1215
1234
|
'weight_scaled': df['weight_scaled'].iloc[i],
|
|
@@ -1234,6 +1253,36 @@ def adjmat2dict(adjmat: pd.DataFrame,
|
|
|
1234
1253
|
|
|
1235
1254
|
|
|
1236
1255
|
# %% Create unique dataframe and update weights
|
|
1256
|
+
# def create_unique_dataframe(X, logger=None):
|
|
1257
|
+
# """Combine source-target into adjacency matrix with updated weights.
|
|
1258
|
+
|
|
1259
|
+
# Parameters
|
|
1260
|
+
# ----------
|
|
1261
|
+
# X : DataFrame
|
|
1262
|
+
# Data frame containing the columns [source, target, weight].
|
|
1263
|
+
# logger : Object, optional
|
|
1264
|
+
# Logger object. The default is None.
|
|
1265
|
+
|
|
1266
|
+
# Returns
|
|
1267
|
+
# -------
|
|
1268
|
+
# X : pd.DataFrame
|
|
1269
|
+
# Unique adjacency matrix containing with index as source and columns as target labels. Weights are in the matrix.
|
|
1270
|
+
|
|
1271
|
+
# References
|
|
1272
|
+
# ----------
|
|
1273
|
+
# * This function is similar to that of d3blocks.
|
|
1274
|
+
|
|
1275
|
+
# """
|
|
1276
|
+
# # Check whether labels are unique
|
|
1277
|
+
# if isinstance(X, pd.DataFrame):
|
|
1278
|
+
# Iloc = ismember(X.columns, ['source', 'target', 'weight'])[0]
|
|
1279
|
+
# X = X.loc[:, Iloc]
|
|
1280
|
+
# if 'weight' in X.columns: X['weight'] = X['weight'].astype(float)
|
|
1281
|
+
# # Groupby values and sum the weights
|
|
1282
|
+
# X = X.groupby(by=['source', 'target']).sum()
|
|
1283
|
+
# X.reset_index(drop=False, inplace=True)
|
|
1284
|
+
# return X
|
|
1285
|
+
|
|
1237
1286
|
def create_unique_dataframe(X, logger=None):
|
|
1238
1287
|
"""Combine source-target into adjacency matrix with updated weights.
|
|
1239
1288
|
|
|
@@ -1254,15 +1303,12 @@ def create_unique_dataframe(X, logger=None):
|
|
|
1254
1303
|
* This function is similar to that of d3blocks.
|
|
1255
1304
|
|
|
1256
1305
|
"""
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
X = X.groupby(by=['source', 'target']).sum()
|
|
1264
|
-
X.reset_index(drop=False, inplace=True)
|
|
1265
|
-
return X
|
|
1306
|
+
X = X[['source', 'target', 'weight']]
|
|
1307
|
+
return (
|
|
1308
|
+
X.groupby(['source', 'target'], sort=False, observed=True)
|
|
1309
|
+
.agg({'weight': 'sum'})
|
|
1310
|
+
.reset_index()
|
|
1311
|
+
)
|
|
1266
1312
|
|
|
1267
1313
|
|
|
1268
1314
|
# %% Convert dict with edges to graph (G) (also works with lower versions of networkx)
|
|
@@ -1565,61 +1611,80 @@ def vec2adjmat(source, target, weight=None, symmetric: bool = True, aggfunc='sum
|
|
|
1565
1611
|
|
|
1566
1612
|
|
|
1567
1613
|
# %% Convert adjacency matrix to vector
|
|
1614
|
+
# def adjmat2vec(adjmat, min_weight: float = 1.0) -> pd.DataFrame:
|
|
1615
|
+
# """Convert adjacency matrix into vector with source and target.
|
|
1616
|
+
|
|
1617
|
+
# Parameters
|
|
1618
|
+
# ----------
|
|
1619
|
+
# adjmat : pd.DataFrame()
|
|
1620
|
+
# Adjacency matrix.
|
|
1621
|
+
|
|
1622
|
+
# min_weight : float
|
|
1623
|
+
# edges are returned with a minimum weight.
|
|
1624
|
+
|
|
1625
|
+
# Returns
|
|
1626
|
+
# -------
|
|
1627
|
+
# pd.DataFrame()
|
|
1628
|
+
# nodes that are connected based on source and target
|
|
1629
|
+
|
|
1630
|
+
# Examples
|
|
1631
|
+
# --------
|
|
1632
|
+
# >>> source = ['Cloudy', 'Cloudy', 'Sprinkler', 'Rain']
|
|
1633
|
+
# >>> target = ['Sprinkler', 'Rain', 'Wet_Grass', 'Wet_Grass']
|
|
1634
|
+
# >>> adjmat = vec2adjmat(source, target, weight=[1, 2, 1, 3])
|
|
1635
|
+
# >>> vector = adjmat2vec(adjmat)
|
|
1636
|
+
|
|
1637
|
+
# """
|
|
1638
|
+
# # Convert adjacency matrix into vector
|
|
1639
|
+
# logger.info('Converting adjacency matrix into source-target..')
|
|
1640
|
+
# adjmat = adjmat.stack().reset_index()
|
|
1641
|
+
# # Set columns
|
|
1642
|
+
# adjmat.columns = ['source', 'target', 'weight']
|
|
1643
|
+
# # Remove self loops and no-connected edges
|
|
1644
|
+
# Iloc1 = adjmat['source'] != adjmat['target']
|
|
1645
|
+
# Iloc2 = adjmat['weight'] >= min_weight
|
|
1646
|
+
# Iloc = Iloc1 & Iloc2
|
|
1647
|
+
# # Take only connected nodes
|
|
1648
|
+
# adjmat = adjmat.loc[Iloc, :]
|
|
1649
|
+
# adjmat.reset_index(drop=True, inplace=True)
|
|
1650
|
+
# return adjmat
|
|
1651
|
+
|
|
1652
|
+
|
|
1568
1653
|
def adjmat2vec(adjmat, min_weight: float = 1.0) -> pd.DataFrame:
|
|
1569
|
-
"""
|
|
1654
|
+
"""
|
|
1655
|
+
Fast conversion of adjacency matrix → edge list.
|
|
1656
|
+
Removes self-loops and filters by min_weight.
|
|
1657
|
+
"""
|
|
1658
|
+
weights = adjmat.to_numpy(copy=False)
|
|
1570
1659
|
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
adjmat : pd.DataFrame()
|
|
1574
|
-
Adjacency matrix.
|
|
1660
|
+
# Mask of valid edges (stack() drops NaN, so we mimic that)
|
|
1661
|
+
mask = ~np.isnan(weights)
|
|
1575
1662
|
|
|
1576
|
-
|
|
1577
|
-
|
|
1663
|
+
# Extract coordinates of non-NaN entries
|
|
1664
|
+
row_idx, col_idx = np.nonzero(mask)
|
|
1578
1665
|
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
pd.DataFrame()
|
|
1582
|
-
nodes that are connected based on source and target
|
|
1666
|
+
index = adjmat.index.to_numpy()
|
|
1667
|
+
columns = adjmat.columns.to_numpy()
|
|
1583
1668
|
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1669
|
+
# Build edge list
|
|
1670
|
+
df = pd.DataFrame({
|
|
1671
|
+
'source': index[row_idx],
|
|
1672
|
+
'target': columns[col_idx],
|
|
1673
|
+
'weight': weights[row_idx, col_idx],
|
|
1674
|
+
})
|
|
1590
1675
|
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
logger.info('Converting adjacency matrix into source-target..')
|
|
1594
|
-
adjmat = adjmat.stack().reset_index()
|
|
1595
|
-
# Set columns
|
|
1596
|
-
adjmat.columns = ['source', 'target', 'weight']
|
|
1597
|
-
# Remove self loops and no-connected edges
|
|
1598
|
-
Iloc1 = adjmat['source'] != adjmat['target']
|
|
1599
|
-
Iloc2 = adjmat['weight'] >= min_weight
|
|
1600
|
-
Iloc = Iloc1 & Iloc2
|
|
1601
|
-
# Take only connected nodes
|
|
1602
|
-
adjmat = adjmat.loc[Iloc, :]
|
|
1603
|
-
adjmat.reset_index(drop=True, inplace=True)
|
|
1604
|
-
return adjmat
|
|
1676
|
+
# Remove self-loops
|
|
1677
|
+
df = df[df['source'] != df['target']]
|
|
1605
1678
|
|
|
1679
|
+
# Apply minimum weight filter
|
|
1680
|
+
if min_weight is not None:
|
|
1681
|
+
df = df[df['weight'] >= min_weight]
|
|
1682
|
+
|
|
1683
|
+
df.reset_index(drop=True, inplace=True)
|
|
1684
|
+
return df
|
|
1606
1685
|
|
|
1607
|
-
# def _check_hex_color(color, n=None, cmap='Set1'):
|
|
1608
|
-
# if isinstance(color, str) and len(color) != 7:
|
|
1609
|
-
# logger.warning('Input parameter [color] has wrong format. Must be like color="#000000" <auto-fixing>')
|
|
1610
|
-
# return get_hex_color(color, cmap=cmap)[0]
|
|
1611
|
-
# if isinstance(color, (list, np.ndarray, pd.Series, pd.Series, StringArray)) and len(color) == 0:
|
|
1612
|
-
# logger.warning('Input parameter [color] has wrong format and length. Must be like: color=["#000000", "...", "#000000"] <auto-fixing>')
|
|
1613
|
-
# return get_hex_color(color, cmap=cmap)[0]
|
|
1614
|
-
# if isinstance(color, (list, np.ndarray, pd.Series, pd.Series, StringArray)) and (not np.all(list(map(lambda x: len(x) == 7, color)))):
|
|
1615
|
-
# logger.warning('[color] contains incorrect hex-colors. Hex must be of length 7: ["#000000", "#000000", etc] <auto-fixing>')
|
|
1616
|
-
# return get_hex_color(color, cmap=cmap)[0]
|
|
1617
|
-
# if (n is not None) and isinstance(color, (list, np.ndarray, pd.Series, pd.Series, StringArray)) and len(color) != n:
|
|
1618
|
-
# logger.warning(f'Input parameter [color] has wrong length. Must be of length: {str(n)} <auto-fixing>')
|
|
1619
|
-
# return get_hex_color(color, cmap=cmap)[0]
|
|
1620
|
-
# # Return original input
|
|
1621
|
-
# return color
|
|
1622
1686
|
|
|
1687
|
+
# %%
|
|
1623
1688
|
def _check_hex_color(color, n=None, cmap='Set1'):
|
|
1624
1689
|
seq_types = (list, np.ndarray, pd.Series, StringArray)
|
|
1625
1690
|
|
|
@@ -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,25 @@
|
|
|
1
|
+
# %%
|
|
2
|
+
from d3graph import d3graph, vec2adjmat, import_example
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
df = pd.read_csv('https://github.com/d3blocks/d3blocks/files/11995798/Df.csv', sep=',', index_col=False)
|
|
8
|
+
del df['Unnamed: 0']
|
|
9
|
+
df = df[0:10000]
|
|
10
|
+
adjmat = vec2adjmat(source=df['source'], target=df['target'], weight=df['weight'])
|
|
11
|
+
|
|
12
|
+
d3 = d3graph()
|
|
13
|
+
|
|
14
|
+
start = time.perf_counter()
|
|
15
|
+
d3.graph(adjmat)
|
|
16
|
+
end = time.perf_counter()
|
|
17
|
+
|
|
18
|
+
d3.show()
|
|
19
|
+
|
|
20
|
+
print(f"Elapsed: {end - start:.6f} seconds")
|
|
21
|
+
|
|
22
|
+
|
|
1
23
|
# %% SET PATH ISSUE https://github.com/erdogant/d3graph/issues/42
|
|
2
24
|
from pathlib import Path
|
|
3
25
|
from d3graph import d3graph, vec2adjmat, import_example
|
|
@@ -109,15 +131,18 @@ adjmat = vec2adjmat(source=df['source'], target=df['target'], weight=df['weight'
|
|
|
109
131
|
|
|
110
132
|
|
|
111
133
|
# sticky=True (default) — drag to pin, right-click to release
|
|
112
|
-
d3 = d3graph(
|
|
134
|
+
d3 = d3graph(sticky=True)
|
|
135
|
+
d3.graph(adjmat)
|
|
113
136
|
d3.show()
|
|
114
137
|
|
|
115
138
|
# sticky=False — classic spring-back behaviour
|
|
116
|
-
d3 = d3graph(
|
|
139
|
+
d3 = d3graph(sticky=False)
|
|
140
|
+
d3.graph(adjmat)
|
|
117
141
|
d3.show()
|
|
118
142
|
|
|
119
143
|
# Can also be overridden per-render:
|
|
120
|
-
d3.show(
|
|
144
|
+
d3.show(sticky=False)
|
|
145
|
+
d3.graph(adjmat)
|
|
121
146
|
d3.show()
|
|
122
147
|
|
|
123
148
|
|
|
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
|