Skip to content

Instantly share code, notes, and snippets.

@jbmartin
Last active December 21, 2015 07:48
Show Gist options
  • Save jbmartin/6273497 to your computer and use it in GitHub Desktop.
Save jbmartin/6273497 to your computer and use it in GitHub Desktop.
This gist provides an API that wraps and extends d3's svg/xml import methods. This API aims to avoid callback nesting by wrapping D3's async xml calls in promises (see jQuery 1.8+ docs). This approach allows users to ignore common async issues because promises are resolved internally. In sum, this is really just syntactic sugar for d3's [xhr obj…
// Requires d3 3.2+ and jQuery 1.8+
// CONTRACT
// void -> SVG
//
// PURPOSE
// This API both abstracts and extends d3's svg/xml import methods. The goal is to avoid
// callback nesting by wrapping d3's async xml calls in a promise, and
// then resolving them internally (see jQuery 1.8+ AJAX specs). All
// methods return `this` to allow for method cascading.
//
// EXAMPLES
// myStimulus = SVG().load("static/svg/stimulus.svg").appendTo("#stimulus");
// myStimulus.on('mouseover', function(){ console.log('mouseover!'); })
// myStimulus.layer('#layer1').css('fill', 'red').css('width', '100px');
//
// TODO
// Inheret either jQuery or d3's prototype(s), making all select/manipulate
// methods available. Note, inherited methods would need to be wrapped in a
// $.done/$.when call to not break current async flow. Alternatively, make SVG a
// d3 plugin (same async issues apply). See
// http://stackoverflow.com/questions/13983864/how-to-make-a-d3-plugin
var SVG = function() {
// CONTRACT
// attachAsyncJQueryMethod :: SVG, String -> SVG
//
// PURPOSE
// TO extend SVG objects with async jQuery methods.
//
// EXAMPLES: attachAsyncJQueryMethod(css)
function attachAsyncJQueryMethod(SVG, method) {
args = getArgs()
return function() { // Anon f'n returned on invocation
$.when(that.loadingRootSVGNode, that.loadingLayer).done(function() {
attachMethod(method, args);
}, null);
return that
}
}
// CONTRACT
// getArgs :: Arguments -> [a]
//
// PURPOSE
// TO convert arguments to an array. According to dev.mozilla: The
// arguments object is not an Array. It is similar to an Array, but does
// not have any Array properties except length. For example, it does not
// have the pop method. However it can be converted to a real Array.
//
// EXAMPLES
//
function getArgs() {
return Array.prototype.slice.apply(arguments);
}
// CONTRACT
// attachMethodWhenReady :: String, [a] -> void
//
// PURPOSE
//
// EXAMPLES
//
function attachMethodWhenReady(method, args) {
ifSVGLoaded(attachMethod, method, args);
}
// CONTRACT
// attachMethod :: String, [a] -> void
//
// PURPOSE
//
// EXAMPLES
//
function attachMethod(method, args) {
$(that.el)[method](args[0], args[1]); // TODO(): Make n-ary.
}
// CONTRACT
// ifSVGLoadedCall :: (a -> b) -> c
//
// PURPOSE
// TO run callbacks on SVG object once it's loaded.
//
// EXAMPLES
// function sum() {
// var total = 0,
// args = getArgs();
// $.each(args, function(){ total += this; });
// }
//
// // Loaded
// ifSVGLoadedCall(sum, 2, 2)
// >> 4
//
// //Not loaded
// ifSVGLoadedCall(sum, 2, 2)
// >> Exception Missing SVG element. Please load before appending.
function ifSVGLoadedCall(fn) {
isNodeImported = that.importedNode;
if (isNodeImported) {
throw new Error("Missing SVG element. Please load before appending.");
} else {
fn(getArgs())
}
}
return { // This is the object that will be returned when SVG is invoked.
el: null, // DOM element associated with SVG.
importedNode: null, // String containing SVG script.
loadingRootSVGNode: null, // Promise (thunk) container for async loading SVG root node.
loadingSVGLayer: null, // Promise (thunk) container for async loading SVG layer node.
// CONTRACT
// String -> SVG
//
// PURPOSE
// TO asynchronously pull SVG file from URL, place it in importedNode
// member, and then resolve promise.
//
// EXAMPLES
load: function(url) {
var that = this; // Work around for Javascript's function scoping.
that.loadingRootSVGNode = new $.Deferred(); // Create a new promise
d3.xml(url, "image/svg+xml", function(xml) {
that.importedNode = document.importNode(xml.documentElement, true);
that.loadingRootSVGNode.resolve();
});
return that
},
setElement: function(el) {
this.el = el;
},
// CONTRACT
// appendTo :: String -> SVG
//
// PURPOSE
// Check if SVG has been loaded, if so append code to `el`,
// else wait. El is a standard jQuery selector string ('#svg',
// '.svg', etc.).
//
// EXAMPLES
//
appendTo: function(el) {
var that = this; // Work around for Javascript's function scoping.
setElement(el);
function appendLayer(node) {
d3.select(el).node().appendLayer(node);
}
ifSVGLoadedCall(appendLayer, that.importedNode)
return that
},
// CONTRACT
// layer :: String -> SVG
//
// PURPOSE
// Selector for SVG layers (nodes). Useful for binding events and other
// jQuery/d3 events. `selector` must be a proper jQuery selector
// ('#item', '.items', etc.). The example assumes a SVG element has
// already been loaded.
//
// EXAMPLES
// myStimulus.layer('#layer').css('fill', 'red');
layer: function(selector) {
var that = this; // Work around for Javascript's function scoping.
that.loadingSVGLayer = new $.Deferred();
that.loadingRootSVGNode.done(function() {
ifSVGLoadedCall()
that.el = selector
that.loadingSVGLayer.resolve();
});
return that
},
// Attach async jQuery methods.
// TODO(): Add more jQuery and d3 methods.
// TODO(): Cascading method calls (e.g., .css('...').on('...'), etc.)
// are blocking. Fix.
on: attachAsyncJQueryMethod('on'),
css: attachAsyncJQueryMethod('css'),
attr: attachAsyncJQueryMethod('attr')
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment