@@ -32,160 +32,132 @@ 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-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, // 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, // 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 = d3.map(nodes, nodeRadius);
68
- const LS = d3.map(links, linkSource).map(intern);
69
- const LT = d3.map(links, linkTarget).map(intern);
70
- const T = d3.map(nodes, nodeTitle);
71
- const G = nodeGroup == null ? null : d3.map(nodes, nodeGroup).map(intern);
72
- const W = typeof linkStrokeWidth !== "function" ? null : d3.map(links, linkStrokeWidth);
73
- const L = typeof linkStroke !== "function" ? null : d3.map(links, linkStroke);
74
-
75
-
76
- // Replace the input nodes and links with mutable objects for the simulation.
77
- nodes = d3.map(nodes, (_, i) => ({id: N[i]}));
78
- links = d3.map(links, (_, i) => ({source: LS[i], target: LT[i]}));
79
-
80
- // Compute default domains.
81
- if (G && nodeGroups === undefined) nodeGroups = d3.sort(G);
82
-
83
- // Construct the scales.
84
- const color = nodeGroup == null ? null : d3.scaleOrdinal(nodeGroups, colors);
85
-
86
- // Construct the forces.
87
- const forceNode = d3.forceManyBody();
88
- const forceLink = d3.forceLink(links).id(({index: i}) => N[i]);
89
- if (nodeStrength !== undefined) forceNode.strength(nodeStrength);
90
- if (linkStrength !== undefined) forceLink.strength(linkStrength);
35
+ here is some text
36
+
37
+
38
+ ``` {ojs import-network-data}
39
+ network = FileAttachment("results-user-network.json").json()
40
+ ```
41
+
42
+ ``` {ojs ForceGraph-plot}
43
+ Swatches(chart.scales.color)
44
+ chart = {
45
+
46
+ const width = 928;
47
+ const height = 600;
48
+
49
+ const nodes = network.nodes;
50
+ const links = network.links;
51
+ const types = Array.from(new Set(links.map(d => d.type)));
52
+
53
+ const color = d3.scaleOrdinal(types, d3.schemeCategory10);
91
54
92
55
const simulation = d3.forceSimulation(nodes)
93
- .force("link", forceLink)
94
- .force("charge", forceNode )
95
- .force("center ", d3.forceCenter ())
96
- .on("tick ", ticked );
56
+ .force("link", d3. forceLink(links).id(d => d.id) )
57
+ .force("charge", d3.forceManyBody().strength(-400) )
58
+ .force("x ", d3.forceX ())
59
+ .force("y ", d3.forceY() );
97
60
98
61
const svg = d3.create("svg")
62
+ .attr("viewBox", [-width / 2, -height / 2, width, height])
99
63
.attr("width", width)
100
64
.attr("height", height)
101
- .attr("viewBox", [-width / 2, -height / 2, width, height])
102
- .attr("style", "max-width: 100%; height: auto; height: intrinsic; font: 12px sans-serif;");
65
+ .attr("style", "max-width: 100%; height: auto; font: 12px sans-serif;");
66
+
67
+ // Per-type markers, as they don't inherit styles.
68
+ svg.append("defs").selectAll("marker")
69
+ .data(types)
70
+ .join("marker")
71
+ .attr("id", d => `arrow-${d}`)
72
+ .attr("viewBox", "0 -5 10 10")
73
+ .attr("refX", 15)
74
+ .attr("refY", -0.5)
75
+ .attr("markerWidth", 6)
76
+ .attr("markerHeight", 6)
77
+ .attr("orient", "auto")
78
+ .append("path")
79
+ .attr("fill", color)
80
+ .attr("d", "M0,-5L10,0L0,5");
103
81
104
82
const link = svg.append("g")
105
- .attr("stroke", typeof linkStroke !== "function" ? linkStroke : null)
106
- .attr("stroke-opacity", linkStrokeOpacity)
107
- .attr("stroke-width", typeof linkStrokeWidth !== "function" ? linkStrokeWidth : null)
108
- .attr("stroke-linecap", linkStrokeLinecap)
109
- .selectAll("line")
83
+ .attr("fill", "none")
84
+ .attr("stroke-width", 1.5)
85
+ .selectAll("path")
110
86
.data(links)
111
- .join("line");
87
+ .join("path")
88
+ .attr("stroke", d => color(d.type))
89
+ .attr("stroke-width", d => Math.sqrt(d.value));
112
90
113
91
const node = svg.append("g")
114
- .attr("fill", nodeFill)
115
- .attr("stroke", nodeStroke)
116
- .attr("stroke-opacity", nodeStrokeOpacity)
117
- .attr("stroke-width", nodeStrokeWidth)
118
- .selectAll("circle")
92
+ .attr("fill", "currentColor")
93
+ .attr("stroke-linecap", "round")
94
+ .attr("stroke-linejoin", "round")
95
+ .selectAll("g")
119
96
.data(nodes)
120
- .join("circle")
121
- .attr("r", nodeRadius)
97
+ .join("g")
122
98
.call(drag(simulation));
123
99
124
- if (W) link.attr("stroke-width", ({index: i}) => W[i]);
125
- if (L) link.attr("stroke", ({index: i}) => L[i]);
126
- if (G) node.attr("fill", ({index: i}) => color(G[i]));
127
- node.attr("r", ({index: i}) => R[i]);
128
- node.append("title").text(({index: i}) => T[i]);
129
- if (invalidation != null) invalidation.then(() => simulation.stop());
100
+ node.append("circle")
101
+ .attr("stroke", "white")
102
+ .attr("stroke-width", 1.5)
103
+ .data(nodes)
104
+ .join("circle")
105
+ .attr("r", d => 5 * Math.log10(d.contributions + 1));
106
+
107
+ node.append("text")
108
+ .attr("x", 8)
109
+ .attr("y", "0.31em")
110
+ .text(d => d.id)
111
+ .clone(true).lower()
112
+ .attr("fill", "none")
113
+ .attr("stroke", "white")
114
+ .attr("stroke-width", 3);
115
+
116
+ simulation.on("tick", () => {
117
+ link.attr("d", linkArc);
118
+ node.attr("transform", d => `translate(${d.x},${d.y})`);
119
+ });
120
+
121
+ invalidation.then(() => simulation.stop());
122
+
123
+ return Object.assign(svg.node(), {scales: {color}});
124
+ }
125
+ ```
130
126
131
- function intern(value) {
132
- return value !== null && typeof value === "object" ? value.valueOf() : value;
127
+ ``` {ojs}
128
+ function linkArc(d) {
129
+ const r = Math.hypot(d.target.x - d.source.x, d.target.y - d.source.y);
130
+ return `
131
+ M${d.source.x},${d.source.y}
132
+ A${r},${r} 0 0,1 ${d.target.x},${d.target.y}
133
+ `;
134
+ }
135
+ ```
136
+
137
+ ``` {ojs}
138
+ drag = simulation => {
139
+
140
+ function dragstarted(event, d) {
141
+ if (!event.active) simulation.alphaTarget(0.3).restart();
142
+ d.fx = d.x;
143
+ d.fy = d.y;
133
144
}
134
145
135
- function ticked() {
136
- link
137
- .attr("x1", d => d.source.x)
138
- .attr("y1", d => d.source.y)
139
- .attr("x2", d => d.target.x)
140
- .attr("y2", d => d.target.y);
146
+ function dragged(event, d) {
147
+ d.fx = event.x;
148
+ d.fy = event.y;
149
+ }
141
150
142
- node
143
- .attr("cx", d => d.x)
144
- .attr("cy", d => d.y);
151
+ function dragended(event, d) {
152
+ if (!event.active) simulation.alphaTarget(0);
153
+ d.fx = null;
154
+ d.fy = null;
145
155
}
146
156
147
- function drag(simulation) {
148
- function dragstarted(event) {
149
- if (!event.active) simulation.alphaTarget(0.3).restart();
150
- event.subject.fx = event.subject.x;
151
- event.subject.fy = event.subject.y;
152
- }
153
-
154
- function dragged(event) {
155
- event.subject.fx = event.x;
156
- event.subject.fy = event.y;
157
- }
158
-
159
- function dragended(event) {
160
- if (!event.active) simulation.alphaTarget(0);
161
- event.subject.fx = null;
162
- event.subject.fy = null;
163
- }
164
-
165
- return d3.drag()
157
+ return d3.drag()
166
158
.on("start", dragstarted)
167
159
.on("drag", dragged)
168
160
.on("end", dragended);
169
- }
170
-
171
- return Object.assign(svg.node(), {scales: {color}});
172
161
}
173
- ```
174
-
175
- ``` {ojs import-network-data}
176
- network = FileAttachment("results-user-network.json").json()
177
- ```
178
-
179
- ``` {ojs ForceGraph-plot}
180
- chart = ForceGraph(network, {
181
- nodeId: d => d.id,
182
- nodeGroup: d => d.group,
183
- nodeTitle: d => d.id,
184
- nodeRadius: d => 10 * Math.log10(d.contributions + 1),
185
- linkStrokeWidth: l => Math.sqrt(l.value),
186
- width,
187
- height: 400,
188
- linkStrength: 0.001,
189
- invalidation // a promise to stop the simulation when the cell is re-run
190
- })
162
+ import {Swatches} from "@d3/color-legend"
191
163
```
0 commit comments