polly-graph 0.1.7 → 0.1.9

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