seolpyo-mplchart 0.0.41__py3-none-any.whl → 0.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.

Potentially problematic release.


This version of seolpyo-mplchart might be problematic. Click here for more details.

@@ -10,10 +10,75 @@ from pathlib import Path
10
10
  from typing import Literal
11
11
 
12
12
  import matplotlib.pyplot as plt
13
+ from matplotlib.figure import Figure
13
14
  import pandas as pd
14
15
 
15
16
 
16
- from .slider import Chart
17
+ from .slider import Chart as CM
18
+
19
+
20
+ __all__ = [
21
+ 'pd',
22
+ 'plt',
23
+
24
+ 'Chart',
25
+
26
+ 'sample',
27
+ 'show',
28
+ 'close',
29
+ ]
30
+
31
+
32
+ class Chart(CM):
33
+ r"""
34
+ You can see the guidance document:
35
+ Korean: https://white.seolpyo.com/entry/147/
36
+ English: https://white.seolpyo.com/entry/148/
37
+
38
+ Variables:
39
+ unit_price, unit_volume: unit for price and volume. default ('원', '주').
40
+
41
+ figsize: figure size if you use plt.show(). default (12, 6).
42
+ ratio_ax_slider, ratio_ax_legend, ratio_ax_price, ratio_ax_volume: Axes ratio. default (3, 2, 18, 5).
43
+ adjust: figure adjust. default dict(top=0.95, bottom=0.05, left=0.01, right=0.93, wspace=0, hspace=0).
44
+ slider_top: ax_slider is located at the top or bottom. default True.
45
+ color_background: color of background. default '#fafafa'.
46
+ color_grid: color of grid. default '#d0d0d0'.
47
+
48
+ df: stock data.
49
+ date: date column key. default 'date'
50
+ Open, high, low, close: price column key. default ('open', 'high', 'low', 'close')
51
+ volume: volume column key. default 'volume'
52
+
53
+ label_ma: moving average legend label format. default '{}일선'
54
+ list_ma: Decide how many days to draw the moving average line. default (5, 20, 60, 120, 240)
55
+ list_macolor: Color the moving average line. If the number of colors is greater than the moving average line, black is applied. default ('darkred', 'fuchsia', 'olive', 'orange', 'navy', 'darkmagenta', 'limegreen', 'darkcyan',)
56
+
57
+ candle_on_ma: Decide whether to draw candles on the moving average line. default True
58
+ color_sliderline: Color of closing price line in ax_slider. default 'k'
59
+ color_navigatorline: Color of left and right dividing lines in selected area. default '#1e78ff'
60
+ color_navigator: Color of unselected area. default 'k'
61
+
62
+ color_up: The color of the candle. When the closing price is greater than the opening price. default '#fe3032'
63
+ color_down: The color of the candle. When the opening price is greater than the opening price. default '#0095ff'
64
+ color_flat: The color of the candle. WWhen the closing price is the same as the opening price. default 'k'
65
+ color_up_down: The color of the candle. If the closing price is greater than the opening price, but is lower than the previous day's closing price. default 'w'
66
+ color_down_up: The color of the candle. If the opening price is greater than the closing price, but is higher than the closing price of the previous day. default 'w'
67
+ colors_volume: The color of the volume bar. default '#1f77b4'
68
+
69
+ lineKwargs: Options applied to horizontal and vertical lines drawn along the mouse position. default dict(edgecolor='k', linewidth=1, linestyle='-')
70
+ textboxKwargs: Options that apply to the information text box. dufault dict(boxstyle='round', facecolor='w')
71
+
72
+ fraction: Decide whether to express information as a fraction. default False
73
+ candleformat: Candle information text format. default '{}\n\n종가:  {}\n등락률: {}\n대비:  {}\n시가:  {}({})\n고가:  {}({})\n저가:  {}({})\n거래량: {}({})'
74
+ volumeformat: Volume information text format. default '{}\n\n거래량   : {}\n거래량증가율: {}'
75
+ digit_price, digit_volume: Number of decimal places expressed in informational text. default (0, 0)
76
+
77
+ min_distance: Minimum number of candles that can be selected with the slider. default 30
78
+ simpler: Decide whether to display candles simply when moving the chart. default False
79
+ limit_volume: Maximum number of volume bars drawn when moving the chart. default 2_000
80
+ """
81
+ pass
17
82
 
18
83
 
19
84
  _name = {'samsung', 'apple'}
@@ -37,7 +102,7 @@ def sample(name: Literal['samsung', 'apple']='samsung'):
37
102
  c.volumeformat = '{}\n\nvolume: {}\nvolume rate: {}'
38
103
  c.set_data(df)
39
104
  show()
40
-
105
+ close()
41
106
  return
42
107
 
43
108
 
@@ -45,5 +110,11 @@ def show():
45
110
  return plt.show()
46
111
 
47
112
 
113
+ def close(fig: int|str|Figure|None='all'):
114
+ return plt.close(fig)
115
+
116
+
48
117
  if __name__ == '__main__':
49
- sample('apple')
118
+ sample('apple')
119
+
120
+
@@ -11,9 +11,6 @@ from .utils import float_to_str
11
11
 
12
12
 
13
13
  class Mixin:
14
- def create_background(self):
15
- "This function works befor canvas.copy_from_bbox()."
16
- return
17
14
  def on_draw(self, e):
18
15
  "This function works if draw event active."
19
16
  return
@@ -64,12 +61,13 @@ class CollectionMixin(DrawMixin):
64
61
  _set_key = {'rate', 'compare', 'rate_open', 'rate_high', 'rate_low', 'rate_volume',}
65
62
 
66
63
  class DataMixin(CollectionMixin):
67
- def _generate_data(self, df: pd.DataFrame):
64
+ def _generate_data(self, df, sort_df=True, calc_ma=True):
68
65
  for i in ['date', 'Open', 'high', 'low', 'close', 'volume']:
69
66
  v = getattr(self, i)
70
67
  if v in _set_key: raise Exception(f'you can not set "self.{i}" value in {_set_key}.\nself.{i}={v!r}')
71
68
 
72
- super()._generate_data(df)
69
+ super()._generate_data(df, sort_df, calc_ma)
70
+ df = self.df
73
71
 
74
72
  df['rate'] = ((df[self.close] - df[self.close].shift(1)) / df[self.close] * 100).__round__(2).fillna(0)
75
73
  df['compare'] = (df[self.close] - df[self.close].shift(1)).fillna(0)
@@ -77,6 +75,36 @@ class DataMixin(CollectionMixin):
77
75
  df['rate_high'] = ((df[self.high] - df[self.close].shift(1)) / df[self.close] * 100).__round__(2).fillna(0)
78
76
  df['rate_low'] = ((df[self.low] - df[self.close].shift(1)) / df[self.close] * 100).__round__(2).fillna(0)
79
77
  df['rate_volume'] = ((df[self.volume] - df[self.volume].shift(1)) / df[self.volume].shift(1) * 100).__round__(2).fillna(0)
78
+
79
+ self.df = df
80
+ return
81
+
82
+ def set_text_coordante(self, vmin, vmax, pmin, pmax, volmax):
83
+ # 주가, 거래량 텍스트 x 위치
84
+ x_distance = (vmax - vmin) / 30
85
+ self.v0, self.v1 = (vmin + x_distance, vmax - x_distance)
86
+ self.text_price.set_x(self.v0)
87
+ self.text_volume.set_x(self.v0)
88
+
89
+ self.vmin, self.vmax = (vmin, vmax)
90
+ self.vmiddle = vmax - int((vmax - vmin) / 2)
91
+
92
+ psub = pmax - pmin
93
+ self.min_psub = psub / 12
94
+
95
+ # 주가 날짜 텍스트 y 위치
96
+ y = (psub) / 20 + pmin
97
+ self.text_date_price.set_y(y)
98
+ # 주가 정보 y 위치
99
+ y = pmax - (psub) / 20
100
+ self.text_price_info.set_y(y)
101
+
102
+ # 거래량 날짜 텍스트 y 위치
103
+ y = volmax * 0.85
104
+ self.text_date_volume.set_y(y)
105
+ # 거래량 정보 y 위치
106
+ self.text_volume_info.set_y(y)
107
+
80
108
  return
81
109
 
82
110
 
@@ -95,8 +123,8 @@ class LineMixin(DataMixin):
95
123
  self.canvas.blit()
96
124
  return
97
125
 
98
- def set_data(self, df):
99
- super().set_data(df)
126
+ def _set_data(self, df: pd.DataFrame, sort_df=True, calc_ma=True, change_lim=True):
127
+ super()._set_data(df, sort_df, calc_ma, change_lim)
100
128
 
101
129
  self.vmin, self.vmax = (self.xmin, self.xmax)
102
130
  return
@@ -172,8 +200,12 @@ class LineMixin(DataMixin):
172
200
  if self.in_index:
173
201
  intx = self.intx
174
202
 
175
- high = self.df[self.high][intx] * 1.02
176
- low = self.df[self.low][intx] * 0.98
203
+ h = self.df[self.high][intx]
204
+ l = self.df[self.low][intx]
205
+ sub = (h - l) / 2
206
+ if sub < self.min_psub: sub = self.min_psub
207
+ high = h + sub
208
+ low = l - sub
177
209
  if high < y or y < low: self._in_candle = False
178
210
  else:
179
211
  self._in_candle = True
@@ -214,8 +246,8 @@ class InfoMixin(LineMixin):
214
246
  volumeformat = '{}\n\n거래량   : {}\n거래량증가율: {}'
215
247
  digit_price, digit_volume = (0, 0)
216
248
 
217
- def set_data(self, df):
218
- super().set_data(df)
249
+ def _set_data(self, df: pd.DataFrame, sort_df=True, calc_ma=True, change_lim=True):
250
+ super()._set_data(df, sort_df, calc_ma, change_lim)
219
251
 
220
252
  # 슬라이더 날짜 텍스트 y 위치
221
253
  y = self._slider_ymax - (self._slider_ymax - self._slider_ymin) / 6
@@ -227,31 +259,6 @@ class InfoMixin(LineMixin):
227
259
 
228
260
  return
229
261
 
230
- def set_text_coordante(self, vmin, vmax, pmin, pmax, volmax):
231
- # 주가, 거래량 텍스트 x 위치
232
- x_distance = (vmax - vmin) / 30
233
- self.v0, self.v1 = (vmin + x_distance, vmax - x_distance)
234
- self.text_price.set_x(self.v0)
235
- self.text_volume.set_x(self.v0)
236
-
237
- self.vmin, self.vmax = (vmin, vmax)
238
- self.vmiddle = vmax - int((vmax - vmin) / 2)
239
-
240
- # 주가 날짜 텍스트 y 위치
241
- y = (pmax - pmin) / 20 + pmin
242
- self.text_date_price.set_y(y)
243
- # 주가 정보 y 위치
244
- y = pmax - (pmax - pmin) / 20
245
- self.text_price_info.set_y(y)
246
-
247
- # 거래량 날짜 텍스트 y 위치
248
- y = volmax * 0.85
249
- self.text_date_volume.set_y(y)
250
- # 거래량 정보 y 위치
251
- self.text_volume_info.set_y(y)
252
-
253
- return
254
-
255
262
  def _slider_move_action(self, e):
256
263
  super()._slider_move_action(e)
257
264
 
@@ -388,10 +395,6 @@ class CursorMixin(InfoMixin):
388
395
 
389
396
 
390
397
  class Chart(CursorMixin, CM, Mixin):
391
- def _generate_data(self, df):
392
- super()._generate_data(df)
393
- return self.generate_data(df)
394
-
395
398
  def _on_draw(self, e):
396
399
  super()._on_draw(e)
397
400
  return self.on_draw(e)
@@ -400,10 +403,6 @@ class Chart(CursorMixin, CM, Mixin):
400
403
  self.on_pick(e)
401
404
  return super()._on_pick(e)
402
405
 
403
- def _draw_artist(self):
404
- super()._draw_artist()
405
- return self.create_background()
406
-
407
406
  def _blit(self):
408
407
  super()._blit()
409
408
  return self.on_blit()
@@ -424,7 +423,8 @@ if __name__ == '__main__':
424
423
  file = Path(__file__).parent / 'data/apple.txt'
425
424
  with open(file, 'r', encoding='utf-8') as txt:
426
425
  data = json.load(txt)
427
- data = data[:100]
426
+ n = 2600
427
+ data = data[n:n+100]
428
428
  df = pd.DataFrame(data)
429
429
 
430
430
  t = time()
seolpyo_mplchart/draw.py CHANGED
@@ -9,10 +9,6 @@ from .base import Base
9
9
 
10
10
 
11
11
  class Mixin:
12
- def generate_data(self, df):
13
- "This function works after data generate process is done."
14
- return
15
-
16
12
  def on_blit(self):
17
13
  "This function works after cavas.blit()."
18
14
  return
@@ -27,7 +23,7 @@ class Mixin:
27
23
  return
28
24
 
29
25
 
30
- _set_key = {'x', 'left', 'right', 'top', 'bottom',}
26
+ _set_key = {'zero', 'x', 'left', 'right', 'top', 'bottom',}
31
27
 
32
28
  class DataMixin(Base):
33
29
  df: pd.DataFrame
@@ -48,7 +44,7 @@ class DataMixin(Base):
48
44
  color_down_up = 'w'
49
45
  colors_volume = '#1f77b4'
50
46
 
51
- def _generate_data(self, df: pd.DataFrame):
47
+ def _generate_data(self, df: pd.DataFrame, sort_df=True, calc_ma=True):
52
48
  for i in ['date', 'Open', 'high', 'low', 'close', 'volume']:
53
49
  k: str = getattr(self, i)
54
50
  if k in _set_key: raise Exception(f'you can not set "self.{i}" value in {_set_key}.\nself.{i}={k!r}')
@@ -56,7 +52,13 @@ class DataMixin(Base):
56
52
  dtype = df[k].dtype
57
53
  if not isinstance(dtype, (np.dtypes.Float64DType, np.dtypes.Int64DType, np.dtypes.Float32DType, np.dtypes.Int32DType)): raise TypeError(f'Data column type must be "float64" or "int64" or "float32" or "int32".(excluding "date" column)\ndf[{k!r}].dtype={dtype!r}')
58
54
 
59
- for i in self.list_ma: df[f'ma{i}'] = df[self.close].rolling(i).mean()
55
+ # DataFrame 정렬
56
+ if sort_df:
57
+ df = df.sort_values([self.date]).reset_index()
58
+
59
+ if not self.list_ma: self.list_ma = tuple()
60
+ if calc_ma:
61
+ for i in self.list_ma: df[f'ma{i}'] = df[self.close].rolling(i).mean()
60
62
 
61
63
  candlewidth_half = 0.3
62
64
  volumewidth_half = 0.36
@@ -72,7 +74,7 @@ class DataMixin(Base):
72
74
  df['bottom'] = np.where(df[self.close] < df[self.Open], df[self.close], df[self.Open])
73
75
 
74
76
  # 양봉
75
- df.loc[:, ['facecolor', 'edgecolor']] = (self.color_up, self.color_up)
77
+ df.loc[:, ['zero', 'facecolor', 'edgecolor']] = (0, self.color_up, self.color_up)
76
78
  if self.color_up != self.color_down:
77
79
  # 음봉
78
80
  df.loc[df[self.close] < df[self.Open], ['facecolor', 'edgecolor']] = (self.color_down, self.color_down)
@@ -89,7 +91,6 @@ class DataMixin(Base):
89
91
  self.df = df
90
92
  return
91
93
 
92
-
93
94
  class CollectionMixin(DataMixin):
94
95
  color_sliderline = 'k'
95
96
 
@@ -123,57 +124,55 @@ class CollectionMixin(DataMixin):
123
124
 
124
125
  return
125
126
 
126
- def _get_candlesegment(self, s: pd.Series):
127
- v = s.values
128
- segment = (
129
- (v[0], v[3]), # 심지 상단
130
- (v[0], v[5]), # 몸통 상단
131
- (v[1], v[5]), # 몸통 상단 좌측
132
- (v[1], v[6]), # 몸통 하단 좌측
133
- (v[0], v[6]), # 몸통 하단
134
- (v[0], v[4]), # 심지 하단
135
- (v[0], v[6]), # 몸통 하단
136
- (v[2], v[6]), # 몸통 하단 우측
137
- (v[2], v[5]), # 몸통 상단 우측
138
- (v[0], v[5]), # 몸통 상단
139
- )
140
- return segment
141
-
142
- def _get_volumesegment(self, s: pd.Series):
143
- v = s.values
144
- segment = (
145
- (v[0], 0), # 몸통 하단 좌측
146
- (v[0], v[2]), # 몸통 상단 좌측
147
- (v[1], v[2]), # 몸통 상단 우측
148
- (v[1], 0), # 몸통 하단 우측
149
- )
150
- return segment
151
-
152
127
  def _set_collection(self):
153
- self.df.loc[:, ['candlesegment']] = self.df[['x', 'left', 'right', self.high, self.low, 'top', 'bottom']].agg(self._get_candlesegment, axis=1)
154
- self.df.loc[:, ['volumesegment']] = self.df[['vleft', 'vright', self.volume]].agg(self._get_volumesegment, axis=1)
128
+ candleseg = self.df[[
129
+ 'x', self.high,
130
+ 'x', 'top',
131
+ 'left', 'top',
132
+ 'left', 'bottom',
133
+ 'x', 'bottom',
134
+ 'x', self.low,
135
+ 'x', 'bottom',
136
+ 'right', 'bottom',
137
+ 'right', 'top',
138
+ 'x', 'top',
139
+ ]].values
140
+ candleseg = candleseg.reshape(candleseg.shape[0], 10, 2)
141
+
142
+ self.candlecollection.set_segments(candleseg)
143
+ self.candlecollection.set_facecolor(self.df['facecolor'].values)
144
+ self.candlecollection.set_edgecolor(self.df['edgecolor'].values)
145
+
146
+ volseg = self.df[[
147
+ 'left', 'zero',
148
+ 'left', self.volume,
149
+ 'right', self.volume,
150
+ 'right', 'zero',
151
+ ]].values
152
+ volseg = volseg.reshape(volseg.shape[0], 4, 2)
153
+
154
+ self.volumecollection.set_segments(volseg)
155
155
 
156
156
  self._set_macollection()
157
157
 
158
158
  # 가격이동평균선
159
- segments = list(reversed(self._masegment.values()))
159
+ maseg = reversed(self._masegment.values())
160
160
  colors, widths = ([], [])
161
161
  for i in reversed(self._macolors.values()): (colors.append(i), widths.append(1))
162
- self.macollection.set_segments(segments)
162
+ self.macollection.set_segments(maseg)
163
163
  self.macollection.set_edgecolor(colors)
164
164
 
165
165
  # 슬라이더 선형차트
166
- segments.append(self.df[['x', self.close]].apply(tuple, axis=1).tolist())
166
+ keys = []
167
+ for i in reversed(self.list_ma):
168
+ keys.append('x')
169
+ keys.append(f'ma{i}')
170
+ sliderseg = self.df[keys + ['x', self.close]].values
171
+ sliderseg = sliderseg.reshape(sliderseg.shape[0], self.list_ma.__len__()+1, 2).swapaxes(0, 1)
167
172
  (colors.append(self.color_sliderline), widths.append(1.8))
168
- self.slidercollection.set_segments(segments)
173
+ self.slidercollection.set_segments(sliderseg)
169
174
  self.slidercollection.set_edgecolor(colors)
170
175
  self.slidercollection.set_linewidth(widths)
171
-
172
- self.candlecollection.set_segments(self.df['candlesegment'])
173
- self.candlecollection.set_facecolor(self.df['facecolor'].values)
174
- self.candlecollection.set_edgecolor(self.df['edgecolor'].values)
175
-
176
- self.volumecollection.set_segments(self.df['volumesegment'])
177
176
  return
178
177
 
179
178
  def _set_macollection(self):
@@ -188,7 +187,10 @@ class CollectionMixin(DataMixin):
188
187
  try: c = self.list_macolor[n]
189
188
  except: c = self.color_sliderline
190
189
  self._macolors[i] = c
191
- self._masegment[i] = self.df[['x', f'ma{i}']].apply(tuple, axis=1).tolist()
190
+ # seg = self.df['x', f'ma{i}'].values
191
+ seg = self.df.loc[self.df[f'ma{i}'] != np.nan, ['x', f'ma{i}']].values
192
+ # print(f'{seg[:5]=}')
193
+ self._masegment[i] = seg
192
194
 
193
195
  handles.append(Line2D([0, 1], [0, 1], color=c, linewidth=5, label=i))
194
196
  labels.append(self.label_ma.format(i))
@@ -300,25 +302,29 @@ class BackgroundMixin(CollectionMixin):
300
302
  self.canvas.renderer.restore_region(self.background)
301
303
  return
302
304
 
303
-
304
305
  class DrawMixin(BackgroundMixin):
305
- def set_data(self, df: pd.DataFrame):
306
- self._generate_data(df)
306
+ def set_data(self, df: pd.DataFrame, sort_df=True, calc_ma=True, change_lim=True):
307
+ self._set_data(df, sort_df, calc_ma, change_lim)
308
+ return self.df
309
+
310
+ def _set_data(self, df: pd.DataFrame, sort_df=True, calc_ma=True, change_lim=True):
311
+ self._generate_data(df, sort_df, calc_ma)
307
312
  self._set_collection()
308
- self._draw_collection()
313
+ self._draw_collection(change_lim)
309
314
  return
310
315
 
311
- def _draw_collection(self):
316
+ def _draw_collection(self, change_lim=True):
312
317
  xmax = self.df['x'].values[-1] + 1
313
318
 
314
319
  xspace = xmax / 40
315
320
  self.xmin, self.xmax = (-xspace, xmax+xspace)
316
321
  # 슬라이더 xlim
317
322
  self.ax_slider.set_xlim(self.xmin, self.xmax)
318
- # 주가 xlim
319
- self.ax_price.set_xlim(self.xmin, self.xmax)
320
- # 거래량 xlim
321
- self.ax_volume.set_xlim(self.xmin, self.xmax)
323
+ if change_lim:
324
+ # 주가 xlim
325
+ self.ax_price.set_xlim(self.xmin, self.xmax)
326
+ # 거래량 xlim
327
+ self.ax_volume.set_xlim(self.xmin, self.xmax)
322
328
 
323
329
  ymin, ymax = (self.df[self.low].min(), self.df[self.high].max())
324
330
  ysub = (ymax - ymin) / 15
@@ -329,19 +335,15 @@ class DrawMixin(BackgroundMixin):
329
335
 
330
336
  # 주가 ylim
331
337
  self._price_ymin, self._price_ymax = (ymin-ysub, ymax+ysub)
332
- self.ax_price.set_ylim(self._price_ymin, self._price_ymax)
338
+ if change_lim: self.ax_price.set_ylim(self._price_ymin, self._price_ymax)
333
339
 
