dash icon indicating copy to clipboard operation
dash copied to clipboard

provide virtual WebGL support

Open alexcjohnson opened this issue 1 year ago • 3 comments

Plotly.js v2.28.0 added support for virtual-webgl, which allows you to put arbitrarily many WebGL-based graphs on a single page, as opposed to the limit of typically 4-8 before you hit the maximum contexts a browser will give you (note most desktop browsers give you up to 16 contexts, but one plotly.js graph uses up to 3 contexts). We would like to expose this option in Dash too. It can already be done by adding https://unpkg.com/[email protected]/src/virtual-webgl.js to external_scripts (or downloading that and putting it in your assets folder), so we could just document that, but it would be nicer to have a built-in way to do this.

Two important things to note about this:

  • It's expected to have some performance impact, so if you only have one or two WebGL graphs at a time you're likely better off not using it, though we haven't quantified this.
  • It takes over ALL WebGL on the page, not just plotly.js. This could be good, as you can then put an unlimited number of WebGL-based components of any type on the page, however we haven't tested it with other components so it's possible there will be issues. Aside from plotly.js (dcc.Graph), WebGL is used by some dash-bio components, dash-vtk, dash-deck, and possibly others (I don't think dash-cytoscape or dash-leaflet use WebGL but I'm not 100% sure of this).

Initially I was thinking we would make this opt-in as a prop of dcc.Graph, but because of the global (and irreversible) nature of this I'm now thinking a global setting like a Dash() constructor arg would be better.

alexcjohnson avatar Feb 01 '24 14:02 alexcjohnson

@alexcjohnson

I have tested all WebGL dash-bio components, as well as dash-vtk with the shared WebGL context, and all seem to be working fine as far as I can tell.

dash-deck I'm less familiar with; the basic "Hello World" example seems to be loading OK but I haven't yet been able to put together a more comprehensive example to test with.

Sample Dash app used for testing:
import urllib.request as urlreq

import dash
from dash import dcc, html, callback, Input, Output
import dash_bio
from dash_bio.utils import ngl_parser, xyz_reader
import dash_deck
import dash_vtk
import numpy as np
import pandas as pd
import plotly.graph_objects as go
from pyvista import examples


app = dash.Dash(
    __name__,
    external_scripts=[
        "https://unpkg.com/[email protected]/src/virtual-webgl.js",  # Enable shared virtual WebGL context
    ],
)
server = app.server


def layout():
    return html.Div(
        [
            dcc.Dropdown(
                id="content-dropdown",
                options=[
                    {"label": x, "value": x}
                    for x in ["dcc-graph", "dash-bio", "dash-vtk", "dash-deck"]
                ],
                value="dcc-graph",
            ),
            dcc.Loading(html.Div(id="content")),
        ]
    )


@callback(
    Output("content", "children"),
    Input("content-dropdown", "value"),
)
def select_content(value):
    if value == "dcc-graph":
        return dcc_graphs_webgl()
    elif value == "dash-bio":
        return dash_bio_components()
    elif value == "dash-vtk":
        return dash_vtk_components()
    elif value == "dash-deck":
        return dash_deck_components()
    else:
        return html.Div(f"Invalid selection `{value}`")


def dcc_graphs_webgl():
    return html.Div(
        [
            dcc.Graph(figure=go.Figure(data=[go.Scattergl(x=[1, 2, 3], y=[1, 3, 2])])),
        ]
    )


def dash_bio_components():
    return html.Div(
        [
            # #  Speck
            html.Div("Speck"),
            html.Div(
                dash_bio.Speck(
                    id="default-speck",
                    data=xyz_reader.read_xyz(
                        datapath_or_datastring=urlreq.urlopen(
                            "https://git.io/speck_methane.xyz"
                        )
                        .read()
                        .decode("utf-8"),
                        is_datafile=False,
                    ),
                ),
                style={"border": "2px solid black"},
            ),
            #  NglMoleculeViewer
            html.Div("NglMoleculeViewer"),
            html.Div(
                dash_bio.NglMoleculeViewer(
                    id="default-ngl-molecule",
                    data=[
                        ngl_parser.get_data(
                            data_path="https://raw.githubusercontent.com/plotly/datasets/master/Dash_Bio/Molecular/",
                            pdb_id="1BNA",
                            color="red",
                            reset_view=True,
                            local=False,
                        )
                    ],
                ),
                style={"border": "2px solid black"},
            ),
            #  AlignmentChart
            html.Div("AlignmentChart"),
            html.Div(
                dash_bio.AlignmentChart(
                    id="my-default-alignment-viewer",
                    data=urlreq.urlopen("https://git.io/alignment_viewer_p53.fasta")
                    .read()
                    .decode("utf-8"),
                    height=900,
                    tilewidth=30,
                ),
                style={"border": "2px solid black"},
            ),
            #  ManhattanPlot
            html.Div("ManhattanPlot"),
            html.Div(
                dcc.Graph(
                    id="default-dashbio-manhattanplot",
                    figure=dash_bio.ManhattanPlot(
                        dataframe=pd.read_csv("https://git.io/manhattan_data.csv")
                    ),
                ),
                style={"border": "2px solid black"},
            ),
        ]
    )


def dash_vtk_components():
    return html.Div(
        [
            #  vtk view with point cloud
            vtk_view_point_cloud(),
        ]
    )


def vtk_view_point_cloud():

    # Get point cloud data from PyVista
    dataset = examples.download_lidar()
    subset = 0.2
    selection = np.random.randint(
        low=0, high=dataset.n_points - 1, size=int(dataset.n_points * subset)
    )
    points = dataset.points[selection]
    xyz = points.ravel()
    elevation = points[:, -1].ravel()
    min_elevation = np.amin(elevation)
    max_elevation = np.amax(elevation)

    return html.Div(
        dash_vtk.View(
            [
                dash_vtk.PointCloudRepresentation(
                    xyz=xyz,
                    scalars=elevation,
                    colorDataRange=[min_elevation, max_elevation],
                    property={"pointSize": 2},
                )
            ]
        ),
        style={"height": "400px", "width": "600px"},
    )


# How to keep this from taking over entire browser canvas?
def dash_deck_components():
    data = {
        "description": "A minimal deck.gl example",
        "initialViewState": {"longitude": -122.45, "latitude": 37.8, "zoom": 12},
        "layers": [
            {
                "@@type": "TextLayer",
                "data": [{"position": [-122.45, 37.8], "text": "Hello World"}],
            },
        ],
    }

    return html.Div(
        dash_deck.DeckGL(data=data, id="deck-gl"),
        style={"width": "600px", "height": "400px"},
    )


app.layout = layout

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

emilykl avatar Apr 08 '24 14:04 emilykl

Perhaps @graingert-coef or @vogt31337 could be enticed to test out dash-deck with virtual webgl, and provide a more complete test case?

alexcjohnson avatar Apr 09 '24 14:04 alexcjohnson