d3graph 2.6.1__tar.gz → 2.7.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,49 +1,19 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: d3graph
3
- Version: 2.6.1
3
+ Version: 2.7.0
4
4
  Summary: Python package to create interactive network based on d3js.
5
5
  Author-email: Erdogan Taskesen <erdogant@gmail.com>
6
- License: d3graph is distributed with the 3-clause BSD license.
7
-
8
- ::
9
-
10
- Copyright (C) 2000-2022,
11
- Erdogan Taskesen <erdogant@gmail.com>
12
- All rights reserved.
13
-
14
- Redistribution and use in source and binary forms, with or without
15
- modification, are permitted provided that the following conditions are
16
- met:
17
-
18
- * Redistributions of source code must retain the above copyright
19
- notice, this list of conditions and the following disclaimer.
20
-
21
- * Redistributions in binary form must reproduce the above
22
- copyright notice, this list of conditions and the following
23
- disclaimer in the documentation and/or other materials provided
24
- with the distribution.
25
-
26
- * Neither the name of the d3graph nor the names of its
27
- contributors may be used to endorse or promote products derived
28
- from this software without specific prior written permission.
29
-
30
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
31
- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
32
- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
33
- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
34
- OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35
- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
36
- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
37
- DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
38
- THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
39
- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
40
- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
6
+ License-Expression: BSD-3-Clause
41
7
  Project-URL: Homepage, https://erdogant.github.io/d3graph
42
8
  Project-URL: Download, https://github.com/erdogant/d3graph/archive/{version}.tar.gz
43
- Keywords: Python,
9
+ Keywords: Python,network,graph,network analysis,visualization,interactive,d3js,d3graph,data visualization,web,modular,customizable
44
10
  Classifier: Programming Language :: Python :: 3
45
- Classifier: License :: OSI Approved :: BSD License
46
11
  Classifier: Operating System :: OS Independent
12
+ Classifier: Intended Audience :: Education
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Operating System :: Unix
15
+ Classifier: Operating System :: Microsoft :: Windows
16
+ Classifier: Operating System :: MacOS
47
17
  Requires-Python: >=3
48
18
  Description-Content-Type: text/markdown
49
19
  License-File: LICENSE
@@ -15,7 +15,7 @@ from d3graph.d3graph import (
15
15
 
16
16
  __author__ = 'Erdogan Tasksen'
17
17
  __email__ = 'erdogant@gmail.com'
18
- __version__ = '2.6.1'
18
+ __version__ = '2.7.0'
19
19
 
20
20
  # Setup root logger
21
21
  _logger = logging.getLogger('d3graph')
@@ -63,7 +63,6 @@ Examples
63
63
  References
64
64
  ----------
65
65
  * 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
66
  * Github : https://github.com/erdogant/d3graph
68
67
  * Documentation: https://erdogant.github.io/d3graph/
69
68
 
@@ -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
 
@@ -27,9 +27,6 @@ from packaging import version
27
27
  import datazets as dz
28
28
 
29
29
  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
30
 
34
31
  # %%
35
32
  class d3graph:
@@ -58,8 +55,7 @@ class d3graph:
58
55
 
59
56
  References
60
57
  ----------
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
58
+ * Medium: https://erdogant.medium.com/
63
59
  * Github : https://github.com/erdogant/d3graph
64
60
  * Documentation: https://erdogant.github.io/d3graph/
65
61
 
@@ -70,8 +66,17 @@ class d3graph:
70
66
  charge: int = 600,
71
67
  slider=None,
72
68
  support: str = 'text',
69
+ link_tension: float = 1,
70
+ sticky: bool = True,
73
71
  verbose: int = 20) -> None:
74
- """Initialize d3graph."""
72
+ """Initialize d3graph.
73
+
74
+ Parameters (additional to existing)
75
+ ------------------------------------
76
+ sticky : bool, (default: False)
77
+ When True, nodes stay fixed in place after being dragged.
78
+ Right-click a fixed node to release it back into the simulation.
79
+ """
75
80
  if slider is None: slider = [None, None]
76
81
  # Cleaning
77
82
  self._clean()
@@ -85,6 +90,8 @@ class d3graph:
85
90
  self.config['charge'] = -abs(charge)
86
91
  self.config['slider'] = slider
87
92
  self.config['support'] = get_support(support)
93
+ self.config['link_tension'] = link_tension
94
+ self.config['sticky'] = sticky
88
95
  # Set paths
89
96
  self.config['curpath'] = os.path.dirname(os.path.abspath(__file__))
90
97
  self.config['d3_library'] = os.path.abspath(os.path.join(self.config['curpath'], 'd3js/d3.v3.js'))
@@ -110,6 +117,8 @@ class d3graph:
110
117
  dark_mode = False,
111
118
  notebook: bool = False,
112
119
  save_button: bool = True,
120
+ link_tension: float = None,
121
+ sticky: bool = None,
113
122
  ) -> None:
114
123
  """Build and show the graph.
115
124
 
@@ -150,6 +159,11 @@ class d3graph:
150
159
  save_button : bool, (default: True)
151
160
  True: Save button is shown in the HTML to save the image in svg.
152
161
  False: No save button is shown in the HTML.
162
+ sticky : bool, (default: None)
163
+ When True, nodes stay fixed in place after being dragged (overrides the value set in __init__).
164
+ When False, nodes are released after dragging (default simulation behaviour).
165
+ When None, the value set in __init__ is used.
166
+ Right-click a fixed node to release it back into the simulation.
153
167
 
154
168
  Returns
155
169
  -------
@@ -171,6 +185,12 @@ class d3graph:
171
185
  self.config['save_button'] = save_button
172
186
  self.config['background_color'] = background_color
173
187
  self.config['dark_mode'] = dark_mode
188
+ # Allow show() to override the link_tension set at __init__ time
189
+ if link_tension is not None:
190
+ self.config['link_tension'] = link_tension
191
+ # Override sticky only when explicitly passed
192
+ if sticky is not None:
193
+ self.config['sticky'] = sticky
174
194
  # if self.config.get('filepath', None) != 'd3graph.html':
175
195
  self.config['filepath'] = self.set_path(filepath)
176
196
 
@@ -308,6 +328,10 @@ class d3graph:
308
328
  self.config['label'] = label
309
329
  self.config['label_color'] = label_color
310
330
  self.config['label_fontsize'] = label_fontsize
331
+
332
+ if not hasattr(self, 'adjmat'):
333
+ logger.error('adjmat is missing. Initialize first with d3 = d3graph(adjmat)')
334
+ return
311
335
 
312
336
  if (not directed) and (marker_end is not None) or (marker_start is not None):
313
337
  logger.info('Set directed=True to see the markers!')
@@ -447,6 +471,10 @@ class d3graph:
447
471
  'edge_color': edge_color of the node
448
472
 
449
473
  """
474
+ if not hasattr(self, 'adjmat'):
475
+ logger.error('adjmat is missing. Initialize first with d3 = d3graph(adjmat)')
476
+ return
477
+
450
478
  if minmax is None: minmax = [8, 13]
451
479
  node_names = self.adjmat.columns.astype(str)
452
480
  nodecount = self.adjmat.shape[0]
@@ -763,6 +791,8 @@ class d3graph:
763
791
  'max_slider': self.config['slider'][1],
764
792
  'directed': self.config['directed'],
765
793
  'collision': self.config['collision'],
794
+ 'link_tension': self.config.get('link_tension', 1.0),
795
+ 'sticky': self.config.get('sticky', False),
766
796
  'CLICK_COMMENT': CLICK_COMMENT,
767
797
  'CLICK_FILL': click_properties['fill'],
768
798
  'CLICK_STROKE': click_properties['stroke'],
@@ -823,10 +853,10 @@ class d3graph:
823
853
  dirname = os.path.join(gettempdir(), 'd3graph')
824
854
  else:
825
855
  dirname, filename = os.path.split(filepath)
826
-
856
+
827
857
  if filename in (None, ''):
828
858
  filename = 'd3graph.html'
829
-
859
+
830
860
  if dirname in (None, ''):
831
861
  dirname = os.path.join(gettempdir(), 'd3graph')
832
862
 
@@ -1009,6 +1039,7 @@ def json_create(G: nx.Graph) -> str:
1009
1039
  links[i]['label'] = links[i]['label']
1010
1040
  links[i]['label_color'] = links[i]['label_color']
1011
1041
  links[i]['label_fontsize'] = links[i]['label_fontsize']
1042
+ links[i]['tooltip'] = links[i]['tooltip']
1012
1043
  links_new.append(links[i])
1013
1044
 
1014
1045
  # Set node properties
@@ -1125,6 +1156,7 @@ def adjmat2dict(adjmat: pd.DataFrame,
1125
1156
  'label': Text label for the edge.
1126
1157
  'label_color': color of the label.
1127
1158
  'label_fontsize': fontsize of the label.
1159
+ 'tooltip': Text that is shown when hovering over the edge.
1128
1160
 
1129
1161
  """
1130
1162
  # Convert adjacency matrix into vector
@@ -1191,16 +1223,10 @@ def adjmat2dict(adjmat: pd.DataFrame,
1191
1223
  df['label_color']=label_color
1192
1224
  df['label_fontsize']=label_fontsize
1193
1225
  df['edge_style']=edge_style
1226
+ df['edge_color']=edge_color
1194
1227
  df['edge_opacity'] = edge_opacity
1228
+ df['tooltip'] = df['weight'].astype(str)
1195
1229
 
1196
- # Handle node_source edge color
1197
- if edge_color == 'node_source' and node_color_map is not None:
1198
- df['edge_color'] = df['source'].map(node_color_map)
1199
- elif edge_color == 'node_target' and node_color_map is not None:
1200
- df['edge_color'] = df['target'].map(node_color_map)
1201
- else:
1202
- df['edge_color'] = edge_color
1203
-
1204
1230
  # Creation dictionary
1205
1231
  source_target = list(zip(df['source'], df['target']))
1206
1232
  if len(source_target)==0:
@@ -1218,6 +1244,7 @@ def adjmat2dict(adjmat: pd.DataFrame,
1218
1244
  'label': df['label'].iloc[i],
1219
1245
  'label_color': df['label_color'].iloc[i],
1220
1246
  'label_fontsize': df['label_fontsize'].iloc[i],
1247
+ 'tooltip': df['tooltip'].iloc[i]
1221
1248
  } for
1222
1249
  i, edge in enumerate(source_target)}
1223
1250
 
@@ -1294,6 +1321,7 @@ def edges2G(edge_properties: dict, G: nx.Graph = None) -> nx.Graph:
1294
1321
  label=edge_properties[edge]['label'],
1295
1322
  label_color=edge_properties[edge]['label_color'],
1296
1323
  label_fontsize=edge_properties[edge]['label_fontsize'],
1324
+ tooltip=edge_properties[edge]['tooltip']
1297
1325
  )
1298
1326
  return G
1299
1327
 
@@ -1360,6 +1388,7 @@ def _normalize_size(getsizes, minscale=0.5, maxscale=4, scaler: str = 'zscore'):
1360
1388
  # Instead of Min-Max scaling, that shrinks any distribution in the [0, 1] interval, scaling the variables to
1361
1389
  # Z-scores is better. Min-Max Scaling is too sensitive to outlier observations and generates unseen problems.
1362
1390
 
1391
+ getsizes = getsizes.copy()
1363
1392
  # Set sizes to 0 if not available
1364
1393
  getsizes[np.isinf(getsizes)]=0
1365
1394
  getsizes[np.isnan(getsizes)]=0
@@ -1737,4 +1766,4 @@ def get_support(support):
1737
1766
  script = script + '\n' + "<div data-ea-publisher='erdogantgithubio' data-ea-type='{TYPE}' data-ea-style='stickybox'></div>".replace('{TYPE}', support)
1738
1767
 
1739
1768
  # Return
1740
- return script
1769
+ return script
@@ -1,4 +1,3 @@
1
-
2
1
  function d3graphscript(config = {
3
2
  // Default values
4
3
  width: 800,
@@ -7,6 +6,8 @@ function d3graphscript(config = {
7
6
  distance: 0,
8
7
  directed: false,
9
8
  collision: 0.5,
9
+ link_tension: 1,
10
+ sticky: false,
10
11
  background_color: '#FFFFFF'
11
12
  }) {
12
13
 
@@ -14,6 +15,7 @@ function d3graphscript(config = {
14
15
  var width = config.width;
15
16
  var height = config.height;
16
17
  var background_color = config.background_color || '#FFFFFF';
18
+ var sticky = config.sticky || false;
17
19
 
18
20
  // Set the body background color
19
21
  document.body.style.backgroundColor = background_color;
@@ -25,20 +27,42 @@ function d3graphscript(config = {
25
27
  .charge(config.charge)
26
28
  .linkDistance((d) => d.edge_distance || config.distance)
27
29
  //.linkDistance((d) => config.distance > 0 ? config.distance : d.edge_weight)
30
+ .linkStrength(config.link_tension !== undefined ? config.link_tension : 1)
28
31
  .size([width, height]);
29
32
 
30
- // DRAGGING START
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
+
31
39
  function dragstarted(d) {
32
40
  d3.event.sourceEvent.stopPropagation();
33
41
  d3.select(this).classed("dragging", true);
42
+ if (sticky) {
43
+ d.fixed = true;
44
+ force.start();
45
+ }
34
46
  }
35
47
 
36
48
  function dragged(d) {
37
- d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
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
+ }
38
55
  }
39
56
 
40
57
  function dragended(d) {
41
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
+ }
42
66
  }
43
67
 
44
68
  var drag = force.drag()
@@ -47,7 +71,7 @@ function d3graphscript(config = {
47
71
  .on("drag", dragged)
48
72
  .on("dragend", dragended);
49
73
 
50
- // DRAGGING STOP
74
+ // ---- END DRAGGING ----
51
75
 
52
76
  //Append a SVG to the body of the html page. Assign this SVG as an object to svg
53
77
  var svg = d3.select("body").append("svg")
@@ -58,8 +82,6 @@ function d3graphscript(config = {
58
82
  .on("dblclick.zoom", null)
59
83
  .append("g")
60
84
 
61
- //.on("dblclick", threshold); // EXPLODE ALL CONNECTED POINTS
62
-
63
85
  graphRec = JSON.parse(JSON.stringify(graph));
64
86
 
65
87
  //Creates the graph data structure out of the json data
@@ -67,31 +89,31 @@ function d3graphscript(config = {
67
89
  .links(graph.links)
68
90
  .start();
69
91
 
70
- // Create all the line svgs but without locations yet
92
+ // Create all the line svgs but without locations yet
71
93
  var link = svg.selectAll(".link")
72
94
  .data(graph.links)
73
95
  .enter().append("line")
74
96
  .attr("class", "link")
75
97
  .attr('marker-start', function(d){ return 'url(#marker_' + d.marker_start + ')' })
76
- .attr("marker-end", function(d) {
77
- if (config.directed) {return 'url(#marker_' + d.marker_end + ')' }})
98
+ .attr("marker-end", function(d) {
99
+ if (config.directed) {return 'url(#marker_' + d.marker_end + ')' }})
78
100
  .style("stroke-width", function(d) {return d.edge_width;}) // LINK-WIDTH
79
101
  .style("stroke", function(d) {return d.edge_color;}) // EDGE-COLORS
80
102
  .style("stroke-dasharray", function(d) {return d.edge_style;}) // EDGE-STYLE
81
103
  .style("opacity", function(d) {return d.edge_opacity;}) // EDGE-OPACITY
82
- // .style("stroke-width", 1); // WIDTH OF THE LINKS
83
104
  ;
84
105
 
106
+ link.append("title").text(function(d) { return d.tooltip; });
107
+
85
108
  // ADD TEXT ON THE EDGES (PART 1/2)
86
- var linkText = svg.selectAll(".link-text")
87
- .data(graph.links)
88
- .enter().append("text")
89
- .attr("class", "link-text")
90
- .attr("font-size", function(d) {return d.label_fontsize + "px";})
91
- .style("fill", function(d) {return d.label_color;})
92
- .style("font-family", "Arial")
93
- //.attr("transform", "rotate(90)")
94
- .text(function(d) { return d.label; });
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; });
95
117
 
96
118
  //Do the same with the circles for the nodes
97
119
  var node = svg.selectAll(".node")
@@ -101,26 +123,37 @@ function d3graphscript(config = {
101
123
  .call(drag)
102
124
  .on('dblclick', connectedNodes); // HIGHLIGHT ON/OFF
103
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
+
104
139
  {{ CLICK_COMMENT }} node.on('click', color_on_click); // ON CLICK HANDLER
105
140
 
106
141
 
107
142
  node.append("circle")
108
- .attr("r", function(d) { return d.node_size; }) // NODE SIZE
109
- .style("fill", function(d) {return d.node_color;}) // NODE-COLOR
110
- .style("opacity", function(d) {return d.node_opacity;}) // NODE-OPACITY
111
- .style("stroke-width", function(d) {return d.node_size_edge;}) // NODE-EDGE-SIZE
112
- .style("stroke", function(d) {return d.node_color_edge;}) // NODE-COLOR-EDGE
113
- // .style("stroke", '#000') // NODE-EDGE-COLOR (all black)
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
114
148
 
115
149
  // Text in nodes
116
150
  node.append("text")
117
151
  .attr("dx", 10)
118
152
  .attr("dy", ".35em")
119
- .text(function(d) {return d.node_name}) // NODE-TEXT
120
- .style("font-size", function(d) {return d.node_fontsize + "px";}) // set font size equal to node edge size
121
- .style("fill", function(d) {return d.node_fontcolor;}) // set the text fill color to the same as node color
122
- .style("font-family", "monospace");
123
- // .style("stroke", "gray");
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");
124
157
 
125
158
  let showInHover = ["node_tooltip"]; // Tooltip
126
159
  node.append("title")
@@ -140,10 +173,9 @@ function d3graphscript(config = {
140
173
  .attr("cy", function(d) { return d.y; });
141
174
  d3.selectAll("text").attr("x", function(d) { return d.x; })
142
175
  .attr("y", function(d) { return d.y; })
143
- linkText.attr("x", function(d) { return (d.source.x + d.target.x) / 2; }) // ADD TEXT ON THE EDGES (PART 2/2)
176
+ linkText.attr("x", function(d) { return (d.source.x + d.target.x) / 2; }) // ADD TEXT ON THE EDGES (PART 2/2)
144
177
  .attr("y", function(d) { return (d.source.y + d.target.y) / 2; })
145
178
  .attr("text-anchor", "middle");
146
- ;
147
179
 
148
180
  node.each(collide(config.collision)); //COLLISION DETECTION. High means a big fight to get untouchable nodes (default=0.5)
149
181
 
@@ -158,8 +190,6 @@ function d3graphscript(config = {
158
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' }
159
191
  ]
160
192
 
161
- //console.log(JSON.stringify(link))
162
-
163
193
  svg.append("defs").selectAll("marker")
164
194
  .data(data_marker)
165
195
  .enter()
@@ -167,17 +197,13 @@ function d3graphscript(config = {
167
197
  .attr('id', function(d){ return 'marker_' + d.name})
168
198
  .attr('markerHeight', 10)
169
199
  .attr('markerWidth', 10)
170
- //.attr('markerUnits', 'strokeWidth')
171
200
  .attr("markerUnits", "userSpaceOnUse") // Fix marker width
172
201
  .attr('orient', 'auto')
173
- //.attr('refX', -15) // Offset marker-start
174
202
  .attr('refX', 15) // Offset marker-end
175
203
  .attr('refY', 0)
176
204
  .attr('viewBox', function(d){ return d.viewbox })
177
205
  .append('svg:path')
178
- //.attr("transform", "rotate(180)") // Marker-start mirrored
179
206
  .attr('d', function(d){ return d.path }) // Marker type
180
- //.style("fill", function(d) {return d.marker_color;}) // Marker color
181
207
  .style("fill", '#808080') // Marker color
182
208
  .style("stroke", '#808080') // Marker edge-color
183
209
  .style("opacity", 0.95) // Marker opacity
@@ -219,7 +245,7 @@ function d3graphscript(config = {
219
245
  // collision detection end
220
246
 
221
247
 
222
- //Toggle stores whether the highlighting is on **********************
248
+ //Toggle stores whether the highlighting is on
223
249
  var toggle = 0;
224
250
  //Create an array logging what is connected to what
225
251
  var linkedByIndex = {};
@@ -235,25 +261,27 @@ function d3graphscript(config = {
235
261
  }
236
262
 
237
263
 
238
- // COLOR ON CLICK
239
- function color_on_click() {
240
- // Give the original color back
241
- d3.selectAll(".node")
242
- .select("circle")
243
- .style("fill", function(d) {return d.node_color;})
244
- .style("opacity", function(d) {return d.node_opacity;})
245
- .style("stroke", function(d) {return d.node_color_edge;})
246
- .style("stroke-width", function(d) {return d.edge_width;})
247
- .attr("r", function(d) { return d.node_size; })
248
- ;
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
+ ;
249
277
 
250
- // Set the color on click
251
- d3.select(this).select("circle")
252
- .style("fill", {{ CLICK_FILL }})
253
- .style("stroke", "{{ CLICK_STROKE }}")
254
- .style("stroke-width", {{ CLICK_STROKEW }})
255
- .attr("r", function(d) { return d.node_size*{{ CLICK_SIZE }}; })
256
- ;}
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
+ ;}
257
285
 
258
286
 
259
287
 
@@ -267,7 +295,6 @@ function d3graphscript(config = {
267
295
  link.style("opacity", function(o) {
268
296
  return d.index == o.source.index | d.index == o.target.index ? 1 : 0.1;
269
297
  });
270
- //Reduce the op
271
298
  toggle = 1;
272
299
  } else {
273
300
  //Put them back to opacity=1
@@ -277,14 +304,12 @@ function d3graphscript(config = {
277
304
  toggle = 0;
278
305
  }
279
306
  }
280
- //*************************************************************
281
307
 
282
308
 
283
309
  //adjust threshold
284
310
  function threshold() {
285
311
  let thresh = this.value;
286
312
 
287
- // console.log('Setting threshold', thresh)
288
313
  graph.links.splice(0, graph.links.length);
289
314
  linkText = linkText.data([]); // CLEAR EDGE-LABELS: Clear the linkText selection
290
315
  linkText.exit().remove(); // CLEAR EDGE-LABELS: Clear the linkText elements from the DOM
@@ -312,12 +337,11 @@ function d3graphscript(config = {
312
337
  link.exit().remove();
313
338
  link.enter().insert("line", ".node").attr("class", "link");
314
339
  link.style("stroke-width", function(d) {return d.edge_width;}); // LINK-WIDTH AFTER BREAKING WITH SLIDER
315
- //link.style('marker-start', function(d){ return 'url(#marker_' + d.marker_start + ')' })
316
- link.style("marker-end", function(d) { // Include the markers.
317
- if (config.directed) {return 'url(#marker_' + d.marker_end + ')' }})
318
- link.style("stroke", function(d) {return d.edge_color;}); // EDGE-COLOR AFTER BREAKING WITH SLIDER
319
- link.style("stroke-dasharray", function(d) {return d.edge_style;}) // EDGE-STYLE
320
- link.style("opacity", function(d) {return d.edge_opacity;}); // EDGE-OPACITY 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
321
345
 
322
346
  // Update EDGE-LABELS
323
347
  linkText = linkText.data(graph.links);
@@ -334,4 +358,4 @@ function d3graphscript(config = {
334
358
  force.start();
335
359
  }
336
360
 
337
- }
361
+ }
@@ -126,6 +126,8 @@
126
126
  distance: {{ edge_distance }},
127
127
  directed: {{ directed | lower }},
128
128
  collision: {{ collision }},
129
+ link_tension: {{ link_tension }},
130
+ sticky: {{ sticky | lower }},
129
131
  background_color: '{{ background_color }}'
130
132
  })
131
133
  });
@@ -16,4 +16,10 @@ h3 {
16
16
  font-style: italic;
17
17
  font-size: 14px;
18
18
  font-family: "Helvetica";
19
+ }
20
+
21
+ /* Sticky / fixed node indicator: dashed outline ring */
22
+ circle.node-fixed {
23
+ stroke-dasharray: 4, 2;
24
+ stroke-width: 2.5px;
19
25
  }
@@ -1,6 +1,108 @@
1
1
  # Import library
2
2
  from d3graph import d3graph, vec2adjmat
3
3
 
4
+ # Initialize with default settings
5
+ d3 = d3graph(link_tension=0.2)
6
+
7
+ # Load example data
8
+ df = d3.import_example('stormofswords')
9
+ adjmat = vec2adjmat(source=df['source'], target=df['target'], weight=df['weight'])
10
+
11
+
12
+ # sticky=True (default) — drag to pin, right-click to release
13
+ d3 = d3graph(adjmat, sticky=True)
14
+ d3.show()
15
+
16
+ # sticky=False — classic spring-back behaviour
17
+ d3 = d3graph(adjmat, sticky=False)
18
+ d3.show()
19
+
20
+ # Can also be overridden per-render:
21
+ d3.show(adjmat, sticky=False)
22
+ d3.show()
23
+
24
+
25
+ # %%
26
+
27
+
28
+ # Import library
29
+ from d3graph import d3graph, vec2adjmat
30
+
31
+ # Initialize with default settings
32
+ d3 = d3graph(sticky=True, link_tension=0.2, collision=1)
33
+
34
+ # Load example data
35
+ df = d3.import_example('stormofswords')
36
+
37
+ # Convert df to adjmat
38
+ adjmat = vec2adjmat(source=df['source'], target=df['target'], weight=df['weight'])
39
+ # adjmat = np.exp(adjmat)
40
+ # adjmat = adjmat-1
41
+
42
+ # Create the network
43
+ d3.graph(adjmat)
44
+
45
+ # Set Node properties
46
+ d3.set_node_properties(edge_color='#000000', cmap='Blues', minmax=[5, 13], fontcolor='#808080')
47
+ # Show the graph
48
+ # d3.show()
49
+
50
+ # d3.set_edge_properties(directed=False, marker_end='arrow', edge_color='#ff0000', edge_opacity=[0.1, 0.2])
51
+
52
+ # Set edge properties
53
+ d3.set_edge_properties(directed=True, marker_end='arrow')
54
+ d3.show()
55
+
56
+
57
+
58
+
59
+ # Import library
60
+ # from d3graph import d3graph, vec2adjmat
61
+
62
+ # Initialize with default settings
63
+ # d3 = d3graph(support=None)
64
+
65
+ # Load example data
66
+ # df = d3.import_example('stormofswords')
67
+
68
+ # Convert df to adjmat
69
+ # adjmat = vec2adjmat(source=df['source'], target=df['target'], weight=df['weight'])
70
+ # adjmat = np.exp(adjmat)
71
+ # adjmat = adjmat-1
72
+
73
+ # Create the network
74
+ # d3.graph(adjmat)
75
+
76
+ # # Set Node properties
77
+ # # d3.set_node_properties(edge_color='#000000', cmap='Blues', minmax=[5, 13], fontcolor='#808080')
78
+ # d3.set_edge_properties(edge_color='#FF0000')
79
+
80
+ # # Show the graph
81
+ # d3.show()
82
+
83
+ # # %%
84
+
85
+
86
+ # %% opacity
87
+ from d3graph import d3graph, adjmat2vec, vec2adjmat
88
+ # intialize to load example dataset
89
+ d3 = d3graph()
90
+ #
91
+ df = d3.import_example(data='energy')
92
+ df=vec2adjmat(source=df['source'], target=df['target'], weight=df['weight'])
93
+ # d3.graph(df, size='degree', opacity='degree', color='cluster', scaler='zscore')
94
+ d3.graph(df)
95
+
96
+ # d3.set_node_properties(opacity='centrality')
97
+ d3.show(filepath='c://temp/network.html')
98
+
99
+
100
+ # %%
101
+
102
+
103
+ # Import library
104
+ from d3graph import d3graph, vec2adjmat
105
+
4
106
  # Initialize with default settings
5
107
  d3 = d3graph(support=None)
6
108
 
@@ -74,7 +176,7 @@ df_edges_c['isin']=0
74
176
 
75
177
  for index, edge in df_edges.iterrows():
76
178
  Iloc = np.sum(edge==df_edges_c[df_edges.columns], axis=1)==len(df_edges.columns)
77
- df_edges_c['isin'][Iloc]=1
179
+ df_edges_c.loc[Iloc, 'isin'] = 1
78
180
 
79
181
  assert df_edges_c['isin'].sum()==df_edges.shape[0]
80
182
 
@@ -162,19 +264,6 @@ d3.set_node_properties(marker=['circle', 'circle', 'circle', 'rect', 'rect', 're
162
264
  d3.show(filepath=r'c:\temp\\d3graph\circle.html', set_slider=5, save_button=False)
163
265
 
164
266
 
165
- # %% opacity
166
- from d3graph import d3graph, adjmat2vec
167
- # intialize to load example dataset
168
- d3 = d3graph()
169
- #
170
- df = d3.import_example(data='energy')
171
- df=vec2adjmat(source=df['source'], target=df['target'], weight=df['weight'])
172
- # d3.graph(df, size='degree', opacity='degree', color='cluster', scaler='zscore')
173
- d3.graph(df)
174
-
175
- # d3.set_node_properties(opacity='centrality')
176
- d3.show(filepath='c://temp/network.html')
177
-
178
267
 
179
268
  # %% Edge distance
180
269
 
@@ -1,49 +1,19 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: d3graph
3
- Version: 2.6.1
3
+ Version: 2.7.0
4
4
  Summary: Python package to create interactive network based on d3js.
5
5
  Author-email: Erdogan Taskesen <erdogant@gmail.com>
6
- License: d3graph is distributed with the 3-clause BSD license.
7
-
8
- ::
9
-
10
- Copyright (C) 2000-2022,
11
- Erdogan Taskesen <erdogant@gmail.com>
12
- All rights reserved.
13
-
14
- Redistribution and use in source and binary forms, with or without
15
- modification, are permitted provided that the following conditions are
16
- met:
17
-
18
- * Redistributions of source code must retain the above copyright
19
- notice, this list of conditions and the following disclaimer.
20
-
21
- * Redistributions in binary form must reproduce the above
22
- copyright notice, this list of conditions and the following
23
- disclaimer in the documentation and/or other materials provided
24
- with the distribution.
25
-
26
- * Neither the name of the d3graph nor the names of its
27
- contributors may be used to endorse or promote products derived
28
- from this software without specific prior written permission.
29
-
30
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
31
- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
32
- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
33
- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
34
- OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35
- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
36
- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
37
- DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
38
- THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
39
- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
40
- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
6
+ License-Expression: BSD-3-Clause
41
7
  Project-URL: Homepage, https://erdogant.github.io/d3graph
42
8
  Project-URL: Download, https://github.com/erdogant/d3graph/archive/{version}.tar.gz
43
- Keywords: Python,
9
+ Keywords: Python,network,graph,network analysis,visualization,interactive,d3js,d3graph,data visualization,web,modular,customizable
44
10
  Classifier: Programming Language :: Python :: 3
45
- Classifier: License :: OSI Approved :: BSD License
46
11
  Classifier: Operating System :: OS Independent
12
+ Classifier: Intended Audience :: Education
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Operating System :: Unix
15
+ Classifier: Operating System :: Microsoft :: Windows
16
+ Classifier: Operating System :: MacOS
47
17
  Requires-Python: >=3
48
18
  Description-Content-Type: text/markdown
49
19
  License-File: LICENSE
@@ -2,7 +2,6 @@ LICENSE
2
2
  MANIFEST.in
3
3
  README.md
4
4
  pyproject.toml
5
- setup.cfg
6
5
  d3graph/__init__.py
7
6
  d3graph/d3graph.py
8
7
  d3graph/examples.py
@@ -15,5 +14,4 @@ d3graph.egg-info/top_level.txt
15
14
  d3graph/d3js/d3.v3.js
16
15
  d3graph/d3js/d3graphscript.js
17
16
  d3graph/d3js/index.html.j2
18
- d3graph/d3js/style.css
19
- tests/test_d3graph.py
17
+ d3graph/d3js/style.css
@@ -9,12 +9,29 @@ authors = [{ name = "Erdogan Taskesen", email = "erdogant@gmail.com" },]
9
9
  description = "Python package to create interactive network based on d3js."
10
10
  readme = "README.md"
11
11
  requires-python = ">=3"
12
- license = { file = "LICENSE" }
13
- keywords = ["Python", ""]
12
+ license = "BSD-3-Clause"
13
+ keywords = [
14
+ "Python",
15
+ "network",
16
+ "graph",
17
+ "network analysis",
18
+ "visualization",
19
+ "interactive",
20
+ "d3js",
21
+ "d3graph",
22
+ "data visualization",
23
+ "web",
24
+ "modular",
25
+ "customizable"
26
+ ]
14
27
  classifiers = [
15
28
  "Programming Language :: Python :: 3",
16
- "License :: OSI Approved :: BSD License",
17
29
  "Operating System :: OS Independent",
30
+ "Intended Audience :: Education",
31
+ "Intended Audience :: Science/Research",
32
+ "Operating System :: Unix",
33
+ "Operating System :: Microsoft :: Windows",
34
+ "Operating System :: MacOS",
18
35
  ]
19
36
  dependencies = [
20
37
  'pandas',
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
d3graph-2.6.1/setup.cfg DELETED
@@ -1,10 +0,0 @@
1
- [metadata]
2
- name = d3graph
3
-
4
- [options]
5
- packages = find:
6
-
7
- [egg_info]
8
- tag_build =
9
- tag_date = 0
10
-
@@ -1,39 +0,0 @@
1
- from copy import deepcopy
2
- import numpy as np
3
- import pandas as pd
4
- from d3graph import d3graph, adjmat2vec, vec2adjmat
5
-
6
-
7
- def test_instantiate_d3graph_no_args() -> None:
8
- """Test instantiation works with defaults"""
9
- d3 = d3graph()
10
- assert isinstance(d3, type(d3graph()))
11
-
12
-
13
- def test_clean(d3, helpers) -> None:
14
- """Test _clean method deletes the attributes in clean_fields"""
15
- clean_fields: tuple = ('adjmat', 'config', 'edge_properties', 'G', 'node_properties')
16
-
17
- # Set attrs to dummy value (i.e., 0) and assert they exist in the object
18
- original_attrs = {field: 0 for field in clean_fields}
19
-
20
- # Convert the array to a DataFrame for comparison
21
- def test_adjmat2vec_and_back() -> None:
22
- edges = np.array([('Cloudy', 'Sprinkler'), ('Cloudy', 'Rain'), ('Sprinkler', 'Wet_Grass'), ('Rain', 'Wet_Grass')])
23
- weight = [1,2,3,4]
24
- df_edges = pd.DataFrame(edges, columns=['source', 'target'])
25
- df_edges['weight']=weight
26
-
27
- df_edges_c = adjmat2vec(vec2adjmat(source=df_edges['source'], target=df_edges['target'], weight=df_edges['weight']))
28
- adjmat = vec2adjmat(source=df_edges_c['source'], target=df_edges_c['target'], weight=df_edges_c['weight'])
29
- df_edges_c = adjmat2vec(adjmat)
30
-
31
- df_edges_c = df_edges_c.sort_values(by='weight')
32
- df_edges_c['isin']=0
33
-
34
- for index, edge in df_edges.iterrows():
35
- Iloc = np.sum(edge==df_edges_c[df_edges.columns], axis=1)==len(df_edges.columns)
36
- df_edges_c['isin'][Iloc]=1
37
-
38
- assert df_edges_c['isin'].sum()==df_edges.shape[0]
39
-
File without changes
File without changes
File without changes
File without changes