Bar chart unable to execute transition after button clicked - d3.js

I am drawing a simple bar chart with two buttons to toggle the chart based on two datasets.
However, the chart does not do the transition when I click on the buttons.
The data is loaded from a csv file.
party,ge14,latest
PKR,47,50
DAP,42,42
Umno,54,38
GPS,0,19
PAS,18,18
Bersatu,13,16
Independent,3,12
Amanah,11,11
Warisan,8,9
GBS,0,3
Other BN parties,25,2
Upko,0,1
Source code
d3.csv('data/seatcount.csv')
.then(data => {
const width = 900,
height = 700,
margin = 25;
const svg = d3.select('#bar')
.append('svg')
.attr('width', width)
.attr('height', height)
.attr('viewBox', '0 0 ' + Math.min(width, height) + ' ' + Math.min(width, height))
.attr('preserveAspectRatio', 'xMinYMin')
.append('g');
// define scale
const xScale = d3.scaleBand()
.domain(data.map(d => d.party))
.range([margin, (width - margin)])
.padding(.2);
const yScale = d3.scaleLinear()
.domain([0, 60])
.range([(height - margin), margin]);
// define axes
const xAxis = d3.axisBottom(xScale)
.ticks(12)
const yAxis = d3.axisLeft(yScale)
.ticks(6);
svg.append('g')
.attr('transform', `translate(0, ${height - margin})`)
.call(xAxis)
.style('font-size', '.7em');
svg.append('g')
.attr('transform', `translate(${margin}, 0)`)
.call(yAxis)
.style('font-size', '.7em');
// plot columns
let cols = svg.selectAll('.col')
.data(data)
.enter();
cols.append('rect')
.attr('x', (d) => xScale(d.party))
.attr('y', (height - margin))
.attr('width', xScale.bandwidth())
.attr('height', 0)
.style('fill', '#dddddd')
.transition()
.delay((d, i) => 100 * i)
.attr('y', (d) => {
return yScale(d.latest)
})
.attr('height', (d) => (height - margin - yScale(d.latest)));
// label the bars
cols.append('text')
.attr('x', d => {
return (xScale(d.party) + xScale.bandwidth() / 2);
})
.attr('y', d => {
return (yScale(d.latest) + 13);
})
.style('fill', '#333333')
.attr('text-anchor', 'middle')
.text(d => d.latest)
.style('font-size', '.8em')
.style('visibility', 'hidden')
.transition()
.delay((d, i) => 100 * i)
.style('visibility', 'visible');
function moveCols(data, period) {
cols.data(data)
.transition()
.attr('x', function(d) {
return xScale(d.party);
})
.attr('y', function(d) {
console.log(yScale(d[period]));
return yScale(d[period]);
})
.attr('width', xScale.bandwidth())
.attr('height', function(d) {
return (height - margin - yScale(d[period]));
});
cols.selectAll('text')
.remove();
cols.append('text')
.attr('x', d => {
return (xScale(d.party) + xScale.bandwidth() / 2);
})
.attr('y', d => {
return (yScale(d[period]) + 13);
})
.style('fill', '#333333')
.attr('text-anchor', 'middle')
.text(d => d[period])
.style('font-size', '.8em')
.style('visibility', (d, i) => {
// console.log(d[period]);
if (d[period] <= 0) {
return 'hidden';
} else {
return 'visible';
}
});
}
d3.select('#latest').on('click', () => {
moveCols(data, 'latest')
});
d3.select('#ge14').on('click', () => {
moveCols(data, 'ge14')
})
});
In the end, once I have clicked on #latest or #ge14, only the label of the bars changed, but not the bars themselves.
And there are errors showed in the console.
Uncaught TypeError: this.getAttribute is not a function
at ot.<anonymous> (d3.v5.min.js:2)
at ot.e (d3.v5.min.js:2)
at o (d3.v5.min.js:2)
at d3.v5.min.js:2
at fr (d3.v5.min.js:2)
at cr (d3.v5.min.js:2)

