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)