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 @@
+
+
+
+
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'"
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'"
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")