Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor text properties #43

Merged
merged 3 commits into from
Mar 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions tests/test_layers.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,9 +304,9 @@ def test_with_text(backend: str):
canvas = new_canvas(backend=backend)
x = np.arange(10)
y = np.sqrt(x)
canvas.add_line(x, y).with_text([f"{i}" for i in range(10)]).add_text_offset(0.1 ,0.1)
canvas.add_markers(x, y).with_text([f"{i}" for i in range(10)]).add_text_offset(0.1 ,0.1)
canvas.add_bars(x, y).with_text([f"{i}" for i in range(10)]).add_text_offset(0.1 ,0.1)
canvas.add_line(x, y).with_text([f"{i}" for i in range(10)]).with_text_offset(0.1 ,0.1)
canvas.add_markers(x, y).with_text([f"{i}" for i in range(10)]).with_text_offset(0.1 ,0.1)
canvas.add_bars(x, y).with_text([f"{i}" for i in range(10)]).with_text_offset(0.1 ,0.1)
canvas.add_line(x, y).with_text("x={x:.2f}, y={y:.2f}")
canvas.add_markers(x, y).with_text("x={x:.2f}, y={y:.2f}")
canvas.add_bars(x, y).with_text("x={x:.2f}, y={y:.2f}")
Expand Down
210 changes: 93 additions & 117 deletions whitecanvas/backend/bokeh/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@
from whitecanvas.utils.normalize import arr_color, as_color_array, hex_color
from whitecanvas.utils.type_check import is_real_number

# column names
TEXT = "text"
TEXT_SIZE = "text_font_size"
TEXT_COLOR = "text_color"
TEXT_ANGLE = "angle"
BG_COLOR = "background_fill_color"
BG_HATCH = "background_hatch_pattern"
BD_COLOR = "border_line_color"
BD_WIDTH = "border_line_width"
BD_STYLE = "border_line_dash"

INVISIBLE = "#00000000"


@check_protocol(TextProtocol)
class Texts(BokehLayer[bk_models.Text]):
Expand All @@ -27,33 +40,31 @@
data={
"x": x,
"y": y,
"text": text,
"text_font": ["Arial"] * ntexts,
"text_font_size": ["12pt"] * ntexts,
"text_align": ["left"] * ntexts,
"text_color": ["black"] * ntexts,
"angle": [0] * ntexts,
"background_fill_color": ["#00000000"] * ntexts,
"background_hatch_pattern": [""] * ntexts,
"border_line_color": ["#00000000"] * ntexts,
"border_line_width": [0] * ntexts,
"border_line_dash": ["solid"] * ntexts,
TEXT: text,
TEXT_SIZE: ["12pt"] * ntexts,
TEXT_COLOR: ["black"] * ntexts,
TEXT_ANGLE: [0] * ntexts,
BG_COLOR: [INVISIBLE] * ntexts,
BG_HATCH: [""] * ntexts,
BD_COLOR: [INVISIBLE] * ntexts,
BD_WIDTH: [0] * ntexts,
BD_STYLE: ["solid"] * ntexts,
}
)
self._model = bk_models.Text(
x="x",
y="y",
text="text",
text_font="text_font",
text_font_size="text_font_size",
text_color="text_color",
text_align="text_align",
angle="angle",
background_fill_color="background_fill_color",
background_hatch_pattern="background_hatch_pattern",
border_line_color="border_line_color",
border_line_width="border_line_width",
border_line_dash="border_line_dash",
text=TEXT,
text_font="helvetica",
text_font_size=TEXT_SIZE,
text_color=TEXT_COLOR,
text_align="left",
angle=TEXT_ANGLE,
background_fill_color=BG_COLOR,
background_hatch_pattern=BG_HATCH,
border_line_color=BD_COLOR,
border_line_width=BD_WIDTH,
border_line_dash=BD_STYLE,
)
self._visible = True

Expand All @@ -64,39 +75,39 @@
def _plt_set_visible(self, visible: bool):
self._visible = visible
if visible:
self._model.text_color = "#00000000"
self._model.background_fill_color = "#00000000"
self._model.border_line_color = "#00000000"
self._model.text_color = INVISIBLE
self._model.background_fill_color = INVISIBLE
self._model.border_line_color = INVISIBLE
else:
self._model.text_color = "text_color"
self._model.background_fill_color = "background_fill_color"
self._model.border_line_color = "border_line_color"
self._model.text_color = TEXT_COLOR
self._model.background_fill_color = BG_COLOR
self._model.border_line_color = BD_COLOR

##### TextProtocol #####

def _plt_get_text(self) -> list[str]:
return self._data.data["text"]
return self._data.data[TEXT]

def _plt_set_text(self, text: list[str]):
self._data.data["text"] = text
self._data.data[TEXT] = text

def _plt_get_text_color(self):
return np.stack([arr_color(c) for c in self._data.data["text_color"]])
return np.stack([arr_color(c) for c in self._data.data[TEXT_COLOR]])

def _plt_set_text_color(self, color):
color = as_color_array(color, len(self._data.data["text"]))
self._data.data["text_color"] = [hex_color(c) for c in color]
color = as_color_array(color, len(self._data.data[TEXT]))
self._data.data[TEXT_COLOR] = [hex_color(c) for c in color]

def _plt_get_text_size(self) -> float:
return np.array(
[float(s.rstrip("pt")) for s in self._data.data["text_font_size"]],
[float(s.rstrip("pt")) for s in self._data.data[TEXT_SIZE]],
dtype=np.float32,
)

def _plt_set_text_size(self, size: float):
if is_real_number(size):
size = np.full(len(self._data.data["text"]), size)
self._data.data["text_font_size"] = [f"{round(s, 1)}pt" for s in size]
size = np.full(len(self._data.data[TEXT]), size)
self._data.data[TEXT_SIZE] = [f"{round(s, 1)}pt" for s in size]

def _plt_get_text_position(
self,
Expand All @@ -109,122 +120,87 @@
x, y = position
cur_data = self._data.data.copy()
cur_data["x"], cur_data["y"] = x, y
cur_size = len(cur_data["text"])
cur_size = len(cur_data[TEXT])
if x.size > cur_size:
_n = x.size - cur_size
cur_data["text"] = np.concatenate([cur_data["text"], [""] * _n])
cur_data["text_font"] = np.concatenate(
[cur_data["text_font"], ["Arial"] * _n]
)
cur_data["text_font_size"] = np.concatenate(
[cur_data["text_font_size"], ["12pt"] * _n]
)
cur_data["text_align"] = np.concatenate(
[cur_data["text_align"], ["left"] * _n]
)
cur_data["text_color"] = np.concatenate(
[cur_data["text_color"], ["black"] * _n]
)
cur_data["angle"] = np.concatenate([cur_data["angle"], [0] * _n])
cur_data["background_fill_color"] = np.concatenate(
[cur_data["background_fill_color"], ["#00000000"] * _n]
)
cur_data["background_hatch_pattern"] = np.concatenate(
[cur_data["background_hatch_pattern"], [""] * _n]
)
cur_data["border_line_color"] = np.concatenate(
[cur_data["border_line_color"], ["#00000000"] * _n]
)
cur_data["border_line_width"] = np.concatenate(
[cur_data["border_line_width"], [0] * _n]
)
cur_data["border_line_dash"] = np.concatenate(
[cur_data["border_line_dash"], ["solid"] * _n]
)
_concat = np.concatenate
cur_data[TEXT] = _concat([cur_data[TEXT], [""] * _n])
cur_data[TEXT_SIZE] = _concat([cur_data[TEXT_SIZE], ["12pt"] * _n])
cur_data[TEXT_COLOR] = _concat([cur_data[TEXT_COLOR], ["black"] * _n])
cur_data[TEXT_ANGLE] = _concat([cur_data[TEXT_ANGLE], [0] * _n])
cur_data[BG_COLOR] = _concat([cur_data[BG_COLOR], [INVISIBLE] * _n])
cur_data[BG_HATCH] = _concat([cur_data[BG_HATCH], [""] * _n])
cur_data[BD_COLOR] = _concat([cur_data[BD_COLOR], [INVISIBLE] * _n])
cur_data[BD_WIDTH] = _concat([cur_data[BD_WIDTH], [0] * _n])
cur_data[BD_STYLE] = _concat([cur_data[BD_STYLE], ["solid"] * _n])
elif x.size < cur_size:
cur_data["text"] = cur_data["text"][: x.size]
cur_data["text_font"] = cur_data["text_font"][: x.size]
cur_data["text_font_size"] = cur_data["text_font_size"][: x.size]
cur_data["text_align"] = cur_data["text_align"][: x.size]
cur_data["text_color"] = cur_data["text_color"][: x.size]
cur_data["angle"] = cur_data["angle"][: x.size]
cur_data["background_fill_color"] = cur_data["background_fill_color"][
: x.size
]
cur_data["background_hatch_pattern"] = cur_data["background_hatch_pattern"][
: x.size
]
cur_data["border_line_color"] = cur_data["border_line_color"][: x.size]
cur_data["border_line_width"] = cur_data["border_line_width"][: x.size]
cur_data["border_line_dash"] = cur_data["border_line_dash"][: x.size]
cur_data[TEXT] = cur_data[TEXT][: x.size]
cur_data[TEXT_SIZE] = cur_data[TEXT_SIZE][: x.size]
cur_data[TEXT_COLOR] = cur_data[TEXT_COLOR][: x.size]
cur_data[TEXT_ANGLE] = cur_data[TEXT_ANGLE][: x.size]
cur_data[BG_COLOR] = cur_data[BG_COLOR][: x.size]
cur_data[BG_HATCH] = cur_data[BG_HATCH][: x.size]
cur_data[BD_COLOR] = cur_data[BD_COLOR][: x.size]
cur_data[BD_WIDTH] = cur_data[BD_WIDTH][: x.size]
cur_data[BD_STYLE] = cur_data[BD_STYLE][: x.size]
self._data.data = cur_data

def _plt_get_text_anchor(self) -> list[Alignment]:
return [Alignment(anc) for anc in self._data.data["text_align"]]
def _plt_get_text_anchor(self) -> Alignment:
return Alignment(self._model.text_align)

def _plt_set_text_anchor(self, anc: Alignment | list[Alignment]):
if isinstance(anc, Alignment):
anc = [anc] * len(self._data.data["text"])
self._data.data["text_align"] = [a.value for a in anc]
def _plt_set_text_anchor(self, anc: Alignment):
self._model.text_align = anc.value

def _plt_get_text_rotation(self) -> NDArray[np.floating]:
return np.array(self._data.data["angle"], dtype=np.float32)
return np.array(self._data.data[TEXT_ANGLE], dtype=np.float32)

def _plt_set_text_rotation(self, rotation: float | NDArray[np.floating]):
if is_real_number(rotation):
rotation = np.full(len(self._data.data["text"]), rotation)
self._data.data["angle"] = rotation
rotation = np.full(len(self._data.data[TEXT]), rotation)

Check warning on line 159 in whitecanvas/backend/bokeh/text.py

View check run for this annotation

Codecov / codecov/patch

whitecanvas/backend/bokeh/text.py#L159

Added line #L159 was not covered by tests
self._data.data[TEXT_ANGLE] = rotation

def _plt_get_text_fontfamily(self) -> list[str]:
return self._data.data["text_font"]
def _plt_get_text_fontfamily(self) -> str:
return self._model.text_font

def _plt_set_text_fontfamily(self, fontfamily: str | list[str]):
if isinstance(fontfamily, str):
fontfamily = [fontfamily] * len(self._data.data["text"])
self._data.data["text_font"] = fontfamily
def _plt_set_text_fontfamily(self, fontfamily: str):
self._model.text_font = fontfamily

##### HasFaces #####

def _plt_get_face_color(self):
return np.stack(
[arr_color(c) for c in self._data.data["background_fill_color"]]
)
return np.stack([arr_color(c) for c in self._data.data[BG_COLOR]])

def _plt_set_face_color(self, color):
color = as_color_array(color, len(self._data.data["text"]))
self._data.data["background_fill_color"] = [hex_color(c) for c in color]
color = as_color_array(color, len(self._data.data[TEXT]))
self._data.data[BG_COLOR] = [hex_color(c) for c in color]

def _plt_get_face_hatch(self) -> list[Hatch]:
return [
from_bokeh_hatch(h) for h in self._data.data["background_hatch_pattern"]
]
return [from_bokeh_hatch(h) for h in self._data.data[BG_HATCH]]

def _plt_set_face_hatch(self, pattern: Hatch | list[Hatch]):
if isinstance(pattern, Hatch):
pattern = [pattern] * len(self._data.data["text"])
self._data.data["background_hatch_pattern"] = [
to_bokeh_hatch(p) for p in pattern
]
pattern = [pattern] * len(self._data.data[TEXT])
self._data.data[BG_HATCH] = [to_bokeh_hatch(p) for p in pattern]

def _plt_get_edge_color(self):
return np.stack([arr_color(c) for c in self._data.data["border_line_color"]])
return np.stack([arr_color(c) for c in self._data.data[BD_COLOR]])

def _plt_set_edge_color(self, color):
color = as_color_array(color, len(self._data.data["text"]))
self._data.data["border_line_color"] = [hex_color(c) for c in color]
color = as_color_array(color, len(self._data.data[TEXT]))
self._data.data[BD_COLOR] = [hex_color(c) for c in color]

def _plt_get_edge_width(self) -> NDArray[np.floating]:
return np.array(self._data.data["border_line_width"], dtype=np.float32)
return np.array(self._data.data[BD_WIDTH], dtype=np.float32)

def _plt_set_edge_width(self, width: float | NDArray[np.floating]):
if is_real_number(width):
width = np.full(len(self._data.data["text"]), width)
self._data.data["border_line_width"] = width
width = np.full(len(self._data.data[TEXT]), width)
self._data.data[BD_WIDTH] = width

def _plt_get_edge_style(self) -> list[LineStyle]:
return [from_bokeh_line_style(s) for s in self._data.data["border_line_dash"]]
return [from_bokeh_line_style(s) for s in self._data.data[BD_STYLE]]

def _plt_set_edge_style(self, style: LineStyle | list[LineStyle]):
if isinstance(style, LineStyle):
style = [style] * len(self._data.data["text"])
self._data.data["border_line_dash"] = [to_bokeh_line_style(s) for s in style]
style = [style] * len(self._data.data[TEXT])
self._data.data[BD_STYLE] = [to_bokeh_line_style(s) for s in style]
54 changes: 17 additions & 37 deletions whitecanvas/backend/matplotlib/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ def __init__(
color=np.array([0, 0, 0, 1], dtype=np.float32),
) # fmt: skip
)
self._font_family = "Arial"
self._align = Alignment.BOTTOM_LEFT
self._remove_method = _remove_method

