polly-graph 0.1.13 → 0.1.15

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.
Files changed (3) hide show
  1. package/dist/index.cjs +342 -28
  2. package/dist/index.js +342 -28
  3. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -4868,10 +4868,7 @@ function renderLinkLabels(params, links) {
4868
4868
  const labelSelection = params.root.select('[data-layer="link-labels"]').selectAll(".link-label").data(renderableLinks, (item) => getLinkKey2(item.link)).join("g").attr("class", "link-label").style("opacity", (item) => {
4869
4869
  const visibility = item.style.label.visibility ?? "always";
4870
4870
  return visibility === "always" ? 1 : 0;
4871
- }).style("pointer-events", (item) => {
4872
- const visibility = item.style.label.visibility ?? "always";
4873
- return visibility === "always" ? "auto" : "none";
4874
- }).style("cursor", "pointer");
4871
+ }).style("pointer-events", "none").style("cursor", "pointer");
4875
4872
  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);
4876
4873
  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 ?? "");
4877
4874
  textSelection.each(function(item) {
@@ -5117,34 +5114,294 @@ function createDragBehavior(simulation, onDragStart, canvasBounds) {
5117
5114
  });
5118
5115
  }
5119
5116
 
5117
+ // src/utils/node-style-manager.ts
5118
+ var NodeStyleManager = class {
5119
+ styleStates = /* @__PURE__ */ new Map();
5120
+ /**
5121
+ * Initialize a node's style state by capturing its original styles
5122
+ */
5123
+ initializeNode(element, node) {
5124
+ if (this.styleStates.has(element)) {
5125
+ return;
5126
+ }
5127
+ const domBackup = {
5128
+ stroke: element.getAttribute("stroke"),
5129
+ strokeWidth: element.getAttribute("stroke-width"),
5130
+ opacity: element.getAttribute("opacity"),
5131
+ fill: element.getAttribute("fill")
5132
+ };
5133
+ const originalStyle = {
5134
+ stroke: node.style?.stroke || void 0,
5135
+ strokeWidth: node.style?.strokeWidth || void 0,
5136
+ opacity: node.style?.opacity || void 0,
5137
+ fill: node.style?.fill || void 0
5138
+ };
5139
+ this.styleStates.set(element, {
5140
+ original: originalStyle,
5141
+ current: { ...originalStyle },
5142
+ domBackup
5143
+ });
5144
+ }
5145
+ /**
5146
+ * Apply temporary styles (e.g., hover effects) that will be reset later
5147
+ */
5148
+ applyTemporaryStyles(element, styles) {
5149
+ this.applyStylesToDOM(element, styles);
5150
+ }
5151
+ /**
5152
+ * Apply permanent styles that become the new base styles
5153
+ */
5154
+ applyPermanentStyles(element, styles) {
5155
+ const state = this.styleStates.get(element);
5156
+ if (!state) {
5157
+ console.warn("[NodeStyleManager] Node not initialized, cannot apply permanent styles");
5158
+ return;
5159
+ }
5160
+ this.applyStylesToDOM(element, styles);
5161
+ Object.assign(state.current, styles);
5162
+ }
5163
+ /**
5164
+ * Reset node to its current base styles (removes temporary styles)
5165
+ */
5166
+ resetToBase(element) {
5167
+ const state = this.styleStates.get(element);
5168
+ if (!state) {
5169
+ this.clearAllStyles(element);
5170
+ return;
5171
+ }
5172
+ this.clearAllStyles(element);
5173
+ this.applyStylesToDOM(element, state.current);
5174
+ }
5175
+ /**
5176
+ * Reset node to its original styles (as captured during initialization)
5177
+ */
5178
+ resetToOriginal(element) {
5179
+ const state = this.styleStates.get(element);
5180
+ if (!state) {
5181
+ this.clearAllStyles(element);
5182
+ return;
5183
+ }
5184
+ this.clearAllStyles(element);
5185
+ this.restoreOriginalDOM(element, state.domBackup);
5186
+ state.current = { ...state.original };
5187
+ }
5188
+ /**
5189
+ * Check if node is in a specific state (selected, hovered, etc.)
5190
+ */
5191
+ hasState(element, stateName) {
5192
+ return element.dataset[stateName] === "true";
5193
+ }
5194
+ /**
5195
+ * Set state marker on node
5196
+ */
5197
+ setState(element, stateName, value) {
5198
+ if (value) {
5199
+ element.dataset[stateName] = "true";
5200
+ } else {
5201
+ delete element.dataset[stateName];
5202
+ }
5203
+ }
5204
+ /**
5205
+ * Get the original styles for a node
5206
+ */
5207
+ getOriginalStyles(element) {
5208
+ const state = this.styleStates.get(element);
5209
+ return state ? { ...state.original } : null;
5210
+ }
5211
+ /**
5212
+ * Get the current base styles for a node
5213
+ */
5214
+ getCurrentStyles(element) {
5215
+ const state = this.styleStates.get(element);
5216
+ return state ? { ...state.current } : null;
5217
+ }
5218
+ /**
5219
+ * Remove a node from management (cleanup)
5220
+ */
5221
+ removeNode(element) {
5222
+ this.styleStates.delete(element);
5223
+ }
5224
+ /**
5225
+ * Clear all managed nodes (for cleanup)
5226
+ */
5227
+ clear() {
5228
+ this.styleStates.clear();
5229
+ }
5230
+ /**
5231
+ * Private: Apply styles to DOM element
5232
+ */
5233
+ applyStylesToDOM(element, styles) {
5234
+ if (styles.stroke !== void 0) {
5235
+ if (styles.stroke === null || styles.stroke === "") {
5236
+ element.removeAttribute("stroke");
5237
+ element.style.stroke = "";
5238
+ } else {
5239
+ element.style.stroke = styles.stroke;
5240
+ }
5241
+ }
5242
+ if (styles.strokeWidth !== void 0) {
5243
+ if (styles.strokeWidth === null || styles.strokeWidth === 0) {
5244
+ element.removeAttribute("stroke-width");
5245
+ element.style.strokeWidth = "";
5246
+ } else {
5247
+ element.style.strokeWidth = String(styles.strokeWidth);
5248
+ }
5249
+ }
5250
+ if (styles.opacity !== void 0) {
5251
+ if (styles.opacity === null || styles.opacity === 1) {
5252
+ element.removeAttribute("opacity");
5253
+ element.style.opacity = "";
5254
+ } else {
5255
+ element.style.opacity = String(styles.opacity);
5256
+ }
5257
+ }
5258
+ if (styles.fill !== void 0) {
5259
+ if (styles.fill === null || styles.fill === "") {
5260
+ element.removeAttribute("fill");
5261
+ element.style.fill = "";
5262
+ } else {
5263
+ element.style.fill = styles.fill;
5264
+ }
5265
+ }
5266
+ if (styles.radius !== void 0) {
5267
+ if (styles.radius === null || styles.radius === 0) {
5268
+ element.removeAttribute("r");
5269
+ element.style.removeProperty("r");
5270
+ } else {
5271
+ element.setAttribute("r", String(styles.radius));
5272
+ }
5273
+ }
5274
+ }
5275
+ /**
5276
+ * Private: Clear all inline styles and remove hover-related attributes
5277
+ */
5278
+ clearAllStyles(element) {
5279
+ element.style.stroke = "";
5280
+ element.style.strokeWidth = "";
5281
+ element.style.opacity = "";
5282
+ element.style.fill = "";
5283
+ element.style.removeProperty("r");
5284
+ this.removeIfHoverAttribute(element, "stroke");
5285
+ this.removeIfHoverAttribute(element, "stroke-width");
5286
+ this.removeIfHoverAttribute(element, "opacity");
5287
+ }
5288
+ /**
5289
+ * Private: Restore original DOM attributes
5290
+ */
5291
+ restoreOriginalDOM(element, domBackup) {
5292
+ if (domBackup.stroke !== void 0) {
5293
+ if (domBackup.stroke === null) {
5294
+ element.removeAttribute("stroke");
5295
+ } else {
5296
+ element.setAttribute("stroke", domBackup.stroke);
5297
+ }
5298
+ }
5299
+ if (domBackup.strokeWidth !== void 0) {
5300
+ if (domBackup.strokeWidth === null) {
5301
+ element.removeAttribute("stroke-width");
5302
+ } else {
5303
+ element.setAttribute("stroke-width", domBackup.strokeWidth);
5304
+ }
5305
+ }
5306
+ if (domBackup.opacity !== void 0) {
5307
+ if (domBackup.opacity === null) {
5308
+ element.removeAttribute("opacity");
5309
+ } else {
5310
+ element.setAttribute("opacity", domBackup.opacity);
5311
+ }
5312
+ }
5313
+ if (domBackup.fill !== void 0) {
5314
+ if (domBackup.fill === null) {
5315
+ element.removeAttribute("fill");
5316
+ } else {
5317
+ element.setAttribute("fill", domBackup.fill);
5318
+ }
5319
+ }
5320
+ }
5321
+ /**
5322
+ * Private: Remove attribute only if it looks like it was set by hover/interaction
5323
+ */
5324
+ removeIfHoverAttribute(element, attr) {
5325
+ const value = element.getAttribute(attr);
5326
+ if (!value) return;
5327
+ const hoverPatterns = {
5328
+ "stroke": ["#6366f1", "#8b5cf6", "#3b82f6", "#ffffff", "#fff", "white"],
5329
+ // Common hover stroke colors
5330
+ "stroke-width": ["2", "2.5", "3", "4"],
5331
+ // Common hover stroke widths
5332
+ "opacity": ["0.8", "0.9", "0.7"]
5333
+ // Common hover opacity values
5334
+ };
5335
+ const patterns = hoverPatterns[attr];
5336
+ if (patterns && patterns.includes(value)) {
5337
+ element.removeAttribute(attr);
5338
+ }
5339
+ }
5340
+ };
5341
+ var nodeStyleManager = new NodeStyleManager();
5342
+ function applyHoverStyles(element, node, hoverStyle) {
5343
+ if (nodeStyleManager.hasState(element, "selected")) {
5344
+ return;
5345
+ }
5346
+ nodeStyleManager.initializeNode(element, node);
5347
+ nodeStyleManager.applyTemporaryStyles(element, hoverStyle);
5348
+ nodeStyleManager.setState(element, "hovered", true);
5349
+ }
5350
+ function removeHoverStyles(element, node) {
5351
+ if (nodeStyleManager.hasState(element, "selected")) {
5352
+ return;
5353
+ }
5354
+ nodeStyleManager.initializeNode(element, node);
5355
+ nodeStyleManager.resetToBase(element);
5356
+ nodeStyleManager.setState(element, "hovered", false);
5357
+ }
5358
+
5120
5359
  // src/interactions/create-node-hover.ts
