Layers

Some charts in the linked-charts library allow the user to put several plots on top of each other. These plots are called layers and can be of the same or of different types. All the layers use the same chart as basis and therefore have the same size (width and height), margins, will be zoomed in or out together and updated at the same time (unless a layer-specific update method is used). Yet, each layer has its own properties, such as nelements, colour, size that doesn't influence one another and need to be defined independently.

In the current implementation of the linked-charts library only the charts with X and Y axes (scatter, beeswarm, xLine, yLine, parametricCurve, barchart) can have multiple layers.

In this tutorial we will explain how to use layers. The last section is for more advanced users and will tell you, how to define your own layer type.

Add or remove a layer

Actually, even if you have just started with the linked-charts library and tried only the most basic examples, you probably already know, how to generate a chart with layers, since all the functions that initialise charts with axes in fact create an empty axesChart and add a layer to it. All these functions have two optional arguments: the first one defines an ID for the new layer and the other is a chart which this layer will be added to. If the second one is undefined, a new chart is created. Thus, all you need to do, to add a layer to an existing chart is to define that second argument.

Let's have a look at a simple example.

var plot = lc.scatter()
  .nelements(10)
  .x(function(k) {return k})
  .y(function(k) {return k + Math.random() - 0.5});

lc.xLine("line", plot)
  .lineFun(function(t) {return t})
  .place();

Note, that place function should be called only once per chart. Otherwise you will get several plots on the page with incorrect functionality. But it doen't mean that after calling the place function you can't add layers. There is also a place_layer method, that can be used for any layer separately. The following code will give exactly the same result as the example above.

var plot = lc.scatter()
  .nelements(10)
  .x(function(k) {return k})
  .y(function(k) {return k + Math.random() - 0.5})
  .place();

lc.xLine("line", plot)
  .lineFun(function(t) {return t})
  .place_layer("line");

To remove a layer one can just use the remove_layer method. It requires an ID of the layer as an argument.

Another way of adding and removing layers, which is usefull, when you have too many layers to add all of them manually or don't know beforehand, how many layers you need, you can find in the section Multiple layers selection.

Layers and properties

Each layer has its own properties and the most straightforwad way to set them is first to get a layer object via get_layer method and only then to set its properties. It looks like this

chart.get_layer(layerID).property(newValue);

One may find it really annoying, especially since property setters always return chart object (not layers!). Therefore in the linked-charts library we made it possible to access layer properties directly from the chart object.

Let's take our previous example and modify it a bit (symbol and dasharray are both layer specific properties).

var plot = lc.scatter("scatter")
  .nelements(10)
  .x(function(k) {return k})
  .y(function(k) {return k + Math.random() - 0.5});

lc.xLine("line", plot)
  .lineFun(function(t) {return t})
  .place();

plot.dasharray(5)
  .symbol("Triangle")
//.colour("red")
  .update();

As you can see, we changed the properties of both layers without using the get_layer method. But what happens if several layers have the same property? Try to uncomment setting colour and run the example again.

Now the triangles are still black. Only the line changed colour, since it belongs to the layer that has been added the last. Any layerChart has an activeLayer property. It defines a layer, whose property can be set directly from the chart object without selecting a layer at the current moment. So if property1 is a chart-property, property2 is a property of the active layer and property3 is a property of some other layer, one can set them like this.

chart.property1(newValue1)
  .property2(newValue2)
  .get_layer(id)
    .property3(newValue3);

By default, each chart becomes active immediately after initialising.

Try adding any of these two pieces of code to the previous example.

plot.get_layer("scatter")
    .colour("blue")
    .opacity(0.5)
    .update();
plot.activeLayer(plot.get_layer("scatter"))
    .colour("blue")
    .opacity(0.5)
    .update();

As you can see, in the first example the line remained the active layer and its opacity property has been changed, but in the second one it was set for the triangles.

