polly-graph 0.1.6 → 0.1.8

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