简体   繁体   中英

Multiple brushes on an axis in parallel coordinate in d3 V4?

I have created a parallel coordinate in d3 V4 (with a lot of pain)which has both numerical and ordinal axis, with basic features like axis dragging, brushing, brush snapping. Here is the working sample http://plnkr.co/edit/dCNuBsaDNBwr7CrAJUBe?p=preview

I am looking to have multiple brushes in an axis, (for example I want to brush 0.2 to 0.5 and 0.7 to 0.9 of column1 in my example at the same time). So basically based on multiple brush areas the corresponding lines should be highlighted. Please suggest some way to do this. Thanks in advance

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

svg {
  font: 10px sans-serif;
}

.background path {
  fill: none;
  stroke: #ddd;
   stroke-opacity: .4;  
  shape-rendering: crispEdges;
}

.foreground path {
  fill: none;
  stroke: steelblue;
   stroke-opacity: .7;
}

.brush .extent {
  fill-opacity: .3;
  stroke: #fff;

  shape-rendering: crispEdges;
}

.axis line,
.axis path {
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}

.axis text {
  text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff;
  cursor: move;
}

</style>
<body>
<script src="http://d3js.org/d3.v4.min.js"></script>
<script>


var margin = {top: 30, right: 10, bottom: 10, left: 10},
    width = 600 - margin.left - margin.right,
    height = 200 - margin.top - margin.bottom;

var x = d3.scalePoint().rangeRound([0, width]).padding(1),
    y = {},
    dragging = {};


var line = d3.line(),
    //axis = d3.axisLeft(x),
    background,
    foreground,
    extents;


 var container = d3.select("body").append("div")
    .attr("class", "parcoords")
    .style("width", width + margin.left + margin.right + "px")
    .style("height", height + margin.top + margin.bottom + "px");

var svg = container.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 + ")");

var quant_p = function(v){return (parseFloat(v) == v) || (v == "")};     

d3.json("convertcsv.json", function(error, cars) {

    dimensions = d3.keys(cars[0]);

    x.domain(dimensions);

    dimensions.forEach(function(d) {
    var vals = cars.map(function(p) {return p[d];});
    if (vals.every(quant_p)){ 
     y[d] = d3.scaleLinear()
        .domain(d3.extent(cars, function(p) { 
            return +p[d]; }))
        .range([height, 0])

        console.log(y[d]);
      }
    else{
     vals.sort();           
      y[d] = d3.scalePoint()
              .domain(vals.filter(function(v, i) {return vals.indexOf(v) == i;}))
              .range([height, 0],1);
       }

  })


 extents = dimensions.map(function(p) { return [0,0]; });


  // Add grey background lines for context.
  background = svg.append("g")
      .attr("class", "background")
    .selectAll("path")
      .data(cars)
    .enter().append("path")
      .attr("d", path);

  // Add blue foreground lines for focus.
  foreground = svg.append("g")
      .attr("class", "foreground")
    .selectAll("path")
      .data(cars)
    .enter().append("path")
      .attr("d", path);



  // Add a group element for each dimension.

  var g = svg.selectAll(".dimension")
      .data(dimensions)
    .enter().append("g")
      .attr("class", "dimension")
      .attr("transform", function(d) {  return "translate(" + x(d) + ")"; })
      .call(d3.drag()
        .subject(function(d) { return {x: x(d)}; })
        .on("start", function(d) {
          dragging[d] = x(d);
          background.attr("visibility", "hidden");
        })
        .on("drag", function(d) {
          dragging[d] = Math.min(width, Math.max(0, d3.event.x));
          foreground.attr("d", path);
          dimensions.sort(function(a, b) { return position(a) - position(b); });
          x.domain(dimensions);
          g.attr("transform", function(d) { return "translate(" + position(d) + ")"; })
        })
        .on("end", function(d) {
          delete dragging[d];
          transition(d3.select(this)).attr("transform", "translate(" + x(d) + ")");
          transition(foreground).attr("d", path);
          background
              .attr("d", path)
            .transition()
              .delay(500)
              .duration(0)
              .attr("visibility", null);
        }));


  // Add an axis and title.
   var g = svg.selectAll(".dimension");
  g.append("g")
      .attr("class", "axis")
      .each(function(d) {  d3.select(this).call(d3.axisLeft(y[d]));})
      //text does not show up because previous line breaks somehow
    .append("text")
      .attr("fill", "black")
      .style("text-anchor", "middle")
      .attr("y", -9) 
      .text(function(d) { return d; });

  // Add and store a brush for each axis.
  g.append("g")
      .attr("class", "brush")
      .each(function(d) {
        if(y[d].name == 'r'){
         // console.log(this);

        d3.select(this).call(y[d].brush = d3.brushY().extent([[-8, 0], [8,height]]).on("brush start", brushstart).on("brush", go_brush).on("brush", brush_parallel_chart).on("end", brush_end));
        }


      else if(y[d].name == 'n')
             d3.select(this).call(y[d].brush = d3.brushY().extent([[-8, 0], [15,height]]).on("brush start", brushstart).on("brush", go_brush).on("brush", brush_parallel).on("end", brush_end_ordinal)); 


      })
    .selectAll("rect")
      .attr("x", -8)
      .attr("width", 16);  
});  // closing

function position(d) {
  var v = dragging[d];
  return v == null ? x(d) : v;
}

function transition(g) {
  return g.transition().duration(500);
}

// Returns the path for a given data point.
function path(d) {
  return line(dimensions.map(function(p) { return [position(p), y[p](d[p])]; }));
}

function go_brush() {
  d3.event.sourceEvent.stopPropagation();
}


invertExtent = function(y) {
  return domain.filter(function(d, i) { return y === range[i]; });
};


function brushstart(selectionName) {
  foreground.style("display", "none")

  //console.log(selectionName);

  var dimensionsIndex = dimensions.indexOf(selectionName);

  //console.log(dimensionsIndex);

  extents[dimensionsIndex] = [0, 0];

  foreground.style("display", function(d) {
    return dimensions.every(function(p, i) {
        if(extents[i][0]==0 && extents[i][0]==0) {
            return true;
        }
      return extents[i][1] <= d[p] && d[p] <= extents[i][0];
    }) ? null : "none";
  });
}



// Handles a brush event, toggling the display of foreground lines.
function brush_parallel_chart() { 

   for(var i=0;i<dimensions.length;++i){


        if(d3.event.target==y[dimensions[i]].brush) {
         //if (d3.event.sourceEvent.type === "brush") return;

            extents[i]=d3.event.selection.map(y[dimensions[i]].invert,y[dimensions[i]]);

    }

}

     foreground.style("display", function(d) {
        return dimensions.every(function(p, i) {
            if(extents[i][0]==0 && extents[i][0]==0) {
                return true;
            }
          return extents[i][1] <= d[p] && d[p] <= extents[i][0];
        }) ? null : "none";
      }); 
}    


function brush_end(){



  if (!d3.event.sourceEvent) return; // Only transition after input.
  if (!d3.event.selection) return; // Ignore empty selections.


for(var i=0;i<dimensions.length;++i){

    if(d3.event.target==y[dimensions[i]].brush) {

  extents[i]=d3.event.selection.map(y[dimensions[i]].invert,y[dimensions[i]]);

   extents[i][0] = Math.round( extents[i][0] * 10 ) / 10;
   extents[i][1] = Math.round( extents[i][1] * 10 ) / 10;



    d3.select(this).transition().call(d3.event.target.move, extents[i].map(y[dimensions[i]]));

     }

  }

}

//   brush for ordinal cases
function brush_parallel() {


for(var i=0;i<dimensions.length;++i){

        if(d3.event.target==y[dimensions[i]].brush) {


       var  yScale = y[dimensions[i]];
       var selected =  yScale.domain().filter(function(d){
            // var s = d3.event.target.extent();
          var s = d3.event.selection;

      return (s[0] <= yScale(d)) && (yScale(d) <= s[1])

      });


var temp = selected.sort();
extents[i] = [temp[temp.length-1], temp[0]];


   }

}

foreground.style("display", function(d) {
        return dimensions.every(function(p, i) {
            if(extents[i][0]==0 && extents[i][0]==0) {
                return true;
            }
          //var p_new = (y[p].ticks)?d[p]:y[p](d[p]); 
          //return extents[i][1] <= p_new && p_new <= extents[i][0];
        return extents[i][1] <= d[p] && d[p] <= extents[i][0];
        }) ? null : "none";
      });     
}




