Skip to content

Commit

Permalink
Merge pull request #43 from hanjinliu/text-props
Browse files Browse the repository at this point in the history
Refactor text properties
  • Loading branch information
hanjinliu authored Mar 10, 2024
2 parents 1bc0a45 + c331e05 commit e8b729f
Show file tree
Hide file tree
Showing 12 changed files with 252 additions and 355 deletions.
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 @@ def __init__(
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_get_visible(self) -> bool:
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 @@ def _plt_set_text_position(
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)
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

0 comments on commit e8b729f

Please sign in to comment.