1# -*- coding: utf-8 -*- 2# pylint: disable=E1101, C0330, C0103 3# E1101: Module X has no Y member 4# C0330: Wrong continued indentation 5# C0103: Invalid attribute/variable/method name 6""" 7polyobjects.py 8============== 9 10This contains all of the PolyXXX objects used by :mod:`wx.lib.plot`. 11 12""" 13__docformat__ = "restructuredtext en" 14 15# Standard Library 16import time as _time 17import wx 18import warnings 19from collections import namedtuple 20 21# Third-Party 22try: 23 import numpy as np 24except: 25 msg = """ 26 This module requires the NumPy module, which could not be 27 imported. It probably is not installed (it's not part of the 28 standard Python distribution). See the Numeric Python site 29 (http://numpy.scipy.org) for information on downloading source or 30 binaries.""" 31 raise ImportError("NumPy not found.\n" + msg) 32 33# Package 34from .utils import pendingDeprecation 35from .utils import TempStyle 36from .utils import pairwise 37 38 39class PolyPoints(object): 40 """ 41 Base Class for lines and markers. 42 43 :param points: The points to plot 44 :type points: list of ``(x, y)`` pairs 45 :param attr: Additional attributes 46 :type attr: dict 47 48 .. warning:: 49 All methods are private. 50 """ 51 52 def __init__(self, points, attr): 53 self._points = np.array(points).astype(np.float64) 54 self._logscale = (False, False) 55 self._absScale = (False, False) 56 self._symlogscale = (False, False) 57 self._pointSize = (1.0, 1.0) 58 self.currentScale = (1, 1) 59 self.currentShift = (0, 0) 60 self.scaled = self.points 61 self.attributes = {} 62 self.attributes.update(self._attributes) 63 for name, value in attr.items(): 64 if name not in self._attributes.keys(): 65 err_txt = "Style attribute incorrect. Should be one of {}" 66 raise KeyError(err_txt.format(self._attributes.keys())) 67 self.attributes[name] = value 68 69 @property 70 def logScale(self): 71 """ 72 A tuple of ``(x_axis_is_log10, y_axis_is_log10)`` booleans. If a value 73 is ``True``, then that axis is plotted on a logarithmic base 10 scale. 74 75 :getter: Returns the current value of logScale 76 :setter: Sets the value of logScale 77 :type: tuple of bool, length 2 78 :raises ValueError: when setting an invalid value 79 """ 80 return self._logscale 81 82 @logScale.setter 83 def logScale(self, logscale): 84 if not isinstance(logscale, tuple) or len(logscale) != 2: 85 raise ValueError("`logscale` must be a 2-tuple of bools") 86 self._logscale = logscale 87 88 def setLogScale(self, logscale): 89 """ 90 Set to change the axes to plot Log10(values) 91 92 Value must be a tuple of booleans (x_axis_bool, y_axis_bool) 93 94 .. deprecated:: Feb 27, 2016 95 96 Use the :attr:`~wx.lib.plot.polyobjects.PolyPoints.logScale` 97 property instead. 98 """ 99 pendingDeprecation("self.logScale property") 100 self._logscale = logscale 101 102 @property 103 def symLogScale(self): 104 """ 105 .. warning:: 106 107 Not yet implemented. 108 109 A tuple of ``(x_axis_is_SymLog10, y_axis_is_SymLog10)`` booleans. 110 If a value is ``True``, then that axis is plotted on a symmetric 111 logarithmic base 10 scale. 112 113 A Symmetric Log10 scale means that values can be positive and 114 negative. Any values less than 115 :attr:`~wx.lig.plot.PolyPoints.symLogThresh` will be plotted on 116 a linear scale to avoid the plot going to infinity near 0. 117 118 :getter: Returns the current value of symLogScale 119 :setter: Sets the value of symLogScale 120 :type: tuple of bool, length 2 121 :raises ValueError: when setting an invalid value 122 123 .. notes:: 124 125 This is a simplified example of how SymLog works:: 126 127 if x >= thresh: 128 x = Log10(x) 129 elif x =< thresh: 130 x = -Log10(Abs(x)) 131 else: 132 x = x 133 134 .. seealso:: 135 136 + :attr:`~wx.lib.plot.PolyPoints.symLogThresh` 137 + See http://matplotlib.org/examples/pylab_examples/symlog_demo.html 138 for an example. 139 """ 140 return self._symlogscale 141 142 # TODO: Implement symmetric log scale 143 @symLogScale.setter 144 def symLogScale(self, symlogscale, thresh): 145 raise NotImplementedError("Symmetric Log Scale not yet implemented") 146 147 if not isinstance(symlogscale, tuple) or len(symlogscale) != 2: 148 raise ValueError("`symlogscale` must be a 2-tuple of bools") 149 self._symlogscale = symlogscale 150 151 @property 152 def symLogThresh(self): 153 """ 154 .. warning:: 155 156 Not yet implemented. 157 158 A tuple of ``(x_thresh, y_thresh)`` floats that define where the plot 159 changes to linear scale when using a symmetric log scale. 160 161 :getter: Returns the current value of symLogThresh 162 :setter: Sets the value of symLogThresh 163 :type: tuple of float, length 2 164 :raises ValueError: when setting an invalid value 165 166 .. notes:: 167 168 This is a simplified example of how SymLog works:: 169 170 if x >= thresh: 171 x = Log10(x) 172 elif x =< thresh: 173 x = -Log10(Abs(x)) 174 else: 175 x = x 176 177 .. seealso:: 178 179 + :attr:`~wx.lib.plot.PolyPoints.symLogScale` 180 + See http://matplotlib.org/examples/pylab_examples/symlog_demo.html 181 for an example. 182 """ 183 return self._symlogscale 184 185 # TODO: Implement symmetric log scale threshold 186 @symLogThresh.setter 187 def symLogThresh(self, symlogscale, thresh): 188 raise NotImplementedError("Symmetric Log Scale not yet implemented") 189 190 if not isinstance(symlogscale, tuple) or len(symlogscale) != 2: 191 raise ValueError("`symlogscale` must be a 2-tuple of bools") 192 self._symlogscale = symlogscale 193 194 @property 195 def absScale(self): 196 """ 197 A tuple of ``(x_axis_is_abs, y_axis_is_abs)`` booleans. If a value 198 is ``True``, then that axis is plotted on an absolute value scale. 199 200 :getter: Returns the current value of absScale 201 :setter: Sets the value of absScale 202 :type: tuple of bool, length 2 203 :raises ValueError: when setting an invalid value 204 """ 205 return self._absScale 206 207 @absScale.setter 208 def absScale(self, absscale): 209 210 if not isinstance(absscale, tuple) and len(absscale) == 2: 211 raise ValueError("`absscale` must be a 2-tuple of bools") 212 self._absScale = absscale 213 214 @property 215 def points(self): 216 """ 217 Get or set the plotted points. 218 219 :getter: Returns the current value of points, adjusting for the 220 various scale options such as Log, Abs, or SymLog. 221 :setter: Sets the value of points. 222 :type: list of `(x, y)` pairs 223 224 .. Note:: 225 226 Only set unscaled points - do not perform the log, abs, or symlog 227 adjustments yourself. 228 """ 229 data = np.array(self._points, copy=True) # need the copy 230 # TODO: get rid of the 231 # need for copy 232 233 # work on X: 234 if self.absScale[0]: 235 data = self._abs(data, 0) 236 if self.logScale[0]: 237 data = self._log10(data, 0) 238 239 if self.symLogScale[0]: 240 # TODO: implement symLogScale 241 # Should symLogScale override absScale? My vote is no. 242 # Should symLogScale override logScale? My vote is yes. 243 # - symLogScale could be a parameter passed to logScale... 244 pass 245 246 # work on Y: 247 if self.absScale[1]: 248 data = self._abs(data, 1) 249 if self.logScale[1]: 250 data = self._log10(data, 1) 251 252 if self.symLogScale[1]: 253 # TODO: implement symLogScale 254 pass 255 256 return data 257 258 @points.setter 259 def points(self, points): 260 self._points = points 261 262 def _log10(self, data, index): 263 """ Take the Log10 of the data, dropping any negative values """ 264 data = np.compress(data[:, index] > 0, data, 0) 265 data[:, index] = np.log10(data[:, index]) 266 return data 267 268 def _abs(self, data, index): 269 """ Take the Abs of the data """ 270 data[:, index] = np.abs(data[:, index]) 271 return data 272 273 def boundingBox(self): 274 """ 275 Returns the bouding box for the entire dataset as a tuple with this 276 format:: 277 278 ((minX, minY), (maxX, maxY)) 279 280 :returns: boundingbox 281 :rtype: numpy array of ``[[minX, minY], [maxX, maxY]]`` 282 """ 283 if len(self.points) == 0: 284 # no curves to draw 285 # defaults to (-1,-1) and (1,1) but axis can be set in Draw 286 minXY = np.array([-1.0, -1.0]) 287 maxXY = np.array([1.0, 1.0]) 288 else: 289 minXY = np.minimum.reduce(self.points) 290 maxXY = np.maximum.reduce(self.points) 291 return minXY, maxXY 292 293 def scaleAndShift(self, scale=(1, 1), shift=(0, 0)): 294 """ 295 Scales and shifts the data for plotting. 296 297 :param scale: The values to scale the data by. 298 :type scale: list of floats: ``[x_scale, y_scale]`` 299 :param shift: The value to shift the data by. This should be in scaled 300 units 301 :type shift: list of floats: ``[x_shift, y_shift]`` 302 :returns: None 303 """ 304 if len(self.points) == 0: 305 # no curves to draw 306 return 307 308 # TODO: Can we remove the if statement alltogether? Does 309 # scaleAndShift ever get called when the current value equals 310 # the new value? 311 312 # cast everything to list: some might be np.ndarray objects 313 if (list(scale) != list(self.currentScale) 314 or list(shift) != list(self.currentShift)): 315 # update point scaling 316 self.scaled = scale * self.points + shift 317 self.currentScale = scale 318 self.currentShift = shift 319 # else unchanged use the current scaling 320 321 def getLegend(self): 322 return self.attributes['legend'] 323 324 def getClosestPoint(self, pntXY, pointScaled=True): 325 """ 326 Returns the index of closest point on the curve, pointXY, 327 scaledXY, distance x, y in user coords. 328 329 if pointScaled == True, then based on screen coords 330 if pointScaled == False, then based on user coords 331 """ 332 if pointScaled: 333 # Using screen coords 334 p = self.scaled 335 pxy = self.currentScale * np.array(pntXY) + self.currentShift 336 else: 337 # Using user coords 338 p = self.points 339 pxy = np.array(pntXY) 340 # determine distance for each point 341 d = np.sqrt(np.add.reduce((p - pxy) ** 2, 1)) # sqrt(dx^2+dy^2) 342 pntIndex = np.argmin(d) 343 dist = d[pntIndex] 344 return [pntIndex, 345 self.points[pntIndex], 346 self.scaled[pntIndex] / self._pointSize, 347 dist] 348 349 350class PolyLine(PolyPoints): 351 """ 352 Creates PolyLine object 353 354 :param points: The points that make up the line 355 :type points: list of ``[x, y]`` values 356 :param **attr: keyword attributes 357 358 =========================== ============= ==================== 359 Keyword and Default Description Type 360 =========================== ============= ==================== 361 ``colour='black'`` Line color :class:`wx.Colour` 362 ``width=1`` Line width float 363 ``style=wx.PENSTYLE_SOLID`` Line style :class:`wx.PenStyle` 364 ``legend=''`` Legend string str 365 ``drawstyle='line'`` see below str 366 =========================== ============= ==================== 367 368 ================== ================================================== 369 Draw style Description 370 ================== ================================================== 371 ``'line'`` Draws an straight line between consecutive points 372 ``'steps-pre'`` Draws a line down from point A and then right to 373 point B 374 ``'steps-post'`` Draws a line right from point A and then down 375 to point B 376 ``'steps-mid-x'`` Draws a line horizontally to half way between A 377 and B, then draws a line vertically, then again 378 horizontally to point B. 379 ``'steps-mid-y'`` Draws a line vertically to half way between A 380 and B, then draws a line horizonatally, then 381 again vertically to point B. 382 *Note: This typically does not look very good* 383 ================== ================================================== 384 385 .. warning:: 386 387 All methods except ``__init__`` are private. 388 """ 389 390 _attributes = {'colour': 'black', 391 'width': 1, 392 'style': wx.PENSTYLE_SOLID, 393 'legend': '', 394 'drawstyle': 'line', 395 } 396 _drawstyles = ("line", "steps-pre", "steps-post", 397 "steps-mid-x", "steps-mid-y") 398 399 def __init__(self, points, **attr): 400 PolyPoints.__init__(self, points, attr) 401 402 def draw(self, dc, printerScale, coord=None): 403 """ 404 Draw the lines. 405 406 :param dc: The DC to draw on. 407 :type dc: :class:`wx.DC` 408 :param printerScale: 409 :type printerScale: float 410 :param coord: The legend coordinate? 411 :type coord: ??? 412 """ 413 colour = self.attributes['colour'] 414 width = self.attributes['width'] * printerScale * self._pointSize[0] 415 style = self.attributes['style'] 416 drawstyle = self.attributes['drawstyle'] 417 418 if not isinstance(colour, wx.Colour): 419 colour = wx.Colour(colour) 420 pen = wx.Pen(colour, width, style) 421 pen.SetCap(wx.CAP_BUTT) 422 dc.SetPen(pen) 423 if coord is None: 424 if len(self.scaled): # bugfix for Mac OS X 425 for c1, c2 in zip(self.scaled, self.scaled[1:]): 426 self._path(dc, c1, c2, drawstyle) 427 else: 428 dc.DrawLines(coord) # draw legend line 429 430 def getSymExtent(self, printerScale): 431 """ 432 Get the Width and Height of the symbol. 433 434 :param printerScale: 435 :type printerScale: float 436 """ 437 h = self.attributes['width'] * printerScale * self._pointSize[0] 438 w = 5 * h 439 return (w, h) 440 441 def _path(self, dc, coord1, coord2, drawstyle): 442 """ 443 Calculates the path from coord1 to coord 2 along X and Y 444 445 :param dc: The DC to draw on. 446 :type dc: :class:`wx.DC` 447 :param coord1: The first coordinate in the coord pair 448 :type coord1: list, length 2: ``[x, y]`` 449 :param coord2: The second coordinate in the coord pair 450 :type coord2: list, length 2: ``[x, y]`` 451 :param drawstyle: The type of connector to use 452 :type drawstyle: str 453 """ 454 if drawstyle == 'line': 455 # Straight line between points. 456 line = [coord1, coord2] 457 elif drawstyle == 'steps-pre': 458 # Up/down to next Y, then right to next X 459 intermediate = [coord1[0], coord2[1]] 460 line = [coord1, intermediate, coord2] 461 elif drawstyle == 'steps-post': 462 # Right to next X, then up/down to Y 463 intermediate = [coord2[0], coord1[1]] 464 line = [coord1, intermediate, coord2] 465 elif drawstyle == 'steps-mid-x': 466 # need 3 lines between points: right -> up/down -> right 467 mid_x = ((coord2[0] - coord1[0]) / 2) + coord1[0] 468 intermediate1 = [mid_x, coord1[1]] 469 intermediate2 = [mid_x, coord2[1]] 470 line = [coord1, intermediate1, intermediate2, coord2] 471 elif drawstyle == 'steps-mid-y': 472 # need 3 lines between points: up/down -> right -> up/down 473 mid_y = ((coord2[1] - coord1[1]) / 2) + coord1[1] 474 intermediate1 = [coord1[0], mid_y] 475 intermediate2 = [coord2[0], mid_y] 476 line = [coord1, intermediate1, intermediate2, coord2] 477 else: 478 err_txt = "Invalid drawstyle '{}'. Must be one of {}." 479 raise ValueError(err_txt.format(drawstyle, self._drawstyles)) 480 481 dc.DrawLines(line) 482 483 484class PolySpline(PolyLine): 485 """ 486 Creates PolySpline object 487 488 :param points: The points that make up the spline 489 :type points: list of ``[x, y]`` values 490 :param **attr: keyword attributes 491 492 =========================== ============= ==================== 493 Keyword and Default Description Type 494 =========================== ============= ==================== 495 ``colour='black'`` Line color :class:`wx.Colour` 496 ``width=1`` Line width float 497 ``style=wx.PENSTYLE_SOLID`` Line style :class:`wx.PenStyle` 498 ``legend=''`` Legend string str 499 =========================== ============= ==================== 500 501 .. warning:: 502 503 All methods except ``__init__`` are private. 504 """ 505 506 _attributes = {'colour': 'black', 507 'width': 1, 508 'style': wx.PENSTYLE_SOLID, 509 'legend': ''} 510 511 def __init__(self, points, **attr): 512 PolyLine.__init__(self, points, **attr) 513 514 def draw(self, dc, printerScale, coord=None): 515 """ Draw the spline """ 516 colour = self.attributes['colour'] 517 width = self.attributes['width'] * printerScale * self._pointSize[0] 518 style = self.attributes['style'] 519 if not isinstance(colour, wx.Colour): 520 colour = wx.Colour(colour) 521 pen = wx.Pen(colour, width, style) 522 pen.SetCap(wx.CAP_ROUND) 523 dc.SetPen(pen) 524 if coord is None: 525 if len(self.scaled) >= 3: 526 dc.DrawSpline(self.scaled) 527 else: 528 dc.DrawLines(coord) # draw legend line 529 530 531class PolyMarker(PolyPoints): 532 """ 533 Creates a PolyMarker object. 534 535 :param points: The marker coordinates. 536 :type points: list of ``[x, y]`` values 537 :param **attr: keyword attributes 538 539 ================================= ============= ==================== 540 Keyword and Default Description Type 541 ================================= ============= ==================== 542 ``marker='circle'`` see below str 543 ``size=2`` Marker size float 544 ``colour='black'`` Outline color :class:`wx.Colour` 545 ``width=1`` Outline width float 546 ``style=wx.PENSTYLE_SOLID`` Outline style :class:`wx.PenStyle` 547 ``fillcolour=colour`` fill color :class:`wx.Colour` 548 ``fillstyle=wx.BRUSHSTYLE_SOLID`` fill style :class:`wx.BrushStyle` 549 ``legend=''`` Legend string str 550 ================================= ============= ==================== 551 552 =================== ================================== 553 Marker Description 554 =================== ================================== 555 ``'circle'`` A circle of diameter ``size`` 556 ``'dot'`` A dot. Does not have a size. 557 ``'square'`` A square with side length ``size`` 558 ``'triangle'`` An upward-pointed triangle 559 ``'triangle_down'`` A downward-pointed triangle 560 ``'cross'`` An "X" shape 561 ``'plus'`` A "+" shape 562 =================== ================================== 563 564 .. warning:: 565 566 All methods except ``__init__`` are private. 567 """ 568 _attributes = {'colour': 'black', 569 'width': 1, 570 'size': 2, 571 'fillcolour': None, 572 'fillstyle': wx.BRUSHSTYLE_SOLID, 573 'marker': 'circle', 574 'legend': ''} 575 576 def __init__(self, points, **attr): 577 PolyPoints.__init__(self, points, attr) 578 579 def draw(self, dc, printerScale, coord=None): 580 """ Draw the points """ 581 colour = self.attributes['colour'] 582 width = self.attributes['width'] * printerScale * self._pointSize[0] 583 size = self.attributes['size'] * printerScale * self._pointSize[0] 584 fillcolour = self.attributes['fillcolour'] 585 fillstyle = self.attributes['fillstyle'] 586 marker = self.attributes['marker'] 587 588 if colour and not isinstance(colour, wx.Colour): 589 colour = wx.Colour(colour) 590 if fillcolour and not isinstance(fillcolour, wx.Colour): 591 fillcolour = wx.Colour(fillcolour) 592 593 dc.SetPen(wx.Pen(colour, width)) 594 if fillcolour: 595 dc.SetBrush(wx.Brush(fillcolour, fillstyle)) 596 else: 597 dc.SetBrush(wx.Brush(colour, fillstyle)) 598 if coord is None: 599 if len(self.scaled): # bugfix for Mac OS X 600 self._drawmarkers(dc, self.scaled, marker, size) 601 else: 602 self._drawmarkers(dc, coord, marker, size) # draw legend marker 603 604 def getSymExtent(self, printerScale): 605 """Width and Height of Marker""" 606 s = 5 * self.attributes['size'] * printerScale * self._pointSize[0] 607 return (s, s) 608 609 def _drawmarkers(self, dc, coords, marker, size=1): 610 f = getattr(self, "_{}".format(marker)) 611 f(dc, coords, size) 612 613 def _circle(self, dc, coords, size=1): 614 fact = 2.5 * size 615 wh = 5.0 * size 616 rect = np.zeros((len(coords), 4), np.float) + [0.0, 0.0, wh, wh] 617 rect[:, 0:2] = coords - [fact, fact] 618 dc.DrawEllipseList(rect.astype(np.int32)) 619 620 def _dot(self, dc, coords, size=1): 621 dc.DrawPointList(coords) 622 623 def _square(self, dc, coords, size=1): 624 fact = 2.5 * size 625 wh = 5.0 * size 626 rect = np.zeros((len(coords), 4), np.float) + [0.0, 0.0, wh, wh] 627 rect[:, 0:2] = coords - [fact, fact] 628 dc.DrawRectangleList(rect.astype(np.int32)) 629 630 def _triangle(self, dc, coords, size=1): 631 shape = [(-2.5 * size, 1.44 * size), 632 (2.5 * size, 1.44 * size), (0.0, -2.88 * size)] 633 poly = np.repeat(coords, 3, 0) 634 poly.shape = (len(coords), 3, 2) 635 poly += shape 636 dc.DrawPolygonList(poly.astype(np.int32)) 637 638 def _triangle_down(self, dc, coords, size=1): 639 shape = [(-2.5 * size, -1.44 * size), 640 (2.5 * size, -1.44 * size), (0.0, 2.88 * size)] 641 poly = np.repeat(coords, 3, 0) 642 poly.shape = (len(coords), 3, 2) 643 poly += shape 644 dc.DrawPolygonList(poly.astype(np.int32)) 645 646 def _cross(self, dc, coords, size=1): 647 fact = 2.5 * size 648 for f in [[-fact, -fact, fact, fact], [-fact, fact, fact, -fact]]: 649 lines = np.concatenate((coords, coords), axis=1) + f 650 dc.DrawLineList(lines.astype(np.int32)) 651 652 def _plus(self, dc, coords, size=1): 653 fact = 2.5 * size 654 for f in [[-fact, 0, fact, 0], [0, -fact, 0, fact]]: 655 lines = np.concatenate((coords, coords), axis=1) + f 656 dc.DrawLineList(lines.astype(np.int32)) 657 658 659class PolyBarsBase(PolyPoints): 660 """ 661 Base class for PolyBars and PolyHistogram. 662 663 .. warning:: 664 665 All methods are private. 666 """ 667 _attributes = {'edgecolour': 'black', 668 'edgewidth': 2, 669 'edgestyle': wx.PENSTYLE_SOLID, 670 'legend': '', 671 'fillcolour': 'red', 672 'fillstyle': wx.BRUSHSTYLE_SOLID, 673 'barwidth': 1.0 674 } 675 676 def __init__(self, points, attr): 677 """ 678 """ 679 PolyPoints.__init__(self, points, attr) 680 681 def _scaleAndShift(self, data, scale=(1, 1), shift=(0, 0)): 682 """same as override method, but retuns a value.""" 683 scaled = scale * data + shift 684 return scaled 685 686 def getSymExtent(self, printerScale): 687 """Width and Height of Marker""" 688 h = self.attributes['edgewidth'] * printerScale * self._pointSize[0] 689 w = 5 * h 690 return (w, h) 691 692 def set_pen_and_brush(self, dc, printerScale): 693 pencolour = self.attributes['edgecolour'] 694 penwidth = (self.attributes['edgewidth'] 695 * printerScale * self._pointSize[0]) 696 penstyle = self.attributes['edgestyle'] 697 fillcolour = self.attributes['fillcolour'] 698 fillstyle = self.attributes['fillstyle'] 699 700 if not isinstance(pencolour, wx.Colour): 701 pencolour = wx.Colour(pencolour) 702 pen = wx.Pen(pencolour, penwidth, penstyle) 703 pen.SetCap(wx.CAP_BUTT) 704 705 if not isinstance(fillcolour, wx.Colour): 706 fillcolour = wx.Colour(fillcolour) 707 brush = wx.Brush(fillcolour, fillstyle) 708 709 dc.SetPen(pen) 710 dc.SetBrush(brush) 711 712 def scale_rect(self, rect): 713 # Scale the points to the plot area 714 scaled_rect = self._scaleAndShift(rect, 715 self.currentScale, 716 self.currentShift) 717 718 # Convert to (left, top, width, height) for drawing 719 wx_rect = [scaled_rect[0][0], # X (left) 720 scaled_rect[0][1], # Y (top) 721 scaled_rect[1][0] - scaled_rect[0][0], # Width 722 scaled_rect[1][1] - scaled_rect[0][1]] # Height 723 724 return wx_rect 725 726 def draw(self, dc, printerScale, coord=None): 727 pass 728 729 730class PolyBars(PolyBarsBase): 731 """ 732 Creates a PolyBars object. 733 734 :param points: The data to plot. 735 :type points: sequence of ``(center, height)`` points 736 :param **attr: keyword attributes 737 738 ================================= ============= ======================= 739 Keyword and Default Description Type 740 ================================= ============= ======================= 741 ``barwidth=1.0`` bar width float or list of floats 742 ``edgecolour='black'`` edge color :class:`wx.Colour` 743 ``edgewidth=1`` edge width float 744 ``edgestyle=wx.PENSTYLE_SOLID`` edge style :class:`wx.PenStyle` 745 ``fillcolour='red'`` fill color :class:`wx.Colour` 746 ``fillstyle=wx.BRUSHSTYLE_SOLID`` fill style :class:`wx.BrushStyle` 747 ``legend=''`` legend string str 748 ================================= ============= ======================= 749 750 .. important:: 751 752 If ``barwidth`` is a list of floats: 753 754 + each bar will have a separate width 755 + ``len(barwidth)`` must equal ``len(points)``. 756 757 .. warning:: 758 759 All methods except ``__init__`` are private. 760 """ 761 def __init__(self, points, **attr): 762 PolyBarsBase.__init__(self, points, attr) 763 764 def calc_rect(self, x, y, w): 765 """ Calculate the rectangle for plotting. """ 766 return self.scale_rect([[x - w / 2, y], # left, top 767 [x + w / 2, 0]]) # right, bottom 768 769 def draw(self, dc, printerScale, coord=None): 770 """ Draw the bars """ 771 self.set_pen_and_brush(dc, printerScale) 772 barwidth = self.attributes['barwidth'] 773 774 if coord is None: 775 if isinstance(barwidth, (int, float)): 776 # use a single width for all bars 777 pts = ((x, y, barwidth) for x, y in self.points) 778 elif isinstance(barwidth, (list, tuple)): 779 # use a separate width for each bar 780 if len(barwidth) != len(self.points): 781 err_str = ("Barwidth ({} items) and Points ({} items) do " 782 "not have the same length!") 783 err_str = err_str.format(len(barwidth), len(self.points)) 784 raise ValueError(err_str) 785 pts = ((x, y, w) for (x, y), w in zip(self.points, barwidth)) 786 else: 787 # invalid attribute type 788 err_str = ("Invalid type for 'barwidth'. Expected float, " 789 "int, or list or tuple of (int or float). Got {}.") 790 raise TypeError(err_str.format(type(barwidth))) 791 792 rects = [self.calc_rect(x, y, w) for x, y, w in pts] 793 dc.DrawRectangleList(rects) 794 else: 795 dc.DrawLines(coord) # draw legend line 796 797 798class PolyHistogram(PolyBarsBase): 799 """ 800 Creates a PolyHistogram object. 801 802 :param hist: The histogram data. 803 :type hist: sequence of ``y`` values that define the heights of the bars 804 :param binspec: The bin specification. 805 :type binspec: sequence of ``x`` values that define the edges of the bins 806 :param **attr: keyword attributes 807 808 ================================= ============= ======================= 809 Keyword and Default Description Type 810 ================================= ============= ======================= 811 ``edgecolour='black'`` edge color :class:`wx.Colour` 812 ``edgewidth=3`` edge width float 813 ``edgestyle=wx.PENSTYLE_SOLID`` edge style :class:`wx.PenStyle` 814 ``fillcolour='blue'`` fill color :class:`wx.Colour` 815 ``fillstyle=wx.BRUSHSTYLE_SOLID`` fill style :class:`wx.BrushStyle` 816 ``legend=''`` legend string str 817 ================================= ============= ======================= 818 819 .. tip:: 820 821 Use ``np.histogram()`` to easily create your histogram parameters:: 822 823 hist_data, binspec = np.histogram(data) 824 hist_plot = PolyHistogram(hist_data, binspec) 825 826 .. important:: 827 828 ``len(binspec)`` must equal ``len(hist) + 1``. 829 830 .. warning:: 831 832 All methods except ``__init__`` are private. 833 """ 834 def __init__(self, hist, binspec, **attr): 835 if len(binspec) != len(hist) + 1: 836 raise ValueError("Len(binspec) must equal len(hist) + 1") 837 838 self.hist = hist 839 self.binspec = binspec 840 841 # define the bins and center x locations 842 self.bins = list(pairwise(self.binspec)) 843 bar_center_x = (pair[0] + (pair[1] - pair[0])/2 for pair in self.bins) 844 845 points = list(zip(bar_center_x, self.hist)) 846 PolyBarsBase.__init__(self, points, attr) 847 848 def calc_rect(self, y, low, high): 849 """ Calculate the rectangle for plotting. """ 850 return self.scale_rect([[low, y], # left, top 851 [high, 0]]) # right, bottom 852 853 def draw(self, dc, printerScale, coord=None): 854 """ Draw the bars """ 855 self.set_pen_and_brush(dc, printerScale) 856 857 if coord is None: 858 rects = [self.calc_rect(y, low, high) 859 for y, (low, high) 860 in zip(self.hist, self.bins)] 861 862 dc.DrawRectangleList(rects) 863 else: 864 dc.DrawLines(coord) # draw legend line 865 866 867class PolyBoxPlot(PolyPoints): 868 """ 869 Creates a PolyBoxPlot object. 870 871 :param data: Raw data to create a box plot from. 872 :type data: sequence of int or float 873 :param **attr: keyword attributes 874 875 ================================= ============= ======================= 876 Keyword and Default Description Type 877 ================================= ============= ======================= 878 ``colour='black'`` edge color :class:`wx.Colour` 879 ``width=1`` edge width float 880 ``style=wx.PENSTYLE_SOLID`` edge style :class:`wx.PenStyle` 881 ``legend=''`` legend string str 882 ================================= ============= ======================= 883 884 .. note:: 885 886 ``np.NaN`` and ``np.inf`` values are ignored. 887 888 .. admonition:: TODO 889 890 + [ ] Figure out a better way to get multiple box plots side-by-side 891 (current method is a hack). 892 + [ ] change the X axis to some labels. 893 + [ ] Change getClosestPoint to only grab box plot items and outlers? 894 Currently grabs every data point. 895 + [ ] Add more customization such as Pens/Brushes, outlier shapes/size, 896 and box width. 897 + [ ] Figure out how I want to handle log-y: log data then calcBP? Or 898 should I calc the BP first then the plot it on a log scale? 899 """ 900 _attributes = {'colour': 'black', 901 'width': 1, 902 'style': wx.PENSTYLE_SOLID, 903 'legend': '', 904 } 905 906 def __init__(self, points, **attr): 907 # Set various attributes 908 self.box_width = 0.5 909 910 # Determine the X position and create a 1d dataset. 911 self.xpos = points[0, 0] 912 points = points[:, 1] 913 914 # Calculate the box plot points and the outliers 915 self._bpdata = self.calcBpData(points) 916 self._outliers = self.calcOutliers(points) 917 points = np.concatenate((self._bpdata, self._outliers)) 918 points = np.array([(self.xpos, x) for x in points]) 919 920 # Create a jitter for the outliers 921 self.jitter = (0.05 * np.random.random_sample(len(self._outliers)) 922 + self.xpos - 0.025) 923 924 # Init the parent class 925 PolyPoints.__init__(self, points, attr) 926 927 def _clean_data(self, data=None): 928 """ 929 Removes NaN and Inf from the data. 930 """ 931 if data is None: 932 data = self.points 933 934 # clean out NaN and infinity values. 935 data = data[~np.isnan(data)] 936 data = data[~np.isinf(data)] 937 938 return data 939 940 def boundingBox(self): 941 """ 942 Returns bounding box for the plot. 943 944 Override method. 945 """ 946 xpos = self.xpos 947 948 minXY = np.array([xpos - self.box_width / 2, self._bpdata.min * 0.95]) 949 maxXY = np.array([xpos + self.box_width / 2, self._bpdata.max * 1.05]) 950 return minXY, maxXY 951 952 def getClosestPoint(self, pntXY, pointScaled=True): 953 """ 954 Returns the index of closest point on the curve, pointXY, 955 scaledXY, distance x, y in user coords. 956 957 Override method. 958 959 if pointScaled == True, then based on screen coords 960 if pointScaled == False, then based on user coords 961 """ 962 963 xpos = self.xpos 964 965 # combine the outliers with the box plot data 966 data_to_use = np.concatenate((self._bpdata, self._outliers)) 967 data_to_use = np.array([(xpos, x) for x in data_to_use]) 968 969 if pointScaled: 970 # Use screen coords 971 p = self.scaled 972 pxy = self.currentScale * np.array(pntXY) + self.currentShift 973 else: 974 # Using user coords 975 p = self._points 976 pxy = np.array(pntXY) 977 978 # determine distnace for each point 979 d = np.sqrt(np.add.reduce((p - pxy) ** 2, 1)) # sqrt(dx^2+dy^2) 980 pntIndex = np.argmin(d) 981 dist = d[pntIndex] 982 return [pntIndex, 983 self.points[pntIndex], 984 self.scaled[pntIndex] / self._pointSize, 985 dist] 986 987 def getSymExtent(self, printerScale): 988 """Width and Height of Marker""" 989 # TODO: does this need to be updated? 990 h = self.attributes['width'] * printerScale * self._pointSize[0] 991 w = 5 * h 992 return (w, h) 993 994 def calcBpData(self, data=None): 995 """ 996 Box plot points: 997 998 Median (50%) 999 75% 1000 25% 1001 low_whisker = lowest value that's >= (25% - (IQR * 1.5)) 1002 high_whisker = highest value that's <= 75% + (IQR * 1.5) 1003 1004 outliers are outside of 1.5 * IQR 1005 1006 Parameters 1007 ---------- 1008 data : array-like 1009 The data to plot 1010 1011 Returns 1012 ------- 1013 bpdata : collections.namedtuple 1014 Descriptive statistics for data: 1015 (min_data, low_whisker, q25, median, q75, high_whisker, max_data) 1016 1017 """ 1018 data = self._clean_data(data) 1019 1020 min_data = float(np.min(data)) 1021 max_data = float(np.max(data)) 1022 q25 = float(np.percentile(data, 25)) 1023 q75 = float(np.percentile(data, 75)) 1024 1025 iqr = q75 - q25 1026 1027 low_whisker = float(data[data >= q25 - 1.5 * iqr].min()) 1028 high_whisker = float(data[data <= q75 + 1.5 * iqr].max()) 1029 1030 median = float(np.median(data)) 1031 1032 BPData = namedtuple("bpdata", ("min", "low_whisker", "q25", "median", 1033 "q75", "high_whisker", "max")) 1034 1035 bpdata = BPData(min_data, low_whisker, q25, median, 1036 q75, high_whisker, max_data) 1037 1038 return bpdata 1039 1040 def calcOutliers(self, data=None): 1041 """ 1042 Calculates the outliers. Must be called after calcBpData. 1043 """ 1044 data = self._clean_data(data) 1045 1046 outliers = data 1047 outlier_bool = np.logical_or(outliers > self._bpdata.high_whisker, 1048 outliers < self._bpdata.low_whisker) 1049 outliers = outliers[outlier_bool] 1050 return outliers 1051 1052 def _scaleAndShift(self, data, scale=(1, 1), shift=(0, 0)): 1053 """same as override method, but retuns a value.""" 1054 scaled = scale * data + shift 1055 return scaled 1056 1057 @TempStyle('pen') 1058 def draw(self, dc, printerScale, coord=None): 1059 """ 1060 Draws a box plot on the DC. 1061 1062 Notes 1063 ----- 1064 The following draw order is required: 1065 1066 1. First the whisker line 1067 2. Then the IQR box 1068 3. Lasly the median line. 1069 1070 This is because 1071 1072 + The whiskers are drawn as single line rather than two lines 1073 + The median line must be visable over the box if the box has a fill. 1074 1075 Other than that, the draw order can be changed. 1076 """ 1077 self._draw_whisker(dc, printerScale) 1078 self._draw_iqr_box(dc, printerScale) 1079 self._draw_median(dc, printerScale) # median after box 1080 self._draw_whisker_ends(dc, printerScale) 1081 self._draw_outliers(dc, printerScale) 1082 1083 @TempStyle('pen') 1084 def _draw_whisker(self, dc, printerScale): 1085 """Draws the whiskers as a single line""" 1086 xpos = self.xpos 1087 1088 # We draw it as one line and then hide the middle part with 1089 # the IQR rectangle 1090 whisker_line = np.array([[xpos, self._bpdata.low_whisker], 1091 [xpos, self._bpdata.high_whisker]]) 1092 1093 whisker_line = self._scaleAndShift(whisker_line, 1094 self.currentScale, 1095 self.currentShift) 1096 1097 whisker_pen = wx.Pen(wx.BLACK, 2, wx.PENSTYLE_SOLID) 1098 whisker_pen.SetCap(wx.CAP_BUTT) 1099 dc.SetPen(whisker_pen) 1100 dc.DrawLines(whisker_line) 1101 1102 @TempStyle('pen') 1103 def _draw_iqr_box(self, dc, printerScale): 1104 """Draws the Inner Quartile Range box""" 1105 xpos = self.xpos 1106 box_w = self.box_width 1107 1108 iqr_box = [[xpos - box_w / 2, self._bpdata.q75], # left, top 1109 [xpos + box_w / 2, self._bpdata.q25]] # right, bottom 1110 1111 # Scale it to the plot area 1112 iqr_box = self._scaleAndShift(iqr_box, 1113 self.currentScale, 1114 self.currentShift) 1115 1116 # rectangles are drawn (left, top, width, height) so adjust 1117 iqr_box = [iqr_box[0][0], # X (left) 1118 iqr_box[0][1], # Y (top) 1119 iqr_box[1][0] - iqr_box[0][0], # Width 1120 iqr_box[1][1] - iqr_box[0][1]] # Height 1121 1122 box_pen = wx.Pen(wx.BLACK, 3, wx.PENSTYLE_SOLID) 1123 box_brush = wx.Brush(wx.GREEN, wx.BRUSHSTYLE_SOLID) 1124 dc.SetPen(box_pen) 1125 dc.SetBrush(box_brush) 1126 1127 dc.DrawRectangleList([iqr_box]) 1128 1129 @TempStyle('pen') 1130 def _draw_median(self, dc, printerScale, coord=None): 1131 """Draws the median line""" 1132 xpos = self.xpos 1133 1134 median_line = np.array( 1135 [[xpos - self.box_width / 2, self._bpdata.median], 1136 [xpos + self.box_width / 2, self._bpdata.median]] 1137 ) 1138 1139 median_line = self._scaleAndShift(median_line, 1140 self.currentScale, 1141 self.currentShift) 1142 1143 median_pen = wx.Pen(wx.BLACK, 4, wx.PENSTYLE_SOLID) 1144 median_pen.SetCap(wx.CAP_BUTT) 1145 dc.SetPen(median_pen) 1146 dc.DrawLines(median_line) 1147 1148 @TempStyle('pen') 1149 def _draw_whisker_ends(self, dc, printerScale): 1150 """Draws the end caps of the whiskers""" 1151 xpos = self.xpos 1152 fence_top = np.array( 1153 [[xpos - self.box_width * 0.2, self._bpdata.high_whisker], 1154 [xpos + self.box_width * 0.2, self._bpdata.high_whisker]] 1155 ) 1156 1157 fence_top = self._scaleAndShift(fence_top, 1158 self.currentScale, 1159 self.currentShift) 1160 1161 fence_bottom = np.array( 1162 [[xpos - self.box_width * 0.2, self._bpdata.low_whisker], 1163 [xpos + self.box_width * 0.2, self._bpdata.low_whisker]] 1164 ) 1165 1166 fence_bottom = self._scaleAndShift(fence_bottom, 1167 self.currentScale, 1168 self.currentShift) 1169 1170 fence_pen = wx.Pen(wx.BLACK, 2, wx.PENSTYLE_SOLID) 1171 fence_pen.SetCap(wx.CAP_BUTT) 1172 dc.SetPen(fence_pen) 1173 dc.DrawLines(fence_top) 1174 dc.DrawLines(fence_bottom) 1175 1176 @TempStyle('pen') 1177 def _draw_outliers(self, dc, printerScale): 1178 """Draws dots for the outliers""" 1179 # Set the pen 1180 outlier_pen = wx.Pen(wx.BLUE, 5, wx.PENSTYLE_SOLID) 1181 dc.SetPen(outlier_pen) 1182 1183 outliers = self._outliers 1184 1185 # Scale the data for plotting 1186 pt_data = np.array([self.jitter, outliers]).T 1187 pt_data = self._scaleAndShift(pt_data, 1188 self.currentScale, 1189 self.currentShift) 1190 1191 # Draw the outliers 1192 size = 0.5 1193 fact = 2.5 * size 1194 wh = 5.0 * size 1195 rect = np.zeros((len(pt_data), 4), np.float) + [0.0, 0.0, wh, wh] 1196 rect[:, 0:2] = pt_data - [fact, fact] 1197 dc.DrawRectangleList(rect.astype(np.int32)) 1198 1199 1200class PlotGraphics(object): 1201 """ 1202 Creates a PlotGraphics object. 1203 1204 :param objects: The Poly objects to plot. 1205 :type objects: list of :class:`~wx.lib.plot.PolyPoints` objects 1206 :param title: The title shown at the top of the graph. 1207 :type title: str 1208 :param xLabel: The x-axis label. 1209 :type xLabel: str 1210 :param yLabel: The y-axis label. 1211 :type yLabel: str 1212 1213 .. warning:: 1214 1215 All methods except ``__init__`` are private. 1216 """ 1217 def __init__(self, objects, title='', xLabel='', yLabel=''): 1218 if type(objects) not in [list, tuple]: 1219 raise TypeError("objects argument should be list or tuple") 1220 self.objects = objects 1221 self._title = title 1222 self._xLabel = xLabel 1223 self._yLabel = yLabel 1224 self._pointSize = (1.0, 1.0) 1225 1226 @property 1227 def logScale(self): 1228 if len(self.objects) == 0: 1229 return 1230 return [obj.logScale for obj in self.objects] 1231 1232 @logScale.setter 1233 def logScale(self, logscale): 1234 # XXX: error checking done by PolyPoints class 1235# if not isinstance(logscale, tuple) and len(logscale) != 2: 1236# raise TypeError("logscale must be a 2-tuple of bools") 1237 if len(self.objects) == 0: 1238 return 1239 for obj in self.objects: 1240 obj.logScale = logscale 1241 1242 def setLogScale(self, logscale): 1243 """ 1244 Set the log scale boolean value. 1245 1246 .. deprecated:: Feb 27, 2016 1247 1248 Use the :attr:`~wx.lib.plot.polyobjects.PlotGraphics.logScale` 1249 property instead. 1250 """ 1251 pendingDeprecation("self.logScale property") 1252 self.logScale = logscale 1253 1254 @property 1255 def absScale(self): 1256 if len(self.objects) == 0: 1257 return 1258 return [obj.absScale for obj in self.objects] 1259 1260 @absScale.setter 1261 def absScale(self, absscale): 1262 # XXX: error checking done by PolyPoints class 1263# if not isinstance(absscale, tuple) and len(absscale) != 2: 1264# raise TypeError("absscale must be a 2-tuple of bools") 1265 if len(self.objects) == 0: 1266 return 1267 for obj in self.objects: 1268 obj.absScale = absscale 1269 1270 def boundingBox(self): 1271 p1, p2 = self.objects[0].boundingBox() 1272 for o in self.objects[1:]: 1273 p1o, p2o = o.boundingBox() 1274 p1 = np.minimum(p1, p1o) 1275 p2 = np.maximum(p2, p2o) 1276 return p1, p2 1277 1278 def scaleAndShift(self, scale=(1, 1), shift=(0, 0)): 1279 for o in self.objects: 1280 o.scaleAndShift(scale, shift) 1281 1282 def setPrinterScale(self, scale): 1283 """ 1284 Thickens up lines and markers only for printing 1285 1286 .. deprecated:: Feb 27, 2016 1287 1288 Use the :attr:`~wx.lib.plot.polyobjects.PlotGraphics.printerScale` 1289 property instead. 1290 """ 1291 pendingDeprecation("self.printerScale property") 1292 self.printerScale = scale 1293 1294 def setXLabel(self, xLabel=''): 1295 """ 1296 Set the X axis label on the graph 1297 1298 .. deprecated:: Feb 27, 2016 1299 1300 Use the :attr:`~wx.lib.plot.polyobjects.PlotGraphics.xLabel` 1301 property instead. 1302 """ 1303 pendingDeprecation("self.xLabel property") 1304 self.xLabel = xLabel 1305 1306 def setYLabel(self, yLabel=''): 1307 """ 1308 Set the Y axis label on the graph 1309 1310 .. deprecated:: Feb 27, 2016 1311 1312 Use the :attr:`~wx.lib.plot.polyobjects.PlotGraphics.yLabel` 1313 property instead. 1314 """ 1315 pendingDeprecation("self.yLabel property") 1316 self.yLabel = yLabel 1317 1318 def setTitle(self, title=''): 1319 """ 1320 Set the title at the top of graph 1321 1322 .. deprecated:: Feb 27, 2016 1323 1324 Use the :attr:`~wx.lib.plot.polyobjects.PlotGraphics.title` 1325 property instead. 1326 """ 1327 pendingDeprecation("self.title property") 1328 self.title = title 1329 1330 def getXLabel(self): 1331 """ 1332 Get X axis label string 1333 1334 .. deprecated:: Feb 27, 2016 1335 1336 Use the :attr:`~wx.lib.plot.polyobjects.PlotGraphics.xLabel` 1337 property instead. 1338 """ 1339 pendingDeprecation("self.xLabel property") 1340 return self.xLabel 1341 1342 def getYLabel(self): 1343 """ 1344 Get Y axis label string 1345 1346 .. deprecated:: Feb 27, 2016 1347 1348 Use the :attr:`~wx.lib.plot.polyobjects.PlotGraphics.yLabel` 1349 property instead. 1350 """ 1351 pendingDeprecation("self.yLabel property") 1352 return self.yLabel 1353 1354 def getTitle(self, title=''): 1355 """ 1356 Get the title at the top of graph 1357 1358 .. deprecated:: Feb 27, 2016 1359 1360 Use the :attr:`~wx.lib.plot.polyobjects.PlotGraphics.title` 1361 property instead. 1362 """ 1363 pendingDeprecation("self.title property") 1364 return self.title 1365 1366 @property 1367 def printerScale(self): 1368 return self._printerScale 1369 1370 @printerScale.setter 1371 def printerScale(self, scale): 1372 """Thickens up lines and markers only for printing""" 1373 self._printerScale = scale 1374 1375 @property 1376 def xLabel(self): 1377 """Get the X axis label on the graph""" 1378 return self._xLabel 1379 1380 @xLabel.setter 1381 def xLabel(self, text): 1382 self._xLabel = text 1383 1384 @property 1385 def yLabel(self): 1386 """Get the Y axis label on the graph""" 1387 return self._yLabel 1388 1389 @yLabel.setter 1390 def yLabel(self, text): 1391 self._yLabel = text 1392 1393 @property 1394 def title(self): 1395 """Get the title at the top of graph""" 1396 return self._title 1397 1398 @title.setter 1399 def title(self, text): 1400 self._title = text 1401 1402 def draw(self, dc): 1403 for o in self.objects: 1404# t=_time.perf_counter() # profile info 1405 o._pointSize = self._pointSize 1406 o.draw(dc, self._printerScale) 1407# print(o, "time=", _time.perf_counter()-t) 1408 1409 def getSymExtent(self, printerScale): 1410 """Get max width and height of lines and markers symbols for legend""" 1411 self.objects[0]._pointSize = self._pointSize 1412 symExt = self.objects[0].getSymExtent(printerScale) 1413 for o in self.objects[1:]: 1414 o._pointSize = self._pointSize 1415 oSymExt = o.getSymExtent(printerScale) 1416 symExt = np.maximum(symExt, oSymExt) 1417 return symExt 1418 1419 def getLegendNames(self): 1420 """Returns list of legend names""" 1421 lst = [None] * len(self) 1422 for i in range(len(self)): 1423 lst[i] = self.objects[i].getLegend() 1424 return lst 1425 1426 def __len__(self): 1427 return len(self.objects) 1428 1429 def __getitem__(self, item): 1430 return self.objects[item] 1431 1432 1433# ------------------------------------------------------------------------- 1434# Used to layout the printer page 1435 1436 1437class PlotPrintout(wx.Printout): 1438 """Controls how the plot is made in printing and previewing""" 1439 # Do not change method names in this class, 1440 # we have to override wx.Printout methods here! 1441 1442 def __init__(self, graph): 1443 """graph is instance of plotCanvas to be printed or previewed""" 1444 wx.Printout.__init__(self) 1445 self.graph = graph 1446 1447 def HasPage(self, page): 1448 if page == 1: 1449 return True 1450 else: 1451 return False 1452 1453 def GetPageInfo(self): 1454 return (1, 1, 1, 1) # disable page numbers 1455 1456 def OnPrintPage(self, page): 1457 dc = self.GetDC() # allows using floats for certain functions 1458# print("PPI Printer",self.GetPPIPrinter()) 1459# print("PPI Screen", self.GetPPIScreen()) 1460# print("DC GetSize", dc.GetSize()) 1461# print("GetPageSizePixels", self.GetPageSizePixels()) 1462 # Note PPIScreen does not give the correct number 1463 # Calulate everything for printer and then scale for preview 1464 PPIPrinter = self.GetPPIPrinter() # printer dots/inch (w,h) 1465 # PPIScreen= self.GetPPIScreen() # screen dots/inch (w,h) 1466 dcSize = dc.GetSize() # DC size 1467 if self.graph._antiAliasingEnabled and not isinstance(dc, wx.GCDC): 1468 try: 1469 dc = wx.GCDC(dc) 1470 except Exception: 1471 pass 1472 else: 1473 if self.graph._hiResEnabled: 1474 # high precision - each logical unit is 1/20 of a point 1475 dc.SetMapMode(wx.MM_TWIPS) 1476 pageSize = self.GetPageSizePixels() # page size in terms of pixcels 1477 clientDcSize = self.graph.GetClientSize() 1478 1479 # find what the margins are (mm) 1480 pgSetupData = self.graph.pageSetupData 1481 margLeftSize, margTopSize = pgSetupData.GetMarginTopLeft() 1482 margRightSize, margBottomSize = pgSetupData.GetMarginBottomRight() 1483 1484 # calculate offset and scale for dc 1485 pixLeft = margLeftSize * PPIPrinter[0] / 25.4 # mm*(dots/in)/(mm/in) 1486 pixRight = margRightSize * PPIPrinter[0] / 25.4 1487 pixTop = margTopSize * PPIPrinter[1] / 25.4 1488 pixBottom = margBottomSize * PPIPrinter[1] / 25.4 1489 1490 plotAreaW = pageSize[0] - (pixLeft + pixRight) 1491 plotAreaH = pageSize[1] - (pixTop + pixBottom) 1492 1493 # ratio offset and scale to screen size if preview 1494 if self.IsPreview(): 1495 ratioW = float(dcSize[0]) / pageSize[0] 1496 ratioH = float(dcSize[1]) / pageSize[1] 1497 pixLeft *= ratioW 1498 pixTop *= ratioH 1499 plotAreaW *= ratioW 1500 plotAreaH *= ratioH 1501 1502 # rescale plot to page or preview plot area 1503 self.graph._setSize(plotAreaW, plotAreaH) 1504 1505 # Set offset and scale 1506 dc.SetDeviceOrigin(pixLeft, pixTop) 1507 1508 # Thicken up pens and increase marker size for printing 1509 ratioW = float(plotAreaW) / clientDcSize[0] 1510 ratioH = float(plotAreaH) / clientDcSize[1] 1511 aveScale = (ratioW + ratioH) / 2 1512 if self.graph._antiAliasingEnabled and not self.IsPreview(): 1513 scale = dc.GetUserScale() 1514 dc.SetUserScale(scale[0] / self.graph._pointSize[0], 1515 scale[1] / self.graph._pointSize[1]) 1516 self.graph._setPrinterScale(aveScale) # tickens up pens for printing 1517 1518 self.graph._printDraw(dc) 1519 # rescale back to original 1520 self.graph._setSize() 1521 self.graph._setPrinterScale(1) 1522 self.graph.Redraw() # to get point label scale and shift correct 1523 1524 return True 1525