├── .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 |
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 |