seolpyo-mplchart 0.1.3.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 (50) hide show
  1. seolpyo_mplchart/__init__.py +164 -99
  2. seolpyo_mplchart/_base.py +117 -0
  3. seolpyo_mplchart/_chart/__init__.py +137 -0
  4. seolpyo_mplchart/_chart/_base.py +217 -0
  5. seolpyo_mplchart/_chart/_cursor/__init__.py +2 -0
  6. seolpyo_mplchart/_chart/_cursor/_artist.py +217 -0
  7. seolpyo_mplchart/_chart/_cursor/_cursor.py +165 -0
  8. seolpyo_mplchart/_chart/_cursor/_info.py +187 -0
  9. seolpyo_mplchart/_chart/_draw/__init__.py +2 -0
  10. seolpyo_mplchart/_chart/_draw/_artist.py +50 -0
  11. seolpyo_mplchart/_chart/_draw/_data.py +314 -0
  12. seolpyo_mplchart/_chart/_draw/_draw.py +103 -0
  13. seolpyo_mplchart/_chart/_draw/_lim.py +265 -0
  14. seolpyo_mplchart/_chart/_slider/__init__.py +1 -0
  15. seolpyo_mplchart/_chart/_slider/_base.py +268 -0
  16. seolpyo_mplchart/_chart/_slider/_data.py +105 -0
  17. seolpyo_mplchart/_chart/_slider/_mouse.py +176 -0
  18. seolpyo_mplchart/_chart/_slider/_nav.py +204 -0
  19. seolpyo_mplchart/_chart/test.py +121 -0
  20. seolpyo_mplchart/_config/__init__.py +3 -0
  21. seolpyo_mplchart/_config/ax.py +28 -0
  22. seolpyo_mplchart/_config/candle.py +30 -0
  23. seolpyo_mplchart/_config/config.py +21 -0
  24. seolpyo_mplchart/_config/cursor.py +49 -0
  25. seolpyo_mplchart/_config/figure.py +41 -0
  26. seolpyo_mplchart/_config/format.py +51 -0
  27. seolpyo_mplchart/_config/ma.py +15 -0
  28. seolpyo_mplchart/_config/slider/__init__.py +2 -0
  29. seolpyo_mplchart/_config/slider/config.py +24 -0
  30. seolpyo_mplchart/_config/slider/figure.py +20 -0
  31. seolpyo_mplchart/_config/slider/nav.py +9 -0
  32. seolpyo_mplchart/_config/unit.py +19 -0
  33. seolpyo_mplchart/_config/utils.py +67 -0
  34. seolpyo_mplchart/_config/volume.py +26 -0
  35. seolpyo_mplchart/_cursor.py +559 -0
  36. seolpyo_mplchart/_draw.py +634 -0
  37. seolpyo_mplchart/_slider.py +634 -0
  38. seolpyo_mplchart/base.py +70 -67
  39. seolpyo_mplchart/cursor.py +308 -271
  40. seolpyo_mplchart/draw.py +449 -237
  41. seolpyo_mplchart/slider.py +451 -396
  42. seolpyo_mplchart/test.py +173 -24
  43. seolpyo_mplchart/utils.py +15 -4
  44. seolpyo_mplchart/xl_to_dict.py +47 -0
  45. seolpyo_mplchart-2.0.0.3.dist-info/METADATA +710 -0
  46. seolpyo_mplchart-2.0.0.3.dist-info/RECORD +50 -0
  47. {seolpyo_mplchart-0.1.3.1.dist-info → seolpyo_mplchart-2.0.0.3.dist-info}/WHEEL +1 -1
  48. seolpyo_mplchart-0.1.3.1.dist-info/METADATA +0 -49
  49. seolpyo_mplchart-0.1.3.1.dist-info/RECORD +0 -13
  50. {seolpyo_mplchart-0.1.3.1.dist-info → seolpyo_mplchart-2.0.0.3.dist-info}/top_level.txt +0 -0
