vega / altair

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

Linking the x-axis of multiple charts when zooming #1481

Open dbk123 opened 5 years ago

dbk123 commented 5 years ago

Is there any way to link the x-axis of multiple charts so that all they zoom together and maintain the same x-axis range when one plot is zoomed?

Thanks!

jakevdp commented 5 years ago

Yes, but you'll need to learn a bit about binding selections

When you do a chart like this:

import altair as alt
from vega_datasets import data

cars = data.cars()

alt.Chart(cars).mark_point().encode(
    x='Horsepower',
    y='Miles_per_Gallon',
    color='Origin'
).interactive()

The interactive() method is a shorthand for this:

alt.Chart(cars).mark_point().encode(
    x='Horsepower',
    y='Miles_per_Gallon'
    color='Origin'
).add_selection(
    alt.selection_interval(bind='scales')
)

Each time you call interactive(), it will create a new (and independent) selection with a binding to scales. If you would like two panels in a chart to share the same selection binding, then you can specify it manually and attach the same selection object to each chart, for example:

resize = alt.selection_interval(bind='scales')

chart1 = alt.Chart(cars).mark_point().encode(
    x='Horsepower',
    y='Miles_per_Gallon',
    color='Origin'
).add_selection(
    resize
)

chart2 = alt.Chart(cars).mark_point().encode(
    x='Horsepower',
    y='Acceleration',
    color='Origin'
).add_selection(
    resize
)

alt.concat(chart1, chart2)

Because the selection is attached to both charts, changing it will change the scales of both charts.

dbk123 commented 5 years ago

Jake,

Thank you for the prompt and detailed feedback!!!!

-Dave

jakevdp commented 5 years ago

I'm going to reopen this as a reminder to myself to add this info to the docs.

dbk123 commented 5 years ago

Got it - sorry. (That will be very helpful thing to include!)

YashaPushak commented 5 years ago

Is it possible to have this work with layered charts as well?

Essentially I have:

backgroundChartOne = alt.Chart(df)...
foregroundChartOne = alt.Chart(df)...
backgroundChartTwo = alt.Chart(df)...
foregroundChartTwo = alt.Chart(df)...

chartOne = backgroundChartOne + foregroundChartOne
chartTwo = backgroundChartTwo + foregroundChartTwo

chartOne | chartTwo

I would like to have an interactive x-axis that is linked between chartOne and chartTwo. However, I haven't been able to get this to work yet, I keep getting a Duplicate signal name: "selector067_tuple" error.

I have tried, for example:

backgroundChartOne = alt.Chart(df)...
foregroundChartOne = alt.Chart(df)...
backgroundChartTwo = alt.Chart(df)...
foregroundChartTwo = alt.Chart(df)...

resize = alt.selection_interval(bind='scales')

backgroundChartOne = backgroundChartOne.add_selection(resize)
foregroundChartOne = foregroundChartOne.add_selection(resize)
backgroundChartTwo = backgroundChartTwo.add_selection(resize)
foregroundChartTwo = foregroundChartTwo.add_selection(resize)

chartOne = backgroundChartOne + foregroundChartOne
chartTwo = backgroundChartTwo + foregroundChartTwo

chartOne | chartTwo

and

backgroundChartOne = alt.Chart(df)...
foregroundChartOne = alt.Chart(df)...
backgroundChartTwo = alt.Chart(df)...
foregroundChartTwo = alt.Chart(df)...

resize = alt.selection_interval(bind='scales')

chartOne = backgroundChartOne + foregroundChartOne
chartTwo = backgroundChartTwo + foregroundChartTwo

chartOne = chartOne.add_selection(resize)
chartTwo = chartTwo.add_selection(resize)

chartOne | chartTwo

and neither of them seem to work.

jakevdp commented 5 years ago

You cannot bind the same selection object to two charts in a layer (this leads to duplicate signal names). Try binding the selection to only one of the subcharts in each layer.

jakevdp commented 5 years ago

For example, like this:

import altair as alt
import pandas as pd

df = pd.DataFrame({
    'x': range(5),
    'y': [1, 3, 2, 4, 0]
})

zoom = alt.selection_interval(bind='scales')

layer1 = alt.Chart(df).mark_line().encode(x='x', y='y').add_selection(zoom)
layer2 = alt.Chart(df).mark_point().encode(x='x', y='y')

chartOne = layer1 + layer2
YashaPushak commented 5 years ago

Perfect, thank you so much!

mycarta commented 3 years ago

Yes, but you'll need to learn a bit about binding selections

When you do a chart like this:

import altair as alt
from vega_datasets import data

cars = data.cars()

alt.Chart(cars).mark_point().encode(
    x='Horsepower',
    y='Miles_per_Gallon',
    color='Origin'
).interactive()

The interactive() method is a shorthand for this:

alt.Chart(cars).mark_point().encode(
    x='Horsepower',
    y='Miles_per_Gallon'
    color='Origin'
).add_selection(
    alt.selection_interval(bind='scales')
)

Each time you call interactive(), it will create a new (and independent) selection with a binding to scales. If you would like two panels in a chart to share the same selection binding, then you can specify it manually and attach the same selection object to each chart, for example:

resize = alt.selection_interval(bind='scales')

chart1 = alt.Chart(cars).mark_point().encode(
    x='Horsepower',
    y='Miles_per_Gallon',
    color='Origin'
).add_selection(
    resize
)

chart2 = alt.Chart(cars).mark_point().encode(
    x='Horsepower',
    y='Acceleration',
    color='Origin'
).add_selection(
    resize
)

alt.concat(chart1, chart2)

Because the selection is attached to both charts, changing it will change the scales of both charts.

@jakevdp - is it possible to link both x and y axes so they are both synchronized on zooming?

jakevdp commented 3 years ago

Yes, the last code snippet you quoted should do this.

kennydataml commented 3 years ago
zoom = alt.selection_interval(bind='scales')
        selector = alt.selection_single(
            empty='all',
            fields=['ticker']
        )

base = alt.Chart(df).transform_filter(
    # slider_selection
    (alt.datum.date2 >= select_range_start.date) & (
        alt.datum.date2 <= select_range_end.date)
).add_selection(
    selector,
    select_range_start,
    select_range_end
)

bars = base.mark_bar().encode(
    # https://stackoverflow.com/questions/52877697/order-bar-chart-in-altair
    x=alt.X('ticker',
            sort=alt.EncodingSortField(
                field="ticker", op="count", order='descending'),
            axis=alt.Axis(title='Stock Tickers')
            ),
    y=alt.Y("count(id)",
            axis=alt.Axis(title='Number of Mentions'),
            # scale=alt.Scale(zero=False)
            ),
    color=alt.condition(selector, 'id:O', alt.value(
        'lightgray'), legend=None),
).properties(
    width=1400,
    height=600
).add_selection(
    zoom
)

The x axis is just stock tickers. they y axis is count. I want to zoom in the bars that are really small. ie - the bars would get taller as I zoom-in on just the y-axis Is this even supported? seems like zoom only works if both x and y axis are not string.

EDIT: used a slider, selection_single, transform_aggregate, and transform_filter to achieve what I wanted

jakevdp commented 3 years ago

Vega-lite currently does not support selections on aggregated fields; see https://github.com/vega/vega-lite/issues/5308. You're using a count aggregate, which makes the zoom selection fail.