Saturday 30 December 2017

r - How to persist changes in a text file using shiny?

itemprop="text">


I have a small shiny app
for annotating text
files.




  1. The UI provides
    fileInput to select .txt files. One of
    the files is the default when the app is launched.

  2. Next,
    Previous buttons allow user to display the contents of the
    file, one sentence at a time.

  3. User may select any text
    within a sentence and click the Add Markup button to annotate
    the sentence. The Action Button triggers javascript function
    addMarkup().

  4. The sentence is
    displayed after being marked
    up.




I am only
posting the shiny app code here. Complete code of the app is available on href="https://github.com/beyond2013/dynamicContent" rel="nofollow noreferrer">github
repository




library(shiny)
ui <- fluidPage(

tags$head(tags$script(src="textselection.js")),
titlePanel("Corpus Annotation
Utility"),
sidebarLayout(
sidebarPanel(

fileInput('fileInput', 'Select Corpus', accept = c('text', 'text','.txt')),

actionButton("Previous", "Previous"),

actionButton("Next",
"Next"),
actionButton("mark", "Add Markup")
),

mainPanel(
tags$h1("Sentence: "),

htmlOutput("sentence"),
tags$h1("Sentence marked up: "),

htmlOutput("sentenceMarkedUp")
)

)

)
server <- function(input, output) {

sourceData <- reactive({
corpusFile <- input$fileInput

if(is.null(corpusFile)){
return(readCorpus('data/news.txt'))

}
readCorpus(corpusFile$datapath)

})


corpus <- reactive({sourceData()})

values <- reactiveValues(current = 1)
observeEvent(input$Next,{

if(values$current >=1 & values$current < length(corpus())){

values$current <- values$current + 1
}
})

observeEvent(input$Previous,{
if(values$current > 1 & values$current
<= length(corpus())){
values$current <- values$current -
1

}
})
output$sentence <-
renderText(corpus()[values$current])
}
shinyApp(ui = ui, server =
server)



readCorpus()
function looks like
this:



readCorpus <-
function(pathToFile){

con <- file(pathToFile)

sentences <- readLines(con, encoding = "UTF-8")
close(con)

return(sentences)
}


My
question is how can I persist the sentences to a file after they have been
annotated?



href="https://i.stack.imgur.com/2hfTF.png" rel="nofollow noreferrer"> src="https://i.stack.imgur.com/2hfTF.png" alt="screenshot of the
app">




Update:
I
have gone through href="https://shiny.rstudio.com/articles/persistent-data-storage.html" rel="nofollow
noreferrer">Persistent data storage in Shiny apps, and hope that I will be
able to follow along the documentation regarding persistent storage. However I am still
unsure how to capture the sentence after it has been marked up.



Answer




You have two issues here - persisting the
changes, and then saving the output. I solved the problem using a bit of JS and a bit of
R code. I'll do a pull request on Github to submit the broader code. However, here's the
core of it.



In your Javascript that you use to
select things, you can use Shiny.onInputChange() to update an
element of the input vector. Doing this, you can create a
reactiveValues item for the corpus, and then update it with
inputs from your interface.



Below, you'll notice
that I switched from using a textnode to using just the inner HTML. Using a node, and
firstChild, as you had it before, you end up truncating the
sentence after the first annotation (since it only picks the stuff before
. Doing it this way seems to work
better.




window.onload =
function(){
document.getElementById('mark').addEventListener('click',
addMarkup);
}

function addMarkup(){
var
sentence = document.getElementById("sentence").innerHTML,

selection="";
if(window.getSelection){
selection =
window.getSelection().toString();
}

else
if(document.selection && document.selection.type != "Control"){

selection = document.selection.createRange().text;
}

if(selection.length === 0){
return;
}
marked =
"".concat(selection).concat("");
result =
sentence.replace(selection, marked);

document.getElementById("sentence").innerHTML = result;

Shiny.onInputChange("textresult",result);

}


Next,
I've tried to simplify your server.R code. You were using a
reactive context to pull from another reactive context
(sourceData into corpus), which seemed
unnecessary. So, I tried to refactor it a
bit.



library(shiny)
source("MyUtils.R")
ui
<- fluidPage(
tags$head(tags$script(src="textselection.js")),

titlePanel("Corpus Annotation Utility"),

sidebarLayout(

sidebarPanel(
fileInput('fileInput', 'Select Corpus', accept = c('text',
'text','.txt')),
actionButton("Previous", "Previous"),

actionButton("Next", "Next"),
actionButton("mark", "Add Markup"),

downloadButton(outputId = "save",label = "Download")),
mainPanel(

tags$h1("Sentence: "),
htmlOutput("sentence"))


)
)

server <- function(input, output) {

corpus <- reactive({
corpusFile <- input$fileInput

if(is.null(corpusFile)) {
return(readCorpus('data/news.txt'))
}
else {
return(readCorpus(corpusFile$datapath))


}
})

values <- reactiveValues(current =
1)
observe({
values$corpus <- corpus()
})

output$sentence <- renderText(values$corpus[values$current])


observeEvent(input$Next,{

if(values$current >=1 &
values$current < length(corpus())) {
values$current <- values$current +
1
}
})
observeEvent(input$Previous,{

if(values$current > 1 & values$current <= length(corpus())) {

values$current <- values$current - 1
}
})

observeEvent(input$mark,{

values$corpus[values$current] <-
input$textresult
})
output$save <- downloadHandler(filename =
"marked_corpus.txt",
content = function(file) {


writeLines(text = values$corpus,
con = file,
sep =
"\n")

})
}



Now,
the code has a few changes. The loading from file is basically the same. I was right
about my skepticism on isolate - replacing it with an
observe accomplishes what I wanted to do, whereas
isolate would only give you the initial load. Anyway, we use
observe to load the corpus values into the
reactiveValues object you created - this is to give us a place
to propagate changes to the data.



We keep the
remaining logic for moving forward and backward. However, we change the way the output
is rendered so that it looks at the reactiveValues object.
Then, we create an observer that updates the reactiveValues
object with the input from our updated Javascript. When this happens, the data gets
stored permanently, and you can also mark more than one sequence in the string (though I
have not done anything with nested marking or with removing marks). Finally, a save
function is added - the resulting strings are saved out with
used to show the marked
areas.



If you load a previously marked file, the
marks will show up again.


No comments:

Post a Comment

php - file_get_contents shows unexpected output while reading a file

I want to output an inline jpg image as a base64 encoded string, however when I do this : $contents = file_get_contents($filename); print &q...