seolpyo-mplchart 0.1.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,120 @@
1
+ """
2
+ This software includes Matplotlib, which is licensed under the BSD License.
3
+ Matplotlib Copyright (c) 2012- Matplotlib Development Team.
4
+ Full license can be found in the LICENSE file or at https://matplotlib.org/stable/users/license.html
5
+ """
6
+
7
+
8
+ import json
9
+ from pathlib import Path
10
+ from typing import Literal
11
+
12
+ import matplotlib.pyplot as plt
13
+ from matplotlib.figure import Figure
14
+ import pandas as pd
15
+
16
+
17
+ from .slider import Chart as CM
18
+
19
+
20
+ __all__ = [
21
+ 'pd',
22
+ 'plt',
23
+
24
+ 'Chart',
25
+
26
+ 'sample',
27
+ 'show',
28
+ 'close',
29
+ ]
30
+
31
+
32
+ class Chart(CM):
33
+ r"""
34
+ You can see the guidance document:
35
+ Korean: https://white.seolpyo.com/entry/147/
36
+ English: https://white.seolpyo.com/entry/148/
37
+
38
+ Variables:
39
+ unit_price, unit_volume: unit for price and volume. default ('원', '주').
40
+
41
+ figsize: figure size if you use plt.show(). default (12, 6).
42
+ ratio_ax_slider, ratio_ax_legend, ratio_ax_price, ratio_ax_volume: Axes ratio. default (3, 2, 18, 5).
43
+ adjust: figure adjust. default dict(top=0.95, bottom=0.05, left=0.01, right=0.93, wspace=0, hspace=0).
44
+ slider_top: ax_slider is located at the top or bottom. default True.
45
+ color_background: color of background. default '#fafafa'.
46
+ color_grid: color of grid. default '#d0d0d0'.
47
+
48
+ df: stock data.
49
+ date: date column key. default 'date'
50
+ Open, high, low, close: price column key. default ('open', 'high', 'low', 'close')
51
+ volume: volume column key. default 'volume'
52
+
53
+ label_ma: moving average legend label format. default '{}일선'
54
+ list_ma: Decide how many days to draw the moving average line. default (5, 20, 60, 120, 240)
55
+ list_macolor: Color the moving average line. If the number of colors is greater than the moving average line, black is applied. default ('darkred', 'fuchsia', 'olive', 'orange', 'navy', 'darkmagenta', 'limegreen', 'darkcyan',)
56
+
57
+ candle_on_ma: Decide whether to draw candles on the moving average line. default True
58
+ color_sliderline: Color of closing price line in ax_slider. default 'k'
59
+ color_navigatorline: Color of left and right dividing lines in selected area. default '#1e78ff'
60
+ color_navigator: Color of unselected area. default 'k'
61
+
62
+ color_up: The color of the candle. When the closing price is greater than the opening price. default '#fe3032'
63
+ color_down: The color of the candle. When the opening price is greater than the opening price. default '#0095ff'
64
+ color_flat: The color of the candle. WWhen the closing price is the same as the opening price. default 'k'
65
+ color_up_down: The color of the candle. If the closing price is greater than the opening price, but is lower than the previous day's closing price. default 'w'
66
+ color_down_up: The color of the candle. If the opening price is greater than the closing price, but is higher than the closing price of the previous day. default 'w'
67
+ colors_volume: The color of the volume bar. default '#1f77b4'
68
+
69
+ lineKwargs: Options applied to horizontal and vertical lines drawn along the mouse position. default dict(edgecolor='k', linewidth=1, linestyle='-')
70
+ textboxKwargs: Options that apply to the information text box. dufault dict(boxstyle='round', facecolor='w')
71
+
72
+ fraction: Decide whether to express information as a fraction. default False
73
+ candleformat: Candle information text format. default '{dt}\n\n종가:  {close}\n등락률: {rate}\n대비:  {compare}\n시가:  {open}({rate_open})\n고가:  {high}({rate_high})\n저가:  {low}({rate_low})\n거래량: {volume}({rate_volume})'
74
+ volumeformat: Volume information text format. default '{dt}\n\n거래량   : {volume}\n거래량증가율: {rate_volume}'
75
+ digit_price, digit_volume: Number of decimal places expressed in informational text. default (0, 0)
76
+
77
+ min_distance: Minimum number of candles that can be selected with the slider. default 30
78
+ simpler: Decide whether to display candles simply when moving the chart. default False
79
+ limit_volume: Maximum number of volume bars drawn when moving the chart. default 2_000
80
+ """
81
+ pass
82
+
83
+
84
+ _name = {'samsung', 'apple'}
85
+ def sample(name: Literal['samsung', 'apple']='samsung'):
86
+ if name not in _name:
87
+ print('name should be either samsung or apple.')
88
+ return
89
+ file = Path(__file__).parent / f'data/{name}.txt'
90
+ with open(file, 'r', encoding='utf-8') as txt:
91
+ data = json.load(txt)
92
+ data = data
93
+ df = pd.DataFrame(data)
94
+
95
+ c = Chart()
96
+ if name == 'apple':
97
+ c.unit_price = '$'
98
+ c.unit_volume = ' vol'
99
+ c.digit_price = 3
100
+ c.label_ma = 'ma{}'
101
+ c.candleformat = '{}\n\nend: {}\nrate: {}\ncompare: {}\nopen: {}({})\nhigh: {}({})\nlow: {}({})\nvolume: {}({})'
102
+ c.volumeformat = '{}\n\nvolume: {}\nvolume rate: {}'
103
+ c.set_data(df)
104
+ show()
105
+ close()
106
+ return
107
+
108
+
109
+ def show():
110
+ return plt.show()
111
+
112
+
113
+ def close(fig: int|str|Figure|None='all'):
114
+ return plt.close(fig)
115
+
116
+
117
+ if __name__ == '__main__':
118
+ sample('apple')
119
+
120
+
@@ -0,0 +1,111 @@
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
5
+
6
+
7
+ from .utils import convert_unit
8
+
9
+
10
+ try: plt.switch_backend('TkAgg')
11
+ except: pass
12
+
13
+ # 한글 깨짐 문제 방지
14
+ try: plt.rcParams['font.family'] ='Malgun Gothic'
15
+ except: pass
16
+
17
+ mplstyle.use('fast')
18
+
19
+
20
+ class Base:
21
+ canvas: FigureCanvasAgg
22
+ unit_price, unit_volume = ('원', '주')
23
+
24
+ figsize = (12, 6)
25
+ ratio_ax_slider, ratio_ax_legend, ratio_ax_price, ratio_ax_volume = (3, 2, 18, 5)
26
+ adjust = dict(
27
+ top=0.95, bottom=0.05, left=0.01, right=0.93, # 여백
28
+ wspace=0, hspace=0 # 플롯간 간격
29
+ )
30
+ color_grid = '#d0d0d0'
31
+ color_background = '#fafafa'
32
+
33
+ slider_top = True
34
+ title = 'seolpyo mplchart'
35
+
36
+ def __init__(self, *args, **kwargs):
37
+ # 기본 툴바 비활성화
38
+ plt.rcParams['toolbar'] = 'None'
39
+
40
+ self._set_plot()
41
+ return
42
+
43
+ def _set_plot(self):
44
+ if self.slider_top:
45
+ fig, ax = plt.subplots(
46
+ 4, # row 수
47
+ figsize=self.figsize, # 기본 크기
48
+ height_ratios=(self.ratio_ax_slider, self.ratio_ax_legend, self.ratio_ax_price, self.ratio_ax_volume) # row 크기 비율
49
+ )
50
+ ax: list[Axes]
51
+ ax_slider, ax_legend, ax_price, ax_volume = ax
52
+ else:
53
+ fig, ax = plt.subplots(
54
+ 5, # row 수
55
+ figsize=self.figsize, # 기본 크기
56
+ height_ratios=(self.ratio_ax_legend, self.ratio_ax_price, self.ratio_ax_volume, self.ratio_ax_legend, self.ratio_ax_slider) # row 크기 비율
57
+ )
58
+ ax: list[Axes]
59
+ ax_legend, ax_price, ax_volume, ax_none, ax_slider = ax
60
+ # 사용하지 않는 axes 숨기기
61
+ ax_none.axis('off')
62
+ ax_legend.axis('off')
63
+
64
+ ax_slider.xaxis.set_animated(True)
65
+ ax_slider.yaxis.set_animated(True)
66
+
67
+ ax_price.xaxis.set_animated(True)
68
+ ax_price.yaxis.set_animated(True)
69
+
70
+ ax_volume.xaxis.set_animated(True)
71
+ ax_volume.yaxis.set_animated(True)
72
+
73
+ fig.canvas.manager.set_window_title(f'{self.title}')
74
+
75
+ # 플롯간 간격 제거(Configure subplots)
76
+ fig.subplots_adjust(**self.adjust)
77
+
78
+ # y ticklabel foramt 설정
79
+ ax_slider.yaxis.set_major_formatter(lambda x, _: convert_unit(x, word=self.unit_price))
80
+ ax_price.yaxis.set_major_formatter(lambda x, _: convert_unit(x, word=self.unit_price))
81
+ ax_volume.yaxis.set_major_formatter(lambda x, _: convert_unit(x, word=self.unit_volume))
82
+
83
+ # 공통 설정
84
+ for a in [ax_slider, ax_price, ax_volume]:
85
+ # y tick 우측으로 이동
86
+ a.tick_params(left=False, right=True, labelleft=False, labelright=True)
87
+ # 차트 영역 배경 색상
88
+ a.set_facecolor(self.color_background)
89
+ # grid(구분선, 격자) 그리기
90
+ a.grid(True, color=self.color_grid, linewidth=1)
91
+ # x tick 제거
92
+ a.set_xticklabels([])
93
+
94
+ self.fig, self.canvas = (fig, fig.canvas)
95
+ self.ax_slider, self.ax_legend, self.ax_price, self.ax_volume = (ax_slider, ax_legend, ax_price, ax_volume)
96
+
97
+ return self.set_plot()
98
+
99
+ def set_plot(self):
100
+ "This function works after set plot process is done."
101
+ return
102
+
103
+
104
+ class Chart(Base):
105
+ pass
106
+
107
+
108
+ if __name__ == '__main__':
109
+ Base()
110
+
111
+ plt.show()
@@ -0,0 +1,448 @@
1
+ from fractions import Fraction
2
+
3
+ from matplotlib.backend_bases import MouseEvent
4
+ from matplotlib.collections import LineCollection
5
+ from matplotlib.text import Text
6
+ import pandas as pd
7
+
8
+
9
+ from .draw import DrawMixin, Chart as CM
10
+ from .utils import float_to_str
11
+
12
+
13
+ class Mixin:
14
+ def on_draw(self, e):
15
+ "This function works if draw event active."
16
+ return
17
+ def on_move(self, e):
18
+ "This function works if mouse move event active."
19
+ return
20
+
21
+
22
+ class CollectionMixin(DrawMixin):
23
+ lineKwargs = dict(edgecolor='k', linewidth=1, linestyle='-')
24
+ textboxKwargs = dict(boxstyle='round', facecolor='w')
25
+
26
+ def _add_collection(self):
27
+ super()._add_collection()
28
+ self.sliderline = LineCollection([], animated=True, **self.lineKwargs)
29
+ self.ax_slider.add_artist(self.sliderline)
30
+ self.slider_text = Text(animated=True, bbox=self.textboxKwargs, verticalalignment='top', horizontalalignment='center')
31
+ self.ax_slider.add_artist(self.slider_text)
32
+
33
+ self.price_vline = LineCollection([], animated=True, **self.lineKwargs)
34
+ self.ax_price.add_artist(self.price_vline)
35
+ self.text_date_price = Text(animated=True, bbox=self.textboxKwargs, verticalalignment='bottom', horizontalalignment='center')
36
+ self.ax_price.add_artist(self.text_date_price)
37
+ self.text_price = Text(animated=True, bbox=self.textboxKwargs, verticalalignment='center', horizontalalignment='left')
38
+ self.ax_price.add_artist(self.text_price)
39
+
40
+ self.volumeh_vline = LineCollection([], animated=True, **self.lineKwargs)
41
+ self.ax_volume.add_artist(self.volumeh_vline)
42
+ self.text_date_volume = Text(animated=True, bbox=self.textboxKwargs, verticalalignment='top', horizontalalignment='center')
43
+ self.ax_volume.add_artist(self.text_date_volume)
44
+ self.text_volume = Text(animated=True, bbox=self.textboxKwargs, verticalalignment='center', horizontalalignment='left')
45
+ self.ax_volume.add_artist(self.text_volume)
46
+
47
+ self.price_hline = LineCollection([], animated=True, **self.lineKwargs)
48
+ self.ax_price.add_artist(self.price_hline)
49
+ self.price_box = LineCollection([], animated=True, linewidth=1.1, edgecolor='k')
50
+ self.ax_price.add_artist(self.price_box)
51
+ self.text_price_info = Text(animated=True, bbox=self.textboxKwargs, verticalalignment='top', horizontalalignment='left')
52
+ self.ax_price.add_artist(self.text_price_info)
53
+
54
+ self.volume_hline = LineCollection([], animated=True, **self.lineKwargs)
55
+ self.ax_volume.add_artist(self.volume_hline)
56
+ self.volume_box = LineCollection([], animated=True, linewidth=1.1, edgecolor='k')
57
+ self.ax_volume.add_artist(self.volume_box)
58
+ self.text_volume_info = Text(animated=True, bbox=self.textboxKwargs, verticalalignment='top', horizontalalignment='left')
59
+ self.ax_volume.add_artist(self.text_volume_info)
60
+
61
+ return
62
+
63
+
64
+ _set_key = {'rate', 'compare', 'rate_open', 'rate_high', 'rate_low', 'rate_volume',}
65
+
66
+ class DataMixin(CollectionMixin):
67
+ def _generate_data(self, df, sort_df=True, calc_ma=True, calc_info=True):
68
+ for i in ('date', 'Open', 'high', 'low', 'close', 'volume'):
69
+ v = getattr(self, i)
70
+ if v in _set_key:
71
+ raise Exception(f'you can not set "self.{i}" value in {_set_key}.\nself.{i}={v!r}')
72
+
73
+ super()._generate_data(df, sort_df, calc_ma)
74
+ df = self.df
75
+
76
+ if not calc_info:
77
+ keys = set(df.keys())
78
+ for i in ('rate', 'compare', 'rate_open', 'rate_high', 'rate_low', 'rate_volume'):
79
+ if i not in keys:
80
+ raise Exception(f'"{i}" column not in DataFrame.\nadd column or set calc_info=True.')
81
+ else:
82
+ df['rate'] = ((df[self.close] - df[self.close].shift(1)) / df[self.close] * 100).__round__(2).fillna(0)
83
+ df['compare'] = (df[self.close] - df[self.close].shift(1)).fillna(0)
84
+ df['rate_open'] = ((df[self.Open] - df[self.close].shift(1)) / df[self.close] * 100).__round__(2).fillna(0)
85
+ df['rate_high'] = ((df[self.high] - df[self.close].shift(1)) / df[self.close] * 100).__round__(2).fillna(0)
86
+ df['rate_low'] = ((df[self.low] - df[self.close].shift(1)) / df[self.close] * 100).__round__(2).fillna(0)
87
+ df['rate_volume'] = ((df[self.volume] - df[self.volume].shift(1)) / df[self.volume].shift(1) * 100).__round__(2).fillna(0)
88
+
89
+ self.df = df
90
+ return
91
+
92
+ def set_text_coordante(self, vmin, vmax, pmin, pmax, volmax):
93
+ # 주가, 거래량 텍스트 x 위치
94
+ x_distance = (vmax - vmin) / 30
95
+ self.v0, self.v1 = (vmin + x_distance, vmax - x_distance)
96
+ self.text_price.set_x(self.v0)
97
+ self.text_volume.set_x(self.v0)
98
+
99
+ self.vmin, self.vmax = (vmin, vmax)
100
+ self.vmiddle = vmax - int((vmax - vmin) / 2)
101
+
102
+ psub = pmax - pmin
103
+ self.min_psub = psub / 12
104
+
105
+ # 주가 날짜 텍스트 y 위치
106
+ y = (psub) / 20 + pmin
107
+ self.text_date_price.set_y(y)
108
+ # 주가 정보 y 위치
109
+ y = pmax - (psub) / 20
110
+ self.text_price_info.set_y(y)
111
+
112
+ # 거래량 날짜 텍스트 y 위치
113
+ y = volmax * 0.85
114
+ self.text_date_volume.set_y(y)
115
+ # 거래량 정보 y 위치
116
+ self.text_volume_info.set_y(y)
117
+
118
+ return
119
+
120
+
121
+ class LineMixin(DataMixin):
122
+ in_slider, in_price, in_volume = (False, False, False)
123
+
124
+ intx, in_index = (None, False)
125
+ _in_candle, _in_volumebar = (False, False)
126
+
127
+ def _connect_event(self):
128
+ super()._connect_event()
129
+ self.canvas.mpl_connect('motion_notify_event', lambda x: self._on_move(x))
130
+ return
131
+
132
+ def _blit(self):
133
+ self.canvas.blit()
134
+ return
135
+
136
+ def set_data(self, df, sort_df=True, calc_ma=True, change_lim=True, calc_info=True, *args, **kwargs):
137
+ return super().set_data(df, sort_df, calc_ma, change_lim, calc_info=calc_info, *args, **kwargs)
138
+ def _set_data(self, df: pd.DataFrame, sort_df=True, calc_ma=True, change_lim=True, calc_info=True, *args, **kwargs):
139
+ super()._set_data(df, sort_df, calc_ma, change_lim, calc_info=calc_info, *args, **kwargs)
140
+
141
+ self.vmin, self.vmax = (self.xmin, self.xmax)
142
+ return
143
+
144
+ def _on_move(self, e):
145
+ self._restore_region()
146
+
147
+ self._on_move_action(e)
148
+
149
+ if self.in_slider or self.in_price or self.in_volume:
150
+ self._slider_move_action(e)
151
+ if self.in_price or self.in_volume:
152
+ self._chart_move_action(e)
153
+
154
+ self._blit()
155
+ return
156
+
157
+ def _on_move_action(self, e: MouseEvent):
158
+ if not e.inaxes:
159
+ self.intx, self.in_index = (None, False)
160
+ else:
161
+ self._check_ax(e)
162
+ x, y = (e.xdata, e.ydata)
163
+ self.intx = x.__int__()
164
+ if self.intx < 0: self.in_index = False
165
+ else:
166
+ try: self.df['x'][self.intx]
167
+ except: self.in_index = False
168
+ else: self.in_index = True
169
+ return
170
+
171
+ def _check_ax(self, e: MouseEvent):
172
+ ax = e.inaxes
173
+
174
+ self.in_slider = ax is self.ax_slider
175
+ self.in_price = False if self.in_slider else ax is self.ax_price
176
+ self.in_volume = False if (self.in_slider or self.in_price) else ax is self.ax_volume
177
+ return
178
+
179
+ def _slider_move_action(self, e: MouseEvent):
180
+ x = e.xdata
181
+
182
+ # 수직선
183
+ self.sliderline.set_segments([((x, self._slider_ymin), (x, self._slider_ymax))])
184
+ self.ax_slider.draw_artist(self.sliderline)
185
+ return
186
+
187
+ def _chart_move_action(self, e: MouseEvent):
188
+ x, y = (e.xdata, e.ydata)
189
+ if not y: return
190
+ roundy = y.__round__()
191
+
192
+ self.price_vline.set_segments([((x, self._price_ymin), (x, self._price_ymax))])
193
+ self.volumeh_vline.set_segments([((x, 0), (x, self._vol_ymax))])
194
+ self.ax_price.draw_artist(self.price_vline)
195
+ self.ax_volume.draw_artist(self.volumeh_vline)
196
+
197
+ if self.in_price: self._price_move_action(x, y, roundy)
198
+ else: self._volume_move_action(x, y, roundy)
199
+ return
200
+
201
+ def _price_move_action(self, _, y, roundy):
202
+ # 수평선
203
+ self.price_hline.set_segments([((self.vmin, y), (self.vmax, y))])
204
+ self.ax_price.draw_artist(self.price_hline)
205
+
206
+ # 가격
207
+ self.text_price.set_text(f'{roundy:,}{self.unit_price}')
208
+ self.text_price.set_y(y)
209
+ self.ax_price.draw_artist(self.text_price)
210
+
211
+ # 캔들 강조
212
+ if self.in_index:
213
+ intx = self.intx
214
+
215
+ h = self.df[self.high][intx]
216
+ l = self.df[self.low][intx]
217
+ sub = (h - l) / 2
218
+ if sub < self.min_psub: sub = self.min_psub
219
+ high = h + sub
220
+ low = l - sub
221
+ if high < y or y < low: self._in_candle = False
222
+ else:
223
+ self._in_candle = True
224
+ x1, x2 = (intx-0.3, intx+1.4)
225
+ self.price_box.set_segments([((x1, high), (x2, high), (x2, low), (x1, low), (x1, high))])
226
+ self.ax_price.draw_artist(self.price_box)
227
+ return
228
+
229
+ def _volume_move_action(self, _, y, roundy):
230
+ # 수평선
231
+ self.volume_hline.set_segments([((self.vmin, y), (self.vmax, y))])
232
+ self.ax_volume.draw_artist(self.volume_hline)
233
+
234
+ # 거래량
235
+ self.text_volume.set_text(f'{roundy:,}{self.unit_volume}')
236
+ self.text_volume.set_y(y)
237
+ self.ax_volume.draw_artist(self.text_volume)
238
+
239
+ # 거래량 강조
240
+ if self.in_index:
241
+ intx = self.intx
242
+
243
+ high = self.df[self.volume][intx] * 1.1
244
+ low = 0
245
+ self._volumerange = (0, high)
246
+ if high < y or y < low: self._in_volumebar: False
247
+ else:
248
+ self._in_volumebar = True
249
+ x1, x2 = (intx-0.3, intx+1.4)
250
+ self.volume_box.set_segments([((x1, high), (x2, high), (x2, low), (x1, low), (x1, high))])
251
+ self.ax_volume.draw_artist(self.volume_box)
252
+ return
253
+
254
+
255
+ class InfoMixin(LineMixin):
256
+ fraction = False
257
+ candleformat = '{dt}\n\n종가:  {close}\n등락률: {rate}\n대비:  {compare}\n시가:  {open}({rate_open})\n고가:  {high}({rate_high})\n저가:  {low}({rate_low})\n거래량: {volume}({rate_volume})'
258
+ volumeformat = '{dt}\n\n거래량   : {volume}\n거래량증가율: {rate_volume}'
259
+ digit_price, digit_volume = (0, 0)
260
+
261
+ def _set_data(self, df: pd.DataFrame, sort_df=True, calc_ma=True, change_lim=True, calc_info=True, *args, **kwargs):
262
+ super()._set_data(df, sort_df, calc_ma, change_lim, calc_info, *args, **kwargs)
263
+
264
+ # 슬라이더 날짜 텍스트 y 위치
265
+ y = self._slider_ymax - (self._slider_ymax - self._slider_ymin) / 6
266
+ self.slider_text.set_y(y)
267
+
268
+ v = self.df[self.volume].max()
269
+ self._length_text = len(f'{v:,}')
270
+ self.set_text_coordante(self.xmin, self.xmax, self._price_ymin, self._price_ymax, self._vol_ymax)
271
+
272
+ return
273
+
274
+ def _slider_move_action(self, e):
275
+ super()._slider_move_action(e)
276
+
277
+ intx = self.intx
278
+
279
+ if self.in_slider and self.in_index:
280
+ self.slider_text.set_text(f'{self.df[self.date][intx]}')
281
+ self.slider_text.set_x(e.xdata)
282
+ self.ax_slider.draw_artist(self.slider_text)
283
+ return
284
+
285
+ def _price_move_action(self, x, y, roundy):
286
+ super()._price_move_action(x, y, roundy)
287
+ if not self.in_index: return
288
+ intx = self.intx
289
+
290
+ # 텍스트
291
+ text = f'{self.df[self.date][intx]}'
292
+ self.text_date_volume.set_text(text)
293
+ self.text_date_volume.set_x(x)
294
+ self.ax_volume.draw_artist(self.text_date_volume)
295
+
296
+ # 캔들 강조
297
+ if self.in_price and self._in_candle:
298
+ # 캔들 정보
299
+ self.text_price_info.set_text(self._get_info(intx))
300
+ if x < self.vmiddle:
301
+ # 텍스트박스 크기 가져오기
302
+ bbox = self.text_price_info.get_window_extent().transformed(self.ax_price.transData.inverted())
303
+ width = bbox.x1 - bbox.x0
304
+ self.text_price_info.set_x(self.v1 - width)
305
+ else:
306
+ self.text_price_info.set_x(self.v0)
307
+ self.text_price_info.set_horizontalalignment('left')
308
+ self.ax_price.draw_artist(self.text_price_info)
309
+ return
310
+
311
+ def _volume_move_action(self, x, y, roundy):
312
+ super()._volume_move_action(x, y, roundy)
313
+ if not self.in_index: return
314
+ intx = self.intx
315
+
316
+ text = f'{self.df[self.date][intx]}'
317
+ self.text_date_price.set_text(text)
318
+ self.text_date_price.set_x(x)
319
+ self.ax_price.draw_artist(self.text_date_price)
320
+
321
+ # 거래량 강조
322
+ if self.in_volume and self._in_volumebar:
323
+ # 거래량 정보
324
+ if x < self.vmiddle:
325
+ bbox = self.text_volume_info.get_window_extent().transformed(self.ax_price.transData.inverted())
326
+ width = bbox.x1 - bbox.x0
327
+ self.text_volume_info.set_x(self.v1 - width)
328
+ else:
329
+ self.text_volume_info.set_x(self.v0)
330
+ self.text_volume_info.set_horizontalalignment('left')
331
+ self.text_volume_info.set_text(self._get_info(intx, False))
332
+ self.ax_volume.draw_artist(self.text_volume_info)
333
+ return
334
+
335
+ def _get_info(self, index, is_price=True):
336
+ dt = self.df[self.date][index]
337
+ v = self.df[self.volume][index]
338
+ v = float_to_str(v, self.digit_volume)
339
+ vr = self.df['rate_volume'][index]
340
+ if is_price:
341
+ o, h, l, c = (self.df[self.Open][index], self.df[self.high][index], self.df[self.low][index], self.df[self.close][index])
342
+ rate, compare = (self.df['rate'][index], self.df['compare'][index])
343
+ r = f'{rate:+06,.2f}'
344
+ Or, hr, lr = (self.df['rate_open'][index], self.df['rate_high'][index], self.df['rate_low'][index])
345
+
346
+ if self.fraction:
347
+ c = c.__round__(self.digit_price)
348
+ cd = divmod(c, 1)
349
+ if cd[1]: c = f'{float_to_str(cd[0])} {Fraction((cd[1]))}'
350
+ else: c = float_to_str(cd[0])
351
+
352
+ comd = divmod(compare, 1)
353
+ if comd[1]: com = f'{float_to_str(comd[0], plus=True)} {Fraction(comd[1])}'
354
+ else: com = float_to_str(comd[0], plus=True)
355
+
356
+ o = o.__round__(self.digit_price)
357
+ od = divmod(o, 1)
358
+ if od[1]: o = f'{float_to_str(od[0])} {Fraction(od[1])}'
359
+ else: o = float_to_str(od[0])
360
+
361
+ h = h.__round__(self.digit_price)
362
+ hd = divmod(h, 1)
363
+ if hd[1]: h = f'{float_to_str(hd[0])} {Fraction(hd[1])}'
364
+ else: h = float_to_str(hd[0])
365
+
366
+ l = l.__round__(self.digit_price)
367
+ ld = divmod(l, 1)
368
+ if ld[1]: l = f'{float_to_str(ld[0])} {Fraction(ld[1])}'
369
+ else: l = float_to_str(ld[0])
370
+
371
+ text = self.candleformat.format(
372
+ dt=dt,
373
+ close=f'{c:>{self._length_text}}{self.unit_price}',
374
+ rate=f'{r:>{self._length_text}}%',
375
+ compare=f'{com:>{self._length_text}}{self.unit_price}',
376
+ open=f'{o:>{self._length_text}}{self.unit_price}', rate_open=f'{Or:+06,.2f}%',
377
+ high=f'{h:>{self._length_text}}{self.unit_price}', rate_high=f'{hr:+06,.2f}%',
378
+ low=f'{l:>{self._length_text}}{self.unit_price}', rate_low=f'{lr:+06,.2f}%',
379
+ volume=f'{v:>{self._length_text}}{self.unit_volume}', rate_volume=f'{vr:+06,.2f}%',
380
+ )
381
+ else:
382
+ o, h, l, c = (float_to_str(o, self.digit_price), float_to_str(h, self.digit_price), float_to_str(l, self.digit_price), float_to_str(c, self.digit_price))
383
+ com = float_to_str(compare, self.digit_price, plus=True)
384
+
385
+ text = self.candleformat.format(
386
+ dt=dt,
387
+ close=f'{c:>{self._length_text}}{self.unit_price}',
388
+ rate=f'{r:>{self._length_text}}%',
389
+ compare=f'{com:>{self._length_text}}{self.unit_price}',
390
+ open=f'{o:>{self._length_text}}{self.unit_price}', rate_open=f'{Or:+06,.2f}%',
391
+ high=f'{h:>{self._length_text}}{self.unit_price}', rate_high=f'{hr:+06,.2f}%',
392
+ low=f'{l:>{self._length_text}}{self.unit_price}', rate_low=f'{lr:+06,.2f}%',
393
+ volume=f'{v:>{self._length_text}}{self.unit_volume}', rate_volume=f'{vr:+06,.2f}%',
394
+ )
395
+ else:
396
+ vrate = f'{vr:+06,.2f}'
397
+ text = self.volumeformat.format(
398
+ dt=dt,
399
+ volume=f'{v:>{self._length_text}}{self.unit_volume}',
400
+ rate_volume=f'{vrate:>{self._length_text}}%',
401
+ )
402
+ return text
403
+
404
+
405
+ class CursorMixin(InfoMixin):
406
+ pass
407
+
408
+
409
+ class Chart(CursorMixin, CM, Mixin):
410
+ def _on_draw(self, e):
411
+ super()._on_draw(e)
412
+ return self.on_draw(e)
413
+
414
+ def _on_pick(self, e):
415
+ self.on_pick(e)
416
+ return super()._on_pick(e)
417
+
418
+ def _on_move(self, e):
419
+ super()._on_move(e)
420
+ return self.on_move(e)
421
+
422
+
423
+ if __name__ == '__main__':
424
+ import json
425
+ from time import time
426
+
427
+ import matplotlib.pyplot as plt
428
+ from pathlib import Path
429
+
430
+ file = Path(__file__).parent / 'data/samsung.txt'
431
+ file = Path(__file__).parent / 'data/apple.txt'
432
+ with open(file, 'r', encoding='utf-8') as txt:
433
+ data = json.load(txt)
434
+ n = 2600
435
+ data = data[n:n+100]
436
+ df = pd.DataFrame(data)
437
+ print(f'{df.keys()=}')
438
+
439
+ t = time()
440
+ c = CursorMixin()
441
+ c.unit_price = '$'
442
+ # c.fraction = True
443
+ c.set_data(df[['date', 'open', 'high', 'low', 'close', 'volume']])
444
+ t2 = time() - t
445
+ print(f'{t2=}')
446
+ plt.show()
447
+
448
+