diff options
Diffstat (limited to 'web/src/vendor/benchmark/plugin/ui.browserscope.js')
-rw-r--r-- | web/src/vendor/benchmark/plugin/ui.browserscope.js | 1052 |
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…', + + /** + * The text shown while posting the results snapshot to Browserscope. + * + * @memberOf ui.browserscope.texts + * @type String + */ + 'post': 'Posting results snapshot…', + + /** + * The text shown while benchmarks are running. + * + * @memberOf ui.browserscope.texts + * @type String + */ + 'wait': 'Benchmarks running. Please wait…' + }, + + // 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)); |