d3graph 2.7.0__tar.gz → 2.8.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: d3graph
3
- Version: 2.7.0
3
+ Version: 2.8.0
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.7.0'
20
+ __version__ = '2.8.0'
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
@@ -58,7 +60,7 @@ Examples
58
60
  >>> d3.set_edge_properties(directed=True)
59
61
  >>>
60
62
  >>> # Plot
61
- >>> d3.show()
63
+ >>> d3.show(node_text_inside=True)
62
64
 
63
65
  References
64
66
  ----------
@@ -21,6 +21,7 @@ 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
@@ -119,6 +120,7 @@ class d3graph:
119
120
  save_button: bool = True,
120
121
  link_tension: float = None,
121
122
  sticky: bool = None,
123
+ node_text_inside: bool = False,
122
124
  ) -> None:
123
125
  """Build and show the graph.
124
126
 
@@ -185,6 +187,8 @@ class d3graph:
185
187
  self.config['save_button'] = save_button
186
188
  self.config['background_color'] = background_color
187
189
  self.config['dark_mode'] = dark_mode
190
+ self.config['node_text_inside'] = node_text_inside
191
+
188
192
  # Allow show() to override the link_tension set at __init__ time
189
193
  if link_tension is not None:
190
194
  self.config['link_tension'] = link_tension
@@ -192,7 +196,8 @@ class d3graph:
192
196
  if sticky is not None:
193
197
  self.config['sticky'] = sticky
194
198
  # if self.config.get('filepath', None) != 'd3graph.html':
195
- self.config['filepath'] = self.set_path(filepath)
199
+ if filepath is not None or self.config.get('filepath') is None:
200
+ self.set_path(filepath)
196
201
 
197
202
  # Create dataframe from co-occurrence matrix
198
203
  self.G = make_graph(self.node_properties, self.edge_properties)
@@ -394,7 +399,8 @@ class d3graph:
394
399
  * ['label 1','label 2','label 3', ...]
395
400
  marker : list of markers (default: 'circle')
396
401
  The marker that is used for the Node.
397
- * ['circle','rect','rect', ...]
402
+ * 'circle' or 'hexagon' etc
403
+ * ['circle', 'star', 'diamond', 'square', 'pentagon', 'hexagon', 'rectangle', 'triangle-down', 'triangle']
398
404
  tooltip : list of names (default: None)
399
405
  The text that is shown when hovering over the Node.
400
406
  If not specified, the text will inherit from the label.
@@ -480,13 +486,13 @@ class d3graph:
480
486
  nodecount = self.adjmat.shape[0]
481
487
  group = np.zeros_like(node_names).astype(int)
482
488
  # Check validity of color.
483
- _check_hex_color(color, nodecount)
489
+ color = _check_hex_color(color, nodecount, cmap=cmap)
484
490
  # Store in config
485
491
  self.config['cmap'] = 'Paired' if cmap is None else cmap
486
492
  self.config['node_scaler'] = scaler
487
493
 
488
494
  # ############ Set node label #############
489
- if isinstance(label, list):
495
+ if isinstance(label, (list, np.ndarray, pd.Series, pd.Series, StringArray)):
490
496
  label = np.array(label).astype(str)
491
497
  elif 'numpy' in str(type(label)):
492
498
  pass
@@ -499,7 +505,7 @@ class d3graph:
499
505
  if len(label) != nodecount: raise ValueError("[label] must be of same length as the number of nodes")
500
506
 
501
507
  # ############ tooltip text #############
502
- if isinstance(tooltip, list):
508
+ if isinstance(tooltip, (list, np.ndarray, pd.Series, StringArray)):
503
509
  tooltip = np.array(tooltip).astype(str)
504
510
  elif 'numpy' in str(type(tooltip)):
505
511
  pass
@@ -512,7 +518,7 @@ class d3graph:
512
518
  if len(tooltip) != nodecount: raise ValueError("[tooltip text] must be of same length as the number of nodes")
513
519
 
514
520
  # ############ Set node color #############
515
- if isinstance(color, list) and len(color) == nodecount:
521
+ if isinstance(color, (list, np.ndarray, pd.Series, StringArray)) and len(color) == nodecount:
516
522
  color = np.array(color)
517
523
  elif 'numpy' in str(type(color)):
518
524
  color = _get_hexcolor(color, cmap=self.config['cmap'])
@@ -542,7 +548,7 @@ class d3graph:
542
548
  fontsize = _set_node_fontsize(self, fontsize, nodecount)
543
549
 
544
550
  # ########## Set node color edge #############
545
- if isinstance(edge_color, list):
551
+ if isinstance(edge_color, (list, np.ndarray, pd.Series, StringArray)):
546
552
  edge_color = np.array(edge_color)
547
553
  elif 'numpy' in str(type(edge_color)):
548
554
  pass
@@ -574,7 +580,7 @@ class d3graph:
574
580
  marker = _set_marker(self, marker, nodecount)
575
581
 
576
582
  # ############ Set node edge size #############
577
- if isinstance(edge_size, list):
583
+ if isinstance(edge_size, (list, np.ndarray, pd.Series, StringArray)):
578
584
  edge_size = np.array(edge_size)
579
585
  elif 'numpy' in str(type(edge_size)):
580
586
  pass
@@ -597,16 +603,16 @@ class d3graph:
597
603
  'marker': marker[i],
598
604
  'label': label[i],
599
605
  'tooltip': tooltip[i],
600
- 'color': color[i].astype(str),
601
- 'opacity': opacity[i].astype(str),
602
- 'fontcolor': fontcolor[i].astype(str),
603
- 'fontsize': fontsize[i].astype(int),
606
+ 'color': str(color[i]),
607
+ 'opacity': str(opacity[i]),
608
+ 'fontcolor': str(fontcolor[i]),
609
+ 'fontsize': str(fontsize[i]),
604
610
  'size': size[i],
605
611
  'edge_size': edge_size[i],
606
612
  'edge_color': edge_color[i],
607
613
  'group': group[i]}
608
614
 
609
- logger.info('Number of unique nodes: %.0d', len(self.node_properties.keys()))
615
+ logger.info(f'Number of unique nodes: {len(self.node_properties.keys())}')
610
616
 
611
617
  # compute clusters
612
618
  def get_cluster_color(self, node_names: list = None, color: str = '#000080') -> tuple:
@@ -713,13 +719,13 @@ class d3graph:
713
719
 
714
720
  Examples
715
721
  --------
716
- >>> from d3graph import d3graph
722
+ >>> from d3graph import d3graph, import_example
717
723
  >>>
718
724
  >>> # Initialize
719
725
  >>> d3 = d3graph()
720
726
  >>>
721
727
  >>> # Load karate example
722
- >>> adjmat, df = d3.import_example('karate')
728
+ >>> adjmat, df = import_example('karate')
723
729
  >>>
724
730
  >>> # Initialize
725
731
  >>> d3.graph(adjmat)
@@ -793,6 +799,7 @@ class d3graph:
793
799
  'collision': self.config['collision'],
794
800
  'link_tension': self.config.get('link_tension', 1.0),
795
801
  'sticky': self.config.get('sticky', False),
802
+ 'node_text_inside': self.config.get('node_text_inside', False),
796
803
  'CLICK_COMMENT': CLICK_COMMENT,
797
804
  'CLICK_FILL': click_properties['fill'],
798
805
  'CLICK_STROKE': click_properties['stroke'],
@@ -863,7 +870,8 @@ class d3graph:
863
870
  os.makedirs(dirname, exist_ok=True)
864
871
  filepath = os.path.abspath(os.path.join(dirname, filename))
865
872
  logger.debug(f'filepath is set to [{filepath}]')
866
- return Path(filepath)
873
+ # Set to config
874
+ self.config['filepath'] = Path(filepath)
867
875
 
868
876
  def import_example(self, data='energy', url=None, sep=','):
869
877
  """Import example dataset from github source.
