fenix-hub / godot-engine.easy-charts

A Godot Engine addon for plotting general purpose charts. A collection of Control, 2D and 3D Nodes to plot every chart possible.
MIT License
647 stars 48 forks source link

[BUG] Invalid get index 'lb' (on base: 'Dictionary') #84

Closed Tianmaru closed 6 months ago

Tianmaru commented 1 year ago

Calling plot on chart after it was added to the scene tree (so after ready, for example to plot some new functions) results in a crash. The error message reads:

Invalid get index 'lb' (on base: 'Dictionary')

After some experimenting, the error is most likely caused by the FunctionPlotter's x_domain and y_domain values not being initialized properly yet. This means, that the _draw function is called on the function_plotters, before it is called on chart, since this would set the domains, right? Adding the following to the end of the load_functions method of chart seems to solve the issue, but causes some other unexpected side effects:

var x_domain: Dictionary = calculate_domain(x)
var y_domain: Dictionary = calculate_domain(y)
for function_plotter in functions_box.get_children():
    function_plotter.update_values(x_domain, y_domain)
Tianmaru commented 1 year ago

If plot is called repeatedly, older function_plotter nodes should also be removed from the scene tree, if not already doing so.

fenix-hub commented 1 year ago

Mind sharing your main script please?

fenix-hub commented 1 year ago

By the way:

This means, that the _draw function is called on the function_plotters, before it is called on chart, since this would set the domains, right?

the _draw() function is called first on the Chart object, and then on all the FunctionPlotters nodes. In this way the Chart sets the domain for the grid, and then functions are plotted.

Tianmaru commented 1 year ago

For example, if I change the script example/bar_chart/Control.gd to

extends Control

@onready var chart: Chart = $VBoxContainer/Chart

# This Chart will plot 3 different functions
var f1: Function

func _ready():
        hide()
    set_process(false)

func _unhandled_input(event):
    if event.is_action_pressed("ui_accept"):
        show()
        plot()

func plot():
    # Let's create our @x values
    var x: Array = ["Day 1", "Day 2", "Day 3", "Day 4"]

    # And our y values. It can be an n-size array of arrays.
    # NOTE: `x.size() == y.size()` or `x.size() == y[n].size()`
    var y: Array = [20, 10, 50, 30]

    # Let's customize the chart properties, which specify how the chart
    # should look, plus some additional elements like labels, the scale, etc...
    var cp: ChartProperties = ChartProperties.new()
    cp.colors.frame = Color("#161a1d")
    cp.colors.background = Color.TRANSPARENT
    cp.colors.grid = Color("#283442")
    cp.colors.ticks = Color("#283442")
    cp.colors.text = Color.WHITE_SMOKE
    cp.y_scale = 10
    cp.draw_origin = true
    cp.draw_bounding_box = false
    cp.draw_vertical_grid = false
    cp.interactive = true # false by default, it allows the chart to create a tooltip to show point values
    # and interecept clicks on the plot

    # Let's add values to our functions
    f1 = Function.new(
        x, y, "User", # This will create a function with x and y values taken by the Arrays 
                        # we have created previously. This function will also be named "Pressure"
                        # as it contains 'pressure' values.
                        # If set, the name of a function will be used both in the Legend
                        # (if enabled thourgh ChartProperties) and on the Tooltip (if enabled).
        {
            type = Function.Type.BAR,
            bar_size = 5
        }
    )

    # Now let's plot our data
    chart.plot([f1], cp)

    # Uncommenting this line will show how real time data plotting works
    set_process(false)

var new_val: float = 4.5

func _process(delta: float):
    # This function updates the values of a function and then updates the plot
    new_val += 5

    # we can use the `Function.add_point(x, y)` method to update a function
    f1.add_point(new_val, cos(new_val) * 20)
    chart.queue_redraw() # This will force the Chart to be updated

func _on_CheckButton_pressed():
    set_process(not is_processing())

it will throw said error message after pressing return a second time.

I was trying to create a separate project for demonstration, but for some reason, the data_tooltip folder appears to be missing from containers now when installing Easy Chart 4.x from the Asset Library? Or am I just being very confused right now?

Tianmaru commented 1 year ago

the _draw() function is called first on the Chart object, and then on all the FunctionPlotters nodes. In this way the Chart sets the domain for the grid, and then functions are plotted.

Yeah, I also assumed it would work that why. To be honest, I am not entirely sure why I receive that error and what is happening. But x_domain and y_domain of function plotters not being set correctly seems to be involved in some way.

fenix-hub commented 1 year ago

For example, if I change the script example/bar_chart/Control.gd to

extends Control

@onready var chart: Chart = $VBoxContainer/Chart

# This Chart will plot 3 different functions
var f1: Function

func _ready():
        hide()
  set_process(false)

func _unhandled_input(event):
  if event.is_action_pressed("ui_accept"):
      show()
      plot()

func plot():
  # Let's create our @x values
  var x: Array = ["Day 1", "Day 2", "Day 3", "Day 4"]

  # And our y values. It can be an n-size array of arrays.
  # NOTE: `x.size() == y.size()` or `x.size() == y[n].size()`
  var y: Array = [20, 10, 50, 30]

  # Let's customize the chart properties, which specify how the chart
  # should look, plus some additional elements like labels, the scale, etc...
  var cp: ChartProperties = ChartProperties.new()
  cp.colors.frame = Color("#161a1d")
  cp.colors.background = Color.TRANSPARENT
  cp.colors.grid = Color("#283442")
  cp.colors.ticks = Color("#283442")
  cp.colors.text = Color.WHITE_SMOKE
  cp.y_scale = 10
  cp.draw_origin = true
  cp.draw_bounding_box = false
  cp.draw_vertical_grid = false
  cp.interactive = true # false by default, it allows the chart to create a tooltip to show point values
  # and interecept clicks on the plot

  # Let's add values to our functions
  f1 = Function.new(
      x, y, "User", # This will create a function with x and y values taken by the Arrays 
                      # we have created previously. This function will also be named "Pressure"
                      # as it contains 'pressure' values.
                      # If set, the name of a function will be used both in the Legend
                      # (if enabled thourgh ChartProperties) and on the Tooltip (if enabled).
      {
          type = Function.Type.BAR,
          bar_size = 5
      }
  )

  # Now let's plot our data
  chart.plot([f1], cp)

  # Uncommenting this line will show how real time data plotting works
  set_process(false)

var new_val: float = 4.5

func _process(delta: float):
  # This function updates the values of a function and then updates the plot
  new_val += 5

  # we can use the `Function.add_point(x, y)` method to update a function
  f1.add_point(new_val, cos(new_val) * 20)
  chart.queue_redraw() # This will force the Chart to be updated

func _on_CheckButton_pressed():
  set_process(not is_processing())

it will throw said error message after pressing return a second time.

I was trying to create a separate project for demonstration, but for some reason, the data_tooltip folder appears to be missing from containers now when installing Easy Chart 4.x from the Asset Library? Or am I just being very confused right now?

Could you try testing over the main branch please? Eventually the version you are using (I suppose from the AssetLib) had some bugs in it.

Snafuh commented 1 year ago

Hi I'm having very similar issues with the main branch code. The error disappears when calling queue_redraw() before plot.

    #chart.queue_redraw() # this fixes the crash
    var x_axis = [1, 2, 3];
    var y_axis = [1, 2, 3];
    var F = Function.new(x_axis, y_axis, "Eh", {color = Color.WEB_GREEN, type = Function.Type.LINE })
    chart.plot([F])

This code is called on a button click.

If plot is called repeatedly, older function_plotter nodes should also be removed from the scene tree, if not already doing so.

There is also an error happening here. When switching between a plot with a single function and one with 2 functions, I receive a graph with 3 functions when trying to display 2. The single plot works correctly. I will open a separate Issue for this though, after investigating the issue myself a bit more.

fenix-hub commented 6 months ago

Closing due to inactivity. Feel free to reopen.