/*\ title: $:/plugins/tiddlywiki/d3/barwidget.js type: application/javascript module-type: widget A widget for displaying stacked or grouped bar charts. Derived from http://bl.ocks.org/mbostock/3943967 \*/ (function(){ /*jslint node: true, browser: true */ /*global $tw: false */ "use strict"; var Widget = require("$:/core/modules/widgets/widget.js").widget, d3 = require("$:/plugins/tiddlywiki/d3/d3.js").d3; var BarWidget = function(parseTreeNode,options) { this.initialise(parseTreeNode,options); }; /* Inherit from the base widget class */ BarWidget.prototype = new Widget(); /* Render this widget into the DOM */ BarWidget.prototype.render = function(parent,nextSibling) { // Save the parent dom node this.parentDomNode = parent; // Compute our attributes this.computeAttributes(); // Execute our logic this.execute(); // Create the chart var chart = this.createChart(parent,nextSibling); this.updateChart = chart.updateChart; if(this.updateChart) { this.updateChart(); } // Insert the chart into the DOM and render any children parent.insertBefore(chart.domNode,nextSibling); this.domNodes.push(chart.domNode); }; BarWidget.prototype.createChart = function(parent,nextSibling) { // Get the data we're plotting var data = this.wiki.getTiddlerData(this.barData), n,m,stack,layers; if(data) { n = data.layers; m = data.samples; layers = data.data; } else { // Use randomly generated data if we don't have any n = 4; // number of layers m = 58; // number of samples per layer stack = d3.layout.stack(); layers = stack(d3.range(n).map(function() { return bumpLayer(m, .1); })); } // Calculate the maximum data values var yGroupMax = d3.max(layers, function(layer) { return d3.max(layer, function(d) { return d.y; }); }), yStackMax = d3.max(layers, function(layer) { return d3.max(layer, function(d) { return d.y0 + d.y; }); }); // Calculate margins and width and height var margin = {top: 40, right: 10, bottom: 20, left: 10}, width = 960 - margin.left - margin.right, height = 500 - margin.top - margin.bottom; // x-scale var x = d3.scale.ordinal() .domain(d3.range(m)) .rangeRoundBands([0, width], .08); // y-scale var y = d3.scale.linear() .domain([0, yStackMax]) .range([height, 0]); // Array of colour values var color = d3.scale.linear() .domain([0, n - 1]) .range(["#aad", "#556"]); // x-axis var xAxis = d3.svg.axis() .scale(x) .tickSize(0) .tickPadding(6) .orient("bottom"); // Create SVG element var svgElement = d3.select(parent).insert("svg",nextSibling) .attr("viewBox", "0 0 960 500") .attr("preserveAspectRatio", "xMinYMin meet") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom); // Create main group var mainGroup = svgElement.append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); // Create the layers var layer = mainGroup.selectAll(".layer") .data(layers) .enter().append("g") .attr("class", "layer") .style("fill", function(d, i) { return color(i); }); // Create the rectangles in each layer var rect = layer.selectAll("rect") .data(function(d) { return d; }) .enter().append("rect") .attr("x", function(d) { return x(d.x); }) .attr("y", height) .attr("width", x.rangeBand()) .attr("height", 0); // Transition the rectangles to their final height rect.transition() .delay(function(d, i) { return i * 10; }) .attr("y", function(d) { return y(d.y0 + d.y); }) .attr("height", function(d) { return y(d.y0) - y(d.y0 + d.y); }); // Add to the DOM mainGroup.append("g") .attr("class", "x axis") .attr("transform", "translate(0," + height + ")") .call(xAxis); var self = this; // Return the svg node return { domNode: svgElement[0][0], updateChart: function() { if (self.barGrouped !== "no") { transitionGrouped(); } else { transitionStacked(); } } }; function transitionGrouped() { y.domain([0, yGroupMax]); rect.transition() .duration(500) .delay(function(d, i) { return i * 10; }) .attr("x", function(d, i, j) { return x(d.x) + x.rangeBand() / n * j; }) .attr("width", x.rangeBand() / n) .transition() .attr("y", function(d) { return y(d.y); }) .attr("height", function(d) { return height - y(d.y); }); } function transitionStacked() { y.domain([0, yStackMax]); rect.transition() .duration(500) .delay(function(d, i) { return i * 10; }) .attr("y", function(d) { return y(d.y0 + d.y); }) .attr("height", function(d) { return y(d.y0) - y(d.y0 + d.y); }) .transition() .attr("x", function(d) { return x(d.x); }) .attr("width", x.rangeBand()); } // Inspired by Lee Byron's test data generator. function bumpLayer(n, o) { function bump(a) { var x = 1 / (.1 + Math.random()), y = 2 * Math.random() - .5, z = 10 / (.1 + Math.random()); for (var i = 0; i < n; i++) { var w = (i / n - y) * z; a[i] += x * Math.exp(-w * w); } } var a = [], i; for (i = 0; i < n; ++i) a[i] = o + o * Math.random(); for (i = 0; i < 5; ++i) bump(a); return a.map(function(d, i) { return {x: i, y: Math.max(0, d)}; }); } }; /* Compute the internal state of the widget */ BarWidget.prototype.execute = function() { // Get the parameters from the attributes this.barData = this.getAttribute("data"); this.barGrouped = this.getAttribute("grouped","no"); }; /* Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering */ BarWidget.prototype.refresh = function(changedTiddlers) { var changedAttributes = this.computeAttributes(); if(changedAttributes.data || changedTiddlers[this.barData]) { this.refreshSelf(); return true; } else if(changedAttributes.grouped) { this.execute(); if(this.updateChart) { this.updateChart(); } return true; } return false; }; /* Remove any DOM nodes created by this widget or its children */ BarWidget.prototype.removeChildDomNodes = function() { $tw.utils.each(this.domNodes,function(domNode) { domNode.parentNode.removeChild(domNode); }); this.domNodes = []; }; exports.d3bar = BarWidget; })();