Raphaël-ZPD click to zoom in/out - svg

Does anyone know how to trigger zoom in/out with Raphaël-ZPD by clicking on a button?

If take a look to the code of Raphael-ZPD you would see that all the zooming and panning are made by using svg matrices. In the particular case of zooming, it first draw a reference point where the mouse is, and calculates the zoom direction relative to that point, using the delta of the mouse wheel to apply the amount of zoom.
Now, if you want to use a button, you would have to decide the delta on your code, and you could maybe use the center of the screen as the relative point to calculate the direction of zooming.
For starters you could just use the svg scale property, something like this could be ok if you are using Raphael ZPD:
paper = Raphael('container',600, 600);
paper.ZPD({ zoom: true, pan: true, drag: false });
scale = 1;
var zoomin = document.getElementById('in')
var zoomout = document.getElementById('out')
zoomin.onclick = function(){
scale *= 1.2
paper.canvas.setAttribute("transform", "scale("+scale+")")
}
zoomout.onclick = function(){
scale *= 0.8
paper.canvas.setAttribute("transform", "scale("+scale+")")
}
Here is Fiddle with a working example
I reccomend to add some translate calculating the center of the screen, so the zoom doesn't go to corner. But think all this could point you in the right direction.

Related

SVG getScreenCTM() on FireFox

GOAL
I am trying to build a signature pad in SVG. You can view
Sample project here.
Description
So far, it works in Chrome, Edge and Opera as desired but, in Firefox, getScreenCTM() doesn't account for the scale.
Research
I went through a bunch of documentation from bugzilla and a couple of posts here in SO such as SVG: GetScreenCTM() for nested SVG is different in Firefox but, still couldn't figure out how to fix my issue.
Problem
I've added a browse check to handle only Firefox (since all other browsers provides the desired result) which allows me to add extra code to fix the problem but, thus far, had no success. (SPSignature:416)
Reproduce the error
To reproduce the error, open the sample link in FireFox than, resize the box so it is at least 20% smaller as in fullscreen. You will see the mouseX/Y position change as scale changes.
I've tried to get the matrix transform from the group tag but, it returns similar result from the SVG. How do I calculate the CTM, so its result is similar/equal to Chrome?
Code
Source code here SPSignature:416
_getCursorPoint(event)
{
const svg = document.querySelector('.spsignature svg');
let pt = svg.createSVGPoint();
pt.x = event.clientX;
pt.y = event.clientY;
// firefox workaround:
if (this._checkBrowser() === 'Firefox') {
const matrix = this._decomposeMatrix(svg.getScreenCTM());
let cood = {
x: event.layerX,
y: event.layerY
};
if (matrix.scaleX < 0.9 || matrix.scaleY < 0.9) {
console.log('%c #todo: fix mouse position for FF on getScreenCTM().', 'background:#c00;color:#fff;padding:3px;');
}
return cood;
}
return pt.matrixTransform(svg.getScreenCTM().inverse());
}
To "fix" the mouse XY problem on FF, I've used the root scale value to allow for the calculation of the cursor XY position.
The scale can be retrieved from the matrix based on https://gist.github.com/2052247.
Also, in FF, I've used the event.layerX and event.layerY instead of event.clientX and event.clientY for the initial mouse coordinate values.
So, instead of using matrixTransform(svg.getScreenCTM().inverse()) to return the calculated XY relative to the SVG, I've used the scale and layerXY to calculate the right XY position.

PyQtGraph How to zoom in using a button

I'm using PyQTGraph along with PyQT5, and I've added a GraphicsLayoutWidget, with mouse events to add markers.
canvas_ruler = pg.GraphicsLayoutWidget()
formLayout.addWidget(self.canvas_ruler)
plot_ruler = canvas_ruler.addPlot(name="Ruler")
plot_ruler.hideAxis('left')
canvas_ruler.scene().sigMouseMoved.connect(self.mouseMoved)
canvas_ruler.scene().sigMouseClicked.connect(self.mouseClicked)
plot_ruler.setMouseEnabled(x=True, y=False)
What I would like to add, are 2 buttons, to zoom in and zoom out, currently the mouse wheel event, does zooming in/out. But I want to disable that and add 2 buttons for it. I probably should:
setMouseEnabled(x=False, y=False)
But I don't know how to trigger the zoom using code, I couldn't find it's APIs, like sigMouseMoved.connect.
The nearest native thing pyqtgraph does that approaches this (AFAIK) is the zoom with the mouse wheel feature. The MouseWheelEvent() function in ViewBox achives this with a call to the ViewBox function scaleBy().
You can see the source for all inputs and more detail but the main one is s (scale) and center (center around which to zoom). Leave center as None to zoom around the center of the plot.
Pass a tuple of x and y zoom scales to the function to zoom, < 1 for in and > 1 for out. e.g.:
def handle_plot_zoom_buttons(self, in_or_out):
"""
see ViewBox.scaleBy()
pyqtgraph wheel zoom is s = ~0.75
"""
s = 0.9
zoom = (s, s) if in_or_out == "in" else (1/s, 1/s)
self.plot.vb.scaleBy(zoom)

AndroidPlot - Labels and text

I am a non-developer product manager for an application built in both Android and iOS. We have a bar graph in iOS that provides text for the content of the graph. It displays Totals for each bar, and percentages for each segment of each bar.
In Android, using AndroidPlot (so I understand) we just display the bars with different color segments and no percent totals or totals. I am told by the developer that we can't show more.
I would display the images here, but stackoverflow tells me I don't have enough reputation points to do this. I have created a link to my dropbox with the images https://www.dropbox.com/sh/2uocm5bn79rerbe/AAB7s9QEEYIRIgXhKbUAaOyDa
Is it possible to use AndroidPlot to emulate this iOS chart or at least represent to same information to the end user?
Your developer is more or less correct but you have options. Androidplot's BarRenderer by default provides only an optional label at the top of each bar, which in your iOS images is occupied by the "available", "new", "used" and "rent" label. That label appears to be unused in your Android screenshot so one option would be to utilize those labels do display your totals.
As far as exactly matching the iOS implementation with Androidplot, the missing piece is the ability to add additional labels horizontally and vertically along the side of each bar. You can extend BarRenderer to do this by overriding it's onRender(...) method. Here's a link for your developer that shows where in the code he'll want to modify onRender(...).
I'd suggest these modifications to add the vertical labels:
Invoke Canvas.save(Canvas.ALL_SAVE_FLAG) to store the default orientation of the Canvas.
Use Canvas.translate(leftX, bottom) to center on the bottom left point of the bar
Rotate the Canvas 90 degrees using Canvas.rotate(90) to enable vertical text drawing
Draw whatever text is needed along the side of the plot; 0,0 now corresponds to the bottom left corner of the bar so start there when invoking canvas.drawText(x,y).
Invoke Canvas.restore() to restore the canvas' original orientation.
After implementing the above, adding horizontal "%" labels should be self evident but if you run into trouble feel free to ask more questions along the way.
UPDATE:
Here's a very basic implementation of the above. First the drawVerticalText method:
/**
*
* #param canvas
* #param paint paint used to draw the text
* #param text the text to be drawn
* #param x x-coord of where the text should be drawn
* #param y y-coord of where the text should be drawn
*/
protected void drawVerticalText(Canvas canvas, Paint paint, String text, float x, float y) {
// record the state of the canvas before the draw:
canvas.save(Canvas.ALL_SAVE_FLAG);
// center the canvas on our drawing coords:
canvas.translate(x, y);
// rotate into the desired "vertical" orientation:
canvas.rotate(-90);
// draw the text; note that we are drawing at 0, 0 and *not* x, y.
canvas.drawText(text, 0, 0, paint);
// restore the canvas state:
canvas.restore();
}
All that's left is to invoke this method where necessary. In your case it should be done once per BarGroup and should maintain a consistent position on the y axis. I added the following code to the STACKED case in BarRenderer.onRender(...), immediately above the break:
// needed some paint to draw with so I'll just create it here for now:
Paint paint = new Paint();
paint.setColor(Color.WHITE);
paint.setTextSize(PixelUtils.spToPix(20));
drawVerticalText(
canvas,
paint,
"test",
barGroup.leftX,
basePositionY - PixelUtils.dpToPix(50)); // offset so the text doesnt intersect with the origin
Here's a screenshot of the result...sorry it's so huge:
Personally, I don't care for the fixed y-position of these vertical labels and would prefer them to float along the upper part of the bars. To accomplish this I modify my invocation of drawVerticalText(...) to look like this:
// needed some paint to draw with so I'll just create it here for now:
Paint paint = new Paint();
paint.setColor(Color.WHITE);
paint.setTextSize(PixelUtils.spToPix(20));
// right-justify the text so it doesnt extend beyond the top of the bar
paint.setTextAlign(Paint.Align.RIGHT);
drawVerticalText(
canvas,
paint,
"test",
barGroup.leftX,
bottom);
Which produces this result:

D3 - Positioning tooltip on SVG element not working

I have a webpage with an SVG. On some of its shapes I need to display a tooltip. However, I can't get the tooltip to appear where it should, just some pixels away from the shape itself.
It appears way on the right hand side of the screen, maybe some 300px away.
The code I am using to get the coordinates is as follows:
d3.select("body")
.select("svg")
.select("g")
.selectAll("circle")
.on("mouseover", function(){return tooltip.style("visibility", "visible");})
.on("mousemove", function(){
var svgPos = $('svg').offset(),
/*** Tooltip ***/
//This should be the correct one, but is displaying at all working at all.
/*x = svgPos.left + d3.event.target.cx.animVal.value,
y = svgPos.top + d3.event.target.cy.animVal.value;*/
//This displays a tool tip but way much to the left of the screen.
x = svgPos.left + d3.event.target.cx.animVal.value,
y = svgPos.top + d3.event.target.cy.animVal.value;
Tooltip
window.alert("svgPos: "+svgPos+" top: "+y+"px left: "+x+"px "+d3.event.target.cx.animVal.value);
return tooltip.style("top", x+"px").style("left",y+"px");
})
.on("mouseout", function(){return tooltip.style("visibility", "hidden");});
I got to this code following this SO post.
I have changed $(ev.target).attr(cx) as it is not returning a value on my machine; d3.event.target.cx is, even though it seems it is not affecting the end result anyway.
What am I doing wrong? Could somebody help me please? Thank you very much in advance for your time.
If your tooltip is an HTML element, then you want to position it relative to the page as a whole, not the internal SVG coordinates, so accessing the cx/cy value is just complicating things. I can't say for sure without looking at your code, but if you have any transforms on your <svg> or <g> elements, then that could be what's throwing you off.
However, there is a much easier solution. Just access the mouse event's default .pageX and .pageY properties, which give the position of the mouse relative to the HTML body, and use these coordinates to position your tooltip div.
Example here: http://fiddle.jshell.net/tPv46/1/
Key code:
.on("mousemove", function () {
//console.log(d3.event);
return tooltip
.style("top", (d3.event.pageY + 16) + "px")
.style("left", (d3.event.pageX + 16) + "px");
})
Even with rotational transforms on the SVG circles, the mouse knows where it is on the page and the tooltip is positioned accordingly.
There are other ways to do this, including getting a tooltip to show up in a fixed location relative to the circle instead of following the mouse around, but I just checked the examples I was working on and realized they aren't cross-browser compatible, so I'll have to standardize them and get back to you. In the meantime, I hope this gets you back on track with your project.
Edit 1
For comparison, here is the same example implemented with both an HTML tooltip (a <div> element) and an SVG tooltip (a <g> element).
http://fiddle.jshell.net/tPv46/4/
The default mouse event coordinates may be great for positioning HTML elements that are direct children of <body>, but they are less useful for positioning SVG elements. The d3.mouse() function calculates the mouse coordinates of the current event relative to a specified SVG element's coordinate system, after all transformations have been applied. It can therefore be used to get the mouse coordinates in the form we need to position an SVG tooltip.
Key code:
.on("mousemove", function () {
var mouseCoords = d3.mouse(
SVGtooltip[0][0].parentNode);
//the d3.mouse() function calculates the mouse
//position relative to an SVG Element, in that
//element's coordinate system
//(after transform or viewBox attributes).
//Because we're using the coordinates to position
//the SVG tooltip, we want the coordinates to be
//with respect to that element's parent.
//SVGtooltip[0][0] accesses the (first and only)
//selected element from the saved d3 selection object.
SVGtooltip
.attr("transform", "translate(" + (mouseCoords[0]-30)
+ "," + (mouseCoords[1]-30) + ")");
HTMLtooltip
.style("top", (d3.event.pageY + 16) + "px")
.style("left", (d3.event.pageX + 16) + "px");
})
Note that it works even though I've scaled the SVG with a viewBox attribute and put the tooltip inside a <g> with a transform attribute.
Tested and works in Chrome, Firefox, and Opera (reasonably recent versions) -- although the text in the SVG tooltip might extend past its rectangle depending on your font settings. One reason to use an HTML tooltip! Another reason is that it doesn't get cut off by the edge of the SVG.
Leave a comment if you have any bugs in Safari or IE9/10/11. (IE8 and under are out of luck, since they don't do SVG).
Edit 2
So what about your original idea, to position the tooltip on the circle itself? There are definite benefits to being able to position the tip exactly: better layout control, and the text doesn't wiggle around with the mouse. And most importantly, you can just position it once, on the mouseover event, instead of reacting to every mousemove event.
But to do this, you can no longer just use the mouse position to figure out where to put the tooltip -- you need to figure out the position of the element, which means you have to deal with transformations. The SVG spec introduces a set of interfaces for locating SVG elements relative to other parts of the DOM.
For converting between two SVG transformation systems you use SVGElement.getTransformToElement(SVGElement); for converting between an SVG coordinate system and the screen, you use SVGElement.getScreenCTM(). The result are transformation matrices from which you can
extract the net horizontal and vertical translation.
The key code for the SVG tooltip is
var tooltipParent = SVGtooltip[0][0].parentNode;
var matrix =
this.getTransformToElement(tooltipParent)
.translate(+this.getAttribute("cx"),
+this.getAttribute("cy"));
SVGtooltip
.attr("transform", "translate(" + (matrix.e)
+ "," + (matrix.f - 30) + ")");
The key code for the HTML tooltip is
var matrix = this.getScreenCTM()
.translate(+this.getAttribute("cx"),
+this.getAttribute("cy"));
absoluteHTMLtooltip
.style("left",
(window.pageXOffset + matrix.e) + "px")
.style("top",
(window.pageYOffset + matrix.f + 30) + "px");
Live example: http://fiddle.jshell.net/tPv46/89/
Again, I'd appreciate a confirmation comment from anyone who can test this in Safari or IE -- or any mobile browser. I'm pretty sure I've used standard API for everything, but just because the API is standard doesn't mean it's universally implemented!

Efficient way of scaling hundreds of QGraphicItems

I have a zoomable QGraphicsView with scene that contains hundreds (and even thousands) of datapoints. The points are represented by QGraphicsEllipseItems and collected into a QGraphicsItemGroup. When the view is zoomed in to I want datapoints to stay at a constant size (i.e., the distances between neigbouring points increase but the sizes stay the same). Right now I achieve this by running this code every time the user zooms in:
#get all the QGraphicsEllipseItems that make up the QGraphicsItemGroup
children = graphics_item_group.childItems()
for c in children:
#base_size_x and base_size_y are the sizes of the
#untrasformed ellipse (on the scene) when zoom factor is 1
#New width and height are obtained from the original sizes and
#the new zoom factors (h_scale, v_scale)
new_width = base_size_x/h_scale
new_height = base_size_y/v_scale
#The top-left corner of the new rectangle for the item has to be recalculated
#when scaling in order to keep the center at a constant position
#For this, the center of the item has to be stored first
old_center_x = c.rect().center().x()
old_center_y = c.rect().center().y()
#New coordinates of the rectangle top left point are calculated
new_topleft_x = old_center_x - new_width/2.
new_topleft_y = old_center_y - new_height/2.
#Finally a new rectangle is set for the ellipse
c.setRect(new_topleft_x, new_topleft_y, new_width, new_height)
This code works. The problem is that it is quite slow (without the compensatory scaling zooming in/out works very smoothly). I tried turning off antialiasing for the view but it makes things look pretty ugly. Is there anything else that I can do to make the processing/redrawing faster?
In the constructor of the QGraphicsItem:
setFlag(ItemIgnoresTransformations);
When you zoom, the item will remain the same size, and you won't have to manually scale it.

Resources