From 2b33f654fd8c4be911f0a887609a2ce06aa83719 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Wed, 17 Jan 2024 10:14:21 -0600 Subject: [PATCH 1/7] Docstring for validate_shapes --- data_prototype/containers.py | 37 ++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/data_prototype/containers.py b/data_prototype/containers.py index dc1a411..a4a5d06 100644 --- a/data_prototype/containers.py +++ b/data_prototype/containers.py @@ -48,8 +48,41 @@ def validate_shapes( specification: dict[str, ShapeSpec | "Desc"], actual: dict[str, ShapeSpec | "Desc"], *, - broadcast=False, - ) -> bool: + broadcast: bool = False, + ) -> None: + """Validate specified shape relationships against a provided set of shapes. + + Shapes provided are tuples of int | str. If a specification calls for an int, + the exact size is expected. + If it is a str, it must be a single capital letter optionally followed by ``+`` + or ``-`` an integer value. + The same letter used in the specification must represent the same value in all + appearances. The value may, however, be a variable (with an offset) in the + actual shapes (which does not need to have the same letter). + + Shapes may be provided as raw tuples or as ``Desc`` objects. + + Parameters + ---------- + specification: dict[str, ShapeSpec | "Desc"] + The desired shape relationships + actual: dict[str, ShapeSpec | "Desc"] + The shapes to test for compliance + + Keyword Parameters + ------------------ + broadcast: bool + Whether to allow broadcasted shapes to pass (i.e. actual shapes with a ``1`` + will not cause exceptions regardless of what the specified shape value is) + + Raises + ------ + KeyError: + If a required field from the specification is missing in the provided actual + values. + ValueError: + If shapes are incompatible in any other way + """ specvars: dict[str, int | tuple[str, int]] = {} for fieldname in specification: spec = specification[fieldname] From 623aa74f04fc9450c0c4fb6b44dd3dcf06b4a3fb Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Wed, 24 Jan 2024 10:01:10 -0600 Subject: [PATCH 2/7] Initial rework of conversions as edges --- data_prototype/containers.py | 21 ++++- data_prototype/conversion_edge.py | 128 ++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 data_prototype/conversion_edge.py diff --git a/data_prototype/containers.py b/data_prototype/containers.py index a4a5d06..620f6d8 100644 --- a/data_prototype/containers.py +++ b/data_prototype/containers.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dataclasses import dataclass from typing import ( Protocol, @@ -38,10 +40,8 @@ class Desc: # - what is the relative size to the other variable values (N vs N+1) # We are probably going to have to implement a DSL for this (😞) shape: ShapeSpec - # TODO: is using a string better? dtype: np.dtype - # TODO: do we want to include this at this level? "naive" means unit-unaware. - units: str = "naive" + coordinates: str = "naive" @staticmethod def validate_shapes( @@ -129,6 +129,21 @@ def validate_shapes( ) return None + @staticmethod + def compatible(a: dict[str, Desc], b: dict[str, Desc]) -> bool: + """Determine if ``a`` is a valid input for ``b``. + + Note: ``a`` _may_ have additional keys. + """ + try: + Desc.validate_shapes(b, a) + except (KeyError, ValueError): + return False + for k, v in b.items(): + if a[k].coordinates != v.coordinates: + return False + return True + class DataContainer(Protocol): def query( diff --git a/data_prototype/conversion_edge.py b/data_prototype/conversion_edge.py new file mode 100644 index 0000000..d7acdea --- /dev/null +++ b/data_prototype/conversion_edge.py @@ -0,0 +1,128 @@ +from collections.abc import Sequence +from dataclasses import dataclass +from typing import Any + +from .containers import Desc + + +@dataclass +class Edge: + name: str + input: dict[str, Desc] + output: dict[str, Desc] + invertable: bool = False + + def evaluate(self, input: dict[str, Any]) -> dict[str, Any]: + return input + + @property + def inverse(self) -> "Edge": + raise NotImplementedError + + +@dataclass +class SequenceEdge(Edge): + edges: Sequence[Edge] = () + + @classmethod + def from_edges(cls, name: str, edges: Sequence[Edge], output: dict[str, Desc]): + input = {} + intermediates = {} + invertable = True + for edge in edges: + input |= {k: v for k, v in edge.input.items() if k not in intermediates} + intermediates |= edge.output + if not edge.invertable: + invertable = False + return cls(name, input, output, invertable, edges) + + def evaluate(self, input: dict[str, Any]) -> dict[str, Any]: + for edge in self.edges: + input |= edge.evaluate(**{k: input[k] for k in edge.input}) + return {k: input[k] for k in self.output} + + +class Graph: + def __init__(self, edges: Sequence[Edge]): + self._edges = edges + # TODO: precompute some internal representation? + # - Nodes between edges, potentially in discrete subgraphs + # - Inversions are not included right now + + def evaluator(self, input: dict[str, Desc], output: dict[str, Desc]) -> Edge: + # May wish to solve for each output independently + # Probably can be smarter here and prune more effectively. + q: list[tuple[dict[str, Desc], tuple[Edge, ...]]] = [(input, ())] + + def trace(x: dict[str, Desc]) -> tuple[tuple[str, str], ...]: + return tuple(sorted([(k, v.coordinates) for k, v in x.items()])) + + explored: set[tuple[tuple[str, str], ...]] = set() + explored.add(trace(input)) + edges = () + while q: + v, edges = q.pop() + if Desc.compatible(v, output): + break + for e in self._edges: + if Desc.compatible(v, e.input): + w = (v | e.output, (*edges, e)) + w_trace = trace(w[0]) + if w_trace in explored: + # This may need to be more explicitly checked... + # May not accurately be checking what we consider "in" + continue + explored.add(w_trace) + q.append(w) + else: + # TODO: case where non-linear solving is needed + raise NotImplementedError( + "This may be possible, but is not a simple case already considered" + ) + if len(edges) == 0: + return Edge("noop", input, output) + elif len(edges) == 1: + return edges[0] + else: + return SequenceEdge.from_edges("eval", edges, output) + + def visualize(self, input: dict[str, Desc] | None = None): + import networkx as nx + import matplotlib.pyplot as plt + from pprint import pformat + + def node_format(x): + return pformat({k: v.coordinates for k, v in x.items()}) + + G = nx.DiGraph() + + if input is not None: + q: list[dict[str, Desc]] = [input] + explored: set[tuple[tuple[str, str], ...]] = set() + explored.add(tuple(sorted(((k, v.coordinates) for k, v in q[0].items())))) + G.add_node(node_format(q[0])) + while q: + n = q.pop() + for e in self._edges: + if Desc.compatible(n, e.input): + w = n | e.output + if node_format(w) not in G: + G.add_node(node_format(w)) + explored.add( + tuple( + sorted(((k, v.coordinates) for k, v in w.items())) + ) + ) + q.append(w) + if node_format(w) != node_format(n): + G.add_edge(node_format(n), node_format(w), name=e.name) + else: + for edge in self._edges: + G.add_edge( + node_format(edge.input), node_format(edge.output), name=edge.name + ) + + pos = nx.planar_layout(G) + nx.draw(G, pos=pos, with_labels=True) + nx.draw_networkx_edge_labels(G, pos=pos) + plt.show() From dee651810bb4665568ceb0dc4653b5e761805c0a Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Fri, 2 Feb 2024 12:45:33 -0600 Subject: [PATCH 3/7] FuncEdge and TransformEdge --- data_prototype/conversion_edge.py | 69 ++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/data_prototype/conversion_edge.py b/data_prototype/conversion_edge.py index d7acdea..242ddc4 100644 --- a/data_prototype/conversion_edge.py +++ b/data_prototype/conversion_edge.py @@ -1,8 +1,12 @@ from collections.abc import Sequence +from typing import Callable from dataclasses import dataclass from typing import Any +import numpy as np -from .containers import Desc +from data_prototype.containers import Desc + +from matplotlib.transforms import Transform @dataclass @@ -42,6 +46,66 @@ def evaluate(self, input: dict[str, Any]) -> dict[str, Any]: return {k: input[k] for k in self.output} +@dataclass +class FuncEdge(Edge): + # TODO: more explicit callable boundaries? + func: Callable = lambda: {} + + @classmethod + def from_func( + cls, + name: str, + func: Callable, + input: str | dict[str, Desc], + output: str | dict[str, Desc], + ): + # dtype/shape is reductive here, but I like the idea of being able to just + # supply a function and the input/output coordinates for many things + if isinstance(input, str): + import inspect + + input_vars = inspect.signature(func).parameters.keys() + input = {k: Desc(("N",), np.dtype("f8"), input) for k in input_vars} + if isinstance(output, str): + output = {k: Desc(("N",), np.dtype("f8"), output) for k in input.keys()} + + return cls(name, input, output, False, func) + + def evaluate(self, input: dict[str, Any]) -> dict[str, Any]: + res = self.func(**{k: input[k] for k in self.input}) + + if isinstance(res, dict): + # TODO: more sanity checks here? + # How forgiving do we _really_ wish to be? + return res + elif isinstance(res, tuple): + if len(res) != len(self.output): + raise RuntimeError( + f"Expected {len(self.output)} return values, got {len(res)}" + ) + return {k: v for k, v in zip(self.output, res)} + elif len(self.output) == 1: + return {k: res for k in self.output} + raise RuntimeError("Output of function does not match expected output") + + +@dataclass +class TransformEdge(Edge): + transform: Transform | None = None + + # TODO: helper for common cases/validation? + + def evaluate(self, input: dict[str, Any]) -> dict[str, Any]: + # TODO: ensure ordering? + # Stacking and unstacking at every step seems inefficient, + # especially if initially given as stacked + if self.transform is None: + return input + inp = np.stack([input[k] for k in self.input], axis=-1) + outp = self.transform.transform(inp) + return {k: v for k, v in zip(self.output, outp.T)} + + class Graph: def __init__(self, edges: Sequence[Edge]): self._edges = edges @@ -123,6 +187,7 @@ def node_format(x): ) pos = nx.planar_layout(G) + plt.figure() nx.draw(G, pos=pos, with_labels=True) nx.draw_networkx_edge_labels(G, pos=pos) - plt.show() + # plt.show() From df2fb89b24af7d8e334e3098c95fed4d95aaa9d9 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Fri, 1 Mar 2024 13:03:33 -0600 Subject: [PATCH 4/7] Fix examples --- data_prototype/containers.py | 200 +++++++++++++++++++++++------- data_prototype/conversion_edge.py | 47 ++++++- data_prototype/wrappers.py | 41 ++++-- examples/animation.py | 8 +- examples/lissajous.py | 7 +- examples/subsample.py | 45 ++++++- examples/widgets.py | 10 +- 7 files changed, 282 insertions(+), 76 deletions(-) diff --git a/data_prototype/containers.py b/data_prototype/containers.py index 620f6d8..4886810 100644 --- a/data_prototype/containers.py +++ b/data_prototype/containers.py @@ -19,6 +19,11 @@ import numpy as np import pandas as pd +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .conversion_edge import Graph + class _MatplotlibTransform(Protocol): def transform(self, verts): @@ -148,9 +153,8 @@ def compatible(a: dict[str, Desc], b: dict[str, Desc]) -> bool: class DataContainer(Protocol): def query( self, - # TODO 3D?!! - coord_transform: _MatplotlibTransform, - size: Tuple[int, int], + graph: Graph, + parent_coordinates: str = "axes", /, ) -> Tuple[Dict[str, Any], Union[str, int]]: """ @@ -208,8 +212,8 @@ def __init__(self, **data): def query( self, - coord_transform: _MatplotlibTransform, - size: Tuple[int, int], + graph: Graph, + parent_coordinates: str = "axes", ) -> Tuple[Dict[str, Any], Union[str, int]]: return dict(self._data), self._cache_key @@ -233,8 +237,8 @@ def __init__(self, **shapes): def query( self, - coord_transform: _MatplotlibTransform, - size: Tuple[int, int], + graph: Graph, + parent_coordinates: str = "axes", ) -> Tuple[Dict[str, Any], Union[str, int]]: return {k: np.random.randn(*d.shape) for k, d in self._desc.items()}, str( uuid.uuid4() @@ -301,31 +305,101 @@ def _query_hash(self, coord_transform, size): def query( self, - coord_transform: _MatplotlibTransform, - size: Tuple[int, int], + graph: Graph, + parent_coordinates: str = "axes", ) -> Tuple[Dict[str, Any], Union[str, int]]: - hash_key = self._query_hash(coord_transform, size) - if hash_key in self._cache: - return self._cache[hash_key], hash_key + # hash_key = self._query_hash(coord_transform, size) + # if hash_key in self._cache: + # return self._cache[hash_key], hash_key + + data_lim = graph.evaluator( + { + "x": Desc( + ("N",), + np.dtype( + "f8", + ), + coordinates="data", + ), + "y": Desc( + ("N",), + np.dtype( + "f8", + ), + coordinates="data", + ), + }, + { + "x": Desc( + ("N",), + np.dtype( + "f8", + ), + coordinates=parent_coordinates, + ), + "y": Desc( + ("N",), + np.dtype( + "f8", + ), + coordinates=parent_coordinates, + ), + }, + ).inverse + screen_size = graph.evaluator( + { + "x": Desc( + ("N",), + np.dtype( + "f8", + ), + coordinates=parent_coordinates, + ), + "y": Desc( + ("N",), + np.dtype( + "f8", + ), + coordinates=parent_coordinates, + ), + }, + { + "x": Desc( + ("N",), + np.dtype( + "f8", + ), + coordinates="display", + ), + "y": Desc( + ("N",), + np.dtype( + "f8", + ), + coordinates="display", + ), + }, + ) - xpix, ypix = size - x_data, _ = coord_transform.transform( - np.vstack( - [ - np.linspace(0, 1, int(xpix) * 2), - np.zeros(int(xpix) * 2), - ] - ).T - ).T - _, y_data = coord_transform.transform( - np.vstack( - [ - np.zeros(int(ypix) * 2), - np.linspace(0, 1, int(ypix) * 2), - ] - ).T - ).T + screen_dims = screen_size.evaluate({"x": [0, 1], "y": [0, 1]}) + xpix, ypix = np.ceil(np.abs(np.diff(screen_dims["x"]))), np.ceil( + np.abs(np.diff(screen_dims["y"])) + ) + x_data = data_lim.evaluate( + { + "x": np.linspace(0, 1, int(xpix) * 2), + "y": np.zeros(int(xpix) * 2), + } + )["x"] + y_data = data_lim.evaluate( + { + "x": np.zeros(int(ypix) * 2), + "y": np.linspace(0, 1, int(ypix) * 2), + } + )["y"] + + hash_key = str(uuid.uuid4()) ret = self._cache[hash_key] = dict( **{k: f(x_data) for k, f in self._xfuncs.items()}, **{k: f(y_data) for k, f in self._yfuncs.items()}, @@ -350,11 +424,49 @@ def __init__(self, raw_data, num_bins: int): def query( self, - coord_transform: _MatplotlibTransform, - size: Tuple[int, int], + graph: Graph, + parent_coordinates: str = "axes", ) -> Tuple[Dict[str, Any], Union[str, int]]: dmin, dmax = self._full_range - xmin, ymin, xmax, ymax = coord_transform.transform([[0, 0], [1, 1]]).flatten() + + data_lim = graph.evaluator( + { + "x": Desc( + ("N",), + np.dtype( + "f8", + ), + coordinates="data", + ), + "y": Desc( + ("N",), + np.dtype( + "f8", + ), + coordinates="data", + ), + }, + { + "x": Desc( + ("N",), + np.dtype( + "f8", + ), + coordinates=parent_coordinates, + ), + "y": Desc( + ("N",), + np.dtype( + "f8", + ), + coordinates=parent_coordinates, + ), + }, + ).inverse + + pts = data_lim.evaluate({"x": (0, 1), "y": (0, 1)}) + xmin, xmax = pts["x"] + ymin, ymax = pts["y"] xmin, xmax = np.clip([xmin, xmax], dmin, dmax) hash_key = hash((xmin, xmax)) @@ -398,8 +510,8 @@ def __init__(self, series: pd.Series, *, index_name: str, col_name: str): def query( self, - coord_transform: _MatplotlibTransform, - size: Tuple[int, int], + graph: Graph, + parent_coordinates: str = "axes", ) -> Tuple[Dict[str, Any], Union[str, int]]: return { self._index_name: self._data.index.values, @@ -440,8 +552,8 @@ def __init__( def query( self, - coord_transform: _MatplotlibTransform, - size: Tuple[int, int], + graph: Graph, + parent_coordinates: str = "axes", ) -> Tuple[Dict[str, Any], Union[str, int]]: ret = {} if self._index_name is not None: @@ -463,10 +575,10 @@ def __init__(self, data: DataContainer, mapping: Dict[str, str]): def query( self, - coord_transform: _MatplotlibTransform, - size: Tuple[int, int], + graph: Graph, + parent_coordinates: str = "axes", ) -> Tuple[Dict[str, Any], Union[str, int]]: - base, cache_key = self._data.query(coord_transform, size) + base, cache_key = self._data.query(graph, parent_coordinates) return {v: base[k] for k, v in self._mapping.items()}, cache_key def describe(self): @@ -481,13 +593,13 @@ def __init__(self, *data: DataContainer): def query( self, - coord_transform: _MatplotlibTransform, - size: Tuple[int, int], + graph: Graph, + parent_coordinates: str = "axes", ) -> Tuple[Dict[str, Any], Union[str, int]]: cache_keys = [] ret = {} for data in self._datas: - base, cache_key = data.query(coord_transform, size) + base, cache_key = data.query(graph, parent_coordinates) ret.update(base) cache_keys.append(cache_key) return ret, hash(tuple(cache_keys)) @@ -499,8 +611,8 @@ def describe(self): class WebServiceContainer: def query( self, - coord_transform: _MatplotlibTransform, - size: Tuple[int, int], + graph: Graph, + parent_coordinates: str = "axes", ) -> Tuple[Dict[str, Any], Union[str, int]]: def hit_some_database(): {}, "1" diff --git a/data_prototype/conversion_edge.py b/data_prototype/conversion_edge.py index 242ddc4..af19c98 100644 --- a/data_prototype/conversion_edge.py +++ b/data_prototype/conversion_edge.py @@ -14,14 +14,14 @@ class Edge: name: str input: dict[str, Desc] output: dict[str, Desc] - invertable: bool = False + invertable: bool = True def evaluate(self, input: dict[str, Any]) -> dict[str, Any]: return input @property def inverse(self) -> "Edge": - raise NotImplementedError + return Edge(self.name + "_r", self.output, self.input) @dataclass @@ -42,14 +42,23 @@ def from_edges(cls, name: str, edges: Sequence[Edge], output: dict[str, Desc]): def evaluate(self, input: dict[str, Any]) -> dict[str, Any]: for edge in self.edges: - input |= edge.evaluate(**{k: input[k] for k in edge.input}) + print(input) + input |= edge.evaluate({k: input[k] for k in edge.input}) + print(input) return {k: input[k] for k in self.output} + @property + def inverse(self) -> "SequenceEdge": + return SequenceEdge.from_edges( + self.name + "_r", [e.inverse for e in self.edges[::-1]], self.input + ) + @dataclass class FuncEdge(Edge): # TODO: more explicit callable boundaries? func: Callable = lambda: {} + inverse_func: Callable | None = None @classmethod def from_func( @@ -58,6 +67,7 @@ def from_func( func: Callable, input: str | dict[str, Desc], output: str | dict[str, Desc], + inverse: Callable | None = None, ): # dtype/shape is reductive here, but I like the idea of being able to just # supply a function and the input/output coordinates for many things @@ -69,7 +79,7 @@ def from_func( if isinstance(output, str): output = {k: Desc(("N",), np.dtype("f8"), output) for k in input.keys()} - return cls(name, input, output, False, func) + return cls(name, input, output, inverse is not None, func, inverse) def evaluate(self, input: dict[str, Any]) -> dict[str, Any]: res = self.func(**{k: input[k] for k in self.input}) @@ -88,10 +98,16 @@ def evaluate(self, input: dict[str, Any]) -> dict[str, Any]: return {k: res for k in self.output} raise RuntimeError("Output of function does not match expected output") + @property + def inverse(self) -> "FuncEdge": + return FuncEdge.from_func( + self.name + "_r", self.inverse_func, self.output, self.input, self.func + ) + @dataclass class TransformEdge(Edge): - transform: Transform | None = None + transform: Transform | Callable[[], Transform] | None = None # TODO: helper for common cases/validation? @@ -101,10 +117,29 @@ def evaluate(self, input: dict[str, Any]) -> dict[str, Any]: # especially if initially given as stacked if self.transform is None: return input + elif isinstance(self.transform, Callable): + trf = self.transform() + else: + trf = self.transform inp = np.stack([input[k] for k in self.input], axis=-1) - outp = self.transform.transform(inp) + outp = trf.transform(inp) return {k: v for k, v in zip(self.output, outp.T)} + @property + def inverse(self) -> "TransformEdge": + if isinstance(self.transform, Callable): + return TransformEdge( + self.name + "_r", + self.output, + self.input, + True, + lambda: self.transform().inverted(), + ) + + return TransformEdge( + self.name + "_r", self.output, self.input, True, self.transform.inverted() + ) + class Graph: def __init__(self, edges: Sequence[Edge]): diff --git a/data_prototype/wrappers.py b/data_prototype/wrappers.py index faa29ef..7b2d420 100644 --- a/data_prototype/wrappers.py +++ b/data_prototype/wrappers.py @@ -18,7 +18,8 @@ ) from matplotlib.artist import Artist as _Artist -from data_prototype.containers import DataContainer, _MatplotlibTransform +from data_prototype.containers import DataContainer, _MatplotlibTransform, Desc +from data_prototype.conversion_edge import TransformEdge, Graph from data_prototype.conversion_node import ( ConversionNode, RenameConversionNode, @@ -131,19 +132,37 @@ def _query_and_transform(self, renderer) -> Dict[str, Any]: """ # extract what we need to about the axes to query the data ax = self.axes - # TODO do we want to trust the implicit renderer on the Axes? - ax_bbox = ax.get_window_extent(renderer) # actually query the underlying data. This returns both the (raw) data # and key to use for caching. - bb_size = ax_bbox.size - data, cache_key = self.data.query( - # TODO do this needs to be (de) unitized - # TODO figure out why caching this did not work - ax.transAxes - ax.transData, - # TODO find better way to placate mypy - (int(round(bb_size[0])), int(round(bb_size[1]))), - ) + edges = [ + TransformEdge( + "axes", + { + "x": Desc(("N",), np.dtype("f8"), coordinates="data"), + "y": Desc(("N",), np.dtype("f8"), coordinates="data"), + }, + { + "x": Desc(("N",), np.dtype("f8"), coordinates="axes"), + "y": Desc(("N",), np.dtype("f8"), coordinates="axes"), + }, + transform=ax.transData - ax.transAxes, + ), + TransformEdge( + "axes", + { + "x": Desc(("N",), np.dtype("f8"), coordinates="axes"), + "y": Desc(("N",), np.dtype("f8"), coordinates="axes"), + }, + { + "x": Desc(("N",), np.dtype("f8"), coordinates="display"), + "y": Desc(("N",), np.dtype("f8"), coordinates="display"), + }, + transform=ax.transAxes, + ), + ] + graph = Graph(edges) + data, cache_key = self.data.query(graph, "axes") # see if we can short-circuit try: return self._cache[cache_key] diff --git a/examples/animation.py b/examples/animation.py index 7c08a16..fa70874 100644 --- a/examples/animation.py +++ b/examples/animation.py @@ -16,7 +16,9 @@ import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation -from data_prototype.containers import _MatplotlibTransform, Desc +from data_prototype.containers import Desc +from data_prototype.conversion_edge import Graph + from data_prototype.conversion_node import FunctionConversionNode from data_prototype.wrappers import LineWrapper, FormattedText @@ -37,8 +39,8 @@ def describe(self): def query( self, - coord_transformtransform: _MatplotlibTransform, - size: Tuple[int, int], + graph: Graph, + parent_coordinates: str = "axes", ) -> Tuple[Dict[str, Any], Union[str, int]]: th = np.linspace(0, 2 * np.pi, self.N) diff --git a/examples/lissajous.py b/examples/lissajous.py index 6563e31..1c0cda9 100644 --- a/examples/lissajous.py +++ b/examples/lissajous.py @@ -18,7 +18,8 @@ import matplotlib.markers as mmarkers from matplotlib.animation import FuncAnimation -from data_prototype.containers import _MatplotlibTransform, Desc +from data_prototype.containers import Desc +from data_prototype.conversion_edge import Graph from data_prototype.wrappers import PathCollectionWrapper @@ -41,8 +42,8 @@ def describe(self): def query( self, - transform: _MatplotlibTransform, - size: Tuple[int, int], + graph: Graph, + parent_coordinates: str = "axes", ) -> Tuple[Dict[str, Any], Union[str, int]]: def next_time(): cur_time = time.time() diff --git a/examples/subsample.py b/examples/subsample.py index fc84c5a..83c8c06 100644 --- a/examples/subsample.py +++ b/examples/subsample.py @@ -21,7 +21,7 @@ import numpy as np from data_prototype.wrappers import ImageWrapper -from data_prototype.containers import _MatplotlibTransform, Desc +from data_prototype.containers import Desc from skimage.transform import downscale_local_mean @@ -45,10 +45,47 @@ def describe(self): def query( self, - coord_transform: _MatplotlibTransform, - size: Tuple[int, int], + graph, + parent_coordinates="axes", ) -> Tuple[Dict[str, Any], Union[str, int]]: - (x1, y1), (x2, y2) = coord_transform.transform([[0, 0], [1, 1]]) + data_lim = graph.evaluator( + { + "x": Desc( + ("N",), + np.dtype( + "f8", + ), + coordinates="data", + ), + "y": Desc( + ("N",), + np.dtype( + "f8", + ), + coordinates="data", + ), + }, + { + "x": Desc( + ("N",), + np.dtype( + "f8", + ), + coordinates=parent_coordinates, + ), + "y": Desc( + ("N",), + np.dtype( + "f8", + ), + coordinates=parent_coordinates, + ), + }, + ).inverse + + pts = data_lim.evaluate({"x": (0, 1), "y": (0, 1)}) + x1, x2 = pts["x"] + y1, y2 = pts["y"] xi1 = np.argmin(np.abs(x - x1)) yi1 = np.argmin(np.abs(y - y1)) diff --git a/examples/widgets.py b/examples/widgets.py index cfa25e3..2a9fc5d 100644 --- a/examples/widgets.py +++ b/examples/widgets.py @@ -23,7 +23,7 @@ def __init__(self, xfuncs, /, **sliders): self._sliders = sliders for slider in sliders.values(): slider.on_changed( - lambda x, sld=slider: sld.ax.figure.canvas.draw_idle(), + lambda _, sld=slider: sld.ax.figure.canvas.draw_idle(), ) def get_needed_keys(f, offset=1): @@ -43,8 +43,8 @@ def get_needed_keys(f, offset=1): }, ) - def _query_hash(self, coord_transform, size): - key = super()._query_hash(coord_transform, size) + def _query_hash(self, graph, parent_coordinates): + key = super()._query_hash(graph, parent_coordinates) # inject the slider values into the hashing logic return hash((key, tuple(s.val for s in self._sliders.values()))) @@ -111,7 +111,7 @@ def _query_hash(self, coord_transform, size): ), # the color data has to take the x (because reasons), but just # needs the phase - "color": ((1,), lambda t, phase: phase), + "color": ((1,), lambda _, phase: phase), }, # bind the sliders to the data container amplitude=amp_slider, @@ -133,7 +133,7 @@ def _query_hash(self, coord_transform, size): resetax = fig.add_axes([0.8, 0.025, 0.1, 0.04]) button = Button(resetax, "Reset", hovercolor="0.975") button.on_clicked( - lambda event: [sld.reset() for sld in (freq_slider, amp_slider, phase_slider)] + lambda _: [sld.reset() for sld in (freq_slider, amp_slider, phase_slider)] ) plt.show() From f01c02383dbbe0566038d69f903c03d651261c39 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Thu, 7 Mar 2024 13:29:41 -0600 Subject: [PATCH 5/7] Move Desc to its own file, add helper function to get similar descriptions --- data_prototype/containers.py | 234 ++--------------------- data_prototype/description.py | 133 +++++++++++++ data_prototype/tests/test_check_shape.py | 2 +- data_prototype/wrappers.py | 25 +-- examples/animation.py | 2 +- examples/lissajous.py | 2 +- examples/subsample.py | 39 +--- 7 files changed, 165 insertions(+), 272 deletions(-) create mode 100644 data_prototype/description.py diff --git a/data_prototype/containers.py b/data_prototype/containers.py index 4886810..f2c09d8 100644 --- a/data_prototype/containers.py +++ b/data_prototype/containers.py @@ -1,6 +1,5 @@ from __future__ import annotations -from dataclasses import dataclass from typing import ( Protocol, Dict, @@ -10,7 +9,6 @@ Union, Callable, MutableMapping, - TypeAlias, ) import uuid @@ -19,6 +17,8 @@ import numpy as np import pandas as pd +from .description import Desc, desc_like + from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -33,123 +33,6 @@ def __sub__(self, other) -> "_MatplotlibTransform": ... -ShapeSpec: TypeAlias = Tuple[Union[str, int], ...] - - -@dataclass(frozen=True) -class Desc: - # TODO: sort out how to actually spell this. We need to know: - # - what the number of dimensions is (1d vs 2d vs ...) - # - is this a fixed size dimension (e.g. 2 for xextent) - # - is this a variable size depending on the query (e.g. N) - # - what is the relative size to the other variable values (N vs N+1) - # We are probably going to have to implement a DSL for this (😞) - shape: ShapeSpec - dtype: np.dtype - coordinates: str = "naive" - - @staticmethod - def validate_shapes( - specification: dict[str, ShapeSpec | "Desc"], - actual: dict[str, ShapeSpec | "Desc"], - *, - broadcast: bool = False, - ) -> None: - """Validate specified shape relationships against a provided set of shapes. - - Shapes provided are tuples of int | str. If a specification calls for an int, - the exact size is expected. - If it is a str, it must be a single capital letter optionally followed by ``+`` - or ``-`` an integer value. - The same letter used in the specification must represent the same value in all - appearances. The value may, however, be a variable (with an offset) in the - actual shapes (which does not need to have the same letter). - - Shapes may be provided as raw tuples or as ``Desc`` objects. - - Parameters - ---------- - specification: dict[str, ShapeSpec | "Desc"] - The desired shape relationships - actual: dict[str, ShapeSpec | "Desc"] - The shapes to test for compliance - - Keyword Parameters - ------------------ - broadcast: bool - Whether to allow broadcasted shapes to pass (i.e. actual shapes with a ``1`` - will not cause exceptions regardless of what the specified shape value is) - - Raises - ------ - KeyError: - If a required field from the specification is missing in the provided actual - values. - ValueError: - If shapes are incompatible in any other way - """ - specvars: dict[str, int | tuple[str, int]] = {} - for fieldname in specification: - spec = specification[fieldname] - if fieldname not in actual: - raise KeyError( - f"Actual is missing {fieldname!r}, required by specification." - ) - desc = actual[fieldname] - if isinstance(spec, Desc): - spec = spec.shape - if isinstance(desc, Desc): - desc = desc.shape - if not broadcast: - if len(spec) != len(desc): - raise ValueError( - f"{fieldname!r} shape {desc} incompatible with specification " - f"{spec}." - ) - elif len(desc) > len(spec): - raise ValueError( - f"{fieldname!r} shape {desc} incompatible with specification " - f"{spec}." - ) - for speccomp, desccomp in zip(spec[::-1], desc[::-1]): - if broadcast and desccomp == 1: - continue - if isinstance(speccomp, str): - specv, specoff = speccomp[0], int(speccomp[1:] or 0) - - if isinstance(desccomp, str): - descv, descoff = desccomp[0], int(desccomp[1:] or 0) - entry = (descv, descoff - specoff) - else: - entry = desccomp - specoff - - if specv in specvars and entry != specvars[specv]: - raise ValueError(f"Found two incompatible values for {specv!r}") - - specvars[specv] = entry - elif speccomp != desccomp: - raise ValueError( - f"{fieldname!r} shape {desc} incompatible with specification " - f"{spec}" - ) - return None - - @staticmethod - def compatible(a: dict[str, Desc], b: dict[str, Desc]) -> bool: - """Determine if ``a`` is a valid input for ``b``. - - Note: ``a`` _may_ have additional keys. - """ - try: - Desc.validate_shapes(b, a) - except (KeyError, ValueError): - return False - for k, v in b.items(): - if a[k].coordinates != v.coordinates: - return False - return True - - class DataContainer(Protocol): def query( self, @@ -184,6 +67,7 @@ def query( This is a key that clients can use to cache down-stream computations on this data. """ + ... def describe(self) -> Dict[str, Desc]: """ @@ -193,6 +77,7 @@ def describe(self) -> Dict[str, Desc]: ------- Dict[str, Desc] """ + ... class NoNewKeys(ValueError): @@ -312,73 +197,16 @@ def query( # if hash_key in self._cache: # return self._cache[hash_key], hash_key + desc = Desc(("N",), np.dtype("f8")) + xy = {"x": desc, "y": desc} data_lim = graph.evaluator( - { - "x": Desc( - ("N",), - np.dtype( - "f8", - ), - coordinates="data", - ), - "y": Desc( - ("N",), - np.dtype( - "f8", - ), - coordinates="data", - ), - }, - { - "x": Desc( - ("N",), - np.dtype( - "f8", - ), - coordinates=parent_coordinates, - ), - "y": Desc( - ("N",), - np.dtype( - "f8", - ), - coordinates=parent_coordinates, - ), - }, + desc_like(xy, coordinates="data"), + desc_like(xy, coordinates=parent_coordinates), ).inverse + screen_size = graph.evaluator( - { - "x": Desc( - ("N",), - np.dtype( - "f8", - ), - coordinates=parent_coordinates, - ), - "y": Desc( - ("N",), - np.dtype( - "f8", - ), - coordinates=parent_coordinates, - ), - }, - { - "x": Desc( - ("N",), - np.dtype( - "f8", - ), - coordinates="display", - ), - "y": Desc( - ("N",), - np.dtype( - "f8", - ), - coordinates="display", - ), - }, + desc_like(xy, coordinates=parent_coordinates), + desc_like(xy, coordinates="display"), ) screen_dims = screen_size.evaluate({"x": [0, 1], "y": [0, 1]}) @@ -429,39 +257,11 @@ def query( ) -> Tuple[Dict[str, Any], Union[str, int]]: dmin, dmax = self._full_range + desc = Desc(("N",), np.dtype("f8")) + xy = {"x": desc, "y": desc} data_lim = graph.evaluator( - { - "x": Desc( - ("N",), - np.dtype( - "f8", - ), - coordinates="data", - ), - "y": Desc( - ("N",), - np.dtype( - "f8", - ), - coordinates="data", - ), - }, - { - "x": Desc( - ("N",), - np.dtype( - "f8", - ), - coordinates=parent_coordinates, - ), - "y": Desc( - ("N",), - np.dtype( - "f8", - ), - coordinates=parent_coordinates, - ), - }, + desc_like(xy, coordinates="data"), + desc_like(xy, coordinates=parent_coordinates), ).inverse pts = data_lim.evaluate({"x": (0, 1), "y": (0, 1)}) @@ -493,7 +293,7 @@ def describe(self) -> Dict[str, Desc]: class SeriesContainer: - _data: pd.DataFrame + _data: pd.Series _index_name: str _hash_key: str @@ -615,7 +415,7 @@ def query( parent_coordinates: str = "axes", ) -> Tuple[Dict[str, Any], Union[str, int]]: def hit_some_database(): - {}, "1" + return {}, "1" data, etag = hit_some_database() return data, etag diff --git a/data_prototype/description.py b/data_prototype/description.py new file mode 100644 index 0000000..a963a92 --- /dev/null +++ b/data_prototype/description.py @@ -0,0 +1,133 @@ +from dataclasses import dataclass +from typing import TypeAlias, Tuple, Union + +import numpy as np + + +ShapeSpec: TypeAlias = Tuple[Union[str, int], ...] + + +@dataclass(frozen=True) +class Desc: + # TODO: sort out how to actually spell this. We need to know: + # - what the number of dimensions is (1d vs 2d vs ...) + # - is this a fixed size dimension (e.g. 2 for xextent) + # - is this a variable size depending on the query (e.g. N) + # - what is the relative size to the other variable values (N vs N+1) + # We are probably going to have to implement a DSL for this (😞) + shape: ShapeSpec + dtype: np.dtype + coordinates: str = "naive" + + @staticmethod + def validate_shapes( + specification: dict[str, ShapeSpec | "Desc"], + actual: dict[str, ShapeSpec | "Desc"], + *, + broadcast: bool = False, + ) -> None: + """Validate specified shape relationships against a provided set of shapes. + + Shapes provided are tuples of int | str. If a specification calls for an int, + the exact size is expected. + If it is a str, it must be a single capital letter optionally followed by ``+`` + or ``-`` an integer value. + The same letter used in the specification must represent the same value in all + appearances. The value may, however, be a variable (with an offset) in the + actual shapes (which does not need to have the same letter). + + Shapes may be provided as raw tuples or as ``Desc`` objects. + + Parameters + ---------- + specification: dict[str, ShapeSpec | "Desc"] + The desired shape relationships + actual: dict[str, ShapeSpec | "Desc"] + The shapes to test for compliance + + Keyword Parameters + ------------------ + broadcast: bool + Whether to allow broadcasted shapes to pass (i.e. actual shapes with a ``1`` + will not cause exceptions regardless of what the specified shape value is) + + Raises + ------ + KeyError: + If a required field from the specification is missing in the provided actual + values. + ValueError: + If shapes are incompatible in any other way + """ + specvars: dict[str, int | tuple[str, int]] = {} + for fieldname in specification: + spec = specification[fieldname] + if fieldname not in actual: + raise KeyError( + f"Actual is missing {fieldname!r}, required by specification." + ) + desc = actual[fieldname] + if isinstance(spec, Desc): + spec = spec.shape + if isinstance(desc, Desc): + desc = desc.shape + if not broadcast: + if len(spec) != len(desc): + raise ValueError( + f"{fieldname!r} shape {desc} incompatible with specification " + f"{spec}." + ) + elif len(desc) > len(spec): + raise ValueError( + f"{fieldname!r} shape {desc} incompatible with specification " + f"{spec}." + ) + for speccomp, desccomp in zip(spec[::-1], desc[::-1]): + if broadcast and desccomp == 1: + continue + if isinstance(speccomp, str): + specv, specoff = speccomp[0], int(speccomp[1:] or 0) + + if isinstance(desccomp, str): + descv, descoff = desccomp[0], int(desccomp[1:] or 0) + entry = (descv, descoff - specoff) + else: + entry = desccomp - specoff + + if specv in specvars and entry != specvars[specv]: + raise ValueError(f"Found two incompatible values for {specv!r}") + + specvars[specv] = entry + elif speccomp != desccomp: + raise ValueError( + f"{fieldname!r} shape {desc} incompatible with specification " + f"{spec}" + ) + return None + + @staticmethod + def compatible(a: dict[str, "Desc"], b: dict[str, "Desc"]) -> bool: + """Determine if ``a`` is a valid input for ``b``. + + Note: ``a`` _may_ have additional keys. + """ + try: + Desc.validate_shapes(b, a) + except (KeyError, ValueError): + return False + for k, v in b.items(): + if a[k].coordinates != v.coordinates: + return False + return True + + +def desc_like(desc, shape=None, dtype=None, coordinates=None): + if isinstance(desc, dict): + return {k: desc_like(v, shape, dtype, coordinates) for k, v in desc.items()} + if shape is None: + shape = desc.shape + if dtype is None: + dtype = desc.dtype + if coordinates is None: + coordinates = desc.coordinates + return Desc(shape, dtype, coordinates) diff --git a/data_prototype/tests/test_check_shape.py b/data_prototype/tests/test_check_shape.py index 0f8d6bb..f499761 100644 --- a/data_prototype/tests/test_check_shape.py +++ b/data_prototype/tests/test_check_shape.py @@ -1,6 +1,6 @@ import pytest -from data_prototype.containers import Desc +from data_prototype.description import Desc @pytest.mark.parametrize( diff --git a/data_prototype/wrappers.py b/data_prototype/wrappers.py index 7b2d420..e93916d 100644 --- a/data_prototype/wrappers.py +++ b/data_prototype/wrappers.py @@ -18,7 +18,8 @@ ) from matplotlib.artist import Artist as _Artist -from data_prototype.containers import DataContainer, _MatplotlibTransform, Desc +from data_prototype.containers import DataContainer, _MatplotlibTransform +from data_prototype.description import Desc, desc_like from data_prototype.conversion_edge import TransformEdge, Graph from data_prototype.conversion_node import ( ConversionNode, @@ -135,29 +136,19 @@ def _query_and_transform(self, renderer) -> Dict[str, Any]: # actually query the underlying data. This returns both the (raw) data # and key to use for caching. + desc = Desc(("N",), np.dtype("f8"), coordinates="data") + xy = {"x": desc, "y": desc} edges = [ TransformEdge( "axes", - { - "x": Desc(("N",), np.dtype("f8"), coordinates="data"), - "y": Desc(("N",), np.dtype("f8"), coordinates="data"), - }, - { - "x": Desc(("N",), np.dtype("f8"), coordinates="axes"), - "y": Desc(("N",), np.dtype("f8"), coordinates="axes"), - }, + xy, + desc_like(xy, coordinates="axes"), transform=ax.transData - ax.transAxes, ), TransformEdge( "axes", - { - "x": Desc(("N",), np.dtype("f8"), coordinates="axes"), - "y": Desc(("N",), np.dtype("f8"), coordinates="axes"), - }, - { - "x": Desc(("N",), np.dtype("f8"), coordinates="display"), - "y": Desc(("N",), np.dtype("f8"), coordinates="display"), - }, + desc_like(xy, coordinates="axes"), + desc_like(xy, coordinates="display"), transform=ax.transAxes, ), ] diff --git a/examples/animation.py b/examples/animation.py index fa70874..5ca5a17 100644 --- a/examples/animation.py +++ b/examples/animation.py @@ -16,8 +16,8 @@ import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation -from data_prototype.containers import Desc from data_prototype.conversion_edge import Graph +from data_prototype.description import Desc from data_prototype.conversion_node import FunctionConversionNode diff --git a/examples/lissajous.py b/examples/lissajous.py index 1c0cda9..5082f03 100644 --- a/examples/lissajous.py +++ b/examples/lissajous.py @@ -18,8 +18,8 @@ import matplotlib.markers as mmarkers from matplotlib.animation import FuncAnimation -from data_prototype.containers import Desc from data_prototype.conversion_edge import Graph +from data_prototype.description import Desc from data_prototype.wrappers import PathCollectionWrapper diff --git a/examples/subsample.py b/examples/subsample.py index 83c8c06..b963d88 100644 --- a/examples/subsample.py +++ b/examples/subsample.py @@ -20,8 +20,8 @@ import numpy as np +from data_prototype.description import Desc, desc_like from data_prototype.wrappers import ImageWrapper -from data_prototype.containers import Desc from skimage.transform import downscale_local_mean @@ -48,40 +48,9 @@ def query( graph, parent_coordinates="axes", ) -> Tuple[Dict[str, Any], Union[str, int]]: - data_lim = graph.evaluator( - { - "x": Desc( - ("N",), - np.dtype( - "f8", - ), - coordinates="data", - ), - "y": Desc( - ("N",), - np.dtype( - "f8", - ), - coordinates="data", - ), - }, - { - "x": Desc( - ("N",), - np.dtype( - "f8", - ), - coordinates=parent_coordinates, - ), - "y": Desc( - ("N",), - np.dtype( - "f8", - ), - coordinates=parent_coordinates, - ), - }, - ).inverse + desc = Desc(("N",), np.dtype("f8"), coordinates="data") + xy = {"x": desc, "y": desc} + data_lim = graph.evaluator(xy, desc_like(xy, coordinates="axes")).inverse pts = data_lim.evaluate({"x": (0, 1), "y": (0, 1)}) x1, x2 = pts["x"] From a929e7ddb40c9956d7e2a233a41fbc3f371a54d5 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Thu, 7 Mar 2024 13:32:05 -0600 Subject: [PATCH 6/7] Update precommit, black version was out of sync with CI --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8927e46..1a7887e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ default_language_version: python: python3 repos: - repo: https://github.com/ambv/black - rev: 23.3.0 + rev: 24.2.0 hooks: - id: black - repo: https://github.com/pre-commit/pre-commit-hooks @@ -10,6 +10,6 @@ repos: hooks: - id: flake8 - repo: https://github.com/kynan/nbstripout - rev: 0.6.1 + rev: 0.7.1 hooks: - id: nbstripout From 3c0c6ffcc287d05355a5c14d21e5cf78370a131a Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Thu, 7 Mar 2024 13:34:14 -0600 Subject: [PATCH 7/7] reblacken --- data_prototype/axes.py | 28 +++++++++++++++++----------- data_prototype/containers.py | 17 ++++++++--------- data_prototype/wrappers.py | 12 ++++-------- examples/animation.py | 1 + examples/lissajous.py | 1 + examples/scatter_with_custom_axes.py | 1 - examples/simple_patch.py | 1 + examples/simple_scatter.py | 1 + examples/units.py | 1 + examples/widgets.py | 1 + 10 files changed, 35 insertions(+), 29 deletions(-) diff --git a/data_prototype/axes.py b/data_prototype/axes.py index 3b9dd48..5659701 100644 --- a/data_prototype/axes.py +++ b/data_prototype/axes.py @@ -102,22 +102,28 @@ def scatter( pipeline.append(lambda x: np.ma.ravel(x)) pipeline.append(lambda y: np.ma.ravel(y)) pipeline.append( - lambda s: np.ma.ravel(s) - if s is not None - else [20] - if mpl.rcParams["_internal.classic_mode"] - else [mpl.rcParams["lines.markersize"] ** 2.0] + lambda s: ( + np.ma.ravel(s) + if s is not None + else ( + [20] + if mpl.rcParams["_internal.classic_mode"] + else [mpl.rcParams["lines.markersize"] ** 2.0] + ) + ) ) # TODO plotnonfinite/mask combining pipeline.append( - lambda marker: marker - if marker is not None - else mpl.rcParams["scatter.marker"] + lambda marker: ( + marker if marker is not None else mpl.rcParams["scatter.marker"] + ) ) pipeline.append( - lambda marker: marker - if isinstance(marker, mmarkers.MarkerStyle) - else mmarkers.MarkerStyle(marker) + lambda marker: ( + marker + if isinstance(marker, mmarkers.MarkerStyle) + else mmarkers.MarkerStyle(marker) + ) ) pipeline.append( FunctionConversionNode.from_funcs( diff --git a/data_prototype/containers.py b/data_prototype/containers.py index f2c09d8..9640e01 100644 --- a/data_prototype/containers.py +++ b/data_prototype/containers.py @@ -26,11 +26,9 @@ class _MatplotlibTransform(Protocol): - def transform(self, verts): - ... + def transform(self, verts): ... - def __sub__(self, other) -> "_MatplotlibTransform": - ... + def __sub__(self, other) -> "_MatplotlibTransform": ... class DataContainer(Protocol): @@ -80,8 +78,7 @@ def describe(self) -> Dict[str, Desc]: ... -class NoNewKeys(ValueError): - ... +class NoNewKeys(ValueError): ... class ArrayContainer: @@ -89,9 +86,11 @@ def __init__(self, **data): self._data = data self._cache_key = str(uuid.uuid4()) self._desc = { - k: Desc(v.shape, v.dtype) - if isinstance(v, np.ndarray) - else Desc((), type(v)) + k: ( + Desc(v.shape, v.dtype) + if isinstance(v, np.ndarray) + else Desc((), type(v)) + ) for k, v in data.items() } diff --git a/data_prototype/wrappers.py b/data_prototype/wrappers.py index e93916d..e998dfa 100644 --- a/data_prototype/wrappers.py +++ b/data_prototype/wrappers.py @@ -35,8 +35,7 @@ class _BBox(Protocol): class _Axis(Protocol): - def convert_units(self, Any) -> Any: - ... + def convert_units(self, Any) -> Any: ... class _Axes(Protocol): @@ -46,14 +45,11 @@ class _Axes(Protocol): transData: _MatplotlibTransform transAxes: _MatplotlibTransform - def get_xlim(self) -> Tuple[float, float]: - ... + def get_xlim(self) -> Tuple[float, float]: ... - def get_ylim(self) -> Tuple[float, float]: - ... + def get_ylim(self) -> Tuple[float, float]: ... - def get_window_extent(self, renderer) -> _BBox: - ... + def get_window_extent(self, renderer) -> _BBox: ... class _Aritst(Protocol): diff --git a/examples/animation.py b/examples/animation.py index 5ca5a17..70f0d25 100644 --- a/examples/animation.py +++ b/examples/animation.py @@ -7,6 +7,7 @@ :class:`.wrappers.LineWrapper`, and :class:`.wrappers.FormattedText`. """ + import time from typing import Dict, Tuple, Any, Union from functools import partial diff --git a/examples/lissajous.py b/examples/lissajous.py index 5082f03..c6b7865 100644 --- a/examples/lissajous.py +++ b/examples/lissajous.py @@ -8,6 +8,7 @@ An animated scatter plot using a custom container and :class:`.wrappers.PathCollectionWrapper` """ + import time from typing import Dict, Tuple, Any, Union from functools import partial diff --git a/examples/scatter_with_custom_axes.py b/examples/scatter_with_custom_axes.py index aa37c76..a5122d4 100644 --- a/examples/scatter_with_custom_axes.py +++ b/examples/scatter_with_custom_axes.py @@ -11,7 +11,6 @@ implementation of container-based artist drawing. """ - import data_prototype.axes # side-effect registers projection # noqa import matplotlib.pyplot as plt diff --git a/examples/simple_patch.py b/examples/simple_patch.py index 48f1c2a..91bce7c 100644 --- a/examples/simple_patch.py +++ b/examples/simple_patch.py @@ -8,6 +8,7 @@ :class:`.containers.ArrayContainer`. """ + import numpy as np import matplotlib.pyplot as plt diff --git a/examples/simple_scatter.py b/examples/simple_scatter.py index 4979a8b..9b3fd90 100644 --- a/examples/simple_scatter.py +++ b/examples/simple_scatter.py @@ -6,6 +6,7 @@ A quick scatter plot using :class:`.containers.ArrayContainer` and :class:`.wrappers.PathCollectionWrapper`. """ + import numpy as np import matplotlib.pyplot as plt diff --git a/examples/units.py b/examples/units.py index ae6702b..42c654d 100644 --- a/examples/units.py +++ b/examples/units.py @@ -5,6 +5,7 @@ Using third party units functionality in conjunction with Matplotlib Axes """ + import numpy as np import matplotlib.pyplot as plt diff --git a/examples/widgets.py b/examples/widgets.py index 2a9fc5d..0e12f0b 100644 --- a/examples/widgets.py +++ b/examples/widgets.py @@ -7,6 +7,7 @@ a sine wave. """ + import inspect import numpy as np