seolpyo-mplchart 1.3.1.1__tar.gz → 1.4.0__tar.gz

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 (18) hide show
  1. {seolpyo_mplchart-1.3.1.1/seolpyo_mplchart.egg-info → seolpyo_mplchart-1.4.0}/PKG-INFO +1 -1
  2. {seolpyo_mplchart-1.3.1.1 → seolpyo_mplchart-1.4.0}/pyproject.toml +1 -1
  3. {seolpyo_mplchart-1.3.1.1 → seolpyo_mplchart-1.4.0}/seolpyo_mplchart/__init__.py +8 -8
  4. seolpyo_mplchart-1.4.0/seolpyo_mplchart/_cursor.py +533 -0
  5. {seolpyo_mplchart-1.3.1.1 → seolpyo_mplchart-1.4.0}/seolpyo_mplchart/_draw.py +211 -235
  6. {seolpyo_mplchart-1.3.1.1 → seolpyo_mplchart-1.4.0}/seolpyo_mplchart/_slider.py +136 -137
  7. {seolpyo_mplchart-1.3.1.1 → seolpyo_mplchart-1.4.0/seolpyo_mplchart.egg-info}/PKG-INFO +1 -1
  8. seolpyo_mplchart-1.3.1.1/seolpyo_mplchart/_cursor.py +0 -493
  9. {seolpyo_mplchart-1.3.1.1 → seolpyo_mplchart-1.4.0}/MANIFEST.in +0 -0
  10. {seolpyo_mplchart-1.3.1.1 → seolpyo_mplchart-1.4.0}/README.md +0 -0
  11. {seolpyo_mplchart-1.3.1.1 → seolpyo_mplchart-1.4.0}/seolpyo_mplchart/_base.py +0 -0
  12. {seolpyo_mplchart-1.3.1.1 → seolpyo_mplchart-1.4.0}/seolpyo_mplchart/test.py +0 -0
  13. {seolpyo_mplchart-1.3.1.1 → seolpyo_mplchart-1.4.0}/seolpyo_mplchart/utils.py +0 -0
  14. {seolpyo_mplchart-1.3.1.1 → seolpyo_mplchart-1.4.0}/seolpyo_mplchart.egg-info/SOURCES.txt +0 -0
  15. {seolpyo_mplchart-1.3.1.1 → seolpyo_mplchart-1.4.0}/seolpyo_mplchart.egg-info/dependency_links.txt +0 -0
  16. {seolpyo_mplchart-1.3.1.1 → seolpyo_mplchart-1.4.0}/seolpyo_mplchart.egg-info/requires.txt +0 -0
  17. {seolpyo_mplchart-1.3.1.1 → seolpyo_mplchart-1.4.0}/seolpyo_mplchart.egg-info/top_level.txt +0 -0
  18. {seolpyo_mplchart-1.3.1.1 → seolpyo_mplchart-1.4.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: seolpyo-mplchart
3
- Version: 1.3.1.1
3
+ Version: 1.4.0
4
4
  Summary: Fast candlestick chart using Python. Includes navigator, slider, navigation, and text information display functions
5
5
  Author-email: white-seolpyo <white-seolpyo@naver.com>
6
6
  License: MIT License
@@ -8,7 +8,7 @@ build-backend = "setuptools.build_meta"
8
8
 
9
9
  [project]
10
10
  name = "seolpyo-mplchart"
11
- version = "1.3.1.1"
11
+ version = "1.4.0"
12
12
  dependencies = [
13
13
  "matplotlib >= 3.7.0",
14
14
  "pandas >= 2.0.0",
@@ -297,19 +297,19 @@ def set_theme(chart: SliderChart|CursorChart|OnlyChart, theme: Literal['light',
297
297
  chart.color_volume_up, chart.color_volume_down = ('#32CD32', '#FF4500')
298
298
  chart.color_volume_flat = 'w'
299
299
 
300
- chart.list_macolor = ('#FFFF00', '#7FFF00', '#00FFFF', '#FFA07A', '#FF00FF')
300
+ chart.list_macolor = ('#1E90FF', '#FFA500', '#FF1493', '#FFFF00', '#00CED1')
301
301
 
302
302
  chart.lineKwargs = {'edgecolor': 'w'}
303
303
  chart.color_box = 'w'
304
304
  chart.textboxKwargs = {'facecolor': 'k', 'edgecolor': 'w'}
305
305
  chart.textKwargs = {'color': 'w'}
306
- chart.color_navigator_cover, chart.color_navigator_line = ('w', '#FF2400')
306
+ chart.color_navigator_cover, chart.color_navigator_line = ('k', '#FF2400')
307
307
 
308
308
  if initialized:
309
309
  chart.change_background_color('k')
310
310
  chart.change_tick_color('w')
311
311
  chart.change_line_color('w')
312
- if hasattr(chart, 'navigator'): chart.navigator.set_edgecolor([chart.color_navigator_cover, chart.color_navigator_line])
312
+ if hasattr(chart, 'navigator'): chart.collection_navigator.set_edgecolor([chart.color_navigator_cover, chart.color_navigator_line])
313
313
 
314
314
  if hasattr(chart, 'df'):
315
315
  chart.set_data(chart.df, sort_df=False, calc_ma=False, set_candlecolor=True, set_volumecolor=True, calc_info=False, change_lim=False)
@@ -324,22 +324,22 @@ def set_theme(chart: SliderChart|CursorChart|OnlyChart, theme: Literal['light',
324
324
  chart.color_flat = 'k'
325
325
  chart.color_up_down, chart.color_down_up = ('w', 'w')
326
326
 
327
- chart.color_volume_up, chart.color_volume_down = ('#FF4D4D', '#5CA8F4')
328
- chart.color_volume_flat = '#A9A9A9'
327
+ chart.color_volume_up, chart.color_volume_down = ('#FF6666', '#5CA8F4')
328
+ chart.color_volume_flat = '#808080'
329
329
 
330
- chart.list_macolor = ('#B22222', '#228B22', '#1E90FF', '#FF8C00', '#4B0082')
330
+ chart.list_macolor = ('#006400', '#8B008B', '#FFA500', '#0000FF', '#FF0000')
331
331
 
332
332
  chart.lineKwargs = {'edgecolor': 'k'}
333
333
  chart.color_box = 'k'
334
334
  chart.textboxKwargs = {'facecolor': 'w', 'edgecolor': 'k'}
335
335
  chart.textKwargs = {'color': 'k'}
336
- chart.color_navigator_cover, chart.color_navigator_line = ('k', '#1e78ff')
336
+ chart.color_navigator_cover, chart.color_navigator_line = ('k', '#1E78FF')
337
337
 
338
338
  if initialized:
339
339
  chart.change_background_color('#fafafa')
340
340
  chart.change_tick_color('k')
341
341
  chart.change_line_color('k')
342
- if hasattr(chart, 'navigator'): chart.navigator.set_edgecolor([chart.color_navigator_cover, chart.color_navigator_line])
342
+ if hasattr(chart, 'navigator'): chart.collection_navigator.set_edgecolor([chart.color_navigator_cover, chart.color_navigator_line])
343
343
 
344
344
  if hasattr(chart, 'df'):
345
345
  chart.set_data(chart.df, sort_df=False, calc_ma=False, set_candlecolor=True, set_volumecolor=True, calc_info=False, change_lim=False)
@@ -0,0 +1,533 @@
1
+ from fractions import Fraction
2
+
3
+ import matplotlib.pyplot as plt
4
+ from matplotlib.backend_bases import MouseEvent
5
+ from matplotlib.collections import LineCollection
6
+ from matplotlib.text import Text
7
+ import pandas as pd
8
+
9
+ from ._draw import BaseMixin as BM, Mixin as M
10
+ from .utils import float_to_str
11
+
12
+
13
+ class Mixin(M):
14
+ def on_move(self, e):
15
+ "If mouse move event active, This method work."
16
+ return
17
+
18
+
19
+ class CollectionMixin(BM):
20
+ lineKwargs = {}
21
+ textboxKwargs = {}
22
+ textKwargs = {}
23
+ color_box = 'k'
24
+
25
+ def _add_collection(self):
26
+ super()._add_collection()
27
+
28
+ lineKwargs = {'edgecolor': 'k', 'linewidth': 1, 'linestyle': '-'}
29
+ lineKwargs.update(self.lineKwargs)
30
+ lineKwargs.update({'segments': [], 'animated': True})
31
+ textboxKwargs = {'boxstyle': 'round', 'facecolor': 'w'}
32
+ textboxKwargs.update(self.textboxKwargs)
33
+ textKwargs = self.textKwargs
34
+ textKwargs.update({'animated': True, 'bbox': textboxKwargs, 'horizontalalignment': '', 'verticalalignment': ''})
35
+ (textKwargs.pop('horizontalalignment'), textKwargs.pop('verticalalignment'))
36
+
37
+ self.collection_price_crossline = LineCollection(**lineKwargs)
38
+ self.ax_price.add_artist(self.collection_price_crossline)
39
+ self.artist_text_date_price = Text(**textKwargs, horizontalalignment='center', verticalalignment='bottom')
40
+ self.ax_price.add_artist(self.artist_text_date_price)
41
+ self.artist_text_price = Text(**textKwargs, horizontalalignment='left', verticalalignment='center')
42
+ self.ax_price.add_artist(self.artist_text_price)
43
+
44
+ self.collection_volume_crossline = LineCollection(**lineKwargs)
45
+ self.ax_volume.add_artist(self.collection_volume_crossline)
46
+ self.artist_text_date_volume = Text(**textKwargs, horizontalalignment='center', verticalalignment='top')
47
+ self.ax_volume.add_artist(self.artist_text_date_volume)
48
+ self.artist_text_volume = Text(**textKwargs, horizontalalignment='left', verticalalignment='center')
49
+ self.ax_volume.add_artist(self.artist_text_volume)
50
+
51
+ self.collection_price_box = LineCollection([], animated=True, linewidth=1.2, edgecolor=self.color_box)
52
+ self.ax_price.add_artist(self.collection_price_box)
53
+ self.artist_text_price_info = Text(**textKwargs, horizontalalignment='left', verticalalignment='top')
54
+ self.ax_price.add_artist(self.artist_text_price_info)
55
+
56
+ self.collection_volume_box = LineCollection([], animated=True, linewidth=1.2, edgecolor=self.color_box)
57
+ self.ax_volume.add_artist(self.collection_volume_box)
58
+ self.artist_text_volume_info = Text(**textKwargs, horizontalalignment='left', verticalalignment='top')
59
+ self.ax_volume.add_artist(self.artist_text_volume_info)
60
+ return
61
+
62
+ def change_background_color(self, color):
63
+ super().change_background_color(color)
64
+
65
+ self.artist_text_price.set_backgroundcolor(color)
66
+ self.artist_text_volume.set_backgroundcolor(color)
67
+
68
+ self.artist_text_date_price.set_backgroundcolor(color)
69
+ self.artist_text_date_volume.set_backgroundcolor(color)
70
+
71
+ self.artist_text_price_info.set_backgroundcolor(color)
72
+ self.artist_text_volume_info.set_backgroundcolor(color)
73
+ return
74
+
75
+ def change_text_color(self, color):
76
+ super().change_text_color(color)
77
+
78
+ self.artist_text_price.set_color(color)
79
+ self.artist_text_volume.set_color(color)
80
+
81
+ self.artist_text_date_price.set_color(color)
82
+ self.artist_text_date_volume.set_color(color)
83
+
84
+ self.artist_text_price_info.set_color(color)
85
+ self.artist_text_volume_info.set_color(color)
86
+ return
87
+
88
+ def change_line_color(self, color):
89
+ self.collection_price_crossline.set_edgecolor(color)
90
+ self.collection_volume_crossline.set_edgecolor(color)
91
+
92
+ self.collection_price_box.set_edgecolor(color)
93
+ self.collection_volume_box.set_edgecolor(color)
94
+
95
+ self.artist_text_price.get_bbox_patch().set_edgecolor(color)
96
+ self.artist_text_volume.get_bbox_patch().set_edgecolor(color)
97
+
98
+ self.artist_text_date_price.get_bbox_patch().set_edgecolor(color)
99
+ self.artist_text_date_volume.get_bbox_patch().set_edgecolor(color)
100
+
101
+ self.artist_text_price_info.get_bbox_patch().set_edgecolor(color)
102
+ self.artist_text_volume_info.get_bbox_patch().set_edgecolor(color)
103
+ return
104
+
105
+
106
+ _set_key = {
107
+ 'compare', 'rate',
108
+ 'rate_open', 'rate_high', 'rate_low',
109
+ 'compare_volume', 'rate_volume',
110
+ 'space_box_candle',
111
+ 'bottom_box_candle', 'top_box_candle',
112
+ 'max_box_volume',
113
+ }
114
+
115
+ class DataMixin(CollectionMixin):
116
+ def _validate_column_key(self, df):
117
+ super()._validate_column_key(df)
118
+
119
+ for i in ('date', 'Open', 'high', 'low', 'close', 'volume'):
120
+ v = getattr(self, i)
121
+ if v in _set_key: raise Exception(f'you can not set "{i}" to column key.\nself.{i}={v!r}')
122
+ return
123
+
124
+ def set_data(self, df, sort_df=True, calc_ma=True, set_candlecolor=True, set_volumecolor=True, calc_info=True, *args, **kwargs):
125
+ super().set_data(df, sort_df, calc_ma, set_candlecolor, set_volumecolor, calc_info, *args, **kwargs)
126
+ return
127
+
128
+ def _generate_data(self, df: pd.DataFrame, sort_df, calc_ma, set_candlecolor, set_volumecolor, calc_info, *_, **__):
129
+ super()._generate_data(df, sort_df, calc_ma, set_candlecolor, set_volumecolor, *_, **__)
130
+
131
+ if not calc_info:
132
+ keys = set(df.keys())
133
+ list_key = ['compare', 'rate', 'rate_open', 'rate_high', 'rate_low',]
134
+ if self.volume: list_key += ['compare_volume', 'rate_volume',]
135
+ for i in list_key:
136
+ if i not in keys:
137
+ raise Exception(f'"{i}" column not in DataFrame.\nadd column or set calc_info=True.')
138
+ else:
139
+ self.df['compare'] = (self.df[self.close] - self.df['close_pre']).fillna(0)
140
+ self.df['rate'] = (self.df['compare'] / self.df[self.close] * 100).__round__(2).fillna(0)
141
+ self.df['rate_open'] = ((self.df[self.Open] - self.df['close_pre']) / self.df[self.close] * 100).__round__(2).fillna(0)
142
+ self.df['rate_high'] = ((self.df[self.high] - self.df['close_pre']) / self.df[self.close] * 100).__round__(2).fillna(0)
143
+ self.df['rate_low'] = ((self.df[self.low] - self.df['close_pre']) / self.df[self.close] * 100).__round__(2).fillna(0)
144
+ if self.volume:
145
+ self.df['compare_volume'] = (self.df[self.volume] - self.df[self.volume].shift(1)).fillna(0)
146
+ self.df['rate_volume'] = (self.df['compare_volume'] / self.df[self.volume].shift(1) * 100).__round__(2).fillna(0)
147
+
148
+ self.df['space_box_candle'] = (self.df[self.high] - self.df[self.low]) / 5
149
+ self.df['bottom_box_candle'] = self.df[self.low] - self.df['space_box_candle']
150
+ self.df['top_box_candle'] = self.df[self.high] + self.df['space_box_candle']
151
+ self.df['height_box_candle'] = self.df['top_box_candle'] - self.df['bottom_box_candle']
152
+ if self.volume: self.df['max_box_volume'] = self.df[self.volume] * 1.15
153
+ return
154
+
155
+ def _set_lim(self, xmin, xmax, simpler=False, set_ma=True):
156
+ super()._set_lim(xmin, xmax, simpler, set_ma)
157
+
158
+ psub = (self.price_ymax - self.price_ymin)
159
+ self.min_height_box_candle = psub / 8
160
+
161
+ pydistance = psub / 20
162
+ self.artist_text_date_price.set_y(self.price_ymin + pydistance)
163
+
164
+ self.min_height_box_volume = self.volume_ymax / 4
165
+
166
+ vxsub = self.vxmax - self.vxmin
167
+ self.vmiddle = self.vxmax - int((vxsub) / 2)
168
+
169
+ vxdistance = vxsub / 50
170
+ self.v0, self.v1 = (self.vxmin + vxdistance, self.vxmax - vxdistance)
171
+ self.vsixth = self.vxmin + int((vxsub) / 6)
172
+ self.veighth = self.vxmin + int((vxsub) / 8)
173
+
174
+ yvolume = self.volume_ymax * 0.85
175
+ self.artist_text_date_volume.set_y(yvolume)
176
+
177
+ # 정보 텍스트박스
178
+ self.artist_text_price_info.set_y(self.price_ymax - pydistance)
179
+ self.artist_text_volume_info.set_y(yvolume)
180
+ return
181
+
182
+
183
+ class EventMixin(DataMixin):
184
+ in_price_chart, in_volume_chart = (False, False)
185
+ intx = None
186
+
187
+ def _connect_event(self):
188
+ super()._connect_event()
189
+ self.figure.canvas.mpl_connect('motion_notify_event', lambda x: self._on_move(x))
190
+ return
191
+
192
+ def _on_move(self, e):
193
+ self._on_move_action(e)
194
+ return
195
+
196
+ def _on_move_action(self, e: MouseEvent):
197
+ self._check_ax(e)
198
+
199
+ self.intx = None
200
+ if self.in_price_chart or self.in_volume_chart: self._get_x(e)
201
+ return
202
+
203
+ def _check_ax(self, e: MouseEvent):
204
+ ax = e.inaxes
205
+ if not ax or e.xdata is None or e.ydata is None:
206
+ self.in_price_chart, self.in_volume_chart = (False, False)
207
+ else:
208
+ if ax is self.ax_price:
209
+ self.in_price_chart = True
210
+ self.in_volume_chart = False
211
+ elif ax is self.ax_volume:
212
+ self.in_price_chart = False
213
+ self.in_volume_chart = True
214
+ else:
215
+ self.in_price_chart = False
216
+ self.in_volume_chart = False
217
+ return
218
+
219
+ def _get_x(self, e: MouseEvent):
220
+ self.intx = e.xdata.__int__()
221
+ if self.intx < 0: self.intx = None
222
+ else:
223
+ try: self.list_index[self.intx]
224
+ except: self.intx = None
225
+ return
226
+
227
+
228
+ class CrossLineMixin(EventMixin):
229
+ digit_price, digit_volume = (0, 0)
230
+ in_candle, in_volumebar = (False, False)
231
+
232
+ def _on_move(self, e):
233
+ super()._on_move(e)
234
+
235
+ if self.in_price_chart or self.in_volume_chart:
236
+ self._restore_region()
237
+ self._draw_crossline(e, self.in_price_chart)
238
+ self.figure.canvas.blit()
239
+ else:
240
+ if self._erase_crossline():
241
+ self._restore_region()
242
+ self.figure.canvas.blit()
243
+ return
244
+
245
+ def _erase_crossline(self):
246
+ seg = self.collection_price_crossline.get_segments()
247
+ if seg:
248
+ self.collection_price_crossline.set_segments([])
249
+ return True
250
+ return False
251
+
252
+ def _draw_crossline(self, e: MouseEvent, in_price_chart):
253
+ x, y = (e.xdata, e.ydata)
254
+
255
+ if in_price_chart:
256
+ self.collection_price_crossline.set_segments([((x, self.price_ymin), (x, self.price_ymax)), ((self.vxmin, y), (self.vxmax, y))])
257
+ self.collection_volume_crossline.set_segments([((x, 0), (x, self.volume_ymax))])
258
+ else:
259
+ self.collection_price_crossline.set_segments([((x, self.price_ymin), (x, self.price_ymax))])
260
+ self.collection_volume_crossline.set_segments([((x, 0), (x, self.volume_ymax)), ((self.vxmin, y), (self.vxmax, y))])
261
+
262
+ renderer = self.figure.canvas.renderer
263
+ self.collection_price_crossline.draw(renderer)
264
+ self.collection_volume_crossline.draw(renderer)
265
+
266
+ self._draw_text_artist(e, in_price_chart)
267
+ return
268
+
269
+ def _draw_text_artist(self, e: MouseEvent, in_price_chart):
270
+ x, y = (e.xdata, e.ydata)
271
+
272
+ renderer = self.figure.canvas.renderer
273
+ if in_price_chart:
274
+ # 가격
275
+ self.artist_text_price.set_text(f'{float_to_str(y, self.digit_price)}{self.unit_price}')
276
+ self.artist_text_price.set_x(self.v0 if self.veighth < x else self.vsixth)
277
+ self.artist_text_price.set_y(y)
278
+ self.artist_text_price.draw(renderer)
279
+
280
+ if self.intx:
281
+ # 기준시간 표시
282
+ self.artist_text_date_volume.set_text(f'{self.df[self.date][self.intx]}')
283
+ self.artist_text_date_volume.set_x(x)
284
+ self.artist_text_date_volume.draw(renderer)
285
+ else:
286
+ # 거래량
287
+ self.artist_text_volume.set_text(f'{float_to_str(y, self.digit_volume)}{self.unit_volume}')
288
+ self.artist_text_volume.set_x(self.v0 if self.veighth < x else self.vsixth)
289
+ self.artist_text_volume.set_y(y)
290
+ self.artist_text_volume.draw(renderer)
291
+
292
+ if self.intx:
293
+ # 기준시간 표시
294
+ self.artist_text_date_price.set_text(f'{self.df[self.date][self.intx]}')
295
+ self.artist_text_date_price.set_x(x)
296
+ self.artist_text_date_price.draw(renderer)
297
+ return
298
+
299
+
300
+ class BoxMixin(CrossLineMixin):
301
+ def _draw_crossline(self, e, in_price_chart):
302
+ super()._draw_crossline(e, in_price_chart)
303
+ self._draw_box_artist(e, in_price_chart)
304
+ return
305
+
306
+ def _draw_box_artist(self, e: MouseEvent, in_price_chart):
307
+ y = e.ydata
308
+
309
+ renderer = self.figure.canvas.renderer
310
+ if self.intx:
311
+ if in_price_chart:
312
+ # 박스 크기
313
+ high = self.df['top_box_candle'][self.intx]
314
+ low = self.df['bottom_box_candle'][self.intx]
315
+ height = self.df['height_box_candle'][self.intx]
316
+ if height < self.min_height_box_candle:
317
+ sub = (self.min_height_box_candle - height) / 2
318
+ high, low = (high+sub, low-sub)
319
+
320
+ # 커서가 캔들 사이에 있는지 확인
321
+ if high < y or y < low: self.in_candle = False
322
+ else:
323
+ # 캔들 강조
324
+ self.in_candle = True
325
+ x1, x2 = (self.intx-0.3, self.intx+1.4)
326
+ self.collection_price_box.set_segments([((x1, high), (x2, high), (x2, low), (x1, low), (x1, high))])
327
+ self.collection_price_box.draw(renderer)
328
+ else:
329
+ # 거래량 강조
330
+ high = self.df['max_box_volume'][self.intx]
331
+ low = 0
332
+ if high < self.min_height_box_volume: high = self.min_height_box_volume
333
+
334
+ if high < y or y < low: self.in_volumebar = False
335
+ else:
336
+ self.in_volumebar = True
337
+ x1, x2 = (self.intx-0.3, self.intx+1.4)
338
+ self.collection_volume_box.set_segments([((x1, high), (x2, high), (x2, low), (x1, low), (x1, high))])
339
+ self.collection_volume_box.draw(renderer)
340
+ return
341
+
342
+
343
+ format_candleinfo_ko = """\
344
+ {dt}
345
+
346
+ 종가:  {close}
347
+ 등락률: {rate}
348
+ 대비:  {compare}
349
+ 시가:  {open}({rate_open})
350
+ 고가:  {high}({rate_high})
351
+ 저가:  {low}({rate_low})
352
+ 거래량: {volume}({rate_volume})\
353
+ """
354
+ format_volumeinfo_ko = """\
355
+ {dt}
356
+
357
+ 거래량:    {volume}
358
+ 거래량증가율: {rate_volume}
359
+ 대비:     {compare}\
360
+ """
361
+ format_candleinfo_en = """\
362
+ {dt}
363
+
364
+ close: {close}
365
+ rate: {rate}
366
+ compare: {compare}
367
+ open: {open}({rate_open})
368
+ high: {high}({rate_high})
369
+ low: {low}({rate_low})
370
+ volume: {volume}({rate_volume})\
371
+ """
372
+ format_volumeinfo_en = """\
373
+ {dt}
374
+
375
+ volume: {volume}
376
+ volume rate: {rate_volume}
377
+ compare: {compare}\
378
+ """
379
+
380
+ class InfoMixin(BoxMixin):
381
+ fraction = False
382
+ format_candleinfo = format_candleinfo_ko
383
+ format_volumeinfo = format_volumeinfo_ko
384
+
385
+ def set_data(self, df, sort_df=True, calc_ma=True, set_candlecolor=True, set_volumecolor=True, calc_info=True, *args, **kwargs):
386
+ super().set_data(df, sort_df, calc_ma, set_candlecolor, set_volumecolor, calc_info, *args, **kwargs)
387
+
388
+ self._length_text = self.df[(self.volume if self.volume else self.high)].apply(lambda x: len(f'{x:,}')).max()
389
+ return
390
+
391
+ def _draw_box_artist(self, e, in_price_chart):
392
+ super()._draw_box_artist(e, in_price_chart)
393
+
394
+ if self.intx:
395
+ if self.in_candle: self._draw_candle_info_artist(e)
396
+ elif self.in_volumebar: self._draw_volume_info_artist(e)
397
+ return
398
+
399
+ def _draw_candle_info_artist(self, e: MouseEvent):
400
+ # 캔들 정보
401
+ self.artist_text_price_info.set_text(self._get_info(self.intx))
402
+
403
+ # 정보 텍스트를 중앙에 몰리게 설정할 수도 있지만,
404
+ # 그런 경우 차트를 가리므로 좌우 끝단에 위치하도록 설정
405
+ if self.vmiddle < e.xdata:
406
+ self.artist_text_price_info.set_x(self.v0)
407
+ else:
408
+ # self.artist_text_price_info.set_x(self.vmax - self.x_distance)
409
+ # self.artist_text_price_info.set_horizontalalignment('right')
410
+ # 텍스트박스 크기 가져오기
411
+ bbox = self.artist_text_price_info.get_window_extent().transformed(self.ax_price.transData.inverted())
412
+ width = bbox.x1 - bbox.x0
413
+ self.artist_text_price_info.set_x(self.v1 - width)
414
+
415
+ self.artist_text_price_info.draw(self.figure.canvas.renderer)
416
+ return
417
+
418
+ def _draw_volume_info_artist(self, e: MouseEvent):
419
+ # 거래량 정보
420
+ self.artist_text_volume_info.set_text(self._get_info(self.intx, is_price=False))
421
+
422
+ if self.vmiddle < e.xdata: self.artist_text_volume_info.set_x(self.v0)
423
+ else:
424
+ # self.artist_text_volume_info.set_x(self.vmax - self.x_distance)
425
+ # self.artist_text_volume_info.set_horizontalalignment('right')
426
+ # 텍스트박스 크기 가져오기
427
+ bbox = self.artist_text_volume_info.get_window_extent().transformed(self.ax_price.transData.inverted())
428
+ width = bbox.x1 - bbox.x0
429
+ self.artist_text_volume_info.set_x(self.v1 - width)
430
+
431
+ self.artist_text_volume_info.draw(self.figure.canvas.renderer)
432
+ return
433
+
434
+ def _get_info(self, index, is_price=True):
435
+ dt = self.df[self.date][index]
436
+ if not self.volume: v, vr = ('-', '-')
437
+ else:
438
+ v = self.df[self.volume][index]
439
+ v = float_to_str(v, self.digit_volume)
440
+ # if not v % 1: v = int(v)
441
+ vr = self.df['rate_volume'][index]
442
+ vr = f'{vr:+06,.2f}'
443
+
444
+ if is_price:
445
+ o, h, l, c = (self.df[self.Open][index], self.df[self.high][index], self.df[self.low][index], self.df[self.close][index])
446
+ rate, compare = (self.df['rate'][index], self.df['compare'][index])
447
+ r = f'{rate:+06,.2f}'
448
+ Or, hr, lr = (self.df['rate_open'][index], self.df['rate_high'][index], self.df['rate_low'][index])
449
+
450
+ if self.fraction:
451
+ c = c.__round__(self.digit_price)
452
+ cd = divmod(c, 1)
453
+ if cd[1]: c = f'{float_to_str(cd[0])} {Fraction((cd[1]))}'
454
+ else: c = float_to_str(cd[0])
455
+ comd = divmod(compare, 1)
456
+ if comd[1]: com = f'{float_to_str(comd[0], plus=True)} {Fraction(comd[1])}'
457
+ else: com = float_to_str(comd[0], plus=True)
458
+ o = o.__round__(self.digit_price)
459
+ od = divmod(o, 1)
460
+ if od[1]: o = f'{float_to_str(od[0])} {Fraction(od[1])}'
461
+ else: o = float_to_str(od[0])
462
+ h = h.__round__(self.digit_price)
463
+ hd = divmod(h, 1)
464
+ if hd[1]: h = f'{float_to_str(hd[0])} {Fraction(hd[1])}'
465
+ else: h = float_to_str(hd[0])
466
+ l = l.__round__(self.digit_price)
467
+ ld = divmod(l, 1)
468
+ if ld[1]: l = f'{float_to_str(ld[0])} {Fraction(ld[1])}'
469
+ else: l = float_to_str(ld[0])
470
+
471
+ text = self.format_candleinfo.format(
472
+ dt=dt,
473
+ close=f'{c:>{self._length_text}}{self.unit_price}',
474
+ rate=f'{r:>{self._length_text}}%',
475
+ compare=f'{com:>{self._length_text}}{self.unit_price}',
476
+ open=f'{o:>{self._length_text}}{self.unit_price}', rate_open=f'{Or:+06,.2f}%',
477
+ high=f'{h:>{self._length_text}}{self.unit_price}', rate_high=f'{hr:+06,.2f}%',
478
+ low=f'{l:>{self._length_text}}{self.unit_price}', rate_low=f'{lr:+06,.2f}%',
479
+ volume=f'{v:>{self._length_text}}{self.unit_volume}', rate_volume=f'{vr}%',
480
+ )
481
+ else:
482
+ 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))
483
+ com = float_to_str(compare, self.digit_price, plus=True)
484
+
485
+ text = self.format_candleinfo.format(
486
+ dt=dt,
487
+ close=f'{c:>{self._length_text}}{self.unit_price}',
488
+ rate=f'{r:>{self._length_text}}%',
489
+ compare=f'{com:>{self._length_text}}{self.unit_price}',
490
+ open=f'{o:>{self._length_text}}{self.unit_price}', rate_open=f'{Or:+06,.2f}%',
491
+ high=f'{h:>{self._length_text}}{self.unit_price}', rate_high=f'{hr:+06,.2f}%',
492
+ low=f'{l:>{self._length_text}}{self.unit_price}', rate_low=f'{lr:+06,.2f}%',
493
+ volume=f'{v:>{self._length_text}}{self.unit_volume}', rate_volume=f'{vr}%',
494
+ )
495
+ elif self.volume:
496
+ compare = self.df['compare_volume'][index]
497
+ com = float_to_str(compare, self.digit_volume, plus=True)
498
+ text = self.format_volumeinfo.format(
499
+ dt=dt,
500
+ volume=f'{v:>{self._length_text}}{self.unit_volume}',
501
+ rate_volume=f'{vr:>{self._length_text}}%',
502
+ compare=f'{com:>{self._length_text}}{self.unit_volume}',
503
+ )
504
+ else: text = ''
505
+
506
+ return text
507
+
508
+
509
+ class BaseMixin(InfoMixin):
510
+ pass
511
+
512
+
513
+ class Chart(BaseMixin, Mixin):
514
+ def _draw_artist(self):
515
+ super()._draw_artist()
516
+ return self.draw_artist()
517
+
518
+ def _set_lim(self, xmin, xmax, simpler=False, set_ma=True):
519
+ super()._set_lim(xmin, xmax, simpler, set_ma)
520
+ return self.on_change_xlim(xmin, xmax, simpler, set_ma)
521
+
522
+ def _on_draw(self, e):
523
+ super()._on_draw(e)
524
+ return self.on_draw(e)
525
+
526
+ def _on_pick(self, e):
527
+ self.on_pick(e)
528
+ return super()._on_pick(e)
529
+
530
+ def _on_move(self, e):
531
+ super()._on_move(e)
532
+ return self.on_move(e)
533
+