@@ -1,342 +1,380 @@
1
1
  from fractions import Fraction
2
2
 
3
+ import matplotlib.pyplot as plt
3
4
  from matplotlib.backend_bases import MouseEvent
4
5
  from matplotlib.collections import LineCollection
5
6
  from matplotlib.text import Text
6
7
  import pandas as pd
7
8
 
8
-
9
- from .draw import DrawMixin, Chart as CM
9
+ from .draw import BaseMixin as BM, Mixin as M
10
10
  from .utils import float_to_str
11
11
 
12
12
 
13
- class Mixin:
14
- def on_draw(self, e):
15
- "This function works if draw event active."
16
- return
13
+ class Mixin(M):
17
14
  def on_move(self, e):
18
- "This function works if mouse move event active."
15
+ "If mouse move event active, This method work."
19
16
  return
20
17
 
21
18
 
22
- class CollectionMixin(DrawMixin):
23
- lineKwargs = dict(edgecolor='k', linewidth=1, linestyle='-')
24
- textboxKwargs = dict(boxstyle='round', facecolor='w')
19
+ class CollectionMixin(BM):
20
+ lineKwargs = {}
21
+ textboxKwargs = {}
22
+ textKwargs = {}
23
+ color_box = 'k'
25
24
 
26
25
  def _add_collection(self):
27
26
  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')
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.price_crossline = LineCollection(**lineKwargs)
38
+ self.ax_price.add_artist(self.price_crossline)
39
+ self.text_date_price = Text(**textKwargs, horizontalalignment='center', verticalalignment='bottom')
36
40
  self.ax_price.add_artist(self.text_date_price)
37
- self.text_price = Text(animated=True, bbox=self.textboxKwargs, verticalalignment='center', horizontalalignment='left')
41
+ self.text_price = Text(**textKwargs, horizontalalignment='left', verticalalignment='center')
38
42
  self.ax_price.add_artist(self.text_price)
39
43
 
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')
44
+ self.volume_crossline = LineCollection(**lineKwargs)
45
+ self.ax_volume.add_artist(self.volume_crossline)
46
+ self.text_date_volume = Text(**textKwargs, horizontalalignment='center', verticalalignment='top')
43
47
  self.ax_volume.add_artist(self.text_date_volume)
44
- self.text_volume = Text(animated=True, bbox=self.textboxKwargs, verticalalignment='center', horizontalalignment='left')
48
+ self.text_volume = Text(**textKwargs, horizontalalignment='left', verticalalignment='center')
45
49
  self.ax_volume.add_artist(self.text_volume)
46
50
 
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')
51
+ self.price_box = LineCollection([], animated=True, linewidth=1.2, edgecolor=self.color_box)
50
52
  self.ax_price.add_artist(self.price_box)
51
- self.text_price_info = Text(animated=True, bbox=self.textboxKwargs, verticalalignment='top', horizontalalignment='left')
53
+ self.text_price_info = Text(**textKwargs, horizontalalignment='left', verticalalignment='top')
52
54
  self.ax_price.add_artist(self.text_price_info)
53
55
 
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')
56
+ self.volume_box = LineCollection([], animated=True, linewidth=1.2, edgecolor=self.color_box)
57
57
  self.ax_volume.add_artist(self.volume_box)
58
- self.text_volume_info = Text(animated=True, bbox=self.textboxKwargs, verticalalignment='top', horizontalalignment='left')
58
+ self.text_volume_info = Text(**textKwargs, horizontalalignment='left', verticalalignment='top')
59
59
  self.ax_volume.add_artist(self.text_volume_info)
