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,32 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, ClassVar, Literal
4
+
5
+ if TYPE_CHECKING:
6
+ from plotly.graph_objects import Figure as PlotlyFigure
7
+
8
+ from pfund_plot.enums import PlottingBackend
9
+ from pfund_plot.plots.plot import BasePlot
10
+
11
+
12
+ class Plotly(BasePlot):
13
+ SUPPORTED_BACKENDS: ClassVar[list[Literal[PlottingBackend.plotly]]] = [
14
+ PlottingBackend.plotly
15
+ ]
16
+ REQUIRED_DATA: ClassVar[bool] = False
17
+
18
+ def __init__(
19
+ self, fig: PlotlyFigure, sizing_mode: str | None = None, **pane_kwargs: Any
20
+ ):
21
+ """
22
+ Args:
23
+ fig: A Plotly Figure 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.Plotly,
26
+ e.g. height, width, max_width, margin.
27
+ """
28
+ super().__init__(data=None)
29
+ self._plot: PlotlyFigure = 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,62 @@
1
+ # pyright: reportArgumentType=false, reportOptionalMemberAccess=false, reportOptionalSubscript=false, reportCallIssue=false, reportUnknownMemberType=false, reportUnknownVariableType=false, reportUnknownArgumentType=false
2
+ from __future__ import annotations
3
+
4
+ from typing import TYPE_CHECKING, Any, ClassVar
5
+
6
+ if TYPE_CHECKING:
7
+ from narwhals.typing import IntoFrame
8
+
9
+ from pfund_plot.widgets.base import BaseWidget
10
+
11
+ from pfund_plot.enums import PlottingBackend
12
+ from pfund_plot.plots.plot import BasePlot
13
+ from pfund_plot.widgets.datetime_widget import DatetimeRangeWidget
14
+
15
+ __all__ = ["Scatter"]
16
+
17
+
18
+ class ScatterStyle:
19
+ from pfund_plot.plots.scatter.bokeh import style as bokeh_style
20
+
21
+ bokeh = bokeh_style
22
+
23
+
24
+ class ScatterControl:
25
+ from pfund_plot.plots.scatter.bokeh import control as bokeh_control
26
+
27
+ bokeh = bokeh_control
28
+
29
+
30
+ class Scatter(BasePlot):
31
+ """Generic scatter plot — renders points on a 2D plane.
32
+
33
+ Supports per-point styling via DataFrame columns or uniform styling via kwargs.
34
+ Works standalone or composed onto another plot via the * operator.
35
+
36
+ Args:
37
+ data: DataFrame with point positions
38
+ x: Column name for x-axis position
39
+ y: Column name for y-axis position
40
+
41
+ Example:
42
+ plt.scatter(df, x='x', y='y')
43
+ plt.ohlc(ohlc_df) * plt.scatter(df, x='date', y='price')
44
+ """
45
+
46
+ SUPPORTED_BACKENDS: ClassVar[list[PlottingBackend]] = [PlottingBackend.bokeh]
47
+ # TODO: support other streaming feeds like EngineFeed etc.
48
+ # SUPPORT_STREAMING: ClassVar[bool] = True
49
+ SUPPORTED_WIDGETS: ClassVar[list[type[BaseWidget]]] = [DatetimeRangeWidget]
50
+ style = ScatterStyle
51
+ control = ScatterControl
52
+
53
+ # TODO: add "by" parameter to group by, see https://hvplot.holoviz.org/en/docs/latest/ref/api/manual/hvplot.hvPlot.scatter.html
54
+ def __init__(
55
+ self,
56
+ data: IntoFrame,
57
+ x: str | None = None,
58
+ y: str | list[str] | None = None,
59
+ name: str | None = None,
60
+ **reactive_params: Any,
61
+ ):
62
+ super().__init__(data=data, x=x, y=y, name=name, **reactive_params)
@@ -0,0 +1,158 @@
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
+ # Marker mapping from user-friendly names to Bokeh scatter types
17
+ MARKER_MAP = {
18
+ "triangle_up": "triangle",
19
+ "triangle_down": "inverted_triangle",
20
+ }
21
+
22
+ DEFAULT_MARKER = "circle"
23
+ DEFAULT_SIZE = 100 # hvplot takes sqrt(size), so 100 -> rendered size of 10
24
+ DEFAULT_COLOR = "steelblue"
25
+ DEFAULT_HEIGHT = 280
26
+
27
+
28
+ def style(
29
+ title: str = "Scatter",
30
+ xlabel: str = "",
31
+ ylabel: str = "",
32
+ color: str = DEFAULT_COLOR,
33
+ marker: Literal[
34
+ "circle",
35
+ "square",
36
+ "triangle_up",
37
+ "triangle_down",
38
+ "diamond",
39
+ "cross",
40
+ "x",
41
+ "star",
42
+ ]
43
+ | str = DEFAULT_MARKER,
44
+ size: int | str = DEFAULT_SIZE,
45
+ opacity: float = 0.8,
46
+ grid: bool = False,
47
+ total_height: int | None = None,
48
+ height: int = DEFAULT_HEIGHT,
49
+ width: int | None = None,
50
+ ):
51
+ """
52
+ Args:
53
+ title: the title of the plot
54
+ xlabel: the label of the x-axis
55
+ ylabel: the label of the y-axis
56
+ color: Column name for per-point color, or a literal color string
57
+ marker: Column name for per-point marker, or a literal marker string.
58
+ Supported markers: circle, square, triangle_up, triangle_down,
59
+ diamond, cross, x, star
60
+ size: Point size. Either a fixed int (e.g. size=15) for uniform sizing,
61
+ or a column name (e.g. size='volume') for per-point sizing from data.
62
+ opacity: scatter opacity (0.0 to 1.0)
63
+ grid: whether to show the grid
64
+ total_height: the height of the component (including the figure + widgets).
65
+ Default is None, Panel will automatically adjust its height.
66
+ height: the height of the figure
67
+ width: the width of the plot, since the plot is responsive, this is only used in panel layout
68
+ """
69
+ return locals()
70
+
71
+
72
+ def control(
73
+ num_data: int | None = None,
74
+ slider_step: int | None = None,
75
+ widgets: bool = True,
76
+ linked_axes: bool = True,
77
+ include_extra_cols: bool = False,
78
+ ):
79
+ """
80
+ Args:
81
+ num_data: (DatetimeRangeWidget) initial number of most recent data points to display.
82
+ slider_step: (DatetimeRangeWidget) step size in ms for the datetime range slider.
83
+ If None, derived from data resolution.
84
+ widgets: whether to show widgets. default is True.
85
+ For granular control, use remove_widgets() to remove specific widget classes.
86
+ linked_axes: whether to link axes across plots in a layout (plt.layout(...)).
87
+ include_extra_cols: whether to include extra columns in the hover tooltip.
88
+ """
89
+ return locals()
90
+
91
+
92
+ def plot(
93
+ df: nw.DataFrame[Any],
94
+ style: dict[str, Any],
95
+ control: dict[str, Any],
96
+ x: str | None = None,
97
+ y: str | list[str] | None = None,
98
+ **kwargs: Any,
99
+ ) -> Overlay:
100
+ import hvplot
101
+
102
+ _ = hvplot.extension(PlottingBackend.bokeh)
103
+
104
+ color = style["color"]
105
+ marker = style["marker"]
106
+ size = style["size"]
107
+
108
+ columns = df.columns
109
+
110
+ if control["include_extra_cols"]:
111
+ exclude: set[str] = set()
112
+ if x:
113
+ exclude.add(x)
114
+ if isinstance(y, list):
115
+ exclude.update(y)
116
+ elif y:
117
+ exclude.add(y)
118
+ kwargs["hover_cols"] = [c for c in columns if c not in exclude]
119
+
120
+ color_is_col = isinstance(color, str) and color in columns
121
+ size_is_col = isinstance(size, str) and size in columns
122
+ marker_is_col = isinstance(marker, str) and marker in columns
123
+
124
+ if color_is_col:
125
+ kwargs["c"] = color
126
+ else:
127
+ kwargs["color"] = color
128
+
129
+ if size_is_col:
130
+ kwargs["s"] = size
131
+ else:
132
+ kwargs["size"] = size
133
+
134
+ if marker_is_col:
135
+ df = df.with_columns(
136
+ nw.col(marker).replace_strict(MARKER_MAP, default=nw.col(marker))
137
+ )
138
+ kwargs["marker"] = marker
139
+ else:
140
+ kwargs["marker"] = MARKER_MAP.get(marker, marker)
141
+
142
+ return (
143
+ df.to_native()
144
+ .hvplot.scatter(
145
+ x=x,
146
+ y=y,
147
+ responsive=True,
148
+ alpha=style["opacity"],
149
+ grid=style["grid"],
150
+ **kwargs,
151
+ )
152
+ .opts(
153
+ title=style["title"],
154
+ xlabel=style["xlabel"],
155
+ ylabel=style["ylabel"],
156
+ height=style["height"],
157
+ )
158
+ )
@@ -0,0 +1,107 @@
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, Literal
6
+
7
+ if TYPE_CHECKING:
8
+ from narwhals.typing import IntoFrame
9
+
10
+ from pfund_plot.typing import Control, Plot, Style
11
+
12
+ import narwhals as nw
13
+
14
+ from pfund_plot.plots.scatter import Scatter
15
+
16
+ __all__ = ["Marker"]
17
+
18
+
19
+ class Marker(Scatter):
20
+ """Marker plot that colors points by signal direction.
21
+
22
+ Positive signal values (>= 0) get one color/marker, negative values get another.
23
+
24
+ Args:
25
+ data: DataFrame with marker positions
26
+ x: Column name for x-axis position
27
+ y: Column name for y-axis position (e.g. 'close' price)
28
+ signal: Column whose sign determines positive/negative styling.
29
+ If None, defaults to the y column.
30
+ pos_color: Color for positive values (default: 'green')
31
+ neg_color: Color for negative values (default: 'red')
32
+ pos_marker: Marker shape for positive values (default: 'triangle_up')
33
+ neg_marker: Marker shape for negative values (default: 'triangle_down')
34
+
35
+ Example:
36
+ plt.marker(df, x='date', y='pnl')
37
+ plt.ohlc(df) * plt.marker(df, x='date', y='close', signal='trade')
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ data: IntoFrame,
43
+ x: str | None = None,
44
+ y: str | list[str] | None = None,
45
+ signal: str | None = None,
46
+ pos_color: str = "green",
47
+ neg_color: str = "red",
48
+ pos_marker: Literal[
49
+ "circle",
50
+ "square",
51
+ "triangle_up",
52
+ "triangle_down",
53
+ "diamond",
54
+ "cross",
55
+ "x",
56
+ "star",
57
+ ] = "triangle_up",
58
+ neg_marker: Literal[
59
+ "circle",
60
+ "square",
61
+ "triangle_up",
62
+ "triangle_down",
63
+ "diamond",
64
+ "cross",
65
+ "x",
66
+ "star",
67
+ ] = "triangle_down",
68
+ name: str | None = None,
69
+ **reactive_params: Any,
70
+ ):
71
+ self._signal = signal
72
+ self._pos_color = pos_color
73
+ self._neg_color = neg_color
74
+ self._pos_marker = pos_marker
75
+ self._neg_marker = neg_marker
76
+ super().__init__(data=data, x=x, y=y, name=name, **reactive_params)
77
+
78
+ @property
79
+ def _plot_func(self) -> Callable[[nw.DataFrame[Any], Style, Control], Plot]:
80
+ """Runs the plot function for the current backend."""
81
+ import importlib
82
+
83
+ module_path = f"pfund_plot.plots.scatter.{self._backend}"
84
+ module = importlib.import_module(module_path)
85
+ return module.plot
86
+
87
+ def _standardize_df(self, df: IntoFrame) -> nw.DataFrame[Any]:
88
+ df: nw.DataFrame[Any] = super()._standardize_df(df)
89
+ signal_col = self._signal if self._signal else self._y
90
+ is_positive = nw.col(signal_col) >= 0
91
+ df = df.with_columns(
92
+ nw.when(is_positive)
93
+ .then(nw.lit(self._pos_color))
94
+ .otherwise(nw.lit(self._neg_color))
95
+ .alias("_color"),
96
+ nw.when(is_positive)
97
+ .then(nw.lit(self._pos_marker))
98
+ .otherwise(nw.lit(self._neg_marker))
99
+ .alias("_marker"),
100
+ )
101
+ return df
102
+
103
+ def _set_style(self, style: dict[str, Any] | None = None):
104
+ super()._set_style(style)
105
+ if self._style is not None:
106
+ self._style["color"] = "_color"
107
+ self._style["marker"] = "_marker"
pfund_plot/plots/ta.py ADDED
@@ -0,0 +1,6 @@
1
+ # TODO: add technical analysis plots using talib?
2
+ """
3
+ e.g. plt.sma(df, period=10)
4
+ first convert it to plt.ta(df, indicator='sma', period=10)
5
+ it will use sma from talib directly, and return a plot
6
+ """
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ if TYPE_CHECKING:
7
+ from panel.io.callbacks import PeriodicCallback
8
+ from panel.io.server import Server
9
+ from panel.io.threads import StoppableThread
10
+
11
+ from pfund_plot.enums import NotebookType
12
+ from pfund_plot.typing import Component, RenderedResult
13
+
14
+ from abc import ABC, abstractmethod
15
+
16
+ import panel as pn
17
+
18
+
19
+ class BaseRenderer(ABC):
20
+ def __init__(self):
21
+ from pfund_kit.utils import get_notebook_type
22
+
23
+ self._periodic_callbacks: list[PeriodicCallback] = []
24
+ self._port: int | None = None
25
+ self._server: StoppableThread | Server | None = None
26
+ self._notebook_type: NotebookType | None = get_notebook_type()
27
+
28
+ def is_in_notebook_env(self) -> bool:
29
+ return self._notebook_type is not None
30
+
31
+ @property
32
+ def server(self) -> StoppableThread | Server | None:
33
+ return self._server
34
+
35
+ def add_periodic_callback(self, periodic_callback: PeriodicCallback):
36
+ from panel.io.callbacks import PeriodicCallback
37
+
38
+ if not isinstance(periodic_callback, PeriodicCallback):
39
+ raise ValueError(
40
+ "periodic_callback must be a panel.io.callbacks.PeriodicCallback instance"
41
+ )
42
+ self._periodic_callbacks.append(periodic_callback)
43
+
44
+ def run_periodic_callbacks(self):
45
+ for periodic_callback in self._periodic_callbacks:
46
+ periodic_callback.start()
47
+
48
+ def set_port_in_use(self, port: int):
49
+ self._port = port
50
+
51
+ @staticmethod
52
+ def _get_free_port() -> int:
53
+ from pfund_kit.utils import get_free_port
54
+
55
+ return get_free_port()
56
+
57
+ @abstractmethod
58
+ def render(self, component: Component, *args: Any, **kwargs: Any) -> RenderedResult:
59
+ pass
60
+
61
+ def serve(
62
+ self,
63
+ # component or factory function that returns a component
64
+ renderable: Component | Callable[[], Component],
65
+ show: bool = False,
66
+ threaded: bool = True,
67
+ port: int | None = None,
68
+ ) -> StoppableThread | Server:
69
+ from pfund_plot.config import get_config
70
+
71
+ if port is None:
72
+ port = self._get_free_port()
73
+ if self._server is not None:
74
+ raise ValueError("Server is already running")
75
+ config = get_config()
76
+ self.set_port_in_use(port)
77
+ self._server = pn.serve( # pyright: ignore[reportUnknownMemberType]
78
+ renderable, # pyright: ignore[reportArgumentType]
79
+ show=show,
80
+ threaded=threaded,
81
+ port=port,
82
+ static_dirs=config.static_dirs,
83
+ )
84
+ return self._server
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, cast
4
+
5
+ if TYPE_CHECKING:
6
+ from panel.io.threads import StoppableThread
7
+
8
+ from pfund_plot.typing import Component
9
+
10
+ from pfund_plot.renderers.base import BaseRenderer
11
+
12
+
13
+ class BrowserRenderer(BaseRenderer):
14
+ def render(self, component: Component):
15
+ if self.is_in_notebook_env(): # run browser mode in a notebook environment
16
+ thread = cast(
17
+ "StoppableThread", self.serve(component, show=True, threaded=True)
18
+ )
19
+ self.run_periodic_callbacks()
20
+ return thread
21
+ else: # run in a python script
22
+ # wrap component in a factory so periodic callbacks start inside the server's event loop
23
+ def _servable():
24
+ self.run_periodic_callbacks()
25
+ return component
26
+
27
+ # this will block the main thread
28
+ _ = self.serve(_servable, show=True, threaded=False)
@@ -0,0 +1,109 @@
1
+ # pyright: reportUnknownMemberType=false
2
+ from __future__ import annotations
3
+
4
+ from typing import TYPE_CHECKING, cast
5
+
6
+ if TYPE_CHECKING:
7
+ from panel.io.threads import StoppableThread
8
+
9
+ from pfund_plot.typing import Component
10
+
11
+ from multiprocessing import Event as create_event
12
+ from multiprocessing import Process
13
+ from multiprocessing.synchronize import Event
14
+ from threading import Thread
15
+
16
+ from pfund_kit.style import RichColor, TextStyle, cprint
17
+
18
+ from pfund_plot.renderers.browser import BrowserRenderer
19
+
20
+
21
+ def _run_webview(title: str, port: int, window_ready: Event):
22
+ import webview as wv
23
+
24
+ _ = wv.create_window(
25
+ title,
26
+ url=f"http://localhost:{port}",
27
+ resizable=True,
28
+ )
29
+ window_ready.set()
30
+ wv.start()
31
+
32
+
33
+ class DesktopRenderer(BrowserRenderer):
34
+ def render(self, component: Component):
35
+ port = self._get_free_port()
36
+ title = getattr(component, "name", "PFund Plot")
37
+ window_ready = create_event()
38
+ if self.is_in_notebook_env():
39
+ server = cast(
40
+ "StoppableThread",
41
+ self.serve(component, show=False, threaded=True, port=port),
42
+ )
43
+
44
+ def run_process():
45
+ try:
46
+ process = Process(
47
+ target=_run_webview,
48
+ name=title,
49
+ args=(
50
+ title,
51
+ port,
52
+ window_ready,
53
+ ),
54
+ daemon=True,
55
+ )
56
+ process.start()
57
+ process.join()
58
+ except Exception as e:
59
+ cprint(
60
+ f"An error occurred: {e}", style=TextStyle.BOLD + RichColor.RED
61
+ )
62
+ finally:
63
+ server.stop()
64
+
65
+ thread = Thread(target=run_process, daemon=True)
66
+ thread.start()
67
+ # wait for the window to be ready before starting the periodic callback to prevent data loss during streaming
68
+ _ = window_ready.wait()
69
+ self.run_periodic_callbacks()
70
+ return server
71
+ else:
72
+ process = Process(
73
+ target=_run_webview,
74
+ name=title,
75
+ args=(
76
+ title,
77
+ port,
78
+ window_ready,
79
+ ),
80
+ daemon=True,
81
+ )
82
+ try:
83
+ process.start()
84
+ except RuntimeError as e:
85
+ if "freeze_support" in str(
86
+ e
87
+ ) or "current process has finished its bootstrapping phase" in str(e):
88
+ raise RuntimeError(
89
+ "Failed to start desktop renderer process.\n"
90
+ + "Please wrap your code with:\n\n"
91
+ + " if __name__ == '__main__':\n"
92
+ + " # your code here\n"
93
+ ) from None
94
+ raise
95
+ _ = window_ready.wait()
96
+
97
+ def _servable():
98
+ self.run_periodic_callbacks()
99
+ return component
100
+
101
+ thread = Thread(
102
+ target=lambda: self.serve(
103
+ _servable, show=False, threaded=False, port=port
104
+ ),
105
+ daemon=True,
106
+ )
107
+ thread.start()
108
+
109
+ process.join()
@@ -0,0 +1,92 @@
1
+ # pyright: reportUnknownMemberType=false, reportOptionalMemberAccess=false, reportCallIssue=false, reportUnusedVariable=false, reportOperatorIssue=false, reportUnknownArgumentType=false
2
+ from __future__ import annotations
3
+
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from pfund_plot.typing import Component
8
+
9
+ try:
10
+ import marimo as mo
11
+ except ImportError:
12
+ mo = None
13
+
14
+ from pfund_plot.enums import NotebookType
15
+ from pfund_plot.renderers.base import BaseRenderer
16
+
17
+
18
+ class NotebookRenderer(BaseRenderer):
19
+ # REVIEW: if want more control of the callback thread in marimo, add it back
20
+ # def run_periodic_callbacks(self):
21
+ # if self._notebook_type != NotebookType.marimo:
22
+ # super().run_periodic_callbacks()
23
+ # return
24
+ # # Don't use the thread created by periodic_callback.start(),
25
+ # # instead create a marimo thread to stream updates.
26
+ # def stream_updates(_periodic_callback: PeriodicCallback):
27
+ # thread = mo.current_thread()
28
+ # time.sleep(1) # HACK: wait some time to avoid data loss during streaming
29
+ # while not thread.should_exit:
30
+ # _periodic_callback.callback()
31
+ # time.sleep(_periodic_callback.period / 1000)
32
+
33
+ # for periodic_callback in self._periodic_callbacks:
34
+ # stream_thread = mo.Thread(target=stream_updates, args=(periodic_callback,), daemon=True)
35
+ # stream_thread.start()
36
+
37
+ def render(
38
+ self,
39
+ component: Component,
40
+ use_iframe: bool = False,
41
+ iframe_style: str | None = None,
42
+ ):
43
+ """
44
+ Args:
45
+ use_iframe: if True, use an iframe to display the plot in a notebook.
46
+ It is a workaround when the plot can't be displayed in a notebook.
47
+ iframe_style: the style of the iframe when use_iframe is True.
48
+ """
49
+ if not self.is_in_notebook_env():
50
+ raise ValueError("Not in a notebook environment")
51
+
52
+ if not use_iframe:
53
+ self.run_periodic_callbacks()
54
+ if self._notebook_type == NotebookType.marimo:
55
+ return mo.as_html(component)
56
+ else:
57
+ return component
58
+ else:
59
+ raise NotImplementedError("Iframe is not supported in notebook mode yet")
60
+ # if iframe_style is None:
61
+ # cprint(
62
+ # "No iframe_style is provided for iframe in notebook",
63
+ # style=TextStyle.BOLD + RichColor.YELLOW
64
+ # )
65
+ # port = self._get_free_port()
66
+ # if self._notebook_type == NotebookType.jupyter:
67
+ # cprint(
68
+ # f"If the plot can't be displayed, try to use 'from IPython.display import IFrame' and 'IFrame(src='http://localhost:{port}', width=..., height=...)'",
69
+ # style=TextStyle.BOLD + RichColor.YELLOW
70
+ # )
71
+ # html_pane: Pane = pn.pane.HTML(
72
+ # f'''
73
+ # <iframe
74
+ # src="http://localhost:{port}"
75
+ # style="{iframe_style}"
76
+ # </iframe>
77
+ # ''',
78
+ # )
79
+ # # let pane HTML inherit the height, width and sizing_mode from the figure, useful in layout_plot()
80
+ # html_pane.param.update(
81
+ # height=component.height,
82
+ # width=component.width,
83
+ # sizing_mode=component.sizing_mode,
84
+ # )
85
+ # thread: StoppableThread = self.serve(component, show=False, threaded=True, port=port)
86
+ # self.run_periodic_callbacks()
87
+ # return thread
88
+ # FIXME: should return html_pane instead of thread?
89
+ # if self._notebook_type == NotebookType.marimo:
90
+ # return mo.as_html(html_pane)
91
+ # else:
92
+ # return html_pane