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.
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.
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>
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!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With