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.
- seolpyo_mplchart/__init__.py +164 -99
- seolpyo_mplchart/_base.py +117 -0
- seolpyo_mplchart/_chart/__init__.py +137 -0
- seolpyo_mplchart/_chart/_base.py +217 -0
- seolpyo_mplchart/_chart/_cursor/__init__.py +2 -0
- seolpyo_mplchart/_chart/_cursor/_artist.py +217 -0
- seolpyo_mplchart/_chart/_cursor/_cursor.py +165 -0
- seolpyo_mplchart/_chart/_cursor/_info.py +187 -0
- seolpyo_mplchart/_chart/_draw/__init__.py +2 -0
- seolpyo_mplchart/_chart/_draw/_artist.py +50 -0
- seolpyo_mplchart/_chart/_draw/_data.py +314 -0
- seolpyo_mplchart/_chart/_draw/_draw.py +103 -0
- seolpyo_mplchart/_chart/_draw/_lim.py +265 -0
- seolpyo_mplchart/_chart/_slider/__init__.py +1 -0
- seolpyo_mplchart/_chart/_slider/_base.py +268 -0
- seolpyo_mplchart/_chart/_slider/_data.py +105 -0
- seolpyo_mplchart/_chart/_slider/_mouse.py +176 -0
- seolpyo_mplchart/_chart/_slider/_nav.py +204 -0
- seolpyo_mplchart/_chart/test.py +121 -0
- seolpyo_mplchart/_config/__init__.py +3 -0
- seolpyo_mplchart/_config/ax.py +28 -0
- seolpyo_mplchart/_config/candle.py +30 -0
- seolpyo_mplchart/_config/config.py +21 -0
- seolpyo_mplchart/_config/cursor.py +49 -0
- seolpyo_mplchart/_config/figure.py +41 -0
- seolpyo_mplchart/_config/format.py +51 -0
- seolpyo_mplchart/_config/ma.py +15 -0
- seolpyo_mplchart/_config/slider/__init__.py +2 -0
- seolpyo_mplchart/_config/slider/config.py +24 -0
- seolpyo_mplchart/_config/slider/figure.py +20 -0
- seolpyo_mplchart/_config/slider/nav.py +9 -0
- seolpyo_mplchart/_config/unit.py +19 -0
- seolpyo_mplchart/_config/utils.py +67 -0
- seolpyo_mplchart/_config/volume.py +26 -0
- seolpyo_mplchart/_cursor.py +559 -0
- seolpyo_mplchart/_draw.py +634 -0
- seolpyo_mplchart/_slider.py +634 -0
- seolpyo_mplchart/base.py +70 -67
- seolpyo_mplchart/cursor.py +308 -271
- seolpyo_mplchart/draw.py +449 -237
- seolpyo_mplchart/slider.py +451 -396
- seolpyo_mplchart/test.py +173 -24
- seolpyo_mplchart/utils.py +15 -4
- seolpyo_mplchart/xl_to_dict.py +47 -0
- seolpyo_mplchart-2.0.0.3.dist-info/METADATA +710 -0
- seolpyo_mplchart-2.0.0.3.dist-info/RECORD +50 -0
- {seolpyo_mplchart-0.1.3.1.dist-info → seolpyo_mplchart-2.0.0.3.dist-info}/WHEEL +1 -1
- seolpyo_mplchart-0.1.3.1.dist-info/METADATA +0 -49
- seolpyo_mplchart-0.1.3.1.dist-info/RECORD +0 -13
- {seolpyo_mplchart-0.1.3.1.dist-info → seolpyo_mplchart-2.0.0.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
from matplotlib.backend_bases import PickEvent
|
|
2
|
+
from matplotlib.text import Text
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from ._data import BaseMixin as Base
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EventMixin(Base):
|
|
9
|
+
def __init__(self, *args, **kwargs):
|
|
10
|
+
super().__init__(*args, **kwargs)
|
|
11
|
+
|
|
12
|
+
self.connect_events()
|
|
13
|
+
return
|
|
14
|
+
|
|
15
|
+
def connect_events(self):
|
|
16
|
+
self._connect_events()
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
def _connect_events(self):
|
|
20
|
+
self.figure.canvas.mpl_connect('pick_event', lambda x: self.on_pick(x))
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
def on_pick(self, e):
|
|
24
|
+
self._on_pick(e)
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
def _on_pick(self, e):
|
|
28
|
+
self._pick_ma_action(e)
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
def _pick_ma_action(self, e: PickEvent):
|
|
32
|
+
handle = e.artist
|
|
33
|
+
ax = handle.axes
|
|
34
|
+
# print(f'{(ax is self.ax_legend)=}')
|
|
35
|
+
if ax is not self.ax_legend:
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
visible = handle.get_alpha() == 0.2
|
|
39
|
+
handle.set_alpha(1.0 if visible else 0.2)
|
|
40
|
+
|
|
41
|
+
n = int(handle.get_label())
|
|
42
|
+
if visible:
|
|
43
|
+
self._visible_ma = {i for i in self.CONFIG.MA.ma_list if i in self._visible_ma or i == n}
|
|
44
|
+
else:
|
|
45
|
+
self._visible_ma = {i for i in self._visible_ma if i != n}
|
|
46
|
+
|
|
47
|
+
alphas = [(1 if i in self._visible_ma else 0) for i in reversed(self.CONFIG.MA.ma_list)]
|
|
48
|
+
self.collection_ma.set_alpha(alphas)
|
|
49
|
+
|
|
50
|
+
self.figure.canvas.draw()
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class LimMixin(EventMixin):
|
|
55
|
+
candle_on_ma = True
|
|
56
|
+
|
|
57
|
+
def _set_data(self, df, *args, **kwargs):
|
|
58
|
+
super()._set_data(df, *args, **kwargs)
|
|
59
|
+
|
|
60
|
+
self.set_segments()
|
|
61
|
+
|
|
62
|
+
vmin, vmax = self.get_default_lim()
|
|
63
|
+
self.axis(vmin, xmax=vmax, simpler=False, draw_ma=True)
|
|
64
|
+
|
|
65
|
+
# 노출 영역에 맞게 collection segment 조정하기
|
|
66
|
+
self.set_collections(self.vxmin, xmax=self.vxmax, simpler=False, draw_ma=True)
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
def axis(self, xmin, *, xmax, simpler=False, draw_ma=True):
|
|
70
|
+
self._axis(xmin, xmax=xmax, simpler=simpler, draw_ma=draw_ma)
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
def _convert_xlim(self, xmin, *, xmax):
|
|
74
|
+
if xmin < 0:
|
|
75
|
+
xmin = 0
|
|
76
|
+
if xmax < 1:
|
|
77
|
+
xmax = 1
|
|
78
|
+
return (xmin, xmax)
|
|
79
|
+
|
|
80
|
+
def _get_price_ylim(self, xmin, *, xmax):
|
|
81
|
+
ymin, ymax = (self.df['low'][xmin:xmax].min(), self.df['high'][xmin:xmax].max())
|
|
82
|
+
ysub = ymax - ymin
|
|
83
|
+
if ysub < 15:
|
|
84
|
+
ysub = 15
|
|
85
|
+
yspace = ysub / 14
|
|
86
|
+
ymin = ymin - yspace
|
|
87
|
+
ymax = ymax + yspace
|
|
88
|
+
if ymin == ymax:
|
|
89
|
+
if ymax:
|
|
90
|
+
ymin, ymax = (round(ymax * 0.9), round(ymax * 1.1))
|
|
91
|
+
else:
|
|
92
|
+
ymin, ymax = (0, 10)
|
|
93
|
+
return (ymin, ymax)
|
|
94
|
+
|
|
95
|
+
def _get_volume_ylim(self, xmin, *, xmax):
|
|
96
|
+
if not self.key_volume:
|
|
97
|
+
ymax = 1
|
|
98
|
+
else:
|
|
99
|
+
series = self.df['volume'][xmin:xmax]
|
|
100
|
+
# print(f'{series=}')
|
|
101
|
+
ymax = series.max()
|
|
102
|
+
yspace = ymax / 5
|
|
103
|
+
ymax = ymax + yspace
|
|
104
|
+
if ymax < 1:
|
|
105
|
+
ymax = 1
|
|
106
|
+
# print(f'{ymax=}')
|
|
107
|
+
return (0, ymax)
|
|
108
|
+
|
|
109
|
+
def _axis(self, xmin, xmax, simpler=False, draw_ma=True):
|
|
110
|
+
self.set_collections(xmin, xmax=xmax, simpler=simpler, draw_ma=draw_ma)
|
|
111
|
+
|
|
112
|
+
self.vxmin, self.vxmax = (xmin, xmax)
|
|
113
|
+
xmin, xmax = self._convert_xlim(xmin, xmax=xmax)
|
|
114
|
+
|
|
115
|
+
self.price_ymin, self.price_ymax = self._get_price_ylim(xmin, xmax=xmax)
|
|
116
|
+
|
|
117
|
+
# 주가 차트 xlim
|
|
118
|
+
self.ax_price.set_xlim(self.vxmin, xmax)
|
|
119
|
+
# 주가 차트 ylim
|
|
120
|
+
self.ax_price.set_ylim(self.price_ymin, self.price_ymax)
|
|
121
|
+
|
|
122
|
+
# 거래량 차트 xlim
|
|
123
|
+
self.ax_volume.set_xlim(self.vxmin, xmax)
|
|
124
|
+
self.key_volume_ymax = 1
|
|
125
|
+
if self.key_volume:
|
|
126
|
+
_, self.key_volume_ymax = self._get_volume_ylim(xmin, xmax=xmax)
|
|
127
|
+
# 거래량 차트 ylim
|
|
128
|
+
self.ax_volume.set_ylim(0, self.key_volume_ymax)
|
|
129
|
+
|
|
130
|
+
# x축에 일부 date 표시하기
|
|
131
|
+
# x tick 외부 눈금 표시
|
|
132
|
+
self.ax_volume.xaxis.set_ticks_position('bottom')
|
|
133
|
+
xhalf = (xmax-xmin) // 2
|
|
134
|
+
xmiddle = xmin + xhalf
|
|
135
|
+
indices = []
|
|
136
|
+
aligns = ['left', 'center', 'center']
|
|
137
|
+
for idx in [xmin, xmiddle, xmax-1]:
|
|
138
|
+
if idx <= self.index_list[-1]:
|
|
139
|
+
indices.append(idx)
|
|
140
|
+
# print(f'{indices=}')
|
|
141
|
+
if xmin == 0 and self.vxmin < 0:
|
|
142
|
+
if xhalf / 2 < xmin - indices[-1]:
|
|
143
|
+
indices = [indices[-1]]
|
|
144
|
+
aligns = aligns[2:]
|
|
145
|
+
else:
|
|
146
|
+
indices = [0, indices[-1]]
|
|
147
|
+
aligns = aligns[1:]
|
|
148
|
+
elif len(indices) < 2:
|
|
149
|
+
if xmin - indices[-1] < xhalf / 2:
|
|
150
|
+
indices = [xmin, self.index_list[-1]]
|
|
151
|
+
aligns = [aligns[0], aligns[0]]
|
|
152
|
+
elif len(indices) < 3:
|
|
153
|
+
indices[-1] = self.index_list[-1]
|
|
154
|
+
aligns = aligns[:2]
|
|
155
|
+
|
|
156
|
+
date_list = [self.df.iloc[idx]['date'] for idx in indices]
|
|
157
|
+
# 라벨을 노출할 틱 위치
|
|
158
|
+
self.ax_volume.set_xticks([idx+0.5 for idx in indices])
|
|
159
|
+
# 라벨
|
|
160
|
+
self.ax_volume.set_xticklabels(date_list)
|
|
161
|
+
labels: list[Text] = self.ax_volume.get_xticklabels()
|
|
162
|
+
for label, align in zip(labels, aligns):
|
|
163
|
+
# 라벨 텍스트 정렬
|
|
164
|
+
label.set_horizontalalignment(align)
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
def set_collections(self, xmin, *, xmax, simpler=False, draw_ma=True):
|
|
168
|
+
self._set_collections(xmin, index_end=xmax, simpler=simpler, draw_ma=draw_ma)
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
def _set_collections(self, index_start, *, index_end, simpler, draw_ma):
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
def get_default_lim(self):
|
|
175
|
+
return (0, self.index_list[-1]+1)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class CollectionMixin(LimMixin):
|
|
179
|
+
limit_candle = 400
|
|
180
|
+
limit_wick = 2_000
|
|
181
|
+
limit_volume = 200
|
|
182
|
+
limit_ma = None
|
|
183
|
+
|
|
184
|
+
def _set_collections(self, index_start, *, index_end, simpler, draw_ma):
|
|
185
|
+
if index_start < 0:
|
|
186
|
+
index_start = 0
|
|
187
|
+
indsub = index_end - index_start
|
|
188
|
+
# print(f'{indsub=:,}')
|
|
189
|
+
|
|
190
|
+
if not self.limit_candle or indsub < self.limit_candle:
|
|
191
|
+
# print('candle')
|
|
192
|
+
self._set_candle_segments(index_start, index_end=index_end)
|
|
193
|
+
self._set_volume_segments(index_start, index_end=index_end)
|
|
194
|
+
else:
|
|
195
|
+
self._set_volume_wick_segments(index_start, index_end, simpler=simpler)
|
|
196
|
+
|
|
197
|
+
if not self.limit_wick or indsub < self.limit_wick:
|
|
198
|
+
# print('wick')
|
|
199
|
+
self._set_candle_wick_segments(index_start, index_end)
|
|
200
|
+
else:
|
|
201
|
+
# print('line')
|
|
202
|
+
self._set_priceline_segments(index_start, index_end)
|
|
203
|
+
|
|
204
|
+
self._set_ma_segments(index_start, index_end, draw_ma)
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
def _set_candle_segments(self, index_start, index_end):
|
|
208
|
+
self.collection_candle.set_segments(self.segment_candle[index_start:index_end])
|
|
209
|
+
self.collection_candle.set_facecolor(self.facecolor_candle[index_start:index_end])
|
|
210
|
+
self.collection_candle.set_edgecolor(self.edgecolor_candle[index_start:index_end])
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
def _set_candle_wick_segments(self, index_start, index_end):
|
|
214
|
+
self.collection_candle.set_segments(self.segment_candle_wick[index_start:index_end])
|
|
215
|
+
self.collection_candle.set_facecolor([])
|
|
216
|
+
self.collection_candle.set_edgecolor(self.edgecolor_candle[index_start:index_end])
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
def _set_priceline_segments(self, index_start, index_end):
|
|
220
|
+
self.collection_candle.set_segments(self.segment_priceline[:, index_start:index_end])
|
|
221
|
+
self.collection_candle.set_facecolor([])
|
|
222
|
+
self.collection_candle.set_edgecolor(self.CONFIG.CANDLE.line_color)
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
def _set_ma_segments(self, index_start, index_end, draw_ma):
|
|
226
|
+
if not draw_ma:
|
|
227
|
+
self.collection_ma.set_segments([])
|
|
228
|
+
else:
|
|
229
|
+
self.collection_ma.set_segments(self.segment_ma[:, index_start:index_end])
|
|
230
|
+
self.collection_ma.set_edgecolor(self.edgecolor_ma)
|
|
231
|
+
return
|
|
232
|
+
|
|
233
|
+
def _set_volume_segments(self, index_start, index_end):
|
|
234
|
+
if not self.key_volume:
|
|
235
|
+
self.collection_volume.set_segments([])
|
|
236
|
+
return
|
|
237
|
+
self.collection_volume.set_segments(self.segment_volume[index_start:index_end])
|
|
238
|
+
self.collection_volume.set_linewidth(0.7)
|
|
239
|
+
self.collection_volume.set_facecolor(self.facecolor_volume[index_start:index_end])
|
|
240
|
+
self.collection_volume.set_edgecolor(self.edgecolor_volume[index_start:index_end])
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
def _set_volume_wick_segments(self, index_start, index_end, simpler):
|
|
244
|
+
if not self.key_volume:
|
|
245
|
+
self.collection_volume.set_segments([])
|
|
246
|
+
return
|
|
247
|
+
seg_volume = self.segment_volume_wick[index_start:index_end]
|
|
248
|
+
seg_facecolor_volume = self.facecolor_volume[index_start:index_end]
|
|
249
|
+
seg_edgecolor_volume = self.edgecolor_volume[index_start:index_end]
|
|
250
|
+
if simpler:
|
|
251
|
+
values = seg_volume[:, 1, 1]
|
|
252
|
+
top_index = np.argsort(-values)[:self.limit_volume]
|
|
253
|
+
seg_volume = seg_volume[top_index]
|
|
254
|
+
seg_facecolor_volume = seg_facecolor_volume[top_index]
|
|
255
|
+
seg_edgecolor_volume = seg_edgecolor_volume[top_index]
|
|
256
|
+
self.collection_volume.set_segments(seg_volume)
|
|
257
|
+
self.collection_volume.set_linewidth(1.3)
|
|
258
|
+
self.collection_volume.set_facecolor(seg_facecolor_volume)
|
|
259
|
+
self.collection_volume.set_edgecolor(seg_edgecolor_volume)
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class BaseMixin(CollectionMixin):
|
|
264
|
+
pass
|
|
265
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from ._nav import BaseMixin, Chart
|
|
@@ -0,0 +1,268 @@
|
|
|
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
|
+
|
|
6
|
+
from ..._config import SLIDERCONFIG, SliderConfigData
|
|
7
|
+
from .._cursor import BaseMixin as Base
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PlotMixin(Base):
|
|
11
|
+
slider_top = True
|
|
12
|
+
CONFIG: SliderConfigData
|
|
13
|
+
|
|
14
|
+
def __init__(self, config=SLIDERCONFIG, *args, **kwargs):
|
|
15
|
+
super().__init__(config=config, *args, **kwargs)
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
def add_axes(self):
|
|
19
|
+
if self.slider_top:
|
|
20
|
+
self.figure, axes = plt.subplots(
|
|
21
|
+
4, # row 수
|
|
22
|
+
figsize=self.CONFIG.FIGURE.figsize, # 기본 크기
|
|
23
|
+
height_ratios=(
|
|
24
|
+
self.CONFIG.FIGURE.RATIO.slider,
|
|
25
|
+
self.CONFIG.FIGURE.RATIO.legend,
|
|
26
|
+
self.CONFIG.FIGURE.RATIO.price, self.CONFIG.FIGURE.RATIO.volume
|
|
27
|
+
) # row 크기 비율
|
|
28
|
+
)
|
|
29
|
+
axes: list[Axes]
|
|
30
|
+
self.ax_slider, self.ax_legend, self.ax_price, self.ax_volume = axes
|
|
31
|
+
else:
|
|
32
|
+
self.figure, axes = plt.subplots(
|
|
33
|
+
5, # row 수
|
|
34
|
+
figsize=self.CONFIG.FIGURE.figsize, # 기본 크기
|
|
35
|
+
height_ratios=(
|
|
36
|
+
self.CONFIG.FIGURE.RATIO.legend,
|
|
37
|
+
self.CONFIG.FIGURE.RATIO.price, self.CONFIG.FIGURE.RATIO.volume,
|
|
38
|
+
self.CONFIG.FIGURE.RATIO.none,
|
|
39
|
+
self.CONFIG.FIGURE.RATIO.slider
|
|
40
|
+
) # row 크기 비율
|
|
41
|
+
)
|
|
42
|
+
axes: list[Axes]
|
|
43
|
+
self.ax_legend, self.ax_price, self.ax_volume, ax_none, self.ax_slider = axes
|
|
44
|
+
|
|
45
|
+
ax_none.set_axis_off()
|
|
46
|
+
ax_none.xaxis.set_animated(True)
|
|
47
|
+
ax_none.yaxis.set_animated(True)
|
|
48
|
+
|
|
49
|
+
self.ax_slider.set_label('slider ax')
|
|
50
|
+
self.ax_legend.set_label('legend ax')
|
|
51
|
+
self.ax_price.set_label('price ax')
|
|
52
|
+
self.ax_volume.set_label('volume ax')
|
|
53
|
+
self.ax_legend.set_axis_off()
|
|
54
|
+
|
|
55
|
+
# y ticklabel foramt 설정
|
|
56
|
+
self.ax_slider.yaxis.set_major_formatter(lambda x, _: self.CONFIG.UNIT.func(x, word=self.CONFIG.UNIT.price, digit=self.CONFIG.UNIT.digit))
|
|
57
|
+
self.ax_price.yaxis.set_major_formatter(lambda x, _: self.CONFIG.UNIT.func(x, word=self.CONFIG.UNIT.price, digit=self.CONFIG.UNIT.digit))
|
|
58
|
+
self.ax_volume.yaxis.set_major_formatter(lambda x, _: self.CONFIG.UNIT.func(x, word=self.CONFIG.UNIT.volume, digit=self.CONFIG.UNIT.digit))
|
|
59
|
+
|
|
60
|
+
# 공통 설정
|
|
61
|
+
for ax in (self.ax_slider, self.ax_price, self.ax_volume):
|
|
62
|
+
ax.xaxis.set_animated(True)
|
|
63
|
+
ax.yaxis.set_animated(True)
|
|
64
|
+
|
|
65
|
+
# x tick 외부 눈금 표시하지 않기
|
|
66
|
+
ax.xaxis.set_ticks_position('none')
|
|
67
|
+
# x tick label 제거
|
|
68
|
+
ax.set_xticklabels([])
|
|
69
|
+
# y tick 우측으로 이동
|
|
70
|
+
ax.tick_params(
|
|
71
|
+
left=False, right=True, labelleft=False, labelright=True,
|
|
72
|
+
colors=self.CONFIG.AX.TICK.edgecolor
|
|
73
|
+
)
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
def _set_axes(self):
|
|
77
|
+
super()._set_axes()
|
|
78
|
+
|
|
79
|
+
self.ax_slider.set_facecolor(self.CONFIG.AX.facecolor)
|
|
80
|
+
self.ax_slider.grid(**self.CONFIG.AX.GRID.__dict__)
|
|
81
|
+
|
|
82
|
+
# 틱 색상
|
|
83
|
+
self.ax_slider.tick_params('both', colors=self.CONFIG.AX.TICK.edgecolor)
|
|
84
|
+
# 틱 라벨 색상
|
|
85
|
+
ticklabels: list[Text] = self.ax_slider.get_xticklabels() + self.ax_slider.get_yticklabels()
|
|
86
|
+
for ticklabel in ticklabels:
|
|
87
|
+
ticklabel.set_color(self.CONFIG.AX.TICK.fontcolor)
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
def _set_figure(self):
|
|
91
|
+
self.figure.canvas.manager.set_window_title('Seolpyo MPLChart')
|
|
92
|
+
|
|
93
|
+
# 차트 비율 변경
|
|
94
|
+
# print(f'{self.CONFIG.FIGURE.RATIO.volume=}')
|
|
95
|
+
gs = self.ax_price.get_subplotspec().get_gridspec()
|
|
96
|
+
if len(self.figure.axes) == 4:
|
|
97
|
+
gs.set_height_ratios([
|
|
98
|
+
self.CONFIG.FIGURE.RATIO.slider,
|
|
99
|
+
self.CONFIG.FIGURE.RATIO.legend,
|
|
100
|
+
self.CONFIG.FIGURE.RATIO.price, self.CONFIG.FIGURE.RATIO.volume,
|
|
101
|
+
])
|
|
102
|
+
else:
|
|
103
|
+
gs.set_height_ratios([
|
|
104
|
+
self.CONFIG.FIGURE.RATIO.legend,
|
|
105
|
+
self.CONFIG.FIGURE.RATIO.price, self.CONFIG.FIGURE.RATIO.volume,
|
|
106
|
+
self.CONFIG.FIGURE.RATIO.none,
|
|
107
|
+
self.CONFIG.FIGURE.RATIO.slider,
|
|
108
|
+
])
|
|
109
|
+
self.figure.tight_layout()
|
|
110
|
+
|
|
111
|
+
# 플롯간 간격 설정(Configure subplots)
|
|
112
|
+
self.figure.subplots_adjust(**self.CONFIG.FIGURE.ADJUST.__dict__)
|
|
113
|
+
|
|
114
|
+
self.figure.set_facecolor(self.CONFIG.FIGURE.facecolor)
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class CollectionMixin(PlotMixin):
|
|
119
|
+
def add_artists(self):
|
|
120
|
+
super().add_artists()
|
|
121
|
+
|
|
122
|
+
# 슬라이더에 그려질 주가 선형 차트
|
|
123
|
+
self.collection_slider = LineCollection([], animated=True)
|
|
124
|
+
self.ax_slider.add_artist(self.collection_slider)
|
|
125
|
+
|
|
126
|
+
# 슬라이더 네비게이터
|
|
127
|
+
self.collection_navigator = LineCollection([], animated=True, alpha=(0.3, 1.0))
|
|
128
|
+
self.ax_slider.add_artist(self.collection_navigator)
|
|
129
|
+
|
|
130
|
+
# 현재 위치 표시용 line
|
|
131
|
+
self.collection_slider_vline = LineCollection(segments=[], animated=True)
|
|
132
|
+
self.ax_slider.add_artist(self.collection_slider_vline)
|
|
133
|
+
|
|
134
|
+
# 현대 위치에 해당하는 date 출력용 text
|
|
135
|
+
self.artist_text_slider = Text(text='', animated=True, horizontalalignment='center', verticalalignment='top')
|
|
136
|
+
self.ax_slider.add_artist(self.artist_text_slider)
|
|
137
|
+
|
|
138
|
+
self._set_slider_artists()
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
def _set_slider_artists(self):
|
|
142
|
+
edgecolors = [self.CONFIG.SLIDER.NAVIGATOR.facecolor, self.CONFIG.SLIDER.NAVIGATOR.edgecolor]
|
|
143
|
+
self.collection_navigator.set_edgecolor(edgecolors)
|
|
144
|
+
|
|
145
|
+
kwargs = self.CONFIG.CURSOR.CROSSLINE.__dict__
|
|
146
|
+
kwargs.update({'segments': [], 'animated': True})
|
|
147
|
+
self.collection_slider_vline.set(**kwargs)
|
|
148
|
+
|
|
149
|
+
kwargs = self.CONFIG.CURSOR.TEXT.to_dict()
|
|
150
|
+
kwargs.update({'text': ' ', 'animated': True})
|
|
151
|
+
self.artist_text_slider.set(**kwargs)
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
def _set_artists(self):
|
|
155
|
+
super()._set_artists()
|
|
156
|
+
|
|
157
|
+
self._set_slider_artists()
|
|
158
|
+
self._set_slider_collection()
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
def _set_slider_collection(self):
|
|
162
|
+
keys = []
|
|
163
|
+
for i in reversed(self.CONFIG.MA.ma_list):
|
|
164
|
+
keys.append('x')
|
|
165
|
+
keys.append(f'ma{i}')
|
|
166
|
+
|
|
167
|
+
series = self.df[keys + ['x', 'close']]
|
|
168
|
+
series['x'] = series['x'] - 0.5
|
|
169
|
+
segment_slider = series.values
|
|
170
|
+
sizes = [segment_slider.shape[0], len(self.CONFIG.MA.ma_list)+1, 2]
|
|
171
|
+
segment_slider = segment_slider.reshape(*sizes).swapaxes(0, 1)
|
|
172
|
+
self.collection_slider.set_segments(segment_slider)
|
|
173
|
+
|
|
174
|
+
linewidth = []
|
|
175
|
+
ma_colors = []
|
|
176
|
+
for n, _ in enumerate(self.CONFIG.MA.ma_list):
|
|
177
|
+
linewidth.append(0.9)
|
|
178
|
+
try:
|
|
179
|
+
ma_colors.append(self.CONFIG.MA.color_list[n])
|
|
180
|
+
except:
|
|
181
|
+
ma_colors.append(self.CONFIG.MA.color_default)
|
|
182
|
+
|
|
183
|
+
self.collection_slider.set_linewidth(linewidth + [2.4])
|
|
184
|
+
self.collection_slider.set_edgecolor(ma_colors + [self.CONFIG.CANDLE.line_color])
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class NavigatorMixin(CollectionMixin):
|
|
189
|
+
def _connect_events(self):
|
|
190
|
+
super()._connect_events()
|
|
191
|
+
|
|
192
|
+
self.figure.canvas.mpl_connect('resize_event', lambda x: self.on_resize(x))
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
def on_resize(self, e):
|
|
196
|
+
self._on_resize(e)
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
def _on_resize(self, e):
|
|
200
|
+
self._set_navigator_artists()
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
def _refresh(self):
|
|
204
|
+
super()._refresh()
|
|
205
|
+
self._set_navigator_artists()
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
def _set_navigator_artists(self):
|
|
209
|
+
if not getattr(self, 'index_list', False):
|
|
210
|
+
return
|
|
211
|
+
xmax = self.index_list[-1]
|
|
212
|
+
# 슬라이더 xlim
|
|
213
|
+
xdistance = xmax / 30
|
|
214
|
+
self.slider_xmin, self.slider_xmax = (-xdistance, xmax+xdistance)
|
|
215
|
+
self.ax_slider.set_xlim(self.slider_xmin, self.slider_xmax)
|
|
216
|
+
|
|
217
|
+
# 슬라이더 ylim
|
|
218
|
+
ymin, ymax = (self.df['low'].min(), self.df['high'].max())
|
|
219
|
+
ysub = ymax - ymin
|
|
220
|
+
self.sldier_ymiddle = ymin + (ysub / 2)
|
|
221
|
+
ydistance = ysub / 5
|
|
222
|
+
self.slider_ymin, self.slider_ymax = (ymin-ydistance, ymax+ydistance)
|
|
223
|
+
self.ax_slider.set_ylim(self.slider_ymin, self.slider_ymax)
|
|
224
|
+
|
|
225
|
+
# 슬라이더 텍스트 y
|
|
226
|
+
self.artist_text_slider.set_y(ymax)
|
|
227
|
+
|
|
228
|
+
self.collection_navigator.set_linewidth([self.ax_slider.bbox.height, 5])
|
|
229
|
+
|
|
230
|
+
# 슬라이더 라인 선택 범위
|
|
231
|
+
xsub = self.slider_xmax - self.slider_xmin
|
|
232
|
+
self._navLineWidth = xsub * 8 / 1_000
|
|
233
|
+
if self._navLineWidth < 1:
|
|
234
|
+
self._navLineWidth = 1
|
|
235
|
+
self._navLineWidth_half = self._navLineWidth / 2
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
def _axis_navigator(self, navmin, navmax):
|
|
239
|
+
seg = [
|
|
240
|
+
# 좌측 오버레이
|
|
241
|
+
(
|
|
242
|
+
(self.slider_xmin, self.sldier_ymiddle),
|
|
243
|
+
(navmin, self.sldier_ymiddle),
|
|
244
|
+
),
|
|
245
|
+
# 좌측 네비게이터
|
|
246
|
+
(
|
|
247
|
+
(navmin, self.slider_ymin),
|
|
248
|
+
(navmin, self.slider_ymax),
|
|
249
|
+
),
|
|
250
|
+
# 우측 네비게이터
|
|
251
|
+
(
|
|
252
|
+
(navmax, self.sldier_ymiddle),
|
|
253
|
+
(self.slider_xmax, self.sldier_ymiddle),
|
|
254
|
+
),
|
|
255
|
+
# 우측 오버레이
|
|
256
|
+
(
|
|
257
|
+
(navmax, self.slider_ymin),
|
|
258
|
+
(navmax, self.slider_ymax),
|
|
259
|
+
),
|
|
260
|
+
]
|
|
261
|
+
|
|
262
|
+
self.collection_navigator.set_segments(seg)
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class BaseMixin(NavigatorMixin):
|
|
267
|
+
min_distance = 5
|
|
268
|
+
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from ._base import BaseMixin as Base
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DataMixin(Base):
|
|
5
|
+
navcoordinate: tuple[int, int] = (0, 0)
|
|
6
|
+
|
|
7
|
+
def set_data(self, df, change_lim=True, *args, **kwargs):
|
|
8
|
+
# print(f'{change_lim=}')
|
|
9
|
+
return super().set_data(df, change_lim=change_lim, *args, **kwargs)
|
|
10
|
+
|
|
11
|
+
def _set_data(self, df, change_lim=True, *args, **kwargs):
|
|
12
|
+
# print(f'{change_lim=}')
|
|
13
|
+
super()._set_data(df, *args, **kwargs)
|
|
14
|
+
|
|
15
|
+
vmin, vmax = self.navcoordinate
|
|
16
|
+
min_distance = 5 if not self.min_distance or self.min_distance < 5 else self.min_distance
|
|
17
|
+
if not change_lim and min_distance <= (vmax-vmin):
|
|
18
|
+
vmax += 1
|
|
19
|
+
else:
|
|
20
|
+
vmin, vmax = self.get_default_lim()
|
|
21
|
+
self.navcoordinate = (vmin, vmax-1)
|
|
22
|
+
|
|
23
|
+
self._set_slider_collection()
|
|
24
|
+
|
|
25
|
+
self.axis(vmin, xmax=vmax)
|
|
26
|
+
|
|
27
|
+
self._set_navigator_artists()
|
|
28
|
+
self._set_slider_xtick()
|
|
29
|
+
|
|
30
|
+
self._axis_navigator(*self.navcoordinate)
|
|
31
|
+
|
|
32
|
+
self._set_length_text()
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
def _set_slider_xtick(self):
|
|
36
|
+
if self.slider_top:
|
|
37
|
+
self.ax_slider.xaxis.set_ticks_position('top')
|
|
38
|
+
else:
|
|
39
|
+
self.ax_slider.xaxis.set_ticks_position('bottom')
|
|
40
|
+
self.ax_slider.get_yticks()
|
|
41
|
+
|
|
42
|
+
# grid가 xtick에 영향을 받기 때문에 구간별 tick을 설정해주어야 한다.
|
|
43
|
+
step = len(self.index_list) // 6
|
|
44
|
+
indices = []
|
|
45
|
+
for idx in self.index_list[::step]:
|
|
46
|
+
indices.append(idx)
|
|
47
|
+
if indices[-1] + 1 < self.index_list[-1]:
|
|
48
|
+
indices += [self.index_list[-1]]
|
|
49
|
+
else:
|
|
50
|
+
indices[-1] = self.index_list[-1]
|
|
51
|
+
# print(f'{indices=}')
|
|
52
|
+
# tick label은 0과 -1 구간에만 설정
|
|
53
|
+
date_list = ['' for _ in indices]
|
|
54
|
+
date_list[0] = self.df.iloc[0]['date']
|
|
55
|
+
date_list[-1] = self.df.iloc[-1]['date']
|
|
56
|
+
# xtick 설정
|
|
57
|
+
self.ax_slider.set_xticks(indices)
|
|
58
|
+
self.ax_slider.set_xticklabels(date_list)
|
|
59
|
+
labels = self.ax_slider.get_xticklabels()
|
|
60
|
+
for label, align in zip(labels, ['center', 'center']):
|
|
61
|
+
# 라벨 텍스트 정렬
|
|
62
|
+
label.set_horizontalalignment(align)
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
def get_default_lim(self):
|
|
66
|
+
xmax = self.index_list[-1] + 1
|
|
67
|
+
xmin = xmax - 120
|
|
68
|
+
if xmin < 0:
|
|
69
|
+
xmin = 0
|
|
70
|
+
return (xmin, xmax)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class BackgroundMixin(DataMixin):
|
|
74
|
+
def _copy_bbox(self):
|
|
75
|
+
renderer = self.figure.canvas.renderer
|
|
76
|
+
|
|
77
|
+
self.ax_slider.xaxis.draw(renderer)
|
|
78
|
+
self.ax_slider.yaxis.draw(renderer)
|
|
79
|
+
self.collection_slider.draw(renderer)
|
|
80
|
+
self.background_emtpy = renderer.copy_from_bbox(self.figure.bbox)
|
|
81
|
+
|
|
82
|
+
self.draw_artists()
|
|
83
|
+
self.background = renderer.copy_from_bbox(self.figure.bbox)
|
|
84
|
+
|
|
85
|
+
self.collection_navigator.draw(renderer)
|
|
86
|
+
self.background_with_nav = renderer.copy_from_bbox(self.figure.bbox)
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
def _restore_region(self, is_empty=False, with_nav=True):
|
|
90
|
+
if not self.background:
|
|
91
|
+
self._create_background()
|
|
92
|
+
|
|
93
|
+
func = self.figure.canvas.renderer.restore_region
|
|
94
|
+
if is_empty:
|
|
95
|
+
func(self.background_emtpy)
|
|
96
|
+
elif with_nav:
|
|
97
|
+
func(self.background_with_nav)
|
|
98
|
+
else:
|
|
99
|
+
func(self.background)
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class BaseMixin(BackgroundMixin):
|
|
104
|
+
pass
|
|
105
|
+
|