def draw(self, renderer):
Expand Down Expand Up @@ -101,35 +103,17 @@ def _plt_set_text_position(
child.set_position((x0, y0))

def _plt_get_text_anchor(self) -> Alignment:
out = []
for child in self.get_children():
va = child.get_verticalalignment()
ha = child.get_horizontalalignment()
if (aln := _ALIGNMENTS.get((va, ha))) is None:
v = _VERTICAL_ALIGNMENTS[va]
h = _HORIZONTAL_ALIGNMENTS[ha]
aln = Alignment.merge(v, h)
_ALIGNMENTS[(va, ha)] = aln
_ALIGNMENTS_INV[aln] = (va, ha)
out.append(aln)
return out
return self._align

def _plt_set_text_anchor(self, anc: Alignment | list[Alignment]):
def _plt_set_text_anchor(self, anc: Alignment):
"""Set the text position."""
if isinstance(anc, Alignment):
v, h = anc.split()
va = _VERTICAL_ALIGNMENTS_INV[v]
ha = _HORIZONTAL_ALIGNMENTS_INV[h]
for child in self.get_children():
child.set_verticalalignment(va)
child.set_horizontalalignment(ha)
else:
for child, anc0 in zip(self.get_children(), anc):
v, h = anc0.split()
va = _VERTICAL_ALIGNMENTS_INV[v]
ha = _HORIZONTAL_ALIGNMENTS_INV[h]
child.set_verticalalignment(va)
child.set_horizontalalignment(ha)
v, h = anc.split()
va = _VERTICAL_ALIGNMENTS_INV[v]
ha = _HORIZONTAL_ALIGNMENTS_INV[h]
for child in self.get_children():
child.set_verticalalignment(va)
child.set_horizontalalignment(ha)
self._align = anc

def _plt_get_text_rotation(self) -> NDArray[np.float32]:
return np.array(
Expand All @@ -140,14 +124,13 @@ def _plt_set_text_rotation(self, rotation: NDArray[np.float32]):
for child, rotation0 in zip(self.get_children(), rotation):
child.set_rotation(rotation0)

def _plt_get_text_fontfamily(self) -> list[str]:
return [child.get_fontfamily() for child in self.get_children()]
def _plt_get_text_fontfamily(self) -> str:
return self._font_family

def _plt_set_text_fontfamily(self, fontfamily: str | list[str]):
if isinstance(fontfamily, str):
fontfamily = [fontfamily] * len(self.get_children())
for child, fontfamily0 in zip(self.get_children(), fontfamily):
child.set_fontfamily(fontfamily0)
def _plt_set_text_fontfamily(self, fontfamily: str):
for child in self.get_children():
child.set_fontfamily(fontfamily)
self._font_family = fontfamily

##### HasFaces #####

Expand Down Expand Up @@ -266,9 +249,6 @@ def _plt_set_edge_style(self, style: LineStyle):
}
_HORIZONTAL_ALIGNMENTS_INV = {v: k for k, v in _HORIZONTAL_ALIGNMENTS.items()}

_ALIGNMENTS: dict[tuple[str, str], Alignment] = {}
_ALIGNMENTS_INV: dict[Alignment, tuple[str, str]] = {}


def _remove_method(this: Texts):
for child in this.get_children():
Expand Down
Loading
Loading