Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a Who We Are page #81

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ jobs:
bio
company
avatarUrl
websiteUrl
socialAccounts(first:10) {
edges {
node {
Expand Down
4 changes: 2 additions & 2 deletions _quarto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ website:
text: Home
- href: index.qmd#events
text: Events
- href: whoWeAre.qmd
text: Who We Are
- href: resbaz/resbazTucson2024.qmd
text: ResBaz 2024
# - href: whoWeAre.qmd
# text: Who We Are
body-footer: |
::: {.footer}
![ResBaz Logo](/img/logos/ResBazAZrectanglelogo-small.png) \
Expand Down
237 changes: 237 additions & 0 deletions components/nodeLinkDiagram.ojs
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
function nodeLinkDiagram(nodes, links) {
let selectNode,
selectedNode = null;

const height = globalThis.screen.height;

const personNodeRadius = 25;
const personNodePaddedRadius = personNodeRadius + 15;

const highlightOutlineRadius = 5;
const highlightStrokeWeight = 5;

const teamNodeRadius = 75;

const strokeWeight = 3;

const gravityMultiplier = 0.3;
const maxGravityAlpha = 0.0005;
const bounceStrength = 2;
const chargeStrength = -2000; // -10 * (personNodeRadius + teamNodeRadius);

const teamColors = d3.scaleOrdinal(
["WEEKLY", "FESTIVAL"],
["#ea5a2a", "#1e58ac"]
);

const simulation = d3
.forceSimulation(nodes)
.force(
"link",
d3.forceLink(links).id((d) => d.id)
)
.force("charge", d3.forceManyBody().strength(chargeStrength))
.force("centerAndBounds", (alpha) => {
nodes.forEach((d) => {
const radius =
d.type === "PERSON" ? personNodePaddedRadius : teamNodeRadius;
// Kinda weird, but has a nice effect: apply gravity more strongly
// (within a limit) at the beginning of a layout / while you're
// dragging, but taper it off toward the end
const gravityAlpha = Math.min(
(alpha * gravityMultiplier) ** 2,
maxGravityAlpha
);

if (d.x < radius) {
d.x = radius;
d.vx += alpha * bounceStrength * (radius - d.x);
} else if (d.x > width - radius) {
d.x = width - radius;
d.vx += -alpha * bounceStrength * (d.x - width - radius);
}
const dx = width / 2 - d.x;
d.vx += Math.sign(dx) * gravityAlpha * dx ** 2;

if (d.y < radius) {
d.y = radius;
d.vy += alpha * bounceStrength * (radius - d.y);
} else if (d.y > height - radius) {
d.y = height - radius;
d.vy += -alpha * bounceStrength * (d.y - height - radius);
}
const dy = height / 2 - d.y;
d.vy += Math.sign(dy) * gravityAlpha * dy ** 2;
});
})
.force(
"collide",
d3.forceCollide((d) =>
d.type === "PERSON" ? personNodePaddedRadius : teamNodeRadius
)
);

const svg = d3
.create("svg")
.attr("viewBox", [0, 0, width, height])
.style("user-select", "none");
svg.append("g").classed("links", true);
svg.append("g").classed("nodes", true);

let draggedNode = null;
let dragOffset = null;

function mousedown(event, node) {
if (draggedNode) {
return;
}
const bounds = svg.node().getBoundingClientRect();
simulation.alphaTarget(0.025).restart();
draggedNode = node;
selectNode(draggedNode);
const clickedPoint = {
x: event.x - bounds.left,
y: event.y - bounds.top,
};
dragOffset = {
dx: clickedPoint.x - draggedNode.x,
dy: clickedPoint.y - draggedNode.y,
};
draggedNode.fx = draggedNode.x;
draggedNode.fy = draggedNode.y;
}

function mousemove(event) {
if (!draggedNode) {
return;
}
const bounds = svg.node().getBoundingClientRect();
const clickedPoint = {
x: event.x - bounds.left,
y: event.y - bounds.top,
};
draggedNode.fx = clickedPoint.x - dragOffset.dx;
draggedNode.fy = clickedPoint.y - dragOffset.dy;
}

function mouseup(event) {
if (!draggedNode) {
return;
}
draggedNode.fx = null;
draggedNode.fy = null;
draggedNode = null;
dragOffset = null;
simulation.alphaTarget(0);
}

function* render(_selectNode, _selectedNode) {
selectNode = _selectNode;
selectedNode = _selectedNode;

let link = svg
.select(".links")
.selectAll("line")
.data(links, (d) => `${d.from?.id}_${d.to?.id}`);
const linkEnter = link
.enter()
.append("line")
.attr("stroke", "#999")
.attr("stroke-opacity", 0.6)
.attr("stroke-width", strokeWeight);
link.exit().remove();
link = link.merge(linkEnter);

let node = svg
.select(".nodes")
.selectAll("g.node")
.data(nodes, (d) => d.id);
const nodeEnter = node.enter().append("g").classed("node", true);
node.exit().remove();
node = node
.merge(nodeEnter)
// d3.drag() does weird things with quarto's minified version of d3, and
// isn't very retina display-friendly... so we manage interactions ourselves
.on("mousedown", mousedown);
d3.select(document).on("mousemove", mousemove).on("mouseup", mouseup);

nodeEnter
.append("circle")
.classed("outline", true)
.attr(
"r",
(d) =>
highlightOutlineRadius +
(d.type === "PERSON" ? personNodeRadius : teamNodeRadius)
)
.style("fill", "none")
.style("stroke", "#333")
.style("stroke-width", highlightStrokeWeight);
node
.select(".outline")
.style("display", (d) => (d.id === selectedNode?.id ? null : "none"));

nodeEnter
.filter((d) => d.type !== "PERSON")
.append("circle")
.attr("r", teamNodeRadius)
.style("fill", (d) => teamColors(d.type));

nodeEnter
.filter((d) => d.type === "PERSON")
.append("clipPath")
.attr("id", (d) => d.id)
.append("circle")
.attr("id", (d) => d.id)
.attr("r", personNodeRadius);

nodeEnter
.filter((d) => d.type === "PERSON")
.append("image")
.attr("href", (d) => d.avatarUrl)
.attr("x", (d) => -personNodeRadius)
.attr("y", (d) => -personNodeRadius)
.attr("width", personNodeRadius * 2)
.attr("height", personNodeRadius * 2)
.attr("clip-path", (d) => `url(#${d.id})`)
.attr("preserveAspectRatio", "xMidYMin slice");

nodeEnter
.append("text")
.attr("class", "node_label")
.style("fill", (d) => (d.type === "PERSON" ? "black" : "white"))
.style("dominant-baseline", (d) =>
d.type === "PERSON" ? "hanging" : "bottom"
)
.style("text-anchor", "middle")
.style("font-size", "10pt")
.text((d) => d.name || d.login);
node.select("text").attr("y", (d) => {
if (d.type !== "PERSON") {
return "0.5em";
}
if (d.id === selectedNode?.id) {
return `${personNodeRadius + 2 * highlightOutlineRadius}px`;
}
return `${personNodeRadius}px`;
});

nodeEnter.append("title").text((d) => d.name || d.login);

simulation.on("tick", () => {
node.attr("transform", (d) => "translate(" + d.x + "," + d.y + ")");

link
.attr("x1", (d) => d.source.x)
.attr("y1", (d) => d.source.y)
.attr("x2", (d) => d.target.x)
.attr("y2", (d) => d.target.y);
});

invalidation.then(() => simulation.stop());

yield svg.node();
}

return render;
}
25 changes: 10 additions & 15 deletions components/randomAvatars.ojs
Original file line number Diff line number Diff line change
@@ -1,34 +1,29 @@
peopleFile = FileAttachment("data/people.json").json();

/**
* An observable.js widget that shows profile pictures for ResBaz GitHub Team
* members; see index.qmd for examples. Additionally, some relevant styles are
* in styles/index.css
*
* @param peopleData The results of combineAndOverrideGithubData()
* @param teamName A string that should correspond to the name of an entry in
* data/people.json, under data.organization.teams.nodes
* data/people.json under data.organization.teams.nodes,
* or a key under teams in data/overrides.json,
* @returns A DOM element that can be embedded via an ojs cell
*/
function randomAvatars(teamName) {
const team = peopleFile.data.organization.teams.nodes.find(
(team) => team.name === teamName
);
const people = team.members.nodes
.map(({ id }) =>
peopleFile.data.organization.membersWithRole.nodes.find(
(person) => person.id === id
)
)
function randomAvatars(peopleData, teamId) {
const team = peopleData.teamsById[teamId];
const people = team.members
.map((id) => peopleData.peopleById[id])
.sort(() => 2 * Math.random() - 1)
.slice(0, 5);
const container = d3.create("div").classed("randomAvatars", true);
container
.selectAll("img")
.selectAll("a")
.data(people)
.enter()
.append("a")
.attr("href", (person) => `./whoWeAre.html#${person.hash}`)
.append("img")
.attr("src", (person) => person.avatarUrl)
.attr("title", (person) => person.name);
// TODO: link to the Who We Are page, when a profile picture is clicked
return container.node();
}
81 changes: 81 additions & 0 deletions components/utilities.ojs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
people = FileAttachment("data/people.json").json();
overrides = FileAttachment("data/overrides.json").json();

function combineAndOverrideGithubData() {
const peopleById = {
// Include people manually added in overrides.json, that may not have a
// github login (or their info hasn't been queried yet by
// .github/workflows/build.yml, e.g. during local development)
...overrides.people,
...Object.fromEntries(
people.data.organization.membersWithRole.nodes.map((person) => [
person.login,
{
...person,
// Github stores people by an illegible hash; we want to select +
// override data using Github usernames as ids (but still use
// hashes for url navigation)
hash: person.id,
id: person.login,
type: "PERSON",
// Override github profile information with details in overrides.json
...(overrides.people[person?.login] || {}),
},
])
),
};

const peopleByHash = Object.fromEntries(
Object.values(peopleById).map((person) => [person.hash, person])
);

const teamsById = {
// Include this separately for manual "teams" that aren't on Github
...overrides.teams,
...Object.fromEntries(
people.data.organization.teams.nodes.map((team) => {
const teamId = team.name.toLowerCase().replace(/\s+/g, "_");
return [
teamId,
{
id: teamId,
name: team.name,
type: "FESTIVAL", // For non-festival "teams," add an override!
members: team.members.nodes.map(({ id }) => peopleByHash[id].login),
// Override any github teams with details in overrides.json
...(overrides.teams[teamId] || {}),
},
];
})
),
};
Object.entries(teamsById).forEach(([teamId, team]) => {
// Some extra fields that we don't want to have to hand-code
// in overrides.json
if (!team.hash) {
team.id = teamId;
team.hash = teamId;
}
if (!team.type) {
team.type = "FESTIVAL";
}
});

const nodes = [...Object.values(peopleById), ...Object.values(teamsById)];
const links = [];
Object.values(teamsById).forEach((team) => {
team.members.forEach((member) => {
links.push({ source: member, target: team.id });
});
});

return {
peopleById,
peopleByHash,
teamsById,
graph: {
nodes,
links,
},
};
}
Loading