polly-graph 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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,248 +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/utils/get-link-marker-id.ts
3771
- function getLinkMarkerId(style) {
3772
- const markerStyle = {
3773
- stroke: style.stroke ?? "#94a3b8",
3774
- strokeWidth: style.strokeWidth ?? 2,
3775
- arrowFill: style.arrow?.fill ?? style.stroke ?? "#94a3b8",
3776
- arrowSize: style.arrow?.size ?? 6
3777
- };
3778
- const serializedStyle = JSON.stringify(markerStyle);
3779
- const hash = createHash(serializedStyle);
3780
- return `graph-arrow-${hash}`;
3781
- }
3782
- function createHash(value) {
3783
- let hash = 0;
3784
- for (let index2 = 0; index2 < value.length; index2 += 1) {
3785
- const charCode = value.charCodeAt(index2);
3786
- hash = (hash << 5) - hash + charCode;
3787
- hash |= 0;
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;
3788
4426
  }
3789
- return Math.abs(hash).toString(36);
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
+ });
3790
4456
  }
3791
4457
 
3792
- // src/core/create-arrow-marker.ts
3793
- function createArrowMarker(params) {
3794
- const markerId = getLinkMarkerId(params.style);
3795
- const existingMarker = params.svg.querySelector(`#${markerId}`);
3796
- if (existingMarker) {
3797
- return markerId;
3798
- }
3799
- const arrowSize = params.style.arrow?.size ?? 6;
3800
- const fill = params.style.arrow?.fill ?? params.style.stroke ?? "#94a3b8";
3801
- const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
3802
- const marker = document.createElementNS("http://www.w3.org/2000/svg", "marker");
3803
- marker.setAttribute("id", markerId);
3804
- marker.setAttribute("viewBox", "0 0 20 20");
3805
- marker.setAttribute("refX", "0");
3806
- marker.setAttribute("refY", "10");
3807
- marker.setAttribute("markerWidth", String(arrowSize * 2));
3808
- marker.setAttribute("markerHeight", String(arrowSize * 2));
3809
- marker.setAttribute("orient", "auto");
3810
- marker.setAttribute("markerUnits", "userSpaceOnUse");
3811
- const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
3812
- path.setAttribute("d", "M 0 0 L 20 10 L 0 20 z");
3813
- path.setAttribute("fill", fill);
3814
- marker.appendChild(path);
3815
- defs.appendChild(marker);
3816
- params.svg.insertBefore(defs, params.svg.firstChild);
3817
- return markerId;
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
+ };
3818
4477
  }
3819
4478
 
3820
- // src/controls/graph-controls.utils.ts
3821
- function resolveControlsPosition(position) {
3822
- return position ?? "bottom-left";
3823
- }
3824
- function resolveControlsOrientation(orientation) {
3825
- return orientation ?? "vertical";
3826
- }
3827
- function shouldRenderControl(config, key) {
3828
- const show = config.show;
3829
- if (!show) {
3830
- return true;
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;
3831
4490
  }
3832
- const value = show[key];
3833
- if (value === void 0) {
3834
- return true;
3835
- }
3836
- return value;
3837
- }
3838
-
3839
- // src/assets/plus.svg?raw
3840
- 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>';
3841
-
3842
- // src/assets/minus.svg?raw
3843
- 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>';
3844
-
3845
- // src/assets/fit.svg?raw
3846
- 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>';
3847
-
3848
- // src/assets/reset.svg?raw
3849
- 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>';
3850
-
3851
- // src/controls/graph-controls.icons.ts
3852
- var ICON_MAP = {
3853
- "zoom-in": plus_default,
3854
- "zoom-out": minus_default,
3855
- fit: fit_default,
3856
- reset: reset_default
3857
4491
  };
