seolpyo-mplchart 0.1.3.4__py3-none-any.whl → 1.0.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.

Potentially problematic release.


This version of seolpyo-mplchart might be problematic. Click here for more details.

seolpyo_mplchart/draw.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from matplotlib.backend_bases import PickEvent
2
2
  from matplotlib.collections import LineCollection
3
3
  from matplotlib.lines import Line2D
4
+ from matplotlib.text import Text
4
5
  import numpy as np
5
6
  import pandas as pd
6
7
 
@@ -9,208 +10,353 @@ from .base import Base
9
10
 
10
11
 
11
12
  class Mixin:
13
+ def add_collection(self):
14
+ "This method work when ```__init__()``` run."
15
+ return
16
+
17
+ def draw_artist(self):
18
+ "This method work before ```figure.canvas.blit()```."
19
+ return
20
+
21
+ def generate_data(self):
22
+ "This method work before create segments."
23
+ return
24
+
12
25
  def on_draw(self, e):
13
- "This function works if draw event active."
26
+ "If draw event active, This method work."
14
27
  return
28
+
15
29
  def on_pick(self, e):
16
- "This function works if pick event active."
30
+ "If draw pick active, This method work."
31
+ return
32
+
33
+
34
+ class CollectionMixin(Base):
35
+ facecolor_volume, edgecolor_volume = ('#1f77b4', 'k')
36
+ watermark = 'seolpyo mplchart'
37
+
38
+ def __init__(self, *args, **kwargs):
39
+ super().__init__(*args, **kwargs)
40
+
41
+ self._add_collection()
42
+ return
43
+
44
+ def _add_collection(self):
45
+ self.collection_ma = LineCollection([], animated=True, linewidths=1)
46
+ self.ax_price.add_collection(self.collection_ma)
47
+
48
+ self.collection_candle = LineCollection([], animated=True, linewidths=0.8)
49
+ self.ax_price.add_collection(self.collection_candle)
50
+
51
+ self.collection_volume = LineCollection([], animated=True, linewidths=1)
52
+ self.ax_volume.add_collection(self.collection_volume)
53
+
54
+ x = (self.adjust['right']-self.adjust['left']) / 2
55
+ self.text_watermark = Text(
56
+ x=x, y=0.51, text=self.watermark,
57
+ animated=True,
58
+ fontsize=20, color=self.color_tick_label, alpha=0.2,
59
+ horizontalalignment='center', verticalalignment='center',
60
+ )
61
+ self.figure.add_artist(self.text_watermark)
17
62
  return
18
63
 
19
64
 
20
- _set_key = {'zero', 'x', 'left', 'right', 'top', 'bottom',}
65
+ _set_key = {'_x', '_left', '_right', '_volleft', '_volright', '_top', '_bottom', '_pre', '_zero', '_volymax',}
21
66
 
22
- class DataMixin(Base):
67
+ class DataMixin(CollectionMixin):
23
68
  df: pd.DataFrame
69
+
24
70
  date = 'date'
25
71
  Open, high, low, close = ('open', 'high', 'low', 'close')
26
72
  volume = 'volume'
27
-
28
- _visible_ma = set()
29
- label_ma = '{}일선'
30
73
  list_ma = (5, 20, 60, 120, 240)
31
- # https://matplotlib.org/stable/gallery/color/named_colors.html
32
- list_macolor = ('darkred', 'fuchsia', 'olive', 'orange', 'navy', 'darkmagenta', 'limegreen', 'darkcyan',)
33
74
 
75
+ candle_width_half, volume_width_half = (0.24, 0.36)
34
76
  color_up, color_down = ('#fe3032', '#0095ff')
35
77
  color_flat = 'k'
36
78
  color_up_down, color_down_up = ('w', 'w')
37
- colors_volume = '#1f77b4'
38
79
 
39
- candlewidth_half, volumewidth_half = (0.3, 0.36)
80
+ def _generate_data(self, df: pd.DataFrame, sort_df, calc_ma, set_candlecolor, set_volumecolor, *_, **__):
81
+ self._validate_column_key()
40
82
 
41
- def _generate_data(self, df: pd.DataFrame, sort_df=True, calc_ma=True, **_):
42
- for i in ('date', 'Open', 'high', 'low', 'close', 'volume'):
43
- k: str = getattr(self, i)
44
- if k in _set_key: raise Exception(f'you can not set "self.{i}" value in {_set_key}.\nself.{i}={k!r}')
45
- if i != 'date':
46
- dtype = df[k].dtype
47
- if not isinstance(dtype, (np.dtypes.Float64DType, np.dtypes.Int64DType, np.dtypes.Float32DType, np.dtypes.Int32DType)):
48
- raise TypeError(f'column dtype must be one of "float64" or "int64" or "float32" or "int32".(excluding "date" column)\ndf[{k!r}].dtype={dtype!r}')
83
+ # 오름차순 정렬
84
+ if sort_df: df = df.sort_values([self.date])
85
+ df = df.reset_index()
49
86
 
50
- # DataFrame 정렬
51
- if sort_df:
52
- df = df.sort_values([self.date]).reset_index()
87
+ self.list_index = df.index.tolist()
88
+ self.xmin, self.xmax = (0, self.list_index[-1])
53
89
 
54
90
  if not self.list_ma: self.list_ma = tuple()
55
- if calc_ma:
56
- for i in self.list_ma: df[f'ma{i}'] = df[self.close].rolling(i).mean()
57
91
  else:
92
+ self.list_ma = sorted(self.list_ma)
93
+ # 가격이동평균선 계산
94
+ if calc_ma:
95
+ for i in self.list_ma: df[f'ma{i}'] = df[self.close].rolling(i).mean()
96
+ else:
97
+ set_key = set(self.df.keys())
98
+ for i in self.list_ma:
99
+ key = f'ma{i}'
100
+ if key not in set_key:
101
+ raise KeyError(f'"{key}" column not found.\nset calc_ma=True or add "{key}" column.')
102
+
103
+ df['_x'] = df.index + 0.5
104
+ df['_left'] = df['_x'] - self.candle_width_half
105
+ df['_right'] = df['_x'] + self.candle_width_half
106
+ df['_volleft'] = df['_x'] - self.volume_width_half
107
+ df['_volright'] = df['_x'] + self.volume_width_half
108
+ df.loc[:, '_zero'] = 0
109
+
110
+ df['_top'] = np.where(df[self.Open] <= df[self.close], df[self.close], df[self.Open])
111
+ df['_top'] = np.where(df[self.close] < df[self.Open], df[self.Open], df[self.close])
112
+ df['_bottom'] = np.where(df[self.Open] <= df[self.close], df[self.Open], df[self.close])
113
+ df['_bottom'] = np.where(df[self.close] < df[self.Open], df[self.close], df[self.Open])
114
+
115
+ df['_pre'] = df[self.close].shift(1)
116
+ if self.volume: df['_volymax'] = df[self.volume] * 1.2
117
+
118
+ if not set_candlecolor:
119
+ keys = set(df.keys())
120
+ for i in ('facecolor', 'edgecolor', 'volumefacecolor',):
121
+ if i not in keys:
122
+ raise Exception(f'"{i}" column not in DataFrame.\nadd column or set set_candlecolor=True.')
123
+ else:
124
+ # 양봉
125
+ df.loc[:, ['facecolor', 'edgecolor']] = (self.color_up, self.color_up)
126
+ if self.color_up != self.color_down:
127
+ # 음봉
128
+ df.loc[df[self.close] < df[self.Open], ['facecolor', 'edgecolor']] = (self.color_down, self.color_down)
129
+ if self.color_up != self.color_flat:
130
+ # 보합
131
+ df.loc[df[self.close] == df[self.Open], ['facecolor', 'edgecolor']] = (self.color_flat, self.color_flat)
132
+ if self.color_up != self.color_up_down:
133
+ # 양봉(비우기)
134
+ df.loc[(df['facecolor'] == self.color_up) & (df[self.close] < df['_pre']), 'facecolor'] = self.color_up_down
135
+ if self.color_down != self.color_down_up:
136
+ # 음봉(비우기)
137
+ df.loc[(df['facecolor'] == self.color_down) & (df['_pre'] < df[self.close]), ['facecolor']] = self.color_down_up
138
+
139
+ if not set_volumecolor:
58
140
  keys = set(df.keys())
59
- for i in self.list_ma:
60
- if f'ma{i}' not in keys:
61
- raise Exception(f'"ma{i}" column not in DataFrame.\nadd column or set calc_ma=True.')
62
-
63
- df['x'] = df.index + 0.5
64
- df['left'] = df['x'] - self.candlewidth_half
65
- df['right'] = df['x'] + self.candlewidth_half
66
- df['vleft'] = df['x'] - self.volumewidth_half
67
- df['vright'] = df['x'] + self.volumewidth_half
68
-
69
- df['top'] = np.where(df[self.Open] <= df[self.close], df[self.close], df[self.Open])
70
- df['top'] = np.where(df[self.close] < df[self.Open], df[self.Open], df[self.close])
71
- df['bottom'] = np.where(df[self.Open] <= df[self.close], df[self.Open], df[self.close])
72
- df['bottom'] = np.where(df[self.close] < df[self.Open], df[self.close], df[self.Open])
73
-
74
- # 양봉
75
- df.loc[:, ['zero', 'facecolor', 'edgecolor']] = (0, self.color_up, self.color_up)
76
- if self.color_up != self.color_down:
77
- # 음봉
78
- df.loc[df[self.close] < df[self.Open], ['facecolor', 'edgecolor']] = (self.color_down, self.color_down)
79
- if self.color_up != self.color_flat:
80
- # 보합
81
- df.loc[df[self.close] == df[self.Open], ['facecolor', 'edgecolor']] = (self.color_flat, self.color_flat)
82
- if self.color_up != self.color_up_down:
83
- # 양봉(비우기)
84
- df.loc[(df['facecolor'] == self.color_up) & (df[self.close] < df[self.close].shift(1)), 'facecolor'] = self.color_up_down
85
- if self.color_down != self.color_down_up:
86
- # 음봉(비우기)
87
- df.loc[(df['facecolor'] == self.color_down) & (df[self.close].shift(1) < df[self.close]), ['facecolor']] = self.color_down_up
141
+ for i in ('volumefacecolor', 'volumeedgecolor',):
142
+ if i not in keys:
143
+ raise Exception(f'"{i}" column not in DataFrame.\nadd column or set set_volumecolor=True.')
144
+ else:
145
+ # 거래량
146
+ df.loc[:, ['volumefacecolor', 'volumeedgecolor']] = (self.facecolor_volume, self.edgecolor_volume)
88
147
 
89
148
  self.df = df
90
149
  return
91
150
 
92
- class CollectionMixin(DataMixin):
93
- color_sliderline = 'k'
151
+ def _validate_column_key(self):
152
+ for i in ('date', 'Open', 'high', 'low', 'close', 'volume'):
153
+ v = getattr(self, i)
154
+ if v in _set_key: raise Exception(f'you can not set "{i}" to column key.\nself.{i}={v!r}')
155
+ return
94
156
 
95
- _masegment, _macolors = ({}, {})
96
157
 
97
- def __init__(self, *args, **kwargs):
98
- super().__init__(*args, **kwargs)
158
+ class SegmentMixin(DataMixin):
159
+ _visible_ma = set()
99
160
 
100
- self._connect_event()
101
- self._add_collection()
102
- return
161
+ limit_candle = 800
162
+ limit_wick = 4_000
163
+ limit_volume = 800
103
164
 
104
- def _connect_event(self):
105
- self.canvas.mpl_connect('pick_event', lambda x: self._on_pick(x))
106
- return
165
+ color_priceline = 'k'
166
+ format_ma = '{}일선'
167
+ # https://matplotlib.org/stable/gallery/color/named_colors.html
168
+ list_macolor = ('darkred', 'fuchsia', 'olive', 'orange', 'navy', 'darkmagenta', 'limegreen', 'darkcyan',)
107
169
 
