1

I'm working with D3js v4. What I'm trying to achieve is to combine brush & zoom behaviour according to this example with click-to-recenter brush where after click the brush gets recentered and the brush boundarys are rounded with a smooth transition. Here is my fiddle so far.

My problem is that the function brushended never gets executed. It seems like the zoom prevents the brush from receiving mouseup events. Only when zoom is completely disabled by commenting out all zoom functions I get brushended working. I tried event.stopPropagation and event.stopImmediatePropagation on mousedown like in the following code snippet to prevent the zoom from receiving mousedown and mouseup events but it did not work.

context.append("g")
  .attr("class", "brush")
  .call(brush)
  .call(brush.move, [x2(new Date(2013, 0, 1)), x2(new Date(2013, 6, 1))])
  .selectAll(".overlay")
  .each(function(d) {
    d.type = "selection";
  })
  .on("mousedown touchstart", function() { d3.event.stopPropagation(); })
  .on("mousedown touchstart", brushcentered)

Do I placed stopPropagation in the wrong place or am I completely missing something? Any help on this would be appreciated.

StefanS
  • 13
  • 4
  • I get both events firing: https://jsfiddle.net/u9Lbm0cy/30/ – Mark Jul 21 '17 at 23:01
  • 1
    @Mark: You are right, but I forget to enable zoom by adding the line _d3.event.sourceEvent.type === "brush"_ in the zoomend function, sorry! Now with this line added zoomend is working and brushend is fired but rounding and transition were not executed. Nothing happens on brushend. I corrected my fiddle to illustrate the problem: https://jsfiddle.net/u9Lbm0cy/34/. – StefanS Jul 22 '17 at 14:18

1 Answers1

0

Whoa! You got a lot of event handling going on here. Let's break this down, first, it's this call:

 svg.select(".zoom").call(zoom.transform, d3.zoomIdentity
   .scale(width / (s[1] - s[0]))
   .translate(-s[0], 0));

in your brushed event handler that's eating your brushend. My first thought was to use the simple setTimeout hack to allow the brushend to process. While this worked, it just lead a new problem, the brush.move in the brushedend was then being raised as a zoom event and not being process in brushed. So, instead, I simply replace the move with a zoom and let the zoom event handle the brush positioning like:

function brushended() {
  if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") return; // ignore brush-by-zoom
  if (!d3.event.selection) return; // Ignore empty selections.

  var d0 = d3.event.selection.map(x2.invert), //=Datum, linker+rechter Brush-Rand
    d1 = d0.map(d3.timeMonth.round);

  // If empty when rounded, use floor & ceil instead.
  if (d1[0] >= d1[1]) {
    d1[0] = d3.timeMonth.floor(d0[0]);
    d1[1] = d3.timeMonth.offset(d1[0]);
  };

  var s = d1.map(x2);
  setTimeout(function(){
    svg.select(".zoom").call(zoom.transform, d3.zoomIdentity
      .scale(width / (s[1] - s[0]))
      .translate(-s[0], 0));
  });
}

I think this is producing the behavior you are looking for.

Running code below and plunker here:

<!DOCTYPE html>
<html>

