Alternating tick padding in d3.js - d3.js

I am writing the following d3.js function for brush. This function works fine for me but i want to alternate the tick padding for the brush so that two consecutive ticks are not on the same height and hence do not overlap. This maintains the clarity of the ticks. Can anyone suggest the right way to do so in the following function.
$scope.drawBrush = function (span) {
var width = 400,
height = 50;
var x = d3.time.scale()
.domain([new Date($scope.range1), new Date($scope.range2)])
.range([0, width]);
d3.select('#brushd').remove();
var brush = d3.svg.brush()
.x(x)
.extent([new Date($scope.range1), new Date($scope.range2)])
.on('brush', brushed);
var svg = d3.select('#brush-div').append('svg')
.attr('id', 'brushd')
.attr('width', '400')
.attr('height', '80')
.style('margin-left', '0px')
.append('g')
.attr('transform', 'translate(' + 50 + ',' + 0 + ')');
svg.append('rect')
.attr('class', 'grid-background')
.attr('width', width)
.attr('height', height);
svg.append('g')
.attr('class', 'x grid')
.attr('transform', 'translate(0,' + height + ')')
.call(d3.svg.axis()
.scale(x)
.orient('bottom')
.ticks(d3.time.hours, 12)
.tickSize(-height)
.tickFormat(''))
.selectAll('.tick')
.classed('minor', function (d) {
return d.getHours();
});
svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + height + ')')
.call(d3.svg.axis()
.scale(x)
.orient('bottom')
.tickFormat(d3.time.format('%b-%y'))
.tickPadding(0))
.selectAll('text')
.attr('x', 6)
.style('text-anchor', null);
var gBrush = svg.append('g')
.attr('class', 'brush')
.call(brush);
gBrush.selectAll('rect')
.attr('height', height);
function brushed() {
var extent0 = brush.extent(),
extent1;
// if dragging, preserve the width of the extent
if (d3.event.mode === 'move') {
var d0 = d3.time.day.round(extent0[0]),
d1 = d3.time.day.offset(d0, Math.round((extent0[1] - extent0[0]) / 864e5));
extent1 = [d0, d1];
}
// otherwise, if resizing, round both dates
else {
if (span === 'daily') {
extent1 = extent0.map(d3.time.day.round);
// if empty when rounded, use floor & ceil instead
if (extent1[0] >= extent1[1]) {
extent1[0] = d3.time.day.floor(extent0[0]);
extent1[1] = d3.time.day.ceil(extent0[1]);
}
} else if (span === 'weekly') {
extent1 = extent0.map(d3.time.week.round);
// if empty when rounded, use floor & ceil instead
if (extent1[0] >= extent1[1]) {
extent1[0] = d3.time.week.floor(extent0[0]);
extent1[1] = d3.time.week.ceil(extent0[1]);
}
} else {
extent1 = extent0.map(d3.time.month.round);
// if empty when rounded, use floor & ceil instead
if (extent1[0] >= extent1[1]) {
extent1[0] = d3.time.month.floor(extent0[0]);
extent1[1] = d3.time.month.ceil(extent0[1]);
}
}
}
d3.select(this).call(brush.extent(extent1));
$scope.getLimit(brush.extent()[0], brush.extent()[1]);
}
};

One way to alternate between long and short ticks on your x-axis, to achieve this sort of thing:
... is like this:
// flag to alternate between long and short ticks
var alternate_ticks = false;
var short_tick_length = 4;
var long_tick_length = 16;
// Alternate the tick line between long and short ticks
d3.selectAll("g.x.axis g.tick line")
.attr("y2", function () {
if (alternate_ticks) {
alternate_ticks = false;
return long_tick_length;
} else {
alternate_ticks = true;
return short_tick_length;
}
});
var alternate_text = false;
// Alternate the tick label text to match up with the tick length
d3.selectAll("g.x.axis g.tick text")
.attr("y", function () {
if (alternate_text) {
alternate_text = false;
return long_tick_length + 1;
} else {
alternate_text = true;
return short_tick_length + 1;
}
});
It's a simpler version of the same approach as Lars K pointed out above, but using a simpler function that just alternates between false and true to determine tick length and text positioning on the relevant set of objects.

Try this for tick labeling.
// Alternate the tick label text to match up with the tick length
d3.selectAll("g.x.axis g.tick text")
.each(function (d, i) {
//console.log(i % 2);
if ((i % 2)) { //even
this.remove();
}
});

Related

How to change d3 legend entry spacing/alignment

I've got this legend:
As you can see, each legend entry is the same width. Instead, I'd like each legend entry's width to vary based upon the width of the entry's symbol and text. Ultimately, I want the same distance between the ends of the leading entry's text and the start of the following entry's symbol. In other words, I'd like the same distance between 'OA' and the plus sign as between the 'OI' and the diamond and the 'RARC' and the square. I need this to be based on pixels (string lengths won't suffice). I've been trying all sorts of stuff, but haven't been successful.
Here's my code:
var legendData = [["OA", "yellow", "circle"], ["OI", "blue", "cross"], ["RARC", "green", "diamond"], ["CAPE", "red", "square"], ["Other", "black", "triangle-down"]];
this.svg.selectAll('.legend').remove() //remove remnants of previous legend so new legend has clean slate...eliminates overlays during resizing
var legend = this.svg.append('g')
.attr("class", "legend")
.attr("height", 0)
.attr("width", 0)
.attr('transform', 'translate(' + (ScatterChart.Config.margins.left + (width * .008)) + ',' + (height += .40 * ScatterChart.Config.margins.bottom) + ')');
var legendRect = legend
.selectAll('g')
.data(legendData)
;
var labelLength = 0
var labelLengthPrevious = 0
var legendRectE = legendRect.enter()
.append("g")
.attr("transform", function (d, i) {
//labelLength = labelLengthPrevious //Need to figure out pixel lengths
//labelLengthPrevious += (d[0].length) + 50
//return 'translate(' + labelLength + ', ' + 0 + ' )'; // y is constant and x growing
return 'translate(' + (i * (.15 * width)) + ', ' + 0 + ' )'; // y is constant and x growing
})
;
legendRectE
.append('path')
.attr("d", d3.svg.symbol().type((d) => {
return d[2]
}
).size((d3.min([height, width]) * ScatterChart.Config.axisFontMultiplier) * (d3.min([height, width]) * ScatterChart.Config.symbolSizeMultiplier)))
.style("fill", function (d) {
return d[1];
})
.attr('stroke', 'black')
;
//This asserts legendRectE as a node...I think. I do this so I can use the width and height measurements of legendRectE.
var node: SVGElement = <SVGElement>legendRectE.node()
legendRectE
.append("text")
.attr("x", function (d) {
return node.getBoundingClientRect().width
})
.attr("y", function (d) {
return node.getBoundingClientRect().height / 2.25
})
.text(function (d) {
return d[0];
})
.style('font-size', function () { return d3.min([height, width]) * ScatterChart.Config.axisFontMultiplier + "px" })
;
I think the answer would have something to do with this line: return 'translate(' + (i * (.15 * width)) + ', ' + 0 + ' )'; // y is constant and x growing. Right now, it just shifts to the right by multiplying the index by 15% of the chart's width. I figure I need to somehow substitute the width of the legendRectE (or of legendRect or legend) in place of (I * (.15 * width)). I can't figure out how to do that.
You can see that I use the following to get the width of legendRectE later in the code: var node: SVGElement = <SVGElement>legendRectE.node(), followed by node.getBoundingClientRect().width.
node.getBoundingClientRect().width gives me a width value where you see it being used now, but when I use this same approach to determine a value for the translate I mentioned, it chokes; and when I use legendRect or legend instead of legendRectE I only get '0'.
I thought I'd be able to edit the transform function something like this:
var legendRectE = legendRect.enter()
.append("g")
.attr("transform", function (d, i) {
var node: SVGElement = <SVGElement>legendRectE.node()
return 'translate(' + node.getBoundingClientRect().width + ', ' + 0 + ' )'; // y is constant and x growing
})
;
Obviously, I was wrong. Any ideas/advice?
p.s. I'm using d3 v3.5.
The challenge is that it is (as far as I know) difficult to determine the transform when appending elements initially as the widths are unknown. But you could go back and calculate the width of each legend entry after they are all appended and then reposition the legend entries accordingly.
The snippet below positions everything overtop of each other to start, then calculates the svg width of each legend g using getBBox. Then, using d3.sum, calculates the width of each element that was appended before it (and thus should be to the left of it) and sets the translate value to the sum of those widths accordingly.
It can probably be cleaned up a bit probably, it's a little quick. If there is lag before the elements are positioned correctly, appending them transparently and then fading them in after they are positioned might be an elegant (visually, less so programatically) solution (or appending them initially outside of the view box).
d3v4:
var data = ['short text','much longer text','the longest text passage','short text'];
var svg = d3.select('body')
.append('svg')
.attr('width',800)
.attr('height',200);
var groups = svg.selectAll('g')
.data(data)
.enter()
.append('g');
var rect = groups.append('rect')
.attr('fill',function(d,i) { return d3.schemeCategory10[i];})
.attr('height',30)
.attr('width',30);
var text = groups.append('text')
.attr('y', 20)
.attr('x', 35)
.text(function(d) { return d; });
// Now space the groups out after they have been appended:
var padding = 10;
groups.attr('transform', function(d,i) {
return "translate("+(d3.sum(data, function(e,j) {
if (j < i) { return groups.nodes()[j].getBBox().width; } else return 0; }) + padding * i) + ",0)";
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
d3v3:
var data = ['short text','much longer text','the longest text passage','short text'];
var svg = d3.select('body')
.append('svg')
.attr('width',800)
.attr('height',200);
var groups = svg.selectAll('g')
.data(data)
.enter()
.append('g');
var color = ["orange","red","purple","green"];
var rect = groups.append('rect')
.attr('fill',function(d,i) { return color[i];})
.attr('height',30)
.attr('width',30);
var text = groups.append('text')
.attr('y', 20)
.attr('x', 35)
.text(function(d) { return d; });
// Now space the groups out after they have been appended:
var padding = 10;
groups.attr('transform', function(d,i) {
return "translate("+(d3.sum(data, function(e,j) {
if (j < i) { return groups[0][j].getBBox().width; } else return 0; }) + padding * i) + ",0)";
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

D3.js Radar chart line drawing

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");

d3 redraw bar chart with new values

My barchart draws fine when the page first loads.
But choose hour 2 from the drop-down, and it doesn't want to update to hour 2 data, it just keeps displaying hour 1.
FIDDLE
This is my d3 and js:
$('#bar_chart').css('overflow-x','scroll');
var margin = {top: 20, right: 20, bottom: 40, left: 80},
width = 220 - margin.left - margin.right,
height = 233 - margin.top - margin.bottom;
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], .1, 1);
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient('bottom');
var formatComma = d3.format('0,000');
var yAxis = d3.svg.axis()
.scale(y)
.orient('left')
.ticks(5)
.outerTickSize(0)
.tickFormat(formatComma);
var svg = d3.select('th.chart-here').append('svg')
.attr('viewBox', '0 0 220 233')
.attr('preserveAspectRatio','xMinYMin meet')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left/1.5 + ',' + margin.top/1.5 + ')');
var table_i = 0;
var arr1 =
[
{'hour':1,'car':[{'audi':1377},{'bmw':716},{'ford':3819},{'mazda':67},{'toyota':11580},{'tesla':0}]},
{'hour':2,'car':[{'audi':9000},{'bmw':2000},{'ford':7000},{'mazda':1000},{'toyota':5000},{'tesla':700}]},
];
var hour = arr1[table_i];
var car=hour.car;
var newobj = [];
for(var hourx1=0;hourx1<car.length;hourx1++){
var xx = car[hourx1];
for (var value in xx) {
var chartvar = newobj.push({car:value,miles:xx[value]});
var data = newobj;
}
}
x.domain(data.map(function(d) { return d.car; }));
y.domain([0, d3.max(data, function(d) { return d.miles; })]);
svg.append('g')
.attr('class', 'y axis')
.call(yAxis)
.append('text')
.attr('y', 6)
.attr('dy', '.71em')
.style('text-anchor', 'start');
function changeHour(){
svg.selectAll('.bar')
.data(data)
.enter().append('rect')
.attr('class', 'bar')
.attr('transform','translate(-20)') //move rects closer to Y axis
.attr('x', function(d) { return x(d.car); })
.attr('width', x.rangeBand()*1)
.attr('y', function(d) { return y(d.miles); })
.attr('height', function(d) { return height - y(d.miles); });
xtext = svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(-20,' + height + ')') //move tick text so it aligns with rects
.call(xAxis);
xtext.selectAll('text')
.attr('transform', function(d) {
return 'translate(' + this.getBBox().height*50 + ',' + this.getBBox().height + ')rotate(0)';
});
//code to enable jqm checkbox
$('#checkbox-2a').on('change', function(e){
originalchange(e);
});
$( '#checkbox-2a' ).checkboxradio({
defaults: true
});
var sortTimeout = setTimeout(function() {
$('#checkbox-2a').prop('checked', false).checkboxradio( 'refresh' ).change();
}, 1000);
function originalchange() {
clearTimeout(sortTimeout);
var IsChecked = $('#checkbox-2a').is(':checked');
// Copy-on-write since tweens are evaluated after a delay.
var x0 = x.domain(data.sort(IsChecked
? function(a, b) { return b.miles - a.miles; }
: function(a, b) { return d3.ascending(a.car, b.car); })
.map(function(d) { return d.car; }))
.copy();
svg.selectAll('.bar')
.sort(function(a, b) { return x0(a.car) - x0(b.car); });
var transition = svg.transition().duration(950),
delay = function(d, i) { return i * 50; };
transition.selectAll('.bar')
.delay(delay)
.attr('x', function(d) { return x0(d.car); });
transition.select('.x.axis')
.call(xAxis)
.selectAll('g')
.delay(delay);
};
}
changeHour();
$('select').change(function() { //function to change hourly data
table_i = $(this).val();
var hour = arr1[table_i];
var car=hour.car;
var newobj = [];
for(var hourx1=0;hourx1<car.length;hourx1++){
var xx = car[hourx1];
for (var value in xx) {
var chartvar = newobj.push({car:value,miles:xx[value]});
var data = newobj;
}
}
x.domain(data.map(function(d) { return d.car; }));
y.domain([0, d3.max(data, function(d) { return d.miles; })]);
changeHour();
})
I thought that by updating in the function changeHour I could isolate just the rects and the text that goes with them, and redraw them based on the selected hour's data.
But it just keeps drawing the first hour.
What am I doing wrong?
2 things not working:
firstly "data" needs to be declared without 'var' in the change function at the end. Declaring it with 'var' makes it a local variable to that function, and once you leave that function it's gone. Saying "data = " without the var means you're using the data variable you've declared further up. It's all to do with scope which is something I still struggle with, but basically with 'var' it doesn't work.
var newobj = [];
for(var hourx1=0;hourx1<car.length;hourx1++){
var xx = car[hourx1];
for (var value in xx) {
var chartvar = newobj.push({car:value,miles:xx[value]});
}
}
data = newobj;
Secondly, your changeHour function only looks for new elements as it hangs all its attribute settings on an .enter() selection, changeHour should be like this:
var dataJoin = svg.selectAll('.bar')
.data(data, function(d) { return d.car; });
// possible new elements, fired first time, set non-data dependent attributes
dataJoin
.enter()
.append('rect')
.attr('class', 'bar')
.attr('transform','translate(-20)') //move rects closer to Y axis
// changes to existing elements (now including the newly appended elements from above) which depend on data values (d)
dataJoin
.attr('x', function(d) { return x(d.car); })
.attr('width', x.rangeBand()*1)
.attr('y', function(d) { return y(d.miles); })
.attr('height', function(d) { return height - y(d.miles); });
For completeness there should be a dataJoin.exit().remove() in there as well but its not something that happens in this dataset

d3.js categorical time series (evolustrip)

Working in d3.js, I am looking for a good way to display categorical time series data. The data values cannot co-occur, and are not evenly spaced, so I've data exactly like:
location = [[time1: home], [time4: work], [time5: cafe], [time7: home]]
and so on. My ideal resulting graph is something like what might be called an evolustrip - one way of seeing this chart is as a time series chart with variable width bars, bar color corresponding to category (e.g. 'home').
Can anyone point me in the right direction? Thank you so much!
So I ended up crafting my own d3.js solution:
I used a d3.time.scale scale for the time dimension, and then a d3.scale.category20 scale to provide colors for the categories. I then plotted the categorical data as same-height rects on the time axis by start time, and used the d3.time.scale scale to compute the appropriate bin width for each rect.
A reusable component (following the pattern at http://bost.ocks.org/mike/chart/) example can be seen here:
function timeSeriesCategorical() {
var w = 860,
h = 70,
margin = {top: 20, right: 80, bottom: 30, left: 50},
width = w - margin.left - margin.right,
height = h - margin.top - margin.bottom;
var xValue = function(d) { return d[0]; },
yValue = function(d) { return d[1]; };
var yDomain = null;
var xScale = d3.time.scale()
.range([0, width]);
var yScale = d3.scale.category20();
var xAxis = d3.svg.axis()
.scale(xScale)
.tickSubdivide(1)
.tickSize(-height)
.orient('bottom');
var yAxis = d3.svg.axis()
.scale(yScale)
.ticks(5)
.orient('left');
var binwidth = 20;
function chart(selection) {
selection.each(function(data) {
// convert data to standard representation
data = data.map(function(d, i) {
return [xValue.call(data, d, i), yValue.call(data, d, i)];
//return d;
});
// scale the x and y domains based on the actual data
xScale.domain(d3.extent(data, function(d) { return d[0]; }));
if (!yDomain) {
yScale.domain(d3.extent(data, function(d) { return d[1]; }));
} else {
yScale.domain(yDomain);
}
// compute binwidths for TODO better comment
// d looks like {timestamp, category}
data.forEach(function(d, i) {
if (data[i+1]) {
w_current = xScale(data[i][0]);
w_next = xScale(data[i+1][0]);
binwidth = w_next - w_current;
}
d.binwidth = binwidth;
});
// create chart space as svg
// note: 'this' el should not contain svg already
var svg = d3.select(this).append('svg').data(data);
// external dimensions
svg.attr('width', w)
.attr('height', h);
// internal dimensions
svg = svg.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
// x axis
svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis);
// TODO bars legend
// bars
svg.selectAll('rect')
.data(data)
.enter().append('rect')
.attr('x', function(d, i) { return xScale(d[0]); })
.attr('width', function(d, i) { return d.binwidth; })
.attr('height', height)
.attr('fill', function(d, i) { return yScale(d[1]); })
.attr('stroke', function(d, i) { return yScale(d[1]); });
});
}
chart.x = function(_) {
if (!arguments.length) return xValue;
xValue = _;
return chart;
};
chart.y = function(_) {
if (!arguments.length) return yValue;
yValue = _;
return chart;
};
chart.yDomain = function(_) {
if (!arguments.length) return yDomain;
yDomain = _;
return chart;
};
return chart;
}
and is callable with something like:
d3.csv('./data.csv', function(data) {
var chartActivity = timeSeriesCategorical()
.x(function(d) { return d.when; })
.y(function(d) { return d.activity; })
.yDomain([0,1]);
d3.select('#chart-activity')
.datum(data)
.call(chartActivity);
});
Hopefully this is helpful to someone! The project this was made for is at https://github.com/interaction-design-lab/stress-sense-portal

Reference line for line graph

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]);
})
);

Resources