multiple column/row facet wrap in altair
Asked Answered
E

6

26

In ggplot2, it's easy to create a faceted plot with facets that span both rows and columns. Is there a "slick" way to do this in altair? facet documentation

It's possible to have facets plot in a single column,

import altair as alt
from vega_datasets import data
iris = data.iris

chart = alt.Chart(iris).mark_point().encode(
    x='petalLength:Q',
    y='petalWidth:Q',
    color='species:N'
).properties(
    width=180,
    height=180
).facet(
    row='species:N'
)

and in a single row,

chart = alt.Chart(iris).mark_point().encode(
    x='petalLength:Q',
    y='petalWidth:Q',
    color='species:N'
).properties(
    width=180,
    height=180
).facet(
    column='species:N'
)

but often, I just want to plot them in a grid using more than one column/row, i.e. those that line up in a single column/row don't mean anything in particular.

For example, see facet_wrap from ggplot2: http://www.cookbook-r.com/Graphs/Facets_(ggplot2)/#facetwrap

Eyla answered 3/5, 2018 at 21:18 Comment(0)
C
26

In Altair version 3.1 or newer (released June 2019), wrapped facets are supported directly within the Altair API. Modifying your iris example, you can wrap your facets at two columns like this:

import altair as alt
from vega_datasets import data
iris = data.iris()

alt.Chart(iris).mark_point().encode(
    x='petalLength:Q',
    y='petalWidth:Q',
    color='species:N'
).properties(
    width=180,
    height=180
).facet(
    facet='species:N',
    columns=2
)

enter image description here

Alternatively, the same chart can be specified with the facet as an encoding:

alt.Chart(iris).mark_point().encode(
    x='petalLength:Q',
    y='petalWidth:Q',
    color='species:N',
    facet=alt.Facet('species:N', columns=2)
).properties(
    width=180,
    height=180,
)

The columns argument can be similarly specified for concatenated charts in alt.concat() and repeated charts alt.Chart.repeat().

Concord answered 11/6, 2019 at 5:27 Comment(3)
Thanks, clear example. I had alt.layer(line, band, data=df).facet(column='series') which produced 8 facets, I wanted 4 wide, so tried alt.layer(line, band, data=df).facet(column='series', columns=4) which doesn't make sense in retrospect. The column is a way of making the layout dynamic/data-driven, and columns is a static setting, using both column and columns together is ambiguous, but not an error. columns=4 is ignored and it displays 8 charts horizontally. alt.layer(line, band, data=df).facet(facet='series', columns=4) gave me the 2 rows of 4 charts I wanted.Nonobedience
this is very neat! How do you deal with the bottom right (empty) graph having visible x-axis? In reality the top right should have it visible.Rafi
Re: my previous comment. Vega has an open issue github.com/vega/vega-lite/issues/7194Rafi
T
8

You can do this by specifying .repeat() and the row and column list of variables. This is closer to ggplot's facet_grid() than facet_wrap() but the API is very elegant. (See discussion here.) The API is here

iris = data.iris()

alt.Chart(iris).mark_circle().encode(
    alt.X(alt.repeat("column"), type='quantitative'),
    alt.Y(alt.repeat("row"), type='quantitative'),
    color='species:N'
).properties(
    width=250,
    height=250
).repeat(
    row=['petalLength', 'petalWidth'],
    column=['sepalLength', 'sepalWidth']
).interactive()

Which produces:

enter image description here

Note that the entire set is interactive in tandem (zoom-in, zoom-out).

Be sure to check out RepeatedCharts and FacetedCharts in the Documentation.

Creating a facet_wrap() style grid of plots

If you want a ribbon of charts laid out one after another (not necessarily mapping a column or row to variables in your data frame) you can do that by wrapping a combination of hconcat() and vconcat() over a list of Altair plots.

I am sure there are more elegant ways, but this is how I did it.

Logic used in the code below:

  1. First, create a base Altair chart
  2. Use transform_filter() to filter your data into multiple subplots
  3. Decide on the number of plots in one row and slice up that list
  4. Loop through the list of lists, laying down one row at a time.

-

import altair as alt
from vega_datasets import data
from altair.expr import datum

iris = data.iris()

base = alt.Chart(iris).mark_point().encode(
    x='petalLength:Q',
    y='petalWidth:Q',
    color='species:N'
).properties(
    width=60,
    height=60
)

#create a list of subplots
subplts = []
for pw in iris['petalWidth'].unique():
    subplts.append(base.transform_filter(datum.petalWidth == pw))


def facet_wrap(subplts, plots_per_row):
    rows = [subplts[i:i+plots_per_row] for i in range(0, len(subplts), plots_per_row)]
    compound_chart = alt.hconcat()
    for r in rows:
        rowplot = alt.vconcat() #start a new row
        for item in r:
            rowplot |= item #add suplot to current row as a new column
        compound_chart &= rowplot # add the entire row of plots as a new row
    return compound_chart


compound_chart = facet_wrap(subplts, plots_per_row=6)    
compound_chart

to produce:

enter image description here

