Skip to content

Commit

Permalink
Merge pull request #299 from tpvasconcelos/159-fix-regressions-tests
Browse files Browse the repository at this point in the history
Fix regressions tests
  • Loading branch information
tpvasconcelos authored Dec 23, 2024
2 parents 3904759 + 139054f commit 88b9663
Show file tree
Hide file tree
Showing 37 changed files with 325 additions and 226 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# Project specific
# ============================================================================

docs/_static/charts
docs/api/autogen
docs/api/public

Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ repos:
- id: pretty-format-json
args: [ --autofix, --no-sort-keys ]
# ignore jupyter notebooks
exclude: (^.*\.ipynb$|^pyrightconfig\.json$)
exclude: (^.*\.ipynb$|^pyrightconfig\.json$|^tests/e2e/artifacts/.*\.json$)
- id: pretty-format-json
args: [ --autofix, --no-sort-keys, --indent=1, --no-ensure-ascii ]
# only jupyter notebooks
Expand Down
6 changes: 4 additions & 2 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ include docs/make.bat
include docs/robots.txt
include docs/_static/favicon.ico
recursive-include docs *.md *.py *.rst
recursive-include docs/_static/charts *.html *.webp *.jpeg
recursive-include docs/_static/charts *.html *.webp
recursive-include docs/_static/css *.css
recursive-include docs/_static/img *.png *.webp *.svg *.gif *.jpg
recursive-include docs/_static/js *.js
recursive-include docs/_templates *.html
prune docs/build
prune docs/_build
prune docs/_static/charts
prune docs/api/autogen
prune docs/api/public

Expand All @@ -37,6 +38,7 @@ recursive-include cicd_utils README.md *.py
recursive-include misc *.py *.ipynb *.txt *.png
recursive-include requirements *.txt
recursive-include tests *.py
recursive-include tests/e2e/artifacts *.jpeg *.json