334
340
  # 거래량 ylim
335
341
  self._vol_ymax = self.df[self.volume].max() * 1.2
336
- self.ax_volume.set_ylim(0, self._vol_ymax)
342
+ if change_lim: self.ax_volume.set_ylim(0, self._vol_ymax)
337
343
  return
338
344
 
339
345
 
340
346
  class Chart(DrawMixin, Mixin):
341
- def _generate_data(self, df):
342
- super()._generate_data(df)
343
- return self.generate_data(self.df)
344
-
345
347
  def _on_draw(self, e):
346
348
  super()._on_draw(e)
347
349
  return self.on_draw(e)
@@ -21,6 +21,10 @@ class Mixin(CursorMixin):
21
21
  def on_release(self, e):
22
22
  "This function works if mouse button release event active."
23
23
  return
24
+ def draw_artist(self):
25
+ "This function works before canvas.blit()."
26
+ return
27
+
24
28
 
25
29
  class NavgatorMixin(Mixin):
26
30
  min_distance = 30
@@ -39,8 +43,8 @@ class NavgatorMixin(Mixin):
39
43
  self.ax_slider.add_artist(self.navigator)
40
44
  return
41
45
 
42
- def set_data(self, df):
43
- super().set_data(df)
46
+ def _set_data(self, df: pd.DataFrame, sort_df=True, calc_ma=True, change_lim=True):
47
+ super()._set_data(df, sort_df, calc_ma, False)
44
48
 
45
49
  # 네비게이터 라인 선택 영역
46
50
  xsub = self.xmax - self.xmin
@@ -96,9 +100,6 @@ class NavgatorMixin(Mixin):
96
100
 
97
101
  if self._navcoordinate[0] == self._navcoordinate[1]:
98
102
  self._navcoordinate = (self._navcoordinate[0], self._navcoordinate[1]+self.min_distance)
99
-
100
- self.background = None
101
- self._draw()
102
103
  return
103
104
 
104
105
 
@@ -108,19 +109,27 @@ class BackgroundMixin(NavgatorMixin):
108
109
  self._restore_region()
109
110
  return
110
111
 
111
- def _restore_region(self, with_nav=True):
112
+ def _restore_region(self, with_nav=True, empty=False, empty_with_nav=False):
112
113
  if not self.background: self._create_background()
113
114
 
114
- if with_nav: self.canvas.restore_region(self.background_with_nav)
115
+ if empty: self.canvas.restore_region(self.background_empty)
116
+ elif empty_with_nav: self.canvas.restore_region(self.background_empty_with_nav)
117
+ elif with_nav: self.canvas.restore_region(self.background_with_nav)
115
118
  else: self.canvas.renderer.restore_region(self.background)
116
119
  return
117
120
 
118
121
  def _copy_bbox(self):
119
- self.ax_slider.xaxis.draw(self.canvas.renderer)
120
- self.ax_slider.yaxis.draw(self.canvas.renderer)
121
- self.slidercollection.draw(self.canvas.renderer)
122
+ renderer = self.canvas.renderer
122
123
 
123
- super()._copy_bbox()
124
+ self.background_empty = renderer.copy_from_bbox(self.fig.bbox)
125
+
126
+ self.ax_slider.xaxis.draw(renderer)
127
+ self.ax_slider.yaxis.draw(renderer)
128
+ self.slidercollection.draw(renderer)
129
+ self.background_empty_with_nav = self.canvas.renderer.copy_from_bbox(self.fig.bbox)
130
+
131
+ self._draw_artist()
132
+ self.background = self.canvas.renderer.copy_from_bbox(self.fig.bbox)
124
133
 
125
134
  self.navigator.draw(self.canvas.renderer)
126
135
  self.background_with_nav = self.canvas.renderer.copy_from_bbox(self.fig.bbox)
@@ -147,26 +156,24 @@ class BackgroundMixin(NavgatorMixin):
147
156
 
148
157
 
149
158
  class DrawMixin(BackgroundMixin):
150
- def set_data(self, df):
151
- super().set_data(df)
159
+ def _set_data(self, df: pd.DataFrame, sort_df=True, calc_ma=True, change_lim=True):
160
+ super()._set_data(df, sort_df, calc_ma, change_lim)
152
161
 
153
162
  # 네비게이터 높이 설정
154
- if 0 < self._slider_ymin: ysub = self._slider_ymax
155
- else: ysub = self._slider_ymax - self._slider_ymin
156
- self._ymiddle = ysub / 2
163
+ ysub = self._slider_ymax - self._slider_ymin
164
+ self._ymiddle = self._slider_ymax - ysub / 2
157
165
  self.navigator.set_linewidth((ysub, 5))
158
166
  return
159
167
 
160
168
  def _on_release(self, e: MouseEvent):
161
- if e.inaxes is not self.ax_slider: return
162
- self.is_click, self.is_move = (False, False)
163
-
164
- if self._navcoordinate[0] == self._navcoordinate[1]:
165
- self._navcoordinate = (self._navcoordinate[0], self._navcoordinate[1]+self.min_distance)
169
+ super()._on_release(e)
166
170
  self._set_navigator(*self._navcoordinate)
167
171
 
168
- self.background = None
169
- self._draw()
172
+ self._restore_region(empty=True)
173
+ self._creating_background = False
174
+ self._create_background()
175
+ self._restore_region()
176
+ self._blit()
170
177
  return
171
178
 
172
179
  def _on_move(self, e: MouseEvent):
@@ -238,27 +245,6 @@ class DrawMixin(BackgroundMixin):
238
245
 
239
246
 
240
247
  class LimMixin(DrawMixin):
