pythonplotlynicegui

How can I update a Plotly Map in NiceGUI with new coordinates?


I assume this is a simple fix that I cannot seem to wrap my head around. I've defined the default values in four NiceGUI ui.inputs and bound them to a dict. I know these values update on_change, but the Plotly plot doesn't. I can't seem to understand what function to create that takes these updated values from the input to the dict to the new plot. Ultimately I want the user to be able to draw and redraw a bounding box on the map by changing the float values in the input.

As is, code simply brings up a map and bounding box that can't be changed. Uncomment the fig.data = [] line and it will start as an empty graph. What is up with that behavior? Why would that function run before the button is clicked?

from nicegui import ui
import plotly.graph_objects as go

bbox_dict = {
            'lat_n': '',
            'lat_s': '',
            'lon_w': '',
            'lon_e': '',
    }

ui.input(label='North Latitude', value=45, placeholder='eg 45.0').bind_value_to(bbox_dict, 'lat_n')
ui.input(label='South Latitude', value=40, placeholder='eg 40.0').bind_value_to(bbox_dict, 'lat_s')
ui.input(label='West Longitude', value=-90, placeholder='eg -90.0').bind_value_to(bbox_dict, 'lon_w')
ui.input(label='East Longitude', value=-88, placeholder='eg -88.0').bind_value_to(bbox_dict, 'lon_e')

lon = [bbox_dict['lon_w'], bbox_dict['lon_e'], bbox_dict['lon_e'], bbox_dict['lon_w']]
lat = [bbox_dict['lat_n'], bbox_dict['lat_n'],bbox_dict['lat_s'], bbox_dict['lat_s']]

lon_mid = (bbox_dict['lon_w']+bbox_dict['lon_e'])/2
lat_mid = (bbox_dict['lat_n']+bbox_dict['lat_s'])/2

fig = go.Figure(go.Scattermap(
    fill = "toself",
    lon = lon, lat = lat,
    marker = { 'size': 10, 'color': "orange" }, 
    name='BBox'))
fig.update_layout(margin=dict(l=0,r=0,t=0,b=0), width=400, showlegend=False,
                  map = {
                      'style': 'carto-darkmatter',
                      'center': {'lon': lon_mid, 'lat': lat_mid },
                      'zoom':5})
plot = ui.plotly(fig)

def update_plt():
    # fig.data = []
    plot.update()

ui.button('Update', on_click=update_plt())

ui.run()

Solution

  • on_click needs function's name without () - on_click=update_plt - and when you will press button then it will use () to execute this function.

    You can see this probably in all GUI's (i.e. tkinter, PyQt), and in many languages (ie. in JavaScript), and often this function's name is called "callback".

    Your code with on_click=update_plt() runs like

    result = update_plt()
    ui.button('Update', on_click=result)
    

    So it runs update_plt() when you start program, and it doesn't wait for click.
    And this runs fig.data = [] at start, and it creates empty graph at start.


    You need:

    ui.button('Update', on_click=update_plt)
    

    As for updating values I would do it in update_plt without fig.data = []

    fig.data[0] should gives access to existing go.Scattermap()

    def update_plt():
        #global lon  # to assign new list to external variable `lon` instead of creating local `lon`
        #global lat  # to assign new list to external variable `lat` instead of creating local `lat`
    
        lon = [bbox_dict['lon_w'], bbox_dict['lon_e'], bbox_dict['lon_e'], bbox_dict['lon_w']]
        lat = [bbox_dict['lat_n'], bbox_dict['lat_n'], bbox_dict['lat_s'], bbox_dict['lat_s']]
    
        fig.data[0].lon = lon
        fig.data[0].lat = lat
    
        plot.update()
    

    But I expect that ui.input() gives value as string and it may need to convert to float()

    def update_plt():
        #global lon  # to assign new list to external variable `lon` instead of creating local `lon`
        #global lat  # to assign new list to external variable `lat` instead of creating local `lat`
        #global lon_mid
        #global lat_mid
    
        lon = [bbox_dict['lon_w'], bbox_dict['lon_e'], bbox_dict['lon_e'], bbox_dict['lon_w']]
        lat = [bbox_dict['lat_n'], bbox_dict['lat_n'], bbox_dict['lat_s'], bbox_dict['lat_s']]
        lon = [float(i) for i in lon]
        lat = [float(i) for i in lat]
    
        fig.data[0].lon = lon
        fig.data[0].lat = lat
    
        # ---
    
        lon_mid = (float(bbox_dict['lon_w']) + float(bbox_dict['lon_e'])) / 2
        lat_mid = (float(bbox_dict['lat_n']) + float(bbox_dict['lat_s'])) / 2
    
        fig.update_layout(map={'center': {'lon': lon_mid, 'lat': lat_mid }))
    
        # ---
    
        plot.update()
    

    EDIT:

    It seems bind_value_to() allows to use forward=callback to assign function which will be executed before sending data to binded variable. And it can convert string to float.

    .bind_value_to(bbox_dict, 'lat_n', forward=float)
    

    And again it has to be function's name without ()

    This way it doesn't need float() in update_plt()

    def update_plt():
        #global lon  # to assign new list to external variable `lon` instead of creating local `lon`
        #global lat  # to assign new list to external variable `lat` instead of creating local `lat`
        #global lon_mid
        #global lat_mid
    
        lon = [bbox_dict['lon_w'], bbox_dict['lon_e'], bbox_dict['lon_e'], bbox_dict['lon_w']]
        lat = [bbox_dict['lat_n'], bbox_dict['lat_n'], bbox_dict['lat_s'], bbox_dict['lat_s']]
    
        fig.data[0].lon = lon
        fig.data[0].lat = lat
    
        # ---
    
        lon_mid = (bbox_dict['lon_w'] + bbox_dict['lon_e']) / 2
        lat_mid = (bbox_dict['lat_n'] + bbox_dict['lat_s']) / 2
    
        fig.update_layout(map={'center': {'lon': lon_mid, 'lat': lat_mid}})
    
        # ---
    
        plot.update()
    

    Doc: ui.input() (there is .bind_valuet_to(..., forward=...)


    Full working code:

    from nicegui import ui
    import plotly.graph_objects as go
    
    bbox_dict = {
                'lat_n': '',
                'lat_s': '',
                'lon_w': '',
                'lon_e': '',
        }
    
    ui.input(label='North Latitude', value=45,  placeholder='eg 45.0' ).bind_value_to(bbox_dict, 'lat_n', forward=float)
    ui.input(label='South Latitude', value=40,  placeholder='eg 40.0' ).bind_value_to(bbox_dict, 'lat_s', forward=float)
    ui.input(label='West Longitude', value=-90, placeholder='eg -90.0').bind_value_to(bbox_dict, 'lon_w', forward=float)
    ui.input(label='East Longitude', value=-88, placeholder='eg -88.0').bind_value_to(bbox_dict, 'lon_e', forward=float)
    
    lon = [bbox_dict['lon_w'], bbox_dict['lon_e'], bbox_dict['lon_e'], bbox_dict['lon_w']]
    lat = [bbox_dict['lat_n'], bbox_dict['lat_n'], bbox_dict['lat_s'], bbox_dict['lat_s']]
    
    lon_mid = (bbox_dict['lon_w'] + bbox_dict['lon_e']) / 2
    lat_mid = (bbox_dict['lat_n'] + bbox_dict['lat_s']) / 2
    
    fig = go.Figure(
        go.Scattermap(
            fill="toself",
            lon=lon, lat=lat,
            marker={'size': 10, 'color': "orange"}, 
            name='BBox'
        )
    )
    
    fig.update_layout(
        margin=dict(l=0, r=0, t=0, b=0), 
        width=400, 
        showlegend=False,
        map={
            'style': 'carto-darkmatter',
            'center': {'lon': lon_mid, 'lat': lat_mid},
            'zoom': 5
        }
    )
    
    plot = ui.plotly(fig)
    
    def update_plt():
        #global lon  # to assign new list to external variable `lon` instead of creating local `lon`
        #global lat  # to assign new list to external variable `lat` instead of creating local `lat`
        #global lon_mid
        #global lat_mid
    
        lon = [bbox_dict['lon_w'], bbox_dict['lon_e'], bbox_dict['lon_e'], bbox_dict['lon_w']]
        lat = [bbox_dict['lat_n'], bbox_dict['lat_n'], bbox_dict['lat_s'], bbox_dict['lat_s']]
    
        fig.data[0].lon = lon
        fig.data[0].lat = lat
    
        # ---
    
        lon_mid = (bbox_dict['lon_w'] + bbox_dict['lon_e']) / 2
        lat_mid = (bbox_dict['lat_n'] + bbox_dict['lat_s']) / 2
    
        fig.update_layout(map={'center': {'lon': lon_mid, 'lat': lat_mid}})
    
        # ---
    
        plot.update()
    
    ui.button('Update', on_click=update_plt)
    
    ui.run()