60
+ return
61
+
62
+ def change_background_color(self, color):
63
+ super().change_background_color(color)
64
+
65
+ self.text_price.set_backgroundcolor(color)
66
+ self.text_volume.set_backgroundcolor(color)
67
+
68
+ self.text_date_price.set_backgroundcolor(color)
69
+ self.text_date_volume.set_backgroundcolor(color)
70
+
71
+ self.text_price_info.set_backgroundcolor(color)
72
+ self.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.text_price.set_color(color)
79
+ self.text_volume.set_color(color)
80
+
81
+ self.text_date_price.set_color(color)
82
+ self.text_date_volume.set_color(color)
60
83
 
84
+ self.text_price_info.set_color(color)
85
+ self.text_volume_info.set_color(color)
61
86
  return
62
87
 
88
+ def change_line_color(self, color):
89
+ self.price_crossline.set_edgecolor(color)
90
+ self.volume_crossline.set_edgecolor(color)
63
91
 
64
- _set_key = {'rate', 'compare', 'rate_open', 'rate_high', 'rate_low', 'rate_volume',}
92
+ self.price_box.set_edgecolor(color)
93
+ self.volume_box.set_edgecolor(color)
94
+
95
+ self.text_price.get_bbox_patch().set_edgecolor(color)
96
+ self.text_volume.get_bbox_patch().set_edgecolor(color)
97
+
98
+ self.text_date_price.get_bbox_patch().set_edgecolor(color)
99
+ self.text_date_volume.get_bbox_patch().set_edgecolor(color)
100
+
101
+ self.text_price_info.get_bbox_patch().set_edgecolor(color)
102
+ self.text_volume_info.get_bbox_patch().set_edgecolor(color)
103
+ return
104
+
105
+
106
+ _set_key = {'rate', 'compare', 'rate_open', 'rate_high', 'rate_low', 'rate_volume', '_boxheight', '_boxmin', '_boxmax', '_volumeboxmax',}
65
107
 
66
108
  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'):
109
+ def _validate_column_key(self):
110
+ super()._validate_column_key()
111
+ for i in ['date', 'Open', 'high', 'low', 'close', 'volume']:
69
112
  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}')
113
+ if v in _set_key: raise Exception(f'you can not set "{i}" to column key.\nself.{i}={v!r}')
114
+ return
72
115
 
73
- super()._generate_data(df, sort_df, calc_ma)
74
- df = self.df
116
+ def _generate_data(self, df, sort_df, calc_ma, set_candlecolor, set_volumecolor, calc_info, *_, **__):
117
+ super()._generate_data(df, sort_df, calc_ma, set_candlecolor, set_volumecolor, *_, **__)
75
118
 
76
119
  if not calc_info:
77
120
  keys = set(df.keys())
78
- for i in ('rate', 'compare', 'rate_open', 'rate_high', 'rate_low', 'rate_volume'):
121
+ list_key = ['rate', 'compare', 'rate_open', 'rate_high', 'rate_low',]
122
+ if self.volume: list_key.append('rate_volume')
123
+ for i in list_key:
79
124
  if i not in keys:
80
125
  raise Exception(f'"{i}" column not in DataFrame.\nadd column or set calc_info=True.')
81
126
  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
127
+ self.df['compare'] = (self.df[self.close] - self.df['_pre']).fillna(0)
128
+ self.df['rate'] = (self.df['compare'] / self.df[self.close] * 100).__round__(2).fillna(0)
129
+ self.df['rate_open'] = ((self.df[self.Open] - self.df['_pre']) / self.df[self.close] * 100).__round__(2).fillna(0)
130
+ self.df['rate_high'] = ((self.df[self.high] - self.df['_pre']) / self.df[self.close] * 100).__round__(2).fillna(0)
131
+ self.df['rate_low'] = ((self.df[self.low] - self.df['_pre']) / self.df[self.close] * 100).__round__(2).fillna(0)
132
+ if self.volume:
133
+ self.df['compare_volume'] = (self.df[self.volume] - self.df[self.volume].shift(1)).fillna(0)
134
+ self.df['rate_volume'] = (self.df['compare_volume'] / self.df[self.volume].shift(1) * 100).__round__(2).fillna(0)
135
+
136
+ self.df['_boxheight'] = (self.df[self.high] - self.df[self.low]) / 5
137
+ self.df['_boxmin'] = self.df[self.low] - self.df['_boxheight']
138
+ self.df['_boxmax'] = self.df[self.high] + self.df['_boxheight']
139
+ if self.volume: self.df['_volumeboxmax'] = self.df[self.volume] * 1.13
90
140
  return
