@@ -32,8 +32,145 @@ nodes_user <- data.frame (id = dat_pkg$rm$contribs_from_gh_api$login, group = 4L
32
32
nodes_user$group [which (nodes_user$id %in% names (dat_users))] <- 3L
33
33
```
34
34
35
- ``` {ojs ForceGraph-import}
36
- import {ForceGraph} from "@d3/force-directed-graph-component"
35
+ ``` {ojs ForceGraph-definition}
36
+ // Copyright 2021-2024 Observable, Inc.
37
+ // Released under the ISC license.
38
+ // https://observablehq.com/@d3/force-directed-graph
39
+ function ForceGraph({
40
+ nodes, // an iterable of node objects (typically [{id}, …])
41
+ links // an iterable of link objects (typically [{source, target}, …])
42
+ }, {
43
+ nodeId = d => d.id, // given d in nodes, returns a unique identifier (string)
44
+ nodeGroup, // given d in nodes, returns an (ordinal) value for color
45
+ nodeGroups, // an array of ordinal values representing the node groups
46
+ nodeTitle, // given d in nodes, a title string
47
+ nodeFill = "currentColor", // node stroke fill (if not using a group color encoding)
48
+ nodeStroke = "#fff", // node stroke color
49
+ nodeStrokeWidth = 1.5, // node stroke width, in pixels
50
+ nodeStrokeOpacity = 1, // node stroke opacity
51
+ nodeRadius = 5, // node radius, in pixels
52
+ nodeStrength,
53
+ linkSource = ({source}) => source, // given d in links, returns a node identifier string
54
+ linkTarget = ({target}) => target, // given d in links, returns a node identifier string
55
+ linkStroke = "#999", // link stroke color
56
+ linkStrokeOpacity = 0.6, // link stroke opacity
57
+ linkStrokeWidth = 1.5, // given d in links, returns a stroke width in pixels
58
+ linkStrokeLinecap = "round", // link stroke linecap
59
+ linkStrength,
60
+ colors = d3.schemeTableau10, // an array of color strings, for the node groups
61
+ width = 640, // outer width, in pixels
62
+ height = 400, // outer height, in pixels
63
+ invalidation // when this promise resolves, stop the simulation
64
+ } = {}) {
65
+ // Compute values.
66
+ const N = d3.map(nodes, nodeId).map(intern);
67
+ const R = typeof nodeRadius !== "function" ? null : d3.map(nodes, nodeRadius);
68
+ const LS = d3.map(links, linkSource).map(intern);
69
+ const LT = d3.map(links, linkTarget).map(intern);
70
+ if (nodeTitle === undefined) nodeTitle = (_, i) => N[i];
71
+ const T = nodeTitle == null ? null : d3.map(nodes, nodeTitle);
72
+ const G = nodeGroup == null ? null : d3.map(nodes, nodeGroup).map(intern);
73
+ const W = typeof linkStrokeWidth !== "function" ? null : d3.map(links, linkStrokeWidth);
74
+ const L = typeof linkStroke !== "function" ? null : d3.map(links, linkStroke);
75
+
76
+
77
+ // Replace the input nodes and links with mutable objects for the simulation.
78
+ nodes = d3.map(nodes, (_, i) => ({id: N[i]}));
79
+ links = d3.map(links, (_, i) => ({source: LS[i], target: LT[i]}));
80
+
81
+ // Compute default domains.
82
+ if (G && nodeGroups === undefined) nodeGroups = d3.sort(G);
83
+
84
+ // Construct the scales.
85
+ const color = nodeGroup == null ? null : d3.scaleOrdinal(nodeGroups, colors);
86
+
87
+ // Construct the forces.
88
+ const forceNode = d3.forceManyBody();
89
+ const forceLink = d3.forceLink(links).id(({index: i}) => N[i]);
90
+ if (nodeStrength !== undefined) forceNode.strength(nodeStrength);
91
+ if (linkStrength !== undefined) forceLink.strength(linkStrength);
92
+
93
+ const simulation = d3.forceSimulation(nodes)
94
+ .force("link", forceLink)
95
+ .force("charge", forceNode)
96
+ .force("center", d3.forceCenter())
97
+ .on("tick", ticked);
98
+
99
+ const svg = d3.create("svg")
100
+ .attr("width", width)
101
+ .attr("height", height)
102
+ .attr("viewBox", [-width / 2, -height / 2, width, height])
103
+ .attr("style", "max-width: 100%; height: auto; height: intrinsic;");
104
+
105
+ const link = svg.append("g")
106
+ .attr("stroke", typeof linkStroke !== "function" ? linkStroke : null)
107
+ .attr("stroke-opacity", linkStrokeOpacity)
108
+ .attr("stroke-width", typeof linkStrokeWidth !== "function" ? linkStrokeWidth : null)
109
+ .attr("stroke-linecap", linkStrokeLinecap)
110
+ .selectAll("line")
111
+ .data(links)
112
+ .join("line");
113
+
114
+ const node = svg.append("g")
115
+ .attr("fill", nodeFill)
116
+ .attr("stroke", nodeStroke)
117
+ .attr("stroke-opacity", nodeStrokeOpacity)
118
+ .attr("stroke-width", nodeStrokeWidth)
119
+ .selectAll("circle")
120
+ .data(nodes)
121
+ .join("circle")
122
+ .attr("r", nodeRadius)
123
+ .call(drag(simulation));
124
+
125
+ if (W) link.attr("stroke-width", ({index: i}) => W[i]);
126
+ if (L) link.attr("stroke", ({index: i}) => L[i]);
127
+ if (G) node.attr("fill", ({index: i}) => color(G[i]));
128
+ if (R) node.attr("r", ({index: i}) => R[i]);
129
+ if (T) node.append("title").text(({index: i}) => T[i]);
130
+ if (invalidation != null) invalidation.then(() => simulation.stop());
131
+
132
+ function intern(value) {
133
+ return value !== null && typeof value === "object" ? value.valueOf() : value;
134
+ }
135
+
136
+ function ticked() {
137
+ link
138
+ .attr("x1", d => d.source.x)
139
+ .attr("y1", d => d.source.y)
140
+ .attr("x2", d => d.target.x)
141
+ .attr("y2", d => d.target.y);
142
+
143
+ node
144
+ .attr("cx", d => d.x)
145
+ .attr("cy", d => d.y);
146
+ }
147
+
148
+ function drag(simulation) {
149
+ function dragstarted(event) {
150
+ if (!event.active) simulation.alphaTarget(0.3).restart();
151
+ event.subject.fx = event.subject.x;
152
+ event.subject.fy = event.subject.y;
153
+ }
154
+
155
+ function dragged(event) {
156
+ event.subject.fx = event.x;
157
+ event.subject.fy = event.y;
158
+ }
159
+
160
+ function dragended(event) {
161
+ if (!event.active) simulation.alphaTarget(0);
162
+ event.subject.fx = null;
163
+ event.subject.fy = null;
164
+ }
165
+
166
+ return d3.drag()
167
+ .on("start", dragstarted)
168
+ .on("drag", dragged)
169
+ .on("end", dragended);
170
+ }
171
+
172
+ return Object.assign(svg.node(), {scales: {color}});
173
+ }
37
174
```
38
175
39
176
``` {ojs import-network-data}
0 commit comments