Loon: An Interactive Statistical Visualization Toolkit
nodes <- LETTERS[1:5]
G <- completegraph(nodes)
LG <- linegraph(G)

g <- l_graph(LG)

l_navigator_add(g)

Graph

Good To Know

Navigation Graphs

To turn a graph into a navigation graph you need to add one or more navigators. Navigators have their own set of states such as from, to and proportion. You can create state bindings for the navigator that call a function when a navigator changes its position on the graph. States and state bindings for navigators provide the facility to implement any graph semantic. However, certain graph semantics (e.g. the default semantic with 2d projection along a geodesic path between spaces) involve lots of logic and control over plots and, hence, it makes sense to en encapsulate them. We do this by providing contexts. A context is added to a navigator and will do a specific task if the navigator's position on the graph changes.

We use the example at the beginning of this section:

nodes <- LETTERS[1:5]
G <- completegraph(nodes)
LG <- linegraph(G)

g <- l_graph(LG)

The following code ads a navigator to the graph g

nav <- l_navigator_add(g, color='orange')

The navigator with the id stored in the nav has its own states that can be listed as follows

nstates <- l_info_states(nav)
names(nstates)

The position of the navigator on the graph is defined by the states from, to and proportion. The states from and to hold vectors of node names of the graph. The proportion state is a number between and including 0 and 1 and defines how far the navigator is between the last element of from and the first element of to. The to state can also be an empty string '' if there is no further node to go to. Hence, the concatenation of from and to define a path on the graph.

Interaction with the Navigators

The position of the navigator on the graph can be controlled programatically as follows:

l_configure(nav, from=c('A:B','B:C','C:D','A:D'), to=c('D:E','B:E'),
    proportion=0.2)

The elements related to the navigator you see in the plot above are

The graph display supports direct interaction with the navigator and navigator path using the mouse and keyboard. To move the navigator with the mouse you must first click on it to select it which will set the activeNavigator graph state to the navigator id and causes the navigator to be highlighted with the navigator outline in the selection color.

In this state the following interactions are possible

Note that the highlighting of the adjacent nodes of a navigator and the edge selection circle are mouse interaction states and have no equivalent display states. That is, they are all transient and are undone as soon as the Shift key and/or mouse button press gets released.

The animation of the navigator can also be done programatically with any of the following commands

l_navigator_walk_forward(nav)
l_navigator_walk_backward(nav)
l_navigator_walk_forward(nav, 'C:D')
l_navigator_walk_backward(nav, 'B:C')
l_navigator_walk_path(nav, path=c('D:E','B:E','B:D','A:D'))

Navigators support state bindings. You can use state bindings to implement your custom navigation graph semantic.

We use the following graph and navigator for our example:

nodes <- LETTERS[1:5]
G <- completegraph(nodes)
LG <- linegraph(G)

g <- l_graph(LG)

nav  <- l_navigator_add(g)

To add a state binding use

my_semantic <- function(widget, navigator) {
    navi <- navigator
    class(navi) <- c("loon", "l_navigator")
    attr(navi, "widget") <- widget
    cat(paste0('do stuff: ', widget, ', ', navigator, ': ',
        tail(navi['from'],n=1),'-',
        round(navi['proportion'],2),'-',
        navi['to'][1],'\n'))
}
        
l_bind_state(nav, event=c('from', 'to', 'proportion'),
    function(W,nav) my_semantic(W,nav))

Substitution

The current substitutions for navigator state bindings are

argument name substituted value
W widget path name
nav navigator id
e events (states changed)
b binding id

Remember that these substitutions get passed to the R function as a Tcl object, hence you need to convert them to the desired type before using them in your code (e.g. with l_toR or as.numeric).

Contexts

Contexts implement standard graph semantics. Common to all contexts is that they sign up to the navigators state changes and will evaluate its (i.e. the context's) command state. The contexts add substitution in the command evaluation that are meaningful for the particular context. Currently the following contexts are implemented:

Geodesic 2d

The following code adds a geodesic2d context to a navigator:

G <- completegraph(names(iris[,-5]))
LG <- linegraph(G)

g <- l_graph(LG)
nav <- l_navigator_add(g)

con <- l_context_add_geodesic2d(navigator=nav, data=iris[,-5])

This will open a new scatterplot showing the projection defined by the navigator location. Every navigator position change will evaluate the command in the command state of the context. The default command state is

con['command']

#> [1] ".l2.plot configure -x %x -y %y -xlabel %xlabel -ylabel %ylabel"

where .l2.plot is the widget path name of the newly created scatterplot. If the command state is specified at context creation time then no scatterplot will be created. The command state supports substitutions similar to bindings. The substitution table is

argument name substituted value
W widget path name (i.e. the graph)
nav navigator id
con context id
x x coordinates of projection
y y coordinates of projection
xlabel suitable x label for projection
ylabel suitable y label for projection
from from state of navigator
to to state of navigator
p proportion state of navigator

Hence, it is easy to use a different scatterplot device, say the basic R plots as follows:

plot(iris[,1:2], col=iris$Species, pch=16)

con['command'] <- function(x,y,xlabel,ylabel) {
    plot(l_toR(x, as.numeric), l_toR(y, as.numeric), xlab=xlabel, ylab=ylabel,
        col=iris$Species, pch=16, xlim=c(-5,5), ylim=c(-5,5))
}

Or you can add contour lines of the density estimates

require(MASS)

con['command'] <- function(x,y,xlabel,ylabel) {
    x <- l_toR(x, as.numeric)
    y <- l_toR(y, as.numeric)

    fit <- kde2d(x,y)

    plot(x, y, xlab=xlabel, ylab=ylabel,
        col=iris$Species, pch=16, xlim=c(-5,5), ylim=c(-5,5))

    contour(fit, add=TRUE)
}

The context2d has a couple of noteworthy states, use the info states approach to learn more about them:

names(l_info_states(con))

l_info_states(con)$scaling

Context 2d

The context2d substitutions are

argument name substituted value
W widget path name (i.e. the graph)
nav navigator id
con context id
xvars x variables
yvars y variables
from from state of navigator
to to state of navigator
p proportion state of navigator

If the context2d description above wasn't clear enough use the following code to get a sense of how xvars and yvars change.

G <- completegraph(c('A','B','C','D','E','F','G'))
LG <- linegraph(G, separator='-')

g <- l_graph(LG)
nav <- l_navigator_add(g)

foo <- function(xvars, yvars, p) {
    cat(paste0(paste(xvars, collapse=' '), ' to ',
        paste(yvars, collapse=' '), ': ',
        round(as.numeric(p), 3), '\n'))
}

con <- l_context_add_context2d(nav, separator='-',
    command=function(xvars,yvars,p)foo(xvars,yvars,p))

The graph can be switched as follows

LGnot <- loon::complement(LG)

l_configure(g, nodes=LGnot$nodes, from=LGnot$from,
    to=LGnot$to, isDirected=LGnot$isDirected)

Graph Switch Widget

Sometimes it is useful to easily switch between different graphs on a graph display. The graph switch widget maintains a list of graphs and updates the activewidget if a graph in its list gets selected.

For this example we pack a graph switch widget next to a graph display. More on widget layouts can be read here.

tt <- tktoplevel()
tktitle(tt) <- paste("Loon graph example with a graph switch")

g <- l_graph.default(parent=tt)

gs <- l_graphswitch(activewidget=g, parent=tt)

tkpack(g, side='left', fill='both', expand=TRUE)
tkpack(gs, side='left', fill='y')

Graphs are added to the graph switch as follows

G1 <- completegraph(LETTERS[1:4])
G2 <- loongraph(nodes=c('a','b','c'), from=c('a','a'),
    to=c('b','c'), isDirected=FALSE)
G3 <- linegraph(G1)
G4 <- loon::complement(G3)

idG1 <- l_graphswitch_add(gs, G1, label='G1')
idG2 <- l_graphswitch_add(gs, G2, label='G2')
idG3 <- l_graphswitch_add(gs, G3, label='G3=linegraph(G1)')
idG4 <- l_graphswitch_add(gs, G4, label='complement(G4)')

l_graphswitch_set(gs, idG3)

Working with the Graph Switch

The API of the graph switch is similar to that of the layers, except that graphs are arranged in a flat list and layers are arranged in a tree structure.

Either graphs of class loongraph or class graph (defined in the graph R package) can be added as follows

gs <- l_graphswitch()

graphId <- l_graphswitch_add(gs, graph=loongraph(nodes=c('A','B','C'),
    from=c('A','B'), to=c('C','C'), isDirected=FALSE), label='loongraph')

## or

library(graph)
graphId2 <- l_graphswitch_add(gs, graph=randomEGraph(LETTERS[1:15], edges=50),
    label='graph R package graph')

The add method returns an id for the added graph.

Currently the activewidget state of gs is not set to any graph widget. Selecting a graph will throw an error saying that the graphswitch has no activewidget set. To set an activewidget (i.e. a graph widget) use

g <- l_graph()

gs['activewidget'] <- g

Now, to push a graph in gs to the graph widget g you can either mouse select a graph on the graphswitch widget or set it programmatically as follows

l_graphswitch_set(gs, id=graphId2)

We continue by adding a few more graphs in order to introduce the other graphswitch related functions.

l_graphswitch_add(gs, graph=randomEGraph(LETTERS[1:15], edges=10))
l_graphswitch_add(gs, graph=randomEGraph(LETTERS[1:15], edges=20))

To list the ids of all graph in the graphswitch use

l_graphswitch_ids(gs)

If you have followed this example then the ids method should return a list with the ids graph0, graph1, graph2 and graph3, where the order of the ids is how they appear in the graphswitch widget. To move a graph to a different position in the list do as follows

l_graphswitch_move(gs, id='graph0', index=3)

to move graph0 to the second last place. To reorder all graphs use

l_graphswitch_reorder(gs, ids=c('graph1', 'graph0', 'graph3', 'graph2'))

To get the label of a graph use

l_graphswitch_getLabel(gs, id='graph1')

To relabel a graph use

l_graphswitch_relabel(gs, id='graph1', label="A special graph")

To delete a graph use

l_graphswitch_delete(gs, id='graph2')

And to get the graph as a loongraph object use

l_graphswitch_get(gs, id='graph1')

Graph Utilities

The loon R package comes with functions to create graphs. These are fairly basic. If you have a situation where you need more demanding graph manipulations then we recommend to use the algorithms and data structure from graph R package. To coerce between loongraph and graph object use the as.loongraph and as.graph functions. Note that the loongraph does not contain graph attributes such as edge weights etc. The plot.loongraph method will plot the graph if the graph package and Rgraphviz package are loaded.

To create a graph of class loongraph use the following function

G <- loongraph(nodes=c('A','B','C'), from=c('A','A','B'), to=c('B','C','C'),
    isDirected=FALSE)

Or, to create a complete loongraph with the nodes A, B, and C use

completegraph(nodes=c('A','B','C'))

To get the linegraph use

LG <- linegraph(G, sep="-")

and the complement (you may specify that the function is from the loon package with the prefix loon:: as if you load the graph package after you load the loon package then the complement function is masked by the graph package)

loon::complement(LG)

And, given some node names of a graph, on can get the undirected n-d transition graph as follows

ndtransitiongraph(nodes=c('A:B', 'A:F', 'B:C', 'B:F'), n=3, separator=':')

Navigation Graph Workflows

In the loon R package we provide a couple of convenience functions to create a navigation graph setup with a single function call.

The l_navgraph function creates a navigation graph, a graphswitch, a navigator and a geodesic2d context added, and a scatterplot. If the graph argument is not used then a 3d and 4d transition graph and a complete transition graph is added.

data(olive)
ng <- l_navgraph(olive[,-c(1,2)], sep='-', color=olive$Area)

the additional arguments ... in l_navgraph will get passed on to a configure call for the scatterplot.

l_ng_ranges

The l_ng_ranges produces a graph based on variable pair measures. A min-max slider is provided to filter variable pairs based on their associated measures.

library(scagnostics)

oliveAcids <- olive[,-c(1,2)]
scags <- scagnostics(oliveAcids)

nav <- l_ng_ranges(measures=scags, data=oliveAcids, color=olive$Area)

For more information see the examples of l_ng_ranges.

l_ng_plots

Uses same formal arguments as l_ng_ranges but creates a scatterplot matrix of measures to select the variable pairs.

library(scagnostics)

oliveAcids <- olive[,-c(1,2)]
scags <- scagnostics(oliveAcids)

nav <- l_ng_plots(measures=scags, data=oliveAcids, color=olive$Area)

Measures

It is possible to use l_ng_ranges and l_ng_plots with arbitrary 1d and 2d measures.

Arbitrary Measures

The measures can be arbitrary

n <- 100
dat <- data.frame(
    A = rnorm(n), B = rnorm(n), C = rnorm(n),
    D = rnorm(n), E = rnorm(n)
)

m2d <- data.frame(
    cor = with(dat, c(cor(A,B), cor(A,C), cor(B,D), cor(D,E), cor(A,E))),
    my_measure = c(1, 3, 2, 1, 4),
    row.names = c('A:B', 'A:C', 'B:D', 'D:E', 'A:E')
)

nav <- l_ng_ranges(measures=m2d, data=dat, separator=':')

It is important that the separator string does not appear in the variable names.

Closures of Measures

With the measures1d and measures2d functions it is possible to encapsulate the calculation of the measures. In turn, this makes it possible to recalculate the measures based on a subset of data.

iqr <- function(x) { diff(quantile(x, probs=c(0.75, 0.25))) }
kurtosis <- function(x) { mean((x-mean(x))^4)/mean((x-mean(x))^2)^2 - 3 }
skewness <- function(x) { mean((x-mean(x))^3)/sd(x)^3 }

s_oliveAcids <- scale(oliveAcids)

m1dc <- measures1d(data=s_oliveAcids, separator='+',
                   median = median,
                   irq = iqr,
                   kurtosis = kurtosis,
                   skewness = skewness)

nav <- l_ng_ranges(measures=m1dc, color=olive$Area)

Or with 2d scagnostics measures

library(scagnostics)
oliveAcids <- olive[,-c(1,2)]
scags2d <- scagnostics2d(oliveAcids)

nav <- l_ng_plots(measures=scags2d, color=olive$Area)