diff --git a/pyproject.toml b/pyproject.toml
index f8a4c7b2..efa25ca0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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/"
@@ -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",
diff --git a/src/rtichoke/_renderers.py b/src/rtichoke/_renderers.py
new file mode 100644
index 00000000..983da1cf
--- /dev/null
+++ b/src/rtichoke/_renderers.py
@@ -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"""
+
+
+
+
+
+ rtichoke {self.spec.get("type")} chart
+
+
+
+
+
+
+
+"""
+ 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.")
diff --git a/src/rtichoke/_vendor/rtichoke_viz/VENDORED_FROM b/src/rtichoke/_vendor/rtichoke_viz/VENDORED_FROM
index 8407263a..df5ec55d 100644
--- a/src/rtichoke/_vendor/rtichoke_viz/VENDORED_FROM
+++ b/src/rtichoke/_vendor/rtichoke_viz/VENDORED_FROM
@@ -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
diff --git a/src/rtichoke/_vendor/rtichoke_viz/rtichoke-viz-0.3.0.tar.gz b/src/rtichoke/_vendor/rtichoke_viz/rtichoke-viz-0.3.0.tar.gz
new file mode 100644
index 00000000..4962a386
Binary files /dev/null and b/src/rtichoke/_vendor/rtichoke_viz/rtichoke-viz-0.3.0.tar.gz differ
diff --git a/src/rtichoke/_vendor/rtichoke_viz/rtichoke-viz-v2.schema.json b/src/rtichoke/_vendor/rtichoke_viz/rtichoke-viz-v2.schema.json
index 10293695..95a21a72 100644
--- a/src/rtichoke/_vendor/rtichoke_viz/rtichoke-viz-v2.schema.json
+++ b/src/rtichoke/_vendor/rtichoke_viz/rtichoke-viz-v2.schema.json
@@ -160,37 +160,98 @@
"items": {
"anyOf": [
{
- "type": "object",
"allOf": [
{
- "type": "object",
- "required": [
- "type"
- ],
- "properties": {
- "type": {
- "anyOf": [
- {
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
"const": "identity",
"type": "string"
},
- {
+ "label": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
"const": "horizontal",
"type": "string"
},
- {
- "const": "vertical",
+ "value": {
+ "type": "number"
+ },
+ "label": {
"type": "string"
}
- ]
+ }
},
- "value": {
- "type": "number"
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "vertical",
+ "type": "string"
+ },
+ "value": {
+ "type": "number"
+ },
+ "label": {
+ "type": "string"
+ }
+ }
},
- "label": {
- "type": "string"
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "points"
+ ],
+ "properties": {
+ "type": {
+ "const": "path",
+ "type": "string"
+ },
+ "points": {
+ "minItems": 2,
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "x",
+ "y"
+ ],
+ "properties": {
+ "x": {
+ "type": "number"
+ },
+ "y": {
+ "type": "number"
+ }
+ }
+ }
+ },
+ "label": {
+ "type": "string"
+ }
+ }
}
- }
+ ]
},
{
"type": "object",
@@ -207,37 +268,98 @@
]
},
{
- "type": "object",
"allOf": [
{
- "type": "object",
- "required": [
- "type"
- ],
- "properties": {
- "type": {
- "anyOf": [
- {
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
"const": "identity",
"type": "string"
},
- {
+ "label": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
"const": "horizontal",
"type": "string"
},
- {
- "const": "vertical",
+ "value": {
+ "type": "number"
+ },
+ "label": {
"type": "string"
}
- ]
+ }
},
- "value": {
- "type": "number"
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "vertical",
+ "type": "string"
+ },
+ "value": {
+ "type": "number"
+ },
+ "label": {
+ "type": "string"
+ }
+ }
},
- "label": {
- "type": "string"
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "points"
+ ],
+ "properties": {
+ "type": {
+ "const": "path",
+ "type": "string"
+ },
+ "points": {
+ "minItems": 2,
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "x",
+ "y"
+ ],
+ "properties": {
+ "x": {
+ "type": "number"
+ },
+ "y": {
+ "type": "number"
+ }
+ }
+ }
+ },
+ "label": {
+ "type": "string"
+ }
+ }
}
- }
+ ]
},
{
"type": "object",
@@ -258,37 +380,98 @@
]
},
{
- "type": "object",
"allOf": [
{
- "type": "object",
- "required": [
- "type"
- ],
- "properties": {
- "type": {
- "anyOf": [
- {
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
"const": "identity",
"type": "string"
},
- {
+ "label": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
"const": "horizontal",
"type": "string"
},
- {
- "const": "vertical",
+ "value": {
+ "type": "number"
+ },
+ "label": {
"type": "string"
}
- ]
+ }
},
- "value": {
- "type": "number"
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "vertical",
+ "type": "string"
+ },
+ "value": {
+ "type": "number"
+ },
+ "label": {
+ "type": "string"
+ }
+ }
},
- "label": {
- "type": "string"
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "points"
+ ],
+ "properties": {
+ "type": {
+ "const": "path",
+ "type": "string"
+ },
+ "points": {
+ "minItems": 2,
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "x",
+ "y"
+ ],
+ "properties": {
+ "x": {
+ "type": "number"
+ },
+ "y": {
+ "type": "number"
+ }
+ }
+ }
+ },
+ "label": {
+ "type": "string"
+ }
+ }
}
- }
+ ]
},
{
"type": "object",
@@ -531,37 +714,98 @@
"items": {
"anyOf": [
{
- "type": "object",
"allOf": [
{
- "type": "object",
- "required": [
- "type"
- ],
- "properties": {
- "type": {
- "anyOf": [
- {
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
"const": "identity",
"type": "string"
},
- {
+ "label": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
"const": "horizontal",
"type": "string"
},
- {
- "const": "vertical",
+ "value": {
+ "type": "number"
+ },
+ "label": {
"type": "string"
}
- ]
+ }
},
- "value": {
- "type": "number"
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "vertical",
+ "type": "string"
+ },
+ "value": {
+ "type": "number"
+ },
+ "label": {
+ "type": "string"
+ }
+ }
},
- "label": {
- "type": "string"
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "points"
+ ],
+ "properties": {
+ "type": {
+ "const": "path",
+ "type": "string"
+ },
+ "points": {
+ "minItems": 2,
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "x",
+ "y"
+ ],
+ "properties": {
+ "x": {
+ "type": "number"
+ },
+ "y": {
+ "type": "number"
+ }
+ }
+ }
+ },
+ "label": {
+ "type": "string"
+ }
+ }
}
- }
+ ]
},
{
"type": "object",
@@ -578,37 +822,98 @@
]
},
{
- "type": "object",
"allOf": [
{
- "type": "object",
- "required": [
- "type"
- ],
- "properties": {
- "type": {
- "anyOf": [
- {
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
"const": "identity",
"type": "string"
},
- {
+ "label": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
"const": "horizontal",
"type": "string"
},
- {
- "const": "vertical",
+ "value": {
+ "type": "number"
+ },
+ "label": {
"type": "string"
}
- ]
+ }
},
- "value": {
- "type": "number"
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "vertical",
+ "type": "string"
+ },
+ "value": {
+ "type": "number"
+ },
+ "label": {
+ "type": "string"
+ }
+ }
},
- "label": {
- "type": "string"
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "points"
+ ],
+ "properties": {
+ "type": {
+ "const": "path",
+ "type": "string"
+ },
+ "points": {
+ "minItems": 2,
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "x",
+ "y"
+ ],
+ "properties": {
+ "x": {
+ "type": "number"
+ },
+ "y": {
+ "type": "number"
+ }
+ }
+ }
+ },
+ "label": {
+ "type": "string"
+ }
+ }
}
- }
+ ]
},
{
"type": "object",
@@ -629,37 +934,98 @@
]
},
{
- "type": "object",
"allOf": [
{
- "type": "object",
- "required": [
- "type"
- ],
- "properties": {
- "type": {
- "anyOf": [
- {
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
"const": "identity",
"type": "string"
},
- {
+ "label": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
"const": "horizontal",
"type": "string"
},
- {
- "const": "vertical",
+ "value": {
+ "type": "number"
+ },
+ "label": {
"type": "string"
}
- ]
+ }
},
- "value": {
- "type": "number"
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "vertical",
+ "type": "string"
+ },
+ "value": {
+ "type": "number"
+ },
+ "label": {
+ "type": "string"
+ }
+ }
},
- "label": {
- "type": "string"
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "points"
+ ],
+ "properties": {
+ "type": {
+ "const": "path",
+ "type": "string"
+ },
+ "points": {
+ "minItems": 2,
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "x",
+ "y"
+ ],
+ "properties": {
+ "x": {
+ "type": "number"
+ },
+ "y": {
+ "type": "number"
+ }
+ }
+ }
+ },
+ "label": {
+ "type": "string"
+ }
+ }
}
- }
+ ]
},
{
"type": "object",
@@ -950,37 +1316,98 @@
"items": {
"anyOf": [
{
- "type": "object",
"allOf": [
{
- "type": "object",
- "required": [
- "type"
- ],
- "properties": {
- "type": {
- "anyOf": [
- {
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
"const": "identity",
"type": "string"
},
- {
+ "label": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
"const": "horizontal",
"type": "string"
},
- {
- "const": "vertical",
+ "value": {
+ "type": "number"
+ },
+ "label": {
"type": "string"
}
- ]
+ }
},
- "value": {
- "type": "number"
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "vertical",
+ "type": "string"
+ },
+ "value": {
+ "type": "number"
+ },
+ "label": {
+ "type": "string"
+ }
+ }
},
- "label": {
- "type": "string"
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "points"
+ ],
+ "properties": {
+ "type": {
+ "const": "path",
+ "type": "string"
+ },
+ "points": {
+ "minItems": 2,
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "x",
+ "y"
+ ],
+ "properties": {
+ "x": {
+ "type": "number"
+ },
+ "y": {
+ "type": "number"
+ }
+ }
+ }
+ },
+ "label": {
+ "type": "string"
+ }
+ }
}
- }
+ ]
},
{
"type": "object",
@@ -997,37 +1424,98 @@
]
},
{
- "type": "object",
"allOf": [
{
- "type": "object",
- "required": [
- "type"
- ],
- "properties": {
- "type": {
- "anyOf": [
- {
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
"const": "identity",
"type": "string"
},
- {
+ "label": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
"const": "horizontal",
"type": "string"
},
- {
- "const": "vertical",
+ "value": {
+ "type": "number"
+ },
+ "label": {
"type": "string"
}
- ]
+ }
},
- "value": {
- "type": "number"
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "vertical",
+ "type": "string"
+ },
+ "value": {
+ "type": "number"
+ },
+ "label": {
+ "type": "string"
+ }
+ }
},
- "label": {
- "type": "string"
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "points"
+ ],
+ "properties": {
+ "type": {
+ "const": "path",
+ "type": "string"
+ },
+ "points": {
+ "minItems": 2,
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "x",
+ "y"
+ ],
+ "properties": {
+ "x": {
+ "type": "number"
+ },
+ "y": {
+ "type": "number"
+ }
+ }
+ }
+ },
+ "label": {
+ "type": "string"
+ }
+ }
}
- }
+ ]
},
{
"type": "object",
@@ -1048,37 +1536,98 @@
]
},
{
- "type": "object",
"allOf": [
{
- "type": "object",
- "required": [
- "type"
- ],
- "properties": {
- "type": {
- "anyOf": [
- {
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
"const": "identity",
"type": "string"
},
- {
+ "label": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
"const": "horizontal",
"type": "string"
},
- {
- "const": "vertical",
+ "value": {
+ "type": "number"
+ },
+ "label": {
"type": "string"
}
- ]
- },
- "value": {
- "type": "number"
+ }
},
- "label": {
- "type": "string"
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "vertical",
+ "type": "string"
+ },
+ "value": {
+ "type": "number"
+ },
+ "label": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "points"
+ ],
+ "properties": {
+ "type": {
+ "const": "path",
+ "type": "string"
+ },
+ "points": {
+ "minItems": 2,
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "x",
+ "y"
+ ],
+ "properties": {
+ "x": {
+ "type": "number"
+ },
+ "y": {
+ "type": "number"
+ }
+ }
+ }
+ },
+ "label": {
+ "type": "string"
+ }
+ }
}
- }
+ ]
},
{
"type": "object",
@@ -1162,6 +1711,560 @@
}
}
]
+ },
+ {
+ "type": "object",
+ "allOf": [
+ {
+ "type": "object",
+ "required": [
+ "schemaVersion",
+ "evaluations",
+ "series",
+ "xAxis",
+ "yAxis"
+ ],
+ "properties": {
+ "schemaVersion": {
+ "const": "2.0",
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "evaluations": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "id",
+ "population"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "model": {
+ "type": "string"
+ },
+ "population": {
+ "type": "string"
+ },
+ "label": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "series": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "id",
+ "evaluationId",
+ "display"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "evaluationId": {
+ "type": "string"
+ },
+ "horizon": {
+ "minimum": 0,
+ "type": "number"
+ },
+ "display": {
+ "type": "object",
+ "required": [
+ "label",
+ "group",
+ "role"
+ ],
+ "properties": {
+ "label": {
+ "type": "string"
+ },
+ "group": {
+ "type": "string"
+ },
+ "role": {
+ "anyOf": [
+ {
+ "const": "model",
+ "type": "string"
+ },
+ {
+ "const": "population",
+ "type": "string"
+ },
+ {
+ "const": "evaluation",
+ "type": "string"
+ },
+ {
+ "const": "context",
+ "type": "string"
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "xAxis": {
+ "type": "object",
+ "required": [
+ "label"
+ ],
+ "properties": {
+ "label": {
+ "type": "string"
+ },
+ "domain": {
+ "type": "array",
+ "items": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "additionalItems": false,
+ "minItems": 2,
+ "maxItems": 2
+ }
+ }
+ },
+ "yAxis": {
+ "type": "object",
+ "required": [
+ "label"
+ ],
+ "properties": {
+ "label": {
+ "type": "string"
+ },
+ "domain": {
+ "type": "array",
+ "items": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "additionalItems": false,
+ "minItems": 2,
+ "maxItems": 2
+ }
+ }
+ },
+ "references": {
+ "type": "array",
+ "items": {
+ "anyOf": [
+ {
+ "allOf": [
+ {
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
+ "const": "identity",
+ "type": "string"
+ },
+ "label": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "horizontal",
+ "type": "string"
+ },
+ "value": {
+ "type": "number"
+ },
+ "label": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "vertical",
+ "type": "string"
+ },
+ "value": {
+ "type": "number"
+ },
+ "label": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "points"
+ ],
+ "properties": {
+ "type": {
+ "const": "path",
+ "type": "string"
+ },
+ "points": {
+ "minItems": 2,
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "x",
+ "y"
+ ],
+ "properties": {
+ "x": {
+ "type": "number"
+ },
+ "y": {
+ "type": "number"
+ }
+ }
+ }
+ },
+ "label": {
+ "type": "string"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "type": "object",
+ "required": [
+ "scope"
+ ],
+ "properties": {
+ "scope": {
+ "const": "global",
+ "type": "string"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "allOf": [
+ {
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
+ "const": "identity",
+ "type": "string"
+ },
+ "label": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "horizontal",
+ "type": "string"
+ },
+ "value": {
+ "type": "number"
+ },
+ "label": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "vertical",
+ "type": "string"
+ },
+ "value": {
+ "type": "number"
+ },
+ "label": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "points"
+ ],
+ "properties": {
+ "type": {
+ "const": "path",
+ "type": "string"
+ },
+ "points": {
+ "minItems": 2,
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "x",
+ "y"
+ ],
+ "properties": {
+ "x": {
+ "type": "number"
+ },
+ "y": {
+ "type": "number"
+ }
+ }
+ }
+ },
+ "label": {
+ "type": "string"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "type": "object",
+ "required": [
+ "scope",
+ "population"
+ ],
+ "properties": {
+ "scope": {
+ "const": "population",
+ "type": "string"
+ },
+ "population": {
+ "type": "string"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "allOf": [
+ {
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
+ "const": "identity",
+ "type": "string"
+ },
+ "label": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "horizontal",
+ "type": "string"
+ },
+ "value": {
+ "type": "number"
+ },
+ "label": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "const": "vertical",
+ "type": "string"
+ },
+ "value": {
+ "type": "number"
+ },
+ "label": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "points"
+ ],
+ "properties": {
+ "type": {
+ "const": "path",
+ "type": "string"
+ },
+ "points": {
+ "minItems": 2,
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "x",
+ "y"
+ ],
+ "properties": {
+ "x": {
+ "type": "number"
+ },
+ "y": {
+ "type": "number"
+ }
+ }
+ }
+ },
+ "label": {
+ "type": "string"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "type": "object",
+ "required": [
+ "scope",
+ "population",
+ "horizon"
+ ],
+ "properties": {
+ "scope": {
+ "const": "population_horizon",
+ "type": "string"
+ },
+ "population": {
+ "type": "string"
+ },
+ "horizon": {
+ "minimum": 0,
+ "type": "number"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "data",
+ "x",
+ "y"
+ ],
+ "properties": {
+ "type": {
+ "const": "gains",
+ "type": "string"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "seriesId",
+ "cutoff",
+ "ppcr",
+ "sensitivity"
+ ],
+ "properties": {
+ "seriesId": {
+ "type": "string"
+ },
+ "cutoff": {
+ "type": "number"
+ },
+ "ppcr": {
+ "minimum": 0,
+ "maximum": 1,
+ "type": "number"
+ },
+ "sensitivity": {
+ "minimum": 0,
+ "maximum": 1,
+ "type": "number"
+ }
+ }
+ }
+ },
+ "x": {
+ "const": "ppcr",
+ "type": "string"
+ },
+ "y": {
+ "const": "sensitivity",
+ "type": "string"
+ }
+ }
+ }
+ ]
}
]
}
diff --git a/src/rtichoke/_vendor/rtichoke_viz/rtichoke-viz.js b/src/rtichoke/_vendor/rtichoke_viz/rtichoke-viz.js
index 24cba4b3..3ecf24bb 100644
--- a/src/rtichoke/_vendor/rtichoke_viz/rtichoke-viz.js
+++ b/src/rtichoke/_vendor/rtichoke_viz/rtichoke-viz.js
@@ -2715,15 +2715,31 @@ var SeriesSpecSchema = Type.Object({
horizon: Type.Optional(Type.Number({ minimum: 0 })),
display: DisplayGroupingSpecSchema
});
-var ReferenceGeometrySchema = Type.Object({
- type: Type.Union([
- Type.Literal("identity"),
- Type.Literal("horizontal"),
- Type.Literal("vertical")
- ]),
- value: Type.Optional(Type.Number()),
- label: Type.Optional(Type.String())
+var ReferencePointSchema = Type.Object({
+ x: Type.Number(),
+ y: Type.Number()
});
+var ReferenceGeometrySchema = Type.Union([
+ Type.Object({
+ type: Type.Literal("identity"),
+ label: Type.Optional(Type.String())
+ }),
+ Type.Object({
+ type: Type.Literal("horizontal"),
+ value: Type.Number(),
+ label: Type.Optional(Type.String())
+ }),
+ Type.Object({
+ type: Type.Literal("vertical"),
+ value: Type.Number(),
+ label: Type.Optional(Type.String())
+ }),
+ Type.Object({
+ type: Type.Literal("path"),
+ points: Type.Array(ReferencePointSchema, { minItems: 2 }),
+ label: Type.Optional(Type.String())
+ })
+]);
var GlobalReferenceLineSpecSchema = Type.Intersect([
ReferenceGeometrySchema,
Type.Object({ scope: Type.Literal("global") })
@@ -2784,6 +2800,23 @@ var CalibrationV2SpecSchema = Type.Intersect([
})
]);
+// src/spec/v2/gains.ts
+var GainsV2DatumSchema = Type.Object({
+ seriesId: Type.String(),
+ cutoff: Type.Number(),
+ ppcr: Type.Number({ minimum: 0, maximum: 1 }),
+ sensitivity: Type.Number({ minimum: 0, maximum: 1 })
+});
+var GainsV2SpecSchema = Type.Intersect([
+ BaseChartV2SpecSchema,
+ Type.Object({
+ type: Type.Literal("gains"),
+ data: Type.Array(GainsV2DatumSchema),
+ x: Type.Literal("ppcr"),
+ y: Type.Literal("sensitivity")
+ })
+]);
+
// src/spec/v2/precision_recall.ts
var PrecisionRecallV2DatumSchema = Type.Object({
seriesId: Type.String(),
@@ -2820,7 +2853,12 @@ var RocV2SpecSchema = Type.Intersect([
// src/spec/v2/chart.ts
var RtichokeChartSpecV2Schema = Type.Union(
- [RocV2SpecSchema, CalibrationV2SpecSchema, PrecisionRecallV2SpecSchema],
+ [
+ RocV2SpecSchema,
+ CalibrationV2SpecSchema,
+ PrecisionRecallV2SpecSchema,
+ GainsV2SpecSchema
+ ],
{
$id: "https://rtichoke.dev/schema/viz/2.0.json",
title: "rtichoke visualization specification v2"
@@ -18605,6 +18643,18 @@ var BASE_STYLE2 = {
fontFamily: "Arial, Helvetica, sans-serif",
fontSize: "13px"
};
+function resolveV2RenderOptions(groupCount, options = {}) {
+ const width = options.width ?? 600;
+ const height = options.height ?? 600;
+ if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(height) || height <= 0) {
+ throw new Error("Renderer width and height must be positive finite numbers");
+ }
+ const colors = groupCount <= 1 ? ["#000000"] : [...options.colors ?? RTICHOKE_COLORS3];
+ if (colors.length < groupCount) {
+ throw new Error("Renderer colors must contain at least one color per display group");
+ }
+ return { width, height, colors: colors.slice(0, Math.max(groupCount, 1)) };
+}
function displayBySeries(spec) {
return new Map(spec.series.map((series) => [series.id, series.display]));
}
@@ -18616,31 +18666,39 @@ function seriesRenderData(spec, data) {
label: displays.get(datum2.seriesId).label
}));
}
+function referenceMarks(spec) {
+ const marks2 = [];
+ for (const reference of spec.references ?? []) {
+ if (reference.type === "identity") {
+ marks2.push(line([{ x: 0, y: 0 }, { x: 1, y: 1 }], {
+ x: "x",
+ y: "y",
+ stroke: "#BEBEBE",
+ strokeWidth: 2,
+ strokeDasharray: "4,4"
+ }));
+ } else if (reference.type === "path" && reference.points) {
+ marks2.push(line(reference.points, {
+ x: "x",
+ y: "y",
+ stroke: "#BEBEBE",
+ strokeWidth: 2,
+ strokeDasharray: "4,4"
+ }));
+ }
+ }
+ return marks2;
+}
function renderRocV2(spec) {
assertV2ReferentialIntegrity(spec);
const groups2 = [...new Set(spec.series.map((series) => series.display.group))];
const showLegend = groups2.length > 1;
- const data = seriesRenderData(spec, spec.data).map((datum2) => ({
- ...datum2,
- false_positive_rate: 1 - datum2.specificity
- }));
+ const data = seriesRenderData(spec, spec.data).map((datum2) => ({ ...datum2, false_positive_rate: 1 - datum2.specificity }));
const marks2 = [];
if (spec.references?.some((reference) => reference.type === "identity")) {
- marks2.push(line([{ x: 0, y: 0 }, { x: 1, y: 1 }], {
- x: "x",
- y: "y",
- stroke: "#BEBEBE",
- strokeWidth: 2
- }));
+ marks2.push(line([{ x: 0, y: 0 }, { x: 1, y: 1 }], { x: "x", y: "y", stroke: "#BEBEBE", strokeWidth: 2 }));
}
- marks2.push(line(data, {
- x: "false_positive_rate",
- y: "sensitivity",
- z: "seriesId",
- stroke: "group",
- strokeWidth: 2,
- tip: true
- }));
+ marks2.push(line(data, { x: "false_positive_rate", y: "sensitivity", z: "seriesId", stroke: "group", strokeWidth: 2, tip: true }));
return plot({
width: 600,
height: 600,
@@ -18661,34 +18719,11 @@ function renderCalibrationV2(spec) {
const data = seriesRenderData(spec, spec.data);
const marks2 = [];
if (spec.references?.some((reference) => reference.type === "identity")) {
- marks2.push(line([{ x: 0, y: 0 }, { x: 1, y: 1 }], {
- x: "x",
- y: "y",
- stroke: "#BEBEBE",
- strokeWidth: 2,
- strokeDasharray: "4,4"
- }));
+ marks2.push(line([{ x: 0, y: 0 }, { x: 1, y: 1 }], { x: "x", y: "y", stroke: "#BEBEBE", strokeWidth: 2, strokeDasharray: "4,4" }));
}
- marks2.push(line(data, {
- x: "predicted",
- y: "observed",
- z: "seriesId",
- stroke: "group",
- strokeWidth: 2,
- tip: true
- }));
+ marks2.push(line(data, { x: "predicted", y: "observed", z: "seriesId", stroke: "group", strokeWidth: 2, tip: true }));
const discrete = data.filter((datum2) => datum2.method === "discrete");
- if (discrete.length > 0) {
- marks2.push(dot(discrete, {
- x: "predicted",
- y: "observed",
- fill: "group",
- stroke: "white",
- strokeWidth: 1.5,
- r: 5,
- tip: true
- }));
- }
+ if (discrete.length > 0) marks2.push(dot(discrete, { x: "predicted", y: "observed", fill: "group", stroke: "white", strokeWidth: 1.5, r: 5, tip: true }));
const hasDistribution = (spec.distribution?.length ?? 0) > 0;
const calibration = plot({
width: 600,
@@ -18713,14 +18748,7 @@ function renderCalibrationV2(spec) {
x: { label: spec.xAxis.label, domain: spec.xAxis.domain, grid: false, ticks: 6 },
y: { label: null, grid: false, ticks: 3 },
color: { legend: false, range: colorRange },
- marks: [rectY(distribution, {
- x1: (datum2) => datum2.midpoint - datum2.binWidth / 2,
- x2: (datum2) => datum2.midpoint + datum2.binWidth / 2,
- y: "count",
- fill: "group",
- fillOpacity: 1 / Math.max(groups2.length, 1),
- tip: true
- })]
+ marks: [rectY(distribution, { x1: (datum2) => datum2.midpoint - datum2.binWidth / 2, x2: (datum2) => datum2.midpoint + datum2.binWidth / 2, y: "count", fill: "group", fillOpacity: 1 / Math.max(groups2.length, 1), tip: true })]
});
const container = document.createElement("div");
container.style.width = "600px";
@@ -18734,22 +18762,9 @@ function renderPrecisionRecallV2(spec) {
const showLegend = groups2.length > 1;
const data = seriesRenderData(spec, spec.data);
const marks2 = [];
- for (const reference of spec.references ?? []) {
- if (reference.type !== "horizontal" || reference.value === void 0) continue;
- marks2.push(ruleY([reference.value], {
- stroke: "#BEBEBE",
- strokeWidth: 2,
- strokeDasharray: "4,4"
- }));
- }
- marks2.push(line(data, {
- x: "sensitivity",
- y: "ppv",
- z: "seriesId",
- stroke: "group",
- strokeWidth: 2,
- tip: true
- }));
+ for (const reference of spec.references ?? []) if (reference.type === "horizontal" && reference.value !== void 0) marks2.push(ruleY([reference.value], { stroke: "#BEBEBE", strokeWidth: 2, strokeDasharray: "4,4" }));
+ marks2.push(line(data, { x: "sensitivity", y: "ppv", z: "seriesId", stroke: "group", strokeWidth: 2, tip: true }));
+ marks2.push(dot(data, { x: "sensitivity", y: "ppv", fill: "group", stroke: "white", strokeWidth: 1.5, r: 4, tip: true }));
return plot({
width: 600,
height: 600,
@@ -18758,7 +18773,28 @@ function renderPrecisionRecallV2(spec) {
style: BASE_STYLE2,
x: { label: spec.xAxis.label, domain: spec.xAxis.domain, grid: false, ticks: 6 },
y: { label: spec.yAxis.label, domain: spec.yAxis.domain, grid: false, ticks: 6 },
- color: { legend: showLegend, range: showLegend ? RTICHOKE_COLORS3 : ["#000000"] },
+ color: { legend: showLegend, domain: groups2, range: showLegend ? RTICHOKE_COLORS3 : ["#000000"] },
+ marks: marks2
+ });
+}
+function renderGainsV2(spec, options = {}) {
+ assertV2ReferentialIntegrity(spec);
+ const groups2 = [...new Set(spec.series.map((series) => series.display.group))];
+ const showLegend = groups2.length > 1;
+ const resolved = resolveV2RenderOptions(groups2.length, options);
+ const data = seriesRenderData(spec, spec.data);
+ const marks2 = referenceMarks(spec);
+ marks2.push(line(data, { x: "ppcr", y: "sensitivity", z: "seriesId", stroke: "group", strokeWidth: 2, tip: true }));
+ marks2.push(dot(data, { x: "ppcr", y: "sensitivity", fill: "group", stroke: "white", strokeWidth: 1.5, r: 4, tip: true }));
+ return plot({
+ width: resolved.width,
+ height: resolved.height,
+ marginLeft: 64,
+ marginBottom: 56,
+ style: BASE_STYLE2,
+ x: { label: spec.xAxis.label, domain: spec.xAxis.domain, grid: false, ticks: 6 },
+ y: { label: spec.yAxis.label, domain: spec.yAxis.domain, grid: false, ticks: 6 },
+ color: { legend: showLegend, domain: groups2, range: resolved.colors },
marks: marks2
});
}
@@ -18768,6 +18804,7 @@ export {
DisplayGroupingSpecSchema,
DisplayRoleSchema,
EvaluationSpecSchema,
+ GainsV2SpecSchema,
PrecisionRecallV2SpecSchema,
ReferenceLineV2SpecSchema,
RocSpecSchema,
@@ -18780,6 +18817,7 @@ export {
calibrationV2SpecFromRtichokeRows,
renderCalibration,
renderCalibrationV2,
+ renderGainsV2,
renderPrecisionRecallV2,
renderRoc,
renderRocV2,
diff --git a/src/rtichoke/discrimination/gains.py b/src/rtichoke/discrimination/gains.py
index cb131d8a..40a0fd16 100644
--- a/src/rtichoke/discrimination/gains.py
+++ b/src/rtichoke/discrimination/gains.py
@@ -2,7 +2,7 @@
A module for Gains Curves using Plotly helpers
"""
-from typing import Dict, List, Sequence, Union
+from typing import Any, Dict, List, Sequence, Union
from plotly.graph_objs._figure import Figure
from rtichoke.processing.binary_color_values import _apply_color_values_binary
from rtichoke.processing.plotly_helper_functions import (
@@ -16,6 +16,10 @@
)
import numpy as np
import polars as pl
+from rtichoke._renderers import _render_gains_v2, _validate_renderer
+from rtichoke._viz_spec_v2 import _gains_v2_spec_from_performance_data
+from rtichoke.performance_data.performance_data import prepare_performance_data
+from rtichoke.processing.evaluation_semantics import _build_evaluation_metadata
def _get_gains_aj_estimates_times(performance_data: pl.DataFrame) -> pl.DataFrame:
@@ -90,7 +94,8 @@ def create_gains_curve(
"#D1603D",
"#585123",
],
-) -> Figure:
+ renderer: str = "plotly",
+) -> Any:
"""Creates a Gains curve.
A Gains curve is a marketing and business analytics tool that evaluates
@@ -113,12 +118,37 @@ def create_gains_curve(
The width and height of the plot in pixels. Defaults to 600.
color_values : List[str], optional
A list of hex color strings for the plot lines.
+ renderer : {"plotly", "matplotlib", "browser", "rtichoke_viz"}, optional
+ Rendering backend. The default, ``"plotly"``, preserves the existing
+ return value and behavior. ``"matplotlib"`` requires the optional
+ Matplotlib dependency. ``"browser"`` and its ``"rtichoke_viz"`` alias
+ return an offline browser chart backed by the packaged TypeScript bundle.
Returns
-------
- Figure
- A Plotly ``Figure`` object representing the Gains curve.
+ Figure or RtichokeBrowserChart
+ A Plotly or Matplotlib figure, or an offline browser chart, depending
+ on ``renderer``.
"""
+ selected_renderer = _validate_renderer(renderer)
+ if selected_renderer != "plotly":
+ performance_data = prepare_performance_data(
+ probs=probs,
+ reals=reals,
+ stratified_by=stratified_by,
+ by=by,
+ )
+ evaluation_metadata = _build_evaluation_metadata(probs, reals, np.array([]))
+ spec = _gains_v2_spec_from_performance_data(
+ performance_data, evaluation_metadata
+ )
+ return _render_gains_v2(
+ spec,
+ renderer=selected_renderer,
+ size=size,
+ color_values=color_values,
+ )
+
fig = _create_rtichoke_plotly_curve_binary(
probs,
reals,
diff --git a/tests/test_gains_renderers.py b/tests/test_gains_renderers.py
new file mode 100644
index 00000000..8acaafc4
--- /dev/null
+++ b/tests/test_gains_renderers.py
@@ -0,0 +1,70 @@
+from pathlib import Path
+
+import matplotlib.figure
+import numpy as np
+import plotly.graph_objects as go
+import plotly.io as pio
+import pytest
+
+from rtichoke import create_gains_curve
+from rtichoke._renderers import RtichokeBrowserChart
+
+
+def _inputs():
+ return (
+ {
+ "Model A": np.array([0.05, 0.2, 0.7, 0.95]),
+ "Model B": np.array([0.1, 0.4, 0.6, 0.9]),
+ },
+ np.array([0, 0, 1, 1]),
+ )
+
+
+def test_default_and_explicit_plotly_preserve_existing_renderer():
+ probs, reals = _inputs()
+
+ default = create_gains_curve(probs, reals, by=0.25)
+ explicit = create_gains_curve(probs, reals, by=0.25, renderer="plotly")
+
+ assert isinstance(default, go.Figure)
+ assert pio.to_json(default) == pio.to_json(explicit)
+
+
+def test_matplotlib_renders_canonical_gains_spec():
+ probs, reals = _inputs()
+
+ figure = create_gains_curve(probs, reals, by=0.25, renderer="matplotlib")
+
+ assert isinstance(figure, matplotlib.figure.Figure)
+ assert len(figure.axes[0].lines) == 4 # random, perfect, and two series
+ assert figure.axes[0].get_xlabel() == "Predicted Positives (Rate)"
+
+
+@pytest.mark.parametrize("renderer", ["browser", "rtichoke_viz"])
+def test_browser_renderer_writes_offline_v2_chart(renderer: str, tmp_path: Path):
+ probs, reals = _inputs()
+
+ chart = create_gains_curve(probs, reals, by=0.25, renderer=renderer)
+ assert isinstance(chart, RtichokeBrowserChart)
+ assert chart.spec["schemaVersion"] == "2.0"
+ assert chart.spec["type"] == "gains"
+ assert len(chart.spec["evaluations"]) == 2
+ assert {row["seriesId"] for row in chart.spec["data"]} == {
+ "series-1",
+ "series-2",
+ }
+
+ output = chart.write_html(tmp_path / "gains.html")
+ html = output.read_text(encoding="utf-8")
+ assert 'import { renderGainsV2 } from "./rtichoke-viz.js"' in html
+ assert '"schemaVersion":"2.0"' in html
+ assert (tmp_path / "rtichoke-viz.js").is_file()
+ assert (tmp_path / "rtichoke-viz.css").is_file()
+ assert "http://" not in html and "https://" not in html
+
+
+def test_unsupported_renderer_is_clear():
+ probs, reals = _inputs()
+
+ with pytest.raises(ValueError, match="Unsupported renderer 'canvas'"):
+ create_gains_curve(probs, reals, renderer="canvas")
diff --git a/tests/test_rtichoke_viz_vendor.py b/tests/test_rtichoke_viz_vendor.py
index ce36ad7f..aa069e3c 100644
--- a/tests/test_rtichoke_viz_vendor.py
+++ b/tests/test_rtichoke_viz_vendor.py
@@ -1,19 +1,35 @@
+import hashlib
+import tarfile
from pathlib import Path
_VENDOR = Path(__file__).parents[1] / "src" / "rtichoke" / "_vendor" / "rtichoke_viz"
-def test_vendored_rtichoke_viz_v020_provenance_and_schemas():
+def test_vendored_rtichoke_viz_v030_provenance_archive_and_schemas():
provenance = (_VENDOR / "VENDORED_FROM").read_text()
- assert "release=v0.2.0" in provenance
- assert "source_commit=45dc109a6a0679d0f8f3f9452d8de9306a89b906" in provenance
- assert "archive=rtichoke-viz-0.2.0.tar.gz" in provenance
+ assert "release=v0.3.0" in provenance
+ assert "source_commit=aca9188ea856167557efb20980a0b43e0481b8c8" in provenance
+ assert "archive=rtichoke-viz-0.3.0.tar.gz" in provenance
assert (
- "sha256=3861277c01b3983f8b344a9ee0237c7d09fd0ba4c3d1e0cce489962e7b559d9f"
+ "sha256=558f8d9e16f9544659b84e33f72511065163291a1b97a3c5511b61d1e1f0cac1"
in provenance
)
+ archive = _VENDOR / "rtichoke-viz-0.3.0.tar.gz"
+ assert hashlib.sha256(archive.read_bytes()).hexdigest() == (
+ "558f8d9e16f9544659b84e33f72511065163291a1b97a3c5511b61d1e1f0cac1"
+ )
+ with tarfile.open(archive, "r:gz") as release:
+ assert set(release.getnames()) == {
+ "rtichoke-viz-0.3.0",
+ "rtichoke-viz-0.3.0/MANIFEST",
+ "rtichoke-viz-0.3.0/rtichoke-viz.css",
+ "rtichoke-viz-0.3.0/rtichoke-viz.js",
+ "rtichoke-viz-0.3.0/rtichoke-viz.schema.json",
+ "rtichoke-viz-0.3.0/rtichoke-viz-v2.schema.json",
+ }
+
assert (_VENDOR / "rtichoke-viz.js").stat().st_size > 0
assert (_VENDOR / "rtichoke-viz.css").stat().st_size > 0
@@ -23,11 +39,13 @@ def test_vendored_rtichoke_viz_v020_provenance_and_schemas():
assert '"$id": "https://rtichoke.dev/schema/viz/2.0.json"' in v2_schema
-def test_v020_bundle_keeps_v1_browser_exports():
- bundle = (_VENDOR / "rtichoke-viz.js").read_text()
+def test_v030_bundle_keeps_v1_and_adds_v2_browser_exports():
+ bundle = (_VENDOR / "rtichoke-viz.js").read_text(encoding="utf-8")
for export_name in (
"renderRoc",
"renderCalibration",
"RtichokeChartSpecSchema",
+ "renderGainsV2",
+ "RtichokeChartSpecV2Schema",
):
assert export_name in bundle
diff --git a/uv.lock b/uv.lock
index 14353270..d921c5a3 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2366,43 +2366,50 @@ wheels = [
[[package]]
name = "pyzmq"
-version = "26.4.0"
+version = "27.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "implementation_name == 'pypy'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/b1/11/b9213d25230ac18a71b39b3723494e57adebe36e066397b961657b3b41c1/pyzmq-26.4.0.tar.gz", hash = "sha256:4bd13f85f80962f91a651a7356fe0472791a5f7a92f227822b5acf44795c626d", size = 278293, upload-time = "2025-04-04T12:05:44.049Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/10/44/a778555ebfdf6c7fc00816aad12d185d10a74d975800341b1bc36bad1187/pyzmq-26.4.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:5227cb8da4b6f68acfd48d20c588197fd67745c278827d5238c707daf579227b", size = 1341586, upload-time = "2025-04-04T12:03:41.954Z" },
- { url = "https://files.pythonhosted.org/packages/9c/4f/f3a58dc69ac757e5103be3bd41fb78721a5e17da7cc617ddb56d973a365c/pyzmq-26.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1c07a7fa7f7ba86554a2b1bef198c9fed570c08ee062fd2fd6a4dcacd45f905", size = 665880, upload-time = "2025-04-04T12:03:43.45Z" },
- { url = "https://files.pythonhosted.org/packages/fe/45/50230bcfb3ae5cb98bee683b6edeba1919f2565d7cc1851d3c38e2260795/pyzmq-26.4.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae775fa83f52f52de73183f7ef5395186f7105d5ed65b1ae65ba27cb1260de2b", size = 902216, upload-time = "2025-04-04T12:03:45.572Z" },
- { url = "https://files.pythonhosted.org/packages/41/59/56bbdc5689be5e13727491ad2ba5efd7cd564365750514f9bc8f212eef82/pyzmq-26.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66c760d0226ebd52f1e6b644a9e839b5db1e107a23f2fcd46ec0569a4fdd4e63", size = 859814, upload-time = "2025-04-04T12:03:47.188Z" },
- { url = "https://files.pythonhosted.org/packages/81/b1/57db58cfc8af592ce94f40649bd1804369c05b2190e4cbc0a2dad572baeb/pyzmq-26.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ef8c6ecc1d520debc147173eaa3765d53f06cd8dbe7bd377064cdbc53ab456f5", size = 855889, upload-time = "2025-04-04T12:03:49.223Z" },
- { url = "https://files.pythonhosted.org/packages/e8/92/47542e629cbac8f221c230a6d0f38dd3d9cff9f6f589ed45fdf572ffd726/pyzmq-26.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3150ef4084e163dec29ae667b10d96aad309b668fac6810c9e8c27cf543d6e0b", size = 1197153, upload-time = "2025-04-04T12:03:50.591Z" },
- { url = "https://files.pythonhosted.org/packages/07/e5/b10a979d1d565d54410afc87499b16c96b4a181af46e7645ab4831b1088c/pyzmq-26.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4448c9e55bf8329fa1dcedd32f661bf611214fa70c8e02fee4347bc589d39a84", size = 1507352, upload-time = "2025-04-04T12:03:52.473Z" },
- { url = "https://files.pythonhosted.org/packages/ab/58/5a23db84507ab9c01c04b1232a7a763be66e992aa2e66498521bbbc72a71/pyzmq-26.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e07dde3647afb084d985310d067a3efa6efad0621ee10826f2cb2f9a31b89d2f", size = 1406834, upload-time = "2025-04-04T12:03:54Z" },
- { url = "https://files.pythonhosted.org/packages/22/74/aaa837b331580c13b79ac39396601fb361454ee184ca85e8861914769b99/pyzmq-26.4.0-cp312-cp312-win32.whl", hash = "sha256:ba034a32ecf9af72adfa5ee383ad0fd4f4e38cdb62b13624278ef768fe5b5b44", size = 577992, upload-time = "2025-04-04T12:03:55.815Z" },
- { url = "https://files.pythonhosted.org/packages/30/0f/55f8c02c182856743b82dde46b2dc3e314edda7f1098c12a8227eeda0833/pyzmq-26.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:056a97aab4064f526ecb32f4343917a4022a5d9efb6b9df990ff72e1879e40be", size = 640466, upload-time = "2025-04-04T12:03:57.231Z" },
- { url = "https://files.pythonhosted.org/packages/e4/29/073779afc3ef6f830b8de95026ef20b2d1ec22d0324d767748d806e57379/pyzmq-26.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f23c750e485ce1eb639dbd576d27d168595908aa2d60b149e2d9e34c9df40e0", size = 556342, upload-time = "2025-04-04T12:03:59.218Z" },
- { url = "https://files.pythonhosted.org/packages/d7/20/fb2c92542488db70f833b92893769a569458311a76474bda89dc4264bd18/pyzmq-26.4.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:c43fac689880f5174d6fc864857d1247fe5cfa22b09ed058a344ca92bf5301e3", size = 1339484, upload-time = "2025-04-04T12:04:00.671Z" },
- { url = "https://files.pythonhosted.org/packages/58/29/2f06b9cabda3a6ea2c10f43e67ded3e47fc25c54822e2506dfb8325155d4/pyzmq-26.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:902aca7eba477657c5fb81c808318460328758e8367ecdd1964b6330c73cae43", size = 666106, upload-time = "2025-04-04T12:04:02.366Z" },
- { url = "https://files.pythonhosted.org/packages/77/e4/dcf62bd29e5e190bd21bfccaa4f3386e01bf40d948c239239c2f1e726729/pyzmq-26.4.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5e48a830bfd152fe17fbdeaf99ac5271aa4122521bf0d275b6b24e52ef35eb6", size = 902056, upload-time = "2025-04-04T12:04:03.919Z" },
- { url = "https://files.pythonhosted.org/packages/1a/cf/b36b3d7aea236087d20189bec1a87eeb2b66009731d7055e5c65f845cdba/pyzmq-26.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31be2b6de98c824c06f5574331f805707c667dc8f60cb18580b7de078479891e", size = 860148, upload-time = "2025-04-04T12:04:05.581Z" },
- { url = "https://files.pythonhosted.org/packages/18/a6/f048826bc87528c208e90604c3bf573801e54bd91e390cbd2dfa860e82dc/pyzmq-26.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6332452034be001bbf3206ac59c0d2a7713de5f25bb38b06519fc6967b7cf771", size = 855983, upload-time = "2025-04-04T12:04:07.096Z" },
- { url = "https://files.pythonhosted.org/packages/0a/27/454d34ab6a1d9772a36add22f17f6b85baf7c16e14325fa29e7202ca8ee8/pyzmq-26.4.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:da8c0f5dd352136853e6a09b1b986ee5278dfddfebd30515e16eae425c872b30", size = 1197274, upload-time = "2025-04-04T12:04:08.523Z" },
- { url = "https://files.pythonhosted.org/packages/f4/3d/7abfeab6b83ad38aa34cbd57c6fc29752c391e3954fd12848bd8d2ec0df6/pyzmq-26.4.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:f4ccc1a0a2c9806dda2a2dd118a3b7b681e448f3bb354056cad44a65169f6d86", size = 1507120, upload-time = "2025-04-04T12:04:10.58Z" },
- { url = "https://files.pythonhosted.org/packages/13/ff/bc8d21dbb9bc8705126e875438a1969c4f77e03fc8565d6901c7933a3d01/pyzmq-26.4.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1c0b5fceadbab461578daf8d1dcc918ebe7ddd2952f748cf30c7cf2de5d51101", size = 1406738, upload-time = "2025-04-04T12:04:12.509Z" },
- { url = "https://files.pythonhosted.org/packages/f5/5d/d4cd85b24de71d84d81229e3bbb13392b2698432cf8fdcea5afda253d587/pyzmq-26.4.0-cp313-cp313-win32.whl", hash = "sha256:28e2b0ff5ba4b3dd11062d905682bad33385cfa3cc03e81abd7f0822263e6637", size = 577826, upload-time = "2025-04-04T12:04:14.289Z" },
- { url = "https://files.pythonhosted.org/packages/c6/6c/f289c1789d7bb6e5a3b3bef7b2a55089b8561d17132be7d960d3ff33b14e/pyzmq-26.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:23ecc9d241004c10e8b4f49d12ac064cd7000e1643343944a10df98e57bc544b", size = 640406, upload-time = "2025-04-04T12:04:15.757Z" },
- { url = "https://files.pythonhosted.org/packages/b3/99/676b8851cb955eb5236a0c1e9ec679ea5ede092bf8bf2c8a68d7e965cac3/pyzmq-26.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:1edb0385c7f025045d6e0f759d4d3afe43c17a3d898914ec6582e6f464203c08", size = 556216, upload-time = "2025-04-04T12:04:17.212Z" },
- { url = "https://files.pythonhosted.org/packages/65/c2/1fac340de9d7df71efc59d9c50fc7a635a77b103392d1842898dd023afcb/pyzmq-26.4.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:93a29e882b2ba1db86ba5dd5e88e18e0ac6b627026c5cfbec9983422011b82d4", size = 1333769, upload-time = "2025-04-04T12:04:18.665Z" },
- { url = "https://files.pythonhosted.org/packages/5c/c7/6c03637e8d742c3b00bec4f5e4cd9d1c01b2f3694c6f140742e93ca637ed/pyzmq-26.4.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb45684f276f57110bb89e4300c00f1233ca631f08f5f42528a5c408a79efc4a", size = 658826, upload-time = "2025-04-04T12:04:20.405Z" },
- { url = "https://files.pythonhosted.org/packages/a5/97/a8dca65913c0f78e0545af2bb5078aebfc142ca7d91cdaffa1fbc73e5dbd/pyzmq-26.4.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f72073e75260cb301aad4258ad6150fa7f57c719b3f498cb91e31df16784d89b", size = 891650, upload-time = "2025-04-04T12:04:22.413Z" },
- { url = "https://files.pythonhosted.org/packages/7d/7e/f63af1031eb060bf02d033732b910fe48548dcfdbe9c785e9f74a6cc6ae4/pyzmq-26.4.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be37e24b13026cfedd233bcbbccd8c0bcd2fdd186216094d095f60076201538d", size = 849776, upload-time = "2025-04-04T12:04:23.959Z" },
- { url = "https://files.pythonhosted.org/packages/f6/fa/1a009ce582802a895c0d5fe9413f029c940a0a8ee828657a3bb0acffd88b/pyzmq-26.4.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:237b283044934d26f1eeff4075f751b05d2f3ed42a257fc44386d00df6a270cf", size = 842516, upload-time = "2025-04-04T12:04:25.449Z" },
- { url = "https://files.pythonhosted.org/packages/6e/bc/f88b0bad0f7a7f500547d71e99f10336f2314e525d4ebf576a1ea4a1d903/pyzmq-26.4.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:b30f862f6768b17040929a68432c8a8be77780317f45a353cb17e423127d250c", size = 1189183, upload-time = "2025-04-04T12:04:27.035Z" },
- { url = "https://files.pythonhosted.org/packages/d9/8c/db446a3dd9cf894406dec2e61eeffaa3c07c3abb783deaebb9812c4af6a5/pyzmq-26.4.0-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:c80fcd3504232f13617c6ab501124d373e4895424e65de8b72042333316f64a8", size = 1495501, upload-time = "2025-04-04T12:04:28.833Z" },
- { url = "https://files.pythonhosted.org/packages/05/4c/bf3cad0d64c3214ac881299c4562b815f05d503bccc513e3fd4fdc6f67e4/pyzmq-26.4.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:26a2a7451606b87f67cdeca2c2789d86f605da08b4bd616b1a9981605ca3a364", size = 1395540, upload-time = "2025-04-04T12:04:30.562Z" },
+sdist = { url = "https://files.pythonhosted.org/packages/e7/8d/5b3d5631c2f4b4b8862f64cd0c9eb777b5710eeb5125b4be8dd0a200a4c0/pyzmq-27.2.0.tar.gz", hash = "sha256:54d4259d1bfae24ecdb5ca79f7acc2eac6c286a02d6a0ae617797cb45f0726d3", size = 292316, upload-time = "2026-08-20T19:08:21.19Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/57/8a/153532fa53db30e116118164f3af269a1f3966b3e2ba32c89b12fe864bd8/pyzmq-27.2.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:591c8de5851c5ea372194469fe97587b97c3b641e9a70f31bb3474acbfde0241", size = 1431074, upload-time = "2026-08-20T19:06:40.601Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/ef/c08b91248bb90a9efa81fa00ba81b69c157c74d0c5efbb2c319d91babb62/pyzmq-27.2.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:00e73942ef12cecbc7951c4a9104bb8ffaed742abb13af2da6833d90dd368cef", size = 973915, upload-time = "2026-08-20T19:06:42.037Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/78/a3a3a86c2b00fadb92ece1ca4f8f028d62b2ce9ac3526097239ab2d6fba9/pyzmq-27.2.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f8079d0521fe94bbb401fe9407578b28f3701627c8be2c9f7e0c5b77dcb0109", size = 697722, upload-time = "2026-08-20T19:06:43.325Z" },
+ { url = "https://files.pythonhosted.org/packages/62/2c/d5828306f795e8d34676d266823b74e2101e0ad3760d12083de3e02abbb2/pyzmq-27.2.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dea74fd65f1fc5f7fe167916a473ebe6ed6174e5e5d9de11ea6583661be6cf43", size = 872258, upload-time = "2026-08-20T19:06:44.627Z" },
+ { url = "https://files.pythonhosted.org/packages/09/52/51253b78fd8739293e283407eeecb14215c02c71b6519af21f6eed8e69cd/pyzmq-27.2.0-cp312-abi3-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcc99ca132b667a4ed750afd42db4ea73288f18425a9b2e3c0af095665c491f5", size = 739591, upload-time = "2026-08-20T19:06:46.214Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/3e/142c85b67a4c9678629b0cf6d5125b29663d75be69bfaa57a3cac344d780/pyzmq-27.2.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b8d5f66e4a8246cf77f7b8f7902af64f00553368fa0373c89d99b78f0ad79394", size = 1689031, upload-time = "2026-08-20T19:06:47.612Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/ee/0776fb0f98ed1eb74d77240087fef0ab045b6ad15cb09555c6c5134c98ad/pyzmq-27.2.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:d1526b42a2e725b84ed226f37becedc250c6347594e5ed304e4e9aff68c9aec3", size = 2059547, upload-time = "2026-08-20T19:06:49.064Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/0e/ec77f691a4aebe29ab6329f996fb0e0270c876a3016086e3ca6ef733bcae/pyzmq-27.2.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f707bcf2c1d007d14d70531d4dd7b41060881c73efa845580bf6faaf9ea24d42", size = 1910457, upload-time = "2026-08-20T19:06:50.783Z" },
+ { url = "https://files.pythonhosted.org/packages/30/97/1f5530ff4fc271b4597048371d5af972c2baab51be132ba15874e0327a6a/pyzmq-27.2.0-cp312-abi3-win32.whl", hash = "sha256:fdaaa4ea3242f6ad298eb5177eb042aea5c73c30e76d20caee7b15af20d24ec2", size = 563450, upload-time = "2026-08-20T19:06:52.307Z" },
+ { url = "https://files.pythonhosted.org/packages/02/8b/b83f7780dad22e0878e4c7bd9158ebd24ed12bc3d5e3a471cd0576f77ded/pyzmq-27.2.0-cp312-abi3-win_amd64.whl", hash = "sha256:2c218c6ab8bc447ba62054b581fd30209689d199c6ecb253f79615ca74a38e12", size = 628633, upload-time = "2026-08-20T19:06:53.809Z" },
+ { url = "https://files.pythonhosted.org/packages/52/aa/3918b5ac7f9987bd9c421b065074fd7409ded88f856f2c704a24341877ec/pyzmq-27.2.0-cp312-abi3-win_arm64.whl", hash = "sha256:348d6fd3e4b81ae4580622ea8c2ea60224e84b2ac1b3be4482e6edc7de06e7a3", size = 556006, upload-time = "2026-08-20T19:06:55.242Z" },
+ { url = "https://files.pythonhosted.org/packages/83/5e/d0541596b48c5a19f85dcbea83d6673d8e91681cdf853eb194c31fc9766e/pyzmq-27.2.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:c551b9e2f86dc625fcb1a032c0d68042678caf96a8dd7c28796766b673bd5b52", size = 1127193, upload-time = "2026-08-20T19:06:56.545Z" },
+ { url = "https://files.pythonhosted.org/packages/50/9f/8c7411bb283982d46e6d56dca6a095678c87eb0398daead12776d9881ac2/pyzmq-27.2.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:288cc790da0e3064a14a38ddc56ba169dada8c8af4cb86518db2bcbd380eedbb", size = 1166833, upload-time = "2026-08-20T19:06:58.011Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/84/a849161ff88b2de9b991cc8ab332218824741122fdc4fdf222a5b822ac8c/pyzmq-27.2.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:3d45189c0c3c99f817b7fefff0d32eeef684cf33e1e3c0fc4281515357c54702", size = 1134452, upload-time = "2026-08-20T19:06:59.898Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/34/ff4aaff0cfba2a4d7ad1a16ffedc52c6deb89fcf673d455085446b23f215/pyzmq-27.2.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:d61910b52be5b2cd8b248dbcbe3a1b0275556a7d99fb613fc43323b546e273b8", size = 1167520, upload-time = "2026-08-20T19:07:01.283Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/07/42111e9dc1041d78b4443d6eb1b82b027f1a58178dc8a38385effbc72ad5/pyzmq-27.2.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:3ab6eb88590e510ab16715c32dbba12000da9bee989fdadd9ee19a234c492eb7", size = 1466289, upload-time = "2026-08-20T19:07:02.738Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/b4/def7a478458da78665840564161772e7e938600c32a89f28e8b221b54d2d/pyzmq-27.2.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1ecbdd131b9669f62d3a45afee5527c7ae9f141e4301267f21714c90bd21725f", size = 975868, upload-time = "2026-08-20T19:07:04.155Z" },
+ { url = "https://files.pythonhosted.org/packages/38/d5/e3e85f7fea37153097aaff49db9e33093909cc2a7b22c1ac4ebe546600fc/pyzmq-27.2.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3146385b94a760236c5eceff468a66a296a716ca98a2e0f9217b1518118466b1", size = 706054, upload-time = "2026-08-20T19:07:05.623Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/ef/3b7d9449b223183222bf517245e1e53d5f1ab8c10be8b45f6a301b2f994a/pyzmq-27.2.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9846e881620dd62566ca76a53e384c3f37490faf4b9240aebc7498810dfca853", size = 878984, upload-time = "2026-08-20T19:07:07.153Z" },
+ { url = "https://files.pythonhosted.org/packages/be/a5/8b49dbd494f6dcfda69dc4cade322a4b02706ef4e3d30cc366d4e369899f/pyzmq-27.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d9527e3dbaef1edaeeb2446fa7379446814a43ade8adc7c4a5ebe69437815ddd", size = 1697489, upload-time = "2026-08-20T19:07:08.945Z" },
+ { url = "https://files.pythonhosted.org/packages/da/5a/4bb8280901130c26ea25f0cbb4a6d39d94250860c6b3dbd912f1cf48fca7/pyzmq-27.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:56b48fa9d478a3af7254f397697a62f5ad3e1bb677e200b2701f0c290d97e5af", size = 2064236, upload-time = "2026-08-20T19:07:10.384Z" },
+ { url = "https://files.pythonhosted.org/packages/de/38/f433af66922554adb2b5f79e897018c8e19a90b9eaeb49c4814f8355ebe4/pyzmq-27.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bf0b6e4ce1bb089751c504c5493d6b0557eabd02dd21b76e9086cf964234b103", size = 1917424, upload-time = "2026-08-20T19:07:11.909Z" },
+ { url = "https://files.pythonhosted.org/packages/36/81/ea1c1ae3f801d96ba2c269e056761ebcfe023476e651d3af2a7817962051/pyzmq-27.2.0-cp314-cp314t-win32.whl", hash = "sha256:fba8afcf265c6e9fbe1594cb045d4765c6c9a7d607653a8196067ef23566b843", size = 591103, upload-time = "2026-08-20T19:07:13.451Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/04/149a627707e780fa9f2c1ede3590c14fa6b18b5576d15744342622299a50/pyzmq-27.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d1bc1d380a91d954ed5fc9f12915dba014eed0978d2de05ee7ca688bdaac144a", size = 670215, upload-time = "2026-08-20T19:07:15.069Z" },
+ { url = "https://files.pythonhosted.org/packages/30/ba/f9c3c1536c41ef3dbf765ea04218990e2056e558f98184ecd883767fc501/pyzmq-27.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:c7cfb75caa83f5153c687e9d2107f64b5ef0ef0d6edd260d3ff920baaaa69101", size = 582252, upload-time = "2026-08-20T19:07:16.582Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/00/78fe097a304a408275747ce43f20428789130b059c5649956277c20f30cf/pyzmq-27.2.0-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:c5129a8fe43ecc49b99eb75616603d483a3c2fcaef504988fafe8ea392aea98b", size = 1134295, upload-time = "2026-08-20T19:07:17.94Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/83/1c36270658d2ee56e23a3f9ef5fbcb94cbd2f9fe966a6641f2f38e697162/pyzmq-27.2.0-cp315-cp315-android_24_x86_64.whl", hash = "sha256:baa2ce3485145653194d6c8c5beedd1e9f0bf46a0919c9fa2fe2204fc35b74d9", size = 1167492, upload-time = "2026-08-20T19:07:19.476Z" },
+ { url = "https://files.pythonhosted.org/packages/58/b2/f0ae223438d7faa991f6feefdc823815f11cc604f898738376b59fd96515/pyzmq-27.2.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:e1ed46048d1920cabc96d952a0d5cfe4127ad8db572c335aae4e3c57b9278d7f", size = 1465992, upload-time = "2026-08-20T19:07:20.941Z" },
+ { url = "https://files.pythonhosted.org/packages/59/46/fb56f3f37a6a0937b0e1d2885e808b5eedc171320bac85573cfae78fa9bc/pyzmq-27.2.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e0fa0bc6b1a184aee59b32efcd1b7f0e6d5b8f9387799e4c16a4cb66a86747d6", size = 976118, upload-time = "2026-08-20T19:07:22.577Z" },
+ { url = "https://files.pythonhosted.org/packages/21/82/a2c9bfd7c4d34eea1278493cd041bc000d41acb4463c89ceaad29dc813b6/pyzmq-27.2.0-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4bd6743e8bf854c3bfce892dd6578a514aabf128e37a4b2eafcf01856f7e44", size = 705968, upload-time = "2026-08-20T19:07:24.019Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/12/b906b269116b6591dc15c0acc5d04c043957c8a531d336999731f4b1d899/pyzmq-27.2.0-cp315-cp315t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95369ed6626afcfe2ac89832fb1b917c077fbeb905fbbe5d918349ce0222b89b", size = 879011, upload-time = "2026-08-20T19:07:25.428Z" },
+ { url = "https://files.pythonhosted.org/packages/12/13/f96359534bfb77651c15f1fbfc4bfdd7ec3489d23f434706d39598dd0dcd/pyzmq-27.2.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:40124779c3a56ad5d91902df1ff89159cb414b6c1a0ee697abcc66cf5e6db62d", size = 1697496, upload-time = "2026-08-20T19:07:26.821Z" },
+ { url = "https://files.pythonhosted.org/packages/21/b4/2c007ae5f2fe5eca86cbfbc874ed86b5135f2f7812615dfd78606d3c93f6/pyzmq-27.2.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:ec8a318dfc27c7d946651b3d9e8025d5734f30c168a822195601827207bac09b", size = 2064347, upload-time = "2026-08-20T19:07:28.315Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/88/767af3a6630c15215f3a66700ec79598a375edd1fdc9d75a3ad522178c01/pyzmq-27.2.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:88c0fac061bac269076edeb3a209acefc96cd6167c239daf1c2b404ac48d7012", size = 1917360, upload-time = "2026-08-20T19:07:29.693Z" },
+ { url = "https://files.pythonhosted.org/packages/35/c1/80dd2d20d6e57bc68e1dce1e84bf3e76c9577c1bf728199985c8b4ea0fd1/pyzmq-27.2.0-cp315-cp315t-win32.whl", hash = "sha256:ac126d48cf18aa955daabef43bf0009ff76ad4deee437d09ecf15388214b5beb", size = 591073, upload-time = "2026-08-20T19:07:31.341Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/b5/33b781666f3f52ae834bc9c8e38f4f0483a826c5a91cccc993292007bf10/pyzmq-27.2.0-cp315-cp315t-win_amd64.whl", hash = "sha256:edce90a1e588ec63adbf612cc0ad582de4169cd216c7ae53c15f42a2ee902f35", size = 670701, upload-time = "2026-08-20T19:07:32.895Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/97/bc4f0edefb992df4fdebcf9f0cc40f631cd4ed277e1ed59ef2cd99a5c8c5/pyzmq-27.2.0-cp315-cp315t-win_arm64.whl", hash = "sha256:a843094b4d3d633bc3623e47a2ff50742d6af02bc1f7606aa2e67e971e21878d", size = 581985, upload-time = "2026-08-20T19:07:34.19Z" },
]
[[package]]
@@ -2579,6 +2586,11 @@ dependencies = [
{ name = "smoothstate" },
]
+[package.optional-dependencies]
+matplotlib = [
+ { name = "matplotlib" },
+]
+
[package.dev-dependencies]
dev = [
{ name = "dcurves" },
@@ -2589,7 +2601,6 @@ dev = [
{ name = "pre-commit" },
{ name = "pytest" },
{ name = "pytest-cov" },
- { name = "pyzmq" },
{ name = "ruff" },
{ name = "scikit-learn" },
{ name = "ty" },
@@ -2602,12 +2613,14 @@ docs = [
[package.metadata]
requires-dist = [
{ name = "great-tables", specifier = ">=0.18.0" },
+ { name = "matplotlib", marker = "extra == 'matplotlib'", specifier = ">=3.9.0" },
{ name = "plotly", specifier = ">=6.0.0,<7.0.0" },
{ name = "polars", specifier = ">=1.31.0" },
{ name = "polarstate", specifier = "==0.1.8" },
{ name = "reactable", specifier = ">=0.1.5" },
{ name = "smoothstate", specifier = ">=0.1.1" },
]
+provides-extras = ["matplotlib"]
[package.metadata.requires-dev]
dev = [
@@ -2619,7 +2632,6 @@ dev = [
{ name = "pre-commit", specifier = ">=4.2.0" },
{ name = "pytest", specifier = ">=7.3.0,<8.0.0" },
{ name = "pytest-cov", specifier = ">=4.0.0,<5.0.0" },
- { name = "pyzmq", specifier = ">=26.3.0,<27.0.0" },
{ name = "ruff", specifier = ">=0.11.0" },
{ name = "scikit-learn", specifier = ">=1.6.1" },
{ name = "ty", specifier = ">=0.0.1a12" },