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 tooltip by default to charts #18625

Merged
merged 2 commits into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
73 changes: 31 additions & 42 deletions py-polars/polars/dataframe/plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,12 @@
import sys

import altair as alt
from altair.typing import (
ChannelColor,
ChannelOrder,
ChannelSize,
ChannelTooltip,
ChannelX,
ChannelY,
EncodeKwds,
)
from altair.typing import ChannelColor as Color
from altair.typing import ChannelOrder as Order
from altair.typing import ChannelSize as Size
from altair.typing import ChannelX as X
from altair.typing import ChannelY as Y
from altair.typing import EncodeKwds
Copy link
Contributor

Choose a reason for hiding this comment

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

Just linking the context for this change #18609 (comment)

cc'ing @mattijn as this is a possible solution for vega/altair#3582 (comment)


from polars import DataFrame

Expand All @@ -29,12 +26,15 @@

Encodings: TypeAlias = Dict[
str,
Union[
ChannelX, ChannelY, ChannelColor, ChannelOrder, ChannelSize, ChannelTooltip
],
Union[X, Y, Color, Order, Size],
]


def _add_tooltip(chart: alt.Chart) -> alt.Chart:
chart.mark = {"type": chart.mark, "tooltip": True}
return chart
Copy link
Contributor

@joelostblom joelostblom Sep 9, 2024

Choose a reason for hiding this comment

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

Are marks guaranteed to be strings here? It seems like they are by default in polars, but I'm flagging just in case you want to do something like this to check that the mark is not a dict (which you might already have seen).

Copy link
Contributor

Choose a reason for hiding this comment

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

@joelostblom I'm pretty sure @MarcoGorelli has been peeking at that PR 😉

Maybe I'm being overly cautious here, but I'm not sure we've tested this thoroughly enough for it to be included in polars without any tests yet

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 both!

I've reworked this to create tooltip= explicitly, reckon this is more robust / robust-enough?

Copy link
Contributor

Choose a reason for hiding this comment

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

Looks good to me too



class DataFramePlot:
"""DataFrame.plot namespace."""

Expand All @@ -45,10 +45,9 @@ def __init__(self, df: DataFrame) -> None:

def bar(
self,
x: ChannelX | None = None,
y: ChannelY | None = None,
color: ChannelColor | None = None,
tooltip: ChannelTooltip | None = None,
x: X | None = None,
y: Y | None = None,
color: Color | None = None,
/,
**kwargs: Unpack[EncodeKwds],
) -> alt.Chart:
Expand Down Expand Up @@ -77,8 +76,6 @@ def bar(
Column with y-coordinates of bars.
color
Column to color bars by.
tooltip
Columns to show values of when hovering over bars with pointer.
**kwargs
Additional keyword arguments passed to Altair.
Comment on lines -80 to 81
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

note that this is a backwards-compatible change, as anyone passing tooltip will still have their code work due to **kwargs


Expand All @@ -102,17 +99,16 @@ def bar(
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(**encodings, **kwargs).interactive()
return _add_tooltip(
self._chart.mark_bar().encode(**encodings, **kwargs).interactive()
)

def line(
self,
x: ChannelX | None = None,
y: ChannelY | None = None,
color: ChannelColor | None = None,
order: ChannelOrder | None = None,
tooltip: ChannelTooltip | None = None,
x: X | None = None,
y: Y | None = None,
color: Color | None = None,
order: Order | None = None,
/,
**kwargs: Unpack[EncodeKwds],
) -> alt.Chart:
Expand Down Expand Up @@ -142,8 +138,6 @@ def line(
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 lines with pointer.
**kwargs
Additional keyword arguments passed to Altair.

Expand All @@ -168,17 +162,16 @@ def line(
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(**encodings, **kwargs).interactive()
return _add_tooltip(
self._chart.mark_line().encode(**encodings, **kwargs).interactive()
)

def point(
self,
x: ChannelX | None = None,
y: ChannelY | None = None,
color: ChannelColor | None = None,
size: ChannelSize | None = None,
tooltip: ChannelTooltip | None = None,
x: X | None = None,
y: Y | None = None,
color: Color | None = None,
size: Size | None = None,
/,
**kwargs: Unpack[EncodeKwds],
) -> alt.Chart:
Expand Down Expand Up @@ -209,8 +202,6 @@ def point(
Column to color points by.
size
Column which determines points' sizes.
tooltip
Columns to show values of when hovering over points with pointer.
**kwargs
Additional keyword arguments passed to Altair.

Expand All @@ -234,9 +225,7 @@ def point(
encodings["color"] = color
if size is not None:
encodings["size"] = size
if tooltip is not None:
encodings["tooltip"] = tooltip
return (
return _add_tooltip(
self._chart.mark_point()
.encode(
**encodings,
Expand All @@ -253,4 +242,4 @@ 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(**kwargs).interactive()
return lambda **kwargs: _add_tooltip(method().encode(**kwargs).interactive())
13 changes: 6 additions & 7 deletions py-polars/polars/series/plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from typing import TYPE_CHECKING, Callable

from polars.dataframe.plotting import _add_tooltip
from polars.dependencies import altair as alt

if TYPE_CHECKING:
Expand Down Expand Up @@ -62,7 +63,7 @@ def hist(
if self._series_name == "count()":
msg = "Cannot use `plot.hist` when Series name is `'count()'`"
raise ValueError(msg)
return (
return _add_tooltip(
alt.Chart(self._df)
.mark_bar()
.encode(x=alt.X(f"{self._series_name}:Q", bin=True), y="count()", **kwargs) # type: ignore[misc]
Expand Down Expand Up @@ -104,7 +105,7 @@ def kde(
if self._series_name == "density":
msg = "Cannot use `plot.kde` when Series name is `'density'`"
raise ValueError(msg)
return (
return _add_tooltip(
alt.Chart(self._df)
.transform_density(self._series_name, as_=[self._series_name, "density"])
.mark_area()
Expand Down Expand Up @@ -147,7 +148,7 @@ def line(
if self._series_name == "index":
msg = "Cannot call `plot.line` when Series name is 'index'"
raise ValueError(msg)
return (
return _add_tooltip(
alt.Chart(self._df.with_row_index())
.mark_line()
.encode(x="index", y=self._series_name, **kwargs) # type: ignore[misc]
Expand All @@ -165,8 +166,6 @@ 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: _add_tooltip(
method().encode(x="index", y=self._series_name, **kwargs).interactive()
)