seolpyo-mplchart 2.0.0.3__py3-none-any.whl → 2.1.0.4__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 (58) hide show
  1. seolpyo_mplchart/__init__.py +17 -133
  2. seolpyo_mplchart/_chart/__init__.py +39 -31
  3. seolpyo_mplchart/_chart/base/__init__.py +111 -0
  4. seolpyo_mplchart/_chart/base/a_canvas.py +250 -0
  5. seolpyo_mplchart/_chart/base/b_artist.py +143 -0
  6. seolpyo_mplchart/_chart/base/c_draw.py +100 -0
  7. seolpyo_mplchart/_chart/base/d_segment.py +262 -0
  8. seolpyo_mplchart/_chart/base/e_axis.py +268 -0
  9. seolpyo_mplchart/_chart/base/f_background.py +62 -0
  10. seolpyo_mplchart/_chart/base/g_event.py +66 -0
  11. seolpyo_mplchart/_chart/base/h_data.py +138 -0
  12. seolpyo_mplchart/_chart/base/test.py +58 -0
  13. seolpyo_mplchart/_chart/cursor/__init__.py +125 -0
  14. seolpyo_mplchart/_chart/cursor/b_artist.py +130 -0
  15. seolpyo_mplchart/_chart/cursor/c_draw.py +96 -0
  16. seolpyo_mplchart/_chart/cursor/d_segment.py +359 -0
  17. seolpyo_mplchart/_chart/cursor/e_axis.py +64 -0
  18. seolpyo_mplchart/_chart/cursor/g_event.py +233 -0
  19. seolpyo_mplchart/_chart/cursor/h_data.py +62 -0
  20. seolpyo_mplchart/_chart/cursor/test.py +69 -0
  21. seolpyo_mplchart/_chart/slider/__init__.py +169 -0
  22. seolpyo_mplchart/_chart/slider/a_canvas.py +260 -0
  23. seolpyo_mplchart/_chart/slider/b_artist.py +91 -0
  24. seolpyo_mplchart/_chart/slider/c_draw.py +54 -0
  25. seolpyo_mplchart/_chart/slider/d_segment.py +166 -0
  26. seolpyo_mplchart/_chart/slider/e_axis.py +70 -0
  27. seolpyo_mplchart/_chart/slider/f_background.py +37 -0
  28. seolpyo_mplchart/_chart/slider/g_event.py +353 -0
  29. seolpyo_mplchart/_chart/slider/h_data.py +102 -0
  30. seolpyo_mplchart/_chart/slider/test.py +71 -0
  31. seolpyo_mplchart/_config/ax.py +2 -0
  32. seolpyo_mplchart/_config/candle.py +1 -0
  33. seolpyo_mplchart/_config/cursor.py +4 -0
  34. seolpyo_mplchart/_config/figure.py +4 -4
  35. seolpyo_mplchart/_config/ma.py +2 -0
  36. seolpyo_mplchart/_config/nums.py +67 -0
  37. seolpyo_mplchart/_config/slider/config.py +2 -2
  38. seolpyo_mplchart/_config/slider/figure.py +3 -4
  39. seolpyo_mplchart/_config/slider/nav.py +3 -2
  40. seolpyo_mplchart/_config/unit.py +1 -1
  41. seolpyo_mplchart/_config/volume.py +1 -0
  42. seolpyo_mplchart/_utils/__init__.py +10 -0
  43. seolpyo_mplchart/_utils/nums.py +1 -0
  44. seolpyo_mplchart/_utils/theme/__init__.py +15 -0
  45. seolpyo_mplchart/_utils/theme/dark.py +57 -0
  46. seolpyo_mplchart/_utils/theme/light.py +56 -0
  47. seolpyo_mplchart/_utils/utils.py +28 -0
  48. seolpyo_mplchart/_utils/xl/__init__.py +15 -0
  49. seolpyo_mplchart/_utils/xl/csv.py +46 -0
  50. seolpyo_mplchart/_utils/xl/xlsx.py +49 -0
  51. seolpyo_mplchart/sample/apple.txt +6058 -0
  52. seolpyo_mplchart/sample/samsung.txt +5938 -0
  53. seolpyo_mplchart/test.py +5 -5
  54. {seolpyo_mplchart-2.0.0.3.dist-info → seolpyo_mplchart-2.1.0.4.dist-info}/METADATA +22 -14
  55. seolpyo_mplchart-2.1.0.4.dist-info/RECORD +90 -0
  56. seolpyo_mplchart-2.0.0.3.dist-info/RECORD +0 -50
  57. {seolpyo_mplchart-2.0.0.3.dist-info → seolpyo_mplchart-2.1.0.4.dist-info}/WHEEL +0 -0
  58. {seolpyo_mplchart-2.0.0.3.dist-info → seolpyo_mplchart-2.1.0.4.dist-info}/top_level.txt +0 -0
@@ -2,11 +2,22 @@ import json
2
2
  from typing import Literal
3
3
  from pathlib import Path
4
4
 
5
- import matplotlib.pyplot as plt
6
- import pandas as pd
7
-
8
5
  from ._chart import OnlyChart, CursorChart, SliderChart
9
- from ._config import DEFAULTCONFIG, DEFAULTCONFIG_EN, SLIDERCONFIG, SLIDERCONFIG_EN, ConfigData, SliderConfigData
6
+ from ._config import (
7
+ ConfigData, DEFAULTCONFIG, DEFAULTCONFIG_EN,
8
+ SliderConfigData, SLIDERCONFIG, SLIDERCONFIG_EN,
9
+ )
10
+ from ._utils import (
11
+ float_to_str,
12
+ xl_to_dataList,
13
+ convert_num,
14
+ float_to_str,
15
+ convert_unit, convert_unit_en,
16
+ data_unit_ko, data_unit_en,
17
+ list_to_DataFrame,
18
+ show, close,
19
+ switch_backend,
20
+ )
10
21
 
11
22
 
12
23
  # __all__ = [
@@ -49,137 +60,10 @@ def sample(stock: Literal['samsung', 'apple']='samsung', chart: Literal['Chart',
49
60
 
50
61
  with open(path_file, 'r', encoding='utf-8') as txt:
51
62
  data = json.load(txt)
52
- df = pd.DataFrame(data)
63
+ df = list_to_DataFrame(data)
53
64
 
54
65
  CHART.set_data(df)
55
66
 
56
- show()
57
- close()
58
- return
59
-
60
-
61
- def switch_backend(newbackend='TkAgg'):
62
- "call matplotlib.pyplot.switch_backend(newbackend)"
63
- return plt.switch_backend(newbackend)
64
-
65
-
66
- def close(fig='all'):
67
- "call matplotlib.pyplot.close(fig)"
68
- return plt.close(fig)
69
-
70
- def show(Close=True):
71
- """
72
- call matplotlib.pyplot.show()
73
- ```if Close``` if True, run matplotlib.pyplot.close('all') after window closee.
74
- """
75
- plt.show()
76
- if Close:
77
- close()
67
+ show(Close=True)
78
68
  return
79
69
 
80
-
81
- def set_theme(config: ConfigData|SliderConfigData, theme: Literal['light', 'dark']='dark'):
82
- if theme == 'light':
83
- # axes
84
- config.FIGURE.facecolor = '#fafafa'
85
- config.AX.facecolor = '#fafafa'
86
- config.AX.TICK.edgecolor = 'k'
87
- config.AX.TICK.fontcolor = 'k'
88
- config.AX.GRID.color = '#d0d0d0'
89
-
90
- # candle
91
- config.CANDLE.line_color = 'k'
92
- config.CANDLE.FACECOLOR.bull_rise = '#FF2400'
93
- config.CANDLE.FACECOLOR.bull_fall = 'w'
94
- config.CANDLE.FACECOLOR.bear_fall = '#1E90FF'
95
- config.CANDLE.FACECOLOR.bear_rise = 'w'
96
-
97
- config.CANDLE.EDGECOLOR.bull_rise = '#FF2400'
98
- config.CANDLE.EDGECOLOR.bull_fall = '#FF2400'
99
- config.CANDLE.EDGECOLOR.bear_fall = '#1E90FF'
100
- config.CANDLE.EDGECOLOR.bear_rise = '#1E90FF'
101
- config.CANDLE.EDGECOLOR.doji = 'k'
102
-
103
- # volume
104
- config.VOLUME.FACECOLOR.rise = '#F27663'
105
- config.VOLUME.FACECOLOR.fall = '#70B5F2'
106
- config.VOLUME.FACECOLOR.doji = '#BEBEBE'
107
-
108
- config.VOLUME.EDGECOLOR.rise = '#F27663'
109
- config.VOLUME.EDGECOLOR.fall = '#70B5F2'
110
- config.VOLUME.EDGECOLOR.doji = '#BEBEBE'
111
-
112
- # ma
113
- config.MA.color_default = 'k'
114
- config.MA.color_list = ['#8B00FF', '#008000', '#A0522D', '#008B8B', '#FF0080']
115
- # text
116
- config.CURSOR.TEXT.BBOX.facecolor = 'w'
117
- config.CURSOR.TEXT.BBOX.edgecolor = 'k'
118
- config.CURSOR.TEXT.color = 'k'
119
-
120
- # box
121
- config.CURSOR.BOX.edgecolor = 'k'
122
-
123
- # line
124
- config.CURSOR.CROSSLINE.edgecolor = 'k'
125
-
126
- # wartermark
127
- config.FIGURE.WATERMARK.color = 'k'
128
-
129
- if getattr(config, 'SLIDER', None):
130
- config.SLIDER.NAVIGATOR.edgecolor = '#2962FF'
131
- config.SLIDER.NAVIGATOR.facecolor = '#0000002E'
132
- elif theme == 'dark':
133
- # axes
134
- config.FIGURE.facecolor = '#0f0f0f'
135
- config.AX.facecolor = '#0f0f0f'
136
- config.AX.TICK.edgecolor = '#dbdbdb'
137
- config.AX.TICK.fontcolor = '#dbdbdb'
138
- config.AX.GRID.color = '#1c1c1c'
139
-
140
- # candle
141
- config.CANDLE.line_color = 'w'
142
- config.CANDLE.FACECOLOR.bull_rise = '#089981'
143
- config.CANDLE.FACECOLOR.bull_fall = '#0f0f0f'
144
- config.CANDLE.FACECOLOR.bear_fall = '#f23645'
145
- config.CANDLE.FACECOLOR.bear_rise = '#0f0f0f'
146
-
147
- config.CANDLE.EDGECOLOR.bull_rise = '#089981'
148
- config.CANDLE.EDGECOLOR.bull_fall = '#089981'
149
- config.CANDLE.EDGECOLOR.bear_fall = '#f23645'
150
- config.CANDLE.EDGECOLOR.bear_rise = '#f23645'
151
- config.CANDLE.EDGECOLOR.doji = 'w'
152
-
153
- # volume
154
- config.VOLUME.FACECOLOR.rise = '#2A8076'
155
- config.VOLUME.FACECOLOR.fall = '#BE4F58'
156
- config.VOLUME.FACECOLOR.doji = '#82828A'
157
-
158
- config.VOLUME.EDGECOLOR.rise = '#2A8076'
159
- config.VOLUME.EDGECOLOR.fall = '#BE4F58'
160
- config.VOLUME.EDGECOLOR.doji = '#82828A'
161
-
162
- # ma
163
- config.MA.color_default = 'w'
164
- config.MA.color_list = ['#FFB300', '#03A9F4', '#AB47BC', '#8BC34A', '#EF5350']
165
-
166
- # text
167
- config.CURSOR.TEXT.BBOX.facecolor = '#3d3d3d'
168
- config.CURSOR.TEXT.BBOX.edgecolor = '#ffffff'
169
- config.CURSOR.TEXT.color = '#ffffff'
170
-
171
- # box
172
- config.CURSOR.BOX.edgecolor = 'w'
173
-
174
- # line
175
- config.CURSOR.CROSSLINE.edgecolor = '#9c9c9c'
176
-
177
- # wartermark
178
- config.FIGURE.WATERMARK.color = 'w'
179
-
180
- if getattr(config, 'SLIDER', None):
181
- config.SLIDER.NAVIGATOR.edgecolor = "#00A6FF"
182
- config.SLIDER.NAVIGATOR.facecolor = '#FFFFFF4D'
183
-
184
- return config
185
-
@@ -1,14 +1,14 @@
1
- from ._draw import Chart as _OC
2
- from ._cursor import Chart as _CC
3
- from ._slider import Chart as _SC
1
+ import pandas as pd
4
2
 
3
+ from .cursor import Chart as _CC, BaseChart as _OC
4
+ from .slider import Chart as _SC
5
5
 
6
6
 
7
7
  class OnlyChart(_OC):
8
8
  r"""
9
9
  You can see the guidance document:
10
10
  Korean: https://white.seolpyo.com/entry/147/?from=package
11
- English: https://github.com/white-seolpyo/seolpyo-mplchart/blob/main/README.md
11
+ English: https://github.com/white-seolpyo/seolpyo-mplchart
12
12
 
13
13
  Quick Start:
14
14
  ```
@@ -20,37 +20,37 @@ class OnlyChart(_OC):
20
20
  ```
21
21
 
22
22
  Class Variables:
23
- watermark: watermark text.
24
-
25
23
  df: stock data DataFrame.
26
24
  key_date: date column key. default 'date'
27
25
  key_open, key_high, key_low, key_close: price column key. default ('open', 'high', 'low', 'close')
28
26
  key_volume: volume column key. default 'volume'. If ```if config.VOLUME.EDGECOLOR.volume``` is ```False```, the volume chart is not drawn.
29
27
 
30
- cnadle_on_ma: if True: draw candle above ma. else: draw candle below ma lines.
28
+ candle_on_ma: if True: draw candle above ma. else: draw candle below ma lines.
29
+
31
30
  limit_candle: If (`the number of candles to draw < limit_candle or not limit_candle`): draw candles, else: draw wicks.
32
31
  limit_wick: If (`the number of candles to draw < limit_wick or not limit_wick`): draw wicks, else: draw line.
33
- limit_volume: If (`the number of candles to draw < limit_volume or not limit_volume`): draw volumebar, else: draw wicks.
34
- limit_ma: If (`the number of candles to draw < limit_ma or not limit_ma`): draw ma lines, else: don't draw ma lines.
35
- fraction: if True and number has a fractional part, display it as a fraction.
32
+
33
+ watermark: watermark text.
36
34
  """
35
+ df: pd.DataFrame
36
+
37
37
  key_date = 'date'
38
38
  key_open, key_high, key_low, key_close = ('open', 'high', 'low', 'close')
39
39
  key_volume = 'volume'
40
40
 
41
41
  candle_on_ma = True
42
+
43
+ watermark = 'seolpyo mplchart'
44
+
42
45
  limit_candle = 400
43
46
  limit_wick = 2_000
44
- limit_volume = 200
45
- limit_ma = None
46
- watermark = 'seolpyo mplchart'
47
47
 
48
48
 
49
49
  class CursorChart(_CC):
50
50
  r"""
51
51
  You can see the guidance document:
52
52
  Korean: https://white.seolpyo.com/entry/147/?from=package
53
- English: https://github.com/white-seolpyo/seolpyo-mplchart/blob/main/README.md
53
+ English: https://github.com/white-seolpyo/seolpyo-mplchart
54
54
 
55
55
  Quick Start:
56
56
  ```
@@ -62,38 +62,39 @@ class CursorChart(_CC):
62
62
  ```
63
63
 
64
64
  Class Variables:
65
- watermark: watermark
66
-
67
65
  df: stock data DataFrame.
68
66
  key_date: date column key. default 'date'
69
67
  key_open, key_high, key_low, key_close: price column key. default ('open', 'high', 'low', 'close')
70
68
  key_volume: volume column key. default 'volume'. If ```if config.VOLUME.EDGECOLOR.volume``` is ```False```, the volume chart is not drawn.
71
69
 
72
- cnadle_on_ma: if True: draw candle above ma. else: draw candle below ma lines.
70
+ candle_on_ma: if True: draw candle above ma. else: draw candle below ma lines.
71
+ fraction: if True and number has a fractional part, display it as a fraction.
72
+
73
73
  limit_candle: If (`the number of candles to draw < limit_candle or not limit_candle`): draw candles, else: draw wicks.
74
74
  limit_wick: If (`the number of candles to draw < limit_wick or not limit_wick`): draw wicks, else: draw line.
75
- limit_volume: If (`the number of candles to draw < limit_volume or not limit_volume`): draw volumebar, else: draw wicks.
76
- limit_ma: If (`the number of candles to draw < limit_ma or not limit_ma`): draw ma lines, else: don't draw ma lines.
77
- fraction: if True and number has a fractional part, display it as a fraction.
75
+
76
+ watermark: watermark
78
77
  """
78
+ df: pd.DataFrame
79
+
79
80
  key_date = 'date'
80
81
  key_open, key_high, key_low, key_close = ('open', 'high', 'low', 'close')
81
82
  key_volume = 'volume'
82
83
 
83
84
  candle_on_ma = True
85
+ fraction = False
86
+
87
+ watermark = 'seolpyo mplchart'
88
+
84
89
  limit_candle = 400
85
90
  limit_wick = 2_000
86
- limit_volume = 200
87
- limit_ma = None
88
- watermark = 'seolpyo mplchart'
89
- fraction = False
90
91
 
91
92
 
92
93
  class SliderChart(_SC):
93
94
  r"""
94
95
  You can see the guidance document:
95
96
  Korean: https://white.seolpyo.com/entry/147/?from=package
96
- English: https://github.com/white-seolpyo/seolpyo-mplchart/blob/main/README.md
97
+ English: https://github.com/white-seolpyo/seolpyo-mplchart
97
98
 
98
99
  Quick Start:
99
100
  ```
@@ -112,26 +113,33 @@ class SliderChart(_SC):
112
113
  key_open, key_high, key_low, key_close: price column key. default ('open', 'high', 'low', 'close')
113
114
  key_volume: volume column key. default 'volume'. If ```if config.VOLUME.EDGECOLOR.volume``` is ```False```, the volume chart is not drawn.
114
115
 
115
- cnadle_on_ma: if True: draw candle above ma. else: draw candle below ma lines.
116
+ candle_on_ma: if True: draw candle above ma. else: draw candle below ma lines.
117
+ fraction: if True and number has a fractional part, display it as a fraction.
118
+ slider_top: set slider position. if True: slider top. else: bottom
119
+
116
120
  limit_candle: If (`the number of candles to draw < limit_candle or not limit_candle`): draw candles, else: draw wicks.
117
121
  limit_wick: If (`the number of candles to draw < limit_wick or not limit_wick`): draw wicks, else: draw line.
118
122
  limit_volume: If (`the number of candles to draw < limit_volume or not limit_volume`): draw volumebar, else: draw wicks.
119
123
  limit_ma: If (`the number of candles to draw < limit_ma or not limit_ma`): draw ma lines, else: don't draw ma lines.
120
- fraction: if True and number has a fractional part, display it as a fraction.
121
- slider_top: set slider position. if True: slider top. else: bottom
124
+
122
125
  min_distance: the minimum number of candles displayed on the screen.
123
126
  """
127
+ df: pd.DataFrame
128
+
124
129
  key_date = 'date'
125
130
  key_open, key_high, key_low, key_close = ('open', 'high', 'low', 'close')
126
131
  key_volume = 'volume'
127
132
 
128
133
  candle_on_ma = True
134
+ fraction = False
135
+ slider_top = True
136
+
137
+ watermark = 'seolpyo mplchart'
138
+
129
139
  limit_candle = 400
130
140
  limit_wick = 2_000
131
141
  limit_volume = 200
132
142
  limit_ma = 8_000
133
- watermark = 'seolpyo mplchart'
134
- fraction = False
135
- slider_top = True
143
+
136
144
  min_distance = 5
137
145
 
@@ -0,0 +1,111 @@
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 DEFAULTCONFIG, ConfigData
9
+
10
+ from .a_canvas import CanvasMixin, Figure
11
+ from .b_artist import ArtistMixin
12
+ from .c_draw import DrawMixin
13
+ from .d_segment import SegmentMixin
14
+ from .e_axis import AxisMixin
15
+ from .f_background import BackgroundMixin
16
+ from .g_event import EventMixin
17
+ from .h_data import DataMixin
18
+
19
+
20
+ class Chart(
21
+ CanvasMixin,
22
+ ArtistMixin,
23
+ DrawMixin,
24
+ SegmentMixin,
25
+ AxisMixin,
26
+ BackgroundMixin,
27
+ EventMixin,
28
+ DataMixin,
29
+ ):
30
+ limit_candle = 400
31
+ limit_wick = 2_000
32
+ candle_on_ma = True
33
+
34
+ key_date = 'date'
35
+ key_open, key_high, key_low, key_close = ('open', 'high', 'low', 'close')
36
+ key_volume = 'volume'
37
+
38
+ index_list: list[int] = []
39
+
40
+ df: pd.DataFrame
41
+
42
+ CONFIG: ConfigData
43
+
44
+ figure: Figure
45
+ ax_legend: Axes
46
+ ax_price: Axes
47
+ ax_volume: Axes
48
+
49
+ artist_watermark: Text
50
+ collection_candle: LineCollection
51
+ collection_volume: LineCollection
52
+ collection_ma: LineCollection
53
+
54
+ ###
55
+
56
+ segment_volume: np.ndarray
57
+ segment_volume_wick: np.ndarray
58
+ facecolor_volume: np.ndarray
59
+ edgecolor_volume: np.ndarray
60
+
61
+ segment_candle: np.ndarray
62
+ segment_candle_wick: np.ndarray
63
+ segment_priceline: np.ndarray
64
+ facecolor_candle: np.ndarray
65
+ edgecolor_candle: np.ndarray
66
+
67
+ segment_ma: np.ndarray
68
+ edgecolor_ma: np.ndarray
69
+
70
+ price_ymin: int
71
+ price_ymax: int
72
+ volume_ymax: int
73
+
74
+ chart_price_ymax: float
75
+ chart_volume_ymax: float
76
+
77
+ vxmin: int
78
+ vxmax: int
79
+
80
+ ###
81
+
82
+ _visible_ma: set[int] = set()
83
+
84
+ _background = None
85
+ _background_background = None
86
+ _creating_background = False
87
+
88
+ def __init__(self, config=DEFAULTCONFIG):
89
+ self.CONFIG = config
90
+ super().__init__()
91
+
92
+ self.add_artists()
93
+ self.connect_events()
94
+ return
95
+
96
+ def refresh(self):
97
+ self.set_artists()
98
+
99
+ self.set_candle_segments()
100
+ self.set_color_segments()
101
+ ma_alpha = self.collection_ma.get_alpha()
102
+ # print(f'{ma_alpha=}')
103
+ if ma_alpha is not None:
104
+ self.collection_ma.set_alpha([1 for _ in ma_alpha])
105
+
106
+ self.axis(self.vxmin, xmax=self.vxmax-1)
107
+
108
+ self.set_canvas()
109
+ self.figure.canvas.draw()
110
+ return
111
+
@@ -0,0 +1,250 @@
1
+ import matplotlib.pyplot as plt
2
+ import matplotlib.style as mplstyle
3
+ from matplotlib.artist import Artist
4
+ from matplotlib.axes import Axes
5
+ from matplotlib.backends.backend_agg import FigureCanvasAgg, RendererAgg
6
+ from matplotlib.backend_bases import FigureManagerBase
7
+ from matplotlib.figure import Figure as Fig
8
+ from matplotlib.text import Text
9
+
10
+ try: plt.switch_backend('TkAgg')
11
+ except: pass
12
+
13
+ # 한글 깨짐 문제 방지
14
+ try: plt.rcParams['font.family'] ='Malgun Gothic'
15
+ except: pass
16
+
17
+ from ..._config import DEFAULTCONFIG, ConfigData
18
+
19
+
20
+ mplstyle.use('fast')
21
+
22
+
23
+ class Canvas(FigureCanvasAgg):
24
+ manager: FigureManagerBase
25
+ renderer = RendererAgg
26
+
27
+ class Figure(Fig):
28
+ canvas: Canvas
29
+
30
+
31
+ class Base:
32
+ CONFIG: ConfigData
33
+ figure: Figure = None
34
+
35
+ def add_axes(self):
36
+ if not self.figure:
37
+ self.figure, *_ = plt.subplots(
38
+ 3, # row 수
39
+ figsize=self.CONFIG.FIGURE.figsize, # 기본 크기
40
+ height_ratios=(
41
+ 1,
42
+ self.CONFIG.FIGURE.RATIO.price,
43
+ self.CONFIG.FIGURE.RATIO.volume,
44
+ ) # row 크기 비율
45
+ )
46
+
47
+ self.ax_legend, self.ax_price, self.ax_volume = self.figure.axes
48
+ self.ax_legend.set_label('legend ax')
49
+ self.ax_price.set_label('price ax')
50
+ self.ax_volume.set_label('volume ax')
51
+
52
+ return
53
+
54
+
55
+ class FigureMixin(Base):
56
+ key_volume: str
57
+ ax_legend: Axes
58
+
59
+ def _set_figure_ratios(self):
60
+ gs = self.figure.axes[0].get_subplotspec().get_gridspec()
61
+
62
+ ratio_volume = self.CONFIG.FIGURE.RATIO.volume
63
+ if not self.key_volume:
64
+ ratio_volume = 0
65
+
66
+ legend = self.ax_legend.get_legend()
67
+ if not legend:
68
+ ratios = [
69
+ 0,
70
+ self.CONFIG.FIGURE.RATIO.price, ratio_volume
71
+ ]
72
+ else:
73
+ fig_heihgt = self.figure.get_figheight()
74
+ fig_px = fig_heihgt * (1-self.CONFIG.FIGURE.ADJUST.hspace*2) * self.figure.dpi
75
+ # print(f'{(fig_heihgt, fig_px)=}')
76
+
77
+ # Legend에 Axes 높이 맞추기
78
+ bbox = legend.get_window_extent().transformed(self.figure.transFigure.inverted())
79
+ ax_pos = self.ax_legend.get_position()
80
+ self.ax_legend.set_position([ax_pos.x0, ax_pos.y0, ax_pos.width, bbox.height])
81
+
82
+ legend_height = bbox.height
83
+ legend_px = legend.get_window_extent().height
84
+ # print(f'{(legend_height, legend_px)=}')
85
+
86
+ chart_px = fig_px - legend_height
87
+ chart_ratio = self.CONFIG.FIGURE.RATIO.price + ratio_volume
88
+ div_chart = chart_px / chart_ratio
89
+ price_px = div_chart * self.CONFIG.FIGURE.RATIO.price
90
+ volume_px = div_chart * ratio_volume
91
+
92
+ # 차트 비율 변경
93
+ ratios = [
94
+ legend_px * 1.2,
95
+ price_px, volume_px
96
+ ]
97
+ # print(f'{ratios=}')
98
+ gs.set_height_ratios(ratios)
99
+
100
+ self.figure.tight_layout()
101
+
102
+ # 플롯간 간격 설정(Configure subplots)
103
+ self.figure.subplots_adjust(**self.CONFIG.FIGURE.ADJUST.__dict__)
104
+
105
+ return
106
+
107
+ def _set_figure(self):
108
+ self.figure.canvas.manager.set_window_title('Seolpyo MPLChart')
109
+
110
+ # print(f'{self.CONFIG.FIGURE.RATIO.volume=}')
111
+ # print(f'{gs.get_height_ratios()=}')
112
+
113
+ self._set_figure_ratios()
114
+
115
+ self.figure.set_facecolor(self.CONFIG.FIGURE.facecolor)
116
+ return
117
+
118
+
119
+ class AxesMixin(FigureMixin):
120
+ def _set_axes(self):
121
+ # ax 요소 animated 처리
122
+ for ax in self.figure.axes:
123
+ ax.patch.set_animated(True)
124
+
125
+ # ax 경계선
126
+ for spine in ax.spines.values():
127
+ spine.set_animated(True)
128
+
129
+ for axis in (ax.xaxis, ax.yaxis):
130
+ axis.set_animated(True)
131
+ axis.label.set_animated(True)
132
+ for tick in axis.get_major_ticks() + axis.get_minor_ticks():
133
+ artists: list[Artist] = [
134
+ tick.tick1line, tick.tick2line,
135
+ tick.gridline,
136
+ tick.label1, tick.label2
137
+ ]
138
+ for artist in artists:
139
+ if artist is not None:
140
+ artist.set_animated(True)
141
+
142
+ # y ticklabel foramt 설정
143
+ self.ax_price.yaxis.set_major_formatter(
144
+ lambda x, _: self.CONFIG.UNIT.func(
145
+ x,
146
+ word=self.CONFIG.UNIT.price,
147
+ digit=self.CONFIG.UNIT.digit+(0 if self.CONFIG.UNIT.digit % 1 else 2)
148
+ )
149
+ )
150
+ self.ax_volume.yaxis.set_major_formatter(
151
+ lambda x, _: self.CONFIG.UNIT.func(
152
+ x,
153
+ word=self.CONFIG.UNIT.volume,
154
+ digit=self.CONFIG.UNIT.digit_volume
155
+ )
156
+ )
157
+
158
+ if not self.key_volume:
159
+ # tick 그리지 않기
160
+ self.ax_volume.set_yticklabels([])
161
+ self.ax_volume.set_yticks([])
162
+
163
+ # 공통 설정
164
+ for ax in (self.ax_price, self.ax_volume):
165
+ ax.xaxis.set_animated(True)
166
+ ax.yaxis.set_animated(True)
167
+
168
+ # x tick 외부 눈금 표시하지 않기
169
+ ax.xaxis.set_ticks_position('none')
170
+ # x tick label 제거
171
+ ax.set_xticklabels([])
172
+ # y tick 위치를 우측으로 이동
173
+ ax.tick_params(left=False, right=True, labelleft=False, labelright=True)
174
+
175
+ # 차트 영역 배경 색상
176
+ ax.set_facecolor(self.CONFIG.AX.facecolor)
177
+
178
+ # Axes 외곽선 색 변경(틱 색과 일치)
179
+ for i in ['top', 'bottom', 'left', 'right']:
180
+ ax.spines[i].set_color(self.CONFIG.AX.TICK.edgecolor)
181
+ # 틱 색상
182
+ ax.tick_params('both', colors=self.CONFIG.AX.TICK.edgecolor)
183
+ # 틱 라벨 색상
184
+ ticklabels: list[Text] = ax.get_xticklabels() + ax.get_yticklabels()
185
+ for ticklabel in ticklabels:
186
+ ticklabel.set_color(self.CONFIG.AX.TICK.fontcolor)
187
+
188
+ # Axes grid(구분선, 격자) 그리기
189
+ # 어째서인지 grid의 zorder 값을 선언해도 1.6을 값으로 한다.
190
+ ax.grid(**self.CONFIG.AX.GRID.__dict__)
191
+
192
+ # 거래량 차트의 x tick 외부 눈금 표시하기
193
+ self.ax_volume.xaxis.set_ticks_position('bottom')
194
+ # major tick mark 길이를 0으로 만들어 튀어나오지 않게 하기
195
+ self.ax_volume.tick_params('x', which='major', length=0)
196
+ # minor tick mark 색상 변경
197
+ self.ax_volume.tick_params('x', which='minor', colors=self.CONFIG.AX.TICK.edgecolor)
198
+ return
199
+
200
+
201
+ class LegendMixin(AxesMixin):
202
+ def _set_axes_legend(self):
203
+ # 이평선 라벨 axis 그리지 않기
204
+ self.ax_legend.set_axis_off()
205
+ self.ax_legend.xaxis.set_animated(True)
206
+ self.ax_legend.yaxis.set_animated(True)
207
+ self.ax_legend.set_animated(True)
208
+
209
+ # 이평선 라벨 Axes 배경색
210
+ legends = self.ax_legend.get_legend()
211
+ if legends:
212
+ legends.get_frame().set_facecolor(self.CONFIG.AX.facecolor)
213
+
214
+ # 이평선 라벨 Axes 테두리색
215
+ legends = self.ax_legend.get_legend()
216
+ if legends:
217
+ legends.get_frame().set_edgecolor(self.CONFIG.AX.TICK.edgecolor)
218
+
219
+ # 이평선 라벨 폰트 색상
220
+ fontcolor = self.CONFIG.AX.TICK.fontcolor
221
+ legends = self.ax_legend.get_legend()
222
+ if legends:
223
+ legend_labels: list[Text] = legends.texts
224
+ for i in legend_labels:
225
+ i.set_color(fontcolor)
226
+ return
227
+
228
+
229
+ class CanvasMixin(LegendMixin):
230
+ figure: Figure
231
+ ax_legend: Axes
232
+ ax_price: Axes
233
+ ax_volume: Axes
234
+
235
+ def __init__(self, config=DEFAULTCONFIG):
236
+ # 기본 툴바 비활성화
237
+ plt.rcParams['toolbar'] = 'None'
238
+ # plt.rcParams['figure.dpi'] = 600
239
+
240
+ self.CONFIG = config
241
+ self.add_axes()
242
+ self.set_canvas()
243
+ return
244
+
245
+ def set_canvas(self):
246
+ self._set_axes()
247
+ self._set_axes_legend()
248
+ self._set_figure()
249
+ return
250
+