Can someone help me implementing a spiral chart similar to the one below using d3.js?
I've just got the basic spiral plot (a simple one) as of now but not been able to append bars to the plot based on the timeline as shown in the image. I'm trying out a few things (if you see the commented code).
Here's my fiddle, and my code:
var width = 400,
height = 430,
axes = 12,
tick_axis = 9,
start = 0,
end = 2.25;
var theta = function(r) {
return 2 * Math.PI * r;
};
var angle = d3.scale.linear()
.domain([0, axes]).range([0, 360])
var r = d3.min([width, height]) / 2 - 40;
var r2 = r;
var radius = d3.scale.linear()
.domain([start, end])
.range([0, r]);
var svg = d3.select("#chart").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + (height / 2 + 8) + ")");
var points = d3.range(start, end + 0.001, (end - start) / 1000);
var spiral = d3.svg.line.radial()
.interpolate("cardinal")
.angle(theta)
.radius(radius);
var path = svg.selectAll(".spiral")
.data([points])
.enter().append("path")
.attr("class", "spiral")
.attr("d", spiral)
var z = d3.scale.category20();
var circles = svg.selectAll('.circle')
.data(points);
/* circles.enter().append('circle')
.attr('r', 5)
.attr('transform', function(d) { return 'translate(' + d + ')'})
.style('fill', function(d) { return z(d); });
*/
var circle = svg.append("circle")
.attr("r", 13)
.attr("transform", "translate(" + points[0] + ")");
var movingCircle = circle.transition().duration(4000)
.attrTween('transform', translateAlongPath(path.node()))
// .attr('cx', function(d) { return radius(d) * Math.cos(theta(d))})
// .attr('cy', function(d) { return radius(d) * Math.sin(theta(d))})
function translateAlongPath(path) {
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
var p = path.getPointAtLength(t * l);
//console.log(p)
return "translate(" + p.x + "," + p.y + ")";
};
};
}
function pathXY(path) {
var l = path.getTotalLength();
var start = 0;
/* for(i=start; i<l; i++) {
var point = path.getPointAtLength(i);
svg.append('rect').transition().duration(400).attr('transform', 'translate(' + point.x +','+point.y+')')
.attr('width', 10).attr('height', 30).style('fill', z);
}*/
}
pathXY(path.node());
/*var test = translateAlongPath(path.node())()();
//console.log(test)
var bars = svg.selectAll('.bar')
.data(points).enter().append('rect').transition().duration(2000)
// .attrTween('transform', translateAlongPath(path.node()))
.attr('class', 'bar')
.attr('width', 10)
.attr('height', 20)
.style('fill', function(d) { return z(d)});
*/
var rect = svg.append('rect').attr('width', 10).attr('height', 10);
rect.transition().duration(3400)
.attrTween('transform', translateAlongPath(path.node()));
It'd be great to have a few similar examples (i.e. spiral timeline plot).
Thanks.
Glad you came back and updated your question, because this is an interesting one. Here's a running minimal implementation. I've commented it ok, so let me know if you have any questions...
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#4.0.0" data-semver="4.0.0" src="https://d3js.org/d3.v4.js"></script>
</head>
<body>
<div id="chart"></div>
<script>
var width = 500,
height = 500,
start = 0,
end = 2.25,
numSpirals = 4;
var theta = function(r) {
return numSpirals * Math.PI * r;
};
var r = d3.min([width, height]) / 2 - 40;
var radius = d3.scaleLinear()
.domain([start, end])
.range([40, r]);
var svg = d3.select("#chart").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
// create the spiral, borrowed from http://bl.ocks.org/syntagmatic/3543186
var points = d3.range(start, end + 0.001, (end - start) / 1000);
var spiral = d3.radialLine()
.curve(d3.curveCardinal)
.angle(theta)
.radius(radius);
var path = svg.append("path")
.datum(points)
.attr("id", "spiral")
.attr("d", spiral)
.style("fill", "none")
.style("stroke", "steelblue");
// fudge some data, 2 years of data starting today
var spiralLength = path.node().getTotalLength(),
N = 730,
barWidth = (spiralLength / N) - 1;
var someData = [];
for (var i = 0; i < N; i++) {
var currentDate = new Date();
currentDate.setDate(currentDate.getDate() + i);
someData.push({
date: currentDate,
value: Math.random()
});
}
// here's our time scale that'll run along the spiral
var timeScale = d3.scaleTime()
.domain(d3.extent(someData, function(d){
return d.date;
}))
.range([0, spiralLength]);
// yScale for the bar height
var yScale = d3.scaleLinear()
.domain([0, d3.max(someData, function(d){
return d.value;
})])
.range([0, (r / numSpirals) - 30]);
// append our rects
svg.selectAll("rect")
.data(someData)
.enter()
.append("rect")
.attr("x", function(d,i){
// placement calculations
var linePer = timeScale(d.date),
posOnLine = path.node().getPointAtLength(linePer),
angleOnLine = path.node().getPointAtLength(linePer - barWidth);
d.linePer = linePer; // % distance are on the spiral
d.x = posOnLine.x; // x postion on the spiral
d.y = posOnLine.y; // y position on the spiral
d.a = (Math.atan2(angleOnLine.y, angleOnLine.x) * 180 / Math.PI) - 90; //angle at the spiral position
return d.x;
})
.attr("y", function(d){
return d.y;
})
.attr("width", function(d){
return barWidth;
})
.attr("height", function(d){
return yScale(d.value);
})
.style("fill", "steelblue")
.style("stroke", "none")
.attr("transform", function(d){
return "rotate(" + d.a + "," + d.x + "," + d.y + ")"; // rotate the bar
});
// add date labels
var tF = d3.timeFormat("%b %Y"),
firstInMonth = {};
svg.selectAll("text")
.data(someData)
.enter()
.append("text")
.attr("dy", 10)
.style("text-anchor", "start")
.style("font", "10px arial")
.append("textPath")
// only add for the first of each month
.filter(function(d){
var sd = tF(d.date);
if (!firstInMonth[sd]){
firstInMonth[sd] = 1;
return true;
}
return false;
})
.text(function(d){
return tF(d.date);
})
// place text along spiral
.attr("xlink:href", "#spiral")
.style("fill", "grey")
.attr("startOffset", function(d){
return ((d.linePer / spiralLength) * 100) + "%";
})
</script>
</body>
</html>
Related
I'm trying to create a bar spiral in d3 and I'm using this resource: http://bl.ocks.org/larsenmtl/222043d93a41d48b58d2bfa1e3d4f708
I'm not getting an error, but also I'm just getting a blank page no chart at all. Any guidance on where I'm going wrong would be appreciated. Also the console.table isn't showing data, even though initially it did.
// reading in the data
const dataset = d3.csv("/Journalists_Death.csv").then(function(data) {
console.log(data[0]);
});
//creating spiral chart
var width = 500,
height = 500,
start = 0,
end = 2.25,
numSpirals = 4;
var theta = function(r) {
return numSpirals * Math.PI * r;
};
var r = d3.min([width, height]) / 2 - 40;
var radius = d3.scaleLinear()
.domain([start, end])
.range([40, r]);
var svg = d3.select("#chart").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("tranform", "translate(" + width / 2 + ","+ height / 2 +")");
// create the spiral, borrowed from http://bl.ocks.org/syntagmatic/3543186
var points = d3.range(start, end + 0.001, (end = start) / 1000);
var spiral = d3.radialLine()
.curve(d3.curveCardinal)
.angle(theta)
.radius(radius);
var path = svg.append("path")
.datum(points)
.attr("id", "spiral")
.attr("d", spiral)
.style("fill", "none")
.style("stroke", "steelblue");
//fudge some data, 2 years of data starting today
var spiralLength = path.node().getTotalLength(),
N = 730,
barWidth = (spiralLength / N) - 1;
year = d => d.year
Total = d => d.Total
//here's our time scale that'll run along the spiral
var timeScale = d3.scaleTime() //line 52
.domain(d3.extent(dataset, function(d){
return d.year;
}))
.range([0, spiralLength]);
//yScale for the bar height
var yScale = d3.scaleLinear()
.domain([0, d3.max(dataset, function(d){
return d.Total
})])
.range([0, (r/numSpirals) - 30]);
//append our rects
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", function(d,i){
//placement calculations
var linePer = timeScale(d.year),
posOnLine = path.node().getPointAtLength(linePer),
angleOnLine = path.node().getPointAtLength(linePer - barWidth);
d.linePer = linePer; // % of distance are on the spiral
d.x = posOnLine.x; // x position on the spiral
d.y = posOnLine.y; // y on position on the spiral
d.a = (Math.atan2(angleOnLine.y, angleOnLine.x) * 180 / Math.PI) - 90;
return d.x;
})
.attr("y", function(d){
return d.y;
})
.attr("width", function(d){
return barWidth;
})
.attr("height", function(d){
return yScale(d.Total);
})
.style("fill", "steelblue")
.style("stroke", "none")
.attr("transform", function(d){
return "rotate(" + d.a + "," + d.x + "," + d.y + ")";
});
Here in my d3 radial chart, I am trying to get the label text just above the segment arcs instead of keeping outside the outer circle.
Fiddle
var width = 360,
height = 300,
barHeight = height / 2 - 40;
var formatNumber = d3.format("s");
var color = d3.scale.ordinal()
.range(["#F15D5D","#FAD64B"]);
var svg = d3.select('#chart').append("svg")
.attr("width", width)
.attr("height", height)
.attr('class','radial')
.append("g")
.attr("transform", "translate(" + width/2 + "," + height/2 + ")");
var data = [{
"name": "ABC",
"value":4
},
{
"name": "XYZ",
"value":5
},{
"name": "DEF",
"value":2
},
{
"name": "GHI",
"value":3
},{
"name": "JKL",
"value":1
}];
data.sort(function(a,b) { return b.value - a.value; });
var extent = [0, d3.max(data, d=>d.value)];
var barScale = d3.scale.linear()
.domain(extent)
.range([0, barHeight]);
var keys = data.map(function(d,i) { return d.name; });
var numBars = keys.length;
// X scale
var x = d3.scale.linear()
.domain(extent)
.range([0, -barHeight]);
// X axis
var xAxis = d3.svg.axis()
.scale(x).orient("left")
.ticks(3)
.tickFormat(formatNumber);
// Inner circles
var circles = svg.selectAll("circle")
.data(x.ticks(5))
.enter().append("circle")
.attr("r", function(d) {return barScale(d);})
.style("fill", "none")
//.style("stroke", "black")
//.style("stroke-dasharray", "2,2")
.style("stroke-width",".5px");
// Create arcs
var arc = d3.svg.arc()
.startAngle(function(d,i) {
var a = (i * 2 * Math.PI) / numBars;
var b = ((i + 1) * 2 * Math.PI) / numBars;
var d = (b-a) / 4;
var x = a+d;
var y = b-d;
return x;//(i * 2 * Math.PI) / numBars;
})
.endAngle(function(d,i) {
var a = (i * 2 * Math.PI) / numBars;
var b = ((i + 1) * 2 * Math.PI) / numBars;
var d = (b-a) / 4;
var x = a+d;
var y = b-d;
return y;//((i + 1) * 2 * Math.PI) / numBars;
})
.innerRadius(0);
// Render colored arcs
var segments = svg.selectAll("path")
.data(data)
.enter().append("path")
.each(function(d) { d.outerRadius = 0; })
.style("fill", function (d) { return color(d.name); })
.attr("d", arc);
// Animate segments
segments.transition().ease("elastic").duration(1000).delay(function(d,i) {return (25-i)*50;})
.attrTween("d", function(d,index) {
var i = d3.interpolate(d.outerRadius, barScale(+d.value));
return function(t) { d.outerRadius = i(t); return arc(d,index); };
});
// Outer circle
svg.append("circle")
.attr("r", barHeight)
.classed("outer", true)
.style("fill", "none")
//.style("stroke", "black")
.style("stroke-width",".5px");
// Apply x axis
svg.append("g")
.attr("class", "x axis")
.call(xAxis);
// Labels
var labelRadius = barHeight * 1.025;
var labels = svg.append("g")
.classed("labels", true);
labels.append("def")
.append("path")
.attr("id", "label-path")
.attr("d", "m0 " + -labelRadius + " a" + labelRadius + " " + labelRadius + " 0 1,1 -0.01 0");
labels.selectAll("text")
.data(keys)
.enter().append("text")
.style("text-anchor", "middle")
.style("font-weight","bold")
.style("fill", function(d, i) {return "#555";})
.append("textPath")
.attr("xlink:href", "#label-path")
.attr("startOffset", function(d, i) {return i * 100 / numBars + 50 / numBars + '%';})
.text(function(d) {return d.toUpperCase(); });
We can set the position of the <defs>'s paths using the same data you used to create the arcs.
First, let's create an enter selection:
var labels = svg.selectAll("foo")
.data(data)
.enter()
.append("g")
.classed("labels", true);
Then, we append the paths using the barScale for each value (hardcoded padding of 4px here):
labels.append("def")
.append("path")
.attr("id", (d, i) => "label-path" + i)
.attr("d", d => "m0 " + -(barScale(d.value) + 4) +
" a" + (barScale(d.value) + 4) + " " +
(barScale(d.value) + 4) + " 0 1,1 -0.01 0");
Please notice that we have to use unique IDs. Then, we change the IDs in the text-paths:
.attr("xlink:href", (d, i) => "#label-path" + i)
Here is your updated fiddle: https://jsfiddle.net/qt3e0rex/
And the same code in the Stack snippet:
var width = 360,
height = 300,
barHeight = height / 2 - 40;
var formatNumber = d3.format("s");
var color = d3.scale.ordinal()
.range(["#F15D5D", "#FAD64B"]);
var svg = d3.select('body').append("svg")
.attr("width", width)
.attr("height", height)
.attr('class', 'radial')
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var data = [{
"name": "ABC",
"value": 4
}, {
"name": "XYZ",
"value": 5
}, {
"name": "DEF",
"value": 2
}, {
"name": "GHI",
"value": 3
}, {
"name": "JKL",
"value": 1
}];
data.sort(function(a, b) {
return b.value - a.value;
});
var extent = [0, d3.max(data, d => d.value)];
var barScale = d3.scale.linear()
.domain(extent)
.range([0, barHeight]);
var keys = data.map(function(d, i) {
return d.name;
});
var numBars = keys.length;
// X scale
var x = d3.scale.linear()
.domain(extent)
.range([0, -barHeight]);
// X axis
var xAxis = d3.svg.axis()
.scale(x).orient("left")
.ticks(3)
.tickFormat(formatNumber);
// Inner circles
var circles = svg.selectAll("circle")
.data(x.ticks(5))
.enter().append("circle")
.attr("r", function(d) {
return barScale(d);
})
.style("fill", "none")
//.style("stroke", "black")
//.style("stroke-dasharray", "2,2")
.style("stroke-width", ".5px");
// Create arcs
var arc = d3.svg.arc()
.startAngle(function(d, i) {
var a = (i * 2 * Math.PI) / numBars;
var b = ((i + 1) * 2 * Math.PI) / numBars;
var d = (b - a) / 4;
var x = a + d;
var y = b - d;
return x; //(i * 2 * Math.PI) / numBars;
})
.endAngle(function(d, i) {
var a = (i * 2 * Math.PI) / numBars;
var b = ((i + 1) * 2 * Math.PI) / numBars;
var d = (b - a) / 4;
var x = a + d;
var y = b - d;
return y; //((i + 1) * 2 * Math.PI) / numBars;
})
.innerRadius(0);
// Render colored arcs
var segments = svg.selectAll("path")
.data(data)
.enter().append("path")
.each(function(d) {
d.outerRadius = 0;
})
.style("fill", function(d) {
return color(d.name);
})
.attr("d", arc);
// Animate segments
segments.transition().ease("elastic").duration(1000).delay(function(d, i) {
return (25 - i) * 50;
})
.attrTween("d", function(d, index) {
var i = d3.interpolate(d.outerRadius, barScale(+d.value));
return function(t) {
d.outerRadius = i(t);
return arc(d, index);
};
});
// Outer circle
svg.append("circle")
.attr("r", barHeight)
.classed("outer", true)
.style("fill", "none")
//.style("stroke", "black")
.style("stroke-width", ".5px");
// Apply x axis
svg.append("g")
.attr("class", "x axis")
.call(xAxis);
// Labels
var labelRadius = barHeight * 1.025;
var labels = svg.selectAll("foo")
.data(data)
.enter()
.append("g")
.classed("labels", true);
labels.append("def")
.append("path")
.attr("id", (d, i) => "label-path" + i)
.attr("d", d => "m0 " + -(barScale(d.value) + 4) + " a" + (barScale(d.value) + 4) + " " + (barScale(d.value) + 4) + " 0 1,1 -0.01 0");
labels.append("text")
.style("text-anchor", "middle")
.style("font-weight", "bold")
.style("fill", function(d, i) {
return "#555";
})
.append("textPath")
.attr("xlink:href", (d, i) => "#label-path" + i)
.attr("startOffset", function(d, i) {
return i * 100 / numBars + 50 / numBars + '%';
})
.text(function(d) {
return d.name.toUpperCase();
});
body {
background-color:#fff;
}
.axis path, .axis line {
fill: none;
shape-rendering: crispEdges;
}
.x.axis path {
display: none;
}
.tick text, line {
display:none;
}
circle {
stroke:#ccc;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
I have a d3 layout that is intended to produce 3 charts:
Custom item layout w/ transitions
Pie chart for sales by category (with transitions at some point)
Bar chart for top 5 performing items w/ transitions.
1 & 2 work OK but when I add the third chart, I see some strange behavior. The intention is to create a bar chart where the width of each bar is tied to the sales metric for the top N items determined like so:
data = data.filter(function(d){ return d.date === someDate});
var cf = crossfilter(data);
var salesDimension = cf.dimension(function(d){ return d.sales; });
topData = salesDimension.top(5);
The problem is that instead of drawing the bar chart, my code somehow overwrites the position of 5 items in chart 1 and moves them back to the origin. If I change 5 to 10 above then 10 items are overwritten and so on.
I double checked the scope of my variables and even tried changing the names of everything in my drawTopItems() which made no difference. I suspect that I am doing something incorrectly when it comes to selecting the svg element or applying classes to the svg group elements that I want to modify but I can't for the life of me see what. Can anyone tell me what I might be doing wrong?
Here is my issue in a fiddle: https://jsfiddle.net/Sledge/4eggpd5e/12/.
Here is my javascript code:
var item_width = 40, item_height = 60;
var margin = {top: 50, right: 50, bottom: 75, left: 40},
width = 700 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scaleLinear().range([0, width]);
var y = d3.scaleLinear().range([0, height]);
var colorScale = d3.scaleLinear().domain([500,3000]).range(["white","#4169e1"]);
// Pie Chart parameters
var pieWidth = 300, pieHeight = 300;
var outerRadius = Math.min(pieWidth, pieHeight) / 2,
innerRadius = outerRadius * .50;
var pieColor = d3.scaleOrdinal(['#42b9f4','#3791f2','#374ff1','#25b22e','#107222']); // custom color scale
var legendRectSize = 18; // NEW
var legendSpacing = 4; // NEW
// Top Item Parameters
var topItemMargin = {top:25, right:25, bottom: 25, left: 25};
var topItemWidth = 300 - topItemMargin.left - topItemMargin.right,
topItemHeight = 300 - topItemMargin.top - topItemMargin.bottom;
var topItemXScale = d3.scaleLinear().range([0, topItemWidth]);
var barHeight = 20, barSpacing = 5;
/* SVG */
var svgItemLayout = d3.select("#item_layout")
.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 + ")");
var svgPieChart = d3.select("#pie_chart")
.append("svg")
.attr("width", pieWidth)
.attr("height", pieHeight)
.append("g")
.attr("transform", "translate(" + outerRadius + "," + outerRadius + ")") ;
var svgTopItems = d3.select("#top_items")
.append("svg")
.attr("width", topItemWidth)
.attr("height", topItemHeight)
.append("g")
.attr("transform", "translate(" + topItemMargin.left + "," + topItemMargin.top + ")");
/* DRAW FUNCTIONS */
// a single function to draw
function drawItemLayout(data, someDate){
data.forEach(function(d) {
d.x_pos = +d.x_pos;
d.y_pos = +d.y_pos;
d.sales = +d.sales;
});
// pre-filter data
data = data.filter(function(d){ return d.date === someDate});
var x_offset = 5, y_offset = 5;
x.domain(d3.extent(data, function(d) { return d.x_pos; })); // set the x domain
y.domain(d3.extent(data, function(d) { return d.y_pos; })); // set the y domain
// create an update selection with a key function
var g_sel = svgItemLayout.selectAll("g")
.data(data, function(d){
return d.item_name;
});
// get rid of those leaving the update
g_sel.exit().remove();
// our entering g
var g_ent = g_sel.enter()
.append("g");
// add our rects to our g
g_ent.append("rect")
.attr("class", "dot") // wonder if I really need this class?
.attr("width", item_width)
.attr("height", item_height)
.attr("rx", 3)
.attr("ry", 3)
.style("fill", function(d){ return colorScale(d.sales); }) // color factor variable
.style("fill-opacity", 0.5);
// add our text to our g
g_ent.append("text")
.attr("font-size", 10)
.attr("text-anchor", "middle")
.attr("fill", "black")
.attr("dx", item_width/2)
.attr("dy", item_height/2)
.text(function(d){ return d.item_name; });
// UPDATE + ENTER selection
g_sel = g_ent.merge(g_sel);
// move them into position with transition
g_sel
.transition()
.duration(1200)
.delay(function(d, i) { return i *40; })
.attr("transform", function(d){
return "translate(" + (x(d.x_pos) + x_offset) + "," + (y(d.y_pos) + y_offset) + ")";
});
}
function drawPieChart(data, someDate) {
data.forEach(function(d) {
d.x_pos = +d.x_pos;
d.y_pos = +d.y_pos;
d.sales = +d.sales;
});
// pre-filter data
data = data.filter(function(d){ return d.date === someDate});
var cf = crossfilter(data);
var salesDimension = cf.dimension(function(d){ return d.sales; });
var categoryDimension = cf.dimension(function(d){ return d.category; });
var categoryGroup = categoryDimension.group();
function reduceInitial(p, v) {
return {
sales : 0,
count : 0
};
}
function reduceAdd(p, v) {
p.sales = p.sales + v.sales;
p.count = p.count + 1;
return p;
}
function reduceRemove(p, v) {
p.sales = p.sales - v.sales;
p.count = p.count - 1;
return p;
}
categoryAggregated = categoryGroup.reduce(reduceAdd, reduceRemove, reduceInitial).all();
var arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
var pie = d3.pie()
.value(function(d) { return d.value.sales; })
.sort(null);
var path = svgPieChart.selectAll('path')
.data(pie(categoryAggregated))
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d, i) { return pieColor(i);});
// Add a legend:
var legend = svgPieChart.selectAll('.legend')
.data(pieColor.domain())
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function(d, i) {
var height = legendRectSize + legendSpacing;
var offset = height * pieColor.domain().length / 2;
var horz = -3 * legendRectSize;
var vert = i * height - offset;
return 'translate(' + horz + ',' + vert + ')';
});
legend.append('rect')
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.style('fill', pieColor)
.style('stroke', pieColor);
legend.append('text')
.attr('x', legendRectSize + legendSpacing)
.attr('y', legendRectSize - legendSpacing)
.text(function(d) { return categoryAggregated[d].key; }); // returns text based on data index
}
function drawTopItems (data, someDate) {
data.forEach(function(d) {
d.x_pos = +d.x_pos;
d.y_pos = +d.y_pos;
d.sales = +d.sales;
});
// pre-filter data
data = data.filter(function(d){ return d.date === someDate});
var cf = crossfilter(data);
var salesDimension = cf.dimension(function(d){ return d.sales; });
topData = salesDimension.top(5);
topItemXScale.domain(d3.extent(topData, function(d) { return d.sales; })); // set the x domain
var f_sel = svgTopItems.selectAll("g")
.data(topData,function(d){ return d.item_name; }).enter();
f_sel.exit().remove();
var f_ent = f_sel.enter().append("g");
f_ent.append("rect")
.attr("class", "dot") // wonder if I really need this class?
.attr("width", function(d){ return d.sales })
.attr("height", barHeight)
.style("fill","#351eff") // color factor variable
.style("fill-opacity", 0.75);
// add our text to our g
f_ent.append("text")
.attr("font-size", 10)
.attr("text-anchor", "left")
.attr("fill", "black")
.attr("dx", item_width/2)
.attr("dy", item_height/2)
.text(function(d){ return d.item_name});
// UPDATE + ENTER selection
f_sel = f_ent.merge(f_sel);
f_sel.transition()
.duration(1200)
.delay(function(d, i) { return i *40; })
.attr("transform", function(d, i){
return "translate( 0, "+ i*25 +")" + ")";
});
}
/* MAIN */
var data = d3.csvParse( d3.select("pre#data").text());
drawItemLayout(data, '1-20-2017');
drawPieChart(data, '1-20-2017');
drawTopItems(data, '1-20-2017');
/* UPDATE DATA */
function updateData(date) {
//d3.csv("http://localhost:8080/udacity_test_vis_1/output_fixture_data.csv", function(data) {
var data = d3.csvParse( d3.select("pre#data").text());
drawItemLayout (data, date);
drawPieChart(data, date);
drawTopItems(data, date);
}
/* GET SELECTION */
$("#select_params").change(function(){
var date = $("#select_params :selected").val();
console.log(date);
updateData(date);
})
Just three problems:
You are repeating the enter() function:
var f_sel = svgTopItems.selectAll("g")
.data(topData,function(d){ return d.item_name; }).enter();
//this is an enter selection...
var f_ent = f_sel.enter().append("g");
//and you have enter() again here
So, remove the first enter: f_sel should be just the data-binding selection.
Move your merged selection to before appending the rectangles
Your translate has an extra parenthesis:
return "translate( 0, "+ i*25 +")" + ")";
With that problems corrected, this is your updated fiddle: https://jsfiddle.net/utf5hva2/
I was trying to break long lines of y-axis labels. My bar chart is like this but with longer labels (usually more than 10 words with spaces). This is my code (it works and I see the chart, the only 'error' is the text, i load the JSON dinamically, this data is only for SO):
HTML:
<svg id='table1'></svg>
<script>
var window_width = $(window).width();
var main_width = (window_width / 100 ) * 80;
var w = main_width - 400;
var data = {
labels: [
'the text to be splitted 1', 'the text to be splitted 2'
],
series: [
{
label: '2012',
values: [4, 8]
},
{
label: '2013',
values: [12, 43]
}
]
};
var chartWidth = w,
barHeight = 25,
groupHeight = barHeight * data.series.length,
gapBetweenGroups = 10,
spaceForLabels = 175,
spaceForLegend = 0;
// Zip the series data together (first values, second values, etc.)
var zippedData = [];
for (var i=0; i<data.labels.length; i++) {
for (var j=0; j<data.series.length; j++) {
zippedData.push(data.series[j].values[i]);
}
}
var chartHeight = barHeight * zippedData.length + gapBetweenGroups * data.labels.length;
var x = d3.scale.linear()
.domain([0, d3.max(zippedData)])
.range([0, chartWidth]);
var y = d3.scale.linear()
.range([chartHeight + gapBetweenGroups, 0]);
var yAxis = d3.svg.axis()
.scale(y)
.tickFormat('')
.tickSize(0)
.orient("left");
// Specify the chart area and dimensions
var chart = d3.select("#table"+ambit)
.attr("width", spaceForLabels + chartWidth + spaceForLegend)
.attr("height", chartHeight);
// Create bars
var bar = chart.selectAll("g")
.data(zippedData)
.enter().append("g")
.attr("transform", function(d, i) {
return "translate(" + spaceForLabels + "," + (i * barHeight + gapBetweenGroups * (0.5 + Math.floor(i/data.series.length))) + ")";
});
// Create rectangles of the correct width
bar.append("rect")
.attr("fill", function(d,i) { return color(i % data.series.length); })
.attr("class", "bar")
.attr("width", x)
.attr("height", barHeight - 1);
// Add text label in bar
bar.append("text")
.attr("class", "label_txt1")
.attr("x", function(d) { return x(d) - 3; })
.attr("y", barHeight / 2)
.attr("fill", "red")
.attr("dy", ".35em")
.text(function(d) { return d; });
// Draw labels
bar.append("text")
.attr("class", "label_txt")
.attr("x", function(d) { return - 10; })
.attr("y", groupHeight / 2)
.attr("dy", ".35em")
.text(function(d,i) {
if (i % data.series.length === 0)
return data.labels[Math.floor(i/data.series.length)];
else
return ""});
chart.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + spaceForLabels + ", " + -gapBetweenGroups/2 + ")")
.call(yAxis);
</script>
I follow multiple examples to solve this, like:
How do I include newlines in labels in D3 charts?
and Adding a line break to D3 graph y-axis labels and Wrapping Long Labels.
First attempt:
var insertLinebreaks = function (d) {
var el = d3.select(this);
var words = d.split(' ');
el.text('');
for (var i = 0; i < words.length; i++) {
var tspan = el.append('tspan').text(words[i]);
if (i > 0)
tspan.attr('x', 0).attr('dy', '15');
}
};
chart.selectAll('g.y.axis text').each(insertLinebreaks);
and:
bar.selectAll('.label_txt1').each(insertLinebreaks);
Also I tried:
bar.append("text")
.attr("class", "label_txt1")
.attr("x", function(d) { return x(d) - 3; })
.attr("y", barHeight / 2)
.attr("fill", "red")
.attr("dy", ".35em")
.text(function(d) { return d; });
chart.selectAll(".label_txt1").call(wrap, 40)
function wrap(text, width) {
alert(JSON.stringify(text));
text.each(function() {
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
y = text.attr("y"),
dy = parseFloat(text.attr("dy")),
lineHeight = 1.1, // ems
tspan = text.text(null).append("tspan").attr("x", function(d) { return d.children || d._children ? -10 : 10; }).attr("y", y).attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
var textWidth = tspan.node().getComputedTextLength();
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
++lineNumber;
tspan = text.append("tspan").attr("x", function(d) { return d.children || d._children ? -10 : 10; }).attr("y", 0).attr("dy", lineNumber * lineHeight + dy + "em").text(word);
}
}
});
}
I changed text() to html() as I read at: How do I include newlines in labels in D3 charts?
In the browser I see the <text> created with class label_txt1. When I try to select the text with selectAll(".label_txt1") I get the values of the line, not the text.
Can someone help me?
This example that you reference is the canonical way to do wrap text when you are using an svg text node. You are simply calling it wrong (on the wrong class, you are wrapping the numbers). Simplify this to:
// Draw labels
bar.append("text")
.attr("class", "label_txt")
.attr("x", function(d) {
return -10;
})
.attr("y", groupHeight / 2) //<-- not sure you need this
.attr("dy", ".35em")
.text(function(d, i) {
if (i % data.series.length === 0)
return data.labels[Math.floor(i / data.series.length)];
else
return ""
})
.call(wrap, 40);
Full code:
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#3.5.3" data-semver="3.5.3" src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.3/d3.js"></script>
</head>
<body>
<svg id="table1"></svg>
<script>
var window_width = 1000;
var main_width = 400;
var w = 400;
var data = {
labels: [
'the text to be splitted 1', 'the text to be splitted 2'
],
series: [{
label: '2012',
values: [4, 8]
}, {
label: '2013',
values: [12, 43]
}]
};
var chartWidth = w,
barHeight = 25,
groupHeight = barHeight * data.series.length,
gapBetweenGroups = 10,
spaceForLabels = 175,
spaceForLegend = 0;
var color = d3.scale.category10();
// Zip the series data together (first values, second values, etc.)
var zippedData = [];
for (var i = 0; i < data.labels.length; i++) {
for (var j = 0; j < data.series.length; j++) {
zippedData.push(data.series[j].values[i]);
}
}
var chartHeight = barHeight * zippedData.length + gapBetweenGroups * data.labels.length;
var x = d3.scale.linear()
.domain([0, d3.max(zippedData)])
.range([0, chartWidth]);
var y = d3.scale.linear()
.range([chartHeight + gapBetweenGroups, 0]);
var yAxis = d3.svg.axis()
.scale(y)
.tickFormat('')
.tickSize(0)
.orient("left");
// Specify the chart area and dimensions
var chart = d3.select("#table1")
.attr("width", spaceForLabels + chartWidth + spaceForLegend)
.attr("height", chartHeight);
// Create bars
var bar = chart.selectAll("g")
.data(zippedData)
.enter().append("g")
.attr("transform", function(d, i) {
return "translate(" + spaceForLabels + "," + (i * barHeight + gapBetweenGroups * (0.5 + Math.floor(i / data.series.length))) + ")";
});
// Create rectangles of the correct width
bar.append("rect")
.attr("fill", function(d, i) {
return color(i % data.series.length);
})
.attr("class", "bar")
.attr("width", x)
.attr("height", barHeight - 1);
// Add text label in bar
bar.append("text")
.attr("class", "label_txt1")
.attr("x", function(d) {
return x(d) - 3;
})
.attr("y", barHeight / 2)
.attr("fill", "red")
.attr("dy", ".35em")
.text(function(d) {
return d;
});
// Draw labels
bar.append("text")
.attr("class", "label_txt")
.attr("x", function(d) {
return -10;
})
//.attr("y", groupHeight / 2) //<-- you don't need this...
.attr("dy", ".35em")
.text(function(d, i) {
if (i % data.series.length === 0)
return data.labels[Math.floor(i / data.series.length)];
else
return ""
})
.call(wrap, 40);
function wrap(text, width) {
text.each(function() {
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
y = text.attr("y"),
dy = parseFloat(text.attr("dy")),
lineHeight = 1.1, // ems
tspan = text.text(null).append("tspan").attr("x", function(d) {
return d.children || d._children ? -10 : 10;
}).attr("y", y).attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
var textWidth = tspan.node().getComputedTextLength();
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
++lineNumber;
tspan = text.append("tspan").attr("x", function(d) {
return d.children || d._children ? -10 : 10;
}).attr("y", 0).attr("dy", lineNumber * lineHeight + dy + "em").text(word);
}
}
});
}
chart.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + spaceForLabels + ", " + -gapBetweenGroups / 2 + ")")
.call(yAxis);
</script>
</body>
</html>
I am creating an arc diagram where I'd like to, hopefully, find a way to prevent the overlap of arcs. There's an example of the working bl.ock here.
The darker lines in this case are overlapping lines where multiple nodes share the same edge. I'd like to prevent that, perhaps by doing two passes: the first would alternate the arc to go above the nodes rather than below, giving a sort of helix appearance; the second would draw a slightly larger arc if an arc already exists above/below to help differentiate the links.
var width = 1000,
height = 500,
margin = 20,
pad = margin / 2,
radius = 6,
yfixed = pad + radius;
var color = d3.scale.category10();
// Main
//-----------------------------------------------------
function arcDiagram(graph) {
var radius = d3.scale.sqrt()
.domain([0, 20])
.range([0, 15]);
var svg = d3.select("#chart").append("svg")
.attr("id", "arc")
.attr("width", width)
.attr("height", height);
// create plot within svg
var plot = svg.append("g")
.attr("id", "plot")
.attr("transform", "translate(" + pad + ", " + pad + ")");
// fix graph links to map to objects
graph.links.forEach(function(d,i) {
d.source = isNaN(d.source) ? d.source : graph.nodes[d.source];
d.target = isNaN(d.target) ? d.target : graph.nodes[d.target];
});
linearLayout(graph.nodes);
drawLinks(graph.links);
drawNodes(graph.nodes);
}
// layout nodes linearly
function linearLayout(nodes) {
nodes.sort(function(a,b) {
return a.uniq - b.uniq;
})
var xscale = d3.scale.linear()
.domain([0, nodes.length - 1])
.range([radius, width - margin - radius]);
nodes.forEach(function(d, i) {
d.x = xscale(i);
d.y = yfixed;
});
}
function drawNodes(nodes) {
var gnodes = d3.select("#plot").selectAll("g.node")
.data(nodes)
.enter().append('g');
var nodes = gnodes.append("circle")
.attr("class", "node")
.attr("id", function(d, i) { return d.name; })
.attr("cx", function(d, i) { return d.x; })
.attr("cy", function(d, i) { return d.y; })
.attr("r", 5)
.style("stroke", function(d, i) { return color(d.gender); });
nodes.append("text")
.attr("dx", function(d) { return 20; })
.attr("cy", ".35em")
.text(function(d) { return d.name; })
}
function drawLinks(links) {
var radians = d3.scale.linear()
.range([Math.PI / 2, 3 * Math.PI / 2]);
var arc = d3.svg.line.radial()
.interpolate("basis")
.tension(0)
.angle(function(d) { return radians(d); });
d3.select("#plot").selectAll(".link")
.data(links)
.enter().append("path")
.attr("class", "link")
.attr("transform", function(d,i) {
var xshift = d.source.x + (d.target.x - d.source.x) / 2;
var yshift = yfixed;
return "translate(" + xshift + ", " + yshift + ")";
})
.attr("d", function(d,i) {
var xdist = Math.abs(d.source.x - d.target.x);
arc.radius(xdist / 2);
var points = d3.range(0, Math.ceil(xdist / 3));
radians.domain([0, points.length - 1]);
return arc(points);
});
}
Any pointers on how I might start approaching the problem?
Here is a bl.ock for reference. It shows your original paths in gray, and the proposed paths in red.
First store the counts for how many times a given path occurs:
graph.links.forEach(function(d,i) {
var pathCount = 0;
for (var j = 0; j < i; j++) {
var otherPath = graph.links[j];
if (otherPath.source === d.source && otherPath.target === d.target) {
pathCount++;
}
}
d.pathCount = pathCount;
});
Then once you have that data, I would use an ellipse instead of a radial line since it appears the radial line can only draw a curve for a circle:
d3.select("#plot").selectAll(".ellipse-link")
.data(links)
.enter().append("ellipse")
.attr("fill", "transparent")
.attr("stroke", "gray")
.attr("stroke-width", 1)
.attr("cx", function(d) {
return (d.target.x - d.source.x) / 2 + radius;
})
.attr("cy", pad)
.attr("rx", function(d) {
return Math.abs(d.target.x - d.source.x) / 2;
})
.attr("ry", function(d) {
return 150 + d.pathCount * 20;
})
.attr("transform", function(d,i) {
var xshift = d.source.x - radius;
var yshift = yfixed;
return "translate(" + xshift + ", " + yshift + ")";
});
Note that changing the value for ry above will change the heights of different curves.
Finally you'll have to use a clippath to restrict the area of each ellipse that's actually shown, so that they only display below the nodes. (This is not done in the bl.ock)