437 lines
17 KiB
Python
437 lines
17 KiB
Python
# Custom
|
|
from model import *
|
|
from appstate import AppState
|
|
|
|
# External
|
|
from imgui_bundle import (
|
|
imgui,
|
|
immapp,
|
|
hello_imgui,
|
|
imgui_md,
|
|
implot,
|
|
implot3d,
|
|
immvision,
|
|
ImVec2,
|
|
ImVec4,
|
|
im_file_dialog
|
|
)
|
|
from PIL import ImageColor
|
|
import numpy as np
|
|
from numpy.typing import NDArray
|
|
|
|
# Built In
|
|
from typing import List, Any
|
|
|
|
def student_list(app_state: AppState) -> None:
|
|
statics = student_list
|
|
if not app_state.current_class_id:
|
|
imgui.text("No Class found in Database")
|
|
return
|
|
|
|
if not hasattr(statics, "select"):
|
|
statics.select = 0
|
|
|
|
statics.students = Student.select().where(Student.class_id == app_state.current_class_id)
|
|
statics.students = statics.students if statics.students else None
|
|
if not statics.students:
|
|
imgui.text(f"No Stundents found in {Class.get_by_id(app_state.current_class_id).name}")
|
|
return
|
|
|
|
for n, student in enumerate(statics.students, start=1):
|
|
display = f"{n}. {student.prename} {student.surname}"
|
|
_, clicked = imgui.selectable(display, statics.select == n-1)
|
|
if clicked:
|
|
statics.select = n-1
|
|
|
|
app_state.current_student_id = statics.students[statics.select].id
|
|
|
|
def lecture_list(app_state: AppState) -> None:
|
|
statics = lecture_list
|
|
|
|
if not app_state.current_class_id:
|
|
imgui.text("No class found in Database")
|
|
return
|
|
|
|
lectures = Lecture.select().where(Lecture.class_id == app_state.current_class_id)
|
|
|
|
if not lectures:
|
|
imgui.text(f"No Lectures found for {Class.get_by_id(app_state.current_class_id).name}")
|
|
return
|
|
|
|
if not hasattr(statics, "select"):
|
|
statics.select = 0
|
|
|
|
for n, lecture in enumerate(lectures, start=1):
|
|
display = f"{n}. {lecture.title}"
|
|
_, clicked = imgui.selectable(display, statics.select == n-1)
|
|
if clicked:
|
|
statics.select = n-1
|
|
|
|
app_state.current_lecture_id = lectures[statics.select].id
|
|
|
|
def class_list(app_state: AppState) -> None:
|
|
statics = class_list
|
|
|
|
if db.is_closed():
|
|
imgui.text("No Database loaded")
|
|
return
|
|
|
|
classes = Class.select() if db else None
|
|
if not classes:
|
|
imgui.text("No Classes currently in Database")
|
|
return
|
|
|
|
if not hasattr(statics, "select"):
|
|
statics.select = 0
|
|
|
|
for n, clas in enumerate(classes, start=1):
|
|
display = f"{n}. {clas.name}"
|
|
_, clicked = imgui.selectable(display, statics.select == n-1)
|
|
if clicked:
|
|
statics.select = n-1
|
|
|
|
app_state.current_class_id = classes[statics.select].id
|
|
|
|
def submissions_list(app_state: AppState) -> None:
|
|
statics = submissions_list
|
|
|
|
if not app_state.current_lecture_id:
|
|
imgui.text("No Lecture found")
|
|
return
|
|
if not app_state.current_student_id:
|
|
imgui.text("No Student found")
|
|
return
|
|
|
|
submissions = Submission.select().where(Submission.lecture_id == app_state.current_lecture_id and Submission.student_id == app_state.current_student_id)
|
|
|
|
if not submissions:
|
|
student = Student.get_by_id(app_state.current_student_id)
|
|
lecture = Lecture.get_by_id(app_state.current_lecture_id)
|
|
imgui.text(f"{student.prename} {student.surname} didn't submitted for {lecture.title}")
|
|
return
|
|
|
|
if not hasattr(statics, "select"):
|
|
statics.select = 0
|
|
|
|
for n, sub in enumerate(submissions, start=1):
|
|
lecture = Lecture.get_by_id(sub.lecture_id)
|
|
points = sub.points
|
|
if points.is_integer():
|
|
points = int(points)
|
|
display = f"{n}. {lecture.title} {points}/{lecture.points}"
|
|
_, clicked = imgui.selectable(display, statics.select == n-1)
|
|
if clicked:
|
|
statics.select = n-1
|
|
|
|
app_state.current_submission_id = submissions[statics.select].id
|
|
|
|
def plot_bar_line_percentage(data: np.array, labels: list, avg: float) -> None:
|
|
if not data.size > 0:
|
|
imgui.text("No Data available")
|
|
return
|
|
|
|
name = hash(avg)
|
|
|
|
if avg.is_integer():
|
|
avg = int(avg)
|
|
avg = np.ones(len(data)) * avg
|
|
|
|
w, h = imgui.get_window_size()
|
|
implot.push_colormap(implot.Colormap_.hot.value)
|
|
if implot.begin_plot(f"Performance##{name}", ImVec2(-1, h*0.4), implot.Flags_.no_mouse_text.value | implot.Flags_.no_inputs.value):
|
|
implot.setup_axes("Lectures", "Percentage")
|
|
implot.setup_axes_limits(-1, len(data), 0, 110)
|
|
implot.push_style_var(implot.StyleVar_.fill_alpha.value, 0.6)
|
|
implot.push_style_var(implot.StyleVar_.line_weight.value, 3)
|
|
implot.setup_axis_ticks(implot.ImAxis_.x1.value, 0, len(labels), len(labels), [" " for _ in labels], False)
|
|
implot.plot_bars("Submissions", data)
|
|
implot.plot_line("Average", avg)
|
|
|
|
implot.push_style_color(implot.Col_.inlay_text, ImVec4(190,190,40,255)/255)
|
|
for x_pos, label in enumerate(labels):
|
|
y_pos = 50
|
|
implot.plot_text(label, x_pos, y_pos//2, ImVec2(0,0), implot.TextFlags_.vertical.value)
|
|
implot.pop_style_color()
|
|
|
|
implot.pop_style_var()
|
|
implot.end_plot()
|
|
|
|
COLOR_TEXT_PASSED = tuple([e/255 for e in ImageColor.getcolor("#1AFE49","RGBA")])
|
|
COLOR_TEXT_FAILED = tuple([e/255 for e in ImageColor.getcolor("#FF124F","RGBA")])
|
|
@immapp.static(inited=False)
|
|
def student_graph(app_state: AppState) -> None:
|
|
statics = student_graph
|
|
if not statics.inited:
|
|
statics.id = -1
|
|
statics.student = None
|
|
statics.lectures = None
|
|
statics.points = None
|
|
statics.sub_points = None
|
|
statics.subs_data = None
|
|
statics.subs_labels = None
|
|
statics.max_points = None
|
|
statics.avg = None
|
|
statics.inited = True
|
|
|
|
if id != app_state.current_student_id:
|
|
statics.id = app_state.current_student_id
|
|
|
|
if not app_state.current_student_id:
|
|
imgui.text("No Students in Database")
|
|
return
|
|
statics.student = Student.get_by_id(app_state.current_student_id)
|
|
submissions = Submission.select().where(Submission.student_id == statics.student.id)
|
|
|
|
statics.lectures = [Lecture.get_by_id(sub.lecture_id) for sub in submissions]
|
|
statics.max_points = np.sum([l.points for l in statics.lectures])
|
|
statics.sub_points = [sub.points for sub in submissions]
|
|
statics.points = np.sum(statics.sub_points)
|
|
if statics.points.is_integer():
|
|
statics.points = int(statics.points)
|
|
|
|
statics.subs_data = np.array([p/mp.points for p, mp in zip(statics.sub_points, statics.lectures)], dtype=np.float32)*100
|
|
statics.subs_labels = [f"{l.title} {int(points) if points.is_integer() else points}/{l.points}" for l, points in zip(statics.lectures, statics.sub_points)]
|
|
statics.avg = statics.points/statics.max_points*100
|
|
|
|
w, h = imgui.get_window_size()
|
|
imgui_md.render(f"# {statics.student.prename} {statics.student.surname}")
|
|
imgui_md.render(f"### {statics.points}/{statics.max_points}")
|
|
imgui.text(" ")
|
|
imgui.progress_bar(statics.points/statics.max_points, ImVec2(w*0.9, h*0.05), f"{statics.points}/{statics.max_points} {statics.points/statics.max_points:.1%}")
|
|
plot_bar_line_percentage(statics.subs_data, statics.subs_labels, statics.avg)
|
|
imgui.separator()
|
|
for n, data in enumerate(zip(statics.lectures, statics.sub_points), start=1):
|
|
lecture, points = data
|
|
COLOR = COLOR_TEXT_PASSED if points >= lecture.points*0.3 else COLOR_TEXT_FAILED
|
|
imgui.text_colored(COLOR, f"{n}. {lecture.title}")
|
|
|
|
@immapp.static(inited=False)
|
|
def sex_graph(app_state: AppState) -> None:
|
|
statics = sex_graph
|
|
|
|
if db.is_closed():
|
|
imgui.text("No Database loaded")
|
|
return
|
|
|
|
if not statics.inited:
|
|
statics.max_points = None
|
|
statics.male_points = None
|
|
statics.female_points = None
|
|
statics.male_percentage = None
|
|
statics.female_percentage = None
|
|
statics.male_labels = None
|
|
statics.female_labels = None
|
|
statics.lectures = None
|
|
statics.class_id = -1
|
|
statics.state = 0
|
|
statics.inited = True
|
|
|
|
if not app_state.current_class_id:
|
|
imgui.text("No Class found")
|
|
return
|
|
|
|
if statics.class_id != app_state.current_class_id:
|
|
statics.class_id = app_state.current_class_id
|
|
lectures = Lecture.select().where(Lecture.class_id == statics.class_id)
|
|
statics.lectures = lectures
|
|
|
|
statics.max_points = np.empty(len(lectures))
|
|
statics.male_points = np.empty(len(lectures))
|
|
statics.female_points = np.empty(len(lectures))
|
|
for n, lecture in enumerate(lectures):
|
|
statics.max_points[n] = lecture.points # Acc points
|
|
|
|
m_count = 0
|
|
f_count = 0
|
|
m_points = 0
|
|
f_points = 0
|
|
|
|
for sub in Submission.select().where(Submission.lecture_id == lecture.id):
|
|
if Student.get_by_id(sub.student_id).sex == 'Male':
|
|
m_points += sub.points
|
|
m_count += 1
|
|
else:
|
|
f_points += sub.points
|
|
f_count += 1
|
|
statics.male_points[n] = m_points/m_count/lecture.points
|
|
statics.female_points[n] = f_points/f_count/lecture.points
|
|
|
|
statics.male_percentage = np.sum(statics.male_points)/len(statics.male_points)
|
|
statics.female_percentage = np.sum(statics.female_points)/len(statics.female_points)
|
|
|
|
statics.male_labels = [f"{l.title} | {points:.1%}" for l, points in zip(lectures, statics.male_points)]
|
|
statics.female_labels = [f"{l.title} | {points:.1%}" for l, points in zip(lectures, statics.female_points)]
|
|
|
|
w, h = imgui.get_window_size()
|
|
if statics.state == 0:
|
|
imgui_md.render("# Male")
|
|
imgui.progress_bar(statics.male_percentage, ImVec2(w*0.9, h*0.05), f"{statics.male_percentage:.1%} n={len(Student.select().where(Student.sex == 'Male'))}")
|
|
plot_bar_line_percentage(statics.male_points*100, statics.male_labels, statics.male_percentage*100)
|
|
|
|
imgui_md.render("# Female")
|
|
imgui.progress_bar(statics.female_percentage, ImVec2(w*0.9, h*0.05), f"{statics.female_percentage:.1%} n={len(Student.select().where(Student.sex == 'Female'))}")
|
|
plot_bar_line_percentage(statics.female_points*100, statics.female_labels, statics.female_percentage*100)
|
|
|
|
if statics.state == 1:
|
|
male_xs = np.arange(1, len(statics.male_points)+1, dtype=np.float32)
|
|
male_ys = male_xs*0
|
|
male_zs = np.array(statics.male_points*100, dtype=np.float32)
|
|
|
|
female_xs = np.arange(1, len(statics.female_points)+1, dtype=np.float32)
|
|
female_ys = np.ones(len(statics.female_points), dtype=np.float32)
|
|
female_zs = np.array(statics.female_points*100, dtype=np.float32)
|
|
|
|
if implot3d.begin_plot("3D Gender Plot", ImVec2(w*0.9, h*0.9)):
|
|
implot3d.setup_axes("Lecture", "Gender", "Percentage")
|
|
implot3d.setup_box_scale(1.1, 1.1, 1.1)
|
|
implot3d.setup_axes_limits(0, len(statics.male_points)+1, -0.2, 1.2, 0, 110)
|
|
|
|
implot3d.set_next_marker_style(implot3d.Marker_.square.value)
|
|
implot3d.plot_line("Male", male_xs, male_ys, male_zs)
|
|
|
|
implot3d.set_next_marker_style(implot3d.Marker_.circle.value)
|
|
implot3d.plot_line("Female", female_xs, female_ys, female_zs)
|
|
|
|
for n, l in enumerate(statics.lectures, start=1):
|
|
implot3d.plot_text(l.title, n, np.clip(n/len(statics.lectures)), 50, np.pi/4)
|
|
|
|
implot3d.end_plot()
|
|
|
|
if imgui.button("Change 2D/3D"):
|
|
statics.state = not statics.state
|
|
|
|
|
|
|
|
COLOR_TEXT_FIRST = tuple([e/255 for e in ImageColor.getcolor("#FFCF40","RGBA")])
|
|
COLOR_TEXT_SECOND = tuple([e/255 for e in ImageColor.getcolor("#A5A9B4","RGBA")])
|
|
COLOR_TEXT_THIRD = tuple([e/255 for e in ImageColor.getcolor("#97564A","RGBA")])
|
|
COLOR_TEXT_ELSE = tuple([e/255 for e in ImageColor.getcolor("#85EBD9","RGBA")])
|
|
@immapp.static(inited=False)
|
|
def ranking(app_state: AppState) -> None:
|
|
statics = ranking
|
|
|
|
if not statics.inited:
|
|
statics.id = -1
|
|
statics.rank = None
|
|
statics.state = 1
|
|
statics.inited = True
|
|
|
|
imgui_md.render("# Student Ranking")
|
|
|
|
if not app_state.current_class_id:
|
|
imgui.text("No class selected")
|
|
return
|
|
|
|
if statics.id != app_state.current_class_id:
|
|
statics.id = app_state.current_class_id
|
|
|
|
students = Student.select().where(Student.class_id == statics.id)
|
|
max_points = np.sum([l.points for l in Lecture.select().where(Lecture.class_id == statics.id)])
|
|
|
|
statics.rank = list()
|
|
for student in students:
|
|
points = list()
|
|
for sub in Submission.select().where(Submission.student_id == student.id):
|
|
points.append(sub.points)
|
|
statics.rank.append((student, sum(points)/max_points))
|
|
statics.rank = sorted(statics.rank, key=lambda item: item[1], reverse=True)
|
|
|
|
if statics.state == 0:
|
|
for n, data in enumerate(statics.rank, start=1):
|
|
student, points = data
|
|
display = f"{n}. {student.prename} {student.surname} {points:.1%}"
|
|
COLOR = COLOR_TEXT_ELSE
|
|
if n == 1:
|
|
COLOR = COLOR_TEXT_FIRST
|
|
if n == 2:
|
|
COLOR = COLOR_TEXT_SECOND
|
|
if n == 3:
|
|
COLOR = COLOR_TEXT_THIRD
|
|
imgui.text_colored(COLOR, display)
|
|
|
|
if statics.state == 1:
|
|
w, h = imgui.get_window_size()
|
|
if implot.begin_plot("Ranking", ImVec2(w*0.9, h*0.9)):
|
|
implot.plot_bars("Ranking", np.array([p[1] for p in reversed(statics.rank)])*100, 0.67, 0, implot.BarsFlags_.horizontal.value)
|
|
for n, s in enumerate(reversed(statics.rank)):
|
|
student = s[0]
|
|
implot.plot_text(f"{student.prename} {student.surname}", s[1]*50, n)
|
|
implot.end_plot()
|
|
|
|
if imgui.button("Change"):
|
|
statics.state = not statics.state
|
|
|
|
def analyzer_docking_splits() -> List[hello_imgui.DockingSplit]:
|
|
split_main_misc = hello_imgui.DockingSplit()
|
|
split_main_misc.initial_dock = "MainDockSpace"
|
|
split_main_misc.new_dock = "MiscSpace"
|
|
split_main_misc.direction = imgui.Dir.down
|
|
split_main_misc.ratio = 0.25
|
|
|
|
# Then, add a space to the left which occupies a column whose width is 25% of the app width
|
|
split_main_command = hello_imgui.DockingSplit()
|
|
split_main_command.initial_dock = "MainDockSpace"
|
|
split_main_command.new_dock = "CommandSpace"
|
|
split_main_command.direction = imgui.Dir.left
|
|
split_main_command.ratio = 0.2
|
|
|
|
# Then, add CommandSpace2 below MainDockSpace
|
|
split_main_command2 = hello_imgui.DockingSplit()
|
|
split_main_command2.initial_dock = "MainDockSpace"
|
|
split_main_command2.new_dock = "CommandSpace2"
|
|
split_main_command2.direction = imgui.Dir.down
|
|
split_main_command2.ratio = 0.25
|
|
|
|
splits = [split_main_misc, split_main_command, split_main_command2]
|
|
return splits
|
|
|
|
def set_analyzer_layout(app_state: AppState) -> List[hello_imgui.DockableWindow]:
|
|
student_selector = hello_imgui.DockableWindow()
|
|
student_selector.label = "Students"
|
|
student_selector.dock_space_name = "CommandSpace"
|
|
student_selector.gui_function = lambda: student_list(app_state)
|
|
|
|
lecture_selector = hello_imgui.DockableWindow()
|
|
lecture_selector.label = "Lectures"
|
|
lecture_selector.dock_space_name = "CommandSpace2"
|
|
lecture_selector.gui_function = lambda: lecture_list(app_state)
|
|
|
|
class_selector = hello_imgui.DockableWindow()
|
|
class_selector.label = "Classes"
|
|
class_selector.dock_space_name = "CommandSpace2"
|
|
class_selector.gui_function = lambda: class_list(app_state)
|
|
|
|
submission_selector = hello_imgui.DockableWindow()
|
|
submission_selector.label = "Submissions"
|
|
submission_selector.dock_space_name = "CommandSpace"
|
|
submission_selector.gui_function = lambda: submissions_list(app_state)
|
|
|
|
student_info = hello_imgui.DockableWindow()
|
|
student_info.label = "Student Analyzer"
|
|
student_info.dock_space_name = "MainDockSpace"
|
|
student_info.gui_function = lambda: student_graph(app_state)
|
|
|
|
sex_info = hello_imgui.DockableWindow()
|
|
sex_info.label = "Analyze by Gender"
|
|
sex_info.dock_space_name = "MainDockSpace"
|
|
sex_info.gui_function = lambda: sex_graph(app_state)
|
|
|
|
student_ranking = hello_imgui.DockableWindow()
|
|
student_ranking.label = "Ranking"
|
|
student_ranking.dock_space_name = "MainDockSpace"
|
|
student_ranking.gui_function = lambda: ranking(app_state)
|
|
|
|
return [
|
|
class_selector, student_selector,
|
|
lecture_selector, submission_selector,
|
|
student_info, sex_info,
|
|
student_ranking
|
|
]
|
|
|
|
def analyzer_layout(app_state: AppState) -> hello_imgui.DockingParams:
|
|
docking_params = hello_imgui.DockingParams()
|
|
docking_params.layout_name = "Analyzer"
|
|
docking_params.docking_splits = analyzer_docking_splits()
|
|
docking_params.dockable_windows = set_analyzer_layout(app_state)
|
|
return docking_params
|