#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')])