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"