polly-graph 0.1.10 → 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,35 +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
- if (circle.dataset.selected === "true") {
5118
- return;
5119
- }
5120
- circle.setAttribute("stroke", hoverStyle.stroke ?? node.style?.stroke ?? "#ffffff");
5121
- circle.setAttribute("stroke-width", String(hoverStyle.strokeWidth ?? node.style?.strokeWidth ?? 1.5));
5122
- circle.setAttribute("opacity", String(hoverStyle.opacity ?? node.style?.opacity ?? 1));
5369
+ applyHoverStyles(circle, node, hoverStyle);
5123
5370
  }).on("mouseleave.hover", function(_event, node) {
5124
5371
  const circle = this;
5125
- if (circle.dataset.selected === "true") {
5126
- return;
5127
- }
5128
- circle.style.stroke = "";
5129
- circle.style.strokeWidth = "";
5130
- circle.style.opacity = "";
5131
- if (!node.style?.stroke) {
5132
- circle.setAttribute("stroke", "#ffffff");
5133
- } else {
5134
- circle.setAttribute("stroke", node.style.stroke);
5135
- }
5136
- if (!node.style?.strokeWidth) {
5137
- circle.setAttribute("stroke-width", "1.5");
5138
- } else {
5139
- circle.setAttribute("stroke-width", String(node.style.strokeWidth));
5140
- }
5141
- if (!node.style?.opacity) {
5142
- circle.setAttribute("opacity", "1");
5143
- } else {
5144
- circle.setAttribute("opacity", String(node.style.opacity));
5145
- }
5372
+ removeHoverStyles(circle, node);
5146
5373
  });
5147
5374
  }
5148
5375
  const svgElement = firstNode.ownerSVGElement;
@@ -5842,12 +6069,7 @@ var SelectionManager = class {
5842
6069
  this.clearSelection();
5843
6070
  this.bringNodeToFront(nodeElement, nodeData);
5844
6071
  if (this.config.nodeStyle) {
5845
- const style = this.config.nodeStyle;
5846
- if (style.fill !== void 0) nodeElement.style.fill = style.fill;
5847
- if (style.stroke !== void 0) nodeElement.style.stroke = style.stroke;
5848
- if (style.strokeWidth !== void 0) nodeElement.style.strokeWidth = String(style.strokeWidth);
5849
- if (style.opacity !== void 0) nodeElement.style.opacity = String(style.opacity);
5850
- if (style.radius !== void 0) nodeElement.style.setProperty("r", String(style.radius));
6072
+ applySelectionStyles(nodeElement, nodeData, this.config.nodeStyle);
5851
6073
  }
5852
6074
  this.root.selectAll(".link-label").filter((item) => {
5853
6075
  if (item.style.label.visibility !== "hover") return false;
@@ -5898,12 +6120,7 @@ var SelectionManager = class {
5898
6120
  if (!this.state.selectedNode) return;
5899
6121
  const { element, data } = this.state.selectedNode;
5900
6122
  this.restoreSelectedElements(data);
5901
- element.style.fill = "";
5902
- element.style.stroke = "";
5903
- element.style.strokeWidth = "";
5904
- element.style.opacity = "";
5905
- element.style.removeProperty("r");
5906
- delete element.dataset.selected;
6123
+ removeSelectionStyles(element, data);
5907
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");
5908
6125
  this.state.selectedNode = null;
5909
6126
  this.eventEmitter.emit("nodeDeselect", { node: data, element });
@@ -6128,18 +6345,42 @@ var DEFAULT_NODE_HOVER_STYLE = {
6128
6345
  };
6129
6346
  function resolveNodeStyle(params) {
6130
6347
  if (params.isSelected) {
6131
- return mergeNodeStyle(DEFAULT_NODE_HOVER_STYLE, params.interaction?.selection?.nodeStyle);
6348
+ return mergeNodeStyleSmart(DEFAULT_NODE_HOVER_STYLE, params.interaction?.selection?.nodeStyle);
6132
6349
  }
6133
6350
  if (params.isHovered) {
6134
- return mergeNodeStyle(DEFAULT_NODE_HOVER_STYLE, params.interaction?.hover?.nodeStyle);
6351
+ return mergeNodeStyleSmart(DEFAULT_NODE_HOVER_STYLE, params.interaction?.hover?.nodeStyle);
6135
6352
  }
6136
6353
  return void 0;
6137
6354
  }
6138
- function mergeNodeStyle(base, override) {
6139
- return {
6140
- ...base,
6141
- ...override
6142
- };
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;
6143
6384
  }
6144
6385
 
6145
6386
  // src/core/interaction-manager.ts
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,35 +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
- if (circle.dataset.selected === "true") {
5086
- return;
5087
- }
5088
- circle.setAttribute("stroke", hoverStyle.stroke ?? node.style?.stroke ?? "#ffffff");
5089
- circle.setAttribute("stroke-width", String(hoverStyle.strokeWidth ?? node.style?.strokeWidth ?? 1.5));
5090
- circle.setAttribute("opacity", String(hoverStyle.opacity ?? node.style?.opacity ?? 1));
5337
+ applyHoverStyles(circle, node, hoverStyle);
5091
5338
  }).on("mouseleave.hover", function(_event, node) {
5092
5339
  const circle = this;
5093
- if (circle.dataset.selected === "true") {
5094
- return;
5095
- }
5096
- circle.style.stroke = "";
5097
- circle.style.strokeWidth = "";
5098
- circle.style.opacity = "";
5099
- if (!node.style?.stroke) {
5100
- circle.setAttribute("stroke", "#ffffff");
5101
- } else {
5102
- circle.setAttribute("stroke", node.style.stroke);
5103
- }
5104
- if (!node.style?.strokeWidth) {
5105
- circle.setAttribute("stroke-width", "1.5");
5106
- } else {
5107
- circle.setAttribute("stroke-width", String(node.style.strokeWidth));
5108
- }
5109
- if (!node.style?.opacity) {
5110
- circle.setAttribute("opacity", "1");
5111
- } else {
5112
- circle.setAttribute("opacity", String(node.style.opacity));
5113
- }
5340
+ removeHoverStyles(circle, node);
5114
5341
  });
5115
5342
  }
5116
5343
  const svgElement = firstNode.ownerSVGElement;
@@ -5810,12 +6037,7 @@ var SelectionManager = class {
5810
6037
  this.clearSelection();
5811
6038
  this.bringNodeToFront(nodeElement, nodeData);
5812
6039
  if (this.config.nodeStyle) {
5813
- const style = this.config.nodeStyle;
5814
- if (style.fill !== void 0) nodeElement.style.fill = style.fill;
5815
- if (style.stroke !== void 0) nodeElement.style.stroke = style.stroke;
5816
- if (style.strokeWidth !== void 0) nodeElement.style.strokeWidth = String(style.strokeWidth);
5817
- if (style.opacity !== void 0) nodeElement.style.opacity = String(style.opacity);
5818
- if (style.radius !== void 0) nodeElement.style.setProperty("r", String(style.radius));
6040
+ applySelectionStyles(nodeElement, nodeData, this.config.nodeStyle);
5819
6041
  }
5820
6042
  this.root.selectAll(".link-label").filter((item) => {
5821
6043
  if (item.style.label.visibility !== "hover") return false;
@@ -5866,12 +6088,7 @@ var SelectionManager = class {
5866
6088
  if (!this.state.selectedNode) return;
5867
6089
  const { element, data } = this.state.selectedNode;
5868
6090
  this.restoreSelectedElements(data);
5869
- element.style.fill = "";
5870
- element.style.stroke = "";
5871
- element.style.strokeWidth = "";
5872
- element.style.opacity = "";
5873
- element.style.removeProperty("r");
5874
- delete element.dataset.selected;
6091
+ removeSelectionStyles(element, data);
5875
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");
5876
6093
  this.state.selectedNode = null;
5877
6094
  this.eventEmitter.emit("nodeDeselect", { node: data, element });
@@ -6096,18 +6313,42 @@ var DEFAULT_NODE_HOVER_STYLE = {
6096
6313
  };
6097
6314
  function resolveNodeStyle(params) {
6098
6315
  if (params.isSelected) {
6099
- return mergeNodeStyle(DEFAULT_NODE_HOVER_STYLE, params.interaction?.selection?.nodeStyle);
6316
+ return mergeNodeStyleSmart(DEFAULT_NODE_HOVER_STYLE, params.interaction?.selection?.nodeStyle);
6100
6317
  }
6101
6318
  if (params.isHovered) {
6102
- return mergeNodeStyle(DEFAULT_NODE_HOVER_STYLE, params.interaction?.hover?.nodeStyle);
6319
+ return mergeNodeStyleSmart(DEFAULT_NODE_HOVER_STYLE, params.interaction?.hover?.nodeStyle);
6103
6320
  }
6104
6321
  return void 0;
6105
6322
  }
6106
- function mergeNodeStyle(base, override) {
6107
- return {
6108
- ...base,
6109
- ...override
6110
- };
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;
6111
6352
  }
6112
6353
 
6113
6354
  // src/core/interaction-manager.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polly-graph",
3
- "version": "0.1.10",
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",