More on Horizon Charts

[This article was first published on Timely Portfolio, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
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.

image

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


HorizonVis

Lead / Contact
Wolfgang Aigner

Team
Wolfgang Aigner, Vienna University of Technology
Michael Atanasov, HTBL Krems
Alexander Rind, Vienna University of Technology
Philipp Schindler, HTBL Krems
Reinhardt Wenzina, HTBL Krems

Partners
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
)

To leave a comment for the author, please follow the link and comment on their blog: Timely Portfolio.

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.

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)