Skip to content

Commit

Permalink
Merge branch 'external_axes'
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielGoldfarb committed Jul 10, 2020
2 parents be2aa09 + 3116b5b commit 7b7b0cd
Show file tree
Hide file tree
Showing 8 changed files with 2,770 additions and 23 deletions.
633 changes: 633 additions & 0 deletions examples/scratch_pad/dev_ext_axes_nightclouds_issue.ipynb

Large diffs are not rendered by default.

755 changes: 755 additions & 0 deletions examples/scratch_pad/dev_ext_axes_subclass.ipynb

Large diffs are not rendered by default.

1,175 changes: 1,175 additions & 0 deletions examples/scratch_pad/dev_external_axes.ipynb

Large diffs are not rendered by default.

52 changes: 52 additions & 0 deletions src/mplfinance/_arg_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import pandas as pd
import numpy as np
import datetime
from mplfinance._helpers import _list_of_dict
import matplotlib as mpl

def _check_and_prepare_data(data, config):
'''
Expand Down Expand Up @@ -266,3 +268,53 @@ def _scale_padding_validator(value):
else:
raise ValueError('`scale_padding` kwarg must be a number, or dict of (left,right,top,bottom) numbers.')
return False

def _check_for_external_axes(config):
'''
Check that all `fig` and `ax` kwargs are either ALL None,
or ALL are valid instances of Figures/Axes:
An external Axes object can be passed in three places:
- mpf.plot() `ax=` kwarg
- mpf.plot() `volume=` kwarg
- mpf.make_addplot() `ax=` kwarg
ALL three places MUST be an Axes object, OR
ALL three places MUST be None. But it may not be mixed.
'''
ap_axlist = []
addplot = config['addplot']
if addplot is not None:
if isinstance(addplot,dict):
addplot = [addplot,] # make list of dict to be consistent
elif not _list_of_dict(addplot):
raise TypeError('addplot must be `dict`, or `list of dict`, NOT '+str(type(addplot)))
for apd in addplot:
ap_axlist.append(apd['ax'])

if len(ap_axlist) > 0:
if config['ax'] is None:
if not all([ax is None for ax in ap_axlist]):
raise ValueError('make_addplot() `ax` kwarg NOT all None, while plot() `ax` kwarg IS None')
else: # config['ax'] is NOT None:
if not isinstance(config['ax'],mpl.axes.Axes):
raise ValueError('plot() ax kwarg must be of type `matplotlib.axis.Axes`')
if not all([isinstance(ax,mpl.axes.Axes) for ax in ap_axlist]):
raise ValueError('make_addplot() `ax` kwargs must all be of type `matplotlib.axis.Axes`')

# At this point, if we have not raised an exception, then plot(ax=) and make_addplot(ax=)
# are in sync: either they are all None, or they are all of type `matplotlib.axes.Axes`.
# Therefore we only need plot(ax=), i.e. config['ax'], as we check `volume` and `fig`:

if config['ax'] is None:
if isinstance(config['volume'],mpl.axes.Axes):
raise ValueError('`volume` set to external Axes requires all other Axes be external.')
if config['fig'] is not None:
raise ValueError('`fig` kwarg must be None if `ax` kwarg is None.')
else:
if not isinstance(config['volume'],mpl.axes.Axes) and config['volume'] != False:
raise ValueError('`volume` must be of type `matplotlib.axis.Axes`')
if not isinstance(config['fig'],mpl.figure.Figure):
raise ValueError('`fig` kwarg must be of type `matplotlib.figure.Figure`')

external_axes_mode = True if isinstance(config['ax'],mpl.axes.Axes) else False
return external_axes_mode
87 changes: 87 additions & 0 deletions src/mplfinance/_mplrcputils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#!/usr/bin/env python
"""
rcparams utilities
"""

import pandas as pd
import matplotlib.pyplot as plt
import sys

__author__ = "Daniel Goldfarb"
__version__ = "0.1.0"
__license__ = "MIT"

def rcParams_to_df(rcp,name=None):
keys = []
vals = []
for item in rcp:
keys.append(item)
vals.append(rcp[item])
df = pd.DataFrame(vals,index=pd.Index(keys,name='rcParamsKey'))
if name is not None:
df.columns = [name]
else:
df.columns = ['Value']
return df

def compare_styles(s1,s2):
with plt.rc_context():
plt.style.use('default')
plt.style.use(s1)
df1 = rcParams_to_df(plt.rcParams,name=s1)

with plt.rc_context():
plt.style.use('default')
plt.style.use(s2)
df2 = rcParams_to_df(plt.rcParams,name=s2)

df = pd.concat([df1,df2],axis=1)
dif = df[df[s1] != df[s2]].dropna(how='all')
return (dif,df,df1,df2)

def main():
""" Main entry point of the app """
def usage():
print('\n Usage: rcparams <command> <arguments> \n')
print(' Available commands: ')
print(' rcparams find <findstring>')
print(' rcparams compare <style1> <style2>')
print('')
exit(1)
commands = ('find','compare')

if len(sys.argv) < 3 :
print('\n Too few arguments!')
usage()

command = sys.argv[1]
if command not in commands:
print('\n Unrecognized command \"'+command+'\"')
usage()

if command == 'find':
findstr = sys.argv[2]
df = rcParams_to_df(plt.rcParams)
if findstr == '--all':
for key in df.index:
print(key+':',df.loc[key,'Value'])
else:
print(df[df.index.str.contains(findstr)])

elif command == 'compare':
if len(sys.argv) < 4 :
print('\n Need two styles to compare!')
usage()
style1 = sys.argv[2]
style2 = sys.argv[3]
dif,df,df1,df2 = compare_styles(style1,style2)
print('\n==== dif ====\n',dif)

else:
print('\n Unrecognized command \"'+command+'\"')
usage()


if __name__ == "__main__":
""" This is executed when run from the command line """
main()
7 changes: 7 additions & 0 deletions src/mplfinance/_panels.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,13 @@ def _build_panels( figure, config ):
#print('panels=')
#print(panels)

# TODO: Throughout this section, right_pad is intentionally *less*
# than left_pad. This assumes that the y-axis labels are on
# the left, which is true for many mpf_styles, but *not* all.
# Ideally need to determine which side has the axis labels.
# And keep in mind, if secondary_y is in effect, then both
# sides can have axis labels.

left_pad = 0.18
right_pad = 0.10
top_pad = 0.12
Expand Down
2 changes: 1 addition & 1 deletion src/mplfinance/_version.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

version_info = (0, 12, 6, 'alpha', 4)
version_info = (0, 12, 6, 'alpha', 5)

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

Expand Down
82 changes: 60 additions & 22 deletions src/mplfinance/plotting.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import matplotlib.dates as mdates
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import matplotlib.axes as mpl_axes
import matplotlib.figure as mpl_fig
import pandas as pd
import numpy as np
import copy
Expand Down Expand Up @@ -33,7 +35,7 @@
from mplfinance._arg_validators import _hlines_validator, _vlines_validator
from mplfinance._arg_validators import _alines_validator, _tlines_validator
from mplfinance._arg_validators import _scale_padding_validator
from mplfinance._arg_validators import _valid_panel_id
from mplfinance._arg_validators import _valid_panel_id, _check_for_external_axes

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

'volume' : { 'Default' : False,
'Validator' : lambda value: isinstance(value,bool) },
'Validator' : lambda value: isinstance(value,bool) or isinstance(value,mpl_axes.Axes) },

'mav' : { 'Default' : None,
'Validator' : _mav_validator },
Expand Down Expand Up @@ -241,6 +243,12 @@ def _valid_plot_kwargs():

'scale_padding' : { 'Default' : 1.0, # Issue#193
'Validator' : lambda value: _scale_padding_validator(value) },

'ax' : { 'Default' : None,
'Validator' : lambda value: isinstance(value,mpl_axes.Axes) },

'fig' : { 'Default' : None,
'Validator' : lambda value: isinstance(value,mpl_fig.Figure) },
}

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

external_axes_mode = _check_for_external_axes(config)
print('external_axes_mode =',external_axes_mode)

style = config['style']
if isinstance(style,str):
style = config['style'] = _styles._get_mpfstyle(style)

if isinstance(style,dict):
_styles._apply_mpfstyle(style)
if not external_axes_mode: _styles._apply_mpfstyle(style)
else:
raise TypeError('style should be a `dict`; why is it not?')
raise TypeError('style should be a `dict`; why is it not?')

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

fig = plt.figure()
if external_axes_mode:
fig = config['fig']
else:
fig = plt.figure()

fig.set_size_inches(fsize)

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

panels = _build_panels(fig, config)

volumeAxes = panels.at[config['volume_panel'],'axes'][0] if config['volume'] is True else None
if external_axes_mode:
panels = None
volumeAxes = config['volume']
else:
panels = _build_panels(fig, config)
volumeAxes = panels.at[config['volume_panel'],'axes'][0] if config['volume'] is True else None

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

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

axA1 = panels.at[config['main_panel'],'axes'][0]
if external_axes_mode:
axA1 = config['ax']
else:
axA1 = panels.at[config['main_panel'],'axes'][0]

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

xrotation = config['xrotation']
_set_ticks_on_bottom_panel_only(panels,formatter,rotation=xrotation)
if not external_axes_mode:
_set_ticks_on_bottom_panel_only(panels,formatter,rotation=xrotation)
else:
axA1.tick_params(axis='x',rotation=xrotation)
axA1.xaxis.set_major_formatter(formatter)

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

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

# put the primary axis on one side,
# and the twinx() on the "other" side:
for panid,row in panels.iterrows():
ax = row['axes']
y_on_right = style['y_on_right'] if row['y_on_right'] is None else row['y_on_right']
_set_ylabels_side(ax[0],ax[1],y_on_right)
if not external_axes_mode:
for panid,row in panels.iterrows():
ax = row['axes']
y_on_right = style['y_on_right'] if row['y_on_right'] is None else row['y_on_right']
_set_ylabels_side(ax[0],ax[1],y_on_right)
else:
y_on_right = style['y_on_right']
_set_ylabels_side(axA1,None,y_on_right)

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

for panid,row in panels.iterrows():
if not row['used2nd']:
row['axes'][1].set_visible(False)
if not external_axes_mode:
for panid,row in panels.iterrows():
if not row['used2nd']:
row['axes'][1].set_visible(False)

if external_axes_mode:
return None

# Should we create a new kwarg to return a flattened axes list
# versus a list of tuples of primary and secondary axes?
Expand Down Expand Up @@ -721,13 +754,15 @@ def _set_ylabels_side(ax_pri,ax_sec,primary_on_right):
if primary_on_right == True:
ax_pri.yaxis.set_label_position('right')
ax_pri.yaxis.tick_right()
ax_sec.yaxis.set_label_position('left')
ax_sec.yaxis.tick_left()
if ax_sec is not None:
ax_sec.yaxis.set_label_position('left')
ax_sec.yaxis.tick_left()
else: # treat non-True as False, whether False, None, or anything else.
ax_pri.yaxis.set_label_position('left')
ax_pri.yaxis.tick_left()
ax_sec.yaxis.set_label_position('right')
ax_sec.yaxis.tick_right()
if ax_sec is not None:
ax_sec.yaxis.set_label_position('right')
ax_sec.yaxis.tick_right()

def _plot_mav(ax,config,xdates,prices,apmav=None,apwidth=None):
style = config['style']
Expand Down Expand Up @@ -827,6 +862,9 @@ def _valid_addplot_kwargs():
'ylim' : {'Default' : None,
'Validator' : lambda value: isinstance(value, (list,tuple)) and len(value) == 2
and all([isinstance(v,(int,float)) for v in value])},

'ax' : {'Default' : None,
'Validator' : lambda value: isinstance(value,mpl_axes.Axes) },
}

_validate_vkwargs_dict(vkwargs)
Expand Down

0 comments on commit 7b7b0cd

Please sign in to comment.