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