vedo.pyplot
Advanced plotting functionalities.
1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3from typing import Union 4from typing_extensions import Self 5import numpy as np 6 7import vedo.vtkclasses as vtki 8 9import vedo 10from vedo import settings 11from vedo.transformations import cart2spher, spher2cart 12from vedo import addons 13from vedo import colors 14from vedo import utils 15from vedo import shapes 16from vedo.pointcloud import merge 17from vedo.mesh import Mesh 18from vedo.assembly import Assembly 19 20__docformat__ = "google" 21 22__doc__ = """ 23Advanced plotting functionalities. 24 25 26""" 27 28__all__ = [ 29 "Figure", 30 "Histogram1D", 31 "Histogram2D", 32 "PlotXY", 33 "PlotBars", 34 "plot", 35 "histogram", 36 "fit", 37 "pie_chart", 38 "violin", 39 "whisker", 40 "streamplot", 41 "matrix", 42 "DirectedGraph", 43] 44 45 46########################################################################## 47class LabelData: 48 """Helper internal class to hold label information.""" 49 50 def __init__(self): 51 """Helper internal class to hold label information.""" 52 self.text = "dataset" 53 self.tcolor = "black" 54 self.marker = "s" 55 self.mcolor = "black" 56 57 58########################################################################## 59class Figure(Assembly): 60 """Format class for figures.""" 61 62 def __init__(self, xlim, ylim, aspect=4 / 3, padding=(0.05, 0.05, 0.05, 0.05), **kwargs): 63 """ 64 Create an empty formatted figure for plotting. 65 66 Arguments: 67 xlim : (list) 68 range of the x-axis as [x0, x1] 69 ylim : (list) 70 range of the y-axis as [y0, y1] 71 aspect : (float, str) 72 the desired aspect ratio of the histogram. Default is 4/3. 73 Use `aspect="equal"` to force the same units in x and y. 74 padding : (float, list) 75 keep a padding space from the axes (as a fraction of the axis size). 76 This can be a list of four numbers. 77 xtitle : (str) 78 title for the x-axis, can also be set using `axes=dict(xtitle="my x axis")` 79 ytitle : (str) 80 title for the y-axis, can also be set using `axes=dict(ytitle="my y axis")` 81 grid : (bool) 82 show the background grid for the axes, can also be set using `axes=dict(xygrid=True)` 83 axes : (dict) 84 an extra dictionary of options for the `vedo.addons.Axes` object 85 """ 86 87 self.verbose = True # printing to stdout on every mouse click 88 89 self.xlim = np.asarray(xlim) 90 self.ylim = np.asarray(ylim) 91 self.aspect = aspect 92 self.padding = padding 93 if not utils.is_sequence(self.padding): 94 self.padding = [self.padding, self.padding, self.padding, self.padding] 95 96 self.force_scaling_types = ( 97 shapes.Glyph, 98 shapes.Line, 99 shapes.Rectangle, 100 shapes.DashedLine, 101 shapes.Tube, 102 shapes.Ribbon, 103 shapes.GeoCircle, 104 shapes.Arc, 105 shapes.Grid, 106 # shapes.Arrows, # todo 107 # shapes.Arrows2D, # todo 108 shapes.Brace, # todo 109 ) 110 111 options = dict(kwargs) 112 113 self.title = options.pop("title", "") 114 self.xtitle = options.pop("xtitle", " ") 115 self.ytitle = options.pop("ytitle", " ") 116 number_of_divisions = 6 117 118 self.legend = None 119 self.labels = [] 120 self.label = options.pop("label", None) 121 if self.label: 122 self.labels = [self.label] 123 124 self.axopts = options.pop("axes", {}) 125 if isinstance(self.axopts, (bool, int, float)): 126 if self.axopts: 127 self.axopts = {} 128 if self.axopts or isinstance(self.axopts, dict): 129 number_of_divisions = self.axopts.pop("number_of_divisions", number_of_divisions) 130 131 self.axopts["xtitle"] = self.xtitle 132 self.axopts["ytitle"] = self.ytitle 133 134 if "xygrid" not in self.axopts: ## modify the default 135 self.axopts["xygrid"] = options.pop("grid", False) 136 137 if "xygrid_transparent" not in self.axopts: ## modify the default 138 self.axopts["xygrid_transparent"] = True 139 140 if "xtitle_position" not in self.axopts: ## modify the default 141 self.axopts["xtitle_position"] = 0.5 142 self.axopts["xtitle_justify"] = "top-center" 143 144 if "ytitle_position" not in self.axopts: ## modify the default 145 self.axopts["ytitle_position"] = 0.5 146 self.axopts["ytitle_justify"] = "bottom-center" 147 148 if self.label: 149 if "c" in self.axopts: 150 self.label.tcolor = self.axopts["c"] 151 152 x0, x1 = self.xlim 153 y0, y1 = self.ylim 154 dx = x1 - x0 155 dy = y1 - y0 156 x0lim, x1lim = (x0 - self.padding[0] * dx, x1 + self.padding[1] * dx) 157 y0lim, y1lim = (y0 - self.padding[2] * dy, y1 + self.padding[3] * dy) 158 dy = y1lim - y0lim 159 160 self.axes = None 161 if xlim[0] >= xlim[1] or ylim[0] >= ylim[1]: 162 vedo.logger.warning(f"Null range for Figure {self.title}... returning an empty Assembly.") 163 super().__init__() 164 self.yscale = 0 165 return 166 167 if aspect == "equal": 168 self.aspect = dx / dy # so that yscale becomes 1 169 170 self.yscale = dx / dy / self.aspect 171 172 y0lim *= self.yscale 173 y1lim *= self.yscale 174 175 self.x0lim = x0lim 176 self.x1lim = x1lim 177 self.y0lim = y0lim 178 self.y1lim = y1lim 179 180 self.ztolerance = options.pop("ztolerance", None) 181 if self.ztolerance is None: 182 self.ztolerance = dx / 5000 183 184 ############## create axes 185 if self.axopts: 186 axes_opts = self.axopts 187 if self.axopts is True or self.axopts == 1: 188 axes_opts = {} 189 190 tp, ts = utils.make_ticks(y0lim / self.yscale, 191 y1lim / self.yscale, number_of_divisions) 192 labs = [] 193 for i in range(1, len(tp) - 1): 194 ynew = utils.lin_interpolate(tp[i], [0, 1], [y0lim, y1lim]) 195 labs.append([ynew, ts[i]]) 196 197 if self.title: 198 axes_opts["htitle"] = self.title 199 axes_opts["y_values_and_labels"] = labs 200 axes_opts["xrange"] = (x0lim, x1lim) 201 axes_opts["yrange"] = (y0lim, y1lim) 202 axes_opts["zrange"] = (0, 0) 203 axes_opts["y_use_bounds"] = True 204 205 if "c" not in axes_opts and "ac" in options: 206 axes_opts["c"] = options["ac"] 207 208 self.axes = addons.Axes(**axes_opts) 209 210 super().__init__([self.axes]) 211 self.name = "Figure" 212 213 vedo.last_figure = self if settings.remember_last_figure_format else None 214 215 216 ################################################################## 217 def _repr_html_(self): 218 """ 219 HTML representation of the Figure object for Jupyter Notebooks. 220 221 Returns: 222 HTML text with the image and some properties. 223 """ 224 import io 225 import base64 226 from PIL import Image 227 228 library_name = "vedo.pyplot.Figure" 229 help_url = "https://vedo.embl.es/docs/vedo/pyplot.html#Figure" 230 231 arr = self.thumbnail(zoom=1.1) 232 233 im = Image.fromarray(arr) 234 buffered = io.BytesIO() 235 im.save(buffered, format="PNG", quality=100) 236 encoded = base64.b64encode(buffered.getvalue()).decode("utf-8") 237 url = "data:image/png;base64," + encoded 238 image = f"<img src='{url}'></img>" 239 240 bounds = "<br/>".join( 241 [ 242 vedo.utils.precision(min_x, 4) + " ... " + vedo.utils.precision(max_x, 4) 243 for min_x, max_x in zip(self.bounds()[::2], self.bounds()[1::2]) 244 ] 245 ) 246 247 help_text = "" 248 if self.name: 249 help_text += f"<b> {self.name}:   </b>" 250 help_text += '<b><a href="' + help_url + '" target="_blank">' + library_name + "</a></b>" 251 if self.filename: 252 dots = "" 253 if len(self.filename) > 30: 254 dots = "..." 255 help_text += f"<br/><code><i>({dots}{self.filename[-30:]})</i></code>" 256 257 all = [ 258 "<table>", 259 "<tr>", 260 "<td>", 261 image, 262 "</td>", 263 "<td style='text-align: center; vertical-align: center;'><br/>", 264 help_text, 265 "<table>", 266 "<tr><td><b> nr. of parts </b></td><td>" + str(self.GetNumberOfPaths()) + "</td></tr>", 267 "<tr><td><b> position </b></td><td>" + str(self.GetPosition()) + "</td></tr>", 268 "<tr><td><b> x-limits </b></td><td>" + utils.precision(self.xlim, 4) + "</td></tr>", 269 "<tr><td><b> y-limits </b></td><td>" + utils.precision(self.ylim, 4) + "</td></tr>", 270 "<tr><td><b> world bounds </b> <br/> (x/y/z) </td><td>" + str(bounds) + "</td></tr>", 271 "</table>", 272 "</table>", 273 ] 274 return "\n".join(all) 275 276 def __add__(self, *obj): 277 # just to avoid confusion, supersede Assembly.__add__ 278 return self.__iadd__(*obj) 279 280 def __iadd__(self, *obj): 281 if len(obj) == 1 and isinstance(obj[0], Figure): 282 return self._check_unpack_and_insert(obj[0]) 283 284 obj = utils.flatten(obj) 285 return self.insert(*obj) 286 287 def _check_unpack_and_insert(self, fig: "Figure") -> Self: 288 289 if fig.label: 290 self.labels.append(fig.label) 291 292 if abs(self.yscale - fig.yscale) > 0.0001: 293 294 colors.printc(":bomb:ERROR: adding incompatible Figure. Y-scales are different:", c='r', invert=True) 295 colors.printc(" first figure:", self.yscale, c='r') 296 colors.printc(" second figure:", fig.yscale, c='r') 297 298 colors.printc("One or more of these parameters can be the cause:", c="r") 299 if list(self.xlim) != list(fig.xlim): 300 colors.printc("xlim --------------------------------------------\n", 301 " first figure:", self.xlim, "\n", 302 " second figure:", fig.xlim, c='r') 303 if list(self.ylim) != list(fig.ylim): 304 colors.printc("ylim --------------------------------------------\n", 305 " first figure:", self.ylim, "\n", 306 " second figure:", fig.ylim, c='r') 307 if list(self.padding) != list(fig.padding): 308 colors.printc("padding -----------------------------------------\n", 309 " first figure:", self.padding, 310 " second figure:", fig.padding, c='r') 311 if self.aspect != fig.aspect: 312 colors.printc("aspect ------------------------------------------\n", 313 " first figure:", self.aspect, "\n", 314 " second figure:", fig.aspect, c='r') 315 316 colors.printc("\n:idea: Consider using fig2 = histogram(..., like=fig1)", c="r") 317 colors.printc(" Or fig += histogram(..., like=fig)\n", c="r") 318 return self 319 320 offset = self.zbounds()[1] + self.ztolerance 321 322 for ele in fig.unpack(): 323 if "Axes" in ele.name: 324 continue 325 ele.z(offset) 326 self.insert(ele, rescale=False) 327 328 return self 329 330 def insert(self, *objs, rescale=True, as3d=True, adjusted=False, cut=True) -> Self: 331 """ 332 Insert objects into a Figure. 333 334 The recommended syntax is to use "+=", which calls `insert()` under the hood. 335 If a whole Figure is added with "+=", it is unpacked and its objects are added 336 one by one. 337 338 Arguments: 339 rescale : (bool) 340 rescale the y axis position while inserting the object. 341 as3d : (bool) 342 if True keep the aspect ratio of the 3d object, otherwise stretch it in y. 343 adjusted : (bool) 344 adjust the scaling according to the shortest axis 345 cut : (bool) 346 cut off the parts of the object which go beyond the axes frame. 347 """ 348 for a in objs: 349 350 if a in self.objects: 351 # should not add twice the same object in plot 352 continue 353 354 if isinstance(a, vedo.Points): # hacky way to identify Points 355 if a.ncells == a.npoints: 356 poly = a.dataset 357 if poly.GetNumberOfPolys() == 0 and poly.GetNumberOfLines() == 0: 358 as3d = False 359 rescale = True 360 361 if isinstance(a, (shapes.Arrow, shapes.Arrow2D)): 362 # discard input Arrow and substitute it with a brand new one 363 # (because scaling would fatally distort the shape) 364 365 py = a.base[1] 366 a.top[1] = (a.top[1] - py) * self.yscale + py 367 b = shapes.Arrow2D(a.base, a.top, s=a.s, fill=a.fill).z(a.z()) 368 369 prop = a.properties 370 prop.LightingOff() 371 b.actor.SetProperty(prop) 372 b.properties = prop 373 b.y(py * self.yscale) 374 a = b 375 376 # elif isinstance(a, shapes.Rectangle) and a.radius is not None: 377 # # discard input Rectangle and substitute it with a brand new one 378 # # (because scaling would fatally distort the shape of the corners) 379 # py = a.corner1[1] 380 # rx1,ry1,rz1 = a.corner1 381 # rx2,ry2,rz2 = a.corner2 382 # ry2 = (ry2-py) * self.yscale + py 383 # b = shapes.Rectangle([rx1,0,rz1], [rx2,ry2,rz2], radius=a.radius).z(a.z()) 384 # b.SetProperty(a.properties) 385 # b.y(py / self.yscale) 386 # a = b 387 388 else: 389 390 if rescale: 391 392 if not isinstance(a, Figure): 393 394 if as3d and not isinstance(a, self.force_scaling_types): 395 if adjusted: 396 scl = np.min([1, self.yscale]) 397 else: 398 scl = self.yscale 399 400 a.scale(scl) 401 402 else: 403 a.scale([1, self.yscale, 1]) 404 405 # shift it in y 406 a.y(a.y() * self.yscale) 407 408 if cut: 409 try: 410 bx0, bx1, by0, by1, _, _ = a.bounds() 411 if self.y0lim > by0: 412 a.cut_with_plane([0, self.y0lim, 0], [0, 1, 0]) 413 if self.y1lim < by1: 414 a.cut_with_plane([0, self.y1lim, 0], [0, -1, 0]) 415 if self.x0lim > bx0: 416 a.cut_with_plane([self.x0lim, 0, 0], [1, 0, 0]) 417 if self.x1lim < bx1: 418 a.cut_with_plane([self.x1lim, 0, 0], [-1, 0, 0]) 419 except: 420 # print("insert(): cannot cut", [a]) 421 pass 422 423 self.AddPart(a.actor) 424 self.objects.append(a) 425 426 return self 427 428 def add_label(self, text: str, c=None, marker="", mc="black") -> Self: 429 """ 430 Manually add en entry label to the legend. 431 432 Arguments: 433 text : (str) 434 text string for the label. 435 c : (str) 436 color of the text 437 marker : (str), Mesh 438 a marker char or a Mesh object to be used as marker 439 mc : (str) 440 color for the marker 441 """ 442 newlabel = LabelData() 443 newlabel.text = text.replace("\n", " ") 444 newlabel.tcolor = c 445 newlabel.marker = marker 446 newlabel.mcolor = mc 447 self.labels.append(newlabel) 448 return self 449 450 def add_legend( 451 self, 452 pos="top-right", 453 relative=True, 454 font=None, 455 s=1, 456 c=None, 457 vspace=1.75, 458 padding=0.1, 459 radius=0, 460 alpha=1, 461 bc="k7", 462 lw=1, 463 lc="k4", 464 z=0, 465 ) -> Self: 466 """ 467 Add existing labels to form a legend box. 468 Labels have been previously filled with eg: `plot(..., label="text")` 469 470 Arguments: 471 pos : (str, list) 472 A string or 2D coordinates. The default is "top-right". 473 relative : (bool) 474 control whether `pos` is absolute or relative, e.i. normalized 475 to the x and y ranges so that x and y in `pos=[x,y]` should be 476 both in the range [0,1]. 477 This flag is ignored if a string despcriptor is passed. 478 Default is True. 479 font : (str, int) 480 font name or number. 481 Check [available fonts here](https://vedo.embl.es/fonts). 482 s : (float) 483 global size of the legend 484 c : (str) 485 color of the text 486 vspace : (float) 487 vertical spacing of lines 488 padding : (float) 489 padding of the box as a fraction of the text size 490 radius : (float) 491 border radius of the box 492 alpha : (float) 493 opacity of the box. Values below 1 may cause poor rendering 494 because of antialiasing. 495 Use alpha = 0 to remove the box. 496 bc : (str) 497 box color 498 lw : (int) 499 border line width of the box in pixel units 500 lc : (int) 501 border line color of the box 502 z : (float) 503 set the zorder as z position (useful to avoid overlap) 504 """ 505 sx = self.x1lim - self.x0lim 506 s = s * sx / 55 # so that input can be about 1 507 508 ds = 0 509 texts = [] 510 mks = [] 511 for i, t in enumerate(self.labels): 512 label = self.labels[i] 513 t = label.text 514 515 if label.tcolor is not None: 516 c = label.tcolor 517 518 tx = vedo.shapes.Text3D(t, s=s, c=c, justify="center-left", font=font) 519 y0, y1 = tx.ybounds() 520 ds = max(y1 - y0, ds) 521 texts.append(tx) 522 523 mk = label.marker 524 if isinstance(mk, vedo.Points): 525 mk = mk.clone(deep=False).lighting("off") 526 cm = mk.center_of_mass() 527 ty0, ty1 = tx.ybounds() 528 oby0, oby1 = mk.ybounds() 529 mk.shift(-cm) 530 mk.SetOrigin(cm) 531 mk.scale((ty1 - ty0) / (oby1 - oby0)) 532 mk.scale([1.1, 1.1, 0.01]) 533 elif mk == "-": 534 mk = vedo.shapes.Marker(mk, s=s * 2) 535 mk.color(label.mcolor) 536 else: 537 mk = vedo.shapes.Marker(mk, s=s) 538 mk.color(label.mcolor) 539 mks.append(mk) 540 541 for i, tx in enumerate(texts): 542 tx.shift(0, -(i + 0) * ds * vspace) 543 544 for i, mk in enumerate(mks): 545 mk.shift(-ds * 1.75, -(i + 0) * ds * vspace, 0) 546 547 acts = texts + mks 548 549 aleg = Assembly(acts) # .show(axes=1).close() 550 x0, x1, y0, y1, _, _ = aleg.GetBounds() 551 552 if alpha: 553 dx = x1 - x0 554 dy = y1 - y0 555 556 if not utils.is_sequence(padding): 557 padding = [padding] * 4 558 padding = min(padding) 559 padding = min(padding * dx, padding * dy) 560 if len(self.labels) == 1: 561 padding *= 4 562 x0 -= padding 563 x1 += padding 564 y0 -= padding 565 y1 += padding 566 567 box = shapes.Rectangle([x0, y0], [x1, y1], radius=radius, c=bc, alpha=alpha) 568 box.shift(0, 0, -dy / 100).pickable(False) 569 if lc: 570 box.lc(lc).lw(lw) 571 aleg.AddPart(box.actor) 572 aleg.objects.append(box) 573 574 xlim = self.xlim 575 ylim = self.ylim 576 if isinstance(pos, str): 577 px, py = 0.0, 0.0 578 rx, ry = (xlim[1] + xlim[0]) / 2, (ylim[1] + ylim[0]) / 2 579 shx, shy = 0.0, 0.0 580 if "top" in pos: 581 if "cent" in pos: 582 px, py = rx, ylim[1] 583 shx, shy = (x0 + x1) / 2, y1 584 elif "left" in pos: 585 px, py = xlim[0], ylim[1] 586 shx, shy = x0, y1 587 else: # "right" 588 px, py = xlim[1], ylim[1] 589 shx, shy = x1, y1 590 elif "bot" in pos: 591 if "left" in pos: 592 px, py = xlim[0], ylim[0] 593 shx, shy = x0, y0 594 elif "right" in pos: 595 px, py = xlim[1], ylim[0] 596 shx, shy = x1, y0 597 else: # "cent" 598 px, py = rx, ylim[0] 599 shx, shy = (x0 + x1) / 2, y0 600 elif "cent" in pos: 601 if "left" in pos: 602 px, py = xlim[0], ry 603 shx, shy = x0, (y0 + y1) / 2 604 elif "right" in pos: 605 px, py = xlim[1], ry 606 shx, shy = x1, (y0 + y1) / 2 607 else: 608 vedo.logger.error(f"in add_legend(), cannot understand {pos}") 609 raise RuntimeError 610 611 else: 612 613 if relative: 614 rx, ry = pos[0], pos[1] 615 px = (xlim[1] - xlim[0]) * rx + xlim[0] 616 py = (ylim[1] - ylim[0]) * ry + ylim[0] 617 z *= xlim[1] - xlim[0] 618 else: 619 px, py = pos[0], pos[1] 620 shx, shy = x0, y1 621 622 zpos = aleg.pos()[2] 623 aleg.pos(px - shx, py * self.yscale - shy, zpos + sx / 50 + z) 624 625 self.insert(aleg, rescale=False, cut=False) 626 self.legend = aleg 627 aleg.name = "Legend" 628 return self 629 630 631######################################################################################### 632class Histogram1D(Figure): 633 "1D histogramming." 634 635 def __init__( 636 self, 637 data, 638 weights=None, 639 bins=None, 640 errors=False, 641 density=False, 642 logscale=False, 643 max_entries=None, 644 fill=True, 645 radius=0.075, 646 c="olivedrab", 647 gap=0.0, 648 alpha=1, 649 outline=False, 650 lw=2, 651 lc="k", 652 texture="", 653 marker="", 654 ms=None, 655 mc=None, 656 ma=None, 657 # Figure and axes options: 658 like=None, 659 xlim=None, 660 ylim=(0, None), 661 aspect=4 / 3, 662 padding=(0.0, 0.0, 0.0, 0.05), 663 title="", 664 xtitle=" ", 665 ytitle=" ", 666 ac="k", 667 grid=False, 668 ztolerance=None, 669 label="", 670 **fig_kwargs, 671 ): 672 """ 673 Creates a `Histogram1D(Figure)` object. 674 675 Arguments: 676 weights : (list) 677 An array of weights, of the same shape as `data`. Each value in `data` 678 only contributes its associated weight towards the bin count (instead of 1). 679 bins : (int) 680 number of bins 681 density : (bool) 682 normalize the area to 1 by dividing by the nr of entries and bin size 683 logscale : (bool) 684 use logscale on y-axis 685 max_entries : (int) 686 if `data` is larger than `max_entries`, a random sample of `max_entries` is used 687 fill : (bool) 688 fill bars with solid color `c` 689 gap : (float) 690 leave a small space btw bars 691 radius : (float) 692 border radius of the top of the histogram bar. Default value is 0.1. 693 texture : (str) 694 url or path to an image to be used as texture for the bin 695 outline : (bool) 696 show outline of the bins 697 errors : (bool) 698 show error bars 699 xtitle : (str) 700 title for the x-axis, can also be set using `axes=dict(xtitle="my x axis")` 701 ytitle : (str) 702 title for the y-axis, can also be set using `axes=dict(ytitle="my y axis")` 703 padding : (float), list 704 keep a padding space from the axes (as a fraction of the axis size). 705 This can be a list of four numbers. 706 aspect : (float) 707 the desired aspect ratio of the histogram. Default is 4/3. 708 grid : (bool) 709 show the background grid for the axes, can also be set using `axes=dict(xygrid=True)` 710 ztolerance : (float) 711 a tolerance factor to superimpose objects (along the z-axis). 712 713 Examples: 714 - [histo_1d_a.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/histo_1d_a.py) 715 - [histo_1d_b.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/histo_1d_b.py) 716 - [histo_1d_c.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/histo_1d_c.py) 717 - [histo_1d_d.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/histo_1d_d.py) 718 719  720 """ 721 722 if max_entries and data.shape[0] > max_entries: 723 data = np.random.choice(data, int(max_entries)) 724 725 # purge NaN from data 726 valid_ids = np.all(np.logical_not(np.isnan(data))) 727 data = np.asarray(data[valid_ids]).ravel() 728 729 # if data.dtype is integer try to center bins by default 730 if like is None and bins is None and np.issubdtype(data.dtype, np.integer): 731 if xlim is None and ylim == (0, None): 732 x1, x0 = data.max(), data.min() 733 if 0 < x1 - x0 <= 100: 734 bins = x1 - x0 + 1 735 xlim = (x0 - 0.5, x1 + 0.5) 736 737 if like is None and vedo.last_figure is not None: 738 if xlim is None and ylim == (0, None): 739 like = vedo.last_figure 740 741 if like is not None: 742 xlim = like.xlim 743 ylim = like.ylim 744 aspect = like.aspect 745 padding = like.padding 746 if bins is None: 747 bins = like.bins 748 if bins is None: 749 bins = 20 750 751 if utils.is_sequence(xlim): 752 # deal with user passing eg [x0, None] 753 _x0, _x1 = xlim 754 if _x0 is None: 755 _x0 = data.min() 756 if _x1 is None: 757 _x1 = data.max() 758 xlim = [_x0, _x1] 759 760 fs, edges = np.histogram(data, bins=bins, weights=weights, range=xlim) 761 binsize = edges[1] - edges[0] 762 ntot = data.shape[0] 763 764 fig_kwargs["title"] = title 765 fig_kwargs["xtitle"] = xtitle 766 fig_kwargs["ytitle"] = ytitle 767 fig_kwargs["ac"] = ac 768 fig_kwargs["ztolerance"] = ztolerance 769 fig_kwargs["grid"] = grid 770 771 unscaled_errors = np.sqrt(fs) 772 if density: 773 scaled_errors = unscaled_errors / (ntot * binsize) 774 fs = fs / (ntot * binsize) 775 if ytitle == " ": 776 ytitle = f"counts / ({ntot} x {utils.precision(binsize,3)})" 777 fig_kwargs["ytitle"] = ytitle 778 elif logscale: 779 se_up = np.log10(fs + unscaled_errors / 2 + 1) 780 se_dw = np.log10(fs - unscaled_errors / 2 + 1) 781 scaled_errors = np.c_[se_up, se_dw] 782 fs = np.log10(fs + 1) 783 if ytitle == " ": 784 ytitle = "log_10 (counts+1)" 785 fig_kwargs["ytitle"] = ytitle 786 787 x0, x1 = np.min(edges), np.max(edges) 788 y0, y1 = ylim[0], np.max(fs) 789 790 _errors = [] 791 if errors: 792 if density: 793 y1 += max(scaled_errors) / 2 794 _errors = scaled_errors 795 elif logscale: 796 y1 = max(scaled_errors[:, 0]) 797 _errors = scaled_errors 798 else: 799 y1 += max(unscaled_errors) / 2 800 _errors = unscaled_errors 801 802 if like is None: 803 ylim = list(ylim) 804 if xlim is None: 805 xlim = [x0, x1] 806 if ylim[1] is None: 807 ylim[1] = y1 808 if ylim[0] != 0: 809 ylim[0] = y0 810 811 self.title = title 812 self.xtitle = xtitle 813 self.ytitle = ytitle 814 self.entries = ntot 815 self.frequencies = fs 816 self.errors = _errors 817 self.edges = edges 818 self.centers = (edges[0:-1] + edges[1:]) / 2 819 self.mean = data.mean() 820 self.mode = self.centers[np.argmax(fs)] 821 self.std = data.std() 822 self.bins = edges # internally used by "like" 823 824 ############################### stats legend as htitle 825 addstats = False 826 if not title: 827 if "axes" not in fig_kwargs: 828 addstats = True 829 axes_opts = {} 830 fig_kwargs["axes"] = axes_opts 831 elif fig_kwargs["axes"] is False: 832 pass 833 else: 834 axes_opts = fig_kwargs["axes"] 835 if "htitle" not in axes_opts: 836 addstats = True 837 838 if addstats: 839 htitle = f"Entries:~~{int(self.entries)} " 840 htitle += f"Mean:~~{utils.precision(self.mean, 4)} " 841 htitle += f"STD:~~{utils.precision(self.std, 4)} " 842 843 axes_opts["htitle"] = htitle 844 axes_opts["htitle_justify"] = "bottom-left" 845 axes_opts["htitle_size"] = 0.016 846 # axes_opts["htitle_offset"] = [-0.49, 0.01, 0] 847 848 if mc is None: 849 mc = lc 850 if ma is None: 851 ma = alpha 852 853 if label: 854 nlab = LabelData() 855 nlab.text = label 856 nlab.tcolor = ac 857 nlab.marker = marker 858 nlab.mcolor = mc 859 if not marker: 860 nlab.marker = "s" 861 nlab.mcolor = c 862 fig_kwargs["label"] = nlab 863 864 ############################################### Figure init 865 super().__init__(xlim, ylim, aspect, padding, **fig_kwargs) 866 867 if not self.yscale: 868 return 869 870 if utils.is_sequence(bins): 871 myedges = np.array(bins) 872 bins = len(bins) - 1 873 else: 874 myedges = edges 875 876 bin_centers = [] 877 for i in range(bins): 878 x = (myedges[i] + myedges[i + 1]) / 2 879 bin_centers.append([x, fs[i], 0]) 880 881 rs = [] 882 maxheigth = 0 883 if not fill and not outline and not errors and not marker: 884 outline = True # otherwise it's empty.. 885 886 if fill: ##################### 887 if outline: 888 gap = 0 889 890 for i in range(bins): 891 F = fs[i] 892 if not F: 893 continue 894 p0 = (myedges[i] + gap * binsize, 0, 0) 895 p1 = (myedges[i + 1] - gap * binsize, F, 0) 896 897 if radius: 898 if gap: 899 rds = np.array([0, 0, radius, radius]) 900 else: 901 rd1 = 0 if i < bins - 1 and fs[i + 1] >= F else radius / 2 902 rd2 = 0 if i > 0 and fs[i - 1] >= F else radius / 2 903 rds = np.array([0, 0, rd1, rd2]) 904 p1_yscaled = [p1[0], p1[1] * self.yscale, 0] 905 r = shapes.Rectangle(p0, p1_yscaled, radius=rds * binsize, res=6) 906 r.scale([1, 1 / self.yscale, 1]) 907 r.radius = None # so it doesnt get recreated and rescaled by insert() 908 else: 909 r = shapes.Rectangle(p0, p1) 910 911 if texture: 912 r.texture(texture) 913 c = "w" 914 915 r.actor.PickableOff() 916 maxheigth = max(maxheigth, p1[1]) 917 if c in colors.cmaps_names: 918 col = colors.color_map((p0[0] + p1[0]) / 2, c, myedges[0], myedges[-1]) 919 else: 920 col = c 921 r.color(col).alpha(alpha).lighting("off") 922 r.z(self.ztolerance) 923 rs.append(r) 924 925 if outline: ##################### 926 lns = [[myedges[0], 0, 0]] 927 for i in range(bins): 928 lns.append([myedges[i], fs[i], 0]) 929 lns.append([myedges[i + 1], fs[i], 0]) 930 maxheigth = max(maxheigth, fs[i]) 931 lns.append([myedges[-1], 0, 0]) 932 outl = shapes.Line(lns, c=lc, alpha=alpha, lw=lw) 933 outl.z(self.ztolerance * 2) 934 rs.append(outl) 935 936 if errors: ##################### 937 for i in range(bins): 938 x = self.centers[i] 939 f = fs[i] 940 if not f: 941 continue 942 err = _errors[i] 943 if utils.is_sequence(err): 944 el = shapes.Line([x, err[0], 0], [x, err[1], 0], c=lc, alpha=alpha, lw=lw) 945 else: 946 el = shapes.Line( 947 [x, f - err / 2, 0], [x, f + err / 2, 0], c=lc, alpha=alpha, lw=lw 948 ) 949 el.z(self.ztolerance * 3) 950 rs.append(el) 951 952 if marker: ##################### 953 954 # remove empty bins (we dont want a marker there) 955 bin_centers = np.array(bin_centers) 956 bin_centers = bin_centers[bin_centers[:, 1] > 0] 957 958 if utils.is_sequence(ms): ### variable point size 959 mk = shapes.Marker(marker, s=1) 960 mk.scale([1, 1 / self.yscale, 1]) 961 msv = np.zeros_like(bin_centers) 962 msv[:, 0] = ms 963 marked = shapes.Glyph( 964 bin_centers, mk, c=mc, orientation_array=msv, scale_by_vector_size=True 965 ) 966 else: ### fixed point size 967 968 if ms is None: 969 ms = (xlim[1] - xlim[0]) / 100.0 970 else: 971 ms = (xlim[1] - xlim[0]) / 100.0 * ms 972 973 if utils.is_sequence(mc): 974 mk = shapes.Marker(marker, s=ms) 975 mk.scale([1, 1 / self.yscale, 1]) 976 msv = np.zeros_like(bin_centers) 977 msv[:, 0] = 1 978 marked = shapes.Glyph( 979 bin_centers, mk, c=mc, orientation_array=msv, scale_by_vector_size=True 980 ) 981 else: 982 mk = shapes.Marker(marker, s=ms) 983 mk.scale([1, 1 / self.yscale, 1]) 984 marked = shapes.Glyph(bin_centers, mk, c=mc) 985 986 marked.alpha(ma) 987 marked.z(self.ztolerance * 4) 988 rs.append(marked) 989 990 self.insert(*rs, as3d=False) 991 self.name = "Histogram1D" 992 993 def print(self, **kwargs) -> None: 994 """Print infos about this histogram""" 995 txt = ( 996 f"{self.name} {self.title}\n" 997 f" xtitle = '{self.xtitle}'\n" 998 f" ytitle = '{self.ytitle}'\n" 999 f" entries = {self.entries}\n" 1000 f" mean = {self.mean}\n" 1001 f" std = {self.std}" 1002 ) 1003 colors.printc(txt, **kwargs) 1004 1005 1006######################################################################################### 1007class Histogram2D(Figure): 1008 """2D histogramming.""" 1009 1010 def __init__( 1011 self, 1012 xvalues, 1013 yvalues=None, 1014 bins=25, 1015 weights=None, 1016 cmap="cividis", 1017 alpha=1, 1018 gap=0, 1019 scalarbar=True, 1020 # Figure and axes options: 1021 like=None, 1022 xlim=None, 1023 ylim=(None, None), 1024 zlim=(None, None), 1025 aspect=1, 1026 title="", 1027 xtitle=" ", 1028 ytitle=" ", 1029 ztitle="", 1030 ac="k", 1031 **fig_kwargs, 1032 ): 1033 """ 1034 Input data formats `[(x1,x2,..), (y1,y2,..)] or [(x1,y1), (x2,y2),..]` 1035 are both valid. 1036 1037 Use keyword `like=...` if you want to use the same format of a previously 1038 created Figure (useful when superimposing Figures) to make sure 1039 they are compatible and comparable. If they are not compatible 1040 you will receive an error message. 1041 1042 Arguments: 1043 bins : (list) 1044 binning as (nx, ny) 1045 weights : (list) 1046 array of weights to assign to each entry 1047 cmap : (str, lookuptable) 1048 color map name or look up table 1049 alpha : (float) 1050 opacity of the histogram 1051 gap : (float) 1052 separation between adjacent bins as a fraction for their size 1053 scalarbar : (bool) 1054 add a scalarbar to right of the histogram 1055 like : (Figure) 1056 grab and use the same format of the given Figure (for superimposing) 1057 xlim : (list) 1058 [x0, x1] range of interest. If left to None will automatically 1059 choose the minimum or the maximum of the data range. 1060 Data outside the range are completely ignored. 1061 ylim : (list) 1062 [y0, y1] range of interest. If left to None will automatically 1063 choose the minimum or the maximum of the data range. 1064 Data outside the range are completely ignored. 1065 aspect : (float) 1066 the desired aspect ratio of the figure. 1067 title : (str) 1068 title of the plot to appear on top. 1069 If left blank some statistics will be shown. 1070 xtitle : (str) 1071 x axis title 1072 ytitle : (str) 1073 y axis title 1074 ztitle : (str) 1075 title for the scalar bar 1076 ac : (str) 1077 axes color, additional keyword for Axes can also be added 1078 using e.g. `axes=dict(xygrid=True)` 1079 1080 Examples: 1081 - [histo_2d_a.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/histo_2d_a.py) 1082 - [histo_2d_b.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/histo_2d_b.py) 1083 1084  1085 """ 1086 xvalues = np.asarray(xvalues) 1087 if yvalues is None: 1088 # assume [(x1,y1), (x2,y2) ...] format 1089 yvalues = xvalues[:, 1] 1090 xvalues = xvalues[:, 0] 1091 else: 1092 yvalues = np.asarray(yvalues) 1093 1094 padding = [0, 0, 0, 0] 1095 1096 if like is None and vedo.last_figure is not None: 1097 if xlim is None and ylim == (None, None) and zlim == (None, None): 1098 like = vedo.last_figure 1099 1100 if like is not None: 1101 xlim = like.xlim 1102 ylim = like.ylim 1103 aspect = like.aspect 1104 padding = like.padding 1105 if bins is None: 1106 bins = like.bins 1107 if bins is None: 1108 bins = 20 1109 1110 if isinstance(bins, int): 1111 bins = (bins, bins) 1112 1113 if utils.is_sequence(xlim): 1114 # deal with user passing eg [x0, None] 1115 _x0, _x1 = xlim 1116 if _x0 is None: 1117 _x0 = xvalues.min() 1118 if _x1 is None: 1119 _x1 = xvalues.max() 1120 xlim = [_x0, _x1] 1121 1122 if utils.is_sequence(ylim): 1123 # deal with user passing eg [x0, None] 1124 _y0, _y1 = ylim 1125 if _y0 is None: 1126 _y0 = yvalues.min() 1127 if _y1 is None: 1128 _y1 = yvalues.max() 1129 ylim = [_y0, _y1] 1130 1131 H, xedges, yedges = np.histogram2d( 1132 xvalues, yvalues, weights=weights, bins=bins, range=(xlim, ylim) 1133 ) 1134 1135 xlim = np.min(xedges), np.max(xedges) 1136 ylim = np.min(yedges), np.max(yedges) 1137 dx, dy = xlim[1] - xlim[0], ylim[1] - ylim[0] 1138 1139 fig_kwargs["title"] = title 1140 fig_kwargs["xtitle"] = xtitle 1141 fig_kwargs["ytitle"] = ytitle 1142 fig_kwargs["ac"] = ac 1143 1144 self.entries = len(xvalues) 1145 self.frequencies = H 1146 self.edges = (xedges, yedges) 1147 self.mean = (xvalues.mean(), yvalues.mean()) 1148 self.std = (xvalues.std(), yvalues.std()) 1149 self.bins = bins # internally used by "like" 1150 1151 ############################### stats legend as htitle 1152 addstats = False 1153 if not title: 1154 if "axes" not in fig_kwargs: 1155 addstats = True 1156 axes_opts = {} 1157 fig_kwargs["axes"] = axes_opts 1158 elif fig_kwargs["axes"] is False: 1159 pass 1160 else: 1161 axes_opts = fig_kwargs["axes"] 1162 if "htitle" not in fig_kwargs["axes"]: 1163 addstats = True 1164 1165 if addstats: 1166 htitle = f"Entries:~~{int(self.entries)} " 1167 htitle += f"Mean:~~{utils.precision(self.mean, 3)} " 1168 htitle += f"STD:~~{utils.precision(self.std, 3)} " 1169 axes_opts["htitle"] = htitle 1170 axes_opts["htitle_justify"] = "bottom-left" 1171 axes_opts["htitle_size"] = 0.0175 1172 1173 ############################################### Figure init 1174 super().__init__(xlim, ylim, aspect, padding, **fig_kwargs) 1175 1176 if self.yscale: 1177 ##################### the grid 1178 acts = [] 1179 g = shapes.Grid( 1180 pos=[(xlim[0] + xlim[1]) / 2, (ylim[0] + ylim[1]) / 2, 0], s=(dx, dy), res=bins[:2] 1181 ) 1182 g.alpha(alpha).lw(0).wireframe(False).flat().lighting("off") 1183 g.cmap(cmap, np.ravel(H.T), on="cells", vmin=zlim[0], vmax=zlim[1]) 1184 if gap: 1185 g.shrink(abs(1 - gap)) 1186 1187 if scalarbar: 1188 sc = g.add_scalarbar3d(ztitle, c=ac).scalarbar 1189 1190 # print(" g.GetBounds()[0]", g.bounds()[:2]) 1191 # print("sc.GetBounds()[0]",sc.GetBounds()[:2]) 1192 delta = sc.GetBounds()[0] - g.bounds()[1] 1193 1194 sc_size = sc.GetBounds()[1] - sc.GetBounds()[0] 1195 1196 sc.SetOrigin(sc.GetBounds()[0], 0, 0) 1197 sc.scale([self.yscale, 1, 1]) ## prescale trick 1198 sc.shift(-delta + 0.25*sc_size*self.yscale) 1199 1200 acts.append(sc) 1201 acts.append(g) 1202 1203 self.insert(*acts, as3d=False) 1204 self.name = "Histogram2D" 1205 1206 1207######################################################################################### 1208class PlotBars(Figure): 1209 """Creates a `PlotBars(Figure)` object.""" 1210 1211 def __init__( 1212 self, 1213 data, 1214 errors=False, 1215 logscale=False, 1216 fill=True, 1217 gap=0.02, 1218 radius=0.05, 1219 c="olivedrab", 1220 alpha=1, 1221 texture="", 1222 outline=False, 1223 lw=2, 1224 lc="k", 1225 # Figure and axes options: 1226 like=None, 1227 xlim=(None, None), 1228 ylim=(0, None), 1229 aspect=4 / 3, 1230 padding=(0.025, 0.025, 0, 0.05), 1231 # 1232 title="", 1233 xtitle=" ", 1234 ytitle=" ", 1235 ac="k", 1236 grid=False, 1237 ztolerance=None, 1238 **fig_kwargs, 1239 ): 1240 """ 1241 Input must be in format `[counts, labels, colors, edges]`. 1242 Either or both `edges` and `colors` are optional and can be omitted. 1243 1244 Use keyword `like=...` if you want to use the same format of a previously 1245 created Figure (useful when superimposing Figures) to make sure 1246 they are compatible and comparable. If they are not compatible 1247 you will receive an error message. 1248 1249 Arguments: 1250 errors : (bool) 1251 show error bars 1252 logscale : (bool) 1253 use logscale on y-axis 1254 fill : (bool) 1255 fill bars with solid color `c` 1256 gap : (float) 1257 leave a small space btw bars 1258 radius : (float) 1259 border radius of the top of the histogram bar. Default value is 0.1. 1260 texture : (str) 1261 url or path to an image to be used as texture for the bin 1262 outline : (bool) 1263 show outline of the bins 1264 xtitle : (str) 1265 title for the x-axis, can also be set using `axes=dict(xtitle="my x axis")` 1266 ytitle : (str) 1267 title for the y-axis, can also be set using `axes=dict(ytitle="my y axis")` 1268 ac : (str) 1269 axes color 1270 padding : (float, list) 1271 keep a padding space from the axes (as a fraction of the axis size). 1272 This can be a list of four numbers. 1273 aspect : (float) 1274 the desired aspect ratio of the figure. Default is 4/3. 1275 grid : (bool) 1276 show the background grid for the axes, can also be set using `axes=dict(xygrid=True)` 1277 1278 Examples: 1279 - [plot_bars.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/plot_bars.py) 1280 1281  1282 """ 1283 ndata = len(data) 1284 if ndata == 4: 1285 counts, xlabs, cols, edges = data 1286 elif ndata == 3: 1287 counts, xlabs, cols = data 1288 edges = np.array(range(len(counts) + 1)) + 0.5 1289 elif ndata == 2: 1290 counts, xlabs = data 1291 edges = np.array(range(len(counts) + 1)) + 0.5 1292 cols = [c] * len(counts) 1293 else: 1294 m = "barplot error: data must be given as [counts, labels, colors, edges] not\n" 1295 vedo.logger.error(m + f" {data}\n bin edges and colors are optional.") 1296 raise RuntimeError() 1297 1298 # sanity checks 1299 assert len(counts) == len(xlabs) 1300 assert len(counts) == len(cols) 1301 assert len(counts) == len(edges) - 1 1302 1303 counts = np.asarray(counts) 1304 edges = np.asarray(edges) 1305 1306 if logscale: 1307 counts = np.log10(counts + 1) 1308 if ytitle == " ": 1309 ytitle = "log_10 (counts+1)" 1310 1311 if like is None and vedo.last_figure is not None: 1312 if xlim == (None, None) and ylim == (0, None): 1313 like = vedo.last_figure 1314 1315 if like is not None: 1316 xlim = like.xlim 1317 ylim = like.ylim 1318 aspect = like.aspect 1319 padding = like.padding 1320 1321 if utils.is_sequence(xlim): 1322 # deal with user passing eg [x0, None] 1323 _x0, _x1 = xlim 1324 if _x0 is None: 1325 _x0 = np.min(edges) 1326 if _x1 is None: 1327 _x1 = np.max(edges) 1328 xlim = [_x0, _x1] 1329 1330 x0, x1 = np.min(edges), np.max(edges) 1331 y0, y1 = ylim[0], np.max(counts) 1332 1333 if like is None: 1334 ylim = list(ylim) 1335 if xlim is None: 1336 xlim = [x0, x1] 1337 if ylim[1] is None: 1338 ylim[1] = y1 1339 if ylim[0] != 0: 1340 ylim[0] = y0 1341 1342 fig_kwargs["title"] = title 1343 fig_kwargs["xtitle"] = xtitle 1344 fig_kwargs["ytitle"] = ytitle 1345 fig_kwargs["ac"] = ac 1346 fig_kwargs["ztolerance"] = ztolerance 1347 fig_kwargs["grid"] = grid 1348 1349 centers = (edges[0:-1] + edges[1:]) / 2 1350 binsizes = (centers - edges[0:-1]) * 2 1351 1352 if "axes" not in fig_kwargs: 1353 fig_kwargs["axes"] = {} 1354 1355 _xlabs = [] 1356 for center, xlb in zip(centers, xlabs): 1357 _xlabs.append([center, str(xlb)]) 1358 fig_kwargs["axes"]["x_values_and_labels"] = _xlabs 1359 1360 ############################################### Figure 1361 self.statslegend = "" 1362 self.edges = edges 1363 self.centers = centers 1364 self.bins = edges # internal used by "like" 1365 super().__init__(xlim, ylim, aspect, padding, **fig_kwargs) 1366 if not self.yscale: 1367 return 1368 1369 rs = [] 1370 maxheigth = 0 1371 if fill: ##################### 1372 if outline: 1373 gap = 0 1374 1375 for i in range(len(centers)): 1376 binsize = binsizes[i] 1377 p0 = (edges[i] + gap * binsize, 0, 0) 1378 p1 = (edges[i + 1] - gap * binsize, counts[i], 0) 1379 1380 if radius: 1381 rds = np.array([0, 0, radius, radius]) 1382 p1_yscaled = [p1[0], p1[1] * self.yscale, 0] 1383 r = shapes.Rectangle(p0, p1_yscaled, radius=rds * binsize, res=6) 1384 r.scale([1, 1 / self.yscale, 1]) 1385 r.radius = None # so it doesnt get recreated and rescaled by insert() 1386 else: 1387 r = shapes.Rectangle(p0, p1) 1388 1389 if texture: 1390 r.texture(texture) 1391 c = "w" 1392 1393 r.actor.PickableOff() 1394 maxheigth = max(maxheigth, p1[1]) 1395 if c in colors.cmaps_names: 1396 col = colors.color_map((p0[0] + p1[0]) / 2, c, edges[0], edges[-1]) 1397 else: 1398 col = cols[i] 1399 r.color(col).alpha(alpha).lighting("off") 1400 r.name = f"bar_{i}" 1401 r.z(self.ztolerance) 1402 rs.append(r) 1403 1404 elif outline: ##################### 1405 lns = [[edges[0], 0, 0]] 1406 for i in range(len(centers)): 1407 lns.append([edges[i], counts[i], 0]) 1408 lns.append([edges[i + 1], counts[i], 0]) 1409 maxheigth = max(maxheigth, counts[i]) 1410 lns.append([edges[-1], 0, 0]) 1411 outl = shapes.Line(lns, c=lc, alpha=alpha, lw=lw).z(self.ztolerance) 1412 outl.name = f"bar_outline_{i}" 1413 rs.append(outl) 1414 1415 if errors: ##################### 1416 for x, f in centers: 1417 err = np.sqrt(f) 1418 el = shapes.Line([x, f - err / 2, 0], [x, f + err / 2, 0], c=lc, alpha=alpha, lw=lw) 1419 el.z(self.ztolerance * 2) 1420 rs.append(el) 1421 1422 self.insert(*rs, as3d=False) 1423 self.name = "PlotBars" 1424 1425 1426######################################################################################### 1427class PlotXY(Figure): 1428 """Creates a `PlotXY(Figure)` object.""" 1429 1430 def __init__( 1431 self, 1432 # 1433 data, 1434 xerrors=None, 1435 yerrors=None, 1436 # 1437 lw=2, 1438 lc=None, 1439 la=1, 1440 dashed=False, 1441 splined=False, 1442 # 1443 elw=2, # error line width 1444 ec=None, # error line or band color 1445 error_band=False, # errors in x are ignored 1446 # 1447 marker="", 1448 ms=None, 1449 mc=None, 1450 ma=None, 1451 # Figure and axes options: 1452 like=None, 1453 xlim=None, 1454 ylim=(None, None), 1455 aspect=4 / 3, 1456 padding=0.05, 1457 # 1458 title="", 1459 xtitle=" ", 1460 ytitle=" ", 1461 ac="k", 1462 grid=True, 1463 ztolerance=None, 1464 label="", 1465 **fig_kwargs, 1466 ): 1467 """ 1468 Arguments: 1469 xerrors : (bool) 1470 show error bars associated to each point in x 1471 yerrors : (bool) 1472 show error bars associated to each point in y 1473 lw : (int) 1474 width of the line connecting points in pixel units. 1475 Set it to 0 to remove the line. 1476 lc : (str) 1477 line color 1478 la : (float) 1479 line "alpha", opacity of the line 1480 dashed : (bool) 1481 draw a dashed line instead of a continuous line 1482 splined : (bool) 1483 spline the line joining the point as a countinous curve 1484 elw : (int) 1485 width of error bar lines in units of pixels 1486 ec : (color) 1487 color of error bar, by default the same as marker color 1488 error_band : (bool) 1489 represent errors on y as a filled error band. 1490 Use `ec` keyword to modify its color. 1491 marker : (str, int) 1492 use a marker for the data points 1493 ms : (float) 1494 marker size 1495 mc : (color) 1496 color of the marker 1497 ma : (float) 1498 opacity of the marker 1499 xlim : (list) 1500 set limits to the range for the x variable 1501 ylim : (list) 1502 set limits to the range for the y variable 1503 aspect : (float, str) 1504 Desired aspect ratio. 1505 Use `aspect="equal"` to force the same units in x and y. 1506 Scaling factor is saved in Figure.yscale. 1507 padding : (float, list) 1508 keep a padding space from the axes (as a fraction of the axis size). 1509 This can be a list of four numbers. 1510 title : (str) 1511 title to appear on the top of the frame, like a header. 1512 xtitle : (str) 1513 title for the x-axis, can also be set using `axes=dict(xtitle="my x axis")` 1514 ytitle : (str) 1515 title for the y-axis, can also be set using `axes=dict(ytitle="my y axis")` 1516 ac : (str) 1517 axes color 1518 grid : (bool) 1519 show the background grid for the axes, can also be set using `axes=dict(xygrid=True)` 1520 ztolerance : (float) 1521 a tolerance factor to superimpose objects (along the z-axis). 1522 1523 Example: 1524 ```python 1525 import numpy as np 1526 from vedo.pyplot import plot 1527 x = np.arange(0, np.pi, 0.1) 1528 fig = plot(x, np.sin(2*x), 'r0-', aspect='equal') 1529 fig+= plot(x, np.cos(2*x), 'blue4 o-', like=fig) 1530 fig.show().close() 1531 ``` 1532  1533 1534 Examples: 1535 - [plot_errbars.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/plot_errbars.py) 1536 - [plot_errband.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/plot_errband.py) 1537 - [plot_pip.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/plot_pip.py) 1538 1539  1540 1541 - [scatter1.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/scatter1.py) 1542 - [scatter2.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/scatter2.py) 1543 1544  1545 """ 1546 line = False 1547 if lw > 0: 1548 line = True 1549 if marker == "" and not line and not splined: 1550 marker = "o" 1551 1552 if like is None and vedo.last_figure is not None: 1553 if xlim is None and ylim == (None, None): 1554 like = vedo.last_figure 1555 1556 if like is not None: 1557 xlim = like.xlim 1558 ylim = like.ylim 1559 aspect = like.aspect 1560 padding = like.padding 1561 1562 if utils.is_sequence(xlim): 1563 # deal with user passing eg [x0, None] 1564 _x0, _x1 = xlim 1565 if _x0 is None: 1566 _x0 = data.min() 1567 if _x1 is None: 1568 _x1 = data.max() 1569 xlim = [_x0, _x1] 1570 1571 # purge NaN from data 1572 data = data[~np.isnan(data).any(axis=1), :] 1573 1574 fig_kwargs["title"] = title 1575 fig_kwargs["xtitle"] = xtitle 1576 fig_kwargs["ytitle"] = ytitle 1577 fig_kwargs["ac"] = ac 1578 fig_kwargs["ztolerance"] = ztolerance 1579 fig_kwargs["grid"] = grid 1580 1581 x0, y0 = np.min(data, axis=0) 1582 x1, y1 = np.max(data, axis=0) 1583 if xerrors is not None and not error_band: 1584 x0 = min(data[:, 0] - xerrors) 1585 x1 = max(data[:, 0] + xerrors) 1586 if yerrors is not None: 1587 y0 = min(data[:, 1] - yerrors) 1588 y1 = max(data[:, 1] + yerrors) 1589 1590 if like is None: 1591 if xlim is None: 1592 xlim = (None, None) 1593 xlim = list(xlim) 1594 if xlim[0] is None: 1595 xlim[0] = x0 1596 if xlim[1] is None: 1597 xlim[1] = x1 1598 ylim = list(ylim) 1599 if ylim[0] is None: 1600 ylim[0] = y0 1601 if ylim[1] is None: 1602 ylim[1] = y1 1603 1604 self.entries = len(data) 1605 self.mean = data.mean() 1606 self.std = data.std() 1607 1608 self.ztolerance = 0 1609 1610 ######### the PlotXY marker 1611 # fall back solutions logic for colors 1612 if "c" in fig_kwargs: 1613 if mc is None: 1614 mc = fig_kwargs["c"] 1615 if lc is None: 1616 lc = fig_kwargs["c"] 1617 if ec is None: 1618 ec = fig_kwargs["c"] 1619 if lc is None: 1620 lc = "k" 1621 if mc is None: 1622 mc = lc 1623 if ma is None: 1624 ma = la 1625 if ec is None: 1626 if mc is None: 1627 ec = lc 1628 else: 1629 ec = mc 1630 1631 if label: 1632 nlab = LabelData() 1633 nlab.text = label 1634 nlab.tcolor = ac 1635 nlab.marker = marker 1636 if line and marker == "": 1637 nlab.marker = "-" 1638 nlab.mcolor = mc 1639 fig_kwargs["label"] = nlab 1640 1641 ############################################### Figure init 1642 super().__init__(xlim, ylim, aspect, padding, **fig_kwargs) 1643 1644 if not self.yscale: 1645 return 1646 1647 acts = [] 1648 1649 ######### the PlotXY Line or Spline 1650 if dashed: 1651 l = shapes.DashedLine(data, c=lc, alpha=la, lw=lw) 1652 acts.append(l) 1653 elif splined: 1654 l = shapes.KSpline(data).lw(lw).c(lc).alpha(la) 1655 acts.append(l) 1656 elif line: 1657 l = shapes.Line(data, c=lc, alpha=la).lw(lw) 1658 acts.append(l) 1659 1660 if marker: 1661 1662 pts = np.c_[data, np.zeros(len(data))] 1663 1664 if utils.is_sequence(ms): 1665 ### variable point size 1666 mk = shapes.Marker(marker, s=1) 1667 mk.scale([1, 1 / self.yscale, 1]) 1668 msv = np.zeros_like(pts) 1669 msv[:, 0] = ms 1670 marked = shapes.Glyph( 1671 pts, mk, c=mc, orientation_array=msv, scale_by_vector_size=True 1672 ) 1673 else: 1674 ### fixed point size 1675 if ms is None: 1676 ms = (xlim[1] - xlim[0]) / 100.0 1677 1678 if utils.is_sequence(mc): 1679 fig_kwargs["marker_color"] = None # for labels 1680 mk = shapes.Marker(marker, s=ms) 1681 mk.scale([1, 1 / self.yscale, 1]) 1682 msv = np.zeros_like(pts) 1683 msv[:, 0] = 1 1684 marked = shapes.Glyph( 1685 pts, mk, c=mc, orientation_array=msv, scale_by_vector_size=True 1686 ) 1687 else: 1688 mk = shapes.Marker(marker, s=ms) 1689 mk.scale([1, 1 / self.yscale, 1]) 1690 marked = shapes.Glyph(pts, mk, c=mc) 1691 1692 marked.name = "Marker" 1693 marked.alpha(ma) 1694 marked.z(3 * self.ztolerance) 1695 acts.append(marked) 1696 1697 ######### the PlotXY marker errors 1698 ztol = self.ztolerance 1699 1700 if error_band: 1701 yerrors = np.abs(yerrors) 1702 du = np.array(data) 1703 dd = np.array(data) 1704 du[:, 1] += yerrors 1705 dd[:, 1] -= yerrors 1706 if splined: 1707 res = len(data) * 20 1708 band1 = shapes.KSpline(du, res=res) 1709 band2 = shapes.KSpline(dd, res=res) 1710 band = shapes.Ribbon(band1, band2, res=(res, 2)) 1711 else: 1712 dd = list(reversed(dd.tolist())) 1713 band = shapes.Line(du.tolist() + dd, closed=True) 1714 band.triangulate().lw(0) 1715 if ec is None: 1716 band.c(lc) 1717 else: 1718 band.c(ec) 1719 band.lighting("off").alpha(la).z(ztol / 20) 1720 acts.append(band) 1721 1722 else: 1723 1724 ## xerrors 1725 if xerrors is not None: 1726 if len(xerrors) == len(data): 1727 errs = [] 1728 for i, val in enumerate(data): 1729 xval, yval = val 1730 xerr = xerrors[i] / 2 1731 el = shapes.Line((xval - xerr, yval, ztol), (xval + xerr, yval, ztol)) 1732 el.lw(elw) 1733 errs.append(el) 1734 mxerrs = merge(errs).c(ec).lw(lw).alpha(ma).z(2 * ztol) 1735 acts.append(mxerrs) 1736 else: 1737 vedo.logger.error("in PlotXY(xerrors=...): mismatch in array length") 1738 1739 ## yerrors 1740 if yerrors is not None: 1741 if len(yerrors) == len(data): 1742 errs = [] 1743 for i, val in enumerate(data): 1744 xval, yval = val 1745 yerr = yerrors[i] 1746 el = shapes.Line((xval, yval - yerr, ztol), (xval, yval + yerr, ztol)) 1747 el.lw(elw) 1748 errs.append(el) 1749 myerrs = merge(errs).c(ec).lw(lw).alpha(ma).z(2 * ztol) 1750 acts.append(myerrs) 1751 else: 1752 vedo.logger.error("in PlotXY(yerrors=...): mismatch in array length") 1753 1754 self.insert(*acts, as3d=False) 1755 self.name = "PlotXY" 1756 1757 1758def plot(*args, **kwargs): 1759 """ 1760 Draw a 2D line plot, or scatter plot, of variable x vs variable y. 1761 Input format can be either `[allx], [allx, ally] or [(x1,y1), (x2,y2), ...]` 1762 1763 Use `like=...` if you want to use the same format of a previously 1764 created Figure (useful when superimposing Figures) to make sure 1765 they are compatible and comparable. If they are not compatible 1766 you will receive an error message. 1767 1768 Arguments: 1769 xerrors : (bool) 1770 show error bars associated to each point in x 1771 yerrors : (bool) 1772 show error bars associated to each point in y 1773 lw : (int) 1774 width of the line connecting points in pixel units. 1775 Set it to 0 to remove the line. 1776 lc : (str) 1777 line color 1778 la : (float) 1779 line "alpha", opacity of the line 1780 dashed : (bool) 1781 draw a dashed line instead of a continuous line 1782 splined : (bool) 1783 spline the line joining the point as a countinous curve 1784 elw : (int) 1785 width of error bar lines in units of pixels 1786 ec : (color) 1787 color of error bar, by default the same as marker color 1788 error_band : (bool) 1789 represent errors on y as a filled error band. 1790 Use `ec` keyword to modify its color. 1791 marker : (str, int) 1792 use a marker for the data points 1793 ms : (float) 1794 marker size 1795 mc : (color) 1796 color of the marker 1797 ma : (float) 1798 opacity of the marker 1799 xlim : (list) 1800 set limits to the range for the x variable 1801 ylim : (list) 1802 set limits to the range for the y variable 1803 aspect : (float) 1804 Desired aspect ratio. 1805 If None, it is automatically calculated to get a reasonable aspect ratio. 1806 Scaling factor is saved in Figure.yscale 1807 padding : (float, list) 1808 keep a padding space from the axes (as a fraction of the axis size). 1809 This can be a list of four numbers. 1810 title : (str) 1811 title to appear on the top of the frame, like a header. 1812 xtitle : (str) 1813 title for the x-axis, can also be set using `axes=dict(xtitle="my x axis")` 1814 ytitle : (str) 1815 title for the y-axis, can also be set using `axes=dict(ytitle="my y axis")` 1816 ac : (str) 1817 axes color 1818 grid : (bool) 1819 show the background grid for the axes, can also be set using `axes=dict(xygrid=True)` 1820 ztolerance : (float) 1821 a tolerance factor to superimpose objects (along the z-axis). 1822 1823 Example: 1824 ```python 1825 import numpy as np 1826 from vedo.pyplot import plot 1827 from vedo import settings 1828 settings.remember_last_figure_format = True ############# 1829 x = np.linspace(0, 6.28, num=50) 1830 fig = plot(np.sin(x), 'r-') 1831 fig+= plot(np.cos(x), 'bo-') # no need to specify like=... 1832 fig.show().close() 1833 ``` 1834 <img src="https://user-images.githubusercontent.com/32848391/74363882-c3638300-4dcb-11ea-8a78-eb492ad9711f.png" width="600"> 1835 1836 Examples: 1837 - [plot_errbars.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/plot_errbars.py) 1838 - [plot_errband.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/plot_errband.py) 1839 - [plot_pip.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/plot_pip.py) 1840 1841  1842 1843 - [scatter1.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/scatter1.py) 1844 - [scatter2.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/scatter2.py) 1845 1846 1847 1848 ------------------------------------------------------------------------- 1849 .. note:: mode="bar" 1850 1851 Creates a `PlotBars(Figure)` object. 1852 1853 Input must be in format `[counts, labels, colors, edges]`. 1854 Either or both `edges` and `colors` are optional and can be omitted. 1855 1856 Arguments: 1857 errors : (bool) 1858 show error bars 1859 logscale : (bool) 1860 use logscale on y-axis 1861 fill : (bool) 1862 fill bars with solid color `c` 1863 gap : (float) 1864 leave a small space btw bars 1865 radius : (float) 1866 border radius of the top of the histogram bar. Default value is 0.1. 1867 texture : (str) 1868 url or path to an image to be used as texture for the bin 1869 outline : (bool) 1870 show outline of the bins 1871 xtitle : (str) 1872 title for the x-axis, can also be set using `axes=dict(xtitle="my x axis")` 1873 ytitle : (str) 1874 title for the y-axis, can also be set using `axes=dict(ytitle="my y axis")` 1875 ac : (str) 1876 axes color 1877 padding : (float, list) 1878 keep a padding space from the axes (as a fraction of the axis size). 1879 This can be a list of four numbers. 1880 aspect : (float) 1881 the desired aspect ratio of the figure. Default is 4/3. 1882 grid : (bool) 1883 show the background grid for the axes, can also be set using `axes=dict(xygrid=True)` 1884 1885 Examples: 1886 - [histo_1d_a.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/histo_1d_a.py) 1887 - [histo_1d_b.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/histo_1d_b.py) 1888 - [histo_1d_c.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/histo_1d_c.py) 1889 - [histo_1d_d.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/histo_1d_d.py) 1890 1891  1892 1893 1894 ---------------------------------------------------------------------- 1895 .. note:: 2D functions 1896 1897 If input is an external function or a formula, draw the surface 1898 representing the function `f(x,y)`. 1899 1900 Arguments: 1901 x : (float) 1902 x range of values 1903 y : (float) 1904 y range of values 1905 zlimits : (float) 1906 limit the z range of the independent variable 1907 zlevels : (int) 1908 will draw the specified number of z-levels contour lines 1909 show_nan : (bool) 1910 show where the function does not exist as red points 1911 bins : (list) 1912 number of bins in x and y 1913 1914 Examples: 1915 - [plot_fxy1.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/plot_fxy1.py) 1916 1917  1918 1919 - [plot_fxy2.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/plot_fxy2.py) 1920 1921 1922 -------------------------------------------------------------------- 1923 .. note:: mode="complex" 1924 1925 If `mode='complex'` draw the real value of the function and color map the imaginary part. 1926 1927 Arguments: 1928 cmap : (str) 1929 diverging color map (white means `imag(z)=0`) 1930 lw : (float) 1931 line with of the binning 1932 bins : (list) 1933 binning in x and y 1934 1935 Examples: 1936 - [plot_fxy.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/plot_fxy.py) 1937 1938  1939 1940 1941 -------------------------------------------------------------------- 1942 .. note:: mode="polar" 1943 1944 If `mode='polar'` input arrays are interpreted as a list of polar angles and radii. 1945 Build a polar (radar) plot by joining the set of points in polar coordinates. 1946 1947 Arguments: 1948 title : (str) 1949 plot title 1950 tsize : (float) 1951 title size 1952 bins : (int) 1953 number of bins in phi 1954 r1 : (float) 1955 inner radius 1956 r2 : (float) 1957 outer radius 1958 lsize : (float) 1959 label size 1960 c : (color) 1961 color of the line 1962 ac : (color) 1963 color of the frame and labels 1964 alpha : (float) 1965 opacity of the frame 1966 ps : (int) 1967 point size in pixels, if ps=0 no point is drawn 1968 lw : (int) 1969 line width in pixels, if lw=0 no line is drawn 1970 deg : (bool) 1971 input array is in degrees 1972 vmax : (float) 1973 normalize radius to this maximum value 1974 fill : (bool) 1975 fill convex area with solid color 1976 splined : (bool) 1977 interpolate the set of input points 1978 show_disc : (bool) 1979 draw the outer ring axis 1980 nrays : (int) 1981 draw this number of axis rays (continuous and dashed) 1982 show_lines : (bool) 1983 draw lines to the origin 1984 show_angles : (bool) 1985 draw angle values 1986 1987 Examples: 1988 - [histo_polar.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/histo_polar.py) 1989 1990  1991 1992 1993 -------------------------------------------------------------------- 1994 .. note:: mode="spheric" 1995 1996 If `mode='spheric'` input must be an external function rho(theta, phi). 1997 A surface is created in spherical coordinates. 1998 1999 Return an `Figure(Assembly)` of 2 objects: the unit 2000 sphere (in wireframe representation) and the surface `rho(theta, phi)`. 2001 2002 Arguments: 2003 rfunc : function 2004 handle to a user defined function `rho(theta, phi)`. 2005 normalize : (bool) 2006 scale surface to fit inside the unit sphere 2007 res : (int) 2008 grid resolution of the unit sphere 2009 scalarbar : (bool) 2010 add a 3D scalarbar to the plot for radius 2011 c : (color) 2012 color of the unit sphere 2013 alpha : (float) 2014 opacity of the unit sphere 2015 cmap : (str) 2016 color map for the surface 2017 2018 Examples: 2019 - [plot_spheric.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/plot_spheric.py) 2020 2021  2022 """ 2023 mode = kwargs.pop("mode", "") 2024 if "spher" in mode: 2025 return _plot_spheric(args[0], **kwargs) 2026 2027 if "bar" in mode: 2028 return PlotBars(args[0], **kwargs) 2029 2030 if isinstance(args[0], str) or "function" in str(type(args[0])): 2031 if "complex" in mode: 2032 return _plot_fz(args[0], **kwargs) 2033 return _plot_fxy(args[0], **kwargs) 2034 2035 # grab the matplotlib-like options 2036 optidx = None 2037 for i, a in enumerate(args): 2038 if i > 0 and isinstance(a, str): 2039 optidx = i 2040 break 2041 if optidx: 2042 opts = args[optidx].replace(" ", "") 2043 if "--" in opts: 2044 opts = opts.replace("--", "") 2045 kwargs["dashed"] = True 2046 elif "-" in opts: 2047 opts = opts.replace("-", "") 2048 else: 2049 kwargs["lw"] = 0 2050 2051 symbs = [".", "o", "O", "0", "p", "*", "h", "D", "d", "v", "^", ">", "<", "s", "x", "a"] 2052 2053 allcols = list(colors.colors.keys()) + list(colors.color_nicks.keys()) 2054 for cc in allcols: 2055 if cc == "o": 2056 continue 2057 if cc in opts: 2058 opts = opts.replace(cc, "") 2059 kwargs["lc"] = cc 2060 kwargs["mc"] = cc 2061 break 2062 2063 for ss in symbs: 2064 if ss in opts: 2065 opts = opts.replace(ss, "", 1) 2066 kwargs["marker"] = ss 2067 break 2068 2069 opts.replace(" ", "") 2070 if opts: 2071 vedo.logger.error(f"in plot(), could not understand option(s): {opts}") 2072 2073 if optidx == 1 or optidx is None: 2074 if utils.is_sequence(args[0][0]) and len(args[0][0]) > 1: 2075 # print('------------- case 1', 'plot([(x,y),..])') 2076 data = np.asarray(args[0]) # (x,y) 2077 x = np.asarray(data[:, 0]) 2078 y = np.asarray(data[:, 1]) 2079 2080 elif len(args) == 1 or optidx == 1: 2081 # print('------------- case 2', 'plot(x)') 2082 if "pandas" in str(type(args[0])): 2083 if "ytitle" not in kwargs: 2084 kwargs.update({"ytitle": args[0].name.replace("_", "_ ")}) 2085 x = np.linspace(0, len(args[0]), num=len(args[0])) 2086 y = np.asarray(args[0]).ravel() 2087 2088 elif utils.is_sequence(args[1]): 2089 # print('------------- case 3', 'plot(allx,ally)',str(type(args[0]))) 2090 if "pandas" in str(type(args[0])): 2091 if "xtitle" not in kwargs: 2092 kwargs.update({"xtitle": args[0].name.replace("_", "_ ")}) 2093 if "pandas" in str(type(args[1])): 2094 if "ytitle" not in kwargs: 2095 kwargs.update({"ytitle": args[1].name.replace("_", "_ ")}) 2096 x = np.asarray(args[0]).ravel() 2097 y = np.asarray(args[1]).ravel() 2098 2099 elif utils.is_sequence(args[0]) and utils.is_sequence(args[0][0]): 2100 # print('------------- case 4', 'plot([allx,ally])') 2101 x = np.asarray(args[0][0]).ravel() 2102 y = np.asarray(args[0][1]).ravel() 2103 2104 elif optidx == 2: 2105 # print('------------- case 5', 'plot(x,y)') 2106 x = np.asarray(args[0]).ravel() 2107 y = np.asarray(args[1]).ravel() 2108 2109 else: 2110 vedo.logger.error(f"plot(): Could not understand input arguments {args}") 2111 return None 2112 2113 if "polar" in mode: 2114 return _plot_polar(np.c_[x, y], **kwargs) 2115 2116 return PlotXY(np.c_[x, y], **kwargs) 2117 2118 2119def histogram(*args, **kwargs): 2120 """ 2121 Histogramming for 1D and 2D data arrays. 2122 2123 This is meant as a convenience function that creates the appropriate object 2124 based on the shape of the provided input data. 2125 2126 Use keyword `like=...` if you want to use the same format of a previously 2127 created Figure (useful when superimposing Figures) to make sure 2128 they are compatible and comparable. If they are not compatible 2129 you will receive an error message. 2130 2131 ------------------------------------------------------------------------- 2132 .. note:: default mode, for 1D arrays 2133 2134 Creates a `Histogram1D(Figure)` object. 2135 2136 Arguments: 2137 weights : (list) 2138 An array of weights, of the same shape as `data`. Each value in `data` 2139 only contributes its associated weight towards the bin count (instead of 1). 2140 bins : (int) 2141 number of bins 2142 vrange : (list) 2143 restrict the range of the histogram 2144 density : (bool) 2145 normalize the area to 1 by dividing by the nr of entries and bin size 2146 logscale : (bool) 2147 use logscale on y-axis 2148 fill : (bool) 2149 fill bars with solid color `c` 2150 gap : (float) 2151 leave a small space btw bars 2152 radius : (float) 2153 border radius of the top of the histogram bar. Default value is 0.1. 2154 texture : (str) 2155 url or path to an image to be used as texture for the bin 2156 outline : (bool) 2157 show outline of the bins 2158 errors : (bool) 2159 show error bars 2160 xtitle : (str) 2161 title for the x-axis, can also be set using `axes=dict(xtitle="my x axis")` 2162 ytitle : (str) 2163 title for the y-axis, can also be set using `axes=dict(ytitle="my y axis")` 2164 padding : (float, list) 2165 keep a padding space from the axes (as a fraction of the axis size). 2166 This can be a list of four numbers. 2167 aspect : (float) 2168 the desired aspect ratio of the histogram. Default is 4/3. 2169 grid : (bool) 2170 show the background grid for the axes, can also be set using `axes=dict(xygrid=True)` 2171 ztolerance : (float) 2172 a tolerance factor to superimpose objects (along the z-axis). 2173 2174 Examples: 2175 - [histo_1d_a.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/histo_1d_a.py) 2176 - [histo_1d_b.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/histo_1d_b.py) 2177 - [histo_1d_c.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/histo_1d_c.py) 2178 - [histo_1d_d.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/histo_1d_d.py) 2179 2180  2181 2182 2183 ------------------------------------------------------------------------- 2184 .. note:: default mode, for 2D arrays 2185 2186 Input data formats `[(x1,x2,..), (y1,y2,..)] or [(x1,y1), (x2,y2),..]` 2187 are both valid. 2188 2189 Arguments: 2190 bins : (list) 2191 binning as (nx, ny) 2192 weights : (list) 2193 array of weights to assign to each entry 2194 cmap : (str, lookuptable) 2195 color map name or look up table 2196 alpha : (float) 2197 opacity of the histogram 2198 gap : (float) 2199 separation between adjacent bins as a fraction for their size. 2200 Set gap=-1 to generate a quad surface. 2201 scalarbar : (bool) 2202 add a scalarbar to right of the histogram 2203 like : (Figure) 2204 grab and use the same format of the given Figure (for superimposing) 2205 xlim : (list) 2206 [x0, x1] range of interest. If left to None will automatically 2207 choose the minimum or the maximum of the data range. 2208 Data outside the range are completely ignored. 2209 ylim : (list) 2210 [y0, y1] range of interest. If left to None will automatically 2211 choose the minimum or the maximum of the data range. 2212 Data outside the range are completely ignored. 2213 aspect : (float) 2214 the desired aspect ratio of the figure. 2215 title : (str) 2216 title of the plot to appear on top. 2217 If left blank some statistics will be shown. 2218 xtitle : (str) 2219 x axis title 2220 ytitle : (str) 2221 y axis title 2222 ztitle : (str) 2223 title for the scalar bar 2224 ac : (str) 2225 axes color, additional keyword for Axes can also be added 2226 using e.g. `axes=dict(xygrid=True)` 2227 2228 Examples: 2229 - [histo_2d_a.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/histo_2d_a.py) 2230 - [histo_2d_b.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/histo_2d_b.py) 2231 2232  2233 2234 2235 ------------------------------------------------------------------------- 2236 .. note:: mode="3d" 2237 2238 If `mode='3d'`, build a 2D histogram as 3D bars from a list of x and y values. 2239 2240 Arguments: 2241 xtitle : (str) 2242 x axis title 2243 bins : (int) 2244 nr of bins for the smaller range in x or y 2245 vrange : (list) 2246 range in x and y in format `[(xmin,xmax), (ymin,ymax)]` 2247 norm : (float) 2248 sets a scaling factor for the z axis (frequency axis) 2249 fill : (bool) 2250 draw solid hexagons 2251 cmap : (str) 2252 color map name for elevation 2253 gap : (float) 2254 keep a internal empty gap between bins [0,1] 2255 zscale : (float) 2256 rescale the (already normalized) zaxis for visual convenience 2257 2258 Examples: 2259 - [histo_2d_b.py](https://github.com/marcomusy/vedo/tree/master/examples/examples/pyplot/histo_2d_b.py) 2260 2261 2262 ------------------------------------------------------------------------- 2263 .. note:: mode="hexbin" 2264 2265 If `mode='hexbin'`, build a hexagonal histogram from a list of x and y values. 2266 2267 Arguments: 2268 xtitle : (str) 2269 x axis title 2270 bins : (int) 2271 nr of bins for the smaller range in x or y 2272 vrange : (list) 2273 range in x and y in format `[(xmin,xmax), (ymin,ymax)]` 2274 norm : (float) 2275 sets a scaling factor for the z axis (frequency axis) 2276 fill : (bool) 2277 draw solid hexagons 2278 cmap : (str) 2279 color map name for elevation 2280 2281 Examples: 2282 - [histo_hexagonal.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/histo_hexagonal.py) 2283 2284  2285 2286 2287 ------------------------------------------------------------------------- 2288 .. note:: mode="polar" 2289 2290 If `mode='polar'` assume input is polar coordinate system (rho, theta): 2291 2292 Arguments: 2293 weights : (list) 2294 Array of weights, of the same shape as the input. 2295 Each value only contributes its associated weight towards the bin count (instead of 1). 2296 title : (str) 2297 histogram title 2298 tsize : (float) 2299 title size 2300 bins : (int) 2301 number of bins in phi 2302 r1 : (float) 2303 inner radius 2304 r2 : (float) 2305 outer radius 2306 phigap : (float) 2307 gap angle btw 2 radial bars, in degrees 2308 rgap : (float) 2309 gap factor along radius of numeric angle labels 2310 lpos : (float) 2311 label gap factor along radius 2312 lsize : (float) 2313 label size 2314 c : (color) 2315 color of the histogram bars, can be a list of length `bins` 2316 bc : (color) 2317 color of the frame and labels 2318 alpha : (float) 2319 opacity of the frame 2320 cmap : (str) 2321 color map name 2322 deg : (bool) 2323 input array is in degrees 2324 vmin : (float) 2325 minimum value of the radial axis 2326 vmax : (float) 2327 maximum value of the radial axis 2328 labels : (list) 2329 list of labels, must be of length `bins` 2330 show_disc : (bool) 2331 show the outer ring axis 2332 nrays : (int) 2333 draw this number of axis rays (continuous and dashed) 2334 show_lines : (bool) 2335 show lines to the origin 2336 show_angles : (bool) 2337 show angular values 2338 show_errors : (bool) 2339 show error bars 2340 2341 Examples: 2342 - [histo_polar.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/histo_polar.py) 2343 2344  2345 2346 2347 ------------------------------------------------------------------------- 2348 .. note:: mode="spheric" 2349 2350 If `mode='spheric'`, build a histogram from list of theta and phi values. 2351 2352 Arguments: 2353 rmax : (float) 2354 maximum radial elevation of bin 2355 res : (int) 2356 sphere resolution 2357 cmap : (str) 2358 color map name 2359 lw : (int) 2360 line width of the bin edges 2361 2362 Examples: 2363 - [histo_spheric.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/histo_spheric.py) 2364 2365  2366 """ 2367 mode = kwargs.pop("mode", "") 2368 if len(args) == 2: # x, y 2369 2370 if "spher" in mode: 2371 return _histogram_spheric(args[0], args[1], **kwargs) 2372 2373 if "hex" in mode: 2374 return _histogram_hex_bin(args[0], args[1], **kwargs) 2375 2376 if "3d" in mode.lower(): 2377 return _histogram_quad_bin(args[0], args[1], **kwargs) 2378 2379 return Histogram2D(args[0], args[1], **kwargs) 2380 2381 elif len(args) == 1: 2382 2383 if isinstance(args[0], vedo.Volume): 2384 data = args[0].pointdata[0] 2385 elif isinstance(args[0], vedo.Points): 2386 pd0 = args[0].pointdata[0] 2387 if pd0 is not None: 2388 data = pd0.ravel() 2389 else: 2390 data = args[0].celldata[0].ravel() 2391 else: 2392 try: 2393 if "pandas" in str(type(args[0])): 2394 if "xtitle" not in kwargs: 2395 kwargs.update({"xtitle": args[0].name.replace("_", "_ ")}) 2396 except: 2397 pass 2398 data = np.asarray(args[0]) 2399 2400 if "spher" in mode: 2401 return _histogram_spheric(args[0][:, 0], args[0][:, 1], **kwargs) 2402 2403 if data.ndim == 1: 2404 if "polar" in mode: 2405 return _histogram_polar(data, **kwargs) 2406 return Histogram1D(data, **kwargs) 2407 2408 if "hex" in mode: 2409 return _histogram_hex_bin(args[0][:, 0], args[0][:, 1], **kwargs) 2410 2411 if "3d" in mode.lower(): 2412 return _histogram_quad_bin(args[0][:, 0], args[0][:, 1], **kwargs) 2413 2414 return Histogram2D(args[0], **kwargs) 2415 2416 vedo.logger.error(f"in histogram(): could not understand input {args[0]}") 2417 return None 2418 2419 2420def fit( 2421 points, deg=1, niter=0, nstd=3, xerrors=None, yerrors=None, vrange=None, res=250, lw=3, c="red4" 2422) -> "vedo.shapes.Line": 2423 """ 2424 Polynomial fitting with parameter error and error bands calculation. 2425 Errors bars in both x and y are supported. 2426 2427 Returns a `vedo.shapes.Line` object. 2428 2429 Additional information about the fitting output can be accessed with: 2430 2431 `fitd = fit(pts)` 2432 2433 - `fitd.coefficients` will contain the coefficients of the polynomial fit 2434 - `fitd.coefficient_errors`, errors on the fitting coefficients 2435 - `fitd.monte_carlo_coefficients`, fitting coefficient set from MC generation 2436 - `fitd.covariance_matrix`, covariance matrix as a numpy array 2437 - `fitd.reduced_chi2`, reduced chi-square of the fitting 2438 - `fitd.ndof`, number of degrees of freedom 2439 - `fitd.data_sigma`, mean data dispersion from the central fit assuming `Chi2=1` 2440 - `fitd.error_lines`, a `vedo.shapes.Line` object for the upper and lower error band 2441 - `fitd.error_band`, the `vedo.mesh.Mesh` object representing the error band 2442 2443 Errors on x and y can be specified. If left to `None` an estimate is made from 2444 the statistical spread of the dataset itself. Errors are always assumed gaussian. 2445 2446 Arguments: 2447 deg : (int) 2448 degree of the polynomial to be fitted 2449 niter : (int) 2450 number of monte-carlo iterations to compute error bands. 2451 If set to 0, return the simple least-squares fit with naive error estimation 2452 on coefficients only. A reasonable non-zero value to set is about 500, in 2453 this case *error_lines*, *error_band* and the other class attributes are filled 2454 nstd : (float) 2455 nr. of standard deviation to use for error calculation 2456 xerrors : (list) 2457 array of the same length of points with the errors on x 2458 yerrors : (list) 2459 array of the same length of points with the errors on y 2460 vrange : (list) 2461 specify the domain range of the fitting line 2462 (only affects visualization, but can be used to extrapolate the fit 2463 outside the data range) 2464 res : (int) 2465 resolution of the output fitted line and error lines 2466 2467 Examples: 2468 - [fit_polynomial1.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/fit_polynomial1.py) 2469 2470  2471 """ 2472 if isinstance(points, vedo.pointcloud.Points): 2473 points = points.coordinates 2474 points = np.asarray(points) 2475 if len(points) == 2: # assume user is passing [x,y] 2476 points = np.c_[points[0], points[1]] 2477 x = points[:, 0] 2478 y = points[:, 1] # ignore z 2479 2480 n = len(x) 2481 ndof = n - deg - 1 2482 if vrange is not None: 2483 x0, x1 = vrange 2484 else: 2485 x0, x1 = np.min(x), np.max(x) 2486 if xerrors is not None: 2487 x0 -= xerrors[0] / 2 2488 x1 += xerrors[-1] / 2 2489 2490 tol = (x1 - x0) / 10000 2491 xr = np.linspace(x0, x1, res) 2492 2493 # project x errs on y 2494 if xerrors is not None: 2495 xerrors = np.asarray(xerrors) 2496 if yerrors is not None: 2497 yerrors = np.asarray(yerrors) 2498 w = 1.0 / yerrors 2499 coeffs = np.polyfit(x, y, deg, w=w, rcond=None) 2500 else: 2501 coeffs = np.polyfit(x, y, deg, rcond=None) 2502 # update yerrors, 1 bootstrap iteration is enough 2503 p1d = np.poly1d(coeffs) 2504 der = (p1d(x + tol) - p1d(x)) / tol 2505 yerrors = np.sqrt(yerrors * yerrors + np.power(der * xerrors, 2)) 2506 2507 if yerrors is not None: 2508 yerrors = np.asarray(yerrors) 2509 w = 1.0 / yerrors 2510 coeffs, V = np.polyfit(x, y, deg, w=w, rcond=None, cov=True) 2511 else: 2512 w = 1 2513 coeffs, V = np.polyfit(x, y, deg, rcond=None, cov=True) 2514 2515 p1d = np.poly1d(coeffs) 2516 theor = p1d(xr) 2517 fitl = shapes.Line(np.c_[xr, theor], lw=lw, c=c).z(tol * 2) 2518 fitl.coefficients = coeffs 2519 fitl.covariance_matrix = V 2520 residuals2_sum = np.sum(np.power(p1d(x) - y, 2)) / ndof 2521 sigma = np.sqrt(residuals2_sum) 2522 fitl.reduced_chi2 = np.sum(np.power((p1d(x) - y) * w, 2)) / ndof 2523 fitl.ndof = ndof 2524 fitl.data_sigma = sigma # worked out from data using chi2=1 hypo 2525 fitl.name = "LinearPolynomialFit" 2526 2527 if not niter: 2528 fitl.coefficient_errors = np.sqrt(np.diag(V)) 2529 return fitl ################################ 2530 2531 if yerrors is not None: 2532 sigma = yerrors 2533 else: 2534 w = None 2535 fitl.reduced_chi2 = 1 2536 2537 Theors, all_coeffs = [], [] 2538 for i in range(niter): 2539 noise = np.random.randn(n) * sigma 2540 coeffs = np.polyfit(x, y + noise, deg, w=w, rcond=None) 2541 all_coeffs.append(coeffs) 2542 P1d = np.poly1d(coeffs) 2543 Theor = P1d(xr) 2544 Theors.append(Theor) 2545 # all_coeffs = np.array(all_coeffs) 2546 fitl.monte_carlo_coefficients = np.array(all_coeffs) 2547 2548 stds = np.std(Theors, axis=0) 2549 fitl.coefficient_errors = np.std(all_coeffs, axis=0) 2550 2551 # check distributions on the fly 2552 # for i in range(deg+1): 2553 # histogram(all_coeffs[:,i],title='par'+str(i)).show(new=1) 2554 # histogram(all_coeffs[:,0], all_coeffs[:,1], 2555 # xtitle='param0', ytitle='param1',scalarbar=1).show(new=1) 2556 # histogram(all_coeffs[:,1], all_coeffs[:,2], 2557 # xtitle='param1', ytitle='param2').show(new=1) 2558 # histogram(all_coeffs[:,0], all_coeffs[:,2], 2559 # xtitle='param0', ytitle='param2').show(new=1) 2560 2561 error_lines = [] 2562 for i in [nstd, -nstd]: 2563 pp = np.c_[xr, theor + stds * i] 2564 el = shapes.Line(pp, lw=1, alpha=0.2, c="k").z(tol) 2565 error_lines.append(el) 2566 el.name = "ErrorLine for sigma=" + str(i) 2567 2568 fitl.error_lines = error_lines 2569 l1 = error_lines[0].coordinates.tolist() 2570 cband = l1 + list(reversed(error_lines[1].coordinates.tolist())) + [l1[0]] 2571 fitl.error_band = shapes.Line(cband).triangulate().lw(0).c("k", 0.15) 2572 fitl.error_band.name = "PolynomialFitErrorBand" 2573 return fitl 2574 2575 2576def _plot_fxy( 2577 z, 2578 xlim=(0, 3), 2579 ylim=(0, 3), 2580 zlim=(None, None), 2581 show_nan=True, 2582 zlevels=10, 2583 c=None, 2584 bc="aqua", 2585 alpha=1, 2586 texture="", 2587 bins=(100, 100), 2588 axes=True, 2589): 2590 import warnings 2591 2592 if c is not None: 2593 texture = None # disable 2594 2595 ps = vtki.new("PlaneSource") 2596 ps.SetResolution(bins[0], bins[1]) 2597 ps.SetNormal([0, 0, 1]) 2598 ps.Update() 2599 poly = ps.GetOutput() 2600 dx = xlim[1] - xlim[0] 2601 dy = ylim[1] - ylim[0] 2602 todel, nans = [], [] 2603 2604 for i in range(poly.GetNumberOfPoints()): 2605 px, py, _ = poly.GetPoint(i) 2606 xv = (px + 0.5) * dx + xlim[0] 2607 yv = (py + 0.5) * dy + ylim[0] 2608 try: 2609 with warnings.catch_warnings(): 2610 warnings.simplefilter("ignore") 2611 zv = z(xv, yv) 2612 if np.isnan(zv) or np.isinf(zv) or np.iscomplex(zv): 2613 zv = 0 2614 todel.append(i) 2615 nans.append([xv, yv, 0]) 2616 except: 2617 zv = 0 2618 todel.append(i) 2619 nans.append([xv, yv, 0]) 2620 poly.GetPoints().SetPoint(i, [xv, yv, zv]) 2621 2622 if todel: 2623 cellIds = vtki.vtkIdList() 2624 poly.BuildLinks() 2625 for i in todel: 2626 poly.GetPointCells(i, cellIds) 2627 for j in range(cellIds.GetNumberOfIds()): 2628 poly.DeleteCell(cellIds.GetId(j)) # flag cell 2629 poly.RemoveDeletedCells() 2630 cl = vtki.new("CleanPolyData") 2631 cl.SetInputData(poly) 2632 cl.Update() 2633 poly = cl.GetOutput() 2634 2635 if not poly.GetNumberOfPoints(): 2636 vedo.logger.error("function is not real in the domain") 2637 return None 2638 2639 if zlim[0]: 2640 poly = Mesh(poly).cut_with_plane((0, 0, zlim[0]), (0, 0, 1)).dataset 2641 if zlim[1]: 2642 poly = Mesh(poly).cut_with_plane((0, 0, zlim[1]), (0, 0, -1)).dataset 2643 2644 cmap = "" 2645 if c in colors.cmaps_names: 2646 cmap = c 2647 c = None 2648 bc = None 2649 2650 mesh = Mesh(poly, c, alpha).compute_normals().lighting("plastic") 2651 2652 if cmap: 2653 mesh.compute_elevation().cmap(cmap) 2654 if bc: 2655 mesh.bc(bc) 2656 if texture: 2657 mesh.texture(texture) 2658 2659 acts = [mesh] 2660 if zlevels: 2661 elevation = vtki.new("ElevationFilter") 2662 elevation.SetInputData(poly) 2663 bounds = poly.GetBounds() 2664 elevation.SetLowPoint(0, 0, bounds[4]) 2665 elevation.SetHighPoint(0, 0, bounds[5]) 2666 elevation.Update() 2667 bcf = vtki.new("BandedPolyDataContourFilter") 2668 bcf.SetInputData(elevation.GetOutput()) 2669 bcf.SetScalarModeToValue() 2670 bcf.GenerateContourEdgesOn() 2671 bcf.GenerateValues(zlevels, elevation.GetScalarRange()) 2672 bcf.Update() 2673 zpoly = bcf.GetContourEdgesOutput() 2674 zbandsact = Mesh(zpoly, "k", alpha).lw(1).lighting("off") 2675 zbandsact.mapper.SetResolveCoincidentTopologyToPolygonOffset() 2676 acts.append(zbandsact) 2677 2678 if show_nan and todel: 2679 bb = mesh.bounds() 2680 if bb[4] <= 0 and bb[5] >= 0: 2681 zm = 0.0 2682 else: 2683 zm = (bb[4] + bb[5]) / 2 2684 nans = np.array(nans) + [0, 0, zm] 2685 nansact = shapes.Points(nans, r=2, c="red5", alpha=alpha) 2686 nansact.properties.RenderPointsAsSpheresOff() 2687 acts.append(nansact) 2688 2689 if isinstance(axes, dict): 2690 axs = addons.Axes(mesh, **axes) 2691 acts.append(axs) 2692 elif axes: 2693 axs = addons.Axes(mesh) 2694 acts.append(axs) 2695 2696 assem = Assembly(acts) 2697 assem.name = "PlotFxy" 2698 return assem 2699 2700 2701def _plot_fz( 2702 z, 2703 x=(-1, 1), 2704 y=(-1, 1), 2705 zlimits=(None, None), 2706 cmap="PiYG", 2707 alpha=1, 2708 lw=0.1, 2709 bins=(75, 75), 2710 axes=True, 2711): 2712 ps = vtki.new("PlaneSource") 2713 ps.SetResolution(bins[0], bins[1]) 2714 ps.SetNormal([0, 0, 1]) 2715 ps.Update() 2716 poly = ps.GetOutput() 2717 dx = x[1] - x[0] 2718 dy = y[1] - y[0] 2719 2720 arrImg = [] 2721 for i in range(poly.GetNumberOfPoints()): 2722 px, py, _ = poly.GetPoint(i) 2723 xv = (px + 0.5) * dx + x[0] 2724 yv = (py + 0.5) * dy + y[0] 2725 try: 2726 zv = z(complex(xv), complex(yv)) 2727 except: 2728 zv = 0 2729 poly.GetPoints().SetPoint(i, [xv, yv, np.real(zv)]) 2730 arrImg.append(np.imag(zv)) 2731 2732 mesh = Mesh(poly, alpha).lighting("plastic") 2733 v = max(abs(np.min(arrImg)), abs(np.max(arrImg))) 2734 mesh.cmap(cmap, arrImg, vmin=-v, vmax=v) 2735 mesh.compute_normals().lw(lw) 2736 2737 if zlimits[0]: 2738 mesh.cut_with_plane((0, 0, zlimits[0]), (0, 0, 1)) 2739 if zlimits[1]: 2740 mesh.cut_with_plane((0, 0, zlimits[1]), (0, 0, -1)) 2741 2742 acts = [mesh] 2743 if axes: 2744 axs = addons.Axes(mesh, ztitle="Real part") 2745 acts.append(axs) 2746 asse = Assembly(acts) 2747 asse.name = "PlotFz" 2748 if isinstance(z, str): 2749 asse.name += " " + z 2750 return asse 2751 2752 2753def _plot_polar( 2754 rphi, 2755 title="", 2756 tsize=0.1, 2757 lsize=0.05, 2758 r1=0, 2759 r2=1, 2760 c="blue", 2761 bc="k", 2762 alpha=1, 2763 ps=5, 2764 lw=3, 2765 deg=False, 2766 vmax=None, 2767 fill=False, 2768 splined=False, 2769 nrays=8, 2770 show_disc=True, 2771 show_lines=True, 2772 show_angles=True, 2773): 2774 if len(rphi) == 2: 2775 rphi = np.stack((rphi[0], rphi[1]), axis=1) 2776 2777 rphi = np.array(rphi, dtype=float) 2778 thetas = rphi[:, 0] 2779 radii = rphi[:, 1] 2780 2781 k = 180 / np.pi 2782 if deg: 2783 thetas = np.array(thetas, dtype=float) / k 2784 2785 vals = [] 2786 for v in thetas: # normalize range 2787 t = np.arctan2(np.sin(v), np.cos(v)) 2788 if t < 0: 2789 t += 2 * np.pi 2790 vals.append(t) 2791 thetas = np.array(vals, dtype=float) 2792 2793 if vmax is None: 2794 vmax = np.max(radii) 2795 2796 angles = [] 2797 points = [] 2798 for t, r in zip(thetas, radii): 2799 r = r / vmax * r2 + r1 2800 ct, st = np.cos(t), np.sin(t) 2801 points.append([r * ct, r * st, 0]) 2802 p0 = points[0] 2803 points.append(p0) 2804 2805 r2e = r1 + r2 2806 lines = None 2807 if splined: 2808 lines = shapes.KSpline(points, closed=True) 2809 lines.c(c).lw(lw).alpha(alpha) 2810 elif lw: 2811 lines = shapes.Line(points) 2812 lines.c(c).lw(lw).alpha(alpha) 2813 2814 points.pop() 2815 2816 ptsact = None 2817 if ps: 2818 ptsact = shapes.Points(points, r=ps, c=c, alpha=alpha) 2819 2820 filling = None 2821 if fill and lw: 2822 faces = [] 2823 coords = [[0, 0, 0]] + lines.coordinates.tolist() 2824 for i in range(1, lines.npoints): 2825 faces.append([0, i, i + 1]) 2826 filling = Mesh([coords, faces]).c(c).alpha(alpha) 2827 2828 back = None 2829 back2 = None 2830 if show_disc: 2831 back = shapes.Disc(r1=r2e, r2=r2e * 1.01, c=bc, res=(1, 360)) 2832 back.z(-0.01).lighting("off").alpha(alpha) 2833 back2 = shapes.Disc(r1=r2e / 2, r2=r2e / 2 * 1.005, c=bc, res=(1, 360)) 2834 back2.z(-0.01).lighting("off").alpha(alpha) 2835 2836 ti = None 2837 if title: 2838 ti = shapes.Text3D(title, (0, 0, 0), s=tsize, depth=0, justify="top-center") 2839 ti.pos(0, -r2e * 1.15, 0.01) 2840 2841 rays = [] 2842 if show_disc: 2843 rgap = 0.05 2844 for t in np.linspace(0, 2 * np.pi, num=nrays, endpoint=False): 2845 ct, st = np.cos(t), np.sin(t) 2846 if show_lines: 2847 l = shapes.Line((0, 0, -0.01), (r2e * ct * 1.03, r2e * st * 1.03, -0.01)) 2848 rays.append(l) 2849 ct2, st2 = np.cos(t + np.pi / nrays), np.sin(t + np.pi / nrays) 2850 lm = shapes.DashedLine((0, 0, -0.01), (r2e * ct2, r2e * st2, -0.01), spacing=0.25) 2851 rays.append(lm) 2852 elif show_angles: # just the ticks 2853 l = shapes.Line( 2854 (r2e * ct * 0.98, r2e * st * 0.98, -0.01), 2855 (r2e * ct * 1.03, r2e * st * 1.03, -0.01), 2856 ) 2857 if show_angles: 2858 if 0 <= t < np.pi / 2: 2859 ju = "bottom-left" 2860 elif t == np.pi / 2: 2861 ju = "bottom-center" 2862 elif np.pi / 2 < t <= np.pi: 2863 ju = "bottom-right" 2864 elif np.pi < t < np.pi * 3 / 2: 2865 ju = "top-right" 2866 elif t == np.pi * 3 / 2: 2867 ju = "top-center" 2868 else: 2869 ju = "top-left" 2870 a = shapes.Text3D(int(t * k), pos=(0, 0, 0), s=lsize, depth=0, justify=ju) 2871 a.pos(r2e * ct * (1 + rgap), r2e * st * (1 + rgap), -0.01) 2872 angles.append(a) 2873 2874 mrg = merge(back, back2, angles, rays, ti) 2875 if mrg: 2876 mrg.color(bc).alpha(alpha).lighting("off") 2877 rh = Assembly([lines, ptsact, filling] + [mrg]) 2878 rh.name = "PlotPolar" 2879 return rh 2880 2881 2882def _plot_spheric(rfunc, normalize=True, res=33, scalarbar=True, c="grey", alpha=0.05, cmap="jet"): 2883 sg = shapes.Sphere(res=res, quads=True) 2884 sg.alpha(alpha).c(c).wireframe() 2885 2886 cgpts = sg.coordinates 2887 r, theta, phi = cart2spher(*cgpts.T) 2888 2889 newr, inans = [], [] 2890 for i in range(len(r)): 2891 try: 2892 ri = rfunc(theta[i], phi[i]) 2893 if np.isnan(ri): 2894 inans.append(i) 2895 newr.append(1) 2896 else: 2897 newr.append(ri) 2898 except: 2899 inans.append(i) 2900 newr.append(1) 2901 2902 newr = np.array(newr, dtype=float) 2903 if normalize: 2904 newr = newr / np.max(newr) 2905 newr[inans] = 1 2906 2907 nanpts = [] 2908 if inans: 2909 redpts = spher2cart(newr[inans], theta[inans], phi[inans]).T 2910 nanpts.append(shapes.Points(redpts, r=4, c="r")) 2911 2912 pts = spher2cart(newr, theta, phi).T 2913 ssurf = sg.clone() 2914 ssurf.coordinates = pts 2915 if inans: 2916 ssurf.delete_cells_by_point_index(inans) 2917 2918 ssurf.alpha(1).wireframe(0).lw(0.1) 2919 2920 ssurf.cmap(cmap, newr) 2921 ssurf.compute_normals() 2922 2923 if scalarbar: 2924 xm = np.max([np.max(pts[0]), 1]) 2925 ym = np.max([np.abs(np.max(pts[1])), 1]) 2926 ssurf.mapper.SetScalarRange(np.min(newr), np.max(newr)) 2927 sb3d = ssurf.add_scalarbar3d(size=(xm * 0.07, ym), c="k").scalarbar 2928 sb3d.rotate_x(90).pos(xm * 1.1, 0, -0.5) 2929 else: 2930 sb3d = None 2931 2932 sg.pickable(False) 2933 asse = Assembly([ssurf, sg] + nanpts + [sb3d]) 2934 asse.name = "PlotSpheric" 2935 return asse 2936 2937 2938def _histogram_quad_bin(x, y, **kwargs): 2939 # generate a histogram with 3D bars 2940 # 2941 histo = Histogram2D(x, y, **kwargs) 2942 2943 gap = kwargs.pop("gap", 0) 2944 zscale = kwargs.pop("zscale", 1) 2945 cmap = kwargs.pop("cmap", "Blues_r") 2946 2947 gr = histo.objects[2] 2948 d = gr.diagonal_size() 2949 tol = d / 1_000_000 # tolerance 2950 if gap >= 0: 2951 gr.shrink(1 - gap - tol) 2952 gr.map_cells_to_points() 2953 2954 faces = np.array(gr.cells) 2955 s = 1 / histo.entries * len(faces) * zscale 2956 zvals = gr.pointdata["Scalars"] * s 2957 2958 pts1 = gr.coordinates 2959 pts2 = np.copy(pts1) 2960 pts2[:, 2] = zvals + tol 2961 newpts = np.vstack([pts1, pts2]) 2962 newzvals = np.hstack([zvals, zvals]) / s 2963 2964 n = pts1.shape[0] 2965 newfaces = [] 2966 for f in faces: 2967 f0, f1, f2, f3 = f 2968 f0n, f1n, f2n, f3n = f + n 2969 newfaces.extend( 2970 [ 2971 [f0, f1, f2, f3], 2972 [f0n, f1n, f2n, f3n], 2973 [f0, f1, f1n, f0n], 2974 [f1, f2, f2n, f1n], 2975 [f2, f3, f3n, f2n], 2976 [f3, f0, f0n, f3n], 2977 ] 2978 ) 2979 2980 msh = Mesh([newpts, newfaces]).pickable(False) 2981 msh.cmap(cmap, newzvals, name="Frequency") 2982 msh.lw(1).lighting("ambient") 2983 2984 histo.objects[2] = msh 2985 histo.RemovePart(gr.actor) 2986 histo.AddPart(msh.actor) 2987 histo.objects.append(msh) 2988 return histo 2989 2990 2991def _histogram_hex_bin( 2992 xvalues, yvalues, bins=12, norm=1, fill=True, c=None, cmap="terrain_r", alpha=1 2993) -> "Assembly": 2994 xmin, xmax = np.min(xvalues), np.max(xvalues) 2995 ymin, ymax = np.min(yvalues), np.max(yvalues) 2996 dx, dy = xmax - xmin, ymax - ymin 2997 2998 if utils.is_sequence(bins): 2999 n, m = bins 3000 else: 3001 if xmax - xmin < ymax - ymin: 3002 n = bins 3003 m = np.rint(dy / dx * n / 1.2 + 0.5).astype(int) 3004 else: 3005 m = bins 3006 n = np.rint(dx / dy * m * 1.2 + 0.5).astype(int) 3007 3008 values = np.stack((xvalues, yvalues), axis=1) 3009 zs = [[0.0]] * len(values) 3010 values = np.append(values, zs, axis=1) 3011 cloud = vedo.Points(values) 3012 3013 col = None 3014 if c is not None: 3015 col = colors.get_color(c) 3016 3017 hexs, binmax = [], 0 3018 ki, kj = 1.33, 1.12 3019 r = 0.47 / n * 1.2 * dx 3020 for i in range(n + 3): 3021 for j in range(m + 2): 3022 cyl = vtki.new("CylinderSource") 3023 cyl.SetResolution(6) 3024 cyl.CappingOn() 3025 cyl.SetRadius(0.5) 3026 cyl.SetHeight(0.1) 3027 cyl.Update() 3028 t = vtki.vtkTransform() 3029 if not i % 2: 3030 p = (i / ki, j / kj, 0) 3031 else: 3032 p = (i / ki, j / kj + 0.45, 0) 3033 q = (p[0] / n * 1.2 * dx + xmin, p[1] / m * dy + ymin, 0) 3034 ne = len(cloud.closest_point(q, radius=r)) 3035 if fill: 3036 t.Translate(p[0], p[1], ne / 2) 3037 t.Scale(1, 1, ne * 10) 3038 else: 3039 t.Translate(p[0], p[1], ne) 3040 t.RotateX(90) # put it along Z 3041 tf = vtki.new("TransformPolyDataFilter") 3042 tf.SetInputData(cyl.GetOutput()) 3043 tf.SetTransform(t) 3044 tf.Update() 3045 if c is None: 3046 col = i 3047 h = Mesh(tf.GetOutput(), c=col, alpha=alpha).flat() 3048 h.lighting("plastic") 3049 h.actor.PickableOff() 3050 hexs.append(h) 3051 if ne > binmax: 3052 binmax = ne 3053 3054 if cmap is not None: 3055 for h in hexs: 3056 z = h.bounds()[5] 3057 col = colors.color_map(z, cmap, 0, binmax) 3058 h.color(col) 3059 3060 asse = Assembly(hexs) 3061 asse.scale([1.2 / n * dx, 1 / m * dy, norm / binmax * (dx + dy) / 4]) 3062 asse.pos([xmin, ymin, 0]) 3063 asse.name = "HistogramHexBin" 3064 return asse 3065 3066 3067def _histogram_polar( 3068 values, 3069 weights=None, 3070 title="", 3071 tsize=0.1, 3072 bins=16, 3073 r1=0.25, 3074 r2=1, 3075 phigap=0.5, 3076 rgap=0.05, 3077 lpos=1, 3078 lsize=0.04, 3079 c="grey", 3080 bc="k", 3081 alpha=1, 3082 cmap=None, 3083 deg=False, 3084 vmin=None, 3085 vmax=None, 3086 labels=(), 3087 show_disc=True, 3088 nrays=8, 3089 show_lines=True, 3090 show_angles=True, 3091 show_errors=False, 3092): 3093 k = 180 / np.pi 3094 if deg: 3095 values = np.array(values, dtype=float) / k 3096 else: 3097 values = np.array(values, dtype=float) 3098 3099 vals = [] 3100 for v in values: # normalize range 3101 t = np.arctan2(np.sin(v), np.cos(v)) 3102 if t < 0: 3103 t += 2 * np.pi 3104 vals.append(t + 0.00001) 3105 3106 histodata, edges = np.histogram(vals, weights=weights, bins=bins, range=(0, 2 * np.pi)) 3107 3108 thetas = [] 3109 for i in range(bins): 3110 thetas.append((edges[i] + edges[i + 1]) / 2) 3111 3112 if vmin is None: 3113 vmin = np.min(histodata) 3114 if vmax is None: 3115 vmax = np.max(histodata) 3116 3117 errors = np.sqrt(histodata) 3118 r2e = r1 + r2 3119 if show_errors: 3120 r2e += np.max(errors) / vmax * 1.5 3121 3122 back = None 3123 if show_disc: 3124 back = shapes.Disc(r1=r2e, r2=r2e * 1.01, c=bc, res=(1, 360)) 3125 back.z(-0.01) 3126 3127 slices = [] 3128 lines = [] 3129 angles = [] 3130 errbars = [] 3131 3132 for i, t in enumerate(thetas): 3133 r = histodata[i] / vmax * r2 3134 d = shapes.Disc((0, 0, 0), r1, r1 + r, res=(1, 360)) 3135 delta = np.pi / bins - np.pi / 2 - phigap / k 3136 d.cut_with_plane(normal=(np.cos(t + delta), np.sin(t + delta), 0)) 3137 d.cut_with_plane(normal=(np.cos(t - delta), np.sin(t - delta), 0)) 3138 if cmap is not None: 3139 cslice = colors.color_map(histodata[i], cmap, vmin, vmax) 3140 d.color(cslice) 3141 else: 3142 if c is None: 3143 d.color(i) 3144 elif utils.is_sequence(c) and len(c) == bins: 3145 d.color(c[i]) 3146 else: 3147 d.color(c) 3148 d.alpha(alpha).lighting("off") 3149 slices.append(d) 3150 3151 ct, st = np.cos(t), np.sin(t) 3152 3153 if show_errors: 3154 err = np.sqrt(histodata[i]) / vmax * r2 3155 errl = shapes.Line( 3156 ((r1 + r - err) * ct, (r1 + r - err) * st, 0.01), 3157 ((r1 + r + err) * ct, (r1 + r + err) * st, 0.01), 3158 ) 3159 errl.alpha(alpha).lw(3).color(bc) 3160 errbars.append(errl) 3161 3162 labs = [] 3163 rays = [] 3164 if show_disc: 3165 outerdisc = shapes.Disc(r1=r2e, r2=r2e * 1.01, c=bc, res=(1, 360)) 3166 outerdisc.z(-0.01) 3167 innerdisc = shapes.Disc(r1=r2e / 2, r2=r2e / 2 * 1.005, c=bc, res=(1, 360)) 3168 innerdisc.z(-0.01) 3169 rays.append(outerdisc) 3170 rays.append(innerdisc) 3171 3172 rgap = 0.05 3173 for t in np.linspace(0, 2 * np.pi, num=nrays, endpoint=False): 3174 ct, st = np.cos(t), np.sin(t) 3175 if show_lines: 3176 l = shapes.Line((0, 0, -0.01), (r2e * ct * 1.03, r2e * st * 1.03, -0.01)) 3177 rays.append(l) 3178 ct2, st2 = np.cos(t + np.pi / nrays), np.sin(t + np.pi / nrays) 3179 lm = shapes.DashedLine((0, 0, -0.01), (r2e * ct2, r2e * st2, -0.01), spacing=0.25) 3180 rays.append(lm) 3181 elif show_angles: # just the ticks 3182 l = shapes.Line( 3183 (r2e * ct * 0.98, r2e * st * 0.98, -0.01), 3184 (r2e * ct * 1.03, r2e * st * 1.03, -0.01), 3185 ) 3186 if show_angles: 3187 if 0 <= t < np.pi / 2: 3188 ju = "bottom-left" 3189 elif t == np.pi / 2: 3190 ju = "bottom-center" 3191 elif np.pi / 2 < t <= np.pi: 3192 ju = "bottom-right" 3193 elif np.pi < t < np.pi * 3 / 2: 3194 ju = "top-right" 3195 elif t == np.pi * 3 / 2: 3196 ju = "top-center" 3197 else: 3198 ju = "top-left" 3199 a = shapes.Text3D(int(t * k), pos=(0, 0, 0), s=lsize, depth=0, justify=ju) 3200 a.pos(r2e * ct * (1 + rgap), r2e * st * (1 + rgap), -0.01) 3201 angles.append(a) 3202 3203 ti = None 3204 if title: 3205 ti = shapes.Text3D(title, (0, 0, 0), s=tsize, depth=0, justify="top-center") 3206 ti.pos(0, -r2e * 1.15, 0.01) 3207 3208 for i, t in enumerate(thetas): 3209 if i < len(labels): 3210 lab = shapes.Text3D( 3211 labels[i], (0, 0, 0), s=lsize, depth=0, justify="center" # font="VTK", 3212 ) 3213 lab.pos( 3214 r2e * np.cos(t) * (1 + rgap) * lpos / 2, 3215 r2e * np.sin(t) * (1 + rgap) * lpos / 2, 3216 0.01, 3217 ) 3218 labs.append(lab) 3219 3220 mrg = merge(lines, angles, rays, ti, labs) 3221 if mrg: 3222 mrg.color(bc).lighting("off") 3223 3224 acts = slices + errbars + [mrg] 3225 asse = Assembly(acts) 3226 asse.frequencies = histodata 3227 asse.bins = edges 3228 asse.name = "HistogramPolar" 3229 return asse 3230 3231 3232def _histogram_spheric(thetavalues, phivalues, rmax=1.2, res=8, cmap="rainbow", gap=0.1): 3233 3234 x, y, z = spher2cart(np.ones_like(thetavalues) * 1.1, thetavalues, phivalues) 3235 ptsvals = np.c_[x, y, z] 3236 3237 sg = shapes.Sphere(res=res, quads=True).shrink(1 - gap) 3238 sgfaces = sg.cells 3239 sgpts = sg.coordinates 3240 3241 cntrs = sg.cell_centers().coordinates 3242 counts = np.zeros(len(cntrs)) 3243 for p in ptsvals: 3244 cell = sg.closest_point(p, return_cell_id=True) 3245 counts[cell] += 1 3246 acounts = np.array(counts, dtype=float) 3247 counts *= (rmax - 1) / np.max(counts) 3248 3249 for cell, cn in enumerate(counts): 3250 if not cn: 3251 continue 3252 fs = sgfaces[cell] 3253 pts = sgpts[fs] 3254 _, t1, p1 = cart2spher(pts[:, 0], pts[:, 1], pts[:, 2]) 3255 x, y, z = spher2cart(1 + cn, t1, p1) 3256 sgpts[fs] = np.c_[x, y, z] 3257 3258 sg.coordinates = sgpts 3259 sg.cmap(cmap, acounts, on="cells") 3260 vals = sg.celldata["Scalars"] 3261 3262 faces = sg.cells 3263 points = sg.coordinates.tolist() + [[0.0, 0.0, 0.0]] 3264 lp = len(points) - 1 3265 newfaces = [] 3266 newvals = [] 3267 for i, f in enumerate(faces): 3268 p0, p1, p2, p3 = f 3269 newfaces.append(f) 3270 newfaces.append([p0, lp, p1]) 3271 newfaces.append([p1, lp, p2]) 3272 newfaces.append([p2, lp, p3]) 3273 newfaces.append([p3, lp, p0]) 3274 for _ in range(5): 3275 newvals.append(vals[i]) 3276 3277 newsg = Mesh([points, newfaces]).cmap(cmap, newvals, on="cells") 3278 newsg.compute_normals().flat() 3279 newsg.name = "HistogramSpheric" 3280 return newsg 3281 3282 3283def pie_chart( 3284 fractions, 3285 title="", 3286 tsize=0.3, 3287 r1=1.7, 3288 r2=1, 3289 phigap=0, 3290 lpos=0.8, 3291 lsize=0.15, 3292 c=None, 3293 bc="k", 3294 alpha=1, 3295 labels=(), 3296 show_disc=False, 3297) -> "Assembly": 3298 """ 3299 Donut plot or pie chart. 3300 3301 Arguments: 3302 title : (str) 3303 plot title 3304 tsize : (float) 3305 title size 3306 r1 : (float) inner radius 3307 r2 : (float) 3308 outer radius, starting from r1 3309 phigap : (float) 3310 gap angle btw 2 radial bars, in degrees 3311 lpos : (float) 3312 label gap factor along radius 3313 lsize : (float) 3314 label size 3315 c : (color) 3316 color of the plot slices 3317 bc : (color) 3318 color of the disc frame 3319 alpha : (float) 3320 opacity of the disc frame 3321 labels : (list) 3322 list of labels 3323 show_disc : (bool) 3324 show the outer ring axis 3325 3326 Examples: 3327 - [donut.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/donut.py) 3328 3329  3330 """ 3331 fractions = np.array(fractions, dtype=float) 3332 angles = np.add.accumulate(2 * np.pi * fractions) 3333 angles[-1] = 2 * np.pi 3334 if angles[-2] > 2 * np.pi: 3335 print("Error in donut(): fractions must sum to 1.") 3336 raise RuntimeError 3337 3338 cols = [] 3339 for i, th in enumerate(np.linspace(0, 2 * np.pi, 360, endpoint=False)): 3340 for ia, a in enumerate(angles): 3341 if th < a: 3342 cols.append(c[ia]) 3343 break 3344 labs = [] 3345 if labels: 3346 angles = np.concatenate([[0], angles]) 3347 labs = [""] * 360 3348 for i in range(len(labels)): 3349 a = (angles[i + 1] + angles[i]) / 2 3350 j = int(a / np.pi * 180) 3351 labs[j] = labels[i] 3352 3353 data = np.linspace(0, 2 * np.pi, 360, endpoint=False) + 0.005 3354 dn = _histogram_polar( 3355 data, 3356 title=title, 3357 bins=360, 3358 r1=r1, 3359 r2=r2, 3360 phigap=phigap, 3361 lpos=lpos, 3362 lsize=lsize, 3363 tsize=tsize, 3364 c=cols, 3365 bc=bc, 3366 alpha=alpha, 3367 vmin=0, 3368 vmax=1, 3369 labels=labs, 3370 show_disc=show_disc, 3371 show_lines=0, 3372 show_angles=0, 3373 show_errors=0, 3374 ) 3375 dn.name = "Donut" 3376 return dn 3377 3378 3379def violin( 3380 values, 3381 bins=10, 3382 vlim=None, 3383 x=0, 3384 width=3, 3385 splined=True, 3386 fill=True, 3387 c="violet", 3388 alpha=1, 3389 outline=True, 3390 centerline=True, 3391 lc="darkorchid", 3392 lw=3, 3393) -> "Assembly": 3394 """ 3395 Violin style histogram. 3396 3397 Arguments: 3398 bins : (int) 3399 number of bins 3400 vlim : (list) 3401 input value limits. Crop values outside range 3402 x : (float) 3403 x-position of the violin axis 3404 width : (float) 3405 width factor of the normalized distribution 3406 splined : (bool) 3407 spline the outline 3408 fill : (bool) 3409 fill violin with solid color 3410 outline : (bool) 3411 add the distribution outline 3412 centerline : (bool) 3413 add the vertical centerline at x 3414 lc : (color) 3415 line color 3416 3417 Examples: 3418 - [histo_violin.py](https://github.com/marcomusy/vedo/tree/master/examples/examples/pyplot/histo_violin.py) 3419 3420  3421 """ 3422 fs, edges = np.histogram(values, bins=bins, range=vlim) 3423 mine, maxe = np.min(edges), np.max(edges) 3424 fs = fs.astype(float) / len(values) * width 3425 3426 rs = [] 3427 3428 if splined: 3429 lnl, lnr = [(0, edges[0], 0)], [(0, edges[0], 0)] 3430 for i in range(bins): 3431 xc = (edges[i] + edges[i + 1]) / 2 3432 yc = fs[i] 3433 lnl.append([-yc, xc, 0]) 3434 lnr.append([yc, xc, 0]) 3435 lnl.append((0, edges[-1], 0)) 3436 lnr.append((0, edges[-1], 0)) 3437 spl = shapes.KSpline(lnl).x(x) 3438 spr = shapes.KSpline(lnr).x(x) 3439 spl.color(lc).alpha(alpha).lw(lw) 3440 spr.color(lc).alpha(alpha).lw(lw) 3441 if outline: 3442 rs.append(spl) 3443 rs.append(spr) 3444 if fill: 3445 rb = shapes.Ribbon(spl, spr, c=c, alpha=alpha).lighting("off") 3446 rs.append(rb) 3447 3448 else: 3449 lns1 = [[0, mine, 0]] 3450 for i in range(bins): 3451 lns1.append([fs[i], edges[i], 0]) 3452 lns1.append([fs[i], edges[i + 1], 0]) 3453 lns1.append([0, maxe, 0]) 3454 3455 lns2 = [[0, mine, 0]] 3456 for i in range(bins): 3457 lns2.append([-fs[i], edges[i], 0]) 3458 lns2.append([-fs[i], edges[i + 1], 0]) 3459 lns2.append([0, maxe, 0]) 3460 3461 if outline: 3462 rs.append(shapes.Line(lns1, c=lc, alpha=alpha, lw=lw).x(x)) 3463 rs.append(shapes.Line(lns2, c=lc, alpha=alpha, lw=lw).x(x)) 3464 3465 if fill: 3466 for i in range(bins): 3467 p0 = (-fs[i], edges[i], 0) 3468 p1 = (fs[i], edges[i + 1], 0) 3469 r = shapes.Rectangle(p0, p1).x(p0[0] + x) 3470 r.color(c).alpha(alpha).lighting("off") 3471 rs.append(r) 3472 3473 if centerline: 3474 cl = shapes.Line([0, mine, 0.01], [0, maxe, 0.01], c=lc, alpha=alpha, lw=2).x(x) 3475 rs.append(cl) 3476 3477 asse = Assembly(rs) 3478 asse.name = "Violin" 3479 return asse 3480 3481 3482def whisker(data, s=0.25, c="k", lw=2, bc="blue", alpha=0.25, r=5, jitter=True, horizontal=False) -> "Assembly": 3483 """ 3484 Generate a "whisker" bar from a 1-dimensional dataset. 3485 3486 Arguments: 3487 s : (float) 3488 size of the box 3489 c : (color) 3490 color of the lines 3491 lw : (float) 3492 line width 3493 bc : (color) 3494 color of the box 3495 alpha : (float) 3496 transparency of the box 3497 r : (float) 3498 point radius in pixels (use value 0 to disable) 3499 jitter : (bool) 3500 add some randomness to points to avoid overlap 3501 horizontal : (bool) 3502 set horizontal layout 3503 3504 Examples: 3505 - [whiskers.py](https://github.com/marcomusy/vedo/tree/master/examples/examples/pyplot/whiskers.py) 3506 3507  3508 """ 3509 xvals = np.zeros_like(np.asarray(data)) 3510 if jitter: 3511 xjit = np.random.randn(len(xvals)) * s / 9 3512 xjit = np.clip(xjit, -s / 2.1, s / 2.1) 3513 xvals += xjit 3514 3515 dmean = np.mean(data) 3516 dq05 = np.quantile(data, 0.05) 3517 dq25 = np.quantile(data, 0.25) 3518 dq75 = np.quantile(data, 0.75) 3519 dq95 = np.quantile(data, 0.95) 3520 3521 pts = None 3522 if r: 3523 pts = shapes.Points(np.array([xvals, data]).T, c=c, r=r) 3524 3525 rec = shapes.Rectangle([-s / 2, dq25], [s / 2, dq75], c=bc, alpha=alpha) 3526 rec.properties.LightingOff() 3527 rl = shapes.Line([[-s / 2, dq25], [s / 2, dq25], [s / 2, dq75], [-s / 2, dq75]], closed=True) 3528 l1 = shapes.Line([0, dq05, 0], [0, dq25, 0], c=c, lw=lw) 3529 l2 = shapes.Line([0, dq75, 0], [0, dq95, 0], c=c, lw=lw) 3530 lm = shapes.Line([-s / 2, dmean], [s / 2, dmean]) 3531 lns = merge(l1, l2, lm, rl) 3532 asse = Assembly([lns, rec, pts]) 3533 if horizontal: 3534 asse.rotate_z(-90) 3535 asse.name = "Whisker" 3536 asse.info["mean"] = dmean 3537 asse.info["quantile_05"] = dq05 3538 asse.info["quantile_25"] = dq25 3539 asse.info["quantile_75"] = dq75 3540 asse.info["quantile_95"] = dq95 3541 return asse 3542 3543 3544def streamplot( 3545 X, Y, U, V, direction="both", max_propagation=None, lw=2, cmap="viridis", probes=() 3546) -> Union["vedo.shapes.Lines", None]: 3547 """ 3548 Generate a streamline plot of a vectorial field (U,V) defined at positions (X,Y). 3549 Returns a `Mesh` object. 3550 3551 Arguments: 3552 direction : (str) 3553 either "forward", "backward" or "both" 3554 max_propagation : (float) 3555 maximum physical length of the streamline 3556 lw : (float) 3557 line width in absolute units 3558 3559 Examples: 3560 - [plot_stream.py](https://github.com/marcomusy/vedo/tree/master/examples/examples/pyplot/plot_stream.py) 3561 3562  3563 """ 3564 n = len(X) 3565 m = len(Y[0]) 3566 if n != m: 3567 print("Limitation in streamplot(): only square grids are allowed.", n, m) 3568 raise RuntimeError() 3569 3570 xmin, xmax = X[0][0], X[-1][-1] 3571 ymin, ymax = Y[0][0], Y[-1][-1] 3572 3573 field = np.sqrt(U * U + V * V) 3574 3575 vol = vedo.Volume(field, dims=(n, n, 1)) 3576 3577 uf = np.ravel(U, order="F") 3578 vf = np.ravel(V, order="F") 3579 vects = np.c_[uf, vf, np.zeros_like(uf)] 3580 vol.pointdata["StreamPlotField"] = vects 3581 3582 if len(probes) == 0: 3583 probe = shapes.Grid(pos=((n - 1) / 2, (n - 1) / 2, 0), s=(n - 1, n - 1), res=(n - 1, n - 1)) 3584 else: 3585 if isinstance(probes, vedo.Points): 3586 probes = probes.coordinates 3587 else: 3588 probes = np.array(probes, dtype=float) 3589 if len(probes[0]) == 2: 3590 probes = np.c_[probes[:, 0], probes[:, 1], np.zeros(len(probes))] 3591 sv = [(n - 1) / (xmax - xmin), (n - 1) / (ymax - ymin), 1] 3592 probes = probes - [xmin, ymin, 0] 3593 probes = np.multiply(probes, sv) 3594 probe = vedo.Points(probes) 3595 3596 stream = vol.compute_streamlines(probe, direction=direction, max_propagation=max_propagation) 3597 if stream: 3598 stream.lw(lw).cmap(cmap).lighting("off") 3599 stream.scale([1 / (n - 1) * (xmax - xmin), 1 / (n - 1) * (ymax - ymin), 1]) 3600 stream.shift(xmin, ymin) 3601 return stream 3602 3603 3604def matrix( 3605 M, 3606 title="Matrix", 3607 xtitle="", 3608 ytitle="", 3609 xlabels=(), 3610 ylabels=(), 3611 xrotation=0, 3612 cmap="Reds", 3613 vmin=None, 3614 vmax=None, 3615 precision=2, 3616 font="Theemim", 3617 scale=0, 3618 scalarbar=True, 3619 lc="white", 3620 lw=0, 3621 c="black", 3622 alpha=1, 3623) -> "Assembly": 3624 """ 3625 Generate a matrix, or a 2D color-coded plot with bin labels. 3626 3627 Returns an `Assembly` object. 3628 3629 Arguments: 3630 M : (list, numpy array) 3631 the input array to visualize 3632 title : (str) 3633 title of the plot 3634 xtitle : (str) 3635 title of the horizontal colmuns 3636 ytitle : (str) 3637 title of the vertical rows 3638 xlabels : (list) 3639 individual string labels for each column. Must be of length m 3640 ylabels : (list) 3641 individual string labels for each row. Must be of length n 3642 xrotation : (float) 3643 rotation of the horizontal labels 3644 cmap : (str) 3645 color map name 3646 vmin : (float) 3647 minimum value of the colormap range 3648 vmax : (float) 3649 maximum value of the colormap range 3650 precision : (int) 3651 number of digits for the matrix entries or bins 3652 font : (str) 3653 font name. Check [available fonts here](https://vedo.embl.es/fonts). 3654 3655 scale : (float) 3656 size of the numeric entries or bin values 3657 scalarbar : (bool) 3658 add a scalar bar to the right of the plot 3659 lc : (str) 3660 color of the line separating the bins 3661 lw : (float) 3662 Width of the line separating the bins 3663 c : (str) 3664 text color 3665 alpha : (float) 3666 plot transparency 3667 3668 Examples: 3669 - [np_matrix.py](https://github.com/marcomusy/vedo/tree/master/examples/examples/pyplot/np_matrix.py) 3670 3671  3672 """ 3673 M = np.asarray(M) 3674 n, m = M.shape 3675 gr = shapes.Grid(res=(m, n), s=(m / (m + n) * 2, n / (m + n) * 2), c=c, alpha=alpha) 3676 gr.wireframe(False).lc(lc).lw(lw) 3677 3678 matr = np.flip(np.flip(M), axis=1).ravel(order="C") 3679 gr.cmap(cmap, matr, on="cells", vmin=vmin, vmax=vmax) 3680 sbar = None 3681 if scalarbar: 3682 gr.add_scalarbar3d(title_font=font, label_font=font) 3683 sbar = gr.scalarbar 3684 labs = None 3685 if scale != 0: 3686 gr.compute_normals(points=False) 3687 labs = gr.labels( 3688 on="cells", 3689 scale=scale / max(m, n), 3690 precision=precision, 3691 font=font, 3692 justify="center", 3693 c=c, 3694 ) 3695 labs.z(0.001) 3696 t = None 3697 if title: 3698 if title == "Matrix": 3699 title += " " + str(n) + "x" + str(m) 3700 t = shapes.Text3D(title, font=font, s=0.04, justify="bottom-center", c=c) 3701 t.shift(0, n / (m + n) * 1.05) 3702 3703 xlabs = None 3704 if len(xlabels) == m: 3705 xlabs = [] 3706 jus = "top-center" 3707 if xrotation > 44: 3708 jus = "right-center" 3709 for i in range(m): 3710 xl = shapes.Text3D(xlabels[i], font=font, s=0.02, justify=jus, c=c).rotate_z(xrotation) 3711 xl.shift((2 * i - m + 1) / (m + n), -n / (m + n) * 1.05) 3712 xlabs.append(xl) 3713 3714 ylabs = None 3715 if len(ylabels) == n: 3716 ylabels = list(reversed(ylabels)) 3717 ylabs = [] 3718 for i in range(n): 3719 yl = shapes.Text3D(ylabels[i], font=font, s=0.02, justify="right-center", c=c) 3720 yl.shift(-m / (m + n) * 1.05, (2 * i - n + 1) / (m + n)) 3721 ylabs.append(yl) 3722 3723 xt = None 3724 if xtitle: 3725 xt = shapes.Text3D(xtitle, font=font, s=0.035, justify="top-center", c=c) 3726 xt.shift(0, -n / (m + n) * 1.05) 3727 if xlabs is not None: 3728 y0, y1 = xlabs[0].ybounds() 3729 xt.shift(0, -(y1 - y0) - 0.55 / (m + n)) 3730 yt = None 3731 if ytitle: 3732 yt = shapes.Text3D(ytitle, font=font, s=0.035, justify="bottom-center", c=c).rotate_z(90) 3733 yt.shift(-m / (m + n) * 1.05, 0) 3734 if ylabs is not None: 3735 x0, x1 = ylabs[0].xbounds() 3736 yt.shift(-(x1 - x0) - 0.55 / (m + n), 0) 3737 asse = Assembly(gr, sbar, labs, t, xt, yt, xlabs, ylabs) 3738 asse.name = "Matrix" 3739 return asse 3740 3741 3742def CornerPlot(points, pos=1, s=0.2, title="", c="b", bg="k", lines=True, dots=True): 3743 """ 3744 Return a `vtkXYPlotActor` that is a plot of `x` versus `y`, 3745 where `points` is a list of `(x,y)` points. 3746 3747 Assign position following this convention: 3748 3749 - 1, topleft, 3750 - 2, topright, 3751 - 3, bottomleft, 3752 - 4, bottomright. 3753 """ 3754 if len(points) == 2: # passing [allx, ally] 3755 points = np.stack((points[0], points[1]), axis=1) 3756 3757 c = colors.get_color(c) # allow different codings 3758 array_x = vtki.vtkFloatArray() 3759 array_y = vtki.vtkFloatArray() 3760 array_x.SetNumberOfTuples(len(points)) 3761 array_y.SetNumberOfTuples(len(points)) 3762 for i, p in enumerate(points): 3763 array_x.InsertValue(i, p[0]) 3764 array_y.InsertValue(i, p[1]) 3765 field = vtki.vtkFieldData() 3766 field.AddArray(array_x) 3767 field.AddArray(array_y) 3768 data = vtki.vtkDataObject() 3769 data.SetFieldData(field) 3770 3771 xyplot = vtki.new("XYPlotActor") 3772 xyplot.AddDataObjectInput(data) 3773 xyplot.SetDataObjectXComponent(0, 0) 3774 xyplot.SetDataObjectYComponent(0, 1) 3775 xyplot.SetXValuesToValue() 3776 xyplot.SetAdjustXLabels(0) 3777 xyplot.SetAdjustYLabels(0) 3778 xyplot.SetNumberOfXLabels(3) 3779 3780 xyplot.GetProperty().SetPointSize(5) 3781 xyplot.GetProperty().SetLineWidth(2) 3782 xyplot.GetProperty().SetColor(colors.get_color(bg)) 3783 xyplot.SetPlotColor(0, c[0], c[1], c[2]) 3784 3785 xyplot.SetXTitle(title) 3786 xyplot.SetYTitle("") 3787 xyplot.ExchangeAxesOff() 3788 xyplot.SetPlotPoints(dots) 3789 3790 if not lines: 3791 xyplot.PlotLinesOff() 3792 3793 if isinstance(pos, str): 3794 spos = 2 3795 if "top" in pos: 3796 if "left" in pos: 3797 spos = 1 3798 elif "right" in pos: 3799 spos = 2 3800 elif "bottom" in pos: 3801 if "left" in pos: 3802 spos = 3 3803 elif "right" in pos: 3804 spos = 4 3805 pos = spos 3806 if pos == 1: 3807 xyplot.GetPositionCoordinate().SetValue(0.0, 0.8, 0) 3808 elif pos == 2: 3809 xyplot.GetPositionCoordinate().SetValue(0.76, 0.8, 0) 3810 elif pos == 3: 3811 xyplot.GetPositionCoordinate().SetValue(0.0, 0.0, 0) 3812 elif pos == 4: 3813 xyplot.GetPositionCoordinate().SetValue(0.76, 0.0, 0) 3814 else: 3815 xyplot.GetPositionCoordinate().SetValue(pos[0], pos[1], 0) 3816 3817 xyplot.GetPosition2Coordinate().SetValue(s, s, 0) 3818 return xyplot 3819 3820 3821def CornerHistogram( 3822 values, 3823 bins=20, 3824 vrange=None, 3825 minbin=0, 3826 logscale=False, 3827 title="", 3828 c="g", 3829 bg="k", 3830 alpha=1, 3831 pos="bottom-left", 3832 s=0.175, 3833 lines=True, 3834 dots=False, 3835 nmax=None, 3836): 3837 """ 3838 Build a histogram from a list of values in n bins. 3839 The resulting object is a 2D actor. 3840 3841 Use `vrange` to restrict the range of the histogram. 3842 3843 Use `nmax` to limit the sampling to this max nr of entries 3844 3845 Use `pos` to assign its position: 3846 - 1, topleft, 3847 - 2, topright, 3848 - 3, bottomleft, 3849 - 4, bottomright, 3850 - (x, y), as fraction of the rendering window 3851 """ 3852 if hasattr(values, "dataset"): 3853 values = utils.vtk2numpy(values.dataset.GetPointData().GetScalars()) 3854 3855 n = values.shape[0] 3856 if nmax and nmax < n: 3857 # subsample: 3858 idxs = np.linspace(0, n, num=int(nmax), endpoint=False).astype(int) 3859 values = values[idxs] 3860 3861 fs, edges = np.histogram(values, bins=bins, range=vrange) 3862 3863 if minbin: 3864 fs = fs[minbin:-1] 3865 if logscale: 3866 fs = np.log10(fs + 1) 3867 pts = [] 3868 for i in range(len(fs)): 3869 pts.append([(edges[i] + edges[i + 1]) / 2, fs[i]]) 3870 3871 cplot = CornerPlot(pts, pos, s, title, c, bg, lines, dots) 3872 cplot.SetNumberOfYLabels(2) 3873 cplot.SetNumberOfXLabels(3) 3874 tprop = vtki.vtkTextProperty() 3875 tprop.SetColor(colors.get_color(bg)) 3876 tprop.SetFontFamily(vtki.VTK_FONT_FILE) 3877 tprop.SetFontFile(utils.get_font_path("Calco")) 3878 tprop.SetOpacity(alpha) 3879 cplot.SetAxisTitleTextProperty(tprop) 3880 cplot.GetProperty().SetOpacity(alpha) 3881 cplot.GetXAxisActor2D().SetLabelTextProperty(tprop) 3882 cplot.GetXAxisActor2D().SetTitleTextProperty(tprop) 3883 cplot.GetXAxisActor2D().SetFontFactor(0.55) 3884 cplot.GetYAxisActor2D().SetLabelFactor(0.0) 3885 cplot.GetYAxisActor2D().LabelVisibilityOff() 3886 return cplot 3887 3888 3889class DirectedGraph(Assembly): 3890 """ 3891 Support for Directed Graphs. 3892 """ 3893 3894 def __init__(self, **kargs): 3895 """ 3896 A graph consists of a collection of nodes (without postional information) 3897 and a collection of edges connecting pairs of nodes. 3898 The task is to determine the node positions only based on their connections. 3899 3900 This class is derived from class `Assembly`, and it assembles 4 Mesh objects 3901 representing the graph, the node labels, edge labels and edge arrows. 3902 3903 Arguments: 3904 c : (color) 3905 Color of the Graph 3906 n : (int) 3907 number of the initial set of nodes 3908 layout : (int, str) 3909 layout in 3910 `['2d', 'fast2d', 'clustering2d', 'circular', 'circular3d', 'cone', 'force', 'tree']`. 3911 Each of these layouts has different available options. 3912 3913 --------------------------------------------------------------- 3914 .. note:: Options for layouts '2d', 'fast2d' and 'clustering2d' 3915 3916 Arguments: 3917 seed : (int) 3918 seed of the random number generator used to jitter point positions 3919 rest_distance : (float) 3920 manually set the resting distance 3921 nmax : (int) 3922 the maximum number of iterations to be used 3923 zrange : (list) 3924 expand 2d graph along z axis. 3925 3926 --------------------------------------------------------------- 3927 .. note:: Options for layouts 'circular', and 'circular3d': 3928 3929 Arguments: 3930 radius : (float) 3931 set the radius of the circles 3932 height : (float) 3933 set the vertical (local z) distance between the circles 3934 zrange : (float) 3935 expand 2d graph along z axis 3936 3937 --------------------------------------------------------------- 3938 .. note:: Options for layout 'cone' 3939 3940 Arguments: 3941 compactness : (float) 3942 ratio between the average width of a cone in the tree, 3943 and the height of the cone. 3944 compression : (bool) 3945 put children closer together, possibly allowing sub-trees to overlap. 3946 This is useful if the tree is actually the spanning tree of a graph. 3947 spacing : (float) 3948 space between layers of the tree 3949 3950 --------------------------------------------------------------- 3951 .. note:: Options for layout 'force' 3952 3953 Arguments: 3954 seed : (int) 3955 seed the random number generator used to jitter point positions 3956 bounds : (list) 3957 set the region in space in which to place the final graph 3958 nmax : (int) 3959 the maximum number of iterations to be used 3960 three_dimensional : (bool) 3961 allow optimization in the 3rd dimension too 3962 random_initial_points : (bool) 3963 use random positions within the graph bounds as initial points 3964 3965 Examples: 3966 - [lineage_graph.py](https://github.com/marcomusy/vedo/tree/master/examples/examples/pyplot/lineage_graph.py) 3967 3968  3969 3970 - [graph_network.py](https://github.com/marcomusy/vedo/tree/master/examples/examples/pyplot/graph_network.py) 3971 3972  3973 """ 3974 3975 super().__init__() 3976 3977 self.nodes = [] 3978 self.edges = [] 3979 3980 self._node_labels = [] # holds strings 3981 self._edge_labels = [] 3982 self.edge_orientations = [] 3983 self.edge_glyph_position = 0.6 3984 3985 self.zrange = 0.0 3986 3987 self.rotX = 0 3988 self.rotY = 0 3989 self.rotZ = 0 3990 3991 self.arrow_scale = 0.15 3992 self.node_label_scale = None 3993 self.node_label_justify = "bottom-left" 3994 3995 self.edge_label_scale = None 3996 3997 self.mdg = vtki.new("MutableDirectedGraph") 3998 3999 n = kargs.pop("n", 0) 4000 for _ in range(n): 4001 self.add_node() 4002 4003 self._c = kargs.pop("c", (0.3, 0.3, 0.3)) 4004 4005 self.gl = vtki.new("GraphLayout") 4006 4007 self.font = kargs.pop("font", "") 4008 4009 s = kargs.pop("layout", "2d") 4010 if isinstance(s, int): 4011 ss = ["2d", "fast2d", "clustering2d", "circular", "circular3d", "cone", "force", "tree"] 4012 s = ss[s] 4013 self.layout = s 4014 4015 if "2d" in s: 4016 if "clustering" in s: 4017 self.strategy = vtki.new("Clustering2DLayoutStrategy") 4018 elif "fast" in s: 4019 self.strategy = vtki.new("Fast2DLayoutStrategy") 4020 else: 4021 self.strategy = vtki.new("Simple2DLayoutStrategy") 4022 self.rotX = 180 4023 opt = kargs.pop("rest_distance", None) 4024 if opt is not None: 4025 self.strategy.SetRestDistance(opt) 4026 opt = kargs.pop("seed", None) 4027 if opt is not None: 4028 self.strategy.SetRandomSeed(opt) 4029 opt = kargs.pop("nmax", None) 4030 if opt is not None: 4031 self.strategy.SetMaxNumberOfIterations(opt) 4032 self.zrange = kargs.pop("zrange", 0) 4033 4034 elif "circ" in s: 4035 if "3d" in s: 4036 self.strategy = vtki.new("Simple3DCirclesStrategy") 4037 self.strategy.SetDirection(0, 0, -1) 4038 self.strategy.SetAutoHeight(True) 4039 self.strategy.SetMethod(1) 4040 self.rotX = -90 4041 opt = kargs.pop("radius", None) # float 4042 if opt is not None: 4043 self.strategy.SetMethod(0) 4044 self.strategy.SetRadius(opt) # float 4045 opt = kargs.pop("height", None) 4046 if opt is not None: 4047 self.strategy.SetAutoHeight(False) 4048 self.strategy.SetHeight(opt) # float 4049 else: 4050 self.strategy = vtki.new("CircularLayoutStrategy") 4051 self.zrange = kargs.pop("zrange", 0) 4052 4053 elif "cone" in s: 4054 self.strategy = vtki.new("ConeLayoutStrategy") 4055 self.rotX = 180 4056 opt = kargs.pop("compactness", None) 4057 if opt is not None: 4058 self.strategy.SetCompactness(opt) 4059 opt = kargs.pop("compression", None) 4060 if opt is not None: 4061 self.strategy.SetCompression(opt) 4062 opt = kargs.pop("spacing", None) 4063 if opt is not None: 4064 self.strategy.SetSpacing(opt) 4065 4066 elif "force" in s: 4067 self.strategy = vtki.new("ForceDirectedLayoutStrategy") 4068 opt = kargs.pop("seed", None) 4069 if opt is not None: 4070 self.strategy.SetRandomSeed(opt) 4071 opt = kargs.pop("bounds", None) 4072 if opt is not None: 4073 self.strategy.SetAutomaticBoundsComputation(False) 4074 self.strategy.SetGraphBounds(opt) # list 4075 opt = kargs.pop("nmax", None) 4076 if opt is not None: 4077 self.strategy.SetMaxNumberOfIterations(opt) # int 4078 opt = kargs.pop("three_dimensional", True) 4079 if opt is not None: 4080 self.strategy.SetThreeDimensionalLayout(opt) # bool 4081 opt = kargs.pop("random_initial_points", None) 4082 if opt is not None: 4083 self.strategy.SetRandomInitialPoints(opt) # bool 4084 4085 elif "tree" in s: 4086 self.strategy = vtki.new("SpanTreeLayoutStrategy") 4087 self.rotX = 180 4088 4089 else: 4090 vedo.logger.error(f"Cannot understand layout {s}. Available layouts:") 4091 vedo.logger.error("[2d,fast2d,clustering2d,circular,circular3d,cone,force,tree]") 4092 raise RuntimeError() 4093 4094 self.gl.SetLayoutStrategy(self.strategy) 4095 4096 if len(kargs) > 0: 4097 vedo.logger.error(f"Cannot understand options: {kargs}") 4098 4099 def add_node(self, label="id") -> int: 4100 """Add a new node to the `Graph`.""" 4101 v = self.mdg.AddVertex() # vtk calls it vertex.. 4102 self.nodes.append(v) 4103 if label == "id": 4104 label = int(v) 4105 self._node_labels.append(str(label)) 4106 return v 4107 4108 def add_edge(self, v1, v2, label="") -> int: 4109 """Add a new edge between to nodes. 4110 An extra node is created automatically if needed.""" 4111 nv = len(self.nodes) 4112 if v1 >= nv: 4113 for _ in range(nv, v1 + 1): 4114 self.add_node() 4115 nv = len(self.nodes) 4116 if v2 >= nv: 4117 for _ in range(nv, v2 + 1): 4118 self.add_node() 4119 e = self.mdg.AddEdge(v1, v2) 4120 self.edges.append(e) 4121 self._edge_labels.append(str(label)) 4122 return e 4123 4124 def add_child(self, v, node_label="id", edge_label="") -> int: 4125 """Add a new edge to a new node as its child. 4126 The extra node is created automatically if needed.""" 4127 nv = len(self.nodes) 4128 if v >= nv: 4129 for _ in range(nv, v + 1): 4130 self.add_node() 4131 child = self.mdg.AddChild(v) 4132 self.edges.append((v, child)) 4133 self.nodes.append(child) 4134 if node_label == "id": 4135 node_label = int(child) 4136 self._node_labels.append(str(node_label)) 4137 self._edge_labels.append(str(edge_label)) 4138 return child 4139 4140 def build(self): 4141 """ 4142 Build the `DirectedGraph(Assembly)`. 4143 Accessory objects are also created for labels and arrows. 4144 """ 4145 self.gl.SetZRange(self.zrange) 4146 self.gl.SetInputData(self.mdg) 4147 self.gl.Update() 4148 4149 gr2poly = vtki.new("GraphToPolyData") 4150 gr2poly.EdgeGlyphOutputOn() 4151 gr2poly.SetEdgeGlyphPosition(self.edge_glyph_position) 4152 gr2poly.SetInputData(self.gl.GetOutput()) 4153 gr2poly.Update() 4154 4155 dgraph = Mesh(gr2poly.GetOutput(0)) 4156 # dgraph.clean() # WRONG!!! dont uncomment 4157 dgraph.flat().color(self._c).lw(2) 4158 dgraph.name = "DirectedGraph" 4159 4160 diagsz = self.diagonal_size() / 1.42 4161 if not diagsz: 4162 return None 4163 4164 dgraph.scale(1 / diagsz) 4165 if self.rotX: 4166 dgraph.rotate_x(self.rotX) 4167 if self.rotY: 4168 dgraph.rotate_y(self.rotY) 4169 if self.rotZ: 4170 dgraph.rotate_z(self.rotZ) 4171 4172 vecs = gr2poly.GetOutput(1).GetPointData().GetVectors() 4173 self.edge_orientations = utils.vtk2numpy(vecs) 4174 4175 # Use Glyph3D to repeat the glyph on all edges. 4176 arrows = None 4177 if self.arrow_scale: 4178 arrow_source = vtki.new("GlyphSource2D") 4179 arrow_source.SetGlyphTypeToEdgeArrow() 4180 arrow_source.SetScale(self.arrow_scale) 4181 arrow_source.Update() 4182 arrow_glyph = vtki.vtkGlyph3D() 4183 arrow_glyph.SetInputData(0, gr2poly.GetOutput(1)) 4184 arrow_glyph.SetInputData(1, arrow_source.GetOutput()) 4185 arrow_glyph.Update() 4186 arrows = Mesh(arrow_glyph.GetOutput()) 4187 arrows.scale(1 / diagsz) 4188 arrows.lighting("off").color(self._c) 4189 if self.rotX: 4190 arrows.rotate_x(self.rotX) 4191 if self.rotY: 4192 arrows.rotate_y(self.rotY) 4193 if self.rotZ: 4194 arrows.rotate_z(self.rotZ) 4195 arrows.name = "DirectedGraphArrows" 4196 4197 node_labels = None 4198 if self._node_labels: 4199 node_labels = dgraph.labels( 4200 self._node_labels, 4201 scale=self.node_label_scale, 4202 precision=0, 4203 font=self.font, 4204 justify=self.node_label_justify, 4205 ) 4206 node_labels.color(self._c).pickable(True) 4207 node_labels.name = "DirectedGraphNodeLabels" 4208 4209 edge_labels = None 4210 if self._edge_labels: 4211 edge_labels = dgraph.labels( 4212 self._edge_labels, on="cells", scale=self.edge_label_scale, precision=0, font=self.font 4213 ) 4214 edge_labels.color(self._c).pickable(True) 4215 edge_labels.name = "DirectedGraphEdgeLabels" 4216 4217 super().__init__([dgraph, node_labels, edge_labels, arrows]) 4218 self.name = "DirectedGraphAssembly" 4219 return self
60class Figure(Assembly): 61 """Format class for figures.""" 62 63 def __init__(self, xlim, ylim, aspect=4 / 3, padding=(0.05, 0.05, 0.05, 0.05), **kwargs): 64 """ 65 Create an empty formatted figure for plotting. 66 67 Arguments: 68 xlim : (list) 69 range of the x-axis as [x0, x1] 70 ylim : (list) 71 range of the y-axis as [y0, y1] 72 aspect : (float, str) 73 the desired aspect ratio of the histogram. Default is 4/3. 74 Use `aspect="equal"` to force the same units in x and y. 75 padding : (float, list) 76 keep a padding space from the axes (as a fraction of the axis size). 77 This can be a list of four numbers. 78 xtitle : (str) 79 title for the x-axis, can also be set using `axes=dict(xtitle="my x axis")` 80 ytitle : (str) 81 title for the y-axis, can also be set using `axes=dict(ytitle="my y axis")` 82 grid : (bool) 83 show the background grid for the axes, can also be set using `axes=dict(xygrid=True)` 84 axes : (dict) 85 an extra dictionary of options for the `vedo.addons.Axes` object 86 """ 87 88 self.verbose = True # printing to stdout on every mouse click 89 90 self.xlim = np.asarray(xlim) 91 self.ylim = np.asarray(ylim) 92 self.aspect = aspect 93 self.padding = padding 94 if not utils.is_sequence(self.padding): 95 self.padding = [self.padding, self.padding, self.padding, self.padding] 96 97 self.force_scaling_types = ( 98 shapes.Glyph, 99 shapes.Line, 100 shapes.Rectangle, 101 shapes.DashedLine, 102 shapes.Tube, 103 shapes.Ribbon, 104 shapes.GeoCircle, 105 shapes.Arc, 106 shapes.Grid, 107 # shapes.Arrows, # todo 108 # shapes.Arrows2D, # todo 109 shapes.Brace, # todo 110 ) 111 112 options = dict(kwargs) 113 114 self.title = options.pop("title", "") 115 self.xtitle = options.pop("xtitle", " ") 116 self.ytitle = options.pop("ytitle", " ") 117 number_of_divisions = 6 118 119 self.legend = None 120 self.labels = [] 121 self.label = options.pop("label", None) 122 if self.label: 123 self.labels = [self.label] 124 125 self.axopts = options.pop("axes", {}) 126 if isinstance(self.axopts, (bool, int, float)): 127 if self.axopts: 128 self.axopts = {} 129 if self.axopts or isinstance(self.axopts, dict): 130 number_of_divisions = self.axopts.pop("number_of_divisions", number_of_divisions) 131 132 self.axopts["xtitle"] = self.xtitle 133 self.axopts["ytitle"] = self.ytitle 134 135 if "xygrid" not in self.axopts: ## modify the default 136 self.axopts["xygrid"] = options.pop("grid", False) 137 138 if "xygrid_transparent" not in self.axopts: ## modify the default 139 self.axopts["xygrid_transparent"] = True 140 141 if "xtitle_position" not in self.axopts: ## modify the default 142 self.axopts["xtitle_position"] = 0.5 143 self.axopts["xtitle_justify"] = "top-center" 144 145 if "ytitle_position" not in self.axopts: ## modify the default 146 self.axopts["ytitle_position"] = 0.5 147 self.axopts["ytitle_justify"] = "bottom-center" 148 149 if self.label: 150 if "c" in self.axopts: 151 self.label.tcolor = self.axopts["c"] 152 153 x0, x1 = self.xlim 154 y0, y1 = self.ylim 155 dx = x1 - x0 156 dy = y1 - y0 157 x0lim, x1lim = (x0 - self.padding[0] * dx, x1 + self.padding[1] * dx) 158 y0lim, y1lim = (y0 - self.padding[2] * dy, y1 + self.padding[3] * dy) 159 dy = y1lim - y0lim 160 161 self.axes = None 162 if xlim[0] >= xlim[1] or ylim[0] >= ylim[1]: 163 vedo.logger.warning(f"Null range for Figure {self.title}... returning an empty Assembly.") 164 super().__init__() 165 self.yscale = 0 166 return 167 168 if aspect == "equal": 169 self.aspect = dx / dy # so that yscale becomes 1 170 171 self.yscale = dx / dy / self.aspect 172 173 y0lim *= self.yscale 174 y1lim *= self