#' Class for the analysis report generator.
#'
#' An object of this class can be considered essentially as a graphical logger for tables, plots,
#' html widgets and other
#' objects created during the execution of your analysis pipeline.
#' The object of this class generates a Markdown text and converts it into a multi-page static
#' Web site with its \code{save} method. Sections, tables and plots are auto-numbered and
#' provided with their own URLs.
#'
#' In the popular R literate programming frameworks like
#' \href{https://cran.r-project.org/package=knitr}{\pkg{knitr}} package,
#' the execution flow of the analysis proceeds linearly from the top to the bottom
#' code chunks on a document page. In contrast to that, with the "logger" approach adopted
#' by \code{anrep}, the user inserts logging calls into arbitrary places of their existing analysis
#' code. It does not matter how deeply nested these calls are inside functions, loops or
#' conditional branches. For example, using regular R loop constructs, it becomes trivial to
#' generate multiple report section that vary only by the parameter values supplied to the
#' otherwise identical analysis protocol.
#'
#' This class relies heavily on the Markdown generation infrastructure provided in the
#' \href{https://CRAN.R-project.org/package=pander}{\pkg{pander}} package.
#' The user can manipulate various \pkg{pander}
#' global options by calling \code{\link[pander]{panderOptions}} method, in order to
#' fine-tune various defaults used in generating the Markdown representations of R objects.
#' The \code{anrep} class was inspired in part by the \code{\link[pander]{Pandoc}} design.
#' The \pkg{anrepr} package provides \code{\link{set_default_external_options}} method
#' that resets various global options in \pkg{pander} and \pkg{ggplot2} packages to the
#' values tuned for best default appearance in the final HTML reports generated by this
#' class.
#'
#' You can create the new instance of this class as \code{report = anrep(...)} where
#' the available constructor arguments are described for the \code{initialize} method
#' below. The object will be writing its output files into the directory that was current
#' when it was created, silently over-writing existing content. In other words, it assumes
#' that the current working directory is dedicated by the user for the report output.
#'
#' In a typical use pattern, you would create singleton report object in a global environment
#' (such as \code{report <<- anrep()}), and then call methods on this global object in various
#' locations of your code. You can also create \code{anrep} object in a local environment and
#' pass this object around any way you want.
#' Because this is the R reference class, the report object is passed by reference without creating
#' new copies.
#' See the description of the \code{add.header} method for growing the hierarchical section
#' structure of your report.
#'
#' @examples \dontrun{
#' ## Refer to the vignettes to see how to build a complete report by calling the methods of this
#' ## class.
#' }
#' @export anrep
#' @importFrom methods setRefClass new
anrep <- setRefClass('anrep',
fields = list(
'author' = 'character',
'title' = 'character',
'date' = 'character',
'entries' = 'list',
'sections' = 'list',
'incremental.save' = 'logical',
'out.file' = 'character',
'out.formats' = 'ANY',
'self.contained.html' = 'logical',
'self.contained.data' = 'logical',
'object.index' = 'list',
'data.dir' = 'character',
'graph.dir' = 'character',
'widget.dir' = 'character',
'widget.deps.dir' = 'character',
'resources.dir' = 'character',
'section.path' = 'list',
'echo' = 'logical',
'under.knitr' = 'logical',
'knitr.auto.asis' = 'logical',
'knitr.meta.attr' = 'list'
)
)
anrep$methods(initialize = function(
title = "Analysis Report",
author = "",
date = base::date(),
out.file = "report",
out.formats = NULL,
incremental.save = FALSE,
self.contained.html=TRUE,
self.contained.data=FALSE,
echo=FALSE,
knitr.auto.asis=TRUE,
...
) {
"Construct new instance.
parameter: title Report title string
parameter: author Author to show below the title
parameter: date of the report
parameter: out.formats Convert generated Markdown to these formats (using Pandoc binary executable). In principle,
the value can be any of the output formats supported by Pandoc, plus an additional value `knitr`.
However, not all features of the report work in every format. If NULL, the default output format will be 'html',
unless we detect that we are executing from a Knitr session, in which case the format will be set to 'knitr'.
With 'html', the final output will be one or more HTML files on disk. With 'knitr', the method `anrep$save()`
will return a string with a single concatenated report (any subreport designations will be ignored). You can
also just type the report variable name or print it instead of calling `anrep$save()` method under Knitr.
If your are creating the report from Knitr but still want HTML output instead of a string, then set this parameter
explicitly to 'html'.
parameter: knitr.auto.asis This has the same effect as `pander::panderOptions` option with the same name. If set,
this report object will try to detect if it is being executed from knitr, and wrap the final returned
Markdown in knitr::asis_output class, so that Markdown is rendered as such, rather than as a literal string.
You would only want to set this to FALSE if you want to obtain the literal Markdown string under Knitr session
for viewing or further manipulation.
parameter: self.contained.html Embed images (default resolution versions) and stylesheets into each HTML file.
Note that generated data files and widget files will still be referenced by links unless
self.contained.data is set to TRUE. In any case, the report directory as a whole will be portable in a sense
that it can be archived into a different location or computer and viewed where.
parameter: self.contained.data Embed everything including hi-res images, exported CSV files and widgets
into the HTML files. These entities will still appear as links on the HTML
pages.
parameter: out.file Base for the output report file names (Markdown and final formats
such as HTML). Files will be named like 1-<out.file>.html.
"
.self$title=title
.self$author=author
.self$date=date
.self$out.file=out.file
.self$out.formats=out.formats
.self$incremental.save=incremental.save
.self$self.contained.html=self.contained.html
.self$self.contained.data=self.contained.data
.self$object.index=list(table=1,figure=1)
.self$data.dir = "data"
.self$echo = echo
.self$under.knitr = FALSE
.self$knitr.auto.asis = knitr.auto.asis
.self$knitr.meta.attr = knitr.meta.attr
if(!is.character(out.file)) {
stop("The `out.file`` argument must be a string")
}
if(isTRUE(getOption('knitr.in.progress')) &&
requireNamespace('knitr', quietly = TRUE)) {
.self$under.knitr = TRUE
}
if(is.null(.self$out.formats)) {
if(.self$under.knitr) {
.self$out.formats = "knitr"
}
else {
.self$out.formats = "html"
}
}
cleanup.before = TRUE
if("knitr" %in% .self$out.formats) {
# For some reason when using devtools::build_vignettes(), deletion in successive
# reports steps on each other.
# WARNING: This is a temprary hack. We probably need to generate unique dir names instead,
# but need to figure out what to do with graph.dir
cleanup.before = FALSE
}
#unlink(.self$data.dir,recursive=TRUE,force=TRUE)
dir.create(.self$data.dir, showWarnings = FALSE, recursive = TRUE)
.self$graph.dir = pander::evalsOptions("graph.dir")
if(cleanup.before) {
if(file.exists(.self$graph.dir) && (normalizePath(getwd()) != normalizePath(.self$graph.dir))) {
unlink(.self$graph.dir,recursive=TRUE,force=TRUE)
}
}
dir.create(.self$graph.dir, showWarnings = FALSE, recursive = TRUE)
.self$widget.dir = "." #normalizePath(graph.dir,winslash = "/")
.self$widget.deps.dir = "widget_deps" #html.path(.self$widget.dir,"widget_deps")
if(cleanup.before) unlink(.self$widget.deps.dir,recursive=TRUE,force=TRUE)
dir.create(.self$widget.deps.dir, showWarnings = FALSE, recursive = TRUE)
.self$resources.dir = "resources"
if(cleanup.before) unlink(.self$resources.dir,recursive=TRUE,force=TRUE)
dir.create(.self$resources.dir, showWarnings = FALSE, recursive = TRUE)
.self$section.path = new.section.path()
.self$incr.section()
.self$push.section()
callSuper(...)
})
anrep$methods(priv.is.knitr.output = function(out.formats=NULL) {
.out.formats = out.formats
ret = FALSE
if(is.null(.out.formats)) .out.formats = .self$out.formats
if("knitr" %in% .out.formats[1]) {
if(length(.out.formats)!=1) {
stop("Multiple output formats are not allowed when one of them is 'knitr'")
}
ret = TRUE
}
ret
})
anrep$methods(priv.enter.add = function() {
state = list(knitr.auto.asis = pander::panderOptions('knitr.auto.asis'))
pander::panderOptions('knitr.auto.asis', FALSE)
state
})
anrep$methods(priv.exit.add = function(state) {
if(state$knitr.auto.asis) pander::panderOptions('knitr.auto.asis', TRUE)
})
## private service method - should be called whenever an element is
## appended to the .self$entries
anrep$methods(priv.append.section = function() {
.self$incr.section.if.zero()
.self$sections[[length(.self$sections)+1]] = .self$get.section()
})
## private service method - should be called whenever an element
## that needs its own index in the report is
## appended to the .self$entries
anrep$methods(priv.append.index = function(type) {
val = .self$object.index[[type]]
if(is.null(val)) {
val = .self$object.index[[type]] = 1
}
.self$object.index[[type]] = val + 1
return(val)
})
anrep$methods(private.add.object = function(x) {
.self$priv.append.section()
.self$entries = c(.self$entries, list(
list(result = x))
)
invisible(x)
})
anrep$methods(private.add.paragraph = function(x) {
.self$priv.append.section()
.self$entries = c(.self$entries, list(
list(result = pander::pandoc.p.return(x)))
)
invisible(x)
})
anrep$methods(add.p = function(x,rule=FALSE,echo=NULL,...) {
"Add new paragraph
parameter: x Text to write in the new paragraph
parameter: rule Also add horizontal rule
"
if(rule) {
.self$private.add.paragraph(pander::pandoc.horizontal.rule.return())
}
if(first_defined_arg(echo,.self$echo)) {
cat(format.section.path(.self$get.section()),x,"\n")
}
return(.self$private.add.paragraph(x,...))
})
anrep$methods(get.section = function() {
invisible(.self$section.path)
})
anrep$methods(set.section = function(x) {
.self$section.path = x
invisible(.self$section.path)
})
anrep$methods(incr.section = function(has.header=NULL) {
.self$section.path = incr.section.path(.self$section.path,has.header=has.header)
invisible(.self$section.path)
})
anrep$methods(incr.section.if.zero = function(has.header=NULL) {
sec.path = .self$get.section()
if(sec.path[[length(sec.path)]]$num==0) return (.self$incr.section(has.header = has.header))
invisible(.self$get.section())
})
anrep$methods(push.section = function(sub=FALSE,has.header=FALSE) {
.self$section.path = push.section.path(.self$section.path,sub=sub,has.header=has.header)
invisible(.self$section.path)
})
anrep$methods(pop.section = function() {
.self$section.path = pop.section.path(.self$section.path)
invisible(.self$section.path)
})
anrep$methods(add.header = function(title,level=NULL,section.action="incr",sub=FALSE,echo=NULL,...) {
"Add new header and automatically start new report section.
You should rarely provide any arguments other than title.
Instead, combine a call to this method with the \\%anrep??\\% infix operators, such as:
report$add.header('Title') \\%anrep>>\\% \\{
report$add.descr('My code block')
\\}
return: The instance of the anrep class on which this method was called. This is needed
for the chaining with the infix %anrep??% operators to allow them updating the state
of the anrep object.
"
x = title
if(sub) {
section.action = "push"
}
sec.path = switch(EXPR=section.action,
incr=.self$incr.section(has.header = TRUE),
push=.self$incr.section(has.header = TRUE),
keep=.self$get.section())
num = extract.path.nums.section.path(sec.path)
if (is.null(level)) {
##headers will shift to the left above level 5 in HTML output
level = min(5,length(num))
}
x = paste(format.section.path(sec.path),x)
##newlines currently break header formatting, remove them
x = gsub("\n"," ",x)
.self$add.p(pander::pandoc.header.return(x,level=level,...),echo=echo)
if(section.action=="push") {
sec.path = .self$push.section(sub=sub,has.header=T)
}
# need to return self for the infix operators to work
invisible(.self)
})
anrep$methods(format.caption = function(caption,sec.path=NULL,type=NULL, collapse=", ", elements=FALSE) {
if(is.null(caption)) {
caption = ""
}
if(!is.null(collapse)) {
caption = paste0(caption,collapse = collapse)
}
anchor = NULL
ind = NULL
anchor.name=NULL
if(!is.null(caption)) {
if(is.null(type)) {
type = ""
ind = ""
}
else {
ind = .self$priv.append.index(type)
}
if(nzchar(ind)) {
anchor = sprintf('%s.%s',type,ind)
anchor.name = sprintf("%s %s.",stringr::str_to_title(type),ind)
name = anrep.anchor.return(anchor,anchor.name)
}
else {
name = ""
}
if(is.null(sec.path)) {
.self$incr.section.if.zero()
sec.path = .self$get.section()
}
caption = paste(format.section.path(sec.path),name,caption)
if(substr(caption,nchar(caption),nchar(caption)+1)!=".") {
caption = paste(caption,".",sep="")
}
}
if(!elements) return (caption)
else {
return (list(caption=caption,
anchor=anchor,
anchor.name=anchor.name,
ind=ind)
)
}
})
anrep$methods(add.widget = function(x,
caption=NULL,
show.image.links=TRUE,
width = 800,
height = 800,
new.paragraph=TRUE,
data.export = NULL,
data.export.descr = NULL,
show.inline = TRUE,
self.contained.data = NULL,
...) {
"Add htmlwidget object.
parameter: caption Text to add as numberd caption
parameter: show.image.links Include a link to the generated widget file to the caption
parameter: data.export File to link from caption. This can be used if the widget has some
associated dataset saved as a separate file, and that datset can be useful to the user by
itself.
parameter: data.export.descr Description for the data.export file, to be added to the
caption.
parameter: show.inline Show widget inline in the current report page (currently IFRAME is used
for that, but this may change). If FALSE, only provide
a link to the widget from the caption. You might want to set this to FALSE if the widget is
very expensive to render in the browser (for example, a multiple sequence alignment widget
for thousands of sequence). In this case, the user will have to navigate to the link in order
to see the widget.
parameter: self.contained.data Embed a separate widget object and optional datasets as data URLs
into the resulting HTML
"
if(!requireNamespace("htmlwidgets", quietly = TRUE)) {
stop("This method requires package htmlwidgets.")
}
.self.contained.data = first_defined_arg(self.contained.data,.self$self.contained.data,F)
if(is.null(width)) {
width = pander::evalsOptions("width")
}
if(is.null(height)) {
height = pander::evalsOptions("height")
}
if(new.paragraph) {
.self$add.p("")
}
caption.type = "widget"
caption.arg = caption
if(is.null(caption)) {
caption = ""
}
caption.el = .self$format.caption(caption,type=caption.type,elements = TRUE)
caption = caption.el$caption
name.base=.self$make.file.name.base.from.caption.elements(caption.el)
fn = .self$make.file.name(name.base,dir=.self$widget.dir,make.unique=TRUE,name.base.first = TRUE,name.ext = ".html")
## we do not need a separate widget file if embedding into knitr doc,
## and with .self.contained.data the final doc gets too large because
## all JS libs are encoded into data URL for every instance of the widget
if(!.self$priv.is.knitr.output()) {
htmlwidgets::saveWidget(x,fn,selfcontained = .self.contained.data,libdir=.self$widget.deps.dir)
caption.res = sprintf("Click to see HTML widget file in full window: %s",
anrep.file.link.verbatim.return(fn,selfcontained=.self.contained.data,
mime="text/html"))
caption = paste(caption,caption.res)
}
if(!is.null(data.export)) {
caption = sprintf("%s. Dataset is also saved here: %s",
caption,
anrep.file.link.verbatim.return(data.export,
selfcontained=.self.contained.data,
mime="application/octet-stream"))
if(!is.null(data.export.descr)) {
caption = sprintf("%s, %s",caption,data.export.descr)
}
}
if(!is.null(caption.arg)) {
.self$add.p(caption)
.self$add.p("")
}
if(show.inline) {
if(!.self$priv.is.knitr.output()) {
iframe.tpl = '<iframe style="max-width=100%"
src="fn"
sandbox="allow-same-origin allow-scripts"
width="100%"
height="%s"
scrolling="no"
seamless="seamless"
frameBorder="0"></iframe>'
iframe.tpl = '<iframe src="%s" width="%s" height="%s"> </iframe>'
.self$add(sprintf(iframe.tpl,
fn,
width,
height))
}
else {
x_kp = htmlwidgets:::knit_print.htmlwidget(x)
.self$add.p(as.character(x_kp))
kn_at = attr(x_kp,"knit_meta")
if(!is.null(kn_at)) {
.self$knitr.meta.attr = c(.self$knitr.meta.attr,list(kn_at))
}
}
}
return(invisible(x))
})
anrep$methods(add = function(x,
caption=NULL,
show.image.links=TRUE,
caption.type=NULL,
graph.output = pander::evalsOptions("graph.output"),
hi.res = pander::evalsOptions("hi.res"),
new.paragraph=TRUE,
self.contained.data=NULL,
...) {
"Add plot object, or any other kind of object.
parameter: caption Text that will be added as a numbered caption
parameter: show.image.links Include links to generated plot image(s) to the caption
parameter: self.contained.data Embed hi.res link as data URL into the resulting HTML
parameter: ... Other arguments to pander::evals()
You should use more specific methods when adding tables (add.table()) or vectors
(add.vector()) because those will try to coerce their inputs into the compatible
format, and allow you setting additional arguments.
"
par_fr = parent.frame()
if(!identical(.GlobalEnv, par_fr)) {
env = list2env(as.list(par_fr, all.names=TRUE),parent=parent.frame(2))
}
else {
env = NULL
}
.self.contained.data = first_defined_arg(self.contained.data,.self$self.contained.data,F)
## work around pander bug in v.0.6.0 where hi.res is created as a broken symlink plots/normal.res
## instead of just normal.res
if(graph.output %in% c('svg','pdf')) {
hi.res = FALSE
}
res = pander::evals(deparse(match.call()[[2]]),env=env,
graph.output = graph.output,
hi.res = hi.res,
graph.dir = .self$graph.dir,
...)
if(new.paragraph) {
.self$add.p("")
}
mime = paste0("image/",graph.output)
if(graph.output=="svg") mime = paste0(mime,"+xml")
is.image = FALSE
caption.res = ""
res_out = list()
for (r in res) {
if(any(r$type=="image")) {
if(show.image.links) {
rr = r$result
caption.res = paste(caption.res,
sprintf("Image file: %s.",
anrep.file.link.verbatim.return(as.character(rr),
selfcontained=.self.contained.data,
mime=mime))
)
hres.ref = attr(rr,"href")
if(!is.null(hres.ref)) {
caption.res = paste(caption.res,
sprintf("High resolution image file: %s.",
anrep.file.link.verbatim.return(hres.ref,
selfcontained=.self.contained.data,
mime=mime))
)
if(isTRUE(.self.contained.data)) {
## do not link image to hi-res image file
attr(r$result,"href") = NULL
}
}
}
is.image = TRUE
}
res_out = c(res_out,list(r))
}
res = res_out
if(is.null(caption.type)) {
if(is.image) {
caption.type = "figure"
}
}
if(!is.null(caption)) {
caption = .self$format.caption(caption,type=caption.type)
}
if(nzchar(caption.res)) {
caption = paste(caption,caption.res)
}
if(!is.null(caption)) {
.self$add.p(caption)
.self$add.p("")
}
.self$entries = c(.self$entries,res)
.self$priv.append.section()
return(invisible(res))
})
anrep$methods(add.list = function(x,...) {
"Add list object"
return(.self$add(as.list(x),...))
})
anrep$methods(make.file.name = function(name.base="",
make.unique=TRUE,
dir=NULL,
sec.path=NULL,
name.base.first=FALSE,
name.ext="") {
"Return new file name that you can use to write your data to be included with the report.
parameter: name.base Basename for the file. If make.unique is TRUE, extra suffix will be generated to
make the final file name unique
parameter: dir Normally should be left unset (NULL), in which case the file name will be located
under the data directory of the report
parameter: sec.path For internal use. Default value will result in a stringified version of the
current section number becoming part of the file name
parameter: name.base.first If adding section path to the file name, add it after the name.base
"
if(is.null(dir)) {
dir = .self$data.dir
}
if(length(name.base)==0) {
name.base = ""
}
if(name.base=="" && !make.unique) {
stop("Need either non-empty name.base or make.unique=TRUE")
}
if(is.null(sec.path)) {
sec.path = .self$get.section()
}
fn.start = format.section.path.as.file(sec.path)
if(name.base.first) {
fn.comps = c(name.base,fn.start)
}
else {
fn.comps = c(fn.start,name.base)
}
fn.comps = paste0(fn.comps,collapse = "-")
if(make.unique) {
fn.comps = sprintf("%s-",fn.comps)
fn = tempfile.unix(fn.comps,tmpdir=dir,fileext=name.ext)
}
else {
fn = file.path(dir,paste0(fn.comps,name.ext),fsep="/")
}
return(fn)
})
anrep$methods(make.file.name.base.from.string = function(x) {
name.base = anrep.str_to_file_name(x,20)
## strip all tail dots since we usually add extension later on
gsub("\\.+$","",name.base)
})
anrep$methods(make.file.name.base.from.caption.elements = function(x) {
if(!is.null(x$anchor.name)) s = x$anchor.name
else s = x$caption
.self$make.file.name.base.from.string(s)
})
anrep$methods(add.file = function(x,
caption=NULL,
wrap.caption=TRUE,
skip.if.empty=FALSE,
...) {
"Add a link to a file.
It is recommended that the file name is generated with make.file.name method.
parameter: wrap.caption Escape Markdown tags in caption
"
if (wrap.caption && !is.null(caption)) {
caption = anrep.escape.special(caption)
}
caption = .self$format.caption(caption,type="dataset")
if(is.null(x)) {
if(!skip.if.empty) {
if(!is.null(caption)) {
.self$add.p(caption)
}
return(.self$add.p("Empty dataset"))
}
else {
return(.self)
}
}
caption = paste(caption,
"Dataset is saved in a file (click to download)",
anrep.link.verbatim.return(x)
)
return(.self$add.p(caption))
})
anrep$methods(add.image = function(x,
caption=NULL,
wrap.caption=TRUE,
skip.if.empty=FALSE,
width=pander::evalsOptions("width"),
height=NULL,
...) {
"Add the image that already exists as a file.
It is recommended that the file name is generated with make.file.name method.
parameter: wrap.caption Escape Markdown tags in caption
parameter: height Default is NULL to let width define it without breaking aspect ratio
"
if(!is.na(caption)) {
if (wrap.caption && !is.null(caption)) {
caption = anrep.escape.special(caption)
}
caption = .self$format.caption(caption,type="dataset")
if(is.null(x)) {
if(!skip.if.empty) {
if(!is.null(caption)) {
.self$add.p(caption)
}
return(.self$add.p("Empty dataset"))
}
else {
return(.self)
}
}
caption = paste(caption,
"Image is available in a file (click to download)",
anrep.link.verbatim.return(x)
)
.self$add.p(caption)
}
if(!is.null(height)) {
img_attr = sprintf("width=%s height=%s",width,height)
}
else {
img_attr = sprintf("width=%s",width)
}
return(.self$add.p(sprintf("[{ %s }](%s)",x,img_attr,x)))
})
anrep$methods(add.table = function(x,
caption=NULL,
show.row.names=is.matrix(x),
wrap.caption=TRUE,
wrap.vals=TRUE,
export.to.file=TRUE,
file.name=NULL,
show.first.rows=200,
show.first.cols=200,
split.tables=Inf,
style="rmarkdown",
skip.if.empty=FALSE,
echo=NULL,
self.contained.data=NULL,
...) {
"Add table object.
parameter: caption Caption
parameter: show.row.names Show row names
parameter: wrap.caption Escape Markdown tags in caption
parameter: wrap.vals Escape Markdown tags in value cells
parameter: export.to.file Also save table in a CSV file
parameter: file.name If exporting to file, use this file name (name will be auto-generated if not set -
this is a recommended way)
parameter: show.first.rows Only show in Markdown that many rows. All rows will be still exported to file.
This option is set to a reasonable default value to prevent the Web browser from slowing
down when viewing report files with many large tables.
parameter: show.first.cols Only show in Markdown that many columns. All columns will be still exported
to file.
parameter: split.tables Overwrites default pander argument for splitting wide tables across rows
parameter: style Overwrites default pander argument for tables Markdown style
parameter: self.contained.data Embed full size data file as data URL in the resulting HTML
parameter: ... Passed to pander::pandoc.table.return
"
.self.contained.data = first_defined_arg(self.contained.data,.self$self.contained.data,F)
if (wrap.caption && !is.null(caption)) {
caption = anrep.escape.special(caption)
}
caption.el = .self$format.caption(caption,type="table",elements = TRUE)
caption = caption.el$caption
if(is.null(x) || nrow(x)==0) {
if(!skip.if.empty) {
if(!is.null(caption)) {
.self$add.p(caption)
}
return(.self$add.p("Empty dataset"))
}
else {
return(.self)
}
}
if(show.first.rows > 0) {
if(show.first.rows >= nrow(x)) {
show.first.rows = 0
}
}
if(show.first.rows > 0) {
caption = paste(caption,sprintf("Showing only %s first rows.",show.first.rows))
}
if(show.first.cols > 0) {
if(show.first.cols >= ncol(x)) {
show.first.cols = 0
}
}
if(show.first.cols > 0) {
caption = paste(caption,sprintf("Showing only %s first columns.",show.first.cols))
}
if(export.to.file) {
file.name = .self$write.table.file(x,
name.base=.self$make.file.name.base.from.caption.elements(caption.el),
descr=NULL,
row.names=show.row.names,
row.names.header=TRUE,
file.name=file.name)
caption = paste(caption,
"Full dataset is also saved in a delimited text file (click to download and open e.g. in Excel)",
anrep.file.link.verbatim.return(file.name,selfcontained=.self.contained.data,mime="txt/csv")
)
}
if(show.first.rows > 0) {
if(inherits(x,"data.table")) x = x[1:show.first.rows]
else x = x[1:show.first.rows,,drop=FALSE]
}
if(show.first.cols > 0) {
if(inherits(x,"data.table")) x = x[,1:show.first.cols,with=FALSE]
else x = x[,1:show.first.cols,drop=FALSE]
}
## With data.table, I am getting this message:
## `data.table inherits from data.frame (from v1.5) but this data.table does not`
## when calling `rn = rownames()` below. Converting to data.frame here to get rid of it.
if(inherits(x,"data.table")) x = as.data.frame(x)
if(!show.row.names) {
rownames(x) = NULL
}
if(wrap.vals) {
rn = rownames(x)
if(show.row.names && !are.automatic.rownames(x)) {
rn = anrep.escape.special(rn)
}
if(is.matrix(x)) {
x = as.data.frame(x)
}
x = sapply(x,anrep.escape.special,USE.NAMES=FALSE,simplify=TRUE)
if(!is.matrix(x)) {
x = t(as.matrix(x))
}
rownames(x) = rn
colnames(x) = anrep.escape.special(colnames(x))
}
.self$add.p(caption)
tbl_p = pander::pandoc.table.return(x,split.tables=split.tables,style=style,caption=NULL,...)
if(first_defined_arg(echo,.self$echo)) {
print(tbl_p)
}
return(.self$add.p(tbl_p))
})
anrep$methods(add.vector = function(x,name=NULL,
show.row.names=TRUE,
caption=NULL,
...) {
"Add vector object.
Vector is added as a table. Parameters to add.table method are accepted here.
"
if(is.null(x) || length(x)==0) {
if(!is.null(caption)) {
.self$add.p(.self$format.caption(caption))
}
return(.self$add.p("Empty dataset"))
}
y = data.frame(x=x)
if(!is.null(name)) {
names(y) = c(name)
}
if(is.null(names(x))) {
show.row.names = FALSE
}
if(show.row.names) {
row.names(y) = names(x)
}
return(.self$add.table(y,caption=caption,show.row.names=show.row.names,...))
})
anrep$methods(add.descr = function(x,caption=NULL,...) {
"Add text.
Arguments to add.p method are accepted here"
if(!is.null(caption)) {
.self$add.p(.self$format.caption(caption))
}
.self$add.p(x,...)
})
anrep$methods(add.package.citation = function(x,caption=NULL,...) {
"Add citation for a package name"
if(!is.null(caption)) {
.self$add.p(.self$format.caption(caption))
}
.self$add.p(capture.output(print(citation(x),style="text")),...)
})
anrep$methods(add.printed = function(x,caption=NULL,echo=NULL,...) {
"Add a chunk of text verbatim preserving it from Markdown formatting.
This is a lazy escape hatch for situations where R function such as Anova
summary returns text carefully formatted with spaces and you want to
include this formatted output instead of generating your own formatting
with Markdown.
parameter: caption If provided, numbered caption will be added to the text chunk.
"
if(!is.null(caption)) {
.self$add.p(.self$format.caption(caption))
}
return(.self$add.p(anrep.as.printed.return(x,...),echo=echo))
})
anrep$methods(write.table.file = function(data,
name.base,
make.unique=TRUE,
descr=NULL,
row.names=FALSE,
row.names.header=TRUE,
file.name=NULL,
...) {
"Save table in a CSV text file.
Most arguments are taken from the signatures of make.file.name method and
base::write.csv.
parameter: row.names.header If writing row names, add a column name 'rownames' to the header. This
works around unintended misallignment of rows when loading the file into some
spreadsheet programs.
"
## if we write row.names, Excel shifts header row to the left when loading
if(row.names && row.names.header) {
data = cbind(rownames=rownames(data),data)
row.names=FALSE
}
if(is.null(file.name)) {
fn = .self$make.file.name(name.base,make.unique=make.unique,name.ext=".csv",name.base.first = TRUE)
}
else {
fn = file.name
}
write.csv(data,
fn,
row.names = row.names,
...)
if (!is.null(descr)) {
.self$add.descr(paste("Wrote",descr,"to file",
pander::pandoc.link.return(fn,fn)))
}
return(fn)
})
anrep$methods(save = function(out.file=NULL,
out.formats=NULL,
self.contained.html=NULL,
pandoc.binary=NULL,
css.file=NULL,
export=TRUE,
concatenate=FALSE,
pandoc.meta=TRUE,
knitr.auto.asis=NULL,
sort.by.sections=FALSE) {
"Save the report to Markdown and convert it to the final output formats.
Call this at the end of your analysis.
Normally, you should leave all arguments at their default values, and simply call
report$save()
parameter: out.file A string to use as a root name for the output report files,
which will be generated as <out.file>_<subreport_section>.<out.format> (for example,
report_1.1.Rmd or report_1.1.html). This value should not normally contain a directory component
because all links will be always generated relative to the current working directory.
Alternatively, out.file can be an open output connection object (inherit from base::connection
class). In that case, concatenate argument will be reset to TRUE, export - to FALSE, and
the single concatenated Markdown stream will be sent to the out.file connnection.
parameter: out.formats Convert Markdown to these formats (using Pandoc binary executable). The only
format that supports all features of our generated Markdown is currently `html`, which will
be the default value. In particular, subreports and htmlwidgets will only work with HTML
output. Another special case is `knitr`. It only can be a single output format, and
in that case the Markdown output will be concatenated and send to standard output stream
without Pandoc meta data header. This mode overrides a number of other parameters.
parameter: self.contained.html Embed images (default resolution versions) and stylesheets into each HTML file.
Note that generated data files and widget files will still be referenced by links. In any
case, the report directory as a whole will be portable in a sense that it can be archived into
a different location or computer and viewed where. Default value is initialized by the report
object constructor.
parameter: pandoc.binary Path to Pandoc binary executable. Default value is initialized from pander
package global options or located in the PATH variable, in that order of preference.
parameter: css.file Path to CSS (Cascading Style Sheets) file to include in all HTML report files.
Use this arguments to modify the style of the reports.
This file will be copied into the report directory and linked from where.
The default file included with the package can be accessed with
system.file('extdata', 'anrep.css', package = 'anrepr')
parameter: export Export Markdown report files into the final output formats (ignored if
Pandoc binary is not found)
parameter: concatenate Flatten all subreports into a single output file
parameter: pandoc.meta Add Pandoc metadata header
parameter: knitr.auto.asis Same meaning as in the constructor
return: data.frame with files names for all (sub)reports for all output formats, and a field named
is_root that is set to TRUE for the row that is the root level report (the report
from which the viewing has to start)
"
out.streaming = FALSE
out.streaming.format = "md"
.out.formats = first_defined_arg(out.formats,.self$out.formats,"html")
out.file.arg = out.file
is.knitr.output = .self$priv.is.knitr.output()
if(is.knitr.output) {
pandoc.meta = FALSE
concatenate = TRUE
# we now only stream Markdown, so no point in the export stage
export = FALSE
}
if(inherits(out.file.arg,"connection")) out.streaming = TRUE
if(out.streaming) {
concatenate = TRUE
# we now only stream Markdown, so no point in the export stage
export = FALSE
}
## If streaming, still use a real file as a temporary output
.out.file = first_defined_arg(if(out.streaming) NULL else out.file.arg,.self$out.file,"report")
.self.contained.html = first_defined_arg(self.contained.html,.self$self.contained.html,T)
# reset and restore on exit some pander options such as knitr.auto.asis
state = .self$priv.enter.add()
on.exit(.self$priv.exit.add(state))
fp = .out.file
fp.all = list()
f_sections = .self$sections
f_entries = .self$entries
stopifnot(length(f_sections)==length(f_entries))
if(sort.by.sections) {
##sort by section lexicographically, using a stable sort
sect_ord = sort.list(
unlist(lapply(.self$sections,format.section.path)),
method="shell")
f_entries = f_entries[sect_ord]
}
write.el = function(el,fp) {
#this is where knitr.asis should be already switched off, else
#we get attributes printed along with the strings.
#Note that pander_return is cat(pander()), and pander() is a generic default
#that acts as a wrapper - resets asis, calls the class methods, then
#wraps the results in asis, restores asis and returns.
#All other pander.class methods work in the opposite direction - they
#do cat(pander.class.return())
el.str = pander::pander_return(el$result)
cat(paste(el.str, collapse = '\n'),
file = fp, append = TRUE)
}
for(i.el in seq_along(f_entries)) {
section = f_sections[[i.el]]
el = f_entries[[i.el]]
section.par = cut.to.bottom.sub.section.path(section)
#print(paste("Full section:",paste(section,collapse=" ")))
#print(paste("Par section:",paste(section.par,collapse=" ")))
if(!concatenate && length(section.par) > 0) {
sub.path = section.par
}
else {
sub.path = section[1:1]
}
fp.sub = .self$make.file.name(name.base=fp,
make.unique=FALSE,
dir=".",
sec.path=sub.path,
name.base.first=TRUE)
fp.sub.md = paste(fp.sub,".Rmd",sep="") #".md"
#print(paste("fp.sub=",fp.sub))
# if this subreport output was not touched before, write Pandoc metadata header
if(pandoc.meta && is.null(fp.all[[fp.sub.md]])) {
cat("%",if(is.null(.self$title)) ""
else .self$title,"\n",
"%",if(is.null(.self$author)) ""
else paste(.self$author,collapse=";"),"\n",
"%",.self$date,"\n",
file = fp.sub.md)
}
if(!concatenate && i.el>1) {
# if subreport level of this record is below than subreport level of
# the previous record, append the header of the previous record into the
# current subreport,
# and append the link to the previouse subreport
# Warning: the useful outcome of this depends on records sorted in section
# order.
sub.level.prev = get.sub.level.section.path(f_sections[[i.el-1]])
sub.level = get.sub.level.section.path(section)
if(sub.level > sub.level.prev) {
cat(anrep.link.verbatim.return(paste(fp.sub,".html",sep=""),"Subreport"), #".html"
file = fp.sub.md.prev, append = TRUE)
if(section[[length(section)]]$has.header) {
write.el(f_entries[[i.el-1]],fp.sub.md)
}
}
}
write.el(el,fp.sub.md)
fp.all[[fp.sub.md]] = fp.sub
fp.sub.md.prev = fp.sub.md
}
if(is.null(pandoc.binary)) {
pandoc.binary = pander::panderOptions("pandoc.binary")
if(is.null(pandoc.binary) || !file.exists(pandoc.binary)) {
pandoc.binary = Sys.which("pandoc")
}
}
if(export) {
if(is.null(pandoc.binary) || !file.exists(pandoc.binary)) {
message("Exetutable file 'pandoc' must be found in the system PATH or in the location provided by you
for the conversion from Markdown to other formats to work. Because pandoc binary is not found,
Markdown will not be converted.")
export = FALSE
}
}
if(!export) {
message("Export with Pandoc will not be performed, but the Pandoc commands will be printed
in case you will want to do the conversion manually")
}
if(is.null(css.file)) {
css_base = "anrep.css" #"github-rmarkdown.css" #"github-pandoc.css"
css.file = system.file("extdata", css_base, package = "anrepr",mustWork = TRUE)
}
if(!file.exists(css.file)) {
stop(sprintf("Provided CSS file not found: %s",css.file))
}
css_base = html.path(.self$resources.dir,"anrep.css")
file.copy(css.file,css_base)
out.files = list(md=names(fp.all),is_root = c(T,rep(F,length(fp.all)-1)))
for(out.format in .out.formats[.out.formats!="knitr"]) {
for(fp.sub.md in names(fp.all)) {
fp.sub = fp.all[[fp.sub.md]]
fp.sub.out = sprintf("%s.%s",fp.sub,out.format)
out.files[[out.format]] = c(out.files[[out.format]],fp.sub.out)
## TODO: Consider adding pandoc "-S" to support
## Pandoc's subscript and suprscript extensions
self_cont = ""
if (.self.contained.html) self_cont = "--self-contained"
cmd = sprintf("pandoc --standalone %s --toc -t %s -c %s %s -o %s",
self_cont,out.format,css_base,fp.sub.md,fp.sub.out)
message(cmd)
if(export) system(cmd)
}
}
out.files = as.data.frame(out.files)
if(out.streaming) {
out.buffers = out.files[[out.streaming.format]]
for(out.buffer in out.buffers) {
writeLines(readLines(out.buffer,warn = FALSE),out.file.arg)
}
unlink(out.buffers)
out.files[,out.streaming.format] = NA
}
if(is.knitr.output && is.null(out.file.arg)) {
ret = ""
out.buffers = out.files[[out.streaming.format]]
## this actually should be only a single buffer since we use 'concatenate' for knitr
for(out.buffer in out.buffers) {
ret = c(ret,paste(readLines(out.buffer,warn = FALSE),collapse = '\n'))
}
unlink(out.buffers)
out.files[,out.streaming.format] = NA
if(first_defined_arg(knitr.auto.asis,.self$knitr.auto.asis)) {
ret = knitr::asis_output(ret)
}
for(kn_at in .self$knitr.meta.attr) {
knitr::knit_meta_add(kn_at)
}
attr(ret,"knit_meta") <- knitr::knit_meta()
}
else {
ret = invisible(out.files)
}
ret
})
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.