Using d3 to generate rect that fits text size - javascript

Lars Kotthof has a good explanation here of how to create SVG elements that correspond to size of text. However, I'm looking to do this dynamically with data pulled from JSON or CSV.
JS Fiddle here.
svg.selectAll('rect')
.data(states.features)
.enter()
.append('rect')
.attrs({
x: function(d) { return path.centroid(d)[0] - 50; },
y: function(d) { return path.centroid(d)[1] - 13; },
'text-anchor': 'middle',
'width': 100,
'height': 18,
'fill': 'rgba(232, 232, 232, 0.8)',
'opacity': 1,
'rx': 7,
'ry': 7
});
svg.selectAll('text')
.data(states.features)
.enter()
.append('text')
.text(function(d) { return d.properties.name; })
.attrs({
x: function(d) { return path.centroid(d)[0]; },
y: function(d) { return path.centroid(d)[1]; },
'text-anchor': 'middle',
'font-size': '7pt',
'fill': 'rgb(25,25,25)',
'opacity': 1
});
The concept I'm not grasping is how I can write a function, similar to Lars's, that creates both the <rect> and the <text> and uses the dimensions of the text to determine the dimensions of the rectangle.

Here's a solution and associated JS Fiddle. Basically what I did is assigned corresponding IDs to each rect and text, and then after the texts were generated, went adjusted the rect size based on the text. Additionally, the x position of the text had to be adjusted accordingly as well.
svg.selectAll('rect')
.data(states.features)
.enter()
.append('rect')
.attrs({
y: function(d) { return path.centroid(d)[1] - 13; },
'text-anchor': 'middle',
'width': 100,
'height': 18,
'fill': 'rgba(232, 232, 232, 0.8)',
'opacity': 1,
'rx': 7,
'ry': 7
});
svg.selectAll('text')
.data(states.features)
.enter()
.append('text')
.text(function(d) { return d.properties.name; })
.attrs({
x: function(d) { return path.centroid(d)[0]; },
y: function(d) { return path.centroid(d)[1]; },
'text-anchor': 'middle',
'font-size': '7pt',
'fill': 'rgb(25,25,25)',
'opacity': 1,
id: function(d) { return 'text' + d.id }
});
svg.selectAll('rect')
.attr('width', function(d) { return document.getElementById('text'+d.id).getBBox().width; })
.attr('x', function(d) { return path.centroid(d)[0] - document.getElementById('text'+d.id).getBBox().width / 2; });

Related

D3 v4 chart with Bi directional chart

