Skip to content

Instantly share code, notes, and snippets.

@acbart
Created February 26, 2025 19:00
Show Gist options
  • Save acbart/8feaae2d63b198e5d4f257a5a8f89eff to your computer and use it in GitHub Desktop.
Save acbart/8feaae2d63b198e5d4f257a5a8f89eff to your computer and use it in GitHub Desktop.
Bookmarklet for visualizing grades in Canvas

This is a bookmarklet, you can save it as a bookmark, and while Canvas is loaded, you click on the bookmark. After a minute, it'll show the statistics for your students across assignments.

javascript:(function()%7B(async function() %7B%0A %0A function startDialog(title%2C body) %7B%0A if (%24('%23dialog').length %3D%3D 0) %7B%0A %24(document.body).append('<div title%3D"' %2B title %2B%0A '" id%3D"dialog"><%2Fdiv>')%3B%0A %7D%0A %24("%23dialog").dialog(%7B%0A autoOpen%3A false%2C%0A show%3A "blind"%2C%0A hide%3A "explode"%2C%0A width%3A '80%25'%2C%0A height%3A document.documentElement.clientHeight - 100%0A %7D)%3B%0A %24("%23dialog").dialog("open")%3B%0A %24('%23dialog').html(body)%3B%0A %7D%0A %0A function stats(arr) %7B%0A %2F%2F sort array ascending%0A const asc %3D arr.sort((a%2C b) %3D> a - b)%3B%0A const sum %3D arr %3D> arr.reduce((a%2C b) %3D> a %2B b%2C 0)%3B%0A const mean %3D sum(arr) %2F arr.length%3B%0A%0A %2F%2F sample standard deviation%0A const mu %3D mean%3B%0A const diffArr %3D arr.map(a %3D> (a - mu) ** 2)%3B%0A const std %3D Math.sqrt(sum(diffArr) %2F (arr.length - 1))%3B%0A%0A const quantile %3D (arr%2C q) %3D> %7B%0A const sorted %3D asc%3B%0A const pos %3D (sorted.length - 1) * q%3B%0A const base %3D Math.floor(pos)%3B%0A const rest %3D pos - base%3B%0A if (sorted%5Bbase %2B 1%5D !%3D%3D undefined) %7B%0A return sorted%5Bbase%5D %2B rest * (sorted%5Bbase %2B 1%5D - sorted%5Bbase%5D)%3B%0A %7D else %7B%0A return sorted%5Bbase%5D%3B%0A %7D%0A %7D%3B%0A%0A const q25 %3D quantile(arr%2C .25)%3B%0A%0A const q50 %3D quantile(arr%2C .50)%3B%0A%0A const q75 %3D quantile(arr%2C .75)%3B%0A %0A const min %3D asc%5B0%5D%3B%0A const max %3D asc%5Basc.length-1%5D%3B%0A%0A const median %3D q50%3B%0A return %5Bmean%2C std%2C min%2C q25%2C median%2C q75%2C max%5D%3B%0A %7D%0A%0A const submissions %3D (await %24.post("https%3A%2F%2Fudel.instructure.com%2Fapi%2Fgraphql"%2C %7B%0A "query"%3A %60query MyQuery(%24courseId%3A ID!) %7B%0A course(id%3A %24courseId) %7B%0A id%0A submissionsConnection %7B%0A nodes %7B%0A score%0A submissionStatus%0A state%0A assignment %7B%0A name%0A dueAt%0A assignmentGroup %7B%0A name%0A %7D%0A %7D%0A %7D%0A %7D%0A %7D%0A %7D%0A %0A %60%2C variables%3A %7BcourseId%3A ENV.COURSE.id%7D%0A %7D))%0A%0A let gathered %3D %7B%7D%3B%0A submissions.data.course.submissionsConnection.nodes.forEach(node %3D> %7B%0A if (!(node.assignment.name in gathered)) %7B%0A gathered%5Bnode.assignment.name%5D %3D %7Bdue%3A node.assignment.dueAt%2C missing%3A 0%2C submitted%3A %5B%5D%2C%0A group%3A node.assignment.assignmentGroup.name%7D%3B%0A %7D%0A const data %3D gathered%5Bnode.assignment.name%5D%3B%0A if (node.assignment.dueAt < data.due) %7B%0A data.due %3D node.assignment.dueAt%3B%0A %7D%0A if (node.state %3D%3D%3D "unsubmitted" %7C%7C node.score %3D%3D%3D 0) %7B data.missing %2B%3D 1 %7D else %7B%0A data.submitted.push(node.score)%3B%0A %7D%0A %7D)%3B%0A gathered %3D Object.entries(gathered).map((%5Bx%2Cy%5D)%3D>(%7Bname%3A x%2C ...y%7D))%3B%0A gathered.sort((a%2C b) %3D> ((a.group%2Ba.name).localeCompare((b.group%2Bb.name))))%3B%0A%0A const TABLE_HEADER %3D "<table class%3D'table table-bordered table-condensed table-striped'>"%3B%0A let table %3D %5B%60%24%7BTABLE_HEADER%7D%0A <tr>%0A <th>Group<%2Fth> %0A <th>Assignment<%2Fth>%0A <th>Missing<%2Fth>%0A <th>Submitted<%2Fth>%0A <th>Mean<%2Fth>%0A <th>Std<%2Fth>%0A <th>Min<%2Fth>%0A <th>25%25<%2Fth>%0A <th>Median<%2Fth>%0A <th>75%25<%2Fth>%0A <th>Max<%2Fth>%0A <%2Ftr>%60%5D%3B%0A gathered.forEach(g %3D> %7B%0A const gStats %3D stats(g.submitted).map(l%3D>Math.round(l*100)%2F100)%3B%0A table.push("<tr>"%2B%5Bg.group%2C g.name%2C g.missing%2C g.submitted.length%2C ...gStats%5D.map(s %3D> %60<td>%24%7Bs%7D<%2Ftd>%60).join("")%2B"<%2Ftr>")%3B%0A %7D)%3B%0A%0A let gradingStandards %3D await %24.get("https%3A%2F%2Fudel.instructure.com%2Fapi%2Fv1%2Fcourses%2F1697956%2Fgrading_standards")%3B%0A if (gradingStandards.length %3D%3D%3D 0) %7B%0A gradingStandards %3D %5B%7Bname%3A "A"%2C value%3A .94%7D%2C %7Bname%3A "A-"%2C value%3A .90%7D%2C %7Bname%3A "B%2B"%2C value%3A .87%7D%2C %7Bname%3A "B"%2C value%3A .84%7D%2C %7Bname%3A "B-"%2C value%3A .80%7D%2C %7Bname%3A "C%2B"%2C value%3A .77%7D%2C %7Bname%3A "C"%2C value%3A .74%7D%2C %7Bname%3A "C-"%2C value%3A .70%7D%2C %7Bname%3A "D%2B"%2C value%3A .67%7D%2C %7Bname%3A "D"%2C value%3A .64%7D%2C %7Bname%3A "D-"%2C value%3A .61%7D%2C %7Bname%3A "F"%2C value%3A 0%7D%5D%3B%0A %7D else %7B%0A gradingStandards %3D gradingStandards%5B0%5D%5B"grading_scheme"%5D%3B%0A %7D%0A const identifyGrade %3D (score) %3D> gradingStandards.find(p %3D> p.value <%3D score).name%3B%0A const grades %3D (await %24.post("https%3A%2F%2Fudel.instructure.com%2Fapi%2Fgraphql"%2C %7B%0A "query"%3A %60query MyQuery(%24courseId%3A ID!) %7B%0A course(id%3A %24courseId) %7B%0A enrollmentsConnection %7B%0A nodes %7B%0A grades %7B%0A finalScore%0A currentScore%0A unpostedFinalScore%0A unpostedCurrentScore%0A %7D%0A sisRole%0A %7D%0A %7D%0A %7D%0A %7D%0A %0A %60%2C variables%3A %7BcourseId%3A ENV.COURSE.id%7D%0A %7D))%0A gathered %3D %7B%7D%3B%0A %2F%2Flet distributions %3D Object.fromEntries(gradingStandards.map(g %3D> %5Bg.name%2C 0%5D))%3B%0A grades.data.course.enrollmentsConnection.nodes%0A .filter((n %3D> n.sisRole %3D%3D%3D 'student'))%0A .forEach(node %3D> %7B%0A Object.entries(node.grades).forEach((%5Bcat%2C score%5D) %3D> %7B%0A if (!(cat in gathered)) %7B%0A gathered%5Bcat%5D %3D %7Bname%3A cat%2C submitted%3A %5B%5D%2C%0A dists%3A Object.fromEntries(gradingStandards.map(g %3D> %5Bg.name%2C 0%5D))%7D%0A %7D%0A gathered%5Bcat%5D.submitted.push(score)%3B%0A gathered%5Bcat%5D.dists%5BidentifyGrade(score%2F100)%5D %2B%3D 1%3B%0A %7D)%3B%0A %7D)%3B%0A gathered %3D Object.entries(gathered).map((%5Bx%2Cy%5D)%3D>(%7Bname%3A x%2C ...y%7D))%3B%0A const afterTable %3D %5B"<table><tr>"%5D%3B%0A gathered.forEach(g %3D> %7B%0A afterTable.push("<td>")%3B%0A const gStats %3D stats(g.submitted).map(l%3D>Math.round(l*100)%2F100)%3B%0A table.push("<tr>"%2B%5B""%2C ""%2C g.name%2C g.submitted.length%2C ...gStats%5D.map(s %3D> %60<td>%24%7Bs%7D<%2Ftd>%60).join("")%2B"<%2Ftr>")%3B%0A afterTable.push(TABLE_HEADER)%3B%0A afterTable.push(%60<tr><th>%24%7Bg.name%7D<%2Fth><th><%2Fth><th><%2Fth><%2Ftr>%60)%3B%0A Object.entries(g.dists).forEach((%5Br%2C s%5D) %3D> %7B%0A const p %3D Math.round(100*s%2Fg.submitted.length)%3B%0A afterTable.push(%60<tr><td>%24%7Br%7D<%2Ftd><td>%24%7Bs%7D<%2Ftd><td>%24%7Bp%7D%25<%2Ftd><%2Ftr>%60)%3B%0A %7D)%3B%0A afterTable.push("<%2Ftable><%2Ftd>")%3B%0A %7D)%3B%0A afterTable.push("<%2Ftr><%2Ftable>")%3B%0A %0A table.push(%60<%2Ftable>%60)%3B%0A startDialog("Assignment Statistics"%2C table.join("") %2B afterTable.join(""))%3B%0A%0A%0A%2F%2F Missing%2C %23 Submitted%2C Mean Submitted Score%2C Std Submitted Score%2C Min%2C Lower Quartile%2C Median Submitted Score%2C Upper Quartile%2C Max%0A%7D)()%3B%7D)()%3B
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment