1#    Copyright (C) 2008 Jeremy S. Sanders
2#    Email: Jeremy Sanders <jeremy@jeremysanders.net>
3#
4#    This program is free software; you can redistribute it and/or modify
5#    it under the terms of the GNU General Public License as published by
6#    the Free Software Foundation; either version 2 of the License, or
7#    (at your option) any later version.
8#
9#    This program is distributed in the hope that it will be useful,
10#    but WITHOUT ANY WARRANTY; without even the implied warranty of
11#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12#    GNU General Public License for more details.
13#
14#    You should have received a copy of the GNU General Public License along
15#    with this program; if not, write to the Free Software Foundation, Inc.,
16#    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17###############################################################################
18
19"""For plotting xy points."""
20
21from __future__ import division
22import numpy as N
23
24from ..compat import czip
25from .. import qtall as qt
26from .. import datasets
27from .. import document
28from .. import setting
29from .. import utils
30
31from . import pickable
32from .plotters import GenericPlotter
33
34from ..helpers import qtloops
35
36def _(text, disambiguation=None, context='XY'):
37    """Translate text."""
38    return qt.QCoreApplication.translate(context, text, disambiguation)
39
40class ErrorBarDraw:
41    """For plotting error bars."""
42
43    def __init__(self, style, linestyle, fillabove, fillbelow, markersize):
44        self.style = style
45        self.linestyle = linestyle
46        self.fillabove = fillabove
47        self.fillbelow = fillbelow
48        self.markersize = markersize
49
50    def plot(self, painter, xmin, xmax, ymin, ymax, xplt, yplt, clip):
51        pen = self.linestyle.makeQPenWHide(painter)
52        pen.setCapStyle(qt.Qt.FlatCap)
53
54        painter.setPen(pen)
55        for function in self.error_functions[self.style]:
56            function(self, painter, xmin, xmax, ymin, ymax, xplt, yplt, clip)
57
58    def errorsBar(self, painter, xmin, xmax, ymin, ymax, xplt, yplt, clip):
59        """Draw bar style error lines."""
60        # vertical error bars
61        if ymin is not None and ymax is not None and not self.linestyle.hideVert:
62            qtloops.plotLinesToPainter(painter, xplt, ymin, xplt, ymax, clip)
63
64        # horizontal error bars
65        if xmin is not None and xmax is not None and not self.linestyle.hideHorz:
66            qtloops.plotLinesToPainter(painter, xmin, yplt, xmax, yplt, clip)
67
68    def errorsBarHi(self, painter, xmin, xmax, ymin, ymax, xplt, yplt, clip):
69        """Draw bar style error lines (top half only)."""
70        if ymin is not None and ymax is not None and not self.linestyle.hideVert:
71            qtloops.plotLinesToPainter(painter, xplt, yplt, xplt, ymax, clip)
72        if xmin is not None and xmax is not None and not self.linestyle.hideHorz:
73            qtloops.plotLinesToPainter(painter, xplt, yplt, xmax, yplt, clip)
74
75    def errorsBarLo(self, painter, xmin, xmax, ymin, ymax, xplt, yplt, clip):
76        """Draw bar style error lines (bottom half only)."""
77        if ymin is not None and ymax is not None and not self.linestyle.hideVert:
78            qtloops.plotLinesToPainter(painter, xplt, yplt, xplt, ymin, clip)
79        if xmin is not None and xmax is not None and not self.linestyle.hideHorz:
80            qtloops.plotLinesToPainter(painter, xplt, yplt, xmin, yplt, clip)
81
82    def errorsEnds(self, painter, xmin, xmax, ymin, ymax, xplt, yplt, clip):
83        """Draw perpendiclar ends on error bars."""
84        size = self.markersize * self.linestyle.endsize
85
86        if ymin is not None and ymax is not None and not self.linestyle.hideVert:
87            qtloops.plotLinesToPainter(
88                painter, xplt-size, ymin, xplt+size, ymin, clip)
89            qtloops.plotLinesToPainter(
90                painter, xplt-size, ymax, xplt+size, ymax, clip)
91
92        if xmin is not None and xmax is not None and not self.linestyle.hideHorz:
93            qtloops.plotLinesToPainter(
94                painter, xmin, yplt-size, xmin, yplt+size, clip)
95            qtloops.plotLinesToPainter(
96                painter, xmax, yplt-size, xmax, yplt+size, clip)
97
98    def errorsEndsHi(self, painter, xmin, xmax, ymin, ymax, xplt, yplt, clip):
99        """Draw perpendiclar ends on error bars (top half only)."""
100        size = self.markersize * self.linestyle.endsize
101        if ymin is not None and ymax is not None and not self.linestyle.hideVert:
102            qtloops.plotLinesToPainter(
103                painter, xplt-size, ymax, xplt+size, ymax, clip)
104        if xmin is not None and xmax is not None and not self.linestyle.hideHorz:
105            qtloops.plotLinesToPainter(
106                painter, xmax, yplt-size, xmax, yplt+size, clip)
107
108    def errorsEndsLo(self, painter, xmin, xmax, ymin, ymax, xplt, yplt, clip):
109        """Draw perpendiclar ends on error bars (bottom half only)."""
110        size = self.markersize * self.linestyle.endsize
111        if ymin is not None and ymax is not None and not self.linestyle.hideVert:
112            qtloops.plotLinesToPainter(
113                painter, xplt-size, ymin, xplt+size, ymin, clip)
114        if xmin is not None and xmax is not None and not self.linestyle.hideHorz:
115            qtloops.plotLinesToPainter(
116                painter, xmin, yplt-size, xmin, yplt+size, clip)
117
118    def errorsBox(self, painter, xmin, xmax, ymin, ymax, xplt, yplt, clip):
119        """Draw box around error region."""
120        if utils.allNotNone(xmin, xmax, ymin, ymax):
121            painter.setBrush(qt.QBrush())
122            qtloops.plotBoxesToPainter(painter, xmin, ymin, xmax, ymax, clip)
123
124    def errorsBoxFilled(self, painter, xmin, xmax, ymin, ymax, xplt, yplt, clip):
125        """Draw box filled region inside error bars."""
126        if utils.anyNone(xmin, xmax, ymin, ymax):
127            return
128
129        # filled region below
130        if not self.fillbelow.hideerror:
131            path = qt.QPainterPath()
132            qtloops.addNumpyPolygonToPath(
133                path, clip, xmin, ymin, xmin, yplt, xmax, yplt, xmax, ymin)
134            utils.brushExtFillPath(painter, self.fillbelow, path, ignorehide=True)
135
136        # filled region above
137        if not self.fillabove.hideerror:
138            path = qt.QPainterPath()
139            qtloops.addNumpyPolygonToPath(
140                path, clip, xmin, yplt, xmax, yplt, xmax, ymax, xmin, ymax)
141            utils.brushExtFillPath(painter, self.fillabove, path, ignorehide=True)
142
143    def errorsDiamond(self, painter, xmin, xmax, ymin, ymax, xplt, yplt, clip):
144        """Draw diamond around error region."""
145        if utils.anyNone(xmin, xmax, ymin, ymax):
146            return
147
148        # expand clip by pen width (urgh)
149        pw = painter.pen().widthF()*2
150        clip = qt.QRectF(
151            qt.QPointF(clip.left()-pw,clip.top()-pw),
152            qt.QPointF(clip.right()+pw,clip.bottom()+pw))
153
154        path = qt.QPainterPath()
155        qtloops.addNumpyPolygonToPath(
156            path, clip, xmin, yplt, xplt, ymax, xmax, yplt, xplt, ymin)
157        painter.setBrush( qt.QBrush() )
158        painter.drawPath(path)
159
160    def errorsDiamondFilled(self, painter, xmin, xmax, ymin, ymax, xplt, yplt, clip):
161        """Draw diamond filled region inside error bars."""
162        if utils.anyNone(xmin, xmax, ymin, ymax):
163            return
164
165        if not self.fillbelow.hideerror:
166            path = qt.QPainterPath()
167            qtloops.addNumpyPolygonToPath(
168                path, clip, xmin, yplt, xplt, ymin, xmax, yplt)
169            utils.brushExtFillPath(painter, self.fillbelow, path, ignorehide=True)
170
171        if not self.fillabove.hideerror:
172            path = qt.QPainterPath()
173            qtloops.addNumpyPolygonToPath(
174                path, clip, xmin, yplt, xplt, ymax, xmax, yplt)
175            utils.brushExtFillPath(painter, self.fillabove, path, ignorehide=True)
176
177    def errorsCurve(self, painter, xmin, xmax, ymin, ymax, xplt, yplt, clip):
178        """Draw curve around error region."""
179        if utils.anyNone(xmin, xmax, ymin, ymax):
180            return
181
182        # non-filling brush
183        painter.setBrush( qt.QBrush() )
184
185        for xp, yp, xmn, ymn, xmx, ymx in czip(xplt, yplt, xmin, ymin, xmax, ymax):
186            p = qt.QPainterPath()
187            p.moveTo(xp + (xmx-xp), yp)
188            p.arcTo(qt.QRectF(
189                xp - (xmx-xp), yp - (yp-ymx), (xmx-xp)*2, (yp-ymx)*2), 0., 90.)
190            p.arcTo(qt.QRectF(
191                xp - (xp-xmn), yp - (yp-ymx), (xp-xmn)*2, (yp-ymx)*2), 90., 90.)
192            p.arcTo(qt.QRectF(
193                xp - (xp-xmn), yp - (ymn-yp), (xp-xmn)*2, (ymn-yp)*2), 180., 90.)
194            p.arcTo(qt.QRectF(
195                xp - (xmx-xp), yp - (ymn-yp), (xmx-xp)*2, (ymn-yp)*2), 270., 90.)
196            painter.drawPath(p)
197
198    def errorsCurveFilled(self, painter, xmin, xmax, ymin, ymax, xplt, yplt, clip):
199        """Fill area around error region."""
200
201        if utils.anyNone(xmin, xmax, ymin, ymax):
202            return
203
204        for xp, yp, xmn, ymn, xmx, ymx in czip(xplt, yplt, xmin, ymin, xmax, ymax):
205
206            if not self.fillabove.hideerror:
207                p = qt.QPainterPath()
208                p.moveTo(xp + (xmx-xp), yp)
209                p.arcTo(qt.QRectF(
210                    xp - (xmx-xp), yp - (yp-ymx), (xmx-xp)*2, (yp-ymx)*2), 0., 90.)
211                p.arcTo(qt.QRectF(
212                    xp - (xp-xmn), yp - (yp-ymx), (xp-xmn)*2, (yp-ymx)*2), 90., 90.)
213                utils.brushExtFillPath(painter, self.fillabove, p, ignorehide=True)
214
215            if not self.fillbelow.hideerror:
216                p = qt.QPainterPath()
217                p.moveTo(xp + (xp-xmn), yp)
218                p.arcTo(qt.QRectF(
219                    xp - (xp-xmn), yp - (ymn-yp), (xp-xmn)*2, (ymn-yp)*2), 180., 90.)
220                p.arcTo(qt.QRectF(
221                    xp - (xmx-xp), yp - (ymn-yp), (xmx-xp)*2, (ymn-yp)*2), 270., 90.)
222                utils.brushExtFillPath(painter, self.fillbelow, p, ignorehide=True)
223
224    def errorsFilled(self, painter, xmin, xmax, ymin, ymax, xplt, yplt, clip):
225        """Draw filled region as error region."""
226
227        ptsabove = qt.QPolygonF()
228        ptsbelow = qt.QPolygonF()
229
230        hidevert = True  # keep track of what's shown
231        hidehorz = True
232        if ( 'vert' in self.style and
233             (ymin is not None and ymax is not None) and
234             not self.linestyle.hideVert ):
235            hidevert = False
236            # lines above/below points
237            if self.style[-2:] != 'hi':
238                qtloops.addNumpyToPolygonF(ptsbelow, xplt, ymin)
239            if self.style[-2:] != 'lo':
240                qtloops.addNumpyToPolygonF(ptsabove, xplt, ymax)
241
242        elif ( 'horz' in self.style and
243               (xmin is not None and xmax is not None) and
244               not self.linestyle.hideHorz ):
245            hidehorz = False
246            # lines left/right points
247            if self.style[-2:] != 'hi':
248                qtloops.addNumpyToPolygonF(ptsbelow, xmin, yplt)
249            if self.style[-2:] != 'lo':
250                qtloops.addNumpyToPolygonF(ptsabove, xmax, yplt)
251
252        # draw filled regions above/left and below/right
253        if 'fill' in self.style and not (hidehorz and hidevert):
254            # construct points for error bar regions
255            retnpts = qt.QPolygonF()
256            qtloops.addNumpyToPolygonF(retnpts, xplt[::-1], yplt[::-1])
257
258            # polygons consist of lines joining the points and continuing
259            # back along the plot line (retnpts)
260            if not self.fillbelow.hideerror and ptsbelow:
261                utils.brushExtFillPolygon(
262                    painter, self.fillbelow, clip, ptsbelow+retnpts, ignorehide=True)
263            if not self.fillabove.hideerror and ptsabove:
264                utils.brushExtFillPolygon(
265                    painter, self.fillabove, clip, ptsabove+retnpts, ignorehide=True)
266
267        # draw optional line (on top of fill)
268        if ptsabove:
269            qtloops.plotClippedPolyline(painter, clip, ptsabove)
270        if ptsbelow:
271            qtloops.plotClippedPolyline(painter, clip, ptsbelow)
272
273    # map error bar names to lists of functions (above)
274    error_functions = {
275        'none': (),
276        'bar': (errorsBar,),
277        'bardiamond': (errorsBar, errorsDiamond,),
278        'barcurve': (errorsBar, errorsCurve,),
279        'barbox': (errorsBar, errorsBox,),
280        'barends': (errorsBar, errorsEnds,),
281        'box':  (errorsBox,),
282        'boxfill': (errorsBoxFilled, errorsBox,),
283        'diamond':  (errorsDiamond,),
284        'diamondfill':  (errorsDiamond, errorsDiamondFilled),
285        'curve': (errorsCurve,),
286        'curvefill': (errorsCurveFilled, errorsCurve,),
287        'fillhorz': (errorsFilled,),
288        'fillvert': (errorsFilled,),
289        'linehorz': (errorsFilled,),
290        'linevert': (errorsFilled,),
291        'linehorzbar': (errorsBar, errorsFilled),
292        'linevertbar': (errorsBar, errorsFilled),
293        'barhi': (errorsBarHi,),
294        'barlo': (errorsBarLo,),
295        'barendshi': (errorsBarHi, errorsEndsHi,),
296        'barendslo': (errorsBarLo, errorsEndsLo,),
297        'linehorzlo': (errorsFilled,),
298        'linehorzhi': (errorsFilled,),
299        'linevertlo': (errorsFilled,),
300        'lineverthi': (errorsFilled,),
301    }
302
303def fillPtsToEdge(painter, pts, posn, cliprect, fillstyle):
304    """Fill points depending on fill mode."""
305    ft = fillstyle.fillto
306    if ft == 'top':
307        x1, x2 = pts[0].x(), pts[-1].x()
308        y1 = y2 = posn[1]
309    elif ft == 'bottom':
310        x1, x2 = pts[0].x(), pts[-1].x()
311        y1 = y2 = posn[3]
312    elif ft == 'left':
313        y1, y2 = pts[0].y(), pts[-1].y()
314        x1 = x2 = posn[0]
315    elif ft == 'right':
316        y1, y2 = pts[0].y(), pts[-1].y()
317        x1 = x2 = posn[2]
318    else:
319        raise RuntimeError('Invalid fillto mode')
320
321    polypts = qt.QPolygonF([qt.QPointF(x1, y1)])
322    polypts += pts
323    polypts.append(qt.QPointF(x2, y2))
324
325    utils.brushExtFillPolygon(painter, fillstyle, cliprect, polypts)
326
327class MarkerFillBrush(setting.Brush):
328    def __init__(self, name, **args):
329        setting.Brush.__init__(self, name, **args)
330
331        self.get('color').newDefault( setting.Reference('../color') )
332
333        self.add( setting.Colormap(
334            'colorMap', 'grey',
335            descr = _('If color markers dataset is given, use this colormap '
336                      'instead of the fill color'),
337            usertext=_('Color map'),
338            formatting=True) )
339        self.add( setting.Bool(
340            'colorMapInvert', False,
341            descr = _('Invert color map'),
342            usertext = _('Invert map'),
343            formatting=True) )
344
345class PointPlotter(GenericPlotter):
346    """A class for plotting points and their errors."""
347
348    typename='xy'
349    allowusercreation=True
350    description=_('Plot points with lines and errorbars')
351
352    @classmethod
353    def addSettings(klass, s):
354        """Construct list of settings."""
355        GenericPlotter.addSettings(s)
356
357        # non-formatting
358        s.add( setting.DatasetExtended(
359            'yData', 'y',
360            descr=_('Y values, given by dataset, expression or list of values'),
361            usertext=_('Y data')), 0 )
362        s.add( setting.DatasetExtended(
363            'xData', 'x',
364            descr=_('X values, given by dataset, expression or list of values'),
365            usertext=_('X data')), 0 )
366        s.add( setting.DatasetOrStr(
367            'labels', '',
368            descr=_('Dataset or string to label points'),
369            usertext=_('Labels')), 5 )
370        s.add( setting.DatasetExtended(
371            'scalePoints', '',
372            descr = _('Scale size of markers given by dataset, expression'
373                      ' or list of values'),
374            usertext=_('Scale markers')), 6 )
375
376        # formatting
377        s.add( setting.Int(
378            'errorthin', 1,
379            minval=1,
380            descr=_('Thin number of error bars plotted by this factor'),
381            usertext=_('Thin errors'),
382            formatting=True), 0 )
383        s.add( setting.Int(
384            'thinfactor', 1,
385            minval=1,
386            descr=_('Thin number of markers plotted'
387                    ' for each datapoint by this factor'),
388            usertext=_('Thin markers'),
389            formatting=True), 0 )
390        s.add( setting.Color(
391            'color',
392            'auto',
393            descr = _('Master color'),
394            usertext = _('Color'),
395            formatting=True), 0 )
396        s.add( setting.DistancePt(
397            'markerSize',
398            '3pt',
399            descr = _('Size of marker to plot'),
400            usertext=_('Marker size'), formatting=True), 0 )
401        s.add( setting.Marker(
402            'marker',
403            'circle',
404            descr = _('Type of marker to plot'),
405            usertext=_('Marker'), formatting=True), 0 )
406        s.add( setting.DataColor('Color') )
407
408        s.add( setting.ErrorStyle(
409            'errorStyle',
410            'bar',
411            descr=_('Style of error bars to plot'),
412            usertext=_('Error style'), formatting=True) )
413
414        s.add( setting.XYPlotLine(
415            'PlotLine',
416            descr = _('Plot line'),
417            usertext = _('Plot line')),
418               pixmap = 'settings_plotline' )
419
420        s.add( setting.MarkerLine(
421            'MarkerLine',
422            descr = _('Line around marker'),
423            usertext = _('Marker border')),
424               pixmap = 'settings_plotmarkerline' )
425        s.add( MarkerFillBrush(
426            'MarkerFill',
427            descr = _('Marker fill'),
428            usertext = _('Marker fill')),
429               pixmap = 'settings_plotmarkerfill' )
430
431        s.add( setting.ErrorBarLine(
432            'ErrorBarLine',
433            descr = _('Error bar line'),
434            usertext = _('Error bar line')),
435               pixmap = 'settings_ploterrorline' )
436        s.ErrorBarLine.get('color').newDefault( setting.Reference('../color') )
437
438        s.add( setting.PointFill(
439            'FillBelow',
440            descr = _('Fill mode 1'),
441            usertext = _('Fill 1')),
442               pixmap = 'settings_plotfillbelow' )
443        s.FillBelow.get('fillto').newDefault('bottom')
444        s.add( setting.PointFill(
445            'FillAbove',
446            descr = _('Fill 2'),
447            usertext = _('Fill 2')),
448               pixmap = 'settings_plotfillabove' )
449        s.add( setting.PointLabel(
450            'Label',
451            descr = _('Label settings'),
452            usertext=_('Label')),
453               pixmap = 'settings_axislabel' )
454
455    @property
456    def userdescription(self):
457        """User-friendly description."""
458
459        s = self.settings
460        return "x='%s', y='%s', marker='%s'" % (
461            s.xData, s.yData, s.marker)
462
463    def _plotErrors(self, posn, painter, xplotter, yplotter,
464                    axes, xdata, ydata, cliprect):
465        """Plot error bars (horizontal and vertical).
466        """
467
468        s = self.settings
469        style = s.errorStyle
470        if style == 'none':
471            return
472
473        # optional thinning of error bars plotted
474        thin = s.errorthin
475
476        # default is no error bars
477        xmin = xmax = ymin = ymax = None
478
479        # draw horizontal error bars
480        if xdata.hasErrors():
481            xmin, xmax = xdata.getPointRanges()
482            if thin>1:
483                xmin, xmax = xmin[::thin], xmax[::thin]
484
485            # convert xmin and xmax to graph coordinates
486            xmin = axes[0].dataToPlotterCoords(posn, xmin)
487            xmax = axes[0].dataToPlotterCoords(posn, xmax)
488
489        # draw vertical error bars
490        if ydata.hasErrors():
491            ymin, ymax = ydata.getPointRanges()
492            if thin>1:
493                ymin, ymax = ymin[::thin], ymax[::thin]
494
495            # convert ymin and ymax to graph coordinates
496            ymin = axes[1].dataToPlotterCoords(posn, ymin)
497            ymax = axes[1].dataToPlotterCoords(posn, ymax)
498
499        # no error bars - break out of processing below
500        if ymin is None and ymax is None and xmin is None and xmax is None:
501            return
502
503        if thin>1:
504            xplotter, yplotter = xplotter[::thin], yplotter[::thin]
505
506        markersize = s.get('markerSize').convert(painter)
507        ebp = ErrorBarDraw(
508            s.errorStyle, s.ErrorBarLine, s.FillAbove, s.FillBelow, markersize)
509        ebp.plot(painter, xmin, xmax, ymin, ymax, xplotter, yplotter, cliprect)
510
511    def affectsAxisRange(self):
512        """This widget provides range information about these axes."""
513        s = self.settings
514        return ( (s.xAxis, 'sx'), (s.yAxis, 'sy') )
515
516    def getRange(self, axis, depname, axrange):
517        """Compute the effect of data on the axis range."""
518        dataname = {'sx': 'xData', 'sy': 'yData'}[depname]
519        dsetn = self.settings.get(dataname)
520        data = dsetn.getData(self.document)
521
522        if data:
523            data.updateRangeAuto(axrange, axis.settings.log)
524        elif dsetn.isEmpty():
525            # no valid dataset.
526            # check if there a valid dataset for the other axis.
527            # if there is, treat this as a row number
528            dataname = {'sy': 'xData', 'sx': 'yData'}[depname]
529            data = self.settings.get(dataname).getData(self.document)
530            if data:
531                length = data.data.shape[0]
532                axrange[0] = min(axrange[0], 1)
533                axrange[1] = max(axrange[1], length)
534
535    def _getLinePoints( self, xvals, yvals, posn, xdata, ydata ):
536        """Get the points corresponding to the line connecting the points."""
537
538        pts = qt.QPolygonF()
539
540        s = self.settings
541        steps = s.PlotLine.steps
542
543        # simple continuous line
544        if steps == 'off':
545            utils.addNumpyToPolygonF(pts, xvals, yvals)
546
547        # stepped line, with points on left
548        elif steps[:4] == 'left':
549            x1 = xvals[:-1]
550            x2 = xvals[1:]
551            y1 = yvals[:-1]
552            y2 = yvals[1:]
553            utils.addNumpyToPolygonF(pts, x1, y1, x2, y1, x2, y2)
554
555        # stepped line, with points on right
556        elif steps[:5] == 'right':
557            x1 = xvals[:-1]
558            x2 = xvals[1:]
559            y1 = yvals[:-1]
560            y2 = yvals[1:]
561            utils.addNumpyToPolygonF(pts, x1, y1, x1, y2, x2, y2)
562
563        # stepped line, with points in centre
564        # this is complex as we can't use the mean of the plotter coords,
565        #  as the axis could be log
566        elif steps[:6] == 'centre':
567            axes = self.parent.getAxes( (s.xAxis, s.yAxis) )
568
569            if xdata.hasErrors():
570                # Special case if error bars on x points:
571                # here we use the error bars to define the steps
572                xmin, xmax = xdata.getPointRanges()
573
574                # this is duplicated from drawing error bars: bad
575                # convert xmin and xmax to graph coordinates
576                xmin = axes[0].dataToPlotterCoords(posn, xmin)
577                xmax = axes[0].dataToPlotterCoords(posn, xmax)
578                utils.addNumpyToPolygonF(pts, xmin, yvals, xmax, yvals)
579
580            else:
581                # we put the bin edges half way between the points
582                # we assume this is the correct thing to do even in log space
583                x1 = xvals[:-1]
584                x2 = xvals[1:]
585                y1 = yvals[:-1]
586                y2 = yvals[1:]
587                xc = 0.5*(x1+x2)
588                utils.addNumpyToPolygonF(pts, x1, y1, xc, y1, xc, y2)
589
590                if len(xvals) > 0:
591                    pts.append( qt.QPointF(xvals[-1], yvals[-1]) )
592
593        elif steps[:7] == 'vcentre':
594            axes = self.parent.getAxes( (s.xAxis, s.yAxis) )
595
596            if ydata.hasErrors():
597                # Special case if error bars on y points:
598                # here we use the error bars to define the steps
599                ymin, ymax = ydata.getPointRanges()
600
601                # this is duplicated from drawing error bars: bad
602                # convert ymin and ymax to graph coordinates
603                ymin = axes[1].dataToPlotterCoords(posn, ymin)
604                ymax = axes[1].dataToPlotterCoords(posn, ymax)
605                utils.addNumpyToPolygonF(pts, xvals, ymin, xvals, ymax)
606
607            else:
608                # we put the bin edges half way between the points
609                # we assume this is the correct thing to do even in log space
610                y1 = yvals[:-1]
611                y2 = yvals[1:]
612                x1 = xvals[:-1]
613                x2 = xvals[1:]
614                yc = 0.5*(y1+y2)
615                utils.addNumpyToPolygonF(pts, x1, y1, x1, yc, x2, yc)
616
617                if len(yvals) > 0:
618                    pts.append( qt.QPointF(xvals[-1], yvals[-1]) )
619
620        else:
621            assert False
622
623        return pts
624
625    def _getBezierLine(self, poly, cliprect):
626        """Try to draw a bezier line connecting the points."""
627
628        # clip to a larger box to help the lines get right angle
629        bigclip = qt.QRectF(
630            cliprect.left()-cliprect.width()*0.5,
631            cliprect.top()-cliprect.height()*0.5,
632            cliprect.width()*2, cliprect.height()*2)
633
634        # clip poly to the rectangle and return the parts
635        polys = qtloops.clipPolyline(bigclip, poly)
636
637        # add each part as a bezier
638        path = qt.QPainterPath()
639        for lpoly in polys:
640            if len(lpoly) >= 2:
641                npts = qtloops.bezier_fit_cubic_multi(lpoly, 0.1, len(lpoly)+1)
642                qtloops.addCubicsToPainterPath(path, npts);
643        return path
644
645    def _drawBezierLine( self, painter, xvals, yvals, posn,
646                         xdata, ydata, cliprect ):
647        """Handle bezier lines and fills."""
648
649        pts = self._getLinePoints(xvals, yvals, posn, xdata, ydata)
650        if len(pts) < 2:
651            return
652        path = self._getBezierLine(pts, cliprect)
653        s = self.settings
654
655        # do filling
656        for fillstyle in s.FillBelow, s.FillAbove:
657            if not fillstyle.hide:
658                x1, y1, x2, y2 = {
659                    'top': (pts[0].x(), posn[1], pts[-1].x(), posn[1]),
660                    'bottom': (pts[0].x(), posn[3], pts[-1].x(), posn[3]),
661                    'left': (posn[0], pts[0].y(), posn[0], pts[-1].y()),
662                    'right': (posn[2], pts[0].y(), posn[2], pts[-1].y())
663                }[fillstyle.fillto]
664
665                temppath = qt.QPainterPath(path)
666                temppath.lineTo(x2, y2)
667                temppath.lineTo(x1, y1)
668                utils.brushExtFillPath(painter, fillstyle, temppath)
669
670        if not s.PlotLine.hide:
671            painter.strokePath(path, s.PlotLine.makeQPen(painter))
672
673    def _drawPlotLine( self, painter, xvals, yvals, posn, xdata, ydata,
674                       cliprect ):
675        """Draw the line connecting the points."""
676
677        pts = self._getLinePoints(xvals, yvals, posn, xdata, ydata)
678        if len(pts) < 2:
679            return
680        s = self.settings
681
682        # do filling
683        for fillstyle in s.FillBelow, s.FillAbove:
684            if not fillstyle.hide:
685                fillPtsToEdge(painter, pts, posn, cliprect, fillstyle)
686
687        # draw line between points
688        if not s.PlotLine.hide:
689            painter.setPen( s.PlotLine.makeQPen(painter) )
690            utils.plotClippedPolyline(painter, cliprect, pts)
691
692    def drawKeySymbol(self, number, painter, x, y, width, height):
693        """Draw the plot symbol and/or line."""
694
695        s = self.settings
696
697        # datasets from document
698        xv = s.get('xData').getData(self.document)
699        yv = s.get('yData').getData(self.document)
700
701        # whether data has errors
702        hasxerrs = xv and xv.hasErrors()
703        hasyerrs = yv and yv.hasErrors()
704
705        # convert horizontal errors to vertical ones
706        errstyle = s.errorStyle
707        if errstyle in ('linehorz', 'fillhorz', 'likehorzbar'):
708            errstyle = errstyle.replace('horz', 'vert')
709            hasxerrs, hasyerrs = hasyerrs, hasxerrs
710
711        # make some fake error bar data to plot
712        yp = y + height/2
713        xpts = N.array([x-width, x+width/2, x+2*width])
714        ypts = N.array([yp, yp, yp])
715
716        # start drawing
717        painter.save()
718        cliprect = qt.QRectF(qt.QPointF(x,y), qt.QPointF(x+width,y+height))
719        painter.setClipRect(cliprect)
720
721        # draw fill setting
722        if not s.FillBelow.hide:
723            path = qt.QPainterPath()
724            path.addRect(qt.QRectF(
725                qt.QPointF(x, yp), qt.QPointF(x+width, yp+height*0.45)))
726            utils.brushExtFillPath(painter, s.FillBelow, path)
727        if not s.FillAbove.hide:
728            path = qt.QPainterPath()
729            path.addRect(qt.QRectF(
730                qt.QPointF(x, yp), qt.QPointF(x+width, yp-height*0.45)))
731            utils.brushExtFillPath(painter, s.FillAbove, path)
732
733        # make points for error bars (if any)
734        errorsize = height*0.4
735        if xv and hasxerrs:
736            xneg = N.array([x-width, x+width/2-errorsize, x+2*width])
737            xpos = N.array([x-width, x+width/2+errorsize, x+2*width])
738        else:
739            xneg = xpos = xpts
740        if yv and hasyerrs:
741            yneg = N.array([yp-errorsize, yp-errorsize, yp-errorsize])
742            ypos = N.array([yp+errorsize, yp+errorsize, yp+errorsize])
743        else:
744            yneg = ypos = ypts
745
746        # plot error bar
747        markersize = s.get('markerSize').convert(painter)
748        ebp = ErrorBarDraw(
749            errstyle, s.ErrorBarLine, s.FillAbove, s.FillBelow, markersize)
750        ebp.plot(painter, xneg, xpos, yneg, ypos, xpts, ypts, cliprect)
751
752        # draw line
753        if not s.PlotLine.hide:
754            painter.setPen( s.PlotLine.makeQPen(painter) )
755            painter.drawLine( qt.QPointF(x, yp), qt.QPointF(x+width, yp) )
756
757        # draw marker
758        if not s.MarkerLine.hide or not s.MarkerFill.hide:
759            if not s.MarkerFill.hide:
760                painter.setBrush( s.MarkerFill.makeQBrush(painter) )
761
762            if not s.MarkerLine.hide:
763                painter.setPen( s.MarkerLine.makeQPen(painter) )
764            else:
765                painter.setPen( qt.QPen( qt.Qt.NoPen ) )
766
767            utils.plotMarker(painter, x+width/2, yp, s.marker, markersize)
768
769        painter.restore()
770
771    def drawLabels(self, painter, xplotter, yplotter,
772                   textvals, markersize):
773        """Draw labels for the points."""
774
775        s = self.settings
776        lab = s.get('Label')
777
778        # work out offset an alignment
779        deltax = markersize*1.5*{'left':-1, 'centre':0, 'right':1}[lab.posnHorz]
780        deltay = markersize*1.5*{'top':-1, 'centre':0, 'bottom':1}[lab.posnVert]
781        alignhorz = {'left':1, 'centre':0, 'right':-1}[lab.posnHorz]
782        alignvert = {'top':-1, 'centre':0, 'bottom':1}[lab.posnVert]
783
784        # make font and len
785        textpen = lab.makeQPen(painter)
786        painter.setPen(textpen)
787        font = lab.makeQFont(painter)
788        angle = lab.angle
789
790        # iterate over each point and plot each label
791        for x, y, t in czip(xplotter+deltax, yplotter+deltay,
792                            textvals):
793            utils.Renderer(
794                painter, font, x, y, t,
795                alignhorz, alignvert, angle,
796                doc=self.document).render()
797
798    def getAxisLabels(self, direction):
799        """Get labels for axis if using a label axis."""
800
801        s = self.settings
802        doc = self.document
803        text = s.get('labels').getData(doc, checknull=True)
804        xv = s.get('xData').getData(doc)
805        yv = s.get('yData').getData(doc)
806
807        # handle missing dataset
808        if yv and not xv and s.get('xData').isEmpty():
809            length = yv.data.shape[0]
810            xv = datasets.DatasetRange(length, (1,length))
811        elif xv and not yv and s.get('yData').isEmpty():
812            length = xv.data.shape[0]
813            yv = datasets.DatasetRange(length, (1,length))
814
815        if text is None or xv is None or yv is None:
816            return (None, None)
817        if direction == 'horizontal':
818            return (text, xv.data)
819        else:
820            return (text, yv.data)
821
822    def _pickable(self, bounds):
823        axes = self.fetchAxes()
824
825        if axes is None:
826            map_fn = None
827        else:
828            map_fn = lambda x, y: (
829                axes[0].dataToPlotterCoords(bounds, x),
830                axes[1].dataToPlotterCoords(bounds, y) )
831
832        return pickable.DiscretePickable(self, 'xData', 'yData', map_fn)
833
834    def pickPoint(self, x0, y0, bounds, distance = 'radial'):
835        return self._pickable(bounds).pickPoint(x0, y0, bounds, distance)
836
837    def pickIndex(self, oldindex, direction, bounds):
838        return self._pickable(bounds).pickIndex(oldindex, direction, bounds)
839
840    def getColorbarParameters(self):
841        """Return parameters for colorbar."""
842        s = self.settings
843        c = s.Color
844        return (c.min, c.max, c.scaling, s.MarkerFill.colorMap, 0,
845                s.MarkerFill.colorMapInvert)
846
847    def dataDraw(self, painter, axes, posn, cliprect):
848        """Plot the data on a plotter."""
849
850        # get data
851        s = self.settings
852        doc = self.document
853        xv = s.get('xData').getData(doc)
854        yv = s.get('yData').getData(doc)
855        text = s.get('labels').getData(doc, checknull=True)
856        scalepoints = s.get('scalePoints').getData(doc)
857        colorpoints = s.Color.get('points').getData(doc)
858
859        # if a missing dataset, make a fake dataset for the second one
860        # based on a row number
861        if xv and not yv and s.get('yData').isEmpty():
862            # use index for y data
863            length = xv.data.shape[0]
864            yv = datasets.DatasetRange(length, (1,length))
865        elif yv and not xv and s.get('xData').isEmpty():
866            # use index for x data
867            length = yv.data.shape[0]
868            xv = datasets.DatasetRange(length, (1,length))
869        if not xv or not yv:
870            # no valid dataset, so exit
871            return
872
873        # if text entered, then multiply up to get same number of values
874        # as datapoints
875        if text:
876            length = min( len(xv.data), len(yv.data) )
877            text = text*(length // len(text)) + text[:length % len(text)]
878
879        # loop over chopped up values
880        for xvals, yvals, tvals, ptvals, cvals in (
881            datasets.generateValidDatasetParts(
882                [xv, yv, text, scalepoints, colorpoints])):
883
884            #print "Calculating coordinates"
885            # calc plotter coords of x and y points
886            xplotter = axes[0].dataToPlotterCoords(posn, xvals.data)
887            yplotter = axes[1].dataToPlotterCoords(posn, yvals.data)
888
889            # points are plotted offset in shift-points modes
890            if s.PlotLine.steps != 'off':
891                xpltpoint = N.array(xplotter)
892                if s.PlotLine.steps == 'right-shift-points':
893                    xpltpoint[1:] = 0.5*(xplotter[:-1] + xplotter[1:])
894                elif s.PlotLine.steps == 'left-shift-points':
895                    xpltpoint[:-1] = 0.5*(xplotter[:-1] + xplotter[1:])
896            else:
897                xpltpoint = xplotter
898            ypltpoint = yplotter
899
900            # plot filled error bars
901            if s.errorStyle in ('fillvert', 'fillhorz'):
902                # filled region errors are painted first
903                self._plotErrors(
904                    posn, painter, xpltpoint, ypltpoint,
905                    axes, xvals, yvals, cliprect)
906
907            #print "Painting plot line"
908            # plot data line (and/or filling above or below)
909            if not s.PlotLine.hide or not s.FillAbove.hide or not s.FillBelow.hide:
910                if s.PlotLine.bezierJoin:
911                    self._drawBezierLine(
912                        painter, xplotter, yplotter, posn,
913                        xvals, yvals, cliprect )
914                else:
915                    self._drawPlotLine(
916                        painter, xplotter, yplotter, posn,
917                        xvals, yvals, cliprect )
918
919            #print "Painting error bars"
920            # plot normal errors bars
921            if s.errorStyle not in ('fillvert', 'fillhorz'):
922                # normally the error bar is painted after the line
923                self._plotErrors(posn, painter, xpltpoint, ypltpoint,
924                                 axes, xvals, yvals, cliprect)
925
926            # plot the points (we do this last so they are on top)
927            markersize = s.get('markerSize').convert(painter)
928            if not s.MarkerLine.hide or not s.MarkerFill.hide:
929
930                #print "Painting marker fill"
931                if not s.MarkerFill.hide:
932                    # filling for markers
933                    painter.setBrush( s.MarkerFill.makeQBrush(painter) )
934                else:
935                    # no-filling brush
936                    painter.setBrush( qt.QBrush() )
937
938                #print "Painting marker lines"
939                if not s.MarkerLine.hide:
940                    # edges of markers
941                    painter.setPen( s.MarkerLine.makeQPen(painter) )
942                else:
943                    # invisible pen
944                    painter.setPen( qt.QPen(qt.Qt.NoPen) )
945
946                # thin datapoints as required
947                if s.thinfactor <= 1:
948                    xplt, yplt = xpltpoint, ypltpoint
949                else:
950                    xplt, yplt = (
951                        xpltpoint[::s.thinfactor],
952                        ypltpoint[::s.thinfactor])
953
954                # whether to scale markers
955                scaling = colorvals = cmap = None
956                if ptvals:
957                    scaling = ptvals.data
958                    if s.thinfactor > 1:
959                        scaling = scaling[::s.thinfactor]
960
961                # color point individually
962                cmapname = s.MarkerFill.colorMap
963                if cvals and not s.MarkerFill.hide and cmapname != 'none':
964                    colorvals = utils.applyScaling(
965                        cvals.data, s.Color.scaling,
966                        s.Color.min, s.Color.max)
967                    if s.thinfactor > 1:
968                        colorvals = colorvals[::s.thinfactor]
969                    cmap = self.document.evaluate.getColormap(
970                        cmapname, s.MarkerFill.colorMapInvert)
971
972                # actually plot datapoints
973                utils.plotMarkers(
974                    painter, xplt, yplt, s.marker, markersize,
975                    scaling=scaling, clip=cliprect,
976                    cmap=cmap, colorvals=colorvals,
977                    scaleline=s.MarkerLine.scaleLine)
978
979            # finally plot any labels
980            if tvals and not s.Label.hide:
981                self.drawLabels(
982                    painter, xpltpoint, ypltpoint,
983                    tvals, markersize)
984
985# allow the factory to instantiate an x,y plotter
986document.thefactory.register( PointPlotter )
987