Published unlisted
Edited
Jun 20, 2022
Fork of Simple D3
1 fork
Importers
Insert cell
Insert cell
Insert cell
Insert cell
viewof settings = Inputs.form({
add_ancestors : Inputs.radio(["Yes", "No"], {label: "Show ancestors of matching courses too", value: "Yes"}),
use_dynamic_data : Inputs.radio(["Yes", "No"], {label: "Load the data dynamically from googe docs", value: "Yes"}),
which_arrows: Inputs.checkbox(["Required", "Useful"], {label: "Show arrows for useful or required courses", value: ["required"], valueof: w => w.toLowerCase()}),
})
Insert cell
//This serves as a global flag to tell observable when to update things
// that get changed by the onclick callbacks attached to the topic and language buttons
// This seems like a hack but I don't know the correct way to do it in observable
mutable update_button_state = true;
Insert cell
//by inserting the update_button_state; line, this cell will autoupdate when a button is clicked
selected_languages = {
update_button_state;
return [...languages].filter(l => selection.language[l])
}
Insert cell
//by inserting the update_button_state; line, this cell will autoupdate when a button is clicked
selected_topics = {
update_button_state;
return [...topics].filter(t => selection.topic[t])
}
Insert cell
selection =
{
let initially_selected_topics = ["Further programming"];
let initally_selected_languages = ["Python", "R"];
return {
topic : _.zipObject([...topics], [...topics].map(t => initially_selected_topics.includes(t))),
language : _.zipObject([...languages], [...languages].map(l => initally_selected_languages.includes(l)))
};
}
Insert cell
Insert cell
shadow_style_on = "5px 5px";
Insert cell
shadow_style_off = "0px 0px";
Insert cell
// function add_shadow(course, shadow_style) {
// d3.select(`p.course[id="${course}"]`)
// .style("box-shadow", shadow_style)
// }
function add_shadow(course, start, end) {
d3.select(`p.course[id="${course}"]`)
.transition()
.duration(300)
.styleTween("box-shadow", function() {
let i = d3.interpolate(start, end);
return function(t) {
return `${i(t)}px ${i(t)}px #888888`;
};
});
}
Insert cell
{
page;
function highlight_matching_courses(key="Packages and Tools", type="topics", shadow_style) {
let matching_courses = filtered_courses.filter(name => course_info[name][type].includes(key));
matching_courses.map(course => add_shadow(course, ...shadow_style));
}

function register_hover(key, type) {
let el = document.querySelector(`button[value="${key}"]`);
['mouseenter','focusin','touchstart'].forEach(
event_name => el.addEventListener(event_name,
event => highlight_matching_courses(key, type, [0,5])));
['mouseleave','focusout','touchend','touchcancel'].forEach(
event_name => el.addEventListener(event_name,
event => highlight_matching_courses(key, type, [5,0])));
}

[...topics].map(t => register_hover(t, "topics"));
[...languages].map(l => register_hover(l, "languages"));

}
Insert cell
Insert cell
LeaderLine = require('leader-line@1.0.7/leader-line.min.js').catch(() => window["LeaderLine"])
Insert cell
import { button } from "@bartok32/diy-inputs"
Insert cell
height = 100
Insert cell
import {course_info as course_info_dynamic} from "@tomhodson/learning-paths-importer"
Insert cell
course_info_dynamic
Insert cell
course_info_static = FileAttachment("course_info@1.json").json()
Insert cell
course_info = settings.use_dynamic_data === "Yes" ? course_info_dynamic : course_info_static;
Insert cell
Insert cell
courses = (Object.keys(course_info))
Insert cell
parents = _.zipObject(courses, courses.map(c => _.concat(...settings.which_arrows.map(w => course_info[c][w]))))
Insert cell
course_graph_data = {
let data = _.zipObject(courses, courses.map(name => ({
"name" : name,
"children": [], //courses that either require or consider this course useful
"parents": parents[name], //courses that are required or useful for this course
})))

// iterate over all courses, and add that course to the children array of all its parents
_.mapValues(data, child => {
child.parents.forEach(parent => {
console.log(child.name, parent);
data[parent].children.push(child.name)
})
})

return data
}
Insert cell
languages = new Set(courses.flatMap(k => course_info[k].languages))
Insert cell
topics = new Set(courses.flatMap(k => course_info[k].topics))
Insert cell
generations = {
function generation(name) {;
return 1 + parents[name].filter(c => filtered_courses.includes(c)).map(generation).reduce((a, b) => Math.max(a,b), -1);
}
return _.zipObject(courses, courses.map(generation));
}
Insert cell
max_generation = _.max(filtered_courses.map(c => generations[c]), 1)
Insert cell
min_generation = _.min(filtered_courses.map(c => generations[c]), 1)
Insert cell
function ancestors(name) {;
return [name, ...parents[name].flatMap(parent => ancestors(parent))];
}
Insert cell
Insert cell
just_matching_courses = courses
.filter(name => selected_languages.some(lang => course_info[name].languages.includes(lang)))
.filter(name => selected_topics.some(topic => course_info[name].topics.includes(topic)))
Insert cell
matching_courses_with_ancestors = _.uniq(just_matching_courses.flatMap(ancestors))
Insert cell
filtered_courses = settings.add_ancestors === "Yes" ? matching_courses_with_ancestors : just_matching_courses
Insert cell
n_children = _.zipObject(filtered_courses, filtered_courses.map(p => course_graph_data[p].children
.filter(c => filtered_courses.includes(c)).length))
Insert cell
//compute the biggest parent of each course in column 2
//For a given course c, the biggest parent is the parent of c with the most displayed children
biggest_parent = _.zipObject(filtered_courses, filtered_courses.map(c => _.maxBy(
course_graph_data[c].parents //get the parents of every course
.filter(p => filtered_courses.includes(p)), //only the ones we're displaying
p => n_children[p]) //take the parent with the most children
))
Insert cell
wind_about_center = function(arr) {
let start_or_end = true;
let vals = []
arr.forEach(v => {
vals.splice(start_or_end ? 0 : vals.length, 0, v);
start_or_end = !start_or_end;
})
return vals
}
Insert cell
groups = {
let g = _.groupBy(filtered_courses, n => generations[n]);

//order the courses in group 0 based on how many children they have
g[0] = _.sortBy(g[0], c => -n_children[c]);
g[0] = wind_about_center(g[0])
//order the courses in group n bases on where their dominant parent is in group n-1
_.range(1, _.size(g)).forEach(i =>
g[i] = _.sortBy(g[i], c => g[i-1].indexOf(biggest_parent[c]))
)

return g
}
Insert cell
course_plot_data = {
let data = _.zipObject(courses, courses.map(name => ({
"name" : name,
"col" : generations[name],
"row" : groups[generations[name]].indexOf(name),
"col_height" : groups[generations[name]].length,
})))
return data
}
Insert cell
courses_to_lines = {
let lines = [];
let courses_to_lines = _.zipObject(filtered_courses, filtered_courses.map(c => ({from:[], to:[]})));
page
filtered_courses.forEach(parent => {
course_graph_data[parent].children.forEach(child => {
// console.log(parent, child);
let useful = course_info[child].useful.includes(parent);
// console.log(useful);
let p = document.getElementById(parent);
let c = document.getElementById(child);
if(p && c) {
let l = new LeaderLine(
p, c,
{
color : "darkslategrey",
size : 2,
dash: useful,
hide: false,
showEffectName: 'draw',
animOptions: {duration: 50, timing: [0.58, 0, 0.42, 1]}
},
);
lines.push(l);
//populate a map giving all the lines to and from a given course
courses_to_lines[parent].from.push(l)
courses_to_lines[child].to.push(l)
}

})
});

lines.forEach(line => line.show())
invalidation.then(() => lines.forEach(line => line.remove()));
return courses_to_lines;
}


Insert cell
{
let course_dom_elements = document.querySelectorAll("p.course");

['mouseenter','focusin','touchstart'].forEach(event_name =>
_.map(course_dom_elements, el => el.addEventListener(event_name, event => {
['from', 'to'].forEach(w => courses_to_lines[el.id][w].map(line => line.color = 'lightslategrey'));
ancestors(el.id).map(a => add_shadow(a, 0,5));
})));

['mouseleave','focusout','touchend','touchcancel'].forEach(event_name =>
_.map(course_dom_elements, el => el.addEventListener(event_name, event => {
['from', 'to'].forEach(w => filtered_courses.forEach(c => courses_to_lines[c][w].map(line => line.color = 'black')));
ancestors(el.id).map(a => add_shadow(a, 5,0));
})));


}

Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more