diff --git a/plugins/plotly-express/docs/sidebar.json b/plugins/plotly-express/docs/sidebar.json
index 3278c9613..405aa056d 100644
--- a/plugins/plotly-express/docs/sidebar.json
+++ b/plugins/plotly-express/docs/sidebar.json
@@ -133,6 +133,10 @@
{
"label": "Multiple axes",
"path": "multiple-axes.md"
+ },
+ {
+ "label": "`unsafe_update_figure` Chart Customization",
+ "path": "unsafe_update_figure.md"
}
]
}
diff --git a/plugins/plotly-express/docs/unsafe-update-figure.md b/plugins/plotly-express/docs/unsafe-update-figure.md
new file mode 100644
index 000000000..2172441f1
--- /dev/null
+++ b/plugins/plotly-express/docs/unsafe-update-figure.md
@@ -0,0 +1,107 @@
+# `unsafe_update_figure` Chart Customization
+
+To customize a chart in a way that is not directly supported by Deephaven Plotly Express (`dx`), use the `unsafe_update_figure` parameter.
+A Plotly [`Figure`](https://plotly.com/python/figure-structure/) object backs every `dx` chart, and `unsafe_update_figure` allows you to directly modify this object.
+
+> [!WARNING]
+> Update figure is marked "unsafe" because some modifications can entirely break your figure, and care must be taken.
+> `dx` maps `Table` columns to an index of a trace within `Figure.data` which will break if the trace order changes. Do not remove traces. Add new traces at the end of the list.
+
+`unsafe_update_figure` accepts a function that takes a Plotly `Figure` object as input and optionally returns a modified `Figure` object. If a `Figure` is not returned, it is assumed that the input `Figure` has been modified in place.
+
+## Examples
+
+### Bar Line
+
+Add a line to bars in a bar plot with `update_traces`.
+
+```python
+import deephaven.plot.express as dx
+
+tips = dx.data.tips()
+
+
+def update(figure):
+ # Add a gray line to the bars
+ figure.update_traces(marker_line_width=3, marker_line_color="gray")
+
+
+bar_lined_plot = dx.bar(tips, x="Day", unsafe_update_figure=update)
+```
+
+### Legend Location
+
+Change the location of the legend to the bottom of the plot by updating the layout.
+
+```python
+import deephaven.plot.express as dx
+
+tips = dx.data.tips()
+
+
+def update(figure):
+ # Update the layout to move the legend to the bottom
+ # y is negative to move the legend outside the plot area
+ figure.update_layout(
+ legend=dict(orientation="h", yanchor="bottom", y=-0.3, xanchor="left", x=0.3)
+ )
+
+
+legend_bottom_plot = dx.scatter(
+ tips, x="TotalBill", y="Tip", color="Day", unsafe_update_figure=update
+)
+```
+
+### Vertical Line
+
+Add a vertical line to a plot with `add_vline`.
+
+```python
+import deephaven.plot.express as dx
+
+tips = dx.data.tips()
+
+
+def update(figure):
+ # Add a dashed orange vertical line at x=20
+ figure.add_vline(x=20, line_width=3, line_dash="dash", line_color="orange")
+
+
+scatter_vline_plot = dx.scatter(
+ tips, x="TotalBill", y="Tip", unsafe_update_figure=update
+)
+```
+
+### Between Line Fill
+
+Fill the area between lines in a line plot with `fill="tonexty"`.
+
+```python
+import deephaven.plot.express as dx
+
+my_table = dx.data.stocks()
+
+# subset data for just DOG transactions and add upper and lower bounds
+dog_prices = my_table.where("Sym = `DOG`").update_view(
+ ["UpperPrice = Price + 5", "LowerPrice = Price - 5"]
+)
+
+
+def update(figure):
+ # tonexty fills the area between the trace and the previous trace in the list
+ figure.update_traces(
+ fill="tonexty", fillcolor="rgba(123,67,0,0.3)", selector={"name": "LowerPrice"}
+ )
+ figure.update_traces(
+ fill="tonexty", fillcolor="rgba(123,67,0,0.3)", selector={"name": "Price"}
+ )
+
+
+# Order matters for y since the fill is between the trace and the previous trace in the list
+filled_line_plot = dx.line(
+ dog_prices,
+ x="Timestamp",
+ y=["UpperPrice", "Price", "LowerPrice"],
+ unsafe_update_figure=update,
+)
+```
\ No newline at end of file
diff --git a/plugins/ui/docs/tutorial.md b/plugins/ui/docs/tutorial.md
new file mode 100644
index 000000000..0accf7b9a
--- /dev/null
+++ b/plugins/ui/docs/tutorial.md
@@ -0,0 +1,751 @@
+# Create a Dashboard with `deephaven.ui`
+
+This guide shows you how to build a dashboard with `deephaven.ui`. [`deephaven.ui`](https://github.com/deephaven/deephaven-plugins/tree/main/plugins/ui) is Deephaven’s Python library to create user interfaces. You’ll use a wide range of components supported by the library to get you familiar with what `deephaven.ui` provides and dive deep into simulated `iris` data, with live dataframes, visualizations, and interactivity, seamlessly integrated into a dashboard.
+First, you'll learn the basic `deephaven.ui` components. Basic components use panels, which are individual and adjustable windows within Deephaven. Tables, charts, and other components are rendered in panels.
+After creating panels, you'll create a dashboard. A dashboard is a collection of panels that open in a separate window within Deephaven.
+Then, you'll create a custom panel component. Custom components enable rich interactivity within your panels and dashboards.
+Finally, you'll embed your custom component into your dashboard.
+
+![img](_assets/deephaven-ui-crash-course/iris_species_dashboard_final.png)
+To follow along, you need the [`deephaven.ui`](https://pypi.org/project/deephaven-plugin-ui/) package.
+You also need simulated data and charts from [`deephaven.plot.express`](https://pypi.org/project/deephaven-plugin-ui/). You’ll create the simulated [`iris` data set](https://en.wikipedia.org/wiki/Iris_flower_data_set) below.
+Both of these packages are included in the default setup.
+Import the data with this script:
+
+```py
+from deephaven import ui
+import deephaven.plot.express as dx
+
+iris = dx.data.iris()
+```
+
+![img](_assets/deephaven-ui-crash-course/iris.png)
+
+You'll specifically investigate `SepalLength`, `SepalWidth`, and `Species`. `Timestamp` is also useful to order the data.
+The `Species` column is a categorical column with three values: `setosa`, `versicolor`, and `virginica`.
+The `SepalLength`, `SepalWidth`, `PetalLength`, and `PetalWidth` columns are continuous numerical columns that contain measurements of the sepal and petal of an iris flower.
+You'll mostly focus on `SepalLength` and `SepalWidth` in this guide.
+
+## Basic Components
+
+Components are the building blocks of `deephaven.ui`. Each component takes parameters that control how the component appears. By default, a component renders in a panel.
+
+### `ui.table`
+
+Wrapping a table in [`ui.table`](components/table.md) unlocks visual functionality on the table.
+Since you're investigating `SepalLength` and `SepalWidth`, create a `ui.table` that accentuates the latest filtered data.
+With `iris`, create a `ui.table` that:
+
+1. Reverses the order so that the newest rows are shown first
+2. Pulls the `Species` column to the front along with `Timestamp`
+3. Hides the `PetalLength`, `PetalWidth`, and `SpeciesID` columns
+4. Uses the compact table density so you can see as many rows as possible
+
+```py
+ui_iris = ui.table(
+ iris,
+ reverse=True,
+ front_columns=["Timestamp", "Species"],
+ hidden_columns=["PetalLength", "PetalWidth", "SpeciesID"],
+ density="compact"
+)
+```
+
+![img](_assets/deephaven-ui-crash-course/ui_iris.png)
+
+### Charts
+
+Charts from Deephaven Plotly Express (`dx`) have no `deephaven.ui` specific wrapping and are added directly. Create a [`dx.scatter`](../../plotly-express/main/scatter.md) chart that compares `SepalLength` and `SepalWidth` by `Species`.
+
+```py
+scatter_by_species = dx.scatter(iris, x = "SepalLength", y = "SepalWidth", by="Species")
+```
+
+![img](_assets/deephaven-ui-crash-course/scatter_by_species.png)
+
+### `ui.text`
+
+Basic text is added with the [`ui.text`](components/text.md) component. Create text to accompany the chart and table.
+
+```py
+sepal_text = ui.text("SepalLength vs. SepalWidth By Species Panel")
+```
+
+![img](_assets/deephaven-ui-crash-course/sepal_text.png)
+
+### `ui.flex`
+
+Wrap your chart and `ui.table` in a [`ui.flex`](components/flex.md) component. `ui.flex` is an implementation of [Flexbox](https://developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout/Flexbox) that enables responsive layouts that adjust as you resize panels.
+Items within a `ui.flex` component stretch and shrink based on available space.
+
+```py
+sepal_flex = ui.flex(ui_iris, scatter_by_species)
+```
+
+![img](_assets/deephaven-ui-crash-course/sepal_flex.png)
+
+The `direction` of `sepal_flex` is `"row"`. Add `sepal_text` and `sepal_flex` to another panel, with a `direction` of `"column"`.
+
+```py
+sepal_flex_column = ui.flex(sepal_text, sepal_flex, direction="column")
+```
+
+![img](_assets/deephaven-ui-crash-course/sepal_flex_column.png)
+
+### Tabs
+
+The [`ui.tabs`](components/tabs.md) component enables tabs within a panel. Create histograms of `SepalLength` to display in tabs.
+Histograms are useful to display comparisons of data distributions, so create [`dx.histogram`](../../plotly-express/main/histogram.md) charts of the columns of interest, `SepalLength` and `SepalWidth`, by `Species`.
+Create `ui.tab` elements for `sepal_flex`, `sepal_length_hist`, and `sepal_width_hist` then pass them to `ui.tabs` to allow you to switch between different views.
+
+```py
+sepal_length_hist = dx.histogram(iris, x="SepalLength", by="Species")
+sepal_width_hist = dx.histogram(iris, x="SepalWidth", by="Species")
+
+sepal_tabs = ui.tabs(
+ ui.tab(sepal_flex, title="Sepal Length vs. Sepal Width"),
+ ui.tab(sepal_length_hist, title="Sepal Length Histogram"),
+ ui.tab(sepal_width_hist, title="Sepal Width Histogram"),
+)
+sepal_flex_tabs = ui.flex(sepal_text, sepal_tabs, direction="column")
+```
+
+![img](_assets/deephaven-ui-crash-course/sepal_flex_tabs.png)
+
+### Markdown
+
+[`ui.markdown`](components/markdown.md) components allow you to provide text in a markdown format. Create an informational panel with markdown text.
+
+```py
+about_markdown = ui.markdown(r"""
+### Iris Dashboard
+
+Explore **SepalLength** and **SepalWidth** from the Iris dataset with **deephaven.ui**
+- The data powering this dashboard is simulated Iris data
+- Charts are from Deephaven Plotly Express
+- Other components are from **deephaven.ui**
+""")
+```
+
+![img](_assets/deephaven-ui-crash-course/about_markdown.png)
+
+Now you have two responsive panels with fundamental components of `deephaven.ui` that explore `SepalLength` and `SepalWidth` by `Species`.
+
+## Dashboard
+
+By default, components are rendered in a panel, but a dashboard enables more complex layouts in an isolated window.
+When you have multiple panels, you can create a dashboard to contain them so that you can add even more panels later for your `iris` data exploration.
+
+
+Expand for code up to this point
+
+```py skip-test
+from deephaven import ui
+import deephaven.plot.express as dx
+from deephaven import agg
+
+iris = dx.data.iris()
+
+ui_iris = ui.table(
+ iris,
+ reverse=True,
+ front_columns=["Timestamp", "Species"],
+ hidden_columns=["PetalLength", "PetalWidth", "SpeciesID"],
+ density="compact",
+)
+
+scatter_by_species = dx.scatter(iris, x="SepalLength", y="SepalWidth", by="Species")
+
+sepal_text = ui.text("SepalLength vs. SepalWidth By Species Panel")
+
+sepal_flex = ui.flex(ui_iris, scatter_by_species)
+
+sepal_flex_column = ui.flex(sepal_text, sepal_flex, direction="column")
+
+sepal_length_hist = dx.histogram(iris, x="SepalLength", by="Species")
+sepal_width_hist = dx.histogram(iris, x="SepalWidth", by="Species")
+
+sepal_tabs = ui.tabs(
+ ui.tab(sepal_flex, title="Sepal Length vs. Sepal Width"),
+ ui.tab(sepal_length_hist, title="Sepal Length Histogram"),
+ ui.tab(sepal_width_hist, title="Sepal Width Histogram"),
+)
+sepal_flex_tabs = ui.flex(sepal_text, sepal_tabs, direction="column")
+
+about_markdown = ui.markdown(r"""
+### Iris Dashboard
+
+Explore the Iris dataset with **deephaven.ui**
+
+- The data powering this dashboard is simulated Iris data
+- Charts are from Deephaven Plotly Express
+- Other components are from **deephaven.ui**
+ """)
+
+sepal_panel = ui.panel(sepal_flex_tabs, title="Sepal Panel")
+about_panel = ui.panel(about_markdown, title="About")
+```
+
+
+
+Before that, create a `ui.panel` manually to provide a `title`.
+
+```py
+sepal_panel = ui.panel(sepal_flex_tabs, title="Sepal Panel")
+iris_dashboard = ui.dashboard(sepal_panel)
+```
+
+![img](_assets/deephaven-ui-crash-course/iris_dashboard.png)
+
+You now have a one panel dashboard.
+
+With multiple panels, you can create a default layout. Create a panel for `about_markdown` so you can provide it a `title`.
+
+```py
+about_panel = ui.panel(about_markdown, title="About")
+```
+
+![img](_assets/deephaven-ui-crash-course/about_panel.png)
+
+### Row
+
+One way to create a default layout is by wrapping your panels in a [`ui.row`](components/dashboard.md) component.
+
+```py
+iris_dashboard_row = ui.dashboard(ui.row(about_panel, sepal_panel))
+```
+
+![img](_assets/deephaven-ui-crash-course/iris_dashboard_row.png)
+
+### Column
+
+Another way to create a default layout is with [`ui.column`](components/dashboard.md).
+
+```py
+iris_dashboard_column = ui.dashboard(ui.column(about_panel, sepal_panel))
+```
+
+![img](_assets/deephaven-ui-crash-course/iris_dashboard_column.png)
+
+### Stack
+
+One more way to create a default layout is with [`ui.stack`](components/dashboard.md). The last panel is active by default.
+`ui.stack` is useful if you want a tabbed layout but also the ability to move the individual components around, which is not possible with `ui.tabs`.
+Create tabs that show the average, max, and min values of `SepalLength`, `SepalWidth`, `PetalLength`, and `PetalWidth` by `Species`.
+You can compare a specific statistic across the different `Species` within the same panel or move between panels to compare different statistics across `Species`.
+
+```py
+from deephaven import agg
+
+iris_avg = iris.agg_by([agg.avg(cols=["SepalLength", "SepalWidth", "PetalLength", "PetalWidth"])], by=["Species"])
+iris_max = iris.agg_by([agg.max_(cols=["SepalLength", "SepalWidth", "PetalLength", "PetalWidth"])], by=["Species"])
+iris_min = iris.agg_by([agg.min_(cols=["SepalLength", "SepalWidth", "PetalLength", "PetalWidth"])], by=["Species"])
+
+ui_iris_avg = ui.panel(iris_avg, title="Average")
+ui_iris_max = ui.panel(iris_max, title="Max")
+ui_iris_min = ui.panel(iris_min, title="Min")
+
+iris_agg_stack = ui.stack(ui_iris_avg, ui_iris_max, ui_iris_min)
+
+iris_dashboard_stack = ui.dashboard(iris_agg_stack)
+```
+
+![img](_assets/deephaven-ui-crash-course/iris_dashboard_stack.png)
+
+## Interactivity
+
+So far, you’ve worked with `deephaven.ui` components that don’t interact with each other. Now, you’ll create your own component with interactivity and embed it into your dashboard.
+Since you're investigating `SepalLength` and `SepalWidth` create a [`dx.densityheatmap`](../../plotly-express/main/density_heatmap.md) chart that shows the density of `SepalLength` and `SepalWidth`, with a `Species` filter.
+
+### `ui.component`
+
+A custom component uses the `ui.component` decorator. The decorator signals to the `deephaven.ui` rendering engine that this component needs to be rendered. Create a function with the `ui.component` decorator that returns `"Hello, World!"`.
+
+```py
+@ui.component
+def custom_component():
+ return "Hello, World!"
+
+custom_panel = custom_component()
+```
+
+![img](_assets/deephaven-ui-crash-course/custom_panel.png)
+
+### Picker
+
+A [`ui.picker`](components/picker.md) allows you to select options from a list. Start by creating one.
+
+```py
+@ui.component
+def species_panel():
+ species_picker = ui.picker("setosa", "versicolor", "virginica")
+
+ return species_picker
+
+picker_panel = species_panel()
+```
+
+![img](_assets/deephaven-ui-crash-course/picker_panel.png)
+
+### Table-backed `ui.picker`
+
+`ui.pickers` can pull directly from a table so they update automatically based on a column in the table. Modify your `ui.picker` to pass in a table instead, which is recommended for dynamic data.
+
+> [!NOTE]
+> It’s important to filter your table down to the distinct values you want. The `ui.picker` does not do this for you.
+
+```py
+species_table = iris.view("Species").select_distinct()
+
+@ui.component
+def species_panel():
+ species_picker = ui.picker(species_table)
+
+ return species_picker
+
+species_picker_panel = species_panel()
+```
+
+![img](_assets/deephaven-ui-crash-course/species_picker_panel.png)
+
+### `ui.use_state`
+
+The [`ui.use_state`](hooks/use_state.md) hook is how you’ll enable interactivity. The hook takes a starting value and returns a tuple of the current value and a function to set the value.
+Next, modify your picker to take the `species` and `set_species` from the hook you just defined using `on_change` and `selected_key`. `on_change` is called whenever an option is selected. `selected_key` is the currently selected option. Using these makes the component controlled, meaning that the `selected_key` is controlled outside of the `ui.picker` itself. This allows you to use the `selected_key` in other ways such as filtering a table.
+
+> [!WARNING]
+> Hooks like `ui.use_state` are only available in components decorated with `ui.component`.
+> Additionally, hooks should not be called conditionally within a component. Create a new component if conditional rendering is needed.
+
+```py
+@ui.component
+def species_panel():
+ species, set_species = ui.use_state()
+ species_picker = ui.picker(species_table, on_change=set_species, selected_key=species, label="Current Species")
+
+ return species_picker
+
+species_picker_panel = species_panel()
+```
+
+![img](_assets/deephaven-ui-crash-course/species_picker_panel_controlled.png)
+
+Now your picker is both controlled by the rendering engine and updates as you pick values and you have an up-to-date value from the table to filter on.
+
+### Utilizing State
+
+Add in a table filtered on this value and a chart that uses this filtered table and return them with the picker.
+You'll create a `dx.heatmap`, which shows the joint density of `SepalLength` and `SepalWidth`, the variables of interest, filtered by `Species`.
+
+```py
+@ui.component
+def species_panel():
+ species, set_species = ui.use_state()
+ species_picker = ui.picker(
+ species_table,
+ on_change=set_species,
+ selected_key=species,
+ label="Current Species"
+ )
+
+ filtered_table = iris.where("Species = species")
+
+ heatmap = dx.density_heatmap(filtered_table, x="SepalLength", y="SepalWidth")
+
+ return ui.panel(ui.flex(species_picker, heatmap, direction="column"), title="Investigate Species")
+
+species_picker_panel = species_panel()
+```
+
+![img](_assets/deephaven-ui-crash-course/species_picker_panel_investigate.png)
+
+### `ui.illustrated_message`
+
+Currently, an empty table and chart appears if no species is selected. Add a [`ui.illustrated_message`](components/illustrated_message.md) component to display instead if no `species` is selected for a more user-friendly experience.
+
+```py
+@ui.component
+def species_panel():
+ species, set_species = ui.use_state()
+ species_picker = ui.picker(
+ species_table,
+ on_change=set_species,
+ selected_key=species,
+ label="Current Species"
+ )
+
+ heatmap = ui.illustrated_message(
+ ui.icon("vsFilter"),
+ ui.heading("Species required"),
+ ui.content("Select a species to display filtered table and chart."),
+ width="100%",
+ )
+
+ if species:
+ filtered_table = iris.where("Species = species")
+
+ heatmap = dx.density_heatmap(filtered_table, x="SepalLength", y="SepalWidth")
+
+ return ui.panel(ui.flex(species_picker, heatmap, direction="column"), title="Investigate Species")
+
+species_picker_panel = species_panel()
+```
+
+![img](_assets/deephaven-ui-crash-course/species_picker_panel_illustrated.png)
+
+
+### Utilizing Custom Components
+
+Next, embed your custom component in your dashboard.
+```python
+iris_species_dashboard = ui.dashboard(
+ ui.column(
+ ui.row(about_panel, iris_agg_stack), ui.row(sepal_panel, species_picker_panel)
+ )
+)
+```
+![img](_assets/deephaven-ui-crash-course/iris_species_dashboard.png)
+
+```py
+iris_species_dashboard_resized = ui.dashboard(ui.column(ui.row(about_panel, iris_agg_stack, height=1), ui.row(sepal_panel, species_picker_panel, height=2)))
+```
+
+![img](_assets/deephaven-ui-crash-course/iris_species_dashboard_resized.png)
+
+### Dashboard State Across Panels
+Currently, the `ui.picker` selects `Species` within a panel. You can change state across panels as well.
+Recreate the `sepal_flex_tabs` panel within the `create_sepal_panel` function, which takes a `set_species` function as an argument.
+`ui.table` events are useful if you see a row you want to investigate further.
+Add a listener, `on_row_double_press`, to the `ui.table`. `on_row_double_press` is called when a row is double-clicked, returning the row data, which then sets the `Species` value that is displayed in the `ui.picker` and `dx.density_heatmap`.
+Pull the `Species` value from the row data and set it with `set_species`.
+Then, create `sepal_panel` with `create_sepal_panel`, passing `set_species` to it.
+
+```python
+def create_sepal_panel(set_species):
+ ui_iris = ui.table(
+ iris,
+ reverse=True,
+ front_columns=["Timestamp", "Species"],
+ hidden_columns=["PetalLength", "PetalWidth", "SpeciesID"],
+ density="compact",
+ on_row_double_press=lambda event: set_species(event["Species"]["value"]),
+ )
+
+ sepal_flex = ui.flex(ui_iris, scatter_by_species)
+
+ sepal_tabs = ui.tabs(
+ ui.tab(sepal_flex, title="Sepal Length vs. Sepal Width"),
+ ui.tab(sepal_length_hist, title="Sepal Length Histogram"),
+ ui.tab(sepal_width_hist, title="Sepal Width Histogram"),
+ )
+
+ sepal_flex_tabs = ui.flex(sepal_text, sepal_tabs, direction="column")
+
+ return ui.panel(sepal_flex_tabs, title="Sepal Panel")
+
+
+@ui.component
+def create_species_dashboard():
+ species, set_species = ui.use_state()
+ species_picker = ui.picker(
+ species_table,
+ on_change=set_species,
+ selected_key=species,
+ label="Current Species",
+ )
+
+ heatmap = ui.illustrated_message(
+ ui.icon("vsFilter"),
+ ui.heading("Species required"),
+ ui.content("Select a species to display filtered table and chart."),
+ width="100%",
+ )
+
+ if species:
+ filtered_table = iris.where("Species = species")
+
+ heatmap = dx.density_heatmap(filtered_table, x="SepalLength", y="SepalWidth")
+
+ species_panel = ui.panel(
+ ui.flex(species_picker, heatmap, direction="column"),
+ title="Investigate Species",
+ )
+
+ sepal_panel = create_sepal_panel(set_species)
+
+ return ui.column(
+ ui.row(about_panel, iris_agg_stack, height=1),
+ ui.row(sepal_panel, species_panel, height=2),
+ )
+
+
+iris_species_dashboard_state = ui.dashboard(create_species_dashboard())
+```
+
+![img](_assets/deephaven-ui-crash-course/iris_species_dashboard_resized.png)
+
+### `ui.use_row_data` and `ui.badge`
+You've got a density heatmap that shows the density of `SepalLength` and `SepalWidth` by `Species`, but it's tricky to see the min, max, and average overall values for `SepalLength` and `SepalWidth` for the selected `Species`. They're in your table stack, but you can make them more prominent.
+
+`deephaven.ui` provides hooks to access table data. Each hook provides access to a specific part of the table and is updated when the table changes.
+The hooks are:
+- [`ui.use_cell_data`](hooks/use_cell_data.md) - Access a single cell value
+- [`ui.use_row_data`](hooks/use_row_data.md) - Access a row of data
+- [`ui.use_row_list`](hooks/use_row_list.md) - Access a row as a list
+- [`ui.use_column_data`](hooks/use_column_data.md) - Access a column of data
+- [`ui.use_table_data`](hooks/use_table_data.md) - Access the entire table
+
+> [!WARNING]
+> Filter your table to only the data you need the hook to pull out. The hooks do not filter the table for you.
+
+Create a custom component that pulls the min, max, and average values for `SepalLength` and `SepalWidth` for the selected `Species`.
+Wrap the values in [`ui.badge`](components/badge.md) components to display them. `ui.badge` components draw attention to specific values.
+
+```python
+@ui.component
+def summary_badges(species):
+ # Filter the tables to the selected species
+ species_min = iris_min.where("Species=species")
+ species_max = iris_max.where("Species=species")
+ species_avg = iris_avg.where("Species=species")
+
+ # Pull the desired columns from the tables before using the hooks
+ sepal_length_min = ui.use_cell_data(species_min.view(["SepalLength"]))
+ sepal_width_min = ui.use_cell_data(species_min.view(["SepalWidth"]))
+ sepal_length_max = ui.use_cell_data(species_max.view(["SepalLength"]))
+ sepal_width_max = ui.use_cell_data(species_max.view(["SepalWidth"]))
+ sepal_length_avg = ui.use_cell_data(species_avg.view(["SepalLength"]))
+ sepal_width_avg = ui.use_cell_data(species_avg.view(["SepalWidth"]))
+
+ # format the values to 3 decimal places
+ return ui.flex(
+ ui.badge(f"SepalLength Min: {sepal_length_min:.3f}", variant="info"),
+ ui.badge(f"SepalLength Max: {sepal_length_max:.3f}", variant="info"),
+ ui.badge(f"SepalLength Avg: {sepal_length_avg:.3f}", variant="info"),
+ ui.badge(f"SepalWidth Min: {sepal_width_min:.3f}", variant="info"),
+ ui.badge(f"SepalWidth Max: {sepal_width_max:.3f}", variant="info"),
+ ui.badge(f"SepalWidth Avg: {sepal_width_avg:.3f}", variant="info"),
+ )
+
+
+@ui.component
+def create_species_dashboard():
+ species, set_species = ui.use_state()
+ species_picker = ui.picker(
+ species_table,
+ on_change=set_species,
+ selected_key=species,
+ label="Current Species",
+ )
+
+ heatmap = ui.illustrated_message(
+ ui.icon("vsFilter"),
+ ui.heading("Species required"),
+ ui.content("Select a species to display filtered table and chart."),
+ width="100%",
+ )
+
+ badges = None
+
+ if species:
+ filtered_table = iris.where("Species = species")
+
+ heatmap = dx.density_heatmap(filtered_table, x="SepalLength", y="SepalWidth")
+
+ badges = summary_badges(species)
+
+ species_panel = ui.panel(
+ ui.flex(species_picker, badges, heatmap, direction="column"),
+ title="Investigate Species",
+ )
+
+ sepal_panel = create_sepal_panel(set_species)
+
+ return ui.column(
+ ui.row(about_panel, iris_agg_stack, height=1),
+ ui.row(sepal_panel, species_panel, height=2),
+ )
+
+
+iris_species_dashboard_final = ui.dashboard(create_species_dashboard())
+```
+
+![img](_assets/deephaven-ui-crash-course/iris_species_dashboard_final.png)
+
+
+Expand for final code
+
+```py skip-test
+from deephaven import ui
+import deephaven.plot.express as dx
+from deephaven import agg
+
+iris = dx.data.iris()
+
+ui_iris = ui.table(
+ iris,
+ reverse=True,
+ front_columns=["Timestamp", "Species"],
+ hidden_columns=["PetalLength", "PetalWidth", "SpeciesID"],
+ density="compact",
+)
+
+scatter_by_species = dx.scatter(iris, x="SepalLength", y="SepalWidth", by="Species")
+
+sepal_text = ui.text("SepalLength vs. SepalWidth By Species Panel")
+
+sepal_flex = ui.flex(ui_iris, scatter_by_species)
+
+sepal_flex_column = ui.flex(sepal_text, sepal_flex, direction="column")
+
+sepal_length_hist = dx.histogram(iris, x="SepalLength", by="Species")
+sepal_width_hist = dx.histogram(iris, x="SepalWidth", by="Species")
+
+sepal_tabs = ui.tabs(
+ ui.tab(sepal_flex, title="Sepal Length vs. Sepal Width"),
+ ui.tab(sepal_length_hist, title="Sepal Length Histogram"),
+ ui.tab(sepal_width_hist, title="Sepal Width Histogram"),
+)
+sepal_flex_tabs = ui.flex(sepal_text, sepal_tabs, direction="column")
+
+about_markdown = ui.markdown(r"""
+### Iris Dashboard
+
+Explore the Iris dataset with **deephaven.ui**
+
+- The data powering this dashboard is simulated Iris data
+- Charts are from Deephaven Plotly Express
+- Other components are from **deephaven.ui**
+ """)
+
+sepal_panel = ui.panel(sepal_flex_tabs, title="Sepal Panel")
+about_panel = ui.panel(about_markdown, title="About")
+
+iris_avg = iris.agg_by([agg.avg(cols=["SepalLength", "SepalWidth", "PetalLength", "PetalWidth"])], by=["Species"])
+iris_max = iris.agg_by([agg.max_(cols=["SepalLength", "SepalWidth", "PetalLength", "PetalWidth"])], by=["Species"])
+iris_min = iris.agg_by([agg.min_(cols=["SepalLength", "SepalWidth", "PetalLength", "PetalWidth"])], by=["Species"])
+
+ui_iris_avg = ui.panel(iris_avg, title="Average")
+ui_iris_max = ui.panel(iris_max, title="Max")
+ui_iris_min = ui.panel(iris_min, title="Min")
+
+iris_agg_stack = ui.stack(ui_iris_avg, ui_iris_max, ui_iris_min)
+
+species_table = iris.view("Species").select_distinct()
+
+def create_sepal_panel(set_species):
+ ui_iris = ui.table(
+ iris,
+ reverse=True,
+ front_columns=["Timestamp", "Species"],
+ hidden_columns=["PetalLength", "PetalWidth", "SpeciesID"],
+ density="compact",
+ on_row_double_press=lambda event: set_species(event["Species"]["value"])
+ )
+
+ sepal_flex = ui.flex(ui_iris, scatter_by_species)
+
+ sepal_tabs = ui.tabs(
+ ui.tab(sepal_flex, title="Sepal Length vs. Sepal Width"),
+ ui.tab(sepal_length_hist, title="Sepal Length Histogram"),
+ ui.tab(sepal_width_hist, title="Sepal Width Histogram"),
+ )
+
+ sepal_flex_tabs = ui.flex(sepal_text, sepal_tabs, direction="column")
+
+ return ui.panel(sepal_flex_tabs, title="Sepal Panel")
+
+@ui.component
+def summary_badges(species):
+ # Filter the tables to the selected species
+ species_min = iris_min.where("Species=species")
+ species_max = iris_max.where("Species=species")
+ species_avg = iris_avg.where("Species=species")
+
+ # Pull the desired columns from the tables before using the hooks
+ sepal_length_min = ui.use_cell_data(species_min.view(["SepalLength"]))
+ sepal_width_min = ui.use_cell_data(species_min.view(["SepalWidth"]))
+ sepal_length_max = ui.use_cell_data(species_max.view(["SepalLength"]))
+ sepal_width_max = ui.use_cell_data(species_max.view(["SepalWidth"]))
+ sepal_length_avg = ui.use_cell_data(species_avg.view(["SepalLength"]))
+ sepal_width_avg = ui.use_cell_data(species_avg.view(["SepalWidth"]))
+
+ # format the values to 3 decimal places
+ return ui.flex(
+ ui.badge(f"SepalLength Min: {sepal_length_min:.3f}", variant="info"),
+ ui.badge(f"SepalLength Max: {sepal_length_max:.3f}", variant="info"),
+ ui.badge(f"SepalLength Avg: {sepal_length_avg:.3f}", variant="info"),
+ ui.badge(f"SepalWidth Min: {sepal_width_min:.3f}", variant="info"),
+ ui.badge(f"SepalWidth Max: {sepal_width_max:.3f}", variant="info"),
+ ui.badge(f"SepalWidth Avg: {sepal_width_avg:.3f}", variant="info"),
+ )
+
+@ui.component
+def create_species_dashboard():
+ species, set_species = ui.use_state()
+ species_picker = ui.picker(
+ species_table,
+ on_change=set_species,
+ selected_key=species,
+ label="Current Species"
+ )
+
+ heatmap = ui.illustrated_message(
+ ui.icon("vsFilter"),
+ ui.heading("Species required"),
+ ui.content("Select a species to display filtered table and chart."),
+ width="100%",
+ )
+
+ badges = None
+
+ if species:
+ filtered_table = iris.where("Species = species")
+
+ heatmap = dx.density_heatmap(filtered_table, x="SepalLength", y="SepalWidth")
+
+ badges = summary_badges(species)
+
+ species_panel = ui.panel(ui.flex(species_picker, badges, heatmap, direction="column"), title="Investigate Species")
+
+ sepal_panel = create_sepal_panel(set_species)
+
+ return ui.column(ui.row(about_panel, iris_agg_stack, height=1), ui.row(sepal_panel, species_panel, height=2))
+
+iris_species_dashboard_final = ui.dashboard(create_species_dashboard())
+```
+
+
+
+Finally, you’ve completed this dashboard crash course, with your custom component and interactivity.
+
+## Wrapping Up
+
+This wraps up the `deephaven.ui` dashboard crash course. In this course you learned about the following components and concepts and created a dashboard with many of them
+
+- [`ui.table`](components/table.md)
+- [`ui.text`](components/text.md)
+- [`ui.flex`](components/flex.md)
+- [`ui.tabs`](components/tabs.md)
+- [`ui.markdown`](components/markdown.md)
+- [`ui.dashboard`](components/dashboard.md)
+- [`ui.row`](components/row.md)
+- [`ui.column`](components/column.md)
+- [`ui.stack`](components/stack.md)
+- [`ui.component`](describing/your_first_component.md)
+- [`ui.picker`](components/picker.md)
+- [`ui.use_state`](hooks/use_state.md)
+- [`ui.illustrated_message`](components/illustrated_message.md)
+- [`ui.use_cell_data`](hooks/use_cell_data.md)
+- [`ui.use_row_data`](hooks/use_row_data.md)
+- [`ui.use_row_list`](hooks/use_row_list.md)
+- [`ui.use_column_data`](hooks/use_column_data.md)
+- [`ui.use_table_data`](hooks/use_table_data.md)
+- [`ui.badge`](components/illustrated_message.md)
+- [`dx.scatter`](../../plotly-express/main/scatter.md)
+- [`dx.histogram`](../../plotly-express/main/histogram.md)
+- [`dx.densityheatmap`](../../plotly-express/main/density_heatmap.md)
+- Component State
+- Custom Components
+
+This gets you started on the rich set of components `deephaven.ui` has to offer to create a dashboard.