Подключение dcc.Слайдера к обратному вызову clientside_call в Dash для анимированного графика

#python #plotly #plotly-dash #plotly-python

Вопрос:

Я хочу иметь базовое приложение для Тире с анимированной диаграммой, которую вы можете воспроизводить/приостанавливать, а также изменять время, отображаемое с помощью ползунка dcc.. Мне также нужно, чтобы был доступен текущий кадр, в котором отображается анимация, чтобы я мог также отображать дополнительные графики/таблицы данных, связанные с отображаемыми в данный момент данными, поэтому я не могу просто использовать график с плотностью.

Мои данные достаточно велики (думаю, ~3 столбца, 50 тысяч строк, при этом отображается ~60 точек одновременно), поэтому использование обратных вызовов на стороне сервера происходит очень медленно. Поэтому вместо этого я передаю фрагменты данных в хранилище dcc.Каждые n интервалов, использую другое хранилище dcc.для отслеживания текущего кадра анимации, а затем использую обратный вызов на стороне клиента для обновления графика.

На данный момент у меня настроено значение ползунка, чтобы отразить сохраненную рамку, чтобы она автоматически обновлялась. Тем не менее, у меня возникли проблемы с определением способа, позволяющего пользователю перемещать значение ползунка и соответствующим образом обновлять график. Так как dcc.Store с кадром обновляется в обратном вызове на стороне клиента, его нельзя обновить в другом месте. Это простая версия того, как код выглядит сейчас:

 # -*- coding: utf-8 -*-

# Run this app with `python app.py` and
# visit http://127.0.0.1:8050/ in your web browser.

import pandas as pd
import numpy as np

import dash
import dash_core_components as dcc
import dash_bootstrap_components as dbc
import dash_html_components as html
from dash.dependencies import Input, Output, State
import dash_table # DataTable library

import plotly.express as px
import plotly.graph_objects as go


external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = dash.Dash(__name__, external_stylesheets=[dbc.themes.SLATE])
df = pd.DataFrame(np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]),
                   columns=['x', 'y', 'time'])
# Initial trace/figure
trace = go.Scatter(x=df.x[:1], y=df.y[:1],
                        name='Location',
                        mode='markers'
                        )

fig = go.Figure(data=[trace])

app.layout = html.Div([
    # Animated graph
    dcc.Graph(
        id="graph", 
        figure=fig.update_layout(
            template='plotly_dark',
            paper_bgcolor= 'rgba(0, 0, 0, 0)',
        ),
    ),
    # Slider to control
    dcc.Slider(
        id="slider", 
        min=0, 
        max=len(df), 
        value=0
    ),
    # Buttons
    dbc.ButtonGroup([
        # Play
        dbc.Button(
            "Play",
            color='success', 
            id="dashPlay", 
            n_clicks=0
        ), 
        # Pause
        dbc.Button(
            "Pause", 
            color='danger',
            id="dashPause", 
            n_clicks=0
        ),],
    size="lg"),
    # Datatable
    dash_table.DataTable(
        id='table',
        columns=[{"name": i, "id": i} for i in df.columns], # Don't display seconds
        data=df.to_dict('records')
    ),  
    # Storing clientside data
    dcc.Store(     
        id='store', 
        data=dict(x=df.x, y=df.y) # Store figure data
        ),
    dcc.Store(
        id='frame', 
        data=0 # Store the current frame
        ),

    # Client-side animation interval
    dcc.Interval(
        id="animateInterval", 
        interval=2000, 
        n_intervals=0, 
        disabled=True
        ),
])

# Update the animation client-side
app.clientside_callback(
    """
    function (n_intervals, data, frame) {
        frame = frame % data.x.length;
        const end = Math.min((frame   1), data.x.length);
        return [[{x: [data.x.slice(frame, end)], y: [data.y.slice(frame, end)]}, [0], 1], end, end]
    }
    """,
    [Output('graph', 'extendData'), Output('frame', 'data'), Output('slider', 'value')],
    [Input('animateInterval', 'n_intervals')], [State('store', 'data'), State('frame', 'data')]
)

# Handles updating the data tables
# --------------------------------
# Updates the currently displayed info to show the
# current second's data.
# https://community.plotly.com/t/update-a-dash-datatable-with-callbacks/21382/2
@app.callback(
    Output("table", "data"),
    Input("frame", "data"),
)
def updateTable(frame):
    # Once the animation has started...
    if frame:
        return (df.loc[df['time'] == frame]).to_dict('records')
    else:
        return (df.loc[df['time'] == 0]).to_dict('records')


# Handles pause/play
# ------------------
# Starts/pauses the 'interval' component, which starts/pauses 
# the animation.
@app.callback(
    Output("animateInterval","disabled"),
    Input("dashPlay", "n_clicks"),
    Input("dashPause", "n_clicks"),
    State("animateInterval","disabled"),
)
def pause_play(play_btn, pause_btn, disabled):
    # Check which button was pressed
    ctx = dash.callback_context
    if not ctx.triggered:
        return True
    else:
        button = ctx.triggered[0]['prop_id']
        if 'dashPlay' in button:
            return False
        else:
            return True

if __name__ == '__main__':
    app.run_server(debug=True)
 

Как вы, вероятно, видите, на данный момент нет способа изменить значение ползунка и изменить кадр анимации.

Моей первой мыслью было просто добавить проверку контекста в обратный вызов на стороне клиента и попытаться учесть изменение ползунка там, но я не мог понять, как это сделать с помощью clientside_callback.

Единственное, о чем я могу думать, — это наличие второго хранилища dcc.Store с текущим кадром, поэтому есть магазин A и магазин B. Затем у меня было бы два обратных вызова: обратный вызов на стороне клиента для обновления анимации и еще один для считывания любых изменений в значениях ползунка. Тогда структура была бы:

Обратный вызов на стороне клиента: Вход: Магазин A Выход: Магазин B

Обратный вызов слайдера: Вход: Магазин B Выход: Магазин A

Затем, если значение ползунка изменится (т. Е. Пользователь переместил ползунок), это будет отражено в хранилище A и обновится в анимации. Аналогично, ползунок будет обновлен обратным вызовом на стороне клиента и перемещен на соответствующее значение. Тем не менее, это, похоже, вновь вводит ожидание на сервере в анимацию графика, и кажется, что это должно быть решенной проблемой с лучшим решением. Я был бы рад любому совету на эту тему!

Ответ №1:

Что вы можете сделать, так это уловить любое вмешательство пользователя, когда он играет со слайдером. Затем вы устанавливаете рамку на значение, полученное с помощью ползунка.

В вашем случае это может быть что-то вроде:

 app.clientside_callback(
"""
function (n_intervals,slider_value, data, frame) {

    const triggered = dash_clientside.callback_context.triggered.map(t => t.prop_id);

    frame = frame % data.x.length;

    if (triggered == "slider.value") {
                frame = slider_value;
            }

    const end = Math.min((frame   1), data.x.length);
    return [[{x: [data.x.slice(frame, end)], y: [data.y.slice(frame, end)]}, [0], 1], end, end]
}
""",
[Output('graph', 'extendData'), Output('frame', 'data'), Output('slider', 'value')],
[Input('animateInterval', 'n_intervals'),Input('slider', 'value')], [State('store', 'data'), State('frame', 'data')])