seolpyo-mplchart 0.1.3.1__py3-none-any.whl → 2.0.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. seolpyo_mplchart/__init__.py +164 -99
  2. seolpyo_mplchart/_base.py +117 -0
  3. seolpyo_mplchart/_chart/__init__.py +137 -0
  4. seolpyo_mplchart/_chart/_base.py +217 -0
  5. seolpyo_mplchart/_chart/_cursor/__init__.py +2 -0
  6. seolpyo_mplchart/_chart/_cursor/_artist.py +217 -0
  7. seolpyo_mplchart/_chart/_cursor/_cursor.py +165 -0
  8. seolpyo_mplchart/_chart/_cursor/_info.py +187 -0
  9. seolpyo_mplchart/_chart/_draw/__init__.py +2 -0
  10. seolpyo_mplchart/_chart/_draw/_artist.py +50 -0
  11. seolpyo_mplchart/_chart/_draw/_data.py +314 -0
  12. seolpyo_mplchart/_chart/_draw/_draw.py +103 -0
  13. seolpyo_mplchart/_chart/_draw/_lim.py +265 -0
  14. seolpyo_mplchart/_chart/_slider/__init__.py +1 -0
  15. seolpyo_mplchart/_chart/_slider/_base.py +268 -0
  16. seolpyo_mplchart/_chart/_slider/_data.py +105 -0
  17. seolpyo_mplchart/_chart/_slider/_mouse.py +176 -0
  18. seolpyo_mplchart/_chart/_slider/_nav.py +204 -0
  19. seolpyo_mplchart/_chart/test.py +121 -0
  20. seolpyo_mplchart/_config/__init__.py +3 -0
  21. seolpyo_mplchart/_config/ax.py +28 -0
  22. seolpyo_mplchart/_config/candle.py +30 -0
  23. seolpyo_mplchart/_config/config.py +21 -0
  24. seolpyo_mplchart/_config/cursor.py +49 -0
  25. seolpyo_mplchart/_config/figure.py +41 -0
  26. seolpyo_mplchart/_config/format.py +51 -0
  27. seolpyo_mplchart/_config/ma.py +15 -0
  28. seolpyo_mplchart/_config/slider/__init__.py +2 -0
  29. seolpyo_mplchart/_config/slider/config.py +24 -0
  30. seolpyo_mplchart/_config/slider/figure.py +20 -0
  31. seolpyo_mplchart/_config/slider/nav.py +9 -0
  32. seolpyo_mplchart/_config/unit.py +19 -0
  33. seolpyo_mplchart/_config/utils.py +67 -0
  34. seolpyo_mplchart/_config/volume.py +26 -0
  35. seolpyo_mplchart/_cursor.py +559 -0
  36. seolpyo_mplchart/_draw.py +634 -0
  37. seolpyo_mplchart/_slider.py +634 -0
  38. seolpyo_mplchart/base.py +70 -67
  39. seolpyo_mplchart/cursor.py +308 -271
  40. seolpyo_mplchart/draw.py +449 -237
  41. seolpyo_mplchart/slider.py +451 -396
  42. seolpyo_mplchart/test.py +173 -24
  43. seolpyo_mplchart/utils.py +15 -4
  44. seolpyo_mplchart/xl_to_dict.py +47 -0
  45. seolpyo_mplchart-2.0.0.3.dist-info/METADATA +710 -0
  46. seolpyo_mplchart-2.0.0.3.dist-info/RECORD +50 -0
  47. {seolpyo_mplchart-0.1.3.1.dist-info → seolpyo_mplchart-2.0.0.3.dist-info}/WHEEL +1 -1
  48. seolpyo_mplchart-0.1.3.1.dist-info/METADATA +0 -49
  49. seolpyo_mplchart-0.1.3.1.dist-info/RECORD +0 -13
  50. {seolpyo_mplchart-0.1.3.1.dist-info → seolpyo_mplchart-2.0.0.3.dist-info}/top_level.txt +0 -0
@@ -1,504 +1,573 @@
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': 0.5, 'color': '#d0d0d0', 'linestyle': '-', 'dashes': (1, 0)}
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.3, 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)
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
+ return
69
135
 
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
136
+ def _get_color_segment(self):
137
+ super()._get_color_segment()
83
138
 
