d3graph 2.7.2__tar.gz → 2.8.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {d3graph-2.7.2/d3graph.egg-info → d3graph-2.8.1}/PKG-INFO +1 -1
- {d3graph-2.7.2 → d3graph-2.8.1}/d3graph/__init__.py +2 -2
- {d3graph-2.7.2 → d3graph-2.8.1}/d3graph/d3graph.py +102 -22
- {d3graph-2.7.2 → d3graph-2.8.1}/d3graph/d3js/d3graphscript.js +210 -31
- {d3graph-2.7.2 → d3graph-2.8.1}/d3graph/d3js/index.html.j2 +2 -1
- {d3graph-2.7.2 → d3graph-2.8.1}/d3graph/d3js/style.css +7 -2
- {d3graph-2.7.2 → d3graph-2.8.1}/d3graph/examples.py +29 -5
- {d3graph-2.7.2 → d3graph-2.8.1/d3graph.egg-info}/PKG-INFO +1 -1
- {d3graph-2.7.2 → d3graph-2.8.1}/LICENSE +0 -0
- {d3graph-2.7.2 → d3graph-2.8.1}/MANIFEST.in +0 -0
- {d3graph-2.7.2 → d3graph-2.8.1}/README.md +0 -0
- {d3graph-2.7.2 → d3graph-2.8.1}/d3graph/d3js/d3.v3.js +0 -0
- {d3graph-2.7.2 → d3graph-2.8.1}/d3graph/examples_docs.py +0 -0
- {d3graph-2.7.2 → d3graph-2.8.1}/d3graph.egg-info/SOURCES.txt +0 -0
- {d3graph-2.7.2 → d3graph-2.8.1}/d3graph.egg-info/dependency_links.txt +0 -0
- {d3graph-2.7.2 → d3graph-2.8.1}/d3graph.egg-info/requires.txt +0 -0
- {d3graph-2.7.2 → d3graph-2.8.1}/d3graph.egg-info/top_level.txt +0 -0
- {d3graph-2.7.2 → d3graph-2.8.1}/pyproject.toml +0 -0
- {d3graph-2.7.2 → d3graph-2.8.1}/setup.cfg +0 -0
|
@@ -17,7 +17,7 @@ from d3graph.d3graph import (
|
|
|
17
17
|
|
|
18
18
|
__author__ = 'Erdogan Tasksen'
|
|
19
19
|
__email__ = 'erdogant@gmail.com'
|
|
20
|
-
__version__ = '2.
|
|
20
|
+
__version__ = '2.8.1'
|
|
21
21
|
|
|
22
22
|
# Setup root logger
|
|
23
23
|
_logger = logging.getLogger('d3graph')
|
|
@@ -60,7 +60,7 @@ Examples
|
|
|
60
60
|
>>> d3.set_edge_properties(directed=True)
|
|
61
61
|
>>>
|
|
62
62
|
>>> # Plot
|
|
63
|
-
>>> d3.show()
|
|
63
|
+
>>> d3.show(node_text_inside=True)
|
|
64
64
|
|
|
65
65
|
References
|
|
66
66
|
----------
|
|
@@ -120,6 +120,7 @@ class d3graph:
|
|
|
120
120
|
save_button: bool = True,
|
|
121
121
|
link_tension: float = None,
|
|
122
122
|
sticky: bool = None,
|
|
123
|
+
node_text_inside: bool = False,
|
|
123
124
|
) -> None:
|
|
124
125
|
"""Build and show the graph.
|
|
125
126
|
|
|
@@ -186,6 +187,8 @@ class d3graph:
|
|
|
186
187
|
self.config['save_button'] = save_button
|
|
187
188
|
self.config['background_color'] = background_color
|
|
188
189
|
self.config['dark_mode'] = dark_mode
|
|
190
|
+
self.config['node_text_inside'] = node_text_inside
|
|
191
|
+
|
|
189
192
|
# Allow show() to override the link_tension set at __init__ time
|
|
190
193
|
if link_tension is not None:
|
|
191
194
|
self.config['link_tension'] = link_tension
|
|
@@ -396,7 +399,8 @@ class d3graph:
|
|
|
396
399
|
* ['label 1','label 2','label 3', ...]
|
|
397
400
|
marker : list of markers (default: 'circle')
|
|
398
401
|
The marker that is used for the Node.
|
|
399
|
-
*
|
|
402
|
+
* 'circle' or 'hexagon' etc
|
|
403
|
+
* ['circle', 'star', 'diamond', 'square', 'pentagon', 'hexagon', 'rectangle', 'triangle-down', 'triangle']
|
|
400
404
|
tooltip : list of names (default: None)
|
|
401
405
|
The text that is shown when hovering over the Node.
|
|
402
406
|
If not specified, the text will inherit from the label.
|
|
@@ -795,6 +799,7 @@ class d3graph:
|
|
|
795
799
|
'collision': self.config['collision'],
|
|
796
800
|
'link_tension': self.config.get('link_tension', 1.0),
|
|
797
801
|
'sticky': self.config.get('sticky', False),
|
|
802
|
+
'node_text_inside': self.config.get('node_text_inside', False),
|
|
798
803
|
'CLICK_COMMENT': CLICK_COMMENT,
|
|
799
804
|
'CLICK_FILL': click_properties['fill'],
|
|
800
805
|
'CLICK_STROKE': click_properties['stroke'],
|
|
@@ -1411,7 +1416,7 @@ def library_compatibility_checks() -> None:
|
|
|
1411
1416
|
None.
|
|
1412
1417
|
|
|
1413
1418
|
"""
|
|
1414
|
-
if not version.parse(nx.__version__) >= version.parse('2.5'):
|
|
1419
|
+
if not version.parse(nx.__version__) >= version.parse('2.5.0'):
|
|
1415
1420
|
logger.error('Networkx version should be >= 2.5')
|
|
1416
1421
|
logger.info('Hint: pip install -U networkx')
|
|
1417
1422
|
|
|
@@ -1597,20 +1602,52 @@ def adjmat2vec(adjmat, min_weight: float = 1.0) -> pd.DataFrame:
|
|
|
1597
1602
|
return adjmat
|
|
1598
1603
|
|
|
1599
1604
|
|
|
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
|
|
1620
|
+
|
|
1600
1621
|
def _check_hex_color(color, n=None, cmap='Set1'):
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
if isinstance(color,
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1622
|
+
seq_types = (list, np.ndarray, pd.Series, StringArray)
|
|
1623
|
+
|
|
1624
|
+
# Single string
|
|
1625
|
+
if isinstance(color, str):
|
|
1626
|
+
if len(color) != 7 or not color.startswith('#'):
|
|
1627
|
+
logger.warning('Invalid [color] format. Expected "#000000" <auto-fixing>')
|
|
1628
|
+
return get_hex_color(color, cmap=cmap)[0]
|
|
1629
|
+
return color
|
|
1630
|
+
|
|
1631
|
+
# Sequence input
|
|
1632
|
+
if isinstance(color, seq_types):
|
|
1633
|
+
|
|
1634
|
+
if len(color) == 0:
|
|
1635
|
+
logger.warning('Invalid [color] sequence. Expected ["#000000", ...] <auto-fixing>')
|
|
1636
|
+
return get_hex_color(color, cmap=cmap)[0]
|
|
1637
|
+
|
|
1638
|
+
valid = np.all([
|
|
1639
|
+
isinstance(x, str) and len(x) == 7 and x.startswith('#')
|
|
1640
|
+
for x in color
|
|
1641
|
+
])
|
|
1642
|
+
|
|
1643
|
+
if not valid:
|
|
1644
|
+
logger.warning('Invalid hex colors in [color]. Expected ["#000000", ...] <auto-fixing>')
|
|
1645
|
+
return get_hex_color(color, cmap=cmap)[0]
|
|
1646
|
+
|
|
1647
|
+
if (n is not None) and (len(color) != n):
|
|
1648
|
+
logger.warning(f'Invalid [color] length. Expected length={n} <auto-fixing>')
|
|
1649
|
+
return get_hex_color(color, cmap=cmap)[0]
|
|
1650
|
+
|
|
1614
1651
|
return color
|
|
1615
1652
|
|
|
1616
1653
|
|
|
@@ -1653,20 +1690,63 @@ def _compute_centrality(adjmat):
|
|
|
1653
1690
|
# opacity = opacity[:-2]
|
|
1654
1691
|
return opacity
|
|
1655
1692
|
|
|
1693
|
+
MARKERS_SUPPORTED = {
|
|
1694
|
+
'circle',
|
|
1695
|
+
'ellipse',
|
|
1696
|
+
'rect', 'rectangle', 'square',
|
|
1697
|
+
'rect-wide', 'rectangle-wide',
|
|
1698
|
+
'triangle', 'triangle-up', 'triangle-down',
|
|
1699
|
+
'diamond',
|
|
1700
|
+
'star',
|
|
1701
|
+
'hexagon',
|
|
1702
|
+
'pentagon',
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
|
|
1656
1706
|
def _set_marker(self, marker, nodecount):
|
|
1657
|
-
|
|
1707
|
+
"""Validate and broadcast the node marker/shape.
|
|
1708
|
+
|
|
1709
|
+
Supported shapes
|
|
1710
|
+
----------------
|
|
1711
|
+
'circle' : Circle (default)
|
|
1712
|
+
'ellipse' : Ellipse (wider than tall)
|
|
1713
|
+
'rect' / 'rectangle' / 'square' : Square
|
|
1714
|
+
'rect-wide' / 'rectangle-wide' : Wide rectangle
|
|
1715
|
+
'triangle' / 'triangle-up' : Triangle pointing up
|
|
1716
|
+
'triangle-down' : Triangle pointing down
|
|
1717
|
+
'diamond' : Diamond (rotated square)
|
|
1718
|
+
'star' : 5-pointed star
|
|
1719
|
+
'hexagon' : Regular hexagon
|
|
1720
|
+
'pentagon' : Regular pentagon
|
|
1721
|
+
"""
|
|
1722
|
+
if marker is None:
|
|
1658
1723
|
marker = 'circle'
|
|
1659
1724
|
|
|
1660
|
-
if isinstance(marker, list) and len(marker)==nodecount:
|
|
1661
|
-
marker = np.array(marker)
|
|
1662
|
-
|
|
1725
|
+
if isinstance(marker, (list, np.ndarray, pd.Series)) and len(marker) == nodecount:
|
|
1726
|
+
marker = np.array([str(m).lower() for m in marker])
|
|
1727
|
+
invalid = set(marker) - MARKERS_SUPPORTED
|
|
1728
|
+
if invalid:
|
|
1729
|
+
logger.warning(
|
|
1730
|
+
'Unknown marker(s) %s. Supported: %s. Falling back to "circle" for those.',
|
|
1731
|
+
sorted(invalid), sorted(MARKERS_SUPPORTED)
|
|
1732
|
+
)
|
|
1733
|
+
marker = np.where(np.isin(marker, list(MARKERS_SUPPORTED)), marker, 'circle')
|
|
1734
|
+
elif 'numpy' in str(type(marker)) and len(marker) == nodecount:
|
|
1663
1735
|
pass
|
|
1664
1736
|
elif isinstance(marker, str):
|
|
1737
|
+
marker = marker.lower()
|
|
1738
|
+
if marker not in MARKERS_SUPPORTED:
|
|
1739
|
+
logger.warning(
|
|
1740
|
+
'Unknown marker "%s". Supported: %s. Falling back to "circle".',
|
|
1741
|
+
marker, sorted(MARKERS_SUPPORTED)
|
|
1742
|
+
)
|
|
1743
|
+
marker = 'circle'
|
|
1665
1744
|
marker = np.repeat(marker, nodecount)
|
|
1745
|
+
else:
|
|
1746
|
+
marker = np.repeat('circle', nodecount)
|
|
1666
1747
|
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
# Return
|
|
1748
|
+
if len(marker) != nodecount:
|
|
1749
|
+
raise ValueError("Node marker must be of same length as the number of nodes")
|
|
1670
1750
|
return marker
|
|
1671
1751
|
|
|
1672
1752
|
def _set_node_size(self, size, minmax, nodecount):
|
|
@@ -8,7 +8,8 @@ function d3graphscript(config = {
|
|
|
8
8
|
collision: 0.5,
|
|
9
9
|
link_tension: 1,
|
|
10
10
|
sticky: false,
|
|
11
|
-
background_color: '#FFFFFF'
|
|
11
|
+
background_color: '#FFFFFF',
|
|
12
|
+
node_text_inside: false,
|
|
12
13
|
}) {
|
|
13
14
|
|
|
14
15
|
//Constants for the SVG
|
|
@@ -59,7 +60,7 @@ function d3graphscript(config = {
|
|
|
59
60
|
if (sticky) {
|
|
60
61
|
// Keep the node fixed and apply a visual "pinned" cue (dashed border)
|
|
61
62
|
d.fixed = true;
|
|
62
|
-
d3.select(this).select("
|
|
63
|
+
d3.select(this).select(".node-shape")
|
|
63
64
|
.style("stroke-dasharray", "4,2")
|
|
64
65
|
.style("stroke-width", function(d) { return Math.max(parseFloat(d.node_size_edge) || 1, 2); });
|
|
65
66
|
}
|
|
@@ -72,7 +73,142 @@ function d3graphscript(config = {
|
|
|
72
73
|
.on("dragend", dragended);
|
|
73
74
|
|
|
74
75
|
// ---- END DRAGGING ----
|
|
75
|
-
|
|
76
|
+
|
|
77
|
+
// =====================================================================
|
|
78
|
+
// ---- SHAPE RENDERING ------------------------------------------------
|
|
79
|
+
// Supported node_marker values:
|
|
80
|
+
// 'circle' – SVG <circle> (default)
|
|
81
|
+
// 'ellipse' – SVG <ellipse> (wider than tall)
|
|
82
|
+
// 'square' – square via <path>
|
|
83
|
+
// 'rectangle' – wide rectangle via <path>
|
|
84
|
+
// 'triangle' / 'triangle-up' – upward equilateral triangle
|
|
85
|
+
// 'triangle-down' – downward triangle
|
|
86
|
+
// 'diamond' – rotated square
|
|
87
|
+
// 'star' – 5-pointed star
|
|
88
|
+
// 'hexagon' – regular hexagon
|
|
89
|
+
// 'pentagon' – regular pentagon
|
|
90
|
+
// =====================================================================
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Returns an SVG path `d` string for polygon/star shapes, centred at (0,0).
|
|
94
|
+
* Returns null for 'circle' and 'ellipse' (handled as native SVG elements).
|
|
95
|
+
*/
|
|
96
|
+
function shapePathD(marker, r) {
|
|
97
|
+
r = parseFloat(r) || 8;
|
|
98
|
+
var m = (marker || 'circle').toLowerCase();
|
|
99
|
+
switch (m) {
|
|
100
|
+
|
|
101
|
+
case 'triangle':
|
|
102
|
+
case 'triangle-up': {
|
|
103
|
+
var h = r * Math.sqrt(3);
|
|
104
|
+
return 'M 0,' + (-r) +
|
|
105
|
+
' L ' + (h / 2) + ',' + (r / 2) +
|
|
106
|
+
' L ' + (-h / 2) + ',' + (r / 2) + ' Z';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
case 'triangle-down': {
|
|
110
|
+
var h = r * Math.sqrt(3);
|
|
111
|
+
return 'M 0,' + r +
|
|
112
|
+
' L ' + (h / 2) + ',' + (-r / 2) +
|
|
113
|
+
' L ' + (-h / 2) + ',' + (-r / 2) + ' Z';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
case 'square': {
|
|
117
|
+
var s = r * 1.4;
|
|
118
|
+
return 'M ' + (-s) + ',' + (-s) +
|
|
119
|
+
' L ' + s + ',' + (-s) +
|
|
120
|
+
' L ' + s + ',' + s +
|
|
121
|
+
' L ' + (-s) + ',' + s + ' Z';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
case 'rect':
|
|
125
|
+
case 'rectangle': {
|
|
126
|
+
var w = r * 2.0, h = r * 1.0;
|
|
127
|
+
return 'M ' + (-w) + ',' + (-h) +
|
|
128
|
+
' L ' + w + ',' + (-h) +
|
|
129
|
+
' L ' + w + ',' + h +
|
|
130
|
+
' L ' + (-w) + ',' + h + ' Z';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
case 'diamond': {
|
|
134
|
+
var rx = r * 1.0, ry = r * 1.4;
|
|
135
|
+
return 'M 0,' + (-ry) +
|
|
136
|
+
' L ' + rx + ',0' +
|
|
137
|
+
' L 0,' + ry +
|
|
138
|
+
' L ' + (-rx) + ',0 Z';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
case 'star': {
|
|
142
|
+
var outerR = r * 1.3, innerR = r * 0.5, pts = [];
|
|
143
|
+
for (var i = 0; i < 10; i++) {
|
|
144
|
+
var angle = (Math.PI / 5) * i - Math.PI / 2;
|
|
145
|
+
var rad = (i % 2 === 0) ? outerR : innerR;
|
|
146
|
+
pts.push(+(rad * Math.cos(angle)).toFixed(3) + ',' + +(rad * Math.sin(angle)).toFixed(3));
|
|
147
|
+
}
|
|
148
|
+
return 'M ' + pts.join(' L ') + ' Z';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
case 'hexagon': {
|
|
152
|
+
var pts = [];
|
|
153
|
+
for (var i = 0; i < 6; i++) {
|
|
154
|
+
var angle = (Math.PI / 3) * i - Math.PI / 6;
|
|
155
|
+
pts.push(+(r * 1.1 * Math.cos(angle)).toFixed(3) + ',' + +(r * 1.1 * Math.sin(angle)).toFixed(3));
|
|
156
|
+
}
|
|
157
|
+
return 'M ' + pts.join(' L ') + ' Z';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
case 'pentagon': {
|
|
161
|
+
var pts = [];
|
|
162
|
+
for (var i = 0; i < 5; i++) {
|
|
163
|
+
var angle = (2 * Math.PI / 5) * i - Math.PI / 2;
|
|
164
|
+
pts.push(+(r * 1.15 * Math.cos(angle)).toFixed(3) + ',' + +(r * 1.15 * Math.sin(angle)).toFixed(3));
|
|
165
|
+
}
|
|
166
|
+
return 'M ' + pts.join(' L ') + ' Z';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
default:
|
|
170
|
+
return null; // circle or ellipse — caller uses native SVG element
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Appends the correct SVG shape child to every node <g>.
|
|
176
|
+
* All shapes receive the CSS class "node-shape" for uniform selection.
|
|
177
|
+
* circle / ellipse → native SVG elements positioned via cx/cy in tick
|
|
178
|
+
* everything else → <path> positioned via transform="translate()" in tick
|
|
179
|
+
*/
|
|
180
|
+
function appendShape(nodeSelection) {
|
|
181
|
+
nodeSelection.each(function(d) {
|
|
182
|
+
var g = d3.select(this);
|
|
183
|
+
var marker = (d.node_marker || 'circle').toLowerCase();
|
|
184
|
+
var r = parseFloat(d.node_size) || 8;
|
|
185
|
+
var shape;
|
|
186
|
+
|
|
187
|
+
if (marker === 'ellipse') {
|
|
188
|
+
shape = g.append('ellipse')
|
|
189
|
+
.attr('class', 'node-shape')
|
|
190
|
+
.attr('rx', r * 1.6)
|
|
191
|
+
.attr('ry', r);
|
|
192
|
+
} else if (marker === 'circle' || shapePathD(marker, r) === null) {
|
|
193
|
+
shape = g.append('circle')
|
|
194
|
+
.attr('class', 'node-shape')
|
|
195
|
+
.attr('r', r);
|
|
196
|
+
} else {
|
|
197
|
+
shape = g.append('path')
|
|
198
|
+
.attr('class', 'node-shape')
|
|
199
|
+
.attr('d', shapePathD(marker, r));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
shape
|
|
203
|
+
.style('fill', d.node_color)
|
|
204
|
+
.style('opacity', d.node_opacity)
|
|
205
|
+
.style('stroke-width', d.node_size_edge)
|
|
206
|
+
.style('stroke', d.node_color_edge);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ---- END SHAPE RENDERING ----
|
|
211
|
+
|
|
76
212
|
//Append a SVG to the body of the html page. Assign this SVG as an object to svg
|
|
77
213
|
var svg = d3.select("body").append("svg")
|
|
78
214
|
.attr("width", width)
|
|
@@ -129,7 +265,7 @@ function d3graphscript(config = {
|
|
|
129
265
|
d3.event.preventDefault();
|
|
130
266
|
d.fixed = false;
|
|
131
267
|
// Remove pinned visual cue
|
|
132
|
-
d3.select(this).select("
|
|
268
|
+
d3.select(this).select(".node-shape")
|
|
133
269
|
.style("stroke-dasharray", null)
|
|
134
270
|
.style("stroke-width", function(d) { return d.node_size_edge; });
|
|
135
271
|
force.resume();
|
|
@@ -137,23 +273,31 @@ function d3graphscript(config = {
|
|
|
137
273
|
}
|
|
138
274
|
|
|
139
275
|
{{ CLICK_COMMENT }} node.on('click', color_on_click); // ON CLICK HANDLER
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
node
|
|
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
|
|
276
|
+
|
|
277
|
+
// Append the correct shape per node (circle, ellipse, rect, triangle, etc.)
|
|
278
|
+
appendShape(node);
|
|
148
279
|
|
|
149
280
|
// Text in nodes
|
|
150
281
|
node.append("text")
|
|
151
|
-
.attr("dx", 10)
|
|
282
|
+
.attr("dx", function(d) { return config.node_text_inside ? 0 : 10; })
|
|
152
283
|
.attr("dy", ".35em")
|
|
153
|
-
.text
|
|
154
|
-
.
|
|
155
|
-
.style("
|
|
156
|
-
|
|
284
|
+
.attr("text-anchor", function(d) { return config.node_text_inside ? "middle" : "start"; })
|
|
285
|
+
.text(function(d) { return d.node_name; }) // NODE-TEXT
|
|
286
|
+
.style("font-size", function(d) {
|
|
287
|
+
if (config.node_text_inside) {
|
|
288
|
+
// Auto-shrink font so the label fits within the circle diameter
|
|
289
|
+
var r = parseFloat(d.node_size) || 8;
|
|
290
|
+
var label = d.node_name || "";
|
|
291
|
+
// Approximate: 0.6 * fontsize ≈ char width; fit label in 2*r with some padding
|
|
292
|
+
var maxFontSize = parseFloat(d.node_fontsize) || 12;
|
|
293
|
+
var fitSize = Math.min(maxFontSize, (1.8 * r) / Math.max(label.length * 0.6, 1));
|
|
294
|
+
return Math.max(fitSize, 6) + "px";
|
|
295
|
+
}
|
|
296
|
+
return d.node_fontsize + "px"; // NODE FONT SIZE
|
|
297
|
+
})
|
|
298
|
+
.style("fill", function(d) {return d.node_fontcolor;}) // NODE FONT COLOR
|
|
299
|
+
.style("font-family", "monospace")
|
|
300
|
+
.style("pointer-events", "none");
|
|
157
301
|
|
|
158
302
|
let showInHover = ["node_tooltip"]; // Tooltip
|
|
159
303
|
node.append("title")
|
|
@@ -169,8 +313,20 @@ function d3graphscript(config = {
|
|
|
169
313
|
.attr("y1", function(d) { return d.source.y; })
|
|
170
314
|
.attr("x2", function(d) { return d.target.x; })
|
|
171
315
|
.attr("y2", function(d) { return d.target.y; });
|
|
172
|
-
|
|
173
|
-
|
|
316
|
+
|
|
317
|
+
// Position each shape according to its SVG element type:
|
|
318
|
+
// circle / ellipse → cx / cy attributes
|
|
319
|
+
// path → transform translate(x, y)
|
|
320
|
+
d3.selectAll(".node-shape").each(function(d) {
|
|
321
|
+
var el = d3.select(this);
|
|
322
|
+
var tag = this.tagName.toLowerCase();
|
|
323
|
+
if (tag === 'circle' || tag === 'ellipse') {
|
|
324
|
+
el.attr("cx", d.x).attr("cy", d.y);
|
|
325
|
+
} else {
|
|
326
|
+
el.attr("transform", "translate(" + d.x + "," + d.y + ")");
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
174
330
|
d3.selectAll("text").attr("x", function(d) { return d.x; })
|
|
175
331
|
.attr("y", function(d) { return d.y; })
|
|
176
332
|
linkText.attr("x", function(d) { return (d.source.x + d.target.x) / 2; }) // ADD TEXT ON THE EDGES (PART 2/2)
|
|
@@ -263,25 +419,48 @@ function d3graphscript(config = {
|
|
|
263
419
|
|
|
264
420
|
// COLOR ON CLICK
|
|
265
421
|
function color_on_click() {
|
|
266
|
-
//
|
|
422
|
+
// Reset all nodes to their original styles
|
|
267
423
|
d3.selectAll(".node")
|
|
268
|
-
.select("
|
|
424
|
+
.select(".node-shape")
|
|
269
425
|
.style("fill", function(d) {return d.node_color;})
|
|
270
426
|
.style("opacity", function(d) {return d.node_opacity;})
|
|
271
427
|
.style("stroke", function(d) {return d.node_color_edge;})
|
|
272
428
|
.style("stroke-width", function(d) {return d.node_size_edge;})
|
|
273
429
|
// Restore pinned cue on still-fixed nodes
|
|
274
|
-
.style("stroke-dasharray", function(d) { return (sticky && d.fixed) ? "4,2" : null; })
|
|
275
|
-
|
|
276
|
-
|
|
430
|
+
.style("stroke-dasharray", function(d) { return (sticky && d.fixed) ? "4,2" : null; });
|
|
431
|
+
|
|
432
|
+
// Reset circle radii
|
|
433
|
+
d3.selectAll(".node").select("circle.node-shape")
|
|
434
|
+
.attr("r", function(d) { return d.node_size; });
|
|
277
435
|
|
|
278
|
-
//
|
|
279
|
-
d3.select(this).select("
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
436
|
+
// Apply click styling to the selected node's shape
|
|
437
|
+
var clickedShape = d3.select(this).select(".node-shape");
|
|
438
|
+
clickedShape
|
|
439
|
+
.style("fill", {{ CLICK_FILL }})
|
|
440
|
+
.style("stroke", "{{ CLICK_STROKE }}")
|
|
441
|
+
.style("stroke-width", {{ CLICK_STROKEW }});
|
|
442
|
+
|
|
443
|
+
// Scale up the shape — strategy depends on element type
|
|
444
|
+
var shapeNode = clickedShape.node();
|
|
445
|
+
if (!shapeNode) return;
|
|
446
|
+
var tag = shapeNode.tagName.toLowerCase();
|
|
447
|
+
if (tag === 'circle') {
|
|
448
|
+
clickedShape.attr("r", function(d) { return d.node_size * {{ CLICK_SIZE }}; });
|
|
449
|
+
} else if (tag === 'ellipse') {
|
|
450
|
+
clickedShape.each(function(d) {
|
|
451
|
+
var r = parseFloat(d.node_size) || 8;
|
|
452
|
+
d3.select(this)
|
|
453
|
+
.attr("rx", r * 1.6 * {{ CLICK_SIZE }})
|
|
454
|
+
.attr("ry", r * {{ CLICK_SIZE }});
|
|
455
|
+
});
|
|
456
|
+
} else {
|
|
457
|
+
// <path>: re-compute path with a scaled radius
|
|
458
|
+
clickedShape.each(function(d) {
|
|
459
|
+
var r = (parseFloat(d.node_size) || 8) * {{ CLICK_SIZE }};
|
|
460
|
+
d3.select(this).attr("d", shapePathD(d.node_marker, r));
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}
|
|
285
464
|
|
|
286
465
|
|
|
287
466
|
|
|
@@ -128,7 +128,8 @@
|
|
|
128
128
|
collision: {{ collision }},
|
|
129
129
|
link_tension: {{ link_tension }},
|
|
130
130
|
sticky: {{ sticky | lower }},
|
|
131
|
-
background_color: '{{ background_color }}'
|
|
131
|
+
background_color: '{{ background_color }}',
|
|
132
|
+
node_text_inside: {{ node_text_inside | lower }},
|
|
132
133
|
})
|
|
133
134
|
});
|
|
134
135
|
|
|
@@ -18,8 +18,13 @@ h3 {
|
|
|
18
18
|
font-family: "Helvetica";
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
/* Sticky / fixed node indicator: dashed outline ring */
|
|
22
|
-
|
|
21
|
+
/* Sticky / fixed node indicator: dashed outline ring — works for all shape types */
|
|
22
|
+
.node .node-shape.node-fixed {
|
|
23
23
|
stroke-dasharray: 4, 2;
|
|
24
24
|
stroke-width: 2.5px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* Shared pointer cursor for all node shapes */
|
|
28
|
+
.node .node-shape {
|
|
29
|
+
cursor: pointer;
|
|
25
30
|
}
|
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
from d3graph import d3graph, import_example, vec2adjmat
|
|
2
|
+
|
|
3
|
+
d3 = d3graph()
|
|
4
|
+
adjmat, df = import_example('karate')
|
|
5
|
+
d3.graph(adjmat)
|
|
6
|
+
|
|
7
|
+
# Mix shapes per node
|
|
8
|
+
markers = ['circle', 'star', 'diamond', 'square', 'pentagon', 'hexagon', 'rectangle', 'triangle-down', 'triangle'] * 8
|
|
9
|
+
d3.set_node_properties(marker=markers[:len(adjmat.columns)], color=df['label'].values, cmap='Set1')
|
|
10
|
+
d3.show(filepath="d:/repos/erdogant.github.io/docs/d3graph/d3graph/node_shapes.html")
|
|
11
|
+
|
|
12
|
+
# %%
|
|
13
|
+
from d3graph import d3graph, import_example
|
|
14
|
+
|
|
15
|
+
d3 = d3graph()
|
|
16
|
+
adjmat, df = import_example('karate')
|
|
17
|
+
d3.graph(adjmat)
|
|
18
|
+
|
|
19
|
+
# Set shape for node
|
|
20
|
+
d3.set_node_properties(marker='hexagon', color=df['label'].values, cmap='Set1')
|
|
21
|
+
d3.show(filepath="d:/repos/erdogant.github.io/docs/d3graph/d3graph/node_shape.html")
|
|
22
|
+
|
|
23
|
+
# %%
|
|
24
|
+
|
|
1
25
|
from d3graph import d3graph, import_example, get_hex_color
|
|
2
26
|
|
|
3
27
|
# Initialize
|
|
@@ -44,10 +68,6 @@ d3.show()
|
|
|
44
68
|
d3 = d3graph(adjmat, sticky=False)
|
|
45
69
|
d3.show()
|
|
46
70
|
|
|
47
|
-
# %%
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
71
|
|
|
52
72
|
# %%
|
|
53
73
|
|
|
@@ -97,7 +117,7 @@ adjmat = vec2adjmat(source=df['source'], target=df['target'], weight=df['weight'
|
|
|
97
117
|
d3.graph(adjmat)
|
|
98
118
|
|
|
99
119
|
# Set Node properties
|
|
100
|
-
d3.set_node_properties(edge_color='#000000', cmap='Blues', minmax=[5, 13], fontcolor='#808080')
|
|
120
|
+
d3.set_node_properties(marker='square', edge_color='#000000', cmap='Blues', minmax=[5, 13], fontcolor='#808080')
|
|
101
121
|
# Show the graph
|
|
102
122
|
# d3.show()
|
|
103
123
|
|
|
@@ -106,6 +126,7 @@ d3.set_node_properties(edge_color='#000000', cmap='Blues', minmax=[5, 13], fontc
|
|
|
106
126
|
# Set edge properties
|
|
107
127
|
d3.set_edge_properties(directed=True, marker_end='arrow')
|
|
108
128
|
d3.show()
|
|
129
|
+
d3.show(node_text_inside=True)
|
|
109
130
|
|
|
110
131
|
|
|
111
132
|
|
|
@@ -207,6 +228,9 @@ from d3graph import d3graph, adjmat2vec, vec2adjmat
|
|
|
207
228
|
|
|
208
229
|
#%%
|
|
209
230
|
from d3graph import check_logger
|
|
231
|
+
import numpy as np
|
|
232
|
+
import pandas as pd
|
|
233
|
+
|
|
210
234
|
check_logger(verbose='debug')
|
|
211
235
|
check_logger(verbose='info')
|
|
212
236
|
check_logger(verbose='info')
|
|
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
|