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""" Provide the DataSpec properties and helpers. 8 9""" 10 11#----------------------------------------------------------------------------- 12# Boilerplate 13#----------------------------------------------------------------------------- 14import logging # isort:skip 15log = logging.getLogger(__name__) 16 17#----------------------------------------------------------------------------- 18# Imports 19#----------------------------------------------------------------------------- 20 21# Bokeh imports 22from ... import colors 23from ...util.serialization import convert_datetime_type, convert_timedelta_type 24from .. import enums 25from .color import Color 26from .container import Dict, List 27from .datetime import Datetime, TimeDelta 28from .descriptors import DataSpecPropertyDescriptor, UnitsSpecPropertyDescriptor 29from .either import Either 30from .enum import Enum 31from .instance import Instance 32from .nullable import Nullable 33from .primitive import Float, Int, Null, String 34from .singletons import Undefined 35from .visual import DashPattern, FontSize, HatchPatternType, MarkerType 36 37#----------------------------------------------------------------------------- 38# Globals and constants 39#----------------------------------------------------------------------------- 40 41__all__ = ( 42 'AlphaSpec', 43 'AngleSpec', 44 'ColorSpec', 45 'DashPatternSpec', 46 'DataSpec', 47 'DataDistanceSpec', 48 'DistanceSpec', 49 'expr', 50 'field', 51 'FontSizeSpec', 52 'FontStyleSpec', 53 'HatchPatternSpec', 54 'IntSpec', 55 'LineCapSpec', 56 'LineJoinSpec', 57 'MarkerSpec', 58 'NumberSpec', 59 'ScreenDistanceSpec', 60 'StringSpec', 61 'TextAlignSpec', 62 'TextBaselineSpec', 63 'UnitsSpec', 64 'value', 65) 66 67#----------------------------------------------------------------------------- 68# Private API 69#----------------------------------------------------------------------------- 70 71_ExprFieldValueTransform = Enum("expr", "field", "value", "transform") 72 73_ExprFieldValueTransformUnits = Enum("expr", "field", "value", "transform", "units") 74 75#----------------------------------------------------------------------------- 76# General API 77#----------------------------------------------------------------------------- 78 79class DataSpec(Either): 80 """ Base class for properties that accept either a fixed value, or a 81 string name that references a column in a 82 :class:`~bokeh.models.sources.ColumnDataSource`. 83 84 Many Bokeh models have properties that a user might want to set either 85 to a single fixed value, or to have the property take values from some 86 column in a data source. As a concrete example consider a glyph with 87 an ``x`` property for location. We might want to set all the glyphs 88 that get drawn to have the same location, say ``x=10``. It would be 89 convenient to just be able to write: 90 91 .. code-block:: python 92 93 glyph.x = 10 94 95 Alternatively, maybe the each glyph that gets drawn should have a 96 different location, according to the "pressure" column of a data 97 source. In this case we would like to be able to write: 98 99 .. code-block:: python 100 101 glyph.x = "pressure" 102 103 Bokeh ``DataSpec`` properties (and subclasses) afford this ease of 104 and consistency of expression. Ultimately, all ``DataSpec`` properties 105 resolve to dictionary values, with either a ``"value"`` key, or a 106 ``"field"`` key, depending on how it is set. 107 108 For instance: 109 110 .. code-block:: python 111 112 glyph.x = 10 # => { 'value': 10 } 113 114 glyph.x = "pressure" # => { 'field': 'pressure' } 115 116 When these underlying dictionary dictionary values are received in 117 the browser, BokehJS knows how to interpret them and take the correct, 118 expected action (i.e., draw the glyph at ``x=10``, or draw the glyph 119 with ``x`` coordinates from the "pressure" column). In this way, both 120 use-cases may be expressed easily in python, without having to handle 121 anything differently, from the user perspective. 122 123 It is worth noting that ``DataSpec`` properties can also be set directly 124 with properly formed dictionary values: 125 126 .. code-block:: python 127 128 glyph.x = { 'value': 10 } # same as glyph.x = 10 129 130 glyph.x = { 'field': 'pressure' } # same as glyph.x = "pressure" 131 132 Setting the property directly as a dict can be useful in certain 133 situations. For instance some ``DataSpec`` subclasses also add a 134 ``"units"`` key to the dictionary. This key is often set automatically, 135 but the dictionary format provides a direct mechanism to override as 136 necessary. Additionally, ``DataSpec`` can have a ``"transform"`` key, 137 that specifies a client-side transform that should be applied to any 138 fixed or field values before they are uses. As an example, you might want 139 to apply a ``Jitter`` transform to the ``x`` values: 140 141 .. code-block:: python 142 143 glyph.x = { 'value': 10, 'transform': Jitter(width=0.4) } 144 145 Note that ``DataSpec`` is not normally useful on its own. Typically, 146 a model will define properties using one of the subclasses such 147 as :class:`~bokeh.core.properties.NumberSpec` or 148 :class:`~bokeh.core.properties.ColorSpec`. For example, a Bokeh 149 model with ``x``, ``y`` and ``color`` properties that can handle 150 fixed values or columns automatically might look like: 151 152 .. code-block:: python 153 154 class SomeModel(Model): 155 156 x = NumberSpec(default=0, help="docs for x") 157 158 y = NumberSpec(default=0, help="docs for y") 159 160 color = ColorSpec(help="docs for color") # defaults to None 161 162 """ 163 def __init__(self, key_type, value_type, default, help=None): 164 super().__init__( 165 String, 166 Dict( 167 key_type, 168 Either( 169 String, 170 Instance('bokeh.models.transforms.Transform'), 171 Instance('bokeh.models.expressions.Expression'), 172 value_type)), 173 value_type, 174 default=default, 175 help=help 176 ) 177 self._type = self._validate_type_param(value_type) 178 179 # TODO (bev) add stricter validation on keys 180 181 def make_descriptors(self, base_name): 182 """ Return a list of ``DataSpecPropertyDescriptor`` instances to 183 install on a class, in order to delegate attribute access to this 184 property. 185 186 Args: 187 base_name (str) : the name of the property these descriptors are for 188 189 Returns: 190 list[DataSpecPropertyDescriptor] 191 192 The descriptors returned are collected by the ``MetaHasProps`` 193 metaclass and added to ``HasProps`` subclasses during class creation. 194 """ 195 return [ DataSpecPropertyDescriptor(base_name, self) ] 196 197 def to_serializable(self, obj, name, val): 198 # Check for spec type value 199 try: 200 self._type.validate(val, False) 201 return dict(value=val) 202 except ValueError: 203 pass 204 205 # Check for data source field name 206 if isinstance(val, str): 207 return dict(field=val) 208 209 # Must be dict, return a new dict 210 return dict(val) 211 212 def _sphinx_type(self): 213 return self._sphinx_prop_link() 214 215class IntSpec(DataSpec): 216 def __init__(self, default, help=None, key_type=_ExprFieldValueTransform): 217 super().__init__(key_type, Int, default=default, help=help) 218 219class NumberSpec(DataSpec): 220 """ A |DataSpec| property that accepts numeric and datetime fixed values. 221 222 By default, date and datetime values are immediately converted to 223 milliseconds since epoch. It is possible to disable processing of datetime 224 values by passing ``accept_datetime=False``. 225 226 By default, timedelta values are immediately converted to absolute 227 milliseconds. It is possible to disable processing of timedelta 228 values by passing ``accept_timedelta=False`` 229 230 Timedelta values are interpreted as absolute milliseconds. 231 232 .. code-block:: python 233 234 m.location = 10.3 # value 235 236 m.location = "foo" # field 237 238 """ 239 def __init__(self, default=Undefined, help=None, key_type=_ExprFieldValueTransform, accept_datetime=True, accept_timedelta=True): 240 super().__init__(key_type, Float, default=default, help=help) 241 if accept_timedelta: 242 self.accepts(TimeDelta, convert_timedelta_type) 243 if accept_datetime: 244 self.accepts(Datetime, convert_datetime_type) 245 246class AlphaSpec(NumberSpec): 247 248 _default_help = """\ 249 Acceptable values are numbers in 0..1 range (transparent..opaque). 250 """ 251 252 def __init__(self, default=1.0, help=None): 253 help = f"{help or ''}\n{self._default_help}" 254 super().__init__(default=default, help=help, key_type=_ExprFieldValueTransform, accept_datetime=False, accept_timedelta=False) 255 256class NullStringSpec(DataSpec): 257 def __init__(self, default=None, help=None, key_type=_ExprFieldValueTransform): 258 super().__init__(key_type, Nullable(List(String)), default=default, help=help) 259 260class StringSpec(DataSpec): 261 """ A |DataSpec| property that accepts string fixed values. 262 263 Because acceptable fixed values and field names are both strings, it can 264 be necessary explicitly to disambiguate these possibilities. By default, 265 string values are interpreted as fields, but the |value| function can be 266 used to specify that a string should interpreted as a value: 267 268 .. code-block:: python 269 270 m.title = value("foo") # value 271 272 m.title = "foo" # field 273 274 """ 275 def __init__(self, default, help=None, key_type=_ExprFieldValueTransform): 276 super().__init__(key_type, List(String), default=default, help=help) 277 278 def prepare_value(self, cls, name, value): 279 if isinstance(value, list): 280 if len(value) != 1: 281 raise TypeError("StringSpec convenience list values must have length 1") 282 value = dict(value=value[0]) 283 return super().prepare_value(cls, name, value) 284 285class FontSizeSpec(DataSpec): 286 """ A |DataSpec| property that accepts font-size fixed values. 287 288 The ``FontSizeSpec`` property attempts to first interpret string values as 289 font sizes (i.e. valid CSS length values). Otherwise string values are 290 interpreted as field names. For example: 291 292 .. code-block:: python 293 294 m.font_size = "13px" # value 295 296 m.font_size = "1.5em" # value 297 298 m.font_size = "foo" # field 299 300 A full list of all valid CSS length units can be found here: 301 302 https://drafts.csswg.org/css-values/#lengths 303 304 """ 305 306 def __init__(self, default, help=None, key_type=_ExprFieldValueTransform): 307 super().__init__(key_type, FontSize, default=default, help=help) 308 309 def validate(self, value, detail=True): 310 # We want to preserve existing semantics and be a little more restrictive. This 311 # validations makes m.font_size = "" or m.font_size = "6" an error 312 super().validate(value, detail) 313 314 if isinstance(value, str): 315 if len(value) == 0 or value[0].isdigit() and not FontSize._font_size_re.match(value): 316 msg = "" if not detail else f"{value!r} is not a valid font size value" 317 raise ValueError(msg) 318 319class FontStyleSpec(DataSpec): 320 def __init__(self, default, help=None, key_type=_ExprFieldValueTransform): 321 super().__init__(key_type, Enum(enums.FontStyle), default=default, help=help) 322 323class TextAlignSpec(DataSpec): 324 def __init__(self, default, help=None, key_type=_ExprFieldValueTransform): 325 super().__init__(key_type, Enum(enums.TextAlign), default=default, help=help) 326 327class TextBaselineSpec(DataSpec): 328 def __init__(self, default, help=None, key_type=_ExprFieldValueTransform): 329 super().__init__(key_type, Enum(enums.TextBaseline), default=default, help=help) 330 331class LineJoinSpec(DataSpec): 332 def __init__(self, default, help=None, key_type=_ExprFieldValueTransform): 333 super().__init__(key_type, Enum(enums.LineJoin), default=default, help=help) 334 335class LineCapSpec(DataSpec): 336 def __init__(self, default, help=None, key_type=_ExprFieldValueTransform): 337 super().__init__(key_type, Enum(enums.LineCap), default=default, help=help) 338 339class DashPatternSpec(DataSpec): 340 def __init__(self, default, help=None, key_type=_ExprFieldValueTransform): 341 super().__init__(key_type, DashPattern, default=default, help=help) 342 343class HatchPatternSpec(DataSpec): 344 """ A |DataSpec| property that accepts hatch pattern types as fixed values. 345 346 The ``HatchPatternSpec`` property attempts to first interpret string values 347 as hatch pattern types. Otherwise string values are interpreted as field 348 names. For example: 349 350 .. code-block:: python 351 352 m.font_size = "." # value 353 354 m.font_size = "ring" # value 355 356 m.font_size = "foo" # field 357 358 """ 359 360 def __init__(self, default, help=None, key_type=_ExprFieldValueTransform): 361 super().__init__(key_type, Nullable(HatchPatternType), default=default, help=help) 362 363class MarkerSpec(DataSpec): 364 """ A |DataSpec| property that accepts marker types as fixed values. 365 366 The ``MarkerSpec`` property attempts to first interpret string values as 367 marker types. Otherwise string values are interpreted as field names. 368 For example: 369 370 .. code-block:: python 371 372 m.font_size = "circle" # value 373 374 m.font_size = "square" # value 375 376 m.font_size = "foo" # field 377 378 """ 379 380 def __init__(self, default, help=None, key_type=_ExprFieldValueTransform): 381 super().__init__(key_type, MarkerType, default=default, help=help) 382 383 384class UnitsSpec(NumberSpec): 385 """ A |DataSpec| property that accepts numeric fixed values, and also 386 serializes associated units values. 387 388 """ 389 def __init__(self, default, units_type, units_default, help=None): 390 super().__init__(default=default, help=help, key_type=_ExprFieldValueTransformUnits) 391 self._units_type = self._validate_type_param(units_type) 392 393 # TODO (bev) units_type was already constructed, so this really should not be needed 394 self._units_type.validate(units_default) 395 self._units_type._default = units_default 396 self._units_type._serialized = False 397 398 def __str__(self): 399 units_default = self._units_type._default 400 return f"{self.__class__.__name__}(units_default={units_default!r})" 401 402 def get_units(self, obj, name): 403 raise NotImplementedError() 404 405 def make_descriptors(self, base_name): 406 """ Return a list of ``PropertyDescriptor`` instances to install on a 407 class, in order to delegate attribute access to this property. 408 409 Unlike simpler property types, ``UnitsSpec`` returns multiple 410 descriptors to install. In particular, descriptors for the base 411 property as well as the associated units property are returned. 412 413 Args: 414 name (str) : the name of the property these descriptors are for 415 416 Returns: 417 list[PropertyDescriptor] 418 419 The descriptors returned are collected by the ``MetaHasProps`` 420 metaclass and added to ``HasProps`` subclasses during class creation. 421 """ 422 units_props = self._units_type.make_descriptors("unused") 423 return [ UnitsSpecPropertyDescriptor(base_name, self, units_props[0]) ] 424 425 def to_serializable(self, obj, name, val): 426 d = super().to_serializable(obj, name, val) 427 if d is not None and 'units' not in d: 428 # d is a PropertyValueDict at this point, we need to convert it to 429 # a plain dict if we are going to modify its value, otherwise a 430 # notify_change that should not happen will be triggered 431 units = self.get_units(obj, name) 432 if units != self._units_type._default: 433 d = dict(**d, units=units) 434 return d 435 436class PropertyUnitsSpec(UnitsSpec): 437 """ A |DataSpec| property that accepts numeric fixed values, and also 438 provides an associated units property to store units information. 439 440 """ 441 def get_units(self, obj, name): 442 return getattr(obj, name+"_units") 443 444 def make_descriptors(self, base_name): 445 """ Return a list of ``PropertyDescriptor`` instances to install on a 446 class, in order to delegate attribute access to this property. 447 448 Unlike simpler property types, ``UnitsSpec`` returns multiple 449 descriptors to install. In particular, descriptors for the base 450 property as well as the associated units property are returned. 451 452 Args: 453 name (str) : the name of the property these descriptors are for 454 455 Returns: 456 list[PropertyDescriptor] 457 458 The descriptors returned are collected by the ``MetaHasProps`` 459 metaclass and added to ``HasProps`` subclasses during class creation. 460 """ 461 units_name = base_name + "_units" 462 units_props = self._units_type.make_descriptors(units_name) 463 return units_props + [ UnitsSpecPropertyDescriptor(base_name, self, units_props[0]) ] 464 465class AngleSpec(PropertyUnitsSpec): 466 """ A |DataSpec| property that accepts numeric fixed values, and also 467 provides an associated units property to store angle units. 468 469 Acceptable values for units are ``"deg"``, ``"rad"``, ``"grad"`` and ``"turn"``. 470 471 """ 472 def __init__(self, default=Undefined, units_default="rad", help=None): 473 super().__init__(default=default, units_type=Enum(enums.AngleUnits), units_default=units_default, help=help) 474 475class DistanceSpec(PropertyUnitsSpec): 476 """ A |DataSpec| property that accepts numeric fixed values or strings 477 that refer to columns in a :class:`~bokeh.models.sources.ColumnDataSource`, 478 and also provides an associated units property to store units information. 479 Acceptable values for units are ``"screen"`` and ``"data"``. 480 481 """ 482 def __init__(self, default=Undefined, units_default="data", help=None): 483 super().__init__(default=default, units_type=Enum(enums.SpatialUnits), units_default=units_default, help=help) 484 485 def prepare_value(self, cls, name, value): 486 try: 487 if value < 0: 488 raise ValueError("Distances must be positive!") 489 except TypeError: 490 pass 491 return super().prepare_value(cls, name, value) 492 493class NullDistanceSpec(DistanceSpec): 494 495 def __init__(self, default=None, units_default="data", help=None): 496 super().__init__(default=default, units_default=units_default, help=help) 497 self._type = Nullable(self._type) 498 self._type_params = [Null()] + self._type_params 499 500 def prepare_value(self, cls, name, value): 501 try: 502 if value is not None and value < 0: 503 raise ValueError("Distances must be positive or None!") 504 except TypeError: 505 pass 506 return super().prepare_value(cls, name, value) 507 508class _FixedUnitsDistanceSpec(UnitsSpec): 509 510 def __init__(self, default=Undefined, help=None): 511 super().__init__(default=default, units_type=Enum(enums.enumeration(self._units)), units_default=self._units, help=help) 512 513 def get_units(self, _obj, _name): 514 return self._units 515 516 def prepare_value(self, cls, name, value): 517 try: 518 if value is not None and value < 0: 519 raise ValueError("Distances must be positive or None!") 520 except TypeError: 521 pass 522 return super().prepare_value(cls, name, value) 523 524class ScreenDistanceSpec(_FixedUnitsDistanceSpec): 525 """ A |DataSpec| property that accepts numeric fixed values for screen-space 526 distances, and also provides an associated units property that reports 527 ``"screen"`` as the units. 528 529 """ 530 _units = "screen" 531 532class DataDistanceSpec(_FixedUnitsDistanceSpec): 533 """ A |DataSpec| property that accepts numeric fixed values for data-space 534 distances, and also provides an associated units property that reports 535 ``"data"`` as the units. 536 537 """ 538 _units = "data" 539 540class ColorSpec(DataSpec): 541 """ A |DataSpec| property that accepts |Color| fixed values. 542 543 The ``ColorSpec`` property attempts to first interpret string values as 544 colors. Otherwise, string values are interpreted as field names. For 545 example: 546 547 .. code-block:: python 548 549 m.color = "#a4225f" # value (hex color string) 550 551 m.color = "firebrick" # value (named CSS color string) 552 553 m.color = "foo" # field (named "foo") 554 555 This automatic interpretation can be override using the dict format 556 directly, or by using the |field| function: 557 558 .. code-block:: python 559 560 m.color = { "field": "firebrick" } # field (named "firebrick") 561 562 m.color = field("firebrick") # field (named "firebrick") 563 564 """ 565 566 _default_help = """\ 567 Acceptable values are: 568 569 - any of the named `CSS colors`_, e.g ``'green'``, ``'indigo'`` 570 - RGB(A) hex strings, e.g., ``'#FF0000'``, ``'#44444444'`` 571 - CSS4 color strings, e.g., ``'rgba(255, 0, 127, 0.6)'``, ``'rgb(0 127 0 / 1.0)'`` 572 - a 3-tuple of integers (r, g, b) between 0 and 255 573 - a 4-tuple of (r, g, b, a) where r, g, b are integers between 0..255 and a is between 0..1 574 - a 32-bit unsiged integers using the 0xRRGGBBAA byte order pattern 575 576 .. _CSS colors: https://www.w3.org/TR/css-color-4/#named-colors 577 578 """ 579 580 def __init__(self, default, help=None, key_type=_ExprFieldValueTransform): 581 help = f"{help or ''}\n{self._default_help}" 582 super().__init__(key_type, Nullable(Color), default=default, help=help) 583 584 @classmethod 585 def isconst(cls, val): 586 """ Whether the value is a string color literal. 587 588 Checks for a well-formed hexadecimal color value or a named color. 589 590 Args: 591 val (str) : the value to check 592 593 Returns: 594 True, if the value is a string color literal 595 596 """ 597 return isinstance(val, str) and \ 598 ((len(val) == 7 and val[0] == "#") or val in enums.NamedColor) 599 600 def to_serializable(self, obj, name, val): 601 if val is None: 602 return dict(value=None) 603 604 # Check for hexadecimal or named color 605 if self.isconst(val): 606 return dict(value=val) 607 608 # Check for RGB or RGBA tuple 609 if isinstance(val, tuple): 610 return dict(value=colors.RGB(*val).to_css()) 611 612 # Check for data source field name 613 if isinstance(val, colors.RGB): 614 return val.to_css() 615 616 # Check for data source field name or rgb(a) string 617 if isinstance(val, str): 618 if val.startswith(("rgb(", "rgba(")): 619 return val 620 621 return dict(field=val) 622 623 # Must be dict, return new dict 624 return dict(val) 625 626 def prepare_value(self, cls, name, value): 627 # Some explanation is in order. We want to accept tuples like 628 # (12.0, 100.0, 52.0) i.e. that have "float" byte values. The 629 # ColorSpec has a transform to adapt values like this to tuples 630 # of integers, but Property validation happens before the 631 # transform step, so values like that will fail Color validation 632 # at this point, since Color is very strict about only accepting 633 # tuples of (integer) bytes. This conditions tuple values to only 634 # have integer RGB components 635 if isinstance(value, tuple) and len(value) in (3, 4) and all(isinstance(v, (float, int)) for v in value): 636 value = tuple(int(v) if i < 3 else v for i, v in enumerate(value)) 637 return super().prepare_value(cls, name, value) 638 639# DataSpec helpers ------------------------------------------------------------ 640 641def expr(expression, transform=None): 642 """ Convenience function to explicitly return an "expr" specification for 643 a Bokeh :class:`~bokeh.core.properties.DataSpec` property. 644 645 Args: 646 expression (Expression) : a computed expression for a 647 ``DataSpec`` property. 648 649 transform (Transform, optional) : a transform to apply (default: None) 650 651 Returns: 652 dict : ``{ "expr": expression }`` 653 654 .. note:: 655 This function is included for completeness. String values for 656 property specifications are by default interpreted as field names. 657 658 """ 659 if transform: 660 return dict(expr=expression, transform=transform) 661 return dict(expr=expression) 662 663 664def field(name, transform=None): 665 """ Convenience function to explicitly return a "field" specification for 666 a Bokeh :class:`~bokeh.core.properties.DataSpec` property. 667 668 Args: 669 name (str) : name of a data source field to reference for a 670 ``DataSpec`` property. 671 672 transform (Transform, optional) : a transform to apply (default: None) 673 674 Returns: 675 dict : ``{ "field": name }`` 676 677 .. note:: 678 This function is included for completeness. String values for 679 property specifications are by default interpreted as field names. 680 681 """ 682 if transform: 683 return dict(field=name, transform=transform) 684 return dict(field=name) 685 686def value(val, transform=None): 687 """ Convenience function to explicitly return a "value" specification for 688 a Bokeh :class:`~bokeh.core.properties.DataSpec` property. 689 690 Args: 691 val (any) : a fixed value to specify for a ``DataSpec`` property. 692 693 transform (Transform, optional) : a transform to apply (default: None) 694 695 Returns: 696 dict : ``{ "value": name }`` 697 698 .. note:: 699 String values for property specifications are by default interpreted 700 as field names. This function is especially useful when you want to 701 specify a fixed value with text properties. 702 703 Example: 704 705 .. code-block:: python 706 707 # The following will take text values to render from a data source 708 # column "text_column", but use a fixed value "16px" for font size 709 p.text("x", "y", text="text_column", 710 text_font_size=value("16px"), source=source) 711 712 """ 713 if transform: 714 return dict(value=val, transform=transform) 715 return dict(value=val) 716 717#----------------------------------------------------------------------------- 718# Dev API 719#----------------------------------------------------------------------------- 720 721#----------------------------------------------------------------------------- 722# Code 723#----------------------------------------------------------------------------- 724