bezel-charts 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.
@@ -0,0 +1,7 @@
1
+ node_modules/
2
+ .DS_Store
3
+ __pycache__/
4
+ *.egg-info/
5
+ .venv/
6
+ .pytest_cache/
7
+ python/dist/
@@ -0,0 +1,10 @@
1
+ # Changelog (Python package)
2
+
3
+ ## 0.1.0 (unreleased)
4
+
5
+ First release of the Python companion to Bezel Charts.
6
+
7
+ - `bc.spark()` and `bc.line()` take a pandas DataFrame or Series, a NumPy array, a dict or a plain list, and return a `Chart` that shows up in Jupyter, saves as an `.svg` file and converts to an SVG string.
8
+ - `bc.to_svg()` and `bc.render()` mirror the JavaScript functions of the same names and take the same options, in snake_case, kebab-case or camelCase.
9
+ - The output matches the web components byte for byte, checked against fixtures generated from the JavaScript on every pull request.
10
+ - Zero dependencies. Python 3.10 or newer.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shaz Moghaddam, https://shazmoghaddam.github.io, https://linkedin.com/in/shazmoghaddam, London.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,160 @@
1
+ Metadata-Version: 2.5
2
+ Name: bezel-charts
3
+ Version: 0.1.0
4
+ Summary: Polished SVG charts from pandas DataFrames and Python lists, drawn exactly like the Bezel Charts web components. Zero dependencies.
5
+ Project-URL: Homepage, https://shazmoghaddam.github.io/bezel-charts/site/
6
+ Project-URL: Documentation, https://github.com/ShazMoghaddam/bezel-charts/tree/main/python#readme
7
+ Project-URL: Repository, https://github.com/ShazMoghaddam/bezel-charts
8
+ Project-URL: Issues, https://github.com/ShazMoghaddam/bezel-charts/issues
9
+ Project-URL: Changelog, https://github.com/ShazMoghaddam/bezel-charts/blob/main/python/CHANGELOG.md
10
+ Author: Shaz Moghaddam
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: charts,data-visualization,jupyter,line-chart,pandas,readme,sparkline,svg
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Framework :: Jupyter
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Intended Audience :: Science/Research
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Programming Language :: Python :: 3.14
26
+ Classifier: Topic :: Scientific/Engineering :: Visualization
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.10
29
+ Provides-Extra: test
30
+ Requires-Dist: numpy>=1.26; extra == 'test'
31
+ Requires-Dist: pandas>=2; extra == 'test'
32
+ Requires-Dist: pytest>=8; extra == 'test'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # Bezel Charts for Python
36
+
37
+ **Polished SVG charts straight from a pandas DataFrame.**
38
+
39
+ The Python companion to [Bezel Charts](https://github.com/ShazMoghaddam/bezel-charts). Pass in a DataFrame, a Series, a NumPy array or a plain list and get back a finished chart tile that shows up in Jupyter, saves as an `.svg` for a README, and drops into reports and slides. Zero dependencies.
40
+
41
+ ![A line chart of revenue against costs from January to September, with a launch note in May](https://raw.githubusercontent.com/ShazMoghaddam/bezel-charts/main/python/docs/line.svg)
42
+
43
+ ![A dark sparkline tile showing monthly revenue of £79k, up £37k since the start](https://raw.githubusercontent.com/ShazMoghaddam/bezel-charts/main/python/docs/spark.svg)
44
+
45
+ - **Identical to the web components.** The renderers are a line-by-line port of the JavaScript ones, and the test suite checks they draw the same SVG, byte for byte.
46
+ - **pandas friendly.** Pick columns with `x=` and `y=`. Dates, indexes and column names become labels and legends.
47
+ - **Made for READMEs.** Every chart is a self-contained SVG with no scripts and no fonts to load, so GitHub shows it.
48
+ - **No dependencies.** pandas and NumPy work when you have them, and are never required.
49
+
50
+ ## Install
51
+
52
+ ```sh
53
+ pip install bezel-charts
54
+ ```
55
+
56
+ Python 3.10 or newer.
57
+
58
+ ## Quick start
59
+
60
+ ```python
61
+ import pandas as pd
62
+ import bezel_charts as bc
63
+
64
+ df = pd.DataFrame({
65
+ "month": pd.date_range("2026-01-01", periods=9, freq="MS"),
66
+ "Revenue": [42, 48, 45, 53, 61, 58, 66, 72, 79],
67
+ "Costs": [30, 31, 35, 34, 36, 41, 40, 43, 44],
68
+ })
69
+
70
+ bc.line(df, x="month", x_format="%b", heading="Revenue vs costs",
71
+ prefix="£", suffix="k", notes={"May": "Launch"}, theme="light")
72
+ ```
73
+
74
+ In a notebook the chart appears as the cell's output. Anywhere else:
75
+
76
+ ```python
77
+ chart = bc.spark(df["Revenue"], heading="Monthly revenue", prefix="£", suffix="k")
78
+ chart.save("docs/revenue.svg") # write an .svg file
79
+ svg = str(chart) # or use the SVG string directly
80
+ ```
81
+
82
+ Then in your README:
83
+
84
+ ```md
85
+ ![Monthly revenue](docs/revenue.svg)
86
+ ```
87
+
88
+ ## Charts in this release
89
+
90
+ | Function | What it draws |
91
+ | --- | --- |
92
+ | `bc.spark(data)` | A sparkline with the latest value and the change since the start. |
93
+ | `bc.line(data)` | A line chart with one or more series, a value axis, labels and a legend. |
94
+
95
+ The other eight (bars and grouped bars, scatter with a trend line, rings, donut, gauge, progress and heatmap) are coming in the next releases. `bc.types` lists what's available.
96
+
97
+ ## Your data
98
+
99
+ | You pass | You get |
100
+ | --- | --- |
101
+ | A list, tuple or NumPy array | One series. |
102
+ | A pandas Series | One series, with the index as labels unless it's the default 0, 1, 2. |
103
+ | A DataFrame | `y="col"` or `y=["a", "b"]` picks the series (by default every numeric column), `x="col"` the labels (by default the index). |
104
+ | A dict like `{"Team A": [...], "Team B": [...]}` | One series per key, named in the legend. |
105
+ | A list of lists | One series per inner list. |
106
+
107
+ Dates become `2026-03-01`, or anything you like with `x_format="%b %Y"`. Missing values are drawn as 0, like the browser version, with a warning so they never go unnoticed. Call `.dropna()` or `.fillna()` first if you'd rather handle them yourself.
108
+
109
+ ## Options
110
+
111
+ Every option from the [web components](https://github.com/ShazMoghaddam/bezel-charts#options-for-every-chart) works as a keyword argument. Use snake_case: `show-values` becomes `show_values=True`.
112
+
113
+ | Option | Example | What it does |
114
+ | --- | --- | --- |
115
+ | `heading` | `heading="Sales"` | Small title at the top. |
116
+ | `prefix`, `suffix` | `prefix="£"`, `suffix=" kWh"` | Around every number. |
117
+ | `decimals` | `decimals=1` | Fixed decimal places. |
118
+ | `compact` | `compact=True` | 15,500 becomes 15.5k. |
119
+ | `stat` | `stat="average"` | Headline number: `last`, `total`, `average`, `max`, `min` or `none`. |
120
+ | `delta`, `delta_label` | `delta="inverse"` | Change since the start. `inverse` makes a fall the good direction. |
121
+ | `goal`, `goal_label` | `goal=50, goal_label="Target"` | A dashed target line. |
122
+ | `notes` | `notes={"May": "Launch"}` | Notes on points, keyed by label or position. |
123
+ | `show_values` | `show_values=True` | A label on every point. |
124
+ | `straight` | `straight=True` | Straight segments instead of curves. |
125
+ | `zero` | `zero=True` | Start the axis at zero (line). |
126
+ | `labels`, `series` | `labels=["Q1", "Q2"]` | Set labels and series names by hand. |
127
+ | `color` | `color=["#ff6600", "#0066ff"]` | Series colours. |
128
+ | `theme` | `theme="light"` | `dark` (the default) or `light`. |
129
+ | `width`, `height` | `width=560` | Size in pixels. The tile grows to fit its heading and headline. |
130
+ | `frame`, `corners`, `background` | `frame="none"` | Drop the bezel, change the corner radius or the tile colour. |
131
+
132
+ Numbers are formatted for `en-GB` by default (`1.5m`, `2bn`), or `lang="en-US"` for `1.5M`, `2B`. Other locales fall back to `en-GB` for now, with a warning.
133
+
134
+ ## Lower level
135
+
136
+ `bc.to_svg(type, **options)` returns the static SVG string and `bc.render(type, **options)` returns it with the hover targets the browser uses. Both take the same options as the JavaScript functions of the same names, and accept a dict too:
137
+
138
+ ```python
139
+ bc.to_svg("line", {"values": "1,2,3|3,2,1", "series": "A,B", "show-values": ""})
140
+ ```
141
+
142
+ ## How it stays in step with the JavaScript
143
+
144
+ `scripts/py-fixtures.mjs` in the main repository runs the real JavaScript charts over a wide set of inputs (empty data, single values, negatives, huge and tiny numbers, notes, goals, both themes) and saves the SVG they draw. The Python tests render the same inputs and check the output is identical. CI regenerates the fixtures on every pull request, so a change to one version that isn't made to the other fails the build.
145
+
146
+ ## Development
147
+
148
+ ```sh
149
+ git clone https://github.com/ShazMoghaddam/bezel-charts.git
150
+ cd bezel-charts/python
151
+ python3 -m venv .venv && source .venv/bin/activate
152
+ pip install -e ".[test]"
153
+ pytest
154
+ ```
155
+
156
+ After changing a JavaScript chart, run `npm run fixtures` from the repository root and commit `python/tests/fixtures`.
157
+
158
+ ## Licence
159
+
160
+ [MIT](https://github.com/ShazMoghaddam/bezel-charts/blob/main/LICENSE). Designed and built by [Shaz Moghaddam](https://shazmoghaddam.github.io), London.
@@ -0,0 +1,126 @@
1
+ # Bezel Charts for Python
2
+
3
+ **Polished SVG charts straight from a pandas DataFrame.**
4
+
5
+ The Python companion to [Bezel Charts](https://github.com/ShazMoghaddam/bezel-charts). Pass in a DataFrame, a Series, a NumPy array or a plain list and get back a finished chart tile that shows up in Jupyter, saves as an `.svg` for a README, and drops into reports and slides. Zero dependencies.
6
+
7
+ ![A line chart of revenue against costs from January to September, with a launch note in May](https://raw.githubusercontent.com/ShazMoghaddam/bezel-charts/main/python/docs/line.svg)
8
+
9
+ ![A dark sparkline tile showing monthly revenue of £79k, up £37k since the start](https://raw.githubusercontent.com/ShazMoghaddam/bezel-charts/main/python/docs/spark.svg)
10
+
11
+ - **Identical to the web components.** The renderers are a line-by-line port of the JavaScript ones, and the test suite checks they draw the same SVG, byte for byte.
12
+ - **pandas friendly.** Pick columns with `x=` and `y=`. Dates, indexes and column names become labels and legends.
13
+ - **Made for READMEs.** Every chart is a self-contained SVG with no scripts and no fonts to load, so GitHub shows it.
14
+ - **No dependencies.** pandas and NumPy work when you have them, and are never required.
15
+
16
+ ## Install
17
+
18
+ ```sh
19
+ pip install bezel-charts
20
+ ```
21
+
22
+ Python 3.10 or newer.
23
+
24
+ ## Quick start
25
+
26
+ ```python
27
+ import pandas as pd
28
+ import bezel_charts as bc
29
+
30
+ df = pd.DataFrame({
31
+ "month": pd.date_range("2026-01-01", periods=9, freq="MS"),
32
+ "Revenue": [42, 48, 45, 53, 61, 58, 66, 72, 79],
33
+ "Costs": [30, 31, 35, 34, 36, 41, 40, 43, 44],
34
+ })
35
+
36
+ bc.line(df, x="month", x_format="%b", heading="Revenue vs costs",
37
+ prefix="£", suffix="k", notes={"May": "Launch"}, theme="light")
38
+ ```
39
+
40
+ In a notebook the chart appears as the cell's output. Anywhere else:
41
+
42
+ ```python
43
+ chart = bc.spark(df["Revenue"], heading="Monthly revenue", prefix="£", suffix="k")
44
+ chart.save("docs/revenue.svg") # write an .svg file
45
+ svg = str(chart) # or use the SVG string directly
46
+ ```
47
+
48
+ Then in your README:
49
+
50
+ ```md
51
+ ![Monthly revenue](docs/revenue.svg)
52
+ ```
53
+
54
+ ## Charts in this release
55
+
56
+ | Function | What it draws |
57
+ | --- | --- |
58
+ | `bc.spark(data)` | A sparkline with the latest value and the change since the start. |
59
+ | `bc.line(data)` | A line chart with one or more series, a value axis, labels and a legend. |
60
+
61
+ The other eight (bars and grouped bars, scatter with a trend line, rings, donut, gauge, progress and heatmap) are coming in the next releases. `bc.types` lists what's available.
62
+
63
+ ## Your data
64
+
65
+ | You pass | You get |
66
+ | --- | --- |
67
+ | A list, tuple or NumPy array | One series. |
68
+ | A pandas Series | One series, with the index as labels unless it's the default 0, 1, 2. |
69
+ | A DataFrame | `y="col"` or `y=["a", "b"]` picks the series (by default every numeric column), `x="col"` the labels (by default the index). |
70
+ | A dict like `{"Team A": [...], "Team B": [...]}` | One series per key, named in the legend. |
71
+ | A list of lists | One series per inner list. |
72
+
73
+ Dates become `2026-03-01`, or anything you like with `x_format="%b %Y"`. Missing values are drawn as 0, like the browser version, with a warning so they never go unnoticed. Call `.dropna()` or `.fillna()` first if you'd rather handle them yourself.
74
+
75
+ ## Options
76
+
77
+ Every option from the [web components](https://github.com/ShazMoghaddam/bezel-charts#options-for-every-chart) works as a keyword argument. Use snake_case: `show-values` becomes `show_values=True`.
78
+
79
+ | Option | Example | What it does |
80
+ | --- | --- | --- |
81
+ | `heading` | `heading="Sales"` | Small title at the top. |
82
+ | `prefix`, `suffix` | `prefix="£"`, `suffix=" kWh"` | Around every number. |
83
+ | `decimals` | `decimals=1` | Fixed decimal places. |
84
+ | `compact` | `compact=True` | 15,500 becomes 15.5k. |
85
+ | `stat` | `stat="average"` | Headline number: `last`, `total`, `average`, `max`, `min` or `none`. |
86
+ | `delta`, `delta_label` | `delta="inverse"` | Change since the start. `inverse` makes a fall the good direction. |
87
+ | `goal`, `goal_label` | `goal=50, goal_label="Target"` | A dashed target line. |
88
+ | `notes` | `notes={"May": "Launch"}` | Notes on points, keyed by label or position. |
89
+ | `show_values` | `show_values=True` | A label on every point. |
90
+ | `straight` | `straight=True` | Straight segments instead of curves. |
91
+ | `zero` | `zero=True` | Start the axis at zero (line). |
92
+ | `labels`, `series` | `labels=["Q1", "Q2"]` | Set labels and series names by hand. |
93
+ | `color` | `color=["#ff6600", "#0066ff"]` | Series colours. |
94
+ | `theme` | `theme="light"` | `dark` (the default) or `light`. |
95
+ | `width`, `height` | `width=560` | Size in pixels. The tile grows to fit its heading and headline. |
96
+ | `frame`, `corners`, `background` | `frame="none"` | Drop the bezel, change the corner radius or the tile colour. |
97
+
98
+ Numbers are formatted for `en-GB` by default (`1.5m`, `2bn`), or `lang="en-US"` for `1.5M`, `2B`. Other locales fall back to `en-GB` for now, with a warning.
99
+
100
+ ## Lower level
101
+
102
+ `bc.to_svg(type, **options)` returns the static SVG string and `bc.render(type, **options)` returns it with the hover targets the browser uses. Both take the same options as the JavaScript functions of the same names, and accept a dict too:
103
+
104
+ ```python
105
+ bc.to_svg("line", {"values": "1,2,3|3,2,1", "series": "A,B", "show-values": ""})
106
+ ```
107
+
108
+ ## How it stays in step with the JavaScript
109
+
110
+ `scripts/py-fixtures.mjs` in the main repository runs the real JavaScript charts over a wide set of inputs (empty data, single values, negatives, huge and tiny numbers, notes, goals, both themes) and saves the SVG they draw. The Python tests render the same inputs and check the output is identical. CI regenerates the fixtures on every pull request, so a change to one version that isn't made to the other fails the build.
111
+
112
+ ## Development
113
+
114
+ ```sh
115
+ git clone https://github.com/ShazMoghaddam/bezel-charts.git
116
+ cd bezel-charts/python
117
+ python3 -m venv .venv && source .venv/bin/activate
118
+ pip install -e ".[test]"
119
+ pytest
120
+ ```
121
+
122
+ After changing a JavaScript chart, run `npm run fixtures` from the repository root and commit `python/tests/fixtures`.
123
+
124
+ ## Licence
125
+
126
+ [MIT](https://github.com/ShazMoghaddam/bezel-charts/blob/main/LICENSE). Designed and built by [Shaz Moghaddam](https://shazmoghaddam.github.io), London.
@@ -0,0 +1,54 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "bezel-charts"
7
+ dynamic = ["version"]
8
+ description = "Polished SVG charts from pandas DataFrames and Python lists, drawn exactly like the Bezel Charts web components. Zero dependencies."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.10"
13
+ authors = [{ name = "Shaz Moghaddam" }]
14
+ keywords = ["charts", "svg", "sparkline", "pandas", "jupyter", "data-visualization", "readme", "line-chart"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Framework :: Jupyter",
18
+ "Intended Audience :: Developers",
19
+ "Intended Audience :: Science/Research",
20
+ "Operating System :: OS Independent",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3 :: Only",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Programming Language :: Python :: 3.14",
28
+ "Topic :: Scientific/Engineering :: Visualization",
29
+ "Typing :: Typed",
30
+ ]
31
+ dependencies = []
32
+
33
+ [project.optional-dependencies]
34
+ test = ["pytest>=8", "pandas>=2", "numpy>=1.26"]
35
+
36
+ [project.urls]
37
+ Homepage = "https://shazmoghaddam.github.io/bezel-charts/site/"
38
+ Documentation = "https://github.com/ShazMoghaddam/bezel-charts/tree/main/python#readme"
39
+ Repository = "https://github.com/ShazMoghaddam/bezel-charts"
40
+ Issues = "https://github.com/ShazMoghaddam/bezel-charts/issues"
41
+ Changelog = "https://github.com/ShazMoghaddam/bezel-charts/blob/main/python/CHANGELOG.md"
42
+
43
+ [tool.hatch.version]
44
+ path = "src/bezel_charts/__init__.py"
45
+
46
+ [tool.hatch.build.targets.sdist]
47
+ include = ["src", "tests", "README.md", "CHANGELOG.md", "LICENSE"]
48
+
49
+ [tool.hatch.build.targets.wheel]
50
+ packages = ["src/bezel_charts"]
51
+
52
+ [tool.pytest.ini_options]
53
+ testpaths = ["tests"]
54
+ addopts = "-q"
@@ -0,0 +1,120 @@
1
+ """Bezel Charts for Python.
2
+
3
+ Polished SVG charts from pandas DataFrames, NumPy arrays and plain lists, drawn
4
+ exactly like the Bezel Charts web components. Zero dependencies.
5
+
6
+ import bezel_charts as bc
7
+
8
+ bc.line(df, x="month", y=["sales", "costs"], heading="Sales", prefix="£")
9
+ bc.spark([4, 6, 5, 8, 9], heading="Signups").save("signups.svg")
10
+
11
+ Created by Shaz Moghaddam, London. https://shazmoghaddam.github.io
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import re
17
+ from os import PathLike
18
+ from pathlib import Path
19
+ from typing import Any
20
+
21
+ from . import _data
22
+ from ._render import ALL_TYPES
23
+ from ._render import render as _render
24
+ from ._render import to_svg as _to_svg
25
+ from ._tile import THEMES as themes
26
+ from .charts import TYPES
27
+
28
+ __version__ = "0.1.0"
29
+ __all__ = ["Chart", "line", "render", "spark", "themes", "to_svg", "types", "__version__"]
30
+
31
+ #: Chart types available in this release. The rest of the ten follow in later releases.
32
+ types = list(TYPES)
33
+
34
+
35
+ def render(type: str, options: dict | None = None, **kwargs: Any) -> str:
36
+ """Renders a chart to an SVG string with hover targets, the same as render() in JavaScript.
37
+
38
+ Options use the HTML attribute names in snake_case, kebab-case or camelCase:
39
+ ``show_values``, ``show-values`` and ``showValues`` all work.
40
+ """
41
+ return _render(type, {**(options or {}), **kwargs})
42
+
43
+
44
+ def to_svg(type: str, options: dict | None = None, **kwargs: Any) -> str:
45
+ """Renders a clean static SVG, the same as toSVG() in JavaScript."""
46
+ return _to_svg(type, {**(options or {}), **kwargs})
47
+
48
+
49
+ class Chart:
50
+ """A rendered chart. Shows itself in Jupyter, and converts to an SVG string with str()."""
51
+
52
+ def __init__(self, type: str, options: dict):
53
+ self.type = type
54
+ self.options = dict(options)
55
+ self.svg = _to_svg(type, self.options)
56
+ m = re.search(r'width="(\d+)" height="(\d+)"', self.svg)
57
+ self.width, self.height = (int(m.group(1)), int(m.group(2))) if m else (0, 0)
58
+
59
+ def __str__(self) -> str:
60
+ return self.svg
61
+
62
+ def __repr__(self) -> str:
63
+ return f"<bezel_charts.Chart {self.type} {self.width}x{self.height}>"
64
+
65
+ def _repr_svg_(self) -> str:
66
+ return self.svg
67
+
68
+ def save(self, path: str | PathLike) -> Path:
69
+ """Writes the chart as an .svg file and returns its path."""
70
+ p = Path(path)
71
+ if p.suffix.lower() != ".svg":
72
+ p = p.with_suffix(".svg") if p.suffix else p.with_name(p.name + ".svg")
73
+ p.write_text(self.svg, encoding="utf-8")
74
+ return p
75
+
76
+
77
+ def _options(options: dict, values, names, lbls) -> dict:
78
+ out = dict(options)
79
+ if values is not None:
80
+ out["values"] = values
81
+ if names and "series" not in out:
82
+ out["series"] = names
83
+ if lbls and "labels" not in out:
84
+ out["labels"] = lbls
85
+ if "notes" in out:
86
+ out["notes"] = _data.notes(out["notes"])
87
+ return out
88
+
89
+
90
+ def spark(data: Any = None, *, x: Any = None, y: Any = None, x_format: str | None = None, **options: Any) -> Chart:
91
+ """A sparkline: one series, its latest value and the change since the start.
92
+
93
+ ``data`` can be a list, a NumPy array, a pandas Series, or a DataFrame with ``y=`` naming the column.
94
+ Labels come from ``x=`` (a column), a non-default index, or ``labels=``. They show up in notes.
95
+ """
96
+ if data is None:
97
+ return Chart("spark", _options(options, None, None, None))
98
+ values, _, lbls = _data.prepare(data, x, y, x_format, single=True)
99
+ return Chart("spark", _options(options, values[0], None, lbls))
100
+
101
+
102
+ def line(data: Any = None, *, x: Any = None, y: Any = None, x_format: str | None = None, **options: Any) -> Chart:
103
+ """A line chart with one or more series, a value axis and labels along the bottom.
104
+
105
+ ``data`` can be a DataFrame (``x=`` for labels, ``y=`` for one column or a list of them; by default
106
+ every numeric column), a Series, a dict of name to values, a list of lists, or a single list.
107
+ Dates are written as YYYY-MM-DD unless you pass ``x_format`` (for example ``"%b %Y"``).
108
+ """
109
+ if data is None:
110
+ return Chart("line", _options(options, None, None, None))
111
+ values, names, lbls = _data.prepare(data, x, y, x_format)
112
+ return Chart("line", _options(options, values, names, lbls))
113
+
114
+
115
+ def __getattr__(name: str):
116
+ if name in ALL_TYPES:
117
+ raise AttributeError(
118
+ f"bezel_charts.{name}() is coming in a later release. Available now: {', '.join(types)}."
119
+ )
120
+ raise AttributeError(f"module 'bezel_charts' has no attribute {name!r}")
@@ -0,0 +1,163 @@
1
+ """Turns pandas, NumPy and plain Python data into the options the renderers read.
2
+
3
+ pandas and NumPy are never imported. Objects are recognised by what they can do
4
+ (a .columns attribute, a .tolist() method), so the package stays dependency free.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import math
10
+ import warnings
11
+ from collections.abc import Mapping
12
+
13
+ from ._js import is_num, js_str, to_string
14
+
15
+
16
+ def _is_frame(obj) -> bool:
17
+ return hasattr(obj, "columns") and hasattr(obj, "dtypes") and hasattr(obj, "__getitem__")
18
+
19
+
20
+ def _is_series(obj) -> bool:
21
+ return hasattr(obj, "index") and hasattr(obj, "tolist") and not _is_frame(obj) and hasattr(obj, "name")
22
+
23
+
24
+ def _to_list(obj) -> list:
25
+ if hasattr(obj, "tolist") and not isinstance(obj, str):
26
+ out = obj.tolist()
27
+ return out if isinstance(out, list) else [out]
28
+ if isinstance(obj, (str, bytes)):
29
+ raise TypeError("bezel_charts: pass numbers as a list, Series or array, not a string.")
30
+ return list(obj)
31
+
32
+
33
+ def _is_default_index(index) -> bool:
34
+ return type(index).__name__ == "RangeIndex"
35
+
36
+
37
+ def _number(v) -> float:
38
+ if hasattr(v, "item") and not isinstance(v, (list, tuple)):
39
+ try:
40
+ v = v.item()
41
+ except (TypeError, ValueError):
42
+ pass
43
+ if v is None or isinstance(v, bool):
44
+ return float("nan") if v is None else float(v)
45
+ try:
46
+ return float(v)
47
+ except (TypeError, ValueError):
48
+ return float("nan")
49
+
50
+
51
+ def numbers(seq, what: str = "values") -> list:
52
+ """Floats, with missing values drawn as 0 like the browser version, plus a warning."""
53
+ out, missing = [], 0
54
+ for v in _to_list(seq):
55
+ n = _number(v)
56
+ if not math.isfinite(n):
57
+ missing += 1
58
+ n = 0.0
59
+ out.append(n)
60
+ if missing:
61
+ warnings.warn(
62
+ f"bezel_charts: {missing} missing or non-numeric {what} drawn as 0. "
63
+ "Use .dropna() or .fillna() first if that is not what you want.",
64
+ stacklevel=4,
65
+ )
66
+ return out
67
+
68
+
69
+ def label(v, x_format: str | None = None) -> str:
70
+ """A tidy text label: dates without midnight times, 2026.0 as 2026, missing as blank."""
71
+ if hasattr(v, "item") and not hasattr(v, "strftime"):
72
+ try:
73
+ v = v.item()
74
+ except (TypeError, ValueError):
75
+ pass
76
+ if v is None or (isinstance(v, float) and v != v):
77
+ return ""
78
+ if hasattr(v, "strftime"):
79
+ if type(v).__name__ == "NaTType":
80
+ return ""
81
+ if x_format:
82
+ return v.strftime(x_format)
83
+ if any(getattr(v, part, 0) for part in ("hour", "minute", "second")):
84
+ return v.strftime("%Y-%m-%d %H:%M")
85
+ return v.strftime("%Y-%m-%d") if hasattr(v, "day") else str(v)
86
+ if is_num(v):
87
+ return js_str(v)
88
+ return to_string(v)
89
+
90
+
91
+ def labels(seq, x_format: str | None = None) -> list:
92
+ return [label(v, x_format) for v in _to_list(seq)]
93
+
94
+
95
+ def notes(value):
96
+ """Accepts {"Mar": "Launch", 4: "Peak"} or [(key, text), ...] as well as the "key:text;..." string."""
97
+ if value is None or isinstance(value, str):
98
+ return value
99
+ items = value.items() if isinstance(value, Mapping) else value
100
+ parts = []
101
+ for key, text in items:
102
+ key_s, text_s = label(key), to_string(text)
103
+ if ":" in key_s or ";" in key_s or ";" in text_s:
104
+ raise ValueError(
105
+ f"bezel_charts: note {key_s!r}: {text_s!r} cannot contain ';', and its key cannot contain ':'."
106
+ )
107
+ parts.append(f"{key_s}:{text_s}")
108
+ return ";".join(parts)
109
+
110
+
111
+ def _numeric_columns(df, exclude) -> list:
112
+ cols = []
113
+ for col, dtype in zip(df.columns, df.dtypes):
114
+ if col != exclude and getattr(dtype, "kind", "") in "iuf":
115
+ cols.append(col)
116
+ return cols
117
+
118
+
119
+ def prepare(data, x=None, y=None, x_format=None, single=False):
120
+ """Returns (series values, series names or None, labels or None) for any supported input."""
121
+ if _is_frame(data):
122
+ if x is not None and x not in data.columns:
123
+ raise KeyError(f"bezel_charts: column {x!r} not found. Columns: {list(data.columns)}")
124
+ if y is None:
125
+ ys = _numeric_columns(data, x)
126
+ if not ys:
127
+ raise ValueError("bezel_charts: no numeric columns to plot. Pick one with y='column'.")
128
+ else:
129
+ ys = [y] if isinstance(y, (str, int)) or not hasattr(y, "__iter__") else list(y)
130
+ for col in ys:
131
+ if col not in data.columns:
132
+ raise KeyError(f"bezel_charts: column {col!r} not found. Columns: {list(data.columns)}")
133
+ if single and len(ys) > 1:
134
+ raise ValueError(f"bezel_charts: a sparkline draws one column. Choose one with y=, from {ys}.")
135
+ values = [numbers(data[col], f"values in {col!r}") for col in ys]
136
+ if x is not None:
137
+ lbls = labels(data[x], x_format)
138
+ elif not _is_default_index(data.index):
139
+ lbls = labels(data.index, x_format)
140
+ else:
141
+ lbls = None
142
+ names = [label(c) for c in ys] if len(ys) > 1 else None
143
+ return values, names, lbls
144
+
145
+ if x is not None or y is not None:
146
+ raise TypeError("bezel_charts: x= and y= pick DataFrame columns. For lists, pass labels= instead.")
147
+
148
+ if _is_series(data):
149
+ lbls = None if _is_default_index(data.index) else labels(data.index, x_format)
150
+ return [numbers(data)], None, lbls
151
+
152
+ if isinstance(data, Mapping):
153
+ if single and len(data) > 1:
154
+ raise ValueError("bezel_charts: a sparkline draws one series. Pass a single list.")
155
+ names = [label(k) for k in data]
156
+ return [numbers(v, f"values in {k!r}") for k, v in data.items()], names, None
157
+
158
+ rows = _to_list(data)
159
+ if rows and all(hasattr(r, "__iter__") and not isinstance(r, str) for r in rows):
160
+ if single and len(rows) > 1:
161
+ raise ValueError("bezel_charts: a sparkline draws one series. Pass a single list.")
162
+ return [numbers(r) for r in rows], None, None
163
+ return [numbers(rows)], None, None