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