// run the demo run(d3.select("body"), 1000); /* * build and bind elements, then set a timer to generate data and update */ function run($context, step) { // setup repeating elements buildRepeatingElements($context); // grab bindable elements on the page var $elements = d3.selectAll("[data-channel]"), keyElements = d3.selectAll("[data-key]").text("-"), channels = []; // get the specified channels $elements.each(function() { channels.push(d3.select(this).attr('data-channel')); }); // set data for all elements //update($elements, generateData(channels, channels.length)); // setup a timer to generate period, random updates, IRL this would be a socket message window.setInterval(function() { update($elements, generateData(channels)); }, step); } /* * build out repeating channel elements */ function buildRepeatingElements($context) { var repeatingElements = $context.selectAll("[data-channels]"), elem, clone, fn; repeatingElements.each(function() { elem = d3.select(this), fn = elem.attr("data-callback"); // duplicate elem.attr("data-channels").split(",").reverse().forEach(function(m, i) { clone = cloneSelection(elem).attr("data-channels", null).attr("data-channel", m).selectAll("td").text("-"); if (typeof window[fn] === "function") clone.attr("data-callback", fn); }); // remove original elem.remove(); }); } /* * update elements based on incoming data * @debug make this more d3-like * @debug cache the elements that require callbacks and formatters */ function update(elements, data) { var $this, elem, fn; elements .data(data, elemKeyFunc) .each(function(d, i) { $this = d3.select(this); // handle callbacks fn = $this.attr("data-callback"); if (fn && (typeof window[fn] === "function")) window[fn]($this, d); // handle single-element updates fmt = $this.attr("data-formatter"); if ($this.attr("data-key")) { // handle formatters if (fmt && (typeof formatters[fmt] === "function")) { $this.text(formatters[fmt](d[$this.attr("data-key")])); } else { $this.text(d[$this.attr("data-key")]); } } }) // @debug reselecting here is inefficient; better way? .selectAll("[data-key]") .each(function(d, i) { elem = d3.select(this); fn = elem.attr("data-callback"), fmt = elem.attr("data-formatter"); // handle callbacks; run before the value is set so callbacks can operate on data if necessary if (fn && (typeof window[fn] === "function")) window[fn](elem, d3.select(this.parentNode).datum()); // handle formatters if (fmt && (typeof formatters[fmt] === "function")) { elem.text(formatters[fmt](d3.select(this.parentNode).datum()[elem.attr("data-key")])); } else { elem.text(d3.select(this.parentNode).datum()[elem.attr("data-key")]); } }); } /* * Data join key function for elements */ function elemKeyFunc(d, i) { return d ? d.channel : d3.select(this).attr("data-channel"); } /* * Return simulated data for some of the channels */ function generateData(channels, count) { var data = []; count = (typeof count == "undefined") ? Math.floor(Math.random() * channels.length) : count; d3.shuffle(channels.slice(0)).slice(Math.floor(Math.random() * channels.length)).forEach(function(channel) { data.push({ "channel": channel, "value": Math.random() * 1000, "time": new Date().valueOf() }); }); return data; } /* * simple selection cloning function */ function cloneSelection($elem) { var node = $elem.node(); return d3.select(node.parentNode.insertBefore(node.cloneNode(true), node.nextSibling)); } /** * callback to set system status class */ function statusCls($elem, data) { $elem.attr("class", (data.value > 500) ? "nominal" : (data.value > 300) ? "marginal" : "failed"); } /** * custom formatters */ var formatters = { "iso": function(value, data) { return d3.time.format.iso(new Date(value)); }, "roundThree": function(value, data) { return d3.round(value, 3); }, "translateStatus": function(value, data) { return (value > 500) ? "nominal" : (value > 300) ? "marginal" : "failed"; }, "wrapParen": function(value, data) { return "(" + Math.floor(value) + ")"; } }