d3.js: enter-update-exit pattern - Old points not removed - javascript

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>

Related

d3.js voronoi event. Mouseover seems to disappear when cursor is exactly above point

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>

D3.js jumpy zoom behaviour

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?

Trying to get circles on map to transition into bars in bar chart with D3

I create a bubble map with D3, and I want the user to be able to click on a button and the circles on the map will transition into bars of a bar chart. I am using the enter, update, exit pattern, but right now what I have isn't working as the bar chart is drawn on top and all the bars and circles are translated, instead of the circles transitioning into bars and the bars being translated into place. Below is the relevant part of my code, and here is the link to the demo: https://jhjanicki.github.io/circles-to-bars/
var projection = d3.geo.mercator()
.scale(150)
.center([20, 40])
.translate([width / 2, height / 2]);
var path= d3.geo.path()
.projection(projection);
var features = countries2.features;
d3.csv("data/sum_by_country.csv", function(error, data) {
data.sort(function(a,b){
return a.value - b.value;
});
var myfeatures= joinData(features, data, ['value']);
var worldMap = svg.append('g');
var world = worldMap.selectAll(".worldcountries")
.data(myfeatures)
.enter()
.append("path")
.attr("class", function(d){
return "World " + d.properties.name+" worldcountries";
})
.attr("d", path)
.style("fill", "#ddd")
.style("stroke", "white")
.style("stroke-width", "1");
var radius = d3.scale.sqrt()
.domain([0,1097805])
.range([3, 20]);
var newFeatures = [];
myfeatures.forEach(function(d){
if(d.properties.hasOwnProperty("value")){
console.log(d.properties.name);
newFeatures.push(d);
}
});
newFeatures.sort(function(a,b){
return b.properties.value - a.properties.value;
});
var bubbles = svg.append("g").classed("bubbleG","true");
bubbles.selectAll("circle")
.data(newFeatures)
.enter().append("circle")
.attr("class", "bubble")
.attr("transform", function(d) {
return "translate(" + path.centroid(d) + ")";
})
.attr("r", function(d){
return radius(d.properties.value);
})
.attr("fill","#2166ac")
.attr("stroke","white")
.attr("id", function(d){
return "circle "+d.properties.name;
});
$('#bubblebar').click(function(){
mapBarTransition(newFeatures,bubbles)
});
});
// button onclick
function mapBarTransition(data,bubbles){
var margin = {top:20, right:20, bottom:120, left:80},
chartW = width - margin.left - margin.right,
chartH = height - margin.top - margin.bottom;
var x = d3.scale.ordinal()
.domain(data.map(function(d) { return d.properties.name; }))
.rangeRoundBands([0, chartW], .4);
var y = d3.scale.linear()
.domain([0,1097805])
.nice()
.range([chartH,0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.ticks(8)
.orient("left");
var barW = width / data.length;
bubbles.append("g").classed("bubblebar-chart-group", true);
bubbles.append("g").classed("bubblebar-x-axis-group axis", true);
bubbles.append("g").classed("bubblebar-y-axis-group axis", true);
bubbles.transition().duration(1000).attr({transform: "translate(" + margin.left + "," + margin.top + ")"});
bubbles.select(".bubblebar-x-axis-group.axis")
.attr({transform: "translate(0," + (chartH) + ")"})
.call(xAxis)
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", function(d) {
return "rotate(-65)"
});
bubbles.select(".bubblebar-y-axis-group.axis")
.transition()
.call(yAxis);
barW = x.rangeBand();
var bars = bubbles.select(".bubblebar-chart-group")
.selectAll(".bubble")
.data(data);
bars.enter().append("rect")
.classed("bubble", true)
.attr("x", function(d) { return x(d.properties.name); })
.attr("y", function(d) { return y(d.properties.value); })
.attr("width", barW)
.attr("height", function(d) { return chartH - y(d.properties.value); })
.attr("fill","#2166ac");
bars.transition()
.attr("x", function(d) { return x(d.properties.name); })
.attr("y", function(d) { return y(d.properties.value); })
.attr("width", barW)
.attr("height", function(d) { return chartH - y(d.properties.value); });
bars.exit().transition().style({opacity: 0}).remove();
}
And here is the repo for your reference: https://github.com/jhjanicki/circles-to-bars
First, you have two very different selections with your circles and bars. They are in separate g containers and when you draw the bar chart, you aren't even selecting the circles.
Second, I'm not sure that d3 has any built-in way to know how to transition from a circle element to a rect element. There's some discussion here and here
That said, here's a quick hack with your code to demonstrate one way you could do this:
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css" rel='stylesheet'>
<link href="//rawgit.com/jhjanicki/circles-to-bars/master/css/style.css" rel='stylesheet'>
<!-- Roboto & Asar CSS -->
<link href='https://fonts.googleapis.com/css?family=Roboto' rel='stylesheet' type='text/css'>
<link href="https://fonts.googleapis.com/css?family=Asar" rel="stylesheet">
</head>
<body>
<button type="button" class="btn btn-primary" id="bubblebar">Bar Chart</button>
<div id="chart"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<!-- D3.geo -->
<script src="https://d3js.org/d3.geo.projection.v0.min.js"></script>
<!-- jQuery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="//rawgit.com/jhjanicki/circles-to-bars/master/data/countries2.js"></script>
<script>
window.onload = function() {
// set up svg and scrolling
var width = window.innerWidth / 2;
var height = window.innerHeight;
var svg = d3.select("#chart").append("svg")
.attr('width', width)
.attr('height', height);
var bubbleMapState = 'map'; // map or bar
var projection = d3.geo.mercator()
.scale(150)
.center([20, 40])
.translate([width / 2, height / 2]);
var path = d3.geo.path()
.projection(projection);
var features = countries2.features;
d3.csv("//rawgit.com/jhjanicki/circles-to-bars/master/data/sum_by_country.csv", function(error, data) {
data.sort(function(a, b) {
return a.value - b.value;
});
var myfeatures = joinData(features, data, ['value']);
var worldMap = svg.append('g');
var world = worldMap.selectAll(".worldcountries")
.data(myfeatures)
.enter()
.append("path")
.attr("class", function(d) {
return "World " + d.properties.name + " worldcountries";
})
.attr("d", path)
.style("fill", "#ddd")
.style("stroke", "white")
.style("stroke-width", "1");
var radius = d3.scale.sqrt()
.domain([0, 1097805])
.range([3, 20]);
var newFeatures = [];
myfeatures.forEach(function(d) {
if (d.properties.hasOwnProperty("value")) {
console.log(d.properties.name);
newFeatures.push(d);
}
});
newFeatures.sort(function(a, b) {
return b.properties.value - a.properties.value;
});
var bubbles = svg.append("g").classed("bubbleG", "true");
bubbles.selectAll("rect")
.data(newFeatures)
.enter().append("rect")
.attr("class", "bubble")
.attr("transform", function(d) {
return "translate(" + path.centroid(d) + ")";
})
.attr("width", function(d) {
return radius(d.properties.value) * 2;
})
.attr("height", function(d){
return radius(d.properties.value) * 2;
})
.attr("rx", 20)
.attr("fill", "#2166ac")
.attr("stroke", "white")
.attr("id", function(d) {
return "circle " + d.properties.name;
});
$('#bubblebar').click(function() {
mapBarTransition(newFeatures, bubbles)
});
});
// button onclick
function mapBarTransition(data, bubbles) {
if (bubbleMapState == 'map') {
bubbleMapState == 'bar';
} else {
bubbleMapState == 'map';
}
var margin = {
top: 20,
right: 20,
bottom: 120,
left: 80
},
chartW = width - margin.left - margin.right,
chartH = height - margin.top - margin.bottom;
var x = d3.scale.ordinal()
.domain(data.map(function(d) {
return d.properties.name;
}))
.rangeRoundBands([0, chartW], .4);
var y = d3.scale.linear()
.domain([0, 1097805])
.nice()
.range([chartH, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.ticks(8)
.orient("left");
var barW = width / data.length;
bubbles.append("g").classed("bubblebar-chart-group", true);
bubbles.append("g").classed("bubblebar-x-axis-group axis", true);
bubbles.append("g").classed("bubblebar-y-axis-group axis", true);
bubbles.transition().duration(1000).attr({
transform: "translate(" + margin.left + "," + margin.top + ")"
});
bubbles.select(".bubblebar-x-axis-group.axis")
.attr({
transform: "translate(0," + (chartH) + ")"
})
.call(xAxis)
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", function(d) {
return "rotate(-65)"
});
bubbles.select(".bubblebar-y-axis-group.axis")
.transition()
.call(yAxis);
barW = x.rangeBand();
var bars = d3.select(".bubbleG")
.selectAll(".bubble")
.data(data);
bars.enter().append("rect")
.classed("bubble", true)
.attr("x", function(d) {
return x(d.properties.name);
})
.attr("y", function(d) {
return y(d.properties.value);
})
.attr("width", barW)
.attr("height", function(d) {
return chartH - y(d.properties.value);
})
.attr("fill", "#2166ac");
bars.transition()
.duration(1000)
.attr("transform", function(d){
return "translate(" + x(d.properties.name) + "," + y(d.properties.value) + ")";
})
.attr("rx", 0)
.attr("width", barW)
.attr("height", function(d) {
return chartH - y(d.properties.value);
});
bars.exit().transition().style({
opacity: 0
}).remove();
}
function joinData(thisFeatures, thisData, DataArray) {
//loop through csv to assign each set of csv attribute values to geojson counties
for (var i = 0; i < thisData.length; i++) {
var csvCountry = thisData[i]; //the current county
var csvKey = csvCountry.Country; //the CSV primary key
//loop through geojson regions to find correct counties
for (var a = 0; a < thisFeatures.length; a++) {
var geojsonProps = thisFeatures[a].properties; //the current region geojson properties
var geojsonKey = geojsonProps.name; //the geojson primary key
//where primary keys match, transfer csv data to geojson properties object
if (geojsonKey == csvKey) {
//assign all attributes and values
DataArray.forEach(function(attr) {
var val = parseFloat(csvCountry[attr]); //get csv attribute value
geojsonProps[attr] = val; //assign attribute and value to geojson properties
});
};
};
};
return thisFeatures;
};
}
</script>
</body>

D3 Stacked bar graph works in Tributary but nowhere else

This has the expected results I want but when I import the code into my HTML file as a script it doesn't show anything at all.
var PUBLIC = [50,40,10];
var NONPROFIT = [30,40,30];
var FOR_PROFIT = [70,15,15];
var data = [
{"key":"PUBLIC", "pop1":PUBLIC[0], "pop2":PUBLIC[1], "pop3":PUBLIC[2]},
{"key":"NONPROFIT", "pop1":NONPROFIT[0], "pop2":NONPROFIT[1], "pop3":NONPROFIT[2]},
{"key":"FORPROFIT", "pop1":FOR_PROFIT[0], "pop2":FOR_PROFIT[1], "pop3":FOR_PROFIT[2]}
];
var n = 3, // Number of layers
m = data.length, // Number of samples per layer
stack = d3.layout.stack(),
labels = data.map(function(d) { return d.key; }),
// Go through each layer (pop1, pop2 etc, that's the range(n) part)
// then go through each object in data and pull out that objects's population data
// and put it into an array where x is the index and y is the number
layers = stack(d3.range(n).map(function(d)
{
var a = [];
for (var i = 0; i < m; ++i)
{
a[i] = { x: i, y: data[i]['pop' + (d+1)] };
}
return a;
})),
// The largest single layer
yGroupMax = d3.max(layers, function(layer) { return d3.max(layer, function(d) { return d.y; }); }),
// The largest stack
yStackMax = d3.max(layers, function(layer) { return d3.max(layer, function(d) { return d.y0 + d.y; }); });
var margin = {top: 40, right: 10, bottom: 20, left: 50},
width = 677 - margin.left - margin.right,
height = 533 - margin.top - margin.bottom;
var y = d3.scale.ordinal()
.domain(d3.range(m))
.rangeRoundBands([2, height], .08);
var x = d3.scale.linear()
.domain([0, yStackMax])
.range([0, width]);
var color = d3.scale.linear()
.domain([0, n - 1])
.range(["#aad", "#556"]);
var svg = d3.select("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 layer = svg.selectAll(".layer")
.data(layers)
.enter().append("g")
.attr("class", "layer")
.style("fill", function(d, i) { return color(i); });
layer.selectAll("rect")
.data(function(d) { return d; })
.enter().append("rect")
.attr("y", function(d) { return y(d.x); })
.attr("x", function(d) { return x(d.y0); })
.attr("height", y.rangeBand())
.attr("width", function(d) { return x(d.y); });
var yAxis = d3.svg.axis()
.scale(y)
.tickSize(1)
.tickPadding(6)
.tickValues(labels)
.orient("left");
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
I believe I have all the required libraries imported and then some:
<!-- D3 Library -->
<script src='https://d3js.org/d3.v3.min.js' charset='utf-8'></script>
<!-- jQuery Mobile -->
<script src="http://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js"></script>
<!-- jQuery Main -->
<script src='https://code.jquery.com/jquery-2.2.3.min.js' charset='utf-8'></script>
While the code to get my variables are a bit simplified (i.e. plainly setting my arrays) they are the same format as what is put within the data array.
Furthermore, this example does not work within CodePen either when I import everything that Tributary uses for its base libraries. While, again, this isn't 100% of the code I have going into the creation a much simpler working example on Tributary does not work on CodePen.
D3 has done nothing but kick my butt these past few weeks and I'm in need of some guidance. Thanks.
You need to wait for the page to be fully loaded or your can put the code before the closing </body> tag.
Solution1:
$(function() {
//Put your code here;
});
Solution2:
<body>
<svg></svg>
<script>
//your code here
</script>
</body>
Demo: https://jsfiddle.net/iRbouh/ag6p4kkg/

Trying to transfer Tributary D3.js example into JSFiddle

I'm rather a beginner at both JS and D3.js; my JSFiddle is here.
JSHint shows no errors, so I think I'm doing the D3 wrong.
I'm trying to do the same thing as in these questions, adapting a Tributary example to run outside of that platform: "Exporting" a Tributary example that makes use of the tributary object - d3.js and Getting horizontal stack bar example to display using d3.js (I'm even adapting the same code as the latter) Unfortunately, there is no corrected JSFiddle or other example in either answer
Here's my code:
var VanillaRunOnDomReady = function() {
var margins = {
top: 12,
left: 48,
right: 24,
bottom: 24
};
var data = [
{"key":"Nonviolent", "cat1":0.69, "cat2":0.21, "cat3":0.10},
{"key":"Violent", "cat1":0.53, "cat2":0.29, "cat3":0.18}
];
var catnames = {cat1: "No mental illness",
cat2: "Mild mental illness",
cat3: "Moderate or severe mental illness"};
var svg = d3.select('body')
.append('svg')
.attr('width', width + margins.left + margins.right)
.attr('height', height + margins.top + margins.bottom)
.append('g')
.attr('transform', 'translate(' + margins.left + ',' + margins.top + ')');
var n = 3, // number of layers
m = data.length, // number of samples per layer
stack = d3.layout.stack(),
labels = data.map(function(d) {return d.key;}),
//go through each layer (cat1, cat2 etc, that's the range(n) part)
//then go through each object in data and pull out that objects's catulation data
//and put it into an array where x is the index and y is the number
layers = stack(d3.range(n).map(function(d) {
var a = [];
for (var i = 0; i < m; ++i) {
a[i] = {x: i, y: data[i]['cat' + (d+1)]};
}
return a;
})),
//the largest single layer
yGroupMax = d3.max(layers, function(layer) { return d3.max(layer, function(d) { return d.y; }); }),
//the largest stack
yStackMax = d3.max(layers, function(layer) { return d3.max(layer, function(d) { return d.y0 + d.y; }); });
var margin = {top: 40, right: 10, bottom: 20, left: 50},
width = 677 - margin.left - margin.right,
height = 533 - margin.top - margin.bottom;
var y = d3.scale.ordinal()
.domain(d3.range(m))
.rangeRoundBands([2, height], 0.08);
var x = d3.scale.linear()
.domain([0, yStackMax])
.range([0, width]);
var color = d3.scale.linear()
.domain([0, n - 1])
.range(["#aad", "#556"]);
var layer = svg.selectAll(".layer")
.data(layers)
.enter().append("g")
.attr("class", "layer")
.style("fill", function(d, i) { return color(i); });
layer.selectAll("rect")
.data(function(d) { return d; })
.enter().append("rect")
.attr("y", function(d) { return y(d.x); })
.attr("x", function(d) { return x(d.y0); })
.attr("height", y.rangeBand())
.attr("width", function(d) { return x(d.y); });
var yAxis = d3.svg.axis()
.scale(y)
.tickSize(1)
.tickPadding(6)
.tickValues(labels)
.orient("left");
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
};
I think you are just missing a (); at the end of your function and before the semicolon. That would make it self executing. I forked your fiddle.
var VanillaRunOnDomReady = function() {
var margins = {
top: 12,
left: 48,
right: 24,
bottom: 24
};
var data = [
{"key":"Nonviolent", "cat1":0.69, "cat2":0.21, "cat3":0.10},
{"key":"Violent", "cat1":0.53, "cat2":0.29, "cat3":0.18}
];
var catnames = {cat1: "No mental illness",
cat2: "Mild mental illness",
cat3: "Moderate or severe mental illness"};
var svg = d3.select('body')
.append('svg')
.attr('width', width + margins.left + margins.right)
.attr('height', height + margins.top + margins.bottom)
.append('g')
.attr('transform', 'translate(' + margins.left + ',' + margins.top + ')');
var n = 3, // number of layers
m = data.length, // number of samples per layer
stack = d3.layout.stack(),
labels = data.map(function(d) {return d.key;}),
//go through each layer (cat1, cat2 etc, that's the range(n) part)
//then go through each object in data and pull out that objects's catulation data
//and put it into an array where x is the index and y is the number
layers = stack(d3.range(n).map(function(d) {
var a = [];
for (var i = 0; i < m; ++i) {
a[i] = {x: i, y: data[i]['cat' + (d+1)]};
}
return a;
})),
//the largest single layer
yGroupMax = d3.max(layers, function(layer) { return d3.max(layer, function(d) { return d.y; }); }),
//the largest stack
yStackMax = d3.max(layers, function(layer) { return d3.max(layer, function(d) { return d.y0 + d.y; }); });
var margin = {top: 40, right: 10, bottom: 20, left: 50},
width = 677 - margin.left - margin.right,
height = 533 - margin.top - margin.bottom;
var y = d3.scale.ordinal()
.domain(d3.range(m))
.rangeRoundBands([2, height], 0.08);
var x = d3.scale.linear()
.domain([0, yStackMax])
.range([0, width]);
var color = d3.scale.linear()
.domain([0, n - 1])
.range(["#aad", "#556"]);
var svg = d3.select("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 layer = svg.selectAll(".layer")
.data(layers)
.enter().append("g")
.attr("class", "layer")
.style("fill", function(d, i) { return color(i); });
layer.selectAll("rect")
.data(function(d) { return d; })
.enter().append("rect")
.attr("y", function(d) { return y(d.x); })
.attr("x", function(d) { return x(d.y0); })
.attr("height", y.rangeBand())
.attr("width", function(d) { return x(d.y); });
var yAxis = d3.svg.axis()
.scale(y)
.tickSize(1)
.tickPadding(6)
.tickValues(labels)
.orient("left");
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
}();

Categories