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