-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathapp.json
1 lines (1 loc) · 36.7 KB
/
app.json
1
[{"name":"app.R","content":"# A lightweight ShinyApp for coding of text data for qualitative analysis\n# QualCA (pronounced like Quokka; not yet final)\n# Written by William Ngiam\n# Started on Sept 15 2024\n# Version 0.1.5 (Jan 16 2024)\n#\n# Motivated by the ANTIQUES project\n#\n# Wishlist\n# Take in various corpus input files\n# Edit text viewer to show all relevant information\n# Add codebook, codes, extracts - have all linked and viewable.\n# Apply specific colours for the code\n# Collaborative coding <- maybe a google sheet that contains code/extracts...\n# Floating menus?\n# Dealing with line breaks\n# Audit trail page - running commentary/notes\n# Automatic sending to OSF project\n# \"Stats\" tab <- Number of extracts, number of documents coded\n#\n# Glossary\n# Corpus: The body of text to be qualitatively analysed\n# Document: Each item that makes up the corpus\n# Extract: The harvested section of text that is coded\n#\n# --- #\n\n# Load required packages\nlibrary(tools)\n#library(pdftools)\nlibrary(shiny)\nlibrary(shinydashboard)\nlibrary(shinyalert)\nlibrary(shinyjs)\nlibrary(shinyjqui)\nlibrary(DT) # for data presentation purposes\nlibrary(tibble)\nlibrary(dplyr) # for data wrangling\nlibrary(readr)\nlibrary(stringr) # for handling strings\nlibrary(markdown)\nlibrary(sortable) # For list sorting\n#library(officer) # Uncomment if want to load in word documents locally\n\n# Workaround for Chromium Issue 468227\n# Copied from https://shinylive.io/r/examples/#r-file-download\ndownloadButton <- function(...) {\n tag <- shiny::downloadButton(...)\n tag$attribs$download <- NULL\n tag\n}\n\n### UI ###\nui <- dashboardPage(\n dashboardHeader(title = \"quokka - a qualitative coding app\",\n titleWidth = 350),\n dashboardSidebar(\n width = 350,\n fluidPage(HTML(\"<h3>Upload Files<\/h3>\")),\n fileInput(\n \"corpusFile\",\n \"Load Corpus\",\n accept = c(\n \"text/csv\",\n \"text/comma-separated-values,text/plain\",\n \".csv\",\n \".pdf\",\n \".docx\"\n )\n ),\n # Upload the corpus of documents to-be-analysed\n uiOutput(\"documentTextSelector\"),\n fileInput(\n \"codebookFile\",\n \"Load Codebook\",\n accept = c(\"text/csv\", \"text/comma-separated-values,text/plain\", \".csv\")\n ),\n # Upload any existing codebook\n uiOutput(\"codebookSelector\"),\n fluidPage(HTML(\"<h3>Download Codebook<\/h3>\")),\n fluidPage(\n downloadButton(\"downloadData\",\n label = \"Download Codebook\",\n style = \"color: black\")\n ),\n fluidPage(HTML(\"<br><h3>Pages<\/h3>\")),\n sidebarMenu(\n id = \"sbMenu\",\n menuItem(\"Home\", tabName = \"home\", icon = icon(\"home\")),\n menuItem(\"Coding\", tabName = \"coder\", icon = icon(\"highlighter\")),\n menuItem(\n \"Reviewing\",\n tabName = \"reviewer\",\n icon = icon(\"magnifying-glass\")\n ),\n menuItem(\n \"Sorting\",\n tabName = \"themes\",\n icon = icon(\"layer-group\")\n )\n ),\n fluidPage(HTML(\"<br>Created by William Ngiam\"))\n ),\n dashboardBody(tabItems(\n # The home tab\n tabItem(tabName = \"home\",\n useShinyjs(), #Initialize shinyjs\n fluidPage(includeHTML(\"home_text.html\"))),\n # The coding tab\n tabItem(\n tabName = \"coder\",\n useShinyjs(),\n # Initialize shinyjs\n fluidRow(\n jqui_resizable(\n box(\n title = \"Document Viewer\",\n width = 6,\n solidHeader = TRUE,\n status = \"primary\",\n fluidPage(fluidRow(\n column(\n width = 5,\n numericInput(\n inputId = \"documentID_viewer\",\n label = \"Document #\",\n value = \"currentDocumentID\"\n )\n ),\n column(\n width = 5,\n offset = 1,\n HTML(\"<strong>Document Navigation <\/strong><br>\"),\n actionButton(\"prevDocument\", \"Previous\", icon = icon(\"arrow-left\")),\n actionButton(\"nextDocument\", \"Next\", icon = icon(\"arrow-right\"))\n )\n )),\n bootstrapPage(\n tags$style(\"#textDisplay {\n overflow-y: auto;\n max-height: 55vh;\n }\"),\n uiOutput(\"textDisplay\"),\n tags$script(\n '\n function getSelectionText() {\n var text = \"\";\n if (window.getSelection) {\n text = window.getSelection().toString();\n } else if (document.selection) {\n text = document.selection.createRange().text;\n }\n return text;\n }\n document.onmouseup = document.onkeyup = document.onselectionchange = function() {\n var selection = getSelectionText();\n Shiny.onInputChange(\"extract\",selection);\n };\n '\n )\n )\n )\n ),\n box(\n title = \"Quick Look\",\n width = 6,\n solidHeader = TRUE,\n status = \"primary\",\n fluidPage(DTOutput(\"counterTable\"))\n ),\n ),\n fluidRow(\n box(\n title = \"Research Question(s)\",\n width = 12,\n solidHeader = TRUE,\n status = \"primary\",\n textAreaInput(\"text\",\n \"Enter your research question(s) below\")\n )\n ),\n fluidRow(\n box(\n title = \"Codebook\",\n width = 12,\n solidHeader = TRUE,\n status = \"primary\",\n actionButton(\n \"addSelectedText\",\n \"Add Selected Text as Extract\",\n icon = icon(\"pencil\")\n ),\n actionButton(\"deleteExtract\", \"Delete Extract from Codebook\", icon = icon(\"trash\")),\n actionButton(\"addColumn\", \"Add Column to Codebook\", icon = icon(\"plus\")),\n actionButton(\"removeColumn\", \"Remove Column from Codebook\", icon = icon(\"minus\")),\n HTML(\"<br><br>\"),\n DTOutput(\"codebookTable\")\n )\n )\n ),\n tabItem(tabName = \"reviewer\",\n fluidRow(\n box(\n title = \"Codebook\",\n width = 3,\n solidHeader = TRUE,\n status = \"primary\",\n DTOutput(\"codesTable\")\n ),\n box(\n title = \"Extracts\",\n width = 9,\n solidHeader = TRUE,\n status = \"primary\",\n DTOutput(\"reviewTable\")\n )\n ),\n fluidRow(\n box(\n title = \"Document Review\",\n width = 12,\n solidHeader = TRUE,\n status = \"primary\",\n uiOutput(\"reviewDocumentText\")\n )\n )),\n tabItem(tabName = \"themes\",\n fluidRow(\n box(\n title = \"Themes\",\n width = 12,\n solidHeader = TRUE,\n status = \"primary\",\n actionButton(\"addTheme\", \"Create a new theme\", icon = icon(\"plus\")),\n downloadButton(\"downloadThemes\", \"Download themes\", icon = icon(\"floppy-disk\")),\n uiOutput(\"uniqueCodes\")\n )\n ))\n ))\n)\n\n### SERVER ###\nserver <- function(input, output, session) {\n # Reactive values to store data and codebook\n values <- reactiveValues(\n corpus = NULL,\n # the text data to be analysed\n documentTextColumn = NULL,\n # the column which contains document text\n selectedText = NULL,\n codebook = tibble(\n Theme = as.character(),\n Code = as.character(),\n Extract = as.character(),\n Document_ID = as.numeric(),\n Timestamp = as.character()\n ),\n counter = data.frame(),\n currentDocumentIndex = 1,\n nDocuments = 1,\n selection = NULL,\n thisExtract = NULL,\n nThemes = 2 # for sorting into themes\n )\n \n ### FUNCTIONS ###\n # Update text display based on currentDocumentIndex and documentTextColumn\n updateTextDisplay <- function() {\n req(values$corpus, values$documentTextColumn)\n text <-\n values$corpus[[values$documentTextColumn]][values$currentDocumentIndex]\n #text <- sub(\"<span*/span>\",\"\",text) # Remove any leftover HTML\n text <-\n gsub(pattern = \"\\n\", replacement = \"\", text) # Remove line breaks because they \"break\" the app...\n text <- gsub(pattern = \"\\\\s+\", replacement = \" \", text)\n #text <- gsub(pattern = \"\\\"\", replacement = \"'\", text) # Replace double quotation marks with single ones.\n textLength <- str_length(text)\n \n # Add highlights here by adding HTML tags to text\n # Retrieve document ID\n currentDoc <- values$currentDocumentIndex\n \n # Get saved extracts for document ID\n currentExtracts <- values$codebook %>%\n dplyr::filter(Document_ID == currentDoc)\n oldStrings = currentExtracts$Extract\n \n if (length(oldStrings) > 0) {\n # Detect where extracts start and finish in text\n stringLocs <-\n as.data.frame(str_locate(text, paste0(\"\\\\Q\", oldStrings, \"\\\\E\")))\n stringLocs <-\n na.omit(stringLocs) # omit NA where matches don't work with special characters\n addedStringStart = last(stringLocs$start)\n # Sort these locations by their starting order\n stringLocs <- stringLocs %>%\n arrange(start)\n \n # If more than one string, check for overlaps (leetcode 56)\n if (length(oldStrings) > 1) {\n # Write the first interval to start result\n reducedStrings <- tibble(start = as.numeric(),\n end = as.numeric()) %>%\n tibble::add_row(\n start = head(stringLocs$start, n = 1),\n end = head(stringLocs$end, n = 1)\n )\n \n for (i in 1:nrow(stringLocs)) {\n # Retrieve the next interval\n thisStringStart <- stringLocs$start[i]\n thisStringEnd <- stringLocs$end[i]\n # Check for overlap\n if (between(\n thisStringStart,\n tail(reducedStrings, n = 1)$start,\n tail(reducedStrings, n = 1)$end\n )) {\n # If overlap, change value of interval\n reducedStrings[nrow(reducedStrings), ncol(reducedStrings)] = max(thisStringEnd, tail(reducedStrings, n = 1)$end)\n }\n else {\n # If no overlap, add the interval\n reducedStrings <- reducedStrings %>%\n add_row(start = thisStringStart, end = thisStringEnd)\n }\n }\n # Arrange in descending order for inserting HTML\n stringLocs <- reducedStrings %>%\n arrange(desc(start))\n }\n \n for (i in 1:nrow(stringLocs)) {\n # Get start and end\n stringStart <- stringLocs$start[i]\n stringEnd <- stringLocs$end[i]\n theString <- str_sub(text, stringStart, stringEnd)\n # Get string\n if (stringStart == addedStringStart) {\n str_sub(text, stringStart, stringEnd) <-\n paste0(\n \"<span id=\\\"lastString\\\" style=\\\"background-color: powderblue\\\">\",\n theString,\n \"<\/span>\"\n )\n } else {\n str_sub(text, stringStart, stringEnd) <-\n paste0(\"<span style=\\\"background-color: powderblue\\\">\",\n theString,\n \"<\/span>\")\n }\n }\n }\n \n # Hope it works\n if (textLength >= 600) {\n output$textDisplay <- renderUI({\n tags$div(\n id = \"textDisplay\",\n tags$p(HTML(text), id = \"currentText\", style = \"font-size: 20px\"),\n tags$script('\n lastString.scrollIntoView();\n ')\n )\n })\n } else {\n output$textDisplay <- renderUI({\n tags$div(id = \"textDisplay\",\n tags$p(HTML(text), id = \"currentText\", style = \"font-size: 20px\"))\n })\n }\n }\n \n # Update document viewer ID number\n updateDocumentID <- function() {\n updateNumericInput(\n session,\n inputId = \"documentID_viewer\",\n value = values$currentDocumentIndex,\n max = values$nDocuments\n )\n }\n \n # Save codebook locally\n saveCodebook <- function() {\n req(values$codebook)\n write.csv(values$codebook, \"temp_codebook.csv\", row.names = FALSE)\n }\n \n # Render codebook\n renderCodebook <- function() {\n output$codebookTable <- renderDT({\n idColNum <- which(colnames(values$codebook) == \"Timestamp\")\n datatable(\n values$codebook,\n options = list(order = list(idColNum - 1, 'desc')),\n editable = TRUE,\n rownames = FALSE\n )\n })\n }\n \n # Render code counter\n renderCounter <- function() {\n values$counter <- values$codebook %>%\n count(Code, name = \"Instances\")\n \n output$counterTable <- renderDT({\n datatable(\n values$counter,\n editable = list(target = \"cell\",\n disable = list(columns = 1)),\n rownames = FALSE\n )\n })\n \n output$codesTable <- renderDT({\n datatable(\n values$counter,\n editable = list(target = \"cell\",\n disable = list(columns = 1)),\n rownames = FALSE\n )\n })\n }\n \n ### UPLOADING DATA ###\n # Load corpus CSV file and update column selector\n observeEvent(input$corpusFile, {\n req(input$corpusFile)\n if (file_ext(input$corpusFile$datapath) == \"csv\") {\n values$corpus <-\n read.csv(input$corpusFile$datapath, stringsAsFactors = FALSE)\n } else if (file_ext(input$corpusFile$datapath) == \"txt\") {\n values$corpus <-\n read.delim(input$corpusFile$datapath,\n header = FALSE,\n sep = \"\\n\")\n } else if (file_ext(input$corpusFile$datapath) == \"pdf\") {\n pdfDocumentText <- pdf_text(input$corpusFile$datapath)\n values$corpus <- tibble(text = pdfDocumentText)\n } else if (file_ext(input$corpusFile$datapath) == \"docx\") {\n doc <- read_docx(input$corpusFile$datapath)\n docContent <- docx_summary(doc) %>%\n filter(text != \"\")\n \n values$corpus <-\n tibble(text = paste(docContent$text, collapse = \"\"))\n \n }\n colnames <- colnames(values$corpus)\n values$documentTextColumn <-\n colnames[1] # Default to the first column\n values$currentDocumentIndex <- 1 # Reset index on new file load\n values$nDocuments <-\n nrow(values$corpus) # Get number of documents\n \n output$documentTextSelector <- renderUI({\n req(values$corpus)\n selectInput(\n \"documentTextColumn\",\n \"Select Text Column\",\n choices = colnames,\n selected = values$documentTextColumn\n )\n })\n \n updateTextDisplay()\n renderCodebook()\n renderCounter()\n \n updateSelectInput(\n session,\n \"codebookSelector\",\n choices = values$codebook$Theme,\n selected = NULL\n )\n })\n \n # Update codebook on upload\n observeEvent(input$codebookFile, {\n req(input$codebookFile)\n values$codebook <-\n read.csv(input$codebookFile$datapath, stringsAsFactors = FALSE)\n colnames <- colnames(values$codebook)\n renderCodebook()\n renderCounter()\n updateTextDisplay()\n })\n \n \n ## DOCUMENT NAVIGATION ##\n # Update display after document text column is selected\n observeEvent(input$documentTextColumn, {\n values$documentTextColumn <- input$documentTextColumn\n updateTextDisplay()\n })\n \n # Updated display after document ID is scrolled\n observeEvent(input$documentID_viewer, {\n req(values$corpus)\n if (input$documentID_viewer > nrow(values$corpus)) {\n values$currentDocumentIndex <- nrow(values$corpus)\n updateTextDisplay()\n } else if (values$currentDocumentIndex > 1) {\n values$currentDocumentIndex <- input$documentID_viewer\n # Updated text display\n updateTextDisplay()\n }\n })\n \n # Action button for previous document\n observeEvent(input$prevDocument, {\n req(values$corpus)\n if (values$currentDocumentIndex > 1) {\n values$currentDocumentIndex <- values$currentDocumentIndex - 1\n \n # Updated text display\n updateDocumentID()\n updateTextDisplay()\n \n }\n })\n \n # Action button for next document\n observeEvent(input$nextDocument, {\n req(values$corpus)\n if (values$currentDocumentIndex < nrow(values$corpus)) {\n values$currentDocumentIndex <- values$currentDocumentIndex + 1\n updateDocumentID()\n updateTextDisplay()\n }\n })\n \n ## CODEBOOK ACTIONS ##\n # Save after any edits to the codebook\n observeEvent(input$codebookTable_cell_edit, {\n values$codebook <-\n editData(values$codebook,\n input$codebookTable_cell_edit,\n rownames = FALSE,\n 'codebookTable')\n saveCodebook()\n renderCounter()\n updateTextDisplay()\n })\n \n # Add selected text as extract\n observeEvent(input$addSelectedText, {\n req(values$codebook, input$extract)\n selectedText <-\n input$extract # Highlighted text within document to-be-extracted\n \n # Detect any selected rows\n # if (nrow(values$counter) > 0) {\n if (!is.null(input$counterTable_rows_selected)) {\n selectedRow <- input$counterTable_rows_selected\n allCodes <- values$counter$Code\n selectedCode <- allCodes[selectedRow]\n } else {\n selectedCode = \"\"\n }\n # } else {\n # selectedCode = \"\"\n # }\n \n # Append the selected text to codebook as extract\n values$codebook <- values$codebook %>%\n add_row(\n Theme = \"\",\n Code = selectedCode,\n Extract = selectedText,\n Document_ID = values$currentDocumentIndex,\n Timestamp = as.character(Sys.time())\n )\n \n renderCodebook()\n renderCounter()\n updateTextDisplay()\n })\n \n # Delete extract from codebook\n observeEvent(input$deleteExtract, {\n req(input$codebookTable_rows_selected)\n whichRow <-\n input$codebookTable_rows_selected # highlighted rows in codebook\n values$codebook <- values$codebook[-whichRow,]\n renderCodebook()\n renderCounter()\n updateTextDisplay()\n })\n \n # Get column name in response to add button\n observeEvent(input$addColumn, {\n req(values$codebook)\n showModal(modalDialog(\n textInput(\n \"colName\",\n \"Name of new column in codebook:\",\n value = \"Notes\",\n placeholder = \"Notes\"\n ),\n footer = tagList(modalButton(\"Cancel\"),\n actionButton(\"addCol\", \"OK\"))\n ))\n })\n \n # Add column to codebook after getting name\n observeEvent(input$addCol, {\n req(values$codebook)\n newColName = input$colName\n if (newColName == \"\") {\n newColName = \"New column\"\n }\n values$codebook <- values$codebook %>%\n mutate(newColumn = as.character(\"\")) %>%\n rename_with(~ newColName, newColumn)\n \n removeModal()\n renderCodebook()\n })\n \n # Remove column name in response to add button\n observeEvent(input$removeColumn, {\n req(values$codebook)\n showModal(modalDialog(\n selectInput(\n \"minusCol\",\n \"Select which column to remove\",\n choices = colnames(values$codebook)\n ),\n footer = tagList(\n modalButton(\"Cancel\"),\n actionButton(\"minusColButton\", \"OK\")\n )\n ))\n })\n \n # Remove column to codebook after getting selection\n observeEvent(input$minusColButton, {\n req(values$codebook)\n minusColName = input$minusCol\n values$codebook <- values$codebook %>%\n select(-matches(paste0(minusColName)))\n \n removeModal()\n renderCodebook()\n })\n \n # Save and apply rewording of code\n observeEvent(input$counterTable_cell_edit, {\n # Update counter\n values$counter <-\n editData(values$counter,\n input$counterTable_cell_edit,\n rownames = FALSE,\n 'counterTable')\n \n ## Get previous code value\n # Recreate previous counter\n oldCounterTable <- values$codebook %>%\n count(Code, name = \"Instances\")\n \n # Retrieve old Code\n info = input$counterTable_cell_clicked\n changeRow = info$row\n changeCol = info$col\n oldValue = info$value\n \n # Retrieve new Code\n new_info = input$counterTable_cell_edit\n newValue = new_info$value\n \n # Replace strings in codebook\n values$codebook$Code[values$codebook$Code == oldValue] <-\n as.character(newValue)\n renderCodebook()\n })\n \n # Download handler for the codebook\n output$downloadData <- downloadHandler(\n filename = function() {\n paste(\"codebook-\", Sys.Date(), \".csv\", sep = \"\")\n },\n content = function(file) {\n write.csv(values$codebook, file, row.names = FALSE)\n }\n )\n \n ## EXTRACT VIEWER (REVIEW TAB)\n # Display all extracts related to a code\n findExtracts <- function() {\n # Get selected code\n whichRow <- input$codesTable_rows_selected\n allCodes <- values$counter$Code\n selectedCode <- allCodes[whichRow]\n \n # Filter for relevant extracts\n relevantExtracts <- values$codebook %>%\n dplyr::filter(grepl(paste(selectedCode,\n collapse = \"|\"),\n Code)) %>%\n select(Extract)\n \n # Organise the display to show all extracts.\n output$reviewTable <- renderDT({\n datatable(relevantExtracts,\n rownames = FALSE)\n })\n }\n \n showDocument <- function() {\n req(input$codesTable_rows_selected,\n input$reviewTable_rows_selected)\n # Get selected codes\n whichRow <- input$codesTable_rows_selected\n allCodes <- values$counter$Code\n selectedCode <- allCodes[whichRow]\n \n # Get selected extract\n whichExtractRow <- input$reviewTable_rows_selected\n whichExtractRow <-\n max(whichExtractRow) # in case multiple selected.\n \n # Filter for data\n documentData <- values$codebook %>%\n dplyr::filter(grepl(paste(selectedCode,\n collapse = \"|\"),\n Code)) %>%\n select(Document_ID, Extract)\n \n # Find which document\n allDocs <- documentData$Document_ID\n selectedDocument <- allDocs[whichExtractRow]\n documentText <-\n values$corpus[[values$documentTextColumn]][selectedDocument]\n \n # Find exact extract\n allExtracts <- documentData$Extract\n selectedExtract <- allExtracts[whichExtractRow]\n \n # Clean document text\n documentText <-\n gsub(pattern = \"\\n\", replacement = \"\", documentText) # Remove line breaks because they \"break\" the app...\n documentText <-\n gsub(pattern = \"\\\\s+\", replacement = \" \", documentText)\n \n # Find location of matches\n stringEnds <-\n as.data.frame(str_locate(documentText, paste0(\"\\\\Q\", selectedExtract, \"\\\\E\")))\n \n # Replace with highlight HTML\n stringBegin <- stringEnds$start\n stringFinish <- stringEnds$end\n str_sub(documentText, stringBegin, stringFinish) <-\n paste0(\"<span style=\\\"background-color: powderblue\\\">\",\n selectedExtract,\n \"<\/span>\")\n \n output$reviewDocumentText <- renderUI({\n tags$div(id = \"reviewDocumentText\",\n tags$p(HTML(documentText), id = \"currentDocumentText\", style = \"font-size: 20px\"))\n })\n \n }\n \n # If reviewing tab selected\n observeEvent(input$codesTable_rows_selected, {\n findExtracts()\n })\n \n observeEvent(input$reviewTable_rows_selected, {\n req(input$codesTable_rows_selected)\n showDocument()\n })\n \n ## SORTING CODES INTO THEMES\n ## Probably need a build UI function, and then a render function.\n ## Also need an output function.\n \n # Define reactive value for the codes organised into themes (bucket_list)\n themeData <- reactiveValues(themeList = list(),\n themeName = list())\n \n # Load codes in initially\n loadInCodes <- function() {\n req(values$counter)\n if (!is.null(values$counter$Code)) {\n allCodes <- values$counter$Code\n themeData$themeList$rank_list_1 <- allCodes\n themeData$themeList$rank_list_2 <- list()\n }\n }\n \n # Render after clicking on the sidebar menu the first time\n observeEvent(input$sbMenu, {\n if (input$sbMenu == \"themes\" && length(themeData$themeList) < 1) {\n loadInCodes()\n }\n })\n \n # Save reactive values for codes organised into themes\n saveThemeData <- function() {\n themeData$themeList <-\n input$codesToThemes # Saves in the current list\n }\n \n observeEvent(input$addTheme, {\n req(input$codesToThemes)\n saveThemeData()\n themeData$themeList[length(themeData$themeList) + 1] <-\n list(NULL)\n updateCodesUI()\n })\n \n observeEvent(input$sbMenu, {\n # Define rank list for bucket\n if (length(themeData$themeList) > 0) {\n buildRankList <-\n lapply(seq(length(themeData$themeList)), function(x) {\n if (x <= length(themeData$themeList)) {\n add_rank_list(\n text = \"\",\n labels = themeData$themeList[[x]],\n input_id = paste0(\"rank_list_\", x)\n )\n } else {\n add_rank_list(\n text = \"\",\n labels = list(),\n input_id = paste0(\"rank_list_\", x)\n )\n }\n })\n \n # Render theme UI\n renderCodesUI <- function() {\n output$uniqueCodes <- renderUI({\n do.call(\"bucket_list\", args = c(\n list(\n header = \"\",\n group_name = \"codesToThemes\",\n orientation = \"horizontal\"\n ),\n buildRankList\n ))\n })\n }\n \n renderCodesUI()\n saveThemeData()\n }\n })\n \n ## Function for rendering codes UI\n # Define rank list for bucket\n updateCodesUI <- function() {\n if (length(themeData$themeList) > 0) {\n buildRankList <-\n lapply(seq(length(themeData$themeList)), function(x) {\n if (x <= length(themeData$themeList)) {\n add_rank_list(\n text = \"\",\n labels = themeData$themeList[[x]],\n input_id = paste0(\"rank_list_\", x)\n )\n } else {\n add_rank_list(\n text = \"\",\n labels = list(),\n input_id = paste0(\"rank_list_\", x)\n )\n }\n })\n \n # Render theme UI\n renderCodesUI <- function() {\n output$uniqueCodes <- renderUI({\n do.call(\"bucket_list\", args = c(\n list(\n header = \"\",\n group_name = \"codesToThemes\",\n orientation = \"horizontal\"\n ),\n buildRankList\n ))\n })\n }\n \n renderCodesUI()\n }\n }\n \n output$downloadThemes <- downloadHandler(\n filename = function() {\n paste0(\"themes-\", Sys.Date(), \".csv\")\n },\n content = function(file) {\n themeData$themeList <- input$codesToThemes\n # Find max length of lists\n maxLength <- max(lengths(themeData$themeList))\n # Convert themes into dataframe\n themesOut <-\n do.call(rbind, lapply(themeData$themeList, `[`, seq_len(maxLength)))\n themesOut <- t(themesOut)\n # Send to download handler\n write.csv(themesOut, file)\n }\n )\n \n}\n\nshinyApp(ui, server)","type":"text"},{"name":"LICENSE","content":"MIT License\n\nCopyright (c) 2024 William Ngiam\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n","type":"text"},{"name":"README.md","content":"# _QualCA_ – Qualitative Coding App\n\n**_QualCA_** (pronounced _quokka_, like the small Australian marsupial) is a lightweight in-browser _R Shiny_ app for extracting and coding texts for qualitative analysis.\n\nThis repository contains the R Shiny code that is deployed at https://palm-lab.github.io/QualCA. You can run the ShinyApp locally within R / RStudio, making changes to the underlying code as needed. One advantage of running the app on your local device is that a temporary codebook file is automatically saved as a back-up for any unexpected crashes.\n\n# How to use the app\n\nOpen the app in R Studio by opening the code file (`app.R`). A **Run App** button should appear in the top right of the code editor.\n\n1. Organize your **corpus** as a CSV file, such that the each **document** is a different row and the **document text** is contained in one column.\n2. Upload your **corpus** CSV to **_QualCA_** using the button on the left-side menu. A menu will appear below to select the CSV column that contains the to-be-coded text, in case the corpus contains mutliple columns.\n3. If you are returning to the **_QualCA_** app, you can upload your previously saved codebook CSV using the button on the left-side menu to continue your analysis.\n4. You can scroll through the documents by pressing the 'previous' or 'next' buttons in the _Document Viewer_ pane of the app. You can also type in a numeric value into the _Document #_ bar to navigate to that document.\n5. To add an extract to the codebook, highlight the text in the document using your cursor and press the 'Add Selected Text as Extract' button in the _Codebook_ pane. When clicked, the highlighted text will appear in the _Extract_ column, and will be highlighted in blue in the _Document Viewer_. \n6. You can then add a Code or Theme to an extract by double clicking on the relevant cell within the codebook table, and typing the new Code.\n7. A _Counter_ pane in the top-right of the app shows how many extracts are associated with each Code. You can edit a Code by double clicking on it in the _Counter_, and typing in the new Code. This will change the Code for all extracts associated with the old Code.\n8. To save your progress, you can click the _Download Codebook_ button in the left-side menu. The Codebook is saved as a CSV file, which you can upload to **_QualCA_** on your next visit.\n\n# Acknowledgements\n\nThe **_QualCA_** app is [hosted on Github Pages at https://palm-lab.github.io/QualCA](https://palm-lab.github.io/QualCA) using the [shinylive](https://posit-dev.github.io/r-shinylive/) package, and makes use of this [helpful StackOverflow answer by user GGamba](https://stackoverflow.com/questions/42274461/can-shiny-recognise-text-selection-with-mouse-highlighted-text). \n\nThis app was created for an ongoing research project with Carly Stagg, Natasha van Antwerpen and Ella Moeck, who brought knowledge on how to conduct qualitative analysis, shared what would be desirable features, and tested earlier versions of the app.\n\n[Clinton Hadinata](https://github.com/hadinata) provided debugging help and useful advice on how to handle overlapping intervals.\n\nWilliam Ngiam created this app while employed as a Lecturer in the School of Psychology at the University of Adelaide.","type":"text"},{"name":"home_text.html","content":"<h2>Welcome to quokka, a qualitative methods coding app.<\/h2>\n<h3>Created by William XQ Ngiam, Lecturer in the School of Psychology at the University of Adelaide.<\/h3>\n<h4>quokka is in alpha (v0.1.5). If you would like to share feedback, please email me at <em>william.ngiam@adelaide.edu.au<\/em>.<\/h4>\n\n<h3>How to use this app:<\/h4><font size = \"3\"><ol>\n<h4>Loading data<\/h4>\n<li>Organize your <b>corpus<\/b> as a CSV file, such that the each <i>document<\/i> is a different row and the <i>document text<\/i> is contained in one column.<\/li>\n<li>Upload your <b>corpus<\/b> CSV to QualCA using the button on the left-side menu. A drop-down menu will appear below to select the CSV column that contains the to-be-coded text, in case the corpus contains mutliple columns.<\/li>\n<li>If you are returning to the QualCA app, you can upload your previously saved codebook CSV using the button on the left-side menu to continue your analysis.<\/li>\n<h4>Coding<\/h4>\n<li>Click the <b>\"Coding\"<\/b> tab on the sidebar to begin coding. You can scroll through the documents by pressing the 'previous' or 'next' buttons in the <i>Document Viewer<\/i> pane of the window. You can also type in a numeric value into the <i>Document # bar<\/i> to navigate to that document.<\/li>\n<li>To add an extract to the codebook, highlight the text in the document using your cursor and press the 'Add Selected Text as Extract' button in the <i>Codebook<\/i> pane. When clicked, the highlighted text will appear in the <i>Extract<\/i> column, and will be highlighted in blue in the <i>Document Viewer<\/i>.<\/li>\n<li>You can then add (or edit) a <i>Code<\/i> or <i>Theme<\/i> to an extract by double clicking on the relevant cell within the codebook table, and typing the new <i>Code<\/i>.<\/li>\n<li>A <i>Counter<\/i> pane in the top-right of the app shows how many extracts are associated with each <i>Code<\/i>. You can edit a <i>Code<\/i> by double clicking on it in the <i>Counter<\/i>, and typing in the new <i>Code<\/i>. This will change the <i>Code<\/i> for all extracts associated with the old <i>Code<\/i>.<\/li>\n<li>If you would like to apply an existing <i>Code<\/i> to a new extract, you may select the <i>Code<\/i> in the <i>Counter<\/i> before highlighting the to-be-extracted text. The Code will automatically be applied when you add the extract to the codebook.<\/li>\n<li>The <b>\"Research Question\"<\/b> box is an open-text box that you can use to keep any relevant text. It is useful to keep your research question as you code, perhaps refining the question as you continue your analysis.<\/li>\n<li>You can add a column to the codebook by pressing the <b>Add Column<\/b> button. This may be useful for keeping notes or other information alongside the extracts.<\/li>\n<li>To save your progress, you can click the Download Codebook button in the left-side menu. The codebook is saved as a CSV file, which you can upload to QualCA on your next visit.<\/li>\n<h4>Reviewing<\/h4>\n<li>If you would like to review your coding so far, you can open the left-side menu and click the <b>\"Reviewing\"<\/b> tab. On this tab, the <i>Counter<\/i> will be shown in the top-left, displaying the codes so far and the number of instances for each code. Click on a code in the <i>Counter<\/i> to display all extracts that are associated with that code.<\/li>\n<li>Click on an extract from the shown list to display the document containing that extract below in the Document Review box. Only the bottom-most extract will be displayed if more than one extract is selected from the <i>Extracts<\/i> list.<\/li>\n<h4>Sorting<\/h4>\n<li>Click on the <b>\"Sorting\"<\/b> tab on the sidebar to organise your codes into themes. All codes from your codebook will appear on the left-side.<\/li>\n<li>To sort the codes, simply drag the desired code into the empty \"bucket\". To add more \"buckets\" indicative of more themes (or sub-themes), press the \"Add theme\" button at the top.<\/li>\n<li>To save the current codes, press the \"Download Themes\" button. The output file is a CSV where each list will be saved as a separate column.<\/li>\n<\/font>","type":"text"},{"name":"temp_codebook.csv","content":"\"Theme\",\"Code\",\"Extract\",\"Document_ID\",\"Timestamp\"\n\"\",\"C\",\"and see if that helps because eating d\",64,\"2025-01-18 14:25:06.787977\"\n\"\",\"b\",\"ncy trumps intensity.\",65,\"2025-01-18 14:25:12.018813\"\n\"\",\"A\",\" us into a consumer-based mindset. I feel if you\",68,\"2025-01-18 14:25:15.883863\"\n","type":"text"}]