I have reformatted my code and it works! I got no complaint.
const margin = { top: 20, right: 20, bottom: 50, left: 40 },
width = 900
height = 700
const svg = d3.select('#bar')
.append('svg')
.attr('width', width)
.attr('height', height)
.attr('viewBox', '0 0 ' + Math.min(width, height) + ' ' + Math.min(width, height))
.attr('preserveAspectRatio', 'xMinYMin');
const x = d3.scaleBand()
.rangeRound([0, (width - margin.left - margin.right)])
.padding(0.1);
const y = d3.scaleLinear()
.rangeRound([(height - margin.bottom), 0]);
const g = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
d3.csv('data/seatcount.csv')
.then(data => {
data.forEach(d => {
d.party = d.party;
d.ge14 = +d.ge14;
d.latest = +d.latest;
});
x.domain(data.map(d => d.party));
y.domain([0, d3.max(data, d => d.latest)]);
let duration = 1000;
// define axes
g.append('g')
.attr('class', 'axis, x-axis')
.attr('transform', `translate(0, ${height - margin.bottom})`)
.call(d3.axisBottom(x));
g.append('g')
.attr('class', 'axis, y-axis')
.call(d3.axisLeft(y));
// plot columns
g.selectAll('.col')
.data(data)
.enter()
.append('rect')
.attr('x', d => x(d.party))
.attr('y', height - margin.bottom)
.attr('width', x.bandwidth())
.attr('height', 0)
.attr('class', 'col')
.transition()
.duration(duration)
.attr('y', d => y(d.latest))
.attr('height', d => (height - margin.bottom - y(d.latest)))
.ease(d3.easeBounce);
g.selectAll('.label')
.data(data)
.enter()
.append('text')
.attr('class', 'label')
.attr('x', d => {
return (x(d.party) + x.bandwidth() / 2);
})
.attr('y', (d, i) => {
if (d.latest <= 5) {
return (y(d.latest) - 5);
} else {
return (y(d.latest) + 13);
}
})
.text(d => d.latest)
.style('font-size', '.8em')
.style('visibility', 'hidden')
.transition()
.delay(duration)
.style('visibility', 'visible');
function moveCols(data, period) {
y.domain([0, d3.max(data, d => d[period])]);
g.select('.y-axis')
.transition()
.call(d3.axisLeft(y));
g.selectAll('.label')
.remove();
g.selectAll('.col')
.data(data)
.transition()
.attr('x', d => x(d.party))
.attr('y', d => y(d[period]))
.attr('width', x.bandwidth())
.attr('height', d => (height - margin.bottom - y(d[period])))
.ease(d3.easeBounce);
g.selectAll('.label')
.data(data)
.enter()
.append('text')
.attr('class', 'label')
.attr('x', d => {
return (x(d.party) + x.bandwidth() / 2);
})
.attr('y', (d, i) => {
if (d[period] <= 5) {
return (y(d[period]) - 5);
} else {
return (y(d[period]) + 13);
}
})
.style('fill', '#333333')
.attr('text-anchor', 'middle')
.text(d => d[period])
.style('font-size', '.8em');
}
d3.select('#latest').on('click', () => {
moveCols(data, 'latest');
});
d3.select('#ge14').on('click', () => {
moveCols(data, 'ge14');
})
});

Related

drag grouped elements move not continuous

I try move a grouped element but after click and drag, the elements jumped away.
demo()
function demo() {
var tooltip = d3.select('body')
.append('div')
.attr('id','tooltip')
.style('position','absolute')
.style('opacity',0)
.style('background','lightsteelblue')
var svg = d3.select("body")
.append("svg")
.attr("width", 300)
.attr("height", 200)
.style("background", "#ececec")
add_grid(svg);
var data = [
{
text: "O",
x: 50,
y: 50
},
];
var g = svg.append('g')
var fontsize = 20;
var box = g.selectAll(".box")
.data(data)
.join('g')
.attr('class','box')
.attr("pointer-events", "all")
box.call(
d3.drag()
.on("start",function(event,d) {
d3.select(this).raise().classed("active", true);
d3.select('#tooltip')
.transition().duration(100)
.style('opacity', 1)
})
.on("drag",function(event,d) {
d.x = event.x
d.y = event.y
d3.select(this).attr('transform',`translate(${d.x},${d.y})`)
var desc = "(" + d.x.toFixed(1) +"," + d.y.toFixed(1) + ")"
d3.select('#tooltip')
.style('left', (event.x+2) + 'px')
.style('top', (event.y-2) + 'px')
.text(desc)
})
.on("end", function dragEnd(event,d) {
d3.select(this).classed("active", false);
d3.select('#tooltip').style('opacity', 0)}
))
.on('mouseover', function(event,d) {
})
.on('mouseout', function(event,d) {
})
.on('mousemove', function(event,d) {
})
.on("mousedown", function(){
})
.on("mouseup", function(){
});
var txt = box.append("text")
.attr("text-anchor", "middle")
.attr("dominant-baseline",'text-before-edge')//'central')//text-bottom
.attr("font-size", fontsize)
.attr("x", (d) => d.x)
.attr("y", (d) => d.y)
var tspan = txt.selectAll(".tspan")
.data((d) => d.text.split("\n"))
.join("tspan")
.attr("class", "tspan")
.attr("x", function (d) {
let x = +d3.select(this.parentNode).attr("x");
return x;
})
.attr("y", function (d,i) {
let y = +d3.select(this.parentNode).attr("y");
return y + i*fontsize * .9;
})
.text((d) => d);
box.each((d,i,n) => {
var bbox = d3.select(n[i]).node().getBBox()
var padding = 2
bbox.x -= padding
bbox.y -= padding
bbox.width += 2*padding
bbox.height += 2*padding
d.bbox = bbox
})
.attr('transform',d => `translate(${0},${-d.bbox.height/2})`)
.append('rect')
.attr('x',d => d.bbox.x)
.attr('y',d => d.bbox.y)
.attr('width', d => d.bbox.width)
.attr('height',d => d.bbox.height)
.attr('stroke','red')
.attr('fill','none')
add_dots(svg,data)
function add_dots(svg,data) {
svg.selectAll('.dots')
.data(data)
.join('circle')
.attr('class','dots')
.attr('r',2)
.attr('cx',d => d.x)
.attr('cy',d => d.y)
.attr('fill','red')
}
function add_grid(svg) {
var w = +svg.attr("width");
var step = 10;
var mygrid = function (d) {
return `M 0,${d} l ${w},0 M ${d},0 l 0,${w}`;
};
var grid = [];
for (var i = 0; i < w; i += step) {
grid.push(i);
}
svg
.append("g")
.selectAll(null)
.data(grid)
.enter()
.append("path")
.attr("d", (d) => mygrid(d))
.attr("fill", "none")
.attr("stroke", "green")
.attr("stroke-width", 0.5);
}
}
<script src="https://unpkg.com/d3#7.0.4/dist/d3.min.js"></script>

