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.
Files changed (50) hide show
  1. seolpyo_mplchart/__init__.py +164 -99
  2. seolpyo_mplchart/_base.py +117 -0
  3. seolpyo_mplchart/_chart/__init__.py +137 -0
  4. seolpyo_mplchart/_chart/_base.py +217 -0
  5. seolpyo_mplchart/_chart/_cursor/__init__.py +2 -0
  6. seolpyo_mplchart/_chart/_cursor/_artist.py +217 -0
  7. seolpyo_mplchart/_chart/_cursor/_cursor.py +165 -0
  8. seolpyo_mplchart/_chart/_cursor/_info.py +187 -0
  9. seolpyo_mplchart/_chart/_draw/__init__.py +2 -0
  10. seolpyo_mplchart/_chart/_draw/_artist.py +50 -0
  11. seolpyo_mplchart/_chart/_draw/_data.py +314 -0
  12. seolpyo_mplchart/_chart/_draw/_draw.py +103 -0
  13. seolpyo_mplchart/_chart/_draw/_lim.py +265 -0
  14. seolpyo_mplchart/_chart/_slider/__init__.py +1 -0
  15. seolpyo_mplchart/_chart/_slider/_base.py +268 -0
  16. seolpyo_mplchart/_chart/_slider/_data.py +105 -0
  17. seolpyo_mplchart/_chart/_slider/_mouse.py +176 -0
  18. seolpyo_mplchart/_chart/_slider/_nav.py +204 -0
  19. seolpyo_mplchart/_chart/test.py +121 -0
  20. seolpyo_mplchart/_config/__init__.py +3 -0
  21. seolpyo_mplchart/_config/ax.py +28 -0
  22. seolpyo_mplchart/_config/candle.py +30 -0
  23. seolpyo_mplchart/_config/config.py +21 -0
  24. seolpyo_mplchart/_config/cursor.py +49 -0
  25. seolpyo_mplchart/_config/figure.py +41 -0
  26. seolpyo_mplchart/_config/format.py +51 -0
  27. seolpyo_mplchart/_config/ma.py +15 -0
  28. seolpyo_mplchart/_config/slider/__init__.py +2 -0
  29. seolpyo_mplchart/_config/slider/config.py +24 -0
  30. seolpyo_mplchart/_config/slider/figure.py +20 -0
  31. seolpyo_mplchart/_config/slider/nav.py +9 -0
  32. seolpyo_mplchart/_config/unit.py +19 -0
  33. seolpyo_mplchart/_config/utils.py +67 -0
  34. seolpyo_mplchart/_config/volume.py +26 -0
  35. seolpyo_mplchart/_cursor.py +559 -0
  36. seolpyo_mplchart/_draw.py +634 -0
  37. seolpyo_mplchart/_slider.py +634 -0
  38. seolpyo_mplchart/base.py +70 -67
  39. seolpyo_mplchart/cursor.py +308 -271
  40. seolpyo_mplchart/draw.py +449 -237
  41. seolpyo_mplchart/slider.py +451 -396
  42. seolpyo_mplchart/test.py +173 -24
  43. seolpyo_mplchart/utils.py +15 -4
  44. seolpyo_mplchart/xl_to_dict.py +47 -0
  45. seolpyo_mplchart-2.0.0.3.dist-info/METADATA +710 -0
  46. seolpyo_mplchart-2.0.0.3.dist-info/RECORD +50 -0
  47. {seolpyo_mplchart-0.1.3.1.dist-info → seolpyo_mplchart-2.0.0.3.dist-info}/WHEEL +1 -1
  48. seolpyo_mplchart-0.1.3.1.dist-info/METADATA +0 -49
  49. seolpyo_mplchart-0.1.3.1.dist-info/RECORD +0 -13
  50. {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
+