Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ScaleBand with Zoom

Tags:

d3.js

I am currently using D3 v4. My x scale is a scaleBand where I am taking a country name for the display and my y scale is scaleTime. When I am trying to get a new xScale in the zoom function it's throwing an error at rescaleX. I looked at many examples online but all of them either have a linearScale or scaleTime.

var countryArr = ["USA", "UK", "Poland", "Sweden"];
var firstDay = moment(day).subtract(1, 'days').toDate();
var lastDay = moment(day).add(1, 'days').toDate();

// define scale
var yScale = d3.scaleTime().range([0, width]); 
yScale.domain([firstDay, lastDay]);
var xScale = d3.scaleBand().range([0, height]);
xScale.domain(countryArr);

// define zoom
var zoom = d3.zoom()
    .scaleExtent([1, 1])
    .translateExtent([[margin.left, margin.top], [width, height]])
    .on("zoom", zoomed);

d3.select("#main-canvas").call(zoom);

function zoomed() {
    //console.log(d3.event.transform.y);
    var new_x = d3.event.transform.rescaleX(xScale);  // ERROR at this line
    var new_y = d3.event.transform.rescaleY(yScale); 
}

enter image description here

like image 290
Sneha Avatar asked Mar 17 '18 10:03

Sneha


2 Answers

As @gerardofurtado explains the rescale function makes little for a ordinal scale. But here's a little trick I like to use for a nice zoom on the graph with rescaling ordinal axis. Essentially, it zoom the axis independently and then inverse scales the components of the axis (lines and text) so that they don't distort:

<!DOCTYPE html>
<html>

<head>
  <script data-require="[email protected]" data-semver="4.0.0" src="https://d3js.org/d3.v4.min.js"></script>
  <style>
    .bar {
      fill: steelblue;
    }
    
    .bar:hover {
      fill: brown;
    }
    
    .axis--x path {
      display: none;
    }
  </style>
</head>

<body>
  <svg width="400" height="400"></svg>
  <script>
    var svg = d3.select("svg"),
      margin = {
        top: 20,
        right: 20,
        bottom: 30,
        left: 40
      },
      width = +svg.attr("width") - margin.left - margin.right,
      height = +svg.attr("height") - margin.top - margin.bottom;

    var zoom = d3.zoom()
      .scaleExtent([1, Infinity])
      .translateExtent([
        [0, 0],
        [width, height]
      ])
      .extent([
        [0, 0],
        [width, height]
      ])
      .on("zoom", zoom);

    svg.call(zoom);

    var x = d3.scaleBand().rangeRound([0, width]).padding(0.1),
      y = d3.scaleBand().rangeRound([height, 0]).padding(0.1);

    var g = svg.append("g")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    var defs = g.append('defs');
    
    defs
      .append('clipPath')
      .attr('id', 'clip')
      .append('rect')
      .attr('x', 0)
      .attr('y', 0)
      .attr('width', width)
      .attr('height', height);
      
    defs
      .append('clipPath')
      .attr('id', 'clipx')
      .append('rect')
      .attr('x', 0)
      .attr('y', height)
      .attr('width', width)
      .attr('height', margin.bottom);
      
    defs
      .append('clipPath')
      .attr('id', 'clipy')
      .append('rect')
      .attr('x', -margin.left)
      .attr('y', -10)
      .attr('width', margin.left+1)
      .attr('height', height+15);

    var data = [{
      "letter": "A",
      "frequency": 'Z'
    }, {
      "letter": "B",
      "frequency": 'Y'
    }, {
      "letter": "C",
      "frequency": 'X'
    }, {
      "letter": "D",
      "frequency": 'W'
    }, {
      "letter": "E",
      "frequency": 'V'
    }, {
      "letter": "F",
      "frequency": 'U'
    }, {
      "letter": "G",
      "frequency": 'T'
    }, {
      "letter": "H",
      "frequency": 'S'
    }, {
      "letter": "I",
      "frequency": 'R'
    }, {
      "letter": "J",
      "frequency": 'Q'
    }, {
      "letter": "K",
      "frequency": 'P'
    }, {
      "letter": "L",
      "frequency": 'O'
    }, {
      "letter": "M",
      "frequency": 'N'
    }, {
      "letter": "N",
      "frequency": 'M'
    }, {
      "letter": "O",
      "frequency": 'L'
    }, {
      "letter": "P",
      "frequency": 'K'
    }, {
      "letter": "Q",
      "frequency": 'J'
    }, {
      "letter": "R",
      "frequency": 'I'
    }, {
      "letter": "S",
      "frequency": 'H'
    }, {
      "letter": "T",
      "frequency": 'G'
    }, {
      "letter": "U",
      "frequency": 'F'
    }, {
      "letter": "V",
      "frequency": 'E'
    }, {
      "letter": "W",
      "frequency": 'D'
    }, {
      "letter": "X",
      "frequency": 'C'
    }, {
      "letter": "Y",
      "frequency": 'B'
    }, {
      "letter": "Z",
      "frequency": 'A'
    }];

    x.domain(data.map(function(d) {
      return d.letter;
    }));
    y.domain(data.map(function(d) {
      return d.frequency;
    }));

    var xAxis = g.append("g")
      .attr('clip-path', 'url(#clipx)')
      .append("g")
      .attr("class", "axis axis--x")
      .attr("transform", "translate(0," + height + ")")
      .call(d3.axisBottom(x));
      
    var yAxis = g.append("g")
      .attr('clip-path', 'url(#clipy)')
      .append("g")
      .attr("class", "axis axis--y")
      .call(d3.axisLeft(y));
      
    yAxis
      .append("text")
      .attr("transform", "rotate(-90)")
      .attr("y", 6)
      .attr("dy", "0.71em")
      .attr("text-anchor", "end")
      .text("Letters Too");

    var bars = g.append("g")
      .attr('clip-path', 'url(#clip)')
      .selectAll(".bar")
      .data(data)
      .enter().append("rect")
      .attr("class", "bar")
      .attr("x", function(d) {
        return x(d.letter);
      })
      .attr("y", function(d) {
        return y(d.frequency) + y.bandwidth()/2;
      })
      .attr("width", x.bandwidth())
      .attr("height", function(d) {
        return height - y(d.frequency);
      });

    function zoom() {
      var t = d3.event.transform;
      bars.attr("transform", t);
      
      xAxis.attr("transform", d3.zoomIdentity.translate(t.x, height).scale(t.k));
      xAxis.selectAll("text")
        .attr("transform",d3.zoomIdentity.scale(1/t.k));
      xAxis.selectAll("line")
        .attr("transform",d3.zoomIdentity.scale(1/t.k));
        
      yAxis.attr("transform", d3.zoomIdentity.translate(0, t.y).scale(t.k));
      yAxis.selectAll("text")
        .attr("transform",d3.zoomIdentity.scale(1/t.k));
      yAxis.selectAll("line")
        .attr("transform",d3.zoomIdentity.scale(1/t.k));
    }
  </script>
</body>

</html>
like image 129
Mark Avatar answered Nov 01 '22 12:11

Mark


It makes little sense using rescaleX in an ordinal (qualitative) scale. The API is clear:

Returns a copy of the continuous scale x whose domain is transformed. This is implemented by first applying the inverse x-transform on the scale’s range, and then applying the inverse scale to compute the corresponding domain. (emphasis mine)

Also, Bostock (D3 creator) already explained that...

... the zoom behavior affects the attached scales’ domains, but it wouldn’t make sense to modify the ordinal scale’s domain when panning and zooming because the domain is discrete.

What you can do is creating a geometric zooming instead of a semantic zooming.

Here is a demo, I'm using this example of a bar chart to add the geometric zooming:

var svg = d3.select("svg"),
  margin = {
    top: 20,
    right: 20,
    bottom: 30,
    left: 40
  },
  width = +svg.attr("width") - margin.left - margin.right,
  height = +svg.attr("height") - margin.top - margin.bottom;

var x = d3.scaleBand().rangeRound([0, width]).padding(0.1),
  y = d3.scaleLinear().rangeRound([height, 0]);

var g = svg.append("g")
  .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

var data = [{
  "letter": "A",
  "frequency": 0.08167
}, {
  "letter": "B",
  "frequency": 0.01492
}, {
  "letter": "C",
  "frequency": 0.02782
}, {
  "letter": "D",
  "frequency": 0.04253
}, {
  "letter": "E",
  "frequency": 0.12702
}, {
  "letter": "F",
  "frequency": 0.02288
}, {
  "letter": "G",
  "frequency": 0.02015
}, {
  "letter": "H",
  "frequency": 0.06094
}, {
  "letter": "I",
  "frequency": 0.06966
}, {
  "letter": "J",
  "frequency": 0.00153
}, {
  "letter": "K",
  "frequency": 0.00772
}, {
  "letter": "L",
  "frequency": 0.04025
}, {
  "letter": "M",
  "frequency": 0.02406
}, {
  "letter": "N",
  "frequency": 0.06749
}, {
  "letter": "O",
  "frequency": 0.07507
}, {
  "letter": "P",
  "frequency": 0.01929
}, {
  "letter": "Q",
  "frequency": 0.00095
}, {
  "letter": "R",
  "frequency": 0.05987
}, {
  "letter": "S",
  "frequency": 0.06327
}, {
  "letter": "T",
  "frequency": 0.09056
}, {
  "letter": "U",
  "frequency": 0.02758
}, {
  "letter": "V",
  "frequency": 0.00978
}, {
  "letter": "W",
  "frequency": 0.0236
}, {
  "letter": "X",
  "frequency": 0.0015
}, {
  "letter": "Y",
  "frequency": 0.01974
}, {
  "letter": "Z",
  "frequency": 0.00074
}];

x.domain(data.map(function(d) {
  return d.letter;
}));
y.domain([0, d3.max(data, function(d) {
  return d.frequency;
})]);

g.append("g")
  .attr("class", "axis axis--x")
  .attr("transform", "translate(0," + height + ")")
  .call(d3.axisBottom(x));

g.append("g")
  .attr("class", "axis axis--y")
  .call(d3.axisLeft(y).ticks(10, "%"))
  .append("text")
  .attr("transform", "rotate(-90)")
  .attr("y", 6)
  .attr("dy", "0.71em")
  .attr("text-anchor", "end")
  .text("Frequency");

g.selectAll(".bar")
  .data(data)
  .enter().append("rect")
  .attr("class", "bar")
  .attr("x", function(d) {
    return x(d.letter);
  })
  .attr("y", function(d) {
    return y(d.frequency);
  })
  .attr("width", x.bandwidth())
  .attr("height", function(d) {
    return height - y(d.frequency);
  });

var rect = g.append("rect")
  .attr("width", width)
  .attr("height", height)
  .attr("opacity", 0)
  .call(d3.zoom()
    .scaleExtent([.1, 10])
    .on("zoom", zoom));

function zoom() {
  g.attr("transform", d3.event.transform);
}
.bar {
  fill: steelblue;
}

.bar:hover {
  fill: brown;
}

.axis--x path {
  display: none;
}
<svg width="960" height="500"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
like image 5
Gerardo Furtado Avatar answered Nov 01 '22 13:11

Gerardo Furtado