How to create this chart with D3? Any help will help full tried with High charts but not help full much
click event is not working on this drill down event is added but it won't work on click on bars or y-axis labels.
const data = [
{ "name": 'IT', "value": 20, "negativeValue": -80 },
{ "name": 'Capital Invest', "value": 30, "negativeValue": -70 },
{ "name": 'Infrastructure', "value": 40, "negativeValue": -60 }
];
Highcharts.setOptions({
lang: {
drillUpText: `◁ Back to {series.description}`,
},
});
Highcharts.chart({
chart: {
type: 'bar',
renderTo: 'alignmentChart',
height: 530,
marginRight: 20,
backgroundColor: 'transparent',
events: {
drilldown(e: any) {
if (e.seriesOptions.fits) {
linesPositive = e.seriesOptions.line;
} else {
lineNegative = e.seriesOptions.line;
}
labels = !!e.seriesOptions && e.seriesOptions.data.map(a => a.name);
},
drillup(e: any) {
if (e.seriesOptions.fits) {
linesPositive = e.seriesOptions.line;
} else {
lineNegative = e.seriesOptions.line;
}
labels = !!e.seriesOptions && e.seriesOptions.data.map(a => a.name);
},
},
},
title: {
text: '',
},
colors: ['#f7a704', '#458dde'],
// tooltip: this.getTooltip(this),
xAxis: {
reversed: false,
tickPositions: Array.from(Array(this.multi.positive.length).keys()),
labels: {
useHTML: true,
formatter() {
return `<span title="${labels[this.value]}">${labels[this.value]}</span>`;
},
style: {
color: '#000000',
},
step: 1,
},
lineWidth: 0,
tickWidth: 0,
},
yAxis: {
title: {
text: null,
},
max: 100,
min: -100,
plotLines: [{
color: '#e5e5e5',
value: 0,
width: 1,
zIndex: 20,
}],
lineWidth: 1,
gridLineWidth: 0,
tickWidth: 1,
// offset: 100,
labels: {
y: 30,
align: 'center',
},
},
plotOptions: {
bar: {
pointWidth: 12,
},
series: {
stacking: 'normal',
dataLabels: {
enabled: true,
color: '#6b6b6b',
style: {
fontSize: '12px',
fontFamily: 'Proxima Nova'
},
formatter() {
return '';
},
inside: false,
},
},
},
series: [{
name: 'Fits Role',
description: 'Subfunctions',
data: this.multi.positive,
type: undefined
}, {
name: 'Not Fit Role',
description: 'Subfunctions',
data: this.multi.negative,
type: undefined
}],
drilldown: {
allowPointDrilldown: false,
activeAxisLabelStyle: {
fontSize: '12px',
fontWeight: 'bold',
color: '#007bc7',
textDecoration: 'none',
},
series: this.multi.drilldowns,
},
credits: {
enabled: false,
},
legend: {
enabled: false,
},
exporting: {
enabled: false,
},
});
I had to make only very few changes compared to the answer I shared. As I said in my comment, I create one g node per item, and draw two rects for every one.
Then I update the rects to have the same datum shape ({ name: string, value: number }), regardless of whether it's positive or negative. That allows me to do exactly the same things for both types.
// Now, the data can also be negative
const data = [{
"name": 'IT',
"value": 20,
"negativeValue": -80
}, {
"name": 'Capital Invest',
"value": 30,
"negativeValue": -70
}, {
"name": 'Infrastructure',
"value": 40,
"negativeValue": -60
}];
const width = 600,
height = 300,
margin = {
top: 20,
left: 100,
right: 40,
bottom: 40
};
// Now, we don't use 0 as a minimum, but get it from the data using d3.extent
const x = d3.scaleLinear()
.domain([-100, 100])
.range([0, width]);
const y = d3.scaleBand()
.domain(data.map(d => d.name))
.range([height, 0])
.padding(0.1);
const svg = d3.select('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom);
const g = svg
.append('g')
.attr('transform', `translate(${margin.left} ${margin.right})`);
// One group per data entry, each holding two bars
const barGroups = g
.selectAll('.barGroup')
.data(data);
barGroups.exit().remove();
const newBarGroups = barGroups.enter()
.append('g')
.attr('class', 'barGroup');
// Append one bar for the positive value, and one for the negative one
newBarGroups
.append('rect')
.attr('class', 'positive')
.attr('fill', 'darkgreen');
newBarGroups
.append('rect')
.attr('class', 'negative')
.attr('fill', 'darkred');
const positiveBars = newBarGroups
.merge(barGroups)
.select('.positive')
.datum(d => ({
name: d.name,
value: d.value
}));
const negativeBars = newBarGroups
.merge(barGroups)
.select('.negative')
.datum(d => ({
name: d.name,
value: d.negativeValue
}));
newBarGroups.selectAll('rect')
// If a bar is positive it starts at x = 0, and has positive width
// If a bar is negative it starts at x < 0 and ends at x = 0
.attr('x', d => d.value > 0 ? x(0) : x(d.value))
.attr('y', d => y(d.name))
// If the bar is positive it ends at x = v, but that means it's x(v) - x(0) wide
// If the bar is negative it ends at x = 0, but that means it's x(0) - x(v) wide
.attr('width', d => d.value > 0 ? x(d.value) - x(0) : x(0) - x(d.value))
.attr('height', y.bandwidth())
// Let's color the bar based on whether the value is positive or negative
g.append('g')
.classed('x-axis', true)
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom(x))
g.append('g')
.classed('y-axis', true)
.attr('transform', `translate(${x(0)}, 0)`)
.call(d3.axisLeft(y))
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg></svg>
Alternatively, you can do it without merging the selections like so:
// Now, the data can also be negative
const data = [{
"name": 'IT',
"value": 20,
"negativeValue": -80
}, {
"name": 'Capital Invest',
"value": 30,
"negativeValue": -70
}, {
"name": 'Infrastructure',
"value": 40,
"negativeValue": -60
}];
const width = 600,
height = 300,
margin = {
top: 20,
left: 100,
right: 40,
bottom: 40
};
// Now, we don't use 0 as a minimum, but get it from the data using d3.extent
const x = d3.scaleLinear()
.domain([-100, 100])
.range([0, width]);
const y = d3.scaleBand()
.domain(data.map(d => d.name))
.range([height, 0])
.padding(0.1);
const svg = d3.select('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom);
const g = svg
.append('g')
.attr('transform', `translate(${margin.left} ${margin.right})`);
// One group per data entry, each holding two bars
const positiveBars = g
.selectAll('.positive')
.data(data);
positiveBars.exit().remove();
positiveBars.enter()
.append('rect')
.attr('class', 'positive')
.attr('fill', 'darkgreen')
.merge(positiveBars)
.attr('x', x(0))
.attr('y', d => y(d.name))
// The bar is positive. It ends at x = v, but that means it's x(v) - x(0) wide
.attr('width', d => x(d.value) - x(0))
.attr('height', y.bandwidth());
const negativeBars = g
.selectAll('.negative')
.data(data);
negativeBars.exit().remove();
negativeBars.enter()
.append('rect')
.attr('class', 'negative')
.attr('fill', 'darkred')
.merge(negativeBars)
.attr('x', d => x(d.negativeValue))
.attr('y', d => y(d.name))
// The bar is negative. It ends at x = 0, but that means it's x(0) - x(v) wide
.attr('width', d => x(0) - x(d.negativeValue))
.attr('height', y.bandwidth());
g.append('g')
.classed('x-axis', true)
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom(x))
g.append('g')
.classed('y-axis', true)
.attr('transform', `translate(${x(0)}, 0)`)
.call(d3.axisLeft(y))
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg></svg>

D3 transition from stacked bar to bar chart only works the first time

