D3 updates every path only when select() is running instead of selectAll() - d3.js

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(...)

Related

Bar chart unable to execute transition after button clicked

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');
})
});

Draggable interactive graphic element using D3

I have a challenging idea to build and couldn't think about a solution yet. The design request to have interactive/draggable graphics, as the one I send by the link below.
However, those graphics elements will be distributed in specific places on the page, with other elements around (Text, images, etc). The idea is to let the user "to play" with the graphics circles, just doing something 'cool and fun'. The user must be able to drag the circles from the graphics and change its visual all along the page.
The problem is: If I place this element in an specific place (inside a div, for example), if we drag the circles outside the 'canvas' area, the elements is no longer visible.
How could I place this canvas-div element in specific place and at the same time to allow the elements inside it to go the outside limited zone?
I thought about putting it in position relative or absolute with 100% height and width of the page, but it will be out of its place in responsive I guess, or pretty complicate to place always at a good place by just using % position. Any suggestion?
I'm using d3.js
Thanks!!
Heres the link: https://codepen.io/A8-XPs/pen/ePWRxZ?editors=0010
HTML
<svg width="500" height="350"></svg>
JS
var svg = d3.select("svg"),
margin = {top: 20, right: 20, bottom: 30, left: 50},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
let points = d3.range(1, 10).map(function(i) {
return [i * width / 10, 50 + Math.random() * (height - 100)];
});
var x = d3.scaleLinear()
.rangeRound([0, width]);
var y = d3.scaleLinear()
.rangeRound([height, 0]);
var xAxis = d3.axisBottom(x),
yAxis = d3.axisLeft(y);
var line = d3.line()
.x(function(d) { return x(d[0]); })
.y(function(d) { return y(d[1]); })
.curve(d3.curveCatmullRom.alpha(0.5))
let drag = d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended);
svg.append('rect')
.attr('class', 'zoom')
.attr('cursor', 'move')
.attr('fill', 'none')
.attr('pointer-events', 'all')
.attr('width', width)
.attr('height', height)
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
var focus = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
x.domain(d3.extent(points, function(d) { return d[0]; }));
y.domain(d3.extent(points, function(d) { return d[1]; }));
focus.append("path")
.datum(points)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("stroke-width", 1.5)
.attr("d", line);
focus.selectAll('circle')
.data(points)
.enter()
.append('circle')
.attr('r', 5.0)
.attr('cx', function(d) { return x(d[0]); })
.attr('cy', function(d) { return y(d[1]); })
.style('cursor', 'pointer')
.style('fill', 'steelblue');
focus.selectAll('circle')
.call(drag);
focus.append('g')
.attr('class', 'axis axis--x')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis);
focus.append('g')
.attr('class', 'axis axis--y')
.call(yAxis);
function dragstarted(d) {
d3.select(this).raise().classed('active', true);
}
function dragged(d) {
d[0] = x.invert(d3.event.x);
d[1] = y.invert(d3.event.y);
d3.select(this)
.attr('cx', x(d[0]))
.attr('cy', y(d[1]))
focus.select('path').attr('d', line);
}
function dragended(d) {
d3.select(this).classed('active', false);
}
PS: I got to solve the problem by just applying simple CSS to the SVG:
Overflow: visible;
Hopefully it will work in a real page scenario as well.

Line chart angles smoother using D3

I am trying to make the angles on this chart smoother, D3.js is being used and I already tried to apply a few ideas as solution, like adding .interpolate("basis") on the code, but for some reason the chart disappear when I do it.
Do you have any clue on what am I doing wrong? The dots are draggable and this is the intended behavior.
Here's a sample to the code: https://codepen.io/A8-XPs/pen/ePWRxZ?editors=1010
HTML:
<svg width="500" height="350"></svg>
JS:
var svg = d3.select("svg"),
margin = {top: 20, right: 20, bottom: 30, left: 50},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
let points = d3.range(1, 10).map(function(i) {
return [i * width / 10, 50 + Math.random() * (height - 100)];
});
var x = d3.scaleLinear()
.rangeRound([0, width]);
var y = d3.scaleLinear()
.rangeRound([height, 0]);
var xAxis = d3.axisBottom(x),
yAxis = d3.axisLeft(y);
var line = d3.line()
.x(function(d) { return x(d[0]); })
.y(function(d) { return y(d[1]); });
let drag = d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended);
svg.append('rect')
.attr('class', 'zoom')
.attr('cursor', 'move')
.attr('fill', 'none')
.attr('pointer-events', 'all')
.attr('width', width)
.attr('height', height)
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
var focus = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
x.domain(d3.extent(points, function(d) { return d[0]; }));
y.domain(d3.extent(points, function(d) { return d[1]; }));
focus.append("path")
.datum(points)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("stroke-width", 1.5)
.attr("d", line);
focus.selectAll('circle')
.data(points)
.enter()
.append('circle')
.attr('r', 5.0)
.attr('cx', function(d) { return x(d[0]); })
.attr('cy', function(d) { return y(d[1]); })
.style('cursor', 'pointer')
.style('fill', 'steelblue');
focus.selectAll('circle')
.call(drag);
focus.append('g')
.attr('class', 'axis axis--x')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis);
focus.append('g')
.attr('class', 'axis axis--y')
.call(yAxis);
function dragstarted(d) {
d3.select(this).raise().classed('active', true);
}
function dragged(d) {
d[0] = x.invert(d3.event.x);
d[1] = y.invert(d3.event.y);
d3.select(this)
.attr('cx', x(d[0]))
.attr('cy', y(d[1]))
focus.select('path').attr('d', line);
}
function dragended(d) {
d3.select(this).classed('active', false);
}
Thank you!
To get basic interpolation use
var line = d3.line()
.x(function(d) { return x(d[0]); })
.y(function(d) { return y(d[1]); })
.curve(d3.curveBasis);
or
.curve(d3.curveCatmullRom.alpha(0.5))

Double bar chart creation

I want to create a bar chart like this:
There are two chart bars one below the other, the first one grows upwards while the second one grows downwards.
They have different scales and data.
This is what I created:
var doublebarSvg1 = d3.select('#doublebar')
.append('svg')
.attr('class', 'doublebarSvg1')
.attr('width', 700)
.attr('height', 400);
var doublebarSvg2 = d3.select('#doublebar')
.append('svg')
.attr('class', 'doublebarSvg2')
.attr('width', 700)
.attr('height', 400);
var margin = {top: 0, right: 0, bottom: 0, left: 50};
var width = doublebarSvg1.attr('width') - margin.left - margin.right;
var height = doublebarSvg1.attr('height') - margin.top - margin.bottom;
var x = d3.scaleBand()
.rangeRound([0, width])
.padding(0.1)
.domain(years);
var y1 = d3.scaleLinear()
.rangeRound([height, 0])
.domain([0, 100]);
var y2 = d3.scaleSqrt()
.rangeRound([height, 0])
.domain([813, 0.1]); // max value 812.05 but domain is [0, 100000]
var doublebarSvgG1 = doublebarSvg1.append('g').attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
var doublebarSvgG2 = doublebarSvg2.append('g').attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
////////////////////////////////////////////////////////////////////////
// Tooltip.
////////////////////////////////////////////////////////////////////////
var svgTip = doublebarSvg1.append('svg').attr('id', 'tooltip');
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-5, 0])
.html(function(d) {
return '<div><span>Country:</span> <span style=\'color:white\'>' + d.country + '</span></div>' +
'<div><span>Perc:</span> <span style=\'color:white\'>' + d.perc + '%</span></div>' +
'<div><span>Rate:</span> <span style=\'color:white\'>' + d.rate + '%</span></div>';
});
svgTip.call(tip);
////////////////////////////////////////////////////////////////////////
// Draw a single double bar
////////////////////////////////////////////////////////////////////////
makeDoublebar1();
function makeDoublebar1() {
// define the axes
var xAxis = d3.axisBottom(x);
var yAxis1 = d3.axisLeft(y1);
// create x axis
doublebarSvgG1.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0, ' + height + ')')
.call(xAxis)
.selectAll('text')
.style('text-anchor', 'end')
.attr('dx', '-.8em')
.attr('dy', '.15em')
.attr('transform', 'rotate(-65)');
// create y axis
doublebarSvgG1.append('g')
.attr('class', 'y axis')
.call(yAxis1)
.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', 6)
.attr('dy', '.71em')
.style('text-anchor', 'end');
// create bar rect
doublebarSvgG1.selectAll('.bar')
.data(testData1) //.data(covFiltered)
.enter().append('rect')
.attr('fill', 'steelblue')
.attr('class', 'bar')
.attr('x', function(d) {
return x(d.year);
})
.attr('y', function(d) {
if(isNaN(d.perc)) {
d.perc = 0;
}
return y1(d.perc);
})
.attr('width', x.bandwidth())
.attr('height', function(d) {
if(isNaN(d.perc)) {
d.perc = 0;
}
return height - y1(d.perc);
})
.on('mouseover', function(d) {
d3.select(this).attr('fill', 'darkblue');
tip.show(d);
})
.on('mouseout', function(d) {
d3.select(this).attr('fill', 'steelblue');
tip.hide(d);
});
}
////////////////////////////////////////////////////////////////////////
// Draw a single double bar
////////////////////////////////////////////////////////////////////////
makeDoublebar2();
function makeDoublebar2() {
// define the axes
var xAxis = d3.axisBottom(x);
var yAxis2 = d3.axisLeft(y2);
// create x axis
doublebarSvgG2.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0, 0)')
.call(xAxis)
.selectAll('text')
.style('text-anchor', 'end')
.attr('dx', '-.8em')
.attr('dy', '.15em')
.attr('transform', 'rotate(-65)');
// create y axis
doublebarSvgG2.append('g')
.attr('class', 'y axis')
.call(yAxis2)
.append('text')
.style('text-anchor', 'end');
// create bar rect
doublebarSvgG2.selectAll('.bar')
.data(testData2)
.enter().append('rect')
.attr('fill', 'tomato')
.attr('class', 'bar')
.attr('x', function(d) { // left start point
return x(d.year);
})
.attr('y', function(d) { // top start point
if(isNaN(d.rate)) {
d.rate = 0;
}
return 0;
})
.attr('width', x.bandwidth())
.attr('height', function(d) {
if(isNaN(d.rate)) {
d.perc = 0;
}
return y2(d.rate);
})
.on('mouseover', function(d) {
d3.select(this).attr('fill', 'red');
tip.show(d);
})
.on('mouseout', function(d) {
d3.select(this).attr('fill', 'tomato');
tip.hide(d);
});
}
PLUNKER here.
There are some problem:
if I replace .axis {display: initial;} with .axis {display: none;}, all the axis disappear but I want the horizontal line between the two chart
I would like there to be only one tooltip, which when the user hovers over any bar, comes out with a tooltip that shows both perc and rate value.
And, more importantly, is this the smartest way to create a chart like that?
Regarding the axis, since you want to keep the horizontal line, just hide the ticks and the texts:
.x.axis text,.x.axis line {
opacity: 0;
}
The tooltip problem is a bit more complex. The issue is that you're binding different data arrays to each set of bars.
Because of that, the best idea is finding the desired object in each array when you hover over a given year and getting the respective properties:
var thisPerc = testData1.find(function(e){return e.year === d.year}).perc;
var thisRate = testData2.find(function(e){return e.year === d.year}).rate;
Then you use those properties for setting the tooltip's text.
Here is the updated Plunker: http://plnkr.co/edit/tfB4TpkETgzp5GF1677p?p=preview
Finally, for your last question ("And, more importantly, is this the smartest way to create a chart like that?"), the answer is no. There are a lot of things that can (and must) be changed here, but this involves a lot of refactoring and it's arguably off topic at Stack Overflow. However, this is an adequate question for Code Review. But please read their docs first for keeping your question on topic, asking there is not the same as asking here.

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