From 32a870272d799e6bf907f3051492720370411028 Mon Sep 17 00:00:00 2001 From: Oriol Abril-Pla Date: Tue, 6 Aug 2024 21:31:47 +0200 Subject: [PATCH] Improve backend documentation and get plot_forest to follow best practices (#78) * start working on best practices and backend docs * use none backend as documentation base * add images for plotly and none * adapt minigallery directive * use none backend instead of arviz_plots.backend * gallery references and plot sizing improvements * fix typo * pylint * wait until using sphinx 8 * pseudo fix for empty minigallery * modify slightly auto sizing * Apply suggestions from code review Co-authored-by: Osvaldo A Martin * add see also --------- Co-authored-by: Osvaldo A Martin --- .gitignore | 7 +- docs/source/_static/no-image.svg | 1 + docs/source/_static/plotly-logo-dark.png | Bin 0 -> 16764 bytes docs/source/_static/plotly-logo-light.png | Bin 0 -> 17530 bytes .../api/backend/{index.part.rst => index.rst} | 36 ++- docs/source/api/backend/none.part.rst | 5 + docs/source/api/backend/plotly.part.rst | 5 + docs/source/conf.py | 2 +- .../gallery/distribution/plot_dist_ecdf.py | 3 - .../gallery/distribution/plot_dist_kde.py | 3 - .../gallery/distribution/plot_forest.py | 3 - .../gallery/distribution/plot_forest_shade.py | 3 - .../plot_dist_models.py | 3 - .../plot_forest_models.py | 3 - .../inference_diagnostics/plot_trace.py | 1 - docs/source/gallery/mixed/plot_forest_ess.py | 3 - docs/source/gallery/mixed/plot_trace_dist.py | 1 - .../model_criticism/plot_forest_pp_obs.py | 3 - docs/sphinxext/gallery_generator.py | 133 ++++++++- pyproject.toml | 2 +- src/arviz_plots/backend/__init__.py | 264 +----------------- src/arviz_plots/backend/bokeh/__init__.py | 28 +- .../backend/matplotlib/__init__.py | 31 +- src/arviz_plots/backend/none/__init__.py | 72 +++-- src/arviz_plots/backend/plotly/__init__.py | 26 +- src/arviz_plots/backend/plotly/legend.py | 8 + src/arviz_plots/plot_collection.py | 2 +- src/arviz_plots/plots/forestplot.py | 114 ++++---- src/arviz_plots/plots/tracedistplot.py | 18 +- src/arviz_plots/plots/traceplot.py | 11 +- src/arviz_plots/styles/arviz-clean.mplstyle | 4 +- .../styles/arviz-clean_01.mplstyle | 4 +- .../styles/arviz-clean_02.mplstyle | 4 +- tests/test_hypothesis_plots.py | 24 +- tox.ini | 5 +- 35 files changed, 353 insertions(+), 479 deletions(-) create mode 100644 docs/source/_static/no-image.svg create mode 100644 docs/source/_static/plotly-logo-dark.png create mode 100644 docs/source/_static/plotly-logo-light.png rename docs/source/api/backend/{index.part.rst => index.rst} (78%) create mode 100644 docs/source/api/backend/none.part.rst create mode 100644 docs/source/api/backend/plotly.part.rst create mode 100644 src/arviz_plots/backend/plotly/legend.py diff --git a/.gitignore b/.gitignore index ab9db98..92f6cfe 100644 --- a/.gitignore +++ b/.gitignore @@ -99,10 +99,11 @@ docs/source/api/**/generated docs/source/gallery/_images docs/source/gallery/_scripts docs/source/gallery/*.md +docs/source/gallery/backreferences.json docs/jupyter_execute -docs/source/api/backend/matplotlib.rst -docs/source/api/backend/bokeh.rst -docs/source/api/backend/index.rst +docs/source/api/backend/*.rst +!docs/source/api/backend/*.part.rst +!docs/source/api/backend/index.rst # PyBuilder .pybuilder/ diff --git a/docs/source/_static/no-image.svg b/docs/source/_static/no-image.svg new file mode 100644 index 0000000..e0c7695 --- /dev/null +++ b/docs/source/_static/no-image.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/source/_static/plotly-logo-dark.png b/docs/source/_static/plotly-logo-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..383f3154702c79e81ca09e3a14fbd759689842b5 GIT binary patch literal 16764 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 literal 0 HcmV?d00001 diff --git a/docs/source/_static/plotly-logo-light.png b/docs/source/_static/plotly-logo-light.png new file mode 100644 index 0000000000000000000000000000000000000000..62f42ac96284f7c0b5b03d3e62ebe025596383e0 GIT binary patch literal 17530 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 B!&U$Q literal 0 HcmV?d00001 diff --git a/docs/source/api/backend/index.part.rst b/docs/source/api/backend/index.rst similarity index 78% rename from docs/source/api/backend/index.part.rst rename to docs/source/api/backend/index.rst index ab8132b..0ad46be 100644 --- a/docs/source/api/backend/index.part.rst +++ b/docs/source/api/backend/index.rst @@ -6,7 +6,7 @@ Interface to plotting backends Available backends ------------------ -.. grid:: 2 +.. grid:: 1 1 2 2 .. grid-item-card:: :link: matplotlib @@ -22,12 +22,44 @@ Available backends :img-background: ../../_static/bokeh-logo-light.svg :class-img-bottom: dark-light + .. grid-item-card:: + :link: plotly + :link-type: doc + :link-alt: plotly + :img-background: ../../_static/plotly-logo-light.png + :class-img-bottom: dark-light + + .. grid-item-card:: + :link: none + :link-type: doc + :link-alt: none + + .. grid:: 3 + + .. grid-item:: + + .. image:: ../../_static/no-image.svg + :class: dark-light + :alt: + + .. grid-item:: + + Data + + .. grid-item:: + + .. raw:: html + + Viz + .. toctree:: :maxdepth: 1 :hidden: Matplotlib Bokeh + Plotly + None (only processing, no plotting) --------------------------- Common interface definition @@ -85,5 +117,3 @@ Common interface definition axis Data axis (x, y or both) on which to apply the function. - -.. autosummary of module elements will be added here from template diff --git a/docs/source/api/backend/none.part.rst b/docs/source/api/backend/none.part.rst new file mode 100644 index 0000000..4cdbdb7 --- /dev/null +++ b/docs/source/api/backend/none.part.rst @@ -0,0 +1,5 @@ +============ +None backend +============ + +.. automodule:: arviz_plots.backend.none diff --git a/docs/source/api/backend/plotly.part.rst b/docs/source/api/backend/plotly.part.rst new file mode 100644 index 0000000..2deebdf --- /dev/null +++ b/docs/source/api/backend/plotly.part.rst @@ -0,0 +1,5 @@ +============== +Plotly backend +============== + +.. automodule:: arviz_plots.backend.plotly diff --git a/docs/source/conf.py b/docs/source/conf.py index efea995..5c83d9f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -56,7 +56,7 @@ ] suppress_warnings = ["mystnb.unknown_mime_type"] -backend_modules = ("index", "matplotlib", "bokeh") +backend_modules = ("none", "matplotlib", "bokeh", "plotly") api_backend_dir = Path(__file__).parent.resolve() / "api" / "backend" with open(api_backend_dir / "interface.template.rst", "r", encoding="utf-8") as f: interface_template = f.read() diff --git a/docs/source/gallery/distribution/plot_dist_ecdf.py b/docs/source/gallery/distribution/plot_dist_ecdf.py index 36703a1..ad2fc5b 100644 --- a/docs/source/gallery/distribution/plot_dist_ecdf.py +++ b/docs/source/gallery/distribution/plot_dist_ecdf.py @@ -1,5 +1,4 @@ """ -(gallery_dist_ecdf)= # ECDF plot Facetted ECDF plots for 1D marginals of the distribution @@ -8,8 +7,6 @@ :::{seealso} API Documentation: {func}`~arviz_plots.plot_dist` - -Other gallery examples using `plot_dist`: {ref}`gallery_dist_kde` ::: """ from arviz_base import load_arviz_data diff --git a/docs/source/gallery/distribution/plot_dist_kde.py b/docs/source/gallery/distribution/plot_dist_kde.py index a733f93..0349328 100644 --- a/docs/source/gallery/distribution/plot_dist_kde.py +++ b/docs/source/gallery/distribution/plot_dist_kde.py @@ -1,5 +1,4 @@ """ -(gallery_dist_kde)= # KDE plot Facetted KDE plots for 1D marginals of the distribution @@ -8,8 +7,6 @@ :::{seealso} API Documentation: {func}`~arviz_plots.plot_dist` - -Other gallery examples using `plot_dist`: {ref}`gallery_dist_ecdf` ::: """ from arviz_base import load_arviz_data diff --git a/docs/source/gallery/distribution/plot_forest.py b/docs/source/gallery/distribution/plot_forest.py index d79e0c6..87cb0f0 100644 --- a/docs/source/gallery/distribution/plot_forest.py +++ b/docs/source/gallery/distribution/plot_forest.py @@ -1,5 +1,4 @@ """ -(gallery_forest)= # Forest plot Default forest plot with marginal distribution summaries @@ -8,8 +7,6 @@ :::{seealso} API Documentation: {func}`~arviz_plots.plot_forest` - -Other gallery examples using `plot_forest`: {ref}`gallery_forest_shade` ::: """ from arviz_base import load_arviz_data diff --git a/docs/source/gallery/distribution/plot_forest_shade.py b/docs/source/gallery/distribution/plot_forest_shade.py index f9cb2e9..7a0d18f 100644 --- a/docs/source/gallery/distribution/plot_forest_shade.py +++ b/docs/source/gallery/distribution/plot_forest_shade.py @@ -1,5 +1,4 @@ """ -(gallery_forest_shade)= # Forest plot with shading Forest plot marginal summaries with row shading to enhance reading @@ -8,8 +7,6 @@ :::{seealso} API Documentation: {func}`~arviz_plots.plot_forest` - -Other gallery examples using `plot_forest`: {ref}`gallery_forest` ::: """ from arviz_base import load_arviz_data diff --git a/docs/source/gallery/distribution_comparison/plot_dist_models.py b/docs/source/gallery/distribution_comparison/plot_dist_models.py index c195933..532fa60 100644 --- a/docs/source/gallery/distribution_comparison/plot_dist_models.py +++ b/docs/source/gallery/distribution_comparison/plot_dist_models.py @@ -1,5 +1,4 @@ """ -(gallery_dist_models)= # Marginal distribution comparison plot Full marginal distribution comparison between different models @@ -9,8 +8,6 @@ :::{seealso} API Documentation: {func}`~arviz_plots.plot_dist` -Other gallery examples using `plot_dist`: {ref}`gallery_dist_kde`, {ref}`gallery_dist_ecdf` - Other examples comparing marginal distributions: {ref}`gallery_forest_models` ::: """ diff --git a/docs/source/gallery/distribution_comparison/plot_forest_models.py b/docs/source/gallery/distribution_comparison/plot_forest_models.py index 72c5230..05b37e0 100644 --- a/docs/source/gallery/distribution_comparison/plot_forest_models.py +++ b/docs/source/gallery/distribution_comparison/plot_forest_models.py @@ -1,5 +1,4 @@ """ -(gallery_forest_models)= # Forest plot comparison Forest plot summaries for 1D marginal distributions @@ -9,8 +8,6 @@ :::{seealso} API Documentation: {func}`~arviz_plots.plot_forest` -Other gallery examples using `plot_forest`: {ref}`gallery_forest`, {ref}`gallery_forest_shade` - Other examples comparing marginal distributions: {ref}`gallery_dist_models` ::: """ diff --git a/docs/source/gallery/inference_diagnostics/plot_trace.py b/docs/source/gallery/inference_diagnostics/plot_trace.py index 247908d..8f93ee3 100644 --- a/docs/source/gallery/inference_diagnostics/plot_trace.py +++ b/docs/source/gallery/inference_diagnostics/plot_trace.py @@ -1,5 +1,4 @@ """ -(gallery_trace)= # Trace plot Facetted plot with MCMC traces for each variable diff --git a/docs/source/gallery/mixed/plot_forest_ess.py b/docs/source/gallery/mixed/plot_forest_ess.py index 435f4ee..25aefe2 100644 --- a/docs/source/gallery/mixed/plot_forest_ess.py +++ b/docs/source/gallery/mixed/plot_forest_ess.py @@ -1,5 +1,4 @@ """ -(gallery_forest_ess)= # Forest plot with ESS Multiple panel visualization with a forest plot and ESS information @@ -8,8 +7,6 @@ :::{seealso} API Documentation: {func}`~arviz_plots.plot_forest` - -Other gallery examples using `plot_forest`: {ref}`gallery_forest`, {ref}`gallery_forest_shade` ::: """ from importlib import import_module diff --git a/docs/source/gallery/mixed/plot_trace_dist.py b/docs/source/gallery/mixed/plot_trace_dist.py index 9603c42..686170e 100644 --- a/docs/source/gallery/mixed/plot_trace_dist.py +++ b/docs/source/gallery/mixed/plot_trace_dist.py @@ -1,5 +1,4 @@ """ -(gallery_trace_dist)= # Trace and distribution plot Two column layout with marginal distributions on the left and MCMC traces on the right diff --git a/docs/source/gallery/model_criticism/plot_forest_pp_obs.py b/docs/source/gallery/model_criticism/plot_forest_pp_obs.py index 4f9e6d6..8c39809 100644 --- a/docs/source/gallery/model_criticism/plot_forest_pp_obs.py +++ b/docs/source/gallery/model_criticism/plot_forest_pp_obs.py @@ -1,5 +1,4 @@ """ -(gallery_forest_pp_obs)= # Posterior predictive and observations forest plot Overlay of forest plot for the posterior predictive samples and the actual observations @@ -8,8 +7,6 @@ :::{seealso} API Documentation: {func}`~arviz_plots.plot_forest` - -Other gallery examples using `plot_forest`: {ref}`gallery_forest`, {ref}`gallery_forest_shade` ::: """ from importlib import import_module diff --git a/docs/sphinxext/gallery_generator.py b/docs/sphinxext/gallery_generator.py index 4a3c721..6380ff2 100644 --- a/docs/sphinxext/gallery_generator.py +++ b/docs/sphinxext/gallery_generator.py @@ -1,9 +1,14 @@ # pylint: disable=invalid-name """Generate images and full gallery pages from python scripts.""" +import json import os +import re +from collections import defaultdict from pathlib import Path import matplotlib.pyplot as plt +from docutils import statemachine +from docutils.parsers.rst import Directive from sphinx.util import logging logger = logging.getLogger(__name__) @@ -39,7 +44,7 @@ {description} ::: -:::{{image}} _images/{basename}.png +:::{{image}} /gallery/_images/{basename}.png :alt: ::: @@ -49,6 +54,33 @@ :::: """ +minigallery_item_template_rst = """ +.. grid-item-card:: + :link: {refname} + :link-type: ref + :text-align: center + :shadow: none + :class-card: example-gallery + + .. div:: example-img-plot-overlay + + {description} + + .. image:: /gallery/_images/{basename}.png + :alt: + + +++ + {title} +""" + +minigallery_in_example = """ +## Other examples with `{fun}` + +```{{eval-rst}} +.. minigallery:: {fun} +``` +""" + def main(app): """Generate thumbnail images with matplotlib backend and put together the full gallery pages.""" @@ -66,6 +98,8 @@ def main(app): os.makedirs(scripts_dir) index_page = ["(example_gallery)=\n# Example gallery"] + backreferences = defaultdict(list) + api_regex = re.compile(r"azp\.(plot_[a-z]+)\(") for folder, title in dir_title_map.items(): category_dir = gallery_dir / folder @@ -82,9 +116,13 @@ def main(app): backend_line_emphasis = "" emph_lines = [] + api_funs = [] for i, line in enumerate(code_text.splitlines()): if 'backend="none"' in line: emph_lines.append(str(i + 1)) + match = api_regex.search(line) + if match is not None: + api_funs.append(match.groups()[0]) if emph_lines: backend_line_emphasis = f":emphasize-lines: {','.join(emph_lines)}" @@ -102,14 +140,16 @@ def main(app): raise ValueError(f"No title found for {basename} example") example_title = head_lines[i] example_description = "\n".join(head_lines[i + 1 :]) + entry = { + "basename": basename, + "refname": basename.replace("plot_", "gallery_"), + "title": example_title.strip("# "), + "description": example_description.strip(" \n").replace("\n", " "), + } + for fun in api_funs: + backreferences[fun].append(entry) - index_page.append( - grid_item_template.format( - basename=basename, - title=example_title.strip("# "), - description=example_description.strip(" \n"), - ) - ) + index_page.append(grid_item_template.format(**entry)) mpl_noshow_code = code_text.replace('backend="none"', 'backend="matplotlib"').replace( "pc.show()", "" @@ -120,7 +160,10 @@ def main(app): fig.savefig(images_dir / f"{basename}.png", dpi=75) plt.close("all") + minigalleries = "\n".join(minigallery_in_example.format(fun=fun) for fun in api_funs) + myst_text = f""" + ({basename.replace("plot_", "gallery_")})= {head_text} ::::::{{tab-set}} @@ -170,23 +213,95 @@ def main(app): {foot_text} + {minigalleries} + :::{{div}} example-plot-download {{download}}`Download Python Source Code: {basename}.py<_scripts/{basename}.py>` ::: """ - myst_text = "\n".join((line.strip(" ") for line in myst_text.splitlines())) + myst_text = "\n".join((line.strip(" ") for line in myst_text.strip("\n").splitlines())) with open(gallery_dir / f"{basename}.md", "w", encoding="utf-8") as fm: fm.write(myst_text) index_page.append("\n:::::\n") + with open(gallery_dir / "backreferences.json", "w", encoding="utf-8") as f: + json.dump(backreferences, f) + with open(gallery_dir / "index.md", "w", encoding="utf-8") as fi: fi.write("\n".join(index_page)) os.chdir(working_dir) +class MiniGallery(Directive): + """Custom directive to insert a mini-gallery. + + The required argument is one or more of the following: + + * fully qualified names of objects + * pathlike strings to example Python files + * glob-style pathlike strings to example Python files + + The string list of arguments is separated by spaces. + + The mini-gallery will be the subset of gallery + examples that make use of that object from that specific namespace + + Options: + + * `add-heading` adds a heading to the mini-gallery. If an argument is + provided, it uses that text for the heading. Otherwise, it uses + default text. + * `heading-level` specifies the heading level of the heading as a single + character. If omitted, the default heading level is `'^'`. + """ + + required_arguments = 1 + has_content = False + optional_arguments = 0 + final_argument_whitespace = True + + def run(self): + """Generate mini-gallery from backreference and example files.""" + gallery_dir = Path(self.state.document.settings.env.srcdir).resolve() / "gallery" + docname = self.state.document.settings.env.docname + with open(gallery_dir / "backreferences.json", "r", encoding="utf-8") as f: + backreferences = json.load(f) + + # Parse the argument into the individual object + target_obj = self.arguments[0].strip() + + lines = [] + + entry_elements = [ + entry + for entry in backreferences[target_obj] + if f"gallery/{entry['basename']}" != docname + ] + + if len(entry_elements) >= 1: + lines.append(".. grid:: 1 2 3 3\n :gutter: 2 2 3 3\n\n") + + for entry in entry_elements: + if f"gallery/{entry['basename']}" == docname: + continue + lines.extend( + [ + f" {line}" + for line in minigallery_item_template_rst.format(**entry).splitlines() + ] + ) + + text = "\n".join(lines) + include_lines = statemachine.string2lines(text, convert_whitespace=True) + self.state_machine.insert_input(include_lines, self.state_machine.get_source_and_line()[0]) + + return [] + + def setup(app): """Connect the extension to sphinx so it is executed when the builder is initialized.""" + app.add_directive("minigallery", MiniGallery) app.connect("builder-inited", main) diff --git a/pyproject.toml b/pyproject.toml index 9116756..9a3ec7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,7 @@ doc = [ "myst-nb", "sphinx-copybutton", "numpydoc", - "sphinx>=5", + "sphinx>=5,<8", "sphinx-design", "jupyter-sphinx", "h5netcdf", diff --git a/src/arviz_plots/backend/__init__.py b/src/arviz_plots/backend/__init__.py index 7c9df7b..b4aa47a 100644 --- a/src/arviz_plots/backend/__init__.py +++ b/src/arviz_plots/backend/__init__.py @@ -1,266 +1,10 @@ -# pylint: disable=unused-argument """Common interface to plotting backends. Each submodule within this module defines a common interface layer to different plotting libraries. -All other modules in ``arviz_subplots`` use this module to interact with the plotting -backends, never interacting directly. Thus, adding a new backend requires only +Outside ``arviz_plots.backend`` the corresponding backend module is imported, +but only the common interface layer is used, making no distinctions between plotting backends. +Each submodule inside ``arviz_plots.backend`` is expected to implement the same functions +with the same call signature. Thus, adding a new backend requires only implementing this common interface for it, with no changes to any of the other modules. - -Throughout the documentation of this module, there are a few type placeholders indicated -(e.g. ``chart type``). When implementing a module, these placeholders can be associated -to any type of the plotting backend or even custom objects, but all instances -of the same placeholder must use the same type (whatever that is). """ -import numpy as np - -error = NotImplementedError( - "The `arviz_plots.backend` module itself is for reference only. " - "A specific backend must be choosen, for example `arviz_plots.backend.bokeh` " - "or `arviz_plots.backend.matplotlib`" -) - - -# generation of default values for aesthetics -def get_default_aes(aes_key, n, kwargs): - """Generate `n` default values for a given aesthetics keyword.""" - if aes_key not in kwargs: - if aes_key in {"x", "y"}: - return np.arange(n) - if aes_key == "alpha": - return np.linspace(0.2, 0.7, n) - return [None] * n - aes_vals = kwargs[aes_key] - n_aes_vals = len(aes_vals) - if n_aes_vals >= n: - return aes_vals[:n] - return np.tile(aes_vals, (n // n_aes_vals) + 1)[:n] - - -# object creation and i/o -def show(chart): - """Show this :term:`chart`. - - Parameters - ---------- - chart : chart type - """ - raise error - - -def create_plotting_grid( - number, - rows=1, - cols=1, - figsize=None, - figsize_units="inches", - squeeze=True, - sharex=False, - sharey=False, - polar=False, - width_ratios=None, - plot_hspace=None, - subplot_kws=None, - **kwargs, -): - """Create a :term:`chart` with a grid of :term:`plots` in it. - - Parameters - ---------- - number : int - Number of plots required - rows, cols : int, default 1 - Number of rows and columns. - figsize : (float, float), optional - Size of the figure in `figsize_units`. - figsize_units : {"inches", "dots"}, default "inches" - Units in which `figsize` is given. - squeeze : bool, default True - Delete dimensions of size 1 in the resulting array of :term:`plots` - sharex, sharey : bool, default False - Flags that indicate the axis limits between the different plots should - be shared. - polar : bool, default False - width_ratios : array_like of shape (cols,), optional - plot_hspace : float, optional - subplot_kws, **kwargs : mapping, optional - Arguments passed downstream to the plotting backend. - - Returns - ------- - chart : chart type - The plotting backend object that represents the created :term:`chart` - plots : plot type or ndarray of plot type - An array of the plotting backend objects that represent the :term:`plots`. - The returned object will be an array unless generating a 1x1 grid - with `squeeze` set to True. - """ - raise error - - -# "geoms" -def line(x, y, target, *, color=None, alpha=None, width=None, linestyle=None, **artist_kws): - """Interface to a line plot. - - Add a line plot to the given `target`. - - Parameters - ---------- - x, y : array-like - target : plot type - color : any - alpha : float - width : float - linestyle : any - **artist_kws : mapping - """ - raise error - - -def scatter( - x, - y, - target, - *, - size=None, - marker=None, - alpha=None, - color=None, - facecolor=None, - edgecolor=None, - width=None, - **artist_kws, -): - """Interface to a line plot. - - Add a line plot to the given `target`. - - Parameters - ---------- - x, y : array-like - target : plot type - size : float or array-like of float - marker : any - The character ``|`` must be a valid marker as it is the default for rug plots. - alpha : float - color : any - Set both facecolor and edgecolor simultaneously but without overriding them if present. - facecolor : any - Color of the marker filling. - edgecolor : any - Color of the marker edge. - width : float - Width of the marker edge. - **artist_kws : mapping - """ - raise error - - -def text( - x, - y, - string, - target, - *, - size=None, - alpha=None, - color=None, - vertical_align=None, - horizontal_align=None, - **artist_kws, -): - """Interface to text annotation inside a plot.""" - raise error - - -def fill_between_y(x, y_bottom, y_top, target, **artist_kws): - """Fill the region between y_bottom and y_top.""" - raise error - - -# general plot appeareance -def title(string, target, *, size=None, color=None, **artist_kws): - """Interface to adding a title to a plot.""" - raise error - - -def ylabel(string, target, *, size=None, color=None, **artist_kws): - """Interface to adding a label to a plot's y axis.""" - raise error - - -def xlabel(string, target, *, size=None, color=None, **artist_kws): - """Interface to adding a label to a plot's x axis.""" - raise error - - -def xticks(ticks, labels, traget, **artist_kws): - """Interface to setting ticks and tick labels of the x axis. - - Parameters - ---------- - ticks : array_like - labels : array_like or None - Labels for the provided `ticks`. Must accept ``None`` as a way - to set only ticks and leave their default labels. - """ - raise error - - -def yticks(ticks, labels, traget, **artist_kws): - """Interface to setting ticks and tick labels of the y axis. - - Parameters - ---------- - ticks : array_like - labels : array_like or None - Labels for the provided `ticks`. Must accept ``None`` as a way - to set only ticks and leave their default labels. - """ - raise error - - -def ticklabel_props(target, *, axis="both", size=None, color=None, **artist_kws): - """Interface to setting size of tick labels.""" - raise error - - -def remove_ticks(target, *, axis="y"): - """Interface to removing ticks from a plot.""" - raise error - - -def remove_axis(target, axis="y"): - """Interface to removing axis from a plot.""" - raise error - - -def legend( - target, - kwarg_list, - label_list, - title=None, # pylint: disable=redefined-outer-name - artist_type="line", - artist_kwargs=None, - **kwargs, -): - """Interface to manually generated legends. - - Parameters - ---------- - target : chart type - kwarg_list : list of dict - List of dictionaries that contain properties and their values for the miniatures - in each entry of the legend. - label_list : list of str - List of labels of the entries in the legend. - title : str, optional - Title of the legend. - artist_type : str, optional - Type of the artist that will be used for the legend miniature. - artist_kwargs : mapping, optional - Keyword arguments passed to the miniatures artist. - **kwargs : mapping, optional - Keyword arguments passed to the backend legend generating function - """ - raise error diff --git a/src/arviz_plots/backend/bokeh/__init__.py b/src/arviz_plots/backend/bokeh/__init__.py index 9ab37b4..73d8a3e 100644 --- a/src/arviz_plots/backend/bokeh/__init__.py +++ b/src/arviz_plots/backend/bokeh/__init__.py @@ -7,7 +7,7 @@ from bokeh.plotting import figure from bokeh.plotting import show as _show -from .. import get_default_aes as get_agnostic_default_aes +from ..none import get_default_aes as get_agnostic_default_aes from .legend import legend @@ -19,8 +19,10 @@ class UnsetDefault: # generation of default values for aesthetics -def get_default_aes(aes_key, n, kwargs): +def get_default_aes(aes_key, n, kwargs=None): """Generate `n` *bokeh valid* default values for a given aesthetics keyword.""" + if kwargs is None: + kwargs = {} if aes_key not in kwargs: if "color" in aes_key: # fmt: off @@ -34,12 +36,12 @@ def get_default_aes(aes_key, n, kwargs): elif aes_key == "marker": vals = ["circle", "cross", "triangle", "x", "diamond"] else: - return get_agnostic_default_aes(aes_key, n, {}) + return get_agnostic_default_aes(aes_key, n) return get_agnostic_default_aes(aes_key, n, {aes_key: vals}) return get_agnostic_default_aes(aes_key, n, kwargs) -def scale_fig_size(figsize, rows=1, cols=1, figsize_units="inches"): +def scale_fig_size(figsize, rows=1, cols=1, figsize_units=None): """Scale figure properties according to figsize, rows and cols. Parameters @@ -62,14 +64,14 @@ def scale_fig_size(figsize, rows=1, cols=1, figsize_units="inches"): linewidth : float linewidth """ + if figsize_units is None: + figsize_units = "dots" if figsize is None: - width = 800 - height = (100 * rows + 100) ** 1.1 + width = cols * (400 if cols < 4 else 250) + height = 100 * (rows + 1) ** 1.1 figsize_units = "dots" else: width, height = figsize - cols = cols * 100 - rows = rows * 100 if figsize_units == "inches": warnings.warn( f"Assuming dpi=100. Use figsize_units='dots' and figsize={figsize} " @@ -80,15 +82,7 @@ def scale_fig_size(figsize, rows=1, cols=1, figsize_units="inches"): elif figsize_units != "dots": raise ValueError(f"figsize_units must be 'dots' or 'inches', but got {figsize_units}") - val = (width * height) ** 0.5 - val2 = (cols * rows) ** 0.5 - scale_factor = val / (4 * val2) - # I didn't find any Bokeh equivalent to theme/rcParams, - # so they are hardcoded for now - labelsize = 14 * scale_factor - linewidth = 1 * scale_factor - - return (width, height), labelsize, linewidth + return (width, height) # object creation and i/o diff --git a/src/arviz_plots/backend/matplotlib/__init__.py b/src/arviz_plots/backend/matplotlib/__init__.py index 8279312..20aaeca 100644 --- a/src/arviz_plots/backend/matplotlib/__init__.py +++ b/src/arviz_plots/backend/matplotlib/__init__.py @@ -16,7 +16,7 @@ from matplotlib.pyplot import subplots from matplotlib.text import Text -from .. import get_default_aes as get_agnostic_default_aes +from ..none import get_default_aes as get_agnostic_default_aes from .legend import legend @@ -28,8 +28,10 @@ class UnsetDefault: # generation of default values for aesthetics -def get_default_aes(aes_key, n, kwargs): - """Generate `n` *bokeh valid* default values for a given aesthetics keyword.""" +def get_default_aes(aes_key, n, kwargs=None): + """Generate `n` *matplotlib valid* default values for a given aesthetics keyword.""" + if kwargs is None: + kwargs = {} if aes_key not in kwargs: default_prop_cycle = rcParams["axes.prop_cycle"].by_key() if ("color" in aes_key) or aes_key == "c": @@ -49,12 +51,12 @@ def get_default_aes(aes_key, n, kwargs): elif aes_key in default_prop_cycle: vals = default_prop_cycle[aes_key] else: - return get_agnostic_default_aes(aes_key, n, {}) + return get_agnostic_default_aes(aes_key, n) return get_agnostic_default_aes(aes_key, n, {aes_key: vals}) return get_agnostic_default_aes(aes_key, n, kwargs) -def scale_fig_size(figsize, rows=1, cols=1, figsize_units="inches"): +def scale_fig_size(figsize, rows=1, cols=1, figsize_units=None): """Scale figure properties according to figsize, rows and cols. Parameters @@ -77,28 +79,23 @@ def scale_fig_size(figsize, rows=1, cols=1, figsize_units="inches"): linewidth : float linewidth """ + if figsize_units is None: + figsize_units = "inches" if figsize is None: - width = rcParams["figure.figsize"][0] - height = (rows + 1) ** 1.1 + default_width, default_height = rcParams["figure.figsize"] + width = cols * (default_width if cols < 4 else 0.6 * default_width) + height = default_height / 4 * (rows + 1) ** 1.1 figsize_units = "inches" else: width, height = figsize - dpi = rcParams["figure.dpi"] - cols = cols * dpi - rows = rows * dpi if figsize_units == "inches": + dpi = rcParams["figure.dpi"] width *= dpi height *= dpi elif figsize_units != "dots": raise ValueError(f"figsize_units must be 'dots' or 'inches', but got {figsize_units}") - val = (width * height) ** 0.5 - val2 = (cols * rows) ** 0.5 - scale_factor = val / (4 * val2) - labelsize = rcParams["font.size"] * scale_factor - linewidth = rcParams["lines.linewidth"] * scale_factor - - return (width, height), labelsize, linewidth + return (width, height) # object creation and i/o diff --git a/src/arviz_plots/backend/none/__init__.py b/src/arviz_plots/backend/none/__init__.py index 3356a6f..0f679ef 100644 --- a/src/arviz_plots/backend/none/__init__.py +++ b/src/arviz_plots/backend/none/__init__.py @@ -11,8 +11,6 @@ import numpy as np -from .. import get_default_aes as get_agnostic_default_aes - ALLOW_KWARGS = True @@ -24,46 +22,70 @@ class UnsetDefault: # generation of default values for aesthetics -def get_default_aes(aes_key, n, kwargs): - """Generate `n` default values for a given aesthetics keyword.""" +def get_default_aes(aes_key, n, kwargs=None): + """Generate `n` default values for a given aesthetics keyword. + + Parameters + ---------- + aes_key : str + The key for which default values should be generated. + Ideally part of {ref}`common interface arguments `. + n : int + Number of values to generate. + kwargs : mapping of {str : array_like}, optional + Mapping with aesthetic keywords as keys and its correponding values as values. + If `aes_key` is present, the provided values will be used, repeating them + with :func:`numpy.tile` if necessary. + + Returns + ------- + ndarray of shape (n,) + The requested `n` default values for `aes_key`. They might not be unique. + """ + if kwargs is None: + kwargs = {} if aes_key not in kwargs: if aes_key in {"x", "y"}: return np.arange(n) + if aes_key == "alpha": + return np.linspace(0.2, 0.7, n) return np.array([f"{aes_key}_{i}" for i in range(n)]) - return get_agnostic_default_aes(aes_key, n, kwargs) + aes_vals = kwargs[aes_key] + n_aes_vals = len(aes_vals) + if n_aes_vals >= n: + return aes_vals[:n] + return np.tile(aes_vals, (n // n_aes_vals) + 1)[:n] -def scale_fig_size(figsize, rows=1, cols=1, figsize_units="inches"): +def scale_fig_size(figsize, rows=1, cols=1, figsize_units=None): """Scale figure properties according to figsize, rows and cols. + Provide a default figure size given `rows` and `cols`. + Parameters ---------- - figsize : (float, float) or None + figsize : tuple of (float, float) or None Size of figure in `figsize_units` - rows : int + rows : int, default 1 Number of rows - cols : int + cols : int, default 1 Number of columns - figsize_units : {"inches", "dots"} + figsize_units : {"inches", "dots"}, optional Ignored if `figsize` is ``None`` Returns ------- - figsize : (float, float) or None + figsize : tuple of (float, float) or None Size of figure in dots - labelsize : float - fontsize for labels - linewidth : float - linewidth """ + if figsize_units is None: + figsize_units = "dots" if figsize is None: - width = 800 - height = (100 * rows + 100) ** 1.1 + width = cols * (400 if cols < 4 else 250) + height = 100 * (rows + 1) ** 1.1 figsize_units = "dots" else: width, height = figsize - cols = cols * 100 - rows = rows * 100 if figsize_units == "inches": warnings.warn( f"Assuming dpi=100. Use figsize_units='dots' and figsize={figsize} " @@ -74,13 +96,7 @@ def scale_fig_size(figsize, rows=1, cols=1, figsize_units="inches"): elif figsize_units != "dots": raise ValueError(f"figsize_units must be 'dots' or 'inches', but got {figsize_units}") - val = (width * height) ** 0.5 - val2 = (cols * rows) ** 0.5 - scale_factor = val / (4 * val2) - labelsize = 14 * scale_factor - linewidth = 1 * scale_factor - - return (width, height), labelsize, linewidth + return (width, height) # object creation and i/o @@ -89,7 +105,7 @@ def show(chart): Parameters ---------- - chart : chart type + chart : chart_type """ raise TypeError("'none' backend objects can't be shown.") @@ -123,7 +139,7 @@ def create_plotting_grid( Number of plots required rows, cols : int, default 1 Number of rows and columns. - figsize : (float, float), optional + figsize : tuple of (float, float), optional Size of the figure in `figsize_units`. figsize_units : {"inches", "dots"}, default "inches" Units in which `figsize` is given. diff --git a/src/arviz_plots/backend/plotly/__init__.py b/src/arviz_plots/backend/plotly/__init__.py index 27ae347..2fb7df2 100644 --- a/src/arviz_plots/backend/plotly/__init__.py +++ b/src/arviz_plots/backend/plotly/__init__.py @@ -13,7 +13,7 @@ from plotly.subplots import make_subplots from webcolors import hex_to_rgb, name_to_rgb -from .. import get_default_aes as get_agnostic_default_aes +from ..none import get_default_aes as get_agnostic_default_aes class UnsetDefault: @@ -47,8 +47,10 @@ def combine_color_alpha(color, alpha=1): # generation of default values for aesthetics -def get_default_aes(aes_key, n, kwargs): +def get_default_aes(aes_key, n, kwargs=None): """Generate `n` *plotly valid* default values for a given aesthetics keyword.""" + if kwargs is None: + kwargs = {} if aes_key not in kwargs: if "color" in aes_key: # fmt: off @@ -69,7 +71,7 @@ def get_default_aes(aes_key, n, kwargs): return get_agnostic_default_aes(aes_key, n, kwargs) -def scale_fig_size(figsize, rows=1, cols=1, figsize_units="inches"): +def scale_fig_size(figsize, rows=1, cols=1, figsize_units=None): """Scale figure properties according to figsize, rows and cols. Parameters @@ -92,14 +94,14 @@ def scale_fig_size(figsize, rows=1, cols=1, figsize_units="inches"): linewidth : float linewidth """ + if figsize_units is None: + figsize_units = "dots" if figsize is None: - width = 800 - height = (100 * rows + 100) ** 1.1 + width = cols * (400 if cols < 4 else 250) + height = 100 * (rows + 1) ** 1.1 figsize_units = "dots" else: width, height = figsize - cols = cols * 100 - rows = rows * 100 if figsize_units == "inches": warnings.warn( f"Assuming dpi=100. Use figsize_units='dots' and figsize={figsize} " @@ -110,15 +112,7 @@ def scale_fig_size(figsize, rows=1, cols=1, figsize_units="inches"): elif figsize_units != "dots": raise ValueError(f"figsize_units must be 'dots' or 'inches', but got {figsize_units}") - val = (width * height) ** 0.5 - val2 = (cols * rows) ** 0.5 - scale_factor = val / (4 * val2) - # I didn't find any Plotly equivalent to theme/rcParams, - # so they are hardcoded for now - labelsize = 14 * scale_factor - linewidth = 1 * scale_factor - - return (width, height), labelsize, linewidth + return (width, height) def get_figsize(plot_collection): diff --git a/src/arviz_plots/backend/plotly/legend.py b/src/arviz_plots/backend/plotly/legend.py new file mode 100644 index 0000000..52a57fe --- /dev/null +++ b/src/arviz_plots/backend/plotly/legend.py @@ -0,0 +1,8 @@ +"""Plotly legend generation.""" + + +def legend( + target, kwarg_list, label_list, title=None, artist_type="line", artist_kwargs=None, **kwargs +): + """Generate a legend with plotly.""" + raise NotImplementedError("Still in progress") diff --git a/src/arviz_plots/plot_collection.py b/src/arviz_plots/plot_collection.py index bb15286..42995bf 100644 --- a/src/arviz_plots/plot_collection.py +++ b/src/arviz_plots/plot_collection.py @@ -482,7 +482,7 @@ def generate_aes_dt(self, aes=None, **kwargs): self._aes = aes self._kwargs = kwargs if not hasattr(self, "backend"): - plot_bknd = import_module(".backend", package="arviz_plots") + plot_bknd = import_module(".backend.none", package="arviz_plots") else: plot_bknd = import_module(f".backend.{self.backend}", package="arviz_plots") get_default_aes = plot_bknd.get_default_aes diff --git a/src/arviz_plots/plots/forestplot.py b/src/arviz_plots/plots/forestplot.py index 9a48811..fbdb6ae 100644 --- a/src/arviz_plots/plots/forestplot.py +++ b/src/arviz_plots/plots/forestplot.py @@ -8,7 +8,7 @@ from arviz_base import rcParams from arviz_base.labels import BaseLabeller -from arviz_plots.plot_collection import PlotCollection +from arviz_plots.plot_collection import PlotCollection, process_facet_dims from arviz_plots.plots.utils import filter_aes, process_group_variables_coords from arviz_plots.visuals import annotate_label, fill_between_y, line_x, remove_axis, scatter_x @@ -112,7 +112,7 @@ def plot_forest( stats_kwargs : mapping, optional Valid keys are: - * credible_interval -> passed to eti or hdi + * trunk, twig -> passed to eti or hdi * point_estimate -> passed to mean, median or mode pc_kwargs : mapping @@ -129,42 +129,28 @@ def plot_forest( in case they are present, which get a smaller spacing to give a sense of grouping among visual elements that only differ on their chain or model id. - Examples + See Also -------- - The following examples focus on behaviour specific to ``plot_forest``. - For a general introduction to batteries-included functions like this one and common - usage examples see :ref:`plots_intro` - - Default forest plot for a single model: - - .. plot:: - :context: close-figs - - >>> from arviz_plots import plot_forest, style - >>> style.use("arviz-clean") - >>> from arviz_base import load_arviz_data - >>> centered = load_arviz_data('centered_eight') - >>> non_centered = load_arviz_data('non_centered_eight') - >>> pc = plot_forest(centered) - - Default forest plot for multiple models: - - .. plot:: - :context: close-figs - - >>> pc = plot_forest({"centered": centered, "non centered": non_centered}) - >>> pc.add_legend("model") + :ref:`plots_intro` : + General introduction to batteries-included plotting functions, common use and logic overview + plot_ridge : Visual representation of marginal distributions over the y axis + Examples + -------- Single model forest plot with color mapped to the variable (mapping which is also applied to the labels) and alternate shading per school. Moreover, to ensure the shading looks continuous, we'll specify we don't want to use constrained layout (set by the "arviz-clean" theme) and to avoid having the labels too squished we'll set the ``width_ratios`` for - :func:`~arviz_plots.backend.create_plotting_grid` via ``pc_kwargs``. + :func:`~arviz_plots.backend.none.create_plotting_grid` via ``pc_kwargs``. .. plot:: :context: close-figs + >>> from arviz_plots import plot_forest, style + >>> style.use("arviz-clean") + >>> from arviz_base import load_arviz_data + >>> non_centered = load_arviz_data('non_centered_eight') >>> pc = plot_forest( >>> non_centered, >>> var_names=["theta", "mu", "theta_t", "tau"], @@ -176,30 +162,7 @@ def plot_forest( >>> shade_label="school", >>> ) - Extend the forest plot with an extra :term:`plot` with ess estimates. - To achieve that, we manually add a "column" dimension with size 3. - ``plot_forest`` only plots on the "labels" and "forest" coordinate values, - leaving the "ess" coordinate empty. Afterwards, we manually use - :meth:`.PlotCollection.map` with the ess result as data on the "ess" column - to plot their values. - - .. plot:: - :context: close-figs - - >>> from arviz_plots import visuals - >>> import arviz_stats # make accessor available - >>> - >>> c_aux = centered["posterior"].expand_dims( - >>> column=3 - >>> ).assign_coords(column=["labels", "forest", "ess"]) - >>> pc = plot_forest(c_aux, combined=True) - >>> pc.map( - >>> visuals.scatter_x, "ess", data=centered.azstats.ess().ds, - >>> coords={"column": "ess"}, color="C0" - >>> ) - - Note that we are using the same :class:`~.PlotCollection`, so when using - ``map`` all the same aesthetic mappings used by ``plot_forest`` are used. + .. minigallery:: plot_forest """ if ci_kind not in ("hdi", "eti", None): @@ -215,7 +178,7 @@ def plot_forest( if ci_probs[0] > ci_probs[1]: raise ValueError("First element of ci_probs must be smaller than the second") if ci_kind is None: - ci_kind = rcParams["stats.ci_kind"] if "stats.ci_kind" in rcParams else "eti" + ci_kind = rcParams["stats.ci_kind"] if point_estimate is None: point_estimate = rcParams["stats.point_estimate"] if plot_kwargs is None: @@ -256,6 +219,7 @@ def plot_forest( backend = rcParams["plot.backend"] else: backend = plot_collection.backend + plot_bknd = import_module(f".backend.{backend}", package="arviz_plots") given_plotcollection = True if plot_collection is None: given_plotcollection = False @@ -291,6 +255,26 @@ def plot_forest( pc_kwargs.setdefault("alpha", [0, 0, 0.3]) if "model" in distribution.dims: pc_kwargs["aes"].setdefault("color", ["model"]) + figsize = pc_kwargs.get("plot_grid_kws", {}).get("figsize", None) + figsize_units = pc_kwargs.get("plot_grid_kws", {}).get("figsize_units", "inches") + if figsize is None: + coeff = 0.2 + n_blocks = process_facet_dims( + pc_data, [dim for dim in pc_kwargs["aes"]["y"] if dim not in ("chain", "model")] + )[0] + if not combined and "chain" in distribution.dims: + coeff += 0.1 + if "model" in distribution.dims: + coeff += 0.1 * distribution.sizes["model"] + figsize = plot_bknd.scale_fig_size( + figsize, + rows=1 + coeff * n_blocks, + cols=process_facet_dims(pc_data, pc_kwargs["cols"])[0], + figsize_units=figsize_units, + ) + figsize_units = "dots" + pc_kwargs["plot_grid_kws"]["figsize"] = figsize + pc_kwargs["plot_grid_kws"]["figsize_units"] = figsize_units plot_collection = PlotCollection.grid( pc_data, backend=backend, @@ -364,13 +348,17 @@ def plot_forest( elif ci_kind == "hdi": ci_fun = distribution.azstats.hdi if twig_kwargs is not False: - ci_twig = ci_fun( - prob=ci_probs[1], dims=ci_dims, **stats_kwargs.get("credible_interval", {}) - ) + twig_stats = stats_kwargs.get("twig", {}) + if isinstance(twig_stats, xr.Dataset): + ci_twig = twig_stats + else: + ci_twig = ci_fun(prob=ci_probs[1], dims=ci_dims, **twig_stats) if trunk_kwargs is not False: - ci_trunk = ci_fun( - prob=ci_probs[0], dims=ci_dims, **stats_kwargs.get("credible_interval", {}) - ) + trunk_stats = stats_kwargs.get("trunk", {}) + if isinstance(trunk_stats, xr.Dataset): + ci_trunk = trunk_stats + else: + ci_trunk = ci_fun(prob=ci_probs[0], dims=ci_dims, **trunk_stats) # compute point estimate pe_kwargs = copy(plot_kwargs.get("point_estimate", {})) @@ -378,10 +366,13 @@ def plot_forest( pe_dims, pe_aes, pe_ignore = filter_aes( plot_collection, aes_map, "point_estimate", sample_dims ) - if point_estimate == "median": - point = distribution.median(dim=pe_dims, **stats_kwargs.get("point_estimate", {})) + pe_stats = stats_kwargs.get("point_estimate", {}) + if isinstance(pe_stats, xr.Dataset): + point = pe_stats + elif point_estimate == "median": + point = distribution.median(dim=pe_dims, **pe_stats) elif point_estimate == "mean": - point = distribution.mean(dim=pe_dims, **stats_kwargs.get("point_estimate", {})) + point = distribution.mean(dim=pe_dims, **pe_stats) else: raise NotImplementedError("coming soon") @@ -475,7 +466,6 @@ def plot_forest( **lab_kwargs, ) x += 1 - plot_bknd = import_module(f".backend.{backend}", package="arviz_plots") ticklabel_kwargs = copy(plot_kwargs.get("ticklabels", {})) if ticklabel_kwargs is not False: plot_bknd.xticks( diff --git a/src/arviz_plots/plots/tracedistplot.py b/src/arviz_plots/plots/tracedistplot.py index 67573d2..689c93f 100644 --- a/src/arviz_plots/plots/tracedistplot.py +++ b/src/arviz_plots/plots/tracedistplot.py @@ -202,7 +202,7 @@ def plot_trace_dist( ) row_dims = ["__variable__"] + aux_dim_list - figsize, textsize, linewidth = plot_bknd.scale_fig_size( + figsize = plot_bknd.scale_fig_size( figsize, rows=process_facet_dims(distribution, row_dims)[0], cols=2, @@ -297,8 +297,6 @@ def plot_trace_dist( } dist_kwargs = copy(plot_kwargs.get(kind, {})) if dist_kwargs is not False: - if "linewidth" not in dist_aes: - dist_kwargs.setdefault("width", linewidth) if neutral_color and "color" not in dist_aes: dist_kwargs.setdefault("color", neutral_color) if neutral_linestyle and "linestyle" not in dist_aes: @@ -325,17 +323,8 @@ def plot_trace_dist( # trace trace_kwargs = copy(plot_kwargs.get("trace", {})) - _, trace_aes, _ = filter_aes(plot_collection, aes_map, "trace", sample_dims) - if trace_kwargs is not False and ("width" not in trace_aes): - trace_kwargs.setdefault("width", linewidth) div_kwargs = copy(plot_kwargs.get("divergence", {})) - _, div_aes, _ = filter_aes(plot_collection, aes_map, "divergence", sample_dims) - if div_kwargs is not False and ("width" not in div_aes): - div_kwargs.setdefault("width", linewidth) xlabel_kwargs = copy(plot_kwargs.get("xlabel_trace", {})) - _, xlabel_aes, _ = filter_aes(plot_collection, aes_map, "xlabel_trace", sample_dims) - if xlabel_kwargs is not False and ("size" not in xlabel_aes): - xlabel_kwargs.setdefault("size", textsize) plot_kwargs_trace = {"trace": trace_kwargs, "divergence": div_kwargs, "xlabel": xlabel_kwargs} plot_kwargs_trace["title"] = False plot_kwargs_trace["ticklabels"] = False @@ -376,8 +365,6 @@ def plot_trace_dist( div_kwargs.setdefault("color", "black") if "marker" not in div_aes: div_kwargs.setdefault("marker", "|") - if "width" not in div_aes: - div_kwargs.setdefault("width", linewidth) if "size" not in div_aes: div_kwargs.setdefault("size", 30) @@ -401,8 +388,6 @@ def plot_trace_dist( if "color" not in labels_aes: label_kwargs.setdefault("color", "black") - label_kwargs.setdefault("size", textsize) - plot_collection.map( labelled_x, "xlabel_dist", @@ -428,7 +413,6 @@ def plot_trace_dist( # Adjust tick labels ticklabels_kwargs = copy(plot_kwargs.get("ticklabels", {})) if ticklabels_kwargs is not False: - ticklabels_kwargs.setdefault("size", textsize) _, _, ticklabels_ignore = filter_aes(plot_collection, aes_map, "ticklabels", sample_dims) plot_collection.map( ticklabel_props, diff --git a/src/arviz_plots/plots/traceplot.py b/src/arviz_plots/plots/traceplot.py index 75b473d..5b647b0 100644 --- a/src/arviz_plots/plots/traceplot.py +++ b/src/arviz_plots/plots/traceplot.py @@ -124,7 +124,7 @@ def plot_trace( n_rows = leaf_dataset(plot_collection.viz, "row").max().to_array().max().item() n_cols = leaf_dataset(plot_collection.viz, "col").max().to_array().max().item() - figsize, textsize, linewidth = plot_bknd.scale_fig_size( + figsize = plot_bknd.scale_fig_size( figsize, rows=n_rows, cols=n_cols, @@ -175,9 +175,7 @@ def plot_trace( default_xname = None xname = trace_kwargs.get("xname", default_xname) trace_kwargs["xname"] = xname - _, trace_aes, trace_ignore = filter_aes(plot_collection, aes_map, "trace", sample_dims) - if "width" not in trace_aes: - trace_kwargs.setdefault("width", linewidth) + _, _, trace_ignore = filter_aes(plot_collection, aes_map, "trace", sample_dims) plot_collection.map( line, "trace", @@ -201,8 +199,6 @@ def plot_trace( divergence_kwargs.setdefault("color", "black") if "marker" not in div_aes: divergence_kwargs.setdefault("marker", "|") - if "width" not in div_aes: - divergence_kwargs.setdefault("width", linewidth) if "size" not in div_aes: divergence_kwargs.setdefault("size", 30) div_reduce_dims = [dim for dim in distribution.dims if dim not in aux_dim_list] @@ -240,8 +236,6 @@ def plot_trace( if "color" not in xlabel_aes: xlabel_kwargs.setdefault("color", "black") - if "size" not in xlabel_aes: - xlabel_kwargs.setdefault("size", textsize) plot_collection.map( labelled_x, @@ -255,7 +249,6 @@ def plot_trace( ticklabels_kwargs = copy(plot_kwargs.get("ticklabels", {})) if ticklabels_kwargs is not False: _, _, ticklabels_ignore = filter_aes(plot_collection, aes_map, "ticklabels", sample_dims) - ticklabels_kwargs.setdefault("size", textsize) plot_collection.map( ticklabel_props, ignore_aes=ticklabels_ignore, diff --git a/src/arviz_plots/styles/arviz-clean.mplstyle b/src/arviz_plots/styles/arviz-clean.mplstyle index 84caee8..3039ff0 100644 --- a/src/arviz_plots/styles/arviz-clean.mplstyle +++ b/src/arviz_plots/styles/arviz-clean.mplstyle @@ -7,8 +7,8 @@ figure.edgecolor: None # broken white outside box figure.titleweight: bold # weight of the figure title figure.titlesize: x-large -figure.figsize: 11.5, 5 -figure.dpi: 300.0 +figure.figsize: 6, 5 +figure.dpi: 200.0 figure.constrained_layout.use: True ## *************************************************************************** diff --git a/src/arviz_plots/styles/arviz-clean_01.mplstyle b/src/arviz_plots/styles/arviz-clean_01.mplstyle index 076cdb7..7f85d61 100644 --- a/src/arviz_plots/styles/arviz-clean_01.mplstyle +++ b/src/arviz_plots/styles/arviz-clean_01.mplstyle @@ -7,8 +7,8 @@ figure.edgecolor: None # broken white outside box figure.titleweight: bold # weight of the figure title figure.titlesize: 18 -figure.figsize: 11.5, 5 -figure.dpi: 300.0 +figure.figsize: 6, 5 +figure.dpi: 200.0 figure.constrained_layout.use: True ## *************************************************************************** diff --git a/src/arviz_plots/styles/arviz-clean_02.mplstyle b/src/arviz_plots/styles/arviz-clean_02.mplstyle index 40de678..ae59b56 100644 --- a/src/arviz_plots/styles/arviz-clean_02.mplstyle +++ b/src/arviz_plots/styles/arviz-clean_02.mplstyle @@ -7,8 +7,8 @@ figure.edgecolor: None # broken white outside box figure.titleweight: bold # weight of the figure title figure.titlesize: 18 -figure.figsize: 11.5, 5 -figure.dpi: 300.0 +figure.figsize: 6, 5 +figure.dpi: 200.0 figure.constrained_layout.use: True ## *************************************************************************** diff --git a/tests/test_hypothesis_plots.py b/tests/test_hypothesis_plots.py index 35cfb31..77bcb44 100644 --- a/tests/test_hypothesis_plots.py +++ b/tests/test_hypothesis_plots.py @@ -1,9 +1,11 @@ # pylint: disable=no-self-use, redefined-outer-name """Test batteries-included plots using the none backend.""" +import arviz_stats # pylint: disable=unused-import import hypothesis.strategies as st import numpy as np import pytest from arviz_base import from_dict +from datatree import DataTree from hypothesis import given from arviz_plots import plot_dist, plot_forest, plot_ridge @@ -19,13 +21,19 @@ def datatree(seed=31): theta = rng.normal(size=(3, 50, 2, 3)) diverging = rng.choice([True, False], size=(3, 50), p=[0.1, 0.9]) - return from_dict( + dt = from_dict( { "posterior": {"mu": mu, "theta": theta, "tau": tau}, "sample_stats": {"diverging": diverging}, }, dims={"theta": ["hierarchy", "group"], "tau": ["hierarchy"]}, ) + dt["point_estimate"] = dt.posterior.mean(("chain", "draw")) + # TODO: should become dt.azstats.eti() after fix in arviz-stats + post = dt.posterior.ds + DataTree(name="trunk", parent=dt, data=post.azstats.eti(prob=0.5)) + DataTree(name="twig", parent=dt, data=post.azstats.eti(prob=0.9)) + return dt kind_value = st.sampled_from(("kde", "ecdf")) @@ -92,14 +100,25 @@ def test_plot_dist(datatree, kind, ci_kind, point_estimate, plot_kwargs): "remove_axis": st.just(False), }, ), + stats_kwargs=st.fixed_dictionaries( + {}, + optional={ + "trunk": st.just(True), + "twig": st.just(True), + "point_estimate": st.just(True), + }, + ), combined=st.booleans(), ci_kind=ci_kind_value, point_estimate=point_estimate_value, labels_shade_label=labels_shade(st.sampled_from(("__variable__", "hierarchy", "group"))), ) -def test_plot_forest(datatree, combined, ci_kind, point_estimate, plot_kwargs, labels_shade_label): +def test_plot_forest( + datatree, combined, ci_kind, point_estimate, plot_kwargs, stats_kwargs, labels_shade_label +): labels = labels_shade_label[0] shade_label = labels_shade_label[1] + stats_kwargs = {key: datatree[key].ds for key in stats_kwargs} pc = plot_forest( datatree, backend="none", @@ -109,6 +128,7 @@ def test_plot_forest(datatree, combined, ci_kind, point_estimate, plot_kwargs, l labels=labels, shade_label=shade_label, plot_kwargs=plot_kwargs, + stats_kwargs=stats_kwargs, ) assert all("plot" not in child for child in pc.viz.children.values()) assert "plot" in pc.viz.data_vars diff --git a/tox.ini b/tox.ini index 9edcd9c..2c8e13f 100644 --- a/tox.ini +++ b/tox.ini @@ -67,7 +67,10 @@ allowlist_externals = find commands = find docs/source/gallery -maxdepth 1 -type f -name '*.md' -delete - rm -r "{toxworkdir}/docs_out" "{toxworkdir}/docs_doctree" "{toxworkdir}/jupyter_execute" docs/source/api/generated docs/source/api/backend/generated docs/source/gallery/_images docs/source/gallery/_scripts + find docs/source/api/backend -maxdepth 1 -type f -name '*.rst' ! -name '*.part.rst' ! -name 'index.rst' ! -name '*.template.rst' -delete + rm -r "{toxworkdir}/docs_out" "{toxworkdir}/docs_doctree" "{toxworkdir}/jupyter_execute" "{toxworkdir}/plot_directive" + rm -r docs/source/api/generated docs/source/api/backend/generated docs/source/gallery/_images docs/source/gallery/_scripts + rm docs/source/gallery/backreferences.json [testenv:viewdocs] description = open HTML docs