Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: indicator chart #1088

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 58 additions & 15 deletions plugins/plotly-express/docs/indicator.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ Visualize a single numeric value with an angular gauge by passing `gauge="angula
```python order=indicator_plot,my_table
import deephaven.plot.express as dx
from deephaven import agg as agg

my_table = dx.data.stocks()

# subset data and aggregate for DOG prices
Expand All @@ -98,6 +99,7 @@ Visualize a single numeric value with a bullet gauge by passing `gauge="bullet"`
```python order=indicator_plot,my_table
import deephaven.plot.express as dx
from deephaven import agg as agg

my_table = dx.data.stocks()

# subset data and aggregate for DOG prices
Expand All @@ -113,12 +115,38 @@ Add a prefix and suffix to the numeric value by passing `prefix` and `suffix`.
```python order=indicator_plot,my_table
import deephaven.plot.express as dx
from deephaven import agg as agg

my_table = dx.data.stocks()

# subset data and aggregate for DOG prices
dog_avg = my_table.where("Sym = `DOG`").agg_by([agg.avg(cols="Price"), agg.first(cols="StartingPrice = Price")])

indicator_plot = dx.indicator(dog_avg, value="Price", reference="StartingPrice", prefix="$", suffix=" USD")
```

## Number Format

Format the numbers by passing a format string to the `number_format` argument.
The format follows [the GWT Java NumberFormat syntax](https://www.gwtproject.org/javadoc/latest/com/google/gwt/i18n/client/NumberFormat.html).
The default format is `#,##0.00`, which formats with a comma separator for every 3 digits, 2 decimal places, and a minimum of 1 digit to the left of the decimal point.
If a prefix or suffix is passed within the format string, it will be overridden by the `prefix` and `suffix` arguments.

```python
import deephaven.plot.express as dx
from deephaven import agg as agg

my_table = dx.data.stocks()

# subset data and aggregate for DOG prices
dog_avg = my_table.where("Sym = `DOG`").agg_by([agg.avg(cols="Price")])

indicator_plot = dx.indicator(dog_avg, value="Price", prefix="$", suffix="USD")
# format the number with a dollar sign prefix, USD suffix, and three decimal places
indicator_plot = dx.indicator(dog_avg, value="Price", number_format="$#,##0.000USD")

# prefix overrides the prefix from the number_format
indicator_plot_prefix = dx.indicator(
dog_avg, value="Price", number_format="$#,##0.000USD", prefix="Dollars: "
)
```

### Delta Symbols
Expand All @@ -128,12 +156,19 @@ Modify the symbol before the delta value by passing `increasing_text` and `decre
```python order=indicator_plot,my_table
import deephaven.plot.express as dx
from deephaven import agg as agg

my_table = dx.data.stocks()

# subset data and aggregate for DOG prices
dog_avg = my_table.where("Sym = `DOG`").agg_by([agg.avg(cols="Price")])
dog_agg = my_table.where("Sym = `DOG`").agg_by([agg.avg(cols="Price"), agg.first(cols="StartingPrice = Price")])

indicator_plot = dx.indicator(dog_avg, value="Price", increasing_text="Up: ", decreasing_text="Down: ")
indicator_plot = dx.indicator(
dog_agg,
value="Price",
reference="StartingPrice",
increasing_text="Up: ",
decreasing_text="Down: "
)
```

### Indicator with text
Expand All @@ -143,26 +178,29 @@ Add text to the indicator by passing the text column name to the `text` argument
```python order=indicator_plot,my_table
import deephaven.plot.express as dx
from deephaven import agg as agg

my_table = dx.data.stocks()

# subset data and aggregate prices, keeping the Sym
dog_avg = my_table.where("Sym = `DOG`").agg_by([agg.avg(cols="Price")])
dog_avg = my_table.where("Sym = `DOG`").agg_by([agg.avg(cols="Price")], by="Sym")

indicator_plot = dx.indicator(dog_avg, value="Price", text="Sym")
indicator_plot = dx.indicator(dog_avg, value="Price", by="Sym", text="Sym")
```

### Multiple indicators

Visualize multiple numeric values by passing in a table with multiple rows. By default, a square grid of indicators is created.
Visualize multiple numeric values by passing in a table with multiple rows and the `by` argument. By default, a square grid of indicators is created.

```python order=indicator_plot,my_table
import deephaven.plot.express as dx
from deephaven import agg as agg

my_table = dx.data.stocks()

# aggregate for average prices by Sym
sym_avg = my_table.agg_by([agg.avg(cols="Price")], by="Sym")

indicator_plot = dx.indicator(sym_avg, value="Price")
indicator_plot = dx.indicator(sym_avg, value="Price", by="Sym")
```

### Multiple rows
Expand All @@ -171,12 +209,14 @@ By default, a grid of indicators is created. To create a specific amount of rows

```python order=indicator_plot,my_table
import deephaven.plot.express as dx
from deephaven import agg as agg

