From bae13a275fd8f99687362d90bcc8cb97942dba2c Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Sat, 1 Nov 2025 08:32:38 +0100 Subject: [PATCH 1/6] add toggle scale on double click --- src/matplotgl/axes.py | 15 +++++++++++++++ src/matplotgl/widgets.py | 6 +++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/matplotgl/axes.py b/src/matplotgl/axes.py index 9b3f445..ad30675 100644 --- a/src/matplotgl/axes.py +++ b/src/matplotgl/axes.py @@ -109,6 +109,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 +121,7 @@ def __init__(self, *, ax: MplAxes, figure=None) -> None: "padding": "0", "margin": "0", }, + tooltip=tooltips.get(name, ""), ) for name in ( "leftspine", @@ -146,6 +152,9 @@ def __init__(self, *, ax: MplAxes, figure=None) -> None: 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(), @@ -487,6 +496,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/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") From 32fae3d3da6b60a405a8852d17d2799c58db3ad7 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Sat, 1 Nov 2025 08:57:51 +0100 Subject: [PATCH 2/6] fix left axis hiding issue --- src/matplotgl/axes.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/matplotgl/axes.py b/src/matplotgl/axes.py index ad30675..7b454e9 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 @@ -347,6 +349,8 @@ def _make_xticks(self): self._ax.transData.transform(xy) )[:, 0] + # width = f"calc({self.width}px + 0.5em)" + bottom_string = ( f'' f'' + # f'' ) self._margins["rightspine"].value = ( From a5565a4c85b6fec609dc930ddd360bc2269300fe Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Sat, 1 Nov 2025 08:58:17 +0100 Subject: [PATCH 3/6] cleanup --- src/matplotgl/axes.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/matplotgl/axes.py b/src/matplotgl/axes.py index 7b454e9..9015d92 100644 --- a/src/matplotgl/axes.py +++ b/src/matplotgl/axes.py @@ -404,16 +404,12 @@ def _make_yticks(self): 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}px + {label_offset}px - 1px)" left_string = ( f'' f'' - # f'' ) self._margins["rightspine"].value = ( From 9f025835021ed436898c41755a225c7d2986e04f Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Tue, 4 Nov 2025 21:46:33 +0100 Subject: [PATCH 4/6] add logo with text for future modifications --- docs/_static/logo-text.svg | 77 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 docs/_static/logo-text.svg 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 From f0657aab576c8aedf0206a2c27b772d2cde32fcf Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Tue, 4 Nov 2025 22:10:50 +0100 Subject: [PATCH 5/6] fix log formatting for y axis --- src/matplotgl/axes.py | 12 +++++++----- src/matplotgl/utils.py | 44 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/src/matplotgl/axes.py b/src/matplotgl/axes.py index 9015d92..4818bd3 100644 --- a/src/matplotgl/axes.py +++ b/src/matplotgl/axes.py @@ -11,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 @@ -147,7 +147,7 @@ def __init__(self, *, ax: MplAxes, figure=None) -> None: "grid_area": "cursor", "padding": "0", "margin": "0", - "width": "80px", + "width": "6em", }, ) @@ -353,7 +353,9 @@ def _make_xticks(self): bottom_string = ( f'' ) @@ -375,7 +377,7 @@ def _make_xticks(self): bottom_string += ( f'' - f"{latex_to_html(label)}" + f"{html_to_svg(latex_to_html(label), baseline='hanging')}" ) bottom_string += "" @@ -431,7 +433,7 @@ def _make_yticks(self): left_string += ( f'' - f"{latex_to_html(label)}" + f"{html_to_svg(latex_to_html(label), baseline='middle')}" ) left_string += "" diff --git a/src/matplotgl/utils.py b/src/matplotgl/utils.py index 9079d9b..19595da 100644 --- a/src/matplotgl/utils.py +++ b/src/matplotgl/utils.py @@ -147,10 +147,10 @@ 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,6 +159,42 @@ def latex_to_html(latex_str: str) -> str: return s +def html_to_svg(text: str, baseline: str) -> str: + """Convert HTML text to SVG-compatible text using tspan for subscripts/superscripts. + + 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": { + "": '', + "": "", + "": '', + "": "", + }, + } + + for entity, replacement in replacements[baseline].items(): + text = text.replace(entity, replacement) + + return text + + # def html_tags_to_svg(text: str) -> str: # """Convert and HTML tags to SVG superscript/subscript using tspan.""" From 7764432bf86f933d1231c07e332b6c69faef4079 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Tue, 4 Nov 2025 22:35:56 +0100 Subject: [PATCH 6/6] add minor ticks --- src/matplotgl/axes.py | 44 ++++++++++++++++++++++++++++++++++++------ src/matplotgl/utils.py | 21 -------------------- 2 files changed, 38 insertions(+), 27 deletions(-) diff --git a/src/matplotgl/axes.py b/src/matplotgl/axes.py index 4818bd3..f2cb501 100644 --- a/src/matplotgl/axes.py +++ b/src/matplotgl/axes.py @@ -345,9 +345,10 @@ 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)" @@ -380,6 +381,20 @@ def _make_xticks(self): 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 @@ -396,9 +411,10 @@ 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 # Need to convert to integer to avoid sub-pixel rendering issues @@ -406,6 +422,7 @@ def _make_yticks(self): 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'' @@ -436,6 +453,21 @@ def _make_yticks(self): 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 diff --git a/src/matplotgl/utils.py b/src/matplotgl/utils.py index 19595da..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(): @@ -193,20 +189,3 @@ def html_to_svg(text: str, baseline: str) -> str: text = text.replace(entity, replacement) return text - - -# def html_tags_to_svg(text: str) -> str: -# """Convert and HTML tags to SVG superscript/subscript using tspan.""" - -# 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) - -# text = re.sub(r"(.*?)", replace_sup, text) -# text = re.sub(r"(.*?)", replace_sub, text) - -# return text