Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Responsive d3 brushing

I have a d3 timeline based on Mike Bostock's 'Focus + Context via brushing' which I'm trying to make responsive.

I've been able to achieve this with most of it, but I'm struggling with the extent of the brush. As a workaround I've tried just making it the new width of the context, but it behaves extremely erratically. Everything else I've tried seems to have no effect – the extent rect doesn't change width.

I need a way to find the x and width of the extent rect and apply them to my x-scale (named xContext) on resize. There's a 'working' version of it here and the full code is below. The resize function is towards the bottom.

Many thanks in advance.

var marginTimeline = {top: 0, right: 18, bottom: 260, left: 0},
    marginContext = {top: 400, right: 18, bottom: 80, left: 0},
    w = parseInt(d3.select("#chart").style("width")) - marginTimeline.left - marginTimeline.right,
    hTimeline = parseInt(d3.select("#chart").style("height")) - marginTimeline.top - marginTimeline.bottom,
    hContext = parseInt(d3.select("#chart").style("height")) - marginContext.top - marginContext.bottom;

//Height of the bars drawn. Context bars are half this.
var barHeight = hTimeline * 0.04;    

var formatDate = d3.time.format("%Y%m%d"),
    parseDate = formatDate.parse;

var xTimeline = d3.time.scale().range([0, w]),
    xContext = d3.time.scale().range([0, w]),
    yTimeline = d3.scale.linear().domain([0, 6]).range([hTimeline, 0]).nice(),
    yContext = d3.scale.linear().range([hContext, 0]);

var thous = d3.format(",");
var displayDate = d3.time.format("%d %b %Y");
var displayMonthYear = d3.time.format("%b %Y");
var displayYear = d3.time.format("%Y");

var xAxisTimeline = d3.svg.axis().scale(xTimeline).orient("bottom"),
    xAxisContext = d3.svg.axis().scale(xContext).orient("bottom"),
    yAxisTimeline = d3.svg.axis().scale(yTimeline).orient("left").outerTickSize(0).ticks(0),
    yAxisContext = d3.svg.axis().scale(yContext).orient("left").outerTickSize(0).ticks(0);

var svg = d3.select("#chart")
    .attr("width", w + marginTimeline.left + marginTimeline.right)
    .attr("height", hTimeline + marginTimeline.top + marginTimeline.bottom)
    .append("g");

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

var opTimeline = svg.append("g")
    .attr("class", "timeline")
    .attr("width", w)
    .attr("height", hTimeline)
    .attr("transform", "translate(10,0)");

var opContext = svg.append("g")
    .attr("class", "context")
    .attr("transform", "translate(10," + marginContext.top + ")");

var brush = d3.svg.brush()
    .x(xContext)
    .extent([0, 1])
    .on("brush", brushed);

queue()
  .defer(d3.json, "http://pasi.com.au/omarpasha/api/get_category_posts/?slug=shows&include=title,url,content,custom_fields")
  .defer(d3.json, "http://pasi.com.au/omarpasha/api/get_category_posts/?slug=timeline&include=title,url,content,custom_fields")
  .await(ready); 

function ready(error, shows, history) {
                  shows.posts.forEach(function(d) {
                  d.id = d.id;
                  d.title = d.title;
                  d.showpage = d.url;
                  d.startDate = parseDate(d.custom_fields.starting_date[0]);
                  d.endDate = parseDate(d.custom_fields.finishing_date[0]);
})
                  history.posts.forEach(function(d) {
                  d.id = d.id;
                  d.title = d.title;
                  d.startDate = parseDate(d.custom_fields.starting_date[0]);
                  d.endDate = parseDate(d.custom_fields.finishing_date[0]);
                  d.line = d.custom_fields.line;
                  d.dateFormat = d.custom_fields.date_format;
});


var minDateShows = d3.min(shows.posts.map(function(d) { return d.startDate; })); 

var minDateHistory = d3.min(history.posts.map(function(d) { return d.startDate; })); 

var minDate =  (minDateShows < minDateHistory ? minDateShows : minDateHistory);

var leftDate = new Date(minDate.getTime());
    leftDate.setDate(leftDate.getDate()-40);

var maxDateShows = d3.max(shows.posts.map(function(d) { return d.endDate; })); 

var maxDateHistory = d3.max(history.posts.map(function(d) { return d.endDate; })); 

var maxDate =  (maxDateShows > maxDateHistory ? maxDateShows : maxDateHistory);

var rightDate = new Date(maxDate.getTime());
    rightDate.setDate(rightDate.getDate()+1400);


  xTimeline.domain([leftDate, rightDate]);
  xContext.domain(xTimeline.domain());
  yContext.domain(yTimeline.domain());

var tip = d3.tip()
  .attr('class', 'd3-tip')
  .offset(function(d) { if (xTimeline(d.endDate)  > 800) { return [-10, 8] } else { return [-10, -8]  } })
  .direction(function(d) { if (xTimeline(d.endDate)  > 800) { return 'nw' } else { return 'ne'  } })
  .html(function(d) {
    if (displayMonthYear(d.startDate) == displayMonthYear(d.endDate)) {
          return d.title + "<br/><p class='yellow'>" + displayMonthYear(d.startDate) + "</p>"; }
        else { 
          return d.title + "<br/><p class='yellow'>"+ displayMonthYear(d.startDate) + " to " + displayMonthYear(d.endDate) + "</p>"; }
  });

var tip2 = d3.tip()
  .attr('class', 'd3-tip')
  .direction(function(d) { if (xTimeline(d.endDate)  > 800) { return 'nw' } else { return 'ne'  } })
  .offset(function(d) {
      if (xTimeline(d.endDate)  > 800) {
        return [-10, 8];
      } else {
        return [-10, -8];
      }
  })
  .html(function(d) {
    var toolTipContent = "";
    if ((xTimeline(d.endDate) - xTimeline(d.startDate) == 0)) {
      toolTipContent = getToolTipContent(d, true);
    } else {
      toolTipContent = getToolTipContent(d, false);
    }
    return toolTipContent;
  });

function getToolTipContent(d, sameDates) {
  var toolTipContent = d.title + "<br/><p class='yellow'>";
  if (d.dateFormat == "Year only") {
    toolTipContent +=  (sameDates)
      ? displayYear(d.startDate) + "</p>" + d.content
      : displayYear(d.startDate) + " to " + displayYear(d.endDate);
  } else if (d.dateFormat == "Month and year") {
    toolTipContent +=  (sameDates)
      ? displayMonthYear(d.startDate) + "</p>" + d.content
      : displayMonthYear(d.startDate) + " to " + displayMonthYear(d.endDate);
  } else {
    toolTipContent +=  (sameDates)
      ? displayDate(d.startDate) + "</p>" + d.content
      : displayDate(d.startDate) + " to " + displayDate(d.endDate);
  }
  toolTipContent += "</p>" + d.content;
  return toolTipContent;
}  

svg.call(tip);
svg.call(tip2);

opTimeline.append("line")
   .attr("class", "show show-line")
   .attr("x1", 0)
   .attr("x2",  w)
   .attr("y1", yTimeline(5))
   .attr("y2", yTimeline(5));

opTimeline.append("line")
   .attr("class", "ost ost-line")
   .attr("x1", 0)
   .attr("x2",  w)
   .attr("y1", yTimeline(3))
   .attr("y2", yTimeline(3));

opTimeline.append("line")
   .attr("class", "blackart blackart-line")
   .attr("x1", 0)
   .attr("x2",  w)
   .attr("y1", yTimeline(1))
   .attr("y2", yTimeline(1));

opContext.append("line")
   .attr("class", "context show context-show-line")
   .attr("x1", 0)
   .attr("x2",  w)
   .attr("y1", yContext(5))
   .attr("y2", yContext(5));

opContext.append("line")
   .attr("class", "context ost context-ost-line")
   .attr("x1", 0)
   .attr("x2",  w)
   .attr("y1", yContext(3))
   .attr("y2", yContext(3));

opContext.append("line")
   .attr("class", "context blackart context-blackart-line")
   .attr("x1", 0)
   .attr("x2",  w)
   .attr("y1", yContext(1))
   .attr("y2", yContext(1));

opTimeline.append("text")
   .attr("class", "show show-text")
   .attr("x", 10)
   .attr("y", yTimeline(5) + 26)
   .text("Shows");

opTimeline.append("text")
   .attr("class", "ost ost-text")
   .attr("x", 10)
   .attr("y", yTimeline(3) + 26)
   .text("Ostrowsky Family");

opTimeline.append("text")
   .attr("class", "blackart blackart-text")
   .attr("x", 10)
   .attr("y", yTimeline(1) + 26)
   .text("Black Art");

svg.append("text")
   .attr("class", "explanation")
   .attr("x", 10)
   .attr("y", 380)
   .text("Move the handles below to adjust the time period");

opTimeline.append("g")
   .selectAll("rect")
   .data(shows.posts)
   .enter()
   .append("svg:a")
   .attr("xlink:href", function(d){return d.showpage;})
   .append("rect")
   .attr("class", "event show-event show")
   .attr("clip-path", "url(#clip)")
   .attr("x", (function(d) { return xTimeline(d.startDate); }))
   .attr("width",  (function(d) { if ((xTimeline(d.endDate) - xTimeline(d.startDate) > 12)) {
    return (xTimeline(d.endDate) - xTimeline(d.startDate));}
    else {
      return 12
    } }))
   .attr("y", yTimeline(5) - (barHeight * 0.5))
   .attr("height", barHeight)
   .attr("rx", 10)
   .attr("ry", 10);

opTimeline.append("g")
   .selectAll("rect")
   .data(history.posts)
   .enter()
   .append("rect")
   .attr("class", (function(d) { if (d.line == "Ostrowsky family") { return "event ost-event ost" } else { return "event blackart-event blackart" } }))
   .attr("clip-path", "url(#clip)")
   .attr("x", (function(d) { return xTimeline(d.startDate); }))
   .attr("width",  (function(d) { if ((xTimeline(d.endDate) - xTimeline(d.startDate) > 12)) {
    return (xTimeline(d.endDate) - xTimeline(d.startDate));}
    else {
      return 12
    } }))
   .attr("y", (function(d) { if (d.line == "Ostrowsky family") { return yTimeline(3) - (barHeight * 0.5) } else { return yTimeline(1) - (barHeight * 0.5) } }))
   .attr("height", barHeight)
   .attr("rx", 10)
   .attr("ry", 10);

opContext.append("g")
   .selectAll("rect")
   .data(shows.posts)
   .enter()
   .append("rect")
   .attr("class", "event show-event show")
   .attr("x", (function(d) { return xContext(d.startDate); }))
   .attr("width",  (function(d) { if ((xTimeline(d.endDate) - xTimeline(d.startDate) > 6)) {
    return (xTimeline(d.endDate) - xTimeline(d.startDate));}
    else {
      return 6
    } }))
   .attr("y", yContext(5) - (barHeight * 0.25))
   .attr("height", barHeight/2)
   .attr("rx", 5)
   .attr("ry", 5);

opContext.append("g")
   .selectAll("rect")
   .data(history.posts)
   .enter()
   .append("rect")
   .attr("class", (function(d) { if (d.line == "Ostrowsky family") { return "event ost-event ost" } else { return "event blackart-event blackart" } }))
   .attr("x", (function(d) { return xContext(d.startDate); }))
   .attr("width",  (function(d) { if ((xTimeline(d.endDate) - xTimeline(d.startDate) > 6)) {
    return (xTimeline(d.endDate) - xTimeline(d.startDate));}
    else {
      return 6
    } }))
   .attr("y", (function(d) { if (d.line == "Ostrowsky family") { return yContext(3) - (barHeight * 0.25) } else { return yContext(1) - (barHeight * 0.25) } }))
   .attr("height", barHeight/2)
   .attr("rx", 5)
   .attr("ry", 5);

opTimeline.append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + hTimeline + ")")
    .call(xAxisTimeline);


opContext.append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + hContext + ")")
      .call(xAxisContext);

var brushg = opContext.append("g")
      .attr("class", "x brush")
      .call(brush)
      .selectAll("rect")
      .attr("y", -6)
      .attr("height", hContext + 7);

opContext.selectAll(".e")
      .append("image")
      .attr("xlink:href",'../wp-content/themes/omarpasha/img/right-handle.png')
      .attr("width", 10)
      .attr("height", 70)
      .attr("y", -6);

opContext.selectAll(".w")
      .append("image")
      .attr("xlink:href",'../wp-content/themes/omarpasha/img/left-handle.png')
      .attr("width", 10)
      .attr("height", 70)
      .attr("x", -10)
      .attr("y", -6);

opTimeline.selectAll(".show-event")
   .on('mouseover', tip.show)
   .on('mouseout', tip.hide);

opTimeline.selectAll(".ost-event, .blackart-event")
   .on('mouseover', tip2.show)
   .on('mouseout', tip2.hide);


function resize() {
    marginContext = {top: 400, right: 18, bottom: 80, left: 0},
    w = parseInt(d3.select("#chart").style("width")) - marginTimeline.left - marginTimeline.right,
    hTimeline = parseInt(d3.select("#chart").style("height")) - marginTimeline.top - marginTimeline.bottom,
    hContext = parseInt(d3.select("#chart").style("height")) - marginContext.top - marginContext.bottom;

    var barHeight = hTimeline * 0.04;    

        xTimeline.range([0, w]),
        xContext.range([0, w]),
        yTimeline.range([hTimeline, 0]).nice(),
        yContext.range([hContext, 0]);

    svg
      .attr("width", w + marginTimeline.left + marginTimeline.right)
      .attr("height", hTimeline + marginTimeline.top + marginTimeline.bottom);

    svg.select("#clip rect")
      .attr("width", w)
      .attr("height", hTimeline);

    d3.select(".background")
      .attr("width", w);

    opTimeline
      .attr("width", w)
      .attr("height", hTimeline)
      .attr("transform", "translate(10,0)");

    opContext
      .attr("transform", "translate(10," + marginContext.top + ")");

    opTimeline.select('.x.axis')
      .attr("transform", "translate(0," + hTimeline + ")")
      .call(xAxisTimeline);

    opContext.select('.x.axis')
      .attr("transform", "translate(0," + hContext + ")")
      .call(xAxisContext);

    opTimeline.select(".show-line")
       .attr("x2",  w);

    opTimeline.select(".ost-line")
       .attr("x2",  w);

    opTimeline.select(".blackart-line")
       .attr("x2",  w);

    opContext.select(".context-show-line")
       .attr("x2",  w);

    opContext.select(".context-ost-line")
       .attr("x2",  w);

    opContext.select(".context-blackart-line")
       .attr("x2",  w);

    opTimeline.selectAll(".event")
       .attr("x", (function(d) { return xTimeline(d.startDate); }))
       .attr("width",  (function(d) { if ((xTimeline(d.endDate) - xTimeline(d.startDate) > 12)) {
        return (xTimeline(d.endDate) - xTimeline(d.startDate));}
        else {
          return 12
        } }));

    opContext.selectAll(".event")
       .attr("x", (function(d) { return xContext(d.startDate); }))
       .attr("width",  (function(d) { if ((xContext(d.endDate) - xContext(d.startDate) > 6)) {
        return (xContext(d.endDate) - xContext(d.startDate));}
        else {
          return 6
        } }));

    brush
      .x(xContext)
      .extent([0, 1])
      .on("brush", brushed);
}

d3.select(window).on('resize', resize); 
  resize();

};

function brushed() {
  xTimeline.domain(brush.empty() ? xContext.domain() : brush.extent());
  opTimeline.selectAll("rect").attr("x", (function(d) { return xTimeline(d.startDate); }))
      .attr("width",  (function(d) { if ((xTimeline(d.endDate) - xTimeline(d.startDate) > 12)) { return (xTimeline(d.endDate) - xTimeline(d.startDate));} else { return 12 } }));
  opTimeline.select(".x.axis").call(xAxisTimeline);
}
like image 887
tgerard Avatar asked May 25 '26 05:05

tgerard


1 Answers

I had someone outside Stack Overflow sort this out for me. The solution was straightforward – capture the state of the brush's extent at the beginning of the resize function. Nothing else was changed. So the resize function now looks like this (still rather verbose, but working):

function resize() {
    var extent = brush.extent();

    w = parseInt(d3.select("#chart").style("width")) - marginTimeline.left - marginTimeline.right,
    hTimeline = parseInt(d3.select("#chart").style("height")) - marginTimeline.top - marginTimeline.bottom;

    var barHeight = hTimeline * 0.04;    

        xTimeline.range([0, w]),
        xContext.range([0, w]),
        yTimeline.range([hTimeline, 0]).nice(),
        yContext.range([hContext, 0]);


    svg
      .attr("width", w + marginTimeline.left + marginTimeline.right);

    svg.select("#clip rect")
      .attr("width", w);

    opTimeline
      .attr("width", w)
      .attr("transform", "translate(10,0)");

    opContext
      .attr("transform", "translate(10," + marginContext.top + ")");

    opTimeline.select('.x.axis')
      .attr("transform", "translate(0," + hTimeline + ")")
      .call(xAxisTimeline);

    opContext.select('.x.axis')
      .attr("transform", "translate(0," + hContext + ")")
      .call(xAxisContext);

    opTimeline.select(".show-line")
       .attr("x2",  w);

    opTimeline.select(".ost-line")
       .attr("x2",  w);

    opTimeline.select(".blackart-line")
       .attr("x2",  w);

    opContext.select(".context-show-line")
       .attr("x2",  w);

    opContext.select(".context-ost-line")
       .attr("x2",  w);

    opContext.select(".context-blackart-line")
       .attr("x2",  w);

    opTimeline.selectAll(".event")
       .attr("x", (function(d) { return xTimeline(d.startDate); }))
       .attr("width",  (function(d) { if ((xTimeline(d.endDate) - xTimeline(d.startDate) > 12)) {
        return (xTimeline(d.endDate) - xTimeline(d.startDate));}
        else {
          return 12
        } }));

    opContext.selectAll(".event")
       .attr("x", (function(d) { return xContext(d.startDate); }))
       .attr("width",  (function(d) { if ((xContext(d.endDate) - xContext(d.startDate) > 6)) {
        return (xContext(d.endDate) - xContext(d.startDate));}
        else {
          return 6
        } }));

  brush.extent(extent);
  // Now just call the methods to update the brush.
  opContext.select("g.x.brush").call(brush);
  brushed();


}
like image 75
tgerard Avatar answered May 26 '26 19:05

tgerard