nodes <- LETTERS[1:5]
G <- completegraph(nodes)
LG <- linegraph(G)
g <- l_graph(LG)
l_navigator_add(g)
A (mathematical) graph in loon
is defined by a list of node names, the from-to list of node names that define the edges and a Boolean value whether the graph is directed or not. This translates into the states nodes
, from
, to
and isDirected
.
The graph layout is defined with the x
and y
states.
Get the state names with
states <- l_info_states(g)
names(states)
Query a state, say background
, as follows
g['background']
Change a state, say again background
, foreground
, and colorEdge
, as follows
g['background'] <- 'gray20'
g['foreground'] <- 'gray90'
g['colorEdge'] <- 'red'
alternatively, and more efficient if you modify more than one state, use
l_configure(g, background='gray20', foreground='gray90', colorEdge='red')
When creating a graph display you may specify any state at plot creation
nodes <- letters[1:3]
G <- completegraph(nodes)
LG <- linegraph(G)
g1 <- l_graph(LG, background='gray20', foreground='gray90', colorEdge='red')
details on a state, say background
, is easily had with
states <- l_info_states(g)
states$background
and a particular field
states$background$description
The graph has n
dimensional states that are associated to nodes and p
dimensional states that are associated with edges. To query which states are p
dimensional use
states <- l_info_states(g)
names(Filter(function(x){x$dimension=='p'}, states))
To change the layout of the graph use a graph layout algorithm and set the x
and y
states of the loon
graph accordingly. loon
supports straight lines for edges only.
Both the loon
and graph
packages define the method complement
(once for S3
and once for S4
). Hence, the complement
method of the package that was loaded last will mask the other complement
method. Therefore it is advisable to specify the method explicitly with loon::complement
or graph::complement
.
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.
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
proprtion
between the last node in from
and the first node in to
. If to
is empty then the navigator sits on the last node of from
.from
and the proportion
that has been traversed on the current edge.to
.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
to=''
) then you can extend the path by dragging the navigator towards a new connected node. Note that
scrollProportionIncrement
state of the navigator.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'))
animationPause
and animationProportionIncrement
to control the animation speed.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))
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 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:
Context2d maps every location on a 2d space graph to a list of xvars
and a list of yvars
such that, while moving the navigator along the graph, as few changes as possible take place in xvars
and yvars
, see the image:
Geodesic2d maps every location on the graph as an orthogonal projection of the data onto a two-dimensional subspace. The nodes then represent the sub-space spanned by a pair of variates and the edges either a 3d- or 4d-transition of one scatterplot into another, depending on how many variates the two nodes connected by the edge share (see Hurley and Oldford 2011). The geodesic2d
context inherits from the context2d
context.
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
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))
:
but it can be changed to any string in the linegraph
function and as a context2d state.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)
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)
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')
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=':')
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.
ng
is a named list with all object handles.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
.
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)
It is possible to use l_ng_ranges
and l_ng_plots
with arbitrary 1d and 2d 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.
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)