All:
I am trying to make a customized line generator like D3 line(), but with ability to customize line segments style when there is data missing(like using dash line)
one thing I did not know how to implement is its .interpolate() function. The math seems complicated, what I am trying to do is just use existing D3 line function to draw those continus segments and connect them with dash line, but I can not figure out how to generate interpolated line?
In the code example below, u can see the dash line is not exactly overlap the solid line:
var data = [];
for(var i=0; i<20; i++){
if( i>0 && (i%4==0) ){
data.push(null);
}else {
data.push({x:i, y:Math.random(i)})
}
}
var x = d3.scale.linear();
var y = d3.scale.linear();
x.domain([0, 19])
.range([10, 390])
y.domain([0, 1])
.range([10, 360]);
var svg = d3.select("body")
.append("svg")
.attr("width", 400)
.attr("height", 400);
var lg = svg.append("g")
.classed("lineGroup", true);
var xg = svg.append("g")
.classed("xaxis", true)
.attr("transform", function(){
return "translate(0, 380)";
});
var line = d3.svg.line()
.interpolate("monotone")
.x(function(d) { return x(d.x); })
.y(function(d) { return y(d.y); });
line.defined(function(d) { return d!=null; });
lg.append("path")
.classed("shadowline", true)
.attr("d", function(){
return line(data.filter(function(d){return d!=null;}));
})
.style("fill", "none")
.style("stroke", "steelblue")
.style("stroke-width", "3px")
.attr("stroke-dasharray", "5,5");
lg.append("path")
.attr("d", function(){
return line(data);
})
.style("fill", "none")
.style("stroke", "steelblue")
.style("stroke-width", "3px");
lg.selectAll("circle")
.data(data.filter(function(d){return d!=null;}))
.enter()
.append("circle")
.style("fill", "orange")
.style("stroke", "red")
.style("stroke-width", "3px")
.attr("r",5)
.attr("cx", function(d){return x(d.x);})
.attr("cy", function(d){return y(d.y);})
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
xg.call(xAxis);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Any help? Thanks
I've come up with a weird algorithm to hide some parts of your line, first of all you have to realize that the interpolation algorithm you chose works by analyzing the previous and next points of any t between the previous and next point, therefore even if you want to generate only segment of the path you have to use the same interpolation algorithm otherwise the first/last points won't have the required curve
With that in mind my algorithm to solve your problem is as follows
render the solid path
render some segments of this solid path but with a white stroke so that it works like a mask
render the dashed path
Implementation
first render the solid path with the desired interpolation
in the data find the extremes of all the gaps e.g. gaps([0, 1, null, 3, 4, null, 5]) is transformed to [[1, 3], [4, 5]]
compute the length of the path at those points, this involves an exhaustive brute force check since there's no api to get the length from the origin of a path to a determined point that lies on it, since your data is increasing on x I did binary search but for the general case as I've said you need to do a brute force check
make a lot of samples between the gap endpoints (seen as path lengths) with path. getPointAtLength and finally render a path for each collection of points, the trick is to set a white stroke
render the dashed path
NOTE: I changed the interpolation function to 'cardinal' so that curves are a lot more noticed and you can see the masks in action
var data = [];
for(var i=0; i<20; i++){
if( i>0 && (i%4==0) ){
data.push(null);
}else {
data.push({x:i, y:Math.random(i)})
}
}
var x = d3.scale.linear();
var y = d3.scale.linear();
x.domain([0, 19])
.range([10, 390])
y.domain([0, 1])
.range([10, 360]);
var svg = d3.select("body")
.append("svg")
.attr("width", 400)
.attr("height", 400);
var lg = svg.append("g")
.classed("lineGroup", true);
var xg = svg.append("g")
.classed("xaxis", true)
.attr("transform", function(){
return "translate(0, 380)";
});
var line = d3.svg.line()
.interpolate("cardinal")
.x(function(d) { return x(d.x); })
.y(function(d) { return y(d.y); });
function lineFiltered(data) {
return line(data.filter(function (d) { return !!d }))
}
var basePath = lg.append("path")
.attr("d", function () { return lineFiltered(data) })
.style("fill", "none")
.style("stroke", "steelblue")
.style("stroke-width", "3px");
function getPathLengthAtPoint(path, point, samples) {
// binary search to find the length of a path closest to point
samples = samples || 100
var lo = 0, hi = path.getTotalLength()
var res = 0
for (var i = 0; i < samples; i += 1) {
var mid = lo + (hi - lo) / 2
var pMid = path.getPointAtLength(mid)
if (pMid.x < x(point.x)) {
res = lo = mid
} else {
hi = mid
}
}
return res
}
// gets endpoints from where there's a gap
// it assumes that a gap has only length 1
function getGapsEndPoints(data) {
var j = 0
var gaps = []
for (var i = 0; i < data.length; i += 1) {
if (typeof data[i] !== 'number') {
gaps.push([data[i - 1], data[i + 1]])
}
}
return gaps
}
// generates multiple points per path
function generatePaths(data, path, samples) {
samples = samples || 50
return data.map(function (d) {
var lo = d[0], hi = d[1]
var points = []
for (var i = 0; i <= samples; i += 1) {
var point = path.getPointAtLength(lo + i/samples * (hi - lo))
points.push({
x: x.invert(point.x),
y: y.invert(point.y)
})
}
return points
})
}
var missingData = data.map(function (d) {
return d && getPathLengthAtPoint(basePath.node(), d)
})
missingData = getGapsEndPoints(missingData)
missingData = generatePaths(missingData, basePath.node())
// finally create the mask paths using the same line generator
lg.selectAll('path.mask').data(missingData)
.enter().append('path').classed('mask', true)
.attr('d', lineFiltered)
.style("fill", "none")
.style("stroke", "white")
.style("stroke-width", "3px")
lg.append("path")
.classed("shadowline", true)
.attr("d", function () { return lineFiltered(data) })
.style("fill", "none")
.style("stroke", "steelblue")
.style("stroke-width", "3px")
.attr("stroke-dasharray", "5,5");
lg.selectAll("circle")
.data(data.filter(function(d){return d!=null;}))
.enter()
.append("circle")
.style("fill", "orange")
.style("stroke", "red")
.style("stroke-width", "3px")
.attr("r",5)
.attr("cx", function(d){return x(d.x);})
.attr("cy", function(d){return y(d.y);})
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
xg.call(xAxis);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Here is my attempt:
Imagine you have an array like this:
[1,2,3, null, 4,5, null,null, 8,9]
Break it into two array groups
datachunks = [[1,2,3],[4,5][8,9]
brokenDataChunks = [[3,4][5,8]]
Now draw the datachunks like this:
datachunks.forEach(function(dc){
lg.append("path")
.attr("d", function(){
return line(dc);
})
.style("fill", "none")
.style("stroke", "steelblue")
.style("stroke-width", "3px")
})
Now draw the brokenDataChunks like this:
brokendatachunks.forEach(function(dc){
lg.append("path")
.classed("shadowline", true)
.attr("d", function(){
return line(dc);
})
.style("fill", "none")
.style("stroke", "red")
.style("stroke-width", "3px")
.attr("stroke-dasharray", "5,5");
})
The main challenge was to get the array split into this fashion:
var datachunks = [];//hold data chunks for the blue line connected one
var brokendatachunks = [];//hold data chunks for the red dashed line disconnected ones
var tempconnected =[];
var tempbroken =[];
data.forEach(function(d, i){
if(d){//if not null
tempconnected.push(d); //push in tempconnected
if (tempbroken.length > 0){
tempbroken.push(d);//if broken was detected before
brokendatachunks.push(tempbroken);//add this array into brokendatachunks.
tempbroken = [];//set the new value in temp broken to get new set of values
}
} else {
if(data[i-1])
tempbroken.push(data[i-1]);//push previous value don't want to insert null here.
datachunks.push(tempconnected);
tempconnected = [];
}
});
if (tempconnected.length > 0){
datachunks.push(tempconnected);
}
if (tempbroken.length > 0)
brokendatachunks.push(tempbroken);
}
working code here
complex case with lot of broken points in between here
In complex case I have put point generation like this
for(var i=0; i<20; i++){
if( i>0 && (i%4==0 || i%3 ==0) ){
data.push(null);
}else {
data.push({x:i, y:Math.random(i)})
}
}
Related
I'm trying to conditionally color these voronoi segments based on the 'd.lon' value. If it's positive, I want it to be green, if it's negative I want it to be red. However at the moment it's returning every segment as green.
Even if I swap my < operand to >, it still returns green.
Live example here: https://allaffects.com/world/
Thank you :)
JS
// Stating variables
var margin = {top: 20, right: 40, bottom: 30, left: 45},
width = parseInt(window.innerWidth) - margin.left - margin.right;
height = (width * .5) - 10;
var projection = d3.geo.mercator()
.center([0, 5 ])
.scale(200)
.rotate([0,0]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var path = d3.geo.path()
.projection(projection);
var voronoi = d3.geom.voronoi()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.clipExtent([[0, 0], [width, height]]);
var g = svg.append("g");
// Map data
d3.json("/world-110m2.json", function(error, topology) {
// Cities data
d3.csv("/cities.csv", function(error, data) {
g.selectAll("circle")
.data(data)
.enter()
.append("a")
.attr("xlink:href", function(d) {
return "https://www.google.com/search?q="+d.city;}
)
.append("circle")
.attr("cx", function(d) {
return projection([d.lon, d.lat])[0];
})
.attr("cy", function(d) {
return projection([d.lon, d.lat])[1];
})
.attr("r", 5)
.style("fill", "red");
});
g.selectAll("path")
.data(topojson.object(topology, topology.objects.countries)
.geometries)
.enter()
.append("path")
.attr("d", path)
});
var voronoi = d3.geom.voronoi()
.clipExtent([[0, 0], [width, height]]);
d3.csv("/cities.csv", function(d) {
return [projection([+d.lon, +d.lat])[0], projection([+d.lon, +d.lat]) [1]];
}, function(error, rows) {
vertices = rows;
console.log(vertices);
drawV(vertices);
}
);
function polygon(d) {
return "M" + d.join("L") + "Z";
}
function drawV(d) {
svg.append("g")
.selectAll("path")
.data(voronoi(d), polygon)
.enter().append("path")
.attr("class", "test")
.attr("d", polygon)
// This is the line I'm trying to get to conditionally fill the segment.
.style("fill", function(d) { return (d.lon < 0 ? "red" : "green" );} )
.style('opacity', .7)
.style('stroke', "pink")
.style("stroke-width", 3);
}
JS EDIT
d3.csv("/static/cities.csv", function(data) {
var rows = [];
data.forEach(function(d){
//Added third item into my array to test against for color
rows.push([projection([+d.lon, +d.lat])[0], projection([+d.lon, +d.lat]) [1], [+d.lon]])
});
console.log(rows); // data for polygons and lon value
console.log(data); // data containing raw csv info (both successfully log)
svg.append("g")
.selectAll("path")
.data(voronoi(rows), polygon)
.enter().append("path")
.attr("d", polygon)
//Trying to access the third item in array for each polygon which contains the lon value to test
.style("fill", function(data) { return (rows[2] < 0 ? "red" : "green" );} )
.style('opacity', .7)
.style('stroke', "pink")
.style("stroke-width", 3)
});
This is what's happening: your row function is modifying the objects of rows array. At the time you get to the function for filling the polygons there is no d.lon anymore, and since d.lon is undefined the ternary operator is evaluated to false, which gives you "green".
Check this:
var d = {};
console.log(d.lon < 0 ? "red" : "green");
Which also explains what you said:
Even if I swap my < operand to >, it still returns green.
Because d.lon is undefined, it doesn't matter what operator you use.
That being said, you have to keep your original rows structure, with the lon property in the objects.
A solution is getting rid of the row function...
d3.csv("cities.csv", function(data){
//the rest of the code
})
... and creating your rows array inside the callback:
var rows = [];
data.forEach(function(d){
rows.push([projection([+d.lon, +d.lat])[0], projection([+d.lon, +d.lat]) [1]])
});
Now you have two arrays: rows, which you can use to create the polygons just as you're using now, and data, which contains the lon values.
Alternatively, you can keep everything in just one array (just changing your row function), which is the best solution because it would make easier to get the d.lon values inside the enter selection for the polygons. However, it's hard providing a working answer without testing it with your actual code (it normally ends up with the OP saying "it's not working!").
I've been looking at this example of a beeswarm plot in d3.js and I'm trying to figure out how to change the size of the dots and without getting the circles to overlap. It seems if the radius of the dots change, it doesn't take this into account when running the calculations of where to place the dots.
This is a cool visualization.
I've made a plunk of it here: https://plnkr.co/edit/VwyXfbc94oXp6kXQ7JFx?p=preview and modified it to work a bit more like you're looking for (I think). The real key is changing the call to handle collision to vary based on the radius of the circles (in the original post it's hard coded to 4, which works well when r === 3 but fails as r grows). The changes:
Make the circle radius into a variable (line 7 of script.js, var r = 3;)
Change the d3.forceCollide call to use that radius and a multiplier - line 110 (.force("collide", d3.forceCollide(r * 1.333)))
Change the .enter() call to use that radius as well (line 130: .attr("r", r))
This works reasonably well for reasonable values of r - but you'll need to adjust the height, and it might even be nice to just change the whole thing so that r is based on height (e.g. var r = height * .01). You'll notice that as is now, the circles go off the bottom and top of the graph area.
This post might be of interest as well: Conflict between d3.forceCollide() and d3.forceX/Y() with high strength() value
Here's the whole of script.js for posterity:
var w = 1000, h = 280;
var padding = [0, 40, 34, 40];
var r = 5;
var xScale = d3.scaleLinear()
.range([ padding[3], w - padding[1] ]);
var xAxis = d3.axisBottom(xScale)
.ticks(10, ".0s")
.tickSizeOuter(0);
var colors = d3.scaleOrdinal()
.domain(["asia", "africa", "northAmerica", "europe", "southAmerica", "oceania"])
.range(['#e41a1c','#377eb8','#4daf4a','#984ea3','#ff7f00','#ffff33']);
d3.select("#africaColor").style("color", colors("africa"));
d3.select("#namericaColor").style("color", colors("northAmerica"));
d3.select("#samericaColor").style("color", colors("southAmerica"));
d3.select("#asiaColor").style("color", colors("asia"));
d3.select("#europeColor").style("color", colors("europe"));
d3.select("#oceaniaColor").style("color", colors("oceania"));
var formatNumber = d3.format(",");
var tt = d3.select("#svganchor").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var svg = d3.select("#svganchor")
.append("svg")
.attr("width", w)
.attr("height", h);
var xline = svg.append("line")
.attr("stroke", "gray")
.attr("stroke-dasharray", "1,2");
var chartState = {};
chartState.variable = "totalEmission";
chartState.scale = "scaleLinear";
chartState.legend = "Total emissions, in kilotonnes";
d3.csv("co2bee.csv", function(error, data) {
if (error) throw error;
var dataSet = data;
xScale.domain(d3.extent(data, function(d) { return +d.totalEmission; }));
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (h - padding[2]) + ")")
.call(xAxis);
var legend = svg.append("text")
.attr("text-anchor", "middle")
.attr("x", w / 2)
.attr("y", h - 4)
.attr("font-family", "PT Sans")
.attr("font-size", 12)
.attr("fill", "darkslategray")
.attr("fill-opacity", 1)
.attr("class", "legend");
redraw(chartState.variable);
d3.selectAll(".button1").on("click", function(){
var thisClicked = this.value;
chartState.variable = thisClicked;
if (thisClicked == "totalEmission"){
chartState.legend = "Total emissions, in kilotonnes";
}
if (thisClicked == "emissionPerCap"){
chartState.legend = "Per Capita emissions, in metric tons";
}
redraw(chartState.variable);
});
d3.selectAll(".button2").on("click", function(){
var thisClicked = this.value;
chartState.scale = thisClicked;
redraw(chartState.variable);
});
d3.selectAll("input").on("change", filter);
function redraw(variable){
if (chartState.scale == "scaleLinear"){ xScale = d3.scaleLinear().range([ padding[3], w - padding[1] ]);}
if (chartState.scale == "scaleLog"){ xScale = d3.scaleLog().range([ padding[3], w - padding[1] ]);}
xScale.domain(d3.extent(dataSet, function(d) { return +d[variable]; }));
var xAxis = d3.axisBottom(xScale)
.ticks(10, ".0s")
.tickSizeOuter(0);
d3.transition(svg).select(".x.axis").transition().duration(1000)
.call(xAxis);
var simulation = d3.forceSimulation(dataSet)
.force("x", d3.forceX(function(d) { return xScale(+d[variable]); }).strength(2))
.force("y", d3.forceY((h / 2)-padding[2]/2))
.force("collide", d3.forceCollide(r * 1.333))
.stop();
for (var i = 0; i < dataSet.length; ++i) simulation.tick();
var countriesCircles = svg.selectAll(".countries")
.data(dataSet, function(d) { return d.countryCode});
countriesCircles.exit()
.transition()
.duration(1000)
.attr("cx", 0)
.attr("cy", (h / 2)-padding[2]/2)
.remove();
countriesCircles.enter()
.append("circle")
.attr("class", "countries")
.attr("cx", 0)
.attr("cy", (h / 2)-padding[2]/2)
.attr("r", r)
.attr("fill", function(d){ return colors(d.continent)})
.merge(countriesCircles)
.transition()
.duration(2000)
.attr("cx", function(d) { console.log(d); return d.x; })
.attr("cy", function(d) { return d.y; });
legend.text(chartState.legend);
d3.selectAll(".countries").on("mousemove", function(d) {
tt.html("Country: <strong>" + d.countryName + "</strong><br>"
+ chartState.legend.slice(0, chartState.legend.indexOf(",")) + ": <strong>" + formatNumber(d[variable]) + "</strong>" + chartState.legend.slice(chartState.legend.lastIndexOf(" ")))
.style('top', d3.event.pageY - 12 + 'px')
.style('left', d3.event.pageX + 25 + 'px')
.style("opacity", 0.9);
xline.attr("x1", d3.select(this).attr("cx"))
.attr("y1", d3.select(this).attr("cy"))
.attr("y2", (h - padding[2]))
.attr("x2", d3.select(this).attr("cx"))
.attr("opacity", 1);
}).on("mouseout", function(d) {
tt.style("opacity", 0);
xline.attr("opacity", 0);
});
d3.selectAll(".x.axis, .legend").on("mousemove", function(){
tt.html("This axis uses SI prefixes:<br>m: 10<sup>-3</sup><br>k: 10<sup>3</sup><br>M: 10<sup>6</sup>")
.style('top', d3.event.pageY - 12 + 'px')
.style('left', d3.event.pageX + 25 + 'px')
.style("opacity", 0.9);
}).on("mouseout", function(d) {
tt.style("opacity", 0);
});
//end of redraw
}
function filter(){
function getCheckedBoxes(chkboxName) {
var checkboxes = document.getElementsByName(chkboxName);
var checkboxesChecked = [];
for (var i=0; i<checkboxes.length; i++) {
if (checkboxes[i].checked) {
checkboxesChecked.push(checkboxes[i].defaultValue);
}
}
return checkboxesChecked.length > 0 ? checkboxesChecked : null;
}
var checkedBoxes = getCheckedBoxes("continent");
var newData = [];
if (checkedBoxes == null){
dataSet = newData;
redraw();
return;
};
for (var i = 0; i < checkedBoxes.length; i++){
var newArray = data.filter(function(d){
return d.continent == checkedBoxes[i];
});
Array.prototype.push.apply(newData, newArray);
}
dataSet = newData;
redraw(chartState.variable);
//end of filter
}
//end of d3.csv
});
I am trying to create a radar chart similar to the link here (
http://www.larsko.org/v/euc/).
I was able to create axes (my work so far), but I am having a problem to draw lines in it.
For instance, if I have a list of values something like below, how can I draw a line in the radar chart?
var tempData = [56784, 5.898, 3417, 0, 0, 0]
Edit: I have included code. I am having a problem finding XY coordinates and I think XY value has to be derived from "scales".
var width = 1000,
height = 960,
r = (960 / 2) - 160;
var svg = d3.select("#radar")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + ", " + height / 2 + ")");
d3.csv("data/results.csv", function(data) {
var headerNames = d3.keys(data[0]);
headerNames.splice(0, 1); //Remove 'scenario'
var minList = $.map(headerNames, function(h) {
return d3.min($.map(data, function(d) {
return d[h];
}));
}),
maxList = $.map(headerNames, function(h) {
return d3.max($.map(data, function(d) {
return d[h];
}));
}),
scales = $.map(headerNames, function(h, i) {
return d3.scale.linear()
.domain([minList[i], maxList[i]])
.range([50, r]);
}),
axes = $.map(headerNames, function(h, i) {
return d3.svg.axis()
.scale(scales[i])
.tickSize(4);
});
function angle(i) {
return i * (2 * Math.PI / headerNames.length) + Math.PI / headerNames.length;
}
var line = d3.svg.line()
.interpolate("cardinal-closed")
/* computing X and Y: I am having a problem here
.x(function(d){ return scales(d); })
.y(function(d){ return scales(d); }); */
$.each(axes, function(i, a) {
svg.append("g")
.attr("transform", "rotate(" + Math.round(angle(i) * (180 / Math.PI)) + ")")
.call(a)
.selectAll("text")
.attr("text-anchor", "middle")
.attr("transform", function(d) {
return "rotate(" + -angle(i) * (180 / Math.PI) + ")";
})
//Drawing line
svg.selectAll(".layer")
.data(data)
.enter()
.append("path")
.attr("class", "layer")
.attr("d", function(d) {
return line(d);
})
}) // End CSV
Example results.csv
scenario,n_dead_oaks,percent_dead_oaks,infected_area_ha,money_spent,area_treated_ha,price_per_oak
baseline,56784,5.898,3417,0,0,0
scen2,52725,5.477,3294,382036,35,94.12071939
RS_1,58037,6.028,3407,796705,59,-635.8379888
RS_2,33571,3.487,2555,1841047,104,79.31103261
RS_3,46111,4.79,2762,1176461,61,110.227771
As Squeegy suggested, you should share some code showing your current progress and how you have achieved to create the axes.
Anyways, this is how I would go about this:
For a given list of values that you want to represent as a line, find the [x,y] coordinates of every point of the line, i.e. place your data-points on each axis. If you have a scale system in place already to draw your axes, this shouldn't be too hard.
Use d3.svg.line to draw a line that goes through all these points.
The code would end up looking like this:
var tempData = [56784, 5.898, 3417, 0, 0, 0];
/** compute tempPoints from tempData **/
var tempPoints = [[123, 30], [12, 123], [123, 123], [0,0], [0,0], [0,0]];
var line = d3.svg.line();
d3.select('svg').append('path').attr('d', line(tempPoints) + 'Z'); // the trailing Z closes the path
I think I have a solution for now and I appreciate all of your response! Here is my current solution for my posting.
function getRowValues(data) {
return $.map(data, function(d, i) {
if (i != "scenario") {
return d;
}
});
}
function getCoor(data) {
console.log(data);
var row = getRowValues(data),
x,
y,
coor = [];
for (var i = 0; i < row.length; i++) {
x = Math.round(Math.cos(angle(i)) * scales[i](row[i]));
y = Math.round(Math.sin(angle(i)) * scales[i](row[i]));
coor.push([x, y]);
}
return coor;
}
var line = d3.svg.line()
.interpolate("cardinal-closed")
.tension(0.85);
svg.selectAll(".layer")
.data(data)
.enter()
.append("path")
.attr("class", "layer")
.attr("d", function(d) { return line(getCoor(d)) + "Z"; })
.style("stroke", function(d, i){ return colors[i]; })
.style("fill", "none");
I would like to reuse the general update pattern III for a project and
want to know how to make the text labels line up better with the circle elements. My experiment is to attach circle elements and text to the "g", but I cannot place the text labels correctly.
Here is how I modified the block:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
text {
font: bold 28px monospace;
}
.enter {
fill: green;
}
.update {
//fill: #333;
fill: red;
}
.exit {
//fill: brown;
fill: blue;
}
</style>
<body>
<script src="../d3.v3.js"></script>
<script>
function randomData(){
return d3.range(~~(Math.random()*50)+1).map(function(d, i){return ~~(Math.random()*100);});
}
var alphabet = "";
var numlist = [];
var randomEntry;
for (i = 0; i< 2; i++) {
randomEntry = randomData();
numlist.push( randomEntry);
}
var temp = numlist.toString();
var temp2 = temp.split('"');
alphabet = temp2.pop();
console.log("alphabet", alphabet);
var temp3 = alphabet.toString();
console.log("temp3", temp3);
console.log("temp3 type", typeof(temp3));
var temp4 = alphabet.split(",");
alphabet = temp4;
console.log("alphabet", alphabet);
var width = 960,
height = 500;
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(32," + (height / 2) + ")");
function update(data) {
// DATA JOIN
// Join new data with old elements, if any.
var text = svg.selectAll("text")
.data(data, function(d) { return d; });
var circles = svg.selectAll("circle")
.data(data, function(d) { return d; });
// UPDATE
// Update old elements as needed.
circles.attr("class", "update")
.transition()
.duration(750)
.attr("opacity", 0.3)
.attr("cx", function(d,i) { return (Math.random(i))*100;})
.attr("cy", function(d,i) { return (Math.random(i))*100;})
.attr("transform", "translate(200," + (-100) + ")");
text.attr("class", "update")
.transition()
.duration(750)
.attr("x", function(d,i) { return (Math.random(i))*100; })
.attr("y", function(d,i) { return (Math.random(i))*100; })
.attr("transform", "translate(200," + (-100) + ")");
// ENTER
// Create new elements as needed.
circles.enter().append("circle")
.attr("class", "enter")
.attr("opacity", 0.3)
.attr("r", 25)
.attr("cy", function(d,i) { return (Math.random(i))*270;})
.attr("cx", function(d,i) { return (Math.random(i))*270;})
.style("fill-opacity", 1e-6)
.transition()
.duration(750)
.attr("r", 30)
.style("fill-opacity", 1);
text.enter().append("text")
.attr("class", "enter")
.attr("dy", ".25em")
.attr("x", function(d) { return (Math.random(i))*100; })
.attr("y", function(d) { return (Math.random(i))*100; })
.style("fill-opacity", 1e-6)
.text(function(d) { return d; })
.transition()
.duration(750)
.style("fill-opacity", 1);
// EXIT
// Remove old elements as needed.
text.exit()
.attr("class", "exit")
.transition()
.duration(750)
.attr("y", 60)
.style("fill-opacity", 1e-6)
.remove();
circles.exit()
.attr("class", "exit")
.transition()
.duration(750)
.style("fill-opacity", 1e-6)
.remove();
}
// The initial display.
update(alphabet);
// Grab a random sample of letters from the alphabet, in alphabetical order.
setInterval(function() {
update(shuffle(alphabet)
.slice(0, Math.floor(Math.random() * 26))
.sort());
}, 1500);
// Shuffles the input array.
function shuffle(array) {
var m = array.length, t, i;
while (m) {
i = Math.floor(Math.random() * m--);
t = array[m], array[m] = array[i], array[i] = t;
}
return array;
}
</script>
How can I change this so the text labels appear next to the circle elements? Thanks for any assistance.
You seem to making a random data for determining circle DOM's cx and cy of the circle:
.attr("cy", function(d,i) { return (Math.random(i))*270;})
.attr("cx", function(d,i) { return (Math.random(i))*270;})
In text DOM you make random points for determining x and y of text.
.attr("x", function(d) { return (Math.random(i))*100; })
.attr("y", function(d) { return (Math.random(i))*100; })
So as a fix you can have a common data which will decide the x/y for text and cx/cy for circle.
BY making a function like this:
function randomData() {
return (Math.random() * 500);//his generates a single random point
}
var alphabet = [];
function randomEntry() {
var numlist = [];
var randomEntry;
for (i = 0; i < 5; i++) {
//generate 5 random coordinate
//here first element willdecide the x and second element decide the y.
numlist.push([randomData(), randomData()]);
}
//this will contain 5 coordinate points.
return numlist;
}
Then set the 5 coordinates point data in the text and circel like this:
var text = svg.selectAll("text")
.data(data, function(d) {
return d;
});
var circles = svg.selectAll("circle")
.data(data, function(d) {
return d;
});
Then in the update
circles.attr("class", "update")
.transition()
.duration(750)
.attr("opacity", 0.3)
.attr("cx", function(d, i) {
return d[0];//here d[0] is the x coordinate which determine the circle center x
})
.attr("cy", function(d, i) {
return d[1];//here d[1] is the y coordinate which determine the circle center y
})
text.attr("class", "update")
.transition()
.duration(750)
.attr("x", function(d, i) {
return d[0];
})
.attr("y", function(d, i) {
return d[1];
})
Working code here
Hope this helps!
I'm trying to graph the median lifetime of our customers in D3.js. I have the data graphed out, but I can't figure out how to draw reference lines showing the median lifetime. I want vertical and horizontal reference lines that intersect my data at the 50% value of the y-axis.
Here's what I have currently:
The vertical reference line needs to intersect the data in the same place as the horizontal reference line.
Here's my code:
d3.json('data.json', function(billingData) {
var paying = billingData.paying;
var w = 800;
var h = 600;
var secondsInInterval = 604800000; // Seconds in a week
var padding = 50;
var age = function(beginDate, secondsInInterval) {
// Calculate how old a subscription is given it's begin date
var diff = new Date() - new Date(beginDate);
return Math.floor(diff / secondsInInterval);
}
var maxAge = d3.max(paying, function(d) { return age(d.subscription.activated_at, secondsInInterval); });
var breakdown = new Array(maxAge);
$.each(paying, function(i,d) {
d.age = age(d.subscription.activated_at, secondsInInterval);
for(var i = 0; i <= d.age; i++) {
if ( typeof breakdown[i] == 'undefined' ) breakdown[i] = 0;
breakdown[i]++;
}
});
// Scales
var xScale = d3.scale.linear().domain([0, maxAge]).range([padding,w-padding]);
var yScale = d3.scale.linear().domain([0, 1]).range([h-padding,padding]);
// Axes
var xAxis = d3.svg.axis().scale(xScale).tickSize(6,3).orient('bottom');
var yAxis = d3.svg.axis().scale(yScale).tickSize(6,3).tickFormat(d3.format('%')).orient('left');
var graph = d3.select('body').append('svg:svg')
.attr('width', 800)
.attr('height', 600);
var line = graph.selectAll('path.line')
.data([breakdown])
.enter()
.append('svg:path')
.attr('fill', 'none')
.attr('stroke', 'blue')
.attr('stroke-width', '1')
.attr("d", d3.svg.line()
.x(function(d,i) {
return xScale(i);
})
.y(function(d,i) {
return yScale(d/paying.length);
})
);
var xMedian = graph.selectAll('path.median.x')
.data([[[maxAge/2,0], [maxAge/2,1]]])
.enter()
.append('svg:path')
.attr('class', 'median x')
.attr("d", d3.svg.line()
.x(function(d,i) {
return xScale(d[0]);
})
.y(function(d,i) {
return yScale(d[1]);
})
);
var yMedian = graph.selectAll('path.median.y')
.data([[[0,.5], [maxAge,0.5]]])
.enter()
.append('svg:path')
.attr('class', 'median y')
.attr("d", d3.svg.line()
.x(function(d,i) {
return xScale(d[0]);
})
.y(function(d,i) {
return yScale(d[1]);
})
);
graph.append('g').attr('class', 'x-axis').call(xAxis).attr('transform', 'translate(0,' + (h - padding) + ')')
graph.append('g').attr('class', 'y-axis').call(yAxis).attr('transform', 'translate(' + padding + ',0)');
graph.append('text').attr('class', 'y-label').attr('text-anchor', 'middle').text('customers').attr('transform', 'translate(10,' + (h / 2) + '), rotate(-90)');
graph.append('text').attr('class', 'x-label').attr('text-anchor', 'middle').text('lifetime (weeks)').attr('transform', 'translate(' + (w/2) + ',' + (h - padding + 40) + ')');
});
You need to search the point where the customers are 50% in your line (around 7 weeks), that's it, search the index i where breakdown[i]/paying.length is near 0.5, save that index as indexMedianCustomers (for example) and modify your code in
var xMedian = graph.selectAll('path.median.x')
.data([[[indexMedianCustomers,0], [indexMedianCustomers,1]]])
.enter()
.append('svg:path')
.attr('class', 'median x')
.attr("d", d3.svg.line()
.x(function(d,i) {
return xScale(d[0]);
})
.y(function(d,i) {
return yScale(d[1]);
})
);