d3graph 2.9.0__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.9.0
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.9.0'
20
+ __version__ = '2.9.1'
21
21
 
22
22
  # Setup root logger
23
23
  _logger = logging.getLogger('d3graph')
@@ -352,7 +352,7 @@ class d3graph:
352
352
  return
353
353
 
354
354
  if (not directed) and (marker_end is not None) or (marker_start is not None):
355
- logger.info('Set directed=True to see the markers!')
355
+ logger.info('Markers not shown. Set directed=True to see the markers.')
356
356
 
357
357
  # Get node color properties which is required to set the edge color
358
358
  node_color_map = None
@@ -364,6 +364,7 @@ class d3graph:
364
364
  self.config['edge_color'] = '#808080'
365
365
 
366
366
  # Set the edge properties
367
+ logger.info('Set the edge properties')
367
368
  self.edge_properties, self.adjmat = adjmat2dict(
368
369
  self.adjmat,
369
370
  filter_weight=0,
@@ -385,7 +386,7 @@ class d3graph:
385
386
  node_color_map=node_color_map,
386
387
  )
387
388
 
388
- logger.debug('Number of edges: %.0d', len(self.edge_properties.keys()))
389
+ # logger.info(f'Number of edges: {len(self.edge_properties.keys())}')
389
390
 
390
391
  def set_node_properties(self,
391
392
  label=None,
@@ -710,7 +711,7 @@ class d3graph:
710
711
  Parameters
711
712
  ----------
712
713
  adjmat : pd.DataFrame()
713
- Adjacency matrix (symmetric). Values > 0 are edges.
714
+ Adjacency matrix (symmetric and Values > 0 are edges).
714
715
  color : list of strings (default: 'cluster')
715
716
  Coloring of the nodes.
716
717
  * 'cluster' or None : Colours are based on the community distance clusters.
@@ -1153,21 +1154,23 @@ def adjmat2dict(adjmat: pd.DataFrame,
1153
1154
  'tooltip': Text that is shown when hovering over the edge.
1154
1155
 
1155
1156
  """
1156
- # Convert adjacency matrix into vector
1157
- df = adjmat.stack().reset_index()
1158
- # Set columns
1159
- df.columns = ['source', 'target', 'weight']
1160
- # Combine source-target values with weights
1161
- df = create_unique_dataframe(df)
1162
- # Remove self loops and no-connected edges
1163
- Iloc = df['source'] != df['target']
1164
- # Keep only edges with a minimum edge strength
1165
- if filter_weight is not None:
1166
- logger.info("Keep only edges with weight>%g" % filter_weight)
1167
- Iloc2 = df['weight'] > filter_weight
1168
- Iloc = Iloc & Iloc2
1169
- df = df.loc[Iloc, :]
1170
- 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)
1171
1174
 
1172
1175
  # Scale the weights for visualization purposes
1173
1176
  if minmax_distance is not None:
@@ -1225,6 +1228,7 @@ def adjmat2dict(adjmat: pd.DataFrame,
1225
1228
  source_target = list(zip(df['source'], df['target']))
1226
1229
  if len(source_target)==0:
1227
1230
  raise Exception('There are no links in the input data set that have unique source-target value with weight > 0')
1231
+
1228
1232
  # Return
1229
1233
  d = {edge: {'weight': df['weight'].iloc[i],
1230
1234
  'weight_scaled': df['weight_scaled'].iloc[i],
@@ -1249,6 +1253,36 @@ def adjmat2dict(adjmat: pd.DataFrame,
1249
1253
 
1250
1254
 
1251
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
+
1252
1286
  def create_unique_dataframe(X, logger=None):
1253
1287
  """Combine source-target into adjacency matrix with updated weights.
1254
1288
 
@@ -1269,15 +1303,12 @@ def create_unique_dataframe(X, logger=None):
1269
1303
  * This function is similar to that of d3blocks.
1270
1304
 
1271
1305
  """
1272
- # Check whether labels are unique
1273
- if isinstance(X, pd.DataFrame):
1274
- Iloc = ismember(X.columns, ['source', 'target', 'weight'])[0]
1275
- X = X.loc[:, Iloc]
1276
- if 'weight' in X.columns: X['weight'] = X['weight'].astype(float)
1277
- # Groupby values and sum the weights
1278
- X = X.groupby(by=['source', 'target']).sum()
1279
- X.reset_index(drop=False, inplace=True)
1280
- 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
+ )
1281
1312
 
1282
1313
 
1283
1314
  # %% Convert dict with edges to graph (G) (also works with lower versions of networkx)
@@ -1580,61 +1611,80 @@ def vec2adjmat(source, target, weight=None, symmetric: bool = True, aggfunc='sum
1580
1611
 
1581
1612
 
1582
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
+
1583
1653
  def adjmat2vec(adjmat, min_weight: float = 1.0) -> pd.DataFrame:
1584
- """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)
1585
1659
 
1586
- Parameters
1587
- ----------
1588
- adjmat : pd.DataFrame()
1589
- Adjacency matrix.
1660
+ # Mask of valid edges (stack() drops NaN, so we mimic that)
1661
+ mask = ~np.isnan(weights)
1590
1662
 
1591
- min_weight : float
1592
- edges are returned with a minimum weight.
1663
+ # Extract coordinates of non-NaN entries
1664
+ row_idx, col_idx = np.nonzero(mask)
1593
1665
 
1594
- Returns
1595
- -------
1596
- pd.DataFrame()
1597
- nodes that are connected based on source and target
1666
+ index = adjmat.index.to_numpy()
1667
+ columns = adjmat.columns.to_numpy()
1598
1668
 
1599
- Examples
1600
- --------
1601
- >>> source = ['Cloudy', 'Cloudy', 'Sprinkler', 'Rain']
1602
- >>> target = ['Sprinkler', 'Rain', 'Wet_Grass', 'Wet_Grass']
1603
- >>> adjmat = vec2adjmat(source, target, weight=[1, 2, 1, 3])
1604
- >>> 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
+ })
1605
1675
 
