Composing Layers

We have reiterated the fact that Muze is a composable library. As such, the most basic composability comes from building complex charts as easily as possible and the extensibility of these charts. In order to establish this, Muze allows you to compose layers on top of one another and link them together to create interactive visualization.

But first, let us understand what we mean by layers.

What is a layer?

A layer is a visualization of data points with mark and encodings applied on it. Planer encoding (x, y) position the points in 2D plane and retinal encoding (color, shape, size) changes visual representation of the points based on value of some variable. So it is safe to say

layer = data + mark

Marks are analogous to plot types we have heard in other visualization libraries.

Muze provides atomic marks which you can use to compose layers. Following are the marks available in Muze

  • Bar
  • Line
  • Area
  • Arc
  • Text
  • Dot
  • Tick

Unit layer

Lets take an example of a simple bar plot

alt text

Here there is only one layer rendered to house the data points with bar mark with planer encoding. If you see the code for the layer it would look something like

canvas.layers([{
    mark: 'bar',
    encoding: {
        y: 'Horsepower',
        x: 'Origin'
    }
}])
logo

Muze generates default definition of layers

You don't have to define a layer always, Muze generates layers' definition based on fields mapped to rows and columns and type of the fields (measure and dimension)

Composite layers

Lets take the example of the visualization from trend line tutorial.

alt text

If you dissect the above visualization, you find out there are two layers, one for the bar chart, lets call it bar layer and another for the line and text, lets call it trendline layer where bar layer and trendline layer sit on top of each other.

You might be thinking why have we grouped both line and text to create one trendline layer, we could have created individual layers for both of them. If you think logically, line and text together form the trendline layer. They can't exist individually. That's why one layer to handle both of them.

logo

Layers are like svg groups

If you have used svg groups ( <g> ... </g> ) before, you might remember you create a group for one logical section and form a hierarchy.

g
|---rect
|---g
    | --- text
    | --- text

Muze's layers are exactly similar to this concept

composed-layer-2
|---bar
|---composed-layer-1
    | --- line
    | --- text

Data

We will be using cars.json data for illustration.

Single layers with marks

Lets create unit layers with default marks provided by Muze

Bar Layer

A bar layer is made up of rectangular bars. They are useful when the data has a categorical (dimension) field against a measure field. The alignment (horizontal / vertical) of the bars are determined from which type of variable is mapped to x and y axis.

Let's consider we need to see how powerful the cars for the different countries are:

main
run-button
run-button
reset-button
// DataModel instance is created from https://www.charts.com/static/cars.json data,
// https://www.charts.com/static/cars-schema.json schema and assigned to variable dm.

const env = muze();
const canvas = env.canvas();

canvas
    .data(dm)
    .width(600)
    .height(400)
    .rows(['Horsepower'])
    .columns(['Origin'])
    .mount('#chart-container');

Although we have not mentioned what kind of chart to be displayed, Muze based on the type of field getting assigned to rows and columns creates a visualization with one layer with mark bar. If you want to know Muze chooses the default mark for a rendering, check this table out.

By adding colors, we can stack or group the bars. Let's see how powerful the different cars are by Cylinder for each country:

main
run-button
run-button
reset-button
// DataModel instance is created from https://www.charts.com/static/cars.json data,
// https://www.charts.com/static/cars-schema.json schema and assigned to variable dm.
  
const env = muze();
const canvas = env.canvas();
  
canvas
    .data(dm)
    .width(600)
    .height(400)
    .rows(['Horsepower'])
    .layers([{
    	mark: 'bar', 
      	transform: {
        	type:'group'
     	}
    }])
    .columns(['Origin'])
    .color('Cylinders')
    .mount('#chart-container');

When a field is assigned to color is assigned to encoding channel, it helps us encode more information in a visualization. We assigned Cylinders to colorencoding channel and notice how we got one colored bar for each cylinders.

With this new details of data by default stack transformed is used by Muze to show the visualization. But here we choose to see group transformation using.

.layers([{
    mark: 'bar', 
    transform: {
        type:'group'
    }
}])
logo

