polly-graph 0.1.9 → 0.1.11

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/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # polly-graph
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/polly-graph)](https://www.npmjs.com/package/polly-graph)
4
+ [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
3
7
  A framework-independent TypeScript-based D3 graph visualization SDK that provides a comprehensive, reusable solution for creating interactive network graphs. Designed to work seamlessly across React, Angular, Vue, Svelte, or vanilla JavaScript applications.
4
8
 
5
9
  ## Features
package/dist/index.cjs CHANGED
@@ -5107,6 +5107,258 @@ function createDragBehavior(simulation, onDragStart, canvasBounds) {
5107
5107
  });
5108
5108
  }
5109
5109
 
5110
+ // src/utils/node-style-manager.ts
5111
+ var NodeStyleManager = class {
5112
+ styleStates = /* @__PURE__ */ new Map();
5113
+ /**
5114
+ * Initialize a node's style state by capturing its original styles
5115
+ */
5116
+ initializeNode(element, node) {
5117
+ if (this.styleStates.has(element)) {
5118
+ return;
5119
+ }
5120
+ const domBackup = {
5121
+ stroke: element.getAttribute("stroke"),
5122
+ strokeWidth: element.getAttribute("stroke-width"),
5123
+ opacity: element.getAttribute("opacity"),
5124
+ fill: element.getAttribute("fill")
5125
+ };
5126
+ const originalStyle = {
5127
+ stroke: node.style?.stroke || void 0,
5128
+ strokeWidth: node.style?.strokeWidth || void 0,
5129
+ opacity: node.style?.opacity || void 0,
5130
+ fill: node.style?.fill || void 0
5131
+ };
5132
+ this.styleStates.set(element, {
5133
+ original: originalStyle,
5134
+ current: { ...originalStyle },
5135
+ domBackup
5136
+ });
5137
+ }
5138
+ /**
5139
+ * Apply temporary styles (e.g., hover effects) that will be reset later
5140
+ */
5141
+ applyTemporaryStyles(element, styles) {
5142
+ this.applyStylesToDOM(element, styles);
5143
+ }
5144
+ /**
5145
+ * Apply permanent styles that become the new base styles
5146
+ */
5147
+ applyPermanentStyles(element, styles) {
5148
+ const state = this.styleStates.get(element);
5149
+ if (!state) {
5150
+ console.warn("[NodeStyleManager] Node not initialized, cannot apply permanent styles");
5151
+ return;
5152
+ }
5153
+ this.applyStylesToDOM(element, styles);
5154
+ Object.assign(state.current, styles);
5155
+ }
5156
+ /**
5157
+ * Reset node to its current base styles (removes temporary styles)
5158
+ */
5159
+ resetToBase(element) {
5160
+ const state = this.styleStates.get(element);
5161
+ if (!state) {
5162
+ this.clearAllStyles(element);
5163
+ return;
5164
+ }
5165
+ this.clearAllStyles(element);
5166
+ this.applyStylesToDOM(element, state.current);
5167
+ }
5168
+ /**
5169
+ * Reset node to its original styles (as captured during initialization)
5170
+ */
5171
+ resetToOriginal(element) {
5172
+ const state = this.styleStates.get(element);
5173
+ if (!state) {
5174
+ this.clearAllStyles(element);
5175
+ return;
5176
+ }
5177
+ this.clearAllStyles(element);
5178
+ this.restoreOriginalDOM(element, state.domBackup);
5179
+ state.current = { ...state.original };
5180
+ }
5181
+ /**
5182
+ * Check if node is in a specific state (selected, hovered, etc.)
5183
+ */
5184
+ hasState(element, stateName) {
5185
+ return element.dataset[stateName] === "true";
5186
+ }
5187
+ /**
5188
+ * Set state marker on node
5189
+ */
5190
+ setState(element, stateName, value) {
5191
+ if (value) {
5192
+ element.dataset[stateName] = "true";
5193
+ } else {
5194
+ delete element.dataset[stateName];
5195
+ }
5196
+ }
5197
+ /**
5198
+ * Get the original styles for a node
5199
+ */
5200
+ getOriginalStyles(element) {
5201
+ const state = this.styleStates.get(element);
5202
+ return state ? { ...state.original } : null;
5203
+ }
5204
+ /**
5205
+ * Get the current base styles for a node
5206
+ */
5207
+ getCurrentStyles(element) {
5208
+ const state = this.styleStates.get(element);
5209
+ return state ? { ...state.current } : null;
5210
+ }
5211
+ /**
5212
+ * Remove a node from management (cleanup)
5213
+ */
5214
+ removeNode(element) {
5215
+ this.styleStates.delete(element);
5216
+ }
5217
+ /**
5218
+ * Clear all managed nodes (for cleanup)
5219
+ */
5220
+ clear() {
5221
+ this.styleStates.clear();
5222
+ }
5223
+ /**
5224
+ * Private: Apply styles to DOM element
5225
+ */
5226
+ applyStylesToDOM(element, styles) {
5227
+ if (styles.stroke !== void 0) {
5228
+ if (styles.stroke === null || styles.stroke === "") {
5229
+ element.removeAttribute("stroke");
5230
+ element.style.stroke = "";
5231
+ } else {
5232
+ element.style.stroke = styles.stroke;
5233
+ }
5234
+ }
5235
+ if (styles.strokeWidth !== void 0) {
5236
+ if (styles.strokeWidth === null || styles.strokeWidth === 0) {
5237
+ element.removeAttribute("stroke-width");
5238
+ element.style.strokeWidth = "";
5239
+ } else {
5240
+ element.style.strokeWidth = String(styles.strokeWidth);
5241
+ }
5242
+ }
5243
+ if (styles.opacity !== void 0) {
5244
+ if (styles.opacity === null || styles.opacity === 1) {
5245
+ element.removeAttribute("opacity");
5246
+ element.style.opacity = "";
5247
+ } else {
5248
+ element.style.opacity = String(styles.opacity);
5249
+ }
5250
+ }
5251
+ if (styles.fill !== void 0) {
5252
+ if (styles.fill === null || styles.fill === "") {
5253
+ element.removeAttribute("fill");
5254
+ element.style.fill = "";
5255
+ } else {
5256
+ element.style.fill = styles.fill;
5257
+ }
5258
+ }
5259
+ if (styles.radius !== void 0) {
5260
+ if (styles.radius === null || styles.radius === 0) {
5261
+ element.removeAttribute("r");
5262
+ element.style.removeProperty("r");
5263
+ } else {
5264
+ element.setAttribute("r", String(styles.radius));
5265
+ }
5266
+ }
5267
+ }
5268
+ /**
5269
+ * Private: Clear all inline styles and remove hover-related attributes
5270
+ */
5271
+ clearAllStyles(element) {
5272
+ element.style.stroke = "";
5273
+ element.style.strokeWidth = "";
5274
+ element.style.opacity = "";
5275
+ element.style.fill = "";
5276
+ element.style.removeProperty("r");
5277
+ this.removeIfHoverAttribute(element, "stroke");
5278
+ this.removeIfHoverAttribute(element, "stroke-width");
5279
+ this.removeIfHoverAttribute(element, "opacity");
5280
+ }
5281
+ /**
5282
+ * Private: Restore original DOM attributes
5283
+ */
5284
+ restoreOriginalDOM(element, domBackup) {
5285
+ if (domBackup.stroke !== void 0) {
5286
+ if (domBackup.stroke === null) {
5287
+ element.removeAttribute("stroke");
5288
+ } else {
5289
+ element.setAttribute("stroke", domBackup.stroke);
5290
+ }
5291
+ }
5292
+ if (domBackup.strokeWidth !== void 0) {
5293
+ if (domBackup.strokeWidth === null) {
5294
+ element.removeAttribute("stroke-width");
5295
+ } else {
5296
+ element.setAttribute("stroke-width", domBackup.strokeWidth);
5297
+ }
5298
+ }
5299
+ if (domBackup.opacity !== void 0) {
5300
+ if (domBackup.opacity === null) {
5301
+ element.removeAttribute("opacity");
5302
+ } else {
5303
+ element.setAttribute("opacity", domBackup.opacity);
5304
+ }
5305
+ }
5306
+ if (domBackup.fill !== void 0) {
5307
+ if (domBackup.fill === null) {
5308
+ element.removeAttribute("fill");
5309
+ } else {
5310
+ element.setAttribute("fill", domBackup.fill);
5311
+ }
5312
+ }
5313
+ }
5314
+ /**
5315
+ * Private: Remove attribute only if it looks like it was set by hover/interaction
5316
+ */
5317
+ removeIfHoverAttribute(element, attr) {
5318
+ const value = element.getAttribute(attr);
5319
+ if (!value) return;
5320
+ const hoverPatterns = {
5321
+ "stroke": ["#6366f1", "#8b5cf6", "#3b82f6", "#ffffff", "#fff", "white"],
5322
+ // Common hover stroke colors
5323
+ "stroke-width": ["2", "2.5", "3", "4"],
5324
+ // Common hover stroke widths
5325
+ "opacity": ["0.8", "0.9", "0.7"]
5326
+ // Common hover opacity values
5327
+ };
5328
+ const patterns = hoverPatterns[attr];
5329
+ if (patterns && patterns.includes(value)) {
5330
+ element.removeAttribute(attr);
5331
+ }
5332
+ }
5333
+ };
5334
+ var nodeStyleManager = new NodeStyleManager();
5335
+ function applyHoverStyles(element, node, hoverStyle) {
5336
+ if (nodeStyleManager.hasState(element, "selected")) {
5337
+ return;
5338
+ }
5339
+ nodeStyleManager.initializeNode(element, node);
5340
+ nodeStyleManager.applyTemporaryStyles(element, hoverStyle);
5341
+ nodeStyleManager.setState(element, "hovered", true);
5342
+ }
5343
+ function removeHoverStyles(element, node) {
5344
+ if (nodeStyleManager.hasState(element, "selected")) {
5345
+ return;
5346
+ }
5347
+ nodeStyleManager.initializeNode(element, node);
5348
+ nodeStyleManager.resetToBase(element);
5349
+ nodeStyleManager.setState(element, "hovered", false);
5350
+ }
5351
+ function applySelectionStyles(element, node, selectionStyle) {
5352
+ nodeStyleManager.initializeNode(element, node);
5353
+ nodeStyleManager.applyPermanentStyles(element, selectionStyle);
5354
+ nodeStyleManager.setState(element, "selected", true);
5355
+ }
5356
+ function removeSelectionStyles(element, node) {
5357
+ nodeStyleManager.initializeNode(element, node);
5358
+ nodeStyleManager.resetToOriginal(element);
5359
+ nodeStyleManager.setState(element, "selected", false);
5360
+ }
5361
+
5110
5362
  // src/interactions/create-node-hover.ts
5111
5363
  function createNodeHover(nodeSelection, hoverStyle) {
5112
5364
  const firstNode = nodeSelection.node();
@@ -5114,14 +5366,10 @@ function createNodeHover(nodeSelection, hoverStyle) {
5114
5366
  if (hoverStyle) {
5115
5367
  nodeSelection.on("mouseenter.hover", function(_event, node) {
5116
5368
  const circle = this;
5117
- circle.setAttribute("stroke", hoverStyle.stroke ?? node.style?.stroke ?? "#ffffff");
5118
- circle.setAttribute("stroke-width", String(hoverStyle.strokeWidth ?? node.style?.strokeWidth ?? 1.5));
5119
- circle.setAttribute("opacity", String(hoverStyle.opacity ?? node.style?.opacity ?? 1));
5369
+ applyHoverStyles(circle, node, hoverStyle);
5120
5370
  }).on("mouseleave.hover", function(_event, node) {
5121
5371
  const circle = this;
5122
- circle.setAttribute("stroke", node.style?.stroke ?? "#ffffff");
5123
- circle.setAttribute("stroke-width", String(node.style?.strokeWidth ?? 1.5));
5124
- circle.setAttribute("opacity", String(node.style?.opacity ?? 1));
5372
+ removeHoverStyles(circle, node);
5125
5373
  });
5126
5374
  }
5127
5375
  const svgElement = firstNode.ownerSVGElement;
@@ -5518,6 +5766,8 @@ function bindNodeTooltip(params) {
5518
5766
  destroy: () => {
5519
5767
  },
5520
5768
  reposition: () => {
5769
+ },
5770
+ hide: () => {
5521
5771
  }
5522
5772
  };
5523
5773
  }
@@ -5527,6 +5777,9 @@ function bindNodeTooltip(params) {
5527
5777
  "mouseenter.tooltip",
5528
5778
  function(event, node) {
5529
5779
  const target = this;
5780
+ if (target.dataset.selected === "true") {
5781
+ return;
5782
+ }
5530
5783
  activeTarget = target;
5531
5784
  const customContent = params.tooltipConfig?.renderContent?.(node);
5532
5785
  const content = customContent ?? getDefaultContent(node);
@@ -5536,6 +5789,11 @@ function bindNodeTooltip(params) {
5536
5789
  "mousemove.tooltip",
5537
5790
  function() {
5538
5791
  const target = this;
5792
+ if (target.dataset.selected === "true") {
5793
+ activeTarget = null;
5794
+ tooltip.hide();
5795
+ return;
5796
+ }
5539
5797
  activeTarget = target;
5540
5798
  tooltip.move(target);
5541
5799
  }
@@ -5552,12 +5810,16 @@ function bindNodeTooltip(params) {
5552
5810
  }
5553
5811
  tooltip.move(activeTarget);
5554
5812
  }
5813
+ function hide() {
5814
+ activeTarget = null;
5815
+ tooltip.hide();
5816
+ }
5555
5817
  function destroy() {
5556
5818
  activeTarget = null;
5557
5819
  params.selection.on(".tooltip", null);
5558
5820
  tooltip.destroy();
5559
5821
  }
5560
- return { destroy, reposition };
5822
+ return { destroy, reposition, hide };
5561
5823
  }
5562
5824
  function getDefaultContent(node) {
5563
5825
  return `
@@ -5787,27 +6049,27 @@ var SelectionManager = class {
5787
6049
  layers;
5788
6050
  linkMarkerSnapshots;
5789
6051
  root;
5790
- constructor(eventEmitter, config, layers, linkMarkerSnapshots, root2) {
6052
+ tooltipBinding;
6053
+ constructor(eventEmitter, config, layers, linkMarkerSnapshots, root2, tooltipBinding) {
5791
6054
  this.eventEmitter = eventEmitter;
5792
6055
  this.config = config;
5793
6056
  this.layers = layers;
5794
6057
  this.linkMarkerSnapshots = linkMarkerSnapshots;
5795
6058
  this.root = root2;
6059
+ this.tooltipBinding = tooltipBinding;
5796
6060
  }
5797
6061
  /**
5798
6062
  * Select a node, automatically deselecting any current selection
5799
6063
  */
5800
6064
  selectNode(nodeElement, nodeData) {
6065
+ if (this.tooltipBinding) {
6066
+ this.tooltipBinding.hide();
6067
+ }
5801
6068
  this.clearHoverState();
5802
6069
  this.clearSelection();
5803
6070
  this.bringNodeToFront(nodeElement, nodeData);
5804
6071
  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));
6072
+ applySelectionStyles(nodeElement, nodeData, this.config.nodeStyle);
5811
6073
  }
5812
6074
  this.root.selectAll(".link-label").filter((item) => {
5813
6075
  if (item.style.label.visibility !== "hover") return false;
@@ -5858,12 +6120,7 @@ var SelectionManager = class {
5858
6120
  if (!this.state.selectedNode) return;
5859
6121
  const { element, data } = this.state.selectedNode;
5860
6122
  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;
6123
+ removeSelectionStyles(element, data);
5867
6124
  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
6125
  this.state.selectedNode = null;
5869
6126
  this.eventEmitter.emit("nodeDeselect", { node: data, element });
@@ -6088,18 +6345,42 @@ var DEFAULT_NODE_HOVER_STYLE = {
6088
6345
  };
6089
6346
  function resolveNodeStyle(params) {
6090
6347
  if (params.isSelected) {
6091
- return mergeNodeStyle(DEFAULT_NODE_HOVER_STYLE, params.interaction?.selection?.nodeStyle);
6348
+ return mergeNodeStyleSmart(DEFAULT_NODE_HOVER_STYLE, params.interaction?.selection?.nodeStyle);
6092
6349
  }
6093
6350
  if (params.isHovered) {
6094
- return mergeNodeStyle(DEFAULT_NODE_HOVER_STYLE, params.interaction?.hover?.nodeStyle);
6351
+ return mergeNodeStyleSmart(DEFAULT_NODE_HOVER_STYLE, params.interaction?.hover?.nodeStyle);
6095
6352
  }
6096
6353
  return void 0;
6097
6354
  }
6098
- function mergeNodeStyle(base, override) {
6099
- return {
6100
- ...base,
6101
- ...override
6102
- };
6355
+ function mergeNodeStyleSmart(base, override) {
6356
+ if (!override) return base;
6357
+ const result = { ...override };
6358
+ if (override.strokeWidth !== void 0 && override.stroke === void 0 && base.stroke !== void 0) {
6359
+ result.stroke = base.stroke;
6360
+ }
6361
+ const baseKeys = Object.keys(base);
6362
+ baseKeys.forEach((key) => {
6363
+ if (key !== "stroke" && override[key] === void 0 && base[key] !== void 0) {
6364
+ switch (key) {
6365
+ case "fill":
6366
+ result.fill = base.fill;
6367
+ break;
6368
+ case "strokeWidth":
6369
+ result.strokeWidth = base.strokeWidth;
6370
+ break;
6371
+ case "opacity":
6372
+ result.opacity = base.opacity;
6373
+ break;
6374
+ case "radius":
6375
+ result.radius = base.radius;
6376
+ break;
6377
+ case "textColor":
6378
+ result.textColor = base.textColor;
6379
+ break;
6380
+ }
6381
+ }
6382
+ });
6383
+ return result;
6103
6384
  }
6104
6385
 
6105
6386
  // src/core/interaction-manager.ts
@@ -6199,7 +6480,8 @@ var InteractionManager = class {
6199
6480
  this.manager.config.interaction.selection,
6200
6481
  this.manager.layers,
6201
6482
  this.manager.linkMarkerSnapshots,
6202
- this.manager.rootSelection
6483
+ this.manager.rootSelection,
6484
+ this.manager.tooltipBinding || void 0
6203
6485
  );
6204
6486
  this.setupSelectionHandlers(selections);
6205
6487
  this.setupBackgroundClickHandler();
package/dist/index.d.cts CHANGED
@@ -537,6 +537,12 @@ interface RenderableGraphLink {
537
537
  readonly markerEnd: string;
538
538
  }
539
539
 
540
+ interface NodeTooltipBinding {
541
+ destroy(): void;
542
+ reposition(): void;
543
+ hide(): void;
544
+ }
545
+
540
546
  /**
541
547
  * Centralized selection management for graph nodes and links.
542
548
  * Simplifies selection logic and ensures consistent behavior.
@@ -560,7 +566,8 @@ declare class SelectionManager {
560
566
  private readonly layers;
561
567
  private readonly linkMarkerSnapshots;
562
568
  private readonly root;
563
- constructor(eventEmitter: TypedGraphEventEmitter, config: SelectionInteractionConfig, layers: GraphLayers, linkMarkerSnapshots: Map<SVGLineElement, string | null>, root: Selection<SVGGElement, unknown, null, undefined>);
569
+ private readonly tooltipBinding?;
570
+ constructor(eventEmitter: TypedGraphEventEmitter, config: SelectionInteractionConfig, layers: GraphLayers, linkMarkerSnapshots: Map<SVGLineElement, string | null>, root: Selection<SVGGElement, unknown, null, undefined>, tooltipBinding?: NodeTooltipBinding);
564
571
  /**
565
572
  * Select a node, automatically deselecting any current selection
566
573
  */
package/dist/index.d.ts CHANGED
@@ -537,6 +537,12 @@ interface RenderableGraphLink {
537
537
  readonly markerEnd: string;
538
538
  }
539
539
 
540
+ interface NodeTooltipBinding {
541
+ destroy(): void;
542
+ reposition(): void;
543
+ hide(): void;
544
+ }
545
+
540
546
  /**
541
547
  * Centralized selection management for graph nodes and links.
542
548
  * Simplifies selection logic and ensures consistent behavior.
@@ -560,7 +566,8 @@ declare class SelectionManager {
560
566
  private readonly layers;
561
567
  private readonly linkMarkerSnapshots;
562
568
  private readonly root;
563
- constructor(eventEmitter: TypedGraphEventEmitter, config: SelectionInteractionConfig, layers: GraphLayers, linkMarkerSnapshots: Map<SVGLineElement, string | null>, root: Selection<SVGGElement, unknown, null, undefined>);
569
+ private readonly tooltipBinding?;
570
+ constructor(eventEmitter: TypedGraphEventEmitter, config: SelectionInteractionConfig, layers: GraphLayers, linkMarkerSnapshots: Map<SVGLineElement, string | null>, root: Selection<SVGGElement, unknown, null, undefined>, tooltipBinding?: NodeTooltipBinding);
564
571
  /**
565
572
  * Select a node, automatically deselecting any current selection
566
573
  */
package/dist/index.js CHANGED
@@ -5075,6 +5075,258 @@ function createDragBehavior(simulation, onDragStart, canvasBounds) {
5075
5075
  });
5076
5076
  }
5077
5077
 
5078
+ // src/utils/node-style-manager.ts
5079
+ var NodeStyleManager = class {
5080
+ styleStates = /* @__PURE__ */ new Map();
5081
+ /**
5082
+ * Initialize a node's style state by capturing its original styles
5083
+ */
5084
+ initializeNode(element, node) {
5085
+ if (this.styleStates.has(element)) {
5086
+ return;
5087
+ }
5088
+ const domBackup = {
5089
+ stroke: element.getAttribute("stroke"),
5090
+ strokeWidth: element.getAttribute("stroke-width"),
5091
+ opacity: element.getAttribute("opacity"),
5092
+ fill: element.getAttribute("fill")
5093
+ };
5094
+ const originalStyle = {
5095
+ stroke: node.style?.stroke || void 0,
5096
+ strokeWidth: node.style?.strokeWidth || void 0,
5097
+ opacity: node.style?.opacity || void 0,
5098
+ fill: node.style?.fill || void 0
5099
+ };
5100
+ this.styleStates.set(element, {
5101
+ original: originalStyle,
5102
+ current: { ...originalStyle },
5103
+ domBackup
5104
+ });
5105
+ }
5106
+ /**
5107
+ * Apply temporary styles (e.g., hover effects) that will be reset later
5108
+ */
5109
+ applyTemporaryStyles(element, styles) {
5110
+ this.applyStylesToDOM(element, styles);
5111
+ }
5112
+ /**
5113
+ * Apply permanent styles that become the new base styles
5114
+ */
5115
+ applyPermanentStyles(element, styles) {
5116
+ const state = this.styleStates.get(element);
5117
+ if (!state) {
5118
+ console.warn("[NodeStyleManager] Node not initialized, cannot apply permanent styles");
5119
+ return;
5120
+ }
5121
+ this.applyStylesToDOM(element, styles);
5122
+ Object.assign(state.current, styles);
5123
+ }
5124
+ /**
5125
+ * Reset node to its current base styles (removes temporary styles)
5126
+ */
5127
+ resetToBase(element) {
5128
+ const state = this.styleStates.get(element);
5129
+ if (!state) {
5130
+ this.clearAllStyles(element);
5131
+ return;
5132
+ }
5133
+ this.clearAllStyles(element);
5134
+ this.applyStylesToDOM(element, state.current);
5135
+ }
5136
+ /**
5137
+ * Reset node to its original styles (as captured during initialization)
5138
+ */
5139
+ resetToOriginal(element) {
5140
+ const state = this.styleStates.get(element);
5141
+ if (!state) {
5142
+ this.clearAllStyles(element);
5143
+ return;
5144
+ }
5145
+ this.clearAllStyles(element);
5146
+ this.restoreOriginalDOM(element, state.domBackup);
5147
+ state.current = { ...state.original };
5148
+ }
5149
+ /**
5150
+ * Check if node is in a specific state (selected, hovered, etc.)
5151
+ */
5152
+ hasState(element, stateName) {
5153
+ return element.dataset[stateName] === "true";
5154
+ }
5155
+ /**
5156
+ * Set state marker on node
5157
+ */
5158
+ setState(element, stateName, value) {
5159
+ if (value) {
5160
+ element.dataset[stateName] = "true";
5161
+ } else {
5162
+ delete element.dataset[stateName];
5163
+ }
5164
+ }
5165
+ /**
5166
+ * Get the original styles for a node
5167
+ */
5168
+ getOriginalStyles(element) {
5169
+ const state = this.styleStates.get(element);
5170
+ return state ? { ...state.original } : null;
5171
+ }
5172
+ /**
5173
+ * Get the current base styles for a node
5174
+ */
5175
+ getCurrentStyles(element) {
5176
+ const state = this.styleStates.get(element);
5177
+ return state ? { ...state.current } : null;
5178
+ }
5179
+ /**
5180
+ * Remove a node from management (cleanup)
5181
+ */
5182
+ removeNode(element) {
5183
+ this.styleStates.delete(element);
5184
+ }
5185
+ /**
5186
+ * Clear all managed nodes (for cleanup)
5187
+ */
5188
+ clear() {
5189
+ this.styleStates.clear();
5190
+ }
5191
+ /**
5192
+ * Private: Apply styles to DOM element
5193
+ */
5194
+ applyStylesToDOM(element, styles) {
5195
+ if (styles.stroke !== void 0) {
5196
+ if (styles.stroke === null || styles.stroke === "") {
5197
+ element.removeAttribute("stroke");
5198
+ element.style.stroke = "";
5199
+ } else {
5200
+ element.style.stroke = styles.stroke;
5201
+ }
5202
+ }
5203
+ if (styles.strokeWidth !== void 0) {
5204
+ if (styles.strokeWidth === null || styles.strokeWidth === 0) {
5205
+ element.removeAttribute("stroke-width");
5206
+ element.style.strokeWidth = "";
5207
+ } else {
5208
+ element.style.strokeWidth = String(styles.strokeWidth);
5209
+ }
5210
+ }
5211
+ if (styles.opacity !== void 0) {
5212
+ if (styles.opacity === null || styles.opacity === 1) {
5213
+ element.removeAttribute("opacity");
5214
+ element.style.opacity = "";
5215
+ } else {
5216
+ element.style.opacity = String(styles.opacity);
5217
+ }
5218
+ }
5219
+ if (styles.fill !== void 0) {
5220
+ if (styles.fill === null || styles.fill === "") {
5221
+ element.removeAttribute("fill");
5222
+ element.style.fill = "";
5223
+ } else {
5224
+ element.style.fill = styles.fill;
5225
+ }
5226
+ }
5227
+ if (styles.radius !== void 0) {
5228
+ if (styles.radius === null || styles.radius === 0) {
5229
+ element.removeAttribute("r");
5230
+ element.style.removeProperty("r");
5231
+ } else {
5232
+ element.setAttribute("r", String(styles.radius));
5233
+ }
5234
+ }
5235
+ }
5236
+ /**
5237
+ * Private: Clear all inline styles and remove hover-related attributes
5238
+ */
5239
+ clearAllStyles(element) {
5240
+ element.style.stroke = "";
5241
+ element.style.strokeWidth = "";
5242
+ element.style.opacity = "";
5243
+ element.style.fill = "";
5244
+ element.style.removeProperty("r");
5245
+ this.removeIfHoverAttribute(element, "stroke");
5246
+ this.removeIfHoverAttribute(element, "stroke-width");
5247
+ this.removeIfHoverAttribute(element, "opacity");
5248
+ }
5249
+ /**
5250
+ * Private: Restore original DOM attributes
5251
+ */
5252
+ restoreOriginalDOM(element, domBackup) {
5253
+ if (domBackup.stroke !== void 0) {
5254
+ if (domBackup.stroke === null) {
5255
+ element.removeAttribute("stroke");
5256
+ } else {
5257
+ element.setAttribute("stroke", domBackup.stroke);
5258
+ }
5259
+ }
5260
+ if (domBackup.strokeWidth !== void 0) {
5261
+ if (domBackup.strokeWidth === null) {
5262
+ element.removeAttribute("stroke-width");
5263
+ } else {
5264
+ element.setAttribute("stroke-width", domBackup.strokeWidth);
5265
+ }
5266
+ }
5267
+ if (domBackup.opacity !== void 0) {
5268
+ if (domBackup.opacity === null) {
5269
+ element.removeAttribute("opacity");
5270
+ } else {
5271
+ element.setAttribute("opacity", domBackup.opacity);
5272
+ }
5273
+ }
5274
+ if (domBackup.fill !== void 0) {
5275
+ if (domBackup.fill === null) {
5276
+ element.removeAttribute("fill");
5277
+ } else {
5278
+ element.setAttribute("fill", domBackup.fill);
5279
+ }
5280
+ }
5281
+ }
5282
+ /**
5283
+ * Private: Remove attribute only if it looks like it was set by hover/interaction
5284
+ */
5285
+ removeIfHoverAttribute(element, attr) {
5286
+ const value = element.getAttribute(attr);
5287
+ if (!value) return;
5288
+ const hoverPatterns = {
5289
+ "stroke": ["#6366f1", "#8b5cf6", "#3b82f6", "#ffffff", "#fff", "white"],
5290
+ // Common hover stroke colors
5291
+ "stroke-width": ["2", "2.5", "3", "4"],
5292
+ // Common hover stroke widths
5293
+ "opacity": ["0.8", "0.9", "0.7"]
5294
+ // Common hover opacity values
5295
+ };
5296
+ const patterns = hoverPatterns[attr];
5297
+ if (patterns && patterns.includes(value)) {
5298
+ element.removeAttribute(attr);
5299
+ }
5300
+ }
5301
+ };
5302
+ var nodeStyleManager = new NodeStyleManager();
5303
+ function applyHoverStyles(element, node, hoverStyle) {
5304
+ if (nodeStyleManager.hasState(element, "selected")) {
5305
+ return;
5306
+ }
5307
+ nodeStyleManager.initializeNode(element, node);
5308
+ nodeStyleManager.applyTemporaryStyles(element, hoverStyle);
5309
+ nodeStyleManager.setState(element, "hovered", true);
5310
+ }
5311
+ function removeHoverStyles(element, node) {
5312
+ if (nodeStyleManager.hasState(element, "selected")) {
5313
+ return;
5314
+ }
5315
+ nodeStyleManager.initializeNode(element, node);
5316
+ nodeStyleManager.resetToBase(element);
5317
+ nodeStyleManager.setState(element, "hovered", false);
5318
+ }
5319
+ function applySelectionStyles(element, node, selectionStyle) {
5320
+ nodeStyleManager.initializeNode(element, node);
5321
+ nodeStyleManager.applyPermanentStyles(element, selectionStyle);
5322
+ nodeStyleManager.setState(element, "selected", true);
5323
+ }
5324
+ function removeSelectionStyles(element, node) {
5325
+ nodeStyleManager.initializeNode(element, node);
5326
+ nodeStyleManager.resetToOriginal(element);
5327
+ nodeStyleManager.setState(element, "selected", false);
5328
+ }
5329
+
5078
5330
  // src/interactions/create-node-hover.ts
5079
5331
  function createNodeHover(nodeSelection, hoverStyle) {
5080
5332
  const firstNode = nodeSelection.node();
@@ -5082,14 +5334,10 @@ function createNodeHover(nodeSelection, hoverStyle) {
5082
5334
  if (hoverStyle) {
5083
5335
  nodeSelection.on("mouseenter.hover", function(_event, node) {
5084
5336
  const circle = this;
5085
- circle.setAttribute("stroke", hoverStyle.stroke ?? node.style?.stroke ?? "#ffffff");
5086
- circle.setAttribute("stroke-width", String(hoverStyle.strokeWidth ?? node.style?.strokeWidth ?? 1.5));
5087
- circle.setAttribute("opacity", String(hoverStyle.opacity ?? node.style?.opacity ?? 1));
5337
+ applyHoverStyles(circle, node, hoverStyle);
5088
5338
  }).on("mouseleave.hover", function(_event, node) {
5089
5339
  const circle = this;
5090
- circle.setAttribute("stroke", node.style?.stroke ?? "#ffffff");
5091
- circle.setAttribute("stroke-width", String(node.style?.strokeWidth ?? 1.5));
5092
- circle.setAttribute("opacity", String(node.style?.opacity ?? 1));
5340
+ removeHoverStyles(circle, node);
5093
5341
  });
5094
5342
  }
5095
5343
  const svgElement = firstNode.ownerSVGElement;
@@ -5486,6 +5734,8 @@ function bindNodeTooltip(params) {
5486
5734
  destroy: () => {
5487
5735
  },
5488
5736
  reposition: () => {
5737
+ },
5738
+ hide: () => {
5489
5739
  }
5490
5740
  };
5491
5741
  }
@@ -5495,6 +5745,9 @@ function bindNodeTooltip(params) {
5495
5745
  "mouseenter.tooltip",
5496
5746
  function(event, node) {
5497
5747
  const target = this;
5748
+ if (target.dataset.selected === "true") {
5749
+ return;
5750
+ }
5498
5751
  activeTarget = target;
5499
5752
  const customContent = params.tooltipConfig?.renderContent?.(node);
5500
5753
  const content = customContent ?? getDefaultContent(node);
@@ -5504,6 +5757,11 @@ function bindNodeTooltip(params) {
5504
5757
  "mousemove.tooltip",
5505
5758
  function() {
5506
5759
  const target = this;
5760
+ if (target.dataset.selected === "true") {
5761
+ activeTarget = null;
5762
+ tooltip.hide();
5763
+ return;
5764
+ }
5507
5765
  activeTarget = target;
5508
5766
  tooltip.move(target);
5509
5767
  }
@@ -5520,12 +5778,16 @@ function bindNodeTooltip(params) {
5520
5778
  }
5521
5779
  tooltip.move(activeTarget);
5522
5780
  }
5781
+ function hide() {
5782
+ activeTarget = null;
5783
+ tooltip.hide();
5784
+ }
5523
5785
  function destroy() {
5524
5786
  activeTarget = null;
5525
5787
  params.selection.on(".tooltip", null);
5526
5788
  tooltip.destroy();
5527
5789
  }
5528
- return { destroy, reposition };
5790
+ return { destroy, reposition, hide };
5529
5791
  }
5530
5792
  function getDefaultContent(node) {
5531
5793
  return `
@@ -5755,27 +6017,27 @@ var SelectionManager = class {
5755
6017
  layers;
5756
6018
  linkMarkerSnapshots;
5757
6019
  root;
5758
- constructor(eventEmitter, config, layers, linkMarkerSnapshots, root2) {
6020
+ tooltipBinding;
6021
+ constructor(eventEmitter, config, layers, linkMarkerSnapshots, root2, tooltipBinding) {
5759
6022
  this.eventEmitter = eventEmitter;
5760
6023
  this.config = config;
5761
6024
  this.layers = layers;
5762
6025
  this.linkMarkerSnapshots = linkMarkerSnapshots;
5763
6026
  this.root = root2;
6027
+ this.tooltipBinding = tooltipBinding;
5764
6028
  }
5765
6029
  /**
5766
6030
  * Select a node, automatically deselecting any current selection
5767
6031
  */
5768
6032
  selectNode(nodeElement, nodeData) {
6033
+ if (this.tooltipBinding) {
6034
+ this.tooltipBinding.hide();
6035
+ }
5769
6036
  this.clearHoverState();
5770
6037
  this.clearSelection();
5771
6038
  this.bringNodeToFront(nodeElement, nodeData);
5772
6039
  if (this.config.nodeStyle) {
5773
- const style = this.config.nodeStyle;
5774
- if (style.fill !== void 0) nodeElement.style.fill = style.fill;
5775
- if (style.stroke !== void 0) nodeElement.style.stroke = style.stroke;
5776
- if (style.strokeWidth !== void 0) nodeElement.style.strokeWidth = String(style.strokeWidth);
5777
- if (style.opacity !== void 0) nodeElement.style.opacity = String(style.opacity);
5778
- if (style.radius !== void 0) nodeElement.style.setProperty("r", String(style.radius));
6040
+ applySelectionStyles(nodeElement, nodeData, this.config.nodeStyle);
5779
6041
  }
5780
6042
  this.root.selectAll(".link-label").filter((item) => {
5781
6043
  if (item.style.label.visibility !== "hover") return false;
@@ -5826,12 +6088,7 @@ var SelectionManager = class {
5826
6088
  if (!this.state.selectedNode) return;
5827
6089
  const { element, data } = this.state.selectedNode;
5828
6090
  this.restoreSelectedElements(data);
5829
- element.style.fill = "";
5830
- element.style.stroke = "";
5831
- element.style.strokeWidth = "";
5832
- element.style.opacity = "";
5833
- element.style.removeProperty("r");
5834
- delete element.dataset.selected;
6091
+ removeSelectionStyles(element, data);
5835
6092
  this.root.selectAll(".link-label.label-selection-pinned").classed("label-selection-pinned", false).interrupt().transition().duration(200).style("opacity", 0).style("pointer-events", "none");
5836
6093
  this.state.selectedNode = null;
5837
6094
  this.eventEmitter.emit("nodeDeselect", { node: data, element });
@@ -6056,18 +6313,42 @@ var DEFAULT_NODE_HOVER_STYLE = {
6056
6313
  };
6057
6314
  function resolveNodeStyle(params) {
6058
6315
  if (params.isSelected) {
6059
- return mergeNodeStyle(DEFAULT_NODE_HOVER_STYLE, params.interaction?.selection?.nodeStyle);
6316
+ return mergeNodeStyleSmart(DEFAULT_NODE_HOVER_STYLE, params.interaction?.selection?.nodeStyle);
6060
6317
  }
6061
6318
  if (params.isHovered) {
6062
- return mergeNodeStyle(DEFAULT_NODE_HOVER_STYLE, params.interaction?.hover?.nodeStyle);
6319
+ return mergeNodeStyleSmart(DEFAULT_NODE_HOVER_STYLE, params.interaction?.hover?.nodeStyle);
6063
6320
  }
6064
6321
  return void 0;
6065
6322
  }
6066
- function mergeNodeStyle(base, override) {
6067
- return {
6068
- ...base,
6069
- ...override
6070
- };
6323
+ function mergeNodeStyleSmart(base, override) {
6324
+ if (!override) return base;
6325
+ const result = { ...override };
6326
+ if (override.strokeWidth !== void 0 && override.stroke === void 0 && base.stroke !== void 0) {
6327
+ result.stroke = base.stroke;
6328
+ }
6329
+ const baseKeys = Object.keys(base);
6330
+ baseKeys.forEach((key) => {
6331
+ if (key !== "stroke" && override[key] === void 0 && base[key] !== void 0) {
6332
+ switch (key) {
6333
+ case "fill":
6334
+ result.fill = base.fill;
6335
+ break;
6336
+ case "strokeWidth":
6337
+ result.strokeWidth = base.strokeWidth;
6338
+ break;
6339
+ case "opacity":
6340
+ result.opacity = base.opacity;
6341
+ break;
6342
+ case "radius":
6343
+ result.radius = base.radius;
6344
+ break;
6345
+ case "textColor":
6346
+ result.textColor = base.textColor;
6347
+ break;
6348
+ }
6349
+ }
6350
+ });
6351
+ return result;
6071
6352
  }
6072
6353
 
6073
6354
  // src/core/interaction-manager.ts
@@ -6167,7 +6448,8 @@ var InteractionManager = class {
6167
6448
  this.manager.config.interaction.selection,
6168
6449
  this.manager.layers,
6169
6450
  this.manager.linkMarkerSnapshots,
6170
- this.manager.rootSelection
6451
+ this.manager.rootSelection,
6452
+ this.manager.tooltipBinding || void 0
6171
6453
  );
6172
6454
  this.setupSelectionHandlers(selections);
6173
6455
  this.setupBackgroundClickHandler();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polly-graph",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
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",
@@ -52,7 +52,11 @@
52
52
  "lint": "eslint \"src/**/*.ts\"",
53
53
  "typecheck": "tsc --noEmit",
54
54
  "test": "vitest run --passWithNoTests",
55
- "prepublishOnly": "npm run lint && npm run typecheck && npm run test && npm run build"
55
+ "prepublishOnly": "npm run lint && npm run typecheck && npm run test && npm run build",
56
+ "version:patch": "npm version patch",
57
+ "version:minor": "npm version minor",
58
+ "version:major": "npm version major",
59
+ "version:prerelease": "npm version prerelease"
56
60
  },
57
61
  "dependencies": {
58
62
  "d3": "7.9.0"