91
141
 
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)
142
+ def _set_lim(self, xmin, xmax, simpler=False, set_ma=True):
143
+ super()._set_lim(xmin, xmax, simpler, set_ma)
98
144
 
99
- self.vmin, self.vmax = (vmin, vmax)
100
- self.vmiddle = vmax - int((vmax - vmin) / 2)
145
+ psub = (self.price_ymax - self.price_ymin)
146
+ self.min_candleboxheight = psub / 8
101
147
 
102
- psub = pmax - pmin
103
- self.min_psub = psub / 12
148
+ pydistance = psub / 20
149
+ self.text_date_price.set_y(self.price_ymin + pydistance)
104
150
 
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)
151
+ self.min_volumeboxheight = self.volume_ymax / 4
111
152
 
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)
153
+ vxsub = self.vxmax - self.vxmin
154
+ self.vmiddle = self.vxmax - int((vxsub) / 2)
117
155
 
118
- return
156
+ vxdistance = vxsub / 50
157
+ self.v0, self.v1 = (self.vxmin + vxdistance, self.vxmax - vxdistance)
158
+ self.vsixth = self.vxmin + int((vxsub) / 6)
159
+ self.veighth = self.vxmin + int((vxsub) / 8)
160
+
161
+ yvolume = self.volume_ymax * 0.85
162
+ self.text_date_volume.set_y(yvolume)
119
163
 
164
+ # 정보 텍스트박스
165
+ self.text_price_info.set_y(self.price_ymax - pydistance)
166
+ self.text_volume_info.set_y(yvolume)
167
+ return
120
168
 
121
- class LineMixin(DataMixin):
122
- in_slider, in_price, in_volume = (False, False, False)
123
169
 
124
- intx, in_index = (None, False)
125
- _in_candle, _in_volumebar = (False, False)
170
+ class EventMixin(DataMixin):
171
+ in_price_chart, in_volume_chart = (False, False)
172
+ intx = None
126
173
 
127
174
  def _connect_event(self):
128
175
  super()._connect_event()
129
- self.canvas.mpl_connect('motion_notify_event', lambda x: self._on_move(x))
176
+ self.figure.canvas.mpl_connect('motion_notify_event', lambda x: self._on_move(x))
130
177
  return
131
178
 
132
- def _blit(self):
133
- self.canvas.blit()
179
+ def _on_move(self, e):
180
+ self._on_move_action(e)
134
181
  return
135
182
 
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)
183
+ def _on_move_action(self, e: MouseEvent):
184
+ self._check_ax(e)
140
185
 
141
- self.vmin, self.vmax = (self.xmin, self.xmax)
186
+ self.intx = None
187
+ if self.in_price_chart or self.in_volume_chart: self._get_x(e)
142
188
  return
143
189
 
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)
190
+ def _check_ax(self, e: MouseEvent):
191
+ ax = e.inaxes
192
+ if not ax or e.xdata is None or e.ydata is None:
193
+ self.in_price_chart, self.in_volume_chart = (False, False)
194
+ else:
195
+ self.in_price_chart = ax is self.ax_price
196
+ self.in_volume_chart = False if self.in_price_chart else ax is self.ax_volume
153
197
 
154
- self._blit()
155
198
  return
156
199
 
157
- def _on_move_action(self, e: MouseEvent):
158
- if not e.inaxes:
159
- self.intx, self.in_index = (None, False)
200
+ def _get_x(self, e: MouseEvent):
201
+ self.intx = e.xdata.__int__()
202
+ if self.intx < 0: self.intx = None
160
203
  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
