Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ classifiers = [
"Topic :: Scientific/Engineering",
]

[project.optional-dependencies]
matplotlib = ["matplotlib>=3.9.0"]

[project.urls]
Documentation = "https://uriahf.github.io/rtichoke_python/"
"Python Tutorials" = "https://uriahf.github.io/rtichoke_blog_python/"
Expand All @@ -48,7 +51,6 @@ dev = [
"jupyter<2.0.0,>=1.0.0",
"pytest-cov<5.0.0,>=4.0.0",
"pytest<8.0.0,>=7.3.0",
"pyzmq<27.0.0,>=26.3.0",
"ruff>=0.11.0",
"ipykernel>=6.29.5",
"lifelines>=0.30.0",
Expand Down
171 changes: 171 additions & 0 deletions src/rtichoke/_renderers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
"""Renderer selection for canonical rtichoke visualization specifications."""

from __future__ import annotations

import json
from dataclasses import dataclass
from importlib.resources import files
from pathlib import Path
from typing import Any, Literal

Renderer = Literal["plotly", "matplotlib", "browser", "rtichoke_viz"]

_SUPPORTED_RENDERERS = ("plotly", "matplotlib", "browser", "rtichoke_viz")


def _validate_renderer(renderer: str) -> Renderer:
"""Validate and normalize a public renderer name."""
if renderer not in _SUPPORTED_RENDERERS:
supported = ", ".join(repr(name) for name in _SUPPORTED_RENDERERS)
raise ValueError(
f"Unsupported renderer {renderer!r}. Supported renderers are: {supported}."
)
return renderer # type: ignore[return-value]


@dataclass(frozen=True)
class RtichokeBrowserChart:
"""A canonical v2 chart that can be written for offline browser rendering."""

spec: dict[str, Any]
size: int = 600

def write_html(self, path: str | Path) -> Path:
"""Write an offline HTML page plus its packaged renderer assets.

Parameters
----------
path : str or pathlib.Path
Destination for the HTML page.

Returns
-------
pathlib.Path
The written HTML path.
"""
output = Path(path)
output.parent.mkdir(parents=True, exist_ok=True)
vendor = files("rtichoke").joinpath("_vendor", "rtichoke_viz")
for asset in ("rtichoke-viz.js", "rtichoke-viz.css"):
(output.parent / asset).write_bytes(vendor.joinpath(asset).read_bytes())

render_export = {
"roc": "renderRocV2",
"calibration": "renderCalibrationV2",
"precision_recall": "renderPrecisionRecallV2",
"gains": "renderGainsV2",
}.get(str(self.spec.get("type")))
if render_export is None:
raise ValueError(
f"rtichoke_viz does not support chart type {self.spec.get('type')!r}."
)

spec_json = json.dumps(self.spec, separators=(",", ":")).replace("</", "<\\/")
html = f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="./rtichoke-viz.css">
<title>rtichoke {self.spec.get("type")} chart</title>
</head>
<body>
<div id="rtichoke-chart" class="rtichoke-viz-chart"></div>
<script id="rtichoke-spec" type="application/json">{spec_json}</script>
<script type="module">
import {{ {render_export} }} from "./rtichoke-viz.js";
const spec = JSON.parse(document.querySelector("#rtichoke-spec").textContent);
const chart = {render_export}(spec, {{ width: {self.size}, height: {self.size} }});
document.querySelector("#rtichoke-chart").append(chart);
</script>
</body>
</html>
"""
output.write_text(html, encoding="utf-8")
return output


def _render_gains_matplotlib(
spec: dict[str, Any], *, size: int, color_values: list[str]
) -> Any:
"""Render canonical gains quantities with an optional Matplotlib backend."""
try:
from matplotlib.figure import Figure
except ImportError as error:
raise ImportError(
"The 'matplotlib' renderer requires the optional matplotlib dependency. "
"Install it with `pip install 'rtichoke[matplotlib]'`."
) from error

figure = Figure(figsize=(size / 100, size / 100), dpi=100)
axis = figure.subplots()
references = spec.get("references", [])
assert isinstance(references, list)
for reference in references:
if not isinstance(reference, dict):
continue
if reference.get("type") == "identity":
x_values, y_values = [0, 1], [0, 1]
elif reference.get("type") == "path":
points = reference.get("points", [])
x_values = [point["x"] for point in points]
y_values = [point["y"] for point in points]
else:
continue
axis.plot(
x_values,
y_values,
color="#BEBEBE",
linestyle="--",
linewidth=2,
)

series = spec.get("series", [])
data = spec.get("data", [])
assert isinstance(series, list) and isinstance(data, list)
display_groups = list(dict.fromkeys(item["display"]["group"] for item in series))
colors = {
group: (
"black"
if len(display_groups) == 1
else color_values[index % len(color_values)]
)
for index, group in enumerate(display_groups)
}
for item in series:
rows = [row for row in data if row["seriesId"] == item["id"]]
display = item["display"]
axis.plot(
[row["ppcr"] for row in rows],
[row["sensitivity"] for row in rows],
label=display["label"],
color=colors[display["group"]],
linewidth=2,
)

x_axis = spec["xAxis"]
y_axis = spec["yAxis"]
axis.set_xlabel(x_axis["label"])
axis.set_ylabel(y_axis["label"])
axis.set_xlim(*x_axis["domain"])
axis.set_ylim(*y_axis["domain"])
if len(series) > 1:
axis.legend()
figure.tight_layout()
return figure


def _render_gains_v2(
spec: dict[str, Any],
*,
renderer: str,
size: int,
color_values: list[str],
) -> Any:
"""Render a canonical gains v2 spec with a non-default backend."""
selected = _validate_renderer(renderer)
if selected == "matplotlib":
return _render_gains_matplotlib(spec, size=size, color_values=color_values)
if selected in {"browser", "rtichoke_viz"}:
return RtichokeBrowserChart(spec=spec, size=size)
raise ValueError("The Plotly renderer must use the existing production path.")
9 changes: 4 additions & 5 deletions src/rtichoke/_vendor/rtichoke_viz/VENDORED_FROM
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
repository=https://github.com/uriahf/rtichoke_viz
release=v0.2.0
source_commit=45dc109a6a0679d0f8f3f9452d8de9306a89b906
archive=rtichoke-viz-0.2.0.tar.gz
sha256=3861277c01b3983f8b344a9ee0237c7d09fd0ba4c3d1e0cce489962e7b559d9f
checksum=rtichoke-viz-0.2.0.tar.gz.sha256
release=v0.3.0
source_commit=aca9188ea856167557efb20980a0b43e0481b8c8
archive=rtichoke-viz-0.3.0.tar.gz
sha256=558f8d9e16f9544659b84e33f72511065163291a1b97a3c5511b61d1e1f0cac1
Binary file not shown.
Loading
Loading