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