From 952078279a2c66cecb9c959cdb133a6a4154da53 Mon Sep 17 00:00:00 2001 From: Relintai Date: Sat, 28 Jan 2023 12:55:40 +0100 Subject: [PATCH] Added the easy charts addon. --- .../control_charts/BarChart/bar_chart.gd | 184 +++++++ .../control_charts/BarChart/bar_chart.tscn | 14 + .../control_charts/LineChart/line_chart.gd | 46 ++ .../control_charts/LineChart/line_chart.tscn | 7 + .../ScatterChart/scatter_chart.gd | 124 +++++ .../ScatterChart/scatter_chart.tscn | 7 + .../easy_charts/control_charts/chart.gd | 503 ++++++++++++++++++ .../easy_charts/control_charts/chart.tscn | 30 ++ .../easy_charts/examples/bar_chart/Control.gd | 50 ++ .../examples/bar_chart/Control.tscn | 49 ++ .../examples/line_chart/Control.gd | 55 ++ .../examples/line_chart/Control.tscn | 49 ++ .../examples/scatter_chart/Control.gd | 50 ++ .../examples/scatter_chart/Control.tscn | 49 ++ game/addons/easy_charts/icon.png | Bin 0 -> 69813 bytes game/addons/easy_charts/icon.png.import | 35 ++ game/addons/easy_charts/plugin.cfg | 7 + game/addons/easy_charts/plugin.gd | 9 + game/addons/easy_charts/templates.json | 44 ++ .../utilities/classes/plotting/bar.gd | 12 + .../classes/plotting/chart_properties.gd | 43 ++ .../utilities/classes/plotting/point.gd | 20 + .../classes/plotting/sampled_axis.gd | 13 + .../classes/structures/array_operations.gd | 56 ++ .../classes/structures/data_frame.gd | 191 +++++++ .../utilities/classes/structures/matrix.gd | 175 ++++++ .../classes/structures/matrix_generator.gd | 160 ++++++ .../utilities/classes/structures/pair.gd | 25 + .../utilities/components/rect/rect.gd | 66 +++ .../utilities/components/rect/rect.tscn | 12 + .../utilities/components/slice/slice.gd | 17 + .../containers/data_tooltip/data_tooltip.gd | 36 ++ .../containers/data_tooltip/data_tooltip.tscn | 116 ++++ .../containers/legend/function_legend.gd | 41 ++ .../containers/legend/function_legend.tscn | 27 + .../easy_charts/utilities/icons/linechart.svg | 155 ++++++ .../utilities/icons/linechart.svg.import | 35 ++ .../utilities/icons/linechart2d.svg | 155 ++++++ .../utilities/icons/linechart2d.svg.import | 35 ++ .../utilities/scripts/ec_utilities.gd | 46 ++ game/project.pandemonium | 108 ++++ 41 files changed, 2856 insertions(+) create mode 100644 game/addons/easy_charts/control_charts/BarChart/bar_chart.gd create mode 100644 game/addons/easy_charts/control_charts/BarChart/bar_chart.tscn create mode 100644 game/addons/easy_charts/control_charts/LineChart/line_chart.gd create mode 100644 game/addons/easy_charts/control_charts/LineChart/line_chart.tscn create mode 100644 game/addons/easy_charts/control_charts/ScatterChart/scatter_chart.gd create mode 100644 game/addons/easy_charts/control_charts/ScatterChart/scatter_chart.tscn create mode 100644 game/addons/easy_charts/control_charts/chart.gd create mode 100644 game/addons/easy_charts/control_charts/chart.tscn create mode 100644 game/addons/easy_charts/examples/bar_chart/Control.gd create mode 100644 game/addons/easy_charts/examples/bar_chart/Control.tscn create mode 100644 game/addons/easy_charts/examples/line_chart/Control.gd create mode 100644 game/addons/easy_charts/examples/line_chart/Control.tscn create mode 100644 game/addons/easy_charts/examples/scatter_chart/Control.gd create mode 100644 game/addons/easy_charts/examples/scatter_chart/Control.tscn create mode 100644 game/addons/easy_charts/icon.png create mode 100644 game/addons/easy_charts/icon.png.import create mode 100644 game/addons/easy_charts/plugin.cfg create mode 100644 game/addons/easy_charts/plugin.gd create mode 100644 game/addons/easy_charts/templates.json create mode 100644 game/addons/easy_charts/utilities/classes/plotting/bar.gd create mode 100644 game/addons/easy_charts/utilities/classes/plotting/chart_properties.gd create mode 100644 game/addons/easy_charts/utilities/classes/plotting/point.gd create mode 100644 game/addons/easy_charts/utilities/classes/plotting/sampled_axis.gd create mode 100644 game/addons/easy_charts/utilities/classes/structures/array_operations.gd create mode 100644 game/addons/easy_charts/utilities/classes/structures/data_frame.gd create mode 100644 game/addons/easy_charts/utilities/classes/structures/matrix.gd create mode 100644 game/addons/easy_charts/utilities/classes/structures/matrix_generator.gd create mode 100644 game/addons/easy_charts/utilities/classes/structures/pair.gd create mode 100644 game/addons/easy_charts/utilities/components/rect/rect.gd create mode 100644 game/addons/easy_charts/utilities/components/rect/rect.tscn create mode 100644 game/addons/easy_charts/utilities/components/slice/slice.gd create mode 100644 game/addons/easy_charts/utilities/containers/data_tooltip/data_tooltip.gd create mode 100644 game/addons/easy_charts/utilities/containers/data_tooltip/data_tooltip.tscn create mode 100644 game/addons/easy_charts/utilities/containers/legend/function_legend.gd create mode 100644 game/addons/easy_charts/utilities/containers/legend/function_legend.tscn create mode 100644 game/addons/easy_charts/utilities/icons/linechart.svg create mode 100644 game/addons/easy_charts/utilities/icons/linechart.svg.import create mode 100644 game/addons/easy_charts/utilities/icons/linechart2d.svg create mode 100644 game/addons/easy_charts/utilities/icons/linechart2d.svg.import create mode 100644 game/addons/easy_charts/utilities/scripts/ec_utilities.gd 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 0000000000000000000000000000000000000000..c1c687201f814a62ff4d2d7165ce595bdc772a4c GIT binary patch literal 69813 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

Xe>N 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 O2UJ;8348Bx!2bZu)GsLj literal 0 HcmV?d00001 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