diff --git a/DESCRIPTION b/DESCRIPTION index 7e53371..5d161fa 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,9 +1,9 @@ Package: LDAvis Title: Interactive Visualization of Topic Models -Version: 0.3.4 +Version: 0.3.5 Authors@R: c(person("Carson", "Sievert", role = c("aut", "cre"), email = "cpsievert1@gmail.com", comment = c(ORCID = "0000-0002-4958-2844")), person("Kenny", "Shirley", role = "aut", - email = "kshirley@research.att.com")) + email = "kshirley@research.att.com"), person("Christopher", "Baker", role = "ctb", email = "chriscrewbaker@gmail.com")) Description: Tools to create an interactive web-based visualization of a topic model that has been fit to a corpus of text data using Latent Dirichlet Allocation (LDA). Given the estimated parameters of diff --git a/NEWS b/NEWS index 04a1e4e..a3d3152 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,7 @@ + CHANGES IN LDAvis VERSION 0.3.5 + +Click events (on topics and terms) are now made available to the Shiny input object (closes [[#45]](https://github.com/cpsievert/LDAvis/issues/45)). When a topic is clicked on, the topic number is made available to Shiny server at input$OUTPUTID_topic_click, where OUTPUTID is the outputId of the LDAvis plot. When a term is clicked on, the term is available at input$OUTPUTID_term_click. See https://github.com/cpsievert/LDAvis/tree/master/inst/examples/shiny for an example Shiny app. + CHANGES IN LDAvis VERSION 0.3.4 A new argument, encoding, was added to serVis(). This sets the encoding which will be used to write the JSON file. This may be set to `UTF-8` to fix an issue where, when running on windows, the JSON output would be encoded in ANSI despite the R data beeing encoded in UTF-8. This should also fix #50. diff --git a/inst/examples/shiny/server.R b/inst/examples/shiny/server.R index d3e91fb..23f298b 100644 --- a/inst/examples/shiny/server.R +++ b/inst/examples/shiny/server.R @@ -5,4 +5,14 @@ shinyServer(function(input, output, session) { with(TwentyNewsgroups, createJSON(phi, theta, doc.length, vocab, term.frequency, R = input$nTerms))}) -}) \ No newline at end of file + + output$termClicked <- renderPrint({ + if (is.null(input$myChart_term_click)) return() + paste("You clicked on term:", input$myChart_term_click) + }) + output$topicClicked <- renderPrint({ + if (is.null(input$myChart_topic_click)) return() + paste("You clicked on topic:", input$myChart_topic_click) + }) + +}) diff --git a/inst/examples/shiny/ui.R b/inst/examples/shiny/ui.R index d83d68d..032096d 100644 --- a/inst/examples/shiny/ui.R +++ b/inst/examples/shiny/ui.R @@ -2,6 +2,8 @@ library(LDAvis) shinyUI( fluidPage( sliderInput("nTerms", "Number of terms to display", min = 20, max = 40, value = 30), + textOutput("termClicked"), + textOutput("topicClicked"), visOutput('myChart') ) ) \ No newline at end of file diff --git a/inst/htmljs/ldavis.js b/inst/htmljs/ldavis.js index 81a6ac0..1d703a9 100644 --- a/inst/htmljs/ldavis.js +++ b/inst/htmljs/ldavis.js @@ -16,7 +16,9 @@ LDAvis = function(to_select, json_file) { vis_state = { lambda: 1, topic: 0, - term: "" + term: "", + topic_clicked: 0, + term_clicked: "" }; // Set up a few 'global' variables to hold the data: @@ -89,7 +91,16 @@ LDAvis = function(to_select, json_file) { return 0; }; } - + + // see if vis is being run as part of a shiny app + // (code adapted from https://github.com/ramnathv/htmlwidgets/blob/master/inst/www/htmlwidgets.js#L15) + var inShinyMode = typeof(window.Shiny) !== "undefined" && !!window.Shiny.outputBindings; + + if (inShinyMode) { + var outputId = to_select.substring(1, to_select.length); + var shinyClickedTopic = outputId + "_topic_click"; + var shinyClickedTerm = outputId + "_term_click"; + } // The actual read-in of the data and main code: d3.json(json_file, function(error, data) { @@ -167,9 +178,11 @@ LDAvis = function(to_select, json_file) { // increment the value in the input box document.getElementById(topicID).value = value_new; topic_off(document.getElementById(topicID + value_old)); - topic_on(document.getElementById(topicID + value_new)); + var oldtopic = document.getElementById(topicID + value_new); + topic_on(oldtopic); vis_state.topic = value_new; state_save(true); + topic_click(oldtopic, value_new); }) d3.select("#" + topicDown) @@ -183,9 +196,11 @@ LDAvis = function(to_select, json_file) { // increment the value in the input box document.getElementById(topicID).value = value_new; topic_off(document.getElementById(topicID + value_old)); - topic_on(document.getElementById(topicID + value_new)); + var oldtopic = document.getElementById(topicID + value_new); + topic_on(oldtopic); vis_state.topic = value_new; state_save(true); + topic_click(oldtopic, value_new); }) d3.select("#" + topicID) @@ -198,10 +213,12 @@ LDAvis = function(to_select, json_file) { var value_new = document.getElementById(topicID).value; if (!isNaN(value_new) && value_new > 0) { value_new = Math.min(K, Math.max(1, value_new)) - topic_on(document.getElementById(topicID + value_new)); + var oldtopic = document.getElementById(topicID + value_new); + topic_on(oldtopic); vis_state.topic = value_new; state_save(true); document.getElementById(topicID).value = vis_state.topic; + topic_click(oldtopic, value_new); } }) @@ -415,6 +432,7 @@ LDAvis = function(to_select, json_file) { document.getElementById(topicID).value = vis_state.topic = d.topics; state_save(true); topic_on(this); + topic_click(this, d.topics); }) .on("mouseout", function(d) { if (vis_state.topic != d.topics) topic_off(this); @@ -549,6 +567,9 @@ LDAvis = function(to_select, json_file) { // term_on(this); // debugger; // }) + .on("click", function(d) { + term_click(this, d.Term) + }) .on("mouseout", function() { vis_state.term = ""; term_off(this); @@ -992,6 +1013,17 @@ LDAvis = function(to_select, json_file) { function topic_on(circle) { if (circle == null) return null; + // whenever the topic changes we have to remove the underline style + // from any clicked term + var old_term_clicked_id = termID + vis_state.term_clicked; + var topic_clicked_id = topicID + vis_state.topic_clicked; + if (vis_state.term_clicked != "" && circle.id != topic_clicked_id) { + var oldterm = document.getElementById(old_term_clicked_id); + if (oldterm != null) { + oldterm.style.textDecoration = null; + } + } + // grab data bound to this element var d = circle.__data__ var Freq = Math.round(d.Freq * 10) / 10, @@ -1343,8 +1375,24 @@ LDAvis = function(to_select, json_file) { } function state_reset() { + + // set the style of any clicked term back to be non-underline + var old_term_clicked_id = termID + vis_state.term_clicked; + if (vis_state.term_clicked != "") { + var oldterm = document.getElementById(old_term_clicked_id); + if (oldterm != null) { + oldterm.style.textDecoration = null; + } + } + if (vis_state.topic > 0) { topic_off(document.getElementById(topicID + vis_state.topic)); + // set the style of any topic clicked to be back to regular style + // (no thick border around topic circle) + var old_topic_clicked_id = topicID + vis_state.topic_clicked; + if (vis_state.topic_clicked > 0) { + document.getElementById(old_topic_clicked_id).style.strokeWidth = null; + } } if (vis_state.term != "") { term_off(document.getElementById(termID + vis_state.term)); @@ -1352,6 +1400,72 @@ LDAvis = function(to_select, json_file) { vis_state.term = ""; document.getElementById(topicID).value = vis_state.topic = 0; state_save(true); + + // make sure term ids are all correct + d3.selectAll(".terms").attr("id", function(d) { + return (termID + d.Term) + }); + + // update shiny inputs to be null + if (inShinyMode) { + Shiny.onInputChange(shinyClickedTopic, null); + Shiny.onInputChange(shinyClickedTerm, null); + } + + // set state of topic_clicked to 0, so we can click on topic x, reset + // vis, then click on topic x again without any problems + vis_state.topic_clicked = 0; + } + + function topic_click(newtopic, newtopic_num) { + if (!inShinyMode) { + return null; + } + // set style of clicked topic to have thicker border + newtopic.style.strokeWidth = 2; + + // set style of old selected topic back to regular border + var old_topic_clicked_id = topicID + vis_state.topic_clicked; + if (vis_state.topic_clicked > 0 && old_topic_clicked_id != this.id) { + document.getElementById(old_topic_clicked_id).style.strokeWidth = null; + } + + // save state of topic clicked + vis_state.topic_clicked = newtopic_num; + + // update shiny topic input object to be new topic clicked + Shiny.onInputChange(shinyClickedTopic, newtopic_num); + + // since topic changed, we want to reset the input term object back to null + Shiny.onInputChange(shinyClickedTerm, null); + } + + function term_click(newterm, newterm_term) { + if (!inShinyMode) { + return null; + } + // make sure term ids are up to date + d3.selectAll(".terms").attr("id", function(d) { + return (termID + d.Term) + }); + + // underline clicked term + newterm.style.textDecoration = "underline"; + + // set style of old clicked term back to non-underline + var old_term_clicked_id = termID + vis_state.term_clicked; + if (old_term_clicked_id != newterm.id) { + var oldterm = document.getElementById(old_term_clicked_id); + if (oldterm != null) { + oldterm.style.textDecoration = null; + } + } + + // save state of term clicked + vis_state.term_clicked = newterm_term; + + // update shiny term input object to know about new term clicked + Shiny.onInputChange(shinyClickedTerm, newterm_term); } });