Heatmap with two colour channels

Generally, when a heatmap is generated, each cell corresponds to a single numeric value which in turn unambiguously defines colour of this cell via some colour scale. Yet, nothing in the heatmap chart implemented in the linked-charts library requires the variables passed through the value property to be just single numeric values. Of course, the default way of defining colour scale expects these value to be single numbers, but a user can easily override this by setting the colour property. The function, user puts here, gets the value exactly how it has been passed to value property. It can be anything: an array, a sting, another function - everything will do as soon as the user can define a way of transforming it into colour.

Here, we give an example how to utilise values that are not single numbers. The data that we are using here as an example contain the results of two assays, where 50 drugs in 5 different concentrations each were tested against 21 pancreatic cancer cell lines. RealTime-Glo (RTG) measures cell viability and is proportional to the number of methabolically active cells in a well. CellTox (CTX) gives us a value that is proportional to the amount of dead cells in a well. For each drug combination and concentration both values have been measured.

If a drug is active only in one of the assays, it means that either it doesn't really kill cells and they can recover afterwards, or it kills them not fast enough, leaving plenty of alive and active cells unharmed. Therefore it can be useful to look at both assays simultaniously. To this end we decided to use a well known in microscopy trick. We use a green channel for one assay and red for the other. Thus, the drugs, which in both assays demonstrated high effect will be shown in yellow, non-effective drugs will be shown as black cells and all others will be either red or green.

Therefore, this heatmap has two values per cell, each corresponds to a separate colour channel. For both channels we create an interactive colourSlider. We also demonstrate, how one can select and manipulate multiple cells, and how to select and update several layers at ones. The chart to the rigth shows the inhibition values for each indifidual concentration of all the selected drug.

The code for the example

A user of our framework can create apps with very little code. Bellow we show the part of the code that hasn't been explained in previous examples. You can find the complete code at the bottom of the page.

...
var heatmap = lc.heatmap()
  .rowIds( drugs )
  .colIds( cellLines )
...
  .value( function( rowId, colId ) {  
    return [inputData.RTG[rowId][colId].avInh, 
            inputData.CTX[rowId][colId].avInh];
   })
  .informText( function( rowId, colId ) {
      return "Row: <b>" + rowId + "</b>;<br>" + 
            "Col: <b>" + colId + "</b>;<br>" + 
            "RTG = " + heatmap.get_value( rowId, colId )[0].toFixed( 2 ) + 
            " ; CTX = " + heatmap.get_value( rowId, colId )[1].toFixed( 2 );
      })
  .on_click( function( rowId, colId ) {
    heatmap.mark( d3.select( this ) );
  })
  .markedUpdated( function(){
    var ids = [],
      data = heatmap.get_marked();
    for(var i = 0; i < data.length; i++) {
      ids.push( data[i][0] + "/" + data[i][1] + "/scatter" );
      ids.push( data[i][0] + "/" + data[i][1] + "/xLine" );
    }
    curveFit
      .layerIds( ids )
      .layerType( function( layerId ) {
        return layerId.split( "/" )[2];
      })
      .select_layers()
        .nelements( function( layerId ) {
          return layerId.split( "/" )[2] == "scatter" ? 10 : 2;
        } )
        .x(function( layerId, k ) {
          return k % 5;
        })
        .y(function( layerId, k ) {
          var drug = layerId.split( "/" )[0],
            cellLine = layerId.split( "/" )[1];
          return Math.floor( k / 5 ) == 0 ? inputData.RTG[drug][cellLine]["D" + ( +k + 1 )] : 
                                          inputData.CTX[drug][cellLine]["D" + ( k - 4 )];
        })
        .symbol( function( layerId, k ) {
          return k > 4 ? "Triangle" : "Wye";
        })
        .lineFun( function( layerId, x, k ) {
          var drug = layerId.split( "/" )[0],
            cellLine = layerId.split( "/" )[1],
            screen;
          k == 0 ? screen = "RTG" : screen = "CTX";
          return get_curve( screen, drug, cellLine, x );
        })
        .dasharray ( function( layerId, k ) {
          return k == 0 ? undefined : 5;
        })
        .colour( function( layerId ) {
          return d3.schemeCategory10[
                    Math.floor( curveFit.layerIds().indexOf( layerId ) % 20 / 2 )
                  ]
        });
    curveFit.update();
    var legIds = [], colours = [];
    for( var i = 0; i < ids.length; i += 2 ){
      legIds.push( ids[i].split( "/" )[0] + "/" + ids[i].split( "/" )[1]);
      colours.push( curveFit.get_layer(ids[i]).get_colour() );
    }
    curveFit.legend.updateScale( [legIds, colours], "drug_and_cell_line" );
  })
  .place( "#heatmap" );
...
var RTGSlider = lc.colourSlider()
  .set_margins( {left: 100} )
  .title( "RealTime-Glo" )
  .titleX( 45 )
  .titleY( 40 )
  .titleSize( 14 )
  .straightColourScale( 
    d3.scaleLinear()
      .range( [ "black", "rgb(0, 255, 0)" ] )
      .domain( [0, 50] ) 
  )
  .on_change(function(){
    heatmap.updateCellColour();
  })
  .place( "#heatmap" );
var CTXSlider = lc.colourSlider()
...
  .place( "#heatmap" );
...
heatmap.colour( function( val ){
  return "rgb(" + Math.round( CTXSlider.the_sigmoid( val[1] ) * 255 ) + ", " 
             + Math.round( RTGSlider.the_sigmoid( val[0] ) * 255 ) + ", 0)";
} )
  .updateCellColour();
...
var curveFit = lc.scatter( "void" )
...
  .place( "#scatterplot" );
  curveFit.legend.ncol(1)
    .legend.add_block( [["RTG", "CTX"], ["Wye", "Triangle"]], "symbol", "screen" )
    .legend.add_block( [["RTG", "CTX"], [undefined, 5]], "dash", "fit" )
    .legend.add_block( [[], []], "colour", "drug_and_cell_line" );
...

Click on all the yellow bubbles (), going from top to bottom, to see explanations of the code.


Here, we initialize and place the heampap. This heatmap will have two values per cell and the resulting colour will be influenced by them separately. Another difference from previously described heatmaps will be the reaction to click. Now cells are not clicked, but rather marked, allowing user to select several cells at once. By default, this behaviour is activated, when Shift is pressed, but here we are using it as a reaction to a usual click as well.

Here, the row IDs are drug names and column IDs are cell lines.

Here, we have not one, but two values. Both are average inhibitions, but measured by two different assays. RTG shows the decrease of number of metabolically active cells compared to the control, while CTX reflects the proportion of dead cells. In this heatmap we want to show the two values simultaniously and therefore both are passed to the heatmap.

By now you probably have already noticed the label that appears each time the cursor hovers onver a cell, point or line. The informText property sets the HTML content of this label. The default setting assumes that there is only one value for a cell, and can't properly work with an array of two values. So we redefine it here.

Any chart in the linked-charts library has two modes: clicking and selecting. In clicking mode each click on an element triggers on_click function. In selecting mode a click selects or deselects the element. You can switch between the modes in the instrument panel. Also you can keep the Shif button pressed to select an element instead of clicking on it.

Here, we additionally make on_click work as selector/deselector, using the mark method.

The markedUpdated property is connected with the mark method. A function, defined here, is called each time a set of marked points or cells is changed. By default, it's an empty function. Here, we will define a function that updates the layers on the curveFit plot, so that for each selected cell, we get two layers: one with dots and the other with lines.

Here, we generate IDs for all the layers we want to have on the curve fit plot. We take all the marked cells IDs and make a combination of a drug name, a cell line name and a type of the layer we want to have.

get_marked returns an array of IDs (or pairs of row and column IDs) for all the marked elements of the chart.

Here, we define all the dinamic properties of the curveFit chart.

Here, we define all the layers, providing a set of their IDs. This property works like elementIds or rowIds. The layers are added or removed to fit the given array of IDs.

