More on Horizon Charts
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
for background please see prior posts Application of Horizon Plots, Horizon Plot Already Available, and Cubism Horizon Charts in R
Some feedback has led me to think that I might have been a little ambitious with my last post on horizon charts. I thought it might be helpful to quickly provide an overview on horizon charts. Since I am not a visualization expert, I will try to compile the best examples and tutorials that I have found. At the end, I will do a step by step walkthrough of the construction of a horizon chart in R.
Below is a great talk in its entirety by Mike Bostock. For the discussion on horizon charts, skip to the 11:20 mark.
Mike Bostock @ Square talks about Time Series Visualization from Librato on Vimeo.
Fortunately, Mike also provides the interactive example from the video demonstrating the construction of a horizon chart from an area chart. I have embedded it below (see https://bl.ocks.org/1483226 for the full example from the original source).
This quick YouTube clip from Panopticon all the way back in 2009 also does a very nice job explaining construction. If you learn better from reading, a short paper from Hannes Reijner of Panopticon Software covers the same material.
Jeffrey Heer of Stanford University with Nicholas Kong and Maneesh Agrawala from University of California, Berkeley, investigate the effectiveness of horizon plots and make some recommendations for their use. The chart below also offers another explanation of the steps in building a horizon plot.
They recommend and conclude
Layered Bands Are Beneficial As Chart Size Decreases
We found that dividing a chart into layered bands reliably increased estimation time and increased estimation error at constant chart heights. However, we also found that 2-band mirrored charts led to better estimation accuracies for chart heights less than 24 pixels (6.8 mm on our displays). For larger chart sizes, we advise scaling 1-band mirrored charts. For smaller sizes, we advise adding layered bands.
Extending the horizon into healthcare, here is an interesting project using horizon graphs for visualization of diabetes care.
HorizonVis
Interactive Visual Exploration of Multivariate Medical Measurements in Diabetes Care
![]()
Lead / Contact
Wolfgang AignerTeam
Wolfgang Aigner, Vienna University of Technology
Michael Atanasov, HTBL Krems
Alexander Rind, Vienna University of Technology
Philipp Schindler, HTBL Krems
Reinhardt Wenzina, HTBL KremsPartners
Vienna University of Technology, Institute of Software Technology & Interactive Systems
HTBL Krems, Department of Information Technology
Now For My Own Attempt at Explaining
If we use yesterday’s example of a 200 day moving average system where you enter when above the moving average and exit when below, we might like to see a standard time series plot like this one.
![]() |
From TimelyPortfolio |
In a simple world with only one asset or stock, this might be sufficient. However, we probably will have multiple instruments that we would like to monitor, and dedicating this much height per instrument will require lots of space. Ideally, we could condense each of these plots, so that we could see many at a time.
In the first step toward condensing, we could extract just the information that is most meaningful, which I consider to be the percent above or below the moving average.
![]() |
From TimelyPortfolio |
We might then try an area chart to better depict above or below 0.
![]() |
From TimelyPortfolio |
I’m sure you are wondering though when we will start reducing height and saving space. We could start by changing all the negative values to positive values, and add color to represent positive or negative. This means we cut our chart height by about 1/2.
![]() |
From TimelyPortfolio |
However, we need much more efficiency, so let’s separate the chart into bands.
![]() |
From TimelyPortfolio |
Is there any potential way of separating each of these bands and then recombining them to save space? Let’s look at each band separately.
Band 1 (0 to 10%)
![]() |
From TimelyPortfolio |
Band 2 (10% to 20%)
![]() |
From TimelyPortfolio |
Band 3 (20% to 30%)
![]() |
From TimelyPortfolio |
Band 4 (30% to 40%) exceeding 3 bands is not recommended
![]() |
From TimelyPortfolio |
Notice that similar to a heat map, the band colors increase in intensity and opaqueness as their values increase. If we layer each band on top of each other, we get a horizon plot, and we can reduce the original chart by 1/6 or even more without significant loss of information.
![]() |
From TimelyPortfolio |
I hope this helps explain why we might use horizon plots and how to make them. If nothing else, maybe you will have learned some lattice techniques in R.
R code in GIST (do raw for copy/paste):
#look at steps in constructing a horizon plot version | |
#of http://www.mebanefaber.com/timing-model/ | |
#do horizon of percent above or below 10 month / 200 day moving average | |
require(lattice) | |
require(latticeExtra) | |
require(quantmod) | |
#since we are focused on the horizon plot, let's just look at one stock | |
tckrs <- "VTI" | |
getSymbols(tckrs, from = "2006-12-31") | |
#do horizon of percent above or below 10 month or 200 day moving average | |
prices <- get(tckrs[1])[,6] | |
#remove comments below if you would like to look at more than one symbol | |
#for (i in 2:length(tckrs)) { | |
# prices <- merge(prices,get(tckrs[i])[,4]) | |
#} | |
colnames(prices) <- tckrs | |
#set n to desired moving average width; we'll do 200 | |
n=200 | |
ma <- runMean(prices, n = n) | |
colnames(ma) <- paste(tckrs, ".MovAvg", sep = "") | |
xyplot(merge(prices,ma), | |
col = c("black", "red"), | |
lty = c(1,3), | |
screens = 1, | |
scales = list(tck = c(1,0)), | |
xlab = NULL, | |
main = "VTI and 200-day Moving Average") | |
#but for timing system more interested in whether above or below | |
#get percent above or below | |
#we'll leave code to expand beyond one symbol | |
pctdiff <- (prices / apply(prices, MARGIN = 2, FUN = runMean, n = n) - 1)[n:NROW(prices),] | |
xyplot(pctdiff, | |
col.line = "steelblue4", | |
scales = list(tck = c(1,0)), | |
xlab = NULL) | |
xyplot(pctdiff, | |
border = NA, | |
col.line = "steelblue4", | |
scales = list(tck = c(1,0)), | |
xlab = NULL, | |
panel = function (...) { | |
panel.xyarea(origin=0, ...) | |
#draw horizontal lines at 10% to show where we will place bands | |
panel.abline(h = seq(-0.4, 0.4, 0.10), col = "white", lwd = 2) | |
#add black 0 axis back | |
panel.abline(h = 0, col = "black") | |
}) | |
#takes a lot of height to represent so let's mirror the negative | |
#so we can cut height by 1/2 | |
xyplot(pctdiff, | |
#remove border around chart and axis lines | |
par.settings = list(axis.line = list(col = NA), | |
strip.border = list(col = NA), | |
strip.background = list(col = NA)), | |
border = NA, | |
scales = list(tck = c(1,0), #remove tick lines on top | |
y = list(col.line="black", rot=0)), #make ticks black and not rotated | |
xlab = NULL, | |
#limit to max of absolute value since we will mirror the negative | |
ylim = c(0,ceiling(max(abs(coredata(pctdiff)))*10)/10), | |
panel = function (x, y, ...) { | |
#do the positive values in blue | |
panel.xyarea(x, ifelse(y > 0, y ,0), col.line = "steelblue4", origin=0, ...) | |
#do the positive values in blue | |
panel.xyarea(x, ifelse(y < 0, abs(y) ,0), col.line = "indianred3", origin=0, ...) | |
#draw horizontal lines at 10% to show where we will place bands | |
panel.abline(h = seq(-0.4, 0.4, 0.10), col = "white", lwd = 2) | |
#add black 0 axis back | |
panel.abline(h = 0, col = "black") | |
}) | |
#do same chart as above but draw box around bands | |
xyplot(pctdiff, | |
#remove border around chart and axis lines | |
par.settings = list(axis.line = list(col = NA), | |
strip.border = list(col = NA), | |
strip.background = list(col = NA)), | |
border = NA, | |
scales = list(tck = c(1,0), #remove tick lines on top | |
y = list(col.line="black", rot=0)), #make ticks black and not rotated | |
xlab = NULL, | |
#limit to max of absolute value since we will mirror the negative | |
ylim = c(0,ceiling(max(abs(coredata(pctdiff)))*10)/10), | |
panel = function (x, y, ...) { | |
#do the positive values in blue | |
panel.xyarea(x, ifelse(y > 0, y ,0), col.line = "steelblue4", origin=0, ...) | |
#do the positive values in blue | |
panel.xyarea(x, ifelse(y < 0, abs(y) ,0), col.line = "indianred3", origin=0, ...) | |
#draw horizontal lines at 10% to show where we will place bands | |
panel.abline(h = seq(-0.4, 0.4, 0.10), col = "white", lwd = 2) | |
#add black 0 axis back | |
panel.abline(h = 0, col = "black") | |
panel.xblocks(x,abs(y)>0,height=0.128,col="white",border="black",alpha=0.3) | |
panel.text(x=x[1],y=0.11,labels="band1", pos=4) | |
panel.xblocks(x,abs(y)>0,height=0.228,col="white",border="black",alpha=0.25) | |
panel.text(x=x[1],y=0.21,labels="band2", pos=4) | |
panel.xblocks(x,abs(y)>0,height=0.328,col="white",border="black",alpha=0.2) | |
panel.text(x=x[1],y=0.31,labels="band3", pos=4) | |
panel.xblocks(x,abs(y)>0,height=0.428,col="white",border="black",alpha=0.15) | |
panel.text(x=x[1],y=0.41,labels="band4", pos=4) | |
}) | |
#get 4 reds and 4 blues (one for each band) | |
reds <- brewer.pal("Reds", n=8)[4:8] | |
blues <- brewer.pal("Blues", n=8)[4:8] | |
#so let's start banding to use even less height | |
#for band1 so we will only show graph from 0 to 0.1 | |
band1 <- xyplot(pctdiff, | |
#remove border around chart and axis lines | |
par.settings = list(axis.line = list(col = NA), | |
strip.border = list(col = NA), | |
strip.background = list(col = NA)), | |
lattice.options = list(axis.padding = list(numeric = 0)), | |
border = NA, | |
scales = list(tck = c(1,0), #remove tick lines on top | |
x = list(col.line="black"), | |
y = list(col.line="black", rot=0)), #make ticks black and not rotated | |
xlab = NULL, | |
#limit y to band height; in this case 10% | |
ylim = c(0,0.1), | |
panel = function (x, y, ...) { | |
#do the positive values in blue | |
panel.xyarea(x, ifelse(y > 0, y ,0), col.line = blues[4], origin=0, alpha = 0.3, ...) | |
#do the positive values in blue | |
panel.xyarea(x, ifelse(y < 0, abs(y) ,0), col.line = reds[4], origin=0, alpha = 0.3, ...) | |
#add black 0 axis back | |
panel.abline(h = 0, col = "black") | |
}) | |
print(band1) | |
#we are missing all the values > 0.10 | |
#we will draw band2 0.1 to 0.2 with a darker color | |
band2 <- xyplot(pctdiff, | |
#remove border around chart and axis lines | |
par.settings = list(axis.line = list(col = NA), | |
strip.border = list(col = NA), | |
strip.background = list(col = NA)), | |
lattice.options = list(axis.padding = list(numeric = 0)), | |
border = NA, | |
scales = list(tck = c(1,0), #remove tick lines on top | |
x = list(draw = FALSE), | |
y = list(col.line="black", rot=0)), #make ticks black and not rotated | |
xlab = NULL, | |
#limit y to band height; in this case 10% | |
ylim = c(0, 0.1), | |
panel = function (x, y, ...) { | |
#do the positive values in blue | |
panel.xyarea(x, ifelse(y > 0.1, y - 0.1, 0), col.line = blues[4], origin=0, alpha = 0.5, ...) | |
#do the positive values in blue | |
panel.xyarea(x, ifelse(y < -0.1, abs(y) - 0.1, 0), col.line = reds[4], origin=0, alpha = 0.5, ...) | |
}) | |
print(band2) | |
#now we are missing all the values > 0.10 + 0.10 | |
#we will draw band3 0.2 to 0.3 with a darker color | |
band3 <- xyplot(pctdiff, | |
#remove border around chart and axis lines | |
par.settings = list(axis.line = list(col = NA), | |
strip.border = list(col = NA), | |
strip.background = list(col = NA)), | |
lattice.options = list(axis.padding = list(numeric = 0)), | |
border = NA, | |
scales = list(tck = c(1,0), #remove tick lines on top | |
x = list(draw = FALSE), | |
y = list(col.line="black", rot=0)), #make ticks black and not rotated | |
xlab = NULL, | |
#limit y to band height; in this case 10% | |
ylim = c(0, 0.1), | |
panel = function (x, y, ...) { | |
#do the positive values in blue | |
panel.xyarea(x, ifelse(y > 0.2, y - 0.2, 0), col.line = blues[4], origin=0, alpha = 0.7, ...) | |
#do the positive values in blue | |
panel.xyarea(x, ifelse(y < -0.2, abs(y) - 0.2, 0), col.line = reds[4], origin=0, alpha = 0.7, ...) | |
}) | |
print(band3) | |
#going to four bands is not recommended but for this example we will | |
band4 <- xyplot(pctdiff, | |
#remove border around chart and axis lines | |
par.settings = list(axis.line = list(col = NA), | |
strip.border = list(col = NA), | |
strip.background = list(col = NA)), | |
lattice.options = list(axis.padding = list(numeric = 0)), | |
border = NA, | |
scales = list(tck = c(1,0), #remove tick lines on top | |
x = list(draw = FALSE), | |
y = list(col.line="black", rot=0)), #make ticks black and not rotated | |
xlab = NULL, | |
#limit y to band height; in this case 10% | |
ylim = c(0, 0.1), | |
panel = function (x, y, ...) { | |
#do the positive values in blue | |
panel.xyarea(x, ifelse(y > 0.3, y - 0.3, 0), col.line = blues[4], origin=0, alpha = 1, ...) | |
#do the positive values in blue | |
panel.xyarea(x, ifelse(y < -0.3, abs(y) - 0.3, 0), col.line = reds[4], origin=0, alpha = 1, ...) | |
}) | |
print(band4) | |
#combine all four bands/layers to one horizonplot | |
print(band1+band2+band3+band4) | |
#of course using the horizonplot function from latticeExtra | |
#makes this much easier | |
horizonplot(pctdiff, | |
strip.left=FALSE, | |
strip=FALSE, | |
#remove border around chart and axis lines | |
par.settings = list(axis.line = list(col = NA), | |
strip.border = list(col = NA), | |
strip.background = list(col = NA)), | |
lattice.options = list(axis.padding = list(numeric = 0)), | |
border = NA, | |
scales = list(tck = c(1,0), #remove tick lines on top | |
x = list(col.line="black"), | |
y = list(col.line="black", rot=0)), #make ticks black and not rotated | |
xlab = NULL | |
) |
R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.