Source code for meerkat.interactive.app.src.lib.component.plotly.dynamic_scatter

import json
from typing import List

from meerkat import env
from meerkat.dataframe import DataFrame
from meerkat.ibis import IbisDataFrame
from meerkat.interactive.endpoint import Endpoint, EndpointProperty, endpoint
from meerkat.interactive.event import EventInterface
from meerkat.interactive.graph import Store, reactive
from meerkat.tools.lazy_loader import LazyLoader
from meerkat.tools.utils import classproperty, requires

from ...abstract import Component

px = LazyLoader("plotly.express")
ibis = LazyLoader("ibis")


class OnRelayoutInterface(EventInterface):
    """Defines the interface for an event.

    Subclass this to define the interface for a new event type.
    The class will specify the keyword arguments returned by an event from the
    frontend to any endpoint that has subscribed to it.

    All endpoints that are expected to receive an event of this type should
    ensure they have a signature that matches the keyword arguments defined
    in this class.
    """

    x_range: List[float]
    y_range: List[float]


[docs]class DynamicScatter(Component): df: DataFrame on_click: EndpointProperty = None on_relayout: EndpointProperty = None selected: List[str] = [] on_select: Endpoint = None data: str = "" layout: str = "" filtered_df: DataFrame = None
[docs] @requires("plotly.express") def __init__( self, df: DataFrame, *, x=None, y=None, color=None, max_points: int = 1_000, on_click: EndpointProperty = None, on_relayout: EndpointProperty = None, selected: List[str] = [], on_select: Endpoint = None, **kwargs, ): """See https://plotly.com/python-api- reference/generated/plotly.express.scatter.html for more details.""" if not env.is_package_installed("plotly"): raise ValueError( "Plotly components require plotly. Install with `pip install plotly`." ) if df.primary_key_name is None: raise ValueError("Dataframe must have a primary key") full_size = len(df) # noqa: F841 @reactive() def get_layout(df: DataFrame, x: str, y: str, color: str): if isinstance(df, IbisDataFrame): fig_df = DataFrame.from_pandas( df.expr.order_by(ibis.random()) .limit(max_points)[x, y, color] .execute() ) else: fig_df = df if len(df) <= max_points else df.sample(max_points) fig = px.scatter(fig_df.to_pandas(), x=x, y=y, color=color, **kwargs) return json.dumps(json.loads(fig.to_json())["layout"]) layout = get_layout(df, x=x, y=y, color=color) axis_range = Store({"x0": None, "x1": None, "y0": None, "y1": None}) @endpoint() def on_relayout(axis_range: Store[dict], x_range, y_range): axis_range.set( {"x0": x_range[0], "x1": x_range[1], "y0": y_range[0], "y1": y_range[1]} ) @reactive() def filter_df(df: DataFrame, axis_range: dict, x: str, y: str): is_ibis = isinstance(df, IbisDataFrame) if is_ibis: df = df.expr else: df = df.view() if axis_range["x0"] is not None: df = df[axis_range["x0"] < df[x]] if axis_range["x1"] is not None: df = df[df[x] < axis_range["x1"]] if axis_range["y0"] is not None: df = df[axis_range["y0"] < df[y]] if axis_range["y1"] is not None: df = df[df[y] < axis_range["y1"]] if is_ibis: df = IbisDataFrame(df) return df @reactive() def sample_df(df: DataFrame): is_ibis = isinstance(df, IbisDataFrame) if is_ibis: df = DataFrame.from_pandas( df.expr.order_by(ibis.random()) .limit(max_points)[x, y, color, df.primary_key_name] .execute() ) else: df = df.view() if len(df) > max_points: df = df.sample(max_points) return df @reactive() def compute_plotly(df: DataFrame, x: str, y: str, color: str): fig = px.scatter(df.to_pandas(), x=x, y=y, color=color, **kwargs) return json.dumps(json.loads(fig.to_json())["data"]) df.mark() filtered_df = filter_df(df=df, axis_range=axis_range, x=x, y=y) sampled_df = sample_df(df=filtered_df) plotly_data = compute_plotly(df=sampled_df, x=x, y=y, color=color) super().__init__( df=df, on_click=on_click, selected=selected, on_select=on_select, on_relayout=on_relayout.partial(axis_range=axis_range), data=plotly_data, layout=layout, filtered_df=filtered_df, )
@classproperty def namespace(cls): return "plotly" def _get_ipython_height(self): return "800px"