Skip to content

Commit 0425042

Browse files
committed
Merge branch 'main' into dh-code-review
2 parents 636dccd + 8dc1049 commit 0425042

9 files changed

Lines changed: 2099 additions & 0 deletions

File tree

assets/js/branches.js

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
import { line, curveNatural, curveBumpY } from "d3-shape";
2+
import { select, selectAll } from "d3-selection";
3+
import { randomNumBetween } from "./leaves";
4+
import { BaseSVG } from "./utils";
5+
6+
// combine into d3 object for convenience
7+
const d3 = {
8+
line,
9+
curveNatural,
10+
curveBumpY,
11+
select,
12+
selectAll,
13+
};
14+
15+
function drawTreeSegment(points) {
16+
const curve = d3.line().curve(d3.curveBumpY);
17+
return curve(points.map((d) => [d.x, d.y])); // [start, end]);
18+
}
19+
20+
// how to do this? needs context/scale
21+
// trunk width should be a constant,
22+
// top/bottom coords should be fixed
23+
// - needed for both tree and roots
24+
25+
const trunkWidth = 150; // 110;
26+
const trunkBaseWidth = trunkWidth * 0.9;
27+
28+
const trunk = {
29+
width: trunkWidth,
30+
topLeft: -trunkWidth / 2,
31+
topRight: trunkWidth / 2,
32+
bottomLeft: -trunkBaseWidth / 2,
33+
bottomRight: trunkBaseWidth / 2,
34+
};
35+
36+
function drawTrunk(container, [min_x, min_y, width, height], trunkTop) {
37+
// draw lines for the trunk,
38+
// to make it easier to read as a tree
39+
40+
const max_y = min_y + height;
41+
42+
// extend trunk off the bottom edge of the svg,
43+
// so that trunk and roots stay connected when resizing
44+
const trunkExtra = 600;
45+
46+
// add points for unevenness at the thirds of the trunk
47+
let onethird = (trunkTop - trunk.bottomLeft) / 4;
48+
49+
// generate points for left side
50+
const leftSidePoints = [
51+
[trunk.topLeft, trunkTop],
52+
[trunk.topLeft * 0.9, trunkTop + onethird],
53+
[trunk.topLeft * 0.8, trunkTop + onethird * 2],
54+
[trunk.bottomLeft * 0.9, trunkTop + onethird * 3],
55+
[trunk.bottomLeft, max_y],
56+
[trunk.bottomLeft, max_y + trunkExtra],
57+
].map((d) => {
58+
return { x: d[0], y: d[1] };
59+
});
60+
61+
// draw the path for the left side
62+
container
63+
.append("path")
64+
.attr("class", "trunk")
65+
.attr("d", drawTreeSegment(leftSidePoints));
66+
67+
// generate points for right side
68+
69+
const rightSidePoints = [
70+
[trunk.topRight, trunkTop],
71+
[trunk.topRight * 0.9, trunkTop + onethird],
72+
[trunk.topRight * 0.95, trunkTop + onethird * 2],
73+
[trunk.bottomRight * 0.9, trunkTop + onethird * 3],
74+
[trunk.bottomRight, max_y],
75+
[trunk.bottomRight, max_y + trunkExtra], // extend off the edge of the svg, for resizing
76+
].map((d) => {
77+
return { x: d[0], y: d[1] };
78+
});
79+
80+
// draw the path for the right side
81+
container
82+
.append("path")
83+
.attr("class", "trunk")
84+
.attr("d", drawTreeSegment(rightSidePoints));
85+
}
86+
87+
function drawBranches(nodes, container, branches, trunkTop) {
88+
// draw branches
89+
90+
let branchNodes = nodes.filter((d) => d.type == "branch");
91+
// calculate starting coordinates for each branch
92+
// tree width is defined as a constant;
93+
// assuming center of svg is 0,0
94+
// top of tree is passed in from timetree code
95+
let leftBranchX = -trunkWidth / 2;
96+
// second branch starts up 1/3 of the trunk width
97+
let secondBranchY = trunkWidth * 0.3;
98+
// third and fourth stair step down in thirds
99+
let steps = (trunkTop - secondBranchY) * 0.3;
100+
101+
// calculate starting coordinates for each of the five branches
102+
let branchStart = [
103+
// left-most branch
104+
{ x: trunk.topLeft, y: trunkTop },
105+
// [leftBranchX, trunkTop], // left-most branch
106+
// second branch starts over 6% of tree width
107+
{ x: trunk.topLeft + trunkWidth * 0.06, y: trunkTop - secondBranchY },
108+
// third is 48% of width
109+
{
110+
x: trunk.topLeft + trunkWidth * 0.48,
111+
y: trunkTop - secondBranchY + steps,
112+
},
113+
// fourth is 70% of width
114+
{
115+
x: trunk.topLeft + trunkWidth * 0.7,
116+
y: trunkTop - secondBranchY + steps + steps,
117+
},
118+
// right-most branch
119+
{ x: trunk.topRight, y: trunkTop },
120+
];
121+
122+
// insert branches before node group layer,
123+
// so it will render as underneath the leaves
124+
let branchPaths = container
125+
.insert("g", ".nodes")
126+
.attr("class", "branches")
127+
.selectAll("path")
128+
.data(Object.keys(branches)) // join to branch names passed in
129+
.join("path")
130+
// draw branch path for leaves, empty path for everything else
131+
.attr("class", "branch")
132+
.attr("d", (b, i) => {
133+
// start at the calculated branch point for this branch,
134+
// then use branch pseudo nodes as coordinates
135+
let branchPoints = [
136+
branchStart[i],
137+
...branchNodes.filter((d) => d.branch == b),
138+
];
139+
return drawTreeSegment(branchPoints);
140+
});
141+
}
142+
143+
class Roots extends BaseSVG {
144+
constructor() {
145+
super();
146+
147+
// configure so point [0, 0] is the center top of the svg
148+
149+
// use same logic as for the timetree svg width
150+
let width = this.getSVGWidth(); // width depends on if mobile or not
151+
let height = 130;
152+
let min_x = -width / 2;
153+
let min_y = 0;
154+
155+
// TODO: use a graphic for mobile,
156+
// since it is decorative and not functional ?
157+
158+
let center_x = min_x + width / 2;
159+
160+
const svg = d3
161+
.select("body > footer")
162+
.append("svg")
163+
.lower()
164+
.attr("id", "roots")
165+
.attr("viewBox", [min_x, min_y, width, height]);
166+
167+
// for debugging: mark the center of the svg
168+
// svg
169+
// .append("circle")
170+
// .attr("r", 5)
171+
// .attr("fill", "red")
172+
// .attr("cx", min_x + width / 2)
173+
// // .attr("cx", width / 2)
174+
// // .attr("cx", 0)
175+
// .attr("cy", min_y + height / 2);
176+
177+
const navLinks = document.querySelectorAll("body > footer > nav > a");
178+
let linkCount = navLinks.length;
179+
// divide into equal sections based on the number of nav links
180+
let sectionwidth = width / linkCount + 1;
181+
182+
let center_y = height / 2;
183+
184+
let currentURL = window.location.pathname;
185+
186+
// draw one root for each footer nav link
187+
navLinks.forEach((a, i) => {
188+
// determine if left or right, based half point of leaves
189+
let left = i < linkCount / 2;
190+
191+
let startx = left ? trunk.bottomLeft : trunk.bottomRight;
192+
let targetX = min_x + sectionwidth * i + sectionwidth / 2;
193+
194+
// create a branch off point for secondary root line
195+
let secondaryRootStart = [
196+
// start part way to the target x coord
197+
((targetX - startx) / 3) * 2 + (left ? -45 : 45),
198+
// and somewhere between a third and a half of the svg height
199+
randomNumBetween(height / 3, height / 2),
200+
];
201+
202+
let rootCoords = [
203+
[center_x + startx, min_y],
204+
[center_x + startx + (left ? -8 : 8), min_y + 7],
205+
secondaryRootStart,
206+
[targetX, center_y],
207+
[targetX + (left ? -25 : 25), height],
208+
].map((d) => {
209+
return { x: d[0], y: d[1] };
210+
});
211+
212+
let path = drawTreeSegment(rootCoords);
213+
let current = a.getAttribute("aria-current") == "page";
214+
// set root as current if nav link page is for the current page
215+
svg
216+
.append("path")
217+
.attr("class", `root ${current ? "current" : ""}`)
218+
.attr("d", path);
219+
220+
let secondaryRootCoords = [
221+
rootCoords[2], // = secondary root start
222+
{
223+
x: secondaryRootStart[0] + (left ? -43 : 43),
224+
y: secondaryRootStart[1] + 52,
225+
},
226+
{ x: secondaryRootStart[0] + (left ? -55 : 55), y: height },
227+
];
228+
229+
svg
230+
.append("path")
231+
.attr("class", "root")
232+
.attr("d", drawTreeSegment(secondaryRootCoords));
233+
});
234+
235+
// NOTE: html coords != svg coords, so bounding rects doesn't help
236+
}
237+
}
238+
239+
export { drawTreeSegment, Roots, drawTrunk, drawBranches };

assets/js/keys.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/* mixin for keypress management */
2+
3+
import { select, selectAll } from "d3-selection";
4+
// combine into d3 object for convenience
5+
const d3 = {
6+
select,
7+
selectAll,
8+
};
9+
10+
// mixin extends syntax from
11+
// https://blog.bitsrc.io/inheritance-abstract-classes-and-class-mixin-in-javascript-c636ac00f5a9
12+
13+
const TimeTreeKeysMixin = (Base) =>
14+
class extends Base {
15+
bindKeypressHandler() {
16+
// make panel object available in event handler context
17+
let panel = this.panel;
18+
19+
document.onkeydown = function (evt) {
20+
// Get event object
21+
evt = evt || window.event;
22+
23+
// Keypress switch logic
24+
switch (evt.key) {
25+
// Escape key closes the panel
26+
case "Escape":
27+
case "Esc":
28+
panel.close();
29+
break;
30+
31+
// Enter or space key activates focused element with button role
32+
case "Enter":
33+
case " ":
34+
// if target element has role=button (i.e. leaves in the tree),
35+
// trigger click behavior
36+
if (evt.target.getAttribute("role", "button")) {
37+
d3.select(evt.target).dispatch("click");
38+
}
39+
break;
40+
41+
// ... Add other cases here for more keyboard commands ...
42+
43+
// Otherwise
44+
default:
45+
return; // Do nothing
46+
}
47+
};
48+
}
49+
};
50+
51+
export { TimeTreeKeysMixin };

assets/js/labels.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// const labelLineHeight = 18;
2+
// multiplier to use for calculating size based on characters
3+
// const pixelsPerChar = 7;
4+
5+
class LeafLabel {
6+
static lineHeight = 18;
7+
// multiplier to use for calculating size based on characters
8+
static pixelsPerChar = 7;
9+
10+
constructor(label = null) {
11+
this.text = label;
12+
// always need parts and want to calculate once, so getter doesn't make sense
13+
this.parts = this.splitLabel();
14+
}
15+
16+
splitLabel() {
17+
// split a leaf label into words for wrapping
18+
// for now, splitting on whitespace, but could adjust
19+
if (this.text == null || this.text == undefined) {
20+
return ["no title"];
21+
}
22+
return this.text.split(" ");
23+
}
24+
25+
get height() {
26+
// height is based on line height and number of words
27+
return this.parts.length * LeafLabel.lineHeight;
28+
}
29+
30+
get width() {
31+
// width is based on the longest word
32+
return (
33+
Math.max(...this.parts.map((w) => w.length)) * LeafLabel.pixelsPerChar
34+
);
35+
}
36+
37+
get radius() {
38+
// calculate radius based on text content, for avoiding collision in
39+
// the d3-force simulation
40+
// determine whichever is bigger is the diameter; halve for radius
41+
return Math.max(this.width, this.height) / 2.0;
42+
}
43+
}
44+
45+
export { LeafLabel };

0 commit comments

Comments
 (0)