# Globally excluded patterns
recursive-exclude * *.py[cod] *.egg-info __pycache__ .ipynb_checkpoints/*
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,16 @@ jupyter-init: install ## initialise a jupyter environment
# ==============================================================

.PHONY: clean-all
clean-all: clean-build clean-pyc clean-cov clean-ci-caches clean-tox clean-venv ## remove all artifacts
clean-all: clean-docs clean-build clean-pyc clean-cov clean-ci-caches clean-tox clean-venv ## remove all artifacts
@echo "==> Removed all artifacts!"


.PHONY: clean-docs
clean-docs: ## remove documentation build artifacts
@echo "==> Removing documentation build artifacts..."
rm -fr docs/_build/ docs/_static/charts docs/api/autogen/ docs/api/public/


.PHONY: clean-build
clean-build: ## remove build artifacts
@echo "==> Removing build artifacts..."
Expand Down
134 changes: 0 additions & 134 deletions cicd_utils/cicd/compile_plotly_charts.py

This file was deleted.

32 changes: 12 additions & 20 deletions cicd_utils/ridgeplot_examples/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,15 @@

from typing import TYPE_CHECKING

import plotly.graph_objects as go
from ridgeplot_examples._base import Example

if TYPE_CHECKING:
from collections.abc import Callable
import plotly.graph_objects as go


def tighten_margins(fig: go.Figure, px: int = 0) -> go.Figure:
"""Tighten the margins of a Plotly figure."""
if fig.layout.margin != go.layout.Margin():
# If the Figure's margins are different from the default values,
# we'll assume that the user has set these values intentionally
return fig
# If the Figure does not have a title, we'll leave 40px of space at the
# top to account for the Plotly toolbar. If the Figure does include
# a title, we'll leave the top margin with the default value.
margin_top = None if fig.layout.title.text else max(40, px)
return fig.update_layout(margin=dict(l=px, r=px, t=margin_top, b=px))
__all__ = [
"ALL_EXAMPLES",
"Example",
]


def load_basic() -> go.Figure:
Expand Down Expand Up @@ -51,10 +43,10 @@ def load_probly() -> go.Figure:
return main()


ALL_EXAMPLES: list[tuple[str, Callable[[], go.Figure]]] = [
("basic", load_basic),
("basic_hist", load_basic_hist),
("lincoln_weather", load_lincoln_weather),
("lincoln_weather_red_blue", load_lincoln_weather_red_blue),
("probly", load_probly),
ALL_EXAMPLES: list[Example] = [
Example("basic", load_basic),
Example("basic_hist", load_basic_hist),
Example("lincoln_weather", load_lincoln_weather),
Example("lincoln_weather_red_blue", load_lincoln_weather_red_blue),
Example("probly", load_probly),
]
154 changes: 154 additions & 0 deletions cicd_utils/ridgeplot_examples/_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
from __future__ import annotations

from copy import deepcopy
from dataclasses import dataclass
from typing import TYPE_CHECKING

import plotly.graph_objects as go
from minify_html import minify

if TYPE_CHECKING:
from collections.abc import Callable, Sequence
from pathlib import Path

from ridgeplot._types import NumericT


def tighten_margins(fig: go.Figure, px: int = 0) -> go.Figure:
"""Tighten the margins of a Plotly figure."""
if fig.layout.margin != go.layout.Margin():
# If the Figure's margins are different from the default values,
# we'll assume that the user has set these values intentionally
return fig
# If the Figure does not have a title, we'll leave 40px of space at the
# top to account for the Plotly toolbar. If the Figure does include
# a title, we'll leave the top margin with the default value.
margin_top = None if fig.layout.title.text else max(40, px)
return fig.update_layout(margin=dict(l=px, r=px, t=margin_top, b=px))


def round_sig_figs(x: NumericT, sig_figs: int) -> NumericT:
"""Round a float value to a fixed number of significant figures."""
cls = type(x)
rounded = float(f"{x:.{sig_figs}g}")
return cls(rounded)


def round_seq(seq: Sequence[NumericT], sig_figs: int) -> Sequence[NumericT]:
rounded_seq = [round_sig_figs(x, sig_figs) for x in seq]
if isinstance(seq, (list, tuple)):
return type(seq)(rounded_seq)
raise TypeError(f"Unsupported sequence type: {type(seq)}")


def round_nested_seq(
seq: Sequence[Sequence[NumericT]], sig_figs: int
) -> Sequence[Sequence[NumericT]]:
rounded_seq = [round_seq(sub_seq, sig_figs) for sub_seq in seq]
if isinstance(seq, (list, tuple)):
return type(seq)(rounded_seq)
raise TypeError(f"Unsupported sequence type: {type(seq)}")


def round_fig_data(fig: go.Figure, sig_figs: int) -> go.Figure:
"""Round the float values in the data of a Plotly figure."""
data_attrs = {"x": round_seq, "y": round_seq, "customdata": round_nested_seq}
for i in range(len(fig.data)):
for attr, round_fn in data_attrs.items():
if hasattr(fig.data[i], attr):
attr_val = getattr(fig.data[i], attr)
if attr_val is None:
continue
rounded_attr = round_fn(attr_val, sig_figs)
setattr(fig.data[i], attr, rounded_attr)
return fig


@dataclass
class Example:
plot_id: str
figure_factory: Callable[[], go.Figure]

def __post_init__(self) -> None:
self.fig = self.figure_factory() # pyright: ignore[reportUninitializedInstanceVariable]

def to_html(self, path: Path, minify_html: bool) -> None:
fig = deepcopy(self.fig)

if fig.layout.height is None:
raise ValueError("The Figure's layout.height value must be explicitly set.")
# Overriding the width to None results in a '100%' CSS width.
# This is achieved by setting the `default_width` parameter
# in `fig.to_html()` to "100%" (see below).
fig.layout.width = None

fig = tighten_margins(fig)

# Plotly.js (and MathJax.js if needed) should be included HTML's <head>
# of the Sphinx documentation. This can be done using the `html_js_files`
# configuration option in `conf.py`. For pages with multiple Plotly charts,
# this method is more efficient than including the Plotly.js (+ MathJax.js)
# source code in each HTML artefact. See `include_plotlyjs` below.
html_str = fig.to_html(
include_plotlyjs=False,
include_mathjax=False,
full_html=False,
default_width="100%",
div_id=f"plotly-id-{self.plot_id}",
)

# Wrap the chart's HTML in a <div> tag with a custom class name
# to allow for custom styling in the Sphinx documentation.
# See: https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_css_files
wrapper_div_class = "plotly-graph-wrapper"
html_str = f'<div class="{wrapper_div_class}">{html_str}</div>'
if minify_html:
# Need to set `minify_js` to False to avoid breaking
# the inner HTML set in the `hovertemplate` attribute
# TODO: Investigate if this is still necessary or find a workaround
# (reason: minify_js seems to make a significant difference)
html_str = minify(html_str, minify_js=False)

if not path.exists():
path.mkdir(parents=True)
out_path = path / f"{self.plot_id}.html"
out_path.write_text(html_str, "utf-8")

def to_webp(self, path: Path) -> None:
fig = deepcopy(self.fig)
fig = tighten_margins(fig, px=40)
if not path.exists():
path.mkdir(parents=True)
out_image = path / f"{self.plot_id}.webp"
fig.write_image(
out_image,
format="webp",
width=fig.layout.width,
height=fig.layout.height,
scale=3,
engine="kaleido",
)

def to_jpeg(self, path: Path) -> None:
fig = deepcopy(self.fig)
fig = tighten_margins(fig, px=40)
if not path.exists():
path.mkdir(parents=True)
out_image = path / f"{self.plot_id}.jpeg"
fig.write_image(
out_image,
format="jpeg",
width=fig.layout.width,
height=fig.layout.height,
scale=1,
engine="kaleido",
)

def to_json(self, path: Path, sig_figs: int) -> None:
# We'll round the float values in the JSON to a fixed number of
# significant figures to make the regression tests more robust.
round_fig_data(self.fig, sig_figs=sig_figs)
if not path.exists():
path.mkdir(parents=True)
out_path = path / f"{self.plot_id}.json"
out_path.write_text(self.fig.to_json(), "utf-8")
Loading

0 comments on commit 88b9663

Please sign in to comment.