241
- def _restore_region(self, with_nav=True, empty=False):
242
- if not self.background: self._create_background()
243
-
244
- if empty: self.canvas.restore_region(self.background_empty)
245
- elif with_nav: self.canvas.restore_region(self.background_with_nav)
246
- else: self.canvas.renderer.restore_region(self.background)
247
- return
248
-
249
- def _copy_bbox(self):
250
- self.ax_slider.xaxis.draw(self.canvas.renderer)
251
- self.ax_slider.yaxis.draw(self.canvas.renderer)
252
- self.slidercollection.draw(self.canvas.renderer)
253
- self.background_empty = self.canvas.renderer.copy_from_bbox(self.fig.bbox)
254
-
255
- self._draw_artist()
256
- self.background = self.canvas.renderer.copy_from_bbox(self.fig.bbox)
257
-
258
- self.navigator.draw(self.canvas.renderer)
259
- self.background_with_nav = self.canvas.renderer.copy_from_bbox(self.fig.bbox)
260
- return
261
-
262
248
  def _on_release(self, e: MouseEvent):
263
249
  if e.inaxes is not self.ax_slider: return
264
250
  self.is_click, self.is_move = (False, False)
@@ -268,12 +254,14 @@ class LimMixin(DrawMixin):
268
254
  self._set_navigator(*self._navcoordinate)
269
255
  self._lim()
270
256
 
271
- self.background = None
272
- self._draw()
257
+ self._restore_region(empty=True)
258
+ self._copy_bbox()
259
+ self._restore_region()
260
+ self._blit()
273
261
  return
274
262
 
275
263
  def _on_move(self, e):
276
- self._restore_region(with_nav=(not self.is_click), empty=self.is_click)
264
+ self._restore_region(with_nav=(not self.is_click), empty_with_nav=self.is_click)
277
265
 
278
266
  self._on_move_action(e)
279
267
 
@@ -329,7 +317,8 @@ class LimMixin(DrawMixin):
329
317
 
330
318
  class SimpleMixin(LimMixin):
331
319
  simpler = False
332
- limit_volume = 2_000
320
+ limit_volume = 1_500
321
+ default_left, default_right = (180, 10)
333
322
 
334
323
  def __init__(self, *args, **kwargs):
335
324
  super().__init__(*args, **kwargs)
@@ -345,35 +334,34 @@ class SimpleMixin(LimMixin):
345
334
  self.ax_volume.add_collection(self.blitvolume)
346
335
  return
347
336
 
348
- def set_data(self, df):
349
- super().set_data(df)
337
+ def _set_data(self, df: pd.DataFrame, sort_df=True, calc_ma=True, change_lim=True):
338
+ super()._set_data(df, sort_df, calc_ma, False)
350
339
 
351
- seg = self.df[['x', self.high, self.low]].agg(get_wickline, axis=1)
340
+ seg = self.df[['x', self.high, 'x', self.low]].values
341
+ seg = seg.reshape(seg.shape[0], 2, 2)
352
342
  self.blitcandle.set_segments(seg)
353
343
  self.blitcandle.set_edgecolor(self.df['edgecolor'])
354
- self.priceline.set_verts([self.df[['x', self.close]].apply(tuple, axis=1).tolist()])
355
344
 
356
- volmax = self.df[self.volume].max()
345
+ pseg = self.df[['x', self.close]].values
346
+ self.priceline.set_verts(pseg.reshape(1, *pseg.shape))
347
+
357
348
  l = self.df.__len__()
358
349
  if l < self.limit_volume:
359
- volseg = self.df.loc[:, ['x', self.volume]].agg(get_volumeline, axis=1)
350
+ volseg = self.df.loc[:, ['x', 'zero', 'x', self.volume]].values
360
351
  else:
361
- n, step = (1, 1 / self.limit_volume)
362
- for _ in range(self.limit_volume):
363
- n -= step
364
- volmin = volmax * n
365
- length = self.df.loc[volmin < self.df[self.volume]].__len__()
366
- if self.limit_volume < length: break
367
-
368
- volseg = self.df.loc[volmin < self.df[self.volume], ['x', self.volume]].agg(get_volumeline, axis=1)
369
- self.blitvolume.set_segments(volseg)
370
-
371
- index = self.df.index[-1]
372
- if index < 120: self._navcoordinate = (int(self.xmin)-1, int(self.xmax)+1)
373
- else: self._navcoordinate = (index-80, index+10)
352
+ v = self.df[['x', 'zero', 'x', self.volume]].sort_values([self.volume], axis=0, ascending=False)
353
+ volseg = v[:self.limit_volume].values
354
+
355
+ self.blitvolume.set_segments(volseg.reshape(volseg.shape[0], 2, 2))
356
+
357
+ if change_lim:
358
+ index = self.df.index[-1]
359
+ if index < self.default_left + self.default_right: self._navcoordinate = (int(self.xmin)-1, int(self.xmax)+1)
360
+ else: self._navcoordinate = (index-self.default_left, index+self.default_right)
361
+
374
362
  self._set_navigator(*self._navcoordinate)
375
363
  self._lim()
376
- return self._draw()
364
+ return
377
365
 
378
366
  def _draw_blit_artist(self):
379
367
  renderer = self.canvas.renderer
@@ -381,12 +369,20 @@ class SimpleMixin(LimMixin):
381
369
  self.ax_price.xaxis.draw(renderer)
382
370
  self.ax_price.yaxis.draw(renderer)
383
371
 
384
- if self.simpler: self.blitcandle.draw(renderer)
372
+ left, right = self._navcoordinate
373
+ Range = right - left
374
+ if self.simpler:
375
+ if Range < 1_000: self.blitcandle.draw(renderer)
376
+ else: self.priceline.draw(renderer)
385
377
  elif self.candle_on_ma:
386
378
  self.macollection.draw(renderer)
387
- self.candlecollection.draw(renderer)
379
+ if 2_500 < Range: self.priceline.draw(renderer)
380
+ elif 800 < Range or 9_999 < self.xmax: self.blitcandle.draw(renderer)
381
+ else: self.candlecollection.draw(renderer)
388
382
  else:
389
- self.candlecollection.draw(renderer)
383
+ if 2_500 < Range: self.priceline.draw(renderer)
384
+ elif 800 < Range or 9_999 < self.xmax: self.blitcandle.draw(renderer)
385
+ else: self.candlecollection.draw(renderer)
390
386
  self.macollection.draw(renderer)
391
387
 
392
388
  self.ax_volume.xaxis.draw(renderer)
@@ -436,12 +432,16 @@ class ClickMixin(SimpleMixin):
436
432
  if not self.is_click: return
437
433
  elif e.inaxes is self.ax_slider: return super()._on_release(e)
438
434
  elif not self.in_price and not self.in_volume and not self.is_click_chart: return
435
+ # 차트 click release action
439
436
  self.canvas.set_cursor(cursors.POINTER)
440
437
  self.is_click, self.is_move = (False, False)
441
438
  self.is_click_chart = False
442
439
 
443
- self._draw()
444
- return self._restore_region()
440
+ self._restore_region(empty=True)
441
+ self._copy_bbox()
442
+ self._restore_region()
443
+ self._blit()
444
+ return
445
445
 
446
446
  def _on_chart_click(self, e: MouseEvent):
447
447
  self.is_click = True
@@ -469,7 +469,7 @@ class ClickMixin(SimpleMixin):
469
469
  return
470
470
 
471
471
  def _on_move(self, e):
472
- self._restore_region(with_nav=(not self.is_click), empty=self.is_click)
472
+ self._restore_region(with_nav=(not self.is_click), empty_with_nav=self.is_click)
473
473
 
474
474
  self._on_move_action(e)
475
475
 
@@ -506,58 +506,6 @@ class SliderMixin(ClickMixin):
506
506
 
507
507
 
508
508
  class Chart(SliderMixin, CM, Mixin):
509
- r"""
510
- You can see the guidance document:
511
- Korean: https://white.seolpyo.com/entry/147/
512
- English: https://white.seolpyo.com/entry/148/
513
-
514
- Variables:
515
- unit_price, unit_volume: unit for price and volume. default ('원', '주').
516
-
517
- figsize: figure size if you use plt.show(). default (12, 6).
518
- ratio_ax_slider, ratio_ax_legend, ratio_ax_price, ratio_ax_volume: Axes ratio. default (3, 2, 18, 5).
519
- adjust: figure adjust. default dict(top=0.95, bottom=0.05, left=0.01, right=0.93, wspace=0, hspace=0).
520
- slider_top: ax_slider is located at the top or bottom. default True.
521
- color_background: color of background. default '#fafafa'.
522
- color_grid: color of grid. default '#d0d0d0'.
523
-
524
- df: stock data.
525
- date: date column key. default 'date'
526
- Open, high, low, close: price column key. default ('open', 'high', 'low', 'close')
527
- volume: volume column key. default 'volume'
528
-
529
- label_ma: moving average legend label format. default '{}일선'
530
- list_ma: Decide how many days to draw the moving average line. default (5, 20, 60, 120, 240)
531
- list_macolor: Color the moving average line. If the number of colors is greater than the moving average line, black is applied. default ('darkred', 'fuchsia', 'olive', 'orange', 'navy', 'darkmagenta', 'limegreen', 'darkcyan',)
532
-
533
- candle_on_ma: Decide whether to draw candles on the moving average line. default True
534
- color_sliderline: Color of closing price line in ax_slider. default 'k'
535
- color_navigatorline: Color of left and right dividing lines in selected area. default '#1e78ff'
536
- color_navigator: Color of unselected area. default 'k'
537
-
538
- color_up: The color of the candle. When the closing price is greater than the opening price. default '#fe3032'
539
- color_down: The color of the candle. When the opening price is greater than the opening price. default '#0095ff'
540
- color_flat: The color of the candle. WWhen the closing price is the same as the opening price. default 'k'
541
- color_up_down: The color of the candle. If the closing price is greater than the opening price, but is lower than the previous day's closing price. default 'w'
542
- color_down_up: The color of the candle. If the opening price is greater than the closing price, but is higher than the closing price of the previous day. default 'w'
543
- colors_volume: The color of the volume bar. default '#1f77b4'
544
-
545
- lineKwargs: Options applied to horizontal and vertical lines drawn along the mouse position. default dict(edgecolor='k', linewidth=1, linestyle='-')
546
- textboxKwargs: Options that apply to the information text box. dufault dict(boxstyle='round', facecolor='w')
547
-
548
- fraction: Decide whether to express information as a fraction. default False
549
- candleformat: Candle information text format. default '{}\n\n종가:  {}\n등락률: {}\n대비:  {}\n시가:  {}({})\n고가:  {}({})\n저가:  {}({})\n거래량: {}({})'
550
- volumeformat: Volume information text format. default '{}\n\n거래량   : {}\n거래량증가율: {}'
551
- digit_price, digit_volume: Number of decimal places expressed in informational text. default (0, 0)
552
-
553
- min_distance: Minimum number of candles that can be selected with the slider. default 30
554
- simpler: Decide whether to display candles simply when moving the chart. default False
555
- limit_volume: Maximum number of volume bars drawn when moving the chart. default 2_000
556
- """
557
- def _generate_data(self, df):
558
- super()._generate_data(df)
559
- return self.generate_data(self.df)
560
-
561
509
  def _on_draw(self, e):
562
510
  super()._on_draw(e)
563
511
  return self.on_draw(e)
@@ -568,7 +516,10 @@ class Chart(SliderMixin, CM, Mixin):
568
516
 
569
517
  def _draw_artist(self):
570
518
  super()._draw_artist()
571
- return self.create_background()
519
+ return self.draw_artist()
520
+ def _draw_blit_artist(self):
521
+ super()._draw_blit_artist()
522
+ return self.draw_artist()
572
523
 
573
524
  def _blit(self):
574
525
  super()._blit()
@@ -594,8 +545,10 @@ if __name__ == '__main__':
594
545
  df = pd.DataFrame(data)
595
546
 
596
547
  t = time()
548
+ # c = SimpleMixin()
597
549
  c = SliderMixin()
598
550
  c.set_data(df)
599
551
  t2 = time() - t
600
552
  print(f'{t2=}')
601
553
  plt.show()
554
+
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: seolpyo-mplchart
3
- Version: 0.0.41
4
- Summary: Fast candlestick chart using Python.
3
+ Version: 0.1.0
4
+ Summary: Fast candlestick chart using Python. Includes navigator, slider, navigation, and text information display functions
5
5
  Author-email: white-seolpyo <white-seolpyo@naver.com>
6
6
  License: MIT License
7
7
  Project-URL: Homepage, https://white.seolpyo.com/
@@ -22,7 +22,7 @@ Classifier: Programming Language :: Python :: 3.11
22
22
  Classifier: Programming Language :: Python :: 3.12
23
23
  Requires-Python: >=3.11
24
24
  Description-Content-Type: text/markdown
25
- Requires-Dist: matplotlib>=3.7.0
25
+ Requires-Dist: matplotlib>=3.6.0
26
26
  Requires-Dist: pandas>=2.0.0
27
27
 
28
28
  # Donation
@@ -0,0 +1,13 @@
1
+ seolpyo_mplchart/__init__.py,sha256=HITw-PVvJA4AHOO_gOAsbBxNvEbou-JLtivnKA_dn0A,5157
2
+ seolpyo_mplchart/base.py,sha256=vQ4OOBm3nGwjJ4wjDLaD_3LGxYzlP6AWpI6SZrZiwnQ,3600
3
+ seolpyo_mplchart/cursor.py,sha256=_Pzg8WvfOpZYzN5uNLO9LH2wrRB_gZTNGkqmzsjaaPc,17309
4
+ seolpyo_mplchart/draw.py,sha256=yl813StbNuWC0_3QfS9ECvc0Z5buaIudsqR_qW8d3f4,13021
5
+ seolpyo_mplchart/slider.py,sha256=K-vPj2dsSyXZDELrgn6Ry5VqORgUc65gS2SsTFm5TmE,19725
6
+ seolpyo_mplchart/test.py,sha256=cW2hoaVbRtoSXlpmA4i1BKHBjI3-FAqYq__kryxkrC8,1007
7
+ seolpyo_mplchart/utils.py,sha256=-8cq4-WwiqKQxtwu3NPxOVTDDvoWH28tu4OTWr4hPTg,1208
8
+ seolpyo_mplchart/data/apple.txt,sha256=0izAfweu1lLsC0IwVthdVlo9reG8KGbKGTSX5knI5Zc,1380864
9
+ seolpyo_mplchart/data/samsung.txt,sha256=UejaSkbzr4E5K3lkelCT0yJiWUPfmViBEaTyoXyphIs,2476424
10
+ seolpyo_mplchart-0.1.0.dist-info/METADATA,sha256=W3N4J-vLlzw7Cx_t1WiGBDx2g0eiXdf5VlPoe0RVLDw,2176
11
+ seolpyo_mplchart-0.1.0.dist-info/WHEEL,sha256=A3WOREP4zgxI0fKrHUG8DC8013e3dK3n7a6HDbcEIwE,91
12
+ seolpyo_mplchart-0.1.0.dist-info/top_level.txt,sha256=KgqFn7rKWize7OjMaTCHxKm9ie6vqnyb5c8fN7y_tSo,17
13
+ seolpyo_mplchart-0.1.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.6.0)
2
+ Generator: setuptools (75.7.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,13 +0,0 @@
1
- seolpyo_mplchart/__init__.py,sha256=bsfjo70OG4go5ZNaFziLpXPMZ9wWSGQdx1ujuaGBzCQ,1289
2
- seolpyo_mplchart/base.py,sha256=vQ4OOBm3nGwjJ4wjDLaD_3LGxYzlP6AWpI6SZrZiwnQ,3600
3
- seolpyo_mplchart/cursor.py,sha256=-IUk2JcjvIM-TjHyeJx3lJFVI_Sx0xccDcO_RL-Co4c,17185
4
- seolpyo_mplchart/draw.py,sha256=ai8dpiz1ot4L9fphA3SRKhWMhNcWzvHUvQnZYo5p94I,12963
5
- seolpyo_mplchart/slider.py,sha256=0ZDJIs5rR1spoc3my5chuKXkKOJsXUjbDk6utDBABcU,23060
6
- seolpyo_mplchart/test.py,sha256=cW2hoaVbRtoSXlpmA4i1BKHBjI3-FAqYq__kryxkrC8,1007
7
- seolpyo_mplchart/utils.py,sha256=-8cq4-WwiqKQxtwu3NPxOVTDDvoWH28tu4OTWr4hPTg,1208
8
- seolpyo_mplchart/data/apple.txt,sha256=0izAfweu1lLsC0IwVthdVlo9reG8KGbKGTSX5knI5Zc,1380864
9
- seolpyo_mplchart/data/samsung.txt,sha256=UejaSkbzr4E5K3lkelCT0yJiWUPfmViBEaTyoXyphIs,2476424
10
- seolpyo_mplchart-0.0.41.dist-info/METADATA,sha256=ac59OLKyH8ZtjDSuoXGGElI59kmP-k-xDiE36PF_v74,2098
11
- seolpyo_mplchart-0.0.41.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
12
- seolpyo_mplchart-0.0.41.dist-info/top_level.txt,sha256=KgqFn7rKWize7OjMaTCHxKm9ie6vqnyb5c8fN7y_tSo,17
13
- seolpyo_mplchart-0.0.41.dist-info/RECORD,,