Skip to content

Commit

Permalink
Add grade history chart
Browse files Browse the repository at this point in the history
  • Loading branch information
PurelyAnecdotal committed Jan 3, 2025
1 parent 2192324 commit 8940e35
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 1 deletion.
18 changes: 18 additions & 0 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
"": {
"dependencies": {
"buffer": "^6.0.3",
"chart.js": "^4.4.7",
"chartjs-adapter-date-fns": "^3.0.0",
"chartjs-plugin-gradient": "^0.6.1",
"chartjs-scale-timestack": "^1.0.1",
"fast-xml-parser": "^4.5.1",
"file-type": "^19.6.0",
},
Expand Down Expand Up @@ -141,6 +145,8 @@

"@jridgewell/trace-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],

"@kurkle/color": ["@kurkle/[email protected]", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="],

"@mapbox/node-pre-gyp": ["@mapbox/[email protected]", "", { "dependencies": { "consola": "^3.2.3", "detect-libc": "^2.0.0", "https-proxy-agent": "^7.0.5", "node-fetch": "^2.6.7", "nopt": "^8.0.0", "semver": "^7.5.3", "tar": "^7.4.0" }, "bin": { "node-pre-gyp": "bin/node-pre-gyp" } }, "sha512-nhSMNprz3WmeRvd8iUs5JqkKr0Ncx46JtPxM3AhXes84XpSJfmIwKeWXRpsr53S7kqPkQfPhzrMFUxSNb23qSA=="],

"@nodelib/fs.scandir": ["@nodelib/[email protected]", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
Expand Down Expand Up @@ -325,6 +331,14 @@

"chalk": ["[email protected]", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],

"chart.js": ["[email protected]", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw=="],

"chartjs-adapter-date-fns": ["[email protected]", "", { "peerDependencies": { "chart.js": ">=2.8.0", "date-fns": ">=2.0.0" } }, "sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg=="],

"chartjs-plugin-gradient": ["[email protected]", "", { "peerDependencies": { "chart.js": ">=2.6.0" } }, "sha512-TGHNIh8KqQMLdb+UfY80cBHYRyOC47eeokmgkeajRdKGbFt462lJiyiq4ZJ25fiM7BGsmzoBLhmVyEw4B3gQxw=="],

"chartjs-scale-timestack": ["[email protected]", "", { "dependencies": { "luxon": ">=3" }, "peerDependencies": { "chart.js": ">=4" } }, "sha512-Nzi3BVNqoboo8534ecFIY1XojfdbQ9xfjdFsNxcGbYeAVrRthzmxlixelVtfAPqUExINH1LA1L3tfBYCeobp3w=="],

"chokidar": ["[email protected]", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],

"chownr": ["[email protected]", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
Expand All @@ -351,6 +365,8 @@

"cssesc": ["[email protected]", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],

"date-fns": ["[email protected]", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],

"debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],

"decode-bmp": ["[email protected]", "", { "dependencies": { "@canvas/image-data": "^1.0.0", "to-data-view": "^1.1.0" } }, "sha512-NiOaGe+GN0KJqi2STf24hfMkFitDUaIoUU3eKvP/wAbLe8o6FuW5n/x7MHPR0HKvBokp6MQY/j7w8lewEeVCIA=="],
Expand Down Expand Up @@ -565,6 +581,8 @@

"lru-cache": ["[email protected]", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],

"luxon": ["[email protected]", "", {}, "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ=="],

"magic-string": ["[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],

"merge2": ["[email protected]", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@
},
"dependencies": {
"buffer": "^6.0.3",
"chart.js": "^4.4.7",
"chartjs-adapter-date-fns": "^3.0.0",
"chartjs-plugin-gradient": "^0.6.1",
"chartjs-scale-timestack": "^1.0.1",
"fast-xml-parser": "^4.5.1",
"file-type": "^19.6.0"
},
Expand Down
2 changes: 1 addition & 1 deletion src/lib/assignments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export interface NewHypotheticalAssignment extends ReactiveAssignment {
date: undefined;
}

type Calculable<T extends Assignment> = T & {
export type Calculable<T extends Assignment> = T & {
pointsEarned: number;
pointsPossible: number;
notForGrade: false;
Expand Down
40 changes: 40 additions & 0 deletions src/lib/components/Line.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<!-- inspired by https://github.com/SauravKanchan/svelte-chartjs/issues/158#issuecomment-2456212827 -->

<script lang="ts">
import { Chart, type ChartData, type ChartOptions, type Point } from 'chart.js';
import 'chart.js/auto';
import 'chartjs-adapter-date-fns';
import gradient from 'chartjs-plugin-gradient';
import 'chartjs-scale-timestack';
import type { HTMLCanvasAttributes } from 'svelte/elements';
interface Props extends HTMLCanvasAttributes {
data: ChartData<'line', number[] | Point[], string>;
options: ChartOptions<'line'>;
}
const { data, options, ...rest }: Props = $props();
let canvasElem: HTMLCanvasElement;
let chart: Chart;
$effect(() => {
chart = new Chart(canvasElem, {
type: 'line',
data,
options,
plugins: [gradient]
});
return () => chart.destroy();
});
$effect(() => {
if (chart) {
chart.data = data;
chart.update();
}
});
</script>

<canvas bind:this={canvasElem} {...rest}></canvas>
103 changes: 103 additions & 0 deletions src/routes/(authed)/grades/[index]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { page } from '$app/state';
import { removeClassID } from '$lib';
import {
type Calculable,
calculateAssignmentGPCs,
calculateAssignmentGPCsFromCategories,
calculateAssignmentGPCsFromTotals,
Expand All @@ -21,7 +22,9 @@
type ReactiveAssignment,
type RealAssignment
} from '$lib/assignments';
import Line from '$lib/components/Line.svelte';
import NumberFlow from '@number-flow/svelte';
import type { ChartData, ChartOptions, Point } from 'chart.js';
import {
Alert,
Button,
Expand Down Expand Up @@ -191,6 +194,100 @@
const unseenAssignments = $derived(
realAssignments.filter(({ id }) => !seenAssignmentIDs.has(id))
);
const assignmentsByDate = $derived.by(() => {
const map: Map<number, Calculable<RealAssignment>[]> = new Map();
getCalculableAssignments(realAssignments).forEach((assignment) => {
const ms = assignment.date.getTime();
const assignments = map.get(ms) ?? [];
map.set(ms, [...assignments, assignment]);
});
return map;
});
const dataPoints: Point[] = $derived.by(() => {
const entries = [...assignmentsByDate.entries()].toSorted(([ms_a], [ms_b]) => ms_a - ms_b);
return entries
.map(([ms], i) => {
const assignmentsUntil = entries
.map((entry) => entry[1])
.slice(0, i + 1)
.flat();
const grade = gradeCategories
? calculateCourseGradePercentageFromCategories(
getPointsByCategory(assignmentsUntil),
gradeCategories
)
: calculateCourseGradePercentageFromTotals(assignmentsUntil);
return { x: ms, y: grade };
})
.filter((x) => x !== null);
});
const chartData: ChartData<'line', Point[], string> = $derived({
datasets: [
{
data: dataPoints,
fill: 'start',
borderColor: '#FE795D',
borderWidth: 2,
pointBackgroundColor: '#FE795D',
pointHoverBackgroundColor: '#FE795D',
pointBorderWidth: 0,
pointHoverBorderWidth: 0,
pointRadius: 4,
pointHoverRadius: 8,
pointHitRadius: 16,
gradient: {
backgroundColor: {
axis: 'y',
colors: {
0: 'transparent',
100: '#CC4522'
}
}
}
}
]
});
const dayFormatter = new Intl.DateTimeFormat('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric'
});
const percentFormatter = new Intl.NumberFormat('en-US', {
style: 'percent',
maximumFractionDigits: 3
});
const chartOptions: ChartOptions<'line'> = {
scales: {
x: {
// @ts-expect-error timestack is provided by chartjs-scale-timestack
type: 'timestack',
time: { unit: 'day' }
}
},
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
title: (context) => dayFormatter.format(context[0].parsed.x),
label: (context) => percentFormatter.format(context.parsed.y / 100)
}
}
},
maintainAspectRatio: false,
parsing: false,
normalized: true
};
</script>

<svelte:head>
Expand All @@ -217,6 +314,12 @@
</span>
</div>

{#if rawGradeCalcMatches}
<div class="h-64">
<Line data={chartData} options={chartOptions} class="h-64" />
</div>
{/if}

{#if categories && gradeCategories && totalCategory}
<div class="sm:mx-4">
<Table shadow divClass="overflow-x-auto">
Expand Down

0 comments on commit 8940e35

Please sign in to comment.