Search code examples
d3.jszoomingbrush

d3 using zoom and brush on a bar chart


Hi I am trying to adapt Matthew Izanuk's Unit Bar Chart with Brush and Zoom to a randomly generated bar chart I have already created.

I get an error with the zoom function, on line 261

enter image description here

line 261 is

  x.domain(t.rescaleX(x2).domain());

Here is the code and jsFiddle

<!DOCTYPE html>

<body>
  <style>
    div {
      display: inline-block;
      vertical-align: top;
    }

    #bar_chart {
      border: 2px solid lightgray;
      border-radius: 15px;
    }

    #json {
      max-height: 600px;
      width: 200px;
      overflow: scroll;
      border: 2px solid gray;
      border-radius: 15px;
    }

    .zoom {
      cursor: move;
      fill: none;
      pointer-events: all;
    }
  </style>
  <div id="bar_chart">
  </div>

  <div id="json"></div>
  <script src="https://d3js.org/d3.v4.min.js"></script>
  <script>
    //************* generate data ************
    var data = [];
    var space = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    var spaceLength = space.length;
    makedata();

    function makedata() {
      var obj = {};

      for (var i = 0; i < spaceLength; i++) {
        obj = {};
        value = Math.floor(Math.random() * 500);
        rand = Math.floor(Math.random() * space.length)
        name = space.charAt(rand);
        obj["name"] = name;
        obj["val"] = value;
        data.push(obj);
        space = space.slice(0, rand) + space.slice(rand + 1, space.length)
      }
    }

    // To display json in html page
    document.getElementById("json").innerHTML = "<pre>" + JSON.stringify(data, null, 4) + "</pre>";

    var margin = {
        top: 50,
        right: 20,
        bottom: 90,
        left: 50
      },
      margin2 = {
        top: 530,
        right: 20,
        bottom: 30,
        left: 50
      },
      width = 1000 - margin.left - margin.right,
      height = 600 - margin.top - margin.bottom,
      height2 = 600 - margin2.top - margin2.bottom;

    var x = d3.scaleBand()
      .domain(data.map(function(d) {
        return d.name
      }))
      .range([0, width]);

    var x2 = d3.scaleBand()
      .domain(data.map(function(d) {
        return d.name
      }))
      .range([0, width]);

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

    var y2 = d3.scaleLinear()
      .domain([0, d3.max(data, function(d) {
        return d.val
      })])
      .range([height2, 0]);

    var brush = d3.brushX()
      .extent([
        [0, 0],
        [width, height2]
      ])
      .on("brush", brushed);

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

    var svg = d3.select("#bar_chart")
      // .data(data)
      .append("svg")
      .attr("width", width + margin.left + margin.right)
      .attr("height", height + margin.top + margin.bottom);

    // .append("g")
    // .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    svg.append("defs").append("clipPath")
      .attr("id", "clip")
      .append("rect")
      .attr("width", width)
      .attr("height", height);

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

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

    var g0 = focus.append("g")
      .attr("class", "focus")
      .attr("transform", "translate(0,0)");

    var xAxis = d3.axisBottom(x);
    var xAxis2 = d3.axisBottom(x2);

    focus.append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + height + ")")
      .call(xAxis);

    // Add the Y Axis
    focus.append("g")
      .attr("class", "axis")
      .call(d3.axisLeft(y)
        .ticks(7));

    var tooltip = d3.select("#info")
      .append("div")
      .style("position", "absolute")
      .style("z-index", "10")
      .style("visibility", "hidden");

    svg.append("rect")
      .attr("class", "zoom")
      .attr("width", width)
      .attr("height", height)
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
      .call(zoom);

    var focus_group = focus.append("g");
    focus_group.attr("clip-path", "url(#clip)");

    var rects = focus_group.selectAll('rect')
      .data(data);

    //********* Bar Chart 1 ****************
    var newRects1 = rects.enter();

    newRects1.append('rect')
      .attr('class', 'bar mainBars')
      .attr('x', function(d, i) {
        return x(d.name);
      })
      .attr('y', function(d, i) {
        return y(d.val);
      })
      .attr('height', function(d, i) {
        return height - y(d.val)
      })
      .attr('opacity', 0.85)
      .attr('width', 10)
      .attr("transform", "translate(" + 4 + ",0)")
      .style('fill', 'lightblue')
      .style('stroke', 'gray');

    var focus_group = context.append("g");
    focus_group.attr("clip-path", "url(#clip)");

    var brushRects = focus_group.selectAll('rect')
      .data(data);

    //********* Brush Bar Chart ****************
    var brushRects1 = brushRects.enter();

    brushRects1.append('rect')
      .attr('class', 'bar mainBars')
      .attr('x', function(d, i) {
        return x2(d.name);
      })
      .attr('y', function(d, i) {
        return y2(d.val);
      })
      .attr('height', function(d, i) {
        return height2 - y2(d.val)
      })
      .attr('opacity', 0.85)
      .attr('width', 10)
      .attr("transform", "translate(" + 4 + ",0)")
      .style('fill', 'lightblue')
      .style('stroke', 'gray');

    //append brush xAxis2
    context.append("g")
      .attr("class", "axis x-axis")
      .attr("transform", "translate(0," + height2 + ")")
      .call(xAxis2);

    context.append("g")
      .attr("class", "brush")
      .call(brush)
      .call(brush.move, x.range());

    //create brush function redraw scatterplot with selection
    function brushed() {
      if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") return; // ignore brush-by-zoom
      var s = d3.event.selection || x2.range();
      x.domain(s.map(x2.invert, x2));
      focus.selectAll(".mainBars")
        .attr("x", function(d) {
          return x(d.name);
        })
        .attr("y", function(d) {
          return y(d.val);
        })
        .attr('height', function(d, i) {
          return height - y(d.val)
        })
        .attr('opacity', 0.85)
        .attr('width', 10);
      focus.select(".x-axis").call(xAxis);
      svg.select(".zoom").call(zoom.transform, d3.zoomIdentity
        .scale(width / (s[1] - s[0]))
        .translate(-s[0], 0));
    }

    function zoomed() {
      if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") return; // ignore zoom-by-brush
      var t = d3.event.transform;
      x.domain(t.rescaleX(x2).domain());
      focus.selectAll(".mainBars")
        .attr("x", function(d) {
          return x(d.name);
        })
        .attr("y", function(d) {
          return y(d.val);
        })
        .attr('height', function(d, i) {
          return height - y(d.val)
        })
        .attr('opacity', 0.85)
        .attr('width', 10);
      focus.select(".x-axis").call(xAxis);
      context.select(".brush").call(brush.move, x.range().map(t.invertX, t));
    }
  </script>

Any help would be greatly appreciated,

thanks


Solution

  • As mentioned in the comments, invert only exists for continuous ranges. Your scaleBand is a ordinal scale composed of discreet values. One way I've worked around this is to simply figure which values in my domain fall in the selection:

    function brushed() {
      if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") return; 
      // ignore brush-by-zoom
    
      // get bounds of selection
      var s = d3.event.selection,
          nD = [];
    
      // for each "tick" in our domain is it in selection
      x2.domain().forEach((d)=>{
        var pos = x2(d) + x2.bandwidth()/2;
        if (pos > s[0] && pos < s[1]){
          nD.push(d);
        }
      });
    
      // set new domain
      x.domain(nD);
    
      // redraw
      focus.selectAll(".mainBars")
        // hide bars not in domain
        .style("opacity", function(d){
          return x.domain().indexOf(d.name) === -1 ? 0 : 100;
        })
        .attr("x", function(d) {
          return x(d.name)+ x.bandwidth()/2 - 5;
        })
        .attr("y", function(d) {
          return y(d.val);
        })
        .attr('height', function(d, i) {
          return height - y(d.val)
        })
        .attr('opacity', 0.85)
        .attr('width', 10);
    
    
      focus.select(".x.axis").call(xAxis);
    
      svg.select(".zoom").call(zoom.transform, d3.zoomIdentity
        .scale(width / (s[1] - s[0]))
        .translate(-s[0], 0));
    }
    

    Running code:

    <!DOCTYPE html>
    
    <head>
      <style>
        div {
          display: inline-block;
          vertical-align: top;
        }
        
        #bar_chart {
          border: 2px solid lightgray;
          border-radius: 15px;
        }
        
        #json {
          max-height: 600px;
          width: 200px;
          overflow: scroll;
          border: 2px solid gray;
          border-radius: 15px;
        }
        
        .zoom {
          cursor: move;
          fill: none;
          pointer-events: all;
        }
      </style>
    </head>
    
    <body>
      <div id="bar_chart">
      </div>
    
      <div id="json"></div>
      <script src="https://d3js.org/d3.v4.min.js"></script>
      <script>
        //************* generate data ************
        var data = [];
        var space = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
        var spaceLength = space.length;
        makedata();
    
        function makedata() {
          var obj = {};
    
          for (var i = 0; i < spaceLength; i++) {
            obj = {};
            value = Math.floor(Math.random() * 500);
            rand = Math.floor(Math.random() * space.length)
            name = space.charAt(rand);
            obj["name"] = name;
            obj["val"] = value;
            data.push(obj);
            space = space.slice(0, rand) + space.slice(rand + 1, space.length)
          }
        }
    
        // To display json in html page
        document.getElementById("json").innerHTML = "<pre>" + JSON.stringify(data, null, 4) + "</pre>";
    
        var margin = {
            top: 50,
            right: 20,
            bottom: 90,
            left: 50
          },
          margin2 = {
            top: 530,
            right: 20,
            bottom: 30,
            left: 50
          },
          width = 1000 - margin.left - margin.right,
          height = 600 - margin.top - margin.bottom,
          height2 = 600 - margin2.top - margin2.bottom;
    
        var x = d3.scaleBand()
          .domain(data.map(function(d) {
            return d.name
          }))
          .range([0, width]);
    
        var x2 = d3.scaleBand()
          .domain(data.map(function(d) {
            return d.name
          }))
          .range([0, width]);
    
        var y = d3.scaleLinear()
          .domain([0, d3.max(data, function(d) {
            return d.val
          })])
          .range([height, 0]);
    
        var y2 = d3.scaleLinear()
          .domain([0, d3.max(data, function(d) {
            return d.val
          })])
          .range([height2, 0]);
    
        var brush = d3.brushX()
          .extent([
            [0, 0],
            [width, height2]
          ])
          .on("brush", brushed);
    
        var zoom = d3.zoom()
          .scaleExtent([1, Infinity])
          .translateExtent([
            [0, 0],
            [width, height]
          ])
          .extent([
            [0, 0],
            [width, height]
          ])
          .on("zoom", zoomed);
    
        var svg = d3.select("#bar_chart")
          // .data(data)
          .append("svg")
          .attr("width", width + margin.left + margin.right)
          .attr("height", height + margin.top + margin.bottom);
    
        // .append("g")
        // .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
    
        svg.append("defs").append("clipPath")
          .attr("id", "clip")
          .append("rect")
          .attr("width", width)
          .attr("height", height);
    
        var focus = svg.append("g")
          .attr("class", "focus")
          .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
    
        var context = svg.append("g")
          .attr("class", "context")
          .attr("transform", "translate(" + margin2.left + "," + margin2.top + ")");
    
        var xAxis = d3.axisBottom(x);
        var xAxis2 = d3.axisBottom(x2);
    
        focus.append("g")
          .attr("class", "x axis")
          .attr("transform", "translate(0," + height + ")")
          .call(xAxis);
    
        // Add the Y Axis
        focus.append("g")
          .attr("class", "axis")
          .call(d3.axisLeft(y)
            .ticks(7));
    
        var tooltip = d3.select("#info")
          .append("div")
          .style("position", "absolute")
          .style("z-index", "10")
          .style("visibility", "hidden");
    
        svg.append("rect")
          .attr("class", "zoom")
          .attr("width", width)
          .attr("height", height)
          .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
          .call(zoom);
    
        var focus_group = focus.append("g");
        focus_group.attr("clip-path", "url(#clip)");
    
        var rects = focus_group.selectAll('rect')
          .data(data);
    
        //********* Bar Chart 1 ****************
        var newRects1 = rects.enter();
    
        newRects1.append('rect')
          .attr('class', 'bar mainBars')
          .attr('x', function(d, i) {
            return x(d.name) + x.bandwidth()/2;
          })
          .attr('y', function(d, i) {
            return y(d.val);
          })
          .attr('height', function(d, i) {
            return height - y(d.val)
          })
          .attr('opacity', 0.85)
          .attr('width', 10)
          .style('fill', 'lightblue')
          .style('stroke', 'gray');
    
        var focus_group = context.append("g");
        focus_group.attr("clip-path", "url(#clip)");
    
        var brushRects = focus_group.selectAll('rect')
          .data(data);
    
        //********* Brush Bar Chart ****************
        var brushRects1 = brushRects.enter();
    
        brushRects1.append('rect')
          .attr('class', 'bar mainBars')
          .attr('x', function(d, i) {
            return x2(d.name);
          })
          .attr('y', function(d, i) {
            return y2(d.val);
          })
          .attr('height', function(d, i) {
            return height2 - y2(d.val)
          })
          .attr('opacity', 0.85)
          .attr('width', 10)
          .attr("transform", "translate(" + 4 + ",0)")
          .style('fill', 'lightblue')
          .style('stroke', 'gray');
    
        //append brush xAxis2
        context.append("g")
          .attr("class", "axis x-axis")
          .attr("transform", "translate(0," + height2 + ")")
          .call(xAxis2);
    
        context.append("g")
          .attr("class", "brush")
          .call(brush)
          .call(brush.move, x.range());
    
        //create brush function redraw scatterplot with selection
        function brushed() {
          if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") return; // ignore brush-by-zoom
    
          // get bounds of selection
          var s = d3.event.selection,
              nD = [];
          x2.domain().forEach((d)=>{
            var pos = x2(d) + x2.bandwidth()/2;
            if (pos > s[0] && pos < s[1]){
              nD.push(d);
            }
          });
          
          x.domain(nD);
          
          focus.selectAll(".mainBars")
            .style("opacity", function(d){
              return x.domain().indexOf(d.name) === -1 ? 0 : 100;
            })
            .attr("x", function(d) {
              return x(d.name)+ x.bandwidth()/2 - 5;
            })
            .attr("y", function(d) {
              return y(d.val);
            })
            .attr('height', function(d, i) {
              return height - y(d.val)
            })
            .attr('opacity', 0.85)
            .attr('width', 10);
    
          
          focus.select(".x.axis").call(xAxis);
          
          svg.select(".zoom").call(zoom.transform, d3.zoomIdentity
            .scale(width / (s[1] - s[0]))
            .translate(-s[0], 0));
        }
    
        function zoomed() {
          /*
          if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") return; // ignore zoom-by-brush
          var t = d3.event.transform;
          
          x.domain(t.rescaleX(x2).domain());
          focus.selectAll(".mainBars")
            .attr("x", function(d) {
              return x(d.name) + x.bandwidth()/2;
            })
            .attr("y", function(d) {
              return y(d.val);
            })
            .attr('height', function(d, i) {
              return height - y(d.val)
            })
            .attr('opacity', 0.85)
            .attr('width', 10);
          focus.select(".x-axis").call(xAxis);
          context.select(".brush").call(brush.move, x.range().map(t.invertX, t));
          */
        }
      </script>