polly-graph 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,198 +4261,336 @@ function createZoom({
3703
4261
  }
3704
4262
 
3705
4263
  // src/core/create-graph-simulation.ts
4264
+ function getAdaptiveDefaults(nodeCount) {
4265
+ if (nodeCount < 50) {
4266
+ return {
4267
+ alpha: 1,
4268
+ alphaDecay: 0.05,
4269
+ alphaMin: 1e-3,
4270
+ velocityDecay: 0.6,
4271
+ forces: {
4272
+ link: { strength: 0.5, distance: 200 },
4273
+ charge: { strength: -500 },
4274
+ center: { strength: 0.05 },
4275
+ collide: { strength: 0.8, iterations: 1 }
4276
+ }
4277
+ };
4278
+ } else if (nodeCount < 200) {
4279
+ return {
4280
+ alpha: 1,
4281
+ alphaDecay: 0.05,
4282
+ alphaMin: 1e-3,
4283
+ velocityDecay: 0.6,
4284
+ forces: {
4285
+ link: { strength: 0.5, distance: 200 },
4286
+ charge: { strength: -500 },
4287
+ center: { strength: 0.05 },
4288
+ collide: { strength: 0.7, iterations: 1 }
4289
+ }
4290
+ };
4291
+ } else {
4292
+ return {
4293
+ alpha: 1,
4294
+ alphaDecay: 0.05,
4295
+ alphaMin: 1e-3,
4296
+ velocityDecay: 0.6,
4297
+ forces: {
4298
+ link: { strength: 0.5, distance: 200 },
4299
+ charge: { strength: -500 },
4300
+ center: { strength: 0.05 },
4301
+ collide: { strength: 0.6, iterations: 1 }
4302
+ }
4303
+ };
4304
+ }
4305
+ }
4306
+ function warmupSimulation(simulation, ticks) {
4307
+ for (let i = 0; i < ticks; i++) {
4308
+ simulation.tick();
4309
+ }
4310
+ }
3706
4311
  function createGraphSimulation(config) {
3707
4312
  const centerX = config.width / 2;
3708
4313
  const centerY = config.height / 2;
3709
- const 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/controls/graph-controls.utils.ts
3735
- function resolveControlsPosition(position) {
3736
- return position ?? "bottom-left";
3737
- }
3738
- function resolveControlsOrientation(orientation) {
3739
- return orientation ?? "vertical";
3740
- }
3741
- function shouldRenderControl(config, key) {
3742
- const show = config.show;
3743
- if (!show) {
3744
- return true;
3745
- }
3746
- const value = show[key];
3747
- if (value === void 0) {
3748
- return true;
4390
+ function seedNodePositions(nodes, containerWidth, containerHeight) {
4391
+ if (containerWidth <= 0 || containerHeight <= 0) {
4392
+ console.warn("\u{1F6AB} [seedNodePositions] Invalid container dimensions, skipping positioning");
4393
+ return;
3749
4394
  }
3750
- return value;
4395
+ const centerX = containerWidth / 2;
4396
+ const centerY = containerHeight / 2;
4397
+ const padding = 50;
4398
+ const maxRadius = Math.min(
4399
+ (containerWidth - padding * 2) / 2,
4400
+ (containerHeight - padding * 2) / 2
4401
+ );
4402
+ const nodeBasedRadius = Math.max(50, Math.min(200, nodes.length * 3));
4403
+ const seedRadius = Math.min(maxRadius, nodeBasedRadius);
4404
+ nodes.forEach(
4405
+ (node, index2) => {
4406
+ if (node.x != null && node.y != null && !isNaN(node.x) && !isNaN(node.y) && isFinite(node.x) && isFinite(node.y)) {
4407
+ return;
4408
+ }
4409
+ const angle = index2 / Math.max(nodes.length, 1) * Math.PI * 2 + (Math.random() - 0.5) * 0.5;
4410
+ const radius = seedRadius * (0.3 + Math.random() * 0.7);
4411
+ const x3 = centerX + Math.cos(angle) * radius;
4412
+ const y3 = centerY + Math.sin(angle) * radius;
4413
+ const nodeRadius = 12;
4414
+ const clampedX = Math.max(nodeRadius, Math.min(containerWidth - nodeRadius, x3));
4415
+ const clampedY = Math.max(nodeRadius, Math.min(containerHeight - nodeRadius, y3));
4416
+ node.x = clampedX;
4417
+ node.y = clampedY;
4418
+ }
4419
+ );
4420
+ nodes.forEach((node) => {
4421
+ node.initialX = node.x;
4422
+ node.initialY = node.y;
4423
+ });
3751
4424
  }
3752
4425
 
3753
- // src/assets/plus.svg?raw
3754
- 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>';
3755
-
3756
- // src/assets/minus.svg?raw
3757
- 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>';
3758
-
3759
- // src/assets/fit.svg?raw
3760
- 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>';
3761
-
3762
- // src/assets/reset.svg?raw
3763
- 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>';
3764
-
3765
- // src/controls/graph-controls.icons.ts
3766
- var ICON_MAP = {
3767
- "zoom-in": plus_default,
3768
- "zoom-out": minus_default,
3769
- fit: fit_default,
3770
- reset: reset_default
3771
- };
3772
- function getControlIcon(icon) {
3773
- const raw = ICON_MAP[icon];
3774
- if (!raw) {
3775
- throw new Error(`Icon not found: ${icon}`);
3776
- }
3777
- return raw.replace("<svg", '<svg class="pg-icon"');
4426
+ // src/utils/observe-resize.ts
4427
+ function observeResize(element, onResize) {
4428
+ const observer = new ResizeObserver(
4429
+ (entries) => {
4430
+ const entry = entries[0];
4431
+ if (!entry) {
4432
+ return;
4433
+ }
4434
+ const width = entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width;
4435
+ const height = entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height;
4436
+ if (width > 0 && height > 0) {
4437
+ onResize(width, height);
4438
+ }
4439
+ }
4440
+ );
4441
+ observer.observe(element);
4442
+ return () => {
4443
+ observer.disconnect();
4444
+ };
3778
4445
  }
3779
4446
 
3780
- // src/controls/create-graph-controls.ts
3781
- function createGraphControls(overlay, graph, config) {
3782
- let root2 = null;
3783
- function mount() {
3784
- if (!config.enabled) {
4447
+ // src/utils/error-handler.ts
4448
+ var GraphError = class extends Error {
4449
+ code;
4450
+ context;
4451
+ recoverable;
4452
+ constructor(message, code, context, recoverable = false) {
4453
+ super(message);
4454
+ this.name = "GraphError";
4455
+ this.code = code;
4456
+ this.context = context;
4457
+ this.recoverable = recoverable;
4458
+ }
4459
+ };
4460
+ var ErrorHandler = class {
4461
+ static isDestroyed = false;
4462
+ /**
4463
+ * Handle errors with appropriate logging and recovery
4464
+ */
4465
+ static handle(error, context, fallback) {
4466
+ if (this.isDestroyed) return;
4467
+ const graphError = error instanceof GraphError ? error : new GraphError(
4468
+ error.message,
4469
+ "UNKNOWN_ERROR",
4470
+ context,
4471
+ true
4472
+ );
4473
+ console.error(`[Polly Graph] ${graphError.code}:`, {
4474
+ message: graphError.message,
4475
+ context: graphError.context,
4476
+ stack: graphError.stack
4477
+ });
4478
+ if (graphError.recoverable && fallback) {
4479
+ try {
4480
+ fallback();
4481
+ } catch (fallbackError) {
4482
+ console.error("[Polly Graph] Fallback failed:", fallbackError);
4483
+ }
4484
+ } else if (!graphError.recoverable) {
4485
+ throw graphError;
4486
+ }
4487
+ }
4488
+ /**
4489
+ * Wrap async operations with error handling
4490
+ */
4491
+ static async wrapAsync(operation, context, fallback) {
4492
+ try {
4493
+ return await operation();
4494
+ } catch (error) {
4495
+ this.handle(error, context, fallback ? () => fallback() : void 0);
4496
+ if (fallback) {
4497
+ return fallback();
4498
+ }
4499
+ throw error;
4500
+ }
4501
+ }
4502
+ /**
4503
+ * Wrap synchronous operations with error handling
4504
+ */
4505
+ static wrap(operation, context, fallback) {
4506
+ try {
4507
+ return operation();
4508
+ } catch (error) {
4509
+ this.handle(error, context, fallback);
4510
+ return fallback ? fallback() : void 0;
4511
+ }
4512
+ }
4513
+ /**
4514
+ * Safe DOM operation wrapper
4515
+ */
4516
+ static safeDOMOperation(operation, context, fallback) {
4517
+ if (!document || this.isDestroyed) {
4518
+ if (fallback) {
4519
+ return fallback();
4520
+ }
4521
+ return void 0;
4522
+ }
4523
+ return this.wrap(operation, {
4524
+ ...context,
4525
+ operation: `DOM: ${context.operation}`
4526
+ }, fallback);
4527
+ }
4528
+ /**
4529
+ * Safe D3 operation wrapper
4530
+ */
4531
+ static safeD3Operation(operation, context, fallback) {
4532
+ return this.wrap(operation, {
4533
+ ...context,
4534
+ operation: `D3: ${context.operation}`
4535
+ }, fallback);
4536
+ }
4537
+ /**
4538
+ * Create recoverable error for non-critical failures
4539
+ */
4540
+ static createRecoverableError(message, code, context) {
4541
+ return new GraphError(message, code, context, true);
4542
+ }
4543
+ /**
4544
+ * Create non-recoverable error for critical failures
4545
+ */
4546
+ static createCriticalError(message, code, context) {
4547
+ return new GraphError(message, code, context, false);
4548
+ }
4549
+ /**
4550
+ * Validate and handle D3 selection operations
4551
+ */
4552
+ static validateSelection(selection2, context, operation) {
4553
+ if (!selection2 || !selection2.size || selection2.size() === 0) {
4554
+ this.handle(
4555
+ this.createRecoverableError(
4556
+ "D3 selection is empty",
4557
+ "SELECTION_EMPTY",
4558
+ context
4559
+ ),
4560
+ context
4561
+ );
3785
4562
  return;
3786
4563
  }
3787
- root2 = document.createElement("div");
3788
- root2.className = "pg-controls";
3789
- const position = resolveControlsPosition(config.position);
3790
- root2.classList.add(`pg-pos-${position}`);
3791
- const orientation = resolveControlsOrientation(config.orientation);
3792
- root2.classList.add(`pg-orient-${orientation}`);
3793
- if (config.offset) {
3794
- root2.style.setProperty("--pg-controls-offset-x", `${config.offset.x}px`);
3795
- root2.style.setProperty("--pg-controls-offset-y", `${config.offset.y}px`);
3796
- }
3797
- appendControls(root2, config, graph);
3798
- overlay.appendChild(root2);
4564
+ this.safeD3Operation(() => operation(selection2), context);
3799
4565
  }
3800
- function appendControls(root3, config2, graph2) {
3801
- const actions = [
3802
- { key: "zoomIn", icon: "zoom-in", label: "Zoom in", fn: graph2.zoomIn.bind(graph2) },
3803
- { key: "zoomOut", icon: "zoom-out", label: "Zoom out", fn: graph2.zoomOut.bind(graph2) },
3804
- { key: "fit", icon: "fit", label: "Fit view", fn: graph2.fitView.bind(graph2) },
3805
- { key: "reset", icon: "reset", label: "Reset view", fn: graph2.resetView.bind(graph2) }
3806
- ];
3807
- actions.forEach((action) => {
3808
- if (shouldRenderControl(config2, action.key)) {
3809
- 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();
3810
4578
  }
3811
4579
  });
3812
4580
  }
3813
- function createButton(type, label, onClick) {
3814
- const button = document.createElement("button");
3815
- button.className = "pg-control-btn";
3816
- button.type = "button";
3817
- button.setAttribute("aria-label", label);
3818
- const wrapper = document.createElement("div");
3819
- wrapper.className = "pg-icon-wrapper";
3820
- wrapper.innerHTML = getControlIcon(type);
3821
- const svg = wrapper.querySelector("svg");
3822
- if (svg) {
3823
- svg.classList.add("pg-icon");
3824
- button.appendChild(svg);
3825
- }
3826
- button.addEventListener("click", onClick);
3827
- return button;
3828
- }
3829
- function destroy() {
3830
- if (!root2) {
3831
- return;
3832
- }
3833
- if (root2.parentNode === overlay) {
3834
- overlay.removeChild(root2);
3835
- }
3836
- root2 = null;
3837
- }
3838
- return { mount, destroy };
3839
- }
3840
-
3841
- // src/assets/caret.svg?raw
3842
- var caret_default = '<svg\n xmlns="http://www.w3.org/2000/svg"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n stroke-width="2"\n stroke-linecap="round"\n stroke-linejoin="round"\n>\n <path d="M9 20L16.5 12L9 4" />\n</svg>';
3843
-
3844
- // src/legends/graph-legend-icon.ts
3845
- var LEGEND_ICON_MAP = { caret: caret_default };
3846
- function getLegendIcon(icon) {
3847
- const raw = LEGEND_ICON_MAP[icon];
3848
- if (!raw) {
3849
- throw new Error(`Legend icon not found: ${icon}`);
3850
- }
3851
- return raw.replace("<svg", '<svg class="pg-icon"');
3852
- }
3853
-
3854
- // src/legends/create-graph-legends.ts
3855
- function createGraphLegend(overlay, config) {
3856
- const legendWrapper = document.createElement("div");
3857
- legendWrapper.className = "pg-legend";
3858
- const position = config.position || "bottom-right";
3859
- legendWrapper.classList.add(`pg-pos-${position}`);
3860
- if (config.defaultExpanded === false) {
3861
- legendWrapper.classList.add("pg-is-collapsed");
4581
+ /**
4582
+ * Mark error handler as destroyed to prevent further operations
4583
+ */
4584
+ static destroy() {
4585
+ this.isDestroyed = true;
3862
4586
  }
3863
- if (config.collapsible) {
3864
- const toggleBtn = document.createElement("button");
3865
- toggleBtn.className = "pg-legend-toggle";
3866
- toggleBtn.type = "button";
3867
- toggleBtn.innerHTML = getLegendIcon("caret");
3868
- toggleBtn.onclick = (e) => {
3869
- e.stopPropagation();
3870
- legendWrapper.classList.toggle("pg-is-collapsed");
3871
- };
3872
- legendWrapper.appendChild(toggleBtn);
4587
+ /**
4588
+ * Reset error handler state
4589
+ */
4590
+ static reset() {
4591
+ this.isDestroyed = false;
3873
4592
  }
3874
- const body = document.createElement("div");
3875
- body.className = "pg-legend-body";
3876
- const list = document.createElement("ul");
3877
- list.className = "pg-legend-list";
3878
- config.items.forEach((item) => {
3879
- const listItem = document.createElement("li");
3880
- listItem.className = "pg-legend-item";
3881
- const swatch = document.createElement("span");
3882
- swatch.className = `pg-legend-swatch is-${item.shape || "circle"}`;
3883
- swatch.style.backgroundColor = item.color;
3884
- const label = document.createElement("span");
3885
- label.className = "pg-legend-label";
3886
- label.innerText = item.label;
3887
- listItem.appendChild(swatch);
3888
- listItem.appendChild(label);
3889
- list.appendChild(listItem);
3890
- });
3891
- body.appendChild(list);
3892
- legendWrapper.appendChild(body);
3893
- overlay.appendChild(legendWrapper);
3894
- return () => {
3895
- if (legendWrapper.parentNode === overlay) overlay.removeChild(legendWrapper);
3896
- };
3897
- }
4593
+ };
3898
4594
 
3899
4595
  // src/utils/resolve-link-style.ts
3900
4596
  var DEFAULT_LINK_STYLE = {
@@ -3915,7 +4611,7 @@ var DEFAULT_LINK_STYLE = {
3915
4611
  borderWidth: 1.5,
3916
4612
  borderRadius: 4,
3917
4613
  textColor: "color-mix(in srgb, #8E42EE, #000000 40%)",
3918
- fontSize: 12,
4614
+ fontSize: 10,
3919
4615
  paddingX: 8,
3920
4616
  paddingY: 4,
3921
4617
  height: 24
@@ -4010,6 +4706,27 @@ function createArrowMarker(params) {
4010
4706
  }
4011
4707
 
4012
4708
  // src/renderer/links.ts
4709
+ function getShortenedSourcePoint(link, style) {
4710
+ const source = link.source;
4711
+ const target = link.target;
4712
+ const sourceX = source.x ?? 0;
4713
+ const sourceY = source.y ?? 0;
4714
+ const targetX = target.x ?? 0;
4715
+ const targetY = target.y ?? 0;
4716
+ const dx = targetX - sourceX;
4717
+ const dy = targetY - sourceY;
4718
+ const distance = Math.sqrt(dx * dx + dy * dy) || 1;
4719
+ const sourceRadius = source.style?.radius ?? 12;
4720
+ const sourceStrokeWidth = source.style?.strokeWidth ?? 1.5;
4721
+ const linkStrokeCompensation = style.strokeWidth / 2;
4722
+ const nodeStrokeOffset = sourceStrokeWidth / 2;
4723
+ const visualSpacing = 2;
4724
+ const offset = sourceRadius + nodeStrokeOffset + linkStrokeCompensation + visualSpacing;
4725
+ return {
4726
+ x: sourceX + dx / distance * offset,
4727
+ y: sourceY + dy / distance * offset
4728
+ };
4729
+ }
4013
4730
  function getShortenedTargetPoint(link, style) {
4014
4731
  const source = link.source;
4015
4732
  const target = link.target;
@@ -4021,10 +4738,12 @@ function getShortenedTargetPoint(link, style) {
4021
4738
  const dy = targetY - sourceY;
4022
4739
  const distance = Math.sqrt(dx * dx + dy * dy) || 1;
4023
4740
  const targetRadius = target.style?.radius ?? 12;
4741
+ const targetStrokeWidth = target.style?.strokeWidth ?? 1.5;
4024
4742
  const arrowLength = style.arrow.enabled ? style.arrow.size * 2 : 0;
4025
- const 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,18 +5113,253 @@ function createNodeHover(nodeSelection, hoverStyle) {
4191
5113
  const svgElement = firstNode.ownerSVGElement;
4192
5114
  if (!svgElement) return;
4193
5115
  const root2 = select_default2(svgElement);
4194
- 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) {
5116
+ function clearAllHoverLayers() {
5117
+ const hoverNodesLayer = root2.select('[data-layer="hover-nodes"]').node();
5118
+ const nodesLayer = root2.select('[data-layer="nodes"]').node();
5119
+ if (hoverNodesLayer && nodesLayer) {
5120
+ while (hoverNodesLayer.firstChild) {
5121
+ nodesLayer.appendChild(hoverNodesLayer.firstChild);
5122
+ }
5123
+ }
5124
+ const hoverNodeLabelsLayer = root2.select('[data-layer="hover-node-labels"]').node();
5125
+ const nodeLabelsLayer = root2.select('[data-layer="node-labels"]').node();
5126
+ if (hoverNodeLabelsLayer && nodeLabelsLayer) {
5127
+ while (hoverNodeLabelsLayer.firstChild) {
5128
+ nodeLabelsLayer.appendChild(hoverNodeLabelsLayer.firstChild);
5129
+ }
5130
+ }
5131
+ const hoverLinksLayer = root2.select('[data-layer="hover-links"]').node();
5132
+ const linksLayer = root2.select('[data-layer="links"]').node();
5133
+ if (hoverLinksLayer && linksLayer) {
5134
+ while (hoverLinksLayer.firstChild) {
5135
+ const linkElement = hoverLinksLayer.firstChild;
5136
+ linksLayer.appendChild(linkElement);
5137
+ const event = new MouseEvent("mouseleave", {
5138
+ bubbles: true,
5139
+ cancelable: false,
5140
+ view: window
5141
+ });
5142
+ linkElement.dispatchEvent(event);
5143
+ }
5144
+ }
5145
+ const hoverLinkLabelsLayer = root2.select('[data-layer="hover-link-labels"]').node();
5146
+ const linkLabelsLayer = root2.select('[data-layer="link-labels"]').node();
5147
+ if (hoverLinkLabelsLayer && linkLabelsLayer) {
5148
+ while (hoverLinkLabelsLayer.firstChild) {
5149
+ const labelElement = hoverLinkLabelsLayer.firstChild;
5150
+ const labelData = labelElement.__data__;
5151
+ if (labelData && labelData.style.label.visibility === "hover" && !labelElement.classList.contains("label-selection-pinned")) {
5152
+ labelElement.style.opacity = "0";
5153
+ labelElement.style.pointerEvents = "none";
5154
+ }
5155
+ linkLabelsLayer.appendChild(labelElement);
5156
+ }
5157
+ }
5158
+ }
5159
+ nodeSelection.on("mouseenter.links", function(_event, hoveredNode) {
5160
+ const hoveredNodeElement = this;
5161
+ if (hoveredNodeElement.dataset.selected === "true") {
5162
+ return;
5163
+ }
5164
+ clearAllHoverLayers();
5165
+ const hoverNodesLayer = root2.select('[data-layer="hover-nodes"]').node();
5166
+ if (hoverNodesLayer) {
5167
+ hoverNodesLayer.appendChild(hoveredNodeElement);
5168
+ }
5169
+ root2.selectAll("text").filter((d) => d.id === hoveredNode.id).each(function() {
5170
+ const labelElement = this;
5171
+ const hoverNodeLabelsLayer = root2.select('[data-layer="hover-node-labels"]').node();
5172
+ if (hoverNodeLabelsLayer) {
5173
+ hoverNodeLabelsLayer.appendChild(labelElement);
5174
+ }
5175
+ });
5176
+ const connectedLinks = root2.selectAll("line:not(.link-hit-area)").filter((renderableLink) => {
5177
+ const source = renderableLink.link.source;
5178
+ const target = renderableLink.link.target;
5179
+ return source.id === hoveredNode.id || target.id === hoveredNode.id;
5180
+ });
5181
+ connectedLinks.each(function(_renderableLink) {
5182
+ const linkElement = this;
5183
+ const hoverLinksLayer = root2.select('[data-layer="hover-links"]').node();
5184
+ if (hoverLinksLayer) {
5185
+ hoverLinksLayer.appendChild(linkElement);
5186
+ }
5187
+ const event = new MouseEvent("mouseenter", {
5188
+ bubbles: true,
5189
+ cancelable: false,
5190
+ view: window
5191
+ });
5192
+ linkElement.dispatchEvent(event);
5193
+ });
5194
+ root2.selectAll(".link-label").filter((item) => {
5195
+ const source = item.link.source;
5196
+ const target = item.link.target;
5197
+ return source.id === hoveredNode.id || target.id === hoveredNode.id;
5198
+ }).each(function(item) {
5199
+ const labelElement = this;
5200
+ const hoverLinkLabelsLayer = root2.select('[data-layer="hover-link-labels"]').node();
5201
+ if (hoverLinkLabelsLayer) {
5202
+ hoverLinkLabelsLayer.appendChild(labelElement);
5203
+ if (item.style.label.visibility === "hover") {
5204
+ labelElement.style.opacity = "1";
5205
+ labelElement.style.pointerEvents = "auto";
5206
+ }
5207
+ }
5208
+ });
5209
+ }).on("mouseleave.links", function(_event, _hoveredNode) {
5210
+ clearAllHoverLayers();
5211
+ });
5212
+ }
5213
+
5214
+ // src/interactions/create-link-hover.ts
5215
+ function createLinkHover(linkSelection, hoverStyle) {
5216
+ const firstLink = linkSelection.node();
5217
+ if (!firstLink) return;
5218
+ const svgElement = firstLink.ownerSVGElement;
5219
+ if (!svgElement) return;
5220
+ const root2 = select_default2(svgElement);
5221
+ const originalMarkers = /* @__PURE__ */ new Map();
5222
+ function clearAllHoverStates() {
5223
+ root2.selectAll("line[data-hovered]").each(function(_d) {
5224
+ const linkElement = this;
5225
+ delete linkElement.dataset.hovered;
5226
+ const isSelected = linkElement.dataset.selected === "true";
5227
+ if (!isSelected) {
5228
+ linkElement.style.stroke = "";
5229
+ linkElement.style.strokeWidth = "";
5230
+ linkElement.style.opacity = "";
5231
+ const originalMarker = originalMarkers.get(linkElement);
5232
+ if (originalMarker !== void 0) {
5233
+ if (originalMarker) {
5234
+ linkElement.setAttribute("marker-end", originalMarker);
5235
+ } else {
5236
+ linkElement.removeAttribute("marker-end");
5237
+ }
5238
+ }
5239
+ }
5240
+ });
5241
+ root2.select('[data-layer="link-labels"]').selectAll(".link-label").filter(function(item) {
5242
+ return item.style.label.visibility === "hover" && !this.classList.contains("label-selection-pinned");
5243
+ }).style("opacity", 0).style("pointer-events", "none");
5244
+ }
5245
+ linkSelection.on("mouseenter.hover", function(_event, renderableLink) {
5246
+ const hoveredElement = this;
5247
+ clearAllHoverStates();
5248
+ let targetLinkElement;
5249
+ if (hoveredElement.classList.contains("link-hit-area")) {
5250
+ const visibleLinkNode = root2.select('[data-layer="links"]').selectAll("line:not(.link-hit-area)").filter((d) => d.link === renderableLink.link).node();
5251
+ if (!visibleLinkNode) return;
5252
+ targetLinkElement = visibleLinkNode;
5253
+ } else {
5254
+ targetLinkElement = hoveredElement;
5255
+ }
5256
+ if (!originalMarkers.has(targetLinkElement)) {
5257
+ originalMarkers.set(targetLinkElement, targetLinkElement.getAttribute("marker-end"));
5258
+ }
5259
+ if (hoverStyle) {
5260
+ if (hoverStyle.stroke !== void 0) {
5261
+ targetLinkElement.style.stroke = hoverStyle.stroke;
5262
+ if (renderableLink.style.arrow.enabled) {
5263
+ const hoverMarkerStyle = {
5264
+ stroke: hoverStyle.stroke,
5265
+ arrow: { fill: hoverStyle.stroke, size: renderableLink.style.arrow.size }
5266
+ };
5267
+ const hoverMarkerId = createArrowMarker({ svg: svgElement, style: hoverMarkerStyle });
5268
+ targetLinkElement.setAttribute("marker-end", `url(#${hoverMarkerId})`);
5269
+ }
5270
+ }
5271
+ if (hoverStyle.strokeWidth !== void 0) {
5272
+ targetLinkElement.style.strokeWidth = String(hoverStyle.strokeWidth);
5273
+ }
5274
+ if (hoverStyle.opacity !== void 0) {
5275
+ targetLinkElement.style.opacity = String(hoverStyle.opacity);
5276
+ }
5277
+ }
5278
+ const labelSelection2 = root2.select('[data-layer="link-labels"]').selectAll(".link-label");
5279
+ labelSelection2.filter((item) => item.link === renderableLink.link && item.style.label.visibility === "hover").style("opacity", 1).style("pointer-events", "auto");
5280
+ }).on("mouseleave.hover", function(_event, renderableLink) {
5281
+ const hoveredElement = this;
5282
+ let targetLinkElement;
5283
+ if (hoveredElement.classList.contains("link-hit-area")) {
5284
+ const visibleLinkNode = root2.select('[data-layer="links"]').selectAll("line:not(.link-hit-area)").filter((d) => d.link === renderableLink.link).node();
5285
+ if (!visibleLinkNode) return;
5286
+ targetLinkElement = visibleLinkNode;
5287
+ } else {
5288
+ targetLinkElement = hoveredElement;
5289
+ }
5290
+ const isSelected = targetLinkElement.dataset.selected === "true";
5291
+ if (!isSelected) {
5292
+ targetLinkElement.style.stroke = "";
5293
+ targetLinkElement.style.strokeWidth = "";
5294
+ targetLinkElement.style.opacity = "";
5295
+ const originalMarker = originalMarkers.get(targetLinkElement);
5296
+ if (originalMarker !== void 0) {
5297
+ if (originalMarker) {
5298
+ targetLinkElement.setAttribute("marker-end", originalMarker);
5299
+ } else {
5300
+ targetLinkElement.removeAttribute("marker-end");
5301
+ }
5302
+ }
5303
+ }
5304
+ const labelSelection2 = root2.select('[data-layer="link-labels"]').selectAll(".link-label");
5305
+ labelSelection2.filter(function(item) {
4204
5306
  return item.style.label.visibility === "hover" && !this.classList.contains("label-selection-pinned");
4205
- }).interrupt().transition().duration(200).style("opacity", 0).style("pointer-events", "none");
5307
+ }).style("opacity", 0).style("pointer-events", "none");
5308
+ });
5309
+ const labelSelection = root2.select('[data-layer="link-labels"]').selectAll(".link-label");
5310
+ labelSelection.on("mouseenter.link-hover", function(_event, renderableLinkLabel) {
5311
+ const correspondingHitArea = root2.select('[data-layer="links"]').selectAll("line.link-hit-area").filter((d) => d.link === renderableLinkLabel.link);
5312
+ const correspondingLink = root2.select('[data-layer="links"]').selectAll("line:not(.link-hit-area)").filter((d) => d.link === renderableLinkLabel.link);
5313
+ if (correspondingLink.node() && correspondingHitArea.node()) {
5314
+ const linkElement = correspondingLink.node();
5315
+ const renderableLink = correspondingLink.datum();
5316
+ if (!originalMarkers.has(linkElement)) {
5317
+ originalMarkers.set(linkElement, linkElement.getAttribute("marker-end"));
5318
+ }
5319
+ if (hoverStyle) {
5320
+ if (hoverStyle.stroke !== void 0) {
5321
+ linkElement.style.stroke = hoverStyle.stroke;
5322
+ if (renderableLink.style.arrow.enabled) {
5323
+ const hoverMarkerStyle = {
5324
+ stroke: hoverStyle.stroke,
5325
+ arrow: { fill: hoverStyle.stroke, size: renderableLink.style.arrow.size }
5326
+ };
5327
+ const hoverMarkerId = createArrowMarker({ svg: svgElement, style: hoverMarkerStyle });
5328
+ linkElement.setAttribute("marker-end", `url(#${hoverMarkerId})`);
5329
+ }
5330
+ }
5331
+ if (hoverStyle.strokeWidth !== void 0) {
5332
+ linkElement.style.strokeWidth = String(hoverStyle.strokeWidth);
5333
+ }
5334
+ if (hoverStyle.opacity !== void 0) {
5335
+ linkElement.style.opacity = String(hoverStyle.opacity);
5336
+ }
5337
+ }
5338
+ }
5339
+ }).on("mouseleave.link-hover", function(_event, renderableLinkLabel) {
5340
+ const correspondingHitArea = root2.select('[data-layer="links"]').selectAll("line.link-hit-area").filter((d) => d.link === renderableLinkLabel.link);
5341
+ const correspondingLink = root2.select('[data-layer="links"]').selectAll("line:not(.link-hit-area)").filter((d) => d.link === renderableLinkLabel.link);
5342
+ if (correspondingLink.node() && correspondingHitArea.node()) {
5343
+ const linkElement = correspondingLink.node();
5344
+ const isSelected = linkElement.dataset.selected === "true";
5345
+ if (!isSelected) {
5346
+ linkElement.style.stroke = "";
5347
+ linkElement.style.strokeWidth = "";
5348
+ linkElement.style.opacity = "";
5349
+ const originalMarker = originalMarkers.get(linkElement);
5350
+ if (originalMarker !== void 0) {
5351
+ if (originalMarker) {
5352
+ linkElement.setAttribute("marker-end", originalMarker);
5353
+ } else {
5354
+ linkElement.removeAttribute("marker-end");
5355
+ }
5356
+ }
5357
+ }
5358
+ const labelSelection2 = root2.select('[data-layer="link-labels"]').selectAll(".link-label");
5359
+ labelSelection2.filter(function(item) {
5360
+ return item.style.label.visibility === "hover" && !this.classList.contains("label-selection-pinned");
5361
+ }).style("opacity", 0).style("pointer-events", "none");
5362
+ }
4206
5363
  });
4207
5364
  }
4208
5365
 
@@ -4400,25 +5557,9 @@ function getDefaultContent(node) {
4400
5557
  `;
4401
5558
  }
4402
5559
 
4403
- // src/utils/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,435 +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");
4487
- }
4488
5674
  }
4489
- }
4490
-
4491
- // src/utils/node-link-selection.utils.ts
4492
- function deselectNode(nodeElement, root2, nodeSelectHandlers, nodeDeselectHandlers) {
4493
- nodeElement.style.fill = "";
4494
- nodeElement.style.stroke = "";
4495
- nodeElement.style.strokeWidth = "";
4496
- nodeElement.style.opacity = "";
4497
- nodeElement.style.removeProperty("r");
4498
- root2.selectAll(".link-label.label-selection-pinned").classed("label-selection-pinned", false).interrupt().transition().duration(200).style("opacity", 0).style("pointer-events", "none");
4499
- const nodeData = select_default2(nodeElement).datum();
4500
- nodeSelectHandlers.clear();
4501
- nodeDeselectHandlers.forEach((handler) => handler(nodeData, nodeElement));
4502
- }
4503
- function deselectLink(linkElement, linkMarkerSnapshots, linkSelectHandlers, linkDeselectHandlers) {
4504
- linkElement.style.stroke = "";
4505
- linkElement.style.strokeWidth = "";
4506
- linkElement.style.opacity = "";
4507
- const originalMarkerEnd = linkMarkerSnapshots.get(linkElement);
4508
- if (originalMarkerEnd) {
4509
- linkElement.setAttribute("marker-end", originalMarkerEnd);
4510
- } else {
4511
- linkElement.removeAttribute("marker-end");
5675
+ /**
5676
+ * Update label dimensions with caching
5677
+ */
5678
+ updateLabelDimensions(group, item, linkId) {
5679
+ const text = group.querySelector("text");
5680
+ const rect = group.querySelector("rect");
5681
+ if (!text || !rect) return;
5682
+ const textContent = text.textContent || "";
5683
+ const cached = this.state.labelDimensionsCache.get(linkId);
5684
+ if (cached && cached.textContent === textContent) {
5685
+ rect.setAttribute("x", String(cached.x));
5686
+ rect.setAttribute("y", String(cached.y));
5687
+ rect.setAttribute("width", String(cached.width));
5688
+ rect.setAttribute("height", String(cached.height));
5689
+ return;
5690
+ }
5691
+ const bBox = text.getBBox();
5692
+ const padding = 6;
5693
+ const dimensions = {
5694
+ width: bBox.width + padding * 2,
5695
+ height: bBox.height + padding * 2,
5696
+ x: bBox.x - padding,
5697
+ y: bBox.y - padding,
5698
+ textContent
5699
+ };
5700
+ this.state.labelDimensionsCache.set(linkId, dimensions);
5701
+ rect.setAttribute("x", String(dimensions.x));
5702
+ rect.setAttribute("y", String(dimensions.y));
5703
+ rect.setAttribute("width", String(dimensions.width));
5704
+ rect.setAttribute("height", String(dimensions.height));
5705
+ }
5706
+ /**
5707
+ * Schedule updates using requestAnimationFrame for better performance
5708
+ */
5709
+ scheduleFrameUpdate(callback) {
5710
+ if (this.animationFrameId !== null) {
5711
+ return;
5712
+ }
5713
+ this.animationFrameId = requestAnimationFrame(() => {
5714
+ this.animationFrameId = null;
5715
+ callback();
5716
+ });
5717
+ }
5718
+ /**
5719
+ * Generate unique ID for link caching
5720
+ */
5721
+ getLinkId(link) {
5722
+ const sourceId = typeof link.source === "string" ? link.source : link.source.id;
5723
+ const targetId = typeof link.target === "string" ? link.target : link.target.id;
5724
+ return `${sourceId}::${targetId}::${link.label ?? ""}`;
5725
+ }
5726
+ /**
5727
+ * Clear caches when graph data changes
5728
+ */
5729
+ clearCaches() {
5730
+ this.state.labelDimensionsCache.clear();
5731
+ this.state.lastPositions.clear();
5732
+ this.state.pendingUpdates.clear();
5733
+ }
5734
+ /**
5735
+ * Update configuration at runtime
5736
+ */
5737
+ updateConfig(newConfig) {
5738
+ Object.assign(this.config, newConfig);
5739
+ this.state.frameInterval = 1e3 / this.config.frameRate;
5740
+ }
5741
+ /**
5742
+ * Get performance metrics
5743
+ */
5744
+ getMetrics() {
5745
+ return {
5746
+ cacheSize: this.state.labelDimensionsCache.size,
5747
+ pendingUpdates: this.state.pendingUpdates.size,
5748
+ lastFrameTime: this.state.lastUpdateTime,
5749
+ frameInterval: this.state.frameInterval
5750
+ };
5751
+ }
5752
+ /**
5753
+ * Clean up resources
5754
+ */
5755
+ destroy() {
5756
+ this.isDestroyed = true;
5757
+ if (this.animationFrameId !== null) {
5758
+ cancelAnimationFrame(this.animationFrameId);
5759
+ this.animationFrameId = null;
5760
+ }
5761
+ this.clearCaches();
5762
+ }
5763
+ };
5764
+
5765
+ // src/utils/selection-manager.ts
5766
+ var SelectionManager = class {
5767
+ state = {
5768
+ selectedNode: null,
5769
+ selectedLink: null
5770
+ };
5771
+ eventEmitter;
5772
+ config;
5773
+ layers;
5774
+ linkMarkerSnapshots;
5775
+ root;
5776
+ constructor(eventEmitter, config, layers, linkMarkerSnapshots, root2) {
5777
+ this.eventEmitter = eventEmitter;
5778
+ this.config = config;
5779
+ this.layers = layers;
5780
+ this.linkMarkerSnapshots = linkMarkerSnapshots;
5781
+ this.root = root2;
5782
+ }
5783
+ /**
5784
+ * Select a node, automatically deselecting any current selection
5785
+ */
5786
+ selectNode(nodeElement, nodeData) {
5787
+ this.clearHoverState();
5788
+ this.clearSelection();
5789
+ this.bringNodeToFront(nodeElement, nodeData);
5790
+ if (this.config.nodeStyle) {
5791
+ const style = this.config.nodeStyle;
5792
+ if (style.fill !== void 0) nodeElement.style.fill = style.fill;
5793
+ if (style.stroke !== void 0) nodeElement.style.stroke = style.stroke;
5794
+ if (style.strokeWidth !== void 0) nodeElement.style.strokeWidth = String(style.strokeWidth);
5795
+ if (style.opacity !== void 0) nodeElement.style.opacity = String(style.opacity);
5796
+ if (style.radius !== void 0) nodeElement.style.setProperty("r", String(style.radius));
5797
+ }
5798
+ this.root.selectAll(".link-label").filter((item) => {
5799
+ if (item.style.label.visibility !== "hover") return false;
5800
+ const source = item.link.source;
5801
+ const target = item.link.target;
5802
+ return source.id === nodeData.id || target.id === nodeData.id;
5803
+ }).classed("label-selection-pinned", true).interrupt().transition().duration(200).style("opacity", 1).style("pointer-events", "auto");
5804
+ this.state.selectedNode = { element: nodeElement, data: nodeData };
5805
+ this.eventEmitter.emit("nodeSelect", { node: nodeData, element: nodeElement });
5806
+ }
5807
+ /**
5808
+ * Select a link, automatically deselecting any current selection
5809
+ */
5810
+ selectLink(linkElement, renderableLink, event) {
5811
+ if (event) {
5812
+ event.stopPropagation();
5813
+ }
5814
+ this.clearSelection();
5815
+ this.bringLinkToFront(linkElement, renderableLink);
5816
+ linkElement.dataset.selected = "true";
5817
+ if (this.config.linkStyle) {
5818
+ const style = this.config.linkStyle;
5819
+ if (style.stroke !== void 0) linkElement.style.stroke = style.stroke;
5820
+ if (style.strokeWidth !== void 0) linkElement.style.strokeWidth = String(style.strokeWidth);
5821
+ if (style.opacity !== void 0) linkElement.style.opacity = String(style.opacity);
5822
+ if (style.stroke !== void 0 && renderableLink.style.arrow.enabled) {
5823
+ const selectionMarkerStyle = {
5824
+ stroke: style.stroke,
5825
+ arrow: { fill: style.stroke, size: renderableLink.style.arrow.size }
5826
+ };
5827
+ const selectionMarkerId = createArrowMarker({ svg: this.layers.svg, style: selectionMarkerStyle });
5828
+ linkElement.setAttribute("marker-end", `url(#${selectionMarkerId})`);
5829
+ }
5830
+ }
5831
+ this.root.selectAll(".link-label").filter((item) => item.link === renderableLink.link).classed("label-selection-pinned", true).interrupt().transition().duration(200).style("opacity", 1).style("pointer-events", "auto");
5832
+ const originalMarker = this.linkMarkerSnapshots.get(linkElement) || null;
5833
+ this.state.selectedLink = {
5834
+ element: linkElement,
5835
+ data: renderableLink.link,
5836
+ originalMarker
5837
+ };
5838
+ this.eventEmitter.emit("linkSelect", { link: renderableLink.link, element: linkElement });
5839
+ }
5840
+ /**
5841
+ * Deselect the currently selected node
5842
+ */
5843
+ deselectNode() {
5844
+ if (!this.state.selectedNode) return;
5845
+ const { element, data } = this.state.selectedNode;
5846
+ this.restoreSelectedElements(data);
5847
+ element.style.fill = "";
5848
+ element.style.stroke = "";
5849
+ element.style.strokeWidth = "";
5850
+ element.style.opacity = "";
5851
+ element.style.removeProperty("r");
5852
+ delete element.dataset.selected;
5853
+ this.root.selectAll(".link-label.label-selection-pinned").classed("label-selection-pinned", false).interrupt().transition().duration(200).style("opacity", 0).style("pointer-events", "none");
5854
+ this.state.selectedNode = null;
5855
+ this.eventEmitter.emit("nodeDeselect", { node: data, element });
5856
+ }
5857
+ /**
5858
+ * Deselect the currently selected link
5859
+ */
5860
+ deselectLink() {
5861
+ if (!this.state.selectedLink) return;
5862
+ const { element, data, originalMarker } = this.state.selectedLink;
5863
+ this.layers.links.appendChild(element);
5864
+ this.root.selectAll(".link-label").filter((item) => item.link === data).each((d, i, nodes) => {
5865
+ const element2 = nodes[i];
5866
+ if (element2) {
5867
+ this.layers.linkLabels.appendChild(element2);
5868
+ }
5869
+ });
5870
+ delete element.dataset.selected;
5871
+ element.style.stroke = "";
5872
+ element.style.strokeWidth = "";
5873
+ element.style.opacity = "";
5874
+ if (originalMarker) {
5875
+ element.setAttribute("marker-end", originalMarker);
5876
+ } else {
5877
+ element.removeAttribute("marker-end");
5878
+ }
5879
+ this.root.selectAll(".link-label.label-selection-pinned").filter((item) => {
5880
+ return item.link === data && item.style.label.visibility === "hover";
5881
+ }).classed("label-selection-pinned", false).interrupt().transition().duration(200).style("opacity", 0).style("pointer-events", "none");
5882
+ this.state.selectedLink = null;
5883
+ this.eventEmitter.emit("linkDeselect", { link: data, element });
5884
+ }
5885
+ /**
5886
+ * Clear all selections
5887
+ */
5888
+ clearSelection() {
5889
+ this.deselectNode();
5890
+ this.deselectLink();
5891
+ }
5892
+ /**
5893
+ * Get current selection state
5894
+ */
5895
+ getSelectionState() {
5896
+ return { ...this.state };
5897
+ }
5898
+ /**
5899
+ * Check if a specific node is selected
5900
+ */
5901
+ isNodeSelected(nodeData) {
5902
+ return this.state.selectedNode?.data.id === nodeData.id;
5903
+ }
5904
+ /**
5905
+ * Check if a specific link is selected
5906
+ */
5907
+ isLinkSelected(linkData) {
5908
+ if (!this.state.selectedLink) return false;
5909
+ const selectedLink = this.state.selectedLink.data;
5910
+ const sourceId = typeof selectedLink.source === "string" ? selectedLink.source : selectedLink.source.id;
5911
+ const targetId = typeof selectedLink.target === "string" ? selectedLink.target : selectedLink.target.id;
5912
+ const checkSourceId = typeof linkData.source === "string" ? linkData.source : linkData.source.id;
5913
+ const checkTargetId = typeof linkData.target === "string" ? linkData.target : linkData.target.id;
5914
+ return sourceId === checkSourceId && targetId === checkTargetId;
5915
+ }
5916
+ /**
5917
+ * Handle click on background (deselect all)
5918
+ */
5919
+ handleBackgroundClick(event, svg, interactionRect) {
5920
+ if (event.target !== svg && event.target !== interactionRect) return;
5921
+ this.clearSelection();
5922
+ }
5923
+ /**
5924
+ * Bring node and related elements to front using selection layer sub-layers
5925
+ */
5926
+ bringNodeToFront(nodeElement, nodeData) {
5927
+ this.layers.selectionLayer.nodes.appendChild(nodeElement);
5928
+ nodeElement.dataset.selected = "true";
5929
+ const connectedLinks = this.root.selectAll("line:not(.link-hit-area)").filter((d) => {
5930
+ const source = d.link.source;
5931
+ const target = d.link.target;
5932
+ return source.id === nodeData.id || target.id === nodeData.id;
5933
+ });
5934
+ connectedLinks.each((d, i, nodes) => {
5935
+ const element = nodes[i];
5936
+ if (element) {
5937
+ this.layers.selectionLayer.links.appendChild(element);
5938
+ }
5939
+ });
5940
+ this.root.selectAll("text").filter((d) => d.id === nodeData.id).each((d, i, nodes) => {
5941
+ const element = nodes[i];
5942
+ if (element) {
5943
+ this.layers.selectionLayer.nodeLabels.appendChild(element);
5944
+ }
5945
+ });
5946
+ this.root.selectAll(".link-label").filter((item) => {
5947
+ const source = item.link.source;
5948
+ const target = item.link.target;
5949
+ return source.id === nodeData.id || target.id === nodeData.id;
5950
+ }).each((d, i, nodes) => {
5951
+ const element = nodes[i];
5952
+ if (element) {
5953
+ this.layers.selectionLayer.linkLabels.appendChild(element);
5954
+ }
5955
+ });
5956
+ }
5957
+ /**
5958
+ * Bring link and its label to front using selection layer
5959
+ */
5960
+ bringLinkToFront(linkElement, renderableLink) {
5961
+ this.layers.selectionLayer.links.appendChild(linkElement);
5962
+ this.root.selectAll(".link-label").filter((item) => item.link === renderableLink.link).each((d, i, nodes) => {
5963
+ const element = nodes[i];
5964
+ if (element) {
5965
+ this.layers.selectionLayer.linkLabels.appendChild(element);
5966
+ }
5967
+ });
5968
+ }
5969
+ /**
5970
+ * Restore elements back to their original layers
5971
+ */
5972
+ restoreSelectedElements(nodeData) {
5973
+ this.root.selectAll("circle").filter((d) => d.id === nodeData.id).each((d, i, nodes) => {
5974
+ const element = nodes[i];
5975
+ if (element) {
5976
+ this.layers.nodes.appendChild(element);
5977
+ }
5978
+ });
5979
+ this.root.selectAll("line").filter((d) => {
5980
+ const source = d.link.source;
5981
+ const target = d.link.target;
5982
+ return source.id === nodeData.id || target.id === nodeData.id;
5983
+ }).each((d, i, nodes) => {
5984
+ const element = nodes[i];
5985
+ if (element) {
5986
+ this.layers.links.appendChild(element);
5987
+ }
5988
+ });
5989
+ this.root.selectAll("text").filter((d) => d.id === nodeData.id).each((d, i, nodes) => {
5990
+ const element = nodes[i];
5991
+ if (element) {
5992
+ this.layers.nodeLabels.appendChild(element);
5993
+ }
5994
+ });
5995
+ this.root.selectAll(".link-label").filter((item) => {
5996
+ const source = item.link.source;
5997
+ const target = item.link.target;
5998
+ return source.id === nodeData.id || target.id === nodeData.id;
5999
+ }).each((d, i, nodes) => {
6000
+ const element = nodes[i];
6001
+ if (element) {
6002
+ this.layers.linkLabels.appendChild(element);
6003
+ }
6004
+ });
6005
+ }
6006
+ /**
6007
+ * Utility method to bring any SVG element to front using appendChild
6008
+ * Based on the reference implementation pattern
6009
+ */
6010
+ bringElementToFront(element) {
6011
+ if (element.parentNode) {
6012
+ element.parentNode.appendChild(element);
6013
+ }
6014
+ }
6015
+ /**
6016
+ * Clear hover state to prevent conflicts with selection
6017
+ * Similar to the clearAllHoverLayers function in create-node-hover.ts
6018
+ */
6019
+ clearHoverState() {
6020
+ const hoverNodesLayer = this.root.select('[data-layer="hover-nodes"]').node();
6021
+ const nodesLayer = this.root.select('[data-layer="nodes"]').node();
6022
+ if (hoverNodesLayer && nodesLayer) {
6023
+ while (hoverNodesLayer.firstChild) {
6024
+ nodesLayer.appendChild(hoverNodesLayer.firstChild);
6025
+ }
6026
+ }
6027
+ const hoverNodeLabelsLayer = this.root.select('[data-layer="hover-node-labels"]').node();
6028
+ const nodeLabelsLayer = this.root.select('[data-layer="node-labels"]').node();
6029
+ if (hoverNodeLabelsLayer && nodeLabelsLayer) {
6030
+ while (hoverNodeLabelsLayer.firstChild) {
6031
+ nodeLabelsLayer.appendChild(hoverNodeLabelsLayer.firstChild);
6032
+ }
6033
+ }
6034
+ const hoverLinksLayer = this.root.select('[data-layer="hover-links"]').node();
6035
+ const linksLayer = this.root.select('[data-layer="links"]').node();
6036
+ if (hoverLinksLayer && linksLayer) {
6037
+ while (hoverLinksLayer.firstChild) {
6038
+ const linkElement = hoverLinksLayer.firstChild;
6039
+ linksLayer.appendChild(linkElement);
6040
+ const event = new MouseEvent("mouseleave", {
6041
+ bubbles: true,
6042
+ cancelable: false,
6043
+ view: window
6044
+ });
6045
+ linkElement.dispatchEvent(event);
6046
+ }
6047
+ }
6048
+ const hoverLinkLabelsLayer = this.root.select('[data-layer="hover-link-labels"]').node();
6049
+ const linkLabelsLayer = this.root.select('[data-layer="link-labels"]').node();
6050
+ if (hoverLinkLabelsLayer && linkLabelsLayer) {
6051
+ while (hoverLinkLabelsLayer.firstChild) {
6052
+ const labelElement = hoverLinkLabelsLayer.firstChild;
6053
+ const labelData = labelElement.__data__;
6054
+ if (labelData && labelData.style.label.visibility === "hover" && !labelElement.classList.contains("label-selection-pinned")) {
6055
+ labelElement.style.opacity = "0";
6056
+ labelElement.style.pointerEvents = "none";
6057
+ }
6058
+ linkLabelsLayer.appendChild(labelElement);
6059
+ }
6060
+ }
6061
+ }
6062
+ /**
6063
+ * Clean up resources
6064
+ */
6065
+ destroy() {
6066
+ this.clearSelection();
6067
+ }
6068
+ };
6069
+
6070
+ // src/core/interaction-manager.ts
6071
+ var InteractionManager = class {
6072
+ constructor(manager) {
6073
+ this.manager = manager;
6074
+ }
6075
+ manager;
6076
+ /**
6077
+ * Setup all interactions for the graph
6078
+ */
6079
+ setupInteractions(selections) {
6080
+ this.setupTickManager(selections);
6081
+ this.setupHoverInteractions(selections);
6082
+ this.setupDragBehavior(selections);
6083
+ this.setupSelectionManagement(selections);
6084
+ this.setupLinkHitAreas(selections);
6085
+ }
6086
+ /**
6087
+ * Setup performance tick manager
6088
+ */
6089
+ setupTickManager(selections) {
6090
+ if (!this.manager.tickManager) {
6091
+ this.manager.tickManager = new PerformanceTickManager({
6092
+ frameRate: 30,
6093
+ positionThreshold: 0.5,
6094
+ batchSize: 50,
6095
+ enableThrottling: true
6096
+ });
6097
+ }
6098
+ const optimizedTickHandler = this.manager.tickManager.createTickHandler({
6099
+ linkSelection: selections.linkSelection,
6100
+ linkLabelSelection: selections.linkLabelSelection,
6101
+ nodeSelection: selections.nodeSelection,
6102
+ labelSelection: selections.labelSelection,
6103
+ tooltipBinding: this.manager.tooltipBinding
6104
+ });
6105
+ if (this.manager.simulation) {
6106
+ this.manager.simulation.on("tick", optimizedTickHandler);
6107
+ }
6108
+ }
6109
+ /**
6110
+ * Setup hover interactions
6111
+ */
6112
+ setupHoverInteractions(selections) {
6113
+ if (this.manager.config.interaction?.hover?.enabled) {
6114
+ if (this.manager.config.interaction.hover.tooltip?.enabled) {
6115
+ this.manager.tooltipBinding = bindNodeTooltip({
6116
+ container: this.manager.config.container,
6117
+ selection: selections.nodeSelection,
6118
+ tooltipConfig: this.manager.config.interaction.hover.tooltip
6119
+ });
6120
+ }
6121
+ createNodeHover(selections.nodeSelection, this.manager.config.interaction.hover.nodeStyle);
6122
+ createLinkHover(selections.linkSelection, this.manager.config.interaction.hover.linkStyle);
6123
+ }
6124
+ }
6125
+ /**
6126
+ * Setup drag behavior
6127
+ */
6128
+ setupDragBehavior(selections) {
6129
+ if (this.manager.config.interaction?.drag?.enabled !== false && this.manager.simulation) {
6130
+ selections.nodeSelection.call(
6131
+ createDragBehavior(
6132
+ this.manager.simulation,
6133
+ () => {
6134
+ this.manager.reheatSimulation(0.3);
6135
+ },
6136
+ this.manager.dimensions
6137
+ )
6138
+ );
6139
+ }
6140
+ }
6141
+ /**
6142
+ * Setup selection management
6143
+ */
6144
+ setupSelectionManagement(selections) {
6145
+ if (this.manager.config.interaction?.selection?.enabled && this.manager.eventEmitter && this.manager.layers && this.manager.rootGroup) {
6146
+ if (!this.manager.linkMarkerSnapshots) {
6147
+ this.manager.linkMarkerSnapshots = /* @__PURE__ */ new Map();
6148
+ const manager = this.manager;
6149
+ selections.linkSelection.each(function() {
6150
+ manager.linkMarkerSnapshots.set(this, this.getAttribute("marker-end"));
6151
+ });
6152
+ }
6153
+ if (!this.manager.rootSelection) {
6154
+ this.manager.rootSelection = select_default2(this.manager.rootGroup);
6155
+ }
6156
+ this.manager.selectionManager = new SelectionManager(
6157
+ this.manager.eventEmitter,
6158
+ this.manager.config.interaction.selection,
6159
+ this.manager.layers,
6160
+ this.manager.linkMarkerSnapshots,
6161
+ this.manager.rootSelection
6162
+ );
6163
+ this.setupSelectionHandlers(selections);
6164
+ this.setupBackgroundClickHandler();
6165
+ }
6166
+ }
6167
+ /**
6168
+ * Setup selection event handlers
6169
+ */
6170
+ setupSelectionHandlers(selections) {
6171
+ if (!this.manager.selectionManager) return;
6172
+ selections.nodeSelection.on("click.select", (event, node) => {
6173
+ const nodeElement = event.currentTarget;
6174
+ this.manager.selectionManager?.selectNode(nodeElement, node);
6175
+ });
6176
+ selections.linkLabelSelection.on("click.select", (event, renderableLinkLabel) => {
6177
+ event.stopPropagation();
6178
+ const correspondingLink = selections.linkSelection.filter((d) => d.link === renderableLinkLabel.link).node();
6179
+ if (correspondingLink && this.manager.selectionManager) {
6180
+ const linkData = selections.linkSelection.filter((d) => d.link === renderableLinkLabel.link).datum();
6181
+ this.manager.selectionManager.selectLink(correspondingLink, linkData, event);
6182
+ }
6183
+ });
6184
+ }
6185
+ /**
6186
+ * Setup background click to clear selection
6187
+ */
6188
+ setupBackgroundClickHandler() {
6189
+ if (!this.manager.selectionManager || !this.manager.layers) return;
6190
+ select_default2(this.manager.layers.svg).on("click.deselect", (event) => {
6191
+ if (this.manager.selectionManager) {
6192
+ this.manager.selectionManager.handleBackgroundClick(
6193
+ event,
6194
+ this.manager.layers.svg,
6195
+ this.manager.layers.interactionRect
6196
+ );
6197
+ }
6198
+ });
6199
+ }
6200
+ /**
6201
+ * Setup link hit areas for better interaction
6202
+ */
6203
+ setupLinkHitAreas(selections) {
6204
+ if (!this.manager.rootGroup) {
6205
+ console.warn("[InteractionManager] No root group available for link hit areas");
6206
+ return;
6207
+ }
6208
+ const rootSelection = select_default2(this.manager.rootGroup);
6209
+ const linkHitAreaSelection = createLinkHitArea(rootSelection, selections.linkSelection);
6210
+ if (linkHitAreaSelection && !linkHitAreaSelection.empty()) {
6211
+ if (this.manager.config.interaction?.hover?.enabled) {
6212
+ createLinkHover(linkHitAreaSelection, this.manager.config.interaction.hover.linkStyle);
6213
+ }
6214
+ if (this.manager.simulation) {
6215
+ this.manager.simulation.on("tick.hitarea", () => {
6216
+ linkHitAreaSelection.attr("x1", (item) => item.link.source.x ?? 0).attr("y1", (item) => item.link.source.y ?? 0).attr("x2", (item) => item.link.target.x ?? 0).attr("y2", (item) => item.link.target.y ?? 0);
6217
+ });
6218
+ }
6219
+ if (this.manager.selectionManager) {
6220
+ linkHitAreaSelection.on("click.select", (event, renderableLink) => {
6221
+ const visibleLinkNode = selections.linkSelection.filter((d) => d === renderableLink).node();
6222
+ if (visibleLinkNode) {
6223
+ this.manager.selectionManager?.selectLink(visibleLinkNode, renderableLink, event);
6224
+ }
6225
+ });
6226
+ }
6227
+ }
6228
+ }
6229
+ };
6230
+
6231
+ // src/controls/graph-controls.utils.ts
6232
+ function resolveControlsPosition(position) {
6233
+ return position ?? "bottom-left";
6234
+ }
6235
+ function resolveControlsOrientation(orientation) {
6236
+ return orientation ?? "vertical";
6237
+ }
6238
+ function shouldRenderControl(config, key) {
6239
+ const show = config.show;
6240
+ if (!show) {
6241
+ return true;
4512
6242
  }
4513
- const linkData = select_default2(linkElement).datum().link;
4514
- linkSelectHandlers.clear();
4515
- linkDeselectHandlers.forEach((handler) => handler(linkData, linkElement));
6243
+ const value = show[key];
6244
+ if (value === void 0) {
6245
+ return true;
6246
+ }
6247
+ return value;
4516
6248
  }
4517
- function selectLink(event, renderableLink, linkElement, selectionConfig, layers, linkSelectHandlers) {
4518
- event.stopPropagation();
4519
- const linkStyle = selectionConfig.linkStyle;
4520
- if (linkStyle) {
4521
- if (linkStyle.stroke !== void 0) {
4522
- linkElement.style.stroke = linkStyle.stroke;
6249
+
6250
+ // src/assets/plus.svg?raw
6251
+ var plus_default = '<svg\n xmlns="http://www.w3.org/2000/svg"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n stroke-width="2"\n stroke-linecap="round"\n stroke-linejoin="round"\n>\n <path d="M5 12h14m-7-7v14" />\n</svg>';
6252
+
6253
+ // src/assets/minus.svg?raw
6254
+ var minus_default = '<svg\n xmlns="http://www.w3.org/2000/svg"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n stroke-width="2"\n stroke-linecap="round"\n stroke-linejoin="round"\n>\n <path d="M19 12H5" />\n</svg>';
6255
+
6256
+ // src/assets/fit.svg?raw
6257
+ var fit_default = '<svg\n xmlns="http://www.w3.org/2000/svg"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n stroke-width="2"\n stroke-linecap="round"\n stroke-linejoin="round"\n>\n <path d="M5 9V5H9" />\n <path d="M19 9V5H15" />\n <path d="M5 15V19H9" />\n <path d="M19 15V19H15" />\n</svg>';
6258
+
6259
+ // src/assets/reset.svg?raw
6260
+ var reset_default = '<svg\n xmlns="http://www.w3.org/2000/svg"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n stroke-width="2"\n stroke-linecap="round"\n stroke-linejoin="round"\n>\n <path d="M20 12a8 8 0 1 1-2.3-5.7" />\n <path d="M20 4.5v4h-4" />\n</svg>';
6261
+
6262
+ // src/controls/graph-controls.icons.ts
6263
+ var ICON_MAP = {
6264
+ "zoom-in": plus_default,
6265
+ "zoom-out": minus_default,
6266
+ fit: fit_default,
6267
+ reset: reset_default
6268
+ };
6269
+ function getControlIcon(icon) {
6270
+ const raw = ICON_MAP[icon];
6271
+ if (!raw) {
6272
+ throw new Error(`Icon not found: ${icon}`);
6273
+ }
6274
+ return raw.replace("<svg", '<svg class="pg-icon"');
6275
+ }
6276
+
6277
+ // src/controls/create-graph-controls.ts
6278
+ function createGraphControls(overlay, graph, config) {
6279
+ let root2 = null;
6280
+ function mount() {
6281
+ if (!config.enabled) {
6282
+ return;
4523
6283
  }
4524
- if (linkStyle.strokeWidth !== void 0) {
4525
- linkElement.style.strokeWidth = String(linkStyle.strokeWidth);
6284
+ root2 = document.createElement("div");
6285
+ root2.className = "pg-controls";
6286
+ const position = resolveControlsPosition(config.position);
6287
+ root2.classList.add(`pg-pos-${position}`);
6288
+ const orientation = resolveControlsOrientation(config.orientation);
6289
+ root2.classList.add(`pg-orient-${orientation}`);
6290
+ if (config.offset) {
6291
+ root2.style.setProperty("--pg-controls-offset-x", `${config.offset.x}px`);
6292
+ root2.style.setProperty("--pg-controls-offset-y", `${config.offset.y}px`);
4526
6293
  }
4527
- if (linkStyle.opacity !== void 0) {
4528
- linkElement.style.opacity = String(linkStyle.opacity);
6294
+ appendControls(root2, config, graph);
6295
+ overlay.appendChild(root2);
6296
+ }
6297
+ function appendControls(root3, config2, graph2) {
6298
+ const actions = [
6299
+ { key: "zoomIn", icon: "zoom-in", label: "Zoom in", fn: graph2.zoomIn.bind(graph2) },
6300
+ { key: "zoomOut", icon: "zoom-out", label: "Zoom out", fn: graph2.zoomOut.bind(graph2) },
6301
+ { key: "fit", icon: "fit", label: "Fit view", fn: graph2.fitView.bind(graph2) },
6302
+ { key: "reset", icon: "reset", label: "Reset view", fn: graph2.resetView.bind(graph2) }
6303
+ ];
6304
+ actions.forEach((action) => {
6305
+ if (shouldRenderControl(config2, action.key)) {
6306
+ root3.appendChild(createButton(action.icon, action.label, action.fn));
6307
+ }
6308
+ });
6309
+ }
6310
+ function createButton(type, label, onClick) {
6311
+ const button = document.createElement("button");
6312
+ button.className = "pg-control-btn";
6313
+ button.type = "button";
6314
+ button.setAttribute("aria-label", label);
6315
+ const wrapper = document.createElement("div");
6316
+ wrapper.className = "pg-icon-wrapper";
6317
+ wrapper.innerHTML = getControlIcon(type);
6318
+ const svg = wrapper.querySelector("svg");
6319
+ if (svg) {
6320
+ svg.classList.add("pg-icon");
6321
+ button.appendChild(svg);
4529
6322
  }
4530
- if (linkStyle.stroke !== void 0 && renderableLink.style.arrow.enabled) {
4531
- const selectionMarkerStyle = {
4532
- stroke: linkStyle.stroke,
4533
- arrow: { fill: linkStyle.stroke, size: renderableLink.style.arrow.size }
4534
- };
4535
- const selectionMarkerId = createArrowMarker({ svg: layers.svg, style: selectionMarkerStyle });
4536
- select_default2(linkElement).attr("marker-end", `url(#${selectionMarkerId})`);
6323
+ button.addEventListener("click", onClick);
6324
+ return button;
6325
+ }
6326
+ function destroy() {
6327
+ if (!root2) {
6328
+ return;
6329
+ }
6330
+ if (root2.parentNode === overlay) {
6331
+ overlay.removeChild(root2);
4537
6332
  }
6333
+ root2 = null;
4538
6334
  }
4539
- linkSelectHandlers.forEach((handler) => handler(renderableLink.link, linkElement));
6335
+ return { mount, destroy };
4540
6336
  }
4541
- function createLinkHitArea(root2, linkSelection) {
4542
- 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);
6337
+
6338
+ // src/assets/caret.svg?raw
6339
+ var caret_default = '<svg\n xmlns="http://www.w3.org/2000/svg"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n stroke-width="2"\n stroke-linecap="round"\n stroke-linejoin="round"\n>\n <path d="M9 20L16.5 12L9 4" />\n</svg>';
6340
+
6341
+ // src/legends/graph-legend-icon.ts
6342
+ var LEGEND_ICON_MAP = { caret: caret_default };
6343
+ function getLegendIcon(icon) {
6344
+ const raw = LEGEND_ICON_MAP[icon];
6345
+ if (!raw) {
6346
+ throw new Error(`Legend icon not found: ${icon}`);
6347
+ }
6348
+ return raw.replace("<svg", '<svg class="pg-icon"');
4543
6349
  }
4544
6350
 
4545
- // src/create-graph.ts
4546
- function createGraph(config) {
4547
- let cleanupResize = null;
4548
- let cleanupZoom = null;
4549
- let tooltipBinding = null;
4550
- let controls = null;
4551
- let legendCleanup = null;
4552
- let fitViewTimer = null;
4553
- let dimensions = { width: 0, height: 0 };
4554
- let rootGroup = null;
4555
- let svgElement = null;
4556
- let zoomBehavior = null;
4557
- let simulation = null;
4558
- const nodeSelectHandlers = /* @__PURE__ */ new Set();
4559
- const nodeDeselectHandlers = /* @__PURE__ */ new Set();
4560
- const linkSelectHandlers = /* @__PURE__ */ new Set();
4561
- const linkDeselectHandlers = /* @__PURE__ */ new Set();
4562
- function on(event, handler) {
4563
- if (event === "nodeSelect") {
4564
- nodeSelectHandlers.add(handler);
4565
- return () => {
4566
- nodeSelectHandlers.delete(handler);
6351
+ // src/legends/create-graph-legends.ts
6352
+ function generateLegendItems(nodes) {
6353
+ const uniqueTypes = Array.from(new Set(nodes.map((node) => node.type)));
6354
+ return uniqueTypes.map((type) => {
6355
+ const sampleNode = nodes.find((node) => node.type === type);
6356
+ return {
6357
+ label: type,
6358
+ color: sampleNode?.style?.fill ?? "#94a3b8",
6359
+ shape: "circle"
6360
+ };
6361
+ });
6362
+ }
6363
+ function createGraphLegend(overlay, config, nodes) {
6364
+ const legendWrapper = document.createElement("div");
6365
+ legendWrapper.className = "pg-legend";
6366
+ const position = config.position || "bottom-right";
6367
+ legendWrapper.classList.add(`pg-pos-${position}`);
6368
+ if (config.defaultExpanded === false) {
6369
+ legendWrapper.classList.add("pg-is-collapsed");
6370
+ }
6371
+ if (config.collapsible) {
6372
+ const toggleBtn = document.createElement("button");
6373
+ toggleBtn.className = "pg-legend-toggle";
6374
+ toggleBtn.type = "button";
6375
+ toggleBtn.innerHTML = getLegendIcon("caret");
6376
+ toggleBtn.onclick = (e) => {
6377
+ e.stopPropagation();
6378
+ legendWrapper.classList.toggle("pg-is-collapsed");
6379
+ };
6380
+ legendWrapper.appendChild(toggleBtn);
6381
+ }
6382
+ const body = document.createElement("div");
6383
+ body.className = "pg-legend-body";
6384
+ const list = document.createElement("ul");
6385
+ list.className = "pg-legend-list";
6386
+ const legendItems = config.items ?? (nodes ? generateLegendItems(nodes) : []);
6387
+ legendItems.forEach((item) => {
6388
+ const listItem = document.createElement("li");
6389
+ listItem.className = "pg-legend-item";
6390
+ const swatch = document.createElement("span");
6391
+ swatch.className = `pg-legend-swatch is-${item.shape || "circle"}`;
6392
+ swatch.style.backgroundColor = item.color;
6393
+ const label = document.createElement("span");
6394
+ label.className = "pg-legend-label";
6395
+ label.innerText = item.label;
6396
+ listItem.appendChild(swatch);
6397
+ listItem.appendChild(label);
6398
+ list.appendChild(listItem);
6399
+ });
6400
+ body.appendChild(list);
6401
+ legendWrapper.appendChild(body);
6402
+ overlay.appendChild(legendWrapper);
6403
+ return () => {
6404
+ if (legendWrapper.parentNode === overlay) overlay.removeChild(legendWrapper);
6405
+ };
6406
+ }
6407
+
6408
+ // src/utils/export-graph.ts
6409
+ async function captureAndDownloadGraph(container, options = {}) {
6410
+ const {
6411
+ fileName = `graph-export-${Date.now()}.png`,
6412
+ backgroundColor = "#ffffff",
6413
+ scale = 2,
6414
+ includeLegend = true
6415
+ } = options;
6416
+ const svgElement = container.querySelector("svg.pg-canvas");
6417
+ if (!svgElement) {
6418
+ throw new Error("SVG element not found in container");
6419
+ }
6420
+ const svgRect = svgElement.getBoundingClientRect();
6421
+ const graphWidth = svgRect.width || 800;
6422
+ const graphHeight = svgRect.height || 600;
6423
+ const svgClone = svgElement.cloneNode(true);
6424
+ let totalWidth = graphWidth;
6425
+ let totalHeight = graphHeight;
6426
+ if (includeLegend) {
6427
+ const legendEntries = extractLegendData(container);
6428
+ if (legendEntries.length > 0) {
6429
+ const legendDimensions = calculateLegendDimensions(legendEntries, graphWidth);
6430
+ totalWidth = graphWidth + 20 + legendDimensions.width + 20;
6431
+ totalHeight = Math.max(graphHeight, legendDimensions.height + 40);
6432
+ svgClone.setAttribute("width", totalWidth.toString());
6433
+ svgClone.setAttribute("height", totalHeight.toString());
6434
+ svgClone.setAttribute("viewBox", `0 0 ${totalWidth} ${totalHeight}`);
6435
+ const legendGroup = createLegendSVGElement(legendEntries, legendDimensions);
6436
+ svgClone.appendChild(legendGroup);
6437
+ }
6438
+ }
6439
+ svgClone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
6440
+ svgClone.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
6441
+ const svgString = new XMLSerializer().serializeToString(svgClone);
6442
+ const svgBlob = new Blob([svgString], { type: "image/svg+xml;charset=utf-8" });
6443
+ const svgUrl = URL.createObjectURL(svgBlob);
6444
+ try {
6445
+ const canvas = document.createElement("canvas");
6446
+ const ctx = canvas.getContext("2d");
6447
+ if (!ctx) {
6448
+ throw new Error("Could not get canvas context");
6449
+ }
6450
+ canvas.width = totalWidth * scale;
6451
+ canvas.height = totalHeight * scale;
6452
+ ctx.scale(scale, scale);
6453
+ ctx.fillStyle = backgroundColor;
6454
+ ctx.fillRect(0, 0, totalWidth, totalHeight);
6455
+ await new Promise((resolve, reject) => {
6456
+ const img = new Image();
6457
+ const cleanup = () => {
6458
+ img.onload = null;
6459
+ img.onerror = null;
6460
+ img.src = "";
6461
+ URL.revokeObjectURL(svgUrl);
4567
6462
  };
4568
- }
4569
- if (event === "nodeDeselect") {
4570
- nodeDeselectHandlers.add(handler);
4571
- return () => {
4572
- nodeDeselectHandlers.delete(handler);
6463
+ img.onload = () => {
6464
+ try {
6465
+ ctx.drawImage(img, 0, 0, totalWidth, totalHeight);
6466
+ canvas.toBlob((blob) => {
6467
+ cleanup();
6468
+ if (blob) {
6469
+ const url = URL.createObjectURL(blob);
6470
+ const link = document.createElement("a");
6471
+ link.href = url;
6472
+ link.download = fileName;
6473
+ document.body.appendChild(link);
6474
+ link.click();
6475
+ document.body.removeChild(link);
6476
+ URL.revokeObjectURL(url);
6477
+ resolve();
6478
+ } else {
6479
+ reject(new Error("Failed to generate image blob"));
6480
+ }
6481
+ }, "image/png", 0.95);
6482
+ } catch (error) {
6483
+ cleanup();
6484
+ reject(error);
6485
+ }
4573
6486
  };
4574
- }
4575
- if (event === "linkSelect") {
4576
- linkSelectHandlers.add(handler);
4577
- return () => {
4578
- linkSelectHandlers.delete(handler);
6487
+ img.onerror = () => {
6488
+ cleanup();
6489
+ reject(new Error("Failed to load SVG image"));
4579
6490
  };
6491
+ img.src = svgUrl;
6492
+ });
6493
+ } catch (error) {
6494
+ URL.revokeObjectURL(svgUrl);
6495
+ throw error;
6496
+ }
6497
+ }
6498
+ function extractLegendData(container) {
6499
+ const legendEntries = [];
6500
+ const legendContainer = container.querySelector(".pg-legend");
6501
+ if (!legendContainer) {
6502
+ return legendEntries;
6503
+ }
6504
+ const legendItems = legendContainer.querySelectorAll(".pg-legend-item");
6505
+ legendItems.forEach((item) => {
6506
+ const swatch = item.querySelector(".pg-legend-swatch");
6507
+ const label = item.querySelector(".pg-legend-label");
6508
+ if (swatch && label) {
6509
+ const backgroundColor = swatch.style.backgroundColor || getComputedStyle(swatch).backgroundColor;
6510
+ const type = label.textContent || "Unknown";
6511
+ const color2 = normalizeColor(backgroundColor);
6512
+ legendEntries.push({
6513
+ type: type.trim(),
6514
+ color: color2
6515
+ });
4580
6516
  }
4581
- linkDeselectHandlers.add(handler);
4582
- return () => {
4583
- linkDeselectHandlers.delete(handler);
6517
+ });
6518
+ return legendEntries;
6519
+ }
6520
+ function calculateLegendDimensions(legendEntries, graphWidth) {
6521
+ const longestTypeName = legendEntries.reduce((max, entry) => {
6522
+ return entry.type.length > max ? entry.type.length : max;
6523
+ }, 0);
6524
+ const minLegendWidth = 180;
6525
+ const calculatedWidth = longestTypeName * 8 + 60;
6526
+ const width = Math.max(minLegendWidth, Math.min(calculatedWidth, 320));
6527
+ const itemHeight = 24;
6528
+ const itemSpacing = 8;
6529
+ const padding = 16;
6530
+ const height = padding + legendEntries.length * (itemHeight + itemSpacing) - itemSpacing + padding;
6531
+ const legendMargin = 20;
6532
+ const x3 = graphWidth + legendMargin;
6533
+ const y3 = 20;
6534
+ return {
6535
+ width,
6536
+ height,
6537
+ x: x3,
6538
+ y: y3,
6539
+ padding,
6540
+ itemHeight,
6541
+ itemSpacing
6542
+ };
6543
+ }
6544
+ function createLegendSVGElement(legendEntries, dimensions) {
6545
+ const {
6546
+ x: legendX,
6547
+ y: legendY,
6548
+ width: legendWidth,
6549
+ height: legendHeight,
6550
+ padding: legendPadding,
6551
+ itemHeight: legendItemHeight,
6552
+ itemSpacing: legendItemSpacing
6553
+ } = dimensions;
6554
+ const legendGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
6555
+ legendGroup.setAttribute("class", "export-legend");
6556
+ const legendBg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
6557
+ legendBg.setAttribute("x", legendX.toString());
6558
+ legendBg.setAttribute("y", legendY.toString());
6559
+ legendBg.setAttribute("width", legendWidth.toString());
6560
+ legendBg.setAttribute("height", legendHeight.toString());
6561
+ legendBg.setAttribute("fill", "#ffffff");
6562
+ legendBg.setAttribute("stroke", "#e2e8f0");
6563
+ legendBg.setAttribute("stroke-width", "1");
6564
+ legendBg.setAttribute("rx", "8");
6565
+ legendGroup.appendChild(legendBg);
6566
+ legendEntries.forEach((entry, index2) => {
6567
+ const itemY = legendY + legendPadding + index2 * (legendItemHeight + legendItemSpacing);
6568
+ const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
6569
+ circle.setAttribute("cx", (legendX + legendPadding + 7).toString());
6570
+ circle.setAttribute("cy", (itemY + legendItemHeight / 2).toString());
6571
+ circle.setAttribute("r", "7");
6572
+ circle.setAttribute("fill", entry.color);
6573
+ circle.setAttribute("stroke", "none");
6574
+ legendGroup.appendChild(circle);
6575
+ const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
6576
+ text.setAttribute("x", (legendX + legendPadding + 24).toString());
6577
+ text.setAttribute("y", (itemY + legendItemHeight / 2).toString());
6578
+ text.setAttribute("font-family", "Inter, -apple-system, BlinkMacSystemFont, sans-serif");
6579
+ text.setAttribute("font-size", "12");
6580
+ text.setAttribute("fill", "#6b7280");
6581
+ text.setAttribute("dominant-baseline", "middle");
6582
+ text.textContent = entry.type;
6583
+ legendGroup.appendChild(text);
6584
+ });
6585
+ return legendGroup;
6586
+ }
6587
+ function normalizeColor(color2) {
6588
+ const rgbMatch = color2.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
6589
+ if (rgbMatch) {
6590
+ const r = parseInt(rgbMatch[1] ?? "0");
6591
+ const g = parseInt(rgbMatch[2] ?? "0");
6592
+ const b = parseInt(rgbMatch[3] ?? "0");
6593
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
6594
+ }
6595
+ return color2;
6596
+ }
6597
+
6598
+ // src/utils/validation.ts
6599
+ var GraphValidator = class {
6600
+ /**
6601
+ * Validate complete graph configuration
6602
+ */
6603
+ static validateConfig(config) {
6604
+ const errors = [];
6605
+ const warnings = [];
6606
+ const containerValidation = this.validateContainer(config.container);
6607
+ errors.push(...containerValidation.errors);
6608
+ warnings.push(...containerValidation.warnings);
6609
+ const nodeValidation = this.validateNodes(config.nodes);
6610
+ errors.push(...nodeValidation.errors);
6611
+ warnings.push(...nodeValidation.warnings);
6612
+ const linkValidation = this.validateLinks(config.links, config.nodes);
6613
+ errors.push(...linkValidation.errors);
6614
+ warnings.push(...linkValidation.warnings);
6615
+ if (config.interaction) {
6616
+ const interactionValidation = this.validateInteraction(config.interaction);
6617
+ errors.push(...interactionValidation.errors);
6618
+ warnings.push(...interactionValidation.warnings);
6619
+ }
6620
+ return {
6621
+ isValid: errors.length === 0,
6622
+ errors,
6623
+ warnings
4584
6624
  };
4585
6625
  }
4586
- function off(event, handler) {
4587
- if (event === "nodeSelect") {
4588
- nodeSelectHandlers.delete(handler);
4589
- } else if (event === "nodeDeselect") {
4590
- nodeDeselectHandlers.delete(handler);
4591
- } else if (event === "linkSelect") {
4592
- linkSelectHandlers.delete(handler);
4593
- } else {
4594
- linkDeselectHandlers.delete(handler);
6626
+ /**
6627
+ * Validate container element
6628
+ */
6629
+ static validateContainer(container) {
6630
+ const errors = [];
6631
+ const warnings = [];
6632
+ if (!container) {
6633
+ errors.push({
6634
+ field: "container",
6635
+ message: "Container element is required",
6636
+ value: container,
6637
+ code: "CONTAINER_REQUIRED"
6638
+ });
6639
+ return { isValid: false, errors, warnings };
6640
+ }
6641
+ if (!(container instanceof HTMLElement)) {
6642
+ errors.push({
6643
+ field: "container",
6644
+ message: "Container must be a valid HTMLElement",
6645
+ value: container,
6646
+ code: "CONTAINER_INVALID_TYPE"
6647
+ });
4595
6648
  }
4596
- }
4597
- function render() {
4598
- destroy();
4599
- const layers = createGraphLayers(config.container);
4600
- svgElement = layers.svg;
4601
- rootGroup = layers.root;
4602
- cleanupResize = observeResize(config.container, (width, height) => {
4603
- dimensions = { width, height };
4604
- layers.svg.setAttribute("width", String(width));
4605
- layers.svg.setAttribute("height", String(height));
4606
- layers.interactionRect.setAttribute("width", String(width));
4607
- layers.interactionRect.setAttribute("height", String(height));
4608
- if (simulation) {
4609
- simulation.force("center", center_default(width / 2, height / 2));
4610
- simulation.alpha(0.3).restart();
4611
- }
4612
- if (fitViewTimer) {
4613
- clearTimeout(fitViewTimer);
4614
- }
4615
- fitViewTimer = setTimeout(() => {
4616
- fitView();
4617
- fitViewTimer = null;
4618
- }, 150);
4619
- });
4620
- const zoomResult = createZoom({
4621
- svg: layers.svg,
4622
- interactionLayer: layers.interactionLayer,
4623
- root: layers.root
6649
+ if (!document.contains(container)) {
6650
+ warnings.push("Container element is not attached to the DOM");
6651
+ }
6652
+ const rect = container.getBoundingClientRect();
6653
+ if (rect.width === 0 || rect.height === 0) {
6654
+ warnings.push("Container has zero width or height, graph may not render properly");
6655
+ }
6656
+ const styles = window.getComputedStyle(container);
6657
+ if (styles.position === "static") {
6658
+ warnings.push("Container should have position: relative for proper overlay positioning");
6659
+ }
6660
+ return { isValid: errors.length === 0, errors, warnings };
6661
+ }
6662
+ /**
6663
+ * Validate graph nodes
6664
+ */
6665
+ static validateNodes(nodes) {
6666
+ const errors = [];
6667
+ const warnings = [];
6668
+ if (!Array.isArray(nodes)) {
6669
+ errors.push({
6670
+ field: "nodes",
6671
+ message: "Nodes must be an array",
6672
+ value: nodes,
6673
+ code: "NODES_INVALID_TYPE"
6674
+ });
6675
+ return { isValid: false, errors, warnings };
6676
+ }
6677
+ if (nodes.length === 0) {
6678
+ warnings.push("No nodes provided, graph will be empty");
6679
+ return { isValid: true, errors, warnings };
6680
+ }
6681
+ const nodeIds = /* @__PURE__ */ new Set();
6682
+ const duplicateIds = /* @__PURE__ */ new Set();
6683
+ nodes.forEach((node, index2) => {
6684
+ const nodePrefix = `nodes[${index2}]`;
6685
+ if (!node || typeof node !== "object") {
6686
+ errors.push({
6687
+ field: `${nodePrefix}`,
6688
+ message: "Node must be an object",
6689
+ value: node,
6690
+ code: "NODE_INVALID_TYPE"
6691
+ });
6692
+ return;
6693
+ }
6694
+ if (!node.id || typeof node.id !== "string") {
6695
+ errors.push({
6696
+ field: `${nodePrefix}.id`,
6697
+ message: "Node ID is required and must be a string",
6698
+ value: node.id,
6699
+ code: "NODE_ID_REQUIRED"
6700
+ });
6701
+ } else {
6702
+ if (nodeIds.has(node.id)) {
6703
+ duplicateIds.add(node.id);
6704
+ errors.push({
6705
+ field: `${nodePrefix}.id`,
6706
+ message: `Duplicate node ID: ${node.id}`,
6707
+ value: node.id,
6708
+ code: "NODE_ID_DUPLICATE"
6709
+ });
6710
+ }
6711
+ nodeIds.add(node.id);
6712
+ }
6713
+ if (!node.type || typeof node.type !== "string") {
6714
+ errors.push({
6715
+ field: `${nodePrefix}.type`,
6716
+ message: "Node type is required and must be a string",
6717
+ value: node.type,
6718
+ code: "NODE_TYPE_REQUIRED"
6719
+ });
6720
+ }
6721
+ if (node.style) {
6722
+ const styleValidation = this.validateNodeStyle(node.style, `${nodePrefix}.style`);
6723
+ errors.push(...styleValidation.errors);
6724
+ warnings.push(...styleValidation.warnings);
6725
+ }
4624
6726
  });
4625
- zoomBehavior = zoomResult.behavior;
4626
- cleanupZoom = zoomResult.cleanup;
4627
- const root2 = select_default2(layers.root);
4628
- const renderContext = { svg: layers.svg, root: root2, interaction: config.interaction };
4629
- const linkSelection = renderLinks(renderContext, config.links);
4630
- const linkLabelSelection = renderLinkLabels(renderContext, config.links);
4631
- const nodeSelection = renderNodes(renderContext, config.nodes);
4632
- const labelSelection = renderNodeLabels(renderContext, config.nodes);
4633
- const simulationConfig = {
4634
- nodes: config.nodes,
4635
- links: config.links,
4636
- // Uses the observed dimensions to ensure physics are calculated on actual container size
4637
- width: dimensions.width || config.container.clientWidth,
4638
- height: dimensions.height || config.container.clientHeight
4639
- };
4640
- const simulationResult = createGraphSimulation(simulationConfig);
4641
- simulation = simulationResult.simulation;
4642
- simulation.on("tick", () => {
4643
- linkSelection.attr("x1", (item) => item.link.source.x ?? 0).attr("y1", (item) => item.link.source.y ?? 0).attr("x2", (item) => getShortenedTargetPoint(item.link, item.style).x).attr("y2", (item) => getShortenedTargetPoint(item.link, item.style).y);
4644
- linkLabelSelection.attr("transform", (item) => {
4645
- const link = item.link;
4646
- const source = link.source;
4647
- const targetPoint = getLinkTargetPoint(link);
4648
- const x3 = ((source.x ?? 0) + targetPoint.x) / 2;
4649
- const y3 = ((source.y ?? 0) + targetPoint.y) / 2;
4650
- return `translate(${x3}, ${y3})`;
4651
- }).each(function() {
4652
- const group = this;
4653
- const text = group.querySelector("text");
4654
- const rect = group.querySelector("rect");
4655
- if (!text || !rect) return;
4656
- const bBox = text.getBBox();
4657
- const padding = 6;
4658
- rect.setAttribute("x", String(bBox.x - padding));
4659
- rect.setAttribute("y", String(bBox.y - padding));
4660
- rect.setAttribute("width", String(bBox.width + padding * 2));
4661
- rect.setAttribute("height", String(bBox.height + padding * 2));
6727
+ return { isValid: errors.length === 0, errors, warnings };
6728
+ }
6729
+ /**
6730
+ * Validate graph links
6731
+ */
6732
+ static validateLinks(links, nodes) {
6733
+ const errors = [];
6734
+ const warnings = [];
6735
+ if (!Array.isArray(links)) {
6736
+ errors.push({
6737
+ field: "links",
6738
+ message: "Links must be an array",
6739
+ value: links,
6740
+ code: "LINKS_INVALID_TYPE"
4662
6741
  });
4663
- nodeSelection.attr("cx", (d) => d.x ?? 0).attr("cy", (d) => d.y ?? 0);
4664
- labelSelection.attr("x", (d) => d.x ?? 0).attr("y", (d) => d.y ?? 0);
4665
- tooltipBinding?.reposition();
6742
+ return { isValid: false, errors, warnings };
6743
+ }
6744
+ const nodeIds = new Set(nodes.map((n) => n.id).filter(Boolean));
6745
+ links.forEach((link, index2) => {
6746
+ const linkPrefix = `links[${index2}]`;
6747
+ if (!link || typeof link !== "object") {
6748
+ errors.push({
6749
+ field: `${linkPrefix}`,
6750
+ message: "Link must be an object",
6751
+ value: link,
6752
+ code: "LINK_INVALID_TYPE"
6753
+ });
6754
+ return;
6755
+ }
6756
+ const sourceId = typeof link.source === "string" ? link.source : link.source?.id;
6757
+ if (!sourceId) {
6758
+ errors.push({
6759
+ field: `${linkPrefix}.source`,
6760
+ message: "Link source is required",
6761
+ value: link.source,
6762
+ code: "LINK_SOURCE_REQUIRED"
6763
+ });
6764
+ } else if (!nodeIds.has(sourceId)) {
6765
+ errors.push({
6766
+ field: `${linkPrefix}.source`,
6767
+ message: `Link source node not found: ${sourceId}`,
6768
+ value: sourceId,
6769
+ code: "LINK_SOURCE_NOT_FOUND"
6770
+ });
6771
+ }
6772
+ const targetId = typeof link.target === "string" ? link.target : link.target?.id;
6773
+ if (!targetId) {
6774
+ errors.push({
6775
+ field: `${linkPrefix}.target`,
6776
+ message: "Link target is required",
6777
+ value: link.target,
6778
+ code: "LINK_TARGET_REQUIRED"
6779
+ });
6780
+ } else if (!nodeIds.has(targetId)) {
6781
+ errors.push({
6782
+ field: `${linkPrefix}.target`,
6783
+ message: `Link target node not found: ${targetId}`,
6784
+ value: targetId,
6785
+ code: "LINK_TARGET_NOT_FOUND"
6786
+ });
6787
+ }
6788
+ if (sourceId === targetId) {
6789
+ warnings.push(`Self-loop detected in link ${index2}: ${sourceId} -> ${targetId}`);
6790
+ }
4666
6791
  });
4667
- if (config.interaction?.hover?.enabled) {
4668
- if (config.interaction?.hover?.tooltip?.enabled) {
4669
- tooltipBinding = bindNodeTooltip({
4670
- container: config.container,
4671
- selection: nodeSelection,
4672
- tooltipConfig: config.interaction.hover.tooltip
6792
+ return { isValid: errors.length === 0, errors, warnings };
6793
+ }
6794
+ /**
6795
+ * Validate interaction configuration
6796
+ */
6797
+ static validateInteraction(interaction) {
6798
+ const errors = [];
6799
+ const warnings = [];
6800
+ if (interaction.hover?.tooltip?.enabled) {
6801
+ if (interaction.hover.tooltip.renderContent && typeof interaction.hover.tooltip.renderContent !== "function") {
6802
+ errors.push({
6803
+ field: "interaction.hover.tooltip.renderContent",
6804
+ message: "renderContent must be a function",
6805
+ value: interaction.hover.tooltip.renderContent,
6806
+ code: "TOOLTIP_RENDER_FUNCTION_INVALID"
4673
6807
  });
4674
6808
  }
4675
- createNodeHover(nodeSelection, config.interaction.hover.nodeStyle);
4676
- }
4677
- if (config.interaction?.drag?.enabled !== false) {
4678
- nodeSelection.call(createDragBehavior(simulation));
4679
6809
  }
4680
- const selectionConfig = config.interaction?.selection;
4681
- if (selectionConfig?.enabled) {
4682
- let selectedNodeElement = null;
4683
- let selectedLinkElement = null;
4684
- const linkMarkerSnapshots = /* @__PURE__ */ new Map();
4685
- linkSelection.each(function() {
4686
- const linkElement = this;
4687
- linkMarkerSnapshots.set(linkElement, linkElement.getAttribute("marker-end"));
6810
+ return { isValid: errors.length === 0, errors, warnings };
6811
+ }
6812
+ /**
6813
+ * Validate node style
6814
+ */
6815
+ static validateNodeStyle(style, fieldPrefix) {
6816
+ const errors = [];
6817
+ const warnings = [];
6818
+ if (typeof style !== "object" || style === null) {
6819
+ errors.push({
6820
+ field: fieldPrefix,
6821
+ message: "Style must be an object",
6822
+ value: style,
6823
+ code: "STYLE_INVALID_TYPE"
4688
6824
  });
4689
- nodeSelection.on("click.select", function(event, node) {
4690
- event.stopPropagation();
4691
- const nodeElement = this;
4692
- if (selectedNodeElement === nodeElement) {
4693
- deselectNode(nodeElement, root2, nodeSelectHandlers, nodeDeselectHandlers);
4694
- selectedNodeElement = null;
4695
- return;
4696
- }
4697
- if (selectedNodeElement) {
4698
- deselectNode(selectedNodeElement, root2, nodeSelectHandlers, nodeDeselectHandlers);
4699
- selectedNodeElement = null;
4700
- }
4701
- if (selectedLinkElement) {
4702
- deselectLink(selectedLinkElement, linkMarkerSnapshots, linkSelectHandlers, linkDeselectHandlers);
4703
- selectedLinkElement = null;
4704
- }
4705
- selectedNodeElement = nodeElement;
4706
- const nodeStyle = selectionConfig.nodeStyle;
4707
- if (nodeStyle) {
4708
- if (nodeStyle.fill !== void 0) {
4709
- nodeElement.style.fill = nodeStyle.fill;
4710
- }
4711
- if (nodeStyle.stroke !== void 0) {
4712
- nodeElement.style.stroke = nodeStyle.stroke;
4713
- }
4714
- if (nodeStyle.strokeWidth !== void 0) {
4715
- nodeElement.style.strokeWidth = String(nodeStyle.strokeWidth);
4716
- }
4717
- if (nodeStyle.opacity !== void 0) {
4718
- nodeElement.style.opacity = String(nodeStyle.opacity);
4719
- }
4720
- if (nodeStyle.radius !== void 0) {
4721
- nodeElement.style.setProperty("r", String(nodeStyle.radius));
4722
- }
4723
- }
4724
- root2.selectAll(".link-label").filter((item) => {
4725
- if (item.style.label.visibility !== "hover") {
4726
- return false;
4727
- }
4728
- const source = item.link.source;
4729
- const target = item.link.target;
4730
- return source.id === node.id || target.id === node.id;
4731
- }).classed("label-selection-pinned", true).interrupt().transition().duration(200).style("opacity", 1).style("pointer-events", "auto");
4732
- nodeSelectHandlers.forEach((handler) => handler(node, nodeElement));
4733
- });
4734
- linkSelection.on("click.select", function(event, renderableLink) {
4735
- const linkElement = this;
4736
- if (selectedLinkElement === linkElement) {
4737
- deselectLink(linkElement, linkMarkerSnapshots, linkSelectHandlers, linkDeselectHandlers);
4738
- selectedLinkElement = null;
4739
- return;
4740
- }
4741
- if (selectedLinkElement) {
4742
- deselectLink(selectedLinkElement, linkMarkerSnapshots, linkSelectHandlers, linkDeselectHandlers);
4743
- selectedLinkElement = null;
4744
- }
4745
- if (selectedNodeElement) {
4746
- deselectNode(selectedNodeElement, root2, nodeSelectHandlers, nodeDeselectHandlers);
4747
- selectedNodeElement = null;
6825
+ return { isValid: false, errors, warnings };
6826
+ }
6827
+ if (style.radius !== void 0) {
6828
+ if (typeof style.radius !== "number" || style.radius < 0) {
6829
+ errors.push({
6830
+ field: `${fieldPrefix}.radius`,
6831
+ message: "Radius must be a non-negative number",
6832
+ value: style.radius,
6833
+ code: "RADIUS_INVALID"
6834
+ });
6835
+ }
6836
+ }
6837
+ const colorFields = ["fill", "stroke", "textColor"];
6838
+ colorFields.forEach((field) => {
6839
+ if (style[field] !== void 0) {
6840
+ if (typeof style[field] !== "string") {
6841
+ errors.push({
6842
+ field: `${fieldPrefix}.${field}`,
6843
+ message: `${field} must be a string`,
6844
+ value: style[field],
6845
+ code: "COLOR_INVALID_TYPE"
6846
+ });
4748
6847
  }
4749
- selectedLinkElement = linkElement;
4750
- selectLink(event, renderableLink, linkElement, selectionConfig, layers, linkSelectHandlers);
4751
- });
4752
- const linkHitAreaSelection = createLinkHitArea(root2, linkSelection);
4753
- simulation.on("tick.hitarea", () => {
4754
- 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);
6848
+ }
6849
+ });
6850
+ return { isValid: errors.length === 0, errors, warnings };
6851
+ }
6852
+ /**
6853
+ * Validate runtime environment
6854
+ */
6855
+ static validateEnvironment() {
6856
+ const errors = [];
6857
+ const warnings = [];
6858
+ if (typeof window === "undefined") {
6859
+ errors.push({
6860
+ field: "environment",
6861
+ message: "Graph library requires a browser environment",
6862
+ value: "server",
6863
+ code: "ENVIRONMENT_NOT_BROWSER"
4755
6864
  });
4756
- linkHitAreaSelection.on("click.select", function(event, renderableLink) {
4757
- const visibleLinkNode = linkSelection.filter((d) => d === renderableLink).node();
4758
- if (visibleLinkNode) {
4759
- if (selectedLinkElement === visibleLinkNode) {
4760
- deselectLink(visibleLinkNode, linkMarkerSnapshots, linkSelectHandlers, linkDeselectHandlers);
4761
- selectedLinkElement = null;
4762
- return;
4763
- }
4764
- if (selectedLinkElement) {
4765
- deselectLink(selectedLinkElement, linkMarkerSnapshots, linkSelectHandlers, linkDeselectHandlers);
4766
- selectedLinkElement = null;
4767
- }
4768
- if (selectedNodeElement) {
4769
- deselectNode(selectedNodeElement, root2, nodeSelectHandlers, nodeDeselectHandlers);
4770
- selectedNodeElement = null;
4771
- }
4772
- selectedLinkElement = visibleLinkNode;
4773
- selectLink(event, renderableLink, visibleLinkNode, selectionConfig, layers, linkSelectHandlers);
4774
- }
6865
+ }
6866
+ if (typeof document === "undefined") {
6867
+ errors.push({
6868
+ field: "environment.document",
6869
+ message: "Document API not available",
6870
+ value: void 0,
6871
+ code: "DOCUMENT_API_MISSING"
4775
6872
  });
4776
- select_default2(layers.svg).on("click.deselect", () => {
4777
- if (selectedNodeElement) {
4778
- deselectNode(selectedNodeElement, root2, nodeSelectHandlers, nodeDeselectHandlers);
4779
- selectedNodeElement = null;
4780
- }
4781
- if (selectedLinkElement) {
4782
- deselectLink(selectedLinkElement, linkMarkerSnapshots, linkSelectHandlers, linkDeselectHandlers);
4783
- selectedLinkElement = null;
6873
+ }
6874
+ if (typeof ResizeObserver === "undefined") {
6875
+ warnings.push("ResizeObserver not available, responsive features may not work");
6876
+ }
6877
+ if (typeof requestAnimationFrame === "undefined") {
6878
+ warnings.push("requestAnimationFrame not available, performance optimizations disabled");
6879
+ }
6880
+ return { isValid: errors.length === 0, errors, warnings };
6881
+ }
6882
+ };
6883
+ var GraphValidationError = class extends Error {
6884
+ errors;
6885
+ warnings;
6886
+ constructor(result) {
6887
+ const errorMessages = result.errors.map((e) => `${e.field}: ${e.message}`).join(", ");
6888
+ super(`Graph validation failed: ${errorMessages}`);
6889
+ this.name = "GraphValidationError";
6890
+ this.errors = result.errors;
6891
+ this.warnings = result.warnings;
6892
+ }
6893
+ };
6894
+
6895
+ // src/create-graph.ts
6896
+ function createGraph(config) {
6897
+ const envValidation = GraphValidator.validateEnvironment();
6898
+ if (!envValidation.isValid) {
6899
+ throw new GraphValidationError(envValidation);
6900
+ }
6901
+ if (envValidation.warnings.length > 0) {
6902
+ console.warn("[Polly Graph] Environment warnings:", envValidation.warnings);
6903
+ }
6904
+ const configValidation = GraphValidator.validateConfig(config);
6905
+ if (!configValidation.isValid) {
6906
+ throw new GraphValidationError(configValidation);
6907
+ }
6908
+ if (configValidation.warnings.length > 0) {
6909
+ console.warn("[Polly Graph] Configuration warnings:", configValidation.warnings);
6910
+ }
6911
+ const graphManager = new GraphManager(config);
6912
+ const renderPipeline = new RenderPipeline(graphManager);
6913
+ const interactionManager = new InteractionManager(graphManager);
6914
+ function render() {
6915
+ try {
6916
+ renderPipeline.execute().then((selections) => {
6917
+ interactionManager.setupInteractions(selections);
6918
+ setupAdditionalComponents();
6919
+ if (graphManager.needsImmediateFitView) {
6920
+ graphManager.needsImmediateFitView = false;
6921
+ fitViewWithInitialPositions();
4784
6922
  }
6923
+ }).catch((error) => {
6924
+ console.error("[Polly Graph] Render failed:", error);
4785
6925
  });
6926
+ } catch (error) {
6927
+ console.error("[Polly Graph] Render failed:", error);
4786
6928
  }
4787
- if (config.controls?.enabled) {
4788
- controls = createGraphControls(
4789
- layers.overlay,
6929
+ }
6930
+ function setupAdditionalComponents() {
6931
+ if (config.controls?.enabled && graphManager.layers) {
6932
+ graphManager.controls = createGraphControls(
6933
+ graphManager.layers.overlay,
4790
6934
  { zoomIn, zoomOut, resetView, fitView },
4791
6935
  config.controls
4792
6936
  );
4793
- controls.mount();
6937
+ graphManager.controls.mount();
6938
+ }
6939
+ if (config.legend?.enabled && graphManager.layers) {
6940
+ const legendCleanup = createGraphLegend(
6941
+ graphManager.layers.overlay,
6942
+ config.legend,
6943
+ config.nodes
6944
+ );
6945
+ graphManager.addCleanup(legendCleanup);
4794
6946
  }
4795
- if (config.legend?.enabled) {
4796
- legendCleanup = createGraphLegend(layers.overlay, config.legend);
6947
+ const handleVisibilityChange = () => {
6948
+ if (!graphManager.simulation || !graphManager.timerManager) return;
6949
+ graphManager.timerManager.clearTimer("simulation-cooldown");
6950
+ if (document.hidden) {
6951
+ graphManager.simulation.stop();
6952
+ return;
6953
+ }
6954
+ graphManager.reheatSimulation(0.3);
6955
+ };
6956
+ document.addEventListener("visibilitychange", handleVisibilityChange);
6957
+ graphManager.addCleanup(() => {
6958
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
6959
+ });
6960
+ }
6961
+ function zoomIn() {
6962
+ if (!graphManager.zoomBehavior || !graphManager.svgElement) {
6963
+ console.warn("[Polly Graph] Zoom behavior not available");
6964
+ return;
6965
+ }
6966
+ const svg = graphManager.svgElement;
6967
+ select_default2(svg).transition().duration(400).call(graphManager.zoomBehavior.scaleBy, 1.5);
6968
+ }
6969
+ function zoomOut() {
6970
+ if (!graphManager.zoomBehavior || !graphManager.svgElement) {
6971
+ console.warn("[Polly Graph] Zoom behavior not available");
6972
+ return;
4797
6973
  }
6974
+ const svg = graphManager.svgElement;
6975
+ select_default2(svg).transition().duration(400).call(graphManager.zoomBehavior.scaleBy, 1 / 1.5);
4798
6976
  }
4799
6977
  function resetView() {
4800
- if (!zoomBehavior || !svgElement) {
6978
+ if (!graphManager.zoomBehavior || !graphManager.svgElement) {
6979
+ console.warn("[Polly Graph] Zoom behavior not available");
4801
6980
  return;
4802
6981
  }
4803
- 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);
4804
6984
  }
4805
6985
  function fitView() {
4806
- 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");
4807
6988
  return;
4808
6989
  }
4809
- const bounds = rootGroup.getBBox();
4810
- 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) {
4811
7005
  return;
4812
7006
  }
4813
- const scale = Math.min(dimensions.width / bounds.width, dimensions.height / bounds.height) * 0.9;
4814
- const translateX = (dimensions.width - bounds.width * scale) / 2 - bounds.x * scale;
4815
- const translateY = (dimensions.height - bounds.height * scale) / 2 - bounds.y * scale;
4816
- const transform2 = identity2.translate(translateX, translateY).scale(scale);
4817
- 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
+ }
4818
7014
  }
4819
- function zoomIn() {
4820
- 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");
4821
7018
  return;
4822
7019
  }
4823
- select_default2(svgElement).transition().call(zoomBehavior.scaleBy, 1.2);
4824
- }
4825
- function zoomOut() {
4826
- 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) {
4827
7035
  return;
4828
7036
  }
4829
- 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
+ }
4830
7044
  }
4831
- async function exportGraph(fileName) {
4832
- fitView();
4833
- await new Promise((resolve) => setTimeout(resolve, 500));
4834
- await captureAndDownloadGraph(config.container, {
4835
- 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",
4836
7052
  pixelRatio: 2
4837
7053
  });
4838
7054
  }
4839
- function destroy() {
4840
- if (fitViewTimer) {
4841
- clearTimeout(fitViewTimer);
4842
- fitViewTimer = null;
4843
- }
4844
- if (cleanupResize) {
4845
- cleanupResize();
4846
- cleanupResize = null;
4847
- }
4848
- if (cleanupZoom) {
4849
- cleanupZoom();
4850
- cleanupZoom = null;
4851
- }
4852
- if (tooltipBinding) {
4853
- tooltipBinding.destroy();
4854
- tooltipBinding = null;
4855
- }
4856
- if (simulation) {
4857
- simulation.stop();
4858
- simulation = null;
7055
+ function clearSelection() {
7056
+ if (graphManager.selectionManager) {
7057
+ graphManager.selectionManager.clearSelection();
4859
7058
  }
4860
- if (controls) {
4861
- controls.destroy();
4862
- controls = null;
4863
- }
4864
- if (legendCleanup) {
4865
- legendCleanup();
4866
- legendCleanup = null;
4867
- }
4868
- rootGroup = null;
4869
- svgElement = null;
4870
- zoomBehavior = null;
4871
- while (config.container.firstChild) {
4872
- config.container.removeChild(config.container.firstChild);
7059
+ }
7060
+ function on(event, handler) {
7061
+ if (!graphManager.eventEmitter) {
7062
+ console.warn("[Polly Graph] Event emitter not available");
7063
+ return () => {
7064
+ };
4873
7065
  }
7066
+ switch (event) {
7067
+ case "nodeSelect":
7068
+ return graphManager.eventEmitter.on("nodeSelect", (data) => handler(data.node, data.element));
7069
+ case "nodeDeselect":
7070
+ return graphManager.eventEmitter.on("nodeDeselect", (data) => handler(data.node, data.element));
7071
+ case "linkSelect":
7072
+ return graphManager.eventEmitter.on("linkSelect", (data) => handler(data.link, data.element));
7073
+ case "linkDeselect":
7074
+ return graphManager.eventEmitter.on("linkDeselect", (data) => handler(data.link, data.element));
7075
+ default:
7076
+ console.warn("[Polly Graph] Unknown event:", event);
7077
+ return () => {
7078
+ };
7079
+ }
7080
+ }
7081
+ function off(event, _handler) {
7082
+ if (!graphManager.eventEmitter) return;
7083
+ graphManager.eventEmitter.removeAllListeners(event);
4874
7084
  }
4875
- return { render, zoomIn, zoomOut, resetView, fitView, destroy, exportGraph, on, off };
7085
+ function destroy() {
7086
+ graphManager.destroy();
7087
+ }
7088
+ graphManager.fitViewCallback = () => {
7089
+ fitView();
7090
+ };
7091
+ return {
7092
+ render,
7093
+ zoomIn,
7094
+ zoomOut,
7095
+ resetView,
7096
+ fitView,
7097
+ destroy,
7098
+ exportGraph,
7099
+ clearSelection,
7100
+ on,
7101
+ off
7102
+ };
4876
7103
  }
4877
7104
  export {
7105
+ ErrorHandler,
7106
+ GraphError,
7107
+ GraphValidationError,
7108
+ GraphValidator,
7109
+ SelectionManager,
7110
+ TypedGraphEventEmitter,
4878
7111
  createGraph
4879
7112
  };