function brush_end_ordinal(){

  console.log("hhhhh");


  if (!d3.event.sourceEvent) return; // Only transition after input.

  if (!d3.event.selection) return; // Ignore empty selections.

  for(var i=0;i<dimensions.length;++i){

        if(d3.event.target==y[dimensions[i]].brush) {


       var  yScale = y[dimensions[i]];
       var selected =  yScale.domain().filter(function(d){
      // var s = d3.event.target.extent();
      var s = d3.event.selection;

      return (s[0] <= yScale(d)) && (yScale(d) <= s[1])

      });

  var temp = selected.sort();
  extents[i] = [temp[temp.length-1], temp[0]];

if(selected.length >1)
d3.select(this).transition().call(d3.event.target.move, extents[i].map(y[dimensions[i]]));


     }  
 } 



}

</script> 

An implementation of multi brush in d3 V4 is there in https://github.com/BigFatDog/parcoords-es

But I guess demo examples are not there.

Here's a Plunkr using multiple brushes in d3 v4 and of course working on your parallel chart:

Code Plunkr for multi brush

https://github.com/shashank2104/d3.svg.multibrush (I'll suggest the v4 patch to the guy who has written the v3 version once it's completely done)

There's one thing that's bothering me which is the brush clicking, as of now, the brushing works with CLICK to start, MOUSEMOVE and CLICK to end . Just play around with the brushing and you'll notice the difference.

The new brushing function is the default one (as follows);

// Handles a brush event, toggling the display of foreground lines.
function brush() {
  var actives = dimensions.filter(function(p) { return !y[p].brush.empty(); }),
  extents = actives.map(function(p) { return y[p].brush.extent(); });
  foreground.style("display", function(d) {
  return actives.every(function(p, i) {
        return extents[i].some(function(e){
            return e[0] <= d[p] && d[p] <= e[1];
        });
  }) ? null : "none";
 });
}

I'll be working on it and hopefully it'll be ready by morning.

Until then, ordinal axis shouldn't be a problem to fix.

I'll keep you posted and edit the same post. (It'd be nice if you could not accept the answer as the right one until it's completely fixed. It'll be on my plate to fix)

Let me know if you come across any questions.

Shashank thanks a lot for the working example, Its like first working example of multi brush in V4. I observed the points you mentioned about brushing.Its a bit different from the traditional brushing method. But the multi brush library from V3 has the conventional brushing way , right.

I have placed the code for ordinal brushing also in your (default) brush function.

function brush() {
  var actives = dimensions.filter(function(p) { return !y[p].brush.empty(); }),
      extents = actives.map(function(p) { return y[p].brush.extent(); });

  foreground.style("display", function(d) {
    return actives.every(function(p, i) {
      var p_new = (y[p].ticks)?d[p]:y[p](d[p]);
            return extents[i].some(function(e){
                //return e[0] <= d[p] && d[p] <= e[1];
        return e[0] <= p_new && p_new <= e[1];
            });
    }) ? null : "none";
  });
}

In my plunkr example, I have used d3.brushY() functions which are in V4 and thats why I have to implement invert functions (for numerical axis , referred from Appended text not showing in d3 v4 ) and refeered http://bl.ocks.org/chrisbrich/4173587 for ordinal axis. With these combinations I was able to do brush snapping also.

Also Is there a way that I can do brush snapping also (both numerical and ordinal axis) with your multi brush plunkr example.

I know I am asking more, but basically two points: 1) Is there a way to do multi brushing like that in V3, like as click- drag - leave ! 2) Brush snapping (numerical and ordinal) in your multi brush example ?

Thanks again for all your effort and time , means a lot. I am making points so that once you suggest V4 patch, its ll be like fully good.:)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM