Skip to content

Commit 8127704

Browse files
authored
Merge pull request #590 from Nightxade/course-guide-cand
Fixed up graph, added dependency highlighting
2 parents cbf03cf + 0ad027c commit 8127704

File tree

1 file changed

+132
-59
lines changed

1 file changed

+132
-59
lines changed

hknweb/templates/studentservices/course_guide_test.html

Lines changed: 132 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
width: 100%;
2727
overflow: hidden;
2828
}
29+
.graph {
30+
width: 80%;
31+
height: auto;
32+
}
2933

3034
</style>
3135
<script>
@@ -72,7 +76,9 @@ <h2 style="text-align: center;">
7276
</h2>
7377

7478
<div class="svg-container">
75-
<svg class="graph">
79+
<svg class="graph"
80+
viewBox="0 0 2000 1200"
81+
preserveAspectRatio="xMidYMid meet">
7682
</svg>
7783
</div>
7884
<button id="editButton" onclick="toggleEdit()"> Edit </button>
@@ -92,55 +98,52 @@ <h2 style="text-align: center;">
9298

9399
}
94100

95-
96-
const width = 1000;
97-
const height = 1000;
98-
const centerX = width / 2;
99-
const centerY = width / 2;
100-
const link_stroke_width = 5;
101-
const arrowhead_size = link_stroke_width*1.5;
102-
const circle_radius = 30;
101+
const width = 1000,
102+
height = 1000,
103+
centerX = width / 2,
104+
centerY = width / 2,
105+
link_stroke_width = 5,
106+
arrowhead_size = link_stroke_width*1.5,
107+
circle_radius = {{ params.circle_radius }},
108+
title_circle_radius = circle_radius * 1.2,
109+
node_stroke_width = 1,
110+
node_stroke = "gray"
103111

104112
const svg = d3.select("svg")
105113
.attr("width", width)
106114
.attr("height", width)
107115
.attr("viewBox", [-width / 2, -height / 2, width, height]);
108-
109-
110116

111117

112118
async function grabData() {
113119
const data = await d3.json("{% url 'studentservices:course_guide_data' %}?groups={{ request.GET.groups }}")
114-
console.log(data)
115120
return data
116121
}
117122

118123
async function generate() {
119124
const graph = await grabData();
120-
const nodes = graph.nodes.map(d => ({ ...d }));
125+
const nodes = graph.nodes.map(d => ({ ...d, fd: [], bk: [] }));
121126
const links = graph.links.map(d => ({ ...d }));
122-
123-
const color = d3.scaleOrdinal(d3.schemeCategory10);
127+
128+
const nodeById = new Map(nodes.map(d => [d.id, d]));
129+
links.forEach(l => {
130+
nodeById.get(l.source).fd.push(l.target);
131+
nodeById.get(l.target).bk.push(l.source);
132+
});
133+
134+
const color = d3.scaleOrdinal(d3.schemePaired);
124135

125136
const simulation = d3.forceSimulation(nodes)
126-
.force("link", d3.forceLink(links).id(d => d.id).strength(0.05))
137+
.force("link", d3.forceLink(links)
138+
.id(d => d.id)
139+
.strength(0.05)
140+
.distance((d) => (d.title ? title_circle_radius*10 : circle_radius*10 )))
127141
.force("charge", d3.forceManyBody().strength(-300))
128-
.force("overlap", d3.forceCollide())
142+
.force("overlap", d3.forceCollide((d) => (d.title ? title_circle_radius : circle_radius)*1.25).strength(1))
129143
.force("x", d3.forceX())
130144
.force("y", d3.forceY())
131-
.force("radial", d3.forceRadial(d => d.level * 300).strength(0.7));
132-
133-
svg.append("defs").append("marker")
134-
.attr("id", "arrow")
135-
.attr("viewBox", `0 0 ${arrowhead_size} ${arrowhead_size}`) // simplified
136-
.attr("refX", circle_radius + arrowhead_size * 0.6) // offset by radius + a bit
137-
.attr("refY", arrowhead_size / 2)
138-
.attr("markerWidth", arrowhead_size)
139-
.attr("markerHeight", arrowhead_size)
140-
.attr("orient", "auto")
141-
.append("path")
142-
.attr("d", `M0,0 L${arrowhead_size},${arrowhead_size / 2} L0,${arrowhead_size} Z`)
143-
.attr("fill", "#999");
145+
// .force("center", d3.forceCenter(centerX, centerY));
146+
.force("radial", d3.forceRadial(d => d.level * 300, centerX, centerY).strength(0));
144147

145148
// Creates a group that holds everything in the svg (ie nodes, link, etc)
146149
const container = svg.append("g")
@@ -157,9 +160,6 @@ <h2 style="text-align: center;">
157160
.attr("fill", "none");
158161

159162

160-
161-
162-
163163
// Defines a zoom interaction that occurs on whatever elements calls it
164164
const zoom = d3.zoom()
165165
.scaleExtent([0.1, 5])
@@ -170,49 +170,117 @@ <h2 style="text-align: center;">
170170
// Binds the defined zoom interaction to the entire svg element
171171
svg.call(zoom)
172172

173+
const defs = svg.insert("defs", ":first-child");
174+
const arrowhead_size = link_stroke_width*1.5;
175+
for (let i = 1; i <= {{ groups|length }}; i++) {
176+
defs.append("marker")
177+
.attr("id", `head-${i}`)
178+
.attr("viewBox", `0 -${arrowhead_size} ${arrowhead_size*2} ${arrowhead_size*2}`)
179+
.attr("markerUnits", "userSpaceOnUse")
180+
.attr("refX", circle_radius + arrowhead_size*1.5)
181+
.attr("refY", 0)
182+
.attr("markerWidth", arrowhead_size*2)
183+
.attr("markerHeight", arrowhead_size*2)
184+
.attr("orient", "auto")
185+
.append("path")
186+
.attr("d", `M 0 -${arrowhead_size} L ${arrowhead_size*2} 0 L 0 ${arrowhead_size}`)
187+
.attr("fill", color(i));
188+
}
189+
173190
// Creates the links that appear
174191
const link = container.append("g")
175-
.attr("stroke", "#aaa")
176-
.attr("stroke-opacity", 0.6)
192+
.attr("class", "links")
177193
.selectAll("line")
178-
.data(links)
179-
.enter()
180-
.append("line")
181-
.attr("stroke-width", 1.5)
182-
.attr("marker-end", "url(#arrow)");
194+
.data(links)
195+
.enter().append("path")
196+
.attr("fill", "none")
197+
.attr("stroke-width", link_stroke_width)
198+
.attr("stroke", d => color(
199+
nodeById.get(
200+
typeof d.source === "object" ? d.source.id : d.source
201+
).group))
202+
.attr("marker-end", d => {
203+
const g = nodeById.get(
204+
typeof d.source==="object" ? d.source.id : d.source
205+
).group;
206+
return `url(#head-${g})`;
207+
});
183208

184209
// Creates the nodes that appear
185210
const node = container.append("g")
211+
.attr("class", "nodes")
186212
.selectAll("g")
187-
.data(nodes)
188-
.enter()
189-
.append("g")
213+
.data(nodes)
214+
.enter().append("g")
190215
.call(d3.drag()
191216
.on("start", dragstarted)
192217
.on("drag", dragged)
193218
.on("end", dragended));
194219

195-
node.append("circle")
196-
.attr("r", circle_radius)
197-
.attr("fill", d => color(d.group));
220+
circles = node.append("a")
221+
.attr("xlink:href", d => d.link)
222+
.attr("target", "_blank")
223+
.append("circle")
224+
.attr("r", d => d.title ? title_circle_radius : circle_radius)
225+
.attr("fill", d => color(d.group));
198226

199-
node.append("a")
200-
.attr("xlink:href", d => d.link)
201-
.attr("target", "_blank")
202-
.append("text")
203-
.attr("text-anchor", "middle")
204-
.attr("alignment-baseline", "middle")
227+
labels = node.append("a")
228+
.attr("xlink:href", d => d.link)
229+
.attr("target", "_blank")
230+
.append("text")
231+
.attr("text-anchor", "middle")
232+
.attr("alignment-baseline", "middle")
205233
.text(d => d.id)
206234

207235
node.append("title")
208236
.text(d => d.id);
237+
238+
node.on("mouseover", function(d) {
239+
d = d.srcElement.__data__;
240+
circles
241+
.attr("opacity", 0.6);
242+
243+
let marked = new Set(),
244+
queue = [];
245+
queue.push(d.id);
246+
while (queue.length) {
247+
const id0 = queue.shift();
248+
if (marked.has(id0)) continue;
249+
marked.add(id0);
250+
queue = [...queue, ...nodeById.get(id0).bk];
251+
}
252+
circles
253+
.filter(d2 => marked.has(d2.id))
254+
.attr("opacity", 1);
255+
circles
256+
.filter(d2 => d2.id === d.id)
257+
.attr("stroke", "black")
258+
.attr("stroke-width", node_stroke_width * 3);
259+
link.filter(l => {
260+
return !marked.has(l.source.id) || !marked.has(l.target.id);
261+
})
262+
.attr("opacity", 0.05);
263+
svg.selectAll("text")
264+
.filter(d2 => d2.group !== d.group)
265+
.attr("opacity", 0.5);
266+
svg.selectAll("text")
267+
.filter(d2 => d2.id === d.id)
268+
.attr("font-weight", "bold");
269+
})
270+
.on("mouseout", function() {
271+
svg.selectAll("circle")
272+
.attr("opacity", 1)
273+
.attr("stroke", node_stroke)
274+
.attr("stroke-width", node_stroke_width);
275+
link.attr("opacity", 1);
276+
svg.selectAll("text")
277+
.attr("opacity", 1)
278+
.attr("font-weight", "normal");
279+
});
209280

210281
simulation.on("tick", () => {
211282
link
212-
.attr("x1", d => d.source.x)
213-
.attr("y1", d => d.source.y)
214-
.attr("x2", d => d.target.x)
215-
.attr("y2", d => d.target.y);
283+
.attr("d", d => `M ${d.source.x} ${d.source.y} L ${d.target.x} ${d.target.y}`);
216284

217285
node
218286
.attr("transform", d => `translate(${d.x},${d.y})`)
@@ -230,9 +298,14 @@ <h2 style="text-align: center;">
230298
}
231299

232300
function dragended(event, d) {
233-
if (!event.active) simulation.alphaTarget(0);
234-
d.fx = null;
235-
d.fy = null;
301+
if (isEditMode) {
302+
d.fx = event.x;
303+
d.fy = event.y;
304+
} else {
305+
if (!event.active) simulation.alphaTarget(0);
306+
d.fx = null;
307+
d.fy = null;
308+
}
236309
}
237310

238311

0 commit comments

Comments
 (0)