diff --git a/data_prototype/artist.py b/data_prototype/artist.py index 7beddc3..6186c05 100644 --- a/data_prototype/artist.py +++ b/data_prototype/artist.py @@ -1,4 +1,5 @@ from bisect import insort +from collections import namedtuple from typing import Sequence import numpy as np @@ -234,6 +235,7 @@ def __init__(self, axes): self.figure = None self._clippath = None self.zorder = 2 + self.sticky_edges = namedtuple("Sticky", ["x", "y"])([], []) @property def axes(self): @@ -248,7 +250,10 @@ def axes(self, ax): return desc: Desc = Desc(("N",), coordinates="data") + desc_scal: Desc = Desc((), coordinates="data") xy: dict[str, Desc] = {"x": desc, "y": desc} + xy_scal: dict[str, Desc] = {"x": desc_scal, "y": desc_scal} + self._graph = Graph( [ TransformEdge( @@ -263,6 +268,18 @@ def axes(self, ax): desc_like(xy, coordinates="display"), transform=self._axes.transAxes, ), + TransformEdge( + "data_scal", + xy_scal, + desc_like(xy_scal, coordinates="axes"), + transform=self._axes.transData - self._axes.transAxes, + ), + TransformEdge( + "axes_scal", + desc_like(xy_scal, coordinates="axes"), + desc_like(xy_scal, coordinates="display"), + transform=self._axes.transAxes, + ), FuncEdge.from_func( "xunits", lambda: self._axes.xaxis.units, diff --git a/data_prototype/axes.py b/data_prototype/axes.py index 5659701..1f06d3f 100644 --- a/data_prototype/axes.py +++ b/data_prototype/axes.py @@ -2,9 +2,12 @@ import matplotlib as mpl +from data_prototype.artist import CompatibilityAxes from matplotlib.axes._axes import Axes as MPLAxes, _preprocess_data +from matplotlib.axes._base import _process_plot_var_args import matplotlib.collections as mcoll import matplotlib.cbook as cbook +import matplotlib.lines as mlines import matplotlib.markers as mmarkers import matplotlib.projections as mprojections @@ -14,6 +17,7 @@ FunctionConversionNode, RenameConversionNode, ) +from .line import Line from .wrappers import PathCollectionWrapper @@ -21,6 +25,12 @@ class Axes(MPLAxes): # Name for registering as a projection so we can experiment with it name = "data-prototype" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._ca = CompatibilityAxes(self) + self._get_lines_mirror = _process_plot_var_args("mirror") + self.add_artist(self._ca) + @_preprocess_data( replace_names=[ "x", @@ -142,6 +152,26 @@ def scatter( self._request_autoscale_view() return pcw + def plot(self, *args, scalex=True, scaley=True, data=None, **kwargs): + kwargs = cbook.normalize_kwargs(kwargs, mlines.Line2D) + line_args = [ + *self._get_lines_mirror( + self, *args, data=data, **kwargs, return_kwargs=True + ) + ] + print(line_args) + lines = [] + for coord, kws in line_args: + cont = ArrayContainer(**{"x": coord[0], "y": coord[1]}) + line = Line(cont, **kws) + lines.append(line) + self._ca.add_artist(line) + if scalex: + self._request_autoscale_view("x") + if scaley: + self._request_autoscale_view("y") + return lines + # This is a handy trick to allow e.g. plt.subplots(subplot_kw={'projection': 'data-prototype'}) mprojections.register_projection(Axes) diff --git a/data_prototype/conversion_edge.py b/data_prototype/conversion_edge.py index 2a09cc1..fbcce56 100644 --- a/data_prototype/conversion_edge.py +++ b/data_prototype/conversion_edge.py @@ -7,7 +7,7 @@ from typing import Any import numpy as np -from data_prototype.description import Desc, desc_like +from data_prototype.description import Desc, desc_like, ShapeSpec from matplotlib.transforms import Transform @@ -112,6 +112,17 @@ def from_default_value( ) -> "DefaultEdge": return cls(name, {}, {key: output}, weight, invertable=False, value=value) + @classmethod + def from_rc( + cls, rc_name: str, key: str | None = None, coordinates: str = "display" + ): + from matplotlib import rcParams + + if key is None: + key = rc_name.split(".")[-1] + scalar = Desc((), coordinates) + return cls.from_default_value(f"{rc_name}_rc", key, scalar, rcParams[rc_name]) + def evaluate(self, input: dict[str, Any]) -> dict[str, Any]: return {k: self.value for k in self.output} @@ -327,6 +338,7 @@ def edges(self): import matplotlib.pyplot as plt self.visualize(input) + self.visualize() plt.show() raise NotImplementedError( "This may be possible, but is not a simple case already considered" @@ -344,7 +356,7 @@ def edges(self): else: out_edges.append(SequenceEdge.from_edges("eval", edges, output_subset)) - found_outputs = set() + found_outputs = set(input) for out in out_edges: found_outputs |= set(out.output) if missing := set(output) - found_outputs: @@ -418,3 +430,25 @@ def __add__(self, other: Graph) -> Graph: aother = {k: v for k, v in other._aliases} aliases = tuple((aself | aother).items()) return Graph(self._edges + other._edges, aliases) + + +def coord_and_default( + key: str, + shape: ShapeSpec = (), + coordinates: str = "display", + default_value: Any = None, + default_rc: str | None = None, +): + if default_rc is not None: + if default_value is not None: + raise ValueError( + "Only one of 'default_value' and 'default_rc' may be specified" + ) + def_edge = DefaultEdge.from_rc(default_rc, key, coordinates) + else: + scalar = Desc((), coordinates) + def_edge = DefaultEdge.from_default_value( + f"{key}_def", key, scalar, default_value + ) + coord_edge = CoordinateEdge.from_coords(key, {key: Desc(shape)}, coordinates) + return coord_edge, def_edge diff --git a/data_prototype/image.py b/data_prototype/image.py index 005c76f..786f1e8 100644 --- a/data_prototype/image.py +++ b/data_prototype/image.py @@ -76,6 +76,14 @@ def __init__(self, container, edges=None, norm=None, cmap=None, **kwargs): {"image": Desc(("O", "P", 4), "rgba_resampled")}, {"image": Desc(("O", "P", 4), "display")}, ), + FuncEdge.from_func( + "rgb_rgba", + lambda image: np.append( + image, np.ones(image.shape[:-1] + (1,)), axis=-1 + ), + {"image": Desc(("M", "N", 3), "rgb")}, + {"image": Desc(("M", "N", 4), "rgba")}, + ), self._interpolation_edge, ] diff --git a/data_prototype/line.py b/data_prototype/line.py index 8805e8b..f7c01ff 100644 --- a/data_prototype/line.py +++ b/data_prototype/line.py @@ -18,7 +18,7 @@ def __init__(self, container, edges=None, **kwargs): scalar = Desc((), "display") # ... this needs thinking... - edges = [ + default_edges = [ CoordinateEdge.from_coords("xycoords", {"x": "auto", "y": "auto"}, "data"), CoordinateEdge.from_coords("color", {"color": Desc(())}, "display"), CoordinateEdge.from_coords("linewidth", {"linewidth": Desc(())}, "display"), @@ -45,7 +45,7 @@ def __init__(self, container, edges=None, **kwargs): DefaultEdge.from_default_value("mew_def", "markeredgewidth", scalar, 1), DefaultEdge.from_default_value("marker_def", "marker", scalar, "None"), ] - self._graph = self._graph + Graph(edges) + self._graph = self._graph + Graph(default_edges) # Currently ignoring: # - cap/join style # - url diff --git a/data_prototype/text.py b/data_prototype/text.py new file mode 100644 index 0000000..cdc30bf --- /dev/null +++ b/data_prototype/text.py @@ -0,0 +1,97 @@ +import numpy as np + +from matplotlib.font_manager import FontProperties + +from .artist import Artist +from .description import Desc +from .conversion_edge import Graph, CoordinateEdge, coord_and_default + + +class Text(Artist): + def __init__(self, container, edges=None, **kwargs): + super().__init__(container, edges, **kwargs) + + edges = [ + CoordinateEdge.from_coords( + "xycoords", {"x": Desc((), "auto"), "y": Desc((), "auto")}, "data" + ), + *coord_and_default("text", default_value=""), + *coord_and_default("color", default_rc="text.color"), + *coord_and_default("alpha", default_value=1), + *coord_and_default("fontproperties", default_value=FontProperties()), + *coord_and_default("usetex", default_rc="text.usetex"), + *coord_and_default("rotation", default_value=0), + *coord_and_default("antialiased", default_rc="text.antialiased"), + ] + + self._graph = self._graph + Graph(edges) + + def draw(self, renderer, graph: Graph) -> None: + if not self.get_visible(): + return + g = graph + self._graph + conv = g.evaluator( + self._container.describe(), + { + "x": Desc((), "display"), + "y": Desc((), "display"), + "text": Desc((), "display"), + "color": Desc((), "display"), + "alpha": Desc((), "display"), + "fontproperties": Desc((), "display"), + "usetex": Desc((), "display"), + # "parse_math": Desc((), "display"), + # "wrap": Desc((), "display"), + # "verticalalignment": Desc((), "display"), + # "horizontalalignment": Desc((), "display"), + "rotation": Desc((), "display"), + # "linespacing": Desc((), "display"), + # "rotation_mode": Desc((), "display"), + "antialiased": Desc((), "display"), + }, + ) + + query, _ = self._container.query(g) + evald = conv.evaluate(query) + + text = evald["text"] + if text == "": + return + + x = evald["x"] + y = evald["y"] + + _, canvash = renderer.get_canvas_width_height() + if renderer.flipy(): + y = canvash - y + + if not np.isfinite(x) or not np.isfinite(y): + # TODO: log? + return + + # TODO bbox? + # TODO implement wrapping/layout? + # TODO implement math? + # TODO implement path_effects? + + # TODO gid? + renderer.open_group("text", None) + + gc = renderer.new_gc() + gc.set_foreground(evald["color"]) + gc.set_alpha(evald["alpha"]) + # TODO url? + gc.set_antialiased(evald["antialiased"]) + # TODO clipping? + + if evald["usetex"]: + renderer.draw_tex( + gc, x, y, text, evald["fontproperties"], evald["rotation"] + ) + else: + renderer.draw_text( + gc, x, y, text, evald["fontproperties"], evald["rotation"] + ) + + gc.restore() + renderer.close_group("text") diff --git a/examples/2Dfunc.py b/examples/2Dfunc.py index deaad36..883b932 100644 --- a/examples/2Dfunc.py +++ b/examples/2Dfunc.py @@ -10,7 +10,8 @@ import matplotlib.pyplot as plt import numpy as np -from data_prototype.wrappers import ImageWrapper +from data_prototype.artist import CompatibilityAxes +from data_prototype.image import Image from data_prototype.containers import FuncContainer from matplotlib.colors import Normalize @@ -19,8 +20,8 @@ fc = FuncContainer( {}, xyfuncs={ - "xextent": ((2,), lambda x, y: [x[0], x[-1]]), - "yextent": ((2,), lambda x, y: [y[0], y[-1]]), + "x": ((2,), lambda x, y: [x[0], x[-1]]), + "y": ((2,), lambda x, y: [y[0], y[-1]]), "image": ( ("N", "M"), lambda x, y: np.sin(x).reshape(1, -1) * np.cos(y).reshape(-1, 1), @@ -28,11 +29,14 @@ }, ) norm = Normalize(vmin=-1, vmax=1) -im = ImageWrapper(fc, norm=norm) +im = Image(fc, norm=norm) + +fig, nax = plt.subplots() +ax = CompatibilityAxes(nax) +nax.add_artist(ax) -fig, ax = plt.subplots() ax.add_artist(im) ax.set_xlim(-5, 5) ax.set_ylim(-5, 5) -fig.colorbar(im) +# fig.colorbar(im, ax=nax) plt.show() diff --git a/examples/animation.py b/examples/animation.py index d3e276c..ee2cf45 100644 --- a/examples/animation.py +++ b/examples/animation.py @@ -20,11 +20,11 @@ from data_prototype.conversion_edge import Graph from data_prototype.description import Desc -from data_prototype.conversion_node import FunctionConversionNode -from data_prototype.wrappers import FormattedText -from data_prototype.artist import CompatibilityArtist as CA +from data_prototype.artist import CompatibilityAxes from data_prototype.line import Line +from data_prototype.text import Text +from data_prototype.conversion_edge import FuncEdge class SinOfTime: @@ -63,15 +63,24 @@ def update(frame, art): sot_c = SinOfTime() -lw = CA(Line(sot_c, linewidth=5, color="green", label="sin(time)")) -fc = FormattedText( +lw = Line(sot_c, linewidth=5, color="green", label="sin(time)") +fc = Text( sot_c, - FunctionConversionNode.from_funcs( - {"text": lambda phase: f"ϕ={phase:.2f}", "x": lambda: 2 * np.pi, "y": lambda: 1} - ), + [ + FuncEdge.from_func( + "text", + lambda phase: f"ϕ={phase:.2f}", + {"phase": Desc((), "auto")}, + {"text": Desc((), "display")}, + ), + ], + x=2 * np.pi, + y=1, ha="right", ) -fig, ax = plt.subplots() +fig, nax = plt.subplots() +ax = CompatibilityAxes(nax) +nax.add_artist(ax) ax.add_artist(lw) ax.add_artist(fc) ax.set_xlim(0, 2 * np.pi) diff --git a/examples/mapped.py b/examples/mapped.py index 85cd636..4800019 100644 --- a/examples/mapped.py +++ b/examples/mapped.py @@ -12,13 +12,12 @@ from matplotlib.colors import Normalize -from data_prototype.wrappers import FormattedText -from data_prototype.artist import CompatibilityArtist as CA +from data_prototype.artist import CompatibilityAxes from data_prototype.line import Line from data_prototype.containers import ArrayContainer from data_prototype.description import Desc -from data_prototype.conversion_node import FunctionConversionNode from data_prototype.conversion_edge import FuncEdge +from data_prototype.text import Text cmap = plt.colormaps["viridis"] @@ -49,19 +48,35 @@ ), ] -text_converter = FunctionConversionNode.from_funcs( - { - "text": lambda j, cat: f"index={j[()]} class={cat!r}", - "y": lambda j: j, - "x": lambda x: 2 * np.pi, - }, -) +text_edges = [ + FuncEdge.from_func( + "text", + lambda j, cat: f"index={j[()]} class={cat!r}", + {"j": Desc((), "auto"), "cat": Desc((), "auto")}, + {"text": Desc((), "display")}, + ), + FuncEdge.from_func( + "y", + lambda j: j, + {"j": Desc((), "auto")}, + {"y": Desc((), "data")}, + ), + FuncEdge.from_func( + "x", + lambda: 2 * np.pi, + {}, + {"x": Desc((), "data")}, + ), +] th = np.linspace(0, 2 * np.pi, 128) delta = np.pi / 9 -fig, ax = plt.subplots() +fig, nax = plt.subplots() + +ax = CompatibilityAxes(nax) +nax.add_artist(ax) for j in range(10): ac = ArrayContainer( @@ -74,20 +89,18 @@ } ) ax.add_artist( - CA( - Line( - ac, - line_edges, - ) + Line( + ac, + line_edges, ) ) ax.add_artist( - FormattedText( + Text( ac, - text_converter, + text_edges, x=2 * np.pi, - ha="right", - bbox={"facecolor": "gray", "alpha": 0.5}, + # ha="right", + # bbox={"facecolor": "gray", "alpha": 0.5}, ) ) ax.set_xlim(0, np.pi * 2) diff --git a/examples/mulivariate_cmap.py b/examples/mulivariate_cmap.py index c00b709..8b33ca8 100644 --- a/examples/mulivariate_cmap.py +++ b/examples/mulivariate_cmap.py @@ -11,9 +11,11 @@ import matplotlib.pyplot as plt import numpy as np -from data_prototype.wrappers import ImageWrapper +from data_prototype.image import Image +from data_prototype.artist import CompatibilityAxes +from data_prototype.description import Desc from data_prototype.containers import FuncContainer -from data_prototype.conversion_node import FunctionConversionNode +from data_prototype.conversion_edge import FuncEdge from matplotlib.colors import hsv_to_rgb @@ -35,15 +37,24 @@ def image_nu(image): fc = FuncContainer( {}, xyfuncs={ - "xextent": ((2,), lambda x, y: [x[0], x[-1]]), - "yextent": ((2,), lambda x, y: [y[0], y[-1]]), + "x": ((2,), lambda x, y: [x[0], x[-1]]), + "y": ((2,), lambda x, y: [y[0], y[-1]]), "image": (("N", "M", 2), func), }, ) -im = ImageWrapper(fc, FunctionConversionNode.from_funcs({"image": image_nu})) +image_edges = FuncEdge.from_func( + "image", + image_nu, + {"image": Desc(("M", "N", 2), "auto")}, + {"image": Desc(("M", "N", 3), "rgb")}, +) + +im = Image(fc, [image_edges]) -fig, ax = plt.subplots() +fig, nax = plt.subplots() +ax = CompatibilityAxes(nax) +nax.add_artist(ax) ax.add_artist(im) ax.set_xlim(-5, 5) ax.set_ylim(-5, 5)