1606
- """
1607
- # Convert adjacency matrix into vector
1608
- logger.info('Converting adjacency matrix into source-target..')
1609
- adjmat = adjmat.stack().reset_index()
1610
- # Set columns
1611
- adjmat.columns = ['source', 'target', 'weight']
1612
- # Remove self loops and no-connected edges
1613
- Iloc1 = adjmat['source'] != adjmat['target']
1614
- Iloc2 = adjmat['weight'] >= min_weight
1615
- Iloc = Iloc1 & Iloc2
1616
- # Take only connected nodes
1617
- adjmat = adjmat.loc[Iloc, :]
1618
- adjmat.reset_index(drop=True, inplace=True)
1619
- return adjmat
1676
+ # Remove self-loops
1677
+ df = df[df['source'] != df['target']]
1620
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
1621
1685
 
1622
- # def _check_hex_color(color, n=None, cmap='Set1'):
1623
- # if isinstance(color, str) and len(color) != 7:
1624
- # logger.warning('Input parameter [color] has wrong format. Must be like color="#000000" <auto-fixing>')
1625
- # return get_hex_color(color, cmap=cmap)[0]
1626
- # if isinstance(color, (list, np.ndarray, pd.Series, pd.Series, StringArray)) and len(color) == 0:
1627
- # logger.warning('Input parameter [color] has wrong format and length. Must be like: color=["#000000", "...", "#000000"] <auto-fixing>')
1628
- # return get_hex_color(color, cmap=cmap)[0]
1629
- # if isinstance(color, (list, np.ndarray, pd.Series, pd.Series, StringArray)) and (not np.all(list(map(lambda x: len(x) == 7, color)))):
1630
- # logger.warning('[color] contains incorrect hex-colors. Hex must be of length 7: ["#000000", "#000000", etc] <auto-fixing>')
1631
- # return get_hex_color(color, cmap=cmap)[0]
1632
- # if (n is not None) and isinstance(color, (list, np.ndarray, pd.Series, pd.Series, StringArray)) and len(color) != n:
1633
- # logger.warning(f'Input parameter [color] has wrong length. Must be of length: {str(n)} <auto-fixing>')
1634
- # return get_hex_color(color, cmap=cmap)[0]
1635
- # # Return original input
1636
- # return color
1637
1686
 
1687
+ # %%
1638
1688
  def _check_hex_color(color, n=None, cmap='Set1'):
1639
1689
  seq_types = (list, np.ndarray, pd.Series, StringArray)
1640
1690
 
@@ -1,17 +1,25 @@
1
1
  # %%
2
2
  from d3graph import d3graph, vec2adjmat, import_example
3
3
  import pandas as pd
4
+ import time
5
+
4
6
 
5
7
  df = pd.read_csv('https://github.com/d3blocks/d3blocks/files/11995798/Df.csv', sep=',', index_col=False)
6
8
  del df['Unnamed: 0']
7
- df = df[0:5000]
9
+ df = df[0:10000]
8
10
  adjmat = vec2adjmat(source=df['source'], target=df['target'], weight=df['weight'])
9
11
 
10
- # sticky=False — classic spring-back behaviour
11
12
  d3 = d3graph()
13
+
14
+ start = time.perf_counter()
12
15
  d3.graph(adjmat)
16
+ end = time.perf_counter()
17
+
13
18
  d3.show()
14
19
 
20
+ print(f"Elapsed: {end - start:.6f} seconds")
21
+
22
+
15
23
  # %% SET PATH ISSUE https://github.com/erdogant/d3graph/issues/42
16
24
  from pathlib import Path
17
25
  from d3graph import d3graph, vec2adjmat, import_example
@@ -123,15 +131,18 @@ adjmat = vec2adjmat(source=df['source'], target=df['target'], weight=df['weight'
123
131
 
124
132
 
125
133
  # sticky=True (default) — drag to pin, right-click to release
126
- d3 = d3graph(adjmat, sticky=True)
134
+ d3 = d3graph(sticky=True)
135
+ d3.graph(adjmat)
127
136
  d3.show()
128
137
 
129
138
  # sticky=False — classic spring-back behaviour
130
- d3 = d3graph(adjmat, sticky=False)
139
+ d3 = d3graph(sticky=False)
140
+ d3.graph(adjmat)
131
141
  d3.show()
132
142
 
133
143
  # Can also be overridden per-render:
134
- d3.show(adjmat, sticky=False)
144
+ d3.show(sticky=False)
145
+ d3.graph(adjmat)
135
146
  d3.show()
136
147
 
137
148
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: d3graph
3
- Version: 2.9.0
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
File without changes