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.
Files changed (50) hide show
  1. seolpyo_mplchart/__init__.py +164 -99
  2. seolpyo_mplchart/_base.py +117 -0
  3. seolpyo_mplchart/_chart/__init__.py +137 -0
  4. seolpyo_mplchart/_chart/_base.py +217 -0
  5. seolpyo_mplchart/_chart/_cursor/__init__.py +2 -0
  6. seolpyo_mplchart/_chart/_cursor/_artist.py +217 -0
  7. seolpyo_mplchart/_chart/_cursor/_cursor.py +165 -0
  8. seolpyo_mplchart/_chart/_cursor/_info.py +187 -0
  9. seolpyo_mplchart/_chart/_draw/__init__.py +2 -0
  10. seolpyo_mplchart/_chart/_draw/_artist.py +50 -0
  11. seolpyo_mplchart/_chart/_draw/_data.py +314 -0
  12. seolpyo_mplchart/_chart/_draw/_draw.py +103 -0
  13. seolpyo_mplchart/_chart/_draw/_lim.py +265 -0
  14. seolpyo_mplchart/_chart/_slider/__init__.py +1 -0
  15. seolpyo_mplchart/_chart/_slider/_base.py +268 -0
  16. seolpyo_mplchart/_chart/_slider/_data.py +105 -0
  17. seolpyo_mplchart/_chart/_slider/_mouse.py +176 -0
  18. seolpyo_mplchart/_chart/_slider/_nav.py +204 -0
  19. seolpyo_mplchart/_chart/test.py +121 -0
  20. seolpyo_mplchart/_config/__init__.py +3 -0
  21. seolpyo_mplchart/_config/ax.py +28 -0
  22. seolpyo_mplchart/_config/candle.py +30 -0
  23. seolpyo_mplchart/_config/config.py +21 -0
  24. seolpyo_mplchart/_config/cursor.py +49 -0
  25. seolpyo_mplchart/_config/figure.py +41 -0
  26. seolpyo_mplchart/_config/format.py +51 -0
  27. seolpyo_mplchart/_config/ma.py +15 -0
  28. seolpyo_mplchart/_config/slider/__init__.py +2 -0
  29. seolpyo_mplchart/_config/slider/config.py +24 -0
  30. seolpyo_mplchart/_config/slider/figure.py +20 -0
  31. seolpyo_mplchart/_config/slider/nav.py +9 -0
  32. seolpyo_mplchart/_config/unit.py +19 -0
  33. seolpyo_mplchart/_config/utils.py +67 -0
  34. seolpyo_mplchart/_config/volume.py +26 -0
  35. seolpyo_mplchart/_cursor.py +559 -0
  36. seolpyo_mplchart/_draw.py +634 -0
  37. seolpyo_mplchart/_slider.py +634 -0
  38. seolpyo_mplchart/base.py +70 -67
  39. seolpyo_mplchart/cursor.py +308 -271
  40. seolpyo_mplchart/draw.py +449 -237
  41. seolpyo_mplchart/slider.py +451 -396
  42. seolpyo_mplchart/test.py +173 -24
  43. seolpyo_mplchart/utils.py +15 -4
  44. seolpyo_mplchart/xl_to_dict.py +47 -0
  45. seolpyo_mplchart-2.0.0.3.dist-info/METADATA +710 -0
  46. seolpyo_mplchart-2.0.0.3.dist-info/RECORD +50 -0
  47. {seolpyo_mplchart-0.1.3.1.dist-info → seolpyo_mplchart-2.0.0.3.dist-info}/WHEEL +1 -1
  48. seolpyo_mplchart-0.1.3.1.dist-info/METADATA +0 -49
  49. seolpyo_mplchart-0.1.3.1.dist-info/RECORD +0 -13
  50. {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
+