user5072412
user5072412

Reputation: 187

D3 Brush and Zoom error

I am trying to replicate the following brush and zoom example with a different display: https://bl.ocks.org/mbostock/34f08d5e11952a80609169b7917d4172

Rather than display the Path I would like to display multiple horizontal lines. To an extent I have the program working, but unfortunately in the process of displaying the lines I have broken the zoom. I would appreciate it if someone could point me in the right direction (and also confirm that I've amended the code in the correct way). I've looked at other examples on Stackoverflow, but I don't think my knowledge of D3 is good enough to determine the solution from other peoples problems.

Here's what I have so far:

    <!DOCTYPE html>
<meta charset="utf-8">
<style>

.zoom {
  cursor: move;
  fill: none;
  pointer-events: all;
}

.TIMELINE {
  stroke-width : 2px;
  stroke-opacity:0.5;
}

.TIMELINE2 {
  stroke-width : 0.5px;
  stroke-opacity:0.5;
}

</style>
<svg width="1200" height="600"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<!--script src="./data3.js"></script-->
<script>

const data={"summary": [
      {
      "SheetName":  "Line 1",
      "N" : "3922 ",
      "Min" : "19990508 ",
      "Max" : "20180905 ",
      "Range" : "19.34 "
      },
      {
      "SheetName":  "Line 2",
      "N" : "482 ",
      "Min" : "20080215 ",
      "Max" : "20180720 ",
      "Range" : "10.41 "
      },
      {
      "SheetName":  "Line 3",
      "N" : "77 ",
      "Min" : "20080917 ",
      "Max" : "20150514 ",
      "Range" : "6.66 "
      },
      {
      "SheetName":  "Line 4",
      "N" : "60 ",
      "Min" : "20090325 ",
      "Max" : "20171205 ",
      "Range" : "8.76 "
      }],
      "data" :[
      {
      "DMY" : "19990101 ",
      },
      {
      "DMY" : "20190101 ",
      }
]};

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

var parseDate = d3.timeParse("%b %Y");

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

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

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

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

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

//------------------------------------;

  x.domain(d3.extent(data.data, d=> parse(d.DMY)));
  y.domain(data.summary.map(d => d.SheetName));
  x2.domain(x.domain());
  y2.domain(y.domain());

  focus.append("line")
       .datum(data.summary)
       .attr("class", "TIMELINE")
       .attr("stroke", "black");

  focus.selectAll(".TIMELINE")
                       .data(data.summary)
                       .enter()
                       .append("line")
                       .attr("class", "TIMELINE axis--x")
                       .attr("stroke", "black")
                       .attr("x1", d => x(parse(d.Min)))
                       .attr("y1", d => y(d.SheetName))
                       .attr("x2", d => x(parse(d.Max)))
                       .attr("y2", d => y(d.SheetName))      

  context.selectAll(".TIMELINE2")
                       .data(data.summary)
                       .enter()
                       .append("line")
                       .attr("class", "TIMELINE2 axis--x")
                       .attr("stroke", "black")
                       .attr("x1", d => x(parse(d.Min)))
                       .attr("y1", d => y2(d.SheetName))
                       .attr("x2", d => x(parse(d.Max)))
                       .attr("y2", d => y2(d.SheetName))      

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

  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, x.range());

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

//-----------------------------------------;

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(".TIMELINE")
       .attr("x1", d => x(Math.max(parse(d.Min), x.domain()[0])))  
       .attr("x2", d => x(Math.min(parse(d.Max), x.domain()[1])))  ; 
  focus.select(".axis--x").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(".TIMELINE2")
  focus.select(".axis--x").call(xAxis);
  context.select(".brush").call(brush.move, x.range().map(t.invertX, t));
}

function type(d) {
  d.date = parseDate(d.date);
  d.price = +d.price;
  return d;
}

function parse(str) {
  var y = str.substr(0,4),
      m = str.substr(4,2) - 1,
      d = str.substr(6,2);
  return new Date(y,m,d);
}


</script>

Upvotes: 1

Views: 422

Answers (1)

Shashank
Shashank

Reputation: 5670

Few things missing:

  1. axis--x class is being assigned to the actual x axis <g> AND all the lines as well and hence the redrawing of the axes in brushing doesn't work. Fixed this the following way:

    .append("line")
    .attr("class", "TIMELINE")
    

    AND in the brushed function,

    focus.select("g.axis--x").call(xAxis);
    
  2. You're setting the new x domain in the brushed function. So you just got to use it for setting the lines just like you do in the initial phase.

    focus.selectAll(".TIMELINE")
       .attr("x1", d => x(parse(d.Min)))  
       .attr("x2", d => x(parse(d.Max)));
    
  3. The above line rendering was missing in the zoomed function. Added that as well.

  4. Clip-path was being added to the SVG but was never being used. Added it (to both focus and context lines).

    .append("line")
    .attr("class", "TIMELINE")
    .style("stroke", "black")
    .style("clip-path", "url(#clip)")  
    
  5. Aligned the lines according to the y axis bandwidth (to center them by changing y1 and y2 for both focus and context):

    .attr("x1", d => x(parse(d.Min)))
    .attr("y1", d => y(d.SheetName) + y.bandwidth()/2)
    .attr("x2", d => x(parse(d.Max)))
    .attr("y2", d => y(d.SheetName) + y.bandwidth()/2)
    

With the above changes, here's a working snippet:

const data={"summary": [
      {
      "SheetName":  "Line 1",
      "N" : "3922 ",
      "Min" : "19990508 ",
      "Max" : "20180905 ",
      "Range" : "19.34 "
      },
      {
      "SheetName":  "Line 2",
      "N" : "482 ",
      "Min" : "20080215 ",
      "Max" : "20180720 ",
      "Range" : "10.41 "
      },
      {
      "SheetName":  "Line 3",
      "N" : "77 ",
      "Min" : "20080917 ",
      "Max" : "20150514 ",
      "Range" : "6.66 "
      },
      {
      "SheetName":  "Line 4",
      "N" : "60 ",
      "Min" : "20090325 ",
      "Max" : "20171205 ",
      "Range" : "8.76 "
      }],
      "data" :[
      {
      "DMY" : "19990101 ",
      },
      {
      "DMY" : "20190101 ",
      }
]};

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

var parseDate = d3.timeParse("%b %Y");

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

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

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

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

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

//------------------------------------;

  x.domain(d3.extent(data.data, d=> parse(d.DMY)));
  y.domain(data.summary.map(d => d.SheetName));
  x2.domain(x.domain());
  y2.domain(y.domain());

/*   focus.append("line")
       .datum(data.summary)
       .attr("class", "TIMELINE")
       .attr("stroke", "black"); */

  focus.selectAll(".TIMELINE")
                       .data(data.summary)
                       .enter()
                       .append("line")
                       .attr("class", "TIMELINE")
                       .style("stroke", "black")
                       .style("clip-path", "url(#clip)")                       
                       .attr("x1", d => x(parse(d.Min)))
                       .attr("y1", d => y(d.SheetName) + y.bandwidth()/2)
                       .attr("x2", d => x(parse(d.Max)))
                       .attr("y2", d => y(d.SheetName) + y.bandwidth()/2)      

  context.selectAll(".TIMELINE2")
                       .data(data.summary)
                       .enter()
                       .append("line")
                       .attr("class", "TIMELINE2 axis--x")
                       .style("stroke", "black")
                       .style("clip-path", "url(#clip)")                                              
                       .attr("x1", d => x(parse(d.Min)))
                       .attr("y1", d => y2(d.SheetName) + y2.bandwidth()/2)
                       .attr("x2", d => x(parse(d.Max)))
                       .attr("y2", d => y2(d.SheetName) + y2.bandwidth()/2)      

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

  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, x.range());

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

//-----------------------------------------;

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(".TIMELINE")
       .attr("x1", d => x(parse(d.Min)))  
       .attr("x2", d => x(parse(d.Max)));
  focus.select("g.axis--x").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(".TIMELINE")
       .attr("x1", d => x(parse(d.Min)))  
       .attr("x2", d => x(parse(d.Max)));
  focus.select("g.axis--x").call(xAxis);
  context.select(".brush").call(brush.move, x.range().map(t.invertX, t));
}

function type(d) {
  d.date = parseDate(d.date);
  d.price = +d.price;
  return d;
}

function parse(str) {
  var y = str.substr(0,4),
      m = str.substr(4,2) - 1,
      d = str.substr(6,2);
  return new Date(y,m,d);
}
.zoom {
  cursor: move;
  fill: none;
  pointer-events: all;
}

.TIMELINE {
  stroke-width : 2px;
  stroke-opacity:0.5;
}

.TIMELINE2 {
  stroke-width : 0.5px;
  stroke-opacity:0.5;
}
<script src="https://d3js.org/d3.v4.min.js"></script>

<svg width="900" height="500"></svg>

and a JSFIDDLE. Hope this helps.

Upvotes: 1

Related Questions