158 lines
5.6 KiB
GDScript
158 lines
5.6 KiB
GDScript
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
|