Besides properties, there are also layer-specific methods: updateElements, updateElementStyle and updateElementPosition. They follow the same logic as properties.

Multiple layers selection

If you don't want to define and then customize each of the layers separately, you can manipulate several layers at the same time. First of all, to add or remove multiple layers at once, one can just set the layerIds property. It takes an array of IDs of all the layers you want to have at the current moment. New empty layers will be added and all those, that are no longer required, will be removed. Along with this property goes the layerType property, since if a type of the new layer is not defined, an empty layer is created, which is of no use unless you want to define your own type of plot.

Setting these two properties you can quickly create any number of layers, but all of them are empty. Now, you can manually go through each of them and fill them with data. Or you can use the select_layers method and modify several of them at once.

select_layers takes as an optional argument an array of IDs of the layers you want to modify. If no array is provided, all existing layers are selected. The returned layer selection is similar to a chart object with two main differences:

Another important thing is that any property you set using a layer selection will be applied only to layers that have this property. So if you, for example, want to set lineFun for several lines, you don't need to select only layers with lines. You can have anything else in your selection.

Now let's summarise all of this with an example

//define IDs for all our layers
var ids = ["cos_scatter", "sin_scatter",
           "cos_xLine", "sin_xLine"];

//create a chart with four empty layers
var plot = lc.axesChart()
  .layerIds(ids)
  .layerType(function(id) {return id.split("_")[1]});

plot.select_layers()
  //each scatter plot will have ten elements
  //each line chart will get only one line
  .nelements(function(layerId) {
    return layerId.split("_")[1] == "scatter" ? 10 : 1;
  })
  //these are properties of scatters
  .x(function(layerId, k) {return k})
  .y(function(layerId, k) {
    return layerId.split("_")[0] == "sin" ? Math.sin(k) :
                                          Math.cos(k);  
  })
  //this is a property of line charts only
  .lineFun(function(layerId, x) {
    return layerId.split("_")[0] == "sin" ? Math.sin(x) :
                                          Math.cos(x);  
  })
  //this property will be set for all the layers;
  .colour(function(layerId) {
    return layerId.split("_")[0] == "sin" ? "red" :
                                          "blue"; 
  });

//we can't place a layer selection
plot.place();

Here is another example to show you, how it works.

Domains

In the current implementation of the linked-charts library all the layers share the same axes. So X and Y domains of the axis scales are defined so that to fit all elements from all the layers or, more precicely, to fill all layerDomainX and layerDomainY, which are in most cases defined so that to fit all the elemetns in the layer.

You can change layer domains individually or you can change the resulting chart domain by setting domainX and domainY properties. Of course, it influences only the original domains - the ones that you see, when the chart is just generated (or the ones that you get after a double click). After that there are no limitations on moving around, zooming in or out.

One thing that you can't change is type of the scale: whether it's continuous or categorical. Here, the rule is simple - the resulting scale is continuos if and only if all the layer scales are continuous as well.

Sometimes, layerDomainX and layerDomainY may have no default value, like, for example, lines in the previous example. It means, that they will not influence the resulting domain and will be plotted in whatever area is now displayed.

var plot = lc.xLine()
    .lineFun(function(x) {return Math.sin(x)})
    .colour("red");

lc.xLine("cos", plot)
    .lineFun(function(x) {return Math.cos(x)})
    .layerDomainX([2, 3])
    .colour("blue")
    .domainX([0, 5])
    .domainY([-1, 1])
    .place();

Now the cosinus function is only displayed for x values from 2 to 3, while the sinus is shown for all x values. Try to remove lines, setting the chart's domains to see what happens. By the way, if all the layers have undefined domains for some axis, the [0, 1] range is used as a default domain.

Layer structure

Each layer inherits from the layerBase object that already has some predefined functionality, most common for different type of layers. And, which is more important, this common layer ancestor defines structure that is used to link chart and its layers. All individual aspects of any layer are described by few functions, which means that one can easily add new types of layers by defining or modifying these functions.

It's completely up to the user, how to define this methods, as long as they perform the actions, they are expected to do. Yet, the linked-charts library is heavily based on the d3 library and therefore to understand the further examples that are taken from the linked-charts source code one needs at least some understanding of the d3 library. Specifically, selections and the idea of data binding are important. This tutorial may be of some use.

The following functions are required for any layer:

The next two functions are requiered if you want to make your layer fully functional.

Here is an example of this function for scatter plots.
layer.findElements = function(lu, rb){
  return layer.g.selectAll(".data_element")
    .filter(function(d) {
      var loc = [layer.chart.axes.scale_x(layer.get_x(d)), 
                layer.chart.axes.scale_y(layer.get_y(d))]
      return (loc[0] - layer.get_size(d) <= rb[0]) && 
        (loc[1] - layer.get_size(d) <= rb[1]) && 
        (loc[0] + layer.get_size(d) >= lu[0]) && 
        (loc[1] + layer.get_size(d) >= lu[1]);
    }).data().map(function(e) {return [layer.id, e]});
}

Now, let's make an example of a new layer type. Each data element in this plot will be a rectangle with user defined upper and lower boundaries and equal widths. Here, to make things simple, we will use meaningless, artificially generated data, but a use case for such a plot and a more detailed example you can find here.

//create a chart with axes and add an empty layer
var plot = lc.axesChart()
  .add_layer("layer");
//save this layer to a variable
var layer = plot.activeLayer();

//updateElements is defined almost exactly as in most implemented
//types of layers
layer.updateElements = function(){
  var sel = layer.g.selectAll( ".data_element" )
    .data( layer.elementIds(), function(d) {return d;} );
  sel.exit()
    .remove();  
    sel.enter().append( "rect" )
    .attr( "class", "data_element" )
    .merge(sel)
      .attr("id", function(d) {
        return "p" + (layer.id + "_" + d).replace(/[ .]/g,"_");
      })
      .on( "click", layer.get_on_click )
      .on( "mouseover", layer.get_elementMouseOver )
      .on( "mouseout", layer.get_elementMouseOut );
}

//here we define height, width and position of a left-upper 
//corner for each rectangle
layer.updateElementPosition = function(){
  layer.g.selectAll(".data_element")
    //we assume that all the IDs are just numbers
    //therefore left-upper corner's coordinates are defined
    //as [id, upper_value]
    .attr("x", function(d) {return layer.chart.axes.scale_x(d)})
    .attr("y", function(d) {
      return layer.chart.axes.scale_y(layer.get_upper(d));
    })
    //width of each rectangle is 1 in the chart's coordinate system
    .attr("width", function() {
      return layer.chart.axes.scale_x(1) - layer.chart.axes.scale_x(0);
    })
    //height is defined as difference between lower and upper side
    .attr("height", function(d) {
      return Math.abs(layer.chart.axes.scale_y(layer.get_lower(d)) - 
                      layer.chart.axes.scale_y(layer.get_upper(d)));
    });
}

//let's for now just allow our rectangles to be of different colour
layer.updateElementStyle = function() {
  layer.resetColourScale();
  layer.g.selectAll(".data_element")
     .attr("fill", function(d) {return layer.get_colour(d)})
}

//add some new properties
layer
  .add_property("lower", function(k) {
    return Math.log(k + 1) - Math.random();
  })
  .add_property("upper", function(k) {
    return Math.log(k + 1) + Math.random();
  });
//this will allow to user layer's properties using only the chart object
plot.syncProperties(layer);

//now we set other properties and place the chart
plot
  .nelements(15)
  .layerDomainX([0, 15])
  .layerDomainY([-1, 5])
  .colourValue(function(k) {return k})
  .place();

By the way, in this test examples random numbers are generated on each update call. So don't be surprised that the plot changes each time you use any interactivity.