Skip to content

Commit 9d368ae

Browse files
committed
use different kind of force graph for network plot #59
1 parent ebef1ba commit 9d368ae

File tree

4 files changed

+107
-134
lines changed

4 files changed

+107
-134
lines changed

DESCRIPTION

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Package: repometrics
22
Title: Metrics for Your Code Repository
3-
Version: 0.1.3.050
3+
Version: 0.1.3.051
44
Authors@R:
55
person("Mark", "Padgham", , "mark.padgham@email.com", role = c("aut", "cre"),
66
comment = c(ORCID = "0000-0003-2172-5265"))

R/quarto-dashboard.R

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ repometrics_dashboard <- function (data_repo, data_users, action = "preview") {
3232
saveRDS (data_users, fs::path (dir, "results-users.Rds"))
3333

3434
dat_user_network <- get_user_network (data_repo, data_users)
35+
dat_user_network$links$type <- "person"
3536
jsonlite::write_json (
3637
dat_user_network,
3738
fs::path (dir, "results-user-network.json")

codemeta.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"codeRepository": "https://github.com/ropensci-review-tools/repometrics",
99
"issueTracker": "https://github.com/ropensci-review-tools/repometrics/issues",
1010
"license": "https://spdx.org/licenses/GPL-3.0",
11-
"version": "0.1.3.050",
11+
"version": "0.1.3.051",
1212
"programmingLanguage": {
1313
"@type": "ComputerLanguage",
1414
"name": "R",

inst/extdata/quarto/network.qmd

+104-132
Original file line numberDiff line numberDiff line change
@@ -32,160 +32,132 @@ nodes_user <- data.frame (id = dat_pkg$rm$contribs_from_gh_api$login, group = 4L
3232
nodes_user$group [which (nodes_user$id %in% names (dat_users))] <- 3L
3333
```
3434

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);
9154
9255
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());
9760
9861
const svg = d3.create("svg")
62+
.attr("viewBox", [-width / 2, -height / 2, width, height])
9963
.attr("width", width)
10064
.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");
10381
10482
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")
11086
.data(links)
111-
.join("line");
87+
.join("path")
88+
.attr("stroke", d => color(d.type))
89+
.attr("stroke-width", d => Math.sqrt(d.value));
11290
11391
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")
11996
.data(nodes)
120-
.join("circle")
121-
.attr("r", nodeRadius)
97+
.join("g")
12298
.call(drag(simulation));
12399
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+
```
130126

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;
133144
}
134145
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+
}
141150
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;
145155
}
146156
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()
166158
.on("start", dragstarted)
167159
.on("drag", dragged)
168160
.on("end", dragended);
169-
}
170-
171-
return Object.assign(svg.node(), {scales: {color}});
172161
}
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"
191163
```

0 commit comments

Comments
 (0)