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

add a method for binding_color? #2760

Open
mattijn opened this issue Dec 17, 2022 · 11 comments
Open

add a method for binding_color? #2760

mattijn opened this issue Dec 17, 2022 · 11 comments
Labels

Comments

@mattijn
Copy link
Contributor

mattijn commented Dec 17, 2022

I saw this tweet https://twitter.com/WoottonDylan/status/1603799316270718977?s=20&t=TA76W5lTMJvZmfPbEXfTMA.

This can be reproduced in altair with:

import altair as alt
from vega_datasets import data

source = data.seattle_weather()

color_rain = alt.param(value="#317bb4", bind=alt.binding(input='color', name='color rain'))
color_sun = alt.param(value="#ffb54d", bind=alt.binding(input='color', name='color sun'))
color_fog = alt.param(value="#adadad", bind=alt.binding(input='color', name='color fog'))
color_drizzle = alt.param(value="#5d7583", bind=alt.binding(input='color', name='color drizzle'))
color_snow = alt.param(value="#90edf4", bind=alt.binding(input='color', name='color snow'))

scale = alt.Scale(domain=['sun', 'fog', 'drizzle', 'rain', 'snow'],
                  range=[color_sun, color_fog, color_drizzle, color_rain, color_snow])
color = alt.Color('weather:N', scale=scale)

# Top panel is scatter plot of temperature vs time
points = alt.Chart().mark_point().encode(
    alt.X('monthdate(date):T', title='Date'),
    alt.Y('temp_max:Q',
        title='Maximum Daily Temperature (C)',
        scale=alt.Scale(domain=[-5, 40])
    ),
    color=color,
    size=alt.Size('precipitation:Q', scale=alt.Scale(range=[5, 200]))
).properties(
    width=550,
    height=300
)

# Bottom panel is a bar chart of weather type
bars = alt.Chart().mark_bar().encode(
    x='count()',
    y='weather:N',
    color=color,
).properties(
    width=550,
)

alt.vconcat(
    points,
    bars,
    data=source,
    title="Seattle Weather: 2012-2015"
).add_params(color_sun, color_fog, color_drizzle, color_rain, color_snow)

image

Would it be useful to have a binding_color utility around here https://github.com/altair-viz/altair/blob/master/altair/vegalite/v5/api.py#L431?

I could not find a specific core.BindColor class, so it could be something as such:

def binding_color(**kwargs):
    """A color picker binding"""
    return core.BindInput(input='color', **kwargs)

not sure if binding_color(name='color rain') is a true improvement over alt.binding(input='color', name='color rain') though.

@joelostblom
Copy link
Contributor

joelostblom commented Dec 18, 2022

Very cool! I saw that there is an open PR on this in the Vega-Lite repo as well vega/vega-lite#8601. I agree with you that a special function just for this does not seem to add that much, but we should definitely add an example to the gallery or main documentation to make it more discoverable.

@joelostblom
Copy link
Contributor

A note here that if we add binding_color, we should consider deprecating binding similarly to what we did with selections in #2923

@mattijn
Copy link
Contributor Author

mattijn commented Feb 25, 2023

Not exactly sure since there are likely more options that could be explored using the binding function, see: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input

@joelostblom
Copy link
Contributor

joelostblom commented Mar 21, 2023

You're right, there are many cool options to explore. I just discovered that it it possible to build a search box to filter/highlight the data directly in altair/vega-lite! I think we should add the color and search binding examples to the gallery, what do you think? Maybe even adding a section in the interactive page in the docs (although that page is becoming long now, maybe this could be a tutorial/showcase)

import altair as alt
from vega_datasets import data


search_box = alt.param(
    value='',
    bind=alt.binding(
        input='search',
        placeholder="Car model",
        name='Search ',
    )
)
alt.Chart(data.cars.url).mark_point(size=60).encode(
    x='Horsepower:Q',
    y='Miles_per_Gallon:Q',
    tooltip='Name:N',
    opacity=alt.condition(
        alt.expr.test(alt.expr.regexp(search_box, 'i'), alt.datum.Name),
#        alt.expr.indexof(alt.datum.Name, search_box) != -1,  # this also works, but is not as powerful as regex
        alt.value(0.8),
        alt.value(0.1)
    )
).add_params(
    search_box
)
altair-search-2023-03-20_19.10.01.1.mp4

You can also search for regexes, e.g. mazda|ford. Inspiration from https://vega.github.io/vega/examples/job-voyager/ (it would be neat to also include an example of the "all" option in the altair docs, since it is not straightforward but useful)

@mattijn
Copy link
Contributor Author

mattijn commented Mar 21, 2023

Really nice. Yes. An example for this would be great. Maybe a new section named 'binding' and then an example per bind type?

@joelostblom
Copy link
Contributor

Yeah, or maybe I can fit them into our current binding and expression sections... I might need to re-organize a bit, but that's ok since I am not sure the data-driven and logic-driven organization I suggested before really makes sense anyways. I'll see if I can try something in the next few days.

@Sann5
Copy link

Sann5 commented Feb 23, 2024

Hey @joelostblom, thank you for showcasing such a useful feature! Would you mind showing an example where one can do filtering instead of highlighting data, with a regex expression? I've been trying several things with no luck.

@mattijn
Copy link
Contributor Author

mattijn commented Feb 23, 2024

Instead of a condition for opacity highlighting you should include a filter transform.

import altair as alt
from vega_datasets import data

search_box = alt.param(
    value='',
    bind=alt.binding(
        input='search',
        placeholder="Car model",
        name='Search ',
    )
)
alt.Chart(data.cars.url).mark_point(size=60).encode(
    x='Horsepower:Q',
    y='Miles_per_Gallon:Q',
    tooltip='Name:N',
).add_params(
    search_box
).transform_filter(alt.expr.test(alt.expr.regexp(search_box, 'i'), alt.datum.Name))

image

@Sann5
Copy link

Sann5 commented Feb 23, 2024

Thank you for the answer @mattijn 🙌🏼 ! Do you know if it is possible to only update the graph when pressing enter, rather than as one types?

@mattijn
Copy link
Contributor Author

mattijn commented Feb 23, 2024

You would need to make use of an interactive JupyterChart object (docs). As such:

import altair as alt
from vega_datasets import data
import ipywidgets

# define a text and button widget
text = ipywidgets.Text(placeholder='enter a partial match to search', description='search')
button = ipywidgets.Button(description='click to search')

# define an altair parameter named SEARCH
param_search = alt.param(name='SEARCH')

# build altair chart
chart = alt.Chart(data.cars.url).mark_point(size=60).encode(
    x='Horsepower:Q',
    y='Miles_per_Gallon:Q',
    tooltip='Name:N',
).add_params(
    param_search
).transform_filter(alt.expr.test(alt.expr.regexp(param_search, 'i'), alt.datum.Name))

# create a JupyterChart object from the altair chart
jchart = alt.JupyterChart(chart)

# define a function to handle button click events
# on button click: update the SEARCH param using the input of the text widget
def on_button_clicked(b):
    jchart.params.SEARCH = text.value

# register the function to the button
button.on_click(on_button_clicked)

# layout the text and button widget horizontally and the JupyerChart object below it
ipywidgets.VBox([
    ipywidgets.HBox([text, button]),
    jchart
])

text_button_jchart

@dangotbanned
Copy link
Member

I've linked #3403 as a parent, since it looks like (despite the title of the issue) the conclusion was to improve the docs.

Specific to the original proposal to add alt.binding_color, I see another option that would also tie in with #2918

diff altair/vegalite/v5/api.py

diff --git a/altair/vegalite/v5/api.py b/altair/vegalite/v5/api.py
index d836615f..71d9f512 100644
--- a/altair/vegalite/v5/api.py
+++ b/altair/vegalite/v5/api.py
@@ -103,7 +103,9 @@ if TYPE_CHECKING:
         AggregateOp,
         AnyMark,
         BindCheckbox,
+        BindDirect,
         Binding,
+        BindInput,
         BindRadioSelect,
         BindRange,
         BinParams,
@@ -1727,6 +1729,52 @@ def selection_point(
     )
 
 
+class bind:
+    @utils.use_signature(core.BindInput)
+    def __new__(cls, **kwds: Any) -> BindInput:
+        return core.BindInput(**kwds)
+
+    @staticmethod
+    @utils.use_signature(core.BindCheckbox)
+    def checkbox(**kwds: Any) -> BindCheckbox:
+        return core.BindCheckbox(input="checkbox", **kwds)
+
+    @staticmethod
+    @utils.use_signature(core.BindDirect)
+    def direct(**kwds: Any) -> BindDirect:
+        return core.BindDirect(**kwds)
+
+    @staticmethod
+    @utils.use_signature(core.BindInput)
+    def input(**kwds: Any) -> BindInput:
+        return core.BindInput(**kwds)
+
+    @staticmethod
+    @utils.use_signature(core.BindRadioSelect)
+    def radio(**kwds: Any) -> BindRadioSelect:
+        return core.BindRadioSelect(input="radio", **kwds)
+
+    @staticmethod
+    @utils.use_signature(core.BindRange)
+    def range(**kwds: Any) -> BindRange:
+        return core.BindRange(input="range", **kwds)
+
+    @staticmethod
+    @utils.use_signature(core.BindRadioSelect)
+    def select(**kwds: Any) -> BindRadioSelect:
+        return core.BindRadioSelect(input="select", **kwds)
+
+    @staticmethod
+    @utils.use_signature(core.BindInput)
+    def color(**kwds: Any) -> core.BindInput:
+        return core.BindInput(input="color", **kwds)
+
+    @staticmethod
+    @utils.use_signature(core.BindInput)
+    def search(**kwds: Any) -> core.BindInput:
+        return core.BindInput(input="search", **kwds)
+
+
 @utils.deprecated(version="5.0.0", alternative="selection_point")
 def selection_multi(**kwargs: Any) -> Parameter:
     """'selection_multi' is deprecated.  Use 'selection_point'."""

Benefits

  • More options, specifically those that don't map to a single class
  • Smaller footprint on the overall api.py namespace
    • After a deprecation period, all would be behind a single entry point:
    • alt.bind.color, alt.bind.checkbox, etc
  • Slightly shorter than existing function names
    • alt.bind.checkbox
    • alt.binding_checkbox
  • All of the schema classes are already named Bind... - so there isn't anything new to learn

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants