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.
- PySide6Plot/__init__.py +187 -0
- PySide6Plot/compoents/__init__.py +3 -0
- PySide6Plot/compoents/average_line.py +232 -0
- PySide6Plot/compoents/draw_line.py +624 -0
- PySide6Plot/compoents/frame_recorder.py +213 -0
- PySide6Plot/compoents/zoom_move.py +160 -0
- PySide6Plot/libs/__init__.py +3 -0
- PySide6Plot/libs/constant.py +19 -0
- PySide6Plot/libs/data_handler.py +230 -0
- PySide6Plot/libs/helpers.py +401 -0
- PySide6Plot/libs/plot_item.py +233 -0
- PySide6Plot/libs/style.py +119 -0
- PySide6Plot/resources/icons/ChevronLeft_black.svg +44 -0
- PySide6Plot/resources/icons/ChevronLeft_white.svg +44 -0
- PySide6Plot/resources/qss/dark/navigation_view_interface.qss +16 -0
- PySide6Plot/resources/qss/light/navigation_view_interface.qss +16 -0
- PySide6Plot/widgets/__init__.py +3 -0
- PySide6Plot/widgets/colorful_toggle_button.py +131 -0
- PySide6Plot/widgets/fluent_scroller.py +450 -0
- PySide6Plot/widgets/line_card.py +138 -0
- PySide6Plot/widgets/navigation_widget.py +49 -0
- PySide6Plot/widgets/q_plot_widget.py +750 -0
- PySide6Plot/widgets/removable_table.py +259 -0
- PySide6Plot/widgets/transparent_Line_edit.py +69 -0
- PySide6Plot/widgets/transparent_selector.py +208 -0
- PySide6Plot/widgets/value_select_box.py +369 -0
- PySide6Plot/widgets/zoom_bar.py +136 -0
- pyside6plot-0.0.1.dist-info/METADATA +37 -0
- pyside6plot-0.0.1.dist-info/RECORD +31 -0
- pyside6plot-0.0.1.dist-info/WHEEL +5 -0
- pyside6plot-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
from qfluentwidgets import FluentIcon, Action, RoundMenu, MessageBox
|
|
2
|
+
import pyqtgraph as pg
|
|
3
|
+
from ..widgets.q_plot_widget import QPlotWidget
|
|
4
|
+
from ..widgets.value_select_box import select_str, select_limited_str
|
|
5
|
+
from ..widgets.removable_table import RemovableTable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SavedFrameTable(RemovableTable):
|
|
9
|
+
"""
|
|
10
|
+
Table widget for displaying saved frames.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, parent=None):
|
|
14
|
+
super().__init__(
|
|
15
|
+
parent=parent,
|
|
16
|
+
show_row_index=False,
|
|
17
|
+
resize_columns_to_contents=False,
|
|
18
|
+
show_move_up_down_buttons=False,
|
|
19
|
+
show_delete_button=True,
|
|
20
|
+
show_rename_button=True,
|
|
21
|
+
accept_name="Jump to",
|
|
22
|
+
accept_icon=FluentIcon.CHEVRON_RIGHT,
|
|
23
|
+
link_default_slot=True,
|
|
24
|
+
)
|
|
25
|
+
self.set_header_labels(["Name", "Location", "Range"])
|
|
26
|
+
self.setColumnWidth(0, 100)
|
|
27
|
+
self.setColumnWidth(1, 150)
|
|
28
|
+
self.setColumnWidth(2, 150)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FrameRecorderComponent:
|
|
32
|
+
"""
|
|
33
|
+
A Component for recording and managing frames in a plot widget.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, plot_widget: QPlotWidget, parent=None) -> None:
|
|
37
|
+
"""
|
|
38
|
+
Initialize the FrameRecorderComponent.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
plot_widget (QPlotWidget): The plot widget to record frames from.
|
|
42
|
+
parent (QWidget, optional): The parent widget. Defaults to None.
|
|
43
|
+
"""
|
|
44
|
+
self.parent = parent
|
|
45
|
+
self.plot_widget = plot_widget
|
|
46
|
+
view_rect = self.plot_widget.viewRect()
|
|
47
|
+
self.current_x_loc = view_rect.left()
|
|
48
|
+
self.current_x_range = view_rect.width()
|
|
49
|
+
self.record_current_frame_change = True
|
|
50
|
+
self.previous_frame = []
|
|
51
|
+
self.saved_frame = []
|
|
52
|
+
self.num_previous_frame = 30
|
|
53
|
+
self.full_range_action = Action(FluentIcon.FULL_SCREEN, "Full range")
|
|
54
|
+
self.latest_frame_action = Action(FluentIcon.RIGHT_ARROW, "Latest")
|
|
55
|
+
self.previous_frame_action = Action(FluentIcon.CANCEL, "Previous frame")
|
|
56
|
+
self.given_frame_action = Action(FluentIcon.LABEL, "Input coordinate")
|
|
57
|
+
self.record_current_frame_action = Action(FluentIcon.ADD_TO, "Record current frame")
|
|
58
|
+
self.jump_to_menu = RoundMenu("Jump to", self.parent)
|
|
59
|
+
self.jump_to_menu.setIcon(FluentIcon.CHEVRON_RIGHT)
|
|
60
|
+
self.jump_to_menu.addAction(self.previous_frame_action)
|
|
61
|
+
self.jump_to_menu.addAction(self.full_range_action)
|
|
62
|
+
self.jump_to_menu.addAction(self.latest_frame_action)
|
|
63
|
+
self.jump_to_menu.addAction(self.given_frame_action)
|
|
64
|
+
self.saved_frame_table = SavedFrameTable(self.parent)
|
|
65
|
+
self.saved_frame_table.sigAcceptClicked.connect(self.__on_table_jump_to_clicked)
|
|
66
|
+
self.saved_frame_table.sigRowClicked.connect(self.__on_table_jump_to_clicked)
|
|
67
|
+
self.saved_frame_table.sigNameEdited.connect(self.__on_table_name_changed)
|
|
68
|
+
self.saved_frame_table.sigDeleteClicked.connect(self.__on_table_row_deleted)
|
|
69
|
+
self.saved_frame_table.hide()
|
|
70
|
+
|
|
71
|
+
self.__init_connections()
|
|
72
|
+
|
|
73
|
+
def __init_connections(self):
|
|
74
|
+
"""
|
|
75
|
+
Initialize the signal connections.
|
|
76
|
+
"""
|
|
77
|
+
self.full_range_action.triggered.connect(
|
|
78
|
+
lambda: self.plot_widget.update_plot(x_loc=self.plot_widget.x_start, x_range=self.plot_widget.x_range_max)
|
|
79
|
+
)
|
|
80
|
+
self.latest_frame_action.triggered.connect(
|
|
81
|
+
lambda: self.plot_widget.update_plot(
|
|
82
|
+
x_loc=self.plot_widget.x_end - self.plot_widget.viewRect().width(),
|
|
83
|
+
x_range=self.plot_widget.viewRect().width(),
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
self.previous_frame_action.triggered.connect(self.__on_previous_frame_clicked)
|
|
87
|
+
self.given_frame_action.triggered.connect(self.__on_given_frame_clicked)
|
|
88
|
+
self.record_current_frame_action.triggered.connect(self.__on_record_current_frame_clicked)
|
|
89
|
+
self.pg_plotter_view_changed_slot = pg.SignalProxy(
|
|
90
|
+
self.plot_widget.sigRangeChanged, rateLimit=100, slot=self.__on_pg_plotter_view_changed
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def __on_pg_plotter_view_changed(self, event):
|
|
94
|
+
"""
|
|
95
|
+
Handle the event when the plot view changes.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
event: The event object.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
None
|
|
102
|
+
"""
|
|
103
|
+
if self.record_current_frame_change: # move plot will also trigger this event
|
|
104
|
+
view_rect = self.plot_widget.viewRect()
|
|
105
|
+
if view_rect.left() != self.current_x_loc or view_rect.width() != self.current_x_range:
|
|
106
|
+
self.current_x_loc = view_rect.left()
|
|
107
|
+
self.current_x_range = view_rect.width()
|
|
108
|
+
if len(self.previous_frame) == self.num_previous_frame:
|
|
109
|
+
self.previous_frame.pop(0)
|
|
110
|
+
self.previous_frame.append((self.current_x_loc, self.current_x_range))
|
|
111
|
+
else:
|
|
112
|
+
self.previous_frame.append((self.current_x_loc, self.current_x_range))
|
|
113
|
+
if len(self.previous_frame) > 1:
|
|
114
|
+
self.previous_frame_action.setVisible(True)
|
|
115
|
+
self.record_current_frame_change = True
|
|
116
|
+
|
|
117
|
+
def __on_previous_frame_clicked(self):
|
|
118
|
+
"""
|
|
119
|
+
Move the plot to the previous frame and update the previous_frame list.
|
|
120
|
+
If there is only one frame left in the previous_frame list, hide the 'previous_frame' option in the context menu.
|
|
121
|
+
"""
|
|
122
|
+
if len(self.previous_frame) > 1:
|
|
123
|
+
self.record_current_frame_change = False
|
|
124
|
+
self.plot_widget.update_plot(x_loc=self.previous_frame[-2][0], x_range=self.previous_frame[-2][1])
|
|
125
|
+
self.previous_frame.pop(-1)
|
|
126
|
+
if len(self.previous_frame) == 1:
|
|
127
|
+
self.previous_frame_action.setVisible(False)
|
|
128
|
+
|
|
129
|
+
def __on_record_current_frame_clicked(self):
|
|
130
|
+
"""
|
|
131
|
+
Records the current frame by prompting the user to input a frame name and saves it to the `saved_frame` list.
|
|
132
|
+
Updates the saved frame menu afterwards.
|
|
133
|
+
"""
|
|
134
|
+
frame_name = select_str(self.parent.window(), "Input frame name", f"saved_frame_{len(self.saved_frame) + 1}")
|
|
135
|
+
if frame_name is not None:
|
|
136
|
+
view_rect = self.plot_widget.viewRect()
|
|
137
|
+
self.saved_frame.append([frame_name, view_rect.left(), view_rect.width()])
|
|
138
|
+
self.saved_frame_table.add_row_data(
|
|
139
|
+
[frame_name, self.plot_widget.getAxis("bottom").tick_str(view_rect.left()), str(view_rect.width())]
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def __on_given_frame_clicked(self):
|
|
143
|
+
"""
|
|
144
|
+
Handle the event when a frame is clicked.
|
|
145
|
+
|
|
146
|
+
This method prompts the user to input a coordinate and moves the plot accordingly.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
None
|
|
150
|
+
"""
|
|
151
|
+
if self.plot_widget.getAxis("bottom").plot_strs is None:
|
|
152
|
+
MessageBox(
|
|
153
|
+
"Error", content="There is no support coordinate system in the plot!", parent=self.parent.window()
|
|
154
|
+
).exec()
|
|
155
|
+
else:
|
|
156
|
+
items = list(self.plot_widget.getAxis("bottom").plot_strs.values())
|
|
157
|
+
select_x = select_limited_str(self.parent.window(), "Input coordinate", items)
|
|
158
|
+
if select_x is not None:
|
|
159
|
+
self.plot_widget.update_plot(
|
|
160
|
+
x_loc=items.index(select_x) - self.plot_widget.x_range_min / 2, x_range=self.plot_widget.x_range_min
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def __on_table_jump_to_clicked(self, row: int):
|
|
164
|
+
"""
|
|
165
|
+
Handle the event when a frame is clicked.
|
|
166
|
+
|
|
167
|
+
This method moves the plot to the selected frame.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
row (int): The row index of the selected frame.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
None
|
|
174
|
+
"""
|
|
175
|
+
self.plot_widget.update_plot(x_loc=self.saved_frame[row][1], x_range=self.saved_frame[row][2])
|
|
176
|
+
|
|
177
|
+
def __on_table_name_changed(self, row: int, new_name: str):
|
|
178
|
+
"""
|
|
179
|
+
Handle the event when the name of a frame is changed.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
row (int): The row index of the frame.
|
|
183
|
+
new_name (str): The new name for the frame.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
None
|
|
187
|
+
"""
|
|
188
|
+
if new_name != "":
|
|
189
|
+
self.saved_frame[row][0] = new_name
|
|
190
|
+
self.saved_frame_table.set_row_data_item(row, 0, new_name)
|
|
191
|
+
else:
|
|
192
|
+
self.saved_frame_table.set_row_data_item(row, 0, self.saved_frame[row][0])
|
|
193
|
+
|
|
194
|
+
def __on_table_row_deleted(self, row: int):
|
|
195
|
+
"""
|
|
196
|
+
Handle the event when a frame is deleted.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
row (int): The row index of the frame to be deleted.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
None
|
|
203
|
+
"""
|
|
204
|
+
self.saved_frame.pop(row)
|
|
205
|
+
|
|
206
|
+
def get_widget(self):
|
|
207
|
+
"""
|
|
208
|
+
Get the widget associated with the FrameRecorderComponent.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
QWidget: The widget associated with the FrameRecorderComponent.
|
|
212
|
+
"""
|
|
213
|
+
return self.saved_frame_table
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from pyqtgraph import SignalProxy
|
|
2
|
+
from ..widgets.q_plot_widget import QPlotWidget
|
|
3
|
+
from ..widgets.zoom_bar import ZoomBar
|
|
4
|
+
from ..widgets.fluent_scroller import HorizontalFluentScroller, VerticalFluentScroller
|
|
5
|
+
from ..libs.constant import ZOOM_MODEL, YLOC_MODEL
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class StockWidgetZoomBar(ZoomBar):
|
|
9
|
+
"""
|
|
10
|
+
A custom zoom bar widget for the stock plot widget.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
plot_widget (QPlotWidget): The parent plot widget.
|
|
14
|
+
parent (QWidget): The parent widget.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
plot_widget (QPlotWidget): The parent plot widget.
|
|
18
|
+
current_value (float): The current value of the plot widget's view rectangle width.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, plot_widget: QPlotWidget, parent=None) -> None:
|
|
22
|
+
super().__init__(parent=parent, use_opacity_effect=True)
|
|
23
|
+
self.plot_widget = plot_widget
|
|
24
|
+
self.current_value = plot_widget.viewRect().width()
|
|
25
|
+
|
|
26
|
+
def update_position():
|
|
27
|
+
self.setGeometry(
|
|
28
|
+
self.plot_widget.geometry().width() - 300 - 20, self.plot_widget.geometry().y() + 10, 300, 40
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
update_position()
|
|
32
|
+
self.plot_widget.sigSizeChanged.connect(update_position)
|
|
33
|
+
|
|
34
|
+
def update_boundings():
|
|
35
|
+
self.current_value = self.plot_widget.viewRect().width()
|
|
36
|
+
self.update_min_max_value(min_v=self.plot_widget.x_range_min, max_v=self.plot_widget.x_range_max)
|
|
37
|
+
|
|
38
|
+
update_boundings()
|
|
39
|
+
self.plot_widget.sigBoundingUpdated.connect(update_boundings)
|
|
40
|
+
|
|
41
|
+
def update_widget():
|
|
42
|
+
self.update_widget(value=self.plot_widget.viewRect().width())
|
|
43
|
+
|
|
44
|
+
update_widget()
|
|
45
|
+
self.plotter_view_changed_slot = SignalProxy(self.plot_widget.sigRangeChanged, rateLimit=50, slot=update_widget)
|
|
46
|
+
|
|
47
|
+
def apply_value_func(self, value):
|
|
48
|
+
"""
|
|
49
|
+
Apply the given value to the plot widget's x range.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
value (float): The value to be applied.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
None
|
|
56
|
+
|
|
57
|
+
"""
|
|
58
|
+
# x_loc = (self.plot_widget.viewRect().right()+self.plot_widget.viewRect().left())/2-value/2
|
|
59
|
+
self.plot_widget.update_plot(x_range=value)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class StockWidgetHorizontalScroller(HorizontalFluentScroller):
|
|
63
|
+
"""
|
|
64
|
+
A custom horizontal scroller widget for controlling the movement of a plot widget.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
plot_widget (QPlotWidget): The plot widget to be controlled.
|
|
68
|
+
parent: The parent widget.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(self, plot_widget: QPlotWidget, parent: None) -> None:
|
|
72
|
+
super().__init__(parent)
|
|
73
|
+
self.plot_widget = plot_widget
|
|
74
|
+
self.move_unit_ratio = 0.02
|
|
75
|
+
self.move_unit_min = 1
|
|
76
|
+
self.move_from_update = False
|
|
77
|
+
self.update_location()
|
|
78
|
+
self.plot_widget.sigBoundingUpdated.connect(self.update_location)
|
|
79
|
+
self.plotter_view_changed_slot = SignalProxy(
|
|
80
|
+
self.plot_widget.sigRangeChanged, rateLimit=50, slot=self.update_location
|
|
81
|
+
)
|
|
82
|
+
self.valueChanged.connect(self.on_value_changed)
|
|
83
|
+
|
|
84
|
+
def update_location(self):
|
|
85
|
+
"""
|
|
86
|
+
Update the location and range of the scroller based on the plot widget's view.
|
|
87
|
+
"""
|
|
88
|
+
self.move_unit = max(self.plot_widget.viewRect().width() * self.move_unit_ratio, self.move_unit_min)
|
|
89
|
+
self.setRange(
|
|
90
|
+
0,
|
|
91
|
+
int(
|
|
92
|
+
(self.plot_widget.x_end - self.plot_widget.x_start - self.plot_widget.viewRect().width())
|
|
93
|
+
/ self.move_unit
|
|
94
|
+
),
|
|
95
|
+
)
|
|
96
|
+
self.move_from_update = True
|
|
97
|
+
self.setValue(int((self.plot_widget.viewRect().left() - self.plot_widget.x_start) / self.move_unit))
|
|
98
|
+
self.move_from_update = False
|
|
99
|
+
|
|
100
|
+
def on_value_changed(self):
|
|
101
|
+
"""
|
|
102
|
+
Handle the value changed signal of the scroller and update the plot widget's plot accordingly.
|
|
103
|
+
"""
|
|
104
|
+
if not self.move_from_update:
|
|
105
|
+
self.plot_widget.update_plot(x_loc=self.value() * self.move_unit + self.plot_widget.x_start)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class StockWidgetVerticalScroller(VerticalFluentScroller):
|
|
109
|
+
"""
|
|
110
|
+
A custom vertical scroller widget for controlling the y-axis movement of a plot widget.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
plot_widget (QPlotWidget): The plot widget to control.
|
|
114
|
+
parent (None): The parent widget.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def __init__(self, plot_widget: QPlotWidget, parent: None) -> None:
|
|
118
|
+
super().__init__(parent)
|
|
119
|
+
self.plot_widget = plot_widget
|
|
120
|
+
self.move_unit_ratio = 0.02
|
|
121
|
+
self.move_unit_min = 1
|
|
122
|
+
self.move_from_update = False
|
|
123
|
+
self.update_location()
|
|
124
|
+
self.on_zoom_loc_model_changed()
|
|
125
|
+
self.plot_widget.sigZoomModelChanged.connect(self.on_zoom_loc_model_changed)
|
|
126
|
+
self.plot_widget.sigYLocModelChanged.connect(self.on_zoom_loc_model_changed)
|
|
127
|
+
self.plot_widget.sigBoundingUpdated.connect(self.update_location)
|
|
128
|
+
self.plotter_view_changed_slot = SignalProxy(
|
|
129
|
+
self.plot_widget.sigRangeChanged, rateLimit=50, slot=self.update_location
|
|
130
|
+
)
|
|
131
|
+
self.valueChanged.connect(self.on_value_changed)
|
|
132
|
+
|
|
133
|
+
def update_location(self):
|
|
134
|
+
"""
|
|
135
|
+
Update the location and range of the scroller based on the plot widget's current state.
|
|
136
|
+
"""
|
|
137
|
+
self.move_unit = max(self.plot_widget.viewRect().height() * self.move_unit_ratio, self.move_unit_min)
|
|
138
|
+
maximum_y = int(
|
|
139
|
+
(self.plot_widget.y_end - self.plot_widget.y_start - self.plot_widget.viewRect().height()) / self.move_unit
|
|
140
|
+
)
|
|
141
|
+
self.setRange(0, maximum_y)
|
|
142
|
+
self.move_from_update = True
|
|
143
|
+
self.setValue(maximum_y - int((self.plot_widget.viewRect().top() - self.plot_widget.y_start) / self.move_unit))
|
|
144
|
+
self.move_from_update = False
|
|
145
|
+
|
|
146
|
+
def on_value_changed(self):
|
|
147
|
+
"""
|
|
148
|
+
Handle the value changed signal of the scroller and update the plot widget's y-axis location accordingly.
|
|
149
|
+
"""
|
|
150
|
+
if not self.move_from_update:
|
|
151
|
+
self.plot_widget.move_y_loc((self.maximum() - self.value()) * self.move_unit + self.plot_widget.y_start)
|
|
152
|
+
|
|
153
|
+
def on_zoom_loc_model_changed(self):
|
|
154
|
+
"""
|
|
155
|
+
Handle the zoom and y-axis location model changed signals of the plot widget and enable/disable the scroller accordingly.
|
|
156
|
+
"""
|
|
157
|
+
if self.plot_widget.zoom_model != ZOOM_MODEL.AUTO_RANGE and self.plot_widget.y_loc_model == YLOC_MODEL.FREE:
|
|
158
|
+
self.setEnabled(True)
|
|
159
|
+
else:
|
|
160
|
+
self.setEnabled(False)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .helpers import GeneralDataClass
|
|
2
|
+
|
|
3
|
+
ZOOM_MODEL = GeneralDataClass(
|
|
4
|
+
AUTO_RANGE=0,
|
|
5
|
+
FIXED_RATIO=1,
|
|
6
|
+
FIXED_YRANGE=2,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
YLOC_MODEL = GeneralDataClass(
|
|
10
|
+
FREE=0, # allow free move, y_loc is changed as while y_center is not changed
|
|
11
|
+
DATA_CENTERED=1, # doesn't allow free move, y_loc is always the center of the data
|
|
12
|
+
FIXED=2, # dosen't allow free move, y_loc is not changed
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
SCALE_LOC_MODEL = GeneralDataClass(
|
|
16
|
+
CENTRAL=0,
|
|
17
|
+
LEFT=1,
|
|
18
|
+
RIGHT=2,
|
|
19
|
+
)
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ChildDataFrame:
|
|
7
|
+
"""
|
|
8
|
+
A class representing a child DataFrame.
|
|
9
|
+
|
|
10
|
+
Attributes:
|
|
11
|
+
data_frame (pd.DataFrame): The parent DataFrame.
|
|
12
|
+
data_keys (list): The key(s) of the data column(s) in the parent DataFrame.
|
|
13
|
+
max_y_key (str): The key of the maximum y-value column.
|
|
14
|
+
min_y_key (str): The key of the minimum y-value column.
|
|
15
|
+
x_ticks (dict): A dictionary mapping index values to x-labels.
|
|
16
|
+
__index_start (int): The starting index of the child DataFrame.
|
|
17
|
+
|
|
18
|
+
Methods:
|
|
19
|
+
get_min_x(): Returns the minimum x-value in the parent DataFrame.
|
|
20
|
+
get_max_x(): Returns the maximum x-value in the parent DataFrame.
|
|
21
|
+
get_local_range(x_start, x_end): Returns the local range of y-values between x_start and x_end.
|
|
22
|
+
get_x_ticks(): Returns the x-ticks dictionary.
|
|
23
|
+
__len__(): Returns the length of the child DataFrame.
|
|
24
|
+
__getitem__(idx): Returns a tuple of data values at the given index.
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self, data_frame: pd.DataFrame, data_keys: str | list, max_y_key=None, min_y_key=None, x_label_key=None
|
|
30
|
+
) -> None:
|
|
31
|
+
"""
|
|
32
|
+
Initialize the DataHandler object.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
data_frame (pd.DataFrame): The parent DataFrame containing the data.
|
|
36
|
+
data_keys (Union[str, list]): The key(s) to access the data in the parent DataFrame.
|
|
37
|
+
max_y_key (str, optional): The key to access the maximum y-value data. Defaults to None.
|
|
38
|
+
min_y_key (str, optional): The key to access the minimum y-value data. Defaults to None.
|
|
39
|
+
x_label_key (str, optional): The key to access the x-label data. Defaults to None.
|
|
40
|
+
"""
|
|
41
|
+
data_keys = data_keys if isinstance(data_keys, list) else [data_keys]
|
|
42
|
+
self.data_keys = deepcopy(data_keys)
|
|
43
|
+
if max_y_key is not None:
|
|
44
|
+
if max_y_key not in data_keys:
|
|
45
|
+
data_keys.append(max_y_key)
|
|
46
|
+
else:
|
|
47
|
+
max_y_key = data_keys[0]
|
|
48
|
+
self.max_y_key = max_y_key
|
|
49
|
+
if min_y_key is not None:
|
|
50
|
+
if min_y_key not in data_keys:
|
|
51
|
+
data_keys.append(min_y_key)
|
|
52
|
+
else:
|
|
53
|
+
min_y_key = data_keys[0]
|
|
54
|
+
self.min_y_key = min_y_key
|
|
55
|
+
x_label_key = x_label_key if x_label_key is not None else "date"
|
|
56
|
+
if x_label_key not in data_keys:
|
|
57
|
+
data_keys.append(x_label_key)
|
|
58
|
+
self.data_frame = data_frame[data_keys]
|
|
59
|
+
self.x_ticks: dict = {i: str(self.data_frame[x_label_key][i]) for i in self.data_frame.index}
|
|
60
|
+
self.__index_start = self.get_min_x()
|
|
61
|
+
|
|
62
|
+
def get_min_x(self):
|
|
63
|
+
"""
|
|
64
|
+
Returns the minimum x-value in the parent DataFrame.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
int: The minimum x-value.
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
return self.data_frame.index[0]
|
|
71
|
+
|
|
72
|
+
def get_max_x(self):
|
|
73
|
+
"""
|
|
74
|
+
Returns the maximum x-value in the parent DataFrame.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
int: The maximum x-value.
|
|
78
|
+
|
|
79
|
+
"""
|
|
80
|
+
return self.data_frame.index[-1]
|
|
81
|
+
|
|
82
|
+
def get_local_range(self, x_start, x_end):
|
|
83
|
+
"""
|
|
84
|
+
Returns the local range of y-values between x_start and x_end.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
x_start (int): The starting x-value.
|
|
88
|
+
x_end (int): The ending x-value.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
tuple: A tuple containing the minimum and maximum y-values.
|
|
92
|
+
|
|
93
|
+
"""
|
|
94
|
+
x_start = int(x_start)
|
|
95
|
+
x_end = int(x_end)
|
|
96
|
+
return pd.to_numeric(self.data_frame.loc[x_start:x_end, self.min_y_key]).min(), pd.to_numeric(
|
|
97
|
+
self.data_frame.loc[x_start:x_end, self.max_y_key]
|
|
98
|
+
).max()
|
|
99
|
+
|
|
100
|
+
def get_x_ticks(self):
|
|
101
|
+
"""
|
|
102
|
+
Returns the x-ticks dictionary.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
dict: A dictionary mapping index values to x-labels.
|
|
106
|
+
|
|
107
|
+
"""
|
|
108
|
+
return self.x_ticks
|
|
109
|
+
|
|
110
|
+
def __len__(self):
|
|
111
|
+
"""
|
|
112
|
+
Returns the length of the child DataFrame.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
int: The length of the child DataFrame.
|
|
116
|
+
|
|
117
|
+
"""
|
|
118
|
+
return len(self.data_frame)
|
|
119
|
+
|
|
120
|
+
def __getitem__(self, idx):
|
|
121
|
+
"""
|
|
122
|
+
Returns a tuple of data values at the given index.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
idx (int): The index.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
tuple: A tuple containing the index value and data values.
|
|
129
|
+
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
return tuple([self.data_frame.index[idx]]) + tuple(
|
|
133
|
+
pd.to_numeric(self.data_frame[key][self.__index_start + idx], errors="coerce") for key in self.data_keys
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class PricesDataFrame(ChildDataFrame):
|
|
138
|
+
"""
|
|
139
|
+
A class representing a DataFrame containing prices data.
|
|
140
|
+
|
|
141
|
+
Parameters:
|
|
142
|
+
data_frame (pd.DataFrame): The parent DataFrame containing the prices data.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
def __init__(self, data_frame: pd.DataFrame):
|
|
146
|
+
super().__init__(
|
|
147
|
+
data_frame,
|
|
148
|
+
data_keys=["open", "close", "high", "low"],
|
|
149
|
+
max_y_key="high",
|
|
150
|
+
min_y_key="low",
|
|
151
|
+
x_label_key="date",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class VolumeDataFrame(ChildDataFrame):
|
|
156
|
+
"""
|
|
157
|
+
A class representing a volume data frame.
|
|
158
|
+
|
|
159
|
+
This class inherits from the ChildDataFrame class and provides additional functionality
|
|
160
|
+
for handling volume data.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
data_frame (pd.DataFrame): The parent data frame from which the volume data frame is derived.
|
|
164
|
+
|
|
165
|
+
Attributes:
|
|
166
|
+
data_keys (list): A list of data keys for the volume data.
|
|
167
|
+
x_label_key (str): The key for the x-axis label in the volume data frame.
|
|
168
|
+
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
def __init__(self, data_frame: pd.DataFrame):
|
|
172
|
+
super().__init__(data_frame, data_keys=["volume"], x_label_key="date")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@dataclass
|
|
176
|
+
class TradeData:
|
|
177
|
+
prices: PricesDataFrame
|
|
178
|
+
volume: VolumeDataFrame
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class DataHandler:
|
|
182
|
+
def __init__(self):
|
|
183
|
+
self.day_data: TradeData = None
|
|
184
|
+
self.week_data: TradeData = None
|
|
185
|
+
self.month_data: TradeData = None
|
|
186
|
+
|
|
187
|
+
def save(self, hdf5_path):
|
|
188
|
+
for key, data in zip(
|
|
189
|
+
["day_data", "week_data", "month_data"], [self.day_data, self.week_data, self.month_data], strict=False
|
|
190
|
+
):
|
|
191
|
+
price = data.prices.data_frame
|
|
192
|
+
volume = data.volume.data_frame
|
|
193
|
+
volume = volume.drop(columns=["date"])
|
|
194
|
+
df = price.join(volume)
|
|
195
|
+
df.to_hdf(hdf5_path, key=key)
|
|
196
|
+
|
|
197
|
+
def load(self, hdf5_path):
|
|
198
|
+
# self.__df_day = pd.read_hdf(hdf5_path, key="day_data")
|
|
199
|
+
# self.__df_week = pd.read_hdf(hdf5_path, key="week_data")
|
|
200
|
+
# self.__df_month = pd.read_hdf(hdf5_path, key="month_data")
|
|
201
|
+
self.day_data = self._load_data(hdf5_path, "day_data")
|
|
202
|
+
self.week_data = self._load_data(hdf5_path, "week_data")
|
|
203
|
+
self.month_data = self._load_data(hdf5_path, "month_data")
|
|
204
|
+
|
|
205
|
+
def _load_data(self, path, key):
|
|
206
|
+
try:
|
|
207
|
+
df = pd.read_hdf(path, key=key)
|
|
208
|
+
except Exception as e:
|
|
209
|
+
print(f"Error loading data from {path} with key {key}: {e}")
|
|
210
|
+
return None
|
|
211
|
+
return TradeData(PricesDataFrame(df), VolumeDataFrame(df))
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class HDF5Handler(DataHandler):
|
|
215
|
+
"""
|
|
216
|
+
A class that handles candlestick data from an HDF5 file.
|
|
217
|
+
|
|
218
|
+
Attributes:
|
|
219
|
+
day_data (TradeData): Candlestick data for daily intervals.
|
|
220
|
+
week_data (TradeData): Candlestick data for weekly intervals.
|
|
221
|
+
month_data (TradeData): Candlestick data for monthly intervals.
|
|
222
|
+
|
|
223
|
+
Methods:
|
|
224
|
+
__init__(hdf5_path): Initializes the HDF5Handler object.
|
|
225
|
+
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
def __init__(self, hdf5_path: str) -> None:
|
|
229
|
+
super().__init__()
|
|
230
|
+
self.load(hdf5_path)
|