Search code examples
pythongraphkivywidgetdimension

Kivy custom widget to plot data, can't get current widget dimensions


I'd like to create a custom kivy widget that plots data in response to user input or live location data. I need the current dimensions and position of the widget to draw the line, but I can't seem to get those correctly, and I can't trigger a line update either with a button click for example.

Here is a sample code, when button is clicked, the GraphWidget should plot the line.

import numpy as np
from kivy.app import App
from kivy.properties import ObjectProperty, NumericProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.widget import Widget

class BoxLayoutMain(BoxLayout):

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        self.gw = GraphWidget()

    def plot_line(self):
        # called when user clicks button
        data = np.asarray([[0,0],[100,100],[200,50]]) # test data
        self.gw.plot(data, min_x=0, max_x=200, min_y=0, max_y=150)

class GraphWidget(Widget):

    trace_points = ObjectProperty((0, 0, 0, 0))

    graph_w = NumericProperty(100)
    graph_h = NumericProperty(100)
    graph_x0 = NumericProperty(0)
    graph_y0 = NumericProperty(0)

    def on_size(self, *args):
        # dimensions are correct here:
        self.graph_w = self.width
        self.graph_h = self.height
        self.graph_x0 = self.pos[0]
        self.graph_y0 = self.pos[1]

        self.test()

    def test(self):

        # dimensions are also correct here:
        print('W ', self.graph_w)
        print('H ', self.graph_h)
        print('X ', self.graph_x0)
        print('Y ', self.graph_y0)

        # a test line is drawn when triggered by on_size method
        self.trace_points = [400, 0, 600, 500]

    def plot(self,data_xy, min_x, max_x, min_y, max_y):

        # called when user clicks button
        # dimensions are not correct here:
        print('W ', self.graph_w)
        print('H ', self.graph_h)
        print('X ', self.graph_x0)
        print('Y ', self.graph_y0)

        # transforming data points to pixels, needs correct widget dimensions/position:

        xf = self.graph_w / (max_x - min_x)
        yf = self.graph_h / (max_y - min_y)

        X = (data_xy[:, 0] - min_x) * xf  + self.graph_x0
        Y = (data_xy[:, 1] - min_y) * yf  + self.graph_y0

        nps = np.column_stack((X, Y))

        # line is not drawn/updated
        self.trace_points = list(nps.flatten().astype(int))


class GraphTestApp(App):
    pass

GraphTestApp().run()

The kv file:

BoxLayoutMain:

<BoxLayoutMain>:
    orientation: 'horizontal'

    Button:
        text: 'Click'
        on_press: root.plot_line()

    GraphWidget:

<GraphWidget>:

    canvas:
        Color:
            rgb: 1,0,0
        Line:
            width: 2
            points: self.trace_points

Thanks!


Solution

  • The problem is that the line:

    self.gw = GraphWidget()
    

    in the __init__() mthod of BoxLayoutMain is creating a new instance of GraphWidget, and that instance is not in your GUI. So any reference to self.gw is not relevant to the GraphWidget in your GUI. You just need to get a reference to the correct instance of GraphWidget. One way to do that is by using ObjectProperty and ids. You can modify your BoxLayoutMain to have an ObjectProperty named gw:

    class BoxLayoutMain(BoxLayout):
        gw = ObjectProperty(None)
    
        # def __init__(self, **kwargs):
        #     super().__init__(**kwargs)
        #     self.gw = GraphWidget()
    

    And the __init__() method is no longer needed. Then add the ids to your kv:

    BoxLayoutMain:
    
    <BoxLayoutMain>:
        gw: gw  # set the gw ObjectProperty to the widget with id of gw
        orientation: 'horizontal'
    
        Button:
            text: 'Click'
            on_press: root.plot_line()
    
        GraphWidget:
            id: gw  # id used above to get a reference to this GraphWidget
    
    <GraphWidget>:
    
        canvas:
            Color:
                rgb: 1,0,0
            Line:
                width: 2
                points: self.trace_points