In D3 hexbin, increase radius of multiple hexagons with mouseover - javascript

EDIT: And here is a link to a codepen of mine where I have the simpler hover functionality working.
I am new to D3 and trying to create a fairly tricky hover effect on a hexbin graph. I attached the image of the hexes below to describe my effect.
An individual hexagon in a hex graph like this (unless its on the edge) borders 6 other hexagons. My goal is that when a user hovers over a hex, the radius of both that hex, as well as the 6 surrounding hexes, increases, to give a sort of pop up effect.
Using Bostocks starter hexbin code here and adjusting it a bit (adding a radiusScale and hover effect), I made the following code snippet below that has a simpler hover effect:
var svg = d3.select("svg"),
margin = {top: 20, right: 20, bottom: 30, left: 40},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
const randomX = d3.randomNormal(width / 2, 80),
randomY = d3.randomNormal(height / 2, 80),
points = d3.range(2000).map(function() { return [randomX(), randomY()]; });
const color = d3.scaleSequential(d3.interpolateLab("white", "steelblue"))
.domain([0, 20]);
const hexbin = d3.hexbin()
.radius(20)
.extent([[0, 0], [width, height]]);
const x = d3.scaleLinear()
.domain([0, width])
.range([0, width]);
const y = d3.scaleLinear()
.domain([0, height])
.range([height, 0]);
// radiusScale
const radiusScale = d3.scaleSqrt()
.domain([0, 10]) // domain is # elements in hexbin
.range([0, 8]); // range is mapping to pixels (or coords) for radius
g.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
g.append("g")
.attr("class", "hexagon")
.attr("clip-path", "url(#clip)")
.selectAll("path")
.data(hexbin(points))
.enter().append("path")
.attr("d", d => hexbin.hexagon(radiusScale(d.length)))
// .attr("d", hexbin.hexagon())
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
.attr("fill", function(d) { return color(d.length); })
.on('mouseover', function(d) {
d3.select(this)
.attr("d", d => hexbin.hexagon(radiusScale((5+d.length)*2)))
})
.on('mouseout', function(d) {
d3.select(this)
.attr("d", d => hexbin.hexagon(radiusScale(d.length)))
})
g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(y).tickSizeOuter(-width));
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).tickSizeOuter(-height));
.hexagon {
stroke: #000;
stroke-width: 0.5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<script src="https://d3js.org/d3-hexbin.v0.2.min.js"></script>
<svg width="500" height="400"></svg>
This effect only increases the radius of the single hexagon being hovered over, not also the surrounding hexagons.
To begin addressing the issue of increasing the radius of surrounding hexagons, I wrote this function that takes the binned data, an (x,y) location (center of a hexagon), and a radius that is wide enough to capture the (x,y) centers of neighbor hexagons:
// hexbinData, which was created using the hexbin() function,
// has a .x and .y value for each element, and the .x and .y values
// represent the center of that hexagon.
const findNeighborHexs = function(hexbinData, xHex, yHex, radius) {
var neighborHexs = hexbinData
.filter(row => row.x < (xHex+radius) & row.x > (xHex-radius))
.filter(row => row.y < (yHex+radius) & row.y > (yHex-radius))
return neighborHexs;
}
And here is where I'm stuck... I'm not sure how to use findNeighborHexs to (1) select those elements on hovering and (2) change those elements sizes. As a very tough (3), I think I may need to move the (x,y) centers for these neighbox hexes too to account for larger radius.
Thanks in advance for any help with this. I know this is a long post but I've got a bunch of stuff done already for this and this would be a very cool hover effect I'm working on so any help is appreciated!

Here is a slightly modified version of your code which also plays with adjacent hexagons of the hovered hexagon:
var svg = d3.select("svg"),
margin = {top: 20, right: 20, bottom: 30, left: 40},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
const randomX = d3.randomNormal(width / 2, 80),
randomY = d3.randomNormal(height / 2, 80),
points = d3.range(2000).map(function() { return [randomX(), randomY()]; });
const color = d3.scaleSequential(d3.interpolateLab("white", "steelblue"))
.domain([0, 20]);
const hexbin = d3.hexbin()
.radius(20)
.extent([[0, 0], [width, height]]);
const x = d3.scaleLinear()
.domain([0, width])
.range([0, width]);
const y = d3.scaleLinear()
.domain([0, height])
.range([height, 0]);
// radiusScale
const radiusScale = d3.scaleSqrt()
.domain([0, 10]) // domain is # elements in hexbin
.range([0, 8]); // range is mapping to pixels (or coords) for radius
g.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
function unique(arr) {
var u = {}, a = [];
for(var i = 0, l = arr.length; i < l; ++i){
if(!u.hasOwnProperty(arr[i])) {
a.push(arr[i]);
u[arr[i]] = 1;
}
}
return a;
}
var xs = unique(hexbin(points).map(h => parseFloat(h.x))).sort(function(a,b) { return a - b;});
var ys = unique(hexbin(points).map(h => parseFloat(h.y))).sort(function(a,b) { return a - b;});
g.append("g")
.attr("class", "hexagon")
.attr("clip-path", "url(#clip)")
.selectAll("path")
.data(hexbin(points))
.enter().append("path")
.attr("id", d => xs.indexOf(d.x) + "-" + ys.indexOf(d.y))
.attr("length", d => d.length)
.attr("d", d => hexbin.hexagon(radiusScale(d.length)))
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
.attr("fill", function(d) { return color(d.length); })
.on('mouseover', function(d) {
d3.select(this).attr("d", d => hexbin.hexagon(radiusScale((5 + d.length) * 2)));
var dx = xs.indexOf(d.x);
var dy = ys.indexOf(d.y);
[[-2, 0], [-1, -1], [1, -1], [2, 0], [1, 1], [-1, 1]].forEach( neighbour => {
var elmt = document.getElementById((dx + neighbour[0]) + "-" + (dy + neighbour[1]))
if (elmt) {
var elmtLength = parseInt(elmt.getAttribute("length"));
elmt.setAttribute("d", hexbin.hexagon(radiusScale(5 + elmtLength)));
}
});
})
.on('mouseout', function(d) {
d3.select(this).attr("d", d => hexbin.hexagon(radiusScale(d.length)));
var dx = xs.indexOf(d.x);
var dy = ys.indexOf(d.y);
[[-2, 0], [-1, -1], [1, -1], [2, 0], [1, 1], [-1, 1]].forEach( neighbour => {
var elmt = document.getElementById((dx + neighbour[0]) + "-" + (dy + neighbour[1]))
if (elmt) {
var elmtLength = parseInt(elmt.getAttribute("length"));
elmt.setAttribute("d", hexbin.hexagon(radiusScale(elmtLength)));
}
});
})
g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(y).tickSizeOuter(-width));
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).tickSizeOuter(-height));
.hexagon {
stroke: #000;
stroke-width: 0.5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<script src="https://d3js.org/d3-hexbin.v0.2.min.js"></script>
<svg width="500" height="400"></svg>
The idea is to give each hexagon an id in order to be able to select it.
If the hexagon being hovered is the 6th from the left and the 3rd from the top, then we can give it the id #6-3.
This way when this hexagon is hovered, we can play with its adjacent hexagons by selecting them by their id, the one on its left for instance has the id #5-3.
In order to give each hexagon an id, as d3's hexbin(input) replaces our input with only the hexagons' x and y coordinates, we'll have to find find all xs and ys produced:
var xs = unique(hexbin(points).map(h => parseFloat(h.x))).sort(function(a,b) { return a - b;});
var ys = unique(hexbin(points).map(h => parseFloat(h.y))).sort(function(a,b) { return a - b;});
where unique is whatever function keeping only distinct values.
This way, our hexagons can be given an id this way:
...
.data(hexbin(points))
.enter().append("path")
.attr("id", d => xs.indexOf(d.x) + "-" + ys.indexOf(d.y))
...
Now that our hexagons have an id, we can modify our mouseover and mouseout to play with these adjacent hexagons:
Adjacent hexagons are the ones for which we need to sum x and y of the hovered hexagon by:
[[-2, 0], [-1, -1], [1, -1], [2, 0], [1, 1], [-1, 1]]
which gives for the mouseover (in addition to modifying the size of the hovered hexagon):
.on('mouseover', function(d) {
d3.select(this).attr("d", d => hexbin.hexagon(radiusScale((5 + d.length) * 2)));
var dx = xs.indexOf(d.x);
var dy = ys.indexOf(d.y);
[[-2, 0], [-1, -1], [1, -1], [2, 0], [1, 1], [-1, 1]].forEach( neighbour => {
var elmt = document.getElementById((dx + neighbour[0]) + "-" + (dy + neighbour[1]))
if (elmt) {
var elmtLength = parseInt(elmt.getAttribute("length"));
elmt.setAttribute("d", hexbin.hexagon(radiusScale(5 + elmtLength)));
}
});
})
Note that in addition to setting the id of each hexagon, we also include the length attribute in order to easily change the hovered size of hexagons.

you could amend you mouseover and mouseout functions to be the following, which selects all the hexagons and sets the size based on whether they fall within your defined radius:
.on('mouseover', function(d) {
let dx = d.x
let dy = d.y
let r = 50 //set this to be an appropriate size radius
d3.selectAll(".hexagon").selectAll("path")
.attr("d", function(f) {
if ((f.x < (dx + r) & f.x > (dx - r)) & (f.y < (dy + r) & f.y > (dy - r))) {
return hexbin.hexagon(radiusScale((5+f.length)*2))
}
else {
return hexbin.hexagon(radiusScale((f.length)))
}
})
})
.on('mouseout', function(d) {
d3.selectAll(".hexagon").selectAll("path")
.attr("d", d => hexbin.hexagon(radiusScale(d.length)))
})

Related

Change zooming from geometric to sementic ( zooming only on x-axis )

I have a graph of lines paths .
Actually when I zoom it applies a transform attribute.
I would like to make a semantic zoom by zooming only on the x-axis.
Here's the code for zooming
private manageZoom(svgs: AllSvg, allAxis: AllAxis, dimension: Dimension): D3ZoomBehavior {
const zoom: D3ZoomBehavior = d3
.zoom()
.scaleExtent([1, 40])
.translateExtent([
[0, 0],
[dimension.width, dimension.height]
])
.on('zoom', zoomed.bind(null, allAxis));
svgs.svgContainer.call(zoom);
return zoom;
function zoomed({ xAxis, xAxisBottom, yAxis }: AllAxis, { transform }: any) {
svgs.sillons.attr('transform', transform);
xAxisBottom.axisContainer.call(xAxisBottom.axis.scale(transform.rescaleX(xAxisBottom.scale)) as any);
}
}
the sillons object is an array of paths + text + circles
I would that the lines get re-drawed in the right position as the x-axis get larger, but not zoom sillons on y-axis.
I have checked many posts but can't repoduce them to solve my issue. for example
When you set up something along the lines of
svg.call(
d3.zoom()
.on("zoom", zoom)
)
the zoom function can be just about anything that you want. The first argument of zoom is the zoom event itself. Let's denote it by evt. Then
evt.transform.k tells you the scale factor,
evt.transform.x tells you the horizontal translation, and
evt.transform.y tells you the vertical translation.
You don't have to use all of those, though. Rather, you can redraw your image however you want.
Here's a slightly cute example that rescales the image only horizontally.
let w = 500;
let h = 100;
let svg = d3
.select("#container")
.append("svg")
.attr("width", w)
.attr("height", h)
.style("border", "solid 1px black");
let n = 500;
let pts0 = d3.range(n).map((_) => [d3.randomNormal(w / 2, w / 20)(), 0]);
let pts1 = pts0.map((pt) => [w - pt[0], h]);
let g = svg.append("g");
let link_group = g.append("g");
link_group
.selectAll("path")
.data(d3.range(n))
.join("path")
.attr("d", (i) => d3.linkVertical()({ source: pts0[i], target: pts1[i] }))
.attr("fill", "none")
.attr("stroke", "#000")
.attr("stroke-opacity", 0.1)
.attr("stroke-width", 1.5);
let all_pts = pts0.concat(pts1);
let circle_group = g.append("g");
let circles = circle_group
.attr("fill", "black")
.attr("fill-opacity", 0.2)
.selectAll("circle")
.data(all_pts)
.join("circle")
.attr("cx", (d) => d[0])
.attr("cy", (d) => d[1])
.attr("data-x", (d) => d[0])
.attr("r", 4);
svg.call(
d3
.zoom()
.scaleExtent([1 / 4, 20])
.duration(500)
.on("zoom", function (evt) {
let k = evt.transform.k;
link_group.selectAll("path").attr("d", function (i) {
let x00 = pts0[i][0];
let x01 = k * (x00 - w / 2) + w / 2;
let x10 = pts1[i][0];
let x11 = k * (x10 - w / 2) + w / 2;
return d3.linkVertical()({ source: [x01, 0], target: [x11, h] });
});
circle_group
.selectAll("circle")
.nodes()
.forEach(function (c) {
let x0 = c.getAttribute("data-x");
let k = evt.transform.k;
let x1 = k * (x0 - w / 2) + w / 2;
c.setAttribute("cx", x1);
});
})
);
<script src="https://d3js.org/d3.v7.min.js"></script>
<div id="container"></div>

d3js beeswarm with force simulation

I try to do a beeswarm plot with different radius; inspired by this code
The issue I have, is that my point are offset regarding my x axis:
The point on the left should be at 31.7%. I don't understand why, so I would appreciate if you could guide me. This could be improved by changing the domain of x scale, but this can't match the exact value; same issue if I remove the d3.forceCollide()
Thank you,
Data are available here.
Here is my code:
$(document).ready(function () {
function tp(d) {
return d.properties.tp60;
}
function pop_mun(d) {
return d.properties.pop_mun;
}
var margin = {top: 20, right: 20, bottom: 20, left: 40},
width = 1280 - margin.right - margin.left,
height = 300 - margin.top - margin.bottom;
var svg = d3.select("body")
.append("svg")
.attr("viewBox", `0 0 ${width} ${height}`)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var z = d3.scaleThreshold()
.domain([.2, .3, .4, .5, .6, .7])
.range(["#35ff00", "#f1a340", "#fee0b6",
"#ff0000", "#998ec3", "#542788"]);
var loading = svg.append("text")
.attr("x", (width) / 2)
.attr("y", (height) / 2)
// .attr("dy", ".35em")
.style("text-anchor", "middle")
.text("Simulating. One moment please…");
var formatPercent = d3.format(".0%"),
formatNumber = d3.format(".0f");
d3.json('static/data/qp_full.json').then(function (data) {
features = data.features
//1 create scales
var x = d3.scaleLinear()
.domain([0, d3.max(features, tp)/100])
.range([0, width - margin.right])
var y = d3.scaleLinear().domain([0, 0.1]).range([margin.left, width - margin.right])
var r = d3.scaleSqrt().domain([0, d3.max(features, pop_mun)])
.range([0, 25]);
//2 create axis
var xAxis = d3.axisBottom(x).ticks(20)
.tickFormat(formatPercent);
svg.append("g")
.attr("class", "x axis")
.call(xAxis);
var nodes = features.map(function (node, index) {
return {
radius: r(node.properties.pop_mun),
color: '#ff7f0e',
x: x(node.properties.tp60 / 100),
y: height + Math.random(),
pop_mun: node.properties.pop_mun,
tp60: node.properties.tp60
};
});
function tick() {
for (i = 0; i < nodes.length; i++) {
var node = nodes[i];
node.cx = node.x;
node.cy = node.y;
}
}
setTimeout(renderGraph, 10);
function renderGraph() {
// Run the layout a fixed number of times.
// The ideal number of times scales with graph complexity.
// Of course, don't run too long—you'll hang the page!
const NUM_ITERATIONS = 1000;
var force = d3.forceSimulation(nodes)
.force('charge', d3.forceManyBody().strength(-3))
.force('center', d3.forceCenter(width / 2, height/2))
.force('x', d3.forceX(d => d.x))
.force('y', d3.forceY(d => d.y))
.force('collide', d3.forceCollide().radius(d => d.radius))
.on("tick", tick)
.stop();
force.tick(NUM_ITERATIONS);
force.stop();
svg.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", d => d.radius)
.style("fill", d => z(d.tp60/100))
.on("mouseover", function (d, i) {
d3.select(this).style('fill', "orange")
console.log(i.tp60,i)
svg.append("text")
.attr("id", "t")
.attr("x", function () {
return d.x - 50;
})
.attr("y", function () {
return d.y - 50;
})
.text(function () {
return [x.invert(i.x), i.tp60]; // Value of the text
})
})
.on("mouseout", function (d, i) {
d3.select("#t").remove(); // Remove text location
console.log(i)
d3.select(this).style('fill', z(i.tp60/100));
});
loading.remove();
}
})
})

Slight lag in translation animation using D3.interval()

I have created a time series animation. Essentially, I generate a random datum every 20ms and the time axis slides continuously leftwards on a one second interval. D3.interval() creates what I want, but the graph seems "choppy". I assume it has something to do with the web browser or D3? Here is my code (expand the snippet window for a better view):
////////////////////////////////////////////////////////////
////////////////////// Set-up /////////////////////////////
////////////////////////////////////////////////////////////
const margin = { left: 80, right: 80, top: 30, bottom: 165},
TIME_MEASUREMENT = 20,
TIME_INTERVAL = 1000,
TIME_FRAME = 10000;
let width = $("#chart").width() - margin.left - margin.right,
height = $("#chart").height() - margin.top - margin.bottom;
let date1 = 0,
date2 = TIME_FRAME;
let accuracy = 20,
precision = 20,
aF = 2, //accuracyFactor
pF = 1; //precisionFactor
let random = d3.randomUniform(-(pF * precision), pF * precision),
count = TIME_FRAME/TIME_MEASUREMENT + 1;
let data = d3.range(count).map((d,i) => random() + accuracy );
const yDomain = [0, 50],
yTickValues = [0, 10, 20, 30, 40, 50];
////////////////////////////////////////////////////////////
///////////////////////// SVG //////////////////////////////
////////////////////////////////////////////////////////////
const svg = d3.select("#chart")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
const g = svg.append("g")
.attr("transform", "translate(" + margin.left + ", " + margin.top + ")");
////////////////////////////////////////////////////////////
///////////////////// Axes & Scales ////////////////////////
////////////////////////////////////////////////////////////
//X axis - dynamic
let xAxisGroup = g.append("g")
.attr("class", "x-axis")
.attr("transform", "translate(0," + (height) + ")");
let xScale = d3.scaleTime();
const xAxis = d3.axisBottom(xScale)
.ticks(d3.timeSecond.every(1))
.tickSizeInner(15)
.tickFormat(d3.timeFormat("%M:%S"));
//Y axis - static
let yAxisGroup = g.append("g")
.attr("class", "y-axis");
let yScale = d3.scaleLinear()
.domain(yDomain)
.range([height, 0]);
const yAxis = d3.axisLeft()
.scale(yScale)
.tickValues(yTickValues)
.tickFormat(d3.format(".0f"));
yAxisGroup.call(yAxis);
//Data points - static
let dataScale = d3.scaleTime()
.domain([date1,date2])
.range([0,width]);
////////////////////////////////////////////////////////////
/////////////////////// Functions //////////////////////////
////////////////////////////////////////////////////////////
function drawData(){
xScale.domain([date1, date2])
.range([0, width]);
xAxisGroup
.transition()
.duration(TIME_MEASUREMENT)
.ease(d3.easeLinear)
.call(xAxis);
g.selectAll("circle")
.data(data)
.join(
function(enter){
enter.append("circle")
.attr("cx", (d,i) => dataScale(i*TIME_MEASUREMENT))
.attr("cy", (d,i) => yScale(d))
.attr("fill", "black")
.attr("r","3");
},
function(update){
update
.attr("cx", (d,i) => dataScale(i*TIME_MEASUREMENT))
.attr("cy", (d,i) => yScale(d))
.transition()
.ease(d3.easeLinear)
.attr("transform", "translate(" + dataScale(-TIME_MEASUREMENT) + ", 0)");
}
);
data.push(random() + accuracy);
data.shift();
date1 += TIME_INTERVAL/50;
date2 += TIME_INTERVAL/50;
};
////////////////////////////////////////////////////////////
///////////////////////// Main /////////////////////////////
////////////////////////////////////////////////////////////
d3.interval(drawData, TIME_MEASUREMENT);
x-axis,
.y-axis {
font-size: 0.8em;
stroke-width: 0.06em;
}
#chart {
width: 600px;
height: 500px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="//d3js.org/d3.v6.min.js"></script>
<div id="chart"></div>
I don't get this "choppy effect" when I use d3.timer or setInterval, but each second slides as if it were ~850ms. Thank you for your input!

D3.js brush behaviour fires two 'end' events

So, implementing a brush behaviour inspired from M Bostock example I came across something I did not quite understand.
If set a callback for the 'end' event of the brush, this gets called as expected whenever you're interacting directly with the brush.
But whenever I recenter the brush, it seems that the end event is fired twice.
Why is that the case? Or, is it something I'm doing wrong here?
<!DOCTYPE html>
<style>
.selected {
fill: red;
stroke: brown;
}
</style>
<svg width="960" height="150"></svg>
<div>Event fired <span id="test"></span></div>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var fired=0;
var randomX = d3.randomUniform(0, 10),
randomY = d3.randomNormal(0.5, 0.12),
data = d3.range(800).map(function() { return [randomX(), randomY()]; });
var svg = d3.select("svg"),
margin = {top: 10, right: 50, bottom: 30, left: 50},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var x = d3.scaleLinear()
.domain([0, 10])
.range([0, width]);
var y = d3.scaleLinear()
.range([height, 0]);
var brush = d3.brushX()
.extent([[0, 0], [width, height]])
.on("start brush", brushed)
.on("end", brushend);
var dot = g.append("g")
.attr("fill-opacity", 0.2)
.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("transform", function(d) { return "translate(" + x(d[0]) + "," + y(d[1]) + ")"; })
.attr("r", 3.5);
g.append("g")
.call(brush)
.call(brush.move, [3, 5].map(x))
.selectAll(".overlay")
.each(function(d) { d.type = "selection"; }) // Treat overlay interaction as move.
.on("mousedown touchstart", brushcentered); // Recenter before brushing.
g.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
function brushcentered() {
var dx = x(1) - x(0), // Use a fixed width when recentering.
cx = d3.mouse(this)[0],
x0 = cx - dx / 2,
x1 = cx + dx / 2;
d3.select(this.parentNode).call(brush.move, x1 > width ? [width - dx, width] : x0 < 0 ? [0, dx] : [x0, x1]);
}
function brushed() {
var extent = d3.event.selection.map(x.invert, x);
dot.classed("selected", function(d) { return extent[0] <= d[0] && d[0] <= extent[1]; });
}
function brushend() {
document.getElementById('test').innerHTML = ++fired;
// console.log('end fired - ' + (++fired));
}
</script>
Whenever you want to stop an event from triggering multiple layers of actions, you can use:
d3.event.stopPropagation();
Here you can include it at the end of the brushcentered function:
function brushcentered() {
var dx = x(1) - x(0), // Use a fixed width when recentering.
cx = d3.mouse(this)[0],
x0 = cx - dx / 2,
x1 = cx + dx / 2;
d3.select(this.parentNode).call(brush.move, x1 > width ? [width - dx, width] : x0 < 0 ? [0, dx] : [x0, x1]);
d3.event.stopPropagation();
}
And the demo:
<style>
.selected {
fill: red;
stroke: brown;
}
</style>
<svg width="960" height="150"></svg>
<div>Event fired <span id="test"></span></div>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var fired=0;
var randomX = d3.randomUniform(0, 10),
randomY = d3.randomNormal(0.5, 0.12),
data = d3.range(800).map(function() { return [randomX(), randomY()]; });
var svg = d3.select("svg"),
margin = {top: 10, right: 50, bottom: 30, left: 50},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var x = d3.scaleLinear()
.domain([0, 10])
.range([0, width]);
var y = d3.scaleLinear()
.range([height, 0]);
var brush = d3.brushX()
.extent([[0, 0], [width, height]])
.on("start brush", brushed)
.on("end", brushend);
var dot = g.append("g")
.attr("fill-opacity", 0.2)
.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("transform", function(d) { return "translate(" + x(d[0]) + "," + y(d[1]) + ")"; })
.attr("r", 3.5);
g.append("g")
.call(brush)
.call(brush.move, [3, 5].map(x))
.selectAll(".overlay")
.each(function(d) { d.type = "selection"; }) // Treat overlay interaction as move.
.on("mousedown touchstart", brushcentered); // Recenter before brushing.
g.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
function brushcentered() {
var dx = x(1) - x(0), // Use a fixed width when recentering.
cx = d3.mouse(this)[0],
x0 = cx - dx / 2,
x1 = cx + dx / 2;
d3.select(this.parentNode).call(brush.move, x1 > width ? [width - dx, width] : x0 < 0 ? [0, dx] : [x0, x1]);
d3.event.stopPropagation();
}
function brushed() {
var extent = d3.event.selection.map(x.invert, x);
dot.classed("selected", function(d) { return extent[0] <= d[0] && d[0] <= extent[1]; });
}
function brushend() {
document.getElementById('test').innerHTML = ++fired;
}
</script>
-UPDATE-
For the purpose of this snippet, I can use a boolean flag to stop the first event and let the second go through. This means that I am still able to drag the brush after recentering, all in one go.
<!DOCTYPE html>
<style>
.selected {
fill: red;
stroke: brown;
}
</style>
<svg width="960" height="150"></svg>
<div>Event fired <span id="test"></span></div>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var fired=0;
var justcentered = false;
var randomX = d3.randomUniform(0, 10),
randomY = d3.randomNormal(0.5, 0.12),
data = d3.range(800).map(function() {
return [randomX(), randomY()];
});
var svg = d3.select("svg"),
margin = { top: 10, right: 50, bottom: 30, left: 50 },
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var x = d3.scaleLinear()
.domain([0, 10])
.range([0, width]);
var y = d3.scaleLinear()
.range([height, 0]);
var brush = d3.brushX()
.extent([[0, 0], [width, height]])
.on("start brush", brushed)
.on("end", brushend);
var dot = g.append("g")
.attr("fill-opacity", 0.2)
.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("transform", function(d) {
return "translate(" + x(d[0]) + "," + y(d[1]) + ")";
})
.attr("r", 3.5);
g.append("g")
.call(brush)
.call(brush.move, [3, 5].map(x))
.selectAll(".overlay")
.each(function(d) { d.type = "selection"; }) // Treat overlay interaction as move.
.on("mousedown touchstart", brushcentered); // Recenter before brushing.
g.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
function brushcentered() {
var dx = x(1) - x(0), // Use a fixed width when recentering.
cx = d3.mouse(this)[0],
x0 = cx - dx / 2,
x1 = cx + dx / 2;
justcentered = true;
d3.select(this.parentNode)
.call(brush.move, x1 > width ? [width - dx, width] : x0 < 0 ? [0, dx] : [x0, x1]);
}
function brushed() {
var extent = d3.event.selection.map(x.invert, x);
dot.classed("selected", function(d) { return extent[0] <= d[0] && d[0] <= extent[1]; });
}
function brushend() {
if(justcentered) {
justcentered = false;
return;
}
document.getElementById('test').innerHTML = ++fired;
}
</script>

Using adjacent data values in d3

I am using D3 to visualize some data that updates with time, and I have a list of (x,y) coordinates; for example:
[[0.1,0.2],
[0.3,0.4],
[0.5,0.4],
[0.7,0.2]]
I would like to draw triangles from (0,0) to each of the pairs of adjacent coordinates, for example, the first triangle would have coordinates (0,0), (0.1,0.2), (0.3,0.4) and the second triangle would have coordinates (0,0), (0.3,0.4), (0.5,0.4) and so on.
My question is whether there is a way to access "neighboring" values in D3; the D3 paradigm seems to be to pass in a function that gets access to each data value separately. So I was able to do this, but only by explicitly constructing a new data set of the triangle coordinates from the entire data set of the individual points:
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 500 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// add the graph canvas to the body of the webpage
var svg = d3.select("div#plot1").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
var axis = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var xsc = d3.scaleLinear()
.domain([-2, 2]) // the range of the values to plot
.range([ 0, width ]); // the pixel range of the x-axis
var ysc = d3.scaleLinear()
.domain([-2, 2])
.range([ height, 0 ]);
var closedLine = d3.line()
.x(function(d){ return xsc(d[0]); })
.y(function(d){ return ysc(d[1]); })
.curve(d3.curveLinearClosed);
function attrfunc(f,attr) {
return function(d) {
return f(d[attr]);
};
}
function doit(data)
{
var items = axis.selectAll("path.item")
.data(data);
items.enter()
.append("path")
.attr("class", "item")
.merge(items)
.attr("d", attrfunc(closedLine, "xy"))
.attr("stroke", "gray")
.attr("stroke-width", 1)
.attr("stroke-opacity", function(d) { return 1-d.age;})
.attr("fill", "gray")
.attr("fill-opacity", function(d) {return 1-d.age;});
items.exit().remove();
}
var state = {
t: 0,
theta: 0,
omega: 0.5,
A: 1.0,
N: 60,
history: []
}
d3.timer(function(elapsed)
{
var S = state;
if (S.history.length > S.N)
S.history.shift();
var dt = Math.min(0.1, elapsed*1e-3);
S.t += dt;
S.theta += S.omega * dt;
var sample = {
t: S.t,
x: S.A*(Math.cos(S.theta)+0.1*Math.cos(6*S.theta)),
y: S.A*(Math.sin(S.theta)+0.1*Math.sin(6*S.theta))
}
S.history.push(sample);
// Create triangular regions
var data = [];
for (var k = 0; k < S.history.length-1; ++k)
{
var pt1 = S.history[k];
var pt2 = S.history[k+1];
data.push({age: (S.history.length-1-k)/S.N,
xy:
[[0,0],
[pt1.x,pt1.y],
[pt2.x,pt2.y]]
});
}
doit(data);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.8.0/d3.min.js"></script>
<div id="plot1">
</div>
You can get any point in the data array using the second argument, which is the index.
When you pass a datum to any D3 method, traditionally using a parameter named d (for datum), you are in fact using data[i], i being the current index. You can change this index to get data points before or after the current datum.
Thus, in any D3 method:
.attr("foo", function(d, i){
console.log(d)//this is the current datum
console.log(data[i])//this is the same current datum!
console.log(data[i + 1])//this is the next (adjacent) datum
});
Here is a simple snippet showing this:
var data = ["foo", "bar", "baz", "foobar", "foobaz"];
var foo = d3.selectAll("foo")
.data(data)
.enter()
.append("foo")
.attr("foo", function(d, i) {
if (data[i + 1]) {
console.log("this datum is " + d + ", the next datum is " + data[i + 1]);
}
})
<script src="https://d3js.org/d3.v4.min.js"></script>
Have a look at the if statement: we have to check if there is a data[i + 1] because, of course, the last data point has no adjacent data after it.
Here is a demo using your data array:
var svg = d3.select("svg");
var scale = d3.scaleLinear()
.domain([-1, 1])
.range([0, 150]);
var data = [
[0.1, 0.2],
[0.3, 0.4],
[0.5, 0.4],
[0.7, 0.2]
];
var triangles = svg.selectAll("foo")
.data(data)
.enter()
.append("polygon");
triangles.attr("stroke", "teal")
.attr("stroke-width", 2)
.attr("fill", "none")
.attr("points", function(d, i) {
if (data[i + 1]) {
return scale(0) + "," + scale(0) + " " + scale(data[i][0]) + "," + scale(data[i][1]) + " " + scale(data[i + 1][0]) + "," + scale(data[i + 1][1]) + " " + scale(0) + "," + scale(0);
}
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="150" height="150"></svg>
PS: I'm not using your snippet because I'm drawing the triangles using polygons, but the principle is the same.

Categories