planetary.js/documentation/plugins.html
Brandon Tilley 829c8a60b5 Update site
2013-12-25 23:25:56 -08:00

243 lines
12 KiB
HTML

<!doctype html>
<html>
<head>
<title>Planetary.js: Awesome interactive globes for the web</title>
<link href='http://fonts.googleapis.com/css?family=Source+Sans+Pro:400,700|Open+Sans:300italic,400,300,700' rel='stylesheet' type='text/css'>
<link type="text/css" rel="stylesheet" href="/semantic/css/semantic.min.css">
<link type="text/css" rel="stylesheet" href="/css/planetaryjs.css">
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible">
<link href="/css/prism.css" rel="stylesheet">
</head>
<body>
<div class='ui fixed inverted large menu main-menu'>
<div class='items'>
<a class='item title' href='/'>
<i class='globe icon'></i>Planetary.js
</a>
<span class='spacer hide-on-mobile'></span>
<a class='item minor ' href='/download/'>
<i class='download icon'></i><span class='hide-on-mobile'>Download</span>
</a>
<a class='item minor ' href='/examples/'>
<i class='laptop icon'></i><span class='hide-on-mobile'>Examples</span>
</a>
<a class='item minor active' href='/documentation/'>
<i class='book icon'></i><span class='hide-on-mobile'>Documentation</span>
</a>
<a class='item minor' href='https://github.com/BinaryMuse/planetary.js'>
<i class='github alternate icon'></i><span class='hide-on-mobile'>Fork on GitHub</span>
</a>
</div>
</div>
<div class='content container'>
<div class='page ui slim stackable grid'>
<div class='four wide column'>
<div class='ui fluid vertical menu'>
<a class='red item ' href='/documentation/index.html'>
Introduction
<i class='icon home'></i>
</a>
<a class='blue item ' href='/documentation/core.html'>
Core API
<i class='icon setting'></i>
</a>
<a class='orange item ' href='/documentation/planet.html'>
Planet API
<i class='icon globe'></i>
</a>
<a class='teal item active' href='/documentation/plugins.html'>
Plugins
<i class='icon edit'></i>
</a>
<a class='purple item with-subitems ' href='/documentation/builtin.html'>
Built-In Plugins
<i class='icon bolt'></i>
</a>
<div class='item contains-subitems'>
<div class='menu'>
<a class='item ' href='/documentation/builtin_earth.html'>Earth</a>
<a class='item ' href='/documentation/builtin_topojson.html'>TopoJSON</a>
<a class='item ' href='/documentation/builtin_oceans.html'>Oceans</a>
<a class='item ' href='/documentation/builtin_land.html'>Land</a>
<a class='item ' href='/documentation/builtin_borders.html'>Borders</a>
<a class='item ' href='/documentation/builtin_pings.html'>Pings</a>
<a class='item ' href='/documentation/builtin_zoom.html'>Zoom</a>
<a class='item ' href='/documentation/builtin_drag.html'>Drag</a>
</div>
</div>
<a class='red item ' href='/documentation/faq.html'>
FAQ
<i class='icon help'></i>
</a>
<a class='green item ' href='/documentation/help.html'>
Getting Help
<i class='icon phone'></i>
</a>
</div>
</div>
<div class='twelve wide column'>
<h1>Plugins</h1>
<p>Planetary.js uses a plugin-based architecture, and all the built-in functionality is built using this architecture. This makes Planetary.js extremely flexible.</p>
<h2>Loading Plugins</h2>
<p>Plugins are loaded either globally by <code>planetaryjs.loadPlugin</code> or for a specific planet instance by <code>planet.loadPlugin</code>. If you call <code>draw</code> on a planet and it has no plugins loaded at all (from either source), Planetary.js will use the default plugin stack, which consists of the <code>earth</code> and <code>pings</code> plugins.</p>
<h2>Anatomy of a Plugin</h2>
<p>A plugin is simply a JavaScript function that takes a planet instance as a parameter and performs some operation on it. <strong>The best plugins do one tiny thing.</strong> If you want a plugin to do a lot of things at once, you should build a plugin that wraps other, smaller plugins; in fact, this is exactly how the <code>earth</code> plugin is built. See the <a href="/documentation/builtin_earth.html">Earth Plugin documentation</a> for more details.</p>
<p>Most of the time, a plugin will implement its behavior by registering callbacks into a planet&#39;s lifecycle hooks. For example, the following simple plugin increments the planet&#39;s projection&#39;s rotation by one degree every tick (this would make for a very fast spinning globe, but demonstrates the idea nicely enough):</p>
<div class='ui raised segment'>
<div class='ui red ribbon label'>JavaScript</div>
<pre><code class="language-javascript">var autorotate = function(planet) {
planet.onDraw(function() {
var rotation = planet.projection.rotate();
rotation[0] += 1;
if (rotation[0] &gt;= 180) rotation[0] -= 360;
planet.projection.rotate(rotation);
});
};
planet.loadPlugin(autorotate);</code></pre>
<p></div></p>
<h2>Configurable Plugins</h2>
<p>Often, you&#39;ll want your plugin to be configurable with some user-defined values. In this case, you can create a higher-order function, which takes your configuration data and then <em>returns</em> the plugin function. You can then call this function to generate the plugin for use by <code>loadPlugin</code>.</p>
<div class='ui raised segment'>
<div class='ui red ribbon label'>JavaScript</div>
<pre><code class="language-javascript">var autorotate = function(degreesPerTick) {
return function(planet) {
planet.onDraw(function() {
var rotation = planet.projection.rotate();
rotation[0] += degreesPerTick;
if (rotation[0] &gt;= 180) rotation[0] -= 360;
planet.projection.rotate(rotation);
});
};
};
planet.loadPlugin(autorotate(5));</code></pre>
<p></div></p>
<h2>Setting Yourself Up</h2>
<p>If you need to do some work before your plugin is ready to be used, you can add a hook to a planet&#39;s <code>onInit</code> hook to do the necessary setup.</p>
<div class='ui raised segment'>
<div class='ui red ribbon label'>JavaScript</div>
<pre><code class="language-javascript">var somePlugin = function(planet) {
planet.onInit(function() {
doSomeSetupWork();
});
};</code></pre>
<p></div></p>
<p>If you need to do some asynchronous setup--such as fetching data with an Ajax request--before your plugin is ready, you can accept an argument to your <code>onInit</code> function. This argument is a function that tells Planetary.js when you&#39;re done setting up; simply call this function after your asynchronous operations are complete and Planetary.js will continue to initialize the planet. <strong>If you accept the parameter but don&#39;t call it, the initialization process will stop</strong> (and your planet will not work).</p>
<div class='ui raised segment'>
<div class='ui red ribbon label'>JavaScript</div>
<pre><code class="language-javascript">var somePlugin = function(planet) {
planet.onInit(function(done) {
doSomeAsynchronousSetupWork(function() {
done();
});
});
};</code></pre>
<p></div></p>
<h2>Drawing on the Canvas</h2>
<p>Many plugins will want to draw onto the globe&#39;s canvas; you can do so by registering a function to a planet&#39;s <code>onDraw</code> hook.</p>
<div class='ui raised segment'>
<div class='ui red ribbon label'>JavaScript</div>
<pre><code class="language-javascript">var somePlugin = function(planet) {
planet.onDraw(function() {
planet.withSavedContext(function(context) {
context.beginPath();
planet.path.context(context)({type: &#39;Sphere&#39;});
context.fillStyle = &#39;black&#39;;
context.fill();
});
});
};</code></pre>
<p></div></p>
<p>The planet exposes properties and methods, such as <code>context</code>, <code>path</code>, and <code>withSavedContext</code> to assist with drawing to the canvas. The <a href="/documentation/planet.html">Planet API documentation</a> goes into more detail on individual properties.</p>
<h3>Drawing Geo Paths</h3>
<p>As explained in the <code>planet.path</code> documentation on the <a href="/documentation/planet.html">Planet API page</a>, <code>planet.path</code> is a <a href="https://github.com/mbostock/d3/wiki/Geo-Paths"><code>d3.geo.path</code></a> object that can be used to draw geographical geometry onto the canvas. The path will take care of transforming the coordinates to be projected onto the orthographic view of the globe.</p>
<p>As a demonstration of this technique, the following is a plugin that will take the land data from a TopoJSON data source (stored on <code>planet.plugins.topojson.world</code>), convert it to a GeoJSON feature, and draw it on the planet. This code is similar to (but slightly simplified from) the <a href="/documentation/builtin_land.html">Land plugin&#39;s</a> implementation.</p>
<div class='ui raised segment'>
<div class='ui red ribbon label'>JavaScript</div>
<pre><code class="language-javascript">var drawLand = function(planet) {
planet.onDraw(function() {
planet.withSavedContext(function(context) {
var world = planet.plugins.topojson.world;
var land = topojson.feature(world, world.objects.land);
context.beginPath();
planet.path.context(context)(land);
context.fillStyle = &#39;white&#39;;
context.fill();
});
});
};</code></pre>
<p></div></p>
<h2>Exposing Data and Methods</h2>
<p>Obviously, you can use private internal variables to keep track of any data your plugin needs in order to operate. However, if you want to expose a public API to users of your plugin, you should avoid attaching them directly to the planet and instead attach them to the planet&#39;s <code>plugins</code> namespace. You should use a name specific to your plugin, and this name should be well documented in your plugin&#39;s documentation.</p>
<div class='ui raised segment'>
<div class='ui red ribbon label'>JavaScript</div>
<pre><code class="language-javascript">var autorotate = function(degreesPerTick) {
return function(planet) {
var paused = false;
// Attach our public API to `planet.plugins`
// on the `autorotate` namespace.
planet.plugins.autorotate = {
pause: function() { paused = true; },
resume: function() { paused = false; }
};
planet.onDraw(function() {
if (paused) return;
var rotation = planet.projection.rotate();
rotation[0] += degreesPerTick;
if (rotation[0] &gt;= 180) rotation[0] -= 360;
planet.projection.rotate(rotation);
});
};
};
planet.loadPlugin(autorotate(5));
planet.draw(canvas);
setTimeout(function() {
planet.plugins.autorotate.pause();
}, 5000);</code></pre>
<p></div></p>
<h2>Best Practices</h2>
<p>There are a few things you can do to make your plugin all it can be:</p>
<ol>
<li>Make your plugin very small; ideally, it should do only <em>one thing</em> very well. Be extremely liberal with splitting plugins into smaller plugins, which makes them easier to understand, test, and compose. It&#39;s easy to say &quot;this plugin renders the Earth,&quot; but it really renders oceans, land masses, and borders.</li>
<li>Use higher-order functions to generate your plugin (as described above in &quot;Plugin Generators&quot;) instead of passing function references to <code>loadPlugin</code> directly, even if your plugin doesn&#39;t take any configuration options. It makes for a more consistent API, and allows you to more easily add the ability to specify configuration options in the future.</li>
<li>Make configuration optional if at all possible. Write your plugin so that it checks for missing values and uses sensible defaults.</li>
<li>Only publish public data and API methods to <code>planet.plugins.pluginName</code>, where <code>pluginName</code> is the name of your plugin.</li>
</ol>
</div>
</div>
</div>
<script type='text/javascript' src='/js/prism.js'></script>
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-46705270-1', 'planetaryjs.com');
ga('send', 'pageview');
</script>
</body>
</html>