1#!/usr/bin/env python
2# -*- coding: utf-8; py-indent-offset:4 -*-
3###############################################################################
4#
5# Copyright (C) 2015, 2016, 2017 Daniel Rodriguez
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program.  If not, see <http://www.gnu.org/licenses/>.
19#
20###############################################################################
21from __future__ import (absolute_import, division, print_function,
22                        unicode_literals)
23
24import bisect
25import collections
26import datetime
27import itertools
28import math
29import operator
30import sys
31
32import matplotlib
33import numpy as np  # guaranteed by matplotlib
34import matplotlib.dates as mdates
35import matplotlib.font_manager as mfontmgr
36import matplotlib.legend as mlegend
37import matplotlib.ticker as mticker
38
39from ..utils.py3 import range, with_metaclass, string_types, integer_types
40from .. import AutoInfoClass, MetaParams, TimeFrame, date2num
41
42from .finance import plot_candlestick, plot_ohlc, plot_volume, plot_lineonclose
43from .formatters import (MyVolFormatter, MyDateFormatter, getlocator)
44from . import locator as loc
45from .multicursor import MultiCursor
46from .scheme import PlotScheme
47from .utils import tag_box_style
48
49
50class PInfo(object):
51    def __init__(self, sch):
52        self.sch = sch
53        self.nrows = 0
54        self.row = 0
55        self.clock = None
56        self.x = None
57        self.xlen = 0
58        self.sharex = None
59        self.figs = list()
60        self.cursors = list()
61        self.daxis = collections.OrderedDict()
62        self.vaxis = list()
63        self.zorder = dict()
64        self.coloridx = collections.defaultdict(lambda: -1)
65        self.handles = collections.defaultdict(list)
66        self.labels = collections.defaultdict(list)
67        self.legpos = collections.defaultdict(int)
68
69        self.prop = mfontmgr.FontProperties(size=self.sch.subtxtsize)
70
71    def newfig(self, figid, numfig, mpyplot):
72        fig = mpyplot.figure(figid + numfig)
73        self.figs.append(fig)
74        self.daxis = collections.OrderedDict()
75        self.vaxis = list()
76        self.row = 0
77        self.sharex = None
78        return fig
79
80    def nextcolor(self, ax):
81        self.coloridx[ax] += 1
82        return self.coloridx[ax]
83
84    def color(self, ax):
85        return self.sch.color(self.coloridx[ax])
86
87    def zordernext(self, ax):
88        z = self.zorder[ax]
89        if self.sch.zdown:
90            return z * 0.9999
91        return z * 1.0001
92
93    def zordercur(self, ax):
94        return self.zorder[ax]
95
96
97class Plot_OldSync(with_metaclass(MetaParams, object)):
98    params = (('scheme', PlotScheme()),)
99
100    def __init__(self, **kwargs):
101        for pname, pvalue in kwargs.items():
102            setattr(self.p.scheme, pname, pvalue)
103
104    def drawtag(self, ax, x, y, facecolor, edgecolor, alpha=0.9, **kwargs):
105
106        txt = ax.text(x, y, '%.2f' % y, va='center', ha='left',
107                      fontsize=self.pinf.sch.subtxtsize,
108                      bbox=dict(boxstyle=tag_box_style,
109                                facecolor=facecolor,
110                                edgecolor=edgecolor,
111                                alpha=alpha),
112                      # 3.0 is the minimum default for text
113                      zorder=self.pinf.zorder[ax] + 3.0,
114                      **kwargs)
115
116    def plot(self, strategy, figid=0, numfigs=1, iplot=True,
117             start=None, end=None, **kwargs):
118        # pfillers={}):
119        if not strategy.datas:
120            return
121
122        if not len(strategy):
123            return
124
125        if iplot:
126            if 'ipykernel' in sys.modules:
127                matplotlib.use('nbagg')
128
129        # this import must not happen before matplotlib.use
130        import matplotlib.pyplot as mpyplot
131        self.mpyplot = mpyplot
132
133        self.pinf = PInfo(self.p.scheme)
134        self.sortdataindicators(strategy)
135        self.calcrows(strategy)
136
137        st_dtime = strategy.lines.datetime.plot()
138        if start is None:
139            start = 0
140        if end is None:
141            end = len(st_dtime)
142
143        if isinstance(start, datetime.date):
144            start = bisect.bisect_left(st_dtime, date2num(start))
145
146        if isinstance(end, datetime.date):
147            end = bisect.bisect_right(st_dtime, date2num(end))
148
149        if end < 0:
150            end = len(st_dtime) + 1 + end  # -1 =  len() -2 = len() - 1
151
152        slen = len(st_dtime[start:end])
153        d, m = divmod(slen, numfigs)
154        pranges = list()
155        for i in range(numfigs):
156            a = d * i + start
157            if i == (numfigs - 1):
158                d += m  # add remainder to last stint
159            b = a + d
160
161            pranges.append([a, b, d])
162
163        figs = []
164
165        for numfig in range(numfigs):
166            # prepare a figure
167            fig = self.pinf.newfig(figid, numfig, self.mpyplot)
168            figs.append(fig)
169
170            self.pinf.pstart, self.pinf.pend, self.pinf.psize = pranges[numfig]
171            self.pinf.xstart = self.pinf.pstart
172            self.pinf.xend = self.pinf.pend
173
174            self.pinf.clock = strategy
175            self.pinf.xreal = self.pinf.clock.datetime.plot(
176                self.pinf.pstart, self.pinf.psize)
177            self.pinf.xlen = len(self.pinf.xreal)
178            self.pinf.x = list(range(self.pinf.xlen))
179            # self.pinf.pfillers = {None: []}
180            # for key, val in pfillers.items():
181            #     pfstart = bisect.bisect_left(val, self.pinf.pstart)
182            #     pfend = bisect.bisect_right(val, self.pinf.pend)
183            #     self.pinf.pfillers[key] = val[pfstart:pfend]
184
185            # Do the plotting
186            # Things that go always at the top (observers)
187            self.pinf.xdata = self.pinf.x
188            for ptop in self.dplotstop:
189                self.plotind(None, ptop, subinds=self.dplotsover[ptop])
190
191            # Create the rest on a per data basis
192            dt0, dt1 = self.pinf.xreal[0], self.pinf.xreal[-1]
193            for data in strategy.datas:
194                if not data.plotinfo.plot:
195                    continue
196
197                self.pinf.xdata = self.pinf.x
198                xd = data.datetime.plotrange(self.pinf.xstart, self.pinf.xend)
199                if len(xd) < self.pinf.xlen:
200                    self.pinf.xdata = xdata = []
201                    xreal = self.pinf.xreal
202                    dts = data.datetime.plot()
203                    xtemp = list()
204                    for dt in (x for x in dts if dt0 <= x <= dt1):
205                        dtidx = bisect.bisect_left(xreal, dt)
206                        xdata.append(dtidx)
207                        xtemp.append(dt)
208
209                    self.pinf.xstart = bisect.bisect_left(dts, xtemp[0])
210                    self.pinf.xend = bisect.bisect_right(dts, xtemp[-1])
211
212                for ind in self.dplotsup[data]:
213                    self.plotind(
214                        data,
215                        ind,
216                        subinds=self.dplotsover[ind],
217                        upinds=self.dplotsup[ind],
218                        downinds=self.dplotsdown[ind])
219
220                self.plotdata(data, self.dplotsover[data])
221
222                for ind in self.dplotsdown[data]:
223                    self.plotind(
224                        data,
225                        ind,
226                        subinds=self.dplotsover[ind],
227                        upinds=self.dplotsup[ind],
228                        downinds=self.dplotsdown[ind])
229
230            cursor = MultiCursor(
231                fig.canvas, list(self.pinf.daxis.values()),
232                useblit=True,
233                horizOn=True, vertOn=True,
234                horizMulti=False, vertMulti=True,
235                horizShared=True, vertShared=False,
236                color='black', lw=1, ls=':')
237
238            self.pinf.cursors.append(cursor)
239
240            # Put the subplots as indicated by hspace
241            fig.subplots_adjust(hspace=self.pinf.sch.plotdist,
242                                top=0.98, left=0.05, bottom=0.05, right=0.95)
243
244            laxis = list(self.pinf.daxis.values())
245
246            # Find last axis which is not a twinx (date locator fails there)
247            i = -1
248            while True:
249                lastax = laxis[i]
250                if lastax not in self.pinf.vaxis:
251                    break
252
253                i -= 1
254
255            self.setlocators(lastax)  # place the locators/fmts
256
257            # Applying fig.autofmt_xdate if the data axis is the last one
258            # breaks the presentation of the date labels. why?
259            # Applying the manual rotation with setp cures the problem
260            # but the labels from all axis but the last have to be hidden
261            for ax in laxis:
262                self.mpyplot.setp(ax.get_xticklabels(), visible=False)
263
264            self.mpyplot.setp(lastax.get_xticklabels(), visible=True,
265                              rotation=self.pinf.sch.tickrotation)
266
267            # Things must be tight along the x axis (to fill both ends)
268            axtight = 'x' if not self.pinf.sch.ytight else 'both'
269            self.mpyplot.autoscale(enable=True, axis=axtight, tight=True)
270
271        return figs
272
273    def setlocators(self, ax):
274        comp = getattr(self.pinf.clock, '_compression', 1)
275        tframe = getattr(self.pinf.clock, '_timeframe', TimeFrame.Days)
276
277        if self.pinf.sch.fmt_x_data is None:
278            if tframe == TimeFrame.Years:
279                fmtdata = '%Y'
280            elif tframe == TimeFrame.Months:
281                fmtdata = '%Y-%m'
282            elif tframe == TimeFrame.Weeks:
283                fmtdata = '%Y-%m-%d'
284            elif tframe == TimeFrame.Days:
285                fmtdata = '%Y-%m-%d'
286            elif tframe == TimeFrame.Minutes:
287                fmtdata = '%Y-%m-%d %H:%M'
288            elif tframe == TimeFrame.Seconds:
289                fmtdata = '%Y-%m-%d %H:%M:%S'
290            elif tframe == TimeFrame.MicroSeconds:
291                fmtdata = '%Y-%m-%d %H:%M:%S.%f'
292            elif tframe == TimeFrame.Ticks:
293                fmtdata = '%Y-%m-%d %H:%M:%S.%f'
294        else:
295            fmtdata = self.pinf.sch.fmt_x_data
296
297        fordata = MyDateFormatter(self.pinf.xreal, fmt=fmtdata)
298        for dax in self.pinf.daxis.values():
299            dax.fmt_xdata = fordata
300
301        # Major locator / formatter
302        locmajor = loc.AutoDateLocator(self.pinf.xreal)
303        ax.xaxis.set_major_locator(locmajor)
304        if self.pinf.sch.fmt_x_ticks is None:
305            autofmt = loc.AutoDateFormatter(self.pinf.xreal, locmajor)
306        else:
307            autofmt = MyDateFormatter(self.pinf.xreal,
308                                      fmt=self.pinf.sch.fmt_x_ticks)
309        ax.xaxis.set_major_formatter(autofmt)
310
311    def calcrows(self, strategy):
312        # Calculate the total number of rows
313        rowsmajor = self.pinf.sch.rowsmajor
314        rowsminor = self.pinf.sch.rowsminor
315        nrows = 0
316
317        datasnoplot = 0
318        for data in strategy.datas:
319            if not data.plotinfo.plot:
320                # neither data nor indicators nor volume add rows
321                datasnoplot += 1
322                self.dplotsup.pop(data, None)
323                self.dplotsdown.pop(data, None)
324                self.dplotsover.pop(data, None)
325
326            else:
327                pmaster = data.plotinfo.plotmaster
328                if pmaster is data:
329                    pmaster = None
330                if pmaster is not None:
331                    # data doesn't add a row, but volume may
332                    if self.pinf.sch.volume:
333                        nrows += rowsminor
334                else:
335                    # data adds rows, volume may
336                    nrows += rowsmajor
337                    if self.pinf.sch.volume and not self.pinf.sch.voloverlay:
338                        nrows += rowsminor
339
340        if False:
341            # Datas and volumes
342            nrows += (len(strategy.datas) - datasnoplot) * rowsmajor
343            if self.pinf.sch.volume and not self.pinf.sch.voloverlay:
344                nrows += (len(strategy.datas) - datasnoplot) * rowsminor
345
346        # top indicators/observers
347        nrows += len(self.dplotstop) * rowsminor
348
349        # indicators above datas
350        nrows += sum(len(v) for v in self.dplotsup.values())
351        nrows += sum(len(v) for v in self.dplotsdown.values())
352
353        self.pinf.nrows = nrows
354
355    def newaxis(self, obj, rowspan):
356        ax = self.mpyplot.subplot2grid(
357            (self.pinf.nrows, 1), (self.pinf.row, 0),
358            rowspan=rowspan, sharex=self.pinf.sharex)
359
360        # update the sharex information if not available
361        if self.pinf.sharex is None:
362            self.pinf.sharex = ax
363
364        # update the row index with the taken rows
365        self.pinf.row += rowspan
366
367        # save the mapping indicator - axis and return
368        self.pinf.daxis[obj] = ax
369
370        # Activate grid in all axes if requested
371        ax.yaxis.tick_right()
372        ax.grid(self.pinf.sch.grid, which='both')
373
374        return ax
375
376    def plotind(self, iref, ind,
377                subinds=None, upinds=None, downinds=None,
378                masterax=None):
379
380        sch = self.p.scheme
381
382        # check subind
383        subinds = subinds or []
384        upinds = upinds or []
385        downinds = downinds or []
386
387        # plot subindicators on self with independent axis above
388        for upind in upinds:
389            self.plotind(iref, upind)
390
391        # Get an axis for this plot
392        ax = masterax or self.newaxis(ind, rowspan=self.pinf.sch.rowsminor)
393
394        indlabel = ind.plotlabel()
395
396        # Scan lines quickly to find out if some lines have to be skipped for
397        # legend (because matplotlib reorders the legend)
398        toskip = 0
399        for lineidx in range(ind.size()):
400            line = ind.lines[lineidx]
401            linealias = ind.lines._getlinealias(lineidx)
402            lineplotinfo = getattr(ind.plotlines, '_%d' % lineidx, None)
403            if not lineplotinfo:
404                lineplotinfo = getattr(ind.plotlines, linealias, None)
405            if not lineplotinfo:
406                lineplotinfo = AutoInfoClass()
407            pltmethod = lineplotinfo._get('_method', 'plot')
408            if pltmethod != 'plot':
409                toskip += 1 - lineplotinfo._get('_plotskip', False)
410
411        if toskip >= ind.size():
412            toskip = 0
413
414        for lineidx in range(ind.size()):
415            line = ind.lines[lineidx]
416            linealias = ind.lines._getlinealias(lineidx)
417
418            lineplotinfo = getattr(ind.plotlines, '_%d' % lineidx, None)
419            if not lineplotinfo:
420                lineplotinfo = getattr(ind.plotlines, linealias, None)
421
422            if not lineplotinfo:
423                lineplotinfo = AutoInfoClass()
424
425            if lineplotinfo._get('_plotskip', False):
426                continue
427
428            # Legend label only when plotting 1st line
429            if masterax and not ind.plotinfo.plotlinelabels:
430                label = indlabel * (not toskip) or '_nolegend'
431            else:
432                label = (indlabel + '\n') * (not toskip)
433                label += lineplotinfo._get('_name', '') or linealias
434
435            toskip -= 1  # one line less until legend can be added
436
437            # plot data
438            lplot = line.plotrange(self.pinf.xstart, self.pinf.xend)
439
440            # Global and generic for indicator
441            if self.pinf.sch.linevalues and ind.plotinfo.plotlinevalues:
442                plotlinevalue = lineplotinfo._get('_plotvalue', True)
443                if plotlinevalue and not math.isnan(lplot[-1]):
444                    label += ' %.2f' % lplot[-1]
445
446            plotkwargs = dict()
447            linekwargs = lineplotinfo._getkwargs(skip_=True)
448
449            if linekwargs.get('color', None) is None:
450                if not lineplotinfo._get('_samecolor', False):
451                    self.pinf.nextcolor(ax)
452                plotkwargs['color'] = self.pinf.color(ax)
453
454            plotkwargs.update(dict(aa=True, label=label))
455            plotkwargs.update(**linekwargs)
456
457            if ax in self.pinf.zorder:
458                plotkwargs['zorder'] = self.pinf.zordernext(ax)
459
460            pltmethod = getattr(ax, lineplotinfo._get('_method', 'plot'))
461
462            xdata, lplotarray = self.pinf.xdata, lplot
463            if lineplotinfo._get('_skipnan', False):
464                # Get the full array and a mask to skipnan
465                lplotarray = np.array(lplot)
466                lplotmask = np.isfinite(lplotarray)
467
468                # Get both the axis and the data masked
469                lplotarray = lplotarray[lplotmask]
470                xdata = np.array(xdata)[lplotmask]
471
472            plottedline = pltmethod(xdata, lplotarray, **plotkwargs)
473            try:
474                plottedline = plottedline[0]
475            except:
476                # Possibly a container of artists (when plotting bars)
477                pass
478
479            self.pinf.zorder[ax] = plottedline.get_zorder()
480
481            vtags = lineplotinfo._get('plotvaluetags', True)
482            if self.pinf.sch.valuetags and vtags:
483                linetag = lineplotinfo._get('_plotvaluetag', True)
484                if linetag and not math.isnan(lplot[-1]):
485                    # line has valid values, plot a tag for the last value
486                    self.drawtag(ax, len(self.pinf.xreal), lplot[-1],
487                                 facecolor='white',
488                                 edgecolor=self.pinf.color(ax))
489
490            farts = (('_gt', operator.gt), ('_lt', operator.lt), ('', None),)
491            for fcmp, fop in farts:
492                fattr = '_fill' + fcmp
493                fref, fcol = lineplotinfo._get(fattr, (None, None))
494                if fref is not None:
495                    y1 = np.array(lplot)
496                    if isinstance(fref, integer_types):
497                        y2 = np.full_like(y1, fref)
498                    else:  # string, naming a line, nothing else is supported
499                        l2 = getattr(ind, fref)
500                        prl2 = l2.plotrange(self.pinf.xstart, self.pinf.xend)
501                        y2 = np.array(prl2)
502                    kwargs = dict()
503                    if fop is not None:
504                        kwargs['where'] = fop(y1, y2)
505
506                    falpha = self.pinf.sch.fillalpha
507                    if isinstance(fcol, (list, tuple)):
508                        fcol, falpha = fcol
509
510                    ax.fill_between(self.pinf.xdata, y1, y2,
511                                    facecolor=fcol,
512                                    alpha=falpha,
513                                    interpolate=True,
514                                    **kwargs)
515
516        # plot subindicators that were created on self
517        for subind in subinds:
518            self.plotind(iref, subind, subinds=self.dplotsover[subind],
519                         masterax=ax)
520
521        if not masterax:
522            # adjust margin if requested ... general of particular
523            ymargin = ind.plotinfo._get('plotymargin', 0.0)
524            ymargin = max(ymargin, self.pinf.sch.yadjust)
525            if ymargin:
526                ax.margins(y=ymargin)
527
528            # Set specific or generic ticks
529            yticks = ind.plotinfo._get('plotyticks', [])
530            if not yticks:
531                yticks = ind.plotinfo._get('plotyhlines', [])
532
533            if yticks:
534                ax.set_yticks(yticks)
535            else:
536                locator = mticker.MaxNLocator(nbins=4, prune='both')
537                ax.yaxis.set_major_locator(locator)
538
539            # Set specific hlines if asked to
540            hlines = ind.plotinfo._get('plothlines', [])
541            if not hlines:
542                hlines = ind.plotinfo._get('plotyhlines', [])
543            for hline in hlines:
544                ax.axhline(hline, color=self.pinf.sch.hlinescolor,
545                           ls=self.pinf.sch.hlinesstyle,
546                           lw=self.pinf.sch.hlineswidth)
547
548            if self.pinf.sch.legendind and \
549               ind.plotinfo._get('plotlegend', True):
550
551                handles, labels = ax.get_legend_handles_labels()
552                # Ensure that we have something to show
553                if labels:
554                    # location can come from the user
555                    loc = ind.plotinfo.legendloc or self.pinf.sch.legendindloc
556
557                    # Legend done here to ensure it includes all plots
558                    legend = ax.legend(loc=loc,
559                                       numpoints=1, frameon=False,
560                                       shadow=False, fancybox=False,
561                                       prop=self.pinf.prop)
562
563                    # legend.set_title(indlabel, prop=self.pinf.prop)
564                    # hack: if title is set. legend has a Vbox for the labels
565                    # which has a default "center" set
566                    legend._legend_box.align = 'left'
567
568        # plot subindicators on self with independent axis below
569        for downind in downinds:
570            self.plotind(iref, downind)
571
572    def plotvolume(self, data, opens, highs, lows, closes, volumes, label):
573        pmaster = data.plotinfo.plotmaster
574        if pmaster is data:
575            pmaster = None
576        voloverlay = (self.pinf.sch.voloverlay and pmaster is None)
577
578        # if sefl.pinf.sch.voloverlay:
579        if voloverlay:
580            rowspan = self.pinf.sch.rowsmajor
581        else:
582            rowspan = self.pinf.sch.rowsminor
583
584        ax = self.newaxis(data.volume, rowspan=rowspan)
585
586        # if self.pinf.sch.voloverlay:
587        if voloverlay:
588            volalpha = self.pinf.sch.voltrans
589        else:
590            volalpha = 1.0
591
592        maxvol = volylim = max(volumes)
593        if maxvol:
594
595            # Plot the volume (no matter if as overlay or standalone)
596            vollabel = label
597            volplot, = plot_volume(ax, self.pinf.xdata, opens, closes, volumes,
598                                   colorup=self.pinf.sch.volup,
599                                   colordown=self.pinf.sch.voldown,
600                                   alpha=volalpha, label=vollabel)
601
602            nbins = 6
603            prune = 'both'
604            # if self.pinf.sch.voloverlay:
605            if voloverlay:
606                # store for a potential plot over it
607                nbins = int(nbins / self.pinf.sch.volscaling)
608                prune = None
609
610                volylim /= self.pinf.sch.volscaling
611                ax.set_ylim(0, volylim, auto=True)
612            else:
613                # plot a legend
614                handles, labels = ax.get_legend_handles_labels()
615                if handles:
616
617                    # location can come from the user
618                    loc = data.plotinfo.legendloc or self.pinf.sch.legendindloc
619
620                    # Legend done here to ensure it includes all plots
621                    legend = ax.legend(loc=loc,
622                                       numpoints=1, frameon=False,
623                                       shadow=False, fancybox=False,
624                                       prop=self.pinf.prop)
625
626            locator = mticker.MaxNLocator(nbins=nbins, prune=prune)
627            ax.yaxis.set_major_locator(locator)
628            ax.yaxis.set_major_formatter(MyVolFormatter(maxvol))
629
630        if not maxvol:
631            ax.set_yticks([])
632            return None
633
634        return volplot
635
636    def plotdata(self, data, indicators):
637        for ind in indicators:
638            upinds = self.dplotsup[ind]
639            for upind in upinds:
640                self.plotind(data, upind,
641                             subinds=self.dplotsover[upind],
642                             upinds=self.dplotsup[upind],
643                             downinds=self.dplotsdown[upind])
644
645        opens = data.open.plotrange(self.pinf.xstart, self.pinf.xend)
646        highs = data.high.plotrange(self.pinf.xstart, self.pinf.xend)
647        lows = data.low.plotrange(self.pinf.xstart, self.pinf.xend)
648        closes = data.close.plotrange(self.pinf.xstart, self.pinf.xend)
649        volumes = data.volume.plotrange(self.pinf.xstart, self.pinf.xend)
650
651        vollabel = 'Volume'
652        pmaster = data.plotinfo.plotmaster
653        if pmaster is data:
654            pmaster = None
655
656        datalabel = ''
657        if hasattr(data, '_name') and data._name:
658            datalabel += data._name
659
660        voloverlay = (self.pinf.sch.voloverlay and pmaster is None)
661
662        if not voloverlay:
663            vollabel += ' ({})'.format(datalabel)
664
665        # if self.pinf.sch.volume and self.pinf.sch.voloverlay:
666        axdatamaster = None
667        if self.pinf.sch.volume and voloverlay:
668            volplot = self.plotvolume(
669                data, opens, highs, lows, closes, volumes, vollabel)
670            axvol = self.pinf.daxis[data.volume]
671            ax = axvol.twinx()
672            self.pinf.daxis[data] = ax
673            self.pinf.vaxis.append(ax)
674        else:
675            if pmaster is None:
676                ax = self.newaxis(data, rowspan=self.pinf.sch.rowsmajor)
677            elif getattr(data.plotinfo, 'sameaxis', False):
678                axdatamaster = self.pinf.daxis[pmaster]
679                ax = axdatamaster
680            else:
681                axdatamaster = self.pinf.daxis[pmaster]
682                ax = axdatamaster.twinx()
683                self.pinf.vaxis.append(ax)
684
685        if hasattr(data, '_compression') and \
686           hasattr(data, '_timeframe'):
687            tfname = TimeFrame.getname(data._timeframe, data._compression)
688            datalabel += ' (%d %s)' % (data._compression, tfname)
689
690        plinevalues = getattr(data.plotinfo, 'plotlinevalues', True)
691        if self.pinf.sch.style.startswith('line'):
692            if self.pinf.sch.linevalues and plinevalues:
693                datalabel += ' C:%.2f' % closes[-1]
694
695            if axdatamaster is None:
696                color = self.pinf.sch.loc
697            else:
698                self.pinf.nextcolor(axdatamaster)
699                color = self.pinf.color(axdatamaster)
700
701            plotted = plot_lineonclose(
702                ax, self.pinf.xdata, closes,
703                color=color, label=datalabel)
704        else:
705            if self.pinf.sch.linevalues and plinevalues:
706                datalabel += ' O:%.2f H:%.2f L:%.2f C:%.2f' % \
707                             (opens[-1], highs[-1], lows[-1], closes[-1])
708            if self.pinf.sch.style.startswith('candle'):
709                plotted = plot_candlestick(
710                    ax, self.pinf.xdata, opens, highs, lows, closes,
711                    colorup=self.pinf.sch.barup,
712                    colordown=self.pinf.sch.bardown,
713                    label=datalabel,
714                    alpha=self.pinf.sch.baralpha,
715                    fillup=self.pinf.sch.barupfill,
716                    filldown=self.pinf.sch.bardownfill)
717
718            elif self.pinf.sch.style.startswith('bar') or True:
719                # final default option -- should be "else"
720                plotted = plot_ohlc(
721                    ax, self.pinf.xdata, opens, highs, lows, closes,
722                    colorup=self.pinf.sch.barup,
723                    colordown=self.pinf.sch.bardown,
724                    label=datalabel)
725
726        self.pinf.zorder[ax] = plotted[0].get_zorder()
727
728        # Code to place a label at the right hand side with the last value
729        vtags = data.plotinfo._get('plotvaluetags', True)
730        if self.pinf.sch.valuetags and vtags:
731            self.drawtag(ax, len(self.pinf.xreal), closes[-1],
732                         facecolor='white', edgecolor=self.pinf.sch.loc)
733
734        ax.yaxis.set_major_locator(mticker.MaxNLocator(prune='both'))
735        # make sure "over" indicators do not change our scale
736        if data.plotinfo._get('plotylimited', True):
737            if axdatamaster is None:
738                ax.set_ylim(ax.get_ylim())
739
740        if self.pinf.sch.volume:
741            # if not self.pinf.sch.voloverlay:
742            if not voloverlay:
743                self.plotvolume(
744                    data, opens, highs, lows, closes, volumes, vollabel)
745            else:
746                # Prepare overlay scaling/pushup or manage own axis
747                if self.pinf.sch.volpushup:
748                    # push up overlaid axis by lowering the bottom limit
749                    axbot, axtop = ax.get_ylim()
750                    axbot *= (1.0 - self.pinf.sch.volpushup)
751                    ax.set_ylim(axbot, axtop)
752
753        for ind in indicators:
754            self.plotind(data, ind, subinds=self.dplotsover[ind], masterax=ax)
755
756        handles, labels = ax.get_legend_handles_labels()
757        a = axdatamaster or ax
758        if handles:
759            # put data and volume legend entries in the 1st positions
760            # because they are "collections" they are considered after Line2D
761            # for the legend entries, which is not our desire
762            # if self.pinf.sch.volume and self.pinf.sch.voloverlay:
763
764            ai = self.pinf.legpos[a]
765            if self.pinf.sch.volume and voloverlay:
766                if volplot:
767                    # even if volume plot was requested, there may be no volume
768                    labels.insert(ai, vollabel)
769                    handles.insert(ai, volplot)
770
771            didx = labels.index(datalabel)
772            labels.insert(ai, labels.pop(didx))
773            handles.insert(ai, handles.pop(didx))
774
775            if axdatamaster is None:
776                self.pinf.handles[ax] = handles
777                self.pinf.labels[ax] = labels
778            else:
779                self.pinf.handles[axdatamaster] = handles
780                self.pinf.labels[axdatamaster] = labels
781                # self.pinf.handles[axdatamaster].extend(handles)
782                # self.pinf.labels[axdatamaster].extend(labels)
783
784            h = self.pinf.handles[a]
785            l = self.pinf.labels[a]
786
787            axlegend = a
788            loc = data.plotinfo.legendloc or self.pinf.sch.legenddataloc
789            legend = axlegend.legend(h, l,
790                                     loc=loc,
791                                     frameon=False, shadow=False,
792                                     fancybox=False, prop=self.pinf.prop,
793                                     numpoints=1, ncol=1)
794
795            # hack: if title is set. legend has a Vbox for the labels
796            # which has a default "center" set
797            legend._legend_box.align = 'left'
798
799        for ind in indicators:
800            downinds = self.dplotsdown[ind]
801            for downind in downinds:
802                self.plotind(data, downind,
803                             subinds=self.dplotsover[downind],
804                             upinds=self.dplotsup[downind],
805                             downinds=self.dplotsdown[downind])
806
807        self.pinf.legpos[a] = len(self.pinf.handles[a])
808
809        if data.plotinfo._get('plotlog', False):
810            a = axdatamaster or ax
811            a.set_yscale('log')
812
813    def show(self):
814        self.mpyplot.show()
815
816    def savefig(self, fig, filename, width=16, height=9, dpi=300, tight=True):
817        fig.set_size_inches(width, height)
818        bbox_inches = 'tight' * tight or None
819        fig.savefig(filename, dpi=dpi, bbox_inches=bbox_inches)
820
821    def sortdataindicators(self, strategy):
822        # These lists/dictionaries hold the subplots that go above each data
823        self.dplotstop = list()
824        self.dplotsup = collections.defaultdict(list)
825        self.dplotsdown = collections.defaultdict(list)
826        self.dplotsover = collections.defaultdict(list)
827
828        # Sort observers in the different lists/dictionaries
829        for x in strategy.getobservers():
830            if not x.plotinfo.plot or x.plotinfo.plotskip:
831                continue
832
833            if x.plotinfo.subplot:
834                self.dplotstop.append(x)
835            else:
836                key = getattr(x._clock, 'owner', x._clock)
837                self.dplotsover[key].append(x)
838
839        # Sort indicators in the different lists/dictionaries
840        for x in strategy.getindicators():
841            if not hasattr(x, 'plotinfo'):
842                # no plotting support - so far LineSingle derived classes
843                continue
844
845            if not x.plotinfo.plot or x.plotinfo.plotskip:
846                continue
847
848            x._plotinit()  # will be plotted ... call its init function
849
850            # support LineSeriesStub which has "owner" to point to the data
851            key = getattr(x._clock, 'owner', x._clock)
852            if key is strategy:  # a LinesCoupler
853                key = strategy.data
854
855            if getattr(x.plotinfo, 'plotforce', False):
856                if key not in strategy.datas:
857                    datas = strategy.datas
858                    while True:
859                        if key not in strategy.datas:
860                            key = key._clock
861                        else:
862                            break
863
864            xpmaster = x.plotinfo.plotmaster
865            if xpmaster is x:
866                xpmaster = None
867            if xpmaster is not None:
868                key = xpmaster
869
870            if x.plotinfo.subplot and xpmaster is None:
871                if x.plotinfo.plotabove:
872                    self.dplotsup[key].append(x)
873                else:
874                    self.dplotsdown[key].append(x)
875            else:
876                self.dplotsover[key].append(x)
877
878
879Plot = Plot_OldSync
880