<head>
  <script src="https://d3js.org/d3.v4.js"></script>
  <style>
    #Type1 {
      stroke: DarkTurquoise;
    }
    
    #Type2 {
      stroke: steelblue;
    }
    
    .data-line {
      fill: none;
      stroke-width: 2.5;
      opacity: 0.8;
      stroke-linecap: round;
      clip-path: url(#clip);
      transition: 0.5s;
    }
    
    .context-line {
      fill: none;
      stroke-width: 1.5;
      stroke: grey;
      stroke-linecap: round;
      clip-path: url(#clip);
    }
    
    div.svg-container {
      display: inline-block;
      position: relative;
      width: 100%;
      height: 100%;
      vertical-align: top;
    }
    
    .zoom {
      cursor: move;
      fill: none;
      pointer-events: all;
    }
  </style>
</head>

<body>
    <div>
  </div>
  <script>
    var svg = d3.select("div")
      .append("div").classed("svg-container", true)
      .append("svg")
      .attr("width", 700)
      .attr("height", 400)

    var margin = {
        top: 20,
        right: 20,
        bottom: 130,
        left: 60
      },
      margin2 = {
        top: 300,
        right: 20,
        bottom: 30,
        left: 60
      },
      width = +svg.attr("width") - margin.left - margin.right,
      height = +svg.attr("height") - margin.top - margin.bottom,
      height2 = +svg.attr("height") - margin2.top - margin2.bottom;

    //    Achsen Datumsbehandlung
    var parseDate = d3.timeParse("%Y-%m-%d %H:%M:%S");

    var x = d3.scaleTime().range([0, width]),
      x2 = d3.scaleTime().range([0, width]),
      y = d3.scaleLinear().range([height, 0]);
    y2 = d3.scaleLinear().range([height2, 0]);

    var xAxis = d3.axisBottom(x)
    xAxis2 = d3.axisBottom(x2),
      yAxis = d3.axisLeft(y);

    //Linien
    var line = d3.line()
      .x(function(d) {
        return x(d.Datum);
      })
      //    .y0(height) //nur area-chType
      .y(function(d) {
        return y(d.Summe);
      });
    //    .y1(function(d) { return y(d.Summe); }); //nur area-chType

    //Linien
    var lineType1 = d3.line()
      .curve(d3.curveStepAfter)
      .x(function(d) {
        return x(d.Datum);
      })
      .y(function(d) {
        return y(d.Summe);
      });

    var line2 = d3.line()
      .x(function(d) {
        return x2(d.Datum);
      })
      .y(function(d) {
        return y2(d.Summe);
      });

    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 + ")");

    //Zoom & Brush
    var brush = d3.brushX()
      .extent([
        [0, 0],
        [width, height2]
      ])
      .handleSize(10)
      .on("start brush", brushed)
      .on("end", brushended);

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

    // ===Daten===
    var data = [{
      "Datum": "2013-02-04 00:00:00",
      "Summe": "1000.00",
      "Type": "Type1",
      "Notizen": null
    }, {
      "Datum": "2013-02-04 00:00:00",
      "Summe": "200.00",
      "Type": "Type2",
      "Notizen": null
    }, {
      "Datum": "2013-02-21 00:00:00",
      "Summe": "4000.00",
      "Type": "Type1",
      "Notizen": null
    }, {
      "Datum": "2013-02-23 00:00:00",
      "Summe": "2000.00",
      "Type": "Type1",
      "Notizen": null
    }, {
      "Datum": "2013-02-23 00:00:00",
      "Summe": "601.00",
      "Type": "Type2",
      "Notizen": null
    }, {
      "Datum": "2013-03-04 00:00:00",
      "Summe": "775.00",
      "Type": "Type1",
      "Notizen": null
    }, {
      "Datum": "2013-03-04 00:00:00",
      "Summe": "1395.10",
      "Type": "Type2",
      "Notizen": null
    }, {
      "Datum": "2013-04-03 00:00:00",
      "Summe": "400.00",
      "Type": "Type1",
      "Notizen": null
    }, {
      "Datum": "2013-04-03 00:00:00",
      "Summe": "1040.00",
      "Type": "Type2",
      "Notizen": null
    }, {
      "Datum": "2013-05-24 00:00:00",
      "Summe": "400.00",
      "Type": "Type1",
      "Notizen": null
    }, {
      "Datum": "2013-05-24 00:00:00",
      "Summe": "3288.88",
      "Type": "Type2",
      "Notizen": null
    }, {
      "Datum": "2013-05-28 00:00:00",
      "Summe": "400.00",
      "Type": "Type1",
      "Notizen": null
    }, {
      "Datum": "2013-05-28 00:00:00",
      "Summe": "4407.10",
      "Type": "Type2",
      "Notizen": null
    }, {
      "Datum": "2013-06-01 00:00:00",
      "Summe": "400.00",
      "Type": "Type1",
      "Notizen": null
    }, {
      "Datum": "2013-06-01 00:00:00",
      "Summe": "3525.86",
      "Type": "Type2",
      "Notizen": null
    }, {
      "Datum": "2013-06-04 00:00:00",
      "Summe": "400.00",
      "Type": "Type1",
      "Notizen": null
    }, {
      "Datum": "2013-06-04 00:00:00",
      "Summe": "2990.17",
      "Type": "Type2",
      "Notizen": null
    }, {
      "Datum": "2013-06-10 00:00:00",
      "Summe": "390.00",
      "Type": "Type1",
      "Notizen": null
    }, {
      "Datum": "2013-06-10 00:00:00",
      "Summe": "366.00",
      "Type": "Type2",
      "Notizen": null
    }, {
      "Datum": "2013-06-14 00:00:00",
      "Summe": "390.00",
      "Type": "Type1",
      "Notizen": null
    }, {
      "Datum": "2013-06-14 00:00:00",
      "Summe": "925.18",
      "Type": "Type2",
      "Notizen": null
    }, {
      "Datum": "2013-06-16 00:00:00",
      "Summe": "708.44",
      "Type": "Type1",
      "Notizen": null
    }, {
      "Datum": "2013-06-16 00:00:00",
      "Summe": "609.10",
      "Type": "Type2",
      "Notizen": null
    }, {
      "Datum": "2013-06-20 00:00:00",
      "Summe": "708.44",
      "Type": "Type1",
      "Notizen": null
    }, {
      "Datum": "2013-06-20 00:00:00",
      "Summe": "1760.80",
      "Type": "Type2",
      "Notizen": null
    }]

    data.forEach(function(d) {
      d.Datum = parseDate(d.Datum);
      d.Summe = +d.Summe;
    });

    x.domain([d3.min(data, function(d) {
      return d.Datum;
    }), d3.max(data, function(d) {
      return d.Datum;
    })]).nice(d3.timeYear);
    y.domain([d3.min(data, function(d) {
      return d.Summe;
    }), 1.1 * d3.max(data, function(d) {
      return d.Summe;
    })]).nice();
    x2.domain(x.domain());
    y2.domain(y.domain());

    var dataGroup = d3.nest()
      .key(function(d) {
        return d.Type;
      })
      .entries(data);

    focus.append("rect")
      .attr("class", "zoom")
      .attr("width", width)
      .attr("height", height)
       .call(zoom);

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

    focus.append("g")
      .attr("class", "axis axis--y")
      .call(yAxis);

    focus.selectAll("path#Type2")
      .data(dataGroup)
      .enter()
      .append("path")
      .filter(function(d) {
        return d.key == "Type2"
      })
      .attr("class", "data-line")
      .attr("d", function(d) {
        return line(d.values);
      })
      .attr("id", function(d) {
        return d.key
      });

    focus.selectAll("path#Type1")
      .data(dataGroup)
      .enter()
      .append("path")
      .filter(function(d) {
        return d.key == "Type1"
      })
      .attr("class", "data-line")
      .attr("d", function(d) {
        return lineType1(d.values);
      })
      .attr("id", function(d) {
        return d.key
      });

    context.selectAll(".context-line")
      .data(dataGroup)
      .enter()
      .append("path")
      .attr("class", "context-line")
      .attr("d", function(d) {
        return line2(d.values);
      });

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

    context.append("g")
      .attr("class", "brush")
      .call(brush)
      .call(brush.move, [x2(new Date(2013, 0, 1)), x2(new Date(2013, 6, 1))])
      .selectAll(".overlay")
      .each(function(d) {
        d.type = "selection";
      })
      .on("mousedown touchstart", brushcentered);

    // ===Funktionen===

    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("path#Type1").attr("d", function(d) {
        return lineType1(d.values);
      });
      focus.selectAll("path#Type2").attr("d", function(d) {
        return line(d.values);
      });
      focus.select(".axis--x").call(xAxis);
      
      setTimeout(function(){
        svg.select(".zoom").call(zoom.transform, d3.zoomIdentity
          .scale(width / (s[1] - s[0]))
          .translate(-s[0], 0));
      });
    }

    function brushcentered() {
      var dx = 30, // Use a fixed width when recentering, = ca 2 Monate
        cx = d3.mouse(this)[0],
        x0 = cx - dx / 2,
        x1 = cx + dx / 2;
      d3.select(this.parentNode).call(brush.move, x1 > width ? [width - dx, width] : x0 < 0 ? [0, dx] : [x0, x1]);
    }

    function brushended() {
      if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") return; // ignore brush-by-zoom
      if (!d3.event.selection) return; // Ignore empty selections.

      var d0 = d3.event.selection.map(x2.invert), //=Datum, linker+rechter Brush-Rand
        d1 = d0.map(d3.timeMonth.round);

      // If empty when rounded, use floor & ceil instead.
      if (d1[0] >= d1[1]) {
        d1[0] = d3.timeMonth.floor(d0[0]);
        d1[1] = d3.timeMonth.offset(d1[0]);
      };

      var s = d1.map(x2);
      setTimeout(function(){
        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;
      var t = d3.event.transform;
      x.domain(t.rescaleX(x2).domain());
      focus.selectAll("path#Type1").attr("d", function(d) {
        return lineType1(d.values)
      });
      focus.selectAll("path#Type2").attr("d", function(d) {
        return line(d.values)
      });
      focus.select(".axis--x").call(xAxis);
      context.select(".brush").call(brush.move, x.range().map(t.invertX, t));
    }
  </script>
</body>

</html>
Mark
  • 106,305
  • 20
  • 172
  • 230
  • Thank you, it works! I never thought it would be that hard for me to find a solution. I was on the wrong track entirely. – StefanS Jul 23 '17 at 18:42