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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: d3graph
3
- Version: 2.8.2
3
+ Version: 2.9.1
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.2'
20
+ __version__ = '2.9.1'
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:
@@ -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.debug('Number of edges: %.0d', len(self.edge_properties.keys()))
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). Values > 0 are edges.
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
- logger.info("Keep only edges with weight>%g" % filter_weight)
1152
- Iloc2 = df['weight'] > filter_weight
1153
- Iloc = Iloc & Iloc2
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
- # Check whether labels are unique
1258
- if isinstance(X, pd.DataFrame):
1259
- Iloc = ismember(X.columns, ['source', 'target', 'weight'])[0]
1260
- X = X.loc[:, Iloc]
1261
- if 'weight' in X.columns: X['weight'] = X['weight'].astype(float)
1262
- # Groupby values and sum the weights
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
- """Convert adjacency matrix into vector with source and target.
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
- Parameters
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
- min_weight : float
1577
- edges are returned with a minimum weight.
1663
+ # Extract coordinates of non-NaN entries
1664
+ row_idx, col_idx = np.nonzero(mask)
1578
1665
 
1579
- Returns
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
- Examples
1585
- --------
1586
- >>> source = ['Cloudy', 'Cloudy', 'Sprinkler', 'Rain']
1587
- >>> target = ['Sprinkler', 'Rain', 'Wet_Grass', 'Wet_Grass']
1588
- >>> adjmat = vec2adjmat(source, target, weight=[1, 2, 1, 3])
1589
- >>> vector = adjmat2vec(adjmat)
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
- # Convert adjacency matrix into vector
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 () { 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,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(adjmat, sticky=True)
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(adjmat, sticky=False)
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(adjmat, sticky=False)
144
+ d3.show(sticky=False)
145
+ d3.graph(adjmat)
121
146
  d3.show()
122
147
 
123
148
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: d3graph
3
- Version: 2.8.2
3
+ Version: 2.9.1
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