Why are mouse events not firing on correct element? - javascript

I'm having an issue with the following code. A yellow border should appear around the graph that the mouse is over. The 'mouseover'/'mouseout' event only fires for the last instance of the Chart class. Mousing over the first and second instantiations of Chart produce mouseover/mouseout events over the last instantiation of Chart. I've tested this code under Firefox, Safari, and Chrome, with the same results. Why is that - what am I doing wrong?
<!DOCTYPE html>
<html>
<head>
<title>test</title>
<meta charset="utf-8">
</head>
<style>
rect { fill: #fff0e0; stroke: #000; stroke-width: 2.0px; }
.line { fill: none; stroke: steelblue; stroke-width: 3.0px; }
</style>
<body>
<script src="d3.v2.js"></script>
<script>
var kev_vs_rho= [{
values: [{x: 0.01, y: 0.2058},{x: 0.03, y: 0.2039},{x: 0.99, y: 23.2020}] }];
kev_vs_rho.minX=0.01; kev_vs_rho.maxX=0.99;
kev_vs_rho.minY=0.01; kev_vs_rho.maxY=33.66;
</script>
<script>
var kev_vs_sec= [{
values: [{x: 1.5, y: 0.2058},{x: 9.494, y: 1.6468},{x: 1000.0, y:23.4699}] }];
kev_vs_sec.minX=1.50; kev_vs_sec.maxX=1000.00;
kev_vs_sec.minY=0.01; kev_vs_sec.maxY=33.66;
</script>
<div id="chart1"> </div> <hr/>
<div id="chart2"> </div> <hr/>
<div id="chart3"> </div>
<script>
"use strict";
function Chart ( _width, _height, _data, _anchor ) {
self = this;
this.data = _data;
this.anchor = _anchor;
this.margin = {top: 30, right: 30, bottom: 30, left: 80};
this.width = _width - this.margin.left - this.margin.right;
this.height = _height - this.margin.top - this.margin.bottom;
this.xscale = d3.scale.linear()
.domain([this.data.minX, this.data.maxX])
.range([0, this.width]);
this.yscale = d3.scale.linear()
.domain([this.data.minY, this.data.maxY])
.range([this.height, 0]);
this.lineA = d3.svg.line()
.x(function (d) { return self.xscale(d.x); })
.y(function (d) { return self.yscale(d.y); });
this.div = d3.select(this.anchor).append("div");
this.svg = this.div.append("svg")
.datum(this.data[0].values)
.attr("width", this.width + this.margin.left + this.margin.right)
.attr("height", this.height + this.margin.top + this.margin.bottom)
.append("g")
.attr("transform", "translate(" + this.margin.left + "," + this.margin.top + ")");
this.svg.append("rect")
.attr("width", this.width)
.attr("height", this.height);
this.lineB = this.svg.append("path")
.attr("class", "line")
.attr("d", self.lineA);
d3.select(this.anchor)
.on("mouseover", function () {
self.div.style("background", "yellow");
})
.on("mouseout", function () {
self.div.style("background", null);
})
};
Chart.prototype.redraw = function() {
this.lineB
.datum(this.data[0].values)
.attr("d", this.lineA);
};
var chart3 = new Chart( 960, 200, kev_vs_rho, "#chart3");
var chart2 = new Chart( 960, 200, kev_vs_sec, "#chart2");
var chart1 = new Chart( 960, 200, kev_vs_rho, "#chart1");
chart1.redraw();
chart2.redraw();
chart3.redraw();
</script>
</body>
</html>

the two (likely) reasons are either you have a transparent element overlaying the page linked to each graph (and because the 3rd graph is drawn last, it ends on top) that intercepts all the mouse movements, or there's something screwy in your event handler assignment (like a re-used reference).
You can use firebug (or other browsers equivalent) to test for transparent elements (just right click and 'inspect here', and see where it drops you in the DOM object graph). To test the event handler assignment, try changing the last step of your code to:
d3.select(this.anchor)
.on("mouseover", function () {
d3.select(this).select("div").style("background", "yellow");
})
.on("mouseout", function () {
d3.select(this).select("div").style("background", null);
})
Because inside the handler, this will have been set to the element the event was triggered on - so if you are truely mousing over, e.g., Chart2, then this should be set to Chart2, and Chart2 will therefor get the background.
If this doesn't work, can I suggest you put your code into a jsfiddle (including any additional JS / CSS / etc, so that people can actually play with it and see the problem for themselves.
Edit: updated to correct the selection tree.

Related

Mark element as clicked in d3.js v6+

My Problem: I have some data points that are bound to circle elements. Now, each time the user clicks on a circle I want to change its color to red. For this, I want to use an update function which is called each time the user clicks on a circle. Since I want to change not only the clicked circle but also other elements based on which circle was clicked, I need to somehow remember which one was clicked. I saw this being done in d3.js v3 by simply saving the datum to a variable (clickedCircle) in the event listener and recalling it later. However, this doesn't seem to work in d3.js v6+ anymore. What would be the best way to do this in v6+?
Quick d3.js v3 Example (Basic idea adapted from this chart):
var data = [
{x: 10, y: 20},
{x: 30, y: 10},
{x: 20, y: 55},
];
svg = d3.select(#div1)
.append("svg")
.attr("width", 100)
.attr("height", 100)
;
var circles;
var clickedCircle;
function update() {
circles = svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) { return d.position.x } )
.attr("cy", function(d) { return d.position.y } )
.attr("r", 10)
.on("click", function(e, d) { clickedCircle = d; update(); } )
;
circles
.style("fill", function(d) {
if (d === clickedCircle) {
return "red"
} else {
return "black"
}
})
;
}
Here's an example with D3 v7. If you click a circle, that circle becomes red and its data is shown in a text label.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://d3js.org/d3.v7.js"></script>
</head>
<body>
<div id="chart"></div>
<script>
// margin convention set up
const margin = { top: 10, bottom: 10, left: 10, right: 10 };
const width = 110 - margin.left - margin.right;
const height = 110 - margin.top - margin.bottom;
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})`);
// data
const data = [
{ x: 10, y: 20 },
{ x: 30, y: 10 },
{ x: 20, y: 55 },
];
// draw cirlces
const circles = g.selectAll('circle')
.data(data)
.join('circle')
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('r', 10)
.attr('fill', 'black')
.on('click', update);
// text label
const label = g.append('text')
.attr('y', height);
// update when circle is clicked
function update(event, d) {
// reset all circles to black
circles.attr('fill', 'black');
// color the clicked circle red
d3.select(this).attr('fill', 'red');
// update the label
label.text(`x: ${d.x}, y: ${d.y}`);
}
</script>
</body>
</html>

Pie chart not updating D3.JS

I know this question has been asked many times but I am not able to solve the problem of updating my pie chart. I am completely lost. Could you please tell me what seems to the problem here ?
I tried the following way but the chart doesn't seem to update. I added a function change that is supposed to update the chart. I am updating the data, redrawing the arcs and changing the labels but it is not working.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Load D3 -->
<script src="https://d3js.org/d3.v5.min.js"></script>
<title>Document</title>
</head>
<body>
<div id="pie"></div>
<script>
var dataChart = { a: 9, b: 20, c: 30, d: 8, e: 12 };
var dataChart2 = { f: 9, g: 20, h: 30, i: 8 };
console.log(dataChart);
var width = 300,
height = 300,
// Think back to 5th grade. Radius is 1/2 of the diameter. What is the limiting factor on the diameter? Width or height, whichever is smaller
radius = Math.min(width, height) / 2;
var color = d3.scaleOrdinal()
.range(["#2C93E8", "#838690", "#F56C4E"]);
var pie = d3.pie()
.value(function (d) { return d.value; });
data = pie(d3.entries(dataChart));
var arc = d3.arc()
.outerRadius(radius - 10)
.innerRadius(0);
var labelArc = d3.arc()
.outerRadius(radius - 40)
.innerRadius(radius - 40);
var svg = d3.select("#pie")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")"); // Moving the center point. 1/2 the width and 1/2 the height
var g = svg.selectAll("arc")
.data(data)
.enter().append("g")
.attr("class", "arc");
g.append("path")
.attr("d", arc)
.style("fill", function (d) { return color(d.data.key); });
g.append("text")
.attr("transform", function (d) { return "translate(" + labelArc.centroid(d) + ")"; })
.text(function (d) { return d.data.key; })
.style("fill", "#fff");
function change(dataChart) {
var pie = d3.pie()
.value(function (d) { return d.value; });
data = pie(d3.entries(dataChart));
path = d3.select("#pie").selectAll("path").data(data); // Compute the new angles
path.attr("d", arc); // redrawing the path
d3.selectAll("text").data(data).attr("transform", function (d) { return "translate(" + labelArc.centroid(d) + ")"; }); // recomputing the centroid and translating the text accordingly.
}
// calling the update functions
change(dataChart);
change (dataChart2);
</script>
</body>
</html>

.ease(d3.easeLinear) yields Cannot read property 'indexOf' of undefined

I've copied and pasted the code from Point-Along-Path Interpolation example on bl.ocks.org and then added the line about transition easing (highlighted below with a comment), but it yields the error Cannot read property 'indexOf' of undefined error. What's wrong?
(JSFiddle here)
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<style>
path {
fill: none;
stroke: #000;
stroke-width: 3px;
}
circle {
fill: steelblue;
stroke: #fff;
stroke-width: 3px;
}
</style>
<script src="d3.v3.min.js"></script>
<script>
var points = [
[480, 200],
[580, 400],
[680, 100],
[780, 300],
[180, 300],
[280, 100],
[380, 400]
];
var svg = d3.select("body").append("svg")
.attr("width", 960)
.attr("height", 500);
var path = svg.append("path")
.data([points])
.attr("d", d3.svg.line()
.tension(0) // Catmull–Rom
.interpolate("cardinal-closed"));
svg.selectAll(".point")
.data(points)
.enter().append("circle")
.attr("r", 4)
.attr("transform", function(d) { return "translate(" + d + ")"; });
var circle = svg.append("circle")
.attr("r", 13)
.attr("transform", "translate(" + points[0] + ")");
transition();
function transition() {
circle.transition()
.duration(10000)
.ease(d3.easeLinear) // this is the line I've added
.attrTween("transform", translateAlong(path.node()))
.each("end", transition);
}
// Returns an attrTween for translating along the specified path element.
function translateAlong(path) {
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";
};
};
}
</script>
You are adding an ease function as one would do in v4 or 5 of d3. D3v4 made a structural change to how D3 approaches parameters passed to a number of methods. Rather than pass a string to these methods to specify certain functionality, in D3v4+, one passes an object or function (that is often a property of D3) to the method instead.
So in d3v3:
transition.ease("linear")
Is now in d3v4+
transition.ease(d3.easeLinear);
So, as your code is d3v3, you just need to use the older format. Here's an updated fiddle.

D3: slow zoomable heatmap

I have this zoomable heatmap, which looks too slow when zooming-in or out. Is there anything to make it faster/smoother or it is just too many points and that is the best I can have. I was wondering if there is some trick to make it lighter for the browser please while keeping enhancements like tooltips. Or maybe my code handling the zoom feature is not great .
<!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>Bar 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>
</head>
<body>
<div id="chart" style="width: 700px; height: 500px"></div>
<script>
var dataset = [];
for (let i = 1; i < 360; i++) {
for (j = 1; j < 75; j++) {
dataset.push({
day: i,
hour: j,
tOutC: Math.random() * 25,
})
}
};
var days = d3.max(dataset, function(d) {
return d.day;
}) -
d3.min(dataset, function(d) {
return d.day;
});
var hours = d3.max(dataset, function(d) {
return d.hour;
}) -
d3.min(dataset, function(d) {
return d.hour;
});
var tMin = d3.min(dataset, function(d) {
return d.tOutC;
}),
tMax = d3.max(dataset, function(d) {
return d.tOutC;
});
var dotWidth = 1,
dotHeight = 3,
dotSpacing = 0.5;
var margin = {
top: 0,
right: 25,
bottom: 40,
left: 25
},
width = (dotWidth * 2 + dotSpacing) * days,
height = (dotHeight * 2 + dotSpacing) * hours;
var colors = ['#2C7BB6', '#00A6CA','#00CCBC','#90EB9D','#FFFF8C','#F9D057','#F29E2E','#E76818','#D7191C'];
var xScale = d3.scaleLinear()
.domain(d3.extent(dataset, function(d){return d.day}))
.range([0, width]);
var yScale = d3.scaleLinear()
.domain(d3.extent(dataset, function(d){return d.hour}))
.range([(dotHeight * 2 + dotSpacing) * hours, dotHeight * 2 + dotSpacing]);
var colorScale = d3.scaleQuantile()
.domain([0, colors.length - 1, d3.max(dataset, function(d) {
return d.tOutC;
})])
.range(colors);
var xAxis = d3.axisBottom().scale(xScale);
// Define Y axis
var yAxis = d3.axisLeft().scale(yScale);
var zoom = d3.zoom()
.scaleExtent([dotWidth, dotHeight])
.translateExtent([
[80, 20],
[width, height]
])
.on("zoom", zoomed);
var tooltip = d3.select("body").append("div")
.attr("id", "tooltip")
.style("opacity", 0);
// SVG canvas
var svg = d3.select("#chart")
.append("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);
// Heatmap dots
svg.append("g")
.attr("clip-path", "url(#clip)")
.selectAll("ellipse")
.data(dataset)
.enter()
.append("ellipse")
.attr("cx", function(d) {
return xScale(d.day);
})
.attr("cy", function(d) {
return yScale(d.hour);
})
.attr("rx", dotWidth)
.attr("ry", dotHeight)
.attr("fill", function(d) {
return colorScale(d.tOutC);
})
.on("mouseover", function(d){
$("#tooltip").html("X: "+d.day+"<br/>Y:"+d.hour+"<br/>Value:"+Math.round(d.tOutC*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);
});
//Create X axis
var renderXAxis = svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + yScale(0) + ")")
.call(xAxis)
//Create Y axis
var renderYAxis = svg.append("g")
.attr("class", "y axis")
.call(yAxis);
function zoomed() {
// update: rescale x axis
renderXAxis.call(xAxis.scale(d3.event.transform.rescaleX(xScale)));
update();
}
function update() {
// update: cache rescaleX value
var rescaleX = d3.event.transform.rescaleX(xScale);
svg.selectAll("ellipse")
.attr('clip-path', 'url(#clip)')
// update: apply rescaleX value
.attr("cx", function(d) {
return rescaleX(d.day);
})
// .attr("cy", function(d) {
// return yScale(d.hour);
// })
// update: apply rescaleX value
.attr("rx", function(d) {
return (dotWidth * d3.event.transform.k);
})
.attr("fill", function(d) {
return colorScale(d.tOutC);
});
}
</script>
</body>
</html>
Thanks
The solution is not to update all the dots for the zoom but to apply the zoom transform to the group containing the dots.
Clipping of the group needs to be done on an additional parent g heatDotsGroup.
The zoom scale of y is taken care of (set it fixed to 1) with a regex replace, limit translate in y by setting the transform.y to 0, and limit the translate of x based on the current scale.
Allow a little translate past 0 to show the first dot complete when zoomed in.
var zoom = d3.zoom()
.scaleExtent([dotWidth, dotHeight])
.on("zoom", zoomed);
// Heatmap dots
var heatDotsGroup = svg.append("g")
.attr("clip-path", "url(#clip)")
.append("g");
heatDotsGroup.selectAll("ellipse")
.data(dataset)
.enter()
.append("ellipse")
.attr("cx", function(d) { return xScale(d.day); })
.attr("cy", function(d) { return yScale(d.hour); })
.attr("rx", dotWidth)
.attr("ry", dotHeight)
.attr("fill", function(d) { return colorScale(d.tOutC); })
.on("mouseover", function(d){
$("#tooltip").html("X: "+d.day+"<br/>Y:"+d.hour+"<br/>Value:"+Math.round(d.tOutC*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);
});
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 );
// update: rescale x axis
renderXAxis.call(xAxis.scale(d3.event.transform.rescaleX(xScale)));
heatDotsGroup.attr("transform", d3.event.transform.toString().replace(/scale\((.*?)\)/, "scale($1, 1)"));
}
Try Canvas
You have 27 000 nodes. This is probably around the point where SVG performance drops off for most and Canvas starts to really shine. Sure, Canvas isn't stateful like SVG, its just pixels with no nice elements to mouse over in the DOM and tell you where and what they are. But, there are ways to address this shortcoming so that we can retain speed and interactive abilities.
For the initial rendering using your snippet, I have a average rendering time of ~440ms. But, through the magic of canvas, I can render the same heat map with an average rendering time of ~103ms. Those savings can be applied to things like zooming, animation etc.
For very small things like your ellipses there is a risk of aliasing issues that is harder to fix with canvas as opposed to SVG, though how each browser renders this will differ
Design Implications
With Canvas we can retain the enter/exit/update cycle as with SVG, but we also have the option of dropping it. At times the enter/exit/update cycle pairs extremely well with canvas: transitions, dynamic data, heirarcical data, etc. I have previously spent some time on some of the higher level differences between Canvas and SVG with regards to D3 here.
For my answer here, we'll leave the enter cycle. When we want to update the visualization we just redraw everything based on the data array itself.
Drawing the Heat Map
I'm using rectangles for the sake of brevity. Canvas's ellipse method isn't quite ready, but you can emulate it easily enough.
We need a function that draws the dataset. If you had x/y/color hard coded into the dataset we could use a very simple:
function drawNodes()
dataset.forEach(function(d) {
ctx.beginPath();
ctx.rect(d.x,d.y,width,height);
ctx.fillStyle = d.color;
ctx.fill();
})
}
But we need to scale your values, calculate a color, and we should apply the zoom. I ended up with a relatively simple:
function drawNodes()
var k = d3.event ? d3.event.transform.k : 1;
var dw = dotWidth * k;
ctx.clearRect(0,0,width,height); // erase what's there
dataset.forEach(function(d) {
var x = xScale(d.day);
var y = yScale(d.hour);
var fill = colorScale(d.tOutC);
ctx.beginPath();
ctx.rect(x,y,dw,dotHeight);
ctx.fillStyle = fill;
ctx.strokeStyle = fill;
ctx.stroke();
ctx.fill();
})
}
This can be used to initially draw the nodes (when d3.event isn't defined), or on zoom/pan events (after which this function is called each time).
What about the axes?
d3-axis is intended for SVG. So, I've just superimposed an SVG overtop of a Canvas element positioning both absolutely and disabling mouse events on the overlying SVG.
Speaking of axes, I only have one drawing function (no difference between update/initial drawing), so I use a reference x scale and a rendering x scale from the get go, rather than creating a disposable rescaled x scale in the update function
Now I Have a Canvas, How Do I Interact With It?
There are a few methods we could use take a pixel position and convert it to a specific datum:
Use a Voronoi diagram (using the .find method to locate a datum)
Use a Force layout (also using the .find method to locate a datum)
Use a hidden Canvas (using pixel color to indicate datum index)
Use a scale's invert function (when data is gridded)
The third option may be one of the most common, and while the first two look similar the find methods do differ internally (voronoi neighbors vs quad tree). The last method is fairly appropriate in this case: we have a grid of data and we can invert the mouse coordinate to get row and column data. Based on your snippet that might look like:
function mousemove() {
var xy = d3.mouse(this);
var x = Math.round(xScale.invert(xy[0]));
var y = Math.round(yScale.invert(xy[1]));
// For rounding on canvas edges:
if(x > xScaleRef.domain()[1]) x = xScaleRef.domain()[1];
if(x < xScaleRef.domain()[0]) x = xScaleRef.domain()[0];
if(y > yScale.domain()[1]) y = yScale.domain()[1];
if(y < yScale.domain()[0]) y = yScale.domain()[0];
var index = --x*74 + y-1; // minus ones for non zero indexed x,y values.
var d = dataset[index];
console.log(x,y,index,d)
$("#tooltip").html("X: "+d.day+"<br/>Y:"+d.hour+"<br/>Value:"+Math.round(d.tOutC*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);
}
*I've used mousemove since mouseover will trigger once when moving over the canvas, we need to continuously update, if we wanted to hide the tooltip, we could just check to see if the pixel selected is white:
var p = ctx.getImageData(xy[0], xy[1], 1, 1).data; // pixel data:
if (!p[0] && !p[1] && !p[2]) { /* show tooltip */ }
else { /* hide tooltip */ }
Example
I've explicitly mentioned most of the changes above, but I've made some additional changes below. First, I need to select the canvas, position it, get the context, etc. I also have swapped rects for ellipses, so the positioning is a bit different (but you have other positioning issues to from using a linear scale (the ellipse centroids can fall on the edge of the svg as is), I've not modified this to account for the width/height of the ellipses/rects. This scale issue was far enough from the question that I didn't modify it.
var dataset = [];
for (let i = 1; i < 360; i++) {
for (j = 1; j < 75; j++) {
dataset.push({
day: i,
hour: j,
tOutC: Math.random() * 25,
})
}
};
var days = d3.max(dataset, function(d) { return d.day; }) - d3.min(dataset, function(d) { return d.day; });
var hours = d3.max(dataset, function(d) { return d.hour; }) - d3.min(dataset, function(d) { return d.hour; });
var tMin = d3.min(dataset, function(d) { return d.tOutC; }), tMax = d3.max(dataset, function(d) { return d.tOutC; });
var dotWidth = 1,
dotHeight = 3,
dotSpacing = 0.5;
var margin = { top: 20, right: 25, bottom: 40, left: 25 },
width = (dotWidth * 2 + dotSpacing) * days,
height = (dotHeight * 2 + dotSpacing) * hours;
var tooltip = d3.select("body").append("div")
.attr("id", "tooltip")
.style("opacity", 0);
var colors = ['#2C7BB6', '#00A6CA','#00CCBC','#90EB9D','#FFFF8C','#F9D057','#F29E2E','#E76818','#D7191C'];
var xScale = d3.scaleLinear()
.domain(d3.extent(dataset, function(d){return d.day}))
.range([0, width]);
var xScaleRef = xScale.copy();
var yScale = d3.scaleLinear()
.domain(d3.extent(dataset, function(d){return d.hour}))
.range([height,0]);
var colorScale = d3.scaleQuantile()
.domain([0, colors.length - 1, d3.max(dataset, function(d) { return d.tOutC; })])
.range(colors);
var xAxis = d3.axisBottom().scale(xScale);
var yAxis = d3.axisLeft().scale(yScale);
var zoom = d3.zoom()
.scaleExtent([dotWidth, dotHeight])
.translateExtent([
[0,0],
[width, height]
])
.on("zoom", zoomed);
var tooltip = d3.select("body").append("div")
.attr("id", "tooltip")
.style("opacity", 0);
// SVG & Canvas:
var canvas = d3.select("#chart")
.append("canvas")
.attr("width", width)
.attr("height", height)
.style("left", margin.left + "px")
.style("top", margin.top + "px")
.style("position","absolute")
.on("mousemove", mousemove)
.on("mouseout", mouseout);
var svg = d3.select("#chart")
.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 ctx = canvas.node().getContext("2d");
canvas.call(zoom);
// Initial Draw:
drawNodes(dataset);
//Create Axes:
var renderXAxis = svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + yScale(0) + ")")
.call(xAxis)
var renderYAxis = svg.append("g")
.attr("class", "y axis")
.call(yAxis);
// Handle Zoom:
function zoomed() {
// rescale the x Axis:
xScale = d3.event.transform.rescaleX(xScaleRef); // Use Reference Scale.
// Redraw the x Axis:
renderXAxis.call(xAxis.scale(xScale));
// Clear and redraw the nodes:
drawNodes();
}
// Draw nodes:
function drawNodes() {
var k = d3.event ? d3.event.transform.k : 1;
var dw = dotWidth * k;
ctx.clearRect(0,0,width,height);
dataset.forEach(function(d) {
var x = xScale(d.day);
var y = yScale(d.hour);
var fill = colorScale(d.tOutC);
ctx.beginPath();
ctx.rect(x,y,dw,dotHeight);
ctx.fillStyle = fill;
ctx.strokeStyle = fill;
ctx.stroke();
ctx.fill();
})
}
// Mouse movement:
function mousemove() {
var xy = d3.mouse(this);
var x = Math.round(xScale.invert(xy[0]));
var y = Math.round(yScale.invert(xy[1]));
if(x > xScaleRef.domain()[1]) x = xScaleRef.domain()[1];
if(x < xScaleRef.domain()[0]) x = xScaleRef.domain()[0];
if(y > yScale.domain()[1]) y = yScale.domain()[1];
if(y < yScale.domain()[0]) y = yScale.domain()[0];
var index = --x*74 + y-1; // minus ones for non zero indexed x,y values.
var d = dataset[index];
$("#tooltip").html("X: "+d.day+"<br/>Y:"+d.hour+"<br/>Value:"+Math.round(d.tOutC*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);
}
function mouseout() {
$("#tooltip").animate({duration: 500}).css("opacity",0);
};
.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;
}
svg {
position: absolute;
top: 0;
left:0;
pointer-events: none;
}
<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>
<div id="chart" style="width: 700px; height: 500px"></div>
The result of all following combined suggestions is not perfect, but it is subjectively slightly better:
<!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>Bar 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>
</head>
<body>
<div id="chart" style="width: 700px; height: 500px"></div>
<script>
var dataset = [];
for (let i = 1; i < 360; i++) {
for (j = 1; j < 75; j++) {
dataset.push({
day: i,
hour: j,
tOutC: Math.random() * 25,
})
}
};
var days = d3.max(dataset, function(d) {
return d.day;
}) -
d3.min(dataset, function(d) {
return d.day;
});
var hours = d3.max(dataset, function(d) {
return d.hour;
}) -
d3.min(dataset, function(d) {
return d.hour;
});
var tMin = d3.min(dataset, function(d) {
return d.tOutC;
}),
tMax = d3.max(dataset, function(d) {
return d.tOutC;
});
var dotWidth = 1,
dotHeight = 3,
dotSpacing = 0.5;
var margin = {
top: 0,
right: 25,
bottom: 40,
left: 25
},
width = (dotWidth * 2 + dotSpacing) * days,
height = (dotHeight * 2 + dotSpacing) * hours;
var colors = ['#2C7BB6', '#00A6CA','#00CCBC','#90EB9D','#FFFF8C','#F9D057','#F29E2E','#E76818','#D7191C'];
var xScale = d3.scaleLinear()
.domain(d3.extent(dataset, function(d){return d.day}))
.range([0, width]);
var yScale = d3.scaleLinear()
.domain(d3.extent(dataset, function(d){return d.hour}))
.range([(dotHeight * 2 + dotSpacing) * hours, dotHeight * 2 + dotSpacing]);
var colorScale = d3.scaleQuantile()
.domain([0, colors.length - 1, d3.max(dataset, function(d) {
return d.tOutC;
})])
.range(colors);
var xAxis = d3.axisBottom().scale(xScale);
// Define Y axis
var yAxis = d3.axisLeft().scale(yScale);
var zoom = d3.zoom()
.scaleExtent([dotWidth, dotHeight])
.translateExtent([
[80, 20],
[width, height]
])
// .on("zoom", zoomed);
.on("end", zoomed);
var tooltip = d3.select("body").append("div")
.attr("id", "tooltip")
.style("opacity", 0);
// SVG canvas
var svg = d3.select("#chart")
.append("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);
// Heatmap dots
svg.append("g")
.attr("clip-path", "url(#clip)")
.selectAll("ellipse")
.data(dataset)
.enter()
.append("ellipse")
.attr("cx", function(d) {
return xScale(d.day);
})
.attr("cy", function(d) {
return yScale(d.hour);
})
.attr("rx", dotWidth)
.attr("ry", dotHeight)
.attr("fill", function(d) {
return colorScale(d.tOutC);
})
.on("mouseover", function(d){
$("#tooltip").html("X: "+d.day+"<br/>Y:"+d.hour+"<br/>Value:"+Math.round(d.tOutC*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);
});
//Create X axis
var renderXAxis = svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + yScale(0) + ")")
.call(xAxis)
//Create Y axis
var renderYAxis = svg.append("g")
.attr("class", "y axis")
.call(yAxis);
function zoomed() {
// update: rescale x axis
renderXAxis.call(xAxis.scale(d3.event.transform.rescaleX(xScale)));
update();
}
function update() {
// update: cache rescaleX value
var rescaleX = d3.event.transform.rescaleX(xScale);
var scaledRadius = dotWidth * d3.event.transform.k;
var scaledCxes = [...Array(360).keys()].map(i => rescaleX(i));
svg.selectAll("ellipse")
// .attr('clip-path', 'url(#clip)')
// update: apply rescaleX value
.attr("cx", d => scaledCxes[d.day])
// .attr("cy", function(d) {
// return yScale(d.hour);
// })
// update: apply rescaleX value
.attr("rx", scaledRadius)
// .attr("fill", function(d) {
// return colorScale(d.tOutC);
// });
}
</script>
</body>
</html>
Using on("end", zoomed) instead of on("zoom", zoomed):
First thing we can try is to activate the zoom change only at the end of the zoom event in order not to have these non deterministic updates jumps during a single zoom event. It has for effect to lower the required processing as only one computation happens, and it removes the global jump discomfort:
var zoom = d3.zoom()
.scaleExtent([dotWidth, dotHeight])
.translateExtent([ [80, 20], [width, height] ])
.on("end", zoomed); // instead of .on("zoom", zoomed);
Remove updates of things which remains the same during the zoom:
We can also remove from the nodes update things which stay the same such as the color of a circle which during the zoom remains the same anyway .attr("fill", function(d) { return colorScale(d.tOutC); }); and .attr('clip-path', 'url(#clip)').
Computing only once things used several times:
The new circle radius after the zoom can only be computed once instead of 27K times as it's the same for all circles:
var scaledRadius = dotWidth * d3.event.transform.k;
.attr("rx", scaledRadius)
Same for x positions, we can compute it once per possible x value (360 times) and store it in an array to access them in constant time instead of computing it 27K times:
var scaledCxes = [...Array(360).keys()].map(i => rescaleX(i));
.attr("cx", d => scaledCxes[d.day])
Last obvious option would be to reduce the number of nodes since it's the root of the issue!
If the zoom extent would have been bigger, I would have also suggested filtering nodes not visible anymore.
Do check LightningChart JS heatmaps - it's free to use non-commercially.
Here is a performance comparison of best performing heatmap web charts https://github.com/Arction/javascript-charts-performance-comparison-heatmaps
As you can see over there we are talking about visualizing heatmaps that are in range of billions of data points and user interactions still work just fine.
// Source https://www.arction.com/lightningchart-js-interactive-examples/edit/lcjs-example-0800-heatmapGrid.html
/*
* LightningChartJS example that showcases a simple XY line series.
*/
// Extract required parts from LightningChartJS.
const { lightningChart, PalettedFill, LUT, ColorRGBA, emptyLine, Themes } =
lcjs;
const { createWaterDropDataGenerator } = xydata;
// Specify the resolution used for the heatmap.
const resolutionX = 1000;
const resolutionY = 1000;
// Create a XY Chart.
const chart = lightningChart()
.ChartXY({
// theme: Themes.darkGold
})
.setTitle(
`Heatmap Grid Series ${resolutionX}x${resolutionY} (${(
(resolutionX * resolutionY) /
1000000
).toFixed(1)} million data points)`
)
.setPadding({ right: 40 });
// Create LUT and FillStyle
const palette = new LUT({
units: "intensity",
steps: [
{ value: 0, color: ColorRGBA(255, 255, 0) },
{ value: 30, color: ColorRGBA(255, 204, 0) },
{ value: 45, color: ColorRGBA(255, 128, 0) },
{ value: 60, color: ColorRGBA(255, 0, 0) },
],
interpolate: false,
});
// Generate heatmap data.
createWaterDropDataGenerator()
.setRows(resolutionX)
.setColumns(resolutionY)
.generate()
.then((data) => {
// Add a Heatmap to the Chart.
const heatmap = chart
.addHeatmapGridSeries({
columns: resolutionX,
rows: resolutionY,
start: { x: 0, y: 0 },
end: { x: resolutionX, y: resolutionY },
dataOrder: "columns",
})
// Color Heatmap using previously created color look up table.
.setFillStyle(new PalettedFill({ lut: palette }))
.setWireframeStyle(emptyLine)
.invalidateIntensityValues(data)
.setMouseInteractions(false);
// Add LegendBox.
const legend = chart.addLegendBox()
// Dispose example UI elements automatically if they take too much space. This is to avoid bad UI on mobile / etc. devices.
.setAutoDispose({
type: 'max-height',
maxHeight: 0.70,
})
.add(chart)
});
<script src="http://unpkg.com/#arction/lcjs#3.1.0/dist/lcjs.iife.js"></script>
<script src="http://unpkg.com/#arction/xydata#1.4.0/dist/xydata.iife.js"></script>

d3 arc tween won't work inside a function call

I'm making a pie chart using this blocks example for growing the pie slice on mouseover. In addition to the tween, I also want to have the pie slices change colors and have a tooltip, so I want to have another function called that can work with d and i.
The problem is that although the arcTween function works as written on the mouseover, it will not work if it is wrapped in another function -- which I want to do so that I can access the index for tooltip.
Can someone explain to me why this isn't working? I have tried many ways of returning the function with different variables and can't get it working.
Full code:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
path {
stroke: #333;
fill-opacity: 90%;
stroke-width: 1.5px;
transition: fill 250ms linear;
transition-delay: 150ms;
}
path:hover {
stroke: #000;
fill-opacity: 100%;
transition-delay: 0;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var data = [44, 16, 9, 8,8,8,4,3];
function findColor(index){
var colors = [
"#cb5b49",
"#8bbbd3",
"#1f61a3",
"#c7dae4",
"#f0d0bd",
"#e89d7b",
"#a91729",
"#408cb9"];
return colors[index];
}
var width = 960,
height = 500;
var outerRadius = height / 2 - 20,
cornerRadius = 10;
var pie = d3.layout.pie();
var arc = d3.svg.arc()
.padRadius(outerRadius);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
svg.selectAll("path")
.data(pie(data))
.enter().append("path")
.each(function(d) { d.outerRadius = outerRadius - 20; })
.attr("d", arc)
.attr("fill",function(d, i){return findColor(i)})
.on("mouseover", arcTween(outerRadius + 20, 0)) //THIS WORKS
//THE BELOW DOES NOT WORK FOR SOME REASON
// .on("mouseover", function(d,i){console.log(i); return arcTween(outerRadius + 20, 0);})
.on("mouseout", arcTween(outerRadius - 20, 150));
function arcTween(outerRadius, delay) {
return function() {
d3.select(this).transition().delay(delay).attrTween("d", function(d) {
var i = d3.interpolate(d.outerRadius, outerRadius);
return function(t) { d.outerRadius = i(t); return arc(d); };
});
};
}
</script>
Per #LarsKotthoff, you need this for a fix: jsfiddle.net/vndrw57a
svg.selectAll("path")
.data(pie(data))
.enter().append("path")
.each(function(d) { d.outerRadius = outerRadius - 20; })
.attr("d", arc)
.attr("fill",function(d, i){return findColor(i)})
.on("mouseover", function(){ arcTween(outerRadius + 20, 0, this)(); })
.on("mouseout", arcTween(outerRadius - 20, 150));
function arcTween(outerRadius, delay, that) {
return function() {
var el = (that !== undefined ? that : this);
d3.select(el).transition().delay(delay).attrTween("d", function(d) {
var i = d3.interpolate(d.outerRadius, outerRadius);
return function(t) { d.outerRadius = i(t); return arc(d); };
});
};
The code
function(d,i){console.log(i); return arcTween(outerRadius + 20, 0);})
returns a function (the return value of arcTween), it doesn't execute the function. You can execute the function by adding ()
function(d,i){console.log(i); arcTween(outerRadius + 20, 0)();})

Categories