user3636636
user3636636

Reputation: 2499

select data based on multiple attributes d3

I have a grid represented using d3 and svg. I am trying to select the neighbouring (adjacent) tiles to any specific tile on the grid. the tiles are accessed via their x and y coordinates on the grid. What I have feels fairly messy, and doesn't do exactly what I want, I don't want the clicked tile to be selected, or the tiles diagonal to it.

var w = 960,
    h = 500,
    z = 20,
    x = w / z,
    y = h / z;

var svg = d3.select("body").append("svg")
    .attr("width", w)
    .attr("height", h);

svg.selectAll("rect")
    .data(d3.range(x * y))
  .enter().append("rect")
    .attr("width", z)
    .attr("height", z)
    .attr("clicked", false)
    .attr('x', horizontalpos)
    .attr('y', verticalpos)
    .on("click", test)
    .style("stroke", "rgb(6,120,155)")
    .style("stroke-width", 2)
    .style("fill", "rgb(255, 255, 255)")

function translate(d) {
  return "translate(" + (d % x) * z + "," + Math.floor(d / x) * z + ")";
}


function verticalpos(d) {
  return ((d % x) * z);
}

function horizontalpos(d) {
  return (Math.floor(d / x) * z );
 }

function test(){
  var d = d3.selectAll("[x='40']").filter("[y='40']");
  d3.selectAll("[x=" + "'"+ (parseInt(d.attr("x")) +20).toString() +"'" +"],[x=" + "'"+ (parseInt(d.attr("x")) -20).toString() +"'" +"],"+ "[x=" + "'"+ (parseInt(d.attr("x"))).toString() +"'" +"]")
    .filter("[y=" + "'"+ (parseInt(d.attr("y"))).toString() +"'" +"],[y=" + "'"+ (parseInt(d.attr("y")) +20).toString() +"'" +"]"+",[y=" + "'"+ (parseInt(d.attr("y")) -20).toString() +"'" +"]")
    .transition()
    .style("fill", "black"); 

}

jsfiddle - https://jsfiddle.net/wkencq2w/15/

What I'm wondering is - Is there a way to select the data via two attributes, like this:

d3.select("[x='40'], [y='40']") 

This does not work for me, but the logic behind it is how I would like to select the data.

Upvotes: 5

Views: 3039

Answers (3)

Robert Longson
Robert Longson

Reputation: 124014

You can select data via two attributes just by putting them together e.g. [x='40'][y='40']. This together with the , css operator allows the generation of a css selection string that gives you what you asked for.

var w = 960,
    h = 500,
    z = 20,
    x = w / z,
    y = h / z;

var svg = d3.select("body").append("svg")
    .attr("width", w)
    .attr("height", h);

svg.selectAll("rect")
    .data(d3.range(x * y))
  .enter().append("rect")
    .attr("width", z)
    .attr("height", z)
    .attr("clicked", false)
    .attr('x', horizontalpos)
    .attr('y', verticalpos)
    .on("click", test)
    .style("stroke", "rgb(6,120,155)")
    .style("stroke-width", 2)
    .style("fill", "rgb(255, 255, 255)")
    
function translate(d) {
  return "translate(" + (d % x) * z + "," + Math.floor(d / x) * z + ")";
}


function verticalpos(d) {
  return ((d % x) * z);
}

function horizontalpos(d) {
  return (Math.floor(d / x) * z );
 }
 
function test(d) {
  x = parseInt(d3.select(this).attr("x"));
  y = parseInt(d3.select(this).attr("y"));
  var selector = ""
  for (var dx=-20;dx<=20;dx+=20) {
  	for (var dy=-20;dy<=20;dy+=20) {
      selector += "[x='"+ (x + dx) +"'][y='"+ (y + dy) +"'],"
    }
  }
  // cut off the final extraneous comma
  selector = selector.substring(0, selector.length - 1);
  d3.selectAll(selector)
    .transition()
    .style("fill", "black"); 
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

Or if you just want a cross without the centre as you describe in the question you could do this...

var w = 960,
    h = 500,
    z = 20,
    x = w / z,
    y = h / z;

var svg = d3.select("body").append("svg")
    .attr("width", w)
    .attr("height", h);

svg.selectAll("rect")
    .data(d3.range(x * y))
  .enter().append("rect")
    .attr("width", z)
    .attr("height", z)
    .attr("clicked", false)
    .attr('x', horizontalpos)
    .attr('y', verticalpos)
    .on("click", test)
    .style("stroke", "rgb(6,120,155)")
    .style("stroke-width", 2)
    .style("fill", "rgb(255, 255, 255)")
    
function translate(d) {
  return "translate(" + (d % x) * z + "," + Math.floor(d / x) * z + ")";
}


function verticalpos(d) {
  return ((d % x) * z);
}

function horizontalpos(d) {
  return (Math.floor(d / x) * z );
 }
 
function test(d) {
  x = parseInt(d3.select(this).attr("x"));
  y = parseInt(d3.select(this).attr("y"));
  var selector = ""
  var deltas = [[-20, 0], [20, 0], [0, 20], [0, -20]];
  for (var i=0;i < deltas.length;i++) {
    selector += "[x='"+ (x + deltas[i][0]) +"'][y='"+ (y + deltas[i][1]) +"'],"
  }
  
  // cut off the final extraneous comma
  selector = selector.substring(0, selector.length - 1);
  d3.selectAll(selector)
    .transition()
    .style("fill", "black"); 
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

Upvotes: 3

altocumulus
altocumulus

Reputation: 21578

Because it's D3, I won't do this based on calculations of positions but rather on data binding. This will simplify matters a lot and reduce the amount and the complexity of your code. One possible way might be to define a two-dimensional array of objects having x and y properties which is then bound to a D3 selection:

var grid = d3.range(y).map(function(dy) {
  return d3.range(x).map(function(dx) {
    return {x: dx, y: dy};
  });
});

var g = svg.selectAll("g")
    .data(grid)
  .enter().append("g")                 // Group each row's rects in a svg:g
    .selectAll("rect")                 // Do a nested selection
    .data(function(d) { return d; })   // Bind the sub-array for this row

The part which benefits most from this approach is your test() function which may now act on the data bound to each rect rather than having to get attribute values and doing calculation with them.

function test(d) {
  var clicked = d3.select(this).datum();   // Object bound to the rect.
  d3.selectAll("rect").filter(function(d) {
    // Do the filtering based on data rather than on positions.
    return d.x === clicked.x && Math.abs(d.y - clicked.y) === 1 ||
           d.y === clicked.y && Math.abs(d.x - clicked.x) === 1;
  })
  .transition()
  .style("fill", "black"); 
}

Have a look at this JSFiddle for a full example.

Upvotes: 3

Cyril Cherian
Cyril Cherian

Reputation: 32327

I tried to fix this problem using filter:

function test(d){
  var x = d3.select(this);
  var x1 = (parseInt(x.attr("x")) +20);
  var x2 = (parseInt(x.attr("x")) -20);
  var y1 = (parseInt(x.attr("y")) +20);
  var y2 = (parseInt(x.attr("y")) -20);
var f = d3.selectAll("rect")[0].filter(function(d){
  //left rect
    if (d3.select(d).attr("x") == x1 && d3.select(d).attr("y") == parseInt(x.attr("y")))
    return true;
  //right rect  
    if (d3.select(d).attr("x") == x2 && d3.select(d).attr("y") == parseInt(x.attr("y")))
    return true;
  //bottom rect  
    if (d3.select(d).attr("y") == y1 && d3.select(d).attr("x") == parseInt(x.attr("x")))
    return true;
  //top rect  
    if (d3.select(d).attr("y") == y2 && d3.select(d).attr("x") == parseInt(x.attr("x")))
    return true;

  return false;
});
//select all filtered and make their fill black
d3.selectAll(f).transition().delay(100).style("fill", "black");

}

Working code here

Hope this helps!

Upvotes: 2

Related Questions