Related
What am I doing wrong here please? I want to increase the point size when the mouse enters the associated voronoi cell, however the point goes back to its original size when the mouse is exaclty above that point; I have tried both the mouseover and mousemove events without any luck. Code in snippet, you can zoom in and you will be able to see what I just described.
Many thanks!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Chart</title>
<!-- 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>
<style>
.grid line {
stroke: #ddd;
}
</style>
<div id='scatter-plot'>
<svg width="700" height="500">
</svg>
</div>
<script>
var data = [];
for (let i = 0; i < 200; i++) {
data.push({
x: Math.random(),
y: Math.random(),
dotNum: i,
})
}
renderChart(data)
function renderChart(data) {
var totalWidth = 920,
totalHeight = 480;
var margin = {
top: 10,
left: 50,
bottom: 30,
right: 0
}
var width = totalWidth - margin.left - margin.right,
height = totalHeight - margin.top - margin.bottom;
// inner chart dimensions, where the dots are plotted
// var width = width - margin.left - margin.right;
// var height = height - margin.top - margin.bottom;
var tsn = d3.transition().duration(200);
// radius of points in the scatterplot
var pointRadius = 2;
var extent = {
x: d3.extent(data, function (d) {return d.x}),
y: d3.extent(data, function (d) {return d.y}),
};
var scale = {
x: d3.scaleLinear().range([0, width]),
y: d3.scaleLinear().range([height, 0]),
};
var axis = {
x: d3.axisBottom(scale.x).ticks(xTicks).tickSizeOuter(0),
y: d3.axisLeft(scale.y).ticks(yTicks).tickSizeOuter(0),
};
var gridlines = {
x: d3.axisBottom(scale.x).tickFormat("").tickSize(height),
y: d3.axisLeft(scale.y).tickFormat("").tickSize(-width),
}
var colorScale = d3.scaleLinear().domain([0, 1]).range(['#06a', '#06a']);
// select the root container where the chart will be added
var container = d3.select('#scatter-plot');
var zoom = d3.zoom()
.scaleExtent([1, 20])
.on("zoom", zoomed);
var tooltip = d3.select("body").append("div")
.attr("id", "tooltip")
.style("opacity", 0);
// initialize main SVG
var svg = container.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);
// Heatmap dots
var dotsGroup = svg.append("g")
.attr("clip-path", "url(#clip)")
.append("g");
//Create X axis
var renderXAxis = svg.append("g")
.attr("class", "x axis")
//Create Y axis
var renderYAxis = svg.append("g")
.attr("class", "y axis")
// set up axis generating functions
var xTicks = Math.round(width / 50);
var yTicks = Math.round(height / 50);
function updateScales(data, scale){
scale.x.domain([extent.x[0], extent.x[1]]).nice(),
scale.y.domain([extent.y[0], extent.y[1]]).nice()
}
function zoomed() {
d3.event.transform.x = d3.event.transform.x;
d3.event.transform.y = d3.event.transform.y;
// update: rescale x axis
renderXAxis.call(axis.x.scale(d3.event.transform.rescaleX(scale.x)));
renderYAxis.call(axis.y.scale(d3.event.transform.rescaleX(scale.y)));
dotsGroup.attr("transform", d3.event.transform);
}
// add the overlay on top of everything to take the mouse events
dotsGroup.append('rect')
.attr('class', 'overlay')
.attr('width', width)
.attr('height', height)
.style('fill', 'red')
.style('opacity', 0)
.on('mouseover', mouseMoveHandler)
.on('mouseleave', () => {
// hide the highlight circle when the mouse leaves the chart
highlight(null);
});
renderPlot(data);
function renderPlot(data){
updateScales(data, scale);
svg.select('.y.axis')
.attr("transform", "translate(" + -pointRadius + " 0)" )
.call(axis.y);
var h = height + pointRadius;
svg.select('.x.axis')
.attr("transform", "translate(0, " + h + ")")
.call(axis.x);
svg.append("g")
.attr("class", "grid")
.call(gridlines.x);
svg.append("g")
.attr("class", "grid")
.call(gridlines.y);
//Do the chart
var update = dotsGroup.selectAll("circle").data(data)
update
.enter()
.append('circle')
.attr('r', pointRadius)
.attr('cx', d => scale.x(d.x))
.attr('cy', d => scale.y(d.y))
.attr('fill', d => colorScale(d.y))
};
// create a voronoi diagram
var voronoiDiagram = d3.voronoi()
.x(d => scale.x(d.x))
.y(d => scale.y(d.y))
.size([width, height])(data);
// add a circle for indicating the highlighted point
dotsGroup.append('circle')
.attr('class', 'highlight-circle')
.attr('r', pointRadius*2) // increase the size if highlighted
.style('fill', 'red')
.style('display', 'none');
// callback to highlight a point
function highlight(d) {
// no point to highlight - hide the circle and the tooltip
if (!d) {
d3.select('.highlight-circle').style('display', 'none');
//tooltip.style("opacity",0);
// otherwise, show the highlight circle at the correct position
} else {
d3.select('.highlight-circle')
.style('display', '')
.style('stroke', colorScale(d.y))
.attr('cx', scale.x(d.x))
.attr('cy', scale.y(d.y));
}
}
// callback for when the mouse moves across the overlay
function mouseMoveHandler() {
// get the current mouse position
var [mx, my] = d3.mouse(this);
var site = voronoiDiagram.find(mx, my);
// highlight the point if we found one, otherwise hide the highlight circle
highlight(site && site.data);
for (let i = 0; i < site.data.dotNum; i++) {
//do something....
}
}
}
</script>
</body>
</html>
you have to draw the overlay rect after the circles and the highlight circle. If not then hovering over a circle generates a mouse leave event and you see a flashing of the highlight circle
use the mousemove event not the mouseover, that is kind of a mouse-enter event
I have added logic to only update the highlight when it changes dots
the grid is not updated on zoom and translate (not fixed)
even when moving over the overlay there were still mouseleave events - they where caused by the grid lines. Moved the dots group after the grid line groups
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Chart</title>
<!-- 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>
<style>
.grid line { stroke: #ddd; }
</style>
<div id='scatter-plot'>
<svg width="700" height="500">
</svg>
</div>
<script>
var data = [];
for (let i = 0; i < 200; i++) {
data.push({
x: Math.random(),
y: Math.random(),
dotNum: i,
})
}
renderChart(data);
function renderChart(data) {
var totalWidth = 920,
totalHeight = 480;
var margin = {
top: 10,
left: 50,
bottom: 30,
right: 0
}
var width = totalWidth - margin.left - margin.right,
height = totalHeight - margin.top - margin.bottom;
// inner chart dimensions, where the dots are plotted
// var width = width - margin.left - margin.right;
// var height = height - margin.top - margin.bottom;
var tsn = d3.transition().duration(200);
// radius of points in the scatterplot
var pointRadius = 2;
var extent = {
x: d3.extent(data, function (d) {return d.x}),
y: d3.extent(data, function (d) {return d.y}),
};
var scale = {
x: d3.scaleLinear().range([0, width]),
y: d3.scaleLinear().range([height, 0]),
};
var axis = {
x: d3.axisBottom(scale.x).ticks(xTicks).tickSizeOuter(0),
y: d3.axisLeft(scale.y).ticks(yTicks).tickSizeOuter(0),
};
var gridlines = {
x: d3.axisBottom(scale.x).tickFormat("").tickSize(height),
y: d3.axisLeft(scale.y).tickFormat("").tickSize(-width),
}
var colorScale = d3.scaleLinear().domain([0, 1]).range(['#06a', '#06a']);
// select the root container where the chart will be added
var container = d3.select('#scatter-plot');
var zoom = d3.zoom()
.scaleExtent([1, 20])
.on("zoom", zoomed);
var tooltip = d3.select("body").append("div")
.attr("id", "tooltip")
.style("opacity", 0);
// initialize main SVG
var svg = container.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);
//Create X axis
var renderXAxis = svg.append("g")
.attr("class", "x axis")
//Create Y axis
var renderYAxis = svg.append("g")
.attr("class", "y axis")
// set up axis generating functions
var xTicks = Math.round(width / 50);
var yTicks = Math.round(height / 50);
function updateScales(data, scale){
scale.x.domain([extent.x[0], extent.x[1]]).nice(),
scale.y.domain([extent.y[0], extent.y[1]]).nice()
}
function zoomed() {
d3.event.transform.x = d3.event.transform.x;
d3.event.transform.y = d3.event.transform.y;
// update: rescale x axis
renderXAxis.call(axis.x.scale(d3.event.transform.rescaleX(scale.x)));
renderYAxis.call(axis.y.scale(d3.event.transform.rescaleX(scale.y)));
dotsGroup.attr("transform", d3.event.transform);
}
var dotsGroup;
renderPlot(data);
function renderPlot(data){
updateScales(data, scale);
svg.select('.y.axis')
.attr("transform", "translate(" + -pointRadius + " 0)" )
.call(axis.y);
var h = height + pointRadius;
svg.select('.x.axis')
.attr("transform", "translate(0, " + h + ")")
.call(axis.x);
svg.append("g")
.attr("class", "grid")
.call(gridlines.x);
svg.append("g")
.attr("class", "grid")
.call(gridlines.y);
dotsGroup = svg.append("g")
.attr("clip-path", "url(#clip)")
.append("g");
//Do the chart
var update = dotsGroup.selectAll("circle").data(data)
update
.enter()
.append('circle')
.attr('r', pointRadius)
.attr('cx', d => scale.x(d.x))
.attr('cy', d => scale.y(d.y))
.attr('fill', d => colorScale(d.y))
};
// create a voronoi diagram
var voronoiDiagram = d3.voronoi()
.x(d => scale.x(d.x))
.y(d => scale.y(d.y))
.size([width, height])(data);
// add a circle for indicating the highlighted point
dotsGroup.append('circle')
.attr('class', 'highlight-circle')
.attr('r', pointRadius*2) // increase the size if highlighted
.style('fill', 'red')
.style('display', 'none');
// add the overlay on top of everything to take the mouse events
dotsGroup.append('rect')
.attr('class', 'overlay')
.attr('width', width)
.attr('height', height)
.style('fill', 'red')
.style('opacity', 0)
.on('mousemove', mouseMoveHandler)
.on('mouseleave', () => {
// hide the highlight circle when the mouse leaves the chart
console.log('mouse leave');
highlight(null);
});
var prevHighlightDotNum = null;
// callback to highlight a point
function highlight(d) {
// no point to highlight - hide the circle and the tooltip
if (!d) {
d3.select('.highlight-circle').style('display', 'none');
prevHighlightDotNum = null;
//tooltip.style("opacity",0);
// otherwise, show the highlight circle at the correct position
} else {
if (prevHighlightDotNum !== d.dotNum) {
d3.select('.highlight-circle')
.style('display', '')
.style('stroke', colorScale(d.y))
.attr('cx', scale.x(d.x))
.attr('cy', scale.y(d.y));
prevHighlightDotNum = d.dotNum;
}
}
}
// callback for when the mouse moves across the overlay
function mouseMoveHandler() {
// get the current mouse position
var [mx, my] = d3.mouse(this);
var site = voronoiDiagram.find(mx, my);
//console.log('site', site);
// highlight the point if we found one, otherwise hide the highlight circle
highlight(site && site.data);
for (let i = 0; i < site.data.dotNum; i++) {
//do something....
}
}
}
</script>
</body>
</html>
I am trying to do the enter-update-exit pattern on this graph below (which was built with the tremendous help of some very kind ppl here at SO but I am now stuck again unfortunately. I cant make the pattern work but I am certain I pick up the correct object (named heatDotsGroup in the code below).
I can however check in Chrome's developer tools that this object contains the nodes (ellipses) but the pattern doesn't work, therefore clearly I am doing something wrong.
Any ideas please? Many thanks!
function heatmap(dataset) {
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: 60, 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 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 colors = ['#2C7BB6', '#00A6CA', '#00CCBC', '#90EB9D', '#FFFF8C', '#F9D057', '#F29E2E', '#E76818', '#D7191C'];
// the scale
var scale = {
x: d3.scaleLinear()
.range([-1, width]),
y: d3.scaleLinear()
.range([height, 0]),
};
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([0, 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([1, 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");
//Create X axis
var renderXAxis = svg.append("g")
.attr("class", "x axis")
//.attr("transform", "translate(0," + scale.y(-0.5) + ")")
//.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);
// console.log(d3.event.transform)
// update: rescale x axis
renderXAxis.call(axis.x.scale(d3.event.transform.rescaleX(scale.x)));
// Make sure that only the x axis is zoomed
heatDotsGroup.attr("transform", d3.event.transform.toString().replace(/scale\((.*?)\)/, "scale($1, 1)"));
}
svg.call(renderPlot, dataset)
function renderPlot(selection, dataset){
//Do the axes
updateScales(dataset)
selection.select('.y.axis').call(axis.y)
selection.select('.x.axis')
.attr("transform", "translate(0," + scale.y(-0.5) + ")")
.call(axis.x)
// Do the chart
const update = heatDotsGroup.selectAll("ellipse")
.data(dataset);
update
.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);}
)
.merge(update).transition().duration(800);
update.exit().remove();
}
};
#clickMe{
height:50px;
width:150px;
background-color:lavender;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<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_v4.js' type='text/javascript'></script>
</head>
<body>
<input id="clickMe" type="button" value="click me to push new data" onclick="run();" />
<div id='chart'>
<svg width="700" height="500">
<g class="focus">
<g class="xaxis"></g>
<g class="yaxis"></g>
</g>
</svg>
</div>
<script>
function run() {
var dataset = [];
for (let i = 1; i < 360; i++) { //360
for (j = 1; j < 7; j++) { //75
dataset.push({
xKey: i,
xLabel: "xMark " + i,
yKey: j,
yLabel: "yMark " + j,
val: Math.random() * 25,
})
}
};
heatmap(dataset)
}
$(document).ready(function() {});
</script>
</body>
</html>
The issue is that you are not using the same selection each time you run the enter/exit/update cycle. When the button is pushed you:
Generate new data
Run the heatmap function
The heatmap function selects the svg and appends a fresh g called heatDotsGroup
The update function is called and passed the newly created g as a selection
The enter cycle appends everything because the new g is empty.
As a result both the exit and udpate cycles are empty. Try:
console.log(update.size(),update.exit().size()) // *Without any merge*
You should see both are empty each update. This is because all elements are entered each time, which why each update increases the number of ellipses.
I've pulled out a bunch of variable declarations and append statements from the heatmap function, things that only need to be run once (I could go further, but I just did a minimum). I've also merged your update and enter selection prior to setting attributes (as we want to set the new attributes if we update).The below snippet should demonstrate this change.
In the snippet, on button push the following happens:
Generate new data
Run the heatmap function
The heatmap function selects existing selections and doesn't append anything new
The update function is called and passed the selection used by previous update/enter/exit cycles containing any existing nodes.
The update function enters/exits/updates elements as needed based on the existing nodes.
Here's a working version based on the above:
// Things to set/append once:
var svg = d3.select("#chart")
.select("svg")
var margin = {top: 0, right: 25,bottom: 60, left: 75};
var width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
svg = 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 clip = svg.append("clipPath")
.attr("id", "clip")
.append("rect")
var heatDotsGroup = svg.append("g")
.attr("clip-path", "url(#clip)")
.append("g");
var xAxis = svg.append("g").attr("class", "x axis");
var yAxis = svg.append("g").attr("class", "y axis")
function heatmap(dataset) {
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 dotSpacing = 0,
dotWidth = width/(2*(xLabels.length+1)),
dotHeight = height/(2*yLabels.length);
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 colors = ['#2C7BB6', '#00A6CA', '#00CCBC', '#90EB9D', '#FFFF8C', '#F9D057', '#F29E2E', '#E76818', '#D7191C'];
// the scale
var scale = {
x: d3.scaleLinear()
.range([-1, width]),
y: d3.scaleLinear()
.range([height, 0]),
};
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([0, 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([1, dotHeight])
.on("zoom", zoomed);
var tooltip = d3.select("body").append("div")
.attr("id", "tooltip")
.style("opacity", 0);
// SVG canvas
svg.call(zoom);
// Clip path
clip.attr("width", width)
.attr("height", height+dotHeight);
//Create X axis
var renderXAxis = xAxis
//.attr("transform", "translate(0," + scale.y(-0.5) + ")")
//.call(axis.x)
//Create Y axis
var renderYAxis = yAxis.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);
// console.log(d3.event.transform)
// update: rescale x axis
renderXAxis.call(axis.x.scale(d3.event.transform.rescaleX(scale.x)));
// Make sure that only the x axis is zoomed
heatDotsGroup.attr("transform", d3.event.transform.toString().replace(/scale\((.*?)\)/, "scale($1, 1)"));
}
svg.call(renderPlot, dataset)
function renderPlot(selection, dataset){
//Do the axes
updateScales(dataset)
selection.select('.y.axis').call(axis.y)
selection.select('.x.axis')
.attr("transform", "translate(0," + scale.y(-0.5) + ")")
.call(axis.x)
// Do the chart
const update = heatDotsGroup.selectAll("ellipse")
.data(dataset);
update
.enter()
.append("ellipse")
.merge(update)
.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);}
)
update.exit().remove();
}
};
#clickMe{
height:50px;
width:150px;
background-color:lavender;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<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_v4.js' type='text/javascript'></script>
</head>
<body>
<input id="clickMe" type="button" value="click me to push new data" onclick="run();" />
<div id='chart'>
<svg width="700" height="500">
<g class="focus">
<g class="xaxis"></g>
<g class="yaxis"></g>
</g>
</svg>
</div>
<script>
function run() {
var dataset = [];
for (let i = 1; i < 360; i++) { //360
for (j = 1; j < 7; j++) { //75
dataset.push({
xKey: i,
xLabel: "xMark " + i,
yKey: j,
yLabel: "yMark " + j,
val: Math.random() * 25,
})
}
};
heatmap(dataset)
}
$(document).ready(function() {});
</script>
</body>
</html>
The exit selection here is still empty as the size of the data array is fixed. D3 assumes the new data replaces the old, but it can't know that the new data should be represented as new elements, unless of course we specify a key function as noted in a now deleted comment. This may or may not be the desired functionality that you want.
I have a slightly different approach than Andrew.
A bunch of global variables will get messy when you have multiple charts.
When you click the button:
call the renderPlot(dataset)
check if we have a #clip element in the svg
if not: call heatmap(dataset)
Construct all the static stuff and append to the svg.
append a datum object to the svg with the variables needed for the update
fetch the datum from the svg
update the content of the svg using the datum object
function heatmap(dataset) {
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: 60, 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 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 colors = ['#2C7BB6', '#00A6CA', '#00CCBC', '#90EB9D', '#FFFF8C', '#F9D057', '#F29E2E', '#E76818', '#D7191C'];
// the scale
var scale = {
x: d3.scaleLinear()
.range([-1, width]),
y: d3.scaleLinear()
.range([height, 0]),
};
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]),
};
var colorScale = d3.scaleQuantile()
.domain([0, colors.length - 1, d3.max(dataset, function (d) {return d.val;})])
.range(colors);
var zoom = d3.zoom()
.scaleExtent([1, dotHeight])
.on("zoom", zoomed);
var tooltip = d3.select("body").append("div")
.attr("id", "tooltip")
.style("opacity", 0);
// SVG canvas
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");
//Create X axis
var renderXAxis = svg.append("g")
.attr("class", "x axis")
//.attr("transform", "translate(0," + scale.y(-0.5) + ")")
//.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);
// console.log(d3.event.transform)
// update: rescale x axis
renderXAxis.call(axis.x.scale(d3.event.transform.rescaleX(scale.x)));
// Make sure that only the x axis is zoomed
heatDotsGroup.attr("transform", d3.event.transform.toString().replace(/scale\((.*?)\)/, "scale($1, 1)"));
}
var chartData = {};
chartData.scale = scale;
chartData.axis = axis;
chartData.xBand = xBand;
chartData.yBand = yBand;
chartData.colorScale = colorScale;
chartData.heatDotsGroup = heatDotsGroup;
chartData.dotWidth = dotWidth;
chartData.dotHeight = dotHeight;
svg.datum(chartData);
//svg.call(renderPlot, dataset)
}
function updateScales(data, scale){
scale.x.domain([0, d3.max(data, d => d.xKey)]),
scale.y.domain([0, d3.max(data, d => d.yKey)])
}
function renderPlot(dataset){
var svg = d3.select("#chart")
.select("svg");
if (svg.select("#clip").empty()) { heatmap(dataset); }
chartData = svg.datum();
//Do the axes
updateScales(dataset, chartData.scale);
svg.select('.y.axis').call(chartData.axis.y)
svg.select('.x.axis')
.attr("transform", "translate(0," + chartData.scale.y(-0.5) + ")")
.call(chartData.axis.x)
// Do the chart
const update = chartData.heatDotsGroup.selectAll("ellipse")
.data(dataset);
update
.enter()
.append("ellipse")
.attr("rx", chartData.dotWidth)
.attr("ry", chartData.dotHeight)
.merge(update)
.transition().duration(800)
.attr("cx", function (d) {return chartData.scale.x(d.xKey) - chartData.xBand.bandwidth();})
.attr("cy", function (d) {return chartData.scale.y(d.yKey) + chartData.yBand.bandwidth();})
.attr("fill", function (d) { return chartData.colorScale(d.val);} );
update.exit().remove();
}
#clickMe{
height:50px;
width:150px;
background-color:lavender;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<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_v4.js' type='text/javascript'></script>
</head>
<body>
<input id="clickMe" type="button" value="click me to push new data" onclick="run();" />
<div id='chart'>
<svg width="700" height="500">
<g class="focus">
<g class="xaxis"></g>
<g class="yaxis"></g>
</g>
</svg>
</div>
<script>
function run() {
var dataset = [];
for (let i = 1; i < 360; i++) { //360
for (j = 1; j < 7; j++) { //75
dataset.push({
xKey: i,
xLabel: "xMark " + i,
yKey: j,
yLabel: "yMark " + j,
val: Math.random() * 25,
})
}
};
renderPlot(dataset)
}
$(document).ready(function() {});
</script>
</body>
</html>
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>
Original code can be found at: http://bl.ocks.org/Guerino1/be1a49bc4c4ad4d0f787a8e26ab2718e (refer to the HTML div labeled "Transitions Axes using a button, with a short delay")
In the above example, I try to transpose the Y axis to X and the X axis to Y using the following D3 code...
function draw70( xAxisData, yAxisData, selectString )
{
var flag = true;
var w = 500;
var h = 400;
var marginLeft = 10;
var marginRight = w - 10;
var marginTop = 20;
var marginBottom = h - 20;
var lineData = [];
var pt1 = {x: 0, y: 0};
lineData.push(pt1);
var pt2 = {x: 0, y: h};
lineData.push(pt2);
var pt3 = {x: w, y: h};
lineData.push(pt3);
var pt4 = {x: w, y: 0};
lineData.push(pt4);
var pt5 = {x: 0, y: 0};
lineData.push(pt5);
var lineFunction = d3.svg.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.interpolate("linear");
var canvas = d3.select(selectString).append("svg")
.attr("height", h)
.attr("width", w)
// Put a border around the canvas for visual effects
canvas.append("path")
.attr("d", lineFunction(lineData))
.attr("stroke", "blue")
.attr("stroke-width", 4)
.attr("fill", "none");
// InnerCanvas is the offset canvas, that is offset away
// from the margins, using the transform/translate for the
// entire canvas, instead of just for individual axis.
var innerCanvas = canvas.append("g")
.attr("transform", "translate(60,10)");
// Setup y axis : Range is the length of the line
// NOTE: A value of "1" for rangeRoundBands allows points
// to be centered over the ordinal text markers
var yAxisScale = d3.scale.ordinal().domain(yAxisData).rangeRoundBands([marginBottom-20, marginTop], 1);
var yAxis = d3.svg.axis().scale(yAxisScale).orient("left");
var yAxisGroup = innerCanvas.append("g")
.attr("class", "y axis")
.call(yAxis);
// Setup x axis : Range is the length of the line
// NOTE: A value of "1" for rangeRoundBands allows points
// to be centered over the ordinal text markers
var xAxisScale = d3.scale.ordinal().domain(xAxisData).rangeRoundBands([0, (marginRight-100)], 1);
var xAxis = d3.svg.axis().scale(xAxisScale).orient("bottom");
var xAxisGroup = innerCanvas.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0,354)")
.call(xAxis);
d3.select("#buttonChart70")
.on("click", function(){
var color;
if (flag) {
//yAxisScale = d3.scale.ordinal().domain(xAxisData).rangeRoundBands([marginBottom-20, marginTop], 1);
//xAxisScale = d3.scale.ordinal().domain(yAxisData).rangeRoundBands([0, (marginRight-100)], 1);
yAxisScale = d3.scale.ordinal().domain(xAxisData).rangeRoundBands([marginTop, marginBottom-20], 1);
xAxisScale = d3.scale.ordinal().domain(yAxisData).rangeRoundBands([(marginRight-100), 0], 1);
yAxis.orient("bottom");
xAxis.orient("left");
color = "red";
} else {
yAxisScale = d3.scale.ordinal().domain(yAxisData).rangeRoundBands([marginBottom-20, marginTop], 1);
xAxisScale = d3.scale.ordinal().domain(xAxisData).rangeRoundBands([0, (marginRight-100)], 1);
yAxis.orient("bottom");
xAxis.orient("left");
color = "black";
}
flag = !flag
// Transitions the Y axis to X value set.
var yTransitions = innerCanvas.selectAll(".y.axis")
.transition()
.duration(2000)
.delay(200)
.attr("fill", color)
.attr("transform", function(){
if (flag){
return "transform(0, 354)";
} else {
return "transform(marginTop, marginBottom-20)";
//return "transform(marginBottom-20, marginTop)";
};
}
)
.call(yAxis);
// Transitions the X axis to Y value set.
var xTransitions = innerCanvas.selectAll(".x.axis")
.transition()
.duration(2000)
.delay(200)
.attr("fill", color)
//.attr("transform", "translate(0,354)")
.attr("transform", function(){
if (flag){
return "transform(marginTop, marginBottom-20)";
//return "transform(marginBottom-20, marginTop)";
} else {
return "transform(0, 354)";
};
}
)
.call(xAxis);
})
.on("mouseover", function(){
var thisObject = d3.select(this);
thisObject.style({"background-color": "DarkGreen"});
})
.on("mouseleave", function(){
var thisObject = d3.select(this);
thisObject.style({"background-color": "#4CAF50"});
});
};
Upon selecting the button, the axes move but not to the correct locations, nor do they go back to the right locations. Also the text is reversed after they move.
NOTE: I want to transition, both, the text and the line/path.
What's the right way to do this?
Thanks for any help you can offer.
If I understand correctly, you want to swap the xAxis for the yAxis, is that right? In that case, you have to do all the changes you want in the scales and the axis and, after applying the changes, transitioning the axis:
d3.transition(svg)
.select(".x.axis")//this class was previously applied
.transition()
.duration(2000)
.call(xAxis);
Check this snippet:
var width = 400, height = 400, flag = true;
var svg = d3.select("#svgdiv")
.append("svg")
.attr("width", width)
.attr("height", height);
var xScale = d3.scale.ordinal()
.domain("ABCDEFGHIJ".split(""))
.rangeBands([30, width - 20]);
var yScale = d3.scale.linear()
.domain([0, 10])
.range([height - 20, 20]);
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left");
svg.append("g")
.attr("class", "x axis")
.attr("transform", `translate(0,${height-20})`)
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(30, 0)")
.call(yAxis);
//here is the code for transposing the axes:
d3.select("#myButton").on("click", function(){
if(flag){
yAxis.orient("bottom");
xAxis.orient("left");
xScale.domain("ABCDEFGHIJ".split(""))
.rangeBands([width - 20, 30]);
yScale.domain([0, 10])
.range([20, height - 20]);
} else {
yAxis.orient("left");
xAxis.orient("bottom");
xScale.domain("ABCDEFGHIJ".split(""))
.rangeBands([30, width - 20]);
yScale.domain([0, 10])
.range([height - 20, 20]);
};
d3.transition(svg)
.select(".x.axis")
.transition()
.duration(2000)
.attr("transform", function(){
if(flag){
return "translate(30, 0)"
} else { return `translate(0,${height-20})`}})
.call(xAxis);
d3.transition(svg)
.select(".y.axis")
.transition()
.duration(2000)
.attr("transform", function(){
if(!flag){
return "translate(30, 0)"
} else { return `translate(10,${height-20})`}})
.call(yAxis);
flag = !flag;
});
.axis path,
.axis line {
fill: none;
stroke: #aaa;
shape-rendering: crispEdges;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<button id="myButton">Swap</button>
<div id="svgdiv"></div>
I'm trying to flip a graph in JS without changing the axis.
JavaScript isn't my specialty and after googling for 4 hours I'm starting to think it isn't possible.
Can someone point me in the right direction please?
var width = 960,
height = 500;
d3.json("data2.json", function(error, heatmap) {
var dx = heatmap[0].length,
dy = heatmap.length;
// Fix the aspect ratio.
// var ka = dy / dx, kb = height / width;
// if (ka < kb) height = width * ka;
// else width = height / ka;
var x = d3.scale.linear()
.domain([0, dx])
.range([0, width]);
var y = d3.scale.linear()
.domain([0, dy])
.range([height, 0]);
var color = d3.scale.linear()
.domain([null, 0, 30, 60, 90, 120])
.range(["#FFFFFF", "#FF0000", "#FF9933", "#FFFF00", "#99FF33", "#00FF00"]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("top")
.ticks(20);
var yAxis = d3.svg.axis()
.scale(y)
.orient("right");
d3.select("body").append("canvas")
.attr("width", dx)
.attr("height", dy)
.style("width", width + "px")
.style("height", height + "px")
.call(drawImage);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.call(removeZero);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.call(removeZero);
function drawImage(canvas) {
var context = canvas.node().getContext("2d"),
image = context.createImageData(dx, dy);
for (var y = 0, p = -1; y < dy; ++y) {
for (var x = 0; x < dx; ++x) {
var c = d3.rgb(color(heatmap[y][x]));
image.data[++p] = c.r;
image.data[++p] = c.g;
image.data[++p] = c.b;
image.data[++p] = 255;
}
}
context.putImageData(image, 0, 0);
}
function removeZero(axis) {
axis.selectAll("g").filter(function(d) { return !d; }).remove();
}
});
I see that you're actually not really using D3 for the part of the code that makes the image itself. In the nested loops, when you're manually producing the bitmap image, you can just traverse the data in reverse. Instead of indexing the row heatmap[y] directly on x, you should index on dx - x - 1, the column number counting from the right.
jsfiddle
As an aside, it seems a little strange to be mixing SVG and canvas drawing techniques here. Depending on how much data you're showing, it may be valuable to draw the heatmap with SVG as well, which would allow you to use just a single API to draw the chart and would enable interactions as well. Alternatively, you could go for drawing the whole thing on canvas, if a static image is more appropriate, for instance if the scale of data is massive.