在 Dash 中动态更改绘图范围时,保持 plotly updatemenu 按钮处于活动状态

Keep plotly updatemenu button active when changing plot range dynamically in Dash

我需要一些关于 plotly 中 updatemenu 按钮的建议。我正在动态更改图表的绘图范围,并使用 updatemenu 按钮触发不同的轨迹。但是,只要我移动范围滑块,就会再次显示默认轨迹(并且上一个按钮未被选中或不再处于活动状态)。我希望视图保留在先前选择的(通过 updatemenu 按钮)轨迹上。

我花了大约 8 个小时来修复它,这是我最后的选择 :D 否则,它不会得到修复。

我确实相信这是可能的。我认为这超出了我目前的技能范围。也许有人可以想出一个巧妙的实现方式。

提前致谢:)

我的想法


  1. 属性 uirevision
  2. 自定义按钮,修改一个全局变量
  3. 创建一个额外的回调来监听 update 方法并将其保存在全局变量中
  4. 在我的回调中生成新图形之前读出图形的状态
  5. 没有为任何跟踪设置 visible 属性

更新:想法 4 按预期工作,请参阅下面的答案。

对想法的一些评论:

  1. 我认为这可能是最简单的方法。但是,当我将它设置为常量值时,它只保存了 zoom/selected 数据。它没有保持之前激活的痕迹可见。
  2. 由于按钮数量的变化,这是迄今为止最耗时的尝试。我能够读出哪个按钮被按下了,但我无法将它保存在一个全局变量中(我知道不推荐这样做,但这里无所谓)。即使 dcc.Store 它也不起作用,因为我没有修改它而改变了值。
  3. 不知怎么只能听restylerelayout方法。我无法用这两种方法切换轨迹,因此我没有继续。
  4. 我尝试将图形作为状态包含在 @ app.callback()dash.State 中。但是,我无法获取当前活动的按钮。
  5. 我目前的做法是,它在初始化时工作,并且在更改滑块时立即看到一个图表。如果不提供 visible 属性,更改后将看不到任何痕迹。此外,我无法重新创建所需的行为。

最小工作示例(已实施答案)


实施意见

应用程序的当前版本要复杂得多。我在回调中有更多的元素,数据集有点复杂。

由于更改了数据集中的列,重要的是,我在读出列后创建轨迹和更新菜单按钮。固定数量和固定 labels/ids.

会更容易

进口:

import dash
import dash_core_components as dcc
import dash_html_components as html
import plotly.graph_objects as go
import numpy as np
import webbrowser
import pandas as pd

申请:

def launch_app(data) -> None:

    # Creates the Application
    app = dash.Dash(__name__)

    # HTML Layout
    app.layout = html.Div(children=[
        html.Div([
            # Graph Container
            html.Div(id='graph-container'),
            # Range Slider
            html.Div(
                dcc.RangeSlider(id='range-slider', min=0, vertical=True),
            ),
        ], style={"display": "flex", "align-items": "center", "justify-content": "space-evenly"}),
    ])

    # Callback to update graphs when slider changes
    @ app.callback(
        dash.Output("range-slider", "value"),
        dash.Output("graph-container", "children"),
        dash.State("graph-container", "children"),
        dash.Input("range-slider", "value"),
    )
    def update(graph_container, slider):

        # Setting max
        max = len(data["data"]["petal_length"])

        # First call to initialize
        if slider is None:
            end_value = max
            slider_value = [0, end_value]
            return slider_value, update_graphs(0, end_value)

        # Get active Button
        active_button = graph_container[0]['props']['figure']['layout']['updatemenus'][0]['active']

        # Set values depending on the trigger
        start_value = slider[0]
        end_value = slider[1]
        slider_value = slider

        return slider_value, update_graphs(start_value, end_value, active_button)

    # Running the app
    app.run_server(debug=True, port=8050)

生成新图:

def update_graphs(start: int, end: int, active_button: int = 0) -> list[dcc.Graph]:
    """
    Updates the dcc.Graph and returns the updated ones.

    Parameters
    ----------
    start : int
        lower index which was selected
    end : int
        upper index which was selected
    active_button: int
        which button is active of the plotly updatemenu buttons, by default 0

    Returns
    -------
    list[dcc.Graph]
        Updated Graph
    """

    fig = go.Figure()

    # Read out columns automatically
    all_columns = [col for col in list(data["data"].head())]

    # Generate X-Axis
    xvalues = np.arange(0, len(data["data"]["petal_length"]))

    # CREATION OF TRACES
    for ii, col in enumerate(all_columns):
        if ii == active_button:
            visible = True
        else:
            visible = False
        fig.add_trace(
            go.Scatter(x=xvalues[start: end],
                       y=data["data"][f"{col}"][start: end],
                       visible=visible)
        )

    # Generation of visible array for buttons properties
    show_list = []
    for ii, val in enumerate(all_columns):
        # Initialization
        temp = np.zeros(len(all_columns))
        temp[ii] = 1
        temp = np.array(temp, dtype=bool)
        show_list.append(temp)

    # CREATION OF BUTTONS
    all_buttons = []

    for ii, col in enumerate(all_columns):
        all_buttons.append(dict(label=f"{col}", method="update", args=[
            {"visible": show_list[ii]},
            {"yaxis": {'title': f'yaxis {col}'}}
        ]))

    # Update Menu Buttons
    fig.update_layout(
        updatemenus=[
            dict(
                type="buttons",
                active=active_button,
                showactive=True,
                buttons=all_buttons,
                x=0.0,
                y=1.2,
                xanchor="left",
                yanchor="top",
                direction="right",
            )
        ])

    return [
        dcc.Graph(
            id='time',
            figure=fig,
        )
    ]

脚本:

if __name__ == "__main__":

    # Loading sample Data
    iris = pd.read_csv(
        'https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv')
    data = {"id": 20, "data": iris}

    # Opens the server
    webbrowser.open("http://127.0.0.1:8050/")
    # Launches the server
    launch_app(data)

您的想法 (4) 将图形(或其父对象)作为状态包含在回调函数中 update 应该可行。您只需找到正确的“活动”属性,然后将活动按钮作为参数传递给您的 update_graphs 函数。

应进行以下更改:

  1. 添加“graph-container”作为输入。
@ app.callback(
        dash.Output("range-slider", "value"),
        dash.Output("graph-container", "children"),
        dash.Input("graph-container", "children"),
        dash.Input("range-slider", "value"),
    )
    def update(graph_container, slider):
  1. 获取当前活动按钮的索引并将其传递给update_graphs
active_button = graph_container[1]['props']['figure']['layout']['updatemenus'][0]['active']

return slider_value, update_graphs(start_value, end_value, active_button=active_button)
  1. update_graphs中接收active_button索引并使用它。
def update_graphs(start: int, end: int, active_button: int):
  # Some stuff
  # Update Menu Buttons
    fig.update_layout(
        updatemenus=[
            dict(
                # other stuff
                active=active_button,
                
            )
        ])