From 8ee088273a3ab22250b6d0eab49b94e075c11741 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 3 Nov 2022 21:31:46 -0400 Subject: [PATCH 01/11] ENH: make all of the properties on LineWrapper mappable --- data_prototype/wrappers.py | 3 +- examples/mapped.py | 57 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 examples/mapped.py diff --git a/data_prototype/wrappers.py b/data_prototype/wrappers.py index 2d4875b..0542b38 100644 --- a/data_prototype/wrappers.py +++ b/data_prototype/wrappers.py @@ -187,7 +187,8 @@ def draw(self, renderer): return self._wrapped_instance.draw(renderer) def _update_wrapped(self, data): - self._wrapped_instance.set_data(data["x"], data["y"]) + for k, v in data.items(): + getattr(self._wrapped_instance, f"set_{k}")(v) class ImageWrapper(ProxyWrapper): diff --git a/examples/mapped.py b/examples/mapped.py new file mode 100644 index 0000000..56321d9 --- /dev/null +++ b/examples/mapped.py @@ -0,0 +1,57 @@ +""" +======================= +Mapping Line Properties +======================= + +Leveraging the nu functions to transform users space data to visualization data. + +""" + +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd + +from matplotlib.colors import Normalize + +from data_prototype.wrappers import LineWrapper +from data_prototype.containers import ArrayContainer + +cmap = plt.colormaps["viridis"] +cmap.set_over("k") +cmap.set_under("r") +norm = Normalize(1, 8) + +nus = { + # arbitrary functions + "lw": lambda lw: min(1 + lw, 5), + # standard color mapping + "color": lambda color: cmap(norm(color)), + # categorical + "ls": lambda ls: {"A": "-", "B": ":", "C": "--"}[ls[()]], +} + +th = np.linspace(0, 2 * np.pi, 128) +delta = np.pi / 9 + +fig, ax = plt.subplots() + +for j in range(10): + ax.add_artist( + LineWrapper( + ArrayContainer( + **{ + "x": th, + "y": np.sin(th + j * delta) + j, + "color": np.asarray(j), + "lw": np.asarray(j), + "ls": np.asarray({0: "A", 1: "B", 2: "C"}[j % 3]), + } + ), + nus, + ) + ) + +ax.set_xlim(0, np.pi * 2) +ax.set_ylim(-1.1, 10.1) + +plt.show() From 4ad3c49acf96a2de0224307360a2cbb56b0106c2 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 4 Nov 2022 11:54:32 -0400 Subject: [PATCH 02/11] DOC: update logo unbreaks doc build for me locally --- docs/source/_static/logo2.svg | 552 ++++++++++++++++++++++++++++++++++ docs/source/conf.py | 6 +- 2 files changed, 556 insertions(+), 2 deletions(-) create mode 100644 docs/source/_static/logo2.svg diff --git a/docs/source/_static/logo2.svg b/docs/source/_static/logo2.svg new file mode 100644 index 0000000..f2d289c --- /dev/null +++ b/docs/source/_static/logo2.svg @@ -0,0 +1,552 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/source/conf.py b/docs/source/conf.py index 38ee482..e3644ff 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -163,8 +163,10 @@ def matplotlib_reduced_latex_scraper(block, block_vars, gallery_conf, **kwargs): # further. For a list of options available for each theme, see the # documentation. # - -html_theme_options = {"logo": {}} +html_logo = "_static/logo2.svg" +html_theme_options = { + "logo": {"link": "index", "image_light": "images/logo2.svg", "image_dark": "images/logo_dark.svg"}, +} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, From 23cd0674b38bbdb18e52ab484edd7afdc50fb506 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 4 Nov 2022 11:55:42 -0400 Subject: [PATCH 03/11] ENH: make the nus functions more filtering and powerful Make it possible to rename as part of the transforms --- data_prototype/wrappers.py | 41 ++++++++++++++++++++++++++++++-------- examples/animation.py | 2 +- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/data_prototype/wrappers.py b/data_prototype/wrappers.py index 0542b38..0a55227 100644 --- a/data_prototype/wrappers.py +++ b/data_prototype/wrappers.py @@ -1,4 +1,5 @@ from typing import List, Dict, Any, Protocol, Tuple, get_type_hints +import inspect import numpy as np @@ -88,6 +89,7 @@ class ProxyWrapperBase: data: DataContainer axes: _Axes stale: bool + required_keys: set = set() @_stale_wrapper def draw(self, renderer): @@ -137,18 +139,38 @@ def _query_and_transform(self, renderer, *, xunits: List[str], yunits: List[str] # doing the nu work here is nice because we can write it once, but we # really want to push this computation down a layer # TODO sort out how this interoperates with the transform stack - data = {k: self.nus.get(k, lambda x: x)(v) for k, v in data.items()} - self._cache[cache_key] = data - return data + transformed_data = {} + for k, (nu, sig) in self._sigs.items(): + to_pass = set(sig.parameters) + transformed_data[k] = nu(**{k: data[k] for k in to_pass}) + self._cache[cache_key] = transformed_data + return transformed_data def __init__(self, data, nus, **kwargs): super().__init__(**kwargs) self.data = data self._cache = LFUCache(64) # TODO make sure mutating this will invalidate the cache! - self.nus = nus or {} + self._nus = nus or {} + for k in self.required_keys: + + def identity(**kwargs): + (_,) = kwargs.values() + return _ + + identity.__signature__ = inspect.Signature( + [inspect.Parameter(k, inspect.Parameter.POSITIONAL_OR_KEYWORD)] + ) + + self._nus.setdefault(k, identity) + self._sigs = {k: (nu, inspect.signature(nu)) for k, nu in self._nus.items()} self.stale = True + # TODO add a setter + @property + def nus(self): + return dict(self._nus) + class ProxyWrapper(ProxyWrapperBase): _privtized_methods: Tuple[str, ...] = () @@ -163,7 +185,7 @@ def __getattr__(self, key): return getattr(self._wrapped_instance, key) def __setattr__(self, key, value): - if key in ("_wrapped_instance", "data", "_cache", "nus", "stale"): + if key in ("_wrapped_instance", "data", "_cache", "_nus", "stale", "_sigs"): super().__setattr__(key, value) elif hasattr(self, "_wrapped_instance") and hasattr(self._wrapped_instance, key): setattr(self._wrapped_instance, key, value) @@ -174,6 +196,7 @@ def __setattr__(self, key, value): class LineWrapper(ProxyWrapper): _wrapped_class = _Line2D _privtized_methods = ("set_xdata", "set_ydata", "set_data", "get_xdata", "get_ydata", "get_data") + required_keys = {"x", "y"} def __init__(self, data: DataContainer, nus=None, /, **kwargs): super().__init__(data, nus) @@ -188,6 +211,7 @@ def draw(self, renderer): def _update_wrapped(self, data): for k, v in data.items(): + k = {"x": "xdata", "y": "ydata"}.get(k, k) getattr(self._wrapped_instance, f"set_{k}")(v) @@ -244,10 +268,9 @@ class FormatedText(ProxyWrapper): _wrapped_class = _Text _privtized_methods = ("set_text",) - def __init__(self, data: DataContainer, format_func, nus=None, /, **kwargs): + def __init__(self, data: DataContainer, nus=None, /, **kwargs): super().__init__(data, nus) self._wrapped_instance = self._wrapped_class(text="", **kwargs) - self._format_func = format_func @_stale_wrapper def draw(self, renderer): @@ -257,7 +280,9 @@ def draw(self, renderer): return self._wrapped_instance.draw(renderer) def _update_wrapped(self, data): - self._wrapped_instance.set_text(self._format_func(**data)) + for k, v in data.items(): + k = {"x": "xdata", "y": "ydata"}.get(k, k) + getattr(self._wrapped_instance, f"set_{k}")(v) @_forwarder( diff --git a/examples/animation.py b/examples/animation.py index 4468eb8..34c89d0 100644 --- a/examples/animation.py +++ b/examples/animation.py @@ -61,7 +61,7 @@ def update(frame, art): lw = LineWrapper(sot_c, lw=5, color="green", label="sin(time)") fc = FormatedText( sot_c, - "ϕ={phase:.2f} ".format, + {'text': lambda phase: f"ϕ={phase:.2f}"}, x=2 * np.pi, y=1, ha="right", From cc7a67f9c33a025b6fccdc3d532b42bdf5f41ec1 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 4 Nov 2022 11:58:46 -0400 Subject: [PATCH 04/11] MNT: remove a print --- data_prototype/wrappers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/data_prototype/wrappers.py b/data_prototype/wrappers.py index 0a55227..28203c1 100644 --- a/data_prototype/wrappers.py +++ b/data_prototype/wrappers.py @@ -219,7 +219,6 @@ class ImageWrapper(ProxyWrapper): _wrapped_class = _AxesImage def __init__(self, data: DataContainer, nus=None, /, cmap=None, norm=None, **kwargs): - print(kwargs, nus) nus = dict(nus or {}) if cmap is not None or norm is not None: if nus is not None and "image" in nus: From 0eac35e8b8f6c3a0d9d7795dfe1545f93afedb1c Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 4 Nov 2022 12:09:02 -0400 Subject: [PATCH 05/11] ENH: show off using multiple inputs + renaming --- data_prototype/wrappers.py | 1 - examples/mapped.py | 35 ++++++++++++++++++++++------------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/data_prototype/wrappers.py b/data_prototype/wrappers.py index 28203c1..b4a5b7b 100644 --- a/data_prototype/wrappers.py +++ b/data_prototype/wrappers.py @@ -280,7 +280,6 @@ def draw(self, renderer): def _update_wrapped(self, data): for k, v in data.items(): - k = {"x": "xdata", "y": "ydata"}.get(k, k) getattr(self._wrapped_instance, f"set_{k}")(v) diff --git a/examples/mapped.py b/examples/mapped.py index 56321d9..d7a5076 100644 --- a/examples/mapped.py +++ b/examples/mapped.py @@ -13,7 +13,7 @@ from matplotlib.colors import Normalize -from data_prototype.wrappers import LineWrapper +from data_prototype.wrappers import LineWrapper, FormatedText from data_prototype.containers import ArrayContainer cmap = plt.colormaps["viridis"] @@ -25,9 +25,9 @@ # arbitrary functions "lw": lambda lw: min(1 + lw, 5), # standard color mapping - "color": lambda color: cmap(norm(color)), + "color": lambda j: cmap(norm(j)), # categorical - "ls": lambda ls: {"A": "-", "B": ":", "C": "--"}[ls[()]], + "ls": lambda cat: {"A": "-", "B": ":", "C": "--"}[cat[()]], } th = np.linspace(0, 2 * np.pi, 128) @@ -36,21 +36,30 @@ fig, ax = plt.subplots() for j in range(10): + ac = ArrayContainer( + **{ + "x": th, + "y": np.sin(th + j * delta) + j, + "j": np.asarray(j), + "lw": np.asarray(j), + "cat": np.asarray({0: "A", 1: "B", 2: "C"}[j % 3]), + } + ) ax.add_artist( LineWrapper( - ArrayContainer( - **{ - "x": th, - "y": np.sin(th + j * delta) + j, - "color": np.asarray(j), - "lw": np.asarray(j), - "ls": np.asarray({0: "A", 1: "B", 2: "C"}[j % 3]), - } - ), + ac, nus, ) ) - + ax.add_artist( + FormatedText( + ac, + {"text": lambda j, cat: f"index={j[()]} class={cat[()]!r}", "y": lambda j: j}, + x=2 * np.pi, + ha="right", + bbox={"facecolor": "gray", "alpha": 0.5}, + ) + ) ax.set_xlim(0, np.pi * 2) ax.set_ylim(-1.1, 10.1) From 98282e00572c522ee0fd3917fe4fe7bc9e2a711c Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 4 Nov 2022 12:16:03 -0400 Subject: [PATCH 06/11] MNT: add required keys to steps and image --- data_prototype/wrappers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/data_prototype/wrappers.py b/data_prototype/wrappers.py index b4a5b7b..aa570ee 100644 --- a/data_prototype/wrappers.py +++ b/data_prototype/wrappers.py @@ -217,6 +217,7 @@ def _update_wrapped(self, data): class ImageWrapper(ProxyWrapper): _wrapped_class = _AxesImage + required_keys = {"xextent", "yextent", "image"} def __init__(self, data: DataContainer, nus=None, /, cmap=None, norm=None, **kwargs): nus = dict(nus or {}) @@ -247,6 +248,7 @@ def _update_wrapped(self, data): class StepWrapper(ProxyWrapper): _wrapped_class = _StepPatch _privtized_methods = () # ("set_data", "get_data") + required_keys = {"edges", "density"} def __init__(self, data: DataContainer, nus=None, /, **kwargs): super().__init__(data, nus) From 4e9276db5622abe781ca496193c70f2384ac5d86 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 4 Nov 2022 12:16:14 -0400 Subject: [PATCH 07/11] ENH: pass any "extra" data through with assumed unity transform --- data_prototype/wrappers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data_prototype/wrappers.py b/data_prototype/wrappers.py index aa570ee..0a9bb30 100644 --- a/data_prototype/wrappers.py +++ b/data_prototype/wrappers.py @@ -143,6 +143,9 @@ def _query_and_transform(self, renderer, *, xunits: List[str], yunits: List[str] for k, (nu, sig) in self._sigs.items(): to_pass = set(sig.parameters) transformed_data[k] = nu(**{k: data[k] for k in to_pass}) + for k, v in data.items(): + transformed_data.setdefault(k, v) + self._cache[cache_key] = transformed_data return transformed_data From 96c50f50c45dee2af1226ce3e16271f943149b05 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 4 Nov 2022 12:24:29 -0400 Subject: [PATCH 08/11] ENH: being permissive in passing data was a bad idea Introduce "expected keys" --- data_prototype/wrappers.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/data_prototype/wrappers.py b/data_prototype/wrappers.py index 0a9bb30..66bbc33 100644 --- a/data_prototype/wrappers.py +++ b/data_prototype/wrappers.py @@ -47,6 +47,15 @@ class _Aritst(Protocol): axes: _Axes +def _make_identity(k): + def identity(**kwargs): + (_,) = kwargs.values() + return _ + + identity.__signature__ = inspect.Signature([inspect.Parameter(k, inspect.Parameter.POSITIONAL_OR_KEYWORD)]) + return identity + + def _forwarder(forwards, cls=None): if cls is None: return partial(_forwarder, forwards) @@ -90,6 +99,7 @@ class ProxyWrapperBase: axes: _Axes stale: bool required_keys: set = set() + expected_keys: set = set() @_stale_wrapper def draw(self, renderer): @@ -143,8 +153,6 @@ def _query_and_transform(self, renderer, *, xunits: List[str], yunits: List[str] for k, (nu, sig) in self._sigs.items(): to_pass = set(sig.parameters) transformed_data[k] = nu(**{k: data[k] for k in to_pass}) - for k, v in data.items(): - transformed_data.setdefault(k, v) self._cache[cache_key] = transformed_data return transformed_data @@ -156,16 +164,11 @@ def __init__(self, data, nus, **kwargs): # TODO make sure mutating this will invalidate the cache! self._nus = nus or {} for k in self.required_keys: - - def identity(**kwargs): - (_,) = kwargs.values() - return _ - - identity.__signature__ = inspect.Signature( - [inspect.Parameter(k, inspect.Parameter.POSITIONAL_OR_KEYWORD)] - ) - - self._nus.setdefault(k, identity) + self._nus.setdefault(k, _make_identity(k)) + desc = data.describe() + for k in self.expected_keys: + if k in desc: + self._nus.setdefault(k, _make_identity(k)) self._sigs = {k: (nu, inspect.signature(nu)) for k, nu in self._nus.items()} self.stale = True @@ -325,6 +328,9 @@ def get_children(self): class ErrorbarWrapper(MultiProxyWrapper): + required_keys = {"x", "y"} + expected_keys = {f"{axis}{dirc}" for axis in ["x", "y"] for dirc in ["upper", "lower"]} + def __init__(self, data: DataContainer, nus=None, /, **kwargs): super().__init__(data, nus) # TODO all of the kwarg teasing apart that is needed From fdf5e2704e1d3df36411e4af766c63987bdc0c57 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 4 Nov 2022 12:24:47 -0400 Subject: [PATCH 09/11] BLD: always run all of the examples when build the docs --- docs/source/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index e3644ff..03e060d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -99,6 +99,7 @@ def matplotlib_reduced_latex_scraper(block, block_vars, gallery_conf, **kwargs): "matplotlib_animations": True, "image_srcset": ["2x"], "junit": "../test-results/sphinx-gallery/junit.xml" if CIRCLECI else "", + "run_stale_examples": True, } mathmpl_fontsize = 11.0 From e4837b2e2a275fd495151b7ecfa5e6241bebe4c0 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 4 Nov 2022 12:31:16 -0400 Subject: [PATCH 10/11] MNT: make clear all FormattedText objects share the same functions --- examples/mapped.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/examples/mapped.py b/examples/mapped.py index d7a5076..4624f7c 100644 --- a/examples/mapped.py +++ b/examples/mapped.py @@ -21,7 +21,7 @@ cmap.set_under("r") norm = Normalize(1, 8) -nus = { +line_nus = { # arbitrary functions "lw": lambda lw: min(1 + lw, 5), # standard color mapping @@ -30,6 +30,12 @@ "ls": lambda cat: {"A": "-", "B": ":", "C": "--"}[cat[()]], } +text_nus = { + "text": lambda j, cat: f"index={j[()]} class={cat[()]!r}", + "y": lambda j: j, +} + + th = np.linspace(0, 2 * np.pi, 128) delta = np.pi / 9 @@ -48,13 +54,13 @@ ax.add_artist( LineWrapper( ac, - nus, + line_nus, ) ) ax.add_artist( FormatedText( ac, - {"text": lambda j, cat: f"index={j[()]} class={cat[()]!r}", "y": lambda j: j}, + text_nus, x=2 * np.pi, ha="right", bbox={"facecolor": "gray", "alpha": 0.5}, From ddd0d6967eaf4a6a3effd968804ea6072351005d Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 4 Nov 2022 13:49:13 -0400 Subject: [PATCH 11/11] STY: fix linting --- examples/animation.py | 2 +- examples/mapped.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/animation.py b/examples/animation.py index 34c89d0..e92c176 100644 --- a/examples/animation.py +++ b/examples/animation.py @@ -61,7 +61,7 @@ def update(frame, art): lw = LineWrapper(sot_c, lw=5, color="green", label="sin(time)") fc = FormatedText( sot_c, - {'text': lambda phase: f"ϕ={phase:.2f}"}, + {"text": lambda phase: f"ϕ={phase:.2f}"}, x=2 * np.pi, y=1, ha="right", diff --git a/examples/mapped.py b/examples/mapped.py index 4624f7c..1da081c 100644 --- a/examples/mapped.py +++ b/examples/mapped.py @@ -9,7 +9,6 @@ import matplotlib.pyplot as plt import numpy as np -import pandas as pd from matplotlib.colors import Normalize