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
def plot( data, **kwargs ): """ Given a Pandas DataFrame containing columns Open,High,Low,Close and optionally Volume with a DatetimeIndex, plot the data. Available plots include ohlc bars, candlestick, and line plots. Also provide visually analysis in the form of common technical studies, such as: moving averages, renko, etc. Also provide ability to plot trading signals, and/or addtional user-defined data. """ config = _process_kwargs(kwargs, _valid_plot_kwargs()) # translate alias types: config['type'] = _get_valid_plot_types(config['type']) dates,opens,highs,lows,closes,volumes = _check_and_prepare_data(data, config) if config['type'] in VALID_PMOVE_TYPES and config['addplot'] is not None: err = "`addplot` is not supported for `type='" + config['type'] +"'`" raise ValueError(err) external_axes_mode = _check_for_external_axes(config) if external_axes_mode: if config['figscale'] is not None: warnings.warn('\n\n ================================================================= '+ '\n\n WARNING: `figscale` has NO effect in External Axes Mode.'+ '\n\n ================================================================ ', category=UserWarning) if config['figratio'] is not None: warnings.warn('\n\n ================================================================= '+ '\n\n WARNING: `figratio` has NO effect in External Axes Mode.'+ '\n\n ================================================================ ', category=UserWarning) if config['figsize'] is not None: warnings.warn('\n\n ================================================================= '+ '\n\n WARNING: `figsize` has NO effect in External Axes Mode.'+ '\n\n ================================================================ ', category=UserWarning) else: if config['figscale'] is None: config['figscale'] = 1.0 if config['figratio'] is None: config['figratio'] = DEFAULT_FIGRATIO style = config['style'] if external_axes_mode and hasattr(config['ax'],'mpfstyle') and style is None: style = config['ax'].mpfstyle elif style is None: style = 'default' if isinstance(style,str): style = _styles._get_mpfstyle(style) config['style'] = style if isinstance(style,dict): if not external_axes_mode: _styles._apply_mpfstyle(style) else: raise TypeError('style should be a `dict`; why is it not?') # ---------------------------------------------------------------------- # TODO: Add some warnings, or raise an exception, if external_axes_mode # and user is trying to figscale, figratio, or figsize. # ---------------------------------------------------------------------- if not external_axes_mode: fig = plt.figure() _adjust_figsize(fig,config) else: fig = None if config['volume'] and volumes is None: raise ValueError('Request for volume, but NO volume data.') if external_axes_mode: panels = None if config['volume']: volumeAxes = config['volume'] volumeAxes.set_axisbelow(config['saxbelow']) 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'] ) ptype = config['type'] if config['show_nontrading']: formatter = mdates.DateFormatter(fmtstring) xdates = dates else: formatter = IntegerIndexDateTimeFormatter(dates, fmtstring) xdates = np.arange(len(dates)) if external_axes_mode: axA1 = config['ax'] axA1.set_axisbelow(config['saxbelow']) 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) rwc = config['return_width_config'] if isinstance(rwc,dict) and len(rwc)==0: config['return_width_config'].update(config['_width_config']) collections = None if ptype == 'line': lw = config['_width_config']['line_width'] axA1.plot(xdates, closes, color=config['linecolor'], linewidth=lw) else: collections =_construct_mpf_collections(ptype,dates,xdates,opens,highs,lows,closes,volumes,config,style) if ptype in VALID_PMOVE_TYPES: collections, new_dates, volumes, brick_values, size = collections formatter = IntegerIndexDateTimeFormatter(new_dates, fmtstring) xdates = np.arange(len(new_dates)) if collections is not None: for collection in collections: axA1.add_collection(collection) if ptype in VALID_PMOVE_TYPES: mavprices = _plot_mav(axA1,config,xdates,brick_values) else: mavprices = _plot_mav(axA1,config,xdates,closes) avg_dist_between_points = (xdates[-1] - xdates[0]) / float(len(xdates)) if not config['tight_layout']: minx = xdates[0] - avg_dist_between_points maxx = xdates[-1] + avg_dist_between_points else: minx = xdates[0] - (0.45 * avg_dist_between_points) maxx = xdates[-1] + (0.45 * avg_dist_between_points) if len(xdates) == 1: # kludge special case minx = minx - 0.75 maxx = maxx + 0.75 if ptype not in VALID_PMOVE_TYPES: _lows = lows _highs = highs else: _lows = brick_values _highs = [brick+size for brick in brick_values] miny = np.nanmin(_lows) maxy = np.nanmax(_highs) if config['ylim'] is not None: axA1.set_ylim(config['ylim'][0], config['ylim'][1]) elif config['tight_layout']: ydelta = 0.01 * (maxy-miny) axA1.set_ylim(miny-ydelta,maxy+ydelta) if config['xlim'] is not None: axA1.set_xlim(config['xlim'][0], config['xlim'][1]) elif config['tight_layout']: axA1.set_xlim(minx,maxx) if (config['ylim'] is None and config['xlim'] is None and not config['tight_layout']): corners = (minx, miny), (maxx, maxy) axA1.update_datalim(corners) if config['return_calculated_values'] is not None: retdict = config['return_calculated_values'] if ptype in VALID_PMOVE_TYPES: prekey = ptype retdict[prekey+'_bricks'] = brick_values retdict[prekey+'_dates'] = mdates.num2date(new_dates) retdict[prekey+'_size'] = size if config['volume']: retdict[prekey+'_volumes'] = volumes if config['mav'] is not None: mav = config['mav'] if len(mav) != len(mavprices): warnings.warn('len(mav)='+str(len(mav))+' BUT len(mavprices)='+str(len(mavprices))) else: for jj in range(0,len(mav)): retdict['mav' + str(mav[jj])] = mavprices[jj] retdict['minx'] = minx retdict['maxx'] = maxx retdict['miny'] = miny retdict['maxy'] = maxy # Note: these are NOT mutually exclusive, so the order of this # if/elif is important: VALID_PMOVE_TYPES must be first. if ptype in VALID_PMOVE_TYPES: dtix = pd.DatetimeIndex([dt for dt in mdates.num2date(new_dates)]) elif not config['show_nontrading']: dtix = data.index else: dtix = None line_collections = [] line_collections.append(_construct_aline_collections(config['alines'], dtix)) line_collections.append(_construct_hline_collections(config['hlines'], minx, maxx)) line_collections.append(_construct_vline_collections(config['vlines'], dtix, miny, maxy)) tlines = config['tlines'] if isinstance(tlines,(list,tuple)) and all([isinstance(item,dict) for item in tlines]): pass else: tlines = [tlines,] for tline_item in tlines: line_collections.append(_construct_tline_collections(tline_item, dtix, dates, opens, highs, lows, closes)) for collection in line_collections: if collection is not None: axA1.add_collection(collection) datalen = len(xdates) if config['volume']: vup,vdown = style['marketcolors']['volume'].values() #-- print('vup,vdown=',vup,vdown) vcolors = _updown_colors(vup, vdown, opens, closes, use_prev_close=style['marketcolors']['vcdopcod']) #-- print('len(vcolors),len(opens),len(closes)=',len(vcolors),len(opens),len(closes)) #-- print('vcolors=',vcolors) w = config['_width_config']['volume_width'] lw = config['_width_config']['volume_linewidth'] adjc = _adjust_color_brightness(vcolors,0.90) volumeAxes.bar(xdates,volumes,width=w,linewidth=lw,color=vcolors,ec=adjc) vymin = 0.3 * np.nanmin(volumes) vymax = 1.1 * np.nanmax(volumes) volumeAxes.set_ylim(vymin,vymax) xrotation = config['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: # NOTE: If in external_axes_mode, then all code relating # to panels and secondary_y becomes irrrelevant. # If the user wants something on a secondary_y then user should # determine that externally, and pass in the appropriate axes. if not external_axes_mode: # Calculate the Order of Magnitude Range ('mag') # If addplot['secondary_y'] == 'auto', then: If the addplot['data'] # is out of the Order of Magnitude Range, then use secondary_y. lo = math.log(max(math.fabs(np.nanmin(lows)),1e-7),10) - 0.5 hi = math.log(max(math.fabs(np.nanmax(highs)),1e-7),10) + 0.5 panels['mag'] = [None]*len(panels) # create 'mag'nitude column panels.at[config['main_panel'],'mag'] = {'lo':lo,'hi':hi} # update main panel magnitude range if config['volume']: lo = math.log(max(math.fabs(np.nanmin(volumes)),1e-7),10) - 0.5 hi = math.log(max(math.fabs(np.nanmax(volumes)),1e-7),10) + 0.5 panels.at[config['volume_panel'],'mag'] = {'lo':lo,'hi':hi} 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 apdict in addplot: panid = apdict['panel'] if not external_axes_mode: if panid == 'main' : panid = 0 # for backwards compatibility elif panid == 'lower': panid = 1 # for backwards compatibility if apdict['y_on_right'] is not None: panels.at[panid,'y_on_right'] = apdict['y_on_right'] aptype = apdict['type'] if aptype == 'ohlc' or aptype == 'candle': ax = _addplot_collections(panid,panels,apdict,xdates,config) _addplot_apply_supplements(ax,apdict) else: apdata = apdict['data'] if isinstance(apdata,list) and not isinstance(apdata[0],(float,int)): raise TypeError('apdata is list but NOT of float or int') if isinstance(apdata,pd.DataFrame): havedf = True else: havedf = False # must be a single series or array apdata = [apdata,] # make it iterable for column in apdata: ydata = apdata.loc[:,column] if havedf else column ax = _addplot_columns(panid,panels,ydata,apdict,xdates,config) _addplot_apply_supplements(ax,apdict) # fill_between is NOT supported for external_axes_mode # (caller can easily call ax.fill_between() themselves). if config['fill_between'] is not None and not external_axes_mode: fb = config['fill_between'] panid = config['main_panel'] if isinstance(fb,dict): if 'x' in fb: raise ValueError('fill_between dict may not contain `x`') if 'panel' in fb: panid = fb['panel'] del fb['panel'] else: fb = dict(y1=fb) fb['x'] = xdates ax = panels.at[panid,'axes'][0] ax.fill_between(**fb) # put the primary axis on one side, # and the twinx() on the "other" side: 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: # TODO: =========== # TODO: It appears to me that there may be some or significant overlap # TODO: between what the following functions actually do: # TODO: At the very least, all four of them appear to communicate # TODO: to matplotlib that the xaxis should be treated as dates: # TODO: -> 'ax.autoscale_view()' # TODO: -> 'ax.xaxis_dates()' # TODO: -> 'plt.autofmt_xdates()' # TODO: -> 'fig.autofmt_xdate()' # TODO: ================================================================ #if config['autofmt_xdate']: #print('CALLING fig.autofmt_xdate()') #fig.autofmt_xdate() axA1.autoscale_view() # Is this really necessary?? # It appears to me, based on experience coding types 'ohlc' and 'candle' # for `addplot`, that this IS necessary when the only thing done to the # the axes is .add_collection(). (However, if ax.plot() .scatter() or # .bar() was called, then possibly this is not necessary; not entirely # sure, but it definitely was necessary to get 'ohlc' and 'candle' # working in `addplot`). axA1.set_ylabel(config['ylabel']) if config['volume']: if external_axes_mode: volumeAxes.tick_params(axis='x',rotation=xrotation) volumeAxes.xaxis.set_major_formatter(formatter) vxp = config['volume_exponent'] if vxp == 'legacy': volumeAxes.figure.canvas.draw() # This is needed to calculate offset offset = volumeAxes.yaxis.get_major_formatter().get_offset() if len(offset) > 0: offset = (' x '+offset) elif isinstance(vxp,int) and vxp > 0: volumeAxes.ticklabel_format(useOffset=False,scilimits=(vxp,vxp),axis='y') offset = ' $10^{'+str(vxp)+'}$' elif isinstance(vxp,int) and vxp == 0: volumeAxes.ticklabel_format(useOffset=False,style='plain',axis='y') offset = '' else: offset = '' scilims = plt.rcParams['axes.formatter.limits'] if scilims[0] < scilims[1]: for power in (5,4,3,2,1): xp = scilims[1]*power if vymax >= 10.**xp: volumeAxes.ticklabel_format(useOffset=False,scilimits=(xp,xp),axis='y') offset = ' $10^{'+str(xp)+'}$' break elif scilims[0] == scilims[1] and scilims[1] != 0: volumeAxes.ticklabel_format(useOffset=False,scilimits=scilims,axis='y') offset = ' $10^'+str(scilims[1])+'$' volumeAxes.yaxis.offsetText.set_visible(False) if config['ylabel_lower'] is None: vol_label = 'Volume'+offset else: if len(offset) > 0: offset = '\n'+offset vol_label = config['ylabel_lower'] + offset volumeAxes.set_ylabel(vol_label) if config['title'] is not None: if config['tight_layout']: # IMPORTANT: 0.89 is based on the top of the top panel # being at 0.18+0.7 = 0.88. See _panels.py # If the value changes there, then it needs to change here. title_kwargs = dict(size='x-large',weight='semibold', va='bottom', y=0.89) else: title_kwargs = dict(size='x-large',weight='semibold', va='center') if isinstance(config['title'],dict): title_dict = config['title'] if 'title' not in title_dict: raise ValueError('Must have "title" entry in title dict') else: title = title_dict['title'] del title_dict['title'] title_kwargs.update(title_dict) # allows override default values set by mplfinance above else: title = config['title'] # config['title'] is a string fig.suptitle(title,**title_kwargs) if config['axtitle'] is not None: axA1.set_title(config['axtitle']) 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? # For now, for backwards compatibility, we flatten axes list: axlist = [ax for axes in panels['axes'] for ax in axes] if config['axisoff']: for ax in axlist: ax.set_axis_off() if config['savefig'] is not None: save = config['savefig'] if isinstance(save,dict): if config['tight_layout'] and 'bbox_inches' not in save: plt.savefig(**save,bbox_inches='tight') else: plt.savefig(**save) else: if config['tight_layout']: plt.savefig(save,bbox_inches='tight') else: plt.savefig(save) if config['closefig']: # True or 'auto' plt.close(fig) elif not config['returnfig']: plt.show(block=config['block']) # https://stackoverflow.com/a/13361748/1639359 if config['closefig'] == True or (config['block'] and config['closefig']): plt.close(fig) if config['returnfig']: if config['closefig'] == True: plt.close(fig) return (fig, axlist)
def plot(data, **kwargs): """ Given a Pandas DataFrame containing columns Open,High,Low,Close and optionally Volume with a DatetimeIndex, plot the data. Available plots include ohlc bars, candlestick, and line plots. Also provide visually analysis in the form of common technical studies, such as: moving averages, renko, etc. Also provide ability to plot trading signals, and/or addtional user-defined data. """ config = _process_kwargs(kwargs, _valid_plot_kwargs()) dates, opens, highs, lows, closes, volumes = _check_and_prepare_data( data, config) if config['type'] in VALID_PMOVE_TYPES and config['addplot'] is not None: err = "`addplot` is not supported for `type='" + config['type'] + "'`" raise ValueError(err) style = config['style'] if isinstance(style, str): style = _styles._get_mpfstyle(style) if isinstance(style, dict): _styles._apply_mpfstyle(style) if config['figsize'] is None: w, h = config['figratio'] r = float(w) / float(h) if r < 0.20 or r > 5.0: raise ValueError( '"figratio" (aspect ratio) must be between 0.20 and 5.0 (but is ' + str(r) + ')') default_scale = DEFAULT_FIGRATIO[1] / h h *= default_scale w *= default_scale base = (w, h) figscale = config['figscale'] fsize = [d * figscale for d in base] else: fsize = config['figsize'] 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 fmtstring = _determine_format_string(dates, config['datetime_format']) ptype = config['type'] if config['show_nontrading']: formatter = mdates.DateFormatter(fmtstring) xdates = dates else: formatter = IntegerIndexDateTimeFormatter(dates, fmtstring) xdates = np.arange(len(dates)) 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) rwc = config['return_width_config'] if isinstance(rwc, dict) and len(rwc) == 0: config['return_width_config'].update(config['_width_config']) collections = None if ptype == 'line': lw = config['_width_config']['line_width'] axA1.plot(xdates, closes, color=config['linecolor'], linewidth=lw) else: collections = _construct_mpf_collections(ptype, dates, xdates, opens, highs, lows, closes, volumes, config, style) if ptype in VALID_PMOVE_TYPES: collections, new_dates, volumes, brick_values, size = collections formatter = IntegerIndexDateTimeFormatter(new_dates, fmtstring) xdates = np.arange(len(new_dates)) if collections is not None: for collection in collections: axA1.add_collection(collection) mavgs = config['mav'] if mavgs is not None: if isinstance(mavgs, int): mavgs = mavgs, # convert to tuple if len(mavgs) > 7: mavgs = mavgs[0:7] # take at most 7 if style['mavcolors'] is not None: mavc = cycle(style['mavcolors']) else: mavc = None # Get rcParams['lines.linewidth'] and scale it # according to the deinsity of data?? for mav in mavgs: if ptype in VALID_PMOVE_TYPES: mavprices = pd.Series(brick_values).rolling(mav).mean().values else: mavprices = pd.Series(closes).rolling(mav).mean().values lw = config['_width_config']['line_width'] if mavc: axA1.plot(xdates, mavprices, linewidth=lw, color=next(mavc)) else: axA1.plot(xdates, mavprices, linewidth=lw) avg_dist_between_points = (xdates[-1] - xdates[0]) / float(len(xdates)) if not config['tight_layout']: #print('plot: xdates[-1]=',xdates[-1]) #print('plot: xdates[ 0]=',xdates[ 0]) #print('plot: len(xdates)=',len(xdates)) #print('plot: avg_dist_between_points =',avg_dist_between_points) minx = xdates[0] - avg_dist_between_points maxx = xdates[-1] + avg_dist_between_points else: minx = xdates[0] - (0.45 * avg_dist_between_points) maxx = xdates[-1] + (0.45 * avg_dist_between_points) if len(xdates) == 1: # kludge special case minx = minx - 0.75 maxx = maxx + 0.75 if ptype not in VALID_PMOVE_TYPES: _lows = lows _highs = highs else: _lows = brick_values _highs = [brick + size for brick in brick_values] miny = np.nanmin(_lows) maxy = np.nanmax(_highs) #if len(xdates) > 1: # stdy = (stat.stdev(_lows) + stat.stdev(_highs)) / 2.0 #else: # kludge special case # stdy = 0.02 * math.fabs(maxy - miny) # print('minx,miny,maxx,maxy,stdy=',minx,miny,maxx,maxy,stdy) if config['set_ylim'] is not None: axA1.set_ylim(config['set_ylim'][0], config['set_ylim'][1]) elif config['tight_layout']: axA1.set_xlim(minx, maxx) ydelta = 0.01 * (maxy - miny) axA1.set_ylim(miny - ydelta, maxy + ydelta) else: corners = (minx, miny), (maxx, maxy) axA1.update_datalim(corners) if config['return_calculated_values'] is not None: retdict = config['return_calculated_values'] if ptype in VALID_PMOVE_TYPES: prekey = ptype retdict[prekey + '_bricks'] = brick_values retdict[prekey + '_dates'] = mdates.num2date(new_dates) retdict[prekey + '_size'] = size if config['volume']: retdict[prekey + '_volumes'] = volumes if mavgs is not None: for i in range(0, len(mavgs)): retdict['mav' + str(mavgs[i])] = mavprices retdict['minx'] = minx retdict['maxx'] = maxx retdict['miny'] = miny retdict['maxy'] = maxy # Note: these are NOT mutually exclusive, so the order of this # if/elif is important: VALID_PMOVE_TYPES must be first. if ptype in VALID_PMOVE_TYPES: dtix = pd.DatetimeIndex([dt for dt in mdates.num2date(new_dates)]) elif not config['show_nontrading']: dtix = data.index else: dtix = None line_collections = [] line_collections.append( _construct_aline_collections(config['alines'], dtix)) line_collections.append( _construct_hline_collections(config['hlines'], minx, maxx)) line_collections.append( _construct_vline_collections(config['vlines'], dtix, miny, maxy)) tlines = config['tlines'] if isinstance(tlines, (list, tuple)) and all( [isinstance(item, dict) for item in tlines]): pass else: tlines = [ tlines, ] for tline_item in tlines: line_collections.append( _construct_tline_collections(tline_item, dtix, dates, opens, highs, lows, closes)) for collection in line_collections: if collection is not None: axA1.add_collection(collection) datalen = len(xdates) if config['volume']: vup, vdown = style['marketcolors']['volume'].values() #-- print('vup,vdown=',vup,vdown) vcolors = _updown_colors( vup, vdown, opens, closes, use_prev_close=style['marketcolors']['vcdopcod']) #-- print('len(vcolors),len(opens),len(closes)=',len(vcolors),len(opens),len(closes)) #-- print('vcolors=',vcolors) w = config['_width_config']['volume_width'] lw = config['_width_config']['volume_linewidth'] adjc = _adjust_color_brightness(vcolors, 0.90) volumeAxes.bar(xdates, volumes, width=w, linewidth=lw, color=vcolors, ec=adjc) miny = 0.3 * np.nanmin(volumes) maxy = 1.1 * np.nanmax(volumes) volumeAxes.set_ylim(miny, maxy) xrotation = config['xrotation'] _set_ticks_on_bottom_panel_only(panels, formatter, rotation=xrotation) addplot = config['addplot'] if addplot is not None and ptype not in VALID_PMOVE_TYPES: # Calculate the Order of Magnitude Range ('mag') # If addplot['secondary_y'] == 'auto', then: If the addplot['data'] # is out of the Order of Magnitude Range, then use secondary_y. # Calculate omrange for Main panel, and for Lower (volume) panel: lo = math.log(max(math.fabs(np.nanmin(lows)), 1e-7), 10) - 0.5 hi = math.log(max(math.fabs(np.nanmax(highs)), 1e-7), 10) + 0.5 panels['mag'] = [None] * len(panels) # create 'mag' column panels.at[config['main_panel'], 'mag'] = { 'lo': lo, 'hi': hi } # update main panel magnitude range if config['volume']: lo = math.log(max(math.fabs(np.nanmin(volumes)), 1e-7), 10) - 0.5 hi = math.log(max(math.fabs(np.nanmax(volumes)), 1e-7), 10) + 0.5 panels.at[config['volume_panel'], 'mag'] = {'lo': lo, 'hi': hi} 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 apdict in addplot: apdata = apdict['data'] if isinstance(apdata, list) and not isinstance(apdata[0], (float, int)): raise TypeError('apdata is list but NOT of float or int') if isinstance(apdata, pd.DataFrame): havedf = True else: havedf = False # must be a single series or array apdata = [ apdata, ] # make it iterable for column in apdata: if havedf: ydata = apdata.loc[:, column] else: ydata = column yd = [y for y in ydata if not math.isnan(y)] ymhi = math.log(max(math.fabs(np.nanmax(yd)), 1e-7), 10) ymlo = math.log(max(math.fabs(np.nanmin(yd)), 1e-7), 10) secondary_y = False panid = apdict['panel'] if panid == 'main': panid = 0 # for backwards compatibility elif panid == 'lower': panid = 1 # for backwards compatibility if apdict['secondary_y'] == 'auto': # If mag(nitude) for this panel is not yet set, then set it # here, as this is the first ydata to be plotted on this panel: # i.e. consider this to be the 'primary' axis for this panel. p = panid, 'mag' if panels.at[p] is None: panels.at[p] = {'lo': ymlo, 'hi': ymhi} elif ymlo < panels.at[p]['lo'] or ymhi > panels.at[p]['hi']: secondary_y = True #if secondary_y: # print('auto says USE secondary_y ... for panel',panid) #else: # print('auto says do NOT use secondary_y ... for panel',panid) else: secondary_y = apdict['secondary_y'] #print("apdict['secondary_y'] says secondary_y is",secondary_y) if secondary_y: ax = panels.at[panid, 'axes'][1] panels.at[panid, 'used2nd'] = True else: ax = panels.at[panid, 'axes'][0] if (apdict["ylabel"] is not None): ax.set_ylabel(apdict["ylabel"]) aptype = apdict['type'] if aptype == 'scatter': size = apdict['markersize'] mark = apdict['marker'] color = apdict['color'] if isinstance(mark, (list, tuple, np.ndarray)): _mscatter(xdates, ydata, ax=ax, m=mark, s=size, color=color) else: ax.scatter(xdates, ydata, s=size, marker=mark, color=color) elif aptype == 'bar': width = apdict['width'] bottom = apdict['bottom'] color = apdict['color'] alpha = apdict['alpha'] ax.bar(xdates, ydata, width=width, bottom=bottom, color=color, alpha=alpha) elif aptype == 'line': ls = apdict['linestyle'] color = apdict['color'] ax.plot(xdates, ydata, linestyle=ls, color=color) #elif aptype == 'ohlc' or aptype == 'candle': # This won't work as is, because here we are looping through one column at a time # and mpf_collections needs ohlc columns: # collections =_construct_mpf_collections(aptype,dates,xdates,opens,highs,lows,closes,volumes,config,style) # if len(collections) == 1: collections = [collections] # for collection in collections: # ax.add_collection(collection) else: raise ValueError('addplot type "' + str(aptype) + '" NOT yet supported.') if config['fill_between'] is not None: fb = config['fill_between'] panid = config['main_panel'] if isinstance(fb, dict): if 'x' in fb: raise ValueError('fill_between dict may not contain `x`') if 'panel' in fb: panid = fb['panel'] del fb['panel'] else: fb = dict(y1=fb) fb['x'] = xdates ax = panels.at[panid, 'axes'][0] ax.fill_between(**fb) if config['set_ylim_panelB'] is not None: miny = config['set_ylim_panelB'][0] maxy = config['set_ylim_panelB'][1] panels.at[1, 'axes'][0].set_ylim(miny, maxy) # put the twinx() on the "other" side: if style['y_on_right']: for ax in panels['axes'].values: ax[0].yaxis.set_label_position('right') ax[0].yaxis.tick_right() ax[1].yaxis.set_label_position('left') ax[1].yaxis.tick_left() else: for ax in panels['axes'].values: ax[0].yaxis.set_label_position('left') ax[0].yaxis.tick_left() ax[1].yaxis.set_label_position('right') ax[1].yaxis.tick_right() # TODO: ================================================================ # TODO: Investigate: # TODO: =========== # TODO: It appears to me that there may be some or significant overlap # TODO: between what the following functions actually do: # TODO: At the very least, all four of them appear to communicate # TODO: to matplotlib that the xaxis should be treated as dates: # TODO: -> 'ax.autoscale_view()' # TODO: -> 'ax.xaxis_dates()' # TODO: -> 'plt.autofmt_xdates()' # TODO: -> 'fig.autofmt_xdate()' # TODO: ================================================================ #if config['autofmt_xdate']: #print('CALLING fig.autofmt_xdate()') #fig.autofmt_xdate() axA1.autoscale_view() # Is this really necessary?? axA1.set_ylabel(config['ylabel']) if config['volume']: volumeAxes.figure.canvas.draw() # This is needed to calculate offset offset = volumeAxes.yaxis.get_major_formatter().get_offset() volumeAxes.yaxis.offsetText.set_visible(False) if len(offset) > 0: offset = (' x ' + offset) if config['ylabel_lower'] is None: vol_label = 'Volume' + offset else: if len(offset) > 0: offset = '\n' + offset vol_label = config['ylabel_lower'] + offset volumeAxes.set_ylabel(vol_label) if config['title'] is not None: if config['tight_layout']: # IMPORTANT: 0.89 is based on the top of the top panel # being at 0.18+0.7 = 0.88. See _panels.py # If the value changes there, then it needs to change here. fig.suptitle(config['title'], size='x-large', weight='semibold', va='bottom', y=0.89) 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) # Should we create a new kwarg to return a flattened axes list # versus a list of tuples of primary and secondary axes? # For now, for backwards compatibility, we flatten axes list: axlist = [ax for axes in panels['axes'] for ax in axes] if config['axisoff']: for ax in axlist: ax.set_xlim(xdates[0], xdates[-1]) ax.set_axis_off() if config['savefig'] is not None: save = config['savefig'] if isinstance(save, dict): # Expand to fill chart if axisoff if config['axisoff'] and 'bbox_inches' not in save: plt.savefig(**save, bbox_inches='tight') else: plt.savefig(**save) else: if config['axisoff']: plt.savefig(save, bbox_inches='tight') else: plt.savefig(save) if config['closefig']: # True or 'auto' plt.close(fig) elif not config['returnfig']: plt.show(block=config['block'] ) # https://stackoverflow.com/a/13361748/1639359 if config['closefig'] == True or (config['block'] and config['closefig']): plt.close(fig) if config['returnfig']: if config['closefig'] == True: plt.close(fig) return (fig, axlist)
lo = math.log(max(math.fabs(np.nanmin(lows)),1e-7),10) - 0.5 hi = math.log(max(math.fabs(np.nanmax(highs)),1e-7),10) + 0.5 panels['mag'] = [None]*len(panels) # create 'mag'nitude column panels.at[config['main_panel'],'mag'] = {'lo':lo,'hi':hi} # update main panel magnitude range if config['volume']: lo = math.log(max(math.fabs(np.nanmin(volumes)),1e-7),10) - 0.5 hi = math.log(max(math.fabs(np.nanmax(volumes)),1e-7),10) + 0.5 panels.at[config['volume_panel'],'mag'] = {'lo':lo,'hi':hi} 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 apdict in addplot: panid = apdict['panel'] if panid == 'main' : panid = 0 # for backwards compatibility elif panid == 'lower': panid = 1 # for backwards compatibility if apdict['y_on_right'] is not None: panels.at[panid,'y_on_right'] = apdict['y_on_right'] aptype = apdict['type'] if aptype == 'ohlc' or aptype == 'candle': ax = _addplot_collections(panid,panels,apdict,xdates,config) if (apdict['ylabel'] is not None):
def _build_panels( figure, config ): """ Create and return a DataFrame containing panel information and Axes objects for each panel, etc. We allow up to 10 panels, identified by their panel id (panid) which is an integer 0 through 9. Parameters ---------- figure : pyplot.Figure figure on which to create the Axes for the panels config : dict config dict from `mplfinance.plot()` Config ------ The following items are used from `config`: num_panels : integer (0-9) or None number of panels to create addplot : dict or None value for the `addplot=` kwarg passed into `mplfinance.plot()` volume_panel : integer (0-9) or None panel id (0-number_of_panels) main_panel : integer (0-9) or None panel id (0-number_of_panels) panel_ratios : sequence or None sequence of relative sizes for the panels; NOTE: If len(panel_ratios) == number of panels (regardless of whether number of panels was specified or inferred), then panel ratios are the relative sizes of each panel, in panel id order, 0 through N (where N = number of panels). If len(panel_ratios) != number of panels, then len(panel_ratios) must equal 2, and panel_ratios[0] is the relative size for the 'main' panel, and panel_ratios[1] is the relative size for all other panels. If the number of panels == 1, the panel_ratios is ignored. Returns ---------- panels : pandas.DataFrame dataframe indexed by panel id (panid) and having the following columns: axes : tuple of matplotlib.Axes (primary and secondary) for each column. used secondary : bool indicating whether or not the seconday Axes is in use. relative size : height of panel as proportion of sum of all relative sizes """ num_panels = config['num_panels'] addplot = config['addplot'] volume = config['volume'] volume_panel = config['volume_panel'] num_panels = config['num_panels'] main_panel = config['main_panel'] panel_ratios = config['panel_ratios'] if not _valid_panel_id(main_panel): raise ValueError('main_panel id must be integer 0 to 9, but is '+str(main_panel)) if num_panels is None: # then infer the number of panels: pset = {0} # start with a set including only panel zero 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))) backwards_panel_compatibility = {'main':0,'lower':1,'A':0,'B':1,'C':2} for apdict in addplot: panel = apdict['panel'] if panel in backwards_panel_compatibility: panel = backwards_panel_compatibility[panel] if not _valid_panel_id(panel): raise ValueError('addplot panel must be integer 0 to 9, but is "'+str(panel)+'"') pset.add(panel) if volume is True: if not _valid_panel_id(volume_panel): raise ValueError('volume_panel must be integer 0 to 9, but is "'+str(volume_panel)+'"') pset.add(volume_panel) pset.add(main_panel) pset = sorted(pset) missing = [m for m in range(len(pset)) if m not in pset] if len(missing) != 0: raise ValueError('inferred panel list is missing panels: '+str(missing)) else: if not isinstance(num_panels,int) or num_panels < 1 or num_panels > 10: raise ValueError('num_panels must be integer 1 to 10, but is "'+str(volume_panel)+'"') pset = range(0,num_panels) _nones = [None]*len(pset) panels = pd.DataFrame(dict(axes=_nones, relsize=_nones, lift=_nones, height=_nones, used2nd=[False]*len(pset), title=_nones, ylabel=_nones), index=pset) panels.index.name = 'panid' # Now determine the height for each panel: # ( figure, num_panels='infer', addplot=None, volume_panel=None, main_panel=0, panel_ratios=None ): if panel_ratios is not None: if not isinstance(panel_ratios,(list,tuple)): raise TypeError('panel_ratios must be a list or tuple') if len(panel_ratios) != len(panels) and not (len(panel_ratios)==2 and len(panels) > 2): err = 'len(panel_ratios) must be 2, or must be same as number of panels' err += '\nlen(panel_ratios)='+str(len(panel_ratios))+' num panels='+str(len(panels)) raise ValueError(err) if len(panel_ratios) == 2 and len(panels) > 2: pratios = [panel_ratios[1]]*len(panels) pratios[main_panel] = panel_ratios[0] else: pratios = panel_ratios else: pratios = [2]*len(panels) pratios[main_panel] = 5 panels['relsize'] = pratios #print('len(panels)=',len(panels)) #print('len(pratios)=',len(pratios)) #print('pratios=') #print(pratios) #print('panels=') #print(panels) psum = sum(pratios) for panid,size in enumerate(pratios): panels.at[panid,'height'] = 0.7 * size / psum # Now create the Axes: for panid,row in panels.iterrows(): height = row.height lift = panels['height'].loc[panid+1:].sum() panels.at[panid,'lift'] = lift if panid == 0: # rect = [left, bottom, width, height] ax0 = figure.add_axes( [0.15, 0.18+lift, 0.70, height] ) else: ax0 = figure.add_axes( [0.15, 0.18+lift, 0.70, height], sharex=panels.at[0,'axes'][0] ) ax1 = ax0.twinx() ax1.grid(False) if config['saxbelow']: # issue#115 issuecomment-639446764 ax0.set_axisbelow(True) # so grid does not show through plot data on any panel. elif panid == volume_panel: ax0.set_axisbelow(True) # so grid does not show through volume bars. panels.at[panid,'axes'] = (ax0,ax1) return panels
def plot(data, **kwargs): """ Given a Pandas DataFrame containing columns Open,High,Low,Close and optionally Volume with a DatetimeIndex, plot the data. Available plots include ohlc bars, candlestick, and line plots. Also provide visually analysis in the form of common technical studies, such as: moving averages, renko, etc. Also provide ability to plot trading signals, and/or addtional user-defined data. """ config = _process_kwargs(kwargs, _valid_plot_kwargs()) dates, opens, highs, lows, closes, volumes = _check_and_prepare_data( data, config) if config['type'] in VALID_PMOVE_TYPES and config['addplot'] is not None: err = "`addplot` is not supported for `type='" + config['type'] + "'`" raise ValueError(err) style = config['style'] if isinstance(style, str): style = _styles._get_mpfstyle(style) if isinstance(style, dict): _styles._apply_mpfstyle(style) w, h = config['figratio'] r = float(w) / float(h) if r < 0.25 or r > 4.0: raise ValueError( '"figratio" (aspect ratio) must be between 0.25 and 4.0 (but is ' + str(r) + ')') base = (w, h) figscale = config['figscale'] fsize = [d * figscale for d in base] fig = plt.figure() fig.set_size_inches(fsize) if config['volume'] and volumes is None: raise ValueError('Request for volume, but NO volume data.') if config['volume']: if config['volume'] not in ['B', 'C']: config['volume'] = 'B' ha, hb, hc = _determine_relative_panel_heights(config['addplot'], config['volume'], config['panel_ratio']) axA1, axA2, axB1, axB2, axC1, axC2, actual_order = _create_panel_axes( fig, ha, hb, hc, config['panel_order']) internalAxes = dict(A=(axA1, axA2), B=(axB1, axB2), C=(axC1, axC2)) volumeAxes = internalAxes[ config['volume']][0] if config['volume'] else None fmtstring = _determine_format_string(dates, config['datetime_format']) ptype = config['type'] if config['show_nontrading']: formatter = mdates.DateFormatter(fmtstring) xdates = dates else: formatter = IntegerIndexDateTimeFormatter(dates, fmtstring) xdates = np.arange(len(dates)) axA1.xaxis.set_major_formatter(formatter) collections = None if ptype == 'line': axA1.plot(xdates, closes, color=config['linecolor']) else: collections = _construct_mpf_collections(ptype, dates, xdates, opens, highs, lows, closes, volumes, config, style) if ptype in VALID_PMOVE_TYPES: collections, new_dates, volumes, brick_values, size = collections formatter = IntegerIndexDateTimeFormatter(new_dates, fmtstring) xdates = np.arange(len(new_dates)) axA1.xaxis.set_major_formatter(formatter) if collections is not None: for collection in collections: axA1.add_collection(collection) mavgs = config['mav'] if mavgs is not None: if isinstance(mavgs, int): mavgs = mavgs, # convert to tuple if len(mavgs) > 7: mavgs = mavgs[0:7] # take at most 7 if style['mavcolors'] is not None: mavc = cycle(style['mavcolors']) else: mavc = None for mav in mavgs: if ptype in VALID_PMOVE_TYPES: mavprices = pd.Series(brick_values).rolling(mav).mean().values else: mavprices = pd.Series(closes).rolling(mav).mean().values if mavc: axA1.plot(xdates, mavprices, color=next(mavc)) else: axA1.plot(xdates, mavprices) avg_dist_between_points = (xdates[-1] - xdates[0]) / float(len(xdates)) minx = xdates[0] - avg_dist_between_points maxx = xdates[-1] + avg_dist_between_points if len(xdates) == 1: # kludge special case minx = minx - 0.75 maxx = maxx + 0.75 if ptype not in VALID_PMOVE_TYPES: _lows = lows _highs = highs else: _lows = brick_values _highs = [brick + size for brick in brick_values] miny = np.nanmin(_lows) maxy = np.nanmax(_highs) #if len(xdates) > 1: # stdy = (stat.stdev(_lows) + stat.stdev(_highs)) / 2.0 #else: # kludge special case # stdy = 0.02 * math.fabs(maxy - miny) # print('minx,miny,maxx,maxy,stdy=',minx,miny,maxx,maxy,stdy) if config['set_ylim'] is not None: axA1.set_ylim(config['set_ylim'][0], config['set_ylim'][1]) else: corners = (minx, miny), (maxx, maxy) axA1.update_datalim(corners) if config['return_calculated_values'] is not None: retdict = config['return_calculated_values'] if ptype in VALID_PMOVE_TYPES: prekey = ptype retdict[prekey + '_bricks'] = brick_values retdict[prekey + '_dates'] = mdates.num2date(new_dates) retdict[prekey + '_size'] = size if config['volume']: retdict[prekey + '_volumes'] = volumes if mavgs is not None: for i in range(0, len(mavgs)): retdict['mav' + str(mavgs[i])] = mavprices retdict['minx'] = minx retdict['maxx'] = maxx retdict['miny'] = miny retdict['maxy'] = maxy # Note: these are NOT mutually exclusive, so the order of this # if/elif is important: VALID_PMOVE_TYPES must be first. if ptype in VALID_PMOVE_TYPES: dtix = pd.DatetimeIndex([dt for dt in mdates.num2date(new_dates)]) elif not config['show_nontrading']: dtix = data.index else: dtix = None line_collections = [] line_collections.append( _construct_aline_collections(config['alines'], dtix)) line_collections.append( _construct_hline_collections(config['hlines'], minx, maxx)) line_collections.append( _construct_vline_collections(config['vlines'], dtix, miny, maxy)) tlines = config['tlines'] if isinstance(tlines, (list, tuple)) and all( [isinstance(item, dict) for item in tlines]): pass else: tlines = [ tlines, ] for tline_item in tlines: line_collections.append( _construct_tline_collections(tline_item, dtix, dates, opens, highs, lows, closes)) for collection in line_collections: if collection is not None: axA1.add_collection(collection) if config['volume']: vup, vdown = style['marketcolors']['volume'].values() #-- print('vup,vdown=',vup,vdown) vcolors = _updown_colors( vup, vdown, opens, closes, use_prev_close=style['marketcolors']['vcdopcod']) #-- print('len(vcolors),len(opens),len(closes)=',len(vcolors),len(opens),len(closes)) #-- print('vcolors=',vcolors) width = 0.5 * avg_dist_between_points volumeAxes.bar(xdates, volumes, width=width, color=vcolors) miny = 0.3 * np.nanmin(volumes) maxy = 1.1 * np.nanmax(volumes) volumeAxes.set_ylim(miny, maxy) xrotation = config['xrotation'] _adjust_ticklabels_per_bottom_panel(axA1, axB1, axC1, actual_order, hb, hc, formatter, xrotation) used_axA2 = False used_axB2 = False used_axC2 = False addplot = config['addplot'] if addplot is not None and ptype not in VALID_PMOVE_TYPES: # Calculate the Order of Magnitude Range # If addplot['secondary_y'] == 'auto', then: If the addplot['data'] # is out of the Order of Magnitude Range, then use secondary_y. # Calculate omrange for Main panel, and for Lower (volume) panel: lo = math.log(max(math.fabs(np.nanmin(lows)), 1e-7), 10) - 0.5 hi = math.log(max(math.fabs(np.nanmax(highs)), 1e-7), 10) + 0.5 # May 2020: Main panel is now called 'A', and Lower is called 'B' omrange = {'A': {'lo': lo, 'hi': hi}, 'B': None, 'C': None} if config['volume']: lo = math.log(max(math.fabs(np.nanmin(volumes)), 1e-7), 10) - 0.5 hi = math.log(max(math.fabs(np.nanmax(volumes)), 1e-7), 10) + 0.5 omrange.update(B={'lo': lo, 'hi': hi}) 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 apdict in addplot: apdata = apdict['data'] if isinstance(apdata, list) and not isinstance(apdata[0], (float, int)): raise TypeError('apdata is list but NOT of float or int') if isinstance(apdata, pd.DataFrame): havedf = True else: havedf = False # must be a single series or array apdata = [ apdata, ] # make it iterable for column in apdata: if havedf: ydata = apdata.loc[:, column] else: ydata = column yd = [y for y in ydata if not math.isnan(y)] ymhi = math.log(max(math.fabs(np.nanmax(yd)), 1e-7), 10) ymlo = math.log(max(math.fabs(np.nanmin(yd)), 1e-7), 10) secondary_y = False if apdict['secondary_y'] == 'auto': if apdict['panel'] == 'lower' or apdict['panel'] == 'B': # If omrange['lower'] is not yet set by volume, # then set it here as this is the first ydata # to be plotted on the lower panel, so consider # it to be the 'primary' lower panel axis. if omrange['B'] is None: omrange.update(B={'lo': ymlo, 'hi': ymhi}) elif ymlo < omrange['B']['lo'] or ymhi > omrange['B'][ 'hi']: secondary_y = True elif apdict['panel'] == 'C': if omrange['C'] is None: omrange.update(B={'lo': ymlo, 'hi': ymhi}) elif ymlo < omrange['C']['lo'] or ymhi > omrange['C'][ 'hi']: secondary_y = True elif ymlo < omrange['A']['lo'] or ymhi > omrange['A']['hi']: secondary_y = True # if secondary_y: # print('auto says USE secondary_y') # else: # print('auto says do NOT use secondary_y') else: secondary_y = apdict['secondary_y'] # print("apdict['secondary_y'] says secondary_y is",secondary_y) if apdict['panel'] == 'lower' or apdict['panel'] == 'B': ax = axB2 if secondary_y else axB1 elif apdict['panel'] == 'C': ax = axC2 if secondary_y else axC1 else: ax = axA2 if secondary_y else axA1 if ax == axA2: used_axA2 = True if ax == axB2: used_axB2 = True if ax == axC2: used_axC2 = True aptype = apdict['type'] if aptype == 'scatter': size = apdict['markersize'] mark = apdict['marker'] color = apdict['color'] # -------------------------------------------------------- # # This fixes Issue#77, but breaks other stuff: # ax.set_ylim(ymin=(miny - 0.4*stdy),ymax=(maxy + 0.4*stdy)) # -------------------------------------------------------- # ax.scatter(xdates, ydata, s=size, marker=mark, color=color) elif aptype == 'bar': width = apdict['width'] bottom = apdict['bottom'] color = apdict['color'] alpha = apdict['alpha'] ax.bar(xdates, ydata, width=width, bottom=bottom, color=color, alpha=alpha) elif aptype == 'line': ls = apdict['linestyle'] color = apdict['color'] ax.plot(xdates, ydata, linestyle=ls, color=color) #elif aptype == 'ohlc' or aptype == 'candle': # This won't work as is, because here we are looping through one column at a time # and mpf_collections needs ohlc columns: # collections =_construct_mpf_collections(aptype,dates,xdates,opens,highs,lows,closes,volumes,config,style) # if len(collections) == 1: collections = [collections] # for collection in collections: # ax.add_collection(collection) else: raise ValueError('addplot type "' + str(aptype) + '" NOT yet supported.') if config['set_ylim_panelB'] is not None: miny = config['set_ylim_panelB'][0] maxy = config['set_ylim_panelB'][1] axB1.set_ylim(miny, maxy) if config['set_ylim_panelC'] is not None: miny = config['set_ylim_panelC'][0] maxy = config['set_ylim_panelC'][1] axC1.set_ylim(miny, maxy) if config['yscale'] is not None: yscale = config['yscale'] panel = 'A' kwargs = None if isinstance(yscale, dict): if 'panel' in yscale: panel = yscale['panel'] if 'kwargs' in yscale: kwargs = yscale['kwargs'] yscale = yscale['yscale'] ax = internalAxes[panel][0] if kwargs is not None: ax.set_yscale(yscale, **kwargs) else: ax.set_yscale(yscale) # put the twinx() on the "other" side: if style['y_on_right']: axA1.yaxis.set_label_position('right') axA1.yaxis.tick_right() axA2.yaxis.set_label_position('left') axA2.yaxis.tick_left() if axB1 and axB2: axB1.yaxis.set_label_position('right') axB1.yaxis.tick_right() if axB2 != axB1: axB2.yaxis.set_label_position('left') axB2.yaxis.tick_left() else: axA1.yaxis.set_label_position('left') axA1.yaxis.tick_left() axA2.yaxis.set_label_position('right') axA2.yaxis.tick_right() if axB1 and axB2: axB1.yaxis.set_label_position('left') axB1.yaxis.tick_left() if axB2 != axB1: axB2.yaxis.set_label_position('right') axB2.yaxis.tick_right() # TODO: ================================================================ # TODO: Investigate: # TODO: =========== # TODO: It appears to me that there may be some or significant overlap # TODO: between what the following functions actually do: # TODO: At the very least, all four of them appear to communicate # TODO: to matplotlib that the xaxis should be treated as dates: # TODO: -> 'ax.autoscale_view()' # TODO: -> 'ax.xaxis_dates()' # TODO: -> 'plt.autofmt_xdates()' # TODO: -> 'fig.autofmt_xdate()' # TODO: ================================================================ #if config['autofmt_xdate']: #print('CALLING fig.autofmt_xdate()') #fig.autofmt_xdate() axA1.autoscale_view() # Is this really necessary?? axA1.set_ylabel(config['ylabel']) if config['volume']: volumeAxes.figure.canvas.draw() # This is needed to calculate offset offset = volumeAxes.yaxis.get_major_formatter().get_offset() volumeAxes.yaxis.offsetText.set_visible(False) if len(offset) > 0: offset = (' x ' + offset) if config['ylabel_lower'] is None: vol_label = 'Volume' + offset else: if len(offset) > 0: offset = '\n' + offset vol_label = config['ylabel_lower'] + offset volumeAxes.set_ylabel(vol_label) if config['title'] is not None: fig.suptitle(config['title'], size='x-large', weight='semibold') if not used_axA2 and axA2 is not None: axA2.get_yaxis().set_visible(False) if not used_axB2 and axB2 is not None: axB2.get_yaxis().set_visible(False) if not used_axC2 and axC2 is not None: axC2.get_yaxis().set_visible(False) axlist = [axA1, axA2] if axB1: axlist.append(axB1) if axB2: axlist.append(axB2) if axC1: axlist.append(axC1) if axC2: axlist.append(axC2) if config['axesoffdark']: fig.patch.set_facecolor('black') if config['axesoff']: fig.patch.set_visible(False) if config['axesoffdark'] or config['axesoff']: for ax in axlist: ax.set_xlim(xdates[0], xdates[-1]) ax.set_axis_off() if config['savefig'] is not None: save = config['savefig'] if isinstance(save, dict): plt.savefig(**save) else: plt.savefig(save) if config['closefig']: plt.close(fig) elif not config['returnfig']: plt.show(block=config['block'] ) # https://stackoverflow.com/a/13361748/1639359 if config['block']: plt.close(fig) if config['returnfig']: return (fig, axlist)