PySide6Plot 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.
@@ -0,0 +1,187 @@
1
+ from PySide6.QtGui import QResizeEvent
2
+ from PySide6.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout
3
+ from PySide6.QtWidgets import QGridLayout, QSizePolicy
4
+ from qfluentwidgets import TransparentToggleToolButton, FluentIcon
5
+ from .widgets.q_plot_widget import QPlotWidget
6
+ from .widgets.navigation_widget import SegmentedInterface
7
+ from .compoents.zoom_move import (
8
+ StockWidgetZoomBar,
9
+ StockWidgetHorizontalScroller,
10
+ StockWidgetVerticalScroller,
11
+ )
12
+ from .compoents.draw_line import DrawLineComponent
13
+ from .compoents.frame_recorder import FrameRecorderComponent
14
+ from .compoents.average_line import AverageLineComponent
15
+ from .libs.plot_item import get_plot_item, AdaptiveGraphObject
16
+ from .libs.style import QStockIcon, set_background_with_theme, qconfig
17
+ from .libs.data_handler import PricesDataFrame, VolumeDataFrame, TradeData
18
+
19
+
20
+ class QStockPlotter(QWidget):
21
+ def __init__(self, show_zoom_bar=True, parent=None) -> None:
22
+ super().__init__(parent)
23
+
24
+ self.main_item: AdaptiveGraphObject | None = None
25
+
26
+ self.setMinimumSize(450, 200)
27
+ self.main_plotter = QPlotWidget(self)
28
+
29
+ self.y_scroller = StockWidgetVerticalScroller(self.main_plotter, parent=self)
30
+ self.x_scroller = StockWidgetHorizontalScroller(self.main_plotter, parent=self)
31
+ if show_zoom_bar:
32
+ self.zoom_bar = StockWidgetZoomBar(self.main_plotter, parent=self)
33
+
34
+ self.draw_line_component = DrawLineComponent(self.main_plotter, parent=self)
35
+ self.draw_line_command_bar, self.draw_line_table = self.draw_line_component.get_widget()
36
+ self.draw_line_table.show()
37
+
38
+ self.frame_recorder_component = FrameRecorderComponent(self.main_plotter, parent=self)
39
+ self.saved_frame_table = self.frame_recorder_component.get_widget()
40
+ self.saved_frame_table.show()
41
+
42
+ self.average_line_component = AverageLineComponent(self.main_plotter, parent=self)
43
+ self.average_line_command_bar = self.average_line_component.get_widget()
44
+
45
+ # self.navigation_widget = PivotInterface(parent=self)
46
+ self.navigation_widget = SegmentedInterface(parent=self)
47
+ self.navigation_widget.addSubInterface(self.draw_line_table, "saved_line_table", "Lines")
48
+ self.navigation_widget.addSubInterface(self.saved_frame_table, "saved_frame_table", "Frames")
49
+ self.navigation_widget.hide()
50
+ self.show_up_button = TransparentToggleToolButton(QStockIcon.CHEVRON_LEFT)
51
+ self.show_up_button.setFixedWidth(10)
52
+ self.show_up_button.clicked.connect(self.__on_show_up_button_clicked)
53
+
54
+ self.main_plotter.insert_context_menu(self.frame_recorder_component.jump_to_menu)
55
+ self.main_plotter.insert_context_menu(self.frame_recorder_component.record_current_frame_action)
56
+ self.main_plotter.insert_context_menu(self.draw_line_component.menu_action)
57
+
58
+ self.main_layout = QHBoxLayout(self)
59
+ self.main_layout.setContentsMargins(0, 0, 0, 0)
60
+ self.main_layout.setSpacing(0)
61
+ self.setLayout(self.main_layout)
62
+ self.inner_layout = QVBoxLayout()
63
+ self.inner_layout.setContentsMargins(0, 0, 0, 0)
64
+ self.inner_layout.setSpacing(0)
65
+ self.plotter_grid_layout = QGridLayout()
66
+ self.plotter_grid_layout.setContentsMargins(0, 0, 0, 0)
67
+ self.plotter_grid_layout.setSpacing(0)
68
+
69
+ self.main_layout.addLayout(self.inner_layout)
70
+ self.main_layout.addWidget(self.show_up_button)
71
+ self.main_layout.addWidget(self.navigation_widget)
72
+ self.inner_layout.addWidget(self.draw_line_command_bar)
73
+ self.inner_layout.addLayout(self.plotter_grid_layout)
74
+ self.inner_layout.addWidget(self.average_line_command_bar)
75
+ self.plotter_grid_layout.addWidget(self.main_plotter, 0, 0, 1, 1)
76
+ self.plotter_grid_layout.addWidget(self.y_scroller, 0, 1, 1, 1)
77
+ self.plotter_grid_layout.addWidget(self.x_scroller, 1, 0, 1, 2)
78
+
79
+ for m in [
80
+ "update_plot",
81
+ "move_y_loc",
82
+ "set_zoom_model",
83
+ "set_y_loc_model",
84
+ "set_full_range_enabled",
85
+ "set_x_range",
86
+ "move_to_end",
87
+ "move_to_start",
88
+ "full_range",
89
+ ]:
90
+ setattr(self, m, getattr(self.main_plotter, m))
91
+
92
+ set_background_with_theme(self)
93
+
94
+ qconfig.themeChanged.connect(lambda theme: set_background_with_theme(self, theme))
95
+
96
+ def add_main_item(self, plot_item, x_ticks=None, y_ticks=None):
97
+ if self.main_item is not None:
98
+ raise Exception("Main item already exists. There can only be one main item.")
99
+ self.main_item = plot_item
100
+ self.main_plotter.add_item(plot_item, x_ticks, y_ticks)
101
+ # self.average_line_component.add_default_average_lines()
102
+
103
+ def remove_main_item(self):
104
+ if self.main_item is None:
105
+ raise Exception("No main item exists.")
106
+ self.main_plotter.remove_item(self.main_item)
107
+ self.main_item = None
108
+
109
+ def __on_show_up_button_clicked(self):
110
+ if self.navigation_widget.isHidden():
111
+ self.navigation_widget.show()
112
+ self.show_up_button.setIcon(FluentIcon.CHEVRON_RIGHT)
113
+ else:
114
+ self.navigation_widget.hide()
115
+ self.show_up_button.setIcon(QStockIcon.CHEVRON_LEFT)
116
+
117
+ def resizeEvent(self, a0: QResizeEvent) -> None:
118
+ self.navigation_widget.setFixedWidth(min(int(self.width() * 0.3), 350))
119
+ return super().resizeEvent(a0)
120
+
121
+ def update_plot(self, x_loc: float | None = None, x_range: float | None = None):
122
+ self.main_plotter.update_plot(x_loc, x_range)
123
+
124
+
125
+ class PriceVolumePlotter(QWidget):
126
+ def __init__(self, parent=None):
127
+ super().__init__(parent)
128
+ self.main_layout = QVBoxLayout(self)
129
+ self.main_layout.setContentsMargins(8, 8, 8, 8)
130
+ self.main_layout.setSpacing(8)
131
+ self.setLayout(self.main_layout)
132
+
133
+ self.price_plotter = QStockPlotter(show_zoom_bar=True)
134
+ self.volume_plotter = QStockPlotter(show_zoom_bar=True)
135
+
136
+ self.price_plotter.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
137
+ self.volume_plotter.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
138
+ self.main_layout.addWidget(self.price_plotter, stretch=3)
139
+ self.main_layout.addWidget(self.volume_plotter, stretch=1)
140
+
141
+ self.price_plotter.main_plotter.sigViewChanged.connect(
142
+ lambda: self.__on_view_changed(self.price_plotter.main_plotter)
143
+ )
144
+ self.volume_plotter.main_plotter.sigViewChanged.connect(
145
+ lambda: self.__on_view_changed(self.volume_plotter.main_plotter)
146
+ )
147
+
148
+ set_background_with_theme(self)
149
+
150
+ qconfig.themeChanged.connect(lambda theme: set_background_with_theme(self, theme))
151
+
152
+ def __on_view_changed(self, active_plot_widget):
153
+ if active_plot_widget == self.price_plotter.main_plotter:
154
+ self.volume_plotter.update_plot(
155
+ x_loc=active_plot_widget.viewRect().left(),
156
+ x_range=active_plot_widget.viewRect().width(),
157
+ )
158
+ else:
159
+ self.price_plotter.update_plot(
160
+ x_loc=active_plot_widget.viewRect().left(),
161
+ x_range=active_plot_widget.viewRect().width(),
162
+ )
163
+
164
+ def plot_price_volume(self, price_data: PricesDataFrame, volume_data: VolumeDataFrame):
165
+ price_item = get_plot_item(price_data)
166
+ self.price_plotter.add_main_item(price_item, x_ticks=price_item.get_x_ticks())
167
+ volume_item = get_plot_item(volume_data)
168
+ self.volume_plotter.add_main_item(volume_item, x_ticks=volume_item.get_x_ticks())
169
+
170
+ def plot_trade_data(self, trade_data: TradeData):
171
+ self.plot_price_volume(trade_data.prices, trade_data.volume)
172
+
173
+ def update_plot(self, x_loc: float | None = None, x_range: float | None = None):
174
+ self.price_plotter.update_plot(x_loc, x_range)
175
+
176
+ def set_x_range(self, x_loc: float | None = None, x_range: float | None = None):
177
+ self.price_plotter.set_x_range(x_loc, x_range)
178
+ self.volume_plotter.set_x_range(x_loc, x_range)
179
+
180
+ def move_to_end(self):
181
+ self.price_plotter.move_to_end()
182
+
183
+ def move_to_start(self):
184
+ self.price_plotter.move_to_start()
185
+
186
+ def full_range(self):
187
+ self.price_plotter.full_range()
@@ -0,0 +1,3 @@
1
+ """
2
+ Functional components for plotters.
3
+ """
@@ -0,0 +1,232 @@
1
+ import numpy as np
2
+ from PySide6.QtGui import QContextMenuEvent, QColor, QIcon
3
+ from PySide6.QtCore import Qt, Signal
4
+ from PySide6.QtWidgets import QWidget
5
+ from qfluentwidgets import CommandBar, FluentIcon, Action, FluentIconBase, qconfig, RoundMenu
6
+ from qfluentwidgets.common.overload import singledispatchmethod
7
+ import pyqtgraph as pg
8
+ from pyqtgraph import PlotCurveItem
9
+ from ..widgets.q_plot_widget import QPlotWidget
10
+ from ..widgets.colorful_toggle_button import ColorfulToggleButton
11
+ from ..widgets.value_select_box import NewAverageLineBox
12
+
13
+
14
+ class AverageLineItem(PlotCurveItem):
15
+ """
16
+ A class representing an average line item on a plot.
17
+
18
+ Attributes:
19
+ num_average_data (int): The number of data points to use for calculating the average.
20
+ """
21
+
22
+ def __init__(self, data, num_average_data: int, color: QColor, line_width: float):
23
+ """
24
+ Initializes an AverageLineItem object.
25
+
26
+ Args:
27
+ data (array-like): The data points for which the average line is calculated.
28
+ num_average_data (int): The number of data points to use for calculating the average.
29
+ color (str): The color of the average line.
30
+ line_width (float): The width of the average line.
31
+ """
32
+ super().__init__(
33
+ pen=pg.mkPen(color, width=line_width),
34
+ clickable=False,
35
+ x=np.arange(num_average_data - 1, len(data)),
36
+ y=np.asarray([np.mean(data[i : i + num_average_data]) for i in range(len(data) - num_average_data + 1)]),
37
+ )
38
+ self.num_average_data = num_average_data
39
+
40
+ def get_local_plot_range(self, start: float, end: float):
41
+ """
42
+ Returns the minimum and maximum values of the average line within the specified range.
43
+
44
+ Args:
45
+ start (float): The start value of the range.
46
+ end (float): The end value of the range.
47
+
48
+ Returns:
49
+ tuple: A tuple containing the minimum and maximum values of the average line within the range.
50
+ If there are no data points within the range, returns None.
51
+ """
52
+ _, ys = self.getData()
53
+ ys = ys[max(0, int(start - self.num_average_data)) : max(0, int(end - self.num_average_data))]
54
+ if len(ys) > 0:
55
+ return np.min(ys), np.max(ys)
56
+ else:
57
+ return None
58
+
59
+
60
+ class AverageLineButton(ColorfulToggleButton):
61
+ """
62
+ A button widget that represents an average line.
63
+
64
+ This button provides functionality for removing the average line and displays a context menu when right-clicked.
65
+
66
+ Signals:
67
+ - sigRemoveClicked: Emitted when the remove action is triggered.
68
+
69
+ Args:
70
+ parent (QWidget): The parent widget.
71
+
72
+ Overloaded Methods:
73
+ - __init__(self, parent: QWidget = None)
74
+ - __init__(self, color: QColor, text: str, parent: QWidget = None, icon: Union[QIcon, FluentIconBase, str] = None)
75
+ - __init__(self, icon: QIcon, color: QColor, text: str, parent: QWidget = None)
76
+ - __init__(self, icon: FluentIconBase, color: QColor, text: str, parent: QWidget = None)
77
+ """
78
+
79
+ sigRemoveClicked = Signal()
80
+
81
+ @singledispatchmethod
82
+ def __init__(self, parent: QWidget = None):
83
+ super().__init__(parent)
84
+ self.color = None
85
+ qconfig.themeChangedFinished.connect(self.set_color)
86
+ self.context_menu = RoundMenu("Context Menu")
87
+ self.remove_action = Action("Remove")
88
+ self.remove_action.setIcon(FluentIcon.DELETE)
89
+ self.remove_action.triggered.connect(self.sigRemoveClicked.emit)
90
+ self.context_menu.addAction(self.remove_action)
91
+
92
+ @__init__.register
93
+ def _(self, color: QColor, text: str, parent: QWidget = None, icon: QIcon | FluentIconBase | str = None):
94
+ self.__init__(parent)
95
+ self.setText(text)
96
+ self.setIcon(icon)
97
+ self.color = color
98
+ self.set_color()
99
+
100
+ @__init__.register
101
+ def _(self, icon: QIcon, color: QColor, text: str, parent: QWidget = None):
102
+ self.__init__(color, text, parent, icon)
103
+
104
+ @__init__.register
105
+ def _(self, icon: FluentIconBase, color: QColor, text: str, parent: QWidget = None):
106
+ self.__init__(color, text, parent, icon)
107
+
108
+ def contextMenuEvent(self, a0: QContextMenuEvent) -> None:
109
+ self.context_menu.exec(a0.globalPos())
110
+
111
+
112
+ class AverageLineComponent:
113
+ """
114
+ A component that handles the addition and removal of average lines on a plot.
115
+ """
116
+
117
+ def __init__(self, plot_widget: QPlotWidget, parent=None) -> None:
118
+ """
119
+ Initializes the AverageLineComponent object.
120
+
121
+ Parameters:
122
+ - plot_widget (QPlotWidget): The plot widget where the average lines are added.
123
+ - parent: The parent object of the component.
124
+ """
125
+ self.parent = parent
126
+ self.plot_widget = plot_widget
127
+ self.plot_items_bar = CommandBar(parent=self.parent)
128
+ self.plot_items_bar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
129
+ self.plot_items_bar.setFixedHeight(25)
130
+ self.plot_items_bar.addSeparator()
131
+ self.add_line_action = Action("Add average line")
132
+ self.add_line_action.setIcon(FluentIcon.ADD)
133
+ self.add_line_action.triggered.connect(self.__on_add_line_action_clicked)
134
+ self.plot_items_bar.addAction(self.add_line_action)
135
+ self.show_all_line_action = Action("Show all average lines")
136
+ self.show_all_line_action.setIcon(FluentIcon.VIEW)
137
+ self.show_all_line_action.triggered.connect(self.show_all_average_lines)
138
+ self.plot_items_bar.addAction(self.show_all_line_action)
139
+ self.hide_all_line_action = Action("Hide all average lines")
140
+ self.hide_all_line_action.setIcon(FluentIcon.HIDE)
141
+ self.hide_all_line_action.triggered.connect(self.hide_all_average_lines)
142
+ self.plot_items_bar.addAction(self.hide_all_line_action)
143
+
144
+ self.average_lines = {}
145
+
146
+ def __on_add_line_action_clicked(self):
147
+ """
148
+ Handles the event when the add line action is clicked.
149
+ Shows a dialog box for selecting the number of data points and color for the average line.
150
+ Adds the average line to the plot.
151
+ """
152
+ msg = NewAverageLineBox(
153
+ num_data=len(self.parent.main_item.get_feature_value()),
154
+ existed_values=self.average_lines.keys(),
155
+ parent=self.parent,
156
+ )
157
+ if msg.exec():
158
+ self.add_average_line(int(msg.value_edit.text()), msg.color_selector.current_item)
159
+
160
+ def get_widget(self):
161
+ """
162
+ Returns the plot_items_bar widget.
163
+
164
+ Returns:
165
+ - QWidget: The plot_items_bar widget.
166
+ """
167
+ return self.plot_items_bar
168
+
169
+ def add_average_line(self, num_average_data, color):
170
+ """
171
+ Adds an average line to the plot.
172
+
173
+ Parameters:
174
+ - num_average_data (int): The number of data points to calculate the average.
175
+ - color: The color of the average line.
176
+ """
177
+ if not hasattr(self.parent, "main_item"):
178
+ raise ValueError("The parent plotter has no main item")
179
+ if self.parent.main_item is None:
180
+ raise ValueError("The main item of the parent plotter is None")
181
+ data = self.parent.main_item.get_feature_value()
182
+ style = self.parent.main_item.style
183
+ average_line = AverageLineItem(data, num_average_data, color, style.line_width)
184
+ self.average_lines[num_average_data] = average_line
185
+ self.plot_widget.add_item(average_line)
186
+ toggle_button = AverageLineButton(color=color, parent=self.plot_items_bar, text=f"MA{num_average_data}")
187
+ toggle_button.setFixedHeight(25)
188
+ toggle_button.setChecked(True)
189
+
190
+ def on_toggle_button_clicked():
191
+ if toggle_button.isChecked():
192
+ self.plot_widget.add_item(average_line)
193
+ self.average_lines[num_average_data] = average_line
194
+ else:
195
+ self.plot_widget.remove_item(average_line)
196
+ self.average_lines.pop(num_average_data)
197
+
198
+ def on_remove_button_clicked():
199
+ self.plot_widget.remove_item(average_line)
200
+ self.average_lines.pop(num_average_data)
201
+ self.plot_items_bar.removeWidget(toggle_button)
202
+ toggle_button.deleteLater()
203
+
204
+ toggle_button.clicked.connect(on_toggle_button_clicked)
205
+ toggle_button.sigRemoveClicked.connect(on_remove_button_clicked)
206
+ self.plot_items_bar._insertWidgetToLayout(len(self.plot_items_bar._widgets) - 4, toggle_button)
207
+
208
+ def add_default_average_lines(self):
209
+ """
210
+ Adds the default average lines based on the style of the main item.
211
+ """
212
+ style = self.parent.main_item.style
213
+ for num_average_data in style.average_line_color.keys():
214
+ self.add_average_line(num_average_data, style.average_line_color[num_average_data])
215
+
216
+ def show_all_average_lines(self):
217
+ """
218
+ Shows all the average lines.
219
+ """
220
+ for item in self.plot_items_bar._widgets:
221
+ if isinstance(item, AverageLineButton):
222
+ if not item.isChecked():
223
+ item.click()
224
+
225
+ def hide_all_average_lines(self):
226
+ """
227
+ Hides all the average lines.
228
+ """
229
+ for item in self.plot_items_bar._widgets:
230
+ if isinstance(item, AverageLineButton):
231
+ if item.isChecked():
232
+ item.click()