Skip to content

Commit 7b7b0cd

Browse files
Merge branch 'external_axes'
2 parents be2aa09 + 3116b5b commit 7b7b0cd

File tree

8 files changed

+2770
-23
lines changed

8 files changed

+2770
-23
lines changed

examples/scratch_pad/dev_ext_axes_nightclouds_issue.ipynb

Lines changed: 633 additions & 0 deletions
Large diffs are not rendered by default.

examples/scratch_pad/dev_ext_axes_subclass.ipynb

Lines changed: 755 additions & 0 deletions
Large diffs are not rendered by default.

examples/scratch_pad/dev_external_axes.ipynb

Lines changed: 1175 additions & 0 deletions
Large diffs are not rendered by default.

src/mplfinance/_arg_validators.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import pandas as pd
33
import numpy as np
44
import datetime
5+
from mplfinance._helpers import _list_of_dict
6+
import matplotlib as mpl
57

68
def _check_and_prepare_data(data, config):
79
'''
@@ -266,3 +268,53 @@ def _scale_padding_validator(value):
266268
else:
267269
raise ValueError('`scale_padding` kwarg must be a number, or dict of (left,right,top,bottom) numbers.')
268270
return False
271+
272+
def _check_for_external_axes(config):
273+
'''
274+
Check that all `fig` and `ax` kwargs are either ALL None,
275+
or ALL are valid instances of Figures/Axes:
276+
277+
An external Axes object can be passed in three places:
278+
- mpf.plot() `ax=` kwarg
279+
- mpf.plot() `volume=` kwarg
280+
- mpf.make_addplot() `ax=` kwarg
281+
ALL three places MUST be an Axes object, OR
282+
ALL three places MUST be None. But it may not be mixed.
283+
'''
284+
ap_axlist = []
285+
addplot = config['addplot']
286+
if addplot is not None:
287+
if isinstance(addplot,dict):
288+
addplot = [addplot,] # make list of dict to be consistent
289+
elif not _list_of_dict(addplot):
290+
raise TypeError('addplot must be `dict`, or `list of dict`, NOT '+str(type(addplot)))
291+
for apd in addplot:
292+
ap_axlist.append(apd['ax'])
293+
294+
if len(ap_axlist) > 0:
295+
if config['ax'] is None:
296+
if not all([ax is None for ax in ap_axlist]):
297+
raise ValueError('make_addplot() `ax` kwarg NOT all None, while plot() `ax` kwarg IS None')
298+
else: # config['ax'] is NOT None:
299+
if not isinstance(config['ax'],mpl.axes.Axes):
300+
raise ValueError('plot() ax kwarg must be of type `matplotlib.axis.Axes`')
301+
if not all([isinstance(ax,mpl.axes.Axes) for ax in ap_axlist]):
302+
raise ValueError('make_addplot() `ax` kwargs must all be of type `matplotlib.axis.Axes`')
303+
304+
# At this point, if we have not raised an exception, then plot(ax=) and make_addplot(ax=)
305+
# are in sync: either they are all None, or they are all of type `matplotlib.axes.Axes`.
306+
# Therefore we only need plot(ax=), i.e. config['ax'], as we check `volume` and `fig`:
307+
308+
if config['ax'] is None:
309+
if isinstance(config['volume'],mpl.axes.Axes):
310+
raise ValueError('`volume` set to external Axes requires all other Axes be external.')
311+
if config['fig'] is not None:
312+
raise ValueError('`fig` kwarg must be None if `ax` kwarg is None.')
313+
else:
314+
if not isinstance(config['volume'],mpl.axes.Axes) and config['volume'] != False:
315+
raise ValueError('`volume` must be of type `matplotlib.axis.Axes`')
316+
if not isinstance(config['fig'],mpl.figure.Figure):
317+
raise ValueError('`fig` kwarg must be of type `matplotlib.figure.Figure`')
318+
319+
external_axes_mode = True if isinstance(config['ax'],mpl.axes.Axes) else False
320+
return external_axes_mode

src/mplfinance/_mplrcputils.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#!/usr/bin/env python
2+
"""
3+
rcparams utilities
4+
"""
5+
6+
import pandas as pd
7+
import matplotlib.pyplot as plt
8+
import sys
9+
10+
__author__ = "Daniel Goldfarb"
11+
__version__ = "0.1.0"
12+
__license__ = "MIT"
13+
14+
def rcParams_to_df(rcp,name=None):
15+
keys = []
16+
vals = []
17+
for item in rcp:
18+
keys.append(item)
19+
vals.append(rcp[item])
20+
df = pd.DataFrame(vals,index=pd.Index(keys,name='rcParamsKey'))
21+
if name is not None:
22+
df.columns = [name]
23+
else:
24+
df.columns = ['Value']
25+
return df
26+
27+
def compare_styles(s1,s2):
28+
with plt.rc_context():
29+
plt.style.use('default')
30+
plt.style.use(s1)
31+
df1 = rcParams_to_df(plt.rcParams,name=s1)
32+
33+
with plt.rc_context():
34+
plt.style.use('default')
35+
plt.style.use(s2)
36+
df2 = rcParams_to_df(plt.rcParams,name=s2)
37+
38+
df = pd.concat([df1,df2],axis=1)
39+
dif = df[df[s1] != df[s2]].dropna(how='all')
40+
return (dif,df,df1,df2)
41+
42+
def main():
43+
""" Main entry point of the app """
44+
def usage():
45+
print('\n Usage: rcparams <command> <arguments> \n')
46+
print(' Available commands: ')
47+
print(' rcparams find <findstring>')
48+
print(' rcparams compare <style1> <style2>')
49+
print('')
50+
exit(1)
51+
commands = ('find','compare')
52+
53+
if len(sys.argv) < 3 :
54+
print('\n Too few arguments!')
55+
usage()
56+
57+
command = sys.argv[1]
58+
if command not in commands:
59+
print('\n Unrecognized command \"'+command+'\"')
60+
usage()
61+
62+
if command == 'find':
63+
findstr = sys.argv[2]
64+
df = rcParams_to_df(plt.rcParams)
65+
if findstr == '--all':
66+
for key in df.index:
67+
print(key+':',df.loc[key,'Value'])
68+
else:
69+
print(df[df.index.str.contains(findstr)])
70+
71+
elif command == 'compare':
72+
if len(sys.argv) < 4 :
73+
print('\n Need two styles to compare!')
74+
usage()
75+
style1 = sys.argv[2]
76+
style2 = sys.argv[3]
77+
dif,df,df1,df2 = compare_styles(style1,style2)
78+
print('\n==== dif ====\n',dif)
79+
80+
else:
81+
print('\n Unrecognized command \"'+command+'\"')
82+
usage()
83+
84+
85+
if __name__ == "__main__":
86+
""" This is executed when run from the command line """
87+
main()

src/mplfinance/_panels.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,13 @@ def _build_panels( figure, config ):
145145
#print('panels=')
146146
#print(panels)
147147

148+
# TODO: Throughout this section, right_pad is intentionally *less*
149+
# than left_pad. This assumes that the y-axis labels are on
150+
# the left, which is true for many mpf_styles, but *not* all.
151+
# Ideally need to determine which side has the axis labels.
152+
# And keep in mind, if secondary_y is in effect, then both
153+
# sides can have axis labels.
154+
148155
left_pad = 0.18
149156
right_pad = 0.10
150157
top_pad = 0.12

src/mplfinance/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11

2-
version_info = (0, 12, 6, 'alpha', 4)
2+
version_info = (0, 12, 6, 'alpha', 5)
33

44
_specifier_ = {'alpha': 'a','beta': 'b','candidate': 'rc','final': ''}
55

src/mplfinance/plotting.py

Lines changed: 60 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import matplotlib.dates as mdates
22
import matplotlib.pyplot as plt
33
import matplotlib.colors as mcolors
4+
import matplotlib.axes as mpl_axes
5+
import matplotlib.figure as mpl_fig
46
import pandas as pd
57
import numpy as np
68
import copy
@@ -33,7 +35,7 @@
3335
from mplfinance._arg_validators import _hlines_validator, _vlines_validator
3436
from mplfinance._arg_validators import _alines_validator, _tlines_validator
3537
from mplfinance._arg_validators import _scale_padding_validator
36-
from mplfinance._arg_validators import _valid_panel_id
38+
from mplfinance._arg_validators import _valid_panel_id, _check_for_external_axes
3739

3840
from mplfinance._panels import _build_panels
3941
from mplfinance._panels import _set_ticks_on_bottom_panel_only
@@ -104,7 +106,7 @@ def _valid_plot_kwargs():
104106
'Validator' : lambda value: value in _styles.available_styles() or isinstance(value,dict) },
105107

106108
'volume' : { 'Default' : False,
107-
'Validator' : lambda value: isinstance(value,bool) },
109+
'Validator' : lambda value: isinstance(value,bool) or isinstance(value,mpl_axes.Axes) },
108110

109111
'mav' : { 'Default' : None,
110112
'Validator' : _mav_validator },
@@ -241,6 +243,12 @@ def _valid_plot_kwargs():
241243

242244
'scale_padding' : { 'Default' : 1.0, # Issue#193
243245
'Validator' : lambda value: _scale_padding_validator(value) },
246+
247+
'ax' : { 'Default' : None,
248+
'Validator' : lambda value: isinstance(value,mpl_axes.Axes) },
249+
250+
'fig' : { 'Default' : None,
251+
'Validator' : lambda value: isinstance(value,mpl_fig.Figure) },
244252
}
245253

246254
_validate_vkwargs_dict(vkwargs)
@@ -266,14 +274,17 @@ def plot( data, **kwargs ):
266274
err = "`addplot` is not supported for `type='" + config['type'] +"'`"
267275
raise ValueError(err)
268276

277+
external_axes_mode = _check_for_external_axes(config)
278+
print('external_axes_mode =',external_axes_mode)
279+
269280
style = config['style']
270281
if isinstance(style,str):
271282
style = config['style'] = _styles._get_mpfstyle(style)
272283

273284
if isinstance(style,dict):
274-
_styles._apply_mpfstyle(style)
285+
if not external_axes_mode: _styles._apply_mpfstyle(style)
275286
else:
276-
raise TypeError('style should be a `dict`; why is it not?')
287+
raise TypeError('style should be a `dict`; why is it not?')
277288

278289
if config['figsize'] is None:
279290
w,h = config['figratio']
@@ -289,15 +300,22 @@ def plot( data, **kwargs ):
289300
else:
290301
fsize = config['figsize']
291302

292-
fig = plt.figure()
303+
if external_axes_mode:
304+
fig = config['fig']
305+
else:
306+
fig = plt.figure()
307+
293308
fig.set_size_inches(fsize)
294309

295310
if config['volume'] and volumes is None:
296311
raise ValueError('Request for volume, but NO volume data.')
297312

298-
panels = _build_panels(fig, config)
299-
300-
volumeAxes = panels.at[config['volume_panel'],'axes'][0] if config['volume'] is True else None
313+
if external_axes_mode:
314+
panels = None
315+
volumeAxes = config['volume']
316+
else:
317+
panels = _build_panels(fig, config)
318+
volumeAxes = panels.at[config['volume_panel'],'axes'][0] if config['volume'] is True else None
301319

302320
fmtstring = _determine_format_string( dates, config['datetime_format'] )
303321

@@ -310,7 +328,10 @@ def plot( data, **kwargs ):
310328
formatter = IntegerIndexDateTimeFormatter(dates, fmtstring)
311329
xdates = np.arange(len(dates))
312330

313-
axA1 = panels.at[config['main_panel'],'axes'][0]
331+
if external_axes_mode:
332+
axA1 = config['ax']
333+
else:
334+
axA1 = panels.at[config['main_panel'],'axes'][0]
314335

315336
# Will have to handle widths config separately for PMOVE types ??
316337
config['_width_config'] = _determine_width_config(xdates, config)
@@ -437,7 +458,11 @@ def plot( data, **kwargs ):
437458
volumeAxes.set_ylim( miny, maxy )
438459

439460
xrotation = config['xrotation']
440-
_set_ticks_on_bottom_panel_only(panels,formatter,rotation=xrotation)
461+
if not external_axes_mode:
462+
_set_ticks_on_bottom_panel_only(panels,formatter,rotation=xrotation)
463+
else:
464+
axA1.tick_params(axis='x',rotation=xrotation)
465+
axA1.xaxis.set_major_formatter(formatter)
441466

442467
addplot = config['addplot']
443468
if addplot is not None and ptype not in VALID_PMOVE_TYPES:
@@ -511,7 +536,7 @@ def plot( data, **kwargs ):
511536
# corners = (minx, miny), (maxx, maxy)
512537
# ax.update_datalim(corners)
513538

514-
if config['fill_between'] is not None:
539+
if config['fill_between'] is not None and not external_axes_mode:
515540
fb = config['fill_between']
516541
panid = config['main_panel']
517542
if isinstance(fb,dict):
@@ -528,10 +553,14 @@ def plot( data, **kwargs ):
528553

529554
# put the primary axis on one side,
530555
# and the twinx() on the "other" side:
531-
for panid,row in panels.iterrows():
532-
ax = row['axes']
533-
y_on_right = style['y_on_right'] if row['y_on_right'] is None else row['y_on_right']
534-
_set_ylabels_side(ax[0],ax[1],y_on_right)
556+
if not external_axes_mode:
557+
for panid,row in panels.iterrows():
558+
ax = row['axes']
559+
y_on_right = style['y_on_right'] if row['y_on_right'] is None else row['y_on_right']
560+
_set_ylabels_side(ax[0],ax[1],y_on_right)
561+
else:
562+
y_on_right = style['y_on_right']
563+
_set_ylabels_side(axA1,None,y_on_right)
535564

536565
# TODO: ================================================================
537566
# TODO: Investigate:
@@ -584,9 +613,13 @@ def plot( data, **kwargs ):
584613
else:
585614
fig.suptitle(config['title'],size='x-large',weight='semibold', va='center')
586615

587-
for panid,row in panels.iterrows():
588-
if not row['used2nd']:
589-
row['axes'][1].set_visible(False)
616+
if not external_axes_mode:
617+
for panid,row in panels.iterrows():
618+
if not row['used2nd']:
619+
row['axes'][1].set_visible(False)
620+
621+
if external_axes_mode:
622+
return None
590623

591624
# Should we create a new kwarg to return a flattened axes list
592625
# versus a list of tuples of primary and secondary axes?
@@ -721,13 +754,15 @@ def _set_ylabels_side(ax_pri,ax_sec,primary_on_right):
721754
if primary_on_right == True:
722755
ax_pri.yaxis.set_label_position('right')
723756
ax_pri.yaxis.tick_right()
724-
ax_sec.yaxis.set_label_position('left')
725-
ax_sec.yaxis.tick_left()
757+
if ax_sec is not None:
758+
ax_sec.yaxis.set_label_position('left')
759+
ax_sec.yaxis.tick_left()
726760
else: # treat non-True as False, whether False, None, or anything else.
727761
ax_pri.yaxis.set_label_position('left')
728762
ax_pri.yaxis.tick_left()
729-
ax_sec.yaxis.set_label_position('right')
730-
ax_sec.yaxis.tick_right()
763+
if ax_sec is not None:
764+
ax_sec.yaxis.set_label_position('right')
765+
ax_sec.yaxis.tick_right()
731766

732767
def _plot_mav(ax,config,xdates,prices,apmav=None,apwidth=None):
733768
style = config['style']
@@ -827,6 +862,9 @@ def _valid_addplot_kwargs():
827862
'ylim' : {'Default' : None,
828863
'Validator' : lambda value: isinstance(value, (list,tuple)) and len(value) == 2
829864
and all([isinstance(v,(int,float)) for v in value])},
865+
866+
'ax' : {'Default' : None,
867+
'Validator' : lambda value: isinstance(value,mpl_axes.Axes) },
830868
}
831869

832870
_validate_vkwargs_dict(vkwargs)

0 commit comments

Comments
 (0)