diff --git a/data_prototype/artist.py b/data_prototype/artist.py index 9adbb23..3afef54 100644 --- a/data_prototype/artist.py +++ b/data_prototype/artist.py @@ -277,7 +277,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( @@ -292,6 +295,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/conversion_edge.py b/data_prototype/conversion_edge.py index 20adc9f..8f850b0 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} @@ -165,7 +176,6 @@ def evaluate(self, input: dict[str, Any]) -> dict[str, Any]: @property def inverse(self) -> "FuncEdge": - if self.inverse_func is None: raise RuntimeError("Trying to invert a non-invertable edge") @@ -327,6 +337,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 +355,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: @@ -372,7 +383,6 @@ def node_format(x): G = nx.DiGraph() if input is not None: - for _, edges in self._subgraphs: q: list[dict[str, Desc]] = [input] explored: set[tuple[tuple[str, str], ...]] = set() @@ -427,3 +437,25 @@ def cache_key(self): import uuid return str(uuid.uuid4()) + + +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)