From bab3ab0703f0bdd3f9b85d75e0b08c5818246064 Mon Sep 17 00:00:00 2001 From: ClaytonGillespie Date: Sun, 21 Dec 2025 19:10:42 +0000 Subject: [PATCH 01/12] Updated series.py for Polars Series/Dataframes. Pd unchanged (still uses index) --- .gitignore | 6 +++++- easychart/models/series.py | 29 +++++++++++++++++++++++++---- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 2044005..1905b5a 100644 --- a/.gitignore +++ b/.gitignore @@ -134,4 +134,8 @@ notes.txt # Ruff .ruff_cache -.ruff_cache/* \ No newline at end of file +.ruff_cache/* + +# Claude Code +CLAUDE.md +plan.md \ No newline at end of file diff --git a/easychart/models/series.py b/easychart/models/series.py index 442e293..89859f1 100644 --- a/easychart/models/series.py +++ b/easychart/models/series.py @@ -2,6 +2,9 @@ import pandas as pd import collections +import narwhals.stable.v2 as nw +from narwhals.stable.v2 import dependencies as nw_dep + import easychart.internals as internals @@ -82,11 +85,15 @@ def append(self, data=None, **kwargs): if isinstance(kwargs["color"], int): kwargs["colorIndex"] = kwargs.pop("color") - if isinstance(data, pd.Series): - if "name" not in kwargs: - kwargs["name"] = data.name + # Extract series name (works for pandas and polars) + if nw_dep.is_into_series(data): + s = nw.from_native(data, series_only=True, eager_only=True) + if "name" not in kwargs and s.name is not None: + kwargs["name"] = s.name + # Data conversion if isinstance(data, (pd.Series, pd.DataFrame)): + # pandas: maintain current behavior (uses index by default) if "index" in kwargs: if isinstance(kwargs["index"], collections.abc.Iterable): data = data.values.tolist() @@ -98,9 +105,23 @@ def append(self, data=None, **kwargs): else: data = data.reset_index().values.tolist() + elif nw_dep.is_into_series(data): + # Non-pandas Series (Polars, etc.): no index concept, just values + s = nw.from_native(data, series_only=True, eager_only=True) + data = s.to_list() + + elif nw_dep.is_into_dataframe(data): + # Non-pandas DataFrame (Polars, etc.): convert to list of rows + df = nw.from_native(data, eager_only=True) + native = nw.to_native(df) + if hasattr(native, "rows"): + data = list(native.rows()) + else: + data = native.to_numpy().tolist() + if "index" in kwargs and isinstance(kwargs["index"], collections.abc.Iterable): data = [ internals.flatten(i, v) for (i, v) in zip(kwargs.pop("index"), data) ] - return super().append(data=data, **kwargs) + return super().append(data=data, **kwargs) \ No newline at end of file From e93405d2cd318767cadb7991f44fccd0cb80f13b Mon Sep 17 00:00:00 2001 From: ClaytonGillespie Date: Sun, 21 Dec 2025 19:16:16 +0000 Subject: [PATCH 02/12] Added narwhals to pandas conversion shortcut for heatmap() --- easychart/__init__.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/easychart/__init__.py b/easychart/__init__.py index 7633670..fcf09a4 100644 --- a/easychart/__init__.py +++ b/easychart/__init__.py @@ -2,6 +2,9 @@ import inspect import re +import narwhals.stable.v2 as nw +from narwhals.stable.v2 import dependencies as nw_dep + from easychart.config import config from easychart.models import Chart, Plot, Grid @@ -371,6 +374,18 @@ def heatmap( ------- easychart.Chart """ + # Convert non-pandas DataFrames (Polars, etc.) to pandas + # Suggested approach for heatmap() is pragmatic - since it relies heavily + # on pandas operations (.stack(), .rename_axis(), index/column handling), + # here convert Polars DataFrames to pandas first + # Could review on more complete narwhals uplift + if not isinstance(data, pd.DataFrame) and nw_dep.is_into_dataframe(data): + native = nw.from_native(data, eager_only=True).to_native() + if hasattr(native, "to_pandas"): + data = native.to_pandas() + else: + data = pd.DataFrame(native.to_numpy()) + if isinstance(data, pd.DataFrame): if not ( isinstance(data.index, pd.DatetimeIndex) From e1d675e1c5610f082bd9494448a81dce45af675c Mon Sep 17 00:00:00 2001 From: ClaytonGillespie Date: Sun, 21 Dec 2025 19:17:54 +0000 Subject: [PATCH 03/12] Added narwhals as a dependency --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 85594dc..d2df9fb 100644 --- a/setup.py +++ b/setup.py @@ -26,6 +26,7 @@ "jinja2", "requests", "ipython", + "narwhals>=2.0.0", ], include_package_data=True, ) From 6c47af319339cb0e3714969045569370fa3c15b9 Mon Sep 17 00:00:00 2001 From: ClaytonGillespie Date: Sun, 21 Dec 2025 19:22:23 +0000 Subject: [PATCH 04/12] Add narwhals serialization support for JSON --- easychart/encoders.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/easychart/encoders.py b/easychart/encoders.py index 3834f17..c52920a 100644 --- a/easychart/encoders.py +++ b/easychart/encoders.py @@ -4,6 +4,9 @@ import pandas as pd import datetime +import narwhals.stable.v2 as nw +from narwhals.stable.v2 import dependencies as nw_dep + def default(value): """ @@ -60,6 +63,19 @@ def default(value): if isinstance(value, (pd.DataFrame, pd.Series, pd.Index)): return value.values.tolist() + # Handle non-pandas DataFrames/Series (Polars, etc.) + # Since we already convert in series.py, this is a safety net + if nw_dep.is_into_series(value): + s = nw.from_native(value, series_only=True, eager_only=True) + return s.to_list() + + if nw_dep.is_into_dataframe(value): + df = nw.from_native(value, eager_only=True) + native = nw.to_native(df) + if hasattr(native, "rows"): + return list(native.rows()) + return native.to_numpy().tolist() + if isinstance( value, ( From e4380b00fd6555dd985236ec8bc75a0b20c943ef Mon Sep 17 00:00:00 2001 From: ClaytonGillespie Date: Sun, 21 Dec 2025 19:33:57 +0000 Subject: [PATCH 05/12] Used improved API for non-polars DFs in nw --- easychart/encoders.py | 5 +---- easychart/models/series.py | 8 ++------ 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/easychart/encoders.py b/easychart/encoders.py index c52920a..af830af 100644 --- a/easychart/encoders.py +++ b/easychart/encoders.py @@ -71,10 +71,7 @@ def default(value): if nw_dep.is_into_dataframe(value): df = nw.from_native(value, eager_only=True) - native = nw.to_native(df) - if hasattr(native, "rows"): - return list(native.rows()) - return native.to_numpy().tolist() + return df.rows() if isinstance( value, diff --git a/easychart/models/series.py b/easychart/models/series.py index 89859f1..b8098f2 100644 --- a/easychart/models/series.py +++ b/easychart/models/series.py @@ -111,13 +111,9 @@ def append(self, data=None, **kwargs): data = s.to_list() elif nw_dep.is_into_dataframe(data): - # Non-pandas DataFrame (Polars, etc.): convert to list of rows + # Non-pandas DataFrame (Polars, PyArrow, etc.): convert to list of rows df = nw.from_native(data, eager_only=True) - native = nw.to_native(df) - if hasattr(native, "rows"): - data = list(native.rows()) - else: - data = native.to_numpy().tolist() + data = df.rows() if "index" in kwargs and isinstance(kwargs["index"], collections.abc.Iterable): data = [ From e80abf13e9a5035f323a1558de5c5d5b623cce3b Mon Sep 17 00:00:00 2001 From: ClaytonGillespie Date: Mon, 22 Dec 2025 09:03:15 +0000 Subject: [PATCH 06/12] Aligned null behaviour for no series name --- easychart/models/series.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/easychart/models/series.py b/easychart/models/series.py index b8098f2..272e603 100644 --- a/easychart/models/series.py +++ b/easychart/models/series.py @@ -88,8 +88,9 @@ def append(self, data=None, **kwargs): # Extract series name (works for pandas and polars) if nw_dep.is_into_series(data): s = nw.from_native(data, series_only=True, eager_only=True) - if "name" not in kwargs and s.name is not None: - kwargs["name"] = s.name + if "name" not in kwargs: + # Use None for empty names so Highcharts defaults to "Series 1" etc. + kwargs["name"] = s.name or None # Data conversion if isinstance(data, (pd.Series, pd.DataFrame)): @@ -120,4 +121,4 @@ def append(self, data=None, **kwargs): internals.flatten(i, v) for (i, v) in zip(kwargs.pop("index"), data) ] - return super().append(data=data, **kwargs) \ No newline at end of file + return super().append(data=data, **kwargs) From 15e7c3cad51b514a3f4f0555f6f4299e46eba61d Mon Sep 17 00:00:00 2001 From: ClaytonGillespie Date: Mon, 22 Dec 2025 09:04:11 +0000 Subject: [PATCH 07/12] Added AI generated narwhals tests --- tests/unit-tests/test_narwhals.py | 80 +++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 tests/unit-tests/test_narwhals.py diff --git a/tests/unit-tests/test_narwhals.py b/tests/unit-tests/test_narwhals.py new file mode 100644 index 0000000..06a5dec --- /dev/null +++ b/tests/unit-tests/test_narwhals.py @@ -0,0 +1,80 @@ +import pytest +import polars as pl +import pandas as pd + +import easychart +from easychart.encoders import default + + +class TestPolarsSupport: + """Tests for Polars DataFrame/Series support via narwhals""" + + def test_polars_series(self): + ps = pl.Series("test_series", [1, 2, 3]) + chart = easychart.new() + chart.plot(ps) + assert chart.series[0]["data"] == [1, 2, 3] + assert chart.series[0]["name"] == "test_series" + + def test_polars_series_unnamed(self): + ps = pl.Series([1, 2, 3]) + chart = easychart.new() + chart.plot(ps) + assert chart.series[0]["data"] == [1, 2, 3] + + def test_polars_dataframe(self): + df = pl.DataFrame({"a": [1, 2], "b": [3, 4]}) + chart = easychart.new() + chart.plot(df) + assert chart.series[0]["data"] == [(1, 3), (2, 4)] + + def test_polars_heatmap(self): + df = pl.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]}) + chart = easychart.heatmap(df) + assert chart.chart.type == "heatmap" + # Verify data was converted and plotted + assert len(chart.series[0]["data"]) == 9 # 3x3 grid + + +class TestPandasRegression: + """Ensure pandas behavior is unchanged""" + + def test_pandas_series_with_index(self): + ps = pd.Series([10, 20, 30], index=[0, 1, 2], name="pandas_test") + chart = easychart.new() + chart.plot(ps) + # Pandas includes index by default + assert chart.series[0]["data"] == [[0, 10], [1, 20], [2, 30]] + assert chart.series[0]["name"] == "pandas_test" + + def test_pandas_series_without_index(self): + ps = pd.Series([10, 20, 30], name="pandas_test") + chart = easychart.new() + chart.plot(ps, index=False) + assert chart.series[0]["data"] == [10, 20, 30] + + def test_pandas_dataframe(self): + df = pd.DataFrame({"a": [1, 2], "b": [3, 4]}) + chart = easychart.new() + chart.plot(df) + # Pandas includes index + assert chart.series[0]["data"] == [[0, 1, 3], [1, 2, 4]] + + +class TestEncoderSupport: + """Test JSON encoder handles Polars types""" + + def test_encoder_polars_series(self): + ps = pl.Series("test", [1, 2, 3]) + result = default(ps) + assert result == [1, 2, 3] + + def test_encoder_polars_dataframe(self): + df = pl.DataFrame({"a": [1, 2], "b": [3, 4]}) + result = default(df) + assert result == [(1, 3), (2, 4)] + + def test_encoder_pandas_still_works(self): + ps = pd.Series([1, 2, 3]) + result = default(ps) + assert result == [1, 2, 3] From e787dacf59dc16e056cbe969cc2da62c3a1ddb0d Mon Sep 17 00:00:00 2001 From: ClaytonGillespie Date: Mon, 22 Dec 2025 09:07:33 +0000 Subject: [PATCH 08/12] Linted with flake8 (removed unused pytest import) --- tests/unit-tests/test_narwhals.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit-tests/test_narwhals.py b/tests/unit-tests/test_narwhals.py index 06a5dec..6992ba0 100644 --- a/tests/unit-tests/test_narwhals.py +++ b/tests/unit-tests/test_narwhals.py @@ -1,4 +1,3 @@ -import pytest import polars as pl import pandas as pd From 62f0153305a262a4fc5a3071b001e2797da53c83 Mon Sep 17 00:00:00 2001 From: ClaytonGillespie Date: Mon, 22 Dec 2025 10:19:38 +0000 Subject: [PATCH 09/12] added heatmap test for pyarrow --- tests/unit-tests/test_narwhals.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/unit-tests/test_narwhals.py b/tests/unit-tests/test_narwhals.py index 6992ba0..79c7c48 100644 --- a/tests/unit-tests/test_narwhals.py +++ b/tests/unit-tests/test_narwhals.py @@ -1,5 +1,6 @@ import polars as pl import pandas as pd +import pyarrow as pa import easychart from easychart.encoders import default @@ -35,6 +36,22 @@ def test_polars_heatmap(self): assert len(chart.series[0]["data"]) == 9 # 3x3 grid +class TestPyArrowSupport: + """Tests for PyArrow Table support via narwhals""" + + def test_pyarrow_dataframe(self): + df = pa.table({"a": [1, 2], "b": [3, 4]}) + chart = easychart.new() + chart.plot(df) + assert chart.series[0]["data"] == [(1, 3), (2, 4)] + + def test_pyarrow_heatmap(self): + df = pa.table({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]}) + chart = easychart.heatmap(df) + assert chart.chart.type == "heatmap" + assert len(chart.series[0]["data"]) == 9 # 3x3 grid + + class TestPandasRegression: """Ensure pandas behavior is unchanged""" From ce5f5e22a4698da7b5949a6c91ef2c488a395c54 Mon Sep 17 00:00:00 2001 From: ClaytonGillespie Date: Mon, 22 Dec 2025 18:03:42 +0000 Subject: [PATCH 10/12] Added importorskip for pyarrow and polars dependency navigation --- tests/unit-tests/test_narwhals.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/unit-tests/test_narwhals.py b/tests/unit-tests/test_narwhals.py index 79c7c48..1f4a454 100644 --- a/tests/unit-tests/test_narwhals.py +++ b/tests/unit-tests/test_narwhals.py @@ -1,6 +1,8 @@ -import polars as pl +import pytest import pandas as pd -import pyarrow as pa + +pl = pytest.importorskip("polars") +pa = pytest.importorskip("pyarrow") import easychart from easychart.encoders import default From 629d8ba6b5d5c0b21ca94b292ea5c89a0ee55cbd Mon Sep 17 00:00:00 2001 From: ClaytonGillespie Date: Mon, 22 Dec 2025 18:58:33 +0000 Subject: [PATCH 11/12] Updated README.md to include Polars example --- README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.md b/README.md index 5752aa7..f9ffa5d 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,32 @@ chart +## Plotting with DataFrames + +easychart works with pandas, Polars, PyArrow, and other dataframe libraries. Pandas is a dependency while other dataframe libraries are supported via [narwhals](https://github.com/narwhals-dev/narwhals). + +```python +import pandas as pd +import easychart + +# pandas Series uses index as x-axis +data = pd.Series([10, 20, 30], index=["a", "b", "c"], name="values") +chart = easychart.new() +chart.plot(data) +chart +``` + +```python +import polars as pl +import easychart + +# Polars DataFrame +data = pl.DataFrame({"x": [1, 2, 3], "y": [10, 20, 30]}) +chart = easychart.new() +chart.plot(data) +chart +``` + ## Documentation Complete documentation is hosted on [read the docs](https://easychart.readthedocs.io/en/latest/). Have a look at one of the [25+ example charts](https://easychart.readthedocs.io/en/latest/contents/examples/index.html). From 8534306822b702ba3fa9467c4357a7e8f8ca2312 Mon Sep 17 00:00:00 2001 From: ClaytonGillespie Date: Sat, 27 Dec 2025 18:22:51 +0000 Subject: [PATCH 12/12] Improved to_pandas conversion --- easychart/__init__.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/easychart/__init__.py b/easychart/__init__.py index fcf09a4..3bf06d7 100644 --- a/easychart/__init__.py +++ b/easychart/__init__.py @@ -375,16 +375,10 @@ def heatmap( easychart.Chart """ # Convert non-pandas DataFrames (Polars, etc.) to pandas - # Suggested approach for heatmap() is pragmatic - since it relies heavily - # on pandas operations (.stack(), .rename_axis(), index/column handling), - # here convert Polars DataFrames to pandas first - # Could review on more complete narwhals uplift + # Heatmap relies heavily on pandas operations (.stack(), .rename_axis(), etc.) + # so convert to pandas first via narwhals if not isinstance(data, pd.DataFrame) and nw_dep.is_into_dataframe(data): - native = nw.from_native(data, eager_only=True).to_native() - if hasattr(native, "to_pandas"): - data = native.to_pandas() - else: - data = pd.DataFrame(native.to_numpy()) + data = nw.from_native(data, eager_only=True).to_pandas() if isinstance(data, pd.DataFrame): if not (