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