library(knitr) knitr::opts_chunk$set( error = FALSE, tidy = FALSE, message = FALSE, warning = FALSE, fig.width = 5, fig.height = 5, fig.align = "center", fig.retina = 2 ) knitr::knit_hooks$set(pngquant = knitr::hook_pngquant) knitr::opts_chunk$set( message = FALSE, dev = "ragg_png", fig.align = "center", pngquant = "--speed=10 --quality=30" ) options(width = 100)
library(spiralize) library(cowplot) library(GetoptLong)
In this vignette, I describe the package spiralize which visualizes data along an Archimedean spiral. It has two major advantages for visualization:
In polar coordinates ($r$, $\theta$), the Archimedean spiral has the following form:
$$ r = b \cdot \theta $$
where $b$ controls the distance between two loops. The radial distance between two neighbouring loops for a given $\theta$ is:
$$ d(\theta) = r(\theta + 2\pi) - r(\theta) = b \cdot (\theta + 2\pi) - b \cdot \theta = b \cdot 2\pi $$
This shows the radial distance between two neighbouring loops is independent to the value of $\theta$ and is a constant value. The following figure demonstrates an Archimedean spiral with 4 loops ($\theta \in [0, 8\pi]$).
theta = seq(0, 360*4, by = 1)/180*pi b = 1/2/pi r = theta*b df = spiralize:::polar_to_cartesian(theta, r) abs_max_x = max(abs(df$x)) abs_max_y = max(abs(df$y)) grid.newpage() padding = unit(c(10, 10), "mm") pushViewport(viewport( width = unit(1, "snpc") - padding[1], height = unit(1, "snpc") - padding[2], xscale = c(-abs_max_x, abs_max_x), yscale = c(-abs_max_y, abs_max_y))) d = seq(0, 360, by = 30) if(d[length(d)] == 360) d = d[-length(d)] dm = matrix(nrow = length(d), ncol = 4) for(i in seq_along(d)) { r0 = max(r + b*2*pi)*1.1 dm[i, ] = c(0, 0, cos(d[i]/180*pi)*r0, sin(d[i]/180*pi)*r0) } grid.segments(dm[, 1], dm[, 2], dm[, 3], dm[, 4], default.units = "native", gp = gpar(col = "grey", lty = 3)) grid.lines(df$x, df$y, default.units = "native") grid.segments(2, 0, 3, 0, arrow = arrow(angle = 20, length = unit(2, "mm"), ends = "both"), default.units = "native") grid.text("d", unit(2.5, "native"), unit(0, "native") + unit(1, "mm"), just = "bottom") popViewport()
Note $\theta$ can also be negative values where the spiral is mirrored by y-axis
(in Cartesian coordinates). In spiralize, we only consider $\theta$ as
positive values. The mirrored spiral can be set by the flip
argument which is introduced later
in this vignette.
Since the distance between any two neighbouring loops for any given $\theta$ is constant, it is a ideal place to put tracks along the spiral where the tracks have identical radial heights everywhere. Later the tracks can be served as virtual coordinate systems to map to data. This is why the package is called "spiralize" (to transform a normal Cartesian coordinate system to a curved spiral shape). The following two figures demonstrate a spiral with one track and with two tracks. The red line is the spiral itself. The spiral ranges between $\pi/2$ and $6\pi$. It is easy to see the upper border of each track is also a spiral but with an offset $a$:
$$ r = a + b \cdot \theta $$
where $a$ is the offset to the "Base spiral" (the red spiral in the following plots).
grid.newpage() pushViewport(viewport(x = 0.25, width = 0.5)) spiral_initialize(start = 90, end = 360*3, newpage = FALSE) spiral_track(height = 0.45) spiral_lines(TRACK_META$xlim, rep(get_track_data("ymin"), 2), gp = gpar(col = "red")) spiral_text(2/11, 0.5, "track 1") spiral_clear() popViewport() pushViewport(viewport(x = 0.75, width = 0.5)) spiral_initialize(start = 90, end = 360*3, newpage = FALSE) spiral_track(height = 0.45) spiral_lines(TRACK_META$xlim, rep(get_track_data("ymin"), 2), gp = gpar(col = "red")) spiral_text(2/11, 0.5, "track 1") spiral_track(height = 0.45, background_gp = gpar(fill = "#CCCCCC")) spiral_text(2/11, 0.5, "track 2") spiral_clear() popViewport()
Denote the maximal radius of the spiral as $d_{max} = b \cdot \theta_{max}$, and denote the length of the spiral as $l$ (which has a complex form, see https://downloads.imagej.net/fiji/snapshots/arc_length.pdf), we can treat $2d_{max}$ as the resolution of the visualization applied in the normal Cartesian coordinate system and $l$ as the resolution of the visualization applied on the spiral. Then the ratio of the two resolutions is:
$$ ratio = \frac{l}{2d_{max}} $$
E.g., for a spiral with 5 loops ($\theta_{max} = 10\pi$), the ratio is 7.89, which means the spiral improves the resolution of visualization almost to 8 folds. Generally, the ratio increases almost linearly to the number of loops.
spiral_initialize() s = current_spiral()
ratio = function(k) { theta = pi*2*k b = 1/2/pi r = b*theta len = s$spiral_length(theta) len/2/r } k = 1:20 plot(k, ratio(k), xlab = "number of loops in the spiral", ylab = "ratio", main = "ratio of the resolution between\nspiral and normal Cartesian system")
The relationship between ratio and $\theta$ has the following form:
$$ ratio = \frac{\mathrm{ln}(\theta + \sqrt{1 + \theta^2})}{4\theta} + \frac{\sqrt{1 + \theta^2}}{4} $$
When $\theta$ gets large,
$$ ratio \approx \frac{\mathrm{ln}(\theta + \theta)}{4\theta} + \frac{\theta}{4} \approx \frac{\theta}{4} $$
Denote $k$ as the number of loops, i.e. $\theta = 2\pi \cdot k$, then,
$$ ratio \approx \frac{\theta}{4} = \frac{\pi}{2} \cdot k $$
The function spiral_initialize()
is used to intialize the spiral. Arguments start
and end
control
the angular range of the spiral. Here the values should be in degrees and they are converted to radians internally.
In spiralize, the parameter $b$ in the spiral equation $r = b \cdot \theta$ is set to $b = 1/2\pi$, so that the distance between two neighbouring loops is $d = 1$. Denote $\theta_e$ as the end angle (in radians) of the spiral, the ranges of the viewport (under grid graphics system) on both x-axis and y-axis that draw the spiral are $[-x, x]$ where
$$x = b \cdot \theta_e + d = 1/2\pi \cdot \theta_e + 1$$
The following two plots demonstrate different values of start
and end
. Also as shown in the following example code,
I suggest to set the values of start
and end
in a form of 360*a + b
, e.g. 360*4 + 180
, so that it is straighforward
to know the positions in the polar coordinates and how many loops there are in the spiral (I think people should feel more natural with degrees than radians).
# the left plot spiral_initialize(start = 90, end = 360) spiral_track() # the right plot spiral_initialize(start = 180, end = 360*4 + 180) spiral_track()
grid.newpage() pushViewport(viewport(x = 0.25, width = 0.5)) spiral_initialize(start = 90, end = 360, newpage = FALSE) spiral_track() spiral_clear() popViewport() pushViewport(viewport(x = 0.75, width = 0.5)) spiral_initialize(start = 180, end = 360*4 + 180, newpage = FALSE) spiral_track() spiral_clear() popViewport()
Argument flip
controls how to flip the spiral. It accpets one of the four values: "none"
/"horizontal"
/"vertical"
/"both"
.
Examples are as follows. In the examples, I additionally add the axes in the tracks to show in which direction the data extends along
the spiral. I also manually adjust the height of the track to give enough space for axes.
# the top left plot spiral_initialize(flip = "none") # default spiral_track(height = 0.6) spiral_axis() # the top right plot spiral_initialize(flip = "horizontal") spiral_track(height = 0.6) spiral_axis() # the bottom left plot spiral_initialize(flip = "vertical") spiral_track(height = 0.6) spiral_axis() # the bottom right plot spiral_initialize(flip = "both") spiral_track(height = 0.6) spiral_axis()
p1 = grid.grabExpr({ spiral_initialize() spiral_track(height = 0.6) spiral_axis() grid.text("flip = 'none'", 0, 1, just = c("left", "top"), gp = gpar(fontsize = 14)) }) p2 = grid.grabExpr({ spiral_initialize(flip = "horizontal") spiral_track(height = 0.6) spiral_axis() grid.text("flip = 'horizontal'", 0, 1, just = c("left", "top"), gp = gpar(fontsize = 14)) }) p3 = grid.grabExpr({ spiral_initialize(flip = "vertical") spiral_track(height = 0.6) spiral_axis() grid.text("flip = 'vertical'", 0, 1, just = c("left", "top"), gp = gpar(fontsize = 14)) }) p4 = grid.grabExpr({ spiral_initialize(flip = "both") spiral_track(height = 0.6) spiral_axis() grid.text("flip = 'both'", 0, 1, just = c("left", "top"), gp = gpar(fontsize = 14)) }) plot_grid(p1, p2, p3, p4, nrow = 2)
More easily, you can directly set clockwise = TRUE
to change the orientation of the
spiral. Compare the following plots:
# the top left plot spiral_initialize(start = 45 + 360) # default spiral_track(height = 0.6) spiral_axis() # the top right plot spiral_initialize(start = 45 + 360, clockwise = TRUE) spiral_track(height = 0.6) spiral_axis() # the bottom left plot spiral_initialize(start = 135 + 360) spiral_track(height = 0.6) spiral_axis() # the bottom right plot spiral_initialize(start = 135 + 360, clockwise = TRUE) spiral_track(height = 0.6) spiral_axis()
p1 = grid.grabExpr({ spiral_initialize(start = 45 + 360) spiral_track(height = 0.6) spiral_axis() grid.text("default", 0, 1, just = c("left", "top"), gp = gpar(fontsize = 14)) }) p2 = grid.grabExpr({ spiral_initialize(start = 45 + 360, clockwise = TRUE) spiral_track(height = 0.6) spiral_axis() grid.text("clockwise = TRUE", 0, 1, just = c("left", "top"), gp = gpar(fontsize = 14)) }) p3 = grid.grabExpr({ spiral_initialize(start = 135 + 360) spiral_track(height = 0.6) spiral_axis() grid.text("default", 0, 1, just = c("left", "top"), gp = gpar(fontsize = 14)) }) p4 = grid.grabExpr({ spiral_initialize(start = 135 + 360, clockwise = TRUE) spiral_track(height = 0.6) spiral_axis() grid.text("clockwise = TRUE", 0, 1, just = c("left", "top"), gp = gpar(fontsize = 14)) }) plot_grid(p1, p2, p3, p4, nrow = 2)
Argument scale_by
controls how to linearly scale the data on the
spiral. It allows value of "angle"
or "curve_length"
(or for short,
"curve"
). "angle"
means equal difference on data corresponds to equal
difference of angles in the polar coordinates. "curve_length"
means equal
difference on data corresponds to equal difference of the length of the
spiral. Observe how the axis ticks distribute in the following two plots. Also
the polar lines are removed for scale_by = "curve_length"
.
# the left plot spiral_initialize(scale_by = "angle") # default spiral_track(height = 0.6) spiral_axis() # the right plot spiral_initialize(scale_by = "curve_length") spiral_track(height = 0.6) spiral_axis()
p1 = grid.grabExpr({ spiral_initialize(scale_by = "angle") spiral_track(height = 0.6) spiral_axis() grid.text("scale_by = 'angle'", 0, 1, just = c("left", "top"), gp = gpar(fontsize = 14)) }) p2 = grid.grabExpr({ spiral_initialize(scale_by = "curve_length") spiral_track(height = 0.6) spiral_axis() grid.text("scale_by = 'curve_length'", 0, 1, just = c("left", "top"), gp = gpar(fontsize = 14)) }) plot_grid(p1, p2)
The following heatmaps might be clearer to demonstrate the difference between "angle"
and "curve_length"
. In this
example, each grid has the equal bin size of the data.
make_plot = function(scale_by) { n = 100 col = circlize::colorRamp2(c(0, 0.5, 1), c("blue", "white", "red")) spiral_initialize(xlim = c(0, n), scale_by = scale_by) spiral_track(height = 0.9) x = runif(n) spiral_rect(1:n - 1, 0, 1:n, 1, gp = gpar(fill = col(x), col = NA)) grid.text(qq("scale_by = '@{scale_by}'"), 0, 1, just = c("left", "top"), gp = gpar(fontsize = 14)) } p1 = grid.grabExpr(make_plot("angle")) p2 = grid.grabExpr(make_plot("curve_length")) plot_grid(p1, p2)
As you can see, when scale_by
is set to "angle"
, in outer loops, even when the actually difference
on data is the same, the physical widths are larger than these in inner loops. Nevertheless, when the data is
time series or periodic, "angle"
is the proper choice because it is easy to directly compare between loops
which are the same time points over different periods. As a comparison, "curve_length"
won't provide any
periodic information.
The spiral grows from inner loops to outer loops, thus, by default, data increases from
the inner loops as well. This can be reversed by setting argument reverse = TRUE
. See
the following example and also observe the axes. The red arrows indicate the direction of axes.
# the left plot spiral_initialize(reverse = FALSE) # default spiral_track() spiral_arrow(0.2, 0.8, gp = gpar(fill = "red")) spiral_axis() # the right plot spiral_initialize(reverse = TRUE) spiral_track() spiral_arrow(0.2, 0.8, gp = gpar(fill = "red")) spiral_axis()
p1 = grid.grabExpr({ spiral_initialize(reverse = FALSE) spiral_track() spiral_arrow(0.2, 0.8, gp = gpar(fill = "red")) spiral_axis() grid.text("reverse = FALSE", 0, 1, just = c("left", "top"), gp = gpar(fontsize = 14)) }) p2 = grid.grabExpr({ spiral_initialize(reverse = TRUE) spiral_track() spiral_arrow(0.2, 0.8, gp = gpar(fill = "red")) spiral_axis() grid.text("reverse = TRUE", 0, 1, just = c("left", "top"), gp = gpar(fontsize = 14)) }) plot_grid(p1, p2)
To map data to spiral, argument xlim
should be set which corresponds to data range on x-axis.
Observe the axes in the following plots.
# the left plot spiral_initialize(xlim = c(0, 1000)) spiral_track(height = 0.6) spiral_axis() # the right plot spiral_initialize(xlim = c(-1000, 1000)) spiral_track(height = 0.6) spiral_axis()
p1 = grid.grabExpr({ spiral_initialize(xlim = c(0, 1000)) spiral_track(height = 0.6) spiral_axis() }) p2 = grid.grabExpr({ spiral_initialize(xlim = c(-1000, 1000)) spiral_track(height = 0.6) spiral_axis() }) plot_grid(p1, p2)
Under "angle" mode, the number of loops can also be controlled by argument period
which controls the length
of data a spiral loop corresponds to. Note in this case, argument end
is ignored and the value for end
is
internally recalculated. See the following example:
# the left plot spiral_initialize(xlim = c(0, 1), period = 1/3) spiral_track(height = 0.6) # the right plot spiral_initialize(xlim = c(0, 1), period = 2) spiral_track(height = 0.6)
p1 = grid.grabExpr({ spiral_initialize(xlim = c(0, 1), period = 1/3) spiral_track(height = 0.6) }) p2 = grid.grabExpr({ spiral_initialize(xlim = c(0, 1), period = 2) spiral_track(height = 0.6) }) plot_grid(p1, p2)
After the spiral is intialized, next we can add tracks along it. Argument height
controls
the height of the track. The value of height
is a value between 0 and 1 which is the fraction of the distance
between two neighbouring loops in the spiral. In the following left plot, I add black border to the track
by setting the argument background_gp
.
# the left plot spiral_initialize() spiral_track(height = 1, background_gp = gpar(col = "black")) # the right plot spiral_initialize() spiral_track(height = 0.5)
p1 = grid.grabExpr({ spiral_initialize() spiral_track(height = 1, background_gp = gpar(col = "black")) grid.text("height = 1", 0, 1, just = c("left", "top"), gp = gpar(fontsize = 14)) }) p2 = grid.grabExpr({ spiral_initialize() spiral_track(height = 0.5) grid.text("height = 0.5", 0, 1, just = c("left", "top"), gp = gpar(fontsize = 14)) }) plot_grid(p1, p2)
Multiple tracks can be added sequentially. Just note the sum of heights of all tracks should not exceed 1.
spiral_initialize() spiral_track(height = 0.4, background_gp = gpar(fill = 2)) spiral_track(height = 0.2, background_gp = gpar(fill = 3)) spiral_track(height = 0.1, background_gp = gpar(fill = 4))
The value for height
can also be unit
object.
spiral_initialize() spiral_track(height = unit(1, "cm"))
Data range on y-axis is specified by the argument ylim
. In the following example, I also add a y-axis
by spiral_yaxis()
.
spiral_initialize() spiral_track(ylim = c(0, 100)) spiral_yaxis()
Direction of y-axis is by default pointing to the outside of spirals. This
direction can be reversed by setting argument reverse_y = TRUE
in
spiral_track()
, but in applications it is rarely used.
spiral_track(reverse_y = TRUE)
Tracks are created with data ranges on both x-axis and y-axis. Now the tracks can be thought as normal Cartesian coordinates. There are following low-level graphics functions so that complex plots can be easily constructed by combining these low-level graphics functions.
Like other graphics functions e.g. points()
or grid.points()
, the "spiral graphics functions" also
accept locations on x-axis and y-axis for data points. spiral_points()
draws points
in the spiral track.
spiral_initialize() # by default xlim = c(0, 1) spiral_track() # by default ylim = c(0, 1) spiral_points(x = runif(1000), y = runif(1000))
Adding lines with spiral_lines()
is also straightforward:
x = sort(runif(1000)) y = runif(1000) spiral_initialize() spiral_track() spiral_lines(x, y)
Argument type
can be set to "h"
so that vertical lines (or radial lines
if you take polar coordinates as reference) are drawn to the baseline for each
data point.
# the left plot spiral_initialize() spiral_track() spiral_lines(x, y, type = "h") # the right plot spiral_initialize() spiral_track() spiral_lines(x, y, type = "h", baseline = 0.5, gp = gpar(col = ifelse(y > 0.5, "red", "blue")))
p1 = grid.grabExpr({ spiral_initialize() spiral_track() spiral_lines(x, y, type = "h") }) p2 = grid.grabExpr({ spiral_initialize() spiral_track() spiral_lines(x, y, type = "h", baseline = 0.5, gp = gpar(col = ifelse(y > 0.5, "red", "blue"))) }) plot_grid(p1, p2)
Argument area
can be set to TRUE
so that area under the lines can be filled with a certain color.
spiral_initialize() spiral_track() spiral_lines(x, y, area = TRUE, gp = gpar(fill = 2, col = NA))
Note you can also set baseline
with area = TRUE
, however, you cannot set different colors for the area
above the baseline and below the baseline. Consider to use spiral_bars()
or spiral_horizon()
for this scenario.
spiral_segments()
draws a list of segments.
n = 1000 x0 = runif(n) y0 = runif(n) x1 = x0 + runif(n, min = -0.01, max = 0.01) y1 = 1 - y0 spiral_initialize(xlim = range(c(x0, x1))) spiral_track() spiral_segments(x0, y0, x1, y1, gp = gpar(col = circlize::rand_color(n, luminosity = "bright"), lwd = runif(n, 0.5, 3)))
The same as grid.segments()
, you can also set the argument arrow
to add arrows on the segments.
n = 100 x0 = runif(n) y0 = runif(n) x1 = x0 + runif(n, min = -0.01, max = 0.01) y1 = 1 - y0 spiral_initialize(xlim = range(c(x0, x1))) spiral_track() spiral_segments(x0, y0, x1, y1, arrow = arrow(length = unit(2, "mm")), gp = gpar(col = circlize::rand_color(n, luminosity = "bright"), lwd = runif(n, 0.5, 3)))
spiral_rect()
draws rectangles, which is the base function for drawing heatmaps and barplots. The first four
arguments are the coordinates of the bottom left and top right of the rectangles.
n = 1000 require(circlize) spiral_initialize(xlim = c(0, n)) spiral_track(height = 0.9) x1 = runif(n) col1 = circlize::colorRamp2(c(0, 0.5, 1), c("blue", "white", "red")) spiral_rect(1:n - 1, 0, 1:n, 0.5, gp = gpar(fill = col1(x1), col = NA)) x2 = runif(n) col2 = circlize::colorRamp2(c(0, 0.5, 1), c("green", "white", "red")) spiral_rect(1:n - 1, 0.5, 1:n, 1, gp = gpar(fill = col2(x2), col = NA))
spiral_bars()
can draw bars simply from a numeric vector. Bars can also be drawn to a baseline.
x = seq(1, 1000, by = 1) - 0.5 # middle points of bars y = runif(1000) # the left plot spiral_initialize(xlim = c(0, 1000)) spiral_track(height = 0.8) spiral_bars(x, y) # the right plot spiral_initialize(xlim = c(0, 1000)) spiral_track(height = 0.8) spiral_bars(x, y, baseline = 0.5, gp = gpar(fill = ifelse(y > 0.5, 2, 3), col = NA))
x = seq(1, 1000, by = 1) - 0.5 # middle points of bars y = runif(1000) p1 = grid.grabExpr({ spiral_initialize(xlim = c(0, 1000)) spiral_track(height = 0.8) spiral_bars(x, y) }) p2 = grid.grabExpr({ spiral_initialize(xlim = c(0, 1000)) spiral_track(height = 0.8) spiral_bars(x, y, baseline = 0.5, gp = gpar(fill = ifelse(y > 0.5, 2, 3), col = NA)) }) plot_grid(p1, p2)
spiral_bars()
can also draw bars from a matrix, then each column in the matrix correspond to one stack of the bars.
y = matrix(runif(3*1000), ncol = 3) y = y/rowSums(y) spiral_initialize(xlim = c(0, 1000)) spiral_track(height = 0.8) spiral_bars(x, y, gp = gpar(fill = 2:4, col = NA))
Width of bars can be different. You can set a vector to the argument of bar_width
. Note x
always corresponds
to the middle of each bar.
w = runif(100) w = w/sum(w) # width of bars, sum of all width is 1 b = c(0, cumsum(w)) x = (b[1:100] + b[2:101])/2 # middle of each bar y = runif(100) spiral_initialize() spiral_track() spiral_bars(x, y, bar_width = w)
spiral_polygon()
draws polygons. Note the polygon must be closed, which means, the last data point should overlap to the first one.
x0 = sort(runif(200)) x0 = matrix(x0, ncol = 2, byrow = TRUE) x1 = sort(runif(200)) x1 = matrix(x1, ncol = 2, byrow = TRUE) spiral_initialize() spiral_track() for(i in 1:100) { pt1 = circlize:::get_bezier_points(x0[i, 1], 0, x1[i, 1], 1, xlim = c(0, 1), ylim = c(0, 1)) pt2 = circlize:::get_bezier_points(x0[i, 2], 0, x1[i, 2], 1, xlim = c(0, 1), ylim = c(0, 1)) spiral_polygon( c(x0[i, 1], x0[i, 2], pt2[, 1], rev(pt1[, 1]), x0[i, 1]), c(0, 0, pt2[, 2], rev(pt1[, 2]), 0), gp = gpar(fill = rand_color(1, luminosity = "bright"), col = NA) ) }
spiral_text()
draws texts. Argument facing
controls the rotation of texts.
x = seq(0.1, 0.9, length = 26) text = strrep(letters, 6) # the top left plot spiral_initialize() spiral_track() spiral_text(x, 0.5, text, facing = "downward") # default # the bottom left plot spiral_initialize() spiral_track() spiral_text(x, 0.5, text, facing = "inside") # the bottom right plot spiral_initialize() spiral_track() spiral_text(x, 0.5, text, facing = "outside")
x = seq(0.1, 0.9, length = 26) text = strrep(letters, 6) p1 = grid.grabExpr({ spiral_initialize() spiral_track() spiral_text(x, 0.5, text, facing = "downward") grid.text("facing = 'downward'", 0, 1, just = c("left", "top"), gp = gpar(fontsize = 14)) }) p2 = grid.grabExpr({ grid.newpage() }) p3 = grid.grabExpr({ spiral_initialize() spiral_track() spiral_text(x, 0.5, text, facing = "inside") grid.text("facing = 'inside'", 0, 1, just = c("left", "top"), gp = gpar(fontsize = 14)) }) p4 = grid.grabExpr({ spiral_initialize() spiral_track() spiral_text(x, 0.5, text, facing = "outside") grid.text("facing = 'outside'", 0, 1, just = c("left", "top"), gp = gpar(fontsize = 14)) }) plot_grid(plot_grid(p1, p2), plot_grid(p3, p4), nrow = 2)
Text can also be set to "clockwise"
or "reverse_clockwise"
:
x = seq(0.1, 0.9, length = 26) # the left plot spiral_initialize() spiral_track() spiral_text(x, 0.5, "aaaa", facing = "clockwise") # the right plot spiral_initialize() spiral_track() spiral_text(x, 0.5, "aaaa", facing = "reverse_clockwise")
x = seq(0.1, 0.9, length = 26) p1 = grid.grabExpr({ spiral_initialize() spiral_track() spiral_text(x, 0.5, "aaaa", facing = "clockwise") grid.text("facing = 'clockwise'", 0, 1, just = c("left", "top"), gp = gpar(fontsize = 14)) }) p2 = grid.grabExpr({ spiral_initialize() spiral_track() spiral_text(x, 0.5, "aaaa", facing = "reverse_clockwise") grid.text("facing = 'reverse_clockwise'", 0, 1, just = c("left", "top"), gp = gpar(fontsize = 14)) }) plot_grid(p1, p2)
For long texts, facing
can be set to "curved_inside"
or "curved_outside"
so that curved
texts are draw along the spiral.
x = seq(0.1, 0.9, length = 10) text = rep(paste(letters, collapse = ""), 10) # the left plot spiral_initialize() spiral_track() spiral_text(x, 0.5, text, facing = "curved_inside") # the right plot spiral_initialize() spiral_track() spiral_text(x, 0.5, text, facing = "curved_outside")
x = seq(0.1, 0.9, length = 10) text = rep(paste(letters, collapse = ""), 10) p1 = grid.grabExpr({ spiral_initialize() spiral_track() spiral_text(x, 0.5, text, facing = "curved_inside") grid.text("facing = 'curved_inside'", 0, 1, just = c("left", "top"), gp = gpar(fontsize = 14)) }, width = 5, height = 5) p2 = grid.grabExpr({ spiral_initialize() spiral_track() spiral_text(x, 0.5, text, facing = "curved_outside") grid.text("facing = 'curved_outside'", 0, 1, just = c("left", "top"), gp = gpar(fontsize = 14)) }, width = 5, height = 5) plot_grid(p1, p2)
Calculation of positions of letters of the curved text depends on the size of current graphics device. When the device
changes its size, the positions of letters will not be correct and you need to regenerate the plot. Also
users need to be careful when using grid.grabExpr()
to capture the plot. By default grid.grabExpr()
captures
graphics output in a device with 7inch x 7inch. Users might need to manually set the device size to make sure
the curved texts are not affected.
In the next example, I use grid.grabExpr()
to capture two spiral plots with curved texts. Later the two plots are merged
with using the cowplot package and the final merged plot is saved in a PDF with 10 inches width and 5 inches height.
I manually set the device size in the two grid.grabExpr()
calls so that the size of the place where the graphics are captured
is the same as the size of the place where they are finally drawn.
p1 = grid.grabExpr({ spiral_initialize() spiral_track() spiral_text(x, 0.5, text, facing = "curved_inside") }, width = 5, height = 5) p2 = grid.grabExpr({ spiral_initialize() spiral_track() spiral_text(x, 0.5, text, facing = "curved_outside") }, width = 5, height = 5) pdf(..., width = 10, height = 5) plot_grid(p1, p2) dev.off()
one last thing for drawing text is that the argument nice_facing
can be set to TRUE
so that the rotation
of texts are automatically adjusted so that they are easy to read, i.e. all the texts always face the lower part of the polar coordinate system.
spiral_aixs()
draws axis along the spiral. So it is the x-axis of the data.
spiral_initialize() spiral_track(height = 0.6) spiral_axis()
Argument major_at
or simply at
controls the break points on the axis and argument labels
controls the corresponding axis labels.
# the left plot spiral_initialize(xlim = c(0, 360*4), start = 360, end = 360*5) spiral_track(height = 0.6) spiral_axis(major_at = seq(0, 360*4, by = 30)) # the right plot spiral_initialize(xlim = c(0, 12*4), start = 360, end = 360*5) spiral_track(height = 0.6) spiral_axis(major_at = seq(0, 12*4, by = 1), labels = c("", rep(month.name, 4)))
p1 = grid.grabExpr({ spiral_initialize(xlim = c(0, 360*4), start = 360, end = 360*5) spiral_track(height = 0.6) spiral_axis(major_at = seq(0, 360*4, by = 30)) }) p2 = grid.grabExpr({ spiral_initialize(xlim = c(0, 12*4), start = 360, end = 360*5) spiral_track(height = 0.6) spiral_axis(major_at = seq(0, 12*4, by = 1), labels = c("", rep(month.name, 4))) }) plot_grid(p1, p2)
If the axis labels are too long, argument curved_labels
can be set to TRUE
so that the labels
are curved along the spiral.
spiral_initialize() spiral_track(height = 0.6) spiral_axis(at = c(0.1, 0.3, 0.6, 0.9), labels = strrep(letters[1:4], 20), curved_labels = TRUE)
spiral_yaxis()
draws y-axis. Argument side
controls which side of the track to put
the y-axis. side
can be set to "both"
so that y-axis is drawn on the two sides of the track.
# the left plot spiral_initialize() spiral_track(height = 0.8) spiral_yaxis(side = "start") spiral_yaxis(side = "end", at = c(0, 0.25, 0.5, 0.75, 1), labels = letters[1:5]) # the right plot spiral_initialize() spiral_track(height = 0.8) spiral_yaxis(side = "both")
p1 = grid.grabExpr({ spiral_initialize() spiral_track(height = 0.8) spiral_yaxis(side = "start") spiral_yaxis(side = "end", at = c(0, 0.25, 0.5, 0.75, 1), labels = letters[1:5]) }) p2 = grid.grabExpr({ spiral_initialize() spiral_track(height = 0.8) spiral_yaxis(side = "both") }) plot_grid(p1, p2)
Tracks along the spiral are long, but the heights of the tracks are normally very small. Horizon chart is an effcient way to visualize distributions by vertically folding the distribution graphics, which makes it possible to visualize in a plotting region with a very small height.
In the next example, I visualize difference of ggplot2 daily downloads to the mean of the current year, between 2015-01-01 to 2020-12-31.
# The data can be downloaded by the next line. The data object is already saved in spiralize package. # df = cranlogs::cran_downloads("ggplot2", from = "2015-01-01") df = readRDS(system.file("extdata", "ggplot2_downloads.rds", package = "spiralize")) # to simplify the data, I only take the complete years between 2015 and 2020 df = df[df$date < as.Date("2021-01-01"), ] day_diff = as.double(df$date[nrow(df)] - df$date[1], "days") year_mean = tapply(df$count, lubridate::year(df$date), function(x) mean(x[x > 0])) df$diff = log2(df$count/year_mean[as.character(lubridate::year(df$date))]) df$diff[is.infinite(df$diff)] = 0 q = quantile(abs(df$diff), 0.99) # adjust outliers df$diff[df$diff > q] = q df$diff[df$diff < -q] = -q head(df)
Function spiral_horizon()
draws the horizon chart along the spiral. The input variables are x-locations
and y-locations of the data. To align weeks at different years, each loop contains 364 (52 weeks). In the following
plot, red areas correspond to those days when daily downloads are higher than the yearly average and blue areas
correspond to the days when daily downloads are less than the yearly average.
spiral_initialize(xlim = c(0, nrow(df)), start = 360, end = 360*(day_diff/364) + 360) # a circle of 52 weeks spiral_track(height = 0.9) spiral_horizon(1:nrow(df) - 0.5, df$diff)
Bars can be used to put on each days by setting argument use_bar = TRUE
:
spiral_initialize(xlim = c(0, nrow(df)), start = 360, end = 360*(day_diff/364) + 360) spiral_track(height = 0.9) spiral_horizon(1:nrow(df) - 0.5, df$diff, use_bars = TRUE)
When using bars, argument bar_width
can be set as a vector if the bar widths are not all equal.
Note, in this example, "time" objects can be directly used as value on x-axis. See vignettes "Initialize Spirals by Special Data Types" and "Real World Examples" for examples.
Lessly used, users can explicitly set the argument y_max
to the maximal absolute values for y
. This
would be useful when there are multiple horizon chart tracks and to make them compariable.
spiral_raster()
adds images to the spiral. Currently it supports formats of png/svg/pdf/eps/jpeg/jpg/tiff. The formats
can be mixed used.
image = system.file("extdata", "Rlogo.png", package = "circlize") x = seq(0.1, 0.9, length = 10) spiral_initialize() spiral_track() spiral_raster(x, 0.5, image)
Similar as text, argument facing
can be set to control the rotation of images. If nice_facing = TRUE
, the image
with rotation facing the top will be automatically adjusted to bottom.
# the left plot spiral_initialize() spiral_track() spiral_raster(x, 0.5, image, facing = "inside") # the right plot spiral_initialize() spiral_track() spiral_raster(x, 0.5, image, facing = "inside", nice_facing = TRUE)
p1 = grid.grabExpr({ spiral_initialize() spiral_track() spiral_raster(x, 0.5, image, facing = "inside") }, width = 5, height = 5) p2 = grid.grabExpr({ spiral_initialize() spiral_track() spiral_raster(x, 0.5, image, facing = "inside", nice_facing = TRUE) }, width = 5, height = 5) plot_grid(p1, p2)
Argument facing
can be set to one of "curved_inside"
or "curved_outside"
so that the image is filled on the track. In this case, arguments width
and
height
can be set as values measured in the data coordiantes. Note every
pixel in the image is actually drawn as a "spiral rectangle", so the plotting
would be slow for large images.
# the left plot spiral_initialize() spiral_track() spiral_raster(c(0.2, 0.4, 0.6, 0.8), 0.5, image, width = 0.05, height = 1, facing = "curved_inside") # the right plot spiral_initialize(scale_by = "curve") spiral_track() spiral_raster(c(0.2, 0.4, 0.6, 0.8), 0.5, image, width = 0.05, height = 1, facing = "curved_outside")
p1 = grid.grabExpr({ spiral_initialize() spiral_track() spiral_raster(c(0.2, 0.4, 0.6, 0.8), 0.5, image, width = 0.05, height = 1, facing = "curved_inside") }) p2 = grid.grabExpr({ spiral_initialize(scale_by = "curve") spiral_track() spiral_raster(c(0.2, 0.4, 0.6, 0.8), 0.5, image, width = 0.05, height = 1, facing = "curved_outside") }) plot_grid(p1, p2)
spiral_arrow()
draws arrows along the spiral.
spiral_initialize() spiral_track() spiral_arrow(0.3, 0.6, gp = gpar(fill = "red", col = NA)) spiral_arrow(0.8, 0.9, gp = gpar(fill = "blue"), tail = "point", arrow_position = "start")
spiral_highlight()
highlights a specific section of the spiral. If the argument type
is set to "rect"
(the default), it
highlights the section with a semi-transparent rectangle which covers the complete range on y-axis. If type
is
set to "line"
, an annotation line is drawn either at the bottom or on the top of the track.
spiral_initialize() spiral_track() spiral_highlight(0.4, 0.6) spiral_highlight(0.1, 0.2, type = "line", gp = gpar(col = "blue")) spiral_highlight(0.7, 0.8, type = "line", line_side = "outside")
If scale_by
is set to angle
(the default), users might also want to
highlight a specific of interval but across several cycles, e.g. from January
to March in year 2000 to 2010. Here the function
spiral_highlight_by_sector()
draws a semi-transparent sector to highlight a
fixed interval over several cycles.
The first four argument x1
, x2
, x3
and x4
in
spiral_highlight_by_sector()
determine the position of the sector. If only
x1
and x2
are specified, the start circle is calculated from x1
and x2
and the end circle is the most outside one. If x3
and x4
are also
specified, the outer circle is calcualted from x3
and x4
.
spiral_initialize(xlim = c(0, 360*4), start = 360, end = 360*5) spiral_track(height = 0.6) spiral_axis() spiral_highlight_by_sector(36, 72) spiral_highlight_by_sector(648, 684) spiral_highlight_by_sector(216, 252, 936, 972, gp = gpar(fill = "blue"))
When the dendrograms or phlogenetic trees have huge number of leaves, it is also suitable to use spiral to visualize them.
There are two functions: spiral_dendrogram()
for dendrogram
objects and spiral_phylo()
for
phylo
objects.
Note since dendrograms or phylogenetic trees do not have "periodic patterns",
scale_by
in spiral_initialize()
is usually set to "curve_length"
.
The dendrogram
object can be rendered with dendextend package. See the following examples.
dend = as.dendrogram(hclust(dist(runif(1000)))) # the left plot spiral_initialize(xlim = c(0, 1000), start = 360, end = 360*2 + 180, scale_by = "curve_length") spiral_track() spiral_dendrogram(dend) # the right plot library(dendextend) dend = color_branches(dend, k = 4) spiral_initialize(xlim = c(0, 1000), start = 360, end = 360*2 + 180, scale_by = "curve_length") spiral_track() spiral_dendrogram(dend)
dend = as.dendrogram(hclust(dist(runif(1000)))) p1 = grid.grabExpr({ spiral_initialize(xlim = c(0, 1000), start = 360, end = 360*2 + 180, scale_by = "curve_length") spiral_track() spiral_dendrogram(dend) }) p2 = grid.grabExpr({ library(dendextend) dend = color_branches(dend, k = 4) spiral_initialize(xlim = c(0, 1000), start = 360, end = 360*2 + 180, scale_by = "curve_length") spiral_track() spiral_dendrogram(dend) }) plot_grid(p1, p2)
spiral_phylo()
works on the phylo
object. In the following example, to cut the phylogenetic tree into several parts,
the phylo
object is converted to a dendrogram
by the function phylo_to_dendrogram()
, later dendextend::cutree.dendrogram()
is applied to obtain the splits.
library(ape) data(hivtree.newick) tree.hiv = read.tree(textConnection(hivtree.newick)) n = length(tree.hiv$tip.label) # number of leaves spiral_initialize(xlim =c(0, n), start = 360, end = 360*2 + 180, scale_by = "curve_length") spiral_track() spiral_phylo(tree.hiv) split = dendextend::cutree(phylo_to_dendrogram(tree.hiv), k = 8) tb = table(split) for(i in seq_along(tb)) { spiral_highlight(sum(tb[seq_len(i-1)]), sum(tb[seq_len(i)]), gp = gpar(fill = i)) }
The phylogenetic tree can also be directly colored by a categorical variable. We use the variable split
generated in the previous example.
spiral_initialize(xlim =c(0, n), start = 360, end = 360*2 + 180, scale_by = "curve_length") spiral_track() spiral_phylo(tree.hiv, group = split)
Both dendrograms and phylogenetic trees can face the outside of the spiral, just by reversing y-axis in spiral_track()
.
spiral_initialize(xlim =c(0, n), start = 360, end = 360*2 + 180, scale_by = "curve_length") spiral_track(reverse_y = TRUE) spiral_phylo(tree.hiv)
There are several utility functions which help to get the information of current spiral plot. spiral_info()
prints
the general information of current spiral:
pdf(NULL) spiral_initialize(xlim = c(0, 100)) spiral_track(ylim = c(-1, 1)) invisible(dev.off()) spiral_info()
The special variable TRACK_META
retrieves several meta-information of the current track.
TRACK_META
names(TRACK_META)
The following two functions convert the data coordinates to polar coordinates or the canvas coordinates (the coordinates where the graphics are finally drawn).
xy_to_cartesian()
xy_to_polar()
The following three functions get or set the tracks:
current_track_index()
set_current_track()
n_track()
There is also one more function which converts canvas coordinates to data coordinates. The data points are assigned to the nearest inner loops. For a data point $P$, denote $r$ as the distance to the origin, $\theta$ is the angle to the line $x > 0, y = 0$ (reverse-clockwise angle). Let's assume the value of $\theta$ is between 0 and $2\pi$ (which is actually not important if $\theta$ multiplies with $2\pi$). Denote $r_k$ and $r_{k+1}$ are the radius of the two loops of the spiral at $\theta + 2\pi \cdot a$ and $\theta + 2\pi \cdot (a+1)$ that below and above the data point (here $a$ is an integer which should be properly calculated to make sure $p$ is between loop $k$ and $k+1$), then the data point $P$ is assigned to the loop $k$.
cartesian_to_xy()
There is an interesting application for cartesian_to_xy()
which is to overlay an image to the spiral. If we treat each pixel as a data point,
then we can only draw those pixels which are inside a track on the spiral. In the following example, I first load an image object:
load(system.file("extdata", "doodle.RData", package = "circlize")) # the loaded object is `img_list` img = img_list[[1]] # img_list contains several images as `raster` objects, here we only use the first one. img = apply(img, 1:2, function(x) rgb(x[1], x[2], x[3])) # convert to color characters
Now img
is a matrix of colors. Note the first element in img
(i.e. img[1, 1]
) corresponds to the top left pixel of the image.
In the following code, I basically test whether the pixels are in the track. If they are, the positions and corresponding colors are saved.
nr = nrow(img) nc = ncol(img) spiral_initialize(start = 0, end = 360*5, polar_lines = FALSE) spiral_track(background = TRUE) s = current_spiral() all_x = NULL all_y = NULL all_col = NULL for(i in 1:nr) { for(j in 1:nc) { x = (j - nc/2)/nc*1.5*s$max_radius y = -(i - nr/2)/nr*1.5*s$max_radius df = cartesian_to_xy(x, y) if(is_in_track(df$x, df$y)) { all_x = c(all_x, df$x) all_y = c(all_y, df$y) all_col = c(all_col, img[i, j]) } } } spiral_points(all_x, all_y, pch = 16, gp = gpar(col = all_col), size = unit(2, "pt"))
Next two plots are drawn with spirals of 20 loops and 50 loops. Also the background of tracks are not drawn.
make_plot = function(k) { spiral_initialize(start = 0, end = 360*k, polar_lines = FALSE) spiral_track(background = FALSE) s = current_spiral() all_x = NULL all_y = NULL all_col = NULL for(i in 1:nr) { for(j in 1:nc) { x = (j - nc/2)/nc*1.5*s$max_radius y = -(i - nr/2)/nr*1.5*s$max_radius df = cartesian_to_xy(x, y) if(is_in_track(df$x, df$y)) { all_x = c(all_x, df$x) all_y = c(all_y, df$y) all_col = c(all_col, img[i, j]) } } } spiral_points(all_x, all_y, pch = 16, gp = gpar(col = all_col), size = unit(2, "pt")) } p1 = grid.grabExpr(make_plot(20)) p2 = grid.grabExpr(make_plot(50)) plot_grid(p1, p2)
sessionInfo()
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.