Plotting

In addition to tables, Deephaven supports dynamic plots as an excellent way to visualize real-time data. You can add plots to your deephaven.ui components. Like tables, plots will update in real time and react to changes in the UI.

The deephaven.ui module provides a simple interface for creating interactive plots using the deephaven-express library. This guide will show you how to create plots that update based on user input.

Memoize plots

Just as you should memoize table operations, it’s important to memoize plots based on the table used to create them and any arguments that may change. This process of memoization prevents the plot from being recreated during every re-render. Instead, the plot will only be recreated when an argument related to plot changes.

from deephaven import time_table, ui
import deephaven.plot.express as dx


@ui.component
def ui_memo_plot_app():
    n, set_n = ui.use_state(1)

    result_table = ui.use_memo(
        lambda: time_table("PT1s").update(f"y=i*{n}").reverse(), [n]
    )

    # memoize the plot
    plot = ui.use_memo(
        lambda: dx.line(result_table, x="Timestamp", y="y"), [result_table]
    )

    return ui.view(
        ui.flex(
            ui.slider(value=n, min_value=1, max_value=10, on_change=set_n, label="n"),
            plot,
            direction="column",
            height="100%",
        ),
        align_self="stretch",
        flex_grow=1,
    )


memo_plot_app = ui_memo_plot_app()

Plot a filtered table

This example demonstrates how to create a simple line plot that updates based on user input. The plot will display the price of a stock filtered based on the stock symbol entered by the user. Here, we have used a ui.text_field to get the value, but it could be driven by any deephaven.ui input, including double clicking on a value from a ui.table. We’ve previously referred to this sort of behavior as a “one-click” component in Enterprise, as the plot updates as soon as the user enters a filter.

import deephaven.plot.express as dx
import deephaven.ui as ui

_stocks = dx.data.stocks()


@ui.component
def plot_filtered_table(table, initial_value):
    text, set_text = ui.use_state(initial_value)
    # the filter is memoized so that it is only recalculated when the text changes
    filtered_table = ui.use_memo(
        lambda: table.where(f"Sym = `{text.upper()}`"), [table, text]
    )
    plot = ui.use_memo(
        lambda: dx.line(
            filtered_table, x="Timestamp", y="Price", title=f"Filtered by: {text}"
        ),
        [filtered_table, text],
    )
    return [ui.text_field(value=text, on_change=set_text), plot]


p = plot_filtered_table(_stocks, "DOG")

Plot a partitioned table

Using a partitioned table, as opposed to a where statement, can be more efficient if you filter the same table multiple times with different values. This is because the partitioning is only done once, and then the key is selected from the partitioned table. Compared to using where, it can be faster to return results, but at the expense of the query engine using more memory. Depending on the size of your table and the number of unique values in the partition key, this trade-off can be worthwhile.

import deephaven.plot.express as dx
import deephaven.ui as ui

_stocks = dx.data.stocks()


@ui.component
def plot_partitioned_table(table, initial_value):
    text, set_text = ui.use_state(initial_value)
    # memoize the partition by so that it only performed once
    partitioned_table = ui.use_memo(lambda: table.partition_by(["Sym"]), [table])
    constituent_table = ui.use_memo(
        lambda: partitioned_table.get_constituent(text.upper()) if text != "" else None,
        [partitioned_table, text],
    )
    # only attempt to plot valid partition keys
    plot = ui.use_memo(
        lambda: dx.line(
            constituent_table, x="Timestamp", y="Price", title=f"partition key: {text}"
        )
        if constituent_table != None
        else ui.text("Please enter a valid partition."),
        [constituent_table, text],
    )
    return [
        ui.text_field(value=text, on_change=set_text),
        plot,
    ]


p = plot_partitioned_table(_stocks, "DOG")

Combine a filter and a partition by

Deephaven Plotly Express allows you to plot by a partition and assign unique colors to each key. Sometimes, as a user, you may also want to filter the data in addition to partitioning it. We’ve previously referred to this as “one-click plot by” behavior in Enterprise. This can be done by either filtering the table first and then partitioning it, or partitioning it first and then filtering it. The choice of which to use depends on the size of the table and the number of unique values in the partition key. The first example is more like a traditional “one-click” component, and the second is more like a parameterized query. Both will give you the same result, but the first one may return results faster, whereas the second one may be more memory efficient.

import deephaven.plot.express as dx
import deephaven.ui as ui

_stocks = dx.data.stocks()


@ui.component
def partition_then_filter(table, by, initial_value):
    """
    Partition the table by both passed columns, then filter it by the value entered by the user
    """
    text, set_text = ui.use_state(initial_value)
    partitioned_table = ui.use_memo(lambda: table.partition_by(by), [table, by])
    filtered = ui.use_memo(
        lambda: partitioned_table.filter(f"{by[0]} = `{text.upper()}`"),
        [text, partitioned_table],
    )
    plot = ui.use_memo(
        lambda: dx.line(filtered, x="Timestamp", y="Price", by=[f"{by[1]}"]),
        [filtered, by],
    )
    return [
        ui.text_field(value=text, on_change=set_text),
        plot,
    ]


@ui.component
def where_then_partition(table, by, initial_value):
    """
    Filter the table by the value entered by the user, then re-partition it by the second passed column
    """
    text, set_text = ui.use_state(initial_value)
    filtered = ui.use_memo(
        lambda: table.where(f"{by[0]} = `{text.upper()}`"), [text, table]
    )
    plot = ui.use_memo(
        lambda: dx.line(filtered, x="Timestamp", y="Price", by=[f"{by[1]}"]),
        [filtered, by],
    )
    return [ui.text_field(value=text, on_change=set_text), plot]


# outputs the same thing, done two different ways depending on how you want the work done
ptf = partition_then_filter(_stocks, ["Sym", "Exchange"], "DOG")
wtp = where_then_partition(_stocks, ["Sym", "Exchange"], "DOG")

Change a plot

In response to user events, you can change data for a plot and you can change the plot itself. In this example, the plot type changes by selecting it from a picker.

import deephaven.plot.express as dx
import deephaven.ui as ui

_stocks = dx.data.stocks().where("Sym = `DOG`")

plot_types = ["Line", "Scatter", "Area"]


@ui.component
def change_plot_type(table):
    plot_type, set_plot_type = ui.use_state("Line")

    def create_plot(t, pt):
        match pt:
            case "Line":
                return dx.line(t, x="Timestamp", y="Price")
            case "Scatter":
                return dx.scatter(t, x="Timestamp", y="Price")
            case "Area":
                return dx.area(t, x="Timestamp", y="Price")
            case _:
                return ui.text(f"Unknown plot type {pt}")

    plot = ui.use_memo(lambda: create_plot(table, plot_type), [table, plot_type])
    return [
        ui.picker(plot_types, selected_key=plot_type, on_change=set_plot_type),
        plot,
    ]


change_plot_type_example = change_plot_type(_stocks)

Plots and Liveness

While you may need to use a liveness scope for Deephaven tables, you do not for Deephaven Express plots.

Deephaven Express tracks liveness internally for the tables used by the plot. It cleans up when the figure is deleted or cleaned up by garbage collection. You should not need to explicitly use liveness scope for Deephaven Express.