diff --git a/.gitignore b/.gitignore
index e43b0f9..ee14eeb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,79 @@
+# ---- Project files ----
+shiny_bookmarks/
+www/errors/*
+
+# ---- Default .gitignore From grkmisc ----
+.Rproj.user
+.Rhistory
+.RData
.DS_Store
+
+# Directories that start with _
+_*/
+
+## https://github.com/github/gitignore/blob/master/R.gitignore
+# History files
+.Rhistory
+.Rapp.history
+
+# Session Data files
+.RData
+
+# Example code in package build process
+*-Ex.R
+
+# Output files from R CMD build
+/*.tar.gz
+
+# Output files from R CMD check
+/*.Rcheck/
+
+# RStudio files
+.Rproj.user/
+
+# produced vignettes
+vignettes/*.html
+vignettes/*.pdf
+
+# OAuth2 token, see https://github.com/hadley/httr/releases/tag/v0.3
+.httr-oauth
+
+# knitr and R markdown default cache directories
+/*_cache/
+/cache/
+
+# Temporary files created by R markdown
+*.utf8.md
+*.knit.md
+
+# Shiny token, see https://shiny.rstudio.com/articles/shinyapps.html
+rsconnect/
+
+## https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..f10a2ca
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,27 @@
+sudo: required
+services:
+ - docker
+
+env:
+ global:
+ - DOCKER_USER="grrrck"
+ - DOCKER_ORG="gerkelab"
+ - secure: "cHdDyac1V5loCeGFS9k+hTejr2cRUWHm5vDB+7vXajkw4ile4mcn+E5vdoy9ExXlaHYuqjrOqURyBAI/HY1U0O06Brif9W8cKlUn7SGwaM6coBgqAxjeOiFDVD9Xg8IOI7wCHjJr9heMRLZyd65cyh9l30S+VKMqx4oFmoYdkvj2v7veDsN9j6kJ7SYGSZOmgv9G/FZmQvyWyLLhpgzMw98WzS2/QsbhG8ZSUmlRYfXo+B1vgw1lDVn8iRFAjG3oFiY4qXTVeaBOi/qAY20Kd44qQpcb2CL1wV/zMjRFGLXtlaBoMMA/4s5uRrFfJHsqUxLIqmhuBlLtqOtyZd2CqP3EGSjkmxfNh/dMDA0zgd3o/IVLuz6owpbHR/9ypUKvuD91vtTp0BUM+6Uma9j+ODC2Zn+IBi6QogjBSBkzz8wEK3TdM2RdjtJ62lBCL8YWxmGCQfIGR+emo1BUFnCsgMYsscC5LoMzFaihBTZAqaMQ3grCi743F2ozHFB3J2DRId1QZD+nje8An3ALsa152BX+ItblyOD7MxfSXa6OtthlholPTiKhYyWBncQqFMBaYsglVVF8MONEYJUzbws2D7+0IdJ5sXZz8XM/sXUwxNkBIpjfQoaqOkYFILCkwab59D7AvZPyYb6hI+XRhvqvA7Z221d+6UloRCFJha/oaR8="
+ - COMMIT=${TRAVIS_COMMIT::8}
+ - REPO="shinydag"
+
+script:
+ - docker build -f Dockerfile -t $DOCKER_ORG/$REPO:$COMMIT .
+
+after_success:
+ - docker login -u $DOCKER_USER -p $DOCKER_PASS
+ - if [[ $TRAVIS_PULL_REQUEST == "false" ]] && [[ $TRAVIS_BRANCH == "master" ]]; then
+ docker tag $DOCKER_ORG/$REPO:$COMMIT $DOCKER_ORG/$REPO:latest;
+ docker tag $DOCKER_ORG/$REPO:$COMMIT $DOCKER_ORG/$REPO:$TRAVIS_BUILD_NUMBER;
+ docker push $DOCKER_ORG/$REPO;
+ fi
+ - if [[ $TRAVIS_PULL_REQUEST == "false" ]] && [[ $TRAVIS_BRANCH == "dev" ]]; then
+ docker tag $DOCKER_ORG/$REPO:$COMMIT $DOCKER_ORG/$REPO:dev;
+ docker tag $DOCKER_ORG/$REPO:$COMMIT $DOCKER_ORG/$REPO:$TRAVIS_BUILD_NUMBER;
+ docker push $DOCKER_ORG/$REPO;
+ fi
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..e12697e
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,64 @@
+FROM rocker/shiny-verse:3.5.3
+
+LABEL maintainer="Travis Gerke (Travis.Gerke@moffitt.org)"
+
+# Install system dependencies for required packages
+RUN apt-get update -qq && apt-get -y --no-install-recommends install \
+ libssl-dev \
+ libxml2-dev \
+ libmagick++-dev \
+ libv8-3.14-dev \
+ libglu1-mesa-dev \
+ freeglut3-dev \
+ mesa-common-dev \
+ libudunits2-dev \
+ libpoppler-cpp-dev \
+ libwebp-dev \
+ && apt-get clean \
+ && rm -rf /var/lib/apt/lists/ \
+ && rm -rf /tmp/downloaded_packages/ /tmp/*.rds
+
+RUN install2.r --error \
+ shinyAce \
+ shinydashboard \
+ shinyWidgets \
+ DiagrammeR \
+ ggdag \
+ igraph \
+ pdftools \
+ shinyBS \
+ && rm -rf /tmp/downloaded_packages/ /tmp/*.rds
+
+RUN Rscript -e "devtools::install_github('metrumresearchgroup/texPreview', ref = 'e954322')" \
+ && rm -rf /tmp/downloaded_packages/ /tmp/*.rds
+
+# Install TinyTeX
+RUN install2.r --error tinytex \
+ && wget -qO- \
+ "https://github.com/yihui/tinytex/raw/master/tools/install-unx.sh" | \
+ sh -s - --admin --no-path \
+ && mv ~/.TinyTeX /opt/TinyTeX \
+ && /opt/TinyTeX/bin/*/tlmgr path add \
+ && tlmgr install metafont mfware inconsolata tex ae parskip listings \
+ && tlmgr install standalone varwidth xcolor colortbl multirow psnfss setspace pgf \
+ && tlmgr path add \
+ && Rscript -e "tinytex::r_texmf()" \
+ && chown -R root:staff /opt/TinyTeX \
+ && chmod -R a+w /opt/TinyTeX \
+ && chmod -R a+wx /opt/TinyTeX/bin \
+ && echo "PATH=${PATH}" >> /usr/local/lib/R/etc/Renviron \
+ && rm -rf /tmp/downloaded_packages/ /tmp/*.rds
+
+RUN install2.r --error shinyjs \
+ && rm -rf /tmp/downloaded_packages/ /tmp/*.rds
+
+RUN install2.r --error plotly shinycssloaders \
+ && rm -rf /tmp/downloaded_packages/ /tmp/*.rds
+
+RUN installGithub.r gadenbuie/shinyThings@4e8becb2972aa2f7f1960da6e5fe6ad39aeceda0 \
+ && rm -rf /tmp/downloaded_packages/ /tmp/*.rds
+
+ARG SHINY_APP_IDLE_TIMEOUT=600
+RUN sed -i "s/directory_index on;/app_idle_timeout ${SHINY_APP_IDLE_TIMEOUT};/g" /etc/shiny-server/shiny-server.conf
+COPY . /srv/shiny-server/shinyDAG
+RUN chown -R shiny:shiny /srv/shiny-server/
diff --git a/Figures/AddNodeEdge.gif b/Figures/AddNodeEdge.gif
index 9610a7a..b736a71 100644
Binary files a/Figures/AddNodeEdge.gif and b/Figures/AddNodeEdge.gif differ
diff --git a/Figures/editEdge.gif b/Figures/editEdge.gif
index 923eca0..7e773af 100644
Binary files a/Figures/editEdge.gif and b/Figures/editEdge.gif differ
diff --git a/Figures/paths.png b/Figures/paths.png
index 525df35..7f2e571 100644
Binary files a/Figures/paths.png and b/Figures/paths.png differ
diff --git a/Figures/paths2.png b/Figures/paths2.png
index 6c5de9b..bcdb9c3 100644
Binary files a/Figures/paths2.png and b/Figures/paths2.png differ
diff --git a/R/aes_ui.R b/R/aes_ui.R
new file mode 100644
index 0000000..97c67fa
--- /dev/null
+++ b/R/aes_ui.R
@@ -0,0 +1,183 @@
+
+# * ui_edge_controls() builds an individual UI control element. These elements
+# are re-rendered whenever the tab is opened, so this function finds the
+# current value of the input and uses that instead of the value declared
+# in the definition in ui_edge_controls_row(). This function also isolates
+# the edge control UI from other changes in nodes, etc, because they happen
+# on different screens.
+ui_controls <- function(hash, inputFn, prefix_input, label, ..., input = NULL) {
+ stopifnot(!is.null(input))
+ current_value_arg_name <- intersect(names(list(...)), c("selected", "value"))
+ if (!length(current_value_arg_name)) {
+ stop("Must specifiy `selected` or `value` when specifying edge UI controls")
+ }
+ input_name <- paste(prefix_input, hash, sep = "__")
+ input_label <- label
+
+ if (input_name %in% names(isolate(input))) {
+ # Make sure current value doesn't change
+ dots <- list(...)
+ dots[current_value_arg_name] <- paste(isolate(input[[input_name]]))
+ dots$inputId <- input_name
+ dots$label <- HTML(input_label)
+ do.call(inputFn, dots)
+ } else {
+ # Create new input
+ inputFn(input_name, HTML(input_label), ...)
+ }
+}
+
+get_hashed_input_with_prefix <- function(input, prefix, hash_sep = "__") {
+ prefix <- glue::glue("^({prefix}){hash_sep}")
+
+ tibble(
+ inputId = grep(prefix, names(input), value = TRUE)
+ ) %>%
+ filter(!grepl("-selectized$", inputId)) %>%
+ # get current value of input
+ mutate(value = lapply(inputId, function(x) input[[x]])) %>%
+ tidyr::separate(inputId, into = c("var", "hash"), sep = hash_sep) %>%
+ tidyr::spread(var, value) %>%
+ mutate_if(is.list, ~ purrr::map(.x, ~ if (is.null(.x)) NA else .x)) %>%
+ tidyr::unnest() %>%
+ split(.$hash)
+}
+
+# The input for angles (here for easy refactoring or future changes)
+selectDegree <- function(inputId, label = "Degree", min = -180, max = 180, by = 15, value = 0, ...) {
+ sliderInput(inputId, label = label, min = min, max = max, value = value, step = by)
+}
+
+
+# Edge Aesthetic UI -------------------------------------------------------
+
+# These helper functions build up the Edge UI elements.
+#
+# * ui_edge_controls_row() creates the entire row of UI elements for a given
+# edge. This function is where the UI inputs are initially defined.
+
+ui_edge_controls_row <- function(hash, from_name, to_name, ..., input = NULL) {
+ stopifnot(!is.null(input))
+
+ extra <- list(...)
+
+ col_4 <- function(x) {
+ tags$div(class = "col-sm-6 col-md-3", style = "min-height: 80px", x)
+ }
+ title_row <- function(x) tags$div(class = "col-xs-12", tags$h3(x))
+ edge_label <- paste0(from_name, " → ", to_name)
+
+ tagList(
+ fluidRow(
+ title_row(HTML(edge_label))
+ ),
+ fluidRow(
+ # Edge Curve Angle
+ col_4(ui_controls(
+ hash,
+ inputFn = selectDegree,
+ prefix_input = "angle",
+ label = "Angle",
+ value = extra[["angle"]] %||% 0,
+ width = "95%",
+ input = input
+ )),
+ # Edge Color
+ col_4(ui_controls(
+ hash,
+ inputFn = xcolorPicker,
+ prefix_input = "color",
+ label = "Edge",
+ selected = extra[["color"]] %||% "Black",
+ width = "95%",
+ input = input
+ )),
+ # Curve Angle
+ col_4(ui_controls(
+ hash,
+ inputFn = selectInput,
+ prefix_input = "lty",
+ label = "Line Type",
+ choices = c("solid", "dashed"),
+ selected = extra[["lty"]] %||% "solid",
+ width = "95%",
+ input = input
+ )),
+ # Curve Angle
+ col_4(ui_controls(
+ hash,
+ inputFn = selectInput,
+ prefix_input = "lineT",
+ label = "Line Thickness",
+ choices = c("ultra thin", "very thin", "thin", "semithick", "thick", "very thick", "ultra thick"),
+ selected = extra[["lineT"]] %||% "thin",
+ width = "95%",
+ input = input
+ ))
+ )
+ )
+}
+
+
+# Node Aesthetic UI -------------------------------------------------------
+
+ui_node_controls_row <- function(hash, name, adjusted, name_latex, ..., input = NULL) {
+ stopifnot(!is.null(input))
+
+ extra <- list(...)
+
+ col_4 <- function(x) {
+ tags$div(class = "col-sm-6 col-md-3", style = "min-height: 80px", x)
+ }
+ title_row <- function(x) tags$div(class = "col-xs-12", tags$h3(x))
+
+ tagList(
+ fluidRow(
+ title_row(HTML(name))
+ ),
+ fluidRow(
+ # LaTeX version of node label
+ col_4(ui_controls(
+ hash,
+ inputFn = textInput,
+ prefix_input = "name_latex",
+ label = "LaTeX Label",
+ value = name_latex,
+ width = "95%",
+ input = input
+ )),
+ # Text Color
+ col_4(ui_controls(
+ hash,
+ inputFn = xcolorPicker,
+ prefix_input = "color_text",
+ label = "Text",
+ selected = extra[["color_text"]] %||% "Black",
+ width = "95%",
+ input = input
+ )),
+ # Fill Color
+ col_4(ui_controls(
+ hash,
+ inputFn = xcolorPicker,
+ prefix_input = "color_fill",
+ label = "Fill",
+ selected = extra[["color_fill"]] %||% "White",
+ width = "95%",
+ input = input
+ )),
+ # Box Color (if shown)
+ if (adjusted) {
+ col_4(ui_controls(
+ hash,
+ inputFn = xcolorPicker,
+ prefix_input = "color_draw",
+ label = "Border",
+ selected = extra[["color_draw"]] %||% "Black",
+ width = "95%",
+ input = input
+ ))
+ }
+ )
+ )
+}
diff --git a/R/columns.R b/R/columns.R
new file mode 100644
index 0000000..a98cfeb
--- /dev/null
+++ b/R/columns.R
@@ -0,0 +1,28 @@
+class_3_col <- "col-md-4 col-md-offset-0 col-sm-8 col-sm-offset-2 col-xs-12"
+
+
+# Component Builders ------------------------------------------------------
+
+two_column_flips_on_mobile <- function(left, right, override_width_classes = TRUE) {
+
+ left_col_class <- "col-sm-12 col-md-pull-6 col-md-6 col-lg-5 col-lg-pull-7"
+ right_col_class <- "col-sm-12 col-md-push-6 col-md-6 col-lg-7 col-lg-push-5"
+
+ if (!override_width_classes) {
+ right <- tags$div(class = right_col_class, right)
+ left <- tags$div(class = left_col_class, left)
+ } else {
+ strip_col_class <- function(x) gsub("col-(xs|sm|md|lg)-\\d{1,2}\\s*", "", x)
+ left$attrib$class <- strip_col_class(left$attrib$class)
+ right$attrib$class <- strip_col_class(right$attrib$class)
+
+ left$attrib$class <- paste(left$attrib$class, left_col_class)
+ right$attrib$class <- paste(right$attrib$class, right_col_class)
+ }
+
+ fluidRow(right, left)
+}
+
+col_4 <- function(x) {
+ tags$div(class = "col-sm-6 col-md-3", style = "min-height: 80px", x)
+}
\ No newline at end of file
diff --git a/R/edge.R b/R/edge.R
new file mode 100644
index 0000000..e5ab7c1
--- /dev/null
+++ b/R/edge.R
@@ -0,0 +1,114 @@
+# rve$edges is a named list, e.g. for hash(A) -> hash(B):
+# rve$edges[edge_key(hash(A), hash(B))] = list(from = hash(A), to = hash(B))
+
+# ---- Edge Helper Functions ----
+edge_key <- function(x, y) digest::digest(c(x, y))
+
+edge_frame <- function(edges, nodes, ...) {
+ dots <- rlang::enexprs(...)
+
+ dag_edges <- edges_in_dag(edges, nodes)
+
+ if (!length(dag_edges)) return(tibble())
+
+ ensure_exists <- function(x, ...) {
+ cols <- list(...)
+ stopifnot(!is.null(names(cols)), all(nzchar(names(cols))))
+ for (col in names(cols)) {
+ x[[col]] <- x[[col]] %||% cols[[col]]
+ }
+ x %>% tidyr::replace_na(cols)
+ }
+
+ edges %>%
+ bind_rows(.id = "hash") %>%
+ filter(hash %in% edges_in_dag(edges, nodes)) %>%
+ tidyr::nest(-from) %>%
+ left_join(
+ nodes %>% node_frame() %>% select(from = hash, from_name = name),
+ by = "from"
+ ) %>%
+ tidyr::unnest() %>%
+ tidyr::nest(-to) %>%
+ left_join(
+ nodes %>% node_frame() %>% select(to = hash, to_name = name),
+ by = "to"
+ ) %>%
+ tidyr::unnest() %>%
+ select(hash, names(edges[[1]]), everything()) %>%
+ ensure_exists(angle = 0L, color = "black", lty = "solid", lineT = "thin") %>%
+ mutate(!!!dots)
+}
+
+edges_in_dag <- function(edges, nodes) {
+ if (!length(nodes) || !length(edges)) return(character())
+ all_edges <- bind_rows(edges) %>%
+ mutate(hash = names(edges)) %>%
+ tidyr::gather(position, node_hash, from:to)
+
+ edges_not_in_graph <- all_edges %>%
+ filter(!node_hash %in% nodes_in_dag(nodes))
+
+ setdiff(all_edges$hash, edges_not_in_graph$hash)
+}
+
+edge_edges <- function(edges, nodes, ...) {
+ do.call(edge, as.list(edge_frame(edges, nodes, ...)))
+}
+
+edge_exists <- function(edges, from_hash = NULL, to_hash = NULL) {
+ if (purrr::some(list(from_hash, to_hash), is.null)) return(FALSE)
+
+ edges %>%
+ purrr::keep(~ .$from %in% from_hash) %>%
+ purrr::keep(~ .$to %in% to_hash) %>%
+ length() %>%
+ `>`(0)
+}
+
+edge_points <- function(edges, nodes, push_by = 0) {
+ dag_edges <- edges_in_dag(edges, nodes)
+
+ if (!length(dag_edges)) return(tibble())
+
+ edge_frame(edges, nodes) %>%
+ tidyr::nest(-from) %>%
+ left_join(
+ nodes %>% node_frame() %>% select(from = hash, from.x = x, from.y = y),
+ by = "from"
+ ) %>%
+ tidyr::unnest() %>%
+ tidyr::nest(-to) %>%
+ left_join(
+ nodes %>% node_frame() %>% select(to = hash, to.x = x, to.y = y),
+ by = "to"
+ ) %>%
+ tidyr::unnest() %>%
+ select(hash, names(edges[[1]]), everything()) %>%
+ mutate(
+ d_x = to.x - from.x,
+ d_y = to.y - from.y,
+ from.x = from.x + push_by * d_x,
+ from.y = from.y + push_by * d_y,
+ to.x = to.x - push_by * d_x,
+ to.y = to.y - push_by * d_y,
+ color = if_else(color == "", "Black", color)
+ ) %>%
+ select(-d_x, -d_y)
+}
+
+edge_toggle <- function(edges, from_hash, to_hash) {
+ existing <-
+ edges %>%
+ purrr::keep(~ .$from == from_hash) %>%
+ purrr::keep(~ .$to == to_hash)
+
+ if (length(existing)) {
+ for (edge_key in names(existing)) {
+ edges[[edge_key]] <- NULL
+ }
+ } else {
+ edges[[edge_key(from_hash, to_hash)]] <- list(from = from_hash, to = to_hash)
+ }
+ edges
+}
diff --git a/R/inputs.R b/R/inputs.R
new file mode 100644
index 0000000..e6a8c8b
--- /dev/null
+++ b/R/inputs.R
@@ -0,0 +1,41 @@
+# A fancy selectizeInput for angles
+selectDegree <- function(
+ inputId,
+ label = "Degree",
+ min = -180 + by,
+ max = 180,
+ by = 45,
+ value = 0,
+ ...
+) {
+ if (sign(min + (max - min)) != sign(by)) {
+ by <- -by
+ }
+ choices <- seq(min, max, by)
+
+ selectizeInput(inputId, label = label, choices, selected = value, multiple = FALSE, ..., )
+}
+
+
+# A button group that toggles state and optionally allows one button to be active at a time
+buttonGroup <- function(inputId, options, btn_class = "btn-default", multiple = FALSE, aria_label = NULL) {
+ btn_class <- paste("btn", paste(btn_class, collapse = " "))
+ button_list <- purrr::imap(options, button_in_group, class = btn_class)
+ selected <- shiny::restoreInput(inputId, default = "")
+ tagList(
+ singleton(tags$head(tags$script(src = "shinythingsButtonGroup.js"))),
+ tags$div(
+ class = "shinythings-btn-group btn-group",
+ id = inputId,
+ `data-input-id` = inputId,
+ `data-active` = selected,
+ `data-multiple` = as.integer(multiple),
+ role = "group",
+ button_list
+ )
+ )
+}
+
+button_in_group <- function(input_id, text, class = "btn btn-default") {
+ tags$button(id = input_id, class = class, text)
+}
\ No newline at end of file
diff --git a/R/module/clickpad.R b/R/module/clickpad.R
new file mode 100644
index 0000000..23db5ad
--- /dev/null
+++ b/R/module/clickpad.R
@@ -0,0 +1,310 @@
+clickpad_UI <- function(id, ...) {
+ library(plotly)
+ ns <- NS(id)
+ tagList(
+ plotlyOutput(ns("plot"), ...)
+ )
+}
+
+clickpad_debug <- function(id, relayout = TRUE, doubleclick = TRUE, selected = FALSE, clickannotation = TRUE) {
+ ns <- NS(id)
+ col_width <- 12 / sum(relayout, doubleclick, selected, clickannotation)
+ tagList(
+ fluidRow(
+ style = "overflow-y: scroll; max-height: 200px;",
+ if (relayout) column(
+ col_width,
+ tags$p(tags$code("plotly_relayout")),
+ verbatimTextOutput(ns("v_relayout"))
+ ),
+ if (doubleclick) column(
+ col_width,
+ tags$p(tags$code("plotly_doubleclick")),
+ verbatimTextOutput(ns("v_doubleclick"))
+ ),
+ if (selected) column(
+ col_width,
+ tags$p(tags$code("plotly_selected")),
+ verbatimTextOutput(ns("v_selected"))
+ ),
+ if (clickannotation) column(
+ col_width,
+ tags$p(tags$code("plotly_clickannotation")),
+ verbatimTextOutput(ns("v_clickannotation"))
+ )
+ )
+ )
+}
+
+clickpad <- function(
+ input, output, session,
+ nodes, edges,
+ plotly_source = "clickpad"
+) {
+ library(plotly)
+ ns <- session$ns
+
+ node_primary <- reactive({ node_parent(nodes()) })
+ node_secondary <- reactive({ node_child(nodes()) })
+ node_is_adjusted <- reactive({ node_adjusted(nodes()) })
+ node_exposure <- reactive({ names(node_with_attribute(nodes(), "exposure")) })
+ node_outcome <- reactive({ names(node_with_attribute(nodes(), "outcome")) })
+
+ output$v_relayout <- renderPrint({
+ str(event_data("plotly_relayout", priority = "event", source = plotly_source))
+ })
+
+ output$v_doubleclick <- renderPrint({
+ str(event_data("plotly_doubleclick", priority = "event", source = plotly_source))
+ })
+
+ output$v_selected <- renderPrint({
+ str(event_data("plotly_clickannotation", priority = "event", source = plotly_source))
+ })
+
+ output$v_clickannotation <- renderPrint({
+ str(event_data("plotly_clickannotation", priority = "event", source = plotly_source))
+ })
+
+ arrow_path <- function(from.x, from.y, to.x, to.y, dist = 0.2, ...) {
+ # angle of the line between `from` and `to`
+ theta <- atan2(to.y - from.y, to.x - from.x)
+
+ # push line starting/ending points away from node by a fixed distance
+ path_points = list(
+ x0 = from.x + dist * cos(theta),
+ y0 = from.y + dist * sin(theta),
+ x1 = to.x - dist * cos(theta),
+ y1 = to.y - dist * sin(theta)
+ )
+
+ # Find points for corners of arrow head (third point is `to`)
+ arrow_anchor_x = path_points$x1 - dist * cos(theta)
+ arrow_anchor_y = path_points$y1 - dist * sin(theta)
+ ad <- 0.1 * dist / tan(1/6 * pi)
+
+ path_points$a1_x = arrow_anchor_x + ad * cos(theta + 1/2 * pi)
+ path_points$a1_y = arrow_anchor_y + ad * sin(theta + 1/2 * pi)
+ path_points$a2_x = arrow_anchor_x - ad * cos(theta + 1/2 * pi)
+ path_points$a2_y = arrow_anchor_y - ad * sin(theta + 1/2 * pi)
+
+ # Draw arrow head in SVG path notation
+ as.character(glue::glue_data(
+ path_points,
+ "M{x0},{y0} L{x1},{y1} L{a1_x},{a1_y} L{a2_x},{a2_y} L{x1},{y1}"
+ ))
+ }
+
+ arrows <- reactive({
+ if (is.null(edges()) || length(edges()) == 0) return(NULL)
+ if (is.null(nodes()) || length(nodes()) == 0) return(NULL)
+
+ ep <- edge_points(edges(), nodes())
+ if (!nrow(ep)) return(NULL)
+
+ ep %>%
+ purrr::pmap_chr(arrow_path, dist = 0.2) %>%
+ purrr::map(~ list(
+ type = "path",
+ line = list(color = "#000", width = 1),
+ fillcolor = "#000",
+ path = .x,
+ opacity = 0.75
+ ))
+ })
+
+ create_node_annotations <- function(x, y, name, hash, ...) {
+ set_color <- function(
+ default, not_in_dag = NULL,
+ primary = NULL, secondary = NULL,
+ exposure = NULL, outcome = NULL, adjusted = NULL,
+ apply_order = c("primary", "not_in_dag", "adjusted", "exposure", "outcome", "secondary")
+ ) {
+ applicable_states <- c(
+ "primary" = !is.null(node_primary()) && hash %in% node_primary(),
+ "secondary" = !is.null(node_secondary()) && hash %in% node_secondary(),
+ "adjusted" = !is.null(node_is_adjusted()) && hash %in% node_is_adjusted(),
+ "exposure" = !is.null(node_exposure()) && hash %in% node_exposure(),
+ "outcome" = !is.null(node_outcome()) && hash %in% node_outcome(),
+ "not_in_dag" = x < 0
+ )
+
+ applicable_states <- applicable_states[applicable_states]
+ if (!length(applicable_states)) return(default)
+
+ applicable_states <- applicable_states[apply_order]
+ applicable_states <- applicable_states[!is.na(applicable_states)]
+ if (!length(applicable_states)) return(default)
+
+ color <- switch(
+ names(applicable_states)[1],
+ primary = primary,
+ secondary = secondary,
+ adjusted = adjusted,
+ outcome = outcome,
+ exposure = exposure,
+ not_in_dag = not_in_dag,
+ default
+ )
+
+ color %||% default
+ }
+
+ background_color <- set_color(
+ default = "rgba(255, 255, 255, 0.5)",
+ not_in_dag = "#FDFDFD",
+ primary = "rgba(246, 227, 209, 0.75)"
+ )
+ font_color <- set_color(
+ default = "#000000",
+ not_in_dag = "#666666",
+ primary = "#D3751C",
+ exposure = "#418c7a",
+ outcome = "#ba2d0b",
+ apply_order = c("not_in_dag", "exposure", "outcome", "primary")
+ )
+ border_color <- set_color(
+ default = "#EDEDED",
+ not_in_dag = "#AAAAAA",
+ primary = list(NULL),
+ adjusted = "#1c2d3f",
+ apply_order = c("not_in_dag", "adjusted", "primary")
+ )
+
+ list(text = name,
+ node_hash = hash,
+ x = x,
+ y = y,
+ font = list(size = 24, color = font_color),
+ showarrow = FALSE,
+ align = "center",
+ captureevents = TRUE,
+ textposition = "middle center",
+ bordercolor = border_color,
+ bgcolor = background_color,
+ borderpad = 4)
+ }
+
+ annotations <- reactive({
+ if (is.null(nodes()) || length(nodes()) == 0) return(NULL)
+ node_frame(nodes(), full = TRUE) %>%
+ purrr::pmap(create_node_annotations)
+ })
+
+ left_margin <- list(
+ type = "rect",
+ line = list(color = "#AAAAAA", width = 1),
+ fillcolor = "#EEEEEE",
+ x0 = -100,
+ y0 = -100,
+ x1 = 0,
+ y1 = 100
+ )
+
+ output$plot <- renderPlotly({
+ debug_line("rendering clickpad")
+ redraw_plot()
+
+ ax <- list(
+ title = "",
+ zeroline = FALSE,
+ showline = FALSE,
+ showticklabels = FALSE,
+ showgrid = TRUE,
+ range = list(-1.5, 12.5)
+ )
+ ay <- ax
+ y_min <- purrr::map_dbl(nodes(), "y") %>% min()
+ y_max <- purrr::map_dbl(nodes(), "y") %>% max()
+ ay$range <- list(min(0.5, y_min), max(7.5, y_max))
+
+ p <- plot_ly(type = "scatter", source = plotly_source)
+
+ p %>%
+ layout(
+ annotations = annotations(),
+ shapes = c(list(left_margin), arrows()),
+ xaxis = ax,
+ yaxis = ay
+ ) %>%
+ config(
+ edits = list(
+ annotationPosition = TRUE
+ ),
+ showAxisDragHandles = FALSE
+ # displayModeBar = FALSE
+ ) %>%
+ plotly::event_register("plotly_click") %>%
+ plotly::event_register("plotly_doubleclick") %>%
+ plotly::event_register("plotly_selected") %>%
+ plotly::event_register("plotly_clickannotation") %>%
+ htmlwidgets::onRender("
+ function(el) {
+ el.on('plotly_hover', function(d) { console.log('Hover: ', d) });
+ el.on('plotly_click', function(d) { console.log('Click: ', d) });
+ el.on('plotly_selected', function(d) { console.log('Select: ', d) });
+ }
+ ")
+ })
+
+ redraw_plot <- reactiveVal(Sys.time())
+
+ new_coords_lag <- list(hash = NA_character_, x = NA_real_, y = NA_real_)
+
+ new_locations <- reactive({
+ req(annotations())
+ ## https://stackoverflow.com/questions/54990350/extract-xyz-coordinates-from-draggable-shape-in-plotly-ternary-r-shiny
+
+ event <- event_data("plotly_relayout", source = plotly_source)
+ if (!length(event)) return()
+
+ annot_event <- event[grepl("^annotations\\[\\d+\\]\\.[xy]$", names(event))]
+ annot_index <- sub(".+\\[(\\d+)\\].+", "\\1", names(annot_event)[1]) %>% as.integer()
+
+ if (is.na(annot_index) || !is.integer(annot_index)) return()
+
+ if (length(annotations()) <= annot_index) {
+ stop("An error occurred, unable to match plotly update to correct node")
+ }
+
+ node_hash <- annotations()[[annot_index + 1]]$node_hash
+ # cli::cat_line("event_name: ", names(event)[1])
+ # cli::cat_line("annot_index: ", annot_index)
+ # cli::cat_line("node_hash: ", node_hash)
+
+ req(!is.null(node_hash))
+
+ new_x <- annot_event[grepl("\\.x", names(annot_event))] %>% unlist() %>% unname()
+ new_x <- if (new_x > 0) round(new_x, 0) else new_x
+
+ new_y <- annot_event[grepl("\\.y", names(annot_event))] %>% unlist() %>% unname()
+ new_y <- if (new_x > 0) round(new_y, 0) else new_y
+
+ i_nodes <- isolate(nodes())
+ current_pos <- i_nodes[[node_hash]][c("x", "y")] %>% unlist() %>% unname()
+
+ new_coords <- list(
+ hash = node_hash,
+ x = new_x,
+ y = new_y
+ )
+
+ if (identical(c(new_coords$x, new_coords$y), current_pos)) {
+ # No change in current node position
+ return()
+ }
+
+ if (identical(new_coords, new_coords_lag)) {
+ # The plotly_redraw may not have been a result of annotation position change
+ return()
+ }
+
+ new_coords_lag <<- new_coords
+
+ # cli::cat_line("new_x: ", new_x)
+ # cli::cat_line("new_y: ", new_y)
+ new_coords
+ })
+
+ return(reactive(new_locations()))
+}
diff --git a/R/module/dagPreview.R b/R/module/dagPreview.R
new file mode 100644
index 0000000..e7468cb
--- /dev/null
+++ b/R/module/dagPreview.R
@@ -0,0 +1,288 @@
+
+# UI Function -------------------------------------------------------------
+
+dagPreviewUI <- function(id, include_graph_downloads = TRUE, start_hidden = FALSE) {
+ ns <- shiny::NS(id)
+
+ class_3_col <- "col-md-4 col-md-offset-0 col-sm-8 col-sm-offset-2 col-xs-12"
+
+ download_choices <- c(
+ "PDF" = "pdf",
+ "PNG" = "png",
+ "LaTeX TikZ" = "tikz"
+ )
+
+ if (include_graph_downloads) {
+ download_choices <- c(
+ download_choices,
+ "dagitty (R: RDS)" = "dag_dagitty",
+ "ggdag (R: RDS)" = "dag_tidy"
+ )
+ }
+
+ tagList(
+ fluidRow(
+ column(
+ width = 12,
+ align = "center",
+ shinyjs::hidden(tags$div(
+ id = ns("tikzOut-help"),
+ class="alert alert-danger",
+ role="alert",
+ HTML(
+ "
An error occurred while compiling the preview.",
+ "Are there syntax errors in your labels?
",
+ "Note that using characters that are",
+ 'reserved',
+ 'characters in LaTeX syntax may cause issues. For example,',
+ "single $
need to be escaped: \\$
.
"
+ )
+ )),
+ tags$div(
+ class = "dag-preview-tikz",
+ shinycssloaders::withSpinner(uiOutput(ns("tikzOut")), color = "#C4C4C4", proxy.height = "400px")
+ )
+ )
+ ),
+ fluidRow(
+ tags$div(
+ class = class_3_col,
+ tags$div(
+ id = ns("showPreviewContainer"),
+ prettySwitch(ns("showPreview"), "Preview DAG", status = "primary", fill = TRUE, value = !start_hidden)
+ )
+ ),
+ tags$div(
+ class = class_3_col,
+ selectInput(
+ inputId = ns("downloadType"),
+ label = "Type of download",
+ choices = download_choices
+ ),
+ uiOutput(ns("downloadType_helptext"))
+ ),
+ tags$div(
+ class = paste(class_3_col, "dagpreview-download-ui"),
+ div(
+ class = "btn-group",
+ role = "group",
+ id = ns("download-buttons"),
+ downloadButton(ns("downloadButton"))
+ )
+ )
+ )
+ )
+}
+
+
+# Server Module -----------------------------------------------------------
+
+# This module takes tikz code and creates DAG preview content and returns TRUE
+# or FALSE value to track whether the preview is visible.
+dagPreview <- function(
+ input, output, session,
+ session_dir,
+ tikz_code,
+ dag_dagitty = reactive(NULL),
+ dag_tidy = reactive(NULL),
+ has_edges = reactive(FALSE)
+) {
+ ns <- session$ns
+ SESSION_TEMPDIR <- file.path(session_dir, sub("-$", "", ns("")))
+
+ tikz_cache_dir <- reactiveVal(NULL)
+
+ # Render tikz preview ----
+ observe({
+ req(input$showPreview)
+
+ tikz_lines <- tikz_code()
+ req(gsub("\\s", "", tikz_lines) != "")
+ debug_input(tikz_lines, ns("tikz_code"))
+
+ useLib <- "\\usetikzlibrary{matrix,arrows,decorations.pathmorphing}"
+
+ pkgs <- paste(buildUsepackage(pkg = list("tikz"), uselibrary = useLib), collapse = "\n")
+
+ tex_dir <-
+ tex_cached_preview(
+ session_dir = SESSION_TEMPDIR,
+ obj = tikz_lines,
+ stem = "DAGimage",
+ imgFormat = "png",
+ returnType = "shiny",
+ density = tex_opts$get("density"),
+ keep_pdf = TRUE,
+ usrPackages = pkgs,
+ margin = tex_opts$get("margin"),
+ cleanup = tex_opts$get("cleanup")
+ )
+ tikz_cache_dir(tex_dir)
+ }, priority = -100)
+
+ # Create tikz preview UI ----
+ output$tikzOut <- renderUI({
+ req(input$showPreview)
+
+ shiny::validate(
+ shiny::need(
+ tryCatch({tikz_code(); TRUE}, error = function(e) FALSE) ||
+ tryCatch(gsub("\\s", "", tikz_code()), error = function(e) "") != "",
+ paste(
+ "Nothing to see here... yet. Please use the Sketch tab to create",
+ "and layout a DAG."
+ )
+ )
+ )
+
+ if (is.null(tikz_cache_dir())) return()
+ if (!length(tikz_cache_dir())) {
+ shinyjs::show("tikzOut-help")
+ return()
+ } else {
+ shinyjs::hide("tikzOut-help")
+ }
+
+ image_path <- file.path(tikz_cache_dir(), "DAGimage.png")
+ if (!file.exists(image_path)) {
+ debug_line("Image does not exist: ", image_path)
+ return()
+ }
+
+ image_tmp <- tempfile("dag_image_", SESSION_TEMPDIR, ".png")
+ file.copy(image_path, image_tmp)
+ debug_line("Serving image: ", image_tmp)
+
+ tags$img(
+ src = sub("www/", "", image_tmp, fixed = TRUE),
+ contentType = "image/png",
+ style = "max-width: 100%; max-height: 600px; -o-object-fit: contain;",
+ alt = "DAG"
+ )
+ })
+
+ output$downloadType_helptext <- renderUI({
+ is_tikz_download <- input$downloadType %in% c("pdf", "png", "tikz")
+ if (is_tikz_download && !input$showPreview) {
+ shinyjs::disable("downloadButton")
+ return(helpText("Please preview DAG to enable downloads"))
+ }
+
+ if (!is_tikz_download && !has_edges()) {
+ shinyjs::disable("downloadButton")
+ return(helpText("Please add at least one edge to the DAG"))
+ }
+
+ if (!length(tikz_cache_dir())) {
+ shinyjs::disable("downloadButton")
+ return()
+ }
+
+ shinyjs::enable("downloadButton")
+ })
+
+ output$downloadButton <- downloadHandler(
+ filename = function() {
+ paste0(
+ "DAG.",
+ switch(
+ input$downloadType,
+ "dagitty" =,
+ "ggdag" = "rds",
+ "tikz" = "tex",
+ "png" = "png",
+ "pdf" = "pdf"
+ )
+ )
+ },
+ content = function(file) {
+ if (input$downloadType == "pdf") {
+
+ file.copy(file.path(tikz_cache_dir(), "DAGimageDoc.pdf"), file)
+
+ } else if (input$downloadType == "png") {
+
+ file.copy(file.path(tikz_cache_dir(), "DAGimage.png"), file)
+
+ } else if (input$downloadType == "tikz") {
+
+ merge_tex_files(
+ file.path(tikz_cache_dir(), "DAGimageDoc.tex"),
+ file.path(tikz_cache_dir(), "DAGimage.tex"),
+ file
+ )
+
+ } else if (input$downloadType == "dag_dagitty") {
+
+ if (is.null(dag_dagitty())) return(NULL)
+
+ saveRDS(dag_dagitty(), file = file)
+
+ } else if (input$downloadType == "dag_tidy") {
+
+ if (is.null(dag_tidy())) return(NULL)
+
+ saveRDS(dag_tidy(), file = file)
+ }
+ },
+ contentType = NA
+ )
+
+ return(reactive(input$showPreview))
+}
+
+
+# Helper Functions --------------------------------------------------------
+
+tex_cached_preview <- function(session_dir, ...) {
+ # Takes arguments for texPreview() except for fileDir
+ # hashes inputs and then writes preview into session_dir/args_hash
+ # Skips rendering if the cache already exists
+ # Returns directory containing the preview documents
+
+ args <- list(...)
+ args_hash <- digest::digest(args)
+
+ session_token <- basename(dirname(session_dir))
+ error_file <- paste0(session_token, "_", args_hash, ".tex")
+
+ cache_dir <- file.path(session_dir, args_hash)
+ error_dir <- file.path("www", "errors")
+
+ if (dir.exists(cache_dir)) {
+ return(cache_dir)
+ } else {
+ if (file.exists(file.path(error_dir, error_file))) {
+ # we already know that this tikz code won't work
+ warning("Bad tikz is still bad: ", error_file)
+ return(character())
+ }
+ }
+
+ dir.create(cache_dir, recursive = TRUE)
+ args$fileDir <- cache_dir
+ tryCatch({
+ do.call("texPreview", args)
+ cache_dir
+ }, error = function(e) {
+ # write bad tex code to disk
+ dir.create(error_dir, showWarnings = FALSE)
+ cat(
+ args$obj,
+ sep = "\n",
+ file = file.path(error_dir, error_file)
+ )
+ unlink(cache_dir, recursive = TRUE)
+ character()
+ })
+}
+
+# Merge tikz TeX source into main TeX file
+merge_tex_files <- function(main_file, input_file, out_file) {
+ x <- readLines(main_file)
+ y <- readLines(input_file)
+ which_line <- grep("input{", x, fixed = TRUE)
+ which_line <- intersect(which_line, grep(basename(input_file), x))
+ x[which_line] <- paste(y, collapse = "\n")
+ writeLines(x, out_file)
+}
diff --git a/R/module/examples.R b/R/module/examples.R
new file mode 100644
index 0000000..60fb419
--- /dev/null
+++ b/R/module/examples.R
@@ -0,0 +1,133 @@
+add_slug <- function(ex) {
+ ex %>%
+ purrr::map(~ {
+ .x$slug <- gsub("[.]rds$", "", .x$file, ignore.case = TRUE)
+ .x
+ })
+}
+
+keep_ex_with_file <- function(ex) {
+ ex %>%
+ purrr::keep(~ file.exists(.$file))
+}
+
+nullify_missing <- function(ex, field = "image") {
+ ex %>%
+ purrr::modify_depth(
+ .depth = 1,
+ ~ purrr::modify_at(., field, ~ {
+ if (!file.exists(.x)) list(NULL) else .x
+ })
+ )
+}
+
+full_path <- function(ex, field, path = file.path("www", "examples")) {
+ ex %>%
+ purrr::modify_depth(
+ .depth = 1,
+ ~ purrr::modify_at(., field, ~ file.path(path, .x))
+ )
+}
+
+rel_path <- function(ex, field, path = file.path("www/")) {
+ ex %>%
+ purrr::modify_depth(
+ .depth = 1,
+ ~ purrr::modify_at(., field, ~ if (!is.null(.x)) sub(path, "", .x, fixed = TRUE) else list(NULL))
+ )
+}
+
+load_example_values <- function(ex) {
+ purrr::map(ex, ~ {
+ values <- readRDS(.x$file)
+ .x$values <- list()
+ .x$values$nodes <- values$rvn$nodes
+ .x$values$edges <- values$rve$edges
+ .x
+ })
+}
+
+load_examples <- function(path = file.path("www", "examples")) {
+ ex_yaml <- file.path(path, "examples.yml")
+ if (!file.exists(ex_yaml)) {
+ stop("Unable to locate ", ex_yaml)
+ }
+
+ ex <- yaml::read_yaml(ex_yaml)
+
+ ex %>%
+ add_slug() %>%
+ full_path("image", path) %>%
+ full_path("file", path) %>%
+ keep_ex_with_file() %>%
+ nullify_missing("image") %>%
+ rel_path("image") %>%
+ load_example_values()
+}
+
+
+EXAMPLES <- load_examples()
+
+examples_UI <- function(id) {
+ ns <- NS(id)
+
+ make_examples_ui <- function(name, description, slug, image = NULL, ...) {
+ tagList(
+ tags$h3(name),
+ if (!is.null(image)) tags$div(
+ class = "example-image",
+ tags$img(src = image)
+ ),
+ tags$p(
+ HTML(description)
+ ),
+ actionButton(ns(slug), "Load Example")
+ )
+ }
+
+ tagList(
+ EXAMPLES %>%
+ purrr::map(`[`, c("name", "description", "slug", "image")) %>%
+ purrr::map(~ purrr::pmap(.x, make_examples_ui))
+ )
+}
+
+examples <- function(input, output, session) {
+ input_ids <- EXAMPLES %>% purrr::map_chr("slug")
+ values <- EXAMPLES %>% purrr::map("values")
+ names(values) <- input_ids
+
+ lagged_value <- setNames(rep(0L, length(input_ids)), input_ids)
+
+ example_value <- reactiveVal(NULL)
+
+ observe({
+ current_btn_vals <- purrr::map_int(input_ids, ~ input[[.x]])
+ req(any(current_btn_vals > 0L))
+ # cli::cat_line("lagged: ", lagged_value)
+ # cli::cat_line("current: ", current_btn_vals)
+
+ idx <- which(current_btn_vals != lagged_value)
+ lagged_value <<- current_btn_vals
+
+ if (!length(idx)) {
+ example_value(NULL)
+ return(NULL)
+ }
+
+ changed_input <- input_ids[idx]
+
+ example_value(values[[changed_input]])
+ })
+
+ return(reactive(example_value()))
+}
+
+
+# ui <- fluidPage(
+# examples_UI("example")
+# )
+# server <- function(input, output, session){
+# callModule(examples, 'example')
+# }
+# shinyApp(ui, server)
\ No newline at end of file
diff --git a/R/node.R b/R/node.R
new file mode 100644
index 0000000..3c711ed
--- /dev/null
+++ b/R/node.R
@@ -0,0 +1,289 @@
+# ---- Node Helper Functions ----
+node_new <- function(nodes, hash, name, gap_y = 0.75, min_y = 1) {
+ # new nodes are added into the clickpad area but with x = -0.75
+ # need to check if there are other nodes in the holding space and adjust y
+ taken_y <- nodes %>% purrr::keep(~ !is.na(.$y)) %>% purrr::map_dbl(`[[`, "y")
+ new_y <- find_new_y(taken_y, gap_y, min_y)
+ nodes[[hash]] <- list(name = name, x = -0.75, y = new_y)
+ nodes
+}
+
+find_new_y <- function(y, gap_y = 0.75, min_y = 0.5) {
+ if (!length(y)) return(min_y)
+ if (min(y) >= (min_y + gap_y)) return(min_y)
+ if (length(y) == 1) return(y + gap_y)
+
+ y <- sort(y)
+
+ gap_size <- c(lead(y) - y)[-length(y)]
+ if (any(gap_size >= 2 * gap_y)) {
+ first_gap <- which(gap_size > 2 * gap_y)[1]
+ return(y[first_gap] + gap_y)
+ }
+
+ max(y) + gap_y
+}
+
+node_name_valid <- function(nodes, name, warn = FALSE) {
+ if (!nzchar(name)) {
+ warnNotification("Please specify a name for the node")
+ return(FALSE)
+ }
+ name_in_nodes <- vapply(nodes, function(n) name == n$name, FALSE)
+ if (any(name_in_nodes)) {
+ if (warn) warnNotification('"', name, '" is already the name of a node')
+ FALSE
+ } else {
+ TRUE
+ }
+}
+
+node_names <- function(nodes, all = FALSE) {
+ if (!length(nodes)) {
+ return(character())
+ }
+ x <- invertNames(sapply(nodes, function(x) x$name))
+ if (all) {
+ return(x)
+ }
+ in_dag <- sapply(nodes, function(n) n$x >= 0)
+ x[in_dag]
+}
+
+node_name_from_hash <- function(nodes, hash) {
+ invertNames(node_names(nodes))[hash]
+}
+
+node_update <- function(nodes, hash, name = NULL, x = NULL, y = NULL, name_latex = NULL) {
+ # in general update a property if arg is not null, default to current value
+ nodes[[hash]]$name <- name %||% nodes[[hash]]$name
+ nodes[[hash]]$x <- x %||% nodes[[hash]]$x
+ nodes[[hash]]$y <- y %||% nodes[[hash]]$y
+ # for name_latex precedence is arg > name (arg) > existing > name (existing)
+ nodes[[hash]]$name_latex <- name_latex %||% (
+ (name %??% name) %>% escape_quotes() %>% escape_latex()
+ ) %||% nodes[[hash]]$name_latex %||% (
+ (nodes[[hash]]$name %??% nodes[[hash]]$name) %>% escape_quotes() %>% escape_latex()
+ )
+ nodes
+}
+
+node_set_attribute <- function(nodes, hash, attribs) {
+ for (node in names(nodes)) {
+ for (attrib in attribs) {
+ nodes[[node]][[attrib]] <- node %in% hash
+ }
+ }
+ nodes
+}
+
+node_unset_attribute <- function(nodes, hashes, attribs) {
+ for (hash in hashes) {
+ for (attrib in attribs) {
+ nodes[[hash]][[attrib]] <- FALSE
+ }
+ }
+ nodes
+}
+
+node_with_attribute <- function(nodes, attrib) {
+ if (length(nodes) == 0) return(NULL)
+ n <- nodes %>%
+ purrr::map(attrib) %>%
+ purrr::keep(isTRUE)
+ if (length(n)) n
+}
+
+node_parent <- function(nodes) {
+ names(node_with_attribute(nodes, "parent"))
+}
+
+node_child <- function(nodes) {
+ names(node_with_attribute(nodes, "child"))
+}
+
+node_adjusted <- function(nodes) {
+ names(node_with_attribute(nodes, "adjusted"))
+}
+
+node_delete <- function(nodes, hash) {
+ .nodes <- nodes[setdiff(names(nodes), hash)]
+ if (length(.nodes)) .nodes else list()
+}
+
+node_frame <- function(nodes, full = FALSE) {
+ if (!length(nodes)) {
+ return(tibble())
+ }
+ x <- bind_rows(nodes) %>%
+ mutate(hash = names(nodes)) %>%
+ select(hash, everything()) %>%
+ mutate(visible = !is.na(x), in_dag = x > 0) %>%
+ node_frame_complete()
+ if (full) {
+ return(x)
+ }
+ filter(x, in_dag)
+}
+
+node_frame_complete <- function(nodes) {
+ nodes$adjusted <- nodes[["adjusted"]] %||% FALSE
+ nodes$color_draw <- nodes[["color_draw"]] %||% "Black"
+ nodes$color_fill <- nodes[["color_fill"]] %||% "White"
+ nodes$color_text <- nodes[["color_text"]] %||% "Black"
+ nodes
+}
+
+node_vertices <- function(nodes) {
+ v_df <- node_frame(nodes)
+ vertices(
+ name = v_df$name,
+ x = v_df$x,
+ y = v_df$y,
+ hash = v_df$hash
+ )
+}
+
+node_nearest <- function(nodes, coordinfo, threshold = 0.5) {
+ nodes %>%
+ node_frame() %>%
+ mutate(dist = (x - coordinfo$x)^2 + (y - coordinfo$y)^2) %>%
+ arrange(dist) %>%
+ filter(dist <= threshold) %>%
+ slice(1) %>%
+ select(-dist)
+}
+
+nodes_in_dag <- function(nodes, include_staged = FALSE) {
+ n <- nodes %>%
+ purrr::keep(~ !is.na(.$x))
+
+ if (!include_staged) {
+ n <- purrr::keep(n, ~ .$x > 0)
+ }
+ names(n)
+}
+
+node_btn_id <- function(node_hash) paste0("node_toggle_", node_hash)
+node_btn_get_hash <- function(node_btn_id) sub("node_toggle_", "", node_btn_id, fixed = TRUE)
+
+node_tikz_style <- function(hash, adjusted, color_draw, color_fill, color_text, ...) {
+ # B/.style={fill=DarkRed, text=White}
+ if (!adjusted && color_fill == "White" && color_text == "Black") {
+ return(NA_character_)
+ }
+ style <-
+ list(
+ draw = if (adjusted) color_draw,
+ fill = color_fill,
+ text = color_text
+ ) %>%
+ purrr::compact() %>%
+ purrr::imap_chr(~ glue::glue("{.y}={.x}")) %>%
+ paste(collapse = ", ")
+
+ glue::glue("{hash}/.style={{{style}}}")
+}
+
+node_frame_add_style <- function(nodes) {
+ if (!"name_latex" %in% names(nodes)) nodes$name_latex <- ""
+ nodes %>%
+ mutate(
+ tikz_style = purrr::pmap_chr(nodes, node_tikz_style),
+ name_latex = case_when(
+ is.na(name_latex) | name_latex == "" ~ escape_latex(name),
+ TRUE ~ name_latex
+ ),
+ tikz_node = case_when(
+ !is.na(tikz_style) ~ paste(glue::glue("|[{hash}]| {name_latex}")),
+ TRUE ~ name_latex
+ )
+ )
+}
+
+escape_quotes <- function(x) {
+ x %??% gsub("(['\"])", "\\\\\\1", x)
+}
+
+escape_latex <- function(x, force = FALSE) {
+ if (is.null(x)) return(NULL)
+ if (!force && grepl("$", x, fixed = TRUE)) {
+ # has at least one dollar sign so we'll try to parse out the math
+ x_math <- chunk_math(x)
+ is_math <- attr(x_math, "is_math")
+ if (is.null(is_math) || !any(is_math)) {
+ # no math, just escape the original string
+ return(escape_latex(x, force = TRUE))
+ }
+ x_math[!is_math] <- x_math[!is_math] %>%
+ purrr::map_chr(escape_latex, force = TRUE)
+
+ return(paste0(x_math, collapse = ""))
+ }
+
+ ## escape: # $ % ^ & _ { }
+ ## replace: ~ -> \~{}
+ ## replace: \ -> \textbackslash
+ ## replace: < > -> \textless \textgreater
+ x <- gsub("\\", "\\textbackslash ", x, fixed = TRUE)
+ x <- gsub("<", "\\textless ", x, fixed = TRUE)
+ x <- gsub(">", "\\textgreater ", x, fixed = TRUE)
+ x <- gsub("([#$%^&_{}])", "\\\\\\1", x)
+ x <- gsub("~", "\\~{}", x, fixed = TRUE)
+ x
+}
+
+chunk_math <- function(x) {
+ x_s <- strsplit(x, character())[[1]]
+
+ idx <- which(grepl("$", x_s, fixed = TRUE))
+ if (!length(idx)) {
+ return(x)
+ }
+ # remove \\$ pairs from indexes
+ idx_has_escape <- which(grepl("\\", x_s[idx[idx > 1L] - 1L], fixed = TRUE))
+ if (length(idx_has_escape)) {
+ idx <- idx[-(idx_has_escape + as.integer(any(idx == 1)))]
+ }
+
+ # only include $ that touch at least one alphanum character
+ x_around_dollar <- purrr::map_chr(idx, ~ {
+ substr(x, max(0, .x - 1, na.rm = TRUE), min(nchar(x), .x + 1))
+ })
+ idx_no_adjacent_alpha <- which(!grepl("[[:alnum:]+=*{}.-]", x_around_dollar))
+ if (length(idx_no_adjacent_alpha)) {
+ idx <- idx[-idx_no_adjacent_alpha]
+ }
+ if (!length(idx)) {
+ return(x)
+ }
+
+ # finally, find the math chunks
+ chunks <- c()
+ is_math <- c()
+ i <- 1L
+ while (i < length(idx)) {
+ # idx[i - 1] ... idx[i]-1 -> not math
+ # idx[i]...idx[i+1] -> math
+ # skip ahead to idx[i + 2]
+ idx_not_math <- max(idx[i-1], 0, na.rm = TRUE)
+ chunks <- c(
+ chunks,
+ if (idx_not_math != idx[i] - 1L) substr(x, idx_not_math, idx[i] - 1L),
+ substr(x, idx[i], idx[i + 1]),
+ if (is.na(idx[i + 2]) & !idx[i + 1] == nchar(x)) {
+ substr(x, idx[i + 1] + 1, nchar(x))
+ }
+ )
+ is_math <- c(
+ is_math,
+ if (idx_not_math != idx[i] - 1L) FALSE,
+ TRUE,
+ if (is.na(idx[i + 2]) & !idx[i + 1] == nchar(x)) FALSE
+ )
+ i <- i + 2L
+ }
+
+ attributes(chunks)$is_math <- is_math
+ chunks
+}
diff --git a/R/tests/test-escape_latex.R b/R/tests/test-escape_latex.R
new file mode 100644
index 0000000..d5e504f
--- /dev/null
+++ b/R/tests/test-escape_latex.R
@@ -0,0 +1,37 @@
+source("../node.R")
+
+latex_text <- list(
+ list(t = "$m^2$", e = "$m^2$"),
+ list(t = "a $m^2$", e = "a $m^2$"),
+ list(t = "$m^2$ b", e = "$m^2$ b"),
+ list(t = "a $m^2$ b", e = "a $m^2$ b"),
+ list(t = "a $e=$$m^2$ b", e = "a $e=$$m^2$ b"),
+ list(t = "a $$ math", e = "a \\$\\$ math"),
+ list(t = "\\textbackslash", e = "\\textbackslash textbackslash"),
+ list(t = "# of", e = "\\# of"),
+ list(t = "$ amount", e = "\\$ amount"),
+ list(t = "my $$ is", e = "my \\$\\$ is"),
+ list(t = "$m $$ m$", e = "$m $$ m$"),
+ list(t = "$$ is $mc^2$", e = "\\$\\$ is $mc^2$"),
+ list(t = "a > b", e = "a \\textgreater b"), #<< extra space before b
+ list(t = "a < b", e = "a \\textless b"), #<< same
+ list(t = "a % b", e = "a \\% b"),
+ list(t = "a_b", e = "a\\_b"),
+ list(t = "a & b", e = "a \\& b"),
+ list(t = "a & b \\ c", e = "a \\& b \\textbackslash c"), #<< + space
+ list(t = "{a}", e = "\\{a\\}"),
+ list(t = "a ~ b", e = "a \\~{} b")
+)
+
+passed_test <- purrr::map_lgl(latex_text, function(x) {
+ identical(escape_latex(x$t), x$e)
+})
+
+if (all(passed_test)) {
+ cat('\nAll (', sum(passed_test), ') tests passed!', sep = '')
+} else {
+ cat('\nThere were', sum(!passed_test), "failures...")
+ purrr::walk(latex_text[!passed_test], function(x) {
+ cat("\n'", x$t, "' returned '", escape_latex(x$t), "' not '", x$e, "'", sep = "")
+ })
+}
diff --git a/R/xcolorPicker.R b/R/xcolorPicker.R
new file mode 100644
index 0000000..d8cc24a
--- /dev/null
+++ b/R/xcolorPicker.R
@@ -0,0 +1,81 @@
+# xcolors list ----
+
+if (!file.exists(file.path("data", "xcolors.csv"))) {
+ if (!dir.exists('data')) {
+ stop("Not sure where I am")
+ }
+ message("Getting xcolors color list")
+ read_gz <- function(x) readLines(gzcon(url(x)))
+
+ xcolors <-
+ list(
+ # x11 = "http://www.ukern.de/tex/xcolor/tex/x11nam.def.gz",
+ svg = "http://www.ukern.de/tex/xcolor/tex/svgnam.def.gz"
+ ) %>%
+ purrr::map(read_gz) %>%
+ purrr::flatten_chr() %>%
+ stringr::str_subset("^(%%|\\\\| )", negate = TRUE) %>%
+ stringr::str_remove("(;%|\\})$") %>%
+ readr::read_csv(col_names = c("color", "r", "g", "b")) %>%
+ arrange(color) %>%
+ readr::write_csv(file.path("data", "xcolors.csv"))
+} else {
+ xcolors <-
+ file.path("data/xcolors.csv") %>%
+ read.csv(stringsAsFactors = FALSE)
+}
+
+# Color Functions ----
+
+choose_dark_or_light <- function(x, black = "#000000", white = "#FFFFFF") {
+ # x = color_hex
+ color_rgb <- col2rgb(x)[, 1]
+ # from https://stackoverflow.com/a/3943023/2022615
+ color_rgb <- color_rgb / 255
+ color_rgb[color_rgb <= 0.03928] <- color_rgb[color_rgb <= 0.03928]/12.92
+ color_rgb[color_rgb > 0.03928] <- ((color_rgb[color_rgb > 0.03928] + 0.055)/1.055)^2.4
+ lum <- t(c(0.2126, 0.7152, 0.0722)) %*% color_rgb
+ if (lum[1, 1] > 0.179) eval(black) else eval(white)
+}
+
+xcolor_style <- function(hex, text, ...) {
+ glue::glue('background-color:{hex};color:{text}')
+}
+
+# Prep Color List ----
+
+xcolors <-
+ xcolors %>%
+ mutate(
+ hex = rgb(r, g, b, maxColorValue = 1),
+ text = purrr::map_chr(hex, choose_dark_or_light)
+ ) %>%
+ select(color, hex, text)
+
+
+xcolors_list <- xcolors$color
+names(xcolors_list) <- purrr::pmap_chr(xcolors, xcolor_style)
+
+xcolor_label <- function(value) {
+ xcolors %>% filter(color == value) %>% purrr::pmap_chr(xcolor_style)
+}
+
+# xcolorPicker() ----
+
+xcolorPicker <- function(inputId, label = NULL, selected = NULL, ...) {
+ selectizeInput(
+ inputId,
+ label = label,
+ choices = c("", xcolors_list),
+ multiple = FALSE,
+ selected = selected,
+ options = list(
+ searchField = "value",
+ render = I(
+ '{
+ item: (item, escape) => `${escape(item.value)}
`,
+ option: (item, escape) => `${escape(item.value)}
`
+ }'
+ ))
+ )
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index f1b8190..e88c855 100644
--- a/README.md
+++ b/README.md
@@ -6,29 +6,29 @@ shinyDAG is a web application that uses R and LaTeX to create publication-qualit
### Adding nodes and edges
-![Alt Text](https://github.com/GerkeLab/ShinyDAG/raw/master/Figures/AddNodeEdge.gif)
+![Alt Text](Figures/AddNodeEdge.gif)
### Editing DAG aesthetics
-![Alt Text](https://github.com/GerkeLab/ShinyDAG/raw/master/Figures/editEdge.gif)
+![Alt Text](Figures/editEdge.gif)
## Examplary usage
The following DAG was reproduced from "A structural approach to selection bias"5 (Figure 6A) using the shinyDAG web app.
-![alt text](https://github.com/tgerke/ShinyDAG/raw/master/Figures/example1.png "Hernan Example")
+![alt text](Figures/example1.png "Hernan Example")
For comparison, the DAG from the original article is shown below.
-![alt text](https://github.com/tgerke/ShinyDAG/raw/master/Figures/example1_hernan.png "Hernan Original")
+![alt text](Figures/example1_hernan.png "Hernan Original")
The DAG represents a study on the effects of antiretroviral therapy (E) on AIDS risk (D), where immunosuppression (U) is unmeasured. L represents presence of symptoms (such as fever, weight loss, and diarrhea) and C represents censoring. A spurious path exists between E and D due to selection bias. We can see this in shinyDAG by ensuring that we've selected E as the exposure, D as the outcome, adjusted for C, and then toggling the "Examine DAG elements" button in the bottom left corner. The spurious open path is displayed as D <- U -> L -> C <- E.
-![alt text](https://github.com/tgerke/ShinyDAG/raw/master/Figures/paths.png "shinyDAG path output")
+![alt text](Figures/paths.png "shinyDAG path output")
One possible resolution for this bias is to adjust for L. After toggling L in the "Select nodes to adjust" section, we see that all spurious E to D paths are now closed.
-![alt text](https://github.com/tgerke/ShinyDAG/raw/master/Figures/paths2.png "shinyDAG final path output")
+![alt text](Figures/paths2.png "shinyDAG final path output")
## Other features
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..3eefcb9
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+1.0.0
diff --git a/app.R b/app.R
deleted file mode 100755
index b791698..0000000
--- a/app.R
+++ /dev/null
@@ -1,633 +0,0 @@
-library(shiny)
-library(shinydashboard)
-library(DiagrammeR)
-library(dagitty)
-library(stringr)
-library(igraph)
-library(texPreview)
-library(shinyAce)
-library(dplyr)
-library(ggdag)
-library(shinyWidgets)
-
-tex_opts$set(list(density=1200,
- margin = list(left = 0, top = 0, right = 0, bottom = 0),
- cleanup = c("aux","log")))
-
-dir.create(file.path(getwd(), "www"))
-
-download.file(url="https://www.dropbox.com/s/ndmblxnkwvfwpot/GerkeLab-1200dpi-square.png?dl=1",
- destfile = file.path(paste0(getwd(),'/www/GerkeLab.png')) )
-
-ui <- dashboardPage(title = "shinyDAG",
- dashboardHeader(disable=TRUE),
- dashboardSidebar(disable = TRUE),
- dashboardBody(
- fluidRow(
- box(title="shinyDAG",
- column(12, align="center",uiOutput("tikzOut")),
- selectInput("downloadType","Type of download",
- choices=list("PDF" = 4, "PNG" = 3,"Latex Tikz" = 2, "dagitty R object" = 1,"ggdag R object" = 5)),
- downloadButton("downloadButton"),
- br(),br(),
- prettySwitch(
- inputId = "showFlow",
- label = "Examine DAG elements",
- status = "primary",
- fill = TRUE
- ),
- conditionalPanel(condition = "input.showFlow == 1",
- # textOutput("adjustText"),
- # verbatimTextOutput("adjustSets"),
- fluidRow(
- column(6,"Open paths",verbatimTextOutput("openPaths")),
- column(6,"Closed paths",verbatimTextOutput("closedPaths"))
- ))
- ),
- tabBox(title=div(img(src="GerkeLab.png",width=40,height=40)),
- tabPanel("Build",
- tags$style(type="text/css",
- ".shiny-output-error { visibility: hidden; }",
- ".shiny-output-error:before { visibility: hidden; }"
- ),
- textInput("nodeLabel","To add a node: type a label and click the grid"),
- checkboxInput("clickType","Click to remove a node",value=FALSE),
- plotOutput("clickPad",
- click = "click1"),
- fluidRow(
- column(6,uiOutput("fromEdge")),
- column(6,uiOutput("toEdge"))
- ),
- actionButton("edgeButton1","Add edge!"),
- actionButton("edgeButton2","Remove edge!"),
- uiOutput("adjustNodeCreate"),
- uiOutput("exposureNodeCreate"),
- uiOutput("outcomeNodeCreate")),
- tabPanel("Edit aesthetics",
- selectInput("arrowShape","Select arrow head", choices = c("stealth","stealth'","diamond",
- "triangle 90","hooks","triangle 45",
- "triangle 60","hooks reversed","*"), selected = "stealth"),
- uiOutput("curveAngle"),
- helpText("A negative degree will change the orientation of the curve."),
- fluidRow(
- column(4,uiOutput("curveColor")),
- column(4,uiOutput("curveLty")),
- column(4,uiOutput("curveThick"))
- )
- ),
- tabPanel("Edit LaTex",
- helpText("WARNING: Editing code here will only change the appearance of the DAG and not the information on paths provided."),
- uiOutput("texEdit"),
- actionButton("redoTex","Initiate Editing!"),
- conditionalPanel(
- condition = "input.redoTex == 1",
- uiOutput("tikzOutNew"),
- selectInput("downloadType2","Type of download",
- choices=list("PDF" = 3,"PNG" = 2,"Latex Tikz" = 1)),
- downloadButton("downloadButton2")
- )),
- tabPanel("About shinyDAG",
- h6("Development Team: Jordan Creed and Travis Gerke"),
- h6("For more information on our lab and other projects please check out our website at http://travisgerke.com"),
- h6("All code is available from https://github.com/GerkeLab/ShinyDAG"),
- h6("Any errors or comments can be directed to travis.gerke@moffitt.org or jordan.h.creed@moffitt.org"))
- )
- )
- )
- )
-
-###################################################################################################
-server <- function(input, output,session) {
-
- g <- make_empty_graph()
-
- makeReactiveBinding('g')
-
- # click Pad points
- points <- list(x=vector("numeric", 0), y=vector("numeric", 0), name=vector("character",0))
- makeReactiveBinding('points')
-
- points2 <- as.data.frame(cbind(x=rep(1:7,each=7), y=rep(1:7,7), name=rep(NA,49)))
- makeReactiveBinding('points2')
-
- # edge data
- edges <- list(from=vector("character", 0), to=vector("character", 0))
- makeReactiveBinding('edges')
-
- errorMessage1 <- NULL
-
- # adding/removing points on clickPad
- observeEvent(input$click1,{
- if(input$nodeLabel %in% points$name){
- errorMessage1<<- showNotification("Unpredictable Behavior: duplicate names",
- duration = 5,
- closeButton = TRUE, type="warning"
- )}
-
- if(input$clickType==FALSE & input$nodeLabel!=""){
- points$x <<- c(points$x,round(input$click1$x))
- points$y <<- c(points$y,round(input$click1$y))
- points$name <<- c(points$name,input$nodeLabel)
- points2$name <<- ifelse(round(input$click1$x)==points2$x & round(input$click1$y)==points2$y,
- input$nodeLabel,points2$name)
- } else if(input$clickType==TRUE){
- rmNode <- intersect(grep(round(input$click1$x),points$x),grep(round(input$click1$y),points$y))
- if(length(rmNode)>0){
- points$x[[rmNode]] <<- NA
- points$y[[rmNode]] <<- NA
- points$name[[rmNode]] <<- NA
- points2$name <<- ifelse(round(input$click1$x)==points2$x & round(input$click1$y)==points2$y,
- NA,points2$name)
- }
- } else{
- points$x <<- points$x
- points$y <<- points$y
- points$name <<- points$name
- }
- updateTextInput(session, "nodeLabel", value="")
- })
-
-
-
- # clickPad display
- output$clickPad <- renderPlot({
- if(length(points$x>=1)){
- plot(points$x,points$y, xlim=c(1, 7), ylim=c(1, 7),bty='n',xaxt='n',yaxt='n',ylab="",xlab="",xaxs="i",col="white")
- text(points$x,points$y, labels=points$name, cex= 2)
- grid()
- } else{
- plot(points$x,points$y, xlim=c(1, 7), ylim=c(1, 7),bty='n',xaxt='n',yaxt='n',ylab="",xlab="",xaxs="i")
- grid()
- }
- })
-
- output$adjustNodeCreate <- renderUI({
- checkboxGroupInput("adjustNode","Select nodes to adjust",choices = points$name[!is.na(points$name)],
- inline=TRUE)
- })
-
- output$exposureNodeCreate <- renderUI({
- checkboxGroupInput("exposureNode","Exposure",choices = points$name[!is.na(points$name)],
- inline=TRUE)
- })
-
- output$outcomeNodeCreate <- renderUI({
- checkboxGroupInput("outcomeNode","Outcome",choices = points$name[!is.na(points$name)],
- inline=TRUE)
- })
-
- output$adjustText <- renderText({
- if(is.null(input$exposureNode) & is.null(input$outcomeNode)){
- paste0("Minimal sufficient adjustment sets")
- } else{paste0("Minimal sufficient adjustment set(s) to estimate the effect of ",
- input$exposureNode," on ",input$outcomeNode)}
- })
-
-# add/remove nodes on DAG
- observeEvent(input$click1,{
- if(input$clickType==FALSE & input$nodeLabel!=""){
- g <<- g %>% add_vertices(1,
- name= input$nodeLabel,
- x = round(input$click1$x),
- y = round(input$click1$y),
- color = "white",
- shape = "none")
- } else if(input$clickType==TRUE){
- rmNode <- intersect(grep(round(input$click1$x),V(g)$x),grep(round(input$click1$y),V(g)$y))
- if(length(rmNode)>0){
- rmNode <- V(g)$name[[rmNode]]
- g <<- g %>% delete_vertices(rmNode)
- } else {g <<- g}
- } else {
- g <<- g
- }
- })
-
- output$fromEdge <- renderUI({
- selectInput("fromEdge2", "Parent node",choices = c("---",points$name[!is.na(points$name)]))
-
- })
-
- output$toEdge <- renderUI({
- selectInput("toEdge2", "Child node",choices = c("---",points$name[!is.na(points$name)]))
-
- })
-
- # add/remove edges to DAG
- observeEvent(input$edgeButton1,{
- if(input$fromEdge2 %in% V(g)$name & input$toEdge2 %in% V(g)$name){
- g <<- g %>%
- add_edges(c(input$fromEdge2,input$toEdge2)) %>%
- set_edge_attr("color", value = "black")
- } else {
- g <<- g
- }
-
- })
-
- observeEvent(input$edgeButton2,{
- if(input$fromEdge2 %in% V(g)$name & input$toEdge2 %in% V(g)$name){
- g <<- g %>%
- delete_edges(paste0(input$fromEdge2,"|",input$toEdge2))
- } else {
- g <<- g
- }
-
- })
-
- output$adjustSets <- renderPrint({
- if(!is.null(input$exposureNode) & !is.null(input$outcomeNode)){
- daggityCode1 <- paste0(ends(g,E(g))[,1],"->",ends(g,E(g))[,2])
- daggityCode1 <- paste(daggityCode1,collapse=";")
- daggityCode2 <- paste0("dag { ",daggityCode1, " }")
-
- g2 <- dagitty(daggityCode2)
-
- exposures(g2) <- input$exposureNode
- outcomes(g2) <- input$outcomeNode
-
- adjustResults <- adjustmentSets(g2)
- return(adjustResults)} else{return(print("Please indicate exposure and outcome"))}
- })
-
- output$condInd <- renderPrint({
- daggityCode1 <- paste0(ends(g,E(g))[,1],"->",ends(g,E(g))[,2])
- daggityCode1 <- paste(daggityCode1,collapse=";")
- daggityCode2 <- paste0("dag { ",daggityCode1, " }")
-
- g2 <- dagitty(daggityCode2)
-
- exposures(g2) <- input$exposureNode
- outcomes(g2) <- input$outcomeNode
- adjustedNodes(g2) <- input$adjustNode
-
- test <- impliedConditionalIndependencies(g2)
-
- return_list <- vector("character",0)
- for(i in 1:length(test)){
- return_list <- c(return_list,paste0(test[[i]]$X," is independent of ",test[[i]]$Y," given: ",paste0(test[[i]]$Z,collapse = " and ")))
- }
- return(cat(return_list, sep="\n"))#} else{return(print(""))}
- })
-
- output$openPaths <- renderPrint({
- if(!is.null(input$exposureNode) & !is.null(input$outcomeNode)){
- daggityCode1 <- paste0(ends(g,E(g))[,1],"->",ends(g,E(g))[,2])
- daggityCode1 <- paste(daggityCode1,collapse=";")
- daggityCode2 <- paste0("dag { ",daggityCode1, " }")
-
- g2 <- dagitty(daggityCode2)
-
- exposures(g2) <- input$exposureNode
- outcomes(g2) <- input$outcomeNode
- adjustedNodes(g2) <- input$adjustNode
-
- allComb <- as.data.frame(combn(names(g2), 2))
-
- pathData <- list(path=vector("character",0),open=vector("character",0))
- for(i in 1:ncol(allComb)){
- pathResults <- paths(g2,from=allComb[1,i],to=allComb[2,i],Z=input$adjustNode)
- pathData$path <- c(pathData$path, pathResults$paths)
- pathData$open <- c(pathData$open, pathResults$open)
- }
-
- openPaths <- grep("TRUE",pathData$open)
-
- return(cat(pathData$path[openPaths][str_count(pathData$path[openPaths], "-") >= 1],sep="\n"))} else{return(print(""))}
- })
-
- output$closedPaths <- renderPrint({
- if(!is.null(input$exposureNode) & !is.null(input$outcomeNode)){
- daggityCode1 <- paste0(ends(g,E(g))[,1],"->",ends(g,E(g))[,2])
- daggityCode1 <- paste(daggityCode1,collapse=";")
- daggityCode2 <- paste0("dag { ",daggityCode1, " }")
-
- g2 <- dagitty(daggityCode2)
-
- exposures(g2) <- input$exposureNode
- outcomes(g2) <- input$outcomeNode
- adjustedNodes(g2) <- input$adjustNode
-
- allComb <- as.data.frame(combn(names(g2), 2))
-
- pathData <- list(path=vector("character",0),open=vector("character",0))
- for(i in 1:ncol(allComb)){
- pathResults <- paths(g2,from=allComb[1,i],to=allComb[2,i],Z=input$adjustNode)
- pathData$path <- c(pathData$path, pathResults$paths)
- pathData$open <- c(pathData$open, pathResults$open)
- }
-
- closedPaths <- grep("FALSE",pathData$open)
-
- return(cat(pathData$path[closedPaths][str_count(pathData$path[closedPaths], "-") >= 1],sep="\n"))} else{return(print(""))}
- })
-
- output$curveAngle<-renderUI({
- if(length(ends(g,E(g))[,1])>=1){
- lapply(1:length(ends(g,E(g))[,1]),function(i){
- sliderInput(paste0("angle",ends(g,E(g))[i,1],"->",ends(g,E(g))[i,2]),paste0("Angle for ",paste0(ends(g,E(g))[i,1],"->",ends(g,E(g))[i,2])),
- min=-180,max=180,value=ifelse(is.null(input[[paste0("angle",ends(g,E(g))[i,1],"->",ends(g,E(g))[i,2])]]),0,input[[paste0("angle",ends(g,E(g))[i,1],"->",ends(g,E(g))[i,2])]]))
- })
- }
- })
-
- output$curveColor<-renderUI({
- lapply(1:length(ends(g,E(g))[,1]),function(i){
- textInput(paste0("color",ends(g,E(g))[i,1],"->",ends(g,E(g))[i,2]),
- paste0("Edge for ",ends(g,E(g))[i,1],"->",ends(g,E(g))[i,2]),
- value=ifelse(is.null(input[[paste0("color",ends(g,E(g))[i,1],"->",ends(g,E(g))[i,2])]]),"black",input[[paste0("color",ends(g,E(g))[i,1],"->",ends(g,E(g))[i,2])]]))
- })
- })
-
- output$curveLty<-renderUI({
- lapply(1:length(ends(g,E(g))[,1]),function(i){
- selectInput(paste0("lty",ends(g,E(g))[i,1],"->",ends(g,E(g))[i,2]),
- paste0("Line type for ",ends(g,E(g))[i,1],"->",ends(g,E(g))[i,2]),
- choices=c("solid","dashed"),
- selected = ifelse(is.null(input[[paste0("lty",ends(g,E(g))[i,1],"->",ends(g,E(g))[i,2])]]),"solid",input[[paste0("lty",ends(g,E(g))[i,1],"->",ends(g,E(g))[i,2])]]))
- })
- })
-
- output$curveThick<-renderUI({
- lapply(1:length(ends(g,E(g))[,1]),function(i){
- selectInput(paste0("lineT",ends(g,E(g))[i,1],"->",ends(g,E(g))[i,2]),
- paste0("Line thickness for ",ends(g,E(g))[i,1],"->",ends(g,E(g))[i,2]),
- choices=c("ultra thin","very thin","thin","semithick","thick","very thick","ultra thick"),
- selected = ifelse(is.null(input[[paste0("lineT",ends(g,E(g))[i,1],"->",ends(g,E(g))[i,2])]]),"thin",input[[paste0("lineT",ends(g,E(g))[i,1],"->",ends(g,E(g))[i,2])]]))
- })
- })
-
- output$tikzOut<-renderUI({
- if(length(V(g)$name)>=1){
- styleZ <- "\\tikzset{ module/.style={draw, rectangle},
- label/.style={ } }"
- startZ <- "\\begin{tikzpicture}[>=latex]"
- endZ <- "\\end{tikzpicture}"
- pathZ <- "\\path[->,font=\\scriptsize,>=angle 90]"
-
- nodeFrame <- points2
- nodeFrame <- nodeFrame[nodeFrame$x>=min(nodeFrame[!is.na(nodeFrame$name),]$x) &
- nodeFrame$x<=max(nodeFrame[!is.na(nodeFrame$name),]$x) &
- nodeFrame$y>=min(nodeFrame[!is.na(nodeFrame$name),]$y) &
- nodeFrame$y<=max(nodeFrame[!is.na(nodeFrame$name),]$y),]
- nodeFrame$name <- ifelse(is.na(nodeFrame$name),"~",nodeFrame$name)
- nodeFrame$nameA <- ifelse(nodeFrame$name %in% input$adjustNode, paste0(" |[module]| ",nodeFrame$name), nodeFrame$name)
- nodeLines <- vector("character",0)
- for (i in unique(nodeFrame$y)){
- createLines <- paste0(paste(nodeFrame[nodeFrame$y==i,]$nameA,collapse="&"),"\\\\")
- nodeLines <- c(nodeLines,createLines)
- }
- nodeLines <- rev(nodeLines)
- nodeLines2 <- nodeLines
-
- nodeLines <- paste0("\\matrix(m)[matrix of nodes, row sep=2.6em, column sep=2.8em,text height=1.5ex, text depth=0.25ex, nodes={label}] {",paste(nodeLines,collapse=""),"};")
-
- edgeLines <- vector("character",0)
-
- if(length(E(g))>=1){
- edgeFrame <- as.data.frame(ends(g,E(g)))
- edgeFrame$name <- paste0(edgeFrame$V1,"->",edgeFrame$V2)
- edgeFrame$angle <- edgeFrame$color <- edgeFrame$thick <- edgeFrame$type <- edgeFrame$loose <- NA
- edgeFrame$parent <- edgeFrame$child <- NA
-
- nodeFrame$revY <- rev(nodeFrame$y)
-
- for(i in 1:length(edgeFrame$name)){
- edgeFrame$angle[i] <- ifelse(!is.null(input[[paste0("angle",edgeFrame$name[i])]]),as.numeric(input[[paste0("angle",edgeFrame$name[i])]]),0)
- edgeFrame$color[i] <- ifelse(is.null(input[[paste0("color",edgeFrame$name[i])]]),"black",input[[paste0("color",edgeFrame$name[i])]])
- edgeFrame$thick[i] <- ifelse(is.null(input[[paste0("lineT",edgeFrame$name[i])]]),"thin",input[[paste0("lineT",edgeFrame$name[i])]])
- edgeFrame$type[i] <- ifelse(is.null(input[[paste0("lty",edgeFrame$name[i])]]),"solid",input[[paste0("lty",edgeFrame$name[i])]])
- edgeFrame$parent[i] <- paste0("(m-",(nodeFrame[nodeFrame$name==edgeFrame$V1[i],]$revY-min(nodeFrame$revY)+1),"-",
- (nodeFrame[nodeFrame$name==edgeFrame$V1[i],]$x-min(nodeFrame$x)+1),")")
- edgeFrame$child[i] <- paste0("(m-",(nodeFrame[nodeFrame$name==edgeFrame$V2[i],]$revY-min(nodeFrame$revY)+1),"-",
- (nodeFrame[nodeFrame$name==edgeFrame$V2[i],]$x-min(nodeFrame$x)+1),")")
- createEdge <- paste0(edgeFrame$parent[i]," edge [>=",input$arrowShape,", bend left = ",edgeFrame$angle[i],
- ", color = ",edgeFrame$color[i],",",edgeFrame$type[i],",", edgeFrame$thick[i],
- "] node[auto] {$~$} ",edgeFrame$child[i]," ")
- edgeLines <- c(edgeLines,createEdge)
- }
- }
-
- edgeLines <- paste0(pathZ,paste(edgeLines,collapse=""),";")
-
- allLines <- c(styleZ,startZ,nodeLines,edgeLines,endZ)
-
- tikzTemp <- paste(allLines,collapse="")
-
- useLib="\\usetikzlibrary{matrix,arrows,decorations.pathmorphing}"
-
- pkgs=paste(buildUsepackage(pkg = list('tikz'),uselibrary = useLib),collapse='\n')
-
- texPreview(obj = tikzTemp,
- stem = 'DAGimage',
- fileDir = paste0(getwd(),"/www"),
- imgFormat = 'png',
- returnType = 'shiny',
- density=tex_opts$get("density"),
- keep_pdf = TRUE,
- usrPackages = pkgs,
- margin = tex_opts$get("margin"),
- cleanup = tex_opts$get("cleanup")
- )
-
- filename <- normalizePath(file.path(paste0(getwd(),'/www/DAGimageDoc.pdf')))
-
- return(tags$iframe(style="height:600px; width:100%",src = "DAGimageDoc.pdf",
- scrolling="auto",seamless="seamless"))
-
- } else{
- startZ <- "\\begin{tikzpicture}[>=latex]"
- endZ <- "\\end{tikzpicture}"
-
- allLines <- c(startZ,endZ)
-
- tikzTemp <- paste(allLines,collapse="")
-
- useLib="\\usetikzlibrary{matrix,arrows,decorations.pathmorphing}"
-
- pkgs=paste(buildUsepackage(pkg = list('tikz'),uselibrary = useLib),collapse='\n')
-
- texPreview(obj = tikzTemp,
- stem = 'DAGimage',
- fileDir = paste0(getwd(),"/www"),
- imgFormat = 'png',
- returnType = 'shiny',
- density=300,
- usrPackages = pkgs)
-
- filename <- normalizePath(file.path(paste0(getwd(),'/www/DAGimageDoc.pdf')))
-
- return(tags$iframe(style="height:600px; width:100%",src = "DAGimageDoc.pdf", zoom=300,
- scrolling="auto",seamless="seamless"))
- }
-
- })
-
- output$downloadButton <- downloadHandler(
- filename = function() {
- paste0("DAG",Sys.Date(),ifelse(input$downloadType==1,".RData",
- ifelse(input$downloadType==2,".tex",
- ifelse(input$downloadType==3,".png",
- ifelse(input$downloadType==5,".RData",".pdf")))))
- },
- content = function(file) {
- if(input$downloadType==1){
- daggityCode1 <- paste0(ends(g,E(g))[,1],"->",ends(g,E(g))[,2])
- daggityCode1 <- paste(daggityCode1,collapse=";")
- daggityCode2 <- paste0("dag { ",daggityCode1, " }")
-
- g2 <- dagitty(daggityCode2)
-
- exposures(g2) <- input$exposureNode
- outcomes(g2) <- input$outcomeNode
- adjustedNodes(g2) <- input$adjustNode
-
- dagitty_code <- g2
- save(dagitty_code,file=file)
- } else if (input$downloadType==2){
- myfile <- paste0(getwd(),"/www/DAGimageDoc.tex")
- file.copy(myfile, file)
- } else if (input$downloadType==3){
- myfile <- paste0(getwd(),"/www/DAGimage.png")
- file.copy(myfile, file)
- } else if (input$downloadType==5) {
- daggityCode1 <- paste0(ends(g,E(g))[,1],"->",ends(g,E(g))[,2])
- daggityCode1 <- paste(daggityCode1,collapse=";")
- daggityCode2 <- paste0("dag { ",daggityCode1, " }")
-
- g2 <- dagitty(daggityCode2)
-
- exposures(g2) <- input$exposureNode
- outcomes(g2) <- input$outcomeNode
- adjustedNodes(g2) <- input$adjustNode
-
- tidy_dag <- tidy_dagitty(g2)
- save(tidy_dag,file=file)
- }else {
- myfile <- paste0(getwd(),"/www/DAGimageDoc.pdf")
- file.copy(myfile, file)
- }
-}, contentType = NA
- )
-
- output$texEdit <- renderUI({
- if(length(V(g)$name)>=1){
- styleZ <- "\\\\tikzset{ module/.style={draw, rectangle},
- label/.style={ } }"
- startZ <- "\\\\begin{tikzpicture}[>=latex]"
- endZ <- "\\\\end{tikzpicture}"
- pathZ <- "\\\\path[->,font=\\\\scriptsize,>=angle 90]"
-
- nodeFrame <- points2
- nodeFrame <- nodeFrame[nodeFrame$x>=min(nodeFrame[!is.na(nodeFrame$name),]$x) &
- nodeFrame$x<=max(nodeFrame[!is.na(nodeFrame$name),]$x) &
- nodeFrame$y>=min(nodeFrame[!is.na(nodeFrame$name),]$y) &
- nodeFrame$y<=max(nodeFrame[!is.na(nodeFrame$name),]$y),]
- nodeFrame$name <- ifelse(is.na(nodeFrame$name),"~",nodeFrame$name)
- nodeFrame$nameA <- ifelse(nodeFrame$name %in% input$adjustNode, paste0(" |[module]| ",nodeFrame$name), nodeFrame$name)
- nodeLines <- vector("character",0)
- for (i in unique(nodeFrame$y)){
- createLines <- paste0(paste(nodeFrame[nodeFrame$y==i,]$nameA,collapse="&"),"\\\\\\\\")
- nodeLines <- c(nodeLines,createLines)
- }
- nodeLines <- rev(nodeLines)
- nodeLines2 <- nodeLines
-
- nodeLines <- paste0("\\\\matrix(m)[matrix of nodes, row sep=2.6em, column sep=2.8em,text height=1.5ex, text depth=0.25ex, nodes={label}] {",paste(nodeLines,collapse=""),"};")
-
- edgeLines <- vector("character",0)
-
- if(length(E(g))>=1){
- edgeFrame <- as.data.frame(ends(g,E(g)))
- edgeFrame$name <- paste0(edgeFrame$V1,"->",edgeFrame$V2)
- edgeFrame$angle <- edgeFrame$color <- edgeFrame$thick <- edgeFrame$type <- edgeFrame$loose <- NA
- edgeFrame$parent <- edgeFrame$child <- NA
-
- nodeFrame$revY <- rev(nodeFrame$y)
-
- for(i in 1:length(edgeFrame$name)){
- edgeFrame$angle[i] <- ifelse(!is.null(input[[paste0("angle",edgeFrame$name[i])]]),as.numeric(input[[paste0("angle",edgeFrame$name[i])]]),0)
- edgeFrame$color[i] <- ifelse(is.null(input[[paste0("color",edgeFrame$name[i])]]),"black",input[[paste0("color",edgeFrame$name[i])]])
- edgeFrame$thick[i] <- ifelse(is.null(input[[paste0("lineT",edgeFrame$name[i])]]),"thin",input[[paste0("lineT",edgeFrame$name[i])]])
- edgeFrame$type[i] <- ifelse(is.null(input[[paste0("lty",edgeFrame$name[i])]]),"solid",input[[paste0("lty",edgeFrame$name[i])]])
- edgeFrame$parent[i] <- paste0("(m-",(nodeFrame[nodeFrame$name==edgeFrame$V1[i],]$revY-min(nodeFrame$revY)+1),"-",
- (nodeFrame[nodeFrame$name==edgeFrame$V1[i],]$x-min(nodeFrame$x)+1),")")
- edgeFrame$child[i] <- paste0("(m-",(nodeFrame[nodeFrame$name==edgeFrame$V2[i],]$revY-min(nodeFrame$revY)+1),"-",
- (nodeFrame[nodeFrame$name==edgeFrame$V2[i],]$x-min(nodeFrame$x)+1),")")
- createEdge <- paste0(edgeFrame$parent[i]," edge [>=",input$arrowShape,", bend left = ",edgeFrame$angle[i],
- ", color = ",edgeFrame$color[i],",",edgeFrame$type[i],",", edgeFrame$thick[i],
- "] node[auto] {$~$} ",edgeFrame$child[i]," ")
- edgeLines <- c(edgeLines,createEdge)
- }
- }
-
- edgeLines <- paste0(pathZ,paste(edgeLines,collapse=""),";")
-
- allLines <- c(styleZ,startZ,nodeLines,edgeLines,endZ)
-
- tikzTemp <- paste(allLines,collapse="")
-
-
- } else{
- startZ <- "\\\\begin{tikzpicture}[>=latex]"
- endZ <- "\\\\end{tikzpicture}"
-
- allLines <- c(startZ,endZ)
-
- tikzTemp <- paste(allLines,collapse="")
-
- }
- aceEditor("texChange",mode="latex",value=paste(allLines,collapse="\n"), theme="cobalt")
- })
-
- output$tikzOutNew<-renderUI({
-
- tikzTemp <- input$texChange
-
- useLib="\\usetikzlibrary{matrix,arrows,decorations.pathmorphing}"
-
- pkgs=paste(buildUsepackage(pkg = list('tikz'),uselibrary = useLib),collapse='\n')
-
- texPreview(obj = tikzTemp,
- stem = 'DAGimageEdit',
- fileDir = paste0(getwd(),"/www"),
- imgFormat = 'png',
- returnType = 'shiny',
- density=tex_opts$get("density"),
- keep_pdf = TRUE,
- usrPackages = pkgs,
- margin = tex_opts$get("margin"),
- cleanup = tex_opts$get("cleanup")
- )
-
- # filename <- normalizePath(file.path(paste0(getwd(),'/DAGimageEdit.png')))
-
- return(tags$iframe(style="height:560px; width:100%",src = "DAGimageEditDoc.pdf",
- scrolling="no",seamless="seamless"))
-
- })
-
- output$downloadButton2 <- downloadHandler(
- filename = function() {
- paste0("DAG",Sys.Date(),ifelse(input$downloadType2==1,".tex",
- ifelse(input$downloadType2==2,".png",".pdf")))
- },
- content = function(file) {
- if(input$downloadType2==1){
- myfile <- paste0(getwd(),"/www/DAGimageEditDoc.tex")
- file.copy(myfile, file)
- } else if (input$downloadType2==2){
- myfile <- paste0(getwd(),"/www/DAGimageEdit.png")
- file.copy(myfile, file)
- } else {
- myfile <- paste0(getwd(),"/www/DAGimageEditDoc.pdf")
- file.copy(myfile, file)
- }
- }, contentType = NA
- )
-
-}
-
-# Run the application
-shinyApp(ui = ui, server = server)
-
diff --git a/data/xcolors.csv b/data/xcolors.csv
new file mode 100644
index 0000000..54f363e
--- /dev/null
+++ b/data/xcolors.csv
@@ -0,0 +1,152 @@
+color,r,g,b
+AliceBlue,0.94,0.972,1
+AntiqueWhite,0.98,0.92,0.844
+Aqua,0,1,1
+Aquamarine,0.498,1,0.83
+Azure,0.94,1,1
+Beige,0.96,0.96,0.864
+Bisque,1,0.894,0.77
+Black,0,0,0
+BlanchedAlmond,1,0.92,0.804
+Blue,0,0,1
+BlueViolet,0.54,0.17,0.888
+Brown,0.648,0.165,0.165
+BurlyWood,0.87,0.72,0.53
+CadetBlue,0.372,0.62,0.628
+Chartreuse,0.498,1,0
+Chocolate,0.824,0.41,0.116
+Coral,1,0.498,0.312
+CornflowerBlue,0.392,0.585,0.93
+Cornsilk,1,0.972,0.864
+Crimson,0.864,0.08,0.235
+Cyan,0,1,1
+DarkBlue,0,0,0.545
+DarkCyan,0,0.545,0.545
+DarkGoldenrod,0.72,0.525,0.044
+DarkGray,0.664,0.664,0.664
+DarkGreen,0,0.392,0
+DarkGrey,0.664,0.664,0.664
+DarkKhaki,0.74,0.716,0.42
+DarkMagenta,0.545,0,0.545
+DarkOliveGreen,0.332,0.42,0.185
+DarkOrange,1,0.55,0
+DarkOrchid,0.6,0.196,0.8
+DarkRed,0.545,0,0
+DarkSalmon,0.912,0.59,0.48
+DarkSeaGreen,0.56,0.736,0.56
+DarkSlateBlue,0.284,0.24,0.545
+DarkSlateGray,0.185,0.31,0.31
+DarkSlateGrey,0.185,0.31,0.31
+DarkTurquoise,0,0.808,0.82
+DarkViolet,0.58,0,0.828
+DeepPink,1,0.08,0.576
+DeepSkyBlue,0,0.75,1
+DimGray,0.41,0.41,0.41
+DimGrey,0.41,0.41,0.41
+DodgerBlue,0.116,0.565,1
+FireBrick,0.698,0.132,0.132
+FloralWhite,1,0.98,0.94
+ForestGreen,0.132,0.545,0.132
+Fuchsia,1,0,1
+Gainsboro,0.864,0.864,0.864
+GhostWhite,0.972,0.972,1
+Gold,1,0.844,0
+Goldenrod,0.855,0.648,0.125
+Gray,0.5,0.5,0.5
+Green,0,0.5,0
+GreenYellow,0.68,1,0.185
+Grey,0.5,0.5,0.5
+Honeydew,0.94,1,0.94
+HotPink,1,0.41,0.705
+IndianRed,0.804,0.36,0.36
+Indigo,0.294,0,0.51
+Ivory,1,1,0.94
+Khaki,0.94,0.9,0.55
+Lavender,0.9,0.9,0.98
+LavenderBlush,1,0.94,0.96
+LawnGreen,0.488,0.99,0
+LemonChiffon,1,0.98,0.804
+LightBlue,0.68,0.848,0.9
+LightCoral,0.94,0.5,0.5
+LightCyan,0.88,1,1
+LightGoldenrod,0.933,0.867,0.51
+LightGoldenrodYellow,0.98,0.98,0.824
+LightGray,0.828,0.828,0.828
+LightGreen,0.565,0.932,0.565
+LightGrey,0.828,0.828,0.828
+LightPink,1,0.712,0.756
+LightSalmon,1,0.628,0.48
+LightSeaGreen,0.125,0.698,0.668
+LightSkyBlue,0.53,0.808,0.98
+LightSlateBlue,0.518,0.44,1
+LightSlateGray,0.468,0.532,0.6
+LightSlateGrey,0.468,0.532,0.6
+LightSteelBlue,0.69,0.77,0.87
+LightYellow,1,1,0.88
+Lime,0,1,0
+LimeGreen,0.196,0.804,0.196
+Linen,0.98,0.94,0.9
+Magenta,1,0,1
+Maroon,0.5,0,0
+MediumAquamarine,0.4,0.804,0.668
+MediumBlue,0,0,0.804
+MediumOrchid,0.73,0.332,0.828
+MediumPurple,0.576,0.44,0.86
+MediumSeaGreen,0.235,0.7,0.444
+MediumSlateBlue,0.484,0.408,0.932
+MediumSpringGreen,0,0.98,0.604
+MediumTurquoise,0.284,0.82,0.8
+MediumVioletRed,0.78,0.084,0.52
+MidnightBlue,0.098,0.098,0.44
+MintCream,0.96,1,0.98
+MistyRose,1,0.894,0.884
+Moccasin,1,0.894,0.71
+NavajoWhite,1,0.87,0.68
+Navy,0,0,0.5
+NavyBlue,0,0,0.5
+OldLace,0.992,0.96,0.9
+Olive,0.5,0.5,0
+OliveDrab,0.42,0.556,0.136
+Orange,1,0.648,0
+OrangeRed,1,0.27,0
+Orchid,0.855,0.44,0.84
+PaleGoldenrod,0.932,0.91,0.668
+PaleGreen,0.596,0.985,0.596
+PaleTurquoise,0.688,0.932,0.932
+PaleVioletRed,0.86,0.44,0.576
+PapayaWhip,1,0.936,0.835
+PeachPuff,1,0.855,0.725
+Peru,0.804,0.52,0.248
+Pink,1,0.752,0.796
+Plum,0.868,0.628,0.868
+PowderBlue,0.69,0.88,0.9
+Purple,0.5,0,0.5
+Red,1,0,0
+RosyBrown,0.736,0.56,0.56
+RoyalBlue,0.255,0.41,0.884
+SaddleBrown,0.545,0.27,0.075
+Salmon,0.98,0.5,0.448
+SandyBrown,0.956,0.644,0.376
+SeaGreen,0.18,0.545,0.34
+Seashell,1,0.96,0.932
+Sienna,0.628,0.32,0.176
+Silver,0.752,0.752,0.752
+SkyBlue,0.53,0.808,0.92
+SlateBlue,0.415,0.352,0.804
+SlateGray,0.44,0.5,0.565
+SlateGrey,0.44,0.5,0.565
+Snow,1,0.98,0.98
+SpringGreen,0,1,0.498
+SteelBlue,0.275,0.51,0.705
+Tan,0.824,0.705,0.55
+Teal,0,0.5,0.5
+Thistle,0.848,0.75,0.848
+Tomato,1,0.39,0.28
+Turquoise,0.25,0.88,0.815
+Violet,0.932,0.51,0.932
+VioletRed,0.816,0.125,0.565
+Wheat,0.96,0.87,0.7
+White,1,1,1
+WhiteSmoke,0.96,0.96,0.96
+Yellow,1,1,0
+YellowGreen,0.604,0.804,0.196
diff --git a/dev/Dockerfile b/dev/Dockerfile
new file mode 100644
index 0000000..e7e905c
--- /dev/null
+++ b/dev/Dockerfile
@@ -0,0 +1,95 @@
+# shiny-verse:3.6.0
+FROM rocker/verse:3.5.3
+
+RUN apt-get update -qq && apt-get -y --no-install-recommends install \
+ libxml2-dev \
+ libcairo2-dev \
+ libsqlite3-dev \
+ libmariadbd-dev \
+ libmariadb-client-lgpl-dev \
+ libpq-dev \
+ libssl-dev \
+ libcurl4-openssl-dev \
+ libssh2-1-dev \
+ unixodbc-dev \
+ && install2.r --error \
+ --deps TRUE \
+ tidyverse \
+ dplyr \
+ devtools \
+ formatR \
+ remotes \
+ selectr \
+ caTools \
+ BiocManager
+
+LABEL maintainer="Travis Gerke (Travis.Gerke@moffitt.org)"
+
+# Install system dependencies for required packages
+RUN apt-get update -qq && apt-get -y --no-install-recommends install \
+ libssl-dev \
+ libxml2-dev \
+ libmagick++-dev \
+ libv8-3.14-dev \
+ libglu1-mesa-dev \
+ freeglut3-dev \
+ mesa-common-dev \
+ libudunits2-dev \
+ libpoppler-cpp-dev \
+ libwebp-dev \
+ && apt-get clean \
+ && rm -rf /var/lib/apt/lists/ \
+ && rm -rf /tmp/downloaded_packages/ /tmp/*.rds
+
+RUN install2.r --error --deps TRUE \
+ shinyAce \
+ shinydashboard \
+ shinyWidgets \
+ DiagrammeR \
+ ggdag \
+ igraph \
+ pdftools \
+ shinyBS \
+ && rm -rf /tmp/downloaded_packages/ /tmp/*.rds
+
+RUN Rscript -e "devtools::install_github('metrumresearchgroup/texPreview', ref = 'e954322')" \
+ && rm -rf /tmp/downloaded_packages/ /tmp/*.rds
+
+# Install TinyTeX
+RUN install2.r --error tinytex \
+ && export CTAN_REPO="http://mirror.las.iastate.edu/tex-archive/systems/texlive/tlnet" \
+ && wget -qO- \
+ "https://github.com/yihui/tinytex/raw/master/tools/install-unx.sh" | \
+ sh -s - --admin --no-path \
+ && mv ~/.TinyTeX /opt/TinyTeX \
+ && /opt/TinyTeX/bin/*/tlmgr path add \
+ && tlmgr update --self \
+ && tlmgr install metafont mfware inconsolata tex ae parskip listings \
+ && tlmgr install standalone varwidth xcolor colortbl multirow psnfss setspace pgf \
+ && tlmgr path add \
+ && Rscript -e "tinytex::r_texmf()" \
+ && chown -R root:staff /opt/TinyTeX \
+ && chmod -R a+w /opt/TinyTeX \
+ && chmod -R a+wx /opt/TinyTeX/bin \
+ && echo "PATH=${PATH}" >> /usr/local/lib/R/etc/Renviron \
+ && rm -rf /tmp/downloaded_packages/ /tmp/*.rds
+
+RUN install2.r --error --deps TRUE shinyjs \
+ && rm -rf /tmp/downloaded_packages/ /tmp/*.rds
+
+RUN install2.r --error plotly \
+ && rm -rf /tmp/downloaded_packages/ /tmp/*.rds
+
+RUN install2.r --error shinycssloaders \
+ && rm -rf /tmp/downloaded_packages/ /tmp/*.rds
+
+ARG TRIGGER_UPDATE=unknown
+RUN installGithub.r gadenbuie/grkstyle r-lib/styler
+
+RUN installGithub.r gadenbuie/shinyThings@undo \
+ && rm -rf /tmp/downloaded_packages/ /tmp/*.rds
+
+#ARG SHINY_APP_IDLE_TIMEOUT=600
+#RUN sed -i "s/directory_index on;/app_idle_timeout ${SHINY_APP_IDLE_TIMEOUT};/g" /etc/shiny-server/shiny-server.conf
+#COPY . /srv/shiny-server/shinyDAG
+#RUN chown -R shiny:shiny /srv/shiny-server/
diff --git a/dev/README.md b/dev/README.md
new file mode 100644
index 0000000..c2430c8
--- /dev/null
+++ b/dev/README.md
@@ -0,0 +1,36 @@
+## Building and developing shinyDAG
+
+### shinyDAG dev environment
+
+I use the Dockerfile in this folder to create a fully-featured RStudio docker container that is *pretty close* to the final shinyDAG environment.
+Note that it's not perfect and if you install packages into this container, you'll need to also update the main shinyDAG docker file [here](../Dockerfile).
+
+To create the dev environment:
+
+```bash
+# make sure you're in the ./dev folder
+cd dev
+
+# make the dev image
+docker build -t shinydag-dev .
+
+# move back to shinyDAG proper and start up the dev image
+cd ..
+docker run --rm -p 8787:8787 -v $(pwd):/home/rstudio/shinydag -e PASSWORD="password" shinydag-dev
+```
+
+(Note that you should probably change the password above, but if you're only running locally it's not a big deal.)
+
+Then navigate to and login using the password you entered.
+
+### Create shinyDAG image
+
+```bash
+docker build -t gerkelab/shinydag:dev .
+
+# To send up to docker hub
+docker push
+```
+
+The `:dev` indicates that this image is tagged `dev`, but this can be anything you want.
+If you don't add a tag, it's assumed to be `:latest` which is kind of like git's `master` but for docker containers.
diff --git a/global.R b/global.R
new file mode 100644
index 0000000..7004b9f
--- /dev/null
+++ b/global.R
@@ -0,0 +1,83 @@
+library(shiny)
+library(shinydashboard)
+library(DiagrammeR)
+library(dagitty)
+library(igraph)
+library(texPreview)
+library(shinyAce)
+library(shinyBS)
+library(dplyr)
+library(ggdag)
+library(shinyWidgets)
+library(shinyjs)
+library(shinycssloaders)
+library(shinyThings)
+source("R/node.R")
+source("R/edge.R")
+source("R/columns.R")
+source("R/module/clickpad.R")
+source("R/module/dagPreview.R")
+source("R/module/examples.R")
+source("R/xcolorPicker.R")
+source("R/aes_ui.R")
+# Additional libraries: tidyr, digest, rlang
+
+enableBookmarking(store = "server")
+
+tex_opts$set(list(
+ density = 1200,
+ margin = list(left = 0, top = 0, right = 0, bottom = 0),
+ cleanup = c("aux", "log")
+))
+
+
+
+# Functions ---------------------------------------------------------------
+
+DEBUG <- getOption("shinydag.debug", FALSE)
+debug_input <- function(x, x_name = NULL) {
+ if (!isTRUE(DEBUG)) return()
+
+ if (is.null(x)) {
+ cat(if (!is.null(x_name)) paste0(x_name, ":"), "NULL", "\n")
+ } else if (inherits(x, "igraph")) {
+ cat(capture.output(print(x)), "", sep = "\n")
+ } else if (length(x) == 1 && !is.list(x)) {
+ cat(if (!is.null(x_name)) paste0(x_name, ":"), if (length(names(x))) names(x), "-", x, "\n")
+ } else if (is.list(x) && length(x) == 0) {
+ cat(if (!is.null(x_name)) paste0(x_name, ":"), "list()", "\n")
+ } else {
+ if (!inherits(x, "data.frame")) x <- tibble::enframe(x)
+ cat(if (!is.null(x_name)) paste0(x_name, ":"), knitr::kable(x), "", sep = "\n")
+ }
+}
+debug_line <- function(...) {
+ if (!isTRUE(DEBUG)) return()
+ cli::cat_line(...)
+}
+
+
+buildUsepackage <- if (length(find("build_usepackage"))) texPreview::build_usepackage else texPreview::buildUsepackage
+
+# use y if x is.null
+`%||%` <- function(x, y) if (is.null(x)) y else x
+# use y if x is not null(ish) (otherwise NULL)
+`%??%` <- function(x, y) if (!is.null(x) && x != "") y
+
+warnNotification <- function(...) showNotification(
+ paste0(...), duration = 5, closeButton = TRUE, type = "warning"
+)
+
+invertNames <- function(x) setNames(names(x), unname(x))
+
+# String utilities ----
+
+str_and <- function(...) {
+ x <- c(...)
+ last <- if (length(x) > 2) ", and " else " and "
+ glue::glue_collapse(x, sep = ", ", last = last)
+}
+
+str_plural <- function(x, word, plural = paste0(word, "s")) {
+ if (length(x) > 1) plural else word
+}
diff --git a/server.R b/server.R
new file mode 100755
index 0000000..ddfa1b3
--- /dev/null
+++ b/server.R
@@ -0,0 +1,895 @@
+
+# Server ------------------------------------------------------------------
+server <- function(input, output, session) {
+ # ---- Global - Session Temp Directory ----
+ SESSION_TEMPDIR <- file.path("www", session$token)
+ dir.create(SESSION_TEMPDIR, showWarnings = FALSE)
+ onStop(function() {
+ message("Removing session tempdir: ", SESSION_TEMPDIR)
+ unlink(SESSION_TEMPDIR, recursive = TRUE)
+ })
+ message("Using session tempdir: ", SESSION_TEMPDIR)
+
+ # ---- Global - Bookmarking ----
+ onBookmark(function(state) {
+ state$values$rvn <- list()
+ state$values$rvn$nodes <- rvn$nodes
+ state$values$rve <- list()
+ state$values$rve$edges <- rve$edges
+ state$values$query_string <- session$clientData$url_search
+
+ # Store outcome/exposure/adjust node selections
+ state$values$sel <- list(
+ exposureNode = input$exposureNode,
+ outcomeNode = input$outcomeNode,
+ adjustNode = input$adjustNode
+ )
+ })
+
+ onBookmarked(function(url) {
+ message("bookmark: ", url)
+ showBookmarkUrlModal(url)
+ updateQueryString(url)
+ })
+
+ onRestore(function(state) {
+ showModal(modalDialog(
+ title = NULL,
+ easyClose = FALSE,
+ footer = NULL,
+ tags$p(class = "text-center", "Loading your shinyDag workspace, please wait."),
+ tags$div(class = "gerkelab-spinner")
+ ))
+
+ # clear selected node and text input to try to prevent existing values from
+ # changing the name of the node that gets selected on restore
+ rvn$nodes <- node_unset_attribute(rvn$nodes, names(rvn$nodes), "parent")
+ updateTextInput(session, "node_list_node_name", value = "")
+
+ if (isTRUE(getOption("shinydag.debug", FALSE))) {
+ names(state$values) %>%
+ purrr::set_names() %>%
+ purrr::map(~ state$values[[.]]) %>%
+ purrr::compact() %>%
+ purrr::iwalk(~ debug_input(.x, paste0("state$values$", .y)))
+ }
+ rvn$nodes <- state$values$rvn$nodes
+ rve$edges <- state$values$rve$edges
+ })
+
+ onRestored(function(state) {
+ removeModal()
+ updateSelectInput(session, "exposureNode", selected = state$values$sel$exposureNode)
+ updateSelectInput(session, "outcomeNode", selected = state$values$sel$outcomeNode)
+ updateSelectizeInput(session, "adjustNode", selected = state$values$sel$adjustNode)
+ })
+
+ # ---- Global - Reactive Values ----
+ rve <- reactiveValues(edges = list())
+ rvn <- reactiveValues(nodes = list())
+
+ # rve$edges is a named list, e.g. for hash(A) -> hash(B):
+ # rve$edges[edge_key(hash(A), hash(B))] = list(from = hash(A), to = hash(B))
+
+ # rvn$nodes is a named list where name is a hash
+ # rvn$nodes$abcdefg = list(name, x, y)
+
+ # ---- Sketch - Reactive Values Undo/Redo ----
+ rv_undo_state <- shinyThings::undoHistory(
+ id = "undo_rv",
+ value = reactive({
+ req(length(rvn$nodes) > 0)
+
+ node_params <- c("name", "x", "y", "parent", "exposure", "outcome", "adjusted")
+ nodes <- rvn$nodes %>%
+ purrr::map(`[`, node_params) %>%
+ purrr::map(purrr::compact)
+
+ edge_params <- c("from", "to")
+ edges <- rve$edges %>%
+ purrr::map(`[`, edge_params) %>%
+ purrr::map(purrr::compact)
+
+ list(
+ nodes = nodes,
+ edges = edges
+ )
+ })
+ )
+
+ observe({
+ req(!is.null(rv_undo_state()))
+
+ rv_state <- rv_undo_state()
+ debug_input(rv_state$nodes, "undo/redo - new nodes")
+ debug_input(rv_state$edges, "undo/redo - new edges")
+ rvn$nodes <- rv_state$nodes
+ rve$edges <- rv_state$edges
+ }, priority = 1000)
+
+ # ---- Sketch - Node Controls ----
+ node_btn_id <- function(node_hash) paste0("node_toggle_", node_hash)
+ node_btn_get_hash <- function(node_btn_id) sub("node_toggle_", "", node_btn_id, fixed = TRUE)
+
+ node_list_buttons_redraw <- reactiveVal(Sys.time())
+ node_list_node_is_new <- reactiveVal(FALSE)
+ node_list_selected_child <- reactive({ node_child(rvn$nodes) }) # TODO: remove
+ node_list_selected_node <- reactiveVal(NULL)
+ observe({
+ I("update selected node?")
+ # this feels hacky but on the one hand we want to be able to update the
+ # selected parent node just by updating rvn$nodes, and on the other we don't
+ # want to propagate a reactive change if the value stays the same. So this
+ # observer is kind of like a debouncer for node_list_selected_node()
+ current_selected_node <- isolate(node_list_selected_node())
+ new_selected_node <- node_parent(rvn$nodes)
+ if (!identical(current_selected_node, new_selected_node)) {
+ node_list_selected_node(new_selected_node)
+ }
+ })
+
+ # debug selected nodes
+ observe({
+ debug_input(node_list_selected_node(), "node_list_selected_node")
+ debug_input(node_list_selected_child(), "node_list_selected_child")
+ })
+
+ # Handle add node button, creates new node and sets focus
+ observeEvent(input$node_list_node_add, {
+ new_node_hash <- digest::digest(Sys.time())
+ rvn$nodes <- node_new(rvn$nodes, new_node_hash, "new node") %>%
+ node_set_attribute(new_node_hash, "parent")
+ node_list_buttons_redraw(Sys.time())
+ node_list_node_is_new(TRUE)
+ })
+
+ # Show, hide or update node name text input
+ observe({
+ I("show/hide/update node name text box")
+ if (is.null(node_list_selected_node())) {
+ shinyjs::hide("node_list_node_name_container")
+ return()
+ }
+
+ s_node_selected <- node_list_selected_node()
+
+ # Selected node already exists, update UI
+ shinyjs::show("node_list_node_name_container")
+ shinyjs::runjs("set_input_focus('node_list_node_name')")
+ s_node_name <- node_name_from_hash(isolate(rvn$nodes), s_node_selected)
+ if (isolate(node_list_node_is_new())) {
+ node_list_node_is_new(FALSE)
+ updateTextInput(session, "node_list_node_name", value = "", placeholder = "Enter Node Name")
+ } else {
+ updateTextInput(
+ session,
+ "node_list_node_name",
+ value = unname(s_node_name)
+ )
+ }
+ }, priority = 1000)
+
+ # Handle node name text input
+ node_name_text_input <- reactive({
+ input$node_list_node_name
+ })
+
+ observe({
+ I("update node name")
+ node_name_debounced <- debounce(node_name_text_input, 750)
+ node_name <- node_name_debounced()
+ debug_input(node_name, "node_list_node_name (debounced)")
+ s_node <- isolate(node_list_selected_node())
+ req(s_node, node_name != "")
+ rvn$nodes <- node_update(isolate(rvn$nodes), s_node, node_name)
+ }, priority = 2000)
+
+ # Show editing buttons when appropriate
+ observe({
+ I("toggle edit buttons")
+ if (is.null(node_list_selected_node()) || !length(rvn$nodes)) {
+ # no node selected, can only add a new node
+ shinyjs::hide("node_list_node_delete")
+ } else {
+ # can now delete any selected node
+ shinyjs::show("node_list_node_delete")
+ }
+ })
+
+ # Action: delete node
+ observeEvent(input$node_list_node_delete, {
+ # Remove node
+ node_to_delete <- node_list_selected_node()
+ rvn$nodes[[node_to_delete]] <- NULL
+
+ # Remove any edges
+ edges_with_node <- rve$edges %>%
+ purrr::keep(~ node_to_delete %in% c(.$from, .$to)) %>%
+ names()
+
+ if (length(edges_with_node)) rve$edges[edges_with_node] <- NULL
+
+ shinyjs::hide("node_list_node_name_container")
+ shinyjs::hide("node_list_node_delete")
+ })
+
+ # ---- Sketch - Help Text ----
+ output$node_list_helptext <- renderUI({
+ s_node <- node_list_selected_node()
+ no_nodes <- length(rvn$nodes) == 0
+ not_enough_nodes <- length(rvn$nodes) < 2
+ no_node_selected <- !no_nodes && is.null(s_node)
+ no_dag_nodes <- !no_nodes && length(nodes_in_dag(rvn$nodes)) == 0
+ not_enough_dag_nodes <- !no_dag_nodes && length(nodes_in_dag(rvn$nodes)) < 2
+ node_in_dag <- !no_dag_nodes && s_node %in% nodes_in_dag(rvn$nodes)
+
+ if (no_nodes) {
+ helpText(
+ "Use the", icon("plus"), "button above to add a node",
+ "to your shinyDAG workspace"
+ )
+ } else if (not_enough_nodes) {
+ helpText("Add another node to your shinyDAG workspace")
+ } else if (no_dag_nodes) {
+ helpText("Drag a node from the staging area into the DAG or click its label to edit")
+ } else if (not_enough_dag_nodes) {
+ helpText("Drag another node from the staging area into the DAG")
+ } else if (input$clickpad_click_action == "parent") {
+ helpText("Click on a node label to activate as causal node or to edit its label")
+ } else if (input$clickpad_click_action == "child") {
+ helpText(
+ "Click on a node label to draw or remove a causal arrow from",
+ tags$strong(node_name_from_hash(rvn$nodes, node_list_selected_node())),
+ "or click",
+ tags$strong(node_name_from_hash(rvn$nodes, node_list_selected_node())),
+ "again to deselect"
+ )
+ }
+ })
+
+ # ---- Sketch - Edge Help Text ----
+ req_nodes <- function() {
+ if (!length(rvn$nodes)) {
+ cat("\n No Nodes!")
+ edge_helptext("Please add a node to the DAG first.")
+ FALSE
+ } else TRUE
+ }
+
+ edge_helptext <- function(inner, tag = "div", class = "help-block text-danger alert-edge") {
+ edge_helptext_trigger(Sys.time())
+ edge_helptext_feedback(list(class = class, inner = inner, tag = tag))
+ }
+
+ edge_normal_help_html <- list(
+ inner = "Double-click on a node to set parent node. Single-click to set child node.",
+ class = "help-block",
+ tag = "p"
+ )
+ edge_helptext_trigger <- reactiveVal(Sys.time())
+ edge_helptext_feedback <- reactiveVal(NULL)
+
+ output$edge_list_helptext <- renderUI({
+ debug_input(isolate(edge_helptext_feedback()), "edge_helptext_feedback")
+
+ edge_helptext_trigger()
+
+ if (!is.null(isolate(edge_helptext_feedback()))) {
+ invalidateLater(4800)
+ }
+
+ html <- isolate(edge_helptext_feedback()) %||% edge_normal_help_html
+ edge_helptext_feedback(NULL)
+ tag(html$tag, list(class = html$class, html$inner))
+ })
+
+ # ---- Sketch - Clickpad ----
+ plotly_source_id <- paste0("clickpad_", session$token)
+ clickpad_new_locations <- callModule(
+ clickpad, "clickpad",
+ nodes = reactive(rvn$nodes),
+ edges = reactive(rve$edges),
+ plotly_source = plotly_source_id
+ )
+
+ observe({
+ req(clickpad_new_locations())
+
+ new <- clickpad_new_locations()
+ debug_input(new, "clickpad_new_locations()")
+
+ rvn$nodes <- node_update(rvn$nodes, new$hash, x = unname(new$x), y = unname(new$y))
+ })
+
+ # ---- Sketch - Clickpad - Click Events ----
+ observe({
+ I("clickpad click event handler")
+ clicked_annotation <- event_data(
+ "plotly_clickannotation", source = plotly_source_id, priority = "event"
+ )
+ req(clicked_annotation[["_input"]]$node_hash)
+
+ click_action = isolate(input$clickpad_click_action)
+ clicked_hash = clicked_annotation[["_input"]]$node_hash
+
+ nodes <- isolate(rvn$nodes)
+
+ s_node_parent <- node_parent(nodes)
+ s_node_child <- node_child(nodes)
+
+ if (click_action == "parent") {
+ # toggle clicked node as parent node
+ update_button <- nodes[[clicked_hash]]$x >= 0 &&
+ nodes %>% purrr::map_dbl("x") %>% { sum(. >= 0) > 1 }
+
+ if (is.null(s_node_parent)) {
+ nodes <- node_set_attribute(nodes, clicked_hash, "parent")
+ } else if (clicked_hash == s_node_parent) {
+ update_button <- FALSE
+ nodes <- node_unset_attribute(nodes, clicked_hash, c("parent", "child"))
+ } else {
+ nodes <- node_set_attribute(nodes, clicked_hash, "parent")
+ nodes <- node_unset_attribute(nodes, clicked_hash, "child")
+ }
+ if (update_button) updateRadioSwitchButtons("clickpad_click_action", "child")
+
+ } else if (click_action == "child") {
+ # toggle clicked node as child node
+ has_edge <- edge_exists(isolate(rve$edges), s_node_parent, s_node_child %||% clicked_hash)
+ has_reverse_edge <- edge_exists(isolate(rve$edges), s_node_child %||% clicked_hash, s_node_parent)
+
+ if (!is.null(s_node_parent) && s_node_parent == clicked_hash) {
+ # Can't add edges to self
+ rvn$nodes <- node_unset_attribute(nodes, names(nodes), c("parent", "child"))
+ updateRadioSwitchButtons("clickpad_click_action", "parent")
+ return()
+ } else if (has_edge) {
+ # Clicked on child node that already has edge, will be removing edge
+ nodes <- node_unset_attribute(nodes, clicked_hash, "child")
+ } else if (nodes[[clicked_hash]]$x < 0) {
+ showNotification(
+ "Edges can only be drawn between nodes that are in the DAG area.",
+ duration = 5,
+ type = "error"
+ )
+ return()
+ } else {
+ nodes <- node_set_attribute(nodes, clicked_hash, "child")
+ }
+
+ # Remove reverse edge if it exists
+ rv_edges <- isolate(rve$edges)
+ if (has_reverse_edge) {
+ rv_edges <- edge_toggle(rv_edges, clicked_hash, s_node_parent)
+ }
+ rve$edges <- edge_toggle(rv_edges, s_node_parent, clicked_hash)
+ }
+ rvn$nodes <- nodes
+ })
+
+ # ---- Sketch - Clickpad - Click Type Buttons ----
+ observe({
+ I("clickpad click action reset to select?")
+ reset_clickpad_action <- function() {
+ updateRadioSwitchButtons("clickpad_click_action", "parent")
+ invisible()
+ }
+
+ if (length(rvn$nodes) < 2) return(reset_clickpad_action())
+
+ dag_has_two_nodes <- rvn$nodes %>% purrr::map_dbl("x") %>% { sum(. >= 0) > 1 }
+ if (!dag_has_two_nodes) return(reset_clickpad_action())
+
+ if (!is.null(node_list_selected_node())) {
+ if (rvn$nodes[[node_list_selected_node()]]$x < 0) {
+ reset_clickpad_action()
+ }
+ }
+ })
+
+ # Don't allow clickpad edge adding unless node conditions are met
+ observeEvent(input$clickpad_click_action, {
+ req(input$clickpad_click_action == "child")
+ valid <- FALSE
+ if (length(rvn$nodes) < 2) {
+ showNotification("Please add at least 2 nodes to your DAG workspace first.", duration = 5)
+ } else if (rvn$nodes %>% purrr::keep(~ .$x >= 0) %>% length() < 2) {
+ showNotification("Please drag at least 2 nodes into the DAG area first.", duration = 5)
+ } else if (is.null(node_list_selected_node())) {
+ showNotification("A parent node must be selected first", duration = 5)
+ } else if (!length(nodes_in_dag(rvn$nodes))) {
+ showNotification(
+ "Please add a node to the DAG by dragging it out of the staging area.",
+ duration = 5
+ )
+ } else {
+ valid <- TRUE
+ }
+ if (!valid) updateRadioSwitchButtons("clickpad_click_action", "parent")
+ })
+
+ # ---- Sketch - Node Options ----
+ update_node_options <- function(
+ nodes,
+ inputId,
+ updateFn,
+ none_choice = TRUE,
+ ...
+ ) {
+ available_choices <- c("None" = "", node_names(nodes))
+ if (!none_choice) available_choices <- available_choices[-1]
+ s_choice <- intersect(isolate(input[[inputId]]), available_choices)
+ if (!length(s_choice) && none_choice) s_choice <- ""
+
+ updateFn(
+ session,
+ inputId,
+ choices = available_choices,
+ selected = s_choice,
+ ...
+ )
+ }
+
+ observe({
+ update_node_options(
+ rvn$nodes %>% purrr::keep(~ .$x >= 0),
+ "adjustNode",
+ updateSelectizeInput
+ )
+ update_node_options(
+ rvn$nodes %>% purrr::keep(~ .$x >= 0),
+ "exposureNode",
+ updateSelectInput
+ )
+ update_node_options(
+ rvn$nodes %>% purrr::keep(~ .$x >= 0),
+ "outcomeNode",
+ updateSelectInput
+ )
+ })
+
+ observeEvent(input$exposureNode, {
+ nodes <- isolate(rvn$nodes)
+ if (input$exposureNode == "") {
+ rvn$nodes <- node_unset_attribute(nodes, names(nodes), "exposure")
+ } else if (input$exposureNode == input$outcomeNode) {
+ updateSelectInput(session, "outcomeNode", selected = "")
+ rvn$nodes <- node_unset_attribute(nodes, names(nodes), "outcome")
+ } else {
+ rvn$nodes <- node_set_attribute(nodes, input$exposureNode, "exposure")
+ }
+ })
+
+ observeEvent(input$outcomeNode, {
+ nodes <- isolate(rvn$nodes)
+ if (input$outcomeNode == "") {
+ rvn$nodes <- node_unset_attribute(nodes, names(nodes), "outcome")
+ } else if (input$outcomeNode == input$exposureNode) {
+ updateSelectInput(session, "exposureNode", selected = "")
+ rvn$nodes <- node_unset_attribute(nodes, names(nodes), "exposure")
+ } else {
+ rvn$nodes <- node_set_attribute(nodes, input$outcomeNode, "outcome")
+ }
+ })
+
+ observeEvent(input$adjustNode, {
+ nodes <- isolate(rvn$nodes)
+ if (is.null(input$adjustNode)) return()
+ s_adjust <- input$adjustNode
+ rvn$nodes <- if (length(s_adjust) == 1 && s_adjust == "") {
+ node_unset_attribute(nodes, names(node), "adjusted")
+ } else {
+ node_set_attribute(nodes, s_adjust, "adjusted")
+ }
+ })
+
+ output$adjustText <- renderText({
+ if (is.null(input$exposureNode) & is.null(input$outcomeNode)) {
+ paste0("Minimal sufficient adjustment sets")
+ } else {
+ paste0(
+ "Minimal sufficient adjustment set(s) to estimate the effect of ",
+ input$exposureNode,
+ " on ",
+ input$outcomeNode
+ )
+ }
+ })
+
+ # ---- DAG - Functions ----
+ make_dagitty <- function(nodes, edges, exposure = NULL, outcome = NULL, adjusted = NULL) {
+ dagitty_edges <- edge_frame(edges, nodes) %>%
+ glue::glue_data('"{from_name}" -> "{to_name}"') %>%
+ paste(collapse = "; ")
+
+ dagitty_code <- glue::glue("dag {{ {dagitty_edges} }}")
+ debug_input(dagitty_code, "dagitty_code")
+
+ gdag <- dagitty(dagitty_code)
+
+ if (isTruthy(exposure)) exposures(gdag) <- node_name_from_hash(nodes, exposure)
+ if (isTruthy(outcome)) outcomes(gdag) <- node_name_from_hash(nodes, outcome)
+ if (isTruthy(adjusted)) adjustedNodes(gdag) <- node_name_from_hash(nodes, adjusted)
+
+ gdag
+ }
+
+ dagitty_open_paths <- function(nodes, edges, exposure, outcome, adjusted) {
+ node_names <- invertNames(node_names(nodes))
+ gd <- make_dagitty(
+ edges = edges, nodes = nodes,
+ exposure = exposure, outcome = outcome, adjusted = adjusted
+ )
+
+ exp_outcome_paths <- paths(
+ gd,
+ Z = adjusted %??% unname(node_names[adjusted])
+ )
+
+ exp_outcome_paths$paths[as.logical(exp_outcome_paths$open)]
+ }
+
+ dagitty_format_paths <- function(paths) {
+ HTML(paste0(
+ "",
+ paste(trimws(paths), collapse = "\n"),
+ "\n
"
+ ))
+ }
+
+ # ---- Sketch - DAG - Open Exp/Outcome Paths ----
+ dagitty_open_exp_outcome_paths <- reactive({
+ req(
+ length(nodes_in_dag(rvn$nodes)),
+ length(edges_in_dag(rve$edges, rvn$nodes))
+ )
+
+ # need both exposure and outcome node
+ requires_nodes <- c("Exposure" = input$exposureNode, "Outcome" = input$outcomeNode)
+ missing_nodes <- names(requires_nodes[grepl("^$", requires_nodes)])
+ validate(
+ need(
+ length(missing_nodes) == 0,
+ glue::glue("Please choose {str_and(missing_nodes)} {str_plural(missing_nodes, 'node')}")
+ )
+ )
+
+ purrr::safely(dagitty_open_paths)(
+ nodes = rvn$nodes, edges = rve$edges, exposure = input$exposureNode,
+ outcome = input$outcomeNode, adjusted = input$adjustNode
+ )
+ })
+
+
+ output$openExpOutcomePaths <- renderUI({
+ validate(need(length(edges_in_dag(rve$edges, rvn$nodes)) > 0, "Please add at least one edge"))
+
+ open_paths <- dagitty_open_exp_outcome_paths()
+
+ validate(need(
+ is.null(open_paths$error),
+ paste(
+ "There was an error building your graph. It may not be fully or",
+ "correctly specified. If you have special characters in your node",
+ "change the node name to something short and representative. You can",
+ "set more detailed node labels in the \"Tweak\" panel."
+ )
+ ), errorClass = " text-danger")
+
+ open_paths <- open_paths$result
+
+ if (length(open_paths)) {
+ tagList(
+ h5("Open associations between exposure and outcome"),
+ dagitty_format_paths(open_paths)
+ )
+ } else {
+ tagList(
+ helpText("No open associations between exposure and outcome.")
+ )
+ }
+ })
+
+ # ---- Tweak - Edge Aesthetics ----
+
+ # Create the edge aesthetics control UI, only updated when tab is activated
+ output$edge_aes_ui <- renderUI({
+ req(input$shinydag_page == "tweak")
+ req(length(isolate(rve$edges)) > 0)
+ rv_edge_frame <- edge_frame(isolate(rve$edges), isolate(rvn$nodes)) %>%
+ arrange(from_name, to_name)
+
+ tagList(
+ purrr:::pmap(rv_edge_frame, ui_edge_controls_row, input = input)
+ )
+ })
+
+ # Watch edge UI inputs and update rve$edges when inputs change
+ observe({
+ I("update edge aesthetics")
+ req(length(rve$edges) > 0, grepl("^angle__", names(input)))
+ rv_edges <- isolate(rve$edges)
+
+ edge_ui <- get_hashed_input_with_prefix(
+ input,
+ prefix = "angle|color|lty|lineT",
+ hash_sep = "__"
+ )
+
+ for (edge in edge_ui) {
+ if (!edge$hash %in% names(rv_edges)) next
+ this_edge <- edge[setdiff(names(edge), "hash")]
+ for (prop in names(this_edge)) {
+ if (is.na(this_edge[[prop]])) next
+ rv_edges[[edge$hash]][[prop]] <- this_edge[[prop]]
+ }
+ }
+ debug_input(bind_rows(rv_edges, .id = "hash"), "rve$edges after aes update")
+ rve$edges <- rv_edges
+ }, priority = -50)
+
+ # ---- Tweak - Node Aesthetics ----
+
+ # Create the node aesthetics control UI, only updated when tab is activated
+ output$node_aes_ui <- renderUI({
+ req(input$shinydag_page == "tweak")
+ req(length(isolate(rvn$nodes)) > 0)
+ rv_node_frame <- node_frame(isolate(rvn$nodes))
+
+ tagList(
+ purrr:::pmap(rv_node_frame, ui_node_controls_row, input = input)
+ )
+ })
+
+ # Watch edge UI inputs and update rve$edges when inputs change
+ observe({
+ I("update node aesthetics")
+ req(length(rvn$nodes) > 0, grepl("^color_fill_", names(input)))
+ rv_nodes <- isolate(rvn$nodes)
+
+ node_ui <- get_hashed_input_with_prefix(
+ input,
+ prefix = "name_latex|(color_(draw|fill|text))",
+ hash_sep = "__"
+ )
+
+ for (node in node_ui) {
+ if (!node$hash %in% names(rv_nodes)) next
+ this_node <- node[setdiff(names(node), "hash")]
+ for (prop in names(this_node)) {
+ if (is.na(this_node[[prop]])) next
+ rv_nodes[[node$hash]][[prop]] <- this_node[[prop]]
+ }
+ }
+ debug_input(bind_rows(rv_nodes, .id = "hash"), "rvn$nodes after aes update")
+ rvn$nodes <- rv_nodes
+ }, priority = -50)
+
+
+ # ---- Global - TikZ Code ----
+ edge_points_rv <- reactive({
+ req(length(rve$edges) > 0)
+ ep <- edge_points(rve$edges, rvn$nodes)
+ req(nrow(ep) > 0)
+ ep
+ })
+
+ dag_node_lines <- function(nodeFrame) {
+ dag_bounds <-
+ nodeFrame %>%
+ filter(!is.na(name)) %>%
+ summarize_at(vars(x, y), list(min = min, max = max))
+
+ nodeFrame <- nodeFrame %>%
+ filter(
+ between(x, dag_bounds$x_min, dag_bounds$x_max) &&
+ between(y, dag_bounds$y_min, dag_bounds$y_max)
+ )
+
+ nodeFrame[is.na(nodeFrame$tikz_node), "tikz_node"] <- "~"
+
+ nodeLines <- vector("character", 0)
+ for (i in unique(nodeFrame$y)) {
+ createLines <- paste0(
+ paste(nodeFrame[nodeFrame$y == i, ]$tikz_node, collapse = " & "),
+ " \\\\\n"
+ )
+ nodeLines <- c(nodeLines, createLines)
+ }
+ nodeLines <- rev(nodeLines)
+
+ paste0(
+ "\\matrix(m)[matrix of nodes, row sep=2.6em, column sep=2.8em,",
+ "text height=1.5ex, text depth=0.25ex]\n",
+ "{\n ", paste(nodeLines, collapse = " "), "};"
+ )
+ }
+
+ tikz_node_points <- reactive({
+ req(length(rvn$nodes))
+ update_tikz_because_global_opts()
+ node_df <- node_frame(rvn$nodes)
+ req(nrow(node_df) > 0)
+ node_df %>% node_frame_add_style()
+ })
+
+ tikz_code_from_app <- reactive({
+ d_tikz_node_points <- debounce(tikz_node_points, 1000)
+ nodePts <- d_tikz_node_points()
+ req(nrow(nodePts) > 0)
+
+ has_style <- any(!is.na(nodePts$tikz_style))
+ tikz_style_defs <- nodePts$tikz_style[!is.na(nodePts$tikz_style)]
+
+ styleZ <- paste(
+ "\\tikzset{",
+ paste0(" every node/.style={ }", if (has_style) "," else "\n}"),
+ if (has_style) paste(" ", tikz_style_defs, collapse = ",\n"),
+ if (has_style) "}",
+ sep = "\n"
+ )
+ startZ <- "\\begin{tikzpicture}[>=latex]"
+ endZ <- "\\end{tikzpicture}"
+ pathZ <- "\\path[->,font=\\scriptsize,>=angle 90]"
+
+ d_x <- min(nodePts$x) - 1L
+ d_y <- min(nodePts$y) - 1L
+
+ nodePts$x <- nodePts$x - d_x
+ nodePts$y <- nodePts$y - d_y
+
+ y_max <- max(nodePts$y)
+
+ nodeLines <- nodePts %>%
+ tidyr::complete(
+ x = seq(min(nodePts$x), max(nodePts$x)),
+ y = seq(min(nodePts$y), max(nodePts$y))
+ ) %>%
+ dag_node_lines()
+
+ edgeLines <- character()
+
+ if (length(edges_in_dag(rve$edges, isolate(rvn$nodes)))) {
+ # edge_points_rv() is a reactive that gathers values from aesthetics UI
+ # but it can be noisy, so we're debouncing to delay TeX rendering until values are constant
+ edgePts <- debounce(edge_points_rv, 5000)()
+
+ tikz_point <- function(x, y, d_x, d_y, y_max) {
+ glue::glue("(m-{y_max - (y - d_y) + 1}-{x - d_x})")
+ }
+
+ edgePts <- edgePts %>%
+ mutate(
+ parent = tikz_point(from.x, from.y, d_x, d_y, y_max),
+ child = tikz_point(to.x, to.y, d_x, d_y, y_max),
+ edgeLine = glue::glue(
+ "{parent} edge [>={input$arrowShape}, bend left = {edgePts$angle}, ",
+ "color = {edgePts$color},{edgePts$lineT},{edgePts$lty}] node[auto] {{$~$}} {child}"
+ )
+ )
+
+ debug_input(select(edgePts, hash, matches("^(from|to)_name"), parent, child, edgeLine), "edgeLines")
+ edgeLines <- edgePts$edgeLine
+ }
+
+ edgeLines <- paste0(pathZ, paste(edgeLines, collapse = ""), ";")
+
+ paste(c(styleZ, startZ, nodeLines, edgeLines, endZ), collapse = "\n")
+ })
+
+ make_graph <- function(nodes, edges) {
+ g <- make_empty_graph()
+ if (nrow(node_frame(nodes))) {
+ g <- g + node_vertices(nodes)
+ }
+ if (length(edges)) {
+ # Add edges
+ g <- g + edge_edges(edges, nodes)
+ }
+ g
+ }
+
+ # ---- Tweak - Global Options ----
+ update_tikz_because_global_opts <- reactiveVal(FALSE)
+
+ observe({
+ I("update tex_opts")
+ `%|%` <- function(x, y) {
+ x <- x %||% y
+ if (is.na(x)) y else x
+ }
+ tex_opts$set(list(
+ density = 1200,
+ margin = list(
+ left = input$tex_opts_margin_left %|% 0,
+ top = input$tex_opts_margin_bottom %|% 0, # bug?
+ right = input$tex_opts_margin_right %|% 0,
+ bottom = input$tex_opts_margin_top %|% 0
+ ),
+ cleanup = c("aux", "log")
+ ))
+ update_tikz_because_global_opts(!isolate(update_tikz_because_global_opts()))
+ })
+
+ # ---- Tweak - dagitty DAG ----
+ dag_dagitty <- reactive({
+ req(
+ tweak_preview_visible(),
+ length(nodes_in_dag(rvn$nodes)),
+ length(edges_in_dag(rve$edges)),
+ input$exposureNode, input$outcomeNode, input$adjustNode
+ )
+ make_dagitty(rvn$nodes, rve$edges, input$exposureNode, input$outcomeNode, input$adjustNode)
+ })
+
+ dag_tidy <- reactive({
+ req(
+ tweak_preview_visible(),
+ length(nodes_in_dag(rvn$nodes)),
+ length(edges_in_dag(rve$edges)),
+ input$exposureNode, input$outcomeNode, input$adjustNode
+ )
+ make_dagitty(rvn$nodes, rve$edges, input$exposureNode, input$outcomeNode, input$adjustNode) %>%
+ tidy_dagitty()
+ })
+
+ # ---- Tweak - Preview ----
+ tweak_preview_visible <- callModule(
+ module = dagPreview,
+ id = "tweak_preview",
+ session_dir = SESSION_TEMPDIR,
+ tikz_code = reactive({
+ req(input$shinydag_page == "tweak")
+ tikz_code_from_app()
+ }),
+ dag_dagitty,
+ dag_tidy,
+ has_edges = reactive(nrow(edge_frame(rve$edges, rvn$nodes)))
+ )
+
+ # ---- LaTeX - Editor ----
+ output$texEdit <- renderUI({
+ tikz_lines <- tikz_code_from_app()
+
+ if (is.null(tikz_lines)) {
+ tikz_lines <- "\\\\begin{tikzpicture}[>=latex]\n\\\\end{tikzpicture}"
+ } else {
+ # double escape backslashes
+ tikz_lines <- gsub("\\", "\\\\", tikz_lines, fixed = TRUE)
+ }
+ aceEditor(
+ "manual_tikz",
+ mode = "latex",
+ value = paste(tikz_lines, collapse = "\n"),
+ theme = "chrome",
+ wordWrap = TRUE,
+ highlightActiveLine = TRUE
+ )
+ })
+
+ latex_preview_visible <- callModule(
+ module = dagPreview,
+ id = "latex_preview",
+ session_dir = SESSION_TEMPDIR,
+ reactive({
+ req(input$shinydag_page == "latex")
+ input$manual_tikz
+ })
+ )
+
+ # ---- About - Examples ----
+ example_value <- callModule(examples, "example")
+
+ observe({
+ req(example_value())
+
+ ex_val <- example_value()
+ rvn$nodes <- ex_val$nodes
+ rve$edges <- ex_val$edges
+
+ Sys.sleep(0.25)
+ shinydashboard::updateTabItems(session, "shinydag_page", "sketch")
+
+ })
+
+}
diff --git a/shinydag.Rproj b/shinydag.Rproj
new file mode 100644
index 0000000..efd491b
--- /dev/null
+++ b/shinydag.Rproj
@@ -0,0 +1,13 @@
+Version: 1.0
+
+RestoreWorkspace: No
+SaveWorkspace: No
+AlwaysSaveHistory: Default
+
+EnableCodeIndexing: Yes
+UseSpacesForTab: Yes
+NumSpacesForTab: 2
+Encoding: UTF-8
+
+RnwWeave: Sweave
+LaTeX: pdfLaTeX
diff --git a/ui.R b/ui.R
new file mode 100644
index 0000000..15a02b5
--- /dev/null
+++ b/ui.R
@@ -0,0 +1,357 @@
+
+# Components --------------------------------------------------------------
+
+components <- list(toolbar = list())
+
+# Components - Clickpad ----
+components$toolbar$clickpad_action <- tags$div(
+ radioSwitchButtons(
+ "clickpad_click_action",
+ HTML(paste(icon("mouse-pointer"), "Click a node to...")),
+ choices = c("Select" = "parent", "Draw/Remove Edge" = "child"),
+ selected = "parent",
+ selected_background = "#D3751C"
+ )
+)
+
+# Components - Node List ----
+components$toolbar$node_list_actions <- tags$div(
+ class = "btn-toolbar",
+ role = "toolbar",
+ tags$form(
+ class = "form-inline",
+ tags$div(
+ class = "form-group",
+ tags$div(
+ class = "btn-group",
+ role = "group",
+ actionButton(
+ input = "node_list_node_add",
+ label = "Add New Node",
+ icon = icon("plus"),
+ alt = "Add New Node to Workspace",
+ `data-toggle` = "tooltip",
+ `data-placement` = "bottom",
+ title = "Add New Node to Workspace"
+ ),
+ shinyjs::hidden(
+ actionButton(
+ "node_list_node_delete", "Delete Node", icon("trash"),
+ alt = "Delete Node",
+ `data-toggle` = "tooltip",
+ `data-placement` = "bottom",
+ title = "Delete Node")
+ )
+ )
+ )
+ )
+)
+
+components$toolbar$node_list_name <- tags$div(
+ style = "padding-top: 15px; min-height: 90px;",
+ shinyjs::hidden(
+ tags$div(
+ id = "node_list_node_name_container",
+ class = "col-xs-12",
+ textInput("node_list_node_name", "Node Name", width = "100%")
+ )
+ )
+)
+
+# Components - About ----
+components$about <- list()
+
+components$about$gerkelab <- tagList(
+ h3("Development Team"),
+ tags$ul(
+ tags$li("Jordan Creed"),
+ tags$li(tags$a(href = "https://www.garrickadebuie.com", "Garrick Aden-Buie")),
+ tags$li(tags$a(href = "https://travisgerke.com", "Travis Gerke"))
+ ),
+ p(
+ "For more information about our lab and other projects please check",
+ "out our website at",
+ tags$a(href = "http://gerkelab.com", "gerkelab.com")
+ ),
+ p(
+ "All code and detailed instructions for usage is available on GitHub at",
+ tags$a(
+ href = "https://github.com/GerkeLab/shinyDAG",
+ "GerkeLab/shinyDag"
+ )
+ ),
+ p(
+ "If you have any questions or comments, we would love to hear them.",
+ "You can email us at",
+ tags$a(href = "mailto:travis.gerke@moffitt.org", "travis.gerke@moffitt.org"),
+ "or",
+ HTML(paste0(
+ tags$a(href = "mailto:jordan.h.creed@moffitt.org", "jordan.h.creed@moffitt.org"),
+ "."
+ )),
+ "Or feel free to",
+ tags$a(
+ href = "https://github.com/GerkeLab/shinyDAG/issues",
+ "open an issue"
+ ),
+ "in our GitHub repository."
+ )
+)
+
+# components$about$usage <- tagList(
+# tags$h3("Using shinyDAG"),
+# tags$p(
+# "For more details on using shinyDAG please check out our",
+# tags$a(
+# href = "https://github.com/GerkeLab/shinyDAG/blob/master/README.md",
+# "README."
+# ))
+# )
+
+# Components - Build ----
+components$build <- box(
+ title = "Build",
+ id = "build-box",
+ width = 12,
+ fluidRow(
+ id = "shinydag-toolbar",
+ tags$div(
+ class = "col-xs-12 col-md-5 shinydag-toolbar-actions",
+ tags$div(
+ class = "col-xs-12 col-sm-6 col-md-12",
+ id = "shinydag-toolbar-node-list-action",
+ components$toolbar$node_list_action
+ ),
+ tags$div(
+ class = "col-xs-12 col-sm-6 col-md-12",
+ style = "padding: 10px",
+ id = "shinydag-toolbar-clickpad-action",
+ components$toolbar$clickpad_action
+ )
+ ),
+ tags$div(
+ class = "col-xs-12 col-md-7",
+ components$toolbar$node_list_name
+ )
+ ),
+ fluidRow(
+ column(
+ width = 12,
+ tags$div(
+ class = "pull-left",
+ uiOutput("node_list_helptext")
+ ),
+ shinyThings::undoHistoryUI(
+ id = "undo_rv",
+ class = "pull-right",
+ back_text = "Undo",
+ fwd_text = "Redo"
+ )
+ )
+ ),
+ fluidRow(
+ column(
+ width = 12,
+ clickpad_UI("clickpad", height = "600px", width = "100%")
+ )
+ ),
+ if (getOption("shinydag.debug", FALSE)) fluidRow(
+ column(width = 12, shinyThings::undoHistoryUI_debug("undo_rv"))
+ ),
+ fluidRow(
+ tags$div(
+ class = class_3_col,
+ selectInput("exposureNode", "Exposure", choices = c("None" = ""), width = "100%")
+ ),
+ tags$div(
+ class = class_3_col,
+ selectInput("outcomeNode", "Outcome", choices = c("None" = ""), width = "100%")
+ ),
+ tags$div(
+ class = class_3_col,
+ selectizeInput("adjustNode", "Adjust for...", choices = c("None" = ""), width = "100%", multiple = TRUE)
+ )
+ ),
+ fluidRow(
+ tags$div(
+ class = "col-sm-12",
+ uiOutput("openExpOutcomePaths")
+ )
+ )
+)
+
+# Components - LaTeX ----
+components$latex <- tagList(
+ tags$p(
+ "Use this tab to manually edit the TikZ generated by shinyDAG."
+ ),
+ helpText(
+ "Note that changes made to the TikZ code below will not affect",
+ "the DAG settings in the app. Changes made to the DAG elsewhere",
+ "in shinyDAG will overwrite any changes made to the manually",
+ "edited TikZ code below."
+ ),
+ uiOutput("texEdit")
+)
+
+# Components - Tweak ----
+components$tweak <- tabBox(
+ title = "Edit DAG",
+ id = "tab_control",
+ # ---- Tab: Edit Aesthetics
+ tabPanel(
+ "Edges",
+ value = "edit_edge_aesthetics",
+ selectInput(
+ "arrowShape",
+ "Select arrow head",
+ choices = c(
+ "stealth",
+ "stealth'",
+ "diamond",
+ "triangle 90",
+ "hooks",
+ "triangle 45",
+ "triangle 60",
+ "hooks reversed",
+ "*"
+ ),
+ selected = "stealth"
+ ),
+ uiOutput("edge_aes_ui")
+ ),
+ tabPanel(
+ "Nodes",
+ value = "edit_node_aesthetics",
+ uiOutput("node_aes_ui")
+ ),
+ tabPanel(
+ "Page",
+ value = "edit_page_aesthetics",
+ tags$h3("Margins"),
+ fluidRow(
+ col_4(
+ numericInput("tex_opts_margin_top", "Top", value = 0L, min = 0L, max = 500L, step = 1L)
+ ),
+ col_4(
+ numericInput("tex_opts_margin_right", "Right", value = 0L, min = 0L, max = 500L, step = 1L)
+ ),
+ col_4(
+ numericInput("tex_opts_margin_bottom", "Bottom", value = 0L, min = 0L, max = 500L, step = 1L)
+ ),
+ col_4(
+ numericInput("tex_opts_margin_left", "Left", value = 0L, min = 0L, max = 500L, step = 1L)
+ )
+ )
+ )
+)
+
+# UI - shinyDAG -----------------------------------------------------------
+
+function(request) {
+ dashboardPage(
+ title = "shinyDAG",
+ skin = "black",
+ dashboardHeader(
+ title = "shinyDAG",
+ tags$li(
+ class = "dropdown",
+ actionLink(
+ inputId = "._bookmark_",
+ label = "Bookmark",
+ icon = icon("link", lib = "glyphicon"),
+ title = "Bookmark shinyDAG's state and get a URL for sharing.",
+ `data-toggle` = "tooltip",
+ `data-placement` = "bottom"
+ )
+ ),
+ tags$li(
+ class = "dropdown",
+ tags$a(
+ href = "https://github.com/gerkelab/shinyDAG/",
+ title = "shinyDAG on GitHub",
+ target = "_blank",
+ icon("github")
+ )
+ ),
+ tags$li(
+ class = "dropdown",
+ tags$a(
+ href = "https://gerkelab.com/project/shinyDAG/",
+ title = "GerkeLab Project Page",
+ target = "_blank",
+ icon("flask")
+ )
+ )
+ ),
+ dashboardSidebar(
+ sidebarMenu(
+ id = "shinydag_page",
+ menuItem("Sketch", tabName = "sketch", icon = icon("share-alt")),
+ menuItem("Tweak", tabName = "tweak", icon = icon("sliders")),
+ menuItem("LaTeX", tabName = "latex", icon = icon("file-text-o")),
+ menuItem("About", tabName = "about", icon = icon("info"))
+ )
+ ),
+ dashboardBody(
+ shinyjs::useShinyjs(),
+ tags$script(src = "shinydag.js", async = TRUE),
+ includeCSS("www/AdminLTE.gerkelab.min.css"),
+ includeCSS("www/_all-skins.gerkelab.min.css"),
+ includeCSS("www/shinydag.css"),
+ chooseSliderSkin("Flat", "#418c7a"),
+ tags$a(
+ href = "https://gerkelab.com",
+ target = "_blank",
+ tags$div(class = "gerkelab-logo")
+ ),
+ tabItems(
+ tabItem(
+ tabName = "sketch",
+ components$build
+ ),
+ tabItem(
+ tabName = "tweak",
+ two_column_flips_on_mobile(
+ components$tweak,
+ box(
+ title = "Preview DAG",
+ dagPreviewUI("tweak_preview", include_graph_downloads = TRUE)
+ )
+ )
+ ),
+ tabItem(
+ tabName = "latex",
+ two_column_flips_on_mobile(
+ box(
+ title = "Edit LaTeX",
+ components$latex
+ ),
+ box(
+ title = "Preview LaTeX",
+ dagPreviewUI("latex_preview", include_graph_downloads = FALSE)
+ )
+ )
+ ),
+ tabItem(
+ tabName = "about",
+ box(
+ title = "Examples",
+ width = "12 col-md-6",
+ examples_UI("example")
+ ),
+ box(
+ title = "About shinyDAG",
+ width = "12 col-md-6",
+ components$about$gerkelab
+ )#,
+ # box(
+ # title = "About shinyDAG",
+ # width = "12 col-md-6",
+ # components$about$usage
+ # )
+ )
+ )
+ )
+ )
+}
diff --git a/www/AdminLTE.gerkelab.min.css b/www/AdminLTE.gerkelab.min.css
new file mode 100644
index 0000000..5dc6349
--- /dev/null
+++ b/www/AdminLTE.gerkelab.min.css
@@ -0,0 +1,7 @@
+@import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,300italic,400italic,600italic);/*!
+ * AdminLTE v2.3.8
+ * Author: Almsaeed Studio
+ * Website: Almsaeed Studio
+ * License: Open source - MIT
+ * Please visit http://opensource.org/licenses/MIT for more information
+!*/html,body{height:100%}.layout-boxed html,.layout-boxed body{height:100%}body{font-family:'Source Sans Pro','Helvetica Neue',Helvetica,Arial,sans-serif;font-weight:400;overflow-x:hidden;overflow-y:auto}.wrapper{height:100%;position:relative;overflow-x:hidden;overflow-y:auto}.wrapper:before,.wrapper:after{content:" ";display:table}.wrapper:after{clear:both}.layout-boxed .wrapper{max-width:1250px;margin:0 auto;min-height:100%;box-shadow:0 0 8px rgba(0,0,0,0.5);position:relative}.layout-boxed{background:url('../img/boxed-bg.jpg') repeat fixed}.content-wrapper,.right-side,.main-footer{-webkit-transition:-webkit-transform .3s ease-in-out,margin .3s ease-in-out;-moz-transition:-moz-transform .3s ease-in-out,margin .3s ease-in-out;-o-transition:-o-transform .3s ease-in-out,margin .3s ease-in-out;transition:transform .3s ease-in-out,margin .3s ease-in-out;margin-left:230px;z-index:820}.layout-top-nav .content-wrapper,.layout-top-nav .right-side,.layout-top-nav .main-footer{margin-left:0}@media (max-width:767px){.content-wrapper,.right-side,.main-footer{margin-left:0}}@media (min-width:768px){.sidebar-collapse .content-wrapper,.sidebar-collapse .right-side,.sidebar-collapse .main-footer{margin-left:0}}@media (max-width:767px){.sidebar-open .content-wrapper,.sidebar-open .right-side,.sidebar-open .main-footer{-webkit-transform:translate(230px, 0);-ms-transform:translate(230px, 0);-o-transform:translate(230px, 0);transform:translate(230px, 0)}}.content-wrapper,.right-side{min-height:100%;background-color:#f8f8f8;z-index:800}.main-footer{background:#fff;padding:15px;color:#444;border-top:1px solid #eee}.fixed .main-header,.fixed .main-sidebar,.fixed .left-side{position:fixed}.fixed .main-header{top:0;right:0;left:0}.fixed .content-wrapper,.fixed .right-side{padding-top:50px}@media (max-width:767px){.fixed .content-wrapper,.fixed .right-side{padding-top:100px}}.fixed.layout-boxed .wrapper{max-width:100%}body.hold-transition .content-wrapper,body.hold-transition .right-side,body.hold-transition .main-footer,body.hold-transition .main-sidebar,body.hold-transition .left-side,body.hold-transition .main-header .navbar,body.hold-transition .main-header .logo{-webkit-transition:none;-o-transition:none;transition:none}.content{min-height:250px;padding:15px;margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:'Source Sans Pro',sans-serif}a{color:#001f3f}a:hover,a:active,a:focus{outline:none;text-decoration:none;color:#00458c}.page-header{margin:10px 0 20px 0;font-size:22px}.page-header>small{color:#666;display:block;margin-top:5px}.main-header{position:relative;max-height:100px;z-index:1030}.main-header .navbar{-webkit-transition:margin-left .3s ease-in-out;-o-transition:margin-left .3s ease-in-out;transition:margin-left .3s ease-in-out;margin-bottom:0;margin-left:230px;border:none;min-height:50px;border-radius:0}.layout-top-nav .main-header .navbar{margin-left:0}.main-header #navbar-search-input.form-control{background:rgba(255,255,255,0.2);border-color:transparent}.main-header #navbar-search-input.form-control:focus,.main-header #navbar-search-input.form-control:active{border-color:rgba(0,0,0,0.1);background:rgba(255,255,255,0.9)}.main-header #navbar-search-input.form-control::-moz-placeholder{color:#ccc;opacity:1}.main-header #navbar-search-input.form-control:-ms-input-placeholder{color:#ccc}.main-header #navbar-search-input.form-control::-webkit-input-placeholder{color:#ccc}.main-header .navbar-custom-menu,.main-header .navbar-right{float:right}@media (max-width:991px){.main-header .navbar-custom-menu a,.main-header .navbar-right a{color:inherit;background:transparent}}@media (max-width:767px){.main-header .navbar-right{float:none}.navbar-collapse .main-header .navbar-right{margin:7.5px -15px}.main-header .navbar-right>li{color:inherit;border:0}}.main-header .sidebar-toggle{float:left;background-color:transparent;background-image:none;padding:15px 15px;font-family:fontAwesome,'Font Awesome 5 Free';font-weight:900}.main-header .sidebar-toggle:before{content:"\f0c9"}.main-header .sidebar-toggle:hover{color:#fff}.main-header .sidebar-toggle:focus,.main-header .sidebar-toggle:active{background:transparent}.main-header .sidebar-toggle .icon-bar{display:none}.main-header .navbar .nav>li.user>a>.fa,.main-header .navbar .nav>li.user>a>.glyphicon,.main-header .navbar .nav>li.user>a>.ion{margin-right:5px}.main-header .navbar .nav>li>a>.label{position:absolute;top:9px;right:7px;text-align:center;font-size:9px;padding:2px 3px;line-height:.9}.main-header .logo{-webkit-transition:width .3s ease-in-out;-o-transition:width .3s ease-in-out;transition:width .3s ease-in-out;display:block;float:left;height:50px;font-size:20px;line-height:50px;text-align:center;width:230px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;padding:0 15px;font-weight:300;overflow:hidden}.main-header .logo .logo-lg{display:block}.main-header .logo .logo-mini{display:none}.main-header .navbar-brand{color:#fff}.content-header{position:relative;padding:15px 15px 0 15px}.content-header>h1{margin:0;font-size:24px}.content-header>h1>small{font-size:15px;display:inline-block;padding-left:4px;font-weight:300}.content-header>.breadcrumb{float:right;background:transparent;margin-top:0;margin-bottom:0;font-size:12px;padding:7px 5px;position:absolute;top:15px;right:10px;border-radius:2px}.content-header>.breadcrumb>li>a{color:#444;text-decoration:none;display:inline-block}.content-header>.breadcrumb>li>a>.fa,.content-header>.breadcrumb>li>a>.glyphicon,.content-header>.breadcrumb>li>a>.ion{margin-right:5px}.content-header>.breadcrumb>li+li:before{content:'>\00a0'}@media (max-width:991px){.content-header>.breadcrumb{position:relative;margin-top:5px;top:0;right:0;float:none;background:#eee;padding-left:10px}.content-header>.breadcrumb li:before{color:#bbb}}.navbar-toggle{color:#fff;border:0;margin:0;padding:15px 15px}@media (max-width:991px){.navbar-custom-menu .navbar-nav>li{float:left}.navbar-custom-menu .navbar-nav{margin:0;float:left}.navbar-custom-menu .navbar-nav>li>a{padding-top:15px;padding-bottom:15px;line-height:20px}}@media (max-width:767px){.main-header{position:relative}.main-header .logo,.main-header .navbar{width:100%;float:none}.main-header .navbar{margin:0}.main-header .navbar-custom-menu{float:right}}@media (max-width:991px){.navbar-collapse.pull-left{float:none !important}.navbar-collapse.pull-left+.navbar-custom-menu{display:block;position:absolute;top:0;right:40px}}.main-sidebar,.left-side{position:absolute;top:0;left:0;padding-top:50px;min-height:100%;width:230px;z-index:810;-webkit-transition:-webkit-transform .3s ease-in-out,width .3s ease-in-out;-moz-transition:-moz-transform .3s ease-in-out,width .3s ease-in-out;-o-transition:-o-transform .3s ease-in-out,width .3s ease-in-out;transition:transform .3s ease-in-out,width .3s ease-in-out}@media (max-width:767px){.main-sidebar,.left-side{padding-top:100px}}@media (max-width:767px){.main-sidebar,.left-side{-webkit-transform:translate(-230px, 0);-ms-transform:translate(-230px, 0);-o-transform:translate(-230px, 0);transform:translate(-230px, 0)}}@media (min-width:768px){.sidebar-collapse .main-sidebar,.sidebar-collapse .left-side{-webkit-transform:translate(-230px, 0);-ms-transform:translate(-230px, 0);-o-transform:translate(-230px, 0);transform:translate(-230px, 0)}}@media (max-width:767px){.sidebar-open .main-sidebar,.sidebar-open .left-side{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}}.sidebar{padding-bottom:10px}.sidebar-form input:focus{border-color:transparent}.user-panel{position:relative;width:100%;padding:10px;overflow:hidden}.user-panel:before,.user-panel:after{content:" ";display:table}.user-panel:after{clear:both}.user-panel>.image>img{width:100%;max-width:45px;height:auto}.user-panel>.info{padding:5px 5px 5px 15px;line-height:1;position:absolute;left:55px}.user-panel>.info>p{font-weight:600;margin-bottom:9px}.user-panel>.info>a{text-decoration:none;padding-right:5px;margin-top:3px;font-size:11px}.user-panel>.info>a>.fa,.user-panel>.info>a>.ion,.user-panel>.info>a>.glyphicon{margin-right:3px}.sidebar-menu{list-style:none;margin:0;padding:0}.sidebar-menu>li{position:relative;margin:0;padding:0}.sidebar-menu>li>a{padding:12px 5px 12px 15px;display:block}.sidebar-menu>li>a>.fa,.sidebar-menu>li>a>.glyphicon,.sidebar-menu>li>a>.ion{width:20px}.sidebar-menu>li .label,.sidebar-menu>li .badge{margin-right:5px}.sidebar-menu>li .badge{margin-top:3px}.sidebar-menu li.header{padding:10px 25px 10px 15px;font-size:12px}.sidebar-menu li>a>.fa-angle-left,.sidebar-menu li>a>.pull-right-container>.fa-angle-left{width:auto;height:auto;padding:0;margin-right:10px}.sidebar-menu li>a>.fa-angle-left{position:absolute;top:50%;right:10px;margin-top:-8px}.sidebar-menu li.active>a>.fa-angle-left,.sidebar-menu li.active>a>.pull-right-container>.fa-angle-left{-webkit-transform:rotate(-90deg);-ms-transform:rotate(-90deg);-o-transform:rotate(-90deg);transform:rotate(-90deg)}.sidebar-menu li.active>.treeview-menu{display:block}.sidebar-menu .treeview-menu{display:none;list-style:none;padding:0;margin:0;padding-left:5px}.sidebar-menu .treeview-menu .treeview-menu{padding-left:20px}.sidebar-menu .treeview-menu>li{margin:0}.sidebar-menu .treeview-menu>li>a{padding:5px 5px 5px 15px;display:block;font-size:14px}.sidebar-menu .treeview-menu>li>a>.fa,.sidebar-menu .treeview-menu>li>a>.glyphicon,.sidebar-menu .treeview-menu>li>a>.ion{width:20px}.sidebar-menu .treeview-menu>li>a>.pull-right-container>.fa-angle-left,.sidebar-menu .treeview-menu>li>a>.pull-right-container>.fa-angle-down,.sidebar-menu .treeview-menu>li>a>.fa-angle-left,.sidebar-menu .treeview-menu>li>a>.fa-angle-down{width:auto}@media (min-width:768px){.sidebar-mini.sidebar-collapse .content-wrapper,.sidebar-mini.sidebar-collapse .right-side,.sidebar-mini.sidebar-collapse .main-footer{margin-left:50px !important;z-index:840}.sidebar-mini.sidebar-collapse .main-sidebar{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0);width:50px !important;z-index:850}.sidebar-mini.sidebar-collapse .sidebar-menu>li{position:relative}.sidebar-mini.sidebar-collapse .sidebar-menu>li>a{margin-right:0}.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>span{border-top-right-radius:4px}.sidebar-mini.sidebar-collapse .sidebar-menu>li:not(.treeview)>a>span{border-bottom-right-radius:4px}.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu{padding-top:5px;padding-bottom:5px;border-bottom-right-radius:4px}.sidebar-mini.sidebar-collapse .sidebar-menu>li:hover>a>span:not(.pull-right),.sidebar-mini.sidebar-collapse .sidebar-menu>li:hover>.treeview-menu{display:block !important;position:absolute;width:180px;left:50px}.sidebar-mini.sidebar-collapse .sidebar-menu>li:hover>a>span{top:0;margin-left:-3px;padding:12px 5px 12px 20px;background-color:inherit}.sidebar-mini.sidebar-collapse .sidebar-menu>li:hover>a>.pull-right-container{position:relative!important;float:right;width:auto!important;left:180px !important;top:-22px !important;z-index:900}.sidebar-mini.sidebar-collapse .sidebar-menu>li:hover>a>.pull-right-container>.label:not(:first-of-type){display:none}.sidebar-mini.sidebar-collapse .sidebar-menu>li:hover>.treeview-menu{top:44px;margin-left:0}.sidebar-mini.sidebar-collapse .main-sidebar .user-panel>.info,.sidebar-mini.sidebar-collapse .sidebar-form,.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>span,.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu,.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>.pull-right,.sidebar-mini.sidebar-collapse .sidebar-menu li.header{display:none !important;-webkit-transform:translateZ(0)}.sidebar-mini.sidebar-collapse .main-header .logo{width:50px}.sidebar-mini.sidebar-collapse .main-header .logo>.logo-mini{display:block;margin-left:-15px;margin-right:-15px;font-size:18px}.sidebar-mini.sidebar-collapse .main-header .logo>.logo-lg{display:none}.sidebar-mini.sidebar-collapse .main-header .navbar{margin-left:50px}}.sidebar-menu,.main-sidebar .user-panel,.sidebar-menu>li.header{white-space:nowrap;overflow:hidden}.sidebar-menu:hover{overflow:visible}.sidebar-form,.sidebar-menu>li.header{overflow:hidden;text-overflow:clip}.sidebar-menu li>a{position:relative}.sidebar-menu li>a>.pull-right-container{position:absolute;right:10px;top:50%;margin-top:-7px}.control-sidebar-bg{position:fixed;z-index:1000;bottom:0}.control-sidebar-bg,.control-sidebar{top:0;right:-230px;width:230px;-webkit-transition:right .3s ease-in-out;-o-transition:right .3s ease-in-out;transition:right .3s ease-in-out}.control-sidebar{position:absolute;padding-top:50px;z-index:1010}@media (max-width:768px){.control-sidebar{padding-top:100px}}.control-sidebar>.tab-content{padding:10px 15px}.control-sidebar.control-sidebar-open,.control-sidebar.control-sidebar-open+.control-sidebar-bg{right:0}.control-sidebar-open .control-sidebar-bg,.control-sidebar-open .control-sidebar{right:0}@media (min-width:768px){.control-sidebar-open .content-wrapper,.control-sidebar-open .right-side,.control-sidebar-open .main-footer{margin-right:230px}}.nav-tabs.control-sidebar-tabs>li:first-of-type>a,.nav-tabs.control-sidebar-tabs>li:first-of-type>a:hover,.nav-tabs.control-sidebar-tabs>li:first-of-type>a:focus{border-left-width:0}.nav-tabs.control-sidebar-tabs>li>a{border-radius:0}.nav-tabs.control-sidebar-tabs>li>a,.nav-tabs.control-sidebar-tabs>li>a:hover{border-top:none;border-right:none;border-left:1px solid transparent;border-bottom:1px solid transparent}.nav-tabs.control-sidebar-tabs>li>a .icon{font-size:16px}.nav-tabs.control-sidebar-tabs>li.active>a,.nav-tabs.control-sidebar-tabs>li.active>a:hover,.nav-tabs.control-sidebar-tabs>li.active>a:focus,.nav-tabs.control-sidebar-tabs>li.active>a:active{border-top:none;border-right:none;border-bottom:none}@media (max-width:768px){.nav-tabs.control-sidebar-tabs{display:table}.nav-tabs.control-sidebar-tabs>li{display:table-cell}}.control-sidebar-heading{font-weight:400;font-size:16px;padding:10px 0;margin-bottom:10px}.control-sidebar-subheading{display:block;font-weight:400;font-size:14px}.control-sidebar-menu{list-style:none;padding:0;margin:0 -15px}.control-sidebar-menu>li>a{display:block;padding:10px 15px}.control-sidebar-menu>li>a:before,.control-sidebar-menu>li>a:after{content:" ";display:table}.control-sidebar-menu>li>a:after{clear:both}.control-sidebar-menu>li>a>.control-sidebar-subheading{margin-top:0}.control-sidebar-menu .menu-icon{float:left;width:35px;height:35px;border-radius:50%;text-align:center;line-height:35px}.control-sidebar-menu .menu-info{margin-left:45px;margin-top:3px}.control-sidebar-menu .menu-info>.control-sidebar-subheading{margin:0}.control-sidebar-menu .menu-info>p{margin:0;font-size:11px}.control-sidebar-menu .progress{margin:0}.control-sidebar-dark{color:#cacdd2}.control-sidebar-dark,.control-sidebar-dark+.control-sidebar-bg{background:#313439}.control-sidebar-dark .nav-tabs.control-sidebar-tabs{border-bottom:#2a2c31}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a{background:#25272b;color:#cacdd2}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:focus{border-left-color:#202226;border-bottom-color:#202226}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:focus,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:active{background:#2a2c31}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover{color:#fff}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:hover,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:focus,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:active{background:#313439;color:#fff}.control-sidebar-dark .control-sidebar-heading,.control-sidebar-dark .control-sidebar-subheading{color:#fff}.control-sidebar-dark .control-sidebar-menu>li>a:hover{background:#2c2f34}.control-sidebar-dark .control-sidebar-menu>li>a .menu-info>p{color:#cacdd2}.control-sidebar-light{color:#7a7a7a}.control-sidebar-light,.control-sidebar-light+.control-sidebar-bg{background:#e0e0e0;border-left:1px solid #eee}.control-sidebar-light .nav-tabs.control-sidebar-tabs{border-bottom:#eee}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a{background:#d3d3d3;color:#616161}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:focus{border-left-color:#eee;border-bottom-color:#eee}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:focus,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:active{background:#d8d8d8}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:hover,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:focus,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:active{background:#e0e0e0;color:#111}.control-sidebar-light .control-sidebar-heading,.control-sidebar-light .control-sidebar-subheading{color:#111}.control-sidebar-light .control-sidebar-menu{margin-left:-14px}.control-sidebar-light .control-sidebar-menu>li>a:hover{background:#e4e4e4}.control-sidebar-light .control-sidebar-menu>li>a .menu-info>p{color:#7a7a7a}.dropdown-menu{box-shadow:none;border-color:#eee}.dropdown-menu>li>a{color:#777}.dropdown-menu>li>a>.glyphicon,.dropdown-menu>li>a>.fa,.dropdown-menu>li>a>.ion{margin-right:10px}.dropdown-menu>li>a:hover{background-color:#fbfbfb;color:#333}.dropdown-menu>.divider{background-color:#eee}.navbar-nav>.notifications-menu>.dropdown-menu,.navbar-nav>.messages-menu>.dropdown-menu,.navbar-nav>.tasks-menu>.dropdown-menu{width:280px;padding:0 0 0 0;margin:0;top:100%}.navbar-nav>.notifications-menu>.dropdown-menu>li,.navbar-nav>.messages-menu>.dropdown-menu>li,.navbar-nav>.tasks-menu>.dropdown-menu>li{position:relative}.navbar-nav>.notifications-menu>.dropdown-menu>li.header,.navbar-nav>.messages-menu>.dropdown-menu>li.header,.navbar-nav>.tasks-menu>.dropdown-menu>li.header{border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0;background-color:#ffffff;padding:7px 10px;border-bottom:1px solid #f4f4f4;color:#444444;font-size:14px}.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a,.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px;font-size:12px;background-color:#fff;padding:7px 10px;border-bottom:1px solid #eeeeee;color:#444 !important;text-align:center}@media (max-width:991px){.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a,.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a{background:#fff !important;color:#444 !important}}.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a:hover,.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a:hover,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a:hover{text-decoration:none;font-weight:normal}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu,.navbar-nav>.messages-menu>.dropdown-menu>li .menu,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu{max-height:200px;margin:0;padding:0;list-style:none;overflow-x:hidden}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a,.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a{display:block;white-space:nowrap;border-bottom:1px solid #f4f4f4}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a:hover,.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:hover,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a:hover{background:#f4f4f4;text-decoration:none}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a{color:#444444;overflow:hidden;text-overflow:ellipsis;padding:10px}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.glyphicon,.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.fa,.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.ion{width:20px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a{margin:0;padding:10px 10px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>div>img{margin:auto 10px auto auto;width:40px;height:40px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>h4{padding:0;margin:0 0 0 45px;color:#444444;font-size:15px;position:relative}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>h4>small{color:#999999;font-size:10px;position:absolute;top:0;right:0}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>p{margin:0 0 0 45px;font-size:12px;color:#888888}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:before,.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:after{content:" ";display:table}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:after{clear:both}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a{padding:10px}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a>h3{font-size:14px;padding:0;margin:0 0 10px 0;color:#666666}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a>.progress{padding:0;margin:0}.navbar-nav>.user-menu>.dropdown-menu{border-top-right-radius:0;border-top-left-radius:0;padding:1px 0 0 0;border-top-width:0;width:280px}.navbar-nav>.user-menu>.dropdown-menu,.navbar-nav>.user-menu>.dropdown-menu>.user-body{border-bottom-right-radius:4px;border-bottom-left-radius:4px}.navbar-nav>.user-menu>.dropdown-menu>li.user-header{height:175px;padding:10px;text-align:center}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>img{z-index:5;height:90px;width:90px;border:3px solid;border-color:transparent;border-color:rgba(255,255,255,0.2)}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>p{z-index:5;color:#fff;color:rgba(255,255,255,0.8);font-size:17px;margin-top:10px}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>p>small{display:block;font-size:12px}.navbar-nav>.user-menu>.dropdown-menu>.user-body{padding:15px;border-bottom:1px solid #f4f4f4;border-top:1px solid #dddddd}.navbar-nav>.user-menu>.dropdown-menu>.user-body:before,.navbar-nav>.user-menu>.dropdown-menu>.user-body:after{content:" ";display:table}.navbar-nav>.user-menu>.dropdown-menu>.user-body:after{clear:both}.navbar-nav>.user-menu>.dropdown-menu>.user-body a{color:#444 !important}@media (max-width:991px){.navbar-nav>.user-menu>.dropdown-menu>.user-body a{background:#fff !important;color:#444 !important}}.navbar-nav>.user-menu>.dropdown-menu>.user-footer{background-color:#f9f9f9;padding:10px}.navbar-nav>.user-menu>.dropdown-menu>.user-footer:before,.navbar-nav>.user-menu>.dropdown-menu>.user-footer:after{content:" ";display:table}.navbar-nav>.user-menu>.dropdown-menu>.user-footer:after{clear:both}.navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default{color:#666666}@media (max-width:991px){.navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default:hover{background-color:#f9f9f9}}.navbar-nav>.user-menu .user-image{float:left;width:25px;height:25px;border-radius:50%;margin-right:10px;margin-top:-2px}@media (max-width:767px){.navbar-nav>.user-menu .user-image{float:none;margin-right:0;margin-top:-8px;line-height:10px}}.open:not(.dropup)>.animated-dropdown-menu{backface-visibility:visible !important;-webkit-animation:flipInX .7s both;-o-animation:flipInX .7s both;animation:flipInX .7s both}@keyframes flipInX{0%{transform:perspective(400px) rotate3d(1, 0, 0, 90deg);transition-timing-function:ease-in;opacity:0}40%{transform:perspective(400px) rotate3d(1, 0, 0, -20deg);transition-timing-function:ease-in}60%{transform:perspective(400px) rotate3d(1, 0, 0, 10deg);opacity:1}80%{transform:perspective(400px) rotate3d(1, 0, 0, -5deg)}100%{transform:perspective(400px)}}@-webkit-keyframes flipInX{0%{-webkit-transform:perspective(400px) rotate3d(1, 0, 0, 90deg);-webkit-transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(1, 0, 0, -20deg);-webkit-transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(1, 0, 0, 10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(1, 0, 0, -5deg)}100%{-webkit-transform:perspective(400px)}}.navbar-custom-menu>.navbar-nav>li{position:relative}.navbar-custom-menu>.navbar-nav>li>.dropdown-menu{position:absolute;right:0;left:auto}@media (max-width:991px){.navbar-custom-menu>.navbar-nav{float:right}.navbar-custom-menu>.navbar-nav>li{position:static}.navbar-custom-menu>.navbar-nav>li>.dropdown-menu{position:absolute;right:5%;left:auto;border:1px solid #ddd;background:#fff}}.form-control{border-radius:0;box-shadow:none;border-color:#eee}.form-control:focus{border-color:#d3751c;box-shadow:none}.form-control::-moz-placeholder,.form-control:-ms-input-placeholder,.form-control::-webkit-input-placeholder{color:#bbb;opacity:1}.form-control:not(select){-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-group.has-success label{color:#418c7a}.form-group.has-success .form-control,.form-group.has-success .input-group-addon{border-color:#418c7a;box-shadow:none}.form-group.has-success .help-block{color:#418c7a}.form-group.has-warning label{color:#d6a136}.form-group.has-warning .form-control,.form-group.has-warning .input-group-addon{border-color:#d6a136;box-shadow:none}.form-group.has-warning .help-block{color:#d6a136}.form-group.has-error label{color:#ba2d0b}.form-group.has-error .form-control,.form-group.has-error .input-group-addon{border-color:#ba2d0b;box-shadow:none}.form-group.has-error .help-block{color:#ba2d0b}.input-group .input-group-addon{border-radius:0;border-color:#eee;background-color:#fff}.btn-group-vertical .btn.btn-flat:first-of-type,.btn-group-vertical .btn.btn-flat:last-of-type{border-radius:0}.icheck>label{padding-left:0}.form-control-feedback.fa{line-height:34px}.input-lg+.form-control-feedback.fa,.input-group-lg+.form-control-feedback.fa,.form-group-lg .form-control+.form-control-feedback.fa{line-height:46px}.input-sm+.form-control-feedback.fa,.input-group-sm+.form-control-feedback.fa,.form-group-sm .form-control+.form-control-feedback.fa{line-height:30px}.progress,.progress>.progress-bar{-webkit-box-shadow:none;box-shadow:none}.progress,.progress>.progress-bar,.progress .progress-bar,.progress>.progress-bar .progress-bar{border-radius:1px}.progress.sm,.progress-sm{height:10px}.progress.sm,.progress-sm,.progress.sm .progress-bar,.progress-sm .progress-bar{border-radius:1px}.progress.xs,.progress-xs{height:7px}.progress.xs,.progress-xs,.progress.xs .progress-bar,.progress-xs .progress-bar{border-radius:1px}.progress.xxs,.progress-xxs{height:3px}.progress.xxs,.progress-xxs,.progress.xxs .progress-bar,.progress-xxs .progress-bar{border-radius:1px}.progress.vertical{position:relative;width:30px;height:200px;display:inline-block;margin-right:10px}.progress.vertical>.progress-bar{width:100%;position:absolute;bottom:0}.progress.vertical.sm,.progress.vertical.progress-sm{width:20px}.progress.vertical.xs,.progress.vertical.progress-xs{width:10px}.progress.vertical.xxs,.progress.vertical.progress-xxs{width:3px}.progress-group .progress-text{font-weight:600}.progress-group .progress-number{float:right}.table tr>td .progress{margin:0}.progress-bar-light-blue,.progress-bar-primary{background-color:#d3751c}.progress-striped .progress-bar-light-blue,.progress-striped .progress-bar-primary{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-green,.progress-bar-success{background-color:#418c7a}.progress-striped .progress-bar-green,.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-aqua,.progress-bar-info{background-color:#989898}.progress-striped .progress-bar-aqua,.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-yellow,.progress-bar-warning{background-color:#d6a136}.progress-striped .progress-bar-yellow,.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-red,.progress-bar-danger{background-color:#ba2d0b}.progress-striped .progress-bar-red,.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.small-box{border-radius:2px;position:relative;display:block;margin-bottom:20px;box-shadow:0 1px 1px rgba(0,0,0,0.1)}.small-box>.inner{padding:10px}.small-box>.small-box-footer{position:relative;text-align:center;padding:3px 0;color:#fff;color:rgba(255,255,255,0.8);display:block;z-index:10;background:rgba(0,0,0,0.1);text-decoration:none}.small-box>.small-box-footer:hover{color:#fff;background:rgba(0,0,0,0.15)}.small-box h3{font-size:38px;font-weight:bold;margin:0 0 10px 0;white-space:nowrap;padding:0}.small-box p{font-size:15px}.small-box p>small{display:block;color:#f9f9f9;font-size:13px;margin-top:5px}.small-box h3,.small-box p{z-index:5}.small-box .icon{-webkit-transition:all .3s linear;-o-transition:all .3s linear;transition:all .3s linear;position:absolute;top:-10px;right:10px;z-index:0;font-size:90px;color:rgba(0,0,0,0.15)}.small-box:hover{text-decoration:none;color:#f9f9f9}.small-box:hover .icon{font-size:95px}@media (max-width:767px){.small-box{text-align:center}.small-box .icon{display:none}.small-box p{font-size:12px}}.box{position:relative;border-radius:3px;background:#ffffff;border-top:3px solid #d2d6de;margin-bottom:20px;width:100%;box-shadow:0 1px 1px rgba(0,0,0,0.1)}.box.box-primary{border-top-color:#d3751c}.box.box-info{border-top-color:#989898}.box.box-danger{border-top-color:#ba2d0b}.box.box-warning{border-top-color:#d6a136}.box.box-success{border-top-color:#418c7a}.box.box-default{border-top-color:#eee}.box.collapsed-box .box-body,.box.collapsed-box .box-footer{display:none}.box .nav-stacked>li{border-bottom:1px solid #f4f4f4;margin:0}.box .nav-stacked>li:last-of-type{border-bottom:none}.box.height-control .box-body{max-height:300px;overflow:auto}.box .border-right{border-right:1px solid #f4f4f4}.box .border-left{border-left:1px solid #f4f4f4}.box.box-solid{border-top:0}.box.box-solid>.box-header .btn.btn-default{background:transparent}.box.box-solid>.box-header .btn:hover,.box.box-solid>.box-header a:hover{background:rgba(0,0,0,0.1)}.box.box-solid.box-default{border:1px solid #eee}.box.box-solid.box-default>.box-header{color:#444;background:#eee;background-color:#eee}.box.box-solid.box-default>.box-header a,.box.box-solid.box-default>.box-header .btn{color:#444}.box.box-solid.box-primary{border:1px solid #d3751c}.box.box-solid.box-primary>.box-header{color:#fff;background:#d3751c;background-color:#d3751c}.box.box-solid.box-primary>.box-header a,.box.box-solid.box-primary>.box-header .btn{color:#fff}.box.box-solid.box-info{border:1px solid #989898}.box.box-solid.box-info>.box-header{color:#fff;background:#989898;background-color:#989898}.box.box-solid.box-info>.box-header a,.box.box-solid.box-info>.box-header .btn{color:#fff}.box.box-solid.box-danger{border:1px solid #ba2d0b}.box.box-solid.box-danger>.box-header{color:#fff;background:#ba2d0b;background-color:#ba2d0b}.box.box-solid.box-danger>.box-header a,.box.box-solid.box-danger>.box-header .btn{color:#fff}.box.box-solid.box-warning{border:1px solid #d6a136}.box.box-solid.box-warning>.box-header{color:#fff;background:#d6a136;background-color:#d6a136}.box.box-solid.box-warning>.box-header a,.box.box-solid.box-warning>.box-header .btn{color:#fff}.box.box-solid.box-success{border:1px solid #418c7a}.box.box-solid.box-success>.box-header{color:#fff;background:#418c7a;background-color:#418c7a}.box.box-solid.box-success>.box-header a,.box.box-solid.box-success>.box-header .btn{color:#fff}.box.box-solid>.box-header>.box-tools .btn{border:0;box-shadow:none}.box.box-solid[class*='bg']>.box-header{color:#fff}.box .box-group>.box{margin-bottom:5px}.box .knob-label{text-align:center;color:#333;font-weight:100;font-size:12px;margin-bottom:0.3em}.box>.overlay,.overlay-wrapper>.overlay,.box>.loading-img,.overlay-wrapper>.loading-img{position:absolute;top:0;left:0;width:100%;height:100%}.box .overlay,.overlay-wrapper .overlay{z-index:50;background:rgba(255,255,255,0.7);border-radius:3px}.box .overlay>.fa,.overlay-wrapper .overlay>.fa{position:absolute;top:50%;left:50%;margin-left:-15px;margin-top:-15px;color:#000;font-size:30px}.box .overlay.dark,.overlay-wrapper .overlay.dark{background:rgba(0,0,0,0.5)}.box-header:before,.box-body:before,.box-footer:before,.box-header:after,.box-body:after,.box-footer:after{content:" ";display:table}.box-header:after,.box-body:after,.box-footer:after{clear:both}.box-header{color:#444;display:block;padding:10px;position:relative}.box-header.with-border{border-bottom:1px solid #f4f4f4}.collapsed-box .box-header.with-border{border-bottom:none}.box-header>.fa,.box-header>.glyphicon,.box-header>.ion,.box-header .box-title{display:inline-block;font-size:18px;margin:0;line-height:1}.box-header>.fa,.box-header>.glyphicon,.box-header>.ion{margin-right:5px}.box-header>.box-tools{position:absolute;right:10px;top:5px}.box-header>.box-tools [data-toggle="tooltip"]{position:relative}.box-header>.box-tools.pull-right .dropdown-menu{right:0;left:auto}.box-header>.box-tools .dropdown-menu>li>a{color:#444!important}.btn-box-tool{padding:5px;font-size:12px;background:transparent;color:#97a0b3}.open .btn-box-tool,.btn-box-tool:hover{color:#606c84}.btn-box-tool.btn:active{box-shadow:none}.box-body{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px;padding:10px}.no-header .box-body{border-top-right-radius:3px;border-top-left-radius:3px}.box-body>.table{margin-bottom:0}.box-body .fc{margin-top:5px}.box-body .full-width-chart{margin:-19px}.box-body.no-padding .full-width-chart{margin:-9px}.box-body .box-pane{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:3px}.box-body .box-pane-right{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:3px;border-bottom-left-radius:0}.box-footer{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px;border-top:1px solid #f4f4f4;padding:10px;background-color:#fff}.chart-legend{margin:10px 0}@media (max-width:991px){.chart-legend>li{float:left;margin-right:10px}}.box-comments{background:#f7f7f7}.box-comments .box-comment{padding:8px 0;border-bottom:1px solid #eee}.box-comments .box-comment:before,.box-comments .box-comment:after{content:" ";display:table}.box-comments .box-comment:after{clear:both}.box-comments .box-comment:last-of-type{border-bottom:0}.box-comments .box-comment:first-of-type{padding-top:0}.box-comments .box-comment img{float:left}.box-comments .comment-text{margin-left:40px;color:#555}.box-comments .username{color:#444;display:block;font-weight:600}.box-comments .text-muted{font-weight:400;font-size:12px}.todo-list{margin:0;padding:0;list-style:none;overflow:auto}.todo-list>li{border-radius:2px;padding:10px;background:#f4f4f4;margin-bottom:2px;border-left:2px solid #e6e7e8;color:#444}.todo-list>li:last-of-type{margin-bottom:0}.todo-list>li>input[type='checkbox']{margin:0 10px 0 5px}.todo-list>li .text{display:inline-block;margin-left:5px;font-weight:600}.todo-list>li .label{margin-left:10px;font-size:9px}.todo-list>li .tools{display:none;float:right;color:#ba2d0b}.todo-list>li .tools>.fa,.todo-list>li .tools>.glyphicon,.todo-list>li .tools>.ion{margin-right:5px;cursor:pointer}.todo-list>li:hover .tools{display:inline-block}.todo-list>li.done{color:#999}.todo-list>li.done .text{text-decoration:line-through;font-weight:500}.todo-list>li.done .label{background:#eee !important}.todo-list .danger{border-left-color:#ba2d0b}.todo-list .warning{border-left-color:#d6a136}.todo-list .info{border-left-color:#989898}.todo-list .success{border-left-color:#418c7a}.todo-list .primary{border-left-color:#d3751c}.todo-list .handle{display:inline-block;cursor:move;margin:0 5px}.chat{padding:5px 20px 5px 10px}.chat .item{margin-bottom:10px}.chat .item:before,.chat .item:after{content:" ";display:table}.chat .item:after{clear:both}.chat .item>img{width:40px;height:40px;border:2px solid transparent;border-radius:50%}.chat .item>.online{border:2px solid #418c7a}.chat .item>.offline{border:2px solid #ba2d0b}.chat .item>.message{margin-left:55px;margin-top:-40px}.chat .item>.message>.name{display:block;font-weight:600}.chat .item>.attachment{border-radius:3px;background:#f4f4f4;margin-left:65px;margin-right:15px;padding:10px}.chat .item>.attachment>h4{margin:0 0 5px 0;font-weight:600;font-size:14px}.chat .item>.attachment>p,.chat .item>.attachment>.filename{font-weight:600;font-size:13px;font-style:italic;margin:0}.chat .item>.attachment:before,.chat .item>.attachment:after{content:" ";display:table}.chat .item>.attachment:after{clear:both}.box-input{max-width:200px}.modal .panel-body{color:#444}.info-box{display:block;min-height:90px;background:#fff;width:100%;box-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:2px;margin-bottom:15px}.info-box small{font-size:14px}.info-box .progress{background:rgba(0,0,0,0.2);margin:5px -10px 5px -10px;height:2px}.info-box .progress,.info-box .progress .progress-bar{border-radius:0}.info-box .progress .progress-bar{background:#fff}.info-box-icon{border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px;display:block;float:left;height:90px;width:90px;text-align:center;font-size:45px;line-height:90px;background:rgba(0,0,0,0.2)}.info-box-icon>img{max-width:100%}.info-box-content{padding:5px 10px;margin-left:90px}.info-box-number{display:block;font-weight:bold;font-size:18px}.progress-description,.info-box-text{display:block;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.info-box-text{text-transform:uppercase}.info-box-more{display:block}.progress-description{margin:0}.timeline{position:relative;margin:0 0 30px 0;padding:0;list-style:none}.timeline:before{content:'';position:absolute;top:0;bottom:0;width:4px;background:#ddd;left:31px;margin:0;border-radius:2px}.timeline>li{position:relative;margin-right:10px;margin-bottom:15px}.timeline>li:before,.timeline>li:after{content:" ";display:table}.timeline>li:after{clear:both}.timeline>li>.timeline-item{-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.1);box-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:3px;margin-top:0;background:#fff;color:#444;margin-left:60px;margin-right:15px;padding:0;position:relative}.timeline>li>.timeline-item>.time{color:#999;float:right;padding:10px;font-size:12px}.timeline>li>.timeline-item>.timeline-header{margin:0;color:#555;border-bottom:1px solid #f4f4f4;padding:10px;font-size:16px;line-height:1.1}.timeline>li>.timeline-item>.timeline-header>a{font-weight:600}.timeline>li>.timeline-item>.timeline-body,.timeline>li>.timeline-item>.timeline-footer{padding:10px}.timeline>li>.fa,.timeline>li>.glyphicon,.timeline>li>.ion{width:30px;height:30px;font-size:15px;line-height:30px;position:absolute;color:#666;background:#eee;border-radius:50%;text-align:center;left:18px;top:0}.timeline>.time-label>span{font-weight:600;padding:5px;display:inline-block;background-color:#fff;border-radius:4px}.timeline-inverse>li>.timeline-item{background:#f0f0f0;border:1px solid #ddd;-webkit-box-shadow:none;box-shadow:none}.timeline-inverse>li>.timeline-item>.timeline-header{border-bottom-color:#ddd}.btn{border-radius:3px;-webkit-box-shadow:none;box-shadow:none;border:1px solid transparent}.btn.uppercase{text-transform:uppercase}.btn.btn-flat{border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;border-width:1px}.btn:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn:focus{outline:none}.btn.btn-file{position:relative;overflow:hidden}.btn.btn-file>input[type='file']{position:absolute;top:0;right:0;min-width:100%;min-height:100%;font-size:100px;text-align:right;opacity:0;filter:alpha(opacity=0);outline:none;background:white;cursor:inherit;display:block}.btn-default{background-color:#f4f4f4;color:#444;border-color:#ddd}.btn-default:hover,.btn-default:active,.btn-default.hover{background-color:#e7e7e7;border-color:#e7e7e7}.btn-primary{background-color:#d3751c;border-color:#bc6919}.btn-primary:hover,.btn-primary:active,.btn-primary.hover{background-color:#bc6919;border-color:#bc6919}.btn-success{background-color:#418c7a;border-color:#397b6b}.btn-success:hover,.btn-success:active,.btn-success.hover{background-color:#397b6b;border-color:#397b6b}.btn-info{background-color:#989898;border-color:#8b8b8b}.btn-info:hover,.btn-info:active,.btn-info.hover{background-color:#8b8b8b;border-color:#8b8b8b}.btn-danger{background-color:#ba2d0b;border-color:#a2270a}.btn-danger:hover,.btn-danger:active,.btn-danger.hover{background-color:#a2270a;border-color:#a2270a}.btn-warning{background-color:#d6a136;border-color:#c99429}.btn-warning:hover,.btn-warning:active,.btn-warning.hover{background-color:#c99429;border-color:#c99429}.btn-outline{border:1px solid #fff;background:transparent;color:#fff}.btn-outline:hover,.btn-outline:focus,.btn-outline:active{color:rgba(255,255,255,0.7);border-color:rgba(255,255,255,0.7)}.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn[class*='bg-']:hover{-webkit-box-shadow:inset 0 0 100px rgba(0,0,0,0.2);box-shadow:inset 0 0 100px rgba(0,0,0,0.2)}.btn-app{border-radius:3px;position:relative;padding:15px 5px;margin:0 0 10px 10px;min-width:80px;height:60px;text-align:center;color:#666;border:1px solid #ddd;background-color:#f4f4f4;font-size:12px}.btn-app>.fa,.btn-app>.glyphicon,.btn-app>.ion{font-size:20px;display:block}.btn-app:hover{background:#f4f4f4;color:#444;border-color:#aaa}.btn-app:active,.btn-app:focus{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-app>.badge{position:absolute;top:-3px;right:-10px;font-size:10px;font-weight:400}.callout{border-radius:3px;margin:0 0 20px 0;padding:15px 30px 15px 15px;border-left:5px solid #eee}.callout a{color:#fff;text-decoration:underline}.callout a:hover{color:#eee}.callout h4{margin-top:0;font-weight:600}.callout p:last-child{margin-bottom:0}.callout code,.callout .highlight{background-color:#fff}.callout.callout-danger{border-color:#8a2108}.callout.callout-warning{border-color:#b48525}.callout.callout-info{border-color:#7f7f7f}.callout.callout-success{border-color:#31695c}.alert{border-radius:3px}.alert h4{font-weight:600}.alert .icon{margin-right:10px}.alert .close{color:#000;opacity:.2;filter:alpha(opacity=20)}.alert .close:hover{opacity:.5;filter:alpha(opacity=50)}.alert a{color:#fff;text-decoration:underline}.alert-success{border-color:#397b6b}.alert-danger,.alert-error{border-color:#a2270a}.alert-warning{border-color:#c99429}.alert-info{border-color:#8b8b8b}.nav>li>a:hover,.nav>li>a:active,.nav>li>a:focus{color:#444;background:#f7f7f7}.nav-pills>li>a{border-radius:0;border-top:3px solid transparent;color:#444}.nav-pills>li>a>.fa,.nav-pills>li>a>.glyphicon,.nav-pills>li>a>.ion{margin-right:5px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{border-top-color:#d3751c}.nav-pills>li.active>a{font-weight:600}.nav-stacked>li>a{border-radius:0;border-top:0;border-left:3px solid transparent;color:#444}.nav-stacked>li.active>a,.nav-stacked>li.active>a:hover{background:transparent;color:#444;border-top:0;border-left-color:#d3751c}.nav-stacked>li.header{border-bottom:1px solid #ddd;color:#777;margin-bottom:10px;padding:5px 10px;text-transform:uppercase}.nav-tabs-custom{margin-bottom:20px;background:#fff;box-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:3px}.nav-tabs-custom>.nav-tabs{margin:0;border-bottom-color:#f4f4f4;border-top-right-radius:3px;border-top-left-radius:3px}.nav-tabs-custom>.nav-tabs>li{border-top:3px solid transparent;margin-bottom:-2px;margin-right:5px}.nav-tabs-custom>.nav-tabs>li>a{color:#444;border-radius:0}.nav-tabs-custom>.nav-tabs>li>a.text-muted{color:#999}.nav-tabs-custom>.nav-tabs>li>a,.nav-tabs-custom>.nav-tabs>li>a:hover{background:transparent;margin:0}.nav-tabs-custom>.nav-tabs>li>a:hover{color:#999}.nav-tabs-custom>.nav-tabs>li:not(.active)>a:hover,.nav-tabs-custom>.nav-tabs>li:not(.active)>a:focus,.nav-tabs-custom>.nav-tabs>li:not(.active)>a:active{border-color:transparent}.nav-tabs-custom>.nav-tabs>li.active{border-top-color:#d3751c}.nav-tabs-custom>.nav-tabs>li.active>a,.nav-tabs-custom>.nav-tabs>li.active:hover>a{background-color:#fff;color:#444}.nav-tabs-custom>.nav-tabs>li.active>a{border-top-color:transparent;border-left-color:#f4f4f4;border-right-color:#f4f4f4}.nav-tabs-custom>.nav-tabs>li:first-of-type{margin-left:0}.nav-tabs-custom>.nav-tabs>li:first-of-type.active>a{border-left-color:transparent}.nav-tabs-custom>.nav-tabs.pull-right{float:none !important}.nav-tabs-custom>.nav-tabs.pull-right>li{float:right}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type{margin-right:0}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type>a{border-left-width:1px}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type.active>a{border-left-color:#f4f4f4;border-right-color:transparent}.nav-tabs-custom>.nav-tabs>li.header{line-height:35px;padding:0 10px;font-size:20px;color:#444}.nav-tabs-custom>.nav-tabs>li.header>.fa,.nav-tabs-custom>.nav-tabs>li.header>.glyphicon,.nav-tabs-custom>.nav-tabs>li.header>.ion{margin-right:5px}.nav-tabs-custom>.tab-content{background:#fff;padding:10px;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.nav-tabs-custom .dropdown.open>a:active,.nav-tabs-custom .dropdown.open>a:focus{background:transparent;color:#999}.nav-tabs-custom.tab-primary>.nav-tabs>li.active{border-top-color:#d3751c}.nav-tabs-custom.tab-info>.nav-tabs>li.active{border-top-color:#989898}.nav-tabs-custom.tab-danger>.nav-tabs>li.active{border-top-color:#ba2d0b}.nav-tabs-custom.tab-warning>.nav-tabs>li.active{border-top-color:#d6a136}.nav-tabs-custom.tab-success>.nav-tabs>li.active{border-top-color:#418c7a}.nav-tabs-custom.tab-default>.nav-tabs>li.active{border-top-color:#eee}.pagination>li>a{background:#fafafa;color:#666}.pagination.pagination-flat>li>a{border-radius:0 !important}.products-list{list-style:none;margin:0;padding:0}.products-list>.item{border-radius:3px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.1);box-shadow:0 1px 1px rgba(0,0,0,0.1);padding:10px 0;background:#fff}.products-list>.item:before,.products-list>.item:after{content:" ";display:table}.products-list>.item:after{clear:both}.products-list .product-img{float:left}.products-list .product-img img{width:50px;height:50px}.products-list .product-info{margin-left:60px}.products-list .product-title{font-weight:600}.products-list .product-description{display:block;color:#999;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.product-list-in-box>.item{-webkit-box-shadow:none;box-shadow:none;border-radius:0;border-bottom:1px solid #f4f4f4}.product-list-in-box>.item:last-of-type{border-bottom-width:0}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{border-top:1px solid #f4f4f4}.table>thead>tr>th{border-bottom:2px solid #f4f4f4}.table tr td .progress{margin-top:5px}.table-bordered{border:1px solid #f4f4f4}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #f4f4f4}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table.no-border,.table.no-border td,.table.no-border th{border:0}table.text-center,table.text-center td,table.text-center th{text-align:center}.table.align th{text-align:left}.table.align td{text-align:right}.label-default{background-color:#eee;color:#444}.direct-chat .box-body{border-bottom-right-radius:0;border-bottom-left-radius:0;position:relative;overflow-x:hidden;padding:0}.direct-chat.chat-pane-open .direct-chat-contacts{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}.direct-chat-messages{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0);padding:10px;height:250px;overflow:auto}.direct-chat-msg,.direct-chat-text{display:block}.direct-chat-msg{margin-bottom:10px}.direct-chat-msg:before,.direct-chat-msg:after{content:" ";display:table}.direct-chat-msg:after{clear:both}.direct-chat-messages,.direct-chat-contacts{-webkit-transition:-webkit-transform .5s ease-in-out;-moz-transition:-moz-transform .5s ease-in-out;-o-transition:-o-transform .5s ease-in-out;transition:transform .5s ease-in-out}.direct-chat-text{border-radius:5px;position:relative;padding:5px 10px;background:#eee;border:1px solid #eee;margin:5px 0 0 50px;color:#444}.direct-chat-text:after,.direct-chat-text:before{position:absolute;right:100%;top:15px;border:solid transparent;border-right-color:#eee;content:' ';height:0;width:0;pointer-events:none}.direct-chat-text:after{border-width:5px;margin-top:-5px}.direct-chat-text:before{border-width:6px;margin-top:-6px}.right .direct-chat-text{margin-right:50px;margin-left:0}.right .direct-chat-text:after,.right .direct-chat-text:before{right:auto;left:100%;border-right-color:transparent;border-left-color:#eee}.direct-chat-img{border-radius:50%;float:left;width:40px;height:40px}.right .direct-chat-img{float:right}.direct-chat-info{display:block;margin-bottom:2px;font-size:12px}.direct-chat-name{font-weight:600}.direct-chat-timestamp{color:#999}.direct-chat-contacts-open .direct-chat-contacts{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}.direct-chat-contacts{-webkit-transform:translate(101%, 0);-ms-transform:translate(101%, 0);-o-transform:translate(101%, 0);transform:translate(101%, 0);position:absolute;top:0;bottom:0;height:250px;width:100%;background:#222d32;color:#fff;overflow:auto}.contacts-list>li{border-bottom:1px solid rgba(0,0,0,0.2);padding:10px;margin:0}.contacts-list>li:before,.contacts-list>li:after{content:" ";display:table}.contacts-list>li:after{clear:both}.contacts-list>li:last-of-type{border-bottom:none}.contacts-list-img{border-radius:50%;width:40px;float:left}.contacts-list-info{margin-left:45px;color:#fff}.contacts-list-name,.contacts-list-status{display:block}.contacts-list-name{font-weight:600}.contacts-list-status{font-size:12px}.contacts-list-date{color:#aaa;font-weight:normal}.contacts-list-msg{color:#999}.direct-chat-danger .right>.direct-chat-text{background:#ba2d0b;border-color:#ba2d0b;color:#fff}.direct-chat-danger .right>.direct-chat-text:after,.direct-chat-danger .right>.direct-chat-text:before{border-left-color:#ba2d0b}.direct-chat-primary .right>.direct-chat-text{background:#d3751c;border-color:#d3751c;color:#fff}.direct-chat-primary .right>.direct-chat-text:after,.direct-chat-primary .right>.direct-chat-text:before{border-left-color:#d3751c}.direct-chat-warning .right>.direct-chat-text{background:#d6a136;border-color:#d6a136;color:#fff}.direct-chat-warning .right>.direct-chat-text:after,.direct-chat-warning .right>.direct-chat-text:before{border-left-color:#d6a136}.direct-chat-info .right>.direct-chat-text{background:#989898;border-color:#989898;color:#fff}.direct-chat-info .right>.direct-chat-text:after,.direct-chat-info .right>.direct-chat-text:before{border-left-color:#989898}.direct-chat-success .right>.direct-chat-text{background:#418c7a;border-color:#418c7a;color:#fff}.direct-chat-success .right>.direct-chat-text:after,.direct-chat-success .right>.direct-chat-text:before{border-left-color:#418c7a}.users-list>li{width:25%;float:left;padding:10px;text-align:center}.users-list>li img{border-radius:50%;max-width:100%;height:auto}.users-list>li>a:hover,.users-list>li>a:hover .users-list-name{color:#999}.users-list-name,.users-list-date{display:block}.users-list-name{font-weight:600;color:#444;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.users-list-date{color:#999;font-size:12px}.carousel-control.left,.carousel-control.right{background-image:none}.carousel-control>.fa{font-size:40px;position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-20px}.modal{background:rgba(0,0,0,0.3)}.modal-content{border-radius:0;-webkit-box-shadow:0 2px 3px rgba(0,0,0,0.125);box-shadow:0 2px 3px rgba(0,0,0,0.125);border:0}@media (min-width:768px){.modal-content{-webkit-box-shadow:0 2px 3px rgba(0,0,0,0.125);box-shadow:0 2px 3px rgba(0,0,0,0.125)}}.modal-header{border-bottom-color:#f4f4f4}.modal-footer{border-top-color:#f4f4f4}.modal-primary .modal-header,.modal-primary .modal-footer{border-color:#a65c16}.modal-warning .modal-header,.modal-warning .modal-footer{border-color:#b48525}.modal-info .modal-header,.modal-info .modal-footer{border-color:#7f7f7f}.modal-success .modal-header,.modal-success .modal-footer{border-color:#31695c}.modal-danger .modal-header,.modal-danger .modal-footer{border-color:#8a2108}.box-widget{border:none;position:relative}.widget-user .widget-user-header{padding:20px;height:120px;border-top-right-radius:3px;border-top-left-radius:3px}.widget-user .widget-user-username{margin-top:0;margin-bottom:5px;font-size:25px;font-weight:300;text-shadow:0 1px 1px rgba(0,0,0,0.2)}.widget-user .widget-user-desc{margin-top:0}.widget-user .widget-user-image{position:absolute;top:65px;left:50%;margin-left:-45px}.widget-user .widget-user-image>img{width:90px;height:auto;border:3px solid #fff}.widget-user .box-footer{padding-top:30px}.widget-user-2 .widget-user-header{padding:20px;border-top-right-radius:3px;border-top-left-radius:3px}.widget-user-2 .widget-user-username{margin-top:5px;margin-bottom:5px;font-size:25px;font-weight:300}.widget-user-2 .widget-user-desc{margin-top:0}.widget-user-2 .widget-user-username,.widget-user-2 .widget-user-desc{margin-left:75px}.widget-user-2 .widget-user-image>img{width:65px;height:auto;float:left}.mailbox-messages>.table{margin:0}.mailbox-controls{padding:5px}.mailbox-controls.with-border{border-bottom:1px solid #f4f4f4}.mailbox-read-info{border-bottom:1px solid #f4f4f4;padding:10px}.mailbox-read-info h3{font-size:20px;margin:0}.mailbox-read-info h5{margin:0;padding:5px 0 0 0}.mailbox-read-time{color:#999;font-size:13px}.mailbox-read-message{padding:10px}.mailbox-attachments li{float:left;width:200px;border:1px solid #eee;margin-bottom:10px;margin-right:10px}.mailbox-attachment-name{font-weight:bold;color:#666}.mailbox-attachment-icon,.mailbox-attachment-info,.mailbox-attachment-size{display:block}.mailbox-attachment-info{padding:10px;background:#f4f4f4}.mailbox-attachment-size{color:#999;font-size:12px}.mailbox-attachment-icon{text-align:center;font-size:65px;color:#666;padding:20px 10px}.mailbox-attachment-icon.has-img{padding:0}.mailbox-attachment-icon.has-img>img{max-width:100%;height:auto}.lockscreen{background:#eee}.lockscreen-logo{font-size:35px;text-align:center;margin-bottom:25px;font-weight:300}.lockscreen-logo a{color:#444}.lockscreen-wrapper{max-width:400px;margin:0 auto;margin-top:10%}.lockscreen .lockscreen-name{text-align:center;font-weight:600}.lockscreen-item{border-radius:4px;padding:0;background:#fff;position:relative;margin:10px auto 30px auto;width:290px}.lockscreen-image{border-radius:50%;position:absolute;left:-10px;top:-25px;background:#fff;padding:5px;z-index:10}.lockscreen-image>img{border-radius:50%;width:70px;height:70px}.lockscreen-credentials{margin-left:70px}.lockscreen-credentials .form-control{border:0}.lockscreen-credentials .btn{background-color:#fff;border:0;padding:0 10px}.lockscreen-footer{margin-top:10px}.login-logo,.register-logo{font-size:35px;text-align:center;margin-bottom:25px;font-weight:300}.login-logo a,.register-logo a{color:#444}.login-page,.register-page{background:#eee}.login-box,.register-box{width:360px;margin:7% auto}@media (max-width:768px){.login-box,.register-box{width:90%;margin-top:20px}}.login-box-body,.register-box-body{background:#fff;padding:20px;border-top:0;color:#666}.login-box-body .form-control-feedback,.register-box-body .form-control-feedback{color:#777}.login-box-msg,.register-box-msg{margin:0;text-align:center;padding:0 20px 20px 20px}.social-auth-links{margin:10px 0}.error-page{width:600px;margin:20px auto 0 auto}@media (max-width:991px){.error-page{width:100%}}.error-page>.headline{float:left;font-size:100px;font-weight:300}@media (max-width:991px){.error-page>.headline{float:none;text-align:center}}.error-page>.error-content{margin-left:190px;display:block}@media (max-width:991px){.error-page>.error-content{margin-left:0}}.error-page>.error-content>h3{font-weight:300;font-size:25px}@media (max-width:991px){.error-page>.error-content>h3{text-align:center}}.invoice{position:relative;background:#fff;border:1px solid #f4f4f4;padding:20px;margin:10px 25px}.invoice-title{margin-top:0}.profile-user-img{margin:0 auto;width:100px;padding:3px;border:3px solid #eee}.profile-username{font-size:21px;margin-top:5px}.post{border-bottom:1px solid #eee;margin-bottom:15px;padding-bottom:15px;color:#666}.post:last-of-type{border-bottom:0;margin-bottom:0;padding-bottom:0}.post .user-block{margin-bottom:15px}.btn-social{position:relative;padding-left:44px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.btn-social>:first-child{position:absolute;left:0;top:0;bottom:0;width:32px;line-height:34px;font-size:1.6em;text-align:center;border-right:1px solid rgba(0,0,0,0.2)}.btn-social.btn-lg{padding-left:61px}.btn-social.btn-lg>:first-child{line-height:45px;width:45px;font-size:1.8em}.btn-social.btn-sm{padding-left:38px}.btn-social.btn-sm>:first-child{line-height:28px;width:28px;font-size:1.4em}.btn-social.btn-xs{padding-left:30px}.btn-social.btn-xs>:first-child{line-height:20px;width:20px;font-size:1.2em}.btn-social-icon{position:relative;padding-left:44px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;height:34px;width:34px;padding:0}.btn-social-icon>:first-child{position:absolute;left:0;top:0;bottom:0;width:32px;line-height:34px;font-size:1.6em;text-align:center;border-right:1px solid rgba(0,0,0,0.2)}.btn-social-icon.btn-lg{padding-left:61px}.btn-social-icon.btn-lg>:first-child{line-height:45px;width:45px;font-size:1.8em}.btn-social-icon.btn-sm{padding-left:38px}.btn-social-icon.btn-sm>:first-child{line-height:28px;width:28px;font-size:1.4em}.btn-social-icon.btn-xs{padding-left:30px}.btn-social-icon.btn-xs>:first-child{line-height:20px;width:20px;font-size:1.2em}.btn-social-icon>:first-child{border:none;text-align:center;width:100%}.btn-social-icon.btn-lg{height:45px;width:45px;padding-left:0;padding-right:0}.btn-social-icon.btn-sm{height:30px;width:30px;padding-left:0;padding-right:0}.btn-social-icon.btn-xs{height:22px;width:22px;padding-left:0;padding-right:0}.btn-adn{color:#fff;background-color:#d87a68;border-color:rgba(0,0,0,0.2)}.btn-adn:focus,.btn-adn.focus{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)}.btn-adn:hover{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)}.btn-adn:active,.btn-adn.active,.open>.dropdown-toggle.btn-adn{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)}.btn-adn:active,.btn-adn.active,.open>.dropdown-toggle.btn-adn{background-image:none}.btn-adn .badge{color:#d87a68;background-color:#fff}.btn-bitbucket{color:#fff;background-color:#205081;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:focus,.btn-bitbucket.focus{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:hover{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:active,.btn-bitbucket.active,.open>.dropdown-toggle.btn-bitbucket{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:active,.btn-bitbucket.active,.open>.dropdown-toggle.btn-bitbucket{background-image:none}.btn-bitbucket .badge{color:#205081;background-color:#fff}.btn-dropbox{color:#fff;background-color:#1087dd;border-color:rgba(0,0,0,0.2)}.btn-dropbox:focus,.btn-dropbox.focus{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)}.btn-dropbox:hover{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)}.btn-dropbox:active,.btn-dropbox.active,.open>.dropdown-toggle.btn-dropbox{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)}.btn-dropbox:active,.btn-dropbox.active,.open>.dropdown-toggle.btn-dropbox{background-image:none}.btn-dropbox .badge{color:#1087dd;background-color:#fff}.btn-facebook{color:#fff;background-color:#3b5998;border-color:rgba(0,0,0,0.2)}.btn-facebook:focus,.btn-facebook.focus{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)}.btn-facebook:hover{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)}.btn-facebook:active,.btn-facebook.active,.open>.dropdown-toggle.btn-facebook{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)}.btn-facebook:active,.btn-facebook.active,.open>.dropdown-toggle.btn-facebook{background-image:none}.btn-facebook .badge{color:#3b5998;background-color:#fff}.btn-flickr{color:#fff;background-color:#ff0084;border-color:rgba(0,0,0,0.2)}.btn-flickr:focus,.btn-flickr.focus{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)}.btn-flickr:hover{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)}.btn-flickr:active,.btn-flickr.active,.open>.dropdown-toggle.btn-flickr{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)}.btn-flickr:active,.btn-flickr.active,.open>.dropdown-toggle.btn-flickr{background-image:none}.btn-flickr .badge{color:#ff0084;background-color:#fff}.btn-foursquare{color:#fff;background-color:#f94877;border-color:rgba(0,0,0,0.2)}.btn-foursquare:focus,.btn-foursquare.focus{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)}.btn-foursquare:hover{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)}.btn-foursquare:active,.btn-foursquare.active,.open>.dropdown-toggle.btn-foursquare{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)}.btn-foursquare:active,.btn-foursquare.active,.open>.dropdown-toggle.btn-foursquare{background-image:none}.btn-foursquare .badge{color:#f94877;background-color:#fff}.btn-github{color:#fff;background-color:#444;border-color:rgba(0,0,0,0.2)}.btn-github:focus,.btn-github.focus{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)}.btn-github:hover{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)}.btn-github:active,.btn-github.active,.open>.dropdown-toggle.btn-github{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)}.btn-github:active,.btn-github.active,.open>.dropdown-toggle.btn-github{background-image:none}.btn-github .badge{color:#444;background-color:#fff}.btn-google{color:#fff;background-color:#dd4b39;border-color:rgba(0,0,0,0.2)}.btn-google:focus,.btn-google.focus{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)}.btn-google:hover{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)}.btn-google:active,.btn-google.active,.open>.dropdown-toggle.btn-google{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)}.btn-google:active,.btn-google.active,.open>.dropdown-toggle.btn-google{background-image:none}.btn-google .badge{color:#dd4b39;background-color:#fff}.btn-instagram{color:#fff;background-color:#3f729b;border-color:rgba(0,0,0,0.2)}.btn-instagram:focus,.btn-instagram.focus{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)}.btn-instagram:hover{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)}.btn-instagram:active,.btn-instagram.active,.open>.dropdown-toggle.btn-instagram{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)}.btn-instagram:active,.btn-instagram.active,.open>.dropdown-toggle.btn-instagram{background-image:none}.btn-instagram .badge{color:#3f729b;background-color:#fff}.btn-linkedin{color:#fff;background-color:#007bb6;border-color:rgba(0,0,0,0.2)}.btn-linkedin:focus,.btn-linkedin.focus{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)}.btn-linkedin:hover{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)}.btn-linkedin:active,.btn-linkedin.active,.open>.dropdown-toggle.btn-linkedin{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)}.btn-linkedin:active,.btn-linkedin.active,.open>.dropdown-toggle.btn-linkedin{background-image:none}.btn-linkedin .badge{color:#007bb6;background-color:#fff}.btn-microsoft{color:#fff;background-color:#2672ec;border-color:rgba(0,0,0,0.2)}.btn-microsoft:focus,.btn-microsoft.focus{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)}.btn-microsoft:hover{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)}.btn-microsoft:active,.btn-microsoft.active,.open>.dropdown-toggle.btn-microsoft{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)}.btn-microsoft:active,.btn-microsoft.active,.open>.dropdown-toggle.btn-microsoft{background-image:none}.btn-microsoft .badge{color:#2672ec;background-color:#fff}.btn-openid{color:#fff;background-color:#f7931e;border-color:rgba(0,0,0,0.2)}.btn-openid:focus,.btn-openid.focus{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)}.btn-openid:hover{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)}.btn-openid:active,.btn-openid.active,.open>.dropdown-toggle.btn-openid{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)}.btn-openid:active,.btn-openid.active,.open>.dropdown-toggle.btn-openid{background-image:none}.btn-openid .badge{color:#f7931e;background-color:#fff}.btn-pinterest{color:#fff;background-color:#cb2027;border-color:rgba(0,0,0,0.2)}.btn-pinterest:focus,.btn-pinterest.focus{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)}.btn-pinterest:hover{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)}.btn-pinterest:active,.btn-pinterest.active,.open>.dropdown-toggle.btn-pinterest{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)}.btn-pinterest:active,.btn-pinterest.active,.open>.dropdown-toggle.btn-pinterest{background-image:none}.btn-pinterest .badge{color:#cb2027;background-color:#fff}.btn-reddit{color:#000;background-color:#eff7ff;border-color:rgba(0,0,0,0.2)}.btn-reddit:focus,.btn-reddit.focus{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)}.btn-reddit:hover{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)}.btn-reddit:active,.btn-reddit.active,.open>.dropdown-toggle.btn-reddit{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)}.btn-reddit:active,.btn-reddit.active,.open>.dropdown-toggle.btn-reddit{background-image:none}.btn-reddit .badge{color:#eff7ff;background-color:#000}.btn-soundcloud{color:#fff;background-color:#f50;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:focus,.btn-soundcloud.focus{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:hover{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:active,.btn-soundcloud.active,.open>.dropdown-toggle.btn-soundcloud{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:active,.btn-soundcloud.active,.open>.dropdown-toggle.btn-soundcloud{background-image:none}.btn-soundcloud .badge{color:#f50;background-color:#fff}.btn-tumblr{color:#fff;background-color:#2c4762;border-color:rgba(0,0,0,0.2)}.btn-tumblr:focus,.btn-tumblr.focus{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)}.btn-tumblr:hover{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)}.btn-tumblr:active,.btn-tumblr.active,.open>.dropdown-toggle.btn-tumblr{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)}.btn-tumblr:active,.btn-tumblr.active,.open>.dropdown-toggle.btn-tumblr{background-image:none}.btn-tumblr .badge{color:#2c4762;background-color:#fff}.btn-twitter{color:#fff;background-color:#55acee;border-color:rgba(0,0,0,0.2)}.btn-twitter:focus,.btn-twitter.focus{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)}.btn-twitter:hover{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)}.btn-twitter:active,.btn-twitter.active,.open>.dropdown-toggle.btn-twitter{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)}.btn-twitter:active,.btn-twitter.active,.open>.dropdown-toggle.btn-twitter{background-image:none}.btn-twitter .badge{color:#55acee;background-color:#fff}.btn-vimeo{color:#fff;background-color:#1ab7ea;border-color:rgba(0,0,0,0.2)}.btn-vimeo:focus,.btn-vimeo.focus{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)}.btn-vimeo:hover{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)}.btn-vimeo:active,.btn-vimeo.active,.open>.dropdown-toggle.btn-vimeo{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)}.btn-vimeo:active,.btn-vimeo.active,.open>.dropdown-toggle.btn-vimeo{background-image:none}.btn-vimeo .badge{color:#1ab7ea;background-color:#fff}.btn-vk{color:#fff;background-color:#587ea3;border-color:rgba(0,0,0,0.2)}.btn-vk:focus,.btn-vk.focus{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)}.btn-vk:hover{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)}.btn-vk:active,.btn-vk.active,.open>.dropdown-toggle.btn-vk{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)}.btn-vk:active,.btn-vk.active,.open>.dropdown-toggle.btn-vk{background-image:none}.btn-vk .badge{color:#587ea3;background-color:#fff}.btn-yahoo{color:#fff;background-color:#720e9e;border-color:rgba(0,0,0,0.2)}.btn-yahoo:focus,.btn-yahoo.focus{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)}.btn-yahoo:hover{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)}.btn-yahoo:active,.btn-yahoo.active,.open>.dropdown-toggle.btn-yahoo{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)}.btn-yahoo:active,.btn-yahoo.active,.open>.dropdown-toggle.btn-yahoo{background-image:none}.btn-yahoo .badge{color:#720e9e;background-color:#fff}.fc-button{background:#f4f4f4;background-image:none;color:#444;border-color:#ddd;border-bottom-color:#ddd}.fc-button:hover,.fc-button:active,.fc-button.hover{background-color:#e9e9e9}.fc-header-title h2{font-size:15px;line-height:1.6em;color:#666;margin-left:10px}.fc-header-right{padding-right:10px}.fc-header-left{padding-left:10px}.fc-widget-header{background:#fafafa}.fc-grid{width:100%;border:0}.fc-widget-header:first-of-type,.fc-widget-content:first-of-type{border-left:0;border-right:0}.fc-widget-header:last-of-type,.fc-widget-content:last-of-type{border-right:0}.fc-toolbar{padding:10px;margin:0}.fc-day-number{font-size:20px;font-weight:300;padding-right:10px}.fc-color-picker{list-style:none;margin:0;padding:0}.fc-color-picker>li{float:left;font-size:30px;margin-right:5px;line-height:30px}.fc-color-picker>li .fa{-webkit-transition:-webkit-transform linear .3s;-moz-transition:-moz-transform linear .3s;-o-transition:-o-transform linear .3s;transition:transform linear .3s}.fc-color-picker>li .fa:hover{-webkit-transform:rotate(30deg);-ms-transform:rotate(30deg);-o-transform:rotate(30deg);transform:rotate(30deg)}#add-new-event{-webkit-transition:all linear .3s;-o-transition:all linear .3s;transition:all linear .3s}.external-event{padding:5px 10px;font-weight:bold;margin-bottom:4px;box-shadow:0 1px 1px rgba(0,0,0,0.1);text-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:3px;cursor:move}.external-event:hover{box-shadow:inset 0 0 90px rgba(0,0,0,0.2)}.select2-container--default.select2-container--focus,.select2-selection.select2-container--focus,.select2-container--default:focus,.select2-selection:focus,.select2-container--default:active,.select2-selection:active{outline:none}.select2-container--default .select2-selection--single,.select2-selection .select2-selection--single{border:1px solid #eee;border-radius:0;padding:6px 12px;height:34px}.select2-container--default.select2-container--open{border-color:#d3751c}.select2-dropdown{border:1px solid #eee;border-radius:0}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#d3751c;color:white}.select2-results__option{padding:6px 12px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{padding-left:0;padding-right:0;height:auto;margin-top:-4px}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:6px;padding-left:20px}.select2-container--default .select2-selection--single .select2-selection__arrow{height:28px;right:3px}.select2-container--default .select2-selection--single .select2-selection__arrow b{margin-top:0}.select2-dropdown .select2-search__field,.select2-search--inline .select2-search__field{border:1px solid #eee}.select2-dropdown .select2-search__field:focus,.select2-search--inline .select2-search__field:focus{outline:none;border:1px solid #d3751c}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option[aria-selected=true],.select2-container--default .select2-results__option[aria-selected=true]:hover{color:#444}.select2-container--default .select2-selection--multiple{border:1px solid #eee;border-radius:0}.select2-container--default .select2-selection--multiple:focus{border-color:#d3751c}.select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#eee}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#d3751c;border-color:#bc6919;padding:1px 10px;color:#fff}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{margin-right:5px;color:rgba(255,255,255,0.7)}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container .select2-selection--single .select2-selection__rendered{padding-right:10px}.pad{padding:10px}.margin{margin:10px}.margin-bottom{margin-bottom:20px}.margin-bottom-none{margin-bottom:0}.margin-r-5{margin-right:5px}.inline{display:inline}.description-block{display:block;margin:10px 0;text-align:center}.description-block.margin-bottom{margin-bottom:25px}.description-block>.description-header{margin:0;padding:0;font-weight:600;font-size:16px}.description-block>.description-text{text-transform:uppercase}.bg-red,.bg-yellow,.bg-aqua,.bg-blue,.bg-light-blue,.bg-green,.bg-navy,.bg-teal,.bg-olive,.bg-lime,.bg-orange,.bg-fuchsia,.bg-purple,.bg-maroon,.bg-black,.bg-red-active,.bg-yellow-active,.bg-aqua-active,.bg-blue-active,.bg-light-blue-active,.bg-green-active,.bg-navy-active,.bg-teal-active,.bg-olive-active,.bg-lime-active,.bg-orange-active,.bg-fuchsia-active,.bg-purple-active,.bg-maroon-active,.bg-black-active,.callout.callout-danger,.callout.callout-warning,.callout.callout-info,.callout.callout-success,.alert-success,.alert-danger,.alert-error,.alert-warning,.alert-info,.label-danger,.label-info,.label-warning,.label-primary,.label-success,.modal-primary .modal-body,.modal-primary .modal-header,.modal-primary .modal-footer,.modal-warning .modal-body,.modal-warning .modal-header,.modal-warning .modal-footer,.modal-info .modal-body,.modal-info .modal-header,.modal-info .modal-footer,.modal-success .modal-body,.modal-success .modal-header,.modal-success .modal-footer,.modal-danger .modal-body,.modal-danger .modal-header,.modal-danger .modal-footer{color:#fff !important}.bg-gray{color:#000;background-color:#eee !important}.bg-gray-light{background-color:#f7f7f7}.bg-black{background-color:#222 !important}.bg-red,.callout.callout-danger,.alert-danger,.alert-error,.label-danger,.modal-danger .modal-body{background-color:#ba2d0b !important}.bg-yellow,.callout.callout-warning,.alert-warning,.label-warning,.modal-warning .modal-body{background-color:#d6a136 !important}.bg-aqua,.callout.callout-info,.alert-info,.label-info,.modal-info .modal-body{background-color:#989898 !important}.bg-blue{background-color:#5e99aa !important}.bg-light-blue,.label-primary,.modal-primary .modal-body{background-color:#d3751c !important}.bg-green,.callout.callout-success,.alert-success,.label-success,.modal-success .modal-body{background-color:#418c7a !important}.bg-navy{background-color:#001f3f !important}.bg-teal{background-color:#5e99aa !important}.bg-olive{background-color:#3d9970 !important}.bg-lime{background-color:#01ff70 !important}.bg-orange{background-color:#ff851b !important}.bg-fuchsia{background-color:#f012be !important}.bg-purple{background-color:#001f3f !important}.bg-maroon{background-color:#d81b60 !important}.bg-gray-active{color:#000;background-color:#d5d5d5 !important}.bg-black-active{background-color:#080808 !important}.bg-red-active,.modal-danger .modal-header,.modal-danger .modal-footer{background-color:#9d2609 !important}.bg-yellow-active,.modal-warning .modal-header,.modal-warning .modal-footer{background-color:#c59128 !important}.bg-aqua-active,.modal-info .modal-header,.modal-info .modal-footer{background-color:#898989 !important}.bg-blue-active{background-color:#4a7d8b !important}.bg-light-blue-active,.modal-primary .modal-header,.modal-primary .modal-footer{background-color:#b86618 !important}.bg-green-active,.modal-success .modal-header,.modal-success .modal-footer{background-color:#397b6b !important}.bg-navy-active{background-color:#001a35 !important}.bg-teal-active{background-color:#528c9c !important}.bg-olive-active{background-color:#368763 !important}.bg-lime-active{background-color:#00e765 !important}.bg-orange-active{background-color:#ff7701 !important}.bg-fuchsia-active{background-color:#db0ead !important}.bg-purple-active{background-color:#001226 !important}.bg-maroon-active{background-color:#ca195a !important}[class^="bg-"].disabled{opacity:.65;filter:alpha(opacity=65)}.text-red{color:#ba2d0b !important}.text-yellow{color:#d6a136 !important}.text-aqua{color:#989898 !important}.text-blue{color:#5e99aa !important}.text-black{color:#222 !important}.text-light-blue{color:#d3751c !important}.text-green{color:#418c7a !important}.text-gray{color:#eee !important}.text-navy{color:#001f3f !important}.text-teal{color:#5e99aa !important}.text-olive{color:#3d9970 !important}.text-lime{color:#01ff70 !important}.text-orange{color:#ff851b !important}.text-fuchsia{color:#f012be !important}.text-purple{color:#001f3f !important}.text-maroon{color:#d81b60 !important}.link-muted{color:#a2a2a2}.link-muted:hover,.link-muted:focus{color:#888}.link-black{color:#666}.link-black:hover,.link-black:focus{color:#999}.hide{display:none !important}.no-border{border:0 !important}.no-padding{padding:0 !important}.no-margin{margin:0 !important}.no-shadow{box-shadow:none !important}.list-unstyled,.chart-legend,.contacts-list,.users-list,.mailbox-attachments{list-style:none;margin:0;padding:0}.list-group-unbordered>.list-group-item{border-left:0;border-right:0;border-radius:0;padding-left:0;padding-right:0}.flat{border-radius:0 !important}.text-bold,.text-bold.table td,.text-bold.table th{font-weight:700}.text-sm{font-size:12px}.jqstooltip{padding:5px !important;width:auto !important;height:auto !important}.bg-teal-gradient{background:#5e99aa !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #5e99aa), color-stop(1, #93bbc6)) !important;background:-ms-linear-gradient(bottom, #5e99aa, #93bbc6) !important;background:-moz-linear-gradient(center bottom, #5e99aa 0, #93bbc6 100%) !important;background:-o-linear-gradient(#93bbc6, #5e99aa) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#93bbc6', endColorstr='#5e99aa', GradientType=0) !important;color:#fff}.bg-light-blue-gradient{background:#d3751c !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #d3751c), color-stop(1, #e69446)) !important;background:-ms-linear-gradient(bottom, #d3751c, #e69446) !important;background:-moz-linear-gradient(center bottom, #d3751c 0, #e69446 100%) !important;background:-o-linear-gradient(#e69446, #d3751c) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#e69446', endColorstr='#d3751c', GradientType=0) !important;color:#fff}.bg-blue-gradient{background:#5e99aa !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #5e99aa), color-stop(1, #75a8b6)) !important;background:-ms-linear-gradient(bottom, #5e99aa, #75a8b6) !important;background:-moz-linear-gradient(center bottom, #5e99aa 0, #75a8b6 100%) !important;background:-o-linear-gradient(#75a8b6, #5e99aa) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#75a8b6', endColorstr='#5e99aa', GradientType=0) !important;color:#fff}.bg-aqua-gradient{background:#989898 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #989898), color-stop(1, #aaa)) !important;background:-ms-linear-gradient(bottom, #989898, #aaa) !important;background:-moz-linear-gradient(center bottom, #989898 0, #aaa 100%) !important;background:-o-linear-gradient(#aaa, #989898) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#aaaaaa', endColorstr='#989898', GradientType=0) !important;color:#fff}.bg-yellow-gradient{background:#d6a136 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #d6a136), color-stop(1, #e4c17a)) !important;background:-ms-linear-gradient(bottom, #d6a136, #e4c17a) !important;background:-moz-linear-gradient(center bottom, #d6a136 0, #e4c17a 100%) !important;background:-o-linear-gradient(#e4c17a, #d6a136) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#e4c17a', endColorstr='#d6a136', GradientType=0) !important;color:#fff}.bg-purple-gradient{background:#001f3f !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #001f3f), color-stop(1, #004791)) !important;background:-ms-linear-gradient(bottom, #001f3f, #004791) !important;background:-moz-linear-gradient(center bottom, #001f3f 0, #004791 100%) !important;background:-o-linear-gradient(#004791, #001f3f) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#004791', endColorstr='#001f3f', GradientType=0) !important;color:#fff}.bg-green-gradient{background:#418c7a !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #418c7a), color-stop(1, #4ca48f)) !important;background:-ms-linear-gradient(bottom, #418c7a, #4ca48f) !important;background:-moz-linear-gradient(center bottom, #418c7a 0, #4ca48f 100%) !important;background:-o-linear-gradient(#4ca48f, #418c7a) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#4ca48f', endColorstr='#418c7a', GradientType=0) !important;color:#fff}.bg-red-gradient{background:#ba2d0b !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #ba2d0b), color-stop(1, #ea390e)) !important;background:-ms-linear-gradient(bottom, #ba2d0b, #ea390e) !important;background:-moz-linear-gradient(center bottom, #ba2d0b 0, #ea390e 100%) !important;background:-o-linear-gradient(#ea390e, #ba2d0b) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ea390e', endColorstr='#ba2d0b', GradientType=0) !important;color:#fff}.bg-black-gradient{background:#222 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #222), color-stop(1, #3c3c3c)) !important;background:-ms-linear-gradient(bottom, #222, #3c3c3c) !important;background:-moz-linear-gradient(center bottom, #222 0, #3c3c3c 100%) !important;background:-o-linear-gradient(#3c3c3c, #222) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#3c3c3c', endColorstr='#222222', GradientType=0) !important;color:#fff}.bg-maroon-gradient{background:#d81b60 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #d81b60), color-stop(1, #e73f7c)) !important;background:-ms-linear-gradient(bottom, #d81b60, #e73f7c) !important;background:-moz-linear-gradient(center bottom, #d81b60 0, #e73f7c 100%) !important;background:-o-linear-gradient(#e73f7c, #d81b60) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#e73f7c', endColorstr='#d81b60', GradientType=0) !important;color:#fff}.description-block .description-icon{font-size:16px}.no-pad-top{padding-top:0}.position-static{position:static !important}.list-header{font-size:15px;padding:10px 4px;font-weight:bold;color:#666}.list-seperator{height:1px;background:#f4f4f4;margin:15px 0 9px 0}.list-link>a{padding:4px;color:#777}.list-link>a:hover{color:#222}.font-light{font-weight:300}.user-block:before,.user-block:after{content:" ";display:table}.user-block:after{clear:both}.user-block img{width:40px;height:40px;float:left}.user-block .username,.user-block .description,.user-block .comment{display:block;margin-left:50px}.user-block .username{font-size:16px;font-weight:600}.user-block .description{color:#999;font-size:13px}.user-block.user-block-sm .username,.user-block.user-block-sm .description,.user-block.user-block-sm .comment{margin-left:40px}.user-block.user-block-sm .username{font-size:14px}.img-sm,.img-md,.img-lg,.box-comments .box-comment img,.user-block.user-block-sm img{float:left}.img-sm,.box-comments .box-comment img,.user-block.user-block-sm img{width:30px !important;height:30px !important}.img-sm+.img-push{margin-left:40px}.img-md{width:60px;height:60px}.img-md+.img-push{margin-left:70px}.img-lg{width:100px;height:100px}.img-lg+.img-push{margin-left:110px}.img-bordered{border:3px solid #eee;padding:3px}.img-bordered-sm{border:2px solid #eee;padding:2px}.attachment-block{border:1px solid #f4f4f4;padding:5px;margin-bottom:10px;background:#f7f7f7}.attachment-block .attachment-img{max-width:100px;max-height:100px;height:auto;float:left}.attachment-block .attachment-pushed{margin-left:110px}.attachment-block .attachment-heading{margin:0}.attachment-block .attachment-text{color:#555}.connectedSortable{min-height:100px}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sort-highlight{background:#f4f4f4;border:1px dashed #ddd;margin-bottom:10px}.full-opacity-hover{opacity:.65;filter:alpha(opacity=65)}.full-opacity-hover:hover{opacity:1;filter:alpha(opacity=100)}.chart{position:relative;overflow:hidden;width:100%}.chart svg,.chart canvas{width:100% !important}@media print{.no-print,.main-sidebar,.left-side,.main-header,.content-header{display:none !important}.content-wrapper,.right-side,.main-footer{margin-left:0 !important;min-height:0 !important;-webkit-transform:translate(0, 0) !important;-ms-transform:translate(0, 0) !important;-o-transform:translate(0, 0) !important;transform:translate(0, 0) !important}.fixed .content-wrapper,.fixed .right-side{padding-top:0 !important}.invoice{width:100%;border:0;margin:0;padding:0}.invoice-col{float:left;width:33.3333333%}.table-responsive{overflow:auto}.table-responsive>.table tr th,.table-responsive>.table tr td{white-space:normal !important}}
\ No newline at end of file
diff --git a/www/GerkeLab.png b/www/GerkeLab.png
new file mode 100644
index 0000000..0871602
Binary files /dev/null and b/www/GerkeLab.png differ
diff --git a/www/_all-skins.gerkelab.min.css b/www/_all-skins.gerkelab.min.css
new file mode 100644
index 0000000..e138c4a
--- /dev/null
+++ b/www/_all-skins.gerkelab.min.css
@@ -0,0 +1 @@
+.skin-blue .main-header .navbar{background-color:#d3751c}.skin-blue .main-header .navbar .nav>li>a{color:#fff}.skin-blue .main-header .navbar .nav>li>a:hover,.skin-blue .main-header .navbar .nav>li>a:active,.skin-blue .main-header .navbar .nav>li>a:focus,.skin-blue .main-header .navbar .nav .open>a,.skin-blue .main-header .navbar .nav .open>a:hover,.skin-blue .main-header .navbar .nav .open>a:focus,.skin-blue .main-header .navbar .nav>.active>a{background:rgba(0,0,0,0.1);color:#f6f6f6}.skin-blue .main-header .navbar .sidebar-toggle{color:#fff}.skin-blue .main-header .navbar .sidebar-toggle:hover{color:#f6f6f6;background:rgba(0,0,0,0.1)}.skin-blue .main-header .navbar .sidebar-toggle{color:#fff}.skin-blue .main-header .navbar .sidebar-toggle:hover{background-color:#bc6919}@media (max-width:767px){.skin-blue .main-header .navbar .dropdown-menu li.divider{background-color:rgba(255,255,255,0.1)}.skin-blue .main-header .navbar .dropdown-menu li a{color:#fff}.skin-blue .main-header .navbar .dropdown-menu li a:hover{background:#bc6919}}.skin-blue .main-header .logo{background-color:#bc6919;color:#fff;border-bottom:0 solid transparent}.skin-blue .main-header .logo:hover{background-color:#b86618}.skin-blue .main-header li.user-header{background-color:#d3751c}.skin-blue .content-header{background:transparent}.skin-blue .wrapper,.skin-blue .main-sidebar,.skin-blue .left-side{background-color:#313439}.skin-blue .user-panel>.info,.skin-blue .user-panel>.info>a{color:#fff}.skin-blue .sidebar-menu>li.header{color:#606670;background:#282a2e}.skin-blue .sidebar-menu>li>a{border-left:3px solid transparent}.skin-blue .sidebar-menu>li:hover>a,.skin-blue .sidebar-menu>li.active>a{color:#fff;background:#2c2f34;border-left-color:#d3751c}.skin-blue .sidebar-menu>li>.treeview-menu{margin:0 1px;background:#3d4147}.skin-blue .sidebar a{color:#cacdd2}.skin-blue .sidebar a:hover{text-decoration:none}.skin-blue .treeview-menu>li>a{color:#a1a6ae}.skin-blue .treeview-menu>li.active>a,.skin-blue .treeview-menu>li>a:hover{color:#fff}.skin-blue .sidebar-form{border-radius:3px;border:1px solid #494d54;margin:10px 10px}.skin-blue .sidebar-form input[type="text"],.skin-blue .sidebar-form .btn{box-shadow:none;background-color:#494d54;border:1px solid transparent;height:35px}.skin-blue .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-blue .sidebar-form input[type="text"]:focus,.skin-blue .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-blue .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-blue .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}.skin-blue.layout-top-nav .main-header>.logo{background-color:#d3751c;color:#fff;border-bottom:0 solid transparent}.skin-blue.layout-top-nav .main-header>.logo:hover{background-color:#ce731b}.skin-blue-light .main-header .navbar{background-color:#d3751c}.skin-blue-light .main-header .navbar .nav>li>a{color:#fff}.skin-blue-light .main-header .navbar .nav>li>a:hover,.skin-blue-light .main-header .navbar .nav>li>a:active,.skin-blue-light .main-header .navbar .nav>li>a:focus,.skin-blue-light .main-header .navbar .nav .open>a,.skin-blue-light .main-header .navbar .nav .open>a:hover,.skin-blue-light .main-header .navbar .nav .open>a:focus,.skin-blue-light .main-header .navbar .nav>.active>a{background:rgba(0,0,0,0.1);color:#f6f6f6}.skin-blue-light .main-header .navbar .sidebar-toggle{color:#fff}.skin-blue-light .main-header .navbar .sidebar-toggle:hover{color:#f6f6f6;background:rgba(0,0,0,0.1)}.skin-blue-light .main-header .navbar .sidebar-toggle{color:#fff}.skin-blue-light .main-header .navbar .sidebar-toggle:hover{background-color:#bc6919}@media (max-width:767px){.skin-blue-light .main-header .navbar .dropdown-menu li.divider{background-color:rgba(255,255,255,0.1)}.skin-blue-light .main-header .navbar .dropdown-menu li a{color:#fff}.skin-blue-light .main-header .navbar .dropdown-menu li a:hover{background:#bc6919}}.skin-blue-light .main-header .logo{background-color:#d3751c;color:#fff;border-bottom:0 solid transparent}.skin-blue-light .main-header .logo:hover{background-color:#ce731b}.skin-blue-light .main-header li.user-header{background-color:#d3751c}.skin-blue-light .content-header{background:transparent}.skin-blue-light .wrapper,.skin-blue-light .main-sidebar,.skin-blue-light .left-side{background-color:#e0e0e0}.skin-blue-light .content-wrapper,.skin-blue-light .main-footer{border-left:1px solid #eee}.skin-blue-light .user-panel>.info,.skin-blue-light .user-panel>.info>a{color:#616161}.skin-blue-light .sidebar-menu>li{-webkit-transition:border-left-color .3s ease;-o-transition:border-left-color .3s ease;transition:border-left-color .3s ease}.skin-blue-light .sidebar-menu>li.header{color:#a0a0a0;background:#e0e0e0}.skin-blue-light .sidebar-menu>li>a{border-left:3px solid transparent;font-weight:600}.skin-blue-light .sidebar-menu>li:hover>a,.skin-blue-light .sidebar-menu>li.active>a{color:#212121;background:#e4e4e4}.skin-blue-light .sidebar-menu>li.active{border-left-color:#d3751c}.skin-blue-light .sidebar-menu>li.active>a{font-weight:600}.skin-blue-light .sidebar-menu>li>.treeview-menu{background:#e4e4e4}.skin-blue-light .sidebar a{color:#616161}.skin-blue-light .sidebar a:hover{text-decoration:none}.skin-blue-light .treeview-menu>li>a{color:#7a7a7a}.skin-blue-light .treeview-menu>li.active>a,.skin-blue-light .treeview-menu>li>a:hover{color:#000}.skin-blue-light .treeview-menu>li.active>a{font-weight:600}.skin-blue-light .sidebar-form{border-radius:3px;border:1px solid #eee;margin:10px 10px}.skin-blue-light .sidebar-form input[type="text"],.skin-blue-light .sidebar-form .btn{box-shadow:none;background-color:#fff;border:1px solid transparent;height:35px}.skin-blue-light .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-blue-light .sidebar-form input[type="text"]:focus,.skin-blue-light .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-blue-light .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-blue-light .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}@media (min-width:768px){.skin-blue-light.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu{border-left:1px solid #eee}}.skin-blue-light .main-footer{border-top-color:#eee}.skin-blue.layout-top-nav .main-header>.logo{background-color:#d3751c;color:#fff;border-bottom:0 solid transparent}.skin-blue.layout-top-nav .main-header>.logo:hover{background-color:#ce731b}.skin-black .main-header{-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.skin-black .main-header .navbar-toggle{color:#333}.skin-black .main-header .navbar-brand{color:#333;border-right:1px solid #eee}.skin-black .main-header .navbar{background-color:#fff}.skin-black .main-header .navbar .nav>li>a{color:#333}.skin-black .main-header .navbar .nav>li>a:hover,.skin-black .main-header .navbar .nav>li>a:active,.skin-black .main-header .navbar .nav>li>a:focus,.skin-black .main-header .navbar .nav .open>a,.skin-black .main-header .navbar .nav .open>a:hover,.skin-black .main-header .navbar .nav .open>a:focus,.skin-black .main-header .navbar .nav>.active>a{background:#fff;color:#999}.skin-black .main-header .navbar .sidebar-toggle{color:#333}.skin-black .main-header .navbar .sidebar-toggle:hover{color:#999;background:#fff}.skin-black .main-header .navbar>.sidebar-toggle{color:#333;border-right:1px solid #eee}.skin-black .main-header .navbar .navbar-nav>li>a{border-right:1px solid #eee}.skin-black .main-header .navbar .navbar-custom-menu .navbar-nav>li>a,.skin-black .main-header .navbar .navbar-right>li>a{border-left:1px solid #eee;border-right-width:0}.skin-black .main-header>.logo{background-color:#fff;color:#333;border-bottom:0 solid transparent;border-right:1px solid #eee}.skin-black .main-header>.logo:hover{background-color:#fcfcfc}@media (max-width:767px){.skin-black .main-header>.logo{background-color:#222;color:#fff;border-bottom:0 solid transparent;border-right:none}.skin-black .main-header>.logo:hover{background-color:#1f1f1f}}.skin-black .main-header li.user-header{background-color:#222}.skin-black .content-header{background:transparent;box-shadow:none}.skin-black .wrapper,.skin-black .main-sidebar,.skin-black .left-side{background-color:#313439}.skin-black .user-panel>.info,.skin-black .user-panel>.info>a{color:#fff}.skin-black .sidebar-menu>li.header{color:#606670;background:#282a2e}.skin-black .sidebar-menu>li>a{border-left:3px solid transparent}.skin-black .sidebar-menu>li:hover>a,.skin-black .sidebar-menu>li.active>a{color:#fff;background:#2c2f34;border-left-color:#fff}.skin-black .sidebar-menu>li>.treeview-menu{margin:0 1px;background:#3d4147}.skin-black .sidebar a{color:#cacdd2}.skin-black .sidebar a:hover{text-decoration:none}.skin-black .treeview-menu>li>a{color:#a1a6ae}.skin-black .treeview-menu>li.active>a,.skin-black .treeview-menu>li>a:hover{color:#fff}.skin-black .sidebar-form{border-radius:3px;border:1px solid #494d54;margin:10px 10px}.skin-black .sidebar-form input[type="text"],.skin-black .sidebar-form .btn{box-shadow:none;background-color:#494d54;border:1px solid transparent;height:35px}.skin-black .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-black .sidebar-form input[type="text"]:focus,.skin-black .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-black .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-black .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}.skin-black .pace .pace-progress{background:#222}.skin-black .pace .pace-activity{border-top-color:#222;border-left-color:#222}.skin-black-light .main-header{-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.skin-black-light .main-header .navbar-toggle{color:#333}.skin-black-light .main-header .navbar-brand{color:#333;border-right:1px solid #eee}.skin-black-light .main-header .navbar{background-color:#fff}.skin-black-light .main-header .navbar .nav>li>a{color:#333}.skin-black-light .main-header .navbar .nav>li>a:hover,.skin-black-light .main-header .navbar .nav>li>a:active,.skin-black-light .main-header .navbar .nav>li>a:focus,.skin-black-light .main-header .navbar .nav .open>a,.skin-black-light .main-header .navbar .nav .open>a:hover,.skin-black-light .main-header .navbar .nav .open>a:focus,.skin-black-light .main-header .navbar .nav>.active>a{background:#fff;color:#999}.skin-black-light .main-header .navbar .sidebar-toggle{color:#333}.skin-black-light .main-header .navbar .sidebar-toggle:hover{color:#999;background:#fff}.skin-black-light .main-header .navbar>.sidebar-toggle{color:#333;border-right:1px solid #eee}.skin-black-light .main-header .navbar .navbar-nav>li>a{border-right:1px solid #eee}.skin-black-light .main-header .navbar .navbar-custom-menu .navbar-nav>li>a,.skin-black-light .main-header .navbar .navbar-right>li>a{border-left:1px solid #eee;border-right-width:0}.skin-black-light .main-header>.logo{background-color:#fff;color:#333;border-bottom:0 solid transparent;border-right:1px solid #eee}.skin-black-light .main-header>.logo:hover{background-color:#fcfcfc}@media (max-width:767px){.skin-black-light .main-header>.logo{background-color:#222;color:#fff;border-bottom:0 solid transparent;border-right:none}.skin-black-light .main-header>.logo:hover{background-color:#1f1f1f}}.skin-black-light .main-header li.user-header{background-color:#222}.skin-black-light .content-header{background:transparent;box-shadow:none}.skin-black-light .wrapper,.skin-black-light .main-sidebar,.skin-black-light .left-side{background-color:#e0e0e0}.skin-black-light .content-wrapper,.skin-black-light .main-footer{border-left:1px solid #eee}.skin-black-light .user-panel>.info,.skin-black-light .user-panel>.info>a{color:#616161}.skin-black-light .sidebar-menu>li{-webkit-transition:border-left-color .3s ease;-o-transition:border-left-color .3s ease;transition:border-left-color .3s ease}.skin-black-light .sidebar-menu>li.header{color:#a0a0a0;background:#e0e0e0}.skin-black-light .sidebar-menu>li>a{border-left:3px solid transparent;font-weight:600}.skin-black-light .sidebar-menu>li:hover>a,.skin-black-light .sidebar-menu>li.active>a{color:#212121;background:#e4e4e4}.skin-black-light .sidebar-menu>li.active{border-left-color:#fff}.skin-black-light .sidebar-menu>li.active>a{font-weight:600}.skin-black-light .sidebar-menu>li>.treeview-menu{background:#e4e4e4}.skin-black-light .sidebar a{color:#616161}.skin-black-light .sidebar a:hover{text-decoration:none}.skin-black-light .treeview-menu>li>a{color:#7a7a7a}.skin-black-light .treeview-menu>li.active>a,.skin-black-light .treeview-menu>li>a:hover{color:#000}.skin-black-light .treeview-menu>li.active>a{font-weight:600}.skin-black-light .sidebar-form{border-radius:3px;border:1px solid #eee;margin:10px 10px}.skin-black-light .sidebar-form input[type="text"],.skin-black-light .sidebar-form .btn{box-shadow:none;background-color:#fff;border:1px solid transparent;height:35px}.skin-black-light .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-black-light .sidebar-form input[type="text"]:focus,.skin-black-light .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-black-light .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-black-light .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}@media (min-width:768px){.skin-black-light.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu{border-left:1px solid #eee}}.skin-green .main-header .navbar{background-color:#418c7a}.skin-green .main-header .navbar .nav>li>a{color:#fff}.skin-green .main-header .navbar .nav>li>a:hover,.skin-green .main-header .navbar .nav>li>a:active,.skin-green .main-header .navbar .nav>li>a:focus,.skin-green .main-header .navbar .nav .open>a,.skin-green .main-header .navbar .nav .open>a:hover,.skin-green .main-header .navbar .nav .open>a:focus,.skin-green .main-header .navbar .nav>.active>a{background:rgba(0,0,0,0.1);color:#f6f6f6}.skin-green .main-header .navbar .sidebar-toggle{color:#fff}.skin-green .main-header .navbar .sidebar-toggle:hover{color:#f6f6f6;background:rgba(0,0,0,0.1)}.skin-green .main-header .navbar .sidebar-toggle{color:#fff}.skin-green .main-header .navbar .sidebar-toggle:hover{background-color:#397b6b}@media (max-width:767px){.skin-green .main-header .navbar .dropdown-menu li.divider{background-color:rgba(255,255,255,0.1)}.skin-green .main-header .navbar .dropdown-menu li a{color:#fff}.skin-green .main-header .navbar .dropdown-menu li a:hover{background:#397b6b}}.skin-green .main-header .logo{background-color:#397b6b;color:#fff;border-bottom:0 solid transparent}.skin-green .main-header .logo:hover{background-color:#377768}.skin-green .main-header li.user-header{background-color:#418c7a}.skin-green .content-header{background:transparent}.skin-green .wrapper,.skin-green .main-sidebar,.skin-green .left-side{background-color:#313439}.skin-green .user-panel>.info,.skin-green .user-panel>.info>a{color:#fff}.skin-green .sidebar-menu>li.header{color:#606670;background:#282a2e}.skin-green .sidebar-menu>li>a{border-left:3px solid transparent}.skin-green .sidebar-menu>li:hover>a,.skin-green .sidebar-menu>li.active>a{color:#fff;background:#2c2f34;border-left-color:#418c7a}.skin-green .sidebar-menu>li>.treeview-menu{margin:0 1px;background:#3d4147}.skin-green .sidebar a{color:#cacdd2}.skin-green .sidebar a:hover{text-decoration:none}.skin-green .treeview-menu>li>a{color:#a1a6ae}.skin-green .treeview-menu>li.active>a,.skin-green .treeview-menu>li>a:hover{color:#fff}.skin-green .sidebar-form{border-radius:3px;border:1px solid #494d54;margin:10px 10px}.skin-green .sidebar-form input[type="text"],.skin-green .sidebar-form .btn{box-shadow:none;background-color:#494d54;border:1px solid transparent;height:35px}.skin-green .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-green .sidebar-form input[type="text"]:focus,.skin-green .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-green .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-green .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}.skin-green-light .main-header .navbar{background-color:#418c7a}.skin-green-light .main-header .navbar .nav>li>a{color:#fff}.skin-green-light .main-header .navbar .nav>li>a:hover,.skin-green-light .main-header .navbar .nav>li>a:active,.skin-green-light .main-header .navbar .nav>li>a:focus,.skin-green-light .main-header .navbar .nav .open>a,.skin-green-light .main-header .navbar .nav .open>a:hover,.skin-green-light .main-header .navbar .nav .open>a:focus,.skin-green-light .main-header .navbar .nav>.active>a{background:rgba(0,0,0,0.1);color:#f6f6f6}.skin-green-light .main-header .navbar .sidebar-toggle{color:#fff}.skin-green-light .main-header .navbar .sidebar-toggle:hover{color:#f6f6f6;background:rgba(0,0,0,0.1)}.skin-green-light .main-header .navbar .sidebar-toggle{color:#fff}.skin-green-light .main-header .navbar .sidebar-toggle:hover{background-color:#397b6b}@media (max-width:767px){.skin-green-light .main-header .navbar .dropdown-menu li.divider{background-color:rgba(255,255,255,0.1)}.skin-green-light .main-header .navbar .dropdown-menu li a{color:#fff}.skin-green-light .main-header .navbar .dropdown-menu li a:hover{background:#397b6b}}.skin-green-light .main-header .logo{background-color:#418c7a;color:#fff;border-bottom:0 solid transparent}.skin-green-light .main-header .logo:hover{background-color:#3f8977}.skin-green-light .main-header li.user-header{background-color:#418c7a}.skin-green-light .content-header{background:transparent}.skin-green-light .wrapper,.skin-green-light .main-sidebar,.skin-green-light .left-side{background-color:#e0e0e0}.skin-green-light .content-wrapper,.skin-green-light .main-footer{border-left:1px solid #eee}.skin-green-light .user-panel>.info,.skin-green-light .user-panel>.info>a{color:#616161}.skin-green-light .sidebar-menu>li{-webkit-transition:border-left-color .3s ease;-o-transition:border-left-color .3s ease;transition:border-left-color .3s ease}.skin-green-light .sidebar-menu>li.header{color:#a0a0a0;background:#e0e0e0}.skin-green-light .sidebar-menu>li>a{border-left:3px solid transparent;font-weight:600}.skin-green-light .sidebar-menu>li:hover>a,.skin-green-light .sidebar-menu>li.active>a{color:#212121;background:#e4e4e4}.skin-green-light .sidebar-menu>li.active{border-left-color:#418c7a}.skin-green-light .sidebar-menu>li.active>a{font-weight:600}.skin-green-light .sidebar-menu>li>.treeview-menu{background:#e4e4e4}.skin-green-light .sidebar a{color:#616161}.skin-green-light .sidebar a:hover{text-decoration:none}.skin-green-light .treeview-menu>li>a{color:#7a7a7a}.skin-green-light .treeview-menu>li.active>a,.skin-green-light .treeview-menu>li>a:hover{color:#000}.skin-green-light .treeview-menu>li.active>a{font-weight:600}.skin-green-light .sidebar-form{border-radius:3px;border:1px solid #eee;margin:10px 10px}.skin-green-light .sidebar-form input[type="text"],.skin-green-light .sidebar-form .btn{box-shadow:none;background-color:#fff;border:1px solid transparent;height:35px}.skin-green-light .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-green-light .sidebar-form input[type="text"]:focus,.skin-green-light .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-green-light .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-green-light .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}@media (min-width:768px){.skin-green-light.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu{border-left:1px solid #eee}}.skin-red .main-header .navbar{background-color:#ba2d0b}.skin-red .main-header .navbar .nav>li>a{color:#fff}.skin-red .main-header .navbar .nav>li>a:hover,.skin-red .main-header .navbar .nav>li>a:active,.skin-red .main-header .navbar .nav>li>a:focus,.skin-red .main-header .navbar .nav .open>a,.skin-red .main-header .navbar .nav .open>a:hover,.skin-red .main-header .navbar .nav .open>a:focus,.skin-red .main-header .navbar .nav>.active>a{background:rgba(0,0,0,0.1);color:#f6f6f6}.skin-red .main-header .navbar .sidebar-toggle{color:#fff}.skin-red .main-header .navbar .sidebar-toggle:hover{color:#f6f6f6;background:rgba(0,0,0,0.1)}.skin-red .main-header .navbar .sidebar-toggle{color:#fff}.skin-red .main-header .navbar .sidebar-toggle:hover{background-color:#a2270a}@media (max-width:767px){.skin-red .main-header .navbar .dropdown-menu li.divider{background-color:rgba(255,255,255,0.1)}.skin-red .main-header .navbar .dropdown-menu li a{color:#fff}.skin-red .main-header .navbar .dropdown-menu li a:hover{background:#a2270a}}.skin-red .main-header .logo{background-color:#a2270a;color:#fff;border-bottom:0 solid transparent}.skin-red .main-header .logo:hover{background-color:#9d2609}.skin-red .main-header li.user-header{background-color:#ba2d0b}.skin-red .content-header{background:transparent}.skin-red .wrapper,.skin-red .main-sidebar,.skin-red .left-side{background-color:#313439}.skin-red .user-panel>.info,.skin-red .user-panel>.info>a{color:#fff}.skin-red .sidebar-menu>li.header{color:#606670;background:#282a2e}.skin-red .sidebar-menu>li>a{border-left:3px solid transparent}.skin-red .sidebar-menu>li:hover>a,.skin-red .sidebar-menu>li.active>a{color:#fff;background:#2c2f34;border-left-color:#ba2d0b}.skin-red .sidebar-menu>li>.treeview-menu{margin:0 1px;background:#3d4147}.skin-red .sidebar a{color:#cacdd2}.skin-red .sidebar a:hover{text-decoration:none}.skin-red .treeview-menu>li>a{color:#a1a6ae}.skin-red .treeview-menu>li.active>a,.skin-red .treeview-menu>li>a:hover{color:#fff}.skin-red .sidebar-form{border-radius:3px;border:1px solid #494d54;margin:10px 10px}.skin-red .sidebar-form input[type="text"],.skin-red .sidebar-form .btn{box-shadow:none;background-color:#494d54;border:1px solid transparent;height:35px}.skin-red .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-red .sidebar-form input[type="text"]:focus,.skin-red .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-red .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-red .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}.skin-red-light .main-header .navbar{background-color:#ba2d0b}.skin-red-light .main-header .navbar .nav>li>a{color:#fff}.skin-red-light .main-header .navbar .nav>li>a:hover,.skin-red-light .main-header .navbar .nav>li>a:active,.skin-red-light .main-header .navbar .nav>li>a:focus,.skin-red-light .main-header .navbar .nav .open>a,.skin-red-light .main-header .navbar .nav .open>a:hover,.skin-red-light .main-header .navbar .nav .open>a:focus,.skin-red-light .main-header .navbar .nav>.active>a{background:rgba(0,0,0,0.1);color:#f6f6f6}.skin-red-light .main-header .navbar .sidebar-toggle{color:#fff}.skin-red-light .main-header .navbar .sidebar-toggle:hover{color:#f6f6f6;background:rgba(0,0,0,0.1)}.skin-red-light .main-header .navbar .sidebar-toggle{color:#fff}.skin-red-light .main-header .navbar .sidebar-toggle:hover{background-color:#a2270a}@media (max-width:767px){.skin-red-light .main-header .navbar .dropdown-menu li.divider{background-color:rgba(255,255,255,0.1)}.skin-red-light .main-header .navbar .dropdown-menu li a{color:#fff}.skin-red-light .main-header .navbar .dropdown-menu li a:hover{background:#a2270a}}.skin-red-light .main-header .logo{background-color:#ba2d0b;color:#fff;border-bottom:0 solid transparent}.skin-red-light .main-header .logo:hover{background-color:#b52c0b}.skin-red-light .main-header li.user-header{background-color:#ba2d0b}.skin-red-light .content-header{background:transparent}.skin-red-light .wrapper,.skin-red-light .main-sidebar,.skin-red-light .left-side{background-color:#e0e0e0}.skin-red-light .content-wrapper,.skin-red-light .main-footer{border-left:1px solid #eee}.skin-red-light .user-panel>.info,.skin-red-light .user-panel>.info>a{color:#616161}.skin-red-light .sidebar-menu>li{-webkit-transition:border-left-color .3s ease;-o-transition:border-left-color .3s ease;transition:border-left-color .3s ease}.skin-red-light .sidebar-menu>li.header{color:#a0a0a0;background:#e0e0e0}.skin-red-light .sidebar-menu>li>a{border-left:3px solid transparent;font-weight:600}.skin-red-light .sidebar-menu>li:hover>a,.skin-red-light .sidebar-menu>li.active>a{color:#212121;background:#e4e4e4}.skin-red-light .sidebar-menu>li.active{border-left-color:#ba2d0b}.skin-red-light .sidebar-menu>li.active>a{font-weight:600}.skin-red-light .sidebar-menu>li>.treeview-menu{background:#e4e4e4}.skin-red-light .sidebar a{color:#616161}.skin-red-light .sidebar a:hover{text-decoration:none}.skin-red-light .treeview-menu>li>a{color:#7a7a7a}.skin-red-light .treeview-menu>li.active>a,.skin-red-light .treeview-menu>li>a:hover{color:#000}.skin-red-light .treeview-menu>li.active>a{font-weight:600}.skin-red-light .sidebar-form{border-radius:3px;border:1px solid #eee;margin:10px 10px}.skin-red-light .sidebar-form input[type="text"],.skin-red-light .sidebar-form .btn{box-shadow:none;background-color:#fff;border:1px solid transparent;height:35px}.skin-red-light .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-red-light .sidebar-form input[type="text"]:focus,.skin-red-light .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-red-light .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-red-light .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}@media (min-width:768px){.skin-red-light.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu{border-left:1px solid #eee}}.skin-yellow .main-header .navbar{background-color:#d6a136}.skin-yellow .main-header .navbar .nav>li>a{color:#fff}.skin-yellow .main-header .navbar .nav>li>a:hover,.skin-yellow .main-header .navbar .nav>li>a:active,.skin-yellow .main-header .navbar .nav>li>a:focus,.skin-yellow .main-header .navbar .nav .open>a,.skin-yellow .main-header .navbar .nav .open>a:hover,.skin-yellow .main-header .navbar .nav .open>a:focus,.skin-yellow .main-header .navbar .nav>.active>a{background:rgba(0,0,0,0.1);color:#f6f6f6}.skin-yellow .main-header .navbar .sidebar-toggle{color:#fff}.skin-yellow .main-header .navbar .sidebar-toggle:hover{color:#f6f6f6;background:rgba(0,0,0,0.1)}.skin-yellow .main-header .navbar .sidebar-toggle{color:#fff}.skin-yellow .main-header .navbar .sidebar-toggle:hover{background-color:#c99429}@media (max-width:767px){.skin-yellow .main-header .navbar .dropdown-menu li.divider{background-color:rgba(255,255,255,0.1)}.skin-yellow .main-header .navbar .dropdown-menu li a{color:#fff}.skin-yellow .main-header .navbar .dropdown-menu li a:hover{background:#c99429}}.skin-yellow .main-header .logo{background-color:#c99429;color:#fff;border-bottom:0 solid transparent}.skin-yellow .main-header .logo:hover{background-color:#c59128}.skin-yellow .main-header li.user-header{background-color:#d6a136}.skin-yellow .content-header{background:transparent}.skin-yellow .wrapper,.skin-yellow .main-sidebar,.skin-yellow .left-side{background-color:#313439}.skin-yellow .user-panel>.info,.skin-yellow .user-panel>.info>a{color:#fff}.skin-yellow .sidebar-menu>li.header{color:#606670;background:#282a2e}.skin-yellow .sidebar-menu>li>a{border-left:3px solid transparent}.skin-yellow .sidebar-menu>li:hover>a,.skin-yellow .sidebar-menu>li.active>a{color:#fff;background:#2c2f34;border-left-color:#d6a136}.skin-yellow .sidebar-menu>li>.treeview-menu{margin:0 1px;background:#3d4147}.skin-yellow .sidebar a{color:#cacdd2}.skin-yellow .sidebar a:hover{text-decoration:none}.skin-yellow .treeview-menu>li>a{color:#a1a6ae}.skin-yellow .treeview-menu>li.active>a,.skin-yellow .treeview-menu>li>a:hover{color:#fff}.skin-yellow .sidebar-form{border-radius:3px;border:1px solid #494d54;margin:10px 10px}.skin-yellow .sidebar-form input[type="text"],.skin-yellow .sidebar-form .btn{box-shadow:none;background-color:#494d54;border:1px solid transparent;height:35px}.skin-yellow .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-yellow .sidebar-form input[type="text"]:focus,.skin-yellow .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-yellow .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-yellow .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}.skin-yellow-light .main-header .navbar{background-color:#d6a136}.skin-yellow-light .main-header .navbar .nav>li>a{color:#fff}.skin-yellow-light .main-header .navbar .nav>li>a:hover,.skin-yellow-light .main-header .navbar .nav>li>a:active,.skin-yellow-light .main-header .navbar .nav>li>a:focus,.skin-yellow-light .main-header .navbar .nav .open>a,.skin-yellow-light .main-header .navbar .nav .open>a:hover,.skin-yellow-light .main-header .navbar .nav .open>a:focus,.skin-yellow-light .main-header .navbar .nav>.active>a{background:rgba(0,0,0,0.1);color:#f6f6f6}.skin-yellow-light .main-header .navbar .sidebar-toggle{color:#fff}.skin-yellow-light .main-header .navbar .sidebar-toggle:hover{color:#f6f6f6;background:rgba(0,0,0,0.1)}.skin-yellow-light .main-header .navbar .sidebar-toggle{color:#fff}.skin-yellow-light .main-header .navbar .sidebar-toggle:hover{background-color:#c99429}@media (max-width:767px){.skin-yellow-light .main-header .navbar .dropdown-menu li.divider{background-color:rgba(255,255,255,0.1)}.skin-yellow-light .main-header .navbar .dropdown-menu li a{color:#fff}.skin-yellow-light .main-header .navbar .dropdown-menu li a:hover{background:#c99429}}.skin-yellow-light .main-header .logo{background-color:#d6a136;color:#fff;border-bottom:0 solid transparent}.skin-yellow-light .main-header .logo:hover{background-color:#d59f32}.skin-yellow-light .main-header li.user-header{background-color:#d6a136}.skin-yellow-light .content-header{background:transparent}.skin-yellow-light .wrapper,.skin-yellow-light .main-sidebar,.skin-yellow-light .left-side{background-color:#e0e0e0}.skin-yellow-light .content-wrapper,.skin-yellow-light .main-footer{border-left:1px solid #eee}.skin-yellow-light .user-panel>.info,.skin-yellow-light .user-panel>.info>a{color:#616161}.skin-yellow-light .sidebar-menu>li{-webkit-transition:border-left-color .3s ease;-o-transition:border-left-color .3s ease;transition:border-left-color .3s ease}.skin-yellow-light .sidebar-menu>li.header{color:#a0a0a0;background:#e0e0e0}.skin-yellow-light .sidebar-menu>li>a{border-left:3px solid transparent;font-weight:600}.skin-yellow-light .sidebar-menu>li:hover>a,.skin-yellow-light .sidebar-menu>li.active>a{color:#212121;background:#e4e4e4}.skin-yellow-light .sidebar-menu>li.active{border-left-color:#d6a136}.skin-yellow-light .sidebar-menu>li.active>a{font-weight:600}.skin-yellow-light .sidebar-menu>li>.treeview-menu{background:#e4e4e4}.skin-yellow-light .sidebar a{color:#616161}.skin-yellow-light .sidebar a:hover{text-decoration:none}.skin-yellow-light .treeview-menu>li>a{color:#7a7a7a}.skin-yellow-light .treeview-menu>li.active>a,.skin-yellow-light .treeview-menu>li>a:hover{color:#000}.skin-yellow-light .treeview-menu>li.active>a{font-weight:600}.skin-yellow-light .sidebar-form{border-radius:3px;border:1px solid #eee;margin:10px 10px}.skin-yellow-light .sidebar-form input[type="text"],.skin-yellow-light .sidebar-form .btn{box-shadow:none;background-color:#fff;border:1px solid transparent;height:35px}.skin-yellow-light .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-yellow-light .sidebar-form input[type="text"]:focus,.skin-yellow-light .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-yellow-light .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-yellow-light .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}@media (min-width:768px){.skin-yellow-light.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu{border-left:1px solid #eee}}.skin-purple .main-header .navbar{background-color:#001f3f}.skin-purple .main-header .navbar .nav>li>a{color:#fff}.skin-purple .main-header .navbar .nav>li>a:hover,.skin-purple .main-header .navbar .nav>li>a:active,.skin-purple .main-header .navbar .nav>li>a:focus,.skin-purple .main-header .navbar .nav .open>a,.skin-purple .main-header .navbar .nav .open>a:hover,.skin-purple .main-header .navbar .nav .open>a:focus,.skin-purple .main-header .navbar .nav>.active>a{background:rgba(0,0,0,0.1);color:#f6f6f6}.skin-purple .main-header .navbar .sidebar-toggle{color:#fff}.skin-purple .main-header .navbar .sidebar-toggle:hover{color:#f6f6f6;background:rgba(0,0,0,0.1)}.skin-purple .main-header .navbar .sidebar-toggle{color:#fff}.skin-purple .main-header .navbar .sidebar-toggle:hover{background-color:#001226}@media (max-width:767px){.skin-purple .main-header .navbar .dropdown-menu li.divider{background-color:rgba(255,255,255,0.1)}.skin-purple .main-header .navbar .dropdown-menu li a{color:#fff}.skin-purple .main-header .navbar .dropdown-menu li a:hover{background:#001226}}.skin-purple .main-header .logo{background-color:#001226;color:#fff;border-bottom:0 solid transparent}.skin-purple .main-header .logo:hover{background-color:#001020}.skin-purple .main-header li.user-header{background-color:#001f3f}.skin-purple .content-header{background:transparent}.skin-purple .wrapper,.skin-purple .main-sidebar,.skin-purple .left-side{background-color:#313439}.skin-purple .user-panel>.info,.skin-purple .user-panel>.info>a{color:#fff}.skin-purple .sidebar-menu>li.header{color:#606670;background:#282a2e}.skin-purple .sidebar-menu>li>a{border-left:3px solid transparent}.skin-purple .sidebar-menu>li:hover>a,.skin-purple .sidebar-menu>li.active>a{color:#fff;background:#2c2f34;border-left-color:#001f3f}.skin-purple .sidebar-menu>li>.treeview-menu{margin:0 1px;background:#3d4147}.skin-purple .sidebar a{color:#cacdd2}.skin-purple .sidebar a:hover{text-decoration:none}.skin-purple .treeview-menu>li>a{color:#a1a6ae}.skin-purple .treeview-menu>li.active>a,.skin-purple .treeview-menu>li>a:hover{color:#fff}.skin-purple .sidebar-form{border-radius:3px;border:1px solid #494d54;margin:10px 10px}.skin-purple .sidebar-form input[type="text"],.skin-purple .sidebar-form .btn{box-shadow:none;background-color:#494d54;border:1px solid transparent;height:35px}.skin-purple .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-purple .sidebar-form input[type="text"]:focus,.skin-purple .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-purple .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-purple .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}.skin-purple-light .main-header .navbar{background-color:#001f3f}.skin-purple-light .main-header .navbar .nav>li>a{color:#fff}.skin-purple-light .main-header .navbar .nav>li>a:hover,.skin-purple-light .main-header .navbar .nav>li>a:active,.skin-purple-light .main-header .navbar .nav>li>a:focus,.skin-purple-light .main-header .navbar .nav .open>a,.skin-purple-light .main-header .navbar .nav .open>a:hover,.skin-purple-light .main-header .navbar .nav .open>a:focus,.skin-purple-light .main-header .navbar .nav>.active>a{background:rgba(0,0,0,0.1);color:#f6f6f6}.skin-purple-light .main-header .navbar .sidebar-toggle{color:#fff}.skin-purple-light .main-header .navbar .sidebar-toggle:hover{color:#f6f6f6;background:rgba(0,0,0,0.1)}.skin-purple-light .main-header .navbar .sidebar-toggle{color:#fff}.skin-purple-light .main-header .navbar .sidebar-toggle:hover{background-color:#001226}@media (max-width:767px){.skin-purple-light .main-header .navbar .dropdown-menu li.divider{background-color:rgba(255,255,255,0.1)}.skin-purple-light .main-header .navbar .dropdown-menu li a{color:#fff}.skin-purple-light .main-header .navbar .dropdown-menu li a:hover{background:#001226}}.skin-purple-light .main-header .logo{background-color:#001f3f;color:#fff;border-bottom:0 solid transparent}.skin-purple-light .main-header .logo:hover{background-color:#001c3a}.skin-purple-light .main-header li.user-header{background-color:#001f3f}.skin-purple-light .content-header{background:transparent}.skin-purple-light .wrapper,.skin-purple-light .main-sidebar,.skin-purple-light .left-side{background-color:#e0e0e0}.skin-purple-light .content-wrapper,.skin-purple-light .main-footer{border-left:1px solid #eee}.skin-purple-light .user-panel>.info,.skin-purple-light .user-panel>.info>a{color:#616161}.skin-purple-light .sidebar-menu>li{-webkit-transition:border-left-color .3s ease;-o-transition:border-left-color .3s ease;transition:border-left-color .3s ease}.skin-purple-light .sidebar-menu>li.header{color:#a0a0a0;background:#e0e0e0}.skin-purple-light .sidebar-menu>li>a{border-left:3px solid transparent;font-weight:600}.skin-purple-light .sidebar-menu>li:hover>a,.skin-purple-light .sidebar-menu>li.active>a{color:#212121;background:#e4e4e4}.skin-purple-light .sidebar-menu>li.active{border-left-color:#001f3f}.skin-purple-light .sidebar-menu>li.active>a{font-weight:600}.skin-purple-light .sidebar-menu>li>.treeview-menu{background:#e4e4e4}.skin-purple-light .sidebar a{color:#616161}.skin-purple-light .sidebar a:hover{text-decoration:none}.skin-purple-light .treeview-menu>li>a{color:#7a7a7a}.skin-purple-light .treeview-menu>li.active>a,.skin-purple-light .treeview-menu>li>a:hover{color:#000}.skin-purple-light .treeview-menu>li.active>a{font-weight:600}.skin-purple-light .sidebar-form{border-radius:3px;border:1px solid #eee;margin:10px 10px}.skin-purple-light .sidebar-form input[type="text"],.skin-purple-light .sidebar-form .btn{box-shadow:none;background-color:#fff;border:1px solid transparent;height:35px}.skin-purple-light .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-purple-light .sidebar-form input[type="text"]:focus,.skin-purple-light .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-purple-light .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-purple-light .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}@media (min-width:768px){.skin-purple-light.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu{border-left:1px solid #eee}}
\ No newline at end of file
diff --git a/www/examples/README.md b/www/examples/README.md
new file mode 100644
index 0000000..4e5519b
--- /dev/null
+++ b/www/examples/README.md
@@ -0,0 +1,31 @@
+## Creating Examples
+
+To create a new example, run ShinyDAG locally using the shinydag dev docker file.
+
+Set up your example and then save it as a bookmark.
+
+Note the URL created by shiny for the bookmark, it should end with
+
+```
+?_state_id_=9d49cb0ba72b00f2
+```
+
+Navigate to the `shiny_bookmarks` folder and find the folder with the bookmark token, e.g. `9d49cb0ba72b00f2`.
+
+Copy `values.rds` to `www/examples` and give the file a descriptive name.
+These names are used for the shiny inputs, so keep the characters sane (and no spaces).
+
+Also, save the DAG image into `www/examples` with the same name (not required but a good idea).
+
+Finally, add the description text to `www/examples/examples.yml`.
+Here's an example template that you can copy.
+Note that if you can use HTML in the `description`, but it needs to be valid or it will cause problems on the page.
+
+```yaml
+- name: Classic Confounding
+ description: >
+ This is a description of classic confounding. Descriptions may include
+ HTML.
+ file: classic-confounding.rds
+ image: classic-confounding.png
+```
\ No newline at end of file
diff --git a/www/examples/classic-confounding.png b/www/examples/classic-confounding.png
new file mode 100644
index 0000000..d4ddab4
Binary files /dev/null and b/www/examples/classic-confounding.png differ
diff --git a/www/examples/classic-confounding.rds b/www/examples/classic-confounding.rds
new file mode 100644
index 0000000..7936480
Binary files /dev/null and b/www/examples/classic-confounding.rds differ
diff --git a/www/examples/differential-loss-to-follow-up.png b/www/examples/differential-loss-to-follow-up.png
new file mode 100644
index 0000000..c5bc3e0
Binary files /dev/null and b/www/examples/differential-loss-to-follow-up.png differ
diff --git a/www/examples/differential-loss-to-follow-up.rds b/www/examples/differential-loss-to-follow-up.rds
new file mode 100644
index 0000000..d00c6a9
Binary files /dev/null and b/www/examples/differential-loss-to-follow-up.rds differ
diff --git a/www/examples/examples.yml b/www/examples/examples.yml
new file mode 100644
index 0000000..3dff0c1
--- /dev/null
+++ b/www/examples/examples.yml
@@ -0,0 +1,23 @@
+- name: Classic Confounding
+ description: >
+ This depicts classic confounding, where a confounder C is a common cause of exposure (E) and outcome (Y).
+ file: classic-confounding.rds
+ image: classic-confounding.png
+
+- name: Differential Loss to Follow-Up
+ description: >
+ This depicts differential loss to follow up, where patients may be censored (C) depending on their value of L.
+ file: differential-loss-to-follow-up.rds
+ image: differential-loss-to-follow-up.png
+
+- name: Mediator with Confounding
+ description: >
+ This shows a mediator with confounding, where the variable M mediates the effect of E on Y, which is confounded by the variable C.
+ file: mediator-with-confounding.rds
+ image: mediator-with-confounding.png
+
+- name: Selection Bias
+ description: >
+ This depicts classic selection bias, where C denotes criteria under which the data are observed which, in turn, is a downstream consequence of exposure (E) and outcome (D).
+ file: selection-bias.rds
+ image: selection-bias.png
diff --git a/www/examples/mediator-with-confounding.png b/www/examples/mediator-with-confounding.png
new file mode 100644
index 0000000..409778c
Binary files /dev/null and b/www/examples/mediator-with-confounding.png differ
diff --git a/www/examples/mediator-with-confounding.rds b/www/examples/mediator-with-confounding.rds
new file mode 100644
index 0000000..b352eef
Binary files /dev/null and b/www/examples/mediator-with-confounding.rds differ
diff --git a/www/examples/selection-bias.png b/www/examples/selection-bias.png
new file mode 100644
index 0000000..5bf80b1
Binary files /dev/null and b/www/examples/selection-bias.png differ
diff --git a/www/examples/selection-bias.rds b/www/examples/selection-bias.rds
new file mode 100644
index 0000000..1583c5b
Binary files /dev/null and b/www/examples/selection-bias.rds differ
diff --git a/www/shinydag.css b/www/shinydag.css
new file mode 100644
index 0000000..91379c2
--- /dev/null
+++ b/www/shinydag.css
@@ -0,0 +1,264 @@
+@import url('https://fonts.googleapis.com/css?family=Lato:300,400,400i,700,700i');
+
+@media (min-width: 768px) and (max-width: 991px) {
+ #shinydag-toolbar-node-list-action {
+ padding-top: 32px;
+ }
+}
+
+/* ---- GerkeLab Admin LTE Theme Tweaks ---- */
+body, h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6, .main-header .logo {
+ font-family: Lato, sans-serif;
+}
+
+.main-header > .logo {
+ color: #989898 !important;
+ text-decoration: none;
+ font-weight: bold;
+}
+
+.main-header {
+ position: fixed;
+ width: 100%;
+ box-shadow: 0 3px 3px rgba(0,0,0,0.1) !important;
+ -webkit-box-shadow: 0 3px 3px rgba(0,0,0,0.1) !important;
+}
+
+.wrapper, .content-wrapper {
+ min-height: 100vh !important;
+ height: 100% !important;
+ background-color: #f0f0f0 !important;
+}
+
+@media (max-width:767px) {
+ .content-wrapper {
+ padding-top: 100px;
+ }
+}
+@media (min-width:768px) {
+ .content-wrapper {
+ padding-top: 50px;
+ }
+}
+
+.skin-black .main-header .navbar .navbar-custom-menu .navbar-nav > li > a,
+.skin-black .main-header .navbar .navbar-right > li > a {
+ border-left: none;
+}
+
+.gerkelab-logo {
+ background: url("GerkeLab.png");
+ background-size: contain;
+ width: 100px;
+ height: 100px;
+ position: absolute;
+ bottom: 25px;
+ left: 65px;
+ filter: drop-shadow(0 3px 4px rgba(0,0,0,0.2));
+ transition: z-index 0s, left 0.25s ease-in-out, filter 0.25s ease-in-out, opacity 1s ease-in-out;
+ z-index: 1000;
+ opacity: 1;
+}
+
+.sidebar-collapse .gerkelab-logo {
+ transition: z-index 0s 0.5s, left 0.25s ease-in-out, filter 0.25s ease-in-out;
+ z-index: 0;
+ left: 2%;
+ bottom: 25px;
+}
+
+.sidebar-collapse .gerkelab-logo:hover {
+ filter: drop-shadow(0 0 1px rgba(0,0,0,0.2));
+}
+
+@media (max-width: 767px) {
+ .gerkelab-logo {
+ z-index: 0;
+ opacity: 0;
+ }
+}
+
+.disable-buttons {
+ pointer-events: none;
+}
+
+.disabled {
+ pointer-events: none;
+}
+
+#shiny-tab-sketch .box {
+ margin-bottom: 100px;
+}
+
+.dag-preview-tikz {
+ min-height: 400px;
+}
+
+.example-image {
+ text-align: center;
+}
+
+.example-image img {
+ width: 80%;
+ max-width: 500px;
+}
+
+/* ---- ShinyDAG specific tweaks ---- */
+
+#edge_aes_ui label {
+ color: #777;
+ font-weight: normal;
+}
+
+#showPreviewContainer {
+ padding-top: 32px;
+}
+
+.dagpreview-download-ui {
+ padding-top: 25px;
+}
+
+#node_delete {
+ margin-top: 20px;
+ color: #FFF
+}
+
+#edge_btn {
+ margin-top: 25px;
+ color: #FFF
+}
+
+#ui_edge_swap_btn {
+ margin-top: 25px;
+}
+
+@media (min-width: 768px) {
+ #node_delete {
+ margin-left: -25px;
+ }
+}
+
+.edge-selector-hint {
+ font-size: 28px;
+ line-height: 14px;
+ padding-left: 4px;
+ vertical-align: top;
+}
+
+.help-block {
+ padding-top: 0;
+ font-style: italic;
+}
+
+.help-block.text-warning {
+ color: #db8b0b;
+ background-color: #db8b0b20;
+}
+
+.help-block.text-danger {
+ color: #d33724;
+ background-color: #d3372420;
+}
+
+#edge_list_helptext .help-block, #node_list_helptext .help-block {
+ padding: 0.5em;
+ border-radius: 3px;
+}
+
+.btn-text {
+ padding-left: 5px;
+}
+
+@media (min-width: 992px) and (max-width: 1825px) {
+ .btn-text {
+ display: none;
+ }
+}
+
+.gerkelab-spinner {
+ margin: auto;
+ width: 100px;
+ height: 100px;
+ background: url("GerkeLab.png");
+ background-size: cover;
+ -webkit-animation-name: spin;
+ -webkit-animation-duration: 4000ms;
+ -webkit-animation-iteration-count: infinite;
+ -webkit-animation-timing-function: linear;
+ -moz-animation-name: spin;
+ -moz-animation-duration: 4000ms;
+ -moz-animation-iteration-count: infinite;
+ -moz-animation-timing-function: linear;
+ -ms-animation-name: spin;
+ -ms-animation-duration: 4000ms;
+ -ms-animation-iteration-count: infinite;
+ -ms-animation-timing-function: linear;
+
+ animation-name: spin;
+ animation-duration: 4000ms;
+ animation-iteration-count: infinite;
+ animation-timing-function: linear;
+}
+@-ms-keyframes spin {
+ from { -ms-transform: rotate(0deg); }
+ to { -ms-transform: rotate(-360deg); }
+}
+@-moz-keyframes spin {
+ from { -moz-transform: rotate(0deg); }
+ to { -moz-transform: rotate(-360deg); }
+}
+@-webkit-keyframes spin {
+ from { -webkit-transform: rotate(0deg); }
+ to { -webkit-transform: rotate(-360deg); }
+}
+@keyframes spin {
+ from {
+ transform:rotate(0deg);
+ }
+ to {
+ transform:rotate(-360deg);
+ }
+}
+
+.alert-edge {
+ animation: fadeout 5s;
+ -moz-animation: fadeout 5s;
+ -webkit-animation: fadeout 5s;
+ -o-animation: fadeout 5s;
+}
+
+@keyframes fadeout {
+ 0% {
+ opacity: 1;
+ }
+ 75% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: 0;
+ }
+}
+
+@-moz-keyframes fadeout {
+ 0% {
+ opacity: 1;
+ }
+ 75% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: 0;
+ }
+}
+
+@-webkit-keyframes fadeout {
+ 0% {
+ opacity: 1;
+ }
+ 75% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: 0;
+ }
+}
\ No newline at end of file
diff --git a/www/shinydag.js b/www/shinydag.js
new file mode 100644
index 0000000..7d6c7df
--- /dev/null
+++ b/www/shinydag.js
@@ -0,0 +1,78 @@
+const set_input_focus = (id) => {
+ const el = document.getElementById(id);
+ if (el) {
+ el.focus();
+ }
+};
+
+const wrap_btn_text_in_span = (id, text) => {
+ var $el = $("#" + id);
+ $el.html([$el.children()[0], "" + text + ""]);
+};
+
+$( document ).ready(function() {
+ setTimeout(function() {wrap_btn_text_in_span("downloadButton", "Download")}, 1000);
+ setTimeout(function() {wrap_btn_text_in_span("\\._bookmark_", "Bookmark")}, 1000);
+});
+
+// Block name change updates while Shiny is re-rendering to avoid wonkiness
+var text_input_timeout;
+$(document).on("shiny:busy", (e) => {
+ text_input_timeout = setTimeout(() => {
+ $("#node_list_node_name").prop("disabled", true);
+ }, 500);
+});
+$(document).on("shiny:idle", (e) => {
+ clearTimeout(text_input_timeout);
+ $("#node_list_node_name").prop("disabled", false);
+});
+
+// Block node change buttons when updating names to avoid infinite looping wonkiness
+// disables buttons when user starts typing in text box
+$("#node_list_node_name").keydown(() => {
+ $("#shinydag-toolbar-node-list-action button").prop("disabled", true);
+});
+
+// re-enable buttons when Shiny updates or the text bar loses focus (in case no change)
+$(document).on("shiny:value", () => {
+ $("#shinydag-toolbar-node-list-action button").prop("disabled", false);
+});
+$("#node_list_node_name").blur(() => {
+ $("#shinydag-toolbar-node-list-action button").prop("disabled", false);
+});
+
+// Block undo/redo buttons during Shiny updates as well
+var undo_disable_timeout = null;
+$("#undo_rv-history_back, #undo_rv-history_forward").on("click", () => {
+ undo_disable_timeout = setTimeout(() => {
+ $("#undo_rv-history_back").parent().addClass("disable-buttons");
+ }, 10);
+})
+
+// Bock undo/redo with a delay for general Shiny updates
+$(document).on("shiny:busy", () => {
+ if (!undo_disable_timeout) {
+ undo_disable_timeout = setTimeout(() => {
+ $("#undo_rv-history_back").parent().addClass("disable-buttons");
+ }, 250);
+ }
+});
+
+// re-enable undo/redo buttons when Shiny is idle
+$(document).on("shiny:idle", () => {
+ clearTimeout(undo_disable_timeout);
+ undo_disable_timeout = null;
+ $("#undo_rv-history_back").parent().removeClass("disable-buttons");
+});
+
+// Animate logo when app is busy
+var app_busy_timeout;
+$(document).on("shiny:busy", e => {
+ app_busy_timeout = setTimeout(() => {
+ $(".gerkelab-logo").addClass("gerkelab-spinner");
+ }, 500);
+});
+$(document).on("shiny:idle", e => {
+ clearTimeout(app_busy_timeout);
+ $(".gerkelab-logo").removeClass("gerkelab-spinner");
+});