Skip to content

Instantly share code, notes, and snippets.

@johan
Forked from johan/amo.js
Created October 23, 2011 18:28
Show Gist options
  • Save johan/1307679 to your computer and use it in GitHub Desktop.
Save johan/1307679 to your computer and use it in GitHub Desktop.
Greasemonkey users by browser version (6 months)
<!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="load_amo_stats.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; }
.axis {
shape-rendering: crispEdges;
}
.x.axis .minor {
stroke-opacity: .5;
}
.x.axis path {
display: none;
}
.y.axis line, .y.axis path {
fill: none;
stroke: #000;
}
</style>
</head>
<body>
<script type="text/javascript">
var w = 960
, h = 500
, a = 748 // Greasemonkey
, d = 365 >> 1 // number of days; AMO seems to cap somewhere below a year
, p = [20, 80, 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()
, xs = d3.time.scale().range([0, w - p[1] - p[3]])
, xAxis = d3.svg.axis().scale(xs).tickSize(5)
, yAxis = d3.svg.axis().scale(y).ticks(4).orient("right")
, 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 dwim_sort(a, b, order) {
function n(c) { return String.fromCharCode(c); }
order = order || d3.ascending;
return order(a.replace(/\d+/g, n), b.replace(/\d+/g, n));
}
function draw(data, keys) {
// Transpose the data into layers by browser name+version.
var values = d3.layout.stack()(keys.map(function(key) {
return data.map(function(d) {
return { x: d.date, y: d[key], n: key };
}).sort(function(a, b) { return dwim_sort(a.n, b.n); });
}));
// Compute the x-domain (by date) and y-domain (by top).
x.domain(values[0].map(function(d) { return d.x; }));
y.domain([0, d3.max(data, function(d) { return d.total; })]).nice();
// Add a group for each browser version
var key = svg.selectAll("g.key")
.data(values)
.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());
y.domain(y.domain().reverse()); // enumerate y axis in [high, low] order
xs.domain([data[0].date, data[data.length-1].date]);
svg.append("svg:g")
.attr("class", "y axis")
.attr("transform", "translate("+ (w - p[1] - p[3]) +","+ (p[0] + p[2] - h) +")")
.call(yAxis);
// Add the x-axis.
svg.append("svg:g")
.attr("class", "x axis")
.call(xAxis);
}
</script>
</body>
</html>
// 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;
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment