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