diff --git a/docs/_static/logo-text.svg b/docs/_static/logo-text.svg new file mode 100644 index 0000000..2bf7bbd --- /dev/null +++ b/docs/_static/logo-text.svg @@ -0,0 +1,77 @@ + + + +matplotgl diff --git a/src/matplotgl/axes.py b/src/matplotgl/axes.py index 9b3f445..f2cb501 100644 --- a/src/matplotgl/axes.py +++ b/src/matplotgl/axes.py @@ -1,5 +1,7 @@ # SPDX-License-Identifier: BSD-3-Clause +import math + import ipywidgets as ipw import numpy as np import pythreejs as p3 @@ -9,7 +11,7 @@ from .line import Line from .mesh import Mesh from .points import Points -from .utils import latex_to_html +from .utils import html_to_svg, latex_to_html from .widgets import ClickableHTML @@ -109,6 +111,11 @@ def __init__(self, *, ax: MplAxes, figure=None) -> None: # self._margin_with_ticks = 50 self._thin_margin = 3 + tooltips = { + "leftspine": "Double-click to toggle y-scale", + "bottomspine": "Double-click to toggle x-scale", + } + self._margins = { name: ClickableHTML( layout={ @@ -116,6 +123,7 @@ def __init__(self, *, ax: MplAxes, figure=None) -> None: "padding": "0", "margin": "0", }, + tooltip=tooltips.get(name, ""), ) for name in ( "leftspine", @@ -139,13 +147,16 @@ def __init__(self, *, ax: MplAxes, figure=None) -> None: "grid_area": "cursor", "padding": "0", "margin": "0", - "width": "80px", + "width": "6em", }, ) if figure is not None: self.set_figure(figure) + self._margins['leftspine'].on_dblclick(self._toggle_yscale) + self._margins['bottomspine'].on_dblclick(self._toggle_xscale) + super().__init__( children=[ *self._margins.values(), @@ -334,13 +345,18 @@ def _make_xticks(self): xlabels = [lab.get_text() for lab in self.get_xticklabels()] xy = np.vstack((xticks, np.zeros_like(xticks))).T - xticks_axes = self._ax.transAxes.inverted().transform( - self._ax.transData.transform(xy) - )[:, 0] + + inv_trans_axes = self._ax.transAxes.inverted() + trans_data = self._ax.transData + xticks_axes = inv_trans_axes.transform(trans_data.transform(xy))[:, 0] + + # width = f"calc({self.width}px + 0.5em)" bottom_string = ( f'' ) @@ -362,9 +378,23 @@ def _make_xticks(self): bottom_string += ( f'' - f"{latex_to_html(label)}" + f"{html_to_svg(latex_to_html(label), baseline='hanging')}" ) + minor_ticks = self._ax.xaxis.get_minorticklocs() + if len(minor_ticks) > 0: + xy = np.vstack((minor_ticks, np.zeros_like(minor_ticks))).T + xticks_axes = inv_trans_axes.transform(trans_data.transform(xy))[:, 0] + + for tick in xticks_axes: + if tick < 0 or tick > 1.0: + continue + x = tick * self.width + bottom_string += ( + f'' + ) + bottom_string += "" self._margins["bottomspine"].value = bottom_string @@ -381,15 +411,18 @@ def _make_yticks(self): ytexts = [lab.get_text() for lab in ylabels] xy = np.vstack((np.zeros_like(yticks), yticks)).T - yticks_axes = self._ax.transAxes.inverted().transform( - self._ax.transData.transform(xy) - )[:, 1] + + inv_trans_axes = self._ax.transAxes.inverted() + trans_data = self._ax.transData + yticks_axes = inv_trans_axes.transform(trans_data.transform(xy))[:, 1] # Predict width of the left margin based on the longest label - max_length = max(lab.get_tightbbox().width for lab in ylabels) + # Need to convert to integer to avoid sub-pixel rendering issues + max_length = math.ceil(max(lab.get_tightbbox().width for lab in ylabels)) width = f"calc({max_length}px + {tick_length}px + {label_offset}px)" width1 = f"calc({max_length}px + {label_offset}px)" width2 = f"calc({max_length}px)" + width3 = f"calc({max_length}px + {tick_length * 0.3}px + {label_offset}px)" left_string = ( f'' @@ -417,9 +450,24 @@ def _make_yticks(self): left_string += ( f'' - f"{latex_to_html(label)}" + f"{html_to_svg(latex_to_html(label), baseline='middle')}" ) + minor_ticks = self._ax.yaxis.get_minorticklocs() + if len(minor_ticks) > 0: + xy = np.vstack((np.zeros_like(minor_ticks), minor_ticks)).T + yticks_axes = inv_trans_axes.transform(trans_data.transform(xy))[:, 1] + + for tick in yticks_axes: + if tick < 0 or tick > 1.0: + continue + y = self.height - (tick * self.height) + left_string += ( + f'' + ) + left_string += "" self._margins["leftspine"].value = left_string @@ -487,6 +535,12 @@ def set_yscale(self, scale): self.autoscale() self._make_yticks() + def _toggle_xscale(self, _): + self.set_xscale("log" if self.get_xscale() == "linear" else "linear") + + def _toggle_yscale(self, _): + self.set_yscale("log" if self.get_yscale() == "linear" else "linear") + def zoom(self, box): self._zoom_limits = { "xmin": box[0], diff --git a/src/matplotgl/utils.py b/src/matplotgl/utils.py index 9079d9b..89a6adc 100644 --- a/src/matplotgl/utils.py +++ b/src/matplotgl/utils.py @@ -147,10 +147,6 @@ def latex_to_html(latex_str: str) -> str: # Special cases that don't follow the pattern (optional overrides) special_replacements = { "ċ": "·", - "": '', - "": "", - "": '', - "": "", } for entity, replacement in special_replacements.items(): @@ -159,18 +155,37 @@ def latex_to_html(latex_str: str) -> str: return s -# def html_tags_to_svg(text: str) -> str: -# """Convert and HTML tags to SVG superscript/subscript using tspan.""" +def html_to_svg(text: str, baseline: str) -> str: + """Convert HTML text to SVG-compatible text using tspan for subscripts/superscripts. -# def replace_sup(match): -# content = match.group(1) -# return "".join(superscripts.get(c, c) for c in content) - -# def replace_sub(match): -# content = match.group(1) -# return "".join(subscripts.get(c, c) for c in content) + Parameters + ---------- + text: + The input HTML text. + baseline: + The dominant baseline alignment for the text ('hanging', 'middle', 'baseline'). + + Returns + ------- + str + The SVG-compatible text. + """ + replacements = { + "hanging": { + "": '', + "": "", + "": '', + "": "", + }, + "middle": { + "": '', + "": "", + "": '', + "": "", + }, + } -# text = re.sub(r"(.*?)", replace_sup, text) -# text = re.sub(r"(.*?)", replace_sub, text) + for entity, replacement in replacements[baseline].items(): + text = text.replace(entity, replacement) -# return text + return text diff --git a/src/matplotgl/widgets.py b/src/matplotgl/widgets.py index d8677a7..46a8150 100644 --- a/src/matplotgl/widgets.py +++ b/src/matplotgl/widgets.py @@ -118,8 +118,8 @@ class ClickableHTML(anywidget.AnyWidget): tooltip_text = traitlets.Unicode("").tag(sync=True) _dblclick_trigger = traitlets.Int(0).tag(sync=True) - def __init__(self, value="", tooltip="", on_dblclick=None, **kwargs): + def __init__(self, value="", tooltip="", **kwargs): super().__init__(value=value, tooltip_text=tooltip, **kwargs) - if on_dblclick: - self.observe(lambda change: on_dblclick(self), "_dblclick_trigger") + def on_dblclick(self, on_dblclick): + self.observe(lambda change: on_dblclick(self), "_dblclick_trigger")