seolpyo-mplchart 1.4.1__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 +53 -333
- seolpyo_mplchart/_chart/__init__.py +145 -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/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/_chart/test.py +121 -0
- seolpyo_mplchart/_config/__init__.py +3 -0
- seolpyo_mplchart/_config/ax.py +28 -0
- seolpyo_mplchart/_config/candle.py +31 -0
- seolpyo_mplchart/_config/config.py +21 -0
- seolpyo_mplchart/_config/cursor.py +49 -0
- seolpyo_mplchart/_config/figure.py +40 -0
- seolpyo_mplchart/_config/format.py +51 -0
- seolpyo_mplchart/_config/ma.py +17 -0
- seolpyo_mplchart/_config/slider/__init__.py +2 -0
- seolpyo_mplchart/_config/slider/config.py +24 -0
- seolpyo_mplchart/_config/slider/figure.py +19 -0
- seolpyo_mplchart/_config/slider/nav.py +10 -0
- seolpyo_mplchart/_config/unit.py +19 -0
- seolpyo_mplchart/_config/utils.py +67 -0
- seolpyo_mplchart/_config/volume.py +27 -0
- seolpyo_mplchart/_cursor.py +27 -25
- seolpyo_mplchart/_draw.py +7 -18
- seolpyo_mplchart/_slider.py +26 -20
- 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 +172 -56
- seolpyo_mplchart/xl_to_dict.py +47 -0
- seolpyo_mplchart-2.1.0.dist-info/METADATA +718 -0
- seolpyo_mplchart-2.1.0.dist-info/RECORD +89 -0
- {seolpyo_mplchart-1.4.1.dist-info → seolpyo_mplchart-2.1.0.dist-info}/WHEEL +1 -1
- seolpyo_mplchart-1.4.1.dist-info/METADATA +0 -57
- seolpyo_mplchart-1.4.1.dist-info/RECORD +0 -17
- {seolpyo_mplchart-1.4.1.dist-info → seolpyo_mplchart-2.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
from ..._config import ConfigData
|
|
4
|
+
|
|
5
|
+
class Base:
|
|
6
|
+
CONFIG: ConfigData
|
|
7
|
+
|
|
8
|
+
key_volume: str
|
|
9
|
+
df: pd.DataFrame
|
|
10
|
+
|
|
11
|
+
_add_columns: callable
|
|
12
|
+
set_variables: callable
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Mixin(Base):
|
|
16
|
+
def _add_columns(self):
|
|
17
|
+
super()._add_columns()
|
|
18
|
+
|
|
19
|
+
self.df['compare'] = (self.df['close'] - self.df['pre_close']).fillna(0)
|
|
20
|
+
self.df['rate'] = (self.df['compare'] * 100 / self.df['pre_close']).__round__(2).fillna(0)
|
|
21
|
+
self.df['rate_open'] = ((self.df['open'] - self.df['pre_close']) * 100 / self.df['pre_close']).__round__(2).fillna(0)
|
|
22
|
+
self.df['rate_high'] = ((self.df['high'] - self.df['pre_close']) * 100 / self.df['pre_close']).__round__(2).fillna(0)
|
|
23
|
+
self.df['rate_low'] = ((self.df['low'] - self.df['pre_close']) * 100 / self.df['pre_close']).__round__(2).fillna(0)
|
|
24
|
+
if self.key_volume:
|
|
25
|
+
self.df['pre_volume'] = self.df['volume'].shift(1)
|
|
26
|
+
self.df['compare_volume'] = (self.df['volume'] - self.df['pre_volume']).fillna(0)
|
|
27
|
+
self.df['rate_volume'] = (self.df['compare_volume'] * 100 / self.df['pre_volume']).__round__(2).fillna(0)
|
|
28
|
+
|
|
29
|
+
self.df['space_box_candle'] = (self.df['high'] - self.df['low']) / 5
|
|
30
|
+
self.df['box_candle_top'] = self.df['high'] + self.df['space_box_candle']
|
|
31
|
+
self.df['box_candle_bottom'] = self.df['low'] - self.df['space_box_candle']
|
|
32
|
+
self.df['box_candle_height'] = self.df['box_candle_top'] - self.df['box_candle_bottom']
|
|
33
|
+
|
|
34
|
+
if self.key_volume:
|
|
35
|
+
self.df['box_volume_top'] = self.df['volume'] * 1.15
|
|
36
|
+
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
def set_variables(self):
|
|
40
|
+
super().set_variables()
|
|
41
|
+
|
|
42
|
+
self._set_length_text()
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
def _set_length_text(self):
|
|
46
|
+
func = lambda x: len(self.CONFIG.UNIT.func(x, digit=self.CONFIG.UNIT.digit, word=self.CONFIG.UNIT.price))
|
|
47
|
+
self._length_text = self.df['high'].apply(func).max()
|
|
48
|
+
|
|
49
|
+
if self.key_volume:
|
|
50
|
+
func = lambda x: len(self.CONFIG.UNIT.func(x, digit=self.CONFIG.UNIT.digit_volume, word=self.CONFIG.UNIT.volume))
|
|
51
|
+
lenth_volume = self.df['volume'].apply(func).max()
|
|
52
|
+
# print(f'{self._length_text=}')
|
|
53
|
+
# print(f'{lenth_volume=}')
|
|
54
|
+
if self._length_text < lenth_volume:
|
|
55
|
+
self._length_text = lenth_volume
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class DataMixin(Mixin):
|
|
60
|
+
_length_text: int
|
|
61
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
|
|
15
|
+
from seolpyo_mplchart._utils.theme import set_theme
|
|
16
|
+
from seolpyo_mplchart._chart.cursor import Chart
|
|
17
|
+
from seolpyo_mplchart._config import DEFAULTCONFIG, DEFAULTCONFIG_EN
|
|
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[:300])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class C(Chart):
|
|
26
|
+
# limit_candle = 200
|
|
27
|
+
# limit_wick = 200
|
|
28
|
+
t = 'light'
|
|
29
|
+
# watermark = ''
|
|
30
|
+
def __init__(self):
|
|
31
|
+
super().__init__()
|
|
32
|
+
self.figure.canvas.mpl_connect('button_press_event', lambda x: self.theme(x))
|
|
33
|
+
self.CONFIG.FORMAT.candle += '\nCustom: {custom}'
|
|
34
|
+
|
|
35
|
+
def get_info_kwargs(self, is_price, **kwargs):
|
|
36
|
+
kwargs = super().get_info_kwargs(is_price, **kwargs)
|
|
37
|
+
kwargs['close'] = 'Cusotom close value'
|
|
38
|
+
kwargs['custom'] = 'this is custom add info kwargs'
|
|
39
|
+
return kwargs
|
|
40
|
+
|
|
41
|
+
def theme(self, e):
|
|
42
|
+
btn = getattr(e, 'button')
|
|
43
|
+
# print(f'{str(btn)=}')
|
|
44
|
+
if str(btn) == '3':
|
|
45
|
+
# print('refresh')
|
|
46
|
+
if self.t == 'light':
|
|
47
|
+
self.t = 'dark'
|
|
48
|
+
self.CONFIG = set_theme(DEFAULTCONFIG_EN, theme=self.t)
|
|
49
|
+
else:
|
|
50
|
+
self.t = 'light'
|
|
51
|
+
self.CONFIG = set_theme(DEFAULTCONFIG, theme=self.t)
|
|
52
|
+
# print(f'{self.t=}')
|
|
53
|
+
# self.CONFIG.MA.ma_list = []
|
|
54
|
+
self.refresh()
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def run():
|
|
59
|
+
chart = C()
|
|
60
|
+
chart.set_data(df)
|
|
61
|
+
plt.show()
|
|
62
|
+
plt.close()
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if __name__ == '__main__':
|
|
67
|
+
run()
|
|
68
|
+
|
|
69
|
+
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
from matplotlib.axes import Axes
|
|
2
|
+
from matplotlib.collections import LineCollection
|
|
3
|
+
from matplotlib.text import Text
|
|
4
|
+
from matplotlib.axes import Axes
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
from ..._config import SLIDERCONFIG, SliderConfigData
|
|
9
|
+
from ..base import Chart as BaseChart
|
|
10
|
+
from ..cursor import CursorMixin
|
|
11
|
+
|
|
12
|
+
from .a_canvas import CanvasMixin, Figure
|
|
13
|
+
from .b_artist import ArtistMixin
|
|
14
|
+
from .c_draw import DrawMixin
|
|
15
|
+
from .d_segment import SegmentMixin
|
|
16
|
+
from .e_axis import AxisMixin
|
|
17
|
+
from .f_background import BackgroundMixin
|
|
18
|
+
from .g_event import EventMixin
|
|
19
|
+
from .h_data import DataMixin
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SliderMixin(
|
|
23
|
+
CanvasMixin,
|
|
24
|
+
ArtistMixin,
|
|
25
|
+
DrawMixin,
|
|
26
|
+
SegmentMixin,
|
|
27
|
+
AxisMixin,
|
|
28
|
+
BackgroundMixin,
|
|
29
|
+
EventMixin,
|
|
30
|
+
DataMixin,
|
|
31
|
+
):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Chart(SliderMixin, CursorMixin, BaseChart):
|
|
36
|
+
candle_on_ma = True
|
|
37
|
+
slider_top = True
|
|
38
|
+
fraction = False
|
|
39
|
+
|
|
40
|
+
limit_candle = 400
|
|
41
|
+
limit_wick = 2_000
|
|
42
|
+
limit_volume = 200
|
|
43
|
+
limit_ma = 8_000
|
|
44
|
+
|
|
45
|
+
min_distance = 5
|
|
46
|
+
|
|
47
|
+
key_date = 'date'
|
|
48
|
+
key_open, key_high, key_low, key_close = ('open', 'high', 'low', 'close')
|
|
49
|
+
key_volume = 'volume'
|
|
50
|
+
|
|
51
|
+
index_list: list[int] = []
|
|
52
|
+
|
|
53
|
+
df: pd.DataFrame
|
|
54
|
+
|
|
55
|
+
CONFIG: SliderConfigData
|
|
56
|
+
|
|
57
|
+
figure: Figure
|
|
58
|
+
ax_legend: Axes
|
|
59
|
+
ax_price: Axes
|
|
60
|
+
ax_volume: Axes
|
|
61
|
+
|
|
62
|
+
ax_slider: Axes
|
|
63
|
+
ax_none: Axes
|
|
64
|
+
_ax_slider_top: Axes
|
|
65
|
+
_ax_slider_bottom: Axes
|
|
66
|
+
|
|
67
|
+
artist_watermark: Text
|
|
68
|
+
collection_candle: LineCollection
|
|
69
|
+
collection_volume: LineCollection
|
|
70
|
+
collection_ma: LineCollection
|
|
71
|
+
|
|
72
|
+
collection_slider: LineCollection
|
|
73
|
+
collection_nav: LineCollection
|
|
74
|
+
collection_slider_vline: LineCollection
|
|
75
|
+
artist_text_slider: Text
|
|
76
|
+
|
|
77
|
+
in_candle = False
|
|
78
|
+
in_volume = False
|
|
79
|
+
|
|
80
|
+
collection_price_crossline: LineCollection
|
|
81
|
+
collection_volume_crossline: LineCollection
|
|
82
|
+
|
|
83
|
+
artist_label_x: Text = None
|
|
84
|
+
artist_label_y: Text = None
|
|
85
|
+
|
|
86
|
+
collection_box_price: LineCollection
|
|
87
|
+
collection_box_volume: LineCollection
|
|
88
|
+
|
|
89
|
+
artist_info_candle: Text
|
|
90
|
+
artist_info_volume: Text
|
|
91
|
+
|
|
92
|
+
v0: int
|
|
93
|
+
v1: int
|
|
94
|
+
vmiddle: int
|
|
95
|
+
|
|
96
|
+
min_height_box_candle: float
|
|
97
|
+
min_height_box_volume: float
|
|
98
|
+
|
|
99
|
+
_length_text: int
|
|
100
|
+
|
|
101
|
+
_in_mouse_move = False
|
|
102
|
+
|
|
103
|
+
###
|
|
104
|
+
|
|
105
|
+
segment_nav: np.ndarray
|
|
106
|
+
segment_volume: np.ndarray
|
|
107
|
+
segment_volume_wick: np.ndarray
|
|
108
|
+
facecolor_volume: np.ndarray
|
|
109
|
+
edgecolor_volume: np.ndarray
|
|
110
|
+
|
|
111
|
+
segment_candle: np.ndarray
|
|
112
|
+
segment_candle_wick: np.ndarray
|
|
113
|
+
segment_priceline: np.ndarray
|
|
114
|
+
facecolor_candle: np.ndarray
|
|
115
|
+
edgecolor_candle: np.ndarray
|
|
116
|
+
|
|
117
|
+
segment_ma: np.ndarray
|
|
118
|
+
edgecolor_ma: np.ndarray
|
|
119
|
+
|
|
120
|
+
price_ymin: int
|
|
121
|
+
price_ymax: int
|
|
122
|
+
volume_ymax: int
|
|
123
|
+
|
|
124
|
+
chart_price_ymax: float
|
|
125
|
+
chart_volume_ymax: float
|
|
126
|
+
|
|
127
|
+
vxmin: int
|
|
128
|
+
vxmax: int
|
|
129
|
+
|
|
130
|
+
_nav_width: float
|
|
131
|
+
|
|
132
|
+
slider_xmin: int
|
|
133
|
+
slider_xmax: int
|
|
134
|
+
slider_ymin: float
|
|
135
|
+
slider_ymax: float
|
|
136
|
+
|
|
137
|
+
in_chart = False
|
|
138
|
+
in_slider = False
|
|
139
|
+
in_chart_price = False
|
|
140
|
+
in_chart_volume = False
|
|
141
|
+
|
|
142
|
+
is_move_chart = False
|
|
143
|
+
is_click_slider = False
|
|
144
|
+
is_click_chart = False
|
|
145
|
+
x_click: float
|
|
146
|
+
|
|
147
|
+
click_nav_left = False
|
|
148
|
+
click_nav_right = False
|
|
149
|
+
|
|
150
|
+
navcoordinate: tuple[int, int]
|
|
151
|
+
|
|
152
|
+
###
|
|
153
|
+
|
|
154
|
+
_visible_ma: set[int] = set()
|
|
155
|
+
_edgecolor_ma = []
|
|
156
|
+
|
|
157
|
+
_background = None
|
|
158
|
+
_creating_background = False
|
|
159
|
+
|
|
160
|
+
def __init__(self, config=SLIDERCONFIG):
|
|
161
|
+
self.CONFIG = config
|
|
162
|
+
super().__init__()
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
def refresh(self):
|
|
166
|
+
self._set_slider_artists()
|
|
167
|
+
super().refresh()
|
|
168
|
+
return
|
|
169
|
+
|
|
@@ -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
|
+
|