seolpyo-mplchart 1.4.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 (44) hide show
  1. seolpyo_mplchart/__init__.py +144 -308
  2. seolpyo_mplchart/_chart/__init__.py +137 -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/test.py +121 -0
  19. seolpyo_mplchart/_config/__init__.py +3 -0
  20. seolpyo_mplchart/_config/ax.py +28 -0
  21. seolpyo_mplchart/_config/candle.py +30 -0
  22. seolpyo_mplchart/_config/config.py +21 -0
  23. seolpyo_mplchart/_config/cursor.py +49 -0
  24. seolpyo_mplchart/_config/figure.py +41 -0
  25. seolpyo_mplchart/_config/format.py +51 -0
  26. seolpyo_mplchart/_config/ma.py +15 -0
  27. seolpyo_mplchart/_config/slider/__init__.py +2 -0
  28. seolpyo_mplchart/_config/slider/config.py +24 -0
  29. seolpyo_mplchart/_config/slider/figure.py +20 -0
  30. seolpyo_mplchart/_config/slider/nav.py +9 -0
  31. seolpyo_mplchart/_config/unit.py +19 -0
  32. seolpyo_mplchart/_config/utils.py +67 -0
  33. seolpyo_mplchart/_config/volume.py +26 -0
  34. seolpyo_mplchart/_cursor.py +27 -25
  35. seolpyo_mplchart/_draw.py +7 -18
  36. seolpyo_mplchart/_slider.py +26 -20
  37. seolpyo_mplchart/test.py +172 -56
  38. seolpyo_mplchart/xl_to_dict.py +47 -0
  39. seolpyo_mplchart-2.0.0.3.dist-info/METADATA +710 -0
  40. seolpyo_mplchart-2.0.0.3.dist-info/RECORD +50 -0
  41. {seolpyo_mplchart-1.4.1.dist-info → seolpyo_mplchart-2.0.0.3.dist-info}/WHEEL +1 -1
  42. seolpyo_mplchart-1.4.1.dist-info/METADATA +0 -57
  43. seolpyo_mplchart-1.4.1.dist-info/RECORD +0 -17
  44. {seolpyo_mplchart-1.4.1.dist-info → seolpyo_mplchart-2.0.0.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,217 @@
1
+ import matplotlib.pyplot as plt
2
+ import matplotlib.style as mplstyle
3
+ from matplotlib.axes import Axes
4
+ from matplotlib.backends.backend_agg import FigureCanvasAgg, RendererAgg
5
+ from matplotlib.backend_bases import FigureManagerBase
6
+ from matplotlib.figure import Figure as Fig
7
+ from matplotlib.text import Text
8
+
9
+ from .._config import DEFAULTCONFIG
10
+
11
+ try: plt.switch_backend('TkAgg')
12
+ except: pass
13
+
14
+ # 한글 깨짐 문제 방지
15
+ try: plt.rcParams['font.family'] ='Malgun Gothic'
16
+ except: pass
17
+
18
+ mplstyle.use('fast')
19
+
20
+
21
+ class Canvas(FigureCanvasAgg):
22
+ manager: FigureManagerBase
23
+ renderer = RendererAgg
24
+
25
+ class Figure(Fig):
26
+ canvas: Canvas
27
+
28
+
29
+ class Base:
30
+ watermark = 'seolpyo mplchart'
31
+
32
+ figure: Figure
33
+
34
+ def __init__(self, config=DEFAULTCONFIG):
35
+ # 기본 툴바 비활성화
36
+ plt.rcParams['toolbar'] = 'None'
37
+ # plt.rcParams['figure.dpi'] = 600
38
+
39
+ self.CONFIG = config
40
+ self.add_axes()
41
+ self.set_window()
42
+ return
43
+
44
+ def add_axes(self):
45
+ self.figure, axes = plt.subplots(
46
+ 3, # row 수
47
+ figsize=self.CONFIG.FIGURE.figsize, # 기본 크기
48
+ height_ratios=(
49
+ self.CONFIG.FIGURE.RATIO.legend,
50
+ self.CONFIG.FIGURE.RATIO.price,
51
+ self.CONFIG.FIGURE.RATIO.volume,
52
+ ) # row 크기 비율
53
+ )
54
+
55
+ axes: list[Axes]
56
+ self.ax_legend, self.ax_price, self.ax_volume = axes
57
+ self.ax_legend.set_label('legend ax')
58
+ self.ax_price.set_label('price ax')
59
+ self.ax_volume.set_label('volume ax')
60
+
61
+ # 이평선 라벨 axis 그리지 않기
62
+ self.ax_legend.set_axis_off()
63
+
64
+ # y ticklabel foramt 설정
65
+ self.ax_price.yaxis.set_major_formatter(
66
+ lambda x, _: self.CONFIG.UNIT.func(
67
+ x,
68
+ word=self.CONFIG.UNIT.price,
69
+ digit=self.CONFIG.UNIT.digit
70
+ )
71
+ )
72
+ self.ax_volume.yaxis.set_major_formatter(
73
+ lambda x, _: self.CONFIG.UNIT.func(
74
+ x,
75
+ word=self.CONFIG.UNIT.volume,
76
+ digit=self.CONFIG.UNIT.digit
77
+ )
78
+ )
79
+
80
+ # 공통 설정
81
+ for ax in (self.ax_price, self.ax_volume):
82
+ ax.xaxis.set_animated(True)
83
+ ax.yaxis.set_animated(True)
84
+
85
+ # x tick 외부 눈금 표시하지 않기
86
+ ax.xaxis.set_ticks_position('none')
87
+ # x tick label 제거
88
+ ax.set_xticklabels([])
89
+ # y tick 위치를 우측으로 이동
90
+ ax.tick_params(left=False, right=True, labelleft=False, labelright=True)
91
+ return
92
+
93
+
94
+ def set_window(self):
95
+ self._set_figure()
96
+ self._set_axes()
97
+ return
98
+
99
+ def _set_figure(self):
100
+ self.figure.canvas.manager.set_window_title('Seolpyo MPLChart')
101
+
102
+ # 차트 비율 변경
103
+ # print(f'{self.CONFIG.FIGURE.RATIO.volume=}')
104
+ gs = self.ax_price.get_subplotspec().get_gridspec()
105
+ gs.set_height_ratios([
106
+ self.CONFIG.FIGURE.RATIO.legend,
107
+ self.CONFIG.FIGURE.RATIO.price,
108
+ self.CONFIG.FIGURE.RATIO.volume,
109
+ ])
110
+ # print(f'{gs.get_height_ratios()=}')
111
+ self.figure.tight_layout()
112
+
113
+ # 플롯간 간격 설정(Configure subplots)
114
+ self.figure.subplots_adjust(**self.CONFIG.FIGURE.ADJUST.__dict__)
115
+
116
+ self.figure.set_facecolor(self.CONFIG.FIGURE.facecolor)
117
+ return
118
+
119
+ def _set_axes(self):
120
+ self._set_axes_legend()
121
+
122
+ # 공통 설정
123
+ for ax in (self.ax_price, self.ax_volume):
124
+ # 차트 영역 배경 색상
125
+ ax.set_facecolor(self.CONFIG.AX.facecolor)
126
+
127
+ # Axes 외곽선 색 변경(틱 색과 일치)
128
+ for i in ['top', 'bottom', 'left', 'right']:
129
+ ax.spines[i].set_color(self.CONFIG.AX.TICK.edgecolor)
130
+ # 틱 색상
131
+ ax.tick_params('both', colors=self.CONFIG.AX.TICK.edgecolor)
132
+ # 틱 라벨 색상
133
+ ticklabels: list[Text] = ax.get_xticklabels() + ax.get_yticklabels()
134
+ for ticklabel in ticklabels:
135
+ ticklabel.set_color(self.CONFIG.AX.TICK.fontcolor)
136
+
137
+ # Axes grid(구분선, 격자) 그리기
138
+ # 어째서인지 grid의 zorder 값을 선언해도 1.6을 값으로 한다.
139
+ ax.grid(**self.CONFIG.AX.GRID.__dict__)
140
+ return
141
+
142
+ def _set_axes_legend(self):
143
+ color = self.CONFIG.AX.facecolor
144
+
145
+ # 이평선 라벨 Axes 배경색
146
+ legends = self.ax_legend.get_legend()
147
+ if legends:
148
+ legends.get_frame().set_facecolor(color)
149
+
150
+ # 이평선 라벨 Axes 테두리색
151
+ color = self.CONFIG.AX.TICK.edgecolor
152
+ legends = self.ax_legend.get_legend()
153
+ if legends:
154
+ legends.get_frame().set_edgecolor(color)
155
+
156
+ # 이평선 라벨 폰트 색상
157
+ color = self.CONFIG.AX.TICK.fontcolor
158
+ legends = self.ax_legend.get_legend()
159
+ if legends:
160
+ legend_labels: list[Text] = legends.texts
161
+ for i in legend_labels:
162
+ i.set_color(color)
163
+ return
164
+
165
+
166
+ class ArtistMixin(Base):
167
+ def __init__(self, *args, **kwargs):
168
+ super().__init__(*args, **kwargs)
169
+
170
+ self.add_artists()
171
+ return
172
+
173
+ def add_artists(self):
174
+ self._add_artists()
175
+ return
176
+
177
+ def _add_artists(self):
178
+ self._add_watermark()
179
+ self._set_wartermark()
180
+ return
181
+
182
+ def set_artists(self):
183
+ self._set_artists()
184
+ return
185
+
186
+ def _set_artists(self):
187
+ self._set_wartermark()
188
+ return
189
+
190
+ def _add_watermark(self):
191
+ self.artist_watermark = Text(
192
+ x=0.5, y=0.5, text='',
193
+ animated=True,
194
+ horizontalalignment='center', verticalalignment='center',
195
+ transform=self.ax_price.transAxes
196
+ )
197
+ self.ax_price.add_artist(self.artist_watermark)
198
+ return
199
+
200
+ def _set_wartermark(self):
201
+ self.artist_watermark.set_text(self.watermark)
202
+ self.artist_watermark.set_fontsize(self.CONFIG.FIGURE.WATERMARK.fontsize)
203
+ self.artist_watermark.set_color(self.CONFIG.FIGURE.WATERMARK.color)
204
+ self.artist_watermark.set_alpha(self.CONFIG.FIGURE.WATERMARK.alpha)
205
+ return
206
+
207
+
208
+ class BaseMixin(ArtistMixin):
209
+ def refresh(self):
210
+ self._refresh()
211
+ self.figure.canvas.draw()
212
+ return
213
+
214
+ def _refresh(self):
215
+ self.set_window()
216
+ self.set_artists()
217
+ return
@@ -0,0 +1,2 @@
1
+ from ._info import BaseMixin, Chart
2
+
@@ -0,0 +1,217 @@
1
+ from matplotlib.collections import LineCollection
2
+ from matplotlib.text import Text
3
+ import pandas as pd
4
+
5
+ from .._draw import BaseMixin as Base
6
+
7
+
8
+ class ArtistMixin(Base):
9
+ def _add_artists(self):
10
+ super()._add_artists()
11
+
12
+ self._add_crosslines()
13
+ self._set_crosslines()
14
+
15
+ self._add_label_texts()
16
+ self._set_label_texts()
17
+
18
+ self._add_info_texts()
19
+ self._set_info_texts()
20
+
21
+ self._add_box_collections()
22
+ self._set_box_collections()
23
+ return
24
+
25
+ def _set_artists(self):
26
+ super()._set_artists()
27
+
28
+ self._set_crosslines()
29
+ self._set_label_texts()
30
+ self._set_info_texts()
31
+ self._set_box_collections()
32
+ return
33
+
34
+ def _add_crosslines(self):
35
+ kwargs = {'segments': [], 'animated': True}
36
+
37
+ self.collection_price_crossline = LineCollection(**kwargs)
38
+ self.ax_price.add_artist(self.collection_price_crossline)
39
+
40
+ self.collection_volume_crossline = LineCollection(**kwargs)
41
+ self.ax_volume.add_artist(self.collection_volume_crossline)
42
+ return
43
+
44
+ def _set_crosslines(self):
45
+ kwargs = self.CONFIG.CURSOR.CROSSLINE.__dict__
46
+ kwargs.update({'segments': [], 'animated': True})
47
+
48
+ self.collection_price_crossline.set(**kwargs)
49
+ self.collection_volume_crossline.set(**kwargs)
50
+ return
51
+
52
+ def _add_label_texts(self):
53
+ kwargs = {'text': '', 'animated': True, 'horizontalalignment': 'center', 'verticalalignment': 'top'}
54
+
55
+ self.artist_text_x = Text(**kwargs)
56
+ self.figure.add_artist(self.artist_text_x)
57
+
58
+ kwargs.update({'horizontalalignment': 'left', 'verticalalignment': 'center'})
59
+ self.artist_text_y = Text(**kwargs)
60
+ self.figure.add_artist(self.artist_text_y)
61
+ return
62
+
63
+ def _set_label_texts(self):
64
+ kwargs = self.CONFIG.CURSOR.TEXT.to_dict()
65
+ kwargs.update({'text': ' ', 'animated': True, 'horizontalalignment': 'center', 'verticalalignment': 'top'})
66
+
67
+ self.artist_text_x.set(**kwargs)
68
+
69
+ kwargs.update({'horizontalalignment': 'left', 'verticalalignment': 'center'})
70
+ self.artist_text_y.set(**kwargs)
71
+ return
72
+
73
+ def _add_box_collections(self):
74
+ kwargs = {'segments': [], 'animated': True,}
75
+
76
+ self.collection_box_price = LineCollection(**kwargs)
77
+ self.ax_price.add_artist(self.collection_box_price)
78
+ self.collection_box_volume = LineCollection(**kwargs)
79
+ self.ax_volume.add_artist(self.collection_box_volume)
80
+ return
81
+
82
+ def _set_box_collections(self):
83
+ kwargs = self.CONFIG.CURSOR.BOX.__dict__
84
+ kwargs.update({'segments': [], 'animated': True,})
85
+
86
+ self.collection_box_price.set(**kwargs)
87
+ self.collection_box_volume.set(**kwargs)
88
+ return
89
+
90
+ def _add_info_texts(self):
91
+ kwargs = {'text': '', 'animated': True, 'horizontalalignment': 'left', 'verticalalignment': 'top',}
92
+
93
+ self.artist_info_candle = Text(**kwargs)
94
+ self.ax_price.add_artist(self.artist_info_candle)
95
+ self.artist_info_volume = Text(**kwargs)
96
+ self.ax_volume.add_artist(self.artist_info_volume)
97
+ return
98
+
99
+ def _set_info_texts(self):
100
+ kwargs = self.CONFIG.CURSOR.TEXT.to_dict()
101
+ kwargs.update({'text': '', 'animated': True, 'horizontalalignment': 'left', 'verticalalignment': 'top',})
102
+
103
+ self.artist_info_candle.set(**kwargs)
104
+ self.artist_info_volume.set(**kwargs)
105
+ return
106
+
107
+
108
+ class DataMixin(ArtistMixin):
109
+ def _set_data(self, df: pd.DataFrame, *args, **kwargs):
110
+ super()._set_data(df, *args, **kwargs)
111
+
112
+ self.df['compare'] = (self.df['close'] - self.df['pre_close']).fillna(0)
113
+ self.df['rate'] = (self.df['compare'] * 100 / self.df['pre_close']).__round__(2).fillna(0)
114
+ self.df['rate_open'] = ((self.df['open'] - self.df['pre_close']) * 100 / self.df['pre_close']).__round__(2).fillna(0)
115
+ self.df['rate_high'] = ((self.df['high'] - self.df['pre_close']) * 100 / self.df['pre_close']).__round__(2).fillna(0)
116
+ self.df['rate_low'] = ((self.df['low'] - self.df['pre_close']) * 100 / self.df['pre_close']).__round__(2).fillna(0)
117
+ if self.key_volume:
118
+ self.df['pre_volume'] = self.df['volume'].shift(1)
119
+ self.df['compare_volume'] = (self.df['volume'] - self.df['pre_volume']).fillna(0)
120
+ self.df['rate_volume'] = (self.df['compare_volume'] * 100 / self.df['pre_volume']).__round__(2).fillna(0)
121
+
122
+ self.df['space_box_candle'] = (self.df['high'] - self.df['low']) / 5
123
+ self.df['bottom_box_candle'] = self.df['low'] - self.df['space_box_candle']
124
+ self.df['top_box_candle'] = self.df['high'] + self.df['space_box_candle']
125
+ self.df['height_box_candle'] = self.df['top_box_candle'] - self.df['bottom_box_candle']
126
+ if self.key_volume: self.df['max_box_volume'] = self.df['volume'] * 1.15
127
+
128
+ self._set_label_texts_position()
129
+ return
130
+
131
+ def _refresh(self):
132
+ super()._refresh()
133
+
134
+ self._set_label_texts_position()
135
+ return
136
+
137
+ def _axis(self, xmin, xmax, simpler=False, draw_ma=True):
138
+ super()._axis(xmin, xmax=xmax, simpler=simpler, draw_ma=draw_ma)
139
+
140
+ psub = (self.price_ymax - self.price_ymin)
141
+ self.min_height_box_candle = psub / 8
142
+
143
+ pydistance = psub / 20
144
+
145
+ self.min_height_box_volume = 10
146
+ if self.key_volume:
147
+ self.min_height_box_volume = self.key_volume_ymax / 4
148
+
149
+ vxsub = self.vxmax - self.vxmin
150
+ self.vmiddle = self.vxmax - int((vxsub) / 2)
151
+
152
+ vxdistance = vxsub / 50
153
+ self.v0, self.v1 = (self.vxmin + vxdistance, self.vxmax - vxdistance)
154
+
155
+ yvolume = self.key_volume_ymax * 0.85
156
+
157
+ # 정보 텍스트박스 y축 설정
158
+ self.artist_info_candle.set_y(self.price_ymax - pydistance)
159
+ self.artist_info_volume.set_y(yvolume)
160
+
161
+ return
162
+
163
+ def _set_label_texts_position(self):
164
+ self._set_label_x_position()
165
+ self._set_label_y_position()
166
+ return
167
+
168
+ def _set_label_x_position(self):
169
+ renderer = getattr(self.figure.canvas, 'renderer', None)
170
+ if not renderer:
171
+ renderer
172
+
173
+ # Axes 우측 경계 좌표
174
+ x1 = self.ax_price.get_position().x1
175
+ # Text bbox 너비
176
+ bbox = self.artist_text_y.get_bbox_patch()\
177
+ .get_window_extent(renderer)
178
+ bbox_width = bbox.width
179
+ # 밀어야 하는 x값
180
+ fig_width = self.figure.bbox.width
181
+ # fig_width = self.figure.get_size_inches()[0] * self.figure.dpi
182
+ box_width_fig = (bbox_width+14) / fig_width
183
+ # print(f'{box_width_fig=}')
184
+
185
+ # x축 값(가격 또는 거래량)
186
+ # self.artist_text_y.set_x(x1)
187
+ x = x1 + (box_width_fig / 2)
188
+ # print(f'{(x1, x)=}')
189
+ self.artist_text_y.set_x(x)
190
+ return
191
+
192
+ def _set_label_y_position(self):
193
+ renderer = getattr(self.figure.canvas, 'renderer', None)
194
+ if not renderer:
195
+ return
196
+
197
+ # Axes 하단 경계 좌표
198
+ y0 = self.ax_volume.get_position().y0
199
+ # Text bbox 높이
200
+ bbox = self.artist_text_x.get_bbox_patch()\
201
+ .get_window_extent(renderer)
202
+ height_px = bbox.height
203
+ # print(f'{height_px=}')
204
+ # 밀어야 하는 y값
205
+ fig_height_px = self.figure.bbox.height
206
+ box_height_fig = (height_px+14) / fig_height_px
207
+
208
+ # y축 값(날짜)
209
+ y = y0 - (box_height_fig/2)
210
+ # print(f'{(y0, y)=}')
211
+ self.artist_text_x.set_y(y)
212
+ return
213
+
214
+
215
+ class BaseMixin(DataMixin):
216
+ pass
217
+
@@ -0,0 +1,165 @@
1
+ from matplotlib.backend_bases import MouseEvent
2
+
3
+ from ._artist import BaseMixin as Base
4
+ from ..._config.utils import float_to_str
5
+
6
+
7
+ class EventMixin(Base):
8
+ in_price_chart, in_volume_chart = (False, False)
9
+ intx = None
10
+
11
+ def _connect_events(self):
12
+ super()._connect_events()
13
+
14
+ self.figure.canvas.mpl_connect('motion_notify_event', lambda x: self.on_move(x))
15
+ return
16
+
17
+ def on_move(self, e):
18
+ self._on_move_action(e)
19
+ return
20
+
21
+ def _on_move_action(self, e: MouseEvent):
22
+ self._check_ax(e)
23
+
24
+ self.intx = None
25
+ if self.in_price_chart or self.in_volume_chart: self._get_x(e)
26
+ return
27
+
28
+ def _get_x(self, e: MouseEvent):
29
+ self.intx = e.xdata.__int__()
30
+ if self.intx < 0: self.intx = None
31
+ else:
32
+ try: self.index_list[self.intx]
33
+ except: self.intx = None
34
+ return
35
+
36
+ def _check_ax(self, e: MouseEvent):
37
+ ax = e.inaxes
38
+ if not ax or e.xdata is None or e.ydata is None:
39
+ self.in_price_chart, self.in_volume_chart = (False, False)
40
+ else:
41
+ if ax is self.ax_price:
42
+ self.in_price_chart = True
43
+ self.in_volume_chart = False
44
+ elif ax is self.ax_volume:
45
+ self.in_price_chart = False
46
+ self.in_volume_chart = True
47
+ else:
48
+ self.in_price_chart = False
49
+ self.in_volume_chart = False
50
+ return
51
+
52
+
53
+ class CrossLineMixin(EventMixin):
54
+ in_candle, in_volumebar = (False, False)
55
+
56
+ def _on_move_action(self, e):
57
+ super()._on_move_action(e)
58
+
59
+ if self.in_price_chart or self.in_volume_chart:
60
+ self._restore_region()
61
+ self._draw_crossline(e)
62
+ self.figure.canvas.blit()
63
+ else:
64
+ if self._erase_crossline():
65
+ self._restore_region()
66
+ self.figure.canvas.blit()
67
+ return
68
+
69
+ def _erase_crossline(self):
70
+ seg = self.collection_price_crossline.get_segments()
71
+ if seg:
72
+ self.collection_price_crossline.set_segments([])
73
+ return True
74
+ return False
75
+
76
+ def _draw_crossline(self, e: MouseEvent):
77
+ x, y = (e.xdata, e.ydata)
78
+
79
+ if self.in_price_chart:
80
+ self.collection_price_crossline.set_segments([((x, self.price_ymin), (x, self.price_ymax)), ((self.vxmin, y), (self.vxmax, y))])
81
+ self.collection_volume_crossline.set_segments([((x, 0), (x, self.key_volume_ymax))])
82
+ else:
83
+ self.collection_price_crossline.set_segments([((x, self.price_ymin), (x, self.price_ymax))])
84
+ self.collection_volume_crossline.set_segments([((x, 0), (x, self.key_volume_ymax)), ((self.vxmin, y), (self.vxmax, y))])
85
+
86
+ renderer = self.figure.canvas.renderer
87
+ self.collection_price_crossline.draw(renderer)
88
+ self.collection_volume_crossline.draw(renderer)
89
+
90
+ self._draw_text_artist(e)
91
+ return
92
+
93
+ def _draw_text_artist(self, e: MouseEvent):
94
+ x, y = (e.xdata, e.ydata)
95
+
96
+ display_coords = e.inaxes.transData.transform((e.xdata, e.ydata))
97
+ figure_coords = self.figure.transFigure.inverted().transform(display_coords)
98
+ # print(f'{figure_coords=}')
99
+
100
+ renderer = self.figure.canvas.renderer
101
+ if self.in_price_chart:
102
+ text = f'{float_to_str(y, digit=self.CONFIG.UNIT.digit)}{self.CONFIG.UNIT.price}'
103
+ else:
104
+ text = f'{float_to_str(y, digit=self.CONFIG.UNIT.digit_volume)}{self.CONFIG.UNIT.volume}'
105
+
106
+ # y축 값(가격 또는 거래량)
107
+ self.artist_text_y.set_text(text)
108
+ self.artist_text_y.set_y(figure_coords[1])
109
+ self.artist_text_y.draw(renderer)
110
+
111
+ if self.intx is not None:
112
+ # x축 값(날짜)
113
+ self.artist_text_x.set_text(f'{self.df['date'][self.intx]}')
114
+ self.artist_text_x.set_x(figure_coords[0])
115
+ self.artist_text_x.draw(renderer)
116
+ return
117
+
118
+
119
+ class BoxMixin(CrossLineMixin):
120
+ def _draw_crossline(self, e):
121
+ super()._draw_crossline(e)
122
+ self._draw_box_artist(e)
123
+ return
124
+
125
+ def _draw_box_artist(self, e: MouseEvent):
126
+ y = e.ydata
127
+
128
+ renderer = self.figure.canvas.renderer
129
+
130
+ self.in_candle = False
131
+ self.in_volumebar = False
132
+ if self.intx is not None:
133
+ if self.in_price_chart:
134
+ # 박스 크기
135
+ high = self.df['top_box_candle'][self.intx]
136
+ low = self.df['bottom_box_candle'][self.intx]
137
+ height = self.df['height_box_candle'][self.intx]
138
+ if height < self.min_height_box_candle:
139
+ sub = (self.min_height_box_candle - height) / 2
140
+ high, low = (high+sub, low-sub)
141
+
142
+ # 커서가 캔들 사이에 있는지 확인
143
+ if low <= y and y <= high:
144
+ # 캔들 강조
145
+ self.in_candle = True
146
+ x1, x2 = (self.intx-0.3, self.intx+1.3)
147
+ self.collection_box_price.set_segments([((x1, high), (x2, high), (x2, low), (x1, low), (x1, high))])
148
+ self.collection_box_price.draw(renderer)
149
+ elif self.in_volume_chart and self.key_volume:
150
+ # 거래량 강조
151
+ high = self.df['max_box_volume'][self.intx]
152
+ low = 0
153
+ if high < self.min_height_box_volume: high = self.min_height_box_volume
154
+
155
+ if low <= y and y <= high:
156
+ self.in_volumebar = True
157
+ x1, x2 = (self.intx-0.3, self.intx+1.3)
158
+ self.collection_box_volume.set_segments([((x1, high), (x2, high), (x2, low), (x1, low), (x1, high))])
159
+ self.collection_box_volume.draw(renderer)
160
+ return
161
+
162
+
163
+ class BaseMixin(BoxMixin):
164
+ pass
165
+