v0.2 update

This commit is contained in:
fenix-hub 2020-05-20 17:28:10 +02:00
parent 157f8e3142
commit 9a199ff1c7
20 changed files with 2169 additions and 532 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
*.import *.import
*.png

View File

@ -1,7 +1,6 @@
tool tool
extends Control extends Control
""" """
[BarChart] - General purpose node for Bar Charts [BarChart] - General purpose node for Bar Charts
@ -39,9 +38,9 @@ var y_pass : float
# vertical distance between y consecutive points used for intervals # vertical distance between y consecutive points used for intervals
var v_dist : float var v_dist : float
var h_dist : float
# quantization, representing the interval in which values will be displayed # quantization, representing the interval in which values will be displayed
var x_decim : float = 1.0
# define values on x an y axis # define values on x an y axis
var x_chors : Array var x_chors : Array
@ -57,15 +56,19 @@ var datas : Array
# amount of functions to represent # amount of functions to represent
var functions : int = 0 var functions : int = 0
var x_label : String
# database values # database values
var x_datas : Array var x_datas : Array
var y_datas : Array var y_datas : Array
# labels displayed on chart # labels displayed on chart
var x_label : String var x_labels : Array
var y_labels : Array var y_labels : Array
var x_margin_min : int = 0
var y_margin_min : int = 0
# actual values of point, from the database # actual values of point, from the database
var point_values : Array var point_values : Array
@ -75,47 +78,46 @@ var point_positions : Array
var legend : Array setget set_legend,get_legend var legend : Array setget set_legend,get_legend
# --------------------- # ---------------------
#export (bool)
var SIZE : Vector2 = Vector2() var SIZE : Vector2 = Vector2()
export (String, FILE) var source : String = "" export (String, FILE, "*.txt, *.csv") var source : String = ""
export (String) var delimiter : String = ";" export (String) var delimiter : String = ";"
export (bool) var origin_at_zero : bool = true
export (bool) var are_values_columns : bool = false export (bool) var are_values_columns : bool = false
export (bool) var invert_xy : bool = false export (int,0,100) var x_values_index : int = 0
export(bool) var show_x_values_as_labels : bool = true
export (int,0,100) var x_values : int = 0
export (float,1,20,0.5) var column_width : float = 10 export (float,1,20,0.5) var column_width : float = 10
export (float,0,10,0.5) var column_gap : float = 2 export (float,0,10,0.5) var column_gap : float = 2
export (float,0,10) var y_decim : float = 5.0 export (float,0.1,10.0) var x_decim : float = 5.0
export (PoolColorArray) var function_colors = [Color("#1e1e1e")] export (float,0.1,10.0) var y_decim : float = 5.0
export (bool) var boxed : bool = true export (int,"Dot,Triangle,Square") var point_shape : int = 0
export (PoolColorArray) var function_colors = [Color("#1e1e1e")]
export (Color) var v_lines_color : Color = Color("#cacaca") export (Color) var v_lines_color : Color = Color("#cacaca")
export (Color) var h_lines_color : Color = Color("#cacaca") export (Color) var h_lines_color : Color = Color("#cacaca")
export (Color) var outline_color : Color = Color("#1e1e1e")
export (bool) var boxed : bool = true
export (Color) var box_color : Color = Color("#1e1e1e")
export (Font) var font : Font export (Font) var font : Font
export (Font) var bold_font : Font export (Font) var bold_font : Font
export (Color) var font_color : Color = Color("#1e1e1e") export (Color) var font_color : Color = Color("#1e1e1e")
export (String,"Default","Clean","Gradient","Minimal","Invert") var template : String = "Default" setget apply_template export (String,"Default","Clean","Gradient","Minimal","Invert") var template : String = "Default" setget apply_template
export (bool) var invert_chart : bool = false
var templates : Dictionary = {} var templates : Dictionary = {}
signal chart_plotted(chart) signal chart_plotted(chart)
signal point_pressed(point) signal point_pressed(point)
func _point_drawn(): func _point_plotted():
pass pass
func _ready(): func _ready():
pass pass
func _script_changed():
_enter_tree()
_ready()
func load_font(): func load_font():
if font != null: if font != null:
font_size = font.get_height() font_size = font.get_height()
@ -129,26 +131,27 @@ func load_font():
if bold_font != null: if bold_font != null:
PointData.Data.set("custom_fonts/font",bold_font) PointData.Data.set("custom_fonts/font",bold_font)
func _plot(source_ : String, delimiter_ : String, are_values_columns_ : bool, x_values_ : int): func _plot(source_ : String, delimiter_ : String, are_values_columns_ : bool, x_values_index_ : int):
randomize() randomize()
load_font() load_font()
PointData.hide() PointData.hide()
datas = read_datas(source_,delimiter_) datas = read_datas(source_,delimiter_)
count_functions() count_functions()
structure_datas(datas,are_values_columns_,x_values_) structure_datas(datas,are_values_columns_,x_values_index_)
build_chart() build_chart()
calculate_pass() calculate_pass()
calculate_coordinates() calculate_coordinates()
calculate_colors() calculate_colors()
create_legend() create_legend()
emit_signal("chart_plotted", self) emit_signal("chart_plotted")
func plot(): func plot():
randomize() randomize()
load_font() load_font()
PointData.hide() PointData.hide()
@ -157,72 +160,35 @@ func plot():
return return
datas = read_datas(source,delimiter) datas = read_datas(source,delimiter)
count_functions() count_functions()
structure_datas(datas,are_values_columns,x_values) structure_datas(datas,are_values_columns,x_values_index)
build_chart() build_chart()
calculate_pass() calculate_pass()
calculate_coordinates() calculate_coordinates()
calculate_colors() calculate_colors()
create_legend() create_legend()
emit_signal("chart_plotted", self) emit_signal("chart_plotted")
func _draw(): func clear_points():
draw_chart() if Points.get_children():
for function in Points.get_children():
function.queue_free()
for legend in Legend.get_children():
legend.queue_free()
func calculate_colors(): func calculate_colors():
if function_colors.empty() or function_colors.size() < functions: if function_colors.empty() or function_colors.size() < functions:
for function in functions: for function in functions:
function_colors.append(Color("#1e1e1e")) function_colors.append(Color("#1e1e1e"))
func draw_chart(): func build_chart():
clear_points() SIZE = get_size()
draw_outlines() origin = Vector2(OFFSET.x,SIZE.y-OFFSET.y)
draw_v_grid()
draw_h_grid()
draw_functions()
func draw_outlines(): func point_pressed(point : Point):
if boxed: emit_signal("point_pressed",point)
draw_line(Vector2(origin.x,0),Vector2(SIZE.x,0),outline_color,1,true)
draw_line(Vector2(SIZE.x,0),Vector2(SIZE.x,origin.y),outline_color,1,true)
draw_line(Vector2(SIZE.x,origin.y),origin,outline_color,1,true)
draw_line(origin,Vector2(origin.x,0),outline_color,1,true)
func draw_v_grid(): func _enter_tree():
for p in x_chors.size(): templates = Utilities._load_templates()
var point : Vector2 = origin+Vector2((p)*x_pass + OFFSET.x/2,0)
# v grid
draw_line(Vector2(point.x,origin.y),Vector2(point.x,origin.y-5),v_lines_color,0.2,true)
draw_string(font,point+Vector2(-const_width/2*x_chors[p].length() + (column_width/2) * ( y_datas.size() if not invert_xy else y_datas[0].size()+1 ) + column_gap,font_size),x_chors[p],font_color)
func draw_h_grid():
for p in y_chors.size():
var point : Vector2 = origin-Vector2(0,(p)*y_pass)
# h grid
draw_line(point,Vector2(SIZE.x,point.y),h_lines_color,0.2,true)
draw_string(font,point-Vector2(y_chors[p].length()*const_width+font_size,font_size/2),y_chors[p],font_color)
func draw_functions():
for function in point_positions.size():
draw_function(function,point_positions[function])
func draw_function(f_index : int, function : Array):
for point in function.size():
var pointv : Point
pointv = point_node.instance()
pointv.connect("_mouse_entered",self,"show_data",[pointv])
pointv.connect("_mouse_exited",self,"hide_data")
pointv.connect("_point_pressed",self,"point_pressed")
pointv.create_point(function_colors[f_index], Color.white, function[point]+Vector2(0,5),
pointv.format_value(point_values[f_index][point],false,true),(x_datas[f_index] if invert_xy else y_labels[f_index]))
add_child(pointv)
pointv.rect_size.y = origin.y - function[point].y
draw_line(
Vector2(function[point].x, origin.y),
function[point], function_colors[f_index], column_width, true)
func construct_column(f_index : int, function : Array):
draw_line(Vector2(function[f_index].x,origin.y), function[f_index], function_colors[f_index], column_width, true)
func read_datas(source : String, delimiter : String): func read_datas(source : String, delimiter : String):
var file : File = File.new() var file : File = File.new()
@ -237,8 +203,8 @@ func read_datas(source : String, delimiter : String):
content.erase(data) content.erase(data)
return content return content
func structure_datas(database : Array, are_values_columns : bool, x_values : int): func structure_datas(database : Array, are_values_columns : bool, x_values_index : int):
# @x_values can be either a column or a row relative to x values # @x_values_index can be either a column or a row relative to x values
# @y_values can be either a column or a row relative to y values # @y_values can be either a column or a row relative to y values
self.are_values_columns = are_values_columns self.are_values_columns = are_values_columns
match are_values_columns: match are_values_columns:
@ -246,11 +212,19 @@ func structure_datas(database : Array, are_values_columns : bool, x_values : int
for row in database.size(): for row in database.size():
var t_vals : Array var t_vals : Array
for column in database[row].size(): for column in database[row].size():
if column == x_values: if column == x_values_index:
x_datas.append(database[row][column]) var x_data = database[row][column]
if x_data.is_valid_float() or x_data.is_valid_integer():
x_datas.append(x_data as float)
else:
x_datas.append(x_data.replace(",",".") as float)
else: else:
if row != 0: if row != 0:
t_vals.append(float(database[row][column])) var y_data = database[row][column]
if y_data.is_valid_float() or y_data.is_valid_integer():
t_vals.append(y_data as float)
else:
t_vals.append(y_data.replace(",",".") as float)
else: else:
y_labels.append(str(database[row][column])) y_labels.append(str(database[row][column]))
if not t_vals.empty(): if not t_vals.empty():
@ -258,7 +232,7 @@ func structure_datas(database : Array, are_values_columns : bool, x_values : int
x_label = str(x_datas.pop_front()) x_label = str(x_datas.pop_front())
false: false:
for row in database.size(): for row in database.size():
if row == x_values: if row == x_values_index:
x_datas = (database[row]) x_datas = (database[row])
x_label = x_datas.pop_front() as String x_label = x_datas.pop_front() as String
else: else:
@ -269,34 +243,66 @@ func structure_datas(database : Array, are_values_columns : bool, x_values : int
for value in data.size(): for value in data.size():
data[value] = data[value] as float data[value] = data[value] as float
# draw y labels
var to_order : Array var to_order : Array
var to_order_min : Array
for cluster in y_datas.size(): for cluster in y_datas.size():
# define x_chors and y_chors # define x_chors and y_chors
var margin = y_datas[cluster][y_datas[cluster].size()-1] var ordered_cluster = y_datas[cluster] as Array
to_order.append(margin) ordered_cluster.sort()
ordered_cluster = PoolIntArray(ordered_cluster)
var margin_max = ordered_cluster[ordered_cluster.size()-1]
var margin_min = ordered_cluster[0]
to_order.append(margin_max)
to_order_min.append(margin_min)
to_order.sort() to_order.sort()
to_order_min.sort()
var margin = to_order.pop_back() var margin = to_order.pop_back()
if not origin_at_zero:
y_margin_min = to_order_min.pop_front()
v_dist = y_decim * pow(10.0,str(margin).length()-2) v_dist = y_decim * pow(10.0,str(margin).length()-2)
var multi = 0 var multi = 0
var p = v_dist*multi var p = (v_dist*multi) + ((y_margin_min) if not origin_at_zero else 0)
y_chors.append(p as String) y_chors.append(p as String)
while p < margin: while p < margin:
multi+=1 multi+=1
p = v_dist*multi p = (v_dist*multi) + ((y_margin_min) if not origin_at_zero else 0)
y_chors.append(p as String) y_chors.append(p as String)
# draw x_labels
if not show_x_values_as_labels:
to_order.clear()
to_order = x_datas as PoolIntArray
to_order.sort()
margin = to_order.pop_back()
if not origin_at_zero:
x_margin_min = to_order.pop_front()
h_dist = x_decim * pow(10.0,str(margin).length()-2)
multi = 0
p = (h_dist*multi) + ((x_margin_min) if not origin_at_zero else 0)
x_labels.append(p as String)
while p < margin:
multi+=1
p = (h_dist*multi) + ((x_margin_min) if not origin_at_zero else 0)
x_labels.append(p as String)
func build_chart():
SIZE = get_size()
origin = Vector2(OFFSET.x,SIZE.y-OFFSET.y)
func calculate_pass(): func calculate_pass():
if invert_xy: if invert_chart:
x_chors = y_labels as PoolStringArray x_chors = y_labels as PoolStringArray
else: else:
x_chors = x_datas as PoolStringArray if show_x_values_as_labels:
x_chors = x_datas as PoolStringArray
else:
x_chors = x_labels
# calculate distance in pixel between 2 consecutive values/datas # calculate distance in pixel between 2 consecutive values/datas
x_pass = (SIZE.x - OFFSET.x*2 - (column_width) * ( y_datas.size() if not invert_xy else y_datas[0].size()+1 ) - column_gap - column_width/2) / (x_chors.size()-1) if not are_values_columns:
x_pass = (SIZE.x - OFFSET.x*2 - (column_width) * ( y_datas.size() if not invert_chart else y_datas[0].size()+1 ) - column_gap - column_width/2) / (x_chors.size()-1)
else:
x_pass = (SIZE.x - OFFSET.x*2 - (column_width) * ( y_datas.size() if invert_chart else y_datas[0].size()+1 ) - column_gap - column_width/2) / (x_chors.size()-1)
y_pass = origin.y / (y_chors.size()-1) y_pass = origin.y / (y_chors.size()-1)
func calculate_coordinates(): func calculate_coordinates():
@ -305,58 +311,127 @@ func calculate_coordinates():
point_values.clear() point_values.clear()
point_positions.clear() point_positions.clear()
if invert_xy: if invert_chart:
for column in y_datas[0].size(): for column in y_datas[0].size():
var single_coordinates : Array var single_coordinates : Array
for row in y_datas: for row in y_datas:
single_coordinates.append((row[column]*y_pass)/v_dist) if origin_at_zero:
single_coordinates.append((row[column]*y_pass)/v_dist)
else:
single_coordinates.append((row[column] - y_margin_min)*y_pass/v_dist)
y_coordinates.append(single_coordinates) y_coordinates.append(single_coordinates)
else: else:
for cluster in y_datas: for cluster in y_datas:
var single_coordinates : Array var single_coordinates : Array
for value in cluster.size(): for value in cluster.size():
single_coordinates.append((cluster[value]*y_pass)/v_dist) if origin_at_zero:
single_coordinates.append((cluster[value]*y_pass)/v_dist)
else:
single_coordinates.append((cluster[value] - y_margin_min)*y_pass/v_dist)
y_coordinates.append(single_coordinates) y_coordinates.append(single_coordinates)
for x in x_chors.size(): if show_x_values_as_labels:
x_coordinates.append(x_pass*x) for x in x_datas.size():
x_coordinates.append(x_pass*x)
else:
for x in x_datas.size():
if origin_at_zero:
if not invert_chart:
x_coordinates.append(x_pass*x)
else:
x_coordinates.append(x_datas[x]*x_pass/h_dist)
else:
x_coordinates.append((x_datas[x] - x_margin_min)*x_pass/h_dist)
for f in functions: for f in functions:
point_values.append([]) point_values.append([])
point_positions.append([]) point_positions.append([])
if invert_xy: if invert_chart:
for function in y_coordinates.size(): for function in y_coordinates.size():
for function_value in y_coordinates[function].size(): for function_value in y_coordinates[function].size():
point_positions[function].append(Vector2(OFFSET.x/2 + column_width/2 + (column_width + column_gap)*function + x_coordinates[function_value]+origin.x, origin.y-y_coordinates[function][function_value])) if are_values_columns:
point_values[function].append([x_chors[function_value],y_datas[function_value][function]]) point_values[function].append([x_datas[function_value],y_datas[function_value][function]])
point_positions[function].append(Vector2(OFFSET.x/2 + column_width/2 + (column_width + column_gap)*function + x_coordinates[function_value]+origin.x,origin.y-y_coordinates[function][function_value]))
else:
point_positions[function].append(Vector2(OFFSET.x/2 + column_width/2 + (column_width + column_gap)*function + x_coordinates[function_value]+origin.x,origin.y-y_coordinates[function][function_value]))
point_values[function].append([x_datas[function_value],y_datas[function_value][function]])
else: else:
for cluster in y_coordinates.size(): for cluster in y_coordinates.size():
for y in y_coordinates[cluster].size(): for y in y_coordinates[cluster].size():
point_values[cluster].append([x_chors[y],y_datas[cluster][y]]) if are_values_columns:
point_positions[cluster].append(Vector2(OFFSET.x/2 + column_width/2 + (column_width + column_gap)*cluster + x_coordinates[y]+origin.x, origin.y-y_coordinates[cluster][y])) point_positions[y].append(Vector2(OFFSET.x/2 + column_width/2 + (column_width + column_gap)*y + x_coordinates[cluster] + origin.x, origin.y-y_coordinates[cluster][y]))
point_values[y].append([x_datas[cluster],y_datas[cluster][y]])
else:
point_values[cluster].append([x_datas[y],y_datas[cluster][y]])
point_positions[cluster].append(Vector2(OFFSET.x/2 + column_width/2 + (column_width + column_gap)*cluster + x_coordinates[y]+origin.x,origin.y-y_coordinates[cluster][y]))
func _draw():
clear_points()
draw_grid()
draw_chart_outlines()
var defined_colors : bool = false
if function_colors.size():
defined_colors = true
for _function in point_values.size():
var PointContainer : Control = Control.new()
Points.add_child(PointContainer)
for function_point in point_values[_function].size():
var point : Point = point_node.instance()
point.connect("_point_pressed",self,"point_pressed")
point.connect("_mouse_entered",self,"show_data")
point.connect("_mouse_exited",self,"hide_data")
point.create_point(point_shape, function_colors[function_point if invert_chart else _function],
Color.white, point_positions[_function][function_point],
point.format_value(point_values[_function][function_point], false, false),
y_labels[function_point if invert_chart else _function] as String)
PointContainer.add_child(point)
point.rect_size.y = origin.y - point_positions[_function][function_point].y
draw_line( Vector2(point_positions[_function][function_point].x, origin.y),
point_positions[_function][function_point], function_colors[_function], column_width, true)
func draw_grid():
# ascisse
for p in x_chors.size():
var point : Vector2 = origin+Vector2((p)*x_pass,0)
# v grid
draw_line(point,point-Vector2(0,SIZE.y-OFFSET.y),v_lines_color,0.2,true)
# ascisse
draw_line(point-Vector2(0,5),point,v_lines_color,1,true)
var calculated_gap : float
if not are_values_columns:
calculated_gap = ( y_datas.size() if not invert_chart else y_datas[0].size()+1 )
else:
calculated_gap = ( y_datas.size() if invert_chart else y_datas[0].size()+1 )
draw_string(font,point+Vector2(-const_width/2*x_chors[p].length() + (column_width/2) * calculated_gap + column_gap,font_size),x_chors[p],font_color)
# ordinate
for p in y_chors.size():
var point : Vector2 = origin-Vector2(0,(p)*y_pass)
# h grid
draw_line(point,point+Vector2(SIZE.x-OFFSET.x,0),h_lines_color,0.2,true)
# ordinate
draw_line(point,point+Vector2(5,0),h_lines_color,1,true)
draw_string(font,point-Vector2(y_chors[p].length()*const_width+font_size,font_size/2),y_chors[p],font_color)
func draw_chart_outlines():
if boxed:
draw_line(Vector2(origin.x,0),Vector2(SIZE.x,0),box_color,1,true)
draw_line(Vector2(SIZE.x,0),Vector2(SIZE.x,origin.y),box_color,1,true)
draw_line(Vector2(SIZE.x,origin.y),origin,box_color,1,true)
draw_line(origin,Vector2(origin.x,0),box_color,1,true)
func redraw(): func redraw():
pass build_chart()
calculate_pass()
calculate_coordinates()
update()
func invert_chart():
invert_xy = !invert_xy
count_functions()
redraw()
create_legend()
func count_functions():
if are_values_columns:
if not invert_xy:
functions = datas[0].size()-1
else:
functions = datas.size()-1
else:
if invert_xy:
functions = datas[0].size()-1
else:
functions = datas.size()-1
func show_data(point): func show_data(point):
PointData.update_datas(point) PointData.update_datas(point)
@ -365,20 +440,37 @@ func show_data(point):
func hide_data(): func hide_data():
PointData.hide() PointData.hide()
func clear_points(): func set_legend(l : Array):
if Points.get_children(): legend = l
for function in Points.get_children():
function.queue_free() func get_legend():
for legend in Legend.get_children(): return legend
legend.queue_free()
func invert_chart():
invert_chart = !invert_chart
count_functions()
redraw()
create_legend()
func count_functions():
if are_values_columns:
if not invert_chart:
functions = datas[0].size()-1
else:
functions = datas.size()-1
else:
if invert_chart:
functions = datas[0].size()-1
else:
functions = datas.size()-1
func create_legend(): func create_legend():
legend.clear() legend.clear()
for function in functions: for function in functions:
var function_legend = FunctionLegend.instance() var function_legend = FunctionLegend.instance()
var f_name : String var f_name : String
if invert_xy: if invert_chart:
f_name = x_datas[function] f_name = x_datas[function] as String
else: else:
f_name = y_labels[function] f_name = y_labels[function]
var legend_font : Font var legend_font : Font
@ -389,26 +481,14 @@ func create_legend():
function_legend.create_legend(f_name,function_colors[function],bold_font,font_color) function_legend.create_legend(f_name,function_colors[function],bold_font,font_color)
legend.append(function_legend) legend.append(function_legend)
func set_legend(l : Array):
legend = l
func get_legend() -> Array:
return legend
func apply_template(template_name : String): func apply_template(template_name : String):
if Engine.editor_hint: template = template_name
if template_name!=null and template_name!="":
template = template_name
var custom_template = templates[template_name.to_lower()]
function_colors = custom_template.function_colors
v_lines_color = Color(custom_template.v_lines_color)
h_lines_color = Color(custom_template.h_lines_color)
outline_color = Color(custom_template.outline_color)
font_color = Color(custom_template.font_color)
property_list_changed_notify()
func point_pressed(point : Point):
emit_signal("point_pressed",point)
func _enter_tree():
templates = Utilities._load_templates() templates = Utilities._load_templates()
if template_name!=null and template_name!="":
var custom_template = templates[template.to_lower()]
function_colors = custom_template.function_colors
v_lines_color = Color(custom_template.v_lines_color)
h_lines_color = Color(custom_template.h_lines_color)
box_color = Color(custom_template.outline_color)
font_color = Color(custom_template.font_color)
property_list_changed_notify()

View File

@ -12,13 +12,19 @@ rect_min_size = Vector2( 70, 50 )
mouse_filter = 2 mouse_filter = 2
script = ExtResource( 2 ) script = ExtResource( 2 )
__meta__ = { __meta__ = {
"_edit_use_anchors_": false "_edit_use_anchors_": false,
"_editor_description_": "[BarChart] - General purpose node for Bar Charts
A bar chart or bar graph is a chart or graph that presents categorical data with
rectangular bars with heights or lengths proportional to the values that they represent.
The bars can be plotted vertically or horizontally. A vertical bar chart is sometimes
called a column chart.
A bar graph shows comparisons among discrete categories. One axis of the chart shows
the specific categories being compared, and the other axis represents a measured value.
Some bar graphs present bars clustered in groups of more than one, showing the
values of more than one measured variable."
} }
function_colors = PoolColorArray( 0.117647, 0.117647, 0.117647, 1 ) function_colors = [ "#1e1e1e", "#1e1e1e", "#1e1e1e", "#1e1e1e" ]
v_lines_color = Color( 0.792157, 0.792157, 0.792157, 1 )
h_lines_color = Color( 0.792157, 0.792157, 0.792157, 1 )
outline_color = Color( 0.117647, 0.117647, 0.117647, 1 )
font_color = Color( 0.117647, 0.117647, 0.117647, 1 )
[node name="Background" type="ColorRect" parent="."] [node name="Background" type="ColorRect" parent="."]
visible = false visible = false
@ -51,6 +57,5 @@ __meta__ = {
[node name="PointData" parent="PointData" index="0"] [node name="PointData" parent="PointData" index="0"]
visible = false visible = false
theme = SubResource( 1 ) theme = SubResource( 1 )
[connection signal="script_changed" from="." to="." method="_script_changed"]
[editable path="PointData"] [editable path="PointData"]

View File

@ -1,7 +1,6 @@
tool tool
extends Node2D extends Node2D
""" """
[BarChart2D] - General purpose node for Bar Charts [BarChart2D] - General purpose node for Bar Charts
@ -17,10 +16,10 @@ values of more than one measured variable.
/ source : Wikipedia / / source : Wikipedia /
""" """
onready var FunctionsTween : Tween = $FunctionsTween
onready var OutlinesTween : Tween = $OutlinesTween onready var OutlinesTween : Tween = $OutlinesTween
onready var GridTween : Tween = $GridTween onready var FunctionsTween : Tween = $FunctionsTween
onready var Functions : Node2D = $Functions onready var Functions : Node2D = $Functions
onready var GridTween : Tween = $GridTween
onready var PointData = $PointData/PointData onready var PointData = $PointData/PointData
onready var Outlines : Line2D = $Outlines onready var Outlines : Line2D = $Outlines
onready var Grid : Node2D = $Grid onready var Grid : Node2D = $Grid
@ -43,9 +42,9 @@ var y_pass : float
# vertical distance between y consecutive points used for intervals # vertical distance between y consecutive points used for intervals
var v_dist : float var v_dist : float
var h_dist : float
# quantization, representing the interval in which values will be displayed # quantization, representing the interval in which values will be displayed
var x_decim : float = 1.0
# define values on x an y axis # define values on x an y axis
var x_chors : Array var x_chors : Array
@ -61,15 +60,19 @@ var datas : Array
# amount of functions to represent # amount of functions to represent
var functions : int = 0 var functions : int = 0
var x_label : String
# database values # database values
var x_datas : Array var x_datas : Array
var y_datas : Array var y_datas : Array
# labels displayed on chart # labels displayed on chart
var x_label : String var x_labels : Array
var y_labels : Array var y_labels : Array
var x_margin_min : int = 0
var y_margin_min : int = 0
# actual values of point, from the database # actual values of point, from the database
var point_values : Array var point_values : Array
@ -79,81 +82,64 @@ var point_positions : Array
var legend : Array setget set_legend,get_legend var legend : Array setget set_legend,get_legend
# --------------------- # ---------------------
#export (bool) export (Vector2) var SIZE : Vector2 = Vector2() setget _set_size
export (Vector2) var SIZE : Vector2 = Vector2() export (String, FILE, "*.txt, *.csv") var source : String = ""
export (String, FILE) var source : String = ""
export (String) var delimiter : String = ";" export (String) var delimiter : String = ";"
export (bool) var origin_at_zero : bool = true
export (bool) var are_values_columns : bool = false export (bool) var are_values_columns : bool = false
export (bool) var invert_xy : bool = false export (int,0,100) var x_values_index : int = 0
export(bool) var show_x_values_as_labels : bool = true
export (int,0,100) var x_values : int = 0
export (float,1,20,0.5) var column_width : float = 10 export (float,1,20,0.5) var column_width : float = 10
export (float,0,10,0.5) var column_gap : float = 2 export (float,0,10,0.5) var column_gap : float = 2
export (float,0,10) var y_decim : float = 5.0 export (float,0.1,10.0) var x_decim : float = 5.0
export (float,0.1,10.0) var y_decim : float = 5.0
export (int,"Dot,Triangle,Square") var point_shape : int = 0
export (PoolColorArray) var function_colors = [Color("#1e1e1e")] export (PoolColorArray) var function_colors = [Color("#1e1e1e")]
export (bool) var boxed : bool = true
export (Color) var v_lines_color : Color = Color("#cacaca") export (Color) var v_lines_color : Color = Color("#cacaca")
export (Color) var h_lines_color : Color = Color("#cacaca") export (Color) var h_lines_color : Color = Color("#cacaca")
export (Color) var outline_color : Color = Color("#1e1e1e")
export (float,0.01,1) var drawing_duration : float = 0.3 export (bool) var boxed : bool = true
export (Color) var box_color : Color = Color("#1e1e1e")
export (Font) var font : Font export (Font) var font : Font
export (Font) var bold_font : Font export (Font) var bold_font : Font
export (Color) var font_color : Color = Color("#1e1e1e") export (Color) var font_color : Color = Color("#1e1e1e")
export (String,"Default","Clean","Gradient","Minimal","Invert") var template : String = "Default" setget apply_template export (String,"Default","Clean","Gradient","Minimal","Invert") var template : String = "Default" setget apply_template
export (float,0.1,1) var drawing_duration : float = 0.5
export (bool) var invert_chart : bool = false
var templates : Dictionary = {} var templates : Dictionary = {}
signal chart_plotted(chart) signal chart_plotted(chart)
signal point_pressed(point) signal point_pressed(point)
func _point_drawn(): func _point_plotted():
pass pass
func _ready(): func _ready():
pass pass
func _script_changed(): func _set_size(size : Vector2):
_ready() SIZE = size
build_chart()
func _process(delta):
if Engine.editor_hint: if Engine.editor_hint:
Outlines.set_default_color(outline_color) Outlines.set_point_position(0,Vector2(origin.x,0))
Grid.get_node("VLine").set_default_color(v_lines_color) Outlines.set_point_position(1,Vector2(SIZE.x,0))
Grid.get_node("HLine").set_default_color(h_lines_color) Outlines.set_point_position(2,Vector2(SIZE.x,origin.y))
if function_colors.size(): Outlines.set_point_position(3,origin)
Functions.get_node("Function").set_default_color(function_colors[0]) Outlines.set_point_position(4,Vector2(origin.x,0))
else:
Functions.get_node("Function").set_default_color(Color("#1e1e1e"))
if SIZE!=Vector2(): Grid.get_node("VLine").set_point_position(0,Vector2((OFFSET.x+SIZE.x)/2,0))
build_chart() Grid.get_node("VLine").set_point_position(1,Vector2((OFFSET.x+SIZE.x)/2,origin.y))
Outlines.set_point_position(0,Vector2(origin.x,0)) Grid.get_node("HLine").set_point_position(0,Vector2(origin.x,origin.y/2))
Outlines.set_point_position(1,Vector2(SIZE.x,0)) Grid.get_node("HLine").set_point_position(1,Vector2(SIZE.x,origin.y/2))
Outlines.set_point_position(2,Vector2(SIZE.x,origin.y))
Outlines.set_point_position(3,origin)
Outlines.set_point_position(4,Vector2(origin.x,0))
Grid.get_node("VLine").set_point_position(0,Vector2((OFFSET.x+SIZE.x)/2,0))
Grid.get_node("VLine").set_point_position(1,Vector2((OFFSET.x+SIZE.x)/2,origin.y))
Grid.get_node("HLine").set_point_position(0,Vector2(origin.x,origin.y/2))
Grid.get_node("HLine").set_point_position(1,Vector2(SIZE.x,origin.y/2))
if function_colors.size():
Functions.get_node("Function").set_point_position(0,Vector2((OFFSET.x+SIZE.x)/2,origin.y))
Functions.get_node("Function").set_point_position(1,Vector2((OFFSET.x+SIZE.x)/2,0))
#func _ready():
# plot_line_chart("res://ChartNode/datas2.csv",";",false,0,invert_xy,function_colors,drawing_duration,SIZE)
func clear(): func clear():
Outlines.points = [] Outlines.points = []
Grid.get_node("HLine").queue_free() Grid.get_node("HLine").queue_free()
Grid.get_node("VLine").queue_free() Grid.get_node("VLine").queue_free()
Functions.get_node("Function").queue_free()
func load_font(): func load_font():
if font != null: if font != null:
@ -161,10 +147,14 @@ func load_font():
var theme : Theme = Theme.new() var theme : Theme = Theme.new()
theme.set_default_font(font) theme.set_default_font(font)
PointData.set_theme(theme) PointData.set_theme(theme)
else:
var lbl = Label.new()
font = lbl.get_font("")
lbl.free()
if bold_font != null: if bold_font != null:
PointData.Data.set("custom_fonts/font",bold_font) PointData.Data.set("custom_fonts/font",bold_font)
func _plot(source_ : String, delimiter_ : String, are_values_columns_ : bool, x_values_ : int): func _plot(source_ : String, delimiter_ : String, are_values_columns_ : bool, x_values_index_ : int):
randomize() randomize()
clear() clear()
@ -174,7 +164,7 @@ func _plot(source_ : String, delimiter_ : String, are_values_columns_ : bool, x_
datas = read_datas(source_,delimiter_) datas = read_datas(source_,delimiter_)
count_functions() count_functions()
structure_datas(datas,are_values_columns_,x_values_) structure_datas(datas,are_values_columns_,x_values_index_)
build_chart() build_chart()
calculate_pass() calculate_pass()
calculate_coordinates() calculate_coordinates()
@ -197,7 +187,7 @@ func plot():
return return
datas = read_datas(source,delimiter) datas = read_datas(source,delimiter)
count_functions() count_functions()
structure_datas(datas,are_values_columns,x_values) structure_datas(datas,are_values_columns,x_values_index)
build_chart() build_chart()
calculate_pass() calculate_pass()
calculate_coordinates() calculate_coordinates()
@ -220,7 +210,7 @@ func draw_chart():
func draw_outlines(): func draw_outlines():
if boxed: if boxed:
Outlines.set_default_color(outline_color) Outlines.set_default_color(box_color)
OutlinesTween.interpolate_method(Outlines,"add_point", OutlinesTween.interpolate_method(Outlines,"add_point",
Vector2(origin.x,0),Vector2(SIZE.x,0),drawing_duration*0.5,Tween.TRANS_QUINT,Tween.EASE_OUT) Vector2(origin.x,0),Vector2(SIZE.x,0),drawing_duration*0.5,Tween.TRANS_QUINT,Tween.EASE_OUT)
OutlinesTween.start() OutlinesTween.start()
@ -240,12 +230,17 @@ func draw_outlines():
func draw_v_grid(): func draw_v_grid():
for p in x_chors.size(): for p in x_chors.size():
var point : Vector2 = origin+Vector2((p)*x_pass + OFFSET.x/2,0) var point : Vector2 = origin+Vector2((p)*x_pass,0)
var v_grid : Line2D = Line2D.new() var v_grid : Line2D = Line2D.new()
Grid.add_child(v_grid) Grid.add_child(v_grid)
v_grid.set_width(1) v_grid.set_width(1)
v_grid.set_default_color(v_lines_color) v_grid.set_default_color(v_lines_color)
add_label(point+Vector2(-const_width/2*x_chors[p].length() + (column_width/2) * ( y_datas.size() if not invert_xy else y_datas[0].size()+1 ) + column_gap,font_size/2), x_chors[p]) var calculated_gap : float
if not are_values_columns:
calculated_gap = ( y_datas.size() if not invert_chart else y_datas[0].size()+1 )
else:
calculated_gap = ( y_datas.size() if invert_chart else y_datas[0].size()+1 )
add_label(point+Vector2(-const_width/2*x_chors[p].length() + (column_width/2) * calculated_gap + column_gap,font_size/2), x_chors[p])
GridTween.interpolate_method(v_grid,"add_point",Vector2(point.x,origin.y),Vector2(point.x,origin.y-5),drawing_duration/(x_chors.size()),Tween.TRANS_EXPO,Tween.EASE_OUT) GridTween.interpolate_method(v_grid,"add_point",Vector2(point.x,origin.y),Vector2(point.x,origin.y-5),drawing_duration/(x_chors.size()),Tween.TRANS_EXPO,Tween.EASE_OUT)
GridTween.start() GridTween.start()
yield(GridTween,"tween_all_completed") yield(GridTween,"tween_all_completed")
@ -277,15 +272,19 @@ func draw_functions():
draw_function(function,point_positions[function]) draw_function(function,point_positions[function])
func draw_function(f_index : int, function : Array): func draw_function(f_index : int, function : Array):
var pointv : Point
for point in function.size(): for point in function.size():
var line : Line2D = Line2D.new() var line : Line2D = Line2D.new()
var pointv : Control
pointv = point_node.instance() pointv = point_node.instance()
line.add_child(pointv) line.add_child(pointv)
pointv.connect("_mouse_entered",self,"show_data",[pointv]) pointv.connect("_mouse_entered",self,"show_data")
pointv.connect("_mouse_exited",self,"hide_data") pointv.connect("_mouse_exited",self,"hide_data")
pointv.connect("_point_pressed",self,"point_pressed") pointv.connect("_point_pressed",self,"point_pressed")
pointv.create_point(function_colors[f_index], Color.white, function[point]+Vector2(0,5), pointv.format_value(point_values[f_index][point],false,true),(x_datas[f_index] if invert_xy else y_labels[f_index]))
pointv.create_point(point_shape, function_colors[f_index], Color.white, function[point],
pointv.format_value(point_values[f_index][point],false,false),
y_labels[point if invert_chart else f_index] as String)
pointv.rect_size.y = origin.y - function[point].y pointv.rect_size.y = origin.y - function[point].y
construct_column(line,f_index,function) construct_column(line,f_index,function)
FunctionsTween.interpolate_method(line,"add_point",Vector2(function[point].x,origin.y),function[point],drawing_duration/function.size(),Tween.TRANS_QUINT,Tween.EASE_OUT) FunctionsTween.interpolate_method(line,"add_point",Vector2(function[point].x,origin.y),function[point],drawing_duration/function.size(),Tween.TRANS_QUINT,Tween.EASE_OUT)
@ -296,7 +295,7 @@ func construct_column(line : Line2D, f_index : int, function : Array):
line.set_width(column_width) line.set_width(column_width)
line.set_default_color(function_colors[f_index]) line.set_default_color(function_colors[f_index])
line.antialiased = true line.antialiased = true
Functions.add_child(line) Functions.add_child(line,true)
func read_datas(source : String, delimiter : String): func read_datas(source : String, delimiter : String):
var file : File = File.new() var file : File = File.new()
@ -311,8 +310,8 @@ func read_datas(source : String, delimiter : String):
content.erase(data) content.erase(data)
return content return content
func structure_datas(database : Array, are_values_columns : bool, x_values : int): func structure_datas(database : Array, are_values_columns : bool, x_values_index : int):
# @x_values can be either a column or a row relative to x values # @x_values_index can be either a column or a row relative to x values
# @y_values can be either a column or a row relative to y values # @y_values can be either a column or a row relative to y values
self.are_values_columns = are_values_columns self.are_values_columns = are_values_columns
match are_values_columns: match are_values_columns:
@ -320,11 +319,19 @@ func structure_datas(database : Array, are_values_columns : bool, x_values : int
for row in database.size(): for row in database.size():
var t_vals : Array var t_vals : Array
for column in database[row].size(): for column in database[row].size():
if column == x_values: if column == x_values_index:
x_datas.append(database[row][column]) var x_data = database[row][column]
if x_data.is_valid_float() or x_data.is_valid_integer():
x_datas.append(x_data as float)
else:
x_datas.append(x_data.replace(",",".") as float)
else: else:
if row != 0: if row != 0:
t_vals.append(float(database[row][column])) var y_data = database[row][column]
if y_data.is_valid_float() or y_data.is_valid_integer():
t_vals.append(y_data as float)
else:
t_vals.append(y_data.replace(",",".") as float)
else: else:
y_labels.append(str(database[row][column])) y_labels.append(str(database[row][column]))
if not t_vals.empty(): if not t_vals.empty():
@ -332,7 +339,7 @@ func structure_datas(database : Array, are_values_columns : bool, x_values : int
x_label = str(x_datas.pop_front()) x_label = str(x_datas.pop_front())
false: false:
for row in database.size(): for row in database.size():
if row == x_values: if row == x_values_index:
x_datas = (database[row]) x_datas = (database[row])
x_label = x_datas.pop_front() as String x_label = x_datas.pop_front() as String
else: else:
@ -343,33 +350,68 @@ func structure_datas(database : Array, are_values_columns : bool, x_values : int
for value in data.size(): for value in data.size():
data[value] = data[value] as float data[value] = data[value] as float
# draw y labels
var to_order : Array var to_order : Array
var to_order_min : Array
for cluster in y_datas.size(): for cluster in y_datas.size():
# define x_chors and y_chors # define x_chors and y_chors
var margin = y_datas[cluster][y_datas[cluster].size()-1] var ordered_cluster = y_datas[cluster] as Array
to_order.append(margin) ordered_cluster.sort()
ordered_cluster = PoolIntArray(ordered_cluster)
var margin_max = ordered_cluster[ordered_cluster.size()-1]
var margin_min = ordered_cluster[0]
to_order.append(margin_max)
to_order_min.append(margin_min)
to_order.sort() to_order.sort()
to_order_min.sort()
var margin = to_order.pop_back() var margin = to_order.pop_back()
if not origin_at_zero:
y_margin_min = to_order_min.pop_front()
v_dist = y_decim * pow(10.0,str(margin).length()-2) v_dist = y_decim * pow(10.0,str(margin).length()-2)
var multi = 0 var multi = 0
var p = v_dist*multi var p = (v_dist*multi) + ((y_margin_min) if not origin_at_zero else 0)
y_chors.append(p as String) y_chors.append(p as String)
while p < margin: while p < margin:
multi+=1 multi+=1
p = v_dist*multi p = (v_dist*multi) + ((y_margin_min) if not origin_at_zero else 0)
y_chors.append(p as String) y_chors.append(p as String)
# draw x_labels
if not show_x_values_as_labels:
to_order.clear()
to_order = x_datas as PoolIntArray
to_order.sort()
margin = to_order.pop_back()
if not origin_at_zero:
x_margin_min = to_order.pop_front()
h_dist = x_decim * pow(10.0,str(margin).length()-2)
multi = 0
p = (h_dist*multi) + ((x_margin_min) if not origin_at_zero else 0)
x_labels.append(p as String)
while p < margin:
multi+=1
p = (h_dist*multi) + ((x_margin_min) if not origin_at_zero else 0)
x_labels.append(p as String)
func build_chart(): func build_chart():
origin = Vector2(OFFSET.x,SIZE.y-OFFSET.y) origin = Vector2(OFFSET.x,SIZE.y-OFFSET.y)
func calculate_pass(): func calculate_pass():
if invert_xy: if invert_chart:
x_chors = y_labels as PoolStringArray x_chors = y_labels as PoolStringArray
else: else:
x_chors = x_datas as PoolStringArray if show_x_values_as_labels:
x_chors = x_datas as PoolStringArray
else:
x_chors = x_labels
# calculate distance in pixel between 2 consecutive values/datas # calculate distance in pixel between 2 consecutive values/datas
x_pass = (SIZE.x - OFFSET.x*2 - (column_width) * ( y_datas.size() if not invert_xy else y_datas[0].size()+1 ) - column_gap - column_width/2) / (x_chors.size()-1) if not are_values_columns:
x_pass = (SIZE.x - OFFSET.x*2 - (column_width) * ( y_datas.size() if not invert_chart else y_datas[0].size()+1 ) - column_gap - column_width/2) / (x_chors.size()-1)
else:
x_pass = (SIZE.x - OFFSET.x*2 - (column_width) * ( y_datas.size() if invert_chart else y_datas[0].size()+1 ) - column_gap - column_width/2) / (x_chors.size()-1)
y_pass = origin.y / (y_chors.size()-1) y_pass = origin.y / (y_chors.size()-1)
func calculate_coordinates(): func calculate_coordinates():
@ -378,58 +420,67 @@ func calculate_coordinates():
point_values.clear() point_values.clear()
point_positions.clear() point_positions.clear()
if invert_xy: if invert_chart:
for column in y_datas[0].size(): for column in y_datas[0].size():
var single_coordinates : Array var single_coordinates : Array
for row in y_datas: for row in y_datas:
single_coordinates.append((row[column]*y_pass)/v_dist) if origin_at_zero:
single_coordinates.append((row[column]*y_pass)/v_dist)
else:
single_coordinates.append((row[column] - y_margin_min)*y_pass/v_dist)
y_coordinates.append(single_coordinates) y_coordinates.append(single_coordinates)
else: else:
for cluster in y_datas: for cluster in y_datas:
var single_coordinates : Array var single_coordinates : Array
for value in cluster.size(): for value in cluster.size():
single_coordinates.append((cluster[value]*y_pass)/v_dist) if origin_at_zero:
single_coordinates.append((cluster[value]*y_pass)/v_dist)
else:
single_coordinates.append((cluster[value] - y_margin_min)*y_pass/v_dist)
y_coordinates.append(single_coordinates) y_coordinates.append(single_coordinates)
for x in x_chors.size(): if show_x_values_as_labels:
x_coordinates.append(x_pass*x) for x in x_datas.size():
x_coordinates.append(x_pass*x)
else:
for x in x_datas.size():
if origin_at_zero:
if not invert_chart:
x_coordinates.append(x_pass*x)
else:
x_coordinates.append(x_datas[x]*x_pass/h_dist)
else:
x_coordinates.append((x_datas[x] - x_margin_min)*x_pass/h_dist)
for f in functions: for f in functions:
point_values.append([]) point_values.append([])
point_positions.append([]) point_positions.append([])
if invert_xy: if invert_chart:
for function in y_coordinates.size(): for function in y_coordinates.size():
for function_value in y_coordinates[function].size(): for function_value in y_coordinates[function].size():
point_positions[function].append(Vector2(OFFSET.x/2 + column_width/2 + (column_width + column_gap)*function + x_coordinates[function_value]+origin.x, origin.y-y_coordinates[function][function_value])) if are_values_columns:
point_values[function].append([x_chors[function_value],y_datas[function_value][function]]) point_values[function].append([x_datas[function_value],y_datas[function_value][function]])
point_positions[function].append(Vector2(OFFSET.x/2 + column_width/2 + (column_width + column_gap)*function + x_coordinates[function_value]+origin.x,origin.y-y_coordinates[function][function_value]))
else:
point_positions[function].append(Vector2(OFFSET.x/2 + column_width/2 + (column_width + column_gap)*function + x_coordinates[function_value]+origin.x,origin.y-y_coordinates[function][function_value]))
point_values[function].append([x_datas[function_value],y_datas[function_value][function]])
else: else:
for cluster in y_coordinates.size(): for cluster in y_coordinates.size():
for y in y_coordinates[cluster].size(): for y in y_coordinates[cluster].size():
point_values[cluster].append([x_chors[y],y_datas[cluster][y]]) if are_values_columns:
point_positions[cluster].append(Vector2(OFFSET.x/2 + column_width/2 + (column_width + column_gap)*cluster + x_coordinates[y]+origin.x, origin.y-y_coordinates[cluster][y])) point_positions[y].append(Vector2(OFFSET.x/2 + column_width/2 + (column_width + column_gap)*y + x_coordinates[cluster] + origin.x, origin.y-y_coordinates[cluster][y]))
point_values[y].append([x_datas[cluster],y_datas[cluster][y]])
else:
point_values[cluster].append([x_datas[y],y_datas[cluster][y]])
point_positions[cluster].append(Vector2(OFFSET.x/2 + column_width/2 + (column_width + column_gap)*cluster + x_coordinates[y]+origin.x,origin.y-y_coordinates[cluster][y]))
func redraw(): func redraw():
pass build_chart()
calculate_pass()
calculate_coordinates()
update()
func invert_chart():
invert_xy = !invert_xy
count_functions()
redraw()
create_legend()
func count_functions():
if are_values_columns:
if not invert_xy:
functions = datas[0].size()-1
else:
functions = datas.size()-1
else:
if invert_xy:
functions = datas[0].size()-1
else:
functions = datas.size()-1
func show_data(point): func show_data(point):
PointData.update_datas(point) PointData.update_datas(point)
@ -444,13 +495,37 @@ func clear_points():
for function in Functions.get_children(): for function in Functions.get_children():
function.queue_free() function.queue_free()
func set_legend(l : Array):
legend = l
func get_legend():
return legend
func invert_chart():
invert_chart = !invert_chart
count_functions()
redraw()
create_legend()
func count_functions():
if are_values_columns:
if not invert_chart:
functions = datas[0].size()-1
else:
functions = datas.size()-1
else:
if invert_chart:
functions = datas[0].size()-1
else:
functions = datas.size()-1
func create_legend(): func create_legend():
legend.clear() legend.clear()
for function in functions: for function in functions:
var function_legend = FunctionLegend.instance() var function_legend = FunctionLegend.instance()
var f_name : String var f_name : String
if invert_xy: if invert_chart:
f_name = x_datas[function] f_name = x_datas[function] as String
else: else:
f_name = y_labels[function] f_name = y_labels[function]
var legend_font : Font var legend_font : Font
@ -461,26 +536,19 @@ func create_legend():
function_legend.create_legend(f_name,function_colors[function],bold_font,font_color) function_legend.create_legend(f_name,function_colors[function],bold_font,font_color)
legend.append(function_legend) legend.append(function_legend)
func set_legend(l : Array):
legend = l
func get_legend() -> Array:
return legend
func apply_template(template_name : String): func apply_template(template_name : String):
if Engine.editor_hint: template = template_name
if template_name!=null and template_name!="":
template = template_name
var custom_template = templates[template_name.to_lower()]
function_colors = custom_template.function_colors
v_lines_color = Color(custom_template.v_lines_color)
h_lines_color = Color(custom_template.h_lines_color)
outline_color = Color(custom_template.outline_color)
font_color = Color(custom_template.font_color)
property_list_changed_notify()
func point_pressed(point : Point):
emit_signal("point_pressed",point)
func _enter_tree():
templates = Utilities._load_templates() templates = Utilities._load_templates()
if template_name!=null and template_name!="":
var custom_template = templates[template.to_lower()]
function_colors = custom_template.function_colors
v_lines_color = Color(custom_template.v_lines_color)
h_lines_color = Color(custom_template.h_lines_color)
box_color = Color(custom_template.outline_color)
font_color = Color(custom_template.font_color)
property_list_changed_notify()
if Engine.editor_hint:
Outlines.set_default_color(box_color)
Grid.get_node("VLine").set_default_color(v_lines_color)
Grid.get_node("HLine").set_default_color(h_lines_color)

View File

@ -6,8 +6,19 @@
[node name="BarChart2D" type="Node2D"] [node name="BarChart2D" type="Node2D"]
script = ExtResource( 1 ) script = ExtResource( 1 )
__meta__ = { __meta__ = {
"_edit_group_": true "_edit_group_": true,
"_editor_description_": "[BarChart2D] - General purpose node for Bar Charts
A bar chart or bar graph is a chart or graph that presents categorical data with
rectangular bars with heights or lengths proportional to the values that they represent.
The bars can be plotted vertically or horizontally. A vertical bar chart is sometimes
called a column chart.
A bar graph shows comparisons among discrete categories. One axis of the chart shows
the specific categories being compared, and the other axis represents a measured value.
Some bar graphs present bars clustered in groups of more than one, showing the
values of more than one measured variable."
} }
function_colors = [ "#1e1e1e", "#1e1e1e", "#1e1e1e", "#1e1e1e" ]
[node name="Grid" type="Node2D" parent="."] [node name="Grid" type="Node2D" parent="."]
@ -43,6 +54,5 @@ default_color = Color( 0.117647, 0.117647, 0.117647, 1 )
[node name="PointData" parent="PointData" index="0"] [node name="PointData" parent="PointData" index="0"]
visible = false visible = false
[connection signal="script_changed" from="." to="." method="_script_changed"]
[editable path="PointData"] [editable path="PointData"]

View File

@ -1,6 +1,20 @@
tool tool
extends Control extends Control
"""
[Linechart] - General purpose node for Line Charts
A line chart or line plot or line graph or curve chart is a type of chart which
displays information as a series of data points called 'markers'
connected by straight line segments.
It is a basic type of chart common in many fields. It is similar to a scatter plot
except that the measurement points are ordered (typically by their x-axis value)
and joined with straight line segments.
A line chart is often used to visualize a trend in data over intervals of time
a time series thus the line is often drawn chronologically.
In these cases they are known as run charts.
/ source : Wikipedia /
"""
onready var PointData = $PointData/PointData onready var PointData = $PointData/PointData
onready var Points = $Points onready var Points = $Points
onready var Legend = $Legend onready var Legend = $Legend
@ -23,9 +37,10 @@ var y_pass : float
# vertical distance between y consecutive points used for intervals # vertical distance between y consecutive points used for intervals
var v_dist : float var v_dist : float
var h_dist : float
# quantization, representing the interval in which values will be displayed # quantization, representing the interval in which values will be displayed
var x_decim : float = 1.0
# define values on x an y axis # define values on x an y axis
var x_chors : Array var x_chors : Array
@ -48,8 +63,13 @@ var y_datas : Array
# labels displayed on chart # labels displayed on chart
var x_label : String var x_label : String
var x_labels : Array
var y_labels : Array var y_labels : Array
var x_margin_min : int = 0
var y_margin_min : int = 0
# actual values of point, from the database # actual values of point, from the database
var point_values : Array var point_values : Array
@ -60,28 +80,32 @@ var legend : Array setget set_legend,get_legend
# --------------------- # ---------------------
var SIZE : Vector2 = Vector2() var SIZE : Vector2 = Vector2()
export (String, FILE) var source : String = "" export (String, FILE, "*.txt, *.csv") var source : String = ""
export (String) var delimiter : String = ";" export (String) var delimiter : String = ";"
export (bool) var origin_at_zero : bool = true
export (bool) var are_values_columns : bool = false export (bool) var are_values_columns : bool = false
export (bool) var invert_xy : bool = false export (int,0,100) var x_values_index : int = 0
export(bool) var show_x_values_as_labels : bool = true
export (int,0,100) var x_values : int = 0 #export (float,1,20,0.5) var column_width : float = 10
#export (float,0,10,0.5) var column_gap : float = 2
export (float,1,20,0.5) var column_width : float = 10 export (float,0.1,10.0) var x_decim : float = 5.0
export (float,0,10,0.5) var column_gap : float = 2 export (float,0.1,10.0) var y_decim : float = 5.0
export (int,"Dot,Triangle,Square") var point_shape : int = 0
export (float,0,10) var y_decim : float = 5.0
export (PoolColorArray) var function_colors = [Color("#1e1e1e")] export (PoolColorArray) var function_colors = [Color("#1e1e1e")]
export (bool) var boxed : bool = true
export (Color) var v_lines_color : Color = Color("#cacaca") export (Color) var v_lines_color : Color = Color("#cacaca")
export (Color) var h_lines_color : Color = Color("#cacaca") export (Color) var h_lines_color : Color = Color("#cacaca")
export (Color) var outline_color : Color = Color("#1e1e1e")
export (bool) var boxed : bool = true
export (Color) var box_color : Color = Color("#1e1e1e")
export (Font) var font : Font export (Font) var font : Font
export (Font) var bold_font : Font export (Font) var bold_font : Font
export (Color) var font_color : Color = Color("#1e1e1e") export (Color) var font_color : Color = Color("#1e1e1e")
export (String,"Default","Clean","Gradient","Minimal","Invert") var template : String = "Default" setget apply_template export (String,"Default","Clean","Gradient","Minimal","Invert") var template : String = "Default" setget apply_template
export (bool) var invert_chart : bool = false
var templates : Dictionary = {} var templates : Dictionary = {}
@ -90,9 +114,9 @@ signal point_pressed(point)
func _ready(): func _ready():
pass apply_template(template)
func _plot(source_ : String, delimiter_ : String, are_values_columns_ : bool, x_values_ : int, invert_xy_ : bool = false): func _plot(source_ : String, delimiter_ : String, are_values_columns_ : bool, x_values_index_ : int, invert_chart_ : bool = false):
randomize() randomize()
load_font() load_font()
@ -100,7 +124,7 @@ func _plot(source_ : String, delimiter_ : String, are_values_columns_ : bool, x_
datas = read_datas(source_,delimiter_) datas = read_datas(source_,delimiter_)
count_functions() count_functions()
structure_datas(datas,are_values_columns_,x_values_) structure_datas(datas,are_values_columns_,x_values_index_)
build_chart() build_chart()
calculate_pass() calculate_pass()
calculate_coordinates() calculate_coordinates()
@ -119,7 +143,7 @@ func plot():
return return
datas = read_datas(source,delimiter) datas = read_datas(source,delimiter)
count_functions() count_functions()
structure_datas(datas,are_values_columns,x_values) structure_datas(datas,are_values_columns,x_values_index)
build_chart() build_chart()
calculate_pass() calculate_pass()
calculate_coordinates() calculate_coordinates()
@ -158,8 +182,8 @@ func read_datas(source : String, delimiter : String):
content.erase(data) content.erase(data)
return content return content
func structure_datas(database : Array, are_values_columns : bool, x_values : int): func structure_datas(database : Array, are_values_columns : bool, x_values_index : int):
# @x_values can be either a column or a row relative to x values # @x_values_index can be either a column or a row relative to x values
# @y_values can be either a column or a row relative to y values # @y_values can be either a column or a row relative to y values
self.are_values_columns = are_values_columns self.are_values_columns = are_values_columns
match are_values_columns: match are_values_columns:
@ -167,11 +191,19 @@ func structure_datas(database : Array, are_values_columns : bool, x_values : int
for row in database.size(): for row in database.size():
var t_vals : Array var t_vals : Array
for column in database[row].size(): for column in database[row].size():
if column == x_values: if column == x_values_index:
x_datas.append(database[row][column]) var x_data = database[row][column]
if x_data.is_valid_float() or x_data.is_valid_integer():
x_datas.append(x_data as float)
else:
x_datas.append(x_data.replace(",",".") as float)
else: else:
if row != 0: if row != 0:
t_vals.append(float(database[row][column])) var y_data = database[row][column]
if y_data.is_valid_float() or y_data.is_valid_integer():
t_vals.append(y_data as float)
else:
t_vals.append(y_data.replace(",",".") as float)
else: else:
y_labels.append(str(database[row][column])) y_labels.append(str(database[row][column]))
if not t_vals.empty(): if not t_vals.empty():
@ -179,7 +211,7 @@ func structure_datas(database : Array, are_values_columns : bool, x_values : int
x_label = str(x_datas.pop_front()) x_label = str(x_datas.pop_front())
false: false:
for row in database.size(): for row in database.size():
if row == x_values: if row == x_values_index:
x_datas = (database[row]) x_datas = (database[row])
x_label = x_datas.pop_front() as String x_label = x_datas.pop_front() as String
else: else:
@ -190,32 +222,64 @@ func structure_datas(database : Array, are_values_columns : bool, x_values : int
for value in data.size(): for value in data.size():
data[value] = data[value] as float data[value] = data[value] as float
# draw y labels
var to_order : Array var to_order : Array
var to_order_min : Array
for cluster in y_datas.size(): for cluster in y_datas.size():
# define x_chors and y_chors # define x_chors and y_chors
var margin = y_datas[cluster][y_datas[cluster].size()-1] var ordered_cluster = y_datas[cluster] as Array
to_order.append(margin) ordered_cluster.sort()
ordered_cluster = PoolIntArray(ordered_cluster)
var margin_max = ordered_cluster[ordered_cluster.size()-1]
var margin_min = ordered_cluster[0]
to_order.append(margin_max)
to_order_min.append(margin_min)
to_order.sort() to_order.sort()
to_order_min.sort()
var margin = to_order.pop_back() var margin = to_order.pop_back()
if not origin_at_zero:
y_margin_min = to_order_min.pop_front()
v_dist = y_decim * pow(10.0,str(margin).length()-2) v_dist = y_decim * pow(10.0,str(margin).length()-2)
var multi = 0 var multi = 0
var p = v_dist*multi var p = (v_dist*multi) + ((y_margin_min) if not origin_at_zero else 0)
y_chors.append(p as String) y_chors.append(p as String)
while p < margin: while p < margin:
multi+=1 multi+=1
p = v_dist*multi p = (v_dist*multi) + ((y_margin_min) if not origin_at_zero else 0)
y_chors.append(p as String) y_chors.append(p as String)
# draw x_labels
if not show_x_values_as_labels:
to_order.clear()
to_order = x_datas as PoolIntArray
to_order.sort()
margin = to_order.pop_back()
if not origin_at_zero:
x_margin_min = to_order.pop_front()
h_dist = x_decim * pow(10.0,str(margin).length()-2)
multi = 0
p = (h_dist*multi) + ((x_margin_min) if not origin_at_zero else 0)
x_labels.append(p as String)
while p < margin:
multi+=1
p = (h_dist*multi) + ((x_margin_min) if not origin_at_zero else 0)
x_labels.append(p as String)
func build_chart(): func build_chart():
SIZE = get_size() SIZE = get_size()
origin = Vector2(OFFSET.x,SIZE.y-OFFSET.y) origin = Vector2(OFFSET.x,SIZE.y-OFFSET.y)
func calculate_pass(): func calculate_pass():
if invert_xy: if invert_chart:
x_chors = y_labels as PoolStringArray x_chors = y_labels as PoolStringArray
else: else:
x_chors = x_datas as PoolStringArray if show_x_values_as_labels:
x_chors = x_datas as PoolStringArray
else:
x_chors = x_labels
# calculate distance in pixel between 2 consecutive values/datas # calculate distance in pixel between 2 consecutive values/datas
x_pass = (SIZE.x - OFFSET.x) / (x_chors.size()-1) x_pass = (SIZE.x - OFFSET.x) / (x_chors.size()-1)
y_pass = origin.y / (y_chors.size()-1) y_pass = origin.y / (y_chors.size()-1)
@ -226,36 +290,60 @@ func calculate_coordinates():
point_values.clear() point_values.clear()
point_positions.clear() point_positions.clear()
if invert_xy: if invert_chart:
for column in y_datas[0].size(): for column in y_datas[0].size():
var single_coordinates : Array var single_coordinates : Array
for row in y_datas: for row in y_datas:
single_coordinates.append((row[column]*y_pass)/v_dist) if origin_at_zero:
single_coordinates.append((row[column]*y_pass)/v_dist)
else:
single_coordinates.append((row[column] - y_margin_min)*y_pass/v_dist)
y_coordinates.append(single_coordinates) y_coordinates.append(single_coordinates)
else: else:
for cluster in y_datas: for cluster in y_datas:
var single_coordinates : Array var single_coordinates : Array
for value in cluster.size(): for value in cluster.size():
single_coordinates.append((cluster[value]*y_pass)/v_dist) if origin_at_zero:
single_coordinates.append((cluster[value]*y_pass)/v_dist)
else:
single_coordinates.append((cluster[value] - y_margin_min)*y_pass/v_dist)
y_coordinates.append(single_coordinates) y_coordinates.append(single_coordinates)
for x in x_chors.size(): if show_x_values_as_labels:
x_coordinates.append(x_pass*x) for x in x_datas.size():
x_coordinates.append(x_pass*x)
else:
for x in x_datas.size():
if origin_at_zero:
if invert_chart:
x_coordinates.append(x_pass*x)
else:
x_coordinates.append(x_datas[x]*x_pass/h_dist)
else:
x_coordinates.append((x_datas[x] - x_margin_min)*x_pass/h_dist)
for f in functions: for f in functions:
point_values.append([]) point_values.append([])
point_positions.append([]) point_positions.append([])
if invert_xy: if invert_chart:
for function in y_coordinates.size()-1: for function in y_coordinates.size():
for function_value in y_coordinates[function].size(): for function_value in y_coordinates[function].size():
point_positions[function].append(Vector2(x_coordinates[function_value]+origin.x,origin.y-y_coordinates[function][function_value])) if are_values_columns:
point_values[function].append([x_chors[function_value],y_datas[function_value][function]]) point_positions[function_value].append(Vector2(x_coordinates[function]+origin.x, origin.y-y_coordinates[function][function_value]))
point_values[function_value].append([x_datas[function_value],y_datas[function_value][function]])
else:
point_positions[function].append(Vector2(x_coordinates[function_value]+origin.x,origin.y-y_coordinates[function][function_value]))
point_values[function].append([x_datas[function_value],y_datas[function_value][function]])
else: else:
for cluster in y_coordinates.size(): for cluster in y_coordinates.size():
for y in y_coordinates[cluster].size(): for y in y_coordinates[cluster].size():
point_values[cluster].append([x_chors[y],y_datas[cluster][y]]) if are_values_columns:
point_positions[cluster].append(Vector2(x_coordinates[y]+origin.x,origin.y-y_coordinates[cluster][y])) point_values[y].append([x_datas[cluster],y_datas[cluster][y]])
point_positions[y].append(Vector2(x_coordinates[cluster]+origin.x,origin.y-y_coordinates[cluster][y]))
else:
point_values[cluster].append([x_datas[y],y_datas[cluster][y]])
point_positions[cluster].append(Vector2(x_coordinates[y]+origin.x,origin.y-y_coordinates[cluster][y]))
func redraw(): func redraw():
build_chart() build_chart()
@ -277,41 +365,21 @@ func _draw():
var PointContainer : Control = Control.new() var PointContainer : Control = Control.new()
Points.add_child(PointContainer) Points.add_child(PointContainer)
if invert_xy: for function_point in point_values[_function].size():
for function_point in point_values[_function].size(): var point : Point = point_node.instance()
var point : Control = point_node.instance() point.connect("_point_pressed",self,"point_pressed")
point.connect("_mouse_entered",self,"show_data",[point]) point.connect("_mouse_entered",self,"show_data")
point.connect("_mouse_exited",self,"hide_data") point.connect("_mouse_exited",self,"hide_data")
point.create_point(function_colors[_function], Color.white, point_positions[_function][function_point],point.format_value(point_values[_function][function_point],false,true),x_datas[_function])
PointContainer.add_child(point) point.create_point(point_shape,function_colors[function_point if invert_chart else _function],
if function_point > 0: Color.white, point_positions[_function][function_point],
draw_line(point_positions[_function][function_point-1],point_positions[_function][function_point],function_colors[_function],2,true) point.format_value(point_values[_function][function_point], false, false),
else: y_labels[function_point if invert_chart else _function] as String)
for function_point in point_values[_function].size():
var point : Control = point_node.instance() PointContainer.add_child(point)
point.connect("_mouse_entered",self,"show_data",[point]) if function_point > 0:
point.connect("_mouse_exited",self,"hide_data") draw_line(point_positions[_function][function_point-1], point_positions[_function][function_point],
point.create_point(function_colors[_function], Color.white, point_positions[_function][function_point],point.format_value(point_values[_function][function_point],false,true),y_labels[_function]) function_colors[function_point if invert_chart else _function], 2, false)
PointContainer.add_child(point)
if function_point > 0:
draw_line(point_positions[_function][function_point-1],point_positions[_function][function_point],function_colors[_function],2,true)
func create_legend():
legend.clear()
for function in functions:
var function_legend = FunctionLegend.instance()
var f_name : String
if invert_xy:
f_name = x_datas[function]
else:
f_name = y_labels[function]
var legend_font : Font
if font != null:
legend_font = font
if bold_font != null:
legend_font = bold_font
function_legend.create_legend(f_name,function_colors[function],bold_font,font_color)
legend.append(function_legend)
func draw_grid(): func draw_grid():
# ascisse # ascisse
@ -333,10 +401,29 @@ func draw_grid():
draw_string(font,point-Vector2(y_chors[p].length()*const_width+font_size,-const_height),y_chors[p],font_color) draw_string(font,point-Vector2(y_chors[p].length()*const_width+font_size,-const_height),y_chors[p],font_color)
func draw_chart_outlines(): func draw_chart_outlines():
draw_line(origin,SIZE-Vector2(0,OFFSET.y),outline_color,1,true) draw_line(origin,SIZE-Vector2(0,OFFSET.y),box_color,1,true)
draw_line(origin,Vector2(OFFSET.x,0),outline_color,1,true) draw_line(origin,Vector2(OFFSET.x,0),box_color,1,true)
draw_line(Vector2(OFFSET.x,0),Vector2(SIZE.x,0),outline_color,1,true) draw_line(Vector2(OFFSET.x,0),Vector2(SIZE.x,0),box_color,1,true)
draw_line(Vector2(SIZE.x,0),SIZE-Vector2(0,OFFSET.y),outline_color,1,true) draw_line(Vector2(SIZE.x,0),SIZE-Vector2(0,OFFSET.y),box_color,1,true)
func create_legend():
legend.clear()
for function in functions:
var function_legend = FunctionLegend.instance()
var f_name : String
if invert_chart:
f_name = x_datas[function] as String
else:
f_name = y_labels[function]
var legend_font : Font
if font != null:
legend_font = font
if bold_font != null:
legend_font = bold_font
function_legend.create_legend(f_name,function_colors[function],bold_font,font_color)
legend.append(function_legend)
var can_grab_x : bool = false var can_grab_x : bool = false
var can_grab_y : bool = false var can_grab_y : bool = false
@ -365,17 +452,17 @@ var range_mouse : float = 7
# if not (event.position.x > SIZE.x-range_mouse+rect_position.x and event.position.x < SIZE.x+range_mouse + rect_position.x) and not (event.position.y > rect_position.y+ origin.y-range_mouse and event.position.y < rect_position.y+origin.y+range_mouse ): # if not (event.position.x > SIZE.x-range_mouse+rect_position.x and event.position.x < SIZE.x+range_mouse + rect_position.x) and not (event.position.y > rect_position.y+ origin.y-range_mouse and event.position.y < rect_position.y+origin.y+range_mouse ):
# set_default_cursor_shape(Control.CURSOR_ARROW) # set_default_cursor_shape(Control.CURSOR_ARROW)
#
func _process(delta): #func _process(delta):
if can_grab_x: # if can_grab_x:
PointData.hide() # PointData.hide()
get_parent().rect_size.x = get_global_mouse_position().x - rect_position.x # get_parent().rect_size.x = get_global_mouse_position().x - rect_position.x
redraw() # redraw()
#
if can_grab_y: # if can_grab_y:
PointData.hide() # PointData.hide()
get_parent().rect_size.y = get_global_mouse_position().y - rect_position.y + OFFSET.y # get_parent().rect_size.y = get_global_mouse_position().y - rect_position.y + OFFSET.y
redraw() # redraw()
func show_data(point): func show_data(point):
PointData.update_datas(point) PointData.update_datas(point)
@ -398,38 +485,31 @@ func get_legend():
return legend return legend
func invert_chart(): func invert_chart():
invert_xy = !invert_xy invert_chart = !invert_chart
count_functions() count_functions()
redraw() redraw()
create_legend() create_legend()
func count_functions(): func count_functions():
if are_values_columns: if are_values_columns:
if not invert_xy: if not invert_chart:
functions = datas[0].size()-1 functions = datas[0].size()-1
else: else:
functions = datas.size()-1 functions = datas.size()-1
else: else:
if invert_xy: if invert_chart:
functions = datas[0].size()-1 functions = datas[0].size()-1
else: else:
functions = datas.size()-1 functions = datas.size()-1
func apply_template(template_name : String): func apply_template(template_name : String):
if Engine.editor_hint: template = template_name
if template_name!=null and template_name!="":
template = template_name
var custom_template = templates[template_name.to_lower()]
function_colors = custom_template.function_colors
v_lines_color = Color(custom_template.v_lines_color)
h_lines_color = Color(custom_template.h_lines_color)
outline_color = Color(custom_template.outline_color)
font_color = Color(custom_template.font_color)
property_list_changed_notify()
func _script_changed():
_ready()
func _enter_tree():
templates = Utilities._load_templates() templates = Utilities._load_templates()
if template_name!=null and template_name!="":
var custom_template = templates[template.to_lower()]
function_colors = custom_template.function_colors
v_lines_color = Color(custom_template.v_lines_color)
h_lines_color = Color(custom_template.h_lines_color)
box_color = Color(custom_template.outline_color)
font_color = Color(custom_template.font_color)
property_list_changed_notify()

View File

@ -12,8 +12,19 @@ rect_min_size = Vector2( 70, 50 )
mouse_filter = 2 mouse_filter = 2
script = ExtResource( 4 ) script = ExtResource( 4 )
__meta__ = { __meta__ = {
"_edit_use_anchors_": false "_edit_use_anchors_": false,
"_editor_description_": "[Linechart] - General purpose node for Line Charts
A line chart or line plot or line graph or curve chart is a type of chart which
displays information as a series of data points called 'markers'
connected by straight line segments.
It is a basic type of chart common in many fields. It is similar to a scatter plot
except that the measurement points are ordered (typically by their x-axis value)
and joined with straight line segments.
A line chart is often used to visualize a trend in data over intervals of time
a time series thus the line is often drawn chronologically.
In these cases they are known as run charts."
} }
function_colors = [ "#1e1e1e", "#1e1e1e", "#1e1e1e", "#1e1e1e" ]
[node name="Background" type="ColorRect" parent="."] [node name="Background" type="ColorRect" parent="."]
visible = false visible = false
@ -46,6 +57,5 @@ __meta__ = {
[node name="PointData" parent="PointData" index="0"] [node name="PointData" parent="PointData" index="0"]
visible = false visible = false
theme = SubResource( 1 ) theme = SubResource( 1 )
[connection signal="script_changed" from="." to="." method="_script_changed"]
[editable path="PointData"] [editable path="PointData"]

View File

@ -3,7 +3,6 @@ extends Node2D
""" """
[Linechart2D] - General purpose node for Line Charts [Linechart2D] - General purpose node for Line Charts
A line chart or line plot or line graph or curve chart is a type of chart which A line chart or line plot or line graph or curve chart is a type of chart which
displays information as a series of data points called 'markers' displays information as a series of data points called 'markers'
connected by straight line segments. connected by straight line segments.
@ -13,14 +12,14 @@ and joined with straight line segments.
A line chart is often used to visualize a trend in data over intervals of time A line chart is often used to visualize a trend in data over intervals of time
a time series thus the line is often drawn chronologically. a time series thus the line is often drawn chronologically.
In these cases they are known as run charts. In these cases they are known as run charts.
/ source : Wikipedia / / source : Wikipedia /
""" """
onready var FunctionsTween : Tween = $FunctionsTween
onready var OutlinesTween : Tween = $OutlinesTween onready var OutlinesTween : Tween = $OutlinesTween
onready var GridTween : Tween = $GridTween onready var FunctionsTween : Tween = $FunctionsTween
onready var Functions : Node2D = $Functions onready var Functions : Node2D = $Functions
onready var GridTween : Tween = $GridTween
onready var PointData = $PointData/PointData onready var PointData = $PointData/PointData
onready var Outlines : Line2D = $Outlines onready var Outlines : Line2D = $Outlines
onready var Grid : Node2D = $Grid onready var Grid : Node2D = $Grid
@ -43,9 +42,9 @@ var y_pass : float
# vertical distance between y consecutive points used for intervals # vertical distance between y consecutive points used for intervals
var v_dist : float var v_dist : float
var h_dist : float
# quantization, representing the interval in which values will be displayed # quantization, representing the interval in which values will be displayed
var x_decim : float = 1.0
# define values on x an y axis # define values on x an y axis
var x_chors : Array var x_chors : Array
@ -61,15 +60,19 @@ var datas : Array
# amount of functions to represent # amount of functions to represent
var functions : int = 0 var functions : int = 0
var x_label : String
# database values # database values
var x_datas : Array var x_datas : Array
var y_datas : Array var y_datas : Array
# labels displayed on chart # labels displayed on chart
var x_label : String var x_labels : Array
var y_labels : Array var y_labels : Array
var x_margin_min : int = 0
var y_margin_min : int = 0
# actual values of point, from the database # actual values of point, from the database
var point_values : Array var point_values : Array
@ -79,30 +82,33 @@ var point_positions : Array
var legend : Array setget set_legend,get_legend var legend : Array setget set_legend,get_legend
# --------------------- # ---------------------
export (Vector2) var SIZE : Vector2 = Vector2() export (Vector2) var SIZE : Vector2 = Vector2() setget _set_size
export (String, FILE) var source : String = "" export (String, FILE, "*.txt, *.csv") var source : String = ""
export (String) var delimiter : String = ";" export (String) var delimiter : String = ";"
export (bool) var origin_at_zero : bool = true
export (bool) var are_values_columns : bool = false export (bool) var are_values_columns : bool = false
export (bool) var invert_xy : bool = false export (int,0,100) var x_values_index : int = 0
export(bool) var show_x_values_as_labels : bool = true
export (int,0,100) var x_values : int = 0 #export (float,1,20,0.5) var column_width : float = 10
#export (float,0,10,0.5) var column_gap : float = 2
export (float,1,20,0.5) var column_width : float = 10 export (float,0.1,10.0) var x_decim : float = 5.0
export (float,0,10,0.5) var column_gap : float = 2 export (float,0.1,10.0) var y_decim : float = 5.0
export (int,"Dot,Triangle,Square") var point_shape : int = 0
export (float,0,10) var y_decim : float = 5.0
export (PoolColorArray) var function_colors = [Color("#1e1e1e")] export (PoolColorArray) var function_colors = [Color("#1e1e1e")]
export (bool) var boxed : bool = true
export (Color) var v_lines_color : Color = Color("#cacaca") export (Color) var v_lines_color : Color = Color("#cacaca")
export (Color) var h_lines_color : Color = Color("#cacaca") export (Color) var h_lines_color : Color = Color("#cacaca")
export (Color) var outline_color : Color = Color("#1e1e1e")
export (float,0.01,1) var drawing_duration : float = 0.3 export (bool) var boxed : bool = true
export (Color) var box_color : Color = Color("#1e1e1e")
export (Font) var font : Font export (Font) var font : Font
export (Font) var bold_font : Font export (Font) var bold_font : Font
export (Color) var font_color : Color = Color("#1e1e1e") export (Color) var font_color : Color = Color("#1e1e1e")
export (String,"Default","Clean","Gradient","Minimal","Invert") var template : String = "Default" setget apply_template export (String,"Default","Clean","Gradient","Minimal","Invert") var template : String = "Default" setget apply_template
export (float,0.1,1) var drawing_duration : float = 0.5
export (bool) var invert_chart : bool = false
var templates : Dictionary = {} var templates : Dictionary = {}
@ -115,44 +121,25 @@ func _point_plotted():
func _ready(): func _ready():
pass pass
func _script_changed(): func _set_size(size : Vector2):
_ready() SIZE = size
build_chart()
func _process(delta):
if Engine.editor_hint: if Engine.editor_hint:
Outlines.set_default_color(outline_color) Outlines.set_point_position(0,Vector2(origin.x,0))
Grid.get_node("VLine").set_default_color(v_lines_color) Outlines.set_point_position(1,Vector2(SIZE.x,0))
Grid.get_node("HLine").set_default_color(h_lines_color) Outlines.set_point_position(2,Vector2(SIZE.x,origin.y))
if function_colors.size(): Outlines.set_point_position(3,origin)
Functions.get_node("Function").set_default_color(function_colors[0]) Outlines.set_point_position(4,Vector2(origin.x,0))
else:
Functions.get_node("Function").set_default_color(Color("#1e1e1e"))
if SIZE!=Vector2(): Grid.get_node("VLine").set_point_position(0,Vector2((OFFSET.x+SIZE.x)/2,0))
build_chart() Grid.get_node("VLine").set_point_position(1,Vector2((OFFSET.x+SIZE.x)/2,origin.y))
Outlines.set_point_position(0,Vector2(origin.x,0)) Grid.get_node("HLine").set_point_position(0,Vector2(origin.x,origin.y/2))
Outlines.set_point_position(1,Vector2(SIZE.x,0)) Grid.get_node("HLine").set_point_position(1,Vector2(SIZE.x,origin.y/2))
Outlines.set_point_position(2,Vector2(SIZE.x,origin.y))
Outlines.set_point_position(3,origin)
Outlines.set_point_position(4,Vector2(origin.x,0))
Grid.get_node("VLine").set_point_position(0,Vector2((OFFSET.x+SIZE.x)/2,0))
Grid.get_node("VLine").set_point_position(1,Vector2((OFFSET.x+SIZE.x)/2,origin.y))
Grid.get_node("HLine").set_point_position(0,Vector2(origin.x,origin.y/2))
Grid.get_node("HLine").set_point_position(1,Vector2(SIZE.x,origin.y/2))
if function_colors.size():
Functions.get_node("Function").set_point_position(0,origin)
Functions.get_node("Function").set_point_position(1,Vector2(SIZE.x,0))
#func _ready():
# plot_line_chart("res://ChartNode/datas2.csv",";",false,0,invert_xy,function_colors,drawing_duration,SIZE)
func clear(): func clear():
Outlines.points = [] Outlines.points = []
Grid.get_node("HLine").queue_free() Grid.get_node("HLine").queue_free()
Grid.get_node("VLine").queue_free() Grid.get_node("VLine").queue_free()
Functions.get_node("Function").queue_free()
func load_font(): func load_font():
if font != null: if font != null:
@ -160,10 +147,14 @@ func load_font():
var theme : Theme = Theme.new() var theme : Theme = Theme.new()
theme.set_default_font(font) theme.set_default_font(font)
PointData.set_theme(theme) PointData.set_theme(theme)
else:
var lbl = Label.new()
font = lbl.get_font("")
lbl.free()
if bold_font != null: if bold_font != null:
PointData.Data.set("custom_fonts/font",bold_font) PointData.Data.set("custom_fonts/font",bold_font)
func _plot(source_ : String, delimiter_ : String, are_values_columns_ : bool, x_values_ : int): func _plot(source_ : String, delimiter_ : String, are_values_columns_ : bool, x_values_index_ : int):
randomize() randomize()
clear() clear()
@ -173,7 +164,7 @@ func _plot(source_ : String, delimiter_ : String, are_values_columns_ : bool, x_
datas = read_datas(source_,delimiter_) datas = read_datas(source_,delimiter_)
count_functions() count_functions()
structure_datas(datas,are_values_columns_,x_values_) structure_datas(datas,are_values_columns_,x_values_index_)
build_chart() build_chart()
calculate_pass() calculate_pass()
calculate_coordinates() calculate_coordinates()
@ -196,7 +187,7 @@ func plot():
return return
datas = read_datas(source,delimiter) datas = read_datas(source,delimiter)
count_functions() count_functions()
structure_datas(datas,are_values_columns,x_values) structure_datas(datas,are_values_columns,x_values_index)
build_chart() build_chart()
calculate_pass() calculate_pass()
calculate_coordinates() calculate_coordinates()
@ -219,7 +210,7 @@ func draw_chart():
func draw_outlines(): func draw_outlines():
if boxed: if boxed:
Outlines.set_default_color(outline_color) Outlines.set_default_color(box_color)
OutlinesTween.interpolate_method(Outlines,"add_point", OutlinesTween.interpolate_method(Outlines,"add_point",
Vector2(origin.x,0),Vector2(SIZE.x,0),drawing_duration*0.5,Tween.TRANS_QUINT,Tween.EASE_OUT) Vector2(origin.x,0),Vector2(SIZE.x,0),drawing_duration*0.5,Tween.TRANS_QUINT,Tween.EASE_OUT)
OutlinesTween.start() OutlinesTween.start()
@ -279,27 +270,29 @@ func draw_function(f_index : int, function : Array):
var line : Line2D = Line2D.new() var line : Line2D = Line2D.new()
var backline : Line2D = Line2D.new() var backline : Line2D = Line2D.new()
construct_line(line,backline,f_index,function) construct_line(line,backline,f_index,function)
var pointv : Control var pointv : Point
for point in function.size(): for point in function.size():
pointv = point_node.instance() pointv = point_node.instance()
line.add_child(pointv) Functions.add_child(pointv)
pointv.connect("_mouse_entered",self,"show_data",[pointv]) pointv.connect("_mouse_entered",self,"show_data")
pointv.connect("_mouse_exited",self,"hide_data") pointv.connect("_mouse_exited",self,"hide_data")
pointv.connect("_point_pressed",self,"point_pressed") pointv.connect("_point_pressed",self,"point_pressed")
pointv.create_point(function_colors[f_index], Color.white, function[point],pointv.format_value(point_values[f_index][point],false,true),(x_datas[f_index] if invert_xy else y_labels[f_index])) pointv.create_point(point_shape, function_colors[f_index], Color.white, function[point],
pointv.format_value(point_values[f_index][point],false,false),
y_labels[point if invert_chart else f_index] as String)
if point < function.size()-1: if point < function.size()-1:
FunctionsTween.interpolate_method(line,"add_point",function[point],function[point+1],drawing_duration/function.size(),Tween.TRANS_QUINT,Tween.EASE_OUT) FunctionsTween.interpolate_method(line,"add_point",function[point],function[point+1],drawing_duration/function.size(),Tween.TRANS_QUINT,Tween.EASE_OUT)
# FunctionsTween.interpolate_method(backline,"add_point",function[point],function[point+1],drawing_duration/function.size(),Tween.TRANS_QUINT,Tween.EASE_OUT)
FunctionsTween.start() FunctionsTween.start()
yield(FunctionsTween,"tween_all_completed") yield(FunctionsTween,"tween_all_completed")
func construct_line(line : Line2D, backline : Line2D, f_index : int, function : Array): func construct_line(line : Line2D, backline : Line2D, f_index : int, function : Array):
var midtone = Color(Color(function_colors[f_index]).r,Color(function_colors[f_index]).g,Color(function_colors[f_index]).b,Color(function_colors[f_index]).a/2) var midtone = Color(Color(function_colors[f_index]).r,Color(function_colors[f_index]).g,Color(function_colors[f_index]).b,Color(function_colors[f_index]).a/2)
backline.set_width(3) backline.set_width(3)
backline.set_default_color(midtone) backline.set_default_color(midtone)
backline.antialiased = true backline.antialiased = true
Functions.add_child(backline) Functions.add_child(backline)
line.set_width(4) line.set_width(2)
line.set_default_color(function_colors[f_index]) line.set_default_color(function_colors[f_index])
line.antialiased = true line.antialiased = true
Functions.add_child(line) Functions.add_child(line)
@ -317,8 +310,8 @@ func read_datas(source : String, delimiter : String):
content.erase(data) content.erase(data)
return content return content
func structure_datas(database : Array, are_values_columns : bool, x_values : int): func structure_datas(database : Array, are_values_columns : bool, x_values_index : int):
# @x_values can be either a column or a row relative to x values # @x_values_index can be either a column or a row relative to x values
# @y_values can be either a column or a row relative to y values # @y_values can be either a column or a row relative to y values
self.are_values_columns = are_values_columns self.are_values_columns = are_values_columns
match are_values_columns: match are_values_columns:
@ -326,11 +319,19 @@ func structure_datas(database : Array, are_values_columns : bool, x_values : int
for row in database.size(): for row in database.size():
var t_vals : Array var t_vals : Array
for column in database[row].size(): for column in database[row].size():
if column == x_values: if column == x_values_index:
x_datas.append(database[row][column]) var x_data = database[row][column]
if x_data.is_valid_float() or x_data.is_valid_integer():
x_datas.append(x_data as float)
else:
x_datas.append(x_data.replace(",",".") as float)
else: else:
if row != 0: if row != 0:
t_vals.append(float(database[row][column])) var y_data = database[row][column]
if y_data.is_valid_float() or y_data.is_valid_integer():
t_vals.append(y_data as float)
else:
t_vals.append(y_data.replace(",",".") as float)
else: else:
y_labels.append(str(database[row][column])) y_labels.append(str(database[row][column]))
if not t_vals.empty(): if not t_vals.empty():
@ -338,7 +339,7 @@ func structure_datas(database : Array, are_values_columns : bool, x_values : int
x_label = str(x_datas.pop_front()) x_label = str(x_datas.pop_front())
false: false:
for row in database.size(): for row in database.size():
if row == x_values: if row == x_values_index:
x_datas = (database[row]) x_datas = (database[row])
x_label = x_datas.pop_front() as String x_label = x_datas.pop_front() as String
else: else:
@ -349,31 +350,63 @@ func structure_datas(database : Array, are_values_columns : bool, x_values : int
for value in data.size(): for value in data.size():
data[value] = data[value] as float data[value] = data[value] as float
# draw y labels
var to_order : Array var to_order : Array
var to_order_min : Array
for cluster in y_datas.size(): for cluster in y_datas.size():
# define x_chors and y_chors # define x_chors and y_chors
var margin = y_datas[cluster][y_datas[cluster].size()-1] var ordered_cluster = y_datas[cluster] as Array
to_order.append(margin) ordered_cluster.sort()
ordered_cluster = PoolIntArray(ordered_cluster)
var margin_max = ordered_cluster[ordered_cluster.size()-1]
var margin_min = ordered_cluster[0]
to_order.append(margin_max)
to_order_min.append(margin_min)
to_order.sort() to_order.sort()
to_order_min.sort()
var margin = to_order.pop_back() var margin = to_order.pop_back()
if not origin_at_zero:
y_margin_min = to_order_min.pop_front()
v_dist = y_decim * pow(10.0,str(margin).length()-2) v_dist = y_decim * pow(10.0,str(margin).length()-2)
var multi = 0 var multi = 0
var p = v_dist*multi var p = (v_dist*multi) + ((y_margin_min) if not origin_at_zero else 0)
y_chors.append(p as String) y_chors.append(p as String)
while p < margin: while p < margin:
multi+=1 multi+=1
p = v_dist*multi p = (v_dist*multi) + ((y_margin_min) if not origin_at_zero else 0)
y_chors.append(p as String) y_chors.append(p as String)
# draw x_labels
if not show_x_values_as_labels:
to_order.clear()
to_order = x_datas as PoolIntArray
to_order.sort()
margin = to_order.pop_back()
if not origin_at_zero:
x_margin_min = to_order.pop_front()
h_dist = x_decim * pow(10.0,str(margin).length()-2)
multi = 0
p = (h_dist*multi) + ((x_margin_min) if not origin_at_zero else 0)
x_labels.append(p as String)
while p < margin:
multi+=1
p = (h_dist*multi) + ((x_margin_min) if not origin_at_zero else 0)
x_labels.append(p as String)
func build_chart(): func build_chart():
origin = Vector2(OFFSET.x,SIZE.y-OFFSET.y) origin = Vector2(OFFSET.x,SIZE.y-OFFSET.y)
func calculate_pass(): func calculate_pass():
if invert_xy: if invert_chart:
x_chors = y_labels as PoolStringArray x_chors = y_labels as PoolStringArray
else: else:
x_chors = x_datas as PoolStringArray if show_x_values_as_labels:
x_chors = x_datas as PoolStringArray
else:
x_chors = x_labels
# calculate distance in pixel between 2 consecutive values/datas # calculate distance in pixel between 2 consecutive values/datas
x_pass = (SIZE.x - OFFSET.x) / (x_chors.size()-1) x_pass = (SIZE.x - OFFSET.x) / (x_chors.size()-1)
y_pass = origin.y / (y_chors.size()-1) y_pass = origin.y / (y_chors.size()-1)
@ -384,58 +417,67 @@ func calculate_coordinates():
point_values.clear() point_values.clear()
point_positions.clear() point_positions.clear()
if invert_xy: if invert_chart:
for column in y_datas[0].size(): for column in y_datas[0].size():
var single_coordinates : Array var single_coordinates : Array
for row in y_datas: for row in y_datas:
single_coordinates.append((row[column]*y_pass)/v_dist) if origin_at_zero:
single_coordinates.append((row[column]*y_pass)/v_dist)
else:
single_coordinates.append((row[column] - y_margin_min)*y_pass/v_dist)
y_coordinates.append(single_coordinates) y_coordinates.append(single_coordinates)
else: else:
for cluster in y_datas: for cluster in y_datas:
var single_coordinates : Array var single_coordinates : Array
for value in cluster.size(): for value in cluster.size():
single_coordinates.append((cluster[value]*y_pass)/v_dist) if origin_at_zero:
single_coordinates.append((cluster[value]*y_pass)/v_dist)
else:
single_coordinates.append((cluster[value] - y_margin_min)*y_pass/v_dist)
y_coordinates.append(single_coordinates) y_coordinates.append(single_coordinates)
for x in x_chors.size(): if show_x_values_as_labels:
x_coordinates.append(x_pass*x) for x in x_datas.size():
x_coordinates.append(x_pass*x)
else:
for x in x_datas.size():
if origin_at_zero:
if invert_chart:
x_coordinates.append(x_pass*x)
else:
x_coordinates.append(x_datas[x]*x_pass/h_dist)
else:
x_coordinates.append((x_datas[x] - x_margin_min)*x_pass/h_dist)
for f in functions: for f in functions:
point_values.append([]) point_values.append([])
point_positions.append([]) point_positions.append([])
if invert_xy: if invert_chart:
for function in y_coordinates.size()-1: for function in y_coordinates.size():
for function_value in y_coordinates[function].size(): for function_value in y_coordinates[function].size():
point_positions[function].append(Vector2(x_coordinates[function_value]+origin.x,origin.y-y_coordinates[function][function_value])) if are_values_columns:
point_values[function].append([x_chors[function_value],y_datas[function_value][function]]) point_positions[function_value].append(Vector2(x_coordinates[function]+origin.x, origin.y-y_coordinates[function][function_value]))
point_values[function_value].append([x_datas[function_value],y_datas[function_value][function]])
else:
point_positions[function].append(Vector2(x_coordinates[function_value]+origin.x,origin.y-y_coordinates[function][function_value]))
point_values[function].append([x_datas[function_value],y_datas[function_value][function]])
else: else:
for cluster in y_coordinates.size(): for cluster in y_coordinates.size():
for y in y_coordinates[cluster].size(): for y in y_coordinates[cluster].size():
point_values[cluster].append([x_chors[y],y_datas[cluster][y]]) if are_values_columns:
point_positions[cluster].append(Vector2(x_coordinates[y]+origin.x,origin.y-y_coordinates[cluster][y])) point_values[y].append([x_datas[cluster],y_datas[cluster][y]])
point_positions[y].append(Vector2(x_coordinates[cluster]+origin.x,origin.y-y_coordinates[cluster][y]))
else:
point_values[cluster].append([x_datas[y],y_datas[cluster][y]])
point_positions[cluster].append(Vector2(x_coordinates[y]+origin.x,origin.y-y_coordinates[cluster][y]))
func redraw(): func redraw():
pass build_chart()
calculate_pass()
calculate_coordinates()
update()
func invert_chart():
invert_xy = !invert_xy
count_functions()
redraw()
create_legend()
func count_functions():
if are_values_columns:
if not invert_xy:
functions = datas[0].size()-1
else:
functions = datas.size()-1
else:
if invert_xy:
functions = datas[0].size()-1
else:
functions = datas.size()-1
func show_data(point): func show_data(point):
PointData.update_datas(point) PointData.update_datas(point)
@ -450,13 +492,37 @@ func clear_points():
for function in Functions.get_children(): for function in Functions.get_children():
function.queue_free() function.queue_free()
func set_legend(l : Array):
legend = l
func get_legend():
return legend
func invert_chart():
invert_chart = !invert_chart
count_functions()
redraw()
create_legend()
func count_functions():
if are_values_columns:
if not invert_chart:
functions = datas[0].size()-1
else:
functions = datas.size()-1
else:
if invert_chart:
functions = datas[0].size()-1
else:
functions = datas.size()-1
func create_legend(): func create_legend():
legend.clear() legend.clear()
for function in functions: for function in functions:
var function_legend = FunctionLegend.instance() var function_legend = FunctionLegend.instance()
var f_name : String var f_name : String
if invert_xy: if invert_chart:
f_name = x_datas[function] f_name = x_datas[function] as String
else: else:
f_name = y_labels[function] f_name = y_labels[function]
var legend_font : Font var legend_font : Font
@ -467,28 +533,19 @@ func create_legend():
function_legend.create_legend(f_name,function_colors[function],bold_font,font_color) function_legend.create_legend(f_name,function_colors[function],bold_font,font_color)
legend.append(function_legend) legend.append(function_legend)
func set_legend(l : Array):
legend = l
func get_legend() -> Array:
return legend
func apply_template(template_name : String): func apply_template(template_name : String):
if Engine.editor_hint: template = template_name
if template_name!=null and template_name!="":
template = template_name
var custom_template = templates[template_name.to_lower()]
function_colors.resize(0)
for color in custom_template.function_colors:
function_colors.append(Color(color))
v_lines_color = Color(custom_template.v_lines_color)
h_lines_color = Color(custom_template.h_lines_color)
outline_color = Color(custom_template.outline_color)
font_color = Color(custom_template.font_color)
property_list_changed_notify()
func point_pressed(point : Point):
emit_signal("point_pressed",point)
func _enter_tree():
templates = Utilities._load_templates() templates = Utilities._load_templates()
if template_name!=null and template_name!="":
var custom_template = templates[template.to_lower()]
function_colors = custom_template.function_colors
v_lines_color = Color(custom_template.v_lines_color)
h_lines_color = Color(custom_template.h_lines_color)
box_color = Color(custom_template.outline_color)
font_color = Color(custom_template.font_color)
property_list_changed_notify()
if Engine.editor_hint:
Outlines.set_default_color(box_color)
Grid.get_node("VLine").set_default_color(v_lines_color)
Grid.get_node("HLine").set_default_color(h_lines_color)

View File

@ -4,11 +4,22 @@
[ext_resource path="res://addons/easy_charts/Utilities/Point/PointData.tscn" type="PackedScene" id=2] [ext_resource path="res://addons/easy_charts/Utilities/Point/PointData.tscn" type="PackedScene" id=2]
[node name="LineChart2D" type="Node2D"] [node name="LineChart2D" type="Node2D"]
position = Vector2( -1, 0 )
script = ExtResource( 1 ) script = ExtResource( 1 )
__meta__ = { __meta__ = {
"_edit_group_": true "_edit_group_": true,
"_editor_description_": "[Linechart2D] - General purpose node for Line Charts
A line chart or line plot or line graph or curve chart is a type of chart which
displays information as a series of data points called 'markers'
connected by straight line segments.
It is a basic type of chart common in many fields. It is similar to a scatter plot
except that the measurement points are ordered (typically by their x-axis value)
and joined with straight line segments.
A line chart is often used to visualize a trend in data over intervals of time
a time series thus the line is often drawn chronologically.
In these cases they are known as run charts."
} }
font_color = Color( 0.137255, 0.137255, 0.137255, 1 ) function_colors = [ "#1e1e1e", "#1e1e1e", "#1e1e1e", "#1e1e1e" ]
[node name="Grid" type="Node2D" parent="."] [node name="Grid" type="Node2D" parent="."]
@ -44,6 +55,5 @@ default_color = Color( 0.117647, 0.117647, 0.117647, 1 )
[node name="PointData" parent="PointData" index="0"] [node name="PointData" parent="PointData" index="0"]
visible = false visible = false
[connection signal="script_changed" from="." to="." method="_script_changed"]
[editable path="PointData"] [editable path="PointData"]

View File

@ -0,0 +1,508 @@
tool
extends Control
"""
[ScatterChart] - General purpose node for Scatter Charts
A scatter plot (also called a scatterplot, scatter graph, scatter chart, scattergram, or scatter diagram)
is a type of plot or mathematical diagram using Cartesian coordinates to display values for typically two variables
for a set of data. If the points are coded (color/shape/size), one additional variable can be displayed.
The data are displayed as a collection of points, each having the value of one variable determining the position on
the horizontal axis and the value of the other variable determining the position on the vertical axis.
/ source : Wikipedia /
"""
onready var PointData = $PointData/PointData
onready var Points = $Points
onready var Legend = $Legend
var point_node : PackedScene = preload("../Utilities/Point/Point.tscn")
var FunctionLegend : PackedScene = preload("../Utilities/Legend/FunctionLegend.tscn")
var font_size : float = 16
var const_height : float = font_size/2*font_size/20
var const_width : float = font_size/2
var OFFSET : Vector2 = Vector2(0,0)
#-------------------------------------------------------------------------#
var origin : Vector2
# actual distance between x and y values
var x_pass : float
var y_pass : float
# vertical distance between y consecutive points used for intervals
var v_dist : float
var h_dist : float
# quantization, representing the interval in which values will be displayed
# define values on x an y axis
var x_chors : Array
var y_chors : Array
# actual coordinates of points (in pixel)
var x_coordinates : Array
var y_coordinates : Array
# datas contained in file
var datas : Array
# amount of functions to represent
var functions : int = 0
# database values
var x_datas : Array
var y_datas : Array
# labels displayed on chart
var x_label : String
var x_labels : Array
var y_labels : Array
var x_margin_min : int = 0
var y_margin_min : int = 0
# actual values of point, from the database
var point_values : Array
# actual position of points in pixel
var point_positions : Array
var legend : Array setget set_legend,get_legend
# ---------------------
var SIZE : Vector2 = Vector2()
export (String, FILE, "*.txt, *.csv") var source : String = ""
export (String) var delimiter : String = ";"
export (bool) var origin_at_zero : bool = true
export (bool) var are_values_columns : bool = false
export (int,0,100) var x_values_index : int = 0
export(bool) var show_x_values_as_labels : bool = true
#export (float,1,20,0.5) var column_width : float = 10
#export (float,0,10,0.5) var column_gap : float = 2
export (float,0.1,10.0) var x_decim : float = 5.0
export (float,0.1,10.0) var y_decim : float = 5.0
export (int,"Dot,Triangle,Square") var point_shape : int = 0
export (PoolColorArray) var function_colors = [Color("#1e1e1e")]
export (Color) var v_lines_color : Color = Color("#cacaca")
export (Color) var h_lines_color : Color = Color("#cacaca")
export (bool) var boxed : bool = true
export (Color) var box_color : Color = Color("#1e1e1e")
export (Font) var font : Font
export (Font) var bold_font : Font
export (Color) var font_color : Color = Color("#1e1e1e")
export (String,"Default","Clean","Gradient","Minimal","Invert") var template : String = "Default" setget apply_template
export (bool) var invert_chart : bool = false
var templates : Dictionary = {}
signal chart_plotted(chart)
signal point_pressed(point)
func _ready():
apply_template(template)
func _plot(source_ : String, delimiter_ : String, are_values_columns_ : bool, x_values_index_ : int, invert_chart_ : bool = false):
randomize()
load_font()
PointData.hide()
datas = read_datas(source_,delimiter_)
count_functions()
structure_datas(datas,are_values_columns_,x_values_index_)
build_chart()
calculate_pass()
calculate_coordinates()
calculate_colors()
create_legend()
emit_signal("chart_plotted")
func plot():
randomize()
load_font()
PointData.hide()
if source == "" or source == null:
Utilities._print_message("Can't plot a chart without a Source file. Please, choose it in editor, or use the custom function _plot().",1)
return
datas = read_datas(source,delimiter)
count_functions()
structure_datas(datas,are_values_columns,x_values_index)
build_chart()
calculate_pass()
calculate_coordinates()
calculate_colors()
create_legend()
emit_signal("chart_plotted")
func calculate_colors():
if function_colors.empty() or function_colors.size() < functions:
for function in functions:
function_colors.append(Color("#1e1e1e"))
func load_font():
if font != null:
font_size = font.get_height()
var theme : Theme = Theme.new()
theme.set_default_font(font)
PointData.set_theme(theme)
else:
var lbl = Label.new()
font = lbl.get_font("")
lbl.free()
if bold_font != null:
PointData.Data.set("custom_fonts/font",bold_font)
func read_datas(source : String, delimiter : String):
var file : File = File.new()
file.open(source,File.READ)
var content : Array
while not file.eof_reached():
var line : PoolStringArray = file.get_csv_line(delimiter)
content.append(line)
file.close()
for data in content:
if data.size() < 2:
content.erase(data)
return content
func structure_datas(database : Array, are_values_columns : bool, x_values_index : int):
# @x_values_index can be either a column or a row relative to x values
# @y_values can be either a column or a row relative to y values
self.are_values_columns = are_values_columns
match are_values_columns:
true:
for row in database.size():
var t_vals : Array
for column in database[row].size():
if column == x_values_index:
var x_data = database[row][column]
if x_data.is_valid_float() or x_data.is_valid_integer():
x_datas.append(x_data as float)
else:
x_datas.append(x_data.replace(",",".") as float)
else:
if row != 0:
var y_data = database[row][column]
if y_data.is_valid_float() or y_data.is_valid_integer():
t_vals.append(y_data as float)
else:
t_vals.append(y_data.replace(",",".") as float)
else:
y_labels.append(str(database[row][column]))
if not t_vals.empty():
y_datas.append(t_vals)
x_label = str(x_datas.pop_front())
false:
for row in database.size():
if row == x_values_index:
x_datas = (database[row])
x_label = x_datas.pop_front() as String
else:
var values = database[row] as Array
y_labels.append(values.pop_front() as String)
y_datas.append(values)
for data in y_datas:
for value in data.size():
data[value] = data[value] as float
# draw y labels
var to_order : Array
var to_order_min : Array
for cluster in y_datas.size():
# define x_chors and y_chors
var ordered_cluster = y_datas[cluster] as Array
ordered_cluster.sort()
ordered_cluster = PoolIntArray(ordered_cluster)
var margin_max = ordered_cluster[ordered_cluster.size()-1]
var margin_min = ordered_cluster[0]
to_order.append(margin_max)
to_order_min.append(margin_min)
to_order.sort()
to_order_min.sort()
var margin = to_order.pop_back()
if not origin_at_zero:
y_margin_min = to_order_min.pop_front()
v_dist = y_decim * pow(10.0,str(margin).length()-2)
var multi = 0
var p = (v_dist*multi) + ((y_margin_min) if not origin_at_zero else 0)
y_chors.append(p as String)
while p < margin:
multi+=1
p = (v_dist*multi) + ((y_margin_min) if not origin_at_zero else 0)
y_chors.append(p as String)
# draw x_labels
if not show_x_values_as_labels:
to_order.clear()
to_order = x_datas as PoolIntArray
to_order.sort()
margin = to_order.pop_back()
if not origin_at_zero:
x_margin_min = to_order.pop_front()
h_dist = x_decim * pow(10.0,str(margin).length()-2)
multi = 0
p = (h_dist*multi) + ((x_margin_min) if not origin_at_zero else 0)
x_labels.append(p as String)
while p < margin:
multi+=1
p = (h_dist*multi) + ((x_margin_min) if not origin_at_zero else 0)
x_labels.append(p as String)
func build_chart():
SIZE = get_size()
origin = Vector2(OFFSET.x,SIZE.y-OFFSET.y)
func calculate_pass():
if invert_chart:
x_chors = y_labels as PoolStringArray
else:
if show_x_values_as_labels:
x_chors = x_datas as PoolStringArray
else:
x_chors = x_labels
# calculate distance in pixel between 2 consecutive values/datas
x_pass = (SIZE.x - OFFSET.x) / (x_chors.size()-1)
y_pass = origin.y / (y_chors.size()-1)
func calculate_coordinates():
x_coordinates.clear()
y_coordinates.clear()
point_values.clear()
point_positions.clear()
if invert_chart:
for column in y_datas[0].size():
var single_coordinates : Array
for row in y_datas:
if origin_at_zero:
single_coordinates.append((row[column]*y_pass)/v_dist)
else:
single_coordinates.append((row[column] - y_margin_min)*y_pass/v_dist)
y_coordinates.append(single_coordinates)
else:
for cluster in y_datas:
var single_coordinates : Array
for value in cluster.size():
if origin_at_zero:
single_coordinates.append((cluster[value]*y_pass)/v_dist)
else:
single_coordinates.append((cluster[value] - y_margin_min)*y_pass/v_dist)
y_coordinates.append(single_coordinates)
if show_x_values_as_labels:
for x in x_datas.size():
x_coordinates.append(x_pass*x)
else:
for x in x_datas.size():
if origin_at_zero:
if invert_chart:
x_coordinates.append(x_pass*x)
else:
x_coordinates.append(x_datas[x]*x_pass/h_dist)
else:
x_coordinates.append((x_datas[x] - x_margin_min)*x_pass/h_dist)
for f in functions:
point_values.append([])
point_positions.append([])
if invert_chart:
for function in y_coordinates.size():
for function_value in y_coordinates[function].size():
if are_values_columns:
point_positions[function_value].append(Vector2(x_coordinates[function]+origin.x, origin.y-y_coordinates[function][function_value]))
point_values[function_value].append([x_datas[function_value],y_datas[function_value][function]])
else:
point_positions[function].append(Vector2(x_coordinates[function_value]+origin.x,origin.y-y_coordinates[function][function_value]))
point_values[function].append([x_datas[function_value],y_datas[function_value][function]])
else:
for cluster in y_coordinates.size():
for y in y_coordinates[cluster].size():
if are_values_columns:
point_values[y].append([x_datas[cluster],y_datas[cluster][y]])
point_positions[y].append(Vector2(x_coordinates[cluster]+origin.x,origin.y-y_coordinates[cluster][y]))
else:
point_values[cluster].append([x_datas[y],y_datas[cluster][y]])
point_positions[cluster].append(Vector2(x_coordinates[y]+origin.x,origin.y-y_coordinates[cluster][y]))
func redraw():
build_chart()
calculate_pass()
calculate_coordinates()
update()
func _draw():
clear_points()
draw_grid()
draw_chart_outlines()
var defined_colors : bool = false
if function_colors.size():
defined_colors = true
for _function in point_values.size():
var PointContainer : Control = Control.new()
Points.add_child(PointContainer)
for function_point in point_values[_function].size():
var point : Point = point_node.instance()
point.connect("_point_pressed",self,"point_pressed")
point.connect("_mouse_entered",self,"show_data")
point.connect("_mouse_exited",self,"hide_data")
point.create_point(point_shape, function_colors[function_point if invert_chart else _function],
Color.white, point_positions[_function][function_point],
point.format_value(point_values[_function][function_point], false, false),
y_labels[function_point if invert_chart else _function] as String)
PointContainer.add_child(point)
func create_legend():
legend.clear()
for function in functions:
var function_legend = FunctionLegend.instance()
var f_name : String
if invert_chart:
f_name = x_datas[function] as String
else:
f_name = y_labels[function]
var legend_font : Font
if font != null:
legend_font = font
if bold_font != null:
legend_font = bold_font
function_legend.create_legend(f_name,function_colors[function],bold_font,font_color)
legend.append(function_legend)
func draw_grid():
# ascisse
for p in x_chors.size():
var point : Vector2 = origin+Vector2((p)*x_pass,0)
# v grid
draw_line(point,point-Vector2(0,SIZE.y-OFFSET.y),v_lines_color,0.2,true)
# ascisse
draw_line(point-Vector2(0,5),point,v_lines_color,1,true)
draw_string(font,point+Vector2(-const_width/2*x_chors[p].length(),font_size+const_height),x_chors[p],font_color)
# ordinate
for p in y_chors.size():
var point : Vector2 = origin-Vector2(0,(p)*y_pass)
# h grid
draw_line(point,point+Vector2(SIZE.x-OFFSET.x,0),h_lines_color,0.2,true)
# ordinate
draw_line(point,point+Vector2(5,0),h_lines_color,1,true)
draw_string(font,point-Vector2(y_chors[p].length()*const_width+font_size,-const_height),y_chors[p],font_color)
func draw_chart_outlines():
draw_line(origin,SIZE-Vector2(0,OFFSET.y),box_color,1,true)
draw_line(origin,Vector2(OFFSET.x,0),box_color,1,true)
draw_line(Vector2(OFFSET.x,0),Vector2(SIZE.x,0),box_color,1,true)
draw_line(Vector2(SIZE.x,0),SIZE-Vector2(0,OFFSET.y),box_color,1,true)
var can_grab_x : bool = false
var can_grab_y : bool = false
var can_move : bool = false
var range_mouse : float = 7
#func _input(event):
# if not can_grab_x and (event.position.x > (SIZE.x-range_mouse + rect_position.x) and event.position.x < (SIZE.x+range_mouse + rect_position.x)) :
# set_default_cursor_shape(Control.CURSOR_HSIZE)
# if Input.is_action_pressed("mouse_left"):
# can_grab_x = true
#
# if Input.is_action_just_released("mouse_left") and can_grab_x:
# can_grab_x = false
#
# if not can_grab_y and (event.position.y > ( rect_position.y + origin.y-range_mouse) and event.position.y < (rect_position.y+ origin.y+range_mouse)) :
# set_default_cursor_shape(Control.CURSOR_VSIZE)
# if Input.is_action_pressed("mouse_left"):
# can_grab_y = true
#
# if Input.is_action_just_released("mouse_left") and can_grab_y:
# can_grab_y = false
#
# if (event.position.x > SIZE.x-range_mouse+rect_position.x and event.position.x < SIZE.x+range_mouse + rect_position.x) and (event.position.y > rect_position.y+origin.y-range_mouse and event.position.y < rect_position.y+origin.y+range_mouse):
# set_default_cursor_shape(Control.CURSOR_FDIAGSIZE)
# if not (event.position.x > SIZE.x-range_mouse+rect_position.x and event.position.x < SIZE.x+range_mouse + rect_position.x) and not (event.position.y > rect_position.y+ origin.y-range_mouse and event.position.y < rect_position.y+origin.y+range_mouse ):
# set_default_cursor_shape(Control.CURSOR_ARROW)
#
#func _process(delta):
# if can_grab_x:
# PointData.hide()
# get_parent().rect_size.x = get_global_mouse_position().x - rect_position.x
# redraw()
#
# if can_grab_y:
# PointData.hide()
# get_parent().rect_size.y = get_global_mouse_position().y - rect_position.y + OFFSET.y
# redraw()
func show_data(point):
PointData.update_datas(point)
PointData.show()
func hide_data():
PointData.hide()
func clear_points():
if Points.get_children():
for function in Points.get_children():
function.queue_free()
for legend in Legend.get_children():
legend.queue_free()
func set_legend(l : Array):
legend = l
func get_legend():
return legend
func invert_chart():
invert_chart = !invert_chart
count_functions()
redraw()
create_legend()
func count_functions():
if are_values_columns:
if not invert_chart:
functions = datas[0].size()-1
else:
functions = datas.size()-1
else:
if invert_chart:
functions = datas[0].size()-1
else:
functions = datas.size()-1
func apply_template(template_name : String):
template = template_name
templates = Utilities._load_templates()
if template_name!=null and template_name!="":
var custom_template = templates[template.to_lower()]
function_colors = custom_template.function_colors
v_lines_color = Color(custom_template.v_lines_color)
h_lines_color = Color(custom_template.h_lines_color)
box_color = Color(custom_template.outline_color)
font_color = Color(custom_template.font_color)
property_list_changed_notify()

View File

@ -0,0 +1,58 @@
[gd_scene load_steps=4 format=2]
[ext_resource path="res://addons/easy_charts/Utilities/Point/PointData.tscn" type="PackedScene" id=1]
[ext_resource path="res://addons/easy_charts/ScatterChart/ScatterChart.gd" type="Script" id=2]
[sub_resource type="Theme" id=1]
[node name="ScatterChart" type="Control"]
anchor_right = 1.0
anchor_bottom = 1.0
rect_min_size = Vector2( 70, 50 )
mouse_filter = 2
script = ExtResource( 2 )
__meta__ = {
"_edit_use_anchors_": false,
"_editor_description_": "[ScatterChart] - General purpose node for Scatter Charts
A scatter plot (also called a scatterplot, scatter graph, scatter chart, scattergram, or scatter diagram)
is a type of plot or mathematical diagram using Cartesian coordinates to display values for typically two variables
for a set of data. If the points are coded (color/shape/size), one additional variable can be displayed.
The data are displayed as a collection of points, each having the value of one variable determining the position on
the horizontal axis and the value of the other variable determining the position on the vertical axis."
}
function_colors = [ "#1e1e1e", "#1e1e1e", "#1e1e1e", "#1e1e1e" ]
[node name="Background" type="ColorRect" parent="."]
visible = false
show_behind_parent = true
anchor_right = 1.0
anchor_bottom = 1.0
color = Color( 0.882353, 0.882353, 0.882353, 1 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Points" type="Control" parent="."]
margin_right = 40.0
margin_bottom = 40.0
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Legend" type="HBoxContainer" parent="."]
visible = false
margin_right = 1024.0
margin_bottom = 64.0
alignment = 1
__meta__ = {
"_edit_use_anchors_": false
}
[node name="PointData" parent="." instance=ExtResource( 1 )]
[node name="PointData" parent="PointData" index="0"]
visible = false
theme = SubResource( 1 )
[editable path="PointData"]

View File

@ -0,0 +1,531 @@
tool
extends Node2D
"""
[ScatterChart2D] - General purpose node for Scatter Charts
A scatter plot (also called a scatterplot, scatter graph, scatter chart, scattergram, or scatter diagram)
is a type of plot or mathematical diagram using Cartesian coordinates to display values for typically two variables
for a set of data. If the points are coded (color/shape/size), one additional variable can be displayed.
The data are displayed as a collection of points, each having the value of one variable determining the position on
the horizontal axis and the value of the other variable determining the position on the vertical axis.
/ source : Wikipedia /
"""
onready var OutlinesTween : Tween = $OutlinesTween
onready var PointTween : Tween = $PointTween
onready var Functions : Node2D = $Functions
onready var GridTween : Tween = $GridTween
onready var PointData = $PointData/PointData
onready var Outlines : Line2D = $Outlines
onready var Grid : Node2D = $Grid
var point_node : PackedScene = preload("../Utilities/Point/Point.tscn")
var FunctionLegend : PackedScene = preload("../Utilities/Legend/FunctionLegend.tscn")
var font_size : float = 16
var const_height : float = font_size/2*font_size/20
var const_width : float = font_size/2
var OFFSET : Vector2 = Vector2(0,0)
#-------------------------------------------------------------------------#
var origin : Vector2
# actual distance between x and y values
var x_pass : float
var y_pass : float
# vertical distance between y consecutive points used for intervals
var v_dist : float
var h_dist : float
# quantization, representing the interval in which values will be displayed
# define values on x an y axis
var x_chors : Array
var y_chors : Array
# actual coordinates of points (in pixel)
var x_coordinates : Array
var y_coordinates : Array
# datas contained in file
var datas : Array
# amount of functions to represent
var functions : int = 0
var x_label : String
# database values
var x_datas : Array
var y_datas : Array
# labels displayed on chart
var x_labels : Array
var y_labels : Array
var x_margin_min : int = 0
var y_margin_min : int = 0
# actual values of point, from the database
var point_values : Array
# actual position of points in pixel
var point_positions : Array
var legend : Array setget set_legend,get_legend
# ---------------------
export (Vector2) var SIZE : Vector2 = Vector2() setget _set_size
export (String, FILE, "*.txt, *.csv") var source : String = ""
export (String) var delimiter : String = ";"
export (bool) var origin_at_zero : bool = true
export (bool) var are_values_columns : bool = false
export (int,0,100) var x_values_index : int = 0
export(bool) var show_x_values_as_labels : bool = true
#export (float,1,20,0.5) var column_width : float = 10
#export (float,0,10,0.5) var column_gap : float = 2
export (float,0.1,10.0) var x_decim : float = 5.0
export (float,0.1,10.0) var y_decim : float = 5.0
export (int,"Dot,Triangle,Square") var point_shape : int = 0
export (PoolColorArray) var function_colors = [Color("#1e1e1e")]
export (Color) var v_lines_color : Color = Color("#cacaca")
export (Color) var h_lines_color : Color = Color("#cacaca")
export (bool) var boxed : bool = true
export (Color) var box_color : Color = Color("#1e1e1e")
export (Font) var font : Font
export (Font) var bold_font : Font
export (Color) var font_color : Color = Color("#1e1e1e")
export (String,"Default","Clean","Gradient","Minimal","Invert") var template : String = "Default" setget apply_template
export (float,0.1,1) var drawing_duration : float = 0.5
export (bool) var invert_chart : bool = false
var templates : Dictionary = {}
signal chart_plotted(chart)
signal point_pressed(point)
func _point_plotted():
pass
func _ready():
pass
func _set_size(size : Vector2):
SIZE = size
build_chart()
if Engine.editor_hint:
Outlines.set_point_position(0,Vector2(origin.x,0))
Outlines.set_point_position(1,Vector2(SIZE.x,0))
Outlines.set_point_position(2,Vector2(SIZE.x,origin.y))
Outlines.set_point_position(3,origin)
Outlines.set_point_position(4,Vector2(origin.x,0))
Grid.get_node("VLine").set_point_position(0,Vector2((OFFSET.x+SIZE.x)/2,0))
Grid.get_node("VLine").set_point_position(1,Vector2((OFFSET.x+SIZE.x)/2,origin.y))
Grid.get_node("HLine").set_point_position(0,Vector2(origin.x,origin.y/2))
Grid.get_node("HLine").set_point_position(1,Vector2(SIZE.x,origin.y/2))
func clear():
Outlines.points = []
Grid.get_node("HLine").queue_free()
Grid.get_node("VLine").queue_free()
func load_font():
if font != null:
font_size = font.get_height()
var theme : Theme = Theme.new()
theme.set_default_font(font)
PointData.set_theme(theme)
else:
var lbl = Label.new()
font = lbl.get_font("")
lbl.free()
if bold_font != null:
PointData.Data.set("custom_fonts/font",bold_font)
func _plot(source_ : String, delimiter_ : String, are_values_columns_ : bool, x_values_index_ : int):
randomize()
clear()
load_font()
PointData.hide()
datas = read_datas(source_,delimiter_)
count_functions()
structure_datas(datas,are_values_columns_,x_values_index_)
build_chart()
calculate_pass()
calculate_coordinates()
calculate_colors()
draw_chart()
create_legend()
emit_signal("chart_plotted", self)
func plot():
randomize()
clear()
load_font()
PointData.hide()
if source == "" or source == null:
Utilities._print_message("Can't plot a chart without a Source file. Please, choose it in editor, or use the custom function _plot().",1)
return
datas = read_datas(source,delimiter)
count_functions()
structure_datas(datas,are_values_columns,x_values_index)
build_chart()
calculate_pass()
calculate_coordinates()
calculate_colors()
draw_chart()
create_legend()
emit_signal("chart_plotted", self)
func calculate_colors():
if function_colors.empty() or function_colors.size() < functions:
for function in functions:
function_colors.append(Color("#1e1e1e"))
func draw_chart():
draw_outlines()
draw_v_grid()
draw_h_grid()
draw_functions()
func draw_outlines():
if boxed:
Outlines.set_default_color(box_color)
OutlinesTween.interpolate_method(Outlines,"add_point",
Vector2(origin.x,0),Vector2(SIZE.x,0),drawing_duration*0.5,Tween.TRANS_QUINT,Tween.EASE_OUT)
OutlinesTween.start()
yield(OutlinesTween,"tween_all_completed")
OutlinesTween.interpolate_method(Outlines,"add_point",
Vector2(SIZE.x,0),Vector2(SIZE.x,origin.y),drawing_duration*0.5,Tween.TRANS_QUINT,Tween.EASE_OUT)
OutlinesTween.start()
yield(OutlinesTween,"tween_all_completed")
OutlinesTween.interpolate_method(Outlines,"add_point",
Vector2(SIZE.x,origin.y),origin,drawing_duration*0.5,Tween.TRANS_QUINT,Tween.EASE_OUT)
OutlinesTween.start()
yield(OutlinesTween,"tween_all_completed")
OutlinesTween.interpolate_method(Outlines,"add_point",
origin,Vector2(origin.x,0),drawing_duration*0.5,Tween.TRANS_QUINT,Tween.EASE_OUT)
OutlinesTween.start()
yield(OutlinesTween,"tween_all_completed")
func draw_v_grid():
for p in x_chors.size():
var point : Vector2 = origin+Vector2((p)*x_pass,0)
var v_grid : Line2D = Line2D.new()
Grid.add_child(v_grid)
v_grid.set_width(1)
v_grid.set_default_color(v_lines_color)
add_label(point+Vector2(-const_width/2*x_chors[p].length(),font_size/2), x_chors[p])
GridTween.interpolate_method(v_grid,"add_point",point,point-Vector2(0,SIZE.y-OFFSET.y),drawing_duration/(x_chors.size()),Tween.TRANS_EXPO,Tween.EASE_OUT)
GridTween.start()
yield(GridTween,"tween_all_completed")
func draw_h_grid():
for p in y_chors.size():
var point : Vector2 = origin-Vector2(0,(p)*y_pass)
var h_grid : Line2D = Line2D.new()
Grid.add_child(h_grid)
h_grid.set_width(1)
h_grid.set_default_color(h_lines_color)
add_label(point-Vector2(y_chors[p].length()*const_width+font_size,font_size/2), y_chors[p])
GridTween.interpolate_method(h_grid,"add_point",point,Vector2(SIZE.x,point.y),drawing_duration/(y_chors.size()),Tween.TRANS_EXPO,Tween.EASE_OUT)
GridTween.start()
yield(GridTween,"tween_all_completed")
func add_label(point : Vector2, text : String):
var lbl : Label = Label.new()
if font != null:
lbl.set("custom_fonts/font",font)
lbl.set("custom_colors/font_color",font_color)
Grid.add_child(lbl)
lbl.rect_position = point
lbl.set_text(text)
func draw_functions():
for function in point_positions.size():
draw_function(function,point_positions[function])
func draw_function(f_index : int, function : Array):
var pointv : Point
for point in function.size():
pointv = point_node.instance()
Functions.add_child(pointv)
pointv.connect("_mouse_entered",self,"show_data")
pointv.connect("_mouse_exited",self,"hide_data")
pointv.connect("_point_pressed",self,"point_pressed")
pointv.create_point(point_shape, function_colors[f_index], Color.white, function[point],
pointv.format_value(point_values[f_index][point],false,false),
y_labels[point if invert_chart else f_index] as String)
yield(get_tree().create_timer(drawing_duration/function.size()), "timeout")
func read_datas(source : String, delimiter : String):
var file : File = File.new()
file.open(source,File.READ)
var content : Array
while not file.eof_reached():
var line : PoolStringArray = file.get_csv_line(delimiter)
content.append(line)
file.close()
for data in content:
if data.size() < 2:
content.erase(data)
return content
func structure_datas(database : Array, are_values_columns : bool, x_values_index : int):
# @x_values_index can be either a column or a row relative to x values
# @y_values can be either a column or a row relative to y values
self.are_values_columns = are_values_columns
match are_values_columns:
true:
for row in database.size():
var t_vals : Array
for column in database[row].size():
if column == x_values_index:
var x_data = database[row][column]
if x_data.is_valid_float() or x_data.is_valid_integer():
x_datas.append(x_data as float)
else:
x_datas.append(x_data.replace(",",".") as float)
else:
if row != 0:
var y_data = database[row][column]
if y_data.is_valid_float() or y_data.is_valid_integer():
t_vals.append(y_data as float)
else:
t_vals.append(y_data.replace(",",".") as float)
else:
y_labels.append(str(database[row][column]))
if not t_vals.empty():
y_datas.append(t_vals)
x_label = str(x_datas.pop_front())
false:
for row in database.size():
if row == x_values_index:
x_datas = (database[row])
x_label = x_datas.pop_front() as String
else:
var values = database[row] as Array
y_labels.append(values.pop_front() as String)
y_datas.append(values)
for data in y_datas:
for value in data.size():
data[value] = data[value] as float
# draw y labels
var to_order : Array
var to_order_min : Array
for cluster in y_datas.size():
# define x_chors and y_chors
var ordered_cluster = y_datas[cluster] as Array
ordered_cluster.sort()
ordered_cluster = PoolIntArray(ordered_cluster)
var margin_max = ordered_cluster[ordered_cluster.size()-1]
var margin_min = ordered_cluster[0]
to_order.append(margin_max)
to_order_min.append(margin_min)
to_order.sort()
to_order_min.sort()
var margin = to_order.pop_back()
if not origin_at_zero:
y_margin_min = to_order_min.pop_front()
v_dist = y_decim * pow(10.0,str(margin).length()-2)
var multi = 0
var p = (v_dist*multi) + ((y_margin_min) if not origin_at_zero else 0)
y_chors.append(p as String)
while p < margin:
multi+=1
p = (v_dist*multi) + ((y_margin_min) if not origin_at_zero else 0)
y_chors.append(p as String)
# draw x_labels
if not show_x_values_as_labels:
to_order.clear()
to_order = x_datas as PoolIntArray
to_order.sort()
margin = to_order.pop_back()
if not origin_at_zero:
x_margin_min = to_order.pop_front()
h_dist = x_decim * pow(10.0,str(margin).length()-2)
multi = 0
p = (h_dist*multi) + ((x_margin_min) if not origin_at_zero else 0)
x_labels.append(p as String)
while p < margin:
multi+=1
p = (h_dist*multi) + ((x_margin_min) if not origin_at_zero else 0)
x_labels.append(p as String)
func build_chart():
origin = Vector2(OFFSET.x,SIZE.y-OFFSET.y)
func calculate_pass():
if invert_chart:
x_chors = y_labels as PoolStringArray
else:
if show_x_values_as_labels:
x_chors = x_datas as PoolStringArray
else:
x_chors = x_labels
# calculate distance in pixel between 2 consecutive values/datas
x_pass = (SIZE.x - OFFSET.x) / (x_chors.size()-1)
y_pass = origin.y / (y_chors.size()-1)
func calculate_coordinates():
x_coordinates.clear()
y_coordinates.clear()
point_values.clear()
point_positions.clear()
if invert_chart:
for column in y_datas[0].size():
var single_coordinates : Array
for row in y_datas:
if origin_at_zero:
single_coordinates.append((row[column]*y_pass)/v_dist)
else:
single_coordinates.append((row[column] - y_margin_min)*y_pass/v_dist)
y_coordinates.append(single_coordinates)
else:
for cluster in y_datas:
var single_coordinates : Array
for value in cluster.size():
if origin_at_zero:
single_coordinates.append((cluster[value]*y_pass)/v_dist)
else:
single_coordinates.append((cluster[value] - y_margin_min)*y_pass/v_dist)
y_coordinates.append(single_coordinates)
if show_x_values_as_labels:
for x in x_datas.size():
x_coordinates.append(x_pass*x)
else:
for x in x_datas.size():
if origin_at_zero:
if invert_chart:
x_coordinates.append(x_pass*x)
else:
x_coordinates.append(x_datas[x]*x_pass/h_dist)
else:
x_coordinates.append((x_datas[x] - x_margin_min)*x_pass/h_dist)
for f in functions:
point_values.append([])
point_positions.append([])
if invert_chart:
for function in y_coordinates.size():
for function_value in y_coordinates[function].size():
if are_values_columns:
point_positions[function_value].append(Vector2(x_coordinates[function]+origin.x, origin.y-y_coordinates[function][function_value]))
point_values[function_value].append([x_datas[function_value],y_datas[function_value][function]])
else:
point_positions[function].append(Vector2(x_coordinates[function_value]+origin.x,origin.y-y_coordinates[function][function_value]))
point_values[function].append([x_datas[function_value],y_datas[function_value][function]])
else:
for cluster in y_coordinates.size():
for y in y_coordinates[cluster].size():
if are_values_columns:
point_values[y].append([x_datas[cluster],y_datas[cluster][y]])
point_positions[y].append(Vector2(x_coordinates[cluster]+origin.x,origin.y-y_coordinates[cluster][y]))
else:
point_values[cluster].append([x_datas[y],y_datas[cluster][y]])
point_positions[cluster].append(Vector2(x_coordinates[y]+origin.x,origin.y-y_coordinates[cluster][y]))
func redraw():
build_chart()
calculate_pass()
calculate_coordinates()
update()
func show_data(point):
PointData.update_datas(point)
PointData.show()
func hide_data():
PointData.hide()
func clear_points():
function_colors.clear()
if Functions.get_children():
for function in Functions.get_children():
function.queue_free()
func set_legend(l : Array):
legend = l
func get_legend():
return legend
func invert_chart():
invert_chart = !invert_chart
count_functions()
redraw()
create_legend()
func count_functions():
if are_values_columns:
if not invert_chart:
functions = datas[0].size()-1
else:
functions = datas.size()-1
else:
if invert_chart:
functions = datas[0].size()-1
else:
functions = datas.size()-1
func create_legend():
legend.clear()
for function in functions:
var function_legend = FunctionLegend.instance()
var f_name : String
if invert_chart:
f_name = x_datas[function] as String
else:
f_name = y_labels[function]
var legend_font : Font
if font != null:
legend_font = font
if bold_font != null:
legend_font = bold_font
function_legend.create_legend(f_name,function_colors[function],bold_font,font_color)
legend.append(function_legend)
func apply_template(template_name : String):
template = template_name
templates = Utilities._load_templates()
if template_name!=null and template_name!="":
var custom_template = templates[template.to_lower()]
function_colors = custom_template.function_colors
v_lines_color = Color(custom_template.v_lines_color)
h_lines_color = Color(custom_template.h_lines_color)
box_color = Color(custom_template.outline_color)
font_color = Color(custom_template.font_color)
property_list_changed_notify()
if Engine.editor_hint:
Outlines.set_default_color(box_color)
Grid.get_node("VLine").set_default_color(v_lines_color)
Grid.get_node("HLine").set_default_color(h_lines_color)

View File

@ -0,0 +1,50 @@
[gd_scene load_steps=3 format=2]
[ext_resource path="res://addons/easy_charts/ScatterChart2D/ScatterChart2D.gd" type="Script" id=1]
[ext_resource path="res://addons/easy_charts/Utilities/Point/PointData.tscn" type="PackedScene" id=2]
[node name="ScatterChart2D" type="Node2D"]
script = ExtResource( 1 )
__meta__ = {
"_edit_group_": true,
"_editor_description_": "[ScatterChart2D] - General purpose node for Scatter Charts
A scatter plot (also called a scatterplot, scatter graph, scatter chart, scattergram, or scatter diagram)
is a type of plot or mathematical diagram using Cartesian coordinates to display values for typically two variables
for a set of data. If the points are coded (color/shape/size), one additional variable can be displayed.
The data are displayed as a collection of points, each having the value of one variable determining the position on
the horizontal axis and the value of the other variable determining the position on the vertical axis."
}
function_colors = [ "#1e1e1e", "#1e1e1e", "#1e1e1e", "#1e1e1e" ]
[node name="Grid" type="Node2D" parent="."]
[node name="VLine" type="Line2D" parent="Grid"]
points = PoolVector2Array( 0, 0, 0, 0 )
width = 1.0
default_color = Color( 0.792157, 0.792157, 0.792157, 1 )
[node name="HLine" type="Line2D" parent="Grid"]
points = PoolVector2Array( 0, 0, 0, 0 )
width = 1.0
default_color = Color( 0.792157, 0.792157, 0.792157, 1 )
[node name="Outlines" type="Line2D" parent="."]
points = PoolVector2Array( 0, 0, 2, 0, 2, 2, 0, 2, 0, 0 )
width = 2.0
default_color = Color( 0.117647, 0.117647, 0.117647, 1 )
[node name="Functions" type="Node2D" parent="."]
[node name="OutlinesTween" type="Tween" parent="."]
[node name="GridTween" type="Tween" parent="."]
[node name="PointTween" type="Tween" parent="."]
[node name="PointData" parent="." instance=ExtResource( 2 )]
[node name="PointData" parent="PointData" index="0"]
visible = false
[editable path="PointData"]

View File

@ -10,8 +10,13 @@ var function : String setget set_function, get_function
var mouse_entered : bool = false var mouse_entered : bool = false
enum SHAPES {
DOT, TRIANGLE, SQUARE
}
signal _mouse_entered() var shape : int = 0 setget set_shape, get_shape
signal _mouse_entered(point)
signal _mouse_exited() signal _mouse_exited()
signal _point_pressed(point) signal _point_pressed(point)
@ -22,10 +27,26 @@ func _ready():
func _draw(): func _draw():
if mouse_entered: if mouse_entered:
draw_circle(OFFSET,7,color_outline) draw_point(7,color_outline)
draw_circle(OFFSET,5,color) draw_point(5,color)
func create_point(color : Color, color_outline : Color, position : Vector2, value : Array, function : String): func draw_point(size : float, color : Color):
match shape:
SHAPES.DOT:
draw_circle(OFFSET, size, color)
SHAPES.TRIANGLE:
size+=4
draw_colored_polygon([
OFFSET-Vector2(0,size/2), OFFSET+Vector2(1,1)*size/2, OFFSET-Vector2(1,-1)*size/2
], color,[],null,null,false)
SHAPES.SQUARE:
size+=2
draw_colored_polygon([
OFFSET-Vector2(1,1)*size/2, OFFSET-Vector2(-1,1)*size/2, OFFSET+Vector2(1,1)*size/2, OFFSET-Vector2(1,-1)*size/2
], color,[],null,null,false)
func create_point(shape : int, color : Color, color_outline : Color, position : Vector2, value : Array, function : String):
self.shape = shape
self.color = color self.color = color
self.color_outline = color_outline self.color_outline = color_outline
self.point_position = position self.point_position = position
@ -33,9 +54,10 @@ func create_point(color : Color, color_outline : Color, position : Vector2, valu
self.point_value = value self.point_value = value
self.function = function self.function = function
func _on_Point_mouse_entered(): func _on_Point_mouse_entered():
mouse_entered = true mouse_entered = true
emit_signal("_mouse_entered") emit_signal("_mouse_entered",self)
update() update()
func _on_Point_mouse_exited(): func _on_Point_mouse_exited():
@ -90,3 +112,9 @@ func set_function( f : String ):
func get_function() -> String: func get_function() -> String:
return function return function
func set_shape(s : int):
shape = s
func get_shape() -> int:
return shape

View File

@ -3,8 +3,8 @@
[ext_resource path="res://addons/easy_charts/Utilities/Point/PointData.gd" type="Script" id=1] [ext_resource path="res://addons/easy_charts/Utilities/Point/PointData.gd" type="Script" id=1]
[sub_resource type="StyleBoxFlat" id=1] [sub_resource type="StyleBoxFlat" id=1]
content_margin_left = 8.0 content_margin_left = 10.0
content_margin_right = 8.0 content_margin_right = 10.0
content_margin_top = 5.0 content_margin_top = 5.0
content_margin_bottom = 5.0 content_margin_bottom = 5.0
bg_color = Color( 1, 1, 1, 0 ) bg_color = Color( 1, 1, 1, 0 )
@ -22,21 +22,27 @@ corner_detail = 20
[node name="PointData" type="CanvasLayer"] [node name="PointData" type="CanvasLayer"]
[node name="PointData" type="PanelContainer" parent="."] [node name="PointData" type="PanelContainer" parent="."]
margin_right = 67.0 anchor_right = 0.0694688
margin_bottom = 41.0 anchor_bottom = 0.067
margin_right = -0.136002
margin_bottom = 0.799999
grow_horizontal = 2
mouse_filter = 2 mouse_filter = 2
custom_styles/panel = SubResource( 1 ) custom_styles/panel = SubResource( 1 )
script = ExtResource( 1 ) script = ExtResource( 1 )
__meta__ = { __meta__ = {
"_edit_use_anchors_": false "_edit_use_anchors_": true
} }
[node name="PointData" type="VBoxContainer" parent="PointData"] [node name="PointData" type="VBoxContainer" parent="PointData"]
margin_left = 8.0 margin_left = 10.0
margin_top = 5.0 margin_top = 5.0
margin_right = 59.0 margin_right = 61.0
margin_bottom = 36.0 margin_bottom = 36.0
grow_horizontal = 2
size_flags_horizontal = 3
custom_constants/separation = 3 custom_constants/separation = 3
alignment = 1
__meta__ = { __meta__ = {
"_edit_use_anchors_": false "_edit_use_anchors_": false
} }
@ -44,6 +50,7 @@ __meta__ = {
[node name="Function" type="Label" parent="PointData/PointData"] [node name="Function" type="Label" parent="PointData/PointData"]
margin_right = 51.0 margin_right = 51.0
margin_bottom = 14.0 margin_bottom = 14.0
size_flags_horizontal = 3
align = 1 align = 1
valign = 1 valign = 1
@ -51,18 +58,24 @@ valign = 1
margin_top = 17.0 margin_top = 17.0
margin_right = 51.0 margin_right = 51.0
margin_bottom = 31.0 margin_bottom = 31.0
grow_horizontal = 2
size_flags_horizontal = 6
alignment = 1
[node name="x" type="Label" parent="PointData/PointData/Value"] [node name="x" type="Label" parent="PointData/PointData/Value"]
margin_right = 39.0 margin_right = 39.0
margin_bottom = 14.0 margin_bottom = 14.0
size_flags_horizontal = 3
custom_colors/font_color = Color( 1, 1, 1, 1 ) custom_colors/font_color = Color( 1, 1, 1, 1 )
text = "Value:" text = "Value:"
align = 2
valign = 1 valign = 1
[node name="y" type="Label" parent="PointData/PointData/Value"] [node name="y" type="Label" parent="PointData/PointData/Value"]
margin_left = 43.0 margin_left = 43.0
margin_right = 51.0 margin_right = 51.0
margin_bottom = 14.0 margin_bottom = 14.0
size_flags_horizontal = 3
custom_colors/font_color = Color( 1, 1, 1, 1 ) custom_colors/font_color = Color( 1, 1, 1, 1 )
text = "0" text = "0"
valign = 1 valign = 1

View File

@ -2,16 +2,18 @@ tool
extends Node extends Node
var plugin_name : String = "Easy Charts" var plugin_name : String = "Easy Charts"
var templates : Dictionary = {}
func _ready(): func _ready():
pass templates = _load_templates()
_print_message("Templates loaded")
func _print_message(message : String, type : int = 0): func _print_message(message : String, type : int = 0):
match type: match type:
0: 0:
print("[%s] => %s" % [plugin_name, message]) print("[%s] => %s" % [plugin_name, message])
1: 1:
printerr("[%s] => %s" % [plugin_name, message]) printerr("ERROR: [%s] => %s" % [plugin_name, message])
func _load_templates() -> Dictionary: func _load_templates() -> Dictionary:
var template_file : File = File.new() var template_file : File = File.new()

View File

@ -0,0 +1,10 @@
Year;Column 1
2009;36200
2010;36600
2011;37500
2012;38700
2013;39600
2014;40500
2015;41200
2016;41803
2017;42600
1 Year Column 1
2 2009 36200
3 2010 36600
4 2011 37500
5 2012 38700
6 2013 39600
7 2014 40500
8 2015 41200
9 2016 41803
10 2017 42600

View File

@ -0,0 +1,10 @@
Year;Column 1;Column 2;Column 3;Column 4
2009;36200;27200;26200;17200
2010;36600;27800;26600;17800
2011;37500;28500;27500;18500
2012;38700;29400;28700;19400
2013;39600;30200;29600;10200
2014;40500;30900;20500;10900
2015;41200;31500;21200;11500
2016;41803;31931;21803;11931
2017;42600;32600;22600;12600
1 Year Column 1 Column 2 Column 3 Column 4
2 2009 36200 27200 26200 17200
3 2010 36600 27800 26600 17800
4 2011 37500 28500 27500 18500
5 2012 38700 29400 28700 19400
6 2013 39600 30200 29600 10200
7 2014 40500 30900 20500 10900
8 2015 41200 31500 21200 11500
9 2016 41803 31931 21803 11931
10 2017 42600 32600 22600 12600

View File

@ -0,0 +1,5 @@
Year;2009;2010;2011;2012;2013;2014;2015;2016;2017
Column 1;36200;36600;37500;38700;39600;40500;41200;41803;42600
Column 2;27200;27800;28500;29400;30200;30900;31500;31931;32600
Column 3;26200;26600;27500;28700;29600;20500;21200;21803;22600
Column 4;17200;17800;18500;19400;10200;10900;11500;11931;12600
1 Year 2009 2010 2011 2012 2013 2014 2015 2016 2017
2 Column 1 36200 36600 37500 38700 39600 40500 41200 41803 42600
3 Column 2 27200 27800 28500 29400 30200 30900 31500 31931 32600
4 Column 3 26200 26600 27500 28700 29600 20500 21200 21803 22600
5 Column 4 17200 17800 18500 19400 10200 10900 11500 11931 12600

View File

@ -0,0 +1,101 @@
x;y1;y2
50,05732879;116,61234;90,55501121
53,23295081;105,6457011;47,41275029
47,28686102;95,61795061;36,33108959
53,0250493;106,4552515;85,4302022
57,10271278;111,9088163;39,80610352
40,71992494;78,27201391;79,55208897
55,62752569;122,7859036;94,15837791
54,17819455;107,9817124;39,80351785
40,8610371;107,92736;61,0663229
54,06159551;110,0647495;93,00315399
64,35130431;134,5556319;114,2043276
49,29181834;88,18806126;64,89624292
44,63823505;102,8364528;108,1982178
58,62487836;126,0319541;78,40707574
55,78566879;95,81224088;75,02657209
50,76570445;94,73143882;39,96573437
48,45339259;92,86957776;88,41618517
49,40446173;70,21723661;9,81277488
48,34604978;97,87616091;40,53011113
43,62470151;103,7308337;97,10613219
50,8877046;117,1558657;62,2681611
52,25082012;103,3922334;79,14141328
50,35929987;87,68936817;19,3300683
46,42761861;90,1655428;67,73792419
55,67926036;93,17073084;69,49147048
61,72779383;142,8458546;106,1180608
58,99808851;102,3529041;27,35481559
43,6620999;77,36405233;17,70195243
55,42639088;121,0878726;78,66148172
58,79311097;111,8698686;49,07675763
50,71073988;106,5406487;97,82990882
45,57346035;104,7456111;56,17215075
45,7981314;83,9963622;75,1982308
46,46484131;82,94617575;77,48133444
57,9492853;144,9994608;129,0501755
48,3999722;72,83510082;30,43512862
54,2097076;114,7785416;82,568834
46,67548966;95,25014621;37,57465655
38,21698894;88,21423442;64,99724548
51,95614673;90,93548401;66,97933728
51,22522594;105,1106925;101,8854666
52,84741658;105,0503069;75,20289032
54,78984594;109,9873973;87,19755136
52,00296104;101,442706;89,43974496
51,43150193;88,40178257;36,97028064
47,40407943;89,44141735;55,03733792
51,6749185;96,54147702;31,86655852
40,74049925;83,85842913;79,11792988
49,82155418;112,9093356;96,08778142
57,57763531;115,7709369;72,19330159
51,49652924;127,9054098;118,4088806
53,10710725;112,6337002;90,52659295
42,93645994;102,8111834;102,8747235
44,14066275;78,88897631;41,74831356
46,60936983;98,64046333;54,0310935
47,41415307;95,89594769;58,48179462
47,99032677;106,3655192;102,3751924
50,68360992;106,9507457;44,26713578
50,57070899;83,19613908;61,62543009
57,14992785;115,5596634;40,40973555
50,45105658;97,6950217;64,24396512
46,76779029;99,20006513;60,43227484
50,49802495;99,43092076;87,93289581
51,52523583;84,24653091;17,72129508
40,72692657;102,1715902;67,44466363
54,96034411;111,5346596;82,57431549
48,86213774;101,4305303;58,56839256
52,76166432;108,021472;60,25980768
55,46249302;83,51709356;51,05460054
51,78186354;79,45870906;11,67684552
54,7256505;108,6386184;58,9129679
55,03288163;112,1224042;72,08952257
52,83806054;104,53718;101,6991195
45,86692682;91,39643691;29,52951009
49,67284006;110,1908352;88,51799514
53,18552185;119,0135337;113,8280119
41,69070011;62,78988486;53,09918475
53,36320564;118,5841731;108,2209675
44,27369117;74,58882658;69,31513541
49,92198979;76,93363417;52,01164438
44,91221355;82,33766098;44,42544743
37,92316535;68,0076039;17,08443855
50,10889447;103,6209931;49,51209863
44,20348231;84,19989748;57,99641517
57,11433271;102,0528226;84,93848989
41,90764861;109,1360517;50,22840309
51,88278094;89,01870776;69,13592682
44,31355843;81,19982464;76,88626621
44,25547817;72,64564157;53,3901634
48,93179315;107,7773458;40,84555265
39,36849865;59,39799005;68,0294914
53,33338181;99,04871654;92,71533473
61,63696872;129,3884947;76,75152598
46,40148646;102,9511032;92,54961674
43,81949012;75,15673812;44,337248
53,78046359;85,87008695;50,08962336
41,27977392;60,49700348;50,21722956
52,32206122;106,8462547;57,52419348
41,36660384;105,0465099;78,67990606
47,62423286;96,62455823;84,00032537
1 x y1 y2
2 50,05732879 116,61234 90,55501121
3 53,23295081 105,6457011 47,41275029
4 47,28686102 95,61795061 36,33108959
5 53,0250493 106,4552515 85,4302022
6 57,10271278 111,9088163 39,80610352
7 40,71992494 78,27201391 79,55208897
8 55,62752569 122,7859036 94,15837791
9 54,17819455 107,9817124 39,80351785
10 40,8610371 107,92736 61,0663229
11 54,06159551 110,0647495 93,00315399
12 64,35130431 134,5556319 114,2043276
13 49,29181834 88,18806126 64,89624292
14 44,63823505 102,8364528 108,1982178
15 58,62487836 126,0319541 78,40707574
16 55,78566879 95,81224088 75,02657209
17 50,76570445 94,73143882 39,96573437
18 48,45339259 92,86957776 88,41618517
19 49,40446173 70,21723661 9,81277488
20 48,34604978 97,87616091 40,53011113
21 43,62470151 103,7308337 97,10613219
22 50,8877046 117,1558657 62,2681611
23 52,25082012 103,3922334 79,14141328
24 50,35929987 87,68936817 19,3300683
25 46,42761861 90,1655428 67,73792419
26 55,67926036 93,17073084 69,49147048
27 61,72779383 142,8458546 106,1180608
28 58,99808851 102,3529041 27,35481559
29 43,6620999 77,36405233 17,70195243
30 55,42639088 121,0878726 78,66148172
31 58,79311097 111,8698686 49,07675763
32 50,71073988 106,5406487 97,82990882
33 45,57346035 104,7456111 56,17215075
34 45,7981314 83,9963622 75,1982308
35 46,46484131 82,94617575 77,48133444
36 57,9492853 144,9994608 129,0501755
37 48,3999722 72,83510082 30,43512862
38 54,2097076 114,7785416 82,568834
39 46,67548966 95,25014621 37,57465655
40 38,21698894 88,21423442 64,99724548
41 51,95614673 90,93548401 66,97933728
42 51,22522594 105,1106925 101,8854666
43 52,84741658 105,0503069 75,20289032
44 54,78984594 109,9873973 87,19755136
45 52,00296104 101,442706 89,43974496
46 51,43150193 88,40178257 36,97028064
47 47,40407943 89,44141735 55,03733792
48 51,6749185 96,54147702 31,86655852
49 40,74049925 83,85842913 79,11792988
50 49,82155418 112,9093356 96,08778142
51 57,57763531 115,7709369 72,19330159
52 51,49652924 127,9054098 118,4088806
53 53,10710725 112,6337002 90,52659295
54 42,93645994 102,8111834 102,8747235
55 44,14066275 78,88897631 41,74831356
56 46,60936983 98,64046333 54,0310935
57 47,41415307 95,89594769 58,48179462
58 47,99032677 106,3655192 102,3751924
59 50,68360992 106,9507457 44,26713578
60 50,57070899 83,19613908 61,62543009
61 57,14992785 115,5596634 40,40973555
62 50,45105658 97,6950217 64,24396512
63 46,76779029 99,20006513 60,43227484
64 50,49802495 99,43092076 87,93289581
65 51,52523583 84,24653091 17,72129508
66 40,72692657 102,1715902 67,44466363
67 54,96034411 111,5346596 82,57431549
68 48,86213774 101,4305303 58,56839256
69 52,76166432 108,021472 60,25980768
70 55,46249302 83,51709356 51,05460054
71 51,78186354 79,45870906 11,67684552
72 54,7256505 108,6386184 58,9129679
73 55,03288163 112,1224042 72,08952257
74 52,83806054 104,53718 101,6991195
75 45,86692682 91,39643691 29,52951009
76 49,67284006 110,1908352 88,51799514
77 53,18552185 119,0135337 113,8280119
78 41,69070011 62,78988486 53,09918475
79 53,36320564 118,5841731 108,2209675
80 44,27369117 74,58882658 69,31513541
81 49,92198979 76,93363417 52,01164438
82 44,91221355 82,33766098 44,42544743
83 37,92316535 68,0076039 17,08443855
84 50,10889447 103,6209931 49,51209863
85 44,20348231 84,19989748 57,99641517
86 57,11433271 102,0528226 84,93848989
87 41,90764861 109,1360517 50,22840309
88 51,88278094 89,01870776 69,13592682
89 44,31355843 81,19982464 76,88626621
90 44,25547817 72,64564157 53,3901634
91 48,93179315 107,7773458 40,84555265
92 39,36849865 59,39799005 68,0294914
93 53,33338181 99,04871654 92,71533473
94 61,63696872 129,3884947 76,75152598
95 46,40148646 102,9511032 92,54961674
96 43,81949012 75,15673812 44,337248
97 53,78046359 85,87008695 50,08962336
98 41,27977392 60,49700348 50,21722956
99 52,32206122 106,8462547 57,52419348
100 41,36660384 105,0465099 78,67990606
101 47,62423286 96,62455823 84,00032537