diff --git a/game/addons/easy_charts/control_charts/BarChart/bar_chart.gd b/game/addons/easy_charts/control_charts/BarChart/bar_chart.gd new file mode 100644 index 0000000..8c7fb6b --- /dev/null +++ b/game/addons/easy_charts/control_charts/BarChart/bar_chart.gd @@ -0,0 +1,184 @@ +extends Chart +class_name BarChart + +signal bar_entered(bar) + +# Size of a horizontal vector, which is calculated by `plot_box.size.x / x.size()` +var x_sector_size: float + +# List of all unordered bars belonging to this plot +var bars: Array = [] + +# List of all bars, grouped by function +var function_bars: Array = [] + +# Currently focused bar +var focused_bar: Bar = null + +func _clear_bars() -> void: + bars.clear() + function_bars.clear() + +func _clear() -> void: + _clear_bars() + +func _calc_x_domain() -> void: + pass + +func _sample_x() -> void: + ### @sampled_domain, which are the domain relative to the sampled values + ### x (real value) --> sampling --> x_sampled (pixel value in canvas) + x_sampled_domain = Pair.new(plot_box.position.x, plot_box.end.x) + + # samples + x_sampled = SampledAxis.new(x, x_sampled_domain) + + x_sector_size = (x_sampled_domain.right - x_sampled_domain.left) / x.size() + +func sort_ascending(a: String, b: String): + if a.length() < b.length(): + return true + return false + +func _find_longest_x() -> String: + var longest_x: String = "" + var x_str: Array = x.duplicate(true) + x_str.sort_custom(self, "sort_ascending") + return x_str.back() + + +func _draw_bar(bar: Bar, function_index: int) -> void: + draw_rect( + bar.rect, + chart_properties.get_function_color(function_index), + true, + 1, + false + ) + +func _draw_bars() -> void: + for function in function_bars.size(): + for i in range(0, function_bars[function].size()): + _draw_bar( + function_bars[function][i], + function + ) + +func _get_tick_label(line_index: int, line_value: float) -> String: + return x[line_index] + +func _get_vertical_tick_label_pos(base_position: Vector2, text: String) -> Vector2: + return ._get_vertical_tick_label_pos(base_position, text) + Vector2(x_sector_size / 2, 0) + + +func _draw_vertical_grid() -> void: + # draw vertical lines + + # 1. the amount of lines is equals to the X_scale: it identifies in how many sectors the x domain + # should be devided + # 2. calculate the spacing between each line in pixel. It is equals to x_sampled_domain / x_scale + # 3. calculate the offset in the real x domain, which is x_domain / x_scale. + for _x in x.size(): + var top: Vector2 = Vector2( + (_x * x_sector_size) + plot_box.position.x, + bounding_box.position.y + ) + var bottom: Vector2 = Vector2( + (_x * x_sector_size) + plot_box.position.x, + bounding_box.end.y + ) + + _draw_vertical_gridline_component(top, bottom, _x, x_sector_size) + + ### Draw last gridline + var p1: Vector2 = Vector2( + (x.size() * x_sector_size) + plot_box.position.x, + bounding_box.position.y + ) + + var p2: Vector2 = Vector2( + (x.size() * x_sector_size) + plot_box.position.x, + bounding_box.end.y + ) + + # Draw V Ticks + if chart_properties.ticks: + _draw_tick(p2, p2 + Vector2(0, _x_tick_size), chart_properties.colors.bounding_box) + + # Draw V Grid Lines + if chart_properties.grid: + draw_line(p1, p2, chart_properties.colors.grid, 1, true) + +func _calculate_bars() -> void: + var validation: int = _validate_sampled_axis(x_sampled, y_sampled) + if not validation == OK: + printerr("Cannot plot bars for invalid dataset! Error: %s" % validation) + return + + if y_sampled.values[0] is Array: + for yxi in y_sampled.values.size(): + var _function_bars: Array = [] + for i in y_sampled.values[yxi].size(): + var real_bar_value: Pair = Pair.new(x[i], y[yxi][i]) + var sampled_bar_pos: Vector2 = Vector2( + (x_sector_size * i) + x_sampled_domain.left + (x_sector_size / 2) - (chart_properties.bar_width / 2), + y_sampled.values[yxi][i] + ) + var sampled_bar_size: Vector2 = Vector2( + chart_properties.bar_width, + y_sampled_domain.left - y_sampled.values[yxi][i] + ) + var bar: Bar = Bar.new(Rect2(sampled_bar_pos, sampled_bar_size), real_bar_value) + _function_bars.append(bar) + bars.append(bar) + function_bars.append(_function_bars) + else: + for i in y_sampled.values.size(): + var real_bar_value: Pair = Pair.new(x[i], y[i]) + var sampled_bar_pos: Vector2 = Vector2( + (x_sector_size * i) + x_sampled_domain.left + (x_sector_size / 2) - chart_properties.bar_width, + y_sampled.values[i] + ) + var sampled_bar_size: Vector2 = Vector2( + chart_properties.bar_width, + y_sampled_domain.left - y_sampled.values[i] + ) + var bar: Bar = Bar.new(Rect2(sampled_bar_pos, sampled_bar_size), real_bar_value) + bars.append(bar) + function_bars.append(bars) + +func _draw() -> void: + _calculate_bars() + + _draw_bars() + +func _get_function_bar(bar: Bar) -> int: + var bar_f_index: int = -1 + for f_bar in function_bars.size(): + var found: int = function_bars[f_bar].find(bar) + if found != -1: + bar_f_index = f_bar + break + return bar_f_index + +func _input(event: InputEvent) -> void: + if event is InputEventMouse: + for bar in bars: + if bar.rect.abs().has_point(event.position): + if focused_bar == bar: + return + else: + focused_bar = bar + var func_index: int = _get_function_bar(focused_bar) + $Tooltip.update_values( + str(focused_bar.value.left), + str(focused_bar.value.right), + _get_function_name(func_index), + _get_function_color(func_index) + ) + $Tooltip.show() + emit_signal("bar_entered", bar) + return + # Mouse is not in any bar's box + focused_bar = null + $Tooltip.hide() diff --git a/game/addons/easy_charts/control_charts/BarChart/bar_chart.tscn b/game/addons/easy_charts/control_charts/BarChart/bar_chart.tscn new file mode 100644 index 0000000..7c69b42 --- /dev/null +++ b/game/addons/easy_charts/control_charts/BarChart/bar_chart.tscn @@ -0,0 +1,14 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://addons/easy_charts/control_charts/BarChart/bar_chart.gd" type="Script" id=1] +[ext_resource path="res://addons/easy_charts/control_charts/chart.tscn" type="PackedScene" id=2] + +[node name="BarChart" instance=ExtResource( 2 )] +script = ExtResource( 1 ) + +[node name="Tooltip" parent="." index="1"] +margin_right = 20.0 +margin_bottom = 16.0 +__meta__ = { +"_edit_use_anchors_": true +} diff --git a/game/addons/easy_charts/control_charts/LineChart/line_chart.gd b/game/addons/easy_charts/control_charts/LineChart/line_chart.gd new file mode 100644 index 0000000..1d448ea --- /dev/null +++ b/game/addons/easy_charts/control_charts/LineChart/line_chart.gd @@ -0,0 +1,46 @@ +extends ScatterChart +class_name LineChart + +func _draw_line(from: Point, to: Point, function_index: int) -> void: + draw_line( + from.position, + to.position, + chart_properties.get_function_color(function_index), + chart_properties.line_width, + true + ) + +func _draw_spline(points: Array, function: int, density: float = 10.0, tension: float = 1) -> void: + var spline_points: Array = [] + + var augmented: Array = points.duplicate(true) + var pi: Point = Point.new(points.front().position - Vector2(10, -10), Pair.new()) + var pf: Point = Point.new(points.back().position + Vector2(10, 10), Pair.new()) + + augmented.insert(0, pi) + augmented.append(pf) + + for p in range(1, augmented.size() - 2, 1) : #(inclusive) + + for f in range(0, density + 1, 1): + spline_points.append( + augmented[p].position.cubic_interpolate( + augmented[p + 1].position, + augmented[p - 1].position, + augmented[p + 2].position, + f / density) + ) + + for i in range(1, spline_points.size()): + draw_line(spline_points[i-1], spline_points[i], chart_properties.get_function_color(function), chart_properties.line_width, true) + +func _draw_lines() -> void: + for function in function_points.size(): + if chart_properties.use_splines: + _draw_spline(function_points[function], function) + else: + for i in range(1, function_points[function].size()): + _draw_line(function_points[function][i - 1], function_points[function][i], function) + +func _draw() -> void: + _draw_lines() diff --git a/game/addons/easy_charts/control_charts/LineChart/line_chart.tscn b/game/addons/easy_charts/control_charts/LineChart/line_chart.tscn new file mode 100644 index 0000000..88a0d37 --- /dev/null +++ b/game/addons/easy_charts/control_charts/LineChart/line_chart.tscn @@ -0,0 +1,7 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://addons/easy_charts/control_charts/LineChart/line_chart.gd" type="Script" id=1] +[ext_resource path="res://addons/easy_charts/control_charts/chart.tscn" type="PackedScene" id=2] + +[node name="LineChart" instance=ExtResource( 2 )] +script = ExtResource( 1 ) diff --git a/game/addons/easy_charts/control_charts/ScatterChart/scatter_chart.gd b/game/addons/easy_charts/control_charts/ScatterChart/scatter_chart.gd new file mode 100644 index 0000000..717ca34 --- /dev/null +++ b/game/addons/easy_charts/control_charts/ScatterChart/scatter_chart.gd @@ -0,0 +1,124 @@ +extends Chart +class_name ScatterChart + +signal point_entered(point) + +var _point_box_rad: int = 10 + +# List of all unordered points belonging to this plot +var points: Array = [] + +# List of all points, grouped by function +var function_points: Array = [] + +# Currently focused point +var focused_point: Point = null + +func _clear_points() -> void: + points.clear() + function_points.clear() + +func _clear() -> void: + _clear_points() + +func _get_point_box(point: Point, rad: int) -> Rect2: + return Rect2(point.position - (Vector2.ONE * rad), (Vector2.ONE * rad * 2)) + +func _get_function_point(point: Point) -> int: + var point_f_index: int = -1 + for f_point in function_points.size(): + var found: int = function_points[f_point].find(point) + if found != -1: + point_f_index = f_point + break + return point_f_index + +func _input(event: InputEvent) -> void: + if event is InputEventMouse: + for point in points: + if _get_point_box(point, _point_box_rad).abs().has_point(event.position): + if focused_point == point: + return + else: + focused_point = point + var func_index: int = _get_function_point(focused_point) + $Tooltip.update_values( + str(point.value.left), + str(point.value.right), + _get_function_name(func_index), + _get_function_color(func_index) + ) + $Tooltip.show() + emit_signal("point_entered", point) + return + # Mouse is not in any point's box + focused_point = null + $Tooltip.hide() + +func _draw_point(point: Point, function_index: int) -> void: + match chart_properties.get_point_shape(function_index): + Point.Shape.CIRCLE: + draw_circle(point.position, chart_properties.point_radius, chart_properties.get_function_color(function_index)) + Point.Shape.SQUARE: + draw_rect(_get_point_box(point, chart_properties.point_radius), chart_properties.get_function_color(function_index), true, 1.0, false) + Point.Shape.TRIANGLE: + draw_colored_polygon( + PoolVector2Array([ + point.position + (Vector2.UP * chart_properties.point_radius * 1.3), + point.position + (Vector2.ONE * chart_properties.point_radius * 1.3), + point.position - (Vector2(1, -1) * chart_properties.point_radius * 1.3) + ]), chart_properties.get_function_color(function_index), [], null, null, false + ) + Point.Shape.CROSS: + draw_line( + point.position - (Vector2.ONE * chart_properties.point_radius), + point.position + (Vector2.ONE * chart_properties.point_radius), + chart_properties.get_function_color(function_index), chart_properties.point_radius, true + ) + draw_line( + point.position + (Vector2(1, -1) * chart_properties.point_radius), + point.position + (Vector2(-1, 1) * chart_properties.point_radius), + chart_properties.get_function_color(function_index), chart_properties.point_radius / 2, true + ) + +# # (debug) +# draw_rect( +# _get_point_box(point, _point_box_rad), +# Color.red, +# false, 1, true +# ) + +func _draw_points() -> void: + for function in function_points.size(): + for point in function_points[function]: + _draw_point(point, function) + +func _calculate_points() -> void: + var validation: int = _validate_sampled_axis(x_sampled, y_sampled) + if not validation == OK: + printerr("Cannot plot points for invalid dataset! Error: %s" % validation) + return + + if y_sampled.values[0] is Array: + for yxi in y_sampled.values.size(): + var _function_points: Array = [] + for i in y_sampled.values[yxi].size(): + var real_point_val: Pair = Pair.new(x[i], y[yxi][i]) + var sampled_point_pos: Vector2 = Vector2(x_sampled.values[i], y_sampled.values[yxi][i]) + var point: Point = Point.new(sampled_point_pos, real_point_val) + _function_points.append(point) + points.append(point) + function_points.append(_function_points) + else: + for i in y_sampled.values.size(): + var real_point_val: Pair = Pair.new(x[i], y[i]) + var sampled_point_pos: Vector2 = Vector2(x_sampled.values[i], y_sampled.values[i]) + var point: Point = Point.new(sampled_point_pos, real_point_val) + points.append(point) + function_points.append(points) + +func _draw() -> void: + _calculate_points() + + if chart_properties.points: + _draw_points() diff --git a/game/addons/easy_charts/control_charts/ScatterChart/scatter_chart.tscn b/game/addons/easy_charts/control_charts/ScatterChart/scatter_chart.tscn new file mode 100644 index 0000000..009c249 --- /dev/null +++ b/game/addons/easy_charts/control_charts/ScatterChart/scatter_chart.tscn @@ -0,0 +1,7 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://addons/easy_charts/control_charts/ScatterChart/scatter_chart.gd" type="Script" id=1] +[ext_resource path="res://addons/easy_charts/control_charts/chart.tscn" type="PackedScene" id=2] + +[node name="ScatterChart" instance=ExtResource( 2 )] +script = ExtResource( 1 ) diff --git a/game/addons/easy_charts/control_charts/chart.gd b/game/addons/easy_charts/control_charts/chart.gd new file mode 100644 index 0000000..23c1e28 --- /dev/null +++ b/game/addons/easy_charts/control_charts/chart.gd @@ -0,0 +1,503 @@ +extends Control +class_name Chart + +var x: Array +var y: Array + +var x_min_max: Pair = Pair.new() # Min and Max values of @x +var x_domain: Pair = Pair.new() # Rounded domain of values of @x +var y_min_max: Pair = Pair.new() # Min and Max values of @y +var y_domain: Pair = Pair.new() # Rounded domain of values of @x + +var x_sampled: SampledAxis = SampledAxis.new() +var y_sampled: SampledAxis = SampledAxis.new() + +var x_labels: Array = [] +var y_labels: Array = [] +var functions_names: Array = [] + +###### STYLE +var chart_properties: ChartProperties = ChartProperties.new() + +#### INTERNAL +# The bounding_box of the chart +var node_box: Rect2 +var bounding_box: Rect2 +var plot_offset: Vector2 +var plot_box: Rect2 + +# The Reference Rectangle to plot samples +# It is the @bounding_box Rectangle inverted on the Y axis +var x_sampled_domain: Pair +var y_sampled_domain: Pair + +var _padding_offset: Vector2 = Vector2(20.0, 20.0) +var _internal_offset: Vector2 = Vector2(15.0, 15.0) + +var y_has_decimals: bool +var _y_label_size: Vector2 = Vector2.ZERO # offset only on the X axis +var _y_label_offset: int = 15 # offset only on the X axis +var _y_ticklabel_size: Vector2 # offset only on the X axis +var _y_ticklabel_offset: int = 5 # offset only on the X axis +var _y_tick_size: int = 7 + +var x_has_decimals: bool +var _x_label_size: Vector2 = Vector2.ZERO # offset only on the X axis +var _x_label_offset: int = 15 # offset only on the X axis +var _x_ticklabel_size: Vector2 # offset only on the X axis +var _x_ticklabel_offset: int = 5 # offset only on the X axis +var _x_tick_size: int = 7 + +########### +func _ready() -> void: + set_process_input(false) + set_process(false) + +func validate_input_samples(samples: Array) -> bool: + if samples.size() > 1 and samples[0] is Array: + for sample in samples: + if (not sample is Array) or sample.size() != samples[0].size(): + return false + return true + +func plot(x: Array, y: Array, properties: ChartProperties = self.chart_properties) -> void: + self.x = x + self.y = y + + if properties != null: + self.chart_properties = properties + + set_process_input(chart_properties.interactive) + + update() + +func _map_pair(val: float, rel: Pair, ref: Pair) -> float: + return range_lerp(val, rel.left, rel.right, ref.left, ref.right) + +func _has_decimals(values: Array) -> bool: + var temp: Array = values.duplicate(true) + + if temp[0] is Array: + for dim in temp: + for val in dim: + if val is String: + return false + if abs(fmod(val, 1)) > 0.0: + return true + else: + for val in temp: + if val is String: + return false + if abs(fmod(val, 1)) > 0.0: + return true + + return false + +func _find_min_max(values: Array) -> Pair: + var temp: Array = values.duplicate(true) + var _min: float + var _max: float + + if temp[0] is Array: + var min_ts: Array + var max_ts: Array + for dim in temp: + min_ts.append(dim.min()) + max_ts.append(dim.max()) + _min = min_ts.min() + _max = max_ts.max() + else: + _min = temp.min() + _max = temp.max() + + return Pair.new(_min, _max) + +func _sample_values(values: Array, rel_values: Pair, ref_values: Pair) -> SampledAxis: + if values.empty(): + printerr("Trying to plot an empty dataset!") + return SampledAxis.new() + + if values[0] is Array: + if values.size() > 1: + for dim in values: + if values[0].size() != dim.size(): + printerr("Cannot plot a dataset with dimensions of different size!") + return SampledAxis.new() + + var temp: Array = values.duplicate(true) + + var rels: Array = [] + var division_size: float + if temp[0] is Array: + for t_dim in temp: + var rels_t: Array = [] + for val in t_dim: + rels_t.append(_map_pair(val, rel_values, ref_values)) + rels.append(rels_t) + + else: + division_size = (ref_values.right - ref_values.left) / values.size() + for val_i in temp.size(): + if temp[val_i] is String: + rels.append(val_i * division_size) + else: + rels.append(_map_pair(temp[val_i], rel_values, ref_values)) + + return SampledAxis.new(rels, rel_values) + +func _round_min(val: float) -> float: + return round(val) if abs(val) < 10 else floor(val / 10.0) * 10.0 + +func _round_max(val: float) -> float: + return round(val) if abs(val) < 10 else ceil(val / 10.0) * 10.0 + + +func _calc_x_domain() -> void: + x_min_max = _find_min_max(x) + x_domain = Pair.new(_round_min(x_min_max.left), _round_max(x_min_max.right)) + +func _sample_x() -> void: + ### @sampled_domain, which are the domain relative to the sampled values + ### x (real value) --> sampling --> x_sampled (pixel value in canvas) + x_sampled_domain = Pair.new(plot_box.position.x, plot_box.end.x) + + # samples + x_sampled = _sample_values(x, x_min_max, x_sampled_domain) + +func _calc_y_domain() -> void: + y_min_max = _find_min_max(y) + y_domain = Pair.new(_round_min(y_min_max.left), _round_max(y_min_max.right)) + +func _sample_y() -> void: + ### @sampled_domain, which are the domain relative to the sampled values + ### x (real value) --> sampling --> x_sampled (pixel value in canvas) + y_sampled_domain = Pair.new(plot_box.end.y, plot_box.position.y) + + # samples + y_sampled = _sample_values(y, y_domain, y_sampled_domain) + + +func _find_longest_x() -> String: + return ("%.2f" if x_has_decimals else "%s") % x_domain.right + +func _pre_process() -> void: + _calc_x_domain() + _calc_y_domain() + + var frame: Rect2 = get_global_rect() + + + #### @node_box size, which is the whole "frame" + node_box = Rect2(Vector2.ZERO, frame.size - frame.position) + + #### calculating offset from the @node_box for the @bounding_box. + plot_offset = _padding_offset + + ### if @labels drawing is enabled, calcualte offsets + if chart_properties.labels: + ### labels (X, Y, Title) + _x_label_size = chart_properties.font.get_string_size(chart_properties.x_label) + _y_label_size = chart_properties.font.get_string_size(chart_properties.y_label) + + ### tick labels + + ###### --- X + x_has_decimals = _has_decimals(x) + # calculate the string length of the largest value on the X axis. + # remember that "-" sign adds additional pixels, and it is relative only to negative numbers! + var x_max_formatted: String = _find_longest_x() + _x_ticklabel_size = chart_properties.font.get_string_size(x_max_formatted) + + plot_offset.y += _x_label_offset + _x_label_size.y + _x_ticklabel_offset + _x_ticklabel_size.y + + ###### --- Y + y_has_decimals = _has_decimals(y) + # calculate the string length of the largest value on the Y axis. + # remember that "-" sign adds additional pixels, and it is relative only to negative numbers! + var y_max_formatted: String = ("%.2f" if y_has_decimals else "%s") % y_domain.right + if y_domain.left < 0: + # negative number + var y_min_formatted: String = ("%.2f" if y_has_decimals else "%s") % y_domain.left + if y_min_formatted.length() >= y_max_formatted.length(): + _y_ticklabel_size = chart_properties.font.get_string_size(y_min_formatted) + else: + _y_ticklabel_size = chart_properties.font.get_string_size(y_max_formatted) + else: + _y_ticklabel_size = chart_properties.font.get_string_size(y_max_formatted) + + plot_offset.x += _y_label_offset + _y_label_size.y + _y_ticklabel_offset + _y_ticklabel_size.x + + ### if @ticks drawing is enabled, calculate offsets + if chart_properties.ticks: + plot_offset.x += _y_tick_size + plot_offset.y += _x_tick_size + + ### @bounding_box, where the points will be plotted + bounding_box = Rect2( + plot_offset, + frame.size - (plot_offset * 2) + ) + + plot_box = Rect2( + bounding_box.position + _internal_offset, + bounding_box.size - (_internal_offset * 2) + ) + + _sample_x() + _sample_y() + +func _draw_borders() -> void: + draw_rect(node_box, Color.red, false, 1, true) + +func _draw_bounding_box() -> void: + draw_rect(bounding_box, chart_properties.colors.bounding_box, false, 1, true) + +# # (debug) +# var half: Vector2 = (bounding_box.size) / 2 +# draw_line(bounding_box.position + Vector2(half.x, 0), bounding_box.position + Vector2(half.x, bounding_box.size.y), Color.red, 3, false) +# draw_line(bounding_box.position + Vector2(0, half.y), bounding_box.position + Vector2(bounding_box.size.x, half.y), Color.red, 3, false) + +func _draw_origin() -> void: + var xorigin: float = _map_pair(0.0, x_min_max, x_sampled_domain) + var yorigin: float = _map_pair(0.0, y_domain, y_sampled_domain) + draw_line(Vector2(xorigin, bounding_box.position.y), Vector2(xorigin, bounding_box.position.y + bounding_box.size.y), Color.black, 1, 0) + draw_line(Vector2(bounding_box.position.x, yorigin), Vector2(bounding_box.position.x + bounding_box.size.x, yorigin), Color.black, 1, 0) + draw_string(chart_properties.font, Vector2(xorigin, yorigin) - Vector2(15, -15), "O", chart_properties.colors.bounding_box) + +func _draw_background() -> void: + draw_rect(node_box, Color.white, true, 1.0, false) + +# # (debug) +# var half: Vector2 = node_box.size / 2 +# draw_line(Vector2(half.x, node_box.position.y), Vector2(half.x, node_box.size.y), Color.red, 3, false) +# draw_line(Vector2(node_box.position.x, half.y), Vector2(node_box.size.x, half.y), Color.red, 3, false) + +func _draw_tick(from: Vector2, to: Vector2, color: Color) -> void: + draw_line(from, to, color, 1, true) + + +func _get_vertical_tick_label_pos(base_position: Vector2, text: String) -> Vector2: + return base_position + Vector2( + - chart_properties.font.get_string_size(text).x / 2, + _x_label_size.y + _x_tick_size + ) + +func _get_tick_label(line_index: int, line_value: float) -> String: + var tick_lbl: String = "" + if x_labels.empty(): + tick_lbl = ("%.2f" if x_has_decimals else "%s") % [x_min_max.left + (line_index * line_value)] + else: + tick_lbl = x_labels[clamp(line_value * line_index, 0, x_labels.size() - 1)] + + return tick_lbl + +func _draw_vertical_gridline_component(p1: Vector2, p2: Vector2, line_index: int, line_value: float) -> void: + if chart_properties.labels: + var tick_lbl: String = _get_tick_label(line_index, line_value) + draw_string( + chart_properties.font, + _get_vertical_tick_label_pos(p2, tick_lbl), + tick_lbl, + chart_properties.colors.bounding_box + ) + + # Draw V Ticks + if chart_properties.ticks: + _draw_tick(p2, p2 + Vector2(0, _x_tick_size), chart_properties.colors.bounding_box) + + # Draw V Grid Lines + if chart_properties.grid: + draw_line(p1, p2, chart_properties.colors.grid, 1, true) + + +func _draw_horizontal_tick_label(font: Font, position: Vector2, color: Color, line_index: int, line_value: float) -> void: + var tick_lbl: String = "" + if y_labels.empty(): + tick_lbl = ("%.2f" if y_has_decimals else "%s") % [y_domain.left + (line_index * line_value)] + else: + tick_lbl = y_labels[clamp(y_labels.size() * line_index, 0, y_labels.size() - 1)] + + draw_string( + chart_properties.font, + position - Vector2( + chart_properties.font.get_string_size(tick_lbl).x + _y_ticklabel_offset + _y_tick_size, + - _y_ticklabel_size.y * 0.35 + ), + tick_lbl, + chart_properties.colors.bounding_box + ) + + +func _draw_horizontal_gridline_component(p1: Vector2, p2: Vector2, line_index: int, line_value: float) -> void: + # Draw H labels + if chart_properties.labels: + _draw_horizontal_tick_label( + chart_properties.font, + p1, + chart_properties.colors.bounding_box, + line_index, + line_value + ) + + # Draw H Ticks + if chart_properties.ticks: + _draw_tick(p1, p1 - Vector2(_y_tick_size, 0), chart_properties.colors.bounding_box) + + # Draw H Grid Lines + if chart_properties.grid: + draw_line(p1, p2, chart_properties.colors.grid, 1, true) + +func _draw_vertical_grid() -> void: + # draw vertical lines + + # 1. the amount of lines is equals to the X_scale: it identifies in how many sectors the x domain + # should be devided + # 2. calculate the spacing between each line in pixel. It is equals to x_sampled_domain / x_scale + # 3. calculate the offset in the real x domain, which is x_domain / x_scale. + var x_pixel_dist: float = (x_sampled.min_max.right - x_sampled.min_max.left) / (chart_properties.x_scale) + var x_lbl_val: float = (x_min_max.right - x_min_max.left) / (chart_properties.x_scale) + for _x in chart_properties.x_scale + 1: + var x_val: float = _x * x_pixel_dist + x_sampled.min_max.left + var top: Vector2 = Vector2( + range_lerp(x_val, x_sampled.min_max.left, x_sampled.min_max.right, x_sampled_domain.left, x_sampled_domain.right), + bounding_box.position.y + ) + var bottom: Vector2 = Vector2( + range_lerp(x_val, x_sampled.min_max.left, x_sampled.min_max.right, x_sampled_domain.left, x_sampled_domain.right), + bounding_box.size.y + bounding_box.position.y + ) + + _draw_vertical_gridline_component(top, bottom, _x, x_lbl_val) + +func _draw_horizontal_grid() -> void: + # 1. the amount of lines is equals to the y_scale: it identifies in how many sectors the y domain + # should be devided + # 2. calculate the spacing between each line in pixel. It is equals to y_sampled_domain / y_scale + # 3. calculate the offset in the real y domain, which is y_domain / y_scale. + var y_pixel_dist: float = (y_sampled.min_max.right - y_sampled.min_max.left) / (chart_properties.y_scale) + var y_lbl_val: float = (y_domain.right - y_domain.left) / (chart_properties.y_scale) + for _y in chart_properties.y_scale + 1: + var y_val: float = (_y * y_pixel_dist) + y_sampled.min_max.left + var left: Vector2 = Vector2( + bounding_box.position.x, + range_lerp(y_val, y_sampled.min_max.left, y_sampled.min_max.right, y_sampled_domain.left, y_sampled_domain.right) + ) + var right: Vector2 = Vector2( + bounding_box.size.x + bounding_box.position.x, + range_lerp(y_val, y_sampled.min_max.left, y_sampled.min_max.right, y_sampled_domain.left, y_sampled_domain.right) + ) + + _draw_horizontal_gridline_component(left, right, _y, y_lbl_val) + +func _draw_grid() -> void: + var validation: int = _validate_sampled_axis(x_sampled, y_sampled) + if not validation == OK: + printerr("Cannot draw grid for invalid dataset! Error: %s" % validation) + return + + _draw_vertical_grid() + _draw_horizontal_grid() + +func _create_canvas_label(text: String, position: Vector2, rotation: float = 0.0) -> Label: + var lbl: Label = Label.new() + $Canvas.add_child(lbl) + lbl.set("custom_fonts/font", chart_properties.font) + lbl.set_text(text) + lbl.modulate = chart_properties.colors.bounding_box + lbl.rect_rotation = rotation + lbl.rect_position = position + return lbl + +func _update_canvas_label(canvas_label: Label, text: String, position: Vector2, rotation: float = 0.0) -> void: + canvas_label.set_text(text) + canvas_label.modulate = chart_properties.colors.bounding_box + canvas_label.rect_rotation = rotation + canvas_label.rect_position = position + +func _draw_yaxis_label() -> void: + _update_canvas_label( + $Canvas/YLabel, + chart_properties.y_label, + Vector2(_padding_offset.x, (node_box.size.y / 2) + (_y_label_size.x / 2)), + -90 + ) + +func _draw_xaxis_label() -> void: + _update_canvas_label( + $Canvas/XLabel, + chart_properties.x_label, + Vector2( + node_box.size.x/2 - (_x_label_size.x / 2), + node_box.size.y - _padding_offset.y - _x_label_size.y + ) + ) + +func _draw_title() -> void: + _update_canvas_label( + $Canvas/Title, + chart_properties.title, + Vector2(node_box.size.x / 2, _padding_offset.y*2) - (chart_properties.font.get_string_size(chart_properties.title) / 2) + ) + +func _clear_canvas_labels() -> void: + for label in $Canvas.get_children(): + label.queue_free() + +func _clear() -> void: + _clear_canvas_labels() + +# Draw Loop: +# the drow loop gives order to what thigs will be drawn +# each chart specifies its own draw loop that inherits from this one. +# The draw loop also contains the "processing loop" which is where +# everything is calculated in a separated function. +func _draw(): + if not (validate_input_samples(x) and validate_input_samples(y)): + printerr("Input samples are invalid!") + return + + _clear() + _pre_process() + + if chart_properties.background: + _draw_background() + + if chart_properties.borders: + _draw_borders() + + if chart_properties.grid or chart_properties.ticks or chart_properties.labels: + _draw_grid() + + if chart_properties.bounding_box: + _draw_bounding_box() + + if chart_properties.origin: + _draw_origin() + + if chart_properties.labels: + _draw_xaxis_label() + _draw_yaxis_label() + _draw_title() + +func _validate_sampled_axis(x_data: SampledAxis, y_data: SampledAxis) -> int: + var error: int = 0 # OK + if x_data.values.empty() or y_data.values.empty(): + # Either there are no X or Y + error = 1 + elif y_data.values[0] is Array: + for dim in y_data.values: + if dim.size() != x_data.values.size(): + error = 3 # one of Y dim has not X length + break + else: + if y_data.values.size() != x_data.values.size(): + # X and Y samples don't have same length + error = 2 + return error + +# ----- utilities +func _get_function_name(function_idx: int) -> String: + return functions_names[function_idx] if functions_names.size() > 0 else "Function %s" % function_idx + +func _get_function_color(function_idx: int) -> Color: + return chart_properties.colors.functions[function_idx] if chart_properties.colors.functions.size() > 0 else Color.black diff --git a/game/addons/easy_charts/control_charts/chart.tscn b/game/addons/easy_charts/control_charts/chart.tscn new file mode 100644 index 0000000..ee1235c --- /dev/null +++ b/game/addons/easy_charts/control_charts/chart.tscn @@ -0,0 +1,30 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://addons/easy_charts/control_charts/chart.gd" type="Script" id=1] +[ext_resource path="res://addons/easy_charts/utilities/containers/data_tooltip/data_tooltip.tscn" type="PackedScene" id=2] + +[node name="Chart" type="Control"] +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource( 1 ) + +[node name="Canvas" type="Control" parent="."] +anchor_right = 1.0 +anchor_bottom = 1.0 + +[node name="Title" type="Label" parent="Canvas"] +margin_right = 40.0 +margin_bottom = 14.0 + +[node name="XLabel" type="Label" parent="Canvas"] +margin_right = 40.0 +margin_bottom = 14.0 + +[node name="YLabel" type="Label" parent="Canvas"] +margin_right = 40.0 +margin_bottom = 14.0 + +[node name="Tooltip" parent="." instance=ExtResource( 2 )] +visible = false +margin_right = 20.0 +margin_bottom = 16.0 diff --git a/game/addons/easy_charts/examples/bar_chart/Control.gd b/game/addons/easy_charts/examples/bar_chart/Control.gd new file mode 100644 index 0000000..e7b118d --- /dev/null +++ b/game/addons/easy_charts/examples/bar_chart/Control.gd @@ -0,0 +1,50 @@ +extends Control + +onready var chart: BarChart = $BarChart + +func _ready(): + # Let's create our @x values + var x: Array = ["Day 1", "Day 2", "Day 3", "Day 4", "Day 5"] + # And our y values. It can be an n-size array of arrays. + # NOTE: `x.size() == y.size()` or `x.size() == y[n].size()` + var y: Array = [ + 20, 10, -15, 30, 42 + ] + + # Add some labels for the x axis, we don't want to use our x values array + # they will be printed on the chart ticks instead of the value of the x axis. + var x_labels: Array = ArrayOperations.suffix(x, "s") + + # Let's customize the chart properties, which specify how the chart + # should look, plus some additional elements like labels, the scale, etc... + var cp: ChartProperties = ChartProperties.new() + cp.grid = true + cp.title = "Air Quality Monitoring" + cp.x_label = ("Days") + cp.x_scale = 20 + cp.y_label = ("Sensor values") + cp.y_scale = 10 + cp.points = false + cp.interactive = true # false by default, it allows the chart to create a tooltip to show point values + # and interecept clicks on the plot + + # Set the x_labels +# $LineChart.x_labels = x_labels + + # Plot our data + chart.plot(x, y, cp) + + # Uncommenting this line will show how real time data plotting works + set_process(false) + +func _process(delta: float): + # This function updates the values of chart x, y, and x_labels array + # and updaptes the plot + var new_val: String = "Day %s" % (chart.x.size() + 1) + chart.x.append(new_val) + chart.y.append(randi() % 40) + chart.update() + + +func _on_CheckButton_pressed(): + set_process(not is_processing()) diff --git a/game/addons/easy_charts/examples/bar_chart/Control.tscn b/game/addons/easy_charts/examples/bar_chart/Control.tscn new file mode 100644 index 0000000..a8e7dc4 --- /dev/null +++ b/game/addons/easy_charts/examples/bar_chart/Control.tscn @@ -0,0 +1,49 @@ +[gd_scene load_steps=4 format=2] + +[ext_resource path="res://addons/easy_charts/control_charts/BarChart/bar_chart.tscn" type="PackedScene" id=1] +[ext_resource path="res://addons/easy_charts/examples/bar_chart/Control.gd" type="Script" id=2] + +[sub_resource type="StyleBoxFlat" id=1] +content_margin_right = 5.0 +content_margin_bottom = 5.0 +draw_center = false +border_width_right = 2 +border_width_bottom = 2 +border_color = Color( 0, 0, 0, 1 ) + +[node name="Control" type="Control"] +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource( 2 ) +__meta__ = { +"_edit_use_anchors_": true +} + +[node name="BarChart" parent="." instance=ExtResource( 1 )] + +[node name="CheckButton" type="CheckButton" parent="."] +margin_right = 223.0 +margin_bottom = 40.0 +custom_colors/font_color = Color( 0, 0, 0, 1 ) +custom_colors/font_color_pressed = Color( 0, 0, 0, 1 ) +custom_colors/font_color_hover = Color( 0, 0, 0, 1 ) +custom_colors/font_color_hover_pressed = Color( 0, 0, 0, 1 ) +custom_colors/font_color_focus = Color( 0, 0, 0, 1 ) +custom_colors/font_color_disabled = Color( 0, 0, 0, 1 ) +text = "Start Relatime Plotting" + +[node name="Label" type="Label" parent="."] +anchor_left = 1.0 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +margin_left = -159.0 +margin_top = -19.0 +custom_colors/font_color = Color( 0, 0, 0, 1 ) +custom_styles/normal = SubResource( 1 ) +text = "Try to scale the window!" +__meta__ = { +"_edit_lock_": true +} + +[connection signal="pressed" from="CheckButton" to="." method="_on_CheckButton_pressed"] diff --git a/game/addons/easy_charts/examples/line_chart/Control.gd b/game/addons/easy_charts/examples/line_chart/Control.gd new file mode 100644 index 0000000..80b38bb --- /dev/null +++ b/game/addons/easy_charts/examples/line_chart/Control.gd @@ -0,0 +1,55 @@ +extends Control + +func _ready(): + # Let's create our @x values + var x: Array = ArrayOperations.multiply_float(range(-2*PI, +2*PI, 1), 0.5) + # And our y values. It can be an n-size array of arrays. + # NOTE: `x.size() == y.size()` or `x.size() == y[n].size()` + var y: Array = [ + ArrayOperations.multiply_float(ArrayOperations.cos(x), 1.0), + ArrayOperations.multiply_float(ArrayOperations.sin(x), 1.0) + ] + + # Add some labels for the x axis, we don't want to use our x values array + # they will be printed on the chart ticks instead of the value of the x axis. + var x_labels: Array = ArrayOperations.suffix(x, "s") + + # Let's customize the chart properties, which specify how the chart + # should look, plus some additional elements like labels, the scale, etc... + var cp: ChartProperties = ChartProperties.new() + cp.grid = false + cp.origin = true + cp.title = "Air Quality Monitoring" + cp.x_label = ("Time") + cp.x_scale = 10 + cp.y_label = ("Sensor values") + cp.y_scale = 10 + cp.points = true + cp.line_width = 2.0 + cp.point_radius = 2.5 + cp.use_splines = true + cp.interactive = false # false by default, it allows the chart to create a tooltip to show point values + # and interecept clicks on the plot + + # Set the x_labels +# $LineChart.x_labels = x_labels + + # Plot our data + $LineChart.plot(x, y, cp) + + # Uncommenting this line will show how real time data plotting works + set_process(false) + +func _process(delta: float): + # This function updates the values of chart x, y, and x_labels array + # and updaptes the plot + var new_val: float = $LineChart.x.back() + 1 + $LineChart.x.append(new_val) + $LineChart.y[0].append(cos(new_val)) + $LineChart.y[1].append(2 + sin(new_val)) + $LineChart.x_labels.append(str(new_val) + "s") + $LineChart.update() + + +func _on_CheckButton_pressed(): + set_process(not is_processing()) diff --git a/game/addons/easy_charts/examples/line_chart/Control.tscn b/game/addons/easy_charts/examples/line_chart/Control.tscn new file mode 100644 index 0000000..70e09b7 --- /dev/null +++ b/game/addons/easy_charts/examples/line_chart/Control.tscn @@ -0,0 +1,49 @@ +[gd_scene load_steps=4 format=2] + +[ext_resource path="res://addons/easy_charts/control_charts/LineChart/line_chart.tscn" type="PackedScene" id=1] +[ext_resource path="res://addons/easy_charts/examples/line_chart/Control.gd" type="Script" id=2] + +[sub_resource type="StyleBoxFlat" id=1] +content_margin_right = 5.0 +content_margin_bottom = 5.0 +draw_center = false +border_width_right = 2 +border_width_bottom = 2 +border_color = Color( 0, 0, 0, 1 ) + +[node name="Control" type="Control"] +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource( 2 ) +__meta__ = { +"_edit_use_anchors_": true +} + +[node name="LineChart" parent="." instance=ExtResource( 1 )] + +[node name="CheckButton" type="CheckButton" parent="."] +margin_right = 223.0 +margin_bottom = 40.0 +custom_colors/font_color = Color( 0, 0, 0, 1 ) +custom_colors/font_color_pressed = Color( 0, 0, 0, 1 ) +custom_colors/font_color_hover = Color( 0, 0, 0, 1 ) +custom_colors/font_color_hover_pressed = Color( 0, 0, 0, 1 ) +custom_colors/font_color_focus = Color( 0, 0, 0, 1 ) +custom_colors/font_color_disabled = Color( 0, 0, 0, 1 ) +text = "Start Relatime Plotting" + +[node name="Label" type="Label" parent="."] +anchor_left = 1.0 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +margin_left = -159.0 +margin_top = -19.0 +custom_colors/font_color = Color( 0, 0, 0, 1 ) +custom_styles/normal = SubResource( 1 ) +text = "Try to scale the window!" +__meta__ = { +"_edit_lock_": true +} + +[connection signal="pressed" from="CheckButton" to="." method="_on_CheckButton_pressed"] diff --git a/game/addons/easy_charts/examples/scatter_chart/Control.gd b/game/addons/easy_charts/examples/scatter_chart/Control.gd new file mode 100644 index 0000000..3f32c30 --- /dev/null +++ b/game/addons/easy_charts/examples/scatter_chart/Control.gd @@ -0,0 +1,50 @@ +extends Control + +func _ready(): + # Let's create our @x values + var x: Array = ArrayOperations.multiply_float(range(-10, 10, 1), 0.5) + # And our y values. It can be an n-size array of arrays. + # NOTE: `x.size() == y.size()` or `x.size() == y[n].size()` + var y: Array = [ + ArrayOperations.multiply_int(ArrayOperations.cos(x), 20), + ArrayOperations.add_float(ArrayOperations.multiply_int(ArrayOperations.sin(x), 20), 20) + ] + # Add some labels for the x axis, we don't want to use our x values array + # they will be printed on the chart ticks instead of the value of the x axis. + var x_labels: Array = ArrayOperations.suffix(x, "s") + + # Let's customize the chart properties, which specify how the chart + # should look, plus some additional elements like labels, the scale, etc... + var cp: ChartProperties = ChartProperties.new() + cp.grid = true + cp.origin = false + cp.title = "Air Quality Monitoring" + cp.x_label = ("Time") + cp.x_scale = 10 + cp.y_label = ("Sensor values") + cp.y_scale = 30 + cp.interactive = true # false by default, it allows the chart to create a tooltip to show point values + # and interecept clicks on the plot + + # Set the x_labels +# $ScatterChart.x_labels = x_labels + + # Plot our data + $ScatterChart.plot(x, y, cp) + + # Uncommenting this line will show how real time data plotting works + set_process(false) + +func _process(delta: float): + # This function updates the values of chart x, y, and x_labels array + # and updaptes the plot + var new_val: float = $ScatterChart.x.back() + 1 + $ScatterChart.x.append(new_val) + $ScatterChart.y[0].append(cos(new_val) * 20) + $ScatterChart.y[1].append(20 + sin(new_val) * 20) + $ScatterChart.x_labels.append(str(new_val) + "s") + $ScatterChart.update() + + +func _on_CheckButton_pressed(): + set_process(not is_processing()) diff --git a/game/addons/easy_charts/examples/scatter_chart/Control.tscn b/game/addons/easy_charts/examples/scatter_chart/Control.tscn new file mode 100644 index 0000000..433d9b0 --- /dev/null +++ b/game/addons/easy_charts/examples/scatter_chart/Control.tscn @@ -0,0 +1,49 @@ +[gd_scene load_steps=4 format=2] + +[ext_resource path="res://addons/easy_charts/control_charts/ScatterChart/scatter_chart.tscn" type="PackedScene" id=1] +[ext_resource path="res://addons/easy_charts/examples/scatter_chart/Control.gd" type="Script" id=2] + +[sub_resource type="StyleBoxFlat" id=1] +content_margin_right = 5.0 +content_margin_bottom = 5.0 +draw_center = false +border_width_right = 2 +border_width_bottom = 2 +border_color = Color( 0, 0, 0, 1 ) + +[node name="Control" type="Control"] +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource( 2 ) +__meta__ = { +"_edit_use_anchors_": true +} + +[node name="ScatterChart" parent="." instance=ExtResource( 1 )] + +[node name="CheckButton" type="CheckButton" parent="."] +margin_right = 223.0 +margin_bottom = 40.0 +custom_colors/font_color = Color( 0, 0, 0, 1 ) +custom_colors/font_color_pressed = Color( 0, 0, 0, 1 ) +custom_colors/font_color_hover = Color( 0, 0, 0, 1 ) +custom_colors/font_color_hover_pressed = Color( 0, 0, 0, 1 ) +custom_colors/font_color_focus = Color( 0, 0, 0, 1 ) +custom_colors/font_color_disabled = Color( 0, 0, 0, 1 ) +text = "Start Relatime Plotting" + +[node name="Label" type="Label" parent="."] +anchor_left = 1.0 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +margin_left = -159.0 +margin_top = -19.0 +custom_colors/font_color = Color( 0, 0, 0, 1 ) +custom_styles/normal = SubResource( 1 ) +text = "Try to scale the window!" +__meta__ = { +"_edit_lock_": true +} + +[connection signal="pressed" from="CheckButton" to="." method="_on_CheckButton_pressed"] diff --git a/game/addons/easy_charts/icon.png b/game/addons/easy_charts/icon.png new file mode 100644 index 0000000..c1c6872 Binary files /dev/null and b/game/addons/easy_charts/icon.png differ diff --git a/game/addons/easy_charts/icon.png.import b/game/addons/easy_charts/icon.png.import new file mode 100644 index 0000000..2b03f03 --- /dev/null +++ b/game/addons/easy_charts/icon.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/icon.png-85017c6eecaf83ace12c82c530e61e9d.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/easy_charts/icon.png" +dest_files=[ "res://.import/icon.png-85017c6eecaf83ace12c82c530e61e9d.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/game/addons/easy_charts/plugin.cfg b/game/addons/easy_charts/plugin.cfg new file mode 100644 index 0000000..64b2573 --- /dev/null +++ b/game/addons/easy_charts/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="EasyCharts" +description="" +author="Nicolò \"fenix\" Santilio" +version="2.0.0" +script="plugin.gd" diff --git a/game/addons/easy_charts/plugin.gd b/game/addons/easy_charts/plugin.gd new file mode 100644 index 0000000..7aa2099 --- /dev/null +++ b/game/addons/easy_charts/plugin.gd @@ -0,0 +1,9 @@ +tool +extends EditorPlugin + +func _enter_tree(): + add_autoload_singleton("ECUtilities","res://addons/easy_charts/utilities/scripts/ec_utilities.gd") + +func _exit_tree(): + remove_autoload_singleton("ECUtilities") + diff --git a/game/addons/easy_charts/templates.json b/game/addons/easy_charts/templates.json new file mode 100644 index 0000000..cdf9d1f --- /dev/null +++ b/game/addons/easy_charts/templates.json @@ -0,0 +1,44 @@ +{ + "default": + { + "function_colors" : ["#1e1e1e","#1e1e1e","#1e1e1e","#1e1e1e"], + "v_lines_color" : "#cacaca", + "h_lines_color" : "#cacaca", + "outline_color" : "#1e1e1e", + "font_color" : "#1e1e1e" + }, + "clean": + { + "function_colors" : ["#f7aa29","#f4394a","#5a6b7b","#8fbf59","#504538","#B7A99A","#00D795","#FFECCC","#FF8981"], + "v_lines_color" : "#00000000", + "h_lines_color" : "#3cffffff", + "outline_color" : "#00000000", + "font_color" : "#3cffffff" + }, + "gradient": + { + "function_colors" : ["#F7AA29","#B8A806","#79A117","#2C9433","#00854C","#006571","#2F4858","#2a364f","#27294a"], + "v_lines_color" : "#64ffffff", + "h_lines_color" : "#64ffffff", + "outline_color" : "#64ffffff", + "font_color" : "#64ffffff", + }, + "minimal": + { + "function_colors" : ["#1e1e1e","#1e1e1e","#1e1e1e","#1e1e1e"], + "v_lines_color" : "#00000000", + "h_lines_color" : "#00000000", + "outline_color" : "#00000000", + "font_color" : "#00000000" + }, + "invert": + { + "function_colors" : ["#ffffff","#ffffff","#ffffff","#ffffff"], + "v_lines_color" : "#3b3b3b", + "h_lines_color" : "#3b3b3b", + "outline_color" : "#ffffff", + "font_color" : "#ffffff" + }, +} + + diff --git a/game/addons/easy_charts/utilities/classes/plotting/bar.gd b/game/addons/easy_charts/utilities/classes/plotting/bar.gd new file mode 100644 index 0000000..b9491dc --- /dev/null +++ b/game/addons/easy_charts/utilities/classes/plotting/bar.gd @@ -0,0 +1,12 @@ +extends Reference +class_name Bar + +var rect: Rect2 +var value: Pair + +func _init(rect: Rect2, value: Pair = Pair.new()) -> void: + self.value = value + self.rect = rect + +func _to_string() -> String: + return "Value: %s\nRect: %s" % [self.value, self.rect] diff --git a/game/addons/easy_charts/utilities/classes/plotting/chart_properties.gd b/game/addons/easy_charts/utilities/classes/plotting/chart_properties.gd new file mode 100644 index 0000000..7fba404 --- /dev/null +++ b/game/addons/easy_charts/utilities/classes/plotting/chart_properties.gd @@ -0,0 +1,43 @@ +extends Reference +class_name ChartProperties + +var title: String +var x_label: String +var y_label: String + +var x_scale: float = 5.0 +var y_scale: float = 2.0 + +# Scale type, 0 = linear | 1 = logarithmic +var x_scale_type: int = 0 +var y_scale_type: int = 0 + +var borders: bool = false +var background: bool = true +var bounding_box: bool = true +var grid: bool = false +var ticks: bool = true +var labels: bool = true +var origin: bool = false +var points: bool = true +var interactive: bool = false + +var use_splines: bool = false + +var colors: Dictionary = { + "bounding_box": Color.black, + "grid": Color.gray, + "functions": [Color.red, Color.green, Color.blue, Color.black] +} + +var point_radius: float = 3.0 +var line_width: float = 1.0 +var bar_width: float = 10.0 +var shapes: Array = [Point.Shape.CIRCLE, Point.Shape.SQUARE, Point.Shape.TRIANGLE, Point.Shape.CROSS] +var font: BitmapFont = Label.new().get_theme_font("") + +func get_function_color(function_index: int) -> Color: + return colors.functions[function_index] if function_index < colors.functions.size() else Color.black + +func get_point_shape(function_index: int) -> int: + return shapes[function_index] if function_index < shapes.size() else Point.Shape.CIRCLE diff --git a/game/addons/easy_charts/utilities/classes/plotting/point.gd b/game/addons/easy_charts/utilities/classes/plotting/point.gd new file mode 100644 index 0000000..9e13b36 --- /dev/null +++ b/game/addons/easy_charts/utilities/classes/plotting/point.gd @@ -0,0 +1,20 @@ +tool +extends Reference +class_name Point + +enum Shape { + CIRCLE, + TRIANGLE, + SQUARE, + CROSS +} + +var position: Vector2 +var value: Pair + +func _init(position: Vector2, value: Pair = Pair.new()) -> void: + self.value = value + self.position = position + +func _to_string() -> String: + return "Value: %s\nPosition: %s" % [self.value, self.position] diff --git a/game/addons/easy_charts/utilities/classes/plotting/sampled_axis.gd b/game/addons/easy_charts/utilities/classes/plotting/sampled_axis.gd new file mode 100644 index 0000000..7fc9e03 --- /dev/null +++ b/game/addons/easy_charts/utilities/classes/plotting/sampled_axis.gd @@ -0,0 +1,13 @@ +extends Reference +class_name SampledAxis + +var values: Array +var min_max: Pair + +func _init(values: Array = [], min_max: Pair = Pair.new()) -> void: + self.values = values + self.min_max = min_max + +func _to_string() -> String: + return "values: %s\nmin: %s, max: %s" % [self.values, self.min_max.left, self.min_max.right] + diff --git a/game/addons/easy_charts/utilities/classes/structures/array_operations.gd b/game/addons/easy_charts/utilities/classes/structures/array_operations.gd new file mode 100644 index 0000000..05b3922 --- /dev/null +++ b/game/addons/easy_charts/utilities/classes/structures/array_operations.gd @@ -0,0 +1,56 @@ +extends Reference +class_name ArrayOperations + +static func add_int(array: Array, _int: int) -> Array: + var t: Array = array.duplicate(true) + for ti in t.size(): + t[ti] = int(t[ti] + _int) + return t + +static func add_float(array: Array, _float: float) -> Array: + var t: Array = array.duplicate(true) + for ti in t.size(): + t[ti] = float(t[ti] + _float) + return t + +static func multiply_int(array: Array, _int: int) -> Array: + var t: Array = array.duplicate(true) + for ti in t.size(): + t[ti] = int(t[ti] * _int) + return t + +static func multiply_float(array: Array, _float: float) -> Array: + var t: Array = array.duplicate(true) + for ti in t.size(): + t[ti] = float(t[ti] * _float) + return t + +static func pow(array: Array, _int: int) -> Array: + var t: Array = array.duplicate(true) + for ti in t.size(): + t[ti] = float(pow(t[ti], _int)) + return t + +static func cos(array: Array) -> Array: + var t: Array = array.duplicate(true) + for val in array.size(): + t[val] = cos(t[val]) + return t + +static func sin(array: Array) -> Array: + var t: Array = array.duplicate(true) + for val in array.size(): + t[val] = sin(t[val]) + return t + +static func affix(array: Array, _string: String) -> Array: + var t: Array = array.duplicate(true) + for val in array.size(): + t[val] = str(t[val]) + _string + return t + +static func suffix(array: Array, _string: String) -> Array: + var t: Array = array.duplicate(true) + for val in array.size(): + t[val] = _string + str(t[val]) + return t diff --git a/game/addons/easy_charts/utilities/classes/structures/data_frame.gd b/game/addons/easy_charts/utilities/classes/structures/data_frame.gd new file mode 100644 index 0000000..a8ed422 --- /dev/null +++ b/game/addons/easy_charts/utilities/classes/structures/data_frame.gd @@ -0,0 +1,191 @@ +tool +extends Resource +class_name DataFrame + +var table_name : String = "" +var labels : PoolStringArray = [] +var headers : PoolStringArray = [] +var datamatrix : Matrix = null +var dataset : Array = [] + +func _init(datamatrix : Matrix, headers : PoolStringArray = [], labels : PoolStringArray = [] , table_name : String = "") -> void: + if datamatrix.empty(): datamatrix.resize(labels.size(), headers.size()) + if labels.empty() : for label in range(datamatrix.get_size().x) : labels.append(label as String) + if headers.empty() : for header in range(datamatrix.get_size().y) : headers.append(MatrixGenerator.get_letter_index(header)) + build_dataframe(datamatrix, headers, labels, table_name) + +func build_dataframe(datamatrix : Matrix, headers : PoolStringArray = [], labels : PoolStringArray = [] , table_name : String = "") -> void: + self.datamatrix = datamatrix + self.headers = headers + self.labels = labels + self.table_name = table_name + self.dataset = build_dataset_from_matrix(datamatrix, headers, labels) + +func build_dataset_from_matrix(datamatrix : Matrix, headers : PoolStringArray, labels : PoolStringArray) -> Array: + var data : Array = datamatrix.to_array() + return build_dataset(data, headers, labels) + +func build_dataset(data : Array, headers : PoolStringArray, labels : PoolStringArray) -> Array: + var dataset : Array = [Array([" "]) + Array(headers)] + for row_i in range(labels.size()): dataset.append(([labels[row_i]] + data[row_i]) if not data.empty() else [labels[row_i]]) + return dataset + +func insert_column(column : Array, header : String = "", index : int = dataset[0].size() - 1) -> void: + assert(column.size() == (datamatrix.rows() if not datamatrix.empty() else labels.size()), "error: the column size must match the dataset column size") + headers.insert(index, header if header != "" else MatrixGenerator.get_letter_index(index)) + datamatrix.insert_column(column, index) + dataset = build_dataset_from_matrix(datamatrix, headers, labels) + +func insert_row(row : Array, label : String = "", index : int = dataset.size() - 1) -> PoolStringArray: + assert(row.size() == (datamatrix.columns() if not datamatrix.empty() else headers.size()), "error: the row size must match the dataset row size") + labels.insert(index, label if label != "" else str(index)) + datamatrix.insert_row(row, index) + dataset = build_dataset_from_matrix(datamatrix, headers, labels) + return PoolStringArray([label] + row) + +func get_datamatrix() -> Matrix: + return datamatrix + +func get_dataset() -> Array: + return dataset + +func get_labels() -> PoolStringArray: + return labels + +func transpose(): + build_dataframe(MatrixGenerator.transpose(datamatrix), labels, headers, table_name) + +func _to_string() -> String: + var last_string_len : int + for row in dataset: + for column in row: + var string_len : int = str(column).length() + last_string_len = string_len if string_len > last_string_len else last_string_len + var string : String = "" + for row_i in dataset.size(): + for column_i in dataset[row_i].size(): + string+="%*s" % [last_string_len+1, dataset[row_i][column_i]] + string+="\n" + string+="\n['{table_name}' : {rows} rows x {columns} columns]\n".format({ + "rows": datamatrix.rows(), + "columns": datamatrix.columns(), + "table_name": table_name}) + return string + +# ............................................................................... + +# Return a list of headers corresponding to a list of indexes +func get_headers_names(indexes : PoolIntArray) -> PoolStringArray: + var headers : PoolStringArray = [] + for index in indexes: + headers.append(dataset[0][index]) + return headers + +# Returns the index of an header +func get_column_index(header : String) -> int: + for headers_ix in range(dataset[0].size()): + if dataset[0][headers_ix] == header: + return headers_ix + return -1 + +# Get a column by its header +func get_column(header : String) -> Array: + var headers_i : int = get_column_index(header) + if headers_i!=-1: + return datamatrix.get_column(headers_i) + else: + return [] + +# Get a list of columns by their headers +func columns(headers : PoolStringArray) -> Matrix: + var values : Array = [] + for header in headers: + values.append(get_column(header)) + return MatrixGenerator.transpose(Matrix.new(values)) + + +# Get a column by its index +func get_icolumn(index : int) -> Array: + return datamatrix.get_column(index) + +# Get a list of columns by their indexes +func get_icolumns(indexes : PoolIntArray) -> Array: + var values : Array = [] + for index in indexes: + values.append(datamatrix.get_column(index)) + return values + +# Returns the list of labels corresponding to the list of indexes +func get_labels_names(indexes : PoolIntArray) -> PoolStringArray: + var headers : PoolStringArray = [] + for index in indexes: + headers.append(dataset[index][0]) + return headers + +# Returns the index of a label +func get_row_index(label : String) -> int: + for row in dataset.size(): + if dataset[row][0] == label: + return row + return -1 + +# Get a row by its label +func get_row(label : String) -> Array: + var index : int = get_row_index(label) + if index == -1 : + return [] + else: + return datamatrix.get_row(index) + +# Get a list of rows by their labels +func rows(labels : Array) -> Matrix: + var values : Array = [] + for label in labels: + values.append(get_row(label)) + return Matrix.new(values) + +# Get a row by its index +func get_irow(index : int) -> Array: + return datamatrix.get_row(index) + +# Get a list of rows by their indexes +func get_irows(indexes : PoolIntArray) -> Array: + var values : Array = [] + for index in indexes: + values.append(datamatrix.get_row(index)) + return values + +# Returns a a group of rows or a group of columns, using indexes or names +# dataset["0;5"] ---> Returns an array containing all rows from the 1st to the 4th +# dataset["0:5"] ---> Returns an array containing all columns from the 1st to the 4th +# dataset["label0;label5"] ---> Returns an array containing all row from the one with label == "label0" to the one with label == "label5" +# dataset["header0:header0"] ---> Returns an array containing all columns from the one with label == "label0" to the one with label == "label5" +func _get(_property : StringName): + var propertystr : String = _property + # ":" --> Columns + if ":" in propertystr: + var property : PoolStringArray = propertystr.split(":") + if (property[0]).is_valid_integer(): + if property[1] == "*": + return get_icolumns(range(property[0] as int, headers.size()-1)) + else: + return get_icolumns(range(property[0] as int, property[1] as int +1)) + else: + if property[1] == "*": + return get_icolumns(range(get_column_index(property[0]), headers.size()-1)) + else: + return get_icolumns(range(get_column_index(property[0]), get_column_index(property[1]))) + # ";" --> Rows + elif ";" in propertystr: + var property : PoolStringArray = propertystr.split(";") + if (property[0]).is_valid_integer(): + return get_irows(range(property[0] as int, property[1] as int + 1 )) + else: + return get_irows(range(get_row_index(property[0]), get_row_index(property[1]))) + elif "," in propertystr: + var property : PoolStringArray = propertystr.split(",") + else: + if (propertystr as String).is_valid_integer(): + return get_icolumn(propertystr as int) + else: + return get_column(propertystr) diff --git a/game/addons/easy_charts/utilities/classes/structures/matrix.gd b/game/addons/easy_charts/utilities/classes/structures/matrix.gd new file mode 100644 index 0000000..311ad8e --- /dev/null +++ b/game/addons/easy_charts/utilities/classes/structures/matrix.gd @@ -0,0 +1,175 @@ +tool +extends Resource +class_name Matrix + +var values : Array = [] + +func _init(matrix : Array = [], size : int = 0) -> void: + values = matrix + +func insert_row(row : Array, index : int = values.size()) -> void: + if rows() != 0: + assert(row.size() == columns(), "the row size must match matrix row size") + values.insert(index, row) + +func update_row(row : Array, index : int) -> void: + assert(rows() > index, "the row size must match matrix row size") + values[index] = row + +func remove_row(index: int) -> void: + assert(rows() > index, "the row size must match matrix row size") + values.remove(index) + +func insert_column(column : Array, index : int = values[0].size()) -> void: + if columns() != 0: + assert(column.size() == rows(), "the column size must match matrix column size") + for row_idx in column.size(): + values[row_idx].insert(index, column[row_idx]) + +func update_column(column : Array, index : int) -> void: + assert(columns() > index, "the column size must match matrix column size") + for row_idx in column.size(): + values[row_idx][index] = column[row_idx] + +func remove_column(index: int) -> void: + assert(columns() > index, "the column index must be at least equals to the rows count") + for row in get_rows(): + row.remove(index) + +func resize(rows: int, columns: int) -> void: + for row in range(rows): + var row_column: Array = [] + row_column.resize(columns) + values.append(row_column) + +func to_array() -> Array: + return values.duplicate(true) + +func get_size() -> Vector2: + return Vector2(rows(), columns()) + +func rows() -> int: + return values.size() + +func columns() -> int: + return values[0].size() if rows() != 0 else 0 + +func value(row: int, column: int) -> float: + return values[row][column] + +func set_value(value: float, row: int, column: int) -> void: + values[row][column] = value + +func get_column(column : int) -> Array: + assert(column < columns(), "index of the column requested (%s) exceedes matrix columns (%s)"%[column, columns()]) + var column_array : Array = [] + for row in values: + column_array.append(row[column]) + return column_array + +func get_columns(from : int = 0, to : int = columns()-1) -> Array: + var values : Array = [] + for column in range(from, to): + values.append(get_column(column)) + return values +# return MatrixGenerator.from_array(values) + +func get_row(row : int) -> Array: + assert(row < rows(), "index of the row requested (%s) exceedes matrix rows (%s)"%[row, rows()]) + return values[row] + +func get_rows(from : int = 0, to : int = rows()-1) -> Array: + return values.slice(from, to) +# return MatrixGenerator.from_array(values) + +func is_empty() -> bool: + return rows() == 0 and columns() == 0 + + +func is_square() -> bool: + return columns() == rows() + + +func is_diagonal() -> bool: + if not is_square(): + return false + + for i in rows(): + for j in columns(): + if i != j and values[i][j] != 0: + return false + + return true + + +func is_upper_triangular() -> bool: + if not is_square(): + return false + + for i in rows(): + for j in columns(): + if i > j and values[i][j] != 0: + return false + + return true + + +func is_lower_triangular() -> bool: + if not is_square(): + return false + + for i in rows(): + for j in columns(): + if i < j and values[i][j] != 0: + return false + + return true + + +func is_triangular() -> bool: + return is_upper_triangular() or is_lower_triangular() + + +func is_identity() -> bool: + if not is_diagonal(): + return false + + for i in rows(): + if values[i][i] != 1: + return false + + return true + +func _to_string() -> String: + var last_string_len : int + for row in values: + for column in row: + var string_len : int = str(column).length() + last_string_len = string_len if string_len > last_string_len else last_string_len + var string : String = "\n" + for row_i in values.size(): + for column_i in values[row_i].size(): + string+="%*s" % [last_string_len+1 if column_i!=0 else last_string_len, values[row_i][column_i]] + string+="\n" + return string + +# ---- +func set(position: String, value) -> void: + var t_pos: Array = position.split(",") + values[t_pos[0]][t_pos[1]] = value + +# -------------- +func _get(_property : StringName): + var propertystr : String = _property + # ":" --> Columns + if ":" in propertystr: + var property : PoolStringArray = propertystr.split(":") + var from : PoolStringArray = property[0].split(",") + var to : PoolStringArray = property[1].split(",") + elif "," in propertystr: + var property : PoolStringArray = propertystr.split(",") + if property.size() == 2: + return get_row(property[0] as int)[property[1] as int] + else: + if propertystr.is_valid_integer(): + return get_row(propertystr as int) diff --git a/game/addons/easy_charts/utilities/classes/structures/matrix_generator.gd b/game/addons/easy_charts/utilities/classes/structures/matrix_generator.gd new file mode 100644 index 0000000..f86805a --- /dev/null +++ b/game/addons/easy_charts/utilities/classes/structures/matrix_generator.gd @@ -0,0 +1,160 @@ +tool +extends Reference +class_name MatrixGenerator + +static func zeros(rows: int, columns: int) -> Matrix: + var zeros: Array = [] + var t_rows: Array = [] + t_rows.resize(columns) + t_rows.fill(0.0) + for row in rows: + zeros.append(t_rows.duplicate()) + return Matrix.new(zeros) + +# Generates a Matrix with random values between [from; to] with a given @size (rows, columns) +static func random_float_range(from : float, to : float, size : Vector2, _seed : int = 1234) -> Matrix: + seed(_seed) + randomize() + var array : Array = [] + for row in range(size.x): + var matrix_row : Array = [] + for column in range(size.y): matrix_row.append(rand_range(from,to)) + array.append(matrix_row) + return Matrix.new(array) + +# Generates a Matrix giving an Array (Array must by Array[Array]) +static func from_array(array : Array = []) -> Matrix: + var matrix : Array = [] + matrix.append(array) + return Matrix.new(matrix) + +# Generates a sub-Matrix giving a Matrix, a @from Array [row_i, column_i] and a @to Array [row_j, column_j] +static func sub_matrix(_matrix : Matrix, from : PoolIntArray, to : PoolIntArray) -> Matrix: + assert( not (to[0] > _matrix.rows() or to[1] > _matrix.columns()), + "%s is not an acceptable size for the submatrix, giving a matrix of size %s"%[to, _matrix.get_size()]) + var array : Array = [] + var rows : Array = _matrix.get_rows(from[0], to[0]) + for row in rows: + array.append(row.slice(from[1], to[1])) + return Matrix.new(array) + +# Duplicates a given Matrix +static func duplicate(_matrix : Matrix) -> Matrix: + return Matrix.new(_matrix.to_array().duplicate()) + +# Calculate the determinant of a matrix +static func determinant(matrix: Matrix) -> float: + assert(matrix.is_square(), "Expected square matrix") + + var determinant: float = 0.0 + + if matrix.rows() == 2 : + determinant = (matrix.value(0, 0) * matrix.value(1, 1)) - (matrix.value(0, 1) * matrix.value(1, 0)) + elif matrix.is_diagonal() or matrix.is_triangular() : + for i in matrix.rows(): + determinant *= matrix.value(i, i) + elif matrix.is_identity() : + determinant = 1.0 + else: + # Laplace expansion + var multiplier: float = -1.0 + var submatrix: Matrix = sub_matrix(matrix, [1, 0], [matrix.rows(), matrix.columns()]) + for j in matrix.columns() : + var cofactor: Matrix = copy(submatrix) + cofactor.remove_column(j) + multiplier *= -1.0 + determinant += multiplier * matrix.value(0, j) * determinant(cofactor) + + return determinant + + +# Calculate the inverse of a Matrix +static func inverse(matrix: Matrix) -> Matrix: + var inverse: Matrix + + # Minors and Cofactors + var minors_cofactors: Matrix = zeros(matrix.rows(), matrix.columns()) + var multiplier: float = -1.0 + + for i in minors_cofactors.rows(): + for j in minors_cofactors.columns(): + var t_minor: Matrix = copy(matrix) + t_minor.remove_row(i) + t_minor.remove_column(j) + multiplier *= -1.0 + minors_cofactors.set_value(multiplier * determinant(t_minor), i, j) + + var transpose: Matrix = transpose(minors_cofactors) + var determinant: float = determinant(matrix) + + inverse = multiply_float(transpose, 1 / determinant) + + return inverse + +# Transpose a given Matrix +static func transpose(_matrix : Matrix) -> Matrix: + var array : Array = [] + array.resize(_matrix.get_size().y) + var row : Array = [] + row.resize(_matrix.get_size().x) + for x in array.size(): + array[x] = row.duplicate() + for i in range(_matrix.get_size().x): + for j in range(_matrix.get_size().y): + array[j][i] = (_matrix.to_array()[i][j]) + return Matrix.new(array) + +# Calculates the dot product (A*B) matrix between two Matrixes +static func dot(_matrix1 : Matrix, _matrix2 : Matrix) -> Matrix: + if _matrix1.get_size().y != _matrix2.get_size().x: + printerr("matrix1 number of columns: %s must be the same as matrix2 number of rows: %s"%[_matrix1.get_size().y, _matrix2.get_size().x]) + return Matrix.new() + var array : Array = [] + for x in range(_matrix1.get_size().x): + var row : Array = [] + for y in range(_matrix2.get_size().y): + var sum : float + for k in range(_matrix1.get_size().y): + sum += (_matrix1.to_array()[x][k]*_matrix2.to_array()[k][y]) + row.append(sum) + array.append(row) + return Matrix.new(array) + +# Calculates the hadamard (element-wise product) between two Matrixes +static func hadamard(_matrix1 : Matrix, _matrix2 : Matrix) -> Matrix: + if _matrix1.get_size() != _matrix2.get_size(): + printerr("matrix1 size: %s must be the same as matrix2 size: %s"%[_matrix1.get_size(), _matrix2.get_size()]) + return Matrix.new() + var array : Array = [] + for x in range(_matrix1.to_array().size()): + var row : Array = [] + for y in range(_matrix1.to_array()[x].size()): + assert(typeof(_matrix1.to_array()[x][y]) != TYPE_STRING and typeof(_matrix2.to_array()[x][y]) != TYPE_STRING, "can't apply operations over a Matrix of Strings") + row.append(_matrix1.to_array()[x][y] * _matrix2.to_array()[x][y]) + array.append(row) + return Matrix.new(array) + +# Multiply a given Matrix for an int value +static func multiply_int(_matrix1 : Matrix, _int : int) -> Matrix: + var array : Array = _matrix1.to_array().duplicate() + for x in range(_matrix1.to_array().size()): + for y in range(_matrix1.to_array()[x].size()): + array[x][y]*=_int + array[x][y] = int(array[x][y]) + return Matrix.new(array) + +# Multiply a given Matrix for a float value +static func multiply_float(_matrix1 : Matrix, _float : float) -> Matrix: + var array : Array = _matrix1.to_array().duplicate() + for x in range(_matrix1.to_array().size()): + for y in range(_matrix1.to_array()[x].size()): + array[x][y]*=_float + return Matrix.new(array) + + +static func copy(matrix: Matrix) -> Matrix: + return Matrix.new(matrix.values.duplicate(true)) + +# ------------------------------------------------------------ +static func get_letter_index(index : int) -> String: + return "A B C D E F G H I J K L M N O P Q R S T U V W X Y Z".split(" ")[index] diff --git a/game/addons/easy_charts/utilities/classes/structures/pair.gd b/game/addons/easy_charts/utilities/classes/structures/pair.gd new file mode 100644 index 0000000..063e87d --- /dev/null +++ b/game/addons/easy_charts/utilities/classes/structures/pair.gd @@ -0,0 +1,25 @@ +""" +A class representing a Pair (or Tuple) of values. +It is a lightweight class that can easily replace the improper and/or +unnecessary usage of a 2d Array (ex. `var arr: Array = [0.5, 0.6]`) +or of a Vector2 (ex. `var v2: Vector2 = Vector2(0.6, 0.8)`). +""" +extends Reference +class_name Pair + +var left +var right + +func _init(left = null, right = null) -> void: + self.left = left + self.right = right + +func _format(val) -> String: + var format: String = "%s" + match typeof(val): + TYPE_REAL: + "%.2f" + return format % val + +func _to_string() -> String: + return "[%s, %s]" % [_format(self.left), _format(self.right)] diff --git a/game/addons/easy_charts/utilities/components/rect/rect.gd b/game/addons/easy_charts/utilities/components/rect/rect.gd new file mode 100644 index 0000000..e04c27c --- /dev/null +++ b/game/addons/easy_charts/utilities/components/rect/rect.gd @@ -0,0 +1,66 @@ +extends Control + +var OFFSET : Vector2 = Vector2() +var point_value : Array +var point_position : Vector2 +var color : Color +var color_outline : Color +var function : String + +var mouse_entered : bool = false + + +signal _mouse_entered() +signal _mouse_exited() + +# Called when the node enters the scene tree for the first time. +func _ready(): + pass # Replace with function body. + + +func _draw(): + if mouse_entered: + draw_rect(Rect2(rect_position - OFFSET,rect_size),color_outline,true,12,true) + +func create_point(color : Color, color_outline : Color, position : Vector2, size : Vector2, value : Array, function : String): + + self.color = color + self.color_outline = color_outline + self.point_position = position + self.rect_position = point_position - OFFSET + self.rect_size = size + self.point_value = value + self.function = function + +func format_value(v : Array, format_x : bool, format_y : bool): + var x : String = str(v[0]) + var y : String = str(v[1]) + + if format_x: + x = format(v[1]) + if format_y: + y = format(v[1]) + + return [x,y] + +func format(n): + n = str(n) + var size = n.length() + var s + for i in range(size): + if((size - i) % 3 == 0 and i > 0): + s = str(s,",", n[i]) + else: + s = str(s,n[i]) + + return s.replace("Null","") + +func _on_Rect_mouse_entered(): + mouse_entered = true + emit_signal("_mouse_entered") + update() + +func _on_Rect_mouse_exited(): + mouse_entered = false + emit_signal("_mouse_exited") + update() diff --git a/game/addons/easy_charts/utilities/components/rect/rect.tscn b/game/addons/easy_charts/utilities/components/rect/rect.tscn new file mode 100644 index 0000000..60509e0 --- /dev/null +++ b/game/addons/easy_charts/utilities/components/rect/rect.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://addons/easy_charts/utilities/components/rect/rect.gd" type="Script" id=1] + +[node name="Rect" type="Control"] +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[connection signal="mouse_entered" from="." to="." method="_on_Rect_mouse_entered"] +[connection signal="mouse_exited" from="." to="." method="_on_Rect_mouse_exited"] diff --git a/game/addons/easy_charts/utilities/components/slice/slice.gd b/game/addons/easy_charts/utilities/components/slice/slice.gd new file mode 100644 index 0000000..6dab277 --- /dev/null +++ b/game/addons/easy_charts/utilities/components/slice/slice.gd @@ -0,0 +1,17 @@ +extends Reference +class_name Slice + +var x_value : String +var y_value : String +var from_angle : float +var to_angle : float +var function : String +var color : Color + +func _init(x : String, y : String, from : float, to : float, fun : String, col : Color): + x_value = x + y_value = y + from_angle = from + to_angle = to + function = fun + color = col diff --git a/game/addons/easy_charts/utilities/containers/data_tooltip/data_tooltip.gd b/game/addons/easy_charts/utilities/containers/data_tooltip/data_tooltip.gd new file mode 100644 index 0000000..9b5b858 --- /dev/null +++ b/game/addons/easy_charts/utilities/containers/data_tooltip/data_tooltip.gd @@ -0,0 +1,36 @@ +tool +extends PanelContainer +class_name DataTooltip + +var value : String = "" +var position : Vector2 = Vector2() + +onready var x_lbl : Label = $PointData/x +onready var y_lbl : Label = $PointData/Value/y +onready var func_lbl : Label = $PointData/Value/Function +onready var color_rect: Panel = $PointData/Value/Color + +func _ready(): + hide() + update_size() + +func _process(delta): + if Engine.editor_hint: + return + rect_position = get_global_mouse_position() + Vector2(15, - (get_rect().size.y / 2)) + +func update_values(x: String, y: String, function: String, color: Color): + x_lbl.set_text(x) + y_lbl.set_text(y) + func_lbl.set_text(function) + color_rect.get("custom_styles/panel").set("bg_color", color) + +func update_size(): + x_lbl.set_text("") + y_lbl.set_text("") + func_lbl.set_text("") + rect_size = Vector2.ZERO + +func _on_DataTooltip_visibility_changed(): + if not visible: + update_size() diff --git a/game/addons/easy_charts/utilities/containers/data_tooltip/data_tooltip.tscn b/game/addons/easy_charts/utilities/containers/data_tooltip/data_tooltip.tscn new file mode 100644 index 0000000..d1862fe --- /dev/null +++ b/game/addons/easy_charts/utilities/containers/data_tooltip/data_tooltip.tscn @@ -0,0 +1,116 @@ +[gd_scene load_steps=5 format=2] + +[ext_resource path="res://addons/easy_charts/utilities/containers/data_tooltip/data_tooltip.gd" type="Script" id=1] + +[sub_resource type="StyleBoxFlat" id=1] +content_margin_left = 10.0 +content_margin_right = 10.0 +content_margin_top = 8.0 +content_margin_bottom = 8.0 +bg_color = Color( 0.101961, 0.101961, 0.101961, 0.784314 ) +border_color = Color( 1, 1, 1, 1 ) +corner_radius_top_left = 5 +corner_radius_top_right = 5 +corner_radius_bottom_right = 5 +corner_radius_bottom_left = 5 +corner_detail = 20 +anti_aliasing_size = 0.9 + +[sub_resource type="StyleBoxFlat" id=2] +corner_radius_top_left = 5 +corner_radius_top_right = 5 +corner_radius_bottom_right = 5 +corner_radius_bottom_left = 5 +corner_detail = 20 +anti_aliasing_size = 0.7 + +[sub_resource type="StyleBoxEmpty" id=3] + +[node name="DataTooltip" type="PanelContainer"] +margin_right = 169.0 +margin_bottom = 49.0 +mouse_filter = 2 +custom_styles/panel = SubResource( 1 ) +script = ExtResource( 1 ) + +[node name="PointData" type="VBoxContainer" parent="."] +margin_left = 10.0 +margin_top = 8.0 +margin_right = 159.0 +margin_bottom = 42.0 +grow_horizontal = 2 +size_flags_horizontal = 3 +custom_constants/separation = 6 +alignment = 1 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="x" type="Label" parent="PointData"] +margin_right = 55.0 +margin_bottom = 14.0 +size_flags_horizontal = 0 +custom_colors/font_color = Color( 1, 1, 1, 1 ) +text = "{x value}" +valign = 1 + +[node name="Value" type="HBoxContainer" parent="PointData"] +margin_top = 20.0 +margin_right = 149.0 +margin_bottom = 34.0 +grow_horizontal = 2 +size_flags_horizontal = 7 +custom_constants/separation = 1 + +[node name="Color" type="Panel" parent="PointData/Value"] +margin_top = 2.0 +margin_right = 10.0 +margin_bottom = 12.0 +rect_min_size = Vector2( 10, 10 ) +size_flags_horizontal = 4 +size_flags_vertical = 4 +custom_styles/panel = SubResource( 2 ) + +[node name="VSeparator" type="VSeparator" parent="PointData/Value"] +margin_left = 11.0 +margin_right = 15.0 +margin_bottom = 14.0 +custom_constants/separation = 4 +custom_styles/separator = SubResource( 3 ) + +[node name="Function" type="Label" parent="PointData/Value"] +margin_left = 16.0 +margin_right = 78.0 +margin_bottom = 14.0 +size_flags_horizontal = 0 +size_flags_vertical = 5 +text = "{function}" +valign = 1 + +[node name="sep" type="Label" parent="PointData/Value"] +margin_left = 79.0 +margin_right = 83.0 +margin_bottom = 14.0 +size_flags_horizontal = 0 +size_flags_vertical = 5 +text = ":" +valign = 1 + +[node name="VSeparator2" type="VSeparator" parent="PointData/Value"] +margin_left = 84.0 +margin_right = 88.0 +margin_bottom = 14.0 +custom_constants/separation = 4 +custom_styles/separator = SubResource( 3 ) + +[node name="y" type="Label" parent="PointData/Value"] +margin_left = 89.0 +margin_right = 144.0 +margin_bottom = 14.0 +size_flags_horizontal = 0 +size_flags_vertical = 5 +custom_colors/font_color = Color( 1, 1, 1, 1 ) +text = "{y value}" +valign = 1 + +[connection signal="visibility_changed" from="." to="." method="_on_DataTooltip_visibility_changed"] diff --git a/game/addons/easy_charts/utilities/containers/legend/function_legend.gd b/game/addons/easy_charts/utilities/containers/legend/function_legend.gd new file mode 100644 index 0000000..24596d5 --- /dev/null +++ b/game/addons/easy_charts/utilities/containers/legend/function_legend.gd @@ -0,0 +1,41 @@ +tool +extends VBoxContainer +class_name LegendElement + +onready var Function : Label = $Function +onready var FunctionColor : ColorRect = $Color + +var function : String setget set_function, get_function +var color : Color setget set_function_color, get_function_color +var font_color : Color +var font : Font + +func _ready(): + Function.set("custom_fonts/font",font) + Function.set("custom_colors/font_color",font_color) + Function.set_text(function) + FunctionColor.set_frame_color(color) + +func create_legend(text : String, color : Color, font : Font, font_color : Color): + self.function = text + self.color = color + self.font_color = font_color + self.font = font + +func set_function( t : String ): + function = t + +func get_function() -> String: + return function + +func set_function_color( c : Color ): + color = c + +func get_function_color() -> Color: + return color + +func get_class() -> String: + return "Legend Element" + +func _to_string() -> String: + return "%s (%s, %s) " % [get_class(), get_function(), get_function_color().to_html(true)] diff --git a/game/addons/easy_charts/utilities/containers/legend/function_legend.tscn b/game/addons/easy_charts/utilities/containers/legend/function_legend.tscn new file mode 100644 index 0000000..8ee6c53 --- /dev/null +++ b/game/addons/easy_charts/utilities/containers/legend/function_legend.tscn @@ -0,0 +1,27 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://addons/easy_charts/utilities/containers/legend/function_legend.gd" type="Script" id=1] + +[node name="FunctionLegend" type="VBoxContainer"] +margin_right = 80.0 +margin_bottom = 26.0 +alignment = 1 +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Function" type="Label" parent="."] +margin_top = 2.0 +margin_right = 80.0 +margin_bottom = 16.0 +custom_colors/font_color = Color( 0, 0, 0, 1 ) +align = 1 +valign = 1 + +[node name="Color" type="ColorRect" parent="."] +margin_top = 20.0 +margin_right = 80.0 +margin_bottom = 23.0 +rect_min_size = Vector2( 15, 3 ) +color = Color( 0, 0, 0, 1 ) diff --git a/game/addons/easy_charts/utilities/icons/linechart.svg b/game/addons/easy_charts/utilities/icons/linechart.svg new file mode 100644 index 0000000..7d168d9 --- /dev/null +++ b/game/addons/easy_charts/utilities/icons/linechart.svg @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/game/addons/easy_charts/utilities/icons/linechart.svg.import b/game/addons/easy_charts/utilities/icons/linechart.svg.import new file mode 100644 index 0000000..4452b09 --- /dev/null +++ b/game/addons/easy_charts/utilities/icons/linechart.svg.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/linechart.svg-922834f0462a2c88be644081c47c63ad.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/easy_charts/utilities/icons/linechart.svg" +dest_files=[ "res://.import/linechart.svg-922834f0462a2c88be644081c47c63ad.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/game/addons/easy_charts/utilities/icons/linechart2d.svg b/game/addons/easy_charts/utilities/icons/linechart2d.svg new file mode 100644 index 0000000..46729be --- /dev/null +++ b/game/addons/easy_charts/utilities/icons/linechart2d.svg @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/game/addons/easy_charts/utilities/icons/linechart2d.svg.import b/game/addons/easy_charts/utilities/icons/linechart2d.svg.import new file mode 100644 index 0000000..01a536c --- /dev/null +++ b/game/addons/easy_charts/utilities/icons/linechart2d.svg.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/linechart2d.svg-1067b05eddcc451c2fc80a8734aa8056.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/easy_charts/utilities/icons/linechart2d.svg" +dest_files=[ "res://.import/linechart2d.svg-1067b05eddcc451c2fc80a8734aa8056.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/game/addons/easy_charts/utilities/scripts/ec_utilities.gd b/game/addons/easy_charts/utilities/scripts/ec_utilities.gd new file mode 100644 index 0000000..8a44c69 --- /dev/null +++ b/game/addons/easy_charts/utilities/scripts/ec_utilities.gd @@ -0,0 +1,46 @@ +tool +extends Node + +var alphabet : String = "A B C D E F G H I J K L M N O P Q R S T U V W X Y Z" + +func _ready(): + pass +# templates = _load_templates() + +func _print_message(message : String, type : int = 0): + pass +# match type: +# 0: +# print("[%s] => %s" % [plugin_name, message]) +# 1: +# printerr("ERROR: [%s] => %s" % [plugin_name, message]) + +func _load_templates() -> Dictionary: + pass + return {} +# var template_file : File = File.new() +# template_file.open("res://addons/easy_charts/templates.json",File.READ) +# var templates = JSON.parse(template_file.get_as_text()).get_result() +# template_file.close() +# return templates + +func get_template(template_index : int): + pass +# return templates.get(templates.keys()[template_index]) + +func get_chart_type(chart_type : int) -> Array: + pass + return [] +# return chart_types.get(chart_types.keys()[chart_type]) + +func get_letter_index(index : int) -> String: + return alphabet.split(" ")[index] + +func save_dataframe_to_file(dataframe: DataFrame, path: String, delimiter: String = ";") -> void: + pass +# var file = File.new() +# file.open(path, File.WRITE) +# for row in dataframe.get_dataset(): +# file.store_line(PoolStringArray(row).join(delimiter)) +# file.close() + diff --git a/game/project.pandemonium b/game/project.pandemonium index 802b4d5..91c362a 100644 --- a/game/project.pandemonium +++ b/game/project.pandemonium @@ -8,12 +8,120 @@ config_version=4 +_global_script_classes=[ { +"base": "Reference", +"class": @"ArrayOperations", +"language": @"GDScript", +"path": "res://addons/easy_charts/utilities/classes/structures/array_operations.gd" +}, { +"base": "Reference", +"class": @"Bar", +"language": @"GDScript", +"path": "res://addons/easy_charts/utilities/classes/plotting/bar.gd" +}, { +"base": "Chart", +"class": @"BarChart", +"language": @"GDScript", +"path": "res://addons/easy_charts/control_charts/BarChart/bar_chart.gd" +}, { +"base": "Control", +"class": @"Chart", +"language": @"GDScript", +"path": "res://addons/easy_charts/control_charts/chart.gd" +}, { +"base": "Reference", +"class": @"ChartProperties", +"language": @"GDScript", +"path": "res://addons/easy_charts/utilities/classes/plotting/chart_properties.gd" +}, { +"base": "Resource", +"class": @"DataFrame", +"language": @"GDScript", +"path": "res://addons/easy_charts/utilities/classes/structures/data_frame.gd" +}, { +"base": "PanelContainer", +"class": @"DataTooltip", +"language": @"GDScript", +"path": "res://addons/easy_charts/utilities/containers/data_tooltip/data_tooltip.gd" +}, { +"base": "VBoxContainer", +"class": @"LegendElement", +"language": @"GDScript", +"path": "res://addons/easy_charts/utilities/containers/legend/function_legend.gd" +}, { +"base": "ScatterChart", +"class": @"LineChart", +"language": @"GDScript", +"path": "res://addons/easy_charts/control_charts/LineChart/line_chart.gd" +}, { +"base": "Resource", +"class": @"Matrix", +"language": @"GDScript", +"path": "res://addons/easy_charts/utilities/classes/structures/matrix.gd" +}, { +"base": "Reference", +"class": @"MatrixGenerator", +"language": @"GDScript", +"path": "res://addons/easy_charts/utilities/classes/structures/matrix_generator.gd" +}, { +"base": "Reference", +"class": @"Pair", +"language": @"GDScript", +"path": "res://addons/easy_charts/utilities/classes/structures/pair.gd" +}, { +"base": "Reference", +"class": @"Point", +"language": @"GDScript", +"path": "res://addons/easy_charts/utilities/classes/plotting/point.gd" +}, { +"base": "Reference", +"class": @"SampledAxis", +"language": @"GDScript", +"path": "res://addons/easy_charts/utilities/classes/plotting/sampled_axis.gd" +}, { +"base": "Chart", +"class": @"ScatterChart", +"language": @"GDScript", +"path": "res://addons/easy_charts/control_charts/ScatterChart/scatter_chart.gd" +}, { +"base": "Reference", +"class": @"Slice", +"language": @"GDScript", +"path": "res://addons/easy_charts/utilities/components/slice/slice.gd" +} ] +_global_script_class_icons={ +@"Chart": "", +@"BarChart": "", +@"LineChart": "", +@"ScatterChart": "", +@"Bar": "", +@"ChartProperties": "", +@"Point": "", +@"SampledAxis": "", +@"ArrayOperations": "", +@"DataFrame": "", +@"Matrix": "", +@"MatrixGenerator": "", +@"Pair": "", +@"Slice": "", +@"DataTooltip": "", +@"LegendElement": "" +} + [application] config/name="PMLPP Sample" run/main_scene="res://Main.tscn" config/icon="res://icon.png" +[autoload] + +ECUtilities="*res://addons/easy_charts/utilities/scripts/ec_utilities.gd" + +[editor_plugins] + +enabled=PoolStringArray( "res://addons/easy_charts/plugin.cfg" ) + [physics] common/enable_pause_aware_picking=true