Skip to content

Commit

Permalink
Merge pull request #69 from hanjinliu/regplot
Browse files Browse the repository at this point in the history
Add regplot
  • Loading branch information
hanjinliu authored Oct 29, 2024
2 parents 2eb4e76 + d03ed13 commit cd7bcaf
Show file tree
Hide file tree
Showing 34 changed files with 425 additions and 54 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
platform: [ubuntu-latest, macos-latest, windows-latest]
exclude:
- python-version: "3.13"
platform: macos-latest

steps:
- name: Cancel Previous Runs
Expand Down
13 changes: 13 additions & 0 deletions docs/categorical/num_num.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,19 @@ canvas.cat(df, "x", "y").add_markers(color="label")
canvas.show()
```

Group-wise line regression can be easily added by `with_reg` method.

``` python hl_lines="5"
#!name: categorical_add_markers_symbol
canvas = new_canvas("matplotlib")
(
canvas.cat(df, "x", "y")
.add_markers(color="label")
.with_reg()
)
canvas.show()
```

## Automatic Creation of Legends

As mentioned in [Legend for the Layers](../canvas/legend.md), legends can be
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
Expand Down Expand Up @@ -69,6 +70,7 @@ testing = [
"bokeh>=3.3.1",
"pandas>=1.3.3",
"polars>=0.20.10",
"statsmodels>=0.13.0",
]

docs = [
Expand All @@ -82,6 +84,7 @@ docs = [
"matplotlib>=3.8.2",
"imageio>=2.9.0",
"plotly>=5.3.1",
"statsmodels>=0.13.0",
]

[project.urls]
Expand Down
6 changes: 4 additions & 2 deletions tests/test_categorical.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ def test_cat(backend: str):
cplt.add_line().update_style("--").copy()
cplt.add_line(color="label").update_style("label").with_markers().copy()
cplt.add_markers().copy()
cplt.add_markers(color="label").copy()
cplt.add_markers(hatch="label").copy()
cplt.add_markers(color="label").with_reg().copy()
cplt.add_markers(hatch="label").with_reg(color="black").copy()
cplt.add_regplot(ci=0.9, color="label").copy()
cplt.add_regplot(ci=0.0, style="label").copy()
cplt.add_pointplot(color="label").copy()
cplt.add_hist2d(bins=5).copy()
cplt.add_hist2d(bins=(5, 4)).copy()
Expand Down
6 changes: 6 additions & 0 deletions tests/test_layers.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ def test_markers(backend: str):
assert layer.symbol == layer_copy.symbol
layer.read_json(layer.write_json(), backend=backend)

def test_regression(backend: str):
rng = np.random.default_rng(14453)
canvas = new_canvas(backend=backend)
layer = canvas.add_markers(rng.random(10), rng.random(10), color="red").with_reg()
assert_color_equal(layer[1].line.color, "red")

def test_bars(backend: str):
canvas = new_canvas(backend=backend)
canvas.add_bars(np.arange(10), np.zeros(10), bottom=np.ones(10))
Expand Down
5 changes: 4 additions & 1 deletion tests/test_logics.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from pathlib import Path
import tempfile
import pytest
import matplotlib.pyplot as plt
from whitecanvas import new_canvas
import numpy as np
from numpy.testing import assert_allclose
Expand Down Expand Up @@ -149,13 +150,13 @@ def test_matplotlib_tooltip():
canvas.add_markers([1, 2, 3], [4, 5, 6])
canvas._canvas()._set_tooltip((2, 5), "tooltip")
canvas._canvas()._hide_tooltip()
plt.close("all")

def test_load_dataset():
from whitecanvas import load_dataset
import pandas as pd
import polars as pl

df = load_dataset("iris")
df = load_dataset("iris", type="pandas")
assert isinstance(df, pd.DataFrame)
df = load_dataset("iris", type="polars")
Expand All @@ -172,6 +173,7 @@ def test_add_operation():
l_mb = l + (m + b)
lm._repr_png_()
lmb._repr_png_()
plt.close("all")

def test_read_write_json_file():
from whitecanvas.layers import Markers
Expand All @@ -182,3 +184,4 @@ def test_read_write_json_file():
markers = Markers.read_json(tmpdir / "layer.json")
assert_allclose(markers.data.x, [1, 2, 3])
assert_allclose(markers.data.y, [4, 5, 6])
plt.close("all")
53 changes: 27 additions & 26 deletions tests/test_plt.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,33 @@
import numpy as np

import whitecanvas.plot as plt

from whitecanvas.backend import use_backend

def test_functions():
arr = np.arange(10)
plt.figure(size=(400, 300))
plt.line(arr)
plt.markers(arr)
plt.bars(arr, arr / 2)
plt.band(arr, arr / 2, arr * 2)
plt.errorbars(arr, arr / 2, arr * 2)
plt.hist(arr)
plt.spans([[4, 10], [5, 14]])
plt.infcurve(lambda x: x ** 2)
plt.infline((0, 0), 40)
plt.legend()
plt.xlim()
plt.xlim(0, 10)
plt.ylim()
plt.ylim(0, 10)
plt.title("Title")
plt.xlabel("X")
plt.ylabel("Y")
plt.xticks()
plt.xticks([0, 3, 6, 9])
plt.xticks([0, 3, 6, 9], ["a", "b", "c", "d"])
plt.yticks()
plt.yticks([0, 3, 6, 9])
plt.yticks([0, 3, 6, 9], ["a", "b", "c", "d"])
plt.show()
with use_backend("mock"):
plt.figure(size=(400, 300))
plt.line(arr)
plt.markers(arr)
plt.bars(arr, arr / 2)
plt.band(arr, arr / 2, arr * 2)
plt.errorbars(arr, arr / 2, arr * 2)
plt.hist(arr)
plt.spans([[4, 10], [5, 14]])
plt.infcurve(lambda x: x ** 2)
plt.infline((0, 0), 40)
plt.legend()
plt.xlim()
plt.xlim(0, 10)
plt.ylim()
plt.ylim(0, 10)
plt.title("Title")
plt.xlabel("X")
plt.ylabel("Y")
plt.xticks()
plt.xticks([0, 3, 6, 9])
plt.xticks([0, 3, 6, 9], ["a", "b", "c", "d"])
plt.yticks()
plt.yticks([0, 3, 6, 9])
plt.yticks([0, 3, 6, 9], ["a", "b", "c", "d"])
plt.show()
4 changes: 2 additions & 2 deletions whitecanvas/backend/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from whitecanvas.backend._instance import Backend, patch_dummy_backend
from whitecanvas.backend._instance import Backend, patch_dummy_backend, use_backend

__all__ = ["Backend", "patch_dummy_backend"]
__all__ = ["Backend", "patch_dummy_backend", "use_backend"]
11 changes: 11 additions & 0 deletions whitecanvas/backend/_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,14 @@ def patch_dummy_backend():
yield dummy_name
finally:
del _INSTALLED_MODULES[dummy_name]


@contextmanager
def use_backend(name: str):
"""Context manager to temporarily change the backend."""
old = Backend._default
try:
Backend(name)
yield
finally:
Backend._default = old
2 changes: 1 addition & 1 deletion whitecanvas/backend/vispy/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class Image(visuals.Image):
def __init__(self, data: np.ndarray):
self._cmap_obj = Colormap("gray")
# GPU does not support f64
if data.dtype == np.float64:
if data.dtype in (np.float64, np.int32, np.int64, np.uint32, np.uint64):
data = data.astype(np.float32)
super().__init__(data, cmap="gray")
tr = STTransform()
Expand Down
17 changes: 17 additions & 0 deletions whitecanvas/canvas/dataframe/_feature_cat.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,23 @@ def add_pointplot(
) # fmt: skip
return canvas.add_layer(layer)

def add_regplot(
self,
*,
name: str | None = None,
ci: float = 0.95,
color: NStr | None = None,
width: float | None = None,
style: NStr | None = None,
):
canvas = self._canvas()
layer = _lt.DFRegPlot.from_table(
self._df, self._get_x(), self._get_y(), name=name, color=color, width=width,
style=style, ci=ci, palette=canvas._color_palette,
backend=canvas._get_backend(),
) # fmt: skip
return canvas.add_layer(layer)

def add_hist(
self,
*,
Expand Down
4 changes: 1 addition & 3 deletions whitecanvas/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,8 +284,6 @@ def read_col(path: str | Path) -> CanvasVGrid:
return CanvasVGrid.read_json(path)


@overload
def load_dataset(name: str, type: None = None, cache: bool = True) -> Any: ...
@overload
def load_dataset(
name: str, type: Literal["pandas"], cache: bool = True
Expand All @@ -296,7 +294,7 @@ def load_dataset(
) -> pl.DataFrame: ...


def load_dataset(name, type=None, cache=True) -> Any:
def load_dataset(name, type="pandas", cache=True):
from urllib.request import urlopen, urlretrieve

from platformdirs import user_cache_dir
Expand Down
5 changes: 2 additions & 3 deletions whitecanvas/layers/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,12 +183,11 @@ def _repr_any_(self, method: str, *args: Any, **kwargs: Any) -> Any:
if backend.name == "matplotlib":
import matplotlib.pyplot as plt

_old_mpl_backend = plt.get_backend()
plt.switch_backend("Agg")
ca = plt.gca()
try:
canvas = new_canvas(backend=backend, size=(360, 240))
finally:
plt.switch_backend(_old_mpl_backend)
plt.sca(ca)
else:
canvas = new_canvas(backend=backend, size=(360, 240))
canvas.add_layer(self.copy())
Expand Down
5 changes: 4 additions & 1 deletion whitecanvas/layers/_deserialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ def construct_layer(d: dict[str, Any], backend: Backend | str | None = None) ->
typ = d["type"]
if not isinstance(typ, str):
raise ValueError(f"Layer type must be a string, got {typ!r}.")
return _pick_layer_class(typ).from_dict(d, backend=backend)
layer = _pick_layer_class(typ).from_dict(d, backend=backend)
if not (visible := d.get("visible", True)):
layer.visible = visible
return layer


def construct_layers(
Expand Down
12 changes: 9 additions & 3 deletions whitecanvas/layers/_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,10 @@ def alpha(self, value: float):
class ConstFace(SinglePropertyFaceBase):
@property
def color(self) -> NDArray[np.floating]:
return self._layer._backend._plt_get_face_color()[0]
colors = self._layer._backend._plt_get_face_color()
if len(colors) > 0:
return colors[0]
return np.zeros(4, dtype=np.float32)

@color.setter
def color(self, value: ColorType):
Expand All @@ -327,7 +330,10 @@ def color(self, value: ColorType):

@property
def hatch(self) -> Hatch:
return self._layer._backend._plt_get_face_hatch()[0]
hatches = self._layer._backend._plt_get_face_hatch()
if len(hatches) > 0:
return hatches[0]
return Hatch.SOLID

@hatch.setter
def hatch(self, value: str | Hatch):
Expand Down Expand Up @@ -408,7 +414,7 @@ def color(self) -> NDArray[np.floating]:
colors = self._layer._backend._plt_get_edge_color()
if len(colors) > 0:
return colors[0]
return np.array([0, 0, 0, 0], dtype=np.float32)
return np.zeros(4, dtype=np.float32)

@color.setter
def color(self, value: ColorType):
Expand Down
1 change: 1 addition & 0 deletions whitecanvas/layers/_primitive/band.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ def to_dict(self) -> dict[str, Any]:
"data": self._get_layer_data().to_dict(),
"orient": self.orient.value,
"name": self.name,
"visible": self.visible,
"face": self.face.to_dict(),
"edge": self.edge.to_dict(),
}
Expand Down
1 change: 1 addition & 0 deletions whitecanvas/layers/_primitive/bars.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ def to_dict(self) -> dict[str, Any]:
"orient": self.orient.value,
"extent": self.bar_width,
"name": self.name,
"visible": self.visible,
"face": self.face.to_dict(),
"edge": self.edge.to_dict(),
}
Expand Down
1 change: 1 addition & 0 deletions whitecanvas/layers/_primitive/errorbars.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ def to_dict(self) -> dict[str, Any]:
"data": self._get_layer_data().to_dict(),
"orient": self.orient.value,
"name": self.name,
"visible": self.visible,
"color": self.color,
"width": self.width,
"style": self.style,
Expand Down
1 change: 1 addition & 0 deletions whitecanvas/layers/_primitive/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ def to_dict(self) -> dict[str, Any]:
"type": f"{self.__module__}.{self.__class__.__name__}",
"data": self._get_layer_data(),
"name": self.name,
"visible": self.visible,
"cmap": self.cmap,
"clim": self.clim,
"shift": self.shift,
Expand Down
2 changes: 2 additions & 0 deletions whitecanvas/layers/_primitive/inf_curve.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ def to_dict(self) -> dict[str, Any]:
"bounds": self._bounds,
"params": self.params,
"name": self.name,
"visible": self.visible,
"color": self.color,
"width": self.width,
"style": self.style,
Expand Down Expand Up @@ -255,6 +256,7 @@ def to_dict(self) -> dict[str, Any]:
"pos": self.pos,
"angle": self.angle,
"name": self.name,
"visible": self.visible,
"color": self.color,
"width": self.width,
"style": self.style,
Expand Down
3 changes: 3 additions & 0 deletions whitecanvas/layers/_primitive/line.py
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,7 @@ def to_dict(self) -> dict[str, Any]:
"type": f"{self.__module__}.{self.__class__.__name__}",
"data": self._get_layer_data().to_dict(),
"name": self.name,
"visible": self.visible,
"color": self.color,
"width": self.width,
"style": self.style,
Expand Down Expand Up @@ -810,6 +811,7 @@ def to_dict(self) -> dict[str, Any]:
"type": f"{self.__module__}.{self.__class__.__name__}",
"data": self._get_layer_data().to_dict(),
"name": self.name,
"visible": self.visible,
"where": self.where,
"color": self.color,
"width": self.width,
Expand Down Expand Up @@ -990,6 +992,7 @@ def to_dict(self) -> dict[str, Any]:
"type": f"{self.__module__}.{self.__class__.__name__}",
"data": self._get_layer_data(),
"name": self.name,
"visible": self.visible,
"color": self.color,
"width": self.width,
"style": self.style,
Expand Down
Loading

0 comments on commit cd7bcaf

Please sign in to comment.