5121
- function createNodeHover(nodeSelection, hoverStyle) {
5360
+ var currentHoveredNode = null;
5361
+ var hoverTimerManager = new TimerManager();
5362
+ function createNodeHover(nodeSelection, hoverStyle, options) {
5122
5363
  const firstNode = nodeSelection.node();
5123
5364
  if (!firstNode) return;
5365
+ const {
5366
+ enableDebouncing = false,
5367
+ enterDelay = 16,
5368
+ // ~1 frame at 60fps
5369
+ leaveDelay = 50
5370
+ // Longer delay for smoother transitions
5371
+ } = options || {};
5124
5372
  if (hoverStyle) {
5125
- nodeSelection.on("mouseenter.hover", function(_event, _node) {
5373
+ nodeSelection.on("mouseenter.hover", function(_event, node) {
5126
5374
  const circle = this;
5127
- if (circle.dataset.selected === "true") {
5128
- return;
5129
- }
5130
- if (hoverStyle.stroke !== void 0) {
5131
- circle.style.stroke = hoverStyle.stroke;
5132
- }
5133
- if (hoverStyle.strokeWidth !== void 0) {
5134
- circle.style.strokeWidth = String(hoverStyle.strokeWidth);
5135
- }
5136
- if (hoverStyle.opacity !== void 0) {
5137
- circle.style.opacity = String(hoverStyle.opacity);
5375
+ const applyHover = () => {
5376
+ if (currentHoveredNode && currentHoveredNode.element !== circle) {
5377
+ removeHoverStyles(currentHoveredNode.element, currentHoveredNode.node);
5378
+ clearAllHoverLayers();
5379
+ }
5380
+ currentHoveredNode = { element: circle, node };
5381
+ applyHoverStyles(circle, node, hoverStyle);
5382
+ };
5383
+ if (enableDebouncing) {
5384
+ hoverTimerManager.clearTimer("hover-enter");
5385
+ hoverTimerManager.clearTimer("hover-leave");
5386
+ hoverTimerManager.debounce("hover-enter", applyHover, enterDelay);
5387
+ } else {
5388
+ applyHover();
5138
5389
  }
5139
- }).on("mouseleave.hover", function(_event, _node) {
5390
+ }).on("mouseleave.hover", function(_event, node) {
5140
5391
  const circle = this;
5141
- clearAllHoverLayers();
5142
- if (circle.dataset.selected === "true") {
5143
- return;
5392
+ const removeHover = () => {
5393
+ if (currentHoveredNode?.element === circle) {
5394
+ currentHoveredNode = null;
5395
+ removeHoverStyles(circle, node);
5396
+ clearAllHoverLayers();
5397
+ }
5398
+ };
5399
+ if (enableDebouncing) {
5400
+ hoverTimerManager.clearTimer("hover-enter");
5401
+ hoverTimerManager.debounce("hover-leave", removeHover, leaveDelay);
5402
+ } else {
5403
+ removeHover();
5144
5404
  }
5145
- circle.style.stroke = "";
5146
- circle.style.strokeWidth = "";
5147
- circle.style.opacity = "";
5148
5405
  });
5149
5406
  }
5150
5407
  const svgElement = firstNode.ownerSVGElement;
@@ -5198,6 +5455,9 @@ function createNodeHover(nodeSelection, hoverStyle) {
5198
5455
  if (hoveredNodeElement.dataset.selected === "true") {
5199
5456
  return;
5200
5457
  }
5458
+ if (!currentHoveredNode || currentHoveredNode.element !== hoveredNodeElement) {
5459
+ return;
5460
+ }
5201
5461
  clearAllHoverLayers();
5202
5462
  const hoverNodesLayer = root2.select('[data-layer="hover-nodes"]').node();
5203
5463
  if (hoverNodesLayer) {
@@ -5244,7 +5504,10 @@ function createNodeHover(nodeSelection, hoverStyle) {
5244
5504
  }
5245
5505
  });
5246
5506
  }).on("mouseleave.links", function(_event, _hoveredNode) {
5247
- clearAllHoverLayers();
5507
+ const hoveredNodeElement = this;
5508
+ if (currentHoveredNode?.element === hoveredNodeElement) {
5509
+ clearAllHoverLayers();
5510
+ }
5248
5511
  });
5249
5512
  }
5250
5513
 
@@ -5313,7 +5576,7 @@ function createLinkHover(linkSelection, hoverStyle) {
5313
5576
  }
5314
5577
  }
5315
5578
  const labelSelection2 = root2.select('[data-layer="link-labels"]').selectAll(".link-label");
5316
- labelSelection2.filter((item) => item.link === renderableLink.link && item.style.label.visibility === "hover").style("opacity", 1).style("pointer-events", "auto");
5579
+ labelSelection2.filter((item) => item.link === renderableLink.link && item.style.label.visibility === "hover").style("opacity", 1);
5317
5580
  }).on("mouseleave.hover", function(_event, renderableLink) {
5318
5581
  const hoveredElement = this;
5319
5582
  let targetLinkElement;
@@ -5610,7 +5873,47 @@ function getDefaultContent(node) {
5610
5873
 
5611
5874
  // src/utils/node-link-selection.utils.ts
5612
5875
  function createLinkHitArea(root2, linkSelection) {
5613
- 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);
5876
+ return root2.select('[data-layer="links"]').selectAll("rect.link-hit-area").data(linkSelection.data()).join("rect").attr("class", "link-hit-area").attr("fill", "rgba(0,0,0,0)").style("pointer-events", "fill").style("cursor", "pointer").attr("opacity", 0).each(function(item) {
5877
+ const rectElement = this;
5878
+ updateHitAreaDimensions(rectElement, item, root2);
5879
+ });
5880
+ }
5881
+ function updateHitAreaDimensions(rectElement, item, root2) {
5882
+ const source = item.link.source;
5883
+ const target = item.link.target;
5884
+ if (!source.x || !source.y || !target.x || !target.y) return;
5885
+ const midX = (source.x + target.x) / 2;
5886
+ const midY = (source.y + target.y) / 2;
5887
+ const linkPadding = Math.max(item.style.strokeWidth || 2, item.style.arrow.size) * 2;
5888
+ let width = linkPadding;
5889
+ let height = linkPadding;
5890
+ if (item.link.label) {
5891
+ const labelElement = root2.select('[data-layer="link-labels"]').selectAll(".link-label").filter((labelItem) => labelItem.link === item.link).node();
5892
+ if (labelElement) {
5893
+ try {
5894
+ const textElement = labelElement.querySelector("text");
5895
+ const rectElement2 = labelElement.querySelector("rect");
5896
+ if (textElement && rectElement2) {
5897
+ const bbox = textElement.getBBox();
5898
+ const labelWidth = bbox.width + item.style.label.paddingX * 2;
5899
+ const labelHeight = bbox.height + item.style.label.paddingY * 2;
5900
+ width = Math.max(width, labelWidth + 10);
5901
+ height = Math.max(height, labelHeight + 10);
5902
+ }
5903
+ } catch {
5904
+ const text = item.link.label ?? "";
5905
+ const fontSize = item.style.label.fontSize;
5906
+ const estimatedWidth = text.length * fontSize * 0.6 + item.style.label.paddingX * 2 + 10;
5907
+ const estimatedHeight = fontSize + item.style.label.paddingY * 2 + 10;
5908
+ width = Math.max(width, estimatedWidth);
5909
+ height = Math.max(height, estimatedHeight);
5910
+ }
5911
+ }
5912
+ }
5913
+ rectElement.setAttribute("x", String(midX - width / 2));
5914
+ rectElement.setAttribute("y", String(midY - height / 2));
5915
+ rectElement.setAttribute("width", String(width));
5916
+ rectElement.setAttribute("height", String(height));
5614
5917
  }
5615
5918
 
5616
5919
  // src/utils/get-link-target-point.ts
@@ -6322,7 +6625,18 @@ var InteractionManager = class {
6322
6625
  }
6323
6626
  if (this.manager.simulation) {
6324
6627
  this.manager.simulation.on("tick.hitarea", () => {
6325
- 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);
6628
+ linkHitAreaSelection.each(function(item) {
6629
+ const source = item.link.source;
6630
+ const target = item.link.target;
6631
+ if (!source.x || !source.y || !target.x || !target.y) return;
6632
+ const rectElement = this;
6633
+ const midX = (source.x + target.x) / 2;
6634
+ const midY = (source.y + target.y) / 2;
6635
+ const width = parseFloat(rectElement.getAttribute("width") || "20");
6636
+ const height = parseFloat(rectElement.getAttribute("height") || "20");
6637
+ rectElement.setAttribute("x", String(midX - width / 2));
6638
+ rectElement.setAttribute("y", String(midY - height / 2));
6639
+ });
6326
6640
  });
6327
6641
  }
6328
6642
  if (this.manager.selectionManager) {
package/dist/index.js CHANGED
@@ -4836,10 +4836,7 @@ function renderLinkLabels(params, links) {
4836
4836
  const labelSelection = params.root.select('[data-layer="link-labels"]').selectAll(".link-label").data(renderableLinks, (item) => getLinkKey2(item.link)).join("g").attr("class", "link-label").style("opacity", (item) => {
4837
4837
  const visibility = item.style.label.visibility ?? "always";
4838
4838
  return visibility === "always" ? 1 : 0;
4839
- }).style("pointer-events", (item) => {
4840
- const visibility = item.style.label.visibility ?? "always";
4841
- return visibility === "always" ? "auto" : "none";
4842
- }).style("cursor", "pointer");
4839
+ }).style("pointer-events", "none").style("cursor", "pointer");
4843
4840
  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);
4844
4841
  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 ?? "");
4845
4842
  textSelection.each(function(item) {
@@ -5085,34 +5082,294 @@ function createDragBehavior(simulation, onDragStart, canvasBounds) {
5085
5082
  });
5086
5083
  }
5087
5084
 
5085
+ // src/utils/node-style-manager.ts
5086
+ var NodeStyleManager = class {
5087
+ styleStates = /* @__PURE__ */ new Map();
5088
+ /**
5089
+ * Initialize a node's style state by capturing its original styles
5090
+ */
5091
+ initializeNode(element, node) {
5092
+ if (this.styleStates.has(element)) {
5093
+ return;
5094
+ }
5095
+ const domBackup = {
5096
+ stroke: element.getAttribute("stroke"),
5097
+ strokeWidth: element.getAttribute("stroke-width"),
5098
+ opacity: element.getAttribute("opacity"),
5099
+ fill: element.getAttribute("fill")
5100
+ };
5101
+ const originalStyle = {
5102
+ stroke: node.style?.stroke || void 0,
5103
+ strokeWidth: node.style?.strokeWidth || void 0,
5104
+ opacity: node.style?.opacity || void 0,
5105
+ fill: node.style?.fill || void 0
5106
+ };
5107
+ this.styleStates.set(element, {
5108
+ original: originalStyle,
5109
+ current: { ...originalStyle },
5110
+ domBackup
5111
+ });
5112
+ }
5113
+ /**
5114
+ * Apply temporary styles (e.g., hover effects) that will be reset later
5115
+ */
5116
+ applyTemporaryStyles(element, styles) {
5117
+ this.applyStylesToDOM(element, styles);
5118
+ }
5119
+ /**
5120
+ * Apply permanent styles that become the new base styles
5121
+ */
5122
+ applyPermanentStyles(element, styles) {
5123
+ const state = this.styleStates.get(element);
5124
+ if (!state) {
5125
+ console.warn("[NodeStyleManager] Node not initialized, cannot apply permanent styles");
5126
+ return;
5127
+ }
5128
+ this.applyStylesToDOM(element, styles);
5129
+ Object.assign(state.current, styles);
5130
+ }
5131
+ /**
5132
+ * Reset node to its current base styles (removes temporary styles)
5133
+ */
5134
+ resetToBase(element) {
5135
+ const state = this.styleStates.get(element);
5136
+ if (!state) {
5137
+ this.clearAllStyles(element);
5138
+ return;
5139
+ }
5140
+ this.clearAllStyles(element);
5141
+ this.applyStylesToDOM(element, state.current);
5142
+ }
5143
+ /**
5144
+ * Reset node to its original styles (as captured during initialization)
5145
+ */
5146
+ resetToOriginal(element) {
5147
+ const state = this.styleStates.get(element);
5148
+ if (!state) {
5149
+ this.clearAllStyles(element);
5150
+ return;
5151
+ }
5152
+ this.clearAllStyles(element);
5153
+ this.restoreOriginalDOM(element, state.domBackup);
5154
+ state.current = { ...state.original };
5155
+ }
5156
+ /**
5157
+ * Check if node is in a specific state (selected, hovered, etc.)
5158
+ */
5159
+ hasState(element, stateName) {
5160
+ return element.dataset[stateName] === "true";
5161
+ }
5162
+ /**
5163
+ * Set state marker on node
5164
+ */
5165
+ setState(element, stateName, value) {
5166
+ if (value) {
5167
+ element.dataset[stateName] = "true";
5168
+ } else {
5169
+ delete element.dataset[stateName];
5170
+ }
5171
+ }
5172
+ /**
5173
+ * Get the original styles for a node
5174
+ */
5175
+ getOriginalStyles(element) {
5176
+ const state = this.styleStates.get(element);
5177
+ return state ? { ...state.original } : null;
5178
+ }
5179
+ /**
5180
+ * Get the current base styles for a node
5181
+ */
5182
+ getCurrentStyles(element) {
5183
+ const state = this.styleStates.get(element);
5184
+ return state ? { ...state.current } : null;
5185
+ }
5186
+ /**
5187
+ * Remove a node from management (cleanup)
5188
+ */
5189
+ removeNode(element) {
5190
+ this.styleStates.delete(element);
5191
+ }
5192
+ /**
5193
+ * Clear all managed nodes (for cleanup)
5194
+ */
5195
+ clear() {
5196
+ this.styleStates.clear();
5197
+ }
5198
+ /**
5199
+ * Private: Apply styles to DOM element
5200
+ */
5201
+ applyStylesToDOM(element, styles) {
5202
+ if (styles.stroke !== void 0) {
5203
+ if (styles.stroke === null || styles.stroke === "") {
5204
+ element.removeAttribute("stroke");
5205
+ element.style.stroke = "";
5206
+ } else {
5207
+ element.style.stroke = styles.stroke;
5208
+ }
5209
+ }
5210
+ if (styles.strokeWidth !== void 0) {
5211
+ if (styles.strokeWidth === null || styles.strokeWidth === 0) {
5212
+ element.removeAttribute("stroke-width");
5213
+ element.style.strokeWidth = "";
5214
+ } else {
5215
+ element.style.strokeWidth = String(styles.strokeWidth);
5216
+ }
5217
+ }
5218
+ if (styles.opacity !== void 0) {
5219
+ if (styles.opacity === null || styles.opacity === 1) {
5220
+ element.removeAttribute("opacity");
5221
+ element.style.opacity = "";
5222
+ } else {
5223
+ element.style.opacity = String(styles.opacity);
5224
+ }
5225
+ }
5226
+ if (styles.fill !== void 0) {
5227
+ if (styles.fill === null || styles.fill === "") {
5228
+ element.removeAttribute("fill");
5229
+ element.style.fill = "";
5230
+ } else {
5231
+ element.style.fill = styles.fill;
5232
+ }
5233
+ }
5234
+ if (styles.radius !== void 0) {
5235
+ if (styles.radius === null || styles.radius === 0) {
5236
+ element.removeAttribute("r");
5237
+ element.style.removeProperty("r");
5238
+ } else {
5239
+ element.setAttribute("r", String(styles.radius));
5240
+ }
5241
+ }
5242
+ }
5243
+ /**
5244
+ * Private: Clear all inline styles and remove hover-related attributes
5245
+ */
5246
+ clearAllStyles(element) {
5247
+ element.style.stroke = "";
5248
+ element.style.strokeWidth = "";
5249
+ element.style.opacity = "";
5250
+ element.style.fill = "";
5251
+ element.style.removeProperty("r");
5252
+ this.removeIfHoverAttribute(element, "stroke");
5253
+ this.removeIfHoverAttribute(element, "stroke-width");
5254
+ this.removeIfHoverAttribute(element, "opacity");
5255
+ }
5256
+ /**
5257
+ * Private: Restore original DOM attributes
5258
+ */
5259
+ restoreOriginalDOM(element, domBackup) {
5260
+ if (domBackup.stroke !== void 0) {
5261
+ if (domBackup.stroke === null) {
5262
+ element.removeAttribute("stroke");
5263
+ } else {
5264
+ element.setAttribute("stroke", domBackup.stroke);
5265
+ }
5266
+ }
5267
+ if (domBackup.strokeWidth !== void 0) {
5268
+ if (domBackup.strokeWidth === null) {
5269
+ element.removeAttribute("stroke-width");
5270
+ } else {
5271
+ element.setAttribute("stroke-width", domBackup.strokeWidth);
5272
+ }
5273
+ }
5274
+ if (domBackup.opacity !== void 0) {
5275
+ if (domBackup.opacity === null) {
5276
+ element.removeAttribute("opacity");
5277
+ } else {
5278
+ element.setAttribute("opacity", domBackup.opacity);
5279
+ }
5280
+ }
5281
+ if (domBackup.fill !== void 0) {
5282
+ if (domBackup.fill === null) {
5283
+ element.removeAttribute("fill");
5284
+ } else {
5285
+ element.setAttribute("fill", domBackup.fill);
5286
+ }
5287
+ }
5288
+ }
5289
+ /**
5290
+ * Private: Remove attribute only if it looks like it was set by hover/interaction
5291
+ */
5292
+ removeIfHoverAttribute(element, attr) {
5293
+ const value = element.getAttribute(attr);
5294
+ if (!value) return;
5295
+ const hoverPatterns = {
5296
+ "stroke": ["#6366f1", "#8b5cf6", "#3b82f6", "#ffffff", "#fff", "white"],
5297
+ // Common hover stroke colors
5298
+ "stroke-width": ["2", "2.5", "3", "4"],
5299
+ // Common hover stroke widths
5300
+ "opacity": ["0.8", "0.9", "0.7"]
5301
+ // Common hover opacity values
5302
+ };
5303
+ const patterns = hoverPatterns[attr];
5304
+ if (patterns && patterns.includes(value)) {
5305
+ element.removeAttribute(attr);
5306
+ }
5307
+ }
5308
+ };
5309
+ var nodeStyleManager = new NodeStyleManager();
5310
+ function applyHoverStyles(element, node, hoverStyle) {
5311
+ if (nodeStyleManager.hasState(element, "selected")) {
5312
+ return;
5313
+ }
5314
+ nodeStyleManager.initializeNode(element, node);
5315
+ nodeStyleManager.applyTemporaryStyles(element, hoverStyle);
5316
+ nodeStyleManager.setState(element, "hovered", true);
5317
+ }
5318
+ function removeHoverStyles(element, node) {
5319
+ if (nodeStyleManager.hasState(element, "selected")) {
5320
+ return;
5321
+ }
5322
+ nodeStyleManager.initializeNode(element, node);
5323
+ nodeStyleManager.resetToBase(element);
5324
+ nodeStyleManager.setState(element, "hovered", false);
5325
+ }
5326
+
5088
5327
  // src/interactions/create-node-hover.ts
5089
- function createNodeHover(nodeSelection, hoverStyle) {
5328
+ var currentHoveredNode = null;
5329
+ var hoverTimerManager = new TimerManager();
5330
+ function createNodeHover(nodeSelection, hoverStyle, options) {
5090
5331
  const firstNode = nodeSelection.node();
5091
5332
  if (!firstNode) return;
5333
+ const {
5334
+ enableDebouncing = false,
5335
+ enterDelay = 16,
5336
+ // ~1 frame at 60fps
5337
+ leaveDelay = 50
5338
+ // Longer delay for smoother transitions
5339
+ } = options || {};
5092
5340
  if (hoverStyle) {
5093
- nodeSelection.on("mouseenter.hover", function(_event, _node) {
5341
+ nodeSelection.on("mouseenter.hover", function(_event, node) {
5094
5342
  const circle = this;
5095
- if (circle.dataset.selected === "true") {
5096
- return;
5097
- }
5098
- if (hoverStyle.stroke !== void 0) {
5099
- circle.style.stroke = hoverStyle.stroke;
5100
- }
5101
- if (hoverStyle.strokeWidth !== void 0) {
5102
- circle.style.strokeWidth = String(hoverStyle.strokeWidth);
5103
- }
5104
- if (hoverStyle.opacity !== void 0) {
5105
- circle.style.opacity = String(hoverStyle.opacity);
5343
+ const applyHover = () => {
5344
+ if (currentHoveredNode && currentHoveredNode.element !== circle) {
5345
+ removeHoverStyles(currentHoveredNode.element, currentHoveredNode.node);
5346
+ clearAllHoverLayers();
5347
+ }
5348
+ currentHoveredNode = { element: circle, node };
5349
+ applyHoverStyles(circle, node, hoverStyle);
5350
+ };
5351
+ if (enableDebouncing) {
5352
+ hoverTimerManager.clearTimer("hover-enter");
5353
+ hoverTimerManager.clearTimer("hover-leave");
5354
+ hoverTimerManager.debounce("hover-enter", applyHover, enterDelay);
5355
+ } else {
5356
+ applyHover();
5106
5357
  }
5107
- }).on("mouseleave.hover", function(_event, _node) {
5358
+ }).on("mouseleave.hover", function(_event, node) {
5108
5359
  const circle = this;
5109
- clearAllHoverLayers();
5110
- if (circle.dataset.selected === "true") {
5111
- return;
5360
+ const removeHover = () => {
5361
+ if (currentHoveredNode?.element === circle) {
5362
+ currentHoveredNode = null;
5363
+ removeHoverStyles(circle, node);
5364
+ clearAllHoverLayers();
5365
+ }
5366
+ };
5367
+ if (enableDebouncing) {
5368
+ hoverTimerManager.clearTimer("hover-enter");
5369
+ hoverTimerManager.debounce("hover-leave", removeHover, leaveDelay);
5370
+ } else {
5371
+ removeHover();
5112
5372
  }
5113
- circle.style.stroke = "";
5114
- circle.style.strokeWidth = "";
5115
- circle.style.opacity = "";
5116
5373
  });
5117
5374
  }
5118
5375
  const svgElement = firstNode.ownerSVGElement;
@@ -5166,6 +5423,9 @@ function createNodeHover(nodeSelection, hoverStyle) {
5166
5423
  if (hoveredNodeElement.dataset.selected === "true") {
5167
5424
  return;
5168
5425
  }
5426
+ if (!currentHoveredNode || currentHoveredNode.element !== hoveredNodeElement) {
5427
+ return;
5428
+ }
5169
5429
  clearAllHoverLayers();
5170
5430
  const hoverNodesLayer = root2.select('[data-layer="hover-nodes"]').node();
5171
5431
  if (hoverNodesLayer) {
@@ -5212,7 +5472,10 @@ function createNodeHover(nodeSelection, hoverStyle) {
5212
5472
  }
5213
5473
  });
5214
5474
  }).on("mouseleave.links", function(_event, _hoveredNode) {
5215
- clearAllHoverLayers();
5475
+ const hoveredNodeElement = this;
5476
+ if (currentHoveredNode?.element === hoveredNodeElement) {
5477
+ clearAllHoverLayers();
5478
+ }
5216
5479
  });
5217
5480
  }
5218
5481
 
@@ -5281,7 +5544,7 @@ function createLinkHover(linkSelection, hoverStyle) {
5281
5544
  }
5282
5545
  }
5283
5546
  const labelSelection2 = root2.select('[data-layer="link-labels"]').selectAll(".link-label");
5284
- labelSelection2.filter((item) => item.link === renderableLink.link && item.style.label.visibility === "hover").style("opacity", 1).style("pointer-events", "auto");
5547
+ labelSelection2.filter((item) => item.link === renderableLink.link && item.style.label.visibility === "hover").style("opacity", 1);
5285
5548
  }).on("mouseleave.hover", function(_event, renderableLink) {
5286
5549
  const hoveredElement = this;
5287
5550
  let targetLinkElement;
@@ -5578,7 +5841,47 @@ function getDefaultContent(node) {
5578
5841
 
5579
5842
  // src/utils/node-link-selection.utils.ts
5580
5843
  function createLinkHitArea(root2, linkSelection) {
5581
- 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);
5844
+ return root2.select('[data-layer="links"]').selectAll("rect.link-hit-area").data(linkSelection.data()).join("rect").attr("class", "link-hit-area").attr("fill", "rgba(0,0,0,0)").style("pointer-events", "fill").style("cursor", "pointer").attr("opacity", 0).each(function(item) {
5845
+ const rectElement = this;
5846
+ updateHitAreaDimensions(rectElement, item, root2);
5847
+ });
5848
+ }
5849
+ function updateHitAreaDimensions(rectElement, item, root2) {
5850
+ const source = item.link.source;
5851
+ const target = item.link.target;
5852
+ if (!source.x || !source.y || !target.x || !target.y) return;
5853
+ const midX = (source.x + target.x) / 2;
5854
+ const midY = (source.y + target.y) / 2;
5855
+ const linkPadding = Math.max(item.style.strokeWidth || 2, item.style.arrow.size) * 2;
5856
+ let width = linkPadding;
5857
+ let height = linkPadding;
5858
+ if (item.link.label) {
5859
+ const labelElement = root2.select('[data-layer="link-labels"]').selectAll(".link-label").filter((labelItem) => labelItem.link === item.link).node();
5860
+ if (labelElement) {
5861
+ try {
5862
+ const textElement = labelElement.querySelector("text");
5863
+ const rectElement2 = labelElement.querySelector("rect");
5864
+ if (textElement && rectElement2) {
5865
+ const bbox = textElement.getBBox();
5866
+ const labelWidth = bbox.width + item.style.label.paddingX * 2;
5867
+ const labelHeight = bbox.height + item.style.label.paddingY * 2;
5868
+ width = Math.max(width, labelWidth + 10);
5869
+ height = Math.max(height, labelHeight + 10);
5870
+ }
5871
+ } catch {
5872
+ const text = item.link.label ?? "";
5873
+ const fontSize = item.style.label.fontSize;
5874
+ const estimatedWidth = text.length * fontSize * 0.6 + item.style.label.paddingX * 2 + 10;
5875
+ const estimatedHeight = fontSize + item.style.label.paddingY * 2 + 10;
5876
+ width = Math.max(width, estimatedWidth);
5877
+ height = Math.max(height, estimatedHeight);
5878
+ }
5879
+ }
5880
+ }
5881
+ rectElement.setAttribute("x", String(midX - width / 2));
5882
+ rectElement.setAttribute("y", String(midY - height / 2));
5883
+ rectElement.setAttribute("width", String(width));
5884
+ rectElement.setAttribute("height", String(height));
5582
5885
  }
5583
5886
 
5584
5887
  // src/utils/get-link-target-point.ts
@@ -6290,7 +6593,18 @@ var InteractionManager = class {
6290
6593
  }
6291
6594
  if (this.manager.simulation) {
6292
6595
  this.manager.simulation.on("tick.hitarea", () => {
6293
- 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);
6596
+ linkHitAreaSelection.each(function(item) {
6597
+ const source = item.link.source;
6598
+ const target = item.link.target;
6599
+ if (!source.x || !source.y || !target.x || !target.y) return;
6600
+ const rectElement = this;
6601
+ const midX = (source.x + target.x) / 2;
6602
+ const midY = (source.y + target.y) / 2;
6603
+ const width = parseFloat(rectElement.getAttribute("width") || "20");
6604
+ const height = parseFloat(rectElement.getAttribute("height") || "20");
6605
+ rectElement.setAttribute("x", String(midX - width / 2));
6606
+ rectElement.setAttribute("y", String(midY - height / 2));
6607
+ });
6294
6608
  });
6295
6609
  }
6296
6610
  if (this.manager.selectionManager) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polly-graph",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Reusable D3-based graph visualization SDK with configurable nodes, links, labels, interactions, and layout behaviors.",
5
5
  "license": "MIT",
6
6
  "author": "Badal",