1# -*- encoding: utf-8 -*- 2# 3# 4# Copyright (C) 2002-2012 Jörg Lehmann <joerg@pyx-project.org> 5# Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net> 6# Copyright (C) 2002-2012 André Wobst <wobsta@pyx-project.org> 7# 8# This file is part of PyX (https://pyx-project.org/). 9# 10# PyX is free software; you can redistribute it and/or modify 11# it under the terms of the GNU General Public License as published by 12# the Free Software Foundation; either version 2 of the License, or 13# (at your option) any later version. 14# 15# PyX is distributed in the hope that it will be useful, 16# but WITHOUT ANY WARRANTY; without even the implied warranty of 17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18# GNU General Public License for more details. 19# 20# You should have received a copy of the GNU General Public License 21# along with PyX; if not, write to the Free Software 22# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 23 24 25import logging, math, re, string 26from pyx import canvas, path, trafo, unit 27from pyx.graph.axis import axis, positioner 28 29logger = logging.getLogger("pyx") 30goldenmean = 0.5 * (math.sqrt(5) + 1) 31 32 33# The following two methods are used to register and get a default provider 34# for keys. A key is a variable name in sharedata. A provider is a style 35# which creates variables in sharedata. 36 37_defaultprovider = {} 38 39def registerdefaultprovider(style, keys): 40 """sets a style as a default creator for sharedata variables 'keys'""" 41 for key in keys: 42 assert key in style.providesdata, "key not provided by style" 43 # we might allow for overwriting the defaults, i.e. the following is not checked: 44 # assert key in _defaultprovider.keys(), "default provider already registered for key" 45 _defaultprovider[key] = style 46 47def getdefaultprovider(key): 48 """returns a style, which acts as a default creator for the 49 sharedata variable 'key'""" 50 return _defaultprovider[key] 51 52 53class styledata: 54 """style data storage class 55 56 Instances of this class are used to store data from the styles 57 and to pass point data to the styles by instances named privatedata 58 and sharedata. sharedata is shared between all the style(s) in use 59 by a data instance, while privatedata is private to each style and 60 used as a storage place instead of self to prevent side effects when 61 using a style several times.""" 62 pass 63 64 65class plotitem: 66 67 def __init__(self, graph, data, styles): 68 self.data = data 69 self.title = data.title 70 71 addstyles = [None] 72 while addstyles: 73 # add styles to ensure all needs of the given styles 74 provided = [] # already provided sharedata variables 75 addstyles = [] # a list of style instances to be added in front 76 for s in styles: 77 for n in s.needsdata: 78 if n not in provided: 79 defaultprovider = getdefaultprovider(n) 80 addstyles.append(defaultprovider) 81 provided.extend(defaultprovider.providesdata) 82 provided.extend(s.providesdata) 83 styles = addstyles + styles 84 85 self.styles = styles 86 self.sharedata = styledata() 87 self.dataaxisnames = {} 88 self.privatedatalist = [styledata() for s in self.styles] 89 90 # perform setcolumns to all styles 91 self.usedcolumnnames = set() 92 for privatedata, s in zip(self.privatedatalist, self.styles): 93 self.usedcolumnnames.update(set(s.columnnames(privatedata, self.sharedata, graph, self.data.columnnames, self.dataaxisnames))) 94 95 def selectstyles(self, graph, selectindex, selecttotal): 96 for privatedata, style in zip(self.privatedatalist, self.styles): 97 style.selectstyle(privatedata, self.sharedata, graph, selectindex, selecttotal) 98 99 def adjustaxesstatic(self, graph): 100 for columnname, data in list(self.data.columns.items()): 101 for privatedata, style in zip(self.privatedatalist, self.styles): 102 style.adjustaxis(privatedata, self.sharedata, graph, self, columnname, data) 103 104 def makedynamicdata(self, graph): 105 self.dynamiccolumns = self.data.dynamiccolumns(graph, self.dataaxisnames) 106 107 def adjustaxesdynamic(self, graph): 108 for columnname, data in list(self.dynamiccolumns.items()): 109 for privatedata, style in zip(self.privatedatalist, self.styles): 110 style.adjustaxis(privatedata, self.sharedata, graph, self, columnname, data) 111 112 def draw(self, graph): 113 for privatedata, style in zip(self.privatedatalist, self.styles): 114 style.initdrawpoints(privatedata, self.sharedata, graph) 115 116 point = dict([(columnname, None) for columnname in self.usedcolumnnames]) 117 # fill point with (static) column data first 118 columns = list(self.data.columns.keys()) 119 for values in zip(*list(self.data.columns.values())): 120 for column, value in zip(columns, values): 121 point[column] = value 122 for privatedata, style in zip(self.privatedatalist, self.styles): 123 style.drawpoint(privatedata, self.sharedata, graph, point) 124 125 point = dict([(columnname, None) for columnname in self.usedcolumnnames]) 126 # insert an empty point 127 if self.data.columns and self.dynamiccolumns: 128 for privatedata, style in zip(self.privatedatalist, self.styles): 129 style.drawpoint(privatedata, self.sharedata, graph, point) 130 # fill point with dynamic column data 131 columns = list(self.dynamiccolumns.keys()) 132 for values in zip(*list(self.dynamiccolumns.values())): 133 for key, value in zip(columns, values): 134 point[key] = value 135 for privatedata, style in zip(self.privatedatalist, self.styles): 136 style.drawpoint(privatedata, self.sharedata, graph, point) 137 for privatedata, style in zip(self.privatedatalist, self.styles): 138 style.donedrawpoints(privatedata, self.sharedata, graph) 139 140 def key_pt(self, graph, x_pt, y_pt, width_pt, height_pt): 141 for privatedata, style in zip(self.privatedatalist, self.styles): 142 style.key_pt(privatedata, self.sharedata, graph, x_pt, y_pt, width_pt, height_pt) 143 144 def __getattr__(self, attr): 145 # read only access to the styles privatedata 146 # this is just a convenience method 147 # use case: access the path of a the line style 148 stylesdata = [getattr(styledata, attr) 149 for styledata in self.privatedatalist 150 if hasattr(styledata, attr)] 151 if len(stylesdata) > 1: 152 return stylesdata 153 elif len(stylesdata) == 1: 154 return stylesdata[0] 155 raise AttributeError("access to styledata attribute '%s' failed" % attr) 156 157 158class graph(canvas.canvas): 159 160 def __init__(self): 161 canvas.canvas.__init__(self) 162 for name in ["background", "filldata", "axes.grid", "axes.baseline", "axes.ticks", "axes.labels", "axes.title", "data", "key"]: 163 self.layer(name) 164 self.axes = {} 165 self.plotitems = [] 166 self.keyitems = [] 167 self._calls = {} 168 self.didranges = 0 169 self.didstyles = 0 170 171 def did(self, method, *args, **kwargs): 172 if method not in self._calls: 173 self._calls[method] = [] 174 for callargs in self._calls[method]: 175 if callargs == (args, kwargs): 176 return 1 177 self._calls[method].append((args, kwargs)) 178 return 0 179 180 def bbox(self): 181 self.finish() 182 return canvas.canvas.bbox(self) 183 184 185 def processPS(self, file, writer, context, registry, bbox): 186 self.finish() 187 canvas.canvas.processPS(self, file, writer, context, registry, bbox) 188 189 def processPDF(self, file, writer, context, registry, bbox): 190 self.finish() 191 canvas.canvas.processPDF(self, file, writer, context, registry, bbox) 192 193 def plot(self, data, styles=None, rangewarning=1): 194 if self.didranges and rangewarning: 195 logger.warning("axes ranges have already been analysed; no further adjustments will be performed") 196 if self.didstyles: 197 raise RuntimeError("can't plot further data after dostyles() has been executed") 198 singledata = 0 199 try: 200 for d in data: 201 pass 202 except: 203 usedata = [data] 204 singledata = 1 205 else: 206 usedata = data 207 if styles is None: 208 for d in usedata: 209 if styles is None: 210 styles = d.defaultstyles 211 elif styles != d.defaultstyles: 212 raise RuntimeError("defaultstyles differ") 213 plotitems = [] 214 for d in usedata: 215 plotitems.append(plotitem(self, d, styles)) 216 self.plotitems.extend(plotitems) 217 if self.didranges: 218 for aplotitem in plotitems: 219 aplotitem.makedynamicdata(self) 220 if singledata: 221 return plotitems[0] 222 else: 223 return plotitems 224 225 def doranges(self): 226 if self.did(self.doranges): 227 return 228 for plotitem in self.plotitems: 229 plotitem.adjustaxesstatic(self) 230 for plotitem in self.plotitems: 231 plotitem.makedynamicdata(self) 232 for plotitem in self.plotitems: 233 plotitem.adjustaxesdynamic(self) 234 self.didranges = 1 235 236 def doaxiscreate(self, axisname): 237 if self.did(self.doaxiscreate, axisname): 238 return 239 self.doaxispositioner(axisname) 240 self.axes[axisname].create() 241 242 def dolayout(self): 243 raise NotImplementedError 244 245 def dobackground(self): 246 pass 247 248 def doaxes(self): 249 raise NotImplementedError 250 251 def dostyles(self): 252 if self.did(self.dostyles): 253 return 254 self.dolayout() 255 self.dobackground() 256 257 # count the usage of styles and perform selects 258 styletotal = {} 259 def stylesid(styles): 260 return ":".join([str(id(style)) for style in styles]) 261 for plotitem in self.plotitems: 262 try: 263 styletotal[stylesid(plotitem.styles)] += 1 264 except: 265 styletotal[stylesid(plotitem.styles)] = 1 266 styleindex = {} 267 for plotitem in self.plotitems: 268 try: 269 styleindex[stylesid(plotitem.styles)] += 1 270 except: 271 styleindex[stylesid(plotitem.styles)] = 0 272 plotitem.selectstyles(self, styleindex[stylesid(plotitem.styles)], 273 styletotal[stylesid(plotitem.styles)]) 274 275 self.didstyles = 1 276 277 def doplotitem(self, plotitem): 278 if self.did(self.doplotitem, plotitem): 279 return 280 self.dostyles() 281 plotitem.draw(self) 282 283 def doplot(self): 284 for plotitem in self.plotitems: 285 self.doplotitem(plotitem) 286 287 def dodata(self): 288 logger.warning("dodata() has been deprecated. Use doplot() instead.") 289 self.doplot() 290 291 def dokeyitem(self, plotitem): 292 if self.did(self.dokeyitem, plotitem): 293 return 294 self.dostyles() 295 if plotitem.title is not None: 296 self.keyitems.append(plotitem) 297 298 def dokey(self): 299 raise NotImplementedError 300 301 def finish(self): 302 self.dobackground() 303 self.doaxes() 304 self.doplot() 305 self.dokey() 306 307 308class graphxy(graph): 309 310 def __init__(self, xpos=0, ypos=0, width=None, height=None, ratio=goldenmean, 311 key=None, backgroundattrs=None, axesdist=0.8*unit.v_cm, flipped=False, 312 xaxisat=None, yaxisat=None, **axes): 313 graph.__init__(self) 314 315 self.xpos = xpos 316 self.ypos = ypos 317 self.xpos_pt = unit.topt(self.xpos) 318 self.ypos_pt = unit.topt(self.ypos) 319 self.xaxisat = xaxisat 320 self.yaxisat = yaxisat 321 self.key = key 322 self.backgroundattrs = backgroundattrs 323 self.axesdist_pt = unit.topt(axesdist) 324 self.flipped = flipped 325 326 self.width = width 327 self.height = height 328 if width is None: 329 if height is None: 330 raise ValueError("specify width and/or height") 331 else: 332 self.width = ratio * self.height 333 elif height is None: 334 self.height = (1.0/ratio) * self.width 335 self.width_pt = unit.topt(self.width) 336 self.height_pt = unit.topt(self.height) 337 338 for axisname, aaxis in list(axes.items()): 339 if aaxis is not None: 340 if not isinstance(aaxis, axis.linkedaxis): 341 self.axes[axisname] = axis.anchoredaxis(aaxis, self.textengine, axisname) 342 else: 343 self.axes[axisname] = aaxis 344 for axisname, axisat in [("x", xaxisat), ("y", yaxisat)]: 345 okey = axisname + "2" 346 if axisname not in axes: 347 if okey not in axes or axes[okey] is None: 348 self.axes[axisname] = axis.anchoredaxis(axis.linear(), self.textengine, axisname) 349 if okey not in axes: 350 self.axes[okey] = axis.linkedaxis(self.axes[axisname], okey) 351 else: 352 self.axes[axisname] = axis.linkedaxis(self.axes[okey], axisname) 353 elif okey not in axes and axisat is None: 354 self.axes[okey] = axis.linkedaxis(self.axes[axisname], okey) 355 356 if "x" in self.axes: 357 self.xbasepath = self.axes["x"].basepath 358 self.xvbasepath = self.axes["x"].vbasepath 359 self.xgridpath = self.axes["x"].gridpath 360 self.xtickpoint_pt = self.axes["x"].tickpoint_pt 361 self.xtickpoint = self.axes["x"].tickpoint 362 self.xvtickpoint_pt = self.axes["x"].vtickpoint_pt 363 self.xvtickpoint = self.axes["x"].tickpoint 364 self.xtickdirection = self.axes["x"].tickdirection 365 self.xvtickdirection = self.axes["x"].vtickdirection 366 367 if "y" in self.axes: 368 self.ybasepath = self.axes["y"].basepath 369 self.yvbasepath = self.axes["y"].vbasepath 370 self.ygridpath = self.axes["y"].gridpath 371 self.ytickpoint_pt = self.axes["y"].tickpoint_pt 372 self.ytickpoint = self.axes["y"].tickpoint 373 self.yvtickpoint_pt = self.axes["y"].vtickpoint_pt 374 self.yvtickpoint = self.axes["y"].vtickpoint 375 self.ytickdirection = self.axes["y"].tickdirection 376 self.yvtickdirection = self.axes["y"].vtickdirection 377 378 self.axesnames = ([], []) 379 for axisname, aaxis in list(self.axes.items()): 380 if axisname[0] not in "xy" or (len(axisname) != 1 and (not axisname[1:].isdigit() or 381 axisname[1:] == "1")): 382 raise ValueError("invalid axis name") 383 if axisname[0] == "x": 384 self.axesnames[0].append(axisname) 385 else: 386 self.axesnames[1].append(axisname) 387 aaxis.setcreatecall(self.doaxiscreate, axisname) 388 389 self.axespositioners = dict(x=positioner.lineaxispos_pt(self.xpos_pt, self.ypos_pt, 390 self.xpos_pt + self.width_pt, self.ypos_pt, 391 (0, 1), self.xvgridpath), 392 x2=positioner.lineaxispos_pt(self.xpos_pt, self.ypos_pt + self.height_pt, 393 self.xpos_pt + self.width_pt, self.ypos_pt + self.height_pt, 394 (0, -1), self.xvgridpath), 395 y=positioner.lineaxispos_pt(self.xpos_pt, self.ypos_pt, 396 self.xpos_pt, self.ypos_pt + self.height_pt, 397 (1, 0), self.yvgridpath), 398 y2=positioner.lineaxispos_pt(self.xpos_pt + self.width_pt, self.ypos_pt, 399 self.xpos_pt + self.width_pt, self.ypos_pt + self.height_pt, 400 (-1, 0), self.yvgridpath)) 401 if self.flipped: 402 self.axespositioners = dict(x=self.axespositioners["y2"], 403 y2=self.axespositioners["x2"], 404 y=self.axespositioners["x"], 405 x2=self.axespositioners["y"]) 406 407 def pos_pt(self, x, y, xaxis=None, yaxis=None): 408 if xaxis is None: 409 xaxis = self.axes["x"] 410 if yaxis is None: 411 yaxis = self.axes["y"] 412 vx = xaxis.convert(x) 413 vy = yaxis.convert(y) 414 if self.flipped: 415 vx, vy = vy, vx 416 return (self.xpos_pt + vx*self.width_pt, 417 self.ypos_pt + vy*self.height_pt) 418 419 def pos(self, x, y, xaxis=None, yaxis=None): 420 if xaxis is None: 421 xaxis = self.axes["x"] 422 if yaxis is None: 423 yaxis = self.axes["y"] 424 vx = xaxis.convert(x) 425 vy = yaxis.convert(y) 426 if self.flipped: 427 vx, vy = vy, vx 428 return (self.xpos + vx*self.width, 429 self.ypos + vy*self.height) 430 431 def vpos_pt(self, vx, vy): 432 if self.flipped: 433 vx, vy = vy, vx 434 return (self.xpos_pt + vx*self.width_pt, 435 self.ypos_pt + vy*self.height_pt) 436 437 def vpos(self, vx, vy): 438 if self.flipped: 439 vx, vy = vy, vx 440 return (self.xpos + vx*self.width, 441 self.ypos + vy*self.height) 442 443 def vzindex(self, vx, vy): 444 return 0 445 446 def vangle(self, vx1, vy1, vx2, vy2, vx3, vy3): 447 return 1 448 449 def vgeodesic(self, vx1, vy1, vx2, vy2): 450 """returns a geodesic path between two points in graph coordinates""" 451 if self.flipped: 452 vx1, vy1 = vy1, vx1 453 vx2, vy2 = vy2, vx2 454 return path.line_pt(self.xpos_pt + vx1*self.width_pt, 455 self.ypos_pt + vy1*self.height_pt, 456 self.xpos_pt + vx2*self.width_pt, 457 self.ypos_pt + vy2*self.height_pt) 458 459 def vgeodesic_el(self, vx1, vy1, vx2, vy2): 460 """returns a geodesic path element between two points in graph coordinates""" 461 if self.flipped: 462 vx1, vy1 = vy1, vx1 463 vx2, vy2 = vy2, vx2 464 return path.lineto_pt(self.xpos_pt + vx2*self.width_pt, 465 self.ypos_pt + vy2*self.height_pt) 466 467 def vcap_pt(self, coordinate, length_pt, vx, vy): 468 """returns an error cap path for a given coordinate, lengths and 469 point in graph coordinates""" 470 if self.flipped: 471 coordinate = 1-coordinate 472 vx, vy = vy, vx 473 if coordinate == 0: 474 return path.line_pt(self.xpos_pt + vx*self.width_pt - 0.5*length_pt, 475 self.ypos_pt + vy*self.height_pt, 476 self.xpos_pt + vx*self.width_pt + 0.5*length_pt, 477 self.ypos_pt + vy*self.height_pt) 478 elif coordinate == 1: 479 return path.line_pt(self.xpos_pt + vx*self.width_pt, 480 self.ypos_pt + vy*self.height_pt - 0.5*length_pt, 481 self.xpos_pt + vx*self.width_pt, 482 self.ypos_pt + vy*self.height_pt + 0.5*length_pt) 483 else: 484 raise ValueError("direction invalid") 485 486 def xvgridpath(self, vx): 487 return path.line_pt(self.xpos_pt + vx*self.width_pt, self.ypos_pt, 488 self.xpos_pt + vx*self.width_pt, self.ypos_pt + self.height_pt) 489 490 def yvgridpath(self, vy): 491 return path.line_pt(self.xpos_pt, self.ypos_pt + vy*self.height_pt, 492 self.xpos_pt + self.width_pt, self.ypos_pt + vy*self.height_pt) 493 494 def autokeygraphattrs(self): 495 return dict(direction="vertical", length=self.height) 496 497 def autokeygraphtrafo(self, keygraph): 498 dependsonaxisnumber = None 499 if self.flipped: 500 dependsonaxisname = "x" 501 else: 502 dependsonaxisname = "y" 503 for axisname in self.axes: 504 if axisname[0] == dependsonaxisname: 505 if len(axisname) == 1: 506 axisname += "1" 507 axisnumber = int(axisname[1:]) 508 if not (axisnumber % 2) and not self.flipped or (axisnumber % 2) and self.flipped: 509 if dependsonaxisnumber is None or dependsonaxisnumber < axisnumber: 510 dependsonaxisnumber = axisnumber 511 if dependsonaxisnumber is None: 512 x_pt = self.xpos_pt + self.width_pt 513 else: 514 if dependsonaxisnumber > 1: 515 dependsonaxisname += str(dependsonaxisnumber) 516 self.doaxiscreate(dependsonaxisname) 517 x_pt = self.axes[dependsonaxisname].positioner.x1_pt + self.axes[dependsonaxisname].canvas.extent_pt 518 x_pt += self.axesdist_pt 519 return trafo.translate_pt(x_pt, self.ypos_pt) 520 521 def axisatv(self, axis, v): 522 if axis.positioner.fixtickdirection[0]: 523 # it is a y-axis 524 t = trafo.translate_pt(self.xpos_pt + v*self.width_pt - axis.positioner.x1_pt, 0) 525 else: 526 # it is an x-axis 527 t = trafo.translate_pt(0, self.ypos_pt + v*self.height_pt - axis.positioner.y1_pt) 528 c = canvas.canvas() 529 for layer, subcanvas in list(axis.canvas.layers.items()): 530 c.layer(layer).insert(subcanvas, [t]) 531 assert len(axis.canvas.layers) == len(axis.canvas.items), str(axis.canvas.items) 532 axis.canvas = c 533 534 def doaxispositioner(self, axisname): 535 if self.did(self.doaxispositioner, axisname): 536 return 537 self.doranges() 538 if axisname in ["x", "x2", "y", "y2"]: 539 self.axes[axisname].setpositioner(self.axespositioners[axisname]) 540 else: 541 if axisname[1:] == "3": 542 dependsonaxisname = axisname[0] 543 else: 544 dependsonaxisname = "%s%d" % (axisname[0], int(axisname[1:]) - 2) 545 self.doaxiscreate(dependsonaxisname) 546 sign = 2*(int(axisname[1:]) % 2) - 1 547 if axisname[0] == "x" and self.flipped: 548 sign = -sign 549 if axisname[0] == "x" and not self.flipped or axisname[0] == "y" and self.flipped: 550 y_pt = self.axes[dependsonaxisname].positioner.y1_pt - sign * (self.axes[dependsonaxisname].canvas.extent_pt + self.axesdist_pt) 551 self.axes[axisname].setpositioner(positioner.lineaxispos_pt(self.xpos_pt, y_pt, 552 self.xpos_pt + self.width_pt, y_pt, 553 (0, sign), self.xvgridpath)) 554 else: 555 x_pt = self.axes[dependsonaxisname].positioner.x1_pt - sign * (self.axes[dependsonaxisname].canvas.extent_pt + self.axesdist_pt) 556 self.axes[axisname].setpositioner(positioner.lineaxispos_pt(x_pt, self.ypos_pt, 557 x_pt, self.ypos_pt + self.height_pt, 558 (sign, 0), self.yvgridpath)) 559 560 def dolayout(self): 561 if self.did(self.dolayout): 562 return 563 for axisname in list(self.axes.keys()): 564 self.doaxiscreate(axisname) 565 if self.xaxisat is not None: 566 self.axisatv(self.axes["x"], self.axes["y"].convert(self.xaxisat)) 567 if self.yaxisat is not None: 568 self.axisatv(self.axes["y"], self.axes["x"].convert(self.yaxisat)) 569 570 def dobackground(self): 571 if self.did(self.dobackground): 572 return 573 if self.backgroundattrs is not None: 574 self.layer("background").draw(path.rect_pt(self.xpos_pt, self.ypos_pt, self.width_pt, self.height_pt), 575 self.backgroundattrs) 576 577 def doaxes(self): 578 if self.did(self.doaxes): 579 return 580 self.dolayout() 581 self.dobackground() 582 for axis in list(self.axes.values()): 583 for layer, canvas in list(axis.canvas.layers.items()): 584 self.layer("axes.%s" % layer).insert(canvas) 585 assert len(axis.canvas.layers) == len(axis.canvas.items), str(axis.canvas.items) 586 587 def dokey(self): 588 if self.did(self.dokey): 589 return 590 self.dobackground() 591 for plotitem in self.plotitems: 592 self.dokeyitem(plotitem) 593 if self.key is not None: 594 c = self.key.paint(self.keyitems) 595 bbox = c.bbox() 596 def parentchildalign(pmin, pmax, cmin, cmax, pos, dist, inside): 597 ppos = pmin+0.5*(cmax-cmin)+dist+pos*(pmax-pmin-cmax+cmin-2*dist) 598 cpos = 0.5*(cmin+cmax)+(1-inside)*(1-2*pos)*(cmax-cmin+2*dist) 599 return ppos-cpos 600 if bbox: 601 x = parentchildalign(self.xpos_pt, self.xpos_pt+self.width_pt, 602 bbox.llx_pt, bbox.urx_pt, 603 self.key.hpos, unit.topt(self.key.hdist), self.key.hinside) 604 y = parentchildalign(self.ypos_pt, self.ypos_pt+self.height_pt, 605 bbox.lly_pt, bbox.ury_pt, 606 self.key.vpos, unit.topt(self.key.vdist), self.key.vinside) 607 self.layer("key").insert(c, [trafo.translate_pt(x, y)]) 608 609 610 611class graphx(graphxy): 612 613 def __init__(self, xpos=0, ypos=0, length=None, size=0.5*unit.v_cm, direction="vertical", 614 key=None, backgroundattrs=None, axesdist=0.8*unit.v_cm, **axes): 615 for name in axes: 616 if not name.startswith("x"): 617 raise ValueError("Only x axes are allowed") 618 self.direction = direction 619 if self.direction == "vertical": 620 kwargsxy = dict(width=size, height=length, flipped=True) 621 elif self.direction == "horizontal": 622 kwargsxy = dict(width=length, height=size) 623 else: 624 raise ValueError("vertical or horizontal direction required") 625 kwargsxy.update(**axes) 626 627 graphxy.__init__(self, xpos=xpos, ypos=ypos, ratio=None, key=key, y=axis.lin(min=0, max=1, parter=None), 628 backgroundattrs=backgroundattrs, axesdist=axesdist, **kwargsxy) 629 630 def pos_pt(self, x, xaxis=None): 631 return graphxy.pos_pt(self, x, 0.5, xaxis) 632 633 def pos(self, x, xaxis=None): 634 return graphxy.pos(self, x, 0.5, xaxis) 635 636 def vpos_pt(self, vx): 637 return graphxy.vpos_pt(self, vx, 0.5) 638 639 def vpos(self, vx): 640 return graphxy.vpos(self, vx, 0.5) 641 642 def vgeodesic(self, vx1, vx2): 643 return graphxy.vgeodesic(self, vx1, 0.5, vx2, 0.5) 644 645 def vgeodesic_el(self, vx1, vy1, vx2, vy2): 646 return graphxy.vgeodesic_el(self, vx1, 0.5, vx2, 0.5) 647 648 def vcap_pt(self, coordinate, length_pt, vx): 649 if coordinate == 0: 650 return graphxy.vcap_pt(self, coordinate, length_pt, vx, 0.5) 651 else: 652 raise ValueError("direction invalid") 653 654 def xvgridpath(self, vx): 655 return graphxy.xvgridpath(self, vx) 656 657 def yvgridpath(self, vy): 658 raise Exception("This method does not exist on a one dimensional graph.") 659 660 def axisatv(self, axis, v): 661 raise Exception("This method does not exist on a one dimensional graph.") 662 663 664 665class graphxyz(graph): 666 667 class central: 668 669 def __init__(self, distance, phi, theta, anglefactor=math.pi/180): 670 phi *= anglefactor 671 theta *= anglefactor 672 self.distance = distance 673 674 self.a = (-math.sin(phi), math.cos(phi), 0) 675 self.b = (-math.cos(phi)*math.sin(theta), 676 -math.sin(phi)*math.sin(theta), 677 math.cos(theta)) 678 self.eye = (distance*math.cos(phi)*math.cos(theta), 679 distance*math.sin(phi)*math.cos(theta), 680 distance*math.sin(theta)) 681 682 def point(self, x, y, z): 683 d0 = (self.a[0]*self.b[1]*(z-self.eye[2]) 684 + self.a[2]*self.b[0]*(y-self.eye[1]) 685 + self.a[1]*self.b[2]*(x-self.eye[0]) 686 - self.a[2]*self.b[1]*(x-self.eye[0]) 687 - self.a[0]*self.b[2]*(y-self.eye[1]) 688 - self.a[1]*self.b[0]*(z-self.eye[2])) 689 da = (self.eye[0]*self.b[1]*(z-self.eye[2]) 690 + self.eye[2]*self.b[0]*(y-self.eye[1]) 691 + self.eye[1]*self.b[2]*(x-self.eye[0]) 692 - self.eye[2]*self.b[1]*(x-self.eye[0]) 693 - self.eye[0]*self.b[2]*(y-self.eye[1]) 694 - self.eye[1]*self.b[0]*(z-self.eye[2])) 695 db = (self.a[0]*self.eye[1]*(z-self.eye[2]) 696 + self.a[2]*self.eye[0]*(y-self.eye[1]) 697 + self.a[1]*self.eye[2]*(x-self.eye[0]) 698 - self.a[2]*self.eye[1]*(x-self.eye[0]) 699 - self.a[0]*self.eye[2]*(y-self.eye[1]) 700 - self.a[1]*self.eye[0]*(z-self.eye[2])) 701 return da/d0, db/d0 702 703 def zindex(self, x, y, z): 704 return math.sqrt((x-self.eye[0])*(x-self.eye[0])+(y-self.eye[1])*(y-self.eye[1])+(z-self.eye[2])*(z-self.eye[2]))-self.distance 705 706 def angle(self, x1, y1, z1, x2, y2, z2, x3, y3, z3): 707 sx = (x1-self.eye[0]) 708 sy = (y1-self.eye[1]) 709 sz = (z1-self.eye[2]) 710 nx = (y2-y1)*(z3-z1)-(z2-z1)*(y3-y1) 711 ny = (z2-z1)*(x3-x1)-(x2-x1)*(z3-z1) 712 nz = (x2-x1)*(y3-y1)-(y2-y1)*(x3-x1) 713 return (sx*nx+sy*ny+sz*nz)/math.sqrt(nx*nx+ny*ny+nz*nz)/math.sqrt(sx*sx+sy*sy+sz*sz) 714 715 716 class parallel: 717 718 def __init__(self, phi, theta, anglefactor=math.pi/180): 719 phi *= anglefactor 720 theta *= anglefactor 721 722 self.a = (-math.sin(phi), math.cos(phi), 0) 723 self.b = (-math.cos(phi)*math.sin(theta), 724 -math.sin(phi)*math.sin(theta), 725 math.cos(theta)) 726 self.c = (-math.cos(phi)*math.cos(theta), 727 -math.sin(phi)*math.cos(theta), 728 -math.sin(theta)) 729 730 def point(self, x, y, z): 731 return self.a[0]*x+self.a[1]*y+self.a[2]*z, self.b[0]*x+self.b[1]*y+self.b[2]*z 732 733 def zindex(self, x, y, z): 734 return self.c[0]*x+self.c[1]*y+self.c[2]*z 735 736 def angle(self, x1, y1, z1, x2, y2, z2, x3, y3, z3): 737 nx = (y2-y1)*(z3-z1)-(z2-z1)*(y3-y1) 738 ny = (z2-z1)*(x3-x1)-(x2-x1)*(z3-z1) 739 nz = (x2-x1)*(y3-y1)-(y2-y1)*(x3-x1) 740 return (self.c[0]*nx+self.c[1]*ny+self.c[2]*nz)/math.sqrt(nx*nx+ny*ny+nz*nz) 741 742 743 def __init__(self, xpos=0, ypos=0, size=None, 744 xscale=1, yscale=1, zscale=1/goldenmean, xy12axesat=None, xy12axesatname="z", 745 projector=central(10, -30, 30), axesdist=0.8*unit.v_cm, key=None, 746 **axes): 747 graph.__init__(self) 748 for name in ["hiddenaxes.grid", "hiddenaxes.baseline", "hiddenaxes.ticks", "hiddenaxes.labels", "hiddenaxes.title"]: 749 self.layer(name) 750 self.layer("hiddenaxes", below="filldata") 751 752 self.xpos = xpos 753 self.ypos = ypos 754 self.size = size 755 self.xpos_pt = unit.topt(xpos) 756 self.ypos_pt = unit.topt(ypos) 757 self.size_pt = unit.topt(size) 758 self.xscale = xscale 759 self.yscale = yscale 760 self.zscale = zscale 761 self.xy12axesat = xy12axesat 762 self.xy12axesatname = xy12axesatname 763 self.projector = projector 764 self.axesdist_pt = unit.topt(axesdist) 765 self.key = key 766 767 self.xorder = projector.zindex(0, -1, 0) > projector.zindex(0, 1, 0) and 1 or 0 768 self.yorder = projector.zindex(-1, 0, 0) > projector.zindex(1, 0, 0) and 1 or 0 769 self.zindexscale = math.sqrt(xscale*xscale+yscale*yscale+zscale*zscale) 770 771 # the pXYshow attributes are booleans stating whether plane perpendicular to axis X 772 # at the virtual graph coordinate Y will be hidden by data or not. An axis is considered 773 # to be visible if one of the two planes it is part of is visible. Other axes are drawn 774 # in the hiddenaxes layer (i.e. layer group). 775 # TODO: Tick and grid visibility is treated like the axis visibility at the moment. 776 self.pz0show = self.vangle(0, 0, 0, 1, 0, 0, 1, 1, 0) > 0 777 self.pz1show = self.vangle(0, 0, 1, 0, 1, 1, 1, 1, 1) > 0 778 self.py0show = self.vangle(0, 0, 0, 0, 0, 1, 1, 0, 1) > 0 779 self.py1show = self.vangle(0, 1, 0, 1, 1, 0, 1, 1, 1) > 0 780 self.px0show = self.vangle(0, 0, 0, 0, 1, 0, 0, 1, 1) > 0 781 self.px1show = self.vangle(1, 0, 0, 1, 0, 1, 1, 1, 1) > 0 782 783 for axisname, aaxis in list(axes.items()): 784 if aaxis is not None: 785 if not isinstance(aaxis, axis.linkedaxis): 786 self.axes[axisname] = axis.anchoredaxis(aaxis, self.textengine, axisname) 787 else: 788 self.axes[axisname] = aaxis 789 for axisname in ["x", "y"]: 790 okey = axisname + "2" 791 if axisname not in axes: 792 if okey not in axes or axes[okey] is None: 793 self.axes[axisname] = axis.anchoredaxis(axis.linear(), self.textengine, axisname) 794 if okey not in axes: 795 self.axes[okey] = axis.linkedaxis(self.axes[axisname], okey) 796 else: 797 self.axes[axisname] = axis.linkedaxis(self.axes[okey], axisname) 798 elif okey not in axes: 799 self.axes[okey] = axis.linkedaxis(self.axes[axisname], okey) 800 if "z" not in axes: 801 self.axes["z"] = axis.anchoredaxis(axis.linear(), self.textengine, "z") 802 803 if "x" in self.axes: 804 self.xbasepath = self.axes["x"].basepath 805 self.xvbasepath = self.axes["x"].vbasepath 806 self.xgridpath = self.axes["x"].gridpath 807 self.xtickpoint_pt = self.axes["x"].tickpoint_pt 808 self.xtickpoint = self.axes["x"].tickpoint 809 self.xvtickpoint_pt = self.axes["x"].vtickpoint_pt 810 self.xvtickpoint = self.axes["x"].tickpoint 811 self.xtickdirection = self.axes["x"].tickdirection 812 self.xvtickdirection = self.axes["x"].vtickdirection 813 814 if "y" in self.axes: 815 self.ybasepath = self.axes["y"].basepath 816 self.yvbasepath = self.axes["y"].vbasepath 817 self.ygridpath = self.axes["y"].gridpath 818 self.ytickpoint_pt = self.axes["y"].tickpoint_pt 819 self.ytickpoint = self.axes["y"].tickpoint 820 self.yvtickpoint_pt = self.axes["y"].vtickpoint_pt 821 self.yvtickpoint = self.axes["y"].vtickpoint 822 self.ytickdirection = self.axes["y"].tickdirection 823 self.yvtickdirection = self.axes["y"].vtickdirection 824 825 if "z" in self.axes: 826 self.zbasepath = self.axes["z"].basepath 827 self.zvbasepath = self.axes["z"].vbasepath 828 self.zgridpath = self.axes["z"].gridpath 829 self.ztickpoint_pt = self.axes["z"].tickpoint_pt 830 self.ztickpoint = self.axes["z"].tickpoint 831 self.zvtickpoint_pt = self.axes["z"].vtickpoint 832 self.zvtickpoint = self.axes["z"].vtickpoint 833 self.ztickdirection = self.axes["z"].tickdirection 834 self.zvtickdirection = self.axes["z"].vtickdirection 835 836 self.axesnames = ([], [], []) 837 for axisname, aaxis in list(self.axes.items()): 838 if axisname[0] not in "xyz" or (len(axisname) != 1 and (not axisname[1:].isdigit() or 839 axisname[1:] == "1")): 840 raise ValueError("invalid axis name") 841 if axisname[0] == "x": 842 self.axesnames[0].append(axisname) 843 elif axisname[0] == "y": 844 self.axesnames[1].append(axisname) 845 else: 846 self.axesnames[2].append(axisname) 847 aaxis.setcreatecall(self.doaxiscreate, axisname) 848 849 def pos_pt(self, x, y, z, xaxis=None, yaxis=None, zaxis=None): 850 if xaxis is None: 851 xaxis = self.axes["x"] 852 if yaxis is None: 853 yaxis = self.axes["y"] 854 if zaxis is None: 855 zaxis = self.axes["z"] 856 return self.vpos_pt(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z)) 857 858 def pos(self, x, y, z, xaxis=None, yaxis=None, zaxis=None): 859 if xaxis is None: 860 xaxis = self.axes["x"] 861 if yaxis is None: 862 yaxis = self.axes["y"] 863 if zaxis is None: 864 zaxis = self.axes["z"] 865 return self.vpos(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z)) 866 867 def vpos_pt(self, vx, vy, vz): 868 x, y = self.projector.point(2*self.xscale*(vx - 0.5), 869 2*self.yscale*(vy - 0.5), 870 2*self.zscale*(vz - 0.5)) 871 return self.xpos_pt+x*self.size_pt, self.ypos_pt+y*self.size_pt 872 873 def vpos(self, vx, vy, vz): 874 x, y = self.projector.point(2*self.xscale*(vx - 0.5), 875 2*self.yscale*(vy - 0.5), 876 2*self.zscale*(vz - 0.5)) 877 return self.xpos+x*self.size, self.ypos+y*self.size 878 879 def vzindex(self, vx, vy, vz): 880 return self.projector.zindex(2*self.xscale*(vx - 0.5), 881 2*self.yscale*(vy - 0.5), 882 2*self.zscale*(vz - 0.5))/self.zindexscale 883 884 def vangle(self, vx1, vy1, vz1, vx2, vy2, vz2, vx3, vy3, vz3): 885 return self.projector.angle(2*self.xscale*(vx1 - 0.5), 886 2*self.yscale*(vy1 - 0.5), 887 2*self.zscale*(vz1 - 0.5), 888 2*self.xscale*(vx2 - 0.5), 889 2*self.yscale*(vy2 - 0.5), 890 2*self.zscale*(vz2 - 0.5), 891 2*self.xscale*(vx3 - 0.5), 892 2*self.yscale*(vy3 - 0.5), 893 2*self.zscale*(vz3 - 0.5)) 894 895 def vgeodesic(self, vx1, vy1, vz1, vx2, vy2, vz2): 896 """returns a geodesic path between two points in graph coordinates""" 897 return path.line_pt(*(self.vpos_pt(vx1, vy1, vz1) + self.vpos_pt(vx2, vy2, vz2))) 898 899 def vgeodesic_el(self, vx1, vy1, vz1, vx2, vy2, vz2): 900 """returns a geodesic path element between two points in graph coordinates""" 901 return path.lineto_pt(*self.vpos_pt(vx2, vy2, vz2)) 902 903 def vcap_pt(self, coordinate, length_pt, vx, vy, vz): 904 """returns an error cap path for a given coordinate, lengths and 905 point in graph coordinates""" 906 if coordinate == 0: 907 return self.vgeodesic(vx-0.5*length_pt/self.size_pt, vy, vz, vx+0.5*length_pt/self.size_pt, vy, vz) 908 elif coordinate == 1: 909 return self.vgeodesic(vx, vy-0.5*length_pt/self.size_pt, vz, vx, vy+0.5*length_pt/self.size_pt, vz) 910 elif coordinate == 2: 911 return self.vgeodesic(vx, vy, vz-0.5*length_pt/self.size_pt, vx, vy, vz+0.5*length_pt/self.size_pt) 912 else: 913 raise ValueError("direction invalid") 914 915 def xvtickdirection(self, vx): 916 if self.xorder: 917 x1_pt, y1_pt = self.vpos_pt(vx, 1, 0) 918 x2_pt, y2_pt = self.vpos_pt(vx, 0, 0) 919 else: 920 x1_pt, y1_pt = self.vpos_pt(vx, 0, 0) 921 x2_pt, y2_pt = self.vpos_pt(vx, 1, 0) 922 dx_pt = x2_pt - x1_pt 923 dy_pt = y2_pt - y1_pt 924 norm = math.hypot(dx_pt, dy_pt) 925 return dx_pt/norm, dy_pt/norm 926 927 def yvtickdirection(self, vy): 928 if self.yorder: 929 x1_pt, y1_pt = self.vpos_pt(1, vy, 0) 930 x2_pt, y2_pt = self.vpos_pt(0, vy, 0) 931 else: 932 x1_pt, y1_pt = self.vpos_pt(0, vy, 0) 933 x2_pt, y2_pt = self.vpos_pt(1, vy, 0) 934 dx_pt = x2_pt - x1_pt 935 dy_pt = y2_pt - y1_pt 936 norm = math.hypot(dx_pt, dy_pt) 937 return dx_pt/norm, dy_pt/norm 938 939 def vtickdirection(self, vx1, vy1, vz1, vx2, vy2, vz2): 940 x1_pt, y1_pt = self.vpos_pt(vx1, vy1, vz1) 941 x2_pt, y2_pt = self.vpos_pt(vx2, vy2, vz2) 942 dx_pt = x2_pt - x1_pt 943 dy_pt = y2_pt - y1_pt 944 norm = math.hypot(dx_pt, dy_pt) 945 return dx_pt/norm, dy_pt/norm 946 947 def xvgridpath(self, vx): 948 return path.path(path.moveto_pt(*self.vpos_pt(vx, 0, 0)), 949 path.lineto_pt(*self.vpos_pt(vx, 1, 0)), 950 path.lineto_pt(*self.vpos_pt(vx, 1, 1)), 951 path.lineto_pt(*self.vpos_pt(vx, 0, 1)), 952 path.closepath()) 953 954 def yvgridpath(self, vy): 955 return path.path(path.moveto_pt(*self.vpos_pt(0, vy, 0)), 956 path.lineto_pt(*self.vpos_pt(1, vy, 0)), 957 path.lineto_pt(*self.vpos_pt(1, vy, 1)), 958 path.lineto_pt(*self.vpos_pt(0, vy, 1)), 959 path.closepath()) 960 961 def zvgridpath(self, vz): 962 return path.path(path.moveto_pt(*self.vpos_pt(0, 0, vz)), 963 path.lineto_pt(*self.vpos_pt(1, 0, vz)), 964 path.lineto_pt(*self.vpos_pt(1, 1, vz)), 965 path.lineto_pt(*self.vpos_pt(0, 1, vz)), 966 path.closepath()) 967 968 def autokeygraphattrs(self): 969 return dict(direction="vertical", length=self.size) 970 971 def autokeygraphtrafo(self, keygraph): 972 self.doaxes() 973 x_pt = self.layer("axes").bbox().right_pt() + self.axesdist_pt 974 y_pt = 0.5*(self.layer("axes").bbox().top_pt() + self.layer("axes").bbox().bottom_pt() - self.size_pt) 975 return trafo.translate_pt(x_pt, y_pt) 976 977 def doaxispositioner(self, axisname): 978 if self.did(self.doaxispositioner, axisname): 979 return 980 self.doranges() 981 if self.xy12axesat is not None: 982 self.doaxiscreate(self.xy12axesatname) 983 self.doaxispositioner(self.xy12axesatname) 984 xy12axesatv = self.axes[self.xy12axesatname].convert(self.xy12axesat) 985 else: 986 xy12axesatv = 0 987 if axisname == "x": 988 self.axes[axisname].setpositioner(positioner.flexlineaxispos_pt(lambda vx: self.vpos_pt(vx, self.xorder, xy12axesatv), 989 lambda vx: self.vtickdirection(vx, self.xorder, 0, vx, 1-self.xorder, xy12axesatv), 990 self.xvgridpath)) 991 if self.xorder: 992 self.axes[axisname].hidden = not self.py1show and not self.pz0show 993 else: 994 self.axes[axisname].hidden = not self.py0show and not self.pz0show 995 elif axisname == "x2": 996 self.axes[axisname].setpositioner(positioner.flexlineaxispos_pt(lambda vx: self.vpos_pt(vx, 1-self.xorder, xy12axesatv), 997 lambda vx: self.vtickdirection(vx, 1-self.xorder, 0, vx, self.xorder, xy12axesatv), 998 self.xvgridpath)) 999 if self.xorder: 1000 self.axes[axisname].hidden = not self.py0show and not self.pz0show 1001 else: 1002 self.axes[axisname].hidden = not self.py1show and not self.pz0show 1003 elif axisname == "x3": 1004 self.axes[axisname].setpositioner(positioner.flexlineaxispos_pt(lambda vx: self.vpos_pt(vx, self.xorder, 1), 1005 lambda vx: self.vtickdirection(vx, self.xorder, 1, vx, 1-self.xorder, 1), 1006 self.xvgridpath)) 1007 if self.xorder: 1008 self.axes[axisname].hidden = not self.py1show and not self.pz1show 1009 else: 1010 self.axes[axisname].hidden = not self.py0show and not self.pz1show 1011 elif axisname == "x4": 1012 self.axes[axisname].setpositioner(positioner.flexlineaxispos_pt(lambda vx: self.vpos_pt(vx, 1-self.xorder, 1), 1013 lambda vx: self.vtickdirection(vx, 1-self.xorder, 1, vx, self.xorder, 1), 1014 self.xvgridpath)) 1015 if self.xorder: 1016 self.axes[axisname].hidden = not self.py0show and not self.pz1show 1017 else: 1018 self.axes[axisname].hidden = not self.py1show and not self.pz1show 1019 elif axisname == "y": 1020 self.axes[axisname].setpositioner(positioner.flexlineaxispos_pt(lambda vy: self.vpos_pt(self.yorder, vy, xy12axesatv), 1021 lambda vy: self.vtickdirection(self.yorder, vy, 0, 1-self.yorder, vy, xy12axesatv), 1022 self.yvgridpath)) 1023 if self.yorder: 1024 self.axes[axisname].hidden = not self.px1show and not self.pz0show 1025 else: 1026 self.axes[axisname].hidden = not self.px0show and not self.pz0show 1027 elif axisname == "y2": 1028 self.axes[axisname].setpositioner(positioner.flexlineaxispos_pt(lambda vy: self.vpos_pt(1-self.yorder, vy, xy12axesatv), 1029 lambda vy: self.vtickdirection(1-self.yorder, vy, 0, self.yorder, vy, xy12axesatv), 1030 self.yvgridpath)) 1031 if self.yorder: 1032 self.axes[axisname].hidden = not self.px0show and not self.pz0show 1033 else: 1034 self.axes[axisname].hidden = not self.px1show and not self.pz0show 1035 elif axisname == "y3": 1036 self.axes[axisname].setpositioner(positioner.flexlineaxispos_pt(lambda vy: self.vpos_pt(self.yorder, vy, 1), 1037 lambda vy: self.vtickdirection(self.yorder, vy, 1, 1-self.yorder, vy, 1), 1038 self.yvgridpath)) 1039 if self.yorder: 1040 self.axes[axisname].hidden = not self.px1show and not self.pz1show 1041 else: 1042 self.axes[axisname].hidden = not self.px0show and not self.pz1show 1043 elif axisname == "y4": 1044 self.axes[axisname].setpositioner(positioner.flexlineaxispos_pt(lambda vy: self.vpos_pt(1-self.yorder, vy, 1), 1045 lambda vy: self.vtickdirection(1-self.yorder, vy, 1, self.yorder, vy, 1), 1046 self.yvgridpath)) 1047 if self.yorder: 1048 self.axes[axisname].hidden = not self.px0show and not self.pz1show 1049 else: 1050 self.axes[axisname].hidden = not self.px1show and not self.pz1show 1051 elif axisname == "z": 1052 self.axes[axisname].setpositioner(positioner.flexlineaxispos_pt(lambda vz: self.vpos_pt(0, 0, vz), 1053 lambda vz: self.vtickdirection(0, 0, vz, 1, 1, vz), 1054 self.zvgridpath)) 1055 self.axes[axisname].hidden = not self.px0show and not self.py0show 1056 elif axisname == "z2": 1057 self.axes[axisname].setpositioner(positioner.flexlineaxispos_pt(lambda vz: self.vpos_pt(1, 0, vz), 1058 lambda vz: self.vtickdirection(1, 0, vz, 0, 1, vz), 1059 self.zvgridpath)) 1060 self.axes[axisname].hidden = not self.px1show and not self.py0show 1061 elif axisname == "z3": 1062 self.axes[axisname].setpositioner(positioner.flexlineaxispos_pt(lambda vz: self.vpos_pt(0, 1, vz), 1063 lambda vz: self.vtickdirection(0, 1, vz, 1, 0, vz), 1064 self.zvgridpath)) 1065 self.axes[axisname].hidden = not self.px0show and not self.py1show 1066 elif axisname == "z4": 1067 self.axes[axisname].setpositioner(positioner.flexlineaxispos_pt(lambda vz: self.vpos_pt(1, 1, vz), 1068 lambda vz: self.vtickdirection(1, 1, vz, 0, 0, vz), 1069 self.zvgridpath)) 1070 self.axes[axisname].hidden = not self.px1show and not self.py1show 1071 else: 1072 raise NotImplementedError("4 axis per dimension supported only") 1073 1074 def dolayout(self): 1075 if self.did(self.dolayout): 1076 return 1077 for axisname in list(self.axes.keys()): 1078 self.doaxiscreate(axisname) 1079 1080 def dobackground(self): 1081 if self.did(self.dobackground): 1082 return 1083 1084 def doaxes(self): 1085 if self.did(self.doaxes): 1086 return 1087 self.dolayout() 1088 self.dobackground() 1089 for axis in list(self.axes.values()): 1090 if axis.hidden: 1091 self.layer("hiddenaxes").insert(axis.canvas) 1092 else: 1093 self.layer("axes").insert(axis.canvas) 1094 1095 def dokey(self): 1096 if self.did(self.dokey): 1097 return 1098 self.dobackground() 1099 for plotitem in self.plotitems: 1100 self.dokeyitem(plotitem) 1101 if self.key is not None: 1102 c = self.key.paint(self.keyitems) 1103 bbox = c.bbox() 1104 def parentchildalign(pmin, pmax, cmin, cmax, pos, dist, inside): 1105 ppos = pmin+0.5*(cmax-cmin)+dist+pos*(pmax-pmin-cmax+cmin-2*dist) 1106 cpos = 0.5*(cmin+cmax)+(1-inside)*(1-2*pos)*(cmax-cmin+2*dist) 1107 return ppos-cpos 1108 if bbox: 1109 x = parentchildalign(self.xpos_pt, self.xpos_pt+self.size_pt, 1110 bbox.llx_pt, bbox.urx_pt, 1111 self.key.hpos, unit.topt(self.key.hdist), self.key.hinside) 1112 y = parentchildalign(self.ypos_pt, self.ypos_pt+self.size_pt, 1113 bbox.lly_pt, bbox.ury_pt, 1114 self.key.vpos, unit.topt(self.key.vdist), self.key.vinside) 1115 self.insert(c, [trafo.translate_pt(x, y)]) 1116