// 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: , 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 => 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; }); }