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

New standalone example showing blueprint configuration of some stock #5603

Merged
merged 16 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from 14 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
1 change: 1 addition & 0 deletions crates/re_space_view_time_series/src/space_view_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ It can greatly improve performance (and readability) in such situations as it pr
time_zone_for_timestamps,
)
})
.y_axis_width(3) // in digits
.label_formatter(move |name, value| {
let name = if name.is_empty() { "y" } else { name };
let label = time_type.format(
Expand Down
38 changes: 38 additions & 0 deletions examples/python/blueprint_stocks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<!--[metadata]
title = "Stock Charts"
description = "Uses stock data as an example of how to leverage Rerun blueprints to control the layout and presentation of the viewer."
tags = ["time-series", "blueprint"]
thumbnail = "https://static.rerun.io/blueprint_stocks/8bfe6f16963acdceb2debb9de9a206dc2eb9b280/480w.png"
thumbnail_dimensions = [480, 271]
-->

<picture>
<img src="https://static.rerun.io/blueprint_stocks/8bfe6f16963acdceb2debb9de9a206dc2eb9b280/full.png" alt="">
<source media="(max-width: 480px)" srcset="https://static.rerun.io/blueprint_stocks/8bfe6f16963acdceb2debb9de9a206dc2eb9b280/480w.png">
<source media="(max-width: 768px)" srcset="https://static.rerun.io/blueprint_stocks/8bfe6f16963acdceb2debb9de9a206dc2eb9b280/768w.png">
<source media="(max-width: 1024px)" srcset="https://static.rerun.io/blueprint_stocks/8bfe6f16963acdceb2debb9de9a206dc2eb9b280/1024w.png">
<source media="(max-width: 1200px)" srcset="https://static.rerun.io/blueprint_stocks/8bfe6f16963acdceb2debb9de9a206dc2eb9b280/1200w.png">
</picture>

This example fetches the last 5 days of stock data for a few different stocks.
We show how Rerun blueprints can then be used to present many different views of the same data.

```bash
pip install -r examples/python/blueprint_stocks/requirements.txt
python examples/python/blueprint_stocks/blueprint_main.py
```

The different blueprints can be explored using the `--blueprint` flag. For example:

```
python examples/python/blueprint_stocks/main.py --blueprint=one-stock
```

Available choices are:

- `auto`: Reset the blueprint to the auto layout used by the viewer.
- `one-stock`: Uses a filter to show only a single chart.
- `one-stock-with-info`: Uses a container to layout a chart and its info document
- `one-stock-no-peaks`: Uses a filter to additionally remove some of the data from the chart.
- `compare-two`: Adds data from multiple sources to a single chart.
- `grid`: Shows all the charts in a grid layout.
Comment on lines +31 to +38
Copy link
Member

Choose a reason for hiding this comment

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

I think we have to give them different application ids each: Right now it's quite confusing when you just leave the viewer open and try to execute the script several times with different blueprints :/

Copy link
Member

Choose a reason for hiding this comment

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

discussed on slack, no immediate action to take. We know that application id changing is more wrong. Comes down to figuring out the blueprint lifecycle

215 changes: 215 additions & 0 deletions examples/python/blueprint_stocks/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
#!/usr/bin/env python3
"""
A simple application that fetches stock data from Yahoo Finance and visualizes it using the Rerun SDK.

The main focus of this example is using blueprints to control how the data is displayed in the viewer.
"""
from __future__ import annotations

import argparse
import datetime as dt
from typing import Any

import humanize
import pytz
import rerun as rr
import rerun.blueprint as rrb
import yfinance as yf

################################################################################
# Helper functions to create blueprints
################################################################################


def auto_blueprint() -> rrb.ViewportLike:
"""A blueprint enabling auto space views, which matches the application default."""
return rrb.Viewport(auto_space_views=True, auto_layout=True)


def one_stock(symbol: str) -> rrb.ViewportLike:
"""Create a blueprint showing a single stock."""
return rrb.TimeSeriesView(name=f"{symbol}", origin=f"/stocks/{symbol}")


def one_stock_with_info(symbol: str) -> rrb.ViewportLike:
"""Create a blueprint showing a single stock with its info arranged vertically."""
return rrb.Vertical(
rrb.TextDocumentView(name=f"{symbol}", origin=f"/stocks/{symbol}/info"),
rrb.TimeSeriesView(name=f"{symbol}", origin=f"/stocks/{symbol}"),
row_shares=[1, 4],
)


def compare_two(symbol1: str, symbol2: str, day: Any) -> rrb.ViewportLike:
"""Create a blueprint comparing 2 stocks for a single day."""
return rrb.TimeSeriesView(
name=f"{symbol1} vs {symbol2} ({day})",
contents=[
f"+ /stocks/{symbol1}/{day}",
f"+ /stocks/{symbol2}/{day}",
],
)


def one_stock_no_peaks(symbol: str) -> rrb.ViewportLike:
"""
Create a blueprint showing a single stock without annotated peaks.

This uses an exclusion pattern to hide the peaks.
"""
return rrb.TimeSeriesView(
name=f"{symbol}",
origin=f"/stocks/{symbol}",
contents=[
"+ $origin/**",
"- $origin/peaks/**",
],
)


def stock_grid(symbols: list[str], dates: list[Any]) -> rrb.ViewportLike:
"""Create a grid of stocks and their time series over all days."""
return rrb.Vertical(
contents=[
rrb.Horizontal(
contents=[rrb.TextDocumentView(name=f"{symbol}", origin=f"/stocks/{symbol}/info")]
+ [rrb.TimeSeriesView(name=f"{day}", origin=f"/stocks/{symbol}/{day}") for day in dates],
)
for symbol in symbols
]
)


def hide_panels(viewport: rrb.ViewportLike) -> rrb.BlueprintLike:
"""Wrap a viewport in a blueprint that hides the time and selection panels."""
return rrb.Blueprint(
viewport,
rrb.TimePanel(expanded=False),
rrb.SelectionPanel(expanded=False),
)


################################################################################
# Helper functions for styling
################################################################################

brand_colors = {
"AAPL": 0xA2AAADFF,
"AMZN": 0xFF9900FF,
"GOOGL": 0x34A853FF,
"META": 0x0081FBFF,
"MSFT": 0xF14F21FF,
}


def style_plot(symbol: str) -> rr.SeriesLine:
return rr.SeriesLine(
color=brand_colors[symbol],
name=symbol,
)


def style_peak(symbol: str) -> rr.SeriesPoint:
return rr.SeriesPoint(
color=0xFF0000FF,
name=f"{symbol} (peak)",
marker="Up",
)


################################################################################
# Main script
################################################################################


def main() -> None:
parser = argparse.ArgumentParser(description="Visualize stock data using the Rerun SDK")
parser.add_argument(
"--blueprint",
choices=["auto", "one-stock", "one-stock-with-info", "compare-two", "one-stock-no-peaks", "grid"],
default="grid",
help="Select the blueprint to use",
)
parser.add_argument(
"--show_panels",
action="store_true",
help="Show the time and selection panels",
)

rr.script_add_args(parser)
args = parser.parse_args()

et_timezone = pytz.timezone("America/New_York")
current_date = dt.datetime.now(et_timezone).date()
symbols = ["AAPL", "AMZN", "GOOGL", "META", "MSFT"]
dates = list(filter(lambda x: x.weekday() < 5, [current_date - dt.timedelta(days=i) for i in range(7, 0, -1)]))

blueprint: rrb.BlueprintLike

if args.blueprint == "auto":
blueprint = auto_blueprint()
elif args.blueprint == "one-stock":
blueprint = one_stock("AAPL")
elif args.blueprint == "one-stock-with-info":
blueprint = one_stock_with_info("AMZN")
elif args.blueprint == "one-stock-no-peaks":
blueprint = one_stock_no_peaks("GOOGL")
elif args.blueprint == "compare-two":
blueprint = compare_two("META", "MSFT", dates[-1])
elif args.blueprint == "grid":
blueprint = stock_grid(symbols, dates)
else:
raise ValueError(f"Unknown blueprint: {args.blueprint}")

if not args.show_panels:
blueprint = hide_panels(blueprint)

rr.init("rerun_example_blueprint_stocks", spawn=True, blueprint=blueprint)

# In a future blueprint release, this can move into the blueprint as well
for symbol in symbols:
for day in dates:
rr.log(f"stocks/{symbol}/{day}", style_plot(symbol), timeless=True)
rr.log(f"stocks/{symbol}/peaks/{day}", style_peak(symbol), timeless=True)

for symbol in symbols:
stock = yf.Ticker(symbol)

name = stock.info["shortName"]
industry = stock.info["industry"]
marketCap = humanize.intword(stock.info["marketCap"])
revenue = humanize.intword(stock.info["totalRevenue"])

info_md = (
f"- **Name**: {name}\n"
f"- **Industry**: {industry}\n"
f"- **Market cap**: ${marketCap}\n"
f"- **Total Revenue**: ${revenue}\n"
)

rr.log(
f"stocks/{symbol}/info",
rr.TextDocument(info_md, media_type=rr.MediaType.MARKDOWN),
timeless=True,
)

for day in dates:
open_time = dt.datetime.combine(day, dt.time(9, 30))
close_time = dt.datetime.combine(day, dt.time(16, 00))

hist = stock.history(start=open_time, end=close_time, interval="5m")

hist.index = hist.index - et_timezone.localize(open_time)
peak = hist.High.idxmax()

for row in hist.itertuples():
rr.set_time_seconds("time", row.Index.total_seconds())
rr.log(f"stocks/{symbol}/{day}", rr.Scalar(row.High))
if row.Index == peak:
rr.log(f"stocks/{symbol}/peaks/{day}", rr.Scalar(row.High))

rr.script_teardown(args)


if __name__ == "__main__":
main()
3 changes: 3 additions & 0 deletions examples/python/blueprint_stocks/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
humanize
rerun-sdk
yfinance
3 changes: 2 additions & 1 deletion examples/python/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
-r arkit_scenes/requirements.txt
-r blueprint/requirements.txt
-r blueprint_stocks/requirements.txt
-r clock/requirements.txt
-r controlnet/requirements.txt
-r depth_guided_stable_diffusion/requirements.txt
Expand All @@ -16,8 +17,8 @@
-r live_depth_sensor/requirements.txt
-r llm_embedding_ner/requirements.txt
-r log_file/requirements.txt
-r minimal/requirements.txt
-r minimal_options/requirements.txt
-r minimal/requirements.txt
-r multiprocessing/requirements.txt
-r multithreading/requirements.txt
-r notebook/requirements.txt
Expand Down
2 changes: 2 additions & 0 deletions rerun_py/rerun_sdk/rerun/blueprint/__init__.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 32 additions & 9 deletions rerun_py/rerun_sdk/rerun/blueprint/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ class Container:

def __init__(
self,
*contents: Container | SpaceView,
*args: Container | SpaceView,
contents: Optional[Iterable[Container | SpaceView]] = None,
kind: ContainerKindLike,
column_shares: Optional[ColumnShareArrayLike] = None,
row_shares: Optional[RowShareArrayLike] = None,
Expand All @@ -131,8 +132,11 @@ def __init__(

Parameters
----------
*contents:
All positional arguments are the contents of the container, which may be either other containers or space views.
*args:
All positional arguments are forwarded to the `contents` parameter for convenience.
contents:
The contents of the container. Each item in the iterable must be a `SpaceView` or a `Container`.
This can only be used if no positional arguments are provided.
kind
The kind of the container. This must correspond to a known container kind.
Prefer to use one of the subclasses of `Container` which will populate this for you.
Expand All @@ -150,9 +154,17 @@ def __init__(
The active tab in the container. This is only applicable to `Tabs` containers.

"""

if args and contents is not None:
raise ValueError("Cannot provide both positional and keyword arguments for contents")

if contents is not None:
self.contents = contents
else:
self.contents = args

self.id = uuid.uuid4()
self.kind = kind
self.contents = contents
self.column_shares = column_shares
self.row_shares = row_shares
self.grid_columns = grid_columns
Expand Down Expand Up @@ -214,7 +226,11 @@ class Viewport:
"""

def __init__(
self, root_container: Container, *, auto_layout: bool | None = None, auto_space_views: bool | None = None
self,
root_container: Container | None = None,
*,
auto_layout: bool | None = None,
auto_space_views: bool | None = None,
):
"""
Construct a new viewport.
Expand Down Expand Up @@ -255,11 +271,18 @@ def to_blueprint(self) -> Blueprint:

def _log_to_stream(self, stream: RecordingStream) -> None:
"""Internal method to convert to an archetype and log to the stream."""
self.root_container._log_to_stream(stream)
if self.root_container is not None:
self.root_container._log_to_stream(stream)

root_container_id = self.root_container.id.bytes
space_views = list(self.root_container._iter_space_views())
else:
root_container_id = None
space_views = []

arch = ViewportBlueprint(
space_views=list(self.root_container._iter_space_views()),
root_container=self.root_container.id.bytes,
space_views=space_views,
root_container=root_container_id,
auto_layout=self.auto_layout,
auto_space_views=self.auto_space_views,
)
Expand Down Expand Up @@ -434,7 +457,7 @@ def _log_to_stream(self, stream: RecordingStream) -> None:
self.time_panel._log_to_stream(stream)


BlueprintLike = Union[Blueprint, Viewport, Container, SpaceView]
BlueprintLike = Union[Blueprint, ViewportLike]

"""
A type that can be converted to a blueprint.
Expand Down
Loading
Loading