1#----------------------------------------------------------------------------- 2# Copyright (c) 2012 - 2021, Anaconda, Inc., and Bokeh Contributors. 3# All rights reserved. 4# 5# The full license is in the file LICENSE.txt, distributed with this software. 6#----------------------------------------------------------------------------- 7''' Models for representing top-level plot objects. 8 9''' 10 11#----------------------------------------------------------------------------- 12# Boilerplate 13#----------------------------------------------------------------------------- 14import logging # isort:skip 15log = logging.getLogger(__name__) 16 17#----------------------------------------------------------------------------- 18# Imports 19#----------------------------------------------------------------------------- 20 21# Standard library imports 22import warnings 23 24# Bokeh imports 25from ..core.enums import Location, OutputBackend, ResetPolicy 26from ..core.properties import ( 27 Alias, 28 Bool, 29 Dict, 30 Either, 31 Enum, 32 Float, 33 Include, 34 Instance, 35 Int, 36 List, 37 Null, 38 Nullable, 39 Override, 40 Readonly, 41 String, 42) 43from ..core.property_mixins import ScalarFillProps, ScalarLineProps 44from ..core.query import find 45from ..core.validation import error, warning 46from ..core.validation.errors import ( 47 BAD_EXTRA_RANGE_NAME, 48 INCOMPATIBLE_SCALE_AND_RANGE, 49 REQUIRED_RANGE, 50 REQUIRED_SCALE, 51) 52from ..core.validation.warnings import ( 53 FIXED_HEIGHT_POLICY, 54 FIXED_SIZING_MODE, 55 FIXED_WIDTH_POLICY, 56 MISSING_RENDERERS, 57) 58from ..model import Model 59from ..util.string import nice_join 60from .annotations import Annotation, Legend, Title 61from .axes import Axis 62from .glyphs import Glyph 63from .grids import Grid 64from .layouts import LayoutDOM 65from .ranges import DataRange1d, FactorRange, Range, Range1d 66from .renderers import GlyphRenderer, Renderer, TileRenderer 67from .scales import CategoricalScale, LinearScale, LogScale, Scale 68from .sources import ColumnDataSource, DataSource 69from .tools import HoverTool, Tool, Toolbar 70 71#----------------------------------------------------------------------------- 72# Globals and constants 73#----------------------------------------------------------------------------- 74 75__all__ = ( 76 'Plot', 77) 78 79_VALID_PLACES = ('left', 'right', 'above', 'below', 'center') 80 81#----------------------------------------------------------------------------- 82# General API 83#----------------------------------------------------------------------------- 84 85class Plot(LayoutDOM): 86 ''' Model representing a plot, containing glyphs, guides, annotations. 87 88 ''' 89 90 def select(self, *args, **kwargs): 91 ''' Query this object and all of its references for objects that 92 match the given selector. 93 94 There are a few different ways to call the ``select`` method. 95 The most general is to supply a JSON-like query dictionary as the 96 single argument or as keyword arguments: 97 98 Args: 99 selector (JSON-like) : some sample text 100 101 Keyword Arguments: 102 kwargs : query dict key/values as keyword arguments 103 104 Additionally, for compatibility with ``Model.select``, a selector 105 dict may be passed as ``selector`` keyword argument, in which case 106 the value of ``kwargs['selector']`` is used for the query. 107 108 For convenience, queries on just names can be made by supplying 109 the ``name`` string as the single parameter: 110 111 Args: 112 name (str) : the name to query on 113 114 Also queries on just type can be made simply by supplying the 115 ``Model`` subclass as the single parameter: 116 117 Args: 118 type (Model) : the type to query on 119 120 Returns: 121 seq[Model] 122 123 Examples: 124 125 .. code-block:: python 126 127 # These three are equivalent 128 p.select(selector={"type": HoverTool}) 129 p.select({"type": HoverTool}) 130 p.select(HoverTool) 131 132 # These two are also equivalent 133 p.select({"name": "mycircle"}) 134 p.select("mycircle") 135 136 # Keyword arguments can be supplied in place of selector dict 137 p.select({"name": "foo", "type": HoverTool}) 138 p.select(name="foo", type=HoverTool) 139 140 ''' 141 142 selector = _select_helper(args, kwargs) 143 144 # Want to pass selector that is a dictionary 145 return _list_attr_splat(find(self.references(), selector, {'plot': self})) 146 147 def row(self, row, gridplot): 148 ''' Return whether this plot is in a given row of a GridPlot. 149 150 Args: 151 row (int) : index of the row to test 152 gridplot (GridPlot) : the GridPlot to check 153 154 Returns: 155 bool 156 157 ''' 158 return self in gridplot.row(row) 159 160 def column(self, col, gridplot): 161 ''' Return whether this plot is in a given column of a GridPlot. 162 163 Args: 164 col (int) : index of the column to test 165 gridplot (GridPlot) : the GridPlot to check 166 167 Returns: 168 bool 169 170 ''' 171 return self in gridplot.column(col) 172 173 def _axis(self, *sides): 174 objs = [] 175 for s in sides: 176 objs.extend(getattr(self, s, [])) 177 axis = [obj for obj in objs if isinstance(obj, Axis)] 178 return _list_attr_splat(axis) 179 180 @property 181 def xaxis(self): 182 ''' Splattable list of :class:`~bokeh.models.axes.Axis` objects for the x dimension. 183 184 ''' 185 return self._axis("above", "below") 186 187 @property 188 def yaxis(self): 189 ''' Splattable list of :class:`~bokeh.models.axes.Axis` objects for the y dimension. 190 191 ''' 192 return self._axis("left", "right") 193 194 @property 195 def axis(self): 196 ''' Splattable list of :class:`~bokeh.models.axes.Axis` objects. 197 198 ''' 199 return _list_attr_splat(self.xaxis + self.yaxis) 200 201 @property 202 def legend(self): 203 ''' Splattable list of :class:`~bokeh.models.annotations.Legend` objects. 204 205 ''' 206 panels = self.above + self.below + self.left + self.right + self.center 207 legends = [obj for obj in panels if isinstance(obj, Legend)] 208 return _legend_attr_splat(legends) 209 210 @property 211 def hover(self): 212 ''' Splattable list of :class:`~bokeh.models.tools.HoverTool` objects. 213 214 ''' 215 hovers = [obj for obj in self.tools if isinstance(obj, HoverTool)] 216 return _list_attr_splat(hovers) 217 218 def _grid(self, dimension): 219 grid = [obj for obj in self.center if isinstance(obj, Grid) and obj.dimension == dimension] 220 return _list_attr_splat(grid) 221 222 @property 223 def xgrid(self): 224 ''' Splattable list of :class:`~bokeh.models.grids.Grid` objects for the x dimension. 225 226 ''' 227 return self._grid(0) 228 229 @property 230 def ygrid(self): 231 ''' Splattable list of :class:`~bokeh.models.grids.Grid` objects for the y dimension. 232 233 ''' 234 return self._grid(1) 235 236 @property 237 def grid(self): 238 ''' Splattable list of :class:`~bokeh.models.grids.Grid` objects. 239 240 ''' 241 return _list_attr_splat(self.xgrid + self.ygrid) 242 243 @property 244 def tools(self): 245 return self.toolbar.tools 246 247 @tools.setter 248 def tools(self, tools): 249 self.toolbar.tools = tools 250 251 def add_layout(self, obj, place='center'): 252 ''' Adds an object to the plot in a specified place. 253 254 Args: 255 obj (Renderer) : the object to add to the Plot 256 place (str, optional) : where to add the object (default: 'center') 257 Valid places are: 'left', 'right', 'above', 'below', 'center'. 258 259 Returns: 260 None 261 262 ''' 263 if place not in _VALID_PLACES: 264 raise ValueError( 265 "Invalid place '%s' specified. Valid place values are: %s" % (place, nice_join(_VALID_PLACES)) 266 ) 267 268 getattr(self, place).append(obj) 269 270 def add_tools(self, *tools): 271 ''' Adds tools to the plot. 272 273 Args: 274 *tools (Tool) : the tools to add to the Plot 275 276 Returns: 277 None 278 279 ''' 280 for tool in tools: 281 if not isinstance(tool, Tool): 282 raise ValueError("All arguments to add_tool must be Tool subclasses.") 283 284 self.toolbar.tools.append(tool) 285 286 def add_glyph(self, source_or_glyph, glyph=None, **kw): 287 ''' Adds a glyph to the plot with associated data sources and ranges. 288 289 This function will take care of creating and configuring a Glyph object, 290 and then add it to the plot's list of renderers. 291 292 Args: 293 source (DataSource) : a data source for the glyphs to all use 294 glyph (Glyph) : the glyph to add to the Plot 295 296 297 Keyword Arguments: 298 Any additional keyword arguments are passed on as-is to the 299 Glyph initializer. 300 301 Returns: 302 GlyphRenderer 303 304 ''' 305 if glyph is not None: 306 source = source_or_glyph 307 else: 308 source, glyph = ColumnDataSource(), source_or_glyph 309 310 if not isinstance(source, DataSource): 311 raise ValueError("'source' argument to add_glyph() must be DataSource subclass") 312 313 if not isinstance(glyph, Glyph): 314 raise ValueError("'glyph' argument to add_glyph() must be Glyph subclass") 315 316 g = GlyphRenderer(data_source=source, glyph=glyph, **kw) 317 self.renderers.append(g) 318 return g 319 320 def add_tile(self, tile_source, **kw): 321 ''' Adds new ``TileRenderer`` into ``Plot.renderers`` 322 323 Args: 324 tile_source (TileSource) : a tile source instance which contain tileset configuration 325 326 Keyword Arguments: 327 Additional keyword arguments are passed on as-is to the tile renderer 328 329 Returns: 330 TileRenderer : TileRenderer 331 332 ''' 333 tile_renderer = TileRenderer(tile_source=tile_source, **kw) 334 self.renderers.append(tile_renderer) 335 return tile_renderer 336 337 @error(REQUIRED_RANGE) 338 def _check_required_range(self): 339 missing = [] 340 if not self.x_range: missing.append('x_range') 341 if not self.y_range: missing.append('y_range') 342 if missing: 343 return ", ".join(missing) + " [%s]" % self 344 345 @error(REQUIRED_SCALE) 346 def _check_required_scale(self): 347 missing = [] 348 if not self.x_scale: missing.append('x_scale') 349 if not self.y_scale: missing.append('y_scale') 350 if missing: 351 return ", ".join(missing) + " [%s]" % self 352 353 @error(INCOMPATIBLE_SCALE_AND_RANGE) 354 def _check_compatible_scale_and_ranges(self): 355 incompatible = [] 356 x_ranges = list(self.extra_x_ranges.values()) 357 if self.x_range: x_ranges.append(self.x_range) 358 y_ranges = list(self.extra_y_ranges.values()) 359 if self.y_range: y_ranges.append(self.y_range) 360 361 if self.x_scale is not None: 362 for rng in x_ranges: 363 if isinstance(rng, (DataRange1d, Range1d)) and not isinstance(self.x_scale, (LinearScale, LogScale)): 364 incompatible.append("incompatibility on x-dimension: %s, %s" %(rng, self.x_scale)) 365 elif isinstance(rng, FactorRange) and not isinstance(self.x_scale, CategoricalScale): 366 incompatible.append("incompatibility on x-dimension: %s/%s" %(rng, self.x_scale)) 367 # special case because CategoricalScale is a subclass of LinearScale, should be removed in future 368 if isinstance(rng, (DataRange1d, Range1d)) and isinstance(self.x_scale, CategoricalScale): 369 incompatible.append("incompatibility on x-dimension: %s, %s" %(rng, self.x_scale)) 370 371 if self.y_scale is not None: 372 for rng in y_ranges: 373 if isinstance(rng, (DataRange1d, Range1d)) and not isinstance(self.y_scale, (LinearScale, LogScale)): 374 incompatible.append("incompatibility on y-dimension: %s/%s" %(rng, self.y_scale)) 375 elif isinstance(rng, FactorRange) and not isinstance(self.y_scale, CategoricalScale): 376 incompatible.append("incompatibility on y-dimension: %s/%s" %(rng, self.y_scale)) 377 # special case because CategoricalScale is a subclass of LinearScale, should be removed in future 378 if isinstance(rng, (DataRange1d, Range1d)) and isinstance(self.y_scale, CategoricalScale): 379 incompatible.append("incompatibility on y-dimension: %s, %s" %(rng, self.y_scale)) 380 381 if incompatible: 382 return ", ".join(incompatible) + " [%s]" % self 383 384 @warning(MISSING_RENDERERS) 385 def _check_missing_renderers(self): 386 if len(self.renderers) == 0 and len([x for x in self.center if isinstance(x, Annotation)]) == 0: 387 return str(self) 388 389 @error(BAD_EXTRA_RANGE_NAME) 390 def _check_bad_extra_range_name(self): 391 msg = "" 392 valid = { 393 f'{axis}_name': {'default', *getattr(self, f"extra_{axis}s")} 394 for axis in ("x_range", "y_range") 395 } 396 for place in _VALID_PLACES + ('renderers',): 397 for ref in getattr(self, place): 398 bad = ', '.join( 399 f"{axis}='{getattr(ref, axis)}'" 400 for axis, keys in valid.items() 401 if getattr(ref, axis, 'default') not in keys 402 ) 403 if bad: 404 msg += (", " if msg else "") + f"{bad} [{ref}]" 405 if msg: 406 return msg 407 408 x_range = Instance(Range, default=lambda: DataRange1d(), help=""" 409 The (default) data range of the horizontal dimension of the plot. 410 """) 411 412 y_range = Instance(Range, default=lambda: DataRange1d(), help=""" 413 The (default) data range of the vertical dimension of the plot. 414 """) 415 416 @classmethod 417 def _scale(cls, scale): 418 if scale in ["auto", "linear"]: 419 return LinearScale() 420 elif scale == "log": 421 return LogScale() 422 if scale == "categorical": 423 return CategoricalScale() 424 else: 425 raise ValueError("Unknown mapper_type: %s" % scale) 426 427 x_scale = Instance(Scale, default=lambda: LinearScale(), help=""" 428 What kind of scale to use to convert x-coordinates in data space 429 into x-coordinates in screen space. 430 """) 431 432 y_scale = Instance(Scale, default=lambda: LinearScale(), help=""" 433 What kind of scale to use to convert y-coordinates in data space 434 into y-coordinates in screen space. 435 """) 436 437 extra_x_ranges = Dict(String, Instance(Range), help=""" 438 Additional named ranges to make available for mapping x-coordinates. 439 440 This is useful for adding additional axes. 441 """) 442 443 extra_y_ranges = Dict(String, Instance(Range), help=""" 444 Additional named ranges to make available for mapping y-coordinates. 445 446 This is useful for adding additional axes. 447 """) 448 449 hidpi = Bool(default=True, help=""" 450 Whether to use HiDPI mode when available. 451 """) 452 453 title = Either(Null, String, Instance(Title), default=lambda: Title(text=""), help=""" 454 A title for the plot. Can be a text string or a Title annotation. 455 """) 456 457 title_location = Nullable(Enum(Location), default="above", help=""" 458 Where the title will be located. Titles on the left or right side 459 will be rotated. 460 """) 461 462 outline_props = Include(ScalarLineProps, help=""" 463 The %s for the plot border outline. 464 """) 465 466 outline_line_color = Override(default="#e5e5e5") 467 468 renderers = List(Instance(Renderer), help=""" 469 A list of all renderers for this plot, including guides and annotations 470 in addition to glyphs. 471 472 This property can be manipulated by hand, but the ``add_glyph`` and 473 ``add_layout`` methods are recommended to help make sure all necessary 474 setup is performed. 475 """) 476 477 toolbar = Instance(Toolbar, default=lambda: Toolbar(), help=""" 478 The toolbar associated with this plot which holds all the tools. It is 479 automatically created with the plot if necessary. 480 """) 481 482 toolbar_location = Nullable(Enum(Location), default="right", help=""" 483 Where the toolbar will be located. If set to None, no toolbar 484 will be attached to the plot. 485 """) 486 487 toolbar_sticky = Bool(default=True, help=""" 488 Stick the toolbar to the edge of the plot. Default: True. If False, 489 the toolbar will be outside of the axes, titles etc. 490 """) 491 492 left = List(Instance(Renderer), help=""" 493 A list of renderers to occupy the area to the left of the plot. 494 """) 495 496 right = List(Instance(Renderer), help=""" 497 A list of renderers to occupy the area to the right of the plot. 498 """) 499 500 above = List(Instance(Renderer), help=""" 501 A list of renderers to occupy the area above of the plot. 502 """) 503 504 below = List(Instance(Renderer), help=""" 505 A list of renderers to occupy the area below of the plot. 506 """) 507 508 center = List(Instance(Renderer), help=""" 509 A list of renderers to occupy the center area (frame) of the plot. 510 """) 511 512 width = Override(default=600) 513 514 height = Override(default=600) 515 516 plot_width: int = Alias("width", help=""" 517 The outer width of a plot, including any axes, titles, border padding, etc. 518 """) 519 520 plot_height: int = Alias("height", help=""" 521 The outer height of a plot, including any axes, titles, border padding, etc. 522 """) 523 524 frame_width = Nullable(Int, help=""" 525 The width of a plot frame or the inner width of a plot, excluding any 526 axes, titles, border padding, etc. 527 """) 528 529 frame_height = Nullable(Int, help=""" 530 The height of a plot frame or the inner height of a plot, excluding any 531 axes, titles, border padding, etc. 532 """) 533 534 inner_width = Readonly(Int, help=""" 535 This is the exact width of the plotting canvas, i.e. the width of 536 the actual plot, without toolbars etc. Note this is computed in a 537 web browser, so this property will work only in backends capable of 538 bidirectional communication (server, notebook). 539 540 .. note:: 541 This is an experimental feature and the API may change in near future. 542 543 """) 544 545 inner_height = Readonly(Int, help=""" 546 This is the exact height of the plotting canvas, i.e. the height of 547 the actual plot, without toolbars etc. Note this is computed in a 548 web browser, so this property will work only in backends capable of 549 bidirectional communication (server, notebook). 550 551 .. note:: 552 This is an experimental feature and the API may change in near future. 553 554 """) 555 556 outer_width = Readonly(Int, help=""" 557 This is the exact width of the layout, i.e. the height of 558 the actual plot, with toolbars etc. Note this is computed in a 559 web browser, so this property will work only in backends capable of 560 bidirectional communication (server, notebook). 561 562 .. note:: 563 This is an experimental feature and the API may change in near future. 564 565 """) 566 567 outer_height = Readonly(Int, help=""" 568 This is the exact height of the layout, i.e. the height of 569 the actual plot, with toolbars etc. Note this is computed in a 570 web browser, so this property will work only in backends capable of 571 bidirectional communication (server, notebook). 572 573 .. note:: 574 This is an experimental feature and the API may change in near future. 575 576 """) 577 578 background_props = Include(ScalarFillProps, help=""" 579 The %s for the plot background style. 580 """) 581 582 background_fill_color = Override(default='#ffffff') 583 584 border_props = Include(ScalarFillProps, help=""" 585 The %s for the plot border style. 586 """) 587 588 border_fill_color = Override(default='#ffffff') 589 590 min_border_top = Nullable(Int, help=""" 591 Minimum size in pixels of the padding region above the top of the 592 central plot region. 593 594 .. note:: 595 This is a *minimum*. The padding region may expand as needed to 596 accommodate titles or axes, etc. 597 598 """) 599 600 min_border_bottom = Nullable(Int, help=""" 601 Minimum size in pixels of the padding region below the bottom of 602 the central plot region. 603 604 .. note:: 605 This is a *minimum*. The padding region may expand as needed to 606 accommodate titles or axes, etc. 607 608 """) 609 610 min_border_left = Nullable(Int, help=""" 611 Minimum size in pixels of the padding region to the left of 612 the central plot region. 613 614 .. note:: 615 This is a *minimum*. The padding region may expand as needed to 616 accommodate titles or axes, etc. 617 618 """) 619 620 min_border_right = Nullable(Int, help=""" 621 Minimum size in pixels of the padding region to the right of 622 the central plot region. 623 624 .. note:: 625 This is a *minimum*. The padding region may expand as needed to 626 accommodate titles or axes, etc. 627 628 """) 629 630 min_border = Nullable(Int, default=5, help=""" 631 A convenience property to set all all the ``min_border_X`` properties 632 to the same value. If an individual border property is explicitly set, 633 it will override ``min_border``. 634 """) 635 636 lod_factor = Int(10, help=""" 637 Decimation factor to use when applying level-of-detail decimation. 638 """) 639 640 lod_threshold = Nullable(Int, default=2000, help=""" 641 A number of data points, above which level-of-detail downsampling may 642 be performed by glyph renderers. Set to ``None`` to disable any 643 level-of-detail downsampling. 644 """) 645 646 lod_interval = Int(300, help=""" 647 Interval (in ms) during which an interactive tool event will enable 648 level-of-detail downsampling. 649 """) 650 651 lod_timeout = Int(500, help=""" 652 Timeout (in ms) for checking whether interactive tool events are still 653 occurring. Once level-of-detail mode is enabled, a check is made every 654 ``lod_timeout`` ms. If no interactive tool events have happened, 655 level-of-detail mode is disabled. 656 """) 657 658 output_backend = Enum(OutputBackend, default="canvas", help=""" 659 Specify the output backend for the plot area. Default is HTML5 Canvas. 660 661 .. note:: 662 When set to ``webgl``, glyphs without a WebGL rendering implementation 663 will fall back to rendering onto 2D canvas. 664 """) 665 666 match_aspect = Bool(default=False, help=""" 667 Specify the aspect ratio behavior of the plot. Aspect ratio is defined as 668 the ratio of width over height. This property controls whether Bokeh should 669 attempt the match the (width/height) of *data space* to the (width/height) 670 in pixels of *screen space*. 671 672 Default is ``False`` which indicates that the *data* aspect ratio and the 673 *screen* aspect ratio vary independently. ``True`` indicates that the plot 674 aspect ratio of the axes will match the aspect ratio of the pixel extent 675 the axes. The end result is that a 1x1 area in data space is a square in 676 pixels, and conversely that a 1x1 pixel is a square in data units. 677 678 .. note:: 679 This setting only takes effect when there are two dataranges. This 680 setting only sets the initial plot draw and subsequent resets. It is 681 possible for tools (single axis zoom, unconstrained box zoom) to 682 change the aspect ratio. 683 684 .. warning:: 685 This setting is incompatible with linking dataranges across multiple 686 plots. Doing so may result in undefined behaviour. 687 """) 688 689 aspect_scale = Float(default=1, help=""" 690 A value to be given for increased aspect ratio control. This value is added 691 multiplicatively to the calculated value required for ``match_aspect``. 692 ``aspect_scale`` is defined as the ratio of width over height of the figure. 693 694 For example, a plot with ``aspect_scale`` value of 2 will result in a 695 square in *data units* to be drawn on the screen as a rectangle with a 696 pixel width twice as long as its pixel height. 697 698 .. note:: 699 This setting only takes effect if ``match_aspect`` is set to ``True``. 700 """) 701 702 reset_policy = Enum(ResetPolicy, default="standard", help=""" 703 How a plot should respond to being reset. By deafult, the standard actions 704 are to clear any tool state history, return plot ranges to their original 705 values, undo all selections, and emit a ``Reset`` event. If customization 706 is desired, this property may be set to ``"event_only"``, which will 707 suppress all of the actions except the Reset event. 708 """) 709 710 # XXX: override LayoutDOM's definitions because of plot_{width,height}. 711 @error(FIXED_SIZING_MODE) 712 def _check_fixed_sizing_mode(self): 713 pass 714 715 @error(FIXED_WIDTH_POLICY) 716 def _check_fixed_width_policy(self): 717 pass 718 719 @error(FIXED_HEIGHT_POLICY) 720 def _check_fixed_height_policy(self): 721 pass 722 723#----------------------------------------------------------------------------- 724# Dev API 725#----------------------------------------------------------------------------- 726 727#----------------------------------------------------------------------------- 728# Private API 729#----------------------------------------------------------------------------- 730 731def _check_conflicting_kwargs(a1, a2, kwargs): 732 if a1 in kwargs and a2 in kwargs: 733 raise ValueError("Conflicting properties set on plot: %r and %r" % (a1, a2)) 734 735class _list_attr_splat(list): 736 def __setattr__(self, attr, value): 737 for x in self: 738 setattr(x, attr, value) 739 def __getattribute__(self, attr): 740 if attr in dir(list): 741 return list.__getattribute__(self, attr) 742 if len(self) == 0: 743 raise AttributeError("Trying to access %r attribute on an empty 'splattable' list" % attr) 744 if len(self) == 1: 745 return getattr(self[0], attr) 746 try: 747 return _list_attr_splat([getattr(x, attr) for x in self]) 748 except Exception: 749 raise AttributeError("Trying to access %r attribute on a 'splattable' list, but list items have no %r attribute" % (attr, attr)) 750 751 def __dir__(self): 752 if len({type(x) for x in self}) == 1: 753 return dir(self[0]) 754 else: 755 return dir(self) 756 757_LEGEND_EMPTY_WARNING = """ 758You are attempting to set `plot.legend.%s` on a plot that has zero legends added, this will have no effect. 759 760Before legend properties can be set, you must add a Legend explicitly, or call a glyph method with a legend parameter set. 761""" 762 763class _legend_attr_splat(_list_attr_splat): 764 def __setattr__(self, attr, value): 765 if not len(self): 766 warnings.warn(_LEGEND_EMPTY_WARNING % attr) 767 return super().__setattr__(attr, value) 768 769def _select_helper(args, kwargs): 770 """ Allow flexible selector syntax. 771 772 Returns: 773 dict 774 775 """ 776 if len(args) > 1: 777 raise TypeError("select accepts at most ONE positional argument.") 778 779 if len(args) > 0 and len(kwargs) > 0: 780 raise TypeError("select accepts EITHER a positional argument, OR keyword arguments (not both).") 781 782 if len(args) == 0 and len(kwargs) == 0: 783 raise TypeError("select requires EITHER a positional argument, OR keyword arguments.") 784 785 if args: 786 arg = args[0] 787 if isinstance(arg, dict): 788 selector = arg 789 elif isinstance(arg, str): 790 selector = dict(name=arg) 791 elif isinstance(arg, type) and issubclass(arg, Model): 792 selector = {"type": arg} 793 else: 794 raise TypeError("selector must be a dictionary, string or plot object.") 795 796 elif 'selector' in kwargs: 797 if len(kwargs) == 1: 798 selector = kwargs['selector'] 799 else: 800 raise TypeError("when passing 'selector' keyword arg, not other keyword args may be present") 801 802 else: 803 selector = kwargs 804 805 return selector 806 807#----------------------------------------------------------------------------- 808# Code 809#----------------------------------------------------------------------------- 810