Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

d3.js change zoom behavior to semantic zoom

I'm doing some tests with d3.js regarding zooming. At the moment, I have successfully implemented geometric zoom in my test, but it has a drawback: the elements under the zoomed g are being scaled. As I understood it, this could be solved by using semantic zooming.

The problem is that I need scale in my test, as I'm syncing it with a jQuery.UI slider value.

On the other hand, I would like the text elements being resized to maintain their size after a zoom operation.

I have an example of my current attempt here.

I'm having trouble changing my code to fit this purpose. Can any one share some insight/ideas?

like image 982
Joum Avatar asked Feb 09 '23 07:02

Joum


2 Answers

For your solution I have merged 2 examples:

  • Semantic Zooming
  • Programmatic Zooming

Code snippets:

function zoom() {
  text.attr("transform", transform);
  var scale = zoombehavior.scale();
  //to make the scale rounded to 2 decimal digits
  scale = Math.round(scale * 100) / 100;
  //setting the slider to the new value
  $("#slider").slider( "option", "value", scale );
  //setting the slider text to the new value
  $("#scale").val(scale);
}
//note here we are not handling the scale as its Semantic Zoom
function transform(d) {
  //translate string
  return "translate(" + x(d[0]) + "," + y(d[1]) + ")";
}

function interpolateZoom(translate, scale) {
  zoombehavior
        .scale(scale)//we are setting this zoom only for detecting the scale for slider..we are not zoooming using scale.
        .translate(translate);
      zoom();
}
var slider = $(function() {
  $("#slider").slider({
    value: zoombehavior.scaleExtent()[0],//setting the value
    min: zoombehavior.scaleExtent()[0],//setting the min value
    max: zoombehavior.scaleExtent()[1],//settinng the ax value
    step: 0.01,
    slide: function(event, ui) {
      var newValue = ui.value;
      var  center = [centerX, centerY],
        extent = zoombehavior.scaleExtent(),
        translate = zoombehavior.translate(),
        l = [],
        view = {
          x: translate[0],
          y: translate[1],
          k: zoombehavior.scale()
        };
      //translate w.r.t the center
      translate0 = [(center[0] - view.x) / view.k, (center[1] - view.y) / view.k];
      view.k = newValue;//the scale as per the slider
      //the translate after the scale(so we are multiplying the translate)
      l = [translate0[0] * view.k + view.x, translate0[1] * view.k + view.y];

      view.x += center[0] - l[0];
      view.y += center[1] - l[1];

      interpolateZoom([view.x, view.y], view.k);

    }
  });
});

I am zooming w.r.t. 250,250 which is the center of the clip circle.

Working code here (have added necessary comments)

Hope this helps!

like image 165
Cyril Cherian Avatar answered Feb 11 '23 00:02

Cyril Cherian


To do what you want, you need to refactor the code first a little bit. With d3, it is good practice to use data() to append items to a selection, rather than using for loops.

So this :

for(i=0; i<7; i++){
	pointsGroup.append("text")
  			  .attr("x", function(){
              	var plusOrMinus = Math.random() < 0.5 ? -1 : 1;
                var randx = Math.random();
                return Math.floor(plusOrMinus*randx*75)+centerx;
              })
              .attr("y", function(){
              	var plusOrMinus = Math.random() < 0.5 ? -1 : 1;
                var randy = Math.random();
                return Math.floor(plusOrMinus*randy*75)+centery;
              })
              .html("star")
              .attr("class", "point material-icons")
              .on("click", function(){console.log("click!");});
}

Becomes this

var arr = [];

for(i=0; i<7; i++){
  var plusOrMinus = Math.random() < 0.5 ? -1 : 1;
  var randx = Math.random();
  var x = Math.floor(plusOrMinus*randx*75)+centerx; 
  
  var plusOrMinus = Math.random() < 0.5 ? -1 : 1;
  var randy = Math.random();
  var y = Math.floor(plusOrMinus*randy*75)+centery;
  
  arr.push({"x":x,"y":y});
}
	
pointsGroup.selectAll("text")
            .data(arr)
            .enter()
            .append("text")
            .attr("x", function(d,i){
              	return d.x;// This corresponds to arr[i].x
              })
            .attr("y", function(d,i){
             	return d.y;// This corresponds to arr[i].y
              })
            .html("star")
            .attr("class", "point material-icons")
            .on("click", function(){console.log("click!");});

This way, you can access individual coordinates using for instance.attr("x",function(d,i){ //d.x is equal to arr.x, i is item index in the selection});

Then, to achieve your goal, rather than changing the scale of your items, you should use a linear scale to change each star position.

First, add linear scales to your fiddle and apply them to the zoom:

var scalex = d3.scale.linear();
var scaley = d3.scale.linear();

var zoom = d3.behavior.zoom().x(scalex).y(scaley).scaleExtent([1, 5]).on('zoom', onZoom);

And finally on zoom event apply the scale to each star's x and y

function onZoom(){
  d3.selectAll("text")
    .attr("x",function(d,i){
      
      return scalex(d.x);
    })
    .attr("y",function(d,i){
     
      return scaley(d.y);
    });
}

At this point, zoom will work without the slider. To add the slider, simply change manually the zoom behavior scale value during onSlide event, then call onZoom.

function onSlide(scale){
  var sc = $("#slider").slider("value");
  zoom.scale(sc);
  onZoom();
}

Note: I used this config for the slider:

var slider = $(function() {
	    $( "#slider" ).slider({
			value: 1,
			min: 1,
			max: 5,
			step: 0.1,
			slide: function(event, ui){
              onSlide(5/ui.value);
			}
	    });
	});

Please note that at this point the zoom from the ui is performed relative to (0,0) at this point, and not your "circle" window center. To fix it I simplified the following function from the programmatic example, that computes valid translate and scale to feed to the zoom behavior.

// To handle center zooming
var width = 500;
var height = 600;

function zoomClick(sliderValue) {
  var center = [width / 2, height / 2],
    extent = zoom.scaleExtent(),
    translate = zoom.translate();


  var view = {
    x: zoom.translate()[0],
    y: zoom.translate()[1],
    k: zoom.scale()
  };

  var target_zoom = sliderValue;

  if (target_zoom < extent[0] || target_zoom > extent[1]) {
    return false;
  }

  var translate0 = [(center[0] - view.x) / view.k, (center[1] - view.y) / view.k];

  view.k = target_zoom;
  var l = [];
  l = [translate0[0] * view.k + view.x, translate0[1] * view.k + view.y];

  view.x += center[0] - l[0];
  view.y += center[1] - l[1];

  // [view.x view.y] is valid translate
  // view.k is valid scale
  // Then, simply feed them to the zoom behavior
  zoom
    .scale(view.k)
    .translate([view.x, view.y]);
  // and call onZoom to update points position
  onZoom();

}

then just change onSlide to use this new function every time slider moves

function onSlide(scale){
  var sc = $("#slider").slider("value");
  zoomClick(sc);
}

Full snippet

function onZoom(){
  d3.selectAll("text")
    .attr("x",function(d,i){
      
      return scalex(d.x);
    })
    .attr("y",function(d,i){
     
      return scaley(d.y);
    });
}


function onSlide(scale){
  var sc = $("#slider").slider("value");
  zoomClick(sc);
}

var scalex = d3.scale.linear();
var scaley = d3.scale.linear();

var zoom = d3.behavior.zoom().x(scalex).y(scaley).scaleExtent([1, 5]).on('zoom', onZoom);

var svg = d3.select("body").append("svg")
                            .attr("height", "500px")
                            .attr("width", "500px")
					        .call(zoom)
					        .on("mousedown.zoom", null)
					        .on("touchstart.zoom", null)
    				        .on("touchmove.zoom", null)
    				        .on("touchend.zoom", null);

var centerx = 250,
    centery = 250;

var circleGroup = svg.append("g")
                      .attr("id", "circleGroup");

var circle = circleGroup.append("circle")
                  .attr("cx", "50%")
                  .attr("cy", "50%")
                  .attr("r", 150)
                  .attr("class", "circle");



var pointsParent = svg.append("g").attr("clip-path", "url(#clip)").attr("id", "pointsParent");

var pointsGroup = pointsParent.append("g")
					  .attr("id", "pointsGroup");

var arr = [];

for(i=0; i<7; i++){
  var plusOrMinus = Math.random() < 0.5 ? -1 : 1;
  var randx = Math.random();
  var x = Math.floor(plusOrMinus*randx*75)+centerx; 
  
  var plusOrMinus = Math.random() < 0.5 ? -1 : 1;
  var randy = Math.random();
  var y = Math.floor(plusOrMinus*randy*75)+centery;
  
  arr.push({"x":x,"y":y});
}
	
pointsGroup.selectAll("text")
            .data(arr)
            .enter()
            .append("text")
            .attr("x", function(d,i){
              	return d.x;// This corresponds to arr[i].x
              })
            .attr("y", function(d,i){
             	return d.y;// This corresponds to arr[i].y
              })
            .html("star")
            .attr("class", "point material-icons")
            .on("click", function(){console.log("click!");});

zoom(pointsGroup);

var clip = svg.append("defs").append("svg:clipPath")
        .attr("id", "clip")
        .append("svg:circle")
        .attr("id", "clip-circ")
        .attr("cx", centerx)
        .attr("cy", centery)
        .attr("r", 149);

var slider = $(function() {
	    $( "#slider" ).slider({
			value: 1,
			min: 1,
			max: 5,
			step: 0.1,
			slide: function(event, ui){
              onSlide(5/ui.value);
			}
	    });
	});



// To handle center zooming
var width = 500;
var height = 600;

function zoomClick(sliderValue) {
    var target_zoom = 1,
        center = [width / 2, height / 2],
        extent = zoom.scaleExtent(),
        translate = zoom.translate();
        
    
    var view = {x: zoom.translate()[0], y: zoom.translate()[1], k: zoom.scale()};
  
    target_zoom = sliderValue;

    if (target_zoom < extent[0] || target_zoom > extent[1]) { return false; }

    var translate0 = [];
    translate0 = [(center[0] - view.x) / view.k, (center[1] - view.y) / view.k];
  
    view.k = target_zoom;
    var l = [];
    l = [translate0[0] * view.k + view.x, translate0[1] * view.k + view.y];

    view.x += center[0] - l[0];
    view.y += center[1] - l[1];
  
    zoom
       .scale(view.k)
       .translate([view.x, view.y]);
    onZoom();
  
}
body {
  font: 10px sans-serif;
}

text {
  font: 10px sans-serif;
}

.circle{
  stroke: black;
  stroke-width: 2px;
  fill: white;
}

.point{
  fill: goldenrod;
  cursor: pointer;
}

.blip{
  fill: black;
}

#slider{
  width: 200px;
  margin: auto;
}
<!DOCTYPE html>
<html>
<head>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons"
      rel="stylesheet">
<link href="https://code.jquery.com/ui/1.11.4/themes/smoothness/jquery-ui.css" rel="stylesheet" type="text/css" />
<script src="https://code.jquery.com/jquery-1.11.3.js"></script>
<script src="https://code.jquery.com/ui/1.11.4/jquery-ui.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js"></script>
  

<meta charset=utf-8 />
<title>d3.JS slider zoom</title>
</head>
  
<body>
  <div id="slider"></div>
</body>
</html>
like image 45
Overdrivr Avatar answered Feb 11 '23 00:02

Overdrivr