From 38225536d1be924124edc4f9a82c0bf5d2bf8ec6 Mon Sep 17 00:00:00 2001 From: fenix-hub Date: Wed, 11 Jan 2023 21:40:52 +0100 Subject: [PATCH] scatter chart freature compleat --- .../ScatterChart/scatter_chart.gd | 13 +- .../ScatterChart/scatter_chart.tscn | 5 +- addons/easy_charts/control_charts/chart.gd | 325 +++++++++++++----- .../classes/plotting/chart_properties.gd | 6 + .../classes/plotting/drawing_options.gd | 13 +- .../point_container/point_container.gd | 19 + 6 files changed, 294 insertions(+), 87 deletions(-) create mode 100644 addons/easy_charts/utilities/classes/plotting/chart_properties.gd diff --git a/addons/easy_charts/control_charts/ScatterChart/scatter_chart.gd b/addons/easy_charts/control_charts/ScatterChart/scatter_chart.gd index 3a8d8cf..d311a87 100644 --- a/addons/easy_charts/control_charts/ScatterChart/scatter_chart.gd +++ b/addons/easy_charts/control_charts/ScatterChart/scatter_chart.gd @@ -9,12 +9,15 @@ signal point_exited(point) func _ready(): pass -func plot(x: Array, y: Array) -> void: +func plot(x: Array, y: Array, drawing_options: DrawingOptions = null, chart_properties: ChartProperties = null) -> void: self.x = x self.y = y - _clear() - _pre_process() + if chart_properties != null: + self.chart_properties = chart_properties + if drawing_options != null: + self.drawing_options = drawing_options + update() func _draw_point(point: Point, function_index: int) -> void: @@ -22,8 +25,8 @@ func _draw_point(point: Point, function_index: int) -> void: $Points.add_child(point_container) point_container.set_point( point, - drawing_options.colors.functions[function_index], - drawing_options.shapes[function_index] + drawing_options.get_function_color(function_index), + drawing_options.get_point_shape(function_index) ) point_container.connect("point_entered", self, "_on_point_entered") point_container.connect("point_exited", self, "_on_point_exited") diff --git a/addons/easy_charts/control_charts/ScatterChart/scatter_chart.tscn b/addons/easy_charts/control_charts/ScatterChart/scatter_chart.tscn index 4875b35..d547b30 100644 --- a/addons/easy_charts/control_charts/ScatterChart/scatter_chart.tscn +++ b/addons/easy_charts/control_charts/ScatterChart/scatter_chart.tscn @@ -11,9 +11,12 @@ __meta__ = { } [node name="Points" type="Control" parent="."] -unique_name_in_owner = true anchor_right = 1.0 anchor_bottom = 1.0 __meta__ = { "_edit_use_anchors_": true } + +[node name="Canvas" type="Control" parent="."] +anchor_right = 1.0 +anchor_bottom = 1.0 diff --git a/addons/easy_charts/control_charts/chart.gd b/addons/easy_charts/control_charts/chart.gd index ec865a2..ee6af5f 100644 --- a/addons/easy_charts/control_charts/chart.gd +++ b/addons/easy_charts/control_charts/chart.gd @@ -4,17 +4,20 @@ class_name Chart var x: Array var y: Array -var x_sampled: SampledAxis -var y_sampled: SampledAxis +var x_min_max: Pair = Pair.new() +var y_min_max: Pair = Pair.new() + +var x_sampled: SampledAxis = SampledAxis.new() +var y_sampled: SampledAxis = SampledAxis.new() # var x_scale: float = 5.0 -var y_scale: float = 5.0 +var y_scale: float = 2.0 ###### STYLE var drawing_options: DrawingOptions = DrawingOptions.new() - +var chart_properties: ChartProperties = ChartProperties.new() #### INTERNAL # The bounding_box of the chart @@ -23,21 +26,74 @@ var bounding_box: Rect2 # The Reference Rectangle to plot samples # It is the @bounding_box Rectangle inverted on the Y axis -var ref_x: Pair -var ref_y: Pair -var ref_rect: Rect2 +var x_sampled_domain: Pair +var y_sampled_domain: Pair +var sampled_domain_rect: Rect2 -var _padding_offset: Vector2 = Vector2(70.0, 70.0) +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 + + var point_container_scene: PackedScene = preload("res://addons/easy_charts/utilities/containers/point_container/point_container.tscn") ########### -func plot(x: Array, y: Array) -> void: +func plot(x: Array, y: Array, drawing_options: DrawingOptions = DrawingOptions.new(), chart_properties: ChartProperties = ChartProperties.new()) -> void: pass -func _sample_values(values: Array, ref_values: Pair) -> SampledAxis: +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 abs(fmod(val, 1)) > 0.0: + return true + else: + for val in temp: + 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() @@ -55,65 +111,126 @@ func _sample_values(values: Array, ref_values: Pair) -> SampledAxis: var rels: Array = [] 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() - for t_dim in temp: var rels_t: Array = [] for val in t_dim: - rels_t.append(range_lerp(val, _min, _max, ref_values.left, ref_values.right)) + rels_t.append(_map_pair(val, rel_values, ref_values)) rels.append(rels_t) else: - _min = temp.min() - _max = temp.max() - for val in temp: - rels.append(range_lerp(val, _min, _max, ref_values.left, ref_values.right)) + rels.append(_map_pair(val, rel_values, ref_values)) - return SampledAxis.new(rels, Pair.new(_min, _max)) + return SampledAxis.new(rels, rel_values) -func _pre_process() -> void: + +func _pre_process_drawings() -> void: var t_gr: Rect2 = get_global_rect() - # node box + #### @node_box size, which is the whole "frame" node_box = Rect2(Vector2.ZERO, t_gr.size - t_gr.position) - # bounding_box + #### drawing size for defining @bounding_box + x_min_max = _find_min_max(x) + y_min_max = _find_min_max(y) + + #### calculating offset from the @node_box for the @bounding_box. + var offset: Vector2 = _padding_offset + + ### if @labels drawing is enabled, calcualte offsets + if drawing_options.labels: + ### labels (X, Y, Title) + _x_label_size = drawing_options.font.get_string_size(chart_properties.x_label) + _y_label_size = drawing_options.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 Y axis. + # remember that "-" sign adds additional pixels, and it is relative only to negative numbers! + var x_max_formatted: String = ("%.2f" if x_has_decimals else "%s") % x_min_max.right + _x_ticklabel_size = drawing_options.font.get_string_size(x_max_formatted) + + 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_min_max.right + if y_min_max.left < 0: + # negative number + var y_min_formatted: String = ("%.2f" if y_has_decimals else "%s") % y_min_max.left + if y_min_formatted.length() >= y_max_formatted.length(): + _y_ticklabel_size = drawing_options.font.get_string_size(y_min_formatted) + else: + _y_ticklabel_size = drawing_options.font.get_string_size(y_max_formatted) + else: + _y_ticklabel_size = drawing_options.font.get_string_size(y_max_formatted) + + offset.x += _y_label_offset + _y_label_size.y + _y_ticklabel_offset + _y_ticklabel_size.x + + ### if @ticks drawing is enabled, calculate offsets + if drawing_options.ticks: + offset.x += _y_tick_size + offset.y += _x_tick_size + + ### @bounding_box, where the points will be plotted bounding_box = Rect2( - Vector2.ZERO + _padding_offset, - t_gr.size - t_gr.position - (_padding_offset*2) + offset, + t_gr.size - (offset * 2) ) - # reference rectangle - ref_x = Pair.new(bounding_box.position.x + _internal_offset.x, bounding_box.position.x + bounding_box.size.x - _internal_offset.y) - ref_y = Pair.new(bounding_box.size.y + bounding_box.position.y - _internal_offset.x, bounding_box.position.y + _internal_offset.y) - ref_rect = Rect2( - Vector2(ref_x.left, ref_y.left), - Vector2(ref_x.right, ref_y.right) + ### @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(bounding_box.position.x + _internal_offset.x, bounding_box.position.x + bounding_box.size.x - _internal_offset.y) + y_sampled_domain = Pair.new(bounding_box.size.y + bounding_box.position.y - _internal_offset.x, bounding_box.position.y + _internal_offset.y) + sampled_domain_rect = Rect2( + Vector2(x_sampled_domain.left, y_sampled_domain.left), + Vector2(x_sampled_domain.right, y_sampled_domain.right) ) + +func _pre_process_sampling() -> void: # samples - x_sampled = _sample_values(x, ref_x) - y_sampled = _sample_values(y, ref_y) + x_sampled = _sample_values(x, x_min_max, x_sampled_domain) + y_sampled = _sample_values(y, y_min_max, y_sampled_domain) + +func _pre_process() -> void: + _pre_process_drawings() + _pre_process_sampling() + + + +func _draw_points() -> void: + pass func _draw_borders() -> void: draw_rect(node_box, Color.red, false, 1, true) func _draw_bounding_box() -> void: draw_rect(bounding_box, drawing_options.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_points() -> void: - pass +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_min_max, 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(drawing_options.font, Vector2(xorigin, yorigin) - Vector2(15, -15), "O", drawing_options.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_grid() -> void: var validation: int = _validate_sampled_axis(x_sampled, y_sampled) @@ -126,90 +243,142 @@ func _draw_grid() -> void: for _x in x_scale+1: var x_val: float = _x * v_lines + x_sampled.min_max.left var p1: Vector2 = Vector2( - range_lerp(x_val, x_sampled.min_max.left, x_sampled.min_max.right, ref_x.left, ref_x.right), + 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 p2: Vector2 = Vector2( - range_lerp(x_val, x_sampled.min_max.left, x_sampled.min_max.right, ref_x.left, ref_x.right), + 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 V labels + if drawing_options.labels: + var tick_lbl: String = ("%.2f" if x_has_decimals else "%s") % x_val + + draw_string( + drawing_options.font, + p2 + Vector2( + - drawing_options.font.get_string_size(tick_lbl).x / 2, + _x_label_size.y + _x_tick_size + ), + tick_lbl, + drawing_options.colors.bounding_box + ) + + # Draw V Ticks + if drawing_options.ticks: + draw_line(p2, p2 + Vector2(0, _x_tick_size), drawing_options.colors.bounding_box, 1, true) + # Draw V Grid Lines if drawing_options.grid: draw_line(p1, p2, drawing_options.colors.grid, 1, true) - - # Draw V Ticks - if drawing_options.ticks: - p1.y = p2.y - p2.y += 8.0 - draw_line(p1, p2, drawing_options.colors.bounding_box, 1, true) - - # Draw V labels - if drawing_options.labels: - draw_string( - drawing_options.font, - p2 + Vector2(-drawing_options.font.get_string_size(str(x_val)).x * 0.5, 15.0), - str(x_val), - drawing_options.colors.bounding_box - ) - + # draw horizontal lines var h_lines: float = (y_sampled.min_max.right - y_sampled.min_max.left) / y_scale for _y in y_scale+1: var y_val: float = _y * h_lines + y_sampled.min_max.left var p1: Vector2 = Vector2( bounding_box.position.x, - range_lerp(y_val, y_sampled.min_max.left, y_sampled.min_max.right, ref_y.left, ref_y.right) + range_lerp(y_val, y_sampled.min_max.left, y_sampled.min_max.right, y_sampled_domain.left, y_sampled_domain.right) ) var p2: Vector2 = Vector2( bounding_box.size.x + bounding_box.position.x, - range_lerp(y_val, y_sampled.min_max.left, y_sampled.min_max.right, ref_y.left, ref_y.right) + range_lerp(y_val, y_sampled.min_max.left, y_sampled.min_max.right, y_sampled_domain.left, y_sampled_domain.right) ) + # Draw H labels + if drawing_options.labels: + var tick_lbl: String = ("%.2f" if y_has_decimals else "%s") % y_val + var tick_lbl_size: Vector2 = drawing_options.font.get_string_size(tick_lbl) + + draw_string( + drawing_options.font, + p1 - Vector2(tick_lbl_size.x + _y_ticklabel_offset + _y_tick_size, - _y_ticklabel_size.y * 0.35), + tick_lbl, + drawing_options.colors.bounding_box + ) + + # Draw H Ticks + if drawing_options.ticks: + draw_line( + p1, + p1 - Vector2(_y_tick_size, 0), + drawing_options.colors.bounding_box, 1, true) + # Draw H Grid Lines if drawing_options.grid: draw_line(p1, p2, drawing_options.colors.grid, 1, true) - - # Draw H Ticks - if drawing_options.ticks: - p2.x = p1.x - 8.0 - draw_line(p1, p2, drawing_options.colors.bounding_box, 1, true) - - # Draw H labels - if drawing_options.labels: - draw_string( - drawing_options.font, - p2 - Vector2(drawing_options.font.get_string_size(str(y_val)).x + 5.0, -drawing_options.font.get_string_size(str(y_val)).y * 0.35), - str(y_val), - drawing_options.colors.bounding_box - ) +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", drawing_options.font) + lbl.set_text(text) + lbl.modulate = drawing_options.colors.bounding_box + lbl.rect_rotation = rotation + lbl.rect_position = position + return lbl + +func _draw_yaxis_label() -> void: + _create_canvas_label( + chart_properties.y_label, + Vector2(_padding_offset.x, (node_box.size.y / 2) + (_y_label_size.x / 2)), + -90 + ) + +func _draw_xaxis_label() -> void: + _create_canvas_label( + 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: + _create_canvas_label( + chart_properties.title, + Vector2(node_box.size.x / 2, _padding_offset.y*2) - (drawing_options.font.get_string_size(chart_properties.title) / 2) + ) func _clear_points() -> void: for point in $Points.get_children(): point.queue_free() +func _clear_canvas_labels() -> void: + for label in $Canvas.get_children(): + label.queue_free() + func _clear() -> void: _clear_points() + _clear_canvas_labels() func _draw(): + _clear() + _pre_process() + if drawing_options.background: _draw_background() if drawing_options.borders: _draw_borders() + if drawing_options.labels: + _draw_xaxis_label() + _draw_yaxis_label() + _draw_title() + if drawing_options.grid or drawing_options.ticks or drawing_options.labels: _draw_grid() - + if drawing_options.bounding_box: _draw_bounding_box() + if drawing_options.origin: + _draw_origin() + if drawing_options.points: _draw_points() - -# if drawing_options.labels: -# _draw_labels() func _validate_sampled_axis(x_data: SampledAxis, y_data: SampledAxis) -> int: var error: int = 0 # OK diff --git a/addons/easy_charts/utilities/classes/plotting/chart_properties.gd b/addons/easy_charts/utilities/classes/plotting/chart_properties.gd new file mode 100644 index 0000000..35a6124 --- /dev/null +++ b/addons/easy_charts/utilities/classes/plotting/chart_properties.gd @@ -0,0 +1,6 @@ +extends Reference +class_name ChartProperties + +var title: String +var x_label: String +var y_label: String diff --git a/addons/easy_charts/utilities/classes/plotting/drawing_options.gd b/addons/easy_charts/utilities/classes/plotting/drawing_options.gd index 372b1f5..aa353b4 100644 --- a/addons/easy_charts/utilities/classes/plotting/drawing_options.gd +++ b/addons/easy_charts/utilities/classes/plotting/drawing_options.gd @@ -8,13 +8,20 @@ var background: bool = true var borders: bool = false var ticks: bool = true var labels: bool = true +var origin: bool = true var colors: Dictionary = { bounding_box = Color.black, grid = Color.gray, - functions = [Color.red, Color.green, Color.blue] + functions = [Color.red, Color.green, Color.blue, Color.black] } -var shapes: Array = [PointContainer.PointShape.CIRCLE, PointContainer.PointShape.SQUARE, PointContainer.PointShape.SQUARE] +var shapes: Array = [PointContainer.PointShape.CIRCLE, PointContainer.PointShape.SQUARE, PointContainer.PointShape.TRIANGLE, PointContainer.PointShape.CROSS] var point_radius: float = 3.0 -var font: Font = Label.new().get_font("") +var font: BitmapFont = Label.new().get_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 PointContainer.PointShape.CIRCLE diff --git a/addons/easy_charts/utilities/containers/point_container/point_container.gd b/addons/easy_charts/utilities/containers/point_container/point_container.gd index e67e71e..df38a69 100644 --- a/addons/easy_charts/utilities/containers/point_container/point_container.gd +++ b/addons/easy_charts/utilities/containers/point_container/point_container.gd @@ -49,6 +49,25 @@ func _draw_point() -> void: draw_circle(point_rel_pos, self.radius, self.color) PointShape.SQUARE: draw_rect(Rect2(point_rel_pos * 0.5, point_rel_pos), self.color, true, 1.0, false) + PointShape.TRIANGLE: + draw_colored_polygon( + PoolVector2Array([ + point_rel_pos + (Vector2.UP * self.radius * 1.5), + point_rel_pos + (Vector2.ONE * self.radius * 1.5), + point_rel_pos - (Vector2(1, -1) * self.radius * 1.5) + ]), self.color, [], null, null, false + ) + PointShape.CROSS: + draw_line( + point_rel_pos - (Vector2.ONE * self.radius), + point_rel_pos + (Vector2.ONE * self.radius), + self.color, self.radius, true + ) + draw_line( + point_rel_pos + (Vector2(1, -1) * self.radius), + point_rel_pos + (Vector2(-1, 1) * self.radius), + self.color, self.radius / 2, true + ) func _draw(): # _draw_bounding_box()