pfund-plot 0.0.1__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 (57) hide show
  1. pfund_plot/__init__.py +183 -0
  2. pfund_plot/__main__.py +9 -0
  3. pfund_plot/cli/__init__.py +3 -0
  4. pfund_plot/cli/commands/gallery/__init__.py +15 -0
  5. pfund_plot/cli/commands/gallery/gallery_marimo.py +462 -0
  6. pfund_plot/cli/commands/serve.py +21 -0
  7. pfund_plot/cli/main.py +20 -0
  8. pfund_plot/config.py +109 -0
  9. pfund_plot/enums/__init__.py +16 -0
  10. pfund_plot/enums/dataframe_backend.py +6 -0
  11. pfund_plot/enums/display_mode.py +7 -0
  12. pfund_plot/enums/panel_design.py +8 -0
  13. pfund_plot/enums/panel_theme.py +6 -0
  14. pfund_plot/enums/plotting_backend.py +12 -0
  15. pfund_plot/js_tap/components/candlestick.js +9566 -0
  16. pfund_plot/mixins/streaming_market_feed_mixin.py +162 -0
  17. pfund_plot/plots/altair.py +32 -0
  18. pfund_plot/plots/area/__init__.py +82 -0
  19. pfund_plot/plots/area/bokeh.py +151 -0
  20. pfund_plot/plots/bar/__init__.py +80 -0
  21. pfund_plot/plots/bar/bokeh.py +128 -0
  22. pfund_plot/plots/bokeh.py +32 -0
  23. pfund_plot/plots/candlestick/__init__.py +77 -0
  24. pfund_plot/plots/candlestick/bokeh.py +124 -0
  25. pfund_plot/plots/candlestick/svelte.py +161 -0
  26. pfund_plot/plots/holoviews.py +32 -0
  27. pfund_plot/plots/label/__init__.py +43 -0
  28. pfund_plot/plots/label/bokeh.py +89 -0
  29. pfund_plot/plots/layout/__init__.py +98 -0
  30. pfund_plot/plots/layout/layout.py +116 -0
  31. pfund_plot/plots/layout/panel.py +51 -0
  32. pfund_plot/plots/layout/tabs/__init__.py +36 -0
  33. pfund_plot/plots/layout/tabs/panel.py +51 -0
  34. pfund_plot/plots/lazy.py +408 -0
  35. pfund_plot/plots/line/__init__.py +37 -0
  36. pfund_plot/plots/line/bokeh.py +137 -0
  37. pfund_plot/plots/matplotlib.py +32 -0
  38. pfund_plot/plots/plot.py +1131 -0
  39. pfund_plot/plots/plotly.py +32 -0
  40. pfund_plot/plots/scatter/__init__.py +62 -0
  41. pfund_plot/plots/scatter/bokeh.py +158 -0
  42. pfund_plot/plots/scatter/marker.py +107 -0
  43. pfund_plot/plots/ta.py +6 -0
  44. pfund_plot/renderers/base.py +84 -0
  45. pfund_plot/renderers/browser.py +28 -0
  46. pfund_plot/renderers/desktop.py +109 -0
  47. pfund_plot/renderers/notebook.py +92 -0
  48. pfund_plot/typing.py +29 -0
  49. pfund_plot/utils/__init__.py +176 -0
  50. pfund_plot/utils/bokeh.py +177 -0
  51. pfund_plot/widgets/base.py +76 -0
  52. pfund_plot/widgets/datetime_widget.py +221 -0
  53. pfund_plot/widgets/ticker_widget.py +82 -0
  54. pfund_plot-0.0.1.dist-info/METADATA +148 -0
  55. pfund_plot-0.0.1.dist-info/RECORD +57 -0
  56. pfund_plot-0.0.1.dist-info/WHEEL +4 -0
  57. pfund_plot-0.0.1.dist-info/entry_points.txt +6 -0
@@ -0,0 +1,162 @@
1
+ # pyright: reportArgumentType=false, reportOptionalMemberAccess=false, reportOptionalSubscript=false, reportCallIssue=false, reportUnknownMemberType=false, reportUnknownVariableType=false, reportUnknownArgumentType=false, reportAttributeAccessIssue=false
2
+ from __future__ import annotations
3
+
4
+ from typing import TYPE_CHECKING, Any, cast
5
+
6
+ if TYPE_CHECKING:
7
+ from pfeed.requests.market_feed_stream_request import MarketFeedStreamRequest
8
+ from pfeed.streaming import BarMessage, TickMessage
9
+ from pfeed.streaming.market_data_message import MarketDataMessage
10
+ from pfund.datas.resolution import Resolution
11
+
12
+ from pfund_plot.plots.plot import MessageKey
13
+
14
+ import narwhals as nw
15
+ from pfund_kit.style import RichColor, TextStyle, cprint
16
+
17
+
18
+ class StreamingMarketFeedMixin:
19
+ def _is_streaming_ready(self) -> bool:
20
+ """Return True if all streaming dataframes have at least 2 rows (needed for e.g. ohlc to compute candle width)."""
21
+ if not self._streaming_dfs:
22
+ return False
23
+ return all(df.shape[0] >= 2 for df in self._streaming_dfs.values())
24
+
25
+ def _start_streaming(self):
26
+ from pfund_plot.utils import import_hvplot_df_module
27
+
28
+ # streaming always uses polars internally (see _create_streaming_row)
29
+ import_hvplot_df_module("polars")
30
+
31
+ requests = cast("list[MarketFeedStreamRequest]", self._feed._requests)
32
+ for request in requests:
33
+ resolution = cast("Resolution", request.target_resolution)
34
+ if resolution.is_bar():
35
+ if self._y is not None:
36
+ assert self._y in ["open", "high", "low", "close", "volume"], (
37
+ "y must be 'open', 'high', 'low', 'close', or 'volume' when streaming bar data"
38
+ )
39
+ elif resolution.is_tick():
40
+ if self._y is not None:
41
+ assert self._y in ["price", "volume"], (
42
+ "y must be 'price' or 'volume' when streaming tick data"
43
+ )
44
+ else:
45
+ raise ValueError(f"Unsupported resolution: {resolution}")
46
+
47
+ super()._start_streaming()
48
+
49
+ def _create_streaming_row(self, msg: MarketDataMessage) -> nw.DataFrame[Any]:
50
+ import polars as pl
51
+
52
+ if msg.is_tick():
53
+ tick_msg = cast("TickMessage", msg)
54
+ return nw.from_native(
55
+ pl.DataFrame(
56
+ {
57
+ # msg.ts is int64 ns since epoch; from_epoch yields a tz-naive
58
+ # (already UTC) datetime[ns], preserving nanosecond precision
59
+ "date": pl.from_epoch(pl.Series([tick_msg.ts]), time_unit="ns"),
60
+ "price": [tick_msg.price],
61
+ # volume can be None (e.g. yahoo finance); pin dtype so a leading
62
+ # None row doesn't become a Null column and break later concats
63
+ "volume": pl.Series([tick_msg.volume], dtype=pl.Float64),
64
+ }
65
+ )
66
+ )
67
+ elif msg.is_bar():
68
+ bar_msg = cast("BarMessage", msg)
69
+ return nw.from_native(
70
+ pl.DataFrame(
71
+ {
72
+ # bar_msg.start_ts is int64 ns since epoch (start of bar);
73
+ # from_epoch yields a tz-naive (already UTC) datetime[ns]
74
+ "date": pl.from_epoch(
75
+ pl.Series([bar_msg.start_ts]), time_unit="ns"
76
+ ),
77
+ "open": [bar_msg.open],
78
+ "high": [bar_msg.high],
79
+ "low": [bar_msg.low],
80
+ "close": [bar_msg.close],
81
+ "volume": [bar_msg.volume],
82
+ }
83
+ )
84
+ )
85
+ else:
86
+ raise ValueError(f"Unsupported streaming message type: {type(msg)}")
87
+
88
+ def _create_streaming_df(
89
+ self, msg_key: MessageKey, msg: MarketDataMessage
90
+ ) -> nw.DataFrame[Any]:
91
+ new_row = self._create_streaming_row(msg)
92
+
93
+ if msg_key not in self._streaming_dfs:
94
+ import datetime
95
+
96
+ from pfund.datas.resolution import Resolution
97
+
98
+ # prepend a dummy row so df starts with 2 rows,
99
+ # needed for DatetimeRangeWidget to derive slider step from date_col[1] - date_col[0]
100
+ if msg.is_bar():
101
+ resolution_seconds = Resolution(msg.resolution).to_seconds()
102
+ else:
103
+ # for tick data, use 1 second as the dummy interval
104
+ resolution_seconds = 1
105
+ dummy = new_row.with_columns(
106
+ # cast back to ns: subtracting a timedelta downcasts datetime[ns] -> datetime[us],
107
+ # which would then fail to concat with the ns-precision new_row below
108
+ (nw.col("date") - datetime.timedelta(seconds=resolution_seconds)).cast(
109
+ nw.Datetime("ns")
110
+ ),
111
+ )
112
+ cprint(
113
+ f"Prepending dummy row for {msg_key} to ensure at least 2 data points for the {self._class_name}\n"
114
+ + "i.e. The first data point is dummy data",
115
+ style=TextStyle.BOLD + RichColor.YELLOW,
116
+ )
117
+ df = nw.concat([dummy, new_row])
118
+ else:
119
+ # update the streaming dataframe
120
+ existing_df = self._streaming_dfs[msg_key]
121
+ last_date = existing_df["date"][-1]
122
+ new_date = new_row["date"][0]
123
+ if new_date == last_date:
124
+ # same bar — replace last row
125
+ if msg.is_bar():
126
+ df = nw.concat([existing_df.head(-1), new_row])
127
+ # NOTE: tick data could have the same timestamp!
128
+ elif msg.is_tick():
129
+ df = nw.concat([existing_df, new_row])
130
+ else:
131
+ raise ValueError(f"Unsupported streaming message type: {type(msg)}")
132
+ elif new_date > last_date:
133
+ df = nw.concat([existing_df, new_row])
134
+ else:
135
+ raise ValueError(
136
+ f"New date {new_date} is before last date {last_date}, something is wrong with the streaming data"
137
+ )
138
+ return df
139
+
140
+ def _create_msg_key(self, msg: MarketDataMessage) -> MessageKey:
141
+ """Create a message key for streaming"""
142
+ msg_key = (msg.product, msg.resolution)
143
+ # set the first product as active by default
144
+ if self._active_msg_key is None:
145
+ self._active_msg_key = msg_key
146
+ return msg_key
147
+
148
+ # NOTE: this is added to streaming feed as a custom transformation
149
+ def _on_streaming_callback(self, msg: MarketDataMessage) -> MarketDataMessage:
150
+ # for bar data, skip incremental updates unless incremental_update is enabled
151
+ if (
152
+ msg.is_bar()
153
+ and not self._control["incremental_update"]
154
+ and msg.is_incremental
155
+ ): # pyright: ignore[reportAttributeAccessIssue]
156
+ return msg
157
+
158
+ msg_key = self._create_msg_key(msg)
159
+ df = self._create_streaming_df(msg_key, msg)
160
+ self._update_streaming_df(msg_key, df)
161
+
162
+ return msg
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, ClassVar, Literal
4
+
5
+ if TYPE_CHECKING:
6
+ from altair import Chart as AltairChart
7
+
8
+ from pfund_plot.enums import PlottingBackend
9
+ from pfund_plot.plots.plot import BasePlot
10
+
11
+
12
+ class Altair(BasePlot):
13
+ SUPPORTED_BACKENDS: ClassVar[list[Literal[PlottingBackend.altair]]] = [
14
+ PlottingBackend.altair
15
+ ]
16
+ REQUIRED_DATA: ClassVar[bool] = False
17
+
18
+ def __init__(
19
+ self, fig: AltairChart, sizing_mode: str | None = None, **pane_kwargs: Any
20
+ ):
21
+ """
22
+ Args:
23
+ fig: An Altair Chart object.
24
+ sizing_mode: Panel sizing mode: e.g. "stretch_width", "stretch_height", or "stretch_both".
25
+ **pane_kwargs: Additional keyword arguments passed to pn.pane.Vega,
26
+ e.g. height, width, max_width, margin.
27
+ """
28
+ super().__init__(data=None)
29
+ self._plot: AltairChart = fig
30
+ self._pane_kwargs = pane_kwargs
31
+ if sizing_mode is not None:
32
+ self._pane_kwargs["sizing_mode"] = sizing_mode
@@ -0,0 +1,82 @@
1
+ # pyright: reportArgumentType=false, reportOptionalMemberAccess=false, reportOptionalSubscript=false, reportCallIssue=false, reportUnknownMemberType=false, reportUnknownVariableType=false, reportUnknownArgumentType=false
2
+ from __future__ import annotations
3
+
4
+ from collections.abc import Callable
5
+ from typing import TYPE_CHECKING, Any, ClassVar
6
+
7
+ if TYPE_CHECKING:
8
+ from narwhals.typing import IntoFrame
9
+ from pfeed.feeds.market_feed import MarketFeed
10
+
11
+ from pfund_plot.widgets.base import BaseStreamingWidget, BaseWidget
12
+
13
+ from pfund_plot.enums import PlottingBackend
14
+ from pfund_plot.plots.plot import BasePlot
15
+ from pfund_plot.widgets.datetime_widget import DatetimeRangeWidget
16
+ from pfund_plot.widgets.ticker_widget import TickerSelectWidget
17
+
18
+ __all__ = ["Area"]
19
+
20
+
21
+ class AreaStyle:
22
+ from pfund_plot.plots.area.bokeh import style as bokeh_style
23
+
24
+ bokeh = bokeh_style
25
+
26
+
27
+ class AreaControl:
28
+ from pfund_plot.plots.area.bokeh import control as bokeh_control
29
+
30
+ bokeh = bokeh_control
31
+
32
+
33
+ class Area(BasePlot):
34
+ SUPPORTED_BACKENDS: ClassVar[list[PlottingBackend]] = [PlottingBackend.bokeh]
35
+ SUPPORT_STREAMING: ClassVar[bool] = True
36
+ SUPPORTED_WIDGETS: ClassVar[list[type[BaseWidget]]] = [DatetimeRangeWidget]
37
+ SUPPORTED_STREAMING_WIDGETS: ClassVar[list[type[BaseStreamingWidget]]] = [
38
+ TickerSelectWidget
39
+ ]
40
+ style = AreaStyle
41
+ control = AreaControl
42
+
43
+ def __init__(
44
+ self,
45
+ data: IntoFrame | MarketFeed | None = None,
46
+ x: str | None = None,
47
+ y: str | list[str] | None = None,
48
+ y2: str | None = None,
49
+ callback: Callable[..., Any] | None = None,
50
+ name: str | None = None,
51
+ plot_kwargs: dict[str, Any] | None = None,
52
+ **reactive_params: Any,
53
+ ):
54
+ """
55
+ Args:
56
+ data: The dataframe for static plot or pfeed's feed object for streaming plot
57
+ x: Column name for x-positions. If not specified, the index is used.
58
+ Can refer to continuous and categorical data.
59
+ y: Column name for the first y-position (lower bound of the area)
60
+ # REVIEW: y2 is only supported by hvplot
61
+ y2: Column name for the second y-position (upper bound of the area).
62
+ If not specified, the area is drawn from 0 to y.
63
+ callback: A reactive callback function. When provided with **reactive_params,
64
+ auto-creates widgets that re-fetch data on change.
65
+ name: Display name for this plot (used as label when widgets are shown alongside overlays).
66
+ Defaults to the class name lowercased.
67
+ plot_kwargs: keyword arguments for the plot function.
68
+ e.g. if the plot function is hvplot.line, plot_kwargs will be passed to hvplot.line(**plot_kwargs)
69
+ **reactive_params: name=value pairs for reactive widgets (e.g. ticker=["BTC", "ETH"]).
70
+ Requires callback to be set.
71
+ """
72
+ super().__init__(
73
+ data=data,
74
+ x=x,
75
+ y=y,
76
+ callback=callback,
77
+ name=name,
78
+ plot_kwargs=plot_kwargs,
79
+ **reactive_params,
80
+ )
81
+ if y2 is not None:
82
+ self._plot_kwargs["y2"] = y2
@@ -0,0 +1,151 @@
1
+ # pyright: reportUnusedParameter=false
2
+ from __future__ import annotations
3
+
4
+ from typing import TYPE_CHECKING, Any, Literal
5
+
6
+ if TYPE_CHECKING:
7
+ from holoviews.core.overlay import NdOverlay
8
+
9
+ import narwhals as nw
10
+
11
+ from pfund_plot.enums import PlottingBackend
12
+
13
+ __all__ = ["control", "plot", "style"]
14
+
15
+
16
+ DEFAULT_COLOR = "steelblue"
17
+ DEFAULT_HEIGHT = 280
18
+
19
+
20
+ def style(
21
+ title: str = "",
22
+ xlabel: str = "",
23
+ ylabel: str = "",
24
+ color: str = DEFAULT_COLOR,
25
+ alpha: float = 0.5,
26
+ marker: Literal[
27
+ "circle",
28
+ "square",
29
+ "triangle_up",
30
+ "triangle_down",
31
+ "diamond",
32
+ "cross",
33
+ "x",
34
+ "star",
35
+ ]
36
+ | None = None,
37
+ stacked: bool = True,
38
+ bg_color: str = "", # empty string by default because Panel will automatically use the theme color
39
+ grid: bool = False,
40
+ total_height: int | None = None,
41
+ height: int = DEFAULT_HEIGHT,
42
+ width: int | None = None,
43
+ ):
44
+ """
45
+ Args:
46
+ title: the title of the plot
47
+ xlabel: the label of the x-axis
48
+ ylabel: the label of the y-axis
49
+ color: the color of the plot, hex code is supported, only used when there is only one area chart
50
+ alpha: the alpha of the plot, 0.0 to 1.0
51
+ marker: marker shape for data points. None hides markers, any value shows them.
52
+ Default is None (hidden). Options: 'circle', 'square', 'triangle_up',
53
+ 'triangle_down', 'diamond', 'cross', 'x', 'star'.
54
+ stacked: Whether to stack multiple areas. Default is True.
55
+ bg_color: the background color of the plot, hex code is supported
56
+ total_height: the height of the component (including the figure + widgets)
57
+ Default is None, when it is None, Panel will automatically adjust its height
58
+ height: the height of the figure
59
+ width: the width of the plot, since the plot is responsive, this is only used in panel layout
60
+ grid: whether to show the grid
61
+ """
62
+ return locals()
63
+
64
+
65
+ def control(
66
+ num_data: int | None = None,
67
+ max_data: int | None = None,
68
+ slider_step: int | None = None,
69
+ widgets: bool = True,
70
+ linked_axes: bool = True,
71
+ update_interval: int = 5000, # ms
72
+ incremental_update: bool = True,
73
+ datetime_precision: Literal["d", "s", "ms"] = "s",
74
+ ):
75
+ """
76
+ Args:
77
+ num_data: (DatetimeRangeWidget) initial number of most recent data points to display.
78
+ max_data: (streaming) maximum number of data points kept in memory.
79
+ If None, data will continue to grow unbounded.
80
+ slider_step: (DatetimeRangeWidget) step size in ms for the datetime range slider.
81
+ If None, derived from data resolution.
82
+ widgets: whether to show widgets. default is True.
83
+ For granular control, use remove_widgets() to remove specific widget classes.
84
+ linked_axes: whether to link axes across plots in a layout (plt.layout(...)).
85
+ incremental_update: (streaming) whether to update even when the bar is incomplete. default is True.
86
+ update_interval: (streaming) interval in ms to update the plot. default is 5000 ms.
87
+ datetime_precision: the precision of datetime formatting on the hover tooltip.
88
+ "d" for days (%Y-%m-%d), "s" for seconds (default, %Y-%m-%d %H:%M:%S), "ms" for milliseconds (%Y-%m-%d %H:%M:%S.%3N).
89
+ """
90
+ return locals()
91
+
92
+
93
+ def plot(
94
+ df: nw.DataFrame[Any],
95
+ style: dict[str, Any],
96
+ control: dict[str, Any],
97
+ x: str | None = None,
98
+ y: str | list[str] | None = None,
99
+ **kwargs: Any,
100
+ ) -> NdOverlay:
101
+ import hvplot
102
+ from bokeh.models import CrosshairTool
103
+
104
+ from pfund_plot.plots.area import Area
105
+
106
+ _ = hvplot.extension(PlottingBackend.bokeh)
107
+
108
+ # resolve y column names
109
+ x_col = x
110
+ y_cols = Area._derive_y_cols(df, x, y)
111
+ datetime_precision = control["datetime_precision"]
112
+
113
+ crosshair_tool = CrosshairTool(
114
+ dimensions="height", line_color="gray", line_alpha=0.3
115
+ )
116
+
117
+ if len(y_cols) == 1:
118
+ kwargs["color"] = style["color"]
119
+
120
+ area_plot = (
121
+ df.to_native()
122
+ .hvplot.area(
123
+ x=x,
124
+ y=y,
125
+ tools=[crosshair_tool],
126
+ grid=style["grid"],
127
+ bgcolor=style["bg_color"],
128
+ stacked=style["stacked"],
129
+ alpha=style["alpha"],
130
+ responsive=True,
131
+ # Disable hvplot's built-in hover (shows "???") because hvplot converts
132
+ # area to Patch polygons, destroying original column data in ColumnDataSource
133
+ hover=False,
134
+ **kwargs,
135
+ )
136
+ .opts(
137
+ title=style["title"],
138
+ xlabel=style["xlabel"],
139
+ ylabel=style["ylabel"],
140
+ height=style["height"],
141
+ )
142
+ )
143
+
144
+ # NOTE: this is not needed if hvplot has fixed the tooltip issue in area plot
145
+ # Overlay invisible scatter points that carry the real data for hover tooltips
146
+ from pfund_plot.utils.bokeh import create_hover_scatter
147
+
148
+ hover_scatter = create_hover_scatter(
149
+ df, x_col, y_cols, datetime_precision, marker=style.get("marker")
150
+ )
151
+ return area_plot * hover_scatter
@@ -0,0 +1,80 @@
1
+ # pyright: reportArgumentType=false, reportOptionalMemberAccess=false, reportOptionalSubscript=false, reportCallIssue=false, reportUnknownMemberType=false, reportUnknownVariableType=false, reportUnknownArgumentType=false
2
+ from __future__ import annotations
3
+
4
+ from collections.abc import Callable
5
+ from typing import TYPE_CHECKING, Any, ClassVar
6
+
7
+ if TYPE_CHECKING:
8
+ from narwhals.typing import IntoFrame
9
+ from pfeed.feeds.market_feed import MarketFeed
10
+
11
+ from pfund_plot.widgets.base import BaseStreamingWidget, BaseWidget
12
+
13
+ from pfund_plot.enums import PlottingBackend
14
+ from pfund_plot.plots.plot import BasePlot
15
+ from pfund_plot.widgets.datetime_widget import DatetimeRangeWidget
16
+ from pfund_plot.widgets.ticker_widget import TickerSelectWidget
17
+
18
+ __all__ = ["Bar"]
19
+
20
+
21
+ class BarStyle:
22
+ from pfund_plot.plots.bar.bokeh import style as bokeh_style
23
+
24
+ bokeh = bokeh_style
25
+
26
+
27
+ class BarControl:
28
+ from pfund_plot.plots.bar.bokeh import control as bokeh_control
29
+
30
+ bokeh = bokeh_control
31
+
32
+
33
+ class Bar(BasePlot):
34
+ SUPPORTED_BACKENDS: ClassVar[list[PlottingBackend]] = [PlottingBackend.bokeh]
35
+ SUPPORT_STREAMING: ClassVar[bool] = True
36
+ SUPPORTED_WIDGETS: ClassVar[list[type[BaseWidget]]] = [DatetimeRangeWidget]
37
+ SUPPORTED_STREAMING_WIDGETS: ClassVar[list[type[BaseStreamingWidget]]] = [
38
+ TickerSelectWidget
39
+ ]
40
+ style = BarStyle
41
+ control = BarControl
42
+
43
+ def __init__(
44
+ self,
45
+ data: IntoFrame | MarketFeed | None = None,
46
+ x: str | None = None,
47
+ y: str | list[str] | None = None,
48
+ by: str | list[str] | None = None,
49
+ callback: Callable[..., Any] | None = None,
50
+ name: str | None = None,
51
+ plot_kwargs: dict[str, Any] | None = None,
52
+ **reactive_params: Any,
53
+ ):
54
+ """
55
+ Args:
56
+ data: The dataframe for static plot or pfeed's feed object for streaming plot
57
+ x: Column name for x-positions. If not specified, the index is used.
58
+ Can refer to continuous and categorical data.
59
+ y: Column name for the y-position.
60
+ by: A single column or list of columns to group by. All the subgroups are visualized.
61
+ callback: A reactive callback function. When provided with **reactive_params,
62
+ auto-creates widgets that re-fetch data on change.
63
+ name: Display name for this plot (used as label when widgets are shown alongside overlays).
64
+ Defaults to the class name lowercased.
65
+ plot_kwargs: keyword arguments for the plot function.
66
+ e.g. if the plot function is hvplot.line, plot_kwargs will be passed to hvplot.line(**plot_kwargs)
67
+ **reactive_params: name=value pairs for reactive widgets (e.g. ticker=["BTC", "ETH"]).
68
+ Requires callback to be set.
69
+ """
70
+ super().__init__(
71
+ data=data,
72
+ x=x,
73
+ y=y,
74
+ callback=callback,
75
+ name=name,
76
+ plot_kwargs=plot_kwargs,
77
+ **reactive_params,
78
+ )
79
+ if by is not None:
80
+ self._plot_kwargs["by"] = by
@@ -0,0 +1,128 @@
1
+ # pyright: reportUnusedParameter=false
2
+ from __future__ import annotations
3
+
4
+ from typing import TYPE_CHECKING, Any, Literal
5
+
6
+ if TYPE_CHECKING:
7
+ from holoviews.core.overlay import Overlay
8
+
9
+ import narwhals as nw
10
+
11
+ from pfund_plot.enums import PlottingBackend
12
+
13
+ __all__ = ["control", "plot", "style"]
14
+
15
+
16
+ DEFAULT_COLOR = "steelblue"
17
+ DEFAULT_HEIGHT = 280
18
+
19
+
20
+ def style(
21
+ title: str = "",
22
+ xlabel: str = "",
23
+ ylabel: str = "",
24
+ stacked: bool = False,
25
+ color: str | list[str] = DEFAULT_COLOR,
26
+ bg_color: str = "", # empty string by default because Panel will automatically use the theme color
27
+ grid: bool = False,
28
+ total_height: int | None = None,
29
+ height: int = DEFAULT_HEIGHT,
30
+ width: int | None = None,
31
+ ):
32
+ """
33
+ Args:
34
+ title: the title of the plot
35
+ xlabel: the label of the x-axis
36
+ ylabel: the label of the y-axis
37
+ stacked: Whether to stack multiple bars. Default is False.
38
+ color: the color of the plot, hex code is supported
39
+ bg_color: the background color of the plot, hex code is supported
40
+ total_height: the height of the component (including the figure + widgets)
41
+ Default is None, when it is None, Panel will automatically adjust its height
42
+ height: the height of the figure
43
+ width: the width of the plot, since the plot is responsive, this is only used in panel layout
44
+ grid: whether to show the grid
45
+ """
46
+ return locals()
47
+
48
+
49
+ def control(
50
+ num_data: int | None = None,
51
+ max_data: int | None = None,
52
+ slider_step: int | None = None,
53
+ widgets: bool = True,
54
+ linked_axes: bool = True,
55
+ update_interval: int = 5000, # ms
56
+ incremental_update: bool = True,
57
+ datetime_precision: Literal["d", "s", "ms"] = "s",
58
+ ):
59
+ """
60
+ Args:
61
+ num_data: (DatetimeRangeWidget) initial number of most recent data points to display.
62
+ max_data: (streaming) maximum number of data points kept in memory.
63
+ If None, data will continue to grow unbounded.
64
+ slider_step: (DatetimeRangeWidget) step size in ms for the datetime range slider.
65
+ If None, derived from data resolution.
66
+ widgets: whether to show widgets. default is True.
67
+ For granular control, use remove_widgets() to remove specific widget classes.
68
+ linked_axes: whether to link axes across plots in a layout (plt.layout(...)).
69
+ incremental_update: (streaming) whether to update even when the bar is incomplete. default is True.
70
+ update_interval: (streaming) interval in ms to update the plot. default is 5000 ms.
71
+ datetime_precision: the precision of datetime formatting on the hover tooltip.
72
+ "d" for days (%Y-%m-%d), "s" for seconds (default, %Y-%m-%d %H:%M:%S), "ms" for milliseconds (%Y-%m-%d %H:%M:%S.%3N).
73
+ """
74
+ return locals()
75
+
76
+
77
+ def plot(
78
+ df: nw.DataFrame[Any],
79
+ style: dict[str, Any],
80
+ control: dict[str, Any],
81
+ x: str | None = None,
82
+ y: str | list[str] | None = None,
83
+ **kwargs: Any,
84
+ ) -> Overlay:
85
+ import hvplot
86
+ from bokeh.models import CrosshairTool
87
+
88
+ from pfund_plot.plots.bar import Bar
89
+ from pfund_plot.utils.bokeh import create_bundled_hover_tool
90
+
91
+ _ = hvplot.extension(PlottingBackend.bokeh)
92
+
93
+ x_col = x
94
+ y_cols = Bar._derive_y_cols(df, x, y)
95
+ datetime_precision = control["datetime_precision"]
96
+
97
+ is_single = len(y_cols) == 1
98
+ crosshair_tool = CrosshairTool(
99
+ dimensions="height", line_color="gray", line_alpha=0.3
100
+ )
101
+ if is_single:
102
+ tools = [
103
+ create_bundled_hover_tool(df, x_col, y_cols, datetime_precision),
104
+ crosshair_tool,
105
+ ]
106
+ else:
107
+ tools = [crosshair_tool]
108
+
109
+ return (
110
+ df.to_native()
111
+ .hvplot.bar(
112
+ x=x,
113
+ y=y,
114
+ tools=tools,
115
+ responsive=True,
116
+ color=style["color"],
117
+ stacked=style["stacked"],
118
+ bgcolor=style["bg_color"],
119
+ grid=style["grid"],
120
+ **kwargs,
121
+ )
122
+ .opts(
123
+ title=style["title"],
124
+ xlabel=style["xlabel"],
125
+ ylabel=style["ylabel"],
126
+ height=style["height"],
127
+ )
128
+ )
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, ClassVar, Literal
4
+
5
+ if TYPE_CHECKING:
6
+ from bokeh.model import Model as BokehModel
7
+
8
+ from pfund_plot.enums import PlottingBackend
9
+ from pfund_plot.plots.plot import BasePlot
10
+
11
+
12
+ class Bokeh(BasePlot):
13
+ SUPPORTED_BACKENDS: ClassVar[list[Literal[PlottingBackend.bokeh]]] = [
14
+ PlottingBackend.bokeh
15
+ ]
16
+ REQUIRED_DATA: ClassVar[bool] = False
17
+
18
+ def __init__(
19
+ self, fig: BokehModel, sizing_mode: str | None = None, **pane_kwargs: Any
20
+ ):
21
+ """
22
+ Args:
23
+ fig: A Bokeh Model object.
24
+ sizing_mode: Panel sizing mode: e.g. "stretch_width", "stretch_height", or "stretch_both".
25
+ **pane_kwargs: Additional keyword arguments passed to pn.pane.Bokeh,
26
+ e.g. height, width, max_width, margin.
27
+ """
28
+ super().__init__(data=None)
29
+ self._plot: BokehModel = fig
30
+ self._pane_kwargs = pane_kwargs
31
+ if sizing_mode is not None:
32
+ self._pane_kwargs["sizing_mode"] = sizing_mode