From 57106a7605b37f0be973ac7768c9e11187c4e920 Mon Sep 17 00:00:00 2001
From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com>
Date: Sat, 7 Sep 2024 14:45:53 +0100
Subject: [PATCH] feat(python): add nicer default plot configuration, link to
Altair Chart Configuration docs
---
.../python/user-guide/misc/visualization.py | 12 +-
docs/user-guide/misc/visualization.md | 7 +-
py-polars/polars/dataframe/frame.py | 20 +--
py-polars/polars/dataframe/plotting.py | 167 +++++++++++++++---
py-polars/polars/series/plotting.py | 109 ++++++++----
py-polars/polars/series/series.py | 18 +-
6 files changed, 233 insertions(+), 100 deletions(-)
diff --git a/docs/src/python/user-guide/misc/visualization.py b/docs/src/python/user-guide/misc/visualization.py
index cd256127f1dd..7ec508bf8b82 100644
--- a/docs/src/python/user-guide/misc/visualization.py
+++ b/docs/src/python/user-guide/misc/visualization.py
@@ -15,6 +15,7 @@
y="sepal_length",
by="species",
width=650,
+ title="Irises",
)
# --8<-- [end:hvplot_show_plot]
"""
@@ -27,6 +28,7 @@
y="sepal_length",
by="species",
width=650,
+ title="Irises",
)
hvplot.save(plot, "docs/images/hvplot_scatter.html")
with open("docs/images/hvplot_scatter.html", "r") as f:
@@ -44,6 +46,7 @@
y=df["sepal_length"],
c=df["species"].cast(pl.Categorical).to_physical(),
)
+ax.set_title('Irises')
# --8<-- [end:matplotlib_show_plot]
"""
@@ -58,6 +61,7 @@
y=df["sepal_length"],
c=df["species"].cast(pl.Categorical).to_physical(),
)
+ax.set_title("Irises")
fig.savefig("docs/images/matplotlib_scatter.png")
with open("docs/images/matplotlib_scatter.png", "rb") as f:
png = base64.b64encode(f.read()).decode()
@@ -72,7 +76,7 @@
x="sepal_width",
y="sepal_length",
hue="species",
-)
+).set_title('Irises')
# --8<-- [end:seaborn_show_plot]
"""
@@ -86,7 +90,7 @@
x="sepal_width",
y="sepal_length",
hue="species",
-)
+).set_title("Irises")
fig.savefig("docs/images/seaborn_scatter.png")
with open("docs/images/seaborn_scatter.png", "rb") as f:
png = base64.b64encode(f.read()).decode()
@@ -103,6 +107,7 @@
y="sepal_length",
color="species",
width=650,
+ title="Irises",
)
# --8<-- [end:plotly_show_plot]
"""
@@ -116,6 +121,7 @@
y="sepal_length",
color="species",
width=650,
+ title="Irises",
)
fig.write_html(
"docs/images/plotly_scatter.html", full_html=False, include_plotlyjs="cdn"
@@ -132,6 +138,7 @@
x="sepal_length",
y="sepal_width",
color="species",
+ title="Irises",
)
.properties(width=500)
.configure_scale(zero=False)
@@ -145,6 +152,7 @@
x="sepal_length",
y="sepal_width",
color="species",
+ title="Irises",
)
.properties(width=500)
.configure_scale(zero=False)
diff --git a/docs/user-guide/misc/visualization.md b/docs/user-guide/misc/visualization.md
index 3f7574c07a2e..07e44c8f6578 100644
--- a/docs/user-guide/misc/visualization.md
+++ b/docs/user-guide/misc/visualization.md
@@ -32,14 +32,17 @@ import altair as alt
y="sepal_width",
color="species",
)
- .properties(width=500)
+ .properties(width=500, title="Irises")
.configure_scale(zero=False)
)
```
-and is only provided for convenience, and to signal that Altair is known to work well with
+(with some extra configuration) and is only provided for convenience, and to signal that Altair is known to work well with
Polars.
+For configuration, we suggest reading [Chart Configuration](https://altair-viz.github.io/altair-tutorial/notebooks/08-Configuration.html). For example, you can change the x-axis label rotation by appending
+`.configure_axisX(rotation=30)` to your call.
+
## hvPlot
If you import `hvplot.polars`, then it registers a `hvplot`
diff --git a/py-polars/polars/dataframe/frame.py b/py-polars/polars/dataframe/frame.py
index f0d2d1abe2e4..f2c47b45d902 100644
--- a/py-polars/polars/dataframe/frame.py
+++ b/py-polars/polars/dataframe/frame.py
@@ -618,22 +618,10 @@ def plot(self) -> DataFramePlot:
is add `import hvplot.polars` at the top of your script and replace
`df.plot` with `df.hvplot`.
- Polars does not implement plotting logic itself, but instead defers to
- `Altair `_:
-
- - `df.plot.line(**kwargs)`
- is shorthand for
- `alt.Chart(df).mark_line().encode(**kwargs).interactive()`
- - `df.plot.point(**kwargs)`
- is shorthand for
- `alt.Chart(df).mark_point().encode(**kwargs).interactive()` (and
- `plot.scatter` is provided as an alias)
- - `df.plot.bar(**kwargs)`
- is shorthand for
- `alt.Chart(df).mark_bar().encode(**kwargs).interactive()`
- - for any other attribute `attr`, `df.plot.attr(**kwargs)`
- is shorthand for
- `alt.Chart(df).mark_attr().encode(**kwargs).interactive()`
+ Polars defers to `Altair `_ for plotting, and
+ this functionality is only provided for convenience.
+ For configuration, we suggest reading `Chart Configuration
+ `_.
Examples
--------
diff --git a/py-polars/polars/dataframe/plotting.py b/py-polars/polars/dataframe/plotting.py
index ed118e504656..12020addffaa 100644
--- a/py-polars/polars/dataframe/plotting.py
+++ b/py-polars/polars/dataframe/plotting.py
@@ -35,6 +35,73 @@
]
+def configure_chart(
+ chart: alt.Chart,
+ *,
+ title: str | None,
+ x_axis_title: str | None,
+ y_axis_title: str | None,
+) -> alt.Chart:
+ """
+ A nice-looking default configuration, produced by Altair maintainer.
+
+ Source: https://gist.github.com/binste/b4042fa76a89d72d45cbbb9355ec6906.
+ """
+ properties = {}
+ if title is not None:
+ properties["title"] = title
+ if x_axis_title is not None:
+ chart.encoding.x.title = x_axis_title
+ if y_axis_title is not None:
+ chart.encoding.y.title = y_axis_title
+ return (
+ chart.properties(**properties)
+ .configure_axis(
+ labelFontSize=16,
+ titleFontSize=16,
+ titleFontWeight="normal",
+ gridColor="lightGray",
+ labelAngle=0,
+ labelFlush=False,
+ labelPadding=5,
+ )
+ .configure_axisY(
+ domain=False,
+ ticks=False,
+ labelPadding=10,
+ titleAngle=0,
+ titleY=-20,
+ titleAlign="left",
+ titlePadding=0,
+ )
+ .configure_axisTemporal(grid=False)
+ .configure_axisDiscrete(ticks=False, labelPadding=10, grid=False)
+ .configure_scale(barBandPaddingInner=0.2)
+ .configure_header(labelFontSize=16, titleFontSize=16)
+ .configure_legend(labelFontSize=16, titleFontSize=16, titleFontWeight="normal")
+ .configure_title(
+ fontSize=20,
+ fontStyle="normal",
+ align="left",
+ anchor="start",
+ orient="top",
+ fontWeight=600,
+ offset=10,
+ subtitlePadding=3,
+ subtitleFontSize=16,
+ )
+ .configure_view(
+ strokeWidth=0, continuousHeight=350, continuousWidth=600, step=50
+ )
+ .configure_line(strokeWidth=3.5)
+ .configure_text(fontSize=16)
+ .configure_circle(size=60)
+ .configure_point(size=60)
+ .configure_square(size=60)
+ .interactive()
+ )
+
+
class DataFramePlot:
"""DataFrame.plot namespace."""
@@ -50,18 +117,18 @@ def bar(
color: ChannelColor | None = None,
tooltip: ChannelTooltip | None = None,
/,
+ title: str | None = None,
+ x_axis_title: str | None = None,
+ y_axis_title: str | None = None,
**kwargs: Unpack[EncodeKwds],
) -> alt.Chart:
"""
Draw bar plot.
- Polars does not implement plotting logic itself but instead defers to
- `Altair `_.
-
- `df.plot.bar(**kwargs)` is shorthand for
- `alt.Chart(df).mark_bar().encode(**kwargs).interactive()`,
- and is provided for convenience - for full customisatibility, use a plotting
- library directly.
+ Polars defers to `Altair `_ for plotting, and
+ this functionality is only provided for convenience.
+ For configuration, we suggest reading `Chart Configuration
+ `_.
.. versionchanged:: 1.6.0
In prior versions of Polars, HvPlot was the plotting backend. If you would
@@ -79,6 +146,12 @@ def bar(
Column to color bars by.
tooltip
Columns to show values of when hovering over bars with pointer.
+ title
+ Plot title.
+ x_axis_title
+ Title of x-axis.
+ y_axis_title
+ Title of y-axis.
**kwargs
Additional keyword arguments passed to Altair.
@@ -104,7 +177,12 @@ def bar(
encodings["color"] = color
if tooltip is not None:
encodings["tooltip"] = tooltip
- return self._chart.mark_bar().encode(**encodings, **kwargs).interactive()
+ return configure_chart(
+ self._chart.mark_bar().encode(**encodings, **kwargs),
+ title=title,
+ x_axis_title=x_axis_title,
+ y_axis_title=y_axis_title,
+ )
def line(
self,
@@ -114,17 +192,18 @@ def line(
order: ChannelOrder | None = None,
tooltip: ChannelTooltip | None = None,
/,
+ title: str | None = None,
+ x_axis_title: str | None = None,
+ y_axis_title: str | None = None,
**kwargs: Unpack[EncodeKwds],
) -> alt.Chart:
"""
Draw line plot.
- Polars does not implement plotting logic itself but instead defers to
- `Altair `_.
-
- `alt.Chart(df).mark_line().encode(**kwargs).interactive()`,
- and is provided for convenience - for full customisatibility, use a plotting
- library directly.
+ Polars defers to `Altair `_ for plotting, and
+ this functionality is only provided for convenience.
+ For configuration, we suggest reading `Chart Configuration
+ `_.
.. versionchanged:: 1.6.0
In prior versions of Polars, HvPlot was the plotting backend. If you would
@@ -144,6 +223,12 @@ def line(
Column to use for order of data points in lines.
tooltip
Columns to show values of when hovering over lines with pointer.
+ title
+ Plot title.
+ x_axis_title
+ Title of x-axis.
+ y_axis_title
+ Title of y-axis.
**kwargs
Additional keyword arguments passed to Altair.
@@ -170,7 +255,12 @@ def line(
encodings["order"] = order
if tooltip is not None:
encodings["tooltip"] = tooltip
- return self._chart.mark_line().encode(**encodings, **kwargs).interactive()
+ return configure_chart(
+ self._chart.mark_line().encode(**encodings, **kwargs),
+ title=title,
+ x_axis_title=x_axis_title,
+ y_axis_title=y_axis_title,
+ )
def point(
self,
@@ -180,18 +270,18 @@ def point(
size: ChannelSize | None = None,
tooltip: ChannelTooltip | None = None,
/,
+ title: str | None = None,
+ x_axis_title: str | None = None,
+ y_axis_title: str | None = None,
**kwargs: Unpack[EncodeKwds],
) -> alt.Chart:
"""
Draw scatter plot.
- Polars does not implement plotting logic itself but instead defers to
- `Altair `_.
-
- `df.plot.point(**kwargs)` is shorthand for
- `alt.Chart(df).mark_point().encode(**kwargs).interactive()`,
- and is provided for convenience - for full customisatibility, use a plotting
- library directly.
+ Polars defers to `Altair `_ for plotting, and
+ this functionality is only provided for convenience.
+ For configuration, we suggest reading `Chart Configuration
+ `_.
.. versionchanged:: 1.6.0
In prior versions of Polars, HvPlot was the plotting backend. If you would
@@ -211,6 +301,12 @@ def point(
Column which determines points' sizes.
tooltip
Columns to show values of when hovering over points with pointer.
+ title
+ Plot title.
+ x_axis_title
+ Title of x-axis.
+ y_axis_title
+ Title of y-axis.
**kwargs
Additional keyword arguments passed to Altair.
@@ -236,21 +332,34 @@ def point(
encodings["size"] = size
if tooltip is not None:
encodings["tooltip"] = tooltip
- return (
- self._chart.mark_point()
- .encode(
+ return configure_chart(
+ self._chart.mark_point().encode(
**encodings,
**kwargs,
- )
- .interactive()
+ ),
+ title=title,
+ x_axis_title=x_axis_title,
+ y_axis_title=y_axis_title,
)
# Alias to `point` because of how common it is.
scatter = point
- def __getattr__(self, attr: str) -> Callable[..., alt.Chart]:
+ def __getattr__(
+ self,
+ attr: str,
+ *,
+ title: str | None = None,
+ x_axis_title: str | None = None,
+ y_axis_title: str | None = None,
+ ) -> Callable[..., alt.Chart]:
method = getattr(self._chart, f"mark_{attr}", None)
if method is None:
msg = "Altair has no method 'mark_{attr}'"
raise AttributeError(msg)
- return lambda **kwargs: method().encode(**kwargs).interactive()
+ return lambda **kwargs: configure_chart(
+ method().encode(**kwargs),
+ title=title,
+ x_axis_title=x_axis_title,
+ y_axis_title=y_axis_title,
+ )
diff --git a/py-polars/polars/series/plotting.py b/py-polars/polars/series/plotting.py
index cb5c6c93a1e1..7932c52b2264 100644
--- a/py-polars/polars/series/plotting.py
+++ b/py-polars/polars/series/plotting.py
@@ -2,6 +2,7 @@
from typing import TYPE_CHECKING, Callable
+from polars.dataframe.plotting import configure_chart
from polars.dependencies import altair as alt
if TYPE_CHECKING:
@@ -30,18 +31,18 @@ def __init__(self, s: Series) -> None:
def hist(
self,
/,
+ title: str | None = None,
+ x_axis_title: str | None = None,
+ y_axis_title: str | None = None,
**kwargs: Unpack[EncodeKwds],
) -> alt.Chart:
"""
Draw histogram.
- Polars does not implement plotting logic itself but instead defers to
- `Altair `_.
-
- `s.plot.hist(**kwargs)` is shorthand for
- `alt.Chart(s.to_frame()).mark_bar().encode(x=alt.X(f'{s.name}:Q', bin=True), y='count()', **kwargs).interactive()`,
- and is provided for convenience - for full customisatibility, use a plotting
- library directly.
+ Polars defers to `Altair `_ for plotting, and
+ this functionality is only provided for convenience.
+ For configuration, we suggest reading `Chart Configuration
+ `_.
.. versionchanged:: 1.6.0
In prior versions of Polars, HvPlot was the plotting backend. If you would
@@ -51,6 +52,12 @@ def hist(
Parameters
----------
+ title
+ Plot title.
+ x_axis_title
+ Title of x-axis.
+ y_axis_title
+ Title of y-axis.
**kwargs
Additional arguments and keyword arguments passed to Altair.
@@ -58,32 +65,34 @@ def hist(
--------
>>> s = pl.Series("price", [1, 3, 3, 3, 5, 2, 6, 5, 5, 5, 7])
>>> s.plot.hist() # doctest: +SKIP
- """ # noqa: W505
+ """
if self._series_name == "count()":
msg = "Cannot use `plot.hist` when Series name is `'count()'`"
raise ValueError(msg)
- return (
+ return configure_chart(
alt.Chart(self._df)
.mark_bar()
- .encode(x=alt.X(f"{self._series_name}:Q", bin=True), y="count()", **kwargs) # type: ignore[misc]
- .interactive()
+ .encode(x=alt.X(f"{self._series_name}:Q", bin=True), y="count()", **kwargs), # type: ignore[misc]
+ title=title,
+ x_axis_title=x_axis_title,
+ y_axis_title=y_axis_title,
)
def kde(
self,
/,
+ title: str | None = None,
+ x_axis_title: str | None = None,
+ y_axis_title: str | None = None,
**kwargs: Unpack[EncodeKwds],
) -> alt.Chart:
"""
Draw kernel density estimate plot.
- Polars does not implement plotting logic itself but instead defers to
- `Altair `_.
-
- `s.plot.kde(**kwargs)` is shorthand for
- `alt.Chart(s.to_frame()).transform_density(s.name, as_=[s.name, 'density']).mark_area().encode(x=s.name, y='density:Q', **kwargs).interactive()`,
- and is provided for convenience - for full customisatibility, use a plotting
- library directly.
+ Polars defers to `Altair `_ for plotting, and
+ this functionality is only provided for convenience.
+ For configuration, we suggest reading `Chart Configuration
+ `_.
.. versionchanged:: 1.6.0
In prior versions of Polars, HvPlot was the plotting backend. If you would
@@ -93,6 +102,12 @@ def kde(
Parameters
----------
+ title
+ Plot title.
+ x_axis_title
+ Title of x-axis.
+ y_axis_title
+ Title of y-axis.
**kwargs
Additional keyword arguments passed to Altair.
@@ -100,33 +115,35 @@ def kde(
--------
>>> s = pl.Series("price", [1, 3, 3, 3, 5, 2, 6, 5, 5, 5, 7])
>>> s.plot.kde() # doctest: +SKIP
- """ # noqa: W505
+ """
if self._series_name == "density":
msg = "Cannot use `plot.kde` when Series name is `'density'`"
raise ValueError(msg)
- return (
+ return configure_chart(
alt.Chart(self._df)
.transform_density(self._series_name, as_=[self._series_name, "density"])
.mark_area()
- .encode(x=self._series_name, y="density:Q", **kwargs) # type: ignore[misc]
- .interactive()
+ .encode(x=self._series_name, y="density:Q", **kwargs), # type: ignore[misc]
+ title=title,
+ x_axis_title=x_axis_title,
+ y_axis_title=y_axis_title,
)
def line(
self,
/,
+ title: str | None = None,
+ x_axis_title: str | None = None,
+ y_axis_title: str | None = None,
**kwargs: Unpack[EncodeKwds],
) -> alt.Chart:
"""
Draw line plot.
- Polars does not implement plotting logic itself but instead defers to
- `Altair `_.
-
- `s.plot.line(**kwargs)` is shorthand for
- `alt.Chart(s.to_frame().with_row_index()).mark_line().encode(x='index', y=s.name, **kwargs).interactive()`,
- and is provided for convenience - for full customisatibility, use a plotting
- library directly.
+ Polars defers to `Altair `_ for plotting, and
+ this functionality is only provided for convenience.
+ For configuration, we suggest reading `Chart Configuration
+ `_.
.. versionchanged:: 1.6.0
In prior versions of Polars, HvPlot was the plotting backend. If you would
@@ -136,6 +153,12 @@ def line(
Parameters
----------
+ title
+ Plot title.
+ x_axis_title
+ Title of x-axis.
+ y_axis_title
+ Title of y-axis.
**kwargs
Additional keyword arguments passed to Altair.
@@ -143,18 +166,27 @@ def line(
--------
>>> s = pl.Series("price", [1, 3, 3, 3, 5, 2, 6, 5, 5, 5, 7])
>>> s.plot.kde() # doctest: +SKIP
- """ # noqa: W505
+ """
if self._series_name == "index":
msg = "Cannot call `plot.line` when Series name is 'index'"
raise ValueError(msg)
- return (
+ return configure_chart(
alt.Chart(self._df.with_row_index())
.mark_line()
- .encode(x="index", y=self._series_name, **kwargs) # type: ignore[misc]
- .interactive()
+ .encode(x="index", y=self._series_name, **kwargs), # type: ignore[misc]
+ title=title,
+ x_axis_title=x_axis_title,
+ y_axis_title=y_axis_title,
)
- def __getattr__(self, attr: str) -> Callable[..., alt.Chart]:
+ def __getattr__(
+ self,
+ attr: str,
+ *,
+ title: str | None = None,
+ x_axis_title: str | None = None,
+ y_axis_title: str | None = None,
+ ) -> Callable[..., alt.Chart]:
if self._series_name == "index":
msg = "Cannot call `plot.{attr}` when Series name is 'index'"
raise ValueError(msg)
@@ -165,8 +197,9 @@ def __getattr__(self, attr: str) -> Callable[..., alt.Chart]:
if method is None:
msg = "Altair has no method 'mark_{attr}'"
raise AttributeError(msg)
- return (
- lambda **kwargs: method()
- .encode(x="index", y=self._series_name, **kwargs)
- .interactive()
+ return lambda **kwargs: configure_chart(
+ method().encode(x="index", y=self._series_name, **kwargs),
+ title=title,
+ x_axis_title=x_axis_title,
+ y_axis_title=y_axis_title,
)
diff --git a/py-polars/polars/series/series.py b/py-polars/polars/series/series.py
index e2a4cb936e47..becb37da81b9 100644
--- a/py-polars/polars/series/series.py
+++ b/py-polars/polars/series/series.py
@@ -7403,18 +7403,10 @@ def plot(self) -> SeriesPlot:
is add `import hvplot.polars` at the top of your script and replace
`df.plot` with `df.hvplot`.
- Polars does not implement plotting logic itself, but instead defers to
- Altair:
-
- - `s.plot.hist(**kwargs)`
- is shorthand for
- `alt.Chart(s.to_frame()).mark_bar().encode(x=alt.X(f'{s.name}:Q', bin=True), y='count()', **kwargs).interactive()`
- - `s.plot.kde(**kwargs)`
- is shorthand for
- `alt.Chart(s.to_frame()).transform_density(s.name, as_=[s.name, 'density']).mark_area().encode(x=s.name, y='density:Q', **kwargs).interactive()`
- - for any other attribute `attr`, `s.plot.attr(**kwargs)`
- is shorthand for
- `alt.Chart(s.to_frame().with_row_index()).mark_attr().encode(x='index', y=s.name, **kwargs).interactive()`
+ Polars defers to `Altair `_ for plotting, and
+ this functionality is only provided for convenience.
+ For configuration, we suggest reading `Chart Configuration
+ `_.
Examples
--------
@@ -7430,7 +7422,7 @@ def plot(self) -> SeriesPlot:
Line plot:
>>> s.plot.line() # doctest: +SKIP
- """ # noqa: W505
+ """
if not _ALTAIR_AVAILABLE or parse_version(altair.__version__) < (5, 4, 0):
msg = "altair>=5.4.0 is required for `.plot`"
raise ModuleUpgradeRequiredError(msg)