182 lines
6.5 KiB
GDScript
182 lines
6.5 KiB
GDScript
@icon("res://addons/easy_charts/utilities/icons/linechart.svg")
|
|
extends PanelContainer
|
|
class_name Chart
|
|
|
|
@onready var _canvas: Canvas = $Canvas
|
|
@onready var plot_box: PlotBox = $"%PlotBox"
|
|
@onready var grid_box: GridBox = $"%GridBox"
|
|
@onready var functions_box: Control = $"%FunctionsBox"
|
|
@onready var function_legend: FunctionLegend = $"%FunctionLegend"
|
|
|
|
var functions: Array = []
|
|
var x: Array = []
|
|
var y: Array = []
|
|
|
|
var x_labels_function: Callable = Callable()
|
|
var y_labels_function: Callable = Callable()
|
|
|
|
var x_domain: Dictionary = {}
|
|
var y_domain: Dictionary = {}
|
|
|
|
var chart_properties: ChartProperties = null
|
|
|
|
###########
|
|
|
|
func _ready() -> void:
|
|
if theme == null:
|
|
theme = Theme.new()
|
|
|
|
func plot(functions: Array[Function], properties: ChartProperties = ChartProperties.new()) -> void:
|
|
self.functions = functions
|
|
self.chart_properties = properties
|
|
|
|
theme.set("default_font", self.chart_properties.font)
|
|
_canvas.prepare_canvas(self.chart_properties)
|
|
plot_box.chart_properties = self.chart_properties
|
|
function_legend.chart_properties = self.chart_properties
|
|
|
|
load_functions(functions)
|
|
|
|
func get_function_plotter(function: Function) -> FunctionPlotter:
|
|
var plotter: FunctionPlotter
|
|
match function.get_type():
|
|
Function.Type.LINE:
|
|
plotter = LinePlotter.new(function)
|
|
Function.Type.AREA:
|
|
plotter = AreaPlotter.new(function)
|
|
Function.Type.PIE:
|
|
plotter = PiePlotter.new(function)
|
|
Function.Type.BAR:
|
|
plotter = BarPlotter.new(function)
|
|
Function.Type.SCATTER, _:
|
|
plotter = ScatterPlotter.new(function)
|
|
return plotter
|
|
|
|
func load_functions(functions: Array[Function]) -> void:
|
|
self.x = []
|
|
self.y = []
|
|
|
|
function_legend.clear()
|
|
|
|
# Remove existing function_plotters
|
|
for function_plotter in functions_box.get_children():
|
|
functions_box.remove_child(function_plotter)
|
|
function_plotter.queue_free()
|
|
|
|
for function in functions:
|
|
# Load x and y values
|
|
self.x.append(function.__x)
|
|
self.y.append(function.__y)
|
|
|
|
# Create FunctionPlotter
|
|
var function_plotter: FunctionPlotter = get_function_plotter(function)
|
|
function_plotter.connect("point_entered", Callable(plot_box, "_on_point_entered"))
|
|
function_plotter.connect("point_exited", Callable(plot_box, "_on_point_exited"))
|
|
functions_box.add_child(function_plotter)
|
|
|
|
# Create legend
|
|
match function.get_type():
|
|
Function.Type.PIE:
|
|
for i in function.__x.size():
|
|
var interp_color: Color = function.get_gradient().sample(float(i) / float(function.__x.size()))
|
|
function_legend.add_label(function.get_type(), interp_color, Function.Marker.NONE, function.__y[i])
|
|
_:
|
|
function_legend.add_function(function)
|
|
|
|
func _draw() -> void:
|
|
if (x.size() == 0) or (y.size() == 0) or (x.size() == 1 and x[0].is_empty()) or (y.size() == 1 and y[0].is_empty()):
|
|
printerr("Cannot plot an empty function!")
|
|
return
|
|
|
|
var is_x_fixed: bool = x_domain.get("fixed", false)
|
|
var is_y_fixed: bool = y_domain.get("fixed", false)
|
|
|
|
# GridBox
|
|
if not is_x_fixed or not is_y_fixed :
|
|
if chart_properties.max_samples > 0 :
|
|
var _x: Array = []
|
|
var _y: Array = []
|
|
|
|
_x.resize(x.size())
|
|
_y.resize(y.size())
|
|
|
|
for i in x.size():
|
|
if not is_x_fixed:
|
|
_x[i] = x[i].slice(max(0, x[i].size() - chart_properties.max_samples), x[i].size())
|
|
if not is_y_fixed:
|
|
_y[i] = y[i].slice(max(0, y[i].size() - chart_properties.max_samples), y[i].size())
|
|
|
|
if not is_x_fixed:
|
|
x_domain = calculate_domain(_x)
|
|
if not is_y_fixed:
|
|
y_domain = calculate_domain(_y)
|
|
else:
|
|
if not is_x_fixed:
|
|
x_domain = calculate_domain(x)
|
|
if not is_y_fixed:
|
|
y_domain = calculate_domain(y)
|
|
|
|
# Update values for the PlotBox in order to propagate them to the children
|
|
update_plotbox(x_domain, y_domain, x_labels_function, y_labels_function)
|
|
|
|
# Update GridBox
|
|
update_gridbox(x_domain, y_domain, x_labels_function, y_labels_function)
|
|
|
|
# Update each FunctionPlotter in FunctionsBox
|
|
for function_plotter in functions_box.get_children():
|
|
if function_plotter is FunctionPlotter:
|
|
function_plotter.visible = function_plotter.function.get_visibility()
|
|
if function_plotter.function.get_visibility():
|
|
function_plotter.update_values(x_domain, y_domain)
|
|
|
|
func calculate_domain(values: Array) -> Dictionary:
|
|
for value_array in values:
|
|
if ECUtilities._contains_string(value_array):
|
|
return { lb = 0.0, ub = (value_array.size() - 1), has_decimals = false , fixed = false }
|
|
var min_max: Dictionary = ECUtilities._find_min_max(values)
|
|
|
|
if not chart_properties.smooth_domain:
|
|
return { lb = min_max.min, ub = min_max.max, has_decimals = ECUtilities._has_decimals(values), fixed = false }
|
|
else:
|
|
return { lb = ECUtilities._round_min(min_max.min), ub = ECUtilities._round_max(min_max.max), has_decimals = ECUtilities._has_decimals(values) , fixed = false }
|
|
|
|
func set_x_domain(lb: Variant, ub: Variant) -> void:
|
|
x_domain = { lb = lb, ub = ub, has_decimals = ECUtilities._has_decimals([lb, ub]), fixed = true }
|
|
|
|
func set_y_domain(lb: Variant, ub: Variant) -> void:
|
|
y_domain = { lb = lb, ub = ub, has_decimals = ECUtilities._has_decimals([lb, ub]), fixed = true }
|
|
|
|
func update_plotbox(x_domain: Dictionary, y_domain: Dictionary, x_labels_function: Callable, y_labels_function: Callable) -> void:
|
|
plot_box.box_margins = calculate_plotbox_margins(x_domain, y_domain)
|
|
plot_box.set_labels_functions(x_labels_function, y_labels_function)
|
|
|
|
func update_gridbox(x_domain: Dictionary, y_domain: Dictionary, x_labels_function: Callable, y_labels_function: Callable) -> void:
|
|
grid_box.set_domains(x_domain, y_domain)
|
|
grid_box.set_labels_functions(x_labels_function, y_labels_function)
|
|
grid_box.queue_redraw()
|
|
|
|
func calculate_plotbox_margins(x_domain: Dictionary, y_domain: Dictionary) -> Vector2:
|
|
var plotbox_margins: Vector2 = Vector2(
|
|
chart_properties.x_tick_size,
|
|
chart_properties.y_tick_size
|
|
)
|
|
|
|
if chart_properties.show_tick_labels:
|
|
var x_ticklabel_size: Vector2
|
|
var y_ticklabel_size: Vector2
|
|
|
|
var y_max_formatted: String = ECUtilities._format_value(y_domain.ub, y_domain.has_decimals)
|
|
if y_domain.lb < 0: # negative number
|
|
var y_min_formatted: String = ECUtilities._format_value(y_domain.lb, y_domain.has_decimals)
|
|
if y_min_formatted.length() >= y_max_formatted.length():
|
|
y_ticklabel_size = chart_properties.get_string_size(y_min_formatted)
|
|
else:
|
|
y_ticklabel_size = chart_properties.get_string_size(y_max_formatted)
|
|
else:
|
|
y_ticklabel_size = chart_properties.get_string_size(y_max_formatted)
|
|
|
|
plotbox_margins.x += y_ticklabel_size.x + chart_properties.x_ticklabel_space
|
|
plotbox_margins.y += ThemeDB.fallback_font_size + chart_properties.y_ticklabel_space
|
|
|
|
return plotbox_margins
|