plotlyplotly.jshtmx

How to update a Plotly graph's data with HTMX keeping the current view state (zoom/pan/etc...)


I use FastAPI for a Python backend and Plotly to generate a plot from some data. On the client side I use HTMX.

The server sends directly the Plot's HTML using Figure.to_html( full_html = False) and HTMX swaps it directly in the correct position in the page.

This was all very convenient and easy to do, but now I have the need of live-updating the plot when the data changes. So I've set up a websocket connection using FastAPI and the ws extension of HTMX, to update the plot when needed.

The problem is, that if I send the entire plot again at every update of the data, the entire thing gets swapped by HTMX and the user looses the current view state of the plot, namely zoom rate, pan position, etc...

So is there a way to be more granular about what to send from Plotly?

I would need to:

  1. setup the plot only once (with Plotly.js, I suppose?)
  2. send the data separately (exporting them from the Plotly python library?)
  3. update the plot with the new data in a way that does not disrupt the user's view state.

How do I do that? is it possible?


Solution

  • HTMX aside (the principle should remain the same), I would recommend :

    1. The Python backend serves the initial page : it contains an empty div wrapper with a specific id, the initial data and the scripts for creating/updating the plot with Plotly.js :

    2. The backend send live data (JSON) to the client.

    3. The client append new data to current data and update the plot accordingly (see Plotly.js Function Reference)

    <head>
      <!-- Load plotly.js into the DOM -->
      <script src='https://cdn.plot.ly/plotly-2.35.2.min.js'></script>
    </head>
    <body>
      <div id='myDiv'><!-- Plotly chart will be drawn inside this DIV --></div>
    </body>
    <script>
      // Create graph with initial data
      const gd = document.getElementById('myDiv');
      const data = [{ /* ... */ }];
      const layout = { /* ... */ };
      Plotly.newPlot(gd, data, layout);
    
      // Update graph when receiving new data
      function onUpdate(newData) {
        append(data, newData); // custom function, depends on data (plotly trace type)
        Plotly.restyle(gd, data); // use Plotly.restyle to update data only
      }
    
      /* Logic for updating data (register `onUpdate`, define `append` etc.) ... */ 
    </script>
    

    You can also initialize the whole figure in python to leverage Plotly.py and its templates (which are not included in Plotly.js), so you don't have to do that manually in javascript. Use fig.to_json() to serialize the whole figure, eg.

    import plotly.express as px
    
    df = px.data.medals_wide()
    fig = px.bar(df, x='nation', y=['gold', 'silver', 'bronze'])
    fig_json = fig.to_json()
    

    and inject fig_json in your script (instead of just those data props that need to be updated) :

    // Create graph with initial data
    const gd = document.getElementById('plot');
    const fig = JSON.parse(fig_json);
    const data = fig.data;
    const layout = fig.layout;
    Plotly.newPlot(gd, data, layout);
    

    In order to properly serialize the data props (apart from the figure), specifically when they contain types provided by third-party modules like numpy, pandas, PIL, use pio.json.to_json_plotly() :

    import numpy as np
    import plotly.io as pio
    
    data = dict(x=np.array([1, 2, 3]), y=np.array([1, 2, 3]))
    data_json = pio.json.to_json_plotly(data)