How to use a custom renderer
In order to visualize a dataset, you will need to write
two functions: a placeholder where your graphs etc. will
later be rendered as well as the rendering function
itself. To learn more about the concepts of this dual
component approach, visit the website for the R Shiny
framework that GAMS MIRO is based upon:
https://shiny.rstudio.com/. In particular, we are using
Shiny Modules
to realize the interface between MIRO and your custom
renderer functions. The template for the two components
of every custom renderer is as follows:
mirorenderer_<symbolName>Output <- function(id, height = NULL, options = NULL, path = NULL){
ns <- NS(id)
}
renderMirorenderer_<symbolName> <- function(input, output, session, data, options = NULL, path = NULL, rendererEnv = NULL, views = NULL, attachments = NULL, outputScalarsFull = NULL, ...){
}
Note that you need to replace
<symbolName> with the
name of the GAMS symbol you want to use this renderer
for. Let's go through this code step by step. As
mentioned, for each custom renderer we need to specify
two functions: one that generates the placeholder and one
that fills this placeholder with data. The name of the
placeholder function must be postfixed with "Output" and the name of the function that specifies the actual
rendering must be prefixed with the keyword "render". Let's get back to our transport example. We
would like to see the flow of goods visualized on a map.
The GAMS symbol that contains the flow data is called
optSched. Thus, our initial
template looks like this:
mirorenderer_optschedOutput <- function(id, height = NULL, options = NULL, path = NULL){
ns <- NS(id)
}
renderMirorenderer_optsched <- function(input, output, session, data, options = NULL, path = NULL, rendererEnv = NULL, views = NULL, attachments = NULL, outputScalarsFull = NULL, ...){
}
Note:
MIRO expects your renderer names to follow this
strict convention. Note that the symbol name in your
renderer name should be converted to all lowercase.
Even if the symbol in GAMS is called
optSched, the renderer
function name should be
mirorenderer_optschedOutput
and
renderMirorenderer_optsched.
Note:
Custom renderer scripts must be located in a folder
named:
renderer_<modelname>
in your model directory. The name of the renderer
file should be:
mirorenderer_<symbolname>.R.
Both functions take a number of parameters. Let's start
with the placeholder function: Each custom renderer has
its own ID. In order to avoid
name collisions
with other custom renderers or functions of GAMS MIRO, we
need to prefix our input and output elements with this
ID. How should we prefix our custom input and output
functions, though? Fortunately, Shiny provides us with
the function:
NS(). This function takes the ID of our custom renderer as
its input and returns a function (functions that return
functions are often called closures in R) that
does the prefixing for us. Thus, whenever we want to
specify a new input or output element, we simply hand the
ID we would like to use for this element over to this
prefixing function (which in our case is bound to the
ns variable). We can also specify a height for
our renderer as well as the path where the renderer files
are located. We can also pass additional options to our
renderer functions.
Let's get back to our example. As we would like to
visualize our optimized schedule on an interactive map,
we choose the popular
Leaflet
library. Fortunately, there is already an R/Shiny
interface for this library:
Leaflet for R. This R package comes with the two functions:
leafletOutput() that generates the placeholder
and renderLeaflet() that renders a Leaflet map
object created by the leaflet() function which
takes our dataframe as its first argument. So let's put
the pieces together and extend our code:
mirorenderer_optschedOutput <- function(id, height = NULL, options = NULL, path = NULL){
ns <- NS(id)
leaflet::leafletOutput(ns("map"), height = height)
}
renderMirorenderer_optsched <- function(input, output, session, data, options = NULL, path = NULL, rendererEnv = NULL, views = NULL, attachments = NULL, outputScalarsFull = NULL, ...){
output$map <- leaflet::renderLeaflet(leaflet::leaflet(data))
}
Note that we used the aforementioned
ns() function to prefix the ID ("map") that we chose for our Leaflet element. Just like any
other placeholder element, the function
leafletOutput() generates an element that can
be accessed via the list-like output object.
Inside our rendering function we assign this object the
Leaflet map that is created by the
renderLeaflet() function. In case you find the
whole concept of having an output function, a rendering
function, an output object etc. still very confusing, you
should take a look at the
official tutorial series
for the Shiny framework.
To summarize: elements that generate data can be accessed
by the input object; elements that transform
data to some form of visualization via the
output object and any user-specific
information via the session object.
Note:
If you use functions from packages other than
shiny, DT,
rhandsontable, dplyr,
readr or the R base packages, make sure
you specify the package/namespace that exports this
function. In our example, the functions
leafletOutput,
renderLeaflet and leaflet are
exported from the package leaflet. So we
need to use the double colon operator to
access them (e.g. leaflet::leafletOutput).
The data that you want to visualize is supplied to your
rendering function by the
data argument - an R
tibble, a data structure that is very similar to a Data Frame.
In case you specified
additional datasets
to be communicated with your custom renderer,
data will be a named list of
tibbles where the names are the GAMS symbol names.
In addition, the values of all input and output scalars
can be accessed via the argument
outputScalarsFull (a tibble with the columns
scalar, description, value). The
function argument path is a string (a
one-dimensional character vector) that specifies the
absolute path to your
renderer_<modelname>
directory. This is useful if you want to include external
files in your custom renderer functions. Optional
parameters that you want to pass to the renderer can be
accessed via the argument
options - a (nested) list.
Tip:
The list options always has a nested list
with the key _metdata_, which gives you
access to the metadata of the symbol to render, such
as the symbol type (set, parameter, ...) and header
information (header name, alias, type). For example,
the symbol type can be accessed as follows:
options[["_metadata_"]][["symtype"]]
.
Now that we are familiar with the template that every
custom renderer builds upon, we are still missing one
fundamental concept so that we can use our custom
renderer: binding the renderer to dataset(s) we wish to
visualize.
This binding of GAMS parameter to renderer function is
specified - just like any other renderer binding - in the
<modelname>.json file;
more precisely the
dataRendering section. Let's
assume that in our transportation example the GAMS
parameter that specifies the optimal schedule is defined
as
optSched(lngp, latp, lngm, latm, plant, market)
where (lngP,latP) and
(lngm, latm) are the
coordinates of the plants and markets respectively. Our
transport.json file should then
look like this:
{
"dataRendering":{
"optSched":{
"outType":"mirorenderer_optsched",
"height":"700",
"options":{
"title":"Optimal transportation schedule"
}
}
}
}
As you can see we bound the GAMS parameter
optSched to our new custom
renderer mirorenderer_optsched.
Furthermore, we specified a parameter:
title that can be accessed by
our custom renderer via the
options list.
If we decided to run our MIRO app now, we still would not
be able to see anything other than a blank area. Thus, we
will need to fill our renderer with some life:
mirorenderer_optschedOutput <- function(id, height = NULL, options = NULL, path = NULL){
ns <- NS(id)
tagList(
textOutput(ns("title")),
leaflet::leafletOutput(ns("map"))
)
}
renderMirorenderer_optsched <- function(input, output, session, data, options = NULL, path = NULL, rendererEnv = NULL, views = NULL, attachments = NULL, outputScalarsFull = NULL, ...){
output$title <- renderText(options$title)
output$map <- leaflet::renderLeaflet(leaflet::leaflet(data) %>%
leaflet::addTiles() %>%
leaflet::addMarkers(~lngp, ~latp, label = ~plant)
)
}
We have added a new placeholder for the title. Note the
use of the tagList() function. Since every R
function has a single return value which is either the
last evaluated expression of the function or the argument
to the first return() function that is
encountered in the function body, we need to return a
list object. A tagList() is simply a list with
an additional attribute to identify that the elements are
html tags.
Within our rendering function, we set the title, add the
default
OpenStreetMap
tiles as well as some markers for our plants.
Note:
The syntax ~lngp that you see here is
simply a shorthand for data$lngp - the
pipe operator a(x) %>% b(y) a
shorthand for
tmp <- a(x); b(tmp, y)
If you run your app now, you will be able to see a map with
markers at the coordinates of the plants as you specified
them in your GAMS sets:
lngp and
latp. When you hover over the markers, you will
be able to see the names of the plants as defined by the
set:
plants. You can see a screenshot of the
result below:
If you read until this point, you might have noticed that
there is a parameter in the renderer function that we did
not talk about yet: rendererEnv. This
parameter is a static R environment that is persistent
across different calls to the renderer function. Each
time the data of your widget is updated (e.g. due to the
user loading a new scenario from the database), your
renderer function is called. One case where this can
become problematic is when you make use of
observers
in your renderer functions. Every time your renderer
function is called, all observers are re-registered,
which leads to duplicated observers. To avoid this
problem, you must ensure that observers are cleaned up
when they are no longer needed. You do this by assigning
them to the
rendererEnv environment. An example where this
is extensively used is the model tsp, which
can be found in the MIRO model library.
You now know everything you need in order to get started
writing your own custom renderers! Congratulations! In
case you create a new renderer that you would like to
share so that others can benefit from your work as well,
please
contact us!
Views
In cases where your renderers are interactive, you might
want to allow users to store the current state of your
renderer and load it later with a single click. This is
what MIRO views were designed for. A MIRO view can
be any (nested) list that is JSON serializable. Views are
bound to a particular GAMS symbol and each view has a
unique id. One example of a renderer where the MIRO views
API is used is the
pivot table
renderer.
You might have already seen that there is another
argument in the renderer function that we have not yet
talked about: the views argument. This is a
reference to an instance of the views R6 class. You can
get, add and remove views as well as register callbacks
via this object. The following section explains how to
use the API.
Get view data
views$get(session, id = NULL, filter = c("all", "global", "local"))
session |
The session object passed to the custom
renderer or the name of a GAMS symbol
|
id |
id of the view to load |
filter |
Whether to retrieve app-wide
global views only, scenario-specific
local views only, or
all views.
|
In case id is NULL: a named list
where the names are the ids of the views and the values
are the view data.
In case id is not NULL: a list with
the data of the view. Will throw an error of class
error_not_found in case the id
provided does not exist.
This method allows you to retrieve the data of views
registered for the symbol. You can retrieve either the
data of all views (id is NULL) or
the data of a specific view (id not
NULL).
# myViews <- list(filter1 = list(filter = list(element1 = "Value 1", element2 = c("Value 2", "Value 4"))),
# filter2 = list(filter = list(element3 = "Value 7")))
views$get(session, "filter1")
#> $filter
#> $filter$element1
#> [1] "Value 1"
#>
#> $filter$element2
#> [1] "Value 2" "Value 4"
views$get(session)
#> $filter1
#> $filter1$filter
#> $filter1$filter$element1
#> [1] "Value 1"
#>
#> $filter1$filter$element2
#> [1] "Value 2" "Value 4"
#>
#>
#>
#> $filter2
#> $filter2$filter
#> $filter2$filter$element3
#> [1] "Value 7"
views$get(session, "filter3")
#> Error: View with id: filter3 could not be found.
Get view ids
views$getIds(session, filter = c("all", "global", "local"))
session |
The session object passed to the custom
renderer or the name of a GAMS symbol
|
filter |
Whether to retrieve app-wide
global views only, scenario-specific
local views only, or
all views.
|
Character vector with the view ids registered for the
symbol.
Retrieves the view ids that are currently registered for
the symbol. It is equivalent to
names(views$get(session))
.
# myViews <- list(filter1 = list(filter = list(element1 = "Value 1", element2 = c("Value 2", "Value 4"))),
# filter2 = list(filter = list(element3 = "Value 7")))
views$getIds(session)
#> [1] "filter1" "filter2"
Add view
views$add(session, id, viewConf)
session |
The session object passed to the custom
renderer or the name of a GAMS symbol
|
id |
id of the view to add |
viewConf |
The view configuration |
Adds/registers a new view. The view configuration can be
any JSON serializable list. If a view with the same id
already exists for the symbol, the previous view is
replaced. In case the current renderer is read-only (not
a sandbox scenario), an error of class
error_readonly is thrown. You
can test whether the renderer is read-only with the
method
views$isReadonly(session)
.
# myViews <- list(filter1 = list(filter = list(element1 = "Value 1", element2 = c("Value 2", "Value 4"))),
# filter2 = list(filter = list(element3 = "Value 7")))
views$add(session, "filter3", list(filter = list(element4 = "Value 10")))
Remove view
views$remove(session, id)
session |
The session object passed to the custom
renderer or the name of a GAMS symbol
|
id |
id of the view to remove |
Removes a local view with the specified id. If no view
with this id exists, an error of class
error_not_found is thrown. In
case the current renderer is read-only (not a sandbox
scenario), an error of class
error_readonly is thrown. You
can test whether the renderer is read-only with the
method
views$isReadonly(session)
. Note that global views cannot be removed.
# myViews <- list(filter1 = list(filter = list(element1 = "Value 1", element2 = c("Value 2", "Value 4"))),
# filter2 = list(filter = list(element3 = "Value 7")))
views$get(session)
#> $filter1
#> $filter1$filter
#> $filter1$filter$element1
#> [1] "Value 1"
#>
#> $filter1$filter$element2
#> [1] "Value 2" "Value 4"
#>
#>
#>
#> $filter2
#> $filter2$filter
#> $filter2$filter$element3
#> [1] "Value 7"
views$remove(session, "filter2")
views$get(session, "filter1")
#> $filter
#> $filter$element1
#> [1] "Value 1"
#>
#> $filter$element2
#> [1] "Value 2" "Value 4"
views$remove(session, "filter2")
#> Error: View with id: filter2 does not exist, so it could not be removed.
Register update callback
views$registerUpdateCallback(session, callback)
session |
The session object passed to the custom
renderer or the name of a GAMS symbol
|
callback |
a callback function |
You can register a callback function that is triggered
whenever the view data of a symbol is modified
outside the renderer. This happens when a user
modifies the view data via the
metadata dialog. Note that the callback function is not triggered by
the methods views$add
or
views$remove
described
above.
updateCallback <- function(){
print(sprintf("View data changed from outside! New view ids: %s.",
views$getIds(session)))
}
views$registerUpdateCallback(session, updateCallback)
Attachments
Custom renderers and custom input widgets can access
existing
attachments
as well as add new ones. So, for example, if your model
run depends on the existence of a certain attachment, the
user can be shown a hint in the custom widget whether
this attachment already exists or not. Furthermore, a
corresponding upload field can be displayed, which can be
used to add missing attachments. This is just one of many
possibilities offered by the attachments interface.
Attachments are bound to a MIRO scenario and each
attachment has a unique id.
Attachments can be retrieved, downloaded, added and
removed via the
attachments argument of a custom renderer
function. As with the views argument, this is
a reference to an instance of the attachments R6 class.
The following section explains how to use the API.
Get attachment ids
Character vector with the attachment ids that are part of
the sandbox scenario.
Retrieves the attachment ids that are currently
registered for the scenario.
attachments$getIds()
#> [1] "file1.txt" "file2.gdx" "file3.xls"
Add Attachment
attachmenets$add(session, filePaths, fileNames = NULL, overwrite = FALSE, execPerm = NULL)
session |
The session object passed to the custom
renderer or NULL if used from a
custom data connector
|
filePaths |
Character vector with file paths to read data from
|
fileNames |
Custom name(s) of the file(s) (optional) |
overwrite |
Boolean that specifies whether existing files
should be overwritten
|
execPerm |
Vector with execute permissions (must be NULL or
logical vector with the same length as
filePaths). By default, all files have
execute permissions.
|
Adds/registers new attchment(s). These can be files from
the local file system, files created in the renderer
itself, files accessed from a REST API call or any
others. Attachments can be saved under a new name. If an
attachment with the same name already exists, it can be
overwritten. In addition, it can be specified whether the
attachment may be
read by the underlying GAMS model. In case the current renderer is read-only (not a
sandbox scenario), an error of class
error_readonly is thrown. You
can test whether the renderer is read-only with the
method
attachments$isReadonly(session)
.
In the example below, an attachment is added using the
shiny
fileInput
widget that triggers an observer when the user uploads a
local file.
observeEvent(input$fileInput, {
file <- input$fileInput
filePath <- file$datapath
attachments$add(session, filePath, "custom_attachment.txt", overwrite = TRUE, execPerm = FALSE)
})
attachments$getIds()
#> [1] "custom_attachment.txt"
Save attachments
attachments$save(filePaths, fileNames, overwrite = TRUE)
filePaths |
Character vector where to save files. Either
directory name or directory + filename (in the
latter case the length of fileNames must
be 1).
|
fileNames |
Character vector with names of the files to
download
|
overwrite |
Whether to overwrite existing files. |
Stores file(s) at given location(s). If the user should
be able to download attachments directly in the custom
renderer, this is possible with the
save method. Also, it can be used to access
the data of an attachment in the custom renderer and
process it further. For this purpose the attachment can
be saved to disk and read from there.
In the example below, a
download handler
is used to write an attachment file.txt to disk.
If the file.txt is not found in the attachments,
an error.txt file is downloaded instead.
output$downloadButton <- downloadHandler(
filename = function(){
if(!"file.txt" %in% attachments$getIds()){
return("error.txt")
}
return("file.txt")
},
content = function(file){
if(!"file.txt" %in% attachments$getIds()){
return(writeLines("error", file))
}
attachments$save(file, "file.txt")
}
)
Set execution permission
attachments$setExecPerm(session, fileNames, execPerm)
session |
The session object passed to the custom
renderer or NULL if used from a
custom data connector
|
fileNames |
Vector of file names |
execPerm |
Logical vector (same length as
fileNames or length 1) that specifies
whether files can be read/executed by GAMS
|
Sets read/execute permission for particular
attachment(s). Note that all files that you allow your
model to read must first be downloaded to the working
directory before GAMS is run. It is therefore advisable
to select as readable only those files that are actually
relevant for the optimization run.
attachments$getIds()
#> [1] "file1.txt" "file2.gdx" "file3.xls"
attachments$setExecPerm(session, "file1.txt", execPerm = TRUE)
Remove attachment
attachments$remove(session, fileNames, removeLocal = TRUE)
session |
The session object passed to the custom
renderer or NULL if used from a
custom data connector
|
fileNames |
File name(s) of attachment(s) to remove |
removeLocal |
Whether to remove file(s) from disk |
Removes attachment(s) with the specified filename(s). If
no attachment with this name exists, an error of class
error_not_found is thrown. In
case the current renderer is read-only (not a sandbox
scenario), an error of class
error_readonly is thrown. You
can test whether the renderer is read-only with the
method
attachments$isReadonly(session)
.
attachments$getIds()
#> [1] "file1.txt" "file2.gdx" "file3.xls"
attachments$remove("file1.txt", removeLocal = FALSE)
attachments$getIds()
#> [1] "file2.gdx" "file3.xls"