Skip to content

Commit 4384946

Browse files
committed
Fix the way sessions are computed so that backup gantt plot works
1 parent 3e570f2 commit 4384946

3 files changed

Lines changed: 67 additions & 67 deletions

File tree

Lines changed: 39 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
require 'active_support/time'
1+
require "active_support/time"
22

33
# TODO unit tests
44
class Api::ProblemTimelineController < ApplicationController
@@ -26,7 +26,7 @@ def show
2626
return
2727
end
2828

29-
# TODO error if student doesn't have any backups for this assignment and course
29+
# TODO error if student doesn't have any backups for this assignment and course
3030

3131
problem_name_to_index = AssignmentProblem.where(assignment_id: assignment.id)
3232
.pluck(:display_name, :problem_index)
@@ -40,57 +40,58 @@ def show
4040
)
4141
.order(:created)
4242

43-
problem_to_backups = get_problem_to_backups(backups, problem_name_to_index.keys)
44-
problem_to_sessions = get_problem_to_sessions(problem_to_backups)
45-
timeline_data = get_timeline_data(problem_to_sessions, problem_name_to_index)
43+
processed_backups = process_backups(backups, problem_name_to_index)
44+
sessions = get_sessions(processed_backups)
4645

47-
render json: timeline_data, status: :ok
46+
render json: sessions, status: :ok
4847
end
4948

5049
private
5150

5251
SESSION_TIME_GAP_THRESHOLD = 15.minutes
5352

53+
# TODO move this logic into frontend since it's about styling
5454
# color blind color palette from https://davidmathlogic.com/colorblind/#%23D81B60-%231E88E5-%23FFC107-%23004D40
5555
PINK = "#D81B60"
5656
BLUE = "#1E88E5"
5757
YELLOW = "#FFC107"
5858
DARK_GREEN = "#004D40"
5959

6060
def get_grading_backup_status(grading_message_question)
61+
# TODO separate label into backup type (unlock vs. correctness) and status (passed boolean), then format label in frontend
6162
if grading_message_question.failed == 0
62-
{ :label => "Correctness Tests Passed", :color => DARK_GREEN }
63+
{ label: "Correctness Tests Passed", color: DARK_GREEN }
6364
else
64-
{ :label => "Correctness Tests Failed", :color => PINK }
65+
{ label: "Correctness Tests Failed", color: PINK }
6566
end
6667
end
6768

6869
# This works assuming that the unlock_message_cases are all belonging to the same problem
6970
def get_unlocking_backup_status(unlock_message_cases)
7071
if unlock_message_cases.map { |umc| umc.correct }.all?
71-
{ :label => "Unlocking Tests Passed", :color => BLUE }
72+
{ label: "Unlocking Tests Passed", color: BLUE }
7273
else
73-
{ :label => "Unlocking Tests Failed", :color => YELLOW }
74+
{ label: "Unlocking Tests Failed", color: YELLOW }
7475
end
7576
end
7677

77-
def get_problem_to_backups(backups, problem_names)
78-
# problem name to array of hashes where each hash represents relevant data from a single backup
79-
# hash has: timestamp, label, color
80-
result = problem_names.map { |name| [name, []] }.to_h
78+
def process_backups(backups, problem_name_to_index)
79+
# returns an array of hashes where each hash represents relevant data from a single backup
80+
# hash has: timestamp, label, color, index, problem_name
81+
result = []
8182

8283
backups.each_with_index do |backup, index|
8384
if backup.grading_location.present?
8485
backup.grading_message_questions.each do |gmq|
8586
status = get_grading_backup_status(gmq)
86-
result[gmq.question_display_name] << { :timestamp => Time.iso8601(backup.created), :index => index }.merge(status)
87+
result << { timestamp: Time.iso8601(backup.created), index: index, problem_name: gmq.question_display_name, problem_index: problem_name_to_index[gmq.question_display_name] }.merge(status)
8788
end
8889
end
8990

9091
if backup.unlock_location.present?
9192
# annoyingly, unlock.json doesn't have question display name so we fetch it from analytics.json
9293
backup_problem_names = backup.analytics_message.question_display_names
93-
umc_grouped_by_problem_name = backup_problem_names.map { |name| [name, []] }.to_h
94+
umc_grouped_by_problem_name = backup_problem_names.map { |name| [ name, [] ] }.to_h
9495
backup.unlock_message_cases.each do |umc|
9596
# TODO dangerously (?) assume that unlock message cases
9697
# will always match exactly one of the backup problem names
@@ -102,43 +103,46 @@ def get_problem_to_backups(backups, problem_names)
102103
end
103104
end
104105

105-
umc_grouped_by_problem_name.each do |name, unlock_message_cases|
106+
umc_grouped_by_problem_name.each do |problem_name, unlock_message_cases|
106107
status = get_unlocking_backup_status(unlock_message_cases)
107-
result[name] << { :timestamp => Time.iso8601(backup.created), :index => index }.merge(status)
108+
result << { timestamp: Time.iso8601(backup.created), index: index, problem_name: problem_name, problem_index: problem_name_to_index[problem_name] }.merge(status)
108109
end
109110
end
110111
end
111112

112113
result
113114
end
114115

115-
# This works assuming the array contains backups for the same problem
116-
def get_sessions(backups)
117-
if backups.empty?
116+
def get_sessions(processed_backups)
117+
if processed_backups.empty?
118118
return []
119119
end
120120

121121
# A session is defined as a series of backups where:
122-
# 1. Consecutive timestamps are <= SESSION_TIME_GAP_THRESHOLD, and
123-
# 2. All labels (and therefore colors) are the same within the session
122+
# 1. All problems worked on are the same within the session, AND
123+
# 2. All labels (and therefore colors) are the same within the session, AND
124+
# 3. Consecutive timestamps are <= SESSION_TIME_GAP_THRESHOLD
124125

125126
result = []
126-
# TODO redo sessions computation -- do not group by problem first bc then leads to weird overlap
127+
# TODO only convert to camel case at the end for consistency. generally fix naming conventions in all files...
127128
curr_session =
128129
{
129-
startTime: backups[0][:timestamp],
130-
endTime: backups[0][:timestamp],
131-
label: backups[0][:label],
132-
color: backups[0][:color],
133-
startIndex: backups[0][:index],
134-
endIndex: backups[0][:index] + 1,
130+
startTime: processed_backups[0][:timestamp],
131+
endTime: processed_backups[0][:timestamp],
132+
label: processed_backups[0][:label],
133+
color: processed_backups[0][:color],
134+
startIndex: processed_backups[0][:index],
135+
endIndex: processed_backups[0][:index] + 1,
136+
problemName: processed_backups[0][:problem_name],
137+
problemIndex: processed_backups[0][:problem_index],
135138
}
136139

137-
backups.each_cons(2) do |a, b|
140+
processed_backups.each_cons(2) do |a, b|
141+
problems_differ = a[:problem_index] != b[:problem_index]
138142
has_time_gap = b[:timestamp] - a[:timestamp] > SESSION_TIME_GAP_THRESHOLD
139143
labels_differ = a[:label] != b[:label]
140144

141-
if has_time_gap or labels_differ
145+
if problems_differ or has_time_gap or labels_differ
142146
result << curr_session
143147
curr_session =
144148
{
@@ -147,7 +151,9 @@ def get_sessions(backups)
147151
label: b[:label],
148152
color: b[:color],
149153
startIndex: b[:index],
150-
endIndex: b[:index] + 1
154+
endIndex: b[:index] + 1,
155+
problemName: b[:problem_name],
156+
problemIndex: b[:problem_index]
151157
}
152158
else
153159
curr_session[:endTime] = b[:timestamp]
@@ -158,32 +164,4 @@ def get_sessions(backups)
158164
result << curr_session
159165
result
160166
end
161-
162-
def get_problem_to_sessions(problem_to_backups)
163-
# then have helper function to turn each array into sessions (use threshold)
164-
# compute startTime and endTime, color
165-
problem_to_backups.map { |name, backups| [name, get_sessions(backups)] }.to_h
166-
end
167-
168-
def get_timeline_data(problem_to_sessions, problem_name_to_index)
169-
# then flatten all arrays and assign problemIndex using problem_name_to_index hash
170-
result = []
171-
172-
problem_to_sessions.map do |problem_name, sessions|
173-
problem_index = problem_name_to_index[problem_name]
174-
sessions.each do |session|
175-
result << {
176-
problemIndex: problem_index,
177-
startTime: session[:startTime],
178-
endTime: session[:endTime],
179-
label: session[:label],
180-
startIndex: session[:startIndex],
181-
endIndex: session[:endIndex],
182-
color: session[:color]
183-
}
184-
end
185-
end
186-
187-
result
188-
end
189167
end

src/snapshots-app/client/bundles/components/submission/tabs/summary/BackupGanttPlot.jsx

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ const PROBLEMS = [
2424
"Problem 12",
2525
];
2626

27+
const PINK = "#D81B60";
28+
const BLUE = "#1E88E5";
29+
const YELLOW = "#FFC107";
30+
const DARK_GREEN = "#004D40";
31+
32+
const CATEGORIES = [
33+
{ name: "Correctness Tests Passed", color: DARK_GREEN },
34+
{ name: "Correctness Tests Failed", color: PINK },
35+
{ name: "Unlocking Tests Passed", color: BLUE },
36+
{ name: "Unlocking Tests Failed", color: YELLOW },
37+
];
38+
2739
// TODO make this more DRY and name it better
2840
// TODO fetch problem names and problem timeline once
2941
// TODO add comments explaining options
@@ -53,6 +65,12 @@ const BackupGanttPlot = () => {
5365

5466
const option = useMemo(
5567
() => ({
68+
legend: {
69+
data: CATEGORIES.map((c) => c.name),
70+
bottom: 40, // Place it below the chart
71+
selectedMode: false,
72+
itemGap: 40,
73+
},
5674
tooltip: {
5775
formatter: (params) => {
5876
const start = params.value[1];
@@ -84,9 +102,6 @@ const BackupGanttPlot = () => {
84102

85103
// TODO turn this into jsx instead of string?
86104
return `
87-
<div style="border-bottom: 1px solid #ccc; margin-bottom: 5px;">
88-
${params.name}
89-
</div>
90105
<strong>Duration</strong>: ${durationStr}<br/>
91106
<strong># of backups</strong>: ${numBackups}<br/>
92107
<strong>Start</strong>: ${startTimeStr}<br/>
@@ -108,7 +123,7 @@ const BackupGanttPlot = () => {
108123
top: 80, // Space for the title and top X-axis
109124
left: 100, // Space for problem labels
110125
right: 50, // Padding on the right
111-
bottom: 80, // This creates the gap where the slider lives
126+
bottom: 80, // This creates the gap where the legend and slider live
112127
},
113128
xAxis: {
114129
type: "value",
@@ -169,7 +184,7 @@ const BackupGanttPlot = () => {
169184
encode: { x: [3, 4], y: 0 },
170185
// Map to ECharts internal format
171186
data: timelineData.map((item) => ({
172-
name: item.label,
187+
name: item.label, // This must match CATEGORIES names for interactivity
173188
value: [
174189
item.problemIndex,
175190
new Date(item.startTime).getTime(),
@@ -180,6 +195,13 @@ const BackupGanttPlot = () => {
180195
itemStyle: { color: item.color },
181196
})),
182197
},
198+
...CATEGORIES.map((cat) => ({
199+
name: cat.name,
200+
type: "bar", // Can be anything, bar works well
201+
itemStyle: { color: cat.color },
202+
203+
// data: [], // Empty so it doesn't render bars
204+
})),
183205
],
184206
}),
185207
[timelineData],

src/snapshots-app/client/bundles/components/submission/tabs/summary/SummaryTab.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ function SummaryTab({}) {
264264
</Paper>
265265
</Box>
266266

267-
<ProblemGanttPlot />
267+
{/* <ProblemGanttPlot /> */}
268268

269269
<BackupGanttPlot />
270270
</>

0 commit comments

Comments
 (0)