var student_answers = {}; var comments = {missing:"Missing answer", correct:"Correct", incorrect:"Incorrect"}; var student_id = "{{{STUDENT_ID}}}"; var page_id = "{{{PAGE_ID}}}";
var manipulate = function(form_id, graph_id, inputs, expression) { var render = function(template, values) { for (value in values) {template = template.replace(new RegExp('%' + value + '%', 'g'), values[value], 'g');} return(template); } var rnd_id = (((1+Math.random())*0x10000)|0).toString(16).substring(1); // oops, from internet search (who?) var converter = new Showdown.converter(); html = converter.makeHtml(inputs); // make HTML from markup $("#" + form_id).append("") ; // class='form-horizontal' gives wider layout $("#" + graph_id).append(""); //blank $('.noEnterSubmit').keypress(function(e){ if ( e.which == 13 ) return e.preventDefault(); }); var call_expression = function(params, expression) { var tmp = {} $.each(params, function() {console.log(this.value); tmp[this.name] = this.value}); var out = render(expression, tmp); $.post("https://public.opencpu.org/R/pub/base/identity/save", {x:out}, function(data, status, xhr) { var data = JSON.parse(data) $("#" + graph_id).children()[0].src = "https://public.opencpu.org/R/tmp/" + data.graphs[0] + "/png" }) }; $("#" + form_id).on("change ", ':input:not([type="submit"])', function() { //change input call_expression($("#" + rnd_id).serializeArray(), expression); }) call_expression($("#" + rnd_id).serializeArray(), expression); }
Programming web pages with the gWidgets API
$('#navbar-header').append("
The gWidgetsWWW2.rapache
package implements most of the gWidgets
API to program interactive web pages with pure R
code.
The gWidgets
API is a simple-to-learn set of constructors and
methods that make easy the specifying of widgets for a GUI, their
layout and their interactions. The API is implemented, to varying
degrees, for use with the RGtk2
, tcltk
and qtbase
packages to
make desktop GUIs. This package provides the ability to serve such
interfacess with the apache
web server through Jeffrey Horner's rapache
module linking R
with `apache.
Implementing the API for web programming took some iterations. The
gWidgetsWWW
package (documented in the JSS article
http://www.jstatsoft.org/v49/i10/) was the first start. That package
was rewritten to take advantage of the Rook
package interfacing with
R
's httpd server and incorporated reference classes instead of the
proto
package. The result was gWidgetsWWW2
. With that package,
serving pages locally through Rook
works fine, but the package did
not play nicely with rapache
to serve pages remotely. (One could use
Rook
for this, but that method doesn't scale well.)
This package, gWidgetsWWW2.rapache
, then is a rewrite of the
plumbing to serve pages through apache and a streamlining of some of
the functionality. The main technical issue involved having the
callbacks in R share their environment amongst potentially many R
processes spawned by apache
. The solution used was to work to make
these environments small enough that they can be
serialized/unserialized to disk in a reasonable amount of time.
Once installed (see below) the use of the package is fairly simple. One places a script into the appropriate directory. The name of this script then maps to the URL that will call the script. A simple "hello-world" script might look like:
w <- gwindow('Hello world title')
g <- gvbox(cont=w)
b <- gbutton('Click me world', cont=g)
addHandlerChanged(b, function(h,...) {
galert('Hello world!')
})
The above script demonstrates the main points of the package:
A toplevel window maps to a web page (gwindow
)
Containers are used to organize the layout (gvbox
is a vertical
box container)
Controls are used to give the user a place to interact with
(gbutton
)
dialogs can be called (galert
)
interactivity is created by adding a handler
(addHandlerChanged
). A handler, or callback, is an R
function
that is evaluated in an event driven manner. In this case, the
"changed" event for a button object is triggered when the button is
clicked. The callback above produces some JavaScript that causes a
message to flash briefly on the screen.
If this script is saved as test.R
in the proper directory, then the
page will load through a url such as
http://www.youraddress.com/gw/test.R .
There are more examples in the examples
directory of the
package. A default installation has these accessible out of the box.
Containers are used to organize components (controls or other
containers). Unlike some GUI toolkits, the layout of child components
is integrated into the specification of the widget. The widget
constructors have a container
argument where the parent is
specified. (E.g., above we had gvbox(cont=w)
to next the vertical
box container inside the top-level window.) Layout arguments are
given at the time of construction, such as
expandor
fill`.
The package implements most -- but not all -- of the containers in the
gWidgets
API:
box containers, which pack children in left to right (horizontal) or
top to bottom (vertical): ggroup
, gvbox
(a vertical box),
gframe
(a decorated box) and gexpandgroup
(a box with a
disclosure icon to hide or show the contents).
a page layout container gborderlayout
for placing widgets at the
center or any of the four compass directions. These panels may have
optional titles and disclosure buttons.
a notebook container (gnotebook
) and the card container
(gstackwidget
) for displaying multiple pages one at a time
a container for forms gformlayout
that holds labels and controls
in an neatly organized manner.
Not implemented are the table layout container (glayout
) and the
paned container (gpanedgroup
). The gformlayout
container covers
most uses of the former and the latter can be implemented with
gborderlayout
(though that container does not like to be nested
within other containers.)
Containers have just a few methods:
visible<-
can be used to adjust if the components are visible
delete
can be used to remove a child component. Use add
to
restore.
The gWidgets
API has several controls specified. A control allows
the user to interact with the GUI or the programmer to display
information for the user. Most controls have some event that will
trigger a callback, if specified. For most controls, this event is
clear and is identified through the generic addHandlerChanged
. As
seen above, for a button this is the clicking of the button. A few
controls have more than one event they can respond. For these, there
are other addHandlerXXX
methods.
The basic controls are:
labeling controls: glabel
, gstatusbar
, ghtml
, gseparator
action controls: gbutton
, gmenu
(but there is no
gtoolbar
). These can be configured with sharable gaction
instances.
text controls: gedit
(single line) and gtext
(multi-line)
selection controls: gcheckbox
, gradio
, gcheckboxgroup
,
gcombobox
, gtable
(with selection="checkbox"
), gslider
,
gspinbutton
table displays: gtable
(gdf
is not (yet) implemented in
gWidgetsWWW2.rapache
but is in gWidgetsWWW2
)
graphics displays: gimage
, gcanvas
, gsvg
gpanel
for incorporating external JavaScript libraries
gfile
for uploading files
Implemented in gWidgetsWWW2
but not (yet) in gWidgetsWWW2.rapache
are gtree
, ggooglemaps
, and gdf
.
The main widget methods are
svalue
and svalue<-
for getting and setting the widget's main property
[
and [<-
for accessing the items that selection can occur from
(for the selection widgets). For such widgets, the S3 methods
names
, length
and dim
are typically implemented.
enabled<-
to change if a widget is sensitive to user input
visible<-
to change some visibility property of the widget. (There
are many overloads, but the default is to specify if a widget is
shown or hidden. One overload is for gtable
objects, where this
method is use to show or hide rows of the displayed data.)
tooltip<-
to specify a tooltip when the mouse hovers over the widget.
In addition to user-defined callbacks, all widgets with a state have a
transport handler defined for it that copies changes in the GUI back
to the R
process. As such, these callbacks happen often. Each callback
requires the handling R
process to unserialize an environment
containing the scope of the callback, call the handler, then serialize
the environment so it can be shared with other processes. This should
happen quickly. To make this the case, the environment must be kept
small. (In gWidgetsWWW2
it was seen that environments with reference
classes do not serialize small and so this took far too long.) To
ensure that a few compromises are needed:
you should avoid reference classes
you will need to load packages in the handler call. Packages
attached during a script (not loaded when rapache
is) are not
recorded or restored when the serialization takes place.
By employing these compromises, the response time for handling an
event was kept down to between 30 and 100ms. Not great, but
acceptable. We don't try to track keystrokes in gedit
or all updates
for the gslider
widget, but otherwise, most things work with this
response time.
The gtimer
constructor can be used to call a handler after some
prescribed period of time. The call can be repeated (the default) or
be one.shot
. This can be useful, say, if a long running process is
implemented. The user can interact with multiple R processes, so even
if one is tied up on a long computation, the interface should still be
usable. The gtimer
constructor should be able to poll until the
process is complete to carry back the results to the browser.
The manipulate
example shows one use of gtimer
that unfortunately
has no better idiom within this framework. That example has several
combo boxes. The value of a combobox is only available after the data
is loaded. This is done asynchronously. As such, the initial graphic
can not be made until after some unknow period. Unfortunately, there
is no hook (in gWidgetsWWW2.rapache
) to defer the graphic call to
happen until after that event. Instead, we just use a one-shot timer
with a heuristically chosen delay to pause before making the call to
create the initial graphic.
The display of graphics has several different options:
one can display images created through the png
driver, say, with
the gimage
widget
one can display svg files created with the svg
driver (or others)
with the gsvg
widget
one can use the canvas
package to display graphics through
gcanvas
.
The gcanvas
solution is best for quickly refreshing a graph produced
with R's base graphics. It does not work with lattice graphics or
ggplot2
. One can do low level commands on the JavaScript canvas
object.
The gsvg
solution is arguably the best looking, but flickers when
the graphic is refreshed making it not such a great choice for
interactive graphics.
The gimage
solution looks fine and is nearly as responsive as
gcanvas
.
There are a few examples in the examples
directory including an
implementation of RStudio's manipulate
package which shows how all
three can be used.
The gfile
constructor can be used to allow a user to upload data to
the server. A cap on the size of the upload can be specified when
rapache
is configured.
The widget calls any upload
callback when the data is successfully
uploaded. The example in the examples
directory shows how
gstackwidget
can be used to open a new page once the data is
uploaded. Otherwise, the callback can update portions of the current
GUI to reflect the change in state.
The uploaded file and its name are passed back to the callback via the
svalue
method (the path of the uploaded file) and [
(the original
name).
Each time a page is loaded (even when refreshed) a new environment is
made for evaluating any callbacks. As such, the data within a page is
not persistent across page loads unless it is programmed to be so. One
can write to the file system or data base to keep values across
sessions. (The package uses a redis
server for this purpose.)
The initial web page is constructed through various stages:
a basic template is loaded
some large JavaScript
libraries are loaded from CDNs. These will
be slow the first time, but should be cached so subsequent reloads
are not lengthy
Then a unique session ID is fetched from the server
Then the JavaScript
that creates the page layout is fetched from
the server
Finally the browser uses this JavaScript
to layout out the page.
Of these, the time of the last two steps depends on the complexity of
the GUI design. In basic testing with a modest machine, each
additional widget takes about 40ms to load with a base initialization
of around 100ms (40ms is a minimum, more complex widgets will take
longer). So a not-so-complex interface with 20 or so widgets will take
around 1 second or more to fetch from the server. It may take another
second for the browser to actually process the JavaScript
. Once
loaded the pages are fairly responsive, but the initial loading can
seem somewhat slow. A loading mask it employed to indicate that
activity is taking place.
The package uses the ExtJS
libraries provided by sencha.com
. These
are widely used in commercial and GPL projects. There may be some
restrictions on their use in your project, there may not be. I don't
know and don't want to.
JavaScript
is a language that can be used to manipulate a web page
in numerous ways. The package makes no assumption that an R user knows
JavaScript
. Rather, its novelty is to make it easy for non-web
programmers to create web pages. That being said, there are a few ways
one with JavaScript
knowledge can integrate that into the framework.
The ExtJS
API is very rich and far more extensive than that provided
by gWidgets
. The function call_ext
can be used to call a method on
an object not available through the base interface.
Each constructor has an argument ext.args
for passing in extra
arguments to the ExtJS
constructor. This is specified with a list
that is converted to a JavaScript
object through the function
list_to_object
.
When a callback is run, the package creates JavaScript
to pass back
to the browser. So a command like svalue(obj) <- "some value"
will
produce the appropriate JavaScript
. Each command has its
JavaScript
added to a queue. One can add their own JavaScript
commands to the queue via the push_queue
function. For example, to
use the browsers alert dialog add this to a handler
push_queue("alert('hello world')")
Finally, the gpanel
widget does nothing more than make a DIV
tag
on the page to be filled in by JavaScript
. The widget allows one to
load some external JavaScript
code asynchronously and call a handler
once loaded.
Web security is a big deal. There is the potential for malicious intent on the part of a user. This package provides no additional security features, but standard web safety guidelines should be followed. (E.g., don't eval code that a user can upload, ...) With the advent of cloud-based servers, you might want to deploy your server in a stand-alone manner.
$('#navbar-header').append("
Debugging scripts is made more difficult, as the scripts must be processed through the browser. Here are some useful tricks:
A test setup is useful where a local apache
server is used. (This
isn't helpful for windows users). Most -- but not all -- scripts can
be tested locally with gWidgetsWWW2
under Rook
. There are a few
differences in the API though, so this won't be fool proofed.
Errors are logged in apache's log file
print
statements do not appear in the log file, but those
generated with message
appear in apache's log file. This can be
useful to track down issues with the script.
There may be errors in gWidgetsWWW2.rapache
that are JavaScript
errors. Of course, the package author would love to hear about this,
so they can be fixed. But how can the end user tell? Well, a
JavaScript
console can be used. The main browsers include these:
Chrome and Safari have built in ones available through the menu bar,
Firefox has the excellent firebug
add on http://getfirebug.com/ .
Installing this software requires a few steps:
require(devtools)
install_github("gWidgetsWWW2.rapache", "jverzani")
rapache must be installed (follow the instructions here http://www.rapache.net).
rapache must be configured. There are 2-3 steps:
The mod_R.so
module must be loaded. Likely this is already done,
but if not you must add this to your apache configuration:
LoadModule R_module /usr/libexec/apache2/mod_R.so
The location of mod_R.so
is system dependent, the above is a Mac
OS X installation point.
RSourceOnStartup 'system.file("rapache", startup.R", package="gWidgetsWWW2.rapache")'
Alternatively, one can copy that file to the file system and modify
it. The option gWidgetsWWW2.rapache::script_base
is the most
important, as this specifies where to look for files.
Location
directive is given defining the urls for your scripts:<Location /gw>
LimitRequestBody 1024000
SetHandler r-handler
## from system.file("rapache", "www2.R", package="gWidgetsWWW2.rapache")
RFileHandler /Library/Frameworks/R.framework/Versions/2.15/Resources/library/gWidgetsWWW2.rapache/rapache/www2.R
</Location>
The above uses the base url http://your.address.com/gw/
for accessing
scripts. (So the url http://your.address.com/gw/ex.R
causes a search
for the file ex.R
in the directory (directories) specified through
the option gWidgetsWWW2.rapache::script_base
.
The LimitRequestBody
limits the size of file uploads. A value of 0
is use to indicate no limit.
The path of the www2.R
script comes from invoking system.file
with
the appropriate commands, as indicated in the comment. Alternatively,
this script can be copied and modified should there be a need.
One could add to the above apache configuration, say restricting access using one of apache's modules.
That's it. Not much harder than installing rapache
itself. With the
default installation, the examples directory from the package is where
URLs will be mapped to. Going to http://your.address.com/gw/
will
open a page with links.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.