Skip to content

Commit 334829d

Browse files
feat: DH-18317: Add hierarchical path (#1164)
Fixes https://deephaven.atlassian.net/browse/DH-18317 Examples are in docs --------- Co-authored-by: Mike Bender <mikebender@deephaven.io>
1 parent 86150c2 commit 334829d

18 files changed

Lines changed: 1455 additions & 59 deletions

plugins/plotly-express/docs/icicle.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@ gapminder_recent = (
3131

3232
icicle_plot = dx.icicle(gapminder_recent, names="Continent", values="Pop", parents="World")
3333
```
34+
### An icicle plot with `path`
35+
36+
Instead of manually aggregating and passing in `names` and `parents`, use the `path` argument to specify the hierarchy of the data. The first column is the root category, and the last column is the leaf category. The values are automatically summed up.
37+
38+
```python order=treemap_path_plot,gapminder
39+
import deephaven.plot.express as dx
40+
41+
gapminder = dx.data.gapminder().update_view("World = `World`")
42+
43+
icicle_path_plot = dx.icicle(gapminder, path=["World", "Continent", "Country"], values="Pop")
44+
```
3445

3546
![Icicle Plot Basic Example](./_assets/icicle_plot.png)
3647

plugins/plotly-express/docs/sunburst.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,18 @@ sunburst_plot = dx.sunburst(gapminder_recent, names="Continent", values="Pop", p
3434

3535
![Sunburst Plot Basic Example](./_assets/sunburst_plot.png)
3636

37+
### A sunburst plot with `path`
38+
39+
Instead of manually aggregating and passing in `names` and `parents`, use the `path` argument to specify the hierarchy of the data. The first column is the root category, and the last column is the leaf category. The values are automatically summed up.
40+
41+
```python order=treemap_path_plot,gapminder
42+
import deephaven.plot.express as dx
43+
44+
gapminder = dx.data.gapminder().update_view("World = `World`")
45+
46+
sunburst_path_plot = dx.sunburst(gapminder, path=["World", "Continent", "Country"], values="Pop")
47+
```
48+
3749
# A nested sunburst plot with branch values
3850

3951
By default, the `branchvalues` argument is set to `"remainder"`.

plugins/plotly-express/docs/treemap.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,18 @@ treemap_plot = dx.treemap(gapminder_recent, names="Continent", values="Pop", par
3434

3535
![Treemap Plot Basic Example](./_assets/treemap_plot.png)
3636

37+
### A treemap plot with `path`
38+
39+
Instead of manually aggregating and passing in `names` and `parents`, use the `path` argument to specify the hierarchy of the data. The first column is the root category, and the last column is the leaf category. The values are automatically summed up.
40+
41+
```python order=treemap_path_plot,gapminder
42+
import deephaven.plot.express as dx
43+
44+
gapminder = dx.data.gapminder().update_view("World = `World`")
45+
46+
treemap_path_plot = dx.treemap(gapminder, path=["World", "Continent", "Country"], values="Pop")
47+
```
48+
3749
# A nested treemap plot with branch values
3850

3951
By default, the `branchvalues` argument is set to `"remainder"`.

plugins/plotly-express/src/deephaven/plot/express/plots/PartitionManager.py

Lines changed: 38 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from .. import DeephavenFigure
1616
from ..preprocess.Preprocessor import Preprocessor
1717
from ..shared import get_unique_names
18+
from ..types import AttachedTransforms, HierarchicalTransforms
1819
from .subplots import atomic_make_grid
1920

2021
PARTITION_ARGS = {
@@ -191,11 +192,9 @@ class PartitionManager:
191192
has_color: bool: True if this figure has user set color, False otherwise
192193
facet_row: str: The facet row
193194
facet_col: str: The facet col
194-
(arg, col), (map, ls, new_col)
195-
always_attached: dict[tuple[str, str],
196-
tuple[dict[str, str], list[str], str]: The dict mapping the arg and column
197-
to the style map, dictionary, and new column name, to be used for
198-
AttachedProcessor when dealing with an "always_attached" plot
195+
attached_transforms: to be used for AttachedProcessor when dealing with an "always_attached" plot
196+
hierarchical_transforms: HierarchicalTransforms: to be used for HierarchicalProcessor when dealing with
197+
a hierarchical plot with a path
199198
marginal_x: Type of marginal on the x-axis, if applicable
200199
marginal_y: Type of marginal on the y-axis, if applicable
201200
marg_args: dict[str, Any]: The dictionary of args to pass to marginals
@@ -226,7 +225,8 @@ def __init__(
226225
self.has_color = None
227226
self.facet_row = None
228227
self.facet_col = None
229-
self.always_attached = {}
228+
self.attached_transforms = AttachedTransforms()
229+
self.hierarchical_transforms = HierarchicalTransforms()
230230

231231
self.marginal_x = args.pop("marginal_x", None)
232232
self.marginal_y = args.pop("marginal_y", None)
@@ -310,7 +310,7 @@ def convert_table_to_long_mode(
310310

311311
args["table"] = self.to_long_mode(table, self.cols)
312312

313-
def is_by(self, arg: str, map_val: str | list[str] | None = None) -> None:
313+
def is_by(self, arg: str, map_val: str | list[str] | dict | None = None) -> None:
314314
"""
315315
Given that the specific arg is a by arg, prepare the arg depending on
316316
if it is attached or not
@@ -325,10 +325,16 @@ def is_by(self, arg: str, map_val: str | list[str] | None = None) -> None:
325325

326326
if "always_attached" in self.groups:
327327
new_col = get_unique_names(self.args["table"], [arg])[arg]
328-
self.always_attached[(arg, self.args[arg])] = (
329-
map_val,
330-
self.args[seq_arg],
331-
new_col,
328+
if not isinstance(map_val, dict) and map_val is not None:
329+
raise TypeError(
330+
f"Expected a dictionary for {arg} map, got {type(map_val)}"
331+
)
332+
self.attached_transforms.add(
333+
by_col=self.args[arg],
334+
new_col=new_col,
335+
style_map=map_val,
336+
style_list=self.args[seq_arg],
337+
style=arg,
332338
)
333339
# a new column will be constructed so this color is always updated
334340
self.args[f"attached_{arg}"] = new_col
@@ -395,9 +401,13 @@ def handle_plot_by_arg(
395401
):
396402
if "always_attached" in self.groups:
397403
args["colors"] = args.pop("color")
398-
# just keep the argument in place so it can be passed to plotly
399-
# express directly
400-
pass
404+
if self.args.get("path"):
405+
# is_single_numeric_col must be true so it is safe to pull the first element
406+
if not isinstance(val, str):
407+
val = val[0]
408+
# numeric column that is the source of color need to be aggregated if path is passed
409+
self.hierarchical_transforms.add(avg_col=val)
410+
# otherwise the colors are attached directly
401411
elif val:
402412
self.is_by(arg, args[map_name])
403413
elif plot_by_cols and (
@@ -511,7 +521,8 @@ def process_partitions(self) -> Table | PartitionedTable:
511521
self.preprocessor = Preprocessor(
512522
args,
513523
self.groups,
514-
self.always_attached,
524+
self.attached_transforms,
525+
self.hierarchical_transforms,
515526
self.stacked_column_names,
516527
self.list_param,
517528
)
@@ -645,15 +656,16 @@ def table_partition_generator(
645656
column = (
646657
self.stacked_column_names["value"] if self.stacked_column_names else None
647658
)
648-
if self.preprocessor:
649-
tables = self.preprocessor.preprocess_partitioned_tables(
650-
self.constituents, column
651-
)
652-
for table, current_partition in zip(
653-
tables, self.current_partition_generator()
654-
):
655-
# since this is preprocessed it will always be a tuple
656-
yield cast(Tuple[Table, Dict[str, str]], (table, current_partition))
659+
660+
if self.preprocessor is None:
661+
return
662+
663+
tables = self.preprocessor.preprocess_partitioned_tables(
664+
self.constituents, column
665+
)
666+
for table, current_partition in zip(tables, self.current_partition_generator()):
667+
# since this is preprocessed it will always be a tuple
668+
yield cast(Tuple[Table, Dict[str, str]], (table, current_partition))
657669

658670
def partition_generator(self) -> Generator[dict[str, Any], None, None]:
659671
"""
@@ -681,13 +693,8 @@ def partition_generator(self) -> Generator[dict[str, Any], None, None]:
681693
args["current_partition"] = current_partition
682694
args["table"] = table
683695
yield args
684-
elif (
685-
"preprocess_hist" in self.groups
686-
or "preprocess_freq" in self.groups
687-
or "preprocess_time" in self.groups
688-
or "preprocess_heatmap" in self.groups
689-
) and self.preprocessor:
690-
# still need to preprocess the base table
696+
elif self.preprocessor:
697+
# still need to preprocess the base table if preprocessors were created
691698
table, arg_update = cast(
692699
Tuple,
693700
[*self.preprocessor.preprocess_partitioned_tables([args["table"]])][0],

plugins/plotly-express/src/deephaven/plot/express/plots/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from .bar import bar, frequency_bar, timeline
55
from .distribution import histogram, violin, strip, box
66
from .financial import candlestick, ohlc
7-
from .hierarchial import treemap, icicle, sunburst, funnel, funnel_area
7+
from .hierarchical import treemap, icicle, sunburst, funnel, funnel_area
88
from .pie import pie
99
from ._layer import layer
1010
from .subplots import make_subplots

plugins/plotly-express/src/deephaven/plot/express/plots/hierarchial.py renamed to plugins/plotly-express/src/deephaven/plot/express/plots/hierarchical.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ def treemap(
1616
values: str | None = None,
1717
parents: str | None = None,
1818
ids: str | None = None,
19+
path: str | list[str] | None = None,
1920
color: str | list[str] | None = None,
2021
hover_name: str | None = None,
2122
color_discrete_sequence: list[str] | None = None,
@@ -39,10 +40,14 @@ def treemap(
3940
parents: The column containing parents of the sections
4041
ids: The column containing ids of the sections. Unlike values, these
4142
must be unique. Values are used for ids if ids are not specified.
43+
path: A column or list of columns that describe the hierarchy.
44+
The first column is the root, the second column contains the children
45+
of the root, and so on. The last column is the leaf.
4246
color: A column or list of columns that contain color values.
4347
If only one column is passed, and it contains numeric values, the value
4448
is used as a value on a continuous color scale. Otherwise, the value is
4549
used for a plot by on color.
50+
If path is provided, only a single color column is allowed.
4651
See color_discrete_map for additional behaviors.
4752
hover_name: A column that contains names to bold in the hover tooltip.
4853
labels: A dictionary of labels mapping columns to new labels.
@@ -84,6 +89,7 @@ def sunburst(
8489
values: str | None = None,
8590
parents: str | None = None,
8691
ids: str | None = None,
92+
path: str | list[str] | None = None,
8793
color: str | list[str] | None = None,
8894
hover_name: str | None = None,
8995
color_discrete_sequence: list[str] | None = None,
@@ -107,10 +113,14 @@ def sunburst(
107113
parents: The column containing parents of the sections
108114
ids: The column containing ids of the sections. Unlike values, these
109115
must be unique. Values are used for ids if ids are not specified.
116+
path: A column or list of columns that describe the hierarchy.
117+
The first column is the root, the second column contains the children
118+
of the root, and so on. The last column is the leaf.
110119
color: A column or list of columns that contain color values.
111120
If only one column is passed, and it contains numeric values, the value
112121
is used as a value on a continuous color scale. Otherwise, the value is
113122
used for a plot by on color.
123+
If path is provided, only a single color column is allowed.
114124
See color_discrete_map for additional behaviors.
115125
hover_name: A column that contains names to bold in the hover tooltip.
116126
color_discrete_sequence: A list of colors to sequentially apply to
@@ -152,6 +162,7 @@ def icicle(
152162
values: str | None = None,
153163
parents: str | None = None,
154164
ids: str | None = None,
165+
path: str | list[str] | None = None,
155166
color: str | list[str] | None = None,
156167
hover_name: str | None = None,
157168
color_discrete_sequence: list[str] | None = None,
@@ -173,13 +184,17 @@ def icicle(
173184
names: The column containing names of the sections
174185
values: The column containing values of the sections
175186
parents: The column containing parents of the sections
187+
ids: The column containing ids of the sections. Unlike values, these
188+
must be unique. Values are used for ids if ids are not specified.
189+
path: A column or list of columns that describe the hierarchy.
190+
The first column is the root, the second column contains the children
191+
of the root, and so on. The last column is the leaf.
176192
color: A column or list of columns that contain color values.
177193
If only one column is passed, and it contains numeric values, the value
178194
is used as a value on a continuous color scale. Otherwise, the value is
179195
used for a plot by on color.
196+
If path is provided, only a single color column is allowed.
180197
See color_discrete_map for additional behaviors.
181-
ids: The column containing ids of the sections. Unlike values, these
182-
must be unique. Values are used for ids if ids are not specified.
183198
hover_name: A column that contains names to bold in the hover tooltip.
184199
color_discrete_sequence: A list of colors to sequentially apply to
185200
the series. The colors loop, so if there are more series than colors,

plugins/plotly-express/src/deephaven/plot/express/preprocess/AttachedPreprocessor.py

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
from typing import Any
44

5+
from deephaven.table import Table
6+
57
from .StyleManager import StyleManager
68
from ..shared import get_unique_names
9+
from ..types import AttachedTransforms
710

811

912
class AttachedPreprocessor:
@@ -12,37 +15,57 @@ class AttachedPreprocessor:
1215
1316
Attributes:
1417
args: Args used to create the plot
15-
always_attached: The dict mapping the arg and column
18+
attached_transforms: The dict mapping the arg and column
1619
to the style map, dictionary, and new column name, to be used for
1720
AttachedProcessor when dealing with an "always_attached" plot
21+
color_mask: The column to use for the color mask, which controls if a categorical
22+
color is valid at this level. Can also be a boolean, which applies to all values.
1823
"""
1924

2025
def __init__(
2126
self,
2227
args: dict[str, Any],
23-
always_attached: dict[tuple[str, str], tuple[dict[str, str], list[str], str]],
28+
attached_transforms: AttachedTransforms,
29+
color_mask: str | bool,
2430
):
2531
self.args = args
26-
self.always_attached = always_attached
27-
self.prepare_preprocess()
32+
self.attached_transforms = attached_transforms
33+
self.color_mask = color_mask
2834

29-
def prepare_preprocess(self):
35+
def attach_styles(self, table: Table) -> Table:
3036
"""
31-
Create a table with styles attached
37+
Attach the styles to the table
38+
39+
Args:
40+
table: The table to attach the styles to
3241
"""
33-
# create new columns
34-
table = self.args["table"]
35-
for (arg, col), (map, ls, new_col) in self.always_attached.items():
42+
for (by_col, new_col, style_list, style_map) in self.attached_transforms:
3643
manager_col = get_unique_names(table, [f"{new_col}_manager"])[
3744
f"{new_col}_manager"
3845
]
39-
style_manager = StyleManager(map=map, ls=ls)
46+
style_manager = StyleManager(map=style_map, ls=style_list)
4047

4148
table = table.update_view(
4249
[
4350
f"{manager_col}=style_manager",
44-
f"{new_col}={manager_col}.assign_style({col})",
51+
f"{new_col}={manager_col}.assign_style({by_col}, {self.color_mask})",
4552
]
4653
)
4754

48-
self.args["table"] = table
55+
return table
56+
57+
def preprocess_partitioned_tables(
58+
self, tables: list[Table], column: str | None = None
59+
):
60+
"""
61+
Preprocess the attached styles
62+
63+
Args:
64+
tables: The tables to process
65+
column: column: the column used (ignored)
66+
67+
Returns:
68+
A tuple containing (the new table, an update to make to the args)
69+
"""
70+
for table in tables:
71+
yield self.attach_styles(table), {}

0 commit comments

Comments
 (0)