I have a line-plot with very little data. A bit like this:
plot_ly(x = c( -2, 0, 1.5 ),y = c( -2, 1, 2.2),
type = 'scatter' ,mode = 'lines+markers') %>%
add_trace(x=c(-1,0.4,2.5),y=c(2, 0, -1),type='scatter',mode='lines+markers')
I want to know whether plotly can display all the hoverinfos of a line at ones. Such that for example when I click on "trace 1" in the legend, I can see it has the points (-1,2), (0.4,0), (2.5,-1) beside the corresponding point. Couldn't find anything so far.
You can trigger hover events with Plotly.Fx and add the necessary Javascript code by using htmlwidgets.
Fx.hover needs a curveNumber and curvePoint (the n-th of your trace) as input parameters. If you want to trigger events for multiple points, you need to pass an array of objects.
library("plotly")
library("htmlwidgets")
p <- plot_ly(x = c(-2, 0, 1.5 ),
y = c(-2, 1, 2.2),
type = 'scatter',
mode = 'lines+markers') %>%
add_trace(x = c(-1, 0.4, 2.5),
y = c(2, 0, -1),
type = 'scatter',
mode = 'lines+markers') %>%
layout(hovermode = 'closest')
javascript <- "
var myPlot = document.getElementsByClassName('plotly')[0];
myPlot.on('plotly_click', function(data) {
var hover = [];
for (var i = 0; i < data.points[0].data.x.length; i += 1) {
hover.push({curveNumber: data.points[0].curveNumber,
pointNumber: i});
}
Plotly.Fx.hover(myPlot, hover);
});"
p <- htmlwidgets::prependContent(p, onStaticRenderComplete(javascript), data = list(''))
p
Note: Plotly.Fx will be deprecated.
Related
I'm using Javascript and D3.js to create a Custom Widget in Thingsboard. I've managed to make a Chart work that plots a line from a given datasource. But I'm facing an issue when changing the datasource.
When user clicks on a new datasource, it should replaced the data on the graph. My D3.js chart takes data from an Array of point objects, with X and Y coordinates, that's generated from the datasource.
But I'm having the following issue: The dataset array gets the data correctly only the first time that it gets the points pushed. Whenever I try to clear the array or remove the dataset, and reapply the points, it acts as if the points were already there, but hiden. The problem will be clearer if you look at the Console log output.
Notice that the Array acts as if it was always the same/It contained the same points, even when I renew it with:
dataset = [];
I'm new to Javascript so this could be a very dumb problem, but I cannot find any similar issues nor know how to look for the issue.
My dataset is coded in a string like this:
"25,351,-442,0,112,3447,28,45,..."
Here's the code of my updating function:
var dataset = [];
function addDataPoint (value) {
//widget = this;
widget.string = value + ''; //Prevents compile error from .split()
var pointsArray = [];
var pointsArray = widget.string.split(',');
var newList = []; //As dataset is referenced by the D3.js chart, I wanted to test with an Unreferenced array to check if it had something to do with the reference.
console.log("PointsArrayLength " + pointsArray.length);
for (var index = 0; index < pointsArray.length; index++){
var number = parseInt(pointsArray[index],10);
var point = {
x: index % self.ctx.settings.xRangeMax,
y: number
};
if (widget.lastX && widget.lastX > point.x) {
//widget.data.length = 0;
}else{
if(!isNaN(number)){
widget.lastX = point.x;
newList.push(point);
dataset.push(point);
}
}
}
console.log("UnreferencedList Length: " +newList.length + " Index: "+index);
console.log("Referenced length: " + dataset.length);
//Redraw the graph
widget.line.attr('d', widget.lineGenerator);
}
//The function that gets called when the dataset changes.
self.onDataUpdated = function() {
var xValueString;
try{
var xValueString = self.ctx.defaultSubscription.data[0].data[0][1];
console.log("Size of data: " + xValueString.length);
dataset = [];//NOTE
/* I've tried several methods here to renew the Array.
dataset.length = 0 ; a while loop with dataset.pop() until length = 0, etc.
None of the methods make a difference*/
addDataPoint(xValueString);
console.log("Size of data: " + dataset.length);
} catch(err){
//For other debugging purposes
//console.log(err);
}
And here's the output from the Console Log:
//First run on dataset number 1:
Size of data: 55411
PointsArrayLength 3001
UnreferencedList Length: 3000 Index: 3001
Referenced length: 3000
Size of data: 3000
//First run on dataset Number 2:
//Note: Dataset Number 2 contains dataset number 1, and adds 12879 more points
Size of data: 69021
PointsArrayLength 15879
UnreferencedList Length: 12879 Index: 15879
Referenced length: 12879
Size of data: 12879
//Second Run on dataset number 1:
Size of data: 55411
PointsArrayLength 3001
UnreferencedList Length: 0 Index: 3001
Referenced length: 0
Size of data: 0
//Second Run on dataset Number 2:
Size of data: 69021
PointsArrayLength 15879
UnreferencedList Length: 1 Index: 15879
Referenced length: 1
Size of data: 1
I think that you are having scope issues. Define newlist as a global var.
let dataset = [];
let newlist = [];
function addDataPoint (value) {
//widget = this;
widget.string = value + ''; //Prevents compile error from .split()
let workArray = []
let pointsArray = [];
pointsArray = widget.string.split(',');
console.log("PointsArrayLength " + pointsArray.length);
for (var index = 0; index < pointsArray.length; index++){
var number = parseInt(pointsArray[index],10);
var point = {
x: index % self.ctx.settings.xRangeMax,
y: number
};
if (widget.lastX && widget.lastX > point.x) {
//widget.data.length = 0;
}else{
if(!isNaN(number)){
widget.lastX = point.x;
newList.push(point);
//dataset.push(point);
workArray.push(point);
}
}
}
console.log("UnreferencedList Length: " +newList.length + " Index: "+index);
console.log("Referenced length: " + dataset.length);
//Redraw the graph
widget.line.attr('d', widget.lineGenerator);
return workArray;
}
//The function that gets called when the dataset changes.
self.onDataUpdated = function() {
try{
let xValueString = self.ctx.defaultSubscription.data[0].data[0][1];
console.log("Size of data: " + xValueString.length);
//dataset = [];//NOTE
/* I've tried several methods here to renew the Array.
dataset.length = 0 ;
None of the methods make a difference*/
dataset = addDataPoint(xValueString);
console.log("Size of data: " + dataset.length);
} catch(err){
}
I'd like to customize appearance of clustered markers in Leaflet/Shiny application based on sum of an attribute of child markers.
It is similar to this problem, which makes icon color of clusters based on count of children. What if I want to customize icon based on the sum of magnitude of earthquakes?
With pure javascript application, seems like I should be able to set custom property to individual marker, then access it from iconCreateFunction, as done in this example.
But I am adding marker with addCircleMarkers and addMarkers from leaflet for R, and doesn't seem i can add arbitrary attribute to markers being generated. Code below works but it doesn't if i uncomment two lines (mag = ~mag and sum += markers[i].mag;)
leaflet(quakes) %>% addTiles() %>% addMarkers(
# mag = ~mag,
clusterOptions = markerClusterOptions(
iconCreateFunction=JS("function (cluster) {
var markers = cluster.getAllChildMarkers();
var sum = 0;
for (i = 0; i < markers.length; i++) {
// sum += markers[i].mag;
sum += 1;
}
return new L.DivIcon({ html: '<div><span>' + sum + '</span></div>'});
}")
)
)
I thought about using label= option of addMarkers, and then parse it from Javascript. But the markers accessed with getAllChildMarkers() on marker cluster in JS does not seem to have label property.
I also thought about passing a dataframe from R to leaflet(JS), somehow, maybe like this example, or this ...?
Found my answer. Seems like I can put arbitrary property inside options= in addMarker:
leaflet(quakes) %>% addTiles() %>% addMarkers(
options = markerOptions(mag = ~mag),
clusterOptions = markerClusterOptions(
iconCreateFunction=JS("function (cluster) {
var markers = cluster.getAllChildMarkers();
var sum = 0;
for (i = 0; i < markers.length; i++) {
sum += Number(markers[i].options.mag);
// sum += 1;
}
return new L.DivIcon({ html: '<div><span>' + sum + '</span></div>'});
}")
)
)
I have a lasso and a hover tool in bokeh, each with a similar callback to interact with a secondary plot (the hover will display meta data associated with a single data point, while the lasso will display the same meta data averaged across points).
The callbacks work individually for hover and for lasso, but when both are active, the hover dominates. I'd like to make is so that the user can choose either lasso or hover but not both.
Is there a way to trigger a callback when the active tool is changed, preferably in CustomJS so I don't have to run bokeh server?
Below is some sample code and here is a link to the output.
from bokeh.io import output_file
from bokeh.layouts import row
from bokeh.plotting import figure, show
from bokeh.models import (
ColumnDataSource, CustomJS,
HoverTool, LassoSelectTool)
output_file('toggle_lasso_hover.html')
s1 = ColumnDataSource({
'x': [0, 1, 2],
'y': [1, 1, 1]
})
s2 = ColumnDataSource({
'x': [],
'y': []
})
js_args = {'s1': s1, 's2': s2}
js_code = """
var d1 = s1.data;
var d2 = {'x': [], 'y': []};
for (i=0; i < inds.length; i++) {
j = inds[i];
d2['x'].push(d1['x'][j]);
d2['y'].push(d1['y'][j]);
}
s2.data = d2;
s2.change.emit();
"""
on_hover = CustomJS(args=js_args, code="""
var inds = cb_data.index['1d'].indices;
%s
""" % js_code)
on_lasso = CustomJS(args=js_args, code="""
var inds = cb_obj.selected['1d'].indices;
%s
""" % js_code)
p1 = figure(
width=300, height=300, tools="reset", active_drag=None)
p1.circle('x', 'y', source=s1, size=20, color='blue')
# define tools
hover = HoverTool(tooltips=None, callback=on_hover)
lasso = LassoSelectTool()
p1.add_tools(hover)
p1.add_tools(lasso)
s1.callback = on_lasso
p2 = figure(
width=300, height=300, x_range=p1.x_range, y_range=p1.y_range,
toolbar_location=None)
p2.circle('x', 'y', source=s2, size=20, color='green')
lout = row(p1, p2)
show(lout)
I posted a similar question earlier (Retrieving R object attributes in JavaScript). In that earlier post, I oversimplified my MWE, and so the answer I rewarded unfortunately does not really apply to my real problem. Here, I am showing why I may need to retrieve R object attributes in JavaScript (unless there is another option that I am not aware of).
I have a 5-variable dataset with 100 observations. I used hexagon binning and created a scatterplot matrix. Each of the 10 scatterplots has somewhere between 12-18 hexagons. In order to save the rows of the 100 observations that are in each of the hexagon bins for all 10 scatterplots, I used the base::attr function in R. In the code below, this is done at:
attr(hexdf, "cID") <- h#cID
I am trying to create an interactive R Plotly object of the hexagon binning so that if a user were to click on a given hexagon bin (regardless of which scatterplot), they would obtain the rows of the 100 observations that were grouped into that bin. I have part of this goal completed. My MWE is below:
library(plotly)
library(data.table)
library(GGally)
library(hexbin)
library(htmlwidgets)
set.seed(1)
bindata <- data.frame(ID = paste0("ID",1:100), A=rnorm(100), B=rnorm(100), C=rnorm(100), D=rnorm(100), E=rnorm(100))
bindata$ID <- as.character(bindata$ID)
maxVal = max(abs(bindata[,2:6]))
maxRange = c(-1*maxVal, maxVal)
my_fn <- function(data, mapping, ...){
x = data[,c(as.character(mapping$x))]
y = data[,c(as.character(mapping$y))]
h <- hexbin(x=x, y=y, xbins=5, shape=1, IDs=TRUE, xbnds=maxRange, ybnds=maxRange)
hexdf <- data.frame (hcell2xy (h), hexID = h#cell, counts = h#count)
attr(hexdf, "cID") <- h#cID
p <- ggplot(hexdf, aes(x=x, y=y, fill = counts, hexID=hexID)) + geom_hex(stat="identity")
p
}
p <- ggpairs(bindata[,2:6], lower = list(continuous = my_fn))
pS <- p
for(i in 2:p$nrow) {
for(j in 1:(i-1)) {
pS[i,j] <- p[i,j] +
coord_cartesian(xlim = c(maxRange[1], maxRange[2]), ylim = c(maxRange[1], maxRange[2]))
}
}
ggPS <- ggplotly(pS)
myLength <- length(ggPS[["x"]][["data"]])
for (i in 1:myLength){
item =ggPS[["x"]][["data"]][[i]]$text[1]
if (!is.null(item))
if (!startsWith(item, "co")){
ggPS[["x"]][["data"]][[i]]$hoverinfo <- "none"
}
}
ggPS %>% onRender("
function(el, x, data) {
el = el;
x=x;
var data = data[0];
console.log(el)
console.log(x)
console.log(data)
myLength = Math.sqrt(document.getElementsByClassName('cartesianlayer')[0].childNodes.length);
console.log(myLength)
el.on('plotly_click', function(e) {
console.log(e.points[0])
xVar = (e.points[0].xaxis._id).replace(/[^0-9]/g,'')
if (xVar.length == 0) xVar = 1
yVar = (e.points[0].yaxis._id).replace(/[^0-9]/g,'')
if (yVar.length == 0) yVar = 1
myX = myLength + 1 - (yVar - myLength * (xVar - 1))
myY = xVar
cN = e.points[0].curveNumber
split1 = (x.data[cN].text).split(' ')
hexID = (x.data[cN].text).split(' ')[2]
counts = split1[1].split('<')[0]
console.log(myX)
console.log(myY)
console.log(hexID)
console.log(counts)
})}
", data = pS[5,2]$data)
This creates an image as shown below:
As an example, if I click on the hexagon highlighted in the green box, I can determine which subplot it occurred in ("myX" and "myY"), the ID of the hexagon clicked ("hexID"), and the number of observation points that were binned into that hexagon ("counts"). For this particular hexagon, myX=5, myY=2, hexID=39, and counts=1. So, the user just clicked on hexagon with ID39 in the scatterplot on the fifth row and second column and there should be 1 data point that it binned.
If I leave the onRender() function, and simply type into R the following code:
myX <- 5
myY <- 2
hexID <- 39
obsns <- which(attr(pS[myX,myY]$data, "cID")==hexID)
dat <- bindata[obsns,]
Then, I can obtain the row of the data frame that contains the one observation that was binned into that clicked hexagon:
> dat
ID A B C D E
95 ID95 1.586833 -1.208083 1.778429 -0.1101588 3.810277
My problem is simply in this last step. I am unable to figure out how to use the base::attr() function from within the onRender() function in order to obtain the "obsns" object. Is there any workaround for this issue, or another possible approach I should consider taking? Thank you for any ideas/advice!
I'm not sure you can access the hex IDs from plotly or whether it keeps this data somewhere so one option is to pass all the data needed for your purpose to the onRender function.
First you could add to your bindata dataframe a column per hexplot, called mX-mY (where you replace mX and mY by their value for each column), that would hold for each observation the hexbin it belongs to for that plot:
for(i in 2:5) {
for(j in 1:4) {
bindata[[paste(i,j,sep="-")]] <- attr(pS[i,j]$data, "cID")
}
}
You can then pass bindata to the onRender function and whever you click on a hexagon in one of the plot, check in the corresponding column in bindata which observations belong to that hexbin:
ggPS %>% onRender("
function(el, x, data) {
myLength = Math.sqrt(document.getElementsByClassName('cartesianlayer')[0].childNodes.length);
el.on('plotly_click', function(e) {
xVar = (e.points[0].xaxis._id).replace(/[^0-9]/g,'')
if (xVar.length == 0) xVar = 1
yVar = (e.points[0].yaxis._id).replace(/[^0-9]/g,'')
if (yVar.length == 0) yVar = 1
myX = myLength + 1 - (yVar - myLength * (xVar - 1))
myY = xVar
cN = e.points[0].curveNumber
split1 = (x.data[cN].text).split(' ')
hexID = (x.data[cN].text).split(' ')[2]
counts = split1[1].split('<')[0]
var selected_rows = [];
data.forEach(function(row){
if(row[myX+'-'+myY]==hexID) selected_rows.push(row);
});
console.log(selected_rows);
})}
", data = bindata)
I'm trying to show a line but when I initiate the polyline like this nothing shows:
var geopositions = [];
for (var i = 0; i < c.geo.length; i++) {
var g = c.geo[i];
geopositions.push(parseFloat(g.lon));
geopositions.push(parseFloat(g.lat));
}
var line = {
positions: Cesium.Cartesian3.fromDegreesArray(geopositions),
width: 1,
id: "C" + c.id,
material: Cesium.Material.fromType('Color', {
color: Cesium.Color.fromBytes(255, 0, 0, 255)
}),
show: true
}
var coll = new Cesium.PolylineCollection();
coll.add(line);
primitives.add(coll);
So I thought I'd try to draw lines between all the points of the line (the points in c.geo) like so:
var collection = new Cesium.PolylineCollection();
var prev = null;
for (var j = 0; j < c.geo.length; j++) {
var geo = c.geo[j];
if (prev) {
collection.add(
{
positions: Cesium.Cartesian3.fromDegreesArray([
parseFloat(prev.lon), parseFloat(prev.lat),
parseFloat(geo.lon), parseFloat(geo.lat)]),
width: 2,
material: Cesium.Material.fromType('Color', {
color: Cesium.Color.fromBytes(0, 180, 0, 255)
})
}
);
}
prev = geo;
}
primitives.add(collection);
For some reason this does show the line. I can't find the reason why this would be the case and don't understand why a list of lines does show and a standard polyline doesn't show. Does anyone know how to show the line without chopping the line up in small polylines?
Cesium is supposed to handle the case you described. You are most likely running into some form of this bug; which has been fixed with this pull request.
I fixed the problem.
Apparently the Cesium.Polyline doesn't appreciate two consecutive coordinates (both lat and lon) that are exactly the same. The problem seems to be solved by removing the extra coordinates.