seolpyo-mplchart 0.1.3.3__py3-none-any.whl → 1.0.0__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,347 @@ 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)
139
-
140
- self.candlecollection.set_segments(candleseg)
141
- self.candlecollection.set_facecolor(self.df['facecolor'].values)
142
- self.candlecollection.set_edgecolor(self.df['edgecolor'].values)
186
+ self.segment_candle = segment_candle.reshape(segment_candle.shape[0], 12, 2)
143
187
 
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
269
+
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
+ self.collection_candle.set_segments(self.segment_candle[index_start:index_end])
280
+ self.collection_candle.set_facecolor(self.facecolor_candle[index_start:index_end])
281
+ self.collection_candle.set_edgecolor(self.edgecolor_candle[index_start:index_end])
282
+
283
+ if self.volume:
284
+ self.collection_volume.set_segments(self.segment_volume[index_start:index_end])
285
+ self.collection_volume.set_linewidth(0.7)
286
+ self.collection_volume.set_facecolor(self.facecolor_volume[index_start:index_end])
287
+ self.collection_volume.set_edgecolor(self.edgecolor_volume[index_start:index_end])
288
+
289
+ self.collection_ma.set_segments(self.segment_ma[:, index_start:index_end])
290
+ self.collection_ma.set_edgecolor(list(reversed(self.edgecolor_ma)))
291
+ return
292
+
293
+ def _set_wick_segments(self, index_start, index_end, simpler=False):
294
+ self.collection_candle.set_segments(self.segment_candle_wick[index_start:index_end])
295
+ self.collection_candle.set_facecolor([])
296
+ self.collection_candle.set_edgecolor(self.edgecolor_candle[index_start:index_end])
297
+
298
+ if self.volume:
299
+ seg = self.segment_volume_wick[index_start:index_end]
300
+ if simpler:
301
+ values = seg[:, 1, 1]
302
+ top_index = np.argsort(-values)[:self.limit_volume]
303
+ seg = seg[top_index]
304
+ self.collection_volume.set_segments(seg)
305
+ self.collection_volume.set_linewidth(1.3)
306
+ self.collection_volume.set_facecolor(self.facecolor_volume[index_start:index_end])
307
+ self.collection_volume.set_edgecolor(self.facecolor_volume[index_start:index_end])
308
+
309
+ self.collection_ma.set_segments(self.segment_ma[:, index_start:index_end])
310
+ self.collection_ma.set_edgecolor(list(reversed(self.edgecolor_ma)))
311
+ return
312
+
313
+ def _set_line_segments(self, index_start, index_end, simpler=False, set_ma=True):
314
+ self.collection_candle.set_segments(self.segment_priceline[:, index_start:index_end])
315
+ self.collection_candle.set_facecolor([])
316
+ self.collection_candle.set_edgecolor(self.color_priceline)
317
+
318
+ if self.volume:
319
+ seg = self.segment_volume_wick[index_start:index_end]
320
+ if simpler:
321
+ values = seg[:, 1, 1]
322
+ top_index = np.argsort(-values)[:self.limit_volume]
323
+ seg = seg[top_index]
324
+ self.collection_volume.set_segments(seg)
325
+ self.collection_volume.set_linewidth(1.3)
326
+ self.collection_volume.set_facecolor(self.facecolor_volume[index_start:index_end])
327
+ self.collection_volume.set_edgecolor(self.facecolor_volume[index_start:index_end])
328
+
329
+ if not set_ma: self.collection_ma.set_segments([])
330
+ else:
331
+ self.collection_ma.set_segments(self.segment_ma[:, index_start:index_end])
332
+ self.collection_ma.set_edgecolor(list(reversed(self.edgecolor_ma)))
333
+ return
334
+
335
+
336
+ class EventMixin(SegmentMixin):
337
+ def __init__(self, *args, **kwargs):
338
+ super().__init__(*args, **kwargs)
201
339
 
202
- for i in legends.legend_handles:
203
- i.set_picker(5)
340
+ self._connect_event()
341
+ return
342
+
343
+ def _connect_event(self):
344
+ self.figure.canvas.mpl_connect('pick_event', lambda x: self._on_pick(x))
204
345
  return
205
346
 
206
347
  def _on_pick(self, e):
207
348
  self._pick_ma_action(e)
208
-
209
- return self._draw()
349
+ return
210
350
 
211
351
  def _pick_ma_action(self, e: PickEvent):
212
352
  handle = e.artist
213
- if e.artist.get_alpha() == 0.2:
353
+ if handle.get_alpha() == 0.2:
214
354
  visible = True
215
355
  handle.set_alpha(1.0)
216
356
  else:
@@ -218,130 +358,154 @@ class CollectionMixin(DataMixin):
218
358
  handle.set_alpha(0.2)
219
359
 
220
360
  n = int(handle.get_label())
221
-
222
361
  if visible: self._visible_ma = {i for i in self.list_ma if i in self._visible_ma or i == n}
223
362
  else: self._visible_ma = {i for i in self._visible_ma if i != n}
224
363
 
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)
364
+ alphas = [(1 if i in self._visible_ma else 0) for i in reversed(self.list_ma)]
365
+ self.collection_ma.set_alpha(alphas)
366
+
367
+ self._draw()
228
368
  return
229
369
 
230
370
  def _draw(self):
231
- if self.fig.canvas is not self.canvas:
232
- self.canvas = self.fig.canvas
233
- self.canvas.draw()
371
+ self.figure.canvas.draw()
234
372
  return
235
373
 
236
374
 
237
- class BackgroundMixin(CollectionMixin):
238
- background = None
375
+ class DrawMixin(EventMixin):
239
376
  candle_on_ma = True
240
377
 
241
- _creating_background = False
378
+ def set_data(self, df, sort_df=True, calc_ma=True, set_candlecolor=True, set_volumecolor=True, *args, **kwargs):
379
+ self._generate_data(df, sort_df, calc_ma, set_candlecolor, set_volumecolor, *args, **kwargs)
380
+ self._get_segments()
242
381
 
243
- def __init__(self, *args, **kwargs):
244
- super().__init__(*args, **kwargs)
382
+ vmin, vmax = self.get_default_lim()
383
+ self._set_lim(vmin, vmax)
245
384
  return
246
385
 
247
386
  def _connect_event(self):
248
387
  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
388
+ self.figure.canvas.mpl_connect('draw_event', lambda x: self._on_draw(x))
261
389
  return
262
390
 
263
- def _copy_bbox(self):
391
+ def _on_draw(self, e):
264
392
  self._draw_artist()
265
- self.background = self.canvas.renderer.copy_from_bbox(self.fig.bbox)
393
+ self._blit()
266
394
  return
267
395
 
268
396
  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)
397
+ renderer = self.figure.canvas.renderer
275
398
 
276
399
  self.ax_price.xaxis.draw(renderer)
277
400
  self.ax_price.yaxis.draw(renderer)
278
401
 
279
402
  if self.candle_on_ma:
280
- self.macollection.draw(renderer)
281
- self.candlecollection.draw(renderer)
403
+ self.collection_ma.draw(renderer)
404
+ self.collection_candle.draw(renderer)
282
405
  else:
283
- self.candlecollection.draw(renderer)
284
- self.macollection.draw(renderer)
406
+ self.collection_candle.draw(renderer)
407
+ self.collection_ma.draw(renderer)
408
+
409
+ if self.watermark:
410
+ self.text_watermark.set_text(self.watermark)
411
+ self.text_watermark.draw(renderer)
285
412
 
286
413
  self.ax_volume.xaxis.draw(renderer)
287
414
  self.ax_volume.yaxis.draw(renderer)
288
415
 
289
- self.volumecollection.draw(renderer)
416
+ self.collection_volume.draw(renderer)
290
417
  return
291
418
 
292
- def _on_draw(self, e):
293
- self.background = None
294
- self._restore_region()
419
+ def _blit(self):
420
+ self.figure.canvas.blit()
295
421
  return
296
422
 
297
- def _restore_region(self):
298
- if not self.background: self._create_background()
423
+ def _set_lim(self, xmin, xmax, simpler=False, set_ma=True):
424
+ self.vxmin, self.vxmax = (xmin, xmax + 1)
425
+ if xmin < 0: xmin = 0
426
+ if xmax < 0: xmax = 0
427
+ if xmin == xmax: xmax += 1
428
+
429
+ ymin, ymax = (self.df[self.low][xmin:xmax].min(), self.df[self.high][xmin:xmax].max())
430
+ yspace = (ymax - ymin) / 15
431
+ # 주가 차트 ymin, ymax
432
+ self.price_ymin, self.price_ymax = (ymin-yspace, ymax+yspace)
433
+
434
+ # 거래량 차트 ymax
435
+ self.volume_ymax = self.df['_volymax'][xmin:xmax].max() if self.volume else 1
436
+
437
+ self._set_segments(xmin, xmax, simpler, set_ma)
438
+ self._change_lim(self.vxmin, self.vxmax)
439
+ return
440
+
441
+ def _change_lim(self, xmin, xmax):
442
+ # 주가 차트 xlim
443
+ self.ax_price.set_xlim(xmin, xmax)
444
+ # 거래량 차트 xlim
445
+ self.ax_volume.set_xlim(xmin, xmax)
299
446
 
300
- self.canvas.renderer.restore_region(self.background)
447
+ # 주가 차트 ylim
448
+ self.ax_price.set_ylim(self.price_ymin, self.price_ymax)
449
+ # 거래량 차트 ylim
450
+ self.ax_volume.set_ylim(0, self.volume_ymax)
301
451
  return
302
452
 
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
453
+ def get_default_lim(self):
454
+ return (0, self.list_index[-1])
455
+
456
+
457
+ class BackgroundMixin(DrawMixin):
458
+ background = None
307
459
 
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)
460
+ _creating_background = False
461
+
462
+ def _connect_event(self):
463
+ self.figure.canvas.mpl_connect('pick_event', lambda x: self._on_pick(x))
464
+ self.figure.canvas.mpl_connect('draw_event', lambda x: self._on_draw(x))
312
465
  return
313
466
 
314
- def _draw_collection(self, change_lim=True):
315
- xmax = self.df['x'].values[-1] + 1
467
+ def _create_background(self):
468
+ if self._creating_background: return
316
469
 
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)
470
+ self._creating_background = True
471
+ self._copy_bbox()
472
+ self._creating_background = False
473
+ return
326
474
 
327
- ymin, ymax = (self.df[self.low].min(), self.df[self.high].max())
328
- ysub = (ymax - ymin) / 15
475
+ def _copy_bbox(self):
476
+ self._draw_artist()
477
+ self.background = self.figure.canvas.renderer.copy_from_bbox(self.figure.bbox)
478
+ return
329
479
 
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)
480
+ def _on_draw(self, e):
481
+ self.background = None
482
+ self._restore_region()
483
+ return
333
484
 
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)
485
+ def _restore_region(self):
486
+ if not self.background: self._create_background()
337
487
 
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)
488
+ self.figure.canvas.renderer.restore_region(self.background)
341
489
  return
342
490
 
343
491
 
344
- class Chart(DrawMixin, Mixin):
492
+ class BaseMixin(BackgroundMixin):
493
+ pass
494
+
495
+
496
+ class Chart(BaseMixin, Mixin):
497
+ def _add_collection(self):
498
+ super()._add_collection()
499
+ return self.add_collection()
500
+
501
+ def _draw_artist(self):
502
+ super()._draw_artist()
503
+ return self.draw_artist()
504
+
505
+ def _get_segments(self):
506
+ self.generate_data()
507
+ return super()._get_segments()
508
+
345
509
  def _on_draw(self, e):
346
510
  super()._on_draw(e)
347
511
  return self.on_draw(e)
@@ -350,22 +514,3 @@ class Chart(DrawMixin, Mixin):
350
514
  self.on_pick(e)
351
515
  return super()._on_pick(e)
352
516
 
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()