seolpyo-mplchart 0.1.3.1__py3-none-any.whl → 2.0.0.3__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.
- seolpyo_mplchart/__init__.py +164 -99
- seolpyo_mplchart/_base.py +117 -0
- seolpyo_mplchart/_chart/__init__.py +137 -0
- seolpyo_mplchart/_chart/_base.py +217 -0
- seolpyo_mplchart/_chart/_cursor/__init__.py +2 -0
- seolpyo_mplchart/_chart/_cursor/_artist.py +217 -0
- seolpyo_mplchart/_chart/_cursor/_cursor.py +165 -0
- seolpyo_mplchart/_chart/_cursor/_info.py +187 -0
- seolpyo_mplchart/_chart/_draw/__init__.py +2 -0
- seolpyo_mplchart/_chart/_draw/_artist.py +50 -0
- seolpyo_mplchart/_chart/_draw/_data.py +314 -0
- seolpyo_mplchart/_chart/_draw/_draw.py +103 -0
- seolpyo_mplchart/_chart/_draw/_lim.py +265 -0
- seolpyo_mplchart/_chart/_slider/__init__.py +1 -0
- seolpyo_mplchart/_chart/_slider/_base.py +268 -0
- seolpyo_mplchart/_chart/_slider/_data.py +105 -0
- seolpyo_mplchart/_chart/_slider/_mouse.py +176 -0
- seolpyo_mplchart/_chart/_slider/_nav.py +204 -0
- seolpyo_mplchart/_chart/test.py +121 -0
- seolpyo_mplchart/_config/__init__.py +3 -0
- seolpyo_mplchart/_config/ax.py +28 -0
- seolpyo_mplchart/_config/candle.py +30 -0
- seolpyo_mplchart/_config/config.py +21 -0
- seolpyo_mplchart/_config/cursor.py +49 -0
- seolpyo_mplchart/_config/figure.py +41 -0
- seolpyo_mplchart/_config/format.py +51 -0
- seolpyo_mplchart/_config/ma.py +15 -0
- seolpyo_mplchart/_config/slider/__init__.py +2 -0
- seolpyo_mplchart/_config/slider/config.py +24 -0
- seolpyo_mplchart/_config/slider/figure.py +20 -0
- seolpyo_mplchart/_config/slider/nav.py +9 -0
- seolpyo_mplchart/_config/unit.py +19 -0
- seolpyo_mplchart/_config/utils.py +67 -0
- seolpyo_mplchart/_config/volume.py +26 -0
- seolpyo_mplchart/_cursor.py +559 -0
- seolpyo_mplchart/_draw.py +634 -0
- seolpyo_mplchart/_slider.py +634 -0
- seolpyo_mplchart/base.py +70 -67
- seolpyo_mplchart/cursor.py +308 -271
- seolpyo_mplchart/draw.py +449 -237
- seolpyo_mplchart/slider.py +451 -396
- seolpyo_mplchart/test.py +173 -24
- seolpyo_mplchart/utils.py +15 -4
- seolpyo_mplchart/xl_to_dict.py +47 -0
- seolpyo_mplchart-2.0.0.3.dist-info/METADATA +710 -0
- seolpyo_mplchart-2.0.0.3.dist-info/RECORD +50 -0
- {seolpyo_mplchart-0.1.3.1.dist-info → seolpyo_mplchart-2.0.0.3.dist-info}/WHEEL +1 -1
- seolpyo_mplchart-0.1.3.1.dist-info/METADATA +0 -49
- seolpyo_mplchart-0.1.3.1.dist-info/RECORD +0 -13
- {seolpyo_mplchart-0.1.3.1.dist-info → seolpyo_mplchart-2.0.0.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
import matplotlib.pyplot as plt
|
|
2
|
+
from matplotlib.axes import Axes
|
|
3
|
+
from matplotlib.collections import LineCollection
|
|
4
|
+
from matplotlib.text import Text
|
|
5
|
+
from matplotlib.backend_bases import MouseEvent, MouseButton, cursors
|
|
6
|
+
|
|
7
|
+
from ._base import convert_unit
|
|
8
|
+
from ._cursor import BaseMixin as BM, Mixin as M
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Mixin(M):
|
|
12
|
+
def on_click(self, e):
|
|
13
|
+
"This function works if mouse button click event active."
|
|
14
|
+
return
|
|
15
|
+
def on_release(self, e):
|
|
16
|
+
"This function works if mouse button release event active."
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PlotMixin(BM):
|
|
21
|
+
slider_top = True
|
|
22
|
+
ratio_ax_slider = 3
|
|
23
|
+
ratio_ax_none = 2
|
|
24
|
+
|
|
25
|
+
def _get_plot(self):
|
|
26
|
+
if self.slider_top:
|
|
27
|
+
self.figure, axes = plt.subplots(
|
|
28
|
+
4, # row 수
|
|
29
|
+
figsize=self.figsize, # 기본 크기
|
|
30
|
+
height_ratios=(self.ratio_ax_slider, self.ratio_ax_legend, self.ratio_ax_price, self.ratio_ax_volume) # row 크기 비율
|
|
31
|
+
)
|
|
32
|
+
axes: list[Axes]
|
|
33
|
+
self.ax_slider, self.ax_legend, self.ax_price, self.ax_volume = axes
|
|
34
|
+
else:
|
|
35
|
+
self.figure, axes = plt.subplots(
|
|
36
|
+
5, # row 수
|
|
37
|
+
figsize=self.figsize, # 기본 크기
|
|
38
|
+
height_ratios=(self.ratio_ax_legend, self.ratio_ax_price, self.ratio_ax_volume, self.ratio_ax_none, self.ratio_ax_slider) # row 크기 비율
|
|
39
|
+
)
|
|
40
|
+
axes: list[Axes]
|
|
41
|
+
self.ax_legend, self.ax_price, self.ax_volume, ax_none, self.ax_slider = axes
|
|
42
|
+
|
|
43
|
+
ax_none.set_axis_off()
|
|
44
|
+
ax_none.xaxis.set_animated(True)
|
|
45
|
+
ax_none.yaxis.set_animated(True)
|
|
46
|
+
|
|
47
|
+
self.ax_slider.set_label('slider ax')
|
|
48
|
+
self.ax_legend.set_label('legend ax')
|
|
49
|
+
self.ax_price.set_label('price ax')
|
|
50
|
+
self.ax_volume.set_label('volume ax')
|
|
51
|
+
|
|
52
|
+
self.figure.canvas.manager.set_window_title(f'{self.title}')
|
|
53
|
+
self.figure.set_facecolor(self.color_background)
|
|
54
|
+
|
|
55
|
+
# 플롯간 간격 제거(Configure subplots)
|
|
56
|
+
self.figure.subplots_adjust(**self.adjust)
|
|
57
|
+
|
|
58
|
+
self.ax_legend.set_axis_off()
|
|
59
|
+
|
|
60
|
+
# y ticklabel foramt 설정
|
|
61
|
+
self.ax_slider.yaxis.set_major_formatter(lambda x, _: convert_unit(x, word=self.unit_price, digit=2))
|
|
62
|
+
self.ax_price.yaxis.set_major_formatter(lambda x, _: convert_unit(x, word=self.unit_price, digit=2))
|
|
63
|
+
self.ax_volume.yaxis.set_major_formatter(lambda x, _: convert_unit(x, word=self.unit_volume, digit=2))
|
|
64
|
+
|
|
65
|
+
gridKwargs = {'visible': True, 'linewidth': 0.5, 'color': '#d0d0d0', 'linestyle': '-', 'dashes': (1, 0)}
|
|
66
|
+
gridKwargs.update(self.gridKwargs)
|
|
67
|
+
# 공통 설정
|
|
68
|
+
for ax in (self.ax_slider, self.ax_price, self.ax_volume):
|
|
69
|
+
ax.xaxis.set_animated(True)
|
|
70
|
+
ax.yaxis.set_animated(True)
|
|
71
|
+
|
|
72
|
+
# x tick 외부 눈금 표시하지 않기
|
|
73
|
+
ax.xaxis.set_ticks_position('none')
|
|
74
|
+
# x tick label 제거
|
|
75
|
+
ax.set_xticklabels([])
|
|
76
|
+
# y tick 우측으로 이동
|
|
77
|
+
ax.tick_params(left=False, right=True, labelleft=False, labelright=True, colors=self.color_tick_label)
|
|
78
|
+
# Axes 외곽선 색 변경
|
|
79
|
+
for i in ['top', 'bottom', 'left', 'right']: ax.spines[i].set_color(self.color_tick)
|
|
80
|
+
|
|
81
|
+
# 차트 영역 배경 색상
|
|
82
|
+
ax.set_facecolor(self.color_background)
|
|
83
|
+
|
|
84
|
+
# grid(구분선, 격자) 그리기
|
|
85
|
+
# 어째서인지 grid의 zorder 값을 선언해도 1.6을 값으로 한다.
|
|
86
|
+
ax.grid(**gridKwargs)
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class CollectionMixin(PlotMixin):
|
|
91
|
+
min_distance = 30
|
|
92
|
+
color_navigator_line = '#1E78FF'
|
|
93
|
+
color_navigator_cover = 'k'
|
|
94
|
+
|
|
95
|
+
def _add_collection(self):
|
|
96
|
+
super()._add_collection()
|
|
97
|
+
|
|
98
|
+
self.collection_slider = LineCollection([], animated=True)
|
|
99
|
+
self.ax_slider.add_artist(self.collection_slider)
|
|
100
|
+
|
|
101
|
+
# 슬라이더 네비게이터
|
|
102
|
+
self.collection_navigator = LineCollection([], animated=True, edgecolors=[self.color_navigator_cover, self.color_navigator_line], alpha=(0.3, 1.0))
|
|
103
|
+
self.ax_slider.add_artist(self.collection_navigator)
|
|
104
|
+
|
|
105
|
+
lineKwargs = {'edgecolor': 'k', 'linewidth': 1, 'linestyle': '-'}
|
|
106
|
+
lineKwargs.update(self.lineKwargs)
|
|
107
|
+
lineKwargs.update({'segments': [], 'animated': True})
|
|
108
|
+
|
|
109
|
+
self.collection_slider_vline = LineCollection(**lineKwargs)
|
|
110
|
+
self.ax_slider.add_artist(self.collection_slider_vline)
|
|
111
|
+
|
|
112
|
+
textboxKwargs = {'boxstyle': 'round', 'facecolor': 'w'}
|
|
113
|
+
textboxKwargs.update(self.textboxKwargs)
|
|
114
|
+
textKwargs = self.textKwargs
|
|
115
|
+
textKwargs.update({'animated': True, 'bbox': textboxKwargs, 'horizontalalignment': '', 'verticalalignment': ''})
|
|
116
|
+
(textKwargs.pop('horizontalalignment'), textKwargs.pop('verticalalignment'))
|
|
117
|
+
|
|
118
|
+
self.artist_text_slider = Text(**textKwargs, horizontalalignment='center', verticalalignment='top')
|
|
119
|
+
self.ax_slider.add_artist(self.artist_text_slider)
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
def _create_segments(self):
|
|
123
|
+
super()._create_segments()
|
|
124
|
+
|
|
125
|
+
self._create_slider_segment()
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
def _create_slider_segment(self):
|
|
129
|
+
keys = []
|
|
130
|
+
for i in reversed(self.list_ma):
|
|
131
|
+
keys.append('x')
|
|
132
|
+
keys.append(f'ma{i}')
|
|
133
|
+
|
|
134
|
+
series = self.df[keys + ['x', self.close]]
|
|
135
|
+
series['x'] = series['x'] - 0.5
|
|
136
|
+
segment_slider = series.values
|
|
137
|
+
segment_slider = segment_slider.reshape(segment_slider.shape[0], len(self.list_ma)+1, 2).swapaxes(0, 1)
|
|
138
|
+
self.collection_slider.set_segments(segment_slider)
|
|
139
|
+
linewidth = [1 for _ in self.list_ma]
|
|
140
|
+
self.collection_slider.set_linewidth(linewidth + [2.1])
|
|
141
|
+
|
|
142
|
+
self._create_slider_color_segments()
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
def _create_slider_color_segments(self):
|
|
146
|
+
self.collection_slider.set_edgecolor(self.edgecolor_ma + [self.color_priceline])
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
def change_background_color(self, color):
|
|
150
|
+
super().change_background_color(color)
|
|
151
|
+
|
|
152
|
+
self.ax_slider.set_facecolor(color)
|
|
153
|
+
self.artist_text_slider.set_backgroundcolor(color)
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
def change_tick_color(self, color):
|
|
157
|
+
super().change_tick_color(color)
|
|
158
|
+
|
|
159
|
+
for i in ['top', 'bottom', 'left', 'right']: self.ax_slider.spines[i].set_color(self.color_tick)
|
|
160
|
+
self.ax_slider.tick_params(colors=color)
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
def change_text_color(self, color):
|
|
164
|
+
super().change_text_color(color)
|
|
165
|
+
|
|
166
|
+
self.artist_text_slider.set_color(color)
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
def change_line_color(self, color):
|
|
170
|
+
super().change_line_color(color)
|
|
171
|
+
|
|
172
|
+
self.artist_text_slider.get_bbox_patch().set_edgecolor(color)
|
|
173
|
+
self.collection_slider_vline.set_edgecolor(color)
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class NavigatorMixin(CollectionMixin):
|
|
178
|
+
def _set_slider_lim(self):
|
|
179
|
+
xmax = self.list_index[-1]
|
|
180
|
+
# 슬라이더 xlim
|
|
181
|
+
xdistance = xmax / 30
|
|
182
|
+
self.slider_xmin, self.slider_xmax = (-xdistance, xmax+xdistance)
|
|
183
|
+
self.ax_slider.set_xlim(self.slider_xmin, self.slider_xmax)
|
|
184
|
+
|
|
185
|
+
# 슬라이더 ylim
|
|
186
|
+
ymin, ymax = (self.df[self.low].min(), self.df[self.high].max())
|
|
187
|
+
ysub = ymax - ymin
|
|
188
|
+
self.sldier_ymiddle = ymin + (ysub / 2)
|
|
189
|
+
ydistance = ysub / 5
|
|
190
|
+
self.slider_ymin, self.slider_ymax = (ymin-ydistance, ymax+ydistance)
|
|
191
|
+
self.ax_slider.set_ylim(self.slider_ymin, self.slider_ymax)
|
|
192
|
+
|
|
193
|
+
# 슬라이더 텍스트 y
|
|
194
|
+
self.artist_text_slider.set_y(ymax)
|
|
195
|
+
|
|
196
|
+
self.collection_navigator.set_linewidth([ysub+ysub, 5])
|
|
197
|
+
|
|
198
|
+
# 네비게이터 라인 선택 범위
|
|
199
|
+
xsub = self.slider_xmax - self.slider_xmin
|
|
200
|
+
self._navLineWidth = xsub * 8 / 1_000
|
|
201
|
+
if self._navLineWidth < 1: self._navLineWidth = 1
|
|
202
|
+
self._navLineWidth_half = self._navLineWidth / 2
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
def _set_navigator(self, navmin, navmax):
|
|
206
|
+
seg = [
|
|
207
|
+
(
|
|
208
|
+
(self.slider_xmin, self.sldier_ymiddle),
|
|
209
|
+
(navmin, self.sldier_ymiddle),
|
|
210
|
+
),
|
|
211
|
+
(
|
|
212
|
+
(navmin, self.slider_ymin),
|
|
213
|
+
(navmin, self.slider_ymax),
|
|
214
|
+
),
|
|
215
|
+
(
|
|
216
|
+
(navmax, self.sldier_ymiddle),
|
|
217
|
+
(self.slider_xmax, self.sldier_ymiddle),
|
|
218
|
+
),
|
|
219
|
+
(
|
|
220
|
+
(navmax, self.slider_ymin),
|
|
221
|
+
(navmax, self.slider_ymax),
|
|
222
|
+
),
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
self.collection_navigator.set_segments(seg)
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class DataMixin(NavigatorMixin):
|
|
230
|
+
navcoordinate: tuple[int, int] = (0, 0)
|
|
231
|
+
|
|
232
|
+
def set_data(self, df, sort_df=True, calc_ma=True, set_candlecolor=True, set_volumecolor=True, calc_info=True, change_lim=True, *args, **kwargs):
|
|
233
|
+
self._generate_data(df, sort_df, calc_ma, set_candlecolor, set_volumecolor, calc_info, *args, **kwargs)
|
|
234
|
+
self._create_segments()
|
|
235
|
+
|
|
236
|
+
vmin, vmax = self.navcoordinate
|
|
237
|
+
if change_lim or (vmax-vmin) < self.min_distance:
|
|
238
|
+
vmin, vmax = self.get_default_lim()
|
|
239
|
+
self.navcoordinate = (vmin, vmax-1)
|
|
240
|
+
else: vmax += 1
|
|
241
|
+
|
|
242
|
+
self._set_lim(vmin, vmax)
|
|
243
|
+
self._set_slider_lim()
|
|
244
|
+
self._set_navigator(*self.navcoordinate)
|
|
245
|
+
|
|
246
|
+
self._set_length_text()
|
|
247
|
+
return
|
|
248
|
+
|
|
249
|
+
def get_default_lim(self):
|
|
250
|
+
xmax = self.list_index[-1] + 1
|
|
251
|
+
xmin = xmax - 120
|
|
252
|
+
if xmin < 0: xmin = 0
|
|
253
|
+
return (xmin, xmax)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class BackgroundMixin(DataMixin):
|
|
257
|
+
def _copy_bbox(self):
|
|
258
|
+
renderer = self.figure.canvas.renderer
|
|
259
|
+
|
|
260
|
+
self.ax_slider.xaxis.draw(renderer)
|
|
261
|
+
self.ax_slider.yaxis.draw(renderer)
|
|
262
|
+
self.collection_slider.draw(renderer)
|
|
263
|
+
self.background_emtpy = renderer.copy_from_bbox(self.figure.bbox)
|
|
264
|
+
|
|
265
|
+
self._draw_artist()
|
|
266
|
+
self.background = renderer.copy_from_bbox(self.figure.bbox)
|
|
267
|
+
|
|
268
|
+
self.collection_navigator.draw(self.figure.canvas.renderer)
|
|
269
|
+
self.background_with_nav = renderer.copy_from_bbox(self.figure.bbox)
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
def _restore_region(self, is_empty=False, with_nav=True):
|
|
273
|
+
if not self.background: self._create_background()
|
|
274
|
+
|
|
275
|
+
restore_region = self.figure.canvas.renderer.restore_region
|
|
276
|
+
if is_empty: restore_region(self.background_emtpy)
|
|
277
|
+
elif with_nav: restore_region(self.background_with_nav)
|
|
278
|
+
else: restore_region(self.background)
|
|
279
|
+
return
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class MouseMoveMixin(BackgroundMixin):
|
|
283
|
+
in_slider = False
|
|
284
|
+
is_click_slider = False
|
|
285
|
+
|
|
286
|
+
def _erase_crossline(self):
|
|
287
|
+
boolen = super()._erase_crossline()
|
|
288
|
+
if boolen: return boolen
|
|
289
|
+
|
|
290
|
+
seg = self.collection_slider_vline.get_segments()
|
|
291
|
+
if seg:
|
|
292
|
+
self.collection_slider_vline.set_segments([])
|
|
293
|
+
return True
|
|
294
|
+
return False
|
|
295
|
+
|
|
296
|
+
def _on_move(self, e):
|
|
297
|
+
self._on_move_action(e)
|
|
298
|
+
|
|
299
|
+
if self.in_slider:
|
|
300
|
+
self._restore_region(self.is_click_slider)
|
|
301
|
+
self._on_move_slider(e)
|
|
302
|
+
self.figure.canvas.blit()
|
|
303
|
+
elif self.in_price_chart or self.in_volume_chart:
|
|
304
|
+
self._restore_region()
|
|
305
|
+
self._draw_crossline(e)
|
|
306
|
+
self.figure.canvas.blit()
|
|
307
|
+
else:
|
|
308
|
+
if self._erase_crossline():
|
|
309
|
+
self._restore_region()
|
|
310
|
+
self.figure.canvas.blit()
|
|
311
|
+
return
|
|
312
|
+
|
|
313
|
+
def _on_move_action(self, e: MouseEvent):
|
|
314
|
+
self._check_ax(e)
|
|
315
|
+
|
|
316
|
+
self.intx = None
|
|
317
|
+
if self.in_slider or self.in_price_chart or self.in_volume_chart: self._get_x(e)
|
|
318
|
+
|
|
319
|
+
self._change_cursor(e)
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
def _change_cursor(self, e: MouseEvent):
|
|
323
|
+
# 마우스 커서 변경
|
|
324
|
+
if self.is_click_slider: return
|
|
325
|
+
elif not self.in_slider:
|
|
326
|
+
self.figure.canvas.set_cursor(cursors.POINTER)
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
navleft, navright = self.navcoordinate
|
|
330
|
+
if navleft == navright: return
|
|
331
|
+
|
|
332
|
+
x = e.xdata.__round__()
|
|
333
|
+
|
|
334
|
+
leftmin = navleft - self._navLineWidth
|
|
335
|
+
leftmax = navleft + self._navLineWidth_half
|
|
336
|
+
rightmin = navright - self._navLineWidth_half
|
|
337
|
+
rightmax = navright + self._navLineWidth
|
|
338
|
+
if x < leftmin: self.figure.canvas.set_cursor(cursors.POINTER)
|
|
339
|
+
elif x <= leftmax: self.figure.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
|
|
340
|
+
elif x < rightmin: self.figure.canvas.set_cursor(cursors.MOVE)
|
|
341
|
+
elif x <= rightmax: self.figure.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
|
|
342
|
+
else: self.figure.canvas.set_cursor(cursors.POINTER)
|
|
343
|
+
return
|
|
344
|
+
|
|
345
|
+
def _check_ax(self, e: MouseEvent):
|
|
346
|
+
ax = e.inaxes
|
|
347
|
+
if not ax or e.xdata is None or e.ydata is None:
|
|
348
|
+
self.in_slider, self.in_price_chart, self.in_volume_chart = (False, False, False)
|
|
349
|
+
else:
|
|
350
|
+
if ax is self.ax_slider:
|
|
351
|
+
self.in_slider = True
|
|
352
|
+
self.in_price_chart = False
|
|
353
|
+
self.in_volume_chart = False
|
|
354
|
+
elif ax is self.ax_price:
|
|
355
|
+
self.in_slider = False
|
|
356
|
+
self.in_price_chart = True
|
|
357
|
+
self.in_volume_chart = False
|
|
358
|
+
elif ax is self.ax_volume:
|
|
359
|
+
self.in_slider = False
|
|
360
|
+
self.in_price_chart = False
|
|
361
|
+
self.in_volume_chart = True
|
|
362
|
+
else:
|
|
363
|
+
self.in_slider = False
|
|
364
|
+
self.in_price_chart = False
|
|
365
|
+
self.in_volume_chart = False
|
|
366
|
+
return
|
|
367
|
+
|
|
368
|
+
def _on_move_slider(self, e: MouseEvent):
|
|
369
|
+
x = e.xdata
|
|
370
|
+
if self.intx is not None:
|
|
371
|
+
renderer = self.figure.canvas.renderer
|
|
372
|
+
self.collection_slider_vline.set_segments([((x, self.slider_ymin), (x, self.slider_ymax))])
|
|
373
|
+
self.collection_slider_vline.draw(renderer)
|
|
374
|
+
|
|
375
|
+
if self.in_slider:
|
|
376
|
+
self.artist_text_slider.set_text(f'{self.df[self.date][self.intx]}')
|
|
377
|
+
self.artist_text_slider.set_x(x)
|
|
378
|
+
self.artist_text_slider.draw(renderer)
|
|
379
|
+
return
|
|
380
|
+
|
|
381
|
+
def _draw_crossline(self, e: MouseEvent):
|
|
382
|
+
self.collection_slider_vline.set_segments([((e.xdata, self.slider_ymin), (e.xdata, self.slider_ymax))])
|
|
383
|
+
self.collection_slider_vline.draw(self.figure.canvas.renderer)
|
|
384
|
+
return super()._draw_crossline(e)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
class ClickMixin(MouseMoveMixin):
|
|
388
|
+
x_click = None
|
|
389
|
+
is_move = False
|
|
390
|
+
click_navleft, click_navright = (False, False)
|
|
391
|
+
|
|
392
|
+
def _connect_event(self):
|
|
393
|
+
super()._connect_event()
|
|
394
|
+
|
|
395
|
+
self.figure.canvas.mpl_connect('button_press_event', lambda x: self._on_click(x))
|
|
396
|
+
return
|
|
397
|
+
|
|
398
|
+
def _on_click(self, e: MouseEvent):
|
|
399
|
+
if self.in_slider and not self.is_click_slider and e.button == MouseButton.LEFT: self._on_click_slider(e)
|
|
400
|
+
return
|
|
401
|
+
|
|
402
|
+
def _on_click_slider(self, e: MouseEvent):
|
|
403
|
+
self.background_with_nav_pre = self.background_with_nav
|
|
404
|
+
|
|
405
|
+
self.is_click_slider = True
|
|
406
|
+
self.figure.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
|
|
407
|
+
|
|
408
|
+
navleft, navright = self.navcoordinate
|
|
409
|
+
x = e.xdata.__round__()
|
|
410
|
+
|
|
411
|
+
leftmax = navleft + self._navLineWidth_half
|
|
412
|
+
rightmin = navright - self._navLineWidth_half
|
|
413
|
+
|
|
414
|
+
grater_than_left = leftmax < x
|
|
415
|
+
less_then_right = x < rightmin
|
|
416
|
+
if grater_than_left and less_then_right:
|
|
417
|
+
self.is_move = True
|
|
418
|
+
self.x_click = x
|
|
419
|
+
else:
|
|
420
|
+
leftmin = navleft - self._navLineWidth
|
|
421
|
+
rightmax = navright + self._navLineWidth
|
|
422
|
+
if not grater_than_left and leftmin <= x:
|
|
423
|
+
self.click_navleft = True
|
|
424
|
+
self.x_click = navright
|
|
425
|
+
elif not less_then_right and x <= rightmax:
|
|
426
|
+
self.click_navright = True
|
|
427
|
+
self.x_click = navleft
|
|
428
|
+
else:
|
|
429
|
+
self.x_click = x
|
|
430
|
+
return
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
class SliderSelectMixin(ClickMixin):
|
|
434
|
+
limit_ma = 8_000
|
|
435
|
+
|
|
436
|
+
def _on_move_slider(self, e):
|
|
437
|
+
if self.is_click_slider: self._set_navcoordinate(e)
|
|
438
|
+
return super()._on_move_slider(e)
|
|
439
|
+
|
|
440
|
+
def _set_navcoordinate(self, e: MouseEvent):
|
|
441
|
+
navmin, navmax = self.navcoordinate
|
|
442
|
+
|
|
443
|
+
x = e.xdata.__int__()
|
|
444
|
+
if self.is_move:
|
|
445
|
+
xsub = self.x_click - x
|
|
446
|
+
navmin, navmax = (navmin-xsub, navmax-xsub)
|
|
447
|
+
|
|
448
|
+
# 값 보정
|
|
449
|
+
if navmax < 0: navmin, navmax = (navmin-navmax, 0)
|
|
450
|
+
if self.list_index[-1] < navmin: navmin, navmax = (self.list_index[-1], self.list_index[-1] + (navmax-navmin))
|
|
451
|
+
|
|
452
|
+
self.navcoordinate = (navmin, navmax)
|
|
453
|
+
self.x_click = x
|
|
454
|
+
|
|
455
|
+
self._set_lim(navmin, navmax+1, simpler=True, set_ma=(navmax-navmin < self.limit_ma))
|
|
456
|
+
|
|
457
|
+
self._set_navigator(navmin, navmax)
|
|
458
|
+
self.collection_navigator.draw(self.figure.canvas.renderer)
|
|
459
|
+
|
|
460
|
+
self._draw_artist()
|
|
461
|
+
self.background_with_nav = self.figure.canvas.renderer.copy_from_bbox(self.figure.bbox)
|
|
462
|
+
self._restore_region()
|
|
463
|
+
else:
|
|
464
|
+
navmin, navmax = (x, self.x_click) if x < self.x_click else (self.x_click, x)
|
|
465
|
+
|
|
466
|
+
# 슬라이더가 차트를 벗어나지 않도록 선택 영역 제한
|
|
467
|
+
if navmax < 0 or self.list_index[-1] < navmin:
|
|
468
|
+
seg = self.collection_navigator.get_segments()
|
|
469
|
+
navmin, navmax = (int(seg[1][0][0]), int(seg[3][0][0]))
|
|
470
|
+
|
|
471
|
+
nsub = navmax - navmin
|
|
472
|
+
if nsub < self.min_distance:
|
|
473
|
+
self._restore_region(False, False)
|
|
474
|
+
self._set_navigator(navmin, navmax)
|
|
475
|
+
self.collection_navigator.draw(self.figure.canvas.renderer)
|
|
476
|
+
else:
|
|
477
|
+
self._set_lim(navmin, navmax+1, simpler=True, set_ma=(nsub < self.limit_ma))
|
|
478
|
+
self._set_navigator(navmin, navmax)
|
|
479
|
+
|
|
480
|
+
self.collection_navigator.draw(self.figure.canvas.renderer)
|
|
481
|
+
|
|
482
|
+
self._draw_artist()
|
|
483
|
+
self.background_with_nav = self.figure.canvas.renderer.copy_from_bbox(self.figure.bbox)
|
|
484
|
+
self._restore_region(False, True)
|
|
485
|
+
return
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
class ReleaseMixin(SliderSelectMixin):
|
|
489
|
+
def _connect_event(self):
|
|
490
|
+
super()._connect_event()
|
|
491
|
+
|
|
492
|
+
self.figure.canvas.mpl_connect('button_release_event', lambda x: self._on_release(x))
|
|
493
|
+
return
|
|
494
|
+
|
|
495
|
+
def _on_release(self, e: MouseEvent):
|
|
496
|
+
if self.in_slider and self.is_click_slider and e.button == MouseButton.LEFT: self._on_release_slider(e)
|
|
497
|
+
return
|
|
498
|
+
|
|
499
|
+
def _on_release_slider(self, e: MouseEvent):
|
|
500
|
+
if not self.is_move:
|
|
501
|
+
seg = self.collection_navigator.get_segments()
|
|
502
|
+
navmin, navmax = (int(seg[1][0][0]), int(seg[3][0][0]))
|
|
503
|
+
nsub = navmax - navmin
|
|
504
|
+
if self.min_distance <= nsub: self.navcoordinate = (navmin, navmax)
|
|
505
|
+
else:
|
|
506
|
+
self.background_with_nav = self.background_with_nav_pre
|
|
507
|
+
navmin, navmax = self.navcoordinate
|
|
508
|
+
self._set_lim(navmin, navmax+1, simpler=True, set_ma=(nsub < self.limit_ma))
|
|
509
|
+
self._restore_region(False, True)
|
|
510
|
+
self.figure.canvas.blit()
|
|
511
|
+
self._set_navigator(*self.navcoordinate)
|
|
512
|
+
|
|
513
|
+
self.is_click_slider = False
|
|
514
|
+
self.is_move = False
|
|
515
|
+
self.click_navleft, self.click_navright = (False, False)
|
|
516
|
+
|
|
517
|
+
self.figure.canvas.draw()
|
|
518
|
+
return
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
class ChartClickMixin(ReleaseMixin):
|
|
522
|
+
is_click_chart = False
|
|
523
|
+
|
|
524
|
+
def _on_click(self, e: MouseEvent):
|
|
525
|
+
if e.button == MouseButton.LEFT:
|
|
526
|
+
if (
|
|
527
|
+
not self.is_click_chart
|
|
528
|
+
and (self.in_price_chart or self.in_volume_chart)
|
|
529
|
+
): self._on_click_chart(e)
|
|
530
|
+
elif not self.is_click_slider and self.in_slider:
|
|
531
|
+
self._on_click_slider(e)
|
|
532
|
+
return
|
|
533
|
+
|
|
534
|
+
def _on_click_chart(self, e: MouseEvent):
|
|
535
|
+
self.is_click_chart = True
|
|
536
|
+
x = e.xdata.__int__()
|
|
537
|
+
self.x_click = x - self.navcoordinate[0]
|
|
538
|
+
self.figure.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
|
|
539
|
+
return
|
|
540
|
+
|
|
541
|
+
def _on_release(self, e):
|
|
542
|
+
if e.button == MouseButton.LEFT:
|
|
543
|
+
if (
|
|
544
|
+
self.is_click_chart
|
|
545
|
+
and (self.in_price_chart or self.in_volume_chart)
|
|
546
|
+
): self._on_release_chart(e)
|
|
547
|
+
elif self.is_click_slider and self.in_slider:
|
|
548
|
+
self._on_release_slider(e)
|
|
549
|
+
return
|
|
550
|
+
|
|
551
|
+
def _on_release_chart(self, e):
|
|
552
|
+
self.is_click_chart = False
|
|
553
|
+
self.figure.canvas.set_cursor(cursors.POINTER)
|
|
554
|
+
return
|
|
555
|
+
|
|
556
|
+
def _change_cursor(self, e):
|
|
557
|
+
if self.is_click_chart: return
|
|
558
|
+
return super()._change_cursor(e)
|
|
559
|
+
|
|
560
|
+
def _on_move(self, e):
|
|
561
|
+
self._on_move_action(e)
|
|
562
|
+
|
|
563
|
+
if self.in_slider:
|
|
564
|
+
self._restore_region(self.is_click_slider)
|
|
565
|
+
self._on_move_slider(e)
|
|
566
|
+
self.figure.canvas.blit()
|
|
567
|
+
elif self.in_price_chart or self.in_volume_chart:
|
|
568
|
+
self._restore_region(self.is_click_chart)
|
|
569
|
+
if not self.is_click_chart:
|
|
570
|
+
self._draw_crossline(e)
|
|
571
|
+
else: self._move_chart(e)
|
|
572
|
+
self.figure.canvas.blit()
|
|
573
|
+
else:
|
|
574
|
+
if self._erase_crossline():
|
|
575
|
+
self._restore_region()
|
|
576
|
+
self.figure.canvas.blit()
|
|
577
|
+
return
|
|
578
|
+
|
|
579
|
+
def _move_chart(self, e: MouseEvent):
|
|
580
|
+
left, right = self.navcoordinate
|
|
581
|
+
x = e.xdata.__int__() - left
|
|
582
|
+
xsub = x - self.x_click
|
|
583
|
+
if not xsub:
|
|
584
|
+
self.collection_navigator.draw(self.figure.canvas.renderer)
|
|
585
|
+
self._draw_artist()
|
|
586
|
+
else:
|
|
587
|
+
left, right = (left-xsub, right-xsub)
|
|
588
|
+
if right < 0 or self.df.index[-1] < left: self._restore_region()
|
|
589
|
+
else:
|
|
590
|
+
self.x_click = x
|
|
591
|
+
self.navcoordinate = (left, right)
|
|
592
|
+
self._set_lim(left, right+1, simpler=True, set_ma=((right-left) < self.limit_ma))
|
|
593
|
+
self._set_navigator(left, right)
|
|
594
|
+
self.collection_navigator.draw(self.figure.canvas.renderer)
|
|
595
|
+
|
|
596
|
+
self._draw_artist()
|
|
597
|
+
self.background_with_nav = self.figure.canvas.renderer.copy_from_bbox(self.figure.bbox)
|
|
598
|
+
self._restore_region()
|
|
599
|
+
return
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
class BaseMixin(ChartClickMixin):
|
|
603
|
+
pass
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
class Chart(BaseMixin, Mixin):
|
|
607
|
+
def _draw_artist(self):
|
|
608
|
+
super()._draw_artist()
|
|
609
|
+
return self.draw_artist()
|
|
610
|
+
|
|
611
|
+
def _set_lim(self, xmin, xmax, simpler=False, set_ma=True):
|
|
612
|
+
super()._set_lim(xmin, xmax, simpler, set_ma)
|
|
613
|
+
return self.on_change_xlim(xmin, xmax, simpler, set_ma)
|
|
614
|
+
|
|
615
|
+
def _on_draw(self, e):
|
|
616
|
+
super()._on_draw(e)
|
|
617
|
+
return self.on_draw(e)
|
|
618
|
+
|
|
619
|
+
def _on_pick(self, e):
|
|
620
|
+
self.on_pick(e)
|
|
621
|
+
return super()._on_pick(e)
|
|
622
|
+
|
|
623
|
+
def _on_move(self, e):
|
|
624
|
+
super()._on_move(e)
|
|
625
|
+
return self.on_move(e)
|
|
626
|
+
|
|
627
|
+
def _on_click(self, e):
|
|
628
|
+
super()._on_click(e)
|
|
629
|
+
return self.on_click(e)
|
|
630
|
+
|
|
631
|
+
def on_release(self, e):
|
|
632
|
+
super().on_release(e)
|
|
633
|
+
return self.on_release(e)
|
|
634
|
+
|