I am trying to create two charts, a stacked bar and a bar, each displaying a different dataset. But I would like to transition from the stacked bar chart to the bar chart when I click on a button and vice versa. The code I put together works only the first time, then after that when I want to transition back to the stacked bar chart from the bar chart all the bars stack on top of each other in the form of a single bar. Can someone point me in the right direction of how to transition back from bar to stacked bar? Any help would be appreciated. (I haven't really messed with the axes yet so it is normal that they are not changing).
Here is a link to how it currently looks:https://jhjanicki.github.io/stackbartobar/
Below is my code:
var value = 'stack';
var data = [{
name: "Shihuahuaco",
value: 1067,
china: 772
}, {
name: "Cachimbo",
value: 283,
china: 1
}, {
name: "Estoraque",
value: 204,
china: 150
}, {
name: "Cumala",
value: 154,
china: 0
}, {
name: "Ishpingo",
value: 108,
china: 3
}, {
name: "Huayruro",
value: 108,
china: 1
}, {
name: "Tornillo",
value: 61,
china: 4
}, {
name: "Congona",
value: 54,
china: 0
}, {
name: "Capirona",
value: 37,
china: 5
}, {
name: "Tahuari",
value: 33,
china: 14
}, {
name: "Marupa",
value: 33,
china: 1
}, {
name: "Quinilla",
value: 28,
china: 4
}, {
name: "Azucar huayo",
value: 22,
china: 15
}, {
name: "Protium sp.",
value: 19,
china: 0
}, {
name: "Nogal",
value: 15,
china: 6
}, {
name: "Ana Caspi",
value: 14,
china: 2
}, {
name: "Cedro",
value: 14,
china: 0
}, {
name: "Carapa guianensis",
value: 12,
china: 0
}, {
name: "Leche caspi",
value: 12,
china: 0
}, {
name: "Andiroba",
value: 11,
china: 0
}, {
name: "Copaiba",
value: 7,
china: 4
}, {
name: "Palo baston",
value: 6,
china: 0
}, {
name: "Moena",
value: 5,
china: 0
}, {
name: "Almendro",
value: 5,
china: 0
}, {
name: "Chancaquero",
value: 4,
china: 0
}, {
name: "Caimitillo",
value: 3,
china: 1
}, {
name: "Nogal amarillo",
value: 3,
china: 0
}, {
name: "Couma macrocarpa",
value: 3,
china: 0
}, {
name: "Tulpay",
value: 3,
china: 0
}, {
name: "Carapa",
value: 3,
china: 0
}, {
name: "Dacryodes olivifera",
value: 2,
china: 0
}, {
name: "Capinuri",
value: 2,
china: 2
}, {
name: "Brosimum alicastrum",
value: 2,
china: 0
}, {
name: "Paramachaerium ormosioide",
value: 2,
china: 0
}, {
name: "Brosimum sp.",
value: 2,
china: 0
}, {
name: "Manchinga",
value: 2,
china: 0
}];
// data for stacked bar
var points = [{
'lon': 105.3,
'lat': 33.5,
'name': 'China',
'GTF': 1024,
"ID": "CHN"
},
{
'lon': -70.9,
'lat': 18.8,
'name': 'Dominican Republic',
'GTF': 470,
"ID": "DOM"
},
{
'lon': -101,
'lat': 38,
'name': 'USA',
'GTF': 248,
"ID": "USA"
},
{
'lon': -102.5,
'lat': 22.7,
'name': 'Mexico',
'GTF': 220,
"ID": "MEX"
},
{
'lon': 2.98,
'lat': 46,
'name': 'France',
'GTF': 85,
"ID": "FRA"
}
];
//data for bar
var margin = {
top: 20,
right: 30,
bottom: 150,
left: 60
},
widthB = 700 - margin.left - margin.right,
heightB = 500 - margin.top - margin.bottom;
var dataIntermediate = ['value', 'china'].map(function(key, i) {
return data.map(function(d, j) {
return {
x: d['name'],
y: d[key]
};
})
})
var dataStackLayout = d3.layout.stack()(dataIntermediate);
var svg = d3.select("#chart").append("svg")
.attr("width", widthB + margin.left + margin.right)
.attr("height", heightB + margin.top + margin.bottom)
var gBar = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.attr('class', 'gBar');
var x = d3.scale.ordinal()
.rangeRoundBands([0, widthB], .2);
var y = d3.scale.linear()
.range([heightB, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(8)
.tickFormat(function(d) {
return y.tickFormat(4, d3.format(",d"))(d)
});
data.forEach(function(d) {
d.value = +d.value; // coerce to number
d.china = +d.china;
});
x.domain(dataStackLayout[0].map(function(d) {
return d.x;
}));
y.domain([0, d3.max(dataStackLayout[dataStackLayout.length - 1],
function(d) {
return d.y0 + d.y;
})]).nice();
var layer;
var bars;
//axes
gBar.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (heightB + 10) + ")")
.call(xAxis)
.selectAll("text")
.style('font-size', '14px')
.style('font-family', 'Alegreya')
.style("text-anchor", "end")
.attr("dx", "-0.40em")
.attr("dy", ".10em")
.attr("transform", function(d) {
return "rotate(-65)"
});
gBar.append("g")
.attr("class", "y axis")
.call(yAxis)
.selectAll("text")
.style('font-size', '16px')
.style('font-family', 'Alegreya');
function draw() {
if (value == 'stack') {
layer = gBar.selectAll(".stack")
.data(dataStackLayout);
layer.exit()
.transition()
.delay(function(d, i) {
return 30 * i;
})
.duration(1500)
.style("fill", "none")
.remove();
layer.enter().append("g")
.attr("class", "stack")
.style("fill", function(d, i) {
return i == 0 ? '#b4d5c3' : '#ecaeb3';
});
bars = layer.selectAll("rect")
.data(function(d) {
return d;
});
// the "EXIT" set:
bars.exit()
.transition()
.delay(function(d, i) {
return 30 * i;
})
.duration(1500)
.attr("y", y(0))
.attr("height", heightB - y(0))
.style('fill-opacity', 1e-6)
.remove();
// the "ENTER" set:
bars.enter().append("rect")
.transition()
.delay(function(d, i) {
return 30 * i;
})
.duration(3000)
.attr("x", function(d) {
return x(d.x);
})
.attr("y", function(d) {
return y(d.y + d.y0);
})
.attr("height", function(d) {
return y(d.y0) - y(d.y + d.y0);
})
.attr("width", x.rangeBand());
// the "UPDATE" set:
bars.transition().delay(function(d, i) {
return 30 * i;
}).duration(1500).attr("x", function(d) {
return x(d.x);
})
.attr("width", x.rangeBand()) // constant, so no callback function(d) here
.attr("y", function(d) {
return y(d.y + d.y0);
})
.attr("height", function(d) {
return y(d.y0) - y(d.y + d.y0);
});
} else { // draw bar
x.domain(points.map(function(d) {
return d.name;
}));
y.domain([0, 1024]).nice();
bars = layer.selectAll("rect")
.data(points);
// the "EXIT" set:
bars.exit()
.transition()
.delay(function(d, i) {
return 30 * i;
})
.duration(1500)
.attr("y", y(0))
.attr("height", heightB - y(0))
.style('fill-opacity', 1e-6)
.remove();
// the "ENTER" set:
bars.enter().append("rect")
.transition()
.delay(function(d, i) {
return 30 * i;
})
.duration(3000)
.attr("x", function(d) {
return x(d.name);
})
.attr("y", function(d) {
return y(d.GTF);
})
.attr("height", function(d) {
return heightB - y(d.GTF);;
})
.attr("width", x.rangeBand());
// the "UPDATE" set:
bars.transition().delay(function(d, i) {
return 30 * i;
}).duration(1500).attr("x", function(d) {
return x(d.name);
})
.attr("width", x.rangeBand()) // constant, so no callback function(d) here
.attr("y", function(d) {
return y(d.GTF);
})
.attr("height", function(d) {
return heightB - y(d.GTF);
});
}
}
window.onload = draw();
$("#click").on('click', function() {
if (value == 'stack') {
value = 'bar';
} else {
value = 'stack';
}
draw();
});
body {
font-family: 'Alegreya', serif;
}
.axis text {
font: 10px sans-serif;
}
.axis path {
fill: none;
stroke: #000;
stroke-width: 0px;
shape-rendering: crispEdges;
}
.axis line {
fill: none;
stroke: #000;
stroke-width: 0.5px;
shape-rendering: crispEdges;
}
.x.axis path {
display: none;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="https://d3js.org/d3.v3.js"></script>
<div id="chart"></div>
<p id="click"> click here to change </p>
The problem in your code is that you're changing the scales' domain for the bar chart, but you're not changing them back for the stacked bar chart.
Therefore, you should put this in the draw() section (conditional statement) for the stacked bar:
x.domain(dataStackLayout[0].map(function(d) {
return d.x;
}));
y.domain([0, d3.max(dataStackLayout[dataStackLayout.length - 1],
function(d) {
return d.y0 + d.y;
})]).nice();
Here is your code with that change (I also put a call for the x axis):
var value = 'stack';
var data = [{
name: "Shihuahuaco",
value: 1067,
china: 772
}, {
name: "Cachimbo",
value: 283,
china: 1
}, {
name: "Estoraque",
value: 204,
china: 150
}, {
name: "Cumala",
value: 154,
china: 0
}, {
name: "Ishpingo",
value: 108,
china: 3
}, {
name: "Huayruro",
value: 108,
china: 1
}, {
name: "Tornillo",
value: 61,
china: 4
}, {
name: "Congona",
value: 54,
china: 0
}, {
name: "Capirona",
value: 37,
china: 5
}, {
name: "Tahuari",
value: 33,
china: 14
}, {
name: "Marupa",
value: 33,
china: 1
}, {
name: "Quinilla",
value: 28,
china: 4
}, {
name: "Azucar huayo",
value: 22,
china: 15
}, {
name: "Protium sp.",
value: 19,
china: 0
}, {
name: "Nogal",
value: 15,
china: 6
}, {
name: "Ana Caspi",
value: 14,
china: 2
}, {
name: "Cedro",
value: 14,
china: 0
}, {
name: "Carapa guianensis",
value: 12,
china: 0
}, {
name: "Leche caspi",
value: 12,
china: 0
}, {
name: "Andiroba",
value: 11,
china: 0
}, {
name: "Copaiba",
value: 7,
china: 4
}, {
name: "Palo baston",
value: 6,
china: 0
}, {
name: "Moena",
value: 5,
china: 0
}, {
name: "Almendro",
value: 5,
china: 0
}, {
name: "Chancaquero",
value: 4,
china: 0
}, {
name: "Caimitillo",
value: 3,
china: 1
}, {
name: "Nogal amarillo",
value: 3,
china: 0
}, {
name: "Couma macrocarpa",
value: 3,
china: 0
}, {
name: "Tulpay",
value: 3,
china: 0
}, {
name: "Carapa",
value: 3,
china: 0
}, {
name: "Dacryodes olivifera",
value: 2,
china: 0
}, {
name: "Capinuri",
value: 2,
china: 2
}, {
name: "Brosimum alicastrum",
value: 2,
china: 0
}, {
name: "Paramachaerium ormosioide",
value: 2,
china: 0
}, {
name: "Brosimum sp.",
value: 2,
china: 0
}, {
name: "Manchinga",
value: 2,
china: 0
}];
var points = [{
'lon': 105.3,
'lat': 33.5,
'name': 'China',
'GTF': 1024,
"ID": "CHN"
}, {
'lon': -70.9,
'lat': 18.8,
'name': 'Dominican Republic',
'GTF': 470,
"ID": "DOM"
}, {
'lon': -101,
'lat': 38,
'name': 'USA',
'GTF': 248,
"ID": "USA"
}, {
'lon': -102.5,
'lat': 22.7,
'name': 'Mexico',
'GTF': 220,
"ID": "MEX"
}, {
'lon': 2.98,
'lat': 46,
'name': 'France',
'GTF': 85,
"ID": "FRA"
}];
var margin = {
top: 20,
right: 30,
bottom: 150,
left: 60
},
widthB = 700 - margin.left - margin.right,
heightB = 500 - margin.top - margin.bottom;
var dataIntermediate = ['value', 'china'].map(function(key, i) {
return data.map(function(d, j) {
return {
x: d['name'],
y: d[key]
};
})
})
var dataStackLayout = d3.layout.stack()(dataIntermediate);
var svgBar = d3.select("#chart").append("svg")
.attr("width", widthB + margin.left + margin.right)
.attr("height", heightB + margin.top + margin.bottom)
var gBar = svgBar.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.attr('class', 'gBar');
var x = d3.scale.ordinal()
.rangeRoundBands([0, widthB], .2);
var y = d3.scale.linear()
.range([heightB, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(8)
.tickFormat(function(d) {
return y.tickFormat(4, d3.format(",d"))(d)
});
data.forEach(function(d) {
d.value = +d.value; // coerce to number
d.china = +d.china;
});
x.domain(dataStackLayout[0].map(function(d) {
return d.x;
}));
y.domain([0, d3.max(dataStackLayout[dataStackLayout.length - 1],
function(d) {
return d.y0 + d.y;
})]).nice();
var layer;
// this part
var bars;
var gX = gBar.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (heightB + 10) + ")");
gBar.append("g")
.attr("class", "y axis")
.call(yAxis)
.selectAll("text")
.style('font-size', '16px')
.style('font-family', 'Alegreya');
function draw() {
if (value == 'stack') {
x.domain(dataStackLayout[0].map(function(d) {
return d.x;
}));
y.domain([0, d3.max(dataStackLayout[dataStackLayout.length - 1],
function(d) {
return d.y0 + d.y;
})]).nice();
layer = gBar.selectAll(".stack")
.data(dataStackLayout);
layer.exit()
.transition()
.delay(function(d, i) {
return 30 * i;
})
.duration(1500)
.style("fill", "none")
.remove();
layer.enter().append("g")
.attr("class", "stack")
.style("fill", function(d, i) {
return i == 0 ? '#b4d5c3' : '#ecaeb3';
});
bars = layer.selectAll("rect")
.data(function(d) {
return d;
});
bars.exit()
.transition()
.delay(function(d, i) {
return 30 * i;
})
.duration(1500)
.attr("y", y(0))
.attr("height", heightB - y(0))
.style('fill-opacity', 1e-6)
.remove();
bars.enter().append("rect")
.transition()
.delay(function(d, i) {
return 30 * i;
})
.duration(3000)
.attr("x", function(d) {
return x(d.x);
})
.attr("y", function(d) {
return y(d.y + d.y0);
})
.attr("height", function(d) {
return y(d.y0) - y(d.y + d.y0);
})
.attr("width", x.rangeBand());
// the "UPDATE" set:
bars.transition().delay(function(d, i) {
return 30 * i;
}).duration(1500).attr("x", function(d) {
return x(d.x);
}) // (d) is one item from the data array, x is the scale object from above
.attr("width", x.rangeBand()) // constant, so no callback function(d) here
.attr("y", function(d) {
return y(d.y + d.y0);
})
.attr("height", function(d) {
return y(d.y0) - y(d.y + d.y0);
})
.style("fill-opacity", 1);
gX.call(xAxis)
.selectAll("text")
.style('font-size', '14px')
.style('font-family', 'Alegreya')
.style("text-anchor", "end")
.attr("dx", "-0.40em")
.attr("dy", ".10em")
.attr("transform", function(d) {
return "rotate(-65)"
});
} else {
x.domain(points.map(function(d) {
return d.name;
}));
y.domain([0, 1024]).nice();
// this part
bars = layer.selectAll("rect")
.data(points);
bars.exit()
.transition()
.delay(function(d, i) {
return 30 * i;
})
.duration(1500)
.attr("y", y(0))
.attr("height", heightB - y(0))
.style('fill-opacity', 1e-6)
.remove();
bars.enter().append("rect")
.transition()
.delay(function(d, i) {
return 30 * i;
})
.duration(3000)
.attr("x", function(d) {
return x(d.name);
})
.attr("y", function(d) {
return y(d.GTF);
})
.attr("height", function(d) {
return heightB - y(d.GTF);;
})
.attr("width", x.rangeBand());
// the "UPDATE" set:
bars.transition().delay(function(d, i) {
return 30 * i;
}).duration(1500).attr("x", function(d) {
return x(d.name);
}) // (d) is one item from the data array, x is the scale object from above
.attr("width", x.rangeBand()) // constant, so no callback function(d) here
.attr("y", function(d) {
return y(d.GTF);
})
.attr("height", function(d) {
return heightB - y(d.GTF);
});
gX.call(xAxis);
}
}
window.onload = draw();
$("#click").on('click', function() {
if (value == 'stack') {
value = 'bar';
} else {
value = 'stack';
}
draw();
});
body {
font-family: 'Alegreya', serif;
}
.axis text {
font: 10px sans-serif;
}
.axis path {
fill: none;
stroke: #000;
stroke-width: 0px;
shape-rendering: crispEdges;
}
.axis line {
fill: none;
stroke: #000;
stroke-width: 0.5px;
shape-rendering: crispEdges;
}
.x.axis path {
display: none;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="https://d3js.org/d3.v3.js"></script>
<div id="chart"></div>
<button id="click"> click here to change </button>
PS: Besides that, there is a lot of other minor changes you should do in your code, both for performance and design. As this is (now) a running code, I suggest you post further questions about how to improve it on Code Review, using the d3.js tag.

Hide or Remove vertical line in a specific point D3 chart

I drew a multi series line using d3 and added a vertical line when mouse hover over points. And I want to hide the vertical line and the text in 2018 point which has no data. I don't know how to select the line at the specific point which is translate(415,0), so I am not able to change the style to display:none;
Here is the code:
var data =[
{
'timescale': '2015',
'Not': 31,
'Nearly': 59,
'Standard': 81,
'Exceed':100
},
{
'timescale': '2016',
'Not': 28,
'Nearly': 55,
'Standard': 78,
'Exceed':100
},
{
'timescale': '2017',
'Not': 25,
'Nearly': 51,
'Standard': 75,
'Exceed':100
},
{
'timescale': '2018',
'Not': "null",
'Nearly': "null",
'Standard': "null",
'Exceed':"null"
},
{
'timescale': '2019',
'Not': 41,
'Nearly': 67,
'Standard': 90,
'Exceed':100
},
{
'timescale': '2020',
'Not': 36,
'Nearly': 61,
'Standard': 86,
'Exceed':100
},
{
'timescale': '2021',
'Not': 31,
'Nearly': 55,
'Standard': 82,
'Exceed':100
}
];
//d3.csv("test.csv", function(error,data){
console.log(data);
// set the dimensions and margins of the graph
var margin = { top: 20, right: 80, bottom: 30, left: 50 },
svg = d3.select('svg'),
width = +svg.attr('width') - margin.left - margin.right,
height = +svg.attr('height') - margin.top - margin.bottom;
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// set the ranges
var x = d3.scaleBand().rangeRound([0, width]).padding(1),
y = d3.scaleLinear().rangeRound([height, 0]),
z = d3.scaleOrdinal(["#BBB84B","#789952","#50DB51","#2D602A"]);
// define the line
var line = d3.line()
.defined(function (d) {
return !isNaN(d.total);
})
.x(function(d) { return x(d.timescale); })
.y(function(d) { return y(d.total); });
// scale the range of the data
z.domain(d3.keys(data[0]).filter(function(key) {
return key !== "timescale";
}));
var trends = z.domain().map(function(name) {
return {
name: name,
values: data.map(function(d) {
return {
timescale: d.timescale,
total: +d[name]
};
})
};
});
x.domain(data.map(function(d) { return d.timescale; }));
y.domain([0, d3.max(trends, function(c) {
return d3.max(c.values, function(v) {
return v.total;
});
})]);
// Draw the line
var trend = g.selectAll(".trend")
.data(trends)
.enter()
.append("g")
.attr("class", "trend");
trend.append("path")
.attr("class", "line")
.attr("d", function(d) { return line(d.values); })
.style("stroke", function(d) { return z(d.name); });
// Draw the empty value for every point
var points = g.selectAll('.points')
.data(trends)
.enter()
.append('g')
.attr('class', 'points')
.append('text');
// Draw the circle
trend
.style("fill", "#FFF")
.style("stroke", function(d) { return z(d.name); })
.selectAll("circle.line")
.data(function(d){return d.values} )
.enter()
.append("circle")
.filter(function(d) { return d.timescale !== "2018" })
.attr("r", 5)
.style("stroke-width", 3)
.attr("cx", function(d) { return x(d.timescale); })
.attr("cy", function(d) { return y(d.total); });
// Draw the axis
g.append("g")
.attr("class", "axis axis-x")
.attr("transform", "translate(0, " + height + ")")
.call(d3.axisBottom(x));
g.append("g")
.attr("class", "axis axis-y")
.call(d3.axisLeft(y).ticks(6));
var focus = g.append('g')
.attr('class','focus')
.style('display', 'none');
focus.append('line')
.attr('class', 'x-hover-line hover-line')
.attr('y1' , 0)
.attr('y2', height)
.style('stroke',"black");
svg.append('rect')
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.attr("class", "overlay")
.attr("width", width)
.attr("height", height)
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.on("mousemove", mousemove);
// var timeScales = data.map(function(name) { return x(name.timescale); });
// console.log(timeScales);
var timeScales = [106,209,312,415,518,621,724];
// d3.select('.focus')
// .attr("x1",415)
// .attr("y1",0)
// .attr("x2",415)
// .attr("y2",height)
// .style("display","none");
function mouseover() {
focus.style("display", null);
d3.selectAll('.points text').style("display", null);
}
function mouseout() {
focus.style("display", "none");
d3.selectAll('.points text').style("display", "none");
}
function mousemove() {
var i = d3.bisect(timeScales, d3.mouse(this)[0], 1);
var di = data[i-1];
focus.attr("transform", "translate(" + x(di.timescale) + ",0)");
d3.selectAll('.points text')
.attr('x', function(d) { return x(di.timescale) + 5; })
.attr('y', function(d) { return y(d.values[i-1].total)-5; })
.text(function(d) { return d.values[i-1].total; })
.style('fill', function(d) { return z(d.name); });
}
body {
font-family: 'Proxima Nova', Georgia, sans-serif;
}
.line {
fill: none;
stroke-width: 3px;
}
.overlay {
fill: none;
pointer-events: all;
}
.hover-line {
stroke-width: 2px;
stroke-dasharray: 3,3;
}
<svg width="960" height="500"></svg>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js'></script>
In the mousemove function only perform the transformation if timescale is not 2018:
function mousemove() {
var i = d3.bisect(timeScales, d3.mouse(this)[0], 1);
var di = data[i-1];
if (di.timescale !== '2018') {
focus.attr("transform", "translate(" + x(di.timescale) + ",0)");
d3.selectAll('.points text')
.attr('x', function(d) { return x(di.timescale) + 5; })
.attr('y', function(d) { return y(d.values[i-1].total)-5; })
.text(function(d) { return d.values[i-1].total; })
.style('fill', function(d) { return z(d.name); });
}
}
you have using bisection as a tip
so it will draw only when it selected, lets change that so it will not draw the 2018 when mouseover it
var data =[
{
'timescale': '2015',
'Not': 31,
'Nearly': 59,
'Standard': 81,
'Exceed':100
},
{
'timescale': '2016',
'Not': 28,
'Nearly': 55,
'Standard': 78,
'Exceed':100
},
{
'timescale': '2017',
'Not': 25,
'Nearly': 51,
'Standard': 75,
'Exceed':100
},
{
'timescale': '2018',
'Not': "null",
'Nearly': "null",
'Standard': "null",
'Exceed':"null"
},
{
'timescale': '2019',
'Not': 41,
'Nearly': 67,
'Standard': 90,
'Exceed':100
},
{
'timescale': '2020',
'Not': 36,
'Nearly': 61,
'Standard': 86,
'Exceed':100
},
{
'timescale': '2021',
'Not': 31,
'Nearly': 55,
'Standard': 82,
'Exceed':100
}
];
//d3.csv("test.csv", function(error,data){
// console.log(data);
// set the dimensions and margins of the graph
var margin = { top: 20, right: 80, bottom: 30, left: 50 },
svg = d3.select('svg'),
width = +svg.attr('width') - margin.left - margin.right,
height = +svg.attr('height') - margin.top - margin.bottom;
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// set the ranges
var x = d3.scaleBand().rangeRound([0, width]).padding(1),
y = d3.scaleLinear().rangeRound([height, 0]),
z = d3.scaleOrdinal(["#BBB84B","#789952","#50DB51","#2D602A"]);
// define the line
var line = d3.line()
.defined(function (d) {
return !isNaN(d.total);
})
.x(function(d) { return x(d.timescale); })
.y(function(d) { return y(d.total); });
// scale the range of the data
z.domain(d3.keys(data[0]).filter(function(key) {
return key !== "timescale";
}));
var trends = z.domain().map(function(name) {
return {
name: name,
values: data.map(function(d) {
return {
timescale: d.timescale,
total: +d[name]
};
})
};
});
x.domain(data.map(function(d) { return d.timescale; }));
y.domain([0, d3.max(trends, function(c) {
return d3.max(c.values, function(v) {
return v.total;
});
})]);
// Draw the line
var trend = g.selectAll(".trend")
.data(trends)
.enter()
.append("g")
.attr("class", "trend");
trend.append("path")
.attr("class", "line")
.attr("d", function(d) { return line(d.values); })
.style("stroke", function(d) { return z(d.name); });
// Draw the empty value for every point
var points = g.selectAll('.points')
.data(trends)
.enter()
.append('g')
.attr('class', 'points')
.append('text');
// Draw the circle
trend
.style("fill", "#FFF")
.style("stroke", function(d) { return z(d.name); })
.selectAll("circle.line")
.data(function(d){return d.values} )
.enter()
.append("circle")
.filter(function(d) { return d.timescale !== "2018" })
.attr("r", 5)
.style("stroke-width", 3)
.attr("cx", function(d) { return x(d.timescale); })
.attr("cy", function(d) { return y(d.total); });
// Draw the axis
g.append("g")
.attr("class", "axis axis-x")
.attr("transform", "translate(0, " + height + ")")
.call(d3.axisBottom(x));
g.append("g")
.attr("class", "axis axis-y")
.call(d3.axisLeft(y).ticks(6));
var focus = g.append('g')
.attr('class','focus')
.style('display', 'none');
focus.append('line')
.attr('class', 'x-hover-line hover-line')
.attr('y1' , 0)
.attr('y2', height)
.style('stroke',"black");
svg.append('rect')
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.attr("class", "overlay")
.attr("width", width)
.attr("height", height)
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.on("mousemove", mousemove);
// var timeScales = data.map(function(name) { return x(name.timescale); });
// console.log(timeScales);
var timeScales = [106,209,312,415,518,621,724];
// d3.select('.focus')
// .attr("x1",415)
// .attr("y1",0)
// .attr("x2",415)
// .attr("y2",height)
// .style("display","none");
function mouseover() {
focus.style("display", null);
d3.selectAll('.points text').style("display", null);
}
function mouseout() {
focus.style("display", "none");
d3.selectAll('.points text').style("display", "none");
}
function mousemove() {
var i = d3.bisect(timeScales, d3.mouse(this)[0], 1);
var di = data[i-1];
console.log(di)
if (di.timescale =="2018"){
focus.style('display','none')
d3.selectAll('.points text').style("display", "none");
}else{
focus.style('display','block')
d3.selectAll('.points text').style("display", "block");
focus.attr("transform", "translate(" + x(di.timescale) + ",0)");}
d3.selectAll('.points text')
.attr('x', function(d) { return x(di.timescale) + 5; })
.attr('y', function(d) { return y(d.values[i-1].total)-5; })
.text(function(d) { return d.values[i-1].total; })
.style('fill', function(d) { return z(d.name); });
}
body {
font-family: 'Proxima Nova', Georgia, sans-serif;
}
.line {
fill: none;
stroke-width: 3px;
}
.overlay {
fill: none;
pointer-events: all;
}
.hover-line {
stroke-width: 2px;
stroke-dasharray: 3,3;
}
<svg width="960" height="500"></svg>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js'></script>

D3: Trouble updating selection

I'm having issues with my update() function. Here, within svg.append('rect') I have .on('click') where I simply change the data, then run update().
Why doesn't this work? How do I make it work?
var width = 640,
height = 480;
var graphNodes = [
{ id: 0, x: 39, y: 343, r: 15 },
{ id: 1, x: 425, y: 38, r: 15 },
{ id: 2, x: 183, y: 417, r: 15 },
{ id: 3, x: 564, y: 31, r: 15 },
{ id: 4, x: 553, y: 351, r: 15 },
{ id: 5, x: 454, y: 298, r: 15 },
{ id: 6, x: 493, y: 123, r: 15 },
{ id: 7, x: 471, y: 427, r: 15 },
{ id: 8, x: 142, y: 154, r: 15 }
];
var svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height);
svg.append('rect')
.attr('class', 'graph')
.attr('width', width)
.attr('height', height)
.attr('fill', 'lightblue')
.attr('opacity', 0.3)
.on('click', function(){
graphNodes[8].id = 'hey there'; // <----- Why doesn't this happen?
update();
});
var nodeGroup = svg.selectAll('.nodes')
.data(graphNodes, function(d){ return d.id; })
.enter().append('g')
.attr('class', 'node');
nodeGroup.append('circle')
.attr('cx', function(d) { return d.x })
.attr('cy', function(d) { return d.y })
.attr("r", function(d){ return d.r; })
.attr("fill", "gray");
nodeGroup.append('text')
.attr("dx", function(d){ return d.x + 20; })
.attr("dy", function(d){ return d.y + 5; })
.text(function(d) { return d.id });
function update() {
if(nodeGroup){
// Update nodes
var node = nodeGroup.data(graphNodes, function(d){ return d.id; }),
nodeEnter = node.enter().append('g')
.attr('class', 'node');
nodeEnter.append('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', function(d){ return d.r; })
.attr('fill', 'gray');
nodeEnter.append('text')
.attr("dx", function(d){ return d.x + 20; })
.attr("dy", function(d){ return d.y + 5; })
.text(function(d) { return d.id });
nodeGroup = nodeEnter.merge(node);
node.exit().remove();
}
}
Here's a fiddle
Typed this as a comment in the message you left me, but here it is as an answer.
You need to separate out the things you do on enter, on update and on exit. On enter you want to just append and set any attributes that never change. On update you want to add/change the text and add/change the radius. On exit you remove. Here I've properly handled the enter, update, exit paradigm:
// bind the data
var node = nodeGroup.data(graphNodes, function(d){ return d.id; }),
// this is the enter selection
nodeEnter = node.enter().append('g')
.attr('class', 'node');
// append to enter selection
// append and set color, we never change color
nodeEnter.append('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('fill', 'gray');
// again entering, append text and set position
nodeEnter.append('text')
.attr("dx", function(d){ return d.x + 20; })
.attr("dy", function(d){ return d.y + 5; });
// nodeGroup is the enter + update selection
nodeGroup = nodeEnter.merge(node);
// change the things we want to change on every update
nodeGroup.select("text")
.text(function(d) { return d.text ? d.text : d.id });
nodeGroup.select("circle")
.attr('r', function(d){ return d.r; })
// exit, just remove
node.exit().remove();
Running code:
var width = 640,
height = 480;
var graphNodes = [
{ id: 0, x: 39, y: 343, r: 15 },
{ id: 1, x: 425, y: 38, r: 15 },
{ id: 2, x: 183, y: 417, r: 15 },
{ id: 3, x: 564, y: 31, r: 15 },
{ id: 4, x: 553, y: 351, r: 15 },
{ id: 5, x: 454, y: 298, r: 15 },
{ id: 6, x: 493, y: 123, r: 15 },
{ id: 7, x: 471, y: 427, r: 15 },
{ id: 8, x: 142, y: 154, r: 15 }
];
var svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height);
svg.append('rect')
.attr('class', 'graph')
.attr('width', width)
.attr('height', height)
.attr('fill', 'lightblue')
.attr('opacity', 0.3)
.on('click', function(){
/*graphNodes.push({
x: d3.mouse(this)[0],
y: d3.mouse(this)[1],
id: graphNodes.length,
r: 15
});*/
graphNodes.splice(2, 1);
graphNodes[Math.floor(Math.random() * graphNodes.length)].text = "Tomato!";
graphNodes[Math.floor(Math.random() * graphNodes.length)].r = Math.random() * 30;
update();
});
var nodeGroup = svg.selectAll('.nodes')
.data(graphNodes, function(d){ return d.id; })
.enter().append('g')
.attr('class', 'node');
nodeGroup.append('circle')
.attr('cx', function(d) { return d.x })
.attr('cy', function(d) { return d.y })
.attr("r", function(d){ return d.r; })
.attr("fill", "gray");
nodeGroup.append('text')
.attr("dx", function(d){ return d.x + 20; })
.attr("dy", function(d){ return d.y + 5; })
.text(function(d) { return d.id });
function update() {
if(nodeGroup){
// Update nodes
var node = nodeGroup.data(graphNodes, function(d){ return d.id; }),
nodeEnter = node.enter().append('g')
.attr('class', 'node');
// this is the enter selection
// append and set color, we never change color
nodeEnter.append('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('fill', 'gray');
nodeEnter.append('text')
.attr("dx", function(d){ return d.x + 20; })
.attr("dy", function(d){ return d.y + 5; });
// nodeGroup is the enter + update selection
nodeGroup = nodeEnter.merge(node);
// change the things we want to change on every update
nodeGroup.select("text")
.text(function(d) { return d.text ? d.text : d.id });
nodeGroup.select("circle")
.attr('r', function(d){ return d.r; })
// exit, just remove
node.exit().remove();
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>

Can't delete circle in D3 on update

I'm having a lot of trouble getting a circle to delete. In this example I want to delete the circle labeled "2" but it just deletes the last circle in the array. If you uncomment the commented part and comment the graphNodes.splice(2, 1), it works as it should adding a new circle. But the delete won't work.
What am I missing here?
var width = 640,
height = 480;
var graphNodes = [
{ id: 0, x: 39, y: 343, r: 15 },
{ id: 1, x: 425, y: 38, r: 15 },
{ id: 2, x: 183, y: 417, r: 15 },
{ id: 3, x: 564, y: 31, r: 15 },
{ id: 4, x: 553, y: 351, r: 15 },
{ id: 5, x: 454, y: 298, r: 15 },
{ id: 6, x: 493, y: 123, r: 15 },
{ id: 7, x: 471, y: 427, r: 15 },
{ id: 8, x: 142, y: 154, r: 15 }
];
var svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height);
svg.append('rect')
.attr('class', 'graph')
.attr('width', width)
.attr('height', height)
.attr('fill', 'lightblue')
.attr('opacity', 0.3)
.on('click', function(){
/*graphNodes.push({
x: d3.mouse(this)[0],
y: d3.mouse(this)[1],
id: graphNodes.length,
r: 15
});*/
graphNodes.splice(2, 1);
update();
});
var nodeGroup = svg.selectAll('.nodes')
.data(graphNodes, function(d){ return d.id; })
.enter().append('g')
.attr('class', 'node');
nodeGroup.append('circle')
.attr('cx', function(d) { return d.x })
.attr('cy', function(d) { return d.y })
.attr("r", function(d){ return d.r; })
.attr("fill", "gray");
nodeGroup.append('text')
.attr("dx", function(d){ return d.x + 20; })
.attr("dy", function(d){ return d.y + 5; })
.text(function(d) { return d.id });
function update() {
if(nodeGroup){
// Update nodes
var node = nodeGroup.data(graphNodes),
nodeEnter = node.enter().append('g')
.attr('class', 'node');
nodeEnter.append('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', function(d){ return d.r; })
.attr('fill', 'gray');
nodeEnter.append('text')
.attr("dx", function(d){ return d.x + 20; })
.attr("dy", function(d){ return d.y + 5; })
.text(function(d) { return d.id });
nodeGroup = nodeEnter.merge(node);
node.exit().remove();
}
}
Fiddle
When you re-bind your data in update function, you forgot your key function:
var node = nodeGroup.data(graphNodes, function(d){ return d.id; }),
Update fiddle.

Categories

Resources