@@ -887,38 +895,7 @@ class d3graph:
887
895
  * https://github.com/erdogant/datazets
888
896
 
889
897
  """
890
- if data == 'small':
891
- source = ['node A', 'node F', 'node B', 'node B', 'node B', 'node A', 'node C', 'node Z']
892
- target = ['node F', 'node B', 'node J', 'node F', 'node F', 'node M', 'node M', 'node A']
893
- weight = [5.56, 0.5, 0.64, 0.23, 0.9, 3.28, 0.5, 0.45]
894
- adjmat = vec2adjmat(source, target, weight=weight)
895
- return adjmat, None
896
- elif data == 'bigbang':
897
- df = dz.get(data=data)
898
- adjmat = vec2adjmat(df['source'], df['target'], weight=df['weight'])
899
- return adjmat
900
- elif data == 'karate':
901
- import scipy
902
- if version.parse(scipy.__version__) < version.parse('1.8.0'):
903
- raise ImportError(
904
- '[d3graph] >Error: This release requires scipy version >= 1.8.0. Try: pip install -U scipy>=1.8.0')
905
-
906
- G = nx.karate_club_graph()
907
- adjmat = nx.adjacency_matrix(G).todense()
908
- adjmat = pd.DataFrame(index=range(adjmat.shape[0]), data=adjmat, columns=range(adjmat.shape[0]))
909
- adjmat.columns = adjmat.columns.astype(str)
910
- adjmat.index = adjmat.index.astype(str)
911
- adjmat.iloc[3, 4] = 5
912
- adjmat.iloc[4, 5] = 6
913
- adjmat.iloc[5, 6] = 7
914
-
915
- df = pd.DataFrame(index=adjmat.index)
916
- df['degree'] = np.array([*G.degree()])[:, 1]
917
- df['label'] = [G.nodes[i]['club'] for i in range(len(G.nodes))]
918
-
919
- return adjmat, df
920
- else:
921
- return dz.get(data=data, url=url, sep=sep)
898
+ return import_example(data=data, url=url, sep=sep)
922
899
 
923
900
 
924
901
  # %%
@@ -1427,6 +1404,8 @@ def _get_hexcolor(label, cmap: str = 'Paired'):
1427
1404
 
1428
1405
  return label
1429
1406
 
1407
+ def get_hex_color(labels, cmap='Set1', opaque_type='per_class', gradient=None):
1408
+ return cm.fromlist(labels, scheme='hex', opaque_type=opaque_type, gradient=gradient)
1430
1409
 
1431
1410
  # %% Do checks
1432
1411
  def library_compatibility_checks() -> None:
@@ -1437,7 +1416,7 @@ def library_compatibility_checks() -> None:
1437
1416
  None.
1438
1417
 
1439
1418
  """
