Public
Edited
Jan 29
2 forks
Importers
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart = Plot.plot({
width: width,
height: height,
x: {domain: [0, 110], label: null},
y: {label: "Student", label: null},
style: {fontSize: "16px"},
marginRight: 50,
legend: {marginBottom: 50},
color: {domain: allAssignmnets, range: colorRange, legend: true, width: 500, columns:1},
marks: [
Plot.text(gradeBoundaries, {x: "x", text: "grade", fontSize: "10px", textAnchor: "start", dx: 3, dy: -280}),
Plot.text(gradeBoundaries, {x: "x", text: "grade", fontSize: "10px", textAnchor: "start", dx: 3, dy: 270}),
Plot.ruleX([0, 30, 40, 50, 60, 63, 67, 70, 73, 77, 80, 83, 87, 90, 93, 100]),

//BARS
Plot.barX(tidydata.filter(f => f.points && f.assignment !== "total"), {
x: "points",
y: "emoji",
fill: "assignment",
stroke: "white",
sort: {y: "-x"},
tip: true,
title: d => d.emoji + " " + d.student + "\n\n" + d.assignment.split("(")[0] + "\n\nCurrent grade: " + d.points
}),

//NAMES
Plot.text(tidydata.filter(f => f.assignment === "total"), {
x: 0,
y: "emoji",
text: "student",
textAnchor: "start",
fill: "black",
stroke: "white",
strokeWidth: 3,
fontSize: "14px",
strokeOpacity: 0.4,
paintOrder: "stroke",
pointerEvents: "none",
dx: 10
}),

//FORECAST DUMBBELL
showForecast ?
Plot.link(tidydata.filter(f => f.assignment === "total"), {
x1: forecastMin,
x2: forecastMax,
y1: d => d["emoji"],
y2: d => d["emoji"],
markerEnd: "dot",
markerStart: "dot",
strokeWidth: 2,
stroke: "black"
}) : null,

//FORECAST TEXT
showForecast ?
Plot.text(tidydata.filter(f => f.assignment === "total"), {
x: forecastMin,
y: "emoji",
text: d => d["points"] === d3.max(tidydata, a => a.points) ? "FORECAST low & high": "",
textAnchor: "start",
dx: 5,
dy: -10,
stroke: "white",
strokeWidth: 4,
fontSize: "10px",
paintOrder: "stroke",
fill: "black",
}) : null,

//TOTAL
Plot.text(tidydata.filter(f => f.assignment === "total"), {
x: "points",
y: "emoji",
text: (d) => Math.floor(d.points),
textAnchor: "start",
fill: "black",
stroke: "white",
strokeWidth: 3,
fontSize: "14px",
paintOrder: "stroke",
dx: 2
}),

//MEME
Plot.image(tidydata.slice(0,1), {
x: width,
y: "emoji",
width: 200,
src: "https://i.redd.it/1o8zqjm9bqc81.png"
})
]
})
Insert cell
Insert cell
showForecast = 50 <= percentCourseComplete && percentCourseComplete <= 100
Insert cell
attendancePoints = 24
Insert cell
forecastMin = (d) => (d["points"] - attendancePoints) * forecastFactor + groupProjectMin + attendancePoints
Insert cell
forecastMax = (d) => (d["points"] - attendancePoints) * forecastFactor + groupProjectMax + attendancePoints
Insert cell
gradeBoundaries = [
{x: 0, grade: "F"},
{x: 60, grade: "D-"},
{x: 63, grade: "D"},
{x: 67, grade: "D+"},
{x: 70, grade: "C-"},
{x: 73, grade: "C"},
{x: 77, grade: "C+"},
{x: 80, grade: "B-"},
{x: 83, grade: "B"},
{x: 87, grade: "B+"},
{x: 90, grade: "A-"},
{x: 93, grade: "A"},
{x: 100, grade: "A+"},
]
Insert cell
// a trick to move the legend below the chart and flip the image
{
d3.select(chart).select("div").raise();
d3.select(chart).select("svg").selectAll("image")
.attr("transform","scale(-1,1)")
.attr("x", -width+20)
.attr("y", height-240)
}
Insert cell
width = 928
Insert cell
height = 600
Insert cell
courseEnds = "2024-05-08T0000"
Insert cell
courseStarts = "2024-01-18T0000"
Insert cell
forecastFactor = 100/percentCourseComplete
Insert cell
groupProjectMin = 0
Insert cell
groupProjectMax = 15
Insert cell
files = ({
"2024-01-18T2357": FileAttachment("2024-01-18T2357_Grades-2402046___Data_Visualization@1.csv").csv({typed: true}),
"2024-02-15T1556": FileAttachment("2024-02-15T1556_Grades-2402046___Data_Visualization.csv").csv({typed: true}),
"2024-02-19T2348": FileAttachment("2024-02-19T2348_Grades-2402046___Data_Visualization.csv").csv({typed: true}),
"2024-02-21T0933": FileAttachment("2024-02-21T0933_Grades-2402046___Data_Visualization.csv").csv({typed: true}),
"2024-02-29T1553": FileAttachment("2024-02-29T1553_Grades-2402046___Data_Visualization.csv").csv({typed: true}),
"2024-03-19T2302": FileAttachment("2024-03-19T2302_Grades-2402046___Data_Visualization.csv").csv({typed: true}),
"2024-03-21T1419": FileAttachment("2024-03-21T1419_Grades-2402046___Data_Visualization.csv").csv({typed: true}),
"2024-04-02T1134": FileAttachment("2024-04-02T1134_Grades-2402046___Data_Visualization.csv").csv({typed: true}),
"2024-04-04T2003": FileAttachment("2024-04-04T2003_Grades-2402046___Data_Visualization.csv").csv({typed: true}),
"2024-04-07T1325": FileAttachment("2024-04-07T1325_Grades-2402046___Data_Visualization.csv").csv({typed: true}),
"2024-04-08T0929": FileAttachment("2024-04-08T0929_Grades-2402046___Data_Visualization.csv").csv({typed: true}),
"2024-04-09T2006": FileAttachment("2024-04-09T2006_Grades-2402046___Data_Visualization.csv").csv({typed: true}),
"2024-04-10T2012": FileAttachment("2024-04-10T2012_Grades-2402046___Data_Visualization.csv").csv({typed: true}),
"2024-04-11T1350": FileAttachment("2024-04-11T1350_Grades-2402046___Data_Visualization.csv").csv({typed: true}),
"2024-04-22T1223": FileAttachment("2024-04-22T1223_Grades-2402046___Data_Visualization.csv").csv({typed: true}),
"2024-04-22T2343": FileAttachment("2024-04-22T2343_Grades-2402046___Data_Visualization.csv").csv({typed: true}),
"2024-04-24T1848": FileAttachment("2024-04-24T1848_Grades-2402046___Data_Visualization.csv").csv({typed: true}),
"2024-04-29T1340": FileAttachment("2024-04-29T1340_Grades-2402046___Data_Visualization.csv").csv({typed: true}),
"2024-05-02T1611": FileAttachment("2024-05-02T1611_Grades-2402046___Data_Visualization.csv").csv({typed: true}),
"2024-05-08T2228": FileAttachment("2024-05-08T2228_Grades-2402046___Data_Visualization.csv").csv({typed: true}),
"2024-05-09T1419": FileAttachment("2024-05-09T1419_Grades-2402046___Data_Visualization.csv").csv({typed: true}),
"2024-05-16T1243": FileAttachment("2024-05-16T1243_Grades-2402046___Data_Visualization.csv").csv({typed: true}),
})
Insert cell
timeformat = "%Y-%m-%dT%H%M"
Insert cell
timepoints = Object.keys(files).map(m => d3.utcParse(timeformat)(m))
Insert cell
loadPromise = Promise.all(Object.values(files))
Insert cell
data = loadPromise[Object.keys(files).indexOf(time)]
Insert cell
columns = data.columns
.filter(f => excludeColumnsContaining.every(s => !f.includes(s)) && excludeColumnsEqualTo.every(s => f !== s))
Insert cell
allAssignmnets = columns.filter(f => f!=="name" && f!=="emoji")
Insert cell
excludeColumnsContaining = [" Score", " ID", " Grade"]
Insert cell
excludeColumnsEqualTo = ["ID", "Student", "Section"]
Insert cell
tidydata = applyDataTransformations(data);
Insert cell
applyDataTransformations = function(data) {
const data2 = data.slice(1)
//remove "test student"
.filter(f => f["name"] !== "Points Possible")
//remove unnecessary columns
.map(m => filterObjectProps(m, columns))
//add a total score
.map((m, i) => {m.total = d3.sum(Object.values(m).map(m => m || 0)); return m;})

//unpivot the data table (derp derp, there should be a better way to do that)
const result = []
for (let student of data2) {
for (let assignment of Object.keys(student).filter(f => f!=="name" && f!=="emoji")) {
result.push({student: student.name, emoji: student.emoji, assignment, points: student[assignment]})
}
}
return result
}
Insert cell
d3.select(chart)
.append("svg:style")
.text(`
rect:hover {stroke-width: 3px; stroke: black;}
`)
Insert cell
colorRange = allAssignmnets.map(m => m.slice(0,2))
.map(m => {
switch (m) {
case "⚪️": return "#aaa";
case "🟢": return "#41f71d";
case "🟠": return "#ff9838";
case "⚫️": return "#222";
case "🟤": return "#996100";
case "🔴": return "#ff1a1a";
case "🟣": return "#a64dff";
case "🔵": return "#3d77ff";
}
})
Insert cell
filterObjectProps = function(raw, allowed) {
return Object.keys(raw)
.filter(key => allowed.includes(key))
.reduce((obj, key) => {
obj[key] = raw[key];
return obj;
}, {});
}
Insert cell
import {on} from "@fil/plot-onclick-experimental-plugin"
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