Chapter 7 Advanced layout

7.1 Zooming of sectors

In this section, we will introduce how to zoom sectors and put the zoomed sectors at the same track as the original sectors.

Under the default settings, width of sectors are calculated according to the data range in corresponding categories. Normally it is not a good idea to manually modify the default sector width since it reflects useful information of your data. However, sometimes manually modifying the width of sectors can make more advanced plots, e.g. zoomings.

The basic idea for zooming is to put original sectors on part of the circle and put the zoomed sectors on the other part, so that in the original sectors, widths are still proportional to their data ranges, and in the zoomed sectors, the widths are also proportional to the data ranges in the zoomed sectors.

This type of zooming is rather simple to implement. All we need to do is to copy the data which corresponds to the zoomed sectors, assign new category names to them and append to the original data. The good thing is since the data in the zoomed sectors is exactly the same as the original sectors, if you treat them as normal categories, the graphics will be exactly the same as in the original sectors, but with x direction zoomed.

Following example shows more clearly the basic idea of this “horizontal” zooming.

We first generate a data frame with six categories.

set.seed(123)
df = data.frame(
    sectors = sample(letters[1:6], 400, replace = TRUE),
    x = rnorm(400),
    y = rnorm(400),
    stringsAsFactors = FALSE
)

We want to zoom sector a and the first 10 points in sector b. First we extract these data and format as a new data frame.

zoom_df_a = df[df$sectors == "a", ]
zoom_df_b = df[df$sectors == "b", ]
zoom_df_b = zoom_df_b[order(zoom_df_b[, 2])[1:10], ]
zoom_df = rbind(zoom_df_a, zoom_df_b)

Then we need to change the sector names in the zoomed data frame. Here we just simply add “zoom_” prefix to the original names to show that they are “zoomed” sectors. After that, it is attached to the original data frame.

zoom_df$sectors = paste0("zoom_", zoom_df$sectors)
df2 = rbind(df, zoom_df)

In this example, we will put the original cells in the left half of the circle and the zoomed sectors in the right. As we have already mentioned before, we simply normalize the width of normal sectors and normalize the width of zoomed sectors separately. Note now the sum of the sector width for the original sectors is 1 and the sum of sector width for the zoomed sectors is 1, which means these two types of sectors have their own half circle.

You may notice the sum of the sector.width is not idential to 1. This is fine, they will be further normalized to 1 internally.

Strictly speaking, since the gaps between sectors are not taken into consideration, the width of the original sectors are not exactly 180 degree, but the real value is quite close to it.

xrange = tapply(df2$x, df2$sectors, function(x) max(x) - min(x))
normal_sector_index = unique(df$sectors)
zoomed_sector_index = unique(zoom_df$sectors)
sector.width = c(xrange[normal_sector_index] / sum(xrange[normal_sector_index]), 
                 xrange[zoomed_sector_index] / sum(xrange[zoomed_sector_index]))
sector.width
##         c         f         b         e         d         a    zoom_a    zoom_b 
## 0.1774352 0.1649639 0.1685275 0.1662047 0.1668505 0.1560182 0.7237996 0.2762004

What to do next is just to make the circular plot in the normal way. All the graphics in sector a and b will be automatically zoomed to sector “zoom_a” and “zoom_b.”

In following code, since the sector names are added outside the first track, points.overflow.warning is set to FALSE to turn off the warning messages.

circos.par(start.degree = 90, points.overflow.warning = FALSE)
circos.initialize(df2$sectors, x = df2$x, sector.width = sector.width)
circos.track(df2$sectors, x = df2$x, y = df2$y, 
    panel.fun = function(x, y) {
    circos.points(x, y, col = "red", pch = 16, cex = 0.5)
    circos.text(CELL_META$xcenter, CELL_META$cell.ylim[2] + mm_y(2), 
        CELL_META$sector.index, niceFacing = TRUE)
})

Adding links from original sectors to zoomed sectors is a good idea to show where the zooming occurs (Figure 7.1). Notice that we manually adjust the position of one end of the sector b link.

circos.link("a", get.cell.meta.data("cell.xlim", sector.index = "a"),
    "zoom_a", get.cell.meta.data("cell.xlim", sector.index = "zoom_a"),
    border = NA, col = "#00000020")
circos.link("b", c(zoom_df_b[1, 2], zoom_df_b[10, 2]),
    "zoom_b", get.cell.meta.data("cell.xlim", sector.index = "zoom_b"),
    rou1 = get.cell.meta.data("cell.top.radius", sector.index = "b"),
    border = NA, col = "#00000020")
circos.clear()
Zoom sectors.

Figure 7.1: Zoom sectors.

Chapter 13 introduces another type of zooming by combining two circular plots.

7.2 Visualize part of the circle

canvas.xlim and canvas.ylim parameters in circos.par() are useful to generate plots only in part of the circle. As mentioned in previews chapters, the circular plot is always drawn in a canvas where x values range from -1 to 1 and y values range from -1 to 1. Thus, if canvas.xlim and canvas.ylim are all set to c(0, 1), which means, the canvas is restricted to the right top part, then only sectors between 0 to 90 degree are visible (Figure 7.2).

One quarter of the circle.

Figure 7.2: One quarter of the circle.

To make the right plot in Figure 7.2, we only need to set one sector in the layout and set gap.after to 270. (One sector with gap.after of 270 degree means the width of this sector is exactly 90 degree.)

circos.par("canvas.xlim" = c(0, 1), "canvas.ylim" = c(0, 1),
    "start.degree" = 90, "gap.after" = 270)
sectors = "a" # this is the name of your sector
circos.initialize(sectors = sectors, xlim = ...)
...

Similar idea can be applied to the circle where in some tracks, only a subset of cells are needed. Gererally there are two ways. The first way is to create the track and add graphics with subset of data that only corresponds to the cells that are needed. And the second way is to create an empty track first and customize the cells by circos.update(). Following code illustrates the two methods (Figure 7.3).

sectors = letters[1:4]
circos.initialize(sectors, xlim = c(0, 1))

# directly specify the subset of data
df = data.frame(sectors = rep("a", 100),
                x = runif(100),
                y = runif(100))
circos.track(df$sectors, x = df$x, y = df$y, 
    panel.fun = function(x, y) {
        circos.points(x, y, pch = 16, cex = 0.5)
})

# create empty track first then fill graphics in the cell
circos.track(ylim = range(df$y), bg.border = NA)
circos.update(sector.index = "a", bg.border = "black")
circos.points(df$x, df$y, pch = 16, cex = 0.5)

circos.track(sectors = sectors, ylim = c(0, 1))
circos.track(sectors = sectors, ylim = c(0, 1))
Show subset of cells in tracks.

Figure 7.3: Show subset of cells in tracks.

circos.clear()

7.3 Combine multiple circular plots

circlize finally makes the circular plot in the base R graphic system. Seperated circular plots actually can be put in a same page by some tricks from the base graphic system. Here the key is par(new = TRUE) which allows to draw a new figure as a new layer directly on the previous canvas region. By setting different canvas.xlim and canvas.ylim, it allows to make more complex plots which include more than one circular plots.

Folowing code shows how the two independent circualr plots are added and nested. Figure 7.4 illustrates the invisible canvas coordinate and how the two circular plots overlap.

sectors = letters[1:4]
circos.initialize(sectors, xlim = c(0, 1))
circos.track(ylim = c(0, 1), panel.fun = function(x, y) {
    circos.text(0.5, 0.5, "outer circos", niceFacing = TRUE)
})
circos.clear()

par(new = TRUE) # <- magic
circos.par("canvas.xlim" = c(-2, 2), "canvas.ylim" = c(-2, 2))
sectors = letters[1:3]
circos.initialize(sectors, xlim = c(0, 1))
circos.track(ylim = c(0, 1), panel.fun = function(x, y) {
    circos.text(0.5, 0.5, "inner circos", niceFacing = TRUE)
})
circos.clear()
Nested circular plots.

Figure 7.4: Nested circular plots.

The second example (Figure 7.5) makes a plot where two circular plots separate from each other. You can use technique introduced in Section 7.2 to only show part of the circle, select proper canvas.xlim and canvas.ylim, and finally arrange the two plots into one page. The source code for generating Figure 7.5 is at https://github.com/jokergoo/circlize_book/blob/master/src/intro-20-separated.R.

Two separated circular plots

Figure 7.5: Two separated circular plots

The third example is to draw cells with different radius (Figure 7.6). In fact, it makes four circular plots where only one sector for each plot is plotted.

sectors = letters[1:4]
lim = c(1, 1.1, 1.2, 1.3)
for(i in 1:4) {
    circos.par("canvas.xlim" = c(-lim[i], lim[i]), 
        "canvas.ylim" = c(-lim[i], lim[i]), 
        "track.height" = 0.4)
    circos.initialize(sectors, xlim = c(0, 1))
    circos.track(ylim = c(0, 1), bg.border = NA)
    circos.update(sector.index = sectors[i], bg.border = "black")
    circos.points(runif(10), runif(10), pch = 16)
    circos.clear()
    par(new = TRUE)
}
par(new = FALSE)
Cells with differnet radius.

Figure 7.6: Cells with differnet radius.

Note above plot is different from the example in Figure 7.3. In Figure 7.3, cells both visible and invisible all belong to a same track and they are in a same circular plot, thus they should have same radius. But for the example here, cells have different radius and they belong to different circular plot.

In chapter 13, we use this technique to implement zoomings by combining two circular plots.

7.4 Arrange multiple plots

circlize is implemented in the base R graphic system, thus, you can use layout() or par("mfrow")/par("mfcol") to arrange multiple circular plots in one page (Figure 7.7).

layout(matrix(1:9, 3, 3))
for(i in 1:9) {
    sectors = 1:8
    par(mar = c(0.5, 0.5, 0.5, 0.5))
    circos.par(cell.padding = c(0, 0, 0, 0))
    circos.initialize(sectors, xlim = c(0, 1))
    circos.track(ylim = c(0, 1), track.height = 0.05,
        bg.col = rand_color(8), bg.border = NA)
    for(i in 1:20) {
        se = sample(1:8, 2)
        circos.link(se[1], runif(2), se[2], runif(2), 
            col = rand_color(1, transparency = 0.4), border = NA)
    }
    circos.clear()
}
Arrange multiple circular plots.

Figure 7.7: Arrange multiple circular plots.