lazy-visual 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. lazy_visual-0.1.0/.github/workflows/baseline.yml +29 -0
  2. lazy_visual-0.1.0/.github/workflows/ci.yml +51 -0
  3. lazy_visual-0.1.0/.github/workflows/docs.yml +23 -0
  4. lazy_visual-0.1.0/.gitignore +15 -0
  5. lazy_visual-0.1.0/CHANGELOG.md +13 -0
  6. lazy_visual-0.1.0/CONTRIBUTING.md +8 -0
  7. lazy_visual-0.1.0/LICENSE +9 -0
  8. lazy_visual-0.1.0/PKG-INFO +29 -0
  9. lazy_visual-0.1.0/README.md +3 -0
  10. lazy_visual-0.1.0/docs/api.md +3 -0
  11. lazy_visual-0.1.0/docs/changelog.md +13 -0
  12. lazy_visual-0.1.0/docs/errors.md +28 -0
  13. lazy_visual-0.1.0/docs/gallery.md +14 -0
  14. lazy_visual-0.1.0/docs/index.md +15 -0
  15. lazy_visual-0.1.0/docs/install.md +7 -0
  16. lazy_visual-0.1.0/docs/quickstart.md +17 -0
  17. lazy_visual-0.1.0/docs/themes.md +11 -0
  18. lazy_visual-0.1.0/lazy_visual/__init__.py +82 -0
  19. lazy_visual-0.1.0/lazy_visual/_errors.py +161 -0
  20. lazy_visual-0.1.0/lazy_visual/_options.py +33 -0
  21. lazy_visual-0.1.0/lazy_visual/core/__init__.py +0 -0
  22. lazy_visual-0.1.0/lazy_visual/core/result.py +51 -0
  23. lazy_visual-0.1.0/lazy_visual/core/spec.py +40 -0
  24. lazy_visual-0.1.0/lazy_visual/core/style.py +177 -0
  25. lazy_visual-0.1.0/lazy_visual/defaults.py +14 -0
  26. lazy_visual-0.1.0/lazy_visual/export.py +52 -0
  27. lazy_visual-0.1.0/lazy_visual/io/__init__.py +0 -0
  28. lazy_visual-0.1.0/lazy_visual/io/loader.py +80 -0
  29. lazy_visual-0.1.0/lazy_visual/io/normalize.py +79 -0
  30. lazy_visual-0.1.0/lazy_visual/plots/__init__.py +0 -0
  31. lazy_visual-0.1.0/lazy_visual/plots/_common.py +63 -0
  32. lazy_visual-0.1.0/lazy_visual/plots/box.py +161 -0
  33. lazy_visual-0.1.0/lazy_visual/plots/distribution.py +62 -0
  34. lazy_visual-0.1.0/lazy_visual/plots/heatmap.py +96 -0
  35. lazy_visual-0.1.0/lazy_visual/plots/raincloud.py +153 -0
  36. lazy_visual-0.1.0/lazy_visual/plots/roc.py +79 -0
  37. lazy_visual-0.1.0/lazy_visual/plots/scatter.py +88 -0
  38. lazy_visual-0.1.0/lazy_visual/plots/upset.py +138 -0
  39. lazy_visual-0.1.0/lazy_visual/plots/violin.py +6 -0
  40. lazy_visual-0.1.0/lazy_visual/py.typed +0 -0
  41. lazy_visual-0.1.0/lazy_visual/render/__init__.py +0 -0
  42. lazy_visual-0.1.0/lazy_visual/render/base.py +7 -0
  43. lazy_visual-0.1.0/lazy_visual/render/mpl.py +13 -0
  44. lazy_visual-0.1.0/lazy_visual/stats/__init__.py +0 -0
  45. lazy_visual-0.1.0/lazy_visual/stats/compare.py +159 -0
  46. lazy_visual-0.1.0/lazy_visual/stats/density.py +25 -0
  47. lazy_visual-0.1.0/lazy_visual/stats/roc.py +113 -0
  48. lazy_visual-0.1.0/lazy_visual/stats/sets.py +49 -0
  49. lazy_visual-0.1.0/lazy_visual/themes/__init__.py +0 -0
  50. lazy_visual-0.1.0/lazy_visual/themes/apa.json +5 -0
  51. lazy_visual-0.1.0/lazy_visual/themes/dark.json +4 -0
  52. lazy_visual-0.1.0/lazy_visual/themes/default.json +4 -0
  53. lazy_visual-0.1.0/lazy_visual/themes/nature.json +5 -0
  54. lazy_visual-0.1.0/mkdocs.yml +29 -0
  55. lazy_visual-0.1.0/mypy.ini +16 -0
  56. lazy_visual-0.1.0/pyproject.toml +35 -0
  57. lazy_visual-0.1.0/requirements-dev.txt +4 -0
  58. lazy_visual-0.1.0/tests/__init__.py +0 -0
  59. lazy_visual-0.1.0/tests/baseline/test_golden_box.png +0 -0
  60. lazy_visual-0.1.0/tests/baseline/test_golden_distribution.png +0 -0
  61. lazy_visual-0.1.0/tests/baseline/test_golden_heatmap.png +0 -0
  62. lazy_visual-0.1.0/tests/baseline/test_golden_raincloud.png +0 -0
  63. lazy_visual-0.1.0/tests/baseline/test_golden_roc.png +0 -0
  64. lazy_visual-0.1.0/tests/baseline/test_golden_scatter.png +0 -0
  65. lazy_visual-0.1.0/tests/baseline/test_golden_upset.png +0 -0
  66. lazy_visual-0.1.0/tests/baseline/test_golden_violin.png +0 -0
  67. lazy_visual-0.1.0/tests/conftest.py +25 -0
  68. lazy_visual-0.1.0/tests/test_benchmarks.py +43 -0
  69. lazy_visual-0.1.0/tests/test_errors.py +42 -0
  70. lazy_visual-0.1.0/tests/test_golden.py +108 -0
  71. lazy_visual-0.1.0/tests/test_plots.py +37 -0
  72. lazy_visual-0.1.0/tests/test_smoke.py +42 -0
  73. lazy_visual-0.1.0/tests/test_stats.py +21 -0
