diff --git a/_pkgdown.yml b/_pkgdown.yml index 7539e8eb..e59b58b6 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -1,4 +1,4 @@ -url: https://pages.github.com/SafetyGraphics/safetyGraphics +url: https://safetygraphics.github.io/safetyGraphics template: params: @@ -68,10 +68,6 @@ reference: - settingsMappingUI - settingsCode - settingsCodeUI - - title: Data - desc: Data sets used in the apps - contents: - - meta - title: Helper Functions desc: Utility functions used in the apps contents: @@ -86,4 +82,5 @@ reference: - makeChartParams - makeChartSummary - makeMapping + - makeMeta - prepareChart diff --git a/docs/404.html b/docs/404.html index 1013dc52..8da3389f 100644 --- a/docs/404.html +++ b/docs/404.html @@ -1,66 +1,27 @@ - - -
- + + + + -vignettes/Cookbook.Rmd
+ Source: vignettes/Cookbook.Rmd
Cookbook.RmdThis vignette contains a series of examples showing how to initialize the safetyGraphics Shiny app in different scenarios. For a general overview of the app see this vignette. For more details about adding custom charts, see this vignette.
-This vignette contains a series of examples showing how to initialize the safetyGraphics Shiny app in different scenarios.
+Most of the customization shown here is done by changing 4 key parameters in the safetyGraphicsApp() function:
domainData – Domain-level Study Datamapping – List identifying the key columns/fields in your datacharts – Define the charts used in the app.meta – Metadata table with info about required columns and fieldsdomainData and mapping generally change for every study, while charts and meta can generally be re-used across many studies.
The examples here are generally provided with minimal explanation. For a more detailed discussion of the logic behind these examples see the Chart Configuration Vignette or our 2021 R/Pharma Workshop
+safetyGraphics requires R v4 or higher. These examples have been tested using RStudio v1.4, but should work on other platforms with proper configuration.
-You can install safetyGraphics from CRAN like any other R package:
-install.packages("safetyGraphics")
-library("safetyGraphics")
+You can install safetyGraphics from CRAN like any other R package:
+install.packages("safetyGraphics")
+library("safetyGraphics")
Or to use the most recent development version from GitHub, call:
-devtools::install_github("safetyGraphics/safetyCharts", ref="dev")
-library(safetyCharts)
-devtools::install_github("safetyGraphics/safetyGraphics", ref="dev")
-library(safetyGraphics)
-safetyGraphics::safetyGraphicsApp()
-devtools::install_github("safetyGraphics/safetyCharts", ref="dev")
+library(safetyCharts)
+devtools::install_github("safetyGraphics/safetyGraphics", ref="dev")
+library(safetyGraphics)
+safetyGraphics::safetyGraphicsApp()
+To run the app with no customizations using sample AdAM data from the {safetyData} package, install the package and run:
-safetyGraphics::safetyGraphicsApp()
-The data passed in to the safetyGraphics app can be customized using the data parameter in safetyGraphicsApp(). For example, to run the app with SDTM data saved in {safetyData}, call:
sdtm <- list(
- dm=safetyData::sdtm_dm,
- aes=safetyData::sdtm_ae,
- labs=safetyData::sdtm_lb
-)
-
-safetyGraphics::safetyGraphicsApp(domainData=sdtm)
-safetyGraphics::safetyGraphicsApp()
+The next several examples focus on study-specific customizations for loading and mapping data.
+The data passed in to the safetyGraphics app can be customized using the domainData parameter in safetyGraphicsApp(). For example, to run the app with SDTM data saved in {safetyData}, call:
sdtm <- list(
+ dm=safetyData::sdtm_dm,
+ aes=safetyData::sdtm_ae,
+ labs=safetyData::sdtm_lb
+)
+
+safetyGraphics::safetyGraphicsApp(domainData=sdtm)
+Running the app for a single data domain, is similar:
-justLabs <- list(labs=safetyData::adam_adlbc)
-safetyGraphics::safetyGraphicsApp(domainData=justLabs)
+justLabs <- list(labs=safetyData::adam_adlbc)
+safetyGraphics::safetyGraphicsApp(domainData=justLabs)
Note that charts with missing data are automatically dropped and the filtering tab is not present since it requires demographics data by default.
Users can also generate a list of charts and then drop charts that they don’t want to include. For example, if you wanted to drop charts with type of “htmlwidgets” you could run this code.
library(purrr)
-charts <- makeChartConfig() #gets charts from safetyCharts pacakge by default
-notWidgets <- charts %>% purrr::keep(~.x$type != "htmlwidget")
-safetyGraphicsApp(charts=notWidgets)
-The code below adds a new simple chart showing participants’ age distribution by sex.
-ageDist <- function(data, settings){
- p<-ggplot(data = data, aes_(x=as.name(settings$age_col))) +
- geom_histogram() +
- facet_wrap(as.name(settings$sex_col))
- return(p)
-}
-ageDist_chart<-list(
- env="safetyGraphics",
- name="ageDist",
- label="Age Distribution",
- type="plot",
- domain="dm",
- workflow=list(
- main="ageDist"
- )
-)
-charts <- makeChartConfig()
-charts$ageDist<-ageDist_chart
-safetyGraphicsApp(charts=charts)
-For extensive details on adding and customizing different types of charts, see this vignette.
-Users can also import data from a wide-variety of data formats using standard R workflows and then initialize the app. The example below initializes the app using lab data saved as a sas transport file (.xpt)
+xptLabs <- haven::read_xpt('https://github.com/phuse-org/phuse-scripts/blob/master/data/adam/cdiscpilot01/adlbc.xpt?raw=true')
+safetyGraphics::safetyGraphicsApp(domainData=list(labs=xptLabs))
+Next, let’s initialize the the app with non-standard data. {safetyGraphics} automatically detects AdAM and SDTM data when possible, but for non-standard data, the user must provide a data mapping. This can be done in the app using the data/mapping tab, or can be done when the app is initialized by passing a mapping list to safetyGraphicsApp(). For example:
notAdAM <- list(labs=safetyData::adam_adlbc %>% rename(id = USUBJID))
-idMapping<- list(labs=list(id_col="id"))
-safetyGraphicsApp(domainData=notAdAM, mapping=idMapping)
-For a more realistic example, consider this labs data set (csv). The data can be loaded in to safetyGraphics with the code below, but several items in the mapping page need to be filled in:
-labs <- read.csv("https://raw.githubusercontent.com/SafetyGraphics/SafetyGraphics.github.io/master/pilot/SampleData_NoStandard.csv")
-safetyGraphics::safetyGraphicsApp(domainData=list(labs=labs))
+notAdAM <- list(labs=safetyData::adam_adlbc %>% rename(id = USUBJID))
+idMapping<- list(labs=list(id_col="id"))
+safetyGraphicsApp(domainData=notAdAM, mapping=idMapping)
+For a more realistic example, consider this labs data set (csv). The data can be loaded in to safetyGraphics with the code below, but several items in the mapping page need to be filled in:
+labs <- read.csv("https://raw.githubusercontent.com/SafetyGraphics/SafetyGraphics.github.io/master/pilot/SampleData_NoStandard.csv")
+safetyGraphics::safetyGraphicsApp(domainData=list(labs=labs))

Fortunately there is no need to re-enter this mapping information in every time you re-start the app. After filling in these values once, you can export code to restart the app with the specified settings pre-populated. First, click on the setting icon in the header and then on “code” to see this page:

The YAML code provided here captures the updates you’ve made on the mapping page. To re-start the app with those settings, just save these YAML code in a new file called customSettings.yaml in your working directory, and then call:
labs <- read.csv("https://raw.githubusercontent.com/SafetyGraphics/SafetyGraphics.github.io/master/pilot/SampleData_NoStandard.csv")
-customMapping <- read_yaml("customSettings.yaml")
-safetyGraphics::safetyGraphicsApp(
- domainData=list(labs=labs),
- mapping=customMapping
-)
+labs <- read.csv("https://raw.githubusercontent.com/SafetyGraphics/SafetyGraphics.github.io/master/pilot/SampleData_NoStandard.csv")
+customMapping <- read_yaml("customSettings.yaml")
+safetyGraphics::safetyGraphicsApp(
+ domainData=list(labs=labs),
+ mapping=customMapping
+)
Note, that for more complex customizations, the setting page also provides a .zip file with a fully re-usable version of the app.
The remaining examples focus on creating charts that are reusable across many studies. For extensive details on adding and customizing different types of charts, see this vignette.
+Users can also generate a list of charts and then drop charts that they don’t want to include. For example, if you wanted to drop charts with type of “htmlwidgets” you could run this code.
library(purrr)
+charts <- makeChartConfig() #gets charts from safetyCharts pacakge by default
+notWidgets <- charts %>% purrr::keep(~.x$type != "htmlwidget")
+safetyGraphicsApp(charts=notWidgets)
+Users can also make modifications to the default charts by editing the list of charts directly.
+charts <- makeChartConfig() #gets charts from safetyCharts pacakge by default
+charts$aeTimelines$label <- "An AMAZING timeline"
+safetyGraphicsApp(charts=charts)
+This example creates a simple “hello world” chart that is not linked to the data or mapping loaded in the app.
+helloWorld <- function(data, settings){
+ plot(-1:1, -1:1)
+ text(runif(20, -1,1),runif(20, -1,1),"Hello World")
+}
+
+# Chart Configuration
+helloworld_chart<-list(
+ env="safetyGraphics",
+ name="HelloWorld",
+ label="Hello World!",
+ type="plot",
+ domain="aes",
+ workflow=list(
+ main="helloWorld"
+ )
+)
+
+safetyGraphicsApp(charts=list(helloworld_chart))
+The code below adds a new simple chart showing participants’ age distribution by sex.
+ageDist <- function(data, settings){
+ p<-ggplot(data = data, aes(x=.data[[settings$age_col]])) +
+ geom_histogram() +
+ facet_wrap(settings$sex_col)
+ return(p)
+}
+
+ageDist_chart<-list(
+ env="safetyGraphics",
+ name="ageDist",
+ label="Age Distribution",
+ type="plot",
+ domain="dm",
+ workflow=list(
+ main="ageDist"
+ )
+)
+charts <- makeChartConfig()
+charts$ageDist<-ageDist_chart
+safetyGraphicsApp(charts=charts)
+Here we extend example 9 to include the creating of a new data domain with custom metadata, which is bound to the chart object as chart$meta. See ?makeMeta for more detail about the creation of custom metadata.
helloMeta <- tribble(
+ ~text_key, ~domain, ~label, ~standard_hello, ~description,
+ "x_col", "hello", "x position", "x", "x position for points in hello world chart",
+ "y_col", "hello", "y position", "y", "y position for points in hello world chart"
+) %>% mutate(
+ col_key = text_key,
+ type="column"
+)
+
+helloData<-data.frame(x=runif(50, -1,1), y=runif(50, -1,1))
+
+helloWorld <- function(data, settings){
+ plot(-1:1, -1:1)
+ text(data[[settings$x_col]], data[[settings$y_col]], "Custom Hello Domain!")
+}
+
+helloChart<-prepareChart(
+ list(
+ env="safetyGraphics",
+ name="HelloWorld",
+ label="Hello World!",
+ type="plot",
+ domain="hello",
+ workflow=list(
+ main="helloWorld"
+ ),
+ meta=helloMeta
+ )
+)
+
+charts <- makeChartConfig()
+charts$hello <- helloChart #Easy to combine default and custom charts
+data<-list(
+ labs=safetyData::adam_adlbc,
+ aes=safetyData::adam_adae,
+ dm=safetyData::adam_adsl,
+ hello=helloData
+)
+
+#no need to specify meta since safetyGraphics::makeMeta() will generate the correct list by default.
+safetyGraphicsApp(
+ domainData=data,
+ charts=charts
+)
+This example defines a custom ECG data domain and adapts an existing chart for usage there. See this PR for a full implementation of the ECG domain in safetyCharts.
+adeg <- readr::read_csv("https://physionet.org/files/ecgcipa/1.0.0/adeg.csv?download")
+
+ecg_meta <-tibble::tribble(
+ ~text_key, ~domain, ~label, ~description, ~standard_adam, ~standard_sdtm,
+ "id_col", "custom_ecg", "ID column", "Unique subject identifier variable name.", "USUBJID", "USUBJID",
+ "value_col", "custom_ecg", "Value column", "QT result variable name.", "AVAL", "EGSTRESN",
+ "measure_col", "custom_ecg", "Measure column", "QT measure variable name", "PARAM", "EGTEST",
+"studyday_col", "custom_ecg", "Study Day column", "Visit day variable name", "ADY", "EGDY",
+ "visit_col", "custom_ecg", "Visit column", "Visit variable name", "ATPT", "EGTPT",
+ "visitn_col", "custom_ecg", "Visit number column", "Visit number variable name", "ATPTN", NA,
+ "period_col", "custom_ecg", "Period column", "Period variable name", "APERIOD", NA,
+ "unit_col", "custom_ecg", "Unit column", "Unit of measure variable name", "AVALU", "EGSTRESU"
+) %>% mutate(
+ col_key = text_key,
+ type="column"
+)
+
+qtOutliers<-prepare_chart(read_yaml('https://raw.githubusercontent.com/SafetyGraphics/safetyCharts/dev/inst/config/safetyOutlierExplorer.yaml') )
+qtOutliers$label <- "QT Outlier explorer"
+qtOutliers$domain <- "custom_ecg"
+qtOutliers$meta <- ecg_meta
+
+safetyGraphicsApp(
+ meta=ecg_meta,
+ domainData=list(custom_ecg=adeg),
+ charts=list(qtOutliers)
+)
+Developed by Jeremy Wildfire, Becca Krouse, Preston Burns, Xiao Ni, James Buchanan, Susan Duke.
+ +Developed by Jeremy Wildfire, Becca Krouse, Preston Burns, Xiao Ni, James Buchanan, Susan Duke.
vignettes/TechnicalFAQ.Rmd
+ Source: vignettes/TechnicalFAQ.Rmd
TechnicalFAQ.RmdThis vignette answers frequently asked technical questions about {safetyGraphics}. It addressees questions on a variety of technical topics including Qualification and Validation status, Common Data Pipelines and Security.
-Whenever new questions come in, we’ll update the version of this FAQ in our wiki - so check there first if you have a question. We’ll update the vignette on CRAN whenever a new version of the package is released.
-A: As of the version 2 release, the safetyGraphics package is intended for exploratory use only and is not validated or qualified per 21 CFR Part 11. No warranty or guarantees are included as part of the package. Further, any formal validation should be fit for purpose and follow your organization’s procedures. That said, extensive quality checks are built in to the package (see the question below for details) and in to many of charts that are included by default. We follow the work of R Validation hub closely, and may release validation guidance based on the approach described in their white paper at a future date.
-Whenever new questions come in, we’ll update the version of this FAQ in our wiki - so check there first if you have a question. We’ll update the vignette on CRAN whenever a new version of the package is released.
+A: Yes. Check out our contributor guidelines. Feel free to follow-up with questions on the discussion board.
+A: Yes. Fill out this Google Form and we’ll get in touch.
+A: As of the version 2 release, the safetyGraphics package is intended for exploratory use only and is not validated or qualified per 21 CFR Part 11. No warranty or guarantees are included as part of the package. Further, any formal validation should be fit for purpose and follow your organization’s procedures. That said, extensive quality checks are built in to the package (see the question below for details) and into many of charts that are included by default. The R Consortium has guidance on usage of R in Regulated Trials and we also follow the work of R Validation hub closely, and may release validation guidance based on the approach described in their white paper at a future date.
+A: Study-specific instances of most safetyGraphics charts can be exported either as an R script or as a standalone html report. It may be possible to treat those outputs as standard TLFs (Tables, Listings and Figures) and conduct QC/Validation on them using standard statistical SOPs. Consult with your companies procedures to confirm.
A: Several layers of quality control are included in our standard development workflow including: - Over 200 automated unit tests with testthat that run automatically via Continuous integration with GitHub actions. - Automated unit tests for shiny modules run via a headless browser using shinytest. - Pass all standard R checks in R cmd check - Full code review of all new functionality documented in GitHub PR. - Issue tracking in GitHub - Formal Alpha/Beta user testing before major releases - Basic user tests conducted before minor release
A: Several layers of quality control are included in our standard development workflow including: - Over 200 automated unit tests with testthat that run automatically via Continuous integration with GitHub actions. - Automated unit tests for shiny modules run via a headless browser using shinytest. - Pass all standard R checks in R cmd check - Full code review of all new functionality documented in GitHub PR. - Issue tracking in GitHub - Formal Alpha/Beta user testing before major releases - Basic user tests conducted before minor release
A: As of the v2 release, safetyGraphics is mostly used in an exploratory fashion for a variety of use cases. The details of a ‘production’ implementation of safetyGraphics in a GxP environment would largely be dictated the intended use of the tool. For example, using safetyGraphics as your primary safety oversight platform would likely require more work than using it as a supplemental exploratory tool for biostatisticians.
+That said, the issues surrounding a ‘production’ deployment are mostly technical and operational at this point and could likely be overcome by a motivated organization with ample technical expertise. Some of the issues that would need to be addressed in a production deployment:
+Many of these issues aren’t specific to safetyGraphics and may be easier to address for an organization that has experience using R and Shiny in production. As discussed above, there is a significant push towards using R for many aspects of clinical trials. We plan to keep safetyGraphics up to date with emerging best practices and will provide supporting documentation whenever possible.
+Finally, it is worth noting “Productionize” and “Validate” are slightly different. Joe Cheng (the primary author of Shiny) has a nice talk on this topic from a software engineering perspective.
A: Yes. The package is open source and free to use. Here’s a link to the license and a quick summary of how it works.
+A: safetyGraphics graphics has been used in a variety of ways. Some of the most common use cases thus far are: - Analysts exploring safety data - Clinicians monitoring ongoing studies - Analysts and Clinicians evaluating safety signals in completed studies
-As an open source tool with a flexible data pipeline, many other use cases have been discussed: - Data review by Data Safety Monitoring Boards (link) - Submissions to FDA (link) - Visualizing Analysis results data (link) - Risk based monitoring
-A: No. Any standard (or non-standard) data can be loaded as long as it meets the minimum data requirements for the selected data domain. Metadata capturing default CDISC standards are included with the app (see ?safetyGraphics::meta) so that data mappings can be automatically populated when AdAM and SDTM data are loaded. Other data standards require the user to manually complete the data mapping in the mapping tab - see the cookbook vignette for examples.
A: This topic is covered in detail in the [Loading data section of the Introductory vignette]https://github.com/SafetyGraphics/safetyGraphics/wiki/Intro#loading-study-data)safetyGraphics is designed to support a flexible data pipeline that supports many data types. In short, data can be loaded using the the dataDomains parameter in the safetyGraphicsApp() function or via the safetyGraphicsInit() graphical user interface.
safetyGraphicsApp() - custom data can be loaded via the dataDomains parameter, which should be a list containing dataframes or tibbles for each clinical domain; that list can be populated by importing data from any number of sources including databases, sas files or any number of other sources. See the cookbook vignette for some basic examples of loading custom data.
As an open source tool with a flexible data pipeline, many other use cases have been discussed: - Data review by Data Safety Monitoring Boards (link) - Visualizing Analysis results data (link) - Risk based monitoring
+A: No. Any standard (or non-standard) data can be loaded as long as it meets the minimum data requirements for the selected data domain. Metadata capturing default CDISC standards are included with the app (see ?safetyGraphics::meta) so that data mappings can be automatically populated when AdAM and SDTM data are loaded. Other data standards require the user to manually complete the data mapping in the mapping tab - see the cookbook vignette for examples.
A: This topic is covered in detail in the Loading data section of the Introductory vignette. safetyGraphics is designed to support a flexible data pipeline that supports many data types. In short, data can be loaded using the dataDomains parameter in the safetyGraphicsApp() function or via the safetyGraphicsInit() graphical user interface.
safetyGraphicsApp() - custom data can be loaded via the dataDomains parameter, which should be a list containing dataframes or tibbles for each clinical domain; that list can be populated by importing data from any number of sources including databases, sas files or any number of other sources. See the cookbook vignette for some basic examples of loading custom data.
safetyGraphicsInit() - allows users to load tabular data from a variety of sources using the point-and-click interface provided in the {datamods} package.
More detail is provided in the Loading data section of the Introductory vignette
-A: The safetyGraphics app can be shared using standard shiny methodology. More details for a specific use cases are given in the next few questions. Charts created by safetyGraphics can also be exported and re-used. Charts created with htmlwidgets are especially flexible and can be used in many contexts - including in web applications outside of R.
-safetyGraphics to shinyapps.io to explore trial data from my organization?A: No, we advise against loading non-authorized, private, or non-deidentified patient data outside of your organization’s firewall. Consult with your IT and QA first. There is huge risk associated with confidentiality, IP, and patient privacy. Also refer to ShinyApps.io Chapter 8 Security and Compliance.
-safetyGraphics to an internal rsconnect server?A: Yes - the easiest way to do this is likely to deploy a customized app using shiny modules. In general, you’ll want to use wrap safetyGraphicsServer(), safetyGraphicsUI() and app_startup() in the context of shiny_app and then call rsconnect::deployApp(). The script below shows the code for the app deployed at https://jwildfire.shinyapps.io/safetyGraphics/ using demo data.
library(safetyGraphics)
-library(shiny)
-
-domainData<-list(
- labs=safetyData::adam_adlbc,
- aes=safetyData::adam_adae,
- dm=safetyData::adam_adsl
-)
-
-config <- app_startup(
- domainData=domainData,
- meta=safetyGraphics::meta,
- autoMapping = TRUE,
- filterDomain="dm"
-)
-
-shinyApp(
- ui = safetyGraphicsUI("sg",config$meta, config$domainData, config$mapping, config$standards),
- server = function(input,output,session){
- callModule(
- safetyGraphicsServer,
- "sg",
- config$meta,
- config$mapping,
- config$domainData,
- config$charts,
- config$filterDomain
- )
- }
-)
-
-A: This is a very good question @AlbrechtDurer, and in my experience the answer is complex and varies for different use cases. Focusing on specific toxicities helps, but probably isn’t enough in really big studies. In those cases, I think the most important thing is to design a good data pipeline that includes both a database backend (as opposed to loading all of your study data each time you initialize the app) and visualizations that summarize the data in a reasonable way (as opposed to just plotting every single data point in a scatter plot no matter what). Fortunately this is all doable in R, and improvements in this area are our radar for safetyGraphics after v2 goes live.
+More detail is provided in the Loading data section of the Introductory vignette
+Since the safetyGraphics app typically uses data subject to GxP regulations, data security is extremely important and should always be discussed with your organizations IT and regulatory departments before loading any study data in to the application. No warranty or guarantees are included as part of the package.
+Just like the discussion regarding Validation and Quality Control, data security requirements are dictated by the intended use of the app and should be fit for purpose. There are many different ways to run shiny applications, and the security implications of each approach varies. For example, having statisticians run the app locally using R Studio is quite different than deploying the app on a service like shinyapps.io. This complexity is all the more reason to discuss with IT. There are many resources - related to data security for clinical trials in general (1, 2, 3) and discussing data security in shiny (4, 5, 6) - that could help to facilitate these discussion.
+A: The safetyGraphics app can be shared using standard shiny methodology. More details for specific use cases are given in the next few questions. Charts created by safetyGraphics can also be exported and re-used. Charts created with htmlwidgets are especially flexible and can be used in many contexts - including in web applications outside of R.
+A: It depends a bit on your use-case and how the app is hosted. For example, analysts using the data in an exploratory fashion can probably just run it from RStudio, but if multiple medical monitors using the app for medical monitoring in active studies probably need a more robust (and possibly validated) set up using Shiny Server, RStudio Connect or something similar.
+safetyGraphics to shinyapps.io to explore trial data from my organization?
+A: We advise against loading non-authorized, private, or non-deidentified patient data outside of your organization’s firewall. Consult with your IT and QA first. There is huge risk associated with confidentiality, IP, and patient privacy. Also refer to ShinyApps.io Chapter 8 Security and Compliance.
+safetyGraphics to an internal RStudio Connect server?
+A: Yes. The script below should be easy to deploy via the RStudio interface or by running rsconnect::deployApp() and can easily be customized to support custom data and charts.
# Launch the ShinyApp (Do not remove this comment)
+library(safetyGraphics)
+safetyGraphics::safetyGraphicsApp(runNow = FALSE)
+A: Several of the JavaScript charts build using htmlwidgets do have performance issues with very large data sets. Focusing on specific toxicities helps, but probably isn’t enough for really big studies. In those cases, I think the most important thing is to design a your data pipeline to include both a database backend (as opposed to loading all of your study data each time you initialize the app) and visualizations that summarize the data in a reasonable way (as opposed to just plotting every single data point in a scatter plot no matter what). Fortunately this is all doable in R, and improvements in this area are on our road map for future releases of safetyGraphics.
Developed by Jeremy Wildfire, Becca Krouse, Preston Burns, Xiao Ni, James Buchanan, Susan Duke.
+ +Developed by Jeremy Wildfire, Becca Krouse, Preston Burns, Xiao Ni, James Buchanan, Susan Duke.