I am interested in creating an interactive plot in plotly.js, where a user can click on the plot and add a new point at that position. Essentially this means retrieving the plot coordinates of a mouse click. Most of the built-in click events deal with capturing a click on an already plotted marker, rather than an arbitrary position within the plot.
The following question deals with this issue, Plotly.js create a point on click. However, the code appears to have been made obsolete by changes to plotly.js at some point. An example of the answer to that question was demo'd on codepen. It appears that the xy mouse position detection now only updates within the toolbar region. My guess is that there was a renaming in the various components that make up the plot.
Link to non-functioning code from the comments of that answers:
https://codepen.io/circleoncircles/pen/abObLLE
var traces = [{
x: [1, 2, 3, 4],
y: [10, 15, 13, 17],
mode: 'markers',
type: 'scatter'
}];
traces.push({
x: [2, 3, 4, 5],
y: [16, 5, 11, 9],
mode: 'markers',
type: 'scatter'
});
traces.push({
x: [1, 2, 3, 4],
y: [12, 9, 15, 12],
mode: 'markers',
type: 'scatter'
});
traces.push({
x: [],
y: [],
mode: 'markers',
type: 'scatter'
});
var myPlot = document.getElementById('myPlot')
Plotly.newPlot('myPlot', traces, {hovermode: 'closest'});
Number.prototype.between = function(min, max) {
return this >= min && this <= max;
};
Plotly.d3.select(".plotly").on('click', function(d, i) {
var e = Plotly.d3.event;
var bg = document.getElementsByClassName('bg')[0];
var x = ((e.layerX - bg.attributes['x'].value + 4) / (bg.attributes['width'].value)) * (myPlot.layout.xaxis.range[1] - myPlot.layout.xaxis.range[0]) + myPlot.layout.xaxis.range[0];
var y = ((e.layerY - bg.attributes['y'].value + 4) / (bg.attributes['height'].value)) * (myPlot.layout.yaxis.range[0] - myPlot.layout.yaxis.range[1]) + myPlot.layout.yaxis.range[1]
if (x.between(myPlot.layout.xaxis.range[0], myPlot.layout.xaxis.range[1]) &&
y.between(myPlot.layout.yaxis.range[0], myPlot.layout.yaxis.range[1])) {
Plotly.extendTraces(myPlot, {
x: [
[x]
],
y: [
[y]
]
}, [3]);
}
});
Plotly.d3.select(".plotly").on('mousemove', function(d, i) {
var e = Plotly.d3.event;
var bg = document.getElementsByClassName('bg')[0];
var x = ((e.layerX - bg.attributes['x'].value + 4) / (bg.attributes['width'].value)) * (myPlot.layout.xaxis.range[1] - myPlot.layout.xaxis.range[0]) + myPlot.layout.xaxis.range[0];
var y = ((e.layerY - bg.attributes['y'].value + 4) / (bg.attributes['height'].value)) * (myPlot.layout.yaxis.range[0] - myPlot.layout.yaxis.range[1]) + myPlot.layout.yaxis.range[1]
if (x.between(myPlot.layout.xaxis.range[0], myPlot.layout.xaxis.range[1]) &&
y.between(myPlot.layout.yaxis.range[0], myPlot.layout.yaxis.range[1])) {
console.log("Location X:"+x+" Y"+y)
document.getElementById("xvalue").value = x;
document.getElementById("yvalue").value = y;
}
});
Does anybody have an updated method for this?
I think I've got it. I would still consider this relatively hacky, so if someone else has a better way to handle it, I'm all ears and would be happy to assign a better answer.
All you really need to do is get the bounding box of the element that represents the plot field. It appears that one such element is that with the class gridlayer. Then I grabbed its bounding rect.
So the computation lines in the above would become:
var bgrect = document.getElementsByClassName('gridlayer')[0].getBoundingClientRect();
var x = ((e.x - bgrect['x']) / (bgrect['width'])) * (myPlot.layout.xaxis.range[1] - myPlot.layout.xaxis.range[0]) + myPlot.layout.xaxis.range[0];
var y = ((e.y - bgrect['y']) / (bgrect['height'])) * (myPlot.layout.yaxis.range[0] - myPlot.layout.yaxis.range[1]) + myPlot.layout.yaxis.range[1];
One additional change is necessary if you want to use the latest version of plotly. d3 is no longer embedded in plotly, so it's necessary to get d3 from CDN (the most recent version I found to work for this is v5).
<script src="https://cdn.plot.ly/plotly-2.11.0.min.js"></script>
<script src="//d3js.org/d3.v5.min.js"></script>
In the script you just reference d3 at the root then:
d3.select(".plotly").on('click', function(d, i) {
var e = d3.event;
I revised the codepen example that was posted previously to this new method https://codepen.io/jranalli/pen/eYyVVgr
Related
I have a Functiongraph line defined like this:
const f1 = function(x) {
const slope = me.options.gLine1Slope;
return (x - 2.5) * slope + 2.5;
};
this.l1 = this.board.create('functiongraph', [f1, -30, 30], {
recursionDepthLow: 8,
recursionDepthHigh: 15
});
this.l1.setPosition(window.JXG.COORDS_BY_USER, [
forceFloat(this.options.gLine1OffsetX),
forceFloat(this.options.gLine1OffsetY)
]);
I'm aware that Functiongraph is just a wrapper for Curve, so I've been reading through both API docs.
I'm manually positioning this line based on these offset values because it can be dragged around the plane by the user.
I can get a value close to the Y-intercept, like this:
f1(0) + (this.options.gLine1OffsetY - this.options.gLine1OffsetX)
But it's not quite right, after this line is dragged around a bit. Can anyone give some guidance on how to get the Y-intercept for this curve? I suppose I can just iterate through the data array of points along this curve, and pick the one where Y is closest to 0. I was just hoping there is a more straightforward way as well, though.
You are right. Getting the y-intercept of the function graph after dragging it around freely is not easy. The reason is that dragging objects is mostly implemented using projective transformations. This makes it complicated to get the y-intercept of the curve which is visible at the moment. The easiest approach I can think of for the moment is to intersect the curve with the vertical axis and get the position of that point. Here is a slighlty modified version of your example:
const board = JXG.JSXGraph.initBoard('jxgbox', {
boundingbox: [-5, 5, 5, -5], axis:true
});
var me = {
gLine1Slope:2,
gLine1OffsetX: 1,
gLine1OffsetY: -1
};
const f1 = function(x) {
const slope = me.gLine1Slope;
return x * slope;
};
var l1 = board.create('functiongraph', [f1, -30, 30], {fixed: false});
var p = board.create('intersection', [l1, board.defaultAxes.y], {
withLabel: false, visible: false});
// Now, we can move the curve and get the y-intercept
// in p.Y()
l1.setPosition(window.JXG.COORDS_BY_USER, [
me.gLine1OffsetX,
me.gLine1OffsetY
]);
board.update();
board.on('up', function(evt) {
document.getElementById('jxg_debug').value = p.Y();
});
document.getElementById('jxg_debug').value = p.Y();
See it live at https://jsfiddle.net/3s90qx57/2/
I working on a project that needs me to use graph csv file inputs. I've been using plotly and so far it seems to be working very well.
However, when I try to change the color of the graphs (lines and markers) it doesn't work. I am posting excerpts of my code since the color is overall a small portion of the code and I don't want to dump everything here.
//There's multiple charts so changing color is important
var r = Math.random() * 256
var g = Math.random() * 256
var b = Math.random() * 256
...
//used these as a vars so I can change things to test easily (multiple time series being used)
var color='rgb('+r+', '+g+', '+b+')'
var colora='rgba('+r+', '+g+', '+b+', '+'0.14'+')'
...
//layout of markers
{
x: time,
y: time,
z: data1,
line: {
reversescale: false,
//color: "'"+color+"'"
color: "'rgb("+r+', ' +g+', '+ b+")'",
},
//mode: 'lines',
marker: {
//color: "'"+color+"'",
color: "'rgb("+r+', ' +g+', '+ b+")'",
size: 3,
line: {
//color: "'"+colora+"'",
color: "'rgb("+r+', ' +g+', '+ b+")'",
width: 0.1
},
opacity: 0.8
},
type: 'scatter3d'
}
Both the attempts just give me the standard black dots. When I tried constants that worked fine (something like color:'rgb(100,100,240)'). Is there something I'm missing here? I've console.logged this thing and it doesn't seem to be an issue with the structure of my vars.
You have too many quotation marks around your rgb strings. In order to avoid confusion when concatenating strings, you could also use template strings.
See the working fiddle below.
const r = 0;
const g = 255;
const b = 0;
const color = 'rgb(' + r + ',' + g + ',' + b + ')';
const colorTemplate = `rgb(${r},${g},${b})`;
var trace1 = {
x: [1, 2, 3, 4],
y: [10, 15, 13, 17],
type: 'scatter',
marker: {
color: color
}
};
var trace2 = {
x: [1, 2, 3, 4],
y: [16, 5, 11, 9],
type: 'scatter',
marker: {
color: colorTemplate
}
};
var data = [trace1, trace2];
Plotly.newPlot('myDiv', data);
<head>
<!-- Load plotly.js into the DOM -->
<script src='https://cdn.plot.ly/plotly-latest.min.js'></script>
</head>
<body>
<div id='myDiv'><!-- Plotly chart will be drawn inside this DIV --></div>
</body>
The problem with my code is that I'm using vars. By switching to consts, I can fix the thing. I didn't catch the color not changing because for some reason the legend was displaying the right colors, but the markers and lines don't accept it.
In a related question I was looking for a way to draw points in 3D space so that the points will move according to slider values. Now that is working, but the conic section (a parabola in this case) I am trying to draw through these points, is not drawn.
I thought that the constructor for the element "conic" might be picky about how the given points are defined, so I ended up adding as attributes "sub-objects" that are points that can be referred to when drawing the conic.
In my code below, the constructor function PPoint creates objects that have their respective attributes pcoord, which is a geometric object of type "point" created using the native jsxgraph constructor for points. pcoord is assigned when the method "draw" is called to the draw the points I_1-I-4 and p_1.
In the last lines of the code, the parabola should be drawn by referring to the pcoords of objects I_1-I_4 and p_1, but for some reason the parabola is not drawn.
How could this be fixed? Link to jsfiddle. The code is executed without error notifications when debugging.
HTML
<div id="jxgbox" class="jxgbox" style="width:500px; height:500px">
</div>
JS
const board = JXG.JSXGraph.initBoard('jxgbox', {
boundingbox: [-10, 10, 10, -10],
axis: true,
showCopyright: true,
showNavigation: true,
pan: false,
grid: false,
zoom: {
factorX: 1.25,
factorY: 1.25,
wheel: false
}
});
//create z axis
var zAxis = board.create('axis', [
[0, 0],
[-1, -1]
], {
ticks: {
majorHeight: 10,
drawLabels: false
}
});
//create direction of view for projections
var cam = [4, 4, 30]; // [x,y,z]
var r = 6.0;
var origin = [0, 0, 0];
// Function for parallel projection
var project = function(crd, cam) {
var d = -crd[2] / cam[2];
return [1, crd[0] + d * cam[0], crd[1] + d * cam[1]];
};
//create slider for rotating the parabola
var sRadius = board.create('slider', [
[1, -8.5],
[6, -8.5],
[-10, 0, 10]
], {
name: 'angle',
needsRegularUpdate: true
//snapWidth: 1
});
//create slider for adjusting the angular speed
var sOmega = board.create('slider', [
[1, -7.5],
[6, -7.5],
[0, 0, 10]
], {
name: 'Omega',
needsRegularUpdate: true
//snapWidth: 1,
});
//fix parameters
const g = 9.81 //gravitational acceleration
const h0 = 5 //initial height of the water surface
//define radius from the y-axis for I3 and I4
const R34 = Math.sqrt(2);
// Function for parallel projection
var project = function(crd, cam) {
var d = -crd[2] / cam[2];
return [1, crd[0] + d * cam[0], crd[1] + d * cam[1]];
};
//function creates points for drawing conic sections
function PPoint(radius, sign, namep, fixval) {
this.R = radius;
this.S = sign;
this.Namep = namep;
this.Fixval = fixval;
this.pcoord = undefined; //Cartesian coordinates of the point, stored as a point
}
//method for drawing each Point
PPoint.prototype.draw = function(pp) {
board.create('point', [function() {
var K1 = sOmega.Value() * sOmega.Value() / g,
KK = 1 / 4 * sOmega.Value() * sOmega.Value() / g,
v = sRadius.Value() * Math.PI * 0.5 / 10.0,
c = [pp.S * pp.R * Math.sin(v), K1 / 2 * pp.R * pp.R - KK + h0, pp.S * pp.R * Math.cos(v)];
//store the dynamically assigned coordinates of the point for drawing the parabola
pp.pcoord = board.create('point', [function() {
return project(c, cam);
}], {
visible: false
}); //end storing pp.coord
return project(c, cam);
}], {
fixed: this.Fixval,
name: this.Namep,
visible: true
})
}
//create and draw points
var p_1 = new PPoint(0, -1, 'p_1', 'false');
var I_1 = new PPoint(r, 1, 'I_1', 'false');
var I_2 = new PPoint(r, -1, 'I_2', 'false');
var I_3 = new PPoint(R34, 1, 'I_3', 'false');
var I_4 = new PPoint(R34, -1, 'I_4', 'false');
p_1.draw(p_1)
I_1.draw(I_1)
I_2.draw(I_2)
I_3.draw(I_3)
I_4.draw(I_4)
//draw the rotating parabola
var prbl = board.create('conic', [I_1.pcoord, I_2.pcoord, I_3.pcoord, I_4.pcoord, p_1.pcoord], {
strokeColor: '#CA7291',
strokeWidth: 2,
trace :true
});
//debugger
There are two issues with this code:
1) In PPoint.draw the reference two the JSXGraph point does not work: in each update an new JSXGraph point is created. This makes the code slow and - moreover - does not influence the initial points supplied to the conic section. I would propose to change draw to this:
PPoint.prototype.draw = function(pp) {
pp.pcoord = board.create('point', [function() {
var K1 = sOmega.Value() * sOmega.Value() / g,
KK = 1 / 4 * sOmega.Value() * sOmega.Value() / g,
v = sRadius.Value() * Math.PI * 0.5 / 10.0,
c = [pp.S * pp.R * Math.sin(v),
K1 / 2 * pp.R * pp.R - KK + h0,
pp.S * pp.R * Math.cos(v)];
return project(c, cam);
}], {
fixed: this.Fixval,
name: this.Namep,
visible: true});
2) The second problem is that JSXGraph fails to plot degenerated conics through five points and suffers in precision if the conic is close to be degenerated (there are numerical issues with general parabolas). This is the case here for the start value omega = 0.
Here is a working example: https://jsfiddle.net/L2d4zt8q/
I would like to fill until a specific Y value in PlotlyJS. This is as far as I got from the PlotlyJS docs: Fiddle
{
"x": [
"2016-01-31T00:03:57.000Z",
"2016-02-12T04:35:26.000Z"
],
"y": [
100,
100
],
"fill": "tonexty",
"fillcolor": "#8adcb3"
}
In the documentation, there seems to be two options:
tonexty - Fills as below. The problem is 'tonexty' is a bit limiting - The use case is 'filling until the line', so shading ONLY above 110. Example:
tozeroy - Fills till zero:
Also, do you need to introduce a new trace in order to create a fill?
This means that if I have a chart as follows (with only one trace but a threshold line as a shape): I need to introduce another trace just to create a fill. Maybe there's something I missed in the docs, or this is the wrong approach altogether.
So, how do you fill an area in a trace above a specific Y value in PlotlyJS?
A solution is to use multiple traces.
Split all your traces between ones which are above 0 and ones which are not.
When you are done you can fill them (or not) with the 'tozeroy' value.
The following jsfiddle shows a working example.
The code is as following :
HTML:
<div id="myDiv" style="width:600px;height:250px;"></div>
JS:
var data = [
{
x: ['A', 'B', 'C', 'D'],
y: [1, 3, 6, 0],
fill: 'tozeroy',
fillcolor: '#8adcb3'
},
{
x: ['D', 'F', 'G', 'I'],
y: [0, -3, -2, 0],
fill: 'toself'
},
{
x: ['I', 'J', 'K'],
y: [0, 5, 7],
fill: 'tozeroy',
fillcolor: '#0adcb3'
}
];
Plotly.newPlot('myDiv', data);
The result looks as following :
Here is another solution exploiting Plotly's fill: "toself". The idea is to create a closed line trace which encloses the area above the threshold and the markers of the main line. Works for threshold values above zero and for numerical x-values.
The helper traces have their legend hidden and are grouped with the main trace, thereby preventing ugly artifacts when toggling the legend.
The function checks for each x-y-pair if the y-value is above the threshold, if yes
check if there is already a segment above the threshold and use this one OR create a new sgement
the segement starts from the y-value of the threshold and the intermediate x-value from the point above the threshold and the one before.
each segment is terminated with an y-value which is equal to the threshol and the x-value which the mean of the last point in the segment and the next one
The function itself can be surely written in a nicer way but it's just a proof-of-concept.
function dataToTraces(data, threshold) {
var fillers = [];
var emptyFiller = {
x: [],
y: [],
fill: "toself",
mode: "lines",
line: {
width: 0
},
opacity: 0.5,
fillcolor: "#8adcb3",
showlegend: false,
legendgroup: "main"
}
fillers.push(emptyFiller);
for (var i = 0; i < data.y.length; i += 1) {
if (data.y[i] >= threshold) {
if (i !== 0 && data.y[i - 1] < threshold) {
fillers[fillers.length - 1].x.push(data.x[i - 1] + (threshold - data.y[i - 1]) / (data.y[i] - data.y[i - 1]));
fillers[fillers.length - 1].y.push(threshold);
}
fillers[fillers.length - 1].x.push(data.x[i]);
fillers[fillers.length - 1].y.push(data.y[i]);
} else if (fillers[fillers.length - 1].x.length > 0) {
if (i !== 0 && data.y[i - 1] !== threshold) {
fillers[fillers.length - 1].x.push(data.x[i - 1] + (threshold - data.y[i - 1]) / (data.y[i] - data.y[i - 1]));
fillers[fillers.length - 1].y.push(threshold);
}
fillers.push(emptyFiller);
}
}
return fillers;
}
var data = [{
x: [0, 1, 2, 3, 4, 5, 6, 7, 8],
y: [1, 3, 6, 2, -1, 5, 1, 3, 0],
name: "main",
legendgroup: "main"
}];
var fillers = dataToTraces(data[0], 2);
Plotly.newPlot("myDiv", data.concat(fillers));
<div id="myDiv"></div>
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
Here you can see a chart created using graphael. http://jsfiddle.net/aNJxf/4/
It is shown with it's y axis correctly.
The first y value is 0.03100 and the maximum value at y axis is at 0.031
If we change the value to 0.03104 the maximum value at y axis becomes 1.03 and now all our points are in the bottom.
If we add another 0.00001, which makes that value 0.03105, the maximum at the axis y becomes 0.531 and now our points are shown at the wrong position of the chart.
It seems that something is going wrong while graphael calculates the maximum y axis value.
Why this happens? And how we can fix that?
The code that I have pasted there is
var r = Raphael("holder"),
txtattr = { font: "12px sans-serif" };
var x = [], y = [], y2 = [], y3 = [];
for (var i = 0; i < 1e6; i++) {
x[i] = i * 10;
y[i] = (y[i - 1] || 0) + (Math.random() * 7) - 3;
}
var demoX = [[1, 2, 3, 4, 5, 6, 7],[3.5, 4.5, 5.5, 6.5, 7, 8]];
var demoY = [[12, 32, 23, 15, 17, 27, 22], [10, 20, 30, 25, 15, 28]];
var xVals =[7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58];
var yVals = [0.03100,0.02259,0.02623,0.01967,0.01967,0.00788,0.02217,0.0137,0.01237,0.01764,0.0131,0.00942,0.0076,0.01463,0.02882,0.02093,0.02502,0.01961,0.01551,0.02227,0.0164,0.0191,0.00774,0.03076,0.0281,0.01338,0.02763,0.02334,0.00557,0.00023,0.01523,0.0263,0.03077,0.02404,0.02492,0.01954,0.01954,0.02337,0.01715,0.02271,0.00815,0.01343,0.00985,0.01837,0.00749,0.02967,0.01156,0.0083,0.00209,0.01538,0.01348,0.01353];
//r.text(160, 10, "Symbols, axis and hover effect").attr(txtattr);
var lines = r.linechart(10, 10, 300, 220, xVals, yVals, { nostroke: false, axis: "0 0 1 1", symbol: "circle", smooth: true })
.hoverColumn(function () {
this.tags = r.set();
for (var i = 0, ii = this.y.length; i < ii; i++) {
this.tags.push(r.tag(this.x, this.y[i], this.values[i], 160, 10).insertBefore(this).attr([{ fill: "#fff" }, { fill: this.symbols[i].attr("fill") }]));
}
}, function () {
this.tags && this.tags.remove();
});
lines.symbols.attr({ r: 3 });
Thanks
Sorry, I'm not real familiar with gRaphael, but I did find that converting your yVals into whole numbers (by multiplying each by 1e5) seemed to rid your chart of the awkward behavior.
This suggests that it could be related to the algorithm gRaphael uses to find the max axis value (as you ask in your related question) when your values are small decimal values (and alter at even more significant digits).
I know there are inherent issues with float precision, but I can't be sure that applies to your case, or that your values are low enough to consider this.
Not the best workaround, but if it would be feasible for you, you could display the yValues in an order of magnitude larger, and remind the viewer that they are actually smaller than presented. For example, your chart could go from 0 to 3100 and remind your viewer that the scale is scale * 1e-5.