@@ -0,0 +1,29 @@
1
+ name: Generate golden baselines
2
+ on:
3
+ workflow_dispatch:
4
+ permissions:
5
+ contents: write
6
+ jobs:
7
+ gen:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/checkout@v4
11
+ with:
12
+ token: ${{ secrets.GITHUB_TOKEN }}
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: '3.12'
16
+ - name: Install
17
+ run: |
18
+ python -m pip install --upgrade pip
19
+ python -m pip install numpy scipy pandas matplotlib pytest pytest-mpl .
20
+ - name: Generate baselines
21
+ run: python -m pytest tests/test_golden.py --mpl --mpl-generate-path=tests/baseline -q
22
+ - name: Commit and push
23
+ run: |
24
+ git config user.name 'github-actions[bot]'
25
+ git config user.email 'github-actions[bot]@users.noreply.github.com'
26
+ git add tests/baseline/
27
+ git diff --cached --quiet && echo 'no changes' || \
28
+ git commit -m 'ci: regenerate golden baselines (Linux + latest mpl)'
29
+ git push
@@ -0,0 +1,51 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ${{ matrix.os }}
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ os: [ubuntu-latest, macos-latest, windows-latest]
16
+ python: ["3.10", "3.12", "3.13"]
17
+ deps: ["min", "last"]
18
+ exclude:
19
+ # 3.12 and 3.13 have no wheels for numpy 1.24 / pandas 1.5 / mpl 3.7
20
+ - python: "3.12"
21
+ deps: "min"
22
+ - python: "3.13"
23
+ deps: "min"
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+ - uses: actions/setup-python@v5
27
+ with:
28
+ python-version: ${{ matrix.python }}
29
+ - name: Install min deps
30
+ if: matrix.deps == 'min'
31
+ shell: bash
32
+ run: |
33
+ python -m pip install --upgrade pip
34
+ python -m pip install "numpy==1.24.*" "scipy==1.10.*" "pandas==1.5.*" "matplotlib==3.7.*" pytest
35
+ - name: Install latest deps
36
+ if: matrix.deps == 'last'
37
+ shell: bash
38
+ run: |
39
+ python -m pip install --upgrade pip
40
+ python -m pip install numpy scipy pandas matplotlib pytest pytest-mpl
41
+ - name: Install package
42
+ shell: bash
43
+ run: python -m pip install -e .
44
+ - name: Run tests (golden on ubuntu + py3.12 + latest deps)
45
+ if: matrix.os == 'ubuntu-latest' && matrix.python == '3.12' && matrix.deps == 'last'
46
+ shell: bash
47
+ run: python -m pytest -q --mpl --mpl-baseline-path=tests/baseline
48
+ - name: Run tests (skip golden)
49
+ if: matrix.os != 'ubuntu-latest' || matrix.python != '3.12' || matrix.deps != 'last'
50
+ shell: bash
51
+ run: python -m pytest -q --deselect tests/test_golden.py
@@ -0,0 +1,23 @@
1
+ name: Docs
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ paths: ["docs/**", "mkdocs.yml", "lazy_visual/**", ".github/workflows/docs.yml"]
6
+ workflow_dispatch:
7
+ permissions:
8
+ contents: write
9
+ jobs:
10
+ deploy:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: actions/setup-python@v5
15
+ with: { python-version: "3.12" }
16
+ - run: |
17
+ python -m pip install --upgrade pip
18
+ python -m pip install mkdocs-material 'mkdocstrings[python]' .
19
+ - run: mkdocs build --strict
20
+ - uses: peaceiris/actions-gh-pages@v3
21
+ with:
22
+ github_token: ${{ secrets.GITHUB_TOKEN }}
23
+ publish_dir: ./site
@@ -0,0 +1,15 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .ruff_cache/
7
+ build/
8
+ dist/
9
+ .venv/
10
+ venv/
11
+ *.png
12
+ *.csv
13
+ .DS_Store
14
+
15
+ !tests/baseline/*.png
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ First public release.
6
+
7
+ ### Added
8
+
9
+ - `scatterplot`, `distribution`, `box`, `violin`, `raincloud`, `heatmap`, `upset`, `roc`
10
+ - `lv.load` (csv/tsv/parquet/excel/json + URL)
11
+ - 4 themes: default, nature, apa, dark
12
+ - Built-in stats: Mann-Whitney U, Welch t-test, Kruskal-Wallis, Holm correction, Cohen's d, KDE, DeLong & bootstrap AUC CI
13
+ - Error codes LV-E101..E310, LV-W501..W504
@@ -0,0 +1,8 @@
1
+ # Contributing
2
+
3
+ 1. `pip install -e .[dev]`
4
+ 2. `pytest -q`
5
+ 3. `ruff check lazy_visual/`
6
+ 4. `mypy --config-file mypy.ini lazy_visual/`
7
+
8
+ Keep `lazy_visual/plots/` and `lazy_visual/stats/` free of matplotlib imports; `stats/` must not know about plots.
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 lazy-visual contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,29 @@
1
+ Metadata-Version: 2.5
2
+ Name: lazy-visual
3
+ Version: 0.1.0
4
+ Summary: One-line statistical plots with built-in stats.
5
+ Author: lazy-visual contributors
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: matplotlib>=3.7
10
+ Requires-Dist: numpy>=1.24
11
+ Requires-Dist: pandas>=1.5
12
+ Requires-Dist: scipy>=1.10
13
+ Provides-Extra: dev
14
+ Requires-Dist: mypy; extra == 'dev'
15
+ Requires-Dist: pytest; extra == 'dev'
16
+ Requires-Dist: pytest-benchmark; extra == 'dev'
17
+ Requires-Dist: pytest-mpl; extra == 'dev'
18
+ Requires-Dist: pytest-socket; extra == 'dev'
19
+ Requires-Dist: ruff; extra == 'dev'
20
+ Requires-Dist: scikit-learn; extra == 'dev'
21
+ Provides-Extra: excel
22
+ Requires-Dist: openpyxl>=3.1; extra == 'excel'
23
+ Provides-Extra: parquet
24
+ Requires-Dist: pyarrow>=10; extra == 'parquet'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # lazy-visual
28
+
29
+ One-line statistical plots.
@@ -0,0 +1,3 @@
1
+ # lazy-visual
2
+
3
+ One-line statistical plots.
@@ -0,0 +1,3 @@
1
+ # API reference
2
+
3
+ ::: lazy_visual
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ First public release.
6
+
7
+ ### Added
8
+
9
+ - `scatterplot`, `distribution`, `box`, `violin`, `raincloud`, `heatmap`, `upset`, `roc`
10
+ - `lv.load` (csv/tsv/parquet/excel/json + URL)
11
+ - 4 themes: default, nature, apa, dark
12
+ - Built-in stats: Mann-Whitney U, Welch t-test, Kruskal-Wallis, Holm correction, Cohen's d, KDE, DeLong & bootstrap AUC CI
13
+ - Error codes LV-E101..E310, LV-W501..W504
@@ -0,0 +1,28 @@
1
+ # Error codes
2
+
3
+ Format: `[LV-E101] message. Hint: ...`
4
+
5
+ | Code | Class | Situation |
6
+ |---|---|---|
7
+ | LV-E101 | LvDataError | column not found |
8
+ | LV-E102 | LvDataError | wrong dtype |
9
+ | LV-E103 | LvDataError | empty after NaN drop |
10
+ | LV-E104 | LvDataError | duplicate columns |
11
+ | LV-E106 | LvDataError | y_true not binary |
12
+ | LV-E107 | LvDataError | upset sets 2-20 |
13
+ | LV-E108 | LvDataError | file not found |
14
+ | LV-E109 | LvDataError | unsupported format |
15
+ | LV-E110 | LvDataError | URL unreachable |
16
+ | LV-E201 | LvArgumentError | out of range |
17
+ | LV-E202 | LvArgumentError | wrong type |
18
+ | LV-E203 | LvArgumentError | unknown theme |
19
+ | LV-E204 | LvArgumentError | incompatible args |
20
+ | LV-E205 | LvDataError | >30 groups |
21
+ | LV-E301 | LvExportError | unsupported export |
22
+ | LV-E302 | LvExportError | no write permission |
23
+ | LV-E310 | LvArgumentError | backend unavailable |
24
+ | LV-E401 | LvDependencyError | missing optional dep |
25
+ | LV-W501 | LvDataWarning | NaN rows dropped |
26
+ | LV-W502 | LvDataWarning | subsampled points |
27
+ | LV-W503 | LvPerformanceWarning | large data |
28
+ | LV-W504 | LvDataWarning | stats skipped |
@@ -0,0 +1,14 @@
1
+ # Gallery
2
+
3
+ Same df as Quickstart.
4
+
5
+ ```python
6
+ lv.scatterplot(df, x="x", y="y", hue="group", fit="linear", save="scatter.png")
7
+ lv.distribution(df, x="score", hue="group", kind="both", save="dist.png")
8
+ lv.box(df, y="score", x="group", pairs=[("a","b"),("a","c")], save="box.png")
9
+ lv.violin(df, y="score", x="group", save="violin.png")
10
+ lv.raincloud(df, y="score", x="group", save="raincloud.png")
11
+ lv.heatmap(df, corr="pearson", save="heatmap.png")
12
+ lv.upset({"A":{1,2,3,4,5},"B":{3,4,5,6},"C":{4,5,6,7}}, save="upset.png")
13
+ lv.roc(df, y_true="class", y_score=["s1","s2"], save="roc.png")
14
+ ```
@@ -0,0 +1,15 @@
1
+ # lazy-visual
2
+
3
+ One-line statistical plots with built-in stats.
4
+
5
+ ```python
6
+ import lazy_visual as lv
7
+ lv.raincloud(df, x="group", y="score", save="fig.svg")
8
+ ```
9
+
10
+ - 8 plots: scatterplot, distribution, box, violin, raincloud, heatmap, upset, roc
11
+ - Built-in stats: Mann-Whitney U, Welch, Kruskal-Wallis, Holm, Cohen's d, DeLong/bootstrap AUC CI
12
+ - 4 themes: default, nature, apa, dark
13
+ - Export PNG/SVG/PDF (atomic)
14
+
15
+ MIT. Deps: numpy, scipy, pandas, matplotlib.
@@ -0,0 +1,7 @@
1
+ # Install
2
+
3
+ ```bash
4
+ pip install lazy-visual
5
+ ```
6
+
7
+ Extras: `[parquet]`, `[excel]`. Python 3.10-3.13.
@@ -0,0 +1,17 @@
1
+ # Quickstart
2
+
3
+ ```python
4
+ import numpy as np, pandas as pd, lazy_visual as lv
5
+ rng = np.random.default_rng(0); n = 300
6
+ df = pd.DataFrame({
7
+ "group": rng.choice(["a","b","c"], n),
8
+ "score": rng.normal(0,1,n) + rng.choice([0,1,2], n),
9
+ "x": rng.normal(0,1,n), "y": rng.normal(0,1,n),
10
+ "class": rng.choice([0,1], n),
11
+ "s1": rng.uniform(0,1,n), "s2": rng.uniform(0,1,n),
12
+ })
13
+ r = lv.raincloud(df, x="group", y="score", save="fig.svg")
14
+ print(r)
15
+ lv.set_options(random_state=42)
16
+ lv.theme("nature")
17
+ ```
@@ -0,0 +1,11 @@
1
+ # Themes
2
+
3
+ `default`, `nature`, `apa`, `dark`.
4
+
5
+ ```python
6
+ lv.theme("nature")
7
+ lv.raincloud(df, x="group", y="score")
8
+ lv.raincloud(df, x="group", y="score", theme="dark")
9
+ lv.raincloud(df, x="group", y="score",
10
+ theme={"name":"nature","font_size":8,"palette":"okabe"})
11
+ ```
@@ -0,0 +1,82 @@
1
+ """lazy-visual: one-line statistical plots with built-in stats."""
2
+ from __future__ import annotations
3
+
4
+ from ._errors import (
5
+ LvArgumentError,
6
+ LvDataError,
7
+ LvDataWarning,
8
+ LvDependencyError,
9
+ LvError,
10
+ LvExportError,
11
+ LvPerformanceWarning,
12
+ )
13
+ from ._options import Options, get_options, set_options
14
+
15
+ __version__ = "0.1.0"
16
+
17
+ __all__ = [
18
+ "LvArgumentError",
19
+ "LvDataError",
20
+ "LvDataWarning",
21
+ "LvDependencyError",
22
+ "LvError",
23
+ "LvExportError",
24
+ "LvPerformanceWarning",
25
+ "LvResult",
26
+ "Options",
27
+ "Theme",
28
+ "__version__",
29
+ "box",
30
+ "distribution",
31
+ "get_options",
32
+ "heatmap",
33
+ "load",
34
+ "raincloud",
35
+ "roc",
36
+ "scatterplot",
37
+ "set_options",
38
+ "theme",
39
+ "upset",
40
+ "violin",
41
+ ]
42
+
43
+
44
+ def __getattr__(name: str):
45
+ """Lazy imports so `import lazy_visual` stays fast (NFR-1)."""
46
+ if name == "load":
47
+ from .io.loader import load
48
+ return load
49
+ if name == "theme":
50
+ from .core.style import theme
51
+ return theme
52
+ if name == "Theme":
53
+ from .core.style import Theme
54
+ return Theme
55
+ if name == "LvResult":
56
+ from .core.result import LvResult
57
+ return LvResult
58
+ if name == "scatterplot":
59
+ from .plots.scatter import scatterplot
60
+ return scatterplot
61
+ if name == "distribution":
62
+ from .plots.distribution import distribution
63
+ return distribution
64
+ if name == "box":
65
+ from .plots.box import box
66
+ return box
67
+ if name == "violin":
68
+ from .plots.violin import violin
69
+ return violin
70
+ if name == "raincloud":
71
+ from .plots.raincloud import raincloud
72
+ return raincloud
73
+ if name == "heatmap":
74
+ from .plots.heatmap import heatmap
75
+ return heatmap
76
+ if name == "upset":
77
+ from .plots.upset import upset
78
+ return upset
79
+ if name == "roc":
80
+ from .plots.roc import roc
81
+ return roc
82
+ raise AttributeError(f"module 'lazy_visual' has no attribute {name!r}")
@@ -0,0 +1,161 @@
1
+ from __future__ import annotations
2
+
3
+ import difflib
4
+ from collections.abc import Iterable
5
+
6
+
7
+ class LvError(Exception):
8
+ code: str = "LV-E000"
9
+
10
+ def __init__(self, message: str, *, code: str | None = None,
11
+ hint: str | None = None) -> None:
12
+ self.code = code or self.code
13
+ self.message = message
14
+ self.hint = hint
15
+ super().__init__(self._format())
16
+
17
+ def _format(self) -> str:
18
+ base = f"[{self.code}] {self.message}"
19
+ if self.hint:
20
+ base += f" Hint: {self.hint}"
21
+ return base
22
+
23
+ def __str__(self) -> str:
24
+ return self._format()
25
+
26
+
27
+ class LvDataError(LvError, ValueError):
28
+ code = "LV-E100"
29
+
30
+
31
+ class LvArgumentError(LvError, ValueError):
32
+ code = "LV-E200"
33
+
34
+
35
+ class LvExportError(LvError, OSError):
36
+ code = "LV-E300"
37
+
38
+
39
+ class LvDependencyError(LvError, ImportError):
40
+ code = "LV-E401"
41
+
42
+
43
+ class LvDataWarning(UserWarning):
44
+ pass
45
+
46
+
47
+ class LvPerformanceWarning(UserWarning):
48
+ pass
49
+
50
+
51
+ def _suggest(name: str, available: Iterable[str]) -> str | None:
52
+ m = difflib.get_close_matches(name, list(available), n=1, cutoff=0.6)
53
+ return m[0] if m else None
54
+
55
+
56
+ def err_column_not_found(name: str, available: list) -> LvDataError:
57
+ hint = "column names are case-sensitive"
58
+ s = _suggest(name, available)
59
+ if s:
60
+ hint = f"did you mean '{s}'? {hint}"
61
+ return LvDataError(
62
+ f"Column '{name}' not found. Available: {', '.join(available)}.",
63
+ code="LV-E101", hint=hint)
64
+
65
+
66
+ def err_wrong_dtype(name: str, expected: str, found: str) -> LvDataError:
67
+ return LvDataError(
68
+ f"Column '{name}' has invalid dtype '{found}', expected {expected}.",
69
+ code="LV-E102")
70
+
71
+
72
+ def err_empty_after_nan(n_before: int, n_dropped: int) -> LvDataError:
73
+ return LvDataError(
74
+ f"No rows left after dropping NaNs "
75
+ f"(had {n_before}, dropped {n_dropped}).", code="LV-E103")
76
+
77
+
78
+ def err_duplicate_columns(dupes: list) -> LvDataError:
79
+ return LvDataError(
80
+ f"Duplicate column names: {', '.join(dupes)}.",
81
+ code="LV-E104", hint="rename columns to be unique")
82
+
83
+
84
+ def err_y_true_not_binary(classes: list) -> LvDataError:
85
+ return LvDataError(
86
+ f"y_true must have exactly 2 classes, found {len(classes)}: "
87
+ f"{classes}.", code="LV-E106")
88
+
89
+
90
+ def err_upset_sets_count(n: int) -> LvDataError:
91
+ return LvDataError(f"upset requires 2-20 sets, got {n}.",
92
+ code="LV-E107")
93
+
94
+
95
+ def err_file_not_found(path: str) -> LvDataError:
96
+ return LvDataError(f"File not found or unreadable: {path}.",
97
+ code="LV-E108")
98
+
99
+
100
+ def err_unsupported_format(fmt: str) -> LvDataError:
101
+ return LvDataError(f"Unsupported file format: '{fmt}'.",
102
+ code="LV-E109")
103
+
104
+
105
+ def err_url_unreachable(url: str, reason: str) -> LvDataError:
106
+ return LvDataError(
107
+ f"URL unreachable or timed out: {url} ({reason}).",
108
+ code="LV-E110")
109
+
110
+
111
+ def err_out_of_range(name: str, value: object,
112
+ allowed: str) -> LvArgumentError:
113
+ return LvArgumentError(
114
+ f"Argument '{name}' = {value!r} is out of range. "
115
+ f"Allowed: {allowed}.", code="LV-E201")
116
+
117
+
118
+ def err_wrong_arg_type(name: str, expected: str,
119
+ got: str) -> LvArgumentError:
120
+ return LvArgumentError(
121
+ f"Argument '{name}' must be {expected}, got {got}.",
122
+ code="LV-E202")
123
+
124
+
125
+ def err_unknown_theme(name: str, available: list) -> LvArgumentError:
126
+ return LvArgumentError(
127
+ f"Unknown theme/palette '{name}'. "
128
+ f"Available: {', '.join(available)}.", code="LV-E203")
129
+
130
+
131
+ def err_incompatible_args(message: str) -> LvArgumentError:
132
+ return LvArgumentError(message, code="LV-E204")
133
+
134
+
135
+ def err_too_many_groups(n: int, limit: int) -> LvDataError:
136
+ return LvDataError(
137
+ f"Too many groups on axis ({n} > {limit}).",
138
+ code="LV-E205", hint="reduce the number of categories")
139
+
140
+
141
+ def err_export_format(fmt: str) -> LvExportError:
142
+ return LvExportError(
143
+ f"Unsupported export format '{fmt}'. Use png/svg/pdf.",
144
+ code="LV-E301")
145
+
146
+
147
+ def err_no_write_permission(path: str) -> LvExportError:
148
+ return LvExportError(f"Cannot write to {path}.", code="LV-E302")
149
+
150
+
151
+ def err_backend_unavailable(backend: str) -> LvArgumentError:
152
+ return LvArgumentError(
153
+ f"Backend '{backend}' is not available in this version.",
154
+ code="LV-E310", hint="only 'matplotlib' is supported in 0.1.0")
155
+
156
+
157
+ def err_missing_dependency(pkg: str, extra: str) -> LvDependencyError:
158
+ return LvDependencyError(
159
+ f"Optional dependency '{pkg}' is missing. "
160
+ f"Install with: pip install lazy-visual[{extra}]",
161
+ code="LV-E401")
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from . import defaults
6
+ from ._errors import err_out_of_range
7
+
8
+
9
+ @dataclass
10
+ class Options:
11
+ active_theme: str = "default"
12
+ random_state: int | None = defaults.RANDOM_STATE
13
+ max_points: int = defaults.MAX_POINTS
14
+ n_boot: int = defaults.N_BOOT
15
+
16
+ _OPTIONS = Options()
17
+
18
+ def get_options():
19
+ return _OPTIONS
20
+
21
+ def set_options(*, random_state=defaults.RANDOM_STATE,
22
+ max_points=defaults.MAX_POINTS,
23
+ n_boot=defaults.N_BOOT, active_theme=None):
24
+ if max_points < 1:
25
+ raise err_out_of_range("max_points", max_points, ">= 1")
26
+ if not (defaults.N_BOOT_MIN <= n_boot <= defaults.N_BOOT_MAX):
27
+ raise err_out_of_range("n_boot", n_boot,
28
+ f"{defaults.N_BOOT_MIN}-{defaults.N_BOOT_MAX}")
29
+ _OPTIONS.random_state = random_state
30
+ _OPTIONS.max_points = int(max_points)
31
+ _OPTIONS.n_boot = int(n_boot)
32
+ if active_theme is not None:
33
+ _OPTIONS.active_theme = active_theme
File without changes
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import pandas as pd
8
+
9
+ from .spec import PlotSpec
10
+
11
+
12
+ @dataclass
13
+ class LvResult:
14
+ fig: Any
15
+ ax: Any
16
+ spec: PlotSpec
17
+ stats: list[Any] = field(default_factory=list)
18
+ tables: dict[str, pd.DataFrame] = field(default_factory=dict)
19
+ files: list[Path] = field(default_factory=list)
20
+
21
+ def save(self, path: Any, *, fmt: Any = None,
22
+ dpi: Any = None) -> list[Path]:
23
+ from ..export import save_result
24
+ self.files = save_result(self.fig, path, fmt=fmt, dpi=dpi)
25
+ return self.files
26
+
27
+ def show(self) -> None:
28
+ import matplotlib.pyplot as plt
29
+ plt.show()
30
+
31
+ def to_dict(self) -> dict[str, Any]:
32
+ return {
33
+ "kind": self.spec.kind,
34
+ "stats": [
35
+ s.to_dict() if hasattr(s, "to_dict") else s.__dict__
36
+ for s in self.stats
37
+ ],
38
+ "files": [str(p) for p in self.files],
39
+ }
40
+
41
+ def __repr__(self) -> str:
42
+ head = f"<LvResult {self.spec.kind}"
43
+ if self.stats:
44
+ s = self.stats[0]
45
+ p = getattr(s, "p_value", None)
46
+ if p is not None:
47
+ ps = "p < 0.001" if p < 0.001 else f"p = {p:.3f}"
48
+ head += f", {getattr(s, 'method', '')} {ps}"
49
+ if self.files:
50
+ head += f", saved: {', '.join(p.name for p in self.files)}"
51
+ return head + ">"