d3graph 2.7.0__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.
- {d3graph-2.7.0/d3graph.egg-info → d3graph-2.7.2}/PKG-INFO +2 -2
- {d3graph-2.7.0 → d3graph-2.7.2}/d3graph/__init__.py +5 -3
- {d3graph-2.7.0 → d3graph-2.7.2}/d3graph/d3graph.py +91 -56
- d3graph-2.7.2/d3graph/d3js/d3graphscript.js +361 -0
- {d3graph-2.7.0 → d3graph-2.7.2}/d3graph/examples.py +55 -0
- {d3graph-2.7.0 → d3graph-2.7.2/d3graph.egg-info}/PKG-INFO +2 -2
- {d3graph-2.7.0 → d3graph-2.7.2}/d3graph.egg-info/requires.txt +1 -1
- {d3graph-2.7.0 → d3graph-2.7.2}/pyproject.toml +1 -1
- d3graph-2.7.0/d3graph/d3js/d3graphscript.js +0 -361
- {d3graph-2.7.0 → d3graph-2.7.2}/LICENSE +0 -0
- {d3graph-2.7.0 → d3graph-2.7.2}/MANIFEST.in +0 -0
- {d3graph-2.7.0 → d3graph-2.7.2}/README.md +0 -0
- {d3graph-2.7.0 → d3graph-2.7.2}/d3graph/d3js/d3.v3.js +0 -0
- {d3graph-2.7.0 → d3graph-2.7.2}/d3graph/d3js/index.html.j2 +0 -0
- {d3graph-2.7.0 → d3graph-2.7.2}/d3graph/d3js/style.css +0 -0
- {d3graph-2.7.0 → d3graph-2.7.2}/d3graph/examples_docs.py +0 -0
- {d3graph-2.7.0 → d3graph-2.7.2}/d3graph.egg-info/SOURCES.txt +0 -0
- {d3graph-2.7.0 → d3graph-2.7.2}/d3graph.egg-info/dependency_links.txt +0 -0
- {d3graph-2.7.0 → d3graph-2.7.2}/d3graph.egg-info/top_level.txt +0 -0
- {d3graph-2.7.0 → d3graph-2.7.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: d3graph
|
|
3
|
-
Version: 2.7.
|
|
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
|
|
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.
|
|
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 =
|
|
50
|
+
>>> df = import_example('energy')
|
|
49
51
|
>>> adjmat = vec2adjmat(source=df['source'], target=df['target'], weight=df['weight'])
|
|
50
52
|
>>>
|
|
51
53
|
>>> # Initialize
|
|
@@ -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
|
|
@@ -192,7 +193,8 @@ class d3graph:
|
|
|
192
193
|
if sticky is not None:
|
|
193
194
|
self.config['sticky'] = sticky
|
|
194
195
|
# if self.config.get('filepath', None) != 'd3graph.html':
|
|
195
|
-
self.config
|
|
196
|
+
if filepath is not None or self.config.get('filepath') is None:
|
|
197
|
+
self.set_path(filepath)
|
|
196
198
|
|
|
197
199
|
# Create dataframe from co-occurrence matrix
|
|
198
200
|
self.G = make_graph(self.node_properties, self.edge_properties)
|
|
@@ -480,13 +482,13 @@ class d3graph:
|
|
|
480
482
|
nodecount = self.adjmat.shape[0]
|
|
481
483
|
group = np.zeros_like(node_names).astype(int)
|
|
482
484
|
# Check validity of color.
|
|
483
|
-
_check_hex_color(color, nodecount)
|
|
485
|
+
color = _check_hex_color(color, nodecount, cmap=cmap)
|
|
484
486
|
# Store in config
|
|
485
487
|
self.config['cmap'] = 'Paired' if cmap is None else cmap
|
|
486
488
|
self.config['node_scaler'] = scaler
|
|
487
489
|
|
|
488
490
|
# ############ Set node label #############
|
|
489
|
-
if isinstance(label, list):
|
|
491
|
+
if isinstance(label, (list, np.ndarray, pd.Series, pd.Series, StringArray)):
|
|
490
492
|
label = np.array(label).astype(str)
|
|
491
493
|
elif 'numpy' in str(type(label)):
|
|
492
494
|
pass
|
|
@@ -499,7 +501,7 @@ class d3graph:
|
|
|
499
501
|
if len(label) != nodecount: raise ValueError("[label] must be of same length as the number of nodes")
|
|
500
502
|
|
|
501
503
|
# ############ tooltip text #############
|
|
502
|
-
if isinstance(tooltip, list):
|
|
504
|
+
if isinstance(tooltip, (list, np.ndarray, pd.Series, StringArray)):
|
|
503
505
|
tooltip = np.array(tooltip).astype(str)
|
|
504
506
|
elif 'numpy' in str(type(tooltip)):
|
|
505
507
|
pass
|
|
@@ -512,7 +514,7 @@ class d3graph:
|
|
|
512
514
|
if len(tooltip) != nodecount: raise ValueError("[tooltip text] must be of same length as the number of nodes")
|
|
513
515
|
|
|
514
516
|
# ############ Set node color #############
|
|
515
|
-
if isinstance(color, list) and len(color) == nodecount:
|
|
517
|
+
if isinstance(color, (list, np.ndarray, pd.Series, StringArray)) and len(color) == nodecount:
|
|
516
518
|
color = np.array(color)
|
|
517
519
|
elif 'numpy' in str(type(color)):
|
|
518
520
|
color = _get_hexcolor(color, cmap=self.config['cmap'])
|
|
@@ -542,7 +544,7 @@ class d3graph:
|
|
|
542
544
|
fontsize = _set_node_fontsize(self, fontsize, nodecount)
|
|
543
545
|
|
|
544
546
|
# ########## Set node color edge #############
|
|
545
|
-
if isinstance(edge_color, list):
|
|
547
|
+
if isinstance(edge_color, (list, np.ndarray, pd.Series, StringArray)):
|
|
546
548
|
edge_color = np.array(edge_color)
|
|
547
549
|
elif 'numpy' in str(type(edge_color)):
|
|
548
550
|
pass
|
|
@@ -574,7 +576,7 @@ class d3graph:
|
|
|
574
576
|
marker = _set_marker(self, marker, nodecount)
|
|
575
577
|
|
|
576
578
|
# ############ Set node edge size #############
|
|
577
|
-
if isinstance(edge_size, list):
|
|
579
|
+
if isinstance(edge_size, (list, np.ndarray, pd.Series, StringArray)):
|
|
578
580
|
edge_size = np.array(edge_size)
|
|
579
581
|
elif 'numpy' in str(type(edge_size)):
|
|
580
582
|
pass
|
|
@@ -597,16 +599,16 @@ class d3graph:
|
|
|
597
599
|
'marker': marker[i],
|
|
598
600
|
'label': label[i],
|
|
599
601
|
'tooltip': tooltip[i],
|
|
600
|
-
'color': color[i]
|
|
601
|
-
'opacity': opacity[i]
|
|
602
|
-
'fontcolor': fontcolor[i]
|
|
603
|
-
'fontsize': fontsize[i]
|
|
602
|
+
'color': str(color[i]),
|
|
603
|
+
'opacity': str(opacity[i]),
|
|
604
|
+
'fontcolor': str(fontcolor[i]),
|
|
605
|
+
'fontsize': str(fontsize[i]),
|
|
604
606
|
'size': size[i],
|
|
605
607
|
'edge_size': edge_size[i],
|
|
606
608
|
'edge_color': edge_color[i],
|
|
607
609
|
'group': group[i]}
|
|
608
610
|
|
|
609
|
-
logger.info('Number of unique nodes:
|
|
611
|
+
logger.info(f'Number of unique nodes: {len(self.node_properties.keys())}')
|
|
610
612
|
|
|
611
613
|
# compute clusters
|
|
612
614
|
def get_cluster_color(self, node_names: list = None, color: str = '#000080') -> tuple:
|
|
@@ -713,13 +715,13 @@ class d3graph:
|
|
|
713
715
|
|
|
714
716
|
Examples
|
|
715
717
|
--------
|
|
716
|
-
>>> from d3graph import d3graph
|
|
718
|
+
>>> from d3graph import d3graph, import_example
|
|
717
719
|
>>>
|
|
718
720
|
>>> # Initialize
|
|
719
721
|
>>> d3 = d3graph()
|
|
720
722
|
>>>
|
|
721
723
|
>>> # Load karate example
|
|
722
|
-
>>> adjmat, df =
|
|
724
|
+
>>> adjmat, df = import_example('karate')
|
|
723
725
|
>>>
|
|
724
726
|
>>> # Initialize
|
|
725
727
|
>>> d3.graph(adjmat)
|
|
@@ -863,7 +865,8 @@ class d3graph:
|
|
|
863
865
|
os.makedirs(dirname, exist_ok=True)
|
|
864
866
|
filepath = os.path.abspath(os.path.join(dirname, filename))
|
|
865
867
|
logger.debug(f'filepath is set to [{filepath}]')
|
|
866
|
-
|
|
868
|
+
# Set to config
|
|
869
|
+
self.config['filepath'] = Path(filepath)
|
|
867
870
|
|
|
868
871
|
def import_example(self, data='energy', url=None, sep=','):
|
|
869
872
|
"""Import example dataset from github source.
|
|
@@ -887,38 +890,7 @@ class d3graph:
|
|
|
887
890
|
* https://github.com/erdogant/datazets
|
|
888
891
|
|
|
889
892
|
"""
|
|
890
|
-
|
|
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)
|
|
893
|
+
return import_example(data=data, url=url, sep=sep)
|
|
922
894
|
|
|
923
895
|
|
|
924
896
|
# %%
|
|
@@ -1427,6 +1399,8 @@ def _get_hexcolor(label, cmap: str = 'Paired'):
|
|
|
1427
1399
|
|
|
1428
1400
|
return label
|
|
1429
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)
|
|
1430
1404
|
|
|
1431
1405
|
# %% Do checks
|
|
1432
1406
|
def library_compatibility_checks() -> None:
|
|
@@ -1623,15 +1597,21 @@ def adjmat2vec(adjmat, min_weight: float = 1.0) -> pd.DataFrame:
|
|
|
1623
1597
|
return adjmat
|
|
1624
1598
|
|
|
1625
1599
|
|
|
1626
|
-
def _check_hex_color(color, n=None):
|
|
1627
|
-
if isinstance(color, str) and len(color) != 7:
|
|
1628
|
-
'Input parameter [color] has wrong format. Must be like color="#000000"')
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
if (
|
|
1634
|
-
|
|
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
|
|
1635
1615
|
|
|
1636
1616
|
|
|
1637
1617
|
def _set_opacity(self, opacity, nodecount, node_names):
|
|
@@ -1755,6 +1735,61 @@ def _set_node_fontcolor(self, fontcolor, color, node_names, nodecount):
|
|
|
1755
1735
|
# return
|
|
1756
1736
|
return fontcolor
|
|
1757
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
|
+
|
|
1758
1793
|
|
|
1759
1794
|
def get_support(support):
|
|
1760
1795
|
"""Support."""
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
function d3graphscript(config = {
|
|
2
|
+
// Default values
|
|
3
|
+
width: 800,
|
|
4
|
+
height: 600,
|
|
5
|
+
charge: -250,
|
|
6
|
+
distance: 0,
|
|
7
|
+
directed: false,
|
|
8
|
+
collision: 0.5,
|
|
9
|
+
link_tension: 1,
|
|
10
|
+
sticky: false,
|
|
11
|
+
background_color: '#FFFFFF'
|
|
12
|
+
}) {
|
|
13
|
+
|
|
14
|
+
//Constants for the SVG
|
|
15
|
+
var width = config.width;
|
|
16
|
+
var height = config.height;
|
|
17
|
+
var background_color = config.background_color || '#FFFFFF';
|
|
18
|
+
var sticky = config.sticky || false;
|
|
19
|
+
|
|
20
|
+
// Set the body background color
|
|
21
|
+
document.body.style.backgroundColor = background_color;
|
|
22
|
+
|
|
23
|
+
//Set up the colour scale
|
|
24
|
+
var color = d3.scale.category20();
|
|
25
|
+
|
|
26
|
+
var force = d3.layout.force()
|
|
27
|
+
.charge(config.charge)
|
|
28
|
+
.linkDistance((d) => d.edge_distance || config.distance)
|
|
29
|
+
//.linkDistance((d) => config.distance > 0 ? config.distance : d.edge_weight)
|
|
30
|
+
.linkStrength(config.link_tension !== undefined ? config.link_tension : 1)
|
|
31
|
+
.size([width, height]);
|
|
32
|
+
|
|
33
|
+
// ---- DRAGGING ----
|
|
34
|
+
// Sticky mode: dragstart fixes the node so the simulation stops pulling it.
|
|
35
|
+
// dragend keeps it pinned (dashed stroke indicator).
|
|
36
|
+
// Right-click a pinned node to release it back into the simulation.
|
|
37
|
+
// Normal mode: standard free-drag behaviour is preserved.
|
|
38
|
+
|
|
39
|
+
function dragstarted(d) {
|
|
40
|
+
d3.event.sourceEvent.stopPropagation();
|
|
41
|
+
d3.select(this).classed("dragging", true);
|
|
42
|
+
if (sticky) {
|
|
43
|
+
d.fixed = true;
|
|
44
|
+
force.start();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function dragged(d) {
|
|
49
|
+
if (sticky) {
|
|
50
|
+
d.x = d.px = d3.event.x;
|
|
51
|
+
d.y = d.py = d3.event.y;
|
|
52
|
+
} else {
|
|
53
|
+
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function dragended(d) {
|
|
58
|
+
d3.select(this).classed("dragging", false);
|
|
59
|
+
if (sticky) {
|
|
60
|
+
// Keep the node fixed and apply a visual "pinned" cue (dashed border)
|
|
61
|
+
d.fixed = true;
|
|
62
|
+
d3.select(this).select("circle")
|
|
63
|
+
.style("stroke-dasharray", "4,2")
|
|
64
|
+
.style("stroke-width", function(d) { return Math.max(parseFloat(d.node_size_edge) || 1, 2); });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
var drag = force.drag()
|
|
69
|
+
.origin(function(d) { return d; })
|
|
70
|
+
.on("dragstart", dragstarted)
|
|
71
|
+
.on("drag", dragged)
|
|
72
|
+
.on("dragend", dragended);
|
|
73
|
+
|
|
74
|
+
// ---- END DRAGGING ----
|
|
75
|
+
|
|
76
|
+
//Append a SVG to the body of the html page. Assign this SVG as an object to svg
|
|
77
|
+
var svg = d3.select("body").append("svg")
|
|
78
|
+
.attr("width", width)
|
|
79
|
+
.attr("height", height)
|
|
80
|
+
.style("background-color", background_color)
|
|
81
|
+
.call(d3.behavior.zoom().on("zoom", function () { svg.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")") }))
|
|
82
|
+
.on("dblclick.zoom", null)
|
|
83
|
+
.append("g")
|
|
84
|
+
|
|
85
|
+
graphRec = JSON.parse(JSON.stringify(graph));
|
|
86
|
+
|
|
87
|
+
//Creates the graph data structure out of the json data
|
|
88
|
+
force.nodes(graph.nodes)
|
|
89
|
+
.links(graph.links)
|
|
90
|
+
.start();
|
|
91
|
+
|
|
92
|
+
// Create all the line svgs but without locations yet
|
|
93
|
+
var link = svg.selectAll(".link")
|
|
94
|
+
.data(graph.links)
|
|
95
|
+
.enter().append("line")
|
|
96
|
+
.attr("class", "link")
|
|
97
|
+
.attr('marker-start', function(d){ return 'url(#marker_' + d.marker_start + ')' })
|
|
98
|
+
.attr("marker-end", function(d) {
|
|
99
|
+
if (config.directed) {return 'url(#marker_' + d.marker_end + ')' }})
|
|
100
|
+
.style("stroke-width", function(d) {return d.edge_width;}) // LINK-WIDTH
|
|
101
|
+
.style("stroke", function(d) {return d.edge_color;}) // EDGE-COLORS
|
|
102
|
+
.style("stroke-dasharray", function(d) {return d.edge_style;}) // EDGE-STYLE
|
|
103
|
+
.style("opacity", function(d) {return d.edge_opacity;}) // EDGE-OPACITY
|
|
104
|
+
;
|
|
105
|
+
|
|
106
|
+
link.append("title").text(function(d) { return d.tooltip; });
|
|
107
|
+
|
|
108
|
+
// ADD TEXT ON THE EDGES (PART 1/2)
|
|
109
|
+
var linkText = svg.selectAll(".link-text")
|
|
110
|
+
.data(graph.links)
|
|
111
|
+
.enter().append("text")
|
|
112
|
+
.attr("class", "link-text")
|
|
113
|
+
.attr("font-size", function(d) {return d.label_fontsize + "px";})
|
|
114
|
+
.style("fill", function(d) {return d.label_color;})
|
|
115
|
+
.style("font-family", "Arial")
|
|
116
|
+
.text(function(d) { return d.label; });
|
|
117
|
+
|
|
118
|
+
//Do the same with the circles for the nodes
|
|
119
|
+
var node = svg.selectAll(".node")
|
|
120
|
+
.data(graph.nodes)
|
|
121
|
+
.enter().append("g")
|
|
122
|
+
.attr("class", "node")
|
|
123
|
+
.call(drag)
|
|
124
|
+
.on('dblclick', connectedNodes); // HIGHLIGHT ON/OFF
|
|
125
|
+
|
|
126
|
+
// Right-click handler: release a pinned node back into the simulation (sticky mode only)
|
|
127
|
+
if (sticky) {
|
|
128
|
+
node.on('contextmenu', function(d) {
|
|
129
|
+
d3.event.preventDefault();
|
|
130
|
+
d.fixed = false;
|
|
131
|
+
// Remove pinned visual cue
|
|
132
|
+
d3.select(this).select("circle")
|
|
133
|
+
.style("stroke-dasharray", null)
|
|
134
|
+
.style("stroke-width", function(d) { return d.node_size_edge; });
|
|
135
|
+
force.resume();
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
{{ CLICK_COMMENT }} node.on('click', color_on_click); // ON CLICK HANDLER
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
node.append("circle")
|
|
143
|
+
.attr("r", function(d) { return d.node_size; }) // NODE SIZE
|
|
144
|
+
.style("fill", function(d) {return d.node_color;}) // NODE-COLOR
|
|
145
|
+
.style("opacity", function(d) {return d.node_opacity;}) // NODE-OPACITY
|
|
146
|
+
.style("stroke-width", function(d) {return d.node_size_edge;}) // NODE-EDGE-SIZE
|
|
147
|
+
.style("stroke", function(d) {return d.node_color_edge;}) // NODE-COLOR-EDGE
|
|
148
|
+
|
|
149
|
+
// Text in nodes
|
|
150
|
+
node.append("text")
|
|
151
|
+
.attr("dx", 10)
|
|
152
|
+
.attr("dy", ".35em")
|
|
153
|
+
.text(function(d) {return d.node_name}) // NODE-TEXT
|
|
154
|
+
.style("font-size", function(d) {return d.node_fontsize + "px";}) // NODE FONT SIZE
|
|
155
|
+
.style("fill", function(d) {return d.node_fontcolor;}) // NODE FONT COLOR
|
|
156
|
+
.style("font-family", "monospace");
|
|
157
|
+
|
|
158
|
+
let showInHover = ["node_tooltip"]; // Tooltip
|
|
159
|
+
node.append("title")
|
|
160
|
+
.text((d) => Object.keys(d)
|
|
161
|
+
.filter((key) => showInHover.indexOf(key) !== -1)
|
|
162
|
+
.map((key) => `${d[key]}`)
|
|
163
|
+
.join('\n')
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
//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
|
|
167
|
+
force.on("tick", function() {
|
|
168
|
+
link.attr("x1", function(d) { return d.source.x; })
|
|
169
|
+
.attr("y1", function(d) { return d.source.y; })
|
|
170
|
+
.attr("x2", function(d) { return d.target.x; })
|
|
171
|
+
.attr("y2", function(d) { return d.target.y; });
|
|
172
|
+
d3.selectAll("circle").attr("cx", function(d) { return d.x; })
|
|
173
|
+
.attr("cy", function(d) { return d.y; });
|
|
174
|
+
d3.selectAll("text").attr("x", function(d) { return d.x; })
|
|
175
|
+
.attr("y", function(d) { return d.y; })
|
|
176
|
+
linkText.attr("x", function(d) { return (d.source.x + d.target.x) / 2; }) // ADD TEXT ON THE EDGES (PART 2/2)
|
|
177
|
+
.attr("y", function(d) { return (d.source.y + d.target.y) / 2; })
|
|
178
|
+
.attr("text-anchor", "middle");
|
|
179
|
+
|
|
180
|
+
node.each(collide(config.collision)); //COLLISION DETECTION. High means a big fight to get untouchable nodes (default=0.5)
|
|
181
|
+
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// --------- MARKER FOR EDGE ENDINGS -----------
|
|
185
|
+
|
|
186
|
+
var data_marker = [
|
|
187
|
+
{ id: 0, name: 'circle', path: 'M 0, 0 m -5, 0 a 5,5 0 1,0 10,0 a 5,5 0 1,0 -10,0', viewbox: '-6 -6 12 12' }
|
|
188
|
+
, { id: 1, name: 'square', path: 'M 0,0 m -5,-5 L 5,-5 L 5,5 L -5,5 Z', viewbox: '-5 -5 10 10' }
|
|
189
|
+
, { id: 2, name: 'arrow', path: 'M 0,0 m -5,-5 L 5,0 L -5,5 Z', viewbox: '-5 -5 10 10' }
|
|
190
|
+
, { id: 3, name: 'stub', path: 'M 0,0 m -1,-5 L 1,-5 L 1,5 L -1,5 Z', viewbox: '-1 -5 2 10' }
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
svg.append("defs").selectAll("marker")
|
|
194
|
+
.data(data_marker)
|
|
195
|
+
.enter()
|
|
196
|
+
.append('svg:marker')
|
|
197
|
+
.attr('id', function(d){ return 'marker_' + d.name})
|
|
198
|
+
.attr('markerHeight', 10)
|
|
199
|
+
.attr('markerWidth', 10)
|
|
200
|
+
.attr("markerUnits", "userSpaceOnUse") // Fix marker width
|
|
201
|
+
.attr('orient', 'auto')
|
|
202
|
+
.attr('refX', 15) // Offset marker-end
|
|
203
|
+
.attr('refY', 0)
|
|
204
|
+
.attr('viewBox', function(d){ return d.viewbox })
|
|
205
|
+
.append('svg:path')
|
|
206
|
+
.attr('d', function(d){ return d.path }) // Marker type
|
|
207
|
+
.style("fill", '#808080') // Marker color
|
|
208
|
+
.style("stroke", '#808080') // Marker edge-color
|
|
209
|
+
.style("opacity", 0.95) // Marker opacity
|
|
210
|
+
.style("stroke-width", 1); // Marker edge thickness
|
|
211
|
+
|
|
212
|
+
// --------- END MARKER -----------
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
// collision detection
|
|
216
|
+
|
|
217
|
+
var padding = 1, // separation between circles
|
|
218
|
+
radius = 8;
|
|
219
|
+
|
|
220
|
+
function collide(alpha) {
|
|
221
|
+
var quadtree = d3.geom.quadtree(graph.nodes);
|
|
222
|
+
return function(d) {
|
|
223
|
+
var rb = 2 * radius + padding,
|
|
224
|
+
nx1 = d.x - rb,
|
|
225
|
+
nx2 = d.x + rb,
|
|
226
|
+
ny1 = d.y - rb,
|
|
227
|
+
ny2 = d.y + rb;
|
|
228
|
+
quadtree.visit(function(quad, x1, y1, x2, y2) {
|
|
229
|
+
if (quad.point && (quad.point !== d)) {
|
|
230
|
+
var x = d.x - quad.point.x,
|
|
231
|
+
y = d.y - quad.point.y,
|
|
232
|
+
l = Math.sqrt(x * x + y * y);
|
|
233
|
+
if (l < rb) {
|
|
234
|
+
l = (l - rb) / l * alpha;
|
|
235
|
+
d.x -= x *= l;
|
|
236
|
+
d.y -= y *= l;
|
|
237
|
+
quad.point.x += x;
|
|
238
|
+
quad.point.y += y;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
|
|
242
|
+
});
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
// collision detection end
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
//Toggle stores whether the highlighting is on
|
|
249
|
+
var toggle = 0;
|
|
250
|
+
//Create an array logging what is connected to what
|
|
251
|
+
var linkedByIndex = {};
|
|
252
|
+
for (i = 0; i < graph.nodes.length; i++) {
|
|
253
|
+
linkedByIndex[i + "," + i] = 1;
|
|
254
|
+
};
|
|
255
|
+
graph.links.forEach(function(d) {
|
|
256
|
+
linkedByIndex[d.source.index + "," + d.target.index] = 1;
|
|
257
|
+
});
|
|
258
|
+
//This function looks up whether a pair are neighbours
|
|
259
|
+
function neighboring(a, b) {
|
|
260
|
+
return linkedByIndex[a.index + "," + b.index];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
// COLOR ON CLICK
|
|
265
|
+
function color_on_click() {
|
|
266
|
+
// Give the original color back to all nodes
|
|
267
|
+
d3.selectAll(".node")
|
|
268
|
+
.select("circle")
|
|
269
|
+
.style("fill", function(d) {return d.node_color;})
|
|
270
|
+
.style("opacity", function(d) {return d.node_opacity;})
|
|
271
|
+
.style("stroke", function(d) {return d.node_color_edge;})
|
|
272
|
+
.style("stroke-width", function(d) {return d.node_size_edge;})
|
|
273
|
+
// Restore pinned cue on still-fixed nodes
|
|
274
|
+
.style("stroke-dasharray", function(d) { return (sticky && d.fixed) ? "4,2" : null; })
|
|
275
|
+
.attr("r", function(d) { return d.node_size; })
|
|
276
|
+
;
|
|
277
|
+
|
|
278
|
+
// Set the color on the clicked node
|
|
279
|
+
d3.select(this).select("circle")
|
|
280
|
+
.style("fill", {{ CLICK_FILL }})
|
|
281
|
+
.style("stroke", "{{ CLICK_STROKE }}")
|
|
282
|
+
.style("stroke-width", {{ CLICK_STROKEW }})
|
|
283
|
+
.attr("r", function(d) { return d.node_size*{{ CLICK_SIZE }}; })
|
|
284
|
+
;}
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
function connectedNodes() {
|
|
289
|
+
if (toggle == 0) {
|
|
290
|
+
//Reduce the opacity of all but the neighbouring nodes
|
|
291
|
+
d = d3.select(this).node().__data__;
|
|
292
|
+
node.style("opacity", function(o) {
|
|
293
|
+
return neighboring(d, o) | neighboring(o, d) ? 1 : 0.1;
|
|
294
|
+
});
|
|
295
|
+
link.style("opacity", function(o) {
|
|
296
|
+
return d.index == o.source.index | d.index == o.target.index ? 1 : 0.1;
|
|
297
|
+
});
|
|
298
|
+
toggle = 1;
|
|
299
|
+
} else {
|
|
300
|
+
//Put them back to opacity=1
|
|
301
|
+
node.style("opacity", 0.95);
|
|
302
|
+
link.style("opacity", 1);
|
|
303
|
+
|
|
304
|
+
toggle = 0;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
//adjust threshold
|
|
310
|
+
function threshold() {
|
|
311
|
+
let thresh = this.value;
|
|
312
|
+
|
|
313
|
+
graph.links.splice(0, graph.links.length);
|
|
314
|
+
linkText = linkText.data([]); // CLEAR EDGE-LABELS: Clear the linkText selection
|
|
315
|
+
linkText.exit().remove(); // CLEAR EDGE-LABELS: Clear the linkText elements from the DOM
|
|
316
|
+
|
|
317
|
+
for (var i = 0; i < graphRec.links.length; i++) {
|
|
318
|
+
if (graphRec.links[i].edge_weight > thresh) {
|
|
319
|
+
graph.links.push(graphRec.links[i]);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
restart();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Set the initial value of the slider to the user-defined threshold
|
|
326
|
+
document.getElementById('thresholdSlider').value = {{ SET_SLIDER }};
|
|
327
|
+
// Call the threshold function to set the network state
|
|
328
|
+
threshold.call(document.getElementById('thresholdSlider'));
|
|
329
|
+
|
|
330
|
+
d3.select("#thresholdSlider").on("change", threshold);
|
|
331
|
+
|
|
332
|
+
//Restart the visualisation after any node and link changes
|
|
333
|
+
function restart() {
|
|
334
|
+
|
|
335
|
+
// Update EDGE-LINKS
|
|
336
|
+
link = link.data(graph.links);
|
|
337
|
+
link.exit().remove();
|
|
338
|
+
link.enter().insert("line", ".node").attr("class", "link");
|
|
339
|
+
link.style("stroke-width", function(d) {return d.edge_width;}); // LINK-WIDTH AFTER BREAKING WITH SLIDER
|
|
340
|
+
link.style("marker-end", function(d) { // Include the markers.
|
|
341
|
+
if (config.directed) {return 'url(#marker_' + d.marker_end + ')' }})
|
|
342
|
+
link.style("stroke", function(d) {return d.edge_color;}); // EDGE-COLOR AFTER BREAKING WITH SLIDER
|
|
343
|
+
link.style("stroke-dasharray", function(d) {return d.edge_style;}) // EDGE-STYLE
|
|
344
|
+
link.style("opacity", function(d) {return d.edge_opacity;}); // EDGE-OPACITY AFTER BREAKING WITH SLIDER
|
|
345
|
+
|
|
346
|
+
// Update EDGE-LABELS
|
|
347
|
+
linkText = linkText.data(graph.links);
|
|
348
|
+
linkText.exit().remove();
|
|
349
|
+
linkText.enter().append("text")
|
|
350
|
+
.attr("class", "link-text")
|
|
351
|
+
.attr("font-size", function(d) {return d.label_fontsize + "px";})
|
|
352
|
+
.style("fill", function(d) {return d.label_color;})
|
|
353
|
+
.style("font-family", "Arial")
|
|
354
|
+
.text(function(d) { return d.label; });
|
|
355
|
+
|
|
356
|
+
node = node.data(graph.nodes);
|
|
357
|
+
node.enter().insert("circle", ".cursor").attr("class", "node").attr("r", 5).call(force.drag);
|
|
358
|
+
force.start();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
}
|
|
@@ -1,3 +1,57 @@
|
|
|
1
|
+
from d3graph import d3graph, import_example, get_hex_color
|
|
2
|
+
|
|
3
|
+
# Initialize
|
|
4
|
+
d3 = d3graph()
|
|
5
|
+
|
|
6
|
+
# Load karate example
|
|
7
|
+
adjmat, df = import_example('karate')
|
|
8
|
+
|
|
9
|
+
d3.graph(adjmat)
|
|
10
|
+
|
|
11
|
+
# Node properties
|
|
12
|
+
d3.set_node_properties(label=df['label'].values, color=df['label'].values, size=df['degree'].values, edge_size=df['degree'].values, cmap='Set1')
|
|
13
|
+
|
|
14
|
+
colors, labels = get_hex_color(df['label'].values)
|
|
15
|
+
colors=df['label'].values
|
|
16
|
+
d3.set_node_properties(label=df['label'].values, color=colors, size=df['degree'].values, edge_size=df['degree'].values, cmap='Set1')
|
|
17
|
+
|
|
18
|
+
# Edge properties
|
|
19
|
+
d3.set_edge_properties(directed=True)
|
|
20
|
+
|
|
21
|
+
# Plot
|
|
22
|
+
d3.show()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# %% SET PATH ISSUE https://github.com/erdogant/d3graph/issues/42
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from d3graph import d3graph, vec2adjmat, import_example
|
|
28
|
+
output_path = Path.cwd() / 'd3graph.html'
|
|
29
|
+
|
|
30
|
+
# Load example data
|
|
31
|
+
df = import_example('stormofswords')
|
|
32
|
+
adjmat = vec2adjmat(source=df['source'], target=df['target'], weight=df['weight'])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# sticky=True (default) — drag to pin, right-click to release
|
|
36
|
+
d3 = d3graph(sticky=True)
|
|
37
|
+
d3.set_path(output_path)
|
|
38
|
+
d3.config['filepath']
|
|
39
|
+
|
|
40
|
+
d3.graph(adjmat)
|
|
41
|
+
d3.show()
|
|
42
|
+
|
|
43
|
+
# sticky=False — classic spring-back behaviour
|
|
44
|
+
d3 = d3graph(adjmat, sticky=False)
|
|
45
|
+
d3.show()
|
|
46
|
+
|
|
47
|
+
# %%
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# %%
|
|
53
|
+
|
|
54
|
+
|
|
1
55
|
# Import library
|
|
2
56
|
from d3graph import d3graph, vec2adjmat
|
|
3
57
|
|
|
@@ -464,6 +518,7 @@ d3.set_node_properties(color=adjmat.columns.values, size=[10, 20, 10, 10, 15, 10
|
|
|
464
518
|
d3.node_properties['Penny']['tooltip']='test\ntest2'
|
|
465
519
|
d3.show(filepath='c:\\temp\\network3.html')
|
|
466
520
|
|
|
521
|
+
|
|
467
522
|
# %% Checks with cluster label
|
|
468
523
|
from d3graph import d3graph
|
|
469
524
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: d3graph
|
|
3
|
-
Version: 2.7.
|
|
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
|
|
27
|
+
Requires-Dist: markupsafe
|
|
28
28
|
Requires-Dist: python-louvain
|
|
29
29
|
Requires-Dist: datazets
|
|
30
30
|
Dynamic: license-file
|
|
@@ -1,361 +0,0 @@
|
|
|
1
|
-
function d3graphscript(config = {
|
|
2
|
-
// Default values
|
|
3
|
-
width: 800,
|
|
4
|
-
height: 600,
|
|
5
|
-
charge: -250,
|
|
6
|
-
distance: 0,
|
|
7
|
-
directed: false,
|
|
8
|
-
collision: 0.5,
|
|
9
|
-
link_tension: 1,
|
|
10
|
-
sticky: false,
|
|
11
|
-
background_color: '#FFFFFF'
|
|
12
|
-
}) {
|
|
13
|
-
|
|
14
|
-
//Constants for the SVG
|
|
15
|
-
var width = config.width;
|
|
16
|
-
var height = config.height;
|
|
17
|
-
var background_color = config.background_color || '#FFFFFF';
|
|
18
|
-
var sticky = config.sticky || false;
|
|
19
|
-
|
|
20
|
-
// Set the body background color
|
|
21
|
-
document.body.style.backgroundColor = background_color;
|
|
22
|
-
|
|
23
|
-
//Set up the colour scale
|
|
24
|
-
var color = d3.scale.category20();
|
|
25
|
-
|
|
26
|
-
var force = d3.layout.force()
|
|
27
|
-
.charge(config.charge)
|
|
28
|
-
.linkDistance((d) => d.edge_distance || config.distance)
|
|
29
|
-
//.linkDistance((d) => config.distance > 0 ? config.distance : d.edge_weight)
|
|
30
|
-
.linkStrength(config.link_tension !== undefined ? config.link_tension : 1)
|
|
31
|
-
.size([width, height]);
|
|
32
|
-
|
|
33
|
-
// ---- DRAGGING ----
|
|
34
|
-
// Sticky mode: dragstart fixes the node so the simulation stops pulling it.
|
|
35
|
-
// dragend keeps it pinned (dashed stroke indicator).
|
|
36
|
-
// Right-click a pinned node to release it back into the simulation.
|
|
37
|
-
// Normal mode: standard free-drag behaviour is preserved.
|
|
38
|
-
|
|
39
|
-
function dragstarted(d) {
|
|
40
|
-
d3.event.sourceEvent.stopPropagation();
|
|
41
|
-
d3.select(this).classed("dragging", true);
|
|
42
|
-
if (sticky) {
|
|
43
|
-
d.fixed = true;
|
|
44
|
-
force.start();
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function dragged(d) {
|
|
49
|
-
if (sticky) {
|
|
50
|
-
d.x = d.px = d3.event.x;
|
|
51
|
-
d.y = d.py = d3.event.y;
|
|
52
|
-
} else {
|
|
53
|
-
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function dragended(d) {
|
|
58
|
-
d3.select(this).classed("dragging", false);
|
|
59
|
-
if (sticky) {
|
|
60
|
-
// Keep the node fixed and apply a visual "pinned" cue (dashed border)
|
|
61
|
-
d.fixed = true;
|
|
62
|
-
d3.select(this).select("circle")
|
|
63
|
-
.style("stroke-dasharray", "4,2")
|
|
64
|
-
.style("stroke-width", function(d) { return Math.max(parseFloat(d.node_size_edge) || 1, 2); });
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
var drag = force.drag()
|
|
69
|
-
.origin(function(d) { return d; })
|
|
70
|
-
.on("dragstart", dragstarted)
|
|
71
|
-
.on("drag", dragged)
|
|
72
|
-
.on("dragend", dragended);
|
|
73
|
-
|
|
74
|
-
// ---- END DRAGGING ----
|
|
75
|
-
|
|
76
|
-
//Append a SVG to the body of the html page. Assign this SVG as an object to svg
|
|
77
|
-
var svg = d3.select("body").append("svg")
|
|
78
|
-
.attr("width", width)
|
|
79
|
-
.attr("height", height)
|
|
80
|
-
.style("background-color", background_color)
|
|
81
|
-
.call(d3.behavior.zoom().on("zoom", function () { svg.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")") }))
|
|
82
|
-
.on("dblclick.zoom", null)
|
|
83
|
-
.append("g")
|
|
84
|
-
|
|
85
|
-
graphRec = JSON.parse(JSON.stringify(graph));
|
|
86
|
-
|
|
87
|
-
//Creates the graph data structure out of the json data
|
|
88
|
-
force.nodes(graph.nodes)
|
|
89
|
-
.links(graph.links)
|
|
90
|
-
.start();
|
|
91
|
-
|
|
92
|
-
// Create all the line svgs but without locations yet
|
|
93
|
-
var link = svg.selectAll(".link")
|
|
94
|
-
.data(graph.links)
|
|
95
|
-
.enter().append("line")
|
|
96
|
-
.attr("class", "link")
|
|
97
|
-
.attr('marker-start', function(d){ return 'url(#marker_' + d.marker_start + ')' })
|
|
98
|
-
.attr("marker-end", function(d) {
|
|
99
|
-
if (config.directed) {return 'url(#marker_' + d.marker_end + ')' }})
|
|
100
|
-
.style("stroke-width", function(d) {return d.edge_width;}) // LINK-WIDTH
|
|
101
|
-
.style("stroke", function(d) {return d.edge_color;}) // EDGE-COLORS
|
|
102
|
-
.style("stroke-dasharray", function(d) {return d.edge_style;}) // EDGE-STYLE
|
|
103
|
-
.style("opacity", function(d) {return d.edge_opacity;}) // EDGE-OPACITY
|
|
104
|
-
;
|
|
105
|
-
|
|
106
|
-
link.append("title").text(function(d) { return d.tooltip; });
|
|
107
|
-
|
|
108
|
-
// ADD TEXT ON THE EDGES (PART 1/2)
|
|
109
|
-
var linkText = svg.selectAll(".link-text")
|
|
110
|
-
.data(graph.links)
|
|
111
|
-
.enter().append("text")
|
|
112
|
-
.attr("class", "link-text")
|
|
113
|
-
.attr("font-size", function(d) {return d.label_fontsize + "px";})
|
|
114
|
-
.style("fill", function(d) {return d.label_color;})
|
|
115
|
-
.style("font-family", "Arial")
|
|
116
|
-
.text(function(d) { return d.label; });
|
|
117
|
-
|
|
118
|
-
//Do the same with the circles for the nodes
|
|
119
|
-
var node = svg.selectAll(".node")
|
|
120
|
-
.data(graph.nodes)
|
|
121
|
-
.enter().append("g")
|
|
122
|
-
.attr("class", "node")
|
|
123
|
-
.call(drag)
|
|
124
|
-
.on('dblclick', connectedNodes); // HIGHLIGHT ON/OFF
|
|
125
|
-
|
|
126
|
-
// Right-click handler: release a pinned node back into the simulation (sticky mode only)
|
|
127
|
-
if (sticky) {
|
|
128
|
-
node.on('contextmenu', function(d) {
|
|
129
|
-
d3.event.preventDefault();
|
|
130
|
-
d.fixed = false;
|
|
131
|
-
// Remove pinned visual cue
|
|
132
|
-
d3.select(this).select("circle")
|
|
133
|
-
.style("stroke-dasharray", null)
|
|
134
|
-
.style("stroke-width", function(d) { return d.node_size_edge; });
|
|
135
|
-
force.resume();
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
{{ CLICK_COMMENT }} node.on('click', color_on_click); // ON CLICK HANDLER
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
node.append("circle")
|
|
143
|
-
.attr("r", function(d) { return d.node_size; }) // NODE SIZE
|
|
144
|
-
.style("fill", function(d) {return d.node_color;}) // NODE-COLOR
|
|
145
|
-
.style("opacity", function(d) {return d.node_opacity;}) // NODE-OPACITY
|
|
146
|
-
.style("stroke-width", function(d) {return d.node_size_edge;}) // NODE-EDGE-SIZE
|
|
147
|
-
.style("stroke", function(d) {return d.node_color_edge;}) // NODE-COLOR-EDGE
|
|
148
|
-
|
|
149
|
-
// Text in nodes
|
|
150
|
-
node.append("text")
|
|
151
|
-
.attr("dx", 10)
|
|
152
|
-
.attr("dy", ".35em")
|
|
153
|
-
.text(function(d) {return d.node_name}) // NODE-TEXT
|
|
154
|
-
.style("font-size", function(d) {return d.node_fontsize + "px";}) // NODE FONT SIZE
|
|
155
|
-
.style("fill", function(d) {return d.node_fontcolor;}) // NODE FONT COLOR
|
|
156
|
-
.style("font-family", "monospace");
|
|
157
|
-
|
|
158
|
-
let showInHover = ["node_tooltip"]; // Tooltip
|
|
159
|
-
node.append("title")
|
|
160
|
-
.text((d) => Object.keys(d)
|
|
161
|
-
.filter((key) => showInHover.indexOf(key) !== -1)
|
|
162
|
-
.map((key) => `${d[key]}`)
|
|
163
|
-
.join('\n')
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
//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
|
|
167
|
-
force.on("tick", function() {
|
|
168
|
-
link.attr("x1", function(d) { return d.source.x; })
|
|
169
|
-
.attr("y1", function(d) { return d.source.y; })
|
|
170
|
-
.attr("x2", function(d) { return d.target.x; })
|
|
171
|
-
.attr("y2", function(d) { return d.target.y; });
|
|
172
|
-
d3.selectAll("circle").attr("cx", function(d) { return d.x; })
|
|
173
|
-
.attr("cy", function(d) { return d.y; });
|
|
174
|
-
d3.selectAll("text").attr("x", function(d) { return d.x; })
|
|
175
|
-
.attr("y", function(d) { return d.y; })
|
|
176
|
-
linkText.attr("x", function(d) { return (d.source.x + d.target.x) / 2; }) // ADD TEXT ON THE EDGES (PART 2/2)
|
|
177
|
-
.attr("y", function(d) { return (d.source.y + d.target.y) / 2; })
|
|
178
|
-
.attr("text-anchor", "middle");
|
|
179
|
-
|
|
180
|
-
node.each(collide(config.collision)); //COLLISION DETECTION. High means a big fight to get untouchable nodes (default=0.5)
|
|
181
|
-
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
// --------- MARKER FOR EDGE ENDINGS -----------
|
|
185
|
-
|
|
186
|
-
var data_marker = [
|
|
187
|
-
{ id: 0, name: 'circle', path: 'M 0, 0 m -5, 0 a 5,5 0 1,0 10,0 a 5,5 0 1,0 -10,0', viewbox: '-6 -6 12 12' }
|
|
188
|
-
, { id: 1, name: 'square', path: 'M 0,0 m -5,-5 L 5,-5 L 5,5 L -5,5 Z', viewbox: '-5 -5 10 10' }
|
|
189
|
-
, { id: 2, name: 'arrow', path: 'M 0,0 m -5,-5 L 5,0 L -5,5 Z', viewbox: '-5 -5 10 10' }
|
|
190
|
-
, { id: 3, name: 'stub', path: 'M 0,0 m -1,-5 L 1,-5 L 1,5 L -1,5 Z', viewbox: '-1 -5 2 10' }
|
|
191
|
-
]
|
|
192
|
-
|
|
193
|
-
svg.append("defs").selectAll("marker")
|
|
194
|
-
.data(data_marker)
|
|
195
|
-
.enter()
|
|
196
|
-
.append('svg:marker')
|
|
197
|
-
.attr('id', function(d){ return 'marker_' + d.name})
|
|
198
|
-
.attr('markerHeight', 10)
|
|
199
|
-
.attr('markerWidth', 10)
|
|
200
|
-
.attr("markerUnits", "userSpaceOnUse") // Fix marker width
|
|
201
|
-
.attr('orient', 'auto')
|
|
202
|
-
.attr('refX', 15) // Offset marker-end
|
|
203
|
-
.attr('refY', 0)
|
|
204
|
-
.attr('viewBox', function(d){ return d.viewbox })
|
|
205
|
-
.append('svg:path')
|
|
206
|
-
.attr('d', function(d){ return d.path }) // Marker type
|
|
207
|
-
.style("fill", '#808080') // Marker color
|
|
208
|
-
.style("stroke", '#808080') // Marker edge-color
|
|
209
|
-
.style("opacity", 0.95) // Marker opacity
|
|
210
|
-
.style("stroke-width", 1); // Marker edge thickness
|
|
211
|
-
|
|
212
|
-
// --------- END MARKER -----------
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
// collision detection
|
|
216
|
-
|
|
217
|
-
var padding = 1, // separation between circles
|
|
218
|
-
radius = 8;
|
|
219
|
-
|
|
220
|
-
function collide(alpha) {
|
|
221
|
-
var quadtree = d3.geom.quadtree(graph.nodes);
|
|
222
|
-
return function(d) {
|
|
223
|
-
var rb = 2 * radius + padding,
|
|
224
|
-
nx1 = d.x - rb,
|
|
225
|
-
nx2 = d.x + rb,
|
|
226
|
-
ny1 = d.y - rb,
|
|
227
|
-
ny2 = d.y + rb;
|
|
228
|
-
quadtree.visit(function(quad, x1, y1, x2, y2) {
|
|
229
|
-
if (quad.point && (quad.point !== d)) {
|
|
230
|
-
var x = d.x - quad.point.x,
|
|
231
|
-
y = d.y - quad.point.y,
|
|
232
|
-
l = Math.sqrt(x * x + y * y);
|
|
233
|
-
if (l < rb) {
|
|
234
|
-
l = (l - rb) / l * alpha;
|
|
235
|
-
d.x -= x *= l;
|
|
236
|
-
d.y -= y *= l;
|
|
237
|
-
quad.point.x += x;
|
|
238
|
-
quad.point.y += y;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
|
|
242
|
-
});
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
// collision detection end
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
//Toggle stores whether the highlighting is on
|
|
249
|
-
var toggle = 0;
|
|
250
|
-
//Create an array logging what is connected to what
|
|
251
|
-
var linkedByIndex = {};
|
|
252
|
-
for (i = 0; i < graph.nodes.length; i++) {
|
|
253
|
-
linkedByIndex[i + "," + i] = 1;
|
|
254
|
-
};
|
|
255
|
-
graph.links.forEach(function(d) {
|
|
256
|
-
linkedByIndex[d.source.index + "," + d.target.index] = 1;
|
|
257
|
-
});
|
|
258
|
-
//This function looks up whether a pair are neighbours
|
|
259
|
-
function neighboring(a, b) {
|
|
260
|
-
return linkedByIndex[a.index + "," + b.index];
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
// COLOR ON CLICK
|
|
265
|
-
function color_on_click() {
|
|
266
|
-
// Give the original color back to all nodes
|
|
267
|
-
d3.selectAll(".node")
|
|
268
|
-
.select("circle")
|
|
269
|
-
.style("fill", function(d) {return d.node_color;})
|
|
270
|
-
.style("opacity", function(d) {return d.node_opacity;})
|
|
271
|
-
.style("stroke", function(d) {return d.node_color_edge;})
|
|
272
|
-
.style("stroke-width", function(d) {return d.node_size_edge;})
|
|
273
|
-
// Restore pinned cue on still-fixed nodes
|
|
274
|
-
.style("stroke-dasharray", function(d) { return (sticky && d.fixed) ? "4,2" : null; })
|
|
275
|
-
.attr("r", function(d) { return d.node_size; })
|
|
276
|
-
;
|
|
277
|
-
|
|
278
|
-
// Set the color on the clicked node
|
|
279
|
-
d3.select(this).select("circle")
|
|
280
|
-
.style("fill", {{ CLICK_FILL }})
|
|
281
|
-
.style("stroke", "{{ CLICK_STROKE }}")
|
|
282
|
-
.style("stroke-width", {{ CLICK_STROKEW }})
|
|
283
|
-
.attr("r", function(d) { return d.node_size*{{ CLICK_SIZE }}; })
|
|
284
|
-
;}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
function connectedNodes() {
|
|
289
|
-
if (toggle == 0) {
|
|
290
|
-
//Reduce the opacity of all but the neighbouring nodes
|
|
291
|
-
d = d3.select(this).node().__data__;
|
|
292
|
-
node.style("opacity", function(o) {
|
|
293
|
-
return neighboring(d, o) | neighboring(o, d) ? 1 : 0.1;
|
|
294
|
-
});
|
|
295
|
-
link.style("opacity", function(o) {
|
|
296
|
-
return d.index == o.source.index | d.index == o.target.index ? 1 : 0.1;
|
|
297
|
-
});
|
|
298
|
-
toggle = 1;
|
|
299
|
-
} else {
|
|
300
|
-
//Put them back to opacity=1
|
|
301
|
-
node.style("opacity", 0.95);
|
|
302
|
-
link.style("opacity", 1);
|
|
303
|
-
|
|
304
|
-
toggle = 0;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
//adjust threshold
|
|
310
|
-
function threshold() {
|
|
311
|
-
let thresh = this.value;
|
|
312
|
-
|
|
313
|
-
graph.links.splice(0, graph.links.length);
|
|
314
|
-
linkText = linkText.data([]); // CLEAR EDGE-LABELS: Clear the linkText selection
|
|
315
|
-
linkText.exit().remove(); // CLEAR EDGE-LABELS: Clear the linkText elements from the DOM
|
|
316
|
-
|
|
317
|
-
for (var i = 0; i < graphRec.links.length; i++) {
|
|
318
|
-
if (graphRec.links[i].edge_weight > thresh) {
|
|
319
|
-
graph.links.push(graphRec.links[i]);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
restart();
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Set the initial value of the slider to the user-defined threshold
|
|
326
|
-
document.getElementById('thresholdSlider').value = {{ SET_SLIDER }};
|
|
327
|
-
// Call the threshold function to set the network state
|
|
328
|
-
threshold.call(document.getElementById('thresholdSlider'));
|
|
329
|
-
|
|
330
|
-
d3.select("#thresholdSlider").on("change", threshold);
|
|
331
|
-
|
|
332
|
-
//Restart the visualisation after any node and link changes
|
|
333
|
-
function restart() {
|
|
334
|
-
|
|
335
|
-
// Update EDGE-LINKS
|
|
336
|
-
link = link.data(graph.links);
|
|
337
|
-
link.exit().remove();
|
|
338
|
-
link.enter().insert("line", ".node").attr("class", "link");
|
|
339
|
-
link.style("stroke-width", function(d) {return d.edge_width;}); // LINK-WIDTH AFTER BREAKING WITH SLIDER
|
|
340
|
-
link.style("marker-end", function(d) { // Include the markers.
|
|
341
|
-
if (config.directed) {return 'url(#marker_' + d.marker_end + ')' }})
|
|
342
|
-
link.style("stroke", function(d) {return d.edge_color;}); // EDGE-COLOR AFTER BREAKING WITH SLIDER
|
|
343
|
-
link.style("stroke-dasharray", function(d) {return d.edge_style;}) // EDGE-STYLE
|
|
344
|
-
link.style("opacity", function(d) {return d.edge_opacity;}); // EDGE-OPACITY AFTER BREAKING WITH SLIDER
|
|
345
|
-
|
|
346
|
-
// Update EDGE-LABELS
|
|
347
|
-
linkText = linkText.data(graph.links);
|
|
348
|
-
linkText.exit().remove();
|
|
349
|
-
linkText.enter().append("text")
|
|
350
|
-
.attr("class", "link-text")
|
|
351
|
-
.attr("font-size", function(d) {return d.label_fontsize + "px";})
|
|
352
|
-
.style("fill", function(d) {return d.label_color;})
|
|
353
|
-
.style("font-family", "Arial")
|
|
354
|
-
.text(function(d) { return d.label; });
|
|
355
|
-
|
|
356
|
-
node = node.data(graph.nodes);
|
|
357
|
-
node.enter().insert("circle", ".cursor").attr("class", "node").attr("r", 5).call(force.drag);
|
|
358
|
-
force.start();
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
}
|
|
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
|