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