|
| 1 | +# Plotting |
| 2 | + |
| 3 | +In addition to tables, Deephaven supports [dynamic plots](/core/docs/how-to-guides/plotting/) 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. |
| 4 | + |
| 5 | +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. |
| 6 | + |
| 7 | +## Memoize plots |
| 8 | + |
| 9 | +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. |
| 10 | + |
| 11 | +```python |
| 12 | +from deephaven import time_table, ui |
| 13 | +import deephaven.plot.express as dx |
| 14 | + |
| 15 | + |
| 16 | +@ui.component |
| 17 | +def ui_memo_plot_app(): |
| 18 | + n, set_n = ui.use_state(1) |
| 19 | + |
| 20 | + result_table = ui.use_memo( |
| 21 | + lambda: time_table("PT1s").update(f"y=i*{n}").reverse(), [n] |
| 22 | + ) |
| 23 | + |
| 24 | + # memoize the plot |
| 25 | + plot = ui.use_memo( |
| 26 | + lambda: dx.line(result_table, x="Timestamp", y="y"), [result_table] |
| 27 | + ) |
| 28 | + |
| 29 | + return ui.view( |
| 30 | + ui.flex( |
| 31 | + ui.slider(value=n, min_value=1, max_value=10, on_change=set_n, label="n"), |
| 32 | + plot, |
| 33 | + direction="column", |
| 34 | + height="100%", |
| 35 | + ), |
| 36 | + align_self="stretch", |
| 37 | + flex_grow=1, |
| 38 | + ) |
| 39 | + |
| 40 | + |
| 41 | +memo_plot_app = ui_memo_plot_app() |
| 42 | +``` |
| 43 | + |
| 44 | +## Plot a filtered table |
| 45 | + |
| 46 | +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. |
| 47 | + |
| 48 | +```python |
| 49 | +import deephaven.plot.express as dx |
| 50 | +import deephaven.ui as ui |
| 51 | + |
| 52 | +_stocks = dx.data.stocks() |
| 53 | + |
| 54 | + |
| 55 | +@ui.component |
| 56 | +def plot_filtered_table(table, initial_value): |
| 57 | + text, set_text = ui.use_state(initial_value) |
| 58 | + # the filter is memoized so that it is only recalculated when the text changes |
| 59 | + filtered_table = ui.use_memo( |
| 60 | + lambda: table.where(f"Sym = `{text.upper()}`"), [table, text] |
| 61 | + ) |
| 62 | + plot = ui.use_memo( |
| 63 | + lambda: dx.line( |
| 64 | + filtered_table, x="Timestamp", y="Price", title=f"Filtered by: {text}" |
| 65 | + ), |
| 66 | + [filtered_table, text], |
| 67 | + ) |
| 68 | + return [ui.text_field(value=text, on_change=set_text), plot] |
| 69 | + |
| 70 | + |
| 71 | +p = plot_filtered_table(_stocks, "DOG") |
| 72 | +``` |
| 73 | + |
| 74 | +## Plot a partitioned table |
| 75 | + |
| 76 | +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. |
| 77 | + |
| 78 | +```python |
| 79 | +import deephaven.plot.express as dx |
| 80 | +import deephaven.ui as ui |
| 81 | + |
| 82 | +_stocks = dx.data.stocks() |
| 83 | + |
| 84 | + |
| 85 | +@ui.component |
| 86 | +def plot_partitioned_table(table, initial_value): |
| 87 | + text, set_text = ui.use_state(initial_value) |
| 88 | + # memoize the partition by so that it only performed once |
| 89 | + partitioned_table = ui.use_memo(lambda: table.partition_by(["Sym"]), [table]) |
| 90 | + constituent_table = ui.use_memo( |
| 91 | + lambda: partitioned_table.get_constituent(text.upper()) if text != "" else None, |
| 92 | + [partitioned_table, text], |
| 93 | + ) |
| 94 | + # only attempt to plot valid partition keys |
| 95 | + plot = ui.use_memo( |
| 96 | + lambda: dx.line( |
| 97 | + constituent_table, x="Timestamp", y="Price", title=f"partition key: {text}" |
| 98 | + ) |
| 99 | + if constituent_table != None |
| 100 | + else ui.text("Please enter a valid partition."), |
| 101 | + [constituent_table, text], |
| 102 | + ) |
| 103 | + return [ |
| 104 | + ui.text_field(value=text, on_change=set_text), |
| 105 | + plot, |
| 106 | + ] |
| 107 | + |
| 108 | + |
| 109 | +p = plot_partitioned_table(_stocks, "DOG") |
| 110 | +``` |
| 111 | + |
| 112 | +## Combine a filter and a partition by |
| 113 | + |
| 114 | +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. |
| 115 | + |
| 116 | +```python |
| 117 | +import deephaven.plot.express as dx |
| 118 | +import deephaven.ui as ui |
| 119 | + |
| 120 | +_stocks = dx.data.stocks() |
| 121 | + |
| 122 | + |
| 123 | +@ui.component |
| 124 | +def partition_then_filter(table, by, initial_value): |
| 125 | + """ |
| 126 | + Partition the table by both passed columns, then filter it by the value entered by the user |
| 127 | + """ |
| 128 | + text, set_text = ui.use_state(initial_value) |
| 129 | + partitioned_table = ui.use_memo(lambda: table.partition_by(by), [table, by]) |
| 130 | + filtered = ui.use_memo( |
| 131 | + lambda: partitioned_table.filter(f"{by[0]} = `{text.upper()}`"), |
| 132 | + [text, partitioned_table], |
| 133 | + ) |
| 134 | + plot = ui.use_memo( |
| 135 | + lambda: dx.line(filtered, x="Timestamp", y="Price", by=[f"{by[1]}"]), |
| 136 | + [filtered, by], |
| 137 | + ) |
| 138 | + return [ |
| 139 | + ui.text_field(value=text, on_change=set_text), |
| 140 | + plot, |
| 141 | + ] |
| 142 | + |
| 143 | + |
| 144 | +@ui.component |
| 145 | +def where_then_partition(table, by, initial_value): |
| 146 | + """ |
| 147 | + Filter the table by the value entered by the user, then re-partition it by the second passed column |
| 148 | + """ |
| 149 | + text, set_text = ui.use_state(initial_value) |
| 150 | + filtered = ui.use_memo( |
| 151 | + lambda: table.where(f"{by[0]} = `{text.upper()}`"), [text, table] |
| 152 | + ) |
| 153 | + plot = ui.use_memo( |
| 154 | + lambda: dx.line(filtered, x="Timestamp", y="Price", by=[f"{by[1]}"]), |
| 155 | + [filtered, by], |
| 156 | + ) |
| 157 | + return [ui.text_field(value=text, on_change=set_text), plot] |
| 158 | + |
| 159 | + |
| 160 | +# outputs the same thing, done two different ways depending on how you want the work done |
| 161 | +ptf = partition_then_filter(_stocks, ["Sym", "Exchange"], "DOG") |
| 162 | +wtp = where_then_partition(_stocks, ["Sym", "Exchange"], "DOG") |
| 163 | +``` |
| 164 | + |
| 165 | +## Change a plot |
| 166 | + |
| 167 | +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. |
| 168 | + |
| 169 | +```python |
| 170 | +import deephaven.plot.express as dx |
| 171 | +import deephaven.ui as ui |
| 172 | + |
| 173 | +_stocks = dx.data.stocks().where("Sym = `DOG`") |
| 174 | + |
| 175 | +plot_types = ["Line", "Scatter", "Area"] |
| 176 | + |
| 177 | + |
| 178 | +@ui.component |
| 179 | +def change_plot_type(table): |
| 180 | + plot_type, set_plot_type = ui.use_state("Line") |
| 181 | + |
| 182 | + def create_plot(t, pt): |
| 183 | + match pt: |
| 184 | + case "Line": |
| 185 | + return dx.line(t, x="Timestamp", y="Price") |
| 186 | + case "Scatter": |
| 187 | + return dx.scatter(t, x="Timestamp", y="Price") |
| 188 | + case "Area": |
| 189 | + return dx.area(t, x="Timestamp", y="Price") |
| 190 | + case _: |
| 191 | + return ui.text(f"Unknown plot type {pt}") |
| 192 | + |
| 193 | + plot = ui.use_memo(lambda: create_plot(table, plot_type), [table, plot_type]) |
| 194 | + return [ |
| 195 | + ui.picker(plot_types, selected_key=plot_type, on_change=set_plot_type), |
| 196 | + plot, |
| 197 | + ] |
| 198 | + |
| 199 | + |
| 200 | +change_plot_type_example = change_plot_type(_stocks) |
| 201 | +``` |
| 202 | + |
| 203 | +## Plots and Liveness |
| 204 | + |
| 205 | +While you may need to use a liveness scope for Deephaven tables, you do not for Deephaven Express plots. |
| 206 | + |
| 207 | +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. |
0 commit comments