Plotly / Dash отображает данные в реальном времени в плавной анимации

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

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

Вопрос:

Мы пытаемся создать панель мониторинга в режиме реального времени в plotly-dash, которая отображает текущие данные по мере их создания. В целом мы следуем приведенному здесь руководству (https://dash.plotly.com/live-updates ).

У нас есть обратный вызов, который собирает фрагмент новых точек данных из источника примерно каждую секунду, а затем добавляет данные к графику.

Когда мы делаем это, обновление графика прерывистое, потому что мы генерируем новый объект graph при обратном вызове каждую секунду. Мы хотим, чтобы график отображался плавно, даже если это означает, что мы на секунду или две отстаем от данных в реальном времени.

Мы рассматриваем анимации (https://plotly.com/python/animations /) но не ясно, как мы могли бы применить анимацию к потоку данных в реальном времени, добавляемому к графику.

Комментарии:

1. Насколько быстрым должно быть обновление графика? Достаточна ли частота обновления в 1 секунду или она должна быть быстрее?

2. @DavidParks Спасибо, что приняли мой ответ. Как сейчас обстоят дела на вашей стороне?

3. Фантастический ответ, спасибо! Мы наблюдаем снижение производительности с течением времени, когда мы создаем графики в обратном вызове, мы работаем над выяснением, почему ваш код, похоже, не работает так же, потому что кажется, что то, что у нас есть, делает то же самое, просто возвращает цифру.

4. @DavidParks Я вижу… Что ж, я могу только предложить вам, если это возможно с данными и т.д., Попытаться собрать воспроизводимый фрагмент кода, который воспроизводит проблему, и написать другой вопрос, напрямую касающийся проблем с производительностью.

5. Да, теперь у нас есть минимально воспроизводимый пример, но есть несколько сообщений на форуме по теме, которые нам нужно доработать, прежде чем открывать вопрос конкретно по проблеме производительности. Этот вопрос очень помог нам в разработке того, как структурировать процесс. Часть, которую мы добавляем к этому, заключается в том, что у нас есть подграф из 32 трасс с намного большим количеством данных в каждой. Ответом на это могут быть обновления на стороне клиента.

Ответ №1:

Обновление трассировок Graph компонента без создания нового объекта graph может быть достигнуто с помощью extendData свойства. Вот небольшой пример, который добавляет данные каждую секунду,

 import dash
import dash_html_components as html
import dash_core_components as dcc
import numpy as np

from dash.dependencies import Input, Output

# Example data (a circle).
resolution = 20
t = np.linspace(0, np.pi * 2, resolution)
x, y = np.cos(t), np.sin(t)
# Example app.
figure = dict(data=[{'x': [], 'y': []}], layout=dict(xaxis=dict(range=[-1, 1]), yaxis=dict(range=[-1, 1])))
app = dash.Dash(__name__, update_title=None)  # remove "Updating..." from title
app.layout = html.Div([dcc.Graph(id='graph', figure=figure), dcc.Interval(id="interval")])


@app.callback(Output('graph', 'extendData'), [Input('interval', 'n_intervals')])
def update_data(n_intervals):
    index = n_intervals % resolution
    # tuple is (dict of new data, target trace index, number of points to keep)
    return dict(x=[[x[index]]], y=[[y[index]]]), [0], 10


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

В зависимости от сетевого соединения между клиентом и сервером (при каждом обновлении клиент и сервер обмениваются запросом), этот подход работает с частотой обновления около 1 секунды.

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

 import dash
import dash_html_components as html
import dash_core_components as dcc
import numpy as np

from dash.dependencies import Input, Output, State

# Example data (a circle).
resolution = 1000
t = np.linspace(0, np.pi * 2, resolution)
x, y = np.cos(t), np.sin(t)
# Example app.
figure = dict(data=[{'x': [], 'y': []}], layout=dict(xaxis=dict(range=[-1, 1]), yaxis=dict(range=[-1, 1])))
app = dash.Dash(__name__, update_title=None)  # remove "Updating..." from title
app.layout = html.Div([
    dcc.Graph(id='graph', figure=dict(figure)), dcc.Interval(id="interval", interval=25),
    dcc.Store(id='offset', data=0), dcc.Store(id='store', data=dict(x=x, y=y, resolution=resolution)),
])
app.clientside_callback(
    """
    function (n_intervals, data, offset) {
        offset = offset % data.x.length;
        const end = Math.min((offset   10), data.x.length);
        return [[{x: [data.x.slice(offset, end)], y: [data.y.slice(offset, end)]}, [0], 500], end]
    }
    """,
    [Output('graph', 'extendData'), Output('offset', 'data')],
    [Input('interval', 'n_intervals')], [State('store', 'data'), State('offset', 'data')]
)

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

Обновления на стороне клиента должны быть достаточно быстрыми, чтобы обеспечить плавное обновление. Приведенный ниже gif показывает приведенный выше пример, работающий с частотой обновления 25 мс,

Обновление на стороне клиента

Имейте в виду, что обновление на стороне клиента возможно только в том случае, если данные уже присутствуют на стороне клиента, т. Е. Для извлечения данных с сервера необходим другой механизм. Возможный поток данных может быть

  1. Используйте медленный Interval компонент (например, 2 секунды) для запуска (обычного) обратного вызова, который извлекает фрагмент данных из источника и помещает его в Store компонент
  2. Используйте быстрый Interval компонент (например, 25 мс) для запуска обратного вызова на стороне клиента, который передает данные от Store компонента к Graph компоненту

Комментарии:

1. Это действительно превратилось в отличное решение! И действительно заслуженная кража с отметкой «мое» принятие…

2. ExtendData это то же самое, что ExtendTraces ?

3. Я прав, или этот пример только быстро обновляется, но не анимируется?

4. Можете ли вы продемонстрировать, быстрее ли это, чем метод dataframe от @vestland?

Ответ №2:

Редактировать: Редакция 1

В конце моего предложения вы найдете полностью воспроизводимый, хотя и минимальный фрагмент кода. Но имейте в виду, что это пример, разработанный для запуска в JupyterDash.


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

1. df.plot() для серверной части pandas, создающей графики, установлено значение plotly ,

2. компонент dash, подобный этому:

    dcc.Interval(id='interval-component',
                interval=1*1000, # in milliseconds
                n_intervals=0
        )
  

3. и функция обратного вызова, подобная этой:

 @app.callback(
    Output('graph', 'figure'),
    [Input('interval-component', "n_intervals")]
)
  

Приведенный ниже фрагмент содержит код, который выполняет именно то, что вы описали в своем вопросе:

1. Он собирает фрагмент случайных данных в dataframe df2 каждую секунду,

2. добавляет это к существующему фрейму данных df1 и

3. выводит результат на график.

Начальный рисунок выглядит следующим образом:

введите описание изображения здесь

Через несколько секунд рисунок выглядит следующим образом:

введите описание изображения здесь

И это может звучать слишком хорошо, чтобы быть правдой, но переходы между фигурами выглядят довольно великолепно прямо из коробки. Новые точки добавляются изящно в конце строк, и оси x и y обновляются довольно плавно.

Поначалу обновление может показаться немного прерывистым, но после нескольких тысяч запусков вы увидите, что перемещаются только концы строк:

введите описание изображения здесь

На рисунке выше вы можете видеть, что начальная точка включается после нескольких тысяч запусков. Это, вероятно, очевидно, но если вы хотите сохранить постоянную длину окна, например, после 1000 запусков, просто включите replace df3 = df3.cumsum() на df3 = df3.cumsum().tail(1000) , чтобы получить:

введите описание изображения здесь

Но вам не обязательно верить мне на слово. Просто запустите следующий фрагмент в JupyterLab и убедитесь сами:

 import pandas as pd
import numpy as np
import plotly.express as px
import plotly.graph_objects as go
from jupyter_dash import JupyterDash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output

# code and plot setup
# settings
pd.options.plotting.backend = "plotly"
countdown = 20
#global df

# sample dataframe of a wide format
np.random.seed(4); cols = list('abc')
X = np.random.randn(50,len(cols))  
df=pd.DataFrame(X, columns=cols)
df.iloc[0]=0;

# plotly figure
fig = df.plot(template = 'plotly_dark')

app = JupyterDash(__name__)
app.layout = html.Div([
    html.H1("Streaming of random data"),
            dcc.Interval(
            id='interval-component',
            interval=1*1000, # in milliseconds
            n_intervals=0
        ),
    dcc.Graph(id='graph'),
])

# Define callback to update graph
@app.callback(
    Output('graph', 'figure'),
    [Input('interval-component', "n_intervals")]
)
def streamFig(value):
    
    global df
    
    Y = np.random.randn(1,len(cols))  
    df2 = pd.DataFrame(Y, columns = cols)
    df = df.append(df2, ignore_index=True)#.reset_index()
    df.tail()
    df3=df.copy()
    df3 = df3.cumsum()
    fig = df3.plot(template = 'plotly_dark')
    #fig.show()
    return(fig)

app.run_server(mode='external', port = 8069, dev_tools_ui=True, #debug=True,
              dev_tools_hot_reload =True, threaded=True)
  

Этот пример не очень элегантный, и есть много возможностей для улучшения (даже глобальной переменной ….), но я надеюсь, что он будет полезен для вас.

Редактировать: Редакция 2:

Примерно после 6000 запусков диаграмма будет выглядеть следующим образом:

введите описание изображения здесь

И теперь на это уже не так интересно смотреть, хотя все работает очень гладко. Каждое обновление показывает лишь крошечное движение в конечных точках. Итак, я добавил несколько аннотаций в конце, чтобы было более ясно, что на самом деле все еще работает:

Завершите код аннотациями

 import pandas as pd
import numpy as np
import plotly.express as px
import plotly.graph_objects as go
from jupyter_dash import JupyterDash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output

# code and plot setup
# settings
pd.options.plotting.backend = "plotly"
countdown = 20
#global df

# sample dataframe of a wide format
np.random.seed(4); cols = list('abc')
X = np.random.randn(50,len(cols))  
df=pd.DataFrame(X, columns=cols)
df.iloc[0]=0;

# plotly figure
fig = df.plot(template = 'plotly_dark')

app = JupyterDash(__name__)
app.layout = html.Div([
    html.H1("Streaming of random data"),
            dcc.Interval(
            id='interval-component',
            interval=1*1000, # in milliseconds
            n_intervals=0
        ),
    dcc.Graph(id='graph'),
])

# Define callback to update graph
@app.callback(
    Output('graph', 'figure'),
    [Input('interval-component', "n_intervals")]
)
def streamFig(value):
    
    global df
    
    Y = np.random.randn(1,len(cols))  
    df2 = pd.DataFrame(Y, columns = cols)
    df = df.append(df2, ignore_index=True)#.reset_index()
    #df.tail()
    df3=df.copy()
    df3 = df3.cumsum()#.tail(1000)
    fig = df3.plot(template = 'plotly_dark')
    #fig.show()
    
    colors = px.colors.qualitative.Plotly
    for i, col in enumerate(df3.columns):
            fig.add_annotation(x=df3.index[-1], y=df3[col].iloc[-1],
                                   text = str(df3[col].iloc[-1])[:4],
                                   align="right",
                                   arrowcolor = 'rgba(0,0,0,0)',
                                   ax=25,
                                   ay=0,
                                   yanchor = 'middle',
                                   font = dict(color = colors[i]))
    
    return(fig)

app.run_server(mode='external', port = 8069, dev_tools_ui=True, #debug=True,
              dev_tools_hot_reload =True, threaded=True)
  

Оригинальный ответ и предложения

Вы не предоставили никакого примера кода, поэтому я могу предложить только общее предложение, а именно более подробно рассмотреть, как plotly передает данные forex в примере в галерее Dash:

введите описание изображения здесь

Я бы особенно взглянул на то, как они настроили свои обратные вызовы и функцию generate_figure_callback(pair) из строки 932 в исходном коде:

 # Function to update Graph Figure
def generate_figure_callback(pair):
    def chart_fig_callback(n_i, p, t, s, pairs, a, b, old_fig):

        if pairs is None:
            return {"layout": {}, "data": {}}

        pairs = pairs.split(",")
        if pair not in pairs:
            return {"layout": {}, "data": []}

        if old_fig is None or old_fig == {"layout": {}, "data": {}}:
            return get_fig(pair, a, b, t, s, p)

        fig = get_fig(pair, a, b, t, s, p)
        return fig

    return chart_fig_callback
  

Это все, что у меня есть на данный момент, но я надеюсь, что вы найдете это полезным!

Редактировать: Просто чтобы показать, что обновления не ограничены 5 минутами.

Захват экрана в 21:16:29

введите описание изображения здесь

Захват экрана в 21:16:55

введите описание изображения здесь

В тексте предложения вы видите именно это: ставки и отклонения. И они все время меняются. Если я прав на 100%, строка представляет закрытые сделки, и это происходит только время от времени. Поэтому я думаю, что это всего лишь вопрос того, какие данные вы здесь отображаете. И я надеюсь, что единственное, что вам нужно будет сделать, чтобы получить то, что вам нужно, — это заменить центральные части этого примера вашим источником данных. Вы также могли бы ознакомиться с примером потоковой передачи Wind. Это может быть даже проще реализовать для вашего сценария.

введите описание изображения здесь

Комментарии:

1. Это хорошая демонстрация, спасибо. Хотя он работает только с 5-минутными интервалами, поэтому у него не возникнет проблем с производительностью в реальном времени, и ему не нужно поддерживать плавную прокрутку.

2. @DavidParks На самом деле это работает не только с 5-минутными интервалами. Я могу понять, почему вы так думаете, хотя из-за выпадающего меню с [5, 15, 30] опциями. Но это только для выбора временных интервалов для свечей. Попробуйте изменить график на line , щелкнув значок справа от EURUSD . Теперь вы увидите, что он постоянно обновляется. Это не совсем непрерывный поток, но он не так уж далек от обновления каждые две секунды или около того. Я посмотрю, смогу ли я добавить несколько скриншотов, чтобы лучше проиллюстрировать это.

3. Ах, вы правы, обновление в режиме реального времени изменяет текст ставки / запроса, но не непрерывно прокручивающийся график, который для нас является ключевым.

4. Я бы не советовал использовать глобальные переменные. Это не рекомендуется и может привести к непоследовательному поведению. Смотрите документы для получения дополнительной информации, dash.plotly.com/sharing-data-between-callbacks

5. @emher Спасибо за отзыв! И да, я знаю. Вот почему я специально упомянул это как возможную область для улучшения.