108
- def _add_collection(self):
109
- self.macollection = LineCollection([], animated=True, antialiased=True, linewidth=1)
110
- self.ax_price.add_collection(self.macollection)
111
-
112
- self.slidercollection = LineCollection([], animated=True, antialiased=True)
113
- self.ax_slider.add_collection(self.slidercollection)
114
-
115
- # https://white.seolpyo.com/entry/145/
116
- self.candlecollection = LineCollection([], animated=True, antialiased=True, linewidths=1)
117
- self.ax_price.add_collection(self.candlecollection)
118
-
119
- # https://white.seolpyo.com/entry/145/
120
- self.volumecollection = LineCollection([], animated=True, antialiased=True, facecolors=self.colors_volume, linewidths=1, edgecolors='k')
121
- self.ax_volume.add_collection(self.volumecollection)
122
-
123
- return
124
-
125
- def _set_collection(self):
126
- candleseg = self.df[[
127
- 'x', self.high,
128
- 'x', 'top',
129
- 'left', 'top',
130
- 'left', 'bottom',
131
- 'x', 'bottom',
132
- 'x', self.low,
133
- 'x', 'bottom',
134
- 'right', 'bottom',
135
- 'right', 'top',
136
- 'x', 'top',
170
+ def _get_segments(self):
171
+ # 캔들 세그먼트
172
+ segment_candle = self.df[[
173
+ '_x', self.high,
174
+ '_x', '_top',
175
+ '_left', '_top',
176
+ '_left', '_bottom',
177
+ '_x', '_bottom',
178
+ '_x', self.low,
179
+ '_x', '_bottom',
180
+ '_right', '_bottom',
181
+ '_right', '_top',
182
+ '_x', '_top',
183
+ '_x', self.high,
184
+ '_x', '_top',
137
185
  ]].values
138
- candleseg = candleseg.reshape(candleseg.shape[0], 10, 2)
186
+ self.segment_candle = segment_candle.reshape(segment_candle.shape[0], 12, 2)
139
187
 
140
- self.candlecollection.set_segments(candleseg)
141
- self.candlecollection.set_facecolor(self.df['facecolor'].values)
142
- self.candlecollection.set_edgecolor(self.df['edgecolor'].values)
143
-
144
- volseg = self.df[[
145
- 'left', 'zero',
146
- 'left', self.volume,
147
- 'right', self.volume,
148
- 'right', 'zero',
188
+ # 심지 세그먼트
189
+ segment_wick = self.df[[
190
+ '_x', self.high,
191
+ '_x', self.low,
149
192
  ]].values
150
- volseg = volseg.reshape(volseg.shape[0], 4, 2)
151
-
152
- self.volumecollection.set_segments(volseg)
153
-
154
- self._set_macollection()
155
-
156
- # 가격이동평균선
157
- maseg = reversed(self._masegment.values())
158
- colors, widths = ([], [])
159
- for i in reversed(self._macolors.values()): (colors.append(i), widths.append(1))
160
- self.macollection.set_segments(maseg)
161
- self.macollection.set_edgecolor(colors)
162
-
163
- # 슬라이더 선형차트
164
- keys = []
165
- for i in reversed(self.list_ma):
166
- keys.append('x')
167
- keys.append(f'ma{i}')
168
- sliderseg = self.df[keys + ['x', self.close]].values
169
- sliderseg = sliderseg.reshape(sliderseg.shape[0], self.list_ma.__len__()+1, 2).swapaxes(0, 1)
170
- (colors.append(self.color_sliderline), widths.append(1.8))
171
- self.slidercollection.set_segments(sliderseg)
172
- self.slidercollection.set_edgecolor(colors)
173
- self.slidercollection.set_linewidth(widths)
193
+ self.segment_candle_wick = segment_wick.reshape(segment_wick.shape[0], 2, 2)
194
+
195
+ # 종가 세그먼트
196
+ segment_priceline = segment_wick = self.df[['_x', self.close]].values
197
+ self.segment_priceline = segment_priceline.reshape(1, *segment_wick.shape)
198
+
199
+ self.facecolor_candle = self.df['facecolor'].values
200
+ self.edgecolor_candle = self.df['edgecolor'].values
201
+
202
+ if self.volume:
203
+ # 거래량 바 세그먼트
204
+ segment_volume = self.df[[
205
+ '_volleft', '_zero',
206
+ '_volleft', self.volume,
207
+ '_volright', self.volume,
208
+ '_volright', '_zero',
209
+ ]].values
210
+ self.segment_volume = segment_volume.reshape(segment_volume.shape[0], 4, 2)
211
+
212
+ # 거래량 심지 세그먼트
213
+ segment_volume_wick = self.df[[
214
+ '_x', '_zero',
215
+ '_x', self.volume,
216
+ ]].values
217
+ self.segment_volume_wick = segment_volume_wick.reshape(segment_volume_wick.shape[0], 2, 2)
218
+
219
+ self.facecolor_volume = self.df['volumefacecolor'].values
220
+ self.edgecolor_volume = self.df['volumeedgecolor'].values
221
+
222
+ self._get_ma_segment()
174
223
  return
175
224
 
176
- def _set_macollection(self):
225
+ def _get_ma_segment(self):
177
226
  # 기존 legend 제거
178
227
  legends = self.ax_legend.get_legend()
179
228
  if legends: legends.remove()
180
229
 
181
- self._masegment.clear(), self._macolors.clear()
182
- handles, labels = ([], [])
183
230
  self._visible_ma.clear()
231
+
232
+ if not self.list_ma: return
233
+
234
+ list_handle, list_label, list_color = ([], [], [])
235
+ arr = (0, 1)
184
236
  for n, i in enumerate(self.list_ma):
185
237
  try: c = self.list_macolor[n]
186
- except: c = self.color_sliderline
187
- self._macolors[i] = c
188
- # seg = self.df['x', f'ma{i}'].values
189
- seg = self.df.loc[self.df[f'ma{i}'] != np.nan, ['x', f'ma{i}']].values
190
- # print(f'{seg[:5]=}')
191
- self._masegment[i] = seg
238
+ except: c = self.color_priceline
239
+ list_color.append(c)
192
240
 
193
- handles.append(Line2D([0, 1], [0, 1], color=c, linewidth=5, label=i))
194
- labels.append(self.label_ma.format(i))
241
+ list_handle.append(Line2D(arr, arr, color=c, linewidth=5, label=i))
242
+ list_label.append(self.format_ma.format(i))
195
243
 
196
244
  self._visible_ma.add(i)
197
245
 
246
+ # 주가 차트 가격이동평균선
247
+ key_ma = []
248
+ for i in reversed(self.list_ma):
249
+ key_ma.append('_x')
250
+ key_ma.append(f'ma{i}')
251
+ segment_ma = self.df[key_ma].values
252
+ self.segment_ma = segment_ma.reshape(segment_ma.shape[0], len(self.list_ma), 2).swapaxes(0, 1)
253
+ self.edgecolor_ma = list(reversed(list_color))
254
+
198
255
  # 가격이동평균선 legend 생성
199
- if handles:
200
- legends = self.ax_legend.legend(handles, labels, loc='lower left', ncol=10)
256
+ if list_handle:
257
+ legends = self.ax_legend.legend(
258
+ list_handle, list_label, loc='lower left', ncol=10,
259
+ facecolor=self.color_background, edgecolor=self.color_tick,
260
+ labelcolor=self.color_tick_label,
261
+ )
262
+ for i in legends.legend_handles: i.set_picker(5)
263
+ return
264
+
265
+ def _set_segments(self, index_start, index_end, simpler, set_ma):
266
+ indsub = index_end - index_start
267
+ if index_start < 0: index_start = 0
268
+ if index_end < 1: index_end = 1
201
269
 
202
- for i in legends.legend_handles:
203
- i.set_picker(5)
270
+ if indsub < self.limit_candle:
271
+ self._set_candle_segments(index_start, index_end)
272
+ elif indsub < self.limit_wick:
273
+ self._set_wick_segments(index_start, index_end, simpler)
274
+ else:
275
+ self._set_line_segments(index_start, index_end, simpler, set_ma)
276
+ return
277
+
278
+ def _set_candle_segments(self, index_start, index_end):
279
+ index_end += 1
280
+
281
+ self.collection_candle.set_segments(self.segment_candle[index_start:index_end])
282
+ self.collection_candle.set_facecolor(self.facecolor_candle[index_start:index_end])
283
+ self.collection_candle.set_edgecolor(self.edgecolor_candle[index_start:index_end])
284
+
285
+ if self.volume:
286
+ self.collection_volume.set_segments(self.segment_volume[index_start:index_end])
287
+ self.collection_volume.set_linewidth(0.7)
288
+ self.collection_volume.set_facecolor(self.facecolor_volume[index_start:index_end])
289
+ self.collection_volume.set_edgecolor(self.edgecolor_volume[index_start:index_end])
290
+
291
+ self.collection_ma.set_segments(self.segment_ma[:, index_start:index_end])
292
+ self.collection_ma.set_edgecolor(list(reversed(self.edgecolor_ma)))
293
+ return
294
+
295
+ def _set_wick_segments(self, index_start, index_end, simpler=False):
296
+ index_end += 1
297
+
298
+ self.collection_candle.set_segments(self.segment_candle_wick[index_start:index_end])
299
+ self.collection_candle.set_facecolor([])
300
+ self.collection_candle.set_edgecolor(self.edgecolor_candle[index_start:index_end])
301
+
302
+ if self.volume:
303
+ seg = self.segment_volume_wick[index_start:index_end]
304
+ if simpler:
305
+ values = seg[:, 1, 1]
306
+ top_index = np.argsort(-values)[:self.limit_volume]
307
+ seg = seg[top_index]
308
+ self.collection_volume.set_segments(seg)
309
+ self.collection_volume.set_linewidth(1.3)
310
+ self.collection_volume.set_facecolor(self.facecolor_volume[index_start:index_end])
311
+ self.collection_volume.set_edgecolor(self.facecolor_volume[index_start:index_end])
312
+
313
+ self.collection_ma.set_segments(self.segment_ma[:, index_start:index_end])
314
+ self.collection_ma.set_edgecolor(list(reversed(self.edgecolor_ma)))
315
+ return
316
+
317
+ def _set_line_segments(self, index_start, index_end, simpler=False, set_ma=True):
318
+ index_end += 1
319
+
320
+ self.collection_candle.set_segments(self.segment_priceline[:, index_start:index_end])
321
+ self.collection_candle.set_facecolor([])
322
+ self.collection_candle.set_edgecolor(self.color_priceline)
323
+
324
+ if self.volume:
325
+ seg = self.segment_volume_wick[index_start:index_end]
326
+ if simpler:
327
+ values = seg[:, 1, 1]
328
+ top_index = np.argsort(-values)[:self.limit_volume]
329
+ seg = seg[top_index]
330
+ self.collection_volume.set_segments(seg)
331
+ self.collection_volume.set_linewidth(1.3)
332
+ self.collection_volume.set_facecolor(self.facecolor_volume[index_start:index_end])
333
+ self.collection_volume.set_edgecolor(self.facecolor_volume[index_start:index_end])
334
+
335
+ if not set_ma: self.collection_ma.set_segments([])
336
+ else:
337
+ self.collection_ma.set_segments(self.segment_ma[:, index_start:index_end])
338
+ self.collection_ma.set_edgecolor(list(reversed(self.edgecolor_ma)))
339
+ return
340
+
341
+
342
+ class EventMixin(SegmentMixin):
343
+ def __init__(self, *args, **kwargs):
344
+ super().__init__(*args, **kwargs)
345
+
346
+ self._connect_event()
347
+ return
348
+
349
+ def _connect_event(self):
350
+ self.figure.canvas.mpl_connect('pick_event', lambda x: self._on_pick(x))
204
351
  return
205
352
 
206
353
  def _on_pick(self, e):
207
354
  self._pick_ma_action(e)
208
-
209
- return self._draw()
355
+ return
210
356
 
211
357
  def _pick_ma_action(self, e: PickEvent):
212
358
  handle = e.artist
213
- if e.artist.get_alpha() == 0.2:
359
+ if handle.get_alpha() == 0.2:
214
360
  visible = True
215
361
  handle.set_alpha(1.0)
216
362
  else:
@@ -218,130 +364,154 @@ class CollectionMixin(DataMixin):
218
364
  handle.set_alpha(0.2)
219
365
 
220
366
  n = int(handle.get_label())
221
-
222
367
  if visible: self._visible_ma = {i for i in self.list_ma if i in self._visible_ma or i == n}
223
368
  else: self._visible_ma = {i for i in self._visible_ma if i != n}
224
369
 
225
- self.macollection.set_segments([self._masegment[i] for i in reversed(self._masegment) if i in self._visible_ma])
226
- colors = [self._macolors[i] for i in reversed(self._macolors) if i in self._visible_ma]
227
- self.macollection.set_colors(colors)
370
+ alphas = [(1 if i in self._visible_ma else 0) for i in reversed(self.list_ma)]
371
+ self.collection_ma.set_alpha(alphas)
372
+
373
+ self._draw()
228
374
  return
229
375
 
230
376
  def _draw(self):
231
- if self.fig.canvas is not self.canvas:
232
- self.canvas = self.fig.canvas
233
- self.canvas.draw()
377
+ self.figure.canvas.draw()
234
378
  return
235
379
 
236
380
 
237
- class BackgroundMixin(CollectionMixin):
238
- background = None
381
+ class DrawMixin(EventMixin):
239
382
  candle_on_ma = True
240
383
 
241
- _creating_background = False
384
+ def set_data(self, df, sort_df=True, calc_ma=True, set_candlecolor=True, set_volumecolor=True, *args, **kwargs):
385
+ self._generate_data(df, sort_df, calc_ma, set_candlecolor, set_volumecolor, *args, **kwargs)
386
+ self._get_segments()
242
387
 
243
- def __init__(self, *args, **kwargs):
244
- super().__init__(*args, **kwargs)
388
+ vmin, vmax = self.get_default_lim()
389
+ self._set_lim(vmin, vmax)
245
390
  return
246
391
 
247
392
  def _connect_event(self):
248
393
  super()._connect_event()
249
- self.canvas.mpl_connect('draw_event', lambda x: self._on_draw(x))
250
- return
251
-
252
- def _create_background(self):
253
- if self._creating_background: return
254
-
255
- if self.fig.canvas is not self.canvas:
256
- self.canvas = self.fig.canvas
257
-
258
- self._creating_background = True
259
- self._copy_bbox()
260
- self._creating_background = False
394
+ self.figure.canvas.mpl_connect('draw_event', lambda x: self._on_draw(x))
261
395
  return
262
396
 
263
- def _copy_bbox(self):
397
+ def _on_draw(self, e):
264
398
  self._draw_artist()
265
- self.background = self.canvas.renderer.copy_from_bbox(self.fig.bbox)
399
+ self._blit()
266
400
  return
267
401
 
268
402
  def _draw_artist(self):
269
- renderer = self.canvas.renderer
270
-
271
- self.ax_slider.xaxis.draw(renderer)
272
- self.ax_slider.yaxis.draw(renderer)
273
-
274
- self.slidercollection.draw(renderer)
403
+ renderer = self.figure.canvas.renderer
275
404
 
276
405
  self.ax_price.xaxis.draw(renderer)
277
406
  self.ax_price.yaxis.draw(renderer)
278
407
 
279
408
  if self.candle_on_ma:
280
- self.macollection.draw(renderer)
281
- self.candlecollection.draw(renderer)
409
+ self.collection_ma.draw(renderer)
410
+ self.collection_candle.draw(renderer)
282
411
  else:
283
- self.candlecollection.draw(renderer)
284
- self.macollection.draw(renderer)
412
+ self.collection_candle.draw(renderer)
413
+ self.collection_ma.draw(renderer)
414
+
415
+ if self.watermark:
416
+ self.text_watermark.set_text(self.watermark)
417
+ self.text_watermark.draw(renderer)
285
418
 
286
419
  self.ax_volume.xaxis.draw(renderer)
287
420
  self.ax_volume.yaxis.draw(renderer)
288
421
 
289
- self.volumecollection.draw(renderer)
422
+ self.collection_volume.draw(renderer)
290
423
  return
291
424
 
292
- def _on_draw(self, e):
293
- self.background = None
294
- self._restore_region()
425
+ def _blit(self):
426
+ self.figure.canvas.blit()
295
427
  return
296
428
 
297
- def _restore_region(self):
298
- if not self.background: self._create_background()
429
+ def _set_lim(self, xmin, xmax, simpler=False, set_ma=True):
430
+ self.vxmin, self.vxmax = (xmin, xmax + 1)
431
+ if xmin < 0: xmin = 0
432
+ if xmax < 0: xmax = 0
433
+ if xmin == xmax: xmax += 1
434
+
435
+ ymin, ymax = (self.df[self.low][xmin:xmax].min(), self.df[self.high][xmin:xmax].max())
436
+ yspace = (ymax - ymin) / 15
437
+ # 주가 차트 ymin, ymax
438
+ self.price_ymin, self.price_ymax = (ymin-yspace, ymax+yspace)
439
+
440
+ # 거래량 차트 ymax
441
+ self.volume_ymax = self.df['_volymax'][xmin:xmax].max() if self.volume else 1
442
+
443
+ self._set_segments(xmin, xmax, simpler, set_ma)
444
+ self._change_lim(self.vxmin, self.vxmax)
445
+ return
446
+
447
+ def _change_lim(self, xmin, xmax):
448
+ # 주가 차트 xlim
449
+ self.ax_price.set_xlim(xmin, xmax)
450
+ # 거래량 차트 xlim
451
+ self.ax_volume.set_xlim(xmin, xmax)
299
452
 
300
- self.canvas.renderer.restore_region(self.background)
453
+ # 주가 차트 ylim
454
+ self.ax_price.set_ylim(self.price_ymin, self.price_ymax)
455
+ # 거래량 차트 ylim
456
+ self.ax_volume.set_ylim(0, self.volume_ymax)
301
457
  return
302
458
 
303
- class DrawMixin(BackgroundMixin):
304
- def set_data(self, df: pd.DataFrame, sort_df=True, calc_ma=True, change_lim=True, *_, **kwargs):
305
- self._set_data(df, sort_df, calc_ma, change_lim, **kwargs)
306
- return self.df
459
+ def get_default_lim(self):
460
+ return (0, self.list_index[-1])
461
+
462
+
463
+ class BackgroundMixin(DrawMixin):
464
+ background = None
307
465
 
308
- def _set_data(self, df: pd.DataFrame, sort_df=True, calc_ma=True, change_lim=True, *_, **kwargs):
309
- self._generate_data(df, sort_df, calc_ma, **kwargs)
310
- self._set_collection()
311
- self._draw_collection(change_lim)
466
+ _creating_background = False
467
+
468
+ def _connect_event(self):
469
+ self.figure.canvas.mpl_connect('pick_event', lambda x: self._on_pick(x))
470
+ self.figure.canvas.mpl_connect('draw_event', lambda x: self._on_draw(x))
312
471
  return
313
472
 
314
- def _draw_collection(self, change_lim=True):
315
- xmax = self.df['x'].values[-1] + 1
473
+ def _create_background(self):
474
+ if self._creating_background: return
316
475
 
317
- xspace = xmax / 40
318
- self.xmin, self.xmax = (-xspace, xmax+xspace)
319
- # 슬라이더 xlim
320
- self.ax_slider.set_xlim(self.xmin, self.xmax)
321
- if change_lim:
322
- # 주가 xlim
323
- self.ax_price.set_xlim(self.xmin, self.xmax)
324
- # 거래량 xlim
325
- self.ax_volume.set_xlim(self.xmin, self.xmax)
476
+ self._creating_background = True
477
+ self._copy_bbox()
478
+ self._creating_background = False
479
+ return
326
480
 
327
- ymin, ymax = (self.df[self.low].min(), self.df[self.high].max())
328
- ysub = (ymax - ymin) / 15
481
+ def _copy_bbox(self):
482
+ self._draw_artist()
483
+ self.background = self.figure.canvas.renderer.copy_from_bbox(self.figure.bbox)
484
+ return
329
485
 
330
- # 슬라이더 ylim
331
- self._slider_ymin, self._slider_ymax = (ymin-ysub, ymax+ysub)
332
- self.ax_slider.set_ylim(self._slider_ymin, self._slider_ymax)
486
+ def _on_draw(self, e):
487
+ self.background = None
488
+ self._restore_region()
489
+ return
333
490
 
334
- # 주가 ylim
335
- self._price_ymin, self._price_ymax = (ymin-ysub, ymax+ysub)
336
- if change_lim: self.ax_price.set_ylim(self._price_ymin, self._price_ymax)
491
+ def _restore_region(self):
492
+ if not self.background: self._create_background()
337
493
 
338
- # 거래량 ylim
339
- self._vol_ymax = self.df[self.volume].max() * 1.2
340
- if change_lim: self.ax_volume.set_ylim(0, self._vol_ymax)
494
+ self.figure.canvas.renderer.restore_region(self.background)
341
495
  return
342
496
 
343
497
 
344
- class Chart(DrawMixin, Mixin):
498
+ class BaseMixin(BackgroundMixin):
499
+ pass
500
+
501
+
502
+ class Chart(BaseMixin, Mixin):
503
+ def _add_collection(self):
504
+ super()._add_collection()
505
+ return self.add_collection()
506
+
507
+ def _draw_artist(self):
508
+ super()._draw_artist()
509
+ return self.draw_artist()
510
+
511
+ def _get_segments(self):
512
+ self.generate_data()
513
+ return super()._get_segments()
514
+
345
515
  def _on_draw(self, e):
346
516
  super()._on_draw(e)
347
517
  return self.on_draw(e)
@@ -350,22 +520,3 @@ class Chart(DrawMixin, Mixin):
350
520
  self.on_pick(e)
351
521
  return super()._on_pick(e)
352
522
 
353
-
354
- if __name__ == '__main__':
355
- import json
356
- from time import time
357
-
358
- import matplotlib.pyplot as plt
359
- from pathlib import Path
360
-
361
- with open(Path(__file__).parent / 'data/samsung.txt', 'r', encoding='utf-8') as txt:
362
- data = json.load(txt)
363
- print(f'{len(data)=}')
364
- # data = data[:200]
365
- df = pd.DataFrame(data)
366
-
367
- t = time()
368
- DrawMixin().set_data(df)
369
- t2 = time() - t
370
- print(f'{t2=}')
371
- plt.show()