diff --git a/.gitignore b/.gitignore index c210b96ee1..0bac695bd7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ /dev_bundle*.tar.gz /dist \#*\# -.\#* \ No newline at end of file +.\#* +.idea diff --git a/History.md b/History.md index 364ca06904..c8776e5f82 100644 --- a/History.md +++ b/History.md @@ -1,6 +1,42 @@ ## vNEXT +## v0.4.0 + +* Merge Spark, a new live page update engine + * Breaking API changes + * Input elements no longer preserved based on `id` and `name` attributes. Use [`preserve`](http://docs.meteor.com/#template_preserve) instead. + + * All `Meteor.ui` functions removed. Use `Meteor.render`, `Meteor.renderList`, and [Spark](https://github.com/meteor/meteor/wiki/Spark) functions instead. + + * New template functions (eg. `created`, `rendered`, etc) may collide with existing helpers. Use `Template.foo.helpers()` to avoid conflicts. + + * New syntax for declaring event maps. Use `Template.foo.events({...})`. For backwards compatibility, both syntaxes are allowed for now. + + * New Template features + + * Allow embedding non-Meteor widgets (eg. Google Maps) using [`{{#constant}}`](http://docs.meteor.com/#constant) + + * Callbacks when templates are rendered. See http://docs.meteor.com/#template_rendered + + * Explicit control of which nodes are preserved during re-rendering. See http://docs.meteor.com/#template_preserve + + * Easily find nodes within a template in event handlers and callbacks. See http://docs.meteor.com/#template_find + + * Allow parts of a template to be independently reactive with the [`{{#isolate}}`](http://docs.meteor.com/#isolate) block helper. + + +* Use PACKAGE_DIRS environment variable to override package location. #227 + +* Add `absolute-url` package to construct URLs pointing to the application. + +* Allow modifying documents returned by `observe` callbacks. #209 + +* Fix periodic crash after client disconnect. #212 + +* Fix minimingo crash on dotted queries with undefined keys. #126 + + ## v0.3.9 * Add `spiderable` package to allow web crawlers to index Meteor apps. diff --git a/LICENSE.txt b/LICENSE.txt index e6f90a6950..f6007f22c1 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -581,6 +581,38 @@ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +---------- +d3: https://github.com/mbostock/d3 +---------- + +Copyright (c) 2012, Michael Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* The name Michael Bostock may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + ============= diff --git a/admin/debian/changelog b/admin/debian/changelog index e7af1136c1..b4c3533497 100644 --- a/admin/debian/changelog +++ b/admin/debian/changelog @@ -1,4 +1,4 @@ -meteor (0.3.9-1) unstable; urgency=low +meteor (0.4.0-1) unstable; urgency=low * Automated debian build. diff --git a/admin/install-s3.sh b/admin/install-s3.sh index a98a5a6f93..d302a6ec5b 100755 --- a/admin/install-s3.sh +++ b/admin/install-s3.sh @@ -5,7 +5,7 @@ ## example. URLBASE="https://d3sqy0vbqsdhku.cloudfront.net" -VERSION="0.3.9" +VERSION="0.4.0" PKGVERSION="${VERSION}-1" UNAME=`uname` diff --git a/admin/manifest.json b/admin/manifest.json index 6909f075f4..ec2de4b0dc 100644 --- a/admin/manifest.json +++ b/admin/manifest.json @@ -1,6 +1,6 @@ { - "version": "0.3.9", - "deb_version": "0.3.9-1", - "rpm_version": "0.3.9-1", + "version": "0.4.0", + "deb_version": "0.4.0-1", + "rpm_version": "0.4.0-1", "urlbase": "https://d3sqy0vbqsdhku.cloudfront.net" } diff --git a/admin/meteor.spec b/admin/meteor.spec index b158cf9dfd..31233ed895 100644 --- a/admin/meteor.spec +++ b/admin/meteor.spec @@ -5,7 +5,7 @@ Summary: Meteor platform and JavaScript application server Vendor: Meteor Name: meteor -Version: 0.3.9 +Version: 0.4.0 Release: 1 License: MIT Group: Networking/WWW diff --git a/app/lib/files.js b/app/lib/files.js index 0b9addf12f..53245bbd56 100644 --- a/app/lib/files.js +++ b/app/lib/files.js @@ -191,10 +191,32 @@ var files = module.exports = { else return path.join(__dirname, '../..'); }, - - // Return where the packages are stored - get_package_dir: function () { - return path.join(__dirname, '../../packages'); + + // returns a list of places where packages can be found. + // 1. directories set via process.env.PACKAGES_DIRS + // 2. default is packages/ in the meteor directory + // XXX: 3. a per project directory? (vendor/packages in rails parlance?) + get_package_dirs: function() { + var package_dirs = [path.join(__dirname, '../../packages')]; + if (process.env.PACKAGE_DIRS) + package_dirs = process.env.PACKAGE_DIRS.split(':').concat(package_dirs); + + return package_dirs; + }, + + // search package dirs for a package named name. + // undefined if the package isn't in any dir + get_package_dir: function (name) { + var ret; + _.find(this.get_package_dirs(), function(package_dir) { + var dir = path.join(package_dir, name); + if (path.existsSync(dir)) { + ret = dir; + return true; + } + }); + + return ret; }, // Return the directory that contains the core tool (the top-level diff --git a/app/lib/packages.js b/app/lib/packages.js index 6262c3e56c..cf2a357963 100644 --- a/app/lib/packages.js +++ b/app/lib/packages.js @@ -84,10 +84,10 @@ _.extend(Package.prototype, { init_from_library: function (name) { var self = this; self.name = name; - self.source_root = path.join(__dirname, '../../packages', name); + self.source_root = files.get_package_dir(name); self.serve_root = path.join('/packages', name); - - var fullpath = path.join(files.get_package_dir(), name, 'package.js'); + + var fullpath = path.join(self.source_root, 'package.js'); var code = fs.readFileSync(fullpath).toString(); // \n is necessary in case final line is a //-comment var wrapped = "(function(Package,require){" + code + "\n})"; @@ -128,7 +128,7 @@ _.extend(Package.prototype, { // stack -- has to come before user packages, because we don't // (presently) require packages to declare dependencies on // 'standard meteor stuff' like minimongo. - api.use(['deps', 'session', 'livedata', 'mongo-livedata', 'liveui', + api.use(['deps', 'session', 'livedata', 'mongo-livedata', 'spark', 'templating', 'startup', 'past']); api.use(require('./project.js').get_packages(app_dir)); @@ -266,12 +266,14 @@ var packages = module.exports = { // a package object. list: function () { var ret = {}; - var dir = files.get_package_dir(); - _.each(fs.readdirSync(dir), function (name) { - // skip .meteor directory - if (path.existsSync(path.join(dir, name, 'package.js'))) - ret[name] = packages.get(name); - }); + + _.each(files.get_package_dirs(), function(dir) { + _.each(fs.readdirSync(dir), function (name) { + // skip .meteor directory + if (path.existsSync(path.join(dir, name, 'package.js'))) + ret[name] = packages.get(name); + }); + }) return ret; }, diff --git a/app/lib/updater.js b/app/lib/updater.js index d4fda9083c..16aa957585 100644 --- a/app/lib/updater.js +++ b/app/lib/updater.js @@ -1,4 +1,4 @@ -exports.CURRENT_VERSION = "0.3.9"; +exports.CURRENT_VERSION = "0.4.0"; var fs = require("fs"); var http = require("http"); diff --git a/app/meteor/meteor.js b/app/meteor/meteor.js index c642fd0c58..00118ba12f 100644 --- a/app/meteor/meteor.js +++ b/app/meteor/meteor.js @@ -152,7 +152,7 @@ Commands.push({ var example_dir = path.join(__dirname, '../../examples'); var examples = _.reject(fs.readdirSync(example_dir), function (e) { - return (e === 'unfinished'); + return (e === 'unfinished' || e === 'other'); }); if (argv._.length === 1) { diff --git a/app/meteor/post-upgrade.js b/app/meteor/post-upgrade.js index d1755703ce..c43be76bde 100644 --- a/app/meteor/post-upgrade.js +++ b/app/meteor/post-upgrade.js @@ -2,7 +2,7 @@ try { // XXX can't get this from updater.js because in 0.3.7 and before the // updater didn't have the right NODE_PATH set. At some point we can // remove this and just use updater.CURRENT_VERSION. - var VERSION = "0.3.9"; + var VERSION = "0.4.0"; var fs = require('fs'); var path = require('path'); diff --git a/app/meteor/run.js b/app/meteor/run.js index ce48d3881a..1a5cd0135b 100644 --- a/app/meteor/run.js +++ b/app/meteor/run.js @@ -274,7 +274,7 @@ var DependencyWatcher = function (deps, app_dir, on_change) { self.specific_files = {}; for (var pkg in (deps.packages || {})) { _.each(deps.packages[pkg], function (file) { - self.specific_files[path.join(files.get_package_dir(), pkg, file)] + self.specific_files[path.join(files.get_package_dir(pkg), file)] = true; }); }; diff --git a/docs/client/api.html b/docs/client/api.html index 5217bfd632..656b3cda97 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -28,6 +28,7 @@ put on the screen. }); } +

Publish and subscribe

These functions control how Meteor servers publish sets of records and @@ -514,7 +515,7 @@ those changes may or may not appear in the result set. Cursors are a reactive data source. The first time you retrieve a cursor's documents with `fetch`, `map`, or `forEach` inside a -reactive context (eg, [`Meteor.ui.render`](#meteor_ui_render), +reactive context (eg, [`Meteor.render`](#meteor_render), [`Meteor.autosubscribe`](#meteor_autosubscribe), Meteor will register a dependency on the underlying data. Any change to the collection that changes the documents in a cursor will trigger a recomputation. To @@ -620,23 +621,6 @@ Example: // Delete all the log entries Logs.remove({}); - // Show a list of posts that have been flagged, updating in realtime. - // Put a link next to each post that deletes the post if clicked. - var frag = Meteor.ui.render(function() { - return Meteor.ui.listChunk( - Posts.find({flagged: true}), - function (post) { - // In real code it'd be necessary to sanitize post.name - return "
" + post.name + - " Delete
"; - }, - { events: { - 'click .delete': function () { - Posts.remove(this._id); - } - }}); - }); - document.body.appendChild(frag);

Cursors

@@ -672,7 +656,7 @@ the matching documents. // Display a count of posts matching certain criteria. Automatically // keep it updated as the database changes. - var frag = Meteor.ui.render(function () { + var frag = Meteor.render(function () { var high_scoring = Posts.find({score: {$gt: 10}}); return "

There are " + high_scoring.count() + " posts with " + "scores greater than 10

"; @@ -869,7 +853,7 @@ See [`Meteor.deps`](#meteor_deps) for another example. Example: Session.set("enemy", "Eastasia"); - var frag = Meteor.ui.render(function () { + var frag = Meteor.render(function () { return "

We've always been at war with " + Session.get("enemy") + "

"; }); @@ -916,11 +900,11 @@ Example: "selected" : ""; }; - Template.post_item.events = { + Template.post_item.events({ 'click': function() { Session.set("selected_post", this._id); } - }; + }); // Using Session.equals here means that when the user clicks // on an item and changes the selection, only the newly selected @@ -929,85 +913,260 @@ Example: // If Session.get had been used instead of Session.equals, then // when the selection changed, all the items would be re-rendered. -

Meteor.ui

+

Templates

-`Meteor.ui` provides building blocks for creating reactive UIs out of strings of -HTML, making it easy to create DOM elements that update -automatically as data changes in -[`Session`](#session) variables or in a -[`Meteor.Collection`](#meteor_collection). Meteor's built-in templates already use these functions, but if you prefer a different way of generating HTML, -are integrating a new template language with Meteor, or need to compose a reactive -snippet of HTML on the fly, then this package has what you need. +A template that you declare as `<{{! }}template name="foo"> ... ` can be accessed as the function `Template.foo`, which +returns a string of HTML when called. + +The same template may occur many times on the page, and these +occurrences are called template instances. Template instances have a +life cycle of being created, put into the document, and later taken +out of the document and destroyed. Meteor manages these stages for +you, including determining when a template instance has been removed +or replaced and should be cleaned up. You can associate data with a +template instance, and you can access its DOM nodes when it is in the +document. + +Additionally, Meteor will maintain a template instance and its state +even if its surrounding HTML is re-rendered into new DOM nodes. As +long as the structure of template invocations is the same, Meteor will +not consider any instances to have been created or destroyed. You can +request that the same DOM nodes be retained as well using `preserve` +and `constant`. + +There are a number of callbacks and directives that you can specify on +a named template and that apply to all instances of the template. +They are described below. + +{{> api_box template_call}} + +When called inside a template helper, the body of `Meteor.render`, or +other settings where reactive HTML is being generated, the resulting +HTML is annotated so that it renders as reactive DOM elements. +Otherwise, the HTML is unadorned and static. + + +{{> api_box template_rendered}} + +This callback is called once when an instance of Template.*myTemplate* is rendered into DOM nodes and put into the document for the first time, and again each time any part of the template is re-rendered. + +In the body of the callback, `this` is a [template +instance](#template_inst) object that is unique to this occurrence of +the template and persists across re-renderings. Use the `created` and +`destroyed` callbacks to perform initialization or clean-up on the +object. + +{{> api_box template_created}} + +This callback is called when an invocation of *myTemplate* represents +a new occurrence of the template and not a re-rendering of an existing +template instance. Inside the callback, `this` is the new [template +instance](#template_inst) object. Properties you set on this object +will be visible from the `rendered` and `destroyed` callbacks and from +event handlers. + +This callback fires once and is the first callback to fire. Every +`created` has a corresponding `destroyed`; that is, if you get a +`created` callback with a certain template instance object in `this`, +you will eventually get a `destroyed` callback for the same object. + +{{> api_box template_destroyed}} + +This callback is called when an occurrence of a template is taken off +the page for any reason and not replaced with a re-rendering. Inside +the callback, `this` is the [template instance](#template_inst) object +being destroyed. + +This callback is most useful for cleaning up or undoing any external +effects of `created`. It fires once and is the last callback to fire. + + +{{> api_box template_events}} + +Declare event handers for instances of this template. Multiple calls add +new event handlers in addition to the existing ones. + +See [Event Maps](#eventmaps) for a detailed description of the event +map format and how event handling works in Meteor. + +{{#note}} +This syntax replaces the previous syntax: `Template.myTemplate.events = {...}`, but for now, the +old syntax still works. +{{/note}} + + +{{> api_box template_helpers}} + +Each template has a local dictionary of helpers that are made available to it, +and this call specifies helpers to add to the template's dictionary. + +Example: + + Template.myTemplate.helpers({ + foo: function () { + return Session.get("foo"); + } + }); + +In Handlebars, this helper would then be invoked as `{{dstache}}foo}}`. + +The following syntax is equivalent, but won't work for reserved property +names: + + Template.myTemplate.foo = function () { + return Session.get("foo"); + }; + +{{> api_box template_preserve}} + +You can "preserve" a DOM element during re-rendering, leaving the +existing element in place in the document while replacing the +surrounding HTML. This means that re-rendering a template need not +disturb text fields, iframes, and other sensitive elements it +contains. The elements to preserve must be present both as nodes in +the old DOM and as tags in the new HTML. Meteor will patch the DOM +around the preserved elements. + +Preservation is useful in a variety of cases where replacing a DOM +element with an identical or modified element would not have the same +effect as retaining the original element. These include: + +* Input text fields and other form controls +* Elements with CSS animations +* Iframes +* Nodes with references kept in JavaScript code + +If you want to preserve a whole region of the DOM, an element and its +children, or nodes not rendered by Meteor, use a [constant +region](#constant) instead. + +To preserve nodes, pass a list of selectors each of which should match +at most one element in the template. When the template is re-rendered, +the selector is run on the old DOM and the new DOM, and Meteor will +reuse the old element in place while working in any HTML changes around +it. + +A second form of `preserve` takes a labeling function for each selector +and allows the selectors to match multiple nodes. The node-labeling +function takes a node and returns a label string that is unique for each +node, or `false` to exclude the node from preservation. + +For example, to preserve all `` elements with ids in template 'foo', use: + + Template.foo.preserve({ + 'input[id]': function (node) { return node.id; } + }); + +Selectors are interpreted as rooted at the top level of the template. +Each occurrence of the template operates independently, so the selectors +do not have to be unique on the entire page, only within one occurrence +of the template. Selectors will match nodes even if they are in +sub-templates. + +Preserving a node does *not* preserve its attributes or contents. They +will be updated to reflect the new HTML. Text in input fields is not +preserved unless the input field has focus, in which case the cursor and +selection are left intact. Iframes retain their navigation state and +animations continue to run as long as their parameters haven't changed. + +There are some cases where nodes can not be preserved because of +constraints inherent in the DOM API. For example, an element's tag name +can't be changed, and it can't be moved relative to its parent or other +preserved nodes. For this reason, nodes that are re-ordered or +re-parented by an update will not be preserved. + +{{#note}} +Previous versions of Meteor had an implicit page-wide `preserve` +directive that labeled nodes by their "id" and "name" attributes. +This has been removed in favor of the explicit, opt-in mechanism. +{{/note}} + + +

Template instances

+ +A template instance object represents an occurrence of a template in +the document. It can be used to access the DOM and it can be +assigned properties that persist across page re-renderings. + +Template instance objects are found as the value of `this` in the +`created`, `rendered`, and `destroyed` template callbacks and as an +argument to event handlers. + +In addition to the properties and functions described below, you can +assign additional properties of your choice to the object. Property names +starting with `_` are guaranteed to be available for your use. Use +the `created` and `destroyed` callbacks to perform initialization or +clean-up on the object. + +You can only access `findAll`, `find`, `firstNode`, and `lastNode` +from the `rendered` callback and event handlers, not from `created` +and `destroyed`, because they require the template instance to be +in the DOM. + +{{> api_box template_findAll}} + +Returns an array of DOM elements matching `selector`. + +The template instance serves as the document root for the selector. Only +elements inside the template and its sub-templates can match parts of +the selector. + +{{> api_box template_find}} + +Returns one DOM element matching `selector`, or `null` if there are no +such elements. + +The template instance serves as the document root for the selector. Only +elements inside the template and its sub-templates can match parts of +the selector. + +{{> api_box template_firstNode}} + +The two nodes `firstNode` and `lastNode` indicate the extent of the +rendered template in the DOM. The rendered template includes these +nodes, their intervening siblings, and their descendents. These two +nodes are siblings (they have the same parent), and `lastNode` comes +after `firstNode`, or else they are the same node. + +{{> api_box template_lastNode}} + +{{> api_box template_data}} + +This property provides access to the data context at the top level of +the template. It is updated each time the template is re-rendered. +Access is read-only and non-reactive. -This package is implemented on top of [`Meteor.deps`](#meteor_deps), which provides the -data dependency tracking and invalidation system, while `Meteor.ui` contributes -the ability to turn HTML into DOM elements, keep track of regions of the DOM that -need updating, and patch old DOM content with new, recalculated content. {{> api_box render}} -`Meteor.ui.render` creates a `DocumentFragment` (a sequence of DOM -nodes) that automatically updates in realtime. You pass in -`html_func`, a function that returns an HTML -string. `Meteor.ui.render` calls your function and turns the output -into DOM nodes. Meanwhile, it tracks the data that was used when -`html_func` ran, and automatically wires up callbacks so that whenever -any of the data changes, `html_func` is re-run and the DOM nodes -are updated in place. +`Meteor.render` creates a `DocumentFragment` (a sequence of DOM nodes) +that automatically updates in realtime. Most Meteor apps don't need to +call this directly, they use templates and Meteor handles the rendering. -Insert the returned `DocumentFragment` directly into the DOM wherever -you would like it to appear. The inserted nodes will continue to -update until they are taken off the screen. Then they will be -automatically cleaned up. For the details, see +Pass in `htmlFunc`, a function that returns an HTML +string. `Meteor.render` calls the function and turns the output into +DOM nodes. Meanwhile, it tracks the data that was used when `htmlFunc` +ran, and automatically wires up callbacks so that whenever any of the +data changes, `htmlFunc` is re-run and the DOM nodes are updated in +place. + +You may insert the returned `DocumentFragment` directly into the DOM +wherever you would like it to appear. The inserted nodes will continue +to update until they are taken off the screen. Then they will be +automatically cleaned up. For more details about clean-up, see [`Meteor.flush`](#meteor_flush). -You can also hook up events to the rendered DOM nodes using the -`events` option. If you provide `event_data`, it will be passed to -event handlers in `this`. (See [Event Maps](#eventmaps).) - -When render replaces DOM elements because data changed, it can leave -input elements undisturbed so that focus is preserved, text entered -into fields isn't lost, and so forth. To activate this feature, give -each such element a unique `id`, or give it a unique `name` attribute -inside the nearest enclosing element with an `id`. - -If you want a region of your HTML to be able to update independently -of the other HTML around it, wrap it in [`Meteor.ui.chunk`](#meteor_ui_chunk). - -`Meteor.ui.render` tracks the data dependencies of `html_func` by -running it in a reactive context, so it can respond to changes in any -reactive data sources used by that function. For more information, or -to learn how to make your own reactive data sources, see +`Meteor.render` tracks the data dependencies of `htmlFunc` by running +it in a reactive context, so it can respond to changes in any reactive +data sources used by that function. For more information, or to learn +how to make your own reactive data sources, see [Reactivity](#reactivity). -{{! -Meteor.ui.render runs html_func in a reactive context, then returns a -DocumentFragment that can be inserted anywhere in a Document and that -will automatically update itself in place whenever the context is -invalidated. The updating will stop if the nodes in the fragment are -ever not on the screen (that is, children of `window.document`) when -Meteor.flush runs. - -During an update, if a node has a unique id, or if it has a name that -is unique among the descendants of the nearest enclosing parent that -has an id, then it will be "patched" (updated in place, rather than -replaced), meaning that focus will be preserved, the text in -elements will be not be lost, etc. - - -By default, Meteor.ui.render puts the entire output of `html_func` in -a single invalidation context. For finer control of rerendering, you -can use Meteor.ui.chunk to create a nested tree of invalidation -contexts. -[events] -[more?] -}} - - Example: // Show the number of users online. - var frag = Meteor.ui.render(function () { + var frag = Meteor.render(function () { return "

There are " + Users.find({online: true}).count() + " users online.

"; }); @@ -1017,146 +1176,47 @@ Example: // offline. The count on the screen will automatically update. Users.update({idleTime: {$gt: 30}}, {online: false}); - // Show a counter, and let the user click to increase or decrease it. - Session.set("counter", 0); +{{> api_box renderList}} - var frag = Meteor.ui.render(function () { - return '
Counter: ' + Session.get("counter") + ' ' + - 'Increase' + - 'Decrease
'; - }, { events: - { - 'click .inc': function (event) { - Session.set("counter", Session.get("counter") + 1); - }, - 'click .dec': function (event) { - Session.set("counter", Session.get("counter") - 1); - } - } - }); - document.body.appendChild(frag); +Creates a `DocumentFragment` that automatically updates as the results +of a database query change. Most Meteor apps use `{{dstache}}#each}}` in +a template instead of calling this directly. -{{> api_box chunk}} - -When generating HTML from a function passed to [`Meteor.ui.render`](#meteor_ui_render), you can use `Meteor.ui.chunk` to mark a substring of the HTML as separately reactive. If the data used to generate that substring changes, only the elements corresponding to that substring will be updated, not the elements before and after it. - -Like `render`, `Meteor.ui.chunk` takes a function `html_func` that returns a HTML string. It calls that function, records the data that the function used (using a [reactive context](#meteor_ui_deps)), and arranges to rerun the function as necessary whenever the data changes. What's different from `render` is that it returns another HTML string, not a DocumentFragment. So, unlike `render`, `chunk` may be nested as deeply as you like, for example to render nested views or subtemplates. - -`chunk` can also be used to attach events to part of an HTML string, in much the same way that they could be attached to DOM elements. When the string is parsed into DOM elements by `render`, the event handlers will automatically be hooked up. If `event_data` is provided, it sets the event data for _all_ events that occur that within the chunk, even when those events are handled by handlers declared in enclosing chunks. - -{{#note}} -Note: In Internet Explorer 8 and -earlier, if you manually add elements to a chunk after it has been -rendered — for example, using jQuery or the DOM API — then handlers -in event maps may not fire on these elements. This is a limitation of the implementation. -{{/note}} - -`chunk` works by creating a unique ID for the chunk, wrapping the HTML string in a comment that calls out that ID, and adding an entry to the chunk table for the current invocation of `render`. As `render` turns the HTML string into DOM nodes, it pulls out the comments and wires up the appropriate callbacks and pointers. On the other hand, if there is no current invocation of `render`, `chunk` just passes the string through unchanged. (In this case, if an event map is provided, it is ignored.) - -The contents of a chunk must be balanced HTML tags; the string returned by -`html_func` cannot start or end inside a tag or tag attribute. - -Example: - - Meteor.startup(function() { - - Session.set("greeting", "Hello"); - Session.set("target", "World"); - - // Render two chunks that will be tracked and updated independently. - document.body.appendChild( - Meteor.ui.render(function() { - return "
" + - Meteor.ui.chunk(function() { return Session.get("greeting"); }) + - " " + - Meteor.ui.chunk(function() { return Session.get("target"); }) + - "
"; - })); - - // Updates "Hello" to "Goodbye" without touching "World" - Session.set("greeting", "Goodbye"); - // Updates "World" to "Ralph" without touching "Goodbye" - Session.set("target", "Ralph"); - - }); - - // Every two seconds, alternates between Goodbye Ralph and Goodbye World. - // If you select the word Goodbye or part of it, the selection stays on - // update, because the text node is not being replaced. - Meteor.setInterval(function() { - if (Session.get("target") === "Ralph") { - Session.set("target", "World"); - } else { - Session.set("target", "Ralph"); - } - }, 2000); - - -{{> api_box listChunk}} - -`listChunk` is like `chunk`, but instead of creating one chunk, it creates several, one for each record in the results of a database query. - -It keeps the chunks updated as the results of the database query change. For example, if a new record is created in the database that matches the query, a new chunk is inserted. If the query is sorted, and a database record changes, and the change causes it to move in the sort order, then the chunk is moved appropriately. - -If you provide `else_func`, then whenever the query returns no results, it will be called to render alternative content. You might use this to show a message like "No records match your query." - -You can provide an `events` option to attach a set of event handlers to each chunk that `listChunk` creates. `event_data` is set to the corresponding database record in each chunk, meaning that when any event fires within a chunk, `this` in the event handler will contain the appropriate database record. +`renderList` is more efficient than using `Meteor.render` to render HTML +for a list of documents. For example, if a new document is created in +the database that matches the query, a new item will be rendered and +inserted at the appropriate place in the DOM without re-rendering the +other elements. Similarly, if a document changes position in a sorted +query, the DOM nodes will simply be moved and not re-rendered. +`docFunc` is called as needed to generate HTML for each document. If +you provide `elseFunc`, then whenever the query returns no results, it +will be called to render alternate content. You might use this to show +a message like "No records match your query." +Each call to `docFunc` or `elseFunc` is run in its own reactive +context so that if it has other external data dependencies, it will be +individually re-run when the data changes. Example: // List the titles of all of the posts that have the tag // "frontpage". Keep the list updated as new posts are made, as tags - // change, etc. Let the user click a post to select it. - Session.set("selected", null); - var frag = Meteor.ui.render(function() { - return Meteor.ui.listChunk(Posts.find({tags: "frontpage"}), - function(post) { - var style = Session.equals("selected", post._id) ? "selected" : ""; - // A real app would need to quote/sanitize post.name - return '
' + post.name + '
'; - }, - { events: - { - 'click': function (event) { - Session.set("selected", this._id); - } - } - }); - }); + // change, etc. Display the selected post differently. + var frag = Meteor.renderList( + Posts.find({tags: "frontpage"}), + function(post) { + var style = Session.equals("selectedId", post._id) ? "selected" : ""; + // A real app would need to quote/sanitize post.name + return '
' + post.name + '
'; + }); document.body.appendChild(frag); -{{> api_box flush }} + // Select a post. This will cause only the selected item and the + // previously selected item to update. + var somePost = Posts.findOne({tags: "frontpage"}); + Session.set("selectedId", somePost._id); -Normally, when you make changes (like writing to the database), -their impact (like updating the DOM) is delayed until the system is -idle. This keeps things predictable — you can know that the DOM -won't go changing out from under your code as it runs. It's also one -of the things that makes Meteor fast. - -`Meteor.flush` forces all of the pending reactive updates to complete -(for example, it ensures the DOM has been updated with your recent -database changes.) Call `flush` to apply those pending changes -immediately. The main use for this is to make sure the DOM has been -brought up to date with your latest changes, so you can manually -manipulate it with jQuery or the like. - -When you call `flush`, any auto-updating DOM elements that are not on -the screen may be cleaned up (meaning that Meteor will stop tracking -and updating the elements, so that the browser's garbage collector can -delete them.) So, if you manually call `flush`, you need to make sure -that any auto-updating elements that you have created by calling -[`Meteor.ui.render`](#meteor_ui_render) have already been inserted in the main -DOM tree. - -Technically speaking, `flush` calls the [invalidation -callbacks](#on_invalidate) on every [reactive context](#context) that -has been [invalidated](#invalidate), but hasn't yet has its callbacks -called. If the invalidation callbacks invalidate still more contexts, -flush keeps flushing until everything is totally settled. The DOM -elements are cleaned up because of logic in -[`Meteor.ui.render`](#meteor_ui_render) that works through invalidations. {{#api_box_inline eventmaps}} @@ -1180,14 +1240,14 @@ To handle more than one type of event with the same function, use a comma-separa {{/dtdd}} -The handler function gets one argument, an object with information -about the event. It will receive some additional context data in -`this`, depending on the context of the current element handling the event. -In a Handlebars template, -an element's context is the Handlebars data context where that element -occurs, which is set by block helpers such as `#with` -and `#each`. When using [`Meteor.ui.chunk`](#meteor_ui_chunk), the -data context is set using the `event_data` option. +The handler function receives two arguments: `event`, an object with +information about the event, and `template`, a [template +instance](#template_inst) for the template where the handler is +defined. The handler also receives some additional context data in +`this`, depending on the context of the current element handling the +event. In a Handlebars template, an element's context is the +Handlebars data context where that element occurs, which is set by +block helpers such as `#with` and `#each`. Example: @@ -1325,6 +1385,61 @@ browsers. {{/api_box_inline}} + + +{{#api_box_inline constant}} + +You can mark a region of a template as "constant" and not subject to +re-rendering using the +`{{dstache}}#constant}}...{{dstache}}/constant}}` block helper. +Content inside the `#constant` block helper is preserved exactly as-is +even if the enclosing template is re-rendered. Changes to other parts +of the template are patched in around it in the same manner as +`preserve`. Unlike individual node preservation, a constant region +retains not only the identities of its nodes but also their attributes +and contents. The contents of the block will only be evaluated once +per occurrence of the enclosing template. + +Constant regions allow non-Meteor content to be embedded in a Meteor +template. Many third-party widgets create and manage their own DOM +nodes programmatically. Typically, you put an empty element in your +template, which the widget or library will then populate with +children. Normally, when Meteor re-renders the enclosing template it +would remove the new children, since the template says it should be +empty. If the container is wrapped in a `#constant` block, however, it +is left alone; whatever content is currently in the DOM remains. + +{{#note}} +Constant regions are intended for embedding non-Meteor content. +Event handlers and reactive dependencies don't currently work +correctly inside constant regions. +{{/note}} + + +{{/api_box_inline}} + +{{#api_box_inline isolate}} + +Each template runs in its own reactive context. When the template +accesses a reactive data source, such as by calling `Session.get` or +making a database query, this establishes a data dependency that will +cause the whole template to be re-rendered when the data changes. +This means that the amount of re-rendering for a particular change +is affected by how you've divided your HTML into templates. + +Typically, the exact extent of re-rendering is not crucial, but if you +want more control, such as for performance reasons, you can use the +`{{dstache}}#isolate}}...{{dstache}}/isolate}}` helper. Data +dependencies established inside an `#isolate` block are localized to +the block and will not in themselves cause the parent template to be +re-rendered. This block helper essentially conveys the reactivity +benefits you would get by pulling the content out into a new +sub-template. + +{{/api_box_inline}} + + +

Timers

Meteor uses global environment variables @@ -1513,6 +1628,40 @@ just means that [`run`](#run) sets it, runs some user-supplied code, and then restores its previous value.) +{{> api_box flush }} + +Normally, when you make changes (like writing to the database), +their impact (like updating the DOM) is delayed until the system is +idle. This keeps things predictable — you can know that the DOM +won't go changing out from under your code as it runs. It's also one +of the things that makes Meteor fast. + +`Meteor.flush` forces all of the pending reactive updates to complete +(for example, it ensures the DOM has been updated with your recent +database changes.) Call `flush` to apply those pending changes +immediately. The main use for this is to make sure the DOM has been +brought up to date with your latest changes, so you can manually +manipulate it with jQuery or the like. + +When you call `flush`, any auto-updating DOM elements that are not on +the screen may be cleaned up (meaning that Meteor will stop tracking +and updating the elements, so that the browser's garbage collector can +delete them.) So, if you manually call `flush`, you need to make sure +that any auto-updating elements that you have created by calling +[`Meteor.render`](#meteor_render) have already been inserted in the main +DOM tree. + +Technically speaking, `flush` calls the [invalidation +callbacks](#on_invalidate) on every [reactive context](#context) that +has been [invalidated](#invalidate), but hasn't yet has its callbacks +called. If the invalidation callbacks invalidate still more contexts, +flush keeps flushing until everything is totally settled. The DOM +elements are cleaned up by logic that is triggered by context invalidations. + + + + +

Meteor.http

`Meteor.http` provides an HTTP API on the client and server. To use diff --git a/docs/client/api.js b/docs/client/api.js index 44dbbd7ed8..2380a72584 100644 --- a/docs/client/api.js +++ b/docs/client/api.js @@ -481,13 +481,6 @@ Template.api.Context = { descr: ["Create an invalidation context. Invalidation contexts are used to run a piece of code, and record its dependencies so it can be rerun later if one of its inputs changes.", "An invalidation context is basically just a list of callbacks for an event that can fire only once. The [`on_invalidate`](#on_invalidate) method adds a callback to the list, and the [`invalidate`](#invalidate) method fires the event."] }; -Template.api.current = { - id: "current", - name: "Meteor.deps.Context.current", - locus: "Client", - descr: ["The current [`invalidation context`](#context), or `null` if not being called from inside [`run`](#run)."] -}; - Template.api.run = { id: "run", name: "context.run(func)", @@ -519,77 +512,11 @@ Template.api.invalidate = { descr: ["Add this context to the list of contexts that will have their `on_invalidate|on_invalidate` callbacks called by the next call to [`Meteor.flush`](#meteor_flush)."] }; - -// writeFence -// invalidationCrossbar - -Template.api.render = { - id: "meteor_ui_render", - name: "Meteor.ui.render(html_func, [options])", +Template.api.current = { + id: "current", + name: "Meteor.deps.Context.current", locus: "Client", - descr: ["Create DOM nodes that automatically update themselves as data changes."], - args: [ - {name: "html_func", - type: "Function returning a string of HTML", - descr: "Render function to be called, initially and whenever data changes"} - ], - options: [ - {name: "events", - type: "Object — event map", - type_link: "eventmaps", - descr: "Events to hook up to the rendered elements"}, - {name: "event_data", - type: "Any value", - descr: "Value to bind to `this` in event handlers" - } - ] -}; - -Template.api.chunk = { - id: "meteor_ui_chunk", - name: "Meteor.ui.chunk(html_func, [options])", - locus: "Client", - descr: ["Inside [`Meteor.ui.render`](#meteor_ui_render), give special behavior to a range of HTML."], - args: [ - {name: "html_func", - type: "Function returning a string of HTML", - descr: "Render function to be called, initially and whenever data changes"} - ], - options: [ - {name: "events", - type: "Object — event map", - type_link: "eventmaps", - descr: "Events to hook up to the rendered elements"}, - {name: "event_data", - type: "Any value", - descr: "Value to bind to `this` in event handlers" - } - ] -}; - -Template.api.listChunk = { - id: "meteor_ui_listchunk", - name: "Meteor.ui.listChunk(observable, doc_func, [else_func], [options])", - locus: "Client", - descr: ["Observe a database query and create annotated HTML that will be reactively updated when rendered with [`Meteor.ui.render`](#meteor_ui_render)."], - args: [ - {name: "observable", - type: "Cursor", - type_link: "meteor_collection_cursor", - descr: "Query cursor to observe, as a reactive source of ordered documents"}, - {name: "doc_func", - type: "Function taking a document and returning HTML", - descr: "Render function to be called for each document"}, - {name: "else_func", - type: "Function returning HTML", - descr: "Render function to be called when query is empty"} - ], - options: [ - {name: "events", - type: "Object — event map", - type_link: "eventmaps", - descr: "Events to hook up to the rendered elements"} - ] + descr: ["The current [`invalidation context`](#context), or `null` if not being called from inside [`run`](#run)."] }; Template.api.flush = { @@ -599,11 +526,59 @@ Template.api.flush = { descr: ["Ensure that any reactive updates have finished. Allow auto-updating DOM element to be cleaned up if they are offscreen."] }; + +// writeFence +// invalidationCrossbar + +Template.api.render = { + id: "meteor_render", + name: "Meteor.render(htmlFunc)", + locus: "Client", + descr: ["Create DOM nodes that automatically update themselves as data changes."], + args: [ + {name: "htmlFunc", + type: "Function returning a string of HTML", + descr: "Function that generates HTML to be rendered. Called immediately and re-run whenever data changes. May also be a string of HTML instead of a function."} + ] +}; + +Template.api.renderList = { + id: "meteor_renderlist", + name: "Meteor.renderList(observable, docFunc, [elseFunc])", + locus: "Client", + descr: ["Create DOM nodes that automatically update themselves based on the results of a database query."], + args: [ + {name: "observable", + type: "Cursor", + type_link: "meteor_collection_cursor", + descr: "Query cursor to observe as a reactive source of ordered documents."}, + {name: "docFunc", + type: "Function taking a document and returning HTML", + descr: "Render function to be called for each document."}, + {name: "elseFunc", + type: "Function returning HTML", + descr: "Optional. Render function to be called when query is empty."} + ] +}; + + Template.api.eventmaps = { id: "eventmaps", name: "Event Maps" }; +Template.api.constant = { + id: "constant", + name: "Constant regions" +}; + +Template.api.isolate = { + id: "isolate", + name: "Reactivity isolation" +}; + + + Template.api.setTimeout = { id: "meteor_settimeout", name: "Meteor.setTimeout", @@ -835,3 +810,118 @@ Template.api.http_del = { }; +// XXX move these up to right place +Template.api.template_call = { + id: "template_call", + name: "Template.myTemplate([data])", + locus: "Client", + descr: ["Call a template function by name to produce HTML."], + args: [ + {name: "data", + type: "Object", + descr: 'Optional. The data context object with which to call the template.'} + ] +}; + +Template.api.template_rendered = { + id: "template_rendered", + name: "Template.myTemplate.rendered = function ( ) { ... }", + locus: "Client", + descr: ["Provide a callback when an instance of a template is rendered."] +}; + +Template.api.template_created = { + id: "template_created", + name: "Template.myTemplate.created = function ( ) { ... }", + locus: "Client", + descr: ["Provide a callback when an instance of a template is created."] +}; + +Template.api.template_destroyed = { + id: "template_destroyed", + name: "Template.myTemplate.destroyed = function ( ) { ... }", + locus: "Client", + descr: ["Provide a callback when an instance of a template is destroyed."] +}; + +Template.api.template_events = { + id: "template_events", + name: "Template.myTemplate.events(eventMap)", + locus: "Client", + descr: ["Specify event handlers for this template."], + args: [ + {name: "eventMap", + type: "Object: event map", + type_link: "eventmaps", + descr: "Event handlers to associate with this template."} + ] +}; + +Template.api.template_helpers = { + id: "template_helpers", + name: "Template.myTemplate.helpers(helpers)", + locus: "Client", + descr: ["Specify template helpers available to this template."], + args: [ + {name: "helpers", + type: "Object", + descr: "Dictionary of helper functions by name."} + ] +}; + +Template.api.template_preserve = { + id: "template_preserve", + name: "Template.myTemplate.preserve(selectors)", + locus: "Client", + descr: ["Specify rules for preserving individual DOM elements on re-render."], + args: [ + {name: "selectors", + type: "Array or Object", + descr: "Array of selectors that each match at most one element, such as `['.thing1', '.thing2']`, or, alternatively, a dictionary of selectors and node-labeling functions (see below)."} + ] +}; + +Template.api.template_findAll = { + id: "template_findAll", + name: "this.findAll(selector)", + locus: "Client", + descr: ["Find all elements matching `selector` in this template instance."], + args: [ + {name: "selector", + type: "String", + descr: 'The CSS selector to match, scoped to the template contents.'} + ] +}; + +Template.api.template_find = { + id: "template_find", + name: "this.find(selector)", + locus: "Client", + descr: ["Find one element matching `selector` in this template instance."], + args: [ + {name: "selector", + type: "String", + descr: 'The CSS selector to match, scoped to the template contents.'} + ] +}; + +Template.api.template_firstNode = { + id: "template_firstNode", + name: "this.firstNode", + locus: "Client", + descr: ["The first top-level DOM node in this template instance."] +}; + +Template.api.template_lastNode = { + id: "template_lastNode", + name: "this.lastNode", + locus: "Client", + descr: ["The last top-level DOM node in this template instance."] +}; + +Template.api.template_data = { + id: "template_data", + name: "this.data", + locus: "Client", + descr: ["The data context of this instance's latest invocation."] +}; diff --git a/docs/client/concepts.html b/docs/client/concepts.html index 0d3a159e44..906389c987 100644 --- a/docs/client/concepts.html +++ b/docs/client/concepts.html @@ -189,7 +189,7 @@ application with error-prone logic. These Meteor functions run your code in a reactive context: -* [`Meteor.ui.render`](#meteor_ui_render) and [`Meteor.ui.chunk`](#meteor_ui_chunk) +* [`Meteor.render`](#meteor_render) and [`Meteor.renderList`](#meteor_renderlist) * [`Meteor.autosubscribe`](#meteor_autosubscribe) * [Templates](#templates) @@ -220,7 +220,7 @@ generate it. This optional feature works with any HTML templating library, or even with HTML you generate manually from Javascript. Here's an example: - var fragment = Meteor.ui.render( + var fragment = Meteor.render( function () { var name = Session.get("name") || "Anonymous"; return "
Hello, " + name + "
"; @@ -229,25 +229,18 @@ with HTML you generate manually from Javascript. Here's an example: Session.set("name", "Bob"); // page updates automatically! -[`Meteor.ui.render`](#meteor_ui_render) takes a rendering function, -that is, a function that returns some HTML as a string. It returns an -auto-updating `DocumentFragment`. When there is a change to data used -by the rendering function, it is re-run. The DOM nodes in the -`DocumentFragment` then update themselves in-place, no matter where -they were inserted on the page. - -It's completely automatic. [`Meteor.ui.render`](#meteor_ui_render) -uses [reactive contexts](#reactivity) to discover what data is used by -the rendering function. From within -[`Meteor.ui.render`](#meteor_ui_render), you can use -[`Meteor.ui.chunk`](#meteor_ui_chunk) to customize how much of the -HTML is rerendered on a data change, or -[`Meteor.ui.listChunk`](#meteor_ui_listchunk) to efficiently track a -live database query. +[`Meteor.render`](#meteor_render) takes a rendering function, that is, a +function that returns some HTML as a string. It returns an auto-updating +`DocumentFragment`. When there is a change to data used by the rendering +function, it is re-run. The DOM nodes in the `DocumentFragment` then +update themselves in-place, no matter where they were inserted on the +page. It's completely automatic. [`Meteor.render`](#meteor_render) uses +[reactive contexts](#reactivity) to discover what data is used by the +rendering function. Most of the time, though, you won't call these functions directly — you'll just use your favorite templating package, such as -Handlebars or Jade. The `render` and `chunk` functions are intended +Handlebars or Jade. The `render` and `renderList` functions are intended for people that are implementing new templating systems. Meteor normally batches up any needed updates and executes them only @@ -276,12 +269,12 @@ redrawn. The user could be in for a bumpy ride, as the focus, the cursor position, the partially entered text, and the accented character input state will be lost when the `` is recreated. -This is another problem that Meteor solves automatically. Just make -sure that each of your focusable elements either has a unique `id`, or -has a `name` that is unique within the closest parent that has an -`id`. Meteor will preserve these elements even when their enclosing -template is rerendered, but will still update their children and copy -over any attribute changes. +This is another problem that Meteor solves for you. You can specify +elements to preserve when templates are re-rendered with the +[`preserve`](#template_preserve) directive on the template. Meteor will +preserve these elements even when their enclosing template is +rerendered, but will still update their children and copy over any +attribute changes. {{/better_markdown}} @@ -325,9 +318,9 @@ function `Template.hello`, passing any data for the template: This returns a string. To use the template along with the [`Live HTML`](#livehtml) system, and get DOM elements that update -automatically in place, use [`Meteor.ui.render`](#meteor_ui_render): +automatically in place, use [`Meteor.render`](#meteor_render): - Meteor.ui.render(function () { + Meteor.render(function () { return Template.hello({first: "Alyssa", last: "Hacker"}); }) => automatically updating DOM elements @@ -387,7 +380,7 @@ Helpers can also be used to pass in constant data. // Works fine with {{dstache}}#each sections}} Template.report.sections = ["Situation", "Complication", "Resolution"]; -Finally, you can set the `events` property of a template function to a +Finally, you can use an `events` declaration on a template function to set up a table of event handlers. The format is documented at [Event Maps](#eventmaps). The `this` argument to the event handler will be the data context of the element that triggered the event. @@ -406,11 +399,11 @@ the data context of the element that triggered the event. - Template.player_score.events = { + Template.player_score.events({ 'click .give_points': function () { Users.update({_id: this._id}, {$inc: {score: 2}}); } - }; + }); Putting it all together, here's an example of how you can inject arbitrary data into your templates, and have them update automatically @@ -430,7 +423,7 @@ discussion. > Session.set("weather", "cloudy"); - > document.body.appendChild(Meteor.ui.render(Template.forecast)); + > document.body.appendChild(Meteor.render(Template.forecast)); In DOM:
It'll be cloudy tonight
> Session.set("weather", "cool and dry"); diff --git a/docs/client/docs.html b/docs/client/docs.html index 7ac14a97f5..9306792c71 100644 --- a/docs/client/docs.html +++ b/docs/client/docs.html @@ -11,7 +11,7 @@
-

Meteor 0.3.9

+

Meteor 0.4.0

{{> introduction }} {{> concepts }} {{> api }} @@ -28,7 +28,7 @@ {{/if}} {{#if type "section"}} - {{#if instance}}{{instance}}.{{/if}}{{name}} + {{#if prefix}}{{prefix}}.{{/if}}{{#if instance}}{{instance}}.{{/if}}{{name}} {{/if}} {{/each}} diff --git a/docs/client/docs.js b/docs/client/docs.js index c7dc28497e..b084f37933 100644 --- a/docs/client/docs.js +++ b/docs/client/docs.js @@ -1,4 +1,4 @@ -METEOR_VERSION = "0.3.9"; +METEOR_VERSION = "0.4.0"; Meteor.startup(function () { // XXX this is broken by the new multi-page layout. Also, it was @@ -143,14 +143,29 @@ var toc = [ "Session.equals" ], - "Meteor.ui", [ - "Meteor.ui.render", - "Meteor.ui.chunk", - "Meteor.ui.listChunk", - "Meteor.flush", + {name: "Templates", id: "templates_api"}, [ + {prefix: "Template", instance: "myTemplate", id: "template_call"}, [ + {name: "rendered", id: "template_rendered"}, + {name: "created", id: "template_created"}, + {name: "destroyed", id: "template_destroyed"}, + {name: "events", id: "template_events"}, + {name: "helpers", id: "template_helpers"}, + {name: "preserve", id: "template_preserve"} + ], + {name: "Template instances", id: "template_inst"}, [ + {instance: "this", name: "findAll", id: "template_findAll"}, + {instance: "this", name: "find", id: "template_find"}, + {instance: "this", name: "firstNode", id: "template_firstNode"}, + {instance: "this", name: "lastNode", id: "template_lastNode"}, + {instance: "this", name: "data", id: "template_data"} + ], + "Meteor.render", + "Meteor.renderList", {type: "spacer"}, - {name: "Event maps", style: "noncode"} - ], + {name: "Event maps", style: "noncode"}, + {name: "Constant regions", style: "noncode", id: "constant"}, + {name: "Reactivity isolation", style: "noncode", id: "isolate"} + ], "Timers", [ "Meteor.setTimeout", @@ -165,7 +180,8 @@ var toc = [ {instance: "context", name: "on_invalidate"}, {instance: "context", name: "invalidate"} ], - {name: "Meteor.deps.Context.current", id: "current"} + {name: "Meteor.deps.Context.current", id: "current"}, + "Meteor.flush" // ], // "Environment Variables", [ @@ -321,6 +337,13 @@ Handlebars.registerHelper('better_markdown', function(fn) { return result; }; + // This is a tower of terrible hacks. + // Replace Spark annotations <$...> ... with HTML comments, and + // space out the comments on their own lines. This keeps them from + // interfering with Markdown's paragraph parsing. + // Really, running Markdown multiple times on the same string is just a + // bad idea. + input = input.replace(/<(\/?\$.*?)>/g, ''); input = input.replace(//g, '\n\n$&\n\n'); var hashedBlocks = {}; @@ -370,6 +393,8 @@ Handlebars.registerHelper('better_markdown', function(fn) { return hashedBlocks[a]; }); + output = output.replace(//g, '<$1>'); + return output; }); diff --git a/docs/client/introduction.html b/docs/client/introduction.html index 94d8fb4ac7..f9a1f52273 100644 --- a/docs/client/introduction.html +++ b/docs/client/introduction.html @@ -39,6 +39,8 @@ our thinking. We'd love to hear your feedback. +The following works on all supported platforms. + Install Meteor:
diff --git a/examples/leaderboard/leaderboard.js b/examples/leaderboard/leaderboard.js
index 4860ea44bf..3554300f59 100644
--- a/examples/leaderboard/leaderboard.js
+++ b/examples/leaderboard/leaderboard.js
@@ -17,17 +17,17 @@ if (Meteor.is_client) {
     return Session.equals("selected_player", this._id) ? "selected" : '';
   };
 
-  Template.leaderboard.events = {
+  Template.leaderboard.events({
     'click input.inc': function () {
       Players.update(Session.get("selected_player"), {$inc: {score: 5}});
     }
-  };
+  });
 
-  Template.player.events = {
+  Template.player.events({
     'click': function () {
       Session.set("selected_player", this._id);
     }
-  };
+  });
 }
 
 // On server startup, create some players if the database is empty.
diff --git a/examples/other/template-demo/.meteor/.gitignore b/examples/other/template-demo/.meteor/.gitignore
new file mode 100644
index 0000000000..4083037423
--- /dev/null
+++ b/examples/other/template-demo/.meteor/.gitignore
@@ -0,0 +1 @@
+local
diff --git a/examples/other/template-demo/.meteor/packages b/examples/other/template-demo/.meteor/packages
new file mode 100644
index 0000000000..12c5f051c0
--- /dev/null
+++ b/examples/other/template-demo/.meteor/packages
@@ -0,0 +1,6 @@
+# Meteor packages used by this project, one per line.
+#
+# 'meteor add' and 'meteor remove' will edit this file for you,
+# but you can also edit it by hand.
+
+autopublish
diff --git a/examples/other/template-demo/client/d3.v2.js b/examples/other/template-demo/client/d3.v2.js
new file mode 100644
index 0000000000..6216978cce
--- /dev/null
+++ b/examples/other/template-demo/client/d3.v2.js
@@ -0,0 +1,7034 @@
+(function() {
+  if (!Date.now) Date.now = function() {
+    return +(new Date);
+  };
+  try {
+    document.createElement("div").style.setProperty("opacity", 0, "");
+  } catch (error) {
+    var d3_style_prototype = CSSStyleDeclaration.prototype, d3_style_setProperty = d3_style_prototype.setProperty;
+    d3_style_prototype.setProperty = function(name, value, priority) {
+      d3_style_setProperty.call(this, name, value + "", priority);
+    };
+  }
+  d3 = {
+    version: "2.10.0"
+  };
+  function d3_class(ctor, properties) {
+    try {
+      for (var key in properties) {
+        Object.defineProperty(ctor.prototype, key, {
+          value: properties[key],
+          enumerable: false
+        });
+      }
+    } catch (e) {
+      ctor.prototype = properties;
+    }
+  }
+  var d3_array = d3_arraySlice;
+  function d3_arrayCopy(pseudoarray) {
+    var i = -1, n = pseudoarray.length, array = [];
+    while (++i < n) array.push(pseudoarray[i]);
+    return array;
+  }
+  function d3_arraySlice(pseudoarray) {
+    return Array.prototype.slice.call(pseudoarray);
+  }
+  try {
+    d3_array(document.documentElement.childNodes)[0].nodeType;
+  } catch (e) {
+    d3_array = d3_arrayCopy;
+  }
+  var d3_arraySubclass = [].__proto__ ? function(array, prototype) {
+    array.__proto__ = prototype;
+  } : function(array, prototype) {
+    for (var property in prototype) array[property] = prototype[property];
+  };
+  d3.map = function(object) {
+    var map = new d3_Map;
+    for (var key in object) map.set(key, object[key]);
+    return map;
+  };
+  function d3_Map() {}
+  d3_class(d3_Map, {
+    has: function(key) {
+      return d3_map_prefix + key in this;
+    },
+    get: function(key) {
+      return this[d3_map_prefix + key];
+    },
+    set: function(key, value) {
+      return this[d3_map_prefix + key] = value;
+    },
+    remove: function(key) {
+      key = d3_map_prefix + key;
+      return key in this && delete this[key];
+    },
+    keys: function() {
+      var keys = [];
+      this.forEach(function(key) {
+        keys.push(key);
+      });
+      return keys;
+    },
+    values: function() {
+      var values = [];
+      this.forEach(function(key, value) {
+        values.push(value);
+      });
+      return values;
+    },
+    entries: function() {
+      var entries = [];
+      this.forEach(function(key, value) {
+        entries.push({
+          key: key,
+          value: value
+        });
+      });
+      return entries;
+    },
+    forEach: function(f) {
+      for (var key in this) {
+        if (key.charCodeAt(0) === d3_map_prefixCode) {
+          f.call(this, key.substring(1), this[key]);
+        }
+      }
+    }
+  });
+  var d3_map_prefix = "\0", d3_map_prefixCode = d3_map_prefix.charCodeAt(0);
+  function d3_identity(d) {
+    return d;
+  }
+  function d3_this() {
+    return this;
+  }
+  function d3_true() {
+    return true;
+  }
+  function d3_functor(v) {
+    return typeof v === "function" ? v : function() {
+      return v;
+    };
+  }
+  d3.functor = d3_functor;
+  d3.rebind = function(target, source) {
+    var i = 1, n = arguments.length, method;
+    while (++i < n) target[method = arguments[i]] = d3_rebind(target, source, source[method]);
+    return target;
+  };
+  function d3_rebind(target, source, method) {
+    return function() {
+      var value = method.apply(source, arguments);
+      return arguments.length ? target : value;
+    };
+  }
+  d3.ascending = function(a, b) {
+    return a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN;
+  };
+  d3.descending = function(a, b) {
+    return b < a ? -1 : b > a ? 1 : b >= a ? 0 : NaN;
+  };
+  d3.mean = function(array, f) {
+    var n = array.length, a, m = 0, i = -1, j = 0;
+    if (arguments.length === 1) {
+      while (++i < n) if (d3_number(a = array[i])) m += (a - m) / ++j;
+    } else {
+      while (++i < n) if (d3_number(a = f.call(array, array[i], i))) m += (a - m) / ++j;
+    }
+    return j ? m : undefined;
+  };
+  d3.median = function(array, f) {
+    if (arguments.length > 1) array = array.map(f);
+    array = array.filter(d3_number);
+    return array.length ? d3.quantile(array.sort(d3.ascending), .5) : undefined;
+  };
+  d3.min = function(array, f) {
+    var i = -1, n = array.length, a, b;
+    if (arguments.length === 1) {
+      while (++i < n && ((a = array[i]) == null || a != a)) a = undefined;
+      while (++i < n) if ((b = array[i]) != null && a > b) a = b;
+    } else {
+      while (++i < n && ((a = f.call(array, array[i], i)) == null || a != a)) a = undefined;
+      while (++i < n) if ((b = f.call(array, array[i], i)) != null && a > b) a = b;
+    }
+    return a;
+  };
+  d3.max = function(array, f) {
+    var i = -1, n = array.length, a, b;
+    if (arguments.length === 1) {
+      while (++i < n && ((a = array[i]) == null || a != a)) a = undefined;
+      while (++i < n) if ((b = array[i]) != null && b > a) a = b;
+    } else {
+      while (++i < n && ((a = f.call(array, array[i], i)) == null || a != a)) a = undefined;
+      while (++i < n) if ((b = f.call(array, array[i], i)) != null && b > a) a = b;
+    }
+    return a;
+  };
+  d3.extent = function(array, f) {
+    var i = -1, n = array.length, a, b, c;
+    if (arguments.length === 1) {
+      while (++i < n && ((a = c = array[i]) == null || a != a)) a = c = undefined;
+      while (++i < n) if ((b = array[i]) != null) {
+        if (a > b) a = b;
+        if (c < b) c = b;
+      }
+    } else {
+      while (++i < n && ((a = c = f.call(array, array[i], i)) == null || a != a)) a = undefined;
+      while (++i < n) if ((b = f.call(array, array[i], i)) != null) {
+        if (a > b) a = b;
+        if (c < b) c = b;
+      }
+    }
+    return [ a, c ];
+  };
+  d3.random = {
+    normal: function(µ, σ) {
+      var n = arguments.length;
+      if (n < 2) σ = 1;
+      if (n < 1) µ = 0;
+      return function() {
+        var x, y, r;
+        do {
+          x = Math.random() * 2 - 1;
+          y = Math.random() * 2 - 1;
+          r = x * x + y * y;
+        } while (!r || r > 1);
+        return µ + σ * x * Math.sqrt(-2 * Math.log(r) / r);
+      };
+    },
+    logNormal: function(µ, σ) {
+      var n = arguments.length;
+      if (n < 2) σ = 1;
+      if (n < 1) µ = 0;
+      var random = d3.random.normal();
+      return function() {
+        return Math.exp(µ + σ * random());
+      };
+    },
+    irwinHall: function(m) {
+      return function() {
+        for (var s = 0, j = 0; j < m; j++) s += Math.random();
+        return s / m;
+      };
+    }
+  };
+  function d3_number(x) {
+    return x != null && !isNaN(x);
+  }
+  d3.sum = function(array, f) {
+    var s = 0, n = array.length, a, i = -1;
+    if (arguments.length === 1) {
+      while (++i < n) if (!isNaN(a = +array[i])) s += a;
+    } else {
+      while (++i < n) if (!isNaN(a = +f.call(array, array[i], i))) s += a;
+    }
+    return s;
+  };
+  d3.quantile = function(values, p) {
+    var H = (values.length - 1) * p + 1, h = Math.floor(H), v = values[h - 1], e = H - h;
+    return e ? v + e * (values[h] - v) : v;
+  };
+  d3.transpose = function(matrix) {
+    return d3.zip.apply(d3, matrix);
+  };
+  d3.zip = function() {
+    if (!(n = arguments.length)) return [];
+    for (var i = -1, m = d3.min(arguments, d3_zipLength), zips = new Array(m); ++i < m; ) {
+      for (var j = -1, n, zip = zips[i] = new Array(n); ++j < n; ) {
+        zip[j] = arguments[j][i];
+      }
+    }
+    return zips;
+  };
+  function d3_zipLength(d) {
+    return d.length;
+  }
+  d3.bisector = function(f) {
+    return {
+      left: function(a, x, lo, hi) {
+        if (arguments.length < 3) lo = 0;
+        if (arguments.length < 4) hi = a.length;
+        while (lo < hi) {
+          var mid = lo + hi >>> 1;
+          if (f.call(a, a[mid], mid) < x) lo = mid + 1; else hi = mid;
+        }
+        return lo;
+      },
+      right: function(a, x, lo, hi) {
+        if (arguments.length < 3) lo = 0;
+        if (arguments.length < 4) hi = a.length;
+        while (lo < hi) {
+          var mid = lo + hi >>> 1;
+          if (x < f.call(a, a[mid], mid)) hi = mid; else lo = mid + 1;
+        }
+        return lo;
+      }
+    };
+  };
+  var d3_bisector = d3.bisector(function(d) {
+    return d;
+  });
+  d3.bisectLeft = d3_bisector.left;
+  d3.bisect = d3.bisectRight = d3_bisector.right;
+  d3.first = function(array, f) {
+    var i = 0, n = array.length, a = array[0], b;
+    if (arguments.length === 1) f = d3.ascending;
+    while (++i < n) {
+      if (f.call(array, a, b = array[i]) > 0) {
+        a = b;
+      }
+    }
+    return a;
+  };
+  d3.last = function(array, f) {
+    var i = 0, n = array.length, a = array[0], b;
+    if (arguments.length === 1) f = d3.ascending;
+    while (++i < n) {
+      if (f.call(array, a, b = array[i]) <= 0) {
+        a = b;
+      }
+    }
+    return a;
+  };
+  d3.nest = function() {
+    var nest = {}, keys = [], sortKeys = [], sortValues, rollup;
+    function map(array, depth) {
+      if (depth >= keys.length) return rollup ? rollup.call(nest, array) : sortValues ? array.sort(sortValues) : array;
+      var i = -1, n = array.length, key = keys[depth++], keyValue, object, valuesByKey = new d3_Map, values, o = {};
+      while (++i < n) {
+        if (values = valuesByKey.get(keyValue = key(object = array[i]))) {
+          values.push(object);
+        } else {
+          valuesByKey.set(keyValue, [ object ]);
+        }
+      }
+      valuesByKey.forEach(function(keyValue) {
+        o[keyValue] = map(valuesByKey.get(keyValue), depth);
+      });
+      return o;
+    }
+    function entries(map, depth) {
+      if (depth >= keys.length) return map;
+      var a = [], sortKey = sortKeys[depth++], key;
+      for (key in map) {
+        a.push({
+          key: key,
+          values: entries(map[key], depth)
+        });
+      }
+      if (sortKey) a.sort(function(a, b) {
+        return sortKey(a.key, b.key);
+      });
+      return a;
+    }
+    nest.map = function(array) {
+      return map(array, 0);
+    };
+    nest.entries = function(array) {
+      return entries(map(array, 0), 0);
+    };
+    nest.key = function(d) {
+      keys.push(d);
+      return nest;
+    };
+    nest.sortKeys = function(order) {
+      sortKeys[keys.length - 1] = order;
+      return nest;
+    };
+    nest.sortValues = function(order) {
+      sortValues = order;
+      return nest;
+    };
+    nest.rollup = function(f) {
+      rollup = f;
+      return nest;
+    };
+    return nest;
+  };
+  d3.keys = function(map) {
+    var keys = [];
+    for (var key in map) keys.push(key);
+    return keys;
+  };
+  d3.values = function(map) {
+    var values = [];
+    for (var key in map) values.push(map[key]);
+    return values;
+  };
+  d3.entries = function(map) {
+    var entries = [];
+    for (var key in map) entries.push({
+      key: key,
+      value: map[key]
+    });
+    return entries;
+  };
+  d3.permute = function(array, indexes) {
+    var permutes = [], i = -1, n = indexes.length;
+    while (++i < n) permutes[i] = array[indexes[i]];
+    return permutes;
+  };
+  d3.merge = function(arrays) {
+    return Array.prototype.concat.apply([], arrays);
+  };
+  d3.split = function(array, f) {
+    var arrays = [], values = [], value, i = -1, n = array.length;
+    if (arguments.length < 2) f = d3_splitter;
+    while (++i < n) {
+      if (f.call(values, value = array[i], i)) {
+        values = [];
+      } else {
+        if (!values.length) arrays.push(values);
+        values.push(value);
+      }
+    }
+    return arrays;
+  };
+  function d3_splitter(d) {
+    return d == null;
+  }
+  function d3_collapse(s) {
+    return s.trim().replace(/\s+/g, " ");
+  }
+  d3.range = function(start, stop, step) {
+    if (arguments.length < 3) {
+      step = 1;
+      if (arguments.length < 2) {
+        stop = start;
+        start = 0;
+      }
+    }
+    if ((stop - start) / step === Infinity) throw new Error("infinite range");
+    var range = [], k = d3_range_integerScale(Math.abs(step)), i = -1, j;
+    start *= k, stop *= k, step *= k;
+    if (step < 0) while ((j = start + step * ++i) > stop) range.push(j / k); else while ((j = start + step * ++i) < stop) range.push(j / k);
+    return range;
+  };
+  function d3_range_integerScale(x) {
+    var k = 1;
+    while (x * k % 1) k *= 10;
+    return k;
+  }
+  d3.requote = function(s) {
+    return s.replace(d3_requote_re, "\\$&");
+  };
+  var d3_requote_re = /[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;
+  d3.round = function(x, n) {
+    return n ? Math.round(x * (n = Math.pow(10, n))) / n : Math.round(x);
+  };
+  d3.xhr = function(url, mime, callback) {
+    var req = new XMLHttpRequest;
+    if (arguments.length < 3) callback = mime, mime = null; else if (mime && req.overrideMimeType) req.overrideMimeType(mime);
+    req.open("GET", url, true);
+    if (mime) req.setRequestHeader("Accept", mime);
+    req.onreadystatechange = function() {
+      if (req.readyState === 4) {
+        var s = req.status;
+        callback(!s && req.response || s >= 200 && s < 300 || s === 304 ? req : null);
+      }
+    };
+    req.send(null);
+  };
+  d3.text = function(url, mime, callback) {
+    function ready(req) {
+      callback(req && req.responseText);
+    }
+    if (arguments.length < 3) {
+      callback = mime;
+      mime = null;
+    }
+    d3.xhr(url, mime, ready);
+  };
+  d3.json = function(url, callback) {
+    d3.text(url, "application/json", function(text) {
+      callback(text ? JSON.parse(text) : null);
+    });
+  };
+  d3.html = function(url, callback) {
+    d3.text(url, "text/html", function(text) {
+      if (text != null) {
+        var range = document.createRange();
+        range.selectNode(document.body);
+        text = range.createContextualFragment(text);
+      }
+      callback(text);
+    });
+  };
+  d3.xml = function(url, mime, callback) {
+    function ready(req) {
+      callback(req && req.responseXML);
+    }
+    if (arguments.length < 3) {
+      callback = mime;
+      mime = null;
+    }
+    d3.xhr(url, mime, ready);
+  };
+  var d3_nsPrefix = {
+    svg: "http://www.w3.org/2000/svg",
+    xhtml: "http://www.w3.org/1999/xhtml",
+    xlink: "http://www.w3.org/1999/xlink",
+    xml: "http://www.w3.org/XML/1998/namespace",
+    xmlns: "http://www.w3.org/2000/xmlns/"
+  };
+  d3.ns = {
+    prefix: d3_nsPrefix,
+    qualify: function(name) {
+      var i = name.indexOf(":"), prefix = name;
+      if (i >= 0) {
+        prefix = name.substring(0, i);
+        name = name.substring(i + 1);
+      }
+      return d3_nsPrefix.hasOwnProperty(prefix) ? {
+        space: d3_nsPrefix[prefix],
+        local: name
+      } : name;
+    }
+  };
+  d3.dispatch = function() {
+    var dispatch = new d3_dispatch, i = -1, n = arguments.length;
+    while (++i < n) dispatch[arguments[i]] = d3_dispatch_event(dispatch);
+    return dispatch;
+  };
+  function d3_dispatch() {}
+  d3_dispatch.prototype.on = function(type, listener) {
+    var i = type.indexOf("."), name = "";
+    if (i > 0) {
+      name = type.substring(i + 1);
+      type = type.substring(0, i);
+    }
+    return arguments.length < 2 ? this[type].on(name) : this[type].on(name, listener);
+  };
+  function d3_dispatch_event(dispatch) {
+    var listeners = [], listenerByName = new d3_Map;
+    function event() {
+      var z = listeners, i = -1, n = z.length, l;
+      while (++i < n) if (l = z[i].on) l.apply(this, arguments);
+      return dispatch;
+    }
+    event.on = function(name, listener) {
+      var l = listenerByName.get(name), i;
+      if (arguments.length < 2) return l && l.on;
+      if (l) {
+        l.on = null;
+        listeners = listeners.slice(0, i = listeners.indexOf(l)).concat(listeners.slice(i + 1));
+        listenerByName.remove(name);
+      }
+      if (listener) listeners.push(listenerByName.set(name, {
+        on: listener
+      }));
+      return dispatch;
+    };
+    return event;
+  }
+  d3.format = function(specifier) {
+    var match = d3_format_re.exec(specifier), fill = match[1] || " ", sign = match[3] || "", zfill = match[5], width = +match[6], comma = match[7], precision = match[8], type = match[9], scale = 1, suffix = "", integer = false;
+    if (precision) precision = +precision.substring(1);
+    if (zfill) {
+      fill = "0";
+      if (comma) width -= Math.floor((width - 1) / 4);
+    }
+    switch (type) {
+     case "n":
+      comma = true;
+      type = "g";
+      break;
+     case "%":
+      scale = 100;
+      suffix = "%";
+      type = "f";
+      break;
+     case "p":
+      scale = 100;
+      suffix = "%";
+      type = "r";
+      break;
+     case "d":
+      integer = true;
+      precision = 0;
+      break;
+     case "s":
+      scale = -1;
+      type = "r";
+      break;
+    }
+    if (type == "r" && !precision) type = "g";
+    type = d3_format_types.get(type) || d3_format_typeDefault;
+    return function(value) {
+      if (integer && value % 1) return "";
+      var negative = value < 0 && (value = -value) ? "-" : sign;
+      if (scale < 0) {
+        var prefix = d3.formatPrefix(value, precision);
+        value = prefix.scale(value);
+        suffix = prefix.symbol;
+      } else {
+        value *= scale;
+      }
+      value = type(value, precision);
+      if (zfill) {
+        var length = value.length + negative.length;
+        if (length < width) value = (new Array(width - length + 1)).join(fill) + value;
+        if (comma) value = d3_format_group(value);
+        value = negative + value;
+      } else {
+        if (comma) value = d3_format_group(value);
+        value = negative + value;
+        var length = value.length;
+        if (length < width) value = (new Array(width - length + 1)).join(fill) + value;
+      }
+      return value + suffix;
+    };
+  };
+  var d3_format_re = /(?:([^{])?([<>=^]))?([+\- ])?(#)?(0)?([0-9]+)?(,)?(\.[0-9]+)?([a-zA-Z%])?/;
+  var d3_format_types = d3.map({
+    g: function(x, p) {
+      return x.toPrecision(p);
+    },
+    e: function(x, p) {
+      return x.toExponential(p);
+    },
+    f: function(x, p) {
+      return x.toFixed(p);
+    },
+    r: function(x, p) {
+      return d3.round(x, p = d3_format_precision(x, p)).toFixed(Math.max(0, Math.min(20, p)));
+    }
+  });
+  function d3_format_precision(x, p) {
+    return p - (x ? 1 + Math.floor(Math.log(x + Math.pow(10, 1 + Math.floor(Math.log(x) / Math.LN10) - p)) / Math.LN10) : 1);
+  }
+  function d3_format_typeDefault(x) {
+    return x + "";
+  }
+  function d3_format_group(value) {
+    var i = value.lastIndexOf("."), f = i >= 0 ? value.substring(i) : (i = value.length, ""), t = [];
+    while (i > 0) t.push(value.substring(i -= 3, i + 3));
+    return t.reverse().join(",") + f;
+  }
+  var d3_formatPrefixes = [ "y", "z", "a", "f", "p", "n", "μ", "m", "", "k", "M", "G", "T", "P", "E", "Z", "Y" ].map(d3_formatPrefix);
+  d3.formatPrefix = function(value, precision) {
+    var i = 0;
+    if (value) {
+      if (value < 0) value *= -1;
+      if (precision) value = d3.round(value, d3_format_precision(value, precision));
+      i = 1 + Math.floor(1e-12 + Math.log(value) / Math.LN10);
+      i = Math.max(-24, Math.min(24, Math.floor((i <= 0 ? i + 1 : i - 1) / 3) * 3));
+    }
+    return d3_formatPrefixes[8 + i / 3];
+  };
+  function d3_formatPrefix(d, i) {
+    var k = Math.pow(10, Math.abs(8 - i) * 3);
+    return {
+      scale: i > 8 ? function(d) {
+        return d / k;
+      } : function(d) {
+        return d * k;
+      },
+      symbol: d
+    };
+  }
+  var d3_ease_quad = d3_ease_poly(2), d3_ease_cubic = d3_ease_poly(3), d3_ease_default = function() {
+    return d3_ease_identity;
+  };
+  var d3_ease = d3.map({
+    linear: d3_ease_default,
+    poly: d3_ease_poly,
+    quad: function() {
+      return d3_ease_quad;
+    },
+    cubic: function() {
+      return d3_ease_cubic;
+    },
+    sin: function() {
+      return d3_ease_sin;
+    },
+    exp: function() {
+      return d3_ease_exp;
+    },
+    circle: function() {
+      return d3_ease_circle;
+    },
+    elastic: d3_ease_elastic,
+    back: d3_ease_back,
+    bounce: function() {
+      return d3_ease_bounce;
+    }
+  });
+  var d3_ease_mode = d3.map({
+    "in": d3_ease_identity,
+    out: d3_ease_reverse,
+    "in-out": d3_ease_reflect,
+    "out-in": function(f) {
+      return d3_ease_reflect(d3_ease_reverse(f));
+    }
+  });
+  d3.ease = function(name) {
+    var i = name.indexOf("-"), t = i >= 0 ? name.substring(0, i) : name, m = i >= 0 ? name.substring(i + 1) : "in";
+    t = d3_ease.get(t) || d3_ease_default;
+    m = d3_ease_mode.get(m) || d3_ease_identity;
+    return d3_ease_clamp(m(t.apply(null, Array.prototype.slice.call(arguments, 1))));
+  };
+  function d3_ease_clamp(f) {
+    return function(t) {
+      return t <= 0 ? 0 : t >= 1 ? 1 : f(t);
+    };
+  }
+  function d3_ease_reverse(f) {
+    return function(t) {
+      return 1 - f(1 - t);
+    };
+  }
+  function d3_ease_reflect(f) {
+    return function(t) {
+      return .5 * (t < .5 ? f(2 * t) : 2 - f(2 - 2 * t));
+    };
+  }
+  function d3_ease_identity(t) {
+    return t;
+  }
+  function d3_ease_poly(e) {
+    return function(t) {
+      return Math.pow(t, e);
+    };
+  }
+  function d3_ease_sin(t) {
+    return 1 - Math.cos(t * Math.PI / 2);
+  }
+  function d3_ease_exp(t) {
+    return Math.pow(2, 10 * (t - 1));
+  }
+  function d3_ease_circle(t) {
+    return 1 - Math.sqrt(1 - t * t);
+  }
+  function d3_ease_elastic(a, p) {
+    var s;
+    if (arguments.length < 2) p = .45;
+    if (arguments.length < 1) {
+      a = 1;
+      s = p / 4;
+    } else s = p / (2 * Math.PI) * Math.asin(1 / a);
+    return function(t) {
+      return 1 + a * Math.pow(2, 10 * -t) * Math.sin((t - s) * 2 * Math.PI / p);
+    };
+  }
+  function d3_ease_back(s) {
+    if (!s) s = 1.70158;
+    return function(t) {
+      return t * t * ((s + 1) * t - s);
+    };
+  }
+  function d3_ease_bounce(t) {
+    return t < 1 / 2.75 ? 7.5625 * t * t : t < 2 / 2.75 ? 7.5625 * (t -= 1.5 / 2.75) * t + .75 : t < 2.5 / 2.75 ? 7.5625 * (t -= 2.25 / 2.75) * t + .9375 : 7.5625 * (t -= 2.625 / 2.75) * t + .984375;
+  }
+  d3.event = null;
+  function d3_eventCancel() {
+    d3.event.stopPropagation();
+    d3.event.preventDefault();
+  }
+  function d3_eventSource() {
+    var e = d3.event, s;
+    while (s = e.sourceEvent) e = s;
+    return e;
+  }
+  function d3_eventDispatch(target) {
+    var dispatch = new d3_dispatch, i = 0, n = arguments.length;
+    while (++i < n) dispatch[arguments[i]] = d3_dispatch_event(dispatch);
+    dispatch.of = function(thiz, argumentz) {
+      return function(e1) {
+        try {
+          var e0 = e1.sourceEvent = d3.event;
+          e1.target = target;
+          d3.event = e1;
+          dispatch[e1.type].apply(thiz, argumentz);
+        } finally {
+          d3.event = e0;
+        }
+      };
+    };
+    return dispatch;
+  }
+  d3.transform = function(string) {
+    var g = document.createElementNS(d3.ns.prefix.svg, "g");
+    return (d3.transform = function(string) {
+      g.setAttribute("transform", string);
+      var t = g.transform.baseVal.consolidate();
+      return new d3_transform(t ? t.matrix : d3_transformIdentity);
+    })(string);
+  };
+  function d3_transform(m) {
+    var r0 = [ m.a, m.b ], r1 = [ m.c, m.d ], kx = d3_transformNormalize(r0), kz = d3_transformDot(r0, r1), ky = d3_transformNormalize(d3_transformCombine(r1, r0, -kz)) || 0;
+    if (r0[0] * r1[1] < r1[0] * r0[1]) {
+      r0[0] *= -1;
+      r0[1] *= -1;
+      kx *= -1;
+      kz *= -1;
+    }
+    this.rotate = (kx ? Math.atan2(r0[1], r0[0]) : Math.atan2(-r1[0], r1[1])) * d3_transformDegrees;
+    this.translate = [ m.e, m.f ];
+    this.scale = [ kx, ky ];
+    this.skew = ky ? Math.atan2(kz, ky) * d3_transformDegrees : 0;
+  }
+  d3_transform.prototype.toString = function() {
+    return "translate(" + this.translate + ")rotate(" + this.rotate + ")skewX(" + this.skew + ")scale(" + this.scale + ")";
+  };
+  function d3_transformDot(a, b) {
+    return a[0] * b[0] + a[1] * b[1];
+  }
+  function d3_transformNormalize(a) {
+    var k = Math.sqrt(d3_transformDot(a, a));
+    if (k) {
+      a[0] /= k;
+      a[1] /= k;
+    }
+    return k;
+  }
+  function d3_transformCombine(a, b, k) {
+    a[0] += k * b[0];
+    a[1] += k * b[1];
+    return a;
+  }
+  var d3_transformDegrees = 180 / Math.PI, d3_transformIdentity = {
+    a: 1,
+    b: 0,
+    c: 0,
+    d: 1,
+    e: 0,
+    f: 0
+  };
+  d3.interpolate = function(a, b) {
+    var i = d3.interpolators.length, f;
+    while (--i >= 0 && !(f = d3.interpolators[i](a, b))) ;
+    return f;
+  };
+  d3.interpolateNumber = function(a, b) {
+    b -= a;
+    return function(t) {
+      return a + b * t;
+    };
+  };
+  d3.interpolateRound = function(a, b) {
+    b -= a;
+    return function(t) {
+      return Math.round(a + b * t);
+    };
+  };
+  d3.interpolateString = function(a, b) {
+    var m, i, j, s0 = 0, s1 = 0, s = [], q = [], n, o;
+    d3_interpolate_number.lastIndex = 0;
+    for (i = 0; m = d3_interpolate_number.exec(b); ++i) {
+      if (m.index) s.push(b.substring(s0, s1 = m.index));
+      q.push({
+        i: s.length,
+        x: m[0]
+      });
+      s.push(null);
+      s0 = d3_interpolate_number.lastIndex;
+    }
+    if (s0 < b.length) s.push(b.substring(s0));
+    for (i = 0, n = q.length; (m = d3_interpolate_number.exec(a)) && i < n; ++i) {
+      o = q[i];
+      if (o.x == m[0]) {
+        if (o.i) {
+          if (s[o.i + 1] == null) {
+            s[o.i - 1] += o.x;
+            s.splice(o.i, 1);
+            for (j = i + 1; j < n; ++j) q[j].i--;
+          } else {
+            s[o.i - 1] += o.x + s[o.i + 1];
+            s.splice(o.i, 2);
+            for (j = i + 1; j < n; ++j) q[j].i -= 2;
+          }
+        } else {
+          if (s[o.i + 1] == null) {
+            s[o.i] = o.x;
+          } else {
+            s[o.i] = o.x + s[o.i + 1];
+            s.splice(o.i + 1, 1);
+            for (j = i + 1; j < n; ++j) q[j].i--;
+          }
+        }
+        q.splice(i, 1);
+        n--;
+        i--;
+      } else {
+        o.x = d3.interpolateNumber(parseFloat(m[0]), parseFloat(o.x));
+      }
+    }
+    while (i < n) {
+      o = q.pop();
+      if (s[o.i + 1] == null) {
+        s[o.i] = o.x;
+      } else {
+        s[o.i] = o.x + s[o.i + 1];
+        s.splice(o.i + 1, 1);
+      }
+      n--;
+    }
+    if (s.length === 1) {
+      return s[0] == null ? q[0].x : function() {
+        return b;
+      };
+    }
+    return function(t) {
+      for (i = 0; i < n; ++i) s[(o = q[i]).i] = o.x(t);
+      return s.join("");
+    };
+  };
+  d3.interpolateTransform = function(a, b) {
+    var s = [], q = [], n, A = d3.transform(a), B = d3.transform(b), ta = A.translate, tb = B.translate, ra = A.rotate, rb = B.rotate, wa = A.skew, wb = B.skew, ka = A.scale, kb = B.scale;
+    if (ta[0] != tb[0] || ta[1] != tb[1]) {
+      s.push("translate(", null, ",", null, ")");
+      q.push({
+        i: 1,
+        x: d3.interpolateNumber(ta[0], tb[0])
+      }, {
+        i: 3,
+        x: d3.interpolateNumber(ta[1], tb[1])
+      });
+    } else if (tb[0] || tb[1]) {
+      s.push("translate(" + tb + ")");
+    } else {
+      s.push("");
+    }
+    if (ra != rb) {
+      if (ra - rb > 180) rb += 360; else if (rb - ra > 180) ra += 360;
+      q.push({
+        i: s.push(s.pop() + "rotate(", null, ")") - 2,
+        x: d3.interpolateNumber(ra, rb)
+      });
+    } else if (rb) {
+      s.push(s.pop() + "rotate(" + rb + ")");
+    }
+    if (wa != wb) {
+      q.push({
+        i: s.push(s.pop() + "skewX(", null, ")") - 2,
+        x: d3.interpolateNumber(wa, wb)
+      });
+    } else if (wb) {
+      s.push(s.pop() + "skewX(" + wb + ")");
+    }
+    if (ka[0] != kb[0] || ka[1] != kb[1]) {
+      n = s.push(s.pop() + "scale(", null, ",", null, ")");
+      q.push({
+        i: n - 4,
+        x: d3.interpolateNumber(ka[0], kb[0])
+      }, {
+        i: n - 2,
+        x: d3.interpolateNumber(ka[1], kb[1])
+      });
+    } else if (kb[0] != 1 || kb[1] != 1) {
+      s.push(s.pop() + "scale(" + kb + ")");
+    }
+    n = q.length;
+    return function(t) {
+      var i = -1, o;
+      while (++i < n) s[(o = q[i]).i] = o.x(t);
+      return s.join("");
+    };
+  };
+  d3.interpolateRgb = function(a, b) {
+    a = d3.rgb(a);
+    b = d3.rgb(b);
+    var ar = a.r, ag = a.g, ab = a.b, br = b.r - ar, bg = b.g - ag, bb = b.b - ab;
+    return function(t) {
+      return "#" + d3_rgb_hex(Math.round(ar + br * t)) + d3_rgb_hex(Math.round(ag + bg * t)) + d3_rgb_hex(Math.round(ab + bb * t));
+    };
+  };
+  d3.interpolateHsl = function(a, b) {
+    a = d3.hsl(a);
+    b = d3.hsl(b);
+    var h0 = a.h, s0 = a.s, l0 = a.l, h1 = b.h - h0, s1 = b.s - s0, l1 = b.l - l0;
+    if (h1 > 180) h1 -= 360; else if (h1 < -180) h1 += 360;
+    return function(t) {
+      return d3_hsl_rgb(h0 + h1 * t, s0 + s1 * t, l0 + l1 * t) + "";
+    };
+  };
+  d3.interpolateLab = function(a, b) {
+    a = d3.lab(a);
+    b = d3.lab(b);
+    var al = a.l, aa = a.a, ab = a.b, bl = b.l - al, ba = b.a - aa, bb = b.b - ab;
+    return function(t) {
+      return d3_lab_rgb(al + bl * t, aa + ba * t, ab + bb * t) + "";
+    };
+  };
+  d3.interpolateHcl = function(a, b) {
+    a = d3.hcl(a);
+    b = d3.hcl(b);
+    var ah = a.h, ac = a.c, al = a.l, bh = b.h - ah, bc = b.c - ac, bl = b.l - al;
+    if (bh > 180) bh -= 360; else if (bh < -180) bh += 360;
+    return function(t) {
+      return d3_hcl_lab(ah + bh * t, ac + bc * t, al + bl * t) + "";
+    };
+  };
+  d3.interpolateArray = function(a, b) {
+    var x = [], c = [], na = a.length, nb = b.length, n0 = Math.min(a.length, b.length), i;
+    for (i = 0; i < n0; ++i) x.push(d3.interpolate(a[i], b[i]));
+    for (; i < na; ++i) c[i] = a[i];
+    for (; i < nb; ++i) c[i] = b[i];
+    return function(t) {
+      for (i = 0; i < n0; ++i) c[i] = x[i](t);
+      return c;
+    };
+  };
+  d3.interpolateObject = function(a, b) {
+    var i = {}, c = {}, k;
+    for (k in a) {
+      if (k in b) {
+        i[k] = d3_interpolateByName(k)(a[k], b[k]);
+      } else {
+        c[k] = a[k];
+      }
+    }
+    for (k in b) {
+      if (!(k in a)) {
+        c[k] = b[k];
+      }
+    }
+    return function(t) {
+      for (k in i) c[k] = i[k](t);
+      return c;
+    };
+  };
+  var d3_interpolate_number = /[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g;
+  function d3_interpolateByName(name) {
+    return name == "transform" ? d3.interpolateTransform : d3.interpolate;
+  }
+  d3.interpolators = [ d3.interpolateObject, function(a, b) {
+    return b instanceof Array && d3.interpolateArray(a, b);
+  }, function(a, b) {
+    return (typeof a === "string" || typeof b === "string") && d3.interpolateString(a + "", b + "");
+  }, function(a, b) {
+    return (typeof b === "string" ? d3_rgb_names.has(b) || /^(#|rgb\(|hsl\()/.test(b) : b instanceof d3_Rgb || b instanceof d3_Hsl) && d3.interpolateRgb(a, b);
+  }, function(a, b) {
+    return !isNaN(a = +a) && !isNaN(b = +b) && d3.interpolateNumber(a, b);
+  } ];
+  function d3_uninterpolateNumber(a, b) {
+    b = b - (a = +a) ? 1 / (b - a) : 0;
+    return function(x) {
+      return (x - a) * b;
+    };
+  }
+  function d3_uninterpolateClamp(a, b) {
+    b = b - (a = +a) ? 1 / (b - a) : 0;
+    return function(x) {
+      return Math.max(0, Math.min(1, (x - a) * b));
+    };
+  }
+  d3.rgb = function(r, g, b) {
+    return arguments.length === 1 ? r instanceof d3_Rgb ? d3_rgb(r.r, r.g, r.b) : d3_rgb_parse("" + r, d3_rgb, d3_hsl_rgb) : d3_rgb(~~r, ~~g, ~~b);
+  };
+  function d3_rgb(r, g, b) {
+    return new d3_Rgb(r, g, b);
+  }
+  function d3_Rgb(r, g, b) {
+    this.r = r;
+    this.g = g;
+    this.b = b;
+  }
+  d3_Rgb.prototype.brighter = function(k) {
+    k = Math.pow(.7, arguments.length ? k : 1);
+    var r = this.r, g = this.g, b = this.b, i = 30;
+    if (!r && !g && !b) return d3_rgb(i, i, i);
+    if (r && r < i) r = i;
+    if (g && g < i) g = i;
+    if (b && b < i) b = i;
+    return d3_rgb(Math.min(255, Math.floor(r / k)), Math.min(255, Math.floor(g / k)), Math.min(255, Math.floor(b / k)));
+  };
+  d3_Rgb.prototype.darker = function(k) {
+    k = Math.pow(.7, arguments.length ? k : 1);
+    return d3_rgb(Math.floor(k * this.r), Math.floor(k * this.g), Math.floor(k * this.b));
+  };
+  d3_Rgb.prototype.hsl = function() {
+    return d3_rgb_hsl(this.r, this.g, this.b);
+  };
+  d3_Rgb.prototype.toString = function() {
+    return "#" + d3_rgb_hex(this.r) + d3_rgb_hex(this.g) + d3_rgb_hex(this.b);
+  };
+  function d3_rgb_hex(v) {
+    return v < 16 ? "0" + Math.max(0, v).toString(16) : Math.min(255, v).toString(16);
+  }
+  function d3_rgb_parse(format, rgb, hsl) {
+    var r = 0, g = 0, b = 0, m1, m2, name;
+    m1 = /([a-z]+)\((.*)\)/i.exec(format);
+    if (m1) {
+      m2 = m1[2].split(",");
+      switch (m1[1]) {
+       case "hsl":
+        {
+          return hsl(parseFloat(m2[0]), parseFloat(m2[1]) / 100, parseFloat(m2[2]) / 100);
+        }
+       case "rgb":
+        {
+          return rgb(d3_rgb_parseNumber(m2[0]), d3_rgb_parseNumber(m2[1]), d3_rgb_parseNumber(m2[2]));
+        }
+      }
+    }
+    if (name = d3_rgb_names.get(format)) return rgb(name.r, name.g, name.b);
+    if (format != null && format.charAt(0) === "#") {
+      if (format.length === 4) {
+        r = format.charAt(1);
+        r += r;
+        g = format.charAt(2);
+        g += g;
+        b = format.charAt(3);
+        b += b;
+      } else if (format.length === 7) {
+        r = format.substring(1, 3);
+        g = format.substring(3, 5);
+        b = format.substring(5, 7);
+      }
+      r = parseInt(r, 16);
+      g = parseInt(g, 16);
+      b = parseInt(b, 16);
+    }
+    return rgb(r, g, b);
+  }
+  function d3_rgb_hsl(r, g, b) {
+    var min = Math.min(r /= 255, g /= 255, b /= 255), max = Math.max(r, g, b), d = max - min, h, s, l = (max + min) / 2;
+    if (d) {
+      s = l < .5 ? d / (max + min) : d / (2 - max - min);
+      if (r == max) h = (g - b) / d + (g < b ? 6 : 0); else if (g == max) h = (b - r) / d + 2; else h = (r - g) / d + 4;
+      h *= 60;
+    } else {
+      s = h = 0;
+    }
+    return d3_hsl(h, s, l);
+  }
+  function d3_rgb_lab(r, g, b) {
+    r = d3_rgb_xyz(r);
+    g = d3_rgb_xyz(g);
+    b = d3_rgb_xyz(b);
+    var x = d3_xyz_lab((.4124564 * r + .3575761 * g + .1804375 * b) / d3_lab_X), y = d3_xyz_lab((.2126729 * r + .7151522 * g + .072175 * b) / d3_lab_Y), z = d3_xyz_lab((.0193339 * r + .119192 * g + .9503041 * b) / d3_lab_Z);
+    return d3_lab(116 * y - 16, 500 * (x - y), 200 * (y - z));
+  }
+  function d3_rgb_xyz(r) {
+    return (r /= 255) <= .04045 ? r / 12.92 : Math.pow((r + .055) / 1.055, 2.4);
+  }
+  function d3_rgb_parseNumber(c) {
+    var f = parseFloat(c);
+    return c.charAt(c.length - 1) === "%" ? Math.round(f * 2.55) : f;
+  }
+  var d3_rgb_names = d3.map({
+    aliceblue: "#f0f8ff",
+    antiquewhite: "#faebd7",
+    aqua: "#00ffff",
+    aquamarine: "#7fffd4",
+    azure: "#f0ffff",
+    beige: "#f5f5dc",
+    bisque: "#ffe4c4",
+    black: "#000000",
+    blanchedalmond: "#ffebcd",
+    blue: "#0000ff",
+    blueviolet: "#8a2be2",
+    brown: "#a52a2a",
+    burlywood: "#deb887",
+    cadetblue: "#5f9ea0",
+    chartreuse: "#7fff00",
+    chocolate: "#d2691e",
+    coral: "#ff7f50",
+    cornflowerblue: "#6495ed",
+    cornsilk: "#fff8dc",
+    crimson: "#dc143c",
+    cyan: "#00ffff",
+    darkblue: "#00008b",
+    darkcyan: "#008b8b",
+    darkgoldenrod: "#b8860b",
+    darkgray: "#a9a9a9",
+    darkgreen: "#006400",
+    darkgrey: "#a9a9a9",
+    darkkhaki: "#bdb76b",
+    darkmagenta: "#8b008b",
+    darkolivegreen: "#556b2f",
+    darkorange: "#ff8c00",
+    darkorchid: "#9932cc",
+    darkred: "#8b0000",
+    darksalmon: "#e9967a",
+    darkseagreen: "#8fbc8f",
+    darkslateblue: "#483d8b",
+    darkslategray: "#2f4f4f",
+    darkslategrey: "#2f4f4f",
+    darkturquoise: "#00ced1",
+    darkviolet: "#9400d3",
+    deeppink: "#ff1493",
+    deepskyblue: "#00bfff",
+    dimgray: "#696969",
+    dimgrey: "#696969",
+    dodgerblue: "#1e90ff",
+    firebrick: "#b22222",
+    floralwhite: "#fffaf0",
+    forestgreen: "#228b22",
+    fuchsia: "#ff00ff",
+    gainsboro: "#dcdcdc",
+    ghostwhite: "#f8f8ff",
+    gold: "#ffd700",
+    goldenrod: "#daa520",
+    gray: "#808080",
+    green: "#008000",
+    greenyellow: "#adff2f",
+    grey: "#808080",
+    honeydew: "#f0fff0",
+    hotpink: "#ff69b4",
+    indianred: "#cd5c5c",
+    indigo: "#4b0082",
+    ivory: "#fffff0",
+    khaki: "#f0e68c",
+    lavender: "#e6e6fa",
+    lavenderblush: "#fff0f5",
+    lawngreen: "#7cfc00",
+    lemonchiffon: "#fffacd",
+    lightblue: "#add8e6",
+    lightcoral: "#f08080",
+    lightcyan: "#e0ffff",
+    lightgoldenrodyellow: "#fafad2",
+    lightgray: "#d3d3d3",
+    lightgreen: "#90ee90",
+    lightgrey: "#d3d3d3",
+    lightpink: "#ffb6c1",
+    lightsalmon: "#ffa07a",
+    lightseagreen: "#20b2aa",
+    lightskyblue: "#87cefa",
+    lightslategray: "#778899",
+    lightslategrey: "#778899",
+    lightsteelblue: "#b0c4de",
+    lightyellow: "#ffffe0",
+    lime: "#00ff00",
+    limegreen: "#32cd32",
+    linen: "#faf0e6",
+    magenta: "#ff00ff",
+    maroon: "#800000",
+    mediumaquamarine: "#66cdaa",
+    mediumblue: "#0000cd",
+    mediumorchid: "#ba55d3",
+    mediumpurple: "#9370db",
+    mediumseagreen: "#3cb371",
+    mediumslateblue: "#7b68ee",
+    mediumspringgreen: "#00fa9a",
+    mediumturquoise: "#48d1cc",
+    mediumvioletred: "#c71585",
+    midnightblue: "#191970",
+    mintcream: "#f5fffa",
+    mistyrose: "#ffe4e1",
+    moccasin: "#ffe4b5",
+    navajowhite: "#ffdead",
+    navy: "#000080",
+    oldlace: "#fdf5e6",
+    olive: "#808000",
+    olivedrab: "#6b8e23",
+    orange: "#ffa500",
+    orangered: "#ff4500",
+    orchid: "#da70d6",
+    palegoldenrod: "#eee8aa",
+    palegreen: "#98fb98",
+    paleturquoise: "#afeeee",
+    palevioletred: "#db7093",
+    papayawhip: "#ffefd5",
+    peachpuff: "#ffdab9",
+    peru: "#cd853f",
+    pink: "#ffc0cb",
+    plum: "#dda0dd",
+    powderblue: "#b0e0e6",
+    purple: "#800080",
+    red: "#ff0000",
+    rosybrown: "#bc8f8f",
+    royalblue: "#4169e1",
+    saddlebrown: "#8b4513",
+    salmon: "#fa8072",
+    sandybrown: "#f4a460",
+    seagreen: "#2e8b57",
+    seashell: "#fff5ee",
+    sienna: "#a0522d",
+    silver: "#c0c0c0",
+    skyblue: "#87ceeb",
+    slateblue: "#6a5acd",
+    slategray: "#708090",
+    slategrey: "#708090",
+    snow: "#fffafa",
+    springgreen: "#00ff7f",
+    steelblue: "#4682b4",
+    tan: "#d2b48c",
+    teal: "#008080",
+    thistle: "#d8bfd8",
+    tomato: "#ff6347",
+    turquoise: "#40e0d0",
+    violet: "#ee82ee",
+    wheat: "#f5deb3",
+    white: "#ffffff",
+    whitesmoke: "#f5f5f5",
+    yellow: "#ffff00",
+    yellowgreen: "#9acd32"
+  });
+  d3_rgb_names.forEach(function(key, value) {
+    d3_rgb_names.set(key, d3_rgb_parse(value, d3_rgb, d3_hsl_rgb));
+  });
+  d3.hsl = function(h, s, l) {
+    return arguments.length === 1 ? h instanceof d3_Hsl ? d3_hsl(h.h, h.s, h.l) : d3_rgb_parse("" + h, d3_rgb_hsl, d3_hsl) : d3_hsl(+h, +s, +l);
+  };
+  function d3_hsl(h, s, l) {
+    return new d3_Hsl(h, s, l);
+  }
+  function d3_Hsl(h, s, l) {
+    this.h = h;
+    this.s = s;
+    this.l = l;
+  }
+  d3_Hsl.prototype.brighter = function(k) {
+    k = Math.pow(.7, arguments.length ? k : 1);
+    return d3_hsl(this.h, this.s, this.l / k);
+  };
+  d3_Hsl.prototype.darker = function(k) {
+    k = Math.pow(.7, arguments.length ? k : 1);
+    return d3_hsl(this.h, this.s, k * this.l);
+  };
+  d3_Hsl.prototype.rgb = function() {
+    return d3_hsl_rgb(this.h, this.s, this.l);
+  };
+  d3_Hsl.prototype.toString = function() {
+    return this.rgb().toString();
+  };
+  function d3_hsl_rgb(h, s, l) {
+    var m1, m2;
+    h = h % 360;
+    if (h < 0) h += 360;
+    s = s < 0 ? 0 : s > 1 ? 1 : s;
+    l = l < 0 ? 0 : l > 1 ? 1 : l;
+    m2 = l <= .5 ? l * (1 + s) : l + s - l * s;
+    m1 = 2 * l - m2;
+    function v(h) {
+      if (h > 360) h -= 360; else if (h < 0) h += 360;
+      if (h < 60) return m1 + (m2 - m1) * h / 60;
+      if (h < 180) return m2;
+      if (h < 240) return m1 + (m2 - m1) * (240 - h) / 60;
+      return m1;
+    }
+    function vv(h) {
+      return Math.round(v(h) * 255);
+    }
+    return d3_rgb(vv(h + 120), vv(h), vv(h - 120));
+  }
+  d3.hcl = function(h, c, l) {
+    return arguments.length === 1 ? h instanceof d3_Hcl ? d3_hcl(h.h, h.c, h.l) : h instanceof d3_Lab ? d3_lab_hcl(h.l, h.a, h.b) : d3_lab_hcl((h = d3_rgb_lab((h = d3.rgb(h)).r, h.g, h.b)).l, h.a, h.b) : d3_hcl(+h, +c, +l);
+  };
+  function d3_hcl(h, c, l) {
+    return new d3_Hcl(h, c, l);
+  }
+  function d3_Hcl(h, c, l) {
+    this.h = h;
+    this.c = c;
+    this.l = l;
+  }
+  d3_Hcl.prototype.brighter = function(k) {
+    return d3_hcl(this.h, this.c, Math.min(100, this.l + d3_lab_K * (arguments.length ? k : 1)));
+  };
+  d3_Hcl.prototype.darker = function(k) {
+    return d3_hcl(this.h, this.c, Math.max(0, this.l - d3_lab_K * (arguments.length ? k : 1)));
+  };
+  d3_Hcl.prototype.rgb = function() {
+    return d3_hcl_lab(this.h, this.c, this.l).rgb();
+  };
+  d3_Hcl.prototype.toString = function() {
+    return this.rgb() + "";
+  };
+  function d3_hcl_lab(h, c, l) {
+    return d3_lab(l, Math.cos(h *= Math.PI / 180) * c, Math.sin(h) * c);
+  }
+  d3.lab = function(l, a, b) {
+    return arguments.length === 1 ? l instanceof d3_Lab ? d3_lab(l.l, l.a, l.b) : l instanceof d3_Hcl ? d3_hcl_lab(l.l, l.c, l.h) : d3_rgb_lab((l = d3.rgb(l)).r, l.g, l.b) : d3_lab(+l, +a, +b);
+  };
+  function d3_lab(l, a, b) {
+    return new d3_Lab(l, a, b);
+  }
+  function d3_Lab(l, a, b) {
+    this.l = l;
+    this.a = a;
+    this.b = b;
+  }
+  var d3_lab_K = 18;
+  var d3_lab_X = .95047, d3_lab_Y = 1, d3_lab_Z = 1.08883;
+  d3_Lab.prototype.brighter = function(k) {
+    return d3_lab(Math.min(100, this.l + d3_lab_K * (arguments.length ? k : 1)), this.a, this.b);
+  };
+  d3_Lab.prototype.darker = function(k) {
+    return d3_lab(Math.max(0, this.l - d3_lab_K * (arguments.length ? k : 1)), this.a, this.b);
+  };
+  d3_Lab.prototype.rgb = function() {
+    return d3_lab_rgb(this.l, this.a, this.b);
+  };
+  d3_Lab.prototype.toString = function() {
+    return this.rgb() + "";
+  };
+  function d3_lab_rgb(l, a, b) {
+    var y = (l + 16) / 116, x = y + a / 500, z = y - b / 200;
+    x = d3_lab_xyz(x) * d3_lab_X;
+    y = d3_lab_xyz(y) * d3_lab_Y;
+    z = d3_lab_xyz(z) * d3_lab_Z;
+    return d3_rgb(d3_xyz_rgb(3.2404542 * x - 1.5371385 * y - .4985314 * z), d3_xyz_rgb(-.969266 * x + 1.8760108 * y + .041556 * z), d3_xyz_rgb(.0556434 * x - .2040259 * y + 1.0572252 * z));
+  }
+  function d3_lab_hcl(l, a, b) {
+    return d3_hcl(Math.atan2(b, a) / Math.PI * 180, Math.sqrt(a * a + b * b), l);
+  }
+  function d3_lab_xyz(x) {
+    return x > .206893034 ? x * x * x : (x - 4 / 29) / 7.787037;
+  }
+  function d3_xyz_lab(x) {
+    return x > .008856 ? Math.pow(x, 1 / 3) : 7.787037 * x + 4 / 29;
+  }
+  function d3_xyz_rgb(r) {
+    return Math.round(255 * (r <= .00304 ? 12.92 * r : 1.055 * Math.pow(r, 1 / 2.4) - .055));
+  }
+  function d3_selection(groups) {
+    d3_arraySubclass(groups, d3_selectionPrototype);
+    return groups;
+  }
+  var d3_select = function(s, n) {
+    return n.querySelector(s);
+  }, d3_selectAll = function(s, n) {
+    return n.querySelectorAll(s);
+  }, d3_selectRoot = document.documentElement, d3_selectMatcher = d3_selectRoot.matchesSelector || d3_selectRoot.webkitMatchesSelector || d3_selectRoot.mozMatchesSelector || d3_selectRoot.msMatchesSelector || d3_selectRoot.oMatchesSelector, d3_selectMatches = function(n, s) {
+    return d3_selectMatcher.call(n, s);
+  };
+  if (typeof Sizzle === "function") {
+    d3_select = function(s, n) {
+      return Sizzle(s, n)[0] || null;
+    };
+    d3_selectAll = function(s, n) {
+      return Sizzle.uniqueSort(Sizzle(s, n));
+    };
+    d3_selectMatches = Sizzle.matchesSelector;
+  }
+  var d3_selectionPrototype = [];
+  d3.selection = function() {
+    return d3_selectionRoot;
+  };
+  d3.selection.prototype = d3_selectionPrototype;
+  d3_selectionPrototype.select = function(selector) {
+    var subgroups = [], subgroup, subnode, group, node;
+    if (typeof selector !== "function") selector = d3_selection_selector(selector);
+    for (var j = -1, m = this.length; ++j < m; ) {
+      subgroups.push(subgroup = []);
+      subgroup.parentNode = (group = this[j]).parentNode;
+      for (var i = -1, n = group.length; ++i < n; ) {
+        if (node = group[i]) {
+          subgroup.push(subnode = selector.call(node, node.__data__, i));
+          if (subnode && "__data__" in node) subnode.__data__ = node.__data__;
+        } else {
+          subgroup.push(null);
+        }
+      }
+    }
+    return d3_selection(subgroups);
+  };
+  function d3_selection_selector(selector) {
+    return function() {
+      return d3_select(selector, this);
+    };
+  }
+  d3_selectionPrototype.selectAll = function(selector) {
+    var subgroups = [], subgroup, node;
+    if (typeof selector !== "function") selector = d3_selection_selectorAll(selector);
+    for (var j = -1, m = this.length; ++j < m; ) {
+      for (var group = this[j], i = -1, n = group.length; ++i < n; ) {
+        if (node = group[i]) {
+          subgroups.push(subgroup = d3_array(selector.call(node, node.__data__, i)));
+          subgroup.parentNode = node;
+        }
+      }
+    }
+    return d3_selection(subgroups);
+  };
+  function d3_selection_selectorAll(selector) {
+    return function() {
+      return d3_selectAll(selector, this);
+    };
+  }
+  d3_selectionPrototype.attr = function(name, value) {
+    if (arguments.length < 2) {
+      if (typeof name === "string") {
+        var node = this.node();
+        name = d3.ns.qualify(name);
+        return name.local ? node.getAttributeNS(name.space, name.local) : node.getAttribute(name);
+      }
+      for (value in name) this.each(d3_selection_attr(value, name[value]));
+      return this;
+    }
+    return this.each(d3_selection_attr(name, value));
+  };
+  function d3_selection_attr(name, value) {
+    name = d3.ns.qualify(name);
+    function attrNull() {
+      this.removeAttribute(name);
+    }
+    function attrNullNS() {
+      this.removeAttributeNS(name.space, name.local);
+    }
+    function attrConstant() {
+      this.setAttribute(name, value);
+    }
+    function attrConstantNS() {
+      this.setAttributeNS(name.space, name.local, value);
+    }
+    function attrFunction() {
+      var x = value.apply(this, arguments);
+      if (x == null) this.removeAttribute(name); else this.setAttribute(name, x);
+    }
+    function attrFunctionNS() {
+      var x = value.apply(this, arguments);
+      if (x == null) this.removeAttributeNS(name.space, name.local); else this.setAttributeNS(name.space, name.local, x);
+    }
+    return value == null ? name.local ? attrNullNS : attrNull : typeof value === "function" ? name.local ? attrFunctionNS : attrFunction : name.local ? attrConstantNS : attrConstant;
+  }
+  d3_selectionPrototype.classed = function(name, value) {
+    if (arguments.length < 2) {
+      if (typeof name === "string") {
+        var node = this.node(), n = (name = name.trim().split(/^|\s+/g)).length, i = -1;
+        if (value = node.classList) {
+          while (++i < n) if (!value.contains(name[i])) return false;
+        } else {
+          value = node.className;
+          if (value.baseVal != null) value = value.baseVal;
+          while (++i < n) if (!d3_selection_classedRe(name[i]).test(value)) return false;
+        }
+        return true;
+      }
+      for (value in name) this.each(d3_selection_classed(value, name[value]));
+      return this;
+    }
+    return this.each(d3_selection_classed(name, value));
+  };
+  function d3_selection_classedRe(name) {
+    return new RegExp("(?:^|\\s+)" + d3.requote(name) + "(?:\\s+|$)", "g");
+  }
+  function d3_selection_classed(name, value) {
+    name = name.trim().split(/\s+/).map(d3_selection_classedName);
+    var n = name.length;
+    function classedConstant() {
+      var i = -1;
+      while (++i < n) name[i](this, value);
+    }
+    function classedFunction() {
+      var i = -1, x = value.apply(this, arguments);
+      while (++i < n) name[i](this, x);
+    }
+    return typeof value === "function" ? classedFunction : classedConstant;
+  }
+  function d3_selection_classedName(name) {
+    var re = d3_selection_classedRe(name);
+    return function(node, value) {
+      if (c = node.classList) return value ? c.add(name) : c.remove(name);
+      var c = node.className, cb = c.baseVal != null, cv = cb ? c.baseVal : c;
+      if (value) {
+        re.lastIndex = 0;
+        if (!re.test(cv)) {
+          cv = d3_collapse(cv + " " + name);
+          if (cb) c.baseVal = cv; else node.className = cv;
+        }
+      } else if (cv) {
+        cv = d3_collapse(cv.replace(re, " "));
+        if (cb) c.baseVal = cv; else node.className = cv;
+      }
+    };
+  }
+  d3_selectionPrototype.style = function(name, value, priority) {
+    var n = arguments.length;
+    if (n < 3) {
+      if (typeof name !== "string") {
+        if (n < 2) value = "";
+        for (priority in name) this.each(d3_selection_style(priority, name[priority], value));
+        return this;
+      }
+      if (n < 2) return window.getComputedStyle(this.node(), null).getPropertyValue(name);
+      priority = "";
+    }
+    return this.each(d3_selection_style(name, value, priority));
+  };
+  function d3_selection_style(name, value, priority) {
+    function styleNull() {
+      this.style.removeProperty(name);
+    }
+    function styleConstant() {
+      this.style.setProperty(name, value, priority);
+    }
+    function styleFunction() {
+      var x = value.apply(this, arguments);
+      if (x == null) this.style.removeProperty(name); else this.style.setProperty(name, x, priority);
+    }
+    return value == null ? styleNull : typeof value === "function" ? styleFunction : styleConstant;
+  }
+  d3_selectionPrototype.property = function(name, value) {
+    if (arguments.length < 2) {
+      if (typeof name === "string") return this.node()[name];
+      for (value in name) this.each(d3_selection_property(value, name[value]));
+      return this;
+    }
+    return this.each(d3_selection_property(name, value));
+  };
+  function d3_selection_property(name, value) {
+    function propertyNull() {
+      delete this[name];
+    }
+    function propertyConstant() {
+      this[name] = value;
+    }
+    function propertyFunction() {
+      var x = value.apply(this, arguments);
+      if (x == null) delete this[name]; else this[name] = x;
+    }
+    return value == null ? propertyNull : typeof value === "function" ? propertyFunction : propertyConstant;
+  }
+  d3_selectionPrototype.text = function(value) {
+    return arguments.length < 1 ? this.node().textContent : this.each(typeof value === "function" ? function() {
+      var v = value.apply(this, arguments);
+      this.textContent = v == null ? "" : v;
+    } : value == null ? function() {
+      this.textContent = "";
+    } : function() {
+      this.textContent = value;
+    });
+  };
+  d3_selectionPrototype.html = function(value) {
+    return arguments.length < 1 ? this.node().innerHTML : this.each(typeof value === "function" ? function() {
+      var v = value.apply(this, arguments);
+      this.innerHTML = v == null ? "" : v;
+    } : value == null ? function() {
+      this.innerHTML = "";
+    } : function() {
+      this.innerHTML = value;
+    });
+  };
+  d3_selectionPrototype.append = function(name) {
+    name = d3.ns.qualify(name);
+    function append() {
+      return this.appendChild(document.createElementNS(this.namespaceURI, name));
+    }
+    function appendNS() {
+      return this.appendChild(document.createElementNS(name.space, name.local));
+    }
+    return this.select(name.local ? appendNS : append);
+  };
+  d3_selectionPrototype.insert = function(name, before) {
+    name = d3.ns.qualify(name);
+    function insert() {
+      return this.insertBefore(document.createElementNS(this.namespaceURI, name), d3_select(before, this));
+    }
+    function insertNS() {
+      return this.insertBefore(document.createElementNS(name.space, name.local), d3_select(before, this));
+    }
+    return this.select(name.local ? insertNS : insert);
+  };
+  d3_selectionPrototype.remove = function() {
+    return this.each(function() {
+      var parent = this.parentNode;
+      if (parent) parent.removeChild(this);
+    });
+  };
+  d3_selectionPrototype.data = function(value, key) {
+    var i = -1, n = this.length, group, node;
+    if (!arguments.length) {
+      value = new Array(n = (group = this[0]).length);
+      while (++i < n) {
+        if (node = group[i]) {
+          value[i] = node.__data__;
+        }
+      }
+      return value;
+    }
+    function bind(group, groupData) {
+      var i, n = group.length, m = groupData.length, n0 = Math.min(n, m), n1 = Math.max(n, m), updateNodes = [], enterNodes = [], exitNodes = [], node, nodeData;
+      if (key) {
+        var nodeByKeyValue = new d3_Map, keyValues = [], keyValue, j = groupData.length;
+        for (i = -1; ++i < n; ) {
+          keyValue = key.call(node = group[i], node.__data__, i);
+          if (nodeByKeyValue.has(keyValue)) {
+            exitNodes[j++] = node;
+          } else {
+            nodeByKeyValue.set(keyValue, node);
+          }
+          keyValues.push(keyValue);
+        }
+        for (i = -1; ++i < m; ) {
+          keyValue = key.call(groupData, nodeData = groupData[i], i);
+          if (nodeByKeyValue.has(keyValue)) {
+            updateNodes[i] = node = nodeByKeyValue.get(keyValue);
+            node.__data__ = nodeData;
+            enterNodes[i] = exitNodes[i] = null;
+          } else {
+            enterNodes[i] = d3_selection_dataNode(nodeData);
+            updateNodes[i] = exitNodes[i] = null;
+          }
+          nodeByKeyValue.remove(keyValue);
+        }
+        for (i = -1; ++i < n; ) {
+          if (nodeByKeyValue.has(keyValues[i])) {
+            exitNodes[i] = group[i];
+          }
+        }
+      } else {
+        for (i = -1; ++i < n0; ) {
+          node = group[i];
+          nodeData = groupData[i];
+          if (node) {
+            node.__data__ = nodeData;
+            updateNodes[i] = node;
+            enterNodes[i] = exitNodes[i] = null;
+          } else {
+            enterNodes[i] = d3_selection_dataNode(nodeData);
+            updateNodes[i] = exitNodes[i] = null;
+          }
+        }
+        for (; i < m; ++i) {
+          enterNodes[i] = d3_selection_dataNode(groupData[i]);
+          updateNodes[i] = exitNodes[i] = null;
+        }
+        for (; i < n1; ++i) {
+          exitNodes[i] = group[i];
+          enterNodes[i] = updateNodes[i] = null;
+        }
+      }
+      enterNodes.update = updateNodes;
+      enterNodes.parentNode = updateNodes.parentNode = exitNodes.parentNode = group.parentNode;
+      enter.push(enterNodes);
+      update.push(updateNodes);
+      exit.push(exitNodes);
+    }
+    var enter = d3_selection_enter([]), update = d3_selection([]), exit = d3_selection([]);
+    if (typeof value === "function") {
+      while (++i < n) {
+        bind(group = this[i], value.call(group, group.parentNode.__data__, i));
+      }
+    } else {
+      while (++i < n) {
+        bind(group = this[i], value);
+      }
+    }
+    update.enter = function() {
+      return enter;
+    };
+    update.exit = function() {
+      return exit;
+    };
+    return update;
+  };
+  function d3_selection_dataNode(data) {
+    return {
+      __data__: data
+    };
+  }
+  d3_selectionPrototype.datum = d3_selectionPrototype.map = function(value) {
+    return arguments.length < 1 ? this.property("__data__") : this.property("__data__", value);
+  };
+  d3_selectionPrototype.filter = function(filter) {
+    var subgroups = [], subgroup, group, node;
+    if (typeof filter !== "function") filter = d3_selection_filter(filter);
+    for (var j = 0, m = this.length; j < m; j++) {
+      subgroups.push(subgroup = []);
+      subgroup.parentNode = (group = this[j]).parentNode;
+      for (var i = 0, n = group.length; i < n; i++) {
+        if ((node = group[i]) && filter.call(node, node.__data__, i)) {
+          subgroup.push(node);
+        }
+      }
+    }
+    return d3_selection(subgroups);
+  };
+  function d3_selection_filter(selector) {
+    return function() {
+      return d3_selectMatches(this, selector);
+    };
+  }
+  d3_selectionPrototype.order = function() {
+    for (var j = -1, m = this.length; ++j < m; ) {
+      for (var group = this[j], i = group.length - 1, next = group[i], node; --i >= 0; ) {
+        if (node = group[i]) {
+          if (next && next !== node.nextSibling) next.parentNode.insertBefore(node, next);
+          next = node;
+        }
+      }
+    }
+    return this;
+  };
+  d3_selectionPrototype.sort = function(comparator) {
+    comparator = d3_selection_sortComparator.apply(this, arguments);
+    for (var j = -1, m = this.length; ++j < m; ) this[j].sort(comparator);
+    return this.order();
+  };
+  function d3_selection_sortComparator(comparator) {
+    if (!arguments.length) comparator = d3.ascending;
+    return function(a, b) {
+      return comparator(a && a.__data__, b && b.__data__);
+    };
+  }
+  d3_selectionPrototype.on = function(type, listener, capture) {
+    var n = arguments.length;
+    if (n < 3) {
+      if (typeof type !== "string") {
+        if (n < 2) listener = false;
+        for (capture in type) this.each(d3_selection_on(capture, type[capture], listener));
+        return this;
+      }
+      if (n < 2) return (n = this.node()["__on" + type]) && n._;
+      capture = false;
+    }
+    return this.each(d3_selection_on(type, listener, capture));
+  };
+  function d3_selection_on(type, listener, capture) {
+    var name = "__on" + type, i = type.indexOf(".");
+    if (i > 0) type = type.substring(0, i);
+    function onRemove() {
+      var wrapper = this[name];
+      if (wrapper) {
+        this.removeEventListener(type, wrapper, wrapper.$);
+        delete this[name];
+      }
+    }
+    function onAdd() {
+      var node = this, args = arguments;
+      onRemove.call(this);
+      this.addEventListener(type, this[name] = wrapper, wrapper.$ = capture);
+      wrapper._ = listener;
+      function wrapper(e) {
+        var o = d3.event;
+        d3.event = e;
+        args[0] = node.__data__;
+        try {
+          listener.apply(node, args);
+        } finally {
+          d3.event = o;
+        }
+      }
+    }
+    return listener ? onAdd : onRemove;
+  }
+  d3_selectionPrototype.each = function(callback) {
+    return d3_selection_each(this, function(node, i, j) {
+      callback.call(node, node.__data__, i, j);
+    });
+  };
+  function d3_selection_each(groups, callback) {
+    for (var j = 0, m = groups.length; j < m; j++) {
+      for (var group = groups[j], i = 0, n = group.length, node; i < n; i++) {
+        if (node = group[i]) callback(node, i, j);
+      }
+    }
+    return groups;
+  }
+  d3_selectionPrototype.call = function(callback) {
+    callback.apply(this, (arguments[0] = this, arguments));
+    return this;
+  };
+  d3_selectionPrototype.empty = function() {
+    return !this.node();
+  };
+  d3_selectionPrototype.node = function(callback) {
+    for (var j = 0, m = this.length; j < m; j++) {
+      for (var group = this[j], i = 0, n = group.length; i < n; i++) {
+        var node = group[i];
+        if (node) return node;
+      }
+    }
+    return null;
+  };
+  d3_selectionPrototype.transition = function() {
+    var subgroups = [], subgroup, node;
+    for (var j = -1, m = this.length; ++j < m; ) {
+      subgroups.push(subgroup = []);
+      for (var group = this[j], i = -1, n = group.length; ++i < n; ) {
+        subgroup.push((node = group[i]) ? {
+          node: node,
+          delay: d3_transitionDelay,
+          duration: d3_transitionDuration
+        } : null);
+      }
+    }
+    return d3_transition(subgroups, d3_transitionId || ++d3_transitionNextId, Date.now());
+  };
+  var d3_selectionRoot = d3_selection([ [ document ] ]);
+  d3_selectionRoot[0].parentNode = d3_selectRoot;
+  d3.select = function(selector) {
+    return typeof selector === "string" ? d3_selectionRoot.select(selector) : d3_selection([ [ selector ] ]);
+  };
+  d3.selectAll = function(selector) {
+    return typeof selector === "string" ? d3_selectionRoot.selectAll(selector) : d3_selection([ d3_array(selector) ]);
+  };
+  function d3_selection_enter(selection) {
+    d3_arraySubclass(selection, d3_selection_enterPrototype);
+    return selection;
+  }
+  var d3_selection_enterPrototype = [];
+  d3.selection.enter = d3_selection_enter;
+  d3.selection.enter.prototype = d3_selection_enterPrototype;
+  d3_selection_enterPrototype.append = d3_selectionPrototype.append;
+  d3_selection_enterPrototype.insert = d3_selectionPrototype.insert;
+  d3_selection_enterPrototype.empty = d3_selectionPrototype.empty;
+  d3_selection_enterPrototype.node = d3_selectionPrototype.node;
+  d3_selection_enterPrototype.select = function(selector) {
+    var subgroups = [], subgroup, subnode, upgroup, group, node;
+    for (var j = -1, m = this.length; ++j < m; ) {
+      upgroup = (group = this[j]).update;
+      subgroups.push(subgroup = []);
+      subgroup.parentNode = group.parentNode;
+      for (var i = -1, n = group.length; ++i < n; ) {
+        if (node = group[i]) {
+          subgroup.push(upgroup[i] = subnode = selector.call(group.parentNode, node.__data__, i));
+          subnode.__data__ = node.__data__;
+        } else {
+          subgroup.push(null);
+        }
+      }
+    }
+    return d3_selection(subgroups);
+  };
+  function d3_transition(groups, id, time) {
+    d3_arraySubclass(groups, d3_transitionPrototype);
+    var tweens = new d3_Map, event = d3.dispatch("start", "end"), ease = d3_transitionEase;
+    groups.id = id;
+    groups.time = time;
+    groups.tween = function(name, tween) {
+      if (arguments.length < 2) return tweens.get(name);
+      if (tween == null) tweens.remove(name); else tweens.set(name, tween);
+      return groups;
+    };
+    groups.ease = function(value) {
+      if (!arguments.length) return ease;
+      ease = typeof value === "function" ? value : d3.ease.apply(d3, arguments);
+      return groups;
+    };
+    groups.each = function(type, listener) {
+      if (arguments.length < 2) return d3_transition_each.call(groups, type);
+      event.on(type, listener);
+      return groups;
+    };
+    d3.timer(function(elapsed) {
+      return d3_selection_each(groups, function(node, i, j) {
+        var tweened = [], delay = node.delay, duration = node.duration, lock = (node = node.node).__transition__ || (node.__transition__ = {
+          active: 0,
+          count: 0
+        }), d = node.__data__;
+        ++lock.count;
+        delay <= elapsed ? start(elapsed) : d3.timer(start, delay, time);
+        function start(elapsed) {
+          if (lock.active > id) return stop();
+          lock.active = id;
+          tweens.forEach(function(key, value) {
+            if (value = value.call(node, d, i)) {
+              tweened.push(value);
+            }
+          });
+          event.start.call(node, d, i);
+          if (!tick(elapsed)) d3.timer(tick, 0, time);
+          return 1;
+        }
+        function tick(elapsed) {
+          if (lock.active !== id) return stop();
+          var t = (elapsed - delay) / duration, e = ease(t), n = tweened.length;
+          while (n > 0) {
+            tweened[--n].call(node, e);
+          }
+          if (t >= 1) {
+            stop();
+            d3_transitionId = id;
+            event.end.call(node, d, i);
+            d3_transitionId = 0;
+            return 1;
+          }
+        }
+        function stop() {
+          if (!--lock.count) delete node.__transition__;
+          return 1;
+        }
+      });
+    }, 0, time);
+    return groups;
+  }
+  var d3_transitionPrototype = [], d3_transitionNextId = 0, d3_transitionId = 0, d3_transitionDefaultDelay = 0, d3_transitionDefaultDuration = 250, d3_transitionDefaultEase = d3.ease("cubic-in-out"), d3_transitionDelay = d3_transitionDefaultDelay, d3_transitionDuration = d3_transitionDefaultDuration, d3_transitionEase = d3_transitionDefaultEase;
+  d3_transitionPrototype.call = d3_selectionPrototype.call;
+  d3.transition = function(selection) {
+    return arguments.length ? d3_transitionId ? selection.transition() : selection : d3_selectionRoot.transition();
+  };
+  d3.transition.prototype = d3_transitionPrototype;
+  d3_transitionPrototype.select = function(selector) {
+    var subgroups = [], subgroup, subnode, node;
+    if (typeof selector !== "function") selector = d3_selection_selector(selector);
+    for (var j = -1, m = this.length; ++j < m; ) {
+      subgroups.push(subgroup = []);
+      for (var group = this[j], i = -1, n = group.length; ++i < n; ) {
+        if ((node = group[i]) && (subnode = selector.call(node.node, node.node.__data__, i))) {
+          if ("__data__" in node.node) subnode.__data__ = node.node.__data__;
+          subgroup.push({
+            node: subnode,
+            delay: node.delay,
+            duration: node.duration
+          });
+        } else {
+          subgroup.push(null);
+        }
+      }
+    }
+    return d3_transition(subgroups, this.id, this.time).ease(this.ease());
+  };
+  d3_transitionPrototype.selectAll = function(selector) {
+    var subgroups = [], subgroup, subnodes, node;
+    if (typeof selector !== "function") selector = d3_selection_selectorAll(selector);
+    for (var j = -1, m = this.length; ++j < m; ) {
+      for (var group = this[j], i = -1, n = group.length; ++i < n; ) {
+        if (node = group[i]) {
+          subnodes = selector.call(node.node, node.node.__data__, i);
+          subgroups.push(subgroup = []);
+          for (var k = -1, o = subnodes.length; ++k < o; ) {
+            subgroup.push({
+              node: subnodes[k],
+              delay: node.delay,
+              duration: node.duration
+            });
+          }
+        }
+      }
+    }
+    return d3_transition(subgroups, this.id, this.time).ease(this.ease());
+  };
+  d3_transitionPrototype.filter = function(filter) {
+    var subgroups = [], subgroup, group, node;
+    if (typeof filter !== "function") filter = d3_selection_filter(filter);
+    for (var j = 0, m = this.length; j < m; j++) {
+      subgroups.push(subgroup = []);
+      for (var group = this[j], i = 0, n = group.length; i < n; i++) {
+        if ((node = group[i]) && filter.call(node.node, node.node.__data__, i)) {
+          subgroup.push(node);
+        }
+      }
+    }
+    return d3_transition(subgroups, this.id, this.time).ease(this.ease());
+  };
+  d3_transitionPrototype.attr = function(name, value) {
+    if (arguments.length < 2) {
+      for (value in name) this.attrTween(value, d3_tweenByName(name[value], value));
+      return this;
+    }
+    return this.attrTween(name, d3_tweenByName(value, name));
+  };
+  d3_transitionPrototype.attrTween = function(nameNS, tween) {
+    var name = d3.ns.qualify(nameNS);
+    function attrTween(d, i) {
+      var f = tween.call(this, d, i, this.getAttribute(name));
+      return f === d3_tweenRemove ? (this.removeAttribute(name), null) : f && function(t) {
+        this.setAttribute(name, f(t));
+      };
+    }
+    function attrTweenNS(d, i) {
+      var f = tween.call(this, d, i, this.getAttributeNS(name.space, name.local));
+      return f === d3_tweenRemove ? (this.removeAttributeNS(name.space, name.local), null) : f && function(t) {
+        this.setAttributeNS(name.space, name.local, f(t));
+      };
+    }
+    return this.tween("attr." + nameNS, name.local ? attrTweenNS : attrTween);
+  };
+  d3_transitionPrototype.style = function(name, value, priority) {
+    var n = arguments.length;
+    if (n < 3) {
+      if (typeof name !== "string") {
+        if (n < 2) value = "";
+        for (priority in name) this.styleTween(priority, d3_tweenByName(name[priority], priority), value);
+        return this;
+      }
+      priority = "";
+    }
+    return this.styleTween(name, d3_tweenByName(value, name), priority);
+  };
+  d3_transitionPrototype.styleTween = function(name, tween, priority) {
+    if (arguments.length < 3) priority = "";
+    return this.tween("style." + name, function(d, i) {
+      var f = tween.call(this, d, i, window.getComputedStyle(this, null).getPropertyValue(name));
+      return f === d3_tweenRemove ? (this.style.removeProperty(name), null) : f && function(t) {
+        this.style.setProperty(name, f(t), priority);
+      };
+    });
+  };
+  d3_transitionPrototype.text = function(value) {
+    return this.tween("text", function(d, i) {
+      this.textContent = typeof value === "function" ? value.call(this, d, i) : value;
+    });
+  };
+  d3_transitionPrototype.remove = function() {
+    return this.each("end.transition", function() {
+      var p;
+      if (!this.__transition__ && (p = this.parentNode)) p.removeChild(this);
+    });
+  };
+  d3_transitionPrototype.delay = function(value) {
+    return d3_selection_each(this, typeof value === "function" ? function(node, i, j) {
+      node.delay = value.call(node = node.node, node.__data__, i, j) | 0;
+    } : (value = value | 0, function(node) {
+      node.delay = value;
+    }));
+  };
+  d3_transitionPrototype.duration = function(value) {
+    return d3_selection_each(this, typeof value === "function" ? function(node, i, j) {
+      node.duration = Math.max(1, value.call(node = node.node, node.__data__, i, j) | 0);
+    } : (value = Math.max(1, value | 0), function(node) {
+      node.duration = value;
+    }));
+  };
+  function d3_transition_each(callback) {
+    var id = d3_transitionId, ease = d3_transitionEase, delay = d3_transitionDelay, duration = d3_transitionDuration;
+    d3_transitionId = this.id;
+    d3_transitionEase = this.ease();
+    d3_selection_each(this, function(node, i, j) {
+      d3_transitionDelay = node.delay;
+      d3_transitionDuration = node.duration;
+      callback.call(node = node.node, node.__data__, i, j);
+    });
+    d3_transitionId = id;
+    d3_transitionEase = ease;
+    d3_transitionDelay = delay;
+    d3_transitionDuration = duration;
+    return this;
+  }
+  d3_transitionPrototype.transition = function() {
+    return this.select(d3_this);
+  };
+  d3.tween = function(b, interpolate) {
+    function tweenFunction(d, i, a) {
+      var v = b.call(this, d, i);
+      return v == null ? a != "" && d3_tweenRemove : a != v && interpolate(a, v);
+    }
+    function tweenString(d, i, a) {
+      return a != b && interpolate(a, b);
+    }
+    return typeof b === "function" ? tweenFunction : b == null ? d3_tweenNull : (b += "", tweenString);
+  };
+  var d3_tweenRemove = {};
+  function d3_tweenNull(d, i, a) {
+    return a != "" && d3_tweenRemove;
+  }
+  function d3_tweenByName(b, name) {
+    return d3.tween(b, d3_interpolateByName(name));
+  }
+  var d3_timer_queue = null, d3_timer_interval, d3_timer_timeout;
+  d3.timer = function(callback, delay, then) {
+    var found = false, t0, t1 = d3_timer_queue;
+    if (arguments.length < 3) {
+      if (arguments.length < 2) delay = 0; else if (!isFinite(delay)) return;
+      then = Date.now();
+    }
+    while (t1) {
+      if (t1.callback === callback) {
+        t1.then = then;
+        t1.delay = delay;
+        found = true;
+        break;
+      }
+      t0 = t1;
+      t1 = t1.next;
+    }
+    if (!found) d3_timer_queue = {
+      callback: callback,
+      then: then,
+      delay: delay,
+      next: d3_timer_queue
+    };
+    if (!d3_timer_interval) {
+      d3_timer_timeout = clearTimeout(d3_timer_timeout);
+      d3_timer_interval = 1;
+      d3_timer_frame(d3_timer_step);
+    }
+  };
+  function d3_timer_step() {
+    var elapsed, now = Date.now(), t1 = d3_timer_queue;
+    while (t1) {
+      elapsed = now - t1.then;
+      if (elapsed >= t1.delay) t1.flush = t1.callback(elapsed);
+      t1 = t1.next;
+    }
+    var delay = d3_timer_flush() - now;
+    if (delay > 24) {
+      if (isFinite(delay)) {
+        clearTimeout(d3_timer_timeout);
+        d3_timer_timeout = setTimeout(d3_timer_step, delay);
+      }
+      d3_timer_interval = 0;
+    } else {
+      d3_timer_interval = 1;
+      d3_timer_frame(d3_timer_step);
+    }
+  }
+  d3.timer.flush = function() {
+    var elapsed, now = Date.now(), t1 = d3_timer_queue;
+    while (t1) {
+      elapsed = now - t1.then;
+      if (!t1.delay) t1.flush = t1.callback(elapsed);
+      t1 = t1.next;
+    }
+    d3_timer_flush();
+  };
+  function d3_timer_flush() {
+    var t0 = null, t1 = d3_timer_queue, then = Infinity;
+    while (t1) {
+      if (t1.flush) {
+        t1 = t0 ? t0.next = t1.next : d3_timer_queue = t1.next;
+      } else {
+        then = Math.min(then, t1.then + t1.delay);
+        t1 = (t0 = t1).next;
+      }
+    }
+    return then;
+  }
+  var d3_timer_frame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(callback) {
+    setTimeout(callback, 17);
+  };
+  d3.mouse = function(container) {
+    return d3_mousePoint(container, d3_eventSource());
+  };
+  var d3_mouse_bug44083 = /WebKit/.test(navigator.userAgent) ? -1 : 0;
+  function d3_mousePoint(container, e) {
+    var svg = container.ownerSVGElement || container;
+    if (svg.createSVGPoint) {
+      var point = svg.createSVGPoint();
+      if (d3_mouse_bug44083 < 0 && (window.scrollX || window.scrollY)) {
+        svg = d3.select(document.body).append("svg").style("position", "absolute").style("top", 0).style("left", 0);
+        var ctm = svg[0][0].getScreenCTM();
+        d3_mouse_bug44083 = !(ctm.f || ctm.e);
+        svg.remove();
+      }
+      if (d3_mouse_bug44083) {
+        point.x = e.pageX;
+        point.y = e.pageY;
+      } else {
+        point.x = e.clientX;
+        point.y = e.clientY;
+      }
+      point = point.matrixTransform(container.getScreenCTM().inverse());
+      return [ point.x, point.y ];
+    }
+    var rect = container.getBoundingClientRect();
+    return [ e.clientX - rect.left - container.clientLeft, e.clientY - rect.top - container.clientTop ];
+  }
+  d3.touches = function(container, touches) {
+    if (arguments.length < 2) touches = d3_eventSource().touches;
+    return touches ? d3_array(touches).map(function(touch) {
+      var point = d3_mousePoint(container, touch);
+      point.identifier = touch.identifier;
+      return point;
+    }) : [];
+  };
+  function d3_noop() {}
+  d3.scale = {};
+  function d3_scaleExtent(domain) {
+    var start = domain[0], stop = domain[domain.length - 1];
+    return start < stop ? [ start, stop ] : [ stop, start ];
+  }
+  function d3_scaleRange(scale) {
+    return scale.rangeExtent ? scale.rangeExtent() : d3_scaleExtent(scale.range());
+  }
+  function d3_scale_nice(domain, nice) {
+    var i0 = 0, i1 = domain.length - 1, x0 = domain[i0], x1 = domain[i1], dx;
+    if (x1 < x0) {
+      dx = i0, i0 = i1, i1 = dx;
+      dx = x0, x0 = x1, x1 = dx;
+    }
+    if (nice = nice(x1 - x0)) {
+      domain[i0] = nice.floor(x0);
+      domain[i1] = nice.ceil(x1);
+    }
+    return domain;
+  }
+  function d3_scale_niceDefault() {
+    return Math;
+  }
+  d3.scale.linear = function() {
+    return d3_scale_linear([ 0, 1 ], [ 0, 1 ], d3.interpolate, false);
+  };
+  function d3_scale_linear(domain, range, interpolate, clamp) {
+    var output, input;
+    function rescale() {
+      var linear = Math.min(domain.length, range.length) > 2 ? d3_scale_polylinear : d3_scale_bilinear, uninterpolate = clamp ? d3_uninterpolateClamp : d3_uninterpolateNumber;
+      output = linear(domain, range, uninterpolate, interpolate);
+      input = linear(range, domain, uninterpolate, d3.interpolate);
+      return scale;
+    }
+    function scale(x) {
+      return output(x);
+    }
+    scale.invert = function(y) {
+      return input(y);
+    };
+    scale.domain = function(x) {
+      if (!arguments.length) return domain;
+      domain = x.map(Number);
+      return rescale();
+    };
+    scale.range = function(x) {
+      if (!arguments.length) return range;
+      range = x;
+      return rescale();
+    };
+    scale.rangeRound = function(x) {
+      return scale.range(x).interpolate(d3.interpolateRound);
+    };
+    scale.clamp = function(x) {
+      if (!arguments.length) return clamp;
+      clamp = x;
+      return rescale();
+    };
+    scale.interpolate = function(x) {
+      if (!arguments.length) return interpolate;
+      interpolate = x;
+      return rescale();
+    };
+    scale.ticks = function(m) {
+      return d3_scale_linearTicks(domain, m);
+    };
+    scale.tickFormat = function(m) {
+      return d3_scale_linearTickFormat(domain, m);
+    };
+    scale.nice = function() {
+      d3_scale_nice(domain, d3_scale_linearNice);
+      return rescale();
+    };
+    scale.copy = function() {
+      return d3_scale_linear(domain, range, interpolate, clamp);
+    };
+    return rescale();
+  }
+  function d3_scale_linearRebind(scale, linear) {
+    return d3.rebind(scale, linear, "range", "rangeRound", "interpolate", "clamp");
+  }
+  function d3_scale_linearNice(dx) {
+    dx = Math.pow(10, Math.round(Math.log(dx) / Math.LN10) - 1);
+    return dx && {
+      floor: function(x) {
+        return Math.floor(x / dx) * dx;
+      },
+      ceil: function(x) {
+        return Math.ceil(x / dx) * dx;
+      }
+    };
+  }
+  function d3_scale_linearTickRange(domain, m) {
+    var extent = d3_scaleExtent(domain), span = extent[1] - extent[0], step = Math.pow(10, Math.floor(Math.log(span / m) / Math.LN10)), err = m / span * step;
+    if (err <= .15) step *= 10; else if (err <= .35) step *= 5; else if (err <= .75) step *= 2;
+    extent[0] = Math.ceil(extent[0] / step) * step;
+    extent[1] = Math.floor(extent[1] / step) * step + step * .5;
+    extent[2] = step;
+    return extent;
+  }
+  function d3_scale_linearTicks(domain, m) {
+    return d3.range.apply(d3, d3_scale_linearTickRange(domain, m));
+  }
+  function d3_scale_linearTickFormat(domain, m) {
+    return d3.format(",." + Math.max(0, -Math.floor(Math.log(d3_scale_linearTickRange(domain, m)[2]) / Math.LN10 + .01)) + "f");
+  }
+  function d3_scale_bilinear(domain, range, uninterpolate, interpolate) {
+    var u = uninterpolate(domain[0], domain[1]), i = interpolate(range[0], range[1]);
+    return function(x) {
+      return i(u(x));
+    };
+  }
+  function d3_scale_polylinear(domain, range, uninterpolate, interpolate) {
+    var u = [], i = [], j = 0, k = Math.min(domain.length, range.length) - 1;
+    if (domain[k] < domain[0]) {
+      domain = domain.slice().reverse();
+      range = range.slice().reverse();
+    }
+    while (++j <= k) {
+      u.push(uninterpolate(domain[j - 1], domain[j]));
+      i.push(interpolate(range[j - 1], range[j]));
+    }
+    return function(x) {
+      var j = d3.bisect(domain, x, 1, k) - 1;
+      return i[j](u[j](x));
+    };
+  }
+  d3.scale.log = function() {
+    return d3_scale_log(d3.scale.linear(), d3_scale_logp);
+  };
+  function d3_scale_log(linear, log) {
+    var pow = log.pow;
+    function scale(x) {
+      return linear(log(x));
+    }
+    scale.invert = function(x) {
+      return pow(linear.invert(x));
+    };
+    scale.domain = function(x) {
+      if (!arguments.length) return linear.domain().map(pow);
+      log = x[0] < 0 ? d3_scale_logn : d3_scale_logp;
+      pow = log.pow;
+      linear.domain(x.map(log));
+      return scale;
+    };
+    scale.nice = function() {
+      linear.domain(d3_scale_nice(linear.domain(), d3_scale_niceDefault));
+      return scale;
+    };
+    scale.ticks = function() {
+      var extent = d3_scaleExtent(linear.domain()), ticks = [];
+      if (extent.every(isFinite)) {
+        var i = Math.floor(extent[0]), j = Math.ceil(extent[1]), u = pow(extent[0]), v = pow(extent[1]);
+        if (log === d3_scale_logn) {
+          ticks.push(pow(i));
+          for (; i++ < j; ) for (var k = 9; k > 0; k--) ticks.push(pow(i) * k);
+        } else {
+          for (; i < j; i++) for (var k = 1; k < 10; k++) ticks.push(pow(i) * k);
+          ticks.push(pow(i));
+        }
+        for (i = 0; ticks[i] < u; i++) {}
+        for (j = ticks.length; ticks[j - 1] > v; j--) {}
+        ticks = ticks.slice(i, j);
+      }
+      return ticks;
+    };
+    scale.tickFormat = function(n, format) {
+      if (arguments.length < 2) format = d3_scale_logFormat;
+      if (arguments.length < 1) return format;
+      var k = Math.max(.1, n / scale.ticks().length), f = log === d3_scale_logn ? (e = -1e-12, Math.floor) : (e = 1e-12, Math.ceil), e;
+      return function(d) {
+        return d / pow(f(log(d) + e)) <= k ? format(d) : "";
+      };
+    };
+    scale.copy = function() {
+      return d3_scale_log(linear.copy(), log);
+    };
+    return d3_scale_linearRebind(scale, linear);
+  }
+  var d3_scale_logFormat = d3.format(".0e");
+  function d3_scale_logp(x) {
+    return Math.log(x < 0 ? 0 : x) / Math.LN10;
+  }
+  function d3_scale_logn(x) {
+    return -Math.log(x > 0 ? 0 : -x) / Math.LN10;
+  }
+  d3_scale_logp.pow = function(x) {
+    return Math.pow(10, x);
+  };
+  d3_scale_logn.pow = function(x) {
+    return -Math.pow(10, -x);
+  };
+  d3.scale.pow = function() {
+    return d3_scale_pow(d3.scale.linear(), 1);
+  };
+  function d3_scale_pow(linear, exponent) {
+    var powp = d3_scale_powPow(exponent), powb = d3_scale_powPow(1 / exponent);
+    function scale(x) {
+      return linear(powp(x));
+    }
+    scale.invert = function(x) {
+      return powb(linear.invert(x));
+    };
+    scale.domain = function(x) {
+      if (!arguments.length) return linear.domain().map(powb);
+      linear.domain(x.map(powp));
+      return scale;
+    };
+    scale.ticks = function(m) {
+      return d3_scale_linearTicks(scale.domain(), m);
+    };
+    scale.tickFormat = function(m) {
+      return d3_scale_linearTickFormat(scale.domain(), m);
+    };
+    scale.nice = function() {
+      return scale.domain(d3_scale_nice(scale.domain(), d3_scale_linearNice));
+    };
+    scale.exponent = function(x) {
+      if (!arguments.length) return exponent;
+      var domain = scale.domain();
+      powp = d3_scale_powPow(exponent = x);
+      powb = d3_scale_powPow(1 / exponent);
+      return scale.domain(domain);
+    };
+    scale.copy = function() {
+      return d3_scale_pow(linear.copy(), exponent);
+    };
+    return d3_scale_linearRebind(scale, linear);
+  }
+  function d3_scale_powPow(e) {
+    return function(x) {
+      return x < 0 ? -Math.pow(-x, e) : Math.pow(x, e);
+    };
+  }
+  d3.scale.sqrt = function() {
+    return d3.scale.pow().exponent(.5);
+  };
+  d3.scale.ordinal = function() {
+    return d3_scale_ordinal([], {
+      t: "range",
+      a: [ [] ]
+    });
+  };
+  function d3_scale_ordinal(domain, ranger) {
+    var index, range, rangeBand;
+    function scale(x) {
+      return range[((index.get(x) || index.set(x, domain.push(x))) - 1) % range.length];
+    }
+    function steps(start, step) {
+      return d3.range(domain.length).map(function(i) {
+        return start + step * i;
+      });
+    }
+    scale.domain = function(x) {
+      if (!arguments.length) return domain;
+      domain = [];
+      index = new d3_Map;
+      var i = -1, n = x.length, xi;
+      while (++i < n) if (!index.has(xi = x[i])) index.set(xi, domain.push(xi));
+      return scale[ranger.t].apply(scale, ranger.a);
+    };
+    scale.range = function(x) {
+      if (!arguments.length) return range;
+      range = x;
+      rangeBand = 0;
+      ranger = {
+        t: "range",
+        a: arguments
+      };
+      return scale;
+    };
+    scale.rangePoints = function(x, padding) {
+      if (arguments.length < 2) padding = 0;
+      var start = x[0], stop = x[1], step = (stop - start) / (domain.length - 1 + padding);
+      range = steps(domain.length < 2 ? (start + stop) / 2 : start + step * padding / 2, step);
+      rangeBand = 0;
+      ranger = {
+        t: "rangePoints",
+        a: arguments
+      };
+      return scale;
+    };
+    scale.rangeBands = function(x, padding, outerPadding) {
+      if (arguments.length < 2) padding = 0;
+      if (arguments.length < 3) outerPadding = padding;
+      var reverse = x[1] < x[0], start = x[reverse - 0], stop = x[1 - reverse], step = (stop - start) / (domain.length - padding + 2 * outerPadding);
+      range = steps(start + step * outerPadding, step);
+      if (reverse) range.reverse();
+      rangeBand = step * (1 - padding);
+      ranger = {
+        t: "rangeBands",
+        a: arguments
+      };
+      return scale;
+    };
+    scale.rangeRoundBands = function(x, padding, outerPadding) {
+      if (arguments.length < 2) padding = 0;
+      if (arguments.length < 3) outerPadding = padding;
+      var reverse = x[1] < x[0], start = x[reverse - 0], stop = x[1 - reverse], step = Math.floor((stop - start) / (domain.length - padding + 2 * outerPadding)), error = stop - start - (domain.length - padding) * step;
+      range = steps(start + Math.round(error / 2), step);
+      if (reverse) range.reverse();
+      rangeBand = Math.round(step * (1 - padding));
+      ranger = {
+        t: "rangeRoundBands",
+        a: arguments
+      };
+      return scale;
+    };
+    scale.rangeBand = function() {
+      return rangeBand;
+    };
+    scale.rangeExtent = function() {
+      return d3_scaleExtent(ranger.a[0]);
+    };
+    scale.copy = function() {
+      return d3_scale_ordinal(domain, ranger);
+    };
+    return scale.domain(domain);
+  }
+  d3.scale.category10 = function() {
+    return d3.scale.ordinal().range(d3_category10);
+  };
+  d3.scale.category20 = function() {
+    return d3.scale.ordinal().range(d3_category20);
+  };
+  d3.scale.category20b = function() {
+    return d3.scale.ordinal().range(d3_category20b);
+  };
+  d3.scale.category20c = function() {
+    return d3.scale.ordinal().range(d3_category20c);
+  };
+  var d3_category10 = [ "#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf" ];
+  var d3_category20 = [ "#1f77b4", "#aec7e8", "#ff7f0e", "#ffbb78", "#2ca02c", "#98df8a", "#d62728", "#ff9896", "#9467bd", "#c5b0d5", "#8c564b", "#c49c94", "#e377c2", "#f7b6d2", "#7f7f7f", "#c7c7c7", "#bcbd22", "#dbdb8d", "#17becf", "#9edae5" ];
+  var d3_category20b = [ "#393b79", "#5254a3", "#6b6ecf", "#9c9ede", "#637939", "#8ca252", "#b5cf6b", "#cedb9c", "#8c6d31", "#bd9e39", "#e7ba52", "#e7cb94", "#843c39", "#ad494a", "#d6616b", "#e7969c", "#7b4173", "#a55194", "#ce6dbd", "#de9ed6" ];
+  var d3_category20c = [ "#3182bd", "#6baed6", "#9ecae1", "#c6dbef", "#e6550d", "#fd8d3c", "#fdae6b", "#fdd0a2", "#31a354", "#74c476", "#a1d99b", "#c7e9c0", "#756bb1", "#9e9ac8", "#bcbddc", "#dadaeb", "#636363", "#969696", "#bdbdbd", "#d9d9d9" ];
+  d3.scale.quantile = function() {
+    return d3_scale_quantile([], []);
+  };
+  function d3_scale_quantile(domain, range) {
+    var thresholds;
+    function rescale() {
+      var k = 0, n = domain.length, q = range.length;
+      thresholds = [];
+      while (++k < q) thresholds[k - 1] = d3.quantile(domain, k / q);
+      return scale;
+    }
+    function scale(x) {
+      if (isNaN(x = +x)) return NaN;
+      return range[d3.bisect(thresholds, x)];
+    }
+    scale.domain = function(x) {
+      if (!arguments.length) return domain;
+      domain = x.filter(function(d) {
+        return !isNaN(d);
+      }).sort(d3.ascending);
+      return rescale();
+    };
+    scale.range = function(x) {
+      if (!arguments.length) return range;
+      range = x;
+      return rescale();
+    };
+    scale.quantiles = function() {
+      return thresholds;
+    };
+    scale.copy = function() {
+      return d3_scale_quantile(domain, range);
+    };
+    return rescale();
+  }
+  d3.scale.quantize = function() {
+    return d3_scale_quantize(0, 1, [ 0, 1 ]);
+  };
+  function d3_scale_quantize(x0, x1, range) {
+    var kx, i;
+    function scale(x) {
+      return range[Math.max(0, Math.min(i, Math.floor(kx * (x - x0))))];
+    }
+    function rescale() {
+      kx = range.length / (x1 - x0);
+      i = range.length - 1;
+      return scale;
+    }
+    scale.domain = function(x) {
+      if (!arguments.length) return [ x0, x1 ];
+      x0 = +x[0];
+      x1 = +x[x.length - 1];
+      return rescale();
+    };
+    scale.range = function(x) {
+      if (!arguments.length) return range;
+      range = x;
+      return rescale();
+    };
+    scale.copy = function() {
+      return d3_scale_quantize(x0, x1, range);
+    };
+    return rescale();
+  }
+  d3.scale.threshold = function() {
+    return d3_scale_threshold([ .5 ], [ 0, 1 ]);
+  };
+  function d3_scale_threshold(domain, range) {
+    function scale(x) {
+      return range[d3.bisect(domain, x)];
+    }
+    scale.domain = function(_) {
+      if (!arguments.length) return domain;
+      domain = _;
+      return scale;
+    };
+    scale.range = function(_) {
+      if (!arguments.length) return range;
+      range = _;
+      return scale;
+    };
+    scale.copy = function() {
+      return d3_scale_threshold(domain, range);
+    };
+    return scale;
+  }
+  d3.scale.identity = function() {
+    return d3_scale_identity([ 0, 1 ]);
+  };
+  function d3_scale_identity(domain) {
+    function identity(x) {
+      return +x;
+    }
+    identity.invert = identity;
+    identity.domain = identity.range = function(x) {
+      if (!arguments.length) return domain;
+      domain = x.map(identity);
+      return identity;
+    };
+    identity.ticks = function(m) {
+      return d3_scale_linearTicks(domain, m);
+    };
+    identity.tickFormat = function(m) {
+      return d3_scale_linearTickFormat(domain, m);
+    };
+    identity.copy = function() {
+      return d3_scale_identity(domain);
+    };
+    return identity;
+  }
+  d3.svg = {};
+  d3.svg.arc = function() {
+    var innerRadius = d3_svg_arcInnerRadius, outerRadius = d3_svg_arcOuterRadius, startAngle = d3_svg_arcStartAngle, endAngle = d3_svg_arcEndAngle;
+    function arc() {
+      var r0 = innerRadius.apply(this, arguments), r1 = outerRadius.apply(this, arguments), a0 = startAngle.apply(this, arguments) + d3_svg_arcOffset, a1 = endAngle.apply(this, arguments) + d3_svg_arcOffset, da = (a1 < a0 && (da = a0, a0 = a1, a1 = da), a1 - a0), df = da < Math.PI ? "0" : "1", c0 = Math.cos(a0), s0 = Math.sin(a0), c1 = Math.cos(a1), s1 = Math.sin(a1);
+      return da >= d3_svg_arcMax ? r0 ? "M0," + r1 + "A" + r1 + "," + r1 + " 0 1,1 0," + -r1 + "A" + r1 + "," + r1 + " 0 1,1 0," + r1 + "M0," + r0 + "A" + r0 + "," + r0 + " 0 1,0 0," + -r0 + "A" + r0 + "," + r0 + " 0 1,0 0," + r0 + "Z" : "M0," + r1 + "A" + r1 + "," + r1 + " 0 1,1 0," + -r1 + "A" + r1 + "," + r1 + " 0 1,1 0," + r1 + "Z" : r0 ? "M" + r1 * c0 + "," + r1 * s0 + "A" + r1 + "," + r1 + " 0 " + df + ",1 " + r1 * c1 + "," + r1 * s1 + "L" + r0 * c1 + "," + r0 * s1 + "A" + r0 + "," + r0 + " 0 " + df + ",0 " + r0 * c0 + "," + r0 * s0 + "Z" : "M" + r1 * c0 + "," + r1 * s0 + "A" + r1 + "," + r1 + " 0 " + df + ",1 " + r1 * c1 + "," + r1 * s1 + "L0,0" + "Z";
+    }
+    arc.innerRadius = function(v) {
+      if (!arguments.length) return innerRadius;
+      innerRadius = d3_functor(v);
+      return arc;
+    };
+    arc.outerRadius = function(v) {
+      if (!arguments.length) return outerRadius;
+      outerRadius = d3_functor(v);
+      return arc;
+    };
+    arc.startAngle = function(v) {
+      if (!arguments.length) return startAngle;
+      startAngle = d3_functor(v);
+      return arc;
+    };
+    arc.endAngle = function(v) {
+      if (!arguments.length) return endAngle;
+      endAngle = d3_functor(v);
+      return arc;
+    };
+    arc.centroid = function() {
+      var r = (innerRadius.apply(this, arguments) + outerRadius.apply(this, arguments)) / 2, a = (startAngle.apply(this, arguments) + endAngle.apply(this, arguments)) / 2 + d3_svg_arcOffset;
+      return [ Math.cos(a) * r, Math.sin(a) * r ];
+    };
+    return arc;
+  };
+  var d3_svg_arcOffset = -Math.PI / 2, d3_svg_arcMax = 2 * Math.PI - 1e-6;
+  function d3_svg_arcInnerRadius(d) {
+    return d.innerRadius;
+  }
+  function d3_svg_arcOuterRadius(d) {
+    return d.outerRadius;
+  }
+  function d3_svg_arcStartAngle(d) {
+    return d.startAngle;
+  }
+  function d3_svg_arcEndAngle(d) {
+    return d.endAngle;
+  }
+  function d3_svg_line(projection) {
+    var x = d3_svg_lineX, y = d3_svg_lineY, defined = d3_true, interpolate = d3_svg_lineLinear, interpolateKey = interpolate.key, tension = .7;
+    function line(data) {
+      var segments = [], points = [], i = -1, n = data.length, d, fx = d3_functor(x), fy = d3_functor(y);
+      function segment() {
+        segments.push("M", interpolate(projection(points), tension));
+      }
+      while (++i < n) {
+        if (defined.call(this, d = data[i], i)) {
+          points.push([ +fx.call(this, d, i), +fy.call(this, d, i) ]);
+        } else if (points.length) {
+          segment();
+          points = [];
+        }
+      }
+      if (points.length) segment();
+      return segments.length ? segments.join("") : null;
+    }
+    line.x = function(_) {
+      if (!arguments.length) return x;
+      x = _;
+      return line;
+    };
+    line.y = function(_) {
+      if (!arguments.length) return y;
+      y = _;
+      return line;
+    };
+    line.defined = function(_) {
+      if (!arguments.length) return defined;
+      defined = _;
+      return line;
+    };
+    line.interpolate = function(_) {
+      if (!arguments.length) return interpolateKey;
+      if (typeof _ === "function") interpolateKey = interpolate = _; else interpolateKey = (interpolate = d3_svg_lineInterpolators.get(_) || d3_svg_lineLinear).key;
+      return line;
+    };
+    line.tension = function(_) {
+      if (!arguments.length) return tension;
+      tension = _;
+      return line;
+    };
+    return line;
+  }
+  d3.svg.line = function() {
+    return d3_svg_line(d3_identity);
+  };
+  function d3_svg_lineX(d) {
+    return d[0];
+  }
+  function d3_svg_lineY(d) {
+    return d[1];
+  }
+  var d3_svg_lineInterpolators = d3.map({
+    linear: d3_svg_lineLinear,
+    "linear-closed": d3_svg_lineLinearClosed,
+    "step-before": d3_svg_lineStepBefore,
+    "step-after": d3_svg_lineStepAfter,
+    basis: d3_svg_lineBasis,
+    "basis-open": d3_svg_lineBasisOpen,
+    "basis-closed": d3_svg_lineBasisClosed,
+    bundle: d3_svg_lineBundle,
+    cardinal: d3_svg_lineCardinal,
+    "cardinal-open": d3_svg_lineCardinalOpen,
+    "cardinal-closed": d3_svg_lineCardinalClosed,
+    monotone: d3_svg_lineMonotone
+  });
+  d3_svg_lineInterpolators.forEach(function(key, value) {
+    value.key = key;
+    value.closed = /-closed$/.test(key);
+  });
+  function d3_svg_lineLinear(points) {
+    return points.join("L");
+  }
+  function d3_svg_lineLinearClosed(points) {
+    return d3_svg_lineLinear(points) + "Z";
+  }
+  function d3_svg_lineStepBefore(points) {
+    var i = 0, n = points.length, p = points[0], path = [ p[0], ",", p[1] ];
+    while (++i < n) path.push("V", (p = points[i])[1], "H", p[0]);
+    return path.join("");
+  }
+  function d3_svg_lineStepAfter(points) {
+    var i = 0, n = points.length, p = points[0], path = [ p[0], ",", p[1] ];
+    while (++i < n) path.push("H", (p = points[i])[0], "V", p[1]);
+    return path.join("");
+  }
+  function d3_svg_lineCardinalOpen(points, tension) {
+    return points.length < 4 ? d3_svg_lineLinear(points) : points[1] + d3_svg_lineHermite(points.slice(1, points.length - 1), d3_svg_lineCardinalTangents(points, tension));
+  }
+  function d3_svg_lineCardinalClosed(points, tension) {
+    return points.length < 3 ? d3_svg_lineLinear(points) : points[0] + d3_svg_lineHermite((points.push(points[0]), points), d3_svg_lineCardinalTangents([ points[points.length - 2] ].concat(points, [ points[1] ]), tension));
+  }
+  function d3_svg_lineCardinal(points, tension, closed) {
+    return points.length < 3 ? d3_svg_lineLinear(points) : points[0] + d3_svg_lineHermite(points, d3_svg_lineCardinalTangents(points, tension));
+  }
+  function d3_svg_lineHermite(points, tangents) {
+    if (tangents.length < 1 || points.length != tangents.length && points.length != tangents.length + 2) {
+      return d3_svg_lineLinear(points);
+    }
+    var quad = points.length != tangents.length, path = "", p0 = points[0], p = points[1], t0 = tangents[0], t = t0, pi = 1;
+    if (quad) {
+      path += "Q" + (p[0] - t0[0] * 2 / 3) + "," + (p[1] - t0[1] * 2 / 3) + "," + p[0] + "," + p[1];
+      p0 = points[1];
+      pi = 2;
+    }
+    if (tangents.length > 1) {
+      t = tangents[1];
+      p = points[pi];
+      pi++;
+      path += "C" + (p0[0] + t0[0]) + "," + (p0[1] + t0[1]) + "," + (p[0] - t[0]) + "," + (p[1] - t[1]) + "," + p[0] + "," + p[1];
+      for (var i = 2; i < tangents.length; i++, pi++) {
+        p = points[pi];
+        t = tangents[i];
+        path += "S" + (p[0] - t[0]) + "," + (p[1] - t[1]) + "," + p[0] + "," + p[1];
+      }
+    }
+    if (quad) {
+      var lp = points[pi];
+      path += "Q" + (p[0] + t[0] * 2 / 3) + "," + (p[1] + t[1] * 2 / 3) + "," + lp[0] + "," + lp[1];
+    }
+    return path;
+  }
+  function d3_svg_lineCardinalTangents(points, tension) {
+    var tangents = [], a = (1 - tension) / 2, p0, p1 = points[0], p2 = points[1], i = 1, n = points.length;
+    while (++i < n) {
+      p0 = p1;
+      p1 = p2;
+      p2 = points[i];
+      tangents.push([ a * (p2[0] - p0[0]), a * (p2[1] - p0[1]) ]);
+    }
+    return tangents;
+  }
+  function d3_svg_lineBasis(points) {
+    if (points.length < 3) return d3_svg_lineLinear(points);
+    var i = 1, n = points.length, pi = points[0], x0 = pi[0], y0 = pi[1], px = [ x0, x0, x0, (pi = points[1])[0] ], py = [ y0, y0, y0, pi[1] ], path = [ x0, ",", y0 ];
+    d3_svg_lineBasisBezier(path, px, py);
+    while (++i < n) {
+      pi = points[i];
+      px.shift();
+      px.push(pi[0]);
+      py.shift();
+      py.push(pi[1]);
+      d3_svg_lineBasisBezier(path, px, py);
+    }
+    i = -1;
+    while (++i < 2) {
+      px.shift();
+      px.push(pi[0]);
+      py.shift();
+      py.push(pi[1]);
+      d3_svg_lineBasisBezier(path, px, py);
+    }
+    return path.join("");
+  }
+  function d3_svg_lineBasisOpen(points) {
+    if (points.length < 4) return d3_svg_lineLinear(points);
+    var path = [], i = -1, n = points.length, pi, px = [ 0 ], py = [ 0 ];
+    while (++i < 3) {
+      pi = points[i];
+      px.push(pi[0]);
+      py.push(pi[1]);
+    }
+    path.push(d3_svg_lineDot4(d3_svg_lineBasisBezier3, px) + "," + d3_svg_lineDot4(d3_svg_lineBasisBezier3, py));
+    --i;
+    while (++i < n) {
+      pi = points[i];
+      px.shift();
+      px.push(pi[0]);
+      py.shift();
+      py.push(pi[1]);
+      d3_svg_lineBasisBezier(path, px, py);
+    }
+    return path.join("");
+  }
+  function d3_svg_lineBasisClosed(points) {
+    var path, i = -1, n = points.length, m = n + 4, pi, px = [], py = [];
+    while (++i < 4) {
+      pi = points[i % n];
+      px.push(pi[0]);
+      py.push(pi[1]);
+    }
+    path = [ d3_svg_lineDot4(d3_svg_lineBasisBezier3, px), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, py) ];
+    --i;
+    while (++i < m) {
+      pi = points[i % n];
+      px.shift();
+      px.push(pi[0]);
+      py.shift();
+      py.push(pi[1]);
+      d3_svg_lineBasisBezier(path, px, py);
+    }
+    return path.join("");
+  }
+  function d3_svg_lineBundle(points, tension) {
+    var n = points.length - 1;
+    if (n) {
+      var x0 = points[0][0], y0 = points[0][1], dx = points[n][0] - x0, dy = points[n][1] - y0, i = -1, p, t;
+      while (++i <= n) {
+        p = points[i];
+        t = i / n;
+        p[0] = tension * p[0] + (1 - tension) * (x0 + t * dx);
+        p[1] = tension * p[1] + (1 - tension) * (y0 + t * dy);
+      }
+    }
+    return d3_svg_lineBasis(points);
+  }
+  function d3_svg_lineDot4(a, b) {
+    return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];
+  }
+  var d3_svg_lineBasisBezier1 = [ 0, 2 / 3, 1 / 3, 0 ], d3_svg_lineBasisBezier2 = [ 0, 1 / 3, 2 / 3, 0 ], d3_svg_lineBasisBezier3 = [ 0, 1 / 6, 2 / 3, 1 / 6 ];
+  function d3_svg_lineBasisBezier(path, x, y) {
+    path.push("C", d3_svg_lineDot4(d3_svg_lineBasisBezier1, x), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier1, y), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier2, x), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier2, y), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, x), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, y));
+  }
+  function d3_svg_lineSlope(p0, p1) {
+    return (p1[1] - p0[1]) / (p1[0] - p0[0]);
+  }
+  function d3_svg_lineFiniteDifferences(points) {
+    var i = 0, j = points.length - 1, m = [], p0 = points[0], p1 = points[1], d = m[0] = d3_svg_lineSlope(p0, p1);
+    while (++i < j) {
+      m[i] = (d + (d = d3_svg_lineSlope(p0 = p1, p1 = points[i + 1]))) / 2;
+    }
+    m[i] = d;
+    return m;
+  }
+  function d3_svg_lineMonotoneTangents(points) {
+    var tangents = [], d, a, b, s, m = d3_svg_lineFiniteDifferences(points), i = -1, j = points.length - 1;
+    while (++i < j) {
+      d = d3_svg_lineSlope(points[i], points[i + 1]);
+      if (Math.abs(d) < 1e-6) {
+        m[i] = m[i + 1] = 0;
+      } else {
+        a = m[i] / d;
+        b = m[i + 1] / d;
+        s = a * a + b * b;
+        if (s > 9) {
+          s = d * 3 / Math.sqrt(s);
+          m[i] = s * a;
+          m[i + 1] = s * b;
+        }
+      }
+    }
+    i = -1;
+    while (++i <= j) {
+      s = (points[Math.min(j, i + 1)][0] - points[Math.max(0, i - 1)][0]) / (6 * (1 + m[i] * m[i]));
+      tangents.push([ s || 0, m[i] * s || 0 ]);
+    }
+    return tangents;
+  }
+  function d3_svg_lineMonotone(points) {
+    return points.length < 3 ? d3_svg_lineLinear(points) : points[0] + d3_svg_lineHermite(points, d3_svg_lineMonotoneTangents(points));
+  }
+  d3.svg.line.radial = function() {
+    var line = d3_svg_line(d3_svg_lineRadial);
+    line.radius = line.x, delete line.x;
+    line.angle = line.y, delete line.y;
+    return line;
+  };
+  function d3_svg_lineRadial(points) {
+    var point, i = -1, n = points.length, r, a;
+    while (++i < n) {
+      point = points[i];
+      r = point[0];
+      a = point[1] + d3_svg_arcOffset;
+      point[0] = r * Math.cos(a);
+      point[1] = r * Math.sin(a);
+    }
+    return points;
+  }
+  function d3_svg_area(projection) {
+    var x0 = d3_svg_lineX, x1 = d3_svg_lineX, y0 = 0, y1 = d3_svg_lineY, defined = d3_true, interpolate = d3_svg_lineLinear, interpolateKey = interpolate.key, interpolateReverse = interpolate, L = "L", tension = .7;
+    function area(data) {
+      var segments = [], points0 = [], points1 = [], i = -1, n = data.length, d, fx0 = d3_functor(x0), fy0 = d3_functor(y0), fx1 = x0 === x1 ? function() {
+        return x;
+      } : d3_functor(x1), fy1 = y0 === y1 ? function() {
+        return y;
+      } : d3_functor(y1), x, y;
+      function segment() {
+        segments.push("M", interpolate(projection(points1), tension), L, interpolateReverse(projection(points0.reverse()), tension), "Z");
+      }
+      while (++i < n) {
+        if (defined.call(this, d = data[i], i)) {
+          points0.push([ x = +fx0.call(this, d, i), y = +fy0.call(this, d, i) ]);
+          points1.push([ +fx1.call(this, d, i), +fy1.call(this, d, i) ]);
+        } else if (points0.length) {
+          segment();
+          points0 = [];
+          points1 = [];
+        }
+      }
+      if (points0.length) segment();
+      return segments.length ? segments.join("") : null;
+    }
+    area.x = function(_) {
+      if (!arguments.length) return x1;
+      x0 = x1 = _;
+      return area;
+    };
+    area.x0 = function(_) {
+      if (!arguments.length) return x0;
+      x0 = _;
+      return area;
+    };
+    area.x1 = function(_) {
+      if (!arguments.length) return x1;
+      x1 = _;
+      return area;
+    };
+    area.y = function(_) {
+      if (!arguments.length) return y1;
+      y0 = y1 = _;
+      return area;
+    };
+    area.y0 = function(_) {
+      if (!arguments.length) return y0;
+      y0 = _;
+      return area;
+    };
+    area.y1 = function(_) {
+      if (!arguments.length) return y1;
+      y1 = _;
+      return area;
+    };
+    area.defined = function(_) {
+      if (!arguments.length) return defined;
+      defined = _;
+      return area;
+    };
+    area.interpolate = function(_) {
+      if (!arguments.length) return interpolateKey;
+      if (typeof _ === "function") interpolateKey = interpolate = _; else interpolateKey = (interpolate = d3_svg_lineInterpolators.get(_) || d3_svg_lineLinear).key;
+      interpolateReverse = interpolate.reverse || interpolate;
+      L = interpolate.closed ? "M" : "L";
+      return area;
+    };
+    area.tension = function(_) {
+      if (!arguments.length) return tension;
+      tension = _;
+      return area;
+    };
+    return area;
+  }
+  d3_svg_lineStepBefore.reverse = d3_svg_lineStepAfter;
+  d3_svg_lineStepAfter.reverse = d3_svg_lineStepBefore;
+  d3.svg.area = function() {
+    return d3_svg_area(d3_identity);
+  };
+  d3.svg.area.radial = function() {
+    var area = d3_svg_area(d3_svg_lineRadial);
+    area.radius = area.x, delete area.x;
+    area.innerRadius = area.x0, delete area.x0;
+    area.outerRadius = area.x1, delete area.x1;
+    area.angle = area.y, delete area.y;
+    area.startAngle = area.y0, delete area.y0;
+    area.endAngle = area.y1, delete area.y1;
+    return area;
+  };
+  d3.svg.chord = function() {
+    var source = d3_svg_chordSource, target = d3_svg_chordTarget, radius = d3_svg_chordRadius, startAngle = d3_svg_arcStartAngle, endAngle = d3_svg_arcEndAngle;
+    function chord(d, i) {
+      var s = subgroup(this, source, d, i), t = subgroup(this, target, d, i);
+      return "M" + s.p0 + arc(s.r, s.p1, s.a1 - s.a0) + (equals(s, t) ? curve(s.r, s.p1, s.r, s.p0) : curve(s.r, s.p1, t.r, t.p0) + arc(t.r, t.p1, t.a1 - t.a0) + curve(t.r, t.p1, s.r, s.p0)) + "Z";
+    }
+    function subgroup(self, f, d, i) {
+      var subgroup = f.call(self, d, i), r = radius.call(self, subgroup, i), a0 = startAngle.call(self, subgroup, i) + d3_svg_arcOffset, a1 = endAngle.call(self, subgroup, i) + d3_svg_arcOffset;
+      return {
+        r: r,
+        a0: a0,
+        a1: a1,
+        p0: [ r * Math.cos(a0), r * Math.sin(a0) ],
+        p1: [ r * Math.cos(a1), r * Math.sin(a1) ]
+      };
+    }
+    function equals(a, b) {
+      return a.a0 == b.a0 && a.a1 == b.a1;
+    }
+    function arc(r, p, a) {
+      return "A" + r + "," + r + " 0 " + +(a > Math.PI) + ",1 " + p;
+    }
+    function curve(r0, p0, r1, p1) {
+      return "Q 0,0 " + p1;
+    }
+    chord.radius = function(v) {
+      if (!arguments.length) return radius;
+      radius = d3_functor(v);
+      return chord;
+    };
+    chord.source = function(v) {
+      if (!arguments.length) return source;
+      source = d3_functor(v);
+      return chord;
+    };
+    chord.target = function(v) {
+      if (!arguments.length) return target;
+      target = d3_functor(v);
+      return chord;
+    };
+    chord.startAngle = function(v) {
+      if (!arguments.length) return startAngle;
+      startAngle = d3_functor(v);
+      return chord;
+    };
+    chord.endAngle = function(v) {
+      if (!arguments.length) return endAngle;
+      endAngle = d3_functor(v);
+      return chord;
+    };
+    return chord;
+  };
+  function d3_svg_chordSource(d) {
+    return d.source;
+  }
+  function d3_svg_chordTarget(d) {
+    return d.target;
+  }
+  function d3_svg_chordRadius(d) {
+    return d.radius;
+  }
+  function d3_svg_chordStartAngle(d) {
+    return d.startAngle;
+  }
+  function d3_svg_chordEndAngle(d) {
+    return d.endAngle;
+  }
+  d3.svg.diagonal = function() {
+    var source = d3_svg_chordSource, target = d3_svg_chordTarget, projection = d3_svg_diagonalProjection;
+    function diagonal(d, i) {
+      var p0 = source.call(this, d, i), p3 = target.call(this, d, i), m = (p0.y + p3.y) / 2, p = [ p0, {
+        x: p0.x,
+        y: m
+      }, {
+        x: p3.x,
+        y: m
+      }, p3 ];
+      p = p.map(projection);
+      return "M" + p[0] + "C" + p[1] + " " + p[2] + " " + p[3];
+    }
+    diagonal.source = function(x) {
+      if (!arguments.length) return source;
+      source = d3_functor(x);
+      return diagonal;
+    };
+    diagonal.target = function(x) {
+      if (!arguments.length) return target;
+      target = d3_functor(x);
+      return diagonal;
+    };
+    diagonal.projection = function(x) {
+      if (!arguments.length) return projection;
+      projection = x;
+      return diagonal;
+    };
+    return diagonal;
+  };
+  function d3_svg_diagonalProjection(d) {
+    return [ d.x, d.y ];
+  }
+  d3.svg.diagonal.radial = function() {
+    var diagonal = d3.svg.diagonal(), projection = d3_svg_diagonalProjection, projection_ = diagonal.projection;
+    diagonal.projection = function(x) {
+      return arguments.length ? projection_(d3_svg_diagonalRadialProjection(projection = x)) : projection;
+    };
+    return diagonal;
+  };
+  function d3_svg_diagonalRadialProjection(projection) {
+    return function() {
+      var d = projection.apply(this, arguments), r = d[0], a = d[1] + d3_svg_arcOffset;
+      return [ r * Math.cos(a), r * Math.sin(a) ];
+    };
+  }
+  d3.svg.mouse = d3.mouse;
+  d3.svg.touches = d3.touches;
+  d3.svg.symbol = function() {
+    var type = d3_svg_symbolType, size = d3_svg_symbolSize;
+    function symbol(d, i) {
+      return (d3_svg_symbols.get(type.call(this, d, i)) || d3_svg_symbolCircle)(size.call(this, d, i));
+    }
+    symbol.type = function(x) {
+      if (!arguments.length) return type;
+      type = d3_functor(x);
+      return symbol;
+    };
+    symbol.size = function(x) {
+      if (!arguments.length) return size;
+      size = d3_functor(x);
+      return symbol;
+    };
+    return symbol;
+  };
+  function d3_svg_symbolSize() {
+    return 64;
+  }
+  function d3_svg_symbolType() {
+    return "circle";
+  }
+  function d3_svg_symbolCircle(size) {
+    var r = Math.sqrt(size / Math.PI);
+    return "M0," + r + "A" + r + "," + r + " 0 1,1 0," + -r + "A" + r + "," + r + " 0 1,1 0," + r + "Z";
+  }
+  var d3_svg_symbols = d3.map({
+    circle: d3_svg_symbolCircle,
+    cross: function(size) {
+      var r = Math.sqrt(size / 5) / 2;
+      return "M" + -3 * r + "," + -r + "H" + -r + "V" + -3 * r + "H" + r + "V" + -r + "H" + 3 * r + "V" + r + "H" + r + "V" + 3 * r + "H" + -r + "V" + r + "H" + -3 * r + "Z";
+    },
+    diamond: function(size) {
+      var ry = Math.sqrt(size / (2 * d3_svg_symbolTan30)), rx = ry * d3_svg_symbolTan30;
+      return "M0," + -ry + "L" + rx + ",0" + " 0," + ry + " " + -rx + ",0" + "Z";
+    },
+    square: function(size) {
+      var r = Math.sqrt(size) / 2;
+      return "M" + -r + "," + -r + "L" + r + "," + -r + " " + r + "," + r + " " + -r + "," + r + "Z";
+    },
+    "triangle-down": function(size) {
+      var rx = Math.sqrt(size / d3_svg_symbolSqrt3), ry = rx * d3_svg_symbolSqrt3 / 2;
+      return "M0," + ry + "L" + rx + "," + -ry + " " + -rx + "," + -ry + "Z";
+    },
+    "triangle-up": function(size) {
+      var rx = Math.sqrt(size / d3_svg_symbolSqrt3), ry = rx * d3_svg_symbolSqrt3 / 2;
+      return "M0," + -ry + "L" + rx + "," + ry + " " + -rx + "," + ry + "Z";
+    }
+  });
+  d3.svg.symbolTypes = d3_svg_symbols.keys();
+  var d3_svg_symbolSqrt3 = Math.sqrt(3), d3_svg_symbolTan30 = Math.tan(30 * Math.PI / 180);
+  d3.svg.axis = function() {
+    var scale = d3.scale.linear(), orient = "bottom", tickMajorSize = 6, tickMinorSize = 6, tickEndSize = 6, tickPadding = 3, tickArguments_ = [ 10 ], tickValues = null, tickFormat_, tickSubdivide = 0;
+    function axis(g) {
+      g.each(function() {
+        var g = d3.select(this);
+        var ticks = tickValues == null ? scale.ticks ? scale.ticks.apply(scale, tickArguments_) : scale.domain() : tickValues, tickFormat = tickFormat_ == null ? scale.tickFormat ? scale.tickFormat.apply(scale, tickArguments_) : String : tickFormat_;
+        var subticks = d3_svg_axisSubdivide(scale, ticks, tickSubdivide), subtick = g.selectAll(".minor").data(subticks, String), subtickEnter = subtick.enter().insert("line", "g").attr("class", "tick minor").style("opacity", 1e-6), subtickExit = d3.transition(subtick.exit()).style("opacity", 1e-6).remove(), subtickUpdate = d3.transition(subtick).style("opacity", 1);
+        var tick = g.selectAll("g").data(ticks, String), tickEnter = tick.enter().insert("g", "path").style("opacity", 1e-6), tickExit = d3.transition(tick.exit()).style("opacity", 1e-6).remove(), tickUpdate = d3.transition(tick).style("opacity", 1), tickTransform;
+        var range = d3_scaleRange(scale), path = g.selectAll(".domain").data([ 0 ]), pathEnter = path.enter().append("path").attr("class", "domain"), pathUpdate = d3.transition(path);
+        var scale1 = scale.copy(), scale0 = this.__chart__ || scale1;
+        this.__chart__ = scale1;
+        tickEnter.append("line").attr("class", "tick");
+        tickEnter.append("text");
+        var lineEnter = tickEnter.select("line"), lineUpdate = tickUpdate.select("line"), text = tick.select("text").text(tickFormat), textEnter = tickEnter.select("text"), textUpdate = tickUpdate.select("text");
+        switch (orient) {
+         case "bottom":
+          {
+            tickTransform = d3_svg_axisX;
+            subtickEnter.attr("y2", tickMinorSize);
+            subtickUpdate.attr("x2", 0).attr("y2", tickMinorSize);
+            lineEnter.attr("y2", tickMajorSize);
+            textEnter.attr("y", Math.max(tickMajorSize, 0) + tickPadding);
+            lineUpdate.attr("x2", 0).attr("y2", tickMajorSize);
+            textUpdate.attr("x", 0).attr("y", Math.max(tickMajorSize, 0) + tickPadding);
+            text.attr("dy", ".71em").attr("text-anchor", "middle");
+            pathUpdate.attr("d", "M" + range[0] + "," + tickEndSize + "V0H" + range[1] + "V" + tickEndSize);
+            break;
+          }
+         case "top":
+          {
+            tickTransform = d3_svg_axisX;
+            subtickEnter.attr("y2", -tickMinorSize);
+            subtickUpdate.attr("x2", 0).attr("y2", -tickMinorSize);
+            lineEnter.attr("y2", -tickMajorSize);
+            textEnter.attr("y", -(Math.max(tickMajorSize, 0) + tickPadding));
+            lineUpdate.attr("x2", 0).attr("y2", -tickMajorSize);
+            textUpdate.attr("x", 0).attr("y", -(Math.max(tickMajorSize, 0) + tickPadding));
+            text.attr("dy", "0em").attr("text-anchor", "middle");
+            pathUpdate.attr("d", "M" + range[0] + "," + -tickEndSize + "V0H" + range[1] + "V" + -tickEndSize);
+            break;
+          }
+         case "left":
+          {
+            tickTransform = d3_svg_axisY;
+            subtickEnter.attr("x2", -tickMinorSize);
+            subtickUpdate.attr("x2", -tickMinorSize).attr("y2", 0);
+            lineEnter.attr("x2", -tickMajorSize);
+            textEnter.attr("x", -(Math.max(tickMajorSize, 0) + tickPadding));
+            lineUpdate.attr("x2", -tickMajorSize).attr("y2", 0);
+            textUpdate.attr("x", -(Math.max(tickMajorSize, 0) + tickPadding)).attr("y", 0);
+            text.attr("dy", ".32em").attr("text-anchor", "end");
+            pathUpdate.attr("d", "M" + -tickEndSize + "," + range[0] + "H0V" + range[1] + "H" + -tickEndSize);
+            break;
+          }
+         case "right":
+          {
+            tickTransform = d3_svg_axisY;
+            subtickEnter.attr("x2", tickMinorSize);
+            subtickUpdate.attr("x2", tickMinorSize).attr("y2", 0);
+            lineEnter.attr("x2", tickMajorSize);
+            textEnter.attr("x", Math.max(tickMajorSize, 0) + tickPadding);
+            lineUpdate.attr("x2", tickMajorSize).attr("y2", 0);
+            textUpdate.attr("x", Math.max(tickMajorSize, 0) + tickPadding).attr("y", 0);
+            text.attr("dy", ".32em").attr("text-anchor", "start");
+            pathUpdate.attr("d", "M" + tickEndSize + "," + range[0] + "H0V" + range[1] + "H" + tickEndSize);
+            break;
+          }
+        }
+        if (scale.ticks) {
+          tickEnter.call(tickTransform, scale0);
+          tickUpdate.call(tickTransform, scale1);
+          tickExit.call(tickTransform, scale1);
+          subtickEnter.call(tickTransform, scale0);
+          subtickUpdate.call(tickTransform, scale1);
+          subtickExit.call(tickTransform, scale1);
+        } else {
+          var dx = scale1.rangeBand() / 2, x = function(d) {
+            return scale1(d) + dx;
+          };
+          tickEnter.call(tickTransform, x);
+          tickUpdate.call(tickTransform, x);
+        }
+      });
+    }
+    axis.scale = function(x) {
+      if (!arguments.length) return scale;
+      scale = x;
+      return axis;
+    };
+    axis.orient = function(x) {
+      if (!arguments.length) return orient;
+      orient = x;
+      return axis;
+    };
+    axis.ticks = function() {
+      if (!arguments.length) return tickArguments_;
+      tickArguments_ = arguments;
+      return axis;
+    };
+    axis.tickValues = function(x) {
+      if (!arguments.length) return tickValues;
+      tickValues = x;
+      return axis;
+    };
+    axis.tickFormat = function(x) {
+      if (!arguments.length) return tickFormat_;
+      tickFormat_ = x;
+      return axis;
+    };
+    axis.tickSize = function(x, y, z) {
+      if (!arguments.length) return tickMajorSize;
+      var n = arguments.length - 1;
+      tickMajorSize = +x;
+      tickMinorSize = n > 1 ? +y : tickMajorSize;
+      tickEndSize = n > 0 ? +arguments[n] : tickMajorSize;
+      return axis;
+    };
+    axis.tickPadding = function(x) {
+      if (!arguments.length) return tickPadding;
+      tickPadding = +x;
+      return axis;
+    };
+    axis.tickSubdivide = function(x) {
+      if (!arguments.length) return tickSubdivide;
+      tickSubdivide = +x;
+      return axis;
+    };
+    return axis;
+  };
+  function d3_svg_axisX(selection, x) {
+    selection.attr("transform", function(d) {
+      return "translate(" + x(d) + ",0)";
+    });
+  }
+  function d3_svg_axisY(selection, y) {
+    selection.attr("transform", function(d) {
+      return "translate(0," + y(d) + ")";
+    });
+  }
+  function d3_svg_axisSubdivide(scale, ticks, m) {
+    subticks = [];
+    if (m && ticks.length > 1) {
+      var extent = d3_scaleExtent(scale.domain()), subticks, i = -1, n = ticks.length, d = (ticks[1] - ticks[0]) / ++m, j, v;
+      while (++i < n) {
+        for (j = m; --j > 0; ) {
+          if ((v = +ticks[i] - j * d) >= extent[0]) {
+            subticks.push(v);
+          }
+        }
+      }
+      for (--i, j = 0; ++j < m && (v = +ticks[i] + j * d) < extent[1]; ) {
+        subticks.push(v);
+      }
+    }
+    return subticks;
+  }
+  d3.svg.brush = function() {
+    var event = d3_eventDispatch(brush, "brushstart", "brush", "brushend"), x = null, y = null, resizes = d3_svg_brushResizes[0], extent = [ [ 0, 0 ], [ 0, 0 ] ], extentDomain;
+    function brush(g) {
+      g.each(function() {
+        var g = d3.select(this), bg = g.selectAll(".background").data([ 0 ]), fg = g.selectAll(".extent").data([ 0 ]), tz = g.selectAll(".resize").data(resizes, String), e;
+        g.style("pointer-events", "all").on("mousedown.brush", brushstart).on("touchstart.brush", brushstart);
+        bg.enter().append("rect").attr("class", "background").style("visibility", "hidden").style("cursor", "crosshair");
+        fg.enter().append("rect").attr("class", "extent").style("cursor", "move");
+        tz.enter().append("g").attr("class", function(d) {
+          return "resize " + d;
+        }).style("cursor", function(d) {
+          return d3_svg_brushCursor[d];
+        }).append("rect").attr("x", function(d) {
+          return /[ew]$/.test(d) ? -3 : null;
+        }).attr("y", function(d) {
+          return /^[ns]/.test(d) ? -3 : null;
+        }).attr("width", 6).attr("height", 6).style("visibility", "hidden");
+        tz.style("display", brush.empty() ? "none" : null);
+        tz.exit().remove();
+        if (x) {
+          e = d3_scaleRange(x);
+          bg.attr("x", e[0]).attr("width", e[1] - e[0]);
+          redrawX(g);
+        }
+        if (y) {
+          e = d3_scaleRange(y);
+          bg.attr("y", e[0]).attr("height", e[1] - e[0]);
+          redrawY(g);
+        }
+        redraw(g);
+      });
+    }
+    function redraw(g) {
+      g.selectAll(".resize").attr("transform", function(d) {
+        return "translate(" + extent[+/e$/.test(d)][0] + "," + extent[+/^s/.test(d)][1] + ")";
+      });
+    }
+    function redrawX(g) {
+      g.select(".extent").attr("x", extent[0][0]);
+      g.selectAll(".extent,.n>rect,.s>rect").attr("width", extent[1][0] - extent[0][0]);
+    }
+    function redrawY(g) {
+      g.select(".extent").attr("y", extent[0][1]);
+      g.selectAll(".extent,.e>rect,.w>rect").attr("height", extent[1][1] - extent[0][1]);
+    }
+    function brushstart() {
+      var target = this, eventTarget = d3.select(d3.event.target), event_ = event.of(target, arguments), g = d3.select(target), resizing = eventTarget.datum(), resizingX = !/^(n|s)$/.test(resizing) && x, resizingY = !/^(e|w)$/.test(resizing) && y, dragging = eventTarget.classed("extent"), center, origin = mouse(), offset;
+      var w = d3.select(window).on("mousemove.brush", brushmove).on("mouseup.brush", brushend).on("touchmove.brush", brushmove).on("touchend.brush", brushend).on("keydown.brush", keydown).on("keyup.brush", keyup);
+      if (dragging) {
+        origin[0] = extent[0][0] - origin[0];
+        origin[1] = extent[0][1] - origin[1];
+      } else if (resizing) {
+        var ex = +/w$/.test(resizing), ey = +/^n/.test(resizing);
+        offset = [ extent[1 - ex][0] - origin[0], extent[1 - ey][1] - origin[1] ];
+        origin[0] = extent[ex][0];
+        origin[1] = extent[ey][1];
+      } else if (d3.event.altKey) center = origin.slice();
+      g.style("pointer-events", "none").selectAll(".resize").style("display", null);
+      d3.select("body").style("cursor", eventTarget.style("cursor"));
+      event_({
+        type: "brushstart"
+      });
+      brushmove();
+      d3_eventCancel();
+      function mouse() {
+        var touches = d3.event.changedTouches;
+        return touches ? d3.touches(target, touches)[0] : d3.mouse(target);
+      }
+      function keydown() {
+        if (d3.event.keyCode == 32) {
+          if (!dragging) {
+            center = null;
+            origin[0] -= extent[1][0];
+            origin[1] -= extent[1][1];
+            dragging = 2;
+          }
+          d3_eventCancel();
+        }
+      }
+      function keyup() {
+        if (d3.event.keyCode == 32 && dragging == 2) {
+          origin[0] += extent[1][0];
+          origin[1] += extent[1][1];
+          dragging = 0;
+          d3_eventCancel();
+        }
+      }
+      function brushmove() {
+        var point = mouse(), moved = false;
+        if (offset) {
+          point[0] += offset[0];
+          point[1] += offset[1];
+        }
+        if (!dragging) {
+          if (d3.event.altKey) {
+            if (!center) center = [ (extent[0][0] + extent[1][0]) / 2, (extent[0][1] + extent[1][1]) / 2 ];
+            origin[0] = extent[+(point[0] < center[0])][0];
+            origin[1] = extent[+(point[1] < center[1])][1];
+          } else center = null;
+        }
+        if (resizingX && move1(point, x, 0)) {
+          redrawX(g);
+          moved = true;
+        }
+        if (resizingY && move1(point, y, 1)) {
+          redrawY(g);
+          moved = true;
+        }
+        if (moved) {
+          redraw(g);
+          event_({
+            type: "brush",
+            mode: dragging ? "move" : "resize"
+          });
+        }
+      }
+      function move1(point, scale, i) {
+        var range = d3_scaleRange(scale), r0 = range[0], r1 = range[1], position = origin[i], size = extent[1][i] - extent[0][i], min, max;
+        if (dragging) {
+          r0 -= position;
+          r1 -= size + position;
+        }
+        min = Math.max(r0, Math.min(r1, point[i]));
+        if (dragging) {
+          max = (min += position) + size;
+        } else {
+          if (center) position = Math.max(r0, Math.min(r1, 2 * center[i] - min));
+          if (position < min) {
+            max = min;
+            min = position;
+          } else {
+            max = position;
+          }
+        }
+        if (extent[0][i] !== min || extent[1][i] !== max) {
+          extentDomain = null;
+          extent[0][i] = min;
+          extent[1][i] = max;
+          return true;
+        }
+      }
+      function brushend() {
+        brushmove();
+        g.style("pointer-events", "all").selectAll(".resize").style("display", brush.empty() ? "none" : null);
+        d3.select("body").style("cursor", null);
+        w.on("mousemove.brush", null).on("mouseup.brush", null).on("touchmove.brush", null).on("touchend.brush", null).on("keydown.brush", null).on("keyup.brush", null);
+        event_({
+          type: "brushend"
+        });
+        d3_eventCancel();
+      }
+    }
+    brush.x = function(z) {
+      if (!arguments.length) return x;
+      x = z;
+      resizes = d3_svg_brushResizes[!x << 1 | !y];
+      return brush;
+    };
+    brush.y = function(z) {
+      if (!arguments.length) return y;
+      y = z;
+      resizes = d3_svg_brushResizes[!x << 1 | !y];
+      return brush;
+    };
+    brush.extent = function(z) {
+      var x0, x1, y0, y1, t;
+      if (!arguments.length) {
+        z = extentDomain || extent;
+        if (x) {
+          x0 = z[0][0], x1 = z[1][0];
+          if (!extentDomain) {
+            x0 = extent[0][0], x1 = extent[1][0];
+            if (x.invert) x0 = x.invert(x0), x1 = x.invert(x1);
+            if (x1 < x0) t = x0, x0 = x1, x1 = t;
+          }
+        }
+        if (y) {
+          y0 = z[0][1], y1 = z[1][1];
+          if (!extentDomain) {
+            y0 = extent[0][1], y1 = extent[1][1];
+            if (y.invert) y0 = y.invert(y0), y1 = y.invert(y1);
+            if (y1 < y0) t = y0, y0 = y1, y1 = t;
+          }
+        }
+        return x && y ? [ [ x0, y0 ], [ x1, y1 ] ] : x ? [ x0, x1 ] : y && [ y0, y1 ];
+      }
+      extentDomain = [ [ 0, 0 ], [ 0, 0 ] ];
+      if (x) {
+        x0 = z[0], x1 = z[1];
+        if (y) x0 = x0[0], x1 = x1[0];
+        extentDomain[0][0] = x0, extentDomain[1][0] = x1;
+        if (x.invert) x0 = x(x0), x1 = x(x1);
+        if (x1 < x0) t = x0, x0 = x1, x1 = t;
+        extent[0][0] = x0 | 0, extent[1][0] = x1 | 0;
+      }
+      if (y) {
+        y0 = z[0], y1 = z[1];
+        if (x) y0 = y0[1], y1 = y1[1];
+        extentDomain[0][1] = y0, extentDomain[1][1] = y1;
+        if (y.invert) y0 = y(y0), y1 = y(y1);
+        if (y1 < y0) t = y0, y0 = y1, y1 = t;
+        extent[0][1] = y0 | 0, extent[1][1] = y1 | 0;
+      }
+      return brush;
+    };
+    brush.clear = function() {
+      extentDomain = null;
+      extent[0][0] = extent[0][1] = extent[1][0] = extent[1][1] = 0;
+      return brush;
+    };
+    brush.empty = function() {
+      return x && extent[0][0] === extent[1][0] || y && extent[0][1] === extent[1][1];
+    };
+    return d3.rebind(brush, event, "on");
+  };
+  var d3_svg_brushCursor = {
+    n: "ns-resize",
+    e: "ew-resize",
+    s: "ns-resize",
+    w: "ew-resize",
+    nw: "nwse-resize",
+    ne: "nesw-resize",
+    se: "nwse-resize",
+    sw: "nesw-resize"
+  };
+  var d3_svg_brushResizes = [ [ "n", "e", "s", "w", "nw", "ne", "se", "sw" ], [ "e", "w" ], [ "n", "s" ], [] ];
+  d3.behavior = {};
+  d3.behavior.drag = function() {
+    var event = d3_eventDispatch(drag, "drag", "dragstart", "dragend"), origin = null;
+    function drag() {
+      this.on("mousedown.drag", mousedown).on("touchstart.drag", mousedown);
+    }
+    function mousedown() {
+      var target = this, event_ = event.of(target, arguments), eventTarget = d3.event.target, offset, origin_ = point(), moved = 0;
+      var w = d3.select(window).on("mousemove.drag", dragmove).on("touchmove.drag", dragmove).on("mouseup.drag", dragend, true).on("touchend.drag", dragend, true);
+      if (origin) {
+        offset = origin.apply(target, arguments);
+        offset = [ offset.x - origin_[0], offset.y - origin_[1] ];
+      } else {
+        offset = [ 0, 0 ];
+      }
+      d3_eventCancel();
+      event_({
+        type: "dragstart"
+      });
+      function point() {
+        var p = target.parentNode, t = d3.event.changedTouches;
+        return t ? d3.touches(p, t)[0] : d3.mouse(p);
+      }
+      function dragmove() {
+        if (!target.parentNode) return dragend();
+        var p = point(), dx = p[0] - origin_[0], dy = p[1] - origin_[1];
+        moved |= dx | dy;
+        origin_ = p;
+        d3_eventCancel();
+        event_({
+          type: "drag",
+          x: p[0] + offset[0],
+          y: p[1] + offset[1],
+          dx: dx,
+          dy: dy
+        });
+      }
+      function dragend() {
+        event_({
+          type: "dragend"
+        });
+        if (moved) {
+          d3_eventCancel();
+          if (d3.event.target === eventTarget) w.on("click.drag", click, true);
+        }
+        w.on("mousemove.drag", null).on("touchmove.drag", null).on("mouseup.drag", null).on("touchend.drag", null);
+      }
+      function click() {
+        d3_eventCancel();
+        w.on("click.drag", null);
+      }
+    }
+    drag.origin = function(x) {
+      if (!arguments.length) return origin;
+      origin = x;
+      return drag;
+    };
+    return d3.rebind(drag, event, "on");
+  };
+  d3.behavior.zoom = function() {
+    var translate = [ 0, 0 ], translate0, scale = 1, scale0, scaleExtent = d3_behavior_zoomInfinity, event = d3_eventDispatch(zoom, "zoom"), x0, x1, y0, y1, touchtime;
+    function zoom() {
+      this.on("mousedown.zoom", mousedown).on("mousewheel.zoom", mousewheel).on("mousemove.zoom", mousemove).on("DOMMouseScroll.zoom", mousewheel).on("dblclick.zoom", dblclick).on("touchstart.zoom", touchstart).on("touchmove.zoom", touchmove).on("touchend.zoom", touchstart);
+    }
+    zoom.translate = function(x) {
+      if (!arguments.length) return translate;
+      translate = x.map(Number);
+      return zoom;
+    };
+    zoom.scale = function(x) {
+      if (!arguments.length) return scale;
+      scale = +x;
+      return zoom;
+    };
+    zoom.scaleExtent = function(x) {
+      if (!arguments.length) return scaleExtent;
+      scaleExtent = x == null ? d3_behavior_zoomInfinity : x.map(Number);
+      return zoom;
+    };
+    zoom.x = function(z) {
+      if (!arguments.length) return x1;
+      x1 = z;
+      x0 = z.copy();
+      return zoom;
+    };
+    zoom.y = function(z) {
+      if (!arguments.length) return y1;
+      y1 = z;
+      y0 = z.copy();
+      return zoom;
+    };
+    function location(p) {
+      return [ (p[0] - translate[0]) / scale, (p[1] - translate[1]) / scale ];
+    }
+    function point(l) {
+      return [ l[0] * scale + translate[0], l[1] * scale + translate[1] ];
+    }
+    function scaleTo(s) {
+      scale = Math.max(scaleExtent[0], Math.min(scaleExtent[1], s));
+    }
+    function translateTo(p, l) {
+      l = point(l);
+      translate[0] += p[0] - l[0];
+      translate[1] += p[1] - l[1];
+    }
+    function dispatch(event) {
+      if (x1) x1.domain(x0.range().map(function(x) {
+        return (x - translate[0]) / scale;
+      }).map(x0.invert));
+      if (y1) y1.domain(y0.range().map(function(y) {
+        return (y - translate[1]) / scale;
+      }).map(y0.invert));
+      d3.event.preventDefault();
+      event({
+        type: "zoom",
+        scale: scale,
+        translate: translate
+      });
+    }
+    function mousedown() {
+      var target = this, event_ = event.of(target, arguments), eventTarget = d3.event.target, moved = 0, w = d3.select(window).on("mousemove.zoom", mousemove).on("mouseup.zoom", mouseup), l = location(d3.mouse(target));
+      window.focus();
+      d3_eventCancel();
+      function mousemove() {
+        moved = 1;
+        translateTo(d3.mouse(target), l);
+        dispatch(event_);
+      }
+      function mouseup() {
+        if (moved) d3_eventCancel();
+        w.on("mousemove.zoom", null).on("mouseup.zoom", null);
+        if (moved && d3.event.target === eventTarget) w.on("click.zoom", click, true);
+      }
+      function click() {
+        d3_eventCancel();
+        w.on("click.zoom", null);
+      }
+    }
+    function mousewheel() {
+      if (!translate0) translate0 = location(d3.mouse(this));
+      scaleTo(Math.pow(2, d3_behavior_zoomDelta() * .002) * scale);
+      translateTo(d3.mouse(this), translate0);
+      dispatch(event.of(this, arguments));
+    }
+    function mousemove() {
+      translate0 = null;
+    }
+    function dblclick() {
+      var p = d3.mouse(this), l = location(p);
+      scaleTo(d3.event.shiftKey ? scale / 2 : scale * 2);
+      translateTo(p, l);
+      dispatch(event.of(this, arguments));
+    }
+    function touchstart() {
+      var touches = d3.touches(this), now = Date.now();
+      scale0 = scale;
+      translate0 = {};
+      touches.forEach(function(t) {
+        translate0[t.identifier] = location(t);
+      });
+      d3_eventCancel();
+      if (touches.length === 1) {
+        if (now - touchtime < 500) {
+          var p = touches[0], l = location(touches[0]);
+          scaleTo(scale * 2);
+          translateTo(p, l);
+          dispatch(event.of(this, arguments));
+        }
+        touchtime = now;
+      }
+    }
+    function touchmove() {
+      var touches = d3.touches(this), p0 = touches[0], l0 = translate0[p0.identifier];
+      if (p1 = touches[1]) {
+        var p1, l1 = translate0[p1.identifier];
+        p0 = [ (p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2 ];
+        l0 = [ (l0[0] + l1[0]) / 2, (l0[1] + l1[1]) / 2 ];
+        scaleTo(d3.event.scale * scale0);
+      }
+      translateTo(p0, l0);
+      touchtime = null;
+      dispatch(event.of(this, arguments));
+    }
+    return d3.rebind(zoom, event, "on");
+  };
+  var d3_behavior_zoomDiv, d3_behavior_zoomInfinity = [ 0, Infinity ];
+  function d3_behavior_zoomDelta() {
+    if (!d3_behavior_zoomDiv) {
+      d3_behavior_zoomDiv = d3.select("body").append("div").style("visibility", "hidden").style("top", 0).style("height", 0).style("width", 0).style("overflow-y", "scroll").append("div").style("height", "2000px").node().parentNode;
+    }
+    var e = d3.event, delta;
+    try {
+      d3_behavior_zoomDiv.scrollTop = 1e3;
+      d3_behavior_zoomDiv.dispatchEvent(e);
+      delta = 1e3 - d3_behavior_zoomDiv.scrollTop;
+    } catch (error) {
+      delta = e.wheelDelta || -e.detail * 5;
+    }
+    return delta;
+  }
+  d3.layout = {};
+  d3.layout.bundle = function() {
+    return function(links) {
+      var paths = [], i = -1, n = links.length;
+      while (++i < n) paths.push(d3_layout_bundlePath(links[i]));
+      return paths;
+    };
+  };
+  function d3_layout_bundlePath(link) {
+    var start = link.source, end = link.target, lca = d3_layout_bundleLeastCommonAncestor(start, end), points = [ start ];
+    while (start !== lca) {
+      start = start.parent;
+      points.push(start);
+    }
+    var k = points.length;
+    while (end !== lca) {
+      points.splice(k, 0, end);
+      end = end.parent;
+    }
+    return points;
+  }
+  function d3_layout_bundleAncestors(node) {
+    var ancestors = [], parent = node.parent;
+    while (parent != null) {
+      ancestors.push(node);
+      node = parent;
+      parent = parent.parent;
+    }
+    ancestors.push(node);
+    return ancestors;
+  }
+  function d3_layout_bundleLeastCommonAncestor(a, b) {
+    if (a === b) return a;
+    var aNodes = d3_layout_bundleAncestors(a), bNodes = d3_layout_bundleAncestors(b), aNode = aNodes.pop(), bNode = bNodes.pop(), sharedNode = null;
+    while (aNode === bNode) {
+      sharedNode = aNode;
+      aNode = aNodes.pop();
+      bNode = bNodes.pop();
+    }
+    return sharedNode;
+  }
+  d3.layout.chord = function() {
+    var chord = {}, chords, groups, matrix, n, padding = 0, sortGroups, sortSubgroups, sortChords;
+    function relayout() {
+      var subgroups = {}, groupSums = [], groupIndex = d3.range(n), subgroupIndex = [], k, x, x0, i, j;
+      chords = [];
+      groups = [];
+      k = 0, i = -1;
+      while (++i < n) {
+        x = 0, j = -1;
+        while (++j < n) {
+          x += matrix[i][j];
+        }
+        groupSums.push(x);
+        subgroupIndex.push(d3.range(n));
+        k += x;
+      }
+      if (sortGroups) {
+        groupIndex.sort(function(a, b) {
+          return sortGroups(groupSums[a], groupSums[b]);
+        });
+      }
+      if (sortSubgroups) {
+        subgroupIndex.forEach(function(d, i) {
+          d.sort(function(a, b) {
+            return sortSubgroups(matrix[i][a], matrix[i][b]);
+          });
+        });
+      }
+      k = (2 * Math.PI - padding * n) / k;
+      x = 0, i = -1;
+      while (++i < n) {
+        x0 = x, j = -1;
+        while (++j < n) {
+          var di = groupIndex[i], dj = subgroupIndex[di][j], v = matrix[di][dj], a0 = x, a1 = x += v * k;
+          subgroups[di + "-" + dj] = {
+            index: di,
+            subindex: dj,
+            startAngle: a0,
+            endAngle: a1,
+            value: v
+          };
+        }
+        groups[di] = {
+          index: di,
+          startAngle: x0,
+          endAngle: x,
+          value: (x - x0) / k
+        };
+        x += padding;
+      }
+      i = -1;
+      while (++i < n) {
+        j = i - 1;
+        while (++j < n) {
+          var source = subgroups[i + "-" + j], target = subgroups[j + "-" + i];
+          if (source.value || target.value) {
+            chords.push(source.value < target.value ? {
+              source: target,
+              target: source
+            } : {
+              source: source,
+              target: target
+            });
+          }
+        }
+      }
+      if (sortChords) resort();
+    }
+    function resort() {
+      chords.sort(function(a, b) {
+        return sortChords((a.source.value + a.target.value) / 2, (b.source.value + b.target.value) / 2);
+      });
+    }
+    chord.matrix = function(x) {
+      if (!arguments.length) return matrix;
+      n = (matrix = x) && matrix.length;
+      chords = groups = null;
+      return chord;
+    };
+    chord.padding = function(x) {
+      if (!arguments.length) return padding;
+      padding = x;
+      chords = groups = null;
+      return chord;
+    };
+    chord.sortGroups = function(x) {
+      if (!arguments.length) return sortGroups;
+      sortGroups = x;
+      chords = groups = null;
+      return chord;
+    };
+    chord.sortSubgroups = function(x) {
+      if (!arguments.length) return sortSubgroups;
+      sortSubgroups = x;
+      chords = null;
+      return chord;
+    };
+    chord.sortChords = function(x) {
+      if (!arguments.length) return sortChords;
+      sortChords = x;
+      if (chords) resort();
+      return chord;
+    };
+    chord.chords = function() {
+      if (!chords) relayout();
+      return chords;
+    };
+    chord.groups = function() {
+      if (!groups) relayout();
+      return groups;
+    };
+    return chord;
+  };
+  d3.layout.force = function() {
+    var force = {}, event = d3.dispatch("start", "tick", "end"), size = [ 1, 1 ], drag, alpha, friction = .9, linkDistance = d3_layout_forceLinkDistance, linkStrength = d3_layout_forceLinkStrength, charge = -30, gravity = .1, theta = .8, interval, nodes = [], links = [], distances, strengths, charges;
+    function repulse(node) {
+      return function(quad, x1, y1, x2, y2) {
+        if (quad.point !== node) {
+          var dx = quad.cx - node.x, dy = quad.cy - node.y, dn = 1 / Math.sqrt(dx * dx + dy * dy);
+          if ((x2 - x1) * dn < theta) {
+            var k = quad.charge * dn * dn;
+            node.px -= dx * k;
+            node.py -= dy * k;
+            return true;
+          }
+          if (quad.point && isFinite(dn)) {
+            var k = quad.pointCharge * dn * dn;
+            node.px -= dx * k;
+            node.py -= dy * k;
+          }
+        }
+        return !quad.charge;
+      };
+    }
+    force.tick = function() {
+      if ((alpha *= .99) < .005) {
+        event.end({
+          type: "end",
+          alpha: alpha = 0
+        });
+        return true;
+      }
+      var n = nodes.length, m = links.length, q, i, o, s, t, l, k, x, y;
+      for (i = 0; i < m; ++i) {
+        o = links[i];
+        s = o.source;
+        t = o.target;
+        x = t.x - s.x;
+        y = t.y - s.y;
+        if (l = x * x + y * y) {
+          l = alpha * strengths[i] * ((l = Math.sqrt(l)) - distances[i]) / l;
+          x *= l;
+          y *= l;
+          t.x -= x * (k = s.weight / (t.weight + s.weight));
+          t.y -= y * k;
+          s.x += x * (k = 1 - k);
+          s.y += y * k;
+        }
+      }
+      if (k = alpha * gravity) {
+        x = size[0] / 2;
+        y = size[1] / 2;
+        i = -1;
+        if (k) while (++i < n) {
+          o = nodes[i];
+          o.x += (x - o.x) * k;
+          o.y += (y - o.y) * k;
+        }
+      }
+      if (charge) {
+        d3_layout_forceAccumulate(q = d3.geom.quadtree(nodes), alpha, charges);
+        i = -1;
+        while (++i < n) {
+          if (!(o = nodes[i]).fixed) {
+            q.visit(repulse(o));
+          }
+        }
+      }
+      i = -1;
+      while (++i < n) {
+        o = nodes[i];
+        if (o.fixed) {
+          o.x = o.px;
+          o.y = o.py;
+        } else {
+          o.x -= (o.px - (o.px = o.x)) * friction;
+          o.y -= (o.py - (o.py = o.y)) * friction;
+        }
+      }
+      event.tick({
+        type: "tick",
+        alpha: alpha
+      });
+    };
+    force.nodes = function(x) {
+      if (!arguments.length) return nodes;
+      nodes = x;
+      return force;
+    };
+    force.links = function(x) {
+      if (!arguments.length) return links;
+      links = x;
+      return force;
+    };
+    force.size = function(x) {
+      if (!arguments.length) return size;
+      size = x;
+      return force;
+    };
+    force.linkDistance = function(x) {
+      if (!arguments.length) return linkDistance;
+      linkDistance = d3_functor(x);
+      return force;
+    };
+    force.distance = force.linkDistance;
+    force.linkStrength = function(x) {
+      if (!arguments.length) return linkStrength;
+      linkStrength = d3_functor(x);
+      return force;
+    };
+    force.friction = function(x) {
+      if (!arguments.length) return friction;
+      friction = x;
+      return force;
+    };
+    force.charge = function(x) {
+      if (!arguments.length) return charge;
+      charge = typeof x === "function" ? x : +x;
+      return force;
+    };
+    force.gravity = function(x) {
+      if (!arguments.length) return gravity;
+      gravity = x;
+      return force;
+    };
+    force.theta = function(x) {
+      if (!arguments.length) return theta;
+      theta = x;
+      return force;
+    };
+    force.alpha = function(x) {
+      if (!arguments.length) return alpha;
+      if (alpha) {
+        if (x > 0) alpha = x; else alpha = 0;
+      } else if (x > 0) {
+        event.start({
+          type: "start",
+          alpha: alpha = x
+        });
+        d3.timer(force.tick);
+      }
+      return force;
+    };
+    force.start = function() {
+      var i, j, n = nodes.length, m = links.length, w = size[0], h = size[1], neighbors, o;
+      for (i = 0; i < n; ++i) {
+        (o = nodes[i]).index = i;
+        o.weight = 0;
+      }
+      distances = [];
+      strengths = [];
+      for (i = 0; i < m; ++i) {
+        o = links[i];
+        if (typeof o.source == "number") o.source = nodes[o.source];
+        if (typeof o.target == "number") o.target = nodes[o.target];
+        distances[i] = linkDistance.call(this, o, i);
+        strengths[i] = linkStrength.call(this, o, i);
+        ++o.source.weight;
+        ++o.target.weight;
+      }
+      for (i = 0; i < n; ++i) {
+        o = nodes[i];
+        if (isNaN(o.x)) o.x = position("x", w);
+        if (isNaN(o.y)) o.y = position("y", h);
+        if (isNaN(o.px)) o.px = o.x;
+        if (isNaN(o.py)) o.py = o.y;
+      }
+      charges = [];
+      if (typeof charge === "function") {
+        for (i = 0; i < n; ++i) {
+          charges[i] = +charge.call(this, nodes[i], i);
+        }
+      } else {
+        for (i = 0; i < n; ++i) {
+          charges[i] = charge;
+        }
+      }
+      function position(dimension, size) {
+        var neighbors = neighbor(i), j = -1, m = neighbors.length, x;
+        while (++j < m) if (!isNaN(x = neighbors[j][dimension])) return x;
+        return Math.random() * size;
+      }
+      function neighbor() {
+        if (!neighbors) {
+          neighbors = [];
+          for (j = 0; j < n; ++j) {
+            neighbors[j] = [];
+          }
+          for (j = 0; j < m; ++j) {
+            var o = links[j];
+            neighbors[o.source.index].push(o.target);
+            neighbors[o.target.index].push(o.source);
+          }
+        }
+        return neighbors[i];
+      }
+      return force.resume();
+    };
+    force.resume = function() {
+      return force.alpha(.1);
+    };
+    force.stop = function() {
+      return force.alpha(0);
+    };
+    force.drag = function() {
+      if (!drag) drag = d3.behavior.drag().origin(d3_identity).on("dragstart", dragstart).on("drag", d3_layout_forceDrag).on("dragend", d3_layout_forceDragEnd);
+      this.on("mouseover.force", d3_layout_forceDragOver).on("mouseout.force", d3_layout_forceDragOut).call(drag);
+    };
+    function dragstart(d) {
+      d3_layout_forceDragOver(d3_layout_forceDragNode = d);
+      d3_layout_forceDragForce = force;
+    }
+    return d3.rebind(force, event, "on");
+  };
+  var d3_layout_forceDragForce, d3_layout_forceDragNode;
+  function d3_layout_forceDragOver(d) {
+    d.fixed |= 2;
+  }
+  function d3_layout_forceDragOut(d) {
+    if (d !== d3_layout_forceDragNode) d.fixed &= 1;
+  }
+  function d3_layout_forceDragEnd() {
+    d3_layout_forceDragNode.fixed &= 1;
+    d3_layout_forceDragForce = d3_layout_forceDragNode = null;
+  }
+  function d3_layout_forceDrag() {
+    d3_layout_forceDragNode.px = d3.event.x;
+    d3_layout_forceDragNode.py = d3.event.y;
+    d3_layout_forceDragForce.resume();
+  }
+  function d3_layout_forceAccumulate(quad, alpha, charges) {
+    var cx = 0, cy = 0;
+    quad.charge = 0;
+    if (!quad.leaf) {
+      var nodes = quad.nodes, n = nodes.length, i = -1, c;
+      while (++i < n) {
+        c = nodes[i];
+        if (c == null) continue;
+        d3_layout_forceAccumulate(c, alpha, charges);
+        quad.charge += c.charge;
+        cx += c.charge * c.cx;
+        cy += c.charge * c.cy;
+      }
+    }
+    if (quad.point) {
+      if (!quad.leaf) {
+        quad.point.x += Math.random() - .5;
+        quad.point.y += Math.random() - .5;
+      }
+      var k = alpha * charges[quad.point.index];
+      quad.charge += quad.pointCharge = k;
+      cx += k * quad.point.x;
+      cy += k * quad.point.y;
+    }
+    quad.cx = cx / quad.charge;
+    quad.cy = cy / quad.charge;
+  }
+  function d3_layout_forceLinkDistance(link) {
+    return 20;
+  }
+  function d3_layout_forceLinkStrength(link) {
+    return 1;
+  }
+  d3.layout.partition = function() {
+    var hierarchy = d3.layout.hierarchy(), size = [ 1, 1 ];
+    function position(node, x, dx, dy) {
+      var children = node.children;
+      node.x = x;
+      node.y = node.depth * dy;
+      node.dx = dx;
+      node.dy = dy;
+      if (children && (n = children.length)) {
+        var i = -1, n, c, d;
+        dx = node.value ? dx / node.value : 0;
+        while (++i < n) {
+          position(c = children[i], x, d = c.value * dx, dy);
+          x += d;
+        }
+      }
+    }
+    function depth(node) {
+      var children = node.children, d = 0;
+      if (children && (n = children.length)) {
+        var i = -1, n;
+        while (++i < n) d = Math.max(d, depth(children[i]));
+      }
+      return 1 + d;
+    }
+    function partition(d, i) {
+      var nodes = hierarchy.call(this, d, i);
+      position(nodes[0], 0, size[0], size[1] / depth(nodes[0]));
+      return nodes;
+    }
+    partition.size = function(x) {
+      if (!arguments.length) return size;
+      size = x;
+      return partition;
+    };
+    return d3_layout_hierarchyRebind(partition, hierarchy);
+  };
+  d3.layout.pie = function() {
+    var value = Number, sort = d3_layout_pieSortByValue, startAngle = 0, endAngle = 2 * Math.PI;
+    function pie(data, i) {
+      var values = data.map(function(d, i) {
+        return +value.call(pie, d, i);
+      });
+      var a = +(typeof startAngle === "function" ? startAngle.apply(this, arguments) : startAngle);
+      var k = ((typeof endAngle === "function" ? endAngle.apply(this, arguments) : endAngle) - startAngle) / d3.sum(values);
+      var index = d3.range(data.length);
+      if (sort != null) index.sort(sort === d3_layout_pieSortByValue ? function(i, j) {
+        return values[j] - values[i];
+      } : function(i, j) {
+        return sort(data[i], data[j]);
+      });
+      var arcs = [];
+      index.forEach(function(i) {
+        var d;
+        arcs[i] = {
+          data: data[i],
+          value: d = values[i],
+          startAngle: a,
+          endAngle: a += d * k
+        };
+      });
+      return arcs;
+    }
+    pie.value = function(x) {
+      if (!arguments.length) return value;
+      value = x;
+      return pie;
+    };
+    pie.sort = function(x) {
+      if (!arguments.length) return sort;
+      sort = x;
+      return pie;
+    };
+    pie.startAngle = function(x) {
+      if (!arguments.length) return startAngle;
+      startAngle = x;
+      return pie;
+    };
+    pie.endAngle = function(x) {
+      if (!arguments.length) return endAngle;
+      endAngle = x;
+      return pie;
+    };
+    return pie;
+  };
+  var d3_layout_pieSortByValue = {};
+  d3.layout.stack = function() {
+    var values = d3_identity, order = d3_layout_stackOrderDefault, offset = d3_layout_stackOffsetZero, out = d3_layout_stackOut, x = d3_layout_stackX, y = d3_layout_stackY;
+    function stack(data, index) {
+      var series = data.map(function(d, i) {
+        return values.call(stack, d, i);
+      });
+      var points = series.map(function(d, i) {
+        return d.map(function(v, i) {
+          return [ x.call(stack, v, i), y.call(stack, v, i) ];
+        });
+      });
+      var orders = order.call(stack, points, index);
+      series = d3.permute(series, orders);
+      points = d3.permute(points, orders);
+      var offsets = offset.call(stack, points, index);
+      var n = series.length, m = series[0].length, i, j, o;
+      for (j = 0; j < m; ++j) {
+        out.call(stack, series[0][j], o = offsets[j], points[0][j][1]);
+        for (i = 1; i < n; ++i) {
+          out.call(stack, series[i][j], o += points[i - 1][j][1], points[i][j][1]);
+        }
+      }
+      return data;
+    }
+    stack.values = function(x) {
+      if (!arguments.length) return values;
+      values = x;
+      return stack;
+    };
+    stack.order = function(x) {
+      if (!arguments.length) return order;
+      order = typeof x === "function" ? x : d3_layout_stackOrders.get(x) || d3_layout_stackOrderDefault;
+      return stack;
+    };
+    stack.offset = function(x) {
+      if (!arguments.length) return offset;
+      offset = typeof x === "function" ? x : d3_layout_stackOffsets.get(x) || d3_layout_stackOffsetZero;
+      return stack;
+    };
+    stack.x = function(z) {
+      if (!arguments.length) return x;
+      x = z;
+      return stack;
+    };
+    stack.y = function(z) {
+      if (!arguments.length) return y;
+      y = z;
+      return stack;
+    };
+    stack.out = function(z) {
+      if (!arguments.length) return out;
+      out = z;
+      return stack;
+    };
+    return stack;
+  };
+  function d3_layout_stackX(d) {
+    return d.x;
+  }
+  function d3_layout_stackY(d) {
+    return d.y;
+  }
+  function d3_layout_stackOut(d, y0, y) {
+    d.y0 = y0;
+    d.y = y;
+  }
+  var d3_layout_stackOrders = d3.map({
+    "inside-out": function(data) {
+      var n = data.length, i, j, max = data.map(d3_layout_stackMaxIndex), sums = data.map(d3_layout_stackReduceSum), index = d3.range(n).sort(function(a, b) {
+        return max[a] - max[b];
+      }), top = 0, bottom = 0, tops = [], bottoms = [];
+      for (i = 0; i < n; ++i) {
+        j = index[i];
+        if (top < bottom) {
+          top += sums[j];
+          tops.push(j);
+        } else {
+          bottom += sums[j];
+          bottoms.push(j);
+        }
+      }
+      return bottoms.reverse().concat(tops);
+    },
+    reverse: function(data) {
+      return d3.range(data.length).reverse();
+    },
+    "default": d3_layout_stackOrderDefault
+  });
+  var d3_layout_stackOffsets = d3.map({
+    silhouette: function(data) {
+      var n = data.length, m = data[0].length, sums = [], max = 0, i, j, o, y0 = [];
+      for (j = 0; j < m; ++j) {
+        for (i = 0, o = 0; i < n; i++) o += data[i][j][1];
+        if (o > max) max = o;
+        sums.push(o);
+      }
+      for (j = 0; j < m; ++j) {
+        y0[j] = (max - sums[j]) / 2;
+      }
+      return y0;
+    },
+    wiggle: function(data) {
+      var n = data.length, x = data[0], m = x.length, max = 0, i, j, k, s1, s2, s3, dx, o, o0, y0 = [];
+      y0[0] = o = o0 = 0;
+      for (j = 1; j < m; ++j) {
+        for (i = 0, s1 = 0; i < n; ++i) s1 += data[i][j][1];
+        for (i = 0, s2 = 0, dx = x[j][0] - x[j - 1][0]; i < n; ++i) {
+          for (k = 0, s3 = (data[i][j][1] - data[i][j - 1][1]) / (2 * dx); k < i; ++k) {
+            s3 += (data[k][j][1] - data[k][j - 1][1]) / dx;
+          }
+          s2 += s3 * data[i][j][1];
+        }
+        y0[j] = o -= s1 ? s2 / s1 * dx : 0;
+        if (o < o0) o0 = o;
+      }
+      for (j = 0; j < m; ++j) y0[j] -= o0;
+      return y0;
+    },
+    expand: function(data) {
+      var n = data.length, m = data[0].length, k = 1 / n, i, j, o, y0 = [];
+      for (j = 0; j < m; ++j) {
+        for (i = 0, o = 0; i < n; i++) o += data[i][j][1];
+        if (o) for (i = 0; i < n; i++) data[i][j][1] /= o; else for (i = 0; i < n; i++) data[i][j][1] = k;
+      }
+      for (j = 0; j < m; ++j) y0[j] = 0;
+      return y0;
+    },
+    zero: d3_layout_stackOffsetZero
+  });
+  function d3_layout_stackOrderDefault(data) {
+    return d3.range(data.length);
+  }
+  function d3_layout_stackOffsetZero(data) {
+    var j = -1, m = data[0].length, y0 = [];
+    while (++j < m) y0[j] = 0;
+    return y0;
+  }
+  function d3_layout_stackMaxIndex(array) {
+    var i = 1, j = 0, v = array[0][1], k, n = array.length;
+    for (; i < n; ++i) {
+      if ((k = array[i][1]) > v) {
+        j = i;
+        v = k;
+      }
+    }
+    return j;
+  }
+  function d3_layout_stackReduceSum(d) {
+    return d.reduce(d3_layout_stackSum, 0);
+  }
+  function d3_layout_stackSum(p, d) {
+    return p + d[1];
+  }
+  d3.layout.histogram = function() {
+    var frequency = true, valuer = Number, ranger = d3_layout_histogramRange, binner = d3_layout_histogramBinSturges;
+    function histogram(data, i) {
+      var bins = [], values = data.map(valuer, this), range = ranger.call(this, values, i), thresholds = binner.call(this, range, values, i), bin, i = -1, n = values.length, m = thresholds.length - 1, k = frequency ? 1 : 1 / n, x;
+      while (++i < m) {
+        bin = bins[i] = [];
+        bin.dx = thresholds[i + 1] - (bin.x = thresholds[i]);
+        bin.y = 0;
+      }
+      if (m > 0) {
+        i = -1;
+        while (++i < n) {
+          x = values[i];
+          if (x >= range[0] && x <= range[1]) {
+            bin = bins[d3.bisect(thresholds, x, 1, m) - 1];
+            bin.y += k;
+            bin.push(data[i]);
+          }
+        }
+      }
+      return bins;
+    }
+    histogram.value = function(x) {
+      if (!arguments.length) return valuer;
+      valuer = x;
+      return histogram;
+    };
+    histogram.range = function(x) {
+      if (!arguments.length) return ranger;
+      ranger = d3_functor(x);
+      return histogram;
+    };
+    histogram.bins = function(x) {
+      if (!arguments.length) return binner;
+      binner = typeof x === "number" ? function(range) {
+        return d3_layout_histogramBinFixed(range, x);
+      } : d3_functor(x);
+      return histogram;
+    };
+    histogram.frequency = function(x) {
+      if (!arguments.length) return frequency;
+      frequency = !!x;
+      return histogram;
+    };
+    return histogram;
+  };
+  function d3_layout_histogramBinSturges(range, values) {
+    return d3_layout_histogramBinFixed(range, Math.ceil(Math.log(values.length) / Math.LN2 + 1));
+  }
+  function d3_layout_histogramBinFixed(range, n) {
+    var x = -1, b = +range[0], m = (range[1] - b) / n, f = [];
+    while (++x <= n) f[x] = m * x + b;
+    return f;
+  }
+  function d3_layout_histogramRange(values) {
+    return [ d3.min(values), d3.max(values) ];
+  }
+  d3.layout.hierarchy = function() {
+    var sort = d3_layout_hierarchySort, children = d3_layout_hierarchyChildren, value = d3_layout_hierarchyValue;
+    function recurse(data, depth, nodes) {
+      var childs = children.call(hierarchy, data, depth), node = d3_layout_hierarchyInline ? data : {
+        data: data
+      };
+      node.depth = depth;
+      nodes.push(node);
+      if (childs && (n = childs.length)) {
+        var i = -1, n, c = node.children = [], v = 0, j = depth + 1, d;
+        while (++i < n) {
+          d = recurse(childs[i], j, nodes);
+          d.parent = node;
+          c.push(d);
+          v += d.value;
+        }
+        if (sort) c.sort(sort);
+        if (value) node.value = v;
+      } else if (value) {
+        node.value = +value.call(hierarchy, data, depth) || 0;
+      }
+      return node;
+    }
+    function revalue(node, depth) {
+      var children = node.children, v = 0;
+      if (children && (n = children.length)) {
+        var i = -1, n, j = depth + 1;
+        while (++i < n) v += revalue(children[i], j);
+      } else if (value) {
+        v = +value.call(hierarchy, d3_layout_hierarchyInline ? node : node.data, depth) || 0;
+      }
+      if (value) node.value = v;
+      return v;
+    }
+    function hierarchy(d) {
+      var nodes = [];
+      recurse(d, 0, nodes);
+      return nodes;
+    }
+    hierarchy.sort = function(x) {
+      if (!arguments.length) return sort;
+      sort = x;
+      return hierarchy;
+    };
+    hierarchy.children = function(x) {
+      if (!arguments.length) return children;
+      children = x;
+      return hierarchy;
+    };
+    hierarchy.value = function(x) {
+      if (!arguments.length) return value;
+      value = x;
+      return hierarchy;
+    };
+    hierarchy.revalue = function(root) {
+      revalue(root, 0);
+      return root;
+    };
+    return hierarchy;
+  };
+  function d3_layout_hierarchyRebind(object, hierarchy) {
+    d3.rebind(object, hierarchy, "sort", "children", "value");
+    object.links = d3_layout_hierarchyLinks;
+    object.nodes = function(d) {
+      d3_layout_hierarchyInline = true;
+      return (object.nodes = object)(d);
+    };
+    return object;
+  }
+  function d3_layout_hierarchyChildren(d) {
+    return d.children;
+  }
+  function d3_layout_hierarchyValue(d) {
+    return d.value;
+  }
+  function d3_layout_hierarchySort(a, b) {
+    return b.value - a.value;
+  }
+  function d3_layout_hierarchyLinks(nodes) {
+    return d3.merge(nodes.map(function(parent) {
+      return (parent.children || []).map(function(child) {
+        return {
+          source: parent,
+          target: child
+        };
+      });
+    }));
+  }
+  var d3_layout_hierarchyInline = false;
+  d3.layout.pack = function() {
+    var hierarchy = d3.layout.hierarchy().sort(d3_layout_packSort), padding = 0, size = [ 1, 1 ];
+    function pack(d, i) {
+      var nodes = hierarchy.call(this, d, i), root = nodes[0];
+      root.x = 0;
+      root.y = 0;
+      d3_layout_treeVisitAfter(root, function(d) {
+        d.r = Math.sqrt(d.value);
+      });
+      d3_layout_treeVisitAfter(root, d3_layout_packSiblings);
+      var w = size[0], h = size[1], k = Math.max(2 * root.r / w, 2 * root.r / h);
+      if (padding > 0) {
+        var dr = padding * k / 2;
+        d3_layout_treeVisitAfter(root, function(d) {
+          d.r += dr;
+        });
+        d3_layout_treeVisitAfter(root, d3_layout_packSiblings);
+        d3_layout_treeVisitAfter(root, function(d) {
+          d.r -= dr;
+        });
+        k = Math.max(2 * root.r / w, 2 * root.r / h);
+      }
+      d3_layout_packTransform(root, w / 2, h / 2, 1 / k);
+      return nodes;
+    }
+    pack.size = function(x) {
+      if (!arguments.length) return size;
+      size = x;
+      return pack;
+    };
+    pack.padding = function(_) {
+      if (!arguments.length) return padding;
+      padding = +_;
+      return pack;
+    };
+    return d3_layout_hierarchyRebind(pack, hierarchy);
+  };
+  function d3_layout_packSort(a, b) {
+    return a.value - b.value;
+  }
+  function d3_layout_packInsert(a, b) {
+    var c = a._pack_next;
+    a._pack_next = b;
+    b._pack_prev = a;
+    b._pack_next = c;
+    c._pack_prev = b;
+  }
+  function d3_layout_packSplice(a, b) {
+    a._pack_next = b;
+    b._pack_prev = a;
+  }
+  function d3_layout_packIntersects(a, b) {
+    var dx = b.x - a.x, dy = b.y - a.y, dr = a.r + b.r;
+    return dr * dr - dx * dx - dy * dy > .001;
+  }
+  function d3_layout_packSiblings(node) {
+    if (!(nodes = node.children) || !(n = nodes.length)) return;
+    var nodes, xMin = Infinity, xMax = -Infinity, yMin = Infinity, yMax = -Infinity, a, b, c, i, j, k, n;
+    function bound(node) {
+      xMin = Math.min(node.x - node.r, xMin);
+      xMax = Math.max(node.x + node.r, xMax);
+      yMin = Math.min(node.y - node.r, yMin);
+      yMax = Math.max(node.y + node.r, yMax);
+    }
+    nodes.forEach(d3_layout_packLink);
+    a = nodes[0];
+    a.x = -a.r;
+    a.y = 0;
+    bound(a);
+    if (n > 1) {
+      b = nodes[1];
+      b.x = b.r;
+      b.y = 0;
+      bound(b);
+      if (n > 2) {
+        c = nodes[2];
+        d3_layout_packPlace(a, b, c);
+        bound(c);
+        d3_layout_packInsert(a, c);
+        a._pack_prev = c;
+        d3_layout_packInsert(c, b);
+        b = a._pack_next;
+        for (i = 3; i < n; i++) {
+          d3_layout_packPlace(a, b, c = nodes[i]);
+          var isect = 0, s1 = 1, s2 = 1;
+          for (j = b._pack_next; j !== b; j = j._pack_next, s1++) {
+            if (d3_layout_packIntersects(j, c)) {
+              isect = 1;
+              break;
+            }
+          }
+          if (isect == 1) {
+            for (k = a._pack_prev; k !== j._pack_prev; k = k._pack_prev, s2++) {
+              if (d3_layout_packIntersects(k, c)) {
+                break;
+              }
+            }
+          }
+          if (isect) {
+            if (s1 < s2 || s1 == s2 && b.r < a.r) d3_layout_packSplice(a, b = j); else d3_layout_packSplice(a = k, b);
+            i--;
+          } else {
+            d3_layout_packInsert(a, c);
+            b = c;
+            bound(c);
+          }
+        }
+      }
+    }
+    var cx = (xMin + xMax) / 2, cy = (yMin + yMax) / 2, cr = 0;
+    for (i = 0; i < n; i++) {
+      c = nodes[i];
+      c.x -= cx;
+      c.y -= cy;
+      cr = Math.max(cr, c.r + Math.sqrt(c.x * c.x + c.y * c.y));
+    }
+    node.r = cr;
+    nodes.forEach(d3_layout_packUnlink);
+  }
+  function d3_layout_packLink(node) {
+    node._pack_next = node._pack_prev = node;
+  }
+  function d3_layout_packUnlink(node) {
+    delete node._pack_next;
+    delete node._pack_prev;
+  }
+  function d3_layout_packTransform(node, x, y, k) {
+    var children = node.children;
+    node.x = x += k * node.x;
+    node.y = y += k * node.y;
+    node.r *= k;
+    if (children) {
+      var i = -1, n = children.length;
+      while (++i < n) d3_layout_packTransform(children[i], x, y, k);
+    }
+  }
+  function d3_layout_packPlace(a, b, c) {
+    var db = a.r + c.r, dx = b.x - a.x, dy = b.y - a.y;
+    if (db && (dx || dy)) {
+      var da = b.r + c.r, dc = dx * dx + dy * dy;
+      da *= da;
+      db *= db;
+      var x = .5 + (db - da) / (2 * dc), y = Math.sqrt(Math.max(0, 2 * da * (db + dc) - (db -= dc) * db - da * da)) / (2 * dc);
+      c.x = a.x + x * dx + y * dy;
+      c.y = a.y + x * dy - y * dx;
+    } else {
+      c.x = a.x + db;
+      c.y = a.y;
+    }
+  }
+  d3.layout.cluster = function() {
+    var hierarchy = d3.layout.hierarchy().sort(null).value(null), separation = d3_layout_treeSeparation, size = [ 1, 1 ];
+    function cluster(d, i) {
+      var nodes = hierarchy.call(this, d, i), root = nodes[0], previousNode, x = 0, kx, ky;
+      d3_layout_treeVisitAfter(root, function(node) {
+        var children = node.children;
+        if (children && children.length) {
+          node.x = d3_layout_clusterX(children);
+          node.y = d3_layout_clusterY(children);
+        } else {
+          node.x = previousNode ? x += separation(node, previousNode) : 0;
+          node.y = 0;
+          previousNode = node;
+        }
+      });
+      var left = d3_layout_clusterLeft(root), right = d3_layout_clusterRight(root), x0 = left.x - separation(left, right) / 2, x1 = right.x + separation(right, left) / 2;
+      d3_layout_treeVisitAfter(root, function(node) {
+        node.x = (node.x - x0) / (x1 - x0) * size[0];
+        node.y = (1 - (root.y ? node.y / root.y : 1)) * size[1];
+      });
+      return nodes;
+    }
+    cluster.separation = function(x) {
+      if (!arguments.length) return separation;
+      separation = x;
+      return cluster;
+    };
+    cluster.size = function(x) {
+      if (!arguments.length) return size;
+      size = x;
+      return cluster;
+    };
+    return d3_layout_hierarchyRebind(cluster, hierarchy);
+  };
+  function d3_layout_clusterY(children) {
+    return 1 + d3.max(children, function(child) {
+      return child.y;
+    });
+  }
+  function d3_layout_clusterX(children) {
+    return children.reduce(function(x, child) {
+      return x + child.x;
+    }, 0) / children.length;
+  }
+  function d3_layout_clusterLeft(node) {
+    var children = node.children;
+    return children && children.length ? d3_layout_clusterLeft(children[0]) : node;
+  }
+  function d3_layout_clusterRight(node) {
+    var children = node.children, n;
+    return children && (n = children.length) ? d3_layout_clusterRight(children[n - 1]) : node;
+  }
+  d3.layout.tree = function() {
+    var hierarchy = d3.layout.hierarchy().sort(null).value(null), separation = d3_layout_treeSeparation, size = [ 1, 1 ];
+    function tree(d, i) {
+      var nodes = hierarchy.call(this, d, i), root = nodes[0];
+      function firstWalk(node, previousSibling) {
+        var children = node.children, layout = node._tree;
+        if (children && (n = children.length)) {
+          var n, firstChild = children[0], previousChild, ancestor = firstChild, child, i = -1;
+          while (++i < n) {
+            child = children[i];
+            firstWalk(child, previousChild);
+            ancestor = apportion(child, previousChild, ancestor);
+            previousChild = child;
+          }
+          d3_layout_treeShift(node);
+          var midpoint = .5 * (firstChild._tree.prelim + child._tree.prelim);
+          if (previousSibling) {
+            layout.prelim = previousSibling._tree.prelim + separation(node, previousSibling);
+            layout.mod = layout.prelim - midpoint;
+          } else {
+            layout.prelim = midpoint;
+          }
+        } else {
+          if (previousSibling) {
+            layout.prelim = previousSibling._tree.prelim + separation(node, previousSibling);
+          }
+        }
+      }
+      function secondWalk(node, x) {
+        node.x = node._tree.prelim + x;
+        var children = node.children;
+        if (children && (n = children.length)) {
+          var i = -1, n;
+          x += node._tree.mod;
+          while (++i < n) {
+            secondWalk(children[i], x);
+          }
+        }
+      }
+      function apportion(node, previousSibling, ancestor) {
+        if (previousSibling) {
+          var vip = node, vop = node, vim = previousSibling, vom = node.parent.children[0], sip = vip._tree.mod, sop = vop._tree.mod, sim = vim._tree.mod, som = vom._tree.mod, shift;
+          while (vim = d3_layout_treeRight(vim), vip = d3_layout_treeLeft(vip), vim && vip) {
+            vom = d3_layout_treeLeft(vom);
+            vop = d3_layout_treeRight(vop);
+            vop._tree.ancestor = node;
+            shift = vim._tree.prelim + sim - vip._tree.prelim - sip + separation(vim, vip);
+            if (shift > 0) {
+              d3_layout_treeMove(d3_layout_treeAncestor(vim, node, ancestor), node, shift);
+              sip += shift;
+              sop += shift;
+            }
+            sim += vim._tree.mod;
+            sip += vip._tree.mod;
+            som += vom._tree.mod;
+            sop += vop._tree.mod;
+          }
+          if (vim && !d3_layout_treeRight(vop)) {
+            vop._tree.thread = vim;
+            vop._tree.mod += sim - sop;
+          }
+          if (vip && !d3_layout_treeLeft(vom)) {
+            vom._tree.thread = vip;
+            vom._tree.mod += sip - som;
+            ancestor = node;
+          }
+        }
+        return ancestor;
+      }
+      d3_layout_treeVisitAfter(root, function(node, previousSibling) {
+        node._tree = {
+          ancestor: node,
+          prelim: 0,
+          mod: 0,
+          change: 0,
+          shift: 0,
+          number: previousSibling ? previousSibling._tree.number + 1 : 0
+        };
+      });
+      firstWalk(root);
+      secondWalk(root, -root._tree.prelim);
+      var left = d3_layout_treeSearch(root, d3_layout_treeLeftmost), right = d3_layout_treeSearch(root, d3_layout_treeRightmost), deep = d3_layout_treeSearch(root, d3_layout_treeDeepest), x0 = left.x - separation(left, right) / 2, x1 = right.x + separation(right, left) / 2, y1 = deep.depth || 1;
+      d3_layout_treeVisitAfter(root, function(node) {
+        node.x = (node.x - x0) / (x1 - x0) * size[0];
+        node.y = node.depth / y1 * size[1];
+        delete node._tree;
+      });
+      return nodes;
+    }
+    tree.separation = function(x) {
+      if (!arguments.length) return separation;
+      separation = x;
+      return tree;
+    };
+    tree.size = function(x) {
+      if (!arguments.length) return size;
+      size = x;
+      return tree;
+    };
+    return d3_layout_hierarchyRebind(tree, hierarchy);
+  };
+  function d3_layout_treeSeparation(a, b) {
+    return a.parent == b.parent ? 1 : 2;
+  }
+  function d3_layout_treeLeft(node) {
+    var children = node.children;
+    return children && children.length ? children[0] : node._tree.thread;
+  }
+  function d3_layout_treeRight(node) {
+    var children = node.children, n;
+    return children && (n = children.length) ? children[n - 1] : node._tree.thread;
+  }
+  function d3_layout_treeSearch(node, compare) {
+    var children = node.children;
+    if (children && (n = children.length)) {
+      var child, n, i = -1;
+      while (++i < n) {
+        if (compare(child = d3_layout_treeSearch(children[i], compare), node) > 0) {
+          node = child;
+        }
+      }
+    }
+    return node;
+  }
+  function d3_layout_treeRightmost(a, b) {
+    return a.x - b.x;
+  }
+  function d3_layout_treeLeftmost(a, b) {
+    return b.x - a.x;
+  }
+  function d3_layout_treeDeepest(a, b) {
+    return a.depth - b.depth;
+  }
+  function d3_layout_treeVisitAfter(node, callback) {
+    function visit(node, previousSibling) {
+      var children = node.children;
+      if (children && (n = children.length)) {
+        var child, previousChild = null, i = -1, n;
+        while (++i < n) {
+          child = children[i];
+          visit(child, previousChild);
+          previousChild = child;
+        }
+      }
+      callback(node, previousSibling);
+    }
+    visit(node, null);
+  }
+  function d3_layout_treeShift(node) {
+    var shift = 0, change = 0, children = node.children, i = children.length, child;
+    while (--i >= 0) {
+      child = children[i]._tree;
+      child.prelim += shift;
+      child.mod += shift;
+      shift += child.shift + (change += child.change);
+    }
+  }
+  function d3_layout_treeMove(ancestor, node, shift) {
+    ancestor = ancestor._tree;
+    node = node._tree;
+    var change = shift / (node.number - ancestor.number);
+    ancestor.change += change;
+    node.change -= change;
+    node.shift += shift;
+    node.prelim += shift;
+    node.mod += shift;
+  }
+  function d3_layout_treeAncestor(vim, node, ancestor) {
+    return vim._tree.ancestor.parent == node.parent ? vim._tree.ancestor : ancestor;
+  }
+  d3.layout.treemap = function() {
+    var hierarchy = d3.layout.hierarchy(), round = Math.round, size = [ 1, 1 ], padding = null, pad = d3_layout_treemapPadNull, sticky = false, stickies, ratio = .5 * (1 + Math.sqrt(5));
+    function scale(children, k) {
+      var i = -1, n = children.length, child, area;
+      while (++i < n) {
+        area = (child = children[i]).value * (k < 0 ? 0 : k);
+        child.area = isNaN(area) || area <= 0 ? 0 : area;
+      }
+    }
+    function squarify(node) {
+      var children = node.children;
+      if (children && children.length) {
+        var rect = pad(node), row = [], remaining = children.slice(), child, best = Infinity, score, u = Math.min(rect.dx, rect.dy), n;
+        scale(remaining, rect.dx * rect.dy / node.value);
+        row.area = 0;
+        while ((n = remaining.length) > 0) {
+          row.push(child = remaining[n - 1]);
+          row.area += child.area;
+          if ((score = worst(row, u)) <= best) {
+            remaining.pop();
+            best = score;
+          } else {
+            row.area -= row.pop().area;
+            position(row, u, rect, false);
+            u = Math.min(rect.dx, rect.dy);
+            row.length = row.area = 0;
+            best = Infinity;
+          }
+        }
+        if (row.length) {
+          position(row, u, rect, true);
+          row.length = row.area = 0;
+        }
+        children.forEach(squarify);
+      }
+    }
+    function stickify(node) {
+      var children = node.children;
+      if (children && children.length) {
+        var rect = pad(node), remaining = children.slice(), child, row = [];
+        scale(remaining, rect.dx * rect.dy / node.value);
+        row.area = 0;
+        while (child = remaining.pop()) {
+          row.push(child);
+          row.area += child.area;
+          if (child.z != null) {
+            position(row, child.z ? rect.dx : rect.dy, rect, !remaining.length);
+            row.length = row.area = 0;
+          }
+        }
+        children.forEach(stickify);
+      }
+    }
+    function worst(row, u) {
+      var s = row.area, r, rmax = 0, rmin = Infinity, i = -1, n = row.length;
+      while (++i < n) {
+        if (!(r = row[i].area)) continue;
+        if (r < rmin) rmin = r;
+        if (r > rmax) rmax = r;
+      }
+      s *= s;
+      u *= u;
+      return s ? Math.max(u * rmax * ratio / s, s / (u * rmin * ratio)) : Infinity;
+    }
+    function position(row, u, rect, flush) {
+      var i = -1, n = row.length, x = rect.x, y = rect.y, v = u ? round(row.area / u) : 0, o;
+      if (u == rect.dx) {
+        if (flush || v > rect.dy) v = rect.dy;
+        while (++i < n) {
+          o = row[i];
+          o.x = x;
+          o.y = y;
+          o.dy = v;
+          x += o.dx = Math.min(rect.x + rect.dx - x, v ? round(o.area / v) : 0);
+        }
+        o.z = true;
+        o.dx += rect.x + rect.dx - x;
+        rect.y += v;
+        rect.dy -= v;
+      } else {
+        if (flush || v > rect.dx) v = rect.dx;
+        while (++i < n) {
+          o = row[i];
+          o.x = x;
+          o.y = y;
+          o.dx = v;
+          y += o.dy = Math.min(rect.y + rect.dy - y, v ? round(o.area / v) : 0);
+        }
+        o.z = false;
+        o.dy += rect.y + rect.dy - y;
+        rect.x += v;
+        rect.dx -= v;
+      }
+    }
+    function treemap(d) {
+      var nodes = stickies || hierarchy(d), root = nodes[0];
+      root.x = 0;
+      root.y = 0;
+      root.dx = size[0];
+      root.dy = size[1];
+      if (stickies) hierarchy.revalue(root);
+      scale([ root ], root.dx * root.dy / root.value);
+      (stickies ? stickify : squarify)(root);
+      if (sticky) stickies = nodes;
+      return nodes;
+    }
+    treemap.size = function(x) {
+      if (!arguments.length) return size;
+      size = x;
+      return treemap;
+    };
+    treemap.padding = function(x) {
+      if (!arguments.length) return padding;
+      function padFunction(node) {
+        var p = x.call(treemap, node, node.depth);
+        return p == null ? d3_layout_treemapPadNull(node) : d3_layout_treemapPad(node, typeof p === "number" ? [ p, p, p, p ] : p);
+      }
+      function padConstant(node) {
+        return d3_layout_treemapPad(node, x);
+      }
+      var type;
+      pad = (padding = x) == null ? d3_layout_treemapPadNull : (type = typeof x) === "function" ? padFunction : type === "number" ? (x = [ x, x, x, x ], padConstant) : padConstant;
+      return treemap;
+    };
+    treemap.round = function(x) {
+      if (!arguments.length) return round != Number;
+      round = x ? Math.round : Number;
+      return treemap;
+    };
+    treemap.sticky = function(x) {
+      if (!arguments.length) return sticky;
+      sticky = x;
+      stickies = null;
+      return treemap;
+    };
+    treemap.ratio = function(x) {
+      if (!arguments.length) return ratio;
+      ratio = x;
+      return treemap;
+    };
+    return d3_layout_hierarchyRebind(treemap, hierarchy);
+  };
+  function d3_layout_treemapPadNull(node) {
+    return {
+      x: node.x,
+      y: node.y,
+      dx: node.dx,
+      dy: node.dy
+    };
+  }
+  function d3_layout_treemapPad(node, padding) {
+    var x = node.x + padding[3], y = node.y + padding[0], dx = node.dx - padding[1] - padding[3], dy = node.dy - padding[0] - padding[2];
+    if (dx < 0) {
+      x += dx / 2;
+      dx = 0;
+    }
+    if (dy < 0) {
+      y += dy / 2;
+      dy = 0;
+    }
+    return {
+      x: x,
+      y: y,
+      dx: dx,
+      dy: dy
+    };
+  }
+  function d3_dsv(delimiter, mimeType) {
+    var reParse = new RegExp("\r\n|[" + delimiter + "\r\n]", "g"), reFormat = new RegExp('["' + delimiter + "\n]"), delimiterCode = delimiter.charCodeAt(0);
+    function dsv(url, callback) {
+      d3.text(url, mimeType, function(text) {
+        callback(text && dsv.parse(text));
+      });
+    }
+    dsv.parse = function(text) {
+      var header;
+      return dsv.parseRows(text, function(row, i) {
+        if (i) {
+          var o = {}, j = -1, m = header.length;
+          while (++j < m) o[header[j]] = row[j];
+          return o;
+        } else {
+          header = row;
+          return null;
+        }
+      });
+    };
+    dsv.parseRows = function(text, f) {
+      var EOL = {}, EOF = {}, rows = [], n = 0, t, eol;
+      reParse.lastIndex = 0;
+      function token() {
+        if (reParse.lastIndex >= text.length) return EOF;
+        if (eol) {
+          eol = false;
+          return EOL;
+        }
+        var j = reParse.lastIndex;
+        if (text.charCodeAt(j) === 34) {
+          var i = j;
+          while (i++ < text.length) {
+            if (text.charCodeAt(i) === 34) {
+              if (text.charCodeAt(i + 1) !== 34) break;
+              i++;
+            }
+          }
+          reParse.lastIndex = i + 2;
+          var c = text.charCodeAt(i + 1);
+          if (c === 13) {
+            eol = true;
+            if (text.charCodeAt(i + 2) === 10) reParse.lastIndex++;
+          } else if (c === 10) {
+            eol = true;
+          }
+          return text.substring(j + 1, i).replace(/""/g, '"');
+        }
+        var m = reParse.exec(text);
+        if (m) {
+          eol = m[0].charCodeAt(0) !== delimiterCode;
+          return text.substring(j, m.index);
+        }
+        reParse.lastIndex = text.length;
+        return text.substring(j);
+      }
+      while ((t = token()) !== EOF) {
+        var a = [];
+        while (t !== EOL && t !== EOF) {
+          a.push(t);
+          t = token();
+        }
+        if (f && !(a = f(a, n++))) continue;
+        rows.push(a);
+      }
+      return rows;
+    };
+    dsv.format = function(rows) {
+      return rows.map(formatRow).join("\n");
+    };
+    function formatRow(row) {
+      return row.map(formatValue).join(delimiter);
+    }
+    function formatValue(text) {
+      return reFormat.test(text) ? '"' + text.replace(/\"/g, '""') + '"' : text;
+    }
+    return dsv;
+  }
+  d3.csv = d3_dsv(",", "text/csv");
+  d3.tsv = d3_dsv("\t", "text/tab-separated-values");
+  d3.geo = {};
+  var d3_geo_radians = Math.PI / 180;
+  d3.geo.azimuthal = function() {
+    var mode = "orthographic", origin, scale = 200, translate = [ 480, 250 ], x0, y0, cy0, sy0;
+    function azimuthal(coordinates) {
+      var x1 = coordinates[0] * d3_geo_radians - x0, y1 = coordinates[1] * d3_geo_radians, cx1 = Math.cos(x1), sx1 = Math.sin(x1), cy1 = Math.cos(y1), sy1 = Math.sin(y1), cc = mode !== "orthographic" ? sy0 * sy1 + cy0 * cy1 * cx1 : null, c, k = mode === "stereographic" ? 1 / (1 + cc) : mode === "gnomonic" ? 1 / cc : mode === "equidistant" ? (c = Math.acos(cc), c ? c / Math.sin(c) : 0) : mode === "equalarea" ? Math.sqrt(2 / (1 + cc)) : 1, x = k * cy1 * sx1, y = k * (sy0 * cy1 * cx1 - cy0 * sy1);
+      return [ scale * x + translate[0], scale * y + translate[1] ];
+    }
+    azimuthal.invert = function(coordinates) {
+      var x = (coordinates[0] - translate[0]) / scale, y = (coordinates[1] - translate[1]) / scale, p = Math.sqrt(x * x + y * y), c = mode === "stereographic" ? 2 * Math.atan(p) : mode === "gnomonic" ? Math.atan(p) : mode === "equidistant" ? p : mode === "equalarea" ? 2 * Math.asin(.5 * p) : Math.asin(p), sc = Math.sin(c), cc = Math.cos(c);
+      return [ (x0 + Math.atan2(x * sc, p * cy0 * cc + y * sy0 * sc)) / d3_geo_radians, Math.asin(cc * sy0 - (p ? y * sc * cy0 / p : 0)) / d3_geo_radians ];
+    };
+    azimuthal.mode = function(x) {
+      if (!arguments.length) return mode;
+      mode = x + "";
+      return azimuthal;
+    };
+    azimuthal.origin = function(x) {
+      if (!arguments.length) return origin;
+      origin = x;
+      x0 = origin[0] * d3_geo_radians;
+      y0 = origin[1] * d3_geo_radians;
+      cy0 = Math.cos(y0);
+      sy0 = Math.sin(y0);
+      return azimuthal;
+    };
+    azimuthal.scale = function(x) {
+      if (!arguments.length) return scale;
+      scale = +x;
+      return azimuthal;
+    };
+    azimuthal.translate = function(x) {
+      if (!arguments.length) return translate;
+      translate = [ +x[0], +x[1] ];
+      return azimuthal;
+    };
+    return azimuthal.origin([ 0, 0 ]);
+  };
+  d3.geo.albers = function() {
+    var origin = [ -98, 38 ], parallels = [ 29.5, 45.5 ], scale = 1e3, translate = [ 480, 250 ], lng0, n, C, p0;
+    function albers(coordinates) {
+      var t = n * (d3_geo_radians * coordinates[0] - lng0), p = Math.sqrt(C - 2 * n * Math.sin(d3_geo_radians * coordinates[1])) / n;
+      return [ scale * p * Math.sin(t) + translate[0], scale * (p * Math.cos(t) - p0) + translate[1] ];
+    }
+    albers.invert = function(coordinates) {
+      var x = (coordinates[0] - translate[0]) / scale, y = (coordinates[1] - translate[1]) / scale, p0y = p0 + y, t = Math.atan2(x, p0y), p = Math.sqrt(x * x + p0y * p0y);
+      return [ (lng0 + t / n) / d3_geo_radians, Math.asin((C - p * p * n * n) / (2 * n)) / d3_geo_radians ];
+    };
+    function reload() {
+      var phi1 = d3_geo_radians * parallels[0], phi2 = d3_geo_radians * parallels[1], lat0 = d3_geo_radians * origin[1], s = Math.sin(phi1), c = Math.cos(phi1);
+      lng0 = d3_geo_radians * origin[0];
+      n = .5 * (s + Math.sin(phi2));
+      C = c * c + 2 * n * s;
+      p0 = Math.sqrt(C - 2 * n * Math.sin(lat0)) / n;
+      return albers;
+    }
+    albers.origin = function(x) {
+      if (!arguments.length) return origin;
+      origin = [ +x[0], +x[1] ];
+      return reload();
+    };
+    albers.parallels = function(x) {
+      if (!arguments.length) return parallels;
+      parallels = [ +x[0], +x[1] ];
+      return reload();
+    };
+    albers.scale = function(x) {
+      if (!arguments.length) return scale;
+      scale = +x;
+      return albers;
+    };
+    albers.translate = function(x) {
+      if (!arguments.length) return translate;
+      translate = [ +x[0], +x[1] ];
+      return albers;
+    };
+    return reload();
+  };
+  d3.geo.albersUsa = function() {
+    var lower48 = d3.geo.albers();
+    var alaska = d3.geo.albers().origin([ -160, 60 ]).parallels([ 55, 65 ]);
+    var hawaii = d3.geo.albers().origin([ -160, 20 ]).parallels([ 8, 18 ]);
+    var puertoRico = d3.geo.albers().origin([ -60, 10 ]).parallels([ 8, 18 ]);
+    function albersUsa(coordinates) {
+      var lon = coordinates[0], lat = coordinates[1];
+      return (lat > 50 ? alaska : lon < -140 ? hawaii : lat < 21 ? puertoRico : lower48)(coordinates);
+    }
+    albersUsa.scale = function(x) {
+      if (!arguments.length) return lower48.scale();
+      lower48.scale(x);
+      alaska.scale(x * .6);
+      hawaii.scale(x);
+      puertoRico.scale(x * 1.5);
+      return albersUsa.translate(lower48.translate());
+    };
+    albersUsa.translate = function(x) {
+      if (!arguments.length) return lower48.translate();
+      var dz = lower48.scale() / 1e3, dx = x[0], dy = x[1];
+      lower48.translate(x);
+      alaska.translate([ dx - 400 * dz, dy + 170 * dz ]);
+      hawaii.translate([ dx - 190 * dz, dy + 200 * dz ]);
+      puertoRico.translate([ dx + 580 * dz, dy + 430 * dz ]);
+      return albersUsa;
+    };
+    return albersUsa.scale(lower48.scale());
+  };
+  d3.geo.bonne = function() {
+    var scale = 200, translate = [ 480, 250 ], x0, y0, y1, c1;
+    function bonne(coordinates) {
+      var x = coordinates[0] * d3_geo_radians - x0, y = coordinates[1] * d3_geo_radians - y0;
+      if (y1) {
+        var p = c1 + y1 - y, E = x * Math.cos(y) / p;
+        x = p * Math.sin(E);
+        y = p * Math.cos(E) - c1;
+      } else {
+        x *= Math.cos(y);
+        y *= -1;
+      }
+      return [ scale * x + translate[0], scale * y + translate[1] ];
+    }
+    bonne.invert = function(coordinates) {
+      var x = (coordinates[0] - translate[0]) / scale, y = (coordinates[1] - translate[1]) / scale;
+      if (y1) {
+        var c = c1 + y, p = Math.sqrt(x * x + c * c);
+        y = c1 + y1 - p;
+        x = x0 + p * Math.atan2(x, c) / Math.cos(y);
+      } else {
+        y *= -1;
+        x /= Math.cos(y);
+      }
+      return [ x / d3_geo_radians, y / d3_geo_radians ];
+    };
+    bonne.parallel = function(x) {
+      if (!arguments.length) return y1 / d3_geo_radians;
+      c1 = 1 / Math.tan(y1 = x * d3_geo_radians);
+      return bonne;
+    };
+    bonne.origin = function(x) {
+      if (!arguments.length) return [ x0 / d3_geo_radians, y0 / d3_geo_radians ];
+      x0 = x[0] * d3_geo_radians;
+      y0 = x[1] * d3_geo_radians;
+      return bonne;
+    };
+    bonne.scale = function(x) {
+      if (!arguments.length) return scale;
+      scale = +x;
+      return bonne;
+    };
+    bonne.translate = function(x) {
+      if (!arguments.length) return translate;
+      translate = [ +x[0], +x[1] ];
+      return bonne;
+    };
+    return bonne.origin([ 0, 0 ]).parallel(45);
+  };
+  d3.geo.equirectangular = function() {
+    var scale = 500, translate = [ 480, 250 ];
+    function equirectangular(coordinates) {
+      var x = coordinates[0] / 360, y = -coordinates[1] / 360;
+      return [ scale * x + translate[0], scale * y + translate[1] ];
+    }
+    equirectangular.invert = function(coordinates) {
+      var x = (coordinates[0] - translate[0]) / scale, y = (coordinates[1] - translate[1]) / scale;
+      return [ 360 * x, -360 * y ];
+    };
+    equirectangular.scale = function(x) {
+      if (!arguments.length) return scale;
+      scale = +x;
+      return equirectangular;
+    };
+    equirectangular.translate = function(x) {
+      if (!arguments.length) return translate;
+      translate = [ +x[0], +x[1] ];
+      return equirectangular;
+    };
+    return equirectangular;
+  };
+  d3.geo.mercator = function() {
+    var scale = 500, translate = [ 480, 250 ];
+    function mercator(coordinates) {
+      var x = coordinates[0] / 360, y = -(Math.log(Math.tan(Math.PI / 4 + coordinates[1] * d3_geo_radians / 2)) / d3_geo_radians) / 360;
+      return [ scale * x + translate[0], scale * Math.max(-.5, Math.min(.5, y)) + translate[1] ];
+    }
+    mercator.invert = function(coordinates) {
+      var x = (coordinates[0] - translate[0]) / scale, y = (coordinates[1] - translate[1]) / scale;
+      return [ 360 * x, 2 * Math.atan(Math.exp(-360 * y * d3_geo_radians)) / d3_geo_radians - 90 ];
+    };
+    mercator.scale = function(x) {
+      if (!arguments.length) return scale;
+      scale = +x;
+      return mercator;
+    };
+    mercator.translate = function(x) {
+      if (!arguments.length) return translate;
+      translate = [ +x[0], +x[1] ];
+      return mercator;
+    };
+    return mercator;
+  };
+  function d3_geo_type(types, defaultValue) {
+    return function(object) {
+      return object && types.hasOwnProperty(object.type) ? types[object.type](object) : defaultValue;
+    };
+  }
+  d3.geo.path = function() {
+    var pointRadius = 4.5, pointCircle = d3_path_circle(pointRadius), projection = d3.geo.albersUsa(), buffer = [];
+    function path(d, i) {
+      if (typeof pointRadius === "function") pointCircle = d3_path_circle(pointRadius.apply(this, arguments));
+      pathType(d);
+      var result = buffer.length ? buffer.join("") : null;
+      buffer = [];
+      return result;
+    }
+    function project(coordinates) {
+      return projection(coordinates).join(",");
+    }
+    var pathType = d3_geo_type({
+      FeatureCollection: function(o) {
+        var features = o.features, i = -1, n = features.length;
+        while (++i < n) buffer.push(pathType(features[i].geometry));
+      },
+      Feature: function(o) {
+        pathType(o.geometry);
+      },
+      Point: function(o) {
+        buffer.push("M", project(o.coordinates), pointCircle);
+      },
+      MultiPoint: function(o) {
+        var coordinates = o.coordinates, i = -1, n = coordinates.length;
+        while (++i < n) buffer.push("M", project(coordinates[i]), pointCircle);
+      },
+      LineString: function(o) {
+        var coordinates = o.coordinates, i = -1, n = coordinates.length;
+        buffer.push("M");
+        while (++i < n) buffer.push(project(coordinates[i]), "L");
+        buffer.pop();
+      },
+      MultiLineString: function(o) {
+        var coordinates = o.coordinates, i = -1, n = coordinates.length, subcoordinates, j, m;
+        while (++i < n) {
+          subcoordinates = coordinates[i];
+          j = -1;
+          m = subcoordinates.length;
+          buffer.push("M");
+          while (++j < m) buffer.push(project(subcoordinates[j]), "L");
+          buffer.pop();
+        }
+      },
+      Polygon: function(o) {
+        var coordinates = o.coordinates, i = -1, n = coordinates.length, subcoordinates, j, m;
+        while (++i < n) {
+          subcoordinates = coordinates[i];
+          j = -1;
+          if ((m = subcoordinates.length - 1) > 0) {
+            buffer.push("M");
+            while (++j < m) buffer.push(project(subcoordinates[j]), "L");
+            buffer[buffer.length - 1] = "Z";
+          }
+        }
+      },
+      MultiPolygon: function(o) {
+        var coordinates = o.coordinates, i = -1, n = coordinates.length, subcoordinates, j, m, subsubcoordinates, k, p;
+        while (++i < n) {
+          subcoordinates = coordinates[i];
+          j = -1;
+          m = subcoordinates.length;
+          while (++j < m) {
+            subsubcoordinates = subcoordinates[j];
+            k = -1;
+            if ((p = subsubcoordinates.length - 1) > 0) {
+              buffer.push("M");
+              while (++k < p) buffer.push(project(subsubcoordinates[k]), "L");
+              buffer[buffer.length - 1] = "Z";
+            }
+          }
+        }
+      },
+      GeometryCollection: function(o) {
+        var geometries = o.geometries, i = -1, n = geometries.length;
+        while (++i < n) buffer.push(pathType(geometries[i]));
+      }
+    });
+    var areaType = path.area = d3_geo_type({
+      FeatureCollection: function(o) {
+        var area = 0, features = o.features, i = -1, n = features.length;
+        while (++i < n) area += areaType(features[i]);
+        return area;
+      },
+      Feature: function(o) {
+        return areaType(o.geometry);
+      },
+      Polygon: function(o) {
+        return polygonArea(o.coordinates);
+      },
+      MultiPolygon: function(o) {
+        var sum = 0, coordinates = o.coordinates, i = -1, n = coordinates.length;
+        while (++i < n) sum += polygonArea(coordinates[i]);
+        return sum;
+      },
+      GeometryCollection: function(o) {
+        var sum = 0, geometries = o.geometries, i = -1, n = geometries.length;
+        while (++i < n) sum += areaType(geometries[i]);
+        return sum;
+      }
+    }, 0);
+    function polygonArea(coordinates) {
+      var sum = area(coordinates[0]), i = 0, n = coordinates.length;
+      while (++i < n) sum -= area(coordinates[i]);
+      return sum;
+    }
+    function polygonCentroid(coordinates) {
+      var polygon = d3.geom.polygon(coordinates[0].map(projection)), area = polygon.area(), centroid = polygon.centroid(area < 0 ? (area *= -1, 1) : -1), x = centroid[0], y = centroid[1], z = area, i = 0, n = coordinates.length;
+      while (++i < n) {
+        polygon = d3.geom.polygon(coordinates[i].map(projection));
+        area = polygon.area();
+        centroid = polygon.centroid(area < 0 ? (area *= -1, 1) : -1);
+        x -= centroid[0];
+        y -= centroid[1];
+        z -= area;
+      }
+      return [ x, y, 6 * z ];
+    }
+    var centroidType = path.centroid = d3_geo_type({
+      Feature: function(o) {
+        return centroidType(o.geometry);
+      },
+      Polygon: function(o) {
+        var centroid = polygonCentroid(o.coordinates);
+        return [ centroid[0] / centroid[2], centroid[1] / centroid[2] ];
+      },
+      MultiPolygon: function(o) {
+        var area = 0, coordinates = o.coordinates, centroid, x = 0, y = 0, z = 0, i = -1, n = coordinates.length;
+        while (++i < n) {
+          centroid = polygonCentroid(coordinates[i]);
+          x += centroid[0];
+          y += centroid[1];
+          z += centroid[2];
+        }
+        return [ x / z, y / z ];
+      }
+    });
+    function area(coordinates) {
+      return Math.abs(d3.geom.polygon(coordinates.map(projection)).area());
+    }
+    path.projection = function(x) {
+      projection = x;
+      return path;
+    };
+    path.pointRadius = function(x) {
+      if (typeof x === "function") pointRadius = x; else {
+        pointRadius = +x;
+        pointCircle = d3_path_circle(pointRadius);
+      }
+      return path;
+    };
+    return path;
+  };
+  function d3_path_circle(radius) {
+    return "m0," + radius + "a" + radius + "," + radius + " 0 1,1 0," + -2 * radius + "a" + radius + "," + radius + " 0 1,1 0," + +2 * radius + "z";
+  }
+  d3.geo.bounds = function(feature) {
+    var left = Infinity, bottom = Infinity, right = -Infinity, top = -Infinity;
+    d3_geo_bounds(feature, function(x, y) {
+      if (x < left) left = x;
+      if (x > right) right = x;
+      if (y < bottom) bottom = y;
+      if (y > top) top = y;
+    });
+    return [ [ left, bottom ], [ right, top ] ];
+  };
+  function d3_geo_bounds(o, f) {
+    if (d3_geo_boundsTypes.hasOwnProperty(o.type)) d3_geo_boundsTypes[o.type](o, f);
+  }
+  var d3_geo_boundsTypes = {
+    Feature: d3_geo_boundsFeature,
+    FeatureCollection: d3_geo_boundsFeatureCollection,
+    GeometryCollection: d3_geo_boundsGeometryCollection,
+    LineString: d3_geo_boundsLineString,
+    MultiLineString: d3_geo_boundsMultiLineString,
+    MultiPoint: d3_geo_boundsLineString,
+    MultiPolygon: d3_geo_boundsMultiPolygon,
+    Point: d3_geo_boundsPoint,
+    Polygon: d3_geo_boundsPolygon
+  };
+  function d3_geo_boundsFeature(o, f) {
+    d3_geo_bounds(o.geometry, f);
+  }
+  function d3_geo_boundsFeatureCollection(o, f) {
+    for (var a = o.features, i = 0, n = a.length; i < n; i++) {
+      d3_geo_bounds(a[i].geometry, f);
+    }
+  }
+  function d3_geo_boundsGeometryCollection(o, f) {
+    for (var a = o.geometries, i = 0, n = a.length; i < n; i++) {
+      d3_geo_bounds(a[i], f);
+    }
+  }
+  function d3_geo_boundsLineString(o, f) {
+    for (var a = o.coordinates, i = 0, n = a.length; i < n; i++) {
+      f.apply(null, a[i]);
+    }
+  }
+  function d3_geo_boundsMultiLineString(o, f) {
+    for (var a = o.coordinates, i = 0, n = a.length; i < n; i++) {
+      for (var b = a[i], j = 0, m = b.length; j < m; j++) {
+        f.apply(null, b[j]);
+      }
+    }
+  }
+  function d3_geo_boundsMultiPolygon(o, f) {
+    for (var a = o.coordinates, i = 0, n = a.length; i < n; i++) {
+      for (var b = a[i][0], j = 0, m = b.length; j < m; j++) {
+        f.apply(null, b[j]);
+      }
+    }
+  }
+  function d3_geo_boundsPoint(o, f) {
+    f.apply(null, o.coordinates);
+  }
+  function d3_geo_boundsPolygon(o, f) {
+    for (var a = o.coordinates[0], i = 0, n = a.length; i < n; i++) {
+      f.apply(null, a[i]);
+    }
+  }
+  d3.geo.circle = function() {
+    var origin = [ 0, 0 ], degrees = 90 - .01, radians = degrees * d3_geo_radians, arc = d3.geo.greatArc().source(origin).target(d3_identity);
+    function circle() {}
+    function visible(point) {
+      return arc.distance(point) < radians;
+    }
+    circle.clip = function(d) {
+      if (typeof origin === "function") arc.source(origin.apply(this, arguments));
+      return clipType(d) || null;
+    };
+    var clipType = d3_geo_type({
+      FeatureCollection: function(o) {
+        var features = o.features.map(clipType).filter(d3_identity);
+        return features && (o = Object.create(o), o.features = features, o);
+      },
+      Feature: function(o) {
+        var geometry = clipType(o.geometry);
+        return geometry && (o = Object.create(o), o.geometry = geometry, o);
+      },
+      Point: function(o) {
+        return visible(o.coordinates) && o;
+      },
+      MultiPoint: function(o) {
+        var coordinates = o.coordinates.filter(visible);
+        return coordinates.length && {
+          type: o.type,
+          coordinates: coordinates
+        };
+      },
+      LineString: function(o) {
+        var coordinates = clip(o.coordinates);
+        return coordinates.length && (o = Object.create(o), o.coordinates = coordinates, o);
+      },
+      MultiLineString: function(o) {
+        var coordinates = o.coordinates.map(clip).filter(function(d) {
+          return d.length;
+        });
+        return coordinates.length && (o = Object.create(o), o.coordinates = coordinates, o);
+      },
+      Polygon: function(o) {
+        var coordinates = o.coordinates.map(clip);
+        return coordinates[0].length && (o = Object.create(o), o.coordinates = coordinates, o);
+      },
+      MultiPolygon: function(o) {
+        var coordinates = o.coordinates.map(function(d) {
+          return d.map(clip);
+        }).filter(function(d) {
+          return d[0].length;
+        });
+        return coordinates.length && (o = Object.create(o), o.coordinates = coordinates, o);
+      },
+      GeometryCollection: function(o) {
+        var geometries = o.geometries.map(clipType).filter(d3_identity);
+        return geometries.length && (o = Object.create(o), o.geometries = geometries, o);
+      }
+    });
+    function clip(coordinates) {
+      var i = -1, n = coordinates.length, clipped = [], p0, p1, p2, d0, d1;
+      while (++i < n) {
+        d1 = arc.distance(p2 = coordinates[i]);
+        if (d1 < radians) {
+          if (p1) clipped.push(d3_geo_greatArcInterpolate(p1, p2)((d0 - radians) / (d0 - d1)));
+          clipped.push(p2);
+          p0 = p1 = null;
+        } else {
+          p1 = p2;
+          if (!p0 && clipped.length) {
+            clipped.push(d3_geo_greatArcInterpolate(clipped[clipped.length - 1], p1)((radians - d0) / (d1 - d0)));
+            p0 = p1;
+          }
+        }
+        d0 = d1;
+      }
+      p0 = coordinates[0];
+      p1 = clipped[0];
+      if (p1 && p2[0] === p0[0] && p2[1] === p0[1] && !(p2[0] === p1[0] && p2[1] === p1[1])) {
+        clipped.push(p1);
+      }
+      return resample(clipped);
+    }
+    function resample(coordinates) {
+      var i = 0, n = coordinates.length, j, m, resampled = n ? [ coordinates[0] ] : coordinates, resamples, origin = arc.source();
+      while (++i < n) {
+        resamples = arc.source(coordinates[i - 1])(coordinates[i]).coordinates;
+        for (j = 0, m = resamples.length; ++j < m; ) resampled.push(resamples[j]);
+      }
+      arc.source(origin);
+      return resampled;
+    }
+    circle.origin = function(x) {
+      if (!arguments.length) return origin;
+      origin = x;
+      if (typeof origin !== "function") arc.source(origin);
+      return circle;
+    };
+    circle.angle = function(x) {
+      if (!arguments.length) return degrees;
+      radians = (degrees = +x) * d3_geo_radians;
+      return circle;
+    };
+    return d3.rebind(circle, arc, "precision");
+  };
+  d3.geo.greatArc = function() {
+    var source = d3_geo_greatArcSource, p0, target = d3_geo_greatArcTarget, p1, precision = 6 * d3_geo_radians, interpolate = d3_geo_greatArcInterpolator();
+    function greatArc() {
+      var d = greatArc.distance.apply(this, arguments), t = 0, dt = precision / d, coordinates = [ p0 ];
+      while ((t += dt) < 1) coordinates.push(interpolate(t));
+      coordinates.push(p1);
+      return {
+        type: "LineString",
+        coordinates: coordinates
+      };
+    }
+    greatArc.distance = function() {
+      if (typeof source === "function") interpolate.source(p0 = source.apply(this, arguments));
+      if (typeof target === "function") interpolate.target(p1 = target.apply(this, arguments));
+      return interpolate.distance();
+    };
+    greatArc.source = function(_) {
+      if (!arguments.length) return source;
+      source = _;
+      if (typeof source !== "function") interpolate.source(p0 = source);
+      return greatArc;
+    };
+    greatArc.target = function(_) {
+      if (!arguments.length) return target;
+      target = _;
+      if (typeof target !== "function") interpolate.target(p1 = target);
+      return greatArc;
+    };
+    greatArc.precision = function(_) {
+      if (!arguments.length) return precision / d3_geo_radians;
+      precision = _ * d3_geo_radians;
+      return greatArc;
+    };
+    return greatArc;
+  };
+  function d3_geo_greatArcSource(d) {
+    return d.source;
+  }
+  function d3_geo_greatArcTarget(d) {
+    return d.target;
+  }
+  function d3_geo_greatArcInterpolator() {
+    var x0, y0, cy0, sy0, kx0, ky0, x1, y1, cy1, sy1, kx1, ky1, d, k;
+    function interpolate(t) {
+      var B = Math.sin(t *= d) * k, A = Math.sin(d - t) * k, x = A * kx0 + B * kx1, y = A * ky0 + B * ky1, z = A * sy0 + B * sy1;
+      return [ Math.atan2(y, x) / d3_geo_radians, Math.atan2(z, Math.sqrt(x * x + y * y)) / d3_geo_radians ];
+    }
+    interpolate.distance = function() {
+      if (d == null) k = 1 / Math.sin(d = Math.acos(Math.max(-1, Math.min(1, sy0 * sy1 + cy0 * cy1 * Math.cos(x1 - x0)))));
+      return d;
+    };
+    interpolate.source = function(_) {
+      var cx0 = Math.cos(x0 = _[0] * d3_geo_radians), sx0 = Math.sin(x0);
+      cy0 = Math.cos(y0 = _[1] * d3_geo_radians);
+      sy0 = Math.sin(y0);
+      kx0 = cy0 * cx0;
+      ky0 = cy0 * sx0;
+      d = null;
+      return interpolate;
+    };
+    interpolate.target = function(_) {
+      var cx1 = Math.cos(x1 = _[0] * d3_geo_radians), sx1 = Math.sin(x1);
+      cy1 = Math.cos(y1 = _[1] * d3_geo_radians);
+      sy1 = Math.sin(y1);
+      kx1 = cy1 * cx1;
+      ky1 = cy1 * sx1;
+      d = null;
+      return interpolate;
+    };
+    return interpolate;
+  }
+  function d3_geo_greatArcInterpolate(a, b) {
+    var i = d3_geo_greatArcInterpolator().source(a).target(b);
+    i.distance();
+    return i;
+  }
+  d3.geo.greatCircle = d3.geo.circle;
+  d3.geom = {};
+  d3.geom.contour = function(grid, start) {
+    var s = start || d3_geom_contourStart(grid), c = [], x = s[0], y = s[1], dx = 0, dy = 0, pdx = NaN, pdy = NaN, i = 0;
+    do {
+      i = 0;
+      if (grid(x - 1, y - 1)) i += 1;
+      if (grid(x, y - 1)) i += 2;
+      if (grid(x - 1, y)) i += 4;
+      if (grid(x, y)) i += 8;
+      if (i === 6) {
+        dx = pdy === -1 ? -1 : 1;
+        dy = 0;
+      } else if (i === 9) {
+        dx = 0;
+        dy = pdx === 1 ? -1 : 1;
+      } else {
+        dx = d3_geom_contourDx[i];
+        dy = d3_geom_contourDy[i];
+      }
+      if (dx != pdx && dy != pdy) {
+        c.push([ x, y ]);
+        pdx = dx;
+        pdy = dy;
+      }
+      x += dx;
+      y += dy;
+    } while (s[0] != x || s[1] != y);
+    return c;
+  };
+  var d3_geom_contourDx = [ 1, 0, 1, 1, -1, 0, -1, 1, 0, 0, 0, 0, -1, 0, -1, NaN ], d3_geom_contourDy = [ 0, -1, 0, 0, 0, -1, 0, 0, 1, -1, 1, 1, 0, -1, 0, NaN ];
+  function d3_geom_contourStart(grid) {
+    var x = 0, y = 0;
+    while (true) {
+      if (grid(x, y)) {
+        return [ x, y ];
+      }
+      if (x === 0) {
+        x = y + 1;
+        y = 0;
+      } else {
+        x = x - 1;
+        y = y + 1;
+      }
+    }
+  }
+  d3.geom.hull = function(vertices) {
+    if (vertices.length < 3) return [];
+    var len = vertices.length, plen = len - 1, points = [], stack = [], i, j, h = 0, x1, y1, x2, y2, u, v, a, sp;
+    for (i = 1; i < len; ++i) {
+      if (vertices[i][1] < vertices[h][1]) {
+        h = i;
+      } else if (vertices[i][1] == vertices[h][1]) {
+        h = vertices[i][0] < vertices[h][0] ? i : h;
+      }
+    }
+    for (i = 0; i < len; ++i) {
+      if (i === h) continue;
+      y1 = vertices[i][1] - vertices[h][1];
+      x1 = vertices[i][0] - vertices[h][0];
+      points.push({
+        angle: Math.atan2(y1, x1),
+        index: i
+      });
+    }
+    points.sort(function(a, b) {
+      return a.angle - b.angle;
+    });
+    a = points[0].angle;
+    v = points[0].index;
+    u = 0;
+    for (i = 1; i < plen; ++i) {
+      j = points[i].index;
+      if (a == points[i].angle) {
+        x1 = vertices[v][0] - vertices[h][0];
+        y1 = vertices[v][1] - vertices[h][1];
+        x2 = vertices[j][0] - vertices[h][0];
+        y2 = vertices[j][1] - vertices[h][1];
+        if (x1 * x1 + y1 * y1 >= x2 * x2 + y2 * y2) {
+          points[i].index = -1;
+        } else {
+          points[u].index = -1;
+          a = points[i].angle;
+          u = i;
+          v = j;
+        }
+      } else {
+        a = points[i].angle;
+        u = i;
+        v = j;
+      }
+    }
+    stack.push(h);
+    for (i = 0, j = 0; i < 2; ++j) {
+      if (points[j].index !== -1) {
+        stack.push(points[j].index);
+        i++;
+      }
+    }
+    sp = stack.length;
+    for (; j < plen; ++j) {
+      if (points[j].index === -1) continue;
+      while (!d3_geom_hullCCW(stack[sp - 2], stack[sp - 1], points[j].index, vertices)) {
+        --sp;
+      }
+      stack[sp++] = points[j].index;
+    }
+    var poly = [];
+    for (i = 0; i < sp; ++i) {
+      poly.push(vertices[stack[i]]);
+    }
+    return poly;
+  };
+  function d3_geom_hullCCW(i1, i2, i3, v) {
+    var t, a, b, c, d, e, f;
+    t = v[i1];
+    a = t[0];
+    b = t[1];
+    t = v[i2];
+    c = t[0];
+    d = t[1];
+    t = v[i3];
+    e = t[0];
+    f = t[1];
+    return (f - b) * (c - a) - (d - b) * (e - a) > 0;
+  }
+  d3.geom.polygon = function(coordinates) {
+    coordinates.area = function() {
+      var i = 0, n = coordinates.length, a = coordinates[n - 1][0] * coordinates[0][1], b = coordinates[n - 1][1] * coordinates[0][0];
+      while (++i < n) {
+        a += coordinates[i - 1][0] * coordinates[i][1];
+        b += coordinates[i - 1][1] * coordinates[i][0];
+      }
+      return (b - a) * .5;
+    };
+    coordinates.centroid = function(k) {
+      var i = -1, n = coordinates.length, x = 0, y = 0, a, b = coordinates[n - 1], c;
+      if (!arguments.length) k = -1 / (6 * coordinates.area());
+      while (++i < n) {
+        a = b;
+        b = coordinates[i];
+        c = a[0] * b[1] - b[0] * a[1];
+        x += (a[0] + b[0]) * c;
+        y += (a[1] + b[1]) * c;
+      }
+      return [ x * k, y * k ];
+    };
+    coordinates.clip = function(subject) {
+      var input, i = -1, n = coordinates.length, j, m, a = coordinates[n - 1], b, c, d;
+      while (++i < n) {
+        input = subject.slice();
+        subject.length = 0;
+        b = coordinates[i];
+        c = input[(m = input.length) - 1];
+        j = -1;
+        while (++j < m) {
+          d = input[j];
+          if (d3_geom_polygonInside(d, a, b)) {
+            if (!d3_geom_polygonInside(c, a, b)) {
+              subject.push(d3_geom_polygonIntersect(c, d, a, b));
+            }
+            subject.push(d);
+          } else if (d3_geom_polygonInside(c, a, b)) {
+            subject.push(d3_geom_polygonIntersect(c, d, a, b));
+          }
+          c = d;
+        }
+        a = b;
+      }
+      return subject;
+    };
+    return coordinates;
+  };
+  function d3_geom_polygonInside(p, a, b) {
+    return (b[0] - a[0]) * (p[1] - a[1]) < (b[1] - a[1]) * (p[0] - a[0]);
+  }
+  function d3_geom_polygonIntersect(c, d, a, b) {
+    var x1 = c[0], x2 = d[0], x3 = a[0], x4 = b[0], y1 = c[1], y2 = d[1], y3 = a[1], y4 = b[1], x13 = x1 - x3, x21 = x2 - x1, x43 = x4 - x3, y13 = y1 - y3, y21 = y2 - y1, y43 = y4 - y3, ua = (x43 * y13 - y43 * x13) / (y43 * x21 - x43 * y21);
+    return [ x1 + ua * x21, y1 + ua * y21 ];
+  }
+  d3.geom.voronoi = function(vertices) {
+    var polygons = vertices.map(function() {
+      return [];
+    });
+    d3_voronoi_tessellate(vertices, function(e) {
+      var s1, s2, x1, x2, y1, y2;
+      if (e.a === 1 && e.b >= 0) {
+        s1 = e.ep.r;
+        s2 = e.ep.l;
+      } else {
+        s1 = e.ep.l;
+        s2 = e.ep.r;
+      }
+      if (e.a === 1) {
+        y1 = s1 ? s1.y : -1e6;
+        x1 = e.c - e.b * y1;
+        y2 = s2 ? s2.y : 1e6;
+        x2 = e.c - e.b * y2;
+      } else {
+        x1 = s1 ? s1.x : -1e6;
+        y1 = e.c - e.a * x1;
+        x2 = s2 ? s2.x : 1e6;
+        y2 = e.c - e.a * x2;
+      }
+      var v1 = [ x1, y1 ], v2 = [ x2, y2 ];
+      polygons[e.region.l.index].push(v1, v2);
+      polygons[e.region.r.index].push(v1, v2);
+    });
+    return polygons.map(function(polygon, i) {
+      var cx = vertices[i][0], cy = vertices[i][1];
+      polygon.forEach(function(v) {
+        v.angle = Math.atan2(v[0] - cx, v[1] - cy);
+      });
+      return polygon.sort(function(a, b) {
+        return a.angle - b.angle;
+      }).filter(function(d, i) {
+        return !i || d.angle - polygon[i - 1].angle > 1e-10;
+      });
+    });
+  };
+  var d3_voronoi_opposite = {
+    l: "r",
+    r: "l"
+  };
+  function d3_voronoi_tessellate(vertices, callback) {
+    var Sites = {
+      list: vertices.map(function(v, i) {
+        return {
+          index: i,
+          x: v[0],
+          y: v[1]
+        };
+      }).sort(function(a, b) {
+        return a.y < b.y ? -1 : a.y > b.y ? 1 : a.x < b.x ? -1 : a.x > b.x ? 1 : 0;
+      }),
+      bottomSite: null
+    };
+    var EdgeList = {
+      list: [],
+      leftEnd: null,
+      rightEnd: null,
+      init: function() {
+        EdgeList.leftEnd = EdgeList.createHalfEdge(null, "l");
+        EdgeList.rightEnd = EdgeList.createHalfEdge(null, "l");
+        EdgeList.leftEnd.r = EdgeList.rightEnd;
+        EdgeList.rightEnd.l = EdgeList.leftEnd;
+        EdgeList.list.unshift(EdgeList.leftEnd, EdgeList.rightEnd);
+      },
+      createHalfEdge: function(edge, side) {
+        return {
+          edge: edge,
+          side: side,
+          vertex: null,
+          l: null,
+          r: null
+        };
+      },
+      insert: function(lb, he) {
+        he.l = lb;
+        he.r = lb.r;
+        lb.r.l = he;
+        lb.r = he;
+      },
+      leftBound: function(p) {
+        var he = EdgeList.leftEnd;
+        do {
+          he = he.r;
+        } while (he != EdgeList.rightEnd && Geom.rightOf(he, p));
+        he = he.l;
+        return he;
+      },
+      del: function(he) {
+        he.l.r = he.r;
+        he.r.l = he.l;
+        he.edge = null;
+      },
+      right: function(he) {
+        return he.r;
+      },
+      left: function(he) {
+        return he.l;
+      },
+      leftRegion: function(he) {
+        return he.edge == null ? Sites.bottomSite : he.edge.region[he.side];
+      },
+      rightRegion: function(he) {
+        return he.edge == null ? Sites.bottomSite : he.edge.region[d3_voronoi_opposite[he.side]];
+      }
+    };
+    var Geom = {
+      bisect: function(s1, s2) {
+        var newEdge = {
+          region: {
+            l: s1,
+            r: s2
+          },
+          ep: {
+            l: null,
+            r: null
+          }
+        };
+        var dx = s2.x - s1.x, dy = s2.y - s1.y, adx = dx > 0 ? dx : -dx, ady = dy > 0 ? dy : -dy;
+        newEdge.c = s1.x * dx + s1.y * dy + (dx * dx + dy * dy) * .5;
+        if (adx > ady) {
+          newEdge.a = 1;
+          newEdge.b = dy / dx;
+          newEdge.c /= dx;
+        } else {
+          newEdge.b = 1;
+          newEdge.a = dx / dy;
+          newEdge.c /= dy;
+        }
+        return newEdge;
+      },
+      intersect: function(el1, el2) {
+        var e1 = el1.edge, e2 = el2.edge;
+        if (!e1 || !e2 || e1.region.r == e2.region.r) {
+          return null;
+        }
+        var d = e1.a * e2.b - e1.b * e2.a;
+        if (Math.abs(d) < 1e-10) {
+          return null;
+        }
+        var xint = (e1.c * e2.b - e2.c * e1.b) / d, yint = (e2.c * e1.a - e1.c * e2.a) / d, e1r = e1.region.r, e2r = e2.region.r, el, e;
+        if (e1r.y < e2r.y || e1r.y == e2r.y && e1r.x < e2r.x) {
+          el = el1;
+          e = e1;
+        } else {
+          el = el2;
+          e = e2;
+        }
+        var rightOfSite = xint >= e.region.r.x;
+        if (rightOfSite && el.side === "l" || !rightOfSite && el.side === "r") {
+          return null;
+        }
+        return {
+          x: xint,
+          y: yint
+        };
+      },
+      rightOf: function(he, p) {
+        var e = he.edge, topsite = e.region.r, rightOfSite = p.x > topsite.x;
+        if (rightOfSite && he.side === "l") {
+          return 1;
+        }
+        if (!rightOfSite && he.side === "r") {
+          return 0;
+        }
+        if (e.a === 1) {
+          var dyp = p.y - topsite.y, dxp = p.x - topsite.x, fast = 0, above = 0;
+          if (!rightOfSite && e.b < 0 || rightOfSite && e.b >= 0) {
+            above = fast = dyp >= e.b * dxp;
+          } else {
+            above = p.x + p.y * e.b > e.c;
+            if (e.b < 0) {
+              above = !above;
+            }
+            if (!above) {
+              fast = 1;
+            }
+          }
+          if (!fast) {
+            var dxs = topsite.x - e.region.l.x;
+            above = e.b * (dxp * dxp - dyp * dyp) < dxs * dyp * (1 + 2 * dxp / dxs + e.b * e.b);
+            if (e.b < 0) {
+              above = !above;
+            }
+          }
+        } else {
+          var yl = e.c - e.a * p.x, t1 = p.y - yl, t2 = p.x - topsite.x, t3 = yl - topsite.y;
+          above = t1 * t1 > t2 * t2 + t3 * t3;
+        }
+        return he.side === "l" ? above : !above;
+      },
+      endPoint: function(edge, side, site) {
+        edge.ep[side] = site;
+        if (!edge.ep[d3_voronoi_opposite[side]]) return;
+        callback(edge);
+      },
+      distance: function(s, t) {
+        var dx = s.x - t.x, dy = s.y - t.y;
+        return Math.sqrt(dx * dx + dy * dy);
+      }
+    };
+    var EventQueue = {
+      list: [],
+      insert: function(he, site, offset) {
+        he.vertex = site;
+        he.ystar = site.y + offset;
+        for (var i = 0, list = EventQueue.list, l = list.length; i < l; i++) {
+          var next = list[i];
+          if (he.ystar > next.ystar || he.ystar == next.ystar && site.x > next.vertex.x) {
+            continue;
+          } else {
+            break;
+          }
+        }
+        list.splice(i, 0, he);
+      },
+      del: function(he) {
+        for (var i = 0, ls = EventQueue.list, l = ls.length; i < l && ls[i] != he; ++i) {}
+        ls.splice(i, 1);
+      },
+      empty: function() {
+        return EventQueue.list.length === 0;
+      },
+      nextEvent: function(he) {
+        for (var i = 0, ls = EventQueue.list, l = ls.length; i < l; ++i) {
+          if (ls[i] == he) return ls[i + 1];
+        }
+        return null;
+      },
+      min: function() {
+        var elem = EventQueue.list[0];
+        return {
+          x: elem.vertex.x,
+          y: elem.ystar
+        };
+      },
+      extractMin: function() {
+        return EventQueue.list.shift();
+      }
+    };
+    EdgeList.init();
+    Sites.bottomSite = Sites.list.shift();
+    var newSite = Sites.list.shift(), newIntStar;
+    var lbnd, rbnd, llbnd, rrbnd, bisector;
+    var bot, top, temp, p, v;
+    var e, pm;
+    while (true) {
+      if (!EventQueue.empty()) {
+        newIntStar = EventQueue.min();
+      }
+      if (newSite && (EventQueue.empty() || newSite.y < newIntStar.y || newSite.y == newIntStar.y && newSite.x < newIntStar.x)) {
+        lbnd = EdgeList.leftBound(newSite);
+        rbnd = EdgeList.right(lbnd);
+        bot = EdgeList.rightRegion(lbnd);
+        e = Geom.bisect(bot, newSite);
+        bisector = EdgeList.createHalfEdge(e, "l");
+        EdgeList.insert(lbnd, bisector);
+        p = Geom.intersect(lbnd, bisector);
+        if (p) {
+          EventQueue.del(lbnd);
+          EventQueue.insert(lbnd, p, Geom.distance(p, newSite));
+        }
+        lbnd = bisector;
+        bisector = EdgeList.createHalfEdge(e, "r");
+        EdgeList.insert(lbnd, bisector);
+        p = Geom.intersect(bisector, rbnd);
+        if (p) {
+          EventQueue.insert(bisector, p, Geom.distance(p, newSite));
+        }
+        newSite = Sites.list.shift();
+      } else if (!EventQueue.empty()) {
+        lbnd = EventQueue.extractMin();
+        llbnd = EdgeList.left(lbnd);
+        rbnd = EdgeList.right(lbnd);
+        rrbnd = EdgeList.right(rbnd);
+        bot = EdgeList.leftRegion(lbnd);
+        top = EdgeList.rightRegion(rbnd);
+        v = lbnd.vertex;
+        Geom.endPoint(lbnd.edge, lbnd.side, v);
+        Geom.endPoint(rbnd.edge, rbnd.side, v);
+        EdgeList.del(lbnd);
+        EventQueue.del(rbnd);
+        EdgeList.del(rbnd);
+        pm = "l";
+        if (bot.y > top.y) {
+          temp = bot;
+          bot = top;
+          top = temp;
+          pm = "r";
+        }
+        e = Geom.bisect(bot, top);
+        bisector = EdgeList.createHalfEdge(e, pm);
+        EdgeList.insert(llbnd, bisector);
+        Geom.endPoint(e, d3_voronoi_opposite[pm], v);
+        p = Geom.intersect(llbnd, bisector);
+        if (p) {
+          EventQueue.del(llbnd);
+          EventQueue.insert(llbnd, p, Geom.distance(p, bot));
+        }
+        p = Geom.intersect(bisector, rrbnd);
+        if (p) {
+          EventQueue.insert(bisector, p, Geom.distance(p, bot));
+        }
+      } else {
+        break;
+      }
+    }
+    for (lbnd = EdgeList.right(EdgeList.leftEnd); lbnd != EdgeList.rightEnd; lbnd = EdgeList.right(lbnd)) {
+      callback(lbnd.edge);
+    }
+  }
+  d3.geom.delaunay = function(vertices) {
+    var edges = vertices.map(function() {
+      return [];
+    }), triangles = [];
+    d3_voronoi_tessellate(vertices, function(e) {
+      edges[e.region.l.index].push(vertices[e.region.r.index]);
+    });
+    edges.forEach(function(edge, i) {
+      var v = vertices[i], cx = v[0], cy = v[1];
+      edge.forEach(function(v) {
+        v.angle = Math.atan2(v[0] - cx, v[1] - cy);
+      });
+      edge.sort(function(a, b) {
+        return a.angle - b.angle;
+      });
+      for (var j = 0, m = edge.length - 1; j < m; j++) {
+        triangles.push([ v, edge[j], edge[j + 1] ]);
+      }
+    });
+    return triangles;
+  };
+  d3.geom.quadtree = function(points, x1, y1, x2, y2) {
+    var p, i = -1, n = points.length;
+    if (n && isNaN(points[0].x)) points = points.map(d3_geom_quadtreePoint);
+    if (arguments.length < 5) {
+      if (arguments.length === 3) {
+        y2 = x2 = y1;
+        y1 = x1;
+      } else {
+        x1 = y1 = Infinity;
+        x2 = y2 = -Infinity;
+        while (++i < n) {
+          p = points[i];
+          if (p.x < x1) x1 = p.x;
+          if (p.y < y1) y1 = p.y;
+          if (p.x > x2) x2 = p.x;
+          if (p.y > y2) y2 = p.y;
+        }
+        var dx = x2 - x1, dy = y2 - y1;
+        if (dx > dy) y2 = y1 + dx; else x2 = x1 + dy;
+      }
+    }
+    function insert(n, p, x1, y1, x2, y2) {
+      if (isNaN(p.x) || isNaN(p.y)) return;
+      if (n.leaf) {
+        var v = n.point;
+        if (v) {
+          if (Math.abs(v.x - p.x) + Math.abs(v.y - p.y) < .01) {
+            insertChild(n, p, x1, y1, x2, y2);
+          } else {
+            n.point = null;
+            insertChild(n, v, x1, y1, x2, y2);
+            insertChild(n, p, x1, y1, x2, y2);
+          }
+        } else {
+          n.point = p;
+        }
+      } else {
+        insertChild(n, p, x1, y1, x2, y2);
+      }
+    }
+    function insertChild(n, p, x1, y1, x2, y2) {
+      var sx = (x1 + x2) * .5, sy = (y1 + y2) * .5, right = p.x >= sx, bottom = p.y >= sy, i = (bottom << 1) + right;
+      n.leaf = false;
+      n = n.nodes[i] || (n.nodes[i] = d3_geom_quadtreeNode());
+      if (right) x1 = sx; else x2 = sx;
+      if (bottom) y1 = sy; else y2 = sy;
+      insert(n, p, x1, y1, x2, y2);
+    }
+    var root = d3_geom_quadtreeNode();
+    root.add = function(p) {
+      insert(root, p, x1, y1, x2, y2);
+    };
+    root.visit = function(f) {
+      d3_geom_quadtreeVisit(f, root, x1, y1, x2, y2);
+    };
+    points.forEach(root.add);
+    return root;
+  };
+  function d3_geom_quadtreeNode() {
+    return {
+      leaf: true,
+      nodes: [],
+      point: null
+    };
+  }
+  function d3_geom_quadtreeVisit(f, node, x1, y1, x2, y2) {
+    if (!f(node, x1, y1, x2, y2)) {
+      var sx = (x1 + x2) * .5, sy = (y1 + y2) * .5, children = node.nodes;
+      if (children[0]) d3_geom_quadtreeVisit(f, children[0], x1, y1, sx, sy);
+      if (children[1]) d3_geom_quadtreeVisit(f, children[1], sx, y1, x2, sy);
+      if (children[2]) d3_geom_quadtreeVisit(f, children[2], x1, sy, sx, y2);
+      if (children[3]) d3_geom_quadtreeVisit(f, children[3], sx, sy, x2, y2);
+    }
+  }
+  function d3_geom_quadtreePoint(p) {
+    return {
+      x: p[0],
+      y: p[1]
+    };
+  }
+  d3.time = {};
+  var d3_time = Date, d3_time_daySymbols = [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ];
+  function d3_time_utc() {
+    this._ = new Date(arguments.length > 1 ? Date.UTC.apply(this, arguments) : arguments[0]);
+  }
+  d3_time_utc.prototype = {
+    getDate: function() {
+      return this._.getUTCDate();
+    },
+    getDay: function() {
+      return this._.getUTCDay();
+    },
+    getFullYear: function() {
+      return this._.getUTCFullYear();
+    },
+    getHours: function() {
+      return this._.getUTCHours();
+    },
+    getMilliseconds: function() {
+      return this._.getUTCMilliseconds();
+    },
+    getMinutes: function() {
+      return this._.getUTCMinutes();
+    },
+    getMonth: function() {
+      return this._.getUTCMonth();
+    },
+    getSeconds: function() {
+      return this._.getUTCSeconds();
+    },
+    getTime: function() {
+      return this._.getTime();
+    },
+    getTimezoneOffset: function() {
+      return 0;
+    },
+    valueOf: function() {
+      return this._.valueOf();
+    },
+    setDate: function() {
+      d3_time_prototype.setUTCDate.apply(this._, arguments);
+    },
+    setDay: function() {
+      d3_time_prototype.setUTCDay.apply(this._, arguments);
+    },
+    setFullYear: function() {
+      d3_time_prototype.setUTCFullYear.apply(this._, arguments);
+    },
+    setHours: function() {
+      d3_time_prototype.setUTCHours.apply(this._, arguments);
+    },
+    setMilliseconds: function() {
+      d3_time_prototype.setUTCMilliseconds.apply(this._, arguments);
+    },
+    setMinutes: function() {
+      d3_time_prototype.setUTCMinutes.apply(this._, arguments);
+    },
+    setMonth: function() {
+      d3_time_prototype.setUTCMonth.apply(this._, arguments);
+    },
+    setSeconds: function() {
+      d3_time_prototype.setUTCSeconds.apply(this._, arguments);
+    },
+    setTime: function() {
+      d3_time_prototype.setTime.apply(this._, arguments);
+    }
+  };
+  var d3_time_prototype = Date.prototype;
+  var d3_time_formatDateTime = "%a %b %e %H:%M:%S %Y", d3_time_formatDate = "%m/%d/%y", d3_time_formatTime = "%H:%M:%S";
+  var d3_time_days = d3_time_daySymbols, d3_time_dayAbbreviations = d3_time_days.map(d3_time_formatAbbreviate), d3_time_months = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ], d3_time_monthAbbreviations = d3_time_months.map(d3_time_formatAbbreviate);
+  function d3_time_formatAbbreviate(name) {
+    return name.substring(0, 3);
+  }
+  d3.time.format = function(template) {
+    var n = template.length;
+    function format(date) {
+      var string = [], i = -1, j = 0, c, f;
+      while (++i < n) {
+        if (template.charCodeAt(i) == 37) {
+          string.push(template.substring(j, i), (f = d3_time_formats[c = template.charAt(++i)]) ? f(date) : c);
+          j = i + 1;
+        }
+      }
+      string.push(template.substring(j, i));
+      return string.join("");
+    }
+    format.parse = function(string) {
+      var d = {
+        y: 1900,
+        m: 0,
+        d: 1,
+        H: 0,
+        M: 0,
+        S: 0,
+        L: 0
+      }, i = d3_time_parse(d, template, string, 0);
+      if (i != string.length) return null;
+      if ("p" in d) d.H = d.H % 12 + d.p * 12;
+      var date = new d3_time;
+      date.setFullYear(d.y, d.m, d.d);
+      date.setHours(d.H, d.M, d.S, d.L);
+      return date;
+    };
+    format.toString = function() {
+      return template;
+    };
+    return format;
+  };
+  function d3_time_parse(date, template, string, j) {
+    var c, p, i = 0, n = template.length, m = string.length;
+    while (i < n) {
+      if (j >= m) return -1;
+      c = template.charCodeAt(i++);
+      if (c == 37) {
+        p = d3_time_parsers[template.charAt(i++)];
+        if (!p || (j = p(date, string, j)) < 0) return -1;
+      } else if (c != string.charCodeAt(j++)) {
+        return -1;
+      }
+    }
+    return j;
+  }
+  function d3_time_formatRe(names) {
+    return new RegExp("^(?:" + names.map(d3.requote).join("|") + ")", "i");
+  }
+  function d3_time_formatLookup(names) {
+    var map = new d3_Map, i = -1, n = names.length;
+    while (++i < n) map.set(names[i].toLowerCase(), i);
+    return map;
+  }
+  var d3_time_zfill2 = d3.format("02d"), d3_time_zfill3 = d3.format("03d"), d3_time_zfill4 = d3.format("04d"), d3_time_sfill2 = d3.format("2d");
+  var d3_time_dayRe = d3_time_formatRe(d3_time_days), d3_time_dayAbbrevRe = d3_time_formatRe(d3_time_dayAbbreviations), d3_time_monthRe = d3_time_formatRe(d3_time_months), d3_time_monthLookup = d3_time_formatLookup(d3_time_months), d3_time_monthAbbrevRe = d3_time_formatRe(d3_time_monthAbbreviations), d3_time_monthAbbrevLookup = d3_time_formatLookup(d3_time_monthAbbreviations);
+  var d3_time_formats = {
+    a: function(d) {
+      return d3_time_dayAbbreviations[d.getDay()];
+    },
+    A: function(d) {
+      return d3_time_days[d.getDay()];
+    },
+    b: function(d) {
+      return d3_time_monthAbbreviations[d.getMonth()];
+    },
+    B: function(d) {
+      return d3_time_months[d.getMonth()];
+    },
+    c: d3.time.format(d3_time_formatDateTime),
+    d: function(d) {
+      return d3_time_zfill2(d.getDate());
+    },
+    e: function(d) {
+      return d3_time_sfill2(d.getDate());
+    },
+    H: function(d) {
+      return d3_time_zfill2(d.getHours());
+    },
+    I: function(d) {
+      return d3_time_zfill2(d.getHours() % 12 || 12);
+    },
+    j: function(d) {
+      return d3_time_zfill3(1 + d3.time.dayOfYear(d));
+    },
+    L: function(d) {
+      return d3_time_zfill3(d.getMilliseconds());
+    },
+    m: function(d) {
+      return d3_time_zfill2(d.getMonth() + 1);
+    },
+    M: function(d) {
+      return d3_time_zfill2(d.getMinutes());
+    },
+    p: function(d) {
+      return d.getHours() >= 12 ? "PM" : "AM";
+    },
+    S: function(d) {
+      return d3_time_zfill2(d.getSeconds());
+    },
+    U: function(d) {
+      return d3_time_zfill2(d3.time.sundayOfYear(d));
+    },
+    w: function(d) {
+      return d.getDay();
+    },
+    W: function(d) {
+      return d3_time_zfill2(d3.time.mondayOfYear(d));
+    },
+    x: d3.time.format(d3_time_formatDate),
+    X: d3.time.format(d3_time_formatTime),
+    y: function(d) {
+      return d3_time_zfill2(d.getFullYear() % 100);
+    },
+    Y: function(d) {
+      return d3_time_zfill4(d.getFullYear() % 1e4);
+    },
+    Z: d3_time_zone,
+    "%": function(d) {
+      return "%";
+    }
+  };
+  var d3_time_parsers = {
+    a: d3_time_parseWeekdayAbbrev,
+    A: d3_time_parseWeekday,
+    b: d3_time_parseMonthAbbrev,
+    B: d3_time_parseMonth,
+    c: d3_time_parseLocaleFull,
+    d: d3_time_parseDay,
+    e: d3_time_parseDay,
+    H: d3_time_parseHour24,
+    I: d3_time_parseHour24,
+    L: d3_time_parseMilliseconds,
+    m: d3_time_parseMonthNumber,
+    M: d3_time_parseMinutes,
+    p: d3_time_parseAmPm,
+    S: d3_time_parseSeconds,
+    x: d3_time_parseLocaleDate,
+    X: d3_time_parseLocaleTime,
+    y: d3_time_parseYear,
+    Y: d3_time_parseFullYear
+  };
+  function d3_time_parseWeekdayAbbrev(date, string, i) {
+    d3_time_dayAbbrevRe.lastIndex = 0;
+    var n = d3_time_dayAbbrevRe.exec(string.substring(i));
+    return n ? i += n[0].length : -1;
+  }
+  function d3_time_parseWeekday(date, string, i) {
+    d3_time_dayRe.lastIndex = 0;
+    var n = d3_time_dayRe.exec(string.substring(i));
+    return n ? i += n[0].length : -1;
+  }
+  function d3_time_parseMonthAbbrev(date, string, i) {
+    d3_time_monthAbbrevRe.lastIndex = 0;
+    var n = d3_time_monthAbbrevRe.exec(string.substring(i));
+    return n ? (date.m = d3_time_monthAbbrevLookup.get(n[0].toLowerCase()), i += n[0].length) : -1;
+  }
+  function d3_time_parseMonth(date, string, i) {
+    d3_time_monthRe.lastIndex = 0;
+    var n = d3_time_monthRe.exec(string.substring(i));
+    return n ? (date.m = d3_time_monthLookup.get(n[0].toLowerCase()), i += n[0].length) : -1;
+  }
+  function d3_time_parseLocaleFull(date, string, i) {
+    return d3_time_parse(date, d3_time_formats.c.toString(), string, i);
+  }
+  function d3_time_parseLocaleDate(date, string, i) {
+    return d3_time_parse(date, d3_time_formats.x.toString(), string, i);
+  }
+  function d3_time_parseLocaleTime(date, string, i) {
+    return d3_time_parse(date, d3_time_formats.X.toString(), string, i);
+  }
+  function d3_time_parseFullYear(date, string, i) {
+    d3_time_numberRe.lastIndex = 0;
+    var n = d3_time_numberRe.exec(string.substring(i, i + 4));
+    return n ? (date.y = +n[0], i += n[0].length) : -1;
+  }
+  function d3_time_parseYear(date, string, i) {
+    d3_time_numberRe.lastIndex = 0;
+    var n = d3_time_numberRe.exec(string.substring(i, i + 2));
+    return n ? (date.y = d3_time_expandYear(+n[0]), i += n[0].length) : -1;
+  }
+  function d3_time_expandYear(d) {
+    return d + (d > 68 ? 1900 : 2e3);
+  }
+  function d3_time_parseMonthNumber(date, string, i) {
+    d3_time_numberRe.lastIndex = 0;
+    var n = d3_time_numberRe.exec(string.substring(i, i + 2));
+    return n ? (date.m = n[0] - 1, i += n[0].length) : -1;
+  }
+  function d3_time_parseDay(date, string, i) {
+    d3_time_numberRe.lastIndex = 0;
+    var n = d3_time_numberRe.exec(string.substring(i, i + 2));
+    return n ? (date.d = +n[0], i += n[0].length) : -1;
+  }
+  function d3_time_parseHour24(date, string, i) {
+    d3_time_numberRe.lastIndex = 0;
+    var n = d3_time_numberRe.exec(string.substring(i, i + 2));
+    return n ? (date.H = +n[0], i += n[0].length) : -1;
+  }
+  function d3_time_parseMinutes(date, string, i) {
+    d3_time_numberRe.lastIndex = 0;
+    var n = d3_time_numberRe.exec(string.substring(i, i + 2));
+    return n ? (date.M = +n[0], i += n[0].length) : -1;
+  }
+  function d3_time_parseSeconds(date, string, i) {
+    d3_time_numberRe.lastIndex = 0;
+    var n = d3_time_numberRe.exec(string.substring(i, i + 2));
+    return n ? (date.S = +n[0], i += n[0].length) : -1;
+  }
+  function d3_time_parseMilliseconds(date, string, i) {
+    d3_time_numberRe.lastIndex = 0;
+    var n = d3_time_numberRe.exec(string.substring(i, i + 3));
+    return n ? (date.L = +n[0], i += n[0].length) : -1;
+  }
+  var d3_time_numberRe = /^\s*\d+/;
+  function d3_time_parseAmPm(date, string, i) {
+    var n = d3_time_amPmLookup.get(string.substring(i, i += 2).toLowerCase());
+    return n == null ? -1 : (date.p = n, i);
+  }
+  var d3_time_amPmLookup = d3.map({
+    am: 0,
+    pm: 1
+  });
+  function d3_time_zone(d) {
+    var z = d.getTimezoneOffset(), zs = z > 0 ? "-" : "+", zh = ~~(Math.abs(z) / 60), zm = Math.abs(z) % 60;
+    return zs + d3_time_zfill2(zh) + d3_time_zfill2(zm);
+  }
+  d3.time.format.utc = function(template) {
+    var local = d3.time.format(template);
+    function format(date) {
+      try {
+        d3_time = d3_time_utc;
+        var utc = new d3_time;
+        utc._ = date;
+        return local(utc);
+      } finally {
+        d3_time = Date;
+      }
+    }
+    format.parse = function(string) {
+      try {
+        d3_time = d3_time_utc;
+        var date = local.parse(string);
+        return date && date._;
+      } finally {
+        d3_time = Date;
+      }
+    };
+    format.toString = local.toString;
+    return format;
+  };
+  var d3_time_formatIso = d3.time.format.utc("%Y-%m-%dT%H:%M:%S.%LZ");
+  d3.time.format.iso = Date.prototype.toISOString ? d3_time_formatIsoNative : d3_time_formatIso;
+  function d3_time_formatIsoNative(date) {
+    return date.toISOString();
+  }
+  d3_time_formatIsoNative.parse = function(string) {
+    var date = new Date(string);
+    return isNaN(date) ? null : date;
+  };
+  d3_time_formatIsoNative.toString = d3_time_formatIso.toString;
+  function d3_time_interval(local, step, number) {
+    function round(date) {
+      var d0 = local(date), d1 = offset(d0, 1);
+      return date - d0 < d1 - date ? d0 : d1;
+    }
+    function ceil(date) {
+      step(date = local(new d3_time(date - 1)), 1);
+      return date;
+    }
+    function offset(date, k) {
+      step(date = new d3_time(+date), k);
+      return date;
+    }
+    function range(t0, t1, dt) {
+      var time = ceil(t0), times = [];
+      if (dt > 1) {
+        while (time < t1) {
+          if (!(number(time) % dt)) times.push(new Date(+time));
+          step(time, 1);
+        }
+      } else {
+        while (time < t1) times.push(new Date(+time)), step(time, 1);
+      }
+      return times;
+    }
+    function range_utc(t0, t1, dt) {
+      try {
+        d3_time = d3_time_utc;
+        var utc = new d3_time_utc;
+        utc._ = t0;
+        return range(utc, t1, dt);
+      } finally {
+        d3_time = Date;
+      }
+    }
+    local.floor = local;
+    local.round = round;
+    local.ceil = ceil;
+    local.offset = offset;
+    local.range = range;
+    var utc = local.utc = d3_time_interval_utc(local);
+    utc.floor = utc;
+    utc.round = d3_time_interval_utc(round);
+    utc.ceil = d3_time_interval_utc(ceil);
+    utc.offset = d3_time_interval_utc(offset);
+    utc.range = range_utc;
+    return local;
+  }
+  function d3_time_interval_utc(method) {
+    return function(date, k) {
+      try {
+        d3_time = d3_time_utc;
+        var utc = new d3_time_utc;
+        utc._ = date;
+        return method(utc, k)._;
+      } finally {
+        d3_time = Date;
+      }
+    };
+  }
+  d3.time.second = d3_time_interval(function(date) {
+    return new d3_time(Math.floor(date / 1e3) * 1e3);
+  }, function(date, offset) {
+    date.setTime(date.getTime() + Math.floor(offset) * 1e3);
+  }, function(date) {
+    return date.getSeconds();
+  });
+  d3.time.seconds = d3.time.second.range;
+  d3.time.seconds.utc = d3.time.second.utc.range;
+  d3.time.minute = d3_time_interval(function(date) {
+    return new d3_time(Math.floor(date / 6e4) * 6e4);
+  }, function(date, offset) {
+    date.setTime(date.getTime() + Math.floor(offset) * 6e4);
+  }, function(date) {
+    return date.getMinutes();
+  });
+  d3.time.minutes = d3.time.minute.range;
+  d3.time.minutes.utc = d3.time.minute.utc.range;
+  d3.time.hour = d3_time_interval(function(date) {
+    var timezone = date.getTimezoneOffset() / 60;
+    return new d3_time((Math.floor(date / 36e5 - timezone) + timezone) * 36e5);
+  }, function(date, offset) {
+    date.setTime(date.getTime() + Math.floor(offset) * 36e5);
+  }, function(date) {
+    return date.getHours();
+  });
+  d3.time.hours = d3.time.hour.range;
+  d3.time.hours.utc = d3.time.hour.utc.range;
+  d3.time.day = d3_time_interval(function(date) {
+    var day = new d3_time(0, date.getMonth(), date.getDate());
+    day.setFullYear(date.getFullYear());
+    return day;
+  }, function(date, offset) {
+    date.setDate(date.getDate() + offset);
+  }, function(date) {
+    return date.getDate() - 1;
+  });
+  d3.time.days = d3.time.day.range;
+  d3.time.days.utc = d3.time.day.utc.range;
+  d3.time.dayOfYear = function(date) {
+    var year = d3.time.year(date);
+    return Math.floor((date - year - (date.getTimezoneOffset() - year.getTimezoneOffset()) * 6e4) / 864e5);
+  };
+  d3_time_daySymbols.forEach(function(day, i) {
+    day = day.toLowerCase();
+    i = 7 - i;
+    var interval = d3.time[day] = d3_time_interval(function(date) {
+      (date = d3.time.day(date)).setDate(date.getDate() - (date.getDay() + i) % 7);
+      return date;
+    }, function(date, offset) {
+      date.setDate(date.getDate() + Math.floor(offset) * 7);
+    }, function(date) {
+      var day = d3.time.year(date).getDay();
+      return Math.floor((d3.time.dayOfYear(date) + (day + i) % 7) / 7) - (day !== i);
+    });
+    d3.time[day + "s"] = interval.range;
+    d3.time[day + "s"].utc = interval.utc.range;
+    d3.time[day + "OfYear"] = function(date) {
+      var day = d3.time.year(date).getDay();
+      return Math.floor((d3.time.dayOfYear(date) + (day + i) % 7) / 7);
+    };
+  });
+  d3.time.week = d3.time.sunday;
+  d3.time.weeks = d3.time.sunday.range;
+  d3.time.weeks.utc = d3.time.sunday.utc.range;
+  d3.time.weekOfYear = d3.time.sundayOfYear;
+  d3.time.month = d3_time_interval(function(date) {
+    date = d3.time.day(date);
+    date.setDate(1);
+    return date;
+  }, function(date, offset) {
+    date.setMonth(date.getMonth() + offset);
+  }, function(date) {
+    return date.getMonth();
+  });
+  d3.time.months = d3.time.month.range;
+  d3.time.months.utc = d3.time.month.utc.range;
+  d3.time.year = d3_time_interval(function(date) {
+    date = d3.time.day(date);
+    date.setMonth(0, 1);
+    return date;
+  }, function(date, offset) {
+    date.setFullYear(date.getFullYear() + offset);
+  }, function(date) {
+    return date.getFullYear();
+  });
+  d3.time.years = d3.time.year.range;
+  d3.time.years.utc = d3.time.year.utc.range;
+  function d3_time_scale(linear, methods, format) {
+    function scale(x) {
+      return linear(x);
+    }
+    scale.invert = function(x) {
+      return d3_time_scaleDate(linear.invert(x));
+    };
+    scale.domain = function(x) {
+      if (!arguments.length) return linear.domain().map(d3_time_scaleDate);
+      linear.domain(x);
+      return scale;
+    };
+    scale.nice = function(m) {
+      return scale.domain(d3_scale_nice(scale.domain(), function() {
+        return m;
+      }));
+    };
+    scale.ticks = function(m, k) {
+      var extent = d3_time_scaleExtent(scale.domain());
+      if (typeof m !== "function") {
+        var span = extent[1] - extent[0], target = span / m, i = d3.bisect(d3_time_scaleSteps, target);
+        if (i == d3_time_scaleSteps.length) return methods.year(extent, m);
+        if (!i) return linear.ticks(m).map(d3_time_scaleDate);
+        if (Math.log(target / d3_time_scaleSteps[i - 1]) < Math.log(d3_time_scaleSteps[i] / target)) --i;
+        m = methods[i];
+        k = m[1];
+        m = m[0].range;
+      }
+      return m(extent[0], new Date(+extent[1] + 1), k);
+    };
+    scale.tickFormat = function() {
+      return format;
+    };
+    scale.copy = function() {
+      return d3_time_scale(linear.copy(), methods, format);
+    };
+    return d3.rebind(scale, linear, "range", "rangeRound", "interpolate", "clamp");
+  }
+  function d3_time_scaleExtent(domain) {
+    var start = domain[0], stop = domain[domain.length - 1];
+    return start < stop ? [ start, stop ] : [ stop, start ];
+  }
+  function d3_time_scaleDate(t) {
+    return new Date(t);
+  }
+  function d3_time_scaleFormat(formats) {
+    return function(date) {
+      var i = formats.length - 1, f = formats[i];
+      while (!f[1](date)) f = formats[--i];
+      return f[0](date);
+    };
+  }
+  function d3_time_scaleSetYear(y) {
+    var d = new Date(y, 0, 1);
+    d.setFullYear(y);
+    return d;
+  }
+  function d3_time_scaleGetYear(d) {
+    var y = d.getFullYear(), d0 = d3_time_scaleSetYear(y), d1 = d3_time_scaleSetYear(y + 1);
+    return y + (d - d0) / (d1 - d0);
+  }
+  var d3_time_scaleSteps = [ 1e3, 5e3, 15e3, 3e4, 6e4, 3e5, 9e5, 18e5, 36e5, 108e5, 216e5, 432e5, 864e5, 1728e5, 6048e5, 2592e6, 7776e6, 31536e6 ];
+  var d3_time_scaleLocalMethods = [ [ d3.time.second, 1 ], [ d3.time.second, 5 ], [ d3.time.second, 15 ], [ d3.time.second, 30 ], [ d3.time.minute, 1 ], [ d3.time.minute, 5 ], [ d3.time.minute, 15 ], [ d3.time.minute, 30 ], [ d3.time.hour, 1 ], [ d3.time.hour, 3 ], [ d3.time.hour, 6 ], [ d3.time.hour, 12 ], [ d3.time.day, 1 ], [ d3.time.day, 2 ], [ d3.time.week, 1 ], [ d3.time.month, 1 ], [ d3.time.month, 3 ], [ d3.time.year, 1 ] ];
+  var d3_time_scaleLocalFormats = [ [ d3.time.format("%Y"), function(d) {
+    return true;
+  } ], [ d3.time.format("%B"), function(d) {
+    return d.getMonth();
+  } ], [ d3.time.format("%b %d"), function(d) {
+    return d.getDate() != 1;
+  } ], [ d3.time.format("%a %d"), function(d) {
+    return d.getDay() && d.getDate() != 1;
+  } ], [ d3.time.format("%I %p"), function(d) {
+    return d.getHours();
+  } ], [ d3.time.format("%I:%M"), function(d) {
+    return d.getMinutes();
+  } ], [ d3.time.format(":%S"), function(d) {
+    return d.getSeconds();
+  } ], [ d3.time.format(".%L"), function(d) {
+    return d.getMilliseconds();
+  } ] ];
+  var d3_time_scaleLinear = d3.scale.linear(), d3_time_scaleLocalFormat = d3_time_scaleFormat(d3_time_scaleLocalFormats);
+  d3_time_scaleLocalMethods.year = function(extent, m) {
+    return d3_time_scaleLinear.domain(extent.map(d3_time_scaleGetYear)).ticks(m).map(d3_time_scaleSetYear);
+  };
+  d3.time.scale = function() {
+    return d3_time_scale(d3.scale.linear(), d3_time_scaleLocalMethods, d3_time_scaleLocalFormat);
+  };
+  var d3_time_scaleUTCMethods = d3_time_scaleLocalMethods.map(function(m) {
+    return [ m[0].utc, m[1] ];
+  });
+  var d3_time_scaleUTCFormats = [ [ d3.time.format.utc("%Y"), function(d) {
+    return true;
+  } ], [ d3.time.format.utc("%B"), function(d) {
+    return d.getUTCMonth();
+  } ], [ d3.time.format.utc("%b %d"), function(d) {
+    return d.getUTCDate() != 1;
+  } ], [ d3.time.format.utc("%a %d"), function(d) {
+    return d.getUTCDay() && d.getUTCDate() != 1;
+  } ], [ d3.time.format.utc("%I %p"), function(d) {
+    return d.getUTCHours();
+  } ], [ d3.time.format.utc("%I:%M"), function(d) {
+    return d.getUTCMinutes();
+  } ], [ d3.time.format.utc(":%S"), function(d) {
+    return d.getUTCSeconds();
+  } ], [ d3.time.format.utc(".%L"), function(d) {
+    return d.getUTCMilliseconds();
+  } ] ];
+  var d3_time_scaleUTCFormat = d3_time_scaleFormat(d3_time_scaleUTCFormats);
+  function d3_time_scaleUTCSetYear(y) {
+    var d = new Date(Date.UTC(y, 0, 1));
+    d.setUTCFullYear(y);
+    return d;
+  }
+  function d3_time_scaleUTCGetYear(d) {
+    var y = d.getUTCFullYear(), d0 = d3_time_scaleUTCSetYear(y), d1 = d3_time_scaleUTCSetYear(y + 1);
+    return y + (d - d0) / (d1 - d0);
+  }
+  d3_time_scaleUTCMethods.year = function(extent, m) {
+    return d3_time_scaleLinear.domain(extent.map(d3_time_scaleUTCGetYear)).ticks(m).map(d3_time_scaleUTCSetYear);
+  };
+  d3.time.scale.utc = function() {
+    return d3_time_scale(d3.scale.linear(), d3_time_scaleUTCMethods, d3_time_scaleUTCFormat);
+  };
+})();
\ No newline at end of file
diff --git a/examples/other/template-demo/client/template-demo.css b/examples/other/template-demo/client/template-demo.css
new file mode 100644
index 0000000000..4506e4bebb
--- /dev/null
+++ b/examples/other/template-demo/client/template-demo.css
@@ -0,0 +1,44 @@
+body {
+    font-family: 'Helvetica Neue', Helvetica, Arial, san-serif;
+    width: 600px;
+    margin: auto;
+    padding: 25px 50px;
+    border: 5px dashed #ccc;
+    border-style: none dashed;
+}
+
+h2 {
+    margin-top: 50px;
+    text-decoration: underline;
+}
+
+.clearboth {
+    clear: both;
+}
+
+@-webkit-keyframes spinForward {
+    from {-webkit-transform: rotate(0deg);}
+    to {-webkit-transform: rotate(360deg);}
+}
+
+@-webkit-keyframes spinBackward {
+    from {-webkit-transform: rotate(360deg);}
+    to {-webkit-transform: rotate(0deg);}
+}
+
+.spinner {
+    width: 100px;
+    border: 2px solid black;
+    font-weight: bold;
+    text-align: center;
+    background: white;
+}
+
+.circles {
+    float: left;
+    padding-right: 20px;
+}
+
+.circles svg {
+    border: 2px solid #333;
+}
\ No newline at end of file
diff --git a/examples/other/template-demo/client/template-demo.html b/examples/other/template-demo/client/template-demo.html
new file mode 100644
index 0000000000..d1736b5871
--- /dev/null
+++ b/examples/other/template-demo/client/template-demo.html
@@ -0,0 +1,182 @@
+
+  Advanced Template Demo
+
+
+
+  {{> page}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/other/template-demo/client/template-demo.js b/examples/other/template-demo/client/template-demo.js
new file mode 100644
index 0000000000..0f2f9fe4bd
--- /dev/null
+++ b/examples/other/template-demo/client/template-demo.js
@@ -0,0 +1,296 @@
+Timers = new Meteor.Collection(null);
+
+///////////////////////////////////////////////////////////////////////////////
+
+if (! Session.get("x")) {
+  Session.set("x", 1);
+}
+
+if (! Session.get("y")) {
+  Session.set("y", 1);
+}
+
+if (! Session.get("z")) {
+  Session.set("z", 1);
+}
+
+Template.preserveDemo.x =
+Template.constantDemo.x =
+Template.stateDemo.x =
+function () {
+  return Session.get("x");
+};
+
+Template.timer.y = function () {
+  return Session.get("y");
+};
+
+Template.stateDemo.z =
+function () {
+  return Session.get("z");
+};
+
+Template.page.events({
+  'click input.x': function () {
+    Session.set("x", Session.get("x") + 1);
+  },
+
+  'click input.y': function () {
+    Session.set("y", Session.get("y") + 1);
+  },
+
+  'click input.z': function () {
+    Session.set("z", Session.get("z") + 1);
+  }
+});
+
+///////////////////////////////////////////////////////////////////////////////
+
+if (typeof Session.get("spinForward") !== 'boolean') {
+  Session.set("spinForward", true);
+}
+
+Template.preserveDemo.preserve([ '.spinner', '.spinforward' ]);
+
+Template.preserveDemo.spinForwardChecked = function () {
+  return Session.get('spinForward') ? 'checked="checked"' : '';
+};
+
+Template.preserveDemo.spinAnim = function () {
+  return Session.get('spinForward') ? 'spinForward' : 'spinBackward';
+};
+
+Template.preserveDemo.events({
+  'change .spinforward' : function (event) {
+    Session.set('spinForward', event.currentTarget.checked);
+  }
+});
+
+///////////////////////////////////////////////////////////////////////////////
+
+Template.constantDemo.checked = function (which) {
+  return Session.get('mapchecked' + which) ? 'checked="checked"' : '';
+};
+
+Template.constantDemo.show = function (which) {
+  return ! Session.get('mapchecked' + which);
+};
+
+Template.constantDemo.events({
+  'change .remove' : function (event) {
+    var tgt = event.currentTarget;
+    Session.set('mapchecked' + tgt.getAttribute("which"), tgt.checked);
+  }
+});
+
+///////////////////////////////////////////////////////////////////////////////
+
+Template.stateDemo.events({
+  'click .create': function () {
+    Timers.insert({});
+  }
+});
+
+Template.stateDemo.timers = function () {
+  return Timers.find();
+};
+
+Template.timer.events({
+  'click .reset': function (event, template) {
+    template.elapsed = 0;
+    updateTimer(template);
+  },
+  'click .delete': function () {
+    Timers.remove(this._id);
+  }
+});
+
+var updateTimer = function (timer) {
+  timer.node.innerHTML = timer.elapsed + " second" +
+    ((timer.elapsed === 1) ? "" : "s");
+};
+
+Template.timer.created = function () {
+  var self = this;
+  self.elapsed = 0;
+  self.node = null;
+};
+
+Template.timer.rendered = function () {
+  var self = this;
+  self.node = this.find(".elapsed");
+  updateTimer(self);
+
+  if (! self.timer) {
+    var tick = function () {
+      self.elapsed++;
+      self.timer = setTimeout(tick, 1000);
+      updateTimer(self);
+    };
+    tick();
+  }
+};
+
+Template.timer.destroyed = function () {
+  clearInterval(this.timer);
+};
+
+///////////////////////////////////////////////////////////////////////////////
+
+// Run f(). Record its dependencies. Rerun it whenever the
+// dependencies change.
+//
+// Returns an object with a stop() method. Call stop() to stop the
+// rerunning.
+//
+// XXX this should go into Meteor core as Meteor.autorun
+var autorun = function (f) {
+  var ctx;
+  var slain = false;
+  var rerun = function () {
+    if (slain)
+      return;
+    ctx = new Meteor.deps.Context;
+    ctx.run(f);
+    ctx.on_invalidate(rerun);
+  };
+  rerun();
+  return {
+    stop: function () {
+      slain = true;
+      ctx.invalidate();
+    }
+  };
+};
+
+Template.d3Demo.left = function () {
+  return { group: "left" };
+};
+
+Template.d3Demo.right = function () {
+  return { group: "right" };
+};
+
+Template.circles.events({
+  'mousedown circle': function (evt, template) {
+    Session.set("selectedCircle:" + this.group, evt.currentTarget.id);
+  },
+  'click .add': function () {
+    Circles.insert({x: Meteor.random(), y: Meteor.random(),
+                    r: Meteor.random() * .1 + .02,
+                    color: {
+                      r: Meteor.random(),
+                      g: Meteor.random(),
+                      b: Meteor.random()
+                    },
+                    group: this.group
+                   });
+  },
+  'click .remove': function () {
+    var selected = Session.get("selectedCircle:" + this.group);
+    if (selected) {
+      Circles.remove(selected);
+      Session.set("selectedCircle:" + this.group, null);
+    }
+  },
+  'click .scram': function () {
+    Circles.find({group: this.group}).forEach(function (r) {
+      Circles.update(r._id, {
+        $set: {
+          x: Meteor.random(), y: Meteor.random(), r: Meteor.random() * .1 + .02
+        }
+      });
+    });
+  }
+});
+
+var colorToString = function (color) {
+  var f = function (x) { return Math.floor(x * 256); };
+  return "rgb(" + f(color.r) + "," +
+    + f(color.g) + "," + + f(color.b) + ")";
+};
+
+Template.circles.count = function () {
+  return Circles.find({group: this.group}).count();
+};
+
+Template.circles.disabled = function () {
+  return Session.get("selectedCircle:" + this.group) ?
+    '' : 'disabled="disabled"';
+};
+
+Template.circles.created = function () {
+};
+
+Template.circles.rendered = function () {
+  var self = this;
+  self.node = self.find("svg");
+
+  var data = self.data;
+
+  if (! self.handle) {
+    d3.select(self.node).append("rect");
+    self.handle = autorun(function () {
+      var circle = d3.select(self.node).selectAll("circle")
+        .data(Circles.find({group: data.group}).fetch(),
+              function (d) { return d._id; });
+
+      circle.enter().append("circle")
+        .attr("id", function (d) {
+          return d._id;
+        })
+        .attr("cx", function (d) {
+          return d.x * 272;
+        })
+        .attr("cy", function (d) {
+          return d.y * 272;
+        })
+        .attr("r", 50)
+        .style("fill", function (d) {
+          return colorToString(d.color);
+        })
+        .style("opacity", 0);
+
+      circle.transition()
+        .duration(250)
+        .attr("cx", function (d) {
+          return d.x * 272;
+        })
+        .attr("cy", function (d) {
+          return d.y * 272;
+        })
+        .attr("r", function (d) {
+          return d.r * 272;
+        })
+        .style("fill", function (d) {
+          return colorToString(d.color);
+        })
+        .style("opacity", .9)
+        .ease("cubic-out");
+
+      circle.exit().transition()
+        .duration(250)
+        .attr("r", 0)
+        .remove();
+
+      var selectionId = Session.get("selectedCircle:" + data.group);
+      var s = selectionId && Circles.findOne(selectionId);
+      var rect = d3.select(self.node).select("rect");
+      if (s)
+        rect.attr("x", (s.x - s.r) * 272)
+        .attr("y", (s.y - s.r) * 272)
+        .attr("width", s.r * 2 * 272)
+        .attr("height", s.r * 2 * 272)
+        .attr("display", '')
+        .style("fill", "none")
+        .style("stroke", "red")
+        .style("stroke-width", 3);
+      else
+        rect.attr("display", 'none');
+    });
+  }
+};
+
+Template.circles.destroyed = function () {
+  this.handle && this.handle.stop();
+};
diff --git a/examples/other/template-demo/model.js b/examples/other/template-demo/model.js
new file mode 100644
index 0000000000..ff5a32e480
--- /dev/null
+++ b/examples/other/template-demo/model.js
@@ -0,0 +1 @@
+Circles = new Meteor.Collection("circles");
diff --git a/examples/todos/client/todos.js b/examples/todos/client/todos.js
index 307f1fcc69..5e19a81cb5 100644
--- a/examples/todos/client/todos.js
+++ b/examples/todos/client/todos.js
@@ -39,42 +39,36 @@ Meteor.autosubscribe(function () {
 
 ////////// Helpers for in-place editing //////////
 
-// Returns an event_map key for attaching "ok/cancel" events to
-// a text input (given by selector)
-var okcancel_events = function (selector) {
-  return 'keyup '+selector+', keydown '+selector+', focusout '+selector;
-};
+// Returns an event map that handles the "escape" and "return" keys and
+// "blur" events on a text input (given by selector) and interprets them
+// as "ok" or "cancel".
+var okCancelEvents = function (selector, callbacks) {
+  var ok = callbacks.ok || function () {};
+  var cancel = callbacks.cancel || function () {};
 
-// Creates an event handler for interpreting "escape", "return", and "blur"
-// on a text field and calling "ok" or "cancel" callbacks.
-var make_okcancel_handler = function (options) {
-  var ok = options.ok || function () {};
-  var cancel = options.cancel || function () {};
-
-  return function (evt) {
-    if (evt.type === "keydown" && evt.which === 27) {
-      // escape = cancel
-      cancel.call(this, evt);
-
-    } else if (evt.type === "keyup" && evt.which === 13 ||
-               evt.type === "focusout") {
-      // blur/return/enter = ok/submit if non-empty
-      var value = String(evt.target.value || "");
-      if (value)
-        ok.call(this, value, evt);
-      else
+  var events = {};
+  events['keyup '+selector+', keydown '+selector+', focusout '+selector] =
+    function (evt) {
+      if (evt.type === "keydown" && evt.which === 27) {
+        // escape = cancel
         cancel.call(this, evt);
-    }
-  };
+
+      } else if (evt.type === "keyup" && evt.which === 13 ||
+                 evt.type === "focusout") {
+        // blur/return/enter = ok/submit if non-empty
+        var value = String(evt.target.value || "");
+        if (value)
+          ok.call(this, value, evt);
+        else
+          cancel.call(this, evt);
+      }
+    };
+  return events;
 };
 
-// Finds a text input in the DOM by id and focuses it.
-var focus_field_by_id = function (id) {
-  var input = document.getElementById(id);
-  if (input) {
-    input.focus();
-    input.select();
-  }
+var activateInput = function (input) {
+  input.focus();
+  input.select();
 };
 
 ////////// Lists //////////
@@ -83,7 +77,7 @@ Template.lists.lists = function () {
   return Lists.find({}, {sort: {name: 1}});
 };
 
-Template.lists.events = {
+Template.lists.events({
   'mousedown .list': function (evt) { // select list
     Router.setList(this._id);
   },
@@ -91,15 +85,27 @@ Template.lists.events = {
     // prevent clicks on  from refreshing the page.
     evt.preventDefault();
   },
-  'dblclick .list': function (evt) { // start editing list name
+  'dblclick .list': function (evt, tmpl) { // start editing list name
     Session.set('editing_listname', this._id);
     Meteor.flush(); // force DOM redraw, so we can focus the edit field
-    focus_field_by_id("list-name-input");
+    activateInput(tmpl.find("#list-name-input"));
   }
-};
+});
 
-Template.lists.events[ okcancel_events('#list-name-input') ] =
-  make_okcancel_handler({
+// Attach events to keydown, keyup, and blur on "New list" input box.
+Template.lists.events(okCancelEvents(
+  '#new-list',
+  {
+    ok: function (text, evt) {
+      var id = Lists.insert({name: text});
+      Router.setList(id);
+      evt.target.value = "";
+    }
+  }));
+
+Template.lists.events(okCancelEvents(
+  '#list-name-input',
+  {
     ok: function (value) {
       Lists.update(this._id, {$set: {name: value}});
       Session.set('editing_listname', null);
@@ -107,17 +113,7 @@ Template.lists.events[ okcancel_events('#list-name-input') ] =
     cancel: function () {
       Session.set('editing_listname', null);
     }
-  });
-
-// Attach events to keydown, keyup, and blur on "New list" input box.
-Template.lists.events[ okcancel_events('#new-list') ] =
-  make_okcancel_handler({
-    ok: function (text, evt) {
-      var id = Lists.insert({name: text});
-      Router.setList(id);
-      evt.target.value = "";
-    }
-  });
+  }));
 
 Template.lists.selected = function () {
   return Session.equals('list_id', this._id) ? 'selected' : '';
@@ -131,16 +127,19 @@ Template.lists.editing = function () {
   return Session.equals('editing_listname', this._id);
 };
 
+// Preserve text input fields so that they aren't replaced
+// while the user is typing in them.
+Template.lists.preserve(['#list-name-input', '#new-list']);
+
 ////////// Todos //////////
 
 Template.todos.any_list_selected = function () {
   return !Session.equals('list_id', null);
 };
 
-Template.todos.events = {};
-
-Template.todos.events[ okcancel_events('#new-todo') ] =
-  make_okcancel_handler({
+Template.todos.events(okCancelEvents(
+  '#new-todo',
+  {
     ok: function (text, evt) {
       var tag = Session.get('tag_filter');
       Todos.insert({
@@ -152,7 +151,7 @@ Template.todos.events[ okcancel_events('#new-todo') ] =
       });
       evt.target.value = '';
     }
-  });
+  }));
 
 Template.todos.todos = function () {
   // Determine which todos to display in main pane,
@@ -170,6 +169,8 @@ Template.todos.todos = function () {
   return Todos.find(sel, {sort: {timestamp: 1}});
 };
 
+Template.todos.preserve(['#new-todo']);
+
 Template.todo_item.tag_objs = function () {
   var todo_id = this._id;
   return _.map(this.tags || [], function (tag) {
@@ -193,7 +194,7 @@ Template.todo_item.adding_tag = function () {
   return Session.equals('editing_addtag', this._id);
 };
 
-Template.todo_item.events = {
+Template.todo_item.events({
   'click .check': function () {
     Todos.update(this._id, {$set: {done: !this.done}});
   },
@@ -202,16 +203,16 @@ Template.todo_item.events = {
     Todos.remove(this._id);
   },
 
-  'click .addtag': function (evt) {
+  'click .addtag': function (evt, tmpl) {
     Session.set('editing_addtag', this._id);
     Meteor.flush(); // update DOM before focus
-    focus_field_by_id("edittag-input");
+    activateInput(tmpl.find("#edittag-input"));
   },
 
-  'dblclick .display .todo-text': function (evt) {
+  'dblclick .display .todo-text': function (evt, tmpl) {
     Session.set('editing_itemname', this._id);
     Meteor.flush(); // update DOM before focus
-    focus_field_by_id("todo-input");
+    activateInput(tmpl.find("#todo-input"));
   },
 
   'click .remove': function (evt) {
@@ -234,10 +235,12 @@ Template.todo_item.events = {
       privateTo: Meteor.user()._id
     }});
   }
-};
+});
 
-Template.todo_item.events[ okcancel_events('#todo-input') ] =
-  make_okcancel_handler({
+
+Template.todo_item.events(okCancelEvents(
+  '#todo-input',
+  {
     ok: function (value) {
       Todos.update(this._id, {$set: {text: value}});
       Session.set('editing_itemname', null);
@@ -245,10 +248,11 @@ Template.todo_item.events[ okcancel_events('#todo-input') ] =
     cancel: function () {
       Session.set('editing_itemname', null);
     }
-  });
+  }));
 
-Template.todo_item.events[ okcancel_events('#edittag-input') ] =
-  make_okcancel_handler({
+Template.todo_item.events(okCancelEvents(
+  '#edittag-input',
+  {
     ok: function (value) {
       Todos.update(this._id, {$addToSet: {tags: value}});
       Session.set('editing_addtag', null);
@@ -256,7 +260,9 @@ Template.todo_item.events[ okcancel_events('#edittag-input') ] =
     cancel: function () {
       Session.set('editing_addtag', null);
     }
-  });
+  }));
+
+Template.todo_item.preserve(['#todo-input', '#edittag-input']);
 
 ////////// Tag Filter //////////
 
@@ -290,14 +296,14 @@ Template.tag_filter.selected = function () {
   return Session.equals('tag_filter', this.tag) ? 'selected' : '';
 };
 
-Template.tag_filter.events = {
+Template.tag_filter.events({
   'mousedown .tag': function () {
     if (Session.equals('tag_filter', this.tag))
       Session.set('tag_filter', null);
     else
       Session.set('tag_filter', this.tag);
   }
-};
+});
 
 ////////// Tracking selected list in URL //////////
 
diff --git a/examples/wordplay/client/wordplay.html b/examples/wordplay/client/wordplay.html
index 2149623fde..90fb54aefc 100644
--- a/examples/wordplay/client/wordplay.html
+++ b/examples/wordplay/client/wordplay.html
@@ -3,6 +3,10 @@
 
 
 
+  {{> page}}
+
+
+
 
 
diff --git a/packages/liveui/liveui_tests.js b/packages/liveui/liveui_tests.js
deleted file mode 100644
index 4824e60e7b..0000000000
--- a/packages/liveui/liveui_tests.js
+++ /dev/null
@@ -1,2000 +0,0 @@
-
-(function() {
-
-///// ReactiveVar /////
-
-var ReactiveVar = function(initialValue) {
-  if (! (this instanceof ReactiveVar))
-    return new ReactiveVar(initialValue);
-
-  this._value = (typeof initialValue === "undefined" ? null :
-                 initialValue);
-  this._deps = {};
-};
-ReactiveVar.prototype.get = function() {
-  var context = Meteor.deps.Context.current;
-  if (context && !(context.id in this._deps)) {
-    this._deps[context.id] = context;
-    var self = this;
-    context.on_invalidate(function() {
-      delete self._deps[context.id];
-    });
-  }
-
-  return this._value;
-};
-
-ReactiveVar.prototype.set = function(newValue) {
-  this._value = newValue;
-
-  for(var id in this._deps)
-    this._deps[id].invalidate();
-
-};
-
-ReactiveVar.prototype.numListeners = function() {
-  return _.keys(this._deps).length;
-};
-
-///// WrappedFrag /////
-
-var WrappedFrag = function(frag) {
-  if (! (this instanceof WrappedFrag))
-    return new WrappedFrag(frag);
-
-  this.frag = frag;
-};
-WrappedFrag.prototype.rawHtml = function() {
-  return Meteor.ui._fragmentToHtml(this.frag);
-};
-WrappedFrag.prototype.html = function() {
-  return canonicalizeHtml(this.rawHtml());
-};
-WrappedFrag.prototype.hold = function() {
-  return Meteor.ui._hold(this.frag), this;
-};
-WrappedFrag.prototype.release = function() {
-  return Meteor.ui._release(this.frag), this;
-};
-WrappedFrag.prototype.node = function() {
-  return this.frag;
-};
-
-
-///// TESTS /////
-
-Tinytest.add("liveui - one render", function(test) {
-
-  var R = ReactiveVar("foo");
-
-  var frag = WrappedFrag(Meteor.ui.render(function() {
-    return R.get();
-  })).hold();
-
-  test.equal(R.numListeners(), 1);
-
-  // frag should be "foo" initially
-  test.equal(frag.html(), "foo");
-  R.set("bar");
-  // haven't flushed yet, so update won't have happened
-  test.equal(frag.html(), "foo");
-  Meteor.flush();
-  // flushed now, frag should say "bar"
-  test.equal(frag.html(), "bar");
-  frag.release(); // frag is now considered offscreen
-  Meteor.flush();
-  R.set("baz");
-  Meteor.flush();
-  // no update should have happened, offscreen range dep killed
-  test.equal(frag.html(), "bar");
-
-  // should be back to no listeners
-  test.equal(R.numListeners(), 0);
-
-  // empty return value should work, and show up as a comment
-  frag = WrappedFrag(Meteor.ui.render(function() {
-    return "";
-  }));
-  test.equal(frag.html(), "");
-
-  // nodes coming and going at top level of fragment
-  R.set(true);
-  frag = WrappedFrag(Meteor.ui.render(function() {
-    return R.get() ? "
hello
world
" : ""; - })).hold(); - test.equal(frag.html(), "
hello
world
"); - R.set(false); - Meteor.flush(); - test.equal(frag.html(), ""); - R.set(true); - Meteor.flush(); - test.equal(frag.html(), "
hello
world
"); - test.equal(R.numListeners(), 1); - frag.release(); - Meteor.flush(); - test.equal(R.numListeners(), 0); - - // more complicated changes - R.set(1); - frag = WrappedFrag(Meteor.ui.render(function() { - var result = []; - for(var i=0; i

'+ - R.get()+'

'); - } - return result.join(''); - })).hold(); - test.equal(frag.html(), - '

1

'); - R.set(3); - Meteor.flush(); - test.equal(frag.html(), - '

3

'+ - '

3

'+ - '

3

'); - R.set(2); - Meteor.flush(); - test.equal(frag.html(), - '

2

'+ - '

2

'); - frag.release(); - Meteor.flush(); - test.equal(R.numListeners(), 0); - - // caller violating preconditions - test.throws(function() { - Meteor.ui.render("foo"); - }); - - test.throws(function() { - Meteor.ui.render(function() { return document.createElement("DIV"); }); - }); -}); - -Tinytest.add("liveui - onscreen", function(test) { - - var R = ReactiveVar(123); - - var div = OnscreenDiv(Meteor.ui.render(function() { - return "

The number is "+R.get()+".




underlined"; - })); - - test.equal(div.html(), "

The number is 123.




underlined"); - test.equal(R.numListeners(), 1); - Meteor.flush(); - R.set(456); // won't take effect until flush() - test.equal(div.html(), "

The number is 123.




underlined"); - test.equal(R.numListeners(), 1); - Meteor.flush(); - test.equal(div.html(), "

The number is 456.




underlined"); - test.equal(R.numListeners(), 1); - - div.remove(); - R.set(789); // update should force div dependency to be GCed when div is updated - Meteor.flush(); - test.equal(R.numListeners(), 0); -}); - -Tinytest.add("liveui - tables", function(test) { - var R = ReactiveVar(0); - - var table = OnscreenDiv(Meteor.ui.render(function() { - var buf = []; - buf.push(""); - for(var i=0; i"); - buf.push("
"+(i+1)+"
"); - return buf.join(''); - })); - - R.set(1); - Meteor.flush(); - test.equal(table.html(), "
1
"); - - R.set(10); - test.equal(table.html(), "
1
"); - Meteor.flush(); - test.equal(table.html(), ""+ - ""+ - ""+ - ""+ - ""+ - ""+ - ""+ - ""+ - ""+ - ""+ - ""+ - "
1
2
3
4
5
6
7
8
9
10
"); - - R.set(0); - Meteor.flush(); - test.equal(table.html(), "
"); - table.kill(); - Meteor.flush(); - test.equal(R.numListeners(), 0); - - var div = OnscreenDiv(); - div.node().appendChild(document.createElement("TABLE")); - div.node().firstChild.appendChild(Meteor.ui.render(function() { - var buf = []; - for(var i=0; i"+(i+1)+""); - return buf.join(''); - })); - test.equal(div.html(), "
"); - R.set(3); - Meteor.flush(); - test.equal(div.html(), ""+ - ""+ - ""+ - ""+ - "
1
2
3
"); - test.equal(div.node().firstChild.rows.length, 3); - R.set(0); - Meteor.flush(); - test.equal(div.html(), "
"); - div.kill(); - Meteor.flush(); - - test.equal(R.numListeners(), 0); - - div = OnscreenDiv(); - div.node().appendChild(Meteor.ui._htmlToFragment("
")); - R.set(3); - div.node().getElementsByTagName("tr")[0].appendChild(Meteor.ui.render( - function() { - var buf = []; - for(var i=0; i"+(i+1)+""); - return buf.join(''); - })); - test.equal(div.html(), - ""+ - "
123
"); - R.set(1); - Meteor.flush(); - test.equal(div.html(), - "
1
"); - div.kill(); - Meteor.flush(); - test.equal(R.numListeners(), 0); - - // Test tables with patching - R.set(""); - div = OnscreenDiv(Meteor.ui.render(function() { - return ''+R.get()+'
'; - })); - Meteor.flush(); - R.set("Hello"); - Meteor.flush(); - test.equal( - div.html(), - '
Hello
'); - div.kill(); - Meteor.flush(); - -}); - -Tinytest.add("liveui - preserved nodes (diff/patch)", function(test) { - - var rand; - - var randomNodeList = function(optParentTag, depth) { - var atTopLevel = ! optParentTag; - var len = rand.nextIntBetween(atTopLevel ? 1 : 0, 6); - var buf = []; - for(var i=0; i'); - nodeListToHtml(n.children, is_after, buf); - buf.push(''); - } - } - }); - return optBuf ? null : buf.join(''); - }; - - var fillInElementIdentities = function(list, parent, is_after) { - var elementsInList = _.filter( - list, - function(x) { - return (is_after ? x.existsAfter : x.existsBefore) && x.tagName; - }); - var elementsInDom = _.filter(parent.childNodes, - function(x) { return x.nodeType === 1; }); - test.equal(elementsInList.length, elementsInDom.length); - for(var i=0; i"); - fillInElementIdentities(structure, frag.node()); - var labeledNodes = collectLabeledNodeData(structure); - R.set(true); - Meteor.flush(); - test.equal(frag.html(), nodeListToHtml(structure, true) || ""); - _.each(labeledNodes, function(x) { - test.isTrue(isSameElements(x.parents, getParentChain(x.node))); - }); - - frag.release(); - Meteor.flush(); - test.equal(R.numListeners(), 0); - } - -}); - -Tinytest.add("liveui - copied attributes", function(test) { - // make sure attributes are correctly changed (i.e. copied) - // when preserving old nodes, either because they are labeled - // or because they are a parent of a labeled node. - - var R1 = ReactiveVar("foo"); - var R2 = ReactiveVar("abcd"); - var frag = WrappedFrag(Meteor.ui.render(function() { - return '
'; - })).hold(); - var node1 = frag.node().firstChild; - var node2 = frag.node().firstChild.getElementsByTagName("input")[0]; - test.equal(node1.nodeName, "DIV"); - test.equal(node2.nodeName, "INPUT"); - test.equal(node1.getAttribute("puppy"), "foo"); - test.equal(node2.getAttribute("kittycat"), "abcd"); - - R1.set("bar"); - R2.set("efgh"); - Meteor.flush(); - test.equal(node1.getAttribute("puppy"), "bar"); - test.equal(node2.getAttribute("kittycat"), "efgh"); - - frag.release(); - Meteor.flush(); - test.equal(R1.numListeners(), 0); - test.equal(R2.numListeners(), 0); - - var R; - R = ReactiveVar(false); - frag = WrappedFrag(Meteor.ui.render(function() { - return ''; - })).hold(); - var get_checked = function() { return !! frag.node().firstChild.checked; }; - test.equal(get_checked(), false); - Meteor.flush(); - test.equal(get_checked(), false); - R.set(true); - test.equal(get_checked(), false); - Meteor.flush(); - test.equal(get_checked(), true); - R.set(false); - test.equal(get_checked(), true); - Meteor.flush(); - test.equal(get_checked(), false); - R.set(true); - Meteor.flush(); - test.equal(get_checked(), true); - frag.release(); - R = ReactiveVar(true); - frag = WrappedFrag(Meteor.ui.render(function() { - return ''; - })).hold(); - test.equal(get_checked(), true); - Meteor.flush(); - test.equal(get_checked(), true); - R.set(false); - test.equal(get_checked(), true); - Meteor.flush(); - test.equal(get_checked(), false); - frag.release(); - - - _.each([false, true], function(with_focus) { - R = ReactiveVar("apple"); - var div = OnscreenDiv(Meteor.ui.render(function() { - return ''; - })); - var maybe_focus = function(div) { - if (with_focus) { - div.show(); - focusElement(div.node().firstChild); - } - }; - maybe_focus(div); - var get_value = function() { return div.node().firstChild.value; }; - var set_value = function(v) { div.node().firstChild.value = v; }; - var if_blurred = function(v, v2) { - return with_focus ? v2 : v; }; - test.equal(get_value(), "apple"); - Meteor.flush(); - test.equal(get_value(), "apple"); - R.set(""); - test.equal(get_value(), "apple"); - Meteor.flush(); - test.equal(get_value(), if_blurred("", "apple")); - R.set("pear"); - test.equal(get_value(), if_blurred("", "apple")); - Meteor.flush(); - test.equal(get_value(), if_blurred("pear", "apple")); - set_value("jerry"); // like user typing - R.set("steve"); - Meteor.flush(); - // should overwrite user typing if blurred - test.equal(get_value(), if_blurred("steve", "jerry")); - div.kill(); - R = ReactiveVar(""); - div = OnscreenDiv(Meteor.ui.render(function() { - return ''; - })); - maybe_focus(div); - test.equal(get_value(), ""); - Meteor.flush(); - test.equal(get_value(), ""); - R.set("tom"); - test.equal(get_value(), ""); - Meteor.flush(); - test.equal(get_value(), if_blurred("tom", "")); - div.kill(); - Meteor.flush(); - }); - -}); - -Tinytest.add("liveui - bad labels", function(test) { - // make sure patching behaves gracefully even when labels violate - // the rules that would allow preservation of nodes identity. - - var go = function(html1, html2) { - var R = ReactiveVar(true); - var frag = WrappedFrag(Meteor.ui.render(function() { - return R.get() ? html1 : html2; - })).hold(); - - R.set(false); - Meteor.flush(); - test.equal(frag.html(), html2); - frag.release(); - }; - - go('hello', 'world'); - - // duplicate IDs (bad developer; but should patch correctly) - go('
hello
world', - '
hi
there'); - go('
hello
', - '
hi
'); - go('
hello
world', - '
hi
'); - - // tag name changes - go('
abcd
', - '

efgh

'); - - // parent chain changes at all - go('

test123

', - '

test123

'); - go('

test123

', - '

test123

'); - - // ambiguous names - go('
  • 1
  • 3
  • 3
', - '
  • 4
  • 5
'); -}); - -Tinytest.add("liveui - chunks", function(test) { - - var inc = function(v) { - v.set(v.get() + 1); }; - - var R1 = ReactiveVar(0); - var R2 = ReactiveVar(0); - var R3 = ReactiveVar(0); - var count1 = 0, count2 = 0, count3 = 0; - - var frag = WrappedFrag(Meteor.ui.render(function() { - return R1.get() + "," + (count1++) + " " + - Meteor.ui.chunk(function() { - return R2.get() + "," + (count2++) + " " + - Meteor.ui.chunk(function() { - return R3.get() + "," + (count3++); - }); - }); - })).hold(); - - test.equal(frag.html(), "0,0 0,0 0,0"); - - inc(R1); Meteor.flush(); - test.equal(frag.html(), "1,1 0,1 0,1"); - - inc(R2); Meteor.flush(); - test.equal(frag.html(), "1,1 1,2 0,2"); - - inc(R3); Meteor.flush(); - test.equal(frag.html(), "1,1 1,2 1,3"); - - inc(R2); Meteor.flush(); - test.equal(frag.html(), "1,1 2,3 1,4"); - - inc(R1); Meteor.flush(); - test.equal(frag.html(), "2,2 2,4 1,5"); - - frag.release(); - Meteor.flush(); - test.equal(R1.numListeners(), 0); - test.equal(R2.numListeners(), 0); - test.equal(R3.numListeners(), 0); - - R1.set(0); - R2.set(0); - R3.set(0); - - frag = WrappedFrag(Meteor.ui.render(function() { - var buf = []; - buf.push('
'); - buf.push(Meteor.ui.chunk(function() { - var buf = []; - for(var i=0; i'+R3.get()+'
'; - })); - } - return buf.join(''); - })); - buf.push(''); - return buf.join(''); - })).hold(); - - test.equal(frag.html(), '
'); - R2.set(3); Meteor.flush(); - test.equal(frag.html(), '
'+ - '
0
0
0
'+ - '
'); - - R3.set(5); Meteor.flush(); - test.equal(frag.html(), '
'+ - '
5
5
5
'+ - '
'); - - R1.set(7); Meteor.flush(); - test.equal(frag.html(), '
'+ - '
5
5
5
'+ - '
'); - - R2.set(1); Meteor.flush(); - test.equal(frag.html(), '
'+ - '
5
'+ - '
'); - - R1.set(11); Meteor.flush(); - test.equal(frag.html(), '
'+ - '
5
'+ - '
'); - - R2.set(2); Meteor.flush(); - test.equal(frag.html(), '
'+ - '
5
5
'+ - '
'); - - R3.set(4); Meteor.flush(); - test.equal(frag.html(), '
'+ - '
4
4
'+ - '
'); - - frag.release(); - - // calling chunk() outside of render mode - test.equal(Meteor.ui.chunk(function() { return "foo"; }), "foo"); - - // caller violating preconditions - - test.throws(function() { - Meteor.ui.render(function() { - return Meteor.ui.chunk("foo"); - }); - }); - - test.throws(function() { - Meteor.ui.render(function() { - return Meteor.ui.chunk(function() { - return {}; - }); - }); - }); - - - // unused chunk - - var Q = ReactiveVar("foo"); - Meteor.ui.render(function() { - // create a chunk, in render mode, - // but don't use it. - Meteor.ui.chunk(function() { - return Q.get(); - }); - return ""; - }); - test.equal(Q.numListeners(), 1); - Q.set("bar"); - // flush() should invalidate the unused - // chunk but not assume it has been wired - // up with a LiveRange. - Meteor.flush(); - test.equal(Q.numListeners(), 0); - - // nesting - - var stuff = ReactiveVar(true); - var div = OnscreenDiv(Meteor.ui.render(function() { - return Meteor.ui.chunk(function() { - return "x"+(stuff.get() ? 'y' : '') + Meteor.ui.chunk(function() { - return "hi"; - }); - }); - })); - test.equal(div.html(), "xyhi"); - stuff.set(false); - Meteor.flush(); - test.equal(div.html(), "xhi"); - div.kill(); - Meteor.flush(); -}); - -Tinytest.add("liveui - repeated chunk", function(test) { - test.throws(function() { - var frag = Meteor.ui.render(function() { - var x = Meteor.ui.chunk(function() { - return "abc"; - }); - return x+x; - }); - }); -}); - -Tinytest.add("liveui - leaderboard", function(test) { - // use a simplified, local leaderboard to test some stuff - - var players = new LocalCollection(); - var selected_player = ReactiveVar(); - - var scores = OnscreenDiv(Meteor.ui.render(function() { - return Meteor.ui.listChunk( - players.find({}, {sort: {score: -1}}), - function(player) { - var style; - if (selected_player.get() === player._id) - style = "player selected"; - else - style = "player"; - - return '
' + - '
' + player.name + '
' + - '
' + player.score + '
'; - }, null, { - events: { - "click": function () { - selected_player.set(this._id); - } - } - }); - })); - - var names = ["Glinnes Hulden", "Shira Hulden", "Denzel Warhound", - "Lute Casagave", "Akadie", "Thammas, Lord Gensifer", - "Ervil Savat", "Duissane Trevanyi", "Sagmondo Bandolio", - "Rhyl Shermatz", "Yalden Wirp", "Tyran Lucho", - "Bump Candolf", "Wilmer Guff", "Carbo Gilweg"]; - for (var i = 0; i < names.length; i++) - players.insert({name: names[i], score: i*5}); - - var bump = function() { - players.update(selected_player.get(), {$inc: {score: 5}}); - }; - - var findPlayerNameDiv = function(name) { - var divs = scores.node().getElementsByTagName('DIV'); - return _.find(divs, function(div) { - return div.innerHTML === name; - }); - }; - - Meteor.flush(); - var glinnesNameNode = findPlayerNameDiv(names[0]); - test.isTrue(!! glinnesNameNode); - var glinnesScoreNode = glinnesNameNode.nextSibling; - test.equal(glinnesScoreNode.getAttribute("name"), "score"); - clickElement(glinnesNameNode); - Meteor.flush(); - glinnesNameNode = findPlayerNameDiv(names[0]); - test.isTrue(!! glinnesNameNode); - test.equal(glinnesNameNode.parentNode.className, 'player selected'); - var glinnesId = players.findOne({name: names[0]})._id; - test.isTrue(!! glinnesId); - test.equal(selected_player.get(), glinnesId); - test.equal( - canonicalizeHtml(glinnesNameNode.parentNode.innerHTML), - '
Glinnes Hulden
0
'); - - bump(); - Meteor.flush(); - - glinnesNameNode = findPlayerNameDiv(names[0], glinnesNameNode); - var glinnesScoreNode2 = glinnesNameNode.nextSibling; - test.equal(glinnesScoreNode2.getAttribute("name"), "score"); - // move and patch should leave score node the same, because it - // has a name attribute! - test.equal(glinnesScoreNode, glinnesScoreNode2); - test.equal(glinnesNameNode.parentNode.className, 'player selected'); - test.equal( - canonicalizeHtml(glinnesNameNode.parentNode.innerHTML), - '
Glinnes Hulden
5
'); - - bump(); - Meteor.flush(); - - glinnesNameNode = findPlayerNameDiv(names[0], glinnesNameNode); - test.equal( - canonicalizeHtml(glinnesNameNode.parentNode.innerHTML), - '
Glinnes Hulden
10
'); - - scores.kill(); - Meteor.flush(); - test.equal(selected_player.numListeners(), 0); -}); - -Tinytest.add("liveui - listChunk stop", function(test) { - // test listChunk outside of render mode, on custom observable - - var numHandles = 0; - var observable = { - observe: function(x) { - x.added({_id:"123"}, 0); - x.added({_id:"456"}, 1); - var handle; - numHandles++; - return handle = { - stop: function() { - numHandles--; - } - }; - } - }; - - test.equal(numHandles, 0); - var result = Meteor.ui.listChunk(observable, function(doc) { - return "#"+doc._id; - }); - test.equal(result, "#123#456"); - test.equal(numHandles, 0); // listChunk called handle.stop(); - - - var R = ReactiveVar(1); - var frag = WrappedFrag(Meteor.ui.render(function() { - if (R.get() > 0) - return Meteor.ui.listChunk(observable, function() { return "*"; }); - return ""; - })).hold(); - test.equal(numHandles, 1); - Meteor.flush(); - test.equal(numHandles, 1); - R.set(2); - Meteor.flush(); - test.equal(numHandles, 1); - R.set(-1); - Meteor.flush(); - test.equal(numHandles, 0); - - frag.release(); - Meteor.flush(); -}); - -Tinytest.add("liveui - listChunk table", function(test) { - var c = new LocalCollection(); - - c.insert({value: "fudge", order: "A"}); - c.insert({value: "sundae", order: "B"}); - - var R = ReactiveVar(); - - var table = WrappedFrag(Meteor.ui.render(function() { - var buf = []; - buf.push(''); - buf.push(Meteor.ui.listChunk( - c.find({}, {sort: ['order']}), - function(doc) { - return ""; - }, - function() { - return ""; - })); - buf.push('
"+doc.value + (doc.reactive ? R.get() : '')+ - "
(nothing)
'); - return buf.join(''); - })).hold(); - - var lastHtml; - - var shouldFlushTo = function(html) { - // same before flush - test.equal(table.html(), lastHtml); - Meteor.flush(); - test.equal(table.html(), html); - lastHtml = html; - }; - var tableOf = function(/*htmls*/) { - if (arguments.length === 0) { - return '
'; - } else { - return '
' + - _.toArray(arguments).join('
') + - '
'; - } - }; - - test.equal(table.html(), lastHtml = tableOf('fudge', 'sundae')); - - // switch order - c.update({value: "fudge"}, {$set: {order: "BA"}}); - shouldFlushTo(tableOf('sundae', 'fudge')); - - // change text - c.update({value: "fudge"}, {$set: {value: "hello"}}); - c.update({value: "sundae"}, {$set: {value: "world"}}); - shouldFlushTo(tableOf('world', 'hello')); - - // remove all - c.remove({}); - shouldFlushTo(tableOf('(nothing)')); - - c.insert({value: "1", order: "A"}); - c.insert({value: "5", order: "B"}); - c.insert({value: "3", order: "AB"}); - c.insert({value: "7", order: "BB"}); - c.insert({value: "2", order: "AA"}); - c.insert({value: "4", order: "ABA"}); - c.insert({value: "6", order: "BA"}); - c.insert({value: "8", order: "BBA"}); - shouldFlushTo(tableOf('1', '2', '3', '4', '5', '6', '7', '8')); - - // make one item newly reactive - R.set('*'); - c.update({value: "7"}, {$set: {reactive: true}}); - shouldFlushTo(tableOf('1', '2', '3', '4', '5', '6', '7*', '8')); - - R.set('!'); - shouldFlushTo(tableOf('1', '2', '3', '4', '5', '6', '7!', '8')); - - // move it - c.update({value: "7"}, {$set: {order: "A0"}}); - shouldFlushTo(tableOf('1', '7!', '2', '3', '4', '5', '6', '8')); - - // still reactive? - R.set('?'); - shouldFlushTo(tableOf('1', '7?', '2', '3', '4', '5', '6', '8')); - - // go nuts - c.update({value: '1'}, {$set: {reactive: true}}); - c.update({value: '1'}, {$set: {reactive: false}}); - c.update({value: '2'}, {$set: {reactive: true}}); - c.update({value: '2'}, {$set: {order: "BBB"}}); - R.set(';'); - R.set('.'); - shouldFlushTo(tableOf('1', '7.', '3', '4', '5', '6', '8', '2.')); - - for(var i=1; i<=8; i++) - c.update({value: String(i)}, - {$set: {reactive: true, value: '='+String(i)}}); - R.set('!'); - shouldFlushTo(tableOf('=1!', '=7!', '=3!', '=4!', '=5!', '=6!', '=8!', '=2!')); - - for(var i=1; i<=8; i++) - c.update({value: '='+String(i)}, - {$set: {order: "A"+i}}); - shouldFlushTo(tableOf('=1!', '=2!', '=3!', '=4!', '=5!', '=6!', '=7!', '=8!')); - - var valueFunc = function(n) { return ''+n+''; }; - for(var i=1; i<=8; i++) - c.update({value: '='+String(i)}, - {$set: {value: valueFunc(i)}}); - shouldFlushTo(tableOf.apply( - null, - _.map(_.range(1,9), function(n) { return valueFunc(n)+R.get(); }))); - - test.equal(table.node().firstChild.rows.length, 8); - - var bolds = table.node().firstChild.getElementsByTagName('B'); - test.equal(bolds.length, 8); - _.each(bolds, function(b) { - b.nifty = {}; // mark the nodes; non-primitive value won't appear in IE HTML - }); - - R.set('...'); - shouldFlushTo(tableOf.apply( - null, - _.map(_.range(1,9), function(n) { return valueFunc(n)+R.get(); }))); - var bolds2 = table.node().firstChild.getElementsByTagName('B'); - test.equal(bolds2.length, 8); - // make sure patching is actually happening - _.each(bolds2, function(b) { - test.equal(!! b.nifty, true); - }); - - // change value func, and still we should be patching - var valueFunc2 = function(n) { return ''+n+'yeah'; }; - for(var i=1; i<=8; i++) - c.update({value: valueFunc(i)}, - {$set: {value: valueFunc2(i)}}); - shouldFlushTo(tableOf.apply( - null, - _.map(_.range(1,9), function(n) { return valueFunc2(n)+R.get(); }))); - var bolds3 = table.node().firstChild.getElementsByTagName('B'); - test.equal(bolds3.length, 8); - _.each(bolds3, function(b) { - test.equal(!! b.nifty, true); - }); - - table.release(); - -}); - -Tinytest.add("liveui - listChunk event_data", function(test) { - // this is based on a bug - - var lastClicked = null; - var R = ReactiveVar(0); - var later; - var div = OnscreenDiv(Meteor.ui.render(function() { - return Meteor.ui.listChunk( - { observe: function(observer) { - observer.added({_id: '1', name: 'Foo'}, 0); - observer.added({_id: '2', name: 'Bar'}, 1); - // exercise callback path - later = function() { - observer.added({_id: '3', name: 'Baz'}, 2); - observer.added({_id: '4', name: 'Qux'}, 3); - }; - }}, - function(doc) { - R.get(); // depend on R - return '
' + doc.name + '
'; - }, - { events: - { - 'click': function (event) { - lastClicked = this.name; - R.set(R.get() + 1); // signal all dependers on R - } - } - }); - })); - - var item = function(name) { - return _.find(div.node().getElementsByTagName('div'), function(d) { - return d.innerHTML === name; }); - }; - - later(); - Meteor.flush(); - test.equal(item("Foo").innerHTML, "Foo"); - test.equal(item("Bar").innerHTML, "Bar"); - test.equal(item("Baz").innerHTML, "Baz"); - test.equal(item("Qux").innerHTML, "Qux"); - - var doClick = function(name) { - clickElement(item(name)); - test.equal(lastClicked, name); - Meteor.flush(); - }; - - doClick("Foo"); - doClick("Bar"); - doClick("Baz"); - doClick("Qux"); - doClick("Bar"); - doClick("Foo"); - doClick("Foo"); - doClick("Foo"); - doClick("Qux"); - doClick("Baz"); - doClick("Baz"); - doClick("Baz"); - doClick("Bar"); - doClick("Baz"); - doClick("Foo"); - doClick("Qux"); - doClick("Foo"); - - div.kill(); - Meteor.flush(); - -}); - -Tinytest.add("liveui - events on preserved nodes", function(test) { - var count = ReactiveVar(0); - var demo = OnscreenDiv(Meteor.ui.render(function() { - return '
'+ - ''+ - '
The button has been pressed '+count.get()+' times.
'+ - '
'; - }, {events: { - 'click input': function() { - count.set(count.get() + 1); - } - }})); - - var click = function() { - clickElement(demo.node().getElementsByTagName('input')[0]); - }; - - test.equal(count.get(), 0); - for(var i=0; i<10; i++) { - click(); - Meteor.flush(); - test.equal(count.get(), i+1); - } - - demo.kill(); - Meteor.flush(); -}); - -Tinytest.add("liveui - basic tag contents", function(test) { - - // adapted from nateps / metamorph - - var do_onscreen = function(f) { - var div = OnscreenDiv(); - var stuff = { - div: div, - node: _.bind(div.node, div), - render: function(rfunc) { - div.node().appendChild(Meteor.ui.render(rfunc)); - } - }; - - f.call(stuff); - - div.kill(); - }; - - var R, div; - - // basic text replace - - do_onscreen(function() { - R = ReactiveVar("one two three"); - this.render(function() { - return R.get(); - }); - R.set("three four five six"); - Meteor.flush(); - test.equal(this.div.html(), "three four five six"); - }); - - // work inside a table - - do_onscreen(function() { - R = ReactiveVar("HI!"); - this.render(function() { - return "" + R.get() + "
"; - }); - - test.equal($(this.node()).find("#morphing td").text(), "HI!"); - R.set("BUH BYE!"); - Meteor.flush(); - test.equal($(this.node()).find("#morphing td").text(), "BUH BYE!"); - }); - - // work inside a tbody - - do_onscreen(function() { - R = ReactiveVar("HI!"); - this.render(function() { - return "" + R.get() + "
"; - }); - - test.equal($(this.node()).find("#morphing td").text(), "HI!"); - R.set("BUH BYE!"); - Meteor.flush(); - test.equal($(this.node()).find("#morphing td").text(), "BUH BYE!"); - }); - - // work inside a tr - - do_onscreen(function() { - R = ReactiveVar("HI!"); - this.render(function() { - return "" + R.get() + "
"; - }); - - test.equal($(this.node()).find("#morphing td").text(), "HI!"); - R.set("BUH BYE!"); - Meteor.flush(); - test.equal($(this.node()).find("#morphing td").text(), "BUH BYE!"); - }); - - // work inside a ul - - do_onscreen(function() { - R = ReactiveVar("
  • HI!
  • "); - this.render(function() { - return "
      " + R.get() + "
    "; - }); - - test.equal($(this.node()).find("#morphing li").text(), "HI!"); - R.set("
  • BUH BYE!
  • "); - Meteor.flush(); - test.equal($(this.node()).find("#morphing li").text(), "BUH BYE!"); - }); - - // work inside a select - - do_onscreen(function() { - R = ReactiveVar(""); - this.render(function() { - return ""; - }); - - test.equal($(this.node()).find("#morphing option").text(), "HI!"); - R.set(""); - Meteor.flush(); - test.equal($(this.node()).find("#morphing option").text(), "BUH BYE!"); - }); - -}); - -var eventmap = function(/*args*/) { - // support event_buf as final argument - var event_buf = null; - if (arguments.length && _.isArray(arguments[arguments.length-1])) { - event_buf = arguments[arguments.length-1]; - arguments.length--; - } - var events = {}; - _.each(arguments, function(esel) { - var etyp = esel.split(' ')[0]; - events[esel] = function(evt) { - if (evt.type !== etyp) - throw new Error(etyp+" event arrived as "+evt.type); - (event_buf || this).push(esel); - }; - }); - return events; -}; - -Tinytest.add("liveui - event handling", function(test) { - var event_buf = []; - var getid = function(id) { - return document.getElementById(id); - }; - - var div; - - // clicking on a div at top level - event_buf.length = 0; - div = OnscreenDiv(Meteor.ui.render(function() { - return '
    Foo
    '; - }, {events: eventmap("click"), event_data:event_buf})); - clickElement(getid("foozy")); - test.equal(event_buf, ['click']); - div.kill(); - Meteor.flush(); - - // selector that specifies a top-level div - event_buf.length = 0; - div = OnscreenDiv(Meteor.ui.render(function() { - return '
    Foo
    '; - }, {events: eventmap("click div"), event_data:event_buf})); - clickElement(getid("foozy")); - test.equal(event_buf, ['click div']); - div.kill(); - Meteor.flush(); - - // selector that specifies a second-level span - event_buf.length = 0; - div = OnscreenDiv(Meteor.ui.render(function() { - return '
    Foo
    '; - }, {events: eventmap("click span"), event_data:event_buf})); - clickElement(getid("foozy").firstChild); - test.equal(event_buf, ['click span']); - div.kill(); - Meteor.flush(); - - // replaced top-level elements still have event handlers - // even if not replaced by the chunk wih the handlers - var R = ReactiveVar("p"); - event_buf.length = 0; - div = OnscreenDiv(Meteor.ui.render(function() { - return Meteor.ui.chunk(function() { - return '<'+R.get()+' id="foozy">Hello'; - }); - }, {events: eventmap("click"), event_data:event_buf})); - clickElement(getid("foozy")); - test.equal(event_buf, ['click']); - event_buf.length = 0; - R.set("div"); // change tag, which is sure to replace element - Meteor.flush(); - clickElement(getid("foozy")); // still clickable? - test.equal(event_buf, ['click']); - event_buf.length = 0; - R.set("p"); - Meteor.flush(); - clickElement(getid("foozy")); - test.equal(event_buf, ['click']); - event_buf.length = 0; - div.kill(); - Meteor.flush(); - - // bubbling from event on descendent of element matched - // by selector - event_buf.length = 0; - div = OnscreenDiv(Meteor.ui.render(function() { - return '
    Foo'+ - 'Bar
    '; - }, {events: eventmap("click span"), event_data:event_buf})); - clickElement( - getid("foozy").firstChild.firstChild.firstChild); - test.equal(event_buf, ['click span']); - div.kill(); - Meteor.flush(); - - // bubbling order (for same event, same render node, different selector nodes) - event_buf.length = 0; - div = OnscreenDiv(Meteor.ui.render(function() { - return '
    Foo'+ - 'Bar
    '; - }, {events: eventmap("click span", "click b"), event_data:event_buf})); - clickElement( - getid("foozy").firstChild.firstChild.firstChild); - test.equal(event_buf, ['click b', 'click span']); - div.kill(); - Meteor.flush(); - - // "bubbling" order for handlers at same level - event_buf.length = 0; - div = OnscreenDiv(Meteor.ui.render(function() { - return Meteor.ui.chunk(function() { - return Meteor.ui.chunk(function() { - return 'Hello'; - }, {events: eventmap("click .c"), event_data:event_buf}); - }, {events: eventmap("click .b"), event_data:event_buf}); - }, {events: eventmap("click .a"), event_data:event_buf})); - clickElement(getid("foozy")); - test.equal(event_buf, ['click .c', 'click .b', 'click .a']); - event_buf.length = 0; - div.kill(); - Meteor.flush(); - - // stopPropagation doesn't prevent other event maps from - // handling same node - event_buf.length = 0; - div = OnscreenDiv(Meteor.ui.render(function() { - return Meteor.ui.chunk(function() { - return Meteor.ui.chunk(function() { - return 'Hello'; - }, {events: eventmap("click .c"), event_data:event_buf}); - }, {events: {"click .b": function(evt) { - event_buf.push("click .b"); evt.stopPropagation();}}}); - }, {events: eventmap("click .a"), event_data:event_buf})); - clickElement(getid("foozy")); - test.equal(event_buf, ['click .c', 'click .b', 'click .a']); - event_buf.length = 0; - div.kill(); - Meteor.flush(); - - // stopImmediatePropagation DOES - event_buf.length = 0; - div = OnscreenDiv(Meteor.ui.render(function() { - return Meteor.ui.chunk(function() { - return Meteor.ui.chunk(function() { - return 'Hello'; - }, {events: eventmap("click .c"), event_data:event_buf}); - }, {events: {"click .b": function(evt) { - event_buf.push("click .b"); - evt.stopImmediatePropagation();}}}); - }, {events: eventmap("click .a"), event_data:event_buf})); - clickElement(getid("foozy")); - test.equal(event_buf, ['click .c', 'click .b']); - event_buf.length = 0; - div.kill(); - Meteor.flush(); - - // bubbling continues even with DOM change - event_buf.length = 0; - R = ReactiveVar(true); - div = OnscreenDiv(Meteor.ui.render(function() { - return Meteor.ui.chunk(function() { - return '
    '+(R.get()?'abcd':'')+'
    '; - }, {events: { 'click span': function() { - event_buf.push('click span'); - R.set(false); - Meteor.flush(); // kill the span - }, 'click div': function(evt) { - event_buf.push('click div'); - }}}); - })); - // click on span - clickElement(getid("foozy")); - test.expect_fail(); // doesn't seem to work in old IE - test.equal(event_buf, ['click span', 'click div']); - event_buf.length = 0; - div.kill(); - Meteor.flush(); - - // "deep reach" from high node down to replaced low node. - // Tests that attach_secondary_events actually does the - // right thing in IE. Also tests change event bubbling - // and proper interpretation of event maps. - event_buf.length = 0; - R = ReactiveVar('foo'); - div = OnscreenDiv(Meteor.ui.render(function() { - return '

    '+ - Meteor.ui.chunk(function() { - return ''+R.get(); - }, {events: eventmap('click input'), event_data:event_buf}) + - '

    '; - }, { events: eventmap('change b', 'change input'), event_data:event_buf })); - R.set('bar'); - Meteor.flush(); - // click on input - clickElement(div.node().getElementsByTagName('input')[0]); - event_buf.sort(); // don't care about order - test.equal(event_buf, ['change b', 'change input', 'click input']); - event_buf.length = 0; - div.kill(); - Meteor.flush(); - - // same thing, but with events wired by listChunk "added" and "removed" - event_buf.length = 0; - var lst = []; - lst.observe = function(callbacks) { - lst.callbacks = callbacks; - return { - stop: function() { - lst.callbacks = null; - } - }; - }; - div = OnscreenDiv(Meteor.ui.render(function() { - var chkbx = function(doc) { - return ''+(doc ? doc._id : 'else'); - }; - return '

    '+ - Meteor.ui.listChunk(lst, chkbx, chkbx, - {events: eventmap('click input', event_buf), - event_data:event_buf}) + - '

    '; - }, { events: eventmap('change b', 'change input', event_buf), - event_data:event_buf })); - Meteor.flush(); - test.equal(div.text().match(/\S+/)[0], 'else'); - // click on input - var doClick = function() { - clickElement(div.node().getElementsByTagName('input')[0]); - event_buf.sort(); // don't care about order - test.equal(event_buf, ['change b', 'change input', 'click input']); - event_buf.length = 0; - }; - doClick(); - // add item - lst.push({_id:'foo'}); - lst.callbacks.added(lst[0], 0); - Meteor.flush(); - test.equal(div.text().match(/\S+/)[0], 'foo'); - doClick(); - // remove item, back to "else" case - lst.callbacks.removed(lst[0], 0); - lst.pop(); - Meteor.flush(); - test.equal(div.text().match(/\S+/)[0], 'else'); - doClick(); - // cleanup - div.kill(); - Meteor.flush(); - - // test that 'click *' fires on bubble - event_buf.length = 0; - R = ReactiveVar('foo'); - div = OnscreenDiv(Meteor.ui.render(function() { - return '

    '+ - Meteor.ui.chunk(function() { - return ''+R.get(); - }, {events: eventmap('click input'), event_data:event_buf}) + - '

    '; - }, { events: eventmap('click *'), event_data:event_buf })); - R.set('bar'); - Meteor.flush(); - // click on input - clickElement(div.node().getElementsByTagName('input')[0]); - test.equal( - event_buf, - ['click input', 'click *', 'click *', 'click *', 'click *', 'click *']); - event_buf.length = 0; - div.kill(); - Meteor.flush(); - - // clicking on a div in a nested chunk (without patching) - event_buf.length = 0; - R = ReactiveVar('foo'); - div = OnscreenDiv(Meteor.ui.render(function() { - return R.get() + Meteor.ui.chunk(function() { - return 'ism'; - }, {events: eventmap("click"), event_data:event_buf}); - })); - test.equal(div.text(), 'fooism'); - clickElement(div.node().getElementsByTagName('SPAN')[0]); - test.equal(event_buf, ['click']); - event_buf.length = 0; - R.set('bar'); - Meteor.flush(); - test.equal(div.text(), 'barism'); - clickElement(div.node().getElementsByTagName('SPAN')[0]); - test.equal(event_buf, ['click']); - event_buf.length = 0; - div.kill(); - Meteor.flush(); - - // Event data comes from event.currentTarget, not event.target - var data_buf = []; - div = OnscreenDiv(Meteor.ui.render(function() { - return "
      "+Meteor.ui.chunk(function() { - return '
    • Hello
    • '; - }, { event_data: {x:'listuff'} })+"
    "; - }, { event_data: {x:'ulstuff'}, - events: { 'click ul': function() { data_buf.push(this); }}})); - clickElement(getid("funyard")); - test.equal(data_buf, [{x:'ulstuff'}]); - div.kill(); - Meteor.flush(); -}); - -Tinytest.add("liveui - cleanup", function(test) { - - // more exhaustive clean-up testing - var stuff = new LocalCollection(); - - var add_doc = function() { - stuff.insert({foo:'bar'}); }; - var clear_docs = function() { - stuff.remove({}); }; - var remove_one = function() { - stuff.remove(stuff.findOne()._id); }; - - add_doc(); // start the collection with a doc - - var R = ReactiveVar("x"); - - var div = OnscreenDiv(Meteor.ui.render(function() { - return Meteor.ui.listChunk( - stuff.find(), - function() { return R.get()+"1"; }, - function() { return R.get()+"0"; }); - })); - - test.equal(div.text(), "x1"); - Meteor.flush(); - test.equal(div.text(), "x1"); - test.equal(R.numListeners(), 1); - - clear_docs(); - Meteor.flush(); - test.equal(div.text(), "x0"); - test.equal(R.numListeners(), 1); // test clean-up of doc on remove - - add_doc(); - Meteor.flush(); - test.equal(div.text(), "x1"); - test.equal(R.numListeners(), 1); // test clean-up of "else" listeners - - add_doc(); - Meteor.flush(); - test.equal(div.text(), "x1x1"); - test.equal(R.numListeners(), 2); - - remove_one(); - Meteor.flush(); - test.equal(div.text(), "x1"); - test.equal(R.numListeners(), 1); // test clean-up of doc with other docs - - div.kill(); - Meteor.flush(); - test.equal(R.numListeners(), 0); - -}); - -var make_input_tester = function(render_func, events) { - var buf = []; - - if (typeof render_func === "string") { - var render_str = render_func; - render_func = function() { return render_str; }; - } - if (typeof events === "string") { - events = eventmap.apply(null, _.toArray(arguments).slice(1)); - } - - var R = ReactiveVar(0); - var div = OnscreenDiv( - Meteor.ui.render(function() { - R.get(); // create dependency - return render_func(); - }, { events: events, event_data: buf })); - div.show(true); - - var getbuf = function() { - var ret = buf.slice(); - buf.length = 0; - return ret; - }; - - var self; - return self = { - focus: function(optCallback) { - focusElement(self.inputNode()); - - if (optCallback) - Meteor.defer(function() { optCallback(getbuf()); }); - else - return getbuf(); - }, - blur: function(optCallback) { - blurElement(self.inputNode()); - - if (optCallback) - Meteor.defer(function() { optCallback(getbuf()); }); - else - return getbuf(); - }, - click: function() { - clickElement(self.inputNode()); - return getbuf(); - }, - kill: function() { - // clean up - div.kill(); - Meteor.flush(); - }, - inputNode: function() { - return div.node().getElementsByTagName("input")[0]; - }, - redraw: function() { - R.set(R.get() + 1); - Meteor.flush(); - } - }; -}; - - -// Note: These tests MAY FAIL if the browser window doesn't have focus -// (isn't frontmost) in some browsers, particularly Firefox. -testAsyncMulti("liveui - focus/blur events", - (function() { - - var textLevel1 = ''; - var textLevel2 = ''; - - var focus_test = function(render_func, events, expected_results) { - return function(test, expect) { - var tester = make_input_tester(render_func, events); - var callback = expect(expected_results); - tester.focus(function(buf) { - tester.kill(); - callback(buf); - }); - }; - }; - - var blur_test = function(render_func, events, expected_results) { - return function(test, expect) { - var tester = make_input_tester(render_func, events); - var callback = expect(expected_results); - tester.focus(); - tester.blur(function(buf) { - tester.kill(); - callback(buf); - }); - }; - }; - - return [ - - // focus on top-level input - focus_test(textLevel1, 'focus input', ['focus input']), - - // focus on second-level input - // issue #108 - focus_test(textLevel2, 'focus input', ['focus input']), - - // focusin - focus_test(textLevel1, 'focusin input', ['focusin input']), - focus_test(textLevel2, 'focusin input', ['focusin input']), - - // focusin bubbles - focus_test(textLevel2, 'focusin span', ['focusin span']), - - // focus doesn't bubble - focus_test(textLevel2, 'focus span', []), - - // blur works, doesn't bubble - blur_test(textLevel1, 'blur input', ['blur input']), - blur_test(textLevel2, 'blur input', ['blur input']), - blur_test(textLevel2, 'blur span', []), - - // focusout works, bubbles - blur_test(textLevel1, 'focusout input', ['focusout input']), - blur_test(textLevel2, 'focusout input', ['focusout input']), - blur_test(textLevel2, 'focusout span', ['focusout span']) - ]; - })()); - -Tinytest.add("liveui - change events", function(test) { - - var checkboxLevel1 = ''; - var checkboxLevel2 = ''+ - ''; - - - // on top-level - var checkbox1 = make_input_tester(checkboxLevel1, 'change input'); - test.equal(checkbox1.click(), ['change input']); - checkbox1.kill(); - - // on second-level (should bubble) - var checkbox2 = make_input_tester(checkboxLevel2, - 'change input', 'change span'); - test.equal(checkbox2.click(), ['change input', 'change span']); - test.equal(checkbox2.click(), ['change input', 'change span']); - checkbox2.redraw(); - test.equal(checkbox2.click(), ['change input', 'change span']); - checkbox2.kill(); - - checkbox2 = make_input_tester(checkboxLevel2, 'change input'); - test.equal(checkbox2.focus(), []); - test.equal(checkbox2.click(), ['change input']); - test.equal(checkbox2.blur(), []); - test.equal(checkbox2.click(), ['change input']); - checkbox2.kill(); - - var checkbox2 = make_input_tester( - checkboxLevel2, - 'change input', 'change span', 'change div'); - test.equal(checkbox2.click(), ['change input', 'change span']); - checkbox2.kill(); - -}); - -testAsyncMulti( - "liveui - submit events", - (function() { - var hitlist = []; - var killLater = function(thing) { - hitlist.push(thing); - }; - - var LIVEUI_TEST_RESPONDER = "/liveui_test_responder"; - var IFRAME_URL_1 = LIVEUI_TEST_RESPONDER + "/"; - var IFRAME_URL_2 = "about:blank"; // most cross-browser-compatible - if (window.opera) // opera doesn't like 'about:blank' form target - IFRAME_URL_2 = LIVEUI_TEST_RESPONDER+"/blank"; - - return [ - function(test, expect) { - - // Submit events can be canceled with preventDefault, which prevents the - // browser's native form submission behavior. This behavior takes some - // work to ensure cross-browser, so we want to test it. To detect - // a form submission, we target the form at an iframe. Iframe security - // makes this tricky. What we do is load a page from the server that - // calls us back on 'load' and 'unload'. We wait for 'load', set up the - // test, and then see if we get an 'unload' (due to the form submission - // going through) or not. - // - // This is quite a tricky implementation. - - var withIframe = function(onReady1, onReady2) { - var frameName = "submitframe"+String(Math.random()).slice(2); - var iframeDiv = OnscreenDiv( - Meteor.ui.render(function() { - return ''; - })); - var iframe = iframeDiv.node().firstChild; - - iframe.loadFunc = function() { - onReady1(frameName, iframe, iframeDiv); - onReady2(frameName, iframe, iframeDiv); - }; - iframe.unloadFunc = function() { - iframe.DID_CHANGE_PAGE = true; - }; - }; - var expectCheckLater = function(options) { - var check = expect(function(iframe, iframeDiv) { - if (options.shouldSubmit) - test.isTrue(iframe.DID_CHANGE_PAGE); - else - test.isFalse(iframe.DID_CHANGE_PAGE); - - // must do this inside expect() so it happens in time - killLater(iframeDiv); - }); - var checkLater = function(frameName, iframe, iframeDiv) { - Tinytest.setTimeout(function() { - check(iframe, iframeDiv); - }, 500); // wait for frame to unload - }; - return checkLater; - }; - var buttonFormHtml = function(frameName) { - return '
    '+ - '
    '+ - ''+ - '
    '; - }; - - // test that form submission by click fires event, - // and also actually submits - withIframe(function(frameName, iframe) { - var form = make_input_tester( - buttonFormHtml(frameName), 'submit form'); - test.equal(form.click(), ['submit form']); - killLater(form); - }, expectCheckLater({shouldSubmit:true})); - - // submit bubbles up - withIframe(function(frameName, iframe) { - var form = make_input_tester( - buttonFormHtml(frameName), 'submit form', 'submit div'); - test.equal(form.click(), ['submit form', 'submit div']); - killLater(form); - }, expectCheckLater({shouldSubmit:true})); - - // preventDefault works, still bubbles - withIframe(function(frameName, iframe) { - var form = make_input_tester( - buttonFormHtml(frameName), { - 'submit form': function(evt) { - test.equal(evt.type, 'submit'); - test.equal(evt.target.nodeName, 'FORM'); - this.push('submit form'); - evt.preventDefault(); - }, - 'submit div': function(evt) { - test.equal(evt.type, 'submit'); - test.equal(evt.target.nodeName, 'FORM'); - this.push('submit div'); - }, - 'submit a': function(evt) { - this.push('submit a'); - } - } - ); - test.equal(form.click(), ['submit form', 'submit div']); - killLater(form); - }, expectCheckLater({shouldSubmit:false})); - - }, - function(test, expect) { - _.each(hitlist, function(thing) { - thing.kill(); - }); - Meteor.flush(); - } - ]; - })()); - -Tinytest.add("liveui - controls", function(test) { - - // Radio buttons - - var R = ReactiveVar(""); - var change_buf = []; - var div = OnscreenDiv(Meteor.ui.render(function() { - var buf = []; - buf.push("Band: "); - _.each(["AM", "FM", "XM"], function(band) { - var checked = (R.get() === band) ? 'checked="checked"' : ''; - buf.push(''); - }); - buf.push(R.get()); - return buf.join(''); - }, {events: { - 'change input': function(event) { - // IE 7 is known to fire change events on all - // the radio buttons with checked=false, as if - // each button were deselected before selecting - // the new one. - // However, browsers are consistent if we are - // getting a checked=true notification. - var btn = event.target; - if (btn.checked) { - var band = btn.value; - change_buf.push(band); - R.set(band); - } - } - }})); - - Meteor.flush(); - - // get the three buttons; they should be considered 'labeled' - // by the patcher and not change identities! - var btns = _.toArray(div.node().getElementsByTagName("INPUT")); - - test.equal(_.pluck(btns, 'checked'), [false, false, false]); - test.equal(div.text(), "Band: "); - - clickElement(btns[0]); - test.equal(change_buf, ['AM']); - change_buf.length = 0; - Meteor.flush(); - test.equal(_.pluck(btns, 'checked'), [true, false, false]); - test.equal(div.text(), "Band: AM"); - - clickElement(btns[1]); - test.equal(change_buf, ['FM']); - change_buf.length = 0; - Meteor.flush(); - test.equal(_.pluck(btns, 'checked'), [false, true, false]); - test.equal(div.text(), "Band: FM"); - - clickElement(btns[2]); - test.equal(change_buf, ['XM']); - change_buf.length = 0; - Meteor.flush(); - test.equal(_.pluck(btns, 'checked'), [false, false, true]); - test.equal(div.text(), "Band: XM"); - - clickElement(btns[1]); - test.equal(change_buf, ['FM']); - change_buf.length = 0; - Meteor.flush(); - test.equal(_.pluck(btns, 'checked'), [false, true, false]); - test.equal(div.text(), "Band: FM"); - - div.kill(); - - // Textarea - - R = ReactiveVar("test"); - div = OnscreenDiv(Meteor.ui.render(function() { - return ''; - })); - div.show(true); - - var textarea = div.node().firstChild; - test.equal(textarea.nodeName, "TEXTAREA"); - test.equal(textarea.value, "This is a test"); - - // value updates reactively - R.set("fridge"); - Meteor.flush(); - test.equal(textarea.value, "This is a fridge"); - - // ...unless focused - focusElement(textarea); - R.set("frog"); - Meteor.flush(); - test.equal(textarea.value, "This is a fridge"); - - // blurring and re-setting works - blurElement(textarea); - Meteor.flush(); - test.equal(textarea.value, "This is a fridge"); - R.set("frog"); - Meteor.flush(); - test.equal(textarea.value, "This is a frog"); - - // Setting a value (similar to user typing) should - // not prevent value from being updated reactively. - textarea.value = "foobar"; - R.set("photograph"); - Meteor.flush(); - test.equal(textarea.value, "This is a photograph"); - - - div.kill(); -}); - -})(); diff --git a/packages/liveui/package.js b/packages/liveui/package.js deleted file mode 100644 index 4037d01350..0000000000 --- a/packages/liveui/package.js +++ /dev/null @@ -1,36 +0,0 @@ -Package.describe({ - summary: "Meteor's machinery for making arbitrary templates reactive", - internal: true -}); - -Package.on_use(function (api) { - api.use('livedata'); - api.use(['underscore', 'session'], 'client'); - - // XXX Depends on jquery because we need a selector engine to resolve - // event maps. What would be nice is, if you've included jquery or - // zepto, use one of those; if not, ship our own copy of sizzle (but, - // you still want the event object normalization that jquery provides?) - api.use('jquery'); - - api.add_files(['liveevents_w3c.js', 'liveevents_now3c.js'], 'client'); - api.add_files(['liveevents.js'], 'client'); - api.add_files(['liverange.js', 'liveui.js', 'innerhtml.js', 'smartpatch.js'], - 'client'); -}); - -Package.on_test(function (api) { - api.use(['tinytest', 'templating', 'htmljs']); - api.use(['liveui', 'test-helpers'], 'client'); - - api.add_files('form_responder.js', 'server'); - - api.add_files([ - 'liverange_test_helpers.js', - 'liveui_tests.js', - 'liveui_tests.html', - 'liverange_tests.js', - 'smartpatch_tests.js', - 'liveevents_tests.js' - ], 'client'); -}); diff --git a/packages/madewith/madewith.js b/packages/madewith/madewith.js index 127915bf8f..92fc13be5f 100644 --- a/packages/madewith/madewith.js +++ b/packages/madewith/madewith.js @@ -26,7 +26,7 @@ return shortname; }; - Template.madewith.events = { + Template.madewith.events({ 'click .madewith_upvote': function(event) { var app = apps.findOne(); if (app) { @@ -37,5 +37,5 @@ event.preventDefault(); } } - }; + }); })(); \ No newline at end of file diff --git a/packages/madewith/package.js b/packages/madewith/package.js index 2e5eb8e4df..b67b92bf80 100644 --- a/packages/madewith/package.js +++ b/packages/madewith/package.js @@ -3,7 +3,7 @@ Package.describe({ }); Package.on_use(function (api) { - api.use(['livedata', 'underscore', 'liveui', 'templating'], 'client'); + api.use(['livedata', 'underscore', 'spark', 'templating'], 'client'); api.add_files([ 'madewith.css', diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index 590fb515e6..5faead52da 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -372,6 +372,8 @@ LocalCollection._deepcopy = function (v) { return v; if (v === null) return null; // null has typeof "object" + if (v instanceof Date) + return new Date(v.getTime()); if (_.isArray(v)) { var ret = v.slice(0); for (var i = 0; i < v.length; i++) diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index 13aaaf471a..c5d64c9296 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -199,7 +199,7 @@ Tinytest.add("minimongo - cursors", function (test) { Tinytest.add("minimongo - misc", function (test) { // deepcopy var a = {a: [1, 2, 3], b: "x", c: true, d: {x: 12, y: [12]}, - f: null}; + f: null, g: new Date()}; var b = LocalCollection._deepcopy(a); test.isTrue(LocalCollection._f._equal(a, b)); a.a.push(4); @@ -211,7 +211,10 @@ Tinytest.add("minimongo - misc", function (test) { test.equal(b.d.z, 15); a.d.y.push(88); test.length(b.d.y, 1); - + test.equal(a.g, b.g) + b.g.setDate(b.g.getDate() + 1); + test.notEqual(a.g, b.g) + a = {x: function () {}}; b = LocalCollection._deepcopy(a); a.x.a = 14; @@ -532,6 +535,14 @@ Tinytest.add("minimongo - selector_compiler", function (test) { match({"a.b": /a/}, {a: {b: "cat"}}); nomatch({"a.b": /a/}, {a: {b: "dog"}}); + // trying to access a dotted field that is undefined at some point + // down the chain + nomatch({"a.b": 1}, {x: 2}); + nomatch({"a.b.c": 1}, {a: {x: 2}}); + nomatch({"a.b.c": 1}, {a: {b: {x: 2}}}); + nomatch({"a.b.c": 1}, {a: {b: 1}}); + nomatch({"a.b.c": 1}, {a: {b: 0}}); + // dotted keypaths: literal objects match({"a.b": {c: 1}}, {a: {b: {c: 1}}}); nomatch({"a.b": {c: 1}}, {a: {b: {c: 2}}}); diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index 90fb8b3e87..00f2c3828f 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -384,20 +384,22 @@ LocalCollection._exprForKeypathPredicate = function (keypath, value, literals) { } // now, deal with the orthogonal concern of dotted.key.paths and the - // (potentially multi-level) array searching they require + // (potentially multi-level) array searching they require. + // while at it, make sure to not throw an exception if we hit undefined while + // drilling down through the dotted parts var ret = ''; var innermost = true; while (keyparts.length) { var part = keyparts.pop(); var formal = keyparts.length ? "x" : "doc"; if (innermost) { - ret = '(function(x){return ' + predcode + ';})(' + formal + '[' + + ret = '(function(x){return ' + predcode + ';})(' + formal + '&&' + formal + '[' + JSON.stringify(part) + '])'; innermost = false; } else { // for all but the innermost level of a dotted expression, // if the runtime type is an array, search it - ret = 'f._matches(' + formal + '[' + JSON.stringify(part) + + ret = 'f._matches(' + formal + '&&' + formal + '[' + JSON.stringify(part) + '], function(x){return ' + ret + ';})'; } } diff --git a/packages/sass/sass_tests.js b/packages/sass/sass_tests.js index e5bad17782..2c5622c8fa 100644 --- a/packages/sass/sass_tests.js +++ b/packages/sass/sass_tests.js @@ -1,7 +1,7 @@ Tinytest.add("sass - presence", function(test) { - var d = OnscreenDiv(Meteor.ui.render(function() { + var d = OnscreenDiv(Meteor.render(function() { return '

    '; })); d.node().style.display = 'block'; diff --git a/packages/spark/convenience.js b/packages/spark/convenience.js new file mode 100644 index 0000000000..e4b3c3f81a --- /dev/null +++ b/packages/spark/convenience.js @@ -0,0 +1,21 @@ +Meteor.render = function (htmlFunc) { + return Spark.render(function () { + return Spark.isolate( + typeof htmlFunc === 'function' ? htmlFunc : function() { + // non-function argument becomes a constant (non-reactive) string + return String(htmlFunc); + }); + }); +}; + +Meteor.renderList = function (cursor, itemFunc, elseFunc) { + return Spark.render(function () { + return Spark.list(cursor, function (item) { + return Spark.labelBranch(item._id || null, function () { + return Spark.isolate(_.bind(itemFunc, null, item)); + }); + }, function () { + return elseFunc ? Spark.isolate(elseFunc) : ''; + }); + }); +}; diff --git a/packages/spark/package.js b/packages/spark/package.js new file mode 100644 index 0000000000..98270c414e --- /dev/null +++ b/packages/spark/package.js @@ -0,0 +1,29 @@ +Package.describe({ + summary: "Toolkit for live-updating HTML interfaces", + internal: true +}); + +Package.on_use(function (api) { + api.use(['underscore', 'uuid', 'domutils', 'liverange', 'universal-events'], + 'client'); + + // XXX Depends on jquery because we need a selector engine to resolve + // event maps. What would be nice is, if you've included jquery or + // zepto, use one of those; if not, ship our own copy of sizzle (but, + // you still want the event object normalization that jquery provides?) + api.use('jquery'); + + api.add_files(['spark.js', 'patch.js', 'convenience.js'], 'client'); +}); + +Package.on_test(function (api) { + api.use('tinytest'); + api.use(['spark', 'test-helpers'], 'client'); + + api.add_files('test_form_responder.js', 'server'); + + api.add_files([ + 'spark_tests.js', + 'patch_tests.js' + ], 'client'); +}); diff --git a/packages/liveui/smartpatch.js b/packages/spark/patch.js similarity index 74% rename from packages/liveui/smartpatch.js rename to packages/spark/patch.js index df54b36cba..1582730c34 100644 --- a/packages/liveui/smartpatch.js +++ b/packages/spark/patch.js @@ -1,4 +1,88 @@ -Meteor.ui = Meteor.ui || {}; + +Spark._patch = function(tgtParent, srcParent, tgtBefore, tgtAfter, preservations, + results) { + + var copyFunc = function(t, s) { + LiveRange.transplantTag(Spark._TAG, t, s); + }; + + var patcher = new Spark._Patcher( + tgtParent, srcParent, tgtBefore, tgtAfter); + + + var visitNodes = function(parent, before, after, func) { + for(var n = before ? before.nextSibling : parent.firstChild; + n && n !== after; + n = n.nextSibling) { + if (func(n) !== false && n.firstChild) + visitNodes(n, null, null, func); + } + }; + + // results arg is optional; it is mutated if provided; returned either way + results = (results || {}); + // array of LiveRanges that were successfully preserved from + // the region preservations + var regionPreservations = (results.regionPreservations = + results.regionPreservations || []); + + var lastTgtMatch = null; + + visitNodes(srcParent, null, null, function(src) { + // XXX inefficient to scan for match for every node! + // We could at least skip non-element nodes, except for "range matches" + // used for constant chunks, which may begin on a non-element. + // But really this shouldn't be a linear search. + var pres = _.find(preservations, function (p) { + // find preserved region starting at `src`, if any + return p.type === 'region' && p.newRange.firstNode() === src; + }) || _.find(preservations, function (p) { + // else, find preservation of `src` + return p.type === 'node' && p.to === src; + }); + + if (pres) { + var tgt = (pres.type === 'region' ? pres.fromStart : pres.from); + if (! lastTgtMatch || + DomUtils.elementOrder(lastTgtMatch, tgt) > 0) { + if (pres.type === 'region') { + // preserved region for constant landmark + if (patcher.match(pres.fromStart, pres.newRange.firstNode(), + copyFunc, true)) { + patcher.skipToSiblings(pres.fromEnd, pres.newRange.lastNode()); + // without knowing or caring what DOM nodes are in pres.newRange, + // transplant the range data to pres.fromStart and pres.fromEnd + // (including references to enclosing ranges). + LiveRange.transplantRange( + pres.fromStart, pres.fromEnd, pres.newRange); + regionPreservations.push(pres.newRange); + } + } else if (pres.type === 'node') { + if (patcher.match(tgt, src, copyFunc)) { + // match succeeded + lastTgtMatch = tgt; + if (tgt.firstChild || src.firstChild) { + // Don't patch contents of TEXTAREA tag, + // which are only the initial contents but + // may affect the tag's .value in IE. + if (tgt.nodeName !== "TEXTAREA") { + // recurse! + Spark._patch(tgt, src, null, null, preservations); + } + } + return false; // tell visitNodes not to recurse + } + } + } + } + return true; + }); + + patcher.finish(); + + return results; +}; + // A Patcher manages the controlled replacement of a region of the DOM. // The target region is changed in place to match the source region. @@ -10,20 +94,18 @@ Meteor.ui = Meteor.ui || {}; // of srcParent, which may be a DocumentFragment. // // To use a new Patcher, call `match` zero or more times followed by -// `finish`. Alternatively, just call `diffpatch` to use the standard -// matching strategy. +// `finish`. // // A match is a correspondence between an old node in the target region // and a new node in the source region that will replace it. Based on // this correspondence, the target node is preserved and the attributes // and children of the source node are copied over it. The `match` -// method declares such a correspondence. The `diffpatch` method -// determines the correspondences and calls `match` and `finish`. -// A Patcher that makes no matches just removes the target nodes -// and inserts the source nodes in their place. +// method declares such a correspondence. A Patcher that makes no matches, +// for example, just removes the target nodes and inserts the source nodes +// in their place. // // Constructor: -Meteor.ui._Patcher = function(tgtParent, srcParent, tgtBefore, tgtAfter) { +Spark._Patcher = function(tgtParent, srcParent, tgtBefore, tgtAfter) { this.tgtParent = tgtParent; this.srcParent = srcParent; @@ -34,94 +116,6 @@ Meteor.ui._Patcher = function(tgtParent, srcParent, tgtBefore, tgtAfter) { this.lastKeptSrcNode = null; }; -// Perform a complete patching where nodes with the same `id` or `name` -// are matched. -// -// Any node that has either an "id" or "name" attribute is considered a -// "labeled" node, and these labeled nodes are candidates for preservation. -// For two labeled nodes, old and new, to match, they first must have the same -// label (that is, they have the same id, or neither has an id and they have -// the same name). Labels are considered relative to the nearest enclosing -// labeled ancestor, and must be unique among the labeled nodes that share -// this nearest labeled ancestor. Labeled nodes are also expected to stay -// in the same order, or else some of them won't be matched. - -Meteor.ui._Patcher.prototype.diffpatch = function(copyCallback) { - var self = this; - - var each_labeled_node = function(parent, before, after, func) { - for(var n = before ? before.nextSibling : parent.firstChild; - n && n !== after; - n = n.nextSibling) { - - var label = null; - - if (n.nodeType === 1) { - if (n.id) { - label = '#'+n.id; - } else if (n.getAttribute("name")) { - label = n.getAttribute("name"); - // Radio button special case: radio buttons - // in a group all have the same name. Their value - // determines their identity. - // Checkboxes with the same name and different - // values are also sometimes used in apps, so - // we treat them similarly. - if (n.nodeName === 'INPUT' && - (n.type === 'radio' || n.type === 'checkbox') && - n.value) - label = label + ':' + n.value; - } - } - - if (label) - func(label, n); - else - // not a labeled node; recurse - each_labeled_node(n, null, null, func); - } - }; - - - var targetNodes = {}; - var targetNodeOrder = {}; - var targetNodeCounter = 0; - - each_labeled_node( - self.tgtParent, self.tgtBefore, self.tgtAfter, - function(label, node) { - targetNodes[label] = node; - targetNodeOrder[label] = targetNodeCounter++; - }); - - var lastPos = -1; - each_labeled_node( - self.srcParent, null, null, - function(label, node) { - var tgt = targetNodes[label]; - var src = node; - if (tgt && targetNodeOrder[label] > lastPos) { - if (self.match(tgt, src, copyCallback)) { - // match succeeded - if (tgt.firstChild || src.firstChild) { - // Don't patch contents of TEXTAREA tag, - // which are only the initial contents but - // may affect the tag's .value in IE. - if (tgt.nodeName !== "TEXTAREA") { - // recurse with a new Patcher! - var patcher = new Meteor.ui._Patcher(tgt, src); - patcher.diffpatch(copyCallback); - } - } - } - lastPos = targetNodeOrder[label]; - } - }); - - self.finish(); - -}; - // Advances the patching process up to tgtNode in the target tree, // and srcNode in the source tree. tgtNode will be preserved, with @@ -167,7 +161,8 @@ Meteor.ui._Patcher.prototype.diffpatch = function(copyCallback) { // copyCallback is called on every new matched (tgt, src) pair // right after copying attributes. It's a good time to transplant // liveranges and patch children. -Meteor.ui._Patcher.prototype.match = function(tgtNode, srcNode, copyCallback) { +Spark._Patcher.prototype.match = function( + tgtNode, srcNode, copyCallback, onlyAdvance) { // last nodes "kept" (matched/identified with each other) var lastKeptTgt = this.lastKeptTgtNode; @@ -182,7 +177,6 @@ Meteor.ui._Patcher.prototype.match = function(tgtNode, srcNode, copyCallback) { var starting = ! lastKeptTgt; var finishing = ! tgt; - var elementContains = Meteor.ui._Patcher._elementContains; if (! starting) { // move lastKeptTgt/lastKeptSrc forward and out, @@ -190,7 +184,7 @@ Meteor.ui._Patcher.prototype.match = function(tgtNode, srcNode, copyCallback) { // replacing as we go. If tgt/src is falsy, we make it to the // top level. while (lastKeptTgt.parentNode !== this.tgtParent && - ! (tgt && elementContains(lastKeptTgt.parentNode, tgt))) { + ! (tgt && DomUtils.elementContains(lastKeptTgt.parentNode, tgt))) { // Last-kept nodes are inside parents that are not // parents of the newly matched nodes. Must finish // replacing their contents and back out. @@ -216,9 +210,9 @@ Meteor.ui._Patcher.prototype.match = function(tgtNode, srcNode, copyCallback) { // ancestor of on the left rather than a sibling of an // ancestor. if (! finishing && - (elementContains(lastKeptSrc, src) || + (DomUtils.elementContains(lastKeptSrc, src) || ! (lastKeptSrc.parentNode === this.srcParent || - elementContains(lastKeptSrc.parentNode, src)))) { + DomUtils.elementContains(lastKeptSrc.parentNode, src)))) { return false; } } @@ -227,28 +221,36 @@ Meteor.ui._Patcher.prototype.match = function(tgtNode, srcNode, copyCallback) { this._replaceNodes(lastKeptTgt, null, lastKeptSrc, null, this.tgtParent, this.srcParent); } else { - // Compare tag names and depths to make sure we can match nodes. + // Compare tag names and depths to make sure we can match nodes... + if (! onlyAdvance) { + if (tgt.nodeName !== src.nodeName) + return false; + } + // Look at tags of parents until we hit parent of last-kept, // which we know is ok. - for(var a=tgt, b=src; + for(var a=tgt.parentNode, b=src.parentNode; a !== (starting ? this.tgtParent : lastKeptTgt.parentNode); a = a.parentNode, b = b.parentNode) { - if (b === (starting ? this.srcParent : lastKeptSrc.parentNode)) { + if (b === (starting ? this.srcParent : lastKeptSrc.parentNode)) return false; // src is shallower, b hit top first - } - if (a.nodeName !== b.nodeName) { + if (a.nodeName !== b.nodeName) return false; // tag names don't match - } } if (b !== (starting ? this.srcParent : lastKeptSrc.parentNode)) { return false; // src is deeper, b didn't hit top when a did } + var firstIter = true; // move tgt and src backwards and out, replacing as we go while (true) { - Meteor.ui._Patcher._copyAttributes(tgt, src); - if (copyCallback) - copyCallback(tgt, src); + if (! (firstIter && onlyAdvance)) { + Spark._Patcher._copyAttributes(tgt, src); + if (copyCallback) + copyCallback(tgt, src); + } + + firstIter = false; if ((starting ? this.tgtParent : lastKeptTgt.parentNode) === tgt.parentNode) { @@ -270,11 +272,29 @@ Meteor.ui._Patcher.prototype.match = function(tgtNode, srcNode, copyCallback) { return true; }; +// After a match, skip ahead to later siblings of the last kept nodes, +// without performing any replacements. +Spark._Patcher.prototype.skipToSiblings = function(tgt, src) { + var lastTgt = this.lastKeptTgtNode; + var lastSrc = this.lastKeptSrcNode; + + if (! (lastTgt && lastTgt.parentNode === tgt.parentNode)) + return false; + + if (! (lastSrc && lastSrc.parentNode === src.parentNode)) + return false; + + this.lastKeptTgtNode = tgt; + this.lastKeptSrcNode = src; + + return true; +}; + // Completes patching assuming no more matches. // // Patchers are single-use, so no more methods can be called // on the Patcher. -Meteor.ui._Patcher.prototype.finish = function() { +Spark._Patcher.prototype.finish = function() { return this.match(null, null); }; @@ -284,7 +304,7 @@ Meteor.ui._Patcher.prototype.finish = function() { // // Precondition: tgtBefore and tgtAfter have same parent; either may be falsy, // but not both, unless optTgtParent is provided. Same with srcBefore/srcAfter. -Meteor.ui._Patcher.prototype._replaceNodes = function( +Spark._Patcher.prototype._replaceNodes = function( tgtBefore, tgtAfter, srcBefore, srcAfter, optTgtParent, optSrcParent) { var tgtParent = optTgtParent || (tgtBefore || tgtAfter).parentNode; @@ -326,7 +346,7 @@ Meteor.ui._Patcher.prototype._replaceNodes = function( // // This is complicated by form controls and the fact that old IE // can't keep the difference straight between properties and attributes. -Meteor.ui._Patcher._copyAttributes = function(tgt, src) { +Spark._Patcher._copyAttributes = function(tgt, src) { var srcAttrs = src.attributes; var tgtAttrs = tgt.attributes; @@ -350,9 +370,12 @@ Meteor.ui._Patcher._copyAttributes = function(tgt, src) { // Record for later whether this is a radio button. isRadio = (tgt.type === 'radio'); // Clearing the attributes of a checkbox won't necessarily - // uncheck it, eg in FF12, so we uncheck explicitly. - if (typeof tgt.checked === "boolean") + // uncheck it, eg in FF12, so we uncheck explicitly + // (if necessary; we don't want to generate spurious + // propertychange events in old IE). + if (tgt.checked === true && src.checked === false) { tgt.checked = false; + } } for(var i=tgtAttrs.length-1; i>=0; i--) { @@ -422,8 +445,7 @@ Meteor.ui._Patcher._copyAttributes = function(tgt, src) { tgt.mergeAttributes(src); - if (typeof tgt.checked !== "undefined" || - typeof src.checked !== "undefined") + if (typeof tgt.checked !== "undefined" && src.checked) tgt.checked = src.checked; if (src.name) @@ -467,17 +489,3 @@ Meteor.ui._Patcher._copyAttributes = function(tgt, src) { } }; - -// returns true if element a properly contains element b -Meteor.ui._Patcher._elementContains = function(a, b) { - if (a.nodeType !== 1 || b.nodeType !== 1) { - return false; - } - if (a.compareDocumentPosition) { - return a.compareDocumentPosition(b) & 0x10; - } else { - // Should be only old IE and maybe other old browsers here. - // Modern Safari has both methods but seems to get contains() wrong. - return a !== b && a.contains(b); - } -}; diff --git a/packages/liveui/smartpatch_tests.js b/packages/spark/patch_tests.js similarity index 95% rename from packages/liveui/smartpatch_tests.js rename to packages/spark/patch_tests.js index e10faccfe8..842b8c8e80 100644 --- a/packages/liveui/smartpatch_tests.js +++ b/packages/spark/patch_tests.js @@ -1,6 +1,6 @@ -Tinytest.add("smartpatch - basic", function(test) { +Tinytest.add("spark - patch - basic", function(test) { - var Patcher = Meteor.ui._Patcher; + var Patcher = Spark._Patcher; var div = function(html) { var n = document.createElement("DIV"); @@ -91,7 +91,6 @@ Tinytest.add("smartpatch - basic", function(test) { test.isTrue(ret); assert_html(x, "barbaz"); - var LiveRange = Meteor.ui._LiveRange; var t = "_foo"; var liverange = function(start, end, inner) { return new LiveRange(t, start, end, inner); @@ -107,7 +106,7 @@ Tinytest.add("smartpatch - basic", function(test) { tgt.firstNode().previousSibling, tgt.lastNode().nextSibling); var copyCallback = function(tgt, src) { - LiveRange.transplant_tag(t, tgt, src); + LiveRange.transplantTag(t, tgt, src); }; ret = p.match(tag(x, 'u'), tag(y, 'u'), copyCallback); test.isTrue(ret); @@ -121,7 +120,7 @@ Tinytest.add("smartpatch - basic", function(test) { _.each([["aaa","zzz"], ["",""], ["aaa",""], ["","zzz"]], rangeTest); }); -Tinytest.add("smartpatch - copyAttributes", function(test) { +Tinytest.add("spark - patch - copyAttributes", function(test) { var attrTester = function(tagName, initial) { var node; @@ -137,12 +136,12 @@ Tinytest.add("smartpatch - copyAttributes", function(test) { }); buf.push('>'); var nodeHtml = buf.join(''); - var frag = Meteor.ui._htmlToFragment(nodeHtml); + var frag = DomUtils.htmlToFragment(nodeHtml); var n = frag.firstChild; if (! node) { node = n; } else { - Meteor.ui._Patcher._copyAttributes(node, n); + Spark._Patcher._copyAttributes(node, n); } lastAttrs = {}; _.each(allAttrNames, function(v,k) { @@ -239,4 +238,3 @@ Tinytest.add("smartpatch - copyAttributes", function(test) { }); - diff --git a/packages/spark/spark.js b/packages/spark/spark.js new file mode 100644 index 0000000000..5311d3192c --- /dev/null +++ b/packages/spark/spark.js @@ -0,0 +1,1122 @@ +// XXX adjust Spark API so that the modules (eg, list, events) could +// have been written by third parties on top of the public API? + +// XXX rename isolate to reflect that it is the only root of +// deps-based reactivity ('track'? 'compute'? 'sync'?) + +// XXX specify flush order someday (context dependencies? is this in +// the domain of spark -- overdraw concerns?) + +// XXX if not on IE6-8, don't do the extra work (traversals for event +// setup) those browsers require + +// XXX flag errors if you have two landmarks with the same branch +// path, or if you have multiple preserve nodes in a landmark with the +// same selector and label + +// XXX should functions with an htmlFunc use try/finally inside? + +// XXX test that non-Spark.render case works for each function (eg, +// list() returns the expected HTML, Spark.createLandmark creates and +// then destroys a landmark -- may already be tested?) + +// XXX in landmark-demo, if Template.timer.created throws an exception, +// then it is never called again, even if you push the 'create a +// timer' button again. the problem is almost certainly in atFlushTime +// (not hard to see what it is.) + +(function() { + +Spark = {}; + +Spark._currentRenderer = new Meteor.EnvironmentVariable; + +Spark._TAG = "_spark_" + Meteor.uuid(); +// XXX document contract for each type of annotation? +Spark._ANNOTATION_NOTIFY = "notify"; +Spark._ANNOTATION_DATA = "data"; +Spark._ANNOTATION_ISOLATE = "isolate"; +Spark._ANNOTATION_EVENTS = "events"; +Spark._ANNOTATION_WATCH = "watch"; +Spark._ANNOTATION_LABEL = "label"; +Spark._ANNOTATION_LANDMARK = "landmark"; +Spark._ANNOTATION_LIST = "list"; +Spark._ANNOTATION_LIST_ITEM = "item"; +// XXX why do we need, eg, _ANNOTATION_ISOLATE? it has no semantics? + +// Set in tests to turn on extra UniversalEventListener sanity checks +Spark._checkIECompliance = false; + +var makeRange = function (type, start, end, inner) { + var range = new LiveRange(Spark._TAG, start, end, inner); + range.type = type; + return range; +}; + +var findRangeOfType = function (type, node) { + var range = LiveRange.findRange(Spark._TAG, node); + while (range && range.type !== type) + range = range.findParent(); + + return range; +}; + +var findParentOfType = function (type, range) { + do { + range = range.findParent(); + } while (range && range.type !== type); + + return range; +}; + +var notifyWatchers = function (start, end) { + var tempRange = new LiveRange(Spark._TAG, start, end, true /* innermost */); + for (var walk = tempRange; walk; walk = walk.findParent()) + if (walk.type === Spark._ANNOTATION_WATCH) + walk.notify(); + tempRange.destroy(); +}; + +Spark._createId = function () { + var chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + var id = ""; + for (var i = 0; i < 8; i++) + id += chars.substr(Math.floor(Meteor.random() * 64), 1); + return id; +}; + +Spark._Renderer = function () { + // Map from annotation ID to an annotation function, which is called + // at render time and receives (startNode, endNode). + this.annotations = {}; + + // Map from branch path to "notes" object, organized as a tree. + // Each node in the tree has child pointers named ('_'+label). + // Properties that don't start with '_' are arbitrary notes. + // For example, the "happiness" of the branch path consisting + // of labels "foo" and then "bar" would be + // `this._branchNotes._foo._bar.happiness`. + // Access to these notes is provided by LabelStack objects, of + // which `this.currentBranch` is one. + this._branchNotes = {}; + + // The label stack representing the current branch path we + // are in (based on calls to `Spark.labelBranch(label, htmlFunc)`). + this.currentBranch = this.newLabelStack(); + + // All landmark ranges created during this rendering. + this.landmarkRanges = []; + + // Assembles the preservation information for patching. + this.pc = new PreservationController; +}; + +_.extend(Spark._Renderer.prototype, { + // `what` can be a function that takes a LiveRange, or just a set of + // attributes to add to the liverange. tag and what are optional. + // if no tag is passed, no liverange will be created. + annotate: function (html, type, what, unusedFunc) { + // The annotation tags that we insert into HTML strings must be + // unguessable in order to not create potential cross-site scripting + // attack vectors, so we use random strings. Even a well-written app + // that avoids XSS vulnerabilities might, for example, put + // unescaped < and > in HTML attribute values, where they are normally + // safe. We can't assume that a string like '<1>' came from us + // and not arbitrary user-entered data. + var id = (type || '') + ":" + Spark._createId(); + this.annotations[id] = function (start, end) { + if (! start) { + // materialize called us with no args because this annotation + // wasn't used + unusedFunc && unusedFunc(); + return; + } + if (! type) + // no type given; don't generate a LiveRange + return; + var range = makeRange(type, start, end); + if (what instanceof Function) + what(range); + else + _.extend(range, what); + }; + + return "<$" + id + ">" + html + ""; + }, + + // A LabelStack is a mutable branch path that you can modify + // by pushing or popping labels. At any time, you can ask for + // this Renderer's notes for the current branch path. + // Renderer's `currentBranch` field is a LabelStack, but you + // can create your own for the purpose of walking the branches + // and accessing notes. + newLabelStack: function () { + var stack = [this._branchNotes]; + return { + pushLabel: function (label) { + var top = stack[stack.length - 1]; + var key = '_' + label; + stack.push(top[key] = (top[key] || {})); + }, + popLabel: function () { + stack.pop(); + }, + getNotes: function () { + var top = stack[stack.length - 1]; + return top; + }, + // Mark this branch with `getNotes()[prop] = true` and also + // walk up the stack marking parent branches (until an + // existing truthy value for `prop` is found). + // This makes it easy to test whether any descendent of a + // branch has the mark. + mark: function (prop) { + for (var i = stack.length - 1; + i >= 0 && ! stack[i][prop]; + i--) + stack[i][prop] = true; + } + }; + }, + + // Turn the `html` string into a fragment, applying the annotations + // from 'renderer' in the process. + materialize: function (htmlFunc) { + var self = this; + + var html = Spark._currentRenderer.withValue(self, htmlFunc); + html = self.annotate(html); // wrap with an anonymous annotation + + var fragById = {}; + var replaceInclusions = function (container) { + var n = container.firstChild; + while (n) { + var next = n.nextSibling; + if (n.nodeType === 8) { // COMMENT + var frag = fragById[n.nodeValue]; + if (frag === false) { + // id already used! + throw new Error("Spark HTML fragments may only be used once. " + + "Second use in " + + DomUtils.fragmentToHtml(container)); + } else if (frag) { + fragById[n.nodeValue] = false; // mark as used + DomUtils.wrapFragmentForContainer(frag, n.parentNode); + n.parentNode.replaceChild(frag, n); + } + } else if (n.nodeType === 1) { // ELEMENT + replaceInclusions(n); + } + n = next; + } + }; + + var bufferStack = [[]]; + var idStack = []; + var ret; + + var regex = /<(\/?)\$([^<>]+)>|<|[^<]+/g; + regex.lastIndex = 0; + var parts; + while ((parts = regex.exec(html))) { + var isOpen = ! parts[1]; + var id = parts[2]; + var annotationFunc = self.annotations[id]; + if (annotationFunc === false) { + throw new Error("Spark HTML fragments may be used only once. " + + "Second use of: " + + DomUtils.fragmentToHtml(fragById[id])); + } else if (! annotationFunc) { + bufferStack[bufferStack.length - 1].push(parts[0]); + } else if (isOpen) { + idStack.push(id); + bufferStack.push([]); + } else { + var idOnStack = idStack.pop(); + if (idOnStack !== id) + throw new Error("Range mismatch: " + idOnStack + " / " + id); + var frag = DomUtils.htmlToFragment(bufferStack.pop().join('')); + replaceInclusions(frag); + // empty frag becomes HTML comment so we have start/end + // nodes to pass to the annotation function + if (! frag.firstChild) + frag.appendChild(document.createComment("empty")); + annotationFunc(frag.firstChild, frag.lastChild); + self.annotations[id] = false; // mark as used + if (! idStack.length) { + // we're done; we just rendered the contents of the top-level + // annotation that we wrapped around htmlFunc ourselves. + // there may be unused fragments in fragById that include + // LiveRanges, but only if the user broke the rules by including + // an annotation somewhere besides element level, like inside + // an attribute (which is not allowed). + ret = frag; + break; + } + fragById[id] = frag; + bufferStack[bufferStack.length - 1].push(''); + } + } + + scheduleOnscreenSetup(ret, self.landmarkRanges); + self.landmarkRanges = []; + + _.each(self.annotations, function(annotationFunc) { + if (annotationFunc) + // call annotation func with no arguments to mean "you weren't used" + annotationFunc(); + }); + self.annotations = {}; + + return ret; + } + +}); + +// Decorator for Spark annotations that take `html` and are +// pass-through without a renderer. With this decorator, +// the annotation routine gets the current renderer, and +// if there isn't one returns `html` (the last argument). +var withRenderer = function (f) { + return function (/* arguments */) { + var renderer = Spark._currentRenderer.get(); + var args = _.toArray(arguments); + if (!renderer) + return args.pop(); + args.push(renderer); + return f.apply(null, args); + }; +}; + +/******************************************************************************/ +/* Render and finalize */ +/******************************************************************************/ + +// Schedule setup tasks to run at the next flush, which is when the +// newly rendered fragment must be on the screen (if it doesn't want +// to get garbage-collected.) +// +// 'landmarkRanges' is a list of the landmark ranges in 'frag'. It may be +// omitted if frag doesn't contain any landmarks. +// +// XXX expose in the public API, eg as Spark.introduce(), so the user +// can call it when manually inserting nodes? (via, eg, jQuery?) -- of +// course in that case 'landmarkRanges' would be empty. +var scheduleOnscreenSetup = function (frag, landmarkRanges) { + var renderedRange = new LiveRange(Spark._TAG, frag); + var finalized = false; + renderedRange.finalize = function () { + finalized = true; + }; + + var ctx = new Meteor.deps.Context; + ctx.on_invalidate(function () { + if (finalized) + return; + + if (!DomUtils.isInDocument(renderedRange.firstNode())) { + // We've detected that some nodes were taken off the screen + // without calling Spark.finalize(). This could be because the + // user rendered them, but didn't insert them in the document + // before the next flush(). Or it could be because they used to + // be onscreen, but they were manually taken offscreen (eg, with + // jQuery) and the user neglected to call finalize() on the + // removed nodes. Help the user out by finalizing the entire + // subtree that is offscreen. + var node = renderedRange.firstNode(); + while (node.parentNode) + node = node.parentNode; + if (node["_protect"]) { + // test code can use this property to mark a root-level node + // (such as a DocumentFragment) as immune from + // autofinalization. effectively, the DocumentFragment is + // considered to be a first-class peer of `document`. + } else { + Spark.finalize(node); + return; + } + } + + // Deliver render callbacks to all landmarks that are now + // onscreen (possibly not for the first time.) + _.each(landmarkRanges, function (landmarkRange) { + if (! landmarkRange.isPreservedConstant) + landmarkRange.rendered.call(landmarkRange.landmark); + }); + + // Deliver render callbacks to all landmarks that enclose the + // updated region. + // + // XXX unify with notifyWatchers. maybe remove _ANNOTATION_WATCH + // and just give everyone a contentsModified callback (sibling to + // 'finalize') + // + // future: include an argument in the callback to distinguish this + // case from the previous + var walk = renderedRange; + while ((walk = findParentOfType(Spark._ANNOTATION_LANDMARK, walk))) + walk.rendered.call(walk.landmark); + + // This code can run several times on the same nodes (if the + // output of a render is included in a render), so it must be + // idempotent. This is not the best, asymptotically. There are + // things we could do to improve it. + notifyWatchers(renderedRange.firstNode(), renderedRange.lastNode()); + renderedRange.destroy(); + }); + + ctx.invalidate(); +}; + +Spark.render = function (htmlFunc) { + var renderer = new Spark._Renderer; + var frag = renderer.materialize(htmlFunc); + return frag; +}; + + +// Find all of all nodes and regions that should be preserved in +// patching. Return a list of objects. There are two kinds of objects +// in the list: +// +// A preserved node: +// {type: "node", from: Node, to: Node} +// +// A preserved (constant) region: +// {type: "region", fromStart: Node, fromEnd: Node, +// newRange: LiveRange} +// +// `existingRange` is the range in the document whose contents are to +// be replaced. `newRange` holds the new contents and is not part of +// the document DOM tree. The implementation will temporarily reparent +// the nodes in `newRange` into the document to check for selector matches. +var PreservationController = function () { + this.roots = []; // keys 'landmarkRange', 'fromRange', 'toRange' + this.regionPreservations = []; +}; + +_.extend(PreservationController.prototype, { + // Specify preservations that should be in effect on a fromRange/toRange + // pair. If specified, `optContextNode` should be an ancestor node of + // fromRange that selectors are to be considered relative to. + addRoot: function (preserve, fromRange, toRange, optContextNode) { + var self = this; + self.roots.push({ context: optContextNode, preserve: preserve, + fromRange: fromRange, toRange: toRange}); + }, + + addConstantRegion: function (from, to) { + var self = this; + self.regionPreservations.push({ + type: "region", + fromStart: from.firstNode(), fromEnd: from.lastNode(), + newRange: to + }); + }, + + computePreservations: function (existingRange, newRange) { + var self = this; + var preservations = _.clone(self.regionPreservations); + + var visitLabeledNodes = function (context, clipRange, nodeLabeler, selector, func) { + context = (context || clipRange.containerNode()); + var nodes = DomUtils.findAllClipped( + context, selector, clipRange.firstNode(), clipRange.lastNode()); + + _.each(nodes, function (n) { + var label = nodeLabeler(n); + label && func(n, label); + }); + }; + + // Find the old incarnation of each of the preserved nodes + _.each(self.roots, function (root) { + root.fromNodesByLabel = {}; + _.each(root.preserve, function (nodeLabeler, selector) { + root.fromNodesByLabel[selector] = {}; + visitLabeledNodes( + root.context, root.fromRange, nodeLabeler, selector, + function (n, label) { + root.fromNodesByLabel[selector][label] = n; + }); + }); + }); + + // Temporarily put newRange into the document so that we can do + // properly contextualized selector queries against it. + // + // Create a temporary range around newRange, and also around any enclosing + // ranges that happen to also start and end on those nodes. It is ok + // to temporarily put these in the document as well, because CSS selectors + // don't care and we will put them back. `tempRange` will hold our place + // in the tree `newRange` came from. + var tempRange = new LiveRange(Spark._TAG, newRange.firstNode(), newRange.lastNode()); + var commentFrag = document.createDocumentFragment(); + commentFrag.appendChild(document.createComment("")); + var newRangeFrag = tempRange.replaceContents(commentFrag); + // `wrapperRange` will mark where we inserted newRange into the document. + var wrapperRange = new LiveRange(Spark._TAG, newRangeFrag); + existingRange.insertBefore(newRangeFrag); + + _.each(self.roots, function (root) { + _.each(root.preserve, function (nodeLabeler, selector) { + visitLabeledNodes(root.context, root.toRange, nodeLabeler, selector, function (n, label) { + var match = root.fromNodesByLabel[selector][label]; + if (match) { + preservations.push({ type: "node", from: match, to: n }); + root.fromNodesByLabel[selector][label] = null; + } + }); + }); + }); + + // Extraction is legal because we're just taking the document + // back to the state it was in before insertBefore. + var extractedFrag = wrapperRange.extract(); + wrapperRange.destroy(); + tempRange.replaceContents(extractedFrag); + tempRange.destroy(); + + return preservations; + } +}); + + +// XXX debugging +var pathForRange = function (r) { + var path = [], r; + while ((r = findParentOfType(Spark._ANNOTATION_LABEL, r))) + path.unshift(r.label); + return path.join(' :: '); +}; + +// `range` is a region of `document`. Modify it in-place so that it +// matches the result of Spark.render(htmlFunc), preserving landmarks. +Spark.renderToRange = function (range, htmlFunc) { + var renderer = new Spark._Renderer(); + + // Call 'func' for each landmark in 'range'. Pass two arguments to + // 'func', the range, and an extra "notes" object such that two + // landmarks receive the same (===) notes object iff they have the + // same branch path. 'func' can write to the notes object so long as + // it limits itself to attributes that do not start with '_'. + var visitLandmarksInRange = function (range, func) { + var stack = renderer.newLabelStack(); + + range.visit(function (isStart, r) { + if (r.type === Spark._ANNOTATION_LABEL) { + if (isStart) + stack.pushLabel(r.label); + else + stack.popLabel(); + } else if (r.type === Spark._ANNOTATION_LANDMARK && isStart) { + func(r, stack.getNotes()); + } + }); + }; + + // Find all of the landmarks in the old contents of the range + visitLandmarksInRange(range, function (landmarkRange, notes) { + notes.originalRange = landmarkRange; + }); + + var frag = renderer.materialize(htmlFunc); + + DomUtils.wrapFragmentForContainer(frag, range.containerNode()); + + var tempRange = new LiveRange(Spark._TAG, frag); + + // find preservation roots from matched landmarks inside the + // rerendered region + var pc = renderer.pc; + visitLandmarksInRange( + tempRange, function (landmarkRange, notes) { + if (notes.originalRange) { + if (landmarkRange.constant) + pc.addConstantRegion(notes.originalRange, landmarkRange); + + pc.addRoot(landmarkRange.preserve, + notes.originalRange, landmarkRange); + } + }); + + // find preservation roots that come from landmarks enclosing the + // updated region + var walk = range; + while ((walk = findParentOfType(Spark._ANNOTATION_LANDMARK, walk))) + pc.addRoot(walk.preserve, range, tempRange, walk.containerNode()); + + // compute preservations (must do this before destroying tempRange) + var preservations = pc.computePreservations(range, tempRange); + + tempRange.destroy(); + + var results = {}; + + // Patch! (using preservations) + range.operate(function (start, end) { + // XXX this will destroy all liveranges, including ones + // inside constant regions whose DOM nodes we are going + // to preserve untouched + Spark.finalize(start, end); + Spark._patch(start.parentNode, frag, start.previousSibling, + end.nextSibling, preservations, results); + }); + + _.each(results.regionPreservations, function (landmarkRange) { + // Rely on the fact that computePreservations only emits + // region preservations whose ranges are landmarks. + // This flag means that landmarkRange is a new constant landmark + // range that matched an old one *and* was DOM-preservable by + // the patcher. + landmarkRange.isPreservedConstant = true; + }); +}; + +// Delete all of the liveranges in the range of nodes between `start` +// and `end`, and call their 'finalize' function if any. Or instead of +// `start` and `end` you may pass a fragment in `start`. +Spark.finalize = function (start, end) { + if (! start.parentNode && start.nodeType !== 11 /* DocumentFragment */) { + // Workaround for LiveRanges' current inability to contain + // a node with no parentNode. + var frag = document.createDocumentFragment(); + frag.appendChild(start); + start = frag; + end = null; + } + var wrapper = new LiveRange(Spark._TAG, start, end); + wrapper.visit(function (isStart, range) { + isStart && range.finalize && range.finalize(); + }); + wrapper.destroy(true /* recursive */); +}; + +/******************************************************************************/ +/* Data contexts */ +/******************************************************************************/ + +Spark.setDataContext = withRenderer(function (dataContext, html, _renderer) { + return _renderer.annotate( + html, Spark._ANNOTATION_DATA, { data: dataContext }); +}); + +Spark.getDataContext = function (node) { + var range = findRangeOfType(Spark._ANNOTATION_DATA, node); + return range && range.data; +}; + +/******************************************************************************/ +/* Events */ +/******************************************************************************/ + +var universalListener = null; +var getListener = function () { + if (!universalListener) + universalListener = new UniversalEventListener(function (event) { + // Handle a currently-propagating event on a particular node. + // We walk each enclosing liverange of the node and offer it the + // chance to handle the event. It's range.handler's + // responsibility to check isImmediatePropagationStopped() + // before delivering events to the user. We precompute the list + // of enclosing liveranges to defend against the case where user + // event handlers change the DOM. + + var ranges = []; + var walk = findRangeOfType(Spark._ANNOTATION_EVENTS, + event.currentTarget); + while (walk) { + ranges.push(walk); + walk = findParentOfType(Spark._ANNOTATION_EVENTS, walk); + } + _.each(ranges, function (r) { + r.handler(event); + }); + }, Spark._checkIECompliance); + + return universalListener; +}; + +Spark.attachEvents = withRenderer(function (eventMap, html, _renderer) { + var listener = getListener(); + + var handlerMap = {}; // type -> [{selector, callback}, ...] + // iterate over eventMap, which has form {"type selector, ...": callback}, + // and populate handlerMap + _.each(eventMap, function(callback, spec) { + var clauses = spec.split(/,\s+/); + // iterate over clauses of spec, e.g. ['click .foo', 'click .bar'] + _.each(clauses, function (clause) { + var parts = clause.split(/\s+/); + if (parts.length === 0) + return; + + var type = parts.shift(); + var selector = parts.join(' '); + + handlerMap[type] = handlerMap[type] || []; + handlerMap[type].push({selector: selector, callback: callback}); + }); + }); + + var eventTypes = _.keys(handlerMap); + + var installHandlers = function (range) { + _.each(eventTypes, function (t) { + for(var n = range.firstNode(), + after = range.lastNode().nextSibling; + n && n !== after; + n = n.nextSibling) + listener.installHandler(n, t); + }); + }; + + html = _renderer.annotate( + html, Spark._ANNOTATION_WATCH, { + notify: function () { + installHandlers(this); + } + }); + + var finalized = false; + + html = _renderer.annotate( + html, Spark._ANNOTATION_EVENTS, function (range) { + _.each(eventTypes, function (t) { + listener.addType(t); + }); + installHandlers(range); + + range.finalize = function () { + finalized = true; + }; + + range.handler = function (event) { + var handlers = handlerMap[event.type] || []; + + for (var i = 0; i < handlers.length; i++) { + if (finalized || event.isImmediatePropagationStopped()) + return; + + var handler = handlers[i]; + var callback = handler.callback; + var selector = handler.selector; + + if (selector) { + // This ends up doing O(n) findAllClipped calls when an + // event bubbles up N level in the DOM. If this ends up + // being too slow, we could memoize findAllClipped across + // the processing of each event. + var results = DomUtils.findAllClipped( + range.containerNode(), selector, range.firstNode(), range.lastNode()); + // This is a linear search through what could be a large + // result set. + if (! _.contains(results, event.currentTarget)) + continue; + } else { + // if no selector, only match the event target + if (event.currentTarget !== event.target) + continue; + } + + // Found a matching handler. Call it. + var eventData = Spark.getDataContext(event.currentTarget); + var landmarkRange = + findParentOfType(Spark._ANNOTATION_LANDMARK, range); + var landmark = (landmarkRange && landmarkRange.landmark); + + // Note that the handler can do arbitrary things, like call + // Meteor.flush() or otherwise remove and finalize parts of + // the DOM. We can't assume `range` is valid past this point, + // and we'll check the `finalized` flag at the top of the loop. + var returnValue = callback.call(eventData, event, landmark); + + // allow app to `return false` from event handler, just like + // you can in a jquery event handler + if (returnValue === false) { + event.stopImmediatePropagation(); + event.preventDefault(); + } + } + }; + }); + + return html; +}); + +/******************************************************************************/ +/* Isolate */ +/******************************************************************************/ + +Spark.isolate = function (htmlFunc) { + var renderer = Spark._currentRenderer.get(); + if (!renderer) + return htmlFunc(); + + var ctx = new Meteor.deps.Context; + + return renderer.annotate( + ctx.run(htmlFunc), Spark._ANNOTATION_ISOLATE, function (range) { + range.finalize = function () { + // Spark.finalize() was called on us (presumably because we were + // removed from the document.) Tear down our structures without + // doing any more updates. note that range is about to be + // destroyed by finalize. + range = null; + ctx.invalidate(); + }; + + var refresh = function () { + if (! range) + return; // killed by finalize. range has already been destroyed. + + ctx = new Meteor.deps.Context; + Spark.renderToRange(range, function () { + return ctx.run(htmlFunc); + }); + ctx.on_invalidate(refresh); + }; + + ctx.on_invalidate(refresh); + }); +}; + +/******************************************************************************/ +/* Lists */ +/******************************************************************************/ + +// Run 'f' at flush()-time. If atFlushTime is called multiple times, +// we guarantee that the 'f's will run in the order of their +// respective atFlushTime calls. +// +// XXX either break this out into a separate package or fold it into +// deps +var atFlushQueue = []; +var atFlushContext = null; +var atFlushTime = function (f) { + atFlushQueue.push(f); + + if (! atFlushContext) { + atFlushContext = new Meteor.deps.Context; + atFlushContext.on_invalidate(function () { + var f; + while ((f = atFlushQueue.shift())) { + // Since atFlushContext is truthy, if f() calls atFlushTime + // reentrantly, it's guaranteed to append to atFlushQueue and + // not contruct a new atFlushContext. + f(); + } + atFlushContext = null; + }); + + atFlushContext.invalidate(); + } +}; + +Spark.list = function (cursor, itemFunc, elseFunc) { + elseFunc = elseFunc || function () { return ''; }; + + // Create a level of indirection around our cursor callbacks so we + // can change them later + var callbacks = {}; + var observerCallbacks = {}; + _.each(["added", "removed", "moved", "changed"], function (name) { + observerCallbacks[name] = function () { + return callbacks[name].apply(null, arguments); + }; + }); + + // Get the current contents of the cursor. + // XXX currently we count on observe() using only added() to deliver + // the initial contents. are we allow to do that, or do we need to + // implement removed/moved/changed here as well? + var initialContents = []; + _.extend(callbacks, { + added: function (item, beforeIndex) { + initialContents.splice(beforeIndex, 0, item); + } + }); + var handle = cursor.observe(observerCallbacks); + + // Get the renderer, if any + var renderer = Spark._currentRenderer.get(); + var annotate = renderer ? + _.bind(renderer.annotate, renderer) : + function (html) { return html; }; + + // Render the initial contents. If we have a renderer, create a + // range around each item as well as around the list, and save them + // off for later. + var html = ''; + var outerRange; + var itemRanges = []; + if (! initialContents.length) + html = elseFunc(); + else { + for (var i = 0; i < initialContents.length; i++) { + (function (i) { + html += annotate(itemFunc(initialContents[i]), + Spark._ANNOTATION_LIST_ITEM, + function (range) { + itemRanges[i] = range; + }); + })(i); // scope i to closure + } + } + initialContents = null; // save memory + var stopped = false; + var cleanup = function () { + handle.stop(); + stopped = true; + }; + html = annotate(html, Spark._ANNOTATION_LIST, function (range) { + outerRange = range; + outerRange.finalize = cleanup; + }, function () { + // We never ended up on the screen (caller discarded our return + // value) + cleanup(); + }); + + // No renderer? Then we have no way to update the returned html and + // we can close the observer. + if (! renderer) + cleanup(); + + // Called by `removed` and `moved` in order to cause render callbacks on + // parent landmarks. + // XXX This is not the final solution. 1) This code should be unified + // with the code in scheduleOnscreenSetup. 2) In general, lists are + // going to cause a lot of callbacks (one per collection callback). + // Maybe that will make sense if we give render callbacks subrange info. + var notifyParentsRendered = function () { + var walk = outerRange; + while ((walk = findParentOfType(Spark._ANNOTATION_LANDMARK, walk))) + walk.rendered.call(walk.landmark); + }; + + var later = function (f) { + atFlushTime(function () { + if (! stopped) + f(); + }); + }; + + // The DOM update callbacks. + _.extend(callbacks, { + added: function (item, beforeIndex) { + later(function () { + var frag = Spark.render(_.bind(itemFunc, null, item)); + DomUtils.wrapFragmentForContainer(frag, outerRange.containerNode()); + var range = new LiveRange(Spark._TAG, frag); + + if (! itemRanges.length) { + Spark.finalize(outerRange.replaceContents(frag)); + } else if (beforeIndex === itemRanges.length) { + itemRanges[itemRanges.length - 1].insertAfter(frag); + } else { + itemRanges[beforeIndex].insertBefore(frag); + } + + itemRanges.splice(beforeIndex, 0, range); + }); + }, + + removed: function (item, atIndex) { + later(function () { + if (itemRanges.length === 1) { + var frag = Spark.render(elseFunc); + DomUtils.wrapFragmentForContainer(frag, outerRange.containerNode()); + Spark.finalize(outerRange.replaceContents(frag)); + } else + Spark.finalize(itemRanges[atIndex].extract()); + + itemRanges.splice(atIndex, 1); + + notifyParentsRendered(); + }); + }, + + moved: function (item, oldIndex, newIndex) { + later(function () { + if (oldIndex === newIndex) + return; + + var frag = itemRanges[oldIndex].extract(); + var range = itemRanges.splice(oldIndex, 1)[0]; + if (newIndex === itemRanges.length) + itemRanges[itemRanges.length - 1].insertAfter(frag); + else + itemRanges[newIndex].insertBefore(frag); + + itemRanges.splice(newIndex, 0, range); + + notifyParentsRendered(); + }); + }, + + changed: function (item, atIndex) { + later(function () { + Spark.renderToRange(itemRanges[atIndex], _.bind(itemFunc, null, item)); + }); + } + }); + + return html; +}; + +/******************************************************************************/ +/* Labels and landmarks */ +/******************************************************************************/ + +var nextLandmarkId = 1; + +Spark.Landmark = function () { + this.id = nextLandmarkId++; + this._range = null; // will be set when put onscreen +}; + +_.extend(Spark.Landmark.prototype, { + firstNode: function () { + return this._range.firstNode(); + }, + lastNode: function () { + return this._range.lastNode(); + }, + find: function (selector) { + var r = this._range; + return DomUtils.findClipped(r.containerNode(), selector, + r.firstNode(), r.lastNode()); + }, + findAll: function (selector) { + var r = this._range; + return DomUtils.findAllClipped(r.containerNode(), selector, + r.firstNode(), r.lastNode()); + }, + hasDom: function () { + return !! this._range; + } +}); + +Spark.UNIQUE_LABEL = ['UNIQUE_LABEL']; + +// label must be a string. +// or pass label === null to not drop a label after all (meaning that +// this function is a noop) +Spark.labelBranch = function (label, htmlFunc) { + var renderer = Spark._currentRenderer.get(); + if (! renderer || label === null) + return htmlFunc(); + + if (label === Spark.UNIQUE_LABEL) + label = Spark._createId(); + + renderer.currentBranch.pushLabel(label); + var html = htmlFunc(); + var occupied = renderer.currentBranch.getNotes().occupied; + renderer.currentBranch.popLabel(); + + if (! occupied) + // don't create annotation if branch doesn't contain any landmarks. + // if this label isn't on an element-level HTML boundary, then that + // is certainly the case. + return html; + + return renderer.annotate( + html, Spark._ANNOTATION_LABEL, { label: label }); + + // XXX what happens if the user doesn't use the return value, or + // doesn't use it directly, eg, swaps the branches of the tree + // around? "that's an error?" the result would be that the apparent + // branch path of a landmark at render time would be different from + // its apparent branch path in the actual document. seems like the + // answer is to have labelBranch not drop an annotation, and keep + // the branch label info outside of the DOM in a parallel tree of + // labels and landmarks (likely similar to the one we're already + // keeping?) a little tricky since not every node in the label tree + // is actually populated with a landmark? (though we could change + // that I guess -- they would be landmarks without any specific DOM + // nodes?) +}; + +Spark.createLandmark = function (options, htmlFunc) { + var renderer = Spark._currentRenderer.get(); + if (! renderer) { + // no renderer -- create and destroy Landmark inline + var landmark = new Spark.Landmark; + options.created && options.created.call(landmark); + var html = htmlFunc(landmark); + options.destroyed && options.destroyed.call(landmark); + return html; + } + + // Normalize preserve map + var preserve = {}; + if (_.isArray(options.preserve)) + _.each(options.preserve, function (selector) { + preserve[selector] = true; + }); + else + preserve = options.preserve || {}; + for (var selector in preserve) + if (typeof preserve[selector] !== 'function') + preserve[selector] = function () { return true; }; + + renderer.currentBranch.mark('occupied'); + var notes = renderer.currentBranch.getNotes(); + var landmark; + if (notes.originalRange) { + if (notes.originalRange.superceded) + throw new Error("Can't create second landmark in same branch"); + notes.originalRange.superceded = true; // prevent destroyed(), second match + landmark = notes.originalRange.landmark; // the old Landmark + } else { + landmark = new Spark.Landmark; + if (options.created) { + // Run callback outside the current Spark.isolate's deps context. + // XXX Can't call run() on null, so this is a hack. Running inside + // a fresh context wouldn't be equivalent. + var oldCx = Meteor.deps.Context.current; + Meteor.deps.Context.current = null; + try { + options.created.call(landmark); + } finally { + Meteor.deps.Context.current = oldCx; + } + } + } + notes.landmark = landmark; + + var html = htmlFunc(landmark); + return renderer.annotate( + html, Spark._ANNOTATION_LANDMARK, function (range) { + _.extend(range, { + preserve: preserve, + constant: !! options.constant, + rendered: options.rendered || function () {}, + destroyed: options.destroyed || function () {}, + landmark: landmark, + finalize: function () { + if (! this.superceded) { + this.landmark._range = null; + this.destroyed.call(this.landmark); + } + } + }); + + landmark._range = range; + renderer.landmarkRanges.push(range); + }, function () { + // "annotation not used" callback + options.destroyed && options.destroyed.call(landmark); + }); +}; + +// used by unit tests +Spark._getEnclosingLandmark = function (node) { + var range = findRangeOfType(Spark._ANNOTATION_LANDMARK, node); + return range ? range.landmark : null; +}; + +})(); diff --git a/packages/spark/spark_tests.js b/packages/spark/spark_tests.js new file mode 100644 index 0000000000..b28998157e --- /dev/null +++ b/packages/spark/spark_tests.js @@ -0,0 +1,3623 @@ +// XXX make sure that when tests use id="..." to trigger patching, "preserve" happens +// XXX test that events inside constant regions still work after patching +// XXX test arguments to landmark rendered callback +// XXX test variable wrapping (eg TR vs THEAD) inside each branch of Spark.list? + + +Spark._checkIECompliance = true; + +(function () { + +var legacyLabels = { + '*[id], #[name]': function(n) { + var label = null; + + if (n.nodeType === 1) { + if (n.id) { + label = '#'+n.id; + } else if (n.getAttribute("name")) { + label = n.getAttribute("name"); + // Radio button special case: radio buttons + // in a group all have the same name. Their value + // determines their identity. + // Checkboxes with the same name and different + // values are also sometimes used in apps, so + // we treat them similarly. + if (n.nodeName === 'INPUT' && + (n.type === 'radio' || n.type === 'checkbox') && + n.value) + label = label + ':' + n.value; + } + } + + return label; + } +}; + +var renderWithLegacyLabels = function (htmlFunc) { + return Meteor.render(function () { + return Spark.createLandmark({ preserve: legacyLabels }, + htmlFunc); + }); +}; + +var eventmap = function (/*args*/) { + // support event_buf as final argument + var event_buf = null; + if (arguments.length && _.isArray(arguments[arguments.length-1])) { + event_buf = arguments[arguments.length-1]; + arguments.length--; + } + var events = {}; + _.each(arguments, function (esel) { + var etyp = esel.split(' ')[0]; + events[esel] = function (evt) { + if (evt.type !== etyp) + throw new Error(etyp+" event arrived as "+evt.type); + (event_buf || this).push(esel); + }; + }); + return events; +}; + +Tinytest.add("spark - assembly", function (test) { + + var furtherCanon = function(str) { + // further canonicalize innerHTML in IE by adding close + // li tags to "
    • one
    • two
    • three
    " + return str.replace(/
  • (\w*)(?=
  • )/g, function(s) { + return s+"
  • "; + }); + }; + + var doTest = function (calc) { + var frag = Spark.render(function () { + return calc(function (str, expected) { + return Spark.setDataContext(null, str); + }); + }); + var groups = []; + var html = calc(function (str, expected, noRange) { + if (arguments.length > 1) + str = expected; + if (! noRange) + groups.push(str); + return str; + }); + var f = WrappedFrag(frag); + test.equal(furtherCanon(f.html()), html); + + var actualGroups = []; + var tempRange = new LiveRange(Spark._TAG, frag); + tempRange.visit(function (isStart, rng) { + if (! isStart && rng.type === Spark._ANNOTATION_DATA) + actualGroups.push(furtherCanon(canonicalizeHtml( + DomUtils.rangeToHtml(rng.firstNode(), rng.lastNode())))); + }); + test.equal(actualGroups.join(','), groups.join(',')); + }; + + doTest(function (A) { return "

    Hello

    "; }); + doTest(function (A) { return "HelloWorld"; }); + doTest(function (A) { return ""+A("Hello")+""; }); + doTest(function (A) { return A(""+A("Hello")+""); }); + doTest(function (A) { return A(A(A(A(A(A("foo")))))); }); + doTest( + function (A) { return "
    Yo"+A("

    Hello "+A(A("World")),"

    Hello World

    ")+ + "
    "; }); + doTest(function (A) { + return A("
      "+A("
    • one","
    • one
    • ")+ + A("
    • two","
    • two
    • ")+ + A("
    • three","
    • three
    • "), + "
      • one
      • two
      • three
      "); }); + + doTest(function (A) { + return A(""+A(""+A("")+"")+"
      "+A("Hi")+"
      ", + "
      Hi
      "); + }); + + test.throws(function () { + doTest(function (A) { + var z = A("Hello"); + return z+z; + }); + }); + + var frag = Spark.render(function () { + return '
      Hello
      '; + }); + var div = frag.firstChild; + test.equal(div.nodeName, "DIV"); + var attrValue = div.getAttribute('foo'); + test.isTrue(attrValue.indexOf('abcxyz') >= 0, attrValue); +}); + + +Tinytest.add("spark - repeat inclusion", function(test) { + test.throws(function() { + var frag = Spark.render(function() { + var x = Spark.setDataContext({}, "abc"); + return x + x; + }); + }); +}); + + +Tinytest.add("spark - basic tag contents", function (test) { + + // adapted from nateps / metamorph + + var do_onscreen = function (f) { + var div = OnscreenDiv(); + var stuff = { + div: div, + node: _.bind(div.node, div), + render: function (rfunc) { + div.node().appendChild(Meteor.render(rfunc)); + } + }; + + f.call(stuff); + + div.kill(); + }; + + var R, div; + + // basic text replace + + do_onscreen(function () { + R = ReactiveVar("one two three"); + this.render(function () { + return R.get(); + }); + R.set("three four five six"); + Meteor.flush(); + test.equal(this.div.html(), "three four five six"); + }); + + // work inside a table + + do_onscreen(function () { + R = ReactiveVar("HI!"); + this.render(function () { + return "" + R.get() + "
      "; + }); + + test.equal($(this.node()).find("#morphing td").text(), "HI!"); + R.set("BUH BYE!"); + Meteor.flush(); + test.equal($(this.node()).find("#morphing td").text(), "BUH BYE!"); + }); + + // work inside a tbody + + do_onscreen(function () { + R = ReactiveVar("HI!"); + this.render(function () { + return "" + R.get() + "
      "; + }); + + test.equal($(this.node()).find("#morphing td").text(), "HI!"); + R.set("BUH BYE!"); + Meteor.flush(); + test.equal($(this.node()).find("#morphing td").text(), "BUH BYE!"); + }); + + // work inside a tr + + do_onscreen(function () { + R = ReactiveVar("HI!"); + this.render(function () { + return "" + R.get() + "
      "; + }); + + test.equal($(this.node()).find("#morphing td").text(), "HI!"); + R.set("BUH BYE!"); + Meteor.flush(); + test.equal($(this.node()).find("#morphing td").text(), "BUH BYE!"); + }); + + // work inside a ul + + do_onscreen(function () { + R = ReactiveVar("
    • HI!
    • "); + this.render(function () { + return "
        " + R.get() + "
      "; + }); + + test.equal($(this.node()).find("#morphing li").text(), "HI!"); + R.set("
    • BUH BYE!
    • "); + Meteor.flush(); + test.equal($(this.node()).find("#morphing li").text(), "BUH BYE!"); + }); + + // work inside a select + + do_onscreen(function () { + R = ReactiveVar(""); + this.render(function () { + return ""; + }); + + test.equal($(this.node()).find("#morphing option").text(), "HI!"); + R.set(""); + Meteor.flush(); + test.equal($(this.node()).find("#morphing option").text(), "BUH BYE!"); + }); + +}); + + +Tinytest.add("spark - basic isolate", function (test) { + + var R = ReactiveVar('foo'); + + var div = OnscreenDiv(Spark.render(function () { + return '
      ' + Spark.isolate(function () { + return '' + R.get() + ''; + }) + '
      '; + })); + + test.equal(div.html(), '
      foo
      '); + R.set('bar'); + test.equal(div.html(), '
      foo
      '); + Meteor.flush(); + test.equal(div.html(), '
      bar
      '); + R.set('baz'); + Meteor.flush(); + test.equal(div.html(), '
      baz
      '); + + div.kill(); + Meteor.flush(); +}); + +Tinytest.add("spark - one render", function (test) { + + var R = ReactiveVar("foo"); + + var frag = WrappedFrag(Meteor.render(function () { + return R.get(); + })).hold(); + + test.equal(R.numListeners(), 1); + + // frag should be "foo" initially + test.equal(frag.html(), "foo"); + R.set("bar"); + // haven't flushed yet, so update won't have happened + test.equal(frag.html(), "foo"); + Meteor.flush(); + // flushed now, frag should say "bar" + test.equal(frag.html(), "bar"); + frag.release(); // frag is now considered offscreen + Meteor.flush(); + R.set("baz"); + Meteor.flush(); + // no update should have happened, offscreen range dep killed + test.equal(frag.html(), "bar"); + + // should be back to no listeners + test.equal(R.numListeners(), 0); + + // empty return value should work, and show up as a comment + frag = WrappedFrag(Meteor.render(function () { + return ""; + })); + test.equal(frag.html(), ""); + + // nodes coming and going at top level of fragment + R.set(true); + frag = WrappedFrag(Meteor.render(function () { + return R.get() ? "
      hello
      world
      " : ""; + })).hold(); + test.equal(frag.html(), "
      hello
      world
      "); + R.set(false); + Meteor.flush(); + test.equal(frag.html(), ""); + R.set(true); + Meteor.flush(); + test.equal(frag.html(), "
      hello
      world
      "); + test.equal(R.numListeners(), 1); + frag.release(); + Meteor.flush(); + test.equal(R.numListeners(), 0); + + // more complicated changes + R.set(1); + frag = WrappedFrag(Meteor.render(function () { + var result = []; + for(var i=0; i

      '+ + R.get()+'

      '); + } + return result.join(''); + })).hold(); + test.equal(frag.html(), + '

      1

      '); + R.set(3); + Meteor.flush(); + test.equal(frag.html(), + '

      3

      '+ + '

      3

      '+ + '

      3

      '); + R.set(2); + Meteor.flush(); + test.equal(frag.html(), + '

      2

      '+ + '

      2

      '); + frag.release(); + Meteor.flush(); + test.equal(R.numListeners(), 0); + + // caller violating preconditions + test.equal(WrappedFrag(Meteor.render("foo")).html(), "foo"); +}); + +Tinytest.add("spark - heuristic finalize", function (test) { + + var R = ReactiveVar(123); + + var div = OnscreenDiv(Meteor.render(function () { + return "

      The number is "+R.get()+".




      underlined"; + })); + + test.equal(div.html(), "

      The number is 123.




      underlined"); + test.equal(R.numListeners(), 1); + Meteor.flush(); + R.set(456); // won't take effect until flush() + test.equal(div.html(), "

      The number is 123.




      underlined"); + test.equal(R.numListeners(), 1); + Meteor.flush(); + test.equal(div.html(), "

      The number is 456.




      underlined"); + test.equal(R.numListeners(), 1); + + div.remove(); + R.set(789); // update should force div dependency to be GCed when div is updated + Meteor.flush(); + test.equal(R.numListeners(), 0); +}); + +Tinytest.add("spark - isolate", function (test) { + + var inc = function (v) { + v.set(v.get() + 1); }; + + var R1 = ReactiveVar(0); + var R2 = ReactiveVar(0); + var R3 = ReactiveVar(0); + var count1 = 0, count2 = 0, count3 = 0; + + var frag = WrappedFrag(Meteor.render(function () { + return R1.get() + "," + (count1++) + " " + + Spark.isolate(function () { + return R2.get() + "," + (count2++) + " " + + Spark.isolate(function () { + return R3.get() + "," + (count3++); + }); + }); + })).hold(); + + test.equal(frag.html(), "0,0 0,0 0,0"); + + inc(R1); Meteor.flush(); + test.equal(frag.html(), "1,1 0,1 0,1"); + + inc(R2); Meteor.flush(); + test.equal(frag.html(), "1,1 1,2 0,2"); + + inc(R3); Meteor.flush(); + test.equal(frag.html(), "1,1 1,2 1,3"); + + inc(R2); Meteor.flush(); + test.equal(frag.html(), "1,1 2,3 1,4"); + + inc(R1); Meteor.flush(); + test.equal(frag.html(), "2,2 2,4 1,5"); + + frag.release(); + Meteor.flush(); + test.equal(R1.numListeners(), 0); + test.equal(R2.numListeners(), 0); + test.equal(R3.numListeners(), 0); + + R1.set(0); + R2.set(0); + R3.set(0); + + frag = WrappedFrag(Meteor.render(function () { + var buf = []; + buf.push('
      '); + buf.push(Spark.isolate(function () { + var buf = []; + for(var i=0; i'+R3.get()+'
      '; + })); + } + return buf.join(''); + })); + buf.push(''); + return buf.join(''); + })).hold(); + + test.equal(frag.html(), '
      '); + R2.set(3); Meteor.flush(); + test.equal(frag.html(), '
      '+ + '
      0
      0
      0
      '+ + '
      '); + + R3.set(5); Meteor.flush(); + test.equal(frag.html(), '
      '+ + '
      5
      5
      5
      '+ + '
      '); + + R1.set(7); Meteor.flush(); + test.equal(frag.html(), '
      '+ + '
      5
      5
      5
      '+ + '
      '); + + R2.set(1); Meteor.flush(); + test.equal(frag.html(), '
      '+ + '
      5
      '+ + '
      '); + + R1.set(11); Meteor.flush(); + test.equal(frag.html(), '
      '+ + '
      5
      '+ + '
      '); + + R2.set(2); Meteor.flush(); + test.equal(frag.html(), '
      '+ + '
      5
      5
      '+ + '
      '); + + R3.set(4); Meteor.flush(); + test.equal(frag.html(), '
      '+ + '
      4
      4
      '+ + '
      '); + + frag.release(); + + // calling isolate() outside of render mode + test.equal(Spark.isolate(function () { return "foo"; }), "foo"); + + // caller violating preconditions + + test.throws(function () { + Meteor.render(function () { + return Spark.isolate("foo"); + }); + }); + + + // unused isolate + + var Q = ReactiveVar("foo"); + Meteor.render(function () { + // create an isolate, in render mode, + // but don't use it. + Spark.isolate(function () { + return Q.get(); + }); + return ""; + }); + Q.set("bar"); + // might get an error on flush() if implementation + // deals poorly with unused isolates, or a listener + // still existing after flush. + Meteor.flush(); + test.equal(Q.numListeners(), 0); + + // nesting + + var stuff = ReactiveVar(true); + var div = OnscreenDiv(Meteor.render(function () { + return Spark.isolate(function () { + return "x"+(stuff.get() ? 'y' : '') + Spark.isolate(function () { + return "hi"; + }); + }); + })); + test.equal(div.html(), "xyhi"); + stuff.set(false); + Meteor.flush(); + test.equal(div.html(), "xhi"); + div.kill(); + Meteor.flush(); + + // more nesting + + var num1 = ReactiveVar(false); + var num2 = ReactiveVar(false); + var num3 = ReactiveVar(false); + var numset = function (n) { + _.each([num1, num2, num3], function (v, i) { + v.set((i+1) === n); + }); + }; + numset(1); + + var div = OnscreenDiv(Meteor.render(function () { + return Spark.isolate(function () { + return (num1.get() ? '1' : '')+ + Spark.isolate(function () { + return (num2.get() ? '2' : '')+ + Spark.isolate(function () { + return (num3.get() ? '3' : '')+'x'; + }); + }); + }); + })); + test.equal(div.html(), "1x"); + numset(2); + Meteor.flush(); + test.equal(div.html(), "2x"); + numset(3); + Meteor.flush(); + test.equal(div.html(), "3x"); + numset(1); + Meteor.flush(); + test.equal(div.html(), "1x"); + numset(3); + Meteor.flush(); + test.equal(div.html(), "3x"); + numset(2); + Meteor.flush(); + test.equal(div.html(), "2x"); + div.remove(); + Meteor.flush(); + + // the real test for slow-path GC finalization: + num2.set(! num2.get()); + Meteor.flush(); + test.equal(num1.numListeners(), 0); + test.equal(num2.numListeners(), 0); + test.equal(num3.numListeners(), 0); +}); + +Tinytest.add("spark - data context", function (test) { + var d1 = {x: 1}; + var d2 = {x: 2}; + var d3 = {x: 3}; + var d4 = {x: 4}; + var d5 = {x: 5}; + + var traverse = function (frag) { + var out = ''; + var walkChildren = function (parent) { + for (var node = parent.firstChild; node; node = node.nextSibling) { + if (node.nodeType !== 8 /* COMMENT */) { + var data = Spark.getDataContext(node); + out += (data === null) ? "_" : data.x; + } + if (node.nodeType === 1 /* ELEMENT */) + walkChildren(node); + } + }; + walkChildren(frag); + return out; + }; + + var testData = function (serialized, htmlFunc) { + test.equal(traverse(Spark.render(htmlFunc)), serialized); + }; + + testData("_", function () { + return "hi"; + }); + + testData("__", function () { + return "
      hi
      "; + }); + + testData("_1", function () { + return "
      " + Spark.setDataContext(d1, "hi") + "
      "; + }); + + testData("21", function () { + return Spark.setDataContext( + d2, "
      " + Spark.setDataContext(d1, "hi") + "
      "); + }); + + testData("21", function () { + return Spark.setDataContext( + d2, "
      " + + Spark.setDataContext(d3, + Spark.setDataContext(d1, "hi")) + + "
      "); + }); + + testData("23", function () { + return Spark.setDataContext( + d2, "
      " + + Spark.setDataContext(d1, + Spark.setDataContext(d3, "hi")) + + "
      "); + }); + + testData("23", function () { + var html = Spark.setDataContext( + d2, "
      " + + Spark.setDataContext(d1, + Spark.setDataContext(d3, "hi")) + + "
      "); + return Spark.setDataContext(d4, html); + }); + + testData("1_2", function () { + return Spark.setDataContext(d1, "hi") + "-" + + Spark.setDataContext(d2, "there"); + }); + + testData("_122_3__45", function () { + return "
      " + + Spark.setDataContext(d1, "
      ") + + Spark.setDataContext(d2, "
      ") + + "
      " + + Spark.setDataContext(d3, "
      ") + + "
      " + + Spark.setDataContext(d4, "
      " + + Spark.setDataContext(d5, "
      ") + + "
      "); + }); +}); + +Tinytest.add("spark - tables", function (test) { + var R = ReactiveVar(0); + + var table = OnscreenDiv(Meteor.render(function () { + var buf = []; + buf.push(""); + for(var i=0; i"); + buf.push("
      "+(i+1)+"
      "); + return buf.join(''); + })); + + R.set(1); + Meteor.flush(); + test.equal(table.html(), "
      1
      "); + + R.set(10); + test.equal(table.html(), "
      1
      "); + Meteor.flush(); + test.equal(table.html(), ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + "
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      "); + + R.set(0); + Meteor.flush(); + test.equal(table.html(), "
      "); + table.kill(); + Meteor.flush(); + test.equal(R.numListeners(), 0); + + var div = OnscreenDiv(); + div.node().appendChild(document.createElement("TABLE")); + div.node().firstChild.appendChild(Meteor.render(function () { + var buf = []; + for(var i=0; i"+(i+1)+""); + return buf.join(''); + })); + test.equal(div.html(), "
      "); + R.set(3); + Meteor.flush(); + test.equal(div.html(), ""+ + ""+ + ""+ + ""+ + "
      1
      2
      3
      "); + test.equal(div.node().firstChild.rows.length, 3); + R.set(0); + Meteor.flush(); + test.equal(div.html(), "
      "); + div.kill(); + Meteor.flush(); + + test.equal(R.numListeners(), 0); + + div = OnscreenDiv(); + div.node().appendChild(DomUtils.htmlToFragment("
      ")); + R.set(3); + div.node().getElementsByTagName("tr")[0].appendChild(Meteor.render( + function () { + var buf = []; + for(var i=0; i"+(i+1)+""); + return buf.join(''); + })); + test.equal(div.html(), + ""+ + "
      123
      "); + R.set(1); + Meteor.flush(); + test.equal(div.html(), + "
      1
      "); + div.kill(); + Meteor.flush(); + test.equal(R.numListeners(), 0); + + div = OnscreenDiv(renderWithLegacyLabels(function() { + return ''+R.get()+'
      '; + })); + Meteor.flush(); + R.set("Hello"); + Meteor.flush(); + test.equal( + div.html(), + '
      Hello
      '); + div.kill(); + Meteor.flush(); + + test.equal(R.numListeners(), 0); +}); + +Tinytest.add("spark - event handling", function (test) { + var event_buf = []; + var getid = function (id) { + return document.getElementById(id); + }; + + var div; + + var chunk = function (htmlFunc, options) { + var html = Spark.isolate(htmlFunc); + options = options || {}; + if (options.events) + html = Spark.attachEvents(options.events, html); + if (options.event_data) + html = Spark.setDataContext(options.event_data, html); + return html; + }; + + var render = function (htmlFunc, options) { + return Spark.render(function () { + return chunk(htmlFunc, options); + }); + }; + + + // clicking on a div at top level + event_buf.length = 0; + div = OnscreenDiv(render(function () { + return '
      Foo
      '; + }, {events: eventmap("click"), event_data:event_buf})); + clickElement(getid("foozy")); + test.equal(event_buf, ['click']); + div.kill(); + Meteor.flush(); + + // selector that specifies a top-level div + event_buf.length = 0; + div = OnscreenDiv(render(function () { + return '
      Foo
      '; + }, {events: eventmap("click div"), event_data:event_buf})); + clickElement(getid("foozy")); + test.equal(event_buf, ['click div']); + div.kill(); + Meteor.flush(); + + // selector that specifies a second-level span + event_buf.length = 0; + div = OnscreenDiv(render(function () { + return '
      Foo
      '; + }, {events: eventmap("click span"), event_data:event_buf})); + clickElement(getid("foozy").firstChild); + test.equal(event_buf, ['click span']); + div.kill(); + Meteor.flush(); + + // replaced top-level elements still have event handlers + // even if replaced by an isolate above the handlers in the DOM + var R = ReactiveVar("p"); + event_buf.length = 0; + div = OnscreenDiv(render(function () { + return chunk(function () { + return '<'+R.get()+' id="foozy">Hello'; + }); + }, {events: eventmap("click"), event_data:event_buf})); + clickElement(getid("foozy")); + test.equal(event_buf, ['click']); + event_buf.length = 0; + R.set("div"); // change tag, which is sure to replace element + Meteor.flush(); + clickElement(getid("foozy")); // still clickable? + test.equal(event_buf, ['click']); + event_buf.length = 0; + R.set("p"); + Meteor.flush(); + clickElement(getid("foozy")); + test.equal(event_buf, ['click']); + event_buf.length = 0; + div.kill(); + Meteor.flush(); + + // bubbling from event on descendent of element matched + // by selector + event_buf.length = 0; + div = OnscreenDiv(render(function () { + return '
      Foo'+ + 'Bar
      '; + }, {events: eventmap("click span"), event_data:event_buf})); + clickElement( + getid("foozy").firstChild.firstChild.firstChild); + test.equal(event_buf, ['click span']); + div.kill(); + Meteor.flush(); + + // bubbling order (for same event, same render node, different selector nodes) + event_buf.length = 0; + div = OnscreenDiv(render(function () { + return '
      Foo'+ + 'Bar
      '; + }, {events: eventmap("click span", "click b"), event_data:event_buf})); + clickElement( + getid("foozy").firstChild.firstChild.firstChild); + test.equal(event_buf, ['click b', 'click span']); + div.kill(); + Meteor.flush(); + + // "bubbling" order for handlers at same level + event_buf.length = 0; + div = OnscreenDiv(render(function () { + return chunk(function () { + return chunk(function () { + return 'Hello'; + }, {events: eventmap("click .c"), event_data:event_buf}); + }, {events: eventmap("click .b"), event_data:event_buf}); + }, {events: eventmap("click .a"), event_data:event_buf})); + clickElement(getid("foozy")); + test.equal(event_buf, ['click .c', 'click .b', 'click .a']); + event_buf.length = 0; + div.kill(); + Meteor.flush(); + + // stopPropagation doesn't prevent other event maps from + // handling same node + event_buf.length = 0; + div = OnscreenDiv(render(function () { + return chunk(function () { + return chunk(function () { + return 'Hello'; + }, {events: eventmap("click .c"), event_data:event_buf}); + }, {events: {"click .b": function (evt) { + event_buf.push("click .b"); evt.stopPropagation();}}}); + }, {events: eventmap("click .a"), event_data:event_buf})); + clickElement(getid("foozy")); + test.equal(event_buf, ['click .c', 'click .b', 'click .a']); + event_buf.length = 0; + div.kill(); + Meteor.flush(); + + // stopImmediatePropagation DOES + event_buf.length = 0; + div = OnscreenDiv(render(function () { + return chunk(function () { + return chunk(function () { + return 'Hello'; + }, {events: eventmap("click .c"), event_data:event_buf}); + }, {events: {"click .b": function (evt) { + event_buf.push("click .b"); + evt.stopImmediatePropagation();}}}); + }, {events: eventmap("click .a"), event_data:event_buf})); + clickElement(getid("foozy")); + test.equal(event_buf, ['click .c', 'click .b']); + event_buf.length = 0; + div.kill(); + Meteor.flush(); + + // bubbling continues even with DOM change + event_buf.length = 0; + R = ReactiveVar(true); + div = OnscreenDiv(render(function () { + return chunk(function () { + return '
      '+(R.get()?'abcd':'')+'
      '; + }, {events: { 'click span': function () { + event_buf.push('click span'); + R.set(false); + Meteor.flush(); // kill the span + }, 'click div': function (evt) { + event_buf.push('click div'); + }}}); + })); + // click on span + clickElement(getid("foozy")); + test.expect_fail(); // doesn't seem to work in old IE + test.equal(event_buf, ['click span', 'click div']); + event_buf.length = 0; + div.kill(); + Meteor.flush(); + + // "deep reach" from high node down to replaced low node. + // Tests that events are registered correctly to work in + // old IE. Also tests change event bubbling + // and proper interpretation of event maps. + event_buf.length = 0; + R = ReactiveVar('foo'); + div = OnscreenDiv(render(function () { + return '

      '+ + chunk(function () { + return ''+R.get(); + }, {events: eventmap('click input'), event_data:event_buf}) + + '

      '; + }, { events: eventmap('change b', 'change input'), event_data:event_buf })); + R.set('bar'); + Meteor.flush(); + // click on input + clickElement(div.node().getElementsByTagName('input')[0]); + event_buf.sort(); // don't care about order + test.equal(event_buf, ['change b', 'change input', 'click input']); + event_buf.length = 0; + div.kill(); + Meteor.flush(); + + // test that 'click *' fires on bubble + event_buf.length = 0; + R = ReactiveVar('foo'); + div = OnscreenDiv(render(function () { + return '

      '+ + chunk(function () { + return ''+R.get(); + }, {events: eventmap('click input'), event_data:event_buf}) + + '

      '; + }, { events: eventmap('click *'), event_data:event_buf })); + R.set('bar'); + Meteor.flush(); + // click on input + clickElement(div.node().getElementsByTagName('input')[0]); + test.equal( + event_buf, + ['click input', 'click *', 'click *', 'click *', 'click *', 'click *']); + event_buf.length = 0; + div.kill(); + Meteor.flush(); + + // clicking on a div in a nested chunk (without patching) + event_buf.length = 0; + R = ReactiveVar('foo'); + div = OnscreenDiv(render(function () { + return R.get() + chunk(function () { + return 'ism'; + }, {events: eventmap("click"), event_data:event_buf}); + })); + test.equal(div.text(), 'fooism'); + clickElement(div.node().getElementsByTagName('SPAN')[0]); + test.equal(event_buf, ['click']); + event_buf.length = 0; + R.set('bar'); + Meteor.flush(); + test.equal(div.text(), 'barism'); + clickElement(div.node().getElementsByTagName('SPAN')[0]); + test.equal(event_buf, ['click']); + event_buf.length = 0; + div.kill(); + Meteor.flush(); + + // Test that reactive fragments manually inserted inside + // a reactive fragment eventually get wired. + event_buf.length = 0; + div = OnscreenDiv(render(function () { + return "
      "; + }, { events: eventmap("click span", event_buf) })); + Meteor.flush(); + div.node().firstChild.appendChild(render(function () { + return 'hello'; + })); + clickElement(getid("foozy")); + // implementation has no way to know we've inserted the fragment + test.equal(event_buf, []); + event_buf.length = 0; + Meteor.flush(); + clickElement(getid("foozy")); + // now should be wired up + test.equal(event_buf, ['click span']); + event_buf.length = 0; + div.kill(); + Meteor.flush(); + + // Event data comes from event.currentTarget, not event.target + var data_buf = []; + div = OnscreenDiv(render(function () { + return "
        "+chunk(function () { + return '
      • Hello
      • '; + }, { event_data: {x:'listuff'} })+"
      "; + }, { event_data: {x:'ulstuff'}, + events: { 'click ul': function () { data_buf.push(this); }}})); + clickElement(getid("funyard")); + test.equal(data_buf, [{x:'ulstuff'}]); + div.kill(); + Meteor.flush(); +}); + + +Tinytest.add("spark - list event handling", function(test) { + var event_buf = []; + var div; + + // same thing, but with events wired by listChunk "added" and "removed" + event_buf.length = 0; + var lst = []; + lst.observe = function(callbacks) { + lst.callbacks = callbacks; + return { + stop: function() { + lst.callbacks = null; + } + }; + }; + div = OnscreenDiv(Meteor.render(function() { + var chkbx = function(doc) { + return ''+(doc ? doc._id : 'else'); + }; + var html = '

      ' + + Spark.setDataContext( + event_buf, Spark.attachEvents( + eventmap('click input', event_buf), Spark.list(lst, chkbx, chkbx))) + + '

      '; + html = Spark.setDataContext(event_buf, html); + html = Spark.attachEvents(eventmap('change b', 'change input', event_buf), + html); + return html; + })); + Meteor.flush(); + test.equal(div.text().match(/\S+/)[0], 'else'); + // click on input + var doClick = function() { + clickElement(div.node().getElementsByTagName('input')[0]); + event_buf.sort(); // don't care about order + test.equal(event_buf, ['change b', 'change input', 'click input']); + event_buf.length = 0; + }; + doClick(); + // add item + lst.push({_id:'foo'}); + lst.callbacks.added(lst[0], 0); + Meteor.flush(); + test.equal(div.text().match(/\S+/)[0], 'foo'); + doClick(); + // remove item, back to "else" case + lst.callbacks.removed(lst[0], 0); + lst.pop(); + Meteor.flush(); + test.equal(div.text().match(/\S+/)[0], 'else'); + doClick(); + // cleanup + div.kill(); + Meteor.flush(); + +}); + + +Tinytest.add("spark - basic landmarks", function (test) { + var R = ReactiveVar("111"); + var x = []; + var expect = function (what) { + test.equal(x, what); + x = []; + }; + + var X = {}; + + var div = OnscreenDiv(Spark.render(function () { + return Spark.isolate(function () { + return R.get() + + Spark.createLandmark({ + created: function () { + x.push("c"); + this.a = X; + }, + rendered: function () { + x.push("r", this.a); + }, + destroyed: function () { + x.push("d", this.a); + } + }, function() { return "hi"; }); + }); + })); + + expect(["c"]); + Meteor.flush(); + expect(["r", X]); + Meteor.flush(); + expect([]); + R.set("222"); + expect([]); + Meteor.flush(); + expect(["r", X]); + Meteor.flush(); + expect([]); + div.remove(); + expect([]); + Meteor.flush(); + expect([]); + div.kill(); + Meteor.flush(); + expect(["d", X]); +}); + +Tinytest.add("spark - labeled landmarks", function (test) { + var R = []; + for (var i = 0; i < 10; i++) + R.push(ReactiveVar("")); + + var x = []; + var s = []; + var expect = function (what_x, what_s) { + test.equal(x, what_x); + test.equal(s, what_s); + x = []; + s = []; + }; + + var excludeLandmarks = []; + for (var i = 0; i < 6; i++) + excludeLandmarks.push(ReactiveVar(false)); + + var isolateLandmarks = ReactiveVar(false); + var serial = 1; + var testLandmark = function (id, htmlFunc) { + if (excludeLandmarks[id].get()) + return ""; + + var f = function () { + var thisSerial = serial++; + + return Spark.createLandmark({ + created: function () { + x.push("c", id); + s.push(thisSerial); + this.id = id; + }, + rendered: function () { + x.push("r", id); + s.push(thisSerial); + test.equal(this.id, id); + }, + destroyed: function () { + x.push("d", id); + s.push(thisSerial); + test.equal(this.id, id); + } + }, htmlFunc); + }; + + if (isolateLandmarks.get()) + return Spark.isolate(function () { return f(); }); + else + return f(); + }; + + var label = Spark.labelBranch; + + var dep = function (i) { + return R[i].get(); + }; + + // this frog is pretty well boiled + var div = OnscreenDiv(Spark.render(function () { + var html = Spark.isolate(function () { + return ( + dep(0) + + testLandmark(1, function () {return "hi" + dep(1); }) + + label("a", function () { + return dep(2) + + testLandmark(2, function () { return "hi" + dep(3);});}) + + label("b", function () { + return dep(4) + + testLandmark(3, function () { + return "hi" + dep(5) + label("c", function () { + return dep(6) + + testLandmark(4, function () { + return "hi" + dep(7) + + label("d", function () { + return label("e", function () { + return dep(8) + + label("f", function () { + return testLandmark( + 5, function () { return "hi" + dep(9);} + );});});});});});});})); + }); + return html; + })); + + // callback order is not specced + expect(["c", 1, "c", 2, "c", 3, "c", 4, "c", 5], [1, 2, 3, 4, 5]); + Meteor.flush(); + expect(["r", 1, "r", 2, "r", 5, "r", 4, "r", 3], [1, 2, 5, 4, 3]); + for (var i = 0; i < 10; i++) { + R[i].set(1); + expect([], []); + Meteor.flush(); + expect(["r", 1, "r", 2, "r", 5, "r", 4, "r", 3], + [i*5 + 6, i*5 + 7, i*5 + 10, i*5 + 9, i*5 + 8]); + }; + + excludeLandmarks[2].set(true); + expect([], []); + Meteor.flush(); + expect(["d", 2, "r", 1, "r", 5, "r", 4, "r", 3], + [52, 56, 59, 58, 57]); + + excludeLandmarks[2].set(false); + excludeLandmarks[3].set(true); + expect([], []); + Meteor.flush(); + expect(["c", 2, "d", 3, "d", 4, "d", 5, "r", 1, "r", 2], + [61, 57, 58, 59, 60, 61]); + + excludeLandmarks[2].set(true); + excludeLandmarks[3].set(false); + expect([], []); + Meteor.flush(); + expect(["c", 3, "c", 4, "c", 5, "d", 2, "r", 1, "r", 5, "r", 4, "r", 3], + [63, 64, 65, 61, 62, 65, 64, 63]); + + excludeLandmarks[2].set(false); + expect([], []); + Meteor.flush(); + expect(["c", 2, "r", 1, "r", 2, "r", 5, "r", 4, "r", 3], + [67, 66, 67, 70, 69, 68]); + + isolateLandmarks.set(true); + expect([], []); + Meteor.flush(); + expect(["r", 1, "r", 2, "r", 5, "r", 4, "r", 3], + [71, 72, 75, 74, 73]); + + for (var i = 0; i < 10; i++) { + var expected = [ + [["r", 1, "r", 2, "r", 5, "r", 4, "r", 3], [76, 77, 80, 79, 78]], + [["r", 1], [81]], + [["r", 1, "r", 2, "r", 5, "r", 4, "r", 3], [82, 83, 86, 85, 84]], + [["r", 2], [87]], + [["r", 1, "r", 2, "r", 5, "r", 4, "r", 3], [88, 89, 92, 91, 90]], + [["r", 5, "r", 4, "r", 3], [95, 94, 93]], + [["r", 5, "r", 4, "r", 3], [98, 97, 96]], + [["r", 5, "r", 4, "r", 3], [100, 99, 96]], + [["r", 5, "r", 4, "r", 3], [102, 101, 96]], + [["r", 5, "r", 4, "r", 3], [103, 101, 96]] + ][i]; + R[i].set(2); + expect([], []); + Meteor.flush(); + expect.apply(null, expected); + }; + + excludeLandmarks[4].set(true); + Meteor.flush(); + expect(["d", 4, "d", 5, "r", 3], [101, 103, 104]); + + excludeLandmarks[4].set(false); + excludeLandmarks[5].set(true); + Meteor.flush(); + expect(["c", 4, "r", 4, "r", 3], [106, 106, 105]); + + excludeLandmarks[5].set(false); + Meteor.flush(); + expect(["c", 5, "r", 5, "r", 4, "r", 3], [108, 108, 107, 105]); + + div.kill(); + Meteor.flush(); +}); + + +Tinytest.add("spark - preserve copies attributes", function(test) { + // make sure attributes are correctly changed (i.e. copied) + // when preserving old nodes, either because they are labeled + // or because they are a parent of a labeled node. + + var R1 = ReactiveVar("foo"); + var R2 = ReactiveVar("abcd"); + + var frag = WrappedFrag(renderWithLegacyLabels(function() { + return '
      '; + })).hold(); + var node1 = frag.node().firstChild; + var node2 = frag.node().firstChild.getElementsByTagName("input")[0]; + test.equal(node1.nodeName, "DIV"); + test.equal(node2.nodeName, "INPUT"); + test.equal(node1.getAttribute("puppy"), "foo"); + test.equal(node2.getAttribute("kittycat"), "abcd"); + + R1.set("bar"); + R2.set("efgh"); + Meteor.flush(); + test.equal(node1.getAttribute("puppy"), "bar"); + test.equal(node2.getAttribute("kittycat"), "efgh"); + + frag.release(); + Meteor.flush(); + test.equal(R1.numListeners(), 0); + test.equal(R2.numListeners(), 0); + + var R; + R = ReactiveVar(false); + frag = WrappedFrag(renderWithLegacyLabels(function() { + return ''; + })).hold(); + var get_checked = function() { return !! frag.node().firstChild.checked; }; + test.equal(get_checked(), false); + Meteor.flush(); + test.equal(get_checked(), false); + R.set(true); + test.equal(get_checked(), false); + Meteor.flush(); + test.equal(get_checked(), true); + R.set(false); + test.equal(get_checked(), true); + Meteor.flush(); + test.equal(get_checked(), false); + R.set(true); + Meteor.flush(); + test.equal(get_checked(), true); + frag.release(); + R = ReactiveVar(true); + frag = WrappedFrag(renderWithLegacyLabels(function() { + return ''; + })).hold(); + test.equal(get_checked(), true); + Meteor.flush(); + test.equal(get_checked(), true); + R.set(false); + test.equal(get_checked(), true); + Meteor.flush(); + test.equal(get_checked(), false); + frag.release(); + + + _.each([false, true], function(with_focus) { + R = ReactiveVar("apple"); + var div = OnscreenDiv(renderWithLegacyLabels(function() { + return ''; + })); + var maybe_focus = function(div) { + if (with_focus) { + div.show(); + focusElement(div.node().firstChild); + } + }; + maybe_focus(div); + var get_value = function() { return div.node().firstChild.value; }; + var set_value = function(v) { div.node().firstChild.value = v; }; + var if_blurred = function(v, v2) { + return with_focus ? v2 : v; }; + test.equal(get_value(), "apple"); + Meteor.flush(); + test.equal(get_value(), "apple"); + R.set(""); + test.equal(get_value(), "apple"); + Meteor.flush(); + test.equal(get_value(), if_blurred("", "apple")); + R.set("pear"); + test.equal(get_value(), if_blurred("", "apple")); + Meteor.flush(); + test.equal(get_value(), if_blurred("pear", "apple")); + set_value("jerry"); // like user typing + R.set("steve"); + Meteor.flush(); + // should overwrite user typing if blurred + test.equal(get_value(), if_blurred("steve", "jerry")); + div.kill(); + R = ReactiveVar(""); + div = OnscreenDiv(renderWithLegacyLabels(function() { + return ''; + })); + maybe_focus(div); + test.equal(get_value(), ""); + Meteor.flush(); + test.equal(get_value(), ""); + R.set("tom"); + test.equal(get_value(), ""); + Meteor.flush(); + test.equal(get_value(), if_blurred("tom", "")); + div.kill(); + Meteor.flush(); + }); +}); + +Tinytest.add("spark - bad labels", function(test) { + // make sure patching behaves gracefully even when labels violate + // the rules that would allow preservation of nodes identity. + + var go = function(html1, html2) { + var R = ReactiveVar(true); + var frag = WrappedFrag(renderWithLegacyLabels(function() { + return R.get() ? html1 : html2; + })).hold(); + + R.set(false); + Meteor.flush(); + test.equal(frag.html(), html2); + frag.release(); + }; + + go('hello', 'world'); + + // duplicate IDs (bad developer; but should patch correctly) + go('
      hello
      world', + '
      hi
      there'); + go('
      hello
      ', + '
      hi
      '); + go('
      hello
      world', + '
      hi
      '); + + // tag name changes + go('
      abcd
      ', + '

      efgh

      '); + + // parent chain changes at all + go('

      test123

      ', + '

      test123

      '); + go('

      test123

      ', + '

      test123

      '); + + // ambiguous names + go('
      • 1
      • 3
      • 3
      ', + '
      • 4
      • 5
      '); +}); + + +Tinytest.add("spark - landmark patching", function(test) { + + var rand; + + var randomNodeList = function(optParentTag, depth) { + var atTopLevel = ! optParentTag; + var len = rand.nextIntBetween(atTopLevel ? 1 : 0, 6); + var buf = []; + for(var i=0; i'); + nodeListToHtml(n.children, is_after, buf); + buf.push(''); + } + } + }); + return optBuf ? null : buf.join(''); + }; + + var fillInElementIdentities = function(list, parent, is_after) { + var elementsInList = _.filter( + list, + function(x) { + return (is_after ? x.existsAfter : x.existsBefore) && x.tagName; + }); + var elementsInDom = _.filter(parent.childNodes, + function(x) { return x.nodeType === 1; }); + test.equal(elementsInList.length, elementsInDom.length); + for(var i=0; i"); + fillInElementIdentities(structure, frag.node()); + var labeledNodes = collectLabeledNodeData(structure); + R.set(true); + Meteor.flush(); + test.equal(frag.html(), nodeListToHtml(structure, true) || ""); + _.each(labeledNodes, function(x) { + test.isTrue(isSameElements(x.parents, getParentChain(x.node))); + }); + + frag.release(); + Meteor.flush(); + test.equal(R.numListeners(), 0); + } + +}); + +Tinytest.add("spark - landmark constant", function(test) { + + var R, div; + + // top-level { constant: true } + + R = ReactiveVar(0); + var states = []; + div = OnscreenDiv(Meteor.render(function() { + R.get(); // create dependency + return Spark.createLandmark({ + constant: true, + rendered: function() { + states.push(this); + } + }, function() { return ''; }); + })); + + var nodes = _.toArray(div.node().childNodes); + test.equal(nodes.length, 3); + Meteor.flush(); + test.equal(states.length, 1); + R.set(1); + Meteor.flush(); + test.equal(states.length, 1); // no render callback on constant + var nodes2 = _.toArray(div.node().childNodes); + test.equal(nodes2.length, 3); + test.isTrue(nodes[0] === nodes2[0]); + test.isTrue(nodes[1] === nodes2[1]); + test.isTrue(nodes[2] === nodes2[2]); + div.kill(); + Meteor.flush(); + test.equal(R.numListeners(), 0); + + // non-top-level + + var i = 1; + // run test with and without matching branch label + _.each([false, true], function(matchLandmark) { + // run test with node before or after, or neither or both + _.each([false, true], function(nodeBefore) { + _.each([false, true], function(nodeAfter) { + var hasSpan = true; + var isConstant = true; + + var crd = null; // [createCount, renderCount, destroyCount] + + R = ReactiveVar('foo'); + div = OnscreenDiv(Meteor.render(function() { + R.get(); // create unconditional dependency + var brnch = matchLandmark ? 'myBranch' : ('branch'+(++i)); + return (nodeBefore ? R.get() : '') + + Spark.labelBranch( + brnch, function () { + return Spark.createLandmark( + { + constant: isConstant, + created: function () { + this.crd = [0,0,0]; + if (! crd) + crd = this.crd; // capture first landmark's crd + this.crd[0]++; + }, + rendered: function () { this.crd[1]++; }, + destroyed: function () { this.crd[2]++; } + }, + function() { return hasSpan ? + 'stuff' : 'blah'; });}) + + (nodeAfter ? R.get() : ''); + })); + + var span = div.node().getElementsByTagName('span')[0]; + hasSpan = false; + + test.equal(div.text(), + (nodeBefore ? 'foo' : '')+ + 'stuff'+ + (nodeAfter ? 'foo' : '')); + + R.set('bar'); + Meteor.flush(); + + // only non-matching landmark should cause the constant + // chunk to be re-rendered + test.equal(div.text(), + (nodeBefore ? 'bar' : '')+ + (matchLandmark ? 'stuff' : 'blah')+ + (nodeAfter ? 'bar' : '')); + // in non-matching case, first landmark is destroyed. + // otherwise, it is kept (and not re-rendered because + // it is constant) + test.equal(crd, matchLandmark ? [1,1,0] : [1,1,1]); + + R.set('baz'); + Meteor.flush(); + + // should be repeatable (liveranges not damaged) + test.equal(div.text(), + (nodeBefore ? 'baz' : '')+ + (matchLandmark ? 'stuff' : 'blah')+ + (nodeAfter ? 'baz' : '')); + + isConstant = false; // no longer constant:true! + R.set('qux'); + Meteor.flush(); + test.equal(div.text(), + (nodeBefore ? 'qux' : '')+ + 'blah'+ + (nodeAfter ? 'qux' : '')); + + // turn constant back on + isConstant = true; + hasSpan = true; + R.set('popsicle'); + Meteor.flush(); + // we don't get the span, instead old "blah" is preserved + test.equal(div.text(), + (nodeBefore ? 'popsicle' : '')+ + (matchLandmark ? 'blah' : 'stuff')+ + (nodeAfter ? 'popsicle' : '')); + + isConstant = false; + R.set('hi'); + Meteor.flush(); + // now we get the span! + test.equal(div.text(), + (nodeBefore ? 'hi' : '')+ + 'stuff'+ + (nodeAfter ? 'hi' : '')); + + div.kill(); + Meteor.flush(); + }); + }); + }); + + // test that constant landmark gets rendered callback if it + // wasn't preserved. + + var renderCount; + + renderCount = 0; + R = ReactiveVar('div'); + div = OnscreenDiv(Meteor.render(function () { + return '<' + R.get() + '>' + Spark.createLandmark( + {constant: true, rendered: function () { renderCount++; }}, + function () { + return "hi"; + }) + + ''; + })); + Meteor.flush(); + test.equal(renderCount, 1); + + R.set('div class="hamburger"'); + Meteor.flush(); + // constant patched around, not re-rendered! + test.equal(renderCount, 1); + + R.set('span class="hamburger"'); + Meteor.flush(); + // can't patch parent to a different tag + test.equal(renderCount, 2); + + R.set('span'); + Meteor.flush(); + // can patch here, renderCount stays the same + test.equal(renderCount, 2); + + div.kill(); + Meteor.flush(); +}); + + +Tinytest.add("spark - leaderboard", function(test) { + // use a simplified, local leaderboard to test some stuff + + var players = new LocalCollection(); + var selected_player = ReactiveVar(); + + var scores = OnscreenDiv(renderWithLegacyLabels(function() { + var html = Spark.list( + players.find({}, {sort: {score: -1}}), + function(player) { + return Spark.labelBranch(player._id, function () { + return Spark.isolate(function () { + var style; + if (selected_player.get() === player._id) + style = "player selected"; + else + style = "player"; + + var html = '
      ' + + '
      ' + player.name + '
      ' + + '
      ' + player.score + '
      '; + html = Spark.setDataContext(player, html); + html = Spark.createLandmark( + {preserve: legacyLabels}, + function() { return html; }); + return html; + }); + }); + }); + html = Spark.attachEvents({ + "click": function () { + selected_player.set(this._id); + } + }, html); + return html; + })); + + // back before we had scientists we had Vancian hussade players + var names = ["Glinnes Hulden", "Shira Hulden", "Denzel Warhound", + "Lute Casagave", "Akadie", "Thammas, Lord Gensifer", + "Ervil Savat", "Duissane Trevanyi", "Sagmondo Bandolio", + "Rhyl Shermatz", "Yalden Wirp", "Tyran Lucho", + "Bump Candolf", "Wilmer Guff", "Carbo Gilweg"]; + for (var i = 0; i < names.length; i++) + players.insert({name: names[i], score: i*5}); + + var bump = function() { + players.update(selected_player.get(), {$inc: {score: 5}}); + }; + + var findPlayerNameDiv = function(name) { + var divs = scores.node().getElementsByTagName('DIV'); + return _.find(divs, function(div) { + return div.innerHTML === name; + }); + }; + + Meteor.flush(); + var glinnesNameNode = findPlayerNameDiv(names[0]); + test.isTrue(!! glinnesNameNode); + var glinnesScoreNode = glinnesNameNode.nextSibling; + test.equal(glinnesScoreNode.getAttribute("name"), "score"); + clickElement(glinnesNameNode); + Meteor.flush(); + glinnesNameNode = findPlayerNameDiv(names[0]); + test.isTrue(!! glinnesNameNode); + test.equal(glinnesNameNode.parentNode.className, 'player selected'); + var glinnesId = players.findOne({name: names[0]})._id; + test.isTrue(!! glinnesId); + test.equal(selected_player.get(), glinnesId); + test.equal( + canonicalizeHtml(glinnesNameNode.parentNode.innerHTML), + '
      Glinnes Hulden
      0
      '); + + bump(); + Meteor.flush(); + + glinnesNameNode = findPlayerNameDiv(names[0], glinnesNameNode); + var glinnesScoreNode2 = glinnesNameNode.nextSibling; + test.equal(glinnesScoreNode2.getAttribute("name"), "score"); + // move and patch should leave score node the same, because it + // has a name attribute! + test.equal(glinnesScoreNode, glinnesScoreNode2); + test.equal(glinnesNameNode.parentNode.className, 'player selected'); + test.equal( + canonicalizeHtml(glinnesNameNode.parentNode.innerHTML), + '
      Glinnes Hulden
      5
      '); + + bump(); + Meteor.flush(); + + glinnesNameNode = findPlayerNameDiv(names[0], glinnesNameNode); + test.equal( + canonicalizeHtml(glinnesNameNode.parentNode.innerHTML), + '
      Glinnes Hulden
      10
      '); + + scores.kill(); + Meteor.flush(); + test.equal(selected_player.numListeners(), 0); +}); + + +Tinytest.add("spark - list cursor stop", function(test) { + // test Spark.list outside of render mode, on custom observable + + var numHandles = 0; + var observable = { + observe: function(x) { + x.added({_id:"123"}, 0); + x.added({_id:"456"}, 1); + var handle; + numHandles++; + return handle = { + stop: function() { + numHandles--; + } + }; + } + }; + + test.equal(numHandles, 0); + var result = Spark.list(observable, function(doc) { + return "#"+doc._id; + }); + test.equal(result, "#123#456"); + Meteor.flush(); + // chunk killed because not created inside Spark.render + test.equal(numHandles, 0); + + + var R = ReactiveVar(1); + var frag = WrappedFrag(Meteor.render(function() { + if (R.get() > 0) + return Spark.list(observable, function() { return "*"; }); + return ""; + })).hold(); + test.equal(numHandles, 1); + Meteor.flush(); + test.equal(numHandles, 1); + R.set(2); + Meteor.flush(); + test.equal(numHandles, 1); + R.set(-1); + Meteor.flush(); + test.equal(numHandles, 0); + + frag.release(); + Meteor.flush(); +}); + +Tinytest.add("spark - list table", function(test) { + var c = new LocalCollection(); + + c.insert({value: "fudge", order: "A"}); + c.insert({value: "sundae", order: "B"}); + + var R = ReactiveVar(); + + var table = WrappedFrag(Meteor.render(function() { + var buf = []; + buf.push(''); + buf.push(Spark.list( + c.find({}, {sort: ['order']}), + function(doc) { + return Spark.labelBranch(doc._id, function () { + return Spark.isolate(function () { + var html = ""; + html = Spark.createLandmark( + {preserve: legacyLabels}, + function() { return html; }); + return html; + }); + }); + }, + function() { + return ""; + })); + buf.push('
      "+doc.value + (doc.reactive ? R.get() : '')+ + "
      (nothing)
      '); + return buf.join(''); + })).hold(); + + var lastHtml; + + var shouldFlushTo = function(html) { + // same before flush + test.equal(table.html(), lastHtml); + Meteor.flush(); + test.equal(table.html(), html); + lastHtml = html; + }; + var tableOf = function(/*htmls*/) { + if (arguments.length === 0) { + return '
      '; + } else { + return '
      ' + + _.toArray(arguments).join('
      ') + + '
      '; + } + }; + + test.equal(table.html(), lastHtml = tableOf('fudge', 'sundae')); + + // switch order + c.update({value: "fudge"}, {$set: {order: "BA"}}); + shouldFlushTo(tableOf('sundae', 'fudge')); + + // change text + c.update({value: "fudge"}, {$set: {value: "hello"}}); + c.update({value: "sundae"}, {$set: {value: "world"}}); + shouldFlushTo(tableOf('world', 'hello')); + + // remove all + c.remove({}); + shouldFlushTo(tableOf('(nothing)')); + + c.insert({value: "1", order: "A"}); + c.insert({value: "5", order: "B"}); + c.insert({value: "3", order: "AB"}); + c.insert({value: "7", order: "BB"}); + c.insert({value: "2", order: "AA"}); + c.insert({value: "4", order: "ABA"}); + c.insert({value: "6", order: "BA"}); + c.insert({value: "8", order: "BBA"}); + shouldFlushTo(tableOf('1', '2', '3', '4', '5', '6', '7', '8')); + + // make one item newly reactive + R.set('*'); + c.update({value: "7"}, {$set: {reactive: true}}); + shouldFlushTo(tableOf('1', '2', '3', '4', '5', '6', '7*', '8')); + + R.set('!'); + shouldFlushTo(tableOf('1', '2', '3', '4', '5', '6', '7!', '8')); + + // move it + c.update({value: "7"}, {$set: {order: "A0"}}); + shouldFlushTo(tableOf('1', '7!', '2', '3', '4', '5', '6', '8')); + + // still reactive? + R.set('?'); + shouldFlushTo(tableOf('1', '7?', '2', '3', '4', '5', '6', '8')); + + // go nuts + c.update({value: '1'}, {$set: {reactive: true}}); + c.update({value: '1'}, {$set: {reactive: false}}); + c.update({value: '2'}, {$set: {reactive: true}}); + c.update({value: '2'}, {$set: {order: "BBB"}}); + R.set(';'); + R.set('.'); + shouldFlushTo(tableOf('1', '7.', '3', '4', '5', '6', '8', '2.')); + + for(var i=1; i<=8; i++) + c.update({value: String(i)}, + {$set: {reactive: true, value: '='+String(i)}}); + R.set('!'); + shouldFlushTo(tableOf('=1!', '=7!', '=3!', '=4!', '=5!', '=6!', '=8!', '=2!')); + + for(var i=1; i<=8; i++) + c.update({value: '='+String(i)}, + {$set: {order: "A"+i}}); + shouldFlushTo(tableOf('=1!', '=2!', '=3!', '=4!', '=5!', '=6!', '=7!', '=8!')); + + var valueFunc = function(n) { return ''+n+''; }; + for(var i=1; i<=8; i++) + c.update({value: '='+String(i)}, + {$set: {value: valueFunc(i)}}); + shouldFlushTo(tableOf.apply( + null, + _.map(_.range(1,9), function(n) { return valueFunc(n)+R.get(); }))); + + test.equal(table.node().firstChild.rows.length, 8); + + var bolds = table.node().firstChild.getElementsByTagName('B'); + test.equal(bolds.length, 8); + _.each(bolds, function(b) { + b.nifty = {}; // mark the nodes; non-primitive value won't appear in IE HTML + }); + + R.set('...'); + shouldFlushTo(tableOf.apply( + null, + _.map(_.range(1,9), function(n) { return valueFunc(n)+R.get(); }))); + var bolds2 = table.node().firstChild.getElementsByTagName('B'); + test.equal(bolds2.length, 8); + // make sure patching is actually happening + _.each(bolds2, function(b) { + test.equal(!! b.nifty, true); + }); + + // change value func, and still we should be patching + var valueFunc2 = function(n) { return ''+n+'yeah'; }; + for(var i=1; i<=8; i++) + c.update({value: valueFunc(i)}, + {$set: {value: valueFunc2(i)}}); + shouldFlushTo(tableOf.apply( + null, + _.map(_.range(1,9), function(n) { return valueFunc2(n)+R.get(); }))); + var bolds3 = table.node().firstChild.getElementsByTagName('B'); + test.equal(bolds3.length, 8); + _.each(bolds3, function(b) { + test.equal(!! b.nifty, true); + }); + + table.release(); + +}); + + +Tinytest.add("spark - list event data", function(test) { + // this is based on a bug + + var lastClicked = null; + var R = ReactiveVar(0); + var later; + var div = OnscreenDiv(Meteor.render(function() { + var html = Spark.list( + { + observe: function(observer) { + observer.added({_id: '1', name: 'Foo'}, 0); + observer.added({_id: '2', name: 'Bar'}, 1); + // exercise callback path + later = function() { + observer.added({_id: '3', name: 'Baz'}, 2); + observer.added({_id: '4', name: 'Qux'}, 3); + }; + return { stop: function() {} }; + } + }, + function(doc) { + var html = Spark.isolate(function () { + R.get(); // depend on R + return '
      ' + doc.name + '
      '; + }); + html = Spark.setDataContext(doc, html); + return html; + } + ); + html = Spark.attachEvents({ + 'click': function (event) { + lastClicked = this.name; + R.set(R.get() + 1); // signal all dependers on R + } + }, html); + return html; + })); + + var item = function(name) { + return _.find(div.node().getElementsByTagName('div'), function(d) { + return d.innerHTML === name; }); + }; + + later(); + Meteor.flush(); + test.equal(item("Foo").innerHTML, "Foo"); + test.equal(item("Bar").innerHTML, "Bar"); + test.equal(item("Baz").innerHTML, "Baz"); + test.equal(item("Qux").innerHTML, "Qux"); + + var doClick = function(name) { + clickElement(item(name)); + test.equal(lastClicked, name); + Meteor.flush(); + }; + + doClick("Foo"); + doClick("Bar"); + doClick("Baz"); + doClick("Qux"); + doClick("Bar"); + doClick("Foo"); + doClick("Foo"); + doClick("Foo"); + doClick("Qux"); + doClick("Baz"); + doClick("Baz"); + doClick("Baz"); + doClick("Bar"); + doClick("Baz"); + doClick("Foo"); + doClick("Qux"); + doClick("Foo"); + + div.kill(); + Meteor.flush(); + +}); + + +Tinytest.add("spark - events on preserved nodes", function(test) { + var count = ReactiveVar(0); + var demo = OnscreenDiv(renderWithLegacyLabels(function() { + var html = Spark.isolate(function () { + return '
      '+ + ''+ + '
      The button has been pressed '+count.get()+' times.
      '+ + '
      '; + }); + html = Spark.attachEvents({ + 'click input': function() { + count.set(count.get() + 1); + } + }, html); + return html; + })); + + var click = function() { + clickElement(demo.node().getElementsByTagName('input')[0]); + }; + + test.equal(count.get(), 0); + for(var i=0; i<10; i++) { + click(); + Meteor.flush(); + test.equal(count.get(), i+1); + } + + demo.kill(); + Meteor.flush(); +}); + + +Tinytest.add("spark - cleanup", function(test) { + + // more exhaustive clean-up testing + var stuff = new LocalCollection(); + + var add_doc = function() { + stuff.insert({foo:'bar'}); }; + var clear_docs = function() { + stuff.remove({}); }; + var remove_one = function() { + stuff.remove(stuff.findOne()._id); }; + + add_doc(); // start the collection with a doc + + var R = ReactiveVar("x"); + + var div = OnscreenDiv(Spark.render(function() { + return Spark.list( + stuff.find(), + function() { + return Spark.isolate(function () { return R.get()+"1"; }); + }, + function() { + return Spark.isolate(function () { return R.get()+"0"; }); + }); + })); + + test.equal(div.text(), "x1"); + Meteor.flush(); + test.equal(div.text(), "x1"); + test.equal(R.numListeners(), 1); + + clear_docs(); + Meteor.flush(); + test.equal(div.text(), "x0"); + test.equal(R.numListeners(), 1); // test clean-up of doc on remove + + add_doc(); + Meteor.flush(); + test.equal(div.text(), "x1"); + test.equal(R.numListeners(), 1); // test clean-up of "else" listeners + + add_doc(); + Meteor.flush(); + test.equal(div.text(), "x1x1"); + test.equal(R.numListeners(), 2); + + remove_one(); + Meteor.flush(); + test.equal(div.text(), "x1"); + test.equal(R.numListeners(), 1); // test clean-up of doc with other docs + + div.kill(); + Meteor.flush(); + test.equal(R.numListeners(), 0); + + //// list stopped if not materialized + + var observeCount = 0; + var stopCount = 0; + var cursor = { + observe: function (callbacks) { + observeCount++; + return { + stop: function () { + stopCount++; + } + }; + } + }; + + div = OnscreenDiv(Spark.render(function () { + var html = Spark.list(cursor, + function () { return ''; }); + // don't return html + return 'hi'; + })); + // we expect that the implementation of Spark.list observed the + // cursor in order to generate HTML, and then stopped it when + // it saw that the annotation wasn't materialized. Other acceptable + // implementations of Spark.list might avoid observing the cursor + // altogether, resulting in [0, 0], or might defer the stopping to + // flush time. + test.equal([observeCount, stopCount], [1, 1]); + + div.kill(); + Meteor.flush(); +}); + + +var make_input_tester = function(render_func, events) { + var buf = []; + + if (typeof render_func === "string") { + var render_str = render_func; + render_func = function() { return render_str; }; + } + if (typeof events === "string") { + events = eventmap.apply(null, _.toArray(arguments).slice(1)); + } + + var R = ReactiveVar(0); + var div = OnscreenDiv( + renderWithLegacyLabels(function() { + R.get(); // create dependency + var html = render_func(); + html = Spark.attachEvents(events, html); + html = Spark.setDataContext(buf, html); + return html; + })); + div.show(true); + + var getbuf = function() { + var ret = buf.slice(); + buf.length = 0; + return ret; + }; + + var self; + return self = { + focus: function(optCallback) { + focusElement(self.inputNode()); + + if (optCallback) + Meteor.defer(function() { optCallback(getbuf()); }); + else + return getbuf(); + }, + blur: function(optCallback) { + blurElement(self.inputNode()); + + if (optCallback) + Meteor.defer(function() { optCallback(getbuf()); }); + else + return getbuf(); + }, + click: function() { + clickElement(self.inputNode()); + return getbuf(); + }, + kill: function() { + // clean up + div.kill(); + Meteor.flush(); + }, + inputNode: function() { + return div.node().getElementsByTagName("input")[0]; + }, + redraw: function() { + R.set(R.get() + 1); + Meteor.flush(); + } + }; +}; + +// Note: These tests MAY FAIL if the browser window doesn't have focus +// (isn't frontmost) in some browsers, particularly Firefox. +testAsyncMulti("spark - focus/blur events", + (function() { + + var textLevel1 = ''; + var textLevel2 = ''; + + var focus_test = function(render_func, events, expected_results) { + return function(test, expect) { + var tester = make_input_tester(render_func, events); + var callback = expect(expected_results); + tester.focus(function(buf) { + tester.kill(); + callback(buf); + }); + }; + }; + + var blur_test = function(render_func, events, expected_results) { + return function(test, expect) { + var tester = make_input_tester(render_func, events); + var callback = expect(expected_results); + tester.focus(); + tester.blur(function(buf) { + tester.kill(); + callback(buf); + }); + }; + }; + + return [ + + // focus on top-level input + focus_test(textLevel1, 'focus input', ['focus input']), + + // focus on second-level input + // issue #108 + focus_test(textLevel2, 'focus input', ['focus input']), + + // focusin + focus_test(textLevel1, 'focusin input', ['focusin input']), + focus_test(textLevel2, 'focusin input', ['focusin input']), + + // focusin bubbles + focus_test(textLevel2, 'focusin span', ['focusin span']), + + // focus doesn't bubble + focus_test(textLevel2, 'focus span', []), + + // blur works, doesn't bubble + blur_test(textLevel1, 'blur input', ['blur input']), + blur_test(textLevel2, 'blur input', ['blur input']), + blur_test(textLevel2, 'blur span', []), + + // focusout works, bubbles + blur_test(textLevel1, 'focusout input', ['focusout input']), + blur_test(textLevel2, 'focusout input', ['focusout input']), + blur_test(textLevel2, 'focusout span', ['focusout span']) + ]; + })()); + + +Tinytest.add("spark - change events", function(test) { + + var checkboxLevel1 = ''; + var checkboxLevel2 = ''+ + ''; + + + // on top-level + var checkbox1 = make_input_tester(checkboxLevel1, 'change input'); + test.equal(checkbox1.click(), ['change input']); + checkbox1.kill(); + + // on second-level (should bubble) + var checkbox2 = make_input_tester(checkboxLevel2, + 'change input', 'change span'); + test.equal(checkbox2.click(), ['change input', 'change span']); + test.equal(checkbox2.click(), ['change input', 'change span']); + checkbox2.redraw(); + test.equal(checkbox2.click(), ['change input', 'change span']); + checkbox2.kill(); + + checkbox2 = make_input_tester(checkboxLevel2, 'change input'); + test.equal(checkbox2.focus(), []); + test.equal(checkbox2.click(), ['change input']); + test.equal(checkbox2.blur(), []); + test.equal(checkbox2.click(), ['change input']); + checkbox2.kill(); + + var checkbox2 = make_input_tester( + checkboxLevel2, + 'change input', 'change span', 'change div'); + test.equal(checkbox2.click(), ['change input', 'change span']); + checkbox2.kill(); + +}); + + +testAsyncMulti( + "spark - submit events", + (function() { + var hitlist = []; + var killLater = function(thing) { + hitlist.push(thing); + }; + + var LIVEUI_TEST_RESPONDER = "/spark_test_responder"; + var IFRAME_URL_1 = LIVEUI_TEST_RESPONDER + "/"; + var IFRAME_URL_2 = "about:blank"; // most cross-browser-compatible + if (window.opera) // opera doesn't like 'about:blank' form target + IFRAME_URL_2 = LIVEUI_TEST_RESPONDER+"/blank"; + + return [ + function(test, expect) { + + // Submit events can be canceled with preventDefault, which prevents the + // browser's native form submission behavior. This behavior takes some + // work to ensure cross-browser, so we want to test it. To detect + // a form submission, we target the form at an iframe. Iframe security + // makes this tricky. What we do is load a page from the server that + // calls us back on 'load' and 'unload'. We wait for 'load', set up the + // test, and then see if we get an 'unload' (due to the form submission + // going through) or not. + // + // This is quite a tricky implementation. + + var withIframe = function(onReady1, onReady2) { + var frameName = "submitframe"+String(Math.random()).slice(2); + var iframeDiv = OnscreenDiv( + Meteor.render(function() { + return ''; + })); + var iframe = iframeDiv.node().firstChild; + + iframe.loadFunc = function() { + onReady1(frameName, iframe, iframeDiv); + onReady2(frameName, iframe, iframeDiv); + }; + iframe.unloadFunc = function() { + iframe.DID_CHANGE_PAGE = true; + }; + }; + var expectCheckLater = function(options) { + var check = expect(function(iframe, iframeDiv) { + if (options.shouldSubmit) + test.isTrue(iframe.DID_CHANGE_PAGE); + else + test.isFalse(iframe.DID_CHANGE_PAGE); + + // must do this inside expect() so it happens in time + killLater(iframeDiv); + }); + var checkLater = function(frameName, iframe, iframeDiv) { + Tinytest.setTimeout(function() { + check(iframe, iframeDiv); + }, 500); // wait for frame to unload + }; + return checkLater; + }; + var buttonFormHtml = function(frameName) { + return '
      '+ + '
      '+ + ''+ + '
      '; + }; + + // test that form submission by click fires event, + // and also actually submits + withIframe(function(frameName, iframe) { + var form = make_input_tester( + buttonFormHtml(frameName), 'submit form'); + test.equal(form.click(), ['submit form']); + killLater(form); + }, expectCheckLater({shouldSubmit:true})); + + // submit bubbles up + withIframe(function(frameName, iframe) { + var form = make_input_tester( + buttonFormHtml(frameName), 'submit form', 'submit div'); + test.equal(form.click(), ['submit form', 'submit div']); + killLater(form); + }, expectCheckLater({shouldSubmit:true})); + + // preventDefault works, still bubbles + withIframe(function(frameName, iframe) { + var form = make_input_tester( + buttonFormHtml(frameName), { + 'submit form': function(evt) { + test.equal(evt.type, 'submit'); + test.equal(evt.target.nodeName, 'FORM'); + this.push('submit form'); + evt.preventDefault(); + }, + 'submit div': function(evt) { + test.equal(evt.type, 'submit'); + test.equal(evt.target.nodeName, 'FORM'); + this.push('submit div'); + }, + 'submit a': function(evt) { + this.push('submit a'); + } + } + ); + test.equal(form.click(), ['submit form', 'submit div']); + killLater(form); + }, expectCheckLater({shouldSubmit:false})); + + }, + function(test, expect) { + _.each(hitlist, function(thing) { + thing.kill(); + }); + Meteor.flush(); + } + ]; + })()); + + +Tinytest.add("spark - controls", function(test) { + + // Radio buttons + + var R = ReactiveVar(""); + var change_buf = []; + var div = OnscreenDiv(renderWithLegacyLabels(function() { + var buf = []; + buf.push("Band: "); + _.each(["AM", "FM", "XM"], function(band) { + var checked = (R.get() === band) ? 'checked="checked"' : ''; + buf.push(''); + }); + buf.push(R.get()); + var html = buf.join(''); + + html = Spark.attachEvents({ + 'change input': function(event) { + // IE 7 is known to fire change events on all + // the radio buttons with checked=false, as if + // each button were deselected before selecting + // the new one. (Meteor doesn't normalize this + // behavior.) + // However, browsers are consistent if we are + // getting a checked=true notification. + var btn = event.target; + if (btn.checked) { + var band = btn.value; + change_buf.push(band); + R.set(band); + } + } + }, html); + return html; + })); + + Meteor.flush(); + + // get the three buttons; they should be considered 'labeled' + // by the patcher and not change identities! + var btns = _.toArray(div.node().getElementsByTagName("INPUT")); + + test.equal(_.pluck(btns, 'checked'), [false, false, false]); + test.equal(div.text(), "Band: "); + + clickElement(btns[0]); + test.equal(change_buf, ['AM']); + change_buf.length = 0; + Meteor.flush(); + test.equal(_.pluck(btns, 'checked'), [true, false, false]); + test.equal(div.text(), "Band: AM"); + + clickElement(btns[1]); + test.equal(change_buf, ['FM']); + change_buf.length = 0; + Meteor.flush(); + test.equal(_.pluck(btns, 'checked'), [false, true, false]); + test.equal(div.text(), "Band: FM"); + + clickElement(btns[2]); + test.equal(change_buf, ['XM']); + change_buf.length = 0; + Meteor.flush(); + test.equal(_.pluck(btns, 'checked'), [false, false, true]); + test.equal(div.text(), "Band: XM"); + + clickElement(btns[1]); + test.equal(change_buf, ['FM']); + change_buf.length = 0; + Meteor.flush(); + test.equal(_.pluck(btns, 'checked'), [false, true, false]); + test.equal(div.text(), "Band: FM"); + + div.kill(); + + // Textarea + + R = ReactiveVar({x:"test"}); + div = OnscreenDiv(renderWithLegacyLabels(function() { + return ''; + })); + div.show(true); + + var textarea = div.node().firstChild; + test.equal(textarea.nodeName, "TEXTAREA"); + test.equal(textarea.value, "This is a test"); + + // value updates reactively + R.set({x:"fridge"}); + Meteor.flush(); + test.equal(textarea.value, "This is a fridge"); + + // ...unless focused + focusElement(textarea); + R.set({x:"frog"}); + Meteor.flush(); + test.equal(textarea.value, "This is a fridge"); + + // blurring and re-setting works + blurElement(textarea); + Meteor.flush(); + test.equal(textarea.value, "This is a fridge"); + R.set({x:"frog"}); + Meteor.flush(); + test.equal(textarea.value, "This is a frog"); + + // Setting a value (similar to user typing) should + // not prevent value from being updated reactively. + textarea.value = "foobar"; + R.set({x:"photograph"}); + Meteor.flush(); + test.equal(textarea.value, "This is a photograph"); + + + div.kill(); +}); + +Tinytest.add("spark - oldschool landmark matching", function(test) { + + // basic created / onscreen / offscreen callback flow + // (ported from old chunk-matching API) + + var buf; + var counts; + + var testCallbacks = function(theNum /*, extend opts*/) { + return _.extend.apply(_, [{ + created: function() { + this.num = String(theNum); + var howManyBefore = counts[this.num] || 0; + counts[this.num] = howManyBefore + 1; + for(var i=0;i" + html + "
      "; + }); + })); + + test.equal(buf, ["c0", "c1"]); + Meteor.flush(); + // what order of chunks {0,1} is preferable?? + // should be consistent but I'm not sure what makes most sense. + test.equal(buf, "c0,c1,r1,r0".split(',')); + buf.length = 0; + + R.set("B"); + Meteor.flush(); + test.equal(buf, "r1,r0".split(',')); + buf.length = 0; + + div.kill(); + Meteor.flush(); + buf.sort(); + test.equal(buf, "d0,d1".split(',')); +}); + + +Tinytest.add("spark - oldschool branch keys", function(test) { + + var R, div; + + // Re-rendered Meteor.render keeps same landmark state + + var objs = []; + R = ReactiveVar("foo"); + div = OnscreenDiv(Meteor.render(function() { + return Spark.createLandmark({ + rendered: function () { objs.push(true); } + }, function () { return R.get(); }); + })); + + Meteor.flush(); + R.set("bar"); + Meteor.flush(); + R.set("baz"); + Meteor.flush(); + + test.equal(objs.length, 3); + test.isTrue(objs[0] === objs[1]); + test.isTrue(objs[1] === objs[2]); + + div.kill(); + Meteor.flush(); + + // track chunk matching / re-rendering in detail + + var buf; + var counts; + + var testCallbacks = function(theNum /*, extend opts*/) { + return _.extend.apply(_, [{ + created: function() { + this.num = String(theNum); + var howManyBefore = counts[this.num] || 0; + counts[this.num] = howManyBefore + 1; + for(var i=0;iapple', 2, 'x'], + ['banana', 3, 'y'], + ['kiwi', 4, 'z'] + ], 1, 'fruit'); + })); + + Meteor.flush(); + buf.sort(); + test.equal(buf, ['c1', 'c2', 'c3', 'c4', 'on1', 'on2', 'on3', 'on4']); + buf.length = 0; + + R.set("bar"); + Meteor.flush(); + buf.sort(); + test.equal(buf, ['on1', 'on2', 'on3', 'on4']); + buf.length = 0; + + R.set("nothing"); + Meteor.flush(); + buf.sort(); + test.equal(buf, ['off1', 'off2', 'off3', 'off4']); + buf.length = 0; + + div.kill(); + Meteor.flush(); + + ///// Chunk 3 has no branch key, should be recreated + + buf = []; + counts = {}; + + R = ReactiveVar("foo"); + div = OnscreenDiv(Meteor.render(function() { + if (R.get() === 'nothing') + return "no chunk!"; + else + return chunk([['apple', 2, 'x'], + ['banana', 3, null], + ['kiwi', 4, 'z'] + ], 1, 'fruit'); + })); + + Meteor.flush(); + buf.sort(); + test.equal(buf, ['c1', 'c2', 'c3', 'c4', 'on1', 'on2', 'on3', 'on4']); + buf.length = 0; + + R.set("bar"); + Meteor.flush(); + buf.sort(); + test.equal(buf, ['c3*', 'off3', 'on1', 'on2', 'on3*', 'on4']); + buf.length = 0; + + div.kill(); + Meteor.flush(); + buf.sort(); + // killing the div should have given us offscreen calls for 1,2,3*,4 + test.equal(buf, ['off1', 'off2', 'off3*', 'off4']); + buf.length = 0; + + + // XXX test intermediate unkeyed chunks; + // duplicate branch keys; different order +}); + +Tinytest.add("spark - isolate inside landmark", function (test) { + + // test that preservation maps from all landmarks are honored when + // an isolate is re-rendered, even the landmarks that are outside + // the isolate and therefore not involved in the re-render. + + var R = ReactiveVar(1); + var d = OnscreenDiv(Spark.render(function () { + return Spark.createLandmark( + { preserve: ['.foo'] }, + function () { + return Spark.isolate(function () { + return '
      ' + R.get(); + }); + }); + })); + + var foo1 = d.node().firstChild; + test.equal(d.node().firstChild.nextSibling.nodeValue, '1'); + R.set(2); + Meteor.flush(); + var foo2 = d.node().firstChild; + test.equal(d.node().firstChild.nextSibling.nodeValue, '2'); + test.isTrue(foo1 === foo2); + d.kill(); + Meteor.flush(); + + // test that selectors in a landmark preservation map are resolved + // relative to the landmark, not relative to the re-rendered + // fragment. the selector may refer to nodes that are outside the + // re-rendered fragment, and the selector will still match. + + R = ReactiveVar(1); + d = OnscreenDiv(Spark.render(function () { + return Spark.createLandmark( + { preserve: ['div .foo'] }, + function () { + return "
      "+Spark.isolate(function () { + return '
      ' + R.get(); + })+"
      "; + }); + })); + + var foo1 = DomUtils.find(d.node(), '.foo'); + test.equal(foo1.nodeName, 'HR'); + test.equal(foo1.nextSibling.nodeValue, '1'); + R.set(2); + Meteor.flush(); + var foo2 = DomUtils.find(d.node(), '.foo'); + test.equal(foo2.nodeName, 'HR'); + test.equal(foo2.nextSibling.nodeValue, '2'); + test.isTrue(foo1 === foo2); + d.kill(); + Meteor.flush(); +}); + +Tinytest.add("spark - nested onscreen processing", function (test) { + var cursor = { + observe: function () { return { stop: function () {} }; } + }; + + var x = []; + var d = OnscreenDiv(Spark.render(function () { + return Spark.list(cursor, function () {}, function () { + return Spark.list(cursor, function () {}, function () { + return Spark.list(cursor, function () {}, function () { + return Spark.createLandmark({ + created: function () { x.push('c'); }, + rendered: function () { x.push('r'); }, + destroyed: function () { x.push('d'); } + }, function () { return "hi"; }); + }); + }); + }); + })); + + Meteor.flush(); + test.equal(x.join(''), 'cr'); + x = []; + d.kill(); + Meteor.flush(); + test.equal(x.join(''), 'd'); +}); + +Tinytest.add("spark - current landmark", function (test) { + var R = ReactiveVar(1); + var callbacks = 0; + var d = OnscreenDiv(Meteor.render(function () { + var html = Spark.createLandmark({ + created: function () { + this.a = 1; + this.renderCount = 0; + test.isFalse('b' in this); + callbacks++; + }, + rendered: function () { + test.equal(this.a, 9); + test.equal(this.b, 2); + if (this.renderCount === 0) + test.isFalse('c' in this); + else + test.isTrue('c' in this); + this.renderCount++; + callbacks++; + }, + destroyed: function () { + test.equal(this.a, 9); + test.equal(this.b, 2); + test.equal(this.c, 3); + callbacks++; + } + }, function (lm) { + var html = 'hi'; + + if (R.get() === 1) { + test.equal(callbacks, 1); + test.equal(lm.a, 1); + lm.a = 9; + lm.b = 2; + test.isFalse('c' in lm); + test.equal(callbacks, 1); + lm = null; + } + + if (R.get() === 2) { + test.equal(callbacks, 2); + test.equal(lm.a, 9); + test.equal(lm.b, 2); + test.equal(lm.c, 3); + test.equal(lm.renderCount, 1); + } + + return html; + }); + + + if (R.get() >= 3) { + html += Spark.labelBranch('branch', function () { + var html = Spark.createLandmark({ + created: function () { + this.outer = true; + }, + rendered: function () { + this.renderCount = (this.renderCount || 0) + 1; + } + }, function (lm) { + var html = 'outer'; + test.isTrue(lm.outer); + test.equal(R.get() - 3, lm.renderCount || 0); + html += Spark.labelBranch("a", function () { + var html = Spark.createLandmark({ + created: function () { + this.innerA = true; + }, + rendered: function () { + this.renderCount = (this.renderCount || 0) + 1; + } + }, function (lm) { + var html = 'innerA'; + test.isTrue(lm.innerA); + return html; + }); + return html; + }); + return html; + }); + + if (R.get() === 3 || R.get() >= 5) { + html += Spark.labelBranch("b", function () { + var html = Spark.createLandmark({ + created: function () { + this.innerB = true; + }, + rendered: function () { + this.renderCount = (this.renderCount || 0) + 1; + } + }, function (lm) { + var html = 'innerB'; + test.isTrue(lm.innerB); + test.equal(R.get() === 3 ? 0 : R.get() - 5, + lm.renderCount || 0); + return html; + }); + return html; + }); + } + return html; + }); + } + return html; + })); + + var findOuter = function () { + return d.node().firstChild.nextSibling; + }; + + var findInnerA = function () { + return findOuter().nextSibling; + }; + + var findInnerB = function () { + return findInnerA().nextSibling; + }; + + test.equal(callbacks, 1); + Meteor.flush(); + test.equal(callbacks, 2); + test.equal(null, Spark._getEnclosingLandmark(d.node())); + var enc = Spark._getEnclosingLandmark(d.node().firstChild); + test.equal(enc.a, 9); + test.equal(enc.b, 2); + test.isFalse('c' in enc); + enc.c = 3; + Meteor.flush(); + test.equal(callbacks, 2); + + R.set(2) + Meteor.flush(); + test.equal(callbacks, 3); + + R.set(3) + Meteor.flush(); + test.equal(callbacks, 4); + + test.isTrue(Spark._getEnclosingLandmark(findOuter()).outer); + test.isTrue(Spark._getEnclosingLandmark(findInnerA()).innerA); + test.isTrue(Spark._getEnclosingLandmark(findInnerB()).innerB); + test.equal(1, Spark._getEnclosingLandmark(findOuter()).renderCount); + test.equal(1, Spark._getEnclosingLandmark(findInnerA()).renderCount); + test.equal(1, Spark._getEnclosingLandmark(findInnerB()).renderCount); + + R.set(4) + Meteor.flush(); + test.equal(callbacks, 5); + test.equal(2, Spark._getEnclosingLandmark(findOuter()).renderCount); + test.equal(2, Spark._getEnclosingLandmark(findInnerA()).renderCount); + + R.set(5) + Meteor.flush(); + test.equal(callbacks, 6); + test.equal(3, Spark._getEnclosingLandmark(findOuter()).renderCount); + test.equal(3, Spark._getEnclosingLandmark(findInnerA()).renderCount); + test.equal(1, Spark._getEnclosingLandmark(findInnerB()).renderCount); + + R.set(6) + Meteor.flush(); + test.equal(callbacks, 7); + test.equal(4, Spark._getEnclosingLandmark(findOuter()).renderCount); + test.equal(4, Spark._getEnclosingLandmark(findInnerA()).renderCount); + test.equal(2, Spark._getEnclosingLandmark(findInnerB()).renderCount); + + d.kill(); + Meteor.flush(); + test.equal(callbacks, 8); + + Meteor.flush(); + test.equal(callbacks, 8); +}); + +Tinytest.add("spark - find/findAll on landmark", function (test) { + var l1, l2; + var R = ReactiveVar(1); + + var d = OnscreenDiv(Spark.render(function () { + return "
      k
      " + + Spark.labelBranch("a", function () { + return Spark.createLandmark({ + created: function () { + test.instanceOf(this, Spark.Landmark); + if (l1) + test.equal(l1, this); + l1 = this; + } + }, function () { + return "a" + + Spark.labelBranch("b", function () { + return Spark.isolate( + function () { + R.get(); + return Spark.createLandmark( + { + created: function () { + test.instanceOf(this, Spark.Landmark); + if (l2) + test.equal(l2, this); + l2 = this; + } + }, function () { + return "b4" + + "b6"; + }); + }); + }) + ""; + }); + }) + "c
      "; + })); + + var ids = function (nodes) { + if (!(nodes instanceof Array)) + nodes = nodes ? [nodes] : []; + return _.pluck(nodes, 'id').join(''); + }; + + var check = function (all) { + var f = all ? 'findAll' : 'find'; + + test.equal(ids(l1[f]('.kitten')), ''); + test.equal(ids(l2[f]('.kitten')), ''); + + test.equal(ids(l1[f]('.a')), '3'); + test.equal(ids(l2[f]('.a')), ''); + + test.equal(ids(l1[f]('.b')), all ? '46' : '4'); + test.equal(ids(l2[f]('.b')), all ? '46' : '4'); + + test.equal(ids(l1[f]('.c')), ''); + test.equal(ids(l2[f]('.c')), ''); + + test.equal(ids(l1[f]('.a .b')), all ? '46' : '4'); + test.equal(ids(l2[f]('.a .b')), ''); + }; + + check(false); + check(true); + R.set(2); + Meteor.flush(); + check(false); + check(true); + + d.kill(); + Meteor.flush(); +}); + +Tinytest.add("spark - landmark clean-up", function (test) { + + var crd; + var makeCrd = function () { + var crd = [0,0,0]; + crd.callbacks = { + created: function () { crd[0]++; }, + rendered: function () { crd[1]++; }, + destroyed: function () { crd[2]++; } + }; + return crd; + }; + + // not inside render + crd = makeCrd(); + Spark.createLandmark(crd.callbacks, function () { return 'hi'; }); + test.equal(crd, [1,0,1]); + + // landmark never materialized + crd = makeCrd(); + Spark.render(function() { + var html = + Spark.createLandmark(crd.callbacks, function () { return 'hi'; }); + return ''; + }); + test.equal(crd, [1,0,1]); + Meteor.flush(); + test.equal(crd, [1,0,1]); + + // two landmarks, only one materialized at a time. + // one replaces the other + var crd1 = makeCrd(); + var crd2 = makeCrd(); + var R = ReactiveVar(1); + var div = OnscreenDiv(Meteor.render(function() { + return (R.get() === 1 ? + Spark.createLandmark(crd1.callbacks, function() { return 'hi'; }) : + Spark.createLandmark(crd2.callbacks, function() { return 'hi'; })); + })); + test.equal(crd1, [1,0,0]); // created + test.equal(crd2, [0,0,0]); + Meteor.flush(); + test.equal(crd1, [1,1,0]); // rendered + test.equal(crd2, [0,0,0]); + R.set(2); + Meteor.flush(); + test.equal(crd1, [1,1,0]); // not destroyed (callback replaced) + test.equal(crd2, [0,1,0]); // matched + + div.kill(); + Meteor.flush(); + test.equal(crd1, [1,1,0]); + test.equal(crd2, [0,1,1]); // destroyed +}); + +Tinytest.add("spark - bubbling render", function (test) { + var makeCrd = function () { + var crd = [0,0,0]; + crd.callbacks = { + created: function () { crd[0]++; }, + rendered: function () { crd[1]++; }, + destroyed: function () { crd[2]++; } + }; + return crd; + }; + + var crd1 = makeCrd(); + var crd2 = makeCrd(); + + var R = ReactiveVar('foo'); + var div = OnscreenDiv(Spark.render(function () { + return Spark.createLandmark(crd1.callbacks, function () { + return Spark.labelBranch('fred', function () { + return Spark.createLandmark(crd2.callbacks, function () { + return Spark.isolate(function () { + return R.get(); + }); + }); + }); + }); + })); + + Meteor.flush(); + test.equal(div.html(), 'foo'); + test.equal(crd1, [1,1,0]); + test.equal(crd2, [1,1,0]); + + R.set('bar'); + Meteor.flush(); + test.equal(div.html(), 'bar'); + test.equal(crd1, [1,2,0]); + test.equal(crd2, [1,2,0]); + + div.kill(); + Meteor.flush(); +}); + +})(); + +Tinytest.add("spark - landmark arg", function (test) { + var div = OnscreenDiv(Spark.render(function () { + return Spark.createLandmark({ + created: function () { + test.isFalse(this.hasDom()); + }, + rendered: function () { + var landmark = this; + landmark.firstNode().innerHTML = 'Greetings'; + landmark.lastNode().innerHTML = 'Line'; + landmark.find('i').innerHTML = + (landmark.findAll('b').length)+"-bold"; + test.isTrue(landmark.hasDom()); + }, + destroyed: function () { + test.isFalse(this.hasDom()); + } + }, function () { + return Spark.attachEvents({ + 'click': function (event, landmark) { + landmark.firstNode().innerHTML = 'Hello'; + landmark.lastNode().innerHTML = 'World'; + landmark.find('i').innerHTML = + (landmark.findAll('*').length)+"-element"; + } + }, 'Foo Bar Baz'); + }); + })); + + test.equal(div.text(), "Foo Bar Baz"); + Meteor.flush(); + test.equal(div.text(), "Greetings 1-bold Line"); + clickElement(DomUtils.find(div.node(), 'i')); + test.equal(div.text(), "Hello 3-element World"); + + div.kill(); + Meteor.flush(); +}); + +Tinytest.add("spark - landmark preserve", function (test) { + var R = ReactiveVar("foo"); + + var lmhr = function () { + return Spark.createLandmark({preserve:['hr']}, function () { + return '
      '; + }); + }; + + var div = OnscreenDiv(Meteor.render(function () { + return "
      " + R.get() + "" + + Spark.labelBranch('A', lmhr) + Spark.labelBranch('B', lmhr) + + "
      "; + })); + + test.equal(div.html(), '
      foo

      '); + var hrs1 = DomUtils.findAll(div.node(), 'hr'); + R.set("bar"); + Meteor.flush(); + test.equal(div.html(), '
      bar

      '); + var hrs2 = DomUtils.findAll(div.node(), 'hr'); + + test.isTrue(hrs1[0] === hrs2[0]); + test.isTrue(hrs1[1] === hrs2[1]); + + div.kill(); + Meteor.flush(); +}); + +Tinytest.add("spark - branch annotation is optional", function (test) { + // test that labelBranch works on HTML that isn't element-balanced + // and doesn't fail by trying to emit an annotation when it contains + // no landmarks. + + var R = ReactiveVar("foo"); + + var Rget = function () { return R.get(); }; + var cnst = function (c) { return function () { return c; }; }; + var lmhr = function () { + return Spark.createLandmark({preserve:['hr']}, function () { + return '
      '; + }); + }; + + var div = OnscreenDiv(Meteor.render(function () { + return '
      ' + + Spark.labelBranch('B', cnst('
      ')) + + Spark.labelBranch('C', lmhr) + Spark.labelBranch('D', lmhr) + + '
      '; + })); + + test.equal(div.html(), '


      '); + var div1 = div.node().firstChild; + var hrs1 = DomUtils.findAll(div.node(), 'hr'); + R.set("bar"); + Meteor.flush(); + test.equal(div.html(), '


      '); + var div2 = div.node().firstChild; + var hrs2 = DomUtils.findAll(div.node(), 'hr'); + + test.isFalse(div1 === div2); + test.isTrue(hrs1[0] === hrs2[0]); + test.isTrue(hrs1[1] === hrs2[1]); + + div.kill(); + Meteor.flush(); +}); + +Tinytest.add("spark - unique label", function (test) { + var buf = []; + var bufstr = function () { + buf.sort(); + var str = buf.join(''); + buf.length = 0; + return str; + }; + + var ublm = function () { + return Spark.labelBranch(Spark.UNIQUE_LABEL, function () { + return Spark.createLandmark({created: function () { buf.push('c'); }, + rendered: function () { buf.push('r'); }, + destroyed: function () { buf.push('d'); }}, + function () { return 'x'; }); + }); + }; + + var R = ReactiveVar("foo"); + + var div = OnscreenDiv(Meteor.render(function () { + return ublm() + ublm() + ublm() + R.get(); + })); + Meteor.flush(); + test.equal(bufstr(), 'cccrrr'); + R.set('bar'); + Meteor.flush(); + test.equal(bufstr(), 'cccdddrrr'); + + div.kill(); + Meteor.flush(); + test.equal(bufstr(), 'ddd'); + +}); + +Tinytest.add("spark - list update", function (test) { + var R = ReactiveVar('foo'); + + var lst = []; + lst.callbacks = []; + lst.observe = function(callbacks) { + lst.callbacks.push(callbacks); + _.each(lst, function(x, i) { + callbacks.added(x, i); + }); + return { + stop: function() { + lst.callbacks = _.without(lst.callbacks, callbacks); + } + }; + }; + lst.another = function () { + var i = lst.length; + lst.push({_id:'item'+i}); + _.each(lst.callbacks, function (callbacks) { + callbacks.added(lst[i], i); + }); + }; + var div = OnscreenDiv(Meteor.render(function() { + return R.get() + Spark.list(lst, function () { + return '
      '; + }); + })); + + lst.another(); + Meteor.flush(); + test.equal(div.html(), "foo
      "); + + lst.another(); + R.set('bar'); + Meteor.flush(); + test.equal(div.html(), "bar

      "); + + R.set('baz'); + lst.another(); + Meteor.flush(); + test.equal(div.html(), "baz


      "); + + div.kill(); + Meteor.flush(); +}); + +Tinytest.add("spark - callback context", function (test) { + // Test that context in template callbacks is null. + + var cxs = []; + var buf = []; + + var R = ReactiveVar("foo"); + var getCx = function () { return Meteor.deps.Context.current; }; + var callbackFunc = function (ltr) { + return function () { + buf.push(ltr); + cxs.push(getCx()); + }; + }; + var div = OnscreenDiv(Meteor.render(function () { + var cx = getCx(); + test.isTrue(cx); // sanity check for getCx + var makeLandmark = function () { + return Spark.createLandmark({created: callbackFunc('c'), + rendered: callbackFunc('r'), + destroyed: callbackFunc('d')}, + function () { + return ''+R.get()+''; + }); + }; + if (R.get() === 'foo') + var unused = makeLandmark(); // will cause created/destroyed + var html = Spark.labelBranch("foo", makeLandmark); + test.isTrue(getCx() === cx); // test that context was restored + return html; + })); + Meteor.flush(); + R.set('bar'); + Meteor.flush(); + div.kill(); + Meteor.flush(); + + test.equal(buf.join(''), 'ccdrrd'); + test.equal(cxs.length, 6); + test.isFalse(cxs[0]); + test.isFalse(cxs[1]); + test.isFalse(cxs[2]); + test.isFalse(cxs[3]); + test.isFalse(cxs[4]); + test.isFalse(cxs[5]); + +}); \ No newline at end of file diff --git a/packages/liveui/form_responder.js b/packages/spark/test_form_responder.js similarity index 90% rename from packages/liveui/form_responder.js rename to packages/spark/test_form_responder.js index 11785289e3..6c424d5264 100644 --- a/packages/liveui/form_responder.js +++ b/packages/spark/test_form_responder.js @@ -1,6 +1,6 @@ (function () { -var TEST_RESPONDER_ROUTE = "/liveui_test_responder"; +var TEST_RESPONDER_ROUTE = "/spark_test_responder"; var respond = function(req, res) { diff --git a/packages/stylus/stylus_tests.js b/packages/stylus/stylus_tests.js index 6ce256b58e..304411ff74 100644 --- a/packages/stylus/stylus_tests.js +++ b/packages/stylus/stylus_tests.js @@ -1,7 +1,7 @@ Tinytest.add("stylus - presence", function(test) { - var d = OnscreenDiv(Meteor.ui.render(function() { + var d = OnscreenDiv(Meteor.render(function() { return '

      '; })); d.node().style.display = 'block'; diff --git a/packages/templating/deftemplate.js b/packages/templating/deftemplate.js index 6f69f97405..730d21a7b5 100644 --- a/packages/templating/deftemplate.js +++ b/packages/templating/deftemplate.js @@ -2,38 +2,174 @@ Meteor._partials = {}; - Meteor._hook_handlebars_each = function () { - Meteor._hook_handlebars_each = function(){}; // install the hook only once + // XXX Handlebars hooking is janky and gross + + Meteor._hook_handlebars = function () { + Meteor._hook_handlebars = function(){}; // install the hook only once var orig = Handlebars._default_helpers.each; Handlebars._default_helpers.each = function (arg, options) { - if (!(arg instanceof LocalCollection.Cursor)) + // if arg isn't an observable (like LocalCollection.Cursor), + // don't use this reactive implementation of #each. + if (!(arg && 'observe' in arg)) return orig.call(this, arg, options); - return Meteor.ui.listChunk(arg, options.fn, options.inverse, null); + return Spark.list( + arg, + function (item) { + return Spark.labelBranch(item._id || null, function () { + var html = Spark.isolate(_.bind(options.fn, null, item)); + return Spark.setDataContext(item, html); + }); + }, + function () { + return options.inverse ? + Spark.isolate(options.inverse) : ''; + } + ); }; + + _.extend(Handlebars._default_helpers, { + isolate: function (options) { + var data = this; + return Spark.isolate(function () { + return options.fn(data); + }); + }, + constant: function (options) { + var data = this; + return Spark.createLandmark({ constant: true }, function () { + return options.fn(data); + }); + } + }); }; + // map from landmark id, to the 'this' object for + // created/rendered/destroyed callbacks on templates + var templateInstanceData = {}; + + var templateObjFromLandmark = function (landmark) { + var template = templateInstanceData[landmark.id] || ( + templateInstanceData[landmark.id] = { + // set these once + find: function (selector) { + if (! landmark.hasDom()) + throw new Error("Template not in DOM"); + return landmark.find(selector); + }, + findAll: function (selector) { + if (! landmark.hasDom()) + throw new Error("Template not in DOM"); + return landmark.findAll(selector); + } + }); + // set these each time + template.firstNode = landmark.hasDom() ? landmark.firstNode() : null; + template.lastNode = landmark.hasDom() ? landmark.lastNode() : null; + return template; + }; + + // XXX forms hooks into this to add "bind"? + Meteor._template_decl_methods = { + // methods store data here (event map, etc.). initialized per template. + _tmpl_data: null, + // these functions must be generic (i.e. use `this`) + events: function (eventMap) { + var events = + (this._tmpl_data.events = (this._tmpl_data.events || {})); + _.extend(events, eventMap); + }, + preserve: function (preserveMap) { + var preserve = + (this._tmpl_data.preserve = (this._tmpl_data.preserve || {})); + + if (_.isArray(preserveMap)) + _.each(preserveMap, function (selector) { + preserve[selector] = true; + }); + else + _.extend(preserve, preserveMap); + }, + helpers: function (helperMap) { + var helpers = + (this._tmpl_data.helpers = (this._tmpl_data.helpers || {})); + for(var h in helperMap) + helpers[h] = helperMap[h]; + } + }; Meteor._def_template = function (name, raw_func) { - Meteor._hook_handlebars_each(); + Meteor._hook_handlebars(); window.Template = window.Template || {}; - var partial = function(data) { - var getHtml = function() { - return raw_func(data, { - helpers: partial, - partials: Meteor._partials + // Define the function assigned to Template.. + + var partial = function (data) { + var tmpl = name && Template[name] || {}; + var tmplData = tmpl._tmpl_data || {}; + + var html = Spark.labelBranch("Template."+name, function () { + var html = Spark.createLandmark({ + preserve: tmplData.preserve || {}, + created: function () { + var template = templateObjFromLandmark(this); + template.data = data; + tmpl.created && tmpl.created.call(template); + }, + rendered: function () { + var template = templateObjFromLandmark(this); + template.data = data; + tmpl.rendered && tmpl.rendered.call(template); + }, + destroyed: function () { + // template.data is already set from previous callbacks + tmpl.destroyed && + tmpl.destroyed.call(templateObjFromLandmark(this)); + delete templateInstanceData[this.id]; + } + }, function (landmark) { + var html = Spark.isolate(function () { + // XXX Forms needs to run a hook before and after raw_func + // (and receive 'landmark') + return raw_func(data, { + helpers: _.extend({}, partial, tmplData.helpers || {}), + partials: Meteor._partials, + name: name + }); + }); + + // take an event map with `function (event, template)` handlers + // and produce one with `function (event, landmark)` handlers + // for Spark, by inserting logic to create the template object. + var wrapEventMap = function (oldEventMap) { + var newEventMap = {}; + _.each(oldEventMap, function (handler, key) { + newEventMap[key] = function (event, landmark) { + return handler.call(this, event, + templateObjFromLandmark(landmark)); + }; + }); + return newEventMap; + }; + + // support old Template.foo.events = {...} format + var events = + (tmpl.events !== Meteor._template_decl_methods.events ? + tmpl.events : tmplData.events); + // events need to be inside the landmark, not outside, so + // that when an event fires, you can retrieve the enclosing + // landmark to get the template data + if (tmpl.events) + html = Spark.attachEvents(wrapEventMap(events), html); + return html; }); - }; + html = Spark.setDataContext(data, html); + return html; + }); - - var react_data = { events: (name ? Template[name].events : {}), - event_data: data, - template_name: name }; - - return Meteor.ui.chunk(getHtml, react_data); + return html; }; // XXX hack.. copy all of Handlebars' built in helpers over to @@ -49,6 +185,8 @@ "'. Each template needs a unique name."); Template[name] = partial; + _.extend(partial, Meteor._template_decl_methods); + partial._tmpl_data = {}; Meteor._partials[name] = partial; } @@ -58,7 +196,3 @@ }; })(); - - - - diff --git a/packages/templating/html_scanner.js b/packages/templating/html_scanner.js index 1c80593e5d..d5ac1954ae 100644 --- a/packages/templating/html_scanner.js +++ b/packages/templating/html_scanner.js @@ -111,7 +111,8 @@ var html_scanner = { _handleTag: function (results, tag, attribs, contents, parseError) { - // trim the tag contents + // trim the tag contents. + // this is a courtesy and is also relied on by some unit tests. contents = contents.match(/^[ \t\r\n]*([\s\S]*?)[ \t\r\n]*$/)[1]; // do we have 1 or more attribs? @@ -146,7 +147,7 @@ var html_scanner = { if (hasAttribs) throw parseError("Attributes on not supported"); results.js += "Meteor.startup(function(){" + - "document.body.appendChild(Meteor.ui.render(" + + "document.body.appendChild(Spark.render(" + "Meteor._def_template(null," + code + ")));});"; } } diff --git a/packages/templating/package.js b/packages/templating/package.js index 2c2fc6c5fc..0ae7952cfe 100644 --- a/packages/templating/package.js +++ b/packages/templating/package.js @@ -11,7 +11,7 @@ Package.on_use(function (api) { // is encountered.. shouldn't be very hard, we just need a way to // get at 'api' from a register_extension handler - api.use(['underscore', 'liveui'], 'client'); + api.use(['underscore', 'spark'], 'client'); // provides the runtime logic to instantiate our templates api.add_files('deftemplate.js', 'client'); @@ -83,7 +83,7 @@ Package.register_extension( Package.on_test(function (api) { api.use('tinytest'); api.use('htmljs'); - api.use('test-helpers', 'client'); + api.use(['test-helpers', 'domutils', 'session'], 'client'); api.add_files([ 'templating_tests.js', 'templating_tests.html' diff --git a/packages/templating/scanner_tests.js b/packages/templating/scanner_tests.js index d05e01c71b..77a596434f 100644 --- a/packages/templating/scanner_tests.js +++ b/packages/templating/scanner_tests.js @@ -24,7 +24,7 @@ Tinytest.add("templating - html scanner", function (test) { }; var BODY_PREAMBLE = "Meteor.startup(function(){" + - "document.body.appendChild(Meteor.ui.render(" + + "document.body.appendChild(Spark.render(" + "Meteor._def_template(null,"; var BODY_POSTAMBLE = ")));});"; var TEMPLATE_PREAMBLE = "Meteor._def_template("; diff --git a/packages/templating/templating_tests.html b/packages/templating/templating_tests.html index 779b2122ea..6f7692ec9b 100644 --- a/packages/templating/templating_tests.html +++ b/packages/templating/templating_tests.html @@ -183,3 +183,125 @@ (biggie={{#get_arg helperListFour platypus thisTest fancyhelper.currentFruit fancyhelper.currentCountry.unicorns a=platypus b=thisTest c=fancyhelper.currentFruit d=fancyhelper.currentCountry.unicorns}}{{/get_arg}}) (twoArgBlock={{#two_args "foo" "foo"}}{{/two_args}}) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/templating/templating_tests.js b/packages/templating/templating_tests.js index 046a769213..4815d809c9 100644 --- a/packages/templating/templating_tests.js +++ b/packages/templating/templating_tests.js @@ -3,8 +3,8 @@ Tinytest.add("templating - assembly", function (test) { // Test for a bug that made it to production -- after a replacement, // we need to also check the newly replaced node for replacements - var frag = Meteor.ui.render(Template.test_assembly_a0); - test.equal(canonicalizeHtml(Meteor.ui._fragmentToHtml(frag)), + var frag = Meteor.render(Template.test_assembly_a0); + test.equal(canonicalizeHtml(DomUtils.fragmentToHtml(frag)), "Hi"); // Another production bug -- we must use LiveRange to replace the @@ -14,7 +14,7 @@ Tinytest.add("templating - assembly", function (test) { return Session.get("stuff"); }; var onscreen = DIV({style: "display: none"}, [ - Meteor.ui.render(Template.test_assembly_b0)]); + Meteor.render(Template.test_assembly_b0)]); document.body.appendChild(onscreen); test.equal(canonicalizeHtml(onscreen.innerHTML), "xyhi"); Session.set("stuff", false); @@ -41,7 +41,7 @@ Tinytest.add("templating - table assembly", function(test) { var table; - table = childWithTag(Meteor.ui.render(Template.test_table_a0), "TABLE"); + table = childWithTag(Meteor.render(Template.test_table_a0), "TABLE"); // table.rows is a great test, as it fails not only when TR/TD tags are // stripped due to improper html-to-fragment, but also when they are present @@ -49,7 +49,7 @@ Tinytest.add("templating - table assembly", function(test) { test.equal(table.rows.length, 3); // this time with an explicit TBODY - table = childWithTag(Meteor.ui.render(Template.test_table_b0), "TABLE"); + table = childWithTag(Meteor.render(Template.test_table_b0), "TABLE"); test.equal(table.rows.length, 3); var c = new LocalCollection(); @@ -58,7 +58,7 @@ Tinytest.add("templating - table assembly", function(test) { c.insert({bar:'c'}); var onscreen = DIV({style: "display: none;"}); onscreen.appendChild( - Meteor.ui.render(_.bind(Template.test_table_each, null, {foo: c.find()}))); + Meteor.render(_.bind(Template.test_table_each, null, {foo: c.find()}))); document.body.appendChild(onscreen); table = childWithTag(onscreen, "TABLE"); @@ -80,17 +80,20 @@ Tinytest.add("templating - event handler this", function(test) { Template.test_event_data_with.TWO = {str: "two"}; Template.test_event_data_with.THREE = {str: "three"}; + Template.test_event_data_with.events({ + 'click': function(event, template) { + test.isTrue(this.str); + test.equal(template.data.str, "one"); + event_buf.push(this.str); + } + }); + var event_buf = []; var tmpl = OnscreenDiv( - Meteor.ui.render( - function() { - return Template.test_event_data_with( - Template.test_event_data_with.ONE); - }, - { events: { 'click': function() { - test.isTrue(this.str); - event_buf.push(this.str); - } }})); + Meteor.render(function () { + return Template.test_event_data_with( + Template.test_event_data_with.ONE); + })); var divs = tmpl.node().getElementsByTagName("div"); test.equal(3, divs.length); @@ -313,3 +316,633 @@ Tinytest.add("templating - helpers and dots", function(test) { test.equal(trials[5], "(twoArgBlock=true,false)"); test.equal(trials.length, 6); }); + + +Tinytest.add("templating - rendered template", function(test) { + var R = ReactiveVar('foo'); + Template.test_render_a.foo = function() { + R.get(); + return this.x + 1; + }; + + Template.test_render_a.preserve(['br']); + + var div = OnscreenDiv( + Meteor.render(function () { + return Template.test_render_a({ x: 123 }); + })); + + test.equal(div.text().match(/\S+/)[0], "124"); + + var br1 = div.node().getElementsByTagName('br')[0]; + var hr1 = div.node().getElementsByTagName('hr')[0]; + test.isTrue(br1); + test.isTrue(hr1); + + R.set('bar'); + Meteor.flush(); + var br2 = div.node().getElementsByTagName('br')[0]; + var hr2 = div.node().getElementsByTagName('hr')[0]; + test.isTrue(br2); + test.isTrue(br1 === br2); + test.isTrue(hr2); + test.isFalse(hr1 === hr2); + + div.kill(); + Meteor.flush(); + + ///// + + R = ReactiveVar('foo'); + + Template.test_render_b.foo = function() { + R.get(); + return (+this) + 1; + }; + Template.test_render_b.preserve(['br']); + + div = OnscreenDiv( + Meteor.render(function () { + return Template.test_render_b({ x: 123 }); + })); + + test.equal(div.text().match(/\S+/)[0], "201"); + + var br1 = div.node().getElementsByTagName('br')[0]; + var hr1 = div.node().getElementsByTagName('hr')[0]; + test.isTrue(br1); + test.isTrue(hr1); + + R.set('bar'); + Meteor.flush(); + var br2 = div.node().getElementsByTagName('br')[0]; + var hr2 = div.node().getElementsByTagName('hr')[0]; + test.isTrue(br2); + test.isTrue(br1 === br2); + test.isTrue(hr2); + test.isFalse(hr1 === hr2); + + div.kill(); + Meteor.flush(); + + ///// + + var stuff = new LocalCollection(); + stuff.insert({foo:'bar'}); + + Template.test_render_c.preserve(['br']); + + div = OnscreenDiv( + Meteor.renderList( + stuff.find(), function (data) { + return Template.test_render_c(data, 'blah'); + })); + + var br1 = div.node().getElementsByTagName('br')[0]; + var hr1 = div.node().getElementsByTagName('hr')[0]; + test.isTrue(br1); + test.isTrue(hr1); + + stuff.update({foo:'bar'}, {$set: {foo: 'baz'}}); + Meteor.flush(); + var br2 = div.node().getElementsByTagName('br')[0]; + var hr2 = div.node().getElementsByTagName('hr')[0]; + test.isTrue(br2); + test.isTrue(br1 === br2); + test.isTrue(hr2); + test.isFalse(hr1 === hr2); + + div.kill(); + Meteor.flush(); + + ///// + + var stuff = new LocalCollection(); + stuff.insert({foo:'bar'}); + + Template.test_render_c.preserve(['br']); + + div = OnscreenDiv(Meteor.renderList(stuff.find(), + Template.test_render_c)); + + var br1 = div.node().getElementsByTagName('br')[0]; + var hr1 = div.node().getElementsByTagName('hr')[0]; + test.isTrue(br1); + test.isTrue(hr1); + + stuff.update({foo:'bar'}, {$set: {foo: 'baz'}}); + Meteor.flush(); + var br2 = div.node().getElementsByTagName('br')[0]; + var hr2 = div.node().getElementsByTagName('hr')[0]; + test.isTrue(br2); + test.isTrue(br1 === br2); + test.isTrue(hr2); + test.isFalse(hr1 === hr2); + + div.kill(); + Meteor.flush(); + +}); + +Tinytest.add("templating - branch labels", function(test) { + var R = ReactiveVar('foo'); + Template.test_branches_a['var'] = function () { + return R.get(); + }; + + var elems = []; + + // use constant landmarks to test that each + // block helper invocation gets a different label + Template.test_branches_a.myConstant = function (options) { + var data = this; + var firstRender = true; + return Spark.createLandmark({ constant: true, + rendered: function () { + if (! firstRender) + return; + firstRender = false; + var hr = this.find('hr'); + hr.myIndex = elems.length; + elems.push(this.find('hr')); + }}, + function () { + return options.fn(data); + }); + }; + + var div = OnscreenDiv(Meteor.render(Template.test_branches_a)); + Meteor.flush(); + test.equal(DomUtils.find(div.node(), 'span').innerHTML, 'foo'); + test.equal(elems.length, 3); + + R.set('bar'); + Meteor.flush(); + var elems2 = DomUtils.findAll(div.node(), 'hr'); + elems2.sort(function(a, b) { return a.myIndex - b.myIndex; }); + test.equal(elems[0], elems2[0]); + test.equal(elems[1], elems2[1]); + test.equal(elems[2], elems2[2]); + test.equal(DomUtils.find(div.node(), 'span').innerHTML, 'bar'); + + div.kill(); + Meteor.flush(); +}); + +Tinytest.add("templating - matching in list", function (test) { + var c = new LocalCollection(); + c.insert({letter:'a'}); + c.insert({letter:'b'}); + c.insert({letter:'c'}); + + _.extend(Template.test_listmatching_a0, { + 'var': function () { return R.get(); }, + c: function () { return c.find(); } + }); + + var buf = []; + _.extend(Template.test_listmatching_a1, { + created: function () { buf.push('+'); }, + rendered: function () { + var letter = canonicalizeHtml( + DomUtils.rangeToHtml(this.firstNode, + this.lastNode).match(/\S+/)[0]); + buf.push('*'+letter); + }, + destroyed: function () { buf.push('-'); } + }); + + var R = ReactiveVar('foo'); + var div = OnscreenDiv(Spark.render(Template.test_listmatching_a0)); + Meteor.flush(); + + test.equal(DomUtils.find(div.node(), 'span').innerHTML, 'foo'); + test.equal(div.html().match(/

      (.*?)<\/p>/)[1].match(/\S+/g), ['a','b','c']); + test.equal(buf.join(''), '+++*a*b*c'); + + buf.length = 0; + R.set('bar'); + Meteor.flush(); + test.equal(DomUtils.find(div.node(), 'span').innerHTML, 'bar'); + test.equal(div.html().match(/

      (.*?)<\/p>/)[1].match(/\S+/g), ['a','b','c']); + test.equal(buf.join(''), '*a*b*c'); + + div.kill(); + Meteor.flush(); + +}); + +Tinytest.add("templating - isolate helper", function (test) { + var Rs = _.map(_.range(4), function () { return ReactiveVar(1); }); + var touch = function (n) { Rs[n-1].get(); }; + var bump = function (n) { Rs[n-1].set(Rs[n-1].get() + 1); }; + var counts = _.map(_.range(4), function () { return 0; }); + var tally = function (n) { return ++counts[n-1]; }; + + _.extend(Template.test_isolate_a, { + helper: function (n) { + touch(n); + return tally(n); + } + }); + + var div = OnscreenDiv(Meteor.render(Template.test_isolate_a)); + + var getTallies = function () { + return _.map(div.html().match(/\S+/g), Number); + }; + var expect = function(str) { + test.equal(getTallies().join(','), str); + }; + + Meteor.flush(); + expect("1,1,1,1"); + bump(1); Meteor.flush(); expect("2,2,2,2"); + bump(2); Meteor.flush(); expect("2,3,3,3"); + bump(3); Meteor.flush(); expect("2,3,4,4"); + bump(4); Meteor.flush(); expect("2,3,4,5"); + Meteor.flush(); expect("2,3,4,5"); + bump(3); Meteor.flush(); expect("2,3,5,6"); + bump(2); Meteor.flush(); expect("2,4,6,7"); + bump(1); Meteor.flush(); expect("3,5,7,8"); + + div.kill(); + Meteor.flush(); + +}); + +Tinytest.add("templating - template arg", function (test) { + Template.test_template_arg_a.events({ + click: function (event, template) { + template.firstNode.innerHTML = 'Hello'; + template.lastNode.innerHTML = 'World'; + template.find('i').innerHTML = + (template.findAll('*').length)+"-element"; + template.lastNode.innerHTML += ' (the secret is '+ + template.secret+')'; + } + }); + + Template.test_template_arg_a.created = function() { + var self = this; + test.isFalse(self.firstNode); + test.isFalse(self.lastNode); + test.throws(function () { return self.find("*"); }); + test.throws(function () { return self.findAll("*"); }); + }; + + Template.test_template_arg_a.rendered = function () { + var template = this; + template.firstNode.innerHTML = 'Greetings'; + template.lastNode.innerHTML = 'Line'; + template.find('i').innerHTML = + (template.findAll('b').length)+"-bold"; + template.secret = "strawberry "+template.data.food; + }; + + Template.test_template_arg_a.destroyed = function() { + var self = this; + test.isFalse(self.firstNode); + test.isFalse(self.lastNode); + test.throws(function () { return self.find("*"); }); + test.throws(function () { return self.findAll("*"); }); + }; + + var div = OnscreenDiv(Spark.render(function () { + return Template.test_template_arg_a({food: "pie"}); + })); + + test.equal(div.text(), "Foo Bar Baz"); + Meteor.flush(); + test.equal(div.text(), "Greetings 1-bold Line"); + clickElement(DomUtils.find(div.node(), 'i')); + test.equal(div.text(), "Hello 3-element World (the secret is strawberry pie)"); + + div.kill(); + Meteor.flush(); +}); + +Tinytest.add("templating - preserve", function (test) { + var R = ReactiveVar('foo'); + + var tmpl = Template.test_template_preserve_a; + tmpl.preserve(['.b']); + tmpl.preserve(['.c']); + tmpl.preserve({'.d': true}); + tmpl.preserve({'span': function (n) { + return _.contains(['e','f'], n.className) && n.className; + }}); + tmpl.preserve(['span.a']); + tmpl['var'] = function () { return R.get(); }; + + var div = OnscreenDiv(Meteor.render(tmpl)); + Meteor.flush(); + test.equal(DomUtils.find(div.node(), 'u').firstChild.nodeValue.match( + /\S+/)[0], 'foo'); + var spans1 = {}; + _.each(DomUtils.findAll(div.node(), 'span'), function (sp) { + spans1[sp.className] = sp; + }); + + R.set('bar'); + Meteor.flush(); + test.equal(DomUtils.find(div.node(), 'u').firstChild.nodeValue.match( + /\S+/)[0], 'bar'); + var spans2 = {}; + _.each(DomUtils.findAll(div.node(), 'span'), function (sp) { + spans2[sp.className] = sp; + }); + + test.isTrue(spans1.a === spans2.a); + test.isTrue(spans1.b === spans2.b); + test.isTrue(spans1.c === spans2.c); + test.isTrue(spans1.d === spans2.d); + test.isTrue(spans1.e === spans2.e); + test.isTrue(spans1.f === spans2.f); + test.isFalse(spans1.y === spans2.y); + test.isFalse(spans1.z === spans2.z); + + div.kill(); + Meteor.flush(); +}); + +Tinytest.add("templating - helpers", function (test) { + var tmpl = Template.test_template_helpers_a; + + tmpl.foo = 'z'; + tmpl.helpers({bar: 'b'}); + // helpers(...) takes precendence of assigned helper + tmpl.helpers({foo: 'a', baz: function() { return 'c'; }}); + + var div = OnscreenDiv(Meteor.render(tmpl)); + test.equal(div.text().match(/\S+/)[0], 'abc'); + div.kill(); + Meteor.flush(); + + tmpl = Template.test_template_helpers_b; + + tmpl.helpers({ + 'name': 'A', + 'arity': 'B', + 'toString': 'C', + 'length': 4, + 'var': 'D' + }); + + div = OnscreenDiv(Meteor.render(tmpl)); + var txt = div.text().match(/\S+/)[0]; + test.isTrue(txt.match(/^ABC?4D$/)); + // We don't get 'C' (the ability to name a helper {{toString}}) + // in IE < 9 because of the famed DontEnum bug. This could be + // fixed but it would require making all the code that handles + // the dictionary of helpers be DontEnum-aware. In practice, + // the Object prototype method names (toString, hasOwnProperty, + // isPropertyOf, ...) make poor helper names and are unlikely + // to be used in apps. + test.expect_fail(); + test.equal(txt, 'ABC4D'); + div.kill(); + Meteor.flush(); + + // test that helpers don't "leak" + tmpl = Template.test_template_helpers_c; + div = OnscreenDiv(Meteor.render(tmpl)); + test.equal(div.text(), 'x'); + div.kill(); + Meteor.flush(); +}); + +Tinytest.add("templating - events", function (test) { + var tmpl = Template.test_template_events_a; + + var buf = []; + + // old style + tmpl.events = { + 'click b': function () { buf.push('b'); } + }; + + var div = OnscreenDiv(Meteor.render(tmpl)); + clickElement(DomUtils.find(div.node(), 'b')); + test.equal(buf, ['b']); + div.kill(); + Meteor.flush(); + + /// + + tmpl = Template.test_template_events_b; + buf = []; + // new style + tmpl.events({ + 'click u': function () { buf.push('u'); } + }); + tmpl.events({ + 'click i': function () { buf.push('i'); } + }); + + var div = OnscreenDiv(Meteor.render(tmpl)); + clickElement(DomUtils.find(div.node(), 'u')); + clickElement(DomUtils.find(div.node(), 'i')); + test.equal(buf, ['u', 'i']); + div.kill(); + Meteor.flush(); + +}); + +Tinytest.add("templating - #each rendered callback", function (test) { + // test that any list modification triggers a rendered callback on the + // enclosing template + + var entries = new LocalCollection(); + entries.insert({x:'a'}); + entries.insert({x:'b'}); + entries.insert({x:'c'}); + + var buf = []; + + var tmpl = Template.test_template_eachrender_a; + tmpl.helpers({entries: function() { + return entries.find({}, {sort: ['x']}); }}); + tmpl.rendered = function () { + buf.push(canonicalizeHtml( + DomUtils.rangeToHtml(this.firstNode, this.lastNode)).replace(/\s/g, '')); + }; + var div = OnscreenDiv(Meteor.render(tmpl)); + Meteor.flush(); + test.equal(buf, ['

      a
      b
      c
      ']); + buf.length = 0; + + // added + entries.insert({x:'d'}); + test.equal(buf, []); + Meteor.flush(); + test.equal(buf, ['
      a
      b
      c
      d
      ']); + buf.length = 0; + + // removed + entries.remove({x:'a'}); + test.equal(buf, []); + Meteor.flush(); + test.equal(buf, ['
      b
      c
      d
      ']); + buf.length = 0; + + // moved/changed + entries.update({x:'b'}, {$set: {x: 'z'}}); + test.equal(buf, []); + Meteor.flush(); + test.equal(buf, ['
      c
      d
      z
      ', + '
      c
      d
      z
      ']); + buf.length = 0; + + div.kill(); + Meteor.flush(); + + // test pure "moved" + + tmpl = Template.test_template_eachrender_b; + var cbks = []; + var xs = ['a','b','c']; + tmpl.helpers({entries: function() { + return { observe: function (callbacks) { + cbks.push(callbacks); + _.each(xs, function(x, i) { + callbacks.added({x:x}, i); + }); + return { + stop: function () { + cbks = _.without(cbks, callbacks); + } + }; + }}; + }}); + tmpl.rendered = function () { + buf.push(canonicalizeHtml( + DomUtils.rangeToHtml(this.firstNode, this.lastNode)).replace(/\s/g, '')); + }; + buf = []; + var div = OnscreenDiv(Meteor.render(tmpl)); + test.equal(buf, []); + Meteor.flush(); + test.equal(buf, ['
      a
      b
      c
      ']); + buf.length = 0; + + _.each(cbks, function (callbacks) { + callbacks.moved({x:'a'}, 0, 2); + }); + test.equal(buf, []); + Meteor.flush(); + test.equal(div.html().replace(/\s/g, ''), + '
      b
      c
      a
      '); + test.equal(buf, ['
      b
      c
      a
      ']); + buf.length = 0; + + div.kill(); + Meteor.flush(); +}); + +Tinytest.add("templating - landmarks in helpers", function (test) { + var buf = []; + + var R = ReactiveVar('foo'); + + var tmpl = Template.test_template_landmarks_a; + tmpl.LM = function () { + return new Handlebars.SafeString( + Spark.createLandmark({created: function () { buf.push('c'); }, + rendered: function () { buf.push('r'); }, + destroyed: function () { buf.push('d'); }}, + function () { return 'x'; })); + }; + tmpl.v = function () { + return R.get(); + }; + + var div = OnscreenDiv(Meteor.render(tmpl)); + test.equal(div.text().match(/\S+/)[0], 'xxxxfoo'); + Meteor.flush(); + buf.sort(); + test.equal(buf.join(''), 'ccccrrrr'); + buf.length = 0; + + R.set('bar'); + Meteor.flush(); + test.equal(div.text().match(/\S+/)[0], 'xxxxbar'); + test.equal(buf.join(''), 'rrrr'); + buf.length = 0; + + div.kill(); + Meteor.flush(); + test.equal(buf.join(''), 'dddd'); +}); + +Tinytest.add("templating - bare each has no matching", function (test) { + var buf = []; + + var R = ReactiveVar('foo'); + + var tmpl = Template.test_template_bare_each_a; + tmpl.abc = [{}, {}, {}]; + tmpl.LM = function () { + return new Handlebars.SafeString( + Spark.createLandmark({created: function () { buf.push('c'); }, + rendered: function () { buf.push('r'); }, + destroyed: function () { buf.push('d'); }}, + function () { return 'x'; })); + }; + tmpl.v = function () { + return R.get(); + }; + + var div = OnscreenDiv(Meteor.render(tmpl)); + Meteor.flush(); + buf.sort(); + test.equal(buf.join(''), 'cccrrr'); + buf.length = 0; + + R.set('bar'); + Meteor.flush(); + buf.sort(); + test.equal(buf.join(''), 'cccdddrrr'); + buf.length = 0; + + div.kill(); + Meteor.flush(); + test.equal(buf.join(''), 'ddd'); +}); + +Tinytest.add("templating - templates are labeled", function (test) { + var buf = []; + + var R = ReactiveVar('foo'); + + var tmpls = _.map([0,1,2,3], function (n) { + return Template['test_template_labels_a'+n]; + }); + tmpls[0].stuff = function () { + return tmpls[1]() + tmpls[2]() + tmpls[3]() + R.get(); + }; + _.each([tmpls[1], tmpls[2], tmpls[3]], function (tmpl) { + tmpl.preserve(['hr']); + tmpl.created = function () { buf.push('c'); }; + tmpl.rendered = function () { buf.push('r'); }; + tmpl.destroyed = function () { buf.push('d'); }; + }); + + var div = OnscreenDiv(Meteor.render(tmpls[0])); + Meteor.flush(); + test.equal(div.html(), "


      foo"); + buf.sort(); + test.equal(buf.join(''), 'cccrrr'); + buf.length = 0; + + R.set('bar'); + Meteor.flush(); + test.equal(div.html(), "


      bar"); + buf.sort(); + test.equal(buf.join(''), 'rrr'); + buf.length = 0; + + div.kill(); + Meteor.flush(); + test.equal(buf.join(''), 'ddd'); +}); diff --git a/packages/test-helpers/canonicalize_html.js b/packages/test-helpers/canonicalize_html.js index 7680e9031b..b36ba4a238 100644 --- a/packages/test-helpers/canonicalize_html.js +++ b/packages/test-helpers/canonicalize_html.js @@ -1,6 +1,6 @@ var canonicalizeHtml = function(html) { var h = html; - // kill IE-specific comments inserted by Meteor liveui + // kill IE-specific comments inserted by Spark h = h.replace(//g, ''); // ignore exact text of comments h = h.replace(//g, ''); diff --git a/packages/test-helpers/onscreendiv.js b/packages/test-helpers/onscreendiv.js index 5500e9f072..0495f28abc 100644 --- a/packages/test-helpers/onscreendiv.js +++ b/packages/test-helpers/onscreendiv.js @@ -13,7 +13,7 @@ var OnscreenDiv = function(optFrag) { if (! (this instanceof OnscreenDiv)) return new OnscreenDiv(optFrag); - this.div = Meteor.ui._htmlToFragment( + this.div = DomUtils.htmlToFragment( '').firstChild; document.body.appendChild(this.div); @@ -46,12 +46,15 @@ OnscreenDiv.prototype.node = function() { // "fast GC" -- i.e., after the next Meteor.flush() // the DIV will be fully cleaned up by LiveUI. OnscreenDiv.prototype.kill = function() { - // remove DIV from document by putting it in a fragment - var frag = document.createDocumentFragment(); - frag.appendChild(this.div); - // instigate clean-up on next flush() - Meteor.ui._hold(frag); - Meteor.ui._release(frag); + var self = this; + if (self.div.parentNode) + self.div.parentNode.removeChild(self.div); + + var cx = new Meteor.deps.Context; + cx.on_invalidate(function() { + Spark.finalize(self.div); + }); + cx.invalidate(); }; // remove the DIV from the document diff --git a/packages/test-helpers/package.js b/packages/test-helpers/package.js index 463d4829d9..1a0f7c4c91 100644 --- a/packages/test-helpers/package.js +++ b/packages/test-helpers/package.js @@ -6,6 +6,11 @@ Package.describe({ Package.on_use(function (api, where) { where = where || ["client", "server"]; + // XXX These files have various dependencies on other packages + // that aren't specified here. :( + // This package should probably get split into several packages, + // each with correct dependencies. + api.add_files('try_all_permutations.js', where); api.add_files('async_multi.js', where); api.add_files('event_simulation.js', where); @@ -13,7 +18,9 @@ Package.on_use(function (api, where) { api.add_files('canonicalize_html.js', where); api.add_files('stub_stream.js', where); api.add_files('onscreendiv.js', where); + api.add_files('wrappedfrag.js', where); api.add_files('current_style.js', where); + api.add_files('reactivevar.js', where); }); Package.on_test(function (api) { diff --git a/packages/test-helpers/reactivevar.js b/packages/test-helpers/reactivevar.js new file mode 100644 index 0000000000..cf369255fd --- /dev/null +++ b/packages/test-helpers/reactivevar.js @@ -0,0 +1,53 @@ +// ReactiveVar is like a portable Session var. When you get it, +// it registers a dependency, and when it's set, it invalidates +// its dependencies. +// +// When set to a primitive value, invalidation +// is only fired if the new value is !== the old one. When set +// to an object value, invalidation always happens. Each behavior +// may be desirable in different test scenarios. +// body and keeps track of it, providing methods that query it, +// mutate, and destroy it. +// +// Constructor, with optional 'new': +// var R = [new] ReactiveVar([initialValue]) + + +var ReactiveVar = function(initialValue) { + if (! (this instanceof ReactiveVar)) + return new ReactiveVar(initialValue); + + this._value = (typeof initialValue === "undefined" ? null : + initialValue); + this._deps = {}; +}; + +ReactiveVar.prototype.get = function() { + var context = Meteor.deps.Context.current; + if (context && !(context.id in this._deps)) { + this._deps[context.id] = context; + var self = this; + context.on_invalidate(function() { + delete self._deps[context.id]; + }); + } + + return this._value; +}; + +ReactiveVar.prototype.set = function(newValue) { + // detect equality and don't invalidate dependers + // when value is a primitive. + if ((typeof newValue !== 'object') && this._value === newValue) + return; + + this._value = newValue; + + for(var id in this._deps) + this._deps[id].invalidate(); + +}; + +ReactiveVar.prototype.numListeners = function() { + return _.keys(this._deps).length; +}; diff --git a/packages/test-helpers/wrappedfrag.js b/packages/test-helpers/wrappedfrag.js new file mode 100644 index 0000000000..5c45b26dd5 --- /dev/null +++ b/packages/test-helpers/wrappedfrag.js @@ -0,0 +1,46 @@ +// A WrappedFrag provides utility methods pertaining to a given +// DocumentFragment that are helpful in tests. For example, +// WrappedFrag(frag).html() constructs a sort of cross-browser +// innerHTML for the fragment. + +// Constructor, with optional 'new': +// var f = [new] WrappedFrag([frag]) +WrappedFrag = function(frag) { + if (! (this instanceof WrappedFrag)) + return new WrappedFrag(frag); + + this.frag = frag; +}; + +WrappedFrag.prototype.rawHtml = function() { + return DomUtils.fragmentToHtml(this.frag); +}; + +WrappedFrag.prototype.html = function() { + return canonicalizeHtml(this.rawHtml()); +}; + +WrappedFrag.prototype.hold = function() { + // increments frag's GC protection reference count + this.frag["_protect"] = (this.frag["_protect"] || 0) + 1; + return this; +}; + +WrappedFrag.prototype.release = function() { + var frag = this.frag; + // decrement frag's GC protection reference count + // Clean up on flush, if hits 0. Wait to decrement + // so no one else cleans it up first. + var cx = new Meteor.deps.Context; + cx.on_invalidate(function() { + if (! --frag["_protect"]) { + Spark.finalize(frag); + } + }); + cx.invalidate(); + return this; +}; + +WrappedFrag.prototype.node = function() { + return this.frag; +}; diff --git a/packages/test-in-browser/driver.js b/packages/test-in-browser/driver.js index cb0d433877..eb37196ca9 100644 --- a/packages/test-in-browser/driver.js +++ b/packages/test-in-browser/driver.js @@ -116,12 +116,12 @@ Template.test.test_class = function() { return classes.join(' '); }; -Template.test.events = { +Template.test.events({ 'click .testname': function() { this.expanded = ! this.expanded; _resultsChanged(); } -}; +}); Template.test.eventsArray = function() { var events = _.filter(this.events, function(e) { @@ -158,7 +158,7 @@ Template.test.eventsArray = function() { }); }; -Template.event.events = { +Template.event.events({ 'click .debug': function () { // the way we manage groupPath, shortName, cookies, etc, is really // messy. needs to be aggressively refactored. @@ -166,7 +166,7 @@ Template.event.events = { test: this.cookie.shortName}); Meteor._debugTest(this.cookie, reportResults); } -}; +}); Template.event.get_details = function() { var details = this.details; diff --git a/packages/test-in-browser/package.js b/packages/test-in-browser/package.js index 1b2a19e379..963d804624 100644 --- a/packages/test-in-browser/package.js +++ b/packages/test-in-browser/package.js @@ -9,7 +9,7 @@ Package.on_use(function (api) { // that tinytest and the driver both implement? api.use('tinytest'); - api.use(['liveui', 'livedata', 'templating'], 'client'); + api.use(['spark', 'livedata', 'templating'], 'client'); api.add_files([ 'driver.css', diff --git a/packages/universal-events/event_tests.js b/packages/universal-events/event_tests.js new file mode 100644 index 0000000000..5ae1288e18 --- /dev/null +++ b/packages/universal-events/event_tests.js @@ -0,0 +1,83 @@ + +Tinytest.add("universal-events - basic", function(test) { + + var runTest = function (testMissingHandlers) { + var msgs = []; + var listeners = []; + + var createListener = function () { + var out = []; + msgs.push(out); + var ret = new UniversalEventListener(function(event) { + var node = event.currentTarget; + if (DomUtils.elementContains(document.body, node)) { + out.push(event.currentTarget.nodeName.toLowerCase()); + } + }, testMissingHandlers); + listeners.push(ret); + return ret; + }; + + var L1 = createListener(); + + var check = function (event, expected) { + _.each(msgs, function (m) { + m.length = 0; + }); + simulateEvent(DomUtils.find(d.node(), "b"), event); + for (var i = 0; i < listeners.length; i++) + test.equal(msgs[i], testMissingHandlers ? [] : expected[i]); + }; + + var d = OnscreenDiv(Meteor.render("
      Hello
      ")); + L1.addType('mousedown'); + if (!testMissingHandlers) + L1.installHandler(d.node(), 'mousedown'); + var x = ['b', 'span', 'div', 'div']; + check('mousedown', [x]); + + check('mouseup', [[]]); + + L1.removeType('mousedown'); + check('mousedown', [[]]); + L1.removeType('mousedown'); + check('mousedown', [[]]); + + L1.addType('mousedown'); + check('mousedown', [x]); + L1.addType('mousedown'); + check('mousedown', [x]); + L1.removeType('mousedown'); + check('mousedown', [[]]); + + var L2 = createListener(); + if (!testMissingHandlers) + L2.installHandler(d.node(), 'mousedown'); + + L1.addType('mousedown'); + check('mousedown', [x, []]); + L2.addType('mousedown'); + check('mousedown', [x, x]); + L2.addType('mousedown'); + check('mousedown', [x, x]); + L1.removeType('mousedown'); + check('mousedown', [[], x]); + L1.removeType('mousedown'); + check('mousedown', [[], x]); + L2.removeType('mousedown'); + check('mousedown', [[], []]); + L1.addType('mousedown'); + check('mousedown', [x, []]); + L1.removeType('mousedown'); + check('mousedown', [[], []]); + L2.addType('mousedown'); + check('mousedown', [[], x]); + L2.removeType('mousedown'); + check('mousedown', [[], []]); + + d.kill(); + }; + + runTest(false); + runTest(true); +}); diff --git a/packages/liveui/liveevents_now3c.js b/packages/universal-events/events-ie.js similarity index 62% rename from packages/liveui/liveevents_now3c.js rename to packages/universal-events/events-ie.js index 4b043091e1..cb8d4503ed 100644 --- a/packages/liveui/liveevents_now3c.js +++ b/packages/universal-events/events-ie.js @@ -1,7 +1,4 @@ -Meteor.ui = Meteor.ui || {}; -Meteor.ui._event = Meteor.ui._event || {}; - -// LiveEvents implementation for "old IE" versions 6-8, which lack +// Universal Events implementation for IE versions 6-8, which lack // addEventListener and event capturing. // // The strategy is very different. We walk the subtree in question @@ -22,56 +19,97 @@ Meteor.ui._event = Meteor.ui._event || {}; // events on checkboxes and radio buttons immediately rather than // only when the user blurs them, another old IE quirk. -Meteor.ui._event._loadNoW3CImpl = function() { +UniversalEventListener._impl = UniversalEventListener._impl || {}; + +// Singleton +UniversalEventListener._impl.ie = function (deliver) { + var self = this; + this.deliver = deliver; + this.curriedHandler = function () { + self.handler.call(this, self); + }; + + // The 'submit' event on IE doesn't bubble. We want to simulate + // bubbling submit to match other browsers, and to do that we use + // IE's own event machinery. We can't dispatch events with arbitrary + // names in IE, so we appropriate the obscure "datasetcomplete" event + // for this purpose. + document.attachEvent('ondatasetcomplete', function () { + var evt = window.event; + var target = evt && evt.srcElement; + if (evt.synthetic && target && + target.nodeName === 'FORM' && + evt.returnValue !== false) + // No event handler called preventDefault on the simulated + // submit event. That means the form should be submitted. + target.submit(); + }); +}; + +_.extend(UniversalEventListener._impl.ie.prototype, { + addType: function (type) { + // not necessary for IE + }, + + removeType: function (type) { + // not necessary for IE + }, + + installHandler: function (node, type) { + // use old-school event binding, so that we can + // access the currentTarget as `this` in the handler. + // note: handler is never removed from node + var prop = 'on' + type; + + if (node.nodeType === 1) { // ELEMENT + this._install(node, prop); + + // hopefully fast traversal, since the browser is doing it + var descendents = node.getElementsByTagName('*'); + + for(var i=0, N = descendents.length; i