Application of date formatting on x-axis text - javascript

I'm trying to apply d3.time.format("%b-%Y") to the dates used on the x-axis.
Here is the code which adds the axis and labels
var xLabels = svg
.append("g")
.attr("transform", "translate(" + margin.left + "," + (margin.top + height + 12) + ")");
var formatDateOutputX = d3.time.format("%b-%Y");
xLabels.selectAll("text.xAxis")
.data(BarData)
.enter()
.append("text")
.text(function(d) {
return d.dt; //<< returns dates
//return formatDateOutputX(d.dt); //<< NOTHING RETURNED
})
.attr({
'text-anchor': "middle",
transform: function(d, i) {
var x = (i * (width / BarData.length)) + ((width / BarData.length - barPadding) / 2);
var y = 20;
return 'translate(' + x + ',' + y + ')rotate(-90)';
},
dy: "0.35em", //dx: "-1.05em",
'class': "xAxis"
});
The above relates to the lines of code 285-309 of this visualisation: https://plnkr.co/edit/3d5UhM?p=preview
Hoping someone can help as this will be a fairly common manipulation that I will want to apply. What am I doing wrong?

You are not passing a valid date to formatDateOutputX so it doesn't know how to process it. You have to parse the value appropriately and you can do it in many ways. Since d3 provides a parse method to time.format that is probably the best way to follow.
In your case something like this will work:
var formatDateOutputX = d3.time.format("%b-%Y");
var readDate = d3.time.format("%d/%m/%Y").parse;
xLabels.selectAll("text.xAxis")
.data(BarData)
.enter()
.append("text")
.text(function(d) {
return formatDateOutputX(readDate(d.dt));
})
Check the Plunkr.

Related

Disable D3 zoom on circle PNG background

I am trying to disable the D3 zoom on a particular element. This element happens to be the PNG background to a circle.
Right now this is not working. I have tried to offset the scale parameter in the zoom, but the background PNG still 'grows' with the circle. Here is my jsfiddle.
This is how I try to offset the zoom:
d3.selectAll("#grump_avatar").attr("transform", "scale(" + 1/d3.event.scale + ")");
I know there are similar questions on SO, but please note none of them have received a satisfactory response thus far. Better luck here, hopefully.
Lots of issues with this code:
Matching by id is an exact match.
Your ids are on def attributes, which aren't the objects, you don't want to scale (those would be the circles).
To match multiple objects, you should be using a class on the circles.
You apply the zoom directly to the svg, you should be wrapping everything in a g. SVG handles the events, g is the zoomable "canvas".
Once you apply the zoom correctly you are going to lose your circle placement because you overwrite the transform without reapplying the translate.
You've made no use of d3 data-binding, so you can't persist your data correctly.
All this in mind, here is how I would refactor your code:
var config = {
"avatar_size": 100
}
var body = d3.select("body");
var svg = body.append("svg")
.attr("width", 500)
.attr("height", 500);
var g = svg.append("g");
var defs = svg.append('svg:defs');
data = [{
posx: 100,
posy: 100,
img: "https://cdn0.iconfinder.com/data/icons/flat-round-system/512/android-128.png",
}, {
posx: 200,
posy: 200,
img: "https://cdn1.iconfinder.com/data/icons/social-media-set/24/Reverbnation-128.png"
}, {
posx: 300,
posy: 300,
img: "https://cdn1.iconfinder.com/data/icons/user-pictures/100/male3-128.png"
}];
defs.selectAll("pattern")
.data(data)
.enter()
.append("pattern")
.attr("id", (d, i) => "grump_avatar" + i)
.attr("width", config.avatar_size)
.attr("height", config.avatar_size)
.attr("patternUnits", "userSpaceOnUse")
.append("svg:image")
.attr("xlink:href", (d) => d.img)
.attr("width", config.avatar_size)
.attr("height", config.avatar_size)
.attr("x", 0)
.attr("y", 0);
g.selectAll(".grump_avatar")
.data(data)
.enter()
.append("circle")
.attr("class", "grump_avatar")
.attr("transform", (d) => "translate(" + d.posx + "," + d.posy + ")")
.attr("cx", config.avatar_size / 2)
.attr("cy", config.avatar_size / 2)
.attr("r", config.avatar_size / 2)
.style("fill", "white")
.style("fill", (d, i) => "url(#grump_avatar" + i + ")");
var zoom = d3.behavior.zoom()
.on("zoom", function() {
g.attr('transform', 'translate(' + d3.event.translate + ') scale(' + d3.event.scale + ')');
d3.selectAll(".grump_avatar").attr("transform", (d) => {
return "scale(" + 1 / d3.event.scale + ")" + "translate(" + (d.posx - d3.event.translate[0]) + "," + (d.posy - d3.event.translate[1]) + ")";
});
});
svg.call(zoom);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
EDITS based on comments:
To scale the circles opposite the zoom and position them, the key is:
d3.selectAll("circle")
.attr("transform", function(d){
return 'scale(' + 1 / d3.event.scale + ')'; // inverse of scale for size
})
.attr("cx", function(d){
return d.x * d3.event.scale; // change position based on scale, d.x is the original unscaled position
})
.attr("cy", function(d){
return d.y * d3.event.scale;
});

Rotation transformation of labels on x-axis

Hello I have this visual: https://plnkr.co/edit/H6M1xoS9cZv5dKCTIyid?p=preview
I'm attempting to rotate the labels of the x-axis and have tried amending the code in lines 299-317 to no avail - so I feel code such as .attr("transform", "rotate(-65)" ); needs adding to the following?
// Add x labels to chart
var xLabels = svg
.append("g")
.attr("transform", "translate(" + margin.left + "," + (margin.top + height) + ")");
xLabels.selectAll("text.xAxis")
.data(BarData)
.enter()
.append("text")
.text(function(d) {
return d.dt;
})
.attr("text-anchor", "middle")
// Set x position to the left edge of each bar plus half the bar width
.attr("x", function(d, i) {
return (i * (width / BarData.length)) + ((width / BarData.length - barPadding) / 2);
})
.attr("y", 15)
.attr("class", "xAxis")
Your issue is that you are using the .attr("x" to position the axis text. You should be using translate to do this otherwise your rotation will rotate all the elements from the bottom left.
the code should look like this, the translate and the rotation should occur within the same transform function:
.attr("transform", function (d, i) {
return "translate("
+ ((i * (width / BarData.length)) + ((width / BarData.length - barPadding) / 2))
+ ", 0) rotate(-65)";
})
Here is a working version of your plunk: https://plnkr.co/edit/UqwtLqTn6iJ2XS012Vr4?p=preview
Hope this helps.
With links help I eventually used the following code:
xLabels.selectAll("text.xAxis")
.data(BarData)
.enter()
.append("text")
.text(function(d) {
return d.dt;
})
.attr({
'text-anchor': "middle",
transform: function(d, i) {
var x = (i * (width / BarData.length)) + ((width / BarData.length - barPadding) / 2);
var y = 20;
return 'translate(' + x + ',' + y + ')rotate(-90)';
},
dy: "0.35em",
'class': "xAxis"
})
Working example here: https://plnkr.co/edit/3d5UhM?p=preview
I also added in the attribute dy: "0.35em" to fully centre the labels to each bar.

How to pass string as a argument to d3.csv() function and process further for plotting heatmap in d3.js?

I want to plot histogram using csv data using d3.js. I'm referring this link. In that tutorial they are using local file csv data but in my application data comes from the web-socket so, I'm thinking that I can collect this data and create one string as a csv data and pass this string to the d3.csv or d3.csv.parse function. meanwhile, I'm getting stuck at one point because my data is not properly populating over web-page.
Web-socket server is sending this file data line by line :
date,bucket,count
1468332421041000,40000,2
1468332421102000,30000,6
1468332421103000,30000,8
1468332421116000,30000,10
1468332421117000,30000,2
1468332421139000,30000,2
1468332421155000,50000,2
1468332421158000,40000,2,
1468332421164000,30000,12
This date is in epoch so, In the client side I need to convert it in human readable format.
Client html program -
<!DOCTYPE html>
<html>
<meta charset="utf-8">
<style>
.label {
font-weight: bold;
}
.tile {
shape-rendering: crispEdges;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
</style>
<head>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
</head>
<body>
<p id="demo"></p>
<script>
var csv_data = '';
var flag = true;
if ("WebSocket" in window)
{
// Let us open a web socket
var ws = new WebSocket("ws://localhost:3333");
ws.onopen = function()
{
};
ws.onmessage = function (evt)
{
var received_msg = evt.data;
if(flag)
{
csv_data = evt.data +"\n"
}
else
{
var b = received_msg.substring(17)
var a = received_msg.substring(0,13)
var dateVal = a;
var date = new Date(parseFloat(dateVal));
csv_data += date.getFullYear() + "-" +
(date.getMonth() + 1) + "-" +
date.getDate() + " " +
date.getHours() + ":" +
date.getMinutes() + ":" +
date.getSeconds() +" "+
date.getMilliseconds() +","+b+"\n"
}
flag = false;
readCsv();
};
ws.onclose = function()
{
alert("Connection is closed...");
};
}
else
{
// The browser doesn't support WebSocket
alert("WebSocket NOT supported by your Browser!");
}
var margin = {top: 20, right: 90, bottom: 30, left: 50},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var parseDate = d3.time.format("%Y-%m-%d").parse,
formatDate = d3.time.format("%b %d");
var x = d3.time.scale().range([0, width]),
y = d3.scale.linear().range([height, 0]),
z = d3.scale.linear().range(["white", "steelblue"]);
// The size of the buckets in the CSV data file.
// This could be inferred from the data if it weren't sparse.
var xStep = 864e5,
yStep = 100;
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 + ")");
function readCsv()
{
d3.csv(csv_data, function(buckets) {
// Coerce the CSV data to the appropriate types.
buckets.forEach(function(d) {
d.date = parseDate(d.date);
d.bucket = +d.bucket;
d.count = +d.count;
});
// Compute the scale domains.
x.domain(d3.extent(buckets, function(d) { return d.date; }));
y.domain(d3.extent(buckets, function(d) { return d.bucket; }));
z.domain([0, d3.max(buckets, function(d) { return d.count; })]);
// Extend the x- and y-domain to fit the last bucket.
// For example, the y-bucket 3200 corresponds to values [3200, 3300].
x.domain([x.domain()[0], +x.domain()[1] + xStep]);
y.domain([y.domain()[0], y.domain()[1] + yStep]);
// Display the tiles for each non-zero bucket.
// See http://bl.ocks.org/3074470 for an alternative implementation.
svg.selectAll(".tile")
.data(buckets)
.enter().append("rect")
.attr("class", "tile")
.attr("x", function(d) { return x(d.date); })
.attr("y", function(d) { return y(d.bucket + yStep); })
.attr("width", x(xStep) - x(0))
.attr("height", y(0) - y(yStep))
.style("fill", function(d) { return z(d.count); });
// Add a legend for the color values.
var legend = svg.selectAll(".legend")
.data(z.ticks(6).slice(1).reverse())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(" + (width + 20) + "," + (20 + i * 20) + ")"; });
legend.append("rect")
.attr("width", 20)
.attr("height", 20)
.style("fill", z);
legend.append("text")
.attr("x", 26)
.attr("y", 10)
.attr("dy", ".35em")
.text(String);
svg.append("text")
.attr("class", "label")
.attr("x", width + 20)
.attr("y", 10)
.attr("dy", ".35em")
.text("Count");
// Add an x-axis with label.
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.svg.axis().scale(x).ticks(d3.time.days).tickFormat(formatDate).orient("bottom"))
.append("text")
.attr("class", "label")
.attr("x", width)
.attr("y", -6)
.attr("text-anchor", "end")
.text("Date");
// Add a y-axis with label.
svg.append("g")
.attr("class", "y axis")
.call(d3.svg.axis().scale(y).orient("left"))
.append("text")
.attr("class", "label")
.attr("y", 6)
.attr("dy", ".71em")
.attr("text-anchor", "end")
.attr("transform", "rotate(-90)")
.text("Latency");
});
}
</script>
</body>
</html>
I want to know how I can provide string to parse as a csv data to d3.csv function and after processing it what changes I want to do further? Because In that tutorial date is only in the format i.e. yyyy-mm-dd But In my scenario date is in (after converting to human readable format) i.e. yyyy-mm-dd hh:mm:ss mmm format.

d3 TypeError: Cannot read property 'length' of undefined

Hi I am using a large json file in d3, about 75 KB. It seems to work for the 32 data objects but then I get the error in the console Cannot read property 'length' of undefined. It seems like my json is ok since I put it in http://jsonlint.com/ and it validated. I know similar questions have been asked here but I'm new to d3 so don't know how to modify the code. I think it may have something to do with how d3 is getting the data from my json file.
Here is d3 code in its entirety:
function truncate(str, maxLength, suffix) {
if(str.length > maxLength) {
str = str.substring(0, maxLength + 1);
str = str.substring(0, Math.min(str.length, str.lastIndexOf(" ")));
str = str + suffix;
}
return str;
}
var margin = {top: 20, right: 200, bottom: 0, left: 20},
width = 300,
height = 650;
var start_year = 2004,
end_year = 2013;
var c = d3.scale.category20c();
var x = d3.scale.linear()
.range([0, width]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("top");
var formatYears = d3.format("0000");
xAxis.tickFormat(formatYears);
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.style("margin-left", margin.left + "px")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.json("data.json", function(error, data) {
if (error) throw error;
x.domain([start_year, end_year]);
var xScale = d3.scale.linear()
.domain([start_year, end_year])
.range([0, width]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + 0 + ")")
.call(xAxis);
console.log(data.length);
var len = data.length;
for (var j = 0; j < len; j++) {
// try{
var g = svg.append("g").attr("class","journal");
var circles = g.selectAll("circle")
.data(data[j]['articles'])
.enter()
.append("circle");
var text = g.selectAll("text")
.data(data[j]['articles'])
.enter()
.append("text");
var rScale = d3.scale.linear()
.domain([0, d3.max(data[j]['articles'], function(d) { return d[1]; })])
.range([2, 9]);
circles
.attr("cx", function(d, i) { return xScale(d[0]); })
.attr("cy", j*20+20)
.attr("r", function(d) { return rScale(d[1]); })
.style("fill", function(d) { return c(j); });
text
.attr("y", j*20+25)
.attr("x",function(d, i) { return xScale(d[0])-5; })
.attr("class","value")
.text(function(d){ return d[1]; })
.style("fill", function(d) { return c(j); })
.style("display","none");
g.append("text")
.attr("y", j*20+25)
.attr("x",width+20)
.attr("class","label")
.text(truncate(data[j]['name'],30,"..."))
.style("fill", function(d) { return c(j); })
.on("mouseover", mouseover)
.on("mouseout", mouseout);
// }
// catch(err){
// console.log(err);
// continue;
// }
};
function mouseover(p) {
var g = d3.select(this).node().parentNode;
d3.select(g).selectAll("circle").style("display","none");
d3.select(g).selectAll("text.value").style("display","block");
}
function mouseout(p) {
var g = d3.select(this).node().parentNode;
d3.select(g).selectAll("circle").style("display","block");
d3.select(g).selectAll("text.value").style("display","none");
}
});
Sample json:
[{"articles":[[2004,25],[2005,25],[2006,26],[2007,31],[2008,20],[2009,26],[2010,19],[2011,18],[2012,24],[2013,17]],"total": 231,"name": " Acta Inf. " },
{"articles":[[2008,1]],"total": 1,"name": " nf. " },
{"articles":[[2005,27],[2006,30],[2007,27],[2008,75],[2009,31],[2010,34],[2011,46],[2012,35],[2013,60]],"total": 365,"name": " Displays " },
{"articles":[[2010,20],[2011,16],[2012,16]],"total": 52,"name": " IJKDB " },
{"articles":[[2004,61],[2005,70],[2006,72],[2007,71],[2008,79],[2009,65],[2010,80],[2011,77],[2012,82],[2013,121]],"total": 778,"name": " Computers in Industry " },
{"articles":[[2010,1]],"total": 1,"name": " rs in Industry " },
{"articles":[[2005,1]],"total": 1,"name": " ry " }, ...
EDIT: no longer getting the error in console, there was something wrong with my JSON..however still not displaying all entries in the visualization, here is my entire JSON file https://api.myjson.com/bins/425wh
EDIT 2: it all displays now! all the data was there but wasn't showing up because the height of the d3 canvas was too small
The problem is in the last entry. The last entry is an array which is in correct it should have be an object, so your dataset has array within array in the last record. You will need to flatten your data set array.
Add this after fetching the data from AJAX.
data = [].concat.apply([], data);//this will flatten all your array within array into a single array of records,
Edit
Second problem:
I only see a few that actually show up? for example the Journal with name "Artificial Intelligence in Education" does not appear.
The problem is that the svg height is less and the data to be displayed is more so it cuts off after 30 records.
So have a dynamic height like this:
d3.select("body svg").attr("height",len*20.2);//20.2 is approx height of one element
Now height will depend on the data length that needs to displayed.
I have updated the fiddle accordingly
Working code here.

How to avoid labels overlapping in a D3.js pie chart?

I'm drawing a pie chart using D3.js with a quite simple script. The problem is that when slices are small, their labels overlap.
What options do I have to prevent them from overlapping? Does D3.js have built-in mechanisms I could exploit?
Demo: http://jsfiddle.net/roxeteer/JTuej/
var container = d3.select("#piechart");
var data = [
{ name: "Group 1", value: 1500 },
{ name: "Group 2", value: 500 },
{ name: "Group 3", value: 100 },
{ name: "Group 4", value: 50 },
{ name: "Group 5", value: 20 }
];
var width = 500;
var height = 500;
var radius = 150;
var textOffset = 14;
var color = d3.scale.category20();
var svg = container.append("svg:svg")
.attr("width", width)
.attr("height", height);
var pie = d3.layout.pie().value(function(d) {
return d.value;
});
var arc = d3.svg.arc()
.outerRadius(function(d) { return radius; });
var arc_group = svg.append("svg:g")
.attr("class", "arc")
.attr("transform", "translate(" + (width/2) + "," + (height/2) + ")");
var label_group = svg.append("svg:g")
.attr("class", "arc")
.attr("transform", "translate(" + (width/2) + "," + (height/2) + ")");
var pieData = pie(data);
var paths = arc_group.selectAll("path")
.data(pieData)
.enter()
.append("svg:path")
.attr("stroke", "white")
.attr("stroke-width", 0.5)
.attr("fill", function(d, i) { return color(i); })
.attr("d", function(d) {
return arc({startAngle: d.startAngle, endAngle: d.endAngle});
});
var labels = label_group.selectAll("path")
.data(pieData)
.enter()
.append("svg:text")
.attr("transform", function(d) {
return "translate(" + Math.cos(((d.startAngle + d.endAngle - Math.PI) / 2)) * (radius + textOffset) + "," + Math.sin((d.startAngle + d.endAngle - Math.PI) / 2) * (radius + textOffset) + ")";
})
.attr("text-anchor", function(d){
if ((d.startAngle +d.endAngle) / 2 < Math.PI) {
return "beginning";
} else {
return "end";
}
})
.text(function(d) {
return d.data.name;
});
D3 doesn't offer anything built-in that does this, but you can do it by, after having added the labels, iterating over them and checking if they overlap. If they do, move one of them.
var prev;
labels.each(function(d, i) {
if(i > 0) {
var thisbb = this.getBoundingClientRect(),
prevbb = prev.getBoundingClientRect();
// move if they overlap
if(!(thisbb.right < prevbb.left ||
thisbb.left > prevbb.right ||
thisbb.bottom < prevbb.top ||
thisbb.top > prevbb.bottom)) {
var ctx = thisbb.left + (thisbb.right - thisbb.left)/2,
cty = thisbb.top + (thisbb.bottom - thisbb.top)/2,
cpx = prevbb.left + (prevbb.right - prevbb.left)/2,
cpy = prevbb.top + (prevbb.bottom - prevbb.top)/2,
off = Math.sqrt(Math.pow(ctx - cpx, 2) + Math.pow(cty - cpy, 2))/2;
d3.select(this).attr("transform",
"translate(" + Math.cos(((d.startAngle + d.endAngle - Math.PI) / 2)) *
(radius + textOffset + off) + "," +
Math.sin((d.startAngle + d.endAngle - Math.PI) / 2) *
(radius + textOffset + off) + ")");
}
}
prev = this;
});
This checks, for each label, if it overlaps with the previous label. If this is the case, a radius offset is computed (off). This offset is determined by half the distance between the centers of the text boxes (this is just a heuristic, there's no specific reason for it to be this) and added to the radius + text offset when recomputing the position of the label as originally.
The maths is a bit involved because everything needs to be checked in two dimensions, but it's farily straightforward. The net result is that if a label overlaps a previous label, it is pushed further out. Complete example here.
#LarsKotthoff
Finally I have solved the problem. I have used stack approach to display the labels. I made a virtual stack on both left and right side. Based the angle of the slice, I allocated the stack-row. If stack row is already filled then I find the nearest empty row on both top and bottom of desired row. If no row found then the value (on the current side) with least share angle is removed from the stack and labels are adjust accordingly.
See the working example here:
http://manicharts.com/#/demosheet/3d-donut-chart-smart-labels
The actual problem here is one of label clutter.
So, you could try not displaying labels for very narrow arcs:
.text(function(d) {
if(d.endAngle - d.startAngle<4*Math.PI/180){return ""}
return d.data.key; });
This is not as elegant as the alternate solution, or codesnooker's resolution to that issue, but might help reduce the number of labels for those who have too many. If you need labels to be able to be shown, a mouseover might do the trick.
For small angles(less than 5% of the Pie Chart), I have changed the centroid value for the respective labels. I have used this code:
arcs.append("text")
.attr("transform", function(d,i) {
var centroid_value = arc.centroid(d);
var pieValue = ((d.endAngle - d.startAngle)*100)/(2*Math.PI);
var accuratePieValue = pieValue.toFixed(0);
if(accuratePieValue <= 5){
var pieLableArc = d3.svg.arc().innerRadius(i*20).outerRadius(outer_radius + i*20);
centroid_value = pieLableArc.centroid(d);
}
return "translate(" + centroid_value + ")";
})
.text(function(d, i) { ..... });

Categories