polly-graph 0.1.7 → 0.1.8
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.
- package/README.md +308 -30
- package/dist/index.cjs +2857 -628
- package/dist/index.css +9 -5
- package/dist/index.d.cts +483 -8
- package/dist/index.d.ts +483 -8
- package/dist/index.js +2851 -618
- package/package.json +2 -1
package/dist/index.cjs
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
2
|
var __defProp = Object.defineProperty;
|
|
4
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
6
|
var __export = (target, all) => {
|
|
9
7
|
for (var name in all)
|
|
@@ -17,19 +15,17 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
17
15
|
}
|
|
18
16
|
return to;
|
|
19
17
|
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
19
|
|
|
30
20
|
// src/index.ts
|
|
31
21
|
var index_exports = {};
|
|
32
22
|
__export(index_exports, {
|
|
23
|
+
ErrorHandler: () => ErrorHandler,
|
|
24
|
+
GraphError: () => GraphError,
|
|
25
|
+
GraphValidationError: () => GraphValidationError,
|
|
26
|
+
GraphValidator: () => GraphValidator,
|
|
27
|
+
SelectionManager: () => SelectionManager,
|
|
28
|
+
TypedGraphEventEmitter: () => TypedGraphEventEmitter,
|
|
33
29
|
createGraph: () => createGraph
|
|
34
30
|
});
|
|
35
31
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -2666,15 +2662,15 @@ function defaultWheelDelta(event) {
|
|
|
2666
2662
|
function defaultTouchable2() {
|
|
2667
2663
|
return navigator.maxTouchPoints || "ontouchstart" in this;
|
|
2668
2664
|
}
|
|
2669
|
-
function defaultConstrain(transform2,
|
|
2670
|
-
var dx0 = transform2.invertX(
|
|
2665
|
+
function defaultConstrain(transform2, extent2, translateExtent) {
|
|
2666
|
+
var dx0 = transform2.invertX(extent2[0][0]) - translateExtent[0][0], dx1 = transform2.invertX(extent2[1][0]) - translateExtent[1][0], dy0 = transform2.invertY(extent2[0][1]) - translateExtent[0][1], dy1 = transform2.invertY(extent2[1][1]) - translateExtent[1][1];
|
|
2671
2667
|
return transform2.translate(
|
|
2672
2668
|
dx1 > dx0 ? (dx0 + dx1) / 2 : Math.min(0, dx0) || Math.max(0, dx1),
|
|
2673
2669
|
dy1 > dy0 ? (dy0 + dy1) / 2 : Math.min(0, dy0) || Math.max(0, dy1)
|
|
2674
2670
|
);
|
|
2675
2671
|
}
|
|
2676
2672
|
function zoom_default2() {
|
|
2677
|
-
var filter2 = defaultFilter2,
|
|
2673
|
+
var filter2 = defaultFilter2, extent2 = defaultExtent, constrain = defaultConstrain, wheelDelta = defaultWheelDelta, touchable = defaultTouchable2, scaleExtent = [0, Infinity], translateExtent = [[-Infinity, -Infinity], [Infinity, Infinity]], duration = 250, interpolate = zoom_default, listeners = dispatch_default2("start", "zoom", "end"), touchstarting, touchfirst, touchending, touchDelay = 500, wheelDelay = 150, clickDistance2 = 0, tapDistance = 10;
|
|
2678
2674
|
function zoom(selection2) {
|
|
2679
2675
|
selection2.property("__zoom", defaultTransform).on("wheel.zoom", wheeled, { passive: false }).on("mousedown.zoom", mousedowned).on("dblclick.zoom", dblclicked).filter(touchable).on("touchstart.zoom", touchstarted).on("touchmove.zoom", touchmoved).on("touchend.zoom touchcancel.zoom", touchended).style("-webkit-tap-highlight-color", "rgba(0,0,0,0)");
|
|
2680
2676
|
}
|
|
@@ -2697,7 +2693,7 @@ function zoom_default2() {
|
|
|
2697
2693
|
};
|
|
2698
2694
|
zoom.scaleTo = function(selection2, k, p, event) {
|
|
2699
2695
|
zoom.transform(selection2, function() {
|
|
2700
|
-
var e =
|
|
2696
|
+
var e = extent2.apply(this, arguments), t0 = this.__zoom, p0 = p == null ? centroid(e) : typeof p === "function" ? p.apply(this, arguments) : p, p1 = t0.invert(p0), k1 = typeof k === "function" ? k.apply(this, arguments) : k;
|
|
2701
2697
|
return constrain(translate(scale(t0, k1), p0, p1), e, translateExtent);
|
|
2702
2698
|
}, p, event);
|
|
2703
2699
|
};
|
|
@@ -2706,12 +2702,12 @@ function zoom_default2() {
|
|
|
2706
2702
|
return constrain(this.__zoom.translate(
|
|
2707
2703
|
typeof x3 === "function" ? x3.apply(this, arguments) : x3,
|
|
2708
2704
|
typeof y3 === "function" ? y3.apply(this, arguments) : y3
|
|
2709
|
-
),
|
|
2705
|
+
), extent2.apply(this, arguments), translateExtent);
|
|
2710
2706
|
}, null, event);
|
|
2711
2707
|
};
|
|
2712
2708
|
zoom.translateTo = function(selection2, x3, y3, p, event) {
|
|
2713
2709
|
zoom.transform(selection2, function() {
|
|
2714
|
-
var e =
|
|
2710
|
+
var e = extent2.apply(this, arguments), t = this.__zoom, p0 = p == null ? centroid(e) : typeof p === "function" ? p.apply(this, arguments) : p;
|
|
2715
2711
|
return constrain(identity2.translate(p0[0], p0[1]).scale(t.k).translate(
|
|
2716
2712
|
typeof x3 === "function" ? -x3.apply(this, arguments) : -x3,
|
|
2717
2713
|
typeof y3 === "function" ? -y3.apply(this, arguments) : -y3
|
|
@@ -2726,8 +2722,8 @@ function zoom_default2() {
|
|
|
2726
2722
|
var x3 = p0[0] - p1[0] * transform2.k, y3 = p0[1] - p1[1] * transform2.k;
|
|
2727
2723
|
return x3 === transform2.x && y3 === transform2.y ? transform2 : new Transform(transform2.k, x3, y3);
|
|
2728
2724
|
}
|
|
2729
|
-
function centroid(
|
|
2730
|
-
return [(+
|
|
2725
|
+
function centroid(extent3) {
|
|
2726
|
+
return [(+extent3[0][0] + +extent3[1][0]) / 2, (+extent3[0][1] + +extent3[1][1]) / 2];
|
|
2731
2727
|
}
|
|
2732
2728
|
function schedule(transition2, transform2, point, event) {
|
|
2733
2729
|
transition2.on("start.zoom", function() {
|
|
@@ -2735,7 +2731,7 @@ function zoom_default2() {
|
|
|
2735
2731
|
}).on("interrupt.zoom end.zoom", function() {
|
|
2736
2732
|
gesture(this, arguments).event(event).end();
|
|
2737
2733
|
}).tween("zoom", function() {
|
|
2738
|
-
var that = this, args = arguments, g = gesture(that, args).event(event), e =
|
|
2734
|
+
var that = this, args = arguments, g = gesture(that, args).event(event), e = extent2.apply(that, args), p = point == null ? centroid(e) : typeof point === "function" ? point.apply(that, args) : point, w = Math.max(e[1][0] - e[0][0], e[1][1] - e[0][1]), a2 = that.__zoom, b = typeof transform2 === "function" ? transform2.apply(that, args) : transform2, i = interpolate(a2.invert(p).concat(w / a2.k), b.invert(p).concat(w / b.k));
|
|
2739
2735
|
return function(t) {
|
|
2740
2736
|
if (t === 1) t = b;
|
|
2741
2737
|
else {
|
|
@@ -2754,7 +2750,7 @@ function zoom_default2() {
|
|
|
2754
2750
|
this.args = args;
|
|
2755
2751
|
this.active = 0;
|
|
2756
2752
|
this.sourceEvent = null;
|
|
2757
|
-
this.extent =
|
|
2753
|
+
this.extent = extent2.apply(that, args);
|
|
2758
2754
|
this.taps = 0;
|
|
2759
2755
|
}
|
|
2760
2756
|
Gesture.prototype = {
|
|
@@ -2847,7 +2843,7 @@ function zoom_default2() {
|
|
|
2847
2843
|
}
|
|
2848
2844
|
function dblclicked(event, ...args) {
|
|
2849
2845
|
if (!filter2.apply(this, arguments)) return;
|
|
2850
|
-
var t0 = this.__zoom, p0 = pointer_default(event.changedTouches ? event.changedTouches[0] : event, this), p1 = t0.invert(p0), k1 = t0.k * (event.shiftKey ? 0.5 : 2), t1 = constrain(translate(scale(t0, k1), p0, p1),
|
|
2846
|
+
var t0 = this.__zoom, p0 = pointer_default(event.changedTouches ? event.changedTouches[0] : event, this), p1 = t0.invert(p0), k1 = t0.k * (event.shiftKey ? 0.5 : 2), t1 = constrain(translate(scale(t0, k1), p0, p1), extent2.apply(this, args), translateExtent);
|
|
2851
2847
|
noevent_default2(event);
|
|
2852
2848
|
if (duration > 0) select_default2(this).transition().duration(duration).call(schedule, t1, p0, event);
|
|
2853
2849
|
else select_default2(this).call(zoom.transform, t1, p0, event);
|
|
@@ -2926,7 +2922,7 @@ function zoom_default2() {
|
|
|
2926
2922
|
return arguments.length ? (touchable = typeof _ === "function" ? _ : constant_default4(!!_), zoom) : touchable;
|
|
2927
2923
|
};
|
|
2928
2924
|
zoom.extent = function(_) {
|
|
2929
|
-
return arguments.length ? (
|
|
2925
|
+
return arguments.length ? (extent2 = typeof _ === "function" ? _ : constant_default4([[+_[0][0], +_[0][1]], [+_[1][0], +_[1][1]]]), zoom) : extent2;
|
|
2930
2926
|
};
|
|
2931
2927
|
zoom.scaleExtent = function(_) {
|
|
2932
2928
|
return arguments.length ? (scaleExtent[0] = +_[0], scaleExtent[1] = +_[1], zoom) : [scaleExtent[0], scaleExtent[1]];
|
|
@@ -2956,6 +2952,471 @@ function zoom_default2() {
|
|
|
2956
2952
|
return zoom;
|
|
2957
2953
|
}
|
|
2958
2954
|
|
|
2955
|
+
// src/create-graph.ts
|
|
2956
|
+
var import_d3 = require("d3");
|
|
2957
|
+
|
|
2958
|
+
// src/utils/timer-manager.ts
|
|
2959
|
+
var TimerManager = class {
|
|
2960
|
+
activeTimers = /* @__PURE__ */ new Map();
|
|
2961
|
+
isDestroyed = false;
|
|
2962
|
+
/**
|
|
2963
|
+
* Schedule a timeout with automatic cleanup tracking
|
|
2964
|
+
*/
|
|
2965
|
+
setTimeout(name, callback, delay) {
|
|
2966
|
+
this.clearTimer(name);
|
|
2967
|
+
if (this.isDestroyed) return;
|
|
2968
|
+
const id2 = window.setTimeout(() => {
|
|
2969
|
+
this.activeTimers.delete(name);
|
|
2970
|
+
if (!this.isDestroyed) {
|
|
2971
|
+
callback();
|
|
2972
|
+
}
|
|
2973
|
+
}, delay);
|
|
2974
|
+
this.activeTimers.set(name, {
|
|
2975
|
+
id: id2,
|
|
2976
|
+
name,
|
|
2977
|
+
type: "timeout",
|
|
2978
|
+
startTime: performance.now()
|
|
2979
|
+
});
|
|
2980
|
+
}
|
|
2981
|
+
/**
|
|
2982
|
+
* Schedule an interval with automatic cleanup tracking
|
|
2983
|
+
*/
|
|
2984
|
+
setInterval(name, callback, delay) {
|
|
2985
|
+
this.clearTimer(name);
|
|
2986
|
+
if (this.isDestroyed) return;
|
|
2987
|
+
const id2 = window.setInterval(() => {
|
|
2988
|
+
if (!this.isDestroyed) {
|
|
2989
|
+
callback();
|
|
2990
|
+
} else {
|
|
2991
|
+
this.clearTimer(name);
|
|
2992
|
+
}
|
|
2993
|
+
}, delay);
|
|
2994
|
+
this.activeTimers.set(name, {
|
|
2995
|
+
id: id2,
|
|
2996
|
+
name,
|
|
2997
|
+
type: "interval",
|
|
2998
|
+
startTime: performance.now()
|
|
2999
|
+
});
|
|
3000
|
+
}
|
|
3001
|
+
/**
|
|
3002
|
+
* Clear a specific timer by name
|
|
3003
|
+
*/
|
|
3004
|
+
clearTimer(name) {
|
|
3005
|
+
const timer2 = this.activeTimers.get(name);
|
|
3006
|
+
if (!timer2) return false;
|
|
3007
|
+
if (timer2.type === "timeout") {
|
|
3008
|
+
clearTimeout(timer2.id);
|
|
3009
|
+
} else {
|
|
3010
|
+
clearInterval(timer2.id);
|
|
3011
|
+
}
|
|
3012
|
+
this.activeTimers.delete(name);
|
|
3013
|
+
return true;
|
|
3014
|
+
}
|
|
3015
|
+
/**
|
|
3016
|
+
* Check if a timer is currently active
|
|
3017
|
+
*/
|
|
3018
|
+
hasActiveTimer(name) {
|
|
3019
|
+
return this.activeTimers.has(name);
|
|
3020
|
+
}
|
|
3021
|
+
/**
|
|
3022
|
+
* Get information about active timers
|
|
3023
|
+
*/
|
|
3024
|
+
getActiveTimers() {
|
|
3025
|
+
const now2 = performance.now();
|
|
3026
|
+
return Array.from(this.activeTimers.values()).map((timer2) => ({
|
|
3027
|
+
name: timer2.name,
|
|
3028
|
+
type: timer2.type,
|
|
3029
|
+
age: now2 - timer2.startTime
|
|
3030
|
+
}));
|
|
3031
|
+
}
|
|
3032
|
+
/**
|
|
3033
|
+
* Debounced execution - only runs the latest call after delay
|
|
3034
|
+
*/
|
|
3035
|
+
debounce(name, callback, delay) {
|
|
3036
|
+
this.setTimeout(name, callback, delay);
|
|
3037
|
+
}
|
|
3038
|
+
/**
|
|
3039
|
+
* Throttled execution - limits frequency of execution
|
|
3040
|
+
*/
|
|
3041
|
+
throttle(name, callback, delay) {
|
|
3042
|
+
if (this.hasActiveTimer(name)) {
|
|
3043
|
+
return;
|
|
3044
|
+
}
|
|
3045
|
+
callback();
|
|
3046
|
+
this.setTimeout(name, () => {
|
|
3047
|
+
}, delay);
|
|
3048
|
+
}
|
|
3049
|
+
/**
|
|
3050
|
+
* Schedule multiple related timers that can be cleared as a group
|
|
3051
|
+
*/
|
|
3052
|
+
scheduleGroup(groupName, timers) {
|
|
3053
|
+
timers.forEach((timer2, index2) => {
|
|
3054
|
+
const timerName = `${groupName}_${timer2.name}_${index2}`;
|
|
3055
|
+
if (timer2.type === "interval") {
|
|
3056
|
+
this.setInterval(timerName, timer2.callback, timer2.delay);
|
|
3057
|
+
} else {
|
|
3058
|
+
this.setTimeout(timerName, timer2.callback, timer2.delay);
|
|
3059
|
+
}
|
|
3060
|
+
});
|
|
3061
|
+
}
|
|
3062
|
+
/**
|
|
3063
|
+
* Clear all timers in a group
|
|
3064
|
+
*/
|
|
3065
|
+
clearGroup(groupName) {
|
|
3066
|
+
let clearedCount = 0;
|
|
3067
|
+
for (const [name] of Array.from(this.activeTimers)) {
|
|
3068
|
+
if (name.startsWith(`${groupName}_`)) {
|
|
3069
|
+
this.clearTimer(name);
|
|
3070
|
+
clearedCount++;
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
return clearedCount;
|
|
3074
|
+
}
|
|
3075
|
+
/**
|
|
3076
|
+
* Get count of active timers
|
|
3077
|
+
*/
|
|
3078
|
+
getActiveTimerCount() {
|
|
3079
|
+
return this.activeTimers.size;
|
|
3080
|
+
}
|
|
3081
|
+
/**
|
|
3082
|
+
* Clear all timers and prevent new ones from being created
|
|
3083
|
+
*/
|
|
3084
|
+
destroy() {
|
|
3085
|
+
this.isDestroyed = true;
|
|
3086
|
+
for (const [name] of Array.from(this.activeTimers)) {
|
|
3087
|
+
this.clearTimer(name);
|
|
3088
|
+
}
|
|
3089
|
+
this.activeTimers.clear();
|
|
3090
|
+
}
|
|
3091
|
+
/**
|
|
3092
|
+
* Reset manager (clear all timers but allow new ones)
|
|
3093
|
+
*/
|
|
3094
|
+
reset() {
|
|
3095
|
+
for (const [name] of Array.from(this.activeTimers)) {
|
|
3096
|
+
this.clearTimer(name);
|
|
3097
|
+
}
|
|
3098
|
+
this.isDestroyed = false;
|
|
3099
|
+
}
|
|
3100
|
+
};
|
|
3101
|
+
|
|
3102
|
+
// src/utils/event-emitter.ts
|
|
3103
|
+
var GraphEventEmitter = class {
|
|
3104
|
+
listeners = /* @__PURE__ */ new Map();
|
|
3105
|
+
options;
|
|
3106
|
+
isDestroyed = false;
|
|
3107
|
+
listenerIdCounter = 0;
|
|
3108
|
+
constructor(options = {}) {
|
|
3109
|
+
this.options = {
|
|
3110
|
+
maxListeners: options.maxListeners ?? 50,
|
|
3111
|
+
enableWarnings: options.enableWarnings ?? true
|
|
3112
|
+
};
|
|
3113
|
+
}
|
|
3114
|
+
/**
|
|
3115
|
+
* Add an event listener
|
|
3116
|
+
*/
|
|
3117
|
+
on(event, listener, namespace) {
|
|
3118
|
+
if (this.isDestroyed) {
|
|
3119
|
+
if (this.options.enableWarnings) {
|
|
3120
|
+
console.warn("[GraphEventEmitter] Cannot add listener to destroyed emitter");
|
|
3121
|
+
}
|
|
3122
|
+
return () => {
|
|
3123
|
+
};
|
|
3124
|
+
}
|
|
3125
|
+
const id2 = `listener_${++this.listenerIdCounter}`;
|
|
3126
|
+
const handler = {
|
|
3127
|
+
listener,
|
|
3128
|
+
namespace,
|
|
3129
|
+
id: id2
|
|
3130
|
+
};
|
|
3131
|
+
if (!this.listeners.has(event)) {
|
|
3132
|
+
this.listeners.set(event, []);
|
|
3133
|
+
}
|
|
3134
|
+
const eventListeners = this.listeners.get(event);
|
|
3135
|
+
eventListeners.push(handler);
|
|
3136
|
+
if (eventListeners.length > this.options.maxListeners && this.options.enableWarnings) {
|
|
3137
|
+
console.warn(
|
|
3138
|
+
`[GraphEventEmitter] Event '${String(event)}' has ${eventListeners.length} listeners. Possible memory leak detected.`
|
|
3139
|
+
);
|
|
3140
|
+
}
|
|
3141
|
+
return () => {
|
|
3142
|
+
this.removeListener(event, handler.id);
|
|
3143
|
+
};
|
|
3144
|
+
}
|
|
3145
|
+
/**
|
|
3146
|
+
* Add a one-time event listener
|
|
3147
|
+
*/
|
|
3148
|
+
once(event, listener, namespace) {
|
|
3149
|
+
const id2 = `listener_${++this.listenerIdCounter}`;
|
|
3150
|
+
const handler = {
|
|
3151
|
+
listener: (data, element) => {
|
|
3152
|
+
this.removeListener(event, id2);
|
|
3153
|
+
listener(data, element);
|
|
3154
|
+
},
|
|
3155
|
+
namespace,
|
|
3156
|
+
once: true,
|
|
3157
|
+
id: id2
|
|
3158
|
+
};
|
|
3159
|
+
if (!this.listeners.has(event)) {
|
|
3160
|
+
this.listeners.set(event, []);
|
|
3161
|
+
}
|
|
3162
|
+
this.listeners.get(event).push(handler);
|
|
3163
|
+
return () => {
|
|
3164
|
+
this.removeListener(event, handler.id);
|
|
3165
|
+
};
|
|
3166
|
+
}
|
|
3167
|
+
/**
|
|
3168
|
+
* Remove a specific listener
|
|
3169
|
+
*/
|
|
3170
|
+
off(event, listener) {
|
|
3171
|
+
const eventListeners = this.listeners.get(event);
|
|
3172
|
+
if (!eventListeners) return;
|
|
3173
|
+
const index2 = eventListeners.findIndex((handler) => handler.listener === listener);
|
|
3174
|
+
if (index2 !== -1) {
|
|
3175
|
+
eventListeners.splice(index2, 1);
|
|
3176
|
+
if (eventListeners.length === 0) {
|
|
3177
|
+
this.listeners.delete(event);
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
/**
|
|
3182
|
+
* Remove listener by ID
|
|
3183
|
+
*/
|
|
3184
|
+
removeListener(event, id2) {
|
|
3185
|
+
const eventListeners = this.listeners.get(event);
|
|
3186
|
+
if (!eventListeners) return;
|
|
3187
|
+
const index2 = eventListeners.findIndex((handler) => handler.id === id2);
|
|
3188
|
+
if (index2 !== -1) {
|
|
3189
|
+
eventListeners.splice(index2, 1);
|
|
3190
|
+
if (eventListeners.length === 0) {
|
|
3191
|
+
this.listeners.delete(event);
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
/**
|
|
3196
|
+
* Remove all listeners for an event or namespace
|
|
3197
|
+
*/
|
|
3198
|
+
removeAllListeners(event, namespace) {
|
|
3199
|
+
if (event && !namespace) {
|
|
3200
|
+
this.listeners.delete(event);
|
|
3201
|
+
} else if (namespace && !event) {
|
|
3202
|
+
for (const [eventName, handlers] of Array.from(this.listeners.entries())) {
|
|
3203
|
+
const filtered = handlers.filter((handler) => handler.namespace !== namespace);
|
|
3204
|
+
if (filtered.length === 0) {
|
|
3205
|
+
this.listeners.delete(eventName);
|
|
3206
|
+
} else {
|
|
3207
|
+
this.listeners.set(eventName, filtered);
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
} else if (event && namespace) {
|
|
3211
|
+
const eventListeners = this.listeners.get(event);
|
|
3212
|
+
if (eventListeners) {
|
|
3213
|
+
const filtered = eventListeners.filter((handler) => handler.namespace !== namespace);
|
|
3214
|
+
if (filtered.length === 0) {
|
|
3215
|
+
this.listeners.delete(event);
|
|
3216
|
+
} else {
|
|
3217
|
+
this.listeners.set(event, filtered);
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
} else {
|
|
3221
|
+
this.listeners.clear();
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
/**
|
|
3225
|
+
* Emit an event to all listeners
|
|
3226
|
+
*/
|
|
3227
|
+
emit(event, data, element) {
|
|
3228
|
+
if (this.isDestroyed) {
|
|
3229
|
+
return false;
|
|
3230
|
+
}
|
|
3231
|
+
const eventListeners = this.listeners.get(event);
|
|
3232
|
+
if (!eventListeners || eventListeners.length === 0) {
|
|
3233
|
+
return false;
|
|
3234
|
+
}
|
|
3235
|
+
const listenersToCall = [...eventListeners];
|
|
3236
|
+
for (const handler of listenersToCall) {
|
|
3237
|
+
try {
|
|
3238
|
+
handler.listener(data, element);
|
|
3239
|
+
} catch (error) {
|
|
3240
|
+
if (this.options.enableWarnings) {
|
|
3241
|
+
console.error(`[GraphEventEmitter] Error in listener for '${String(event)}':`, error);
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
return true;
|
|
3246
|
+
}
|
|
3247
|
+
/**
|
|
3248
|
+
* Get the number of listeners for an event
|
|
3249
|
+
*/
|
|
3250
|
+
listenerCount(event) {
|
|
3251
|
+
return this.listeners.get(event)?.length ?? 0;
|
|
3252
|
+
}
|
|
3253
|
+
/**
|
|
3254
|
+
* Get all event names that have listeners
|
|
3255
|
+
*/
|
|
3256
|
+
eventNames() {
|
|
3257
|
+
return Array.from(this.listeners.keys());
|
|
3258
|
+
}
|
|
3259
|
+
/**
|
|
3260
|
+
* Get listeners for an event
|
|
3261
|
+
*/
|
|
3262
|
+
getListeners(event) {
|
|
3263
|
+
return this.listeners.get(event)?.map((handler) => handler.listener) ?? [];
|
|
3264
|
+
}
|
|
3265
|
+
/**
|
|
3266
|
+
* Check if an event has listeners
|
|
3267
|
+
*/
|
|
3268
|
+
hasListeners(event) {
|
|
3269
|
+
return this.listenerCount(event) > 0;
|
|
3270
|
+
}
|
|
3271
|
+
/**
|
|
3272
|
+
* Get debug information about the emitter
|
|
3273
|
+
*/
|
|
3274
|
+
getDebugInfo() {
|
|
3275
|
+
const events = /* @__PURE__ */ new Map();
|
|
3276
|
+
const namespaces = /* @__PURE__ */ new Map();
|
|
3277
|
+
for (const [event, handlers] of Array.from(this.listeners.entries())) {
|
|
3278
|
+
events.set(event, handlers.length);
|
|
3279
|
+
for (const handler of handlers) {
|
|
3280
|
+
if (handler.namespace) {
|
|
3281
|
+
namespaces.set(handler.namespace, (namespaces.get(handler.namespace) ?? 0) + 1);
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
return {
|
|
3286
|
+
totalEvents: this.listeners.size,
|
|
3287
|
+
totalListeners: Array.from(this.listeners.values()).reduce((sum, handlers) => sum + handlers.length, 0),
|
|
3288
|
+
events: Object.fromEntries(events.entries()),
|
|
3289
|
+
namespaces: Object.fromEntries(namespaces),
|
|
3290
|
+
isDestroyed: this.isDestroyed
|
|
3291
|
+
};
|
|
3292
|
+
}
|
|
3293
|
+
/**
|
|
3294
|
+
* Clean up all listeners and mark as destroyed
|
|
3295
|
+
*/
|
|
3296
|
+
destroy() {
|
|
3297
|
+
this.isDestroyed = true;
|
|
3298
|
+
this.listeners.clear();
|
|
3299
|
+
}
|
|
3300
|
+
/**
|
|
3301
|
+
* Reset the emitter (clear listeners but allow new ones)
|
|
3302
|
+
*/
|
|
3303
|
+
reset() {
|
|
3304
|
+
this.isDestroyed = false;
|
|
3305
|
+
this.listeners.clear();
|
|
3306
|
+
}
|
|
3307
|
+
};
|
|
3308
|
+
var TypedGraphEventEmitter = class extends GraphEventEmitter {
|
|
3309
|
+
constructor(options) {
|
|
3310
|
+
super(options);
|
|
3311
|
+
}
|
|
3312
|
+
};
|
|
3313
|
+
|
|
3314
|
+
// src/core/graph-manager.ts
|
|
3315
|
+
var GraphManager = class {
|
|
3316
|
+
constructor(config) {
|
|
3317
|
+
this.config = config;
|
|
3318
|
+
}
|
|
3319
|
+
config;
|
|
3320
|
+
// Core Managers
|
|
3321
|
+
timerManager = null;
|
|
3322
|
+
tickManager = null;
|
|
3323
|
+
eventEmitter = null;
|
|
3324
|
+
selectionManager = null;
|
|
3325
|
+
// DOM Elements
|
|
3326
|
+
svgElement = null;
|
|
3327
|
+
rootGroup = null;
|
|
3328
|
+
layers = null;
|
|
3329
|
+
dimensions = { width: 0, height: 0 };
|
|
3330
|
+
// D3 Behaviors
|
|
3331
|
+
zoomBehavior = null;
|
|
3332
|
+
simulation = null;
|
|
3333
|
+
// Component Instances
|
|
3334
|
+
controls = null;
|
|
3335
|
+
tooltipBinding = null;
|
|
3336
|
+
// Cleanup Functions
|
|
3337
|
+
cleanupFunctions = [];
|
|
3338
|
+
// Callbacks
|
|
3339
|
+
fitViewCallback = null;
|
|
3340
|
+
// State Tracking
|
|
3341
|
+
linkMarkerSnapshots = null;
|
|
3342
|
+
rootSelection = null;
|
|
3343
|
+
simulationPaused = false;
|
|
3344
|
+
needsImmediateFitView = false;
|
|
3345
|
+
/**
|
|
3346
|
+
* Initialize core managers
|
|
3347
|
+
*/
|
|
3348
|
+
initializeManagers() {
|
|
3349
|
+
if (!this.timerManager) {
|
|
3350
|
+
this.timerManager = new TimerManager();
|
|
3351
|
+
} else {
|
|
3352
|
+
this.timerManager.reset();
|
|
3353
|
+
}
|
|
3354
|
+
if (!this.eventEmitter) {
|
|
3355
|
+
this.eventEmitter = new TypedGraphEventEmitter();
|
|
3356
|
+
} else {
|
|
3357
|
+
this.eventEmitter.reset();
|
|
3358
|
+
}
|
|
3359
|
+
if (this.tickManager) {
|
|
3360
|
+
this.tickManager.clearCaches();
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
/**
|
|
3364
|
+
* Add cleanup function to be called on destroy
|
|
3365
|
+
*/
|
|
3366
|
+
addCleanup(cleanup) {
|
|
3367
|
+
this.cleanupFunctions.push(cleanup);
|
|
3368
|
+
}
|
|
3369
|
+
/**
|
|
3370
|
+
* Clean up all resources
|
|
3371
|
+
*/
|
|
3372
|
+
destroy() {
|
|
3373
|
+
this.cleanupFunctions.forEach((cleanup) => {
|
|
3374
|
+
try {
|
|
3375
|
+
cleanup();
|
|
3376
|
+
} catch (error) {
|
|
3377
|
+
console.warn("[GraphManager] Cleanup function failed:", error);
|
|
3378
|
+
}
|
|
3379
|
+
});
|
|
3380
|
+
this.timerManager?.destroy();
|
|
3381
|
+
this.tickManager?.clearCaches();
|
|
3382
|
+
this.eventEmitter?.reset();
|
|
3383
|
+
if (this.tooltipBinding && "destroy" in this.tooltipBinding) {
|
|
3384
|
+
this.tooltipBinding.destroy();
|
|
3385
|
+
}
|
|
3386
|
+
this.controls?.destroy?.();
|
|
3387
|
+
this.timerManager = null;
|
|
3388
|
+
this.tickManager = null;
|
|
3389
|
+
this.eventEmitter = null;
|
|
3390
|
+
this.selectionManager = null;
|
|
3391
|
+
this.svgElement = null;
|
|
3392
|
+
this.rootGroup = null;
|
|
3393
|
+
this.zoomBehavior = null;
|
|
3394
|
+
this.simulation = null;
|
|
3395
|
+
this.controls = null;
|
|
3396
|
+
this.tooltipBinding = null;
|
|
3397
|
+
this.linkMarkerSnapshots = null;
|
|
3398
|
+
this.rootSelection = null;
|
|
3399
|
+
this.simulationPaused = false;
|
|
3400
|
+
this.needsImmediateFitView = false;
|
|
3401
|
+
this.cleanupFunctions = [];
|
|
3402
|
+
}
|
|
3403
|
+
/**
|
|
3404
|
+
* Reheat simulation with specified alpha
|
|
3405
|
+
*/
|
|
3406
|
+
reheatSimulation(alpha = 0.3) {
|
|
3407
|
+
if (!this.simulation) {
|
|
3408
|
+
console.warn("[GraphManager] Cannot reheat: no simulation");
|
|
3409
|
+
return;
|
|
3410
|
+
}
|
|
3411
|
+
this.simulation.alpha(alpha).restart();
|
|
3412
|
+
if (this.timerManager) {
|
|
3413
|
+
this.timerManager.debounce("simulation-cooldown", () => {
|
|
3414
|
+
this.simulation?.stop();
|
|
3415
|
+
}, 2e3);
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
};
|
|
3419
|
+
|
|
2959
3420
|
// node_modules/d3-force/src/center.js
|
|
2960
3421
|
function center_default(x3, y3) {
|
|
2961
3422
|
var nodes, strength = 1;
|
|
@@ -3644,6 +4105,68 @@ function manyBody_default() {
|
|
|
3644
4105
|
return force;
|
|
3645
4106
|
}
|
|
3646
4107
|
|
|
4108
|
+
// node_modules/d3-force/src/x.js
|
|
4109
|
+
function x_default2(x3) {
|
|
4110
|
+
var strength = constant_default5(0.1), nodes, strengths, xz;
|
|
4111
|
+
if (typeof x3 !== "function") x3 = constant_default5(x3 == null ? 0 : +x3);
|
|
4112
|
+
function force(alpha) {
|
|
4113
|
+
for (var i = 0, n = nodes.length, node; i < n; ++i) {
|
|
4114
|
+
node = nodes[i], node.vx += (xz[i] - node.x) * strengths[i] * alpha;
|
|
4115
|
+
}
|
|
4116
|
+
}
|
|
4117
|
+
function initialize() {
|
|
4118
|
+
if (!nodes) return;
|
|
4119
|
+
var i, n = nodes.length;
|
|
4120
|
+
strengths = new Array(n);
|
|
4121
|
+
xz = new Array(n);
|
|
4122
|
+
for (i = 0; i < n; ++i) {
|
|
4123
|
+
strengths[i] = isNaN(xz[i] = +x3(nodes[i], i, nodes)) ? 0 : +strength(nodes[i], i, nodes);
|
|
4124
|
+
}
|
|
4125
|
+
}
|
|
4126
|
+
force.initialize = function(_) {
|
|
4127
|
+
nodes = _;
|
|
4128
|
+
initialize();
|
|
4129
|
+
};
|
|
4130
|
+
force.strength = function(_) {
|
|
4131
|
+
return arguments.length ? (strength = typeof _ === "function" ? _ : constant_default5(+_), initialize(), force) : strength;
|
|
4132
|
+
};
|
|
4133
|
+
force.x = function(_) {
|
|
4134
|
+
return arguments.length ? (x3 = typeof _ === "function" ? _ : constant_default5(+_), initialize(), force) : x3;
|
|
4135
|
+
};
|
|
4136
|
+
return force;
|
|
4137
|
+
}
|
|
4138
|
+
|
|
4139
|
+
// node_modules/d3-force/src/y.js
|
|
4140
|
+
function y_default2(y3) {
|
|
4141
|
+
var strength = constant_default5(0.1), nodes, strengths, yz;
|
|
4142
|
+
if (typeof y3 !== "function") y3 = constant_default5(y3 == null ? 0 : +y3);
|
|
4143
|
+
function force(alpha) {
|
|
4144
|
+
for (var i = 0, n = nodes.length, node; i < n; ++i) {
|
|
4145
|
+
node = nodes[i], node.vy += (yz[i] - node.y) * strengths[i] * alpha;
|
|
4146
|
+
}
|
|
4147
|
+
}
|
|
4148
|
+
function initialize() {
|
|
4149
|
+
if (!nodes) return;
|
|
4150
|
+
var i, n = nodes.length;
|
|
4151
|
+
strengths = new Array(n);
|
|
4152
|
+
yz = new Array(n);
|
|
4153
|
+
for (i = 0; i < n; ++i) {
|
|
4154
|
+
strengths[i] = isNaN(yz[i] = +y3(nodes[i], i, nodes)) ? 0 : +strength(nodes[i], i, nodes);
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
force.initialize = function(_) {
|
|
4158
|
+
nodes = _;
|
|
4159
|
+
initialize();
|
|
4160
|
+
};
|
|
4161
|
+
force.strength = function(_) {
|
|
4162
|
+
return arguments.length ? (strength = typeof _ === "function" ? _ : constant_default5(+_), initialize(), force) : strength;
|
|
4163
|
+
};
|
|
4164
|
+
force.y = function(_) {
|
|
4165
|
+
return arguments.length ? (y3 = typeof _ === "function" ? _ : constant_default5(+_), initialize(), force) : y3;
|
|
4166
|
+
};
|
|
4167
|
+
return force;
|
|
4168
|
+
}
|
|
4169
|
+
|
|
3647
4170
|
// src/core/create-graph-layers.ts
|
|
3648
4171
|
function createGraphLayers(host) {
|
|
3649
4172
|
host.innerHTML = "";
|
|
@@ -3669,25 +4192,56 @@ function createGraphLayers(host) {
|
|
|
3669
4192
|
interactionRect.setAttribute("pointer-events", "all");
|
|
3670
4193
|
interactionLayer.appendChild(interactionRect);
|
|
3671
4194
|
const graphRoot = createGroup("viewport");
|
|
4195
|
+
const hoverLayerContainer = createGroup("hover-layer");
|
|
4196
|
+
const hoverLinks = createGroup("hover-links");
|
|
4197
|
+
const hoverNodes = createGroup("hover-nodes");
|
|
4198
|
+
const hoverNodeLabels = createGroup("hover-node-labels");
|
|
4199
|
+
const hoverLinkLabels = createGroup("hover-link-labels");
|
|
4200
|
+
hoverLayerContainer.append(hoverLinks, hoverNodes, hoverNodeLabels, hoverLinkLabels);
|
|
4201
|
+
const selectionLayerContainer = createGroup("selection-layer");
|
|
4202
|
+
const selectionLinks = createGroup("selection-links");
|
|
4203
|
+
const selectionNodes = createGroup("selection-nodes");
|
|
4204
|
+
const selectionNodeLabels = createGroup("selection-node-labels");
|
|
4205
|
+
const selectionLinkLabels = createGroup("selection-link-labels");
|
|
4206
|
+
selectionLayerContainer.append(selectionNodes, selectionLinks, selectionNodeLabels, selectionLinkLabels);
|
|
3672
4207
|
const layers = {
|
|
3673
4208
|
svg,
|
|
3674
4209
|
overlay,
|
|
3675
4210
|
interactionLayer,
|
|
3676
4211
|
interactionRect,
|
|
3677
4212
|
root: graphRoot,
|
|
3678
|
-
//
|
|
4213
|
+
// Base graph layers
|
|
3679
4214
|
links: createGroup("links"),
|
|
3680
|
-
linkLabels: createGroup("link-labels"),
|
|
3681
4215
|
nodeRings: createGroup("node-rings"),
|
|
3682
4216
|
nodes: createGroup("nodes"),
|
|
3683
|
-
nodeLabels: createGroup("node-labels")
|
|
4217
|
+
nodeLabels: createGroup("node-labels"),
|
|
4218
|
+
linkLabels: createGroup("link-labels"),
|
|
4219
|
+
// Dedicated interaction state layers with proper sub-layering
|
|
4220
|
+
hoverLayer: {
|
|
4221
|
+
container: hoverLayerContainer,
|
|
4222
|
+
links: hoverLinks,
|
|
4223
|
+
nodes: hoverNodes,
|
|
4224
|
+
nodeLabels: hoverNodeLabels,
|
|
4225
|
+
linkLabels: hoverLinkLabels
|
|
4226
|
+
},
|
|
4227
|
+
selectionLayer: {
|
|
4228
|
+
container: selectionLayerContainer,
|
|
4229
|
+
links: selectionLinks,
|
|
4230
|
+
nodes: selectionNodes,
|
|
4231
|
+
nodeLabels: selectionNodeLabels,
|
|
4232
|
+
linkLabels: selectionLinkLabels
|
|
4233
|
+
}
|
|
3684
4234
|
};
|
|
3685
4235
|
graphRoot.append(
|
|
3686
4236
|
layers.links,
|
|
3687
|
-
layers.linkLabels,
|
|
3688
4237
|
layers.nodeRings,
|
|
3689
4238
|
layers.nodes,
|
|
3690
|
-
layers.nodeLabels
|
|
4239
|
+
layers.nodeLabels,
|
|
4240
|
+
layers.linkLabels,
|
|
4241
|
+
layers.hoverLayer.container,
|
|
4242
|
+
// Hover elements on top
|
|
4243
|
+
layers.selectionLayer.container
|
|
4244
|
+
// Selected elements at very top
|
|
3691
4245
|
);
|
|
3692
4246
|
svg.appendChild(interactionLayer);
|
|
3693
4247
|
svg.appendChild(graphRoot);
|
|
@@ -3698,7 +4252,7 @@ function createGraphLayers(host) {
|
|
|
3698
4252
|
function createZoom({
|
|
3699
4253
|
svg,
|
|
3700
4254
|
root: root2,
|
|
3701
|
-
scaleExtent = [0.
|
|
4255
|
+
scaleExtent = [0.01, 10]
|
|
3702
4256
|
}) {
|
|
3703
4257
|
const svgSelection = select_default2(svg);
|
|
3704
4258
|
const rootSelection = select_default2(root2);
|
|
@@ -3739,198 +4293,336 @@ function createZoom({
|
|
|
3739
4293
|
}
|
|
3740
4294
|
|
|
3741
4295
|
// src/core/create-graph-simulation.ts
|
|
4296
|
+
function getAdaptiveDefaults(nodeCount) {
|
|
4297
|
+
if (nodeCount < 50) {
|
|
4298
|
+
return {
|
|
4299
|
+
alpha: 1,
|
|
4300
|
+
alphaDecay: 0.05,
|
|
4301
|
+
alphaMin: 1e-3,
|
|
4302
|
+
velocityDecay: 0.6,
|
|
4303
|
+
forces: {
|
|
4304
|
+
link: { strength: 0.5, distance: 200 },
|
|
4305
|
+
charge: { strength: -500 },
|
|
4306
|
+
center: { strength: 0.05 },
|
|
4307
|
+
collide: { strength: 0.8, iterations: 1 }
|
|
4308
|
+
}
|
|
4309
|
+
};
|
|
4310
|
+
} else if (nodeCount < 200) {
|
|
4311
|
+
return {
|
|
4312
|
+
alpha: 1,
|
|
4313
|
+
alphaDecay: 0.05,
|
|
4314
|
+
alphaMin: 1e-3,
|
|
4315
|
+
velocityDecay: 0.6,
|
|
4316
|
+
forces: {
|
|
4317
|
+
link: { strength: 0.5, distance: 200 },
|
|
4318
|
+
charge: { strength: -500 },
|
|
4319
|
+
center: { strength: 0.05 },
|
|
4320
|
+
collide: { strength: 0.7, iterations: 1 }
|
|
4321
|
+
}
|
|
4322
|
+
};
|
|
4323
|
+
} else {
|
|
4324
|
+
return {
|
|
4325
|
+
alpha: 1,
|
|
4326
|
+
alphaDecay: 0.05,
|
|
4327
|
+
alphaMin: 1e-3,
|
|
4328
|
+
velocityDecay: 0.6,
|
|
4329
|
+
forces: {
|
|
4330
|
+
link: { strength: 0.5, distance: 200 },
|
|
4331
|
+
charge: { strength: -500 },
|
|
4332
|
+
center: { strength: 0.05 },
|
|
4333
|
+
collide: { strength: 0.6, iterations: 1 }
|
|
4334
|
+
}
|
|
4335
|
+
};
|
|
4336
|
+
}
|
|
4337
|
+
}
|
|
4338
|
+
function warmupSimulation(simulation, ticks) {
|
|
4339
|
+
for (let i = 0; i < ticks; i++) {
|
|
4340
|
+
simulation.tick();
|
|
4341
|
+
}
|
|
4342
|
+
}
|
|
3742
4343
|
function createGraphSimulation(config) {
|
|
3743
4344
|
const centerX = config.width / 2;
|
|
3744
4345
|
const centerY = config.height / 2;
|
|
3745
|
-
const
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
4346
|
+
const nodeCount = config.nodes.length;
|
|
4347
|
+
const adaptiveDefaults = getAdaptiveDefaults(nodeCount);
|
|
4348
|
+
const enhancedConfig = config.config || {};
|
|
4349
|
+
const forces = enhancedConfig.forces || {};
|
|
4350
|
+
const useAdaptive = enhancedConfig.adaptive?.enabled !== false;
|
|
4351
|
+
seedNodePositions(config.nodes, config.width, config.height);
|
|
4352
|
+
const alpha = enhancedConfig.alpha ?? (useAdaptive ? adaptiveDefaults.alpha : 1);
|
|
4353
|
+
const alphaDecay = enhancedConfig.alphaDecay ?? (useAdaptive ? adaptiveDefaults.alphaDecay : 0.05);
|
|
4354
|
+
const alphaMin = enhancedConfig.alphaMin ?? (useAdaptive ? adaptiveDefaults.alphaMin : 1e-3);
|
|
4355
|
+
const velocityDecay = enhancedConfig.velocityDecay ?? (useAdaptive ? adaptiveDefaults.velocityDecay : 0.6);
|
|
4356
|
+
const simulation = simulation_default(config.nodes).alpha(alpha).alphaDecay(alphaDecay).alphaMin(alphaMin).velocityDecay(velocityDecay);
|
|
4357
|
+
if (forces.link?.enabled !== false) {
|
|
4358
|
+
const linkDistance = forces.link?.distance ?? (useAdaptive ? adaptiveDefaults.forces.link.distance : 200);
|
|
4359
|
+
const linkStrength = forces.link?.strength ?? (useAdaptive ? adaptiveDefaults.forces.link.strength : 0.5);
|
|
4360
|
+
simulation.force(
|
|
4361
|
+
"link",
|
|
4362
|
+
link_default(config.links).id((node) => node.id).distance(typeof linkDistance === "function" ? linkDistance : () => linkDistance).strength(typeof linkStrength === "function" ? linkStrength : () => linkStrength).iterations(forces.link?.iterations ?? 1)
|
|
4363
|
+
);
|
|
4364
|
+
}
|
|
4365
|
+
if (forces.charge?.enabled !== false) {
|
|
4366
|
+
const chargeStrength = forces.charge?.strength ?? (useAdaptive ? adaptiveDefaults.forces.charge.strength : -500);
|
|
4367
|
+
const chargeForce = manyBody_default().theta(forces.charge?.theta ?? 0.9).distanceMin(forces.charge?.distanceMin ?? 1).distanceMax(forces.charge?.distanceMax ?? Infinity);
|
|
4368
|
+
if (typeof chargeStrength === "function") {
|
|
4369
|
+
chargeForce.strength((d, _i) => chargeStrength(d));
|
|
4370
|
+
} else {
|
|
4371
|
+
chargeForce.strength(chargeStrength);
|
|
3751
4372
|
}
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
}
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
4373
|
+
simulation.force("charge", chargeForce);
|
|
4374
|
+
}
|
|
4375
|
+
if (forces.collide?.enabled !== false) {
|
|
4376
|
+
const collideRadius = forces.collide?.radius ?? ((node) => (node.style?.radius ?? 12) + 8);
|
|
4377
|
+
const collideStrength = forces.collide?.strength ?? (useAdaptive ? adaptiveDefaults.forces.collide.strength : 0.7);
|
|
4378
|
+
const collideForce = collide_default().strength(collideStrength).iterations(forces.collide?.iterations ?? adaptiveDefaults.forces.collide.iterations);
|
|
4379
|
+
if (typeof collideRadius === "function") {
|
|
4380
|
+
collideForce.radius((d, _i) => collideRadius(d));
|
|
4381
|
+
} else {
|
|
4382
|
+
collideForce.radius(collideRadius);
|
|
4383
|
+
}
|
|
4384
|
+
simulation.force("collide", collideForce);
|
|
4385
|
+
}
|
|
4386
|
+
if (forces.center?.enabled !== false) {
|
|
4387
|
+
const centerStrength = forces.center?.strength ?? (useAdaptive ? adaptiveDefaults.forces.center.strength : 0.05);
|
|
4388
|
+
simulation.force(
|
|
4389
|
+
"center",
|
|
4390
|
+
center_default(
|
|
4391
|
+
forces.center?.x ?? centerX,
|
|
4392
|
+
forces.center?.y ?? centerY
|
|
4393
|
+
).strength(centerStrength)
|
|
4394
|
+
);
|
|
4395
|
+
}
|
|
4396
|
+
if (forces.x?.enabled !== false) {
|
|
4397
|
+
const xForce = x_default2().strength(forces.x?.strength ?? 0.05);
|
|
4398
|
+
const xPosition = forces.x?.x;
|
|
4399
|
+
if (typeof xPosition === "function") {
|
|
4400
|
+
xForce.x((d, _i) => xPosition(d));
|
|
4401
|
+
} else {
|
|
4402
|
+
xForce.x(xPosition ?? centerX);
|
|
4403
|
+
}
|
|
4404
|
+
simulation.force("x", xForce);
|
|
4405
|
+
}
|
|
4406
|
+
if (forces.y?.enabled !== false) {
|
|
4407
|
+
const yForce = y_default2().strength(forces.y?.strength ?? 0.05);
|
|
4408
|
+
const yPosition = forces.y?.y;
|
|
4409
|
+
if (typeof yPosition === "function") {
|
|
4410
|
+
yForce.y((d, _i) => yPosition(d));
|
|
4411
|
+
} else {
|
|
4412
|
+
yForce.y(yPosition ?? centerY);
|
|
4413
|
+
}
|
|
4414
|
+
simulation.force("y", yForce);
|
|
4415
|
+
}
|
|
4416
|
+
if (enhancedConfig.warmup?.enabled !== false) {
|
|
4417
|
+
const warmupTicks = enhancedConfig.warmup?.ticks ?? (useAdaptive ? Math.min(100, nodeCount * 2) : 50);
|
|
4418
|
+
warmupSimulation(simulation, warmupTicks);
|
|
4419
|
+
}
|
|
3767
4420
|
return { simulation };
|
|
3768
4421
|
}
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
}
|
|
3774
|
-
function resolveControlsOrientation(orientation) {
|
|
3775
|
-
return orientation ?? "vertical";
|
|
3776
|
-
}
|
|
3777
|
-
function shouldRenderControl(config, key) {
|
|
3778
|
-
const show = config.show;
|
|
3779
|
-
if (!show) {
|
|
3780
|
-
return true;
|
|
3781
|
-
}
|
|
3782
|
-
const value = show[key];
|
|
3783
|
-
if (value === void 0) {
|
|
3784
|
-
return true;
|
|
4422
|
+
function seedNodePositions(nodes, containerWidth, containerHeight) {
|
|
4423
|
+
if (containerWidth <= 0 || containerHeight <= 0) {
|
|
4424
|
+
console.warn("\u{1F6AB} [seedNodePositions] Invalid container dimensions, skipping positioning");
|
|
4425
|
+
return;
|
|
3785
4426
|
}
|
|
3786
|
-
|
|
4427
|
+
const centerX = containerWidth / 2;
|
|
4428
|
+
const centerY = containerHeight / 2;
|
|
4429
|
+
const padding = 50;
|
|
4430
|
+
const maxRadius = Math.min(
|
|
4431
|
+
(containerWidth - padding * 2) / 2,
|
|
4432
|
+
(containerHeight - padding * 2) / 2
|
|
4433
|
+
);
|
|
4434
|
+
const nodeBasedRadius = Math.max(50, Math.min(200, nodes.length * 3));
|
|
4435
|
+
const seedRadius = Math.min(maxRadius, nodeBasedRadius);
|
|
4436
|
+
nodes.forEach(
|
|
4437
|
+
(node, index2) => {
|
|
4438
|
+
if (node.x != null && node.y != null && !isNaN(node.x) && !isNaN(node.y) && isFinite(node.x) && isFinite(node.y)) {
|
|
4439
|
+
return;
|
|
4440
|
+
}
|
|
4441
|
+
const angle = index2 / Math.max(nodes.length, 1) * Math.PI * 2 + (Math.random() - 0.5) * 0.5;
|
|
4442
|
+
const radius = seedRadius * (0.3 + Math.random() * 0.7);
|
|
4443
|
+
const x3 = centerX + Math.cos(angle) * radius;
|
|
4444
|
+
const y3 = centerY + Math.sin(angle) * radius;
|
|
4445
|
+
const nodeRadius = 12;
|
|
4446
|
+
const clampedX = Math.max(nodeRadius, Math.min(containerWidth - nodeRadius, x3));
|
|
4447
|
+
const clampedY = Math.max(nodeRadius, Math.min(containerHeight - nodeRadius, y3));
|
|
4448
|
+
node.x = clampedX;
|
|
4449
|
+
node.y = clampedY;
|
|
4450
|
+
}
|
|
4451
|
+
);
|
|
4452
|
+
nodes.forEach((node) => {
|
|
4453
|
+
node.initialX = node.x;
|
|
4454
|
+
node.initialY = node.y;
|
|
4455
|
+
});
|
|
3787
4456
|
}
|
|
3788
4457
|
|
|
3789
|
-
// src/
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
};
|
|
3808
|
-
function getControlIcon(icon) {
|
|
3809
|
-
const raw = ICON_MAP[icon];
|
|
3810
|
-
if (!raw) {
|
|
3811
|
-
throw new Error(`Icon not found: ${icon}`);
|
|
3812
|
-
}
|
|
3813
|
-
return raw.replace("<svg", '<svg class="pg-icon"');
|
|
4458
|
+
// src/utils/observe-resize.ts
|
|
4459
|
+
function observeResize(element, onResize) {
|
|
4460
|
+
const observer = new ResizeObserver(
|
|
4461
|
+
(entries) => {
|
|
4462
|
+
const entry = entries[0];
|
|
4463
|
+
if (!entry) {
|
|
4464
|
+
return;
|
|
4465
|
+
}
|
|
4466
|
+
const width = entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width;
|
|
4467
|
+
const height = entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height;
|
|
4468
|
+
if (width > 0 && height > 0) {
|
|
4469
|
+
onResize(width, height);
|
|
4470
|
+
}
|
|
4471
|
+
}
|
|
4472
|
+
);
|
|
4473
|
+
observer.observe(element);
|
|
4474
|
+
return () => {
|
|
4475
|
+
observer.disconnect();
|
|
4476
|
+
};
|
|
3814
4477
|
}
|
|
3815
4478
|
|
|
3816
|
-
// src/
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
4479
|
+
// src/utils/error-handler.ts
|
|
4480
|
+
var GraphError = class extends Error {
|
|
4481
|
+
code;
|
|
4482
|
+
context;
|
|
4483
|
+
recoverable;
|
|
4484
|
+
constructor(message, code, context, recoverable = false) {
|
|
4485
|
+
super(message);
|
|
4486
|
+
this.name = "GraphError";
|
|
4487
|
+
this.code = code;
|
|
4488
|
+
this.context = context;
|
|
4489
|
+
this.recoverable = recoverable;
|
|
4490
|
+
}
|
|
4491
|
+
};
|
|
4492
|
+
var ErrorHandler = class {
|
|
4493
|
+
static isDestroyed = false;
|
|
4494
|
+
/**
|
|
4495
|
+
* Handle errors with appropriate logging and recovery
|
|
4496
|
+
*/
|
|
4497
|
+
static handle(error, context, fallback) {
|
|
4498
|
+
if (this.isDestroyed) return;
|
|
4499
|
+
const graphError = error instanceof GraphError ? error : new GraphError(
|
|
4500
|
+
error.message,
|
|
4501
|
+
"UNKNOWN_ERROR",
|
|
4502
|
+
context,
|
|
4503
|
+
true
|
|
4504
|
+
);
|
|
4505
|
+
console.error(`[Polly Graph] ${graphError.code}:`, {
|
|
4506
|
+
message: graphError.message,
|
|
4507
|
+
context: graphError.context,
|
|
4508
|
+
stack: graphError.stack
|
|
4509
|
+
});
|
|
4510
|
+
if (graphError.recoverable && fallback) {
|
|
4511
|
+
try {
|
|
4512
|
+
fallback();
|
|
4513
|
+
} catch (fallbackError) {
|
|
4514
|
+
console.error("[Polly Graph] Fallback failed:", fallbackError);
|
|
4515
|
+
}
|
|
4516
|
+
} else if (!graphError.recoverable) {
|
|
4517
|
+
throw graphError;
|
|
4518
|
+
}
|
|
4519
|
+
}
|
|
4520
|
+
/**
|
|
4521
|
+
* Wrap async operations with error handling
|
|
4522
|
+
*/
|
|
4523
|
+
static async wrapAsync(operation, context, fallback) {
|
|
4524
|
+
try {
|
|
4525
|
+
return await operation();
|
|
4526
|
+
} catch (error) {
|
|
4527
|
+
this.handle(error, context, fallback ? () => fallback() : void 0);
|
|
4528
|
+
if (fallback) {
|
|
4529
|
+
return fallback();
|
|
4530
|
+
}
|
|
4531
|
+
throw error;
|
|
4532
|
+
}
|
|
4533
|
+
}
|
|
4534
|
+
/**
|
|
4535
|
+
* Wrap synchronous operations with error handling
|
|
4536
|
+
*/
|
|
4537
|
+
static wrap(operation, context, fallback) {
|
|
4538
|
+
try {
|
|
4539
|
+
return operation();
|
|
4540
|
+
} catch (error) {
|
|
4541
|
+
this.handle(error, context, fallback);
|
|
4542
|
+
return fallback ? fallback() : void 0;
|
|
4543
|
+
}
|
|
4544
|
+
}
|
|
4545
|
+
/**
|
|
4546
|
+
* Safe DOM operation wrapper
|
|
4547
|
+
*/
|
|
4548
|
+
static safeDOMOperation(operation, context, fallback) {
|
|
4549
|
+
if (!document || this.isDestroyed) {
|
|
4550
|
+
if (fallback) {
|
|
4551
|
+
return fallback();
|
|
4552
|
+
}
|
|
4553
|
+
return void 0;
|
|
4554
|
+
}
|
|
4555
|
+
return this.wrap(operation, {
|
|
4556
|
+
...context,
|
|
4557
|
+
operation: `DOM: ${context.operation}`
|
|
4558
|
+
}, fallback);
|
|
4559
|
+
}
|
|
4560
|
+
/**
|
|
4561
|
+
* Safe D3 operation wrapper
|
|
4562
|
+
*/
|
|
4563
|
+
static safeD3Operation(operation, context, fallback) {
|
|
4564
|
+
return this.wrap(operation, {
|
|
4565
|
+
...context,
|
|
4566
|
+
operation: `D3: ${context.operation}`
|
|
4567
|
+
}, fallback);
|
|
4568
|
+
}
|
|
4569
|
+
/**
|
|
4570
|
+
* Create recoverable error for non-critical failures
|
|
4571
|
+
*/
|
|
4572
|
+
static createRecoverableError(message, code, context) {
|
|
4573
|
+
return new GraphError(message, code, context, true);
|
|
4574
|
+
}
|
|
4575
|
+
/**
|
|
4576
|
+
* Create non-recoverable error for critical failures
|
|
4577
|
+
*/
|
|
4578
|
+
static createCriticalError(message, code, context) {
|
|
4579
|
+
return new GraphError(message, code, context, false);
|
|
4580
|
+
}
|
|
4581
|
+
/**
|
|
4582
|
+
* Validate and handle D3 selection operations
|
|
4583
|
+
*/
|
|
4584
|
+
static validateSelection(selection2, context, operation) {
|
|
4585
|
+
if (!selection2 || !selection2.size || selection2.size() === 0) {
|
|
4586
|
+
this.handle(
|
|
4587
|
+
this.createRecoverableError(
|
|
4588
|
+
"D3 selection is empty",
|
|
4589
|
+
"SELECTION_EMPTY",
|
|
4590
|
+
context
|
|
4591
|
+
),
|
|
4592
|
+
context
|
|
4593
|
+
);
|
|
3821
4594
|
return;
|
|
3822
4595
|
}
|
|
3823
|
-
|
|
3824
|
-
root2.className = "pg-controls";
|
|
3825
|
-
const position = resolveControlsPosition(config.position);
|
|
3826
|
-
root2.classList.add(`pg-pos-${position}`);
|
|
3827
|
-
const orientation = resolveControlsOrientation(config.orientation);
|
|
3828
|
-
root2.classList.add(`pg-orient-${orientation}`);
|
|
3829
|
-
if (config.offset) {
|
|
3830
|
-
root2.style.setProperty("--pg-controls-offset-x", `${config.offset.x}px`);
|
|
3831
|
-
root2.style.setProperty("--pg-controls-offset-y", `${config.offset.y}px`);
|
|
3832
|
-
}
|
|
3833
|
-
appendControls(root2, config, graph);
|
|
3834
|
-
overlay.appendChild(root2);
|
|
4596
|
+
this.safeD3Operation(() => operation(selection2), context);
|
|
3835
4597
|
}
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
4598
|
+
/**
|
|
4599
|
+
* Handle simulation errors with graceful degradation
|
|
4600
|
+
*/
|
|
4601
|
+
static handleSimulationError(error, context, simulation) {
|
|
4602
|
+
const graphError = this.createRecoverableError(
|
|
4603
|
+
`Simulation error: ${error.message}`,
|
|
4604
|
+
"SIMULATION_ERROR",
|
|
4605
|
+
context
|
|
4606
|
+
);
|
|
4607
|
+
this.handle(graphError, context, () => {
|
|
4608
|
+
if (simulation && typeof simulation.stop === "function") {
|
|
4609
|
+
simulation.stop();
|
|
3846
4610
|
}
|
|
3847
4611
|
});
|
|
3848
4612
|
}
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
const wrapper = document.createElement("div");
|
|
3855
|
-
wrapper.className = "pg-icon-wrapper";
|
|
3856
|
-
wrapper.innerHTML = getControlIcon(type);
|
|
3857
|
-
const svg = wrapper.querySelector("svg");
|
|
3858
|
-
if (svg) {
|
|
3859
|
-
svg.classList.add("pg-icon");
|
|
3860
|
-
button.appendChild(svg);
|
|
3861
|
-
}
|
|
3862
|
-
button.addEventListener("click", onClick);
|
|
3863
|
-
return button;
|
|
3864
|
-
}
|
|
3865
|
-
function destroy() {
|
|
3866
|
-
if (!root2) {
|
|
3867
|
-
return;
|
|
3868
|
-
}
|
|
3869
|
-
if (root2.parentNode === overlay) {
|
|
3870
|
-
overlay.removeChild(root2);
|
|
3871
|
-
}
|
|
3872
|
-
root2 = null;
|
|
3873
|
-
}
|
|
3874
|
-
return { mount, destroy };
|
|
3875
|
-
}
|
|
3876
|
-
|
|
3877
|
-
// src/assets/caret.svg?raw
|
|
3878
|
-
var caret_default = '<svg\n xmlns="http://www.w3.org/2000/svg"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n stroke-width="2"\n stroke-linecap="round"\n stroke-linejoin="round"\n>\n <path d="M9 20L16.5 12L9 4" />\n</svg>';
|
|
3879
|
-
|
|
3880
|
-
// src/legends/graph-legend-icon.ts
|
|
3881
|
-
var LEGEND_ICON_MAP = { caret: caret_default };
|
|
3882
|
-
function getLegendIcon(icon) {
|
|
3883
|
-
const raw = LEGEND_ICON_MAP[icon];
|
|
3884
|
-
if (!raw) {
|
|
3885
|
-
throw new Error(`Legend icon not found: ${icon}`);
|
|
3886
|
-
}
|
|
3887
|
-
return raw.replace("<svg", '<svg class="pg-icon"');
|
|
3888
|
-
}
|
|
3889
|
-
|
|
3890
|
-
// src/legends/create-graph-legends.ts
|
|
3891
|
-
function createGraphLegend(overlay, config) {
|
|
3892
|
-
const legendWrapper = document.createElement("div");
|
|
3893
|
-
legendWrapper.className = "pg-legend";
|
|
3894
|
-
const position = config.position || "bottom-right";
|
|
3895
|
-
legendWrapper.classList.add(`pg-pos-${position}`);
|
|
3896
|
-
if (config.defaultExpanded === false) {
|
|
3897
|
-
legendWrapper.classList.add("pg-is-collapsed");
|
|
4613
|
+
/**
|
|
4614
|
+
* Mark error handler as destroyed to prevent further operations
|
|
4615
|
+
*/
|
|
4616
|
+
static destroy() {
|
|
4617
|
+
this.isDestroyed = true;
|
|
3898
4618
|
}
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
toggleBtn.onclick = (e) => {
|
|
3905
|
-
e.stopPropagation();
|
|
3906
|
-
legendWrapper.classList.toggle("pg-is-collapsed");
|
|
3907
|
-
};
|
|
3908
|
-
legendWrapper.appendChild(toggleBtn);
|
|
4619
|
+
/**
|
|
4620
|
+
* Reset error handler state
|
|
4621
|
+
*/
|
|
4622
|
+
static reset() {
|
|
4623
|
+
this.isDestroyed = false;
|
|
3909
4624
|
}
|
|
3910
|
-
|
|
3911
|
-
body.className = "pg-legend-body";
|
|
3912
|
-
const list = document.createElement("ul");
|
|
3913
|
-
list.className = "pg-legend-list";
|
|
3914
|
-
config.items.forEach((item) => {
|
|
3915
|
-
const listItem = document.createElement("li");
|
|
3916
|
-
listItem.className = "pg-legend-item";
|
|
3917
|
-
const swatch = document.createElement("span");
|
|
3918
|
-
swatch.className = `pg-legend-swatch is-${item.shape || "circle"}`;
|
|
3919
|
-
swatch.style.backgroundColor = item.color;
|
|
3920
|
-
const label = document.createElement("span");
|
|
3921
|
-
label.className = "pg-legend-label";
|
|
3922
|
-
label.innerText = item.label;
|
|
3923
|
-
listItem.appendChild(swatch);
|
|
3924
|
-
listItem.appendChild(label);
|
|
3925
|
-
list.appendChild(listItem);
|
|
3926
|
-
});
|
|
3927
|
-
body.appendChild(list);
|
|
3928
|
-
legendWrapper.appendChild(body);
|
|
3929
|
-
overlay.appendChild(legendWrapper);
|
|
3930
|
-
return () => {
|
|
3931
|
-
if (legendWrapper.parentNode === overlay) overlay.removeChild(legendWrapper);
|
|
3932
|
-
};
|
|
3933
|
-
}
|
|
4625
|
+
};
|
|
3934
4626
|
|
|
3935
4627
|
// src/utils/resolve-link-style.ts
|
|
3936
4628
|
var DEFAULT_LINK_STYLE = {
|
|
@@ -3951,7 +4643,7 @@ var DEFAULT_LINK_STYLE = {
|
|
|
3951
4643
|
borderWidth: 1.5,
|
|
3952
4644
|
borderRadius: 4,
|
|
3953
4645
|
textColor: "color-mix(in srgb, #8E42EE, #000000 40%)",
|
|
3954
|
-
fontSize:
|
|
4646
|
+
fontSize: 10,
|
|
3955
4647
|
paddingX: 8,
|
|
3956
4648
|
paddingY: 4,
|
|
3957
4649
|
height: 24
|
|
@@ -4046,6 +4738,27 @@ function createArrowMarker(params) {
|
|
|
4046
4738
|
}
|
|
4047
4739
|
|
|
4048
4740
|
// src/renderer/links.ts
|
|
4741
|
+
function getShortenedSourcePoint(link, style) {
|
|
4742
|
+
const source = link.source;
|
|
4743
|
+
const target = link.target;
|
|
4744
|
+
const sourceX = source.x ?? 0;
|
|
4745
|
+
const sourceY = source.y ?? 0;
|
|
4746
|
+
const targetX = target.x ?? 0;
|
|
4747
|
+
const targetY = target.y ?? 0;
|
|
4748
|
+
const dx = targetX - sourceX;
|
|
4749
|
+
const dy = targetY - sourceY;
|
|
4750
|
+
const distance = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
4751
|
+
const sourceRadius = source.style?.radius ?? 12;
|
|
4752
|
+
const sourceStrokeWidth = source.style?.strokeWidth ?? 1.5;
|
|
4753
|
+
const linkStrokeCompensation = style.strokeWidth / 2;
|
|
4754
|
+
const nodeStrokeOffset = sourceStrokeWidth / 2;
|
|
4755
|
+
const visualSpacing = 2;
|
|
4756
|
+
const offset = sourceRadius + nodeStrokeOffset + linkStrokeCompensation + visualSpacing;
|
|
4757
|
+
return {
|
|
4758
|
+
x: sourceX + dx / distance * offset,
|
|
4759
|
+
y: sourceY + dy / distance * offset
|
|
4760
|
+
};
|
|
4761
|
+
}
|
|
4049
4762
|
function getShortenedTargetPoint(link, style) {
|
|
4050
4763
|
const source = link.source;
|
|
4051
4764
|
const target = link.target;
|
|
@@ -4057,10 +4770,12 @@ function getShortenedTargetPoint(link, style) {
|
|
|
4057
4770
|
const dy = targetY - sourceY;
|
|
4058
4771
|
const distance = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
4059
4772
|
const targetRadius = target.style?.radius ?? 12;
|
|
4773
|
+
const targetStrokeWidth = target.style?.strokeWidth ?? 1.5;
|
|
4060
4774
|
const arrowLength = style.arrow.enabled ? style.arrow.size * 2 : 0;
|
|
4061
|
-
const
|
|
4775
|
+
const linkStrokeCompensation = style.strokeWidth / 2;
|
|
4776
|
+
const nodeStrokeOffset = targetStrokeWidth / 2;
|
|
4062
4777
|
const visualSpacing = 2;
|
|
4063
|
-
const offset = targetRadius + arrowLength +
|
|
4778
|
+
const offset = targetRadius + nodeStrokeOffset + arrowLength + linkStrokeCompensation + visualSpacing;
|
|
4064
4779
|
return {
|
|
4065
4780
|
x: targetX - dx / distance * offset,
|
|
4066
4781
|
y: targetY - dy / distance * offset
|
|
@@ -4083,12 +4798,6 @@ function getLinkKey(link) {
|
|
|
4083
4798
|
function renderLinks(ctx, links) {
|
|
4084
4799
|
const renderableLinks = createRenderableLinks(ctx, links);
|
|
4085
4800
|
const linkSelection = ctx.root.select('[data-layer="links"]').selectAll("line").data(renderableLinks, (item) => getLinkKey(item.link)).join("line").attr("class", "graph-link").attr("stroke", (item) => item.style.stroke).attr("stroke-width", (item) => item.style.strokeWidth).attr("opacity", (item) => item.style.opacity).attr("marker-end", (item) => item.markerEnd).style("pointer-events", "stroke").style("cursor", "pointer");
|
|
4086
|
-
const labelSelection = ctx.root.selectAll(".link-label");
|
|
4087
|
-
linkSelection.on("mouseenter.label-hover", (_event, d) => {
|
|
4088
|
-
labelSelection.filter((labelItem) => labelItem.link === d.link && labelItem.style.label.visibility === "hover").interrupt().transition().duration(200).style("opacity", 1);
|
|
4089
|
-
}).on("mouseleave.label-hover", (_event, d) => {
|
|
4090
|
-
labelSelection.filter((labelItem) => labelItem.link === d.link && labelItem.style.label.visibility === "hover").interrupt().transition().duration(200).style("opacity", 0);
|
|
4091
|
-
});
|
|
4092
4801
|
return linkSelection;
|
|
4093
4802
|
}
|
|
4094
4803
|
|
|
@@ -4128,7 +4837,7 @@ function renderNodeLabels(ctx, nodes) {
|
|
|
4128
4837
|
"middle"
|
|
4129
4838
|
).attr(
|
|
4130
4839
|
"font-size",
|
|
4131
|
-
|
|
4840
|
+
9
|
|
4132
4841
|
).attr(
|
|
4133
4842
|
"fill",
|
|
4134
4843
|
(node) => node.style?.textColor ?? "#ffffff"
|
|
@@ -4181,29 +4890,238 @@ function renderLinkLabels(params, links) {
|
|
|
4181
4890
|
const visibility = item.style.label.visibility ?? "always";
|
|
4182
4891
|
return visibility === "always" ? "auto" : "none";
|
|
4183
4892
|
}).style("cursor", "pointer");
|
|
4184
|
-
labelSelection.selectAll("rect").data((item) => [item]).join("rect").attr("rx", (item) => item.style.label.borderRadius).attr("ry", (item) => item.style.label.borderRadius).attr("
|
|
4185
|
-
labelSelection.selectAll("text").data((item) => [item]).join("text").attr("text-anchor", "middle").attr("dominant-baseline", "middle").attr("font-size", (item) => item.style.label.fontSize).attr("fill", (item) => item.style.label.textColor).text((item) => item.link.label ?? "");
|
|
4893
|
+
labelSelection.selectAll("rect").data((item) => [item]).join("rect").attr("rx", (item) => item.style.label.borderRadius).attr("ry", (item) => item.style.label.borderRadius).attr("fill", (item) => item.style.label.backgroundFill).attr("stroke", (item) => item.style.label.borderColor).attr("stroke-width", (item) => item.style.label.borderWidth);
|
|
4894
|
+
const textSelection = labelSelection.selectAll("text").data((item) => [item]).join("text").attr("text-anchor", "middle").attr("dominant-baseline", "middle").attr("font-size", (item) => item.style.label.fontSize).attr("fill", (item) => item.style.label.textColor).text((item) => item.link.label ?? "");
|
|
4895
|
+
textSelection.each(function(item) {
|
|
4896
|
+
const textElement = this;
|
|
4897
|
+
const parentGroup = textElement.parentNode;
|
|
4898
|
+
const rectElement = parentGroup.querySelector("rect");
|
|
4899
|
+
if (rectElement) {
|
|
4900
|
+
try {
|
|
4901
|
+
const bbox = textElement.getBBox();
|
|
4902
|
+
const paddingX = item.style.label.paddingX;
|
|
4903
|
+
const paddingY = item.style.label.paddingY;
|
|
4904
|
+
rectElement.setAttribute("width", String(bbox.width + paddingX * 2));
|
|
4905
|
+
rectElement.setAttribute("height", String(bbox.height + paddingY * 2));
|
|
4906
|
+
rectElement.setAttribute("x", String(bbox.x - paddingX));
|
|
4907
|
+
rectElement.setAttribute("y", String(bbox.y - paddingY));
|
|
4908
|
+
} catch {
|
|
4909
|
+
const text = item.link.label ?? "";
|
|
4910
|
+
const fontSize = item.style.label.fontSize;
|
|
4911
|
+
const textWidth = text.length * fontSize * 0.6;
|
|
4912
|
+
const rectWidth = textWidth + item.style.label.paddingX * 2;
|
|
4913
|
+
rectElement.setAttribute("width", String(rectWidth));
|
|
4914
|
+
rectElement.setAttribute("height", String(item.style.label.height));
|
|
4915
|
+
rectElement.setAttribute("x", String(-rectWidth / 2));
|
|
4916
|
+
rectElement.setAttribute("y", String(-item.style.label.height / 2));
|
|
4917
|
+
}
|
|
4918
|
+
}
|
|
4919
|
+
});
|
|
4186
4920
|
return labelSelection;
|
|
4187
4921
|
}
|
|
4188
4922
|
|
|
4923
|
+
// src/core/render-pipeline.ts
|
|
4924
|
+
var RenderPipeline = class {
|
|
4925
|
+
constructor(manager) {
|
|
4926
|
+
this.manager = manager;
|
|
4927
|
+
}
|
|
4928
|
+
manager;
|
|
4929
|
+
/**
|
|
4930
|
+
* Execute the complete render pipeline
|
|
4931
|
+
*/
|
|
4932
|
+
async execute() {
|
|
4933
|
+
this.cleanup();
|
|
4934
|
+
this.manager.initializeManagers();
|
|
4935
|
+
const layers = this.createDOMStructure();
|
|
4936
|
+
this.setupResizeHandling();
|
|
4937
|
+
this.initializeZoom(layers);
|
|
4938
|
+
const selections = this.renderComponents(layers);
|
|
4939
|
+
await this.initializeSimulation(selections);
|
|
4940
|
+
return selections;
|
|
4941
|
+
}
|
|
4942
|
+
/**
|
|
4943
|
+
* Cleanup previous render
|
|
4944
|
+
*/
|
|
4945
|
+
cleanup() {
|
|
4946
|
+
ErrorHandler.safeDOMOperation(() => {
|
|
4947
|
+
if (this.manager.svgElement && this.manager.config.container.contains(this.manager.svgElement)) {
|
|
4948
|
+
this.manager.config.container.removeChild(this.manager.svgElement);
|
|
4949
|
+
}
|
|
4950
|
+
}, { operation: "remove existing SVG", component: "render-pipeline" });
|
|
4951
|
+
}
|
|
4952
|
+
/**
|
|
4953
|
+
* Create DOM structure and layers
|
|
4954
|
+
*/
|
|
4955
|
+
createDOMStructure() {
|
|
4956
|
+
const layers = createGraphLayers(this.manager.config.container);
|
|
4957
|
+
this.manager.layers = layers;
|
|
4958
|
+
this.manager.svgElement = layers.svg;
|
|
4959
|
+
this.manager.rootGroup = layers.root;
|
|
4960
|
+
const initialWidth = this.manager.config.container.clientWidth;
|
|
4961
|
+
const initialHeight = this.manager.config.container.clientHeight;
|
|
4962
|
+
this.manager.dimensions = { width: initialWidth, height: initialHeight };
|
|
4963
|
+
if (initialWidth > 0 && initialHeight > 0) {
|
|
4964
|
+
layers.svg.setAttribute("width", String(initialWidth));
|
|
4965
|
+
layers.svg.setAttribute("height", String(initialHeight));
|
|
4966
|
+
layers.interactionRect.setAttribute("width", String(initialWidth));
|
|
4967
|
+
layers.interactionRect.setAttribute("height", String(initialHeight));
|
|
4968
|
+
}
|
|
4969
|
+
return layers;
|
|
4970
|
+
}
|
|
4971
|
+
/**
|
|
4972
|
+
* Setup resize handling
|
|
4973
|
+
*/
|
|
4974
|
+
setupResizeHandling() {
|
|
4975
|
+
const cleanupResize = observeResize(this.manager.config.container, (width, height) => {
|
|
4976
|
+
this.manager.dimensions = { width, height };
|
|
4977
|
+
if (this.manager.svgElement && this.manager.layers) {
|
|
4978
|
+
this.manager.svgElement.setAttribute("width", String(width));
|
|
4979
|
+
this.manager.svgElement.setAttribute("height", String(height));
|
|
4980
|
+
this.manager.layers.interactionRect.setAttribute("width", String(width));
|
|
4981
|
+
this.manager.layers.interactionRect.setAttribute("height", String(height));
|
|
4982
|
+
}
|
|
4983
|
+
if (this.manager.simulation) {
|
|
4984
|
+
this.manager.simulation.force("center", center_default(width / 2, height / 2));
|
|
4985
|
+
if (this.manager.simulationPaused && width > 0 && height > 0) {
|
|
4986
|
+
this.positionNodesWithValidDimensions(width, height);
|
|
4987
|
+
this.manager.reheatSimulation(0.3);
|
|
4988
|
+
this.manager.simulationPaused = false;
|
|
4989
|
+
} else if (!this.manager.simulationPaused && this.manager.simulation.alpha() < 0.2) {
|
|
4990
|
+
this.manager.reheatSimulation(0.1);
|
|
4991
|
+
}
|
|
4992
|
+
}
|
|
4993
|
+
if (this.manager.timerManager && this.manager.fitViewCallback) {
|
|
4994
|
+
this.manager.timerManager.debounce("fit-view-resize", () => {
|
|
4995
|
+
if (this.manager.fitViewCallback) {
|
|
4996
|
+
this.manager.fitViewCallback();
|
|
4997
|
+
}
|
|
4998
|
+
}, 150);
|
|
4999
|
+
}
|
|
5000
|
+
});
|
|
5001
|
+
this.manager.addCleanup(cleanupResize);
|
|
5002
|
+
}
|
|
5003
|
+
/**
|
|
5004
|
+
* Initialize zoom behavior
|
|
5005
|
+
*/
|
|
5006
|
+
initializeZoom(layers) {
|
|
5007
|
+
const zoomResult = createZoom({
|
|
5008
|
+
svg: layers.svg,
|
|
5009
|
+
interactionLayer: layers.interactionLayer,
|
|
5010
|
+
root: layers.root
|
|
5011
|
+
});
|
|
5012
|
+
this.manager.zoomBehavior = zoomResult.behavior;
|
|
5013
|
+
this.manager.addCleanup(zoomResult.cleanup);
|
|
5014
|
+
}
|
|
5015
|
+
/**
|
|
5016
|
+
* Render graph components
|
|
5017
|
+
*/
|
|
5018
|
+
renderComponents(layers) {
|
|
5019
|
+
const root2 = select_default2(layers.root);
|
|
5020
|
+
const renderContext = {
|
|
5021
|
+
svg: layers.svg,
|
|
5022
|
+
root: root2,
|
|
5023
|
+
interaction: this.manager.config.interaction
|
|
5024
|
+
};
|
|
5025
|
+
const linkSelection = renderLinks(renderContext, this.manager.config.links);
|
|
5026
|
+
const linkLabelSelection = renderLinkLabels(renderContext, this.manager.config.links);
|
|
5027
|
+
const nodeSelection = renderNodes(renderContext, this.manager.config.nodes);
|
|
5028
|
+
const labelSelection = renderNodeLabels(renderContext, this.manager.config.nodes);
|
|
5029
|
+
return { linkSelection, linkLabelSelection, nodeSelection, labelSelection };
|
|
5030
|
+
}
|
|
5031
|
+
/**
|
|
5032
|
+
* Position nodes when valid dimensions become available
|
|
5033
|
+
*/
|
|
5034
|
+
positionNodesWithValidDimensions(width, height) {
|
|
5035
|
+
const centerX = width / 2;
|
|
5036
|
+
const centerY = height / 2;
|
|
5037
|
+
const padding = 50;
|
|
5038
|
+
const maxRadius = Math.min(
|
|
5039
|
+
(width - padding * 2) / 2,
|
|
5040
|
+
(height - padding * 2) / 2
|
|
5041
|
+
);
|
|
5042
|
+
const nodeBasedRadius = Math.max(50, Math.min(200, this.manager.config.nodes.length * 3));
|
|
5043
|
+
const seedRadius = Math.min(maxRadius, nodeBasedRadius);
|
|
5044
|
+
this.manager.config.nodes.forEach((node, index2) => {
|
|
5045
|
+
if (node.x == null || node.y == null) {
|
|
5046
|
+
const angle = index2 / Math.max(this.manager.config.nodes.length, 1) * Math.PI * 2 + (Math.random() - 0.5) * 0.5;
|
|
5047
|
+
const radius = seedRadius * (0.3 + Math.random() * 0.7);
|
|
5048
|
+
const x3 = centerX + Math.cos(angle) * radius;
|
|
5049
|
+
const y3 = centerY + Math.sin(angle) * radius;
|
|
5050
|
+
const nodeRadius = 12;
|
|
5051
|
+
node.x = Math.max(nodeRadius, Math.min(width - nodeRadius, x3));
|
|
5052
|
+
node.y = Math.max(nodeRadius, Math.min(height - nodeRadius, y3));
|
|
5053
|
+
}
|
|
5054
|
+
});
|
|
5055
|
+
}
|
|
5056
|
+
/**
|
|
5057
|
+
* Initialize and configure simulation
|
|
5058
|
+
*/
|
|
5059
|
+
async initializeSimulation(_selections) {
|
|
5060
|
+
const simulationConfig = {
|
|
5061
|
+
nodes: this.manager.config.nodes,
|
|
5062
|
+
links: this.manager.config.links,
|
|
5063
|
+
width: this.manager.dimensions.width || this.manager.config.container.clientWidth,
|
|
5064
|
+
height: this.manager.dimensions.height || this.manager.config.container.clientHeight,
|
|
5065
|
+
config: this.manager.config.simulation
|
|
5066
|
+
};
|
|
5067
|
+
try {
|
|
5068
|
+
const simulationResult = createGraphSimulation(simulationConfig);
|
|
5069
|
+
this.manager.simulation = simulationResult.simulation;
|
|
5070
|
+
const centerX = simulationConfig.width / 2;
|
|
5071
|
+
const centerY = simulationConfig.height / 2;
|
|
5072
|
+
this.manager.simulation.force("center", center_default(centerX, centerY));
|
|
5073
|
+
if (simulationConfig.width > 0 && simulationConfig.height > 0) {
|
|
5074
|
+
this.manager.reheatSimulation(0.3);
|
|
5075
|
+
this.manager.simulationPaused = false;
|
|
5076
|
+
this.manager.needsImmediateFitView = true;
|
|
5077
|
+
} else {
|
|
5078
|
+
this.manager.simulation.stop();
|
|
5079
|
+
this.manager.simulationPaused = true;
|
|
5080
|
+
}
|
|
5081
|
+
} catch (error) {
|
|
5082
|
+
console.error("[RenderPipeline] Simulation creation failed:", error);
|
|
5083
|
+
ErrorHandler.handleSimulationError(
|
|
5084
|
+
error,
|
|
5085
|
+
{ operation: "create simulation", component: "render-pipeline", data: simulationConfig },
|
|
5086
|
+
this.manager.simulation ?? void 0
|
|
5087
|
+
);
|
|
5088
|
+
}
|
|
5089
|
+
}
|
|
5090
|
+
};
|
|
5091
|
+
|
|
4189
5092
|
// src/interactions/create-drag-behavior.ts
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
5093
|
+
var DRAG_ALPHA_TARGET = 0.3;
|
|
5094
|
+
function createDragBehavior(simulation, onDragStart, canvasBounds) {
|
|
5095
|
+
let hasActuallyDragged = false;
|
|
5096
|
+
return drag_default().on(
|
|
5097
|
+
"start",
|
|
5098
|
+
(event, node) => {
|
|
5099
|
+
hasActuallyDragged = false;
|
|
5100
|
+
node.fx = node.x;
|
|
5101
|
+
node.fy = node.y;
|
|
5102
|
+
}
|
|
5103
|
+
).on("drag", (event, node) => {
|
|
5104
|
+
if (!hasActuallyDragged) {
|
|
5105
|
+
hasActuallyDragged = true;
|
|
5106
|
+
if (!event.active) {
|
|
5107
|
+
simulation.alphaTarget(DRAG_ALPHA_TARGET).restart();
|
|
5108
|
+
}
|
|
5109
|
+
onDragStart?.();
|
|
5110
|
+
}
|
|
5111
|
+
node.fx = event.x;
|
|
5112
|
+
node.fy = event.y;
|
|
5113
|
+
}).on("end", (event, node) => {
|
|
5114
|
+
if (hasActuallyDragged && !event.active) {
|
|
5115
|
+
const isOutside = canvasBounds && (event.x < 0 || event.x > canvasBounds.width || event.y < 0 || event.y > canvasBounds.height);
|
|
5116
|
+
if (isOutside) {
|
|
5117
|
+
simulation.alphaTarget(0.1);
|
|
5118
|
+
} else {
|
|
5119
|
+
simulation.alphaTarget(0);
|
|
5120
|
+
}
|
|
5121
|
+
}
|
|
5122
|
+
node.fx = null;
|
|
5123
|
+
node.fy = null;
|
|
5124
|
+
hasActuallyDragged = false;
|
|
4207
5125
|
});
|
|
4208
5126
|
}
|
|
4209
5127
|
|
|
@@ -4227,18 +5145,253 @@ function createNodeHover(nodeSelection, hoverStyle) {
|
|
|
4227
5145
|
const svgElement = firstNode.ownerSVGElement;
|
|
4228
5146
|
if (!svgElement) return;
|
|
4229
5147
|
const root2 = select_default2(svgElement);
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
}
|
|
4238
|
-
|
|
4239
|
-
|
|
5148
|
+
function clearAllHoverLayers() {
|
|
5149
|
+
const hoverNodesLayer = root2.select('[data-layer="hover-nodes"]').node();
|
|
5150
|
+
const nodesLayer = root2.select('[data-layer="nodes"]').node();
|
|
5151
|
+
if (hoverNodesLayer && nodesLayer) {
|
|
5152
|
+
while (hoverNodesLayer.firstChild) {
|
|
5153
|
+
nodesLayer.appendChild(hoverNodesLayer.firstChild);
|
|
5154
|
+
}
|
|
5155
|
+
}
|
|
5156
|
+
const hoverNodeLabelsLayer = root2.select('[data-layer="hover-node-labels"]').node();
|
|
5157
|
+
const nodeLabelsLayer = root2.select('[data-layer="node-labels"]').node();
|
|
5158
|
+
if (hoverNodeLabelsLayer && nodeLabelsLayer) {
|
|
5159
|
+
while (hoverNodeLabelsLayer.firstChild) {
|
|
5160
|
+
nodeLabelsLayer.appendChild(hoverNodeLabelsLayer.firstChild);
|
|
5161
|
+
}
|
|
5162
|
+
}
|
|
5163
|
+
const hoverLinksLayer = root2.select('[data-layer="hover-links"]').node();
|
|
5164
|
+
const linksLayer = root2.select('[data-layer="links"]').node();
|
|
5165
|
+
if (hoverLinksLayer && linksLayer) {
|
|
5166
|
+
while (hoverLinksLayer.firstChild) {
|
|
5167
|
+
const linkElement = hoverLinksLayer.firstChild;
|
|
5168
|
+
linksLayer.appendChild(linkElement);
|
|
5169
|
+
const event = new MouseEvent("mouseleave", {
|
|
5170
|
+
bubbles: true,
|
|
5171
|
+
cancelable: false,
|
|
5172
|
+
view: window
|
|
5173
|
+
});
|
|
5174
|
+
linkElement.dispatchEvent(event);
|
|
5175
|
+
}
|
|
5176
|
+
}
|
|
5177
|
+
const hoverLinkLabelsLayer = root2.select('[data-layer="hover-link-labels"]').node();
|
|
5178
|
+
const linkLabelsLayer = root2.select('[data-layer="link-labels"]').node();
|
|
5179
|
+
if (hoverLinkLabelsLayer && linkLabelsLayer) {
|
|
5180
|
+
while (hoverLinkLabelsLayer.firstChild) {
|
|
5181
|
+
const labelElement = hoverLinkLabelsLayer.firstChild;
|
|
5182
|
+
const labelData = labelElement.__data__;
|
|
5183
|
+
if (labelData && labelData.style.label.visibility === "hover" && !labelElement.classList.contains("label-selection-pinned")) {
|
|
5184
|
+
labelElement.style.opacity = "0";
|
|
5185
|
+
labelElement.style.pointerEvents = "none";
|
|
5186
|
+
}
|
|
5187
|
+
linkLabelsLayer.appendChild(labelElement);
|
|
5188
|
+
}
|
|
5189
|
+
}
|
|
5190
|
+
}
|
|
5191
|
+
nodeSelection.on("mouseenter.links", function(_event, hoveredNode) {
|
|
5192
|
+
const hoveredNodeElement = this;
|
|
5193
|
+
if (hoveredNodeElement.dataset.selected === "true") {
|
|
5194
|
+
return;
|
|
5195
|
+
}
|
|
5196
|
+
clearAllHoverLayers();
|
|
5197
|
+
const hoverNodesLayer = root2.select('[data-layer="hover-nodes"]').node();
|
|
5198
|
+
if (hoverNodesLayer) {
|
|
5199
|
+
hoverNodesLayer.appendChild(hoveredNodeElement);
|
|
5200
|
+
}
|
|
5201
|
+
root2.selectAll("text").filter((d) => d.id === hoveredNode.id).each(function() {
|
|
5202
|
+
const labelElement = this;
|
|
5203
|
+
const hoverNodeLabelsLayer = root2.select('[data-layer="hover-node-labels"]').node();
|
|
5204
|
+
if (hoverNodeLabelsLayer) {
|
|
5205
|
+
hoverNodeLabelsLayer.appendChild(labelElement);
|
|
5206
|
+
}
|
|
5207
|
+
});
|
|
5208
|
+
const connectedLinks = root2.selectAll("line:not(.link-hit-area)").filter((renderableLink) => {
|
|
5209
|
+
const source = renderableLink.link.source;
|
|
5210
|
+
const target = renderableLink.link.target;
|
|
5211
|
+
return source.id === hoveredNode.id || target.id === hoveredNode.id;
|
|
5212
|
+
});
|
|
5213
|
+
connectedLinks.each(function(_renderableLink) {
|
|
5214
|
+
const linkElement = this;
|
|
5215
|
+
const hoverLinksLayer = root2.select('[data-layer="hover-links"]').node();
|
|
5216
|
+
if (hoverLinksLayer) {
|
|
5217
|
+
hoverLinksLayer.appendChild(linkElement);
|
|
5218
|
+
}
|
|
5219
|
+
const event = new MouseEvent("mouseenter", {
|
|
5220
|
+
bubbles: true,
|
|
5221
|
+
cancelable: false,
|
|
5222
|
+
view: window
|
|
5223
|
+
});
|
|
5224
|
+
linkElement.dispatchEvent(event);
|
|
5225
|
+
});
|
|
5226
|
+
root2.selectAll(".link-label").filter((item) => {
|
|
5227
|
+
const source = item.link.source;
|
|
5228
|
+
const target = item.link.target;
|
|
5229
|
+
return source.id === hoveredNode.id || target.id === hoveredNode.id;
|
|
5230
|
+
}).each(function(item) {
|
|
5231
|
+
const labelElement = this;
|
|
5232
|
+
const hoverLinkLabelsLayer = root2.select('[data-layer="hover-link-labels"]').node();
|
|
5233
|
+
if (hoverLinkLabelsLayer) {
|
|
5234
|
+
hoverLinkLabelsLayer.appendChild(labelElement);
|
|
5235
|
+
if (item.style.label.visibility === "hover") {
|
|
5236
|
+
labelElement.style.opacity = "1";
|
|
5237
|
+
labelElement.style.pointerEvents = "auto";
|
|
5238
|
+
}
|
|
5239
|
+
}
|
|
5240
|
+
});
|
|
5241
|
+
}).on("mouseleave.links", function(_event, _hoveredNode) {
|
|
5242
|
+
clearAllHoverLayers();
|
|
5243
|
+
});
|
|
5244
|
+
}
|
|
5245
|
+
|
|
5246
|
+
// src/interactions/create-link-hover.ts
|
|
5247
|
+
function createLinkHover(linkSelection, hoverStyle) {
|
|
5248
|
+
const firstLink = linkSelection.node();
|
|
5249
|
+
if (!firstLink) return;
|
|
5250
|
+
const svgElement = firstLink.ownerSVGElement;
|
|
5251
|
+
if (!svgElement) return;
|
|
5252
|
+
const root2 = select_default2(svgElement);
|
|
5253
|
+
const originalMarkers = /* @__PURE__ */ new Map();
|
|
5254
|
+
function clearAllHoverStates() {
|
|
5255
|
+
root2.selectAll("line[data-hovered]").each(function(_d) {
|
|
5256
|
+
const linkElement = this;
|
|
5257
|
+
delete linkElement.dataset.hovered;
|
|
5258
|
+
const isSelected = linkElement.dataset.selected === "true";
|
|
5259
|
+
if (!isSelected) {
|
|
5260
|
+
linkElement.style.stroke = "";
|
|
5261
|
+
linkElement.style.strokeWidth = "";
|
|
5262
|
+
linkElement.style.opacity = "";
|
|
5263
|
+
const originalMarker = originalMarkers.get(linkElement);
|
|
5264
|
+
if (originalMarker !== void 0) {
|
|
5265
|
+
if (originalMarker) {
|
|
5266
|
+
linkElement.setAttribute("marker-end", originalMarker);
|
|
5267
|
+
} else {
|
|
5268
|
+
linkElement.removeAttribute("marker-end");
|
|
5269
|
+
}
|
|
5270
|
+
}
|
|
5271
|
+
}
|
|
5272
|
+
});
|
|
5273
|
+
root2.select('[data-layer="link-labels"]').selectAll(".link-label").filter(function(item) {
|
|
5274
|
+
return item.style.label.visibility === "hover" && !this.classList.contains("label-selection-pinned");
|
|
5275
|
+
}).style("opacity", 0).style("pointer-events", "none");
|
|
5276
|
+
}
|
|
5277
|
+
linkSelection.on("mouseenter.hover", function(_event, renderableLink) {
|
|
5278
|
+
const hoveredElement = this;
|
|
5279
|
+
clearAllHoverStates();
|
|
5280
|
+
let targetLinkElement;
|
|
5281
|
+
if (hoveredElement.classList.contains("link-hit-area")) {
|
|
5282
|
+
const visibleLinkNode = root2.select('[data-layer="links"]').selectAll("line:not(.link-hit-area)").filter((d) => d.link === renderableLink.link).node();
|
|
5283
|
+
if (!visibleLinkNode) return;
|
|
5284
|
+
targetLinkElement = visibleLinkNode;
|
|
5285
|
+
} else {
|
|
5286
|
+
targetLinkElement = hoveredElement;
|
|
5287
|
+
}
|
|
5288
|
+
if (!originalMarkers.has(targetLinkElement)) {
|
|
5289
|
+
originalMarkers.set(targetLinkElement, targetLinkElement.getAttribute("marker-end"));
|
|
5290
|
+
}
|
|
5291
|
+
if (hoverStyle) {
|
|
5292
|
+
if (hoverStyle.stroke !== void 0) {
|
|
5293
|
+
targetLinkElement.style.stroke = hoverStyle.stroke;
|
|
5294
|
+
if (renderableLink.style.arrow.enabled) {
|
|
5295
|
+
const hoverMarkerStyle = {
|
|
5296
|
+
stroke: hoverStyle.stroke,
|
|
5297
|
+
arrow: { fill: hoverStyle.stroke, size: renderableLink.style.arrow.size }
|
|
5298
|
+
};
|
|
5299
|
+
const hoverMarkerId = createArrowMarker({ svg: svgElement, style: hoverMarkerStyle });
|
|
5300
|
+
targetLinkElement.setAttribute("marker-end", `url(#${hoverMarkerId})`);
|
|
5301
|
+
}
|
|
5302
|
+
}
|
|
5303
|
+
if (hoverStyle.strokeWidth !== void 0) {
|
|
5304
|
+
targetLinkElement.style.strokeWidth = String(hoverStyle.strokeWidth);
|
|
5305
|
+
}
|
|
5306
|
+
if (hoverStyle.opacity !== void 0) {
|
|
5307
|
+
targetLinkElement.style.opacity = String(hoverStyle.opacity);
|
|
5308
|
+
}
|
|
5309
|
+
}
|
|
5310
|
+
const labelSelection2 = root2.select('[data-layer="link-labels"]').selectAll(".link-label");
|
|
5311
|
+
labelSelection2.filter((item) => item.link === renderableLink.link && item.style.label.visibility === "hover").style("opacity", 1).style("pointer-events", "auto");
|
|
5312
|
+
}).on("mouseleave.hover", function(_event, renderableLink) {
|
|
5313
|
+
const hoveredElement = this;
|
|
5314
|
+
let targetLinkElement;
|
|
5315
|
+
if (hoveredElement.classList.contains("link-hit-area")) {
|
|
5316
|
+
const visibleLinkNode = root2.select('[data-layer="links"]').selectAll("line:not(.link-hit-area)").filter((d) => d.link === renderableLink.link).node();
|
|
5317
|
+
if (!visibleLinkNode) return;
|
|
5318
|
+
targetLinkElement = visibleLinkNode;
|
|
5319
|
+
} else {
|
|
5320
|
+
targetLinkElement = hoveredElement;
|
|
5321
|
+
}
|
|
5322
|
+
const isSelected = targetLinkElement.dataset.selected === "true";
|
|
5323
|
+
if (!isSelected) {
|
|
5324
|
+
targetLinkElement.style.stroke = "";
|
|
5325
|
+
targetLinkElement.style.strokeWidth = "";
|
|
5326
|
+
targetLinkElement.style.opacity = "";
|
|
5327
|
+
const originalMarker = originalMarkers.get(targetLinkElement);
|
|
5328
|
+
if (originalMarker !== void 0) {
|
|
5329
|
+
if (originalMarker) {
|
|
5330
|
+
targetLinkElement.setAttribute("marker-end", originalMarker);
|
|
5331
|
+
} else {
|
|
5332
|
+
targetLinkElement.removeAttribute("marker-end");
|
|
5333
|
+
}
|
|
5334
|
+
}
|
|
5335
|
+
}
|
|
5336
|
+
const labelSelection2 = root2.select('[data-layer="link-labels"]').selectAll(".link-label");
|
|
5337
|
+
labelSelection2.filter(function(item) {
|
|
4240
5338
|
return item.style.label.visibility === "hover" && !this.classList.contains("label-selection-pinned");
|
|
4241
|
-
}).
|
|
5339
|
+
}).style("opacity", 0).style("pointer-events", "none");
|
|
5340
|
+
});
|
|
5341
|
+
const labelSelection = root2.select('[data-layer="link-labels"]').selectAll(".link-label");
|
|
5342
|
+
labelSelection.on("mouseenter.link-hover", function(_event, renderableLinkLabel) {
|
|
5343
|
+
const correspondingHitArea = root2.select('[data-layer="links"]').selectAll("line.link-hit-area").filter((d) => d.link === renderableLinkLabel.link);
|
|
5344
|
+
const correspondingLink = root2.select('[data-layer="links"]').selectAll("line:not(.link-hit-area)").filter((d) => d.link === renderableLinkLabel.link);
|
|
5345
|
+
if (correspondingLink.node() && correspondingHitArea.node()) {
|
|
5346
|
+
const linkElement = correspondingLink.node();
|
|
5347
|
+
const renderableLink = correspondingLink.datum();
|
|
5348
|
+
if (!originalMarkers.has(linkElement)) {
|
|
5349
|
+
originalMarkers.set(linkElement, linkElement.getAttribute("marker-end"));
|
|
5350
|
+
}
|
|
5351
|
+
if (hoverStyle) {
|
|
5352
|
+
if (hoverStyle.stroke !== void 0) {
|
|
5353
|
+
linkElement.style.stroke = hoverStyle.stroke;
|
|
5354
|
+
if (renderableLink.style.arrow.enabled) {
|
|
5355
|
+
const hoverMarkerStyle = {
|
|
5356
|
+
stroke: hoverStyle.stroke,
|
|
5357
|
+
arrow: { fill: hoverStyle.stroke, size: renderableLink.style.arrow.size }
|
|
5358
|
+
};
|
|
5359
|
+
const hoverMarkerId = createArrowMarker({ svg: svgElement, style: hoverMarkerStyle });
|
|
5360
|
+
linkElement.setAttribute("marker-end", `url(#${hoverMarkerId})`);
|
|
5361
|
+
}
|
|
5362
|
+
}
|
|
5363
|
+
if (hoverStyle.strokeWidth !== void 0) {
|
|
5364
|
+
linkElement.style.strokeWidth = String(hoverStyle.strokeWidth);
|
|
5365
|
+
}
|
|
5366
|
+
if (hoverStyle.opacity !== void 0) {
|
|
5367
|
+
linkElement.style.opacity = String(hoverStyle.opacity);
|
|
5368
|
+
}
|
|
5369
|
+
}
|
|
5370
|
+
}
|
|
5371
|
+
}).on("mouseleave.link-hover", function(_event, renderableLinkLabel) {
|
|
5372
|
+
const correspondingHitArea = root2.select('[data-layer="links"]').selectAll("line.link-hit-area").filter((d) => d.link === renderableLinkLabel.link);
|
|
5373
|
+
const correspondingLink = root2.select('[data-layer="links"]').selectAll("line:not(.link-hit-area)").filter((d) => d.link === renderableLinkLabel.link);
|
|
5374
|
+
if (correspondingLink.node() && correspondingHitArea.node()) {
|
|
5375
|
+
const linkElement = correspondingLink.node();
|
|
5376
|
+
const isSelected = linkElement.dataset.selected === "true";
|
|
5377
|
+
if (!isSelected) {
|
|
5378
|
+
linkElement.style.stroke = "";
|
|
5379
|
+
linkElement.style.strokeWidth = "";
|
|
5380
|
+
linkElement.style.opacity = "";
|
|
5381
|
+
const originalMarker = originalMarkers.get(linkElement);
|
|
5382
|
+
if (originalMarker !== void 0) {
|
|
5383
|
+
if (originalMarker) {
|
|
5384
|
+
linkElement.setAttribute("marker-end", originalMarker);
|
|
5385
|
+
} else {
|
|
5386
|
+
linkElement.removeAttribute("marker-end");
|
|
5387
|
+
}
|
|
5388
|
+
}
|
|
5389
|
+
}
|
|
5390
|
+
const labelSelection2 = root2.select('[data-layer="link-labels"]').selectAll(".link-label");
|
|
5391
|
+
labelSelection2.filter(function(item) {
|
|
5392
|
+
return item.style.label.visibility === "hover" && !this.classList.contains("label-selection-pinned");
|
|
5393
|
+
}).style("opacity", 0).style("pointer-events", "none");
|
|
5394
|
+
}
|
|
4242
5395
|
});
|
|
4243
5396
|
}
|
|
4244
5397
|
|
|
@@ -4436,25 +5589,9 @@ function getDefaultContent(node) {
|
|
|
4436
5589
|
`;
|
|
4437
5590
|
}
|
|
4438
5591
|
|
|
4439
|
-
// src/utils/
|
|
4440
|
-
function
|
|
4441
|
-
|
|
4442
|
-
(entries) => {
|
|
4443
|
-
const entry = entries[0];
|
|
4444
|
-
if (!entry) {
|
|
4445
|
-
return;
|
|
4446
|
-
}
|
|
4447
|
-
const width = entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width;
|
|
4448
|
-
const height = entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height;
|
|
4449
|
-
if (width > 0 && height > 0) {
|
|
4450
|
-
onResize(width, height);
|
|
4451
|
-
}
|
|
4452
|
-
}
|
|
4453
|
-
);
|
|
4454
|
-
observer.observe(element);
|
|
4455
|
-
return () => {
|
|
4456
|
-
observer.disconnect();
|
|
4457
|
-
};
|
|
5592
|
+
// src/utils/node-link-selection.utils.ts
|
|
5593
|
+
function createLinkHitArea(root2, linkSelection) {
|
|
5594
|
+
return root2.select('[data-layer="links"]').selectAll("line.link-hit-area").data(linkSelection.data()).join("line").attr("class", "link-hit-area").attr("stroke", "rgba(0,0,0,0)").attr("stroke-width", (item) => item.style.arrow.size * 4).style("pointer-events", "stroke").style("cursor", "pointer").attr("opacity", 0);
|
|
4458
5595
|
}
|
|
4459
5596
|
|
|
4460
5597
|
// src/utils/get-link-target-point.ts
|
|
@@ -4481,436 +5618,1528 @@ function getLinkTargetPoint(link) {
|
|
|
4481
5618
|
};
|
|
4482
5619
|
}
|
|
4483
5620
|
|
|
4484
|
-
// src/utils/
|
|
4485
|
-
var
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
5621
|
+
// src/utils/performance-tick-manager.ts
|
|
5622
|
+
var PerformanceTickManager = class {
|
|
5623
|
+
config;
|
|
5624
|
+
state;
|
|
5625
|
+
animationFrameId = null;
|
|
5626
|
+
isDestroyed = false;
|
|
5627
|
+
constructor(config = {}) {
|
|
5628
|
+
this.config = {
|
|
5629
|
+
frameRate: config.frameRate ?? 30,
|
|
5630
|
+
positionThreshold: config.positionThreshold ?? 0.5,
|
|
5631
|
+
batchSize: config.batchSize ?? 50,
|
|
5632
|
+
enableThrottling: config.enableThrottling ?? true
|
|
5633
|
+
};
|
|
5634
|
+
this.state = {
|
|
5635
|
+
lastUpdateTime: 0,
|
|
5636
|
+
frameInterval: 1e3 / this.config.frameRate,
|
|
5637
|
+
labelDimensionsCache: /* @__PURE__ */ new Map(),
|
|
5638
|
+
lastPositions: /* @__PURE__ */ new Map(),
|
|
5639
|
+
pendingUpdates: /* @__PURE__ */ new Set()
|
|
5640
|
+
};
|
|
4504
5641
|
}
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
5642
|
+
/**
|
|
5643
|
+
* Optimized tick handler with throttling and batching
|
|
5644
|
+
*/
|
|
5645
|
+
createTickHandler(selections) {
|
|
5646
|
+
return () => {
|
|
5647
|
+
if (this.isDestroyed) return;
|
|
5648
|
+
const now2 = performance.now();
|
|
5649
|
+
if (this.config.enableThrottling && now2 - this.state.lastUpdateTime < this.state.frameInterval) {
|
|
5650
|
+
return;
|
|
5651
|
+
}
|
|
5652
|
+
this.state.lastUpdateTime = now2;
|
|
5653
|
+
this.updateLinkPositions(selections.linkSelection);
|
|
5654
|
+
this.updateNodePositions(selections.nodeSelection, selections.labelSelection);
|
|
5655
|
+
this.scheduleFrameUpdate(() => {
|
|
5656
|
+
this.updateLinkLabels(selections.linkLabelSelection);
|
|
5657
|
+
selections.tooltipBinding?.reposition();
|
|
5658
|
+
});
|
|
5659
|
+
};
|
|
5660
|
+
}
|
|
5661
|
+
/**
|
|
5662
|
+
* Fast link position updates - now with proper source and target offsets
|
|
5663
|
+
*/
|
|
5664
|
+
updateLinkPositions(linkSelection) {
|
|
5665
|
+
linkSelection.attr("x1", (item) => getShortenedSourcePoint(item.link, item.style).x).attr("y1", (item) => getShortenedSourcePoint(item.link, item.style).y).attr("x2", (item) => getShortenedTargetPoint(item.link, item.style).x).attr("y2", (item) => getShortenedTargetPoint(item.link, item.style).y);
|
|
5666
|
+
}
|
|
5667
|
+
/**
|
|
5668
|
+
* Fast node position updates
|
|
5669
|
+
*/
|
|
5670
|
+
updateNodePositions(nodeSelection, labelSelection) {
|
|
5671
|
+
nodeSelection.attr("cx", (d) => d.x ?? 0).attr("cy", (d) => d.y ?? 0);
|
|
5672
|
+
labelSelection.attr("x", (d) => d.x ?? 0).attr("y", (d) => d.y ?? 0);
|
|
5673
|
+
}
|
|
5674
|
+
/**
|
|
5675
|
+
* Optimized link label updates with caching and conditional updates
|
|
5676
|
+
*/
|
|
5677
|
+
updateLinkLabels(linkLabelSelection) {
|
|
5678
|
+
let updateCount = 0;
|
|
5679
|
+
linkLabelSelection.attr("transform", (item) => {
|
|
5680
|
+
const link = item.link;
|
|
5681
|
+
const source = link.source;
|
|
5682
|
+
const targetPoint = getLinkTargetPoint(link);
|
|
5683
|
+
const x3 = ((source.x ?? 0) + targetPoint.x) / 2;
|
|
5684
|
+
const y3 = ((source.y ?? 0) + targetPoint.y) / 2;
|
|
5685
|
+
const linkId = this.getLinkId(link);
|
|
5686
|
+
const lastPos = this.state.lastPositions.get(linkId);
|
|
5687
|
+
if (lastPos && Math.abs(lastPos.x - x3) < this.config.positionThreshold && Math.abs(lastPos.y - y3) < this.config.positionThreshold) {
|
|
5688
|
+
return `translate(${lastPos.x}, ${lastPos.y})`;
|
|
5689
|
+
}
|
|
5690
|
+
this.state.lastPositions.set(linkId, { x: x3, y: y3 });
|
|
5691
|
+
this.state.pendingUpdates.add(linkId);
|
|
5692
|
+
return `translate(${x3}, ${y3})`;
|
|
5693
|
+
}).each((item, i, nodes) => {
|
|
5694
|
+
if (updateCount >= this.config.batchSize) {
|
|
5695
|
+
return;
|
|
5696
|
+
}
|
|
5697
|
+
const group = nodes[i];
|
|
5698
|
+
const linkId = this.getLinkId(item.link);
|
|
5699
|
+
if (!this.state.pendingUpdates.has(linkId)) {
|
|
5700
|
+
return;
|
|
5701
|
+
}
|
|
5702
|
+
this.updateLabelDimensions(group, item, linkId);
|
|
5703
|
+
this.state.pendingUpdates.delete(linkId);
|
|
5704
|
+
updateCount++;
|
|
4511
5705
|
});
|
|
4512
|
-
const dataUrl = canvas.toDataURL("image/png");
|
|
4513
|
-
const link = document.createElement("a");
|
|
4514
|
-
link.download = fileName;
|
|
4515
|
-
link.href = dataUrl;
|
|
4516
|
-
link.click();
|
|
4517
|
-
} finally {
|
|
4518
|
-
if (controls) controls.style.display = "flex";
|
|
4519
|
-
if (legendToggle) legendToggle.style.display = "flex";
|
|
4520
|
-
if (interactionLayer) interactionLayer.style.display = "block";
|
|
4521
|
-
if (legend && wasCollapsed) {
|
|
4522
|
-
legend.classList.add("pg-is-collapsed");
|
|
4523
|
-
}
|
|
4524
5706
|
}
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
5707
|
+
/**
|
|
5708
|
+
* Update label dimensions with caching
|
|
5709
|
+
*/
|
|
5710
|
+
updateLabelDimensions(group, item, linkId) {
|
|
5711
|
+
const text = group.querySelector("text");
|
|
5712
|
+
const rect = group.querySelector("rect");
|
|
5713
|
+
if (!text || !rect) return;
|
|
5714
|
+
const textContent = text.textContent || "";
|
|
5715
|
+
const cached = this.state.labelDimensionsCache.get(linkId);
|
|
5716
|
+
if (cached && cached.textContent === textContent) {
|
|
5717
|
+
rect.setAttribute("x", String(cached.x));
|
|
5718
|
+
rect.setAttribute("y", String(cached.y));
|
|
5719
|
+
rect.setAttribute("width", String(cached.width));
|
|
5720
|
+
rect.setAttribute("height", String(cached.height));
|
|
5721
|
+
return;
|
|
5722
|
+
}
|
|
5723
|
+
const bBox = text.getBBox();
|
|
5724
|
+
const padding = 6;
|
|
5725
|
+
const dimensions = {
|
|
5726
|
+
width: bBox.width + padding * 2,
|
|
5727
|
+
height: bBox.height + padding * 2,
|
|
5728
|
+
x: bBox.x - padding,
|
|
5729
|
+
y: bBox.y - padding,
|
|
5730
|
+
textContent
|
|
5731
|
+
};
|
|
5732
|
+
this.state.labelDimensionsCache.set(linkId, dimensions);
|
|
5733
|
+
rect.setAttribute("x", String(dimensions.x));
|
|
5734
|
+
rect.setAttribute("y", String(dimensions.y));
|
|
5735
|
+
rect.setAttribute("width", String(dimensions.width));
|
|
5736
|
+
rect.setAttribute("height", String(dimensions.height));
|
|
5737
|
+
}
|
|
5738
|
+
/**
|
|
5739
|
+
* Schedule updates using requestAnimationFrame for better performance
|
|
5740
|
+
*/
|
|
5741
|
+
scheduleFrameUpdate(callback) {
|
|
5742
|
+
if (this.animationFrameId !== null) {
|
|
5743
|
+
return;
|
|
5744
|
+
}
|
|
5745
|
+
this.animationFrameId = requestAnimationFrame(() => {
|
|
5746
|
+
this.animationFrameId = null;
|
|
5747
|
+
callback();
|
|
5748
|
+
});
|
|
5749
|
+
}
|
|
5750
|
+
/**
|
|
5751
|
+
* Generate unique ID for link caching
|
|
5752
|
+
*/
|
|
5753
|
+
getLinkId(link) {
|
|
5754
|
+
const sourceId = typeof link.source === "string" ? link.source : link.source.id;
|
|
5755
|
+
const targetId = typeof link.target === "string" ? link.target : link.target.id;
|
|
5756
|
+
return `${sourceId}::${targetId}::${link.label ?? ""}`;
|
|
5757
|
+
}
|
|
5758
|
+
/**
|
|
5759
|
+
* Clear caches when graph data changes
|
|
5760
|
+
*/
|
|
5761
|
+
clearCaches() {
|
|
5762
|
+
this.state.labelDimensionsCache.clear();
|
|
5763
|
+
this.state.lastPositions.clear();
|
|
5764
|
+
this.state.pendingUpdates.clear();
|
|
5765
|
+
}
|
|
5766
|
+
/**
|
|
5767
|
+
* Update configuration at runtime
|
|
5768
|
+
*/
|
|
5769
|
+
updateConfig(newConfig) {
|
|
5770
|
+
Object.assign(this.config, newConfig);
|
|
5771
|
+
this.state.frameInterval = 1e3 / this.config.frameRate;
|
|
5772
|
+
}
|
|
5773
|
+
/**
|
|
5774
|
+
* Get performance metrics
|
|
5775
|
+
*/
|
|
5776
|
+
getMetrics() {
|
|
5777
|
+
return {
|
|
5778
|
+
cacheSize: this.state.labelDimensionsCache.size,
|
|
5779
|
+
pendingUpdates: this.state.pendingUpdates.size,
|
|
5780
|
+
lastFrameTime: this.state.lastUpdateTime,
|
|
5781
|
+
frameInterval: this.state.frameInterval
|
|
5782
|
+
};
|
|
5783
|
+
}
|
|
5784
|
+
/**
|
|
5785
|
+
* Clean up resources
|
|
5786
|
+
*/
|
|
5787
|
+
destroy() {
|
|
5788
|
+
this.isDestroyed = true;
|
|
5789
|
+
if (this.animationFrameId !== null) {
|
|
5790
|
+
cancelAnimationFrame(this.animationFrameId);
|
|
5791
|
+
this.animationFrameId = null;
|
|
5792
|
+
}
|
|
5793
|
+
this.clearCaches();
|
|
5794
|
+
}
|
|
5795
|
+
};
|
|
5796
|
+
|
|
5797
|
+
// src/utils/selection-manager.ts
|
|
5798
|
+
var SelectionManager = class {
|
|
5799
|
+
state = {
|
|
5800
|
+
selectedNode: null,
|
|
5801
|
+
selectedLink: null
|
|
5802
|
+
};
|
|
5803
|
+
eventEmitter;
|
|
5804
|
+
config;
|
|
5805
|
+
layers;
|
|
5806
|
+
linkMarkerSnapshots;
|
|
5807
|
+
root;
|
|
5808
|
+
constructor(eventEmitter, config, layers, linkMarkerSnapshots, root2) {
|
|
5809
|
+
this.eventEmitter = eventEmitter;
|
|
5810
|
+
this.config = config;
|
|
5811
|
+
this.layers = layers;
|
|
5812
|
+
this.linkMarkerSnapshots = linkMarkerSnapshots;
|
|
5813
|
+
this.root = root2;
|
|
5814
|
+
}
|
|
5815
|
+
/**
|
|
5816
|
+
* Select a node, automatically deselecting any current selection
|
|
5817
|
+
*/
|
|
5818
|
+
selectNode(nodeElement, nodeData) {
|
|
5819
|
+
this.clearHoverState();
|
|
5820
|
+
this.clearSelection();
|
|
5821
|
+
this.bringNodeToFront(nodeElement, nodeData);
|
|
5822
|
+
if (this.config.nodeStyle) {
|
|
5823
|
+
const style = this.config.nodeStyle;
|
|
5824
|
+
if (style.fill !== void 0) nodeElement.style.fill = style.fill;
|
|
5825
|
+
if (style.stroke !== void 0) nodeElement.style.stroke = style.stroke;
|
|
5826
|
+
if (style.strokeWidth !== void 0) nodeElement.style.strokeWidth = String(style.strokeWidth);
|
|
5827
|
+
if (style.opacity !== void 0) nodeElement.style.opacity = String(style.opacity);
|
|
5828
|
+
if (style.radius !== void 0) nodeElement.style.setProperty("r", String(style.radius));
|
|
5829
|
+
}
|
|
5830
|
+
this.root.selectAll(".link-label").filter((item) => {
|
|
5831
|
+
if (item.style.label.visibility !== "hover") return false;
|
|
5832
|
+
const source = item.link.source;
|
|
5833
|
+
const target = item.link.target;
|
|
5834
|
+
return source.id === nodeData.id || target.id === nodeData.id;
|
|
5835
|
+
}).classed("label-selection-pinned", true).interrupt().transition().duration(200).style("opacity", 1).style("pointer-events", "auto");
|
|
5836
|
+
this.state.selectedNode = { element: nodeElement, data: nodeData };
|
|
5837
|
+
this.eventEmitter.emit("nodeSelect", { node: nodeData, element: nodeElement });
|
|
5838
|
+
}
|
|
5839
|
+
/**
|
|
5840
|
+
* Select a link, automatically deselecting any current selection
|
|
5841
|
+
*/
|
|
5842
|
+
selectLink(linkElement, renderableLink, event) {
|
|
5843
|
+
if (event) {
|
|
5844
|
+
event.stopPropagation();
|
|
5845
|
+
}
|
|
5846
|
+
this.clearSelection();
|
|
5847
|
+
this.bringLinkToFront(linkElement, renderableLink);
|
|
5848
|
+
linkElement.dataset.selected = "true";
|
|
5849
|
+
if (this.config.linkStyle) {
|
|
5850
|
+
const style = this.config.linkStyle;
|
|
5851
|
+
if (style.stroke !== void 0) linkElement.style.stroke = style.stroke;
|
|
5852
|
+
if (style.strokeWidth !== void 0) linkElement.style.strokeWidth = String(style.strokeWidth);
|
|
5853
|
+
if (style.opacity !== void 0) linkElement.style.opacity = String(style.opacity);
|
|
5854
|
+
if (style.stroke !== void 0 && renderableLink.style.arrow.enabled) {
|
|
5855
|
+
const selectionMarkerStyle = {
|
|
5856
|
+
stroke: style.stroke,
|
|
5857
|
+
arrow: { fill: style.stroke, size: renderableLink.style.arrow.size }
|
|
5858
|
+
};
|
|
5859
|
+
const selectionMarkerId = createArrowMarker({ svg: this.layers.svg, style: selectionMarkerStyle });
|
|
5860
|
+
linkElement.setAttribute("marker-end", `url(#${selectionMarkerId})`);
|
|
5861
|
+
}
|
|
5862
|
+
}
|
|
5863
|
+
this.root.selectAll(".link-label").filter((item) => item.link === renderableLink.link).classed("label-selection-pinned", true).interrupt().transition().duration(200).style("opacity", 1).style("pointer-events", "auto");
|
|
5864
|
+
const originalMarker = this.linkMarkerSnapshots.get(linkElement) || null;
|
|
5865
|
+
this.state.selectedLink = {
|
|
5866
|
+
element: linkElement,
|
|
5867
|
+
data: renderableLink.link,
|
|
5868
|
+
originalMarker
|
|
5869
|
+
};
|
|
5870
|
+
this.eventEmitter.emit("linkSelect", { link: renderableLink.link, element: linkElement });
|
|
5871
|
+
}
|
|
5872
|
+
/**
|
|
5873
|
+
* Deselect the currently selected node
|
|
5874
|
+
*/
|
|
5875
|
+
deselectNode() {
|
|
5876
|
+
if (!this.state.selectedNode) return;
|
|
5877
|
+
const { element, data } = this.state.selectedNode;
|
|
5878
|
+
this.restoreSelectedElements(data);
|
|
5879
|
+
element.style.fill = "";
|
|
5880
|
+
element.style.stroke = "";
|
|
5881
|
+
element.style.strokeWidth = "";
|
|
5882
|
+
element.style.opacity = "";
|
|
5883
|
+
element.style.removeProperty("r");
|
|
5884
|
+
delete element.dataset.selected;
|
|
5885
|
+
this.root.selectAll(".link-label.label-selection-pinned").classed("label-selection-pinned", false).interrupt().transition().duration(200).style("opacity", 0).style("pointer-events", "none");
|
|
5886
|
+
this.state.selectedNode = null;
|
|
5887
|
+
this.eventEmitter.emit("nodeDeselect", { node: data, element });
|
|
5888
|
+
}
|
|
5889
|
+
/**
|
|
5890
|
+
* Deselect the currently selected link
|
|
5891
|
+
*/
|
|
5892
|
+
deselectLink() {
|
|
5893
|
+
if (!this.state.selectedLink) return;
|
|
5894
|
+
const { element, data, originalMarker } = this.state.selectedLink;
|
|
5895
|
+
this.layers.links.appendChild(element);
|
|
5896
|
+
this.root.selectAll(".link-label").filter((item) => item.link === data).each((d, i, nodes) => {
|
|
5897
|
+
const element2 = nodes[i];
|
|
5898
|
+
if (element2) {
|
|
5899
|
+
this.layers.linkLabels.appendChild(element2);
|
|
5900
|
+
}
|
|
5901
|
+
});
|
|
5902
|
+
delete element.dataset.selected;
|
|
5903
|
+
element.style.stroke = "";
|
|
5904
|
+
element.style.strokeWidth = "";
|
|
5905
|
+
element.style.opacity = "";
|
|
5906
|
+
if (originalMarker) {
|
|
5907
|
+
element.setAttribute("marker-end", originalMarker);
|
|
5908
|
+
} else {
|
|
5909
|
+
element.removeAttribute("marker-end");
|
|
5910
|
+
}
|
|
5911
|
+
this.root.selectAll(".link-label.label-selection-pinned").filter((item) => {
|
|
5912
|
+
return item.link === data && item.style.label.visibility === "hover";
|
|
5913
|
+
}).classed("label-selection-pinned", false).interrupt().transition().duration(200).style("opacity", 0).style("pointer-events", "none");
|
|
5914
|
+
this.state.selectedLink = null;
|
|
5915
|
+
this.eventEmitter.emit("linkDeselect", { link: data, element });
|
|
5916
|
+
}
|
|
5917
|
+
/**
|
|
5918
|
+
* Clear all selections
|
|
5919
|
+
*/
|
|
5920
|
+
clearSelection() {
|
|
5921
|
+
this.deselectNode();
|
|
5922
|
+
this.deselectLink();
|
|
5923
|
+
}
|
|
5924
|
+
/**
|
|
5925
|
+
* Get current selection state
|
|
5926
|
+
*/
|
|
5927
|
+
getSelectionState() {
|
|
5928
|
+
return { ...this.state };
|
|
5929
|
+
}
|
|
5930
|
+
/**
|
|
5931
|
+
* Check if a specific node is selected
|
|
5932
|
+
*/
|
|
5933
|
+
isNodeSelected(nodeData) {
|
|
5934
|
+
return this.state.selectedNode?.data.id === nodeData.id;
|
|
5935
|
+
}
|
|
5936
|
+
/**
|
|
5937
|
+
* Check if a specific link is selected
|
|
5938
|
+
*/
|
|
5939
|
+
isLinkSelected(linkData) {
|
|
5940
|
+
if (!this.state.selectedLink) return false;
|
|
5941
|
+
const selectedLink = this.state.selectedLink.data;
|
|
5942
|
+
const sourceId = typeof selectedLink.source === "string" ? selectedLink.source : selectedLink.source.id;
|
|
5943
|
+
const targetId = typeof selectedLink.target === "string" ? selectedLink.target : selectedLink.target.id;
|
|
5944
|
+
const checkSourceId = typeof linkData.source === "string" ? linkData.source : linkData.source.id;
|
|
5945
|
+
const checkTargetId = typeof linkData.target === "string" ? linkData.target : linkData.target.id;
|
|
5946
|
+
return sourceId === checkSourceId && targetId === checkTargetId;
|
|
5947
|
+
}
|
|
5948
|
+
/**
|
|
5949
|
+
* Handle click on background (deselect all)
|
|
5950
|
+
*/
|
|
5951
|
+
handleBackgroundClick(event, svg, interactionRect) {
|
|
5952
|
+
if (event.target !== svg && event.target !== interactionRect) return;
|
|
5953
|
+
this.clearSelection();
|
|
5954
|
+
}
|
|
5955
|
+
/**
|
|
5956
|
+
* Bring node and related elements to front using selection layer sub-layers
|
|
5957
|
+
*/
|
|
5958
|
+
bringNodeToFront(nodeElement, nodeData) {
|
|
5959
|
+
this.layers.selectionLayer.nodes.appendChild(nodeElement);
|
|
5960
|
+
nodeElement.dataset.selected = "true";
|
|
5961
|
+
const connectedLinks = this.root.selectAll("line:not(.link-hit-area)").filter((d) => {
|
|
5962
|
+
const source = d.link.source;
|
|
5963
|
+
const target = d.link.target;
|
|
5964
|
+
return source.id === nodeData.id || target.id === nodeData.id;
|
|
5965
|
+
});
|
|
5966
|
+
connectedLinks.each((d, i, nodes) => {
|
|
5967
|
+
const element = nodes[i];
|
|
5968
|
+
if (element) {
|
|
5969
|
+
this.layers.selectionLayer.links.appendChild(element);
|
|
5970
|
+
}
|
|
5971
|
+
});
|
|
5972
|
+
this.root.selectAll("text").filter((d) => d.id === nodeData.id).each((d, i, nodes) => {
|
|
5973
|
+
const element = nodes[i];
|
|
5974
|
+
if (element) {
|
|
5975
|
+
this.layers.selectionLayer.nodeLabels.appendChild(element);
|
|
5976
|
+
}
|
|
5977
|
+
});
|
|
5978
|
+
this.root.selectAll(".link-label").filter((item) => {
|
|
5979
|
+
const source = item.link.source;
|
|
5980
|
+
const target = item.link.target;
|
|
5981
|
+
return source.id === nodeData.id || target.id === nodeData.id;
|
|
5982
|
+
}).each((d, i, nodes) => {
|
|
5983
|
+
const element = nodes[i];
|
|
5984
|
+
if (element) {
|
|
5985
|
+
this.layers.selectionLayer.linkLabels.appendChild(element);
|
|
5986
|
+
}
|
|
5987
|
+
});
|
|
5988
|
+
}
|
|
5989
|
+
/**
|
|
5990
|
+
* Bring link and its label to front using selection layer
|
|
5991
|
+
*/
|
|
5992
|
+
bringLinkToFront(linkElement, renderableLink) {
|
|
5993
|
+
this.layers.selectionLayer.links.appendChild(linkElement);
|
|
5994
|
+
this.root.selectAll(".link-label").filter((item) => item.link === renderableLink.link).each((d, i, nodes) => {
|
|
5995
|
+
const element = nodes[i];
|
|
5996
|
+
if (element) {
|
|
5997
|
+
this.layers.selectionLayer.linkLabels.appendChild(element);
|
|
5998
|
+
}
|
|
5999
|
+
});
|
|
6000
|
+
}
|
|
6001
|
+
/**
|
|
6002
|
+
* Restore elements back to their original layers
|
|
6003
|
+
*/
|
|
6004
|
+
restoreSelectedElements(nodeData) {
|
|
6005
|
+
this.root.selectAll("circle").filter((d) => d.id === nodeData.id).each((d, i, nodes) => {
|
|
6006
|
+
const element = nodes[i];
|
|
6007
|
+
if (element) {
|
|
6008
|
+
this.layers.nodes.appendChild(element);
|
|
6009
|
+
}
|
|
6010
|
+
});
|
|
6011
|
+
this.root.selectAll("line").filter((d) => {
|
|
6012
|
+
const source = d.link.source;
|
|
6013
|
+
const target = d.link.target;
|
|
6014
|
+
return source.id === nodeData.id || target.id === nodeData.id;
|
|
6015
|
+
}).each((d, i, nodes) => {
|
|
6016
|
+
const element = nodes[i];
|
|
6017
|
+
if (element) {
|
|
6018
|
+
this.layers.links.appendChild(element);
|
|
6019
|
+
}
|
|
6020
|
+
});
|
|
6021
|
+
this.root.selectAll("text").filter((d) => d.id === nodeData.id).each((d, i, nodes) => {
|
|
6022
|
+
const element = nodes[i];
|
|
6023
|
+
if (element) {
|
|
6024
|
+
this.layers.nodeLabels.appendChild(element);
|
|
6025
|
+
}
|
|
6026
|
+
});
|
|
6027
|
+
this.root.selectAll(".link-label").filter((item) => {
|
|
6028
|
+
const source = item.link.source;
|
|
6029
|
+
const target = item.link.target;
|
|
6030
|
+
return source.id === nodeData.id || target.id === nodeData.id;
|
|
6031
|
+
}).each((d, i, nodes) => {
|
|
6032
|
+
const element = nodes[i];
|
|
6033
|
+
if (element) {
|
|
6034
|
+
this.layers.linkLabels.appendChild(element);
|
|
6035
|
+
}
|
|
6036
|
+
});
|
|
6037
|
+
}
|
|
6038
|
+
/**
|
|
6039
|
+
* Utility method to bring any SVG element to front using appendChild
|
|
6040
|
+
* Based on the reference implementation pattern
|
|
6041
|
+
*/
|
|
6042
|
+
bringElementToFront(element) {
|
|
6043
|
+
if (element.parentNode) {
|
|
6044
|
+
element.parentNode.appendChild(element);
|
|
6045
|
+
}
|
|
6046
|
+
}
|
|
6047
|
+
/**
|
|
6048
|
+
* Clear hover state to prevent conflicts with selection
|
|
6049
|
+
* Similar to the clearAllHoverLayers function in create-node-hover.ts
|
|
6050
|
+
*/
|
|
6051
|
+
clearHoverState() {
|
|
6052
|
+
const hoverNodesLayer = this.root.select('[data-layer="hover-nodes"]').node();
|
|
6053
|
+
const nodesLayer = this.root.select('[data-layer="nodes"]').node();
|
|
6054
|
+
if (hoverNodesLayer && nodesLayer) {
|
|
6055
|
+
while (hoverNodesLayer.firstChild) {
|
|
6056
|
+
nodesLayer.appendChild(hoverNodesLayer.firstChild);
|
|
6057
|
+
}
|
|
6058
|
+
}
|
|
6059
|
+
const hoverNodeLabelsLayer = this.root.select('[data-layer="hover-node-labels"]').node();
|
|
6060
|
+
const nodeLabelsLayer = this.root.select('[data-layer="node-labels"]').node();
|
|
6061
|
+
if (hoverNodeLabelsLayer && nodeLabelsLayer) {
|
|
6062
|
+
while (hoverNodeLabelsLayer.firstChild) {
|
|
6063
|
+
nodeLabelsLayer.appendChild(hoverNodeLabelsLayer.firstChild);
|
|
6064
|
+
}
|
|
6065
|
+
}
|
|
6066
|
+
const hoverLinksLayer = this.root.select('[data-layer="hover-links"]').node();
|
|
6067
|
+
const linksLayer = this.root.select('[data-layer="links"]').node();
|
|
6068
|
+
if (hoverLinksLayer && linksLayer) {
|
|
6069
|
+
while (hoverLinksLayer.firstChild) {
|
|
6070
|
+
const linkElement = hoverLinksLayer.firstChild;
|
|
6071
|
+
linksLayer.appendChild(linkElement);
|
|
6072
|
+
const event = new MouseEvent("mouseleave", {
|
|
6073
|
+
bubbles: true,
|
|
6074
|
+
cancelable: false,
|
|
6075
|
+
view: window
|
|
6076
|
+
});
|
|
6077
|
+
linkElement.dispatchEvent(event);
|
|
6078
|
+
}
|
|
6079
|
+
}
|
|
6080
|
+
const hoverLinkLabelsLayer = this.root.select('[data-layer="hover-link-labels"]').node();
|
|
6081
|
+
const linkLabelsLayer = this.root.select('[data-layer="link-labels"]').node();
|
|
6082
|
+
if (hoverLinkLabelsLayer && linkLabelsLayer) {
|
|
6083
|
+
while (hoverLinkLabelsLayer.firstChild) {
|
|
6084
|
+
const labelElement = hoverLinkLabelsLayer.firstChild;
|
|
6085
|
+
const labelData = labelElement.__data__;
|
|
6086
|
+
if (labelData && labelData.style.label.visibility === "hover" && !labelElement.classList.contains("label-selection-pinned")) {
|
|
6087
|
+
labelElement.style.opacity = "0";
|
|
6088
|
+
labelElement.style.pointerEvents = "none";
|
|
6089
|
+
}
|
|
6090
|
+
linkLabelsLayer.appendChild(labelElement);
|
|
6091
|
+
}
|
|
6092
|
+
}
|
|
6093
|
+
}
|
|
6094
|
+
/**
|
|
6095
|
+
* Clean up resources
|
|
6096
|
+
*/
|
|
6097
|
+
destroy() {
|
|
6098
|
+
this.clearSelection();
|
|
6099
|
+
}
|
|
6100
|
+
};
|
|
6101
|
+
|
|
6102
|
+
// src/core/interaction-manager.ts
|
|
6103
|
+
var InteractionManager = class {
|
|
6104
|
+
constructor(manager) {
|
|
6105
|
+
this.manager = manager;
|
|
6106
|
+
}
|
|
6107
|
+
manager;
|
|
6108
|
+
/**
|
|
6109
|
+
* Setup all interactions for the graph
|
|
6110
|
+
*/
|
|
6111
|
+
setupInteractions(selections) {
|
|
6112
|
+
this.setupTickManager(selections);
|
|
6113
|
+
this.setupHoverInteractions(selections);
|
|
6114
|
+
this.setupDragBehavior(selections);
|
|
6115
|
+
this.setupSelectionManagement(selections);
|
|
6116
|
+
this.setupLinkHitAreas(selections);
|
|
6117
|
+
}
|
|
6118
|
+
/**
|
|
6119
|
+
* Setup performance tick manager
|
|
6120
|
+
*/
|
|
6121
|
+
setupTickManager(selections) {
|
|
6122
|
+
if (!this.manager.tickManager) {
|
|
6123
|
+
this.manager.tickManager = new PerformanceTickManager({
|
|
6124
|
+
frameRate: 30,
|
|
6125
|
+
positionThreshold: 0.5,
|
|
6126
|
+
batchSize: 50,
|
|
6127
|
+
enableThrottling: true
|
|
6128
|
+
});
|
|
6129
|
+
}
|
|
6130
|
+
const optimizedTickHandler = this.manager.tickManager.createTickHandler({
|
|
6131
|
+
linkSelection: selections.linkSelection,
|
|
6132
|
+
linkLabelSelection: selections.linkLabelSelection,
|
|
6133
|
+
nodeSelection: selections.nodeSelection,
|
|
6134
|
+
labelSelection: selections.labelSelection,
|
|
6135
|
+
tooltipBinding: this.manager.tooltipBinding
|
|
6136
|
+
});
|
|
6137
|
+
if (this.manager.simulation) {
|
|
6138
|
+
this.manager.simulation.on("tick", optimizedTickHandler);
|
|
6139
|
+
}
|
|
6140
|
+
}
|
|
6141
|
+
/**
|
|
6142
|
+
* Setup hover interactions
|
|
6143
|
+
*/
|
|
6144
|
+
setupHoverInteractions(selections) {
|
|
6145
|
+
if (this.manager.config.interaction?.hover?.enabled) {
|
|
6146
|
+
if (this.manager.config.interaction.hover.tooltip?.enabled) {
|
|
6147
|
+
this.manager.tooltipBinding = bindNodeTooltip({
|
|
6148
|
+
container: this.manager.config.container,
|
|
6149
|
+
selection: selections.nodeSelection,
|
|
6150
|
+
tooltipConfig: this.manager.config.interaction.hover.tooltip
|
|
6151
|
+
});
|
|
6152
|
+
}
|
|
6153
|
+
createNodeHover(selections.nodeSelection, this.manager.config.interaction.hover.nodeStyle);
|
|
6154
|
+
createLinkHover(selections.linkSelection, this.manager.config.interaction.hover.linkStyle);
|
|
6155
|
+
}
|
|
6156
|
+
}
|
|
6157
|
+
/**
|
|
6158
|
+
* Setup drag behavior
|
|
6159
|
+
*/
|
|
6160
|
+
setupDragBehavior(selections) {
|
|
6161
|
+
if (this.manager.config.interaction?.drag?.enabled !== false && this.manager.simulation) {
|
|
6162
|
+
selections.nodeSelection.call(
|
|
6163
|
+
createDragBehavior(
|
|
6164
|
+
this.manager.simulation,
|
|
6165
|
+
() => {
|
|
6166
|
+
this.manager.reheatSimulation(0.3);
|
|
6167
|
+
},
|
|
6168
|
+
this.manager.dimensions
|
|
6169
|
+
)
|
|
6170
|
+
);
|
|
6171
|
+
}
|
|
6172
|
+
}
|
|
6173
|
+
/**
|
|
6174
|
+
* Setup selection management
|
|
6175
|
+
*/
|
|
6176
|
+
setupSelectionManagement(selections) {
|
|
6177
|
+
if (this.manager.config.interaction?.selection?.enabled && this.manager.eventEmitter && this.manager.layers && this.manager.rootGroup) {
|
|
6178
|
+
if (!this.manager.linkMarkerSnapshots) {
|
|
6179
|
+
this.manager.linkMarkerSnapshots = /* @__PURE__ */ new Map();
|
|
6180
|
+
const manager = this.manager;
|
|
6181
|
+
selections.linkSelection.each(function() {
|
|
6182
|
+
manager.linkMarkerSnapshots.set(this, this.getAttribute("marker-end"));
|
|
6183
|
+
});
|
|
6184
|
+
}
|
|
6185
|
+
if (!this.manager.rootSelection) {
|
|
6186
|
+
this.manager.rootSelection = select_default2(this.manager.rootGroup);
|
|
6187
|
+
}
|
|
6188
|
+
this.manager.selectionManager = new SelectionManager(
|
|
6189
|
+
this.manager.eventEmitter,
|
|
6190
|
+
this.manager.config.interaction.selection,
|
|
6191
|
+
this.manager.layers,
|
|
6192
|
+
this.manager.linkMarkerSnapshots,
|
|
6193
|
+
this.manager.rootSelection
|
|
6194
|
+
);
|
|
6195
|
+
this.setupSelectionHandlers(selections);
|
|
6196
|
+
this.setupBackgroundClickHandler();
|
|
6197
|
+
}
|
|
6198
|
+
}
|
|
6199
|
+
/**
|
|
6200
|
+
* Setup selection event handlers
|
|
6201
|
+
*/
|
|
6202
|
+
setupSelectionHandlers(selections) {
|
|
6203
|
+
if (!this.manager.selectionManager) return;
|
|
6204
|
+
selections.nodeSelection.on("click.select", (event, node) => {
|
|
6205
|
+
const nodeElement = event.currentTarget;
|
|
6206
|
+
this.manager.selectionManager?.selectNode(nodeElement, node);
|
|
6207
|
+
});
|
|
6208
|
+
selections.linkLabelSelection.on("click.select", (event, renderableLinkLabel) => {
|
|
6209
|
+
event.stopPropagation();
|
|
6210
|
+
const correspondingLink = selections.linkSelection.filter((d) => d.link === renderableLinkLabel.link).node();
|
|
6211
|
+
if (correspondingLink && this.manager.selectionManager) {
|
|
6212
|
+
const linkData = selections.linkSelection.filter((d) => d.link === renderableLinkLabel.link).datum();
|
|
6213
|
+
this.manager.selectionManager.selectLink(correspondingLink, linkData, event);
|
|
6214
|
+
}
|
|
6215
|
+
});
|
|
6216
|
+
}
|
|
6217
|
+
/**
|
|
6218
|
+
* Setup background click to clear selection
|
|
6219
|
+
*/
|
|
6220
|
+
setupBackgroundClickHandler() {
|
|
6221
|
+
if (!this.manager.selectionManager || !this.manager.layers) return;
|
|
6222
|
+
select_default2(this.manager.layers.svg).on("click.deselect", (event) => {
|
|
6223
|
+
if (this.manager.selectionManager) {
|
|
6224
|
+
this.manager.selectionManager.handleBackgroundClick(
|
|
6225
|
+
event,
|
|
6226
|
+
this.manager.layers.svg,
|
|
6227
|
+
this.manager.layers.interactionRect
|
|
6228
|
+
);
|
|
6229
|
+
}
|
|
6230
|
+
});
|
|
6231
|
+
}
|
|
6232
|
+
/**
|
|
6233
|
+
* Setup link hit areas for better interaction
|
|
6234
|
+
*/
|
|
6235
|
+
setupLinkHitAreas(selections) {
|
|
6236
|
+
if (!this.manager.rootGroup) {
|
|
6237
|
+
console.warn("[InteractionManager] No root group available for link hit areas");
|
|
6238
|
+
return;
|
|
6239
|
+
}
|
|
6240
|
+
const rootSelection = select_default2(this.manager.rootGroup);
|
|
6241
|
+
const linkHitAreaSelection = createLinkHitArea(rootSelection, selections.linkSelection);
|
|
6242
|
+
if (linkHitAreaSelection && !linkHitAreaSelection.empty()) {
|
|
6243
|
+
if (this.manager.config.interaction?.hover?.enabled) {
|
|
6244
|
+
createLinkHover(linkHitAreaSelection, this.manager.config.interaction.hover.linkStyle);
|
|
6245
|
+
}
|
|
6246
|
+
if (this.manager.simulation) {
|
|
6247
|
+
this.manager.simulation.on("tick.hitarea", () => {
|
|
6248
|
+
linkHitAreaSelection.attr("x1", (item) => item.link.source.x ?? 0).attr("y1", (item) => item.link.source.y ?? 0).attr("x2", (item) => item.link.target.x ?? 0).attr("y2", (item) => item.link.target.y ?? 0);
|
|
6249
|
+
});
|
|
6250
|
+
}
|
|
6251
|
+
if (this.manager.selectionManager) {
|
|
6252
|
+
linkHitAreaSelection.on("click.select", (event, renderableLink) => {
|
|
6253
|
+
const visibleLinkNode = selections.linkSelection.filter((d) => d === renderableLink).node();
|
|
6254
|
+
if (visibleLinkNode) {
|
|
6255
|
+
this.manager.selectionManager?.selectLink(visibleLinkNode, renderableLink, event);
|
|
6256
|
+
}
|
|
6257
|
+
});
|
|
6258
|
+
}
|
|
6259
|
+
}
|
|
6260
|
+
}
|
|
6261
|
+
};
|
|
6262
|
+
|
|
6263
|
+
// src/controls/graph-controls.utils.ts
|
|
6264
|
+
function resolveControlsPosition(position) {
|
|
6265
|
+
return position ?? "bottom-left";
|
|
6266
|
+
}
|
|
6267
|
+
function resolveControlsOrientation(orientation) {
|
|
6268
|
+
return orientation ?? "vertical";
|
|
6269
|
+
}
|
|
6270
|
+
function shouldRenderControl(config, key) {
|
|
6271
|
+
const show = config.show;
|
|
6272
|
+
if (!show) {
|
|
6273
|
+
return true;
|
|
4548
6274
|
}
|
|
4549
|
-
const
|
|
4550
|
-
|
|
4551
|
-
|
|
6275
|
+
const value = show[key];
|
|
6276
|
+
if (value === void 0) {
|
|
6277
|
+
return true;
|
|
6278
|
+
}
|
|
6279
|
+
return value;
|
|
4552
6280
|
}
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
6281
|
+
|
|
6282
|
+
// src/assets/plus.svg?raw
|
|
6283
|
+
var plus_default = '<svg\n xmlns="http://www.w3.org/2000/svg"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n stroke-width="2"\n stroke-linecap="round"\n stroke-linejoin="round"\n>\n <path d="M5 12h14m-7-7v14" />\n</svg>';
|
|
6284
|
+
|
|
6285
|
+
// src/assets/minus.svg?raw
|
|
6286
|
+
var minus_default = '<svg\n xmlns="http://www.w3.org/2000/svg"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n stroke-width="2"\n stroke-linecap="round"\n stroke-linejoin="round"\n>\n <path d="M19 12H5" />\n</svg>';
|
|
6287
|
+
|
|
6288
|
+
// src/assets/fit.svg?raw
|
|
6289
|
+
var fit_default = '<svg\n xmlns="http://www.w3.org/2000/svg"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n stroke-width="2"\n stroke-linecap="round"\n stroke-linejoin="round"\n>\n <path d="M5 9V5H9" />\n <path d="M19 9V5H15" />\n <path d="M5 15V19H9" />\n <path d="M19 15V19H15" />\n</svg>';
|
|
6290
|
+
|
|
6291
|
+
// src/assets/reset.svg?raw
|
|
6292
|
+
var reset_default = '<svg\n xmlns="http://www.w3.org/2000/svg"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n stroke-width="2"\n stroke-linecap="round"\n stroke-linejoin="round"\n>\n <path d="M20 12a8 8 0 1 1-2.3-5.7" />\n <path d="M20 4.5v4h-4" />\n</svg>';
|
|
6293
|
+
|
|
6294
|
+
// src/controls/graph-controls.icons.ts
|
|
6295
|
+
var ICON_MAP = {
|
|
6296
|
+
"zoom-in": plus_default,
|
|
6297
|
+
"zoom-out": minus_default,
|
|
6298
|
+
fit: fit_default,
|
|
6299
|
+
reset: reset_default
|
|
6300
|
+
};
|
|
6301
|
+
function getControlIcon(icon) {
|
|
6302
|
+
const raw = ICON_MAP[icon];
|
|
6303
|
+
if (!raw) {
|
|
6304
|
+
throw new Error(`Icon not found: ${icon}`);
|
|
6305
|
+
}
|
|
6306
|
+
return raw.replace("<svg", '<svg class="pg-icon"');
|
|
6307
|
+
}
|
|
6308
|
+
|
|
6309
|
+
// src/controls/create-graph-controls.ts
|
|
6310
|
+
function createGraphControls(overlay, graph, config) {
|
|
6311
|
+
let root2 = null;
|
|
6312
|
+
function mount() {
|
|
6313
|
+
if (!config.enabled) {
|
|
6314
|
+
return;
|
|
4559
6315
|
}
|
|
4560
|
-
|
|
4561
|
-
|
|
6316
|
+
root2 = document.createElement("div");
|
|
6317
|
+
root2.className = "pg-controls";
|
|
6318
|
+
const position = resolveControlsPosition(config.position);
|
|
6319
|
+
root2.classList.add(`pg-pos-${position}`);
|
|
6320
|
+
const orientation = resolveControlsOrientation(config.orientation);
|
|
6321
|
+
root2.classList.add(`pg-orient-${orientation}`);
|
|
6322
|
+
if (config.offset) {
|
|
6323
|
+
root2.style.setProperty("--pg-controls-offset-x", `${config.offset.x}px`);
|
|
6324
|
+
root2.style.setProperty("--pg-controls-offset-y", `${config.offset.y}px`);
|
|
4562
6325
|
}
|
|
4563
|
-
|
|
4564
|
-
|
|
6326
|
+
appendControls(root2, config, graph);
|
|
6327
|
+
overlay.appendChild(root2);
|
|
6328
|
+
}
|
|
6329
|
+
function appendControls(root3, config2, graph2) {
|
|
6330
|
+
const actions = [
|
|
6331
|
+
{ key: "zoomIn", icon: "zoom-in", label: "Zoom in", fn: graph2.zoomIn.bind(graph2) },
|
|
6332
|
+
{ key: "zoomOut", icon: "zoom-out", label: "Zoom out", fn: graph2.zoomOut.bind(graph2) },
|
|
6333
|
+
{ key: "fit", icon: "fit", label: "Fit view", fn: graph2.fitView.bind(graph2) },
|
|
6334
|
+
{ key: "reset", icon: "reset", label: "Reset view", fn: graph2.resetView.bind(graph2) }
|
|
6335
|
+
];
|
|
6336
|
+
actions.forEach((action) => {
|
|
6337
|
+
if (shouldRenderControl(config2, action.key)) {
|
|
6338
|
+
root3.appendChild(createButton(action.icon, action.label, action.fn));
|
|
6339
|
+
}
|
|
6340
|
+
});
|
|
6341
|
+
}
|
|
6342
|
+
function createButton(type, label, onClick) {
|
|
6343
|
+
const button = document.createElement("button");
|
|
6344
|
+
button.className = "pg-control-btn";
|
|
6345
|
+
button.type = "button";
|
|
6346
|
+
button.setAttribute("aria-label", label);
|
|
6347
|
+
const wrapper = document.createElement("div");
|
|
6348
|
+
wrapper.className = "pg-icon-wrapper";
|
|
6349
|
+
wrapper.innerHTML = getControlIcon(type);
|
|
6350
|
+
const svg = wrapper.querySelector("svg");
|
|
6351
|
+
if (svg) {
|
|
6352
|
+
svg.classList.add("pg-icon");
|
|
6353
|
+
button.appendChild(svg);
|
|
4565
6354
|
}
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
6355
|
+
button.addEventListener("click", onClick);
|
|
6356
|
+
return button;
|
|
6357
|
+
}
|
|
6358
|
+
function destroy() {
|
|
6359
|
+
if (!root2) {
|
|
6360
|
+
return;
|
|
6361
|
+
}
|
|
6362
|
+
if (root2.parentNode === overlay) {
|
|
6363
|
+
overlay.removeChild(root2);
|
|
4573
6364
|
}
|
|
6365
|
+
root2 = null;
|
|
4574
6366
|
}
|
|
4575
|
-
|
|
6367
|
+
return { mount, destroy };
|
|
4576
6368
|
}
|
|
4577
|
-
|
|
4578
|
-
|
|
6369
|
+
|
|
6370
|
+
// src/assets/caret.svg?raw
|
|
6371
|
+
var caret_default = '<svg\n xmlns="http://www.w3.org/2000/svg"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n stroke-width="2"\n stroke-linecap="round"\n stroke-linejoin="round"\n>\n <path d="M9 20L16.5 12L9 4" />\n</svg>';
|
|
6372
|
+
|
|
6373
|
+
// src/legends/graph-legend-icon.ts
|
|
6374
|
+
var LEGEND_ICON_MAP = { caret: caret_default };
|
|
6375
|
+
function getLegendIcon(icon) {
|
|
6376
|
+
const raw = LEGEND_ICON_MAP[icon];
|
|
6377
|
+
if (!raw) {
|
|
6378
|
+
throw new Error(`Legend icon not found: ${icon}`);
|
|
6379
|
+
}
|
|
6380
|
+
return raw.replace("<svg", '<svg class="pg-icon"');
|
|
4579
6381
|
}
|
|
4580
6382
|
|
|
4581
|
-
// src/create-graph.ts
|
|
4582
|
-
function
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
const
|
|
4595
|
-
|
|
4596
|
-
const
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
6383
|
+
// src/legends/create-graph-legends.ts
|
|
6384
|
+
function generateLegendItems(nodes) {
|
|
6385
|
+
const uniqueTypes = Array.from(new Set(nodes.map((node) => node.type)));
|
|
6386
|
+
return uniqueTypes.map((type) => {
|
|
6387
|
+
const sampleNode = nodes.find((node) => node.type === type);
|
|
6388
|
+
return {
|
|
6389
|
+
label: type,
|
|
6390
|
+
color: sampleNode?.style?.fill ?? "#94a3b8",
|
|
6391
|
+
shape: "circle"
|
|
6392
|
+
};
|
|
6393
|
+
});
|
|
6394
|
+
}
|
|
6395
|
+
function createGraphLegend(overlay, config, nodes) {
|
|
6396
|
+
const legendWrapper = document.createElement("div");
|
|
6397
|
+
legendWrapper.className = "pg-legend";
|
|
6398
|
+
const position = config.position || "bottom-right";
|
|
6399
|
+
legendWrapper.classList.add(`pg-pos-${position}`);
|
|
6400
|
+
if (config.defaultExpanded === false) {
|
|
6401
|
+
legendWrapper.classList.add("pg-is-collapsed");
|
|
6402
|
+
}
|
|
6403
|
+
if (config.collapsible) {
|
|
6404
|
+
const toggleBtn = document.createElement("button");
|
|
6405
|
+
toggleBtn.className = "pg-legend-toggle";
|
|
6406
|
+
toggleBtn.type = "button";
|
|
6407
|
+
toggleBtn.innerHTML = getLegendIcon("caret");
|
|
6408
|
+
toggleBtn.onclick = (e) => {
|
|
6409
|
+
e.stopPropagation();
|
|
6410
|
+
legendWrapper.classList.toggle("pg-is-collapsed");
|
|
6411
|
+
};
|
|
6412
|
+
legendWrapper.appendChild(toggleBtn);
|
|
6413
|
+
}
|
|
6414
|
+
const body = document.createElement("div");
|
|
6415
|
+
body.className = "pg-legend-body";
|
|
6416
|
+
const list = document.createElement("ul");
|
|
6417
|
+
list.className = "pg-legend-list";
|
|
6418
|
+
const legendItems = config.items ?? (nodes ? generateLegendItems(nodes) : []);
|
|
6419
|
+
legendItems.forEach((item) => {
|
|
6420
|
+
const listItem = document.createElement("li");
|
|
6421
|
+
listItem.className = "pg-legend-item";
|
|
6422
|
+
const swatch = document.createElement("span");
|
|
6423
|
+
swatch.className = `pg-legend-swatch is-${item.shape || "circle"}`;
|
|
6424
|
+
swatch.style.backgroundColor = item.color;
|
|
6425
|
+
const label = document.createElement("span");
|
|
6426
|
+
label.className = "pg-legend-label";
|
|
6427
|
+
label.innerText = item.label;
|
|
6428
|
+
listItem.appendChild(swatch);
|
|
6429
|
+
listItem.appendChild(label);
|
|
6430
|
+
list.appendChild(listItem);
|
|
6431
|
+
});
|
|
6432
|
+
body.appendChild(list);
|
|
6433
|
+
legendWrapper.appendChild(body);
|
|
6434
|
+
overlay.appendChild(legendWrapper);
|
|
6435
|
+
return () => {
|
|
6436
|
+
if (legendWrapper.parentNode === overlay) overlay.removeChild(legendWrapper);
|
|
6437
|
+
};
|
|
6438
|
+
}
|
|
6439
|
+
|
|
6440
|
+
// src/utils/export-graph.ts
|
|
6441
|
+
async function captureAndDownloadGraph(container, options = {}) {
|
|
6442
|
+
const {
|
|
6443
|
+
fileName = `graph-export-${Date.now()}.png`,
|
|
6444
|
+
backgroundColor = "#ffffff",
|
|
6445
|
+
scale = 2,
|
|
6446
|
+
includeLegend = true
|
|
6447
|
+
} = options;
|
|
6448
|
+
const svgElement = container.querySelector("svg.pg-canvas");
|
|
6449
|
+
if (!svgElement) {
|
|
6450
|
+
throw new Error("SVG element not found in container");
|
|
6451
|
+
}
|
|
6452
|
+
const svgRect = svgElement.getBoundingClientRect();
|
|
6453
|
+
const graphWidth = svgRect.width || 800;
|
|
6454
|
+
const graphHeight = svgRect.height || 600;
|
|
6455
|
+
const svgClone = svgElement.cloneNode(true);
|
|
6456
|
+
let totalWidth = graphWidth;
|
|
6457
|
+
let totalHeight = graphHeight;
|
|
6458
|
+
if (includeLegend) {
|
|
6459
|
+
const legendEntries = extractLegendData(container);
|
|
6460
|
+
if (legendEntries.length > 0) {
|
|
6461
|
+
const legendDimensions = calculateLegendDimensions(legendEntries, graphWidth);
|
|
6462
|
+
totalWidth = graphWidth + 20 + legendDimensions.width + 20;
|
|
6463
|
+
totalHeight = Math.max(graphHeight, legendDimensions.height + 40);
|
|
6464
|
+
svgClone.setAttribute("width", totalWidth.toString());
|
|
6465
|
+
svgClone.setAttribute("height", totalHeight.toString());
|
|
6466
|
+
svgClone.setAttribute("viewBox", `0 0 ${totalWidth} ${totalHeight}`);
|
|
6467
|
+
const legendGroup = createLegendSVGElement(legendEntries, legendDimensions);
|
|
6468
|
+
svgClone.appendChild(legendGroup);
|
|
6469
|
+
}
|
|
6470
|
+
}
|
|
6471
|
+
svgClone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
6472
|
+
svgClone.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
|
|
6473
|
+
const svgString = new XMLSerializer().serializeToString(svgClone);
|
|
6474
|
+
const svgBlob = new Blob([svgString], { type: "image/svg+xml;charset=utf-8" });
|
|
6475
|
+
const svgUrl = URL.createObjectURL(svgBlob);
|
|
6476
|
+
try {
|
|
6477
|
+
const canvas = document.createElement("canvas");
|
|
6478
|
+
const ctx = canvas.getContext("2d");
|
|
6479
|
+
if (!ctx) {
|
|
6480
|
+
throw new Error("Could not get canvas context");
|
|
6481
|
+
}
|
|
6482
|
+
canvas.width = totalWidth * scale;
|
|
6483
|
+
canvas.height = totalHeight * scale;
|
|
6484
|
+
ctx.scale(scale, scale);
|
|
6485
|
+
ctx.fillStyle = backgroundColor;
|
|
6486
|
+
ctx.fillRect(0, 0, totalWidth, totalHeight);
|
|
6487
|
+
await new Promise((resolve, reject) => {
|
|
6488
|
+
const img = new Image();
|
|
6489
|
+
const cleanup = () => {
|
|
6490
|
+
img.onload = null;
|
|
6491
|
+
img.onerror = null;
|
|
6492
|
+
img.src = "";
|
|
6493
|
+
URL.revokeObjectURL(svgUrl);
|
|
4603
6494
|
};
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
6495
|
+
img.onload = () => {
|
|
6496
|
+
try {
|
|
6497
|
+
ctx.drawImage(img, 0, 0, totalWidth, totalHeight);
|
|
6498
|
+
canvas.toBlob((blob) => {
|
|
6499
|
+
cleanup();
|
|
6500
|
+
if (blob) {
|
|
6501
|
+
const url = URL.createObjectURL(blob);
|
|
6502
|
+
const link = document.createElement("a");
|
|
6503
|
+
link.href = url;
|
|
6504
|
+
link.download = fileName;
|
|
6505
|
+
document.body.appendChild(link);
|
|
6506
|
+
link.click();
|
|
6507
|
+
document.body.removeChild(link);
|
|
6508
|
+
URL.revokeObjectURL(url);
|
|
6509
|
+
resolve();
|
|
6510
|
+
} else {
|
|
6511
|
+
reject(new Error("Failed to generate image blob"));
|
|
6512
|
+
}
|
|
6513
|
+
}, "image/png", 0.95);
|
|
6514
|
+
} catch (error) {
|
|
6515
|
+
cleanup();
|
|
6516
|
+
reject(error);
|
|
6517
|
+
}
|
|
4609
6518
|
};
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
return () => {
|
|
4614
|
-
linkSelectHandlers.delete(handler);
|
|
6519
|
+
img.onerror = () => {
|
|
6520
|
+
cleanup();
|
|
6521
|
+
reject(new Error("Failed to load SVG image"));
|
|
4615
6522
|
};
|
|
6523
|
+
img.src = svgUrl;
|
|
6524
|
+
});
|
|
6525
|
+
} catch (error) {
|
|
6526
|
+
URL.revokeObjectURL(svgUrl);
|
|
6527
|
+
throw error;
|
|
6528
|
+
}
|
|
6529
|
+
}
|
|
6530
|
+
function extractLegendData(container) {
|
|
6531
|
+
const legendEntries = [];
|
|
6532
|
+
const legendContainer = container.querySelector(".pg-legend");
|
|
6533
|
+
if (!legendContainer) {
|
|
6534
|
+
return legendEntries;
|
|
6535
|
+
}
|
|
6536
|
+
const legendItems = legendContainer.querySelectorAll(".pg-legend-item");
|
|
6537
|
+
legendItems.forEach((item) => {
|
|
6538
|
+
const swatch = item.querySelector(".pg-legend-swatch");
|
|
6539
|
+
const label = item.querySelector(".pg-legend-label");
|
|
6540
|
+
if (swatch && label) {
|
|
6541
|
+
const backgroundColor = swatch.style.backgroundColor || getComputedStyle(swatch).backgroundColor;
|
|
6542
|
+
const type = label.textContent || "Unknown";
|
|
6543
|
+
const color2 = normalizeColor(backgroundColor);
|
|
6544
|
+
legendEntries.push({
|
|
6545
|
+
type: type.trim(),
|
|
6546
|
+
color: color2
|
|
6547
|
+
});
|
|
4616
6548
|
}
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
6549
|
+
});
|
|
6550
|
+
return legendEntries;
|
|
6551
|
+
}
|
|
6552
|
+
function calculateLegendDimensions(legendEntries, graphWidth) {
|
|
6553
|
+
const longestTypeName = legendEntries.reduce((max, entry) => {
|
|
6554
|
+
return entry.type.length > max ? entry.type.length : max;
|
|
6555
|
+
}, 0);
|
|
6556
|
+
const minLegendWidth = 180;
|
|
6557
|
+
const calculatedWidth = longestTypeName * 8 + 60;
|
|
6558
|
+
const width = Math.max(minLegendWidth, Math.min(calculatedWidth, 320));
|
|
6559
|
+
const itemHeight = 24;
|
|
6560
|
+
const itemSpacing = 8;
|
|
6561
|
+
const padding = 16;
|
|
6562
|
+
const height = padding + legendEntries.length * (itemHeight + itemSpacing) - itemSpacing + padding;
|
|
6563
|
+
const legendMargin = 20;
|
|
6564
|
+
const x3 = graphWidth + legendMargin;
|
|
6565
|
+
const y3 = 20;
|
|
6566
|
+
return {
|
|
6567
|
+
width,
|
|
6568
|
+
height,
|
|
6569
|
+
x: x3,
|
|
6570
|
+
y: y3,
|
|
6571
|
+
padding,
|
|
6572
|
+
itemHeight,
|
|
6573
|
+
itemSpacing
|
|
6574
|
+
};
|
|
6575
|
+
}
|
|
6576
|
+
function createLegendSVGElement(legendEntries, dimensions) {
|
|
6577
|
+
const {
|
|
6578
|
+
x: legendX,
|
|
6579
|
+
y: legendY,
|
|
6580
|
+
width: legendWidth,
|
|
6581
|
+
height: legendHeight,
|
|
6582
|
+
padding: legendPadding,
|
|
6583
|
+
itemHeight: legendItemHeight,
|
|
6584
|
+
itemSpacing: legendItemSpacing
|
|
6585
|
+
} = dimensions;
|
|
6586
|
+
const legendGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
6587
|
+
legendGroup.setAttribute("class", "export-legend");
|
|
6588
|
+
const legendBg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
6589
|
+
legendBg.setAttribute("x", legendX.toString());
|
|
6590
|
+
legendBg.setAttribute("y", legendY.toString());
|
|
6591
|
+
legendBg.setAttribute("width", legendWidth.toString());
|
|
6592
|
+
legendBg.setAttribute("height", legendHeight.toString());
|
|
6593
|
+
legendBg.setAttribute("fill", "#ffffff");
|
|
6594
|
+
legendBg.setAttribute("stroke", "#e2e8f0");
|
|
6595
|
+
legendBg.setAttribute("stroke-width", "1");
|
|
6596
|
+
legendBg.setAttribute("rx", "8");
|
|
6597
|
+
legendGroup.appendChild(legendBg);
|
|
6598
|
+
legendEntries.forEach((entry, index2) => {
|
|
6599
|
+
const itemY = legendY + legendPadding + index2 * (legendItemHeight + legendItemSpacing);
|
|
6600
|
+
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
|
6601
|
+
circle.setAttribute("cx", (legendX + legendPadding + 7).toString());
|
|
6602
|
+
circle.setAttribute("cy", (itemY + legendItemHeight / 2).toString());
|
|
6603
|
+
circle.setAttribute("r", "7");
|
|
6604
|
+
circle.setAttribute("fill", entry.color);
|
|
6605
|
+
circle.setAttribute("stroke", "none");
|
|
6606
|
+
legendGroup.appendChild(circle);
|
|
6607
|
+
const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
|
6608
|
+
text.setAttribute("x", (legendX + legendPadding + 24).toString());
|
|
6609
|
+
text.setAttribute("y", (itemY + legendItemHeight / 2).toString());
|
|
6610
|
+
text.setAttribute("font-family", "Inter, -apple-system, BlinkMacSystemFont, sans-serif");
|
|
6611
|
+
text.setAttribute("font-size", "12");
|
|
6612
|
+
text.setAttribute("fill", "#6b7280");
|
|
6613
|
+
text.setAttribute("dominant-baseline", "middle");
|
|
6614
|
+
text.textContent = entry.type;
|
|
6615
|
+
legendGroup.appendChild(text);
|
|
6616
|
+
});
|
|
6617
|
+
return legendGroup;
|
|
6618
|
+
}
|
|
6619
|
+
function normalizeColor(color2) {
|
|
6620
|
+
const rgbMatch = color2.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
|
|
6621
|
+
if (rgbMatch) {
|
|
6622
|
+
const r = parseInt(rgbMatch[1] ?? "0");
|
|
6623
|
+
const g = parseInt(rgbMatch[2] ?? "0");
|
|
6624
|
+
const b = parseInt(rgbMatch[3] ?? "0");
|
|
6625
|
+
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
6626
|
+
}
|
|
6627
|
+
return color2;
|
|
6628
|
+
}
|
|
6629
|
+
|
|
6630
|
+
// src/utils/validation.ts
|
|
6631
|
+
var GraphValidator = class {
|
|
6632
|
+
/**
|
|
6633
|
+
* Validate complete graph configuration
|
|
6634
|
+
*/
|
|
6635
|
+
static validateConfig(config) {
|
|
6636
|
+
const errors = [];
|
|
6637
|
+
const warnings = [];
|
|
6638
|
+
const containerValidation = this.validateContainer(config.container);
|
|
6639
|
+
errors.push(...containerValidation.errors);
|
|
6640
|
+
warnings.push(...containerValidation.warnings);
|
|
6641
|
+
const nodeValidation = this.validateNodes(config.nodes);
|
|
6642
|
+
errors.push(...nodeValidation.errors);
|
|
6643
|
+
warnings.push(...nodeValidation.warnings);
|
|
6644
|
+
const linkValidation = this.validateLinks(config.links, config.nodes);
|
|
6645
|
+
errors.push(...linkValidation.errors);
|
|
6646
|
+
warnings.push(...linkValidation.warnings);
|
|
6647
|
+
if (config.interaction) {
|
|
6648
|
+
const interactionValidation = this.validateInteraction(config.interaction);
|
|
6649
|
+
errors.push(...interactionValidation.errors);
|
|
6650
|
+
warnings.push(...interactionValidation.warnings);
|
|
6651
|
+
}
|
|
6652
|
+
return {
|
|
6653
|
+
isValid: errors.length === 0,
|
|
6654
|
+
errors,
|
|
6655
|
+
warnings
|
|
4620
6656
|
};
|
|
4621
6657
|
}
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
6658
|
+
/**
|
|
6659
|
+
* Validate container element
|
|
6660
|
+
*/
|
|
6661
|
+
static validateContainer(container) {
|
|
6662
|
+
const errors = [];
|
|
6663
|
+
const warnings = [];
|
|
6664
|
+
if (!container) {
|
|
6665
|
+
errors.push({
|
|
6666
|
+
field: "container",
|
|
6667
|
+
message: "Container element is required",
|
|
6668
|
+
value: container,
|
|
6669
|
+
code: "CONTAINER_REQUIRED"
|
|
6670
|
+
});
|
|
6671
|
+
return { isValid: false, errors, warnings };
|
|
6672
|
+
}
|
|
6673
|
+
if (!(container instanceof HTMLElement)) {
|
|
6674
|
+
errors.push({
|
|
6675
|
+
field: "container",
|
|
6676
|
+
message: "Container must be a valid HTMLElement",
|
|
6677
|
+
value: container,
|
|
6678
|
+
code: "CONTAINER_INVALID_TYPE"
|
|
6679
|
+
});
|
|
4631
6680
|
}
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
const
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
6681
|
+
if (!document.contains(container)) {
|
|
6682
|
+
warnings.push("Container element is not attached to the DOM");
|
|
6683
|
+
}
|
|
6684
|
+
const rect = container.getBoundingClientRect();
|
|
6685
|
+
if (rect.width === 0 || rect.height === 0) {
|
|
6686
|
+
warnings.push("Container has zero width or height, graph may not render properly");
|
|
6687
|
+
}
|
|
6688
|
+
const styles = window.getComputedStyle(container);
|
|
6689
|
+
if (styles.position === "static") {
|
|
6690
|
+
warnings.push("Container should have position: relative for proper overlay positioning");
|
|
6691
|
+
}
|
|
6692
|
+
return { isValid: errors.length === 0, errors, warnings };
|
|
6693
|
+
}
|
|
6694
|
+
/**
|
|
6695
|
+
* Validate graph nodes
|
|
6696
|
+
*/
|
|
6697
|
+
static validateNodes(nodes) {
|
|
6698
|
+
const errors = [];
|
|
6699
|
+
const warnings = [];
|
|
6700
|
+
if (!Array.isArray(nodes)) {
|
|
6701
|
+
errors.push({
|
|
6702
|
+
field: "nodes",
|
|
6703
|
+
message: "Nodes must be an array",
|
|
6704
|
+
value: nodes,
|
|
6705
|
+
code: "NODES_INVALID_TYPE"
|
|
6706
|
+
});
|
|
6707
|
+
return { isValid: false, errors, warnings };
|
|
6708
|
+
}
|
|
6709
|
+
if (nodes.length === 0) {
|
|
6710
|
+
warnings.push("No nodes provided, graph will be empty");
|
|
6711
|
+
return { isValid: true, errors, warnings };
|
|
6712
|
+
}
|
|
6713
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
6714
|
+
const duplicateIds = /* @__PURE__ */ new Set();
|
|
6715
|
+
nodes.forEach((node, index2) => {
|
|
6716
|
+
const nodePrefix = `nodes[${index2}]`;
|
|
6717
|
+
if (!node || typeof node !== "object") {
|
|
6718
|
+
errors.push({
|
|
6719
|
+
field: `${nodePrefix}`,
|
|
6720
|
+
message: "Node must be an object",
|
|
6721
|
+
value: node,
|
|
6722
|
+
code: "NODE_INVALID_TYPE"
|
|
6723
|
+
});
|
|
6724
|
+
return;
|
|
6725
|
+
}
|
|
6726
|
+
if (!node.id || typeof node.id !== "string") {
|
|
6727
|
+
errors.push({
|
|
6728
|
+
field: `${nodePrefix}.id`,
|
|
6729
|
+
message: "Node ID is required and must be a string",
|
|
6730
|
+
value: node.id,
|
|
6731
|
+
code: "NODE_ID_REQUIRED"
|
|
6732
|
+
});
|
|
6733
|
+
} else {
|
|
6734
|
+
if (nodeIds.has(node.id)) {
|
|
6735
|
+
duplicateIds.add(node.id);
|
|
6736
|
+
errors.push({
|
|
6737
|
+
field: `${nodePrefix}.id`,
|
|
6738
|
+
message: `Duplicate node ID: ${node.id}`,
|
|
6739
|
+
value: node.id,
|
|
6740
|
+
code: "NODE_ID_DUPLICATE"
|
|
6741
|
+
});
|
|
6742
|
+
}
|
|
6743
|
+
nodeIds.add(node.id);
|
|
6744
|
+
}
|
|
6745
|
+
if (!node.type || typeof node.type !== "string") {
|
|
6746
|
+
errors.push({
|
|
6747
|
+
field: `${nodePrefix}.type`,
|
|
6748
|
+
message: "Node type is required and must be a string",
|
|
6749
|
+
value: node.type,
|
|
6750
|
+
code: "NODE_TYPE_REQUIRED"
|
|
6751
|
+
});
|
|
6752
|
+
}
|
|
6753
|
+
if (node.style) {
|
|
6754
|
+
const styleValidation = this.validateNodeStyle(node.style, `${nodePrefix}.style`);
|
|
6755
|
+
errors.push(...styleValidation.errors);
|
|
6756
|
+
warnings.push(...styleValidation.warnings);
|
|
6757
|
+
}
|
|
4660
6758
|
});
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
const
|
|
4668
|
-
const
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
};
|
|
4676
|
-
const simulationResult = createGraphSimulation(simulationConfig);
|
|
4677
|
-
simulation = simulationResult.simulation;
|
|
4678
|
-
simulation.on("tick", () => {
|
|
4679
|
-
linkSelection.attr("x1", (item) => item.link.source.x ?? 0).attr("y1", (item) => item.link.source.y ?? 0).attr("x2", (item) => getShortenedTargetPoint(item.link, item.style).x).attr("y2", (item) => getShortenedTargetPoint(item.link, item.style).y);
|
|
4680
|
-
linkLabelSelection.attr("transform", (item) => {
|
|
4681
|
-
const link = item.link;
|
|
4682
|
-
const source = link.source;
|
|
4683
|
-
const targetPoint = getLinkTargetPoint(link);
|
|
4684
|
-
const x3 = ((source.x ?? 0) + targetPoint.x) / 2;
|
|
4685
|
-
const y3 = ((source.y ?? 0) + targetPoint.y) / 2;
|
|
4686
|
-
return `translate(${x3}, ${y3})`;
|
|
4687
|
-
}).each(function() {
|
|
4688
|
-
const group = this;
|
|
4689
|
-
const text = group.querySelector("text");
|
|
4690
|
-
const rect = group.querySelector("rect");
|
|
4691
|
-
if (!text || !rect) return;
|
|
4692
|
-
const bBox = text.getBBox();
|
|
4693
|
-
const padding = 6;
|
|
4694
|
-
rect.setAttribute("x", String(bBox.x - padding));
|
|
4695
|
-
rect.setAttribute("y", String(bBox.y - padding));
|
|
4696
|
-
rect.setAttribute("width", String(bBox.width + padding * 2));
|
|
4697
|
-
rect.setAttribute("height", String(bBox.height + padding * 2));
|
|
6759
|
+
return { isValid: errors.length === 0, errors, warnings };
|
|
6760
|
+
}
|
|
6761
|
+
/**
|
|
6762
|
+
* Validate graph links
|
|
6763
|
+
*/
|
|
6764
|
+
static validateLinks(links, nodes) {
|
|
6765
|
+
const errors = [];
|
|
6766
|
+
const warnings = [];
|
|
6767
|
+
if (!Array.isArray(links)) {
|
|
6768
|
+
errors.push({
|
|
6769
|
+
field: "links",
|
|
6770
|
+
message: "Links must be an array",
|
|
6771
|
+
value: links,
|
|
6772
|
+
code: "LINKS_INVALID_TYPE"
|
|
4698
6773
|
});
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
6774
|
+
return { isValid: false, errors, warnings };
|
|
6775
|
+
}
|
|
6776
|
+
const nodeIds = new Set(nodes.map((n) => n.id).filter(Boolean));
|
|
6777
|
+
links.forEach((link, index2) => {
|
|
6778
|
+
const linkPrefix = `links[${index2}]`;
|
|
6779
|
+
if (!link || typeof link !== "object") {
|
|
6780
|
+
errors.push({
|
|
6781
|
+
field: `${linkPrefix}`,
|
|
6782
|
+
message: "Link must be an object",
|
|
6783
|
+
value: link,
|
|
6784
|
+
code: "LINK_INVALID_TYPE"
|
|
6785
|
+
});
|
|
6786
|
+
return;
|
|
6787
|
+
}
|
|
6788
|
+
const sourceId = typeof link.source === "string" ? link.source : link.source?.id;
|
|
6789
|
+
if (!sourceId) {
|
|
6790
|
+
errors.push({
|
|
6791
|
+
field: `${linkPrefix}.source`,
|
|
6792
|
+
message: "Link source is required",
|
|
6793
|
+
value: link.source,
|
|
6794
|
+
code: "LINK_SOURCE_REQUIRED"
|
|
6795
|
+
});
|
|
6796
|
+
} else if (!nodeIds.has(sourceId)) {
|
|
6797
|
+
errors.push({
|
|
6798
|
+
field: `${linkPrefix}.source`,
|
|
6799
|
+
message: `Link source node not found: ${sourceId}`,
|
|
6800
|
+
value: sourceId,
|
|
6801
|
+
code: "LINK_SOURCE_NOT_FOUND"
|
|
6802
|
+
});
|
|
6803
|
+
}
|
|
6804
|
+
const targetId = typeof link.target === "string" ? link.target : link.target?.id;
|
|
6805
|
+
if (!targetId) {
|
|
6806
|
+
errors.push({
|
|
6807
|
+
field: `${linkPrefix}.target`,
|
|
6808
|
+
message: "Link target is required",
|
|
6809
|
+
value: link.target,
|
|
6810
|
+
code: "LINK_TARGET_REQUIRED"
|
|
6811
|
+
});
|
|
6812
|
+
} else if (!nodeIds.has(targetId)) {
|
|
6813
|
+
errors.push({
|
|
6814
|
+
field: `${linkPrefix}.target`,
|
|
6815
|
+
message: `Link target node not found: ${targetId}`,
|
|
6816
|
+
value: targetId,
|
|
6817
|
+
code: "LINK_TARGET_NOT_FOUND"
|
|
6818
|
+
});
|
|
6819
|
+
}
|
|
6820
|
+
if (sourceId === targetId) {
|
|
6821
|
+
warnings.push(`Self-loop detected in link ${index2}: ${sourceId} -> ${targetId}`);
|
|
6822
|
+
}
|
|
4702
6823
|
});
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
6824
|
+
return { isValid: errors.length === 0, errors, warnings };
|
|
6825
|
+
}
|
|
6826
|
+
/**
|
|
6827
|
+
* Validate interaction configuration
|
|
6828
|
+
*/
|
|
6829
|
+
static validateInteraction(interaction) {
|
|
6830
|
+
const errors = [];
|
|
6831
|
+
const warnings = [];
|
|
6832
|
+
if (interaction.hover?.tooltip?.enabled) {
|
|
6833
|
+
if (interaction.hover.tooltip.renderContent && typeof interaction.hover.tooltip.renderContent !== "function") {
|
|
6834
|
+
errors.push({
|
|
6835
|
+
field: "interaction.hover.tooltip.renderContent",
|
|
6836
|
+
message: "renderContent must be a function",
|
|
6837
|
+
value: interaction.hover.tooltip.renderContent,
|
|
6838
|
+
code: "TOOLTIP_RENDER_FUNCTION_INVALID"
|
|
4709
6839
|
});
|
|
4710
6840
|
}
|
|
4711
|
-
createNodeHover(nodeSelection, config.interaction.hover.nodeStyle);
|
|
4712
|
-
}
|
|
4713
|
-
if (config.interaction?.drag?.enabled !== false) {
|
|
4714
|
-
nodeSelection.call(createDragBehavior(simulation));
|
|
4715
6841
|
}
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
6842
|
+
return { isValid: errors.length === 0, errors, warnings };
|
|
6843
|
+
}
|
|
6844
|
+
/**
|
|
6845
|
+
* Validate node style
|
|
6846
|
+
*/
|
|
6847
|
+
static validateNodeStyle(style, fieldPrefix) {
|
|
6848
|
+
const errors = [];
|
|
6849
|
+
const warnings = [];
|
|
6850
|
+
if (typeof style !== "object" || style === null) {
|
|
6851
|
+
errors.push({
|
|
6852
|
+
field: fieldPrefix,
|
|
6853
|
+
message: "Style must be an object",
|
|
6854
|
+
value: style,
|
|
6855
|
+
code: "STYLE_INVALID_TYPE"
|
|
4724
6856
|
});
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
}
|
|
4747
|
-
if (nodeStyle.stroke !== void 0) {
|
|
4748
|
-
nodeElement.style.stroke = nodeStyle.stroke;
|
|
4749
|
-
}
|
|
4750
|
-
if (nodeStyle.strokeWidth !== void 0) {
|
|
4751
|
-
nodeElement.style.strokeWidth = String(nodeStyle.strokeWidth);
|
|
4752
|
-
}
|
|
4753
|
-
if (nodeStyle.opacity !== void 0) {
|
|
4754
|
-
nodeElement.style.opacity = String(nodeStyle.opacity);
|
|
4755
|
-
}
|
|
4756
|
-
if (nodeStyle.radius !== void 0) {
|
|
4757
|
-
nodeElement.style.setProperty("r", String(nodeStyle.radius));
|
|
4758
|
-
}
|
|
4759
|
-
}
|
|
4760
|
-
root2.selectAll(".link-label").filter((item) => {
|
|
4761
|
-
if (item.style.label.visibility !== "hover") {
|
|
4762
|
-
return false;
|
|
4763
|
-
}
|
|
4764
|
-
const source = item.link.source;
|
|
4765
|
-
const target = item.link.target;
|
|
4766
|
-
return source.id === node.id || target.id === node.id;
|
|
4767
|
-
}).classed("label-selection-pinned", true).interrupt().transition().duration(200).style("opacity", 1).style("pointer-events", "auto");
|
|
4768
|
-
nodeSelectHandlers.forEach((handler) => handler(node, nodeElement));
|
|
4769
|
-
});
|
|
4770
|
-
linkSelection.on("click.select", function(event, renderableLink) {
|
|
4771
|
-
const linkElement = this;
|
|
4772
|
-
if (selectedLinkElement === linkElement) {
|
|
4773
|
-
deselectLink(linkElement, linkMarkerSnapshots, linkSelectHandlers, linkDeselectHandlers);
|
|
4774
|
-
selectedLinkElement = null;
|
|
4775
|
-
return;
|
|
4776
|
-
}
|
|
4777
|
-
if (selectedLinkElement) {
|
|
4778
|
-
deselectLink(selectedLinkElement, linkMarkerSnapshots, linkSelectHandlers, linkDeselectHandlers);
|
|
4779
|
-
selectedLinkElement = null;
|
|
4780
|
-
}
|
|
4781
|
-
if (selectedNodeElement) {
|
|
4782
|
-
deselectNode(selectedNodeElement, root2, nodeSelectHandlers, nodeDeselectHandlers);
|
|
4783
|
-
selectedNodeElement = null;
|
|
6857
|
+
return { isValid: false, errors, warnings };
|
|
6858
|
+
}
|
|
6859
|
+
if (style.radius !== void 0) {
|
|
6860
|
+
if (typeof style.radius !== "number" || style.radius < 0) {
|
|
6861
|
+
errors.push({
|
|
6862
|
+
field: `${fieldPrefix}.radius`,
|
|
6863
|
+
message: "Radius must be a non-negative number",
|
|
6864
|
+
value: style.radius,
|
|
6865
|
+
code: "RADIUS_INVALID"
|
|
6866
|
+
});
|
|
6867
|
+
}
|
|
6868
|
+
}
|
|
6869
|
+
const colorFields = ["fill", "stroke", "textColor"];
|
|
6870
|
+
colorFields.forEach((field) => {
|
|
6871
|
+
if (style[field] !== void 0) {
|
|
6872
|
+
if (typeof style[field] !== "string") {
|
|
6873
|
+
errors.push({
|
|
6874
|
+
field: `${fieldPrefix}.${field}`,
|
|
6875
|
+
message: `${field} must be a string`,
|
|
6876
|
+
value: style[field],
|
|
6877
|
+
code: "COLOR_INVALID_TYPE"
|
|
6878
|
+
});
|
|
4784
6879
|
}
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
6880
|
+
}
|
|
6881
|
+
});
|
|
6882
|
+
return { isValid: errors.length === 0, errors, warnings };
|
|
6883
|
+
}
|
|
6884
|
+
/**
|
|
6885
|
+
* Validate runtime environment
|
|
6886
|
+
*/
|
|
6887
|
+
static validateEnvironment() {
|
|
6888
|
+
const errors = [];
|
|
6889
|
+
const warnings = [];
|
|
6890
|
+
if (typeof window === "undefined") {
|
|
6891
|
+
errors.push({
|
|
6892
|
+
field: "environment",
|
|
6893
|
+
message: "Graph library requires a browser environment",
|
|
6894
|
+
value: "server",
|
|
6895
|
+
code: "ENVIRONMENT_NOT_BROWSER"
|
|
4791
6896
|
});
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
}
|
|
4800
|
-
if (selectedLinkElement) {
|
|
4801
|
-
deselectLink(selectedLinkElement, linkMarkerSnapshots, linkSelectHandlers, linkDeselectHandlers);
|
|
4802
|
-
selectedLinkElement = null;
|
|
4803
|
-
}
|
|
4804
|
-
if (selectedNodeElement) {
|
|
4805
|
-
deselectNode(selectedNodeElement, root2, nodeSelectHandlers, nodeDeselectHandlers);
|
|
4806
|
-
selectedNodeElement = null;
|
|
4807
|
-
}
|
|
4808
|
-
selectedLinkElement = visibleLinkNode;
|
|
4809
|
-
selectLink(event, renderableLink, visibleLinkNode, selectionConfig, layers, linkSelectHandlers);
|
|
4810
|
-
}
|
|
6897
|
+
}
|
|
6898
|
+
if (typeof document === "undefined") {
|
|
6899
|
+
errors.push({
|
|
6900
|
+
field: "environment.document",
|
|
6901
|
+
message: "Document API not available",
|
|
6902
|
+
value: void 0,
|
|
6903
|
+
code: "DOCUMENT_API_MISSING"
|
|
4811
6904
|
});
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
6905
|
+
}
|
|
6906
|
+
if (typeof ResizeObserver === "undefined") {
|
|
6907
|
+
warnings.push("ResizeObserver not available, responsive features may not work");
|
|
6908
|
+
}
|
|
6909
|
+
if (typeof requestAnimationFrame === "undefined") {
|
|
6910
|
+
warnings.push("requestAnimationFrame not available, performance optimizations disabled");
|
|
6911
|
+
}
|
|
6912
|
+
return { isValid: errors.length === 0, errors, warnings };
|
|
6913
|
+
}
|
|
6914
|
+
};
|
|
6915
|
+
var GraphValidationError = class extends Error {
|
|
6916
|
+
errors;
|
|
6917
|
+
warnings;
|
|
6918
|
+
constructor(result) {
|
|
6919
|
+
const errorMessages = result.errors.map((e) => `${e.field}: ${e.message}`).join(", ");
|
|
6920
|
+
super(`Graph validation failed: ${errorMessages}`);
|
|
6921
|
+
this.name = "GraphValidationError";
|
|
6922
|
+
this.errors = result.errors;
|
|
6923
|
+
this.warnings = result.warnings;
|
|
6924
|
+
}
|
|
6925
|
+
};
|
|
6926
|
+
|
|
6927
|
+
// src/create-graph.ts
|
|
6928
|
+
function createGraph(config) {
|
|
6929
|
+
const envValidation = GraphValidator.validateEnvironment();
|
|
6930
|
+
if (!envValidation.isValid) {
|
|
6931
|
+
throw new GraphValidationError(envValidation);
|
|
6932
|
+
}
|
|
6933
|
+
if (envValidation.warnings.length > 0) {
|
|
6934
|
+
console.warn("[Polly Graph] Environment warnings:", envValidation.warnings);
|
|
6935
|
+
}
|
|
6936
|
+
const configValidation = GraphValidator.validateConfig(config);
|
|
6937
|
+
if (!configValidation.isValid) {
|
|
6938
|
+
throw new GraphValidationError(configValidation);
|
|
6939
|
+
}
|
|
6940
|
+
if (configValidation.warnings.length > 0) {
|
|
6941
|
+
console.warn("[Polly Graph] Configuration warnings:", configValidation.warnings);
|
|
6942
|
+
}
|
|
6943
|
+
const graphManager = new GraphManager(config);
|
|
6944
|
+
const renderPipeline = new RenderPipeline(graphManager);
|
|
6945
|
+
const interactionManager = new InteractionManager(graphManager);
|
|
6946
|
+
function render() {
|
|
6947
|
+
try {
|
|
6948
|
+
renderPipeline.execute().then((selections) => {
|
|
6949
|
+
interactionManager.setupInteractions(selections);
|
|
6950
|
+
setupAdditionalComponents();
|
|
6951
|
+
if (graphManager.needsImmediateFitView) {
|
|
6952
|
+
graphManager.needsImmediateFitView = false;
|
|
6953
|
+
fitViewWithInitialPositions();
|
|
4820
6954
|
}
|
|
6955
|
+
}).catch((error) => {
|
|
6956
|
+
console.error("[Polly Graph] Render failed:", error);
|
|
4821
6957
|
});
|
|
6958
|
+
} catch (error) {
|
|
6959
|
+
console.error("[Polly Graph] Render failed:", error);
|
|
4822
6960
|
}
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
6961
|
+
}
|
|
6962
|
+
function setupAdditionalComponents() {
|
|
6963
|
+
if (config.controls?.enabled && graphManager.layers) {
|
|
6964
|
+
graphManager.controls = createGraphControls(
|
|
6965
|
+
graphManager.layers.overlay,
|
|
4826
6966
|
{ zoomIn, zoomOut, resetView, fitView },
|
|
4827
6967
|
config.controls
|
|
4828
6968
|
);
|
|
4829
|
-
controls.mount();
|
|
6969
|
+
graphManager.controls.mount();
|
|
6970
|
+
}
|
|
6971
|
+
if (config.legend?.enabled && graphManager.layers) {
|
|
6972
|
+
const legendCleanup = createGraphLegend(
|
|
6973
|
+
graphManager.layers.overlay,
|
|
6974
|
+
config.legend,
|
|
6975
|
+
config.nodes
|
|
6976
|
+
);
|
|
6977
|
+
graphManager.addCleanup(legendCleanup);
|
|
4830
6978
|
}
|
|
4831
|
-
|
|
4832
|
-
|
|
6979
|
+
const handleVisibilityChange = () => {
|
|
6980
|
+
if (!graphManager.simulation || !graphManager.timerManager) return;
|
|
6981
|
+
graphManager.timerManager.clearTimer("simulation-cooldown");
|
|
6982
|
+
if (document.hidden) {
|
|
6983
|
+
graphManager.simulation.stop();
|
|
6984
|
+
return;
|
|
6985
|
+
}
|
|
6986
|
+
graphManager.reheatSimulation(0.3);
|
|
6987
|
+
};
|
|
6988
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
6989
|
+
graphManager.addCleanup(() => {
|
|
6990
|
+
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
6991
|
+
});
|
|
6992
|
+
}
|
|
6993
|
+
function zoomIn() {
|
|
6994
|
+
if (!graphManager.zoomBehavior || !graphManager.svgElement) {
|
|
6995
|
+
console.warn("[Polly Graph] Zoom behavior not available");
|
|
6996
|
+
return;
|
|
6997
|
+
}
|
|
6998
|
+
const svg = graphManager.svgElement;
|
|
6999
|
+
select_default2(svg).transition().duration(400).call(graphManager.zoomBehavior.scaleBy, 1.5);
|
|
7000
|
+
}
|
|
7001
|
+
function zoomOut() {
|
|
7002
|
+
if (!graphManager.zoomBehavior || !graphManager.svgElement) {
|
|
7003
|
+
console.warn("[Polly Graph] Zoom behavior not available");
|
|
7004
|
+
return;
|
|
4833
7005
|
}
|
|
7006
|
+
const svg = graphManager.svgElement;
|
|
7007
|
+
select_default2(svg).transition().duration(400).call(graphManager.zoomBehavior.scaleBy, 1 / 1.5);
|
|
4834
7008
|
}
|
|
4835
7009
|
function resetView() {
|
|
4836
|
-
if (!zoomBehavior || !svgElement) {
|
|
7010
|
+
if (!graphManager.zoomBehavior || !graphManager.svgElement) {
|
|
7011
|
+
console.warn("[Polly Graph] Zoom behavior not available");
|
|
4837
7012
|
return;
|
|
4838
7013
|
}
|
|
4839
|
-
|
|
7014
|
+
const svg = graphManager.svgElement;
|
|
7015
|
+
select_default2(svg).transition().duration(400).call(graphManager.zoomBehavior.transform, identity2);
|
|
4840
7016
|
}
|
|
4841
7017
|
function fitView() {
|
|
4842
|
-
if (!
|
|
7018
|
+
if (!graphManager.simulation || !graphManager.svgElement) {
|
|
7019
|
+
console.warn("[Polly Graph] Cannot fit view: simulation or SVG not available");
|
|
4843
7020
|
return;
|
|
4844
7021
|
}
|
|
4845
|
-
const
|
|
4846
|
-
|
|
7022
|
+
const svg = graphManager.svgElement;
|
|
7023
|
+
const nodes = config.nodes;
|
|
7024
|
+
if (nodes.length === 0) return;
|
|
7025
|
+
const positions = nodes.map((node) => ({
|
|
7026
|
+
x: node.x ?? 0,
|
|
7027
|
+
y: node.y ?? 0
|
|
7028
|
+
}));
|
|
7029
|
+
const xExtent = (0, import_d3.extent)(positions, (d) => d.x);
|
|
7030
|
+
const yExtent = (0, import_d3.extent)(positions, (d) => d.y);
|
|
7031
|
+
const padding = 50;
|
|
7032
|
+
const width = graphManager.dimensions.width - padding * 2;
|
|
7033
|
+
const height = graphManager.dimensions.height - padding * 2;
|
|
7034
|
+
const nodeWidth = xExtent[1] - xExtent[0];
|
|
7035
|
+
const nodeHeight = yExtent[1] - yExtent[0];
|
|
7036
|
+
if (nodeWidth === 0 || nodeHeight === 0) {
|
|
4847
7037
|
return;
|
|
4848
7038
|
}
|
|
4849
|
-
const scale = Math.min(
|
|
4850
|
-
const
|
|
4851
|
-
const
|
|
4852
|
-
const transform2 = identity2.translate(
|
|
4853
|
-
|
|
7039
|
+
const scale = Math.min(width / nodeWidth, height / nodeHeight, 3);
|
|
7040
|
+
const centerX = (xExtent[0] + xExtent[1]) / 2;
|
|
7041
|
+
const centerY = (yExtent[0] + yExtent[1]) / 2;
|
|
7042
|
+
const transform2 = identity2.translate(graphManager.dimensions.width / 2, graphManager.dimensions.height / 2).scale(scale).translate(-centerX, -centerY);
|
|
7043
|
+
if (graphManager.zoomBehavior) {
|
|
7044
|
+
select_default2(svg).transition().duration(400).call(graphManager.zoomBehavior.transform, transform2);
|
|
7045
|
+
}
|
|
4854
7046
|
}
|
|
4855
|
-
function
|
|
4856
|
-
if (!
|
|
7047
|
+
function fitViewWithInitialPositions() {
|
|
7048
|
+
if (!graphManager.simulation || !graphManager.svgElement) {
|
|
7049
|
+
console.warn("[Polly Graph] Cannot fit view: simulation or SVG not available");
|
|
4857
7050
|
return;
|
|
4858
7051
|
}
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
7052
|
+
const svg = graphManager.svgElement;
|
|
7053
|
+
const nodes = config.nodes;
|
|
7054
|
+
if (nodes.length === 0) return;
|
|
7055
|
+
const positions = nodes.map((node) => ({
|
|
7056
|
+
x: node.initialX ?? node.x ?? 0,
|
|
7057
|
+
y: node.initialY ?? node.y ?? 0
|
|
7058
|
+
}));
|
|
7059
|
+
const xExtent = (0, import_d3.extent)(positions, (d) => d.x);
|
|
7060
|
+
const yExtent = (0, import_d3.extent)(positions, (d) => d.y);
|
|
7061
|
+
const padding = 50;
|
|
7062
|
+
const width = graphManager.dimensions.width - padding * 2;
|
|
7063
|
+
const height = graphManager.dimensions.height - padding * 2;
|
|
7064
|
+
const nodeWidth = xExtent[1] - xExtent[0];
|
|
7065
|
+
const nodeHeight = yExtent[1] - yExtent[0];
|
|
7066
|
+
if (nodeWidth === 0 || nodeHeight === 0) {
|
|
4863
7067
|
return;
|
|
4864
7068
|
}
|
|
4865
|
-
|
|
7069
|
+
const scale = Math.min(width / nodeWidth, height / nodeHeight, 3);
|
|
7070
|
+
const centerX = (xExtent[0] + xExtent[1]) / 2;
|
|
7071
|
+
const centerY = (yExtent[0] + yExtent[1]) / 2;
|
|
7072
|
+
const transform2 = identity2.translate(graphManager.dimensions.width / 2, graphManager.dimensions.height / 2).scale(scale).translate(-centerX, -centerY);
|
|
7073
|
+
if (graphManager.zoomBehavior) {
|
|
7074
|
+
select_default2(svg).transition().duration(400).call(graphManager.zoomBehavior.transform, transform2);
|
|
7075
|
+
}
|
|
4866
7076
|
}
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
7077
|
+
function exportGraph(fileName) {
|
|
7078
|
+
if (!graphManager.svgElement) {
|
|
7079
|
+
console.warn("[Polly Graph] Cannot export: no SVG element");
|
|
7080
|
+
return;
|
|
7081
|
+
}
|
|
7082
|
+
captureAndDownloadGraph(config.container, {
|
|
7083
|
+
fileName: fileName ?? "polly-graph-export",
|
|
4872
7084
|
pixelRatio: 2
|
|
4873
7085
|
});
|
|
4874
7086
|
}
|
|
4875
|
-
function
|
|
4876
|
-
if (
|
|
4877
|
-
|
|
4878
|
-
fitViewTimer = null;
|
|
4879
|
-
}
|
|
4880
|
-
if (cleanupResize) {
|
|
4881
|
-
cleanupResize();
|
|
4882
|
-
cleanupResize = null;
|
|
4883
|
-
}
|
|
4884
|
-
if (cleanupZoom) {
|
|
4885
|
-
cleanupZoom();
|
|
4886
|
-
cleanupZoom = null;
|
|
4887
|
-
}
|
|
4888
|
-
if (tooltipBinding) {
|
|
4889
|
-
tooltipBinding.destroy();
|
|
4890
|
-
tooltipBinding = null;
|
|
4891
|
-
}
|
|
4892
|
-
if (simulation) {
|
|
4893
|
-
simulation.stop();
|
|
4894
|
-
simulation = null;
|
|
7087
|
+
function clearSelection() {
|
|
7088
|
+
if (graphManager.selectionManager) {
|
|
7089
|
+
graphManager.selectionManager.clearSelection();
|
|
4895
7090
|
}
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
legendCleanup = null;
|
|
4903
|
-
}
|
|
4904
|
-
rootGroup = null;
|
|
4905
|
-
svgElement = null;
|
|
4906
|
-
zoomBehavior = null;
|
|
4907
|
-
while (config.container.firstChild) {
|
|
4908
|
-
config.container.removeChild(config.container.firstChild);
|
|
7091
|
+
}
|
|
7092
|
+
function on(event, handler) {
|
|
7093
|
+
if (!graphManager.eventEmitter) {
|
|
7094
|
+
console.warn("[Polly Graph] Event emitter not available");
|
|
7095
|
+
return () => {
|
|
7096
|
+
};
|
|
4909
7097
|
}
|
|
7098
|
+
switch (event) {
|
|
7099
|
+
case "nodeSelect":
|
|
7100
|
+
return graphManager.eventEmitter.on("nodeSelect", (data) => handler(data.node, data.element));
|
|
7101
|
+
case "nodeDeselect":
|
|
7102
|
+
return graphManager.eventEmitter.on("nodeDeselect", (data) => handler(data.node, data.element));
|
|
7103
|
+
case "linkSelect":
|
|
7104
|
+
return graphManager.eventEmitter.on("linkSelect", (data) => handler(data.link, data.element));
|
|
7105
|
+
case "linkDeselect":
|
|
7106
|
+
return graphManager.eventEmitter.on("linkDeselect", (data) => handler(data.link, data.element));
|
|
7107
|
+
default:
|
|
7108
|
+
console.warn("[Polly Graph] Unknown event:", event);
|
|
7109
|
+
return () => {
|
|
7110
|
+
};
|
|
7111
|
+
}
|
|
7112
|
+
}
|
|
7113
|
+
function off(event, _handler) {
|
|
7114
|
+
if (!graphManager.eventEmitter) return;
|
|
7115
|
+
graphManager.eventEmitter.removeAllListeners(event);
|
|
4910
7116
|
}
|
|
4911
|
-
|
|
7117
|
+
function destroy() {
|
|
7118
|
+
graphManager.destroy();
|
|
7119
|
+
}
|
|
7120
|
+
graphManager.fitViewCallback = () => {
|
|
7121
|
+
fitView();
|
|
7122
|
+
};
|
|
7123
|
+
return {
|
|
7124
|
+
render,
|
|
7125
|
+
zoomIn,
|
|
7126
|
+
zoomOut,
|
|
7127
|
+
resetView,
|
|
7128
|
+
fitView,
|
|
7129
|
+
destroy,
|
|
7130
|
+
exportGraph,
|
|
7131
|
+
clearSelection,
|
|
7132
|
+
on,
|
|
7133
|
+
off
|
|
7134
|
+
};
|
|
4912
7135
|
}
|
|
4913
7136
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4914
7137
|
0 && (module.exports = {
|
|
7138
|
+
ErrorHandler,
|
|
7139
|
+
GraphError,
|
|
7140
|
+
GraphValidationError,
|
|
7141
|
+
GraphValidator,
|
|
7142
|
+
SelectionManager,
|
|
7143
|
+
TypedGraphEventEmitter,
|
|
4915
7144
|
createGraph
|
|
4916
7145
|
});
|