1440
- if not version.parse(nx.__version__) >= version.parse('2.5'):
1419
+ if not version.parse(nx.__version__) >= version.parse('2.5.0'):
1441
1420
  logger.error('Networkx version should be >= 2.5')
1442
1421
  logger.info('Hint: pip install -U networkx')
1443
1422
 
@@ -1623,15 +1602,21 @@ def adjmat2vec(adjmat, min_weight: float = 1.0) -> pd.DataFrame:
1623
1602
  return adjmat
1624
1603
 
1625
1604
 
1626
- def _check_hex_color(color, n=None):
1627
- if isinstance(color, str) and len(color) != 7: raise ValueError(
1628
- 'Input parameter [color] has wrong format. Must be like color="#000000"')
1629
- if isinstance(color, list) and len(color) == 0: raise ValueError(
1630
- 'Input parameter [color] has wrong format and length. Must be like: color=["#000000", "...", "#000000"]')
1631
- if isinstance(color, list) and (not np.all(list(map(lambda x: len(x) == 7, color)))): raise ValueError(
1632
- '[color] contains incorrect length of hex-color! Hex must be of length 7: ["#000000", "#000000", etc]')
1633
- if (n is not None) and isinstance(color, list) and len(color) != n:
1634
- raise ValueError(f'Input parameter [color] has wrong length. Must be of length: {str(n)}')
1605
+ def _check_hex_color(color, n=None, cmap='Set1'):
1606
+ if isinstance(color, str) and len(color) != 7:
1607
+ logger.warning('Input parameter [color] has wrong format. Must be like color="#000000" <auto-fixing>')
1608
+ return get_hex_color(color, cmap=cmap)[0]
1609
+ if isinstance(color, (list, np.ndarray, pd.Series, pd.Series, StringArray)) and len(color) == 0:
1610
+ logger.warning('Input parameter [color] has wrong format and length. Must be like: color=["#000000", "...", "#000000"] <auto-fixing>')
1611
+ return get_hex_color(color, cmap=cmap)[0]
1612
+ if isinstance(color, (list, np.ndarray, pd.Series, pd.Series, StringArray)) and (not np.all(list(map(lambda x: len(x) == 7, color)))):
1613
+ logger.warning('[color] contains incorrect hex-colors. Hex must be of length 7: ["#000000", "#000000", etc] <auto-fixing>')
1614
+ return get_hex_color(color, cmap=cmap)[0]
1615
+ if (n is not None) and isinstance(color, (list, np.ndarray, pd.Series, pd.Series, StringArray)) and len(color) != n:
1616
+ logger.warning(f'Input parameter [color] has wrong length. Must be of length: {str(n)} <auto-fixing>')
1617
+ return get_hex_color(color, cmap=cmap)[0]
1618
+ # Return original input
1619
+ return color
1635
1620
 
1636
1621
 
1637
1622
  def _set_opacity(self, opacity, nodecount, node_names):
@@ -1673,20 +1658,63 @@ def _compute_centrality(adjmat):
1673
1658
  # opacity = opacity[:-2]
1674
1659
  return opacity
1675
1660
 
1661
+ MARKERS_SUPPORTED = {
1662
+ 'circle',
1663
+ 'ellipse',
1664
+ 'rect', 'rectangle', 'square',
1665
+ 'rect-wide', 'rectangle-wide',
1666
+ 'triangle', 'triangle-up', 'triangle-down',
1667
+ 'diamond',
1668
+ 'star',
1669
+ 'hexagon',
1670
+ 'pentagon',
1671
+ }
1672
+
1673
+
1676
1674
  def _set_marker(self, marker, nodecount):
1677
- if isinstance(marker, type(None)):
1675
+ """Validate and broadcast the node marker/shape.
1676
+
1677
+ Supported shapes
1678
+ ----------------
1679
+ 'circle' : Circle (default)
1680
+ 'ellipse' : Ellipse (wider than tall)
1681
+ 'rect' / 'rectangle' / 'square' : Square
1682
+ 'rect-wide' / 'rectangle-wide' : Wide rectangle
1683
+ 'triangle' / 'triangle-up' : Triangle pointing up
1684
+ 'triangle-down' : Triangle pointing down
1685
+ 'diamond' : Diamond (rotated square)
1686
+ 'star' : 5-pointed star
1687
+ 'hexagon' : Regular hexagon
1688
+ 'pentagon' : Regular pentagon
1689
+ """
1690
+ if marker is None:
1678
1691
  marker = 'circle'
1679
1692
 
1680
- if isinstance(marker, list) and len(marker)==nodecount:
1681
- marker = np.array(marker)
1682
- elif 'numpy' in str(type(marker)) and len(marker)==nodecount:
1693
+ if isinstance(marker, (list, np.ndarray, pd.Series)) and len(marker) == nodecount:
1694
+ marker = np.array([str(m).lower() for m in marker])
1695
+ invalid = set(marker) - MARKERS_SUPPORTED
1696
+ if invalid:
1697
+ logger.warning(
1698
+ 'Unknown marker(s) %s. Supported: %s. Falling back to "circle" for those.',
1699
+ sorted(invalid), sorted(MARKERS_SUPPORTED)
1700
+ )
1701
+ marker = np.where(np.isin(marker, list(MARKERS_SUPPORTED)), marker, 'circle')
1702
+ elif 'numpy' in str(type(marker)) and len(marker) == nodecount:
1683
1703
  pass
