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.
- pfund_plot/__init__.py +183 -0
- pfund_plot/__main__.py +9 -0
- pfund_plot/cli/__init__.py +3 -0
- pfund_plot/cli/commands/gallery/__init__.py +15 -0
- pfund_plot/cli/commands/gallery/gallery_marimo.py +462 -0
- pfund_plot/cli/commands/serve.py +21 -0
- pfund_plot/cli/main.py +20 -0
- pfund_plot/config.py +109 -0
- pfund_plot/enums/__init__.py +16 -0
- pfund_plot/enums/dataframe_backend.py +6 -0
- pfund_plot/enums/display_mode.py +7 -0
- pfund_plot/enums/panel_design.py +8 -0
- pfund_plot/enums/panel_theme.py +6 -0
- pfund_plot/enums/plotting_backend.py +12 -0
- pfund_plot/js_tap/components/candlestick.js +9566 -0
- pfund_plot/mixins/streaming_market_feed_mixin.py +162 -0
- pfund_plot/plots/altair.py +32 -0
- pfund_plot/plots/area/__init__.py +82 -0
- pfund_plot/plots/area/bokeh.py +151 -0
- pfund_plot/plots/bar/__init__.py +80 -0
- pfund_plot/plots/bar/bokeh.py +128 -0
- pfund_plot/plots/bokeh.py +32 -0
- pfund_plot/plots/candlestick/__init__.py +77 -0
- pfund_plot/plots/candlestick/bokeh.py +124 -0
- pfund_plot/plots/candlestick/svelte.py +161 -0
- pfund_plot/plots/holoviews.py +32 -0
- pfund_plot/plots/label/__init__.py +43 -0
- pfund_plot/plots/label/bokeh.py +89 -0
- pfund_plot/plots/layout/__init__.py +98 -0
- pfund_plot/plots/layout/layout.py +116 -0
- pfund_plot/plots/layout/panel.py +51 -0
- pfund_plot/plots/layout/tabs/__init__.py +36 -0
- pfund_plot/plots/layout/tabs/panel.py +51 -0
- pfund_plot/plots/lazy.py +408 -0
- pfund_plot/plots/line/__init__.py +37 -0
- pfund_plot/plots/line/bokeh.py +137 -0
- pfund_plot/plots/matplotlib.py +32 -0
- pfund_plot/plots/plot.py +1131 -0
- pfund_plot/plots/plotly.py +32 -0
- pfund_plot/plots/scatter/__init__.py +62 -0
- pfund_plot/plots/scatter/bokeh.py +158 -0
- pfund_plot/plots/scatter/marker.py +107 -0
- pfund_plot/plots/ta.py +6 -0
- pfund_plot/renderers/base.py +84 -0
- pfund_plot/renderers/browser.py +28 -0
- pfund_plot/renderers/desktop.py +109 -0
- pfund_plot/renderers/notebook.py +92 -0
- pfund_plot/typing.py +29 -0
- pfund_plot/utils/__init__.py +176 -0
- pfund_plot/utils/bokeh.py +177 -0
- pfund_plot/widgets/base.py +76 -0
- pfund_plot/widgets/datetime_widget.py +221 -0
- pfund_plot/widgets/ticker_widget.py +82 -0
- pfund_plot-0.0.1.dist-info/METADATA +148 -0
- pfund_plot-0.0.1.dist-info/RECORD +57 -0
- pfund_plot-0.0.1.dist-info/WHEEL +4 -0
- pfund_plot-0.0.1.dist-info/entry_points.txt +6 -0
|
@@ -0,0 +1,77 @@
|
|
|
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, ClassVar, cast
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from pfeed.requests.market_feed_stream_request import MarketFeedStreamRequest
|
|
8
|
+
from pfund.datas.resolution import Resolution
|
|
9
|
+
|
|
10
|
+
from pfund_plot.widgets.base import BaseStreamingWidget, BaseWidget
|
|
11
|
+
|
|
12
|
+
from pfund_plot.enums import PlottingBackend
|
|
13
|
+
from pfund_plot.mixins.streaming_market_feed_mixin import StreamingMarketFeedMixin
|
|
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__ = ["Candlestick"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CandlestickStyle:
|
|
22
|
+
from pfund_plot.plots.candlestick.bokeh import style as bokeh_style
|
|
23
|
+
from pfund_plot.plots.candlestick.svelte import style as svelte_style
|
|
24
|
+
|
|
25
|
+
bokeh = bokeh_style
|
|
26
|
+
svelte = svelte_style
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CandlestickControl:
|
|
30
|
+
from pfund_plot.plots.candlestick.bokeh import control as bokeh_control
|
|
31
|
+
from pfund_plot.plots.candlestick.svelte import control as svelte_control
|
|
32
|
+
|
|
33
|
+
bokeh = bokeh_control
|
|
34
|
+
svelte = svelte_control
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Candlestick(StreamingMarketFeedMixin, BasePlot):
|
|
38
|
+
REQUIRED_COLS: ClassVar[list[str]] = ["date", "open", "high", "low", "close"]
|
|
39
|
+
OPTIONAL_COLS: ClassVar[list[str]] = ["volume"]
|
|
40
|
+
SUPPORTED_BACKENDS: ClassVar[list[PlottingBackend]] = [
|
|
41
|
+
PlottingBackend.bokeh,
|
|
42
|
+
PlottingBackend.svelte,
|
|
43
|
+
]
|
|
44
|
+
SUPPORT_STREAMING: ClassVar[bool] = True
|
|
45
|
+
SUPPORTED_WIDGETS: ClassVar[list[type[BaseWidget]]] = [DatetimeRangeWidget]
|
|
46
|
+
SUPPORTED_STREAMING_WIDGETS: ClassVar[list[type[BaseStreamingWidget]]] = [
|
|
47
|
+
TickerSelectWidget
|
|
48
|
+
]
|
|
49
|
+
style = CandlestickStyle
|
|
50
|
+
control = CandlestickControl
|
|
51
|
+
|
|
52
|
+
def _create_component(self):
|
|
53
|
+
# NOTE: somehow data update on anywidget (svelte) in marimo notebook doesn't work using Panel
|
|
54
|
+
# (probably need a refresh of the marimo cell to reflect the changes), so use mo.vstack() as a workaround
|
|
55
|
+
if self._is_using_marimo_svelte_combo():
|
|
56
|
+
import marimo as mo
|
|
57
|
+
from pfund_kit.style import RichColor, TextStyle, cprint
|
|
58
|
+
|
|
59
|
+
# NOTE: self._style (a Panel-layer concern) is NOT applied in this case,
|
|
60
|
+
# since we bypass pn.Column. total_height is the only style with a visible
|
|
61
|
+
# effect here, so warn if it was set and is being silently dropped.
|
|
62
|
+
if self._style and self._style.get("total_height") is not None:
|
|
63
|
+
cprint(
|
|
64
|
+
"total_height is not supported in the marimo + svelte combo and will be ignored.",
|
|
65
|
+
style=TextStyle.BOLD + RichColor.YELLOW,
|
|
66
|
+
)
|
|
67
|
+
self._component = mo.vstack([self._anywidget])
|
|
68
|
+
else:
|
|
69
|
+
super()._create_component()
|
|
70
|
+
|
|
71
|
+
def _start_streaming(self):
|
|
72
|
+
requests = cast("list[MarketFeedStreamRequest]", self._feed._requests)
|
|
73
|
+
assert all(
|
|
74
|
+
cast("Resolution", request.target_resolution).is_bar()
|
|
75
|
+
for request in requests
|
|
76
|
+
), "candlestick streaming only supports bar data"
|
|
77
|
+
super()._start_streaming()
|
|
@@ -0,0 +1,124 @@
|
|
|
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_HEIGHT = 280
|
|
17
|
+
DEFAULT_NUM_DATA = 150
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def style(
|
|
21
|
+
title: str = "Candlestick",
|
|
22
|
+
xlabel: str = "date",
|
|
23
|
+
ylabel: str = "price",
|
|
24
|
+
pos_color: str = "green",
|
|
25
|
+
neg_color: str = "red",
|
|
26
|
+
bg_color: str = "", # empty string by default because Panel will automatically use the theme color
|
|
27
|
+
grid: bool = True,
|
|
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
|
+
pos_color: the color of the upward candle, hex code is supported
|
|
38
|
+
neg_color: the color of the downward candle, 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 = DEFAULT_NUM_DATA,
|
|
51
|
+
max_data: int | None = None,
|
|
52
|
+
slider_step: int | None = None,
|
|
53
|
+
linked_axes: bool = True,
|
|
54
|
+
update_interval: int = 5000, # ms
|
|
55
|
+
incremental_update: bool = True,
|
|
56
|
+
widgets: 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
|
+
linked_axes: whether to link axes across plots in a layout (plt.layout(...)).
|
|
67
|
+
update_interval: (streaming) interval in ms to update the plot. default is 5000 ms.
|
|
68
|
+
incremental_update: (streaming) whether to update even when the bar is incomplete. default is True.
|
|
69
|
+
widgets: whether to show widgets. default is True.
|
|
70
|
+
For granular control, use remove_widgets() to remove specific widget classes.
|
|
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.candlestick import Candlestick
|
|
89
|
+
from pfund_plot.utils.bokeh import create_bundled_hover_tool
|
|
90
|
+
|
|
91
|
+
_ = hvplot.extension(PlottingBackend.bokeh)
|
|
92
|
+
|
|
93
|
+
date_col = Candlestick.REQUIRED_COLS[0]
|
|
94
|
+
ohlc_cols = Candlestick.REQUIRED_COLS[1:]
|
|
95
|
+
# include optional cols (e.g. volume) only when present in the df
|
|
96
|
+
value_cols = ohlc_cols + [
|
|
97
|
+
col for col in Candlestick.OPTIONAL_COLS if col in df.columns
|
|
98
|
+
]
|
|
99
|
+
return (
|
|
100
|
+
df.to_native()
|
|
101
|
+
.hvplot.ohlc(
|
|
102
|
+
date_col,
|
|
103
|
+
ohlc_cols,
|
|
104
|
+
hover_cols=[date_col, *value_cols],
|
|
105
|
+
tools=[
|
|
106
|
+
create_bundled_hover_tool(
|
|
107
|
+
df, date_col, value_cols, control["datetime_precision"]
|
|
108
|
+
),
|
|
109
|
+
CrosshairTool(dimensions="height", line_color="gray", line_alpha=0.3),
|
|
110
|
+
],
|
|
111
|
+
responsive=True,
|
|
112
|
+
grid=style["grid"],
|
|
113
|
+
pos_color=style["pos_color"],
|
|
114
|
+
neg_color=style["neg_color"],
|
|
115
|
+
bgcolor=style["bg_color"],
|
|
116
|
+
**kwargs,
|
|
117
|
+
)
|
|
118
|
+
.opts(
|
|
119
|
+
title=style["title"],
|
|
120
|
+
xlabel=style["xlabel"],
|
|
121
|
+
ylabel=style["ylabel"],
|
|
122
|
+
height=style["height"],
|
|
123
|
+
)
|
|
124
|
+
)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# pyright: reportUnusedParameter=false
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
import narwhals as nw
|
|
8
|
+
import traitlets
|
|
9
|
+
from anywidget import AnyWidget
|
|
10
|
+
|
|
11
|
+
__all__ = ["CandlestickWidget", "control", "plot", "style"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
DEFAULT_HEIGHT = 280
|
|
15
|
+
DEFAULT_WIDTH = 670
|
|
16
|
+
DEFAULT_NUM_DATA = 150
|
|
17
|
+
|
|
18
|
+
# Live build output in the repo, written by `pixi run js-watch`
|
|
19
|
+
# (vite build --watch). Present only in a source checkout.
|
|
20
|
+
_DEV_BUNDLE = (
|
|
21
|
+
Path(__file__).parents[3] / "js-tap" / "dist" / "components" / "candlestick.js"
|
|
22
|
+
)
|
|
23
|
+
# Packaged copy shipped inside the wheel (`pixi run js-package` copies dist ->
|
|
24
|
+
# pfund_plot/js_tap/components/ before build; `dashboards/` will sit alongside).
|
|
25
|
+
# parents[2] == the pfund_plot package root.
|
|
26
|
+
_DIST_BUNDLE = Path(__file__).parents[2] / "js_tap" / "components" / "candlestick.js"
|
|
27
|
+
|
|
28
|
+
# No PYTHON_ENV / VITE_PORT, no dev server, no port to detect: `_esm` is always a
|
|
29
|
+
# built file. In a source checkout the repo build output exists, so we load that;
|
|
30
|
+
# anywidget watches the file (with ANYWIDGET_HMR=1) and live-reloads on rebuild.
|
|
31
|
+
# Installed from a wheel, only the packaged copy exists, so we load that.
|
|
32
|
+
_ESM = _DEV_BUNDLE if _DEV_BUNDLE.exists() else _DIST_BUNDLE
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CandlestickWidget(AnyWidget):
|
|
36
|
+
_esm = _ESM
|
|
37
|
+
# data plus the two config groups the frontend renders from: `style` (visual)
|
|
38
|
+
# and `control` (behavior). They mirror the Python-side style()/control() dicts,
|
|
39
|
+
# carrying only the keys the chart actually uses (e.g. total_height is a
|
|
40
|
+
# Panel-layer concern and stays out). All are set in __init__, so no
|
|
41
|
+
# default_value is needed.
|
|
42
|
+
data = traitlets.List().tag(sync=True) # list of dicts with OHLC + time
|
|
43
|
+
style = traitlets.Dict().tag(sync=True)
|
|
44
|
+
control = traitlets.Dict().tag(sync=True)
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
df: nw.DataFrame[Any],
|
|
49
|
+
style: dict[str, Any],
|
|
50
|
+
control: dict[str, Any],
|
|
51
|
+
) -> None:
|
|
52
|
+
super().__init__()
|
|
53
|
+
self.style = {
|
|
54
|
+
"height": style["height"],
|
|
55
|
+
"width": style["width"],
|
|
56
|
+
"title": style["title"],
|
|
57
|
+
"xlabel": style["xlabel"],
|
|
58
|
+
"ylabel": style["ylabel"],
|
|
59
|
+
"pos_color": style["pos_color"],
|
|
60
|
+
"neg_color": style["neg_color"],
|
|
61
|
+
"bg_color": style["bg_color"],
|
|
62
|
+
"grid": style["grid"],
|
|
63
|
+
}
|
|
64
|
+
self.control = {"datetime_precision": control["datetime_precision"]}
|
|
65
|
+
self.update_data(df)
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def _format_data(df: nw.DataFrame[Any]) -> list[dict[str, Any]]:
|
|
69
|
+
"""
|
|
70
|
+
Convert dataframe to format supported by lightweight-charts,
|
|
71
|
+
e.g. [ { "open": 10, "high": 10.63, "low": 9.49, "close": 9.55, "time": 1642377600 } ]
|
|
72
|
+
|
|
73
|
+
`time` is sent as a UNIX timestamp in seconds (UTCTimestamp). Stringifying
|
|
74
|
+
the datetime instead (e.g. '2025-01-01 09:00:00') is NOT a format
|
|
75
|
+
lightweight-charts parses for intraday: it collapses every bar within a day
|
|
76
|
+
onto a single point. Epoch seconds keep hourly/minute bars distinct and work
|
|
77
|
+
for daily bars too. `dt.timestamp` yields the UTC epoch (naive datetimes are
|
|
78
|
+
read as UTC wall-clock), which lightweight-charts renders back in UTC, so
|
|
79
|
+
input time == displayed time.
|
|
80
|
+
"""
|
|
81
|
+
if "date" in df.columns:
|
|
82
|
+
# narwhals' dt.timestamp only goes down to milliseconds; floor to seconds.
|
|
83
|
+
df = df.with_columns(time=nw.col("date").dt.timestamp("ms") // 1_000).drop(
|
|
84
|
+
"date"
|
|
85
|
+
)
|
|
86
|
+
return df.rows(named=True)
|
|
87
|
+
|
|
88
|
+
def update_data(self, df: nw.DataFrame[Any]):
|
|
89
|
+
"""Update the widget's data from a DataFrame"""
|
|
90
|
+
self.data = self._format_data(df)
|
|
91
|
+
|
|
92
|
+
def append_data(self, new_df: nw.DataFrame[Any]):
|
|
93
|
+
"""Append new data points for streaming"""
|
|
94
|
+
self.data += self._format_data(new_df)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def style(
|
|
98
|
+
title: str = "Candlestick",
|
|
99
|
+
xlabel: str = "date",
|
|
100
|
+
ylabel: str = "price",
|
|
101
|
+
pos_color: str = "#26a69a",
|
|
102
|
+
neg_color: str = "#ef5350",
|
|
103
|
+
bg_color: str = "white",
|
|
104
|
+
grid: bool = True,
|
|
105
|
+
total_height: int | None = None,
|
|
106
|
+
height: int = DEFAULT_HEIGHT,
|
|
107
|
+
width: int = DEFAULT_WIDTH,
|
|
108
|
+
):
|
|
109
|
+
"""
|
|
110
|
+
Args:
|
|
111
|
+
title: the title shown above the plot. Pass an empty string to hide it.
|
|
112
|
+
(lightweight-charts has no native title, so it's rendered as HTML.)
|
|
113
|
+
xlabel: the x-axis label shown below the plot. Empty string to hide.
|
|
114
|
+
(lightweight-charts has no axis titles, so it's rendered as HTML.)
|
|
115
|
+
ylabel: the y-axis label shown left of the plot. Empty string to hide.
|
|
116
|
+
(lightweight-charts has no axis titles, so it's rendered as HTML.)
|
|
117
|
+
pos_color: the color of the upward candle, hex code is supported
|
|
118
|
+
neg_color: the color of the downward candle, hex code is supported
|
|
119
|
+
bg_color: the background color of the plot, hex code is supported
|
|
120
|
+
grid: whether to show the grid
|
|
121
|
+
total_height: the height of the component (including the figure + widgets)
|
|
122
|
+
height: the height of the figure
|
|
123
|
+
width: the width of the plot
|
|
124
|
+
"""
|
|
125
|
+
return locals()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def control(
|
|
129
|
+
num_data: int = DEFAULT_NUM_DATA,
|
|
130
|
+
max_data: int | None = None,
|
|
131
|
+
slider_step: int | None = None,
|
|
132
|
+
update_interval: int = 5000, # ms
|
|
133
|
+
incremental_update: bool = True,
|
|
134
|
+
widgets: bool = True,
|
|
135
|
+
datetime_precision: Literal["d", "s"] = "s",
|
|
136
|
+
):
|
|
137
|
+
"""
|
|
138
|
+
Args:
|
|
139
|
+
num_data: (DatetimeRangeWidget) initial number of most recent data points to display.
|
|
140
|
+
max_data: (streaming) maximum number of data points kept in memory.
|
|
141
|
+
If None, data will continue to grow unbounded.
|
|
142
|
+
slider_step: (DatetimeRangeWidget) step size in ms for the datetime range slider.
|
|
143
|
+
If None, derived from data resolution.
|
|
144
|
+
update_interval: (streaming) interval in ms to update the plot. default is 5000 ms.
|
|
145
|
+
incremental_update: (streaming) whether to update even when the bar is incomplete. default is True.
|
|
146
|
+
widgets: whether to show widgets. default is True.
|
|
147
|
+
For granular control, use remove_widgets() to remove specific widget classes.
|
|
148
|
+
datetime_precision: the precision of datetime shown on the time axis / crosshair.
|
|
149
|
+
"d" for days (date only), "s" for seconds (default, %H:%M:%S). Sub-second
|
|
150
|
+
("ms") is not supported: the time field is a UTCTimestamp (second-resolution).
|
|
151
|
+
"""
|
|
152
|
+
return locals()
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def plot(
|
|
156
|
+
df: nw.DataFrame[Any],
|
|
157
|
+
style: dict[str, Any],
|
|
158
|
+
control: dict[str, Any],
|
|
159
|
+
**kwargs: Any,
|
|
160
|
+
) -> CandlestickWidget:
|
|
161
|
+
return CandlestickWidget(df, style, control)
|
|
@@ -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 holoviews.core import Dimensioned
|
|
7
|
+
|
|
8
|
+
from pfund_plot.enums import PlottingBackend
|
|
9
|
+
from pfund_plot.plots.plot import BasePlot
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Holoviews(BasePlot):
|
|
13
|
+
SUPPORTED_BACKENDS: ClassVar[list[Literal[PlottingBackend.holoviews]]] = [
|
|
14
|
+
PlottingBackend.holoviews
|
|
15
|
+
]
|
|
16
|
+
REQUIRED_DATA: ClassVar[bool] = False
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self, fig: Dimensioned, sizing_mode: str | None = None, **pane_kwargs: Any
|
|
20
|
+
):
|
|
21
|
+
"""
|
|
22
|
+
Args:
|
|
23
|
+
fig: A Holoviews Dimensioned 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.HoloViews,
|
|
26
|
+
e.g. height, width, max_width, margin.
|
|
27
|
+
"""
|
|
28
|
+
super().__init__(data=None)
|
|
29
|
+
self._plot: Dimensioned = 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,43 @@
|
|
|
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.enums import PlottingBackend
|
|
10
|
+
from pfund_plot.plots.plot import BasePlot
|
|
11
|
+
|
|
12
|
+
__all__ = ["Label"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LabelStyle:
|
|
16
|
+
from pfund_plot.plots.label.bokeh import style as bokeh_style
|
|
17
|
+
|
|
18
|
+
bokeh = bokeh_style
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LabelControl:
|
|
22
|
+
from pfund_plot.plots.label.bokeh import control as bokeh_control
|
|
23
|
+
|
|
24
|
+
bokeh = bokeh_control
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Label(BasePlot):
|
|
28
|
+
SUPPORTED_BACKENDS: ClassVar[list[PlottingBackend]] = [PlottingBackend.bokeh]
|
|
29
|
+
style = LabelStyle
|
|
30
|
+
control = LabelControl
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
data: IntoFrame,
|
|
35
|
+
text: str,
|
|
36
|
+
x: str | None = None,
|
|
37
|
+
y: str | list[str] | None = None,
|
|
38
|
+
name: str | None = None,
|
|
39
|
+
**reactive_params: Any,
|
|
40
|
+
):
|
|
41
|
+
self._text = text
|
|
42
|
+
super().__init__(data=data, x=x, y=y, name=name, **reactive_params)
|
|
43
|
+
self._plot_kwargs["text"] = text
|
|
@@ -0,0 +1,89 @@
|
|
|
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 = "black"
|
|
17
|
+
DEFAULT_FONT_SIZE = "10pt"
|
|
18
|
+
DEFAULT_HEIGHT = 280
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def style(
|
|
22
|
+
title: str = "Label",
|
|
23
|
+
xlabel: str = "",
|
|
24
|
+
ylabel: str = "",
|
|
25
|
+
color: str = DEFAULT_COLOR,
|
|
26
|
+
font_size: str = DEFAULT_FONT_SIZE,
|
|
27
|
+
text_align: Literal["left", "center", "right"] = "center",
|
|
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
|
+
color: text color
|
|
38
|
+
font_size: text font size (e.g. '10pt', '12px')
|
|
39
|
+
text_align: horizontal text alignment ('left', 'center', 'right')
|
|
40
|
+
total_height: the height of the component (including the figure + widgets).
|
|
41
|
+
Default 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
|
+
"""
|
|
45
|
+
return locals()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def control(
|
|
49
|
+
widgets: bool = True,
|
|
50
|
+
linked_axes: bool = True,
|
|
51
|
+
):
|
|
52
|
+
"""
|
|
53
|
+
Args:
|
|
54
|
+
widgets: whether to show widgets. default is True.
|
|
55
|
+
linked_axes: whether to link axes across plots in a layout (plt.layout(...)).
|
|
56
|
+
"""
|
|
57
|
+
return locals()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def plot(
|
|
61
|
+
df: nw.DataFrame[Any],
|
|
62
|
+
style: dict[str, Any],
|
|
63
|
+
control: dict[str, Any],
|
|
64
|
+
x: str | None = None,
|
|
65
|
+
y: str | list[str] | None = None,
|
|
66
|
+
**kwargs: Any,
|
|
67
|
+
) -> Overlay:
|
|
68
|
+
import hvplot
|
|
69
|
+
|
|
70
|
+
_ = hvplot.extension(PlottingBackend.bokeh)
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
df.to_native()
|
|
74
|
+
.hvplot.labels(
|
|
75
|
+
x=x,
|
|
76
|
+
y=y,
|
|
77
|
+
responsive=True,
|
|
78
|
+
**kwargs,
|
|
79
|
+
)
|
|
80
|
+
.opts(
|
|
81
|
+
title=style["title"],
|
|
82
|
+
xlabel=style["xlabel"],
|
|
83
|
+
ylabel=style["ylabel"],
|
|
84
|
+
height=style["height"],
|
|
85
|
+
text_color=style["color"],
|
|
86
|
+
text_font_size=style["font_size"],
|
|
87
|
+
text_align=style["text_align"],
|
|
88
|
+
)
|
|
89
|
+
)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from pfund_kit.style import RichColor, TextStyle, cprint
|
|
2
|
+
|
|
3
|
+
from pfund_plot.enums import DisplayMode
|
|
4
|
+
from pfund_plot.plots.layout.layout import BaseLayout
|
|
5
|
+
from pfund_plot.plots.lazy import LazyPlot
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LayoutStyle:
|
|
9
|
+
from pfund_plot.plots.layout.panel import style as panel_style
|
|
10
|
+
|
|
11
|
+
panel = panel_style
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LayoutControl:
|
|
15
|
+
from pfund_plot.plots.layout.panel import control as panel_control
|
|
16
|
+
|
|
17
|
+
panel = panel_control
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Layout(BaseLayout):
|
|
21
|
+
_MAX_COLS = 12 # maximum number of columns in a grid when using GridStack
|
|
22
|
+
|
|
23
|
+
style = LayoutStyle
|
|
24
|
+
control = LayoutControl
|
|
25
|
+
|
|
26
|
+
def __init__(self, *plots: LazyPlot): # pyright: ignore[reportInconsistentConstructor]
|
|
27
|
+
super().__init__(*plots)
|
|
28
|
+
default_mode = DisplayMode.browser
|
|
29
|
+
self._set_mode(default_mode)
|
|
30
|
+
|
|
31
|
+
def _render(self):
|
|
32
|
+
if self._mode == DisplayMode.desktop:
|
|
33
|
+
cprint(
|
|
34
|
+
"There is a known issue in resizing when using plt.layout (GridStack) in desktop mode. Please consider switching to browser mode instead.",
|
|
35
|
+
style=TextStyle.BOLD + RichColor.YELLOW,
|
|
36
|
+
)
|
|
37
|
+
return super()._render()
|
|
38
|
+
|
|
39
|
+
def _validate_grid_specs(self):
|
|
40
|
+
# Check grid_spec consistency: either all plots have it or none do
|
|
41
|
+
grid_specs = [plot._grid_spec for plot in self._plots]
|
|
42
|
+
has_grid_spec = [grid_spec is not None for grid_spec in grid_specs]
|
|
43
|
+
all_has_grid_spec = all(has_grid_spec)
|
|
44
|
+
if any(has_grid_spec):
|
|
45
|
+
if not all_has_grid_spec:
|
|
46
|
+
raise ValueError("All plots must have grid_spec defined if any do.")
|
|
47
|
+
else:
|
|
48
|
+
# REVIEW: somehow when column index exceeds 12, GridStack doesn't work, columns must be within 0-12
|
|
49
|
+
if any(
|
|
50
|
+
grid_spec[1].stop is not None and grid_spec[1].stop > self._MAX_COLS
|
|
51
|
+
for grid_spec in grid_specs
|
|
52
|
+
):
|
|
53
|
+
raise ValueError(
|
|
54
|
+
f"Column index exceeds maximum of {self._MAX_COLS}. Grid uses a {self._MAX_COLS}-column system."
|
|
55
|
+
)
|
|
56
|
+
assert self._control is not None, "control is not set"
|
|
57
|
+
assert self._control["num_cols"] <= self._MAX_COLS, (
|
|
58
|
+
f"'num_cols' must be less than or equal to {self._MAX_COLS}"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def _warn_if_widgets_with_drag(self):
|
|
62
|
+
"""Warn if child plots have widgets and GridStack drag is enabled.
|
|
63
|
+
|
|
64
|
+
GridStack's drag handler intercepts pointer events, which prevents
|
|
65
|
+
click-based widgets (e.g. Select dropdowns, buttons) from working.
|
|
66
|
+
Drag-based widgets (e.g. sliders) are unaffected.
|
|
67
|
+
"""
|
|
68
|
+
if self._control is None or not self._control.get("allow_drag", True):
|
|
69
|
+
return
|
|
70
|
+
for lazyplot in self._plots:
|
|
71
|
+
plot = lazyplot._plot
|
|
72
|
+
has_widgets = (
|
|
73
|
+
plot._widgets or plot._streaming_widgets or plot._reactive_widgets
|
|
74
|
+
)
|
|
75
|
+
if not has_widgets:
|
|
76
|
+
for overlay in plot._overlays:
|
|
77
|
+
has_widgets = (
|
|
78
|
+
overlay._widgets
|
|
79
|
+
or overlay._streaming_widgets
|
|
80
|
+
or overlay._reactive_widgets
|
|
81
|
+
)
|
|
82
|
+
if has_widgets:
|
|
83
|
+
break
|
|
84
|
+
if has_widgets:
|
|
85
|
+
cprint(
|
|
86
|
+
"Widgets detected inside Layout with allow_drag=True. Click-based widgets (e.g. dropdowns, buttons) may not work. "
|
|
87
|
+
+ "Use plt.layout(...).control(allow_drag=False) to fix this.",
|
|
88
|
+
style=TextStyle.BOLD + RichColor.YELLOW,
|
|
89
|
+
)
|
|
90
|
+
break
|
|
91
|
+
|
|
92
|
+
def _create_component(self):
|
|
93
|
+
self._validate_grid_specs()
|
|
94
|
+
super()._create_component()
|
|
95
|
+
|
|
96
|
+
def _create(self):
|
|
97
|
+
super()._create()
|
|
98
|
+
self._warn_if_widgets_with_drag()
|