Создайте обратный вызов на стороне клиента для отслеживания наведения в Dash

#javascript #python #plotly-dash #client-side

Вопрос:

Я создал приложение Dash, которое отслеживает и хранит данные о наведении изображений (см. Код ниже). Я хотел бы превратить часть отслеживания наведения в обратный вызов на стороне клиента, чтобы сделать приложение более масштабируемым и повысить производительность отслеживания наведения. Ниже приведен мой код, использующий обычный обратный вызов. Как я могу превратить эту функцию в обратный вызов на стороне клиента?

 import dash
import dash_core_components as dcc
import dash_html_components as html
import numpy as np
import plotly.express as px
import requests
from dash.dependencies import ClientsideFunction, Input, Output, State
from dash.exceptions import PreventUpdate
from PIL import Image


app = dash.Dash(__name__)
server =  app.server

# In reality, there are 50 screenshot images with non-sequential indexes
urls = ["https://pbs.twimg.com/media/EW8GhG_XkAEOyAh.jpg",
        "https://pbs.twimg.com/media/CqzwpPnWEAAiGjW.jpg"]

def url_to_fig(url):
    rgb_arr = np.array(Image.open(requests.get(url, stream=True).raw))
    fig = px.imshow(rgb_arr)
    fig.update_xaxes(visible=False)
    fig.update_yaxes(visible=False)
    fig.update_layout(
        dragmode=False, width=800, height=800)
    fig.update_traces(hoverinfo='none', hovertemplate=None)
    return fig

app.layout = html.Div([
    dcc.Store(id='ss-idx', data=0),
    dcc.Graph(id='ss-img', figure=url_to_fig(urls[0]), config = {"displayModeBar": False}),
    html.Button("Next", id='next-button', n_clicks=0),
    dcc.Store(id='hoverdata', data=[]), # Place to append new hoverdata
])


# Change to client side callback (in JavaScript)
@app.callback(
    [Output('ss-idx', 'data'),
     Output('ss-img', 'figure'),
     Output('hoverdata', 'data')],
    [Input('ss-img', 'hoverData'),
     Input('next-button', 'n_clicks')],
    [State('hoverdata', 'data'),
     State('ss-idx', 'data')]
)
def add_to_hoverdata(hover_point, next_clicks, hoverdata, ss_idx):
    ctx = dash.callback_context
    if not ctx.triggered:
        raise PreventUpdate

    button_id = ctx.triggered[0]['prop_id'].split('.')[0]
    if 'ss-img' in button_id:
        if hover_point is not None:
            x = hover_point["points"][0]["x"]
            y = hover_point["points"][0]["y"]
            hoverdata.append((x, y))
            return dash.no_update, dash.no_update, hoverdata

    elif 'next-button' in button_id:
        # Add hoverdata and screenshot index to mysql database (code not shown)
        print(next_clicks)
        if next_clicks < len(urls):
            new_idx = ss_idx   1
            return new_idx, url_to_fig(urls[new_idx]), [] # Reset hoverdata
        else:
            raise PreventUpdate


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

 

Ответ №1:

Вот попытка преобразовать его в обратный вызов на стороне клиента… Но сначала два важных замечания:

  1. Поскольку url массив определен на стороне сервера, он должен быть добавлен на сторону клиента с помощью dcc.Store компонента.
  2. Вы не сможете отправлять данные в базу данных при обратном вызове на стороне клиента. Это упомянуто в пункте 3 в самом низу документов Dash. Я думаю, вы могли бы это сделать, если бы могли подключиться к БД и отправлять данные в JS, но это, вероятно, очень плохая идея…

Обратный вызов:

 app.clientside_callback(
    # from dash.dependencies import ClientsideFunction
    ClientsideFunction(
        namespace="clientside",
        function="addToHoverData"
    ),
    [Output('ss-idx', 'data'),
     Output('ss-img', 'figure'),
     Output('hoverdata', 'data')],
    [Input('ss-img', 'hoverData'),
     Input('next-button', 'n_clicks')],
    [State('hoverdata', 'data'),
     State('ss-idx', 'data'),
     # you must create a Store with the url list
     State('url-list', 'data') 
    ]
)
 

assets/clientside.js

 window.dash_clientside = Object.assign({}, window.dash_clientside, {
    clientside: {
        add_to_hoverdata: function(hoverData, nClicks, storeHoverData, storeIdx, storeURLs) {
            // storeURLs is an array that holds the image URLs
            const ctx = dash_clientside.callback_context;

            if (!("triggered" in ctx)){
                return dash_clientside.no_update
            }

            const triggered_id = ctx.triggered[0].prop_id.split(".")[0];

            if (triggered_id === "ss-img"){
                if (hoverData !== undefined){
                    // assumes storeHoverData is an array
                    storeHoverData.push(hoverData.points[0]);
                    return [
                        dash_clientside.no_update,
                        dash_clientside.no_update,
                        storeHoverData
                    ]
                }
            } else if (triggered_id === "next-button"){
                console.log(nClicks);

                if (nClicks < storeURLs.length){
                    return [
                        storeIdx  1,
                        storeURLs[storeIdx  1],
                        []
                    ]
                }
                else {
                    return dash_clientside.no_update
                }
            }
        }
    }
});

 

Я пытался сохранить код js как можно ближе к вашему обратному вызову python.