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!): Use Altair in DataFrame.plot #17995

Merged
merged 50 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
9ed8836
feat(python!): Use Altair in DataFrame.plot
MarcoGorelli Aug 1, 2024
00f7413
missing file
MarcoGorelli Aug 1, 2024
f0c806f
use ChannelType
MarcoGorelli Aug 1, 2024
eaafc23
typing
MarcoGorelli Aug 1, 2024
db6d8f7
requirements
MarcoGorelli Aug 1, 2024
f1e5906
Merge remote-tracking branch 'upstream/main' into altair
MarcoGorelli Aug 11, 2024
08e09d4
add histogram example to docstring
MarcoGorelli Aug 11, 2024
c6e7d6b
update user guide
MarcoGorelli Aug 11, 2024
9a92211
formatting
MarcoGorelli Aug 11, 2024
c59780e
cross-version compat
MarcoGorelli Aug 11, 2024
541361b
py38 typing compat
MarcoGorelli Aug 11, 2024
7f51118
py38 typing compat
MarcoGorelli Aug 11, 2024
bb6116c
fix minimum version
MarcoGorelli Aug 11, 2024
f4b42b1
try setting typing extensions minimum
MarcoGorelli Aug 11, 2024
91a19f8
regular pip install to debug :sunglasses:
MarcoGorelli Aug 11, 2024
5c61982
that worked...what if we put uv back but without compile-bytecode?
MarcoGorelli Aug 11, 2024
8a11760
maybe not
MarcoGorelli Aug 11, 2024
f27326d
try putting torch and extra-index-url on the same line
MarcoGorelli Aug 12, 2024
f294a1e
inline torch install
MarcoGorelli Aug 12, 2024
2a1cba0
UV_INDEX_STRATEGY
MarcoGorelli Aug 12, 2024
b029aed
revert requirements-ci.txt change
MarcoGorelli Aug 12, 2024
e19a0b4
need both altair and hvplot in user guide docs
MarcoGorelli Aug 12, 2024
db6b59f
another strategy
MarcoGorelli Aug 12, 2024
e22550b
maybe a bit of separation was all we needed
MarcoGorelli Aug 12, 2024
6ff8e99
what if we install cython
MarcoGorelli Aug 12, 2024
a9861a0
only use extra index url on linux?
MarcoGorelli Aug 12, 2024
8d329e4
include fi
MarcoGorelli Aug 12, 2024
c7a31f0
regular old-fashioned pip install
MarcoGorelli Aug 12, 2024
f3186b5
revert requirements-ci.txt change
MarcoGorelli Aug 12, 2024
dfd25fa
Merge remote-tracking branch 'upstream/main' into altair
MarcoGorelli Aug 13, 2024
5bd4fb4
Merge remote-tracking branch 'upstream/main' into altair
MarcoGorelli Aug 13, 2024
0f5e803
install typing-extensions _before_ the other requirements
MarcoGorelli Aug 13, 2024
df98a2e
minor updates
MarcoGorelli Aug 14, 2024
8d786e1
extra comment
MarcoGorelli Aug 14, 2024
4491d83
remove unused type alias
MarcoGorelli Aug 14, 2024
fb0438d
Merge remote-tracking branch 'upstream/main' into altair
MarcoGorelli Aug 14, 2024
9266adb
lint
MarcoGorelli Aug 14, 2024
615bc9f
Merge remote-tracking branch 'upstream/main' into altair
MarcoGorelli Aug 14, 2024
6f4d85a
:truck: 1.5.0 => 1.6.0
MarcoGorelli Aug 14, 2024
3942e62
Merge remote-tracking branch 'upstream/main' into altair
MarcoGorelli Aug 16, 2024
4bc052f
wip
MarcoGorelli Aug 17, 2024
e043a35
add Series.plot
MarcoGorelli Aug 17, 2024
65fae74
Merge remote-tracking branch 'upstream/main' into altair
MarcoGorelli Aug 18, 2024
ec57fb0
add Series.plot
MarcoGorelli Aug 18, 2024
28ac596
add missing page, add `scatter` as alias
MarcoGorelli Aug 18, 2024
d5167f1
lint
MarcoGorelli Aug 18, 2024
efed5c9
rename, better bar plot example, simplify
MarcoGorelli Aug 18, 2024
381d481
assorted improvements
MarcoGorelli Aug 18, 2024
ea018b5
assorted docs and typing improvements
MarcoGorelli Aug 19, 2024
40a0e31
Merge remote-tracking branch 'upstream/main' into altair
MarcoGorelli Aug 26, 2024
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
1 change: 0 additions & 1 deletion py-polars/docs/source/reference/series/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ This page gives an overview of all public Series methods.
list
modify_select
miscellaneous
plot
MarcoGorelli marked this conversation as resolved.
Show resolved Hide resolved
string
struct
temporal
Expand Down
7 changes: 0 additions & 7 deletions py-polars/docs/source/reference/series/plot.rst

This file was deleted.

49 changes: 27 additions & 22 deletions py-polars/polars/dataframe/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
from polars._utils.wrap import wrap_expr, wrap_ldf, wrap_s
from polars.dataframe._html import NotebookFormatter
from polars.dataframe.group_by import DynamicGroupBy, GroupBy, RollingGroupBy
from polars.dataframe.plotting import Plot
from polars.datatypes import (
N_INFER_DEFAULT,
Boolean,
Expand All @@ -82,15 +83,15 @@
)
from polars.datatypes.group import INTEGER_DTYPES
from polars.dependencies import (
_ALTAIR_AVAILABLE,
_GREAT_TABLES_AVAILABLE,
_HVPLOT_AVAILABLE,
_PANDAS_AVAILABLE,
_PYARROW_AVAILABLE,
_check_for_numpy,
_check_for_pandas,
_check_for_pyarrow,
altair,
great_tables,
hvplot,
import_optional,
)
from polars.dependencies import numpy as np
Expand Down Expand Up @@ -123,7 +124,6 @@
import numpy.typing as npt
import torch
from great_tables import GT
from hvplot.plotting.core import hvPlotTabularPolars
from xlsxwriter import Workbook

from polars import DataType, Expr, LazyFrame, Series
Expand Down Expand Up @@ -603,17 +603,30 @@ def _replace(self, column: str, new_column: Series) -> DataFrame:

@property
@unstable()
def plot(self) -> hvPlotTabularPolars:
def plot(self) -> Plot:
"""
Create a plot namespace.

.. warning::
This functionality is currently considered **unstable**. It may be
changed at any point without it being considered a breaking change.

.. versionchanged:: 1.4.0
In prior versions of Polars, HvPlot was the plotting backend. If you would
like to restore the previous plotting functionality, all you need to do
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
hvplot. Please see the `hvplot reference gallery <https://hvplot.holoviz.org/reference/index.html>`_
for more information and documentation.
Altair:

- `df.plot.line(*args, **kwargs)`
is shorthand for
`alt.Chart(df).mark_line().encode(*args, **kwargs).interactive()`
- `df.plot.point(*args, **kwargs)`
is shorthand for
`alt.Chart(df).mark_point().encode(*args, **kwargs).interactive()`
- ... (likewise, for any other attribute, e.g. `df.plot.bar`)

Examples
--------
Expand All @@ -626,32 +639,24 @@ def plot(self) -> hvPlotTabularPolars:
... "species": ["setosa", "setosa", "versicolor"],
... }
... )
>>> df.plot.scatter(x="length", y="width", by="species") # doctest: +SKIP
>>> df.plot.point(x="length", y="width", color="species") # doctest: +SKIP

Line plot:

>>> from datetime import date
>>> df = pl.DataFrame(
... {
... "date": [date(2020, 1, 2), date(2020, 1, 3), date(2020, 1, 4)],
... "stock_1": [1, 4, 6],
... "stock_2": [1, 5, 2],
... "date": [date(2020, 1, 2), date(2020, 1, 3), date(2020, 1, 4)] * 2,
... "price": [1, 4, 6, 1, 5, 2],
... "stock": ["a", "a", "a", "b", "b", "b"],
... }
... )
>>> df.plot.line(x="date", y=["stock_1", "stock_2"]) # doctest: +SKIP

For more info on what you can pass, you can use ``hvplot.help``:

>>> import hvplot # doctest: +SKIP
>>> hvplot.help("scatter") # doctest: +SKIP
>>> df.plot.line(x="date", y="price", color="stock") # doctest: +SKIP
"""
if not _HVPLOT_AVAILABLE or parse_version(hvplot.__version__) < parse_version(
"0.9.1"
):
msg = "hvplot>=0.9.1 is required for `.plot`"
if not _ALTAIR_AVAILABLE or parse_version(altair.__version__) < (5, 3, 0):
msg = "altair>=5.3.0 is required for `.plot`"
raise ModuleUpgradeRequiredError(msg)
hvplot.post_patch()
return hvplot.plotting.core.hvPlotTabularPolars(self)
return Plot(self)

@property
@unstable()
Expand Down
236 changes: 236 additions & 0 deletions py-polars/polars/dataframe/plotting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Any, Callable, Mapping, Union

if TYPE_CHECKING:
import sys

import altair as alt

from polars import DataFrame

if sys.version_info >= (3, 10):
from typing import TypeAlias
else:
from typing_extensions import TypeAlias

ChannelType: TypeAlias = Union[str, Mapping[str, Any], Any]
MarcoGorelli marked this conversation as resolved.
Show resolved Hide resolved


class Plot:
"""DataFrame.plot namespace."""

chart: alt.Chart

def __init__(self, df: DataFrame) -> None:
import altair as alt

self.chart = alt.Chart(df)

def bar(
self,
x: ChannelType | None = None,
y: ChannelType | None = None,
color: ChannelType | None = None,
tooltip: ChannelType | None = None,
*args: Any,
**kwargs: Any,
) -> alt.Chart:
"""
Draw bar plot.

Polars does not implement plottinng logic itself but instead defers to Altair.
MarcoGorelli marked this conversation as resolved.
Show resolved Hide resolved
`df.plot.bar(*args, **kwargs)` is shorthand for
`alt.Chart(df).mark_bar().encode(*args, **kwargs).interactive()`,
as is intended for convenience - for full customisatibility, use a plotting
library directly.

.. versionchanged:: 1.4.0
In prior versions of Polars, HvPlot was the plotting backend. If you would
like to restore the previous plotting functionality, all you need to do
add `import hvplot.polars` at the top of your script and replace
`df.plot` with `df.hvplot`.

Parameters
----------
x
Column with x-coordinates of bars.
y
Column with y-coordinates of bars.
color
Column to color bars by.
tooltip
Columns to show values of when hovering over points with pointer.
*args, **kwargs
Additional arguments and keyword arguments passed to Altair.

Examples
--------
>>> from datetime import date
>>> df = pl.DataFrame(
... {
... "date": [date(2020, 1, 2), date(2020, 1, 3), date(2020, 1, 4)] * 2,
... "price": [1, 4, 6, 1, 5, 2],
... "stock": ["a", "a", "a", "b", "b", "b"],
... }
... )
>>> df.plot.line(x="date", y="price", color="stock") # doctest: +SKIP
"""
encodings = {}
if x is not None:
encodings["x"] = x
if y is not None:
encodings["y"] = y
if color is not None:
encodings["color"] = color
if tooltip is not None:
encodings["tooltip"] = tooltip
return (
self.chart.mark_bar().encode(*args, **{**encodings, **kwargs}).interactive()
)

def line(
self,
x: ChannelType | None = None,
y: ChannelType | None = None,
color: ChannelType | None = None,
order: ChannelType | None = None,
tooltip: ChannelType | None = None,
*args: Any,
**kwargs: Any,
) -> alt.Chart:
"""
Draw line plot.

Polars does not implement plottinng logic itself but instead defers to Altair.
MarcoGorelli marked this conversation as resolved.
Show resolved Hide resolved
`df.plot.line(*args, **kwargs)` is shorthand for
`alt.Chart(df).mark_line().encode(*args, **kwargs).interactive()`,
as is intended for convenience - for full customisatibility, use a plotting
library directly.

.. versionchanged:: 1.4.0
In prior versions of Polars, HvPlot was the plotting backend. If you would
like to restore the previous plotting functionality, all you need to do
add `import hvplot.polars` at the top of your script and replace
`df.plot` with `df.hvplot`.

Parameters
----------
x
Column with x-coordinates of lines.
y
Column with y-coordinates of lines.
color
Column to color lines by.
order
Column to use for order of data points in lines.
tooltip
Columns to show values of when hovering over points with pointer.
*args, **kwargs
Additional arguments and keyword arguments passed to Altair.

Examples
--------
>>> from datetime import date
>>> df = pl.DataFrame(
... {
... "date": [date(2020, 1, 2), date(2020, 1, 3), date(2020, 1, 4)] * 2,
... "price": [1, 4, 6, 1, 5, 2],
... "stock": ["a", "a", "a", "b", "b", "b"],
... }
... )
>>> df.plot.line(x="date", y="price", color="stock") # doctest: +SKIP
"""
encodings = {}
if x is not None:
encodings["x"] = x
if y is not None:
encodings["y"] = y
if color is not None:
encodings["color"] = color
if order is not None:
encodings["order"] = order
if tooltip is not None:
encodings["tooltip"] = tooltip
return (
self.chart.mark_line()
.encode(*args, **{**encodings, **kwargs})
.interactive()
)

def point(
self,
x: ChannelType | None = None,
y: ChannelType | None = None,
color: ChannelType | None = None,
size: ChannelType | None = None,
tooltip: ChannelType | None = None,
*args: Any,
**kwargs: Any,
) -> alt.Chart:
"""
Draw scatter plot.

Polars does not implement plottinng logic itself but instead defers to Altair.
MarcoGorelli marked this conversation as resolved.
Show resolved Hide resolved
`df.plot.point(*args, **kwargs)` is shorthand for
`alt.Chart(df).mark_point().encode(*args, **kwargs).interactive()`,
as is intended for convenience - for full customisatibility, use a plotting
library directly.

.. versionchanged:: 1.4.0
In prior versions of Polars, HvPlot was the plotting backend. If you would
like to restore the previous plotting functionality, all you need to do
add `import hvplot.polars` at the top of your script and replace
`df.plot` with `df.hvplot`.

Parameters
----------
x
Column with x-coordinates of points.
y
Column with y-coordinates of points.
color
Column to color points by.
size
Column which determines points' sizes.
tooltip
Columns to show values of when hovering over points with pointer.
*args, **kwargs
Additional arguments and keyword arguments passed to Altair.

Examples
--------
>>> df = pl.DataFrame(
... {
... "length": [1, 4, 6],
... "width": [4, 5, 6],
... "species": ["setosa", "setosa", "versicolor"],
... }
... )
>>> df.plot.point(x="length", y="width", color="species") # doctest: +SKIP
"""
encodings = {}
if x is not None:
encodings["x"] = x
if y is not None:
encodings["y"] = y
if color is not None:
encodings["color"] = color
if size is not None:
encodings["size"] = size
if tooltip is not None:
encodings["tooltip"] = tooltip
return (
self.chart.mark_point()
.encode(*args, **{**encodings, **kwargs})
.interactive()
)

def __getattr__(
self, attr: str, *args: Any, **kwargs: Any
) -> Callable[..., alt.Chart]:
method = self.chart.getattr(f"mark_{attr}", None)
if method is None:
msg = "Altair has no method 'mark_{attr}'"
raise AttributeError(msg)
return method().encode(*args, **kwargs).interactive()
Loading
Loading