3858
- function getControlIcon(icon) {
3859
- const raw = ICON_MAP[icon];
3860
- if (!raw) {
3861
- throw new Error(`Icon not found: ${icon}`);
3862
- }
3863
- return raw.replace("<svg", '<svg class="pg-icon"');
3864
- }
3865
-
3866
- // src/controls/create-graph-controls.ts
3867
- function createGraphControls(overlay, graph, config) {
3868
- let root2 = null;
3869
- function mount() {
3870
- if (!config.enabled) {
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
+ );
3871
4594
  return;
3872
4595
  }
3873
- root2 = document.createElement("div");
3874
- root2.className = "pg-controls";
3875
- const position = resolveControlsPosition(config.position);
3876
- root2.classList.add(`pg-pos-${position}`);
3877
- const orientation = resolveControlsOrientation(config.orientation);
3878
- root2.classList.add(`pg-orient-${orientation}`);
3879
- if (config.offset) {
3880
- root2.style.setProperty("--pg-controls-offset-x", `${config.offset.x}px`);
3881
- root2.style.setProperty("--pg-controls-offset-y", `${config.offset.y}px`);
3882
- }
3883
- appendControls(root2, config, graph);
3884
- overlay.appendChild(root2);
4596
+ this.safeD3Operation(() => operation(selection2), context);
3885
4597
  }
3886
- function appendControls(root3, config2, graph2) {
3887
- const actions = [
3888
- { key: "zoomIn", icon: "zoom-in", label: "Zoom in", fn: graph2.zoomIn.bind(graph2) },
3889
- { key: "zoomOut", icon: "zoom-out", label: "Zoom out", fn: graph2.zoomOut.bind(graph2) },
3890
- { key: "fit", icon: "fit", label: "Fit view", fn: graph2.fitView.bind(graph2) },
3891
- { key: "reset", icon: "reset", label: "Reset view", fn: graph2.resetView.bind(graph2) }
3892
- ];
3893
- actions.forEach((action) => {
3894
- if (shouldRenderControl(config2, action.key)) {
3895
- 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();
3896
4610
  }
3897
4611
  });
3898
4612
  }
3899
- function createButton(type, label, onClick) {
3900
- const button = document.createElement("button");
3901
- button.className = "pg-control-btn";
3902
- button.type = "button";
3903
- button.setAttribute("aria-label", label);
3904
- const wrapper = document.createElement("div");
3905
- wrapper.className = "pg-icon-wrapper";
3906
- wrapper.innerHTML = getControlIcon(type);
3907
- const svg = wrapper.querySelector("svg");
3908
- if (svg) {
3909
- svg.classList.add("pg-icon");
3910
- button.appendChild(svg);
3911
- }
3912
- button.addEventListener("click", onClick);
3913
- return button;
3914
- }
3915
- function destroy() {
3916
- if (!root2) {
3917
- return;
3918
- }
3919
- if (root2.parentNode === overlay) {
3920
- overlay.removeChild(root2);
3921
- }
3922
- root2 = null;
3923
- }
3924
- return { mount, destroy };
3925
- }
3926
-
3927
- // src/assets/caret.svg?raw
3928
- 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>';
3929
-
3930
- // src/legends/graph-legend-icon.ts
3931
- var LEGEND_ICON_MAP = { caret: caret_default };
3932
- function getLegendIcon(icon) {
3933
- const raw = LEGEND_ICON_MAP[icon];
3934
- if (!raw) {
3935
- throw new Error(`Legend icon not found: ${icon}`);
3936
- }
3937
- return raw.replace("<svg", '<svg class="pg-icon"');
3938
- }
3939
-
3940
- // src/legends/create-graph-legends.ts
3941
- function createGraphLegend(overlay, config) {
3942
- const legendWrapper = document.createElement("div");
3943
- legendWrapper.className = "pg-legend";
3944
- const position = config.position || "bottom-right";
3945
- legendWrapper.classList.add(`pg-pos-${position}`);
3946
- if (config.defaultExpanded === false) {
3947
- 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;
3948
4618
  }
3949
- if (config.collapsible) {
3950
- const toggleBtn = document.createElement("button");
3951
- toggleBtn.className = "pg-legend-toggle";
3952
- toggleBtn.type = "button";
3953
- toggleBtn.innerHTML = getLegendIcon("caret");
3954
- toggleBtn.onclick = (e) => {
3955
- e.stopPropagation();
3956
- legendWrapper.classList.toggle("pg-is-collapsed");
3957
- };
3958
- legendWrapper.appendChild(toggleBtn);
4619
+ /**
4620
+ * Reset error handler state
4621
+ */
4622
+ static reset() {
4623
+ this.isDestroyed = false;
3959
4624
  }
3960
- const body = document.createElement("div");
3961
- body.className = "pg-legend-body";
3962
- const list = document.createElement("ul");
3963
- list.className = "pg-legend-list";
3964
- config.items.forEach((item) => {
3965
- const listItem = document.createElement("li");
3966
- listItem.className = "pg-legend-item";
3967
- const swatch = document.createElement("span");
3968
- swatch.className = `pg-legend-swatch is-${item.shape || "circle"}`;
3969
- swatch.style.backgroundColor = item.color;
3970
- const label = document.createElement("span");
3971
- label.className = "pg-legend-label";
3972
- label.innerText = item.label;
3973
- listItem.appendChild(swatch);
3974
- listItem.appendChild(label);
3975
- list.appendChild(listItem);
3976
- });
3977
- body.appendChild(list);
3978
- legendWrapper.appendChild(body);
3979
- overlay.appendChild(legendWrapper);
3980
- return () => {
3981
- if (legendWrapper.parentNode === overlay) overlay.removeChild(legendWrapper);
3982
- };
3983
- }
4625
+ };
3984
4626
 
