I have started playing with P5JS, but I wanted to create a tooltip on mouse over for each of the data points when the pointer is over it.
How can I do this? I have seen some examples using the map function, but unsure how this would work here.
Appreciate any help! I am not a JS dev just yet!
Generally speaking displaying tooltips that are specific to graphics displayed on a canvas involves "hit testing," which is to say: checking if the mouse is hovering over the specific graphics, and then some mechanism for displaying the tooltip.
Since your graphics are all circles, hit testing is quite simple. For each circle that you draw, check the distance to the mouse position. If the distance is less than the radius of the circle than the mouse is hovering over that circle:
let mouseDist = dist(posX, posY, mouseX, mouseY);
if (mouseDist < earthquakeMag * 5) {
// display tooltip, or set a flag to display the
// tooltip after the rest of the graphics have been
// displayed
}
As for displaying the tooltip you have two options: 1) you can rely on the native behavior of browser elements by setting the title attribute of the canvas element, or 2) you can display your own tooltip. The advantage of the former is that it is very trivial, but the advantage of the latter is that you have more control over where/when/how the tooltip is rendered.
Option 1:
let c;
function setup() {
c = createCanvas(500,500);
// ...
}
function draw() {
// ...
for (var i = 0; i < this.data.getRowCount(); i++) {
// ...
let mouseDist = dist(posX, posY, mouseX, mouseY);
if (mouseDist < earthquakeMag * 5) {
c.elt.title = 'hit!';
}
}
}
Option 2:
let tooltipText;
for (var i = 0; i < this.data.getRowCount(); i++) {
// ...
let mouseDist = dist(posX, posY, mouseX, mouseY);
if (mouseDist < earthquakeMag * 5) {
// If we displayed the tooltip at this point then
// some of the circles would overlap it.
tooltipText = date_[i];
}
// ...
}
if (tooltipText) {
// measure the width of the tooltip
let w = textWidth(tooltipText);
// save the current fill/stroke/textAlign state
push();
// draw a lightgray rectangle with a dimgray border
fill('lightgray');
stroke('dimgray');
strokeWeight(1);
// draw this rectangle slightly below and to the
// right of the mouse
rect(mouseX + 10, mouseY + 10, w + 20, 24, 4);
textAlign(LEFT, TOP);
noStroke();
fill('black');
text(tooltipText, mouseX + 20, mouseY + 16);
// restore the previous fill/stroke/textAlign state
pop();
}
Related
I'm building a p5js donut chart, but I'm struggling to show the data labels in the middle. I think I have managed to get the boundaries right for it, but how would match the angle that I'm in? Or is there a way of matching just through the colours?
https://i.stack.imgur.com/enTBo.png
I have started by trying to match the boundaries of the chart to the pointer, which I managed to do using mouseX and mouseY. Any suggestions, please?
if(mouseX >= width / 2 - width * 0.2 && mouseY >= height / 2 - width * 0.2
&& mouseX <= width / 2 + width * 0.2 && mouseY <= height / 2 + width * 0.2)
{
//console.log("YAY!!! I'm inside the pie chart!!!");
}
else
{
textSize(14);
text('Hover over to see the labels', width / 2, height / 2);
}
};
[1]: https://i.stack.imgur.com/enTBo.png
While you could theoretically use the get() function to check the color of the pixel under the mouse cursor and correlate that with one of the entries in your dataset, I think you would be much better off doing the math to determine which segment the mouse is currently over. And conveniently p5.js provides helper functions that make it very easy.
In the example you showed you are only checking if the mouse cursor is in a rectangular region. But in reality you want to check if the mouse cursor is within a circle. To do this you can use the dist(x1, y1, x2, y2) function. Once you've established that the mouse cursor is over your pie chart, you'll want to determine which segment it is over. This can be done by finding the angle between a line draw from the center of the chart to the right (or whichever direction is where you started drawing the wedges), and a line drawn from the center of the chart to the mouse cursor. This can be accomplished using the angleBetween() function of p5.Vector.
Here's a working example:
const colors = ['red', 'green', 'blue'];
const thickness = 40;
let segments = {
foo: 34,
bar: 55,
baz: 89
};
let radius = 80, centerX, centerY;
function setup() {
createCanvas(windowWidth, windowHeight);
noFill();
strokeWeight(thickness);
strokeCap(SQUARE);
ellipseMode(RADIUS);
textAlign(CENTER, CENTER);
textSize(20);
centerX = width / 2;
centerY = height / 2;
}
function draw() {
background(200);
let keys = Object.keys(segments);
let total = keys.map(k => segments[k]).reduce((v, s) => v + s, 0);
let start = 0;
// Check the mouse distance and angle
let mouseDist = dist(centerX, centerY, mouseX, mouseY);
// Find the angle between a vector pointing to the right, and the vector
// pointing from the center of the window to the current mouse position.
let mouseAngle =
createVector(1, 0).angleBetween(
createVector(mouseX - centerX, mouseY - centerY)
);
// Counter clockwise angles will be negative 0 to PI, switch them to be from
// PI to TWO_PI
if (mouseAngle < 0) {
mouseAngle += TWO_PI;
}
for (let i = 0; i < keys.length; i++) {
stroke(colors[i]);
let angle = segments[keys[i]] / total * TWO_PI;
arc(centerX, centerY, radius, radius, start, start + angle);
// Check mouse pos
if (mouseDist > radius - thickness / 2 &&
mouseDist < radius + thickness / 2) {
if (mouseAngle > start && mouseAngle < start + angle) {
// If the mouse is the correct distance from the center to be hovering over
// our "donut" and the angle to the mouse cursor is in the range for the
// current slice, display the slice information
push();
noStroke();
fill(colors[i]);
text(`${keys[i]}: ${segments[keys[i]]}`, centerX, centerY);
pop();
}
}
start += angle;
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.js"></script>
I think I know the source of the problem was that #thenewbie experienced: it is the p5 library being used. I was using the p5.min.js and experiencing the same problem. Once I started using the full p5.js library, the issue was resolved and #Paul's script worked.
Here is a link I came across while researching this which put me onto the solution:
https://github.com/processing/p5.js/issues/3973
Thanks Paul for the clear explanations and code above.
Trying to add a Canvas layer on top of a Bing TileLayer using the extensions below. In addition to that, I would like to animate the canvas drawing using RequestAnimationFrame.
The issue is that it seems that when dragging, the point moves more than what it should (see video). However, at the end of the moving, it comes back to the right place.
I tried different methods, either inheriting CanvasLayer and overriding render() or setting a delegate, always same result.
Here is the relevant part of the code:
// layer creation (TypeScript)
this.mapCanvasLayer = new L.CanvasLayer();
this.mapCanvasLayer.delegate(this).addTo(map);
// L.canvasLayer()
// .delegate(this) // -- if we do not inherit from L.CanvasLayer we can setup a delegate to receive events from L.CanvasLayer
// .addTo(map);
// drawing and animation (TypeScript)
public onDrawLayer(info) {
// quick test, this just draws a red dot that blinks
this.info = info;
var data = [[0,0]];
var ctx = info.canvas.getContext('2d');
ctx.clearRect(0, 0, info.canvas.width, info.canvas.height);
ctx.fillStyle = "rgba(255,0,0," + this.i/10 + ")";
for (var i = 0; i < data.length; i++) {
var d = data[i];
if (info.bounds.contains([d[0], d[1]])) {
var dot = info.layer._map.latLngToContainerPoint([d[0], d[1]]);
ctx.beginPath();
ctx.arc(dot.x, dot.y, 3, 0, Math.PI * 2);
ctx.fill();
ctx.closePath();
}
}
};
public animate() {
// make the point blink
if (this.up) {
this.i++;
if (this.i >= 10) this.up = false;
}
else
{
this.i--;
if (this.i <= 1) this.up = true;
}
// animate:
this.mapCanvasLayer.drawLayer();
window.requestAnimationFrame(this.animate.bind(this));
}
Extensions tried:
gLayers.Leaflet // example is this one
Leaflet.CanvasLayer
Not tried yet:
Leaflet-Fullcanvas
I have the feeling it is due either to the dragging event or the coordinates translations (latLngToContainerPoint) not being properly handled, but have no idea why.
In the samples available for the Leaflet.CanvasLayer dragging an animated map seems to work fine.
Any idea what is wrong with this code? Thank you for your help!
Edit
See comments: replaced the call as below:
// var dot = info.layer._map.latLngToContainerPoint([d[0], d[1]]);
var dot = info.layer._map.latLngToLayerPoint([d[0], d[1]]);
Now, the point moves correctly. However, when I release the button, it now shifts to a position relative to the container, i.e. same distance of window borders, but wrong coordinates (wrong location on map).
If I now do this:
public onDrawLayer(info) {
this.info = info;
var ctx = info.canvas.getContext('2d');
ctx.clearRect(0, 0, info.canvas.width, info.canvas.height);
const fillStyleLayer = "rgba(255,0,0,1)"; // red: layer
const fillStyleContainer = "rgba(0,0,255,1)"; // blue: container
var layerPoint = info.layer._map.latLngToLayerPoint([0,0]);
var containerPoint = info.layer._map.latLngToContainerPoint([0,0]);
// this.dot = (this.dragging)
// ? info.layer._map.latLngToLayerPoint([0,0]);
// : info.layer._map.latLngToContainerPoint([0,0]);
ctx.fillStyle = fillStyleLayer;
ctx.beginPath();
ctx.arc(layerPoint.x, layerPoint.y, 3, 0, Math.PI * 2);
ctx.fill();
ctx.closePath();
ctx.fillStyle = fillStyleContainer;
ctx.beginPath();
ctx.arc(containerPoint.x, containerPoint.y, 3, 0, Math.PI * 2);
ctx.fill();
ctx.closePath();
};
public animate() {
this.mapCanvasLayer.drawLayer();
window.requestAnimationFrame(this.animate.bind(this));
}
Layer point (red) moves correctly when panning but then shifts to wrong location. Container point (blue) moves too much when panning but then shifts back to correct location... :(
A bit late to the game but switching to L.canvasOverlay() worked for me. Here's a great example of the library in action
http://bl.ocks.org/Sumbera/11114288
Aim of my code:
Draw a small rectangle on a HTML canvas whenever a user clicks the canvas. The rectangle should have a small number representing the number of rectangles made by the user.
The user should be able to connect any two rectangles using a straight line. (Preferably by just pressing left mouse button, and taking the mouse from first rectangle to second rectangle)
Approach and my attempt
As you can see in this jsFiddle , I have been able to achieve the first part of above very well. On clicking on the canvas, a rectangle with a number inside of it is made. But I am really clueless about the second part.
How do I make the user connect any two made rectangles? I want the connection to be made only if a rectangle is there ( So I would need to store coordinates of every rectangle that has been made, that's okay as I can use an array for that ).
Basically, I just want to check if the mousedown was at one place and mouseup at the other.
How do I get these two different coordinates ( one of mousedown and other of mouseup ) , and draw a line between them?
I have given the Fiddle above but still here's my jquery:
$(function () {
var x, y;
var globalCounter = 0;
$('#mycanvas').on("click", function (event) {
x = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
x -= mycanvas.offsetLeft;
y = event.clientY + document.body.scrollTop + document.documentElement.scrollTop;
y -= mycanvas.offsetLeft;
// alert("x:"+x+"y: "+y);
drawRectangle(x, y);
});
function drawRectangle(x, y) {
var acanvas = document.getElementById("mycanvas");
var context = acanvas.getContext("2d");
context.strokeRect(x, y, 25, 25);
globalCounter++;
writeNo(x, y, globalCounter);
}
function writeNo(x, y, n) {
var acanvas = document.getElementById("mycanvas");
var context = acanvas.getContext("2d");
context.font = "bold 14px sans-serif";
context.fillText(n, x + 8, y + 12.5);
}
});
The main question is therefore: connecting the two made rectangles by mousedrag
How do I achieve this?
Thank You.
How about this: http://jsfiddle.net/4jqptynt/4/
Ok, first I did a little refactoring for your code to make things easier. Just stuff like putting the code that gets the canvas coordinates into it's own function, and caching some variables (like the canvas context) in the outer function's scope. Oh, and defining your rectangle dimensions as constants because we'll be using the same numbers in a couple of different places.
As you said, the first thing we need is to keep track of the existing rectangles using an array rects (easy enough to do within drawRectangle). Then we need a function to check if a particular pair of coordinates are within some rectangle:
function inRectangle(x, y) {
for (var i = 0, l = rects.length; i < l; i++) {
if ((x - rects[i].x) <= RECT_X && (y - rects[i].y) <= RECT_Y &&
(x - rects[i].x) >= 0 && (y - rects[i].y) >= 0) {
return i;
}
}
}
where RECT_X & RECT_Y define the sides of the rectangle. If the coordinates do exist within some rectangle then this will return the index of that rectangle within the rects array.
Then it's a case of checking whether or not a mousedown occurred within a rectangle, noting that inRectangle will only return a number if the mousedown event was within a rectangle:
$acanvas.on("mousedown", function (event) {
var coords = getCoords(event),
rect = inRectangle(coords.x, coords.y);
if (typeof rect === "number") {
dragStart = rect + 1;
} else {
drawRectangle(coords.x, coords.y);
}
});
if so, make a note of which rectangle using dragStart, if not draw a rectangle as before.
Then to complete the drag, we need to attach a handler to mouseup:
$acanvas.on("mouseup", function (event) {
if (!dragStart) { return; }
var coords = getCoords(event),
rect = inRectangle(coords.x, coords.y);
if (typeof rect === "number") {
drawConnection(dragStart - 1, rect);
}
dragStart = 0;
});
If no drag was started, then it does nothing. If it's coordinates aren't within a rectangle, then it does nothing but reset dragStart. If however, it is within a rectangle, then it draws a connecting line:
function drawConnection(rect1, rect2) {
context.strokeStyle = "black";
context.lineWidth = 1;
context.beginPath();
context.moveTo(rects[rect1].x + RECT_X/2, rects[rect1].y + RECT_Y/2);
context.lineTo(rects[rect2].x + RECT_X/2, rects[rect2].y + RECT_Y/2);
context.stroke();
context.closePath();
}
In the following fiddle, you can click and drag around the image, and it will not be able to exit the blue border. By clicking the red and green rectangles, you can rotate the image. However when you click and drag a rotated object, the image does not follow the mouse. I would like the image to follow the mouse even if it is rotated.
http://jsfiddle.net/n3Sn5/
I think the issue occurs within my move function
move = function (dx, dy)
{
nowX = Math.min(boundary.attr("x")+boundary.attr("width")-this.attr("width"), this.ox + dx);
nowY = Math.min(boundary.attr("y")+boundary.attr("height")-this.attr("height"), this.oy + dy);
nowX = Math.max(boundary.attr("x"), nowX);
nowY = Math.max(boundary.attr("y"), nowY);
this.attr({x: nowX, y: nowY });
}
One thing to notice is that when you click and drag a rotated object, after you release your mouse click, if you rotate the image, it snaps to where your mouse was when you released the mouse click, even obeying the boundary.
I was able to get the rotated image to drag with the mouse previously, but by adding the boundary rectangle, i had to use a more complex approach.
If anyone has an idea of what I need to change, I would be very grateful!
Thanks!
The required output can be achieved in a bit different way. Please check the fiddle at http://jsfiddle.net/6BbRL/. I have trimmed to code to keep the basic parts for demo.
var paper = Raphael(0, 0, 475, 475),
boxX = 100,
boxY = 100,
boxWidth = 300,
boxHeight = 200,
// EDITED
imgWidth = 50,
imgHeight = 50,
box = paper.rect(boxX, boxY, boxWidth, boxHeight).attr({fill:"#ffffff"}),
// EDITED
html5 = paper.image("http://www.w3.org/html/logo/downloads/HTML5_Badge_512.png",boxX+boxWidth-imgWidth,boxY+boxHeight-imgHeight,imgWidth,imgHeight)
.attr({cursor: "move"}),
elementCounterClockwise = paper.rect(180, 0, 50, 50).attr({fill:"#ff5555", cursor:"pointer"}),
elementClockwise = paper.rect(250, 0, 50, 50).attr({ fill: "#55ff55", cursor: "pointer" }),
boundary = paper.rect(50,50,400,300).attr({stroke: '#3333FF'}),
transform,
// EDITED
xBound = {min: 50 + imgWidth/2, max: 450 - imgWidth/2},
yBound = {min: 50 + imgHeight/2, max: 350 - imgHeight/2};
start = function (x, y) {
// Find min and max values of dx and dy for "html5" element and store them for validating dx and dy in move()
// This is required to impose a rectagular bound on drag movement of "html5" element.
transform = html5.transform();
}
move = function (dx, dy, x, y) {
// To restrict movement of the dragged element, Validate dx and dy before applying below.
// Here, dx and dy are shifts along x and y axes, with respect to drag start position.
// EDITED
var deltaX = x > xBound.max && xBound.max - x || x < xBound.min && xBound.min - x || 0;
deltaY = y > yBound.max && yBound.max - y || y < yBound.min && yBound.min - y || 0;
this.attr({transform: transform + 'T'+ [dx + deltaX, dy + deltaY]});
}
up = function () {
};
html5.drag(move, start, up);
elementClockwise.click(function() {
html5.animate({transform: '...r90'}, 100);
})
elementCounterClockwise.click(function() {
html5.animate({transform: '...r-90'}, 100);
})
Use of '...' to append a transformation to the pre-existing transformation state (Raphael API) is important for the rotational issue. While, for translating the element on drag requires absolute translation, which neglects the rotational state of the element while translating the element.
//EDIT NOTE
Drag bounding is worked on and updated. However, there remains an issue with incorporating the difference between mouse position and image center.
I can help you with your rotation and drag problem, you need to store the rotation and apply it after you have moved the object.
elementClockwise.node.onclick = function()
{
html5.animate({'transform': html5.transform() +'r90'}, 100, onAnimComplete);
}
elementCounterClockwise.node.onclick = function()
{
html5.animate({'transform': html5.transform() +'r-90'}, 100, onAnimComplete);
}
function onAnimComplete(){
default_transform = html5.transform();
}
At present I can't get the boundary to work, but will have a try later.
http://jsfiddle.net/n3Sn5/2/
I bumped into the following problem, hope someone will know how to help me:
I work with the JavaScript library Raphael. Now, what I want to do is, when I have many Raphael SVG elements, to simply select more elements with "rectangle selection", i.e. by dragging the mouse starting from the graph's background to create a selection rectangle (I hope I was clear enough), and move the elements which are in this rectangle.
For now, I've found something like this (someone posted it from a previous question of mine):
var paper = Raphael(0, 0, '100%', '100%');
var circle = paper.circle(75, 75, 50);
var rect = paper.rect(150, 150, 50, 50);
var set = paper.set();
set.push(circle, rect);
set.attr({
fill: 'red',
stroke: 0
});
var ox = 0;
var oy = 0;
var dragging = false;
set.mousedown(function(event) {
ox = event.screenX;
oy = event.screenY;
set.attr({
opacity: .5
});
dragging = true;
});
set.mousemove(function(event) {
if (dragging) {
set.translate(event.screenX - ox, event.screenY - oy);
ox = event.screenX;
oy = event.screenY;
}
});
set.mouseup(function(event) {
dragging = false;
set.attr({
opacity: 1
});
});
This code can be executed on jsfiddle. But, as you can see, this selects ALL the elements, by simply adding them to a Raphael set.
Now, I think that my problem will be solved by:
Making a rectangle selection
Adding the nodes which are in the rectangle to a Raphael set
Move only the selected items (i.e. move only the items which are in the Raphael set with set.mousemove)
My problem for now would be the first two issues.
Any ideas how to do this?
Thank you in advance!
Fun problem. You can do this by placing a rectangular "mat" the size of the canvas behind all of your other objects and attaching a drag event to it for selecting other elements. (Note this solution uses the newer version of Raphael, 2.1.0:
var paper = Raphael(0, 0, '100%', '100%');
//make an object in the background on which to attach drag events
var mat = paper.rect(0, 0, paper.width, paper.height).attr("fill", "#FFF");
var circle = paper.circle(75, 75, 50);
var rect = paper.rect(150, 150, 50, 50);
var set = paper.set();
set.push(circle, rect);
set.attr({
fill: 'red',
stroke: 0
});
//the box we're going to draw to track the selection
var box;
//set that will receive the selected items
var selections = paper.set();
Now, we add a drag event -- similar to the mouseover events but with three functions (see documentation), and draw a box to track the selection space:
//DRAG FUNCTIONS
//when mouse goes down over background, start drawing selection box
function dragstart (x, y, event) {
box = paper.rect(x, y, 0, 0).attr("stroke", "#9999FF");
}
// When mouse moves during drag, adjust box.
// If the drag is to the left or above original point,
// you have to translate the whole box and invert the dx
// or dy values since .rect() doesn't take negative width or height
function dragmove (dx, dy, x, y, event) {
var xoffset = 0,
yoffset = 0;
if (dx < 0) {
xoffset = dx;
dx = -1 * dx;
}
if (dy < 0) {
yoffset = dy;
dy = -1 * dy;
}
box.transform("T" + xoffset + "," + yoffset);
box.attr("width", dx);
box.attr("height", dy);
}
function dragend (event) {
//get the bounds of the selections
var bounds = box.getBBox();
box.remove();
reset();
console.log(bounds);
for (var c in set.items) {
// Here, we want to get the x,y vales of each object
// regardless of what sort of shape it is.
// But rect uses rx and ry, circle uses cx and cy, etc
// So we'll see if the bounding boxes intercept instead
var mybounds = set[c].getBBox();
//do bounding boxes overlap?
//is one of this object's x extremes between the selection's xe xtremes?
if (mybounds.x >= bounds.x && mybounds.x <= bounds.x2 || mybounds.x2 >= bounds.x && mybounds.x2 <= bounds.x2) {
//same for y
if (mybounds.y >= bounds.y && mybounds.y <= bounds.y2 || mybounds.y2 >= bounds.y && mybounds.y2 <= bounds.y2) {
selections.push(set[c]);
}
}
selections.attr("opacity", 0.5);
}
}
function reset () {
//empty selections and reset opacity;
selections = paper.set();
set.attr("opacity", 1);
}
mat.drag(dragmove, dragstart, dragend);
mat.click(function(e) {
reset();
});
Just like that, you have a new set (selections) that contains every object that was selected by the mouse drag. You can then apply your mouseover events from the original to that set.
Note that this will select circle objects if you nick the corner of their bounding box with your selection box, even if it doesn't overlap with the area of the circle. You could make a special case for circles if this is a problem.
jsFiddle