204
+ try: self.list_index[self.intx]
205
+ except: self.intx = None
169
206
  return
170
207
 
171
- def _check_ax(self, e: MouseEvent):
172
- ax = e.inaxes
173
208
 
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
209
+ class LineMixin(EventMixin):
210
+ digit_price, digit_volume = (0, 0)
211
+ in_candle, in_volumebar = (False, False)
178
212
 
179
- def _slider_move_action(self, e: MouseEvent):
180
- x = e.xdata
213
+ def _on_move(self, e):
214
+ super()._on_move(e)
181
215
 
182
- # 수직선
183
- self.sliderline.set_segments([((x, self._slider_ymin), (x, self._slider_ymax))])
184
- self.ax_slider.draw_artist(self.sliderline)
216
+ self._restore_region()
217
+
218
+ if self.in_price_chart: self._on_move_price_chart(e)
219
+ elif self.in_volume_chart: self._on_move_volume_chart(e)
220
+
221
+ self._blit()
185
222
  return
186
223
 
187
- def _chart_move_action(self, e: MouseEvent):
224
+ def _on_move_price_chart(self, e: MouseEvent):
188
225
  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
226
 
197
- if self.in_price: self._price_move_action(x, y, roundy)
198
- else: self._volume_move_action(x, y, roundy)
199
- return
227
+ self.price_crossline.set_segments([((x, self.price_ymin), (x, self.price_ymax)), ((self.vxmin, y), (self.vxmax, y))])
228
+ self.volume_crossline.set_segments([((x, 0), (x, self.volume_ymax))])
229
+ self._draw_crossline()
200
230
 
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)
231
+ renderer = self.figure.canvas.renderer
205
232
 
206
233
  # 가격
207
- self.text_price.set_text(f'{roundy:,}{self.unit_price}')
234
+ self.text_price.set_text(f'{float_to_str(y, self.digit_price)}{self.unit_price}')
235
+ self.text_price.set_x(self.v0 if self.veighth < x else self.vsixth)
208
236
  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
237
+ self.text_price.draw(renderer)
238
+
239
+ index = self.intx
240
+ if index is None: self.in_candle = False
241
+ else:
242
+ # 기준시간 표시
243
+ self.text_date_volume.set_text(f'{self.df[self.date][index]}')
244
+ self.text_date_volume.set_x(x)
245
+ self.text_date_volume.draw(renderer)
246
+
247
+ # 캔들 강조
248
+ low = self.df['_boxmin'][index]
249
+ high = self.df['_boxmax'][index]
250
+ sub = high - low
251
+ if sub < self.min_candleboxheight:
252
+ sub = (self.min_candleboxheight - sub) / 2
253
+ low -= sub
254
+ high += sub
255
+
256
+ if high < y or y < low: self.in_candle = False
222
257
  else:
223
- self._in_candle = True
224
- x1, x2 = (intx-0.3, intx+1.4)
258
+ self.in_candle = True
259
+ x1, x2 = (index-0.3, index+1.4)
225
260
  self.price_box.set_segments([((x1, high), (x2, high), (x2, low), (x1, low), (x1, high))])
226
- self.ax_price.draw_artist(self.price_box)
261
+ self.price_box.draw(renderer)
227
262
  return
228
263
 
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)
264
+ def _draw_crossline(self):
265
+ renderer = self.figure.canvas.renderer
266
+ self.price_crossline.draw(renderer)
267
+ self.volume_crossline.draw(renderer)
268
+ return
269
+
270
+ def _on_move_volume_chart(self, e: MouseEvent):
271
+ x, y = (e.xdata, e.ydata)
272
+
273
+ self.price_crossline.set_segments([((x, self.price_ymin), (x, self.price_ymax))])
274
+ self.volume_crossline.set_segments([((x, 0), (x, self.volume_ymax)), ((self.vxmin, y), (self.vxmax, y))])
275
+ self._draw_crossline()
276
+
277
+ if not self.volume: return
278
+
279
+ renderer = self.figure.canvas.renderer
233
280
 
234
281
  # 거래량
235
- self.text_volume.set_text(f'{roundy:,}{self.unit_volume}')
282
+ self.text_volume.set_text(f'{float_to_str(y, self.digit_volume)}{self.unit_volume}')
283
+ self.text_volume.set_x(self.v0 if self.veighth < x else self.vsixth)
236
284
  self.text_volume.set_y(y)
237
- self.ax_volume.draw_artist(self.text_volume)
285
+ self.text_volume.draw(renderer)
238
286
 
239
- # 거래량 강조
240
- if self.in_index:
241
- intx = self.intx
287
+ index = self.intx
288
+ if index is None: self.in_volumebar = False
289
+ else:
290
+ # 기준시간 표시
291
+ self.text_date_price.set_text(f'{self.df[self.date][index]}')
292
+ self.text_date_price.set_x(x)
293
+ self.text_date_price.draw(renderer)
242
294
 
243
- high = self.df[self.volume][intx] * 1.1
295
+ # 거래량 강조
296
+ high = self.df[self.volume][index] * 1.15
244
297
  low = 0
245
- self._volumerange = (0, high)
246
- if high < y or y < low: self._in_volumebar: False
298
+ if high < self.min_volumeboxheight: high = self.min_volumeboxheight
299
+
300
+ if high < y or y < low: self.in_volumebar = False
247
301
  else:
248
- self._in_volumebar = True
249
- x1, x2 = (intx-0.3, intx+1.4)
302
+ self.in_volumebar = True
303
+ x1, x2 = (index-0.3, index+1.4)
250
304
  self.volume_box.set_segments([((x1, high), (x2, high), (x2, low), (x1, low), (x1, high))])
251
- self.ax_volume.draw_artist(self.volume_box)
305
+ self.volume_box.draw(renderer)
252
306
  return
253
307
 
254
308
 
309
+ format_candleinfo_ko = '{dt}\n\n종가:  {close}\n등락률: {rate}\n대비:  {compare}\n시가:  {open}({rate_open})\n고가:  {high}({rate_high})\n저가:  {low}({rate_low})\n거래량: {volume}({rate_volume})'
310
+ format_volumeinfo_ko = '{dt}\n\n거래량:    {volume}\n거래량증가율: {rate_volume}\n대비:     {compare}'
311
+ format_candleinfo_en = '{dt}\n\nclose: {close}\nrate: {rate}\ncompare: {compare}\nopen: {open}({rate_open})\nhigh: {high}({rate_high})\nlow: {low}({rate_low})\nvolume: {volume}({rate_volume})'
312
+ format_volumeinfo_en = '{dt}\n\nvolume: {volume}\nvolume rate: {rate_volume}\ncompare: {compare}'
313
+
255
314
  class InfoMixin(LineMixin):
256
315
  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)
316
+ format_candleinfo = format_candleinfo_ko
317
+ format_volumeinfo = format_volumeinfo_ko
267
318
 
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)
319
+ def set_data(self, df, sort_df=True, calc_ma=True, set_candlecolor=True, set_volumecolor=True, calc_info=True, *args, **kwargs):
320
+ super().set_data(df, sort_df, calc_ma, set_candlecolor, set_volumecolor, calc_info, *args, **kwargs)
271
321
 
322
+ self._length_text = self.df[(self.volume if self.volume else self.high)].apply(lambda x: len(f'{x:,}')).max()
272
323
  return
273
324
 
274
- def _slider_move_action(self, e):
275
- super()._slider_move_action(e)
325
+ def _on_move_price_chart(self, e):
326
+ super()._on_move_price_chart(e)
276
327
 