3985
4627
  // src/utils/resolve-link-style.ts
3986
4628
  var DEFAULT_LINK_STYLE = {
@@ -4001,7 +4643,7 @@ var DEFAULT_LINK_STYLE = {
4001
4643
  borderWidth: 1.5,
4002
4644
  borderRadius: 4,
4003
4645
  textColor: "color-mix(in srgb, #8E42EE, #000000 40%)",
4004
- fontSize: 12,
4646
+ fontSize: 10,
4005
4647
  paddingX: 8,
4006
4648
  paddingY: 4,
4007
4649
  height: 24
@@ -4045,7 +4687,78 @@ function mergeLinkStyle(base, override) {
4045
4687
  };
4046
4688
  }
4047
4689
 
4690
+ // src/utils/get-link-marker-id.ts
4691
+ function getLinkMarkerId(style) {
4692
+ const markerStyle = {
4693
+ stroke: style.stroke ?? "#94a3b8",
4694
+ strokeWidth: style.strokeWidth ?? 2,
4695
+ arrowFill: style.arrow?.fill ?? style.stroke ?? "#94a3b8",
4696
+ arrowSize: style.arrow?.size ?? 6
4697
+ };
4698
+ const serializedStyle = JSON.stringify(markerStyle);
4699
+ const hash = createHash(serializedStyle);
4700
+ return `graph-arrow-${hash}`;
4701
+ }
4702
+ function createHash(value) {
4703
+ let hash = 0;
4704
+ for (let index2 = 0; index2 < value.length; index2 += 1) {
4705
+ const charCode = value.charCodeAt(index2);
4706
+ hash = (hash << 5) - hash + charCode;
4707
+ hash |= 0;
4708
+ }
4709
+ return Math.abs(hash).toString(36);
4710
+ }
4711
+
4712
+ // src/core/create-arrow-marker.ts
4713
+ function createArrowMarker(params) {
4714
+ const markerId = getLinkMarkerId(params.style);
4715
+ const existingMarker = params.svg.querySelector(`#${markerId}`);
4716
+ if (existingMarker) {
4717
+ return markerId;
4718
+ }
4719
+ const arrowSize = params.style.arrow?.size ?? 6;
4720
+ const fill = params.style.arrow?.fill ?? params.style.stroke ?? "#94a3b8";
4721
+ const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
4722
+ const marker = document.createElementNS("http://www.w3.org/2000/svg", "marker");
4723
+ marker.setAttribute("id", markerId);
4724
+ marker.setAttribute("viewBox", "0 0 20 20");
4725
+ marker.setAttribute("refX", "0");
4726
+ marker.setAttribute("refY", "10");
4727
+ marker.setAttribute("markerWidth", String(arrowSize * 2));
4728
+ marker.setAttribute("markerHeight", String(arrowSize * 2));
4729
+ marker.setAttribute("orient", "auto");
4730
+ marker.setAttribute("markerUnits", "userSpaceOnUse");
4731
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
4732
+ path.setAttribute("d", "M 0 0 L 20 10 L 0 20 z");
4733
+ path.setAttribute("fill", fill);
4734
+ marker.appendChild(path);
4735
+ defs.appendChild(marker);
4736
+ params.svg.insertBefore(defs, params.svg.firstChild);
4737
+ return markerId;
4738
+ }
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,22 +5145,257 @@ 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) {
4240
- return item.style.label.visibility === "hover" && !this.classList.contains("label-selection-pinned");
4241
- }).interrupt().transition().duration(200).style("opacity", 0).style("pointer-events", "none");
4242
- });
4243
- }
4244
-
4245
- // src/utils/resolve-tooltip-position.ts
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) {
5338
+ return item.style.label.visibility === "hover" && !this.classList.contains("label-selection-pinned");
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
+ }
5395
+ });
5396
+ }
5397
+
5398
+ // src/utils/resolve-tooltip-position.ts
4246
5399
  function resolveTooltipPosition(params) {
4247
5400
  const preferredPlacement = params.placement ?? "auto";
4248
5401
  const resolvedPlacement = preferredPlacement === "auto" ? resolveAutoPlacement(params) : preferredPlacement;
@@ -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,382 +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");
5706
+ }
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;
4523
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
+ });
4524
5749
  }
4525
- }
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
+ };
4526
5796
 
4527
- // src/create-graph.ts
4528
- function createGraph(config) {
4529
- let cleanupResize = null;
4530
- let cleanupZoom = null;
4531
- let tooltipBinding = null;
4532
- let controls = null;
4533
- let legendCleanup = null;
4534
- let fitViewTimer = null;
4535
- let dimensions = { width: 0, height: 0 };
4536
- let rootGroup = null;
4537
- let svgElement = null;
4538
- let zoomBehavior = null;
4539
- let simulation = null;
4540
- const nodeSelectHandlers = /* @__PURE__ */ new Set();
4541
- const linkSelectHandlers = /* @__PURE__ */ new Set();
4542
- function on(event, handler) {
4543
- if (event === "nodeSelect") {
4544
- nodeSelectHandlers.add(handler);
4545
- return () => {
4546
- nodeSelectHandlers.delete(handler);
4547
- };
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
+ }
4548
5862
  }
4549
- linkSelectHandlers.add(handler);
4550
- return () => {
4551
- linkSelectHandlers.delete(handler);
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
4552
5869
  };
4553
- }
4554
- function off(event, handler) {
4555
- if (event === "nodeSelect") {
4556
- nodeSelectHandlers.delete(handler);
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);
4557
5908
  } else {
4558
- linkSelectHandlers.delete(handler);
4559
- }
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
+ });
4560
5988
  }
4561
- function render() {
4562
- destroy();
4563
- const layers = createGraphLayers(config.container);
4564
- svgElement = layers.svg;
4565
- rootGroup = layers.root;
4566
- cleanupResize = observeResize(config.container, (width, height) => {
4567
- dimensions = { width, height };
4568
- layers.svg.setAttribute("width", String(width));
4569
- layers.svg.setAttribute("height", String(height));
4570
- layers.interactionRect.setAttribute("width", String(width));
4571
- layers.interactionRect.setAttribute("height", String(height));
4572
- if (simulation) {
4573
- simulation.force("center", center_default(width / 2, height / 2));
4574
- simulation.alpha(0.3).restart();
4575
- }
4576
- if (fitViewTimer) {
4577
- clearTimeout(fitViewTimer);
4578
- }
4579
- fitViewTimer = setTimeout(() => {
4580
- fitView();
4581
- fitViewTimer = null;
4582
- }, 150);
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
+ }
4583
5999
  });
4584
- const zoomResult = createZoom({
4585
- svg: layers.svg,
4586
- interactionLayer: layers.interactionLayer,
4587
- root: layers.root
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
+ }
4588
6010
  });
4589
- zoomBehavior = zoomResult.behavior;
4590
- cleanupZoom = zoomResult.cleanup;
4591
- const root2 = select_default2(layers.root);
4592
- const renderContext = { svg: layers.svg, root: root2, interaction: config.interaction };
4593
- const linkSelection = renderLinks(renderContext, config.links);
4594
- const linkLabelSelection = renderLinkLabels(renderContext, config.links);
4595
- const nodeSelection = renderNodes(renderContext, config.nodes);
4596
- const labelSelection = renderNodeLabels(renderContext, config.nodes);
4597
- const simulationConfig = {
4598
- nodes: config.nodes,
4599
- links: config.links,
4600
- // Uses the observed dimensions to ensure physics are calculated on actual container size
4601
- width: dimensions.width || config.container.clientWidth,
4602
- height: dimensions.height || config.container.clientHeight
4603
- };
4604
- const simulationResult = createGraphSimulation(simulationConfig);
4605
- simulation = simulationResult.simulation;
4606
- simulation.on("tick", () => {
4607
- 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);
4608
- linkLabelSelection.attr("transform", (item) => {
4609
- const link = item.link;
4610
- const source = link.source;
4611
- const targetPoint = getLinkTargetPoint(link);
4612
- const x3 = ((source.x ?? 0) + targetPoint.x) / 2;
4613
- const y3 = ((source.y ?? 0) + targetPoint.y) / 2;
4614
- return `translate(${x3}, ${y3})`;
4615
- }).each(function() {
4616
- const group = this;
4617
- const text = group.querySelector("text");
4618
- const rect = group.querySelector("rect");
4619
- if (!text || !rect) return;
4620
- const bBox = text.getBBox();
4621
- const padding = 6;
4622
- rect.setAttribute("x", String(bBox.x - padding));
4623
- rect.setAttribute("y", String(bBox.y - padding));
4624
- rect.setAttribute("width", String(bBox.width + padding * 2));
4625
- rect.setAttribute("height", String(bBox.height + padding * 2));
4626
- });
4627
- nodeSelection.attr("cx", (d) => d.x ?? 0).attr("cy", (d) => d.y ?? 0);
4628
- labelSelection.attr("x", (d) => d.x ?? 0).attr("y", (d) => d.y ?? 0);
4629
- tooltipBinding?.reposition();
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
+ }
4630
6036
  });
4631
- if (config.interaction?.hover?.enabled) {
4632
- if (config.interaction?.hover?.tooltip?.enabled) {
4633
- tooltipBinding = bindNodeTooltip({
4634
- container: config.container,
4635
- selection: nodeSelection,
4636
- tooltipConfig: config.interaction.hover.tooltip
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
4637
6076
  });
6077
+ linkElement.dispatchEvent(event);
4638
6078
  }
4639
- createNodeHover(nodeSelection, config.interaction.hover.nodeStyle);
4640
6079
  }
4641
- if (config.interaction?.drag?.enabled !== false) {
4642
- nodeSelection.call(createDragBehavior(simulation));
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
+ }
4643
6092
  }
4644
- const selectionConfig = config.interaction?.selection;
4645
- if (selectionConfig?.enabled) {
4646
- let selectedNodeElement = null;
4647
- let selectedLinkElement = null;
4648
- const linkMarkerSnapshots = /* @__PURE__ */ new Map();
4649
- linkSelection.each(function() {
4650
- const linkElement = this;
4651
- linkMarkerSnapshots.set(linkElement, linkElement.getAttribute("marker-end"));
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
4652
6128
  });
4653
- const deselectNode = () => {
4654
- if (!selectedNodeElement) {
4655
- return;
4656
- }
4657
- const nodeElement = selectedNodeElement;
4658
- nodeElement.style.fill = "";
4659
- nodeElement.style.stroke = "";
4660
- nodeElement.style.strokeWidth = "";
4661
- nodeElement.style.opacity = "";
4662
- nodeElement.style.removeProperty("r");
4663
- root2.selectAll(".link-label.label-selection-pinned").classed("label-selection-pinned", false).interrupt().transition().duration(200).style("opacity", 0).style("pointer-events", "none");
4664
- selectedNodeElement = null;
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;
6274
+ }
6275
+ const value = show[key];
6276
+ if (value === void 0) {
6277
+ return true;
6278
+ }
6279
+ return value;
6280
+ }
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;
6315
+ }
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`);
6325
+ }
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);
6354
+ }
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);
6364
+ }
6365
+ root2 = null;
6366
+ }
6367
+ return { mount, destroy };
6368
+ }
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"');
6381
+ }
6382
+
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);
4665
6494
  };
4666
- const deselectLink = () => {
4667
- if (!selectedLinkElement) {
4668
- return;
4669
- }
4670
- const linkElement = selectedLinkElement;
4671
- linkElement.style.stroke = "";
4672
- linkElement.style.strokeWidth = "";
4673
- linkElement.style.opacity = "";
4674
- const originalMarkerEnd = linkMarkerSnapshots.get(linkElement);
4675
- if (originalMarkerEnd) {
4676
- linkElement.setAttribute("marker-end", originalMarkerEnd);
4677
- } else {
4678
- linkElement.removeAttribute("marker-end");
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);
4679
6517
  }
4680
- selectedLinkElement = null;
4681
6518
  };
4682
- nodeSelection.on("click.select", function(event, node) {
4683
- event.stopPropagation();
4684
- const nodeElement = this;
4685
- if (selectedNodeElement === nodeElement) {
4686
- deselectNode();
4687
- return;
4688
- }
4689
- deselectNode();
4690
- deselectLink();
4691
- selectedNodeElement = nodeElement;
4692
- const nodeStyle = selectionConfig.nodeStyle;
4693
- if (nodeStyle) {
4694
- if (nodeStyle.fill !== void 0) {
4695
- nodeElement.style.fill = nodeStyle.fill;
4696
- }
4697
- if (nodeStyle.stroke !== void 0) {
4698
- nodeElement.style.stroke = nodeStyle.stroke;
4699
- }
4700
- if (nodeStyle.strokeWidth !== void 0) {
4701
- nodeElement.style.strokeWidth = String(nodeStyle.strokeWidth);
4702
- }
4703
- if (nodeStyle.opacity !== void 0) {
4704
- nodeElement.style.opacity = String(nodeStyle.opacity);
4705
- }
4706
- if (nodeStyle.radius !== void 0) {
4707
- nodeElement.style.setProperty("r", String(nodeStyle.radius));
4708
- }
4709
- }
4710
- root2.selectAll(".link-label").filter((item) => {
4711
- if (item.style.label.visibility !== "hover") {
4712
- return false;
4713
- }
4714
- const source = item.link.source;
4715
- const target = item.link.target;
4716
- return source.id === node.id || target.id === node.id;
4717
- }).classed("label-selection-pinned", true).interrupt().transition().duration(200).style("opacity", 1).style("pointer-events", "auto");
4718
- nodeSelectHandlers.forEach((handler) => handler(node, nodeElement));
6519
+ img.onerror = () => {
6520
+ cleanup();
6521
+ reject(new Error("Failed to load SVG image"));
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
4719
6547
  });
4720
- const selectLink = (event, renderableLink, linkElement) => {
4721
- event.stopPropagation();
4722
- if (selectedLinkElement === linkElement) {
4723
- deselectLink();
4724
- return;
4725
- }
4726
- deselectLink();
4727
- deselectNode();
4728
- selectedLinkElement = linkElement;
4729
- const linkStyle = selectionConfig.linkStyle;
4730
- if (linkStyle) {
4731
- if (linkStyle.stroke !== void 0) {
4732
- linkElement.style.stroke = linkStyle.stroke;
4733
- }
4734
- if (linkStyle.strokeWidth !== void 0) {
4735
- linkElement.style.strokeWidth = String(linkStyle.strokeWidth);
4736
- }
4737
- if (linkStyle.opacity !== void 0) {
4738
- linkElement.style.opacity = String(linkStyle.opacity);
4739
- }
4740
- if (linkStyle.stroke !== void 0 && renderableLink.style.arrow.enabled) {
4741
- const selectionMarkerStyle = {
4742
- stroke: linkStyle.stroke,
4743
- arrow: { fill: linkStyle.stroke, size: renderableLink.style.arrow.size }
4744
- };
4745
- const selectionMarkerId = createArrowMarker({ svg: layers.svg, style: selectionMarkerStyle });
4746
- select_default2(linkElement).attr("marker-end", `url(#${selectionMarkerId})`);
4747
- }
6548
+ }
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
6656
+ };
6657
+ }
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
+ });
6680
+ }
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
+ });
4748
6742
  }
4749
- linkSelectHandlers.forEach((handler) => handler(renderableLink.link, linkElement));
4750
- };
4751
- linkSelection.on("click.select", function(event, renderableLink) {
4752
- selectLink(event, renderableLink, this);
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
+ }
6758
+ });
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"
4753
6773
  });
4754
- const linkHitAreaSelection = root2.select('[data-layer="links"]').selectAll("line.link-hit-area").data(linkSelection.data()).join("line").attr("class", "link-hit-area").attr("stroke", "rgba(0,0,0,0)").attr("stroke-width", (item) => item.style.arrow.size * 4).style("pointer-events", "stroke").style("cursor", "pointer").attr("opacity", 0);
4755
- simulation.on("tick.hitarea", () => {
4756
- 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);
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
+ }
6823
+ });
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"
6839
+ });
6840
+ }
6841
+ }
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"
4757
6856
  });
4758
- linkHitAreaSelection.on("click.select", function(event, renderableLink) {
4759
- const visibleLinkNode = linkSelection.filter((d) => d === renderableLink).node();
4760
- if (visibleLinkNode) {
4761
- selectLink(event, renderableLink, visibleLinkNode);
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
+ });
4762
6879
  }
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"
4763
6896
  });
4764
- select_default2(layers.svg).on("click.deselect", () => {
4765
- deselectNode();
4766
- deselectLink();
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"
4767
6904
  });
4768
6905
  }
4769
- if (config.controls?.enabled) {
4770
- controls = createGraphControls(
4771
- layers.overlay,
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();
6954
+ }
6955
+ }).catch((error) => {
6956
+ console.error("[Polly Graph] Render failed:", error);
6957
+ });
6958
+ } catch (error) {
6959
+ console.error("[Polly Graph] Render failed:", error);
6960
+ }
6961
+ }
6962
+ function setupAdditionalComponents() {
6963
+ if (config.controls?.enabled && graphManager.layers) {
6964
+ graphManager.controls = createGraphControls(
6965
+ graphManager.layers.overlay,
4772
6966
  { zoomIn, zoomOut, resetView, fitView },
4773
6967
  config.controls
4774
6968
  );
4775
- 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);
6978
+ }
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;
4776
6997
  }
4777
- if (config.legend?.enabled) {
4778
- legendCleanup = createGraphLegend(layers.overlay, config.legend);
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;
4779
7005
  }
7006
+ const svg = graphManager.svgElement;
7007
+ select_default2(svg).transition().duration(400).call(graphManager.zoomBehavior.scaleBy, 1 / 1.5);
4780
7008
  }
4781
7009
  function resetView() {
4782
- if (!zoomBehavior || !svgElement) {
7010
+ if (!graphManager.zoomBehavior || !graphManager.svgElement) {
7011
+ console.warn("[Polly Graph] Zoom behavior not available");
4783
7012
  return;
4784
7013
  }
4785
- 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);
4786
7016
  }
4787
7017
  function fitView() {
4788
- 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");
4789
7020
  return;
4790
7021
  }
4791
- const bounds = rootGroup.getBBox();
4792
- 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) {
4793
7037
  return;
4794
7038
  }
4795
- const scale = Math.min(dimensions.width / bounds.width, dimensions.height / bounds.height) * 0.9;
4796
- const translateX = (dimensions.width - bounds.width * scale) / 2 - bounds.x * scale;
4797
- const translateY = (dimensions.height - bounds.height * scale) / 2 - bounds.y * scale;
4798
- const transform2 = identity2.translate(translateX, translateY).scale(scale);
4799
- 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
+ }
4800
7046
  }
4801
- function zoomIn() {
4802
- 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");
4803
7050
  return;
4804
7051
  }
4805
- select_default2(svgElement).transition().call(zoomBehavior.scaleBy, 1.2);
4806
- }
4807
- function zoomOut() {
4808
- 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) {
4809
7067
  return;
4810
7068
  }
4811
- 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
+ }
4812
7076
  }
4813
- async function exportGraph(fileName) {
4814
- fitView();
4815
- await new Promise((resolve) => setTimeout(resolve, 500));
4816
- await captureAndDownloadGraph(config.container, {
4817
- 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",
4818
7084
  pixelRatio: 2
4819
7085
  });
4820
7086
  }
4821
- function destroy() {
4822
- if (fitViewTimer) {
4823
- clearTimeout(fitViewTimer);
4824
- fitViewTimer = null;
4825
- }
4826
- if (cleanupResize) {
4827
- cleanupResize();
4828
- cleanupResize = null;
7087
+ function clearSelection() {
7088
+ if (graphManager.selectionManager) {
7089
+ graphManager.selectionManager.clearSelection();
4829
7090
  }
4830
- if (cleanupZoom) {
4831
- cleanupZoom();
4832
- cleanupZoom = null;
4833
- }
4834
- if (tooltipBinding) {
4835
- tooltipBinding.destroy();
4836
- tooltipBinding = null;
4837
- }
4838
- if (simulation) {
4839
- simulation.stop();
4840
- simulation = null;
4841
- }
4842
- if (controls) {
4843
- controls.destroy();
4844
- controls = null;
4845
- }
4846
- if (legendCleanup) {
4847
- legendCleanup();
4848
- legendCleanup = null;
4849
- }
4850
- rootGroup = null;
4851
- svgElement = null;
4852
- zoomBehavior = null;
4853
- while (config.container.firstChild) {
4854
- 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
+ };
4855
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);
7116
+ }
7117
+ function destroy() {
7118
+ graphManager.destroy();
4856
7119
  }
4857
- return { render, zoomIn, zoomOut, resetView, fitView, destroy, exportGraph, on, off };
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
+ };
4858
7135
  }
4859
7136
  // Annotate the CommonJS export names for ESM import in node:
4860
7137
  0 && (module.exports = {
7138
+ ErrorHandler,
7139
+ GraphError,
7140
+ GraphValidationError,
7141
+ GraphValidator,
7142
+ SelectionManager,
7143
+ TypedGraphEventEmitter,
4861
7144
  createGraph
4862
7145
  });