aboutsummaryrefslogtreecommitdiffstats
path: root/web/src/vendor/benchmark/plugin/ui.browserscope.js
diff options
context:
space:
mode:
Diffstat (limited to 'web/src/vendor/benchmark/plugin/ui.browserscope.js')
-rw-r--r--web/src/vendor/benchmark/plugin/ui.browserscope.js1052
1 files changed, 1052 insertions, 0 deletions
diff --git a/web/src/vendor/benchmark/plugin/ui.browserscope.js b/web/src/vendor/benchmark/plugin/ui.browserscope.js
new file mode 100644
index 00000000..01905bde
--- /dev/null
+++ b/web/src/vendor/benchmark/plugin/ui.browserscope.js
@@ -0,0 +1,1052 @@
+(function(window, document) {
+
+ /** Cache used by various methods */
+ var cache = {
+ 'counter': 0,
+ 'lastAction': 'load',
+ 'lastChart': 'bar',
+ 'lastFilterBy': 'all',
+ 'responses': { /* 'all': null, 'desktop': null, 'major': null, ... */ },
+ 'timers': { /* 'cleanup': null, 'load': null, 'post': null, ... */ },
+ 'trash': createElement('div')
+ };
+
+ /**
+ * Used to filter Browserscope results by browser category.
+ *
+ * @see http://www.browserscope.org/user/tests/howto#urlparams
+ */
+ var filterMap = {
+ 'all': 3,
+ 'desktop': 'top-d',
+ 'family': 0,
+ 'major': 1,
+ 'minor': 2,
+ 'mobile': 'top-m',
+ 'popular': 'top',
+ 'prerelease': 'top-d-e'
+ };
+
+ /** Used to resolve a value's internal [[Class]] */
+ var toString = {}.toString;
+
+ /**
+ * The `uaToken` is prepended to the value of the data cell of the Google
+ * visualization data table object that matches the user's browser name. After
+ * the chart is rendered the element containing the `uaToken` is assigned the
+ * `ui.browserscope.uaClass` class name to allow for the creation of a visual
+ * indicator to help the user more easily find their browser's results.
+ */
+ var uaToken = '\u2028';
+
+ /** Math shortcuts */
+ var floor = Math.floor,
+ max = Math.max,
+ min = Math.min;
+
+ /** Utility shortcuts */
+ var each = Benchmark.each,
+ extend = Benchmark.extend,
+ filter = Benchmark.filter,
+ forOwn = Benchmark.forOwn,
+ formatNumber = Benchmark.formatNumber,
+ hasKey = Benchmark.hasKey,
+ indexOf = Benchmark.indexOf,
+ interpolate = Benchmark.interpolate,
+ invoke = Benchmark.invoke,
+ map = Benchmark.map,
+ reduce = Benchmark.reduce;
+
+ /*--------------------------------------------------------------------------*/
+
+ /**
+ * Registers an event listener.
+ *
+ * @private
+ * @param {Element} element The element.
+ * @param {String} eventName The name of the event to listen to.
+ * @param {Function} handler The event handler.
+ * @returns {Element} The element.
+ */
+ function addListener(element, eventName, handler) {
+ if ((element = typeof element == 'string' ? query(element)[0] : element)) {
+ if (typeof element.addEventListener != 'undefined') {
+ element.addEventListener(eventName, handler, false);
+ } else if (typeof element.attachEvent != 'undefined') {
+ element.attachEvent('on' + eventName, handler);
+ }
+ }
+ return element;
+ }
+
+ /**
+ * Shortcut for `document.createElement()`.
+ *
+ * @private
+ * @param {String} tagName The tag name of the element to create.
+ * @param {String} name A name to assign to the element.
+ * @param {Document|Element} context The document object used to create the element.
+ * @returns {Element} Returns a new element.
+ */
+ function createElement(tagName, name, context) {
+ var result;
+ name && name.nodeType && (context = name, name = 0);
+ context = context ? context.ownerDocument || context : document;
+ name || (name = '');
+
+ try {
+ // set name attribute for IE6/7
+ result = context.createElement('<' + tagName + ' name="' + name + '">');
+ } catch(e) {
+ (result = context.createElement(tagName)).name = name;
+ }
+ return result;
+ }
+
+ /**
+ * Creates a new style element.
+ *
+ * @private
+ * @param {String} cssText The css text of the style element.
+ * @param {Document|Element} context The document object used to create the element.
+ * @returns {Element} Returns the new style element.
+ */
+ function createStyleSheet(cssText, context) {
+ // use a text node, "x", to work around innerHTML issues with style elements
+ // http://msdn.microsoft.com/en-us/library/ms533897(v=vs.85).aspx#1
+ var div = createElement('div', context);
+ div.innerHTML = 'x<style>' + cssText + '</style>';
+ return div.lastChild;
+ }
+
+ /**
+ * Gets the text content of an element.
+ *
+ * @private
+ * @param {Element} element The element.
+ * @returns {String} The text content of the element.
+ */
+ function getText(element) {
+ element = query(element)[0];
+ return element && (element.textContent || element.innerText) || '';
+ }
+
+ /**
+ * Injects a script into the document.
+ *
+ * @private
+ * @param {String} src The external script source.
+ * @param {Object} sibling The element to inject the script after.
+ * @param {Document} context The document object used to create the script element.
+ * @returns {Object} The new script element.
+ */
+ function loadScript(src, sibling, context) {
+ context = sibling ? sibling.ownerDocument || [sibling, sibling = 0][0] : context;
+ var script = createElement('script', context),
+ nextSibling = sibling ? sibling.nextSibling : query('script', context).pop();
+
+ script.src = src;
+ return (sibling || nextSibling).parentNode.insertBefore(script, nextSibling);
+ }
+
+ /**
+ * Queries the document for elements by id or tagName.
+ *
+ * @private
+ * @param {String} selector The css selector to match.
+ * @param {Document|Element} context The element whose descendants are queried.
+ * @returns {Array} The array of results.
+ */
+ function query(selector, context) {
+ var result = [];
+ selector || (selector = '');
+ context = typeof context == 'string' ? query(context)[0] : context || document;
+
+ if (selector.nodeType) {
+ result = [selector];
+ }
+ else if (context) {
+ each(selector.split(','), function(selector) {
+ each(/^#/.test(selector)
+ ? [context.getElementById(selector.slice(1))]
+ : context.getElementsByTagName(selector), function(node) {
+ result.push(node);
+ });
+ });
+ }
+ return result;
+ }
+
+ /**
+ * Set an element's innerHTML property.
+ *
+ * @private
+ * @param {Element} element The element.
+ * @param {String} html The HTML to set.
+ * @param {Object} object The template object used to modify the html.
+ * @returns {Element} The element.
+ */
+ function setHTML(element, html, object) {
+ if ((element = query(element)[0])) {
+ element.innerHTML = interpolate(html, object);
+ }
+ return element;
+ }
+
+ /**
+ * Displays a message in the "results" element.
+ *
+ * @private
+ * @param {String} text The text to display.
+ * @param {Object} object The template object used to modify the text.
+ */
+ function setMessage(text, object) {
+ var me = ui.browserscope,
+ cont = me.container;
+
+ if (cont) {
+ cont.className = 'bs-rt-message';
+ setHTML(cont, text, object);
+ }
+ }
+
+ /*--------------------------------------------------------------------------*/
+
+ /**
+ * Adds a style sheet to the current chart and assigns the `ui.browserscope.uaClass`
+ * class name to the chart element containing the user's browser name.
+ *
+ * @private
+ * @returns {Boolean} Returns `true` if the operation succeeded, else `false`.
+ */
+ function addChartStyle() {
+ var me = ui.browserscope,
+ cssText = [],
+ context = frames[query('iframe', me.container)[0].name].document,
+ chartNodes = query('text,textpath', context),
+ uaClass = me.uaClass,
+ result = false;
+
+ if (chartNodes.length) {
+ // extract CSS rules for `uaClass`
+ each(query('link,style'), function(node) {
+ // avoid access denied errors on external style sheets
+ // outside the same origin policy
+ try {
+ var sheet = node.sheet || node.styleSheet;
+ each(sheet.cssRules || sheet.rules, function(rule) {
+ if ((rule.selectorText || rule.cssText).indexOf('.' + uaClass) > -1) {
+ cssText.push(rule.style && rule.style.cssText || /[^{}]*(?=})/.exec(rule.cssText) || '');
+ }
+ });
+ } catch(e) { }
+ });
+
+ // insert custom style sheet
+ query('head', context)[0].appendChild(
+ createStyleSheet('.' + uaClass + '{' + cssText.join(';') + '}', context));
+
+ // scan chart elements for a match
+ each(chartNodes, function(node) {
+ var nextSibling;
+ if ((node.string || getText(node)).charAt(0) == uaToken) {
+ // for VML
+ if (node.string) {
+ // IE requires reinserting the element to render correctly
+ node.className = uaClass;
+ nextSibling = node.nextSibling;
+ node.parentNode.insertBefore(node.removeNode(), nextSibling);
+ }
+ // for SVG
+ else {
+ node.setAttribute('class', uaClass);
+ }
+ result = true;
+ }
+ });
+ }
+ return result;
+ }
+
+ /**
+ * Periodically executed callback that removes injected script and iframe elements.
+ *
+ * @private
+ */
+ function cleanup() {
+ var me = ui.browserscope,
+ timings = me.timings,
+ timers = cache.timers,
+ trash = cache.trash,
+ delay = timings.cleanup * 1e3;
+
+ // remove injected scripts and old iframes when benchmarks aren't running
+ if (timers.cleanup && !ui.running) {
+ // if expired, destroy the element to prevent pseudo memory leaks.
+ // http://dl.dropbox.com/u/513327/removechild_ie_leak.html
+ each(query('iframe,script'), function(element) {
+ var expire = +(/^browserscope-\d+-(\d+)$/.exec(element.name) || 0)[1] + max(delay, timings.timeout * 1e3);
+ if (new Date > expire || /browserscope\.org|google\.com/.test(element.src)) {
+ trash.appendChild(element);
+ trash.innerHTML = '';
+ }
+ });
+ }
+ // schedule another round
+ timers.cleanup = setTimeout(cleanup, delay);
+ }
+
+ /**
+ * A simple data object cloning utility.
+ *
+ * @private
+ * @param {Mixed} data The data object to clone.
+ * @returns {Mixed} The cloned data object.
+ */
+ function cloneData(data) {
+ var fn,
+ ctor,
+ result = data;
+
+ if (isArray(data)) {
+ result = map(data, cloneData);
+ }
+ else if (data === Object(data)) {
+ ctor = data.constructor;
+ result = ctor == Object ? {} : (fn = function(){}, fn.prototype = ctor.prototype, new fn);
+ forOwn(data, function(value, key) {
+ result[key] = cloneData(value);
+ });
+ }
+ return result;
+ }
+
+ /**
+ * Creates a Browserscope results object.
+ *
+ * @private
+ * @returns {Object|Null} Browserscope results object or null.
+ */
+ function createSnapshot() {
+ // clone benches, exclude those that are errored, unrun, or have hz of Infinity
+ var benches = invoke(filter(ui.benchmarks, 'successful'), 'clone'),
+ fastest = filter(benches, 'fastest'),
+ slowest = filter(benches, 'slowest'),
+ neither = filter(benches, function(bench) {
+ return indexOf(fastest, bench) + indexOf(slowest, bench) == -2;
+ });
+
+ function merge(destination, source) {
+ destination.count = source.count;
+ destination.cycles = source.cycles;
+ destination.hz = source.hz;
+ destination.stats = extend({}, source.stats);
+ }
+
+ // normalize results on slowest in each category
+ each(fastest.concat(slowest), function(bench) {
+ merge(bench, indexOf(fastest, bench) > -1 ? fastest[fastest.length - 1] : slowest[0]);
+ });
+
+ // sort slowest to fastest
+ // (a larger `mean` indicates a slower benchmark)
+ neither.sort(function(a, b) {
+ a = a.stats; b = b.stats;
+ return (a.mean + a.moe > b.mean + b.moe) ? -1 : 1;
+ });
+
+ // normalize the leftover benchmarks
+ reduce(neither, function(prev, bench) {
+ // if the previous slower benchmark is indistinguishable from
+ // the current then use the previous benchmark's values
+ if (prev.compare(bench) == 0) {
+ merge(bench, prev);
+ }
+ return bench;
+ });
+
+ // append benchmark ids for duplicate names or names with no alphanumeric/space characters
+ // and use the upper limit of the confidence interval to compute a lower hz
+ // to avoid recording inflated results caused by a high margin or error
+ return reduce(benches, function(result, bench, key) {
+ var stats = bench.stats;
+ result || (result = {});
+ key = toLabel(bench.name);
+ result[key && !hasKey(result, key) ? key : key + bench.id ] = floor(1 / (stats.mean + stats.moe));
+ return result;
+ }, null);
+ }
+
+ /**
+ * Retrieves the "cells" array from a given Google visualization data row object.
+ *
+ * @private
+ * @param {Object} object The data row object.
+ * @returns {Array} An array of cell objects.
+ */
+ function getDataCells(object) {
+ // resolve cells by duck typing because of munged property names
+ var result = [];
+ forOwn(object, function(value) {
+ return !(isArray(value) && (result = value));
+ });
+ // remove empty entries which occur when not all the tests are recorded
+ return filter(result, Boolean);
+ }
+
+ /**
+ * Retrieves the "labels" array from a given Google visualization data table object.
+ *
+ * @private
+ * @param {Object} object The data table object.
+ * @returns {Array} An array of label objects.
+ */
+ function getDataLabels(object) {
+ var result = [],
+ labelMap = {};
+
+ // resolve labels by duck typing because of munged property names
+ forOwn(object, function(value) {
+ return !(isArray(value) && 0 in value && 'type' in value[0] && (result = value));
+ });
+ // create a data map of labels to names
+ each(ui.benchmarks, function(bench) {
+ var key = toLabel(bench.name);
+ labelMap[key && !hasKey(labelMap, key) ? key : key + bench.id ] = bench.name;
+ });
+ // replace Browserscope's basic labels with benchmark names
+ return each(result, function(cell) {
+ var name = labelMap[cell.label];
+ name && (cell.label = name);
+ });
+ }
+
+ /**
+ * Retrieves the "rows" array from a given Google visualization data table object.
+ *
+ * @private
+ * @param {Object} object The data table object.
+ * @returns {Array} An array of row objects.
+ */
+ function getDataRows(object) {
+ var name,
+ filterBy = cache.lastFilterBy,
+ browserName = toBrowserName(getText(query('strong', '#bs-ua')[0]), filterBy),
+ uaClass = ui.browserscope.uaClass,
+ result = [];
+
+ // resolve rows by duck typing because of munged property names
+ forOwn(object, function(value, key) {
+ return !(isArray(value) && 0 in value && !('type' in value[0]) && (name = key, result = value));
+ });
+ // remove empty rows and set the `p.className` on the browser
+ // name cell that matches the user's browser name
+ if (result.length) {
+ result = object[name] = filter(result, function(value) {
+ var cells = getDataCells(value),
+ first = cells[0],
+ second = cells[1];
+
+ // cells[0] is the browser name cell so instead we check cells[1]
+ // for the presence of ops/sec data to determine if a row is empty or not
+ if (first && second && second.f) {
+ delete first.p.className;
+ if (browserName == toBrowserName(first.f, filterBy)) {
+ first.p.className = uaClass;
+ }
+ return true;
+ }
+ });
+ }
+ return result;
+ }
+
+ /**
+ * Checks if a value has an internal [[Class]] of Array.
+ *
+ * @private
+ * @param {Mixed} value The value to check.
+ * @returns {Boolean} Returns `true` if the value has an internal [[Class]] of
+ * Array, else `false`.
+ */
+ function isArray(value) {
+ return toString.call(value) == '[object Array]';
+ }
+
+ /**
+ * Executes a callback at a given delay interval until it returns `false`.
+ *
+ * @private
+ * @param {Function} callback The function called every poll interval.
+ * @param {Number} delay The delay between callback calls (secs).
+ */
+ function poll(callback, delay) {
+ function poller(init) {
+ if (init || callback() !== false) {
+ setTimeout(poller, delay * 1e3);
+ }
+ }
+ poller(true);
+ }
+
+ /**
+ * Cleans up the last action and sets the current action.
+ *
+ * @private
+ * @param {String} action The current action.
+ */
+ function setAction(action) {
+ clearTimeout(cache.timers[cache.lastAction]);
+ cache.lastAction = action;
+ }
+
+ /**
+ * Converts the browser name version number to the format allowed by the
+ * specified filter.
+ *
+ * @private
+ * @param {String} name The full browser name .
+ * @param {String} filterBy The filter formating rules to apply.
+ * @returns {String} The converted browser name.
+ */
+ function toBrowserName(name, filterBy) {
+ name || (name = '');
+ if (filterBy == 'all') {
+ // truncate something like 1.0.0 to 1
+ name = name.replace(/(\d+)[.0]+$/, '$1');
+ }
+ else if (filterBy == 'family') {
+ // truncate something like XYZ 1.2 to XYZ
+ name = name.replace(/[.\d\s]+$/, '');
+ }
+ else if (/minor|popular/.test(filterBy) && /\d+(?:\.[1-9])+$/.test(name)) {
+ // truncate something like 1.2.3 to 1.2
+ name = name.replace(/(\d+\.[1-9])(\.[.\d]+$)/, '$1');
+ }
+ else {
+ // truncate something like 1.0 to 1 or 1.2.3 to 1 but leave something like 1.2 alone
+ name = name.replace(/(\d+)(?:(\.[1-9]$)|(\.[.\d]+$))/, '$1$2');
+ }
+ return name;
+ }
+
+ /**
+ * Replaces non-alphanumeric characters with spaces because Browserscope labels
+ * can only contain alphanumeric characters and spaces.
+ *
+ * @private
+ * @param {String} text The text to be converted.
+ * @returns {String} The Browserscope safe label text.
+ * @see http://code.google.com/p/browserscope/issues/detail?id=271
+ */
+ function toLabel(text) {
+ return (text || '').replace(/[^a-z0-9]+/gi, ' ');
+ }
+
+ /*--------------------------------------------------------------------------*/
+
+ /**
+ * Loads Browserscope's cumulative results table.
+ *
+ * @static
+ * @memberOf ui.browserscope
+ * @param {Object} options The options object.
+ */
+ function load(options) {
+ options || (options = {});
+
+ var fired,
+ me = ui.browserscope,
+ cont = me.container,
+ filterBy = cache.lastFilterBy = options.filterBy || cache.lastFilterBy,
+ responses = cache.responses,
+ response = cache.responses[filterBy],
+ visualization = window.google && google.visualization;
+
+ function onComplete(response) {
+ var lastResponse = responses[filterBy];
+ if (!fired) {
+ // set the fired flag to avoid Google's own timeout
+ fired = true;
+ // render if the filter is still the same, else cache the result
+ if (filterBy == cache.lastFilterBy) {
+ me.render({ 'force': true, 'response': lastResponse || response });
+ } else if(!lastResponse && response && !response.isError()) {
+ responses[filterBy] = response;
+ }
+ }
+ }
+
+ // set last action in case the load fails and a retry is needed
+ setAction('load');
+
+ // exit early if there is no container element or the response is cached
+ // and retry if the visualization library hasn't loaded yet
+ if (!cont || !visualization || !visualization.Query || response) {
+ cont && onComplete(response);
+ }
+ else if (!ui.running) {
+ // set our own load timeout to display an error message and retry loading
+ cache.timers.load = setTimeout(onComplete, me.timings.timeout * 1e3);
+ // set "loading" message and attempt to load Browserscope data
+ setMessage(me.texts.loading);
+ // request Browserscope pass chart data to `google.visualization.Query.setResponse()`
+ (new visualization.Query(
+ '//www.browserscope.org/gviz_table_data?category=usertest_' + me.key + '&v=' + filterMap[filterBy],
+ { 'sendMethod': 'scriptInjection' }
+ ))
+ .send(onComplete);
+ }
+ }
+
+ /**
+ * Creates a Browserscope beacon and posts the benchmark results.
+ *
+ * @static
+ * @memberOf ui.browserscope
+ */
+ function post() {
+ var idoc,
+ iframe,
+ body = document.body,
+ me = ui.browserscope,
+ key = me.key,
+ timings = me.timings,
+ name = 'browserscope-' + (cache.counter++) + '-' + (+new Date),
+ snapshot = createSnapshot();
+
+ // set last action in case the post fails and a retry is needed
+ setAction('post');
+
+ if (key && snapshot && me.postable && !ui.running && !/Simulator/i.test(Benchmark.platform)) {
+ // create new beacon
+ // (the name contains a timestamp so `cleanup()` can determine when to remove it)
+ iframe = createElement('iframe', name);
+ body.insertBefore(iframe, body.firstChild);
+ idoc = frames[name].document;
+ iframe.style.display = 'none';
+
+ // expose results snapshot
+ me.snapshot = snapshot;
+ // set "posting" message and attempt to post the results snapshot
+ setMessage(me.texts.post);
+ // Note: We originally created an iframe to avoid Browerscope's old limit
+ // of one beacon per page load. It's currently used to implement custom
+ // request timeout and retry routines.
+ idoc.write(interpolate(
+ // the doctype is required so Browserscope detects the correct IE compat mode
+ '#{doctype}<title></title><body><script>' +
+ 'with(parent.ui.browserscope){' +
+ 'var _bTestResults=snapshot,' +
+ '_bC=function(){clearTimeout(_bT);parent.setTimeout(function(){purge();load()},#{refresh}*1e3)},' +
+ '_bT=setTimeout(function(){_bC=function(){};render()},#{timeout}*1e3)' +
+ '}<\/script>' +
+ '<script src=//www.browserscope.org/user/beacon/#{key}?callback=_bC><\/script>',
+ {
+ 'doctype': /css/i.test(document.compatMode) ? '<!doctype html>' : '',
+ 'key': key,
+ 'refresh': timings.refresh,
+ 'timeout': timings.timeout
+ }
+ ));
+ // avoid the IE spinner of doom
+ // http://www.google.com/search?q=IE+throbber+of+doom
+ idoc.close();
+ }
+ else {
+ me.load();
+ }
+ }
+
+ /**
+ * Purges the Browserscope response cache.
+ *
+ * @static
+ * @memberOf ui.browserscope
+ * @param {String} key The key of a single cache entry to clear.
+ */
+ function purge(key) {
+ // we don't pave the cache object with a new one to preserve existing references
+ var responses = cache.responses;
+ if (key) {
+ delete responses[key];
+ } else {
+ forOwn(responses, function(value, key) {
+ delete responses[key];
+ });
+ }
+ }
+
+ /**
+ * Renders the cumulative results table.
+ * (tweak the dimensions and styles to best fit your environment)
+ *
+ * @static
+ * @memberOf ui.browserscope
+ * @param {Object} options The options object.
+ */
+ function render(options) {
+ options || (options = {});
+
+ // coordinates, dimensions, and sizes are in px
+ var areaHeight,
+ cellWidth,
+ data,
+ labels,
+ rowCount,
+ rows,
+ me = ui.browserscope,
+ cont = me.container,
+ responses = cache.responses,
+ visualization = window.google && google.visualization,
+ lastChart = cache.lastChart,
+ chart = cache.lastChart = options.chart || lastChart,
+ lastFilterBy = cache.lastFilterBy,
+ filterBy = cache.lastFilterBy = options.filterBy || lastFilterBy,
+ lastResponse = responses[filterBy],
+ response = responses[filterBy] = 'response' in options ? (response = options.response) && !response.isError() && response : lastResponse,
+ areaWidth = '100%',
+ cellHeight = 80,
+ fontSize = 13,
+ height = 'auto',
+ hTitle = 'operations per second (higher is better)',
+ hTitleHeight = 48,
+ left = 240,
+ legend = 'top',
+ maxChars = 0,
+ maxCharsLimit = 20,
+ maxOps = 0,
+ minHeight = 480,
+ minWidth = cont && cont.offsetWidth || 948,
+ title = '',
+ top = 50,
+ vTitle = '',
+ vTitleWidth = 48,
+ width = minWidth;
+
+ function retry(force) {
+ var action = cache.lastAction;
+ if (force || ui.running) {
+ cache.timers[action] = setTimeout(retry, me.timings.retry * 1e3);
+ } else {
+ me[action].apply(me, action == 'render' ? [options] : []);
+ }
+ }
+
+ // set action to clear any timeouts and prep for retries
+ setAction(response ? 'render' : cache.lastAction);
+
+ // exit early if there is no container element, the data filter has changed or nothing has changed
+ if (!cont || visualization && (filterBy != lastFilterBy ||
+ (!options.force && chart == lastChart && response == lastResponse))) {
+ cont && filterBy != lastFilterBy && load(options);
+ }
+ // retry if response data is empty/errored or the visualization library hasn't loaded yet
+ else if (!response || !visualization) {
+ // set error message for empty/errored response
+ !response && visualization && setMessage(me.texts.error);
+ retry(true);
+ }
+ // visualization chart gallary
+ // http://code.google.com/apis/chart/interactive/docs/gallery.html
+ else if (!ui.running) {
+ cont.className = '';
+ data = cloneData(response.getDataTable());
+ labels = getDataLabels(data);
+ rows = getDataRows(data);
+ rowCount = rows.length;
+ chart = chart.charAt(0).toUpperCase() + chart.slice(1).toLowerCase();
+
+ // adjust data for non-tabular displays
+ if (chart != 'Table') {
+ // remove "# Tests" run count label (without label data the row will be ignored)
+ labels.pop();
+
+ // modify row data
+ each(rows, function(row) {
+ each(getDataCells(row), function(cell, index, cells) {
+ var lastIndex = cells.length - 1;
+
+ // cells[1] through cells[lastIndex - 1] are ops/sec cells
+ if (/^[\d.,]+$/.test(cell.f)) {
+ // assign ops/sec as cell value
+ cell.v = +cell.f.replace(/,/g, '');
+ // add rate to the text
+ cell.f += ' ops/sec';
+ // capture highest ops value to use when computing the left coordinate
+ maxOps = max(maxOps, cell.v);
+ }
+ // cells[0] is the browser name cell
+ // cells[lastIndex] is the run count cell and has no `f` property
+ else if (cell.f) {
+ // add test run count to browser name
+ cell.f += chart == 'Pie' ? '' : ' (' + (cells[lastIndex].v || 1) + ')';
+ // capture longest char count to use when computing left coordinate/cell width
+ maxChars = min(maxCharsLimit, max(maxChars, cell.f.length));
+ }
+ // compute sum of all ops/sec for pie charts
+ if (chart == 'Pie') {
+ if (index == lastIndex) {
+ cells[1].f = formatNumber(cells[1].v) + ' total ops/sec';
+ } else if (index > 1 && typeof cell.v == 'number') {
+ cells[1].v += cell.v;
+ }
+ }
+ // if the browser name matches the user's browser then style it
+ if (cell.p && cell.p.className) {
+ // prefix the browser name with a line separator (\u2028) because it's not rendered
+ // (IE may render a negligible space in the tooltip of browser names truncated with ellipsis)
+ cell.f = uaToken + cell.f;
+ // poll until the chart elements exist and are styled
+ poll(function() { return !addChartStyle(); }, 0.01);
+ }
+ });
+ });
+
+ // adjust captions and chart dimensions
+ if (chart == 'Bar') {
+ // use minHeight to avoid sizing issues when there is only 1 bar
+ height = max(minHeight, top + (rowCount * cellHeight));
+ // compute left by adding the longest approximate vAxis text width and
+ // a right pad of 10px
+ left = (maxChars * (fontSize / 1.6)) + 10;
+ // get percentage of width left after subtracting the chart's left
+ // coordinate and room for the ops/sec number
+ areaWidth = (100 - (((left + 50) / width) * 100)) + '%';
+ }
+ else {
+ // swap captions (the browser list caption is blank to conserve space)
+ vTitle = [hTitle, hTitle = vTitle][0];
+ height = minHeight;
+
+ if (chart == 'Pie') {
+ legend = 'right';
+ title = 'Total operations per second by browser (higher is better)';
+ }
+ else {
+ hTitleHeight = 28;
+ // compute left by getting the sum of the horizontal space wanted
+ // for the vAxis title's width, the approximate vAxis text width, and
+ // the 13px gap between the chart and the right side of the vAxis text
+ left = vTitleWidth + (formatNumber(maxOps).length * (fontSize / 1.6)) + 13;
+ // compute cell width by adding the longest approximate hAxis text
+ // width and wiggle room of 26px
+ cellWidth = (maxChars * (fontSize / 2)) + 26;
+ // use minWidth to avoid clipping the key
+ width = max(minWidth, left + (rowCount * cellWidth));
+ }
+ }
+ // get percentage of height left after subtracting the vertical space wanted
+ // for the hAxis title's height, text size, the chart's top coordinate,
+ // and the 8px gap between the chart and the top of the hAxis text
+ areaHeight = (100 - (((hTitleHeight + fontSize + top + 8) / height) * 100)) + '%';
+ // make chart type recognizable
+ chart += 'Chart';
+ }
+
+ if (rowCount && visualization[chart]) {
+ new visualization[chart](cont).draw(data, {
+ 'colors': ui.browserscope.colors,
+ 'fontSize': fontSize,
+ 'is3D': true,
+ 'legend': legend,
+ 'height': height,
+ 'title': title,
+ 'width': width,
+ 'chartArea': { 'height': areaHeight, 'left': left, 'top': top, 'width': areaWidth },
+ 'hAxis': { 'baseline': 0, 'title': hTitle },
+ 'vAxis': { 'baseline': 0, 'title': vTitle }
+ });
+ } else {
+ setMessage(me.texts.empty);
+ }
+ }
+ }
+
+ /*--------------------------------------------------------------------------*/
+
+ // expose
+ ui.browserscope = {
+
+ /**
+ * Your Browserscope API key.
+ *
+ * @memberOf ui.browserscope
+ * @type String
+ */
+ 'key': '',
+
+ /**
+ * A flag to indicate if posting is enabled or disabled.
+ *
+ * @memberOf ui.browserscope
+ * @type Boolean
+ */
+ 'postable': true,
+
+ /**
+ * The selector of the element to contain the entire Browserscope UI.
+ *
+ * @memberOf ui.browserscope
+ * @type String
+ */
+ 'selector': '',
+
+ /**
+ * The class name used to style the user's browser name when it appears
+ * in charts.
+ *
+ * @memberOf ui.browserscope
+ * @type String
+ */
+ 'uaClass': 'rt-ua-cur',
+
+ /**
+ * Object containing various timings settings.
+ *
+ * @memberOf ui.browserscope
+ * @type Object
+ */
+ 'timings': {
+
+ /**
+ * The delay between removing abandoned script and iframe elements (secs).
+ *
+ * @memberOf ui.browserscope.timings
+ * @type Number
+ */
+ 'cleanup': 10,
+
+ /**
+ * The delay before refreshing the cumulative results after posting (secs).
+ *
+ * @memberOf ui.browserscope.timings
+ * @type Number
+ */
+ 'refresh': 3,
+
+ /**
+ * The delay between load attempts (secs).
+ *
+ * @memberOf ui.browserscope.timings
+ * @type Number
+ */
+ 'retry': 5,
+
+ /**
+ * The time to wait for a request to finish (secs).
+ *
+ * @memberOf ui.browserscope.timings
+ * @type Number
+ */
+ 'timeout': 10
+ },
+
+ /**
+ * Object containing various text messages.
+ *
+ * @memberOf ui.browserscope
+ * @type Object
+ */
+ 'texts': {
+
+ /**
+ * The text shown when their is no recorded data available to report.
+ *
+ * @memberOf ui.browserscope.texts
+ * @type String
+ */
+ 'empty': 'No data available',
+
+ /**
+ * The text shown when the cumulative results data cannot be retrieved.
+ *
+ * @memberOf ui.browserscope.texts
+ * @type String
+ */
+ 'error': 'The get/post request has failed :(',
+
+ /**
+ * The text shown while waiting for the cumulative results data to load.
+ *
+ * @memberOf ui.browserscope.texts
+ * @type String
+ */
+ 'loading': 'Loading cumulative results data&hellip;',
+
+ /**
+ * The text shown while posting the results snapshot to Browserscope.
+ *
+ * @memberOf ui.browserscope.texts
+ * @type String
+ */
+ 'post': 'Posting results snapshot&hellip;',
+
+ /**
+ * The text shown while benchmarks are running.
+ *
+ * @memberOf ui.browserscope.texts
+ * @type String
+ */
+ 'wait': 'Benchmarks running. Please wait&hellip;'
+ },
+
+ // loads cumulative results table
+ 'load': load,
+
+ // posts benchmark snapshot to Browserscope
+ 'post': post,
+
+ // purges the Browserscope response cache
+ 'purge': purge,
+
+ // renders cumulative results table
+ 'render': render
+ };
+
+ /*--------------------------------------------------------------------------*/
+
+ addListener(window, 'load', function() {
+ var me = ui.browserscope,
+ key = me.key,
+ placeholder = key && query(me.selector)[0];
+
+ // create results html
+ if (placeholder) {
+ setHTML(placeholder,
+ '<h1 id=bs-logo><a href=//www.browserscope.org/user/tests/table/#{key}>' +
+ '<span>Browserscope</span></a></h1>' +
+ '<div class=bs-rt><div id=bs-chart></div></div>',
+ { 'key': key });
+
+ // the element the charts are inserted into
+ me.container = query('#bs-chart')[0];
+
+ // Browserscope's UA div is inserted before an element with the id of "bs-ua-script"
+ loadScript('//www.browserscope.org/ua?o=js', me.container).id = 'bs-ua-script';
+
+ // the "autoload" string can be created with
+ // http://code.google.com/apis/loader/autoloader-wizard.html
+ loadScript('//www.google.com/jsapi?autoload=' + encodeURIComponent('{' +
+ 'modules:[{' +
+ 'name:"visualization",' +
+ 'version:1,' +
+ 'packages:["corechart","table"],' +
+ 'callback:ui.browserscope.load' +
+ '}]' +
+ '}'));
+
+ // init garbage collector
+ cleanup();
+ }
+ });
+
+ // hide the chart while benchmarks are running
+ ui.on('start', function() {
+ setMessage(ui.browserscope.texts.wait);
+ })
+ .on('abort', function() {
+ ui.browserscope.render({ 'force': true });
+ });
+
+}(this, document));