diff --git a/nbld.sh b/nbld.sh index 2ee7e4e08..53869018c 100755 --- a/nbld.sh +++ b/nbld.sh @@ -37,6 +37,14 @@ node ./tiddlywiki.js \ --new_rendertiddler $:/core/templates/tiddlywiki5.template.html $TW5_BUILD_OUTPUT/encrypted.html text/plain \ || exit 1 +# Fifth, d3demo.html: wiki to demo d3 plugin + +node ./tiddlywiki.js \ + ./editions/d3demo \ + --verbose \ + --new_rendertiddler $:/core/templates/tiddlywiki5.template.html $TW5_BUILD_OUTPUT/d3demo.html text/plain \ + || exit 1 + # Run tests ./test.sh diff --git a/plugins/tiddlywiki/d3/barwidget.js b/plugins/tiddlywiki/d3/barwidget.js index b2aff30ef..bb24e6205 100644 --- a/plugins/tiddlywiki/d3/barwidget.js +++ b/plugins/tiddlywiki/d3/barwidget.js @@ -1,7 +1,7 @@ /*\ title: $:/plugins/tiddlywiki/d3/barwidget.js type: application/javascript -module-type: widget +module-type: new_widget A widget for displaying stacked or grouped bar charts. Derived from http://bl.ocks.org/mbostock/3943967 @@ -12,104 +12,94 @@ A widget for displaying stacked or grouped bar charts. Derived from http://bl.oc /*global $tw: false */ "use strict"; -var d3 = require("$:/plugins/tiddlywiki/d3/d3.js").d3; +var Widget = require("$:/core/modules/new_widgets/widget.js").widget, + d3 = require("$:/plugins/tiddlywiki/d3/d3.js").d3; -var BarWidget = function(renderer) { - // Save state - this.renderer = renderer; - // Generate child nodes - this.generate(); +var BarWidget = function(parseTreeNode,options) { + this.initialise(parseTreeNode,options); }; -BarWidget.prototype.generate = function() { - // Get the parameters - this.data = this.renderer.getAttribute("data"); - this.grouped = this.renderer.getAttribute("grouped","no"); - // Set the return element - this.tag = "div"; - this.attributes = { - "class": "tw-barwidget" - }; -}; +/* +Inherit from the base widget class +*/ +BarWidget.prototype = new Widget(); -BarWidget.prototype.postRenderInDom = function() { - this.updateChart = this.createChart(); -}; - -BarWidget.prototype.refreshInDom = function(changedAttributes,changedTiddlers) { - // Reexecute the widget if the data reference attributes have changed - if(changedAttributes.data || changedTiddlers[this.data]) { - // Regenerate and rerender the widget and replace the existing DOM node - this.generate(); - var oldDomNode = this.renderer.domNode, - newDomNode = this.renderer.renderInDom(); - oldDomNode.parentNode.replaceChild(newDomNode,oldDomNode); - } else if(changedAttributes.grouped) { - // Update the chart if the grouping setting has changed - this.grouped = this.renderer.getAttribute("grouped","no"); - if(this.updateChart) { - this.updateChart(); - } +/* +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() { - - var n,m,stack,layers; - +BarWidget.prototype.createChart = function(parent,nextSibling) { // Get the data we're plotting - var data = this.renderer.renderTree.wiki.getTiddlerData(this.data); + var data = this.wiki.getTiddlerData(this.barData), + n,m,stack,layers; if(data) { n = data.layers; m = data.samples; layers = data.data; - } else { + } 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"); - - var svg = d3.select(this.renderer.domNode).append("svg") + // 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) - .append("g") + .attr("height", height + margin.top + margin.bottom); + // Create main group + var mainGroup = svgElement.append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); - - var layer = svg.selectAll(".layer") + // 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") @@ -117,33 +107,31 @@ BarWidget.prototype.createChart = function() { .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); }); - - svg.append("g") + // Add to the DOM + mainGroup.append("g") .attr("class", "x axis") .attr("transform", "translate(0," + height + ")") .call(xAxis); - - var self = this, - updateChart = function() { - if (self.grouped !== "no") { - transitionGrouped(); - } else { - transitionStacked(); - } + var self = this; + // Return the svg node + return { + domNode: svgElement[0][0], + updateChart: function() { + if (self.barGrouped !== "no") { + transitionGrouped(); + } else { + transitionStacked(); + } + } }; - // Update the chart according to the grouped setting - updateChart(); - // Return the update function - return updateChart; function transitionGrouped() { y.domain([0, yGroupMax]); - rect.transition() .duration(500) .delay(function(d, i) { return i * 10; }) @@ -156,7 +144,6 @@ BarWidget.prototype.createChart = function() { function transitionStacked() { y.domain([0, yStackMax]); - rect.transition() .duration(500) .delay(function(d, i) { return i * 10; }) @@ -169,7 +156,6 @@ BarWidget.prototype.createChart = function() { // 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, @@ -179,13 +165,48 @@ BarWidget.prototype.createChart = function() { 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; diff --git a/plugins/tiddlywiki/d3/cloudwidget.js b/plugins/tiddlywiki/d3/cloudwidget.js index fa2160754..e2bf1d7b6 100644 --- a/plugins/tiddlywiki/d3/cloudwidget.js +++ b/plugins/tiddlywiki/d3/cloudwidget.js @@ -1,7 +1,7 @@ /*\ title: $:/plugins/tiddlywiki/d3/cloudwidget.js type: application/javascript -module-type: widget +module-type: new_widget A widget for displaying word clouds. Derived from https://github.com/jasondavies/d3-cloud @@ -12,7 +12,8 @@ A widget for displaying word clouds. Derived from https://github.com/jasondavies /*global $tw: false */ "use strict"; -var d3 = require("$:/plugins/tiddlywiki/d3/d3.js").d3; +var Widget = require("$:/core/modules/new_widgets/widget.js").widget, + d3 = require("$:/plugins/tiddlywiki/d3/d3.js").d3; if($tw.browser) { // Frightful hack to give the cloud plugin the global d3 variable it needs @@ -20,98 +21,121 @@ if($tw.browser) { d3.layout.cloud = require("$:/plugins/tiddlywiki/d3/d3.layout.cloud.js").cloud; } -var CloudWidget = function(renderer) { - // Save state - this.renderer = renderer; - // Generate child nodes - this.generate(); +var CloudWidget = function(parseTreeNode,options) { + this.initialise(parseTreeNode,options); }; -CloudWidget.prototype.generate = function() { - // Get the parameters - this.data = this.renderer.getAttribute("data"); - this.spiral = this.renderer.getAttribute("spiral","archimedean"); - // Set the return element - this.tag = "div"; - this.attributes = { - "class": "tw-cloudwidget" +/* +Inherit from the base widget class +*/ +CloudWidget.prototype = new Widget(); + +/* +Render this widget into the DOM +*/ +CloudWidget.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); +}; + +CloudWidget.prototype.createChart = function(parent,nextSibling) { + var self = this, + fill = d3.scale.category20(), + data = this.wiki.getTiddlerData(this.cloudData); + // Use dummy data if none provided + if(!data) { + data = "This word cloud does not have any data in it".split(" ").map(function(d) { + return {text: d, size: 10 + Math.random() * 90}; + }); + } + // Create the svg element + var svgElement = d3.select(parent).insert("svg",nextSibling) + .attr("width", 600) + .attr("height", 400); + // Create the main group + var mainGroup = svgElement + .append("g") + .attr("transform", "translate(300,200)"); + // Create the layout + var layout = d3.layout.cloud().size([600, 400]) + .words(data) + .padding(5) + .rotate(function() { return ~~(Math.random() * 5) * 30 - 60; }) + .font("Impact") + .fontSize(function(d) { return d.size*2; }) + .on("end", draw) + .start(); + // Function to draw all the words + function draw(words) { + mainGroup.selectAll("text") + .data(words) + .enter().append("text") + .style("font-size", function(d) { return d.size + "px"; }) + .style("font-family", "Impact") + .style("fill", function(d, i) { return fill(i); }) + .attr("text-anchor", "middle") + .attr("transform", function(d) { + return "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")"; + }) + .text(function(d) { return d.text; }); + } + function updateChart() { + layout.spiral(self.spiral); + } + return { + domNode: svgElement[0][0], + updateChart: updateChart }; }; -CloudWidget.prototype.postRenderInDom = function() { - this.updateChart = this.createChart(); +/* +Compute the internal state of the widget +*/ +CloudWidget.prototype.execute = function() { + // Get the parameters from the attributes + this.cloudData = this.getAttribute("data"); + this.cloudSpiral = this.getAttribute("spiral","archimedean"); }; -CloudWidget.prototype.refreshInDom = function(changedAttributes,changedTiddlers) { - // Reexecute the widget if the data reference attributes have changed - if(changedAttributes.data || changedTiddlers[this.data]) { - // Regenerate and rerender the widget and replace the existing DOM node - this.generate(); - var oldDomNode = this.renderer.domNode, - newDomNode = this.renderer.renderInDom(); - oldDomNode.parentNode.replaceChild(newDomNode,oldDomNode); +/* +Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering +*/ +CloudWidget.prototype.refresh = function(changedTiddlers) { + var changedAttributes = this.computeAttributes(); + if(changedAttributes.data || changedTiddlers[this.cloudData]) { + this.refreshSelf(); + return true; } else if(changedAttributes.spiral) { - // Update the chart if the spiral setting has changed - this.spiral = this.renderer.getAttribute("spiral","archimedean"); + this.execute(); if(this.updateChart) { this.updateChart(); } + return true; } + return false; }; -CloudWidget.prototype.createChart = function() { - - var self = this; - - var domNode = this.renderer.domNode; - - var fill = d3.scale.category20(); - - - var data = this.renderer.renderTree.wiki.getTiddlerData(this.data); - - if(!data) { - // Use dummy data if none provided - data = "This word cloud does not have any data in it".split(" ").map(function(d) { - return {text: d, size: 10 + Math.random() * 90}; - }) - } - - var svg = d3.select(domNode).append("svg") - .attr("width", 600) - .attr("height", 400) - .append("g") - .attr("transform", "translate(300,200)"); - - var layout = d3.layout.cloud().size([600, 400]) - .words(data) - .padding(5) - .rotate(function() { return ~~(Math.random() * 5) * 30 - 60; }) - .font("Impact") - .fontSize(function(d) { return d.size*2; }) - .on("end", draw) - .start(); - - function draw(words) { - svg.selectAll("text") - .data(words) - .enter().append("text") - .style("font-size", function(d) { return d.size + "px"; }) - .style("font-family", "Impact") - .style("fill", function(d, i) { return fill(i); }) - .attr("text-anchor", "middle") - .attr("transform", function(d) { - return "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")"; - }) - .text(function(d) { return d.text; }); - } - - function updateChart() { - layout.spiral(self.spiral); - } - - return updateChart; - +/* +Remove any DOM nodes created by this widget or its children +*/ +CloudWidget.prototype.removeChildDomNodes = function() { + $tw.utils.each(this.domNodes,function(domNode) { + domNode.parentNode.removeChild(domNode); + }); + this.domNodes = []; }; exports.d3cloud = CloudWidget;