1684
1704
  elif isinstance(marker, str):
1705
+ marker = marker.lower()
1706
+ if marker not in MARKERS_SUPPORTED:
1707
+ logger.warning(
1708
+ 'Unknown marker "%s". Supported: %s. Falling back to "circle".',
1709
+ marker, sorted(MARKERS_SUPPORTED)
1710
+ )
1711
+ marker = 'circle'
1685
1712
  marker = np.repeat(marker, nodecount)
1713
+ else:
1714
+ marker = np.repeat('circle', nodecount)
1686
1715
 
1687
- # Make check
1688
- if len(marker) != nodecount: raise ValueError("Node size must be of same length as the number of nodes")
1689
- # Return
1716
+ if len(marker) != nodecount:
1717
+ raise ValueError("Node marker must be of same length as the number of nodes")
1690
1718
  return marker
1691
1719
 
1692
1720
  def _set_node_size(self, size, minmax, nodecount):
@@ -1755,6 +1783,61 @@ def _set_node_fontcolor(self, fontcolor, color, node_names, nodecount):
1755
1783
  # return
1756
1784
  return fontcolor
1757
1785
 
1786
+ def import_example(data='energy', url=None, sep=','):
1787
+ """Import example dataset from github source.
1788
+
1789
+ Import one of the few datasets from github source or specify your own download url link.
1790
+
1791
+ Parameters
1792
+ ----------
1793
+ data : str
1794
+ Name of datasets: 'sprinkler', 'titanic', 'student', 'fifa', 'cancer', 'waterpump', 'retail'
1795
+ url : str
1796
+ url link to to dataset.
1797
+
1798
+ Returns
1799
+ -------
1800
+ pd.DataFrame()
1801
+ Dataset containing mixed features.
1802
+
1803
+ References
1804
+ ----------
1805
+ * https://github.com/erdogant/datazets
1806
+
1807
+ """
1808
+ if data == 'small':
1809
+ source = ['node A', 'node F', 'node B', 'node B', 'node B', 'node A', 'node C', 'node Z']
1810
+ target = ['node F', 'node B', 'node J', 'node F', 'node F', 'node M', 'node M', 'node A']
1811
+ weight = [5.56, 0.5, 0.64, 0.23, 0.9, 3.28, 0.5, 0.45]
1812
+ adjmat = vec2adjmat(source, target, weight=weight)
1813
+ return adjmat, None
1814
+ elif data == 'bigbang':
1815
+ df = dz.get(data=data)
1816
+ adjmat = vec2adjmat(df['source'], df['target'], weight=df['weight'])
1817
+ return adjmat
1818
+ elif data == 'karate':
1819
+ import scipy
1820
+ if version.parse(scipy.__version__) < version.parse('1.8.0'):
1821
+ raise ImportError(
1822
+ '[d3graph] >Error: This release requires scipy version >= 1.8.0. Try: pip install -U scipy>=1.8.0')
1823
+
1824
+ G = nx.karate_club_graph()
1825
+ adjmat = nx.adjacency_matrix(G).todense()
1826
+ adjmat = pd.DataFrame(index=range(adjmat.shape[0]), data=adjmat, columns=range(adjmat.shape[0]))
1827
+ adjmat.columns = adjmat.columns.astype(str)
1828
+ adjmat.index = adjmat.index.astype(str)
1829
+ adjmat.iloc[3, 4] = 5
1830
+ adjmat.iloc[4, 5] = 6
1831
+ adjmat.iloc[5, 6] = 7
1832
+
1833
+ df = pd.DataFrame(index=adjmat.index)
1834
+ df['degree'] = np.array([*G.degree()])[:, 1]
1835
+ df['label'] = [G.nodes[i]['club'] for i in range(len(G.nodes))]
1836
+
1837
+ return adjmat, df
1838
+ else:
1839
+ return dz.get(data=data, url=url, sep=sep)
1840
+
1758
1841
 
1759
1842
  def get_support(support):
1760
1843
  """Support."""