extends Control const Brand := preload("res://scripts/ui/medmind_style.gd") var points: Array = [] var mode := "rating" var dark := true var hover_index := -1 func _ready() -> void: mouse_filter = Control.MOUSE_FILTER_STOP func set_series(next_points: Array, next_mode: String, dark_mode: bool) -> void: points = next_points.duplicate(true) mode = next_mode dark = dark_mode hover_index = -1 queue_redraw() func _notification(what: int) -> void: if what == NOTIFICATION_RESIZED: queue_redraw() func _gui_input(event: InputEvent) -> void: if event is InputEventMouseMotion: var next_hover := _index_near_x(event.position.x) if next_hover != hover_index: hover_index = next_hover queue_redraw() elif event is InputEventMouseButton and not event.pressed: hover_index = _index_near_x(event.position.x) queue_redraw() func _draw() -> void: var left := 52.0 var top := 12.0 var right := maxf(left + 1.0, size.x - 12.0) var bottom := maxf(top + 1.0, size.y - 34.0) var grid := Color("24314b") if dark else Color("dbe3ef") var muted := Brand.muted_color(dark) var bounds := axis_bounds(points, mode) var minimum := float(bounds.x) var maximum := float(bounds.y) for tick_index in range(5): var ratio := float(tick_index) / 4.0 var y := bottom - (bottom - top) * ratio var tick_value := lerpf(minimum, maximum, ratio) draw_line(Vector2(left, y), Vector2(right, y), grid, 1.0) draw_string(Brand.font, Vector2(2, y + 4), str(int(round(tick_value))), HORIZONTAL_ALIGNMENT_RIGHT, 43, 11, muted) if mode == "accuracy": var target_y := _value_y(70.0, minimum, maximum, top, bottom) draw_dashed_line(Vector2(left, target_y), Vector2(right, target_y), Brand.ROYAL_BLUE, 5.0, 2.0) if points.is_empty(): return var previous: Variant = null for index in range(points.size()): var value: Variant = point_value(points[index], mode) if value == null: previous = null continue var point := Vector2(_point_x(index, left, right), _value_y(float(value), minimum, maximum, top, bottom)) if previous != null: draw_line(previous, point, Brand.ROYAL_BLUE, 2.5, true) draw_circle(point, 3.5, Brand.PURPLE) draw_circle(point, 2.0, Color.WHITE) previous = point for label_index in [0, int(floor(float(points.size() - 1) / 2.0)), points.size() - 1]: var x := _point_x(label_index, left, right) var label := _short_date(str((points[label_index] as Dictionary).get("dateUtc", ""))) draw_string(Brand.font, Vector2(x - 24, bottom + 19), label, HORIZONTAL_ALIGNMENT_CENTER, 48, 10, muted) if hover_index >= 0 and hover_index < points.size(): var hover_value: Variant = point_value(points[hover_index], mode) if hover_value != null: var hover_x := _point_x(hover_index, left, right) var hover_y := _value_y(float(hover_value), minimum, maximum, top, bottom) draw_line(Vector2(hover_x, top), Vector2(hover_x, bottom), muted, 1.0) draw_circle(Vector2(hover_x, hover_y), 5.0, Brand.ROYAL_BLUE) var suffix := "%" if mode == "accuracy" else "" var tooltip := "%s %d%s" % [ _short_date(str((points[hover_index] as Dictionary).get("dateUtc", ""))), int(hover_value), suffix ] var tooltip_size := Brand.font.get_string_size(tooltip, HORIZONTAL_ALIGNMENT_LEFT, -1, 11) + Vector2(16, 10) var tooltip_x := clampf(hover_x - tooltip_size.x / 2.0, left, right - tooltip_size.x) var tooltip_y := maxf(top, hover_y - tooltip_size.y - 10.0) var tooltip_rect := Rect2(Vector2(tooltip_x, tooltip_y), tooltip_size) draw_style_box(_tooltip_box(), tooltip_rect) draw_string(Brand.font, Vector2(tooltip_x + 8, tooltip_y + tooltip_size.y - 7), tooltip, HORIZONTAL_ALIGNMENT_LEFT, -1, 11, Brand.text_color(dark)) static func point_value(point: Dictionary, series_mode: String) -> Variant: if series_mode == "accuracy": return float(point.get("accuracy", 0)) if int(point.get("total", 0)) > 0 else null var value: Variant = point.get("questionBankRating", null) return null if value == null else float(value) static func axis_bounds(series_points: Array, series_mode: String) -> Vector2: if series_mode == "accuracy": return Vector2(0, 100) var values: Array[float] = [] for point in series_points: var value: Variant = point_value(point, series_mode) if value != null: values.append(float(value)) if values.is_empty(): return Vector2(900, 1100) var minimum: float = values.min() var maximum: float = values.max() if maximum - minimum < 100.0: var center: float = (minimum + maximum) / 2.0 minimum = center - 50.0 maximum = center + 50.0 var padding := maxf(25.0, (maximum - minimum) * 0.15) minimum = floor((minimum - padding) / 25.0) * 25.0 maximum = ceil((maximum + padding) / 25.0) * 25.0 return Vector2(minimum, maximum) func _index_near_x(mouse_x: float) -> int: if points.is_empty() or size.x <= 64.0: return -1 var ratio := clampf((mouse_x - 52.0) / maxf(1.0, size.x - 64.0), 0.0, 1.0) return int(round(ratio * float(points.size() - 1))) func _point_x(index: int, left: float, right: float) -> float: return left if points.size() <= 1 else lerpf(left, right, float(index) / float(points.size() - 1)) func _value_y(value: float, minimum: float, maximum: float, top: float, bottom: float) -> float: return bottom - (bottom - top) * inverse_lerp(minimum, maximum, clampf(value, minimum, maximum)) func _short_date(iso_value: String) -> String: var date := iso_value.left(10) if date.length() < 10: return "" return "%s/%s" % [date.substr(5, 2), date.substr(8, 2)] func _tooltip_box() -> StyleBoxFlat: var box := StyleBoxFlat.new() box.bg_color = Color("111d31") if dark else Color.WHITE box.border_color = Color("34435e") if dark else Brand.LIGHT_BORDER box.set_border_width_all(1) box.set_corner_radius_all(5) return box