Plot data shifts vertically after interacting with rangeslider

Upon interacting with range slider in my scatter plot the data keeps shifting vertically towards the top and I do not know how to stop it. Previously, the entire plot kept stretching vertically downwards and after setting a fixed height this is the behaviour now. This ONLY happens when i try to plot just one column from a pandas dataframe, it all works as expected with more than one column. Moreover, the behaviour fixes itself after I minimize and maximize the browser.

Right at this moment I can not provide any code snippets as I’d need to rewrite a clean version of the code for it (do not want to paste something work sensitive by accident).

This is how the basic plot looks initially:

Then after interacting with the rangeslider:

The more you interact with it the more the data shifts:

As I was taking these screenshots I found another two ways to stop this behaviour:

  • move the left rangeslider edge past the right one,
  • select a VERY small portion of data.

None of these work in my case as on default the data should be loaded as β€œall”.

Are there some common mistakes I’ve been a subject to or should I take some time and provide a clean code snippet?

EDIT: Decided to take some time and write a quick and dirty code snippet with identical behaviour:

app.py

from flask import Flask, render_template, Response, request, jsonify
import pandas as pd
import numpy as np
import plotly.graph_objects as go

app = Flask(__name__)


def line_stacked(table_name: str, columns: list[str], distribution: str, inflation: bool = False) -> go.Figure():
    """
    Makes a stacked line plot where each additional value is put on top of the previous one, it can display it either
    in nominal values or as a proportional of total.

    Parameters
    ----------
    table_name: str
        Name of a SQL table to query
    columns: list[str]
        A list of columns to display in the plot
    distribution: str
        Keyword for either nominal or distributional value display
    inflation: bool, Default False
        Adjust historic data by Retail Price Index to display it in modern values

    Returns
    -------
    fig: go.Figure()
        Plotly stacked line plot figure
    """
    np.random.seed(0)
    date_range = pd.date_range(start='2018-01-01', end='2023-12-01', freq='MS')
    data = {
        "date": date_range,
        "var1": np.random.randint(1000, 5000, size=len(date_range)),
        "var2": np.random.randint(1000, 5000, size=len(date_range)),
        "var3": np.random.randint(1000, 5000, size=len(date_range)),
        "var4": np.random.randint(1000, 5000, size=len(date_range)),
    }

    df = pd.DataFrame(data, index=None)
    df["grp1"] = df["var1"] + df["var2"]
    df["grp2"] = df["var3"] + df["var4"]
    df["total"] = df["grp1"] + df["grp2"]

    # Usually this is used to query data.
    # df = fetch_data_as_df(table_name, current_user.owner)

    # Commented out since does not impact the behaviour
    # if inflation:
    #     df = adjust_df_for_inflation(df)

    fig = go.Figure()

    if distribution == "proportional":
        for col in columns:
            fig.add_trace(go.Scatter(x=df["date"],
                                     y=(df[col] / df["total"]).fillna(0),
                                     mode="lines",
                                     fill="tonexty",
                                     name=col.title(),
                                     stackgroup="one",
                                     hoverinfo="x+y+text",
                                     text=f"{col.title()}",
                                     showlegend=True))
        fig.update_layout(
            yaxis=dict(tickvals=[i / 10 for i in range(11)], range=[0, 1], title="Proportion (100%)")
        )

    elif distribution == "nominal":
        for col in columns:
            fig.add_trace(go.Scatter(x=df["date"], y=df[col],
                                     mode="lines",
                                     fill="tonexty",
                                     name=col.title(),
                                     stackgroup="one",
                                     hoverinfo="x+y+text",
                                     text=f"{col.title()}",
                                     showlegend=True))

    fig.update_layout(
        xaxis=dict(
            title="Date",
            rangeselector=dict(
                buttons=list([
                    dict(count=1, label="1y", step="year", stepmode="backward"),
                    dict(count=2, label="2y", step="year", stepmode="backward"),
                    dict(count=5, label="5y", step="year", stepmode="backward"),
                    dict(step="all", stepmode="backward")
                ])
            ),
            rangeslider=dict(visible=True)
        ),
        hovermode="x unified")

    return fig


@app.route("/lineplot", methods=["POST"])
def lineplot() -> Response | None:
    data = request.json
    table_name = data["table_name"]
    columns = data["column_names"]
    distribution = data["distribution"]
    inflation = data["inflation"]

    fig = line_stacked(table_name, columns, distribution, inflation)

    return jsonify(fig.to_json())


@app.route("/")
def index() -> str:
    return render_template("index.html")


if __name__ == "__main__":
    app.run(debug=True)

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
          rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
          crossorigin="anonymous">
    <script src="https://cdn.plot.ly/plotly-latest.min.js" charset="utf-8"></script>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <style>
        .slim-select {
            max-width: 200px;
        }
        .plot-container {
            margin-top: 20px;
            padding: 20px;
            border: 1px solid #ddd;
            border-radius: 5px;
            background-color: #f9f9f9;
        }
        .form-check-label {
            margin-left: 8px;
        }
        .lineplot{
            height: 500px;
        }
    </style>
    <title>Plot Bug</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>    
    <div class="container mt-5">
    <div class="plot-container">
        <form id="plotForm" class="row g-3 align-items-center">
            <div class="col-md-3 slim-select">
                <label for="sortby" class="form-label">Sort by</label>
                <select id="sortby" name="sortby" class="form-select">
                    <option value="sector">Sector</option>
                    <option value="category">Category</option>
                </select>
            </div>
            <div class="col-md-3 slim-select">
                <label for="columns" class="form-label">Selection</label>
                <select id="columns" name="columns" class="form-select">
                    <!-- To be populated by JS -->
                </select>
            </div>
            <div class="col-md-3 slim-select">
                <label for="distribution" class="form-label">Distribution</label>
                <select id="distribution" name="distribution" class="form-select">
                    <option value="nominal">Nominal</option>
                    <option value="proportional">Proportional</option>
                </select>
            </div>
            <div class="col-md-2">
                <div class="form-check pt-4">
                    <input type="checkbox" id="inflation" name="inflation" class="form-check-input">
                    <label for="inflation" class="form-check-label">Inflation</label>
                </div>
            </div>
        </form>
        <div id="plot" class="mt-5 lineplot"></div>
    </div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/5.3.0/js/bootstrap.min.js"></script>

    <script>
        $(document).ready(function() {
            const sectorOptions = {
                detailed: ["var1", "var2", "var3", "var4"],
                grouped: ["grp1", "grp2"],
                total: ["total"]
            };

            const categoryOptions = {
                detailed: ["var1", "var2", "var3", "var4"],
                grouped: ["grp1", "grp2"],
                total: ["total"]
            };

            const config = {responsive: true};

            function updateColumnsDropdown(sortbyDropdown) {
                const columnsDropdown = $("#columns");
                columnsDropdown.empty();

                let options;
                if (sortbyDropdown === "sector") {
                    options = sectorOptions;
                } else if (sortbyDropdown === "category") {
                    options = categoryOptions;
                }

                if (options) {
                    columnsDropdown.append("<option value='total'>Total</option>>");
                    columnsDropdown.append("<option value='grouped'>Grouped</option>");
                    columnsDropdown.append("<option value='detailed'>Detailed</option>")
                }

                submitForm();
            }

            function submitForm() {
                var table_name = $("#sortby").val();
                var columnsSelection = $("#columns").val();
                var columns = table_name === "category" ? categoryOptions[columnsSelection] : sectorOptions[columnsSelection];
                var distribution = $('#distribution').val();
                var inflation = $('#inflation').is(':checked');

                var formData = {
                    table_name: table_name,
                    column_names: columns,
                    distribution: distribution,
                    inflation: inflation
                };

                $.ajax({
                    type: 'POST',
                    url: '/lineplot',
                    contentType: 'application/json',
                    data: JSON.stringify(formData),
                    success: function (response) {
                        var plotData = JSON.parse(response);
                        Plotly.purge("plot");
                        Plotly.newPlot('plot', plotData.data, plotData.layout, config);
                    },
                });
            }


            $("#plotForm").submit(function(event){
                event.preventDefault();
                submitForm();
            });


            $("#sortby").change(function () {
                const sortbySelection = $(this).val();
                updateColumnsDropdown(sortbySelection);
            });

            $("#columns, #distribution, #inflation").change(function () {
                submitForm();
            });

            updateColumnsDropdown($("#sortby").val())});
    </script>
</body>

I had the same issue.

My workaround was, to trigger a manual resize event after each plotly relayout event.

window.dispatchEvent(new Event(β€˜resize’));