pfund-plot 0.0.1.dev2__py3-none-any.whl → 0.0.1.dev3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. pfund_plot/__init__.py +35 -1
  2. pfund_plot/cli/__init__.py +4 -0
  3. pfund_plot/cli/commands/config.py +81 -0
  4. pfund_plot/cli/commands/plot.py +7 -0
  5. pfund_plot/cli/main.py +20 -0
  6. pfund_plot/composites/composite.py +13 -0
  7. pfund_plot/config_handler.py +136 -0
  8. pfund_plot/const/enums/__init__.py +6 -0
  9. pfund_plot/const/enums/dashboard_type.py +8 -0
  10. pfund_plot/const/enums/data_type.py +6 -0
  11. pfund_plot/const/enums/dataframe_backend.py +6 -0
  12. pfund_plot/const/enums/display_mode.py +7 -0
  13. pfund_plot/const/enums/notebook_type.py +7 -0
  14. pfund_plot/const/enums/plotting_backend.py +6 -0
  15. pfund_plot/const/paths.py +15 -0
  16. pfund_plot/exports/__init__.py +0 -0
  17. pfund_plot/layout.py +0 -0
  18. pfund_plot/main.py +18 -0
  19. pfund_plot/mosaic.py +0 -0
  20. pfund_plot/plots/candlestick.py +234 -0
  21. pfund_plot/plots/dataframe.py +159 -0
  22. pfund_plot/plots/line.py +2 -0
  23. pfund_plot/plots/orderbook.py +47 -0
  24. pfund_plot/renderer.py +143 -0
  25. pfund_plot/templates/__init__.py +2 -0
  26. pfund_plot/templates/dashboard.py +13 -0
  27. pfund_plot/templates/notebook.py +15 -0
  28. pfund_plot/templates/spreadsheet.py +1 -0
  29. pfund_plot/templates/template.py +10 -0
  30. pfund_plot/types/core.py +11 -0
  31. pfund_plot/types/literals.py +6 -0
  32. pfund_plot/utils/utils.py +42 -0
  33. pfund_plot/utils/validate.py +50 -0
  34. pfund_plot-0.0.1.dev3.dist-info/METADATA +101 -0
  35. pfund_plot-0.0.1.dev3.dist-info/RECORD +38 -0
  36. pfund_plot-0.0.1.dev3.dist-info/entry_points.txt +5 -0
  37. pfund_plot-0.0.1.dev2.dist-info/METADATA +0 -43
  38. pfund_plot-0.0.1.dev2.dist-info/RECORD +0 -5
  39. {pfund_plot-0.0.1.dev2.dist-info → pfund_plot-0.0.1.dev3.dist-info}/LICENSE +0 -0
  40. {pfund_plot-0.0.1.dev2.dist-info → pfund_plot-0.0.1.dev3.dist-info}/WHEEL +0 -0