Tychonn answered 4/5, 2018 at 2:57 Comment(1)
Thanks! I wanted add a static overlay and a title to each subplot and figured out I needed subplts.append(base.transform_filter(datum ...) + static_plot).properties(title=...).Cadi
T
3

Here's a general solution that has a spot to add layers. The DataFrame in this case has three columns and is in long form.

numcols=3 # specify the number of columns you want 
all_categories=df['Category_Column'].unique() # array of strings to use as your filters and titles

rows=alt.vconcat(data=df)
numrows=int(np.ceil(len(all_categories) / numcols))
pointer=0
for _ in range(numrows):

  row=all_categories[pointer:pointer+numcols]
  cols=alt.hconcat()

  for a_chart in row:

     # add your layers here
     # line chart
     line=alt.Chart().mark_line(point=True).encode(
        x='variable',
        y='value'
     ).transform_filter(datum.Category_Column == a_chart).properties(
        title=a_chart, height=200, width=200)

     # text labels
     text=alt.Chart().mark_text().encode(
        x='variable', 
        y='value'
     ).transform_filter(datum.Category_Column == a_chart)

     both = line + text
     cols |= both

  rows &= cols
  pointer += numcols

rows

enter image description here

Tensity answered 12/9, 2018 at 16:38 Comment(0)
D
1

Starting from Ram's answer, and using a more functional approach, you could also try:

import altair as alt
from vega_datasets import data
from altair.expr import datum

iris = data.iris()

base = alt.Chart(iris).mark_point().encode(
    x='petalLength:Q',
    y='petalWidth:Q',
    color='species:N'
)

# chart factory
def make_chart(base_chart, pw, options):
    title = 'Petal Width {:.2f}'.format(pw)
    chart = base_chart\
      .transform_filter(datum.petalWidth == pw)\
      .properties(width=options['width'], height=options['height'], title=title)
    return chart

# create all charts
options = {'width': 50, 'height': 60}
charts = [make_chart(base, pw, options) for pw in sorted(iris['petalWidth'].unique())]

# make a single row
def make_hcc(row_of_charts):
    hconcat = [chart for chart in row_of_charts]
    hcc = alt.HConcatChart(hconcat=hconcat)
    return hcc

# take an array of charts and produce a facet grid
def facet_wrap(charts, charts_per_row):
    rows_of_charts = [
        charts[i:i+charts_per_row] 
        for i in range(0, len(charts), charts_per_row)]        
    vconcat = [make_hcc(r) for r in rows_of_charts]    
    vcc = alt.VConcatChart(vconcat=vconcat)\
      .configure_axisX(grid=True)\
      .configure_axisY(grid=True)
    return vcc

# assemble the facet grid
compound_chart = facet_wrap(charts, charts_per_row=6)
compound_chart.properties(title='My Facet grid')

Facet grid of 22 charts, 6 per row

This way it should be easy to tweak the code and pass some configuration options to all of your plots (e.g. show/hide ticks, set the same bottom/top limits for all the plots, etc).

Darkness answered 29/8, 2018 at 13:15 Comment(0)
S
1

Do not use column or row in repeat, but repeat as follow:

import altair as alt
from vega_datasets import data

cars = data.cars.url

alt.Chart(cars, width=200, height=150).mark_bar().encode(
    x=alt.X(alt.repeat('repeat'), type='quantitative', bin=alt.Bin(maxbins=20)),
    y='count()'
).repeat(
    repeat=["Horsepower", "Miles_per_Gallon", "Acceleration", "Displacement"], 
    columns=2
)

enter image description here

Salubrious answered 5/3, 2021 at 20:16 Comment(0)
H
0

I found that doing a concatenation of length greater than two in either direction caused the data to become distorted and fall out of the window. I solved this by recursively breaking up the subplot array into quadrants and doing alternating row and column concatenations. If you don't have this problem, good for you: you can use one of the simpler implementations already posted. But, if you do, I hope this helps.

def facet_wrap(subplots, plots_per_row):
    # base cases
    if len(subplots) == 0 or plots_per_row == 0:
        return None
    if len(subplots) == 1:
        return subplots[0]

    # split subplots list into quadrants
    # we always fill top and left first
    quadrants = [[], [], [], []] # tl, tr, bl, br
    for subplot_index, subplot in enumerate(subplots):
        right_half = (subplot_index % plots_per_row) >= plots_per_row // 2
        lower_half = subplot_index >= len(subplots) / 2
        quadrants[2 * lower_half + right_half].append(subplot)

    # recurse on each quadrant
    # we want a single chart or None in place of each quadrant
    m = plots_per_row % 2 # if plots_per_row is odd then we need to split it unevenly
    quadplots = [
        facet_wrap(q, plots_per_row // 2 + m * (0 == (i % 2))) \
        for i, q in enumerate(quadrants)
    ]

    # join the quadrants
    rows = [quadplots[:2], quadplots[2:]]
    colplot = alt.hconcat()
    for row in rows:
        rowplot = alt.vconcat()
        for item in row:
            if item != None:
                rowplot = rowplot | item
        colplot &= rowplot
    return colplot
Hyatt answered 24/2, 2019 at 23:58 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.