diff --git a/apps/dashr-ocean-optics/Aptfile b/apps/dashr-ocean-optics/Aptfile new file mode 100755 index 000000000..187885d0b --- /dev/null +++ b/apps/dashr-ocean-optics/Aptfile @@ -0,0 +1,3 @@ +libcurl4-openssl-dev +libxml2-dev +libv8-3.14-dev diff --git a/apps/dashr-ocean-optics/DOKKU_SCALE b/apps/dashr-ocean-optics/DOKKU_SCALE new file mode 100755 index 000000000..3cb9829e6 --- /dev/null +++ b/apps/dashr-ocean-optics/DOKKU_SCALE @@ -0,0 +1 @@ +web=1 diff --git a/apps/dashr-ocean-optics/Procfile b/apps/dashr-ocean-optics/Procfile new file mode 100755 index 000000000..c6af009a7 --- /dev/null +++ b/apps/dashr-ocean-optics/Procfile @@ -0,0 +1 @@ +web: R -f /app/apps/"$DASH_APP_NAME"/app.R \ No newline at end of file diff --git a/apps/dashr-ocean-optics/README.md b/apps/dashr-ocean-optics/README.md new file mode 100644 index 000000000..3eca09767 --- /dev/null +++ b/apps/dashr-ocean-optics/README.md @@ -0,0 +1,59 @@ +# dashr-ocean-optics +## Introduction +`dashr-ocean-optics` is a demo R app created to demonstrate controlling and reading data from [Ocean Optics](https://oceanoptics.com) spectrometers. Python version (contains also non-demo version) of the application can be found [here](https://github.com/plotly/dash-ocean-optics). + +### Absorption Spectroscopy +Certain wavelengths of electromagnetic radiation correspond to frequencies that allow the electrons in certain atoms to transition to higher or lower energy levels; as these wavelengths are absorbed by the sample, the resulting spectrum can yield insight into the chemical composition of the sample. Read more about spectroscopy [here](https://en.wikipedia.org/wiki/Spectroscopy). + +### dash-daq +[Dash DAQ](https://www.dashdaq.io/) is a data acquisition and control package built on top of Plotly's [Dash](https://plot.ly/products/dash/). + +## How to Run the App + +Clone the git repo: + +``` +git clone https://github.com/plotly/dash-sample-apps +cd dash-sample-apps/apps/dashr-ocean-optics +``` +Run `Rscript init.R` to install all of the required packages + +Run `Rscript app.R`: + +The app will load into your default browser window. If it does not, navigate to 127.0.0.1:8050. You should see something like this: + +![on](screenshots/on.png) + +The demo spectrometer data should appear on the plot, and the model of the spectrometer should appear above the plot. Note that the "autoscale" feature is on by default. To improve the speed and animations of the graph, it is necessary to turn this feature off. + +![autoscale](screenshots/autoscale.png) + +The controls below the plot will allow you to change parameters of the spectrometer. The dial labelled "light intensity" to the right of the plot will allow you to adjust the intensity of the light source selected below. Note that the dial is disabled until a light source is selected from the appropriate dropdown menu. + +![change](screenshots/change.png) + +### Controls +* int. time (us) - The integration time, in microseconds. +* number of scans - The number of scans to average over. +* strobe - Enables/disables the continuous strobe. +* strobe pd. (us) - The period of the continuous strobe, in microseconds. +* light source - The light source to be used. + + +Once they have been changed to the appropriate settings, the "update" button to the right of the plot should be pressed, and each of the settings will be sent to the spectrometer one-by-one. The window below the "update" button displays the commands that failed, with the associated error messages, and the commands that succeeded, with the new values. + +![changefail](screenshots/changefail.png) +![changesuccess](screenshots/changesuccess.png) + +Note that the window below the update button is scrollable! + +## Advanced + +### Configuring the colours +The colours for all of the Dash and Dash-DAQ components are loaded from `colors.txt`. Note that if you want to change the appearance of other components on the page, you'll have to link a different CSS file in `app.R`. + +### Adding your own controls +In order to add a control yourself, you must: +* Create a new control object in `app.R`; note that the new object must have the key `id` in order for the callbacks to be properly triggered. +* Append this new object to the list `controls` within `app.R`. +After the above steps, new controller should be available in user-interface and can also be submitted upon clicking on `update` button. For additional functionality edit the `callbacks` as required. diff --git a/apps/dashr-ocean-optics/app.R b/apps/dashr-ocean-optics/app.R new file mode 100644 index 000000000..792b7cbaa --- /dev/null +++ b/apps/dashr-ocean-optics/app.R @@ -0,0 +1,572 @@ +library(dash) +library(dashDaq) +library(dashCoreComponents) +library(dashHtmlComponents) + +appName <- Sys.getenv("DASH_APP_NAME") + +if (!appName == "") { + pathPrefix <- sprintf("/%s/", appName) + + Sys.setenv(DASH_ROUTES_PATHNAME_PREFIX = pathPrefix, + DASH_REQUESTS_PATHNAME_PREFIX = pathPrefix) + + setwd(sprintf("/app/apps/%s", appName)) +} + +# colors for layout and figure +colorsRaw <- read.table("colors.txt", sep = ' ', comment.char = '', + stringsAsFactors = FALSE) + +colors <- setNames(as.list(colorsRaw[, 2]), colorsRaw[, 1]) + +app <- Dash$new() + + +############################ +# Controls +############################ + +# integration time, microseconds +intTime <- htmlDiv(children = list( + htmlDiv(children = list('int. time (\U{00B5}s)'), className = 'option-name'), + daqNumericInput( + id = 'integration-time-input', + value = 1000, + size = 150, + min = 1000, + max = 650000000, + disabled = FALSE + ) +), +id = 'integration-time') + +# scans to average over +nscansAvg <- htmlDiv(children = list( + htmlDiv(children = list('number of scans'), className = 'option-name'), + daqNumericInput( + id = 'nscans-to-average-input', + value = 1, + size = 150, + min = 1, + max = 100, + disabled = FALSE + ) +), +id = 'nscans-to-average') + +# strobe +strobeEnable <- htmlDiv(children = list( + htmlDiv(children = list('strobe'), className = 'option-name'), + daqBooleanSwitch( + id = 'continuous-strobe-toggle-input', + color = colors[['accent']], + on = FALSE, + disabled = FALSE + ) +), +id = 'continuous-strobe-toggle') + +# strobe period +strobePeriod <- htmlDiv(children = list( + htmlDiv(children = list('strobe pd. (\U{00B5}s)'), className = 'option-name'), + daqNumericInput( + id = 'continuous-strobe-period-input', + value = 1, + size = 150, + min = 1, + max = 100, + disabled = FALSE + ) +), +id = 'continuous-strobe-period') + + +lightOptions <- list(list('label' = 'Lamp 1 at 127.0.0.1', 'value' = 'l1'), + list('label' = 'Lamp 2 at 127.0.0.1', 'value' = 'l2')) + +# light sources +lightSources <- htmlDiv(children = list( + htmlDiv(children = list('light source'), className = 'option-name'), + dccDropdown( + id = 'light-source-input', + placeholder = "select light source", + options = lightOptions, + value = "l2", + disabled = FALSE + ) +), +id = 'light-source') + +controls <- list(intTime, nscansAvg, strobeEnable, strobePeriod, lightSources) + + +############################ +# Layout +############################ + +pageLayout <- list(htmlDiv(id = 'page', children = list( + + # banner + htmlDiv( + id = 'logo', + title = 'Dash by Plotly', + style = list( + 'position' = 'absolute', + 'left' = '10px', + 'top' = '10px', + 'zIndex' = 100 + ), + children = list(htmlA( + htmlImg(src = "/assets/logo-white.png", + style = list('height' = '50px')), + href = "https://plot.ly/dash" + )) + ), + + # plot + htmlDiv(id = 'graph-container', + children = list(htmlDiv( + children = list( + htmlDiv(id = 'graph-title', + children = list("ocean optics USB2000+")), + dccGraph(id = 'spec-readings', animate = TRUE), + dccInterval( + id = 'spec-reading-interval', + interval = 1 * 1000, + n_intervals = 0, + max_intervals = 300 # stop after 5 mins. + # otherwise server has to handle callbacks for idle app + ) + ) + ))), + + # power button + htmlDiv(id = 'power-button-container', + title = 'Turn the power on to begin viewing the data and controlling the spectrometer.', + children = list( + daqPowerButton( + id = 'power-button', + size = 50, + color = colors[['accent']], + on = TRUE + ) + )), + + # status box + htmlDiv( + id = 'status-box', + children = list( + + # light intensity + htmlDiv(className = 'status-box-title', + children = list("light intensity")), + htmlDiv( + id = 'light-intensity-knob-container', + title = 'Controls the intensity of the light source, if any.', + children = list( + daqKnob( + id = 'light-intensity-knob', + size = 110, + color = colors[['accent']], + value = 0 + ) + ) + ), + + # autoscale + htmlDiv(className = 'status-box-title', + children = list("autoscale plot")), + htmlDiv( + id = 'autoscale-switch-container', + title = 'Controls whether the plot automatically resizes to fit the spectra.', + children = list( + daqBooleanSwitch(id = 'autoscale-switch', + on = TRUE, + color = colors[['accent']]) + ) + ), + + # submit button + htmlDiv( + id = 'submit-button-container', + title = 'Sends all of the control values below the graph to the spectrometer.', + children = list( + htmlButton( + 'update', + id = 'submit-button', + n_clicks = 0, + n_clicks_timestamp = 0 + ) + ) + ), + + # displays whether the parameters were successfully changed + htmlDiv( + id = 'submit-status', + title = 'Contains information about the success or failure of your commands.' + ) + ) + ), + + # all controls + htmlDiv( + id = 'controls', + title = 'All of the spectrometer parameters that can be changed.', + children = controls + ), + + # hidden-div integration time + htmlDiv(id = 'hidden-div-int-time', + children = list("1000"), + hidden = TRUE + ), + + # hidden-div light source + htmlDiv(id = 'hidden-div-light-source', + children = list("l2"), + hidden = TRUE + ), + + # about the app + htmlDiv(id = 'infobox', + children = list( + htmlDiv("about this app", + id = 'infobox-title'), + dccMarkdown( + ' + This is a demo app created to act as an interface for an Ocean Optics + spectrometer. The options above are used to control various + properties of the instrument; the integration time, the number of + scans to average over, the strobe and strobe period, and the + light source. + + Clicking \"Update\" after putting in the desired settings will + result in them being sent to the device. A status message + will appear below the button indicating which commands, if any, + were unsuccessful; below the unsuccessful commands, a list of + successful commands can be found. + + (Note that the box containing the status information is + scrollable.) + + + The dial labelled \"light intensity\" will affect the current + selected light source, if any. The switch labelled \"autoscale + plot\" will change the axis limits of the plot to fit all of the + data. Please note that the animations and speed of the graph will + improve if this feature is turned off, and that it will not be + possible to zoom in on any portion of the plot if it is turned + on. + ' + ) + ) + ) +))) + +app$layout(htmlDiv(id = 'main', children = pageLayout)) + + +############################ +# Helper Variables & FUNs +############################ + +# input list control elements +controlValues <- lapply(1:length(controls), function(i) { + input(id = controls[[i]]$props$children[[2]]$props$id, + property = ifelse( + length(controls[[i]]$props$children[[2]]$props$value) > 0, + "value", + "on" + )) +}) + +# state list control elements +controlValuesState <- lapply(1:length(controls), function(i) { + state(id = controls[[i]]$props$children[[2]]$props$id, + property = ifelse( + length(controls[[i]]$props$children[[2]]$props$value) > 0, + "value", + "on" + )) +}) + +# generates intensities +SampleSpectrum <- function(scale, knobIntensity, x) { + scale * (exp(-1 * ( (x - 500) / 5) ** 2) + + 0.01 * runif(length(x))) + knobIntensity * 10 +} + + +############################ +# Callbacks +############################ + +# disable/enable the update button depending on whether options have changed +app$callback( + output = list(id = "submit-button", property = "style"), + params = c(controlValues, + list(input(id = "submit-button", "n_clicks_timestamp"))), + + function(...) { + args <- list(...) + + argsTime <- args[[length(args)]] + now <- as.numeric(Sys.time()) * 1000 + + disabled <- list( + 'color' = colors[['accent']], + 'backgroundColor' = colors[['background']], + 'cursor' = 'not-allowed' + ) + + enabled <- list( + 'color' = colors[['background']], + 'backgroundColor' = colors[['accent']], + 'cursor' = 'pointer' + ) + + # if the button was recently clicked (less than a second ago), then + # it's safe to say that the callback was triggered by the button; so + # we have to "disable" it + if (now - argsTime < 500 && argsTime > 0) { + return(disabled) + } else { + return(enabled) + } + } +) + + +# disable/enable controls +app$callback( + output = list(id = "controls", property = "children"), + params = list(input(id = "power-button", property = "on")), + + function(pwr_on) { + lapply(1:length(controls), + function(i) { + controls[[i]]$props$children[[2]]$props$disabled <- !pwr_on + controls[[i]] + }) + } +) + + +# disable/enable intensity knob +app$callback( + output = list(id = "light-intensity-knob", property = "disabled"), + params = list( + input(id = "power-button", property = "on"), + input(id = "light-source-input", property = "value") + ), + + function(pwr, lsi) { + return(!(pwr && !is.null(lsi[[1]]) && lsi != "")) + } +) + + +# send user-selected options to spectrometer +app$callback( + output = list(id = "submit-status", property = "children"), + params = c(list(input(id = "submit-button", "n_clicks")), + controlValuesState, + list(state(id = "power-button", "on"))), + + function(...) { + args <- list(...) + + # handle `NULL` args` + nullCheck <- sapply(args, class) + if ("list" %in% nullCheck) { + args[[which(nullCheck == "list")]] <- "NULL" + } + + # power-button off case + if (!args[[length(args)]]) { + return ( + list( + "Press the power button to the top-right of the app, + then press the \"update\" button above to apply your options to the spectrometer." + ) + ) + } + + # if submit-button clicked + if (args[[1]] > 0) { + sumList <- list() + nextLine <- list(htmlBr(), htmlBr()) + + if ("l1" %in% args ) { + sumList <- list( + "The following parameters were not successfully updated:") + sumList <- c(sumList, nextLine) + sumList <- c(sumList, list("LIGHT SOURCE: Lamp not found.")) + sumList <- c(sumList, nextLine, list(htmlHr(), htmlBr())) + } + + sumList <- c(sumList, + list("The following parameters were successfully updated:"), + nextLine) + + # append updated controls to sumList + for (i in 1:length(controls)) { + + if (!(args[[i + 1]] == "l1")) { + argName <- controls[[i]]$props$children[[1]]$props$children[[1]] + sumList <- c(sumList, + toupper(argName), + list(": "), + list(as.character(args[[i + 1]])), + list(htmlBr())) + } + } + return(sumList) + } + } +) + + +# store `int. time` for plot after submit button clicked +app$callback( + output = list(id = "hidden-div-int-time", property = "children"), + params = list( + input(id = "submit-button", "n_clicks_timestamp"), + state(id = "integration-time-input", property = "value"), + state(id = "power-button", property = "on") + ), + + function(clicks, value, on) { + if (on) { + list(value) + } else { + list(1000) + } + } +) + + +# store `light source` for plot after submit button clicked +app$callback( + output = list(id = "hidden-div-light-source", property = "children"), + params = list( + input(id = "submit-button", "n_clicks_timestamp"), + state(id = "light-source-input", property = "value"), + state(id = "power-button", property = "on") + ), + + function(clicks, value, on) { + if (on) { + # Handle the case when value is removed + if ( is.list(value) ) { + list("NULL") + } else { + list(value) + } + } else { + list("l2") + } + } +) + + +# update the plot +app$callback( + output = list(id = "spec-readings", property = "figure"), + params = list( + input(id = "spec-reading-interval", property = "n_intervals"), + state(id = "power-button", property = "on"), + state(id = "autoscale-switch", property = "on"), + state(id = "light-intensity-knob", property = "value"), + state(id = "hidden-div-int-time", property = "children"), + state(id = "hidden-div-light-source", property = "children") + ), + + function(n, pwr, auto_range, knob_intensity, scale, lsi) { + + lsi <- unlist(lsi) + knobIntensity <- ifelse(lsi == "l2", as.numeric(unlist(knob_intensity)), 0) + + scale <- as.numeric(unlist(scale)) + + wavelengths <- seq(400, 900, length.out = 5000L) + + if (pwr) { + intensities <- SampleSpectrum(scale = scale, knobIntensity = knobIntensity, wavelengths) + } else { + intensities <- rep(0, length(wavelengths)) + } + + # start creating layout variables + xAxis <- list( + 'title' = 'Wavelength (nm)', + 'titlefont' = list( + 'family' = 'Helvetica, sans-serif', + 'color' = colors[['secondary']] + ), + 'tickfont' = list( + 'color' = colors[['tertiary']] + ), + 'dtick' = 100, + 'color' = colors[['secondary']], + 'gridcolor' = colors[['grid-colour']] + ) + + yAxis <- list( + 'title' = 'Intensity (AU)', + 'titlefont' = list( + 'family' = 'Helvetica, sans-serif', + 'color' = colors[['secondary']] + ), + 'tickfont' = list( + 'color' = colors[['tertiary']] + ), + 'color' = colors[['secondary']], + 'gridcolor' = colors[['grid-colour']] + ) + + # auto-scale feature + if (pwr && auto_range) { + xAxis[['range']] <- list(min(wavelengths), + max(wavelengths)) + yAxis[['range']] <- list(min(intensities), + max(intensities)) + + } + + layout <- list( + height = 600, + font = list('family' = 'Helvetica Neue, sans-serif', + 'size' = 12), + margin = list('t' = 20), + titlefont = list( + 'family' = 'Helvetica, sans-serif', + 'color' = colors[['primary']], + 'size' = 26), + xaxis = xAxis, + yaxis = yAxis, + paper_bgcolor = colors[['background']], + plot_bgcolor = colors[['background']] + ) + + return( + figure <- list(data = list( + list( + x = as.list(wavelengths), + y = as.list(intensities), + name = 'Spectrometer readings', + mode = 'lines', + line = list('width' = 1, 'color' = colors[['accent']]) + ) + ), + layout = layout) + ) + } +) + +if (!appName == ""){ + app$run_server(host = "0.0.0.0", port = Sys.getenv('PORT', 8050)) +} else { + app$run_server(debug = TRUE) +} diff --git a/apps/dashr-ocean-optics/assets/logo-white.png b/apps/dashr-ocean-optics/assets/logo-white.png new file mode 100644 index 000000000..eb700fc71 Binary files /dev/null and b/apps/dashr-ocean-optics/assets/logo-white.png differ diff --git a/apps/dashr-ocean-optics/assets/style.css b/apps/dashr-ocean-optics/assets/style.css new file mode 100755 index 000000000..5684c3d14 --- /dev/null +++ b/apps/dashr-ocean-optics/assets/style.css @@ -0,0 +1,170 @@ +._dash-undo-redo { + display: none; +} +#page { + background-color: #bbbbbb; + height: auto; + width: 95%; + min-width: 1200px; + position: absolute; + left: 0px; + top: 0px; + padding: 5%; + padding-right: 0%; +} +#graph-container { + background-color: #bbbbbb; + width: 70%; + min-width: 200px; + height: auto; + margin-right: 50px; + layout: inline-block; + position: absolute; +} +#graph-title { + color: #ffffff; + margin-top: 30px; + margin-bottom: 30px; + margin-left: 50px; + font-size: 50pt; + font-weight: 100; + font-variant: small-caps; + font-family: Helvetica, sans-serif; +} +#power-button-container { + position: absolute; + right: 50px; + top: 50px; +} +#controls { + position: relative; + margin-top: 25px; + overflow: auto; + padding-bottom:25px; +} +#integration-time, #nscans-to-average, #continuous-strobe-toggle, #continuous-strobe-period, #light-source { + width: 150px; + position: static; + float: left; + background-color: #bbbbbb; + height: 125px; + margin: 10px; + margin-top: 0px; + padding: 10px; + padding-top: 0px; + padding-bottom: 0px; + font-family: Helvetica, sans-serif; +} +.option-name{ + padding: none; + padding-bottom: 10px; + text-align: center; + font-variant: small-caps; + font-family: Helvetica, sans-serif; + font-weight: 100; + font-size: 16pt; + color: #ffffff; +} +#status-box { + width: 225px; + margin-top: 150px; + height: 550px; + position: relative; + left: 80%; + padding: 0px; + padding-top: 5px; + padding-bottom: 5px; + border-color: #efefef; + border-style: solid; + border-width: 1px; + border-radius: 5px; + font-family: Helvetica, sans-serif; + font-size: 14pt; + font-variant: small-caps; + text-align: center; + color: #ffffff; + background-color: #bbbbbb; +} +.status-box-title { + font-family: Helvetica, sans-serif; + font-size: 14pt; + font-variant: small-caps; + text-align: center; + color: #ffffff; +} +#autoscale-switch-container { + margin-top: 10px; +} +::-webkit-scrollbar-track { + display:none +} +#submit-button { + font-family: Helvetica, sans-serif; + font-size: 20px; + font-variant: small-caps; + color: #bbbbbb; + text-align: center; + height: 50px; + width: 125px; + margin-top: 25px; + margin-bottom: 10px; + z-index: 10; + background-color: #2222ff; + position: static; + border-width: 1px; + border-color: #2222ff; + border-radius: 5px; + transition-duration: 500ms; +} +#submit-button:hover { + background-color: #bbbbbb; + color: #2222ff; + cursor: pointer; +} +#submit-button:active, #submit-button:focus, #submit-button:active:focus{ + -webkit-box-shadow: none; + box-shadow: none; + outline: none !important; +} +#submit-status { + width: 175px; + border-style: solid; + border-color: #dfdfdf; + border-width: 1px; + height: 150px; + padding: 10px; + overflow-y: scroll; + overflow-x: auto; + position: static; + margin: auto; + margin-top:10px; + font-family: Courier, monospace; + font-variant: none; + text-align: left; + font-size: 9pt; + color: #efefef; + background-color: #bbbbbb; +} +#infobox { + position: static; + float: left; + width: 700px; + padding: 20px; + margin: 50px; + margin-top: 0px; + border-style: solid; + border-width: 1px; + border-radius: 5px; + border-color: #ffffff; + color: #ffffff; + font-family: Helvetica, sans-serif; +} +#infobox-title { + position: static; + width: 100%; + text-align: center; + font-size: 30pt; + padding: 20px; + padding-top: 0px; + font-variant: small-caps; +} \ No newline at end of file diff --git a/apps/dashr-ocean-optics/colors.txt b/apps/dashr-ocean-optics/colors.txt new file mode 100644 index 000000000..5560d0bdf --- /dev/null +++ b/apps/dashr-ocean-optics/colors.txt @@ -0,0 +1,6 @@ +background #bbbbbb +primary #ffffff +secondary #efefef +tertiary #dfdfdf +grid-colour #eeeeee +accent #2222ff diff --git a/apps/dashr-ocean-optics/init.R b/apps/dashr-ocean-optics/init.R new file mode 100755 index 000000000..a47df43b9 --- /dev/null +++ b/apps/dashr-ocean-optics/init.R @@ -0,0 +1,41 @@ +# R script to run author supplied code, typically used to install additional R packages +# contains placeholders which are inserted by the compile script +# NOTE: this script is executed in the chroot context; check paths! +#r <- getOption("repos") +#r["CRAN"] <- "http://cloud.r-project.org" +#options(repos=r) +# ====================================================================== +# packages go here +install.packages("remotes") +# installs Rcpp, rlang, BH +#install.packages("devtools") +install.packages("later") +install.packages("jsonlite") +install.packages("listenv") +# installs magrittr, promises, R6 +remotes::install_version("httpuv", version = "1.4.5.1", repos = "http://cloud.r-project.org", upgrade="never") +# installs crayon, digest, htmltools, mime, sourcetools, xtable +remotes::install_version("shiny", version = "1.2.0", repos = "http://cloud.r-project.org", upgrade="never") +# installs askpass, assertthat, base64enc, cli, colorspace, crosstalk, curl, data.table, dplyr, fansi, ggplot2, glue, gtable, hexbin, htmlwidgets, httr, labeling, lattice, lazyeval, mgcv, munsell, nlme, openssl, pillar, pkgconfig, plogr, plyr, purrr, RColorBrewer, reshape2, scales, stringi, stringr, sys, tibble, tidyr, tidyselect, utf8, viridisLite, withr, yaml +remotes::install_version("plotly", version = "4.9.0", repos = "http://cloud.r-project.org", upgrade="never") +install.packages("https://cloud.r-project.org/src/contrib/assertthat_0.2.1.tar.gz", type="source", repos=NULL) +install.packages("https://cloud.r-project.org/src/contrib/xml2_1.2.0.tar.gz", type="source", repos=NULL) +install.packages("https://cloud.r-project.org/src/contrib/triebeard_0.3.0.tar.gz", type="source", repos=NULL) +install.packages("https://cloud.r-project.org/src/contrib/Archive/urltools/urltools_1.7.2.tar.gz", type="source", repos=NULL) +install.packages("https://cloud.r-project.org/src/contrib/jsonlite_1.6.tar.gz", type="source", repos=NULL) +install.packages("https://cloud.r-project.org/src/contrib/webutils_0.6.tar.gz", type="source", repos=NULL) +install.packages("https://cloud.r-project.org/src/contrib/brotli_1.2.tar.gz", type="source", repos=NULL) +install.packages("https://cloud.r-project.org/src/contrib/reqres_0.2.2.tar.gz", type="source", repos=NULL) +install.packages("https://cloud.r-project.org/src/contrib/uuid_0.1-2.tar.gz", type="source", repos=NULL) +install.packages("https://cloud.r-project.org/src/contrib/base64enc_0.1-3.tar.gz", type="source", repos=NULL) +install.packages("https://cloud.r-project.org/src/contrib/codetools_0.2-16.tar.gz", type="source", repos=NULL) +install.packages("https://cloud.r-project.org/src/contrib/globals_0.12.4.tar.gz", type="source", repos=NULL) +install.packages("https://cloud.r-project.org/src/contrib/Archive/future/future_1.11.1.1.tar.gz", type="source", repos=NULL) +# fiery and friends +install.packages("https://cloud.r-project.org/src/contrib/routr_0.3.0.tar.gz", type="source", repos=NULL) +install.packages("https://cloud.r-project.org/src/contrib/fiery_1.1.1.tar.gz", type="source", repos=NULL) + +remotes::install_github("plotly/dash-html-components") +remotes::install_github("plotly/dash-core-components") +remotes::install_github("plotly/dashR") +remotes::install_github("plotly/dash-daq") diff --git a/apps/dashr-ocean-optics/screenshots/autoscale.png b/apps/dashr-ocean-optics/screenshots/autoscale.png new file mode 100644 index 000000000..4bfbe6752 Binary files /dev/null and b/apps/dashr-ocean-optics/screenshots/autoscale.png differ diff --git a/apps/dashr-ocean-optics/screenshots/change.png b/apps/dashr-ocean-optics/screenshots/change.png new file mode 100644 index 000000000..3d1279273 Binary files /dev/null and b/apps/dashr-ocean-optics/screenshots/change.png differ diff --git a/apps/dashr-ocean-optics/screenshots/changefail.png b/apps/dashr-ocean-optics/screenshots/changefail.png new file mode 100644 index 000000000..a31e5ff42 Binary files /dev/null and b/apps/dashr-ocean-optics/screenshots/changefail.png differ diff --git a/apps/dashr-ocean-optics/screenshots/changesuccess.png b/apps/dashr-ocean-optics/screenshots/changesuccess.png new file mode 100644 index 000000000..f3f98748e Binary files /dev/null and b/apps/dashr-ocean-optics/screenshots/changesuccess.png differ diff --git a/apps/dashr-ocean-optics/screenshots/initial.png b/apps/dashr-ocean-optics/screenshots/initial.png new file mode 100644 index 000000000..0a7ef89d9 Binary files /dev/null and b/apps/dashr-ocean-optics/screenshots/initial.png differ diff --git a/apps/dashr-ocean-optics/screenshots/on.png b/apps/dashr-ocean-optics/screenshots/on.png new file mode 100644 index 000000000..74be0cabd Binary files /dev/null and b/apps/dashr-ocean-optics/screenshots/on.png differ