How to zoom in chart correctly with multiple y-axis

Viewed 291

I have implemented a zoom function for my chart, where one can zoom into a certain area of the chart by dragging the area and releasing it. My fiddle is accessible here.

The zoom function is working correctly for the blue line as both the blue line and the left axis is being updated. However, the right axis is not being updated while the red line is zoomed in. I have hardcoded a domain from 0 to 200 for the right axis so whenever I zoom in the domain goes from 0 to 200 instead of the correct zoomed in domain. What should be the code for the domain for the axis on the right so that it gets updated during the zoom? Any help is greatly appreciated!

    var data = [ {x: 0, y: 0, y1: 0}, {x: 1, y: 30, y1: 100}, {x: 2, y: 40, y1: 200},
                {x: 3, y: 60, y1: 300}, {x: 4, y: 70, y1: 400}, {x: 5, y: 90, y1: 500} ];

    const margin = {
      left: 20,
      right: 20,
      top: 20,
      bottom: 80
    };

    const svg = d3.select('svg');
    svg.selectAll("*").remove();

    const width = 200 - margin.left - margin.right;
    const height = 200 - margin.top - margin.bottom;

    const g = svg.append('g').attr('transform', `translate(${margin.left},${margin.top})`);

    var x = d3.scaleLinear()
            .domain([0, d3.max(data, function(d){ return d.x; })])
            .range([0,width])
            .nice();

    var y = d3.scaleLinear()
            .domain([0, d3.max(data, function(d){ return d.y; })])
            .range([0,height])
            .nice();

    var y1 = d3.scaleLinear()
            .domain([0, d3.max(data, function(d) { return d.y1; })])
            .range([0, height])
            .nice();

    const xAxis = d3.axisTop()
                .scale(x)
                .ticks(5)
                .tickPadding(3)
                .tickSize(-height)

    const yAxis = d3.axisLeft()
                .scale(y)
                .ticks(5)
                .tickPadding(3)
                .tickSize(-width);

    const yAxis1 = d3.axisRight()
                .scale(y1)

    svg.append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(20,20)")
      .call(xAxis);

    svg.append("g")
      .attr("class", "y axis")
      .attr("transform", "translate(20,20)")
      .call(yAxis);

    svg.append("g")
      .attr("class", "y1 axis")
      .attr("transform", "translate(185,20)")
      .call(yAxis1);


    var lineFunction = d3.line()
    .x(function(d) {return x(d.x); })
    .y(function(d) {return y(d.y); })
    .curve(d3.curveLinear);

    var lineFunctionOne = d3.line()
    .x(function(d) {return x(d.x); })
    .y(function(d) {return y1(d.y1); })
    .curve(d3.curveLinear);

    //defining and plotting the lines
    var path = g.append("path")
              .attr("class", "path1")
              .attr("id", "blueLine")
              .attr("d", lineFunction(data))
              .attr("stroke", "blue")
              .attr("stroke-width", 2)
              .attr("fill", "none")
              .attr("clip-path", "url(#clip)");

    var path1 = g.append("path")
              .attr("class", "path2")
              .attr("id", "redLine")
              .attr("d", lineFunctionOne(data))
              .attr("stroke", "red")
              .attr("stroke-width", 2)
              .attr("fill", "none")
              .attr("clip-path", "url(#clip)");

    //************* Zoom ***************
    //add brushing
    var brush = d3.brush().extent([[0, 0], [width, height]]).on("end", brushended),
        idleTimeout,
        idleDelay = 350;
      g.append("g")
      .attr("class", "brush")
      .call(brush);

    // Add a clipPath: everything out of this area won't be drawn when chart is zoomed in
    var clip = svg.append("defs").append("svg:clipPath")
    .attr("id", "clip")
    .append("svg:rect")
    .attr("width", width)
    .attr("height", height)
    .attr("x", 0) 
    .attr("y", 0);

    function brushended() {

      var s = d3.event.selection;
      //If no selection, re-initialize chart on double click. Otherwise, update x-axis and y-axis domain
      if (!s) {
        // This allows to wait a little bit
        if (!idleTimeout) return idleTimeout = setTimeout(idled, 350);
        x.domain(d3.extent(data, function (d) { return d.x; })).nice();
        y.domain(d3.extent(data, function (d) { return d.y; })).nice();
        y1.domain(d3.extent(data, function (d) { return d.y1; })).nice();
      } else {
        x.domain([s[0][0], s[1][0]].map(x.invert, x));
        y.domain([s[0][1], s[1][1]].map(y.invert, y));
        y1.domain([0, 200]); //hardcoded domain
        //This removes the grey brush area as soon as the selection has been done
        g.select(".brush").call(brush.move, null)
      }
      zoom();
    }

    function idled() {
      idleTimeout = null;
    }

    function zoom() {
      var t = svg.transition().duration(750);
      svg.select(".x.axis").transition(t).call(xAxis);
      svg.select(".y.axis").transition(t).call(yAxis);
      svg.select(".y1.axis").transition(t).call(yAxis1);
      svg.select(".path1").transition(t).attr("d", lineFunction(data));
      svg.select(".path2").transition(t).attr("d", lineFunctionOne(data));
    }
1 Answers