277
- intx = self.intx
328
+ # 캔들 강조 확인
329
+ if not self.in_candle: return
278
330
 
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
331
+ # 캔들 정보
332
+ self.text_price_info.set_text(self._get_info(self.intx))
284
333
 
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)
334
+ if self.vmiddle < e.xdata: self.text_price_info.set_x(self.v0)
335
+ else:
336
+ # self.text_price_info.set_x(self.vmax - self.x_distance)
337
+ # self.text_price_info.set_horizontalalignment('right')
338
+ # 텍스트박스 크기 가져오기
339
+ bbox = self.text_price_info.get_window_extent().transformed(self.ax_price.transData.inverted())
340
+ width = bbox.x1 - bbox.x0
341
+ self.text_price_info.set_x(self.v1 - width)
342
+
343
+ self.text_price_info.draw(self.figure.canvas.renderer)
309
344
  return
310
345
 
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)
346
+ def _on_move_volume_chart(self, e):
347
+ super()._on_move_volume_chart(e)
348
+
349
+ # 거래량 강조 확인
350
+ if not self.in_volumebar: return
351
+
352
+ # 거래량 정보
353
+ self.text_volume_info.set_text(self._get_info(self.intx, is_price=False))
354
+
355
+ if self.vmiddle < e.xdata: self.text_volume_info.set_x(self.v0)
356
+ else:
357
+ # self.text_volume_info.set_x(self.vmax - self.x_distance)
358
+ # self.text_volume_info.set_horizontalalignment('right')
359
+ # 텍스트박스 크기 가져오기
360
+ bbox = self.text_volume_info.get_window_extent().transformed(self.ax_price.transData.inverted())
361
+ width = bbox.x1 - bbox.x0
362
+ self.text_volume_info.set_x(self.v1 - width)
363
+
364
+ self.text_volume_info.draw(self.figure.canvas.renderer)
333
365
  return
334
366
 
335
367
  def _get_info(self, index, is_price=True):
336
368
  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]
369
+ if not self.volume:
370
+ v, vr = ('-', '-%')
371
+ else:
372
+ v = self.df[self.volume][index]
373
+ v = float_to_str(v, self.digit_volume)
374
+ # if not v % 1: v = int(v)
375
+ vr = self.df['rate_volume'][index]
376
+ vr = f'{vr:+06,.2f}%'
377
+
340
378
  if is_price:
341
379
  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
380
  rate, compare = (self.df['rate'][index], self.df['compare'][index])
@@ -348,27 +386,23 @@ class InfoMixin(LineMixin):
348
386
  cd = divmod(c, 1)
349
387
  if cd[1]: c = f'{float_to_str(cd[0])} {Fraction((cd[1]))}'
350
388
  else: c = float_to_str(cd[0])
351
-
352
389
  comd = divmod(compare, 1)
353
390
  if comd[1]: com = f'{float_to_str(comd[0], plus=True)} {Fraction(comd[1])}'
354
391
  else: com = float_to_str(comd[0], plus=True)
355
-
356
392
  o = o.__round__(self.digit_price)
357
393
  od = divmod(o, 1)
358
394
  if od[1]: o = f'{float_to_str(od[0])} {Fraction(od[1])}'
359
395
  else: o = float_to_str(od[0])
360
-
361
396
  h = h.__round__(self.digit_price)
362
397
  hd = divmod(h, 1)
363
398
  if hd[1]: h = f'{float_to_str(hd[0])} {Fraction(hd[1])}'
364
399
  else: h = float_to_str(hd[0])
365
-
366
400
  l = l.__round__(self.digit_price)
367
401
  ld = divmod(l, 1)
368
402
  if ld[1]: l = f'{float_to_str(ld[0])} {Fraction(ld[1])}'
369
403
  else: l = float_to_str(ld[0])
370
404
 
