├── .bowerrc ├── .gitignore ├── Gruntfile.js ├── LICENSE ├── README.md ├── bower.json ├── build ├── thenmap-1.0.0.js ├── thenmap-1.0.0.min.js ├── thenmap-1.0.1.js ├── thenmap-1.0.1.min.js └── thenmap-1.0.3.min.js ├── examples ├── eastern_block_1980.html ├── french_colonies_1949.html ├── municipality_map_1973.html └── us_states.html ├── js └── tabletop.js ├── package-lock.json ├── package.json └── src ├── styles.css ├── styles.js └── thenmap.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | src/thenmap.tmp.js 4 | src/styles.js 5 | build/* 6 | *~ -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | // Project configuration. 4 | grunt.initConfig({ 5 | pkg: grunt.file.readJSON('package.json'), 6 | 7 | str2js: { 8 | CSS: { 'src/styles.js': ['src/styles.css']} 9 | }, 10 | 11 | includereplace: { 12 | dist: { 13 | src: 'src/thenmap.js', 14 | dest: 'src/thenmap.tmp.js' 15 | } 16 | }, 17 | 18 | concat: { 19 | dist: { 20 | src: ['src/thenmap.tmp.js', 'js/tabletop.js'], 21 | dest: 'src/thenmap.tmp.js', 22 | } 23 | }, 24 | 25 | uglify: { 26 | build: { 27 | src: 'src/thenmap.tmp.js', 28 | dest: 'build/thenmap-<%= pkg.version %>.min.js' 29 | } 30 | }, 31 | 32 | copy: { 33 | main: { 34 | src: 'src/thenmap.tmp.js', 35 | dest: 'build/thenmap-<%= pkg.version %>.js' 36 | } 37 | }, 38 | }); 39 | 40 | grunt.loadNpmTasks('grunt-contrib-uglify'); 41 | grunt.loadNpmTasks('grunt-contrib-copy'); 42 | grunt.loadNpmTasks('grunt-contrib-concat'); 43 | grunt.loadNpmTasks('grunt-include-replace'); 44 | grunt.loadNpmTasks('grunt-string-to-js'); 45 | 46 | // Default task(s). 47 | grunt.registerTask('default', ['str2js', 'includereplace', 'concat', 'uglify', 'copy']); 48 | 49 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 J++ Stockholm / Leonard Wallentin 2 | Except for the [Tabletop.js](https://github.com/jsoma/tabletop) library, 3 | Copyright (c) 2012-2018 Jonathan Soma 4 | All code included is released under the MIT license: 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the "Software"), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Simple Javascript library for accessing the [Thenmap](http://www.thenmap.net) API. This script will fetch data for one date at a time. If you want to create sliders, [showing different dates in one visualization](http://old.thenmap.net), we strongly recommend you to rather fetch _all_ borders in one request. 2 | 3 | ## Getting started 4 | 5 | Start by preparing your Google Spreadsheet with data. The spreadsheet should contain a list of the entities that you want to color. The country is entity in the `id` column and the fill color in the `color` column. Thenmap.js accepts any CSS color syntax (e.g. `#99cccc`, `purple` or `rgb(0, 231, 99)`) 6 | 7 | We use [ISO 3166](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) codes for modern nations and subdivisions, where available. Use these [sample datasets for reference](https://docs.google.com/spreadsheets/d/1dj8qw3I75qudflfkr4wBDipeehnecsSslemkn2j5qRE/edit#gid=0) to get correct shape ids. 8 | 9 | Publish the dataset by clicking __File > Publish to the web__ and __Start publishing__. 10 | 11 | Get the id of the Google spreadsheet from the url and add Thenmap.js to you website with the following code snippet: 12 | 13 | ```html 14 |
15 | 16 | 17 | 25 | ``` 26 | 27 | Thenmap takes the following settings: 28 | 29 |
30 |
`width` 31 |
Will determine the size and viewbox of the SVG 32 |
`height` 33 |
Will determine the size and viewbox of the SVG 34 |
`language` 35 |
Language used for names of geographic areas 36 |
`dataKey` 37 |
The id of the Google spreadsheet containing area classes and colors 38 |
`data` 39 |
An array of with area classes and colors (dataKey will be ignored). To color all French and Portuguese colonies: `[{color: "#492A85", id: "fr-"}, {color: "#238AC2", id: "pt-"}]` 40 |
`map` 41 |
One of the id's listed at [thenmap-api.herokuapp.com/doc/v2](http://thenmap-api.herokuapp.com/doc/v2/#datasets), e.g. `se-7` for Swedish municipalities. The default is `world-2`, nations of the world. Previously known as `dataset` 42 |
`date` 43 | `projection` 44 |
`callback` 45 |
A function that will be called when the map is fully rendered (but not necessarily colored yet). 46 |
47 | 48 | See [the API documentation](http://thenmap-api.herokuapp.com/doc/v2/) for more details on the settings. 49 | 50 | See the examples folder and [the demo page](http://www.thenmap.net/demo) for examples. 51 | 52 | ## CDN, download and building 53 | 54 | The Javascript is hosted on Amazon. To include it from there: 55 | 56 | 57 | 58 | ...or you can simply [download it from there](https://drvkoaf101245.cloudfront.net/thenmap-1.0.6.min.js). 59 | 60 | To clone this repo and build the script yourself: 61 | 62 | npm install -g grunt-cli 63 | git clone https://github.com/rotsee/thenmap-js.git 64 | cd thenmap-js 65 | npm install 66 | grunt 67 | 68 | The latest thenmap-x.x.x.min.js file is now in the `build` folder. 69 | 70 | ## License 71 | This code includes Tabletop.js, copyright (c) 2012-2013 Jonathan Soma, and released under MIT license. 72 | 73 | Everything else is copyright 2018 J++ Stockholm, and [released under MIT license](/LICENSE). 74 | 75 | In short: Feel free to use the code as you want. 76 | 77 | ## Changelog 78 | 79 | * 2.2.2 80 | 81 | * Iron out bugs in colour method 82 | 83 | * 2.2.0 84 | * Add colour method to recolour map 85 | 86 | * 2.1.0 87 | 88 | * Upgrade Tabletop.js to `1.5.4` 89 | * Add data option to pass coloring data directly 90 | * Rename `dataset` to `map`, add deprecation notice. 91 | 92 | * 2.0.0 93 | 94 | * Use v2 of the Thenmap API. Some maps may look different, see [this blog post on what's new](http://jplusplus.org/en/blog/version-two-of-the-thenmap-api/). 95 | * Default dataset is now `world-2` (nations of the world). 96 | 97 | * 1.0.6 98 | 99 | * Remove parts of the debug mode that were ironically buggy. 100 | 101 | * 1.0.5 102 | 103 | * Styling fixes 104 | * Really make hover effect work in Chrome 105 | 106 | * 1.0.4 107 | 108 | * Use only one API module, making both back- and frontend a bit faster 109 | * Make hover effect work in Chrome 110 | 111 | * 1.0.3 112 | 113 | * The data module of the API had a _major_ flaw. That is now fixed, but at the cost of breaking backwards compability. 114 | 115 | * 1.0.2 116 | 117 | * Replace depracated API parameter names 118 | * Resize container element on start 119 | * Hover effect 120 | * Minor code fixes 121 | 122 | * 1.0.1 123 | 124 | * Fix bug where a path would miss a title 125 | 126 | * 1.0.0 127 | 128 | * First version 129 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "thenmap-js", 3 | "version": "0.0.0", 4 | "homepage": "https://github.com/rotsee/thenmap-js", 5 | "authors": [ 6 | "leo.wallentin@gmail.com", 7 | "jens.finnas@gmail.com" 8 | ], 9 | "description": "For making historical maps", 10 | "main": "thenmap.js", 11 | "moduleType": [ 12 | "globals" 13 | ], 14 | "keywords": [ 15 | "maps", 16 | "history", 17 | "borders", 18 | "geojson", 19 | "svg" 20 | ], 21 | "license": "MIT", 22 | "dependencies": { 23 | "tabletop": "1.4.2" 24 | }, 25 | "ignore": [ 26 | "**/.*", 27 | "node_modules", 28 | "bower_components", 29 | "app/_bower_components", 30 | "test", 31 | "tests" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /build/thenmap-1.0.0.js: -------------------------------------------------------------------------------- 1 | var Thenmap = { 2 | 3 | debug: false, 4 | apiUrl: "//thenmap-api.herokuapp.com/v1/", 5 | localApiUrl: "http://localhost:3000/v1/", //for debugging 6 | el: null, //container element 7 | svg: null, //svg element 8 | css: null, //css element for dynamically adding styles 9 | defaultColor: "gainsboro", 10 | 11 | // Default settings that can be overridden by passing arguments to Thenmap 12 | settings: { 13 | width: 800, 14 | height: null, 15 | language: null, 16 | projection: null, 17 | dataKey: null, 18 | dataset: "se-7", 19 | date: new Date().toISOString(), //current date, works in any browser that can display SVG 20 | callback: null 21 | }, 22 | 23 | /* Print debug message to the console 24 | */ 25 | log: function(string) { 26 | if (this.debug) { 27 | console.log(string + "\nIn function:"+arguments.callee.caller.name); 28 | } 29 | }, 30 | /* Entry point 31 | */ 32 | init: function(elIdentifier, options) { 33 | var self = this; 34 | self.ColorLayer.thenmap = self; 35 | 36 | // Apply settings 37 | self.settings = self.utils.extend(self.settings, options); 38 | 39 | if (typeof elIdentifier === "string") { 40 | // If first character is #, remove. While technically a valid 41 | // character in an HTML5 id, it's likely meant as id selector 42 | elIdentifier = elIdentifier.replace(/^#/, ''); 43 | self.el = document.getElementById(elIdentifier); 44 | } else if (elIdentifier.nodeType) { 45 | // User gave us a valid reference to an element 46 | self.el = elIdentifier; 47 | } else { 48 | // not a valid identifier 49 | } 50 | 51 | // create CSS element for dynamic styling 52 | var css = document.createElement("style"); 53 | document.getElementsByTagName("head")[0].appendChild(css); 54 | this.css = css; 55 | 56 | // set global styles 57 | var CSS = CSS || {}; 58 | CSS["src/styles.css"] = 'svg.thenmap {\n stroke: white;\n stroke-width: .25px\n}\n@keyframes loading_data {\n 0% {fill-opacity: .25}\n 100% {fill-opacity: .75}\n}\n.loading_data path {\n animation: loading_data 1s linear infinite alternate;\n}'; 59 | ; 60 | self.extendCss(CSS["src/styles.css"]); 61 | 62 | var httpClient = self.HttpClient; 63 | httpClient.get(self.createApiUrl(), function(response) { 64 | var response_json = JSON.parse(response); 65 | var svgString = response_json.svg; 66 | var data = response_json.data; 67 | 68 | // Something of an hack, to make sure SVG is rendered 69 | // Creating a SVG element will not make the SVG render 70 | // in all browsers. innerHTML will. 71 | var tmp = document.createElement("div"); 72 | tmp.innerHTML = svgString; 73 | self.svg = tmp.getElementsByTagName('svg')[0]; 74 | //append SVG before setting viewBox, to get size 75 | self.el.appendChild(self.svg); 76 | // Do we need to explicitly set viewBox? This must be tested, not least on IE 77 | // var bbox = self.svg.getBBox(); 78 | // self.svg.setAttribute("viewBox", [bbox.x, bbox.y, bbox.width, bbox.height].join(" ")); 79 | 80 | //Apply classes, add titles 81 | var paths=self.el.getElementsByTagName('path'); 82 | var i = paths.length; 83 | while(--i) { 84 | //We must support IE10, so can not use dataset 85 | var data_id = paths[i].getAttribute("data-id"); 86 | if (data_id in data){ 87 | 88 | var title = document.createElementNS("http://www.w3.org/2000/svg","title") 89 | title.textContent = data[data_id].name; 90 | paths[i].appendChild(title); 91 | 92 | //element.className is not available for SVG elements 93 | paths[i].setAttribute("class", data[data_id].class); 94 | 95 | } else { 96 | self.log("no data for shape id" + data_id); 97 | } 98 | 99 | } 100 | 101 | // Color the map if a spreadsheet key is given 102 | if (self.settings.dataKey) { 103 | self.ColorLayer.init(self.settings.dataKey); 104 | } 105 | 106 | if (typeof self.settings.callback === "function"){ 107 | self.settings.callback(null, this); 108 | } 109 | 110 | }); 111 | 112 | }, // function init 113 | 114 | createApiUrl: function() { 115 | var self = this; 116 | var apiUrl = this.debug ? this.localApiUrl : this.apiUrl; 117 | apiUrl += [this.settings.dataset, "svg|data", this.settings.date].join("/"); 118 | // Add url parameters 119 | var options = ["data_props=name|class"]; 120 | ["width", "height", "projection", "language"].forEach(function(key){ 121 | if (self.settings[key] !== null){ 122 | options.push(key + "=" + self.settings[key]); 123 | } 124 | }); 125 | apiUrl += "?" + options.join("&"); 126 | return apiUrl; 127 | }, // function createApiUrl 128 | 129 | /* Add code to the global stylesheet 130 | */ 131 | extendCss: function(code) { 132 | 133 | if (this.css.styleSheet) { 134 | // IE 135 | this.css.styleSheet.cssText += code; 136 | } else { 137 | // Other browsers 138 | this.css.innerHTML += code; 139 | } 140 | 141 | }, 142 | 143 | HttpClient: { 144 | get: function(url, callback) { 145 | var httpRequest = new XMLHttpRequest(); 146 | httpRequest.onreadystatechange = function() { 147 | if (httpRequest.readyState == 4 && httpRequest.status == 200) { 148 | callback(httpRequest.responseText); 149 | } 150 | } 151 | 152 | httpRequest.open( "GET", url, true ); 153 | httpRequest.send( null ); 154 | } 155 | }, // HttpClient 156 | 157 | ColorLayer: { 158 | 159 | /* Fetches data from a Google Spreadsheet using Tabletop 160 | */ 161 | getSpreadsheetData: function(spreadsheetKey, callback) { 162 | Tabletop.init({ 163 | key: spreadsheetKey, 164 | callback: function(data, tabletop) { 165 | callback(data); 166 | }, 167 | simpleSheet: true 168 | }) 169 | }, // getSpreadsheetData 170 | 171 | /* Sanitize and validate a SVG color code 172 | Accepts "#99cccc", "9cc", "green", and "rgb(1,32,42)" 173 | */ 174 | getColorCode: function(string){ 175 | 176 | var string = string.trim(); 177 | var allowedColorNames = ["aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","indianred","indigo","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey"," ","","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","red","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","springgreen","steelblue","tan","teal","thistle","tomato","turquoise","violet","wheat","white","whitesmoke","yellow","yellowgreen"]; 178 | if (/(^#[0-9A-F]{6}$){1,2}/i.test(string)) { 179 | // #00cccc 180 | return string; 181 | } else if (/(^[0-9A-F]{6}$){1,2}/i.test(string)) { 182 | // 00cccc 183 | return "#" + string; 184 | } else if (allowedColorNames.indexOf(string.toLowerCase()) > -1) { // will work for all SVG capable browsers 185 | // green 186 | return string.toLowerCase(); 187 | } else if (/rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/i.test(string)){ 188 | // rgb(123,231,432) 189 | return string.toLowerCase(); 190 | } else { 191 | // *invalid 192 | return this.thenmap.defaultColor; 193 | } 194 | 195 | }, 196 | 197 | /* Colorize map 198 | */ 199 | render: function(data) { 200 | var self = this; 201 | var colors = {} 202 | 203 | /* Create a colors object like this: 204 | { green: [class1, class2], ... } 205 | */ 206 | var i = data.length; 207 | while(i--) { 208 | var d = data[i]; 209 | if (d.color) { 210 | var colorCode = self.getColorCode(d.color); 211 | var selector = "path." + d.id; 212 | if (colorCode in colors){ 213 | colors[colorCode].push(selector); 214 | } else { 215 | colors[colorCode] = [selector]; 216 | } 217 | } 218 | } 219 | 220 | /* build and apply CSS */ 221 | var cssCode = ""; 222 | for (var color in colors){ 223 | cssCode += colors[color].join(", ") + "{fill:" + color + "}\n"; 224 | } 225 | self.thenmap.extendCss(cssCode); 226 | }, // ColorLayer.render 227 | 228 | /* Constructor for thenmap.ColorLayer 229 | */ 230 | init: function(spreadsheetKey) { 231 | var self = this; 232 | 233 | // Add loader class while loading 234 | var oldClassName = self.thenmap.el.className || ""; 235 | self.thenmap.el.className = [oldClassName, "loading_data"].join(" "); 236 | self.getSpreadsheetData(spreadsheetKey, function(data) { 237 | // Remove loader class 238 | self.thenmap.el.className = oldClassName; 239 | //Use data 240 | self.render(data); 241 | }); 242 | } // ColorLayer.init 243 | 244 | }, // ColorLayer 245 | 246 | utils: { 247 | extend: function ( defaults, options ) { 248 | var extended = {}; 249 | var prop; 250 | for (prop in defaults) { 251 | if (Object.prototype.hasOwnProperty.call(defaults, prop)) { 252 | extended[prop] = defaults[prop]; 253 | } 254 | } 255 | for (prop in options) { 256 | if (Object.prototype.hasOwnProperty.call(options, prop)) { 257 | extended[prop] = options[prop]; 258 | } 259 | } 260 | return extended; 261 | } // Extend js object 262 | }// Utils 263 | 264 | }; 265 | (function(global) { 266 | "use strict"; 267 | 268 | var inNodeJS = false; 269 | if (typeof module !== 'undefined' && module.exports) { 270 | inNodeJS = true; 271 | var request = require('request'); 272 | } 273 | 274 | var supportsCORS = false; 275 | var inLegacyIE = false; 276 | try { 277 | var testXHR = new XMLHttpRequest(); 278 | if (typeof testXHR.withCredentials !== 'undefined') { 279 | supportsCORS = true; 280 | } else { 281 | if ("XDomainRequest" in window) { 282 | supportsCORS = true; 283 | inLegacyIE = true; 284 | } 285 | } 286 | } catch (e) { } 287 | 288 | // Create a simple indexOf function for support 289 | // of older browsers. Uses native indexOf if 290 | // available. Code similar to underscores. 291 | // By making a separate function, instead of adding 292 | // to the prototype, we will not break bad for loops 293 | // in older browsers 294 | var indexOfProto = Array.prototype.indexOf; 295 | var ttIndexOf = function(array, item) { 296 | var i = 0, l = array.length; 297 | 298 | if (indexOfProto && array.indexOf === indexOfProto) return array.indexOf(item); 299 | for (; i < l; i++) if (array[i] === item) return i; 300 | return -1; 301 | }; 302 | 303 | /* 304 | Initialize with Tabletop.init( { key: '0AjAPaAU9MeLFdHUxTlJiVVRYNGRJQnRmSnQwTlpoUXc' } ) 305 | OR! 306 | Initialize with Tabletop.init( { key: 'https://docs.google.com/spreadsheet/pub?hl=en_US&hl=en_US&key=0AjAPaAU9MeLFdHUxTlJiVVRYNGRJQnRmSnQwTlpoUXc&output=html&widget=true' } ) 307 | OR! 308 | Initialize with Tabletop.init('0AjAPaAU9MeLFdHUxTlJiVVRYNGRJQnRmSnQwTlpoUXc') 309 | */ 310 | 311 | var Tabletop = function(options) { 312 | // Make sure Tabletop is being used as a constructor no matter what. 313 | if(!this || !(this instanceof Tabletop)) { 314 | return new Tabletop(options); 315 | } 316 | 317 | if(typeof(options) === 'string') { 318 | options = { key : options }; 319 | } 320 | 321 | this.callback = options.callback; 322 | this.wanted = options.wanted || []; 323 | this.key = options.key; 324 | this.simpleSheet = !!options.simpleSheet; 325 | this.parseNumbers = !!options.parseNumbers; 326 | this.wait = !!options.wait; 327 | this.reverse = !!options.reverse; 328 | this.postProcess = options.postProcess; 329 | this.debug = !!options.debug; 330 | this.query = options.query || ''; 331 | this.orderby = options.orderby; 332 | this.endpoint = options.endpoint || "https://spreadsheets.google.com"; 333 | this.singleton = !!options.singleton; 334 | this.simple_url = !!options.simple_url; 335 | this.callbackContext = options.callbackContext; 336 | // Default to on, unless there's a proxy, in which case it's default off 337 | this.prettyColumnNames = typeof(options.prettyColumnNames) == 'undefined' ? !options.proxy : options.prettyColumnNames 338 | 339 | if(typeof(options.proxy) !== 'undefined') { 340 | // Remove trailing slash, it will break the app 341 | this.endpoint = options.proxy.replace(/\/$/,''); 342 | this.simple_url = true; 343 | this.singleton = true; 344 | // Let's only use CORS (straight JSON request) when 345 | // fetching straight from Google 346 | supportsCORS = false; 347 | } 348 | 349 | this.parameterize = options.parameterize || false; 350 | 351 | if(this.singleton) { 352 | if(typeof(Tabletop.singleton) !== 'undefined') { 353 | this.log("WARNING! Tabletop singleton already defined"); 354 | } 355 | Tabletop.singleton = this; 356 | } 357 | 358 | /* Be friendly about what you accept */ 359 | if(/key=/.test(this.key)) { 360 | this.log("You passed an old Google Docs url as the key! Attempting to parse."); 361 | this.key = this.key.match("key=(.*?)(&|#|$)")[1]; 362 | } 363 | 364 | if(/pubhtml/.test(this.key)) { 365 | this.log("You passed a new Google Spreadsheets url as the key! Attempting to parse."); 366 | this.key = this.key.match("d\\/(.*?)\\/pubhtml")[1]; 367 | } 368 | 369 | if(!this.key) { 370 | this.log("You need to pass Tabletop a key!"); 371 | return; 372 | } 373 | 374 | this.log("Initializing with key " + this.key); 375 | 376 | this.models = {}; 377 | this.model_names = []; 378 | 379 | this.base_json_path = "/feeds/worksheets/" + this.key + "/public/basic?alt="; 380 | 381 | if (inNodeJS || supportsCORS) { 382 | this.base_json_path += 'json'; 383 | } else { 384 | this.base_json_path += 'json-in-script'; 385 | } 386 | 387 | if(!this.wait) { 388 | this.fetch(); 389 | } 390 | }; 391 | 392 | // A global storage for callbacks. 393 | Tabletop.callbacks = {}; 394 | 395 | // Backwards compatibility. 396 | Tabletop.init = function(options) { 397 | return new Tabletop(options); 398 | }; 399 | 400 | Tabletop.sheets = function() { 401 | this.log("Times have changed! You'll want to use var tabletop = Tabletop.init(...); tabletop.sheets(...); instead of Tabletop.sheets(...)"); 402 | }; 403 | 404 | Tabletop.prototype = { 405 | 406 | fetch: function(callback) { 407 | if(typeof(callback) !== "undefined") { 408 | this.callback = callback; 409 | } 410 | this.requestData(this.base_json_path, this.loadSheets); 411 | }, 412 | 413 | /* 414 | This will call the environment appropriate request method. 415 | 416 | In browser it will use JSON-P, in node it will use request() 417 | */ 418 | requestData: function(path, callback) { 419 | if (inNodeJS) { 420 | this.serverSideFetch(path, callback); 421 | } else { 422 | //CORS only works in IE8/9 across the same protocol 423 | //You must have your server on HTTPS to talk to Google, or it'll fall back on injection 424 | var protocol = this.endpoint.split("//").shift() || "http"; 425 | if (supportsCORS && (!inLegacyIE || protocol === location.protocol)) { 426 | this.xhrFetch(path, callback); 427 | } else { 428 | this.injectScript(path, callback); 429 | } 430 | } 431 | }, 432 | 433 | /* 434 | Use Cross-Origin XMLHttpRequest to get the data in browsers that support it. 435 | */ 436 | xhrFetch: function(path, callback) { 437 | //support IE8's separate cross-domain object 438 | var xhr = inLegacyIE ? new XDomainRequest() : new XMLHttpRequest(); 439 | xhr.open("GET", this.endpoint + path); 440 | var self = this; 441 | xhr.onload = function() { 442 | try { 443 | var json = JSON.parse(xhr.responseText); 444 | } catch (e) { 445 | console.error(e); 446 | } 447 | callback.call(self, json); 448 | }; 449 | xhr.send(); 450 | }, 451 | 452 | /* 453 | Insert the URL into the page as a script tag. Once it's loaded the spreadsheet data 454 | it triggers the callback. This helps you avoid cross-domain errors 455 | http://code.google.com/apis/gdata/samples/spreadsheet_sample.html 456 | 457 | Let's be plain-Jane and not use jQuery or anything. 458 | */ 459 | injectScript: function(path, callback) { 460 | var script = document.createElement('script'); 461 | var callbackName; 462 | 463 | if(this.singleton) { 464 | if(callback === this.loadSheets) { 465 | callbackName = 'Tabletop.singleton.loadSheets'; 466 | } else if (callback === this.loadSheet) { 467 | callbackName = 'Tabletop.singleton.loadSheet'; 468 | } 469 | } else { 470 | var self = this; 471 | callbackName = 'tt' + (+new Date()) + (Math.floor(Math.random()*100000)); 472 | // Create a temp callback which will get removed once it has executed, 473 | // this allows multiple instances of Tabletop to coexist. 474 | Tabletop.callbacks[ callbackName ] = function () { 475 | var args = Array.prototype.slice.call( arguments, 0 ); 476 | callback.apply(self, args); 477 | script.parentNode.removeChild(script); 478 | delete Tabletop.callbacks[callbackName]; 479 | }; 480 | callbackName = 'Tabletop.callbacks.' + callbackName; 481 | } 482 | 483 | var url = path + "&callback=" + callbackName; 484 | 485 | if(this.simple_url) { 486 | // We've gone down a rabbit hole of passing injectScript the path, so let's 487 | // just pull the sheet_id out of the path like the least efficient worker bees 488 | if(path.indexOf("/list/") !== -1) { 489 | script.src = this.endpoint + "/" + this.key + "-" + path.split("/")[4]; 490 | } else { 491 | script.src = this.endpoint + "/" + this.key; 492 | } 493 | } else { 494 | script.src = this.endpoint + url; 495 | } 496 | 497 | if (this.parameterize) { 498 | script.src = this.parameterize + encodeURIComponent(script.src); 499 | } 500 | 501 | document.getElementsByTagName('script')[0].parentNode.appendChild(script); 502 | }, 503 | 504 | /* 505 | This will only run if tabletop is being run in node.js 506 | */ 507 | serverSideFetch: function(path, callback) { 508 | var self = this 509 | request({url: this.endpoint + path, json: true}, function(err, resp, body) { 510 | if (err) { 511 | return console.error(err); 512 | } 513 | callback.call(self, body); 514 | }); 515 | }, 516 | 517 | /* 518 | Is this a sheet you want to pull? 519 | If { wanted: ["Sheet1"] } has been specified, only Sheet1 is imported 520 | Pulls all sheets if none are specified 521 | */ 522 | isWanted: function(sheetName) { 523 | if(this.wanted.length === 0) { 524 | return true; 525 | } else { 526 | return (ttIndexOf(this.wanted, sheetName) !== -1); 527 | } 528 | }, 529 | 530 | /* 531 | What gets send to the callback 532 | if simpleSheet === true, then don't return an array of Tabletop.this.models, 533 | only return the first one's elements 534 | */ 535 | data: function() { 536 | // If the instance is being queried before the data's been fetched 537 | // then return undefined. 538 | if(this.model_names.length === 0) { 539 | return undefined; 540 | } 541 | if(this.simpleSheet) { 542 | if(this.model_names.length > 1 && this.debug) { 543 | this.log("WARNING You have more than one sheet but are using simple sheet mode! Don't blame me when something goes wrong."); 544 | } 545 | return this.models[ this.model_names[0] ].all(); 546 | } else { 547 | return this.models; 548 | } 549 | }, 550 | 551 | /* 552 | Add another sheet to the wanted list 553 | */ 554 | addWanted: function(sheet) { 555 | if(ttIndexOf(this.wanted, sheet) === -1) { 556 | this.wanted.push(sheet); 557 | } 558 | }, 559 | 560 | /* 561 | Load all worksheets of the spreadsheet, turning each into a Tabletop Model. 562 | Need to use injectScript because the worksheet view that you're working from 563 | doesn't actually include the data. The list-based feed (/feeds/list/key..) does, though. 564 | Calls back to loadSheet in order to get the real work done. 565 | 566 | Used as a callback for the worksheet-based JSON 567 | */ 568 | loadSheets: function(data) { 569 | var i, ilen; 570 | var toLoad = []; 571 | this.foundSheetNames = []; 572 | 573 | for(i = 0, ilen = data.feed.entry.length; i < ilen ; i++) { 574 | this.foundSheetNames.push(data.feed.entry[i].title.$t); 575 | // Only pull in desired sheets to reduce loading 576 | if( this.isWanted(data.feed.entry[i].content.$t) ) { 577 | var linkIdx = data.feed.entry[i].link.length-1; 578 | var sheet_id = data.feed.entry[i].link[linkIdx].href.split('/').pop(); 579 | var json_path = "/feeds/list/" + this.key + "/" + sheet_id + "/public/values?alt=" 580 | if (inNodeJS || supportsCORS) { 581 | json_path += 'json'; 582 | } else { 583 | json_path += 'json-in-script'; 584 | } 585 | if(this.query) { 586 | json_path += "&sq=" + this.query; 587 | } 588 | if(this.orderby) { 589 | json_path += "&orderby=column:" + this.orderby.toLowerCase(); 590 | } 591 | if(this.reverse) { 592 | json_path += "&reverse=true"; 593 | } 594 | toLoad.push(json_path); 595 | } 596 | } 597 | 598 | this.sheetsToLoad = toLoad.length; 599 | for(i = 0, ilen = toLoad.length; i < ilen; i++) { 600 | this.requestData(toLoad[i], this.loadSheet); 601 | } 602 | }, 603 | 604 | /* 605 | Access layer for the this.models 606 | .sheets() gets you all of the sheets 607 | .sheets('Sheet1') gets you the sheet named Sheet1 608 | */ 609 | sheets: function(sheetName) { 610 | if(typeof sheetName === "undefined") { 611 | return this.models; 612 | } else { 613 | if(typeof(this.models[ sheetName ]) === "undefined") { 614 | // alert( "Can't find " + sheetName ); 615 | return; 616 | } else { 617 | return this.models[ sheetName ]; 618 | } 619 | } 620 | }, 621 | 622 | sheetReady: function(model) { 623 | this.models[ model.name ] = model; 624 | if(ttIndexOf(this.model_names, model.name) === -1) { 625 | this.model_names.push(model.name); 626 | } 627 | 628 | this.sheetsToLoad--; 629 | if(this.sheetsToLoad === 0) 630 | this.doCallback(); 631 | }, 632 | 633 | /* 634 | Parse a single list-based worksheet, turning it into a Tabletop Model 635 | 636 | Used as a callback for the list-based JSON 637 | */ 638 | loadSheet: function(data) { 639 | var that = this; 640 | var model = new Tabletop.Model( { data: data, 641 | parseNumbers: this.parseNumbers, 642 | postProcess: this.postProcess, 643 | tabletop: this, 644 | prettyColumnNames: this.prettyColumnNames, 645 | onReady: function() { 646 | that.sheetReady(this); 647 | } } ); 648 | }, 649 | 650 | /* 651 | Execute the callback upon loading! Rely on this.data() because you might 652 | only request certain pieces of data (i.e. simpleSheet mode) 653 | Tests this.sheetsToLoad just in case a race condition happens to show up 654 | */ 655 | doCallback: function() { 656 | if(this.sheetsToLoad === 0) { 657 | this.callback.apply(this.callbackContext || this, [this.data(), this]); 658 | } 659 | }, 660 | 661 | log: function(msg) { 662 | if(this.debug) { 663 | if(typeof console !== "undefined" && typeof console.log !== "undefined") { 664 | Function.prototype.apply.apply(console.log, [console, arguments]); 665 | } 666 | } 667 | } 668 | 669 | }; 670 | 671 | /* 672 | Tabletop.Model stores the attribute names and parses the worksheet data 673 | to turn it into something worthwhile 674 | 675 | Options should be in the format { data: XXX }, with XXX being the list-based worksheet 676 | */ 677 | Tabletop.Model = function(options) { 678 | var i, j, ilen, jlen; 679 | this.column_names = []; 680 | this.name = options.data.feed.title.$t; 681 | this.tabletop = options.tabletop; 682 | this.elements = []; 683 | this.onReady = options.onReady; 684 | this.raw = options.data; // A copy of the sheet's raw data, for accessing minutiae 685 | 686 | if(typeof(options.data.feed.entry) === 'undefined') { 687 | options.tabletop.log("Missing data for " + this.name + ", make sure you didn't forget column headers"); 688 | this.original_columns = []; 689 | this.elements = []; 690 | this.onReady.call(this); 691 | return; 692 | } 693 | 694 | for(var key in options.data.feed.entry[0]){ 695 | if(/^gsx/.test(key)) 696 | this.column_names.push( key.replace("gsx$","") ); 697 | } 698 | 699 | this.original_columns = this.column_names; 700 | 701 | for(i = 0, ilen = options.data.feed.entry.length ; i < ilen; i++) { 702 | var source = options.data.feed.entry[i]; 703 | var element = {}; 704 | for(var j = 0, jlen = this.column_names.length; j < jlen ; j++) { 705 | var cell = source[ "gsx$" + this.column_names[j] ]; 706 | if (typeof(cell) !== 'undefined') { 707 | if(options.parseNumbers && cell.$t !== '' && !isNaN(cell.$t)) 708 | element[ this.column_names[j] ] = +cell.$t; 709 | else 710 | element[ this.column_names[j] ] = cell.$t; 711 | } else { 712 | element[ this.column_names[j] ] = ''; 713 | } 714 | } 715 | if(element.rowNumber === undefined) 716 | element.rowNumber = i + 1; 717 | if( options.postProcess ) 718 | options.postProcess(element); 719 | this.elements.push(element); 720 | } 721 | 722 | if(options.prettyColumnNames) 723 | this.fetchPrettyColumns(); 724 | else 725 | this.onReady.call(this); 726 | }; 727 | 728 | Tabletop.Model.prototype = { 729 | /* 730 | Returns all of the elements (rows) of the worksheet as objects 731 | */ 732 | all: function() { 733 | return this.elements; 734 | }, 735 | 736 | fetchPrettyColumns: function() { 737 | if(!this.raw.feed.link[3]) 738 | return this.ready(); 739 | var cellurl = this.raw.feed.link[3].href.replace('/feeds/list/', '/feeds/cells/').replace('https://spreadsheets.google.com', ''); 740 | var that = this; 741 | this.tabletop.requestData(cellurl, function(data) { 742 | that.loadPrettyColumns(data) 743 | }); 744 | }, 745 | 746 | ready: function() { 747 | this.onReady.call(this); 748 | }, 749 | 750 | /* 751 | * Store column names as an object 752 | * with keys of Google-formatted "columnName" 753 | * and values of human-readable "Column name" 754 | */ 755 | loadPrettyColumns: function(data) { 756 | var pretty_columns = {}; 757 | 758 | var column_names = this.column_names; 759 | 760 | var i = 0; 761 | var l = column_names.length; 762 | 763 | for (; i < l; i++) { 764 | if (typeof data.feed.entry[i].content.$t !== 'undefined') { 765 | pretty_columns[column_names[i]] = data.feed.entry[i].content.$t; 766 | } else { 767 | pretty_columns[column_names[i]] = column_names[i]; 768 | } 769 | } 770 | 771 | this.pretty_columns = pretty_columns; 772 | 773 | this.prettifyElements(); 774 | this.ready(); 775 | }, 776 | 777 | /* 778 | * Go through each row, substitutiting 779 | * Google-formatted "columnName" 780 | * with human-readable "Column name" 781 | */ 782 | prettifyElements: function() { 783 | var pretty_elements = [], 784 | ordered_pretty_names = [], 785 | i, j, ilen, jlen; 786 | 787 | var ordered_pretty_names; 788 | for(j = 0, jlen = this.column_names.length; j < jlen ; j++) { 789 | ordered_pretty_names.push(this.pretty_columns[this.column_names[j]]); 790 | } 791 | 792 | for(i = 0, ilen = this.elements.length; i < ilen; i++) { 793 | var new_element = {}; 794 | for(j = 0, jlen = this.column_names.length; j < jlen ; j++) { 795 | var new_column_name = this.pretty_columns[this.column_names[j]]; 796 | new_element[new_column_name] = this.elements[i][this.column_names[j]]; 797 | } 798 | pretty_elements.push(new_element); 799 | } 800 | this.elements = pretty_elements; 801 | this.column_names = ordered_pretty_names; 802 | }, 803 | 804 | /* 805 | Return the elements as an array of arrays, instead of an array of objects 806 | */ 807 | toArray: function() { 808 | var array = [], 809 | i, j, ilen, jlen; 810 | for(i = 0, ilen = this.elements.length; i < ilen; i++) { 811 | var row = []; 812 | for(j = 0, jlen = this.column_names.length; j < jlen ; j++) { 813 | row.push( this.elements[i][ this.column_names[j] ] ); 814 | } 815 | array.push(row); 816 | } 817 | return array; 818 | } 819 | }; 820 | 821 | if(inNodeJS) { 822 | module.exports = Tabletop; 823 | } else if (typeof define === 'function' && define.amd) { 824 | define(function () { 825 | return Tabletop; 826 | }); 827 | } else { 828 | global.Tabletop = Tabletop; 829 | } 830 | 831 | })(this); 832 | -------------------------------------------------------------------------------- /build/thenmap-1.0.0.min.js: -------------------------------------------------------------------------------- 1 | var Thenmap={debug:!1,apiUrl:"//thenmap-api.herokuapp.com/v1/",localApiUrl:"http://localhost:3000/v1/",el:null,svg:null,css:null,defaultColor:"gainsboro",settings:{width:800,height:null,language:null,projection:null,dataKey:null,dataset:"se-7",date:(new Date).toISOString(),callback:null},log:function(a){this.debug&&console.log(a+"\nIn function:"+arguments.callee.caller.name)},init:function(a,b){var c=this;c.ColorLayer.thenmap=c,c.settings=c.utils.extend(c.settings,b),"string"==typeof a?(a=a.replace(/^#/,""),c.el=document.getElementById(a)):a.nodeType&&(c.el=a);var d=document.createElement("style");document.getElementsByTagName("head")[0].appendChild(d),this.css=d;var e=e||{};e["src/styles.css"]="svg.thenmap {\n stroke: white;\n stroke-width: .25px\n}\n@keyframes loading_data {\n 0% {fill-opacity: .25}\n 100% {fill-opacity: .75}\n}\n.loading_data path {\n animation: loading_data 1s linear infinite alternate;\n}",c.extendCss(e["src/styles.css"]);var f=c.HttpClient;f.get(c.createApiUrl(),function(a){var b=JSON.parse(a),d=b.svg,e=b.data,f=document.createElement("div");f.innerHTML=d,c.svg=f.getElementsByTagName("svg")[0],c.el.appendChild(c.svg);for(var g=c.el.getElementsByTagName("path"),h=g.length;--h;){var i=g[h].getAttribute("data-id");if(i in e){var j=document.createElementNS("http://www.w3.org/2000/svg","title");j.textContent=e[i].name,g[h].appendChild(j),g[h].setAttribute("class",e[i]["class"])}else c.log("no data for shape id"+i)}c.settings.dataKey&&c.ColorLayer.init(c.settings.dataKey),"function"==typeof c.settings.callback&&c.settings.callback(null,this)})},createApiUrl:function(){var a=this,b=this.debug?this.localApiUrl:this.apiUrl;b+=[this.settings.dataset,"svg|data",this.settings.date].join("/");var c=["data_props=name|class"];return["width","height","projection","language"].forEach(function(b){null!==a.settings[b]&&c.push(b+"="+a.settings[b])}),b+="?"+c.join("&")},extendCss:function(a){this.css.styleSheet?this.css.styleSheet.cssText+=a:this.css.innerHTML+=a},HttpClient:{get:function(a,b){var c=new XMLHttpRequest;c.onreadystatechange=function(){4==c.readyState&&200==c.status&&b(c.responseText)},c.open("GET",a,!0),c.send(null)}},ColorLayer:{getSpreadsheetData:function(a,b){Tabletop.init({key:a,callback:function(a,c){b(a)},simpleSheet:!0})},getColorCode:function(a){var a=a.trim(),b=["aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","indianred","indigo","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey"," ","","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","red","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","springgreen","steelblue","tan","teal","thistle","tomato","turquoise","violet","wheat","white","whitesmoke","yellow","yellowgreen"];return/(^#[0-9A-F]{6}$){1,2}/i.test(a)?a:/(^[0-9A-F]{6}$){1,2}/i.test(a)?"#"+a:b.indexOf(a.toLowerCase())>-1?a.toLowerCase():/rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/i.test(a)?a.toLowerCase():this.thenmap.defaultColor},render:function(a){for(var b=this,c={},d=a.length;d--;){var e=a[d];if(e.color){var f=b.getColorCode(e.color),g="path."+e.id;f in c?c[f].push(g):c[f]=[g]}}var h="";for(var i in c)h+=c[i].join(", ")+"{fill:"+i+"}\n";b.thenmap.extendCss(h)},init:function(a){var b=this,c=b.thenmap.el.className||"";b.thenmap.el.className=[c,"loading_data"].join(" "),b.getSpreadsheetData(a,function(a){b.thenmap.el.className=c,b.render(a)})}},utils:{extend:function(a,b){var c,d={};for(c in a)Object.prototype.hasOwnProperty.call(a,c)&&(d[c]=a[c]);for(c in b)Object.prototype.hasOwnProperty.call(b,c)&&(d[c]=b[c]);return d}}};!function(a){"use strict";var b=!1;if("undefined"!=typeof module&&module.exports){b=!0;var c=require("request")}var d=!1,e=!1;try{var f=new XMLHttpRequest;"undefined"!=typeof f.withCredentials?d=!0:"XDomainRequest"in window&&(d=!0,e=!0)}catch(g){}var h=Array.prototype.indexOf,i=function(a,b){var c=0,d=a.length;if(h&&a.indexOf===h)return a.indexOf(b);for(;d>c;c++)if(a[c]===b)return c;return-1},j=function(a){return this&&this instanceof j?("string"==typeof a&&(a={key:a}),this.callback=a.callback,this.wanted=a.wanted||[],this.key=a.key,this.simpleSheet=!!a.simpleSheet,this.parseNumbers=!!a.parseNumbers,this.wait=!!a.wait,this.reverse=!!a.reverse,this.postProcess=a.postProcess,this.debug=!!a.debug,this.query=a.query||"",this.orderby=a.orderby,this.endpoint=a.endpoint||"https://spreadsheets.google.com",this.singleton=!!a.singleton,this.simple_url=!!a.simple_url,this.callbackContext=a.callbackContext,this.prettyColumnNames="undefined"==typeof a.prettyColumnNames?!a.proxy:a.prettyColumnNames,"undefined"!=typeof a.proxy&&(this.endpoint=a.proxy.replace(/\/$/,""),this.simple_url=!0,this.singleton=!0,d=!1),this.parameterize=a.parameterize||!1,this.singleton&&("undefined"!=typeof j.singleton&&this.log("WARNING! Tabletop singleton already defined"),j.singleton=this),/key=/.test(this.key)&&(this.log("You passed an old Google Docs url as the key! Attempting to parse."),this.key=this.key.match("key=(.*?)(&|#|$)")[1]),/pubhtml/.test(this.key)&&(this.log("You passed a new Google Spreadsheets url as the key! Attempting to parse."),this.key=this.key.match("d\\/(.*?)\\/pubhtml")[1]),this.key?(this.log("Initializing with key "+this.key),this.models={},this.model_names=[],this.base_json_path="/feeds/worksheets/"+this.key+"/public/basic?alt=",b||d?this.base_json_path+="json":this.base_json_path+="json-in-script",void(this.wait||this.fetch())):void this.log("You need to pass Tabletop a key!")):new j(a)};j.callbacks={},j.init=function(a){return new j(a)},j.sheets=function(){this.log("Times have changed! You'll want to use var tabletop = Tabletop.init(...); tabletop.sheets(...); instead of Tabletop.sheets(...)")},j.prototype={fetch:function(a){"undefined"!=typeof a&&(this.callback=a),this.requestData(this.base_json_path,this.loadSheets)},requestData:function(a,c){if(b)this.serverSideFetch(a,c);else{var f=this.endpoint.split("//").shift()||"http";!d||e&&f!==location.protocol?this.injectScript(a,c):this.xhrFetch(a,c)}},xhrFetch:function(a,b){var c=e?new XDomainRequest:new XMLHttpRequest;c.open("GET",this.endpoint+a);var d=this;c.onload=function(){try{var a=JSON.parse(c.responseText)}catch(e){console.error(e)}b.call(d,a)},c.send()},injectScript:function(a,b){var c,d=document.createElement("script");if(this.singleton)b===this.loadSheets?c="Tabletop.singleton.loadSheets":b===this.loadSheet&&(c="Tabletop.singleton.loadSheet");else{var e=this;c="tt"+ +new Date+Math.floor(1e5*Math.random()),j.callbacks[c]=function(){var a=Array.prototype.slice.call(arguments,0);b.apply(e,a),d.parentNode.removeChild(d),delete j.callbacks[c]},c="Tabletop.callbacks."+c}var f=a+"&callback="+c;this.simple_url?-1!==a.indexOf("/list/")?d.src=this.endpoint+"/"+this.key+"-"+a.split("/")[4]:d.src=this.endpoint+"/"+this.key:d.src=this.endpoint+f,this.parameterize&&(d.src=this.parameterize+encodeURIComponent(d.src)),document.getElementsByTagName("script")[0].parentNode.appendChild(d)},serverSideFetch:function(a,b){var d=this;c({url:this.endpoint+a,json:!0},function(a,c,e){return a?console.error(a):void b.call(d,e)})},isWanted:function(a){return 0===this.wanted.length?!0:-1!==i(this.wanted,a)},data:function(){return 0===this.model_names.length?void 0:this.simpleSheet?(this.model_names.length>1&&this.debug&&this.log("WARNING You have more than one sheet but are using simple sheet mode! Don't blame me when something goes wrong."),this.models[this.model_names[0]].all()):this.models},addWanted:function(a){-1===i(this.wanted,a)&&this.wanted.push(a)},loadSheets:function(a){var c,e,f=[];for(this.foundSheetNames=[],c=0,e=a.feed.entry.length;e>c;c++)if(this.foundSheetNames.push(a.feed.entry[c].title.$t),this.isWanted(a.feed.entry[c].content.$t)){var g=a.feed.entry[c].link.length-1,h=a.feed.entry[c].link[g].href.split("/").pop(),i="/feeds/list/"+this.key+"/"+h+"/public/values?alt=";i+=b||d?"json":"json-in-script",this.query&&(i+="&sq="+this.query),this.orderby&&(i+="&orderby=column:"+this.orderby.toLowerCase()),this.reverse&&(i+="&reverse=true"),f.push(i)}for(this.sheetsToLoad=f.length,c=0,e=f.length;e>c;c++)this.requestData(f[c],this.loadSheet)},sheets:function(a){return"undefined"==typeof a?this.models:"undefined"==typeof this.models[a]?void 0:this.models[a]},sheetReady:function(a){this.models[a.name]=a,-1===i(this.model_names,a.name)&&this.model_names.push(a.name),this.sheetsToLoad--,0===this.sheetsToLoad&&this.doCallback()},loadSheet:function(a){var b=this;new j.Model({data:a,parseNumbers:this.parseNumbers,postProcess:this.postProcess,tabletop:this,prettyColumnNames:this.prettyColumnNames,onReady:function(){b.sheetReady(this)}})},doCallback:function(){0===this.sheetsToLoad&&this.callback.apply(this.callbackContext||this,[this.data(),this])},log:function(a){this.debug&&"undefined"!=typeof console&&"undefined"!=typeof console.log&&Function.prototype.apply.apply(console.log,[console,arguments])}},j.Model=function(a){var b,c,d,e;if(this.column_names=[],this.name=a.data.feed.title.$t,this.tabletop=a.tabletop,this.elements=[],this.onReady=a.onReady,this.raw=a.data,"undefined"==typeof a.data.feed.entry)return a.tabletop.log("Missing data for "+this.name+", make sure you didn't forget column headers"),this.original_columns=[],this.elements=[],void this.onReady.call(this);for(var f in a.data.feed.entry[0])/^gsx/.test(f)&&this.column_names.push(f.replace("gsx$",""));for(this.original_columns=this.column_names,b=0,d=a.data.feed.entry.length;d>b;b++){for(var g=a.data.feed.entry[b],h={},c=0,e=this.column_names.length;e>c;c++){var i=g["gsx$"+this.column_names[c]];"undefined"!=typeof i?a.parseNumbers&&""!==i.$t&&!isNaN(i.$t)?h[this.column_names[c]]=+i.$t:h[this.column_names[c]]=i.$t:h[this.column_names[c]]=""}void 0===h.rowNumber&&(h.rowNumber=b+1),a.postProcess&&a.postProcess(h),this.elements.push(h)}a.prettyColumnNames?this.fetchPrettyColumns():this.onReady.call(this)},j.Model.prototype={all:function(){return this.elements},fetchPrettyColumns:function(){if(!this.raw.feed.link[3])return this.ready();var a=this.raw.feed.link[3].href.replace("/feeds/list/","/feeds/cells/").replace("https://spreadsheets.google.com",""),b=this;this.tabletop.requestData(a,function(a){b.loadPrettyColumns(a)})},ready:function(){this.onReady.call(this)},loadPrettyColumns:function(a){for(var b={},c=this.column_names,d=0,e=c.length;e>d;d++)"undefined"!=typeof a.feed.entry[d].content.$t?b[c[d]]=a.feed.entry[d].content.$t:b[c[d]]=c[d];this.pretty_columns=b,this.prettifyElements(),this.ready()},prettifyElements:function(){var a,b,c,d,e,f=[],e=[];for(b=0,d=this.column_names.length;d>b;b++)e.push(this.pretty_columns[this.column_names[b]]);for(a=0,c=this.elements.length;c>a;a++){var g={};for(b=0,d=this.column_names.length;d>b;b++){var h=this.pretty_columns[this.column_names[b]];g[h]=this.elements[a][this.column_names[b]]}f.push(g)}this.elements=f,this.column_names=e},toArray:function(){var a,b,c,d,e=[];for(a=0,c=this.elements.length;c>a;a++){var f=[];for(b=0,d=this.column_names.length;d>b;b++)f.push(this.elements[a][this.column_names[b]]);e.push(f)}return e}},b?module.exports=j:"function"==typeof define&&define.amd?define(function(){return j}):a.Tabletop=j}(this); -------------------------------------------------------------------------------- /build/thenmap-1.0.1.js: -------------------------------------------------------------------------------- 1 | var Thenmap = { 2 | 3 | debug: false, 4 | apiUrl: "//thenmap-api.herokuapp.com/v1/", 5 | localApiUrl: "http://localhost:3000/v1/", //for debugging 6 | el: null, //container element 7 | svg: null, //svg element 8 | css: null, //css element for dynamically adding styles 9 | defaultColor: "gainsboro", 10 | 11 | // Default settings that can be overridden by passing arguments to Thenmap 12 | settings: { 13 | width: 800, 14 | height: null, 15 | language: null, 16 | projection: null, 17 | dataKey: null, 18 | dataset: "se-7", 19 | date: new Date().toISOString(), //current date, works in any browser that can display SVG 20 | callback: null 21 | }, 22 | 23 | /* Print debug message to the console 24 | */ 25 | log: function(string) { 26 | if (this.debug) { 27 | console.log(string + "\nIn function:"+arguments.callee.caller.name); 28 | } 29 | }, 30 | /* Entry point 31 | */ 32 | init: function(elIdentifier, options) { 33 | var self = this; 34 | self.ColorLayer.thenmap = self; 35 | 36 | // Apply settings 37 | self.settings = self.utils.extend(self.settings, options); 38 | 39 | if (typeof elIdentifier === "string") { 40 | // If first character is #, remove. While technically a valid 41 | // character in an HTML5 id, it's likely meant as id selector 42 | elIdentifier = elIdentifier.replace(/^#/, ''); 43 | self.el = document.getElementById(elIdentifier); 44 | } else if (elIdentifier.nodeType) { 45 | // User gave us a valid reference to an element 46 | self.el = elIdentifier; 47 | } else { 48 | // not a valid identifier 49 | } 50 | 51 | // create CSS element for dynamic styling 52 | var css = document.createElement("style"); 53 | document.getElementsByTagName("head")[0].appendChild(css); 54 | this.css = css; 55 | 56 | // set global styles 57 | var CSS = CSS || {}; 58 | CSS["src/styles.css"] = 'svg.thenmap {\n stroke: white;\n stroke-width: .25px\n}\n@keyframes loading_data {\n 0% {fill-opacity: .25}\n 100% {fill-opacity: .75}\n}\n.loading_data path {\n animation: loading_data 1s linear infinite alternate;\n}'; 59 | ; 60 | self.extendCss(CSS["src/styles.css"]); 61 | 62 | var httpClient = self.HttpClient; 63 | httpClient.get(self.createApiUrl(), function(response) { 64 | var response_json = JSON.parse(response); 65 | var svgString = response_json.svg; 66 | var data = response_json.data; 67 | 68 | // Something of an hack, to make sure SVG is rendered 69 | // Creating a SVG element will not make the SVG render 70 | // in all browsers. innerHTML will. 71 | var tmp = document.createElement("div"); 72 | tmp.innerHTML = svgString; 73 | self.svg = tmp.getElementsByTagName('svg')[0]; 74 | //append SVG before setting viewBox, to get size 75 | self.el.appendChild(self.svg); 76 | // Do we need to explicitly set viewBox? This must be tested, not least on IE 77 | // var bbox = self.svg.getBBox(); 78 | // self.svg.setAttribute("viewBox", [bbox.x, bbox.y, bbox.width, bbox.height].join(" ")); 79 | 80 | //Apply classes, add titles 81 | var paths=self.el.getElementsByTagName('path'); 82 | var i = paths.length; 83 | while(i--) { 84 | //We must support IE10, so can not use dataset 85 | var data_id = paths[i].getAttribute("data-id"); 86 | if (data_id in data){ 87 | 88 | var title = document.createElementNS("http://www.w3.org/2000/svg","title") 89 | title.textContent = data[data_id].name; 90 | paths[i].appendChild(title); 91 | 92 | //element.className is not available for SVG elements 93 | paths[i].setAttribute("class", data[data_id].class); 94 | 95 | } else { 96 | self.log("no data for shape id" + data_id); 97 | } 98 | 99 | } 100 | 101 | // Color the map if a spreadsheet key is given 102 | if (self.settings.dataKey) { 103 | self.ColorLayer.init(self.settings.dataKey); 104 | } 105 | 106 | if (typeof self.settings.callback === "function"){ 107 | self.settings.callback(null, this); 108 | } 109 | 110 | }); 111 | 112 | }, // function init 113 | 114 | createApiUrl: function() { 115 | var self = this; 116 | var apiUrl = this.debug ? this.localApiUrl : this.apiUrl; 117 | apiUrl += [this.settings.dataset, "svg|data", this.settings.date].join("/"); 118 | // Add url parameters 119 | var options = ["data_props=name|class"]; 120 | ["width", "height", "projection", "language"].forEach(function(key){ 121 | if (self.settings[key] !== null){ 122 | options.push(key + "=" + self.settings[key]); 123 | } 124 | }); 125 | apiUrl += "?" + options.join("&"); 126 | return apiUrl; 127 | }, // function createApiUrl 128 | 129 | /* Add code to the global stylesheet 130 | */ 131 | extendCss: function(code) { 132 | 133 | if (this.css.styleSheet) { 134 | // IE 135 | this.css.styleSheet.cssText += code; 136 | } else { 137 | // Other browsers 138 | this.css.innerHTML += code; 139 | } 140 | 141 | }, 142 | 143 | HttpClient: { 144 | get: function(url, callback) { 145 | var httpRequest = new XMLHttpRequest(); 146 | httpRequest.onreadystatechange = function() { 147 | if (httpRequest.readyState == 4 && httpRequest.status == 200) { 148 | callback(httpRequest.responseText); 149 | } 150 | } 151 | 152 | httpRequest.open( "GET", url, true ); 153 | httpRequest.send( null ); 154 | } 155 | }, // HttpClient 156 | 157 | ColorLayer: { 158 | 159 | /* Fetches data from a Google Spreadsheet using Tabletop 160 | */ 161 | getSpreadsheetData: function(spreadsheetKey, callback) { 162 | Tabletop.init({ 163 | key: spreadsheetKey, 164 | callback: function(data, tabletop) { 165 | callback(data); 166 | }, 167 | simpleSheet: true 168 | }) 169 | }, // getSpreadsheetData 170 | 171 | /* Sanitize and validate a SVG color code 172 | Accepts "#99cccc", "9cc", "green", and "rgb(1,32,42)" 173 | */ 174 | getColorCode: function(string){ 175 | 176 | var string = string.trim(); 177 | var allowedColorNames = ["aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","indianred","indigo","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey"," ","","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","red","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","springgreen","steelblue","tan","teal","thistle","tomato","turquoise","violet","wheat","white","whitesmoke","yellow","yellowgreen"]; 178 | if (/(^#[0-9A-F]{6}$){1,2}/i.test(string)) { 179 | // #00cccc 180 | return string; 181 | } else if (/(^[0-9A-F]{6}$){1,2}/i.test(string)) { 182 | // 00cccc 183 | return "#" + string; 184 | } else if (allowedColorNames.indexOf(string.toLowerCase()) > -1) { // will work for all SVG capable browsers 185 | // green 186 | return string.toLowerCase(); 187 | } else if (/rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/i.test(string)){ 188 | // rgb(123,231,432) 189 | return string.toLowerCase(); 190 | } else { 191 | // *invalid 192 | return this.thenmap.defaultColor; 193 | } 194 | 195 | }, 196 | 197 | /* Colorize map 198 | */ 199 | render: function(data) { 200 | var self = this; 201 | var colors = {} 202 | 203 | /* Create a colors object like this: 204 | { green: [class1, class2], ... } 205 | */ 206 | var i = data.length; 207 | while(i--) { 208 | var d = data[i]; 209 | if (d.color) { 210 | var colorCode = self.getColorCode(d.color); 211 | var selector = "path." + d.id; 212 | if (colorCode in colors){ 213 | colors[colorCode].push(selector); 214 | } else { 215 | colors[colorCode] = [selector]; 216 | } 217 | } 218 | } 219 | 220 | /* build and apply CSS */ 221 | var cssCode = ""; 222 | for (var color in colors){ 223 | cssCode += colors[color].join(", ") + "{fill:" + color + "}\n"; 224 | } 225 | self.thenmap.extendCss(cssCode); 226 | }, // ColorLayer.render 227 | 228 | /* Constructor for thenmap.ColorLayer 229 | */ 230 | init: function(spreadsheetKey) { 231 | var self = this; 232 | 233 | // Add loader class while loading 234 | var oldClassName = self.thenmap.el.className || ""; 235 | self.thenmap.el.className = [oldClassName, "loading_data"].join(" "); 236 | self.getSpreadsheetData(spreadsheetKey, function(data) { 237 | // Remove loader class 238 | self.thenmap.el.className = oldClassName; 239 | //Use data 240 | self.render(data); 241 | }); 242 | } // ColorLayer.init 243 | 244 | }, // ColorLayer 245 | 246 | utils: { 247 | extend: function ( defaults, options ) { 248 | var extended = {}; 249 | var prop; 250 | for (prop in defaults) { 251 | if (Object.prototype.hasOwnProperty.call(defaults, prop)) { 252 | extended[prop] = defaults[prop]; 253 | } 254 | } 255 | for (prop in options) { 256 | if (Object.prototype.hasOwnProperty.call(options, prop)) { 257 | extended[prop] = options[prop]; 258 | } 259 | } 260 | return extended; 261 | } // Extend js object 262 | }// Utils 263 | 264 | }; 265 | (function(global) { 266 | "use strict"; 267 | 268 | var inNodeJS = false; 269 | if (typeof module !== 'undefined' && module.exports) { 270 | inNodeJS = true; 271 | var request = require('request'); 272 | } 273 | 274 | var supportsCORS = false; 275 | var inLegacyIE = false; 276 | try { 277 | var testXHR = new XMLHttpRequest(); 278 | if (typeof testXHR.withCredentials !== 'undefined') { 279 | supportsCORS = true; 280 | } else { 281 | if ("XDomainRequest" in window) { 282 | supportsCORS = true; 283 | inLegacyIE = true; 284 | } 285 | } 286 | } catch (e) { } 287 | 288 | // Create a simple indexOf function for support 289 | // of older browsers. Uses native indexOf if 290 | // available. Code similar to underscores. 291 | // By making a separate function, instead of adding 292 | // to the prototype, we will not break bad for loops 293 | // in older browsers 294 | var indexOfProto = Array.prototype.indexOf; 295 | var ttIndexOf = function(array, item) { 296 | var i = 0, l = array.length; 297 | 298 | if (indexOfProto && array.indexOf === indexOfProto) return array.indexOf(item); 299 | for (; i < l; i++) if (array[i] === item) return i; 300 | return -1; 301 | }; 302 | 303 | /* 304 | Initialize with Tabletop.init( { key: '0AjAPaAU9MeLFdHUxTlJiVVRYNGRJQnRmSnQwTlpoUXc' } ) 305 | OR! 306 | Initialize with Tabletop.init( { key: 'https://docs.google.com/spreadsheet/pub?hl=en_US&hl=en_US&key=0AjAPaAU9MeLFdHUxTlJiVVRYNGRJQnRmSnQwTlpoUXc&output=html&widget=true' } ) 307 | OR! 308 | Initialize with Tabletop.init('0AjAPaAU9MeLFdHUxTlJiVVRYNGRJQnRmSnQwTlpoUXc') 309 | */ 310 | 311 | var Tabletop = function(options) { 312 | // Make sure Tabletop is being used as a constructor no matter what. 313 | if(!this || !(this instanceof Tabletop)) { 314 | return new Tabletop(options); 315 | } 316 | 317 | if(typeof(options) === 'string') { 318 | options = { key : options }; 319 | } 320 | 321 | this.callback = options.callback; 322 | this.wanted = options.wanted || []; 323 | this.key = options.key; 324 | this.simpleSheet = !!options.simpleSheet; 325 | this.parseNumbers = !!options.parseNumbers; 326 | this.wait = !!options.wait; 327 | this.reverse = !!options.reverse; 328 | this.postProcess = options.postProcess; 329 | this.debug = !!options.debug; 330 | this.query = options.query || ''; 331 | this.orderby = options.orderby; 332 | this.endpoint = options.endpoint || "https://spreadsheets.google.com"; 333 | this.singleton = !!options.singleton; 334 | this.simple_url = !!options.simple_url; 335 | this.callbackContext = options.callbackContext; 336 | // Default to on, unless there's a proxy, in which case it's default off 337 | this.prettyColumnNames = typeof(options.prettyColumnNames) == 'undefined' ? !options.proxy : options.prettyColumnNames 338 | 339 | if(typeof(options.proxy) !== 'undefined') { 340 | // Remove trailing slash, it will break the app 341 | this.endpoint = options.proxy.replace(/\/$/,''); 342 | this.simple_url = true; 343 | this.singleton = true; 344 | // Let's only use CORS (straight JSON request) when 345 | // fetching straight from Google 346 | supportsCORS = false; 347 | } 348 | 349 | this.parameterize = options.parameterize || false; 350 | 351 | if(this.singleton) { 352 | if(typeof(Tabletop.singleton) !== 'undefined') { 353 | this.log("WARNING! Tabletop singleton already defined"); 354 | } 355 | Tabletop.singleton = this; 356 | } 357 | 358 | /* Be friendly about what you accept */ 359 | if(/key=/.test(this.key)) { 360 | this.log("You passed an old Google Docs url as the key! Attempting to parse."); 361 | this.key = this.key.match("key=(.*?)(&|#|$)")[1]; 362 | } 363 | 364 | if(/pubhtml/.test(this.key)) { 365 | this.log("You passed a new Google Spreadsheets url as the key! Attempting to parse."); 366 | this.key = this.key.match("d\\/(.*?)\\/pubhtml")[1]; 367 | } 368 | 369 | if(!this.key) { 370 | this.log("You need to pass Tabletop a key!"); 371 | return; 372 | } 373 | 374 | this.log("Initializing with key " + this.key); 375 | 376 | this.models = {}; 377 | this.model_names = []; 378 | 379 | this.base_json_path = "/feeds/worksheets/" + this.key + "/public/basic?alt="; 380 | 381 | if (inNodeJS || supportsCORS) { 382 | this.base_json_path += 'json'; 383 | } else { 384 | this.base_json_path += 'json-in-script'; 385 | } 386 | 387 | if(!this.wait) { 388 | this.fetch(); 389 | } 390 | }; 391 | 392 | // A global storage for callbacks. 393 | Tabletop.callbacks = {}; 394 | 395 | // Backwards compatibility. 396 | Tabletop.init = function(options) { 397 | return new Tabletop(options); 398 | }; 399 | 400 | Tabletop.sheets = function() { 401 | this.log("Times have changed! You'll want to use var tabletop = Tabletop.init(...); tabletop.sheets(...); instead of Tabletop.sheets(...)"); 402 | }; 403 | 404 | Tabletop.prototype = { 405 | 406 | fetch: function(callback) { 407 | if(typeof(callback) !== "undefined") { 408 | this.callback = callback; 409 | } 410 | this.requestData(this.base_json_path, this.loadSheets); 411 | }, 412 | 413 | /* 414 | This will call the environment appropriate request method. 415 | 416 | In browser it will use JSON-P, in node it will use request() 417 | */ 418 | requestData: function(path, callback) { 419 | if (inNodeJS) { 420 | this.serverSideFetch(path, callback); 421 | } else { 422 | //CORS only works in IE8/9 across the same protocol 423 | //You must have your server on HTTPS to talk to Google, or it'll fall back on injection 424 | var protocol = this.endpoint.split("//").shift() || "http"; 425 | if (supportsCORS && (!inLegacyIE || protocol === location.protocol)) { 426 | this.xhrFetch(path, callback); 427 | } else { 428 | this.injectScript(path, callback); 429 | } 430 | } 431 | }, 432 | 433 | /* 434 | Use Cross-Origin XMLHttpRequest to get the data in browsers that support it. 435 | */ 436 | xhrFetch: function(path, callback) { 437 | //support IE8's separate cross-domain object 438 | var xhr = inLegacyIE ? new XDomainRequest() : new XMLHttpRequest(); 439 | xhr.open("GET", this.endpoint + path); 440 | var self = this; 441 | xhr.onload = function() { 442 | try { 443 | var json = JSON.parse(xhr.responseText); 444 | } catch (e) { 445 | console.error(e); 446 | } 447 | callback.call(self, json); 448 | }; 449 | xhr.send(); 450 | }, 451 | 452 | /* 453 | Insert the URL into the page as a script tag. Once it's loaded the spreadsheet data 454 | it triggers the callback. This helps you avoid cross-domain errors 455 | http://code.google.com/apis/gdata/samples/spreadsheet_sample.html 456 | 457 | Let's be plain-Jane and not use jQuery or anything. 458 | */ 459 | injectScript: function(path, callback) { 460 | var script = document.createElement('script'); 461 | var callbackName; 462 | 463 | if(this.singleton) { 464 | if(callback === this.loadSheets) { 465 | callbackName = 'Tabletop.singleton.loadSheets'; 466 | } else if (callback === this.loadSheet) { 467 | callbackName = 'Tabletop.singleton.loadSheet'; 468 | } 469 | } else { 470 | var self = this; 471 | callbackName = 'tt' + (+new Date()) + (Math.floor(Math.random()*100000)); 472 | // Create a temp callback which will get removed once it has executed, 473 | // this allows multiple instances of Tabletop to coexist. 474 | Tabletop.callbacks[ callbackName ] = function () { 475 | var args = Array.prototype.slice.call( arguments, 0 ); 476 | callback.apply(self, args); 477 | script.parentNode.removeChild(script); 478 | delete Tabletop.callbacks[callbackName]; 479 | }; 480 | callbackName = 'Tabletop.callbacks.' + callbackName; 481 | } 482 | 483 | var url = path + "&callback=" + callbackName; 484 | 485 | if(this.simple_url) { 486 | // We've gone down a rabbit hole of passing injectScript the path, so let's 487 | // just pull the sheet_id out of the path like the least efficient worker bees 488 | if(path.indexOf("/list/") !== -1) { 489 | script.src = this.endpoint + "/" + this.key + "-" + path.split("/")[4]; 490 | } else { 491 | script.src = this.endpoint + "/" + this.key; 492 | } 493 | } else { 494 | script.src = this.endpoint + url; 495 | } 496 | 497 | if (this.parameterize) { 498 | script.src = this.parameterize + encodeURIComponent(script.src); 499 | } 500 | 501 | document.getElementsByTagName('script')[0].parentNode.appendChild(script); 502 | }, 503 | 504 | /* 505 | This will only run if tabletop is being run in node.js 506 | */ 507 | serverSideFetch: function(path, callback) { 508 | var self = this 509 | request({url: this.endpoint + path, json: true}, function(err, resp, body) { 510 | if (err) { 511 | return console.error(err); 512 | } 513 | callback.call(self, body); 514 | }); 515 | }, 516 | 517 | /* 518 | Is this a sheet you want to pull? 519 | If { wanted: ["Sheet1"] } has been specified, only Sheet1 is imported 520 | Pulls all sheets if none are specified 521 | */ 522 | isWanted: function(sheetName) { 523 | if(this.wanted.length === 0) { 524 | return true; 525 | } else { 526 | return (ttIndexOf(this.wanted, sheetName) !== -1); 527 | } 528 | }, 529 | 530 | /* 531 | What gets send to the callback 532 | if simpleSheet === true, then don't return an array of Tabletop.this.models, 533 | only return the first one's elements 534 | */ 535 | data: function() { 536 | // If the instance is being queried before the data's been fetched 537 | // then return undefined. 538 | if(this.model_names.length === 0) { 539 | return undefined; 540 | } 541 | if(this.simpleSheet) { 542 | if(this.model_names.length > 1 && this.debug) { 543 | this.log("WARNING You have more than one sheet but are using simple sheet mode! Don't blame me when something goes wrong."); 544 | } 545 | return this.models[ this.model_names[0] ].all(); 546 | } else { 547 | return this.models; 548 | } 549 | }, 550 | 551 | /* 552 | Add another sheet to the wanted list 553 | */ 554 | addWanted: function(sheet) { 555 | if(ttIndexOf(this.wanted, sheet) === -1) { 556 | this.wanted.push(sheet); 557 | } 558 | }, 559 | 560 | /* 561 | Load all worksheets of the spreadsheet, turning each into a Tabletop Model. 562 | Need to use injectScript because the worksheet view that you're working from 563 | doesn't actually include the data. The list-based feed (/feeds/list/key..) does, though. 564 | Calls back to loadSheet in order to get the real work done. 565 | 566 | Used as a callback for the worksheet-based JSON 567 | */ 568 | loadSheets: function(data) { 569 | var i, ilen; 570 | var toLoad = []; 571 | this.foundSheetNames = []; 572 | 573 | for(i = 0, ilen = data.feed.entry.length; i < ilen ; i++) { 574 | this.foundSheetNames.push(data.feed.entry[i].title.$t); 575 | // Only pull in desired sheets to reduce loading 576 | if( this.isWanted(data.feed.entry[i].content.$t) ) { 577 | var linkIdx = data.feed.entry[i].link.length-1; 578 | var sheet_id = data.feed.entry[i].link[linkIdx].href.split('/').pop(); 579 | var json_path = "/feeds/list/" + this.key + "/" + sheet_id + "/public/values?alt=" 580 | if (inNodeJS || supportsCORS) { 581 | json_path += 'json'; 582 | } else { 583 | json_path += 'json-in-script'; 584 | } 585 | if(this.query) { 586 | json_path += "&sq=" + this.query; 587 | } 588 | if(this.orderby) { 589 | json_path += "&orderby=column:" + this.orderby.toLowerCase(); 590 | } 591 | if(this.reverse) { 592 | json_path += "&reverse=true"; 593 | } 594 | toLoad.push(json_path); 595 | } 596 | } 597 | 598 | this.sheetsToLoad = toLoad.length; 599 | for(i = 0, ilen = toLoad.length; i < ilen; i++) { 600 | this.requestData(toLoad[i], this.loadSheet); 601 | } 602 | }, 603 | 604 | /* 605 | Access layer for the this.models 606 | .sheets() gets you all of the sheets 607 | .sheets('Sheet1') gets you the sheet named Sheet1 608 | */ 609 | sheets: function(sheetName) { 610 | if(typeof sheetName === "undefined") { 611 | return this.models; 612 | } else { 613 | if(typeof(this.models[ sheetName ]) === "undefined") { 614 | // alert( "Can't find " + sheetName ); 615 | return; 616 | } else { 617 | return this.models[ sheetName ]; 618 | } 619 | } 620 | }, 621 | 622 | sheetReady: function(model) { 623 | this.models[ model.name ] = model; 624 | if(ttIndexOf(this.model_names, model.name) === -1) { 625 | this.model_names.push(model.name); 626 | } 627 | 628 | this.sheetsToLoad--; 629 | if(this.sheetsToLoad === 0) 630 | this.doCallback(); 631 | }, 632 | 633 | /* 634 | Parse a single list-based worksheet, turning it into a Tabletop Model 635 | 636 | Used as a callback for the list-based JSON 637 | */ 638 | loadSheet: function(data) { 639 | var that = this; 640 | var model = new Tabletop.Model( { data: data, 641 | parseNumbers: this.parseNumbers, 642 | postProcess: this.postProcess, 643 | tabletop: this, 644 | prettyColumnNames: this.prettyColumnNames, 645 | onReady: function() { 646 | that.sheetReady(this); 647 | } } ); 648 | }, 649 | 650 | /* 651 | Execute the callback upon loading! Rely on this.data() because you might 652 | only request certain pieces of data (i.e. simpleSheet mode) 653 | Tests this.sheetsToLoad just in case a race condition happens to show up 654 | */ 655 | doCallback: function() { 656 | if(this.sheetsToLoad === 0) { 657 | this.callback.apply(this.callbackContext || this, [this.data(), this]); 658 | } 659 | }, 660 | 661 | log: function(msg) { 662 | if(this.debug) { 663 | if(typeof console !== "undefined" && typeof console.log !== "undefined") { 664 | Function.prototype.apply.apply(console.log, [console, arguments]); 665 | } 666 | } 667 | } 668 | 669 | }; 670 | 671 | /* 672 | Tabletop.Model stores the attribute names and parses the worksheet data 673 | to turn it into something worthwhile 674 | 675 | Options should be in the format { data: XXX }, with XXX being the list-based worksheet 676 | */ 677 | Tabletop.Model = function(options) { 678 | var i, j, ilen, jlen; 679 | this.column_names = []; 680 | this.name = options.data.feed.title.$t; 681 | this.tabletop = options.tabletop; 682 | this.elements = []; 683 | this.onReady = options.onReady; 684 | this.raw = options.data; // A copy of the sheet's raw data, for accessing minutiae 685 | 686 | if(typeof(options.data.feed.entry) === 'undefined') { 687 | options.tabletop.log("Missing data for " + this.name + ", make sure you didn't forget column headers"); 688 | this.original_columns = []; 689 | this.elements = []; 690 | this.onReady.call(this); 691 | return; 692 | } 693 | 694 | for(var key in options.data.feed.entry[0]){ 695 | if(/^gsx/.test(key)) 696 | this.column_names.push( key.replace("gsx$","") ); 697 | } 698 | 699 | this.original_columns = this.column_names; 700 | 701 | for(i = 0, ilen = options.data.feed.entry.length ; i < ilen; i++) { 702 | var source = options.data.feed.entry[i]; 703 | var element = {}; 704 | for(var j = 0, jlen = this.column_names.length; j < jlen ; j++) { 705 | var cell = source[ "gsx$" + this.column_names[j] ]; 706 | if (typeof(cell) !== 'undefined') { 707 | if(options.parseNumbers && cell.$t !== '' && !isNaN(cell.$t)) 708 | element[ this.column_names[j] ] = +cell.$t; 709 | else 710 | element[ this.column_names[j] ] = cell.$t; 711 | } else { 712 | element[ this.column_names[j] ] = ''; 713 | } 714 | } 715 | if(element.rowNumber === undefined) 716 | element.rowNumber = i + 1; 717 | if( options.postProcess ) 718 | options.postProcess(element); 719 | this.elements.push(element); 720 | } 721 | 722 | if(options.prettyColumnNames) 723 | this.fetchPrettyColumns(); 724 | else 725 | this.onReady.call(this); 726 | }; 727 | 728 | Tabletop.Model.prototype = { 729 | /* 730 | Returns all of the elements (rows) of the worksheet as objects 731 | */ 732 | all: function() { 733 | return this.elements; 734 | }, 735 | 736 | fetchPrettyColumns: function() { 737 | if(!this.raw.feed.link[3]) 738 | return this.ready(); 739 | var cellurl = this.raw.feed.link[3].href.replace('/feeds/list/', '/feeds/cells/').replace('https://spreadsheets.google.com', ''); 740 | var that = this; 741 | this.tabletop.requestData(cellurl, function(data) { 742 | that.loadPrettyColumns(data) 743 | }); 744 | }, 745 | 746 | ready: function() { 747 | this.onReady.call(this); 748 | }, 749 | 750 | /* 751 | * Store column names as an object 752 | * with keys of Google-formatted "columnName" 753 | * and values of human-readable "Column name" 754 | */ 755 | loadPrettyColumns: function(data) { 756 | var pretty_columns = {}; 757 | 758 | var column_names = this.column_names; 759 | 760 | var i = 0; 761 | var l = column_names.length; 762 | 763 | for (; i < l; i++) { 764 | if (typeof data.feed.entry[i].content.$t !== 'undefined') { 765 | pretty_columns[column_names[i]] = data.feed.entry[i].content.$t; 766 | } else { 767 | pretty_columns[column_names[i]] = column_names[i]; 768 | } 769 | } 770 | 771 | this.pretty_columns = pretty_columns; 772 | 773 | this.prettifyElements(); 774 | this.ready(); 775 | }, 776 | 777 | /* 778 | * Go through each row, substitutiting 779 | * Google-formatted "columnName" 780 | * with human-readable "Column name" 781 | */ 782 | prettifyElements: function() { 783 | var pretty_elements = [], 784 | ordered_pretty_names = [], 785 | i, j, ilen, jlen; 786 | 787 | var ordered_pretty_names; 788 | for(j = 0, jlen = this.column_names.length; j < jlen ; j++) { 789 | ordered_pretty_names.push(this.pretty_columns[this.column_names[j]]); 790 | } 791 | 792 | for(i = 0, ilen = this.elements.length; i < ilen; i++) { 793 | var new_element = {}; 794 | for(j = 0, jlen = this.column_names.length; j < jlen ; j++) { 795 | var new_column_name = this.pretty_columns[this.column_names[j]]; 796 | new_element[new_column_name] = this.elements[i][this.column_names[j]]; 797 | } 798 | pretty_elements.push(new_element); 799 | } 800 | this.elements = pretty_elements; 801 | this.column_names = ordered_pretty_names; 802 | }, 803 | 804 | /* 805 | Return the elements as an array of arrays, instead of an array of objects 806 | */ 807 | toArray: function() { 808 | var array = [], 809 | i, j, ilen, jlen; 810 | for(i = 0, ilen = this.elements.length; i < ilen; i++) { 811 | var row = []; 812 | for(j = 0, jlen = this.column_names.length; j < jlen ; j++) { 813 | row.push( this.elements[i][ this.column_names[j] ] ); 814 | } 815 | array.push(row); 816 | } 817 | return array; 818 | } 819 | }; 820 | 821 | if(inNodeJS) { 822 | module.exports = Tabletop; 823 | } else if (typeof define === 'function' && define.amd) { 824 | define(function () { 825 | return Tabletop; 826 | }); 827 | } else { 828 | global.Tabletop = Tabletop; 829 | } 830 | 831 | })(this); 832 | -------------------------------------------------------------------------------- /build/thenmap-1.0.1.min.js: -------------------------------------------------------------------------------- 1 | var Thenmap={debug:!1,apiUrl:"//thenmap-api.herokuapp.com/v1/",localApiUrl:"http://localhost:3000/v1/",el:null,svg:null,css:null,defaultColor:"gainsboro",settings:{width:800,height:null,language:null,projection:null,dataKey:null,dataset:"se-7",date:(new Date).toISOString(),callback:null},log:function(a){this.debug&&console.log(a+"\nIn function:"+arguments.callee.caller.name)},init:function(a,b){var c=this;c.ColorLayer.thenmap=c,c.settings=c.utils.extend(c.settings,b),"string"==typeof a?(a=a.replace(/^#/,""),c.el=document.getElementById(a)):a.nodeType&&(c.el=a);var d=document.createElement("style");document.getElementsByTagName("head")[0].appendChild(d),this.css=d;var e=e||{};e["src/styles.css"]="svg.thenmap {\n stroke: white;\n stroke-width: .25px\n}\n@keyframes loading_data {\n 0% {fill-opacity: .25}\n 100% {fill-opacity: .75}\n}\n.loading_data path {\n animation: loading_data 1s linear infinite alternate;\n}",c.extendCss(e["src/styles.css"]);var f=c.HttpClient;f.get(c.createApiUrl(),function(a){var b=JSON.parse(a),d=b.svg,e=b.data,f=document.createElement("div");f.innerHTML=d,c.svg=f.getElementsByTagName("svg")[0],c.el.appendChild(c.svg);for(var g=c.el.getElementsByTagName("path"),h=g.length;h--;){var i=g[h].getAttribute("data-id");if(i in e){var j=document.createElementNS("http://www.w3.org/2000/svg","title");j.textContent=e[i].name,g[h].appendChild(j),g[h].setAttribute("class",e[i]["class"])}else c.log("no data for shape id"+i)}c.settings.dataKey&&c.ColorLayer.init(c.settings.dataKey),"function"==typeof c.settings.callback&&c.settings.callback(null,this)})},createApiUrl:function(){var a=this,b=this.debug?this.localApiUrl:this.apiUrl;b+=[this.settings.dataset,"svg|data",this.settings.date].join("/");var c=["data_props=name|class"];return["width","height","projection","language"].forEach(function(b){null!==a.settings[b]&&c.push(b+"="+a.settings[b])}),b+="?"+c.join("&")},extendCss:function(a){this.css.styleSheet?this.css.styleSheet.cssText+=a:this.css.innerHTML+=a},HttpClient:{get:function(a,b){var c=new XMLHttpRequest;c.onreadystatechange=function(){4==c.readyState&&200==c.status&&b(c.responseText)},c.open("GET",a,!0),c.send(null)}},ColorLayer:{getSpreadsheetData:function(a,b){Tabletop.init({key:a,callback:function(a,c){b(a)},simpleSheet:!0})},getColorCode:function(a){var a=a.trim(),b=["aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","indianred","indigo","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey"," ","","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","red","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","springgreen","steelblue","tan","teal","thistle","tomato","turquoise","violet","wheat","white","whitesmoke","yellow","yellowgreen"];return/(^#[0-9A-F]{6}$){1,2}/i.test(a)?a:/(^[0-9A-F]{6}$){1,2}/i.test(a)?"#"+a:b.indexOf(a.toLowerCase())>-1?a.toLowerCase():/rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/i.test(a)?a.toLowerCase():this.thenmap.defaultColor},render:function(a){for(var b=this,c={},d=a.length;d--;){var e=a[d];if(e.color){var f=b.getColorCode(e.color),g="path."+e.id;f in c?c[f].push(g):c[f]=[g]}}var h="";for(var i in c)h+=c[i].join(", ")+"{fill:"+i+"}\n";b.thenmap.extendCss(h)},init:function(a){var b=this,c=b.thenmap.el.className||"";b.thenmap.el.className=[c,"loading_data"].join(" "),b.getSpreadsheetData(a,function(a){b.thenmap.el.className=c,b.render(a)})}},utils:{extend:function(a,b){var c,d={};for(c in a)Object.prototype.hasOwnProperty.call(a,c)&&(d[c]=a[c]);for(c in b)Object.prototype.hasOwnProperty.call(b,c)&&(d[c]=b[c]);return d}}};!function(a){"use strict";var b=!1;if("undefined"!=typeof module&&module.exports){b=!0;var c=require("request")}var d=!1,e=!1;try{var f=new XMLHttpRequest;"undefined"!=typeof f.withCredentials?d=!0:"XDomainRequest"in window&&(d=!0,e=!0)}catch(g){}var h=Array.prototype.indexOf,i=function(a,b){var c=0,d=a.length;if(h&&a.indexOf===h)return a.indexOf(b);for(;d>c;c++)if(a[c]===b)return c;return-1},j=function(a){return this&&this instanceof j?("string"==typeof a&&(a={key:a}),this.callback=a.callback,this.wanted=a.wanted||[],this.key=a.key,this.simpleSheet=!!a.simpleSheet,this.parseNumbers=!!a.parseNumbers,this.wait=!!a.wait,this.reverse=!!a.reverse,this.postProcess=a.postProcess,this.debug=!!a.debug,this.query=a.query||"",this.orderby=a.orderby,this.endpoint=a.endpoint||"https://spreadsheets.google.com",this.singleton=!!a.singleton,this.simple_url=!!a.simple_url,this.callbackContext=a.callbackContext,this.prettyColumnNames="undefined"==typeof a.prettyColumnNames?!a.proxy:a.prettyColumnNames,"undefined"!=typeof a.proxy&&(this.endpoint=a.proxy.replace(/\/$/,""),this.simple_url=!0,this.singleton=!0,d=!1),this.parameterize=a.parameterize||!1,this.singleton&&("undefined"!=typeof j.singleton&&this.log("WARNING! Tabletop singleton already defined"),j.singleton=this),/key=/.test(this.key)&&(this.log("You passed an old Google Docs url as the key! Attempting to parse."),this.key=this.key.match("key=(.*?)(&|#|$)")[1]),/pubhtml/.test(this.key)&&(this.log("You passed a new Google Spreadsheets url as the key! Attempting to parse."),this.key=this.key.match("d\\/(.*?)\\/pubhtml")[1]),this.key?(this.log("Initializing with key "+this.key),this.models={},this.model_names=[],this.base_json_path="/feeds/worksheets/"+this.key+"/public/basic?alt=",b||d?this.base_json_path+="json":this.base_json_path+="json-in-script",void(this.wait||this.fetch())):void this.log("You need to pass Tabletop a key!")):new j(a)};j.callbacks={},j.init=function(a){return new j(a)},j.sheets=function(){this.log("Times have changed! You'll want to use var tabletop = Tabletop.init(...); tabletop.sheets(...); instead of Tabletop.sheets(...)")},j.prototype={fetch:function(a){"undefined"!=typeof a&&(this.callback=a),this.requestData(this.base_json_path,this.loadSheets)},requestData:function(a,c){if(b)this.serverSideFetch(a,c);else{var f=this.endpoint.split("//").shift()||"http";!d||e&&f!==location.protocol?this.injectScript(a,c):this.xhrFetch(a,c)}},xhrFetch:function(a,b){var c=e?new XDomainRequest:new XMLHttpRequest;c.open("GET",this.endpoint+a);var d=this;c.onload=function(){try{var a=JSON.parse(c.responseText)}catch(e){console.error(e)}b.call(d,a)},c.send()},injectScript:function(a,b){var c,d=document.createElement("script");if(this.singleton)b===this.loadSheets?c="Tabletop.singleton.loadSheets":b===this.loadSheet&&(c="Tabletop.singleton.loadSheet");else{var e=this;c="tt"+ +new Date+Math.floor(1e5*Math.random()),j.callbacks[c]=function(){var a=Array.prototype.slice.call(arguments,0);b.apply(e,a),d.parentNode.removeChild(d),delete j.callbacks[c]},c="Tabletop.callbacks."+c}var f=a+"&callback="+c;this.simple_url?-1!==a.indexOf("/list/")?d.src=this.endpoint+"/"+this.key+"-"+a.split("/")[4]:d.src=this.endpoint+"/"+this.key:d.src=this.endpoint+f,this.parameterize&&(d.src=this.parameterize+encodeURIComponent(d.src)),document.getElementsByTagName("script")[0].parentNode.appendChild(d)},serverSideFetch:function(a,b){var d=this;c({url:this.endpoint+a,json:!0},function(a,c,e){return a?console.error(a):void b.call(d,e)})},isWanted:function(a){return 0===this.wanted.length?!0:-1!==i(this.wanted,a)},data:function(){return 0===this.model_names.length?void 0:this.simpleSheet?(this.model_names.length>1&&this.debug&&this.log("WARNING You have more than one sheet but are using simple sheet mode! Don't blame me when something goes wrong."),this.models[this.model_names[0]].all()):this.models},addWanted:function(a){-1===i(this.wanted,a)&&this.wanted.push(a)},loadSheets:function(a){var c,e,f=[];for(this.foundSheetNames=[],c=0,e=a.feed.entry.length;e>c;c++)if(this.foundSheetNames.push(a.feed.entry[c].title.$t),this.isWanted(a.feed.entry[c].content.$t)){var g=a.feed.entry[c].link.length-1,h=a.feed.entry[c].link[g].href.split("/").pop(),i="/feeds/list/"+this.key+"/"+h+"/public/values?alt=";i+=b||d?"json":"json-in-script",this.query&&(i+="&sq="+this.query),this.orderby&&(i+="&orderby=column:"+this.orderby.toLowerCase()),this.reverse&&(i+="&reverse=true"),f.push(i)}for(this.sheetsToLoad=f.length,c=0,e=f.length;e>c;c++)this.requestData(f[c],this.loadSheet)},sheets:function(a){return"undefined"==typeof a?this.models:"undefined"==typeof this.models[a]?void 0:this.models[a]},sheetReady:function(a){this.models[a.name]=a,-1===i(this.model_names,a.name)&&this.model_names.push(a.name),this.sheetsToLoad--,0===this.sheetsToLoad&&this.doCallback()},loadSheet:function(a){var b=this;new j.Model({data:a,parseNumbers:this.parseNumbers,postProcess:this.postProcess,tabletop:this,prettyColumnNames:this.prettyColumnNames,onReady:function(){b.sheetReady(this)}})},doCallback:function(){0===this.sheetsToLoad&&this.callback.apply(this.callbackContext||this,[this.data(),this])},log:function(a){this.debug&&"undefined"!=typeof console&&"undefined"!=typeof console.log&&Function.prototype.apply.apply(console.log,[console,arguments])}},j.Model=function(a){var b,c,d,e;if(this.column_names=[],this.name=a.data.feed.title.$t,this.tabletop=a.tabletop,this.elements=[],this.onReady=a.onReady,this.raw=a.data,"undefined"==typeof a.data.feed.entry)return a.tabletop.log("Missing data for "+this.name+", make sure you didn't forget column headers"),this.original_columns=[],this.elements=[],void this.onReady.call(this);for(var f in a.data.feed.entry[0])/^gsx/.test(f)&&this.column_names.push(f.replace("gsx$",""));for(this.original_columns=this.column_names,b=0,d=a.data.feed.entry.length;d>b;b++){for(var g=a.data.feed.entry[b],h={},c=0,e=this.column_names.length;e>c;c++){var i=g["gsx$"+this.column_names[c]];"undefined"!=typeof i?a.parseNumbers&&""!==i.$t&&!isNaN(i.$t)?h[this.column_names[c]]=+i.$t:h[this.column_names[c]]=i.$t:h[this.column_names[c]]=""}void 0===h.rowNumber&&(h.rowNumber=b+1),a.postProcess&&a.postProcess(h),this.elements.push(h)}a.prettyColumnNames?this.fetchPrettyColumns():this.onReady.call(this)},j.Model.prototype={all:function(){return this.elements},fetchPrettyColumns:function(){if(!this.raw.feed.link[3])return this.ready();var a=this.raw.feed.link[3].href.replace("/feeds/list/","/feeds/cells/").replace("https://spreadsheets.google.com",""),b=this;this.tabletop.requestData(a,function(a){b.loadPrettyColumns(a)})},ready:function(){this.onReady.call(this)},loadPrettyColumns:function(a){for(var b={},c=this.column_names,d=0,e=c.length;e>d;d++)"undefined"!=typeof a.feed.entry[d].content.$t?b[c[d]]=a.feed.entry[d].content.$t:b[c[d]]=c[d];this.pretty_columns=b,this.prettifyElements(),this.ready()},prettifyElements:function(){var a,b,c,d,e,f=[],e=[];for(b=0,d=this.column_names.length;d>b;b++)e.push(this.pretty_columns[this.column_names[b]]);for(a=0,c=this.elements.length;c>a;a++){var g={};for(b=0,d=this.column_names.length;d>b;b++){var h=this.pretty_columns[this.column_names[b]];g[h]=this.elements[a][this.column_names[b]]}f.push(g)}this.elements=f,this.column_names=e},toArray:function(){var a,b,c,d,e=[];for(a=0,c=this.elements.length;c>a;a++){var f=[];for(b=0,d=this.column_names.length;d>b;b++)f.push(this.elements[a][this.column_names[b]]);e.push(f)}return e}},b?module.exports=j:"function"==typeof define&&define.amd?define(function(){return j}):a.Tabletop=j}(this); -------------------------------------------------------------------------------- /build/thenmap-1.0.3.min.js: -------------------------------------------------------------------------------- 1 | var Thenmap={debug:!1,apiUrl:"//thenmap-api.herokuapp.com/v1/",localApiUrl:"http://localhost:3000/v1/",el:null,svg:null,css:null,defaultColor:"gainsboro",settings:{width:800,height:null,language:null,projection:null,dataKey:null,dataset:"se-7",date:(new Date).toISOString(),callback:null},log:function(a){this.debug&&console.log(a+"\nIn function:"+arguments.callee.caller.name)},init:function(a,b){var c=this;c.ColorLayer.thenmap=c,c.settings=c.utils.extend(c.settings,b),"string"==typeof a?(a=a.replace(/^#/,""),c.el=document.getElementById(a)):a.nodeType&&(c.el=a),c.el.style.width=c.settings.width+"px",c.el.style.height=c.settings.height+"px";var d=document.createElement("style");document.getElementsByTagName("head")[0].appendChild(d),this.css=d;var e=e||{};e["src/styles.css"]="svg.thenmap {\n stroke: white;\n stroke-width: .25px\n}\n@keyframes loading_data {\n 0% {fill-opacity: .25}\n 100% {fill-opacity: .75}\n}\n.loading_data path {\n animation: loading_data 1s linear infinite alternate;\n}\n\nsvg.thenmap path:hover {\n -webkit-filter: sepia(50%);\n -moz-filter: sepia(50%);\n -ms-filter: sepia(50%);\n -o-filter: sepia(50%);\n filter: sepia(50%);\n}",c.extendCss(e["src/styles.css"]);var f=c.HttpClient;f.get(c.createApiUrl(),function(a){var b=JSON.parse(a),d=b.svg,e=b.data,f=document.createElement("div");f.innerHTML=d,c.svg=f.getElementsByTagName("svg")[0],c.el.appendChild(c.svg);for(var g=c.el.getElementsByTagName("path"),h=g.length;h--;){var i=g[h].getAttribute("data-id");if(i in e){var j=e[i][0],k=document.createElementNS("http://www.w3.org/2000/svg","title");k.textContent=j.name,g[h].appendChild(k),g[h].setAttribute("class",j["class"])}else c.log("no data for shape id"+i)}c.settings.dataKey&&c.ColorLayer.init(c.settings.dataKey),"function"==typeof c.settings.callback&&c.settings.callback(null,this)})},createApiUrl:function(){var a=this,b=this.debug?this.localApiUrl:this.apiUrl;b+=[this.settings.dataset,"svg|data",this.settings.date].join("/");var c=["data_props=name|class"],d={width:"svg_width",height:"svg_height",projection:"svg_proj",language:"data_lang"};for(var e in d){var f=d[e];null!==a.settings[e]&&c.push(f+"="+a.settings[e])}return b+="?"+c.join("&")},extendCss:function(a){this.css.styleSheet?this.css.styleSheet.cssText+=a:this.css.innerHTML+=a},HttpClient:{get:function(a,b){var c=new XMLHttpRequest;c.onreadystatechange=function(){4==c.readyState&&200==c.status&&b(c.responseText)},c.open("GET",a,!0),c.send(null)}},ColorLayer:{getSpreadsheetData:function(a,b){Tabletop.init({key:a,callback:function(a,c){b(a)},simpleSheet:!0})},getColorCode:function(a){var a=a.trim(),b=["aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","indianred","indigo","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey"," ","","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","red","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","springgreen","steelblue","tan","teal","thistle","tomato","turquoise","violet","wheat","white","whitesmoke","yellow","yellowgreen"];return/(^#[0-9A-F]{6}$){1,2}/i.test(a)?a:/(^[0-9A-F]{6}$){1,2}/i.test(a)?"#"+a:b.indexOf(a.toLowerCase())>-1?a.toLowerCase():/rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/i.test(a)?a.toLowerCase():this.thenmap.defaultColor},render:function(a){for(var b=this,c={},d=a.length;d--;){var e=a[d];if(e.color){var f=b.getColorCode(e.color),g="path."+e.id;f in c?c[f].push(g):c[f]=[g]}}var h="";for(var i in c)h+=c[i].join(", ")+"{fill:"+i+"}\n";b.thenmap.extendCss(h)},init:function(a){var b=this,c=b.thenmap.el.className||"";b.thenmap.el.className=[c,"loading_data"].join(" "),b.getSpreadsheetData(a,function(a){b.thenmap.el.className=c,b.render(a)})}},utils:{extend:function(a,b){var c,d={};for(c in a)Object.prototype.hasOwnProperty.call(a,c)&&(d[c]=a[c]);for(c in b)Object.prototype.hasOwnProperty.call(b,c)&&(d[c]=b[c]);return d}}};!function(a){"use strict";var b=!1;if("undefined"!=typeof module&&module.exports){b=!0;var c=require("request")}var d=!1,e=!1;try{var f=new XMLHttpRequest;"undefined"!=typeof f.withCredentials?d=!0:"XDomainRequest"in window&&(d=!0,e=!0)}catch(g){}var h=Array.prototype.indexOf,i=function(a,b){var c=0,d=a.length;if(h&&a.indexOf===h)return a.indexOf(b);for(;d>c;c++)if(a[c]===b)return c;return-1},j=function(a){return this&&this instanceof j?("string"==typeof a&&(a={key:a}),this.callback=a.callback,this.wanted=a.wanted||[],this.key=a.key,this.simpleSheet=!!a.simpleSheet,this.parseNumbers=!!a.parseNumbers,this.wait=!!a.wait,this.reverse=!!a.reverse,this.postProcess=a.postProcess,this.debug=!!a.debug,this.query=a.query||"",this.orderby=a.orderby,this.endpoint=a.endpoint||"https://spreadsheets.google.com",this.singleton=!!a.singleton,this.simple_url=!!a.simple_url,this.callbackContext=a.callbackContext,this.prettyColumnNames="undefined"==typeof a.prettyColumnNames?!a.proxy:a.prettyColumnNames,"undefined"!=typeof a.proxy&&(this.endpoint=a.proxy.replace(/\/$/,""),this.simple_url=!0,this.singleton=!0,d=!1),this.parameterize=a.parameterize||!1,this.singleton&&("undefined"!=typeof j.singleton&&this.log("WARNING! Tabletop singleton already defined"),j.singleton=this),/key=/.test(this.key)&&(this.log("You passed an old Google Docs url as the key! Attempting to parse."),this.key=this.key.match("key=(.*?)(&|#|$)")[1]),/pubhtml/.test(this.key)&&(this.log("You passed a new Google Spreadsheets url as the key! Attempting to parse."),this.key=this.key.match("d\\/(.*?)\\/pubhtml")[1]),this.key?(this.log("Initializing with key "+this.key),this.models={},this.model_names=[],this.base_json_path="/feeds/worksheets/"+this.key+"/public/basic?alt=",b||d?this.base_json_path+="json":this.base_json_path+="json-in-script",void(this.wait||this.fetch())):void this.log("You need to pass Tabletop a key!")):new j(a)};j.callbacks={},j.init=function(a){return new j(a)},j.sheets=function(){this.log("Times have changed! You'll want to use var tabletop = Tabletop.init(...); tabletop.sheets(...); instead of Tabletop.sheets(...)")},j.prototype={fetch:function(a){"undefined"!=typeof a&&(this.callback=a),this.requestData(this.base_json_path,this.loadSheets)},requestData:function(a,c){if(b)this.serverSideFetch(a,c);else{var f=this.endpoint.split("//").shift()||"http";!d||e&&f!==location.protocol?this.injectScript(a,c):this.xhrFetch(a,c)}},xhrFetch:function(a,b){var c=e?new XDomainRequest:new XMLHttpRequest;c.open("GET",this.endpoint+a);var d=this;c.onload=function(){try{var a=JSON.parse(c.responseText)}catch(e){console.error(e)}b.call(d,a)},c.send()},injectScript:function(a,b){var c,d=document.createElement("script");if(this.singleton)b===this.loadSheets?c="Tabletop.singleton.loadSheets":b===this.loadSheet&&(c="Tabletop.singleton.loadSheet");else{var e=this;c="tt"+ +new Date+Math.floor(1e5*Math.random()),j.callbacks[c]=function(){var a=Array.prototype.slice.call(arguments,0);b.apply(e,a),d.parentNode.removeChild(d),delete j.callbacks[c]},c="Tabletop.callbacks."+c}var f=a+"&callback="+c;this.simple_url?-1!==a.indexOf("/list/")?d.src=this.endpoint+"/"+this.key+"-"+a.split("/")[4]:d.src=this.endpoint+"/"+this.key:d.src=this.endpoint+f,this.parameterize&&(d.src=this.parameterize+encodeURIComponent(d.src)),document.getElementsByTagName("script")[0].parentNode.appendChild(d)},serverSideFetch:function(a,b){var d=this;c({url:this.endpoint+a,json:!0},function(a,c,e){return a?console.error(a):void b.call(d,e)})},isWanted:function(a){return 0===this.wanted.length?!0:-1!==i(this.wanted,a)},data:function(){return 0===this.model_names.length?void 0:this.simpleSheet?(this.model_names.length>1&&this.debug&&this.log("WARNING You have more than one sheet but are using simple sheet mode! Don't blame me when something goes wrong."),this.models[this.model_names[0]].all()):this.models},addWanted:function(a){-1===i(this.wanted,a)&&this.wanted.push(a)},loadSheets:function(a){var c,e,f=[];for(this.foundSheetNames=[],c=0,e=a.feed.entry.length;e>c;c++)if(this.foundSheetNames.push(a.feed.entry[c].title.$t),this.isWanted(a.feed.entry[c].content.$t)){var g=a.feed.entry[c].link.length-1,h=a.feed.entry[c].link[g].href.split("/").pop(),i="/feeds/list/"+this.key+"/"+h+"/public/values?alt=";i+=b||d?"json":"json-in-script",this.query&&(i+="&sq="+this.query),this.orderby&&(i+="&orderby=column:"+this.orderby.toLowerCase()),this.reverse&&(i+="&reverse=true"),f.push(i)}for(this.sheetsToLoad=f.length,c=0,e=f.length;e>c;c++)this.requestData(f[c],this.loadSheet)},sheets:function(a){return"undefined"==typeof a?this.models:"undefined"==typeof this.models[a]?void 0:this.models[a]},sheetReady:function(a){this.models[a.name]=a,-1===i(this.model_names,a.name)&&this.model_names.push(a.name),this.sheetsToLoad--,0===this.sheetsToLoad&&this.doCallback()},loadSheet:function(a){var b=this;new j.Model({data:a,parseNumbers:this.parseNumbers,postProcess:this.postProcess,tabletop:this,prettyColumnNames:this.prettyColumnNames,onReady:function(){b.sheetReady(this)}})},doCallback:function(){0===this.sheetsToLoad&&this.callback.apply(this.callbackContext||this,[this.data(),this])},log:function(a){this.debug&&"undefined"!=typeof console&&"undefined"!=typeof console.log&&Function.prototype.apply.apply(console.log,[console,arguments])}},j.Model=function(a){var b,c,d,e;if(this.column_names=[],this.name=a.data.feed.title.$t,this.tabletop=a.tabletop,this.elements=[],this.onReady=a.onReady,this.raw=a.data,"undefined"==typeof a.data.feed.entry)return a.tabletop.log("Missing data for "+this.name+", make sure you didn't forget column headers"),this.original_columns=[],this.elements=[],void this.onReady.call(this);for(var f in a.data.feed.entry[0])/^gsx/.test(f)&&this.column_names.push(f.replace("gsx$",""));for(this.original_columns=this.column_names,b=0,d=a.data.feed.entry.length;d>b;b++){for(var g=a.data.feed.entry[b],h={},c=0,e=this.column_names.length;e>c;c++){var i=g["gsx$"+this.column_names[c]];"undefined"!=typeof i?a.parseNumbers&&""!==i.$t&&!isNaN(i.$t)?h[this.column_names[c]]=+i.$t:h[this.column_names[c]]=i.$t:h[this.column_names[c]]=""}void 0===h.rowNumber&&(h.rowNumber=b+1),a.postProcess&&a.postProcess(h),this.elements.push(h)}a.prettyColumnNames?this.fetchPrettyColumns():this.onReady.call(this)},j.Model.prototype={all:function(){return this.elements},fetchPrettyColumns:function(){if(!this.raw.feed.link[3])return this.ready();var a=this.raw.feed.link[3].href.replace("/feeds/list/","/feeds/cells/").replace("https://spreadsheets.google.com",""),b=this;this.tabletop.requestData(a,function(a){b.loadPrettyColumns(a)})},ready:function(){this.onReady.call(this)},loadPrettyColumns:function(a){for(var b={},c=this.column_names,d=0,e=c.length;e>d;d++)"undefined"!=typeof a.feed.entry[d].content.$t?b[c[d]]=a.feed.entry[d].content.$t:b[c[d]]=c[d];this.pretty_columns=b,this.prettifyElements(),this.ready()},prettifyElements:function(){var a,b,c,d,e,f=[],e=[];for(b=0,d=this.column_names.length;d>b;b++)e.push(this.pretty_columns[this.column_names[b]]);for(a=0,c=this.elements.length;c>a;a++){var g={};for(b=0,d=this.column_names.length;d>b;b++){var h=this.pretty_columns[this.column_names[b]];g[h]=this.elements[a][this.column_names[b]]}f.push(g)}this.elements=f,this.column_names=e},toArray:function(){var a,b,c,d,e=[];for(a=0,c=this.elements.length;c>a;a++){var f=[];for(b=0,d=this.column_names.length;d>b;b++)f.push(this.elements[a][this.column_names[b]]);e.push(f)}return e}},b?module.exports=j:"function"==typeof define&&define.amd?define(function(){return j}):a.Tabletop=j}(this); -------------------------------------------------------------------------------- /examples/eastern_block_1980.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The Eastern Bloc, 1980 5 | 6 | 7 | 8 | 15 | 16 | 17 | 18 |
19 |

The Eastern Bloc, 1980

20 |
21 | 22 |

Code

23 |

24 | 
25 |   

Data

26 | 27 | 28 |
29 | 30 | 31 | 42 | 43 | 55 | 56 | -------------------------------------------------------------------------------- /examples/french_colonies_1949.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | French colonies, 1949 5 | 6 | 7 | 8 | 15 | 16 | 17 | 18 |
19 |

French colonies, 1949

20 |
21 | 22 |

Code

23 |

24 | 
25 |   

Data

26 | 27 | 28 |
29 | 30 | 31 | 44 | 45 | 57 | 58 | -------------------------------------------------------------------------------- /examples/municipality_map_1973.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Municipalities where the social democrats got more than 50 percent of votes in 1973 elections 5 | 6 | 7 | 8 | 15 | 16 | 17 | 18 |
19 |

Municipalities where the social democrats got more than 50 percent of votes in 1973 elections

20 |
21 | 22 |

Code

23 |

24 | 
25 |   

Data

26 | 27 | 28 |
29 | 30 | 31 | 44 | 45 | 57 | 58 | -------------------------------------------------------------------------------- /examples/us_states.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | U.S. states and territories 1886 5 | 6 | 7 | 8 | 15 | 16 | 17 | 18 |
19 |

U.S. states and territories 1886

20 |
21 | 22 |

Code

23 |

24 | 
25 |   

Data

26 | 27 | 28 |
29 | 30 | 31 | 42 | 43 | 55 | 56 | -------------------------------------------------------------------------------- /js/tabletop.js: -------------------------------------------------------------------------------- 1 | /*Copyright (c) 2012-2018 Jonathan Soma 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | (function() { 25 | 'use strict'; 26 | 27 | var inNodeJS = false; 28 | if (typeof process !== 'undefined' && !process.browser) { 29 | inNodeJS = true; 30 | var request = require('request'.trim()); //prevents browserify from bundling the module 31 | } 32 | 33 | var supportsCORS = false; 34 | var inLegacyIE = false; 35 | try { 36 | var testXHR = new XMLHttpRequest(); 37 | if (typeof testXHR.withCredentials !== 'undefined') { 38 | supportsCORS = true; 39 | } else { 40 | if ('XDomainRequest' in window) { 41 | supportsCORS = true; 42 | inLegacyIE = true; 43 | } 44 | } 45 | } catch (e) { } 46 | 47 | // Create a simple indexOf function for support 48 | // of older browsers. Uses native indexOf if 49 | // available. Code similar to underscores. 50 | // By making a separate function, instead of adding 51 | // to the prototype, we will not break bad for loops 52 | // in older browsers 53 | var indexOfProto = Array.prototype.indexOf; 54 | var ttIndexOf = function(array, item) { 55 | var i = 0, l = array.length; 56 | 57 | if (indexOfProto && array.indexOf === indexOfProto) { 58 | return array.indexOf(item); 59 | } 60 | 61 | for (; i < l; i++) { 62 | if (array[i] === item) { 63 | return i; 64 | } 65 | } 66 | return -1; 67 | }; 68 | 69 | /* 70 | Initialize with Tabletop.init( { key: '0AjAPaAU9MeLFdHUxTlJiVVRYNGRJQnRmSnQwTlpoUXc' } ) 71 | OR! 72 | Initialize with Tabletop.init( { key: 'https://docs.google.com/spreadsheet/pub?hl=en_US&hl=en_US&key=0AjAPaAU9MeLFdHUxTlJiVVRYNGRJQnRmSnQwTlpoUXc&output=html&widget=true' } ) 73 | OR! 74 | Initialize with Tabletop.init('0AjAPaAU9MeLFdHUxTlJiVVRYNGRJQnRmSnQwTlpoUXc') 75 | */ 76 | 77 | var Tabletop = function(options) { 78 | // Make sure Tabletop is being used as a constructor no matter what. 79 | if(!this || !(this instanceof Tabletop)) { 80 | return new Tabletop(options); 81 | } 82 | 83 | if(typeof(options) === 'string') { 84 | options = { key : options }; 85 | } 86 | 87 | this.callback = options.callback; 88 | this.wanted = options.wanted || []; 89 | this.key = options.key; 90 | this.simpleSheet = !!options.simpleSheet; 91 | this.parseNumbers = !!options.parseNumbers; 92 | this.wait = !!options.wait; 93 | this.reverse = !!options.reverse; 94 | this.postProcess = options.postProcess; 95 | this.debug = !!options.debug; 96 | this.query = options.query || ''; 97 | this.orderby = options.orderby; 98 | this.endpoint = options.endpoint || 'https://spreadsheets.google.com'; 99 | this.singleton = !!options.singleton; 100 | this.simpleUrl = !!(options.simpleUrl || options.simple_url); //jshint ignore:line 101 | this.callbackContext = options.callbackContext; 102 | // Default to on, unless there's a proxy, in which case it's default off 103 | this.prettyColumnNames = typeof(options.prettyColumnNames) === 'undefined' ? !options.proxy : options.prettyColumnNames; 104 | 105 | if(typeof(options.proxy) !== 'undefined') { 106 | // Remove trailing slash, it will break the app 107 | this.endpoint = options.proxy.replace(/\/$/,''); 108 | this.simpleUrl = true; 109 | this.singleton = true; 110 | // Let's only use CORS (straight JSON request) when 111 | // fetching straight from Google 112 | supportsCORS = false; 113 | } 114 | 115 | this.parameterize = options.parameterize || false; 116 | 117 | if (this.singleton) { 118 | if (typeof(Tabletop.singleton) !== 'undefined') { 119 | this.log('WARNING! Tabletop singleton already defined'); 120 | } 121 | Tabletop.singleton = this; 122 | } 123 | 124 | /* Be friendly about what you accept */ 125 | if (/key=/.test(this.key)) { 126 | this.log('You passed an old Google Docs url as the key! Attempting to parse.'); 127 | this.key = this.key.match('key=(.*?)(&|#|$)')[1]; 128 | } 129 | 130 | if (/pubhtml/.test(this.key)) { 131 | this.log('You passed a new Google Spreadsheets url as the key! Attempting to parse.'); 132 | this.key = this.key.match('d\\/(.*?)\\/pubhtml')[1]; 133 | } 134 | 135 | if(/spreadsheets\/d/.test(this.key)) { 136 | this.log('You passed the most recent version of Google Spreadsheets url as the key! Attempting to parse.'); 137 | this.key = this.key.match('d\\/(.*?)\/')[1]; 138 | } 139 | 140 | if (!this.key) { 141 | this.log('You need to pass Tabletop a key!'); 142 | return; 143 | } 144 | 145 | this.log('Initializing with key ' + this.key); 146 | 147 | this.models = {}; 148 | this.modelNames = []; 149 | this.model_names = this.modelNames; //jshint ignore:line 150 | 151 | this.baseJsonPath = '/feeds/worksheets/' + this.key + '/public/basic?alt='; 152 | 153 | if (inNodeJS || supportsCORS) { 154 | this.baseJsonPath += 'json'; 155 | } else { 156 | this.baseJsonPath += 'json-in-script'; 157 | } 158 | 159 | if(!this.wait) { 160 | this.fetch(); 161 | } 162 | }; 163 | 164 | // A global storage for callbacks. 165 | Tabletop.callbacks = {}; 166 | 167 | // Backwards compatibility. 168 | Tabletop.init = function(options) { 169 | return new Tabletop(options); 170 | }; 171 | 172 | Tabletop.sheets = function() { 173 | this.log('Times have changed! You\'ll want to use var tabletop = Tabletop.init(...); tabletop.sheets(...); instead of Tabletop.sheets(...)'); 174 | }; 175 | 176 | Tabletop.prototype = { 177 | 178 | fetch: function(callback) { 179 | if (typeof(callback) !== 'undefined') { 180 | this.callback = callback; 181 | } 182 | this.requestData(this.baseJsonPath, this.loadSheets); 183 | }, 184 | 185 | /* 186 | This will call the environment appropriate request method. 187 | 188 | In browser it will use JSON-P, in node it will use request() 189 | */ 190 | requestData: function(path, callback) { 191 | this.log('Requesting', path); 192 | 193 | if (inNodeJS) { 194 | this.serverSideFetch(path, callback); 195 | } else { 196 | //CORS only works in IE8/9 across the same protocol 197 | //You must have your server on HTTPS to talk to Google, or it'll fall back on injection 198 | var protocol = this.endpoint.split('//').shift() || 'http'; 199 | if (supportsCORS && (!inLegacyIE || protocol === location.protocol)) { 200 | this.xhrFetch(path, callback); 201 | } else { 202 | this.injectScript(path, callback); 203 | } 204 | } 205 | }, 206 | 207 | /* 208 | Use Cross-Origin XMLHttpRequest to get the data in browsers that support it. 209 | */ 210 | xhrFetch: function(path, callback) { 211 | //support IE8's separate cross-domain object 212 | var xhr = inLegacyIE ? new XDomainRequest() : new XMLHttpRequest(); 213 | xhr.open('GET', this.endpoint + path); 214 | var self = this; 215 | xhr.onload = function() { 216 | var json; 217 | try { 218 | json = JSON.parse(xhr.responseText); 219 | } catch (e) { 220 | console.error(e); 221 | } 222 | callback.call(self, json); 223 | }; 224 | xhr.send(); 225 | }, 226 | 227 | /* 228 | Insert the URL into the page as a script tag. Once it's loaded the spreadsheet data 229 | it triggers the callback. This helps you avoid cross-domain errors 230 | http://code.google.com/apis/gdata/samples/spreadsheet_sample.html 231 | 232 | Let's be plain-Jane and not use jQuery or anything. 233 | */ 234 | injectScript: function(path, callback) { 235 | var script = document.createElement('script'); 236 | var callbackName; 237 | 238 | if (this.singleton) { 239 | if (callback === this.loadSheets) { 240 | callbackName = 'Tabletop.singleton.loadSheets'; 241 | } else if (callback === this.loadSheet) { 242 | callbackName = 'Tabletop.singleton.loadSheet'; 243 | } 244 | } else { 245 | var self = this; 246 | callbackName = 'tt' + (+new Date()) + (Math.floor(Math.random()*100000)); 247 | // Create a temp callback which will get removed once it has executed, 248 | // this allows multiple instances of Tabletop to coexist. 249 | Tabletop.callbacks[ callbackName ] = function () { 250 | var args = Array.prototype.slice.call( arguments, 0 ); 251 | callback.apply(self, args); 252 | script.parentNode.removeChild(script); 253 | delete Tabletop.callbacks[callbackName]; 254 | }; 255 | callbackName = 'Tabletop.callbacks.' + callbackName; 256 | } 257 | 258 | var url = path + '&callback=' + callbackName; 259 | 260 | if (this.simpleUrl) { 261 | // We've gone down a rabbit hole of passing injectScript the path, so let's 262 | // just pull the sheet_id out of the path like the least efficient worker bees 263 | if(path.indexOf('/list/') !== -1) { 264 | script.src = this.endpoint + '/' + this.key + '-' + path.split('/')[4]; 265 | } else { 266 | script.src = this.endpoint + '/' + this.key; 267 | } 268 | } else { 269 | script.src = this.endpoint + url; 270 | } 271 | 272 | if (this.parameterize) { 273 | script.src = this.parameterize + encodeURIComponent(script.src); 274 | } 275 | 276 | this.log('Injecting', script.src); 277 | 278 | document.getElementsByTagName('script')[0].parentNode.appendChild(script); 279 | }, 280 | 281 | /* 282 | This will only run if tabletop is being run in node.js 283 | */ 284 | serverSideFetch: function(path, callback) { 285 | var self = this; 286 | 287 | this.log('Fetching', this.endpoint + path); 288 | request({url: this.endpoint + path, json: true}, function(err, resp, body) { 289 | if (err) { 290 | return console.error(err); 291 | } 292 | callback.call(self, body); 293 | }); 294 | }, 295 | 296 | /* 297 | Is this a sheet you want to pull? 298 | If { wanted: ["Sheet1"] } has been specified, only Sheet1 is imported 299 | Pulls all sheets if none are specified 300 | */ 301 | isWanted: function(sheetName) { 302 | if (this.wanted.length === 0) { 303 | return true; 304 | } else { 305 | return (ttIndexOf(this.wanted, sheetName) !== -1); 306 | } 307 | }, 308 | 309 | /* 310 | What gets send to the callback 311 | if simpleSheet === true, then don't return an array of Tabletop.this.models, 312 | only return the first one's elements 313 | */ 314 | data: function() { 315 | // If the instance is being queried before the data's been fetched 316 | // then return undefined. 317 | if (this.modelNames.length === 0) { 318 | return undefined; 319 | } 320 | if (this.simpleSheet) { 321 | if (this.modelNames.length > 1 && this.debug) { 322 | this.log('WARNING You have more than one sheet but are using simple sheet mode! Don\'t blame me when something goes wrong.'); 323 | } 324 | return this.models[this.modelNames[0]].all(); 325 | } else { 326 | return this.models; 327 | } 328 | }, 329 | 330 | /* 331 | Add another sheet to the wanted list 332 | */ 333 | addWanted: function(sheet) { 334 | if(ttIndexOf(this.wanted, sheet) === -1) { 335 | this.wanted.push(sheet); 336 | } 337 | }, 338 | 339 | /* 340 | Load all worksheets of the spreadsheet, turning each into a Tabletop Model. 341 | Need to use injectScript because the worksheet view that you're working from 342 | doesn't actually include the data. The list-based feed (/feeds/list/key..) does, though. 343 | Calls back to loadSheet in order to get the real work done. 344 | 345 | Used as a callback for the worksheet-based JSON 346 | */ 347 | loadSheets: function(data) { 348 | var i, ilen; 349 | var toLoad = []; 350 | this.googleSheetName = data.feed.title.$t; 351 | this.foundSheetNames = []; 352 | 353 | for (i = 0, ilen = data.feed.entry.length; i < ilen ; i++) { 354 | this.foundSheetNames.push(data.feed.entry[i].title.$t); 355 | // Only pull in desired sheets to reduce loading 356 | if (this.isWanted(data.feed.entry[i].content.$t)) { 357 | var linkIdx = data.feed.entry[i].link.length-1; 358 | var sheetId = data.feed.entry[i].link[linkIdx].href.split('/').pop(); 359 | var jsonPath = '/feeds/list/' + this.key + '/' + sheetId + '/public/values?alt='; 360 | if (inNodeJS || supportsCORS) { 361 | jsonPath += 'json'; 362 | } else { 363 | jsonPath += 'json-in-script'; 364 | } 365 | if (this.query) { 366 | // Query Language Reference (0.7) 367 | jsonPath += '&tq=' + this.query; 368 | } 369 | if (this.orderby) { 370 | jsonPath += '&orderby=column:' + this.orderby.toLowerCase(); 371 | } 372 | if (this.reverse) { 373 | jsonPath += '&reverse=true'; 374 | } 375 | toLoad.push(jsonPath); 376 | } 377 | } 378 | 379 | this.sheetsToLoad = toLoad.length; 380 | for(i = 0, ilen = toLoad.length; i < ilen; i++) { 381 | this.requestData(toLoad[i], this.loadSheet); 382 | } 383 | }, 384 | 385 | /* 386 | Access layer for the this.models 387 | .sheets() gets you all of the sheets 388 | .sheets('Sheet1') gets you the sheet named Sheet1 389 | */ 390 | sheets: function(sheetName) { 391 | if (typeof sheetName === 'undefined') { 392 | return this.models; 393 | } else { 394 | if (typeof(this.models[sheetName]) === 'undefined') { 395 | // alert( "Can't find " + sheetName ); 396 | return; 397 | } else { 398 | return this.models[sheetName]; 399 | } 400 | } 401 | }, 402 | 403 | sheetReady: function(model) { 404 | this.models[model.name] = model; 405 | if (ttIndexOf(this.modelNames, model.name) === -1) { 406 | this.modelNames.push(model.name); 407 | } 408 | 409 | this.sheetsToLoad--; 410 | if (this.sheetsToLoad === 0) { 411 | this.doCallback(); 412 | } 413 | }, 414 | 415 | /* 416 | Parse a single list-based worksheet, turning it into a Tabletop Model 417 | 418 | Used as a callback for the list-based JSON 419 | */ 420 | loadSheet: function(data) { 421 | var that = this; 422 | new Tabletop.Model({ 423 | data: data, 424 | parseNumbers: this.parseNumbers, 425 | postProcess: this.postProcess, 426 | tabletop: this, 427 | prettyColumnNames: this.prettyColumnNames, 428 | onReady: function() { 429 | that.sheetReady(this); 430 | } 431 | }); 432 | }, 433 | 434 | /* 435 | Execute the callback upon loading! Rely on this.data() because you might 436 | only request certain pieces of data (i.e. simpleSheet mode) 437 | Tests this.sheetsToLoad just in case a race condition happens to show up 438 | */ 439 | doCallback: function() { 440 | if(this.sheetsToLoad === 0) { 441 | this.callback.apply(this.callbackContext || this, [this.data(), this]); 442 | } 443 | }, 444 | 445 | log: function() { 446 | if(this.debug) { 447 | if(typeof console !== 'undefined' && typeof console.log !== 'undefined') { 448 | Function.prototype.apply.apply(console.log, [console, arguments]); 449 | } 450 | } 451 | } 452 | 453 | }; 454 | 455 | /* 456 | Tabletop.Model stores the attribute names and parses the worksheet data 457 | to turn it into something worthwhile 458 | 459 | Options should be in the format { data: XXX }, with XXX being the list-based worksheet 460 | */ 461 | Tabletop.Model = function(options) { 462 | var i, j, ilen, jlen; 463 | this.columnNames = []; 464 | this.column_names = this.columnNames; // jshint ignore:line 465 | this.name = options.data.feed.title.$t; 466 | this.tabletop = options.tabletop; 467 | this.elements = []; 468 | this.onReady = options.onReady; 469 | this.raw = options.data; // A copy of the sheet's raw data, for accessing minutiae 470 | 471 | if (typeof(options.data.feed.entry) === 'undefined') { 472 | options.tabletop.log('Missing data for ' + this.name + ', make sure you didn\'t forget column headers'); 473 | this.originalColumns = []; 474 | this.elements = []; 475 | this.onReady.call(this); 476 | return; 477 | } 478 | 479 | for (var key in options.data.feed.entry[0]){ 480 | if (/^gsx/.test(key)) { 481 | this.columnNames.push(key.replace('gsx$','')); 482 | } 483 | } 484 | 485 | this.originalColumns = this.columnNames; 486 | this.original_columns = this.originalColumns; // jshint ignore:line 487 | 488 | for (i = 0, ilen = options.data.feed.entry.length ; i < ilen; i++) { 489 | var source = options.data.feed.entry[i]; 490 | var element = {}; 491 | for (j = 0, jlen = this.columnNames.length; j < jlen ; j++) { 492 | var cell = source['gsx$' + this.columnNames[j]]; 493 | if (typeof(cell) !== 'undefined') { 494 | if (options.parseNumbers && cell.$t !== '' && !isNaN(cell.$t)) { 495 | element[this.columnNames[j]] = +cell.$t; 496 | } else { 497 | element[this.columnNames[j]] = cell.$t; 498 | } 499 | } else { 500 | element[this.columnNames[j]] = ''; 501 | } 502 | } 503 | if (element.rowNumber === undefined) { 504 | element.rowNumber = i + 1; 505 | } 506 | 507 | if (options.postProcess) { 508 | options.postProcess(element); 509 | } 510 | 511 | this.elements.push(element); 512 | } 513 | 514 | if (options.prettyColumnNames) { 515 | this.fetchPrettyColumns(); 516 | } else { 517 | this.onReady.call(this); 518 | } 519 | }; 520 | 521 | Tabletop.Model.prototype = { 522 | /* 523 | Returns all of the elements (rows) of the worksheet as objects 524 | */ 525 | all: function() { 526 | return this.elements; 527 | }, 528 | 529 | fetchPrettyColumns: function() { 530 | if (!this.raw.feed.link[3]) { 531 | return this.ready(); 532 | } 533 | 534 | var cellurl = this.raw.feed.link[3].href.replace('/feeds/list/', '/feeds/cells/').replace('https://spreadsheets.google.com', ''); 535 | var that = this; 536 | this.tabletop.requestData(cellurl, function(data) { 537 | that.loadPrettyColumns(data); 538 | }); 539 | }, 540 | 541 | ready: function() { 542 | this.onReady.call(this); 543 | }, 544 | 545 | /* 546 | * Store column names as an object 547 | * with keys of Google-formatted "columnName" 548 | * and values of human-readable "Column name" 549 | */ 550 | loadPrettyColumns: function(data) { 551 | var prettyColumns = {}; 552 | 553 | var columnNames = this.columnNames; 554 | 555 | var i = 0; 556 | var l = columnNames.length; 557 | 558 | for (; i < l; i++) { 559 | if (typeof data.feed.entry[i].content.$t !== 'undefined') { 560 | prettyColumns[columnNames[i]] = data.feed.entry[i].content.$t; 561 | } else { 562 | prettyColumns[columnNames[i]] = columnNames[i]; 563 | } 564 | } 565 | 566 | this.prettyColumns = prettyColumns; 567 | this.pretty_columns = this.prettyColumns; // jshint ignore:line 568 | this.prettifyElements(); 569 | this.ready(); 570 | }, 571 | 572 | /* 573 | * Go through each row, substitutiting 574 | * Google-formatted "columnName" 575 | * with human-readable "Column name" 576 | */ 577 | prettifyElements: function() { 578 | var prettyElements = [], 579 | orderedPrettyNames = [], 580 | i, j, ilen, jlen; 581 | 582 | for (j = 0, jlen = this.columnNames.length; j < jlen ; j++) { 583 | orderedPrettyNames.push(this.prettyColumns[this.columnNames[j]]); 584 | } 585 | 586 | for (i = 0, ilen = this.elements.length; i < ilen; i++) { 587 | var newElement = {}; 588 | for (j = 0, jlen = this.columnNames.length; j < jlen ; j++) { 589 | var newColumnName = this.prettyColumns[this.columnNames[j]]; 590 | newElement[newColumnName] = this.elements[i][this.columnNames[j]]; 591 | } 592 | prettyElements.push(newElement); 593 | } 594 | this.elements = prettyElements; 595 | this.columnNames = orderedPrettyNames; 596 | }, 597 | 598 | /* 599 | Return the elements as an array of arrays, instead of an array of objects 600 | */ 601 | toArray: function() { 602 | var array = [], 603 | i, j, ilen, jlen; 604 | for (i = 0, ilen = this.elements.length; i < ilen; i++) { 605 | var row = []; 606 | for (j = 0, jlen = this.columnNames.length; j < jlen ; j++) { 607 | row.push(this.elements[i][ this.columnNames[j]]); 608 | } 609 | array.push(row); 610 | } 611 | 612 | return array; 613 | } 614 | }; 615 | 616 | if(typeof module !== 'undefined' && module.exports) { //don't just use inNodeJS, we may be in Browserify 617 | module.exports = Tabletop; 618 | } else if (typeof define === 'function' && define.amd) { 619 | define(function () { 620 | return Tabletop; 621 | }); 622 | } else { 623 | window.Tabletop = Tabletop; 624 | } 625 | 626 | })(); 627 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "thenmap-js", 3 | "version": "2.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "abbrev": { 8 | "version": "1.1.1", 9 | "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", 10 | "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", 11 | "dev": true 12 | }, 13 | "ansi-regex": { 14 | "version": "2.1.1", 15 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 16 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", 17 | "dev": true 18 | }, 19 | "ansi-styles": { 20 | "version": "2.2.1", 21 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", 22 | "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", 23 | "dev": true 24 | }, 25 | "argparse": { 26 | "version": "1.0.10", 27 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 28 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 29 | "dev": true, 30 | "requires": { 31 | "sprintf-js": "~1.0.2" 32 | }, 33 | "dependencies": { 34 | "sprintf-js": { 35 | "version": "1.0.3", 36 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 37 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", 38 | "dev": true 39 | } 40 | } 41 | }, 42 | "array-find-index": { 43 | "version": "1.0.2", 44 | "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", 45 | "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", 46 | "dev": true 47 | }, 48 | "async": { 49 | "version": "1.5.2", 50 | "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", 51 | "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", 52 | "dev": true 53 | }, 54 | "balanced-match": { 55 | "version": "1.0.0", 56 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 57 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 58 | "dev": true 59 | }, 60 | "brace-expansion": { 61 | "version": "1.1.11", 62 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 63 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 64 | "dev": true, 65 | "requires": { 66 | "balanced-match": "^1.0.0", 67 | "concat-map": "0.0.1" 68 | } 69 | }, 70 | "builtin-modules": { 71 | "version": "1.1.1", 72 | "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", 73 | "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", 74 | "dev": true 75 | }, 76 | "camelcase": { 77 | "version": "2.1.1", 78 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", 79 | "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", 80 | "dev": true 81 | }, 82 | "camelcase-keys": { 83 | "version": "2.1.0", 84 | "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", 85 | "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", 86 | "dev": true, 87 | "requires": { 88 | "camelcase": "^2.0.0", 89 | "map-obj": "^1.0.0" 90 | } 91 | }, 92 | "chalk": { 93 | "version": "1.1.3", 94 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", 95 | "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", 96 | "dev": true, 97 | "requires": { 98 | "ansi-styles": "^2.2.1", 99 | "escape-string-regexp": "^1.0.2", 100 | "has-ansi": "^2.0.0", 101 | "strip-ansi": "^3.0.0", 102 | "supports-color": "^2.0.0" 103 | } 104 | }, 105 | "coffeescript": { 106 | "version": "1.10.0", 107 | "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-1.10.0.tgz", 108 | "integrity": "sha1-56qDAZF+9iGzXYo580jc3R234z4=", 109 | "dev": true 110 | }, 111 | "color-convert": { 112 | "version": "1.9.3", 113 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 114 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 115 | "dev": true, 116 | "requires": { 117 | "color-name": "1.1.3" 118 | } 119 | }, 120 | "color-name": { 121 | "version": "1.1.3", 122 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 123 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 124 | "dev": true 125 | }, 126 | "colors": { 127 | "version": "1.1.2", 128 | "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", 129 | "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", 130 | "dev": true 131 | }, 132 | "commander": { 133 | "version": "2.17.1", 134 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", 135 | "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", 136 | "dev": true 137 | }, 138 | "concat-map": { 139 | "version": "0.0.1", 140 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 141 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 142 | "dev": true 143 | }, 144 | "currently-unhandled": { 145 | "version": "0.4.1", 146 | "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", 147 | "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", 148 | "dev": true, 149 | "requires": { 150 | "array-find-index": "^1.0.1" 151 | } 152 | }, 153 | "dateformat": { 154 | "version": "1.0.12", 155 | "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz", 156 | "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=", 157 | "dev": true, 158 | "requires": { 159 | "get-stdin": "^4.0.1", 160 | "meow": "^3.3.0" 161 | } 162 | }, 163 | "decamelize": { 164 | "version": "1.2.0", 165 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", 166 | "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", 167 | "dev": true 168 | }, 169 | "duplexer": { 170 | "version": "0.1.1", 171 | "resolved": "http://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", 172 | "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", 173 | "dev": true 174 | }, 175 | "error-ex": { 176 | "version": "1.3.1", 177 | "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", 178 | "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", 179 | "dev": true, 180 | "requires": { 181 | "is-arrayish": "^0.2.1" 182 | } 183 | }, 184 | "escape-string-regexp": { 185 | "version": "1.0.5", 186 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 187 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 188 | "dev": true 189 | }, 190 | "esprima": { 191 | "version": "2.7.3", 192 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", 193 | "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", 194 | "dev": true 195 | }, 196 | "eventemitter2": { 197 | "version": "0.4.14", 198 | "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", 199 | "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=", 200 | "dev": true 201 | }, 202 | "exit": { 203 | "version": "0.1.2", 204 | "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", 205 | "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", 206 | "dev": true 207 | }, 208 | "figures": { 209 | "version": "1.7.0", 210 | "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", 211 | "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", 212 | "dev": true, 213 | "requires": { 214 | "escape-string-regexp": "^1.0.5", 215 | "object-assign": "^4.1.0" 216 | } 217 | }, 218 | "file-sync-cmp": { 219 | "version": "0.1.1", 220 | "resolved": "https://registry.npmjs.org/file-sync-cmp/-/file-sync-cmp-0.1.1.tgz", 221 | "integrity": "sha1-peeo/7+kk7Q7kju9TKiaU7Y7YSs=", 222 | "dev": true 223 | }, 224 | "find-up": { 225 | "version": "1.1.2", 226 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", 227 | "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", 228 | "dev": true, 229 | "requires": { 230 | "path-exists": "^2.0.0", 231 | "pinkie-promise": "^2.0.0" 232 | } 233 | }, 234 | "findup-sync": { 235 | "version": "0.3.0", 236 | "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz", 237 | "integrity": "sha1-N5MKpdgWt3fANEXhlmzGeQpMCxY=", 238 | "dev": true, 239 | "requires": { 240 | "glob": "~5.0.0" 241 | }, 242 | "dependencies": { 243 | "glob": { 244 | "version": "5.0.15", 245 | "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", 246 | "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", 247 | "dev": true, 248 | "requires": { 249 | "inflight": "^1.0.4", 250 | "inherits": "2", 251 | "minimatch": "2 || 3", 252 | "once": "^1.3.0", 253 | "path-is-absolute": "^1.0.0" 254 | } 255 | } 256 | } 257 | }, 258 | "fs.realpath": { 259 | "version": "1.0.0", 260 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 261 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 262 | "dev": true 263 | }, 264 | "get-stdin": { 265 | "version": "4.0.1", 266 | "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", 267 | "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", 268 | "dev": true 269 | }, 270 | "getobject": { 271 | "version": "0.1.0", 272 | "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", 273 | "integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw=", 274 | "dev": true 275 | }, 276 | "glob": { 277 | "version": "7.0.6", 278 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz", 279 | "integrity": "sha1-IRuvr0nlJbjNkyYNFKsTYVKz9Xo=", 280 | "dev": true, 281 | "requires": { 282 | "fs.realpath": "^1.0.0", 283 | "inflight": "^1.0.4", 284 | "inherits": "2", 285 | "minimatch": "^3.0.2", 286 | "once": "^1.3.0", 287 | "path-is-absolute": "^1.0.0" 288 | } 289 | }, 290 | "graceful-fs": { 291 | "version": "4.1.11", 292 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", 293 | "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", 294 | "dev": true 295 | }, 296 | "grunt": { 297 | "version": "1.0.3", 298 | "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.0.3.tgz", 299 | "integrity": "sha512-/JzmZNPfKorlCrrmxWqQO4JVodO+DVd5XX4DkocL/1WlLlKVLE9+SdEIempOAxDhWPysLle6afvn/hg7Ck2k9g==", 300 | "dev": true, 301 | "requires": { 302 | "coffeescript": "~1.10.0", 303 | "dateformat": "~1.0.12", 304 | "eventemitter2": "~0.4.13", 305 | "exit": "~0.1.1", 306 | "findup-sync": "~0.3.0", 307 | "glob": "~7.0.0", 308 | "grunt-cli": "~1.2.0", 309 | "grunt-known-options": "~1.1.0", 310 | "grunt-legacy-log": "~2.0.0", 311 | "grunt-legacy-util": "~1.1.1", 312 | "iconv-lite": "~0.4.13", 313 | "js-yaml": "~3.5.2", 314 | "minimatch": "~3.0.2", 315 | "mkdirp": "~0.5.1", 316 | "nopt": "~3.0.6", 317 | "path-is-absolute": "~1.0.0", 318 | "rimraf": "~2.6.2" 319 | }, 320 | "dependencies": { 321 | "grunt-cli": { 322 | "version": "1.2.0", 323 | "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.2.0.tgz", 324 | "integrity": "sha1-VisRnrsGndtGSs4oRVAb6Xs1tqg=", 325 | "dev": true, 326 | "requires": { 327 | "findup-sync": "~0.3.0", 328 | "grunt-known-options": "~1.1.0", 329 | "nopt": "~3.0.6", 330 | "resolve": "~1.1.0" 331 | } 332 | } 333 | } 334 | }, 335 | "grunt-contrib-concat": { 336 | "version": "1.0.1", 337 | "resolved": "https://registry.npmjs.org/grunt-contrib-concat/-/grunt-contrib-concat-1.0.1.tgz", 338 | "integrity": "sha1-YVCYYwhOhx1+ht5IwBUlntl3Rb0=", 339 | "dev": true, 340 | "requires": { 341 | "chalk": "^1.0.0", 342 | "source-map": "^0.5.3" 343 | }, 344 | "dependencies": { 345 | "source-map": { 346 | "version": "0.5.7", 347 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", 348 | "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", 349 | "dev": true 350 | } 351 | } 352 | }, 353 | "grunt-contrib-copy": { 354 | "version": "1.0.0", 355 | "resolved": "https://registry.npmjs.org/grunt-contrib-copy/-/grunt-contrib-copy-1.0.0.tgz", 356 | "integrity": "sha1-cGDGWB6QS4qw0A8HbgqPbj58NXM=", 357 | "dev": true, 358 | "requires": { 359 | "chalk": "^1.1.1", 360 | "file-sync-cmp": "^0.1.0" 361 | } 362 | }, 363 | "grunt-contrib-uglify": { 364 | "version": "3.4.0", 365 | "resolved": "https://registry.npmjs.org/grunt-contrib-uglify/-/grunt-contrib-uglify-3.4.0.tgz", 366 | "integrity": "sha512-UXsTpeP0pytpTYlmll3RDndsRXfdwmrf1tI/AtD/PrArQAzGmKMvj83aVt3D8egWlE6KqPjsJBLCCvfC52LI/A==", 367 | "dev": true, 368 | "requires": { 369 | "chalk": "^1.0.0", 370 | "maxmin": "^2.1.0", 371 | "uglify-js": "~3.4.0", 372 | "uri-path": "^1.0.0" 373 | } 374 | }, 375 | "grunt-include-replace": { 376 | "version": "5.0.0", 377 | "resolved": "https://registry.npmjs.org/grunt-include-replace/-/grunt-include-replace-5.0.0.tgz", 378 | "integrity": "sha1-CVORY7FoGmxWCRBpVFBOHBpm4Hc=", 379 | "dev": true 380 | }, 381 | "grunt-known-options": { 382 | "version": "1.1.1", 383 | "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-1.1.1.tgz", 384 | "integrity": "sha512-cHwsLqoighpu7TuYj5RonnEuxGVFnztcUqTqp5rXFGYL4OuPFofwC4Ycg7n9fYwvK6F5WbYgeVOwph9Crs2fsQ==", 385 | "dev": true 386 | }, 387 | "grunt-legacy-log": { 388 | "version": "2.0.0", 389 | "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-2.0.0.tgz", 390 | "integrity": "sha512-1m3+5QvDYfR1ltr8hjiaiNjddxGdQWcH0rw1iKKiQnF0+xtgTazirSTGu68RchPyh1OBng1bBUjLmX8q9NpoCw==", 391 | "dev": true, 392 | "requires": { 393 | "colors": "~1.1.2", 394 | "grunt-legacy-log-utils": "~2.0.0", 395 | "hooker": "~0.2.3", 396 | "lodash": "~4.17.5" 397 | } 398 | }, 399 | "grunt-legacy-log-utils": { 400 | "version": "2.0.1", 401 | "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.0.1.tgz", 402 | "integrity": "sha512-o7uHyO/J+i2tXG8r2bZNlVk20vlIFJ9IEYyHMCQGfWYru8Jv3wTqKZzvV30YW9rWEjq0eP3cflQ1qWojIe9VFA==", 403 | "dev": true, 404 | "requires": { 405 | "chalk": "~2.4.1", 406 | "lodash": "~4.17.10" 407 | }, 408 | "dependencies": { 409 | "ansi-styles": { 410 | "version": "3.2.1", 411 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 412 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 413 | "dev": true, 414 | "requires": { 415 | "color-convert": "^1.9.0" 416 | } 417 | }, 418 | "chalk": { 419 | "version": "2.4.1", 420 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", 421 | "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", 422 | "dev": true, 423 | "requires": { 424 | "ansi-styles": "^3.2.1", 425 | "escape-string-regexp": "^1.0.5", 426 | "supports-color": "^5.3.0" 427 | } 428 | }, 429 | "supports-color": { 430 | "version": "5.5.0", 431 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 432 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 433 | "dev": true, 434 | "requires": { 435 | "has-flag": "^3.0.0" 436 | } 437 | } 438 | } 439 | }, 440 | "grunt-legacy-util": { 441 | "version": "1.1.1", 442 | "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-1.1.1.tgz", 443 | "integrity": "sha512-9zyA29w/fBe6BIfjGENndwoe1Uy31BIXxTH3s8mga0Z5Bz2Sp4UCjkeyv2tI449ymkx3x26B+46FV4fXEddl5A==", 444 | "dev": true, 445 | "requires": { 446 | "async": "~1.5.2", 447 | "exit": "~0.1.1", 448 | "getobject": "~0.1.0", 449 | "hooker": "~0.2.3", 450 | "lodash": "~4.17.10", 451 | "underscore.string": "~3.3.4", 452 | "which": "~1.3.0" 453 | } 454 | }, 455 | "grunt-string-to-js": { 456 | "version": "0.0.3", 457 | "resolved": "https://registry.npmjs.org/grunt-string-to-js/-/grunt-string-to-js-0.0.3.tgz", 458 | "integrity": "sha1-fH2B6+U1//TGQvTICr+QteERKOo=", 459 | "dev": true 460 | }, 461 | "gzip-size": { 462 | "version": "3.0.0", 463 | "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-3.0.0.tgz", 464 | "integrity": "sha1-VGGI6b3DN/Zzdy+BZgRks4nc5SA=", 465 | "dev": true, 466 | "requires": { 467 | "duplexer": "^0.1.1" 468 | } 469 | }, 470 | "has-ansi": { 471 | "version": "2.0.0", 472 | "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", 473 | "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", 474 | "dev": true, 475 | "requires": { 476 | "ansi-regex": "^2.0.0" 477 | } 478 | }, 479 | "has-flag": { 480 | "version": "3.0.0", 481 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 482 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 483 | "dev": true 484 | }, 485 | "hooker": { 486 | "version": "0.2.3", 487 | "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", 488 | "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=", 489 | "dev": true 490 | }, 491 | "hosted-git-info": { 492 | "version": "2.5.0", 493 | "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", 494 | "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==", 495 | "dev": true 496 | }, 497 | "iconv-lite": { 498 | "version": "0.4.24", 499 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 500 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 501 | "dev": true, 502 | "requires": { 503 | "safer-buffer": ">= 2.1.2 < 3" 504 | } 505 | }, 506 | "indent-string": { 507 | "version": "2.1.0", 508 | "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", 509 | "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", 510 | "dev": true, 511 | "requires": { 512 | "repeating": "^2.0.0" 513 | } 514 | }, 515 | "inflight": { 516 | "version": "1.0.6", 517 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 518 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 519 | "dev": true, 520 | "requires": { 521 | "once": "^1.3.0", 522 | "wrappy": "1" 523 | } 524 | }, 525 | "inherits": { 526 | "version": "2.0.3", 527 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 528 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 529 | "dev": true 530 | }, 531 | "is-arrayish": { 532 | "version": "0.2.1", 533 | "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", 534 | "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", 535 | "dev": true 536 | }, 537 | "is-builtin-module": { 538 | "version": "1.0.0", 539 | "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", 540 | "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", 541 | "dev": true, 542 | "requires": { 543 | "builtin-modules": "^1.0.0" 544 | } 545 | }, 546 | "is-finite": { 547 | "version": "1.0.2", 548 | "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", 549 | "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", 550 | "dev": true, 551 | "requires": { 552 | "number-is-nan": "^1.0.0" 553 | } 554 | }, 555 | "is-utf8": { 556 | "version": "0.2.1", 557 | "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", 558 | "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", 559 | "dev": true 560 | }, 561 | "isexe": { 562 | "version": "2.0.0", 563 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 564 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", 565 | "dev": true 566 | }, 567 | "js-yaml": { 568 | "version": "3.5.5", 569 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.5.5.tgz", 570 | "integrity": "sha1-A3fDgBfKvHMisNH7zSWkkWQfL74=", 571 | "dev": true, 572 | "requires": { 573 | "argparse": "^1.0.2", 574 | "esprima": "^2.6.0" 575 | } 576 | }, 577 | "load-json-file": { 578 | "version": "1.1.0", 579 | "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", 580 | "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", 581 | "dev": true, 582 | "requires": { 583 | "graceful-fs": "^4.1.2", 584 | "parse-json": "^2.2.0", 585 | "pify": "^2.0.0", 586 | "pinkie-promise": "^2.0.0", 587 | "strip-bom": "^2.0.0" 588 | } 589 | }, 590 | "lodash": { 591 | "version": "4.17.11", 592 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", 593 | "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", 594 | "dev": true 595 | }, 596 | "loud-rejection": { 597 | "version": "1.6.0", 598 | "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", 599 | "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", 600 | "dev": true, 601 | "requires": { 602 | "currently-unhandled": "^0.4.1", 603 | "signal-exit": "^3.0.0" 604 | } 605 | }, 606 | "map-obj": { 607 | "version": "1.0.1", 608 | "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", 609 | "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", 610 | "dev": true 611 | }, 612 | "maxmin": { 613 | "version": "2.1.0", 614 | "resolved": "https://registry.npmjs.org/maxmin/-/maxmin-2.1.0.tgz", 615 | "integrity": "sha1-TTsiCQPZXu5+t6x/qGTnLcCaMWY=", 616 | "dev": true, 617 | "requires": { 618 | "chalk": "^1.0.0", 619 | "figures": "^1.0.1", 620 | "gzip-size": "^3.0.0", 621 | "pretty-bytes": "^3.0.0" 622 | } 623 | }, 624 | "meow": { 625 | "version": "3.7.0", 626 | "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", 627 | "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", 628 | "dev": true, 629 | "requires": { 630 | "camelcase-keys": "^2.0.0", 631 | "decamelize": "^1.1.2", 632 | "loud-rejection": "^1.0.0", 633 | "map-obj": "^1.0.1", 634 | "minimist": "^1.1.3", 635 | "normalize-package-data": "^2.3.4", 636 | "object-assign": "^4.0.1", 637 | "read-pkg-up": "^1.0.1", 638 | "redent": "^1.0.0", 639 | "trim-newlines": "^1.0.0" 640 | } 641 | }, 642 | "minimatch": { 643 | "version": "3.0.4", 644 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 645 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 646 | "dev": true, 647 | "requires": { 648 | "brace-expansion": "^1.1.7" 649 | } 650 | }, 651 | "minimist": { 652 | "version": "1.2.0", 653 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", 654 | "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", 655 | "dev": true 656 | }, 657 | "mkdirp": { 658 | "version": "0.5.1", 659 | "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 660 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 661 | "dev": true, 662 | "requires": { 663 | "minimist": "0.0.8" 664 | }, 665 | "dependencies": { 666 | "minimist": { 667 | "version": "0.0.8", 668 | "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 669 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 670 | "dev": true 671 | } 672 | } 673 | }, 674 | "nopt": { 675 | "version": "3.0.6", 676 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", 677 | "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", 678 | "dev": true, 679 | "requires": { 680 | "abbrev": "1" 681 | } 682 | }, 683 | "normalize-package-data": { 684 | "version": "2.4.0", 685 | "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", 686 | "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", 687 | "dev": true, 688 | "requires": { 689 | "hosted-git-info": "^2.1.4", 690 | "is-builtin-module": "^1.0.0", 691 | "semver": "2 || 3 || 4 || 5", 692 | "validate-npm-package-license": "^3.0.1" 693 | } 694 | }, 695 | "number-is-nan": { 696 | "version": "1.0.1", 697 | "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", 698 | "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", 699 | "dev": true 700 | }, 701 | "object-assign": { 702 | "version": "4.1.1", 703 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 704 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", 705 | "dev": true 706 | }, 707 | "once": { 708 | "version": "1.4.0", 709 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 710 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 711 | "dev": true, 712 | "requires": { 713 | "wrappy": "1" 714 | } 715 | }, 716 | "parse-json": { 717 | "version": "2.2.0", 718 | "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", 719 | "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", 720 | "dev": true, 721 | "requires": { 722 | "error-ex": "^1.2.0" 723 | } 724 | }, 725 | "path-exists": { 726 | "version": "2.1.0", 727 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", 728 | "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", 729 | "dev": true, 730 | "requires": { 731 | "pinkie-promise": "^2.0.0" 732 | } 733 | }, 734 | "path-is-absolute": { 735 | "version": "1.0.1", 736 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 737 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 738 | "dev": true 739 | }, 740 | "path-type": { 741 | "version": "1.1.0", 742 | "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", 743 | "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", 744 | "dev": true, 745 | "requires": { 746 | "graceful-fs": "^4.1.2", 747 | "pify": "^2.0.0", 748 | "pinkie-promise": "^2.0.0" 749 | } 750 | }, 751 | "pify": { 752 | "version": "2.3.0", 753 | "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", 754 | "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", 755 | "dev": true 756 | }, 757 | "pinkie": { 758 | "version": "2.0.4", 759 | "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", 760 | "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", 761 | "dev": true 762 | }, 763 | "pinkie-promise": { 764 | "version": "2.0.1", 765 | "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", 766 | "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", 767 | "dev": true, 768 | "requires": { 769 | "pinkie": "^2.0.0" 770 | } 771 | }, 772 | "pretty-bytes": { 773 | "version": "3.0.1", 774 | "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-3.0.1.tgz", 775 | "integrity": "sha1-J9AAjXeAY6C0gRuzXHnxvV1fvM8=", 776 | "dev": true, 777 | "requires": { 778 | "number-is-nan": "^1.0.0" 779 | } 780 | }, 781 | "read-pkg": { 782 | "version": "1.1.0", 783 | "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", 784 | "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", 785 | "dev": true, 786 | "requires": { 787 | "load-json-file": "^1.0.0", 788 | "normalize-package-data": "^2.3.2", 789 | "path-type": "^1.0.0" 790 | } 791 | }, 792 | "read-pkg-up": { 793 | "version": "1.0.1", 794 | "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", 795 | "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", 796 | "dev": true, 797 | "requires": { 798 | "find-up": "^1.0.0", 799 | "read-pkg": "^1.0.0" 800 | } 801 | }, 802 | "redent": { 803 | "version": "1.0.0", 804 | "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", 805 | "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", 806 | "dev": true, 807 | "requires": { 808 | "indent-string": "^2.1.0", 809 | "strip-indent": "^1.0.1" 810 | } 811 | }, 812 | "repeating": { 813 | "version": "2.0.1", 814 | "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", 815 | "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", 816 | "dev": true, 817 | "requires": { 818 | "is-finite": "^1.0.0" 819 | } 820 | }, 821 | "resolve": { 822 | "version": "1.1.7", 823 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", 824 | "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", 825 | "dev": true 826 | }, 827 | "rimraf": { 828 | "version": "2.6.2", 829 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", 830 | "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", 831 | "dev": true, 832 | "requires": { 833 | "glob": "^7.0.5" 834 | } 835 | }, 836 | "safer-buffer": { 837 | "version": "2.1.2", 838 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 839 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 840 | "dev": true 841 | }, 842 | "semver": { 843 | "version": "5.4.1", 844 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", 845 | "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", 846 | "dev": true 847 | }, 848 | "signal-exit": { 849 | "version": "3.0.2", 850 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", 851 | "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", 852 | "dev": true 853 | }, 854 | "source-map": { 855 | "version": "0.6.1", 856 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 857 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 858 | "dev": true 859 | }, 860 | "spdx-correct": { 861 | "version": "1.0.2", 862 | "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", 863 | "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", 864 | "dev": true, 865 | "requires": { 866 | "spdx-license-ids": "^1.0.2" 867 | } 868 | }, 869 | "spdx-expression-parse": { 870 | "version": "1.0.4", 871 | "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", 872 | "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=", 873 | "dev": true 874 | }, 875 | "spdx-license-ids": { 876 | "version": "1.2.2", 877 | "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", 878 | "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=", 879 | "dev": true 880 | }, 881 | "sprintf-js": { 882 | "version": "1.1.1", 883 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.1.tgz", 884 | "integrity": "sha1-Nr54Mgr+WAH2zqPueLblqrlA6gw=", 885 | "dev": true 886 | }, 887 | "strip-ansi": { 888 | "version": "3.0.1", 889 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", 890 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 891 | "dev": true, 892 | "requires": { 893 | "ansi-regex": "^2.0.0" 894 | } 895 | }, 896 | "strip-bom": { 897 | "version": "2.0.0", 898 | "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", 899 | "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", 900 | "dev": true, 901 | "requires": { 902 | "is-utf8": "^0.2.0" 903 | } 904 | }, 905 | "strip-indent": { 906 | "version": "1.0.1", 907 | "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", 908 | "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", 909 | "dev": true, 910 | "requires": { 911 | "get-stdin": "^4.0.1" 912 | } 913 | }, 914 | "supports-color": { 915 | "version": "2.0.0", 916 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", 917 | "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", 918 | "dev": true 919 | }, 920 | "trim-newlines": { 921 | "version": "1.0.0", 922 | "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", 923 | "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", 924 | "dev": true 925 | }, 926 | "uglify-js": { 927 | "version": "3.4.9", 928 | "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz", 929 | "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==", 930 | "dev": true, 931 | "requires": { 932 | "commander": "~2.17.1", 933 | "source-map": "~0.6.1" 934 | } 935 | }, 936 | "underscore.string": { 937 | "version": "3.3.5", 938 | "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.5.tgz", 939 | "integrity": "sha512-g+dpmgn+XBneLmXXo+sGlW5xQEt4ErkS3mgeN2GFbremYeMBSJKr9Wf2KJplQVaiPY/f7FN6atosWYNm9ovrYg==", 940 | "dev": true, 941 | "requires": { 942 | "sprintf-js": "^1.0.3", 943 | "util-deprecate": "^1.0.2" 944 | } 945 | }, 946 | "uri-path": { 947 | "version": "1.0.0", 948 | "resolved": "https://registry.npmjs.org/uri-path/-/uri-path-1.0.0.tgz", 949 | "integrity": "sha1-l0fwGDWJM8Md4PzP2C0TjmcmLjI=", 950 | "dev": true 951 | }, 952 | "util-deprecate": { 953 | "version": "1.0.2", 954 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 955 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", 956 | "dev": true 957 | }, 958 | "validate-npm-package-license": { 959 | "version": "3.0.1", 960 | "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", 961 | "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", 962 | "dev": true, 963 | "requires": { 964 | "spdx-correct": "~1.0.0", 965 | "spdx-expression-parse": "~1.0.0" 966 | } 967 | }, 968 | "which": { 969 | "version": "1.3.1", 970 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", 971 | "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", 972 | "dev": true, 973 | "requires": { 974 | "isexe": "^2.0.0" 975 | } 976 | }, 977 | "wrappy": { 978 | "version": "1.0.2", 979 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 980 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 981 | "dev": true 982 | } 983 | } 984 | } 985 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "thenmap-js", 3 | "version": "2.2.2", 4 | "description": "A simple tool for making choropleth map from Thenmap data", 5 | "main": "Gruntfile.js", 6 | "directories": { 7 | "examples": "examples" 8 | }, 9 | "dependencies": {}, 10 | "devDependencies": { 11 | "grunt": "^1.0.3", 12 | "grunt-contrib-concat": "^1.0.1", 13 | "grunt-contrib-copy": "^1.0.0", 14 | "grunt-contrib-uglify": "^3.4.0", 15 | "grunt-include-replace": "^5.0.0", 16 | "grunt-string-to-js": "0.0.3" 17 | }, 18 | "scripts": { 19 | "test": "echo \"Error: no test specified\" && exit 1" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/rotsee/thenmap-js.git" 24 | }, 25 | "author": "J++ Stockholm / Leonard Wallentin", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/rotsee/thenmap-js/issues" 29 | }, 30 | "homepage": "https://github.com/rotsee/thenmap-js#readme" 31 | } 32 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | svg.thenmap { 2 | stroke: white; 3 | stroke-width: .5px 4 | } 5 | @keyframes loading_data { 6 | 0% {fill-opacity: .25} 7 | 100% {fill-opacity: .75} 8 | } 9 | .loading_data path { 10 | animation: loading_data 1s linear infinite alternate; 11 | } 12 | 13 | @media screen and (-webkit-min-device-pixel-ratio:0){ 14 | svg.thenmap path:hover { 15 | filter: url("#sepia") !important; /*Chrome*/ 16 | } 17 | } 18 | svg.thenmap path:hover { 19 | -webkit-filter: sepia(50%); 20 | filter: sepia(50%); 21 | } -------------------------------------------------------------------------------- /src/styles.js: -------------------------------------------------------------------------------- 1 | var CSS = CSS || {}; 2 | CSS["src/styles.css"] = 'svg.thenmap {\n stroke: white;\n stroke-width: .5px\n}\n@keyframes loading_data {\n 0% {fill-opacity: .25}\n 100% {fill-opacity: .75}\n}\n.loading_data path {\n animation: loading_data 1s linear infinite alternate;\n}\n\nsvg.thenmap path:hover {\n filter: url("#sepia"); /*Chrome*/\n -webkit-filter: sepia(50%);\n filter: sepia(50%);\n}'; 3 | -------------------------------------------------------------------------------- /src/thenmap.js: -------------------------------------------------------------------------------- 1 | var Thenmap = { 2 | 3 | debug: false, 4 | apiUrl: "https://thenmap-api.herokuapp.com/v2/", 5 | el: null, //container element 6 | svg: null, //svg element 7 | css: null, //css element for dynamically adding styles 8 | defaultColor: "#e2e2e2", 9 | 10 | // Default settings that can be overridden by passing arguments to Thenmap 11 | settings: { 12 | width: 800, 13 | height: null, 14 | language: null, 15 | projection: null, 16 | data: null, 17 | dataKey: null, 18 | map: "world-2", 19 | date: new Date().toISOString(), //current date, works in any browser that can display SVG 20 | callback: null 21 | }, 22 | 23 | /** 24 | * Colorize the map if data is provided. This method is called on init. 25 | * 26 | * @param {Object[]} [data] Optional array of dictionaries with id and colour 27 | * @param {String} data[].id Thenmap id's for political entities 28 | * @param {String} data[].colour Colour code for use in CSS 29 | */ 30 | colour: function(data) { 31 | 32 | if (this.css.styleSheet) { 33 | this.css.styleSheet.cssText = ""; // IE 34 | } else { 35 | this.css.innerHTML = ""; 36 | } 37 | if (data) { 38 | this.ColorLayer.render(data); 39 | } else if (this.settings.data) { 40 | // FIXME: Refactor this, currently hacking into a class meant for Google Sheets rendering 41 | this.ColorLayer.render(this.settings.data); 42 | } else if (this.settings.dataKey) { 43 | this.ColorLayer.init(this.settings.dataKey); 44 | } 45 | }, 46 | 47 | /** 48 | * Entry point 49 | * 50 | * @param {string} elIdentifier ID of the map container elemtent 51 | * @param {Object} options 52 | * @param {Number} options.width=800 Width in pixels 53 | * @param {Number} options.height=null Height in pixels 54 | */ 55 | init: function(elIdentifier, options) { 56 | var self = this; 57 | self.ColorLayer.thenmap = self; 58 | 59 | // Clean up some values 60 | options.width = options.width ? parseInt(options.width) : null; 61 | options.height = options.height ? parseInt(options.height) : null; 62 | /* Backwards compatibility with old parameter name*/ 63 | if ("dataset" in options){ 64 | console.log("Warning, the “dataset” parameter has been renamed “map”. Using “dataset” will stop working in future versions.") 65 | options["map"] = options["dataset"]; 66 | } 67 | 68 | // Apply settings 69 | self.settings = self.utils.extend(self.settings, options); 70 | 71 | if (typeof elIdentifier === "string") { 72 | // If first character is #, remove. While technically a valid 73 | // character in an HTML5 id, it's likely meant as id selector 74 | elIdentifier = elIdentifier.replace(/^#/, ''); 75 | self.el = document.getElementById(elIdentifier); 76 | } else if (elIdentifier.nodeType) { 77 | // User gave us a valid reference to an element 78 | self.el = elIdentifier; 79 | } else { 80 | // not a valid identifier 81 | self.log(elIdentifier + " is not a valid id name or DOM node.") 82 | } 83 | if (self.settings.width){ 84 | self.el.style.width = self.settings.width + "px" 85 | } 86 | if (self.settings.height){ 87 | self.el.style.height = self.settings.height + "px" 88 | } 89 | 90 | // create CSS element for dynamic styling 91 | var css = document.createElement("style"); 92 | document.getElementsByTagName("head")[0].appendChild(css); 93 | this.css = css; 94 | 95 | // set global styles 96 | @@include('styles.js'); 97 | self.extendCss(CSS["src/styles.css"]); 98 | 99 | var httpClient = self.HttpClient; 100 | httpClient.get(self.createApiUrl(), function(svgString) { 101 | 102 | // Something of an hack, to make sure SVG is rendered 103 | // Creating a SVG element will not make the SVG render 104 | // in all browsers. innerHTML will. 105 | var tmp = document.createElement("div"); 106 | tmp.innerHTML = svgString; 107 | self.svg = tmp.getElementsByTagName('svg')[0]; 108 | 109 | //Add filter for hover effect in Chrome 110 | var defsEl = self.svg.getElementsByTagName('defs')[0]; 111 | var svgNS = "http://www.w3.org/2000/svg"; 112 | var filterEl = document.createElementNS(svgNS, "filter"); 113 | filterEl.id = "sepia"; 114 | filterEl.innerHTML = ""; 118 | defsEl.appendChild(filterEl); 119 | 120 | self.el.appendChild(self.svg); 121 | 122 | //Apply classes, add titles 123 | var paths=self.el.getElementsByTagName('path'); 124 | var i = paths.length; 125 | while(i--) { 126 | //There will only be one entity for each shape 127 | var title = document.createElementNS(svgNS,"title"); 128 | title.textContent = paths[i].getAttribute("thenmap:name"); 129 | paths[i].appendChild(title); 130 | 131 | //element.className is not available for SVG elements 132 | paths[i].setAttribute("class", paths[i].getAttribute("thenmap:class")); 133 | } 134 | 135 | // Apply any colouring 136 | self.colour(); 137 | 138 | if (typeof self.settings.callback === "function"){ 139 | self.settings.callback(null, this); 140 | } 141 | 142 | }); 143 | 144 | }, // function init 145 | 146 | createApiUrl: function() { 147 | var self = this; 148 | var apiUrl = this.apiUrl; 149 | apiUrl += [this.settings.map, "svg", this.settings.date].join("/"); 150 | // Add url parameters 151 | var options = ["svg_props=name|class"]; 152 | var paramDict = {width: "svg_width", 153 | height: "svg_height", 154 | projection: "svg_proj", 155 | language: "language"}; 156 | for (var key in paramDict) { 157 | var attr = paramDict[key]; 158 | if (self.settings[key] !== null){ 159 | options.push(attr + "=" + self.settings[key]); 160 | } 161 | } 162 | apiUrl += "?" + options.join("&"); 163 | return apiUrl; 164 | }, // function createApiUrl 165 | 166 | /* Add code to the global stylesheet 167 | */ 168 | extendCss: function(code) { 169 | 170 | if (this.css.styleSheet) { 171 | // IE 172 | this.css.styleSheet.cssText += code; 173 | } else { 174 | // Other browsers 175 | this.css.innerHTML += code; 176 | } 177 | 178 | }, 179 | 180 | HttpClient: { 181 | get: function(url, callback) { 182 | var httpRequest = new XMLHttpRequest(); 183 | httpRequest.onreadystatechange = function() { 184 | if (httpRequest.readyState == 4 && httpRequest.status == 200) { 185 | callback(httpRequest.responseText); 186 | } 187 | } 188 | 189 | httpRequest.open("GET", url, true); 190 | httpRequest.send(null); 191 | } 192 | }, // HttpClient 193 | 194 | ColorLayer: { 195 | 196 | /* Fetches data from a Google Spreadsheet using Tabletop 197 | */ 198 | getSpreadsheetData: function(spreadsheetKey, callback) { 199 | Tabletop.init({ 200 | key: spreadsheetKey, 201 | callback: callback, 202 | simpleSheet: true 203 | }) 204 | }, // getSpreadsheetData 205 | 206 | /* Sanitize and validate a SVG color code 207 | Accepts "#99cccc", "9cc", "green", and "rgb(1,32,42)" 208 | */ 209 | getColorCode: function(string){ 210 | 211 | var string = string.trim(); 212 | var allowedColorNames = ["aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","indianred","indigo","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey"," ","","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","red","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","springgreen","steelblue","tan","teal","thistle","tomato","turquoise","violet","wheat","white","whitesmoke","yellow","yellowgreen"]; 213 | if (/(^#[0-9A-F]{6}$){1,2}/i.test(string)) { 214 | // #00cccc 215 | return string; 216 | } else if (/(^[0-9A-F]{6}$){1,2}/i.test(string)) { 217 | // 00cccc 218 | return "#" + string; 219 | } else if (allowedColorNames.indexOf(string.toLowerCase()) > -1) { // will work for all SVG capable browsers 220 | // green 221 | return string.toLowerCase(); 222 | } else if (/rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/i.test(string)){ 223 | // rgb(123,231,432) 224 | return string.toLowerCase(); 225 | } else { 226 | // *invalid 227 | return this.thenmap.defaultColor; 228 | } 229 | 230 | }, 231 | 232 | /* Colorize map 233 | */ 234 | render: function(data) { 235 | var self = this; 236 | var colors = {} 237 | 238 | /* Create a colors object like this: 239 | { green: [class1, class2], ... } 240 | */ 241 | var i = data.length; 242 | while(i--) { 243 | var d = data[i]; 244 | if (d.color) { 245 | var colorCode = self.getColorCode(d.color); 246 | var selector = "path." + d.id; 247 | if (colorCode in colors){ 248 | colors[colorCode].push(selector); 249 | } else { 250 | colors[colorCode] = [selector]; 251 | } 252 | } 253 | } 254 | 255 | /* build and apply CSS */ 256 | var cssCode = ""; 257 | for (var color in colors) { 258 | cssCode += colors[color].join(", ") + "{fill:" + color + "}\n"; 259 | } 260 | self.thenmap.extendCss(cssCode); 261 | }, // ColorLayer.render 262 | 263 | /* Constructor for thenmap.ColorLayer 264 | */ 265 | init: function(spreadsheetKey) { 266 | var self = this; 267 | 268 | // Add loader class while loading 269 | var oldClassName = self.thenmap.el.className || ""; 270 | self.thenmap.el.className = [oldClassName, "loading_data"].join(" "); 271 | self.getSpreadsheetData(spreadsheetKey, function(data) { 272 | // Remove loader class 273 | self.thenmap.el.className = oldClassName; 274 | //Use data 275 | self.render(data); 276 | }); 277 | } // ColorLayer.init 278 | 279 | }, // ColorLayer 280 | 281 | utils: { 282 | /* Object.assign() replacement, more or less */ 283 | extend: function ( defaults, options ) { 284 | var extended = {}; 285 | var prop; 286 | for (prop in defaults) { 287 | if (Object.prototype.hasOwnProperty.call(defaults, prop)) { 288 | extended[prop] = defaults[prop]; 289 | } 290 | } 291 | for (prop in options) { 292 | if (Object.prototype.hasOwnProperty.call(options, prop)) { 293 | extended[prop] = options[prop]; 294 | } 295 | } 296 | return extended; 297 | } // Extend js object 298 | },// Utils 299 | 300 | /* Print debug message to the console 301 | */ 302 | log: function(string) { 303 | if (this.debug) { 304 | console.log(string + "\nIn function:"+arguments.callee.caller.name); 305 | } 306 | } 307 | }; 308 | --------------------------------------------------------------------------------