Here, we define a type of each layer. In most cases it happens automatically, since layers are initialised by type-specific functions. But here we add layers by updating the set of layer IDs and therefore the type information is required for proper layer initialisation.

The linked-charts library allows the user to manipulate several layers at once. To this end one need to first select them. The select_layers method takes an array of layer IDs and returns a selection of layers. This selection is similar to any chart object with the properties of all the selected layers. The only difference is that any property callback function get the layer ID as its first argument.

Here, we select all the existing layers.

Here, we define the number of elements for each layer depending on its type. A scatter plot will have ten points, a line chart will contain two lines.

Here, we set x and y coordinates for all our scatter layers. Line charts doesn't have x and y properties, so we don't need to care about separating layers of two different types from each other. These properties will be set only for the scatters.

Like in the previous examples, here, on x-axis we have values from 0 to 4 that correspond to one of the tested concentrations. y-axis shows inhibition values.

Here, we set a symbol for each assay. Inhibition values, measured by RTG will be shown as wyes and by CTX - as triangles. The linked-charts library can use the symbols supported by d3.symbol() function. They are "Circle", "Cross", "Diamond", "Square", "Star", "Triangle", "Wye".

This property is also specific only to the scatters, so will be applied only to this type of layers.

Here, we define the lines for curve fits almost the same way is has been done in all previous examples. The only difference here is that we now use both assays and therefore have slightly modified the get_curveFit function so that it, besides a drug name and a cell line name, will also take an assay name.

The dasharray property can make lines dashed. It sets the stroke-dasharray attribute of the lines. Here, we make the line for RTG solid and for CTX - dashed.

Here, we set the colours for both scatter and line charts. Both have the colour property, and so we don't need to do it separately. We use one of the predefined colour sets, provided by the d3 library.

To define a legend one need to provide either a scale function that will transform legend elements (colour, size, symbol etc.) into a label. Another option is an array with two columns: one with elements (colours in this case) and the other with the corresponding labels. Here, we define such an array. We construct an array of all selected drug-cell line combinations and an array of all the used colours.

Here, we update the curveFit plot to display all the changes that were made to the layers.

Instead of updating the entire legend one can just update the scale of a certain block. The updateScale function takes a scale (can be a function or a two-dimensional array) and a legend block ID as arguments.

A colourSlider is another type of basic types of charts implemented in the linked-charts library. It's not a selfsuficient chart, but it can be linked to any continuous colour scale of any of the plots, to allow an easy and interactive way of changing the contrast and the midpoint of the scale.

Here, both sliders are defined in almost identical way, so we explain setting only one of them. The full code you can finde at the bottom of the page.

Here, we define the colour scale that a colour slider will then modify. This scale will take values from 0 to 50 and change from black to green.

By setting the on_change property the user defines, what should happen if one of the pointers of a colour slider has been moved. Here, we would like to have colours of all the cells changed. The most obvious way for that to use the update function. But in this case we know for sure that it's only the colour that can change. So we don't need all the heatmap elements to be recalculated and rerendered, since with larger charts this may take a considerable amount of time. So we use the updateCellColour method instead.

Here, we define a title of the slider.

Here, we change the left margin of the slider to make it nicely aligned under the heatmap and leave space for the title.

Here, we set the position (x and y coordinates) of the title and its size in pixels.

Here, we define colour for each heatmap cell. We can't just take a transformed colourScale as it has been done in the previous example, since our desires colour is a combination of two. So instead from each colourSlider we take the sigmoid transformation of the straightColourScale and use it to get the resulting colour by combining red and green channels.

Here, we define an empty plot where the individual inhibition values and fitted curves will be shown. We set only global chart properties, such as size, axes titles, etc. the same way it has been done previously. You can find the full code at the bottom of the page.

All the blocks of a legend are placed in a rectangle grid. By default, the size of the grid is estimated automatically, but here we fix the number of columns of the grid to 1.

Here, we add to the legend two blocks to show which symbol and which type of line corresponds to which type of assay.

Here, we add an empty block to the legend. This block will be updated after selecting cells of the heatmap.

Show/hide full code