I'm a D3 newbie and trying to build word cloud page using
JasonDavies d3-cloud.
But sometimes all words are showing in circle layout, but sometimes in rectangular.
How can I make them always locate in circle layout?
d3-cloud's layout algorithm places words within an ellipse starting from the center of the rectangle you've provided (d3.layout.cloud().size([width, height])).
If you have a container (size([width, height])) big enough compared to your number of words and the size you've given to these words, then you'll get a nice circle (or an ellipse if your container isn't a square):
Math.seedrandom('hello words');
var data = [{ text: "Hello", count: 38 }, { text: "World", count: 27 }, { text: "Whatever", count: 21 }, { text: "Massive", count: 21 }, { text: "Thing", count: 16 }, { text: "Something", count: 14 }, { text: "What", count: 12 }, { text: "Else", count: 9 }, { text: "Blabla", count: 6 }, { text: "Small", count: 6 }, { text: "VeryLong", count: 6 }, { text: "Word", count: 3 }, { text: "abcdef", count: 4 }, { text: "Elsewhere", count: 9 }, { text: "Anything", count: 3 }, { text: "Placeholder", count: 14 }, { text: "WhateverSmall", count: 3 }, { text: "Work", count: 12 }, { text: "Wording", count: 7 }, { text: "Verb", count: 4 }];
var svg = d3.select("svg").append("g");
let fill = d3.scaleOrdinal(d3.schemeCategory20);
let size = d3.scaleLinear().range([0, 20]).domain([0, d3.max(data, d => d.count)]);
let word_cloud_data = data
.map( function(d) {
return { text: d.text, size: 9 + size(d.count) * 3.5 };
});
let layout = d3.layout.cloud()
.size([600, 600])
.words(word_cloud_data)
.padding(2.5)
.rotate(d => ~~(Math.random() * 2) * -90)
.fontSize(d => d.size)
.on("end", draw);
layout.start();
function draw(words) {
svg.append("g")
.attr("transform", "translate(250,250)")
.selectAll("text")
.data(words)
.enter().append("text")
.style("fill", (d, i) => { d.color = fill(i); return d.color; })
.style("text-anchor", "middle")
.attr("transform", d => "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")")
.text(d => d.text)
.style("font-size", d => d.size + "px");
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://cdn.rawgit.com/jasondavies/d3-cloud/master/build/d3.layout.cloud.js"></script>
<script src="https://d3js.org/d3-scale.v1.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/seedrandom/2.4.4/seedrandom.min.js">
</script>
<svg width="700" height="700"></svg>
If your container can't hold your words as an ellipse/circle, d3-cloud's layout algorithm will start finding space in the corners of the container you've chosen to contain your cloud. And it will start looking like a rectangle/square. (Up to the point there's not even enough space to add the remaining words):
Math.seedrandom('hello words');
var data = [{ text: "Hello", count: 38 }, { text: "World", count: 27 }, { text: "Whatever", count: 21 }, { text: "Massive", count: 21 }, { text: "Thing", count: 16 }, { text: "Something", count: 14 }, { text: "What", count: 12 }, { text: "Else", count: 9 }, { text: "Blabla", count: 6 }, { text: "Small", count: 6 }, { text: "VeryLong", count: 6 }, { text: "Word", count: 3 }];
var svg = d3.select("svg").append("g");
let fill = d3.scaleOrdinal(d3.schemeCategory20);
let size = d3.scaleLinear().range([0, 20]).domain([0, d3.max(data, d => d.count)]);
let word_cloud_data = data
.map( function(d) {
return { text: d.text, size: 9 + size(d.count) * 3.5 };
});
let layout = d3.layout.cloud()
.size([275, 275])
.words(word_cloud_data)
.padding(2.5)
.rotate(d => ~~(Math.random() * 2) * -90)
.fontSize(d => d.size)
.on("end", draw);
layout.start();
function draw(words) {
svg.append("g")
.attr("transform", "translate(150,150)")
.selectAll("text")
.data(words)
.enter().append("text")
.style("fill", (d, i) => { d.color = fill(i); return d.color; })
.style("text-anchor", "middle")
.attr("transform", d => "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")")
.text(d => d.text)
.style("font-size", d => d.size + "px");
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://cdn.rawgit.com/jasondavies/d3-cloud/master/build/d3.layout.cloud.js"></script>
<script src="https://d3js.org/d3-scale.v1.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/seedrandom/2.4.4/seedrandom.min.js">
</script>
<svg width="700" height="700"></svg>
The solutions consist in either scaling down the size of your words in order to give d3-layout enough space or increase the width and height of the cloud container.
For instance by scaling down the size of words:
// domain reduced from [0, 20] to [0, 13]:
var sizeScale = d3.scaleLinear().range([0, 13]).domain([0, d3.max(data, d => d.count)]);
Math.seedrandom('hello word');
var data = [{ text: "Hello", count: 38 }, { text: "World", count: 27 }, { text: "Whatever", count: 21 }, { text: "Massive", count: 21 }, { text: "Thing", count: 16 }, { text: "Something", count: 14 }, { text: "What", count: 12 }, { text: "Else", count: 9 }, { text: "Blabla", count: 6 }, { text: "Small", count: 6 }, { text: "VeryLong", count: 6 }, { text: "Word", count: 3 }];
var svg = d3.select("svg").append("g");
let fill = d3.scaleOrdinal(d3.schemeCategory20);
let size = d3.scaleLinear().range([0, 13]).domain([0, d3.max(data, d => d.count)]);
let word_cloud_data = data
.map( function(d) {
return { text: d.text, size: 9 + size(d.count) * 3.5 };
});
let layout = d3.layout.cloud()
.size([275, 275])
.words(word_cloud_data)
.padding(2.5)
.rotate(d => ~~(Math.random() * 2) * -90)
.fontSize(d => d.size)
.on("end", draw);
layout.start();
function draw(words) {
svg.append("g")
.attr("transform", "translate(150,150)")
.selectAll("text")
.data(words)
.enter().append("text")
.style("fill", (d, i) => { d.color = fill(i); return d.color; })
.style("text-anchor", "middle")
.attr("transform", d => "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")")
.text(d => d.text)
.style("font-size", d => d.size + "px");
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://cdn.rawgit.com/jasondavies/d3-cloud/master/build/d3.layout.cloud.js"></script>
<script src="https://d3js.org/d3-scale.v1.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/seedrandom/2.4.4/seedrandom.min.js">
</script>
<svg width="700" height="700"></svg>
Related
I have created a D3 donut chart by using a code on codepen but I am unable to add the labels to it. I want the labels to be added on the side of every portion. For creating this donut chart I have used D3.js:
This is the code I have used:
<script type="text/javascript">
var dataset = [
{ name: 'Savings', count: 3250 },
{ name: 'Housing', count: 1707 },
{ name: 'Transportation', count: 377 },
{ name: 'Misc', count: 365 },
{ name: 'Insurance', count: 314 },
{ name: 'Utilities', count: 294 },
{ name: 'Student Loans', count: 262 },
{ name: 'Food', count: 250 },
{ name: 'Phone', count: 10 },
];
var total=0;
dataset.forEach(function(d){
total+= d.count;
});
var pie=d3.layout.pie()
.value(function(d){return d.count})
.sort(null);
var data_ready = pie(d3.entries(dataset))
var w=300,h=300;
var outerRadiusArc=w/2;
var innerRadiusArc=100;
var shadowWidth=10;
var outerRadiusArcShadow=innerRadiusArc+1;
var innerRadiusArcShadow=innerRadiusArc-shadowWidth;
var color = d3.scale.ordinal()
.range(['#f5e232', '#64eb34' , '#2d72e0', '#e3251b', '#d61be3', '#f0b00e', '#0ef0c3', '#e61240', '#db12e6']).domain(["Saving", "Housing", "Transportayion", "Misc", "Insurance", "Utilities", "Student Loan", "Food", "Phone"])
;
var svg=d3.select("#chart")
.append("svg")
.attr({
width:w,
height:h,
class:'shadow'
}).append('g')
.attr({
transform:'translate('+w/2+','+h/2+')'
});
var createChart=function(svg,outerRadius,innerRadius,fillFunction,className){
var arc=d3.svg.arc()
.innerRadius(outerRadius)
.outerRadius(innerRadius);
var path=svg.selectAll('.'+className)
.data(pie(dataset))
.enter()
.append('path')
.attr({
class:className,
d:arc,
fill:fillFunction
});
path.transition()
.duration(1000)
.attrTween('d', function(d) {
var interpolate = d3.interpolate({startAngle: 0, endAngle: 0}, d);
return function(t) {
return arc(interpolate(t));
};
});
};
createChart(svg,outerRadiusArc,innerRadiusArc,function(d,i){
return color(d.data.name);
},'path1');
createChart(svg,outerRadiusArcShadow,innerRadiusArcShadow,function(d,i){
var c=d3.hsl(color(d.data.name));
return d3.hsl((c.h+5), (c.s -.07), (c.l -.15));
},'path2');
var addText= function (text,y,size) {
svg.append('text')
.text(text)
.attr({
'text-anchor':'middle',
y:y
})
.style({
fill:'black',
'font-size':size,
});
};
var addTexttwo= function (text,x,y,size) {
svg.append('text')
.text(text)
.attr({
'text-anchor':'middle',
y:y,
x:x,
})
.style({
fill:'white',
'font-size':size,
});
};
var restOfTheData=function(){
addText(function(){
return "$6,830";
},40,'30px');
addText(function(){
return "Shine's";
},-20, '20px');
addText(function(){
return "Monthly Budget";
},0, '20px');
};
setTimeout(restOfTheData,1000);
function numberWithCommas(x) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
</script>
I want the result to look somewhat like this. With the labels on the side of the donut
This code puts the labels around the pie:
const textRadius = d => outerRadiusArc + (d.data.count < 50 ? 15 : 5);
const textX = d => textRadius(d) * Math.sin((d.startAngle + d.endAngle) / 2);
const textY = d => textRadius(d) * -Math.cos((d.startAngle + d.endAngle) / 2);
const textAnchor = d => (d.startAngle + d.endAngle) / 2 > Math.PI ? 'end' : 'start';
svg.selectAll('text.label')
.data(pie(dataset))
.enter()
.append('text')
.classed('label', true)
.text(d => d.data.name)
.attr('x', textX)
.attr('y', textY)
.attr('text-anchor', textAnchor)
.attr('alignment-baseline', 'middle')
var dataset = [
{ name: 'Savings', count: 3250 },
{ name: 'Housing', count: 1707 },
{ name: 'Transportation', count: 377 },
{ name: 'Misc', count: 365 },
{ name: 'Insurance', count: 314 },
{ name: 'Utilities', count: 294 },
{ name: 'Student Loans', count: 262 },
{ name: 'Food', count: 250 },
{ name: 'Phone', count: 10 },
];
var total=0;
dataset.forEach(function(d){
total+= d.count;
});
var pie=d3.layout.pie()
.value(function(d){return d.count})
.sort(null);
var data_ready = pie(d3.entries(dataset))
var w=400,h=300;
var outerRadiusArc=120;
var innerRadiusArc=90;
var shadowWidth=10;
var outerRadiusArcShadow=innerRadiusArc+1;
var innerRadiusArcShadow=innerRadiusArc-shadowWidth;
var color = d3.scale.ordinal()
.range(['red', '#f5e232', 'orange' , '#2d72e0', '#e3251b', '#d61be3', '#f0b00e', '#0ef0c3', '#e61240', '#db12e6']).domain(["Saving", "Housing", "Transportayion", "Misc", "Insurance", "Utilities", "Student Loan", "Food", "Phone"])
;
var svg = d3.select("#chart")
.append("svg")
.attr({
width:w,
height:h,
class:'shadow'
}).append('g')
.attr({
transform:'translate('+w/2+','+h/2+')'
});
var createChart=function(svg,outerRadius,innerRadius,fillFunction,className){
var arc=d3.svg.arc()
.innerRadius(outerRadius)
.outerRadius(innerRadius);
var path=svg.selectAll('.'+className)
.data(pie(dataset))
.enter()
.append('path')
.attr({
class:className,
d:arc,
fill:fillFunction
});
path.transition()
.duration(1000)
.attrTween('d', function(d) {
var interpolate = d3.interpolate({startAngle: 0, endAngle: 0}, d);
return function(t) {
return arc(interpolate(t));
};
})
.each(d => {
console.log(d);
})
const textRadius = d => outerRadiusArc + (d.data.count < 50 ? 15 : 5);
const textX = d => textRadius(d) * Math.sin((d.startAngle + d.endAngle) / 2);
const textY = d => textRadius(d) * -Math.cos((d.startAngle + d.endAngle) / 2);
const textAnchor = d => (d.startAngle + d.endAngle) / 2 > Math.PI ? 'end' : 'start';
svg.selectAll('text.label')
.data(pie(dataset))
.enter()
.append('text')
.classed('label', true)
.text(d => d.data.name)
.attr('x', textX)
.attr('y', textY)
.attr('text-anchor', textAnchor)
.attr('alignment-baseline', 'middle')
/*
svg.selectAll('circle.point')
.data(pie(dataset))
.enter()
.append('circle')
.classed('point', true)
.attr('r', 3)
.attr('cx', textX)
.attr('cy', textY)
*/
};
createChart(svg,outerRadiusArc,innerRadiusArc,function(d,i){
return color(d.data.name);
},'path1');
createChart(svg,outerRadiusArcShadow,innerRadiusArcShadow,function(d,i){
var c=d3.hsl(color(d.data.name));
return d3.hsl((c.h+5), (c.s -.07), (c.l -.15));
},'path2');
var addText= function (text,y,size) {
svg.append('text')
.text(text)
.attr({
'text-anchor':'middle',
y:y
})
.style({
fill:'black',
'font-size':size,
});
};
var addTexttwo= function (text,x,y,size) {
svg.append('text')
.text(text)
.attr({
'text-anchor':'middle',
y:y,
x:x,
})
.style({
fill:'white',
'font-size':size,
});
};
var restOfTheData=function(){
addText(function(){
return "$6,830";
},40,'30px');
addText(function(){
return "Shine's";
},-20, '20px');
addText(function(){
return "Monthly Budget";
},0, '20px');
};
setTimeout(restOfTheData,1000);
function numberWithCommas(x) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
.label {
font-family: 'Ubuntu';
font-size: 10px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<div id="chart" />
I need to draw some links between nodes in a force directed graph but I would like those links to not affect the physics of the graph. There are other existing links that do affect the physics.
In the attached example, I would like to draw the links defined as "links2" but have them just "follow" the layout of the rest of the graph. I would like those lines to be animated as part of the simulation but not influence the final graph.
I have manually drawn what I would like the final result to look like.
I don't know how to separately reference the nodes that are part of the force layout.
var nodes = [
{ name: "node0" },
{ name: "node1" },
{ name: "node2" },
{ name: "node3" },
{ name: "node4" },
{ name: "node5" },
{ name: "node6" },
{ name: "node7" },
{ name: "node8" },
{ name: "node9" },
{ name: "node10" },
{ name: "node11" },
{ name: "node12" },
{ name: "node13" },
{ name: "node14" },
{ name: "node15" },
{ name: "node16" },
{ name: "node17" },
{ name: "node18" },
{ name: "node19" }
];
var links = [
{ source: 0, target: 9 },
{ source: 1, target: 9 },
{ source: 2, target: 9 },
{ source: 3, target: 9 },
{ source: 4, target: 9 },
{ source: 5, target: 9 },
{ source: 6, target: 9 },
{ source: 7, target: 9 },
{ source: 8, target: 9 },
{ source: 10, target: 19 },
{ source: 11, target: 19 },
{ source: 12, target: 19 },
{ source: 13, target: 19 },
{ source: 14, target: 19 },
{ source: 15, target: 19 },
{ source: 16, target: 19 },
{ source: 17, target: 19 },
{ source: 18, target: 19 }
];
var links2 = [
{ source: 0, target: 10 },
{ source: 1, target: 11 },
{ source: 2, target: 12 },
{ source: 3, target: 13 }
]
var width = 700;
var height = 300;
var force = d3.layout.force()
.nodes(nodes)
.links(links)
.size([width, height])
.linkDistance(50)
.charge(-700)
.on("tick", tick)
.start();
var svg = d3.select("#graph").append("svg")
.attr("width", width)
.attr("height", height);
var colors = d3.scale.category10();
var line = svg.append("g").selectAll("line")
.data(force.links())
.enter().append("line")
.attr('class', 'link')
.attr('stroke', function(d, i) {
return colors(i);
})
var circle = svg.append("g").selectAll("circle")
.data(force.nodes())
.enter().append("circle")
.attr("r", 8)
.attr('class', 'circle')
.attr('fill', function(d, i) {
return colors(i);
})
.call(force.drag);
var text = svg.append("g").selectAll("text")
.data(force.nodes())
.enter().append("text")
.attr("x", 14)
.attr("y", ".31em")
.text(function(d) {
return d.name;
});
function tick() {
line.attr({
x1: function(d) {
return d.source.x;
},
y1: function(d) {
return d.source.y;
},
x2: function(d) {
return d.target.x;
},
y2: function(d) {
return d.target.y;
}
});
circle.attr("transform", transform);
text.attr("transform", transform);
}
function transform(d) {
return "translate(" + d.x + "," + d.y + ")";
}
.link {
fill: none;
stroke-width: 1.5px;
}
circle {
stroke: black;
stroke-width: 1.5px;
}
text {
font: 10px sans-serif;
pointer-events: none;
}
#graph {
position: absolute;
top: 0px;
left: 0px;
}
<script src="https://d3js.org/d3.v3.min.js"></script>
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<div id="root">
<div style="background-color:lightgrey" id="graph" height="800px" width="800px">
</div>
</div>
</body>
Any assistance much appreciated.
Your link2 reference the nodes by index, so in the tick use that to index in the nodes array. And it is better to use classes for the different types of links and nodes.
The links that are not force related have a dash pattern.
var nodes = [
{ name: "node0" },
{ name: "node1" },
{ name: "node2" },
{ name: "node3" },
{ name: "node4" },
{ name: "node5" },
{ name: "node6" },
{ name: "node7" },
{ name: "node8" },
{ name: "node9" },
{ name: "node10" },
{ name: "node11" },
{ name: "node12" },
{ name: "node13" },
{ name: "node14" },
{ name: "node15" },
{ name: "node16" },
{ name: "node17" },
{ name: "node18" },
{ name: "node19" }
];
var links = [
{ source: 0, target: 9 },
{ source: 1, target: 9 },
{ source: 2, target: 9 },
{ source: 3, target: 9 },
{ source: 4, target: 9 },
{ source: 5, target: 9 },
{ source: 6, target: 9 },
{ source: 7, target: 9 },
{ source: 8, target: 9 },
{ source: 10, target: 19 },
{ source: 11, target: 19 },
{ source: 12, target: 19 },
{ source: 13, target: 19 },
{ source: 14, target: 19 },
{ source: 15, target: 19 },
{ source: 16, target: 19 },
{ source: 17, target: 19 },
{ source: 18, target: 19 }
];
var links2 = [
{ source: 0, target: 10 },
{ source: 1, target: 11 },
{ source: 2, target: 12 },
{ source: 3, target: 13 }
]
var width = 700;
var height = 300;
var force = d3.layout.force()
.nodes(nodes)
.links(links)
.size([width, height])
.linkDistance(50)
.charge(-700)
.on("tick", tick)
.start();
var svg = d3.select("#graph").append("svg")
.attr("width", width)
.attr("height", height);
var colors = d3.scale.category10();
var line = svg.append("g").selectAll(".link")
.data(force.links())
.enter().append("line")
.attr('class', 'link')
.attr('stroke', function(d, i) { return colors(i); });
var line2 = svg.append("g").selectAll(".link2")
.data(links2)
.enter().append("line")
.attr('class', 'link2')
.attr('stroke-dasharray', '5,5')
.attr('stroke', function(d, i) { return colors(i); });
var circle = svg.append("g").selectAll(".circle")
.data(force.nodes())
.enter().append("circle")
.attr("r", 8)
.attr('class', 'circle')
.attr('fill', function(d, i) {
return colors(i);
})
.call(force.drag);
var text = svg.append("g").selectAll("text")
.data(force.nodes())
.enter().append("text")
.attr("x", 14)
.attr("y", ".31em")
.text(function(d) {
return d.name;
});
function tick() {
line.attr({
x1: function(d) { return d.source.x; },
y1: function(d) { return d.source.y; },
x2: function(d) { return d.target.x; },
y2: function(d) { return d.target.y; }
});
line2.attr({
x1: function(d) { return nodes[d.source].x; },
y1: function(d) { return nodes[d.source].y; },
x2: function(d) { return nodes[d.target].x; },
y2: function(d) { return nodes[d.target].y; }
});
circle.attr("transform", transform);
text.attr("transform", transform);
}
function transform(d) {
return "translate(" + d.x + "," + d.y + ")";
}
.link {
fill: none;
stroke-width: 1.5px;
}
circle {
stroke: black;
stroke-width: 1.5px;
}
text {
font: 10px sans-serif;
pointer-events: none;
}
#graph {
position: absolute;
top: 0px;
left: 0px;
}
<script src="https://d3js.org/d3.v3.min.js"></script>
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<div id="root">
<div style="background-color:lightgrey" id="graph" height="800px" width="800px">
</div>
</div>
</body>
If you look at the example in full page (ignore the crazy line, that is not relevant), you will see that on load the last tick of the X axis is 'fudged' in by 10 by altering the transition attribute.
However, when you click the 'Update' button to run an update and transition of the XAxis data, the data is replaced and the 'fudged' position of the last tick is lost, despite the 'fudging' being re-applied in the customXAxis function that gets fired on the update.
I suspect this is something to do with the transition lifecycle??
Is it possible to make the positioning changes required to child elements (such as .remove() on certain elements prior to the transition of a parent?
var update = document.getElementById("update");
update.addEventListener(
"click",
function() {
updateFunc();
},
false
);
var data = [
{
date: 1,
amount: 58.13
},
{
date: 30,
amount: 53.98
},
{
date: 27,
amount: 67.0
},
{
date: 26,
amount: 89.7
},
{
date: 25,
amount: 99.0
},
{
date: 24,
amount: 130.28
},
{
date: 23,
amount: 166.7
},
{
date: 20,
amount: 234.98
},
{
date: 19,
amount: 345.44
},
{
date: 18,
amount: 443.34
},
{
date: 17,
amount: 543.7
},
{
date: 16,
amount: 580.13
},
{
date: 13,
amount: 605.23
},
{
date: 12,
amount: 622.77
},
{
date: 11,
amount: 626.2
},
{
date: 10,
amount: 628.44
},
{
date: 9,
amount: 636.23
},
{
date: 5,
amount: 633.68
},
{
date: 4,
amount: 624.31
},
{
date: 3,
amount: 629.32
},
{
date: 2,
amount: 618.63
},
{
date: 30,
amount: 599.55
},
{
date: 29,
amount: 609.86
},
{
date: 28,
amount: 617.62
},
{
date: 27,
amount: 614.48
},
{
date: 26,
amount: 606.98
}
];
// set the dimensions and margins of the graph
var margin = { top: 20, right: 20, bottom: 30, left: 50 },
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// set the ranges
var x = d3.scaleLinear().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
// define the line
var valueline = d3
.line()
.x(function(d) {
return x(d.date);
})
.y(function(d) {
return y(d.amount);
});
// append the svg obgect to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var svg = d3
.select("body")
.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 + ")");
// Get the data
// format the data
data.forEach(function(d) {
d.date = +d.date;
d.amount = +d.amount;
});
// Scale the range of the data
x.domain(
d3.extent(data, function(d) {
return d.date;
})
);
y.domain([
0,
d3.max(data, function(d) {
return d.amount;
})
]);
// Add the valueline path.
svg.append("path").data([data]).attr("class", "line").attr("d", valueline);
// Add the X Axis
svg
.append("g")
.attr("transform", "translate(0," + height + ")")
.classed("XAxis", true)
.call(customXAxis);
function customXAxis(g) {
g.call(d3.axisBottom(x));
var firstTicksTransform = svg
.selectAll(".XAxis .tick:first-of-type")
.attr("transform");
var translate = firstTicksTransform
.substring(
firstTicksTransform.indexOf("(") + 1,
firstTicksTransform.indexOf(")")
)
.split(",");
svg
.selectAll("XAxis .tick:first-of-type")
.attr("transform", "translate(" + (parseFloat(translate[0]) + 10) + ", 0)");
var lastTicksTransform = svg.selectAll(".XAxis .tick:last-of-type").attr("transform");
var translateLast = lastTicksTransform
.substring(
lastTicksTransform.indexOf("(") + 1,
lastTicksTransform.indexOf(")")
)
.split(",");
svg
.selectAll(".XAxis .tick:last-of-type")
.attr(
"transform",
"translate(" + (parseFloat(translateLast[0]) - 10) + ", 0)"
);
}
// Add the Y Axis
svg.append("g").classed("YAxis", true).call(d3.axisLeft(y));
var updateFunc = function() {
var svg = d3.select("body svg").transition();
data.forEach(function(d) {
d.date = d.date + 5;
d.amount = +d.amount;
});
x.domain(
d3.extent(data, function(d) {
return d.date;
})
);
svg.select(".XAxis").duration(750).call(customXAxis);
};
.line {
fill: none;
stroke: steelblue;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<button id="update">Update</button>
I have a set of nodes data
var dataNodes = [
{ id: 1, x: 10, y:30, text: "node 1", muteText: false },
{ id: 2, x: 30, y:50, text: "node 2", muteText: false },
{ id: 3, x: 50, y:70, text: "node 3", muteText: false }
];
I add the elements in the DOM using this kind of function (not my real code because of a lot of business complexity) :
function redraw(newData) {
var node = d3
.select("body")
.selectAll("g.node")
.data(dataNodes)
.transition().duration(500)
.attr("transform", d => "translate(" + d.x + "," + d.y + ")");
node.enter()
.append("g")
.attr("class", "node")
.attr("transform", d => "translate(" + d.x + "," + d.y + ")")
.append("text")
.text(d => d.text)
.style("opacity", "0")
.transition().duration(500)
.style("opacity", "1");
node.exit()
.style("opacity", "0");
}
I want to be able to do all the following when the data get updated:
add entering nodes
make already existing nodes move with a transition
hide exitings nodes (opacity 0) because they may reappear
when nodes get their "muteText" property changed to true, make the inner text disapear
I'm quite confortable with the 3 first requiremeents but I really don't know how to do the last one : how can I remove (or even change) sub elements based on a filtered set of data ? Can I use the filter in the d3.data function to do it ?
Let me know if my question is unclear.
If you want to filter, do it on your update selection:
var node = svg
.selectAll("g.node")
.data(someData);
var nodeE = node.enter()
.append("g")
.attr("class", "node");
nodeE.append("text")
.text(d => d.text);
// node is now UPDATE + ENTER
node = nodeE.merge(node);
// filter the text and set how you care
node.filter(function(d) {
return d.muteText
})
.select("text")
.style("opacity", 1)
.transition()
.style("opacity", 0);
node.filter(function(d) {
return !d.muteText
})
.select("text")
.style("opacity", 0)
.transition()
.style("opacity", 1);
Here's a running example:
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#4.0.0" data-semver="4.0.0" src="https://d3js.org/d3.v4.min.js"></script>
<style>
text {
fill: black;
font-family: arial;
}
</style>
</head>
<body>
<script>
var dataNodes = [{
id: 1,
x: 10,
y: 30,
text: "node 1",
muteText: false
}, {
id: 2,
x: 30,
y: 50,
text: "node 2",
muteText: false
}, {
id: 3,
x: 50,
y: 70,
text: "node 3",
muteText: false
}];
var svg = d3.select('body')
.append('svg')
.attr('width', 500)
.attr('height', 500);
redraw([{
id: 1,
x: 10,
y: 30,
text: "node 1",
muteText: false
}, {
id: 2,
x: 30,
y: 50,
text: "node 2",
muteText: false
}, {
id: 3,
x: 50,
y: 70,
text: "node 3",
muteText: false
}]);
setTimeout(function() {
redraw([{
id: 1,
x: 10,
y: 30,
text: "node 1",
muteText: true
}, {
id: 2,
x: 100,
y: 50,
text: "node 2",
muteText: false
}, {
id: 3,
x: 50,
y: 70,
text: "node 3",
muteText: true
}])
}, 2000)
setTimeout(function() {
redraw([{
id: 1,
x: 10,
y: 30,
text: "node 1",
muteText: true
}, {
id: 2,
x: 100,
y: 50,
text: "node 2",
muteText: false
}, {
id: 3,
x: 50,
y: 70,
text: "node 3",
muteText: false
},{
id: 4,
x: 60,
y: 90,
text: "node 4",
muteText: false
}])
}, 4000)
function redraw(someData) {
var node = svg
.selectAll("g.node")
.data(someData);
var nodeE = node.enter()
.append("g")
.attr("class", "node")
.attr("transform", d => "translate(" + d.x + "," + d.y + ")");
nodeE.append("text")
.text(d => d.text)
.style("opacity", 0)
.transition()
.style("opacity", 1);
node = nodeE.merge(node);
node.exit()
.style("opacity", "0");
node.transition().duration(500)
.attr("transform", d => "translate(" + d.x + "," + d.y + ")");
node.filter(function(d) {
return d.muteText
})
.select("text")
.transition()
.style("opacity", 0);
}
</script>
</body>
</html>
This question already has an answer here:
Using d3 to shade area between two lines
(1 answer)
Closed 6 years ago.
I want to fill the lines between two area graphs defined below. I am hitting a bit of a wall -- the issue I seem to have that each path I created does NOT have the other value to compare with, and my efforts to find a work around seem to have hit a bit of a wall.
Any tips?
jQuery(document).ready(function ($) {
var margin = {top: 20, right: 30, bottom: 40, left: 24},
width = 430 - margin.left - margin.right,
height = 225 - margin.top - margin.bottom,
dotRadius = function() { return 3 };
var x = d3.time.scale()
.range([0, width]);
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient('bottom')
.tickFormat(d3.time.format('%b'));
var yAxis = d3.svg.axis()
.scale(y)
.orient('left');
// This is a function that determines the colours of the lines drawn, up to 10.
var color = d3.scale.category10();
// This is used to format the time for our data.
var formatTime = d3.time.format("%Y-%m-%d");
var line = d3.svg.line()
.x(function(d) { return x(d.Period); })
.y(function(d) { return y(d.Value); })
var areaBetweenGraphs = d3.svg.area()
.x(function(d) {
console.log('ABG x is: ', d);
return x(formatTime.parse(d.Time));
})
.y0(function(d) {
console.log('ABG y0 is: ', d);
return y(d.Quantity);
})
.y1(function(d) {
console.log('ABG y1 is: ', d);
return y(d.Amount);
});
var svg = d3.select("#pipeline-chart-render")
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
// This separates the data into the lines we want, although the data is stored
// In the same original object.
color.domain(d3.keys(data[0].values[0]).filter(function(key) {
if (key === 'Amount'
|| key === 'Quantity') {
return key
}
}));
// This returns the data into two separate objects which can be graphed.
// In this case, Amount and Quantity.
var datasets = color.domain().map(function(name) {
return {
name: name,
values: data.map(function(d) {
return {
Period: formatTime.parse(d.values[0].Time),
Value: +d.values[0][name]};
})
};
});
console.log('datasets is: ', datasets);
// set the minYDomainValue to zero instead of letting it be a lingering magic number.
var minDomainValue = 0
var minDate = d3.min(datasets, function(d0){
return d3.min(d0.values, function(d1){
return d1.Period;
})
}),
maxDate = d3.max(datasets, function(d0){
return d3.max(d0.values, function(d1){
return d1.Period;
})
});
x.domain([minDate, maxDate]);
y.domain([
minDomainValue,
d3.max(datasets, function(c) { return d3.max(c.values, function(v) { return v.Value; }); })
])
// Append the x-axis class and move axis around.
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
// Append the y-axis class.
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.append('g')
var pipeline = svg.selectAll('.pipeline')
.data(datasets);
pipeline.enter()
.append('g')
.attr('class', 'pipeline');
pipeline.append('path')
.attr('class', 'line')
.attr('id', function(d, i) {
return 'pipeline-'+(i+1);
})
.attr('d', function(d) { console.log('line d is: ', d); return line(d.values); })
.attr("data-legend",function(d) { return d.name})
.style("stroke", function(d) { return color(d.name); })
pipeline.exit().remove()
// Rendering the points on the graph.
var points = svg.selectAll('.pipelinePoint')
.data(datasets);
points
.enter()
.append('g')
.attr('class', 'pipelinePoint');
points.selectAll('.point')
.data(function(d) {
return d.values;
})
.enter()
.append('circle')
.attr('circleId', function(d, i) {
return 'circleId-'+i;
})
.attr('cx', function(d) {
return x(d.Period);
})
.attr('cy', function(d) {
return y(d.Value);
})
.attr('r', function(d) {
return dotRadius()
});
});
var data = [
{
key: 1,
values: [
{
Amount: 33,
Quantity: 22,
Time: '2015-01-01'
}
]
},
{
key: 2,
values: [
{
Amount: 52,
Quantity: 20,
Time: '2015-02-01'
}
]
},
{
key: 3,
values: [
{
Amount: 63,
Quantity: 30,
Time: '2015-03-01'
}
]
},
{
key: 4,
values: [
{
Amount: 92,
Quantity: 60,
Time: '2015-04-01'
}
]
},
{
key: 5,
values: [
{
Amount: 50,
Quantity: 29,
Time: '2015-05-01'
}
]
},
{
key: 6,
values: [
{
Amount: 53,
Quantity: 25,
Time: '2015-06-01'
}
]
},
{
key: 7,
values: [
{
Amount: 46,
Quantity: 12,
Time: '2015-07-01'
}
]
},
{
key: 8,
values: [
{
Amount: 52,
Quantity: 15,
Time: '2015-08-01'
}
]
},
{
key: 9,
values: [
{
Amount: 55,
Quantity: 20,
Time: '2015-09-01'
}
]
},
{
key: 10,
values: [
{
Amount: 35,
Quantity: 17,
Time: '2015-10-01'
}
]
},
{
key: 11,
values: [
{
Amount: 80,
Quantity: 45,
Time: '2015-11-01'
}
]
},
{
key: 12,
values: [
{
Amount: 64,
Quantity: 24,
Time: '2015-12-01'
}
]
}
]
CSS if you want it to be a less ugly render:
/* Line Chart CSS */
.axis path,
.axis line {
fill: none;
stroke: #000;
stroke-width: 3px;
shape-rendering: crispEdges;
}
#pipeline-1,
#pipeline-2 {
fill: none;
stroke-width: 1.5px;
stroke-linecap: round;
transition: stroke-width 250ms linear;
-moz-transition: stroke-width 250ms linear;
-webkit-transition: stroke-width 250ms linear;
transition-delay: 250ms
-moz-transition-delay: 250ms;
-webkit-transition-delay: 250ms;
}
.x.axis path {
/* Uncomment below if I want to remove x-axis line */
/* display: none;*/
}
.line.hover path {
stroke-width: 6px;
}
#pipeline-chart-render {
padding-left: -50px;
}
.area {
fill: steelblue;
}
This ended up working.
// The following is for defining the area BETWEEN graphs.
var areaAboveQuantity = d3.svg.area()
.x(line.x())
.y0(line.y())
.y1(0);
var areaBelowQuantity = d3.svg.area()
.x(line.x())
.y0(line.y())
.y1(height);
var areaAboveAmount = d3.svg.area()
.x(line.x())
.y0(line.y())
.y1(0);
var areaBelowAmount = d3.svg.area()
.x(line.x())
.y0(line.y())
.y1(height);
var defs = svg.append('defs');
defs.append('clipPath')
.attr('id', 'clip-quantity')
.append('path')
.datum(datasets)
.attr('d', function(d) {
return areaAboveQuantity(d[1].values);
});
defs.append('clipPath')
.attr('id', 'clip-amount')
.append('path')
.datum(datasets)
.attr('d', function(d) {
return areaAboveAmount(d[0].values);
});
svg.append('path')
.datum(datasets)
.attr('class', 'area')
.attr('d', function(d) {
return areaBelowQuantity(d[1].values)
});
// Quantity IS ABOVE Amount
svg.append('path')
.datum(datasets)
.attr('d', function(d) {
areaBelowQuantity(d[1].values);
})
.attr('clip-path', 'url(#clip-amount)')
.style('fill', 'steelblue')
.style('opacity', '0.2');
// Amount IS ABOVE Quanity
svg.append('path')
.datum(datasets)
.attr('d', function(d) {
return areaBelowAmount(d[0].values);
})
.attr('clip-path', 'url(#clip-quantity)')
.style('fill', 'steelblue')
.style('opacity', '0.2');