D3.js zoom and touch device smoothness - javascript

I am using d3.behaviour.zoom on my graph and all is well on the desktop. However, when i do it on an ipad the zooming is very choppy. Is there a way I can smooth it out? It seems like i need to cancel translate calls when zooming.
I have tried a couple of things but none to any great success.
an example is in the zoom handler i get the 2 touch point x and y then get the distance between then and depending if it shrinks or grows from the last stored distance i added or subtracted 0.05 to the d3.event.scale which is used in the transform.
Is this right or am i way off and need to take something else into consideration
Help much appreciated
Cheers
Mark
EDIT:
Thought i would include some code here seeing as i cannot use another service via works network
attaching the zoom handler
RadarDraw.ZoomListener = d3.behavior.zoom().scaleExtent([1, 5]).on("zoom", zoom);
// Create the SVG element, transforming the coordinates so (0,0) is at the centre
svg = d3.select("#radarContainer").append("svg")
.attr("viewBox", "0 0 " + _config.Width + " " + _config.Height + "")
.attr("id", "chartsvg")
.attr("width", _config.Width)
.attr("height", _config.Height)
//.call(zoom)
.append("g")
.attr("id", "svgGElm")
.attr("transform", "translate(" + _config.Width / 2 + "," + ((_config.Height - _config.Voffset) / 2 + _config.Voffset) + ")")
.call(_config.RadarType != "dash" ? RadarDraw.ZoomListener : function () { });
This is the zoom handler
function zoom(){
if (d3.event.scale <= 1 || d3.event.scale >= 5) {
if (!tools.IsArcFocused) {
svg.attr("transform", "translate("
+ (d3.event.translate[0] + (_config.Width / 2)) + "," + (d3.event.translate[1] + (_config.Height / 2))
+ ")scale(" + d3.event.scale + ")");
}
//handle the zoom of arc that occurs from dblclick. this moves to arc and centers it and zooms in into it.
if (tools.IsArcFocused) {
svg.attr("transform", "translate(" + _config.Width / 2 + "," + _config.Height / 2 + ")scale(" + 5 + ")translate("
+ -RadarDraw.transPosX + "," + -RadarDraw.transPosY + ")");
}
debugInfoBar(" scale: " + d3.event.scale
+ " last distance: " + lastDistance
+ " current distance: " + currentDistance);
//detect mouse wheel
if(d3.event.sourceEvent != null)
{
if (d3.event.sourceEvent.type=='mousewheel' || d3.event.sourceEvent.type=='wheel' || d3.event.sourceEvent.type=='DOMMouseScroll')
{
//if we are zoom in on an arc only listen to zoom out command to exit the zoom
//if wheeldelta is forward and in focus mode then ignore it
if (d3.event.sourceEvent.wheelDelta > 0 && tools.IsArcFocused)
{
//make sure dots stay smallest zoomed in size
scaleDots(5);
return;
}
if (d3.event.sourceEvent.wheelDelta < 0) {
if ((tools.IsArcFocused && d3.event.scale >= 5) || (tools.IsArcFocused && d3.event.scale <= 1)) {
tools.ExitZoom(false);
}
//reset to 0,0 scale 1 as we want to zoom out fully
if (d3.event.scale <= 1 && tools.IsArcFocused)
{
svg.attr("transform", "translate(" + _config.Width / 2 + "," + _config.Height / 2 + ")scale(" + 1 + ")");
}
}
}
if (d3.event.sourceEvent.type == "mousemove" && tools.IsArcFocused)
{
d3.event.scale = 5;
scaleDots(5);
return;
}
if (d3.event.sourceEvent.type == "touchmove")
{
//need to handle zoom out via touch (like whats done with mouse wheel)
return;
}
//deal with stopping double tap and double click events via the zoom
if (d3.event.sourceEvent.type == 'dblclick' || d3.event.sourceEvent.type == 'touchstart')
{
if (typeof (d3.event.preventDefault) == "function") {
d3.event.preventDefault();
d3.event.stopPropagation();
}
return;
}
}
scaleDots(d3.event.scale);
if(tools.IsArcFocused)
previousZoomLevel = 5;
else
previousZoomLevel = 1;
return;
} else {
if (tools.IsArcFocused) {
svg.attr("transform", "translate(" + _config.Width / 2 + "," + _config.Height / 2 + ")scale(" + 5 + ")translate("
+ -RadarDraw.transPosX + "," + -RadarDraw.transPosY + ")");
}
inTouchZoom = false;
//detect forward scroll when zoomed in so it exits zoom
if(d3.event.sourceEvent != null)
{
if (d3.event.sourceEvent.type == 'mousewheel' || d3.event.sourceEvent.type == 'wheel' || d3.event.sourceEvent.type == 'DOMMouseScroll') {
if (d3.event.sourceEvent.wheelDelta == 120)
{
d3.event.scale += 0.05;
if (d3.event.scale > 4.9)
d3.event.scale = 4.9;
}
else if (d3.event.sourceEvent.wheelDelta -= 120)
{
d3.event.scale -= 0.05;
if (d3.event.scale < 1)
d3.event.scale = 1;
}
//previousZoomLevel -gets confused sometimes and is 1 when it should be >=5
if (tools.IsArcFocused && previousZoomLevel >= 5) {
tools.ExitZoom(false);
previousZoomLevel = d3.event.scale;
}
}
if (d3.event.sourceEvent.type == "touchmove") {
//if only one touch point then do translation for pan otherwise leave as is
if (d3.event.sourceEvent.touches.length > 1) {
inTouchZoom = true;
//dont update translate use what was take before
d3.event.translate = touchZoomTranslate;
//we have atleast 2 points so use the first 2
var currentDistance = PointDistance(d3.event.sourceEvent.touches[0].pageX, d3.event.sourceEvent.touches[0].pageY, d3.event.sourceEvent.touches[1].pageX, d3.event.sourceEvent.touches[1].pageY);
debugInfoBar(" scale: " + d3.event.scale
+ " tp1 X: " + d3.event.sourceEvent.touches[0].pageX + " tp1Y: " + d3.event.sourceEvent.touches[0].pageY
+ " tp2 X: " + d3.event.sourceEvent.touches[1].pageX + " tp2Y: " + d3.event.sourceEvent.touches[1].pageY
+ " last distance: " + lastDistance
+ " current distance: " + currentDistance);
if (currentDistance > lastDistance) {
d3.event.scale += 0.05;
if(d3.event.scale > 4.9)
d3.event.scale = 4.9;
lastDistance = currentDistance;
}
else {
d3.event.scale -= 0.05;
if (d3.event.scale < 1)
d3.event.scale = 1;
lastDistance = currentDistance;
}
svg.attr("transform", "translate(" + _config.Width / 2 + "," + _config.Height / 2 + ")scale(" + d3.event.scale + ")");
return;
}
}
if (d3.event.sourceEvent.type == "touchend") {
//wipe last distance and we have finished touch
lastDistance = 0;
}
//deal with stopping double tap and double click events via the zoom
if (d3.event.sourceEvent.type == 'dblclick' || (d3.event.sourceEvent.type == 'touchstart' && !inTouchZoom))
{
if (typeof (d3.event.preventDefault) == "function") {
d3.event.preventDefault();
d3.event.stopPropagation();
}
return;
}
}
if (!tools.IsArcFocused) {
svg.attr("transform", "translate("
+ (d3.event.translate[0] + (_config.Width / 2)) + "," + (d3.event.translate[1] + (_config.Height / 2))
+ ")scale(" + d3.event.scale + ")");
}
if (d3.event.scale > 1)
previousZoom = true;
//if we are zoomed in and our previous zoom level is 5 or above then exit zoom (we are either scrolling out or pinching out of zoom)
if (tools.IsArcFocused && previousZoomLevel >= 5) {
tools.ExitZoom(false);
}
scaleDots(d3.event.scale);
}
if (d3.event.scale > 4.9)
d3.event.scale = 4.9;
if(!inTouchZoom)
touchZoomTranslate = d3.event.translate;
}

I corrected this issue by rewriting the zoom handler so it had specific touch input update, where I deal with zooming and not relying on d3. I also listened for a touchend event in zoom but it did not fire I had to attach another handler for zoomend and detect it that way

The zooming can be achieved without all the calculations.
var onZoom = function() {
var scale = d3.event.scale,
translate = d3.event.translate;
elemToZoom.attr('transform', 'scale(' + scale + ')translate(' + translate + ')');
};
var zoom = d3.behavior.zoom().on('zoom', onZoom);
var elemToZoom = d3.select('#zoomable')
.call(zoom);
When you are zooming in the graph, you may have to redraw the axes and the vertical and horizontal lines so that they don't get zoomed too.

Related

How to implement mouse wheel zoom in D3 like Google Map with overlay and Ctrl + Scroll event?

I implemented zoom with D3 js so whenever the mouse is hover over the canvas the zoom events triggers and allow a user to use the mouse wheel to zoom in and out.
Demo : https://diversity.rcc.uchicago.edu/collapsible_tree
But I want to prevent this default behavior of D3 zoom and need to enforce user to user Ctrl + Scroll to zoom the canvas like google map does : http://jsfiddle.net/maunovaha/jptLfhc8/
Is there anyway we can show the overlay to request a user to do use the combination and then only allow zoom.
My code for zoom is like this:
var svg = d3.select("#collapsible-tree")
.append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.call(zm = d3.behavior.zoom().scaleExtent([0.1, 3]).on("zoom", redraw))
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//Redraw for zoom
function redraw() {
//console.log("here", d3.event.translate, d3.event.scale);
svg.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
I had the same problem a few weeks ago. But i used dagreD3. The function i used are from D3. Here is my working fiddle.
JS Fiddle
// Create a new directed graph
var g = new dagreD3.graphlib.Graph().setGraph({})
.setDefaultEdgeLabel(function() {
return {};
});
// Disable Browserzoom with strg + mousewheel
$(document).keydown(function(event) {
if (event.ctrlKey == true && (event.which == '61' || event.which == '107' || event.which == '173' || event.which == '109' || event.which == '187' || event.which == '189')) {
alert('disabling zooming');
event.preventDefault();
}
});
$(window).bind('mousewheel DOMMouseScroll', function(event) {
if (event.ctrlKey == true) {
event.preventDefault();
}
});
// Check if strg is pressed
var ctrlPressed = false;
$(window).keydown(function(evt) {
if (evt.which == 17) {
ctrlPressed = true;
console.log("pressed");
}
}).keyup(function(evt) {
if (evt.which == 17) {
ctrlPressed = false;
console.log("not pressed");
}
});
//adding nodes and edges
g.setNode(0, {
label: "TOP",
});
g.setNode(1, {
label: "S",
});
g.setNode(2, {
label: "NP",
});
g.setNode(3, {
label: "DT",
});
g.setNode(4, {
label: "This",
});
g.setNode(5, {
label: "VP",
});
g.setNode(6, {
label: "VBZ",
});
g.setNode(7, {
label: "is",
});
g.setNode(8, {
label: "NP",
});
g.setNode(9, {
label: "DT",
});
g.setNode(10, {
label: "an",
});
g.setNode(11, {
label: "NN",
});
g.setNode(12, {
label: "example",
});
g.setNode(13, {
label: ".",
});
g.setNode(14, {
label: "sentence",
});
g.setEdge(3, 4);
g.setEdge(2, 3);
g.setEdge(1, 2);
g.setEdge(6, 7);
g.setEdge(5, 6);
g.setEdge(9, 10);
g.setEdge(8, 9);
g.setEdge(11, 12);
g.setEdge(8, 11);
g.setEdge(5, 8);
g.setEdge(1, 5);
g.setEdge(13, 14);
g.setEdge(1, 13);
g.setEdge(0, 1);
// Round the corners of the nodes
g.nodes().forEach(function(v) {
var node = g.node(v);
node.rx = node.ry = 5;
});
//makes the lines smooth
g.edges().forEach(function(e) {
var edge = g.edge(e.v, e.w);
edge.lineInterpolate = 'basis';
});
// Create the renderer
var render = new dagreD3.render();
var width = 500,
height = 1000,
center = [width / 2, height / 2];
// Set up an SVG group so that we can translate the final graph.
var svg = d3.select("svg"),
inner = svg.append("g");
var zoom = d3.behavior.zoom()
.on("zoom", zoomed);
function zoomed() {
inner.attr("transform", "translate(" + zoom.translate() + ")scale(" + zoom.scale() + ")");
}
svg.call(zoom)
svg.on("wheel.zoom", null);
svg.on("dblclick.zoom", null);
svg.call(zoom.event);
document.getElementById("container").addEventListener("wheel", myFunction);
function myFunction(event) {
if (ctrlPressed == true) {
if (event.wheelDelta > 0) {
zoom_by(1.03);
} else if (event.wheelDelta < 0) {
zoom_by(1 / 1.03);
}
}
}
function zoom_by(factor) {
var scale = zoom.scale(),
extent = zoom.scaleExtent(),
translate = zoom.translate(),
x = translate[0],
y = translate[1],
target_scale = scale * factor;
// If we're already at an extent, done
if (target_scale === extent[0] || target_scale === extent[1]) {
return false;
}
// If the factor is too much, scale it down to reach the extent exactly
var clamped_target_scale = Math.max(extent[0], Math.min(extent[1], target_scale));
if (clamped_target_scale != target_scale) {
target_scale = clamped_target_scale;
factor = target_scale / scale;
}
// Center each vector, stretch, then put back
x = (x - center[0]) * factor + center[0];
y = (y - center[1]) * factor + center[1];
// Enact the zoom immediately
zoom.scale(target_scale)
.translate([x, y]);
zoomed();
}
// Run the renderer. This is what draws the final graph.
render(inner, g);
// Center the graph
var initialScale = 1.0;
zoom.translate([(svg.attr("width") - g.graph().width * initialScale) / 2, 20])
.scale(initialScale)
.event(svg);
svg.attr("height", g.graph().height * initialScale + 40);
You can disable required d3 zoom events by setting that particular event to null.
svg.call(zoom) // zoom disable
.on("wheel.zoom", null)
.on("mousedown.zoom", null)
.on("touchstart.zoom", null)
.on("touchmove.zoom", null)
.on("touchend.zoom", null);
I stumbled upon the same problem and solved it by only conditionally calling the original wheel handler (I am using D3v4):
this.zoom = d3.zoom()[...]
var svg = d3.select(this.$refs.chart)
.call(this.zoom);
var wheeled = svg.on("wheel.zoom");
svg
.on("wheel.zoom", function () {
if (d3.event.ctrlKey) {
wheeled.call(this);
// prevent browser zooming at minimum zoom
d3.event.preventDefault();
d3.event.stopImmediatePropagation();
}
});
Using the latest d3 version and referring to Gniestschow answer
const maxScale=4;
var zoomListener = d3
.zoom()
.scaleExtent([0.1, maxScale])
.on("zoom", zoom);
var svgContainer=d3.select("#container").call(zoomListener);
var svgGroup=svgContainer.append("g")
var wheeled = svgContainer.on("wheel.zoom");
svgContainer
.on("wheel.zoom", null)
window.onkeydown = listenToTheKey;
window.onkeyup = listenToKeyUp;
function listenToTheKey(event) {
if (event.ctrlKey) svgContainer.on("wheel.zoom", wheeled);
}
function listenToKeyUp() {
svgContainer.on("wheel.zoom", null);
}
function zoom(event) {
svgGroup.attr("transform", event.transform.toString());
}

d3.js limit panning in force layout

I am using d3.js with a force layout to visualize a large number of nodes. I would like to implement a limitation to the panning option of the zoom.
JSFiddle : https://jsfiddle.net/40z5tw8h/24/
The above fiddle contains a simple version of what I am working on.
Because I would potentially have to visualize a very large dataset, I use a function to scale down the group holding element ('g') after forces are done. In that way i always have the full visualization visible afterwards.
I would like to limit the panning - when the graph is fully visible, to only be able to move it within the viewport.
In case the layout is zoomed, I would like to limit the panning as follows:
The group holding element should not be able to go:
down more than 20 px from the top of the svg.
right more than 20 px from the left side of the svg.
up more than 20 px from the bottom of the svg.
left more than 20 px from the right side of the svg.
I think all the implementation should be within the zoom function, which for now is:
function zoomed(){
if (d3.event.sourceEvent == null){ //when fitFullGraph uses the zoom
g.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
else{
var gElementBounds = g.node().getBoundingClientRect();
var g_bottom = gElementBounds.bottom;
var g_top = gElementBounds.top;
var g_left = gElementBounds.left;
var g_right = gElementBounds.right;
var g_height = gElementBounds.height;
var g_width = gElementBounds.width;
var svg = g.node().parentElement;
var svgElementBounds = svg.getBoundingClientRect();
var svg_bottom = svgElementBounds.bottom;
var svg_top = svgElementBounds.top;
var svg_left = svgElementBounds.left;
var svg_right = svgElementBounds.right;
var svg_height = svgElementBounds.height;
var svg_width = svgElementBounds.width;
var t = d3.event.translate;
var margin = 20;
if(d3.event.sourceEvent.type == 'wheel'){//event is zoom
g.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
else{//event is pan
// if(t[0] < svg_left + margin) t[0]= svg_left + margin;
//else if(t[0] > svg_width-g_width - margin) t[0] = svg_width-g_width - margin;
// if(t[1] < g_height +margin) t[1] = g_height + margin;
//else if (t[1] > svg_height - margin) t[1] = svg_height - margin;
//.attr("transform", "translate(" + t+ ")scale(" + d3.event.scale + ")");
//3.event.translate = t;
g.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
}
}
The limitations I tried to implement are commented out, because they do not work properly.
Does anyone have a solution?
This is not the complete answer to your question.
I used for block panning to left side translate X scale
var translate = d3.event.translate;
var translateX = translate[0];
var translateY = translate[1];
var scale = d3.event.scale;
var tX = translateX * scale;
var tY = translateY * scale;
console.log('tx', tX, 'ty', tY);
// Do not pan more to left
if (tX> 0) {
g.attr("transform", "translate(" + d3.event.translate + ") scale(" + d3.event.scale + ")");
} else {
translate[0] = 0;
g.attr("transform", "translate(" + translate + ") scale(" + d3.event.scale + ")");
}
Which cancels the translation to left but internally it continues. Your user probably stops dragging to the left. Panning to the right gets weird when starting to pan as internally the event has panned far to the left.

Snap.svg scale and animate SVG

I'm trying to scale my SVG with g.animate({ transform: "s2.5,2.5," + bbox.cx + "," + bbox.cy }, 0); and then animate wheelAnimation(bbox.cx, bbox.cy, 1500);
var i = 0;
function wheelAnimation(cx, cy, speed){
i++;
g.animate(
{ transform: "r360," + cx + ',' + cy}, // Basic rotation around a point. No frills.
speed, // Nice slow turning rays
function(){
if(i == 5)
speed = 5000;
g.attr({ transform: 'rotate(0 ' + cx + ' ' + cy}); // Reset the position of the rays.
wheelAnimation(cx,cy, speed); // Repeat this animation so it appears infinite.
}
);
}
But my SVG didn't scaling. It's only rotates. If I remove rotation - SVG scaling. How to combine it to immediately scale and then animate rotation?
Plunker example
I've never used Snap.svg but you might try this:
var i = 0;
function wheelAnimation(cx, cy, speed, scale){
i++;
g.attr({ transform: "r0 " + cx + " " + cy + " s" + scale + "," + scale + "," + cx + "," + cy }); //Reset + Scale setup
g.animate({
transform: "r360," + cx + "," + cy + " s" + scale + "," + scale + "," + cx + "," + cy }, // Basic rotation around a point. No frills.
speed, // Nice slow turning rays
function(){
if(i == 5)
speed = 5000;
wheelAnimation(cx, cy, speed, scale); // Repeat this animation so it appears infinite.
}
);
}
Hope this helps you :)
See Plunkr

Can't Add and Remove Div to Document

I cant get this code to work. It's supposed to be a custom dialog box. When a certain event transpires, the div pops up with an animation like it's moving towards you from a distance. The div has a clickable link that is supposed to trigger the removal of the box when clicked.
It works fine for one iteration, but when I attempt to trigger it again the whole thing falls apart in a horrendous way. The console doesn't issue any warnings or errors, but the browser freezes then crashes.
Any help would be appreciated.
function modalBoxOpen(left, top, width, height, string, element){
var x = $('<div></div>').prependTo(element);
x.attr("id", "pop_up_div");
x.css("position", "absolute");
x.css("border-color", "black");
x.css("border-style", "solid");
x.css("background-color", "rgb(204, 204, 204)" );
x.css("padding", 0 + "%");
x.css("z-index", 10);
var y = $('<div>' + string + '</div>').prependTo(x);
y.attr("id", "inner_pop_up_div");
y.css("text-align", "center");
y.css("font-family", "arial");
y.css("position", "absolute");
y.css("margin", "1%");
y.css("width", "98%");
y.css("height", "93%"); y.css("background-color", "red");
var l = left + .5*width;
var t = top + .5*height;
x.css("left", l + "px");
x.css("top", t + "px");
x.css("width", 2*(left + .5*width - l) + "px");
x.css("height", 2*(top + .5*height - t) + "px");
y.css("font-size", .0413*2*(left + .5*width - l) + "px");
$('<div style = "background-color: black; border-radius: 10px; padding
: 1%; margin-top: 1%; margin-left: 43%; margin-right: 43%">' +
'<a class = "link" id = "ok_button" href = "javascript:;"> OK </a>
</div>').appendTo(y);
okButtonClick = function(e){
e.stopPropagation();
removeElement('pop_up_div');
$(".input_cover").css("display", "none");
bindHandlers();
if ( $("#language_list_container").is(":visible") )
{$("#language_clickable").click();}
}
$("#ok_button").bind("click", okButtonClick);
var count = 0;
timer = setInterval(open, 40);
function open(){
if ( count > 4 ) {
clearInterval(timer);
bindHandlers();
return;
}
l -= .1*width;
t -= .1*height;
x.css("left", l + "px");
x.css("top", t + "px");
x.css("width", 2*(left + .5*width - l) + "px");
x.css("height", 2*(top + .5*height - t) + "px");
x.css("border-radius", 2*(count+1) + "px");
y.css("font-size", .0413*2*(left + .5*width - l) + "px");
count++;
}
}
function removeElement(id) {
return
(elem=document.getElementById(id)).parentNode.removeChild(elem);
}

Changing the appearance of D3 links

I want to visualize data as a tree, but also I want to customize default link appearance. There shown default appearance, but I want to create links which looks like Rational Software Architect links. Is it possible?
The links are SVG path elements. You can style them using CSS to change the color, width etc. For the arrow heads, you can use SVG Markers. To add labels, you would need to add additional SVG text elements. You could for example add a new select with the tree links as data that create the SVG text elements for the UML cardinality.
I've done it with writing my own path handler. Here is the sample code:
function elbow(d) {
var radius = 10;
var xOffsetSign = Math.sign(d.source.x - d.target.x);
var yOffsetSign = Math.sign(d.source.y - d.target.y);
if (xOffsetSign != 0) {
var ellipseXDirection = (xOffsetSign * yOffsetSign) > 0 ? 1 : 0;
return "M" + d.source.x + "," + d.source.y
+ " H" + (d.target.x + xOffsetSign * radius)
+ " A" + radius + "," + radius + " 0 0," + ellipseXDirection + " " + d.target.x + "," + (d.source.y - yOffsetSign * radius)
+ " V" + d.target.y
+ (d.target.children ? "" : "h" + margin.right);
} else {
return "M" + d.source.x + "," + d.source.y
+ " H" + d.target.x + " V" + d.target.y
+ (d.target.children ? "" : "h" + margin.right);
}
}
Function Math.sign is my own implementation

Categories

Resources