vega / altair

Declarative statistical visualization library for Python
https://altair-viz.github.io/
BSD 3-Clause "New" or "Revised" License
9.25k stars 793 forks source link

add a method for binding_color? #2760

Open mattijn opened 1 year ago

mattijn commented 1 year ago

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 commented 1 year ago

Very cool! I saw that there is an open PR on this in the Vega-Lite repo as well https://github.com/vega/vega-lite/pull/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 commented 1 year ago

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

mattijn commented 1 year ago

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 commented 1 year ago

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
)

https://user-images.githubusercontent.com/4560057/226503462-4ff3df58-cbb4-41b9-bf0c-e12b09d122e1.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 commented 1 year ago

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

joelostblom commented 1 year ago

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 commented 7 months ago

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 commented 7 months ago

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 commented 7 months ago

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 commented 7 months ago

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