Multi-View Composition

When visualizing a number of different data fields, we might be tempted to use as many visual encoding channels as we can: x, y, color, size, shape, and so on. However, as the number of encoding channels increases, a chart can rapidly become cluttered and difficult to read. An alternative to “over-loading” a single chart is to instead compose multiple charts in a way that facilitates rapid comparisons.

In this notebook, we will examine a variety of operations for multi-view composition:

We’ll then look at how these operations form a view composition algebra, in which the operations can be combined to build a variety of complex multi-view displays.


Weather Data

We will be visualizing weather statistics for the U.S. cities of Seattle and New York. Let’s load the dataset and peek at the rows:

const weather = vega_datasets['weather.csv']()
Inputs.table(weather)

We will create multi-view displays to examine weather within and across the cities.


Layer

One of the most common ways of combining multiple charts is to layer marks on top of each other. If the underlying scale domains are compatible, we can merge them to form shared axes. If either of the x or y encodings is not compatible, we might instead create a dual-axis chart, which overlays marks using separate scales and axes.

Shared Axes

Let’s start by plotting the minimum and maximum average temperatures per month:

render({
  mark: { type: 'area' },
  data: { values: weather },
  encoding: {
    x: { timeUnit: 'month', field: 'date' },
    y: { aggregate: 'average', field: 'temp_max' },
    y2: { aggregate: 'average', field: 'temp_min' }
  }
})

The plot shows us temperature ranges for each month over the entirety of our data. However, this is pretty misleading as it aggregates the measurements for both Seattle and New York!

Let’s subdivide the data by location using a color encoding, while also adjusting the mark opacity to accommodate overlapping areas:

render({
  mark: { type: 'area', opacity: 0.3 },
  data: { values: weather },
  encoding: {
    x: { timeUnit: 'month', field: 'date' },
    y: { aggregate: 'average', field: 'temp_max' },
    y2: { aggregate: 'average', field: 'temp_min' },
    color: { field: 'location', type: 'N' }
  }
})

We can see that Seattle is more temperate: warmer in the winter, and cooler in the summer.

In this case we’ve created a layered chart without any special features by simply subdividing the area marks by color. While the chart above shows us the temperature ranges, we might also want to emphasize the middle of the range.

Let’s create a line chart showing the average temperature midpoint. We’ll use a calculate transform to compute the midpoints between the minimum and maximum daily temperatures:

render({
  mark: { type: 'line' },
  data: { values: weather },
  transform: [
    { calculate: '(datum.temp_min + datum.temp_max) / 2', as: 'temp_mid' }
  ],
  encoding: {
    x: { timeUnit: 'month', field: 'date' },
    y: { aggregate: 'average', field: 'temp_mid' },
    color: { field: 'location', type: 'N' }
  }
})

We’d now like to combine these charts by layering the midpoint lines over the range areas. Using the layer directive, we can specify that we want a new layered chart in which chart1 is the first layer and chart2 is a second layer drawn on top:

const tempMinMax = {
  mark: { type: 'area', opacity: 0.3 },
  data: { values: weather },
  encoding: {
    x: { timeUnit: 'month', field: 'date' },
    y: { aggregate: 'average', field: 'temp_max' },
    y2: { aggregate: 'average', field: 'temp_min' },
    color: { field: 'location', type: 'N' }
  }
};

const tempMid = {
  mark: { type: 'line' },
  data: { values: weather },
  transform: [
    { calculate: '(datum.temp_min + datum.temp_max) / 2', as: 'temp_mid' }
  ],
  encoding: {
    x: { timeUnit: 'month', field: 'date' },
    y: { aggregate: 'average', field: 'temp_mid' },
    color: { field: 'location', type: 'N' }
  }
}

render({ layer: [ tempMinMax, tempMid ] })

Now we have a multi-layer plot! However, the y-axis title (though informative) has become a bit long and unruly…

Let’s customize our axes to clean up the plot. If we set a custom axis title within one of the layers, it will automatically be used as a shared axis title for all the layers:

const tempMinMax = {
  mark: { type: 'area', opacity: 0.3 },
  data: { values: weather },
  encoding: {
    x: {
      timeUnit: 'month', field: 'date',
      axis: { format: '%b' },
      title: null
    },
    y: {
      aggregate: 'average', field: 'temp_max',
      title: 'Avg. Temperature °C'
    },
    y2: { aggregate: 'average', field: 'temp_min' },
    color: { field: 'location', type: 'N' }
  }
};

