Skip to content

Instantly share code, notes, and snippets.

@jlord
Created August 9, 2012 22:59
Show Gist options
  • Save jlord/3308806 to your computer and use it in GitHub Desktop.
Save jlord/3308806 to your computer and use it in GitHub Desktop.
Create a table from a Google Spreadsheet
/*!
* accounting.js v0.3.2
* Copyright 2011, Joss Crowcroft
*
* Freely distributable under the MIT license.
* Portions of accounting.js are inspired or borrowed from underscore.js
*
* Full details and documentation:
* http://josscrowcroft.github.com/accounting.js/
*/
(function(root, undefined) {
/* --- Setup --- */
// Create the local library object, to be exported or referenced globally later
var lib = {};
// Current version
lib.version = '0.3.2';
/* --- Exposed settings --- */
// The library's settings configuration object. Contains default parameters for
// currency and number formatting
lib.settings = {
currency: {
symbol : "$", // default currency symbol is '$'
format : "%s%v", // controls output: %s = symbol, %v = value (can be object, see docs)
decimal : ".", // decimal point separator
thousand : ",", // thousands separator
precision : 2, // decimal places
grouping : 3 // digit grouping (not implemented yet)
},
number: {
precision : 0, // default precision on numbers is 0
grouping : 3, // digit grouping (not implemented yet)
thousand : ",",
decimal : "."
}
};
/* --- Internal Helper Methods --- */
// Store reference to possibly-available ECMAScript 5 methods for later
var nativeMap = Array.prototype.map,
nativeIsArray = Array.isArray,
toString = Object.prototype.toString;
/**
* Tests whether supplied parameter is a string
* from underscore.js
*/
function isString(obj) {
return !!(obj === '' || (obj && obj.charCodeAt && obj.substr));
}
/**
* Tests whether supplied parameter is a string
* from underscore.js, delegates to ECMA5's native Array.isArray
*/
function isArray(obj) {
return nativeIsArray ? nativeIsArray(obj) : toString.call(obj) === '[object Array]';
}
/**
* Tests whether supplied parameter is a true object
*/
function isObject(obj) {
return obj && toString.call(obj) === '[object Object]';
}
/**
* Extends an object with a defaults object, similar to underscore's _.defaults
*
* Used for abstracting parameter handling from API methods
*/
function defaults(object, defs) {
var key;
object = object || {};
defs = defs || {};
// Iterate over object non-prototype properties:
for (key in defs) {
if (defs.hasOwnProperty(key)) {
// Replace values with defaults only if undefined (allow empty/zero values):
if (object[key] == null) object[key] = defs[key];
}
}
return object;
}
/**
* Implementation of `Array.map()` for iteration loops
*
* Returns a new Array as a result of calling `iterator` on each array value.
* Defers to native Array.map if available
*/
function map(obj, iterator, context) {
var results = [], i, j;
if (!obj) return results;
// Use native .map method if it exists:
if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
// Fallback for native .map:
for (i = 0, j = obj.length; i < j; i++ ) {
results[i] = iterator.call(context, obj[i], i, obj);
}
return results;
}
/**
* Check and normalise the value of precision (must be positive integer)
*/
function checkPrecision(val, base) {
val = Math.round(Math.abs(val));
return isNaN(val)? base : val;
}
/**
* Parses a format string or object and returns format obj for use in rendering
*
* `format` is either a string with the default (positive) format, or object
* containing `pos` (required), `neg` and `zero` values (or a function returning
* either a string or object)
*
* Either string or format.pos must contain "%v" (value) to be valid
*/
function checkCurrencyFormat(format) {
var defaults = lib.settings.currency.format;
// Allow function as format parameter (should return string or object):
if ( typeof format === "function" ) format = format();
// Format can be a string, in which case `value` ("%v") must be present:
if ( isString( format ) && format.match("%v") ) {
// Create and return positive, negative and zero formats:
return {
pos : format,
neg : format.replace("-", "").replace("%v", "-%v"),
zero : format
};
// If no format, or object is missing valid positive value, use defaults:
} else if ( !format || !format.pos || !format.pos.match("%v") ) {
// If defaults is a string, casts it to an object for faster checking next time:
return ( !isString( defaults ) ) ? defaults : lib.settings.currency.format = {
pos : defaults,
neg : defaults.replace("%v", "-%v"),
zero : defaults
};
}
// Otherwise, assume format was fine:
return format;
}
/* --- API Methods --- */
/**
* Takes a string/array of strings, removes all formatting/cruft and returns the raw float value
* alias: accounting.`parse(string)`
*
* Decimal must be included in the regular expression to match floats (defaults to
* accounting.settings.number.decimal), so if the number uses a non-standard decimal
* separator, provide it as the second argument.
*
* Also matches bracketed negatives (eg. "$ (1.99)" => -1.99)
*
* Doesn't throw any errors (`NaN`s become 0) but this may change in future
*/
var unformat = lib.unformat = lib.parse = function(value, decimal) {
// Recursively unformat arrays:
if (isArray(value)) {
return map(value, function(val) {
return unformat(val, decimal);
});
}
// Fails silently (need decent errors):
value = value || 0;
// Return the value as-is if it's already a number:
if (typeof value === "number") return value;
// Default decimal point comes from settings, but could be set to eg. "," in opts:
decimal = decimal || lib.settings.number.decimal;
// Build regex to strip out everything except digits, decimal point and minus sign:
var regex = new RegExp("[^0-9-" + decimal + "]", ["g"]),
unformatted = parseFloat(
("" + value)
.replace(/\((.*)\)/, "-$1") // replace bracketed values with negatives
.replace(regex, '') // strip out any cruft
.replace(decimal, '.') // make sure decimal point is standard
);
// This will fail silently which may cause trouble, let's wait and see:
return !isNaN(unformatted) ? unformatted : 0;
};
/**
* Implementation of toFixed() that treats floats more like decimals
*
* Fixes binary rounding issues (eg. (0.615).toFixed(2) === "0.61") that present
* problems for accounting- and finance-related software.
*/
var toFixed = lib.toFixed = function(value, precision) {
precision = checkPrecision(precision, lib.settings.number.precision);
var power = Math.pow(10, precision);
// Multiply up by precision, round accurately, then divide and use native toFixed():
return (Math.round(lib.unformat(value) * power) / power).toFixed(precision);
};
/**
* Format a number, with comma-separated thousands and custom precision/decimal places
*
* Localise by overriding the precision and thousand / decimal separators
* 2nd parameter `precision` can be an object matching `settings.number`
*/
var formatNumber = lib.formatNumber = function(number, precision, thousand, decimal) {
// Resursively format arrays:
if (isArray(number)) {
return map(number, function(val) {
return formatNumber(val, precision, thousand, decimal);
});
}
// Clean up number:
number = unformat(number);
// Build options object from second param (if object) or all params, extending defaults:
var opts = defaults(
(isObject(precision) ? precision : {
precision : precision,
thousand : thousand,
decimal : decimal
}),
lib.settings.number
),
// Clean up precision
usePrecision = checkPrecision(opts.precision),
// Do some calc:
negative = number < 0 ? "-" : "",
base = parseInt(toFixed(Math.abs(number || 0), usePrecision), 10) + "",
mod = base.length > 3 ? base.length % 3 : 0;
// Format the number:
return negative + (mod ? base.substr(0, mod) + opts.thousand : "") + base.substr(mod).replace(/(\d{3})(?=\d)/g, "$1" + opts.thousand) + (usePrecision ? opts.decimal + toFixed(Math.abs(number), usePrecision).split('.')[1] : "");
};
/**
* Format a number into currency
*
* Usage: accounting.formatMoney(number, symbol, precision, thousandsSep, decimalSep, format)
* defaults: (0, "$", 2, ",", ".", "%s%v")
*
* Localise by overriding the symbol, precision, thousand / decimal separators and format
* Second param can be an object matching `settings.currency` which is the easiest way.
*
* To do: tidy up the parameters
*/
var formatMoney = lib.formatMoney = function(number, symbol, precision, thousand, decimal, format) {
// Resursively format arrays:
if (isArray(number)) {
return map(number, function(val){
return formatMoney(val, symbol, precision, thousand, decimal, format);
});
}
// Clean up number:
number = unformat(number);
// Build options object from second param (if object) or all params, extending defaults:
var opts = defaults(
(isObject(symbol) ? symbol : {
symbol : symbol,
precision : precision,
thousand : thousand,
decimal : decimal,
format : format
}),
lib.settings.currency
),
// Check format (returns object with pos, neg and zero):
formats = checkCurrencyFormat(opts.format),
// Choose which format to use for this value:
useFormat = number > 0 ? formats.pos : number < 0 ? formats.neg : formats.zero;
// Return with currency symbol added:
return useFormat.replace('%s', opts.symbol).replace('%v', formatNumber(Math.abs(number), checkPrecision(opts.precision), opts.thousand, opts.decimal));
};
/**
* Format a list of numbers into an accounting column, padding with whitespace
* to line up currency symbols, thousand separators and decimals places
*
* List should be an array of numbers
* Second parameter can be an object containing keys that match the params
*
* Returns array of accouting-formatted number strings of same length
*
* NB: `white-space:pre` CSS rule is required on the list container to prevent
* browsers from collapsing the whitespace in the output strings.
*/
lib.formatColumn = function(list, symbol, precision, thousand, decimal, format) {
if (!list) return [];
// Build options object from second param (if object) or all params, extending defaults:
var opts = defaults(
(isObject(symbol) ? symbol : {
symbol : symbol,
precision : precision,
thousand : thousand,
decimal : decimal,
format : format
}),
lib.settings.currency
),
// Check format (returns object with pos, neg and zero), only need pos for now:
formats = checkCurrencyFormat(opts.format),
// Whether to pad at start of string or after currency symbol:
padAfterSymbol = formats.pos.indexOf("%s") < formats.pos.indexOf("%v") ? true : false,
// Store value for the length of the longest string in the column:
maxLength = 0,
// Format the list according to options, store the length of the longest string:
formatted = map(list, function(val, i) {
if (isArray(val)) {
// Recursively format columns if list is a multi-dimensional array:
return lib.formatColumn(val, opts);
} else {
// Clean up the value
val = unformat(val);
// Choose which format to use for this value (pos, neg or zero):
var useFormat = val > 0 ? formats.pos : val < 0 ? formats.neg : formats.zero,
// Format this value, push into formatted list and save the length:
fVal = useFormat.replace('%s', opts.symbol).replace('%v', formatNumber(Math.abs(val), checkPrecision(opts.precision), opts.thousand, opts.decimal));
if (fVal.length > maxLength) maxLength = fVal.length;
return fVal;
}
});
// Pad each number in the list and send back the column of numbers:
return map(formatted, function(val, i) {
// Only if this is a string (not a nested array, which would have already been padded):
if (isString(val) && val.length < maxLength) {
// Depending on symbol position, pad after symbol or at index 0:
return padAfterSymbol ? val.replace(opts.symbol, opts.symbol+(new Array(maxLength - val.length + 1).join(" "))) : (new Array(maxLength - val.length + 1).join(" ")) + val;
}
return val;
});
};
/* --- Module Definition --- */
// Export accounting for CommonJS. If being loaded as an AMD module, define it as such.
// Otherwise, just add `accounting` to the global object
if (typeof exports !== 'undefined') {
if (typeof module !== 'undefined' && module.exports) {
exports = module.exports = lib;
}
exports.accounting = lib;
} else if (typeof define === 'function' && define.amd) {
// Return the library as an AMD module:
define([], function() {
return lib;
});
} else {
// Use accounting.noConflict to restore `accounting` back to its original value.
// Returns a reference to the library's `accounting` object;
// e.g. `var numbers = accounting.noConflict();`
lib.noConflict = (function(oldAccounting) {
return function() {
// Reset the value of the root's `accounting` variable:
root.accounting = oldAccounting;
// Delete the noConflict method:
lib.noConflict = undefined;
// Return reference to the library to re-assign it:
return lib;
};
})(root.accounting);
// Declare `fx` on the root (global/window) object:
root['accounting'] = lib;
}
// Root will be `window` in browser or `global` on the server:
}(this));
/*!
ICanHaz.js version 0.10 -- by @HenrikJoreteg
More info at: http://icanhazjs.com
*/
(function () {
/*
mustache.js — Logic-less templates in JavaScript
See http://mustache.github.com/ for more info.
*/
var Mustache = function () {
var _toString = Object.prototype.toString;
Array.isArray = Array.isArray || function (obj) {
return _toString.call(obj) == "[object Array]";
}
var _trim = String.prototype.trim, trim;
if (_trim) {
trim = function (text) {
return text == null ? "" : _trim.call(text);
}
} else {
var trimLeft, trimRight;
// IE doesn't match non-breaking spaces with \s.
if ((/\S/).test("\xA0")) {
trimLeft = /^[\s\xA0]+/;
trimRight = /[\s\xA0]+$/;
} else {
trimLeft = /^\s+/;
trimRight = /\s+$/;
}
trim = function (text) {
return text == null ? "" :
text.toString().replace(trimLeft, "").replace(trimRight, "");
}
}
var escapeMap = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': '&quot;',
"'": '&#39;'
};
function escapeHTML(string) {
return String(string).replace(/&(?!\w+;)|[<>"']/g, function (s) {
return escapeMap[s] || s;
});
}
var regexCache = {};
var Renderer = function () {};
Renderer.prototype = {
otag: "{{",
ctag: "}}",
pragmas: {},
buffer: [],
pragmas_implemented: {
"IMPLICIT-ITERATOR": true
},
context: {},
render: function (template, context, partials, in_recursion) {
// reset buffer & set context
if (!in_recursion) {
this.context = context;
this.buffer = []; // TODO: make this non-lazy
}
// fail fast
if (!this.includes("", template)) {
if (in_recursion) {
return template;
} else {
this.send(template);
return;
}
}
// get the pragmas together
template = this.render_pragmas(template);
// render the template
var html = this.render_section(template, context, partials);
// render_section did not find any sections, we still need to render the tags
if (html === false) {
html = this.render_tags(template, context, partials, in_recursion);
}
if (in_recursion) {
return html;
} else {
this.sendLines(html);
}
},
/*
Sends parsed lines
*/
send: function (line) {
if (line !== "") {
this.buffer.push(line);
}
},
sendLines: function (text) {
if (text) {
var lines = text.split("\n");
for (var i = 0; i < lines.length; i++) {
this.send(lines[i]);
}
}
},
/*
Looks for %PRAGMAS
*/
render_pragmas: function (template) {
// no pragmas
if (!this.includes("%", template)) {
return template;
}
var that = this;
var regex = this.getCachedRegex("render_pragmas", function (otag, ctag) {
return new RegExp(otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" + ctag, "g");
});
return template.replace(regex, function (match, pragma, options) {
if (!that.pragmas_implemented[pragma]) {
throw({message:
"This implementation of mustache doesn't understand the '" +
pragma + "' pragma"});
}
that.pragmas[pragma] = {};
if (options) {
var opts = options.split("=");
that.pragmas[pragma][opts[0]] = opts[1];
}
return "";
// ignore unknown pragmas silently
});
},
/*
Tries to find a partial in the curent scope and render it
*/
render_partial: function (name, context, partials) {
name = trim(name);
if (!partials || partials[name] === undefined) {
throw({message: "unknown_partial '" + name + "'"});
}
if (!context || typeof context[name] != "object") {
return this.render(partials[name], context, partials, true);
}
return this.render(partials[name], context[name], partials, true);
},
/*
Renders inverted (^) and normal (#) sections
*/
render_section: function (template, context, partials) {
if (!this.includes("#", template) && !this.includes("^", template)) {
// did not render anything, there were no sections
return false;
}
var that = this;
var regex = this.getCachedRegex("render_section", function (otag, ctag) {
// This regex matches _the first_ section ({{#foo}}{{/foo}}), and captures the remainder
return new RegExp(
"^([\\s\\S]*?)" + // all the crap at the beginning that is not {{*}} ($1)
otag + // {{
"(\\^|\\#)\\s*(.+)\\s*" + // #foo (# == $2, foo == $3)
ctag + // }}
"\n*([\\s\\S]*?)" + // between the tag ($2). leading newlines are dropped
otag + // {{
"\\/\\s*\\3\\s*" + // /foo (backreference to the opening tag).
ctag + // }}
"\\s*([\\s\\S]*)$", // everything else in the string ($4). leading whitespace is dropped.
"g");
});
// for each {{#foo}}{{/foo}} section do...
return template.replace(regex, function (match, before, type, name, content, after) {
// before contains only tags, no sections
var renderedBefore = before ? that.render_tags(before, context, partials, true) : "",
// after may contain both sections and tags, so use full rendering function
renderedAfter = after ? that.render(after, context, partials, true) : "",
// will be computed below
renderedContent,
value = that.find(name, context);
if (type === "^") { // inverted section
if (!value || Array.isArray(value) && value.length === 0) {
// false or empty list, render it
renderedContent = that.render(content, context, partials, true);
} else {
renderedContent = "";
}
} else if (type === "#") { // normal section
if (Array.isArray(value)) { // Enumerable, Let's loop!
renderedContent = that.map(value, function (row) {
return that.render(content, that.create_context(row), partials, true);
}).join("");
} else if (that.is_object(value)) { // Object, Use it as subcontext!
renderedContent = that.render(content, that.create_context(value),
partials, true);
} else if (typeof value == "function") {
// higher order section
renderedContent = value.call(context, content, function (text) {
return that.render(text, context, partials, true);
});
} else if (value) { // boolean section
renderedContent = that.render(content, context, partials, true);
} else {
renderedContent = "";
}
}
return renderedBefore + renderedContent + renderedAfter;
});
},
/*
Replace {{foo}} and friends with values from our view
*/
render_tags: function (template, context, partials, in_recursion) {
// tit for tat
var that = this;
var new_regex = function () {
return that.getCachedRegex("render_tags", function (otag, ctag) {
return new RegExp(otag + "(=|!|>|&|\\{|%)?([^#\\^]+?)\\1?" + ctag + "+", "g");
});
};
var regex = new_regex();
var tag_replace_callback = function (match, operator, name) {
switch(operator) {
case "!": // ignore comments
return "";
case "=": // set new delimiters, rebuild the replace regexp
that.set_delimiters(name);
regex = new_regex();
return "";
case ">": // render partial
return that.render_partial(name, context, partials);
case "{": // the triple mustache is unescaped
case "&": // & operator is an alternative unescape method
return that.find(name, context);
default: // escape the value
return escapeHTML(that.find(name, context));
}
};
var lines = template.split("\n");
for(var i = 0; i < lines.length; i++) {
lines[i] = lines[i].replace(regex, tag_replace_callback, this);
if (!in_recursion) {
this.send(lines[i]);
}
}
if (in_recursion) {
return lines.join("\n");
}
},
set_delimiters: function (delimiters) {
var dels = delimiters.split(" ");
this.otag = this.escape_regex(dels[0]);
this.ctag = this.escape_regex(dels[1]);
},
escape_regex: function (text) {
// thank you Simon Willison
if (!arguments.callee.sRE) {
var specials = [
'/', '.', '*', '+', '?', '|',
'(', ')', '[', ']', '{', '}', '\\'
];
arguments.callee.sRE = new RegExp(
'(\\' + specials.join('|\\') + ')', 'g'
);
}
return text.replace(arguments.callee.sRE, '\\$1');
},
/*
find `name` in current `context`. That is find me a value
from the view object
*/
find: function (name, context) {
name = trim(name);
// Checks whether a value is thruthy or false or 0
function is_kinda_truthy(bool) {
return bool === false || bool === 0 || bool;
}
var value;
// check for dot notation eg. foo.bar
if (name.match(/([a-z_]+)\./ig)) {
var childValue = this.walk_context(name, context);
if (is_kinda_truthy(childValue)) {
value = childValue;
}
} else {
if (is_kinda_truthy(context[name])) {
value = context[name];
} else if (is_kinda_truthy(this.context[name])) {
value = this.context[name];
}
}
if (typeof value == "function") {
return value.apply(context);
}
if (value !== undefined) {
return value;
}
// silently ignore unkown variables
return "";
},
walk_context: function (name, context) {
var path = name.split('.');
// if the var doesn't exist in current context, check the top level context
var value_context = (context[path[0]] != undefined) ? context : this.context;
var value = value_context[path.shift()];
while (value != undefined && path.length > 0) {
value_context = value;
value = value[path.shift()];
}
// if the value is a function, call it, binding the correct context
if (typeof value == "function") {
return value.apply(value_context);
}
return value;
},
// Utility methods
/* includes tag */
includes: function (needle, haystack) {
return haystack.indexOf(this.otag + needle) != -1;
},
// by @langalex, support for arrays of strings
create_context: function (_context) {
if (this.is_object(_context)) {
return _context;
} else {
var iterator = ".";
if (this.pragmas["IMPLICIT-ITERATOR"]) {
iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator;
}
var ctx = {};
ctx[iterator] = _context;
return ctx;
}
},
is_object: function (a) {
return a && typeof a == "object";
},
/*
Why, why, why? Because IE. Cry, cry cry.
*/
map: function (array, fn) {
if (typeof array.map == "function") {
return array.map(fn);
} else {
var r = [];
var l = array.length;
for(var i = 0; i < l; i++) {
r.push(fn(array[i]));
}
return r;
}
},
getCachedRegex: function (name, generator) {
var byOtag = regexCache[this.otag];
if (!byOtag) {
byOtag = regexCache[this.otag] = {};
}
var byCtag = byOtag[this.ctag];
if (!byCtag) {
byCtag = byOtag[this.ctag] = {};
}
var regex = byCtag[name];
if (!regex) {
regex = byCtag[name] = generator(this.otag, this.ctag);
}
return regex;
}
};
return({
name: "mustache.js",
version: "0.4.0",
/*
Turns a template and view into HTML
*/
to_html: function (template, view, partials, send_fun) {
var renderer = new Renderer();
if (send_fun) {
renderer.send = send_fun;
}
renderer.render(template, view || {}, partials);
if (!send_fun) {
return renderer.buffer.join("\n");
}
}
});
}();
/*!
ICanHaz.js -- by @HenrikJoreteg
*/
/*global */
(function () {
function trim(stuff) {
if (''.trim) return stuff.trim();
else return stuff.replace(/^\s+/, '').replace(/\s+$/, '');
}
var ich = {
VERSION: "0.10",
templates: {},
// grab jquery or zepto if it's there
$: (typeof window !== 'undefined') ? window.jQuery || window.Zepto || null : null,
// public function for adding templates
// can take a name and template string arguments
// or can take an object with name/template pairs
// We're enforcing uniqueness to avoid accidental template overwrites.
// If you want a different template, it should have a different name.
addTemplate: function (name, templateString) {
if (typeof name === 'object') {
for (var template in name) {
this.addTemplate(template, name[template]);
}
return;
}
if (ich[name]) {
console.error("Invalid name: " + name + ".");
} else if (ich.templates[name]) {
console.error("Template \"" + name + " \" exists");
} else {
ich.templates[name] = templateString;
ich[name] = function (data, raw) {
data = data || {};
var result = Mustache.to_html(ich.templates[name], data, ich.templates);
return (ich.$ && !raw) ? ich.$(result) : result;
};
}
},
// clears all retrieval functions and empties cache
clearAll: function () {
for (var key in ich.templates) {
delete ich[key];
}
ich.templates = {};
},
// clears/grabs
refresh: function () {
ich.clearAll();
ich.grabTemplates();
},
// grabs templates from the DOM and caches them.
// Loop through and add templates.
// Whitespace at beginning and end of all templates inside <script> tags will
// be trimmed. If you want whitespace around a partial, add it in the parent,
// not the partial. Or do it explicitly using <br/> or &nbsp;
grabTemplates: function () {
var i,
scripts = document.getElementsByTagName('script'),
script,
trash = [];
for (i = 0, l = scripts.length; i < l; i++) {
script = scripts[i];
if (script && script.innerHTML && script.id && (script.type === "text/html" || script.type === "text/x-icanhaz")) {
ich.addTemplate(script.id, trim(script.innerHTML));
trash.unshift(script);
}
}
for (i = 0, l = trash.length; i < l; i++) {
trash[i].parentNode.removeChild(trash[i]);
}
}
};
// Use CommonJS if applicable
if (typeof require !== 'undefined') {
module.exports = ich;
} else {
// else attach it to the window
window.ich = ich;
}
if (typeof document !== 'undefined') {
if (ich.$) {
ich.$(function () {
ich.grabTemplates();
});
} else {
document.addEventListener('DOMContentLoaded', function () {
ich.grabTemplates();
}, true);
}
}
})();
})();
<!doctype html>
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7" lang="en"> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8" lang="en"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9" lang="en"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en"> <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title></title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width">
<link rel="stylesheet" href="style.css">
<script src="tabletop.js" type="text/javascript"></script>
<script src="ICanHaz.js" type="text/javascript"></script>
<script src="accounting.js" type="text/javascript"></script>
</head>
<body>
<!--[if lt IE 7]><p class=chromeframe>Your browser is <em>ancient!</em> <a href="http://browsehappy.com/">Upgrade to a different browser</a> or <a href="http://www.google.com/chromeframe/?redirect=true">install Google Chrome Frame</a> to experience this site.</p><![endif]-->
<div id="table"></div>
<div id="stats"></div>
<script id="stats" type="text/html">
<h4>{{numberActive}} of {{numberTotalProjects}} projects are active</h4>
<h4>{{numberCompletedProjects}} projects are completed</h4>
<h4>{{totalSpent}} has been spent as of {{currentDate}} </h4>
</script>
<script id="schedule" type="text/html">
<table><thead><tr><td>PROJECT</td><td>Total</td><td>2012</td><td>2013</td><td>2014</td><td>2015</td><td>2016</td><td>2017</td><td>2018</td><td>2019</td></tr>
{{#rows}}
<tr><td>{{project}}</td><td>{{total}}</td><td>{{year2012}}</td><td>{{year2013}}</td><td>{{year2014}}</td><td>{{year2015}}</td><td>{{year2016}}</td><td>{{year2017}}</td><td>{{year2018}}</td><td>{{year2019}}</td></tr>
{{/rows}}
</table>
</script>
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', runWebsite)
function runWebsite() {
var public_spreadsheet_url = 'https://docs.google.com/spreadsheet/pub?key=0Aj3c4mZCQQaMdGE2TVphOWlXMUMyclRXa2Z1c0g5MGc&single=true&gid=1&output=html';
Tabletop.init( { key: public_spreadsheet_url,
callback: showInfo,
simpleSheet: true } )
function showInfo(data, tabletop) {
var edProjects = getType(data, "Economic Development")
var drProjects = getType(data, "Debt Retirement")
var raProjects = getType(data, "Rec & Cultural Arts")
var psProjects = getType(data, "Public Safety")
var numberActive = getActiveProjects(data).length
var numberTotalProjects = 14
var numberCompletedProjects = completedProjects(data)
var totalSpent = amountSpent(data)
var schedule = ich.schedule({
"rows": raProjects
})
var stats = ich.stats({
"numberActive": numberActive,
"numberTotalProjects": numberTotalProjects,
"numberCompletedProjects": numberCompletedProjects,
"totalSpent": accounting.formatMoney(totalSpent),
"currentDate": getCurrentYear()
})
document.getElementById('table').innerHTML = schedule;
document.getElementById('stats').innerHTML = stats;
}
}
function getCurrentYear() {
return new Date().getFullYear()
}
function completedProjects(projects) {
var completed = 0
projects.forEach(function (project) {
if (!hasActiveFuture(project)) completed = completed + 1
})
return completed
}
function isActive(element) {
var currentYear = "year" + getCurrentYear()
var dollars = element[currentYear]
if (dollars > 0) return true
return false
}
function amountSpent(projects) {
var spent = 0
projects.forEach(function (project) {
var currentYear = "year" + getCurrentYear()
var funds = parseInt(project[currentYear])
if (funds > 0) spent = spent + funds
getPreviousYears().forEach(function (year) {
var funds = parseInt(project[year])
if (funds > 0) spent = spent + funds
})
})
return spent
}
function getType(projects, projectFilter) {
var filteredProjects = []
projects.forEach(function getType(element) {
var type = "type"
var projectType = element[type]
if (projectType == projectFilter) filteredProjects.push(element)
})
return filteredProjects
}
function isComplete(element) {
var currentYear = "year" + getCurrentYear()
var dollars = element[currentYear]
if (dollars > 0) return true
return false
}
function getPreviousYears() {
var currentYear = "year" + getCurrentYear()
var years = ["year2012", "year2013", "year2014", "year2015", "year2016", "year2017", "year2018", "year2019"]
return years.slice(0, years.indexOf(currentYear))
}
function getFutureYears() {
var currentYear = "year" + getCurrentYear()
var years = ["year2012", "year2013", "year2014", "year2015", "year2016", "year2017", "year2018", "year2019"]
return years.slice(years.indexOf(currentYear))
}
function hasActiveFuture(element) {
var activeFuture = false
getFutureYears().forEach(function (year){
if (element[year] > 0) activeFuture = true
})
return activeFuture
}
function getActiveProjects(projects) {
var activeProjects = []
projects.forEach(function getActive(element) {
if (isActive(element)) activeProjects.push(element)
})
return activeProjects
}
</script>
</body>
</html>
/* HTML5 Boilerplate */
article, aside, details, figcaption, figure, footer, header, hgroup, nav, section { display: block; }
audio, canvas, video { display: inline-block; *display: inline; *zoom: 1; }
audio:not([controls]) { display: none; }
[hidden] { display: none; }
html { font-size: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
html, button, input, select, textarea { font-family: sans-serif; color: #222; }
body { margin: 0; font-size: 1em; line-height: 1.4; }
::-moz-selection { background: #fe57a1; color: #fff; text-shadow: none; }
::selection { background: #fe57a1; color: #fff; text-shadow: none; }
a { color: #00e; }
a:visited { color: #551a8b; }
a:hover { color: #06e; }
a:focus { outline: thin dotted; }
a:hover, a:active { outline: 0; }
abbr[title] { border-bottom: 1px dotted; }
b, strong { font-weight: bold; }
blockquote { margin: 1em 40px; }
dfn { font-style: italic; }
hr { display: block; height: 1px; border: 0; border-top: 1px solid #ccc; margin: 1em 0; padding: 0; }
ins { background: #ff9; color: #000; text-decoration: none; }
mark { background: #ff0; color: #000; font-style: italic; font-weight: bold; }
pre, code, kbd, samp { font-family: monospace, serif; _font-family: 'courier new', monospace; font-size: 1em; }
pre { white-space: pre; white-space: pre-wrap; word-wrap: break-word; }
q { quotes: none; }
q:before, q:after { content: ""; content: none; }
small { font-size: 85%; }
sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; }
sup { top: -0.5em; }
sub { bottom: -0.25em; }
ul, ol { margin: 1em 0; padding: 0 0 0 40px; }
dd { margin: 0 0 0 40px; }
nav ul, nav ol { list-style: none; list-style-image: none; margin: 0; padding: 0; }
img { border: 0; -ms-interpolation-mode: bicubic; vertical-align: middle; }
svg:not(:root) { overflow: hidden; }
figure { margin: 0; }
form { margin: 0; }
fieldset { border: 0; margin: 0; padding: 0; }
label { cursor: pointer; }
legend { border: 0; *margin-left: -7px; padding: 0; white-space: normal; }
button, input, select, textarea { font-size: 100%; margin: 0; vertical-align: baseline; *vertical-align: middle; }
button, input { line-height: normal; }
button, input[type="button"], input[type="reset"], input[type="submit"] { cursor: pointer; -webkit-appearance: button; *overflow: visible; }
button[disabled], input[disabled] { cursor: default; }
input[type="checkbox"], input[type="radio"] { box-sizing: border-box; padding: 0; *width: 13px; *height: 13px; }
input[type="search"] { -webkit-appearance: textfield; -moz-box-sizing: content-box; -webkit-box-sizing: content-box; box-sizing: content-box; }
input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; }
button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; }
textarea { overflow: auto; vertical-align: top; resize: vertical; }
input:valid, textarea:valid { }
input:invalid, textarea:invalid { background-color: #f0dddd; }
table { border-collapse: collapse; border-spacing: 0; }
td { vertical-align: top; }
.chromeframe { margin: 0.2em 0; background: #ccc; color: black; padding: 0.2em 0; }
@media only screen and (min-width: 35em) {
}
.ir { display: block; border: 0; text-indent: -999em; overflow: hidden; background-color: transparent; background-repeat: no-repeat; text-align: left; direction: ltr; *line-height: 0; }
.ir br { display: none; }
.hidden { display: none !important; visibility: hidden; }
.visuallyhidden { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; }
.visuallyhidden.focusable:active, .visuallyhidden.focusable:focus { clip: auto; height: auto; margin: 0; overflow: visible; position: static; width: auto; }
.invisible { visibility: hidden; }
.clearfix:before, .clearfix:after { content: ""; display: table; }
.clearfix:after { clear: both; }
.clearfix { *zoom: 1; }
@media print {
* { background: transparent !important; color: black !important; box-shadow:none !important; text-shadow: none !important; filter:none !important; -ms-filter: none !important; }
a, a:visited { text-decoration: underline; }
a[href]:after { content: " (" attr(href) ")"; }
abbr[title]:after { content: " (" attr(title) ")"; }
.ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { content: ""; }
pre, blockquote { border: 1px solid #999; page-break-inside: avoid; }
thead { display: table-header-group; }
tr, img { page-break-inside: avoid; }
img { max-width: 100% !important; }
@page { margin: 0.5cm; }
p, h2, h3 { orphans: 3; widows: 3; }
h2, h3 { page-break-after: avoid; }
}
#holder {
width: 700px;
height: 700px;
}
(function(global) {
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function (obj, fromIndex) {
if (fromIndex == null) {
fromIndex = 0;
} else if (fromIndex < 0) {
fromIndex = Math.max(0, this.length + fromIndex);
}
for (var i = fromIndex, j = this.length; i < j; i++) {
if (this[i] === obj)
return i;
}
return -1;
};
}
/*
Initialize with Tabletop.init( { key: '0AjAPaAU9MeLFdHUxTlJiVVRYNGRJQnRmSnQwTlpoUXc' } )
OR!
Initialize with Tabletop.init( { key: 'https://docs.google.com/spreadsheet/pub?hl=en_US&hl=en_US&key=0AjAPaAU9MeLFdHUxTlJiVVRYNGRJQnRmSnQwTlpoUXc&output=html&widget=true' } )
OR!
Initialize with Tabletop.init('0AjAPaAU9MeLFdHUxTlJiVVRYNGRJQnRmSnQwTlpoUXc')
*/
"use strict";
var Tabletop = global.Tabletop = function(options) {
// Make sure Tabletop is being used as a constructor no matter what.
if(!this || this === global) {
return new Tabletop(options);
}
if(typeof(options) == 'string') {
options = { key : options };
}
this.callback = options.callback;
this.wanted = options.wanted || [];
this.key = options.key;
this.simpleSheet = !!options.simpleSheet;
this.parseNumbers = !!options.parseNumbers;
this.wait = !!options.wait;
this.postProcess = options.postProcess;
this.debug = !!options.debug;
/* Be friendly about what you accept */
if(/key=/.test(this.key)) {
this.log("You passed a key as a URL! Attempting to parse.");
this.key = this.key.match("key=(.*?)&")[1];
}
if(!this.key) {
alert("You need to pass Tabletop a key!");
return;
}
this.log("Initializing with key %s", this.key);
this.models = {};
this.model_names = [];
this.base_json_url = "https://spreadsheets.google.com/feeds/worksheets/" + this.key + "/public/basic?alt=json-in-script";
if(!this.wait) {
this.fetch();
}
};
// A global storage for callbacks.
Tabletop.callbacks = {};
// Backwards compatibility.
Tabletop.init = function(options) {
return new Tabletop(options);
};
Tabletop.sheets = function() {
alert("Times have changed! You'll want to use var tabletop = Tabletop.init(...); tabletop.sheets(...); instead of Tabletop.sheets(...)");
};
Tabletop.prototype = {
fetch: function(callback) {
if(typeof(callback) !== "undefined") {
this.callback = callback;
}
this.injectScript(this.base_json_url, this.loadSheets);
},
/*
Insert the URL into the page as a script tag. Once it's loaded the spreadsheet data
it triggers the callback. This helps you avoid cross-domain errors
http://code.google.com/apis/gdata/samples/spreadsheet_sample.html
Let's be plain-Jane and not use jQuery or anything.
*/
injectScript: function(url, callback) {
var script = document.createElement('script'),
self = this,
callbackName = 'tt' + (+new Date()) + (Math.floor(Math.random()*100000));
// Create a temp callback which will get removed once it has executed,
// this allows multiple instances of Tabletop to coexist.
Tabletop.callbacks[ callbackName ] = function () {
var args = Array.prototype.slice.call( arguments, 0 );
callback.apply(self, args);
script.parentNode.removeChild(script);
delete Tabletop.callbacks[callbackName];
};
url = url + "&callback=" + 'Tabletop.callbacks.' + callbackName;
script.src = url;
document.getElementsByTagName('script')[0].parentNode.appendChild(script);
},
/*
Is this a sheet you want to pull?
If { wanted: ["Sheet1"] } has been specified, only Sheet1 is imported
Pulls all sheets if none are specified
*/
isWanted: function(sheetName) {
if(this.wanted.length === 0) {
return true;
} else {
return this.wanted.indexOf(sheetName) != -1;
}
},
/*
What gets send to the callback
if simpleSheet == true, then don't return an array of Tabletop.this.models,
only return the first one's elements
*/
data: function() {
// If the instance is being queried before the data's been fetched
// then return undefined.
if(this.model_names.length === 0) {
return undefined;
}
if(this.simpleSheet) {
if(this.model_names.length > 1 && this.debug)
console.debug("WARNING You have more than one sheet but are using simple sheet mode! Don't blame me when something goes wrong.");
return this.models[ this.model_names[0] ].all();
} else {
return this.models;
}
},
/*
Add another sheet to the wanted list
*/
addWanted: function(sheet) {
if(this.wanted.indexOf(sheet) == -1) {
this.wanted.push(sheet)
}
},
/*
Load all worksheets of the spreadsheet, turning each into a Tabletop Model.
Need to use injectScript because the worksheet view that you're working from
doesn't actually include the data. The list-based feed (/feeds/list/key..) does, though.
Calls back to loadSheet in order to get the real work done.
Used as a callback for the worksheet-based JSON
*/
loadSheets: function(data) {
var i, ilen;
var toInject = [];
for(i = 0, ilen = data.feed.entry.length; i < ilen ; i++) {
// Only pull in desired sheets to reduce loading
if( this.isWanted(data.feed.entry[i].content.$t) ) {
var sheet_id = data.feed.entry[i].link[3].href.substr( data.feed.entry[i].link[3].href.length - 3, 3);
var json_url = "https://spreadsheets.google.com/feeds/list/" + this.key + "/" + sheet_id + "/public/values?alt=json-in-script";
toInject.push(json_url);
}
}
this.sheetsToLoad = toInject.length;
for(i = 0, ilen = toInject.length; i < ilen; i++) {
this.injectScript(toInject[i], this.loadSheet);
}
},
/*
Access layer for the this.models
.sheets() gets you all of the sheets
.sheets('Sheet1') gets you the sheet named Sheet1
*/
sheets: function(sheetName) {
if(typeof sheetName === "undefined")
return this.models;
else
if(typeof(this.models[ sheetName ]) === "undefined") {
// alert( "Can't find " + sheetName );
return;
} else {
return this.models[ sheetName ];
}
},
/*
Parse a single list-based worksheet, turning it into a Tabletop Model
Used as a callback for the list-based JSON
*/
loadSheet: function(data) {
var model = new Tabletop.Model( { data: data,
parseNumbers: this.parseNumbers,
postProcess: this.postProcess } );
this.models[ model.name ] = model;
if(this.model_names.indexOf(model.name) == -1) {
this.model_names.push(model.name);
}
this.sheetsToLoad--;
if(this.sheetsToLoad === 0)
this.doCallback();
},
/*
Execute the callback upon loading! Rely on this.data() because you might
only request certain pieces of data (i.e. simpleSheet mode)
Tests this.sheetsToLoad just in case a race condition happens to show up
*/
doCallback: function() {
if(this.sheetsToLoad === 0)
this.callback(this.data(), this);
},
log: function(msg) {
if(this.debug) {
if(typeof console !== "undefined" && typeof console.log !== "undefined") {
console.log(msg)
}
}
}
};
/*
Tabletop.Model stores the attribute names and parses the worksheet data
to turn it into something worthwhile
Options should be in the format { data: XXX }, with XXX being the list-based worksheet
*/
Tabletop.Model = function(options) {
var i, j, ilen, jlen;
this.column_names = [];
this.name = options.data.feed.title.$t;
this.elements = [];
this.raw = options.data; // A copy of the sheet's raw data, for accessing minutiae
for(var key in options.data.feed.entry[0]){
if(/^gsx/.test(key))
this.column_names.push( key.replace("gsx$","") );
}
for(i = 0, ilen = options.data.feed.entry.length ; i < ilen; i++) {
var source = options.data.feed.entry[i];
var element = {};
for(var j = 0, jlen = this.column_names.length; j < jlen ; j++) {
var cell = source[ "gsx$" + this.column_names[j] ];
if(options.parseNumbers && cell.$t !== '' && !isNaN(cell.$t))
element[ this.column_names[j] ] = +cell.$t;
else
element[ this.column_names[j] ] = cell.$t;
}
if(element.rowNumber === undefined)
element.rowNumber = i + 1;
if( options.postProcess )
options.postProcess(element);
this.elements.push(element);
}
};
Tabletop.Model.prototype = {
/*
Returns all of the elements (rows) of the worksheet as objects
*/
all: function() {
return this.elements;
},
/*
Return the elements as an array of arrays, instead of an array of objects
*/
toArray: function() {
var array = [],
i, j, ilen, jlen;
for(i = 0, ilen = this.elements.length; i < ilen; i++) {
var row = [];
for(j = 0, jlen = this.column_names.length; j < jlen ; j++) {
row.push( this.elements[i][ this.column_names[j] ] );
}
array.push(row);
}
return array;
}
};
})(this);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment