lightgraph 1.1.0__tar.gz → 1.2.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.
- {lightgraph-1.1.0 → lightgraph-1.2.0}/PKG-INFO +1 -1
- {lightgraph-1.1.0 → lightgraph-1.2.0}/lightgraph/__init__.py +2 -0
- {lightgraph-1.1.0 → lightgraph-1.2.0}/lightgraph/assets/lightgraph.js +185 -23
- lightgraph-1.2.0/lightgraph/data/football_edges.csv +614 -0
- lightgraph-1.2.0/lightgraph/data/football_nodes.csv +116 -0
- lightgraph-1.2.0/lightgraph/data/got.csv +353 -0
- lightgraph-1.2.0/lightgraph/data/les_mis.csv +255 -0
- lightgraph-1.2.0/lightgraph/datasets.py +90 -0
- {lightgraph-1.1.0 → lightgraph-1.2.0}/lightgraph/network.py +54 -2
- {lightgraph-1.1.0 → lightgraph-1.2.0}/pyproject.toml +1 -1
- lightgraph-1.2.0/tests/test_datasets.py +70 -0
- {lightgraph-1.1.0 → lightgraph-1.2.0}/tests/test_network.py +55 -0
- {lightgraph-1.1.0 → lightgraph-1.2.0}/uv.lock +1 -1
- {lightgraph-1.1.0 → lightgraph-1.2.0}/README.md +0 -0
- {lightgraph-1.1.0 → lightgraph-1.2.0}/lightgraph/analytics.py +0 -0
- {lightgraph-1.1.0 → lightgraph-1.2.0}/lightgraph/assets/d3.v7.min.js +0 -0
- {lightgraph-1.1.0 → lightgraph-1.2.0}/test.ipynb +0 -0
- {lightgraph-1.1.0 → lightgraph-1.2.0}/tests/__init__.py +0 -0
- {lightgraph-1.1.0 → lightgraph-1.2.0}/tests/test_analytics.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lightgraph
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: A lightweight Python binding for lightGraph network visualization
|
|
5
5
|
Author-email: Hao Zhu <haozhu233@gmail.com>, Aiden McComiskey <aiden.mccomiskey@tufts.edu>, Donna Slonim <donna.slonim@tufts.edu>
|
|
6
6
|
Requires-Python: >=3.6
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from .network import net_vis, NetworkVisualization
|
|
2
2
|
from . import analytics
|
|
3
|
+
from . import datasets
|
|
3
4
|
from .analytics import (
|
|
4
5
|
degree,
|
|
5
6
|
betweenness,
|
|
@@ -17,6 +18,7 @@ __all__ = [
|
|
|
17
18
|
'net_vis',
|
|
18
19
|
'NetworkVisualization',
|
|
19
20
|
'analytics',
|
|
21
|
+
'datasets',
|
|
20
22
|
'degree',
|
|
21
23
|
'betweenness',
|
|
22
24
|
'closeness',
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
edges: {
|
|
35
35
|
defaultColor: '#333333',
|
|
36
36
|
selectedColor: '#999999',
|
|
37
|
-
defaultOpacity: 0.
|
|
37
|
+
defaultOpacity: 0.3,
|
|
38
38
|
selectedOpacity: 0.6,
|
|
39
|
-
defaultWidth: 0.
|
|
39
|
+
defaultWidth: 0.45,
|
|
40
40
|
selectedWidth: 2,
|
|
41
41
|
// Scale edge opacity (and, when weight-mapped widths make
|
|
42
42
|
// strokes thick, edge width) by on-screen ink density so dense
|
|
@@ -91,7 +91,15 @@
|
|
|
91
91
|
groups: {
|
|
92
92
|
fillOpacity: 0.125,
|
|
93
93
|
strokeWidth: 2,
|
|
94
|
-
showEllipses: true
|
|
94
|
+
showEllipses: true,
|
|
95
|
+
// Pinned assignments: {groupName: hexColor}. Wins over the
|
|
96
|
+
// palette for the named groups; unmatched names are ignored so
|
|
97
|
+
// one map can be reused across filtered subsets.
|
|
98
|
+
colors: null,
|
|
99
|
+
// Explicit palette domain: [groupName, ...]. Listed groups keep
|
|
100
|
+
// their palette slot even when absent from the data, so a shared
|
|
101
|
+
// order keeps colors stable across a series of figures.
|
|
102
|
+
colorOrder: null
|
|
95
103
|
},
|
|
96
104
|
canvas: {
|
|
97
105
|
backgroundColor: '#ffffff',
|
|
@@ -112,7 +120,14 @@
|
|
|
112
120
|
theme: 'light',
|
|
113
121
|
showLegend: true,
|
|
114
122
|
showStatistics: false,
|
|
115
|
-
showTooltips: true
|
|
123
|
+
showTooltips: true,
|
|
124
|
+
// Metadata describing a numeric metric encoded as node size
|
|
125
|
+
// and/or color, rendered as an extra legend section. The
|
|
126
|
+
// Python/R wrappers set this automatically for node_metric;
|
|
127
|
+
// standalone users can supply it directly:
|
|
128
|
+
// { label, min, max, map: 'size'|'color'|'both',
|
|
129
|
+
// sizeRange: [minPx, maxPx], colors: [lowHex, highHex] }
|
|
130
|
+
metricLegend: null
|
|
116
131
|
},
|
|
117
132
|
layout: 'force' // 'force' or 'circular'
|
|
118
133
|
};
|
|
@@ -172,6 +187,25 @@
|
|
|
172
187
|
return value;
|
|
173
188
|
}
|
|
174
189
|
|
|
190
|
+
// Linear interpolation between two '#rrggbb' colors; used for the
|
|
191
|
+
// metric legend's size dots when the metric also drives color.
|
|
192
|
+
function lerpHex(hex0, hex1, t) {
|
|
193
|
+
const c0 = [1, 3, 5].map(i => parseInt(hex0.slice(i, i + 2), 16));
|
|
194
|
+
const c1 = [1, 3, 5].map(i => parseInt(hex1.slice(i, i + 2), 16));
|
|
195
|
+
return '#' + c0.map((v, i) => Math.round(v + (c1[i] - v) * t)
|
|
196
|
+
.toString(16).padStart(2, '0')).join('');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Compact human-readable form of a metric value for legend labels.
|
|
200
|
+
function formatMetricValue(value) {
|
|
201
|
+
if (!Number.isFinite(value)) return '';
|
|
202
|
+
const abs = Math.abs(value);
|
|
203
|
+
if (abs !== 0 && (abs < 0.001 || abs >= 100000)) {
|
|
204
|
+
return value.toExponential(2);
|
|
205
|
+
}
|
|
206
|
+
return String(parseFloat(value.toPrecision(3)));
|
|
207
|
+
}
|
|
208
|
+
|
|
175
209
|
// Eigen decomposition of a symmetric 2x2 covariance matrix; used for the
|
|
176
210
|
// orientation and radii of group ellipses.
|
|
177
211
|
function computeEigen(covMatrix) {
|
|
@@ -268,6 +302,10 @@
|
|
|
268
302
|
throw new Error('LightGraph: d3 v7 must be loaded (or passed via options.d3)');
|
|
269
303
|
}
|
|
270
304
|
const self = this;
|
|
305
|
+
// Expose the instance on the container element so embedding hosts
|
|
306
|
+
// (the R htmlwidget, generated Python/R pages, onRender hooks) can
|
|
307
|
+
// reach the API without holding the constructor's return value.
|
|
308
|
+
container.lightgraph = this;
|
|
271
309
|
const userConfig = options.config || {};
|
|
272
310
|
const theme = (userConfig.ui && userConfig.ui.theme) || DEFAULT_CONFIG.ui.theme;
|
|
273
311
|
let config = mergeConfig(
|
|
@@ -793,20 +831,18 @@
|
|
|
793
831
|
bottom: '16px',
|
|
794
832
|
left: '16px',
|
|
795
833
|
padding: '10px 12px',
|
|
796
|
-
|
|
834
|
+
// Cap the legend to roughly half the viewport; long group
|
|
835
|
+
// lists (plus the metric section) scroll beyond that.
|
|
836
|
+
maxHeight: 'min(50%, 340px)',
|
|
797
837
|
overflowY: 'auto',
|
|
798
838
|
fontSize: '12px',
|
|
799
839
|
display: 'none',
|
|
800
840
|
minWidth: '130px'
|
|
801
841
|
});
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
paddingBottom: '5px',
|
|
805
|
-
fontWeight: '600',
|
|
806
|
-
borderBottom: '1px solid var(--lg-border)'
|
|
807
|
-
});
|
|
842
|
+
// Section titles ('Groups', metric label) are rebuilt inside
|
|
843
|
+
// updateLegend along with the items.
|
|
808
844
|
const legendContent = createElement('div', { id: 'legendContent' });
|
|
809
|
-
legendPanel.append(
|
|
845
|
+
legendPanel.append(legendContent);
|
|
810
846
|
lightGraph.appendChild(legendPanel);
|
|
811
847
|
|
|
812
848
|
// 1.3.4 Statistics Panel
|
|
@@ -890,6 +926,35 @@
|
|
|
890
926
|
...d3.schemeSet3
|
|
891
927
|
];
|
|
892
928
|
const groupColorScale = d3.scaleOrdinal(extendedColors);
|
|
929
|
+
// Seed the palette domain deterministically on every data load:
|
|
930
|
+
// config.groups.colorOrder first (listed groups keep their slot even
|
|
931
|
+
// when absent from the data), then the remaining groups in sorted
|
|
932
|
+
// order. Without an explicit domain d3 assigns colors by first paint
|
|
933
|
+
// order, so the same group changes color whenever the node array does.
|
|
934
|
+
function updateGroupColorDomain() {
|
|
935
|
+
const present = [...new Set(nodes.map(n => n.group).filter(Boolean))].sort();
|
|
936
|
+
const order = config.groups.colorOrder;
|
|
937
|
+
const ordered = Array.isArray(order) ? order.map(String)
|
|
938
|
+
: (order != null ? [String(order)] : []);
|
|
939
|
+
const orderSet = new Set(ordered);
|
|
940
|
+
const domain = [...new Set([...ordered, ...present.filter(g => !orderSet.has(g))])];
|
|
941
|
+
if (domain.length > extendedColors.length) {
|
|
942
|
+
console.warn(`lightGraph: ${domain.length} groups exceed the `
|
|
943
|
+
+ `${extendedColors.length}-color palette; colors will repeat. `
|
|
944
|
+
+ `Pin colors via config.groups.colors to disambiguate.`);
|
|
945
|
+
}
|
|
946
|
+
groupColorScale.domain(domain);
|
|
947
|
+
}
|
|
948
|
+
// Single resolution point for every group-colored surface (legend
|
|
949
|
+
// swatch, node fill, ellipse, tooltip dot, SVG export): pinned
|
|
950
|
+
// config.groups.colors first, palette otherwise.
|
|
951
|
+
function getGroupColor(group) {
|
|
952
|
+
const pinned = config.groups.colors;
|
|
953
|
+
if (pinned && Object.prototype.hasOwnProperty.call(pinned, group)) {
|
|
954
|
+
return pinned[group];
|
|
955
|
+
}
|
|
956
|
+
return groupColorScale(group);
|
|
957
|
+
}
|
|
893
958
|
// Modeless interaction: d3-zoom owns wheel events and drags that
|
|
894
959
|
// start on empty space; node drags and shift box-selects fall
|
|
895
960
|
// through to the canvas listeners below.
|
|
@@ -1020,6 +1085,76 @@
|
|
|
1020
1085
|
function hideFloatingInput(x, y) {
|
|
1021
1086
|
floatingInput.style.display = 'none';
|
|
1022
1087
|
}
|
|
1088
|
+
function legendSectionTitle(text, isFirst) {
|
|
1089
|
+
return createElement('div', { textContent: text }, {
|
|
1090
|
+
marginTop: isFirst ? '0' : '10px',
|
|
1091
|
+
marginBottom: '6px',
|
|
1092
|
+
paddingBottom: '5px',
|
|
1093
|
+
fontWeight: '600',
|
|
1094
|
+
borderBottom: '1px solid var(--lg-border)'
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// Legend section for a metric encoded as node size and/or color:
|
|
1099
|
+
// a row of proportionally sized dots for size, a gradient bar for
|
|
1100
|
+
// color, and min/max value labels underneath.
|
|
1101
|
+
function buildMetricLegend(metric) {
|
|
1102
|
+
const section = createElement('div', { id: 'metricLegend' });
|
|
1103
|
+
const mapsSize = metric.map === 'size' || metric.map === 'both';
|
|
1104
|
+
const mapsColor = metric.map === 'color' || metric.map === 'both';
|
|
1105
|
+
const colors = (mapsColor && Array.isArray(metric.colors)
|
|
1106
|
+
&& metric.colors.length >= 2) ? metric.colors : null;
|
|
1107
|
+
|
|
1108
|
+
if (mapsSize) {
|
|
1109
|
+
const range = (Array.isArray(metric.sizeRange)
|
|
1110
|
+
&& metric.sizeRange.length >= 2) ? metric.sizeRange : [4, 20];
|
|
1111
|
+
// Dots keep their relative proportions but are capped so
|
|
1112
|
+
// large node sizes do not blow up the panel.
|
|
1113
|
+
const scale = Math.min(1, 16 / Math.max(range[0], range[1], 1));
|
|
1114
|
+
const row = createElement('div', {}, {
|
|
1115
|
+
display: 'flex',
|
|
1116
|
+
alignItems: 'center',
|
|
1117
|
+
gap: '8px',
|
|
1118
|
+
margin: '2px 0 4px'
|
|
1119
|
+
});
|
|
1120
|
+
[0, 0.5, 1].forEach(t => {
|
|
1121
|
+
const d = Math.max(3,
|
|
1122
|
+
(range[0] + t * (range[1] - range[0])) * scale);
|
|
1123
|
+
row.appendChild(createElement('div', {}, {
|
|
1124
|
+
width: `${d}px`,
|
|
1125
|
+
height: `${d}px`,
|
|
1126
|
+
borderRadius: '50%',
|
|
1127
|
+
backgroundColor: colors
|
|
1128
|
+
? lerpHex(colors[0], colors[1], t)
|
|
1129
|
+
: config.nodes.defaultColor,
|
|
1130
|
+
flexShrink: '0'
|
|
1131
|
+
}));
|
|
1132
|
+
});
|
|
1133
|
+
section.appendChild(row);
|
|
1134
|
+
}
|
|
1135
|
+
if (colors) {
|
|
1136
|
+
section.appendChild(createElement('div', { className: 'lg-metric-gradient' }, {
|
|
1137
|
+
height: '10px',
|
|
1138
|
+
borderRadius: '3px',
|
|
1139
|
+
background: `linear-gradient(to right, ${colors[0]}, ${colors[1]})`,
|
|
1140
|
+
margin: '2px 0 4px'
|
|
1141
|
+
}));
|
|
1142
|
+
}
|
|
1143
|
+
const labels = createElement('div', {}, {
|
|
1144
|
+
display: 'flex',
|
|
1145
|
+
justifyContent: 'space-between',
|
|
1146
|
+
gap: '12px',
|
|
1147
|
+
color: 'var(--lg-muted)',
|
|
1148
|
+
fontSize: '11px'
|
|
1149
|
+
});
|
|
1150
|
+
labels.append(
|
|
1151
|
+
createElement('span', { textContent: formatMetricValue(metric.min) }),
|
|
1152
|
+
createElement('span', { textContent: formatMetricValue(metric.max) })
|
|
1153
|
+
);
|
|
1154
|
+
section.appendChild(labels);
|
|
1155
|
+
return section;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1023
1158
|
function updateLegend() {
|
|
1024
1159
|
if (!config.ui.showLegend) {
|
|
1025
1160
|
legendPanel.style.display = 'none';
|
|
@@ -1027,7 +1162,11 @@
|
|
|
1027
1162
|
}
|
|
1028
1163
|
|
|
1029
1164
|
const groups = [...new Set(nodes.map(node => node.group).filter(Boolean))].sort();
|
|
1030
|
-
|
|
1165
|
+
const metric = config.ui.metricLegend;
|
|
1166
|
+
const hasMetric = !!(metric
|
|
1167
|
+
&& ['size', 'color', 'both'].includes(metric.map)
|
|
1168
|
+
&& Number.isFinite(metric.min) && Number.isFinite(metric.max));
|
|
1169
|
+
if (groups.length === 0 && !hasMetric) {
|
|
1031
1170
|
legendPanel.style.display = 'none';
|
|
1032
1171
|
return;
|
|
1033
1172
|
}
|
|
@@ -1035,6 +1174,9 @@
|
|
|
1035
1174
|
legendPanel.style.display = 'block';
|
|
1036
1175
|
legendContent.innerHTML = '';
|
|
1037
1176
|
|
|
1177
|
+
if (groups.length > 0) {
|
|
1178
|
+
legendContent.appendChild(legendSectionTitle('Groups', true));
|
|
1179
|
+
}
|
|
1038
1180
|
groups.forEach(group => {
|
|
1039
1181
|
const count = nodes.filter(n => n.group === group).length;
|
|
1040
1182
|
const item = createElement('div', { className: 'lg-legend-item' });
|
|
@@ -1043,7 +1185,7 @@
|
|
|
1043
1185
|
width: '11px',
|
|
1044
1186
|
height: '11px',
|
|
1045
1187
|
borderRadius: '3px',
|
|
1046
|
-
backgroundColor:
|
|
1188
|
+
backgroundColor: getGroupColor(group),
|
|
1047
1189
|
flexShrink: '0'
|
|
1048
1190
|
});
|
|
1049
1191
|
|
|
@@ -1059,6 +1201,12 @@
|
|
|
1059
1201
|
|
|
1060
1202
|
legendContent.appendChild(item);
|
|
1061
1203
|
});
|
|
1204
|
+
|
|
1205
|
+
if (hasMetric) {
|
|
1206
|
+
legendContent.appendChild(legendSectionTitle(
|
|
1207
|
+
metric.label || 'Metric', groups.length === 0));
|
|
1208
|
+
legendContent.appendChild(buildMetricLegend(metric));
|
|
1209
|
+
}
|
|
1062
1210
|
}
|
|
1063
1211
|
|
|
1064
1212
|
function updateStatistics() {
|
|
@@ -1207,11 +1355,16 @@
|
|
|
1207
1355
|
const meanScreenWidth = screenEdgeWidth(totalWidth / counted);
|
|
1208
1356
|
const coverage = edges.length * meanScreenLen *
|
|
1209
1357
|
meanScreenWidth * visibleShare / viewArea;
|
|
1210
|
-
//
|
|
1211
|
-
//
|
|
1212
|
-
//
|
|
1358
|
+
// Sub-linear ink budget, tuned on a matrix of graph sizes and
|
|
1359
|
+
// degrees (benchmark: 50-5000 nodes, mean degree 2-16): target
|
|
1360
|
+
// alpha ~= 0.12/sqrt(coverage). Linear-in-coverage budgets fail
|
|
1361
|
+
// at both ends — edges concentrate near the center, so a budget
|
|
1362
|
+
// loose enough for hairballs lets mid-density graphs go gray,
|
|
1363
|
+
// and one tight enough for mid-density erases hairball fringes.
|
|
1364
|
+
// Only ever dims: sparse graphs keep their configured style
|
|
1365
|
+
// (factors cap at 1), and effective alpha never drops below 0.02.
|
|
1213
1366
|
const base = Math.max(baseEdgeAlpha(), 0.001);
|
|
1214
|
-
const ideal = 0.
|
|
1367
|
+
const ideal = 0.12 / Math.sqrt(Math.max(coverage, 1e-6));
|
|
1215
1368
|
const needed = Math.min(1, ideal / base);
|
|
1216
1369
|
// Width absorbs up to half the correction (in log space), but
|
|
1217
1370
|
// the mean stroke never shrinks below the 0.5px hairline — so
|
|
@@ -1287,7 +1440,7 @@
|
|
|
1287
1440
|
maxNodeSize = config.nodes.defaultSize;
|
|
1288
1441
|
for (const d of nodes) {
|
|
1289
1442
|
if (egoSet && !egoSet.has(d)) continue;
|
|
1290
|
-
const color = d.group ?
|
|
1443
|
+
const color = d.group ? getGroupColor(d.group) : (d.color || config.nodes.defaultColor);
|
|
1291
1444
|
const isSelected = selectedNodes.has(d);
|
|
1292
1445
|
const isFaded = highlightSet !== null && !highlightSet.has(d);
|
|
1293
1446
|
let opacity = d.opacity !== undefined ? d.opacity : config.nodes.defaultOpacity;
|
|
@@ -1492,9 +1645,9 @@
|
|
|
1492
1645
|
context.rotate(angle);
|
|
1493
1646
|
context.beginPath();
|
|
1494
1647
|
context.ellipse(0, 0, radiusX + 5, radiusY + 5, 0, 0, 2 * Math.PI);
|
|
1495
|
-
context.fillStyle = hexToRgba(
|
|
1648
|
+
context.fillStyle = hexToRgba(getGroupColor(group), config.groups.fillOpacity);
|
|
1496
1649
|
context.fill();
|
|
1497
|
-
context.strokeStyle =
|
|
1650
|
+
context.strokeStyle = getGroupColor(group);
|
|
1498
1651
|
context.lineWidth = config.groups.strokeWidth;
|
|
1499
1652
|
context.stroke();
|
|
1500
1653
|
context.restore();
|
|
@@ -1532,6 +1685,7 @@
|
|
|
1532
1685
|
nodes = newNodes;
|
|
1533
1686
|
edges = newEdges;
|
|
1534
1687
|
|
|
1688
|
+
updateGroupColorDomain();
|
|
1535
1689
|
recalculateForce();
|
|
1536
1690
|
updateEdgeAlpha();
|
|
1537
1691
|
updateLegend();
|
|
@@ -1813,7 +1967,7 @@
|
|
|
1813
1967
|
if (config.ui.showTooltips && hoveredNode) {
|
|
1814
1968
|
let tooltipContent = `<strong>${escapeHtml(hoveredNode.id)}</strong>`;
|
|
1815
1969
|
if (hoveredNode.group) {
|
|
1816
|
-
tooltipContent += `<br><span style="color: ${
|
|
1970
|
+
tooltipContent += `<br><span style="color: ${getGroupColor(hoveredNode.group)};">● ${escapeHtml(hoveredNode.group)}</span>`;
|
|
1817
1971
|
}
|
|
1818
1972
|
// Add any custom metadata
|
|
1819
1973
|
if (hoveredNode.metadata) {
|
|
@@ -2042,7 +2196,7 @@
|
|
|
2042
2196
|
circle.setAttribute('cy', node.y);
|
|
2043
2197
|
circle.setAttribute('r',
|
|
2044
2198
|
(node.size || config.nodes.defaultSize) * config.nodes.sizeScale / 2);
|
|
2045
|
-
circle.setAttribute('fill', node.group ?
|
|
2199
|
+
circle.setAttribute('fill', node.group ? getGroupColor(node.group) : (node.color || config.nodes.defaultColor));
|
|
2046
2200
|
circle.setAttribute('stroke', config.nodes.borderColor);
|
|
2047
2201
|
circle.setAttribute('stroke-width', config.nodes.borderWidth);
|
|
2048
2202
|
g.appendChild(circle);
|
|
@@ -2130,6 +2284,9 @@
|
|
|
2130
2284
|
// Merge a partial config over the current one and re-render.
|
|
2131
2285
|
this.updateConfig = (partial = {}) => {
|
|
2132
2286
|
config = mergeConfig(config, partial);
|
|
2287
|
+
if (partial.groups && ('colors' in partial.groups || 'colorOrder' in partial.groups)) {
|
|
2288
|
+
updateGroupColorDomain();
|
|
2289
|
+
}
|
|
2133
2290
|
if (partial.edges && 'showArrows' in partial.edges) {
|
|
2134
2291
|
showArrows = config.edges.showArrows;
|
|
2135
2292
|
arrowsToggle.checked = showArrows;
|
|
@@ -2235,6 +2392,9 @@
|
|
|
2235
2392
|
legendPanel, statsPanel, tooltip].forEach(el => el.remove());
|
|
2236
2393
|
lightGraph.classList.remove('lg-root');
|
|
2237
2394
|
lightGraph.removeAttribute('data-lg-theme');
|
|
2395
|
+
if (container.lightgraph === self) {
|
|
2396
|
+
delete container.lightgraph;
|
|
2397
|
+
}
|
|
2238
2398
|
};
|
|
2239
2399
|
|
|
2240
2400
|
// Initial data load
|
|
@@ -2309,6 +2469,8 @@
|
|
|
2309
2469
|
mergeConfig,
|
|
2310
2470
|
escapeHtml,
|
|
2311
2471
|
hexToRgba,
|
|
2472
|
+
lerpHex,
|
|
2473
|
+
formatMetricValue,
|
|
2312
2474
|
computeEigen,
|
|
2313
2475
|
isNodeInSelection
|
|
2314
2476
|
};
|