my_table = dx.data.stocks()

# aggregate for average prices by Sym
sym_avg = my_table.agg_by([agg.avg(cols="Price")], by="Sym")

indicator_plot = dx.indicator(sym_avg, value="Price", rows=2)
indicator_plot = dx.indicator(sym_avg, value="Price", by="Sym", rows=2)
```

### Multiple columns
Expand All @@ -185,12 +225,14 @@ By default, a grid of indicators is created. To create a specific amount of colu

```python order=indicator_plot,my_table
import deephaven.plot.express as dx
from deephaven import agg as agg

my_table = dx.data.stocks()

# aggregate for average prices by Sym
sym_avg = my_table.agg_by([agg.avg(cols="Price")], by="Sym")

indicator_plot = dx.indicator(sym_avg, value="Price", columns=2)
indicator_plot = dx.indicator(sym_avg, value="Price", by="Sym", cols=2)
```

### Delta colors
Expand All @@ -212,9 +254,9 @@ sym_agg = my_table.agg_by(
indicator_plot = dx.indicator(
sym_agg,
value="Price",
reference="Starting Price",
increasing_color_sequence=["green", "darkgreen"],
decreasing_color_sequence=["red", "darkred"],
reference="StartingPrice",
increasing_color_sequence=["darkgreen", "green"],
decreasing_color_sequence=["darkred", "red"],
)
```

Expand All @@ -230,10 +272,10 @@ from deephaven import agg as agg
my_table = dx.data.stocks()

# subset data and aggregate for DOG prices
sym_agg = my_table.agg_by([agg.avg(cols="Price")])
sym_agg = my_table.where("Sym = `DOG`").agg_by([agg.avg(cols="Price")])

indicator_plot = dx.indicator(
sym_agg, value="Price", gauge_color_sequence=["green", "darkgreen"]
sym_agg, value="Price", gauge="angular", gauge_color_sequence=["darkgreen", "green"]
)
```

Expand All @@ -249,7 +291,7 @@ from deephaven import agg as agg
my_table = dx.data.stocks()

# subset data and aggregate prices, keeping the Sym
sym_agg = my_table.agg_by(
sym_agg = my_table.where("Sym = `DOG`").agg_by(
[
agg.avg(cols="Price"),
agg.first(cols="StartingPrice = Price"),
Expand All @@ -262,6 +304,7 @@ indicator_plot = dx.indicator(
value="Price",
reference="StartingPrice",
by="Sym",
by_vars=("increasing_color", "decreasing_color"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example is kind of confusing. What is the by_vars argument for? Why are we doing a by here at all when we've already filtered the table to one Sym?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is so that the symbol can be specified in the increasing_color_map and decreasing_color_map, although it shouldn't have filtered to one symbol so I'll fix that.

increasing_color_map={"DOG": "darkgreen"},
decreasing_color_map={"DOG": "darkred"},
)
Expand Down
4 changes: 4 additions & 0 deletions plugins/plotly-express/docs/sidebar.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@
"label": "Icicle",
"path": "icicle.md"
},
{
"label": "Indicator",
"path": "indicator.md"
},
{
"label": "Line",
"path": "line.md"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
line_geo,
line_mapbox,
density_heatmap,
indicator,
)

from .data import data_generators
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def _setup_listeners(self) -> None:
for table, node in self._partitioned_tables.values():
listen_func = partial(self._on_update, node)
# if a table is not refreshing, it will never update, so no need to listen
if table.is_refreshing:
if table and table.is_refreshing:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might want to have this fix as a separate PR just so it's clear this is fixed in the release notes (is there a ticket for it? When do we run into this issue?)

handle = listen(table, listen_func)
self._handles.append(handle)
self._liveness_scope.manage(handle)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
"x_start": "base",
"color": "marker/color",
"colors": "marker/colors",
"reference": "delta/reference",
"text_indicator": "title/text",
}

# x_end is not used, the calculations are made in preprocessing step and passed to x
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,10 @@
DeephavenFigureNode,
)
from .generate import generate_figure, update_traces
from .custom_draw import draw_ohlc, draw_candlestick, draw_density_heatmap
from .custom_draw import (
draw_ohlc,
draw_candlestick,
draw_density_heatmap,
draw_indicator,
)
from .RevisionManager import RevisionManager
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@
from plotly.graph_objects import Figure
from plotly.validators.heatmap import ColorscaleValidator

# attach a prefix to the number format so that we can identify it as the GWT Java NumberFormat syntax
# https://www.gwtproject.org/javadoc/latest/com/google/gwt/i18n/client/NumberFormat.html
# this differentiates it from the d3 format syntax, which the user could provide through an unsafe update
# this should be safe as it shouldn't appear naturally in a d3 format string
# https://github.com/d3/d3-format/tree/v1.4.5#d3-format
# but isn't a perfect solution
FORMAT_PREFIX = "DEEPHAVEN_JAVA_FORMAT"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kind of want an = at the end just to denote the end of the "prefix".

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done



def draw_finance(
data_frame: DataFrame,
Expand Down Expand Up @@ -186,3 +194,98 @@ def draw_density_heatmap(
)

return heatmap


def draw_indicator(
data_frame: DataFrame,
value: str,
reference: str | None = None,
number: bool = True,
gauge: str | None = None,
axis: bool = False,
prefix: str | None = None,
suffix: str | None = None,
increasing_text: str | None = None,
decreasing_text: str | None = None,
number_format: str | None = None,
text_indicator: str | None = None,
title: str | None = None,
) -> Figure:
"""Create an indicator chart.

Args:
data_frame: The data frame to draw with
value: The column to use as the value.
reference: The column to use as the reference value.
number: True to show the number, False to hide it.
gauge: Specifies the type of gauge to use.
Set to "angular" for a half-circle gauge and "bullet" for a horizontal gauge.
axis: True to show the axis. Only valid if gauge is set.
prefix: A string to prepend to the number value.
suffix: A string to append to the number value.
increasing_text: The text to display before the delta if the number value
is greater than the reference value.
decreasing_text: The text to display before the delta if the number value
is less than the reference value.
number_format: The format to use for the number.
text_indicator: The column to use as text for the indicator.
title: The title of the chart.

Returns:
The plotly indicator chart.

"""

modes = []
if number:
modes.append("number")
if reference:
modes.append("delta")
if gauge:
modes.append("gauge")
mode = "+".join(modes)

fig = go.Figure(
go.Indicator(
value=data_frame[value][0],
mode=mode,
domain={"x": [0, 1], "y": [0, 1]},
),
layout={
"legend": {"tracegroupgap": 0},
"margin": {"t": 60},
},
)

if reference:
fig.update_traces(delta_reference=data_frame[reference][0])

if text_indicator:
fig.update_traces(title_text=data_frame[text_indicator][0])

if gauge:
fig.update_traces(gauge={"shape": gauge, "axis": {"visible": axis}})

if prefix:
fig.update_traces(delta_prefix=prefix, number_prefix=prefix)

if suffix:
fig.update_traces(delta_suffix=suffix, number_suffix=suffix)

if increasing_text:
fig.update_traces(delta_increasing_symbol=increasing_text)

if decreasing_text:
fig.update_traces(delta_decreasing_symbol=decreasing_text)

if number_format:
# Plotly expects d3 format strings so these will be converted on the client.
fig.update_traces(
delta_valueformat=FORMAT_PREFIX + number_format,
number_valueformat=FORMAT_PREFIX + number_format,
)

if title:
fig.update_layout(title=title)

return fig
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@
"lat",
"lon",
"locations",
"value",
"reference",
"text_indicator",
}
DATA_ARGS.update(DATA_LIST_ARGS)

Expand All @@ -82,6 +85,9 @@
"width_sequence": "line_width",
"increasing_color_sequence": "increasing_line_color",
"decreasing_color_sequence": "decreasing_line_color",
"gauge_color_sequence": "gauge_bar_color",
"increasing_color_sequence_indicator": "delta_increasing_color",
"decreasing_color_sequence_indicator": "delta_decreasing_color",
"size_sequence": "marker_size",
"mode": "mode",
}
Expand Down Expand Up @@ -704,6 +710,8 @@ def get_list_var_info(data_cols: Mapping[str, str | list[str]]) -> set[str]:
# for them
types.add("finance" if data_cols.get("x_finance", False) else None)

types.add("indicator" if data_cols.get("value", False) else None)

"""for var, cols in data_cols.items():
# there should only be at most one data list (with the filtered
# exception of finance charts) so the first one encountered is the var
Expand Down Expand Up @@ -798,8 +806,8 @@ def hover_text_generator(
Yields:
A dictionary update
"""
if isinstance(types, set) and "finance" in types:
# finance has no hover text currently (besides the default)
if isinstance(types, set) and ("finance" in types or "indicator" in types):
# finance and indicator have no custom hover text
while True:
yield {}

Expand Down Expand Up @@ -1035,7 +1043,6 @@ def generate_figure(
data_frame = construct_min_dataframe(
table, data_cols=merge_cols(list(data_cols.values()))
)

px_fig = draw(data_frame=data_frame, **filtered_call_args)

data_mapping, hover_mapping = create_data_mapping(
Expand Down
Loading
Loading