Shows the distribution of browsers and browser versions of Greasemonkey users over the past 30 days. Data from addons.mozilla.org, and massaged to a usable format by Yahoo Pipes.
-
-
Save johan/1307434 to your computer and use it in GitHub Desktop.
Greasemonkey users by browser version (1 month)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// loads AMO browser stats for an AMO public-stats addon (748 = Greasemonkey) for the | |
// last half a year (or some other number of days), cleans them up and passes them as | |
// cb([{ date: <Date object>, total: N, "Firefox/6.0": N, … }, { date: … }, …]) | |
function get_amo_stats(cb, addon, days) { | |
var pipe = 'ed7df07cb426304321a88e3cb875226c' | |
, nth = get_amo_stats.nth = (get_amo_stats.nth || 0) + 1 | |
, name = 'cb' + nth.toString(36) | |
, now = new Date | |
, from = new Date(+now - 864e5 * (days || 365 >> 1)) | |
, aurl = 'https://addons.mozilla.org/en-US/firefox/statistics/csv/'+ (addon || 748) | |
+ '/application?start='+ y_m_d(from) +'&end='+ y_m_d(now) | |
, purl = 'http://pipes.yahoo.com/pipes/pipe.run?_id='+ pipe +'&_render=json&c=9&u=' | |
+ encodeURIComponent(aurl) + '&x=8&_callback=get_amo_stats.' + name | |
, load = document.createElement('script'); | |
get_amo_stats[name] = function(json) { | |
delete get_amo_stats[name]; | |
document.head.removeChild(load); | |
cb(cleanup(json)); | |
}; | |
load.src = purl; | |
document.head.appendChild(load); | |
} | |
var y_m_d = d3.time.format('%Y-%m-%d') | |
, guids = | |
{ '{ec8030f7-c20a-464f-9b0e-13a3a9e97384}': 'Firefox' | |
, '{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}': 'SeaMonkey' | |
, '{3550f703-e582-4d05-9a08-453d09bdfdc6}': 'ThunderBird' | |
, '{a463f10c-3994-11da-9945-000d60ca027b}': 'Flock' | |
, '{3db10fab-e461-4c80-8b97-957ad5f8ea47}': 'Netscape' | |
, '{a23983c0-fd0e-11dc-95ff-0800200c9a66}': 'Fennec' | |
}; | |
// AMO produces borked csv; the header line is formatted "# Fields: [date;count;...]", | |
// where each ... is a column name. After that, all lines are properly comma-separated | |
// though. Each ... is a {GUID}/n.n[...] value, where the GUID maps to a browser name. | |
// Drop columns that don't match this format or are from unknown GUIDs (these data are | |
// raw, so there's some unknown, some null / undefined / Invalid cruft in there, too). | |
function cleanup(raw) { | |
function legal(col) { return valid.test(col); } | |
function remap(app) { return app.replace(valid, fix); } | |
function fix(o,a,v) { return guids[a.toLowerCase()] + v; } | |
// rows are listed as objects with col_1, ... col_n properties; produce an array | |
function rollup(row) { | |
for (var c = 1, col, arr = []; row.hasOwnProperty(col = 'col_'+ c); c++) | |
arr.push(row[col]); | |
return arr; | |
} | |
function deref(array) { return function(d, i) { return array[i]; }; } | |
var valid = new RegExp( '^('+ Object.keys(guids).join('|').replace(/([{}])/g, '\\$1') | |
+ ')(/\\d+(?:\\.\\d+)*)$', 'i') | |
, raw_h = rollup(raw.value.items[0]).join(',').split(';') | |
, is_ok = [true, true].concat(raw_h.map(legal).slice(2)) | |
, label = ['date', 'total'].concat(raw_h.filter(legal).map(remap)) | |
, y_m_d = d3.time.format('%Y-%m-%d'); | |
valid = deref(is_ok); // is_ok Array<Boolean> => function(x, index) : Boolean | |
valid = (function(test) { return function(a) { return a.filter(test); }; })(valid); | |
return raw.value.items.slice(1).map(rollup).map(valid).map(function(arr) { | |
function stow(val, idx) { data[label[idx]] = idx ? Number(val) : y_m_d.parse(val); } | |
var data = {}; | |
arr.forEach(stow); | |
return data; | |
}); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Greasemonkey users by browser version</title> | |
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.js?1.29.1"></script> | |
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.layout.js?1.29.1"></script> | |
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.time.js?1.29.1"></script> | |
<script type="text/javascript" src="amo.js"></script> | |
<style type="text/css"> | |
svg { | |
width: 960px; | |
height: 500px; | |
border: solid 1px #ccc; | |
font: 10px sans-serif; | |
shape-rendering: crispEdges; | |
} | |
body, html { margin: 0; } | |
</style> | |
</head> | |
<body> | |
<script type="text/javascript"> | |
var w = 960 | |
, h = 500 | |
, a = 748 // Greasemonkey | |
, d = 30 // number of days | |
, p = [20, 50, 30, 20] | |
, x = d3.scale.ordinal().rangeRoundBands([0, w - p[1] - p[3]]) | |
, y = d3.scale.linear().range([0, h - p[0] - p[2]]) | |
, z = d3.scale.category20c() | |
, legend = d3.time.format("%Y-%m-%d") | |
, data, folded; // for ease of peeking at the input data from the js console | |
var svg = d3.select("body").append("svg:svg") | |
.attr("width", w) | |
.attr("height", h) | |
.append("svg:g") | |
.attr("transform", "translate(" + p[3] + "," + (h - p[2]) + ")"); | |
get_amo_stats(got_amo_stats, a, d); | |
function got_amo_stats(stats) { | |
folded = fold_by(data = stats/*, /([^\/]+)\// */); | |
var apps = Object.keys(folded[0]).filter(function(n) { return /[A-Z]/.test(n); }); | |
draw(folded, apps); | |
} | |
// stats: an object with some keys on a "Browser/maj[.min[.build[...]]]" format | |
// output: ditto but with [.build[...]] keys rolled into "Browser/major" keys | |
// regexp: if given, a regexp whose first match group defines how to fold keys | |
// (example: /^(.*\/(?:0|[1-9]\d*\.?\d*))/ folds into x/0, x/1.0, ...) | |
function fold_by(stats, regexp) { | |
function fold(obj) { | |
var data = {}, renamed, key, val; | |
for (key in obj) { | |
val = obj[key]; | |
if ((renamed = rename(key))) | |
data[renamed] = (data[renamed] || 0) + val; | |
else | |
data[key] = val; | |
} | |
return data; | |
} | |
var want = regexp || /^(.*\/\d+)/ // /^(.*\/(?:0|[1-9]\d*\.?\d*))/ | |
, rename = function(k) { var m = want.exec(k); return m && m[1]; }; | |
return stats.map(fold); | |
} | |
function draw(data, keys) { | |
// Transpose the data into layers by browser name+version. | |
var names = d3.layout.stack()(keys.map(function(key) { | |
return data.map(function(d) { | |
return { x: d.date, y: d[key], n: key }; | |
}); | |
})); | |
// Compute the x-domain (by date) and y-domain (by top). | |
x.domain(names[0].map(function(d) { return d.x; })); | |
y.domain([0, d3.max(names[names.length - 1], function(d) { return d.y0 + d.y; })]); | |
// Add a group for each browser version | |
var key = svg.selectAll("g.key") | |
.data(names) | |
.enter().append("svg:g") | |
.attr("class", "key") | |
.style("fill", function(d, i) { return z(i); }) | |
.style("stroke", function(d, i) { return d3.rgb(z(i)).darker(); }); | |
// ...with a browser name tooltip. | |
key.append("svg:title").text(function(d) { return d[0].n; }); | |
// Add a rect for each date. | |
var rect = key.selectAll("rect") | |
.data(Object) | |
.enter().append("svg:rect") | |
.attr("x", function(d) { return x(d.x); }) | |
.attr("y", function(d) { return -y(d.y0) - y(d.y); }) | |
.attr("height", function(d) { return y(d.y); }) | |
.attr("width", x.rangeBand()); | |
// Add a label per date. | |
var label = svg.selectAll("text") | |
.data(x.domain()) | |
.enter().append("svg:text") | |
.attr("x", function(d) { return x(d) + x.rangeBand() / 2; }) | |
.attr("y", 6) | |
.attr("text-anchor", "middle") | |
.attr("dy", ".71em") | |
.text(legend); | |
// Add y-axis rules. | |
var rule = svg.selectAll("g.rule") | |
.data(y.ticks(5)) | |
.enter().append("svg:g") | |
.attr("class", "rule") | |
.attr("transform", function(d) { return "translate(0," + -y(d) + ")"; }); | |
rule.append("svg:line") | |
.attr("x2", w - p[1] - p[3]) | |
.style("stroke", function(d) { return d ? "#fff" : "#000"; }) | |
.style("stroke-opacity", function(d) { return d ? .7 : null; }); | |
rule.append("svg:text") | |
.attr("x", w - p[1] - p[3] + 6) | |
.attr("dy", ".35em") | |
.text(d3.format(",d")); | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment