I'm essentially trying to replicate the behavior of the graph on this site in a Shiny app.
That is, I want to create an interactive graph, where by hovering the mouse cursor over the graph, you move a "targeting line" along the x-axis. Then, according to the position of the targeting line, the y-values of the plot lines on the graph are displayed on the intersection point of the targeting line and the plot lines. (I was going to post an illustrative figure, but it appears I don't have enough reputation for that yet.)
I've managed to get the application to work. In my current implementation I'm using the hover option in plotOutput to get the location of the cursor on the plot, and then adding a targeting line using abline to a new plot. Along with points and text to add the y-values on the plot.
The issue I'm having is that the targeting line starts to severely lag behind the actual mouse cursor after moving around for a while. I think this is due to having to redraw the entire plot every time the mouse hovering position updates (currently every 500 ms when the cursor is moving, since I'm using hoverOpts(delayType = "throttle")). The rendering just isn't fast enough to keep up with the mouse movement. I was wondering if anybody has an idea on how to get around this problem.
Runnable code for an example of the Shiny app:
library(shiny)
trigWaves <- function(A = 1, ...) {
xval <- seq(0, 2*pi, len = 201)
sinx <- A * sin(xval); cosx <- A * cos(xval)
plot(x = xval, y = sinx, type = 'n', ylab = "f(x)", xlab = "x", ...)
abline(h = A * c(-1, 0, 1), lty = c(2, 1, 2), col = 'gray')
abline(v = pi * seq(0, 2, by = 0.5), lty = 2, col = 'gray')
lines(x = xval, y = sinx, col = 'red')
lines(x = xval, y = cosx, col = 'blue')
box()
invisible(list(x = xval, y = list(sin = sinx, cos = cosx)))
}
# Maximum selectable amplitude
Amax <- 5
runApp(
# Define UI for application
list(ui = pageWithSidebar(
# Application title
headerPanel("Read Function Values Interactively from a Plot"),
sidebarPanel(
sliderInput("amplitude",
"Amplitude:",
min = 1,
max = Amax,
value = 2,
step = 0.1)
),
mainPanel(
plotOutput("trigGraph",
hover =
hoverOpts(
id = "plothover",
delay = 500,
delayType = "throttle"
)
)
)
),
# Define server for application
server = function(input, output, session) {
A <- reactive(input$amplitude)
hoverx <- reactiveValues(initial = 2)
# Hover position
tx <- reactive({
# If no previous hover position found, return initial = 0
if (is.null(hoverx$prev)) return(hoverx$initial)
# Hover resets to NULL every time the plot is redrawn -
# If hover is null, then use the previously saved hover value.
if (is.null(input$plothover)) hoverx$prev else input$plothover$x
})
# Function to plot the 'reader line' and the function values
readLine <- reactive({
abline(v = tx(), col = 'gray'); box()
# Plot coordinates for values and points
pcoords <- list(x = rep(tx(), 2), y = A() * c(sin(tx()), cos(tx())))
points(pcoords, pch = 16, col = c("red", "blue")) # points on lines
text(pcoords, labels = round(pcoords$y, 2), pos = 4) # function values
})
# Render the final output graph
output$trigGraph <- renderPlot({
# Create base plot
trigWaves(A = A(), ylim = Amax * c(-1, 1))
readLine() # Add the reader line and function values
# Add a legend
legend(x = 3.5, y = 0.9 * Amax,
legend = c("sin(x)", "cos(x)"),
col = c("red", "blue"), lty = 1)
# Save the hover position used as the previous position
hoverx$prev <- tx()
})
}), display.mode= "showcase"
)
Six years later, JavaScript is still the way to go for a graph like this.
Here’s an overview of a couple of different R packages to achieve that,
including dygraphs and highcharts originally mentioned in the comments.
# Goal is to make an interactive crosshair plot with data from this.
trigWaves <- function(x, A = 1, ...) {
rbind(
data.frame(x, y = A * sin(x), f = "sin"),
data.frame(x, y = A * cos(x), f = "cos")
)
}
xs <- seq(0, 2 * pi, len = 201)
Amax <- 5 # Maximum amplitude -- determines plot range, too.
Plotting methods
dygraphs
library(dygraphs)
plot_dygraphs = function(data) {
# Unlike other packages, dygraphs wants wide data
wide <- data %>%
tidyr::pivot_wider(
names_from = f,
values_from = y
)
dygraph(wide) %>%
dyCrosshair("vertical") %>%
dyAxis("y", valueRange = c(-1, 1) * Amax)
}
highcharter
library(highcharter)
plot_highcharter = function(data) {
hchart(data, "line", hcaes(x, y, group = f)) %>%
hc_xAxis(crosshair = TRUE) %>%
hc_yAxis(min = -Amax, max = Amax)
}
plotly
library(plotly)
plot_plotly = function(data) {
plot_ly(data) %>%
add_lines(~ x, ~ y, color = ~ f) %>%
layout(
hovermode = "x",
spikedistance = -1,
xaxis = list(
showspikes = TRUE,
spikemode = "across"
),
yaxis = list(range = c(-1, 1) * Amax)
)
}
c3
library(c3)
plot_c3 = function(data) {
c3(data, "x", "y", group = "f") %>%
c3_line("line") %>%
yAxis(min = -Amax, max = Amax) %>%
point_options(show = FALSE)
}
Shiny app
All of the packages also integrate with Shiny. Here’s a demo app showcasing them:
library(shiny)
ui <- fluidPage(
sliderInput("amplitude", "Amplitude:", 0.1, Amax, 1, step = 0.1),
fluidRow(
column(6,
tags$h3("dygraphs"),
dygraphOutput("dygraphs"),
),
column(6,
tags$h3("highcharter"),
highchartOutput("highcharter"),
),
column(6,
tags$h3("plotly"),
plotlyOutput("plotly"),
),
column(6,
tags$h3("c3"),
c3Output("c3", height = "400px"), # All others have 400px default height
)
)
)
server <- function(input, output, session) {
waves <- reactive(trigWaves(xs, input$amplitude))
output$dygraphs <- renderDygraph({ plot_dygraphs(waves()) })
output$highcharter <- renderHighchart({ plot_highcharter(waves()) })
output$plotly <- renderPlotly({ plot_plotly(waves()) })
output$c3 <- renderC3({ plot_c3(waves()) })
}
shinyApp(ui, server)
See it live here: https://mikkmart.shinyapps.io/crosshair/
Related
I want to calculate the distance of the segment between 2 clicked points, i already have a function with that launches a shiny dashboard that allows you to save the clicks and draw a line between the pairs. It is printing the dimension of the image in pixels. Any image can be used changing the image_path.
I want to know if there is a way to select the segments of each pair and calculate the distance between them in pixels and later convert it to cm.
library(shiny)
library(shinydashboard)
library(dplyr)
library(imager)
library(reactable)
click_length <- function(image_path = system.file("example_images", package = "ClickMetrics")){
app <- shinyApp(
ui <- dashboardPage(
skin = 'purple',
dashboardHeader(title = "ClickMetrics"),
dashboardSidebar(disable = TRUE),
dashboardBody(
fluidRow(
box(plotOutput("IMG",
height = 400,
click = "click_plot")),
box(
(selectInput("IMAGE",
"Images:",
list.files(path = image_path,
pattern = ".jpg",
full.names = TRUE,
include.dirs = FALSE)))
),
actionButton("clear","Clear Points"),
reactableOutput("INFO")
)
)
),
server <- function(input, output, session){
# Creating a reactive value that receives image input
img <- reactive({
f <- input$IMAGE
imager::load.image(f)
})
# Store reactive values for coordinates
CLICKS <- reactiveValues(
x = NULL,
y = NULL,
n = NULL,
pair = NULL
)
ns <- session$ns
observeEvent(eventExpr = input$click_plot$x, handlerExpr = { ## Adds the info about clicks
CLICKS$x <- append(CLICKS$x, input$click_plot$x)
CLICKS$y <- append(CLICKS$y, input$click_plot$y)
CLICKS$n <- append(CLICKS$n, length(CLICKS$x))
CLICKS$pair <-
append(CLICKS$pair,
as.integer(ceiling(length(CLICKS$x)/2)))
df <- data.frame(CLICKS$x, CLICKS$y, CLICKS$pair)
df <- split(df, CLICKS$pair)
print(dim(img())) # prints dimensions of the image
})
output$IMG <- renderPlot({
expr = {
img <- img()
par(mar = c(0.5, 0.5, 1.75, 0.5))
plot(img, axes = FALSE)
box(col = 'gray')
mtext(text = input$IMAGE,
side = 3,
line = 0.5,
adj = 0.5,
cex = 1.23)
if (!is.null(CLICKS$x) && length(CLICKS$x) > 0) {
points(x = CLICKS$x,
y = CLICKS$y,
pch = 19,
cex = 0.75,
col = "red")
text(x = CLICKS$x,
y = CLICKS$y,
label = CLICKS$n,
pos = 3)
n_par <- 2 * floor(length(CLICKS$x)/2)
tb_pairs <- cbind(
matrix(CLICKS$x[1:n_par], ncol = 2, byrow = TRUE),
matrix(CLICKS$y[1:n_par], ncol = 2, byrow = TRUE))
segments(x0 = tb_pairs[, 1],
x1 = tb_pairs[, 2],
y0 = tb_pairs[, 3],
y1 = tb_pairs[, 4],
col = "black")
}
}
})
output$INFO <- renderReactable({
df1 <- data.frame(round(CLICKS$x,2), round(CLICKS$y,2), CLICKS$pair)
reactable(df1)
})
observe({ # clear clicked points
if(input$clear>0){
session$reload()
}
})
})
runApp(app)
}
click_length()
I tried some examples using locator, but it does not work inside a shiny dashboard, which i need.
I'm preparing a shiny package which should help me to illustrate a very simple theoretical model in video calls. Basically every graphic consists of 2 straight lines, which can be moved parallel Therefore, I have created the graphic in plotly and it updates itself when I move the respective shape at the corresponding curve. If I want to start the graphic with new data via draw all functions update correctly but the shape points remain constant because I define them as global variables by <<-. Therefore, the data.frame in point does not change. Now I'm looking for an approach how I can
move both lines
changing one shape does not affect the other.
Redraw a fresh plot with different parameter
Full code:
library(shiny)
library(plotly)
library(tidyverse)
library(shinydashboard)
header <- dashboardHeader(
title = "Shiny_economics"
)
body <- dashboardBody(
fluidRow(
column(width = 9,
box(width = NULL, solidHeader = TRUE,
plotlyOutput("p",height="92vh")
)
),
column(width = 3,
box(width = NULL, status = "warning",
h3("Demand"),
splitLayout(
numericInput("intercept_d","Intercept",10),
numericInput("slope_d","Slope",-0.5)),
h3("Supply"),
splitLayout(
numericInput("intercept_s","Intercept",5),
numericInput("slope_s","Slope",0.5)),
sliderInput("range", h3("x limit"),
min = 20, max = 10000, value = 20, step = 10),
actionButton("draw", "Draw")
)
)
)
)
ui<-dashboardPage(
header,
dashboardSidebar(disable = TRUE),
body
)
server <- function(input, output, session) {
#functions to generate data
define_parameter<-function(intercept,slope){
return(list(intercept=intercept,
slope=slope))
}
gleichwicht_x<-function(list1,list2){
gg_x=(list1$intercept-list2$intercept)/(list2$slope-list1$slope)
}
gleichgewicht_p<-function(gg_x,parameter_data){
gg_p=parameter_data$intercept+parameter_data$slope*gg_x
return(gg_p)
}
price_function<-function(parameter,x){
intercept=parameter$intercept
slope=parameter$slope
price=intercept+slope*x
return(price)
}
function_data<-function(parameter_list,quantity,name){
return(tibble(quantity=quantity,
!!name:=price_function(parameter_list,quantity)))
}
observeEvent(input$draw,{
#get input paramter
demand_intercept<-input$intercept_d
demand_slope<-input$slope_d
supply_intercept<-input$intercept_s
supply_slope<-input$slope_s
range<-input$range
#generate data to plot with functions and parameters
supply_start<-function_data(define_parameter(supply_intercept,supply_slope),c(0:range),"supply")
demand_start<-function_data(define_parameter(demand_intercept,demand_slope),c(0:range),"demand")
supply<-function_data(define_parameter(supply_intercept,supply_slope),c(0:range),"supply")
demand<-function_data(define_parameter(demand_intercept,demand_slope),c(0:range),"demand")
output$p <- renderPlotly({
d <- event_data("plotly_relayout", source = "trajectory")
#if first shape is moved recalculate data with new parameter
move_demand <- if (!is.null(d[["shapes[0].yanchor"]])) {
y_demand <<- round(d[["shapes[0].yanchor"]],0)
demand<<-function_data(define_parameter(y_demand,demand_slope),c(0:range),"demand")
} else {
if(!exists("y_demand")){
y_demand<<-demand_intercept
demand<<-demand_start
}
}
#if second shape is moved recalculate data with new parameter
move_supply <- if (!is.null(d[["shapes[1].yanchor"]])) {
y_supply <<- round(d[["shapes[1].yanchor"]],0)
supply<<-function_data(define_parameter(y_supply,supply_slope),c(0:range),"supply")
} else {
if(!exists("y_supply")){
y_supply<<-supply_intercept
supply<<-supply_start
}
}
#create data for shapes
#this does not update when cklicking the draw button and uses the "old" global variables
points<-data.frame(x=c(0,0),y=c(y_demand,y_supply))
intercepts<-map2(points$x,points$y,
~list(
type = "circle",
xanchor = .x,
yanchor = .y,
x0 = -4, x1 = 4,
y0 = -4, y1 = 4,
xsizemode = "pixel",
ysizemode = "pixel",
fillcolor = "blue",
line = list(color = "transparent")
)
)
#plot everything and update plot if something is moved in plotly
plot_ly( source = "trajectory") %>%
add_trace(x = demand_start$quantity, y = demand_start$demand, name = 'Demand_old', mode = 'lines', line=list(color='#9696a3', dash="dash"), type = "scatter") %>%
add_trace(x = supply_start$quantity, y = supply_start$supply, name = 'Supply_old', mode = 'lines', line=list(color='#9696a3', dash="dash"), type = 'scatter') %>%
add_trace(x = demand$quantity, y = demand$demand, name = 'Demand', mode = 'lines', type = "scatter") %>%
add_trace(x = supply$quantity, y = supply$supply, name = 'Supply', mode = 'lines', type = "scatter") %>%
layout(shapes = intercepts) %>%
config(editable = list(shapePosition = TRUE))
})
}
)
}
shinyApp(ui, server)
I am fairly new with rshiny apps and I am trying to build an app which works fine but when I try to add a plot with brush and delete points functionality I run into errors.
Essentially, I am reading a csv file into a reactive function df_products_upload() that I use in some other functions (for plotting and to populate columns for user inputs) but when I call this function inside reacticeValues() to pass the data frame it fails me. I am trying to add brush and delete functionality to a plot (Plot1). I cant re-read the csv file just to input into this reactiveValues so it would work. The whole idea of reactive function dies if I have to keep reading csv file for every other function used in the app redundantly.
vals <- reactiveValues(
df1 <- df_products_upload(),
data=df1
)
############## plotting -1
output$plot1 <- renderPlot({
ggplot(vals$data, aes_string(x = input$column1, y = input$column2)) + geom_point()
})
observe({
df = brushedPoints(vals$data, brush = input$plot1_brush, allRows = TRUE)
vals$data = df[df$selected_== FALSE, ] ## Taking only those data points where the selected_ value is FALSE (alternatively ignoring rows with selected_ = TRUE status)
})
Can someone please suggest how do I go about it? what is the right logic to use reactive and reactiveValues inside each other if its possible at all. if not possible how do I get this plot to work with rest of the code. Clearly shiny doesn't like calling a reactive function inside reactiveValues(). I am having this trouble with the Plot1, code is at the bottom. you can use any csv to test the code, it will only complain about plot -3 where I have hard coded the column names, just change those while testing.
here is complete code:
library(DT)
library(shinydashboard)
library(ggplot2)
library(shinyFiles)
ui <- fluidPage(
# File upload button
shinyFilesButton(id = 'file', label= 'Choose file to upload',
title = 'Select file', multiple = FALSE),
#Shows data table on the main page
fluidRow(
column(12, DT::dataTableOutput('tabl'))
# dataTableOutput("tabl")
),
# h5('Select two Columns to Plot'),
uiOutput("Col1"),
uiOutput("Col2"),
#-----------------------------------------------------------
#Shows Plot button
fluidRow(
column(6, plotOutput('plot2', height = 500)),
column(6, plotOutput('plot3', height = 500))
),
fluidRow(
column(7, class = "row",
h4("Brush and click to exclude Point"),
plotOutput("plot1", height = 500,
# click = "plot1_click",
brush = brushOpts(
id = "plot1_brush"
# resetOnNew = TRUE
)
)
)
)
)
#------------------------------------------------------------------------
server <- function(input, output, session) {
###Read cvs file and convert julian Date to regular Date format
shinyFileChoose(input, 'file', roots= c(wd="/Users/mnoon/Desktop/projects/2018/rShinyApp_imageData"), filetypes= c('', 'csv'))
df_products_upload <- reactive({
inFile <- parseFilePaths(roots=c(wd='/Users/mnoon/Desktop/projects/2018/rShinyApp_imageData/'), input$file)
if (NROW(inFile)){
# return(NULL)
df <- read.csv(as.character(inFile$datapath), header = TRUE, sep = ",", stringsAsFactors = F)
# Convert Julian to Calendar date
df$Julian.Date <- as.Date((as.numeric(df$Julian.Date) - 2400000.5), origin=as.Date("1858-11-17"))
#Change Column name to 'Date'
names(df)[names(df) == 'Julian.Date'] <- 'Date'
df <- as.data.frame(df)
return(df)
}
})
###Previews data table on the main display window
output$tabl<- DT::renderDataTable({
df <- df_products_upload()
DT::datatable(df)
}, server = FALSE)
###The following set of functions populate the column selectors
output$Col1 <- renderUI({
df <-df_products_upload()
if (is.null(df)) return(NULL)
cols=names(df)
names(cols)=cols
selectInput("column1", "Select Column for X-axis", cols)
})
output$Col2 <- renderUI({
df <-df_products_upload()
if (is.null(df)) return(NULL)
cols=names(df)
names(cols)=cols
selectInput("column2", "Select Column for Y-axis", cols)
})
# -------------------------------------------------------------------
###plot2
# # # A scatterplot with certain points highlighted
# #
output$plot2 = renderPlot({
df2 <- df_products_upload()
df <- df2[,c(input$column1, input$column2)]
s1 = input$tabl_rows_current # rows on the current page
s2 = input$tabl_rows_all # rows on all pages (after being filtered)
req(input$column1)
##get xlim values for plot
xdiff <- (as.numeric(max(df[,1])) - as.numeric(min(df[,1])))
xd1 <- (as.numeric(max(df[,1]))) + 0.7*(xdiff)
xd2 <- (as.numeric(min(df[,1]))) - 0.7*(xdiff)
##get ylim values for plot
ydiff <- (ceiling(as.numeric(max(df[,2]))) - floor(as.numeric(min(df[,2]))))
yd1 <- (ceiling(as.numeric(max(df[,2])))) + 0.7*(ydiff)
yd2 <- (floor(as.numeric(min(df[,2])))) - 0.7*(ydiff)
######################## --- Plotting -2
par(mar = c(4, 4, 1, .1))
plot(df, pch = 21, xlim = c(xd2,xd1), ylim = c(yd2,yd1), xlab = input$column1, ylab = input$column2)
grid()
# solid dots (pch = 19) for current page
if (length(s1)) {
points(df[s1, , drop = FALSE], pch = 19, cex = 1.5)
}
# show red circles when performing searching
if (length(s2) > 0 && length(s2) < nrow(df)) {
points(df[s2, , drop = FALSE], pch = 21, cex = 2, col = 'red')
}
# dynamically change the legend text
s = input$tabl_search
txt = if (is.null(s) || s == '') 'Filtered data' else {
sprintf('Data matching "%s"', s)
}
legend(
'topright', c('Original data', 'Data on current page', txt),
pch = c(21, 19, 21), pt.cex = c(1, 1.5, 2), col = c(1, 1, 2),
y.intersp = 2, bty = 'n'
)
})
# -------------------------------------------------------------------
###plot3
########[Always plot these two columns - 'Right.Ascension..deg.', 'Declination..deg.']
output$plot3 = renderPlot({
df2 <- df_products_upload()
## Columns hard-coded (always plot these)
df3 = df2[, c('Right.Ascension..deg.', 'Declination..deg.' )]
s1 = input$tabl_rows_current # rows on the current page
s2 = input$tabl_rows_all # rows on all pages (after being filtered)
##get xlim values for plot
xdiff <- (as.numeric(max(df3[,"Right.Ascension..deg."])) - as.numeric(min(df3[,"Right.Ascension..deg."])))
xd1 <- (as.numeric(max(df3[,"Right.Ascension..deg."]))) + 0.2*(xdiff)
xd2 <- (as.numeric(min(df3[,"Right.Ascension..deg."]))) - 0.2*(xdiff)
##get ylim values for plot
yd1 <- (as.numeric(max(df3[,"Declination..deg."]))) - 0.1
yd2 <- (ceiling((as.numeric(min(df3[,"Declination..deg."])))))
########################## --- Plotting -3
par(mar = c(4, 4, 1, .1))
plot(df3, pch = 21, xlim = c(xd2,xd1), ylim = c(yd2,yd1), xlab = names(df3[1]), ylab = names(df3[2]))
# axis(1, )
grid()
# solid dots (pch = 19) for current page
if (length(s1)) {
points(df3[s1, , drop = FALSE], pch = 19, cex = 1.5)
}
# show red circles when performing searching
if (length(s2) > 0 && length(s2) < nrow(df3)) {
points(df3[s2, , drop = FALSE], pch = 21, cex = 2, col = 'red')
}
# dynamically change the legend text
s = input$tabl_search
txt = if (is.null(s) || s == '') 'Filtered data' else {
sprintf('Data matching "%s"', s)
}
legend(
'topright', c('Original data', 'Data on current page', txt),
pch = c(21, 19, 21), pt.cex = c(1, 1.5, 2), col = c(1, 1, 2),
y.intersp = 2, bty = 'n'
)
})
# -------------------------------------------------------------------
###plot1
# brush and delete with ggplot
vals <- reactiveValues(
df1 <- df_products_upload(),
data=df1
)
############## plotting -1
output$plot1 <- renderPlot({
ggplot(vals$data, aes_string(x = input$column1, y = input$column2)) + geom_point()
})
observe({
df = brushedPoints(vals$data, brush = input$plot1_brush, allRows = TRUE)
vals$data = df[df$selected_== FALSE, ] ## Taking only those data points where the selected_ value is FALSE (alternatively ignoring rows with selected_ = TRUE status)
})
}
#------------------------------------------------------------
shinyApp(ui, server)
I'm trying to get plotly to put values in scientific notation regardless of their size, i.e. 100 should be 1E02 in the ticks, but it keeps showing numbers below 10.000 as normal annotation.
Setting the format is done through exponentformat = "E""but it only affects larger numbers.
Here is an example code of how I write it:
f2 <- list(family = "Old Standard TT, serif", size = 14, color = "black")
ax <- list(showticklabels = TRUE, tickfont = f2, showgrid=F, zeroline=T, showline=T, nticks = 4, exponentformat = "E")
ay <- list(nticks = 4, showticklabels = TRUE, tickfont = f2, showgrid=F, zeroline=T, showline=T, range =c(0,max(mtcars$disp*1.2)), exponentformat = "E")
plot_ly(x = mtcars$mpg , y = mtcars$disp) %>%
add_trace(type = 'scatter', mode = 'markers',
marker = list(color = c('black'))) %>%
add_lines(hoverinfo='none', line = list(color = 'black')) %>%
layout(title = 'A plot in science',yaxis = ay, xaxis = ax,
showlegend = FALSE, hovermode = "y")
manipulating the values to be in the 10k plus range gives the desired output though:
mtcars$disp <- mtcars$disp *100
Let's just do it ourselves in JavaScript, if Plotly doesn't provide the needed functionality.
let's grab all ticks on the y-axis using d3
ticks = Plotly.d3.selectAll('g.ytick');
the raw data is stored in data.x
then change the representation of each one to scientific notation
Plotly.d3
.selectAll('g.ytick')
.each(function(data, i)
{
Plotly.d3.select(this)
.select('text')
.html(formatNumber(data.x, 2));
})
finally inject all the code using htmlwidgets in our graph
p <- onRender(p, javascript)
now it would be one-time only change, every time a user zooms or modifies the plot the changes would be lost. In order to make sure that changes are applied every time the code is wrapped in a function fix_ticks() and added to Plotly's plotly_afterplot event (el is the htmlwidget element)
el.on('plotly_afterplot', fix_ticks);
Update
If you want to change the format of the scientific notation, you could write your function, e.g.
function formatNumber(num, desiredLength)
{
num = num.toExponential().toUpperCase();
var r = /(\\d*)([E][-+])(\\d*)/;
var fields = r.exec(num);
if (fields !== null && fields.length > 3)
{
return fields[1] + fields[2] + fields[3].padStart(desiredLength, '0');
}
else
{
return num;
}
}
and then call it for each tick
ticks.forEach(function(tick)
{
var num = parseInt(tick[0].innerHTML);
tick[0].innerHTML = formatNumber(num, 2);
})
Note: this might not work in RStudio but shows up correctly in your browser after saving the output.
Complete code
library(plotly)
library(htmlwidgets)
p <- plot_ly(x = mtcars$mpg , y = mtcars$disp) %>%
add_lines()
javascript <- "
function(el, x)
{
function fixTicks()
{
Plotly.d3
.selectAll('g.ytick')
.each(function(data, i)
{
Plotly.d3.select(this)
.select('text')
.html(formatNumber(data.x, 2));
})
}
function formatNumber(num, desiredLength)
{
num = num.toExponential().toUpperCase();
var r = /(\\d*)([E][-+])(\\d*)/;
var fields = r.exec(num);
if (fields !== null && fields.length > 3)
{
return fields[1] + fields[2] + fields[3].padStart(desiredLength, '0');
}
else
{
return num;
}
}
el.on('plotly_afterplot', fixTicks);
}"
p <- onRender(p, javascript)
p
Particularly aimed at plots where log scale is used (which seems to cause problems with the current javascript solution), I found another solution without using javascript. It works based on making a list of tickvalues and one of text labels at the whole exponent numbers and leaving the rest empty, and then inserting the two into the plot through the layout arguments for tickvals and ticktext arguments
depending on whether it is a regular scatter or scatter3d the layout code changes a bit, but the principle is the same.
In scatter3d the axes are set within the scene = list() argument. in scatter it is done directly in layout(). camera, autosize etc are arguments used to make the plots nice and square, and for 3D at the right zoom level, and of a fixed size.
The answer is based on another SO post found: here
library(shiny)
library(plotly)
shinyApp(
ui = fluidPage( plotlyOutput('plot') ),
server = function(input, output) {
output$plot <- renderPlotly ({
mtcars <- rbind(mtcars, mtcars*1000, mtcars/1000) #create data with big logarithmic range
maxlog <- round(log10(max(mtcars[['mpg']][mtcars[['mpg']]>0], mtcars[['disp']][mtcars[['disp']]>0],mtcars[['cyl']][mtcars[['cyl']]>0])), digits = 0) +1 # determine max log needed
minlog <- round(log10(min(mtcars[['mpg']][mtcars[['mpg']]>0], mtcars[['disp']][mtcars[['disp']]>0],mtcars[['cyl']][mtcars[['cyl']]>0])), digits = 0) -1 # determine min log needed
logrange <- (maxlog - minlog)*9 +1 # get the distance between smallest and largest log power
tval <- sort(as.vector(sapply(seq(1,9), function(x) x*10^seq(minlog, maxlog)))) #generates a sequence of numbers in logarithmic divisions
ttxt <- rep("",length(tval)) # no label at most of the ticks
ttxt[seq(1,logrange,9)] <- formatC(tval, format = "e", digits = 2)[seq(1,logrange,9)] # every 9th tick is labelled
p <- plot_ly(source = 'ThresholdScatter')
p <- add_trace(p, data = mtcars,
x = mtcars[['mpg']],
y = mtcars[['disp']],
z = mtcars[['cyl']],
type = 'scatter3d',
mode = 'markers',
marker = list(size = 2))
p <- layout(p, autosize = F, width = 500, height = 500,
scene = list(yaxis = list(type="log",
zeroline=F, showline=T,
ticks="outside",
tickvals=tval,
ticktext=ttxt),
xaxis = list(type="log",
zeroline=F, showline=T,
ticks="outside",
tickvals=tval,
ticktext=ttxt),
zaxis = list(type="log",
zeroline=F, showline=T,
ticks="outside",
tickvals=tval,
ticktext=ttxt),
camera = list(eye = list(x = -1.5, y = 1.5, z = 1.5))))
})
}
)
for a 2D solution:
library(shiny)
library(plotly)
shinyApp(
ui = fluidPage( plotlyOutput('plot') ),
server = function(input, output) {
output$plot <- renderPlotly ({
mtcars <- rbind(mtcars, mtcars*1000, mtcars/1000) #create data with big logarithmic range
maxlog <- round(log10(max(mtcars[['mpg']][mtcars[['mpg']]>0], mtcars[['disp']][mtcars[['disp']]>0])), digits = 0) +1 # determine max log needed
minlog <- round(log10(min(mtcars[['mpg']][mtcars[['mpg']]>0], mtcars[['disp']][mtcars[['disp']]>0])), digits = 0) -1 # determine min log needed
logrange <- (maxlog - minlog)*9 +1 # get the distance between smallest and largest log power
tval <- sort(as.vector(sapply(seq(1,9), function(x) x*10^seq(minlog, maxlog)))) #generates a sequence of numbers in logarithmic divisions
ttxt <- rep("",length(tval)) # no label at most of the ticks
ttxt[seq(1,logrange,9)] <- formatC(tval, format = "e", digits = 2)[seq(1,logrange,9)] # every 9th tick is labelled
p <- plot_ly(source = 'ThresholdScatter')
p <- add_trace(p, data = mtcars,
x = mtcars[['mpg']],
y = mtcars[['disp']],
type = 'scatter',
mode = 'markers',
marker = list(size = 2))
p <- layout(p,autosize = F, width = 500, height = 500,
yaxis = list(type="log",
zeroline=F, showline=T,
ticks="outside",
tickvals=tval,
ticktext=ttxt),
xaxis = list(type="log",
zeroline=F, showline=T,
ticks="outside",
tickvals=tval,
ticktext=ttxt))
})
}
)
I want to create an interactive plotly 3d scatter with markers and lines in R. The graphic should be able to highlight individual traces, which is working. It should also be able to change the color according to other variables.
Here is an example of what I want to do, the highlighting works fine, changing the color does not work:
library(plotly)
irs <- data.table(iris)
setkey(irs, `Species`)
p <- plot_ly(type = "scatter3d", mode = "lines+markers")
for (i in levels(irs$Species)) {
xx <- irs[i]$Sepal.Length
yy <- irs[i]$Sepal.Width
zz <- irs[i]$Petal.Length
cc <- irs[i]$Petal.Width
p <- p %>% add_trace(x = xx, y = yy, z = zz, color = cc)
}
p <- p %>%
layout(
updatemenus = list(
## set opacity per trace to highlight a single trace
list(y = 0.6,
buttons = lapply(
levels(irs$Species),
function (x) {
list(method = "restyle",
args = list("opacity",
ifelse(levels(irs$Species) == x,
1, 0.1)),
label = x)
})),
## try to set different colors for points inside traces
## NOT WORKING
list(y = 0.4,
buttons = lapply(
names(irs),
function(x) {
list(
method = "restyle",
args = list(
"color",
split(irs[[x]], irs$Species)
),
label = x
)
}))
)
)
p
The following code solves only a small part of your problem: how to change colors of markers and lines for a single series.
I did not find a solution for the case with multiple series.
In addition, I did not find a way for an automatic rescaling of the color map after changing the color variable from the menu. Hence, I rescaled "by hand" all variables between 1 and 3.
I realise that this is a small contribution to the solution of the problem. Anyway, I hope it can help you.
library(plotly)
library(scales)
irs <- iris
irs[,1:4] <- apply(irs[,1:4],2, rescale, to=c(1,3))
p <- plot_ly(data = irs, type = "scatter3d", mode = "lines+markers")
p <- p %>% add_trace(x=~Sepal.Length, y=~Sepal.Width,
z=~Petal.Length, color=~Petal.Width)
p <- p %>%
layout(
updatemenus = list(
list(y = 0.4,
buttons = lapply(
names(irs),
function(x) {
cols <- as.numeric(irs[,x])
list(
method = "restyle",
label = x,
args = list(
list(marker.color=list(cols),
line.color=list(cols), autocolorscale=TRUE)
)
)
}))
)
)
p