84
- # 그리기 후 최초 클릭이면 좌표 수정
85
- if left == right:
86
- self._navcoordinate = (x, x)
139
+ self.collection_slider.set_edgecolor(self.edgecolor_ma + [self.color_priceline])
87
140
  return
88
141
 
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)
142
+ def change_background_color(self, color):
143
+ super().change_background_color(color)
92
144
 
93
- if self._navcoordinate[0] == self._navcoordinate[1]:
94
- self._navcoordinate = (self._navcoordinate[0], self._navcoordinate[1]+self.min_distance)
145
+ self.ax_slider.set_facecolor(color)
146
+ self.text_slider.set_backgroundcolor(color)
95
147
  return
96
148
 
149
+ def change_tick_color(self, color):
150
+ super().change_tick_color(color)
97
151
 
98
- class BackgroundMixin(NavgatorMixin):
99
- def _on_draw(self, e):
100
- self.background = None
101
- self._restore_region()
152
+ for i in ['top', 'bottom', 'left', 'right']: self.ax_slider.spines[i].set_color(self.color_tick)
153
+ self.ax_slider.tick_params(colors=color)
102
154
  return
103
155
 
104
- def _restore_region(self, with_nav=True, empty=False, empty_with_nav=False):
105
- if not self.background: self._create_background()
156
+ def change_text_color(self, color):
157
+ super().change_text_color(color)
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
+ self.text_slider.set_color(color)
111
160
  return
112
161
 
113
- def _copy_bbox(self):
114
- renderer = self.canvas.renderer
162
+ def change_line_color(self, color):
163
+ super().change_line_color(color)
115
164
 
116
- self.background_empty = renderer.copy_from_bbox(self.fig.bbox)
165
+ self.text_slider.get_bbox_patch().set_edgecolor(color)
166
+ return
117
167
 
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)
122
168
 
123
- self._draw_artist()
124
- self.background = self.canvas.renderer.copy_from_bbox(self.fig.bbox)
169
+ class NavigatorMixin(CollectionMixin):
170
+ def _set_slider_lim(self):
171
+ xmax = self.list_index[-1]
172
+ # 슬라이더 xlim
173
+ xdistance = xmax / 30
174
+ self.slider_xmin, self.slider_xmax = (-xdistance, xmax + xdistance)
175
+ self.ax_slider.set_xlim(self.slider_xmin, self.slider_xmax)
125
176
 
126
- self.navigator.draw(self.canvas.renderer)
127
- self.background_with_nav = self.canvas.renderer.copy_from_bbox(self.fig.bbox)
177
+ # 슬라이더 ylim
178
+ ymin, ymax = (self.df[self.low].min(), self.df[self.high].max())
179
+ ysub = ymax - ymin
180
+ self.sldier_ymiddle = ymin + (ysub / 2)
181
+ ydistance = ysub / 5
182
+ self.slider_ymin, self.slider_ymax = (ymin-ydistance, ymax+ydistance)
183
+ self.ax_slider.set_ylim(self.slider_ymin, self.slider_ymax)
184
+
185
+ # 슬라이더 텍스트 y
186
+ self.text_slider.set_y(ymax)
187
+
188
+ self.navigator.set_linewidth([ysub, 5])
189
+
190
+ # 네비게이터 라인 선택 범위
191
+ xsub = self.slider_xmax - self.slider_xmin
192
+ self._navLineWidth = xsub * 8 / 1_000
193
+ if self._navLineWidth < 1: self._navLineWidth = 1
194
+ self._navLineWidth_half = self._navLineWidth / 2
128
195
  return
129
196
 
130
- def _draw_artist(self):
131
- renderer = self.canvas.renderer
197
+ def _set_navigator(self, navmin, navmax):
198
+ navseg = [
199
+ (
200
+ (self.slider_xmin, self.sldier_ymiddle),
201
+ (navmin, self.sldier_ymiddle)
202
+ ),
203
+ (
204
+ (navmin, self.slider_ymin),
205
+ (navmin, self.slider_ymax)
206
+ ),
207
+ (
208
+ (navmax, self.sldier_ymiddle),
209
+ (self.slider_xmax, self.sldier_ymiddle)
210
+ ),
211
+ (
212
+ (navmax, self.slider_ymin),
213
+ (navmax, self.slider_ymax)
214
+ ),
215
+ ]
216
+
217
+ self.navigator.set_segments(navseg)
218
+ return
132
219
 
133
- self.ax_price.xaxis.draw(renderer)
134
- self.ax_price.yaxis.draw(renderer)
135
220
 
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)
221
+ class DataMixin(NavigatorMixin):
222
+ navcoordinate = (0, 0)
223
+
224
+ 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):
225
+ self._generate_data(df, sort_df, calc_ma, set_candlecolor, set_volumecolor, calc_info, *args, **kwargs)
226
+ self._get_segments()
142
227
 
143
- self.ax_volume.xaxis.draw(renderer)
144
- self.ax_volume.yaxis.draw(renderer)
228
+ vmin, vmax = self.navcoordinate
229
+ if change_lim or (vmax-vmin) < self.min_distance:
230
+ vmin, vmax = self.get_default_lim()
231
+ self.navcoordinate = (vmin, vmax)
145
232
 
146
- self.volumecollection.draw(renderer)
233
+ self._set_lim(vmin, vmax)
234
+ self._set_slider_lim()
235
+ self._set_navigator(vmin, vmax)
236
+
237
+ self._length_text = self.df[(self.volume if self.volume else self.high)].apply(lambda x: len(str(x))).max()
147
238
  return
148
239
 
240
+ def get_default_lim(self):
241
+ xmax = self.list_index[-1]
242
+ return (xmax-120, xmax)
243
+
149
244
 
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)
245
+ class BackgroundMixin(DataMixin):
246
+ def _copy_bbox(self):
247
+ renderer = self.figure.canvas.renderer
153
248
 
154
- # 네비게이터 높이 설정
155
- ysub = self._slider_ymax - self._slider_ymin
156
- self._ymiddle = self._slider_ymax - ysub / 2
157
- self.navigator.set_linewidth((ysub, 5))
249
+ self.ax_slider.xaxis.draw(renderer)
250
+ self.ax_slider.yaxis.draw(renderer)
251
+ self.collection_slider.draw(renderer)
252
+ self.background_emtpy = renderer.copy_from_bbox(self.figure.bbox)
253
+
254
+ self._draw_artist()
255
+ self.background = renderer.copy_from_bbox(self.figure.bbox)
256
+
257
+ self.navigator.draw(self.figure.canvas.renderer)
258
+ self.background_with_nav = renderer.copy_from_bbox(self.figure.bbox)
158
259
  return
159
260
 
160
- def _on_release(self, e: MouseEvent):
161
- super()._on_release(e)
162
- self._set_navigator(*self._navcoordinate)
261
+ def _restore_region(self, is_empty=False, with_nav=True):
262
+ if not self.background: self._create_background()
163
263
 
164
- self._restore_region(empty=True)
165
- self._creating_background = False
166
- self._create_background()
167
- self._restore_region()
168
- self._blit()
264
+ if is_empty: self.figure.canvas.renderer.restore_region(self.background_emtpy)
265
+ elif with_nav: self.figure.canvas.renderer.restore_region(self.background_with_nav)
266
+ else: self.figure.canvas.renderer.restore_region(self.background)
169
267
  return
170
268
 
171
- def _on_move(self, e: MouseEvent):
172
- self._restore_region((not self.is_click))
173
269
 
270
+ class MouseMoveMixin(BackgroundMixin):
271
+ in_slider = False
272
+ is_click_slider = False
273
+
274
+ def _on_move(self, e):
174
275
  self._on_move_action(e)
175
276
 
277
+ self._restore_region((self.is_click_slider and self.in_slider))
278
+
176
279
  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)
280
+ self._on_move_slider(e)
281
+ elif self.in_price_chart:
282
+ self._on_move_price_chart(e)
283
+ elif self.in_volume_chart:
284
+ self._on_move_volume_chart(e)
190
285
 
191
286
  self._blit()
192
287
  return
193
288
 
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)
289
+ def _on_move_action(self, e: MouseEvent):
290
+ self._check_ax(e)
222
291
 
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
292
+ self.intx = None
293
+ if self.in_slider or self.in_price_chart or self.in_volume_chart: self._get_x(e)
227
294
 
228
- def _set_navigator(self, x1, x2):
229
- xmin, xmax = (x1, x2) if x1 < x2 else (x2, x1)
295
+ self._change_cursor(e)
296
+ return
230
297
 
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))
298
+ def _change_cursor(self, e: MouseEvent):
299
+ # 마우스 커서 변경
300
+ if self.is_click_slider: return
301
+ elif not self.in_slider:
302
+ self.figure.canvas.set_cursor(cursors.POINTER)
303
+ return
304
+
305
+ navleft, navright = self.navcoordinate
306
+ if navleft == navright: return
307
+
308
+ x = e.xdata
309
+ leftmin, leftmax = (navleft-self._navLineWidth, navleft+self._navLineWidth_half)
310
+ rightmin, rightmax = (navright-self._navLineWidth_half, navright+self._navLineWidth)
311
+ if x < leftmin: self.figure.canvas.set_cursor(cursors.POINTER)
312
+ elif x < leftmax: self.figure.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
313
+ elif x < rightmin: self.figure.canvas.set_cursor(cursors.MOVE)
314
+ elif x < rightmax: self.figure.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
315
+ else: self.figure.canvas.set_cursor(cursors.POINTER)
236
316
  return
237
317
 
318
+ def _check_ax(self, e: MouseEvent):
319
+ ax = e.inaxes
320
+ if not ax or e.xdata is None or e.ydata is None:
321
+ self.in_slider, self.in_price_chart, self.in_volume_chart = (False, False, False)
322
+ else:
323
+ self.in_slider = ax is self.ax_slider
324
+ self.in_price_chart = False if self.in_slider else ax is self.ax_price
325
+ self.in_volume_chart = False if (self.in_slider or self.in_price_chart) else ax is self.ax_volume
326
+ return
238
327
 
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)
328
+ def _on_move_slider(self, e: MouseEvent):
329
+ x = e.xdata
243
330
 
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()
331
+ if self.intx is not None:
332
+ renderer = self.figure.canvas.renderer
333
+ self.slider_vline.set_segments([((x, self.slider_ymin), (x, self.slider_ymax))])
334
+ self.slider_vline.draw(renderer)
248
335
 
249
- self._restore_region(empty=True)
250
- self._copy_bbox()
251
- self._restore_region()
252
- self._blit()
336
+ if self.in_slider:
337
+ self.text_slider.set_text(f'{self.df[self.date][self.intx]}')
338
+ self.text_slider.set_x(x)
339
+ self.text_slider.draw(renderer)
253
340
  return
254
341
 
255
- def _on_move(self, e):
256
- self._restore_region(with_nav=(not self.is_click), empty_with_nav=self.is_click)
257
342
 
258
- self._on_move_action(e)
343
+ class ClickMixin(MouseMoveMixin):
344
+ x_click = None
345
+ is_move = False
346
+ click_navleft, click_navright = (False, False)
259
347
 
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)
348
+ def _connect_event(self):
349
+ super()._connect_event()
278
350
 
279
- self._blit()
351
+ self.figure.canvas.mpl_connect('button_press_event', lambda x: self._on_click(x))
280
352
  return
281
353
 
282
- def _draw_blit_artist(self):
283
- return self._draw_artist()
354
+ def _on_click(self, e: MouseEvent):
355
+ if self.in_slider: self._on_click_slider(e)
356
+ return
284
357
 
285
- def _lim(self):
286
- xmin, xmax = self._navcoordinate
358
+ def _on_click_slider(self, e: MouseEvent):
359
+ if self.is_click_slider or e.button != MouseButton.LEFT: return
287
360
 
288
- xmax += 1
289
- self.ax_price.set_xlim(xmin, xmax)
290
- self.ax_volume.set_xlim(xmin, xmax)
361
+ self.background_with_nav_pre = self.background_with_nav
291
362
 
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)
363
+ self.is_click_slider = True
364
+ self.figure.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
305
365
 
306
- self.set_text_coordante(xmin, xmax, pmin, pmax, volmax)
366
+ x = e.xdata.__int__()
367
+ navmin, navmax = self.navcoordinate
368
+
369
+ leftmin, leftmax = (navmin-self._navLineWidth, navmin+self._navLineWidth_half)
370
+ rightmin, rightmax = (navmax-self._navLineWidth_half, navmax+self._navLineWidth)
371
+
372
+ grater_than_left, less_then_right = (leftmax < x, x < rightmin)
373
+ if grater_than_left and less_then_right:
374
+ self.is_move = True
375
+ self.x_click = x
376
+ else:
377
+ if not grater_than_left and leftmin <= x:
378
+ self.click_navleft = True
379
+ self.x_click = navmax
380
+ elif not less_then_right and x <= rightmax:
381
+ self.click_navright = True
382
+ self.x_click = navmin
383
+ else:
384
+ self.x_click = x
307
385
  return
308
386
 
309
387
 
310
- class SimpleMixin(LimMixin):
311
- simpler = False
312
- limit_volume = 1_500
313
- default_left, default_right = (180, 10)
314
- _draw_blit = False
388
+ class SliderSelectMixin(ClickMixin):
389
+ limit_ma = 8_000
315
390
 
316
- def __init__(self, *args, **kwargs):
317
- super().__init__(*args, **kwargs)
318
-
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)
391
+ def _on_move_slider(self, e):
392
+ if self.is_click_slider: self._set_navcoordinate(e)
393
+ return super()._on_move_slider(e)
324
394
 
325
- # 영역 이동시 거래량 collection
326
- self.blitvolume = LineCollection([], animated=True, edgecolors=self.colors_volume)
327
- self.ax_volume.add_collection(self.blitvolume)
328
- return
395
+ def _set_navcoordinate(self, e: MouseEvent):
396
+ x = e.xdata.__int__()
397
+ navmin, navmax = self.navcoordinate
329
398
 
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)
399
+ if self.is_move:
400
+ xsub = self.x_click - x
401
+ navmin, navmax = (navmin-xsub, navmax-xsub)
332
402
 
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'])
403
+ # 보정
404
+ if navmax < 0: navmin, navmax = (navmin-navmax, 0)
405
+ if self.list_index[-1] < navmin: navmin, navmax = (self.list_index[-1], self.list_index[-1] + (navmax-navmin))
337
406
 
338
- pseg = self.df[['x', self.close]].values
339
- self.priceline.set_verts(pseg.reshape(1, *pseg.shape))
407
+ self.navcoordinate = (navmin, navmax)
408
+ self.x_click = x
340
409
 
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
410
+ self._set_lim(navmin, navmax, simpler=True, set_ma=(navmax-navmin < self.limit_ma))
347
411
 
348
- self.blitvolume.set_segments(volseg.reshape(volseg.shape[0], 2, 2))
412
+ self._set_navigator(navmin, navmax)
413
+ self.navigator.draw(self.figure.canvas.renderer)
349
414
 
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)
415
+ self._draw_artist()
416
+ self.background_with_nav = self.figure.canvas.renderer.copy_from_bbox(self.figure.bbox)
417
+ self._restore_region(False, True)
418
+ else:
419
+ navmin, navmax = (x, self.x_click) if x < self.x_click else (self.x_click, x)
420
+
421
+ # 슬라이더가 차트를 벗어나지 않도록 선택 영역 제한
422
+ if navmax < 0 or self.list_index[-1] < navmin:
423
+ seg = self.navigator.get_segments()
424
+ navmin, navmax = (int(seg[1][0][0]), int(seg[3][0][0]))
425
+
426
+ nsub = navmax - navmin
427
+ if nsub < self.min_distance:
428
+ self._restore_region(False, False)
429
+ self._set_navigator(navmin, navmax)
430
+ self.navigator.draw(self.figure.canvas.renderer)
431
+ else:
432
+ self._set_lim(navmin, navmax, simpler=True, set_ma=(nsub < self.limit_ma))
433
+ self._set_navigator(navmin, navmax)
354
434
 
355
- self._set_navigator(*self._navcoordinate)
356
- self._lim()
357
- return
435
+ self.navigator.draw(self.figure.canvas.renderer)
358
436
 
359
- def _draw_blit_artist(self):
360
- renderer = self.canvas.renderer
437
+ self._draw_artist()
438
+ self.background_with_nav = self.figure.canvas.renderer.copy_from_bbox(self.figure.bbox)
439
+ self._restore_region(False, True)
440
+ return
361
441
 
362
- self.ax_price.xaxis.draw(renderer)
363
- self.ax_price.yaxis.draw(renderer)
364
442
 
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)
372
- else:
373
- if self._draw_blit: self.blitcandle.draw(renderer)
374
- else: self.candlecollection.draw(renderer)
375
- self.macollection.draw(renderer)
443
+ class ReleaseMixin(SliderSelectMixin):
444
+ def _connect_event(self):
445
+ super()._connect_event()
376
446
 
377
- self.ax_volume.xaxis.draw(renderer)
378
- self.ax_volume.yaxis.draw(renderer)
447
+ self.figure.canvas.mpl_connect('button_release_event', lambda x: self._on_release(x))
448
+ return
379
449
 
380
- self.blitvolume.draw(renderer)
450
+ def _on_release(self, e: MouseEvent):
451
+ if self.in_slider and self.is_click_slider: self._on_release_slider(e)
452
+ return
453
+
454
+ def _on_release_slider(self, e: MouseEvent):
455
+ if not self.is_move:
456
+ seg = self.navigator.get_segments()
457
+ navmin, navmax = (int(seg[1][0][0]), int(seg[3][0][0]))
458
+ nsub = navmax - navmin
459
+ if self.min_distance <= nsub: self.navcoordinate = (navmin, navmax)
460
+ else:
461
+ self.background_with_nav = self.background_with_nav_pre
462
+ navmin, navmax = self.navcoordinate
463
+ self._set_lim(navmin, navmax, simpler=True, set_ma=(nsub < self.limit_ma))
464
+ self._restore_region(False, True)
465
+ self._blit()
466
+ self._set_navigator(*self.navcoordinate)
467
+
468
+ self.is_click_slider = False
469
+ self.is_move = False
470
+ self.click_navleft, self.click_navright = (False, False)
381
471
  return
382
472
 
383
473
 
384
- class ClickMixin(SimpleMixin):
474
+ class ChartClickMixin(ReleaseMixin):
385
475
  is_click_chart = False
386
476
 
387
477
  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
478
+ if self.in_price_chart or self.in_volume_chart: self._on_click_chart(e)
479
+ elif self.in_slider: self._on_click_slider(e)
480
+ return
393
481
 
394
- self.is_click = True
482
+ def _on_click_chart(self, e: MouseEvent):
483
+ if self.is_click_chart: return
395
484
 
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
485
+ self.is_click_chart = True
486
+ self._x_click = e.x.__round__(2)
487
+ self.figure.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
488
+ return
414
489
 
415
- # 그리기 후 최초 클릭이면 좌표 수정
416
- if left == right:
417
- self._navcoordinate = (x, x)
490
+ def _on_release(self, e):
491
+ if self.is_click_chart and (self.in_price_chart or self.in_volume_chart): self._on_release_chart(e)
492
+ elif self.is_click_slider and self.in_slider: self._on_release_slider(e)
418
493
  return
419
494
 
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)
495
+ def _on_release_chart(self, e):
427
496
  self.is_click_chart = False
497
+ self.figure.canvas.set_cursor(cursors.POINTER)
498
+ return
499
+
500
+ def _change_cursor(self, e):
501
+ if self.is_click_chart: return
502
+ return super()._change_cursor(e)
503
+
504
+ def _on_move(self, e):
505
+ self._on_move_action(e)
506
+
507
+ need_slider_action = self.is_click_slider and self.in_slider
508
+ need_chart_action = False if need_slider_action else self.is_click_chart and (self.in_price_chart or self.in_volume_chart)
509
+ self._restore_region((need_slider_action or need_chart_action))
510
+
511
+ if self.in_slider:
512
+ self._on_move_slider(e)
513
+ elif self.in_price_chart:
514
+ self._on_move_price_chart(e)
515
+ elif self.in_volume_chart:
516
+ self._on_move_volume_chart(e)
428
517
 
429
- self._restore_region(empty=True)
430
- self._copy_bbox()
431
- self._restore_region()
432
518
  self._blit()
433
519
  return
434
520
 
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
521
+ def _on_move_price_chart(self, e):
522
+ if self.is_click_chart: self._move_chart(e)
523
+ return super()._on_move_price_chart(e)
441
524
 
442
- def _change_coordinate(self):
443
- if self.is_click_chart: self._change_coordinate_chart()
444
- else: super()._change_coordinate()
445
- return
525
+ def _on_move_volume_chart(self, e):
526
+ if self.is_click_chart: self._move_chart(e)
527
+ return super()._on_move_volume_chart(e)
446
528
 
447
- def _change_coordinate_chart(self, e: MouseEvent):
448
- x = e.x.__int__()
449
- left, right = self._navcoordinate
529
+ def _move_chart(self, e: MouseEvent):
530
+ x = e.x.__round__(2)
531
+ left, right = self.navcoordinate
450
532
  nsub = right - left
451
533
  xsub = x - self._x_click
452
534
  xdiv = (xsub / (1200 / nsub)).__int__()
453
- if xdiv:
535
+ if not xdiv:
536
+ self.navigator.draw(self.figure.canvas.renderer)
537
+ self._draw_artist()
538
+ else:
454
539
  left, right = (left-xdiv, right-xdiv)
455
- if -1 < right and left < self.df.index[-1]:
456
- self._navcoordinate = (left, right)
540
+ if right < 0 or self.df.index[-1] < left: self._restore_region(False, True)
541
+ else:
542
+ self.navcoordinate = (left, right)
543
+ self._set_lim(left, right, simpler=True, set_ma=((right-left) < self.limit_ma))
544
+ self._set_navigator(left, right)
545
+ self.navigator.draw(self.figure.canvas.renderer)
546
+
547
+ self._draw_artist()
548
+ self.background_with_nav = self.figure.canvas.renderer.copy_from_bbox(self.figure.bbox)
549
+ self._restore_region(False, True)
457
550
  self._x_click = x
458
551
  return
459
552
 
460
- def _on_move(self, e):
461
- self._restore_region(with_nav=(not self.is_click), empty_with_nav=self.is_click)
462
553
 
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)
554
+ class BaseMixin(ChartClickMixin):
555
+ pass
492
556
 
493
- self._blit()
494
- return
495
557
 
558
+ class Chart(BaseMixin, Mixin):
559
+ def _add_collection(self):
560
+ super()._add_collection()
561
+ return self.add_artist()
496
562
 
497
- class SliderMixin(ClickMixin):
498
- pass
563
+ def _draw_artist(self):
564
+ super()._draw_artist()
565
+ return self.draw_artist()
499
566
 
567
+ def _get_segments(self):
568
+ self.generate_data()
569
+ return super()._get_segments()
500
570
 
501
- class Chart(SliderMixin, CM, Mixin):
502
571
  def _on_draw(self, e):
503
572
  super()._on_draw(e)
504
573
  return self.on_draw(e)
@@ -507,44 +576,30 @@ class Chart(SliderMixin, CM, Mixin):
507
576
  self.on_pick(e)
508
577
  return super()._on_pick(e)
509
578
 
579
+ def _set_candle_segments(self, index_start, index_end):
580
+ super()._set_candle_segments(index_start, index_end)
581
+ self.set_segment(index_start, index_end)
582
+ return
583
+
584
+ def _set_wick_segments(self, index_start, index_end, simpler=False):
585
+ super()._set_wick_segments(index_start, index_end, simpler)
586
+ self.set_segment(index_start, index_end, simpler)
587
+ return
588
+
589
+ def _set_line_segments(self, index_start, index_end, simpler=False, set_ma=True):
590
+ super()._set_line_segments(index_start, index_end, simpler, set_ma)
591
+ self.set_segment(index_start, index_end, simpler, set_ma)
592
+ return
593
+
510
594
  def _on_move(self, e):
511
595
  super()._on_move(e)
512
596
  return self.on_move(e)
513
597
 
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
598
  def _on_click(self, e):
522
599
  super()._on_click(e)
523
600
  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
601
 
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()
602
+ def on_release(self, e):
603
+ super().on_release(e)
604
+ return self.on_release(e)
550
605