I'm puzzled by your question... all you need to do is the same thing you did just the line above it:

y1.domain([s[0][1], s[1][1]].map(y1.invert, y));

By the way, you don't need the thisArg in the map, it can be just:

y1.domain([s[0][1], s[1][1]].map(y1.invert));

Here is the updated code:

var data = [{
    x: 0,
    y: 0,
    y1: 0
  }, {
    x: 1,
    y: 30,
    y1: 100
  }, {
    x: 2,
    y: 40,
    y1: 200
  },
  {
    x: 3,
    y: 60,
    y1: 300
  }, {
    x: 4,
    y: 70,
    y1: 400
  }, {
    x: 5,
    y: 90,
    y1: 500
  }
];

const margin = {
  left: 20,
  right: 20,
  top: 20,
  bottom: 80
};

const svg = d3.select('svg');
svg.selectAll("*").remove();

const width = 200 - margin.left - margin.right;
const height = 200 - margin.top - margin.bottom;

const g = svg.append('g').attr('transform', `translate(${margin.left},${margin.top})`);

var x = d3.scaleLinear()
  .domain([0, d3.max(data, function(d) {
    return d.x;
  })])
  .range([0, width])
  .nice();

var y = d3.scaleLinear()
  .domain([0, d3.max(data, function(d) {
    return d.y;
  })])
  .range([0, height])
  .nice();

var y1 = d3.scaleLinear()
  .domain([0, d3.max(data, function(d) {
    return d.y1;
  })])
  .range([0, height])
  .nice();

const xAxis = d3.axisTop()
  .scale(x)
  .ticks(5)
  .tickPadding(3)
  .tickSize(-height)

const yAxis = d3.axisLeft()
  .scale(y)
  .ticks(5)
  .tickPadding(3)
  .tickSize(-width);

const yAxis1 = d3.axisRight()
  .scale(y1)

svg.append("g")
  .attr("class", "x axis")
  .attr("transform", "translate(20,20)")
  .call(xAxis);

svg.append("g")
  .attr("class", "y axis")
  .attr("transform", "translate(20,20)")
  .call(yAxis);

svg.append("g")
  .attr("class", "y1 axis")
  .attr("transform", "translate(185,20)")
  .call(yAxis1);


var lineFunction = d3.line()
  .x(function(d) {
    return x(d.x);
  })
  .y(function(d) {
    return y(d.y);
  })
  .curve(d3.curveLinear);

var lineFunctionOne = d3.line()
  .x(function(d) {
    return x(d.x);
  })
  .y(function(d) {
    return y1(d.y1);
  })
  .curve(d3.curveLinear);

//defining the lines
var path = g.append("path")
  .attr("class", "path1")
  .attr("id", "blueLine")
  .attr("d", lineFunction(data))
  .attr("stroke", "blue")
  .attr("stroke-width", 2)
  .attr("fill", "none")
  .attr("clip-path", "url(#clip)");

var path1 = g.append("path")
  .attr("class", "path2")
  .attr("id", "redLine")
  .attr("d", lineFunctionOne(data))
  .attr("stroke", "red")
  .attr("stroke-width", 2)
  .attr("fill", "none")
  .attr("clip-path", "url(#clip)");

//************* Zoom ***************
//add brushing
var brush = d3.brush().extent([
    [0, 0],
    [width, height]
  ]).on("end", brushended),
  idleTimeout,
  idleDelay = 350;
g.append("g")
  .attr("class", "brush")
  .call(brush);

// Add a clipPath: everything out of this area won't be drawn when chart is zoomed in
var clip = svg.append("defs").append("svg:clipPath")
  .attr("id", "clip")
  .append("svg:rect")
  .attr("width", width)
  .attr("height", height)
  .attr("x", 0)
  .attr("y", 0);

function brushended() {

  var s = d3.event.selection;
  //If no selection, re-initialize chart on double click. Otherwise, update x-axis and y-axis domain
  if (!s) {
    // This allows to wait a little bit
    if (!idleTimeout) return idleTimeout = setTimeout(idled, 350);
    x.domain(d3.extent(data, function(d) {
      return d.x;
    })).nice();
    y.domain(d3.extent(data, function(d) {
      return d.y;
    })).nice();
    y1.domain(d3.extent(data, function(d) {
      return d.y1;
    })).nice();
  } else {
    x.domain([s[0][0], s[1][0]].map(x.invert, x));
    y.domain([s[0][1], s[1][1]].map(y.invert, y));
    y1.domain([s[0][1], s[1][1]].map(y1.invert, y)); //hardcoded domain
    //This removes the grey brush area as soon as the selection has been done
    g.select(".brush").call(brush.move, null)
  }
  zoom();
}

function idled() {
  idleTimeout = null;
}

function zoom() {
  var t = svg.transition().duration(750);
  svg.select(".x.axis").transition(t).call(xAxis);
  svg.select(".y.axis").transition(t).call(yAxis);
  svg.select(".y1.axis").transition(t).call(yAxis1);
  svg.select(".path1").transition(t).attr("d", lineFunction(data));
  svg.select(".path2").transition(t).attr("d", lineFunctionOne(data));
}
.xy_chart {
  position: relative;
  left: 50px
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg class="xy_chart"></svg>

Related