seolpyo-mplchart 0.1.3.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.
@@ -0,0 +1,371 @@
1
+ from matplotlib.backend_bases import PickEvent
2
+ from matplotlib.collections import LineCollection
3
+ from matplotlib.lines import Line2D
4
+ import numpy as np
5
+ import pandas as pd
6
+
7
+
8
+ from .base import Base
9
+
10
+
11
+ class Mixin:
12
+ def on_draw(self, e):
13
+ "This function works if draw event active."
14
+ return
15
+ def on_pick(self, e):
16
+ "This function works if pick event active."
17
+ return
18
+
19
+
20
+ _set_key = {'zero', 'x', 'left', 'right', 'top', 'bottom',}
21
+
22
+ class DataMixin(Base):
23
+ df: pd.DataFrame
24
+ date = 'date'
25
+ Open, high, low, close = ('open', 'high', 'low', 'close')
26
+ volume = 'volume'
27
+
28
+ _visible_ma = set()
29
+ label_ma = '{}일선'
30
+ 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
+
34
+ color_up, color_down = ('#fe3032', '#0095ff')
35
+ color_flat = 'k'
36
+ color_up_down, color_down_up = ('w', 'w')
37
+ colors_volume = '#1f77b4'
38
+
39
+ candlewidth_half, volumewidth_half = (0.3, 0.36)
40
+
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}')
49
+
50
+ # DataFrame 정렬
51
+ if sort_df:
52
+ df = df.sort_values([self.date]).reset_index()
53
+
54
+ 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
+ else:
58
+ 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
88
+
89
+ self.df = df
90
+ return
91
+
92
+ class CollectionMixin(DataMixin):
93
+ color_sliderline = 'k'
94
+
95
+ _masegment, _macolors = ({}, {})
96
+
97
+ def __init__(self, *args, **kwargs):
98
+ super().__init__(*args, **kwargs)
99
+
100
+ self._connect_event()
101
+ self._add_collection()
102
+ return
103
+
104
+ def _connect_event(self):
105
+ self.canvas.mpl_connect('pick_event', lambda x: self._on_pick(x))
106
+ return
107
+
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',
137
+ ]].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)
143
+
144
+ volseg = self.df[[
145
+ 'left', 'zero',
146
+ 'left', self.volume,
147
+ 'right', self.volume,
148
+ 'right', 'zero',
149
+ ]].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)
174
+ return
175
+
176
+ def _set_macollection(self):
177
+ # 기존 legend 제거
178
+ legends = self.ax_legend.get_legend()
179
+ if legends: legends.remove()
180
+
181
+ self._masegment.clear(), self._macolors.clear()
182
+ handles, labels = ([], [])
183
+ self._visible_ma.clear()
184
+ for n, i in enumerate(self.list_ma):
185
+ 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
192
+
193
+ handles.append(Line2D([0, 1], [0, 1], color=c, linewidth=5, label=i))
194
+ labels.append(self.label_ma.format(i))
195
+
196
+ self._visible_ma.add(i)
197
+
198
+ # 가격이동평균선 legend 생성
199
+ if handles:
200
+ legends = self.ax_legend.legend(handles, labels, loc='lower left', ncol=10)
201
+
202
+ for i in legends.legend_handles:
203
+ i.set_picker(5)
204
+ return
205
+
206
+ def _on_pick(self, e):
207
+ self._pick_ma_action(e)
208
+
209
+ return self._draw()
210
+
211
+ def _pick_ma_action(self, e: PickEvent):
212
+ handle = e.artist
213
+ if e.artist.get_alpha() == 0.2:
214
+ visible = True
215
+ handle.set_alpha(1.0)
216
+ else:
217
+ visible = False
218
+ handle.set_alpha(0.2)
219
+
220
+ n = int(handle.get_label())
221
+
222
+ if visible: self._visible_ma = {i for i in self.list_ma if i in self._visible_ma or i == n}
223
+ else: self._visible_ma = {i for i in self._visible_ma if i != n}
224
+
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)
228
+ return
229
+
230
+ def _draw(self):
231
+ if self.fig.canvas is not self.canvas:
232
+ self.canvas = self.fig.canvas
233
+ self.canvas.draw()
234
+ return
235
+
236
+
237
+ class BackgroundMixin(CollectionMixin):
238
+ background = None
239
+ candle_on_ma = True
240
+
241
+ _creating_background = False
242
+
243
+ def __init__(self, *args, **kwargs):
244
+ super().__init__(*args, **kwargs)
245
+ return
246
+
247
+ def _connect_event(self):
248
+ 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
261
+ return
262
+
263
+ def _copy_bbox(self):
264
+ self._draw_artist()
265
+ self.background = self.canvas.renderer.copy_from_bbox(self.fig.bbox)
266
+ return
267
+
268
+ 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)
275
+
276
+ self.ax_price.xaxis.draw(renderer)
277
+ self.ax_price.yaxis.draw(renderer)
278
+
279
+ if self.candle_on_ma:
280
+ self.macollection.draw(renderer)
281
+ self.candlecollection.draw(renderer)
282
+ else:
283
+ self.candlecollection.draw(renderer)
284
+ self.macollection.draw(renderer)
285
+
286
+ self.ax_volume.xaxis.draw(renderer)
287
+ self.ax_volume.yaxis.draw(renderer)
288
+
289
+ self.volumecollection.draw(renderer)
290
+ return
291
+
292
+ def _on_draw(self, e):
293
+ self.background = None
294
+ self._restore_region()
295
+ return
296
+
297
+ def _restore_region(self):
298
+ if not self.background: self._create_background()
299
+
300
+ self.canvas.renderer.restore_region(self.background)
301
+ return
302
+
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
307
+
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)
312
+ return
313
+
314
+ def _draw_collection(self, change_lim=True):
315
+ xmax = self.df['x'].values[-1] + 1
316
+
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)
326
+
327
+ ymin, ymax = (self.df[self.low].min(), self.df[self.high].max())
328
+ ysub = (ymax - ymin) / 15
329
+
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)
333
+
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)
337
+
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)
341
+ return
342
+
343
+
344
+ class Chart(DrawMixin, Mixin):
345
+ def _on_draw(self, e):
346
+ super()._on_draw(e)
347
+ return self.on_draw(e)
348
+
349
+ def _on_pick(self, e):
350
+ self.on_pick(e)
351
+ return super()._on_pick(e)
352
+
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()