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