Combine D3 Pie Chart and Hierarchical Edge Bundling - javascript

How do I achieve this with D3? desired output
It's easy to have two layers of pie charts https://embed.plnkr.co/plunk/2p0zmp
Or to use d3 network with graph and nodes, http://using-d3js.com/05_08_links.html
but how could I overlay the concept of "nodes" and "links" onto these arcs of a piechart?
What kind of data structure is preferred?
{
nodes: [
{
layer: 1,
data: [
{name: A },
{name: B },
{name: C },
{name: D }
]
},
{
layer: 2,
data: [
{name: E },
{name: F },
{name: G }
]
}
],
links: [{ source: 'B', target: 'E'}, { source: 'D', target: 'F'}]
}

This gets pretty close with what you're looking for. You can use some additional arc generators and arc.centroid() to get the positions for the start and ends of the links. Then you can use a link generator to draw the links. One drawback of this is that the links can overlap the nodes.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://d3js.org/d3.v7.js"></script>
</head>
<body>
<div id="chart"></div>
<script>
/*
set up
*/
const width = 700;
const height = 400;
const svg = d3.select('#chart')
.append('svg')
.attr('width', width)
.attr('height', height);
const g = svg.append('g')
.attr('transform', `translate(${width / 2},${height})`);
/*
data
*/
const level1Nodes = [
{ name: 'A', value: 50, level: 1, color: 'RoyalBlue' },
{ name: 'B', value: 50, level: 1, color: 'DarkOrange' },
{ name: 'C', value: 50, level: 1, color: 'DarkOrange' },
{ name: 'D', value: 30, level: 1, color: 'Gold' }
];
const level2Nodes = [
{ name: 'E', value: 75, level: 2, color: 'RoyalBlue' },
{ name: 'F', value: 75, level: 2, color: 'DarkOrange' },
{ name: 'G', value: 30, level: 2, color: 'RoyalBlue' },
];
const links = [
{ source: 'B', target: 'E'},
{ source: 'D', target: 'F'}
];
/*
pie generator
*/
const pie = d3.pie()
.value(d => d.value)
.startAngle(-Math.PI / 2)
.endAngle(Math.PI / 2)
.padAngle(Math.PI / 45);
// calculate the angles for the slices of the nodes
const slices = [
...pie(level1Nodes),
...pie(level2Nodes)
];
/*
arcs
*/
const level1InnerRadius = 130;
const level1OuterRadius = 200;
const level2InnerRadius = 270;
const level2OuterRadius = 340;
// for drawing the nodes
const level1Arc = d3.arc()
.innerRadius(level1InnerRadius)
.outerRadius(level1OuterRadius);
const level2Arc = d3.arc()
.innerRadius(level2InnerRadius)
.outerRadius(level2OuterRadius);
const levelToArc = new Map([
[1, level1Arc],
[2, level2Arc]
]);
// for positioning the links along the outside
// of the level 1 nodes and the inside of the
// level 2 nodes
const level1OuterArc = d3.arc()
.innerRadius(level1OuterRadius)
.outerRadius(level1OuterRadius);
const level2InnerArc = d3.arc()
.innerRadius(level2InnerRadius)
.outerRadius(level2InnerRadius);
/*
calculating position of links
*/
// Map from the name of a node to the data for its arc
const nameToSlice = d3.index(slices, d => d.data.name);
// get the start and end positions for each link
const linkPositions = links.map(({source, target}) => ({
source: level1OuterArc.centroid(nameToSlice.get(source)),
target: level2InnerArc.centroid(nameToSlice.get(target)),
}));
/*
drawing
*/
// nodes
g.append('g')
.selectAll('path')
.data(slices)
.join('path')
.attr('d', d => levelToArc.get(d.data.level)(d))
.attr('fill', d => d.data.color);
// node labels
const labelsGroup = g.append('g')
.attr('font-family', 'sans-serif')
.attr('font-weight', 'bold')
.attr('font-size', 30)
.selectAll('text')
.data(slices)
.join('text')
.attr('dominant-baseline', 'middle')
.attr('text-anchor', 'middle')
.attr('transform', d => `translate(${levelToArc.get(d.data.level).centroid(d)})`)
.text(d => d.data.name);
// links
g.append('g')
.selectAll('path')
.data(linkPositions)
.join('path')
.attr('d', d3.linkVertical())
.attr('fill', 'none')
.attr('stroke', 'DarkBlue')
.attr('stroke-width', 2);
// circles at the end of links
g.append('g')
.selectAll('circle')
.data(linkPositions.map(({source, target}) => [source, target]).flat())
.join('circle')
.attr('r', 5)
.attr('fill', 'DarkBlue')
.attr('transform', d => `translate(${d})`);
</script>
</body>
</html>

Related

Horizontal Bar Chart -- unexpected offsetting of y-axis

I am trying to make a horizontal stacked bar chart, starting with this code snippet, updating to d3 v7. Instead of getting a neatly stacked bar chart, each subsequent bar in a stack is getting offset vertically down from where it should be. When I inspect the yScale value, I get the expected value, so I'm extra-confused about this behavior.
I'd include just the relevant piece of the puzzle, but I honestly don't know where my problem is -- am I appending to the wrong 'g' element? Using enter() on the wrong piece of data?
<script src="https://d3js.org/d3.v7.min.js"></script>
<body>
<div id="bar_chart">
<script>
var data = [{
dep_time: "5:30",
risk: 100,
details: [{
time: 19,
source: 'Drive'
},
{
time: 10,
source: 'Margin'
},
{
time: 42,
source: 'Full'
},
{
time: 35,
source: 'Crossing'
},
{
time: 23,
source: 'Drive'
}
]
},
{
dep_time: "6:20",
risk: 80,
details: [{
time: 25,
source: 'Drive'
},
{
time: 1,
source: 'Margin'
},
{
time: 38,
source: 'Full'
},
{
time: 35,
source: 'Crossing'
},
{
time: 25,
source: 'Drive'
}
]
},
{
dep_time: "7:10",
risk: 5,
details: [{
time: 8,
source: 'Drive'
},
{
time: 28,
source: 'Margin'
},
{
time: 38,
source: 'Full'
},
{
time: 35,
source: 'Crossing'
},
{
time: 18,
source: 'Drive'
}
]
}
];
var chartContainer = '.chart-container';
var units = [];
var xMax = 0;
data.forEach(function(s) {
var total = 0;
s.details.forEach(function(s) {
s["x0"] = total; //Abs left
s["x"] = s.time; //Width
s["x1"] = total + s.time; //Abs right
total = total + s.time;
if (total > xMax) xMax = total;
});
s["y"] = s.dep_time;
units.push(s.dep_time);
});
//Need it to look like: newdata = [(Drive) [19, 25, 32.] Margin [0, 1, 28]. Full [42, 38, 38]. Crossing [35, 35, 35]. Drive [23, 25, 18].]
//So it's a row in the array for each column of data.
//re-arrange the data so it makes more sense to d3 (and less sense to any sane human)
var newdata = [];
for (var i = 0; i < data[0].details.length; i++) {
var row = [];
data.forEach(function(s) {
row.push({
x: s.details[i].x,
y: s.dep_time,
x0: s.details[i].x0
});
});
newdata.push(row);
}
console.log("newdata");
console.log(newdata);
var margins = {
left: 50,
bottom: 50,
top: 25,
right: 25
};
var sizes = {
width: 500,
height: 150
};
var width = sizes.width - margins.left - margins.right;
var height = sizes.height - margins.bottom - margins.top;
var svg = d3.select("#bar_chart")
.append('svg')
.attr('width', width + margins.left + margins.right)
.attr('height', height + margins.bottom)
.append('g')
.attr('transform', 'translate(' + margins.left + ', ' + margins.top + ")");
var yScale = d3.scaleBand()
.domain(units)
.rangeRound([0, height]);
var yAxis = d3.axisLeft(yScale);
var yAxisG = svg.append("g")
.attr("transform", "translate(0,0)")
.attr("id", "yaxis")
.call(yAxis);
const xScale = d3.scaleLinear()
.domain([0, xMax])
.range([0, width]);
var xAxis = d3.axisBottom(xScale);
var xAxisG = svg.append("g")
.attr("transform", "translate(0, " + height + ")")
.attr("id", "xaxis")
.call(xAxis
.ticks(8));
var bar_colors = ['red', 'purple', 'green', 'lightblue', 'yellow'];
var colors = function(i) {
return bar_colors[i];
}
var groups = svg.selectAll('g')
.data(newdata)
//.exit()
.append('g')
.style('fill', function(d, i) {
console.log("d");
console.log(d);
//console.log("i"); console.log(i);
return colors(i);
});
groups.selectAll('rect')
.data(function(d) {
//console.log(d);
return d;
})
.enter()
.append('rect')
.attr('x', function(d) {
//console.log("x0"); console.log(d.x0);
return xScale(d.x0);
})
.attr('y', function(d, i) {
//console.log(yScale(d.y));
//console.log(i);
return yScale(d.y);
})
.attr('height', 10) //function (d) {return yScale.rangeBand();})
.attr('width', function(d) {
return xScale(d.x);
});
</script>
</div>
</body>
You are appending the rectangles to existing translated groups (the axes) because of this:
var groups = svg.selectAll("g")
Instead, select nothing (and also remember to enter the selection):
var groups = svg.selectAll(null)
Here's your code with that change:
<script src="https://d3js.org/d3.v7.min.js"></script>
<body>
<div id="bar_chart">
<script>
var data = [{
dep_time: "5:30",
risk: 100,
details: [{
time: 19,
source: 'Drive'
},
{
time: 10,
source: 'Margin'
},
{
time: 42,
source: 'Full'
},
{
time: 35,
source: 'Crossing'
},
{
time: 23,
source: 'Drive'
}
]
},
{
dep_time: "6:20",
risk: 80,
details: [{
time: 25,
source: 'Drive'
},
{
time: 1,
source: 'Margin'
},
{
time: 38,
source: 'Full'
},
{
time: 35,
source: 'Crossing'
},
{
time: 25,
source: 'Drive'
}
]
},
{
dep_time: "7:10",
risk: 5,
details: [{
time: 8,
source: 'Drive'
},
{
time: 28,
source: 'Margin'
},
{
time: 38,
source: 'Full'
},
{
time: 35,
source: 'Crossing'
},
{
time: 18,
source: 'Drive'
}
]
}
];
var chartContainer = '.chart-container';
var units = [];
var xMax = 0;
data.forEach(function(s) {
var total = 0;
s.details.forEach(function(s) {
s["x0"] = total; //Abs left
s["x"] = s.time; //Width
s["x1"] = total + s.time; //Abs right
total = total + s.time;
if (total > xMax) xMax = total;
});
s["y"] = s.dep_time;
units.push(s.dep_time);
});
//Need it to look like: newdata = [(Drive) [19, 25, 32.] Margin [0, 1, 28]. Full [42, 38, 38]. Crossing [35, 35, 35]. Drive [23, 25, 18].]
//So it's a row in the array for each column of data.
//re-arrange the data so it makes more sense to d3 (and less sense to any sane human)
var newdata = [];
for (var i = 0; i < data[0].details.length; i++) {
var row = [];
data.forEach(function(s) {
row.push({
x: s.details[i].x,
y: s.dep_time,
x0: s.details[i].x0
});
});
newdata.push(row);
}
var margins = {
left: 50,
bottom: 50,
top: 25,
right: 25
};
var sizes = {
width: 500,
height: 150
};
var width = sizes.width - margins.left - margins.right;
var height = sizes.height - margins.bottom - margins.top;
var svg = d3.select("#bar_chart")
.append('svg')
.attr('width', width + margins.left + margins.right)
.attr('height', height + margins.bottom)
.append('g')
.attr('transform', 'translate(' + margins.left + ', ' + margins.top + ")");
var yScale = d3.scaleBand()
.domain(units)
.rangeRound([0, height]);
var yAxis = d3.axisLeft(yScale);
var yAxisG = svg.append("g")
.attr("transform", "translate(0,0)")
.attr("id", "yaxis")
.call(yAxis);
const xScale = d3.scaleLinear()
.domain([0, xMax])
.range([0, width]);
var xAxis = d3.axisBottom(xScale);
var xAxisG = svg.append("g")
.attr("transform", "translate(0, " + height + ")")
.attr("id", "xaxis")
.call(xAxis
.ticks(8));
var bar_colors = ['red', 'purple', 'green', 'lightblue', 'yellow'];
var colors = function(i) {
return bar_colors[i];
}
var groups = svg.selectAll(null)
.data(newdata)
.enter()
.append('g')
.style('fill', function(d, i) {
return colors(i);
});
groups.selectAll('rect')
.data(function(d) {
//console.log(d);
return d;
})
.enter()
.append('rect')
.attr('x', function(d) {
return xScale(d.x0);
})
.attr('y', function(d, i) {
return yScale(d.y);
})
.attr('height', 10) //function (d) {return yScale.rangeBand();})
.attr('width', function(d) {
return xScale(d.x);
});
</script>
</div>
</body>

