polly-graph 0.1.4 → 0.1.5

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
@@ -4,14 +4,10 @@ Reusable D3-based graph visualization SDK with configurable nodes, links, labels
4
4
 
5
5
  ## Features
6
6
 
7
- * Interactive force-directed graph rendering
8
- * Configurable nodes, links, and labels
9
- * Hover states with tooltip support
10
- * Node selection support
11
- * Drag and zoom interactions
12
- * Arrow markers and relationship visualization
13
- * Framework-agnostic API
14
- * Angular and React compatible
7
+ * **Managed Root Architecture**: Provide one host element; the SDK internally manages the SVG canvas and the HTML UI overlay.
8
+ * **Smart Positioning**: Controls and legends use a class-based system (`pg-pos-top-right`) with CSS variable overrides for precision offsets.
9
+ * **Declarative Styling**: Fully customizable node and link aesthetics via style objects.
10
+ * **Animated UI**: Legends feature directional "retraction" animations that sync with their anchor position.
15
11
 
16
12
  ---
17
13
 
@@ -21,89 +17,46 @@ Reusable D3-based graph visualization SDK with configurable nodes, links, labels
21
17
  npm install polly-graph
22
18
  ```
23
19
 
24
- No separate D3 installation is required.
25
-
26
20
  ---
27
21
 
28
22
  ## Basic Usage
29
23
 
30
24
  ### HTML
31
-
32
25
  ```html
33
- <div class="graph-wrapper">
34
- <svg id="graph-container"></svg>
35
- </div>
26
+ <div id="graph-viewport" style="position: relative; width: 100%; height: 600px;"></div>
36
27
  ```
37
28
 
38
29
  ### TypeScript
39
-
40
30
  ```ts
41
- import {
42
- createGraph,
43
- GraphNode,
44
- GraphLink,
45
- } from 'polly-graph';
46
-
47
- const container = document.getElementById(
48
- 'graph-container',
49
- ) as SVGSVGElement;
50
-
51
- const nodes: GraphNode[] = [
52
- {
53
- id: 'users',
54
- label: 'Users',
55
- style: {
56
- radius: 24,
57
- fill: '#7c3aed',
58
- stroke: '#6d28d9',
59
- strokeWidth: 2,
60
- textColor: '#ffffff',
61
- },
62
- },
63
- {
64
- id: 'orders',
65
- label: 'Orders',
66
- style: {
67
- radius: 24,
68
- fill: '#2563eb',
69
- stroke: '#1d4ed8',
70
- strokeWidth: 2,
71
- textColor: '#ffffff',
72
- },
73
- },
74
- ];
75
-
76
- const links: GraphLink[] = [
77
- {
78
- source: 'users',
79
- target: 'orders',
80
- label: 'has_many',
81
- style: {
82
- stroke: '#94a3b8',
83
- strokeWidth: 2,
84
- opacity: 1,
85
- },
86
- },
87
- ];
31
+ import { createGraph } from 'polly-graph';
32
+
33
+ const viewport = document.getElementById('graph-viewport') as HTMLElement;
88
34
 
89
35
  const graph = createGraph({
90
- container,
91
- nodes,
92
- links,
93
- interaction: {
94
- drag: {
95
- enabled: true,
96
- },
97
- hover: {
98
- enabled: true,
99
- tooltip: {
100
- enabled: true,
101
- },
102
- },
103
- selection: {
104
- enabled: true,
105
- },
106
- },
36
+ container: viewport,
37
+ nodes: [
38
+ {
39
+ id: 'n1',
40
+ label: 'Core Service',
41
+ style: {
42
+ radius: 30,
43
+ fill: '#7c3aed',
44
+ stroke: '#5b21b6',
45
+ strokeWidth: 2,
46
+ textColor: '#ffffff',
47
+ fontSize: 12
48
+ }
49
+ }
50
+ ],
51
+ links: [
52
+ {
53
+ source: 'n1',
54
+ target: 'n2',
55
+ style: { stroke: '#94a3b8', strokeWidth: 2, opacity: 0.6 }
56
+ }
57
+ ],
58
+ controls: { enabled: true, position: 'top-right' },
59
+ legend: { enabled: true, position: 'bottom-left' }
107
60
  });
108
61
 
109
62
  graph.render();
@@ -111,107 +64,77 @@ graph.render();
111
64
 
112
65
  ---
113
66
 
114
- ## Hover + Tooltip Example
115
-
116
- ```ts
117
- interaction: {
118
- hover: {
119
- enabled: true,
120
- tooltip: {
121
- enabled: true,
122
- theme: 'dark',
123
- },
124
- nodeStyle: {
125
- stroke: '#16a34a',
126
- strokeWidth: 3,
127
- opacity: 1,
128
- },
129
- },
130
- }
131
- ```
132
-
133
- ---
67
+ ## Styling & Customization
134
68
 
135
- ## Selection Example
69
+ ### Node Styles
70
+ Every node can have a unique appearance defined in its `style` object.
71
+ | Property | Type | Description |
72
+ | :--- | :--- | :--- |
73
+ | `radius` | `number` | The size of the node. |
74
+ | `fill` | `string` | Background color (hex/rgb). |
75
+ | `stroke` | `string` | Border color. |
76
+ | `strokeWidth`| `number` | Thickness of the border. |
77
+ | `textColor` | `string` | Label color. |
136
78
 
79
+ ### Link Styles
80
+ Links support custom coloring, thickness, and arrow markers.
137
81
  ```ts
138
- selection: {
139
- enabled: true,
140
- multiSelect: false,
141
- nodeStyle: {
142
- stroke: '#f59e0b',
143
- strokeWidth: 4,
144
- opacity: 1,
145
- },
82
+ style: {
83
+ stroke: '#cbd5e1',
84
+ strokeWidth: 1.5,
85
+ opacity: 0.8,
86
+ dashed: false // Coming soon
146
87
  }
147
88
  ```