const tempMid = {
  mark: { type: 'line' },
  data: { values: weather },
  transform: [
    { calculate: '(datum.temp_min + datum.temp_max) / 2', as: 'temp_mid' }
  ],
  encoding: {
    x: { timeUnit: 'month', field: 'date' },
    y: { aggregate: 'average', field: 'temp_mid' },
    color: { field: 'location', type: 'N' }
  }
};

render({ layer: [ tempMinMax, tempMid ] })

What happens if both layers have custom axis titles? Modify the code above to find out…

When creating multiple views, we might find ourselves redundantly specifying the same input data for multiple marks. If we want to, we can move a shared data definition to the layer-level for more compact specifications, like so:

const tempMinMax = {
  mark: { type: 'area', opacity: 0.3 },
  encoding: {
    x: {
      timeUnit: 'month', field: 'date',
      axis: { format: '%b' },
      title: null
    },
    y: {
      aggregate: 'average', field: 'temp_max',
      title: 'Avg. Temperature °C'
    },
    y2: { aggregate: 'average', field: 'temp_min' },
    color: { field: 'location', type: 'N' }
  }
};

const tempMid = {
  mark: { type: 'line' },
  encoding: {
    x: { timeUnit: 'month', field: 'date' },
    y: { aggregate: 'average', field: 'temp_mid' },
    color: { field: 'location', type: 'N' }
  }
};

render({
  layer: [ tempMinMax, tempMid ],
  data: { values: weather },
  transform: [
    { calculate: '(datum.temp_min + datum.temp_max) / 2', as: 'temp_mid' }
  ]
})

Note that the order of inputs to a layer matters, as subsequent layers will be drawn on top of earlier layers. Try swapping the order of the charts in the cells above. What happens? (Hint: look closely at the color of the line marks.)

Dual-Axis Charts

Seattle has a reputation as a rainy city. Is that deserved?

Let’s look at precipitation alongside temperature to learn more. First let’s create a base plot that shows average monthly precipitation in Seattle:

render({
  mark: { type: 'line', interpolate: 'monotone', stroke: 'grey' },
  data: { values: weather },
   transform: [
    { filter: 'datum.location == "Seattle"' }
  ],
  encoding: {
    x: { timeUnit: 'month', field: 'date', title: null },
    y: { aggregate: 'average', field: 'precipitation', title: 'Precipitation' }
  }
})

To facilitate comparison with the temperature data, let’s create a new layered chart. Here’s what happens if we try to layer the charts as we did earlier:

const tempMinMax = {
  mark: { type: 'area', opacity: 0.3 },
  encoding: {
    x: {
      timeUnit: 'month', field: 'date',
      axis: { format: '%b' },
      title: null
    },
    y: {
      aggregate: 'average', field: 'temp_max',
      title: 'Avg. Temperature °C'
    },
    y2: { aggregate: 'average', field: 'temp_min' }
  }
};

const precip = {
  mark: { type: 'line', interpolate: 'monotone', stroke: 'grey' },
  encoding: {
    x: { timeUnit: 'month', field: 'date', title: null },
    y: { aggregate: 'average', field: 'precipitation', title: 'Precipitation' }
  }
};

render({
  layer: [ tempMinMax, precip ],
  data: { values: weather },
  transform: [
    { filter: 'datum.location == "Seattle"' }
  ]
})

The precipitation values use a much smaller range of the y-axis than the temperatures!

By default, layered charts use a shared domain: the values for the x-axis or y-axis are combined across all the layers to determine a shared extent. This default behavior assumes that the layered values have the same units. However, this doesn’t hold up for this example, as we are combining temperature values (degrees Celsius) with precipitation values (inches)!

If we want to use different y-axis scales, we need to specify how we want Vega-Lite to resolve the data across layers. In this case, we want to resolve the y-axis scale domains to be independent rather than use a shared domain:

const tempMinMax = {
  mark: { type: 'area', opacity: 0.3 },
  encoding: {
    x: {
      timeUnit: 'month', field: 'date',
      axis: { format: '%b' },
      title: null
    },
    y: {
      aggregate: 'average', field: 'temp_max',
      title: 'Avg. Temperature °C'
    },
    y2: { aggregate: 'average', field: 'temp_min' }
  }
};

const precip = {
  mark: { type: 'line', interpolate: 'monotone', stroke: 'grey' },
  encoding: {
    x: { timeUnit: 'month', field: 'date', title: null },
    y: { aggregate: 'average', field: 'precipitation', title: 'Precipitation' }
  }
};