I am not able to add put labels in 3d donut chart using d3.js

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" />

d3.js Compressing links of a component

I am trying to select a set of nodes in a Force Directed Layout graph in d3, then to compress the component the nodes form. My idea was to make a force simulation, as shown below:
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().distance(function(d) {
return d.distance;
}).strength(0.5))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
Since it relies on distance, I thought finding and selecting the appropriate links in the graph's data and shrinking it, such as
graph_data.links[indx].distance = 0;
would compress it. When I think about it, I would have to refresh the graph in some way with this new data. However, that is not ideal as I do not want the graph to rebuild itself every time I select a component. Is there a way to change these distances without having to feed a redrawn graph newly modified data, such as selecting the link in the simulated graph directly rather than the passed data?
However, that is not ideal as I do not want the graph to rebuild itself every time I select a component
You don't really have to, just update the data and restart the simulation:
<!DOCTYPE html>
<html>
<head>
<script src="https://d3js.org/d3.v6.js"></script>
</head>
<body>
<svg height="500" width="500"></svg>
<script>
var svg = d3.select('svg'),
width = +svg.attr('width'),
height = +svg.attr('height');
var data = {
nodes: [
{ id: 'a' },
{ id: 'b' },
{ id: 'c' },
{ id: 'x' },
{ id: 'y' },
{ id: 'z' },
],
links: [
{ source: 'a', target: 'b', distance: 200 },
{ source: 'b', target: 'c', distance: 200 },
{ source: 'c', target: 'a', distance: 200 },
{ source: 'x', target: 'y', distance: 200 },
{ source: 'y', target: 'z', distance: 200 },
{ source: 'z', target: 'x', distance: 200 },
],
};
var simulation = d3
.forceSimulation()
.force(
'link',
d3
.forceLink()
.id((d) => d.id)
.distance(function (d) {
return d.distance;
})
.strength(0.5)
)
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter(width / 2, height / 2));
var link = svg
.append('g')
.attr('class', 'links')
.selectAll('line')
.data(data.links)
.enter()
.append('line')
.attr('stroke', 'black');
var node = svg
.append('g')
.attr('class', 'nodes')
.selectAll('circle')
.data(data.nodes)
.enter()
.append('circle')
.attr('cx', width / 2)
.attr('cy', height / 2)
.attr('r', 20)
.on('click', function (e, d) {
link.data().forEach(function (l) {
if (l.source.id === d.id || l.target.id === d.id) {
l.distance = 0;
} else {
l.distance = 200;
}
});
// re-bind data
simulation.force('link').links(data.links);
// restart simulation
simulation.alpha(1).restart();
});
simulation.nodes(data.nodes).on('tick', ticked);
simulation.force('link').links(data.links);
function ticked() {
node.attr('cx', (d) => d.x).attr('cy', (d) => d.y);
link
.attr('x1', (d) => d.source.x)
.attr('y1', (d) => d.source.y)
.attr('x2', (d) => d.target.x)
.attr('y2', (d) => d.target.y);
}
</script>
</body>
</html>

Access parent data when appending

I basically have some nested data like so:
const shapeGroups = [
{
title: 'shapeGroup_01',
color: 'blue',
shapes: [
{
shape:'rect',
width: 30,
height: 100,
x: 250,
y: 450
}
]
},
{
title: 'shapeGroup_01',
color: 'blue',
shapes: [
{
shape:'rect',
width: 10,
height: 40,
x: 350,
y:50
}
]
}
]
What I'm interested in doing is setting the color in each rect as it's defined in the shapeGroup's color property.
const shapeGroups = [{
title: 'shapeGroup_01',
color: 'blue',
shapes: [{
shape: 'rect',
width: 30,
height: 100,
x: 250,
y: 450
}]
},
{
title: 'shapeGroup_01',
color: 'blue',
shapes: [{
shape: 'rect',
width: 10,
height: 40,
x: 350,
y: 50
}]
}
]
const stage = d3.select('#stageContainer')
.append('svg')
.attr('id', '#stage')
.attr('width', 1000)
.attr('height', 1000)
const groups = stage
.selectAll('.group')
.data(shapeGroups)
const groupEnter = groups
.enter()
.append('g')
.attr('class', 'group');
const getGroup = group => group.shapes;
const createShape = shape => document.createElementNS(d3.namespaces.svg, shape.shape)
groupEnter
.selectAll('.shape')
.data(getGroup)
.enter()
.append(createShape)
.attr('fill', 'red') // I want the color as defined in the current group
.attr('width', d => d.width)
.attr('height', d => d.height)
.attr('x', d => d.x)
.attr('y', d => d.y)
<script src="https://d3js.org/d3.v5.min.js"></script>
<div id="stageContainer"></div>
In other words, I need a way to either set color at the group level, or somehow access the parent datum object when I'm appending individual .shapes.
As you can read in my answer here, you cannot access the parent's datum or its index from inside a selection in D3 v4/v5.
A solution here is getting the datum of the parentNode:
.attr('fill', (_, i, n) => d3.select(n[i].parentNode).datum().color)
Here is your code with that change (I'm making the two rectangles with different colours and the SVG smaller, for better visualising it):
const shapeGroups = [{
title: 'shapeGroup_01',
color: 'blue',
shapes: [{
shape: 'rect',
width: 30,
height: 100,
x: 250,
y: 50
}]
},
{
title: 'shapeGroup_01',
color: 'green',
shapes: [{
shape: 'rect',
width: 10,
height: 40,
x: 350,
y: 20
}]
}
];
const stage = d3.select('body')
.append('svg')
.attr('width', 400)
.attr('height', 400)
const groups = stage
.selectAll('.group')
.data(shapeGroups)
const groupEnter = groups
.enter()
.append('g')
.attr('class', 'group');
const getGroup = group => group.shapes;
const createShape = shape => document.createElementNS(d3.namespaces.svg, shape.shape)
groupEnter
.selectAll('.shape')
.data(getGroup)
.enter()
.append(createShape)
.attr('fill', (_, i, n) => d3.select(n[i].parentNode).datum().color)
.attr('width', d => d.width)
.attr('height', d => d.height)
.attr('x', d => d.x)
.attr('y', d => d.y)
<script src="https://d3js.org/d3.v5.min.js"></script>
However, the simplest solution by far is just setting the fill to the parent selection itself:
const groupEnter = groups
.enter()
.append('g')
.attr('class', 'group')
.attr('fill', d => d.color);
Here is the demo:
const shapeGroups = [{
title: 'shapeGroup_01',
color: 'blue',
shapes: [{
shape: 'rect',
width: 30,
height: 100,
x: 250,
y: 50
}]
},
{
title: 'shapeGroup_01',
color: 'green',
shapes: [{
shape: 'rect',
width: 10,
height: 40,
x: 350,
y: 20
}]
}
];
const stage = d3.select('body')
.append('svg')
.attr('width', 400)
.attr('height', 400)
const groups = stage
.selectAll('.group')
.data(shapeGroups)
const groupEnter = groups
.enter()
.append('g')
.attr('class', 'group')
.attr('fill', d=>d.color);
const getGroup = group => group.shapes;
const createShape = shape => document.createElementNS(d3.namespaces.svg, shape.shape)
groupEnter
.selectAll('.shape')
.data(getGroup)
.enter()
.append(createShape)
.attr('width', d => d.width)
.attr('height', d => d.height)
.attr('x', d => d.x)
.attr('y', d => d.y)
<script src="https://d3js.org/d3.v5.min.js"></script>

Overlay a function over a bar chart in D3

Suppose I create a bar chart from some data that I have. This bar chart is generated on a set of x- and y- axes. How can I plot a function (in the form y=f(x)) as a line over that same set of axes? I want to do this so that you can easily compare the trends shown by the bar chart and the function.
You just generate the data to feed to d3 by running the function over the desired values of the domain and then use for example line genrators to draw the line.
var w = 400;
var h = 400;
var padding = 50;
var svg = d3.select('svg').attr('width', w + padding * 2).attr('height', h + padding * 2);
var xScale = d3.scale.linear().domain([-10, 10]).range([0, w]);
var yScale = d3.scale.linear().domain([2, -2]).range([0, h]);
var xAxis = d3.svg.axis().scale(xScale);
var yAxis = d3.svg.axis().scale(yScale).orient("left");
var g = svg.append("g").attr("transform", "translate(" + padding + "," + padding + ")");
g.append("g").attr("transform", "translate(0, " + w / 2 + ")").call(xAxis);
g.append("g").attr("transform", "translate(" + h / 2 + ", 0)").call(yAxis);
var line = d3.svg.line().interpolate("basis")
.x(function(d, i) {
return xScale(d.x);
})
.y(function(d, i) {
return yScale(d.y);
})
g.append('path')
.data([fn1()])
.attr("d", line);
g.append('path')
.data([fn2()])
.attr("d", line);
g.append('path')
.data([fn3()])
.attr("d", line);
g.append('path')
.data([fn4()])
.attr("d", line);
function fn1() {
function f(x) {
return Math.cos(x);
}
return _.chain(_.range(-10, 10)).map(function(x) {
return {
y: f(x),
x: x
};
}).value();
}
function fn2() {
function f(x) {
return Math.sin(x);
}
return _.chain(_.range(-10, 10)).map(function(x) {
return {
y: f(x),
x: x
};
}).value();
}
function fn3() {
function f(x) {
return Math.exp(x);
}
return _.chain(_.range(-10, 10)).map(function(x) {
return {
y: f(x),
x: x
};
}).value();
}
function fn4() {
function f(x, m, b) {
return m * x + b;
}
return _.chain(_.range(-10, 10)).map(function(x) {
return {
y: f(x, 1, 1),
x: x
};
}).value();
}
path {
stroke: black;
fill: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<body>
<svg></svg>
</body>
I am making use of D3 and Math.js.
The reason I am using Math.js is that the function can be very complex
from:
4 * sin(x) + 5 * tan(x/2)
4 * sin(x^2) + 5 * cos(x/2)^3
Anything but f(x) :)
You can define your any domain of your choice here:
//define xDomain change it domain of your choice
var xDomain = [-10, 10];
//define yDomain change it domain of your choice
var yDomain = [-10, 10];
Then make the conventional bar chart for the domain nothing special this is a standard D3 code for making bar charts.
//make the bar chart by loading the tsv
d3.tsv("data.tsv", type, function(error, data) {
if (error) throw error;
//make x axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + y(0) + ")")
.call(xAxis);
//make y axis
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + x(0) + ",0)")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Frequency");
//make bar chart rectangle
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) {
return x(d.letter) - 10;
})
.attr("width", 20)
.attr("y", function(d) {
return y(d.frequency);
})
.attr("height", function(d) {
return height / 2 - y(d.frequency);
});
makeLineFunction();
});
This is my function which will make the line chart based on the value entered in the text box(for the xdomain):
function makeLineFunction() {
//remove any line if present
d3.selectAll(".line").remove();
//make an array of all x points
var xArray = d3.range(xDomain[0], xDomain[1], 0.5);
//get the formula from the text box above
var value = d3.select("#function_text").node().value;
//generate the data using math.js
var data = xArray.map(function(x1) {
return {
x: x1,
//using math.js for evaluating y point for the given x
y: math.eval(value, {
x: x1
})
};
});
//make the path
svg.append("path")
.datum(data)
.attr("class", "line")
.attr("d", line);
}
Working code here, necessary comments added for help.
Hope this helps!
Here is an example which illustrate the line chart over bar chat. You can generate y=f(x) values using jsp or php at run time.
update the data part
series: [
{
title: 'Column',
type: 'column',
axisY: 'y1',
data: [['A', 1], ['B', 4], ['C', 3],
['D', 5], ['E', 2], ['F', 1]]
},
{
title: 'Line',
type: 'line',
axisY: 'y2',
data: [['A', 40], ['B', 60], ['C', 62],
['D', 52], ['E', 70], ['F', 75]]
}
]
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>
Multiple Axes Example
</title>
<link rel="stylesheet" type="text/css" href="http://www.jqchart.com/css/jquery.jqChart.css" />
<link rel="stylesheet" type="text/css" href="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.21/themes/smoothness/jquery-ui.css" />
<script src="http://www.jqchart.com/js/jquery-1.11.1.min.js" type="text/javascript"></script>
<script src="http://www.jqchart.com/js/jquery.mousewheel.js" type="text/javascript"></script>
<script src="http://www.jqchart.com/js/jquery.jqChart.min.js" type="text/javascript"></script>
<script src="http://www.jqchart.com/js/jquery.jqRangeSlider.min.js" type="text/javascript"></script>
<script lang="javascript" type="text/javascript">
$(document).ready(function () {
var background = {
type: 'linearGradient',
x0: 0,
y0: 0,
x1: 0,
y1: 1,
colorStops: [{ offset: 0, color: '#d2e6c9' },
{ offset: 1, color: 'white' }]
};
$('#jqChart').jqChart({
title: { text: 'Multiple Axes' },
border: { strokeStyle: '#6ba851' },
background: background,
animation: { duration: 1 },
shadows: {
enabled: true
},
axes: [
{
name: 'y1',
location: 'left'
},
{
name: 'y2',
location: 'right',
strokeStyle: '#FCB441',
majorGridLines: {
strokeStyle: '#FCB441'
},
majorTickMarks: {
strokeStyle: '#FCB441'
}
}
],
series: [
{
title: 'Column',
type: 'column',
axisY: 'y1',
data: [['A', 1], ['B', 4], ['C', 3],
['D', 5], ['E', 2], ['F', 1]]
},
{
title: 'Line',
type: 'line',
axisY: 'y2',
data: [['A', 40], ['B', 60], ['C', 62],
['D', 52], ['E', 70], ['F', 75]]
}
]
});
});
</script>
</head>
<body>
<div>
<div id="jqChart" style="width: 500px; height: 300px;"></div>
</div>
</body>
</html>

Categories