D3 updates every path only when select() is running instead of selectAll()

I'm trying to update chord diagram according to the changes in data.
I created groups for each element and updated the data binding.
However, for some reason, it updates the data accordingly, only when I go with 'select' instead of 'selectAll' which feels quite odd for me
because everytime I update data binding I have used selectAlll only to update every element related.
My code is as below.
-Creating initial diagram-
var g = svg.selectAll('g.groups')
.data(figureCalculation.groups)
.join('g')
.attr('class', (d, i) => { return `group ${nameArray[i]}` })
g.append('path')
.attr('class', (d) => { return `arc ${d.value}` })
.attr('d', arc)
.style('fill', 'grey')
.style('stroke', 'pink')
.style("opacity", 0)
.transition().duration(1000)
.style("opacity", 0.8);
var chords = svg.append('g')
.selectAll('path')
.data(figureCalculation.chords)
.join('path')
.attr('class', 'chords')
.attr('d', d3.ribbon().radius(innerRadius))
.style('fill', 'green')
.style('stroke', 'red')
-update the data binding-
setTimeout(updateData, 2500)
function updateData() {
figureCalculation = d3.chord()
.padAngle(0.05)
.sortSubgroups(d3.descending)(matrix1)
figureCalculation.chords = [];
figureCalculation.forEach((d) => {
figureCalculation.chords.push(d)
})
g.select('path').data(figureCalculation.groups)
.join('path').attr('d', arc)
.style('fill', 'grey')
.style('stroke', 'pink')
chords.select('path').data(figureCalculation.chords)
.join('path')
.attr('d', d3.ribbon().radius(innerRadius))
.style('fill', 'green')
.style('stroke', 'red')
}
The full code is in the following link.
https://codepen.io/jotnajoa/pen/qBaXKVW
Your SVG is structured strangely.
First, for your groups, you create a g with one child of path. Your update doesn't work because you do a selectAll of paths on the g with only one child.
Then for your chords that variable is already a collection of path. You are treating it like it's the g element holding the path.
I'd rewrite your code like this:
let margin = { top: 50, bottom: 50, left: 20, right: 20 }
let width = 600 - margin.left - margin.right;
let height = 600 - margin.top - margin.bottom;
let innerRadius = Math.min(width, height) * 0.4;
let outterRadius = innerRadius * 1.2
let svg = d3.select('#graph').append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${width / 2 + margin.left}, ${height / 2 + margin.top})`)
var nameArray = ['A', 'B', 'C', 'D'];
var matrix = [
[11975, 5871, 8916, 2868],
[1951, 10048, 2060, 6171],
[8010, 16145, 8090, 8045],
[1013, 990, 940, 6907]
];
var matrix1 = [
[175, 571, 916, 868],
[1951, 1248, 2060, 5471],
[8010, 14145, 4390, 4245],
[1213, 990, 540, 1207]
];
let figureCalculation = d3.chord()
.padAngle(0.05)
.sortSubgroups(d3.descending)(matrix)
figureCalculation.chords = [];
figureCalculation.forEach((d) => {
figureCalculation.chords.push(d)
})
var arc = d3.arc().innerRadius(innerRadius).outerRadius(outterRadius)
svg
.append('g')
.attr('class', 'groups')
.selectAll('path')
.data(figureCalculation.groups)
.join('path')
.attr('class', (d, i) => { return `group ${nameArray[i]}` })
.attr('d', arc)
.style('fill', 'grey')
.style('stroke', 'pink')
.style("opacity", 0)
.transition().duration(1000)
.style("opacity", 0.8);
svg
.append('g')
.attr('class', 'chords')
.selectAll('path')
.data(figureCalculation.chords)
.join('path')
.attr('class', 'chords')
.attr('d', d3.ribbon().radius(innerRadius))
.style('fill', 'green')
.style('stroke', 'red')
function updateData() {
figureCalculation = d3.chord()
.padAngle(0.05)
.sortSubgroups(d3.descending)(matrix1)
figureCalculation.chords = [];
figureCalculation.forEach((d) => {
figureCalculation.chords.push(d)
})
svg.select('.groups')
.selectAll('path').data(figureCalculation.groups)
.join('path').attr('d', arc)
.style('fill', 'grey')
.style('stroke', 'pink')
svg.select('.chords')
.selectAll('path').data(figureCalculation.chords)
.join('path')
.attr('d', d3.ribbon().radius(innerRadius))
.style('fill', 'green')
.style('stroke', 'red')
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.3.1/d3.min.js"></script>
<button onclick="updateData()">Update Data</button>
<div id="graph"></div>
I think you shouldn't be sub-selecting in updateData at all:
g.data(figureCalculation.groups).join(...)
chords.data(figureCalculation.chords).join(...)

How to set the width and height of svg to be equal to its g?

I have a data legend SVG and I want to set the height and width of this SVG to be equal to the number of g which is based on the data. But how do I achieve this? My SVG height and width is always not according to the my G. I tried node().getBBox() but it is still not giving me the right height and width.
Here is my code :
var legend = d3.select(".svgLegend")
.append('svg')
.attr("id", "legend")
.append('g')
.attr("class", "mainGroup")
.attr('legend', true)
var itemEnter = legend.selectAll('g.legendItem')
.data(legendData)
.enter()
.append('g')
.attr('class', function (d) {
return 'legendItem ' + safe_name(d.name);
})
itemEnter.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', '10')
.attr('height', '10')
.style('fill', function (d) {
return color(d.name);
})
.attr('transform', 'translate(10,6)')
.attr('class', function (d) {
return 'legendRect ' + safe_name(d.name);
})
itemEnter.append('text')
.attr('x', 0)
.attr('y', 0)
.attr('class', 'legendText')
.text(function (d) { return d.name })
.attr('transform', 'translate(25, 15)')
itemEnter.selectAll("text").each(function () {
var textLength = this.getComputedTextLength();
itemEnter.attr("transform", function (d, i) { return "translate(" + i % 8 * (textLength + 60) + "," + Math.floor(i / 8) * itemHeight + ")"; })
})
Legend Data :
[
{
"name":"Malaysia",
"value":350,
"percentage":"48.61"
},
{
"name":"England",
"value":300,
"percentage":"41.67"
},
{
"name":"China",
"value":400,
"percentage":"55.56"
},
{
"name":"South Korea",
"value":600,
"percentage":"83.33"
}
]
What I want to achieve is that the svg's height and width is exact same as itemEnter's height and width.
You can use the values from getClientBoundingRect() to set the width and height of your SVG:
var bRect = legend.node().getBoundingClientRect()
svg.attr('width', bRect.width + 10)
.attr('height', bRect.height)
(adding in an extra 10px to the width for safety)
Demo:
var legendData = [
{
"name":"Malaysia",
"value":350,
"percentage":"48.61"
},
{
"name":"England",
"value":300,
"percentage":"41.67"
},
{
"name":"China",
"value":400,
"percentage":"55.56"
},
{
"name":"South Korea",
"value":600,
"percentage":"83.33"
}
]
function safe_name (t) {
return t.replace(/\W/g, '_')
}
function color (d) {
var colors = {
China: 'deepskyblue',
'South Korea': 'deeppink',
England: 'red',
Malaysia: 'goldenrod'
}
return colors[d]
}
var svg = d3.select(".svgLegend")
.append('svg')
.attr("id", "legend")
var legend = svg
.append('g')
.attr("class", "mainGroup")
.attr('legend', true)
var itemEnter = legend.selectAll('g.legendItem')
.data(legendData)
.enter()
.append('g')
.attr('class', function (d) {
return 'legendItem ' + safe_name(d.name);
})
itemEnter.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', '10')
.attr('height', '10')
.style('fill', function (d) {
return color(d.name);
})
.attr('transform', 'translate(10,6)')
.attr('class', function (d) {
return 'legendRect ' + safe_name(d.name);
})
itemEnter.append('text')
.attr('x', 0)
.attr('y', 0)
.attr('class', 'legendText')
.text(function (d) { return d.name })
.attr('transform', 'translate(25, 15)')
var itemHeight = 25
itemEnter.selectAll("text")
.each(function () {
var textLength = this.getComputedTextLength();
itemEnter.attr("transform", function (d, i) { return "translate(" + i % 8 * (textLength + 60) + "," + Math.floor(i / 8) * itemHeight + ")"; })
})
var bRect = legend.node().getBoundingClientRect()
svg.attr('width', bRect.width + 10)
.attr('height', bRect.height)
<script src="http://d3js.org/d3.v5.js"></script>
<div class="svgLegend"></div>
Using getBBox()is a good idea. This is how I would do it.
let bbox = test.getBBox();
//console.log(bbox)
svg.setAttributeNS(null, "viewBox", `${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height} `)
svg.setAttributeNS(null, "width", bbox.width);
svg.setAttributeNS(null, "height", bbox.height);
<svg id="svg" >
<g id="gtest">
<path id="test" d="M187.476,214.443c-2.566,11.574-4.541,22.658-7.542,33.456
c-3.558,12.8-7.14,25.713-12.242,37.938c-10.223,24.495-41.321,29.239-58.824,9.548c-9.592-10.792-11.295-26.9-3.539-40.556
c11.233-19.778,25.391-37.46,40.447-54.438c1.07-1.207,2.116-2.436,3.893-4.484c-7.212,0.9-13.349,1.988-19.529,2.374
c-16.283,1.018-32.578,2.21-48.881,2.437c-18.686,0.261-32.846-10.154-37.071-26.055c-6.762-25.449,15.666-48.973,41.418-43.338
c23.645,5.175,46.447,12.901,68.424,23.051c1.033,0.478,2.083,0.918,3.933,1.731c-0.83-1.947-1.341-3.225-1.911-4.475
c-9.896-21.701-18.159-43.986-23.192-67.337c-4.587-21.28,8.933-40.56,29.946-43.257c20.134-2.585,38.124,12.991,39.091,34.294
c1.029,22.682-0.049,45.292-3.58,67.755c-0.17,1.079-0.152,2.188-0.246,3.659c8.05-6.831,15.471-13.737,23.52-19.811
c11.147-8.412,22.398-16.795,34.27-24.113c18.35-11.312,40.821-4.481,50.028,14.385c9.091,18.628,0.131,40.586-20.065,48.198
c-11.034,4.158-22.248,7.944-33.594,11.143c-11.321,3.191-22.908,5.438-34.866,8.212c1.189,0.81,2.19,1.504,3.205,2.18
c18.402,12.261,37.157,24.032,55.101,36.932c14.769,10.616,18.619,29.317,10.675,44.578c-7.537,14.477-25.151,22.136-40.767,17.583
c-7.583-2.212-14.022-6.469-18.523-12.919c-12.463-17.86-24.638-35.924-36.898-53.925
C189.24,217.849,188.547,216.357,187.476,214.443z"/>
</g>
</svg>

d3 heatmap chart draw data based on a filter

I am referring this to draw a chart. I want to draw a month vs time of the day chart.From what I infer from this link is that it draws data like in a matrix format (1,1),(1,2) ... What I want is to show Jan Feb ... Dec on x axis and 0:00,1:00 .... 24:00 on y axis. I have a month filter so an user can select from Mar to Jun if he wants to. How can I modify this code to suit my needs?
const margin = { top: 50, right: 0, bottom: 100, left: 30 },
width = 960 - margin.left - margin.right,
height = 430 - margin.top - margin.bottom,
gridSize = Math.floor(width / 24),
legendElementWidth = gridSize*2,
buckets = 9,
colors = ["#ffffd9","#edf8b1","#c7e9b4","#7fcdbb","#41b6c4","#1d91c0","#225ea8","#253494","#081d58"], // alternatively colorbrewer.YlGnBu[9]
days = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul","Aug","Sep","Oct","Nov","Dec"],
times = ["1a", "2a", "3a", "4a", "5a", "6a", "7a", "8a", "9a", "10a", "11a", "12a", "1p", "2p", "3p", "4p", "5p", "6p", "7p", "8p", "9p", "10p", "11p", "12p"];
datasets = ["data.tsv", "data2.tsv"];
const svg = d3.select("#chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
const dayLabels = svg.selectAll(".dayLabel")
.data(days)
.enter().append("text")
.text(function (d) { return d; })
.attr("x", 0)
.attr("y", (d, i) => i * gridSize)
.style("text-anchor", "end")
.attr("transform", "translate(-6," + gridSize / 1.5 + ")")
.attr("class", (d, i) => ((i >= 0 && i <= 4) ? "dayLabel mono axis axis-workweek" : "dayLabel mono axis"));
const timeLabels = svg.selectAll(".timeLabel")
.data(times)
.enter().append("text")
.text((d) => d)
.attr("x", (d, i) => i * gridSize)
.attr("y", 0)
.style("text-anchor", "middle")
.attr("transform", "translate(" + gridSize / 2 + ", -6)")
.attr("class", (d, i) => ((i >= 7 && i <= 16) ? "timeLabel mono axis axis-worktime" : "timeLabel mono axis"));
const type = (d) => {
return {
day: +d.day,
hour: +d.hour,
value: +d.value
};
};
const heatmapChart = function(tsvFile) {
d3.tsv(tsvFile, type, (error, data) => {
const colorScale = d3.scaleQuantile()
.domain([0, buckets - 1, d3.max(data, (d) => d.value)])
.range(colors);
const cards = svg.selectAll(".hour")
.data(data, (d) => d.day+':'+d.hour);
cards.append("title");
cards.enter().append("rect")
.attr("x", (d) => (d.hour - 1) * gridSize)
.attr("y", (d) => (d.day - 1) * gridSize)
.attr("rx", 4)
.attr("ry", 4)
.attr("class", "hour bordered")
.attr("width", gridSize)
.attr("height", gridSize)
.style("fill", colors[0])
.merge(cards)
.transition()
.duration(1000)
.style("fill", (d) => colorScale(d.value));
cards.select("title").text((d) => d.value);
cards.exit().remove();
const legend = svg.selectAll(".legend")
.data([0].concat(colorScale.quantiles()), (d) => d);
const legend_g = legend.enter().append("g")
.attr("class", "legend");
legend_g.append("rect")
.attr("x", (d, i) => legendElementWidth * i)
.attr("y", height)
.attr("width", legendElementWidth)
.attr("height", gridSize / 2)
.style("fill", (d, i) => colors[i]);
legend_g.append("text")
.attr("class", "mono")
.text((d) => "≥ " + Math.round(d))
.attr("x", (d, i) => legendElementWidth * i)
.attr("y", height + gridSize);
legend.exit().remove();
});
};
heatmapChart(datasets[0]);
const datasetpicker = d3.select("#dataset-picker")
.selectAll(".dataset-button")
.data(datasets);
datasetpicker.enter()
.append("input")
.attr("value", (d) => "Dataset " + d)
.attr("type", "button")
.attr("class", "dataset-button")
.on("click", (d) => heatmapChart(d));

d3 not creating enough elements to match my data - why?

so here... http://codepen.io/dwilbank68/pen/VagOKd?editors=0010
I have the same exact array of data creating the right number of dots, but not enough number of text elements.
It's not a margin issue obscuring the names... the elements are not even in the DOM.
I even appended the index to the name, to prove that the graphData array has the right number of elements.
What else could be wrong?
svg.selectAll('.dot') // creates the correct number of dots
.data(graphData)
.enter()
.append('circle')
.attr('class', 'dot')
.attr('r', 5)
.attr('cx', (d)=> xScale(d.secondsBehind) )
.attr('cy', (d)=> yScale(d.place) )
.style('fill', (d)=> colorScale(d.dopingAllegations) );
svg.selectAll('.label') // does not create the last two text elements
.data(graphData)
.enter()
.append('text')
.attr('class', 'label')
.attr('x', (d)=> xScale(d.secondsBehind) + 10)
.attr('y', (d)=> yScale(d.place) + 4)
.text( (d)=> d.name );
var url = "https://raw.githubusercontent.com/FreeCodeCamp/ProjectReferenceData/master/cyclist-data.json";
var m = {t: 20, r: 120, b: 30, l: 40},
width = 800 - m.l - m.r,
height = 700 - m.t - m.b;
var svg = d3.select("body").append("svg")
.attr("width", width + m.l + m.r)
.attr("height", height + m.t + m.b)
.append("g")
.attr("transform", "translate(" + m.l + "," + m.t + ")");
var div = d3.select('body')
.append('div')
.style({
'position':'absolute',
'text-align':'center',
'width':'240px',
'height':'2.5em',
'font':'1.5em sans-serif',
'color':'yellow',
'background':'black',
'border-radius':'8px',
'border':'solid 1px green',
'opacity':0
});
var colorScale = d3.scale.ordinal()
.range(["#FF0000", "#009933"]);
var xScale = d3.scale.linear()
.range([width, 0]);
var yScale = d3.scale.linear()
.range([0, height]);
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom")
.tickFormat(formatMinSec);
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left");
d3.json(url, callback);
function callback (error, data) {
if (error) throw error;
var bestTime = _.sortBy(data, 'Place')[0].Seconds;
var graphData = _.map(data, (d)=> ({
'secondsBehind': Math.abs(bestTime - d.Seconds),
'year': d.Year,
'nationality': d.Nationality,
'doping': d.Doping,
'dopingAllegations': d.Doping.length > 0 ? "Doping Allegations":"No Doping Allegations",
'name': d.Name,
'place': d.Place,
'time': d.Time
}) )
var timeRange = d3.extent(graphData, (d) => d.secondsBehind );
xScale.domain([timeRange[0]-15, timeRange[1]]);
var rankRange = d3.extent(graphData, (d) => d.place );
yScale.domain([rankRange[0], rankRange[1]+1]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.append("text")
.text("Minutes : Seconds Behind Fastest Time")
.attr({
'class': 'label',
'x': width,
'y': -6
})
.style("text-anchor", "end");
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.text("Ranking")
.attr({
'class': 'label',
"transform": "rotate(-90)",
"y": 6,
"dy": ".71em"
})
.style("text-anchor", "end");
svg.selectAll('.dot')
.data(graphData)
.enter()
.append('circle')
.attr('class', 'dot')
.attr('r', 5)
.attr('cx', (d)=> xScale(d.secondsBehind) )
.attr('cy', (d)=> yScale(d.place) )
.style('fill', (d)=> colorScale(d.dopingAllegations) );
svg.selectAll('.label')
.data(graphData)
.enter()
.append('text')
.attr('class', 'label')
.attr('x', (d)=> xScale(d.secondsBehind) + 10)
.attr('y', (d)=> yScale(d.place) + 4)
.text( (d)=> d.name );
// d3.selectAll('.dot')
// .on('mouseover', mouseover)
// .on('mouseout', mouseout);
var legend = svg.selectAll('.legend')
.data(colorScale.domain())
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function(d,i){return 'translate(0,' +i*20+')';});
legend.append('rect')
.attr('x', width)
.attr('y', 100)
.attr('width', 18)
.attr('height', 18)
.style('fill', colorScale);
legend.append('text')
.text((d)=> d)
.attr('x', width - 18)
.attr('y', 108)
.attr('dy', '.35em')
.style('text-anchor', 'end');
};
// function mouseover(d){
// div.html('Sepal Width: ' + d.sepalWidth +
// '<br/>' +
// 'Sepal Length: ' + d.sepalLength)
// .style('left', (d3.event.pageX + 9) +'px')
// .style('top', (d3.event.pageY - 43) +'px')
// .style('opacity', 1);
// }
// function mouseout(){
// div.style('opacity', 1e-6);
// }
function formatMinSec(d){
if( d % 60 > 9){
return Math.floor(d/60) +':'+ d%60
} else {
return Math.floor(d/60) +':0'+ d%60
}
}
body {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.dot {
stroke: #000;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.6.1/lodash.min.js"></script>
you add two labels to your axes with a class label
your selection svg.selectAll('.label') searches the whole svg and is based on .label.
That picks up those two labels, counting them as already created and thus irrelevant in the enter phase
The simplest fix is to wrap your selection in a g element, something like
var graph = svg.append("g");
graph.selectAll('.dot')
.data(graphData)
// ...
graph.selectAll('.label')
.data(graphData)
// ...
And a demo
var url = "https://raw.githubusercontent.com/FreeCodeCamp/ProjectReferenceData/master/cyclist-data.json";
var m = {t: 20, r: 120, b: 30, l: 40},
width = 800 - m.l - m.r,
height = 700 - m.t - m.b;
var svg = d3.select("body").append("svg")
.attr("width", width + m.l + m.r)
.attr("height", height + m.t + m.b)
.append("g")
.attr("transform", "translate(" + m.l + "," + m.t + ")");
var div = d3.select('body')
.append('div')
.style({
'position':'absolute',
'text-align':'center',
'width':'240px',
'height':'2.5em',
'font':'1.5em sans-serif',
'color':'yellow',
'background':'black',
'border-radius':'8px',
'border':'solid 1px green',
'opacity':0
});
var colorScale = d3.scale.ordinal()
.range(["#FF0000", "#009933"]);
var xScale = d3.scale.linear()
.range([width, 0]);
var yScale = d3.scale.linear()
.range([0, height]);
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom")
.tickFormat(formatMinSec);
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left");
d3.json(url, callback);
function callback (error, data) {
if (error) throw error;
var bestTime = _.sortBy(data, 'Place')[0].Seconds;
var graphData = _.map(data, (d)=> ({
'secondsBehind': Math.abs(bestTime - d.Seconds),
'year': d.Year,
'nationality': d.Nationality,
'doping': d.Doping,
'dopingAllegations': d.Doping.length > 0 ? "Doping Allegations":"No Doping Allegations",
'name': d.Name,
'place': d.Place,
'time': d.Time
}) )
var timeRange = d3.extent(graphData, (d) => d.secondsBehind );
xScale.domain([timeRange[0]-15, timeRange[1]]);
var rankRange = d3.extent(graphData, (d) => d.place );
yScale.domain([rankRange[0], rankRange[1]+1]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.append("text")
.text("Minutes : Seconds Behind Fastest Time")
.attr({
'class': 'label',
'x': width,
'y': -6
})
.style("text-anchor", "end");
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.text("Ranking")
.attr({
'class': 'label',
"transform": "rotate(-90)",
"y": 6,
"dy": ".71em"
})
.style("text-anchor", "end");
var graph = svg.append("g");
graph.selectAll('.dot')
.data(graphData)
.enter()
.append('circle')
.attr('class', 'dot')
.attr('r', 5)
.attr('cx', (d)=> xScale(d.secondsBehind) )
.attr('cy', (d)=> yScale(d.place) )
.style('fill', (d)=> colorScale(d.dopingAllegations) );
graph.selectAll('.label')
.data(graphData)
.enter()
.append('text')
.attr('class', 'label')
.attr('x', (d)=> xScale(d.secondsBehind) + 10)
.attr('y', (d)=> yScale(d.place) + 4)
.text( (d)=> d.name );
// d3.selectAll('.dot')
// .on('mouseover', mouseover)
// .on('mouseout', mouseout);
var legend = svg.selectAll('.legend')
.data(colorScale.domain())
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function(d,i){return 'translate(0,' +i*20+')';});
legend.append('rect')
.attr('x', width)
.attr('y', 100)
.attr('width', 18)
.attr('height', 18)
.style('fill', colorScale);
legend.append('text')
.text((d)=> d)
.attr('x', width - 18)
.attr('y', 108)
.attr('dy', '.35em')
.style('text-anchor', 'end');
};
// function mouseout(){
// div.style('opacity', 1e-6);
// }
function formatMinSec(d){
if( d % 60 > 9){
return Math.floor(d/60) +':'+ d%60
} else {
return Math.floor(d/60) +':0'+ d%60
}
}
body {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.dot {
stroke: #000;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.6.1/lodash.min.js"></script>
Instead of this:
svg.selectAll('.dot') // creates the correct number of dots
.data(graphData)
.enter()
.append('circle')
.attr('class', 'dot')
.attr('r', 5)
.attr('cx', (d)=> xScale(d.secondsBehind) )
.attr('cy', (d)=> yScale(d.place) )
.style('fill', (d)=> colorScale(d.dopingAllegations) );
svg.selectAll('.label') // does not create the last two text elements
.data(graphData)
.enter()
.append('text')
.attr('class', 'label')
.attr('x', (d)=> xScale(d.secondsBehind) + 10)
.attr('y', (d)=> yScale(d.place) + 4)
.text( (d)=> d.name );
Do it like this:
var gs = svg.selectAll('.dot')
.data(graphData)
.enter();
gs.append('circle')
.attr('class', 'dot')
.attr('r', 5)
.attr('cx', (d)=> xScale(d.secondsBehind) )
.attr('cy', (d)=> yScale(d.place) )
.style('fill', (d)=> colorScale(d.dopingAllegations) );
gs
.append('text')
.attr('class', 'label')
.attr('x', (d)=> xScale(d.secondsBehind) + 10)
.attr('y', (d)=> yScale(d.place) + 4)
.text( (d)=> { return d.name; } );
working code here
Other option is that instead of
svg.selectAll('.label') // does not create the last two text elements
.data(graphData)
.enter()
.append('text')
.attr('class', 'label')
.attr('x', (d)=> xScale(d.secondsBehind) + 10)
.attr('y', (d)=> yScale(d.place) + 4)
.text( (d)=> d.name );
Do this:
svg.selectAll('.label')
.data(graphData, function(d) {
if (d) {
return d.place; //unique identifier of the data, otherwise Marco Pantani will come only once.
}
})
.enter()
.append('text')
.attr('class', 'label')
.attr('x', (d) => xScale(d.secondsBehind) + 10)
.attr('y', (d) => yScale(d.place) + 4)
.text((d) => d.name);
Working code here

Resources