render({
  layer: [ tempMinMax, precip ],
  data: { values: weather },
  transform: [
    { filter: 'datum.location == "Seattle"' }
  ]
})

We can now see that autumn is the rainiest season in Seattle (peaking in November), complemented by dry summers.

While dual-axis charts can be useful, they are often prone to misinterpretation, as the different units and axis scales may be incommensurate. As is feasible, you might consider transformations that map different data fields to shared units, for example showing quantiles or relative percentage change.


Facet

Faceting involves subdividing a dataset into groups and creating a separate plot for each group. In earlier notebooks, we learned how to create faceted charts using the row and column encoding channels. We’ll first review those channels and then show how they are instances of the more general facet operator.

Let’s start with a basic histogram of maximum temperature values in Seattle:

render({
  mark: { type: 'bar' },
  data: { values: weather },
  transform: [
    { filter: 'datum.location == "Seattle"' }
  ],
  encoding: {
    x: {
      field: 'temp_max', type: 'Q', bin: true,
      title: 'Temperature (°C)'
    },
    y: { aggregate: 'count' }
  }
})

How does this temperature profile change based on the weather of a given day – that is, whether there was drizzle, fog, rain, snow, or sun?

Let’s use the column encoding channel to facet the data by weather type. We can also use color as a redundant encoding, using a customized color range:

const colors = {
  domain: ['drizzle', 'fog', 'rain', 'snow', 'sun'],
  range: ['#aec7e8', '#c7c7c7', '#1f77b4', '#9467bd', '#e7ba52']
};
render({
  mark: { type: 'bar' },
  data: { values: weather },
  transform: [
    { filter: 'datum.location == "Seattle"' }
  ],
  encoding: {
    x: {
      field: 'temp_max', type: 'Q', bin: true,
      title: 'Temperature (°C)'
    },
    y: { aggregate: 'count' },
    color: { field: 'weather', type: 'N', scale: colors },
    column: { field: 'weather', type: 'N' }
  },
  width: 150,
  height: 150
})

Unsurprisingly, those rare snow days center on the coldest temperatures, followed by rainy and foggy days. Sunny days are warmer and, despite Seattle stereotypes, are the most plentiful. Though as any Seattleite can tell you, the drizzle occasionally comes, no matter the temperature!

In addition to row and column encoding channels within a mark definition, we can take a basic mark specification and then apply faceting using an explicit facet operator.

Let’s recreate the chart above, but this time using facet. We start with the same basic histogram definition, but remove the data source, filter transform, and column channel. We then place this definition within a facet specification, passing in the data and specifying that we should facet into columns according to the weather field. The facet property accepts an object with row and/or column properties. The two can be used together to create a 2D grid of faceted plots.

Finally we include our filter transform, applying it to the top-level faceted chart. While we could apply the filter transform to the histogram definition as before, that is slightly less efficient. Rather than filter out “New York” values within each facet cell, applying the filter to the faceted chart lets Vega-Lite know that we can filter out those values up front, prior to the facet subdivision. `

render({
  facet: {
    column: { field: 'weather' }
  },
  data: { values: weather },
  transform: [
    { filter: 'datum.location == "Seattle"' }
  ],
  spec: {
    mark: { type: 'bar' },
    encoding: {
      x: {
        field: 'temp_max', type: 'Q', bin: true,
        title: 'Temperature (°C)'
      },
      y: { aggregate: 'count' },
      color: { field: 'weather', type: 'N', scale: colors },
    },
    width: 150,
    height: 150
  }
})

Given all the extra code above, why would we want to use an explicit facet operator? For basic charts, we should certainly use the column or row encoding channels if we can. However, using the facet operator explicitly is useful if we want to facet composed views, such as layered charts.

Let’s revisit our layered temperature plots from earlier. Instead of plotting data for New York and Seattle in the same plot, let’s break them up into separate facets. The individual chart definitions are nearly the same as before: one area mark and one line mark. We can layer the charts much as before, then invoke facet on the layered view, passing in the data and specifying column facets based on the location field:

const tempMinMax = {
  mark: { type: 'area', opacity: 0.3 },
  encoding: {
    x: {
      timeUnit: 'month', field: 'date',
      axis: { format: '%b' },
      title: null
    },
    y: {
      aggregate: 'average', field: 'temp_max',
      title: 'Avg. Temperature (°C)'
    },
    y2: { aggregate: 'average', field: 'temp_min' },
    color: { field: 'location', type: 'N' }
  }
};

const tempMid = {
  mark: { type: 'line' },
  encoding: {
    x: { timeUnit: 'month', field: 'date' },
    y: { aggregate: 'average', field: 'temp_mid' },
    color: { field: 'location', type: 'N' }
  }
};

render({
  facet: {
    column: { field: 'location' }
  },
  spec: {
    layer: [ tempMinMax, tempMid ]
  },
  data: { values: weather },
  transform: [
    { calculate: '(datum.temp_min + datum.temp_max) / 2', as: 'temp_mid' }
  ]
})

The faceted charts we have seen so far use the same axis domains across the facet cells. This default of using shared scales and axes helps aid accurate comparison of values. However, in some cases you may wish to scale each chart independently, for example if the range of values in the cells differs significantly.

Similar to layered charts, faceted charts also support resolving to independent scales or axes across plots. Let’s see what happens if we call the resolve method to request independent y-axes:

const tempMinMax = {
  mark: { type: 'area', opacity: 0.3 },
  encoding: {
    x: {
      timeUnit: 'month', field: 'date',
      axis: { format: '%b' },
      title: null
    },
    y: {
      aggregate: 'average', field: 'temp_max',
      title: 'Avg. Temperature (°C)'
    },
    y2: { aggregate: 'average', field: 'temp_min' },
    color: { field: 'location', type: 'N' }
  }
};

const tempMid = {
  mark: { type: 'line' },
  encoding: {
    x: { timeUnit: 'month', field: 'date' },
    y: { aggregate: 'average', field: 'temp_mid' },
    color: { field: 'location', type: 'N' }
  }
};

render({
  facet: {
    column: { field: 'location' }
  },
  spec: {
    layer: [ tempMinMax, tempMid ]
  },
  data: { values: weather },
  transform: [
    { calculate: '(datum.temp_min + datum.temp_max) / 2', as: 'temp_mid' }
  ],
  resolve: { axis: { y: 'independent' } }
})

The chart above looks largely unchanged, but the plot for Seattle now includes its own axis.

What if we instead resolve the underlying scale domains?

Now we see facet cells with different axis scale domains. In this case, using independent scales seems like a bad idea! The domains aren’t very different, and one might be fooled into thinking that New York and Seattle have similar maximum summer temperatures.

const tempMinMax = {
  mark: { type: 'area', opacity: 0.3 },
  encoding: {
    x: {
      timeUnit: 'month', field: 'date',
      axis: { format: '%b' },
      title: null
    },
    y: {
      aggregate: 'average', field: 'temp_max',
      title: 'Avg. Temperature (°C)'
    },
    y2: { aggregate: 'average', field: 'temp_min' },
    color: { field: 'location', type: 'N' }
  }
};

const tempMid = {
  mark: { type: 'line' },
  encoding: {
    x: { timeUnit: 'month', field: 'date' },
    y: { aggregate: 'average', field: 'temp_mid' },
    color: { field: 'location', type: 'N' }
  }
};

render({
  facet: {
    column: { field: 'location' }
  },
  spec: {
    layer: [ tempMinMax, tempMid ]
  },
  data: { values: weather },
  transform: [
    { calculate: '(datum.temp_min + datum.temp_max) / 2', as: 'temp_mid' }
  ],
  resolve: { scale: { y: 'independent' } }
})

To borrow a cliché: just because you can do something, doesn’t mean you should


Concatenate

Faceting creates small multiple plots that show separate subdivisions of the data. However, we might wish to create a multi-view display with different views of the same dataset (not subsets) or views involving different datasets.

Vega-Lite provides concatenation operators to combine arbitrary charts into a composed chart. The hconcat operator performs horizontal concatenation, while the vconcat operator performs vertical concatenation.

Let’s start with a basic line chart showing the average maximum temperature per month for both New York and Seattle, much like we’ve seen before:

render({
  mark: { type: 'line' },
  data: { values: weather },
  encoding: {
    x: { timeUnit: 'month', field: 'date', title: null },
    y: { aggregate: 'average', field: 'temp_max' },
    color: { field: 'location', type: 'N' }
  }
})

What if we want to view not just temperature, but also precipitation and wind levels?

Let’s create a concatenated chart consisting of three plots. We’ll start by defining a “base” chart definition that contains all the aspects that should be shared by our three plots. We can then modify this base chart to create customized variants, with different y-axis encodings for the temp_max, precipitation, and wind fields. We can then concatenate them using the hconcat operator:

const base = (encoding) => {
  return {
    mark: { type: 'line' },
    data: { values: weather },
    encoding: {
      x: { timeUnit: 'month', field: 'date', title: null },
      ...encoding,
      color: { field: 'location', type: 'N' }
    },
    width: 240,
    height: 180
  };
};

const temp = base({ y: { aggregate: 'average', field: 'temp_max' } });
const precip = base({ y: { aggregate: 'average', field: 'precipitation' } });
const wind = base({ y: { aggregate: 'average', field: 'wind' } });

render({
  hconcat: [temp, precip, wind]
})

Vertical concatenation works similarly to horizontal concatenation. Using the vconcat operator, modify the code to use a vertical ordering instead of a horizontal ordering.

Finally, note that horizontal and vertical concatenation can be combined. What happens if you write something like this?

As we will revisit later, concatenation operators let you combine any and all charts into a multi-view dashboard!


Repeat

The concatenation operators above are quite general, allowing arbitrary charts to be composed. Nevertheless, the example above was still a bit verbose: we have three very similar charts, yet have to define them separately and then concatenate them.

For cases where only one or two variables are changing, the repeat operator provides a convenient shortcut for creating multiple charts. Given a template specification with some free variables, the repeat operator will then create a chart for each specified assignment of those variables.

Let’s recreate our concatenation example above using the repeat operator. The only aspect that changes across charts is the choice of data field for the y encoding channel. To create a template specification, we can use the repeater variable { repeat: 'column' } as our y-axis field. This code simply states that we want to use the variable assigned to the column repeater, which organizes repeated charts in a horizontal direction.

We then wrap our chart specification within a repeat operator, passing in data field names for each column:

render({
  repeat: {
    column: ['temp_max', 'precipitation', 'wind']
  },
  spec: {
    mark: { type: 'line' },
    data: { values: weather },
    encoding: {
      x: { timeUnit: 'month', field: 'date', title: null },
      y: { aggregate: 'average', field: { repeat: 'column' } },
      color: { field: 'location', type: 'N' }
    },
    width: 240,
    height: 180
  }
})

Repetition is supported for both columns and rows. What happens if you modify the code above to use row instead of column?

We can also use row and column repetition together! One common visualization for exploratory data analysis is the scatter plot matrix (or SPLOM). Given a collection of variables to inspect, a SPLOM provides a grid of all pairwise plots of those variables, allowing us to assess potential associations.

Let’s use the repeat operator to create a SPLOM for the temp_max, precipitation, and wind fields. We first need a template specification, with repeater variables for both the x- and y-axis data fields. We then pass this to the repeat operator, with arrays of field names to use for both row and column. Vega-Lite will then generate the cross product (or, Cartesian product) to create the full space of repeated charts.

Looking at these plots, there does not appear to be a strong association between precipitation and wind, though we do see that extreme wind and precipitation events occur in similar temperature ranges (~5-15° C). However, this observation is not particularly surprising: if we revisit our histogram at the beginning of the facet section, we can plainly see that the days with maximum temperatures in the range of 5-15° C are the most commonly occurring.

render({
  repeat: {
    row: ['temp_max', 'precipitation', 'wind'],
    column: ['wind', 'precipitation', 'temp_max']
  },
  data: { values: weather },
  transform: [
    { filter: 'datum.location == "Seattle"' }
  ],
  spec: {
    mark: { type: 'circle', size: 15, opacity: 0.5 },
    encoding: {
      x: { field: { repeat: 'column' }, type: 'Q' },
      y: { field: { repeat: 'row' }, type: 'Q' }
    },
    width: 150,
    height: 150
  }
})

Now modify the code above to get a better understanding of chart repetition. Try adding another variable (temp_min) to the SPLOM. What happens if you rearrange the order of the field names in either the row or column arguments to the repeat operator?

Finally, to really appreciate what the repeat operator provides, take a moment to imagine how you might recreate the SPLOM above using only hconcat and vconcat!


A View Composition Algebra

Together, the composition operators layer, facet, concat, and repeat form a view composition algebra: the various operators can be combined to construct a variety of multi-view visualizations.

As an example, let’s start with two basic charts: a histogram and a simple line (a single rule mark) showing a global average.

const basic1 = {
  mark: { type: 'bar' },
  data: { values: weather },
  transform: [
    { filter: 'datum.location == "Seattle"' }
  ],
  encoding: {
    x: { timeUnit: 'month', field: 'date', type: 'O', title: 'Month' },
    y: { aggregate: 'average', field: 'temp_max' }
  }
};

const basic2 = {
  mark: { type: 'rule', stroke: 'firebrick' },
  data: { values: weather },
  transform: [
    { filter: 'datum.location == "Seattle"' }
  ],
  encoding: {
    y: { aggregate: 'average', field: 'temp_max' }
  }
};

render({
  hconcat: [basic1, basic2]
})

We can combine the two charts using a layer operator, and then repeat that layered chart to show histograms with overlaid averages for multiple fields:

render({
  repeat: {
    column: ['temp_max', 'precipitation', 'wind']
  },
  data: { values: weather },
  transform: [
    { filter: 'datum.location == "Seattle"' }
  ],
  spec: {
    layer: [
      {
        mark: { type: 'bar' },
        encoding: {
          x: { timeUnit: 'month', field: 'date', type: 'O', title: 'Month' },
          y: { aggregate: 'average', field: { repeat: 'column' } }
        }
      },
      {
        mark: { type: 'rule', stroke: 'firebrick' },
        encoding: {
          y: { aggregate: 'average', field: { repeat: 'column' } }
        }
      }
    ],
    width: 200,
    height: 150
  }
})

Focusing only on the multi-view composition operators, the model for the visualization above is:

 repeat(column: [...])
 |- layer
    |- basic1
    |- basic2

Now let’s explore how we can apply all the operators within a final dashboard that provides an overview of Seattle weather. We’ll combine the SPLOM and faceted histogram displays from earlier sections with the repeated histograms above:

const splom = {
  repeat: {
    row: ['temp_max', 'precipitation', 'wind'],
    column: ['wind', 'precipitation', 'temp_max']
  },
  spec: {
    mark: { type: 'circle', size: 15, opacity: 0.5 },
    encoding: {
      x: { field: { repeat: 'column' }, type: 'Q' },
      y: { field: { repeat: 'row' }, type: 'Q' }
    },
    width: 125,
    height: 125
  }
};

const dateHist = {
  repeat: {
    row: ['temp_max', 'precipitation', 'wind']
  },
  spec: {
    layer: [
      {
        mark: { type: 'bar' },
        encoding: {
          x: { timeUnit: 'month', field: 'date', type: 'O', title: 'Month' },
          y: { aggregate: 'average', field: { repeat: 'row' } }
        }
      },
      {
        mark: { type: 'rule', stroke: 'firebrick' },
        encoding: {
          y: { aggregate: 'average', field: { repeat: 'row' } }
        }
      }
    ],
    width: 175,
    height: 125
  }
};

const tempHist = {
  facet: {
    column: { field: 'weather' }
  },
  spec: {
    mark: { type: 'bar' },
    encoding: {
      x: {
        field: 'temp_max', type: 'Q', bin: true,
        title: 'Temperature (°C)'
      },
      y: { aggregate: 'count' },
      color: {
        field: 'weather', type: 'N',
        scale: {
          domain: ['drizzle', 'fog', 'rain', 'snow', 'sun'],
          range: ['#aec7e8', '#c7c7c7', '#1f77b4', '#9467bd', '#e7ba52']
        }
      }
    },
    width: 115,
    height: 100
  }
};

render({
  data: { values: weather },
  transform: [
    { filter: 'datum.location == "Seattle"' }
  ],
  title: 'Seattle Weather Dashboard',
  vconcat: [
    { hconcat: [splom, dateHist] },
    tempHist
  ],
  resolve: { legend: { color: 'independent' } },
  config: { axis: { labelAngle: 0 } }
});

The full composition model for this dashboard is:

 vconcat
 |- hconcat
 |  |- repeat(row: [...], column: [...])
 |  |  |- splom base chart
 |  |- repeat(row: [...])
 |     |- layer
 |        |- dateHist base chart 1
 |        |- dateHist base chart 2
 |- facet(column: 'weather')
    |- tempHist base chart

Phew! The dashboard also includes a few customizations to improve the layout:

Try removing or modifying any of these adjustments and see how the dashboard layout responds!

This dashboard can be reused to show data for other locations or from other datasets. Update the dashboard to show weather patterns for New York instead of Seattle.


Summary

For more details on multi-view composition, including control over sub-plot spacing and header labels, see the Vega-Lite View Composition documentation.

Now that we’ve seen how to compose multiple views, we’re ready to put them into action. In addition to statically presenting data, multiple views can enable interactive multi-dimensional exploration. For example, using linked selections we can highlight points in one view to see corresponding values highlight in other views.

In the next notebook, we’ll examine how to author interactive selections for both individual plots and multi-view compositions.