d3graph 2.6.2__tar.gz → 2.7.2__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.6.2
3
+ Version: 2.7.2
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
@@ -24,7 +24,7 @@ Requires-Dist: networkx>2
24
24
  Requires-Dist: ismember
25
25
  Requires-Dist: jinja2
26
26
  Requires-Dist: packaging
27
- Requires-Dist: markupsafe==2.0.1
27
+ Requires-Dist: markupsafe
28
28
  Requires-Dist: python-louvain
29
29
  Requires-Dist: datazets
30
30
  Dynamic: license-file
@@ -11,11 +11,13 @@ from d3graph.d3graph import (
11
11
  adjmat2dict,
12
12
  data_checks,
13
13
  check_logger,
14
+ get_hex_color,
15
+ import_example,
14
16
  )
15
17
 
16
18
  __author__ = 'Erdogan Tasksen'
17
19
  __email__ = 'erdogant@gmail.com'
18
- __version__ = '2.6.2'
20
+ __version__ = '2.7.2'
19
21
 
20
22
  # Setup root logger
21
23
  _logger = logging.getLogger('d3graph')
@@ -39,13 +41,13 @@ The ouput is a html file that is interactive and stand alone.
39
41
 
40
42
  Examples
41
43
  --------
42
- >>> from d3graph import d3graph, vec2adjmat
44
+ >>> from d3graph import d3graph, vec2adjmat, import_example
43
45
  >>>
44
46
  >>> # Initialize
45
47
  >>> d3 = d3graph()
46
48
  >>>
47
49
  >>> # Load karate example
48
- >>> df = d3.import_example('energy')
50
+ >>> df = import_example('energy')
49
51
  >>> adjmat = vec2adjmat(source=df['source'], target=df['target'], weight=df['weight'])
50
52
  >>>
51
53
  >>> # Initialize
@@ -63,7 +65,6 @@ Examples
63
65
  References
64
66
  ----------
65
67
  * D3Graph: erdogant.medium.com
66
- * D3Blocks: https://medium.com/data-science-collective/d3blocks-the-python-library-to-create-interactive-standalone-and-beautiful-d3-js-charts-ef8c65286e86
67
68
  * Github : https://github.com/erdogant/d3graph
68
69
  * Documentation: https://erdogant.github.io/d3graph/
69
70
 
@@ -12,7 +12,7 @@ import webbrowser
12
12
  from json import dumps
13
13
  from pathlib import Path
14
14
  from sys import platform
15
- from tempfile import TemporaryDirectory, gettempdir
15
+ from tempfile import gettempdir
16
16
  from unicodedata import normalize
17
17
  import uuid
18
18
 
@@ -21,15 +21,13 @@ import colourmap as cm
21
21
  import networkx as nx
22
22
  import numpy as np
23
23
  import pandas as pd
24
+ from pandas.arrays import StringArray
24
25
  from ismember import ismember
25
26
  from jinja2 import Environment, PackageLoader
26
27
  from packaging import version
27
28
  import datazets as dz
28
29
 
29
30
  logger = logging.getLogger(__name__)
30
- if not logger.hasHandlers():
31
- logging.basicConfig(level=logging.INFO, format='[{asctime}] [{name}] [{levelname}] {msg}', style='{', datefmt='%d-%m-%Y %H:%M:%S')
32
-
33
31
 
34
32
  # %%
35
33
  class d3graph:
@@ -58,8 +56,7 @@ class d3graph:
58
56
 
59
57
  References
60
58
  ----------
61
- * D3Graph: https://erdogant.medium.com/
62
- * D3Blocks: https://medium.com/data-science-collective/d3blocks-the-python-library-to-create-interactive-standalone-and-beautiful-d3-js-charts-ef8c65286e86
59
+ * Medium: https://erdogant.medium.com/
63
60
  * Github : https://github.com/erdogant/d3graph
64
61
  * Documentation: https://erdogant.github.io/d3graph/
65
62
 
@@ -70,8 +67,17 @@ class d3graph:
70
67
  charge: int = 600,
71
68
  slider=None,
72
69
  support: str = 'text',
70
+ link_tension: float = 1,
71
+ sticky: bool = True,
73
72
  verbose: int = 20) -> None:
74
- """Initialize d3graph."""
73
+ """Initialize d3graph.
74
+
75
+ Parameters (additional to existing)
76
+ ------------------------------------
77
+ sticky : bool, (default: False)
78
+ When True, nodes stay fixed in place after being dragged.
79
+ Right-click a fixed node to release it back into the simulation.
80
+ """
75
81
  if slider is None: slider = [None, None]
76
82
  # Cleaning
77
83
  self._clean()
@@ -85,6 +91,8 @@ class d3graph:
85
91
  self.config['charge'] = -abs(charge)
86
92
  self.config['slider'] = slider
87
93
  self.config['support'] = get_support(support)
94
+ self.config['link_tension'] = link_tension
95
+ self.config['sticky'] = sticky
88
96
  # Set paths
89
97
  self.config['curpath'] = os.path.dirname(os.path.abspath(__file__))
90
98
  self.config['d3_library'] = os.path.abspath(os.path.join(self.config['curpath'], 'd3js/d3.v3.js'))
@@ -110,6 +118,8 @@ class d3graph:
110
118
  dark_mode = False,
111
119
  notebook: bool = False,
112
120
  save_button: bool = True,
121
+ link_tension: float = None,
122
+ sticky: bool = None,
113
123
  ) -> None:
114
124
  """Build and show the graph.
115
125
 
@@ -150,6 +160,11 @@ class d3graph:
150
160
  save_button : bool, (default: True)
151
161
  True: Save button is shown in the HTML to save the image in svg.
152
162
  False: No save button is shown in the HTML.
163
+ sticky : bool, (default: None)
164
+ When True, nodes stay fixed in place after being dragged (overrides the value set in __init__).
165
+ When False, nodes are released after dragging (default simulation behaviour).
166
+ When None, the value set in __init__ is used.
167
+ Right-click a fixed node to release it back into the simulation.
153
168
 
154
169
  Returns
155
170
  -------
@@ -171,8 +186,15 @@ class d3graph:
171
186
  self.config['save_button'] = save_button
172
187
  self.config['background_color'] = background_color
173
188
  self.config['dark_mode'] = dark_mode
189
+ # Allow show() to override the link_tension set at __init__ time
190
+ if link_tension is not None:
191
+ self.config['link_tension'] = link_tension
192
+ # Override sticky only when explicitly passed
193
+ if sticky is not None:
194
+ self.config['sticky'] = sticky
174
195
  # if self.config.get('filepath', None) != 'd3graph.html':
175
- self.config['filepath'] = self.set_path(filepath)
196
+ if filepath is not None or self.config.get('filepath') is None:
197
+ self.set_path(filepath)
176
198
 
177
199
  # Create dataframe from co-occurrence matrix
178
200
  self.G = make_graph(self.node_properties, self.edge_properties)
@@ -308,6 +330,10 @@ class d3graph:
308
330
  self.config['label'] = label
309
331
  self.config['label_color'] = label_color
310
332
  self.config['label_fontsize'] = label_fontsize
333
+
334
+ if not hasattr(self, 'adjmat'):
335
+ logger.error('adjmat is missing. Initialize first with d3 = d3graph(adjmat)')
336
+ return
311
337
 
312
338
  if (not directed) and (marker_end is not None) or (marker_start is not None):
313
339
  logger.info('Set directed=True to see the markers!')
@@ -447,18 +473,22 @@ class d3graph:
447
473
  'edge_color': edge_color of the node
448
474
 
449
475
  """
476
+ if not hasattr(self, 'adjmat'):
477
+ logger.error('adjmat is missing. Initialize first with d3 = d3graph(adjmat)')
478
+ return
479
+
450
480
  if minmax is None: minmax = [8, 13]
451
481
  node_names = self.adjmat.columns.astype(str)
452
482
  nodecount = self.adjmat.shape[0]
453
483
  group = np.zeros_like(node_names).astype(int)
454
484
  # Check validity of color.
455
- _check_hex_color(color, nodecount)
485
+ color = _check_hex_color(color, nodecount, cmap=cmap)
456
486
  # Store in config
457
487
  self.config['cmap'] = 'Paired' if cmap is None else cmap
458
488
  self.config['node_scaler'] = scaler
459
489
 
460
490
  # ############ Set node label #############
461
- if isinstance(label, list):
491
+ if isinstance(label, (list, np.ndarray, pd.Series, pd.Series, StringArray)):
462
492
  label = np.array(label).astype(str)
463
493
  elif 'numpy' in str(type(label)):
464
494
  pass
@@ -471,7 +501,7 @@ class d3graph:
471
501
  if len(label) != nodecount: raise ValueError("[label] must be of same length as the number of nodes")
472
502
 
473
503
  # ############ tooltip text #############
474
- if isinstance(tooltip, list):
504
+ if isinstance(tooltip, (list, np.ndarray, pd.Series, StringArray)):
475
505
  tooltip = np.array(tooltip).astype(str)
476
506
  elif 'numpy' in str(type(tooltip)):
477
507
  pass
