Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(python): Add nicer default plot configuration, link to Altair Chart Configuration docs #18609

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions docs/src/python/user-guide/misc/visualization.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
y="sepal_length",
by="species",
width=650,
title="Irises",
)
# --8<-- [end:hvplot_show_plot]
"""
Expand All @@ -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:
Expand All @@ -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]
"""

Expand All @@ -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()
Expand All @@ -72,7 +76,7 @@
x="sepal_width",
y="sepal_length",
hue="species",
)
).set_title('Irises')
# --8<-- [end:seaborn_show_plot]
"""

Expand All @@ -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()
Expand All @@ -103,6 +107,7 @@
y="sepal_length",
color="species",
width=650,
title="Irises",
)
# --8<-- [end:plotly_show_plot]
"""
Expand All @@ -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"
Expand All @@ -132,6 +138,7 @@
x="sepal_length",
y="sepal_width",
color="species",
title="Irises",
)
.properties(width=500)
.configure_scale(zero=False)
Expand All @@ -145,6 +152,7 @@
x="sepal_length",
y="sepal_width",
color="species",
title="Irises",
)
.properties(width=500)
.configure_scale(zero=False)
Expand Down
7 changes: 5 additions & 2 deletions docs/user-guide/misc/visualization.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
20 changes: 4 additions & 16 deletions py-polars/polars/dataframe/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://altair-viz.github.io/>`_:

- `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 <https://altair-viz.github.io/>`_ for plotting, and
this functionality is only provided for convenience.
For configuration, we suggest reading `Chart Configuration
<https://altair-viz.github.io/altair-tutorial/notebooks/08-Configuration.html>`_.

Examples
--------
Expand Down
174 changes: 145 additions & 29 deletions py-polars/polars/dataframe/plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,80 @@
]


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,
)
Comment on lines +59 to +67
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I understand, the alt.Chart.configure_ calls are being used to avoid registering + enabling a theme - which could override a user's custom theme.

These work fine in isolation, but AFAIK would have issues if a user were to layer/concat/facet the result - since config is only valid at the top-level.

You might want to add tests to see if these ops would still be possible

Using a theme would have the benefit of deferring these config settings until the Chart is rendered - placing them in the top-level only.


It might be worth seeing if we can come to a good solution to this as part of vega/altair#3519 since we have already discussed issues with the theme route

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks @dangotbanned for spotting this and for taking a look!

indeed, you're right, it does break combining charts with e.g. chart1 + chart2, it raises an error

OK I think I'll close this for now and open a docs improvement PR, we can revisit the themes later

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries @MarcoGorelli

I'm quite interested in making this easier soon, hopefully we will have the right tools in place for when you revisit this

.configure_axisY(
domain=False,
ticks=False,
# Add some padding to compensate for the removed ticks as else
# the labels would be too close to the axis
labelPadding=10,
MarcoGorelli marked this conversation as resolved.
Show resolved Hide resolved
# The settings below move the axis title on top of the axis
# and orient it horizontally
titleAngle=0,
titleAlign="left",
titlePadding=0,
# This pushes the title high enough so it does not overlap with the latest
# tick. For some edge cases, this might still happen and there is no
# automated way to resolve it right now but it usually works well.
titleY=-25,
)
.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."""

Expand All @@ -50,18 +124,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 <https://altair-viz.github.io/>`_.

`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 <https://altair-viz.github.io/>`_ for plotting, and
this functionality is only provided for convenience.
For configuration, we suggest reading `Chart Configuration
<https://altair-viz.github.io/altair-tutorial/notebooks/08-Configuration.html>`_.

.. versionchanged:: 1.6.0
In prior versions of Polars, HvPlot was the plotting backend. If you would
Expand All @@ -79,6 +153,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.

Expand All @@ -104,7 +184,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,
Expand All @@ -114,17 +199,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 <https://altair-viz.github.io/>`_.

`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 <https://altair-viz.github.io/>`_ for plotting, and
this functionality is only provided for convenience.
For configuration, we suggest reading `Chart Configuration
<https://altair-viz.github.io/altair-tutorial/notebooks/08-Configuration.html>`_.

.. versionchanged:: 1.6.0
In prior versions of Polars, HvPlot was the plotting backend. If you would
Expand All @@ -144,6 +230,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.

Expand All @@ -170,7 +262,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,
Expand All @@ -180,18 +277,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 <https://altair-viz.github.io/>`_.

`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 <https://altair-viz.github.io/>`_ for plotting, and
this functionality is only provided for convenience.
For configuration, we suggest reading `Chart Configuration
<https://altair-viz.github.io/altair-tutorial/notebooks/08-Configuration.html>`_.

.. versionchanged:: 1.6.0
In prior versions of Polars, HvPlot was the plotting backend. If you would
Expand All @@ -211,6 +308,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.

Expand All @@ -236,21 +339,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,
)
Loading