pfund_plot/__init__.py CHANGED
@@ -1 +1,35 @@
1
- import pfund_plot
1
+ from importlib.metadata import version
2
+
3
+ import hvplot
4
+ import panel as pn
5
+
6
+ from pfund_plot.config_handler import get_config, configure
7
+ from pfund_plot.plots.dataframe import (
8
+ dataframe_plot as dataframe,
9
+ dataframe_plot as df,
10
+ )
11
+ from pfund_plot.plots.candlestick import (
12
+ candlestick_plot as candlestick,
13
+ candlestick_plot as ohlc,
14
+ candlestick_plot as kline,
15
+ )
16
+
17
+
18
+ hvplot.extension('bokeh', 'plotly')
19
+ pn.extension('tabulator', 'perspective')
20
+ # used to throttle updates in panel plots
21
+ # NOTE: without it, e.g. dragging a slider will cause the plot to update rapidly and lead to an error
22
+ pn.config.throttled = True
23
+
24
+
25
+ __version__ = version("pfund_plot")
26
+ __all__ = (
27
+ "__version__",
28
+ "get_config",
29
+ "configure",
30
+ "candlestick",
31
+ "ohlc",
32
+ "kline",
33
+ "dataframe",
34
+ "df",
35
+ )
@@ -0,0 +1,4 @@
1
+ from pfund_plot.cli.main import pfund_plot_group
2
+
3
+
4
+ __all__ = ["pfund_plot_group"]
@@ -0,0 +1,81 @@
1
+ import click
2
+
3
+ from pfeed.const.enums import DataTool
4
+ from pfund_plot.const.paths import PROJ_NAME
5
+
6
+
7
+ @click.group()
8
+ def config():
9
+ """Manage configuration settings."""
10
+ pass
11
+
12
+
13
+ @config.command()
14
+ @click.pass_context
15
+ def list(ctx):
16
+ """List all available options."""
17
+ from pprint import pformat
18
+ from dataclasses import asdict
19
+ config_dict = asdict(ctx.obj['config'])
20
+ content = click.style(pformat(config_dict), fg='green')
21
+ click.echo(f"{PROJ_NAME} config:\n{content}")
22
+
23
+
24
+ @config.command()
25
+ @click.pass_context
26
+ def reset(ctx):
27
+ """Reset the configuration to defaults."""
28
+ ctx.obj['config'].reset()
29
+ click.echo(f"{PROJ_NAME} config reset successfully.")
30
+
31
+
32
+ @config.command()
33
+ @click.option('--data-tool', '--dt', type=click.Choice(DataTool, case_sensitive=False), help='Set the data tool')
34
+ @click.option('--max-points', '--mp', type=int, help='Set the maximum number of points to display in the plot')
35
+ @click.option('--data-path', '--dp', type=click.Path(resolve_path=True), help='Set the data path')
36
+ @click.option('--cache-path', '--cp', type=click.Path(resolve_path=True), help='Set the cache path')
37
+ def set(**kwargs):
38
+ """Configures pfund_plot settings."""
39
+ from pfund_plot.config_handler import configure
40
+ provided_options = {k: v for k, v in kwargs.items() if v is not None}
41
+ if not provided_options:
42
+ raise click.UsageError(f"No options provided. Please run '{PROJ_NAME} config set --help' to see all available options.")
43
+ else:
44
+ configure(write=True, **kwargs)
45
+ click.echo(f"{PROJ_NAME} config updated successfully.")
46
+
47
+
48
+ @config.command()
49
+ @click.option('--config-file', '-c', is_flag=True, help=f'Open the {PROJ_NAME}_config.yml file')
50
+ @click.option('--default-editor', '-E', is_flag=True, help='Use default editor')
51
+ def open(config_file, default_editor):
52
+ """Opens config files, e.g. pfund_plot_config.yml."""
53
+ from pfund_plot.const.paths import CONFIG_FILE_PATH
54
+
55
+ if sum([config_file]) > 1:
56
+ click.echo('Please specify only one file to open')
57
+ return
58
+ else:
59
+ if config_file:
60
+ file_path = CONFIG_FILE_PATH
61
+ else:
62
+ click.echo(f'Please specify a file to open, run "{PROJ_NAME} config open --help" for more info')
63
+ return
64
+
65
+ if default_editor:
66
+ click.edit(filename=file_path)
67
+ else:
68
+ open_with_vscode(file_path)
69
+
70
+
71
+ def open_with_vscode(file_path):
72
+ import subprocess
73
+ try:
74
+ subprocess.run(["code", str(file_path)], check=True)
75
+ click.echo(f"Opened {file_path} with VS Code")
76
+ except subprocess.CalledProcessError:
77
+ click.echo("Failed to open with VS Code. Falling back to default editor.")
78
+ click.edit(filename=file_path)
79
+ except FileNotFoundError:
80
+ click.echo("VS Code command 'code' not found. Falling back to default editor.")
81
+ click.edit(filename=file_path)
@@ -0,0 +1,7 @@
1
+ import click
2
+
3
+
4
+ @click.command()
5
+ def plot():
6
+ # TODO: add plot function
7
+ print('plotting!!!')
pfund_plot/cli/main.py ADDED
@@ -0,0 +1,20 @@
1
+ import click
2
+ from trogon import tui
3
+
4
+ from pfund_plot.config_handler import get_config
5
+ from pfund_plot.cli.commands.plot import plot
6
+ from pfund_plot.cli.commands.config import config
7
+
8
+
9
+ @tui(command='tui', help="Open terminal UI")
10
+ @click.group(context_settings={"help_option_names": ["-h", "--help"]})
11
+ @click.pass_context
12
+ @click.version_option()
13
+ def pfund_plot_group(ctx):
14
+ """PFundPlot's CLI"""
15
+ ctx.ensure_object(dict)
16
+ ctx.obj['config'] = get_config(verbose=False)
17
+
18
+
19
+ pfund_plot_group.add_command(plot)
20
+ pfund_plot_group.add_command(config)
@@ -0,0 +1,13 @@
1
+ # TEMP
2
+ # composites are plots combined together on the same canvas
3
+ # it can be used to perform comparison analysis, create a graph that has the price line, equity curve etc.
4
+
5
+ # use plotly for composite plots
6
+ class CompositePlot:
7
+ def __init__(self, plots):
8
+ self.plots = plots
9
+
10
+ def render(self):
11
+ # Implement logic to combine plots
12
+ # This is non-trivial and may require standardizing on a single backend
13
+ pass
@@ -0,0 +1,136 @@
1
+ import os
2
+ import shutil
3
+ from dataclasses import dataclass, asdict
4
+
5
+ import yaml
6
+
7
+ from pfund_plot.const.paths import (
8
+ PROJ_NAME,
9
+ DATA_PATH,
10
+ CACHE_PATH,
11
+ CONFIG_PATH,
12
+ CONFIG_FILE_PATH
13
+ )
14
+
15
+
16
+ __all__ = [
17
+ 'get_config',
18
+ 'configure',
19
+ ]
20
+
21
+
22
+ @dataclass
23
+ class ConfigHandler:
24
+ data_path: str = str(DATA_PATH)
25
+ cache_path: str = str(CACHE_PATH)
26
+
27
+ _instance = None
28
+ _verbose = False
29
+
30
+ @classmethod
31
+ def get_instance(cls):
32
+ if cls._instance is None:
33
+ cls._instance = cls.load()
34
+ return cls._instance
35
+
36
+ @classmethod
37
+ def set_verbose(cls, verbose: bool):
38
+ cls._verbose = verbose
39
+
40
+ @classmethod
41
+ def load(cls):
42
+ '''Loads user's config file and returns a ConfigHandler object'''
43
+ CONFIG_FILE_PATH.parent.mkdir(parents=True, exist_ok=True)
44
+ # Create default config from dataclass fields
45
+ default_config = {
46
+ field.name: field.default
47
+ for field in cls.__dataclass_fields__.values()
48
+ if not field.name.startswith('_') # Skip private fields
49
+ }
50
+ needs_update = False
51
+ if CONFIG_FILE_PATH.is_file():
52
+ with open(CONFIG_FILE_PATH, 'r') as f:
53
+ saved_config = yaml.safe_load(f) or {}
54
+ if cls._verbose:
55
+ print(f"{PROJ_NAME} config loaded from {CONFIG_FILE_PATH}.")
56
+ # Check for new or removed fields
57
+ new_fields = set(default_config.keys()) - set(saved_config.keys())
58
+ removed_fields = set(saved_config.keys()) - set(default_config.keys())
59
+ needs_update = bool(new_fields or removed_fields)
60
+
61
+ if cls._verbose and needs_update:
62
+ if new_fields:
63
+ print(f"New config fields detected: {new_fields}")
64
+ if removed_fields:
65
+ print(f"Removed config fields detected: {removed_fields}")
66
+
67
+ # Filter out removed fields and merge with defaults
68
+ saved_config = {k: v for k, v in saved_config.items() if k in default_config}
69
+ config = {**default_config, **saved_config}
70
+ else:
71
+ config = default_config
72
+ needs_update = True
73
+ config_handler = cls(**config)
74
+ if needs_update:
75
+ config_handler.dump()
76
+ return config_handler
77
+
78
+ @classmethod
79
+ def reset(cls):
80
+ '''Resets the config by deleting the user config directory and reloading the config'''
81
+ shutil.rmtree(CONFIG_PATH)
82
+ if cls._verbose:
83
+ print(f"{PROJ_NAME} config successfully reset.")
84
+ return cls.load()
85
+
86
+ def dump(self):
87
+ with open(CONFIG_FILE_PATH, 'w') as f:
88
+ yaml.dump(asdict(self), f, default_flow_style=False)
89
+ if self._verbose:
90
+ print(f"{PROJ_NAME} config saved to {CONFIG_FILE_PATH}.")
91
+
92
+ def __post_init__(self):
93
+ self._initialize_configs()
94
+
95
+ def _initialize_configs(self):
96
+ for path in [self.data_path, self.cache_path]:
97
+ if not os.path.exists(path):
98
+ os.makedirs(path)
99
+ if self._verbose:
100
+ print(f'{PROJ_NAME} created {path}')
101
+
102
+
103
+ def configure(
104
+ data_path: str | None = None,
105
+ cache_path: str | None = None,
106
+ verbose: bool = False,
107
+ write: bool = False,
108
+ ):
109
+ '''Configures the global config object.
110
+ It will override the existing config values from the existing config file or the default values.
111
+ Args:
112
+ write: If True, the config will be saved to the config file.
113
+ '''
114
+ NON_CONFIG_KEYS = ['verbose', 'write']
115
+ config_updates = locals()
116
+ for k in NON_CONFIG_KEYS:
117
+ config_updates.pop(k)
118
+ config_updates.pop('NON_CONFIG_KEYS')
119
+
120
+ config = get_config(verbose=verbose)
121
+
122
+ # Apply updates for non-None values
123
+ for k, v in config_updates.items():
124
+ if v is not None:
125
+ setattr(config, k, v)
126
+
127
+ if write:
128
+ config.dump()
129
+
130
+ config._initialize_configs()
131
+ return config
132
+
133
+
134
+ def get_config(verbose: bool = False) -> ConfigHandler:
135
+ ConfigHandler.set_verbose(verbose)
136
+ return ConfigHandler.get_instance()
@@ -0,0 +1,6 @@
1
+ from pfund_plot.const.enums.notebook_type import NotebookType
2
+ from pfund_plot.const.enums.dashboard_type import DashboardType
3
+ from pfund_plot.const.enums.plotting_backend import PlottingBackend
4
+ from pfund_plot.const.enums.display_mode import DisplayMode
5
+ from pfund_plot.const.enums.data_type import DataType
6
+ from pfund_plot.const.enums.dataframe_backend import DataFrameBackend
@@ -0,0 +1,8 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class DashboardType(StrEnum):
5
+ DASH = 'DASH'
6
+ STREAMLIT = 'STREAMLIT'
7
+ GRADIO = 'GRADIO'
8
+ TAIPY = 'TAIPY'
@@ -0,0 +1,6 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class DataType(StrEnum):
5
+ dataframe = 'dataframe'
6
+ datafeed = 'datafeed' # pfeed's feed object
@@ -0,0 +1,6 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class DataFrameBackend(StrEnum):
5
+ tabulator = 'tabulator'
6
+ perspective = 'perspective'
@@ -0,0 +1,7 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class DisplayMode(StrEnum):
5
+ notebook = "notebook"
6
+ browser = "browser"
7
+ desktop = "desktop"
@@ -0,0 +1,7 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class NotebookType(StrEnum):
5
+ jupyter = 'jupyter'
6
+ marimo = 'marimo'
7
+ vscode = 'vscode'
@@ -0,0 +1,6 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class PlottingBackend(StrEnum):
5
+ bokeh = 'bokeh'
6
+ plotly = 'plotly'
@@ -0,0 +1,15 @@
1
+ from pathlib import Path
2
+ from platformdirs import user_data_dir, user_config_dir, user_cache_dir
3
+
4
+
5
+ # project paths
6
+ PROJ_NAME = Path(__file__).resolve().parents[1].name
7
+ MAIN_PATH = Path(__file__).resolve().parents[2]
8
+ PROJ_PATH = MAIN_PATH / PROJ_NAME
9
+
10
+
11
+ # user paths
12
+ DATA_PATH = Path(user_data_dir()) / PROJ_NAME
13
+ CACHE_PATH = Path(user_cache_dir()) / PROJ_NAME
14
+ CONFIG_PATH = Path(user_config_dir()) / PROJ_NAME / 'config'
15
+ CONFIG_FILE_PATH = CONFIG_PATH / f'{PROJ_NAME}_config.yml'
File without changes
pfund_plot/layout.py ADDED
File without changes
pfund_plot/main.py ADDED
@@ -0,0 +1,18 @@
1
+ import atexit
2
+
3
+ from pfund_plot.cli import pfund_plot_group
4
+
5
+
6
+ def exit_cli():
7
+ """Application Exitpoint."""
8
+ print("Cleanup actions here...")
9
+
10
+
11
+ def run_cli() -> None:
12
+ """Application Entrypoint."""
13
+ # atexit.register(exit_cli)
14
+ pfund_plot_group(obj={})
15
+
16
+
17
+ if __name__ == '__main__':
18
+ run_cli()
pfund_plot/mosaic.py ADDED
File without changes
@@ -0,0 +1,234 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING
3
+ if TYPE_CHECKING:
4
+ from narwhals.typing import IntoFrameT, FrameT
5
+ from pfeed.types.core import tDataFrame
6
+ from pfeed.feeds.base_feed import BaseFeed
7
+ from pfund_plot.types.literals import tDISPLAY_MODE
8
+ from pfund_plot.types.core import tOutput
9
+ from holoviews.core.overlay import Overlay
10
+ from panel.layout import Panel
11
+
12
+ import panel as pn
13
+ import narwhals as nw
14
+ from bokeh.models import HoverTool, CrosshairTool
15
+
16
+ from pfund_plot.const.enums import DisplayMode, PlottingBackend, DataType
17
+ from pfund_plot.utils.validate import validate_data_type
18
+ from pfund_plot.renderer import render
19
+
20
+
21
+ __all__ = ['candlestick_plot']
22
+
23
+
24
+ REQUIRED_COLS = ['ts', 'open', 'high', 'low', 'close', 'volume']
25
+ DEFAULT_STYLE = {
26
+ 'title': 'Candlestick Chart',
27
+ 'ylabel': 'price',
28
+ 'xlabel': 'time',
29
+ }
30
+ # needs a default value for "responsive"=True to work properly in notebook environment
31
+ DEFAULT_HEIGHT_FOR_NOTEBOOK = 280
32
+
33
+
34
+ def _validate_df(df: IntoFrameT) -> FrameT:
35
+ import datetime
36
+ df: FrameT = nw.from_native(df)
37
+ if isinstance(df, nw.LazyFrame):
38
+ df = df.collect()
39
+ # convert all columns to lowercase
40
+ df = df.rename({col: col.lower() for col in df.columns})
41
+ # rename 'date' to 'ts'
42
+ if 'date' in df.columns and 'ts' not in df.columns:
43
+ df = df.rename({'date': 'ts'})
44
+ missing_cols = [col for col in REQUIRED_COLS if col not in df.columns]
45
+ if missing_cols:
46
+ raise ValueError(f"Missing required columns: {missing_cols}")
47
+ ts_value = df.select('ts').row(0)[0]
48
+ # convert ts to datetime if not already
49
+ if not isinstance(ts_value, datetime.datetime):
50
+ # REVIEW: this might mess up the datetime format
51
+ df = df.with_columns(
52
+ nw.col('ts').str.to_datetime(format=None),
53
+ )
54
+ return df
55
+
56
+
57
+ def _get_style(df: FrameT, display_mode: DisplayMode, height: int | None, width: int | None) -> dict:
58
+ style = DEFAULT_STYLE.copy()
59
+ if height is not None:
60
+ style['height'] = height
61
+ else:
62
+ if display_mode == DisplayMode.notebook:
63
+ style['height'] = DEFAULT_HEIGHT_FOR_NOTEBOOK
64
+ if width is not None:
65
+ style['width'] = width
66
+ product_or_symbol = None
67
+ if 'symbol' in df.columns:
68
+ product_or_symbol = df.select('symbol').row(0)[0]
69
+ elif 'product' in df.columns:
70
+ product_or_symbol = df.select('product').row(0)[0]
71
+ if product_or_symbol:
72
+ style['title'] = f"{product_or_symbol} {style['title']}"
73
+ return style
74
+
75
+
76
+ def _create_hover_tool(df: FrameT) -> HoverTool:
77
+ from pfund_plot.utils.utils import is_daily_data
78
+ ts_format = '%Y-%m-%d' if is_daily_data(df) else '%Y-%m-%d %H:%M:%S'
79
+ return HoverTool(
80
+ tooltips=[
81
+ ('ts', f'@ts{{{ts_format}}}'),
82
+ ('open', '@open'),
83
+ ('high', '@high'),
84
+ ('low', '@low'),
85
+ ('close', '@close'),
86
+ ('volume', '@volume'),
87
+ ],
88
+ formatters={'@ts': 'datetime'},
89
+ mode='vline',
90
+ )
91
+
92
+
93
+ def candlestick_plot(
94
+ data: tDataFrame | BaseFeed,
95
+ streaming: bool = False,
96
+ display_mode: tDISPLAY_MODE = "notebook",
97
+ num_data: int = 100,
98
+ raw_figure: bool = False,
99
+ slider_step: int = 100,
100
+ show_volume: bool = True,
101
+ streaming_freq: int = 1000, # in milliseconds
102
+ # styling
103
+ up_color: str = 'green',
104
+ down_color: str = 'red',
105
+ bg_color: str = 'white',
106
+ height: int | None = None,
107
+ width: int | None = None,
108
+ grid: bool = True,
109
+ ) -> tOutput:
110
+ '''
111
+ Args:
112
+ data: the data to plot, either a dataframe or pfeed's feed object
113
+ streaming: if True, the plot will be updated in real-time as new data is received
114
+ display_mode: where to display the plot, either "notebook", "browser", or "desktop"
115
+ streaming_freq: the update frequency of the streaming data in milliseconds
116
+ raw_figure: if True, returns the raw figure object (e.g. bokeh.plotting.figure or plotly.graph_objects.Figure)
117
+ if False, returns the holoviews.core.overlay.Overlay object
118
+ num_data: the initial number of data points to display.
119
+ This can be changed by a slider in the plot.
120
+ up_color: the color of the up candle, hex code is supported
121
+ down_color: the color of the down candle, hex code is supported
122
+ bg_color: the background color of the plot, hex code is supported
123
+ height: the height of the plot
124
+ width: the width of the plot
125
+ '''
126
+ # TODO: add volume plot when show_volume is True
127
+ # TODO: add range selector
128
+ # TODO: date input
129
+ # TODO: using tick data to update the current candlestick
130
+
131
+
132
+ display_mode, plotting_backend = DisplayMode[display_mode.lower()], PlottingBackend.bokeh
133
+ data_type: DataType = validate_data_type(data, streaming, import_hvplot=True)
134
+ if data_type == DataType.datafeed:
135
+ # TODO: get streaming data in the format of dataframe, and then call _validate_df
136
+ # df = data.get_realtime_data(...)
137
+ pass
138
+ else:
139
+ df = data
140
+ df: FrameT = _validate_df(df)
141
+
142
+
143
+ # Define reactive values
144
+ max_num_data = pn.rx(df.shape[0])
145
+
146
+ # Main Component: candlestick plot
147
+ def _create_plot(_df: FrameT, _num_data: int):
148
+ plot_df: tDataFrame = _df.tail(_num_data).to_native()
149
+ return (
150
+ plot_df
151
+ .hvplot
152
+ .ohlc(
153
+ 'ts', ['open', 'low', 'high', 'close'],
154
+ hover_cols=REQUIRED_COLS,
155
+ tools=[
156
+ _create_hover_tool(_df),
157
+ CrosshairTool(dimensions='height', line_color='gray', line_alpha=0.3)
158
+ ],
159
+ grid=grid,
160
+ pos_color=up_color,
161
+ neg_color=down_color,
162
+ responsive=True,
163
+ bgcolor=bg_color,
164
+ )
165
+ .opts(**_get_style(_df, display_mode, height, width))
166
+ )
167
+
168
+ # Side Components 1: data points slider
169
+ points_slider = pn.widgets.IntSlider(
170
+ name='Number of Most Recent Data Points',
171
+ value=min(num_data, max_num_data.rx.value),
172
+ start=num_data,
173
+ end=max_num_data,
174
+ step=slider_step,
175
+ )
176
+
177
+ # Side Components 2: show all data button
178
+ show_all_data_button = pn.widgets.Button(
179
+ name='Show All',
180
+ button_type='primary',
181
+ )
182
+ def max_out_slider(event):
183
+ points_slider.value = max_num_data.rx.value
184
+ show_all_data_button.on_click(max_out_slider)
185
+
186
+
187
+ periodic_callback = None
188
+ if raw_figure:
189
+ fig: Overlay = _create_plot(df, _num_data=num_data)
190
+ else:
191
+ if not streaming:
192
+ plot_pane = pn.pane.HoloViews(
193
+ pn.bind(_create_plot, _df=df, _num_data=points_slider)
194
+ )
195
+ else:
196
+ # NOTE: do NOT bind the plot to the slider, otherwise the change of slider value and the periodic callback
197
+ # will BOTH trigger the plot update, causing an error, probably a race condition
198
+ plot_pane = pn.pane.HoloViews(_create_plot(df, points_slider.value))
199
+
200
+ def _update_plot():
201
+ # FIXME
202
+ # TEMP: fake streaming data
203
+ import pandas as pd
204
+ nonlocal df
205
+ pandas_df = df.to_native()
206
+ last_ts = pandas_df['ts'].iloc[-1]
207
+ new_ts = last_ts + pd.Timedelta(days=1)
208
+ new_row = pd.DataFrame({
209
+ 'date': [new_ts.date()],
210
+ 'ts': [new_ts],
211
+ 'symbol': ['AAPL'],
212
+ 'product': ['AAPL_USD_STK'],
213
+ 'open': [pandas_df['open'].iloc[-1] * 1.01], # 1% higher than last price
214
+ 'high': [pandas_df['high'].iloc[-1] * 1.02],
215
+ 'low': [pandas_df['low'].iloc[-1] * 0.99],
216
+ 'close': [pandas_df['close'].iloc[-1] * 1.015],
217
+ 'volume': [int(pandas_df['volume'].iloc[-1] * 0.8)]
218
+ })
219
+ df2 = pd.concat([pandas_df, new_row], ignore_index=True)
220
+ df = nw.from_native(df2)
221
+
222
+ # this will also update the slider's end value since it's a reactive object
223
+ max_num_data.rx.value = df.shape[0]
224
+
225
+ plot_pane.object = _create_plot(df, points_slider.value)
226
+
227
+ periodic_callback = pn.state.add_periodic_callback(_update_plot, period=streaming_freq, start=False) # period in milliseconds
228
+ fig: Panel = pn.Column(
229
+ plot_pane,
230
+ pn.Row(points_slider, show_all_data_button, align='center'),
231
+ sizing_mode='stretch_both',
232
+ name=DEFAULT_STYLE['title'],
233
+ )
234
+ return render(fig, display_mode, raw_figure=raw_figure, plotting_backend=plotting_backend, periodic_callback=periodic_callback)