@@ -484,7 +514,7 @@ class d3graph:
484
514
  if len(tooltip) != nodecount: raise ValueError("[tooltip text] must be of same length as the number of nodes")
485
515
 
486
516
  # ############ Set node color #############
487
- if isinstance(color, list) and len(color) == nodecount:
517
+ if isinstance(color, (list, np.ndarray, pd.Series, StringArray)) and len(color) == nodecount:
488
518
  color = np.array(color)
489
519
  elif 'numpy' in str(type(color)):
490
520
  color = _get_hexcolor(color, cmap=self.config['cmap'])
@@ -514,7 +544,7 @@ class d3graph:
514
544
  fontsize = _set_node_fontsize(self, fontsize, nodecount)
515
545
 
516
546
  # ########## Set node color edge #############
517
- if isinstance(edge_color, list):
547
+ if isinstance(edge_color, (list, np.ndarray, pd.Series, StringArray)):
518
548
  edge_color = np.array(edge_color)
519
549
  elif 'numpy' in str(type(edge_color)):
520
550
  pass
@@ -546,7 +576,7 @@ class d3graph:
546
576
  marker = _set_marker(self, marker, nodecount)
547
577
 
548
578
  # ############ Set node edge size #############
549
- if isinstance(edge_size, list):
579
+ if isinstance(edge_size, (list, np.ndarray, pd.Series, StringArray)):
550
580
  edge_size = np.array(edge_size)
551
581
  elif 'numpy' in str(type(edge_size)):
552
582
  pass
@@ -569,16 +599,16 @@ class d3graph:
569
599
  'marker': marker[i],
570
600
  'label': label[i],
571
601
  'tooltip': tooltip[i],
572
- 'color': color[i].astype(str),
573
- 'opacity': opacity[i].astype(str),
574
- 'fontcolor': fontcolor[i].astype(str),
575
- 'fontsize': fontsize[i].astype(int),
602
+ 'color': str(color[i]),
603
+ 'opacity': str(opacity[i]),
604
+ 'fontcolor': str(fontcolor[i]),
605
+ 'fontsize': str(fontsize[i]),
576
606
  'size': size[i],
577
607
  'edge_size': edge_size[i],
578
608
  'edge_color': edge_color[i],
579
609
  'group': group[i]}
580
610
 
581
- logger.info('Number of unique nodes: %.0d', len(self.node_properties.keys()))
611
+ logger.info(f'Number of unique nodes: {len(self.node_properties.keys())}')
582
612
 
583
613
  # compute clusters
584
614
  def get_cluster_color(self, node_names: list = None, color: str = '#000080') -> tuple:
@@ -685,13 +715,13 @@ class d3graph:
685
715
 
686
716
  Examples
687
717
  --------
688
- >>> from d3graph import d3graph
718
+ >>> from d3graph import d3graph, import_example
689
719
  >>>
690
720
  >>> # Initialize
691
721
  >>> d3 = d3graph()
692
722
  >>>
693
723
  >>> # Load karate example
694
- >>> adjmat, df = d3.import_example('karate')
724
+ >>> adjmat, df = import_example('karate')
695
725
  >>>
696
726
  >>> # Initialize
697
727
  >>> d3.graph(adjmat)
@@ -763,6 +793,8 @@ class d3graph:
763
793
  'max_slider': self.config['slider'][1],
764
794
  'directed': self.config['directed'],
765
795
  'collision': self.config['collision'],
796
+ 'link_tension': self.config.get('link_tension', 1.0),
797
+ 'sticky': self.config.get('sticky', False),
766
798
  'CLICK_COMMENT': CLICK_COMMENT,
767
799
  'CLICK_FILL': click_properties['fill'],
768
800
  'CLICK_STROKE': click_properties['stroke'],
@@ -833,7 +865,8 @@ class d3graph:
833
865
  os.makedirs(dirname, exist_ok=True)
834
866
  filepath = os.path.abspath(os.path.join(dirname, filename))
835
867
  logger.debug(f'filepath is set to [{filepath}]')
836
- return Path(filepath)
868
+ # Set to config
869
+ self.config['filepath'] = Path(filepath)
837
870
 
838
871
  def import_example(self, data='energy', url=None, sep=','):
839
872
  """Import example dataset from github source.
@@ -857,38 +890,7 @@ class d3graph:
857
890
  * https://github.com/erdogant/datazets
858
891
 
859
892
  """
860
- if data == 'small':
861
- source = ['node A', 'node F', 'node B', 'node B', 'node B', 'node A', 'node C', 'node Z']
862
- target = ['node F', 'node B', 'node J', 'node F', 'node F', 'node M', 'node M', 'node A']
863
- weight = [5.56, 0.5, 0.64, 0.23, 0.9, 3.28, 0.5, 0.45]
864
- adjmat = vec2adjmat(source, target, weight=weight)
865
- return adjmat, None
866
- elif data == 'bigbang':
867
- df = dz.get(data=data)
868
- adjmat = vec2adjmat(df['source'], df['target'], weight=df['weight'])
869
- return adjmat
870
- elif data == 'karate':
871
- import scipy
872
- if version.parse(scipy.__version__) < version.parse('1.8.0'):
873
- raise ImportError(
874
- '[d3graph] >Error: This release requires scipy version >= 1.8.0. Try: pip install -U scipy>=1.8.0')
875
-
876
- G = nx.karate_club_graph()
877
- adjmat = nx.adjacency_matrix(G).todense()
878
- adjmat = pd.DataFrame(index=range(adjmat.shape[0]), data=adjmat, columns=range(adjmat.shape[0]))
879
- adjmat.columns = adjmat.columns.astype(str)
880
- adjmat.index = adjmat.index.astype(str)
881
- adjmat.iloc[3, 4] = 5
882
- adjmat.iloc[4, 5] = 6
883
- adjmat.iloc[5, 6] = 7
884
-
885
- df = pd.DataFrame(index=adjmat.index)
886
- df['degree'] = np.array([*G.degree()])[:, 1]
887
- df['label'] = [G.nodes[i]['club'] for i in range(len(G.nodes))]
888
-
889
- return adjmat, df
890
- else:
891
- return dz.get(data=data, url=url, sep=sep)
893
+ return import_example(data=data, url=url, sep=sep)
892
894
 
893
895
 
894
896
  # %%
@@ -1193,6 +1195,7 @@ def adjmat2dict(adjmat: pd.DataFrame,
1193
1195
  df['label_color']=label_color
1194
1196
  df['label_fontsize']=label_fontsize
1195
1197
  df['edge_style']=edge_style
1198
+ df['edge_color']=edge_color
1196
1199
  df['edge_opacity'] = edge_opacity
1197
1200
  df['tooltip'] = df['weight'].astype(str)
1198
1201
 
@@ -1357,6 +1360,7 @@ def _normalize_size(getsizes, minscale=0.5, maxscale=4, scaler: str = 'zscore'):
1357
1360
  # Instead of Min-Max scaling, that shrinks any distribution in the [0, 1] interval, scaling the variables to
1358
1361
  # Z-scores is better. Min-Max Scaling is too sensitive to outlier observations and generates unseen problems.
1359
1362
 
1363
+ getsizes = getsizes.copy()
1360
1364
  # Set sizes to 0 if not available
1361
1365
  getsizes[np.isinf(getsizes)]=0
1362
1366
  getsizes[np.isnan(getsizes)]=0
@@ -1395,6 +1399,8 @@ def _get_hexcolor(label, cmap: str = 'Paired'):
1395
1399
 
1396
1400
  return label
1397
1401
 
1402
+ def get_hex_color(labels, cmap='Set1', opaque_type='per_class', gradient=None):
1403
+ return cm.fromlist(labels, scheme='hex', opaque_type=opaque_type, gradient=gradient)
1398
1404
 
1399
1405
  # %% Do checks
1400
1406
  def library_compatibility_checks() -> None:
@@ -1591,15 +1597,21 @@ def adjmat2vec(adjmat, min_weight: float = 1.0) -> pd.DataFrame:
1591
1597
  return adjmat
1592
1598
 
1593
1599
 
1594
- def _check_hex_color(color, n=None):
1595
- if isinstance(color, str) and len(color) != 7: raise ValueError(
1596
- 'Input parameter [color] has wrong format. Must be like color="#000000"')
1597
- if isinstance(color, list) and len(color) == 0: raise ValueError(
1598
- 'Input parameter [color] has wrong format and length. Must be like: color=["#000000", "...", "#000000"]')
1599
- if isinstance(color, list) and (not np.all(list(map(lambda x: len(x) == 7, color)))): raise ValueError(
1600
- '[color] contains incorrect length of hex-color! Hex must be of length 7: ["#000000", "#000000", etc]')
1601
- if (n is not None) and isinstance(color, list) and len(color) != n:
1602
- raise ValueError(f'Input parameter [color] has wrong length. Must be of length: {str(n)}')
1600
+ def _check_hex_color(color, n=None, cmap='Set1'):
1601
+ if isinstance(color, str) and len(color) != 7:
1602
+ logger.warning('Input parameter [color] has wrong format. Must be like color="#000000" <auto-fixing>')
1603
+ return get_hex_color(color, cmap=cmap)[0]
1604
+ if isinstance(color, (list, np.ndarray, pd.Series, pd.Series, StringArray)) and len(color) == 0:
1605
+ logger.warning('Input parameter [color] has wrong format and length. Must be like: color=["#000000", "...", "#000000"] <auto-fixing>')
1606
+ return get_hex_color(color, cmap=cmap)[0]
1607
+ if isinstance(color, (list, np.ndarray, pd.Series, pd.Series, StringArray)) and (not np.all(list(map(lambda x: len(x) == 7, color)))):
1608
+ logger.warning('[color] contains incorrect hex-colors. Hex must be of length 7: ["#000000", "#000000", etc] <auto-fixing>')
1609
+ return get_hex_color(color, cmap=cmap)[0]
1610
+ if (n is not None) and isinstance(color, (list, np.ndarray, pd.Series, pd.Series, StringArray)) and len(color) != n:
1611
+ logger.warning(f'Input parameter [color] has wrong length. Must be of length: {str(n)} <auto-fixing>')
1612
+ return get_hex_color(color, cmap=cmap)[0]
1613
+ # Return original input
1614
+ return color
1603
1615
 
1604
1616
 
1605
1617
  def _set_opacity(self, opacity, nodecount, node_names):
@@ -1723,6 +1735,61 @@ def _set_node_fontcolor(self, fontcolor, color, node_names, nodecount):
1723
1735
  # return
1724
1736
  return fontcolor
1725
1737
 
1738
+ def import_example(data='energy', url=None, sep=','):
1739
+ """Import example dataset from github source.
1740
+
1741
+ Import one of the few datasets from github source or specify your own download url link.
1742
+
1743
+ Parameters
1744
+ ----------
1745
+ data : str
1746
+ Name of datasets: 'sprinkler', 'titanic', 'student', 'fifa', 'cancer', 'waterpump', 'retail'
1747
+ url : str
1748
+ url link to to dataset.
1749
+
1750
+ Returns
1751
+ -------
1752
+ pd.DataFrame()
1753
+ Dataset containing mixed features.
1754
+
1755
+ References
1756
+ ----------
1757
+ * https://github.com/erdogant/datazets
1758
+
1759
+ """
1760
+ if data == 'small':
1761
+ source = ['node A', 'node F', 'node B', 'node B', 'node B', 'node A', 'node C', 'node Z']
1762
+ target = ['node F', 'node B', 'node J', 'node F', 'node F', 'node M', 'node M', 'node A']
1763
+ weight = [5.56, 0.5, 0.64, 0.23, 0.9, 3.28, 0.5, 0.45]
1764
+ adjmat = vec2adjmat(source, target, weight=weight)
1765
+ return adjmat, None
1766
+ elif data == 'bigbang':
1767
+ df = dz.get(data=data)
1768
+ adjmat = vec2adjmat(df['source'], df['target'], weight=df['weight'])
1769
+ return adjmat
1770
+ elif data == 'karate':
1771
+ import scipy
1772
+ if version.parse(scipy.__version__) < version.parse('1.8.0'):
1773
+ raise ImportError(
1774
+ '[d3graph] >Error: This release requires scipy version >= 1.8.0. Try: pip install -U scipy>=1.8.0')
1775
+
1776
+ G = nx.karate_club_graph()
1777
+ adjmat = nx.adjacency_matrix(G).todense()
1778
+ adjmat = pd.DataFrame(index=range(adjmat.shape[0]), data=adjmat, columns=range(adjmat.shape[0]))
1779
+ adjmat.columns = adjmat.columns.astype(str)
1780
+ adjmat.index = adjmat.index.astype(str)
1781
+ adjmat.iloc[3, 4] = 5
1782
+ adjmat.iloc[4, 5] = 6
1783
+ adjmat.iloc[5, 6] = 7
1784
+
1785
+ df = pd.DataFrame(index=adjmat.index)
1786
+ df['degree'] = np.array([*G.degree()])[:, 1]
1787
+ df['label'] = [G.nodes[i]['club'] for i in range(len(G.nodes))]
1788
+
1789
+ return adjmat, df
1790
+ else:
1791
+ return dz.get(data=data, url=url, sep=sep)
1792
+
1726
1793
 
1727
1794
  def get_support(support):
1728
1795
  """Support."""
@@ -1734,4 +1801,4 @@ def get_support(support):
1734
1801
  script = script + '\n' + "<div data-ea-publisher='erdogantgithubio' data-ea-type='{TYPE}' data-ea-style='stickybox'></div>".replace('{TYPE}', support)
1735
1802
 
1736
1803
  # Return
1737
- return script
1804
+ return script