graph inverts if duplicated in gui

Godot Version

4.7.2

Question

I made a custom LineGraph node that extends Container, and for some reason duplicating the graph inverts it’s display.

if I run this simple test code after duplicating my graph in the editor every even-numbered graph will be inverted

var example_data = [0, 1, 2, 2, 2, 1, 0, 1, 2, 3, 4, 4, 4, 4, 3, 3, 2, 2, 3, 3, 3, 0, 0, 1 ,1, 2, 2, 2, 3, 4, 4, 4, 3, 4, 4, 4, 3, 3, 2, 2, 2, 2, 2, 2, 2, 1, 1, 0]

func _ready() -> void:
	var data: Array = []
	var i: int = 0
	while(i < example_data.size()):
		data.append([i, example_data[i]])
		i+=1
	
	var n: Node
	for num in range(get_child_count()):
		n = get_child(num)
		if n is LineGraph:
			n.initialize_graph(data)

However if I duplicate the graph in code every graph is inverted

var graph: LineGraph = get_child(0)
graph.initialize_graph(data)
var new_graph: LineGraph = graph.duplicate()
new_graph.position.x += graph.size.x + 45
add_child(new_graph)
new_graph = new_graph.duplicate()
new_graph.position.y += new_graph.size.y + 45
add_child(new_graph)

I doubt it will help, but here’s the whole class

class_name LineGraph
extends Container

@export var width: float = 10.0
@export var width_curve: Curve
@export var line_color: Color

@export_subgroup("Fill")
enum LineTextureMode {None, Tile, Stretch}
@export var gradient: Gradient
@export var texture: Texture
@export var texture_mode: LineTextureMode

@export_subgroup("Capping")
enum LineJointMode {Sharp, Bevel, Round}
enum LineCapMode {None, Box, Round}
@export var joint_mode: LineJointMode
@export var begin_cap_mode: LineCapMode
@export var end_cap_mode: LineCapMode

@export_subgroup("Border")
@export var sharp_limit: float = 2.0
@export var round_precision: int = 8
@export var antialiased: bool = false

@onready var DEFAULT_LINE_2D: Line2D
var line: Line2D

var data_numbers: Array
var data_normalized: Array
var largest_y: float = -999999999.9
var largest_x: float = -999999999.9
var smallest_x: float = -999999999.9
var smallest_y: float = -999999999.9
func _append_arrays(array_1: Array, array_2: Array, array_3: Array) -> Array:
	var new_array: Array = []
	for d in array_1:
		new_array.append(d)
	for d in array_2:
		new_array.append(d)
	for d in array_3:
		new_array.append(d)
	return new_array

func _quicksort_data(data_array) -> Array:
	if data_array.size() < 1:
		return []
	var index: Array = data_array[0]
	var less: Array
	var more: Array
	
	var i: int = 0
	while(i < data_array.size()-1):
		i+=1
		if data_array[i][0] < index[0]:
			less.append(data_array[i])
		else:
			more.append(data_array[i])
	
	less = _quicksort_data(less)
	more = _quicksort_data(more)
	data_array = _append_arrays(less, [index], more)
	return data_array

func initialize_graph(data: Array):
	#print("initializing graph")
	data_numbers = _quicksort_data(data)
	largest_x = data[data.size()-1][0]
	smallest_x = data[0][0] - 1.0
	smallest_y = data[0][1] + 1.0
	largest_y = data[0][1]-1.0
	for d in data_numbers:
		if d[0] < smallest_x:
			smallest_x = d[0]
		if d[1] < smallest_y:
			smallest_y = d[1]
		if d[1] > largest_y:
			largest_y = d[1]
	#print("largest values found")
	normalize_all_data()
	draw_graph()

func _insert_data(data: Array):
	var dat: Array = []
	var dat_normalized: Array = []
	
	var i: int = 0
	while(i < data_numbers.size()+1):
		if data[0] > data_numbers[i][0]:
			dat.append(data_numbers[i])
			dat_normalized.append(data_normalized[i])
			i+=1
		else:
			dat.append(data)
			dat_normalized.append([dat[0] / largest_x, dat[1] / largest_y])
			break
	
	while (i < data_numbers.size()):
		dat.append(data_numbers[i])
		dat.append(data_normalized[i])
		i+=1
	
func append(x: float, y: float) -> void:
	data_numbers.append([x, y])
	if y > largest_y:
		largest_y = y
	if x > largest_x:
		largest_x = x
		data_numbers.append([x, y])
	else:
		_insert_data([x, y])
	draw_graph()

func _overide_fail(x: float, y: float) -> bool:
	var failure: bool = true
	for i in range(data_numbers.size()):
		if data_numbers[i][0] == x:
			data_numbers[i][1] = y
			draw_graph()
			failure = false
			break
	return failure

func overide_safe(x: float, y: float) -> void:
	if _overide_fail(x, y):
		append(x, y)

func overide(x: float, y: float) -> void:
	if _overide_fail(x, y):
		push_error("x value: {0} not found".format(str(x)))

func normalize_value(value: float, minimum: float, maximum: float) -> float:
	if maximum == minimum:
		return 1
	maximum = maximum - minimum
	value = value - minimum
	return value / maximum

func normalize_all_data() -> void:
	#print("normalizing: " + str(data_numbers.size()))
	data_normalized = []
	for d in data_numbers:
		d[0] = normalize_value(d[0], smallest_x, largest_x)
		d[1] = normalize_value(d[1], smallest_y, largest_y)
		data_normalized.append(d)
	for d in data_normalized:
		print(d)
	#print("normalized: " + str(data_normalized.size()))

func draw_graph() -> void:
	line.clear_points()
	
	var i: int = 0
	for d in data_normalized:
		d[0] = d[0] * size.x
		d[1] = (size.y - (d[1] * size.y))
		line.add_point(Vector2(d[0], d[1]), i)
		i+=1

func _init() -> void:
	DEFAULT_LINE_2D = Line2D.new()
	DEFAULT_LINE_2D.set_default_color(line_color)
	DEFAULT_LINE_2D.set_width(width)
	DEFAULT_LINE_2D.width_curve = width_curve
	line = DEFAULT_LINE_2D.duplicate()
	add_child(line)

var _stop_process: bool = false
var _frames_alive: int = 0

func _process(_delta: float) -> void:
	#stuttered init to win the data race
	if _stop_process == false:
		_frames_alive += 1
	
	if _frames_alive > 3:
		_frames_alive = 0
		_stop_process = true
		line.set_default_color(line_color)
		line.set_width(width)
		line.width_curve = width_curve
		
		#Fill
		line.gradient = gradient
		line.texture = texture
		line.texture_mode = int(texture_mode)
		
		#Capping
		line.joint_mode = int(joint_mode)
		line.begin_cap_mode = int(begin_cap_mode)
		line.end_cap_mode = int(end_cap_mode)
		
		#Border
		line.sharp_limit = sharp_limit
		line.round_precision = round_precision
		line.antialiased = antialiased

I have no concept to what might be causing this bug. The only reason I created my own Godot Line Graph is because I thought it would be easier than learning someone else's implementation

Did you check if a deep duplication helps?

If your data is nested to deep duplicate() might not get it and therefore you manipulating more references than you want and all graphs are the same as the result, maybe.

data_numbers and data_normalized are still referencing the inner arrays of the data argument passed to initialize_graph(). This causes normalize_all_data() and draw_graph() to modify the values for every LineGraph initialized from the same array.

As @kraasch already pointed at, you have to duplicate the arrays before changing their values - for example by deep duplicating the whole nested array.

that fixed the issue when duplicating the graph inside of code, but if i duplicate the graphs in the editor the first issue still persist.

You have a reference issue with the array you pass into your library. A quick fix might be to create your graph with another wrapper around initialized_graph():

extends Control

@onready var line_graph_1: LineGraph = %line_graph
@onready var line_graph_2: LineGraph = %line_graph2

var example_data: Array[int] = [0, 1, 2, 2, 2, 1, 0, 1, 2, 3, 4, 4, 4, 4, 3, 3, 2, 2, 3, 3, 3, 0, 0, 1 ,1, 2, 2, 2, 3, 4, 4, 4, 3, 4, 4, 4, 3, 3, 2, 2, 2, 2, 2, 2, 2, 1, 1, 0]

func _ready() -> void:
	line_graph_1.initialize_graph_fix(example_data)
	line_graph_2.initialize_graph_fix(example_data)

With initialize_graph_fix() looking like this:

func initialize_graph_fix(arr: Array):
	var data = []
	var i: int = 0
	while(i < arr.size()):
		data.append([i, arr[i]])
		i+=1
	initialize_graph(data)

Godot also has an append_array() function, so you can clean up your quicksort like so:



func _quicksort_data(data_array) -> Array:
	if data_array.size() < 1:
		return []
	var index: Array = data_array[0]
	var less: Array
	var more: Array
	var i: int = 0
	while(i < data_array.size()-1):
		i+=1
		if data_array[i][0] < index[0]:
			less.append(data_array[i])
		else:
			more.append(data_array[i])
	less = _quicksort_data(less)
	more = _quicksort_data(more)
	var new_array: Array = []
	new_array.append_array(less)
	new_array.append_array([index])
	new_array.append_array(more)
	return new_array

Your library already depends on Line2D, so you can also hard-code in the types:

@export_subgroup("Fill")
enum LineTextureMode {None, Tile, Stretch}
@export var gradient: Gradient
@export var texture: Texture
@export var texture_mode: Line2D.LineTextureMode

@export_subgroup("Capping")
enum LineJointMode {Sharp, Bevel, Round}
enum LineCapMode {None, Box, Round}
@export var joint_mode: Line2D.LineJointMode
@export var begin_cap_mode: Line2D.LineCapMode
@export var end_cap_mode: Line2D.LineCapMode


# and later....

func _process(_delta: float) -> void:

		# more things here ...

		#Fill
		line.gradient = gradient
		line.texture = texture
		line.texture_mode = texture_mode
		#Capping
		line.joint_mode = joint_mode
		line.begin_cap_mode = begin_cap_mode
		line.end_cap_mode = end_cap_mode
		#Border
		line.sharp_limit = sharp_limit
		line.round_precision = round_precision
		line.antialiased = antialiased

In general, and especially for libraries that do simple math things like graphs this, I recommend a test-driven approach (for example with GDUnit4 or with GUT).

I changed my data_numbers: Array → data_numbers: Array[Vector2] and that somehow fixed everything. I don’t understand what the bug was, I don’t understand why that fixed it.