seolpyo-mplchart 1.4.1__py3-none-any.whl → 2.1.0__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 (83) hide show
  1. seolpyo_mplchart/__init__.py +53 -333
  2. seolpyo_mplchart/_chart/__init__.py +145 -0
  3. seolpyo_mplchart/_chart/_base.py +217 -0
  4. seolpyo_mplchart/_chart/_cursor/__init__.py +2 -0
  5. seolpyo_mplchart/_chart/_cursor/_artist.py +217 -0
  6. seolpyo_mplchart/_chart/_cursor/_cursor.py +165 -0
  7. seolpyo_mplchart/_chart/_cursor/_info.py +187 -0
  8. seolpyo_mplchart/_chart/_draw/__init__.py +2 -0
  9. seolpyo_mplchart/_chart/_draw/_artist.py +50 -0
  10. seolpyo_mplchart/_chart/_draw/_data.py +314 -0
  11. seolpyo_mplchart/_chart/_draw/_draw.py +103 -0
  12. seolpyo_mplchart/_chart/_draw/_lim.py +265 -0
  13. seolpyo_mplchart/_chart/_slider/__init__.py +1 -0
  14. seolpyo_mplchart/_chart/_slider/_base.py +268 -0
  15. seolpyo_mplchart/_chart/_slider/_data.py +105 -0
  16. seolpyo_mplchart/_chart/_slider/_mouse.py +176 -0
  17. seolpyo_mplchart/_chart/_slider/_nav.py +204 -0
  18. seolpyo_mplchart/_chart/base/__init__.py +111 -0
  19. seolpyo_mplchart/_chart/base/a_canvas.py +250 -0
  20. seolpyo_mplchart/_chart/base/b_artist.py +143 -0
  21. seolpyo_mplchart/_chart/base/c_draw.py +100 -0
  22. seolpyo_mplchart/_chart/base/d_segment.py +262 -0
  23. seolpyo_mplchart/_chart/base/e_axis.py +267 -0
  24. seolpyo_mplchart/_chart/base/f_background.py +62 -0
  25. seolpyo_mplchart/_chart/base/g_event.py +66 -0
  26. seolpyo_mplchart/_chart/base/h_data.py +138 -0
  27. seolpyo_mplchart/_chart/base/test.py +58 -0
  28. seolpyo_mplchart/_chart/cursor/__init__.py +125 -0
  29. seolpyo_mplchart/_chart/cursor/b_artist.py +130 -0
  30. seolpyo_mplchart/_chart/cursor/c_draw.py +96 -0
  31. seolpyo_mplchart/_chart/cursor/d_segment.py +359 -0
  32. seolpyo_mplchart/_chart/cursor/e_axis.py +65 -0
  33. seolpyo_mplchart/_chart/cursor/g_event.py +233 -0
  34. seolpyo_mplchart/_chart/cursor/h_data.py +61 -0
  35. seolpyo_mplchart/_chart/cursor/test.py +69 -0
  36. seolpyo_mplchart/_chart/slider/__init__.py +169 -0
  37. seolpyo_mplchart/_chart/slider/a_canvas.py +260 -0
  38. seolpyo_mplchart/_chart/slider/b_artist.py +91 -0
  39. seolpyo_mplchart/_chart/slider/c_draw.py +54 -0
  40. seolpyo_mplchart/_chart/slider/d_segment.py +166 -0
  41. seolpyo_mplchart/_chart/slider/e_axis.py +70 -0
  42. seolpyo_mplchart/_chart/slider/f_background.py +37 -0
  43. seolpyo_mplchart/_chart/slider/g_event.py +353 -0
  44. seolpyo_mplchart/_chart/slider/h_data.py +102 -0
  45. seolpyo_mplchart/_chart/slider/test.py +71 -0
  46. seolpyo_mplchart/_chart/test.py +121 -0
  47. seolpyo_mplchart/_config/__init__.py +3 -0
  48. seolpyo_mplchart/_config/ax.py +28 -0
  49. seolpyo_mplchart/_config/candle.py +31 -0
  50. seolpyo_mplchart/_config/config.py +21 -0
  51. seolpyo_mplchart/_config/cursor.py +49 -0
  52. seolpyo_mplchart/_config/figure.py +40 -0
  53. seolpyo_mplchart/_config/format.py +51 -0
  54. seolpyo_mplchart/_config/ma.py +17 -0
  55. seolpyo_mplchart/_config/slider/__init__.py +2 -0
  56. seolpyo_mplchart/_config/slider/config.py +24 -0
  57. seolpyo_mplchart/_config/slider/figure.py +19 -0
  58. seolpyo_mplchart/_config/slider/nav.py +10 -0
  59. seolpyo_mplchart/_config/unit.py +19 -0
  60. seolpyo_mplchart/_config/utils.py +67 -0
  61. seolpyo_mplchart/_config/volume.py +27 -0
  62. seolpyo_mplchart/_cursor.py +27 -25
  63. seolpyo_mplchart/_draw.py +7 -18
  64. seolpyo_mplchart/_slider.py +26 -20
  65. seolpyo_mplchart/_utils/__init__.py +10 -0
  66. seolpyo_mplchart/_utils/nums.py +67 -0
  67. seolpyo_mplchart/_utils/theme/__init__.py +15 -0
  68. seolpyo_mplchart/_utils/theme/dark.py +57 -0
  69. seolpyo_mplchart/_utils/theme/light.py +56 -0
  70. seolpyo_mplchart/_utils/utils.py +28 -0
  71. seolpyo_mplchart/_utils/xl/__init__.py +15 -0
  72. seolpyo_mplchart/_utils/xl/csv.py +46 -0
  73. seolpyo_mplchart/_utils/xl/xlsx.py +49 -0
  74. seolpyo_mplchart/sample/apple.txt +6058 -0
  75. seolpyo_mplchart/sample/samsung.txt +5938 -0
  76. seolpyo_mplchart/test.py +172 -56
  77. seolpyo_mplchart/xl_to_dict.py +47 -0
  78. seolpyo_mplchart-2.1.0.dist-info/METADATA +718 -0
  79. seolpyo_mplchart-2.1.0.dist-info/RECORD +89 -0
  80. {seolpyo_mplchart-1.4.1.dist-info → seolpyo_mplchart-2.1.0.dist-info}/WHEEL +1 -1
  81. seolpyo_mplchart-1.4.1.dist-info/METADATA +0 -57
  82. seolpyo_mplchart-1.4.1.dist-info/RECORD +0 -17
  83. {seolpyo_mplchart-1.4.1.dist-info → seolpyo_mplchart-2.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,359 @@
1
+ from fractions import Fraction
2
+
3
+ from matplotlib.axes import Axes
4
+ from matplotlib.collections import LineCollection
5
+ from matplotlib.text import Text
6
+ from matplotlib.backend_bases import MouseEvent
7
+ import pandas as pd
8
+
9
+ from ..._config import ConfigData
10
+ from ..._utils.nums import float_to_str
11
+ from ..base.a_canvas import Figure
12
+
13
+
14
+ class Base:
15
+ CONFIG: ConfigData
16
+
17
+ key_volume: str
18
+ _length_text: int
19
+ df: pd.DataFrame
20
+
21
+ watermark: str
22
+
23
+ v0: int
24
+ v1: int
25
+ vmiddle: int
26
+ vxmin: int
27
+ vxmax: int
28
+ price_ymin: int
29
+ price_ymax: int
30
+ volume_ymax: int
31
+
32
+ figure: Figure
33
+ ax_legend: Axes
34
+ ax_price: Axes
35
+ ax_volume: Axes
36
+ artist_watermark: Text
37
+ collection_candle: LineCollection
38
+ collection_volume: LineCollection
39
+ collection_ma: LineCollection
40
+
41
+ collection_price_crossline: LineCollection
42
+ collection_volume_crossline: LineCollection
43
+
44
+ artist_label_x: Text
45
+ artist_label_y: Text
46
+
47
+ collection_box_price: LineCollection
48
+ collection_box_volume: LineCollection
49
+
50
+ artist_info_candle: Text
51
+ artist_info_volume: Text
52
+
53
+ in_chart_price: bool
54
+ in_chart_volume: bool
55
+
56
+
57
+ class CrosslineMixin(Base):
58
+ def _set_crossline(self, e: MouseEvent,):
59
+ x, y = (e.xdata, e.ydata)
60
+
61
+ if self.in_chart_price:
62
+ seg = [
63
+ [
64
+ (x, self.price_ymin),
65
+ (x, self.price_ymax),
66
+ ],
67
+ [
68
+ (self.vxmin, y),
69
+ (self.vxmax, y),
70
+ ]
71
+ ]
72
+ self.collection_price_crossline.set_segments(seg)
73
+ seg = [
74
+ [
75
+ (x, 0),
76
+ (x, self.volume_ymax),
77
+ ]
78
+ ]
79
+ self.collection_volume_crossline.set_segments(seg)
80
+ elif self.in_chart_volume:
81
+ seg = [
82
+ [
83
+ (x, self.price_ymin),
84
+ (x, self.price_ymax),
85
+ ]
86
+ ]
87
+ self.collection_price_crossline.set_segments(seg)
88
+ seg = [
89
+ [
90
+ (x, 0),
91
+ (x, self.volume_ymax),
92
+ ],
93
+ [
94
+ (self.vxmin, y),
95
+ (self.vxmax, y)
96
+ ]
97
+ ]
98
+ self.collection_volume_crossline.set_segments(seg)
99
+
100
+ return
101
+
102
+
103
+ class LabelMixin(Base):
104
+ def _set_label_x(self, e: MouseEvent):
105
+ xdata, ydata = (e.xdata, e.xdata)
106
+ # print(f'{(x, xdata)=}')
107
+
108
+ if xdata < 0 or xdata is None:
109
+ return
110
+
111
+ try:
112
+ text = self.df.iloc[int(xdata)]['date']
113
+ except:
114
+ return
115
+ # print(f'{text=}')
116
+
117
+ display_coords = e.inaxes.transData.transform((xdata, ydata))
118
+ figure_coords = self.figure.transFigure.inverted()\
119
+ .transform(display_coords)
120
+ # print(f'{figure_coords=}')
121
+
122
+ artist = self.artist_label_x
123
+
124
+ artist.set_text(text)
125
+ artist.set_x(figure_coords[0])
126
+ self._set_label_x_position()
127
+ return 1
128
+
129
+ def _set_label_x_position(self):
130
+ artist = self.artist_label_x
131
+ renderer = self.figure.canvas.renderer
132
+
133
+ # Axes 하단 경계 좌표
134
+ boundary = self.ax_volume.get_position()\
135
+ .y0
136
+ # print(f'{y0=}')
137
+
138
+ if not artist.get_text():
139
+ artist.set_text(' ')
140
+
141
+ # Text bbox 너비
142
+ bbox = artist.get_bbox_patch()\
143
+ .get_window_extent(renderer)
144
+ bbox_size = bbox.height
145
+ # 밀어야 하는 값
146
+ fig_size = self.figure.bbox.height
147
+ offset = (bbox_size + 10) / fig_size
148
+ # print(f'{box_width_fig=}')
149
+
150
+ # x축 값(가격 또는 거래량)
151
+ # self.artist_label_y.set_x(x1)
152
+ y = boundary - (offset / 2)
153
+ # print(f'{(x1, x)=}')
154
+ artist.set_y(y)
155
+ return
156
+
157
+ def _set_label_y(self, e: MouseEvent, *, is_price_chart):
158
+ xdata, ydata = (e.xdata, e.ydata)
159
+ artist = self.artist_label_y
160
+
161
+ if is_price_chart:
162
+ text = float_to_str(ydata, digit=self.CONFIG.UNIT.digit) + self.CONFIG.UNIT.price
163
+ else:
164
+ text = float_to_str(ydata, digit=self.CONFIG.UNIT.digit_volume) + self.CONFIG.UNIT.volume
165
+
166
+ display_coords = e.inaxes.transData.transform((xdata, ydata))
167
+ figure_coords = self.figure.transFigure.inverted().transform(display_coords)
168
+
169
+ artist.set_text(text)
170
+ artist.set_y(figure_coords[1])
171
+ self._set_label_y_position()
172
+ return
173
+
174
+ def _set_label_y_position(self):
175
+ artist = self.artist_label_y
176
+ renderer = self.figure.canvas.renderer
177
+
178
+ # Axes 우측 경계 좌표
179
+ boundary = self.ax_volume.get_position()\
180
+ .x1
181
+ # print(f'{boundary=}')
182
+
183
+ if not artist.get_text():
184
+ artist.set_text(' ')
185
+
186
+ # Text bbox 너비
187
+ bbox = artist.get_bbox_patch()\
188
+ .get_window_extent(renderer)
189
+ bbox_size = bbox.width
190
+ # 밀어야 하는 값
191
+ fig_size = self.figure.bbox.width
192
+ offset = (bbox_size + 8) / fig_size
193
+ # print(f'{box_width_fig=}')
194
+
195
+ # x축 값(가격 또는 거래량)
196
+ # self.artist_label_y.set_x(x1)
197
+ x = boundary + (offset / 2)
198
+ # print(f'{(x1, x)=}')
199
+ artist.set_x(x)
200
+ return
201
+
202
+
203
+ class BoxMixin(Base):
204
+ def _set_box_candle(self, segment):
205
+ self.collection_box_price.set_segments(segment)
206
+ return
207
+
208
+ def _set_box_volume(self, segment):
209
+ self.collection_box_volume.set_segments(segment)
210
+ return
211
+
212
+
213
+ class InfoMixin(Base):
214
+ fraction = False
215
+
216
+ def _set_info_candle(self, ind):
217
+ text = self._get_info(ind, is_price=True)
218
+ self.artist_info_candle.set_text(text)
219
+
220
+ # 정보 텍스트를 중앙에 몰리게 설정할 수도 있지만,
221
+ # 그런 경우 차트를 가리므로 좌우 끝단에 위치하도록 설정
222
+ if self.vmiddle < ind:
223
+ self.artist_info_candle.set_x(self.v0)
224
+ else:
225
+ # self.artist_info_candle.set_x(self.vmax - self.x_distance)
226
+ # self.artist_info_candle.set_horizontalalignment('right')
227
+ # 텍스트박스 크기 가져오기
228
+ bbox = self.artist_info_candle.get_window_extent()\
229
+ .transformed(self.ax_price.transData.inverted())
230
+ width = bbox.x1 - bbox.x0
231
+ self.artist_info_candle.set_x(self.v1 - width)
232
+
233
+ self.artist_info_candle.draw(self.figure.canvas.renderer)
234
+ return
235
+
236
+ def _set_info_volume(self, ind):
237
+ text = self._get_info(ind, is_price=False)
238
+ self.artist_info_volume.set_text(text)
239
+
240
+ if self.vmiddle < ind:
241
+ self.artist_info_volume.set_x(self.v0)
242
+ else:
243
+ # self.artist_info_volume.set_x(self.vmax - self.x_distance)
244
+ # self.artist_info_volume.set_horizontalalignment('right')
245
+ # 텍스트박스 크기 가져오기
246
+ bbox = self.artist_info_volume.get_window_extent()\
247
+ .transformed(self.ax_price.transData.inverted())
248
+ width = bbox.x1 - bbox.x0
249
+ self.artist_info_volume.set_x(self.v1 - width)
250
+
251
+ self.artist_info_volume.draw(self.figure.canvas.renderer)
252
+ return
253
+
254
+ def get_info_kwargs(self, is_price: bool, **kwargs)-> dict:
255
+ """
256
+ get text info kwargs
257
+
258
+ Args:
259
+ is_price (bool): is price chart info or not
260
+
261
+ Returns:
262
+ dict[str, any]: text info kwargs
263
+ """
264
+ return kwargs
265
+
266
+ def _get_info(self, ind: int, is_price=True):
267
+ # print(f'{self._length_text=}')
268
+ series = self.df.iloc[ind]
269
+
270
+ dt = series['date']
271
+ if not self.key_volume:
272
+ v, vr = ('-', '-')
273
+ else:
274
+ v, vr = series.loc[['volume', 'rate_volume']]
275
+ # print(f'{self.CONFIG.UNIT.digit_volume=}')
276
+ v = float_to_str(v, digit=self.CONFIG.UNIT.digit_volume)
277
+ # if not v % 1:
278
+ # v = int(v)
279
+ vr = f'{vr:+06,.2f}'
280
+
281
+ if is_price:
282
+ o, h, l, c = (series['open'], series['high'], series['low'], series['close'])
283
+ rate, compare = (series['rate'], series['compare'])
284
+ r = f'{rate:+06,.2f}'
285
+ Or, hr, lr = (series['rate_open'], series['rate_high'], series['rate_low'])
286
+
287
+ if self.fraction:
288
+ data = {}
289
+ c = round(c, self.CONFIG.UNIT.digit)
290
+ for value, key in [
291
+ [c, 'close'],
292
+ [compare, 'compare'],
293
+ [o, 'open'],
294
+ [h, 'high'],
295
+ [l, 'low'],
296
+ ]:
297
+ div = divmod(value, 1)
298
+ if div[1]:
299
+ if div[0]:
300
+ data[key] = f'{float_to_str(div[0])} {Fraction((div[1]))}'
301
+ else:
302
+ data[key] = f'  {Fraction((div[1]))}'
303
+ else:
304
+ data[key] = float_to_str(div[0])
305
+ # print(f'{data=}')
306
+
307
+ kwargs = self.get_info_kwargs(
308
+ is_price=is_price,
309
+ dt=dt,
310
+ close=f'{data["close"]:>{self._length_text}}{self.CONFIG.UNIT.price}',
311
+ rate=f'{r:>{self._length_text}}%',
312
+ compare=f'{data["compare"]:>{self._length_text}}{self.CONFIG.UNIT.price}',
313
+ open=f'{data["open"]:>{self._length_text}}{self.CONFIG.UNIT.price}', rate_open=f'{Or:+06,.2f}%',
314
+ high=f'{data["high"]:>{self._length_text}}{self.CONFIG.UNIT.price}', rate_high=f'{hr:+06,.2f}%',
315
+ low=f'{data["low"]:>{self._length_text}}{self.CONFIG.UNIT.price}', rate_low=f'{lr:+06,.2f}%',
316
+ volume=f'{v:>{self._length_text}}{self.CONFIG.UNIT.volume}', rate_volume=f'{vr}%',
317
+ )
318
+ text = self.CONFIG.FORMAT.candle.format(**kwargs)
319
+ else:
320
+ o, h, l, c = (
321
+ float_to_str(o, digit=self.CONFIG.UNIT.digit),
322
+ float_to_str(h, digit=self.CONFIG.UNIT.digit),
323
+ float_to_str(l, digit=self.CONFIG.UNIT.digit),
324
+ float_to_str(c, digit=self.CONFIG.UNIT.digit),
325
+ )
326
+ com = float_to_str(compare, digit=self.CONFIG.UNIT.digit, plus=True)
327
+
328
+ kwargs = self.get_info_kwargs(
329
+ is_price=is_price,
330
+ dt=dt,
331
+ close=f'{c:>{self._length_text}}{self.CONFIG.UNIT.price}',
332
+ rate=f'{r:>{self._length_text}}%',
333
+ compare=f'{com:>{self._length_text}}{self.CONFIG.UNIT.price}',
334
+ open=f'{o:>{self._length_text}}{self.CONFIG.UNIT.price}', rate_open=f'{Or:+06,.2f}%',
335
+ high=f'{h:>{self._length_text}}{self.CONFIG.UNIT.price}', rate_high=f'{hr:+06,.2f}%',
336
+ low=f'{l:>{self._length_text}}{self.CONFIG.UNIT.price}', rate_low=f'{lr:+06,.2f}%',
337
+ volume=f'{v:>{self._length_text}}{self.CONFIG.UNIT.volume}', rate_volume=f'{vr}%',
338
+ )
339
+ text = self.CONFIG.FORMAT.candle.format(**kwargs)
340
+ elif self.key_volume:
341
+ compare = self.df.loc[ind, 'compare_volume']
342
+ com = float_to_str(compare, digit=self.CONFIG.UNIT.digit_volume, plus=True)
343
+ kwargs = self.get_info_kwargs(
344
+ is_price=is_price,
345
+ dt=dt,
346
+ volume=f'{v:>{self._length_text}}{self.CONFIG.UNIT.volume}',
347
+ rate_volume=f'{vr:>{self._length_text}}%',
348
+ compare=f'{com:>{self._length_text}}{self.CONFIG.UNIT.volume}',
349
+ )
350
+ text = self.CONFIG.FORMAT.volume.format(**kwargs)
351
+ else:
352
+ text = ''
353
+
354
+ return text
355
+
356
+
357
+ class SegmentMixin(CrosslineMixin, LabelMixin, BoxMixin, InfoMixin):
358
+ fraction = False
359
+
@@ -0,0 +1,65 @@
1
+ from matplotlib.axes import Axes
2
+ from matplotlib.text import Text
3
+
4
+ from ..base.a_canvas import Figure
5
+
6
+
7
+
8
+ class Base:
9
+ figure: Figure
10
+ ax_price: Axes
11
+ ax_volume: Axes
12
+
13
+ key_volume: str
14
+
15
+ vxmin: int
16
+ vxmax: int
17
+ price_ymin: int
18
+ price_ymax: int
19
+ volume_ymax: int
20
+
21
+ artist_info_candle: Text
22
+ artist_info_volume: Text
23
+ artist_label_x: Text
24
+ artist_label_y: Text
25
+
26
+ axis: callable
27
+
28
+
29
+ class Mixin(Base):
30
+ def axis(self, xmin, *, xmax):
31
+ super().axis(xmin, xmax=xmax)
32
+ # print('cursor axis')
33
+
34
+ psub = (self.price_ymax - self.price_ymin)
35
+ self.min_height_box_candle = psub / 8
36
+
37
+ pydistance = psub / 20
38
+
39
+ self.min_height_box_volume = 10
40
+ if self.key_volume:
41
+ self.min_height_box_volume = self.volume_ymax / 4
42
+
43
+ vxsub = self.vxmax - self.vxmin
44
+ self.vmiddle = self.vxmax - int((vxsub) / 2)
45
+
46
+ vxdistance = vxsub / 50
47
+ self.v0, self.v1 = (self.vxmin + vxdistance, self.vxmax - vxdistance)
48
+
49
+ yvolume = self.volume_ymax * 0.85
50
+
51
+ # 정보 텍스트박스 y축 설정
52
+ self.artist_info_candle.set_y(self.price_ymax - pydistance)
53
+ self.artist_info_volume.set_y(yvolume)
54
+
55
+ return
56
+
57
+
58
+ class AxisMixin(Mixin):
59
+ v0: int
60
+ v1: int
61
+ vmiddle: int
62
+
63
+ min_height_box_candle: float
64
+ min_height_box_volume: float
65
+
@@ -0,0 +1,233 @@
1
+ from matplotlib.axes import Axes
2
+ from matplotlib.backend_bases import MouseEvent
3
+ from matplotlib.collections import LineCollection
4
+ from matplotlib.text import Text
5
+ import pandas as pd
6
+
7
+ from ..base.a_canvas import Figure
8
+
9
+
10
+ class Base:
11
+ figure: Figure
12
+ ax_price: Axes
13
+ ax_volume: Axes
14
+
15
+ key_volume: str
16
+ df: pd.DataFrame
17
+
18
+ vxmin: int
19
+ vxmax: int
20
+ price_ymin: float
21
+ price_ymax: float
22
+ volume_ymax: float
23
+
24
+ collection_box_price: LineCollection
25
+ collection_box_volume: LineCollection
26
+ collection_price_crossline: LineCollection
27
+
28
+ index_list: list
29
+
30
+ min_height_box_candle: float
31
+ min_height_box_volume: float
32
+
33
+ connect_events: callable
34
+
35
+ artist_label_x: Text
36
+ artist_label_y: Text
37
+
38
+ in_chart = False
39
+ in_chart_price = False
40
+ in_chart_volume = False
41
+
42
+ in_candle = False
43
+ in_volume = False
44
+
45
+ _restore_region: callable
46
+
47
+ _set_crossline: callable
48
+ _draw_crossline: callable
49
+ _set_label_x: callable
50
+ _draw_label_x: callable
51
+
52
+ _set_label_y: callable
53
+ _draw_label_y: callable
54
+
55
+ _set_box_candle: callable
56
+ _draw_box_candle: callable
57
+ _set_box_volume: callable
58
+ _draw_box_volume: callable
59
+
60
+ _set_info_candle: callable
61
+ _draw_info_candle: callable
62
+ _set_info_volume: callable
63
+ _draw_info_volume: callable
64
+
65
+
66
+ class AxMixin(Base):
67
+ def _check_ax(self, e: MouseEvent):
68
+ ax = e.inaxes
69
+ # print(f'{ax=}')
70
+ self.in_chart = False
71
+ self.in_chart_price, self.in_chart_volume = (False, False)
72
+
73
+ if e.xdata is None or e.ydata is None:
74
+ return
75
+
76
+ if self.vxmin <= e.xdata and e.xdata <= self.vxmax:
77
+ if ax is self.ax_price and (
78
+ self.price_ymin <= e.ydata and e.ydata <= self.price_ymax
79
+ ):
80
+ self.in_chart = True
81
+ self.in_chart_price = True
82
+ elif ax is self.ax_volume and (
83
+ 0 <= e.ydata and e.ydata <= self.volume_ymax
84
+ ):
85
+ self.in_chart = True
86
+ self.in_chart_volume = True
87
+ return
88
+
89
+
90
+ class BoxMixin(Base):
91
+ def _draw_box_artist(self, e: MouseEvent):
92
+ xdata, ydata = (e.xdata, e.ydata)
93
+ ind = int(xdata)
94
+
95
+ self.in_candle, self.in_volume = (False, False)
96
+
97
+ if self.in_chart_price:
98
+ series = self.df.iloc[ind]
99
+ # print(f'{series=}')
100
+ # 박스 크기
101
+ high = series['box_candle_top']
102
+ low = series['box_candle_bottom']
103
+ height = series['box_candle_height']
104
+ # print(f'{(low, high)=}')
105
+ # print(f'{height=}')
106
+
107
+ # 박스 높이 보정
108
+ if height < self.min_height_box_candle:
109
+ sub = (self.min_height_box_candle - height) / 2
110
+ high, low = (high+sub, low-sub)
111
+
112
+ # 커서가 캔들 사이에 있는지 확인
113
+ if low <= ydata and ydata <= high:
114
+ self.in_candle = True
115
+
116
+ # 캔들 강조
117
+ x0, x1 = (ind-0.3, ind+1.3)
118
+ segment = [(
119
+ (x0, high),
120
+ (x1, high),
121
+ (x1, low),
122
+ (x0, low),
123
+ (x0, high)
124
+ )]
125
+ self._set_box_candle(segment)
126
+ self._draw_box_candle()
127
+
128
+ return 1
129
+ elif self.in_chart_volume and self.key_volume:
130
+ # 박스 크기
131
+ high = self.df.iloc[ind]['box_volume_top']
132
+ low = 0
133
+ if high < self.min_height_box_volume:
134
+ high = self.min_height_box_volume
135
+
136
+ if low <= ydata and ydata <= high:
137
+ # 거래량 강조
138
+ self.in_volume = True
139
+
140
+ x0, x1 = (ind-0.3, ind+1.3)
141
+ segment = [(
142
+ (x0, high),
143
+ (x1, high),
144
+ (x1, low),
145
+ (x0, low),
146
+ (x0, high)
147
+ )]
148
+ self._set_box_volume(segment)
149
+ self._draw_box_volume()
150
+
151
+ return 1
152
+ return
153
+
154
+
155
+ class InfoMixin(Base):
156
+ def _draw_info_artist(self, e: MouseEvent):
157
+ return
158
+
159
+
160
+ class Mixin(AxMixin, BoxMixin, InfoMixin):
161
+ _in_mouse_move = False
162
+
163
+ def need_restore(self):
164
+ if self.collection_price_crossline.get_segments():
165
+ self.collection_price_crossline.set_segments([])
166
+ return True
167
+ return
168
+
169
+ def _on_move(self, e: MouseEvent):
170
+ # print(f'{not self._in_mouse_move=}')
171
+ if not self._in_mouse_move:
172
+ self._in_mouse_move = True
173
+ # print(f'{(e.xdata, e.ydata)=}')
174
+ self.on_move(e)
175
+ self._in_mouse_move = False
176
+ return
177
+
178
+ def on_move(self, e: MouseEvent):
179
+ self._check_ax(e)
180
+ self._on_move_action(e)
181
+ return
182
+
183
+ def _set_and_draw_crossline(self, e: MouseEvent):
184
+ self._set_crossline(e)
185
+ self._draw_crossline()
186
+
187
+ if self._set_label_x(e):
188
+ # print('draw label x')
189
+ self._draw_label_x()
190
+ if self._draw_box_artist(e):
191
+ ind = int(e.xdata)
192
+ if self.in_chart_price:
193
+ self._set_info_candle(ind)
194
+ self._draw_info_candle()
195
+ else:
196
+ self._set_info_volume(ind)
197
+ self._draw_info_volume()
198
+ self._set_label_y(e, is_price_chart=self.in_chart_price)
199
+ self._draw_label_y()
200
+
201
+ return
202
+
203
+ def _on_move_action(self, e: MouseEvent):
204
+ if self.in_chart:
205
+ self._restore_region()
206
+
207
+ self._set_and_draw_crossline(e)
208
+
209
+ self.figure.canvas.blit()
210
+ self.figure.canvas.flush_events()
211
+ elif self.need_restore():
212
+ self._restore_region()
213
+ self.figure.canvas.blit()
214
+ self.figure.canvas.flush_events()
215
+ return
216
+
217
+
218
+ class EventMixin(Mixin):
219
+ in_chart = False
220
+ in_chart_price = False
221
+ in_chart_volume = False
222
+
223
+ in_candle = False
224
+ in_volume = False
225
+
226
+ _in_mouse_move = False
227
+
228
+ def connect_events(self):
229
+ super().connect_events()
230
+
231
+ self.figure.canvas.mpl_connect('motion_notify_event', lambda x: self._on_move(x))
232
+ return
233
+