seolpyo-mplchart 2.0.0.3__py3-none-any.whl → 2.1.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.
- seolpyo_mplchart/__init__.py +17 -133
- seolpyo_mplchart/_chart/__init__.py +39 -31
- seolpyo_mplchart/_chart/base/__init__.py +111 -0
- seolpyo_mplchart/_chart/base/a_canvas.py +250 -0
- seolpyo_mplchart/_chart/base/b_artist.py +143 -0
- seolpyo_mplchart/_chart/base/c_draw.py +100 -0
- seolpyo_mplchart/_chart/base/d_segment.py +262 -0
- seolpyo_mplchart/_chart/base/e_axis.py +267 -0
- seolpyo_mplchart/_chart/base/f_background.py +62 -0
- seolpyo_mplchart/_chart/base/g_event.py +66 -0
- seolpyo_mplchart/_chart/base/h_data.py +138 -0
- seolpyo_mplchart/_chart/base/test.py +58 -0
- seolpyo_mplchart/_chart/cursor/__init__.py +125 -0
- seolpyo_mplchart/_chart/cursor/b_artist.py +130 -0
- seolpyo_mplchart/_chart/cursor/c_draw.py +96 -0
- seolpyo_mplchart/_chart/cursor/d_segment.py +359 -0
- seolpyo_mplchart/_chart/cursor/e_axis.py +65 -0
- seolpyo_mplchart/_chart/cursor/g_event.py +233 -0
- seolpyo_mplchart/_chart/cursor/h_data.py +61 -0
- seolpyo_mplchart/_chart/cursor/test.py +69 -0
- seolpyo_mplchart/_chart/slider/__init__.py +169 -0
- seolpyo_mplchart/_chart/slider/a_canvas.py +260 -0
- seolpyo_mplchart/_chart/slider/b_artist.py +91 -0
- seolpyo_mplchart/_chart/slider/c_draw.py +54 -0
- seolpyo_mplchart/_chart/slider/d_segment.py +166 -0
- seolpyo_mplchart/_chart/slider/e_axis.py +70 -0
- seolpyo_mplchart/_chart/slider/f_background.py +37 -0
- seolpyo_mplchart/_chart/slider/g_event.py +353 -0
- seolpyo_mplchart/_chart/slider/h_data.py +102 -0
- seolpyo_mplchart/_chart/slider/test.py +71 -0
- seolpyo_mplchart/_config/candle.py +1 -0
- seolpyo_mplchart/_config/figure.py +3 -4
- seolpyo_mplchart/_config/ma.py +2 -0
- seolpyo_mplchart/_config/slider/config.py +2 -2
- seolpyo_mplchart/_config/slider/figure.py +3 -4
- seolpyo_mplchart/_config/slider/nav.py +3 -2
- seolpyo_mplchart/_config/volume.py +1 -0
- seolpyo_mplchart/_utils/__init__.py +10 -0
- seolpyo_mplchart/_utils/nums.py +67 -0
- seolpyo_mplchart/_utils/theme/__init__.py +15 -0
- seolpyo_mplchart/_utils/theme/dark.py +57 -0
- seolpyo_mplchart/_utils/theme/light.py +56 -0
- seolpyo_mplchart/_utils/utils.py +28 -0
- seolpyo_mplchart/_utils/xl/__init__.py +15 -0
- seolpyo_mplchart/_utils/xl/csv.py +46 -0
- seolpyo_mplchart/_utils/xl/xlsx.py +49 -0
- seolpyo_mplchart/sample/apple.txt +6058 -0
- seolpyo_mplchart/sample/samsung.txt +5938 -0
- seolpyo_mplchart/test.py +5 -5
- {seolpyo_mplchart-2.0.0.3.dist-info → seolpyo_mplchart-2.1.0.dist-info}/METADATA +21 -13
- seolpyo_mplchart-2.1.0.dist-info/RECORD +89 -0
- seolpyo_mplchart-2.0.0.3.dist-info/RECORD +0 -50
- {seolpyo_mplchart-2.0.0.3.dist-info → seolpyo_mplchart-2.1.0.dist-info}/WHEEL +0 -0
- {seolpyo_mplchart-2.0.0.3.dist-info → seolpyo_mplchart-2.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,260 @@
|
|
|
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 ..base.a_canvas import CanvasMixin as BaseMixin, Figure
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Base(BaseMixin):
|
|
11
|
+
CONFIG: SliderConfigData
|
|
12
|
+
|
|
13
|
+
def add_axes(self):
|
|
14
|
+
if not self.figure:
|
|
15
|
+
self.figure, *_ = plt.subplots(
|
|
16
|
+
7, # row 수
|
|
17
|
+
figsize=self.CONFIG.FIGURE.figsize, # 기본 크기
|
|
18
|
+
height_ratios=(
|
|
19
|
+
0,
|
|
20
|
+
0,
|
|
21
|
+
0,
|
|
22
|
+
self.CONFIG.FIGURE.RATIO.price,
|
|
23
|
+
self.CONFIG.FIGURE.RATIO.volume,
|
|
24
|
+
0,
|
|
25
|
+
0,
|
|
26
|
+
) # row 크기 비율
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
self._ax_slider_top, self.ax_none_top, self.ax_legend, self.ax_price, self.ax_volume, self.ax_none_bottom, self._ax_slider_bottom = self.figure.axes
|
|
30
|
+
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class FigureMixin(Base):
|
|
35
|
+
key_volume: str
|
|
36
|
+
ax_legend: Axes
|
|
37
|
+
|
|
38
|
+
slider_top = True
|
|
39
|
+
|
|
40
|
+
def _set_figure_ratios(self):
|
|
41
|
+
gs = self.figure.axes[0].get_subplotspec().get_gridspec()
|
|
42
|
+
|
|
43
|
+
ratio_volume = self.CONFIG.FIGURE.RATIO.volume
|
|
44
|
+
if not self.key_volume:
|
|
45
|
+
ratio_volume = 0
|
|
46
|
+
|
|
47
|
+
legend = self.ax_legend.get_legend()
|
|
48
|
+
if not legend:
|
|
49
|
+
if self.slider_top:
|
|
50
|
+
ratios = [
|
|
51
|
+
self.CONFIG.FIGURE.RATIO.slider,
|
|
52
|
+
0,
|
|
53
|
+
0,
|
|
54
|
+
self.CONFIG.FIGURE.RATIO.price, ratio_volume,
|
|
55
|
+
0,
|
|
56
|
+
0,
|
|
57
|
+
]
|
|
58
|
+
else:
|
|
59
|
+
ratios = [
|
|
60
|
+
0,
|
|
61
|
+
0,
|
|
62
|
+
0,
|
|
63
|
+
self.CONFIG.FIGURE.RATIO.price, ratio_volume,
|
|
64
|
+
self.CONFIG.FIGURE.RATIO.none,
|
|
65
|
+
self.CONFIG.FIGURE.RATIO.slider,
|
|
66
|
+
]
|
|
67
|
+
else:
|
|
68
|
+
fig_heihgt = self.figure.get_figheight()
|
|
69
|
+
fig_px = fig_heihgt * (1-self.CONFIG.FIGURE.ADJUST.hspace*2) * self.figure.dpi
|
|
70
|
+
# print(f'{(fig_heihgt, fig_px)=}')
|
|
71
|
+
|
|
72
|
+
# Legend에 Axes 높이 맞추기
|
|
73
|
+
bbox = legend.get_window_extent().transformed(self.figure.transFigure.inverted())
|
|
74
|
+
ax_pos = self.ax_legend.get_position()
|
|
75
|
+
self.ax_legend.set_position([ax_pos.x0, ax_pos.y0, ax_pos.width, bbox.height])
|
|
76
|
+
|
|
77
|
+
legend_height = bbox.height
|
|
78
|
+
legend_px = legend.get_window_extent().height
|
|
79
|
+
# print(f'{(legend_height, legend_px)=}')
|
|
80
|
+
|
|
81
|
+
chart_px = fig_px - legend_height
|
|
82
|
+
chart_ratio = self.CONFIG.FIGURE.RATIO.price + ratio_volume
|
|
83
|
+
|
|
84
|
+
# print(f'{self.CONFIG.FIGURE.RATIO.__dict__=}')
|
|
85
|
+
ratio_none = 0 if self.slider_top else self.CONFIG.FIGURE.RATIO.none
|
|
86
|
+
chart_ratio += self.CONFIG.FIGURE.RATIO.slider
|
|
87
|
+
div_chart = chart_px / chart_ratio
|
|
88
|
+
price_px = div_chart * self.CONFIG.FIGURE.RATIO.price
|
|
89
|
+
volume_px = div_chart * ratio_volume
|
|
90
|
+
slider_px = div_chart * self.CONFIG.FIGURE.RATIO.slider
|
|
91
|
+
none_px = div_chart * ratio_none
|
|
92
|
+
# print(f'{none_px=}')
|
|
93
|
+
|
|
94
|
+
if self.slider_top:
|
|
95
|
+
# 차트 비율
|
|
96
|
+
ratios = [
|
|
97
|
+
slider_px,
|
|
98
|
+
0,
|
|
99
|
+
legend_px * 3,
|
|
100
|
+
price_px, volume_px,
|
|
101
|
+
0,
|
|
102
|
+
0,
|
|
103
|
+
]
|
|
104
|
+
else:
|
|
105
|
+
ratios = [
|
|
106
|
+
0,
|
|
107
|
+
0,
|
|
108
|
+
legend_px * 1.2,
|
|
109
|
+
price_px, volume_px,
|
|
110
|
+
none_px,
|
|
111
|
+
slider_px,
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
# print(f'{ratios=}')
|
|
115
|
+
gs.set_height_ratios(ratios)
|
|
116
|
+
|
|
117
|
+
self.figure.tight_layout()
|
|
118
|
+
|
|
119
|
+
# 플롯간 간격 설정(Configure subplots)
|
|
120
|
+
self.figure.subplots_adjust(**self.CONFIG.FIGURE.ADJUST.__dict__)
|
|
121
|
+
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
def _set_figure(self):
|
|
125
|
+
self.figure.canvas.manager.set_window_title('Seolpyo MPLChart')
|
|
126
|
+
|
|
127
|
+
# print(f'{self.CONFIG.FIGURE.RATIO.volume=}')
|
|
128
|
+
# print(f'{gs.get_height_ratios()=}')
|
|
129
|
+
|
|
130
|
+
self._set_figure_ratios()
|
|
131
|
+
|
|
132
|
+
self.figure.set_facecolor(self.CONFIG.FIGURE.facecolor)
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class AxesMixin(Base):
|
|
137
|
+
slider_top = True
|
|
138
|
+
|
|
139
|
+
ax_slider: Axes = None
|
|
140
|
+
|
|
141
|
+
collection_slider: LineCollection
|
|
142
|
+
collection_nav: LineCollection
|
|
143
|
+
collection_slider_vline: LineCollection
|
|
144
|
+
artist_text_slider: Text
|
|
145
|
+
|
|
146
|
+
def _set_axes(self):
|
|
147
|
+
super()._set_axes()
|
|
148
|
+
|
|
149
|
+
if self.slider_top:
|
|
150
|
+
ax_slider = self._ax_slider_top
|
|
151
|
+
else:
|
|
152
|
+
ax_slider = self._ax_slider_bottom
|
|
153
|
+
|
|
154
|
+
if self.ax_slider and ax_slider is not self.ax_slider:
|
|
155
|
+
# print('move artist')
|
|
156
|
+
# ax_slider 위치가 변경된 경우 artist 이동하기
|
|
157
|
+
artists: list[LineCollection|Text] = [
|
|
158
|
+
self.collection_slider,
|
|
159
|
+
self.collection_nav,
|
|
160
|
+
self.collection_slider_vline,
|
|
161
|
+
self.artist_text_slider,
|
|
162
|
+
]
|
|
163
|
+
for artist in artists:
|
|
164
|
+
artist.remove()
|
|
165
|
+
artist.set_transform(ax_slider.transData)
|
|
166
|
+
ax_slider.add_artist(artist)
|
|
167
|
+
# axis
|
|
168
|
+
ax_slider.set_xlim(*self.ax_slider.get_xlim())
|
|
169
|
+
ax_slider.set_ylim(*self.ax_slider.get_ylim())
|
|
170
|
+
# tick label
|
|
171
|
+
ax_slider.set_xticks(self.ax_slider.get_xticks(minor=True), minor=True)
|
|
172
|
+
ax_slider.set_xticklabels(self.ax_slider.get_xticklabels(minor=True), minor=True)
|
|
173
|
+
|
|
174
|
+
self.ax_slider = ax_slider
|
|
175
|
+
|
|
176
|
+
self._ax_slider_top.set_label('top slider ax')
|
|
177
|
+
self._ax_slider_bottom.set_label('bottom slider ax')
|
|
178
|
+
self.ax_none_top.set_label('top none ax')
|
|
179
|
+
self.ax_none_bottom.set_label('bottom none ax')
|
|
180
|
+
|
|
181
|
+
self.ax_slider.set_label('slider ax')
|
|
182
|
+
|
|
183
|
+
for ax in (self.ax_none_top, self.ax_none_bottom):
|
|
184
|
+
ax.set_animated(True)
|
|
185
|
+
ax.set_axis_off()
|
|
186
|
+
|
|
187
|
+
self._set_axes_slider()
|
|
188
|
+
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
def _set_axes_slider(self):
|
|
192
|
+
# print(f'{self.slider_top=}')
|
|
193
|
+
formatter = lambda x, _: self.CONFIG.UNIT.func(
|
|
194
|
+
x,
|
|
195
|
+
word=self.CONFIG.UNIT.price,
|
|
196
|
+
digit=self.CONFIG.UNIT.digit+2
|
|
197
|
+
)
|
|
198
|
+
# 공통 설정
|
|
199
|
+
for ax in (self._ax_slider_top, self._ax_slider_bottom):
|
|
200
|
+
ax.yaxis.set_major_formatter(formatter)
|
|
201
|
+
# x tick 외부 눈금 표시하지 않기
|
|
202
|
+
ax.xaxis.set_ticks_position('none')
|
|
203
|
+
# x tick label 제거
|
|
204
|
+
ax.set_xticklabels([])
|
|
205
|
+
# y tick 눈금 표시하지 않기
|
|
206
|
+
ax.yaxis.set_ticks_position('none')
|
|
207
|
+
|
|
208
|
+
# 차트 영역 배경 색상
|
|
209
|
+
ax.set_facecolor(self.CONFIG.AX.facecolor)
|
|
210
|
+
|
|
211
|
+
# Axes 외곽선 색 변경(틱 색과 일치)
|
|
212
|
+
for i in ['top', 'bottom', 'left', 'right']:
|
|
213
|
+
ax.spines[i].set_color(self.CONFIG.AX.TICK.edgecolor)
|
|
214
|
+
# 틱 색상
|
|
215
|
+
ax.tick_params('both', colors=self.CONFIG.AX.TICK.edgecolor)
|
|
216
|
+
# 틱 라벨 색상
|
|
217
|
+
ticklabels: list[Text] = ax.get_xticklabels() + ax.get_yticklabels()
|
|
218
|
+
for ticklabel in ticklabels:
|
|
219
|
+
ticklabel.set_color(self.CONFIG.AX.TICK.fontcolor)
|
|
220
|
+
|
|
221
|
+
# Axes grid(구분선, 격자) 그리기
|
|
222
|
+
# 어째서인지 grid의 zorder 값을 선언해도 1.6을 값으로 한다.
|
|
223
|
+
ax.grid(**self.CONFIG.AX.GRID.__dict__)
|
|
224
|
+
|
|
225
|
+
# major tick mark 길이를 0으로 만들어 튀어나오지 않게 하기
|
|
226
|
+
ax.tick_params('x', which='major', length=0)
|
|
227
|
+
# minor tick mark 색상 변경
|
|
228
|
+
ax.tick_params('x', which='minor', colors=self.CONFIG.AX.TICK.edgecolor)
|
|
229
|
+
# 틱 라벨 색상
|
|
230
|
+
ticklabels: list[Text] = ax.get_xticklabels(minor=True) + ax.get_yticklabels()
|
|
231
|
+
for ticklabel in ticklabels:
|
|
232
|
+
ticklabel.set_color(self.CONFIG.AX.TICK.fontcolor)
|
|
233
|
+
|
|
234
|
+
ax.yaxis.set_ticks_position('right')
|
|
235
|
+
# 상단 슬라이더의 x tick 하단 설정
|
|
236
|
+
self._ax_slider_top.xaxis.set_ticks_position('bottom')
|
|
237
|
+
# 하단 슬라이더의 x tick 상단 설정
|
|
238
|
+
self._ax_slider_bottom.xaxis.set_ticks_position('top')
|
|
239
|
+
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class CanvasMixin(FigureMixin, AxesMixin):
|
|
244
|
+
slider_top = True
|
|
245
|
+
|
|
246
|
+
figure: Figure
|
|
247
|
+
|
|
248
|
+
ax_legend: Axes
|
|
249
|
+
ax_price: Axes
|
|
250
|
+
ax_volume: Axes
|
|
251
|
+
ax_slider: Axes
|
|
252
|
+
ax_none: Axes
|
|
253
|
+
_ax_slider_top: Axes
|
|
254
|
+
_ax_slider_bottom: Axes
|
|
255
|
+
|
|
256
|
+
def __init__(self, config=SLIDERCONFIG):
|
|
257
|
+
super().__init__(config=config)
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from matplotlib.axes import Axes
|
|
2
|
+
from matplotlib.collections import LineCollection
|
|
3
|
+
from matplotlib.text import Text
|
|
4
|
+
|
|
5
|
+
from ..._config import SliderConfigData
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Base:
|
|
9
|
+
CONFIG: SliderConfigData
|
|
10
|
+
|
|
11
|
+
ax_slider: Axes
|
|
12
|
+
ax_price: Axes
|
|
13
|
+
ax_volume: Axes
|
|
14
|
+
ax_legend: Axes
|
|
15
|
+
|
|
16
|
+
add_artists: callable
|
|
17
|
+
set_artists: callable
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CollectionMixin(Base):
|
|
21
|
+
def add_artists(self):
|
|
22
|
+
super().add_artists()
|
|
23
|
+
|
|
24
|
+
self.collection_slider = LineCollection([])
|
|
25
|
+
self.collection_nav = LineCollection([])
|
|
26
|
+
self.collection_slider_vline = LineCollection([])
|
|
27
|
+
self.artist_text_slider = Text('')
|
|
28
|
+
|
|
29
|
+
for artist in [
|
|
30
|
+
self.collection_slider,
|
|
31
|
+
self.collection_nav,
|
|
32
|
+
self.collection_slider_vline,
|
|
33
|
+
self.artist_text_slider
|
|
34
|
+
]:
|
|
35
|
+
self.ax_slider.add_artist(artist)
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
def set_artists(self):
|
|
39
|
+
super().set_artists()
|
|
40
|
+
|
|
41
|
+
self._set_slider_artists()
|
|
42
|
+
self._set_slider_test()
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
def _set_slider_artists(self):
|
|
46
|
+
self.collection_slider.set_animated(True)
|
|
47
|
+
self.collection_slider.set_transform(self.ax_slider.transData)
|
|
48
|
+
|
|
49
|
+
color_overay = self.CONFIG.SLIDER.NAV.facecolor
|
|
50
|
+
color_line = self.CONFIG.SLIDER.NAV.edgecolor
|
|
51
|
+
colors = [color_overay, color_overay, color_line, color_line]
|
|
52
|
+
# print(f'{colors=}')
|
|
53
|
+
self.collection_nav.set_linewidth(0.1)
|
|
54
|
+
self.collection_nav.set_facecolor(colors)
|
|
55
|
+
|
|
56
|
+
# facecolor alpha값 제거
|
|
57
|
+
colors = []
|
|
58
|
+
for c in self.collection_nav.get_facecolor():
|
|
59
|
+
# print(c)
|
|
60
|
+
colors.append(c[:3])
|
|
61
|
+
self.collection_nav.set_facecolor(colors)
|
|
62
|
+
|
|
63
|
+
alpha = self.CONFIG.SLIDER.NAV.alpha
|
|
64
|
+
self.collection_nav.set_alpha([alpha, alpha, 1, 1])
|
|
65
|
+
|
|
66
|
+
self.collection_nav.set_edgecolor([(0, 0, 0, 0) for _ in colors])
|
|
67
|
+
self.collection_nav.set_animated(True)
|
|
68
|
+
self.collection_nav.set_transform(self.ax_slider.transData)
|
|
69
|
+
|
|
70
|
+
kwargs = self.CONFIG.CURSOR.CROSSLINE.__dict__
|
|
71
|
+
kwargs.update({'segments': [], 'animated': True})
|
|
72
|
+
self.collection_slider_vline.set(**kwargs)
|
|
73
|
+
|
|
74
|
+
kwargs = self.CONFIG.CURSOR.TEXT.to_dict()
|
|
75
|
+
kwargs.update({'text': ' ', 'animated': True})
|
|
76
|
+
self.artist_text_slider.set(**kwargs)
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
def _set_slider_test(self):
|
|
80
|
+
kwargs = self.CONFIG.CURSOR.TEXT.to_dict()
|
|
81
|
+
kwargs.update(animated=True, verticalalignment='top', horizontalalignment='center')
|
|
82
|
+
self.artist_text_slider.set(**kwargs)
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ArtistMixin(CollectionMixin):
|
|
87
|
+
collection_slider: LineCollection
|
|
88
|
+
collection_nav: LineCollection
|
|
89
|
+
collection_slider_vline: LineCollection
|
|
90
|
+
artist_text_slider: Text
|
|
91
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from matplotlib.axes import Axes
|
|
2
|
+
from matplotlib.collections import LineCollection
|
|
3
|
+
from matplotlib.text import Text
|
|
4
|
+
|
|
5
|
+
from .a_canvas import Figure
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Base:
|
|
9
|
+
figure: Figure
|
|
10
|
+
|
|
11
|
+
ax_slider: Axes
|
|
12
|
+
|
|
13
|
+
collection_slider: LineCollection
|
|
14
|
+
collection_nav: LineCollection
|
|
15
|
+
collection_slider_vline: LineCollection
|
|
16
|
+
artist_text_slider: Text
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SliderMixin(Base):
|
|
20
|
+
def _draw_ax_slider(self):
|
|
21
|
+
renderer = self.figure.canvas.renderer
|
|
22
|
+
|
|
23
|
+
self.ax_slider.xaxis.draw(renderer)
|
|
24
|
+
self.ax_slider.yaxis.draw(renderer)
|
|
25
|
+
|
|
26
|
+
self.collection_slider.draw(renderer)
|
|
27
|
+
c = self.collection_slider.get_edgecolor()
|
|
28
|
+
# print(f'{c=}')
|
|
29
|
+
s = self.collection_slider.get_segments()
|
|
30
|
+
# print(f'{s=}')
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
def _draw_nav(self):
|
|
34
|
+
renderer = self.figure.canvas.renderer
|
|
35
|
+
|
|
36
|
+
self.collection_nav.draw(renderer)
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
def _draw_slider_vline(self):
|
|
40
|
+
renderer = self.figure.canvas.renderer
|
|
41
|
+
|
|
42
|
+
self.collection_slider_vline.draw(renderer)
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
def _draw_slider_text(self):
|
|
46
|
+
renderer = self.figure.canvas.renderer
|
|
47
|
+
|
|
48
|
+
self.artist_text_slider.draw(renderer)
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DrawMixin(SliderMixin):
|
|
53
|
+
pass
|
|
54
|
+
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
from matplotlib.axes import Axes
|
|
2
|
+
from matplotlib.backend_bases import MouseEvent
|
|
3
|
+
from matplotlib.collections import LineCollection
|
|
4
|
+
from matplotlib.text import Text
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from ..._config import SliderConfigData
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Base:
|
|
12
|
+
CONFIG: SliderConfigData
|
|
13
|
+
df: pd.DataFrame
|
|
14
|
+
|
|
15
|
+
ax_slider: Axes
|
|
16
|
+
collection_slider: LineCollection
|
|
17
|
+
collection_nav: LineCollection
|
|
18
|
+
collection_slider_vline: LineCollection
|
|
19
|
+
artist_text_slider: Text
|
|
20
|
+
|
|
21
|
+
set_segments: callable
|
|
22
|
+
set_color_segments: callable
|
|
23
|
+
|
|
24
|
+
slider_ymin: float
|
|
25
|
+
slider_ymax: float
|
|
26
|
+
|
|
27
|
+
slider_xmin: float
|
|
28
|
+
slider_xmax: float
|
|
29
|
+
_nav_width: float
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SliderMixin(Base):
|
|
33
|
+
def _set_slider_collection(self):
|
|
34
|
+
keys = []
|
|
35
|
+
for i in reversed(self.CONFIG.MA.ma_list):
|
|
36
|
+
keys.append('x')
|
|
37
|
+
keys.append(f'ma{i}')
|
|
38
|
+
|
|
39
|
+
series = self.df[keys + ['x', 'close']]
|
|
40
|
+
series['x'] = series['x'] - 0.5
|
|
41
|
+
segment_slider = series.values
|
|
42
|
+
sizes = [segment_slider.shape[0], len(self.CONFIG.MA.ma_list)+1, 2]
|
|
43
|
+
segment_slider = segment_slider.reshape(*sizes).swapaxes(0, 1)
|
|
44
|
+
self.collection_slider.set_segments(segment_slider)
|
|
45
|
+
|
|
46
|
+
linewidth = []
|
|
47
|
+
ma_colors = []
|
|
48
|
+
for n, _ in enumerate(self.CONFIG.MA.ma_list):
|
|
49
|
+
linewidth.append(0.9)
|
|
50
|
+
try:
|
|
51
|
+
ma_colors.append(self.CONFIG.MA.color_list[n])
|
|
52
|
+
except:
|
|
53
|
+
ma_colors.append(self.CONFIG.MA.color_default)
|
|
54
|
+
|
|
55
|
+
self.collection_slider.set_edgecolor(ma_colors + [self.CONFIG.CANDLE.line_color])
|
|
56
|
+
|
|
57
|
+
self.collection_slider.set_linewidth(linewidth + [2.4])
|
|
58
|
+
self.collection_slider.set_edgecolor(ma_colors + [self.CONFIG.CANDLE.line_color])
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
def _set_slider_color_collection(self):
|
|
62
|
+
ma_colors = []
|
|
63
|
+
for n, _ in enumerate(self.CONFIG.MA.ma_list):
|
|
64
|
+
try:
|
|
65
|
+
ma_colors.append(self.CONFIG.MA.color_list[n])
|
|
66
|
+
except:
|
|
67
|
+
ma_colors.append(self.CONFIG.MA.color_default)
|
|
68
|
+
|
|
69
|
+
self.collection_slider.set_edgecolor(ma_colors + [self.CONFIG.CANDLE.line_color])
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class VlineMixin(Base):
|
|
74
|
+
in_slider: bool
|
|
75
|
+
|
|
76
|
+
def _set_slider_vline(self, e: MouseEvent):
|
|
77
|
+
xdata = e.xdata
|
|
78
|
+
if xdata is None:
|
|
79
|
+
return
|
|
80
|
+
xdata = round(xdata, 2)
|
|
81
|
+
# print(f'{xdata=}')
|
|
82
|
+
if not self.in_slider:
|
|
83
|
+
xdata -= 0.5
|
|
84
|
+
|
|
85
|
+
seg = [((xdata, self.slider_ymin), (xdata, self.slider_ymax))]
|
|
86
|
+
# print(f'{seg=}')
|
|
87
|
+
self.collection_slider_vline.set_segments(seg)
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
def _set_slider_text(self, e: MouseEvent):
|
|
91
|
+
xdata = e.xdata
|
|
92
|
+
if xdata is None:
|
|
93
|
+
return
|
|
94
|
+
idx = round(xdata)
|
|
95
|
+
if idx < 0:
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
text = self.df.iloc[idx]['date']
|
|
100
|
+
except:
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
self.artist_text_slider.set_text(text)
|
|
104
|
+
self.artist_text_slider.set_x(round(xdata, 2))
|
|
105
|
+
return 1
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class NavMixin(Base):
|
|
109
|
+
segment_nav: np.ndarray
|
|
110
|
+
|
|
111
|
+
def _set_nav_segment(self, xmin, xmax):
|
|
112
|
+
xmin0 = xmin - self._nav_width
|
|
113
|
+
xmin1 = xmin
|
|
114
|
+
xmax0 = xmax
|
|
115
|
+
xmax1 = xmax + self._nav_width
|
|
116
|
+
seg = [
|
|
117
|
+
# 좌측 오버레이
|
|
118
|
+
(
|
|
119
|
+
(self.slider_xmin, self.slider_ymax),
|
|
120
|
+
(xmin0, self.slider_ymax),
|
|
121
|
+
(xmin0, self.slider_ymin),
|
|
122
|
+
(self.slider_xmin, self.slider_ymin),
|
|
123
|
+
),
|
|
124
|
+
# 우측 오버레이
|
|
125
|
+
(
|
|
126
|
+
(xmax1, self.slider_ymax),
|
|
127
|
+
(self.slider_xmax, self.slider_ymax),
|
|
128
|
+
(self.slider_xmax, self.slider_ymin),
|
|
129
|
+
(xmax1, self.slider_ymin),
|
|
130
|
+
),
|
|
131
|
+
# 좌측 네비게이터
|
|
132
|
+
(
|
|
133
|
+
(xmin1, self.slider_ymax),
|
|
134
|
+
(xmin1, self.slider_ymin),
|
|
135
|
+
(xmin0, self.slider_ymin),
|
|
136
|
+
(xmin0, self.slider_ymax),
|
|
137
|
+
),
|
|
138
|
+
# 우측 네비게이터
|
|
139
|
+
(
|
|
140
|
+
(xmax0, self.slider_ymin),
|
|
141
|
+
(xmax0, self.slider_ymax),
|
|
142
|
+
(xmax1, self.slider_ymax),
|
|
143
|
+
(xmax1, self.slider_ymin),
|
|
144
|
+
),
|
|
145
|
+
]
|
|
146
|
+
self.segment_nav = np.array(seg)
|
|
147
|
+
|
|
148
|
+
self.collection_nav.set_segments(self.segment_nav)
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class SegmentMixin(SliderMixin, VlineMixin, NavMixin):
|
|
153
|
+
segment_nav: np.ndarray
|
|
154
|
+
|
|
155
|
+
def set_segments(self):
|
|
156
|
+
super().set_segments()
|
|
157
|
+
|
|
158
|
+
self._set_slider_collection()
|
|
159
|
+
self._set_slider_color_collection()
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
def set_color_segments(self):
|
|
163
|
+
super().set_color_segments()
|
|
164
|
+
|
|
165
|
+
self._set_slider_color_collection()
|
|
166
|
+
return
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from matplotlib.collections import LineCollection
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Base:
|
|
6
|
+
limit_volume = 200
|
|
7
|
+
limit_ma = 8_000
|
|
8
|
+
|
|
9
|
+
x_click: int
|
|
10
|
+
vxmin: int
|
|
11
|
+
vxmax: int
|
|
12
|
+
|
|
13
|
+
axis: callable
|
|
14
|
+
_set_nav_segment: callable
|
|
15
|
+
set_collections: callable
|
|
16
|
+
|
|
17
|
+
key_volume: str
|
|
18
|
+
segment_volume_wick: np.ndarray
|
|
19
|
+
_set_volume_collection_wick_segments: callable
|
|
20
|
+
_set_ma_collection_segments: callable
|
|
21
|
+
facecolor_volume: np.ndarray
|
|
22
|
+
edgecolor_volume: np.ndarray
|
|
23
|
+
collection_volume: LineCollection
|
|
24
|
+
collection_ma: LineCollection
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Mixin(Base):
|
|
28
|
+
def axis(self, xmin, *, xmax):
|
|
29
|
+
super().axis(xmin, xmax=xmax)
|
|
30
|
+
self._set_nav_segment(self.vxmin, xmax=self.vxmax-1)
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
def _set_volume_collection_wick_segments(self, ind_start, ind_end):
|
|
34
|
+
if not self.key_volume or not self.x_click:
|
|
35
|
+
super()._set_volume_collection_wick_segments(ind_start, ind_end=ind_end)
|
|
36
|
+
else:
|
|
37
|
+
indsub = ind_end - ind_start
|
|
38
|
+
if indsub <= self.limit_volume:
|
|
39
|
+
super()._set_volume_collection_wick_segments(ind_start, ind_end=ind_end)
|
|
40
|
+
else:
|
|
41
|
+
# 일부 거래량만 그리기
|
|
42
|
+
seg_volume = self.segment_volume_wick[ind_start:ind_end]
|
|
43
|
+
values = seg_volume[:, 1, 1]
|
|
44
|
+
top_index = np.argsort(-values)[:self.limit_volume]
|
|
45
|
+
seg = seg_volume[top_index]
|
|
46
|
+
facecolors = self.facecolor_volume[ind_start:ind_end][top_index]
|
|
47
|
+
edgecolors = self.edgecolor_volume[ind_start:ind_end][top_index]
|
|
48
|
+
self.collection_volume.set_segments(seg)
|
|
49
|
+
self.collection_volume.set_linewidth(1.3)
|
|
50
|
+
self.collection_volume.set_facecolor(facecolors)
|
|
51
|
+
self.collection_volume.set_edgecolor(edgecolors)
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
def _set_ma_collection_segments(self, ind_start, ind_end):
|
|
55
|
+
if not self.x_click:
|
|
56
|
+
super()._set_ma_collection_segments(ind_start, ind_end=ind_end)
|
|
57
|
+
else:
|
|
58
|
+
indsub = ind_end - ind_start
|
|
59
|
+
if indsub <= self.limit_ma:
|
|
60
|
+
super()._set_ma_collection_segments(ind_start, ind_end=ind_end)
|
|
61
|
+
else:
|
|
62
|
+
# 이평선 그리지 않기
|
|
63
|
+
self.collection_ma.set_segments([])
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class AxisMixin(Mixin):
|
|
68
|
+
limit_volume = 200
|
|
69
|
+
limit_ma = 8_000
|
|
70
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from matplotlib.axes import Axes
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Base:
|
|
5
|
+
_background = None
|
|
6
|
+
_background_background = None
|
|
7
|
+
|
|
8
|
+
_creating_background = False
|
|
9
|
+
|
|
10
|
+
draw_background: callable
|
|
11
|
+
_draw_ax: callable
|
|
12
|
+
_draw_ax_slider: callable
|
|
13
|
+
draw_artists: callable
|
|
14
|
+
_draw_nav: callable
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SliderMixin(Base):
|
|
18
|
+
ax_slider: Axes
|
|
19
|
+
|
|
20
|
+
def draw_background(self):
|
|
21
|
+
self._draw_ax(self.ax_slider)
|
|
22
|
+
self._draw_ax_slider()
|
|
23
|
+
|
|
24
|
+
super().draw_background()
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
def draw_artists(self):
|
|
28
|
+
super().draw_artists()
|
|
29
|
+
|
|
30
|
+
self._draw_nav()
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BackgroundMixin(SliderMixin):
|
|
35
|
+
_background = None
|
|
36
|
+
_creating_background = False
|
|
37
|
+
|