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.

@@ -1,504 +1,542 @@
1
+ import matplotlib.pyplot as plt
2
+ from matplotlib.axes import Axes
1
3
  from matplotlib.collections import LineCollection
4
+ from matplotlib.text import Text
2
5
  from matplotlib.backend_bases import MouseEvent, MouseButton, cursors
3
6
  import pandas as pd
4
7
 
8
+ from .base import convert_unit
9
+ from .cursor import BaseMixin as BM, Mixin as M
5
10
 
6
- from .cursor import CursorMixin, Chart as CM
7
11
 
8
-
9
- class Mixin:
12
+ class Mixin(M):
10
13
  def on_click(self, e):
11
14
  "This function works if mouse button click event active."
12
15
  return
13
16
  def on_release(self, e):
14
17
  "This function works if mouse button release event active."
15
18
  return
16
- def draw_artist(self):
17
- "This function works before canvas.blit()."
19
+
20
+
21
+ class PlotMixin(BM):
22
+ slider_top = True
23
+ ratio_ax_slider = 3
24
+ ratio_ax_none = 2
25
+
26
+ def _get_plot(self):
27
+ if self.slider_top:
28
+ self.figure, axes = plt.subplots(
29
+ 4, # row 수
30
+ figsize=self.figsize, # 기본 크기
31
+ height_ratios=(self.ratio_ax_slider, self.ratio_ax_legend, self.ratio_ax_price, self.ratio_ax_volume) # row 크기 비율
32
+ )
33
+ axes: list[Axes]
34
+ self.ax_slider, self.ax_legend, self.ax_price, self.ax_volume = axes
35
+ else:
36
+ self.figure, axes = plt.subplots(
37
+ 5, # row 수
38
+ figsize=self.figsize, # 기본 크기
39
+ height_ratios=(self.ratio_ax_legend, self.ratio_ax_price, self.ratio_ax_volume, self.ratio_ax_none, self.ratio_ax_slider) # row 크기 비율
40
+ )
41
+ axes: list[Axes]
42
+ self.ax_legend, self.ax_price, self.ax_volume, ax_none, self.ax_slider = axes
43
+
44
+ ax_none.set_axis_off()
45
+ ax_none.xaxis.set_animated(True)
46
+ ax_none.yaxis.set_animated(True)
47
+
48
+ self.ax_slider.set_label('slider ax')
49
+ self.ax_legend.set_label('legend ax')
50
+ self.ax_price.set_label('price ax')
51
+ self.ax_volume.set_label('volume ax')
52
+
53
+ self.figure.canvas.manager.set_window_title(f'{self.title}')
54
+ self.figure.set_facecolor(self.color_background)
55
+
56
+ # 플롯간 간격 제거(Configure subplots)
57
+ self.figure.subplots_adjust(**self.adjust)
58
+
59
+ self.ax_legend.set_axis_off()
60
+
61
+ # y ticklabel foramt 설정
62
+ self.ax_slider.yaxis.set_major_formatter(lambda x, _: convert_unit(x, word=self.unit_price, digit=2))
63
+ self.ax_price.yaxis.set_major_formatter(lambda x, _: convert_unit(x, word=self.unit_price, digit=2))
64
+ self.ax_volume.yaxis.set_major_formatter(lambda x, _: convert_unit(x, word=self.unit_volume, digit=2))
65
+
66
+ gridKwargs = {'visible': True, 'linewidth': 1, 'color': '#d0d0d0', 'linestyle': '--'}
67
+ gridKwargs.update(self.gridKwargs)
68
+ # 공통 설정
69
+ for ax in (self.ax_slider, self.ax_price, self.ax_volume):
70
+ ax.xaxis.set_animated(True)
71
+ ax.yaxis.set_animated(True)
72
+
73
+ # x tick 외부 눈금 표시하지 않기
74
+ ax.xaxis.set_ticks_position('none')
75
+ # x tick label 제거
76
+ ax.set_xticklabels([])
77
+ # y tick 우측으로 이동
78
+ ax.tick_params(left=False, right=True, labelleft=False, labelright=True, colors=self.color_tick_label)
79
+ # Axes 외곽선 색 변경
80
+ for i in ['top', 'bottom', 'left', 'right']: ax.spines[i].set_color(self.color_tick)
81
+
82
+ # 차트 영역 배경 색상
83
+ ax.set_facecolor(self.color_background)
84
+
85
+ # grid(구분선, 격자) 그리기
86
+ # 어째서인지 grid의 zorder 값을 선언해도 1.6을 값으로 한다.
87
+ ax.grid(**gridKwargs)
18
88
  return
19
89
 
20
90
 
21
- class NavgatorMixin(CursorMixin):
91
+ class CollectionMixin(PlotMixin):
22
92
  min_distance = 30
23
- color_navigatorline = '#1e78ff'
24
- color_navigator = 'k'
25
-
26
- _x_click, _x_release = (0, 0)
27
- is_click, is_move = (False, False)
28
- _navcoordinate = (0, 0)
93
+ color_navigator_line = '#1e78ff'
94
+ color_navigator_cover = 'k'
29
95
 
30
96
  def _add_collection(self):
31
97
  super()._add_collection()
32
98
 
99
+ self.collection_slider = LineCollection([], animated=True)
100
+ self.ax_slider.add_artist(self.collection_slider)
101
+
33
102
  # 슬라이더 네비게이터
34
- self.navigator = LineCollection([], animated=True, edgecolors=[self.color_navigator, self.color_navigatorline], alpha=(0.2, 1.0))
103
+ self.navigator = LineCollection([], animated=True, edgecolors=[self.color_navigator_cover, self.color_navigator_line], alpha=(0.2, 1.0))
35
104
  self.ax_slider.add_artist(self.navigator)
36
- return
37
105
 
38
- def _set_data(self, df: pd.DataFrame, sort_df=True, calc_ma=True, change_lim=True, calc_info=True):
39
- super()._set_data(df, sort_df, calc_ma, change_lim, calc_info)
106
+ lineKwargs = {'edgecolor': 'k', 'linewidth': 1, 'linestyle': '-'}
107
+ lineKwargs.update(self.lineKwargs)
108
+ lineKwargs.update({'segments': [], 'animated': True})
40
109
 
41
- # 네비게이터 라인 선택 영역
42
- xsub = self.xmax - self.xmin
43
- self._navLineWidth = xsub * 0.008
44
- if self._navLineWidth < 1: self._navLineWidth = 1
45
- self._navLineWidth_half = self._navLineWidth / 2
46
- return
110
+ self.slider_vline = LineCollection(**lineKwargs)
111
+ self.ax_slider.add_artist(self.slider_vline)
47
112
 
48
- def _connect_event(self):
49
- super()._connect_event()
50
- self.canvas.mpl_connect('axes_leave_event', lambda x: self._leave_axes(x))
51
- self.canvas.mpl_connect('button_press_event', lambda x: self._on_click(x))
52
- self.canvas.mpl_connect('button_release_event', lambda x: self._on_release(x))
53
- return
113
+ textboxKwargs = {'boxstyle': 'round', 'facecolor': 'w'}
114
+ textboxKwargs.update(self.textboxKwargs)
115
+ textKwargs = self.textKwargs
116
+ textKwargs.update({'animated': True, 'bbox': textboxKwargs, 'horizontalalignment': '', 'verticalalignment': ''})
117
+ (textKwargs.pop('horizontalalignment'), textKwargs.pop('verticalalignment'))
54
118
 
55
- def _leave_axes(self, e: MouseEvent):
56
- if not self.is_click and e.inaxes is self.ax_slider:
57
- self.canvas.set_cursor(cursors.POINTER)
119
+ self.text_slider = Text(**textKwargs, horizontalalignment='center', verticalalignment='top')
120
+ self.ax_slider.add_artist(self.text_slider)
58
121
  return
59
122
 
60
- def _on_click(self, e: MouseEvent):
61
- if self.is_click or e.button != MouseButton.LEFT or e.inaxes is not self.ax_slider: return
123
+ def _get_segments(self):
124
+ super()._get_segments()
62
125
 
63
- self.is_click = True
126
+ keys = []
127
+ for i in reversed(self.list_ma):
128
+ keys.append('_x')
129
+ keys.append(f'ma{i}')
64
130
 
65
- x = e.xdata.__int__()
66
- left, right = self._navcoordinate
67
- lmin, lmax = (left-self._navLineWidth, left+self._navLineWidth_half)
68
- rmin, rmax = (right-self._navLineWidth_half, right+self._navLineWidth)
69
-
70
- gtl, ltr = (lmax < x, x < rmin)
71
- if gtl and ltr:
72
- self._x_click = x
73
- self.is_move = True
74
- self.canvas.set_cursor(cursors.MOVE)
75
- else:
76
- self.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
77
- if not gtl and lmin <= x:
78
- self._x_click = right
79
- elif not ltr and x <= rmax:
80
- self._x_click = left
81
- else:
82
- self._x_click = x
83
-
84
- # 그리기 후 최초 클릭이면 좌표 수정
85
- if left == right:
86
- self._navcoordinate = (x, x)
131
+ segment_slider = self.df[keys + ['_x', self.close] ].values
132
+ segment_slider = segment_slider.reshape(segment_slider.shape[0], len(self.list_ma)+1, 2).swapaxes(0, 1)
133
+ self.collection_slider.set_segments(segment_slider)
134
+ self.collection_slider.set_edgecolor(self.edgecolor_ma + [self.color_priceline])
87
135
  return
88
136
 
89
- def _on_release(self, e: MouseEvent):
90
- if e.inaxes is not self.ax_slider: return
91
- self.is_click, self.is_move = (False, False)
92
137
 
93
- if self._navcoordinate[0] == self._navcoordinate[1]:
94
- self._navcoordinate = (self._navcoordinate[0], self._navcoordinate[1]+self.min_distance)
95
- return
138
+ class NavigatorMixin(CollectionMixin):
139
+ def _set_slider_lim(self):
140
+ xmax = self.list_index[-1]
141
+ # 슬라이더 xlim
142
+ xdistance = xmax / 30
143
+ self.slider_xmin, self.slider_xmax = (-xdistance, xmax + xdistance)
144
+ self.ax_slider.set_xlim(self.slider_xmin, self.slider_xmax)
96
145
 
146
+ # 슬라이더 ylim
147
+ ymin, ymax = (self.df[self.low].min(), self.df[self.high].max())
148
+ ysub = ymax - ymin
149
+ self.sldier_ymiddle = ymin + (ysub / 2)
150
+ ydistance = ysub / 5
151
+ self.slider_ymin, self.slider_ymax = (ymin-ydistance, ymax+ydistance)
152
+ self.ax_slider.set_ylim(self.slider_ymin, self.slider_ymax)
97
153
 
98
- class BackgroundMixin(NavgatorMixin):
99
- def _on_draw(self, e):
100
- self.background = None
101
- self._restore_region()
102
- return
154
+ # 슬라이더 텍스트 y
155
+ self.text_slider.set_y(ymax)
103
156
 
104
- def _restore_region(self, with_nav=True, empty=False, empty_with_nav=False):
105
- if not self.background: self._create_background()
157
+ self.navigator.set_linewidth([ysub, 5])
106
158
 
107
- if empty: self.canvas.restore_region(self.background_empty)
108
- elif empty_with_nav: self.canvas.restore_region(self.background_empty_with_nav)
109
- elif with_nav: self.canvas.restore_region(self.background_with_nav)
110
- else: self.canvas.renderer.restore_region(self.background)
159
+ # 네비게이터 라인 선택 범위
160
+ xsub = self.slider_xmax - self.slider_xmin
161
+ self._navLineWidth = xsub * 8 / 1_000
162
+ if self._navLineWidth < 1: self._navLineWidth = 1
163
+ self._navLineWidth_half = self._navLineWidth / 2
111
164
  return
112
165
 
113
- def _copy_bbox(self):
114
- renderer = self.canvas.renderer
166
+ def _set_navigator(self, navmin, navmax):
167
+ navseg = [
168
+ (
169
+ (self.slider_xmin, self.sldier_ymiddle),
170
+ (navmin, self.sldier_ymiddle)
171
+ ),
172
+ (
173
+ (navmin, self.slider_ymin),
174
+ (navmin, self.slider_ymax)
175
+ ),
176
+ (
177
+ (navmax, self.sldier_ymiddle),
178
+ (self.slider_xmax, self.sldier_ymiddle)
179
+ ),
180
+ (
181
+ (navmax, self.slider_ymin),
182
+ (navmax, self.slider_ymax)
183
+ ),
184
+ ]
185
+
186
+ self.navigator.set_segments(navseg)
187
+ return
115
188
 
116
- self.background_empty = renderer.copy_from_bbox(self.fig.bbox)
117
189
 
118
- self.ax_slider.xaxis.draw(renderer)
119
- self.ax_slider.yaxis.draw(renderer)
120
- self.slidercollection.draw(renderer)
121
- self.background_empty_with_nav = self.canvas.renderer.copy_from_bbox(self.fig.bbox)
190
+ class DataMixin(NavigatorMixin):
191
+ navcoordinate = (0, 0)
122
192
 
123
- self._draw_artist()
124
- self.background = self.canvas.renderer.copy_from_bbox(self.fig.bbox)
193
+ def set_data(self, df, sort_df=True, calc_ma=True, set_candlecolor=True, set_volumecolor=True, calc_info=True, change_lim=True, *args, **kwargs):
194
+ self._generate_data(df, sort_df, calc_ma, set_candlecolor, set_volumecolor, calc_info, *args, **kwargs)
195
+ self._get_segments()
125
196
 
126
- self.navigator.draw(self.canvas.renderer)
127
- self.background_with_nav = self.canvas.renderer.copy_from_bbox(self.fig.bbox)
128
- return
197
+ vmin, vmax = self.navcoordinate
198
+ if change_lim or (vmax-vmin) < self.min_distance:
199
+ vmin, vmax = self.get_default_lim()
200
+ self.navcoordinate = (vmin, vmax)
129
201
 
130
- def _draw_artist(self):
131
- renderer = self.canvas.renderer
202
+ self._set_lim(vmin, vmax)
203
+ self._set_slider_lim()
204
+ self._set_navigator(vmin, vmax)
132
205
 
133
- self.ax_price.xaxis.draw(renderer)
134
- self.ax_price.yaxis.draw(renderer)
206
+ self._length_text = self.df[(self.volume if self.volume else self.high)].apply(lambda x: len(str(x))).max()
207
+ return
135
208
 
136
- if self.candle_on_ma:
137
- self.macollection.draw(renderer)
138
- self.candlecollection.draw(renderer)
139
- else:
140
- self.candlecollection.draw(renderer)
141
- self.macollection.draw(renderer)
209
+ def get_default_lim(self):
210
+ xmax = self.list_index[-1]
211
+ return (xmax-120, xmax)
142
212
 
143
- self.ax_volume.xaxis.draw(renderer)
144
- self.ax_volume.yaxis.draw(renderer)
145
213
 
146
- self.volumecollection.draw(renderer)
147
- return
214
+ class BackgroundMixin(DataMixin):
215
+ def _copy_bbox(self):
216
+ renderer = self.figure.canvas.renderer
148
217
 
218
+ self.ax_slider.xaxis.draw(renderer)
219
+ self.ax_slider.yaxis.draw(renderer)
220
+ self.collection_slider.draw(renderer)
221
+ self.background_emtpy = renderer.copy_from_bbox(self.figure.bbox)
149
222
 
150
- class DrawMixin(BackgroundMixin):
151
- def _set_data(self, df: pd.DataFrame, sort_df=True, calc_ma=True, change_lim=True, calc_info=True):
152
- super()._set_data(df, sort_df, calc_ma, change_lim, calc_info)
223
+ self._draw_artist()
224
+ self.background = renderer.copy_from_bbox(self.figure.bbox)
153
225
 
154
- # 네비게이터 높이 설정
155
- ysub = self._slider_ymax - self._slider_ymin
156
- self._ymiddle = self._slider_ymax - ysub / 2
157
- self.navigator.set_linewidth((ysub, 5))
226
+ self.navigator.draw(self.figure.canvas.renderer)
227
+ self.background_with_nav = renderer.copy_from_bbox(self.figure.bbox)
158
228
  return
159
229
 
160
- def _on_release(self, e: MouseEvent):
161
- super()._on_release(e)
162
- self._set_navigator(*self._navcoordinate)
230
+ def _restore_region(self, is_empty=False, with_nav=True):
231
+ if not self.background: self._create_background()
163
232
 
164
- self._restore_region(empty=True)
165
- self._creating_background = False
166
- self._create_background()
167
- self._restore_region()
168
- self._blit()
233
+ if is_empty: self.figure.canvas.renderer.restore_region(self.background_emtpy)
234
+ elif with_nav: self.figure.canvas.renderer.restore_region(self.background_with_nav)
235
+ else: self.figure.canvas.renderer.restore_region(self.background)
169
236
  return
170
237
 
171
- def _on_move(self, e: MouseEvent):
172
- self._restore_region((not self.is_click))
173
238
 
239
+ class MouseMoveMixin(BackgroundMixin):
240
+ in_slider = False
241
+ is_click_slider = False
242
+
243
+ def _on_move(self, e):
174
244
  self._on_move_action(e)
175
245
 
246
+ self._restore_region((self.is_click_slider and self.in_slider))
247
+
176
248
  if self.in_slider:
177
- self._change_coordinate()
178
- if self.is_click:
179
- if self.is_move: self._set_navigator(*self._navcoordinate)
180
- elif self.intx is not None: self._set_navigator(self._x_click, self.intx)
181
- self.navigator.draw(self.canvas.renderer)
182
- self._slider_move_action(e)
183
- elif self.is_click:
184
- self.navigator.draw(self.canvas.renderer)
185
- else:
186
- if self.in_slider or self.in_price or self.in_volume:
187
- self._slider_move_action(e)
188
- if self.in_price or self.in_volume:
189
- self._chart_move_action(e)
249
+ self._on_move_slider(e)
250
+ elif self.in_price_chart:
251
+ self._on_move_price_chart(e)
252
+ elif self.in_volume_chart:
253
+ self._on_move_volume_chart(e)
190
254
 
191
255
  self._blit()
192
256
  return
193
257
 
194
- def _change_coordinate(self):
195
- if self.intx is None: return
196
- x = self.intx
197
- left, right = self._navcoordinate
198
-
199
- if not self.is_click:
200
- lmin, lmax = (left-self._navLineWidth, left+self._navLineWidth_half)
201
- rmin, rmax = (right-self._navLineWidth_half, right+self._navLineWidth)
202
- ltel, gter = (x <= lmax, rmin <= x)
203
- if ltel and lmin <= x:
204
- self.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
205
- elif gter and x <= rmax:
206
- self.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
207
- elif not ltel and not gter: self.canvas.set_cursor(cursors.MOVE)
208
- else: self.canvas.set_cursor(cursors.POINTER)
209
- else:
210
- # 네비게이터 좌표 수정
211
- intx = x.__int__()
212
- if self.is_move:
213
- xsub = self._x_click - intx
214
- left, right = (left-xsub, right-xsub)
215
- self._x_click = intx
216
- else:
217
- if intx == left: left = intx
218
- elif intx == right: right = intx
219
- else:
220
- if self._x_click < intx: left, right = (self._x_click, intx)
221
- else: left, right = (intx, self._x_click)
222
-
223
- nsub = right - left
224
- if right < 0 or self.df.index[-1] < left or nsub < self.min_distance: left, right = self._navcoordinate
225
- self._navcoordinate = (left, right)
226
- return
258
+ def _on_move_action(self, e: MouseEvent):
259
+ self._check_ax(e)
227
260
 
228
- def _set_navigator(self, x1, x2):
229
- xmin, xmax = (x1, x2) if x1 < x2 else (x2, x1)
261
+ self.intx = None
262
+ if self.in_slider or self.in_price_chart or self.in_volume_chart: self._get_x(e)
230
263
 
231
- left = ((self.xmin, self._ymiddle), (xmin, self._ymiddle))
232
- right = ((xmax, self._ymiddle), (self.xmax, self._ymiddle))
233
- leftline = ((xmin, self._slider_ymin), (xmin, self._slider_ymax))
234
- rightline = ((xmax, self._slider_ymin), (xmax, self._slider_ymax))
235
- self.navigator.set_segments((left, leftline, right, rightline))
264
+ self._change_cursor(e)
236
265
  return
237
266
 
267
+ def _change_cursor(self, e: MouseEvent):
268
+ # 마우스 커서 변경
269
+ if self.is_click_slider: return
270
+ elif not self.in_slider:
271
+ self.figure.canvas.set_cursor(cursors.POINTER)
272
+ return
273
+
274
+ navleft, navright = self.navcoordinate
275
+ if navleft == navright: return
276
+
277
+ x = e.xdata
278
+ leftmin, leftmax = (navleft-self._navLineWidth, navleft+self._navLineWidth_half)
279
+ rightmin, rightmax = (navright-self._navLineWidth_half, navright+self._navLineWidth)
280
+ if x < leftmin: self.figure.canvas.set_cursor(cursors.POINTER)
281
+ elif x < leftmax: self.figure.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
282
+ elif x < rightmin: self.figure.canvas.set_cursor(cursors.MOVE)
283
+ elif x < rightmax: self.figure.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
284
+ else: self.figure.canvas.set_cursor(cursors.POINTER)
285
+ return
238
286
 
239
- class LimMixin(DrawMixin):
240
- def _on_release(self, e: MouseEvent):
241
- if e.inaxes is not self.ax_slider: return
242
- self.is_click, self.is_move = (False, False)
287
+ def _check_ax(self, e: MouseEvent):
288
+ ax = e.inaxes
289
+ if not ax or e.xdata is None or e.ydata is None:
290
+ self.in_slider, self.in_price_chart, self.in_volume_chart = (False, False, False)
291
+ else:
292
+ self.in_slider = ax is self.ax_slider
293
+ self.in_price_chart = False if self.in_slider else ax is self.ax_price
294
+ self.in_volume_chart = False if (self.in_slider or self.in_price_chart) else ax is self.ax_volume
295
+ return
243
296
 
244
- if self._navcoordinate[0] == self._navcoordinate[1]:
245
- self._navcoordinate = (self._navcoordinate[0], self._navcoordinate[1]+self.min_distance)
246
- self._set_navigator(*self._navcoordinate)
247
- self._lim()
297
+ def _on_move_slider(self, e: MouseEvent):
298
+ x = e.xdata
248
299
 
249
- self._restore_region(empty=True)
250
- self._copy_bbox()
251
- self._restore_region()
252
- self._blit()
300
+ if self.intx is not None:
301
+ renderer = self.figure.canvas.renderer
302
+ self.slider_vline.set_segments([((x, self.slider_ymin), (x, self.slider_ymax))])
303
+ self.slider_vline.draw(renderer)
304
+
305
+ if self.in_slider:
306
+ self.text_slider.set_text(f'{self.df[self.date][self.intx]}')
307
+ self.text_slider.set_x(x)
308
+ self.text_slider.draw(renderer)
253
309
  return
254
310
 
255
- def _on_move(self, e):
256
- self._restore_region(with_nav=(not self.is_click), empty_with_nav=self.is_click)
257
311
 
258
- self._on_move_action(e)
312
+ class ClickMixin(MouseMoveMixin):
313
+ x_click = None
314
+ is_move = False
315
+ click_navleft, click_navright = (False, False)
259
316
 
260
- if self.in_slider:
261
- self._change_coordinate()
262
- if self.is_click:
263
- nsub = self._navcoordinate[1] - self._navcoordinate[0]
264
- if self.min_distance <= nsub: self._lim()
265
- if self.is_move: self._set_navigator(*self._navcoordinate)
266
- elif self.intx is not None: self._set_navigator(self._x_click, self.intx)
267
- self.navigator.draw(self.canvas.renderer)
268
- self._draw_blit_artist()
269
- self._slider_move_action(e)
270
- elif self.is_click:
271
- self.navigator.draw(self.canvas.renderer)
272
- self._draw_blit_artist()
273
- else:
274
- if self.in_slider or self.in_price or self.in_volume:
275
- self._slider_move_action(e)
276
- if self.in_price or self.in_volume:
277
- self._chart_move_action(e)
317
+ def _connect_event(self):
318
+ super()._connect_event()
278
319
 
279
- self._blit()
320
+ self.figure.canvas.mpl_connect('button_press_event', lambda x: self._on_click(x))
280
321
  return
281
322
 
282
- def _draw_blit_artist(self):
283
- return self._draw_artist()
284
-
285
- def _lim(self):
286
- xmin, xmax = self._navcoordinate
287
-
288
- xmax += 1
289
- self.ax_price.set_xlim(xmin, xmax)
290
- self.ax_volume.set_xlim(xmin, xmax)
291
-
292
- indmin, indmax = (xmin, xmax)
293
- if xmin < 0: indmin = 0
294
- if indmax < 1: indmax = 1
295
- if indmin == indmax: indmax += 1
296
- ymin, ymax = (self.df[self.low][indmin:indmax].min(), self.df[self.high][indmin:indmax].max())
297
- ysub = (ymax - ymin) / 15
298
- pmin, pmax = (ymin-ysub, ymax+ysub)
299
- self.ax_price.set_ylim(pmin, pmax)
300
-
301
- ymax = self.df[self.volume][indmin:indmax].max()
302
- # self._vol_ymax = ymax*1.2
303
- volmax = ymax * 1.2
304
- self.ax_volume.set_ylim(0, volmax)
305
-
306
- self.set_text_coordante(xmin, xmax, pmin, pmax, volmax)
323
+ def _on_click(self, e: MouseEvent):
324
+ if self.in_slider: self._on_click_slider(e)
307
325
  return
308
326
 
327
+ def _on_click_slider(self, e: MouseEvent):
328
+ if self.is_click_slider or e.button != MouseButton.LEFT: return
329
+
330
+ self.background_with_nav_pre = self.background_with_nav
309
331
 
310
- class SimpleMixin(LimMixin):
311
- simpler = False
312
- limit_volume = 1_500
313
- default_left, default_right = (180, 10)
314
- _draw_blit = False
332
+ self.is_click_slider = True
333
+ self.figure.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
315
334
 
316
- def __init__(self, *args, **kwargs):
317
- super().__init__(*args, **kwargs)
335
+ x = e.xdata.__int__()
336
+ navmin, navmax = self.navcoordinate
318
337
 
319
- # 영역 이동시 주가 collection
320
- self.blitcandle = LineCollection([], animated=True)
321
- self.ax_price.add_collection(self.blitcandle)
322
- self.priceline = LineCollection([], animated=True, edgecolors='k')
323
- self.ax_price.add_artist(self.priceline)
324
-
325
- # 영역 이동시 거래량 collection
326
- self.blitvolume = LineCollection([], animated=True, edgecolors=self.colors_volume)
327
- self.ax_volume.add_collection(self.blitvolume)
338
+ leftmin, leftmax = (navmin-self._navLineWidth, navmin+self._navLineWidth_half)
339
+ rightmin, rightmax = (navmax-self._navLineWidth_half, navmax+self._navLineWidth)
340
+
341
+ grater_than_left, less_then_right = (leftmax < x, x < rightmin)
342
+ if grater_than_left and less_then_right:
343
+ self.is_move = True
344
+ self.x_click = x
345
+ else:
346
+ if not grater_than_left and leftmin <= x:
347
+ self.click_navleft = True
348
+ self.x_click = navmax
349
+ elif not less_then_right and x <= rightmax:
350
+ self.click_navright = True
351
+ self.x_click = navmin
352
+ else:
353
+ self.x_click = x
328
354
  return
329
355
 
330
- def _set_data(self, df: pd.DataFrame, sort_df=True, calc_ma=True, change_lim=True, calc_info=True):
331
- super()._set_data(df, sort_df, calc_ma, False, calc_info)
332
356
 
333
- seg = self.df[['x', self.high, 'x', self.low]].values
334
- seg = seg.reshape(seg.shape[0], 2, 2)
335
- self.blitcandle.set_segments(seg)
336
- self.blitcandle.set_edgecolor(self.df['edgecolor'])
357
+ class SliderSelectMixin(ClickMixin):
358
+ limit_ma = 8_000
337
359
 
338
- pseg = self.df[['x', self.close]].values
339
- self.priceline.set_verts(pseg.reshape(1, *pseg.shape))
360
+ def _on_move_slider(self, e):
361
+ if self.is_click_slider: self._set_navcoordinate(e)
362
+ return super()._on_move_slider(e)
340
363
 
341
- l = self.df.__len__()
342
- if l < self.limit_volume:
343
- volseg = self.df.loc[:, ['x', 'zero', 'x', self.volume]].values
344
- else:
345
- v = self.df[['x', 'zero', 'x', self.volume]].sort_values([self.volume], axis=0, ascending=False)
346
- volseg = v[:self.limit_volume].values
364
+ def _set_navcoordinate(self, e: MouseEvent):
365
+ x = e.xdata.__int__()
366
+ navmin, navmax = self.navcoordinate
347
367
 
348
- self.blitvolume.set_segments(volseg.reshape(volseg.shape[0], 2, 2))
368
+ if self.is_move:
369
+ xsub = self.x_click - x
370
+ navmin, navmax = (navmin-xsub, navmax-xsub)
349
371
 
350
- if change_lim:
351
- index = self.df.index[-1]
352
- if index < self.default_left + self.default_right: self._navcoordinate = (int(self.xmin)-1, int(self.xmax)+1)
353
- else: self._navcoordinate = (index-self.default_left, index+self.default_right)
372
+ # 값 보정
373
+ if navmax < 0: navmin, navmax = (navmin-navmax, 0)
374
+ if self.list_index[-1] < navmin: navmin, navmax = (self.list_index[-1], self.list_index[-1] + (navmax-navmin))
354
375
 
355
- self._set_navigator(*self._navcoordinate)
356
- self._lim()
357
- return
376
+ self.navcoordinate = (navmin, navmax)
377
+ self.x_click = x
358
378
 
359
- def _draw_blit_artist(self):
360
- renderer = self.canvas.renderer
379
+ self._set_lim(navmin, navmax, simpler=True, set_ma=(navmax-navmin < self.limit_ma))
361
380
 
362
- self.ax_price.xaxis.draw(renderer)
363
- self.ax_price.yaxis.draw(renderer)
381
+ self._set_navigator(navmin, navmax)
382
+ self.navigator.draw(self.figure.canvas.renderer)
364
383
 
365
- if self.simpler:
366
- if self._draw_blit: self.priceline.draw(renderer)
367
- else: self.blitcandle.draw(renderer)
368
- elif self.candle_on_ma:
369
- self.macollection.draw(renderer)
370
- if self._draw_blit: self.blitcandle.draw(renderer)
371
- else: self.candlecollection.draw(renderer)
384
+ self._draw_artist()
385
+ self.background_with_nav = self.figure.canvas.renderer.copy_from_bbox(self.figure.bbox)
386
+ self._restore_region(False, True)
372
387
  else:
373
- if self._draw_blit: self.blitcandle.draw(renderer)
374
- else: self.candlecollection.draw(renderer)
375
- self.macollection.draw(renderer)
388
+ navmin, navmax = (x, self.x_click) if x < self.x_click else (self.x_click, x)
389
+
390
+ # 슬라이더가 차트를 벗어나지 않도록 선택 영역 제한
391
+ if navmax < 0 or self.list_index[-1] < navmin:
392
+ seg = self.navigator.get_segments()
393
+ navmin, navmax = (int(seg[1][0][0]), int(seg[3][0][0]))
394
+
395
+ nsub = navmax - navmin
396
+ if nsub < self.min_distance:
397
+ self._restore_region(False, False)
398
+ self._set_navigator(navmin, navmax)
399
+ self.navigator.draw(self.figure.canvas.renderer)
400
+ else:
401
+ self._set_lim(navmin, navmax, simpler=True, set_ma=(nsub < self.limit_ma))
402
+ self._set_navigator(navmin, navmax)
403
+
404
+ self.navigator.draw(self.figure.canvas.renderer)
405
+
406
+ self._draw_artist()
407
+ self.background_with_nav = self.figure.canvas.renderer.copy_from_bbox(self.figure.bbox)
408
+ self._restore_region(False, True)
409
+ return
376
410
 
377
- self.ax_volume.xaxis.draw(renderer)
378
- self.ax_volume.yaxis.draw(renderer)
379
411
 
380
- self.blitvolume.draw(renderer)
412
+ class ReleaseMixin(SliderSelectMixin):
413
+ def _connect_event(self):
414
+ super()._connect_event()
415
+
416
+ self.figure.canvas.mpl_connect('button_release_event', lambda x: self._on_release(x))
417
+ return
418
+
419
+ def _on_release(self, e: MouseEvent):
420
+ if self.in_slider and self.is_click_slider: self._on_release_slider(e)
421
+ return
422
+
423
+ def _on_release_slider(self, e: MouseEvent):
424
+ if not self.is_move:
425
+ seg = self.navigator.get_segments()
426
+ navmin, navmax = (int(seg[1][0][0]), int(seg[3][0][0]))
427
+ nsub = navmax - navmin
428
+ if self.min_distance <= nsub: self.navcoordinate = (navmin, navmax)
429
+ else:
430
+ self.background_with_nav = self.background_with_nav_pre
431
+ navmin, navmax = self.navcoordinate
432
+ self._set_lim(navmin, navmax, simpler=True, set_ma=(nsub < self.limit_ma))
433
+ self._restore_region(False, True)
434
+ self._blit()
435
+ self._set_navigator(*self.navcoordinate)
436
+
437
+ self.is_click_slider = False
438
+ self.is_move = False
439
+ self.click_navleft, self.click_navright = (False, False)
381
440
  return
382
441
 
383
442
 
384
- class ClickMixin(SimpleMixin):
443
+ class ChartClickMixin(ReleaseMixin):
385
444
  is_click_chart = False
386
445
 
387
446
  def _on_click(self, e: MouseEvent):
388
- if not self.is_click and e.button == MouseButton.LEFT:
389
- if e.inaxes is self.ax_slider: pass
390
- elif e.inaxes is self.ax_price or e.inaxes is self.ax_volume: return self._on_chart_click(e)
391
- else: return
392
- else: return
447
+ if self.in_price_chart or self.in_volume_chart: self._on_click_chart(e)
448
+ elif self.in_slider: self._on_click_slider(e)
449
+ return
393
450
 
394
- self.is_click = True
451
+ def _on_click_chart(self, e: MouseEvent):
452
+ if self.is_click_chart: return
395
453
 
396
- x = e.xdata.__int__()
397
- left, right = self._navcoordinate
398
- lmin, lmax = (left-self._navLineWidth, left+self._navLineWidth_half)
399
- rmin, rmax = (right-self._navLineWidth_half, right+self._navLineWidth)
400
-
401
- gtl, ltr = (lmax < x, x < rmin)
402
- if gtl and ltr:
403
- self._x_click = x
404
- self.is_move = True
405
- self.canvas.set_cursor(cursors.MOVE)
406
- else:
407
- self.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
408
- if not gtl and lmin <= x:
409
- self._x_click = right
410
- elif not ltr and x <= rmax:
411
- self._x_click = left
412
- else:
413
- self._x_click = x
454
+ self.is_click_chart = True
455
+ self._x_click = e.x.__round__(2)
456
+ self.figure.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
457
+ return
414
458
 
415
- # 그리기 후 최초 클릭이면 좌표 수정
416
- if left == right:
417
- self._navcoordinate = (x, x)
459
+ def _on_release(self, e):
460
+ if self.is_click_chart and (self.in_price_chart or self.in_volume_chart): self._on_release_chart(e)
461
+ elif self.is_click_slider and self.in_slider: self._on_release_slider(e)
418
462
  return
419
463
 
420
- def _on_release(self, e: MouseEvent):
421
- if not self.is_click: return
422
- elif e.inaxes is self.ax_slider: return super()._on_release(e)
423
- elif not self.in_price and not self.in_volume and not self.is_click_chart: return
424
- # 차트 click release action
425
- self.canvas.set_cursor(cursors.POINTER)
426
- self.is_click, self.is_move = (False, False)
464
+ def _on_release_chart(self, e):
427
465
  self.is_click_chart = False
466
+ self.figure.canvas.set_cursor(cursors.POINTER)
467
+ return
468
+
469
+ def _change_cursor(self, e):
470
+ if self.is_click_chart: return
471
+ return super()._change_cursor(e)
472
+
473
+ def _on_move(self, e):
474
+ self._on_move_action(e)
475
+
476
+ need_slider_action = self.is_click_slider and self.in_slider
477
+ need_chart_action = False if need_slider_action else self.is_click_chart and (self.in_price_chart or self.in_volume_chart)
478
+ self._restore_region((need_slider_action or need_chart_action))
479
+
480
+ if self.in_slider:
481
+ self._on_move_slider(e)
482
+ elif self.in_price_chart:
483
+ self._on_move_price_chart(e)
484
+ elif self.in_volume_chart:
485
+ self._on_move_volume_chart(e)
428
486
 
429
- self._restore_region(empty=True)
430
- self._copy_bbox()
431
- self._restore_region()
432
487
  self._blit()
433
488
  return
434
489
 
435
- def _on_chart_click(self, e: MouseEvent):
436
- self.is_click = True
437
- self.is_click_chart = True
438
- self._x_click = e.x.__int__()
439
- self.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
440
- return
490
+ def _on_move_price_chart(self, e):
491
+ if self.is_click_chart: self._move_chart(e)
492
+ return super()._on_move_price_chart(e)
441
493
 
442
- def _change_coordinate(self):
443
- if self.is_click_chart: self._change_coordinate_chart()
444
- else: super()._change_coordinate()
445
- return
494
+ def _on_move_volume_chart(self, e):
495
+ if self.is_click_chart: self._move_chart(e)
496
+ return super()._on_move_volume_chart(e)
446
497
 
447
- def _change_coordinate_chart(self, e: MouseEvent):
448
- x = e.x.__int__()
449
- left, right = self._navcoordinate
498
+ def _move_chart(self, e: MouseEvent):
499
+ x = e.x.__round__(2)
500
+ left, right = self.navcoordinate
450
501
  nsub = right - left
451
502
  xsub = x - self._x_click
452
503
  xdiv = (xsub / (1200 / nsub)).__int__()
453
- if xdiv:
504
+ if not xdiv:
505
+ self.navigator.draw(self.figure.canvas.renderer)
506
+ self._draw_artist()
507
+ else:
454
508
  left, right = (left-xdiv, right-xdiv)
455
- if -1 < right and left < self.df.index[-1]:
456
- self._navcoordinate = (left, right)
509
+ if right < 0 or self.df.index[-1] < left: self._restore_region(False, True)
510
+ else:
511
+ self.navcoordinate = (left, right)
512
+ self._set_lim(left, right, simpler=True, set_ma=((right-left) < self.limit_ma))
513
+ self._set_navigator(left, right)
514
+ self.navigator.draw(self.figure.canvas.renderer)
515
+
516
+ self._draw_artist()
517
+ self.background_with_nav = self.figure.canvas.renderer.copy_from_bbox(self.figure.bbox)
518
+ self._restore_region(False, True)
457
519
  self._x_click = x
458
520
  return
459
521
 
460
- def _on_move(self, e):
461
- self._restore_region(with_nav=(not self.is_click), empty_with_nav=self.is_click)
462
522
 
463
- self._on_move_action(e)
464
-
465
- if self.in_slider and not self.is_click_chart:
466
- self._change_coordinate()
467
- if self.is_click:
468
- nsub = self._navcoordinate[1] - self._navcoordinate[0]
469
- if self.is_move: self._set_navigator(*self._navcoordinate)
470
- else:
471
- self._draw_blit = 900 < nsub
472
- if self.intx is not None: self._set_navigator(self._x_click, self.intx)
473
-
474
- if self.min_distance <= nsub: self._lim()
475
-
476
- self.navigator.draw(self.canvas.renderer)
477
- self._draw_blit_artist()
478
- self._slider_move_action(e)
479
- elif self.is_click:
480
- if self.is_click_chart and (self.in_price or self.in_volume):
481
- if (self.vmin, self.vmax) != self._navcoordinate:
482
- self._change_coordinate_chart(e)
483
- self._lim()
484
- self._set_navigator(*self._navcoordinate)
485
- self.navigator.draw(self.canvas.renderer)
486
- self._draw_blit_artist()
487
- else:
488
- if self.in_slider or self.in_price or self.in_volume:
489
- self._slider_move_action(e)
490
- if self.in_price or self.in_volume:
491
- self._chart_move_action(e)
523
+ class BaseMixin(ChartClickMixin):
524
+ pass
492
525
 
493
- self._blit()
494
- return
495
526
 
527
+ class Chart(BaseMixin, Mixin):
528
+ def _add_collection(self):
529
+ super()._add_collection()
530
+ return self.add_collection()
496
531
 
497
- class SliderMixin(ClickMixin):
498
- pass
532
+ def _draw_artist(self):
533
+ super()._draw_artist()
534
+ return self.draw_artist()
499
535
 
536
+ def _get_segments(self):
537
+ self.generate_data()
538
+ return super()._get_segments()
500
539
 
501
- class Chart(SliderMixin, CM, Mixin):
502
540
  def _on_draw(self, e):
503
541
  super()._on_draw(e)
504
542
  return self.on_draw(e)
@@ -511,40 +549,11 @@ class Chart(SliderMixin, CM, Mixin):
511
549
  super()._on_move(e)
512
550
  return self.on_move(e)
513
551
 
514
- def _draw_artist(self):
515
- super()._draw_artist()
516
- return self.draw_artist()
517
- def _draw_blit_artist(self):
518
- super()._draw_blit_artist()
519
- return self.draw_artist()
520
-
521
552
  def _on_click(self, e):
522
553
  super()._on_click(e)
523
554
  return self.on_click(e)
524
- def _on_release(self, e):
525
- super()._on_release(e)
526
- return self.on_release(e)
527
-
528
-
529
- if __name__ == '__main__':
530
- import json
531
- from time import time
532
555
 
533
- import matplotlib.pyplot as plt
534
- from pathlib import Path
535
-
536
- file = Path(__file__).parent / 'data/samsung.txt'
537
- # file = Path(__file__).parent / 'data/apple.txt'
538
- with open(file, 'r', encoding='utf-8') as txt:
539
- data = json.load(txt)
540
- data = data
541
- df = pd.DataFrame(data)
542
-
543
- t = time()
544
- # c = SimpleMixin()
545
- c = SliderMixin()
546
- c.set_data(df)
547
- t2 = time() - t
548
- print(f'{t2=}')
549
- plt.show()
556
+ def on_release(self, e):
557
+ super().on_release(e)
558
+ return self.on_release(e)
550
559