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"); +});