Skip to content

Commit

Permalink
Add text layer (#222)
Browse files Browse the repository at this point in the history
* alphabetize

* Add columnlayer and textlayer to js models

* Add text layer to python side

* text layer updates

* bump deck version
  • Loading branch information
kylebarron authored Nov 8, 2023
1 parent 011533a commit 040b7cf
Show file tree
Hide file tree
Showing 6 changed files with 1,172 additions and 179 deletions.
3 changes: 2 additions & 1 deletion lonboard/_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def serialize_float_accessor(data: Union[int, float, NDArray[np.floating]], obj)
if data is None:
return None

if isinstance(data, (int, float)):
if isinstance(data, (str, int, float)):
return data

assert isinstance(data, (pa.ChunkedArray, pa.Array))
Expand All @@ -75,5 +75,6 @@ def infer_rows_per_chunk(table: pa.Table) -> int:


COLOR_SERIALIZATION = {"to_json": serialize_color_accessor}
# TODO: rename as it's used for text as well
FLOAT_SERIALIZATION = {"to_json": serialize_float_accessor}
TABLE_SERIALIZATION = {"to_json": serialize_table}
213 changes: 212 additions & 1 deletion lonboard/experimental/_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@
import pyarrow as pa
import traitlets

from lonboard._constants import EXTENSION_NAME
from lonboard._layer import BaseLayer
from lonboard.experimental.traits import PointAccessor
from lonboard.traits import ColorAccessor, FloatAccessor, PyarrowTableTrait
from lonboard.traits import (
ColorAccessor,
FloatAccessor,
PyarrowTableTrait,
TextAccessor,
)


class ArcLayer(BaseLayer):
Expand Down Expand Up @@ -70,8 +76,12 @@ class ArcLayer(BaseLayer):
"""

get_source_color = ColorAccessor()
"""Source color of each object
"""

get_target_color = ColorAccessor()
"""Target color of each object
"""

get_width = FloatAccessor()
"""The line width of each object, in units specified by `widthUnits`.
Expand All @@ -84,6 +94,8 @@ class ArcLayer(BaseLayer):
"""

get_height = FloatAccessor()
"""Height color of each object
"""

get_tilt = FloatAccessor()
"""
Expand Down Expand Up @@ -112,3 +124,202 @@ def _validate_accessor_length(self, proposal):
raise traitlets.TraitError("accessor must have same length as table")

return proposal["value"]


class TextLayer(BaseLayer):
"""Render text labels at given coordinates."""

_layer_type = traitlets.Unicode("text").tag(sync=True)

table = PyarrowTableTrait(allowed_geometry_types={EXTENSION_NAME.POINT})

billboard = traitlets.Bool().tag(sync=True)
"""If `true`, the text always faces camera. Otherwise the text faces up (z).
- Type: `bool`
- Default: `True`
"""

size_scale = traitlets.Any().tag(sync=True)
"""Text size multiplier.
- Type: `float`.
- Default: `1`
"""

size_units = traitlets.Any().tag(sync=True)
"""The units of the size, one of `'meters'`, `'common'`, and `'pixels'`.
default 'pixels'. See [unit
system](https://deck.gl/docs/developer-guide/coordinate-systems#supported-units).
- Type: `str`, optional
- Default: `'pixels'`
"""

size_min_pixels = traitlets.Any().tag(sync=True)
"""
The minimum size in pixels. When using non-pixel `sizeUnits`, this prop can be used
to prevent the icon from getting too small when zoomed out.
- Type: `float`, optional
- Default: `0`
"""

size_max_pixels = traitlets.Any().tag(sync=True)
"""
The maximum size in pixels. When using non-pixel `sizeUnits`, this prop can be used
to prevent the icon from getting too big when zoomed in.
- Type: `float`, optional
- Default: `None`
"""

# background = traitlets.Bool().tag(sync=True)
# """Whether to render background for the text blocks.

# - Type: `bool`
# - Default: `False`
# """

get_background_color = ColorAccessor()
"""Background color accessor.
default [255, 255, 255, 255]
"""

get_border_color = ColorAccessor()
"""Border color accessor.
default [0, 0, 0, 255]
"""

get_border_width = FloatAccessor()
"""Border width accessor.
default 0
"""

background_padding = traitlets.Any().tag(sync=True)
"""The padding of the background.
- If an array of 2 is supplied, it is interpreted as `[padding_x, padding_y]` in
pixels.
- If an array of 4 is supplied, it is interpreted as `[padding_left, padding_top,
padding_right, padding_bottom]` in pixels.
default [0, 0, 0, 0]
"""

character_set = traitlets.Any().tag(sync=True)
"""
Specifies a list of characters to include in the font. If set to 'auto', will be
automatically generated from the data set.
default (ASCII characters 32-128)
"""

font_family = traitlets.Any().tag(sync=True)
"""CSS font family
default 'Monaco, monospace'
"""

font_weight = traitlets.Any().tag(sync=True)
"""CSS font weight
default 'normal'
"""

line_height = traitlets.Any().tag(sync=True)
"""
A unitless number that will be multiplied with the current text size to set the line
height.
"""

outline_width = traitlets.Any().tag(sync=True)
"""
Width of outline around the text, relative to the text size. Only effective if
`fontSettings.sdf` is `true`.
default 0
"""

outline_color = traitlets.Any().tag(sync=True)
"""
Color of outline around the text, in `[r, g, b, [a]]`. Each channel is a number
between 0-255 and `a` is 255 if not supplied.
default [0, 0, 0, 255]
"""

font_settings = traitlets.Any().tag(sync=True)
"""
Advance options for fine tuning the appearance and performance of the generated
shared `fontAtlas`.
"""

word_break = traitlets.Any().tag(sync=True)
"""
Available options are `break-all` and `break-word`. A valid `maxWidth` has to be
provided to use `wordBreak`.
default 'break-word'
"""

max_width = traitlets.Any().tag(sync=True)
"""
A unitless number that will be multiplied with the current text size to set the
width limit of a string.
If specified, when the text is longer than the width limit, it will be wrapped into
multiple lines using the strategy of `wordBreak`.
default -1
"""

get_text = TextAccessor()
"""Label text accessor"""

# get_position = traitlets.Any().tag(sync=True)
# """Anchor position accessor"""

# ?: Accessor<DataT, Position>;

get_color = ColorAccessor()
"""Label color accessor
default [0, 0, 0, 255]
"""

get_size = FloatAccessor()
"""Label size accessor
default 32
"""

get_angle = FloatAccessor()
"""Label rotation accessor, in degrees
default 0
"""

get_text_anchor = traitlets.Any().tag(sync=True)
"""Horizontal alignment accessor
default 'middle'
"""
# ?: Accessor<DataT, 'start' | 'middle' | 'end'>;

get_alignment_baseline = traitlets.Any().tag(sync=True)
"""Vertical alignment accessor
default 'center'
"""
# ?: Accessor<DataT, 'top' | 'center' | 'bottom'>;

get_pixel_offset = traitlets.Any().tag(sync=True)
"""Label offset from the anchor position, [x, y] in pixels
default [0, 0]
"""
# ?: Accessor<DataT, [number, number]>;
58 changes: 58 additions & 0 deletions lonboard/traits.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,3 +336,61 @@ def validate(self, obj, value) -> Union[float, pa.ChunkedArray, pa.DoubleArray]:

self.error(obj, value)
assert False


class TextAccessor(FixedErrorTraitType):
"""A representation of a deck.gl text accessor.
Various input is allowed:
- A `str`. This will be used as the value for all objects.
- A numpy `ndarray` with a string data type Each value in the array will be used as
the value for the object at the same row index.
- A pandas `Series` with a string data type. Each value in the array will be used as
the value for the object at the same row index.
- A pyarrow [`StringArray`][pyarrow.StringArray] or
[`ChunkedArray`][pyarrow.ChunkedArray] containing a `StringArray`. Each value in
the array will be used as the value for the object at the same row index.
"""

default_value = ""
info_text = (
"a string value or numpy ndarray or pandas Series or pyarrow array representing"
" an array of strings"
)

def __init__(
self: TraitType,
*args,
**kwargs: Any,
) -> None:
super().__init__(*args, **kwargs)
self.tag(sync=True, **FLOAT_SERIALIZATION)

def validate(self, obj, value) -> Union[float, pa.ChunkedArray, pa.DoubleArray]:
if isinstance(value, str):
return value

# pandas Series
if (
value.__class__.__module__.startswith("pandas")
and value.__class__.__name__ == "Series"
):
# Cast pandas Series to pyarrow array
value = pa.array(value)

if isinstance(value, np.ndarray):
value = pa.StringArray.from_pandas(value)

if isinstance(value, (pa.ChunkedArray, pa.Array)):
if not pa.types.is_string(value.type):
self.error(
obj,
value,
info="String pyarrow array must be a string type.",
)

return value

self.error(obj, value)
assert False
Loading

0 comments on commit 040b7cf

Please sign in to comment.