altair icon indicating copy to clipboard operation
altair copied to clipboard

Having both X and Y axes' Scales respond to a selection interval

Open legolego opened this issue 3 years ago • 13 comments

Hello, Is there a way to get the Scale on Y-axis in the bottom chart to respond to the selection brush? I'd like to be able to pan around the top chart with a selection and see the zoomed-in results in the bottom chart. If I uncomment the alt.Y, then both the X and Y axes show Years and it's messed up. Is there a way to pass just an X or Y value in the 'brush' maybe? Thank you very much!

brush = alt.selection_interval(init={'x':[1950, 1970], 'y':[1500000, 2500000]}, encodings=['x', 'y'])
base = alt.Chart().mark_line().encode(
    x=alt.X('Year:Q', title=None),
    y='Deaths:Q',
    color='Entity:N'
)
  
alt.vconcat(    
    base.add_selection(brush).encode().properties(height=150, width=150),
    base.encode(
        alt.X('Year:Q', scale=alt.Scale(domain=brush)),
        #alt.Y('Deaths:Q', scale=alt.Scale(domain=brush)) # (un)commenting this line makes it work/fail only along the x-axis    
    ).properties(
    height=500, width=500
),
    data='https://vega.github.io/vega-datasets/data/disasters.csv'
)

image

legolego avatar Apr 11 '21 01:04 legolego

Is there a way for the selection/brush to only pass the 'x' or 'y' information into the domain?

legolego avatar Apr 13 '21 16:04 legolego

I am not sure if this functionality is provided by Vega-Lite, so it might be a good idea to ask on StackOverflow and tag it with Vega-Lite to see if anyone from that community knows.

joelostblom avatar Apr 13 '21 18:04 joelostblom

Posted here for future reference: https://stackoverflow.com/questions/67358979/having-both-x-and-y-axes-scales-in-altair-respond-to-a-selection-interval

legolego avatar May 02 '21 16:05 legolego

Posted there

mattijn avatar May 02 '21 19:05 mattijn

@legolego pretty late to the party, but for future reference I adapted your example with @mattijn 's Vega editor version and this worked in Python using Altair:

brush = alt.selection_interval(init={'x':[1950, 1970], 'y':[1500000, 2500000]}, encodings=['x', 'y'])
base = alt.Chart().mark_line().encode(
    x=alt.X('Year:Q', title=None),
    y='Deaths:Q',
    color='Entity:N'
)
  
alt.vconcat(    
    base.add_selection(brush).encode().properties(height=150, width=150),
    base.encode(
    ).properties(
        height=500, width=500
    ).transform_filter(brush),
    data='https://vega.github.io/vega-datasets/data/disasters.csv'
)

Would suggest to close this one :)

betaigeuze avatar Nov 30 '22 14:11 betaigeuze

@betaigeuze I tried that and it didn't work like I wanted... the effect I was looking for was like a magnifying glass in the small chart, with the zoomed in results in the large chart. Going back to @mattijn 's answer in vega, it might not be quite right either actually, maybe I didn't notice before, but his x and y-scales move in the zoomed in version, where I wanted to make them constant. Another example would be the zoom-in effect that ebay has (https://www.ebay.com/itm/125622850855) on their item pages. You drag a window around to inspect that zoomed-in section.

legolego avatar Dec 01 '22 06:12 legolego

@legolego I'm sorry to late, but did you find a python code that changes the chart to that area by enlarging the selected area in the chart? I really want to get help. Thanks.

hyerming23 avatar May 30 '24 12:05 hyerming23

@hyerming23 No, I haven't found a solution for this yet.

legolego avatar May 30 '24 14:05 legolego

@mattijn Hello. Could you share the function implemented in the vega-lite page you uploaded before in python code? This function enlarges the selected area in the chart of altair to each of the x and y axes. This is the function of the page below.

https://stackoverflow.com/questions/67358979/having-both-x-and-y-axes-scales-in-altair-respond-to-a-selection-interval

hyerming23 avatar May 31 '24 00:05 hyerming23

There are a few variants possible. I'm not sure which one is wished here. The SO example using altair syntax:

import altair as alt
from vega_datasets import data

cars = data.cars()

brush = alt.selection_interval(value={"x": [45, 100], "y": [30, 40]})

source = alt.Chart(cars).mark_point().encode(
    x='Horsepower',
    y='Miles_per_Gallon',
    color=alt.condition(brush, 'Cylinders:O', alt.value('lightgray'))
).add_params(brush)

detail = alt.Chart(cars).mark_point().encode(
    x=alt.X('Horsepower').scale(zero=False),
    y=alt.Y('Miles_per_Gallon').scale(zero=False),
    color=alt.condition(brush, 'Cylinders:O', alt.value('lightgray'))
).transform_filter(brush)

(source | detail).resolve_legend(color='independent')

image

A variant using only the filter on the x-axis:

import altair as alt
from vega_datasets import data

cars = data.cars()

brush = alt.selection_interval(value={"x": [45, 100]}, encodings=['x'])

source = alt.Chart(cars).mark_point().encode(
    x='Horsepower',
    y='Miles_per_Gallon',
    color=alt.condition(brush, 'Cylinders:O', alt.value('lightgray'))
).add_params(brush)

detail = alt.Chart(cars).mark_point().encode(
    x=alt.X('Horsepower').scale(zero=False),
    y=alt.Y('Miles_per_Gallon').scale(zero=False),
    color=alt.condition(brush, 'Cylinders:O', alt.value('lightgray'))
).transform_filter(brush)

(source | detail).resolve_legend(color='independent')

image

And to make it complete, a variant using filtering on the y-axis:

import altair as alt
from vega_datasets import data

cars = data.cars()

brush = alt.selection_interval(value={"Miles_per_Gallon": [30, 40]}, encodings=['y'])

source = alt.Chart(cars).mark_point().encode(
    x='Horsepower',
    y='Miles_per_Gallon',
    color=alt.condition(brush, 'Cylinders:O', alt.value('lightgray'))
).add_params(brush)

detail = alt.Chart(cars).mark_point().encode(
    x=alt.X('Horsepower').scale(zero=False),
    y=alt.Y('Miles_per_Gallon').scale(zero=False),
    color=alt.condition(brush, 'Cylinders:O', alt.value('lightgray'))
).transform_filter(brush)

(source | detail).resolve_legend(color='independent')

image

If you look careful to the alt.selection_interval() definitions then there are multiple differences in between these examples:

brush = alt.selection_interval(value={"x": [45, 100], "y": [30, 40]})
brush = alt.selection_interval(value={"x": [45, 100]}, encodings=['x'])
brush = alt.selection_interval(value={"Miles_per_Gallon": [30, 40]}, encodings=['y'])

mattijn avatar May 31 '24 14:05 mattijn

@mattijn Thank you very much for your kind explanation. In the above code, can I show the enlarged chart in the source right away, not the separate chart of source and detail? If possible, can I ask you to share sample code?

hyerming23 avatar Jun 04 '24 00:06 hyerming23

@hyerming23, maybe this is something that can help:

import altair as alt
from vega_datasets import data

cars = data.cars()

brush_empty = alt.selection_interval(clear="pointerup")
filter_on_up = alt.selection_interval(on="[pointerdown, type_anything] > pointerup", mark={"fillOpacity":0 , "strokeOpacity": 0, "cursor":"auto"}, clear="dblclick")

alt.Chart(cars).mark_point().encode(
    x=alt.X('Horsepower').scale(zero=False),
    y=alt.Y('Miles_per_Gallon').scale(zero=False),
    color=alt.condition(brush_empty, 'Cylinders:O', alt.value('lightgray'))
).add_params(filter_on_up, brush_empty).transform_filter(filter_on_up)

apply_interval_on_pointerup

I added two interval selections, The first one is only changing the color of the selection (brush_empty) and the second one actually filters the data (filter_on_up). I was trying a few things for the on operator (vl-docs) which mentions that:

For interval selections, the event stream must specify a start and end.

And the related vega-docs on interval streaming mention this:

Between Filters To capture events that occur between two other events, use a bracket notation.

[startSelector, stopSelector] > selector

Between filters are particularly useful to capture streams of drag events:

[rect:mousedown, window:mouseup] > window:mousemove

This example initiates a drag upon mousedown on a rect mark, then tracks the drag using events on the browser window. Using the window as the event source lets the drag continue if the mouse pointer leaves the initial rect mark or the view component.

I made a mistake and found out that it actually works like this: "[pointerdown, type_anything] > pointerup", a non-existing stopSelector applies the interval only after the pointerup is triggered...

It would be nice to actually understand why and how this works, but maybe you or someone else can tell more about this!

mattijn avatar Jun 05 '24 12:06 mattijn

@mattijn Thank you very much😍 You helped me a lot.

hyerming23 avatar Jun 07 '24 00:06 hyerming23