148
89
 
149
90
  ---
150
91
 
151
- ## Angular Example
92
+ ## Positioning Logic
152
93
 
153
- ### Template
94
+ The UI components (Controls & Legend) use a hybrid positioning system.
154
95
 
155
- ```html
156
- <div class="graph-wrapper">
157
- <svg #svgRef></svg>
158
- </div>
159
- ```
96
+ ### 1. Corner Anchoring
97
+ Use the `position` property to anchor elements to viewport corners. The SDK applies classes like `.pg-pos-top-right` which handles the layout logic.
160
98
 
161
- ### Component
99
+ Available positions:
100
+ * `top-left`
101
+ * `top-right`
102
+ * `bottom-left`
103
+ * `bottom-right`
162
104
 
163
- ```ts
164
- this.graph = createGraph({
165
- container: this.svgRef.nativeElement,
166
- nodes,
167
- links,
168
- interaction,
169
- });
105
+ ### 2. Custom Offsets
106
+ While the corners provide the anchor, you can use the `offset` object for fine-tuning. This values are passed into CSS variables `--pg-offset-x` and `--pg-offset-y` internally.
170
107
 
171
- this.graph.render();
108
+ ```ts
109
+ controls: {
110
+ position: 'top-right',
111
+ offset: { x: 24, y: 24 } // 24px away from the top and right edges
112
+ }
172
113
  ```
173
114
 
174
- ---
175
-
176
- ## Public API
177
-
178
- ### Main Exports
179
-
180
- * `createGraph`
181
- * `GraphInstance`
182
- * `GraphNode`
183
- * `GraphLink`
184
- * `GraphConfig`
185
- * `GraphInteractionConfig`
115
+ ### 3. Directional Legend Retraction
116
+ The Legend component is aware of its position.
117
+ * If anchored **Left**: It collapses to the left; the toggle icon points **Right** to expand.
118
+ * If anchored **Right**: It collapses to the right; the toggle icon points **Left** to expand.
186
119
 
187
120
  ---
188
121
 
189
- ## Development
122
+ ## Framework Integration
190
123
 
191
- ```bash
192
- npm install
193
- npm run dev
194
- npm run build
195
- npm run lint
196
- npm run typecheck
197
- npm run test
124
+ ### Angular
125
+ ```ts
126
+ @ViewChild('viewport') viewport!: ElementRef;
127
+
128
+ ngAfterViewInit() {
129
+ this.graph = createGraph({
130
+ container: this.viewport.nativeElement,
131
+ // ... config
132
+ });
133
+ this.graph.render();
134
+ }
198
135
  ```
199
136
 
200
137
  ---
201
138
 
202
- ## Publish Checklist
203
-
204
- * Package name available on npm
205
- * README completed
206
- * LICENSE added
207
- * Build succeeds
208
- * Tests pass
209
- * npm token configured
210
- * GitHub workflow passes
211
- * Repository is public
212
-
213
- ---
214
-
215
139
  ## License
216
-
217
- MIT
140
+ MIT
package/dist/index.cjs CHANGED
@@ -3644,68 +3644,6 @@ function manyBody_default() {
3644
3644
  return force;
3645
3645
  }
3646
3646
 
3647
- // node_modules/d3-force/src/x.js
3648
- function x_default2(x3) {
3649
- var strength = constant_default5(0.1), nodes, strengths, xz;
3650
- if (typeof x3 !== "function") x3 = constant_default5(x3 == null ? 0 : +x3);
3651
- function force(alpha) {
3652
- for (var i = 0, n = nodes.length, node; i < n; ++i) {
3653
- node = nodes[i], node.vx += (xz[i] - node.x) * strengths[i] * alpha;
3654
- }
3655
- }
3656
- function initialize() {
3657
- if (!nodes) return;
3658
- var i, n = nodes.length;
3659
- strengths = new Array(n);
3660
- xz = new Array(n);
3661
- for (i = 0; i < n; ++i) {
3662
- strengths[i] = isNaN(xz[i] = +x3(nodes[i], i, nodes)) ? 0 : +strength(nodes[i], i, nodes);
3663
- }
3664
- }
3665
- force.initialize = function(_) {
3666
- nodes = _;
3667
- initialize();
3668
- };
3669
- force.strength = function(_) {
3670
- return arguments.length ? (strength = typeof _ === "function" ? _ : constant_default5(+_), initialize(), force) : strength;
3671
- };
3672
- force.x = function(_) {
3673
- return arguments.length ? (x3 = typeof _ === "function" ? _ : constant_default5(+_), initialize(), force) : x3;
3674
- };
3675
- return force;
3676
- }
3677
-
3678
- // node_modules/d3-force/src/y.js
3679
- function y_default2(y3) {
3680
- var strength = constant_default5(0.1), nodes, strengths, yz;
3681
- if (typeof y3 !== "function") y3 = constant_default5(y3 == null ? 0 : +y3);
3682
- function force(alpha) {
3683
- for (var i = 0, n = nodes.length, node; i < n; ++i) {
3684
- node = nodes[i], node.vy += (yz[i] - node.y) * strengths[i] * alpha;
3685
- }
3686
- }
3687
- function initialize() {
3688
- if (!nodes) return;
3689
- var i, n = nodes.length;
3690
- strengths = new Array(n);
3691
- yz = new Array(n);
3692
- for (i = 0; i < n; ++i) {
3693
- strengths[i] = isNaN(yz[i] = +y3(nodes[i], i, nodes)) ? 0 : +strength(nodes[i], i, nodes);
3694
- }
3695
- }
3696
- force.initialize = function(_) {
3697
- nodes = _;
3698
- initialize();
3699
- };
3700
- force.strength = function(_) {
3701
- return arguments.length ? (strength = typeof _ === "function" ? _ : constant_default5(+_), initialize(), force) : strength;
3702
- };
3703
- force.y = function(_) {
3704
- return arguments.length ? (y3 = typeof _ === "function" ? _ : constant_default5(+_), initialize(), force) : y3;
3705
- };
3706
- return force;
3707
- }
3708
-
3709
3647
  // src/core/create-graph-layers.ts
3710
3648
  function createGraphLayers(host) {
3711
3649
  host.innerHTML = "";
@@ -3814,10 +3752,17 @@ function createGraphSimulation(config) {
3814
3752
  });
3815
3753
  const simulation = simulation_default(config.nodes).alpha(0.9).alphaDecay(0.12).alphaMin(0.03).velocityDecay(0.5).force(
3816
3754
  "link",
3817
- link_default(config.links).id((d) => d.id).distance(150).strength(0.4)
3755
+ link_default(config.links).id((d) => d.id).distance((d) => {
3756
+ const source = d.source;
3757
+ const target = d.target;
3758
+ const sourceR = source.style?.radius || 20;
3759
+ const targetR = target.style?.radius || 20;
3760
+ const labelBuffer = d.style?.label?.height || 40;
3761
+ return (sourceR + targetR + labelBuffer) * 2;
3762
+ }).strength(0.8)
3818
3763
  ).force("charge", manyBody_default().strength(-220)).force(
3819
3764
  "collide",
3820
- collide_default().radius((node) => (node.style?.radius ?? 12) + 10).strength(0.9)
3765
+ collide_default().radius((node) => (node.style?.radius ?? 12) + 10).iterations(2)
3821
3766
  ).force("center", center_default(centerX, centerY).strength(0.08));
3822
3767
  return { simulation };
3823
3768
  }
@@ -4000,6 +3945,7 @@ var DEFAULT_LINK_STYLE = {
4000
3945
  },
4001
3946
  label: {
4002
3947
  enabled: true,
3948
+ visibility: "always",
4003
3949
  backgroundFill: "color-mix(in srgb, #8E42EE, #FFFFFF 90%)",
4004
3950
  borderColor: "color-mix(in srgb, #8E42EE, #FFFFFF 10%)",
4005
3951
  borderWidth: 1.5,
@@ -4035,6 +3981,7 @@ function mergeLinkStyle(base, override) {
4035
3981
  },
4036
3982
  label: {
4037
3983
  enabled: override?.label?.enabled ?? base.label.enabled,
3984
+ visibility: override?.label?.visibility ?? base.label.visibility,
4038
3985
  backgroundFill: override?.label?.backgroundFill ?? base.label.backgroundFill,
4039
3986
  borderColor: override?.label?.borderColor ?? base.label.borderColor,
4040
3987
  borderWidth: override?.label?.borderWidth ?? base.label.borderWidth,
@@ -4135,7 +4082,14 @@ function getLinkKey(link) {
4135
4082
  }
4136
4083
  function renderLinks(ctx, links) {
4137
4084
  const renderableLinks = createRenderableLinks(ctx, links);
4138
- return ctx.root.select('[data-layer="links"]').selectAll("line").data(renderableLinks, (item) => getLinkKey(item.link)).join("line").attr("stroke", (item) => item.style.stroke).attr("stroke-width", (item) => item.style.strokeWidth).attr("opacity", (item) => item.style.opacity).attr("marker-end", (item) => item.markerEnd);
4085
+ const linkSelection = ctx.root.select('[data-layer="links"]').selectAll("line").data(renderableLinks, (item) => getLinkKey(item.link)).join("line").attr("class", "graph-link").attr("stroke", (item) => item.style.stroke).attr("stroke-width", (item) => item.style.strokeWidth).attr("opacity", (item) => item.style.opacity).attr("marker-end", (item) => item.markerEnd).style("pointer-events", "stroke");
4086
+ const labelSelection = ctx.root.selectAll(".link-label");
4087
+ linkSelection.on("mouseenter.label-hover", (_event, d) => {
4088
+ labelSelection.filter((labelItem) => labelItem.link === d.link && labelItem.style.label.visibility === "hover").interrupt().transition().duration(200).style("opacity", 1);
4089
+ }).on("mouseleave.label-hover", (_event, d) => {
4090
+ labelSelection.filter((labelItem) => labelItem.link === d.link && labelItem.style.label.visibility === "hover").interrupt().transition().duration(200).style("opacity", 0);
4091
+ });
4092
+ return linkSelection;
4139
4093
  }
4140
4094
 
4141
4095
  // src/renderer/nodes.ts
@@ -4205,7 +4159,10 @@ function renderNodeLabels(ctx, nodes) {
4205
4159
  // src/renderer/link-labels.ts
4206
4160
  function createRenderableLinks2(params, links) {
4207
4161
  return links.map(
4208
- (link) => ({ link, style: resolveLinkStyle({ link, interaction: params.interaction }) })
4162
+ (link) => ({
4163
+ link,
4164
+ style: resolveLinkStyle({ link, interaction: params.interaction })
4165
+ })
4209
4166
  ).filter(
4210
4167
  (item) => item.style.label.enabled && Boolean(item.link.label)
4211
4168
  );
@@ -4217,7 +4174,13 @@ function getLinkKey2(link) {
4217
4174
  }
4218
4175
  function renderLinkLabels(params, links) {
4219
4176
  const renderableLinks = createRenderableLinks2(params, links);
4220
- const labelSelection = params.root.select('[data-layer="link-labels"]').selectAll(".link-label").data(renderableLinks, (item) => getLinkKey2(item.link)).join("g").attr("class", "link-label").attr("pointer-events", "auto").attr("cursor", "pointer");
4177
+ 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) => {
4178
+ const visibility = item.style.label.visibility ?? "always";
4179
+ return visibility === "always" ? 1 : 0;
4180
+ }).style("pointer-events", (item) => {
4181
+ const visibility = item.style.label.visibility ?? "always";
4182
+ return visibility === "always" ? "auto" : "none";
4183
+ }).style("cursor", "pointer");
4221
4184
  labelSelection.selectAll("rect").data((item) => [item]).join("rect").attr("rx", (item) => item.style.label.borderRadius).attr("ry", (item) => item.style.label.borderRadius).attr("height", (item) => item.style.label.height).attr("fill", (item) => item.style.label.backgroundFill).attr("stroke", (item) => item.style.label.borderColor).attr("stroke-width", (item) => item.style.label.borderWidth);
4222
4185
  labelSelection.selectAll("text").data((item) => [item]).join("text").attr("text-anchor", "middle").attr("dominant-baseline", "middle").attr("font-size", (item) => item.style.label.fontSize).attr("fill", (item) => item.style.label.textColor).text((item) => item.link.label ?? "");
4223
4186
  return labelSelection;
@@ -4246,58 +4209,35 @@ function createDragBehavior(simulation) {
4246
4209
 
4247
4210
  // src/interactions/create-node-hover.ts
4248
4211
  function createNodeHover(nodeSelection, hoverStyle) {
4249
- if (!hoverStyle) {
4250
- return;
4251
- }
4252
- nodeSelection.on(
4253
- "mouseenter.hover",
4254
- function(_event, node) {
4212
+ const firstNode = nodeSelection.node();
4213
+ if (!firstNode) return;
4214
+ if (hoverStyle) {
4215
+ nodeSelection.on("mouseenter.hover", function(_event, node) {
4255
4216
  const circle = this;
4256
- const hoverStroke = hoverStyle.stroke ?? node.style?.stroke ?? "#ffffff";
4257
- const hoverStrokeWidth = hoverStyle.strokeWidth ?? node.style?.strokeWidth ?? 1.5;
4258
- const hoverOpacity = hoverStyle.opacity ?? node.style?.opacity ?? 1;
4259
- circle.setAttribute(
4260
- "stroke",
4261
- hoverStroke
4262
- );
4263
- circle.setAttribute(
4264
- "stroke-width",
4265
- String(
4266
- hoverStrokeWidth
4267
- )
4268
- );
4269
- circle.setAttribute(
4270
- "opacity",
4271
- String(
4272
- hoverOpacity
4273
- )
4274
- );
4275
- }
4276
- ).on(
4277
- "mouseleave.hover",
4278
- function(_event, node) {
4217
+ circle.setAttribute("stroke", hoverStyle.stroke ?? node.style?.stroke ?? "#ffffff");
4218
+ circle.setAttribute("stroke-width", String(hoverStyle.strokeWidth ?? node.style?.strokeWidth ?? 1.5));
4219
+ circle.setAttribute("opacity", String(hoverStyle.opacity ?? node.style?.opacity ?? 1));
4220
+ }).on("mouseleave.hover", function(_event, node) {
4279
4221
  const circle = this;
4280
- const defaultStroke = node.style?.stroke ?? "#ffffff";
4281
- const defaultStrokeWidth = node.style?.strokeWidth ?? 1.5;
4282
- const defaultOpacity = node.style?.opacity ?? 1;
4283
- circle.setAttribute(
4284
- "stroke",
4285
- defaultStroke
4286
- );
4287
- circle.setAttribute(
4288
- "stroke-width",
4289
- String(
4290
- defaultStrokeWidth
4291
- )
4292
- );
4293
- circle.setAttribute(
4294
- "opacity",
4295
- String(
4296
- defaultOpacity
4297
- )
4298
- );
4299
- }
4300
- );
4222
+ circle.setAttribute("stroke", node.style?.stroke ?? "#ffffff");
4223
+ circle.setAttribute("stroke-width", String(node.style?.strokeWidth ?? 1.5));
4224
+ circle.setAttribute("opacity", String(node.style?.opacity ?? 1));
4225
+ });
4226
+ }
4227
+ const svgElement = firstNode.ownerSVGElement;
4228
+ if (!svgElement) return;
4229
+ const root2 = select_default2(svgElement);
4230
+ const labelSelection = root2.selectAll(".link-label");
4231
+ nodeSelection.on("mouseenter.labels", (_event, d) => {
4232
+ labelSelection.filter((item) => {
4233
+ if (item.style.label.visibility !== "hover") return false;
4234
+ const s = item.link.source;
4235
+ const t = item.link.target;
4236
+ return s.id === d.id || t.id === d.id;
4237
+ }).interrupt().transition().duration(200).style("opacity", 1).style("pointer-events", "auto");
4238
+ }).on("mouseleave.labels", (_event) => {
4239
+ labelSelection.filter((item) => item.style.label.visibility === "hover").interrupt().transition().duration(200).style("opacity", 0).style("pointer-events", "none");
4240
+ });
4301
4241
  }
4302
4242
 
4303
4243
  // src/utils/resolve-tooltip-position.ts
@@ -4589,6 +4529,7 @@ function createGraph(config) {
4589
4529
  let tooltipBinding = null;
4590
4530
  let controls = null;
4591
4531
  let legendCleanup = null;
4532
+ let fitViewTimer = null;
4592
4533
  let dimensions = { width: 0, height: 0 };
4593
4534
  let rootGroup = null;
4594
4535
  let svgElement = null;
@@ -4605,9 +4546,17 @@ function createGraph(config) {
4605
4546
  layers.svg.setAttribute("height", String(height));
4606
4547
  layers.interactionRect.setAttribute("width", String(width));
4607
4548
  layers.interactionRect.setAttribute("height", String(height));
4608
- simulation?.force("x", x_default2(width / 2).strength(0.03));
4609
- simulation?.force("y", y_default2(height / 2).strength(0.03));
4610
- simulation?.alpha(0.25).restart();
4549
+ if (simulation) {
4550
+ simulation.force("center", center_default(width / 2, height / 2));
4551
+ simulation.alpha(0.3).restart();
4552
+ }
4553
+ if (fitViewTimer) {
4554
+ clearTimeout(fitViewTimer);
4555
+ }
4556
+ fitViewTimer = setTimeout(() => {
4557
+ fitView();
4558
+ fitViewTimer = null;
4559
+ }, 150);
4611
4560
  });
4612
4561
  const zoomResult = createZoom({
4613
4562
  svg: layers.svg,
@@ -4625,8 +4574,9 @@ function createGraph(config) {
4625
4574
  const simulationConfig = {
4626
4575
  nodes: config.nodes,
4627
4576
  links: config.links,
4628
- width: config.container.clientWidth,
4629
- height: config.container.clientHeight
4577
+ // Uses the observed dimensions to ensure physics are calculated on actual container size
4578
+ width: dimensions.width || config.container.clientWidth,
4579
+ height: dimensions.height || config.container.clientHeight
4630
4580
  };
4631
4581
  const simulationResult = createGraphSimulation(simulationConfig);
4632
4582
  simulation = simulationResult.simulation;
@@ -4682,7 +4632,7 @@ function createGraph(config) {
4682
4632
  }
4683
4633
  function resetView() {
4684
4634
  if (!zoomBehavior || !svgElement) return;
4685
- select_default2(svgElement).transition().call(zoomBehavior.transform, identity2);
4635
+ select_default2(svgElement).transition().duration(400).call(zoomBehavior.transform, identity2);
4686
4636
  }
4687
4637
  function fitView() {
4688
4638
  if (!zoomBehavior || !rootGroup || !svgElement || dimensions.width === 0 || dimensions.height === 0) return;
@@ -4692,7 +4642,7 @@ function createGraph(config) {
4692
4642
  const translateX = (dimensions.width - bounds.width * scale) / 2 - bounds.x * scale;
4693
4643
  const translateY = (dimensions.height - bounds.height * scale) / 2 - bounds.y * scale;
4694
4644
  const transform2 = identity2.translate(translateX, translateY).scale(scale);
4695
- select_default2(svgElement).transition().call(zoomBehavior.transform, transform2);
4645
+ select_default2(svgElement).transition().duration(400).call(zoomBehavior.transform, transform2);
4696
4646
  }
4697
4647
  function zoomIn() {
4698
4648
  if (!zoomBehavior || !svgElement) return;
@@ -4711,6 +4661,10 @@ function createGraph(config) {
4711
4661
  });
4712
4662
  }
4713
4663
  function destroy() {
4664
+ if (fitViewTimer) {
4665
+ clearTimeout(fitViewTimer);
4666
+ fitViewTimer = null;
4667
+ }
4714
4668
  if (cleanupResize) {
4715
4669
  cleanupResize();
4716
4670
  cleanupResize = null;
package/dist/index.d.cts CHANGED
@@ -66,6 +66,7 @@ interface LinkArrowStyle {
66
66
  }
67
67
  interface LinkLabelStyle {
68
68
  readonly enabled?: boolean;
69
+ readonly visibility?: 'always' | 'hover' | 'selection';
69
70
  readonly backgroundFill?: string;
70
71
  readonly borderColor?: string;
71
72
  readonly borderWidth?: number;
@@ -117,6 +118,13 @@ interface GraphConfig {
117
118
  readonly container: HTMLElement;
118
119
  readonly nodes: GraphNode[];
119
120
  readonly links: GraphLink[];
121
+ readonly autoFit?: boolean;
122
+ readonly responsive?: boolean;
123
+ readonly simulation?: {
124
+ readonly alpha?: number;
125
+ readonly gravity?: number;
126
+ readonly linkDistance?: number | ((link: GraphLink) => number);
127
+ };
120
128
  readonly interaction?: GraphInteractionConfig;
121
129
  readonly controls?: GraphControlsConfig;
122
130
  readonly legend?: LegendConfig;
package/dist/index.d.ts CHANGED
@@ -66,6 +66,7 @@ interface LinkArrowStyle {
66
66
  }
67
67
  interface LinkLabelStyle {
68
68
  readonly enabled?: boolean;
69
+ readonly visibility?: 'always' | 'hover' | 'selection';
69
70
  readonly backgroundFill?: string;
70
71
  readonly borderColor?: string;
71
72
  readonly borderWidth?: number;
@@ -117,6 +118,13 @@ interface GraphConfig {
117
118
  readonly container: HTMLElement;
118
119
  readonly nodes: GraphNode[];
119
120
  readonly links: GraphLink[];
121
+ readonly autoFit?: boolean;
122
+ readonly responsive?: boolean;
123
+ readonly simulation?: {
124
+ readonly alpha?: number;
125
+ readonly gravity?: number;
126
+ readonly linkDistance?: number | ((link: GraphLink) => number);
127
+ };
120
128
  readonly interaction?: GraphInteractionConfig;
121
129
  readonly controls?: GraphControlsConfig;
122
130
  readonly legend?: LegendConfig;
package/dist/index.js CHANGED
@@ -3608,68 +3608,6 @@ function manyBody_default() {
3608
3608
  return force;
3609
3609
  }
3610
3610
 
3611
- // node_modules/d3-force/src/x.js
3612
- function x_default2(x3) {
3613
- var strength = constant_default5(0.1), nodes, strengths, xz;
3614
- if (typeof x3 !== "function") x3 = constant_default5(x3 == null ? 0 : +x3);
3615
- function force(alpha) {
3616
- for (var i = 0, n = nodes.length, node; i < n; ++i) {
3617
- node = nodes[i], node.vx += (xz[i] - node.x) * strengths[i] * alpha;
3618
- }
3619
- }
3620
- function initialize() {
3621
- if (!nodes) return;
3622
- var i, n = nodes.length;
3623
- strengths = new Array(n);
3624
- xz = new Array(n);
3625
- for (i = 0; i < n; ++i) {
3626
- strengths[i] = isNaN(xz[i] = +x3(nodes[i], i, nodes)) ? 0 : +strength(nodes[i], i, nodes);
3627
- }
3628
- }
3629
- force.initialize = function(_) {
3630
- nodes = _;
3631
- initialize();
3632
- };
3633
- force.strength = function(_) {
3634
- return arguments.length ? (strength = typeof _ === "function" ? _ : constant_default5(+_), initialize(), force) : strength;
3635
- };
3636
- force.x = function(_) {
3637
- return arguments.length ? (x3 = typeof _ === "function" ? _ : constant_default5(+_), initialize(), force) : x3;
3638
- };
3639
- return force;
3640
- }
3641
-
3642
- // node_modules/d3-force/src/y.js
3643
- function y_default2(y3) {
3644
- var strength = constant_default5(0.1), nodes, strengths, yz;
3645
- if (typeof y3 !== "function") y3 = constant_default5(y3 == null ? 0 : +y3);
3646
- function force(alpha) {
3647
- for (var i = 0, n = nodes.length, node; i < n; ++i) {
3648
- node = nodes[i], node.vy += (yz[i] - node.y) * strengths[i] * alpha;
3649
- }
3650
- }
3651
- function initialize() {
3652
- if (!nodes) return;
3653
- var i, n = nodes.length;
3654
- strengths = new Array(n);
3655
- yz = new Array(n);
3656
- for (i = 0; i < n; ++i) {
3657
- strengths[i] = isNaN(yz[i] = +y3(nodes[i], i, nodes)) ? 0 : +strength(nodes[i], i, nodes);
3658
- }
3659
- }
3660
- force.initialize = function(_) {
3661
- nodes = _;
3662
- initialize();
3663
- };
3664
- force.strength = function(_) {
3665
- return arguments.length ? (strength = typeof _ === "function" ? _ : constant_default5(+_), initialize(), force) : strength;
3666
- };
3667
- force.y = function(_) {
3668
- return arguments.length ? (y3 = typeof _ === "function" ? _ : constant_default5(+_), initialize(), force) : y3;
3669
- };
3670
- return force;
3671
- }
3672
-
3673
3611
  // src/core/create-graph-layers.ts
3674
3612
  function createGraphLayers(host) {
3675
3613
  host.innerHTML = "";
@@ -3778,10 +3716,17 @@ function createGraphSimulation(config) {
3778
3716
  });
3779
3717
  const simulation = simulation_default(config.nodes).alpha(0.9).alphaDecay(0.12).alphaMin(0.03).velocityDecay(0.5).force(
3780
3718
  "link",
3781
- link_default(config.links).id((d) => d.id).distance(150).strength(0.4)
3719
+ link_default(config.links).id((d) => d.id).distance((d) => {
3720
+ const source = d.source;
3721
+ const target = d.target;
3722
+ const sourceR = source.style?.radius || 20;
3723
+ const targetR = target.style?.radius || 20;
3724
+ const labelBuffer = d.style?.label?.height || 40;
3725
+ return (sourceR + targetR + labelBuffer) * 2;
3726
+ }).strength(0.8)
3782
3727
  ).force("charge", manyBody_default().strength(-220)).force(
3783
3728
  "collide",
3784
- collide_default().radius((node) => (node.style?.radius ?? 12) + 10).strength(0.9)
3729
+ collide_default().radius((node) => (node.style?.radius ?? 12) + 10).iterations(2)
3785
3730
  ).force("center", center_default(centerX, centerY).strength(0.08));
3786
3731
  return { simulation };
3787
3732
  }
@@ -3964,6 +3909,7 @@ var DEFAULT_LINK_STYLE = {
3964
3909
  },
3965
3910
  label: {
3966
3911
  enabled: true,
3912
+ visibility: "always",
3967
3913
  backgroundFill: "color-mix(in srgb, #8E42EE, #FFFFFF 90%)",
3968
3914
  borderColor: "color-mix(in srgb, #8E42EE, #FFFFFF 10%)",
3969
3915
  borderWidth: 1.5,
@@ -3999,6 +3945,7 @@ function mergeLinkStyle(base, override) {
3999
3945
  },
4000
3946
  label: {
4001
3947
  enabled: override?.label?.enabled ?? base.label.enabled,
3948
+ visibility: override?.label?.visibility ?? base.label.visibility,
4002
3949
  backgroundFill: override?.label?.backgroundFill ?? base.label.backgroundFill,
4003
3950
  borderColor: override?.label?.borderColor ?? base.label.borderColor,
4004
3951
  borderWidth: override?.label?.borderWidth ?? base.label.borderWidth,
@@ -4099,7 +4046,14 @@ function getLinkKey(link) {
4099
4046
  }
4100
4047
  function renderLinks(ctx, links) {
4101
4048
  const renderableLinks = createRenderableLinks(ctx, links);
4102
- return ctx.root.select('[data-layer="links"]').selectAll("line").data(renderableLinks, (item) => getLinkKey(item.link)).join("line").attr("stroke", (item) => item.style.stroke).attr("stroke-width", (item) => item.style.strokeWidth).attr("opacity", (item) => item.style.opacity).attr("marker-end", (item) => item.markerEnd);
4049
+ const linkSelection = ctx.root.select('[data-layer="links"]').selectAll("line").data(renderableLinks, (item) => getLinkKey(item.link)).join("line").attr("class", "graph-link").attr("stroke", (item) => item.style.stroke).attr("stroke-width", (item) => item.style.strokeWidth).attr("opacity", (item) => item.style.opacity).attr("marker-end", (item) => item.markerEnd).style("pointer-events", "stroke");
4050
+ const labelSelection = ctx.root.selectAll(".link-label");
4051
+ linkSelection.on("mouseenter.label-hover", (_event, d) => {
4052
+ labelSelection.filter((labelItem) => labelItem.link === d.link && labelItem.style.label.visibility === "hover").interrupt().transition().duration(200).style("opacity", 1);
4053
+ }).on("mouseleave.label-hover", (_event, d) => {
4054
+ labelSelection.filter((labelItem) => labelItem.link === d.link && labelItem.style.label.visibility === "hover").interrupt().transition().duration(200).style("opacity", 0);
4055
+ });
4056
+ return linkSelection;
4103
4057
  }
4104
4058
 
4105
4059
  // src/renderer/nodes.ts
@@ -4169,7 +4123,10 @@ function renderNodeLabels(ctx, nodes) {
4169
4123
  // src/renderer/link-labels.ts
4170
4124
  function createRenderableLinks2(params, links) {
4171
4125
  return links.map(
4172
- (link) => ({ link, style: resolveLinkStyle({ link, interaction: params.interaction }) })
4126
+ (link) => ({
4127
+ link,
4128
+ style: resolveLinkStyle({ link, interaction: params.interaction })
4129
+ })
4173
4130
  ).filter(
4174
4131
  (item) => item.style.label.enabled && Boolean(item.link.label)
4175
4132
  );
@@ -4181,7 +4138,13 @@ function getLinkKey2(link) {
4181
4138
  }
4182
4139
  function renderLinkLabels(params, links) {
4183
4140
  const renderableLinks = createRenderableLinks2(params, links);
4184
- const labelSelection = params.root.select('[data-layer="link-labels"]').selectAll(".link-label").data(renderableLinks, (item) => getLinkKey2(item.link)).join("g").attr("class", "link-label").attr("pointer-events", "auto").attr("cursor", "pointer");
4141
+ 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) => {
4142
+ const visibility = item.style.label.visibility ?? "always";
4143
+ return visibility === "always" ? 1 : 0;
4144
+ }).style("pointer-events", (item) => {
4145
+ const visibility = item.style.label.visibility ?? "always";
4146
+ return visibility === "always" ? "auto" : "none";
4147
+ }).style("cursor", "pointer");
4185
4148
  labelSelection.selectAll("rect").data((item) => [item]).join("rect").attr("rx", (item) => item.style.label.borderRadius).attr("ry", (item) => item.style.label.borderRadius).attr("height", (item) => item.style.label.height).attr("fill", (item) => item.style.label.backgroundFill).attr("stroke", (item) => item.style.label.borderColor).attr("stroke-width", (item) => item.style.label.borderWidth);
4186
4149
  labelSelection.selectAll("text").data((item) => [item]).join("text").attr("text-anchor", "middle").attr("dominant-baseline", "middle").attr("font-size", (item) => item.style.label.fontSize).attr("fill", (item) => item.style.label.textColor).text((item) => item.link.label ?? "");
4187
4150
  return labelSelection;
@@ -4210,58 +4173,35 @@ function createDragBehavior(simulation) {
4210
4173
 
4211
4174
  // src/interactions/create-node-hover.ts
4212
4175
  function createNodeHover(nodeSelection, hoverStyle) {
4213
- if (!hoverStyle) {
4214
- return;
4215
- }
4216
- nodeSelection.on(
4217
- "mouseenter.hover",
4218
- function(_event, node) {
4176
+ const firstNode = nodeSelection.node();
4177
+ if (!firstNode) return;
4178
+ if (hoverStyle) {
4179
+ nodeSelection.on("mouseenter.hover", function(_event, node) {
4219
4180
  const circle = this;
4220
- const hoverStroke = hoverStyle.stroke ?? node.style?.stroke ?? "#ffffff";
4221
- const hoverStrokeWidth = hoverStyle.strokeWidth ?? node.style?.strokeWidth ?? 1.5;
4222
- const hoverOpacity = hoverStyle.opacity ?? node.style?.opacity ?? 1;
4223
- circle.setAttribute(
4224
- "stroke",
4225
- hoverStroke
4226
- );
4227
- circle.setAttribute(
4228
- "stroke-width",
4229
- String(
4230
- hoverStrokeWidth
4231
- )
4232
- );
4233
- circle.setAttribute(
4234
- "opacity",
4235
- String(
4236
- hoverOpacity
4237
- )
4238
- );
4239
- }
4240
- ).on(
4241
- "mouseleave.hover",
4242
- function(_event, node) {
4181
+ circle.setAttribute("stroke", hoverStyle.stroke ?? node.style?.stroke ?? "#ffffff");
4182
+ circle.setAttribute("stroke-width", String(hoverStyle.strokeWidth ?? node.style?.strokeWidth ?? 1.5));
4183
+ circle.setAttribute("opacity", String(hoverStyle.opacity ?? node.style?.opacity ?? 1));
4184
+ }).on("mouseleave.hover", function(_event, node) {
4243
4185
  const circle = this;
4244
- const defaultStroke = node.style?.stroke ?? "#ffffff";
4245
- const defaultStrokeWidth = node.style?.strokeWidth ?? 1.5;
4246
- const defaultOpacity = node.style?.opacity ?? 1;
4247
- circle.setAttribute(
4248
- "stroke",
4249
- defaultStroke
4250
- );
4251
- circle.setAttribute(
4252
- "stroke-width",
4253
- String(
4254
- defaultStrokeWidth
4255
- )
4256
- );
4257
- circle.setAttribute(
4258
- "opacity",
4259
- String(
4260
- defaultOpacity
4261
- )
4262
- );
4263
- }
4264
- );
4186
+ circle.setAttribute("stroke", node.style?.stroke ?? "#ffffff");
4187
+ circle.setAttribute("stroke-width", String(node.style?.strokeWidth ?? 1.5));
4188
+ circle.setAttribute("opacity", String(node.style?.opacity ?? 1));
4189
+ });
4190
+ }
4191
+ const svgElement = firstNode.ownerSVGElement;
4192
+ if (!svgElement) return;
4193
+ const root2 = select_default2(svgElement);
4194
+ const labelSelection = root2.selectAll(".link-label");
4195
+ nodeSelection.on("mouseenter.labels", (_event, d) => {
4196
+ labelSelection.filter((item) => {
4197
+ if (item.style.label.visibility !== "hover") return false;
4198
+ const s = item.link.source;
4199
+ const t = item.link.target;
4200
+ return s.id === d.id || t.id === d.id;
4201
+ }).interrupt().transition().duration(200).style("opacity", 1).style("pointer-events", "auto");
4202
+ }).on("mouseleave.labels", (_event) => {
4203
+ labelSelection.filter((item) => item.style.label.visibility === "hover").interrupt().transition().duration(200).style("opacity", 0).style("pointer-events", "none");
4204
+ });
4265
4205
  }
4266
4206
 
4267
4207
  // src/utils/resolve-tooltip-position.ts
@@ -4553,6 +4493,7 @@ function createGraph(config) {
4553
4493
  let tooltipBinding = null;
4554
4494
  let controls = null;
4555
4495
  let legendCleanup = null;
4496
+ let fitViewTimer = null;
4556
4497
  let dimensions = { width: 0, height: 0 };
4557
4498
  let rootGroup = null;
4558
4499
  let svgElement = null;
@@ -4569,9 +4510,17 @@ function createGraph(config) {
4569
4510
  layers.svg.setAttribute("height", String(height));
4570
4511
  layers.interactionRect.setAttribute("width", String(width));
4571
4512
  layers.interactionRect.setAttribute("height", String(height));
4572
- simulation?.force("x", x_default2(width / 2).strength(0.03));
4573
- simulation?.force("y", y_default2(height / 2).strength(0.03));
4574
- simulation?.alpha(0.25).restart();
4513
+ if (simulation) {
4514
+ simulation.force("center", center_default(width / 2, height / 2));
4515
+ simulation.alpha(0.3).restart();
4516
+ }
4517
+ if (fitViewTimer) {
4518
+ clearTimeout(fitViewTimer);
4519
+ }
4520
+ fitViewTimer = setTimeout(() => {
4521
+ fitView();
4522
+ fitViewTimer = null;
4523
+ }, 150);
4575
4524
  });
4576
4525
  const zoomResult = createZoom({
4577
4526
  svg: layers.svg,
@@ -4589,8 +4538,9 @@ function createGraph(config) {
4589
4538
  const simulationConfig = {
4590
4539
  nodes: config.nodes,
4591
4540
  links: config.links,
4592
- width: config.container.clientWidth,
4593
- height: config.container.clientHeight
4541
+ // Uses the observed dimensions to ensure physics are calculated on actual container size
4542
+ width: dimensions.width || config.container.clientWidth,
4543
+ height: dimensions.height || config.container.clientHeight
4594
4544
  };
4595
4545
  const simulationResult = createGraphSimulation(simulationConfig);
4596
4546
  simulation = simulationResult.simulation;
@@ -4646,7 +4596,7 @@ function createGraph(config) {
4646
4596
  }
4647
4597
  function resetView() {
4648
4598
  if (!zoomBehavior || !svgElement) return;
4649
- select_default2(svgElement).transition().call(zoomBehavior.transform, identity2);
4599
+ select_default2(svgElement).transition().duration(400).call(zoomBehavior.transform, identity2);
4650
4600
  }
4651
4601
  function fitView() {
4652
4602
  if (!zoomBehavior || !rootGroup || !svgElement || dimensions.width === 0 || dimensions.height === 0) return;
@@ -4656,7 +4606,7 @@ function createGraph(config) {
4656
4606
  const translateX = (dimensions.width - bounds.width * scale) / 2 - bounds.x * scale;
4657
4607
  const translateY = (dimensions.height - bounds.height * scale) / 2 - bounds.y * scale;
4658
4608
  const transform2 = identity2.translate(translateX, translateY).scale(scale);
4659
- select_default2(svgElement).transition().call(zoomBehavior.transform, transform2);
4609
+ select_default2(svgElement).transition().duration(400).call(zoomBehavior.transform, transform2);
4660
4610
  }
4661
4611
  function zoomIn() {
4662
4612
  if (!zoomBehavior || !svgElement) return;
@@ -4675,6 +4625,10 @@ function createGraph(config) {
4675
4625
  });
4676
4626
  }
4677
4627
  function destroy() {
4628
+ if (fitViewTimer) {
4629
+ clearTimeout(fitViewTimer);
4630
+ fitViewTimer = null;
4631
+ }
4678
4632
  if (cleanupResize) {
4679
4633
  cleanupResize();
4680
4634
  cleanupResize = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polly-graph",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
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",