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