Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
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();
const [calendarData, setCalendarData] = useState([]);

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);
});
}, [routeParams]);

const releaseDate = "2025-10-10";
// 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";

const option = {
title: {
text: "Project Worksession Heatmap (Class-Wide)",
left: "center",
},
tooltip: {
trigger: "item",
formatter: (params) => {
const [date, count, name] = params.data;
return `<b>${name}</b><br />${date}: ${count} backups`;
},
},
// This controls the color of the dots based on the 'count' (index 1)
visualMap: {
min: 0,
max: Math.max(...calendarData.map((val) => val[1])),
calculable: true,
orient: "vertical",
right: "5%",
top: "center",
dimension: 1, // Point to the 'count' value in the data array
inRange: {
color: ["#ebedf0", "#c6e48b", "#7bc96f", "#239a3b", "#196127"],
},
},
calendar: {
orient: "vertical",
top: 100,
bottom: 40,
left: 80,
right: 150,
cellSize: ["auto", "auto"], // larger for jitter grid
range: [releaseDate, dueDate],
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: calendarData,

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 (
<>
{calendarData.length === 0 ? (
<CircularProgress />
) : (
<div style={{ width: "100%", background: "#fff", padding: "20px" }}>
<ReactECharts
option={option}
style={{ height: "500px", width: "100%" }}
/>
</div>
)}
</>
);
};

export default MultiStudentCalendar;
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -288,6 +289,7 @@ function SummaryTab({}) {
<CircularProgress />
)}

<MultiStudentCalendar />
<BackupCalendarChart />
<BackupGanttPlot />

Expand Down
2 changes: 1 addition & 1 deletion src/snapshots-app/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading