seolpyo-mplchart 1.1.1__py3-none-any.whl → 1.2.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/__init__.py +9 -9
- seolpyo_mplchart/_base.py +114 -0
- seolpyo_mplchart/_cursor.py +485 -0
- seolpyo_mplchart/_draw.py +591 -0
- seolpyo_mplchart/_slider.py +608 -0
- {seolpyo_mplchart-1.1.1.dist-info → seolpyo_mplchart-1.2.0.dist-info}/METADATA +1 -1
- {seolpyo_mplchart-1.1.1.dist-info → seolpyo_mplchart-1.2.0.dist-info}/RECORD +9 -5
- {seolpyo_mplchart-1.1.1.dist-info → seolpyo_mplchart-1.2.0.dist-info}/WHEEL +0 -0
- {seolpyo_mplchart-1.1.1.dist-info → seolpyo_mplchart-1.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
import matplotlib.pyplot as plt
|
|
2
|
+
from matplotlib.axes import Axes
|
|
3
|
+
from matplotlib.collections import LineCollection
|
|
4
|
+
from matplotlib.text import Text
|
|
5
|
+
from matplotlib.backend_bases import MouseEvent, MouseButton, cursors
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
from ._base import convert_unit
|
|
9
|
+
from ._cursor import BaseMixin as BM, Mixin as M
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Mixin(M):
|
|
13
|
+
def on_click(self, e):
|
|
14
|
+
"This function works if mouse button click event active."
|
|
15
|
+
return
|
|
16
|
+
def on_release(self, e):
|
|
17
|
+
"This function works if mouse button release event active."
|
|
18
|
+
return
|
|
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)
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class CollectionMixin(PlotMixin):
|
|
92
|
+
min_distance = 30
|
|
93
|
+
color_navigator_line = '#1e78ff'
|
|
94
|
+
color_navigator_cover = 'k'
|
|
95
|
+
|
|
96
|
+
def _add_collection(self):
|
|
97
|
+
super()._add_collection()
|
|
98
|
+
|
|
99
|
+
self.collection_slider = LineCollection([], animated=True)
|
|
100
|
+
self.ax_slider.add_artist(self.collection_slider)
|
|
101
|
+
|
|
102
|
+
# 슬라이더 네비게이터
|
|
103
|
+
self.navigator = LineCollection([], animated=True, edgecolors=[self.color_navigator_cover, self.color_navigator_line], alpha=(0.3, 1.0))
|
|
104
|
+
self.ax_slider.add_artist(self.navigator)
|
|
105
|
+
|
|
106
|
+
lineKwargs = {'edgecolor': 'k', 'linewidth': 1, 'linestyle': '-'}
|
|
107
|
+
lineKwargs.update(self.lineKwargs)
|
|
108
|
+
lineKwargs.update({'segments': [], 'animated': True})
|
|
109
|
+
|
|
110
|
+
self.slider_vline = LineCollection(**lineKwargs)
|
|
111
|
+
self.ax_slider.add_artist(self.slider_vline)
|
|
112
|
+
|
|
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'))
|
|
118
|
+
|
|
119
|
+
self.text_slider = Text(**textKwargs, horizontalalignment='center', verticalalignment='top')
|
|
120
|
+
self.ax_slider.add_artist(self.text_slider)
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
def _get_segments(self):
|
|
124
|
+
super()._get_segments()
|
|
125
|
+
|
|
126
|
+
keys = []
|
|
127
|
+
for i in reversed(self.list_ma):
|
|
128
|
+
keys.append('_x')
|
|
129
|
+
keys.append(f'ma{i}')
|
|
130
|
+
|
|
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
|
|
135
|
+
|
|
136
|
+
def _get_color_segment(self):
|
|
137
|
+
super()._get_color_segment()
|
|
138
|
+
|
|
139
|
+
self.collection_slider.set_edgecolor(self.edgecolor_ma + [self.color_priceline])
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
def change_background_color(self, color):
|
|
143
|
+
super().change_background_color(color)
|
|
144
|
+
|
|
145
|
+
self.ax_slider.set_facecolor(color)
|
|
146
|
+
self.text_slider.set_backgroundcolor(color)
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
def change_tick_color(self, color):
|
|
150
|
+
super().change_tick_color(color)
|
|
151
|
+
|
|
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)
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
def change_text_color(self, color):
|
|
157
|
+
super().change_text_color(color)
|
|
158
|
+
|
|
159
|
+
self.text_slider.set_color(color)
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
def change_line_color(self, color):
|
|
163
|
+
super().change_line_color(color)
|
|
164
|
+
|
|
165
|
+
self.text_slider.get_bbox_patch().set_edgecolor(color)
|
|
166
|
+
self.slider_vline.set_edgecolor(color)
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class NavigatorMixin(CollectionMixin):
|
|
171
|
+
def _set_slider_lim(self):
|
|
172
|
+
xmax = self.list_index[-1]
|
|
173
|
+
# 슬라이더 xlim
|
|
174
|
+
xdistance = xmax / 30
|
|
175
|
+
self.slider_xmin, self.slider_xmax = (-xdistance, xmax + xdistance)
|
|
176
|
+
self.ax_slider.set_xlim(self.slider_xmin, self.slider_xmax)
|
|
177
|
+
|
|
178
|
+
# 슬라이더 ylim
|
|
179
|
+
ymin, ymax = (self.df[self.low].min(), self.df[self.high].max())
|
|
180
|
+
ysub = ymax - ymin
|
|
181
|
+
self.sldier_ymiddle = ymin + (ysub / 2)
|
|
182
|
+
ydistance = ysub / 5
|
|
183
|
+
self.slider_ymin, self.slider_ymax = (ymin-ydistance, ymax+ydistance)
|
|
184
|
+
self.ax_slider.set_ylim(self.slider_ymin, self.slider_ymax)
|
|
185
|
+
|
|
186
|
+
# 슬라이더 텍스트 y
|
|
187
|
+
self.text_slider.set_y(ymax)
|
|
188
|
+
|
|
189
|
+
self.navigator.set_linewidth([ysub, 5])
|
|
190
|
+
|
|
191
|
+
# 네비게이터 라인 선택 범위
|
|
192
|
+
xsub = self.slider_xmax - self.slider_xmin
|
|
193
|
+
self._navLineWidth = xsub * 8 / 1_000
|
|
194
|
+
if self._navLineWidth < 1: self._navLineWidth = 1
|
|
195
|
+
self._navLineWidth_half = self._navLineWidth / 2
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
def _set_navigator(self, navmin, navmax):
|
|
199
|
+
navseg = [
|
|
200
|
+
(
|
|
201
|
+
(self.slider_xmin, self.sldier_ymiddle),
|
|
202
|
+
(navmin, self.sldier_ymiddle)
|
|
203
|
+
),
|
|
204
|
+
(
|
|
205
|
+
(navmin, self.slider_ymin),
|
|
206
|
+
(navmin, self.slider_ymax)
|
|
207
|
+
),
|
|
208
|
+
(
|
|
209
|
+
(navmax, self.sldier_ymiddle),
|
|
210
|
+
(self.slider_xmax, self.sldier_ymiddle)
|
|
211
|
+
),
|
|
212
|
+
(
|
|
213
|
+
(navmax, self.slider_ymin),
|
|
214
|
+
(navmax, self.slider_ymax)
|
|
215
|
+
),
|
|
216
|
+
]
|
|
217
|
+
|
|
218
|
+
self.navigator.set_segments(navseg)
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class DataMixin(NavigatorMixin):
|
|
223
|
+
navcoordinate = (0, 0)
|
|
224
|
+
|
|
225
|
+
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):
|
|
226
|
+
self._generate_data(df, sort_df, calc_ma, set_candlecolor, set_volumecolor, calc_info, *args, **kwargs)
|
|
227
|
+
self._get_segments()
|
|
228
|
+
|
|
229
|
+
vmin, vmax = self.navcoordinate
|
|
230
|
+
if change_lim or (vmax-vmin) < self.min_distance:
|
|
231
|
+
vmin, vmax = self.get_default_lim()
|
|
232
|
+
self.navcoordinate = (vmin, vmax)
|
|
233
|
+
|
|
234
|
+
self._set_lim(vmin, vmax)
|
|
235
|
+
self._set_slider_lim()
|
|
236
|
+
self._set_navigator(vmin, vmax)
|
|
237
|
+
|
|
238
|
+
self._length_text = self.df[(self.volume if self.volume else self.high)].apply(lambda x: len(str(x))).max()
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
def get_default_lim(self):
|
|
242
|
+
xmax = self.list_index[-1]
|
|
243
|
+
xmin = xmax - 120
|
|
244
|
+
if xmin < 0: xmin = 0
|
|
245
|
+
return (xmin, xmax)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class BackgroundMixin(DataMixin):
|
|
249
|
+
def _copy_bbox(self):
|
|
250
|
+
renderer = self.figure.canvas.renderer
|
|
251
|
+
|
|
252
|
+
self.ax_slider.xaxis.draw(renderer)
|
|
253
|
+
self.ax_slider.yaxis.draw(renderer)
|
|
254
|
+
self.collection_slider.draw(renderer)
|
|
255
|
+
self.background_emtpy = renderer.copy_from_bbox(self.figure.bbox)
|
|
256
|
+
|
|
257
|
+
self._draw_artist()
|
|
258
|
+
self.background = renderer.copy_from_bbox(self.figure.bbox)
|
|
259
|
+
|
|
260
|
+
self.navigator.draw(self.figure.canvas.renderer)
|
|
261
|
+
self.background_with_nav = renderer.copy_from_bbox(self.figure.bbox)
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
def _restore_region(self, is_empty=False, with_nav=True):
|
|
265
|
+
if not self.background: self._create_background()
|
|
266
|
+
|
|
267
|
+
if is_empty: self.figure.canvas.renderer.restore_region(self.background_emtpy)
|
|
268
|
+
elif with_nav: self.figure.canvas.renderer.restore_region(self.background_with_nav)
|
|
269
|
+
else: self.figure.canvas.renderer.restore_region(self.background)
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class MouseMoveMixin(BackgroundMixin):
|
|
274
|
+
in_slider = False
|
|
275
|
+
is_click_slider = False
|
|
276
|
+
|
|
277
|
+
def _on_move(self, e):
|
|
278
|
+
self._on_move_action(e)
|
|
279
|
+
|
|
280
|
+
self._restore_region((self.is_click_slider and self.in_slider))
|
|
281
|
+
|
|
282
|
+
if self.in_slider:
|
|
283
|
+
self._on_move_slider(e)
|
|
284
|
+
elif self.in_price_chart:
|
|
285
|
+
self._on_move_price_chart(e)
|
|
286
|
+
elif self.in_volume_chart:
|
|
287
|
+
self._on_move_volume_chart(e)
|
|
288
|
+
|
|
289
|
+
self._blit()
|
|
290
|
+
return
|
|
291
|
+
|
|
292
|
+
def _on_move_action(self, e: MouseEvent):
|
|
293
|
+
self._check_ax(e)
|
|
294
|
+
|
|
295
|
+
self.intx = None
|
|
296
|
+
if self.in_slider or self.in_price_chart or self.in_volume_chart: self._get_x(e)
|
|
297
|
+
|
|
298
|
+
self._change_cursor(e)
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
def _change_cursor(self, e: MouseEvent):
|
|
302
|
+
# 마우스 커서 변경
|
|
303
|
+
if self.is_click_slider: return
|
|
304
|
+
elif not self.in_slider:
|
|
305
|
+
self.figure.canvas.set_cursor(cursors.POINTER)
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
navleft, navright = self.navcoordinate
|
|
309
|
+
if navleft == navright: return
|
|
310
|
+
|
|
311
|
+
x = e.xdata
|
|
312
|
+
leftmin, leftmax = (navleft-self._navLineWidth, navleft+self._navLineWidth_half)
|
|
313
|
+
rightmin, rightmax = (navright-self._navLineWidth_half, navright+self._navLineWidth)
|
|
314
|
+
if x < leftmin: self.figure.canvas.set_cursor(cursors.POINTER)
|
|
315
|
+
elif x < leftmax: self.figure.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
|
|
316
|
+
elif x < rightmin: self.figure.canvas.set_cursor(cursors.MOVE)
|
|
317
|
+
elif x < rightmax: self.figure.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
|
|
318
|
+
else: self.figure.canvas.set_cursor(cursors.POINTER)
|
|
319
|
+
return
|
|
320
|
+
|
|
321
|
+
def _check_ax(self, e: MouseEvent):
|
|
322
|
+
ax = e.inaxes
|
|
323
|
+
if not ax or e.xdata is None or e.ydata is None:
|
|
324
|
+
self.in_slider, self.in_price_chart, self.in_volume_chart = (False, False, False)
|
|
325
|
+
else:
|
|
326
|
+
self.in_slider = ax is self.ax_slider
|
|
327
|
+
self.in_price_chart = False if self.in_slider else ax is self.ax_price
|
|
328
|
+
self.in_volume_chart = False if (self.in_slider or self.in_price_chart) else ax is self.ax_volume
|
|
329
|
+
return
|
|
330
|
+
|
|
331
|
+
def _on_move_slider(self, e: MouseEvent):
|
|
332
|
+
x = e.xdata
|
|
333
|
+
|
|
334
|
+
if self.intx is not None:
|
|
335
|
+
renderer = self.figure.canvas.renderer
|
|
336
|
+
self.slider_vline.set_segments([((x, self.slider_ymin), (x, self.slider_ymax))])
|
|
337
|
+
self.slider_vline.draw(renderer)
|
|
338
|
+
|
|
339
|
+
if self.in_slider:
|
|
340
|
+
self.text_slider.set_text(f'{self.df[self.date][self.intx]}')
|
|
341
|
+
self.text_slider.set_x(x)
|
|
342
|
+
self.text_slider.draw(renderer)
|
|
343
|
+
return
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
class ClickMixin(MouseMoveMixin):
|
|
347
|
+
x_click = None
|
|
348
|
+
is_move = False
|
|
349
|
+
click_navleft, click_navright = (False, False)
|
|
350
|
+
|
|
351
|
+
def _connect_event(self):
|
|
352
|
+
super()._connect_event()
|
|
353
|
+
|
|
354
|
+
self.figure.canvas.mpl_connect('button_press_event', lambda x: self._on_click(x))
|
|
355
|
+
return
|
|
356
|
+
|
|
357
|
+
def _on_click(self, e: MouseEvent):
|
|
358
|
+
if self.in_slider: self._on_click_slider(e)
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
def _on_click_slider(self, e: MouseEvent):
|
|
362
|
+
if self.is_click_slider or e.button != MouseButton.LEFT: return
|
|
363
|
+
|
|
364
|
+
self.background_with_nav_pre = self.background_with_nav
|
|
365
|
+
|
|
366
|
+
self.is_click_slider = True
|
|
367
|
+
self.figure.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
|
|
368
|
+
|
|
369
|
+
x = e.xdata.__int__()
|
|
370
|
+
navmin, navmax = self.navcoordinate
|
|
371
|
+
|
|
372
|
+
leftmin, leftmax = (navmin-self._navLineWidth, navmin+self._navLineWidth_half)
|
|
373
|
+
rightmin, rightmax = (navmax-self._navLineWidth_half, navmax+self._navLineWidth)
|
|
374
|
+
|
|
375
|
+
grater_than_left, less_then_right = (leftmax < x, x < rightmin)
|
|
376
|
+
if grater_than_left and less_then_right:
|
|
377
|
+
self.is_move = True
|
|
378
|
+
self.x_click = x
|
|
379
|
+
else:
|
|
380
|
+
if not grater_than_left and leftmin <= x:
|
|
381
|
+
self.click_navleft = True
|
|
382
|
+
self.x_click = navmax
|
|
383
|
+
elif not less_then_right and x <= rightmax:
|
|
384
|
+
self.click_navright = True
|
|
385
|
+
self.x_click = navmin
|
|
386
|
+
else:
|
|
387
|
+
self.x_click = x
|
|
388
|
+
return
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
class SliderSelectMixin(ClickMixin):
|
|
392
|
+
limit_ma = 8_000
|
|
393
|
+
|
|
394
|
+
def _on_move_slider(self, e):
|
|
395
|
+
if self.is_click_slider: self._set_navcoordinate(e)
|
|
396
|
+
return super()._on_move_slider(e)
|
|
397
|
+
|
|
398
|
+
def _set_navcoordinate(self, e: MouseEvent):
|
|
399
|
+
x = e.xdata.__int__()
|
|
400
|
+
navmin, navmax = self.navcoordinate
|
|
401
|
+
|
|
402
|
+
if self.is_move:
|
|
403
|
+
xsub = self.x_click - x
|
|
404
|
+
navmin, navmax = (navmin-xsub, navmax-xsub)
|
|
405
|
+
|
|
406
|
+
# 값 보정
|
|
407
|
+
if navmax < 0: navmin, navmax = (navmin-navmax, 0)
|
|
408
|
+
if self.list_index[-1] < navmin: navmin, navmax = (self.list_index[-1], self.list_index[-1] + (navmax-navmin))
|
|
409
|
+
|
|
410
|
+
self.navcoordinate = (navmin, navmax)
|
|
411
|
+
self.x_click = x
|
|
412
|
+
|
|
413
|
+
self._set_lim(navmin, navmax, simpler=True, set_ma=(navmax-navmin < self.limit_ma))
|
|
414
|
+
|
|
415
|
+
self._set_navigator(navmin, navmax)
|
|
416
|
+
self.navigator.draw(self.figure.canvas.renderer)
|
|
417
|
+
|
|
418
|
+
self._draw_artist()
|
|
419
|
+
self.background_with_nav = self.figure.canvas.renderer.copy_from_bbox(self.figure.bbox)
|
|
420
|
+
self._restore_region(False, True)
|
|
421
|
+
else:
|
|
422
|
+
navmin, navmax = (x, self.x_click) if x < self.x_click else (self.x_click, x)
|
|
423
|
+
|
|
424
|
+
# 슬라이더가 차트를 벗어나지 않도록 선택 영역 제한
|
|
425
|
+
if navmax < 0 or self.list_index[-1] < navmin:
|
|
426
|
+
seg = self.navigator.get_segments()
|
|
427
|
+
navmin, navmax = (int(seg[1][0][0]), int(seg[3][0][0]))
|
|
428
|
+
|
|
429
|
+
nsub = navmax - navmin
|
|
430
|
+
if nsub < self.min_distance:
|
|
431
|
+
self._restore_region(False, False)
|
|
432
|
+
self._set_navigator(navmin, navmax)
|
|
433
|
+
self.navigator.draw(self.figure.canvas.renderer)
|
|
434
|
+
else:
|
|
435
|
+
self._set_lim(navmin, navmax, simpler=True, set_ma=(nsub < self.limit_ma))
|
|
436
|
+
self._set_navigator(navmin, navmax)
|
|
437
|
+
|
|
438
|
+
self.navigator.draw(self.figure.canvas.renderer)
|
|
439
|
+
|
|
440
|
+
self._draw_artist()
|
|
441
|
+
self.background_with_nav = self.figure.canvas.renderer.copy_from_bbox(self.figure.bbox)
|
|
442
|
+
self._restore_region(False, True)
|
|
443
|
+
return
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
class ReleaseMixin(SliderSelectMixin):
|
|
447
|
+
def _connect_event(self):
|
|
448
|
+
super()._connect_event()
|
|
449
|
+
|
|
450
|
+
self.figure.canvas.mpl_connect('button_release_event', lambda x: self._on_release(x))
|
|
451
|
+
return
|
|
452
|
+
|
|
453
|
+
def _on_release(self, e: MouseEvent):
|
|
454
|
+
if self.in_slider and self.is_click_slider: self._on_release_slider(e)
|
|
455
|
+
return
|
|
456
|
+
|
|
457
|
+
def _on_release_slider(self, e: MouseEvent):
|
|
458
|
+
if not self.is_move:
|
|
459
|
+
seg = self.navigator.get_segments()
|
|
460
|
+
navmin, navmax = (int(seg[1][0][0]), int(seg[3][0][0]))
|
|
461
|
+
nsub = navmax - navmin
|
|
462
|
+
if self.min_distance <= nsub: self.navcoordinate = (navmin, navmax)
|
|
463
|
+
else:
|
|
464
|
+
self.background_with_nav = self.background_with_nav_pre
|
|
465
|
+
navmin, navmax = self.navcoordinate
|
|
466
|
+
self._set_lim(navmin, navmax, simpler=True, set_ma=(nsub < self.limit_ma))
|
|
467
|
+
self._restore_region(False, True)
|
|
468
|
+
self._blit()
|
|
469
|
+
self._set_navigator(*self.navcoordinate)
|
|
470
|
+
|
|
471
|
+
self.is_click_slider = False
|
|
472
|
+
self.is_move = False
|
|
473
|
+
self.click_navleft, self.click_navright = (False, False)
|
|
474
|
+
return
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
class ChartClickMixin(ReleaseMixin):
|
|
478
|
+
is_click_chart = False
|
|
479
|
+
|
|
480
|
+
def _on_click(self, e: MouseEvent):
|
|
481
|
+
if self.in_price_chart or self.in_volume_chart: self._on_click_chart(e)
|
|
482
|
+
elif self.in_slider: self._on_click_slider(e)
|
|
483
|
+
return
|
|
484
|
+
|
|
485
|
+
def _on_click_chart(self, e: MouseEvent):
|
|
486
|
+
if self.is_click_chart: return
|
|
487
|
+
|
|
488
|
+
self.is_click_chart = True
|
|
489
|
+
self._x_click = e.x.__round__(2)
|
|
490
|
+
self.figure.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
|
|
491
|
+
return
|
|
492
|
+
|
|
493
|
+
def _on_release(self, e):
|
|
494
|
+
if self.is_click_chart and (self.in_price_chart or self.in_volume_chart): self._on_release_chart(e)
|
|
495
|
+
elif self.is_click_slider and self.in_slider: self._on_release_slider(e)
|
|
496
|
+
return
|
|
497
|
+
|
|
498
|
+
def _on_release_chart(self, e):
|
|
499
|
+
self.is_click_chart = False
|
|
500
|
+
self.figure.canvas.set_cursor(cursors.POINTER)
|
|
501
|
+
return
|
|
502
|
+
|
|
503
|
+
def _change_cursor(self, e):
|
|
504
|
+
if self.is_click_chart: return
|
|
505
|
+
return super()._change_cursor(e)
|
|
506
|
+
|
|
507
|
+
def _on_move(self, e):
|
|
508
|
+
self._on_move_action(e)
|
|
509
|
+
|
|
510
|
+
need_slider_action = self.is_click_slider and self.in_slider
|
|
511
|
+
need_chart_action = False if need_slider_action else self.is_click_chart and (self.in_price_chart or self.in_volume_chart)
|
|
512
|
+
self._restore_region((need_slider_action or need_chart_action))
|
|
513
|
+
|
|
514
|
+
if self.in_slider:
|
|
515
|
+
self._on_move_slider(e)
|
|
516
|
+
elif self.in_price_chart:
|
|
517
|
+
self._on_move_price_chart(e)
|
|
518
|
+
elif self.in_volume_chart:
|
|
519
|
+
self._on_move_volume_chart(e)
|
|
520
|
+
|
|
521
|
+
self._blit()
|
|
522
|
+
return
|
|
523
|
+
|
|
524
|
+
def _on_move_price_chart(self, e):
|
|
525
|
+
if self.is_click_chart: self._move_chart(e)
|
|
526
|
+
return super()._on_move_price_chart(e)
|
|
527
|
+
|
|
528
|
+
def _on_move_volume_chart(self, e):
|
|
529
|
+
if self.is_click_chart: self._move_chart(e)
|
|
530
|
+
return super()._on_move_volume_chart(e)
|
|
531
|
+
|
|
532
|
+
def _move_chart(self, e: MouseEvent):
|
|
533
|
+
x = e.x.__round__(2)
|
|
534
|
+
left, right = self.navcoordinate
|
|
535
|
+
nsub = right - left
|
|
536
|
+
xsub = x - self._x_click
|
|
537
|
+
xdiv = (xsub / (1200 / nsub)).__int__()
|
|
538
|
+
if not xdiv:
|
|
539
|
+
self.navigator.draw(self.figure.canvas.renderer)
|
|
540
|
+
self._draw_artist()
|
|
541
|
+
else:
|
|
542
|
+
left, right = (left-xdiv, right-xdiv)
|
|
543
|
+
if right < 0 or self.df.index[-1] < left: self._restore_region(False, True)
|
|
544
|
+
else:
|
|
545
|
+
self.navcoordinate = (left, right)
|
|
546
|
+
self._set_lim(left, right, simpler=True, set_ma=((right-left) < self.limit_ma))
|
|
547
|
+
self._set_navigator(left, right)
|
|
548
|
+
self.navigator.draw(self.figure.canvas.renderer)
|
|
549
|
+
|
|
550
|
+
self._draw_artist()
|
|
551
|
+
self.background_with_nav = self.figure.canvas.renderer.copy_from_bbox(self.figure.bbox)
|
|
552
|
+
self._restore_region(False, True)
|
|
553
|
+
self._x_click = x
|
|
554
|
+
return
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
class BaseMixin(ChartClickMixin):
|
|
558
|
+
pass
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
class Chart(BaseMixin, Mixin):
|
|
562
|
+
def _add_collection(self):
|
|
563
|
+
super()._add_collection()
|
|
564
|
+
return self.add_artist()
|
|
565
|
+
|
|
566
|
+
def _draw_artist(self):
|
|
567
|
+
super()._draw_artist()
|
|
568
|
+
return self.draw_artist()
|
|
569
|
+
|
|
570
|
+
def _get_segments(self):
|
|
571
|
+
self.generate_data()
|
|
572
|
+
return super()._get_segments()
|
|
573
|
+
|
|
574
|
+
def _on_draw(self, e):
|
|
575
|
+
super()._on_draw(e)
|
|
576
|
+
return self.on_draw(e)
|
|
577
|
+
|
|
578
|
+
def _on_pick(self, e):
|
|
579
|
+
self.on_pick(e)
|
|
580
|
+
return super()._on_pick(e)
|
|
581
|
+
|
|
582
|
+
def _set_candle_segments(self, index_start, index_end):
|
|
583
|
+
super()._set_candle_segments(index_start, index_end)
|
|
584
|
+
self.set_segment(index_start, index_end)
|
|
585
|
+
return
|
|
586
|
+
|
|
587
|
+
def _set_wick_segments(self, index_start, index_end, simpler=False):
|
|
588
|
+
super()._set_wick_segments(index_start, index_end, simpler)
|
|
589
|
+
self.set_segment(index_start, index_end, simpler)
|
|
590
|
+
return
|
|
591
|
+
|
|
592
|
+
def _set_line_segments(self, index_start, index_end, simpler=False, set_ma=True):
|
|
593
|
+
super()._set_line_segments(index_start, index_end, simpler, set_ma)
|
|
594
|
+
self.set_segment(index_start, index_end, simpler, set_ma)
|
|
595
|
+
return
|
|
596
|
+
|
|
597
|
+
def _on_move(self, e):
|
|
598
|
+
super()._on_move(e)
|
|
599
|
+
return self.on_move(e)
|
|
600
|
+
|
|
601
|
+
def _on_click(self, e):
|
|
602
|
+
super()._on_click(e)
|
|
603
|
+
return self.on_click(e)
|
|
604
|
+
|
|
605
|
+
def on_release(self, e):
|
|
606
|
+
super().on_release(e)
|
|
607
|
+
return self.on_release(e)
|
|
608
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: seolpyo-mplchart
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Fast candlestick chart using Python. Includes navigator, slider, navigation, and text information display functions
|
|
5
5
|
Author-email: white-seolpyo <white-seolpyo@naver.com>
|
|
6
6
|
License: MIT License
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
seolpyo_mplchart/__init__.py,sha256=
|
|
1
|
+
seolpyo_mplchart/__init__.py,sha256=ruj3FHpJD6PxBxWrzr-eIuhoGLiwXNJOdpBWHdxlOfE,18189
|
|
2
|
+
seolpyo_mplchart/_base.py,sha256=0qdImsIMPzTTkkHzPv479BVe_ojrn45FidGE46eT5x4,3797
|
|
3
|
+
seolpyo_mplchart/_cursor.py,sha256=SagSVM46ATmSswB_klmlIMtw9tX3nYuiI1FKVYtQbYk,20416
|
|
4
|
+
seolpyo_mplchart/_draw.py,sha256=J2gthHuDifXwVtoSD-prAfPYxoKj-YC4cVp1DW7Xf3g,22107
|
|
5
|
+
seolpyo_mplchart/_slider.py,sha256=eiDDEhRB20HiQArpkmPqYmohg-4f4Z31ahIyohtMqSQ,22292
|
|
2
6
|
seolpyo_mplchart/base.py,sha256=0qdImsIMPzTTkkHzPv479BVe_ojrn45FidGE46eT5x4,3797
|
|
3
7
|
seolpyo_mplchart/cursor.py,sha256=qq1WJJa7vCE5C2XeGBECt2XqxR_WxfybZZ5u6zVx5Ps,20415
|
|
4
8
|
seolpyo_mplchart/draw.py,sha256=MiqDhbMJOSlWD6qdAghsyrZwCixcwm4nfmiinvwtt-o,21520
|
|
@@ -7,7 +11,7 @@ seolpyo_mplchart/test.py,sha256=TFnIXphJsl-B7iIhBh7-PZKUz2Gjh7mwNwrk8aUS4SA,180
|
|
|
7
11
|
seolpyo_mplchart/utils.py,sha256=a3XycRBTndrsjBw_1VKTxbSvOGpVYXHRK87v7azgRe8,1433
|
|
8
12
|
seolpyo_mplchart/data/apple.txt,sha256=0izAfweu1lLsC0IwVthdVlo9reG8KGbKGTSX5knI5Zc,1380864
|
|
9
13
|
seolpyo_mplchart/data/samsung.txt,sha256=UejaSkbzr4E5K3lkelCT0yJiWUPfmViBEaTyoXyphIs,2476424
|
|
10
|
-
seolpyo_mplchart-1.
|
|
11
|
-
seolpyo_mplchart-1.
|
|
12
|
-
seolpyo_mplchart-1.
|
|
13
|
-
seolpyo_mplchart-1.
|
|
14
|
+
seolpyo_mplchart-1.2.0.dist-info/METADATA,sha256=k3XKO7EUJSlOWtkhexYeffYID7OMlkJj_zKjKF7bZHA,2352
|
|
15
|
+
seolpyo_mplchart-1.2.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
16
|
+
seolpyo_mplchart-1.2.0.dist-info/top_level.txt,sha256=KgqFn7rKWize7OjMaTCHxKm9ie6vqnyb5c8fN7y_tSo,17
|
|
17
|
+
seolpyo_mplchart-1.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|