371
- text = self.candleformat.format(
405
+ text = self.format_candleinfo.format(
372
406
  dt=dt,
373
407
  close=f'{c:>{self._length_text}}{self.unit_price}',
374
408
  rate=f'{r:>{self._length_text}}%',
@@ -376,13 +410,13 @@ class InfoMixin(LineMixin):
376
410
  open=f'{o:>{self._length_text}}{self.unit_price}', rate_open=f'{Or:+06,.2f}%',
377
411
  high=f'{h:>{self._length_text}}{self.unit_price}', rate_high=f'{hr:+06,.2f}%',
378
412
  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}%',
413
+ volume=f'{v:>{self._length_text}}{self.unit_volume}', rate_volume=vr,
380
414
  )
381
415
  else:
382
416
  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
417
  com = float_to_str(compare, self.digit_price, plus=True)
384
418
 
385
- text = self.candleformat.format(
419
+ text = self.format_candleinfo.format(
386
420
  dt=dt,
387
421
  close=f'{c:>{self._length_text}}{self.unit_price}',
388
422
  rate=f'{r:>{self._length_text}}%',
@@ -390,23 +424,38 @@ class InfoMixin(LineMixin):
390
424
  open=f'{o:>{self._length_text}}{self.unit_price}', rate_open=f'{Or:+06,.2f}%',
391
425
  high=f'{h:>{self._length_text}}{self.unit_price}', rate_high=f'{hr:+06,.2f}%',
392
426
  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}%',
427
+ volume=f'{v:>{self._length_text}}{self.unit_volume}', rate_volume=vr,
394
428
  )
395
- else:
396
- vrate = f'{vr:+06,.2f}'
397
- text = self.volumeformat.format(
429
+ elif self.volume:
430
+ compare = self.df['compare_volume'][index]
431
+ com = float_to_str(compare, self.digit_volume, plus=True)
432
+ text = self.format_volumeinfo.format(
398
433
  dt=dt,
399
434
  volume=f'{v:>{self._length_text}}{self.unit_volume}',
400
- rate_volume=f'{vrate:>{self._length_text}}%',
435
+ rate_volume=f'{vr:>{self._length_text}}%',
436
+ compare=f'{com:>{self._length_text}}{self.unit_volume}',
401
437
  )
438
+ else: text = ''
402
439
  return text
403
440
 
404
441
 
405
- class CursorMixin(InfoMixin):
442
+ class BaseMixin(InfoMixin):
406
443
  pass
407
444
 
408
445
 
409
- class Chart(CursorMixin, CM, Mixin):
446
+ class Chart(BaseMixin, Mixin):
447
+ def _add_collection(self):
448
+ super()._add_collection()
449
+ return self.add_artist()
450
+
451
+ def _draw_artist(self):
452
+ super()._draw_artist()
453
+ return self.draw_artist()
454
+
455
+ def _get_segments(self):
456
+ self.generate_data()
457
+ return super()._get_segments()
458
+
410
459
  def _on_draw(self, e):
411
460
  super()._on_draw(e)
412
461
  return self.on_draw(e)
@@ -415,34 +464,22 @@ class Chart(CursorMixin, CM, Mixin):
415
464
  self.on_pick(e)
416
465
  return super()._on_pick(e)
417
466
 
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
467
+ def _set_candle_segments(self, index_start, index_end):
468
+ super()._set_candle_segments(index_start, index_end)
469
+ self.set_segment(index_start, index_end)
470
+ return
429
471
 
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()=}')
472
+ def _set_wick_segments(self, index_start, index_end, simpler=False):
473
+ super()._set_wick_segments(index_start, index_end, simpler)
474
+ self.set_segment(index_start, index_end, simpler)
475
+ return
438
476
 
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()
477
+ def _set_line_segments(self, index_start, index_end, simpler=False, set_ma=True):
478
+ super()._set_line_segments(index_start, index_end, simpler, set_ma)
479
+ self.set_segment(index_start, index_end, simpler, set_ma)
480
+ return
447
481
 
482
+ def _on_move(self, e):
483
+ super()._on_move(e)
484
+ return self.on_move(e)
448
485