Not all data is suitable for stack transform

Of course, You can change value of the transform property from group

.layers([{
    mark: 'bar',
    transfrom: {
        type: 'stack'
    }
}])

But you cant make sense anything from the chart you will get for stack transfrom here.

Checkout this example where we calculate data for stack transform and then show a proper stacked chart.

Line Layer

A line layer is made up of path. They are useful to check patterns and exceptions in data which has sequential progression, like time. It forms the basis of a line chart. Muze automatically creates a line layer if you assign temporal (dimension) field to either of the axes

Let's check whether the Acceleration of the cars manufactured have actually become better over the time

main
run-button
run-button
reset-button
// DataModel instance is created from https://www.charts.com/static/cars.json data,
// https://www.charts.com/static/cars-schema.json schema and assigned to variable dm.
  
const env = muze();
const canvas = env.canvas();

canvas
    .data(dm)
    .width(600)
    .height(400)
    .rows(['Acceleration'])
    .columns(['Year'])
    .mount('#chart-container');

In the above chart we have not mentioned anywhere to draw a line chart. But Muze figured out that Year is a temporal field (which you have described in schema of DataModel ) in data and Year is assigned in x-axis (columns), so it generates the following config internally

canvas.layer([{
    mark: 'line',
    encoding: {
        x: 'Year',
        y: 'Acceleration'
    }
}])

Area Layer

An area layer is similar to a line chart, except that it shows the volume of a measure by area covered under the graph.

main
run-button
run-button
reset-button
// DataModel instance is created from https://www.charts.com/static/cars.json data,
// https://www.charts.com/static/cars-schema.json schema and assigned to variable dm.
  
const env = muze();
const canvas = env.canvas();

canvas
    .data(dm)
    .width(600)
    .height(400)
    .layers([{
      	mark : 'area',
      	interpolate: 'catmullRom' /* spline */
    }])
    .rows(['Weight_in_lbs'])
    .columns(['Year'])
    .mount('#chart-container');

If you look at the chart above, you will see that the area chart is smooth, unlike the line plot. This is because we have add an interpolator catMullRom in layer to introduce the smoothness.

Point Layer

If measures are assigned to both of the axes, Muze renders a layer with point mark. Each point in the following chart can take any continuous value in x and y coordinate. It forms the basis of scatter and bubbles charts.

Let's see the Power to Weight Distribution of cars

main
run-button
run-button
reset-button
// DataModel instance is created from https://www.charts.com/static/cars.json data,
// https://www.charts.com/static/cars-schema.json schema and assigned to variable dm.
  
const env = muze();
const canvas = env.canvas();

canvas
    .data(dm)
    .width(600)
    .height(400)
    .detail(['Name'])
    .rows(['Weight_in_lbs'])
    .columns(['Horsepower'])
    .mount('#chart-container');

Muze internally generates the following config to render the above layer

canvas.layer([{
    mark: 'point',
    encoding: {
        x: 'Horsepower',
        y: 'Weight_in_lbs'
    }
}])

You can apply color, shape and size retinal encodings on point layer.

Text Layer

A layer with mark text draws text. Muze by default does not show any annotations (text) on a plot. If you need text labels on charts, you have to add this layer along side bar layer.

main
run-button
run-button
reset-button
// DataModel instance is created from https://www.charts.com/static/cars.json data,
// https://www.charts.com/static/cars-schema.json schema and assigned to variable dm.
  
const env = muze();
const canvas = env.canvas();

canvas
    .data(dm)
    .width(600)
    .height(400)
    .rows(['Displacement'])
    .columns(['Origin'])
    .layers([{
    	mark : 'text',
      	encoding: {
        	text: {
            	field: 'Horsepower',
              	formatter: val => 'Horsepower: ' + parseFloat(val, 10).toFixed(2)
            }/* To display the text */
      	} 
    }])
    .mount('#chart-container');

We have done couple of more things than just rendering a text layer. We have formatted the value of the text by using formatter property.

Also notice the x-position of the text is determined by Origin and y-position is determined by Displacement but we are showing the value of Horsepower in the chart.

When we provide encoding in layers we just assign the field name to a particular encoding like

.layers([{
    mark: 'text',
    encoding: {
        text: 'Horsepower'
    }
}])

The above config is a shorthand for

.layers([{
    mark: 'text',
    encoding: {
        text: {
            field: 'Horsepower',
            /* additional properties */
        }
    }
}])

Tick Layer

A tick is largely similar to a point mark except for the fact that it has a start point and an end point and thus is always a straight line.

Arc Layer

The arc layer is the most different of all the layers, in that it does not follow (x,y) cartesian coordinate system, rather it uses the polar coordinates. By dividing a circular structure, it provides the basis of charts like pie chart, donut chart etc. Arc layer has different encodings altogether. Since this layer comes from polar coordinate the planer encodings are radius and angle.

Checkout this example of an arc chart.

Mark overriding

Based on the variable type (measure or dimension) assigned to x axis and y axis (rows and columns) Muze decides the mark type from this lookup table.

How ever you can override the choice Muze has done for you easily.

Lets say you want to show a bar chart to visualize how weight of a car has changed over the time.

main
run-button
run-button
reset-button
// DataModel instance is created from https://www.charts.com/static/cars.json data,
// https://www.charts.com/static/cars-schema.json schema and assigned to variable dm.
  
const env = muze();
const canvas = env.canvas();

canvas
    .data(dm)
    .width(600)
    .height(400)
    .layers([{
      	mark : 'bar'
    }])
    .rows(['Weight_in_lbs'])
    .columns(['Year'])
    .mount('#chart-container');

As you can see, all we have done is, defined the layer ourself and then passed it to Muze.

.layers([{
   mark : 'bar'
}])

If you go ahead and remove the layer function, you will see Muze by default renders a line layer, because it generates the following config if you dont pass the layer definition

.layers([{
   mark : 'line',
   encoding: {
       x: 'Year',
       y: 'Weight_in_lbs'
   }
}])

Also note, in the code for above example, we have not given encoding explicitly in layer function. In this case also, Muze automatically populates encoding object from rows and columns.

Layer encodings

We have only seen few of the encodings which a layer with a particular mark supports. There are few more of these which we have not discussed. The following table shows the supported encoding for each layer with particular mark.

alt text

Arc layer does not support x, x0, y, y0 encoding. It supports radius, angle, color, size encoding.

Let's see what happens if you give y and y0 encoding to area plot.

main
run-button
run-button
reset-button
// DataModel instance is created from https://www.charts.com/static/cars.json data,
// https://www.charts.com/static/cars-schema.json schema and assigned to variable dm.

dm = dm.calculateVariable({
    name: 'min_weight',
    type: 'measure',
    defAggFn: 'min'
}, ['Weight_in_lbs', val => val]); // Max value of weight for a group

dm = dm.calculateVariable({
    name: 'max_weight',
    type: 'measure',
    defAggFn: 'max'
}, ['Weight_in_lbs', val => val]); // Min value of weight for a group

const share = muze.Operators.share;
const env = muze();
const canvas = env.canvas();

canvas
    .data(dm)
    .width(600)
    .height(400)
    .layers([{
          mark : 'area',
          interpolate: 'catmullRom', /* spline */
          encoding: {
            y: 'max_weight',
            y0: 'min_weight',
            color: { value: () => '#ff9800'}
        }
    }])
    .rows([[share('max_weight', 'min_weight') /* create a shared varaibles to be plotted on same axis */]])
    .columns(['Year'])
    .mount('#chart-container');

And that's how easily a range area plot can be created using simple layers.

Wrapping up

By now, you probably have a basic idea of what we are trying to establish here. Let's go about it:

  • Muze creates default layers for specific field type combinations
  • You can override these layers by providing your own layer configurations including marks and encodings
  • Muze automatically takes the encodings for the layers if you we specify
  • Muze allows multiple field to share one axis and create range plot

If you are comfortable with the idea of layer, head to composing layers to understand how layer makes a visualization rich.