Add dashboard performance trends

This commit is contained in:
hz
2026-07-25 22:43:34 -05:00
parent 46953ee71a
commit 500477b612
15 changed files with 640 additions and 78 deletions
+74 -21
View File
@@ -12,6 +12,8 @@ var metric_grid: GridContainer
var analytics_grid: GridContainer
var lower_grid: GridContainer
var trend_chart: Control
var rating_toggle: Button
var accuracy_toggle: Button
var activity_chart: Control
var system_list: VBoxContainer
var focus_content: VBoxContainer
@@ -22,6 +24,7 @@ var metric_values: Dictionary = {}
var metric_details: Dictionary = {}
var pending_review_launch := false
var review_queue_service: Variant
var trend_mode := "rating"
func _ready() -> void:
@@ -81,7 +84,18 @@ func _build() -> void:
var performance := VBoxContainer.new()
performance.size_flags_horizontal = Control.SIZE_EXPAND_FILL
performance.add_theme_constant_override("separation", 10)
performance.add_child(_section_title("Performance trend"))
var trend_header := HBoxContainer.new()
trend_header.add_child(_section_title("Performance trend"))
var trend_space := Control.new()
trend_space.size_flags_horizontal = Control.SIZE_EXPAND_FILL
trend_header.add_child(trend_space)
var trend_group := ButtonGroup.new()
trend_group.allow_unpress = false
rating_toggle = _trend_toggle("Rating", "rating", trend_group)
accuracy_toggle = _trend_toggle("Correct %", "accuracy", trend_group)
trend_header.add_child(rating_toggle)
trend_header.add_child(accuracy_toggle)
performance.add_child(trend_header)
trend_chart = TrendChart.new()
trend_chart.custom_minimum_size = Vector2(520, 190)
trend_chart.size_flags_horizontal = Control.SIZE_EXPAND_FILL
@@ -153,11 +167,7 @@ func _render() -> void:
_set_metric("ccs", str(int(data.get("ccsRating", 1000))), "%d rated cases" % int(data.get("ratedCcsCaseCount", 0)))
_set_metric("accuracy", "%d%%" % int(data.get("firstAttemptAccuracy", 0)), "Last %d questions" % int(data.get("accuracySampleSize", 0)))
_set_metric("time", _format_duration(int(data.get("studyTimeSecondsThisWeek", 0))), "This week")
var trend_values: Array = []
for point in data.get("performanceTrend", []):
if int(point.get("total", 0)) > 0:
trend_values.append(float(point.get("accuracy", 0)))
trend_chart.set_values(trend_values, UIThemeService.is_dark_mode())
trend_chart.set_series(data.get("performanceTrend", []), trend_mode, UIThemeService.is_dark_mode())
_render_systems(data.get("systems", []))
_render_focus(data.get("systems", []))
_render_review(data)
@@ -170,38 +180,52 @@ func _render() -> void:
func _render_systems(systems_value: Array) -> void:
_clear(system_list)
var systems := systems_value.duplicate(true)
systems.sort_custom(func(a: Dictionary, b: Dictionary): return int(a.get("total", 0)) > int(b.get("total", 0)))
if systems.is_empty():
system_list.add_child(_label("Answer questions to build your system breakdown.", 14, Brand.muted_color(UIThemeService.is_dark_mode())))
return
var displayed: Array = systems.slice(0, mini(5, systems.size()))
var best_accuracy := -1
var worst_accuracy := 101
for item in displayed:
best_accuracy = maxi(best_accuracy, int(item.get("accuracy", 0)))
worst_accuracy = mini(worst_accuracy, int(item.get("accuracy", 0)))
for item in displayed:
var attempted: Array = systems.filter(func(item: Dictionary): return int(item.get("total", 0)) > 0)
var strongest_system := ""
var weakest_system := ""
if not attempted.is_empty():
var strongest: Dictionary = attempted[0]
var weakest: Dictionary = attempted[0]
for item in attempted:
if int(item.get("accuracy", 0)) > int(strongest.get("accuracy", 0)) or (
int(item.get("accuracy", 0)) == int(strongest.get("accuracy", 0))
and int(item.get("total", 0)) > int(strongest.get("total", 0))):
strongest = item
if int(item.get("accuracy", 0)) < int(weakest.get("accuracy", 0)) or (
int(item.get("accuracy", 0)) == int(weakest.get("accuracy", 0))
and int(item.get("total", 0)) > int(weakest.get("total", 0))):
weakest = item
strongest_system = str(strongest.get("system", ""))
weakest_system = str(weakest.get("system", ""))
for item in systems:
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 10)
var system_name_label := _label(str(item.get("system", "General")), 13, Brand.text_color(UIThemeService.is_dark_mode()))
system_name_label.custom_minimum_size.x = 190
system_name_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
system_name_label.custom_minimum_size.x = 170
system_name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
system_name_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
row.add_child(system_name_label)
var bar := ProgressBar.new()
bar.show_percentage = false
bar.value = int(item.get("accuracy", 0))
bar.custom_minimum_size = Vector2(150, 7)
bar.custom_minimum_size = Vector2(80, 7)
bar.size_flags_horizontal = Control.SIZE_EXPAND_FILL
bar.size_flags_vertical = Control.SIZE_SHRINK_CENTER
_style_progress(bar, Brand.PURPLE if int(item.get("accuracy", 0)) == worst_accuracy else Brand.ROYAL_BLUE)
_style_progress(bar, Brand.PURPLE if str(item.get("system", "")) == weakest_system else Brand.ROYAL_BLUE)
row.add_child(bar)
var percentage := _label("%d%%" % int(item.get("accuracy", 0)), 14, Brand.ROYAL_BLUE, true)
var total := int(item.get("total", 0))
var percentage_text := "%d%%" % int(item.get("accuracy", 0)) if total > 0 else "—"
var percentage := _label(percentage_text, 14, Brand.ROYAL_BLUE, true)
percentage.custom_minimum_size.x = 48
row.add_child(percentage)
var count := _label("%d / %d" % [int(item.get("correct", 0)), int(item.get("total", 0))], 12, Brand.muted_color(UIThemeService.is_dark_mode()))
var count := _label("%d / %d" % [int(item.get("correct", 0)), total], 12, Brand.muted_color(UIThemeService.is_dark_mode()))
count.custom_minimum_size.x = 58
row.add_child(count)
var marker := "Strongest" if int(item.get("accuracy", 0)) == best_accuracy else ("Needs work" if int(item.get("accuracy", 0)) == worst_accuracy else "")
var system_name := str(item.get("system", ""))
var marker := "Strongest" if system_name == strongest_system else ("Needs work" if system_name == weakest_system else "")
var marker_label := _label(marker, 11, Brand.SUCCESS if marker == "Strongest" else Brand.MAGENTA)
marker_label.custom_minimum_size.x = 72
row.add_child(marker_label)
@@ -444,6 +468,33 @@ func _button(text: String, icon_name: String = "", primary: bool = true) -> Butt
return button
func _trend_toggle(text: String, mode: String, group: ButtonGroup) -> Button:
var button := Button.new()
button.text = text
button.toggle_mode = true
button.button_group = group
button.button_pressed = trend_mode == mode
button.custom_minimum_size = Vector2(86, 32)
button.add_theme_font_override("font", Brand.font)
button.add_theme_font_size_override("font_size", 12)
var normal := Brand.secondary_button_box(UIThemeService.is_dark_mode())
var selected := StyleBoxFlat.new()
selected.bg_color = Brand.ROYAL_BLUE
selected.set_corner_radius_all(6)
button.add_theme_stylebox_override("normal", normal)
button.add_theme_stylebox_override("hover", normal)
button.add_theme_stylebox_override("pressed", selected)
button.add_theme_color_override("font_pressed_color", Color.WHITE)
button.pressed.connect(_set_trend_mode.bind(mode))
return button
func _set_trend_mode(mode: String) -> void:
trend_mode = mode
if trend_chart != null and not DashboardService.data.is_empty():
trend_chart.set_series(DashboardService.data.get("performanceTrend", []), trend_mode, UIThemeService.is_dark_mode())
func _label(text: String, font_size: int, color: Color, bold: bool = false) -> Label:
var label := Label.new()
label.text = text
@@ -489,6 +540,8 @@ func _update_responsive() -> void:
metric_grid.columns = 2 if compact else 4
analytics_grid.columns = 1 if compact else 2
lower_grid.columns = 1 if compact else 2
if trend_chart != null:
trend_chart.custom_minimum_size.x = 300.0 if compact else 520.0
func _clear(container: Container) -> void: