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>
Related
I am trying to do a zoomable heatmap and the community here on SO have helped massively, however I am now stuck the whole day today trying to fix a glitch and I am hitting the wall every single time.
The issue is that the zoom looks jumpy, ie the plot is rendered fine, however when the zoom event is triggered some kind of transformation happens that changes the axes and the scaling in an abrupt way. The code below demonstrates this issue. The problem does not always happen, it depends on the heatmap dimension and/or the number of the dots.
Some similar cases from people with the same problem here on SO turned out to be that the zoom was not applied to the correct object but I think I am not doing that mistake. Many thanks in advance
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<style>
.axis text {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000000;
}
.x.axis path {
//display: none;
}
.chart rect {
fill: steelblue;
}
.chart text {
fill: white;
font: 10px sans-serif;
text-anchor: end;
}
#tooltip {
position: absolute;
background-color: #2B292E;
color: white;
font-family: sans-serif;
font-size: 15px;
pointer-events: none;
/*dont trigger events on the tooltip*/
padding: 15px 20px 10px 20px;
text-align: center;
opacity: 0;
border-radius: 4px;
}
</style>
<title>Heatmap Chart</title>
<!-- Reference style.css -->
<!-- <link rel="stylesheet" type="text/css" href="style.css">-->
<!-- Reference minified version of D3 -->
<script src='https://d3js.org/d3.v4.min.js' type='text/javascript'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'></script>
<script src='heatmap.js' type='text/javascript'></script>
</head>
<body>
<div id="chart">
<svg width="550" height="1000"></svg>
</div>
<script>
var dataset = [];
for (let i = 1; i < 60; i++) { //360
for (j = 1; j < 70; j++) { //75
dataset.push({
xKey: i,
xLabel: "xMark " + i,
yKey: j,
yLabel: "yMark " + j,
val: Math.random() * 25,
})
}
};
var svg = d3.select("#chart")
.select("svg")
var xLabels = [],
yLabels = [];
for (i = 0; i < dataset.length; i++) {
if (i == 0) {
xLabels.push(dataset[i].xLabel);
var j = 0;
while (dataset[j + 1].xLabel == dataset[j].xLabel) {
yLabels.push(dataset[j].yLabel);
j++;
}
yLabels.push(dataset[j].yLabel);
} else {
if (dataset[i - 1].xLabel == dataset[i].xLabel) {
//do nothing
} else {
xLabels.push(dataset[i].xLabel);
}
}
};
var margin = {
top: 0,
right: 25,
bottom: 40,
left: 75
};
var width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
var dotSpacing = 0,
dotWidth = width / (2 * (xLabels.length + 1)),
dotHeight = height / (2 * yLabels.length);
// var dotWidth = 1,
// dotHeight = 3,
// dotSpacing = 0.5;
var daysRange = d3.extent(dataset, function(d) {
return d.xKey
}),
days = daysRange[1] - daysRange[0];
var hoursRange = d3.extent(dataset, function(d) {
return d.yKey
}),
hours = hoursRange[1] - hoursRange[0];
var tRange = d3.extent(dataset, function(d) {
return d.val
}),
tMin = tRange[0],
tMax = tRange[1];
// var width = (dotWidth * 2 + dotSpacing) * days,
// height = (dotHeight * 2 + dotSpacing) * hours;
// var width = +svg.attr("width") - margin.left - margin.right,
// height = +svg.attr("height") - margin.top - margin.bottom;
var colors = ['#2C7BB6', '#00A6CA', '#00CCBC', '#90EB9D', '#FFFF8C', '#F9D057', '#F29E2E', '#E76818', '#D7191C'];
// the scale
var scale = {
x: d3.scaleLinear()
.domain([-1, d3.max(dataset, d => d.xKey)])
.range([-1, width]),
y: d3.scaleLinear()
.domain([0, d3.max(dataset, d => d.yKey)])
.range([height, 0]),
//.range([(dotHeight * 2 + dotSpacing) * hours, dotHeight * 2 + dotSpacing]),
};
var xBand = d3.scaleBand().domain(xLabels).range([0, width]),
yBand = d3.scaleBand().domain(yLabels).range([height, 0]);
var axis = {
x: d3.axisBottom(scale.x).tickFormat((d, e) => xLabels[d]),
y: d3.axisLeft(scale.y).tickFormat((d, e) => yLabels[d]),
};
function updateScales(data) {
scale.x.domain([-1, d3.max(data, d => d.xKey)]),
scale.y.domain([0, d3.max(data, d => d.yKey)])
}
var colorScale = d3.scaleQuantile()
.domain([0, colors.length - 1, d3.max(dataset, function(d) {
return d.val;
})])
.range(colors);
var zoom = d3.zoom()
.scaleExtent([dotWidth, dotHeight])
.on("zoom", zoomed);
var tooltip = d3.select("body").append("div")
.attr("id", "tooltip")
.style("opacity", 0);
// SVG canvas
svg = d3.select("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
//.call(zoom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Clip path
svg.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height + dotHeight);
// Heatmap dots
var heatDotsGroup = svg.append("g")
.attr("clip-path", "url(#clip)")
.append("g");
heatDotsGroup.call(zoom);
//Create X axis
var renderXAxis = svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + scale.y(-1) + ")")
.call(axis.x)
//Create Y axis
var renderYAxis = svg.append("g")
.attr("class", "y axis")
.call(axis.y);
function zoomed() {
d3.event.transform.y = 0;
d3.event.transform.x = Math.min(d3.event.transform.x, 5);
d3.event.transform.x = Math.max(d3.event.transform.x, (1 - d3.event.transform.k) * width);
d3.event.transform.k = Math.max(d3.event.transform.k, 1);
//console.log(d3.event.transform)
// update: rescale x axis
renderXAxis.call(axis.x.scale(d3.event.transform.rescaleX(scale.x)));
heatDotsGroup.attr("transform", d3.event.transform.toString().replace(/scale\((.*?)\)/, "scale($1, 1)"));
}
svg.call(renderPlot, dataset)
function renderPlot(selection, dataset) {
//updateScales(dataset);
heatDotsGroup.selectAll("ellipse")
.data(dataset)
.enter()
.append("ellipse")
.attr("cx", function(d) {
return scale.x(d.xKey) - xBand.bandwidth();
})
.attr("cy", function(d) {
return scale.y(d.yKey) + yBand.bandwidth();
})
.attr("rx", dotWidth)
.attr("ry", dotHeight)
.attr("fill", function(d) {
return colorScale(d.val);
})
.on("mouseover", function(d) {
$("#tooltip").html("X: " + d.xKey + "<br/>Y: " + d.yKey + "<br/>Value: " + Math.round(d.val * 100) / 100);
var xpos = d3.event.pageX + 10;
var ypos = d3.event.pageY + 20;
$("#tooltip").css("left", xpos + "px").css("top", ypos + "px").animate().css("opacity", 1);
}).on("mouseout", function() {
$("#tooltip").animate({
duration: 500
}).css("opacity", 0);
});
}
</script>
</body>
</html>
Change the zoom scaleExtend
var zoom = d3.zoom()
.scaleExtent([1, dotHeight])
.on("zoom", zoomed);
Call the zoom on the whole svg not on the heatDotsGroup because this node receives the tranformation, and also not on the g node that has the graph transformation here variable svg (to keep things a bit obscure)
svg = d3.select("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.call(zoom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// heatDotsGroup.call(zoom);
Don't limit the zoom scale k in the tick. Already taken care of by the scaleExtent()
// d3.event.transform.k = Math.max(d3.event.transform.k, 1);
Why calculate all the d3.max() when you already have calculated the d3.extent() of these values?
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)))
})
I'd like to space the first tick from the origin point, but would also like to have a line connecting them. I suppose I could append an svg to do this for me, but there has to be an easier way that I am missing in the documentation. An image example of what I'm aiming for can be found here
Here's an example of the issue I'm having:
var margin = {
top: 20,
right: 10,
bottom: 40,
left: 40
};
var padding = 20;
var height = 100;
var width = 400
var xAxisTimeScale = [];
for(var i = 8; i < 21; i++) {
xAxisTimeScale.push(i);
}
// scales
var xScale = d3.scaleLinear()
.domain([0, 12])
.range([padding, width - padding]);
var yScale = d3.scaleLinear()
.domain([0, 10])
.range([height, 0]);
function convertTimeToString(time) {
if(time > 12) {
return (time - 12) + "PM";
} else {
return time + "AM";
}
}
var xAxis = d3.axisBottom(xScale)
.ticks(13)
.tickFormat(function(d,i){ return convertTimeToString(xAxisTimeScale[i]);});
var yAxis = d3.axisLeft(yScale);
var svg = d3.select("body").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 + ")");
// add axes
svg.append('g')
.call(yAxis);
svg.append('g')
.attr('transform', 'translate(0, ' + height + ')')
.call(xAxis);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.6.0/d3.min.js"></script>
Here are the approaches I came up with:
Change x axis's path's d attribute to 'M0,0.5V0.5H'+(width-padding). Relevant code changes:
svg.select('.x.axis path.domain').attr('d', function() {
return 'M0,0.5V0.5H'+(width-padding);
});
How did I come up with 0.5 in there? I analyzed d3.js and came across the V0.5 (which was V0 in d3-version3 used for creating X-axis domain. For more details on how a path is formed, check out SVG path d attribute. Using this offset, here's a code snippet implementing the same:
var margin = {
top: 20,
right: 10,
bottom: 40,
left: 40
};
var padding = 20;
var height = 100;
var width = 400
var xAxisTimeScale = [];
for(var i = 8; i < 21; i++) {
xAxisTimeScale.push(i);
}
// scales
var xScale = d3.scaleLinear()
.domain([0, 12])
.range([padding, width - padding]);
var yScale = d3.scaleLinear()
.domain([0, 10])
.range([height, 0]);
function convertTimeToString(time) {
if(time > 12) {
return (time - 12) + "PM";
} else {
return time + "AM";
}
}
var xAxis = d3.axisBottom(xScale)
.ticks(13)
.tickFormat(function(d,i){ return convertTimeToString(xAxisTimeScale[i]);});
var yAxis = d3.axisLeft(yScale);
var svg = d3.select("body").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 + ")");
// add axes
svg.append('g')
.call(yAxis);
svg.append('g').classed('x axis', true)
.attr('transform', 'translate(0, ' + height + ')')
.call(xAxis);
svg.select('.x.axis path.domain').attr('d', function() {
return 'M0,0.5V0.5H'+(width-padding);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.6.0/d3.min.js"></script>
Add a line explicitly from origin to the start-point of X-axis with a simple line code. Relevant code changes:
svg.append('line').classed('connecting-line', true)
.attr('y1', height+0.5).attr('y2', height+0.5).attr('x1', 0).attr('x2', padding).style('stroke', '#000');
0.5 has the same reason as above. Rest attributes are just based on height and width. Here's a code snippet implementing this:
var margin = {
top: 20,
right: 10,
bottom: 40,
left: 40
};
var padding = 20;
var height = 100;
var width = 400
var xAxisTimeScale = [];
for(var i = 8; i < 21; i++) {
xAxisTimeScale.push(i);
}
// scales
var xScale = d3.scaleLinear()
.domain([0, 12])
.range([padding, width - padding]);
var yScale = d3.scaleLinear()
.domain([0, 10])
.range([height, 0]);
function convertTimeToString(time) {
if(time > 12) {
return (time - 12) + "PM";
} else {
return time + "AM";
}
}
var xAxis = d3.axisBottom(xScale)
.ticks(13)
.tickFormat(function(d,i){ return convertTimeToString(xAxisTimeScale[i]);});
var yAxis = d3.axisLeft(yScale);
var svg = d3.select("body").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 + ")");
// add axes
svg.append('g')
.call(yAxis);
svg.append('g').classed('x axis', true)
.attr('transform', 'translate(0, ' + height + ')')
.call(xAxis);
svg.append('line').classed('connecting-line', true)
.attr('y1', height+0.5).attr('y2', height+0.5).attr('x1', 0).attr('x2', padding).style('stroke', '#000');
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.6.0/d3.min.js"></script>
Adding an overlay rectangle either as a box OR as a rect styled with stroke-dasharray. But I think this wouldn't be that helpful as it would be a bit of overriding stuff.
Hope any of the above approaches serves the purpose. And I'm really sorry for not getting back on time (Friday night got me) :)
Given that you have a linear scale disguised as a time scale, the solution here will be an ad hoc one. Otherwise, the solution could be more idiomatic.
First, increase your xAxisTimeScale:
var xAxisTimeScale = d3.range(7, 23, 1)
Then, increase your domain...
.domain([0, 13])
... and remove the first tick:
.tickFormat(function(d, i) {
return i ? convertTimeToString(xAxisTimeScale[i]) : null;
});
Here is your code with those changes:
var margin = {
top: 20,
right: 10,
bottom: 40,
left: 40
};
var padding = 20;
var height = 100;
var width = 400
var xAxisTimeScale = d3.range(7, 23, 1)
// scales
var xScale = d3.scaleLinear()
.domain([0, 13])
.range([0, width - padding]);
var yScale = d3.scaleLinear()
.domain([0, 10])
.range([height, 0]);
function convertTimeToString(time) {
if (time > 12) {
return (time - 12) + "PM";
} else {
return time + "AM";
}
}
var xAxis = d3.axisBottom(xScale)
.ticks(13)
.tickFormat(function(d, i) {
return i ? convertTimeToString(xAxisTimeScale[i]) : null;
});
var yAxis = d3.axisLeft(yScale);
var svg = d3.select("body").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 + ")");
// add axes
svg.append('g')
.call(yAxis);
svg.append('g')
.attr('transform', 'translate(0, ' + height + ')')
.call(xAxis);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.6.0/d3.min.js"></script>
I have been following the block that uses hexagonal binning of random points with the normal distribution but instead trying to tailor it to the exponential distribution.
The code runs, but the output seems to show a mirror along the x-axis. That is, the points are all clustered along the upper-left instead of lower-left. I've been playing with the transform function but can't quite get it. What am I missing? JSFiddle
<!DOCTYPE html>
<style>
.hexagon {
stroke: #000;
stroke-width: 0.5px;
}
</style>
<svg width="500" height="200"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-hexbin.v0.2.min.js"></script>
<script>
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 + ")");
var randomX = d3.randomExponential(1 / 30),
randomY = d3.randomExponential(1 / 30),
points = d3.range(2000).map(function() { return [randomX(), randomY()]; });
var color = d3.scaleSequential(d3.interpolateLab("white", "steelblue"))
.domain([0, 20]);
var hexbin = d3.hexbin()
.radius(5)
.extent([[0, 0], [width, height]]);
var x = d3.scaleLinear()
.domain([0, width])
.range([0, width]);
var y = d3.scaleLinear()
.domain([0, height])
.range([height, 0]);
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", hexbin.hexagon())
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
.attr("fill", function(d) { return color(d.length); });
</script>
You set your scales, but you never use them:
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
Solution: use your scales:
.attr("transform", function(d) {
return "translate(" + x(d.x) + "," + y(d.y) + ")";
//scales here --------^--------------^
})
Here is your code with that change:
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 + ")");
var randomX = d3.randomExponential(1 / 30),
randomY = d3.randomExponential(1 / 30),
points = d3.range(2000).map(function() {
return [randomX(), randomY()];
});
var color = d3.scaleSequential(d3.interpolateLab("white", "steelblue"))
.domain([0, 20]);
var hexbin = d3.hexbin()
.radius(5)
.extent([
[0, 0],
[width, height]
]);
var x = d3.scaleLinear()
.domain([0, width])
.range([0, width]);
var y = d3.scaleLinear()
.domain([0, height])
.range([height, 0]);
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", hexbin.hexagon())
.attr("transform", function(d) {
return "translate(" + x(d.x) + "," + y(d.y) + ")";
})
.attr("fill", function(d) {
return color(d.length);
});
.hexagon {
stroke: #000;
stroke-width: 0.5px;
}
<svg width="500" height="200"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-hexbin.v0.2.min.js"></script>
The following snippet is basically this example https://bl.ocks.org/mbostock/4248145 but with custom data points. No matter how I scale or modify my points array, the hexagons are always at the upper left corner, though it seems that the distribution is displayed correctly.
How can I fix this?
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 + ")");
var points = [[1,1]]
var color = d3.scaleSequential(d3.interpolateLab("white", "#5B85AA"))
.domain([0, 3]);
var hexbin = d3.hexbin()
.radius(20)
.size([0, 3]);
var x = d3.scaleLinear()
.domain([1, 4])
.range([0, width]);
var y = d3.scaleLinear()
.domain([1, 4])
.range([height, 0]);
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", hexbin.hexagon())
.attr("transform", function(d) { return "translate(" + 0 + "," + height + ")"; })
.transition()
.duration(1000)
.delay(function (d, i) {
return i * 10;
})
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
.attr("fill", function(d) { return color(d.length); });
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-hexbin.v0.2.min.js"></script>
<svg width="960" height="500"></svg>
That's the expected result, since your data is just:
[1, 1]
Which is a single data point next to the origin. For instance, using the same code but creating 1000 random data points from 0 to the width...
var points = d3.range(1000).map(d=>([Math.random()*width, Math.random()*width]));
... will have a different result:
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 + ")");
var points = d3.range(1000).map(d=>([Math.random()*width, Math.random()*width]));
var color = d3.scaleSequential(d3.interpolateLab("white", "#5B85AA"))
.domain([0, 3]);
var hexbin = d3.hexbin()
.radius(20)
.size([0, 3]);
var x = d3.scaleLinear()
.domain([1, 4])
.range([0, width]);
var y = d3.scaleLinear()
.domain([1, 4])
.range([height, 0]);
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", hexbin.hexagon())
.attr("transform", function(d) { return "translate(" + 0 + "," + height + ")"; })
.transition()
.duration(1000)
.delay(function (d, i) {
return i * 10;
})
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
.attr("fill", function(d) { return color(d.length); });
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-hexbin.v0.2.min.js"></script>
<svg width="960" height="500"></svg>
Besides that, what you said:
No matter how I scale or modify my points array, the hexagons are always at the upper left corner.
Is not accurate. For instance, this is the same code, but using [[100,100]]. You can see the hexagon further down and to the right:
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 + ")");
var points = [[100,100]];
var color = d3.scaleSequential(d3.interpolateLab("white", "#5B85AA"))
.domain([0, 3]);
var hexbin = d3.hexbin()
.radius(20)
.size([0, 3]);
var x = d3.scaleLinear()
.domain([1, 4])
.range([0, width]);
var y = d3.scaleLinear()
.domain([1, 4])
.range([height, 0]);
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", hexbin.hexagon())
.attr("transform", function(d) { return "translate(" + 0 + "," + height + ")"; })
.transition()
.duration(1000)
.delay(function (d, i) {
return i * 10;
})
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
.attr("fill", function(d) { return color(d.length); });
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-hexbin.v0.2.min.js"></script>
<svg width="960" height="500"></svg>