From 063749dfed8dc230f7f405a0a88b2bc3ee29f1f4 Mon Sep 17 00:00:00 2001 From: Rebecca Dang Date: Tue, 28 Apr 2026 06:49:58 -0700 Subject: [PATCH 1/7] Start BackupCalendarChart on Sun; clarify day of the week labels --- .../submission/tabs/summary/BackupCalendarChart.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/snapshots-app/client/bundles/components/submission/tabs/summary/BackupCalendarChart.jsx b/src/snapshots-app/client/bundles/components/submission/tabs/summary/BackupCalendarChart.jsx index 24f3b4d..b0300f2 100644 --- a/src/snapshots-app/client/bundles/components/submission/tabs/summary/BackupCalendarChart.jsx +++ b/src/snapshots-app/client/bundles/components/submission/tabs/summary/BackupCalendarChart.jsx @@ -82,8 +82,8 @@ const BackupCalendarChart = () => { cellSize: [40, "auto"], yearLabel: { show: false }, dayLabel: { - firstDay: 1, - nameMap: "en", + firstDay: 0, + nameMap: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], }, monthLabel: { position: "start", // Places month names to the left of the grid From 263e06ec736520f778b37e28cc92e53a84f93e00 Mon Sep 17 00:00:00 2001 From: Rebecca Dang Date: Tue, 28 Apr 2026 06:50:12 -0700 Subject: [PATCH 2/7] Add MultiStudentCalendar with dummy data --- .../tabs/summary/MultiStudentCalendar.jsx | 126 ++++++++++++++++++ .../submission/tabs/summary/SummaryTab.jsx | 2 + 2 files changed, 128 insertions(+) create mode 100644 src/snapshots-app/client/bundles/components/submission/tabs/summary/MultiStudentCalendar.jsx diff --git a/src/snapshots-app/client/bundles/components/submission/tabs/summary/MultiStudentCalendar.jsx b/src/snapshots-app/client/bundles/components/submission/tabs/summary/MultiStudentCalendar.jsx new file mode 100644 index 0000000..df2d33b --- /dev/null +++ b/src/snapshots-app/client/bundles/components/submission/tabs/summary/MultiStudentCalendar.jsx @@ -0,0 +1,126 @@ +import React, { useMemo } from "react"; +import ReactECharts from "echarts-for-react"; + +const MultiStudentCalendar = () => { + // Dummy Data: Simulating the schema { [date]: { [studentName]: count } } + const rawData = { + "2024-03-01": { "Alice": 12, "Bob": 5, "Charlie": 2 }, + "2024-03-02": { "Alice": 2, "Bob": 15, "Charlie": 8 }, + "2024-03-05": { "Alice": 20, "Bob": 1, "Charlie": 5 }, + "2024-03-10": { "Alice": 5, "Bob": 5, "Charlie": 5 }, + "2024-03-15": { "Alice": 8, "Bob": 12, "Charlie": 25 }, + }; + const startDate = "2024-03-01"; + const endDate = "2024-03-31"; + + // Flatten data for ECharts scatter series + // Format: [ [date, count, studentName], ... ] + const scatterData = useMemo(() => { + const points = []; + const sortedDates = Object.keys(rawData).sort((a, b) => new Date(a) - new Date(b)); + + sortedDates.forEach((date) => { + const studentCounts = rawData[date]; + const sortedStudents = Object.keys(studentCounts).sort(); + + sortedStudents.forEach((name, localIndex) => { + // We add the localIndex as the 4th element [3] + points.push([date, studentCounts[name], name, localIndex]); + }); + }); + + return points; + }, [rawData]); + + + const option = { + title: { + text: "Project Worksession Heatmap (Class-Wide)", + left: "center", + }, + tooltip: { + trigger: "item", + formatter: (params) => { + const [date, count, name] = params.data; + return `${name}
${date}: ${count} backups`; + }, + }, + // This controls the color of the dots based on the 'count' (index 1) + visualMap: { + min: 0, + max: 25, + calculable: true, + orient: "horizontal", + left: "center", + bottom: 20, + dimension: 1, // Point to the 'count' value in the data array + inRange: { + color: ["#ebedf0", "#c6e48b", "#7bc96f", "#239a3b", "#196127"], + }, + }, + calendar: { + orient: "vertical", + top: 80, + left: 30, + right: 30, + cellSize: [50, 50], // larger for jitter grid + range: [startDate, endDate], + itemStyle: { + borderWidth: 0.5, + borderColor: "#ccc", + }, + yearLabel: { show: false }, + dayLabel: { + firstDay: 0, + nameMap: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], + }, + monthLabel: { + position: "start", // Places month names to the left of the grid + margin: 20, + }, + }, + + series: [{ + type: "custom", + coordinateSystem: "calendar", + data: scatterData, + + renderItem: (params, api) => { + const cellPoint = api.coord(api.value(0)); + const studentIndex = api.value(3); // index of student for this day + + // Define a grid inside the cell (e.g., 5x5 grid) + const columns = 5; + const spacing = 8; // Pixels between dots + + const row = Math.floor(studentIndex / columns); + const col = studentIndex % columns; + + // Center the grid within the cell + const startX = cellPoint[0] - (columns * spacing) / 2; + const startY = cellPoint[1] - (columns * spacing) / 2; + + return { + type: "circle", + shape: { + cx: startX + col * spacing, + cy: startY + row * spacing, + r: 3, // Smaller radius for high density + }, + style: api.style() + }; + } + }], + }; + + return ( +
+ +
+ ); +}; + +export default MultiStudentCalendar; \ No newline at end of file diff --git a/src/snapshots-app/client/bundles/components/submission/tabs/summary/SummaryTab.jsx b/src/snapshots-app/client/bundles/components/submission/tabs/summary/SummaryTab.jsx index 1c54f95..39fdd27 100644 --- a/src/snapshots-app/client/bundles/components/submission/tabs/summary/SummaryTab.jsx +++ b/src/snapshots-app/client/bundles/components/submission/tabs/summary/SummaryTab.jsx @@ -27,6 +27,7 @@ import StatisticsDashboard from "./StatisticsDashboard"; import InfoTooltip from "../../../common/InfoTooltip"; import BackupGanttPlot from "./BackupGanttPlot"; import BackupCalendarChart from "./BackupCalendarChart"; +import MultiStudentCalendar from "./MultiStudentCalendar"; const SCORE_HISTOGRAM_OPTIONS = { histogram: { @@ -288,6 +289,7 @@ function SummaryTab({}) { )} + From 427b1701bcfc50a27b700e15e3cd31cd641e107e Mon Sep 17 00:00:00 2001 From: Rebecca Dang Date: Tue, 28 Apr 2026 07:12:05 -0700 Subject: [PATCH 3/7] Update problem calendar controller to compute multi student calendar data --- .../api/problem_calendar_controller.rb | 39 +++++++++---- .../tabs/summary/MultiStudentCalendar.jsx | 56 +++++++++---------- src/snapshots-app/config/routes.rb | 2 +- 3 files changed, 54 insertions(+), 43 deletions(-) diff --git a/src/snapshots-app/app/controllers/api/problem_calendar_controller.rb b/src/snapshots-app/app/controllers/api/problem_calendar_controller.rb index 9cebbbb..9c03544 100644 --- a/src/snapshots-app/app/controllers/api/problem_calendar_controller.rb +++ b/src/snapshots-app/app/controllers/api/problem_calendar_controller.rb @@ -19,19 +19,34 @@ def show student = course.students.find_by(id: user_id) if student.nil? - render json: { "error": "User ID #{user_id} not a student in course ID #{course_id}" }, status: :not_found - return - end - calendar_data = BackupMetadatum - .where( - course: course.okpy_endpoint, - assignment: assignment.okpy_endpoint, - student_email: student.email - ) - .group("date(created)") - .count + query = BackupMetadatum + .where(course: course.okpy_endpoint, assignment: assignment.okpy_endpoint) + .joins("INNER JOIN users ON users.email = backup_metadata.student_email") + .group("DATE(backup_metadata.created)", "users.email", "users.first_name", "users.last_name") + .order("DATE(backup_metadata.created)", "users.first_name", "users.last_name") + .count + + date_counts = Hash.new(0) + calendar_data = query.map do |(date, email, first_name, last_name), count| + local_index = date_counts[date] + date_counts[date] += 1 + [date, count, "#{first_name} #{last_name}", local_index] + end - render json: calendar_data, status: :ok + render json: calendar_data, status: :ok + + else + calendar_data = BackupMetadatum + .where( + course: course.okpy_endpoint, + assignment: assignment.okpy_endpoint, + student_email: student.email + ) + .group("date(created)") + .count + + render json: calendar_data, status: :ok + end end end diff --git a/src/snapshots-app/client/bundles/components/submission/tabs/summary/MultiStudentCalendar.jsx b/src/snapshots-app/client/bundles/components/submission/tabs/summary/MultiStudentCalendar.jsx index df2d33b..d4afa6c 100644 --- a/src/snapshots-app/client/bundles/components/submission/tabs/summary/MultiStudentCalendar.jsx +++ b/src/snapshots-app/client/bundles/components/submission/tabs/summary/MultiStudentCalendar.jsx @@ -1,37 +1,33 @@ -import React, { useMemo } from "react"; +import React, { useMemo, useEffect, useState } from "react"; import ReactECharts from "echarts-for-react"; +import { useParams } from "react-router"; const MultiStudentCalendar = () => { - // Dummy Data: Simulating the schema { [date]: { [studentName]: count } } - const rawData = { - "2024-03-01": { "Alice": 12, "Bob": 5, "Charlie": 2 }, - "2024-03-02": { "Alice": 2, "Bob": 15, "Charlie": 8 }, - "2024-03-05": { "Alice": 20, "Bob": 1, "Charlie": 5 }, - "2024-03-10": { "Alice": 5, "Bob": 5, "Charlie": 5 }, - "2024-03-15": { "Alice": 8, "Bob": 12, "Charlie": 25 }, - }; - const startDate = "2024-03-01"; - const endDate = "2024-03-31"; - - // Flatten data for ECharts scatter series - // Format: [ [date, count, studentName], ... ] - const scatterData = useMemo(() => { - const points = []; - const sortedDates = Object.keys(rawData).sort((a, b) => new Date(a) - new Date(b)); - - sortedDates.forEach((date) => { - const studentCounts = rawData[date]; - const sortedStudents = Object.keys(studentCounts).sort(); + const routeParams = useParams(); + const [calendarData, setCalendarData] = useState([]); - sortedStudents.forEach((name, localIndex) => { - // We add the localIndex as the 4th element [3] - points.push([date, studentCounts[name], name, localIndex]); + useEffect(() => { + fetch( + `/api/problem_calendar/${routeParams.courseId}/${routeParams.assignmentId}`, + { + method: "GET", + }, + ) + .then((response) => { + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + return response.json(); + }) + .then((responseData) => { + setCalendarData(responseData); }); - }); - - return points; - }, [rawData]); + }, [routeParams]); + const releaseDate = "2025-10-10"; + const checkpointOneDueDate = "2025-10-16"; + const checkpointTwoDueDate = "2025-10-21"; + const dueDate = "2025-10-23"; const option = { title: { @@ -64,7 +60,7 @@ const MultiStudentCalendar = () => { left: 30, right: 30, cellSize: [50, 50], // larger for jitter grid - range: [startDate, endDate], + range: [releaseDate, dueDate], itemStyle: { borderWidth: 0.5, borderColor: "#ccc", @@ -83,7 +79,7 @@ const MultiStudentCalendar = () => { series: [{ type: "custom", coordinateSystem: "calendar", - data: scatterData, + data: calendarData, renderItem: (params, api) => { const cellPoint = api.coord(api.value(0)); diff --git a/src/snapshots-app/config/routes.rb b/src/snapshots-app/config/routes.rb index caca2c2..ea75360 100644 --- a/src/snapshots-app/config/routes.rb +++ b/src/snapshots-app/config/routes.rb @@ -21,7 +21,7 @@ get "backup_file_metadata/:course_id/:assignment_id/:user_id", to: "backup_file_metadata#show" get "summary_statistics/:course_id/:assignment_id/:user_id", to: "summary_statistics#show" get "problem_timeline/:course_id/:assignment_id/:user_id", to: "problem_timeline#show" - get "problem_calendar/:course_id/:assignment_id/:user_id", to: "problem_calendar#show" + get "problem_calendar/:course_id/:assignment_id(/:user_id)", to: "problem_calendar#show" namespace :debugging, defaults: { format: :json } do get "autograder_spam/:course_id/:assignment_id/:user_id", to: "autograder_spam#show" From 04660db151e1d81b725cac1237df7a49192ef822 Mon Sep 17 00:00:00 2001 From: Rebecca Dang Date: Tue, 28 Apr 2026 08:24:05 -0700 Subject: [PATCH 4/7] Fix margins, slider location, and add circular progress when loading --- .../tabs/summary/MultiStudentCalendar.jsx | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/src/snapshots-app/client/bundles/components/submission/tabs/summary/MultiStudentCalendar.jsx b/src/snapshots-app/client/bundles/components/submission/tabs/summary/MultiStudentCalendar.jsx index d4afa6c..dd03b4b 100644 --- a/src/snapshots-app/client/bundles/components/submission/tabs/summary/MultiStudentCalendar.jsx +++ b/src/snapshots-app/client/bundles/components/submission/tabs/summary/MultiStudentCalendar.jsx @@ -1,6 +1,7 @@ -import React, { useMemo, useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; import ReactECharts from "echarts-for-react"; import { useParams } from "react-router"; +import { CircularProgress } from "@mui/material"; const MultiStudentCalendar = () => { const routeParams = useParams(); @@ -25,6 +26,7 @@ const MultiStudentCalendar = () => { }, [routeParams]); const releaseDate = "2025-10-10"; + // TODO highlight checkpoint dates const checkpointOneDueDate = "2025-10-16"; const checkpointTwoDueDate = "2025-10-21"; const dueDate = "2025-10-23"; @@ -44,11 +46,11 @@ const MultiStudentCalendar = () => { // This controls the color of the dots based on the 'count' (index 1) visualMap: { min: 0, - max: 25, + max: Math.max(...calendarData.map((val) => val[1])), calculable: true, - orient: "horizontal", - left: "center", - bottom: 20, + orient: "vertical", + right: "5%", + top: "center", dimension: 1, // Point to the 'count' value in the data array inRange: { color: ["#ebedf0", "#c6e48b", "#7bc96f", "#239a3b", "#196127"], @@ -56,10 +58,11 @@ const MultiStudentCalendar = () => { }, calendar: { orient: "vertical", - top: 80, - left: 30, - right: 30, - cellSize: [50, 50], // larger for jitter grid + top: 100, + bottom: 40, + left: 80, + right: 150, + cellSize: ["auto", "auto"], // larger for jitter grid range: [releaseDate, dueDate], itemStyle: { borderWidth: 0.5, @@ -110,12 +113,16 @@ const MultiStudentCalendar = () => { }; return ( -
- -
+ <> + {calendarData.length === 0 ? :
+ +
+ } + + ); }; From eae884f192eeb9b32088ea9158e9b305f9650a8c Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 15:25:05 +0000 Subject: [PATCH 5/7] Apply Prettier format --- .../tabs/summary/BackupCalendarChart.jsx | 2 +- .../tabs/summary/MultiStudentCalendar.jsx | 76 ++++++++++--------- 2 files changed, 41 insertions(+), 37 deletions(-) diff --git a/src/snapshots-app/client/bundles/components/submission/tabs/summary/BackupCalendarChart.jsx b/src/snapshots-app/client/bundles/components/submission/tabs/summary/BackupCalendarChart.jsx index b0300f2..d3282de 100644 --- a/src/snapshots-app/client/bundles/components/submission/tabs/summary/BackupCalendarChart.jsx +++ b/src/snapshots-app/client/bundles/components/submission/tabs/summary/BackupCalendarChart.jsx @@ -83,7 +83,7 @@ const BackupCalendarChart = () => { yearLabel: { show: false }, dayLabel: { firstDay: 0, - nameMap: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], + nameMap: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], }, monthLabel: { position: "start", // Places month names to the left of the grid diff --git a/src/snapshots-app/client/bundles/components/submission/tabs/summary/MultiStudentCalendar.jsx b/src/snapshots-app/client/bundles/components/submission/tabs/summary/MultiStudentCalendar.jsx index dd03b4b..00546da 100644 --- a/src/snapshots-app/client/bundles/components/submission/tabs/summary/MultiStudentCalendar.jsx +++ b/src/snapshots-app/client/bundles/components/submission/tabs/summary/MultiStudentCalendar.jsx @@ -71,7 +71,7 @@ const MultiStudentCalendar = () => { yearLabel: { show: false }, dayLabel: { firstDay: 0, - nameMap: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], + nameMap: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], }, monthLabel: { position: "start", // Places month names to the left of the grid @@ -79,51 +79,55 @@ const MultiStudentCalendar = () => { }, }, - series: [{ - type: "custom", - coordinateSystem: "calendar", - data: calendarData, + series: [ + { + type: "custom", + coordinateSystem: "calendar", + data: calendarData, - renderItem: (params, api) => { - const cellPoint = api.coord(api.value(0)); - const studentIndex = api.value(3); // index of student for this day + renderItem: (params, api) => { + const cellPoint = api.coord(api.value(0)); + const studentIndex = api.value(3); // index of student for this day - // Define a grid inside the cell (e.g., 5x5 grid) - const columns = 5; - const spacing = 8; // Pixels between dots + // Define a grid inside the cell (e.g., 5x5 grid) + const columns = 5; + const spacing = 8; // Pixels between dots - const row = Math.floor(studentIndex / columns); - const col = studentIndex % columns; + const row = Math.floor(studentIndex / columns); + const col = studentIndex % columns; - // Center the grid within the cell - const startX = cellPoint[0] - (columns * spacing) / 2; - const startY = cellPoint[1] - (columns * spacing) / 2; + // Center the grid within the cell + const startX = cellPoint[0] - (columns * spacing) / 2; + const startY = cellPoint[1] - (columns * spacing) / 2; - return { - type: "circle", - shape: { - cx: startX + col * spacing, - cy: startY + row * spacing, - r: 3, // Smaller radius for high density - }, - style: api.style() - }; - } - }], + return { + type: "circle", + shape: { + cx: startX + col * spacing, + cy: startY + row * spacing, + r: 3, // Smaller radius for high density + }, + style: api.style(), + }; + }, + }, + ], }; return ( <> - {calendarData.length === 0 ? :
- -
- } + {calendarData.length === 0 ? ( + + ) : ( +
+ +
+ )} - ); }; -export default MultiStudentCalendar; \ No newline at end of file +export default MultiStudentCalendar; From b42fdc942248a94a7c5575b23a120a05e9ecd60e Mon Sep 17 00:00:00 2001 From: Rebecca Dang Date: Tue, 28 Apr 2026 08:33:52 -0700 Subject: [PATCH 6/7] Update TODO --- .../components/submission/tabs/summary/MultiStudentCalendar.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/snapshots-app/client/bundles/components/submission/tabs/summary/MultiStudentCalendar.jsx b/src/snapshots-app/client/bundles/components/submission/tabs/summary/MultiStudentCalendar.jsx index dd03b4b..71b0284 100644 --- a/src/snapshots-app/client/bundles/components/submission/tabs/summary/MultiStudentCalendar.jsx +++ b/src/snapshots-app/client/bundles/components/submission/tabs/summary/MultiStudentCalendar.jsx @@ -26,7 +26,7 @@ const MultiStudentCalendar = () => { }, [routeParams]); const releaseDate = "2025-10-10"; - // TODO highlight checkpoint dates + // TODO highlight checkpoint dates (not sure if this is possible with apache echarts in a non-janky way) const checkpointOneDueDate = "2025-10-16"; const checkpointTwoDueDate = "2025-10-21"; const dueDate = "2025-10-23"; From 439087e138a580001e3f4bb9181b6b5648e681bd Mon Sep 17 00:00:00 2001 From: Rebecca Dang Date: Tue, 28 Apr 2026 08:39:21 -0700 Subject: [PATCH 7/7] Run rubocop --- .../app/controllers/api/problem_calendar_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/snapshots-app/app/controllers/api/problem_calendar_controller.rb b/src/snapshots-app/app/controllers/api/problem_calendar_controller.rb index 9c03544..e9ce3cd 100644 --- a/src/snapshots-app/app/controllers/api/problem_calendar_controller.rb +++ b/src/snapshots-app/app/controllers/api/problem_calendar_controller.rb @@ -31,7 +31,7 @@ def show calendar_data = query.map do |(date, email, first_name, last_name), count| local_index = date_counts[date] date_counts[date] += 1 - [date, count, "#{first_name} #{last_name}", local_index] + [ date, count, "#{first_name} #{last_name}", local_index ] end render json: calendar_data, status: :ok