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,353 @@
|
|
|
1
|
+
from matplotlib.axes import Axes
|
|
2
|
+
from matplotlib.backend_bases import MouseEvent, MouseButton, cursors
|
|
3
|
+
from matplotlib.collections import LineCollection
|
|
4
|
+
from matplotlib.text import Text
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from ..cursor.g_event import EventMixin as Base
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseMixin(Base):
|
|
11
|
+
ax_slider: Axes
|
|
12
|
+
|
|
13
|
+
slider_xmin: int
|
|
14
|
+
slider_xmax: int
|
|
15
|
+
slider_ymin: float
|
|
16
|
+
slider_ymax: float
|
|
17
|
+
|
|
18
|
+
in_slider = False
|
|
19
|
+
is_click_slider = False
|
|
20
|
+
is_click_chart = False
|
|
21
|
+
is_move_chart = False
|
|
22
|
+
|
|
23
|
+
click_nav_left = False
|
|
24
|
+
click_nav_right = False
|
|
25
|
+
|
|
26
|
+
min_distance = 5
|
|
27
|
+
|
|
28
|
+
segment_nav: np.ndarray
|
|
29
|
+
|
|
30
|
+
_set_slider_vline: callable
|
|
31
|
+
_draw_slider_vline: callable
|
|
32
|
+
_set_slider_text: callable
|
|
33
|
+
_draw_slider_text: callable
|
|
34
|
+
draw_chart: callable
|
|
35
|
+
axis: callable
|
|
36
|
+
_set_cursor: callable
|
|
37
|
+
_move_chart: callable
|
|
38
|
+
_draw_nav: callable
|
|
39
|
+
_restore_region_background: callable
|
|
40
|
+
|
|
41
|
+
collection_slider_vline: LineCollection
|
|
42
|
+
artist_text_slider: Text
|
|
43
|
+
collection_nav: LineCollection
|
|
44
|
+
|
|
45
|
+
x_click: int
|
|
46
|
+
_nav_width: float
|
|
47
|
+
navcoordinate: tuple[int, int]
|
|
48
|
+
vxmin: int
|
|
49
|
+
vxmax: int
|
|
50
|
+
|
|
51
|
+
def get_nav_xlim(self):
|
|
52
|
+
seg = self.segment_nav
|
|
53
|
+
# print(f'{seg=}')
|
|
54
|
+
xmin = seg[-2][0][0]
|
|
55
|
+
xmax = seg[-1][0][0]
|
|
56
|
+
|
|
57
|
+
return (int(xmin), int(xmax))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class CursorMixin(BaseMixin):
|
|
61
|
+
def _set_cursor(self, e: MouseEvent):
|
|
62
|
+
# 마우스 커서 변경
|
|
63
|
+
if self.is_click_slider:
|
|
64
|
+
return
|
|
65
|
+
elif not self.in_slider:
|
|
66
|
+
self.figure.canvas.set_cursor(cursors.POINTER)
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
xmin, xmax = self.get_nav_xlim()
|
|
70
|
+
if xmin == xmax:
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
x = e.xdata
|
|
74
|
+
|
|
75
|
+
left0 = xmin - self._nav_width
|
|
76
|
+
left1 = xmin
|
|
77
|
+
|
|
78
|
+
if left0 <= x and x <= left1:
|
|
79
|
+
# 커서가 좌경계선 위에 위치
|
|
80
|
+
self.figure.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
right0 = xmax
|
|
84
|
+
right1 = xmax + self._nav_width
|
|
85
|
+
if right0 <= x and x <= right1:
|
|
86
|
+
# 커서가 우경계선 위에 위치
|
|
87
|
+
self.figure.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
if left1 < x and x < right0:
|
|
91
|
+
# 커서가 조회영역 위에 위치
|
|
92
|
+
self.figure.canvas.set_cursor(cursors.MOVE)
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
self.figure.canvas.set_cursor(cursors.POINTER)
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class AxMixin(BaseMixin):
|
|
100
|
+
def _check_ax(self, e: MouseEvent):
|
|
101
|
+
ax = e.inaxes
|
|
102
|
+
# print(f'{ax=}')
|
|
103
|
+
self.in_chart = False
|
|
104
|
+
self.in_slider, self.in_chart_price, self.in_chart_volume = (False, False, False)
|
|
105
|
+
|
|
106
|
+
if e.xdata is None or e.ydata is None:
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
if ax is self.ax_slider:
|
|
110
|
+
if (
|
|
111
|
+
(self.slider_xmin <= e.xdata and e.xdata <= self.slider_xmax)
|
|
112
|
+
and (self.slider_ymin <= e.ydata and e.ydata <= self.slider_ymax)
|
|
113
|
+
):
|
|
114
|
+
self.in_slider = True
|
|
115
|
+
else:
|
|
116
|
+
super()._check_ax(e)
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class ClickMixin(BaseMixin):
|
|
121
|
+
is_click_slider = False
|
|
122
|
+
is_click_chart = False
|
|
123
|
+
|
|
124
|
+
click_nav_left = False
|
|
125
|
+
click_nav_right = False
|
|
126
|
+
|
|
127
|
+
def on_click(self, e: MouseEvent):
|
|
128
|
+
if e.xdata is not None and e.ydata is not None:
|
|
129
|
+
if self.in_chart and not self.is_click_chart:
|
|
130
|
+
if e.button == MouseButton.LEFT:
|
|
131
|
+
self._on_click_chart(e)
|
|
132
|
+
elif self.in_slider and not self.is_click_slider:
|
|
133
|
+
if e.button == MouseButton.LEFT:
|
|
134
|
+
self._on_click_slider(e)
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
def _on_click_chart(self, e: MouseEvent):
|
|
138
|
+
# 조회영역 이동 시작
|
|
139
|
+
self.is_click_chart = True
|
|
140
|
+
self.is_move_chart = True
|
|
141
|
+
|
|
142
|
+
x = int(e.xdata)
|
|
143
|
+
self.x_click = x
|
|
144
|
+
|
|
145
|
+
xmin, xmax = self.get_nav_xlim()
|
|
146
|
+
self.navcoordinate = (xmin, xmax)
|
|
147
|
+
|
|
148
|
+
self.figure.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
def _on_click_slider(self, e: MouseEvent):
|
|
152
|
+
self.is_click_slider = True
|
|
153
|
+
self.figure.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
|
|
154
|
+
|
|
155
|
+
xmin, xmax = self.get_nav_xlim()
|
|
156
|
+
x = round(e.xdata)
|
|
157
|
+
|
|
158
|
+
xmin0 = xmin - self._nav_width
|
|
159
|
+
xmin1 = xmin
|
|
160
|
+
if xmin0 <= x and x <= xmin1:
|
|
161
|
+
# 좌경계선 이동 시작
|
|
162
|
+
self.navcoordinate = (xmin, xmax)
|
|
163
|
+
self.click_nav_left = True
|
|
164
|
+
self.x_click = xmin
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
xmax0 = xmax
|
|
168
|
+
if xmin1 < x and x < xmax0:
|
|
169
|
+
# 조회영역 이동 시작
|
|
170
|
+
self.navcoordinate = (xmin, xmax)
|
|
171
|
+
self.is_move_chart = True
|
|
172
|
+
self.x_click = x
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
if xmax0 <= x and x <= (xmax + self._nav_width):
|
|
176
|
+
# 우경계선 이동 시작
|
|
177
|
+
self.navcoordinate = (xmin, xmax)
|
|
178
|
+
self.click_nav_right = True
|
|
179
|
+
self.x_click = xmax
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
self.navcoordinate = (xmin, xmax)
|
|
183
|
+
self.x_click = x
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class ReleaseMixin(BaseMixin):
|
|
188
|
+
def on_release(self, e: MouseEvent):
|
|
189
|
+
if e.button == MouseButton.LEFT:
|
|
190
|
+
if (
|
|
191
|
+
self.is_click_chart
|
|
192
|
+
and self.in_chart
|
|
193
|
+
):
|
|
194
|
+
self._on_release_chart(e)
|
|
195
|
+
elif self.is_click_slider and self.in_slider:
|
|
196
|
+
self._on_release_slider(e)
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
def _on_release_chart(self, e):
|
|
200
|
+
self.x_click = None
|
|
201
|
+
self.is_click_chart = False
|
|
202
|
+
self.is_move_chart = False
|
|
203
|
+
self.figure.canvas.set_cursor(cursors.POINTER)
|
|
204
|
+
|
|
205
|
+
xmin, xmax = self.navcoordinate
|
|
206
|
+
self.axis(xmin, xmax=xmax)
|
|
207
|
+
self.figure.canvas.draw()
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
def _on_release_slider(self, e: MouseEvent):
|
|
211
|
+
if self.x_click:
|
|
212
|
+
xmin, xmax = self.get_nav_xlim()
|
|
213
|
+
xsub = xmax - xmin
|
|
214
|
+
min_distance = 5 if not self.min_distance or self.min_distance < 5 else self.min_distance
|
|
215
|
+
# print(f'{xsub=}')
|
|
216
|
+
# print(f'{min_distance=}')
|
|
217
|
+
if min_distance <= xsub:
|
|
218
|
+
self.navcoordinate = (xmin, xmax)
|
|
219
|
+
else:
|
|
220
|
+
xmin, xmax = self.navcoordinate
|
|
221
|
+
|
|
222
|
+
self.x_click = None
|
|
223
|
+
self.is_click_slider = False
|
|
224
|
+
self.is_move_chart = False
|
|
225
|
+
self.click_nav_left, self.click_nav_right = (False, False)
|
|
226
|
+
self.axis(xmin, xmax=xmax)
|
|
227
|
+
|
|
228
|
+
self.figure.canvas.draw()
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class ChartMixin(BaseMixin):
|
|
233
|
+
def _move_chart(self, e: MouseEvent):
|
|
234
|
+
if self.is_click_chart and self.in_chart:
|
|
235
|
+
xdata = int(e.xdata)
|
|
236
|
+
# print(f'{(self.x_click, xdata)=}')
|
|
237
|
+
xsub = self.x_click - xdata
|
|
238
|
+
if xsub:
|
|
239
|
+
pre_xmin, pre_xmax = self.navcoordinate
|
|
240
|
+
xmin, xmax = (pre_xmin+xsub, pre_xmax+xsub)
|
|
241
|
+
# print(f'{(xmin, xmax)=}')
|
|
242
|
+
if 0 <= xmax and xmin <= self.index_list[-1] and xmin != xmax:
|
|
243
|
+
self.axis(xmin, xmax=xmax)
|
|
244
|
+
self.navcoordinate = (xmin, xmax)
|
|
245
|
+
elif self.is_click_slider and self.in_slider:
|
|
246
|
+
xdata = round(e.xdata)
|
|
247
|
+
pre_xmin, pre_xmax = self.navcoordinate
|
|
248
|
+
if self.click_nav_left:
|
|
249
|
+
if xdata < pre_xmax:
|
|
250
|
+
xmin, xmax = (xdata, pre_xmax)
|
|
251
|
+
else:
|
|
252
|
+
xmin, xmax = (pre_xmax, xdata)
|
|
253
|
+
elif self.click_nav_right:
|
|
254
|
+
if xdata < pre_xmin:
|
|
255
|
+
xmin, xmax = (xdata, pre_xmin)
|
|
256
|
+
else:
|
|
257
|
+
xmin, xmax = (pre_xmin, xdata)
|
|
258
|
+
else:
|
|
259
|
+
if self.is_move_chart:
|
|
260
|
+
xsub = self.x_click - xdata
|
|
261
|
+
xmax = -1
|
|
262
|
+
if xsub:
|
|
263
|
+
pre_xmin, pre_xmax = self.navcoordinate
|
|
264
|
+
xmin, xmax = (pre_xmin-xsub, pre_xmax-xsub)
|
|
265
|
+
else:
|
|
266
|
+
if xdata == self.x_click:
|
|
267
|
+
xmax = -1
|
|
268
|
+
elif xdata < self.x_click:
|
|
269
|
+
xmin, xmax = (xdata, self.x_click)
|
|
270
|
+
else:
|
|
271
|
+
xmin, xmax = (self.x_click, xdata)
|
|
272
|
+
|
|
273
|
+
if 0 <= xmax and xmin <= self.index_list[-1] and xmin != xmax:
|
|
274
|
+
self.axis(xmin, xmax=xmax)
|
|
275
|
+
return
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class MoveMixin(BaseMixin):
|
|
279
|
+
def _set_and_draw_vline(self, e: MouseEvent):
|
|
280
|
+
self._set_slider_vline(e)
|
|
281
|
+
self._draw_slider_vline()
|
|
282
|
+
if self.in_slider and self._set_slider_text(e):
|
|
283
|
+
self._draw_slider_text()
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
def _set_and_draw_crossline(self, e: MouseEvent):
|
|
287
|
+
self._set_and_draw_vline(e)
|
|
288
|
+
|
|
289
|
+
super()._set_and_draw_crossline(e)
|
|
290
|
+
return
|
|
291
|
+
|
|
292
|
+
def _on_move_action(self, e: MouseEvent):
|
|
293
|
+
if e.xdata is not None:
|
|
294
|
+
if self.x_click is not None:
|
|
295
|
+
self._restore_region_background()
|
|
296
|
+
|
|
297
|
+
self._move_chart(e)
|
|
298
|
+
self.draw_chart()
|
|
299
|
+
self._draw_nav()
|
|
300
|
+
if self.in_slider:
|
|
301
|
+
self._set_and_draw_vline(e)
|
|
302
|
+
|
|
303
|
+
self.figure.canvas.blit()
|
|
304
|
+
self.figure.canvas.flush_events()
|
|
305
|
+
else:
|
|
306
|
+
self._set_cursor(e)
|
|
307
|
+
|
|
308
|
+
if self.in_slider:
|
|
309
|
+
self._restore_region()
|
|
310
|
+
|
|
311
|
+
self._set_and_draw_vline(e)
|
|
312
|
+
|
|
313
|
+
self.figure.canvas.blit()
|
|
314
|
+
self.figure.canvas.flush_events()
|
|
315
|
+
else:
|
|
316
|
+
super()._on_move_action(e)
|
|
317
|
+
else:
|
|
318
|
+
super()._on_move_action(e)
|
|
319
|
+
return
|
|
320
|
+
|
|
321
|
+
def need_restore(self):
|
|
322
|
+
if not super().need_restore():
|
|
323
|
+
if self.collection_slider_vline.get_segments():
|
|
324
|
+
self.collection_slider_vline.set_segments([])
|
|
325
|
+
return True
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class EventMixin(AxMixin, CursorMixin, ClickMixin, ReleaseMixin, ChartMixin, MoveMixin,):
|
|
330
|
+
x_click = None
|
|
331
|
+
in_chart = False
|
|
332
|
+
in_slider = False
|
|
333
|
+
in_chart_price = False
|
|
334
|
+
in_chart_volume = False
|
|
335
|
+
|
|
336
|
+
is_move_chart = False
|
|
337
|
+
is_click_slider = False
|
|
338
|
+
is_click_chart = False
|
|
339
|
+
x_click: float
|
|
340
|
+
|
|
341
|
+
click_nav_left = False
|
|
342
|
+
click_nav_right = False
|
|
343
|
+
|
|
344
|
+
min_distance = 5
|
|
345
|
+
|
|
346
|
+
navcoordinate: tuple[int, int]
|
|
347
|
+
|
|
348
|
+
def connect_events(self):
|
|
349
|
+
super().connect_events()
|
|
350
|
+
|
|
351
|
+
self.figure.canvas.mpl_connect('button_press_event', lambda x: self.on_click(x))
|
|
352
|
+
self.figure.canvas.mpl_connect('button_release_event', lambda x: self.on_release(x))
|
|
353
|
+
return
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from matplotlib.axes import Axes
|
|
2
|
+
from matplotlib.text import Text
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
from ..._config import ConfigData
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Base:
|
|
9
|
+
CONFIG: ConfigData
|
|
10
|
+
df: pd.DataFrame
|
|
11
|
+
|
|
12
|
+
key_date = 'date'
|
|
13
|
+
key_open, key_high, key_low, key_close = ('open', 'high', 'low', 'close')
|
|
14
|
+
key_volume = 'volume'
|
|
15
|
+
|
|
16
|
+
ax_slider: Axes
|
|
17
|
+
|
|
18
|
+
index_list: list[int] = []
|
|
19
|
+
|
|
20
|
+
artist_text_slider: Text
|
|
21
|
+
|
|
22
|
+
axis: callable
|
|
23
|
+
get_default_xlim: callable
|
|
24
|
+
set_variables: callable
|
|
25
|
+
|
|
26
|
+
_set_slider_collection: callable
|
|
27
|
+
vxmin: int
|
|
28
|
+
vxmax: int
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SliderMixin(Base):
|
|
32
|
+
min_distance = 5
|
|
33
|
+
|
|
34
|
+
def get_default_xlim(self):
|
|
35
|
+
xmax = self.index_list[-1] + 1
|
|
36
|
+
xmin = xmax - 120
|
|
37
|
+
if xmin < 0:
|
|
38
|
+
xmin = 0
|
|
39
|
+
return (xmin, xmax)
|
|
40
|
+
|
|
41
|
+
def set_variables(self):
|
|
42
|
+
super().set_variables()
|
|
43
|
+
|
|
44
|
+
self._set_slider()
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
def _set_slider(self):
|
|
48
|
+
# print('_set_slider')
|
|
49
|
+
self._set_slider_xtick()
|
|
50
|
+
|
|
51
|
+
xmax = self.index_list[-1]
|
|
52
|
+
# 슬라이더 xlim
|
|
53
|
+
xdistance = round(xmax / 30)
|
|
54
|
+
self.slider_xmin, self.slider_xmax = (-xdistance, xmax+xdistance)
|
|
55
|
+
self.ax_slider.set_xlim(self.slider_xmin, self.slider_xmax)
|
|
56
|
+
|
|
57
|
+
# 네비게이터 경계선 두께
|
|
58
|
+
self._nav_width = round((self.slider_xmax - self.slider_xmin) / 250, 2)
|
|
59
|
+
|
|
60
|
+
# 슬라이더 ylim
|
|
61
|
+
ymin, ymax = (self.df['low'].min(), self.df['high'].max())
|
|
62
|
+
ysub = ymax - ymin
|
|
63
|
+
ydistance = round(ysub / 5, self.CONFIG.UNIT.digit+2)
|
|
64
|
+
self.slider_ymin, self.slider_ymax = (ymin-ydistance, ymax+ydistance)
|
|
65
|
+
self.ax_slider.set_ylim(self.slider_ymin, self.slider_ymax)
|
|
66
|
+
|
|
67
|
+
self._set_slider_collection()
|
|
68
|
+
self._set_slider_text_position()
|
|
69
|
+
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
def _set_slider_text_position(self):
|
|
73
|
+
# 슬라이더 텍스트 y
|
|
74
|
+
self.artist_text_slider.set_y(self.df['high'].max())
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
def _set_slider_xtick(self):
|
|
78
|
+
indices = [0, self.index_list[-1]]
|
|
79
|
+
# print(f'{indices=}')
|
|
80
|
+
|
|
81
|
+
date_list = [self.df.iloc[idx]['date'] for idx in indices]
|
|
82
|
+
# print(f'{date_list=}')
|
|
83
|
+
# xtick 설정, major tick과 겹쳐서 무시되는 것 방지
|
|
84
|
+
self.ax_slider.set_xticks([idx+0.01 for idx in indices], labels=date_list, minor=True)
|
|
85
|
+
|
|
86
|
+
labels = self.ax_slider.get_xticklabels(minor=True)
|
|
87
|
+
# print(f'{labels=}')
|
|
88
|
+
for label, align in zip(labels, ['center', 'center']):
|
|
89
|
+
# 라벨 텍스트 정렬
|
|
90
|
+
label.set_horizontalalignment(align)
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class DataMixin(SliderMixin):
|
|
95
|
+
min_distance = 5
|
|
96
|
+
_nav_width: float
|
|
97
|
+
|
|
98
|
+
slider_xmin: int
|
|
99
|
+
slider_xmax: int
|
|
100
|
+
slider_ymin: float
|
|
101
|
+
slider_ymax: float
|
|
102
|
+
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
name_pkg = 'seolpyo_mplchart'
|
|
4
|
+
path_pkg = Path(__file__)
|
|
5
|
+
while path_pkg.name != name_pkg:
|
|
6
|
+
path_pkg = path_pkg.parent
|
|
7
|
+
sys.path = [path_pkg.parent.__str__()] + sys.path
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
|
|
11
|
+
import pandas as pd
|
|
12
|
+
import matplotlib.pyplot as plt
|
|
13
|
+
|
|
14
|
+
from seolpyo_mplchart._utils.theme import set_theme
|
|
15
|
+
from seolpyo_mplchart._config import SLIDERCONFIG
|
|
16
|
+
from seolpyo_mplchart._chart.slider import Chart
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
path_file = path_pkg / 'sample' / 'samsung.txt'
|
|
20
|
+
with open(path_file, 'r', encoding='utf-8') as txt:
|
|
21
|
+
data = json.load(txt)
|
|
22
|
+
df = pd.DataFrame(data[:800])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class C(Chart):
|
|
26
|
+
limit_candle = 100
|
|
27
|
+
limit_wick = 300
|
|
28
|
+
limit_volume = 10
|
|
29
|
+
limit_ma = 200
|
|
30
|
+
t = 'light'
|
|
31
|
+
# watermark = ''
|
|
32
|
+
def __init__(self):
|
|
33
|
+
super().__init__()
|
|
34
|
+
# super().__init__(config=set_theme(SLIDERCONFIG, theme='dark'))
|
|
35
|
+
self.figure.canvas.mpl_connect('button_press_event', lambda x: self.theme(x))
|
|
36
|
+
|
|
37
|
+
def theme(self, e):
|
|
38
|
+
btn = getattr(e, 'button')
|
|
39
|
+
# print(f'{str(btn)=}')
|
|
40
|
+
if str(btn) == '3':
|
|
41
|
+
# print('refresh')
|
|
42
|
+
if self.t == 'light':
|
|
43
|
+
self.slider_top = True
|
|
44
|
+
self.t = 'dark'
|
|
45
|
+
# self.CONFIG.MA.linewidth = 1
|
|
46
|
+
self.get_candle_segment = lambda **x: Chart.get_candle_segment(self, **x)
|
|
47
|
+
self.CONFIG.CANDLE.linewidth = 0.8
|
|
48
|
+
else:
|
|
49
|
+
self.slider_top = False
|
|
50
|
+
self.t = 'light'
|
|
51
|
+
# self.CONFIG.MA.linewidth = 3
|
|
52
|
+
self.get_candle_segment = self.get_bar_segment
|
|
53
|
+
self.CONFIG.CANDLE.linewidth = 1.3
|
|
54
|
+
# print(f'{self.t=}')
|
|
55
|
+
self.CONFIG = set_theme(self.CONFIG, theme=self.t)
|
|
56
|
+
self.refresh()
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def run():
|
|
61
|
+
chart = C()
|
|
62
|
+
chart.set_data(df)
|
|
63
|
+
plt.show()
|
|
64
|
+
plt.close()
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == '__main__':
|
|
69
|
+
run()
|
|
70
|
+
|
|
71
|
+
|
|
@@ -22,6 +22,7 @@ CANDLEEDGECOLOR = CandleEdgeColorData()
|
|
|
22
22
|
class CandleData:
|
|
23
23
|
def __init__(self):
|
|
24
24
|
self.half_width = 0.24
|
|
25
|
+
self.linewidth = 0.8
|
|
25
26
|
self.line_color: str|tuple[float, float, float, float] = 'k'
|
|
26
27
|
self.FACECOLOR = CANDLEFACECOLOR
|
|
27
28
|
self.EDGECOLOR = CANDLEEDGECOLOR
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
class RatioData:
|
|
4
4
|
def __init__(self):
|
|
5
|
-
self.
|
|
6
|
-
self.price = 18
|
|
5
|
+
self.price = 5
|
|
7
6
|
self.volume = 5
|
|
8
7
|
|
|
9
8
|
RATIO = RatioData()
|
|
@@ -19,7 +18,7 @@ WATERMARK = WatermarkData()
|
|
|
19
18
|
class AdjustData:
|
|
20
19
|
def __init__(self):
|
|
21
20
|
# 여백
|
|
22
|
-
self.top = 0.
|
|
21
|
+
self.top = 0.98
|
|
23
22
|
self.bottom = 0.05
|
|
24
23
|
self.left = 0.01
|
|
25
24
|
self.right = 0.93
|
|
@@ -31,7 +30,7 @@ ADJUST = AdjustData()
|
|
|
31
30
|
|
|
32
31
|
class FigureData:
|
|
33
32
|
def __init__(self):
|
|
34
|
-
self.facecolor: str|tuple[float, float, float, float] = '
|
|
33
|
+
self.facecolor: str|tuple[float, float, float, float] = '#fafafa'
|
|
35
34
|
self.figsize = (14, 7)
|
|
36
35
|
self.RATIO = RATIO
|
|
37
36
|
self.ADJUST = ADJUST
|
seolpyo_mplchart/_config/ma.py
CHANGED
|
@@ -5,6 +5,8 @@ class MaData:
|
|
|
5
5
|
def __init__(self):
|
|
6
6
|
self.color_default: str|tuple[float, float, float, float] = 'k'
|
|
7
7
|
self.format = '{}일선'
|
|
8
|
+
self.linewidth = 1
|
|
9
|
+
self.ncol = 10
|
|
8
10
|
self.color_list: list[str|tuple[float, float, float, float]] = ['#8B00FF', '#008000', '#A0522D', '#008B8B', '#FF0080']
|
|
9
11
|
self.ma_list = (5, 20, 60, 120, 240)
|
|
10
12
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
def convert_num(num):
|
|
4
|
+
if isinstance(num, float) and num % 1:
|
|
5
|
+
return num
|
|
6
|
+
return int(num)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def float_to_str(num: float, *, digit=0, plus=False):
|
|
10
|
+
if 0 < digit:
|
|
11
|
+
num.__round__(digit)
|
|
12
|
+
text = f'{num:+,.{digit}f}' if plus else f'{num:,.{digit}f}'
|
|
13
|
+
else:
|
|
14
|
+
num = round(num, digit).__int__()
|
|
15
|
+
text = f'{num:+,}' if plus else f'{num:,}'
|
|
16
|
+
return text
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
data_unit_ko = {
|
|
20
|
+
'경': 10_000_000_000_000_000,
|
|
21
|
+
'조': 1_000_000_000_000,
|
|
22
|
+
'억': 100_000_000,
|
|
23
|
+
'만': 10_000,
|
|
24
|
+
}
|
|
25
|
+
def convert_unit(value, *, digit=0, word='원', unit_data: dict[str, int]=None):
|
|
26
|
+
if not unit_data:
|
|
27
|
+
unit_data = data_unit_ko
|
|
28
|
+
# print('ko')
|
|
29
|
+
# print(f'{value=:,}')
|
|
30
|
+
v = abs(value)
|
|
31
|
+
for unit, n in unit_data.items():
|
|
32
|
+
if n <= v:
|
|
33
|
+
# print(f'{n=:,}')
|
|
34
|
+
# print(f'{unit=}')
|
|
35
|
+
num = value / n
|
|
36
|
+
if word.startswith(' '):
|
|
37
|
+
return f'{float_to_str(num, digit=digit)}{unit}{word}'
|
|
38
|
+
return f'{float_to_str(num, digit=digit)}{unit} {word}'
|
|
39
|
+
|
|
40
|
+
if not value % 1:
|
|
41
|
+
value = int(value)
|
|
42
|
+
text = f'{float_to_str(value, digit=digit)}{word}'
|
|
43
|
+
# print(f'{text=}')
|
|
44
|
+
return text
|
|
45
|
+
|
|
46
|
+
data_unit_en = {
|
|
47
|
+
'Qd': 1_000_000_000_000_000,
|
|
48
|
+
'T': 1_000_000_000_000,
|
|
49
|
+
'B': 1_000_000_000,
|
|
50
|
+
'M': 1_000_000,
|
|
51
|
+
'K': 1_000,
|
|
52
|
+
}
|
|
53
|
+
def convert_unit_en(value, *, digit=0, word='$', unit_data: dict[str, int]=None):
|
|
54
|
+
if not unit_data:
|
|
55
|
+
unit_data = data_unit_en
|
|
56
|
+
# print('en')
|
|
57
|
+
# print(f'{value=:,}')
|
|
58
|
+
return convert_unit(value, digit=digit, word=word, unit_data=unit_data)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == '__main__':
|
|
62
|
+
a = 456.123
|
|
63
|
+
print(float_to_str(a))
|
|
64
|
+
print(float_to_str(a, 2))
|
|
65
|
+
print(float_to_str(a, 6))
|
|
66
|
+
|
|
67
|
+
|