简体   繁体   中英

Updating word cloud with D3.js

I have two chars, one displays the publication data per year and the other one is the word cloud. Word cloud displays the titles. Here is the demo:

 // Word cloud layout by Jason Davies, http://www.jasondavies.com/word-cloud/ // Algorithm due to Jonathan Feinberg, http://static.mrfeinberg.com/bv_ch03.pdf (function() { function cloud() { var size = [256, 256], text = cloudText, font = cloudFont, fontSize = cloudFontSize, fontStyle = cloudFontNormal, fontWeight = cloudFontNormal, rotate = cloudRotate, padding = cloudPadding, spiral = archimedeanSpiral, words = [], timeInterval = Infinity, event = d3.dispatch("word", "end"), timer = null, cloud = {}; cloud.start = function() { var board = zeroArray((size[0] >> 5) * size[1]), bounds = null, n = words.length, i = -1, tags = [], data = words.map(function(d, i) { d.text = text.call(this, d, i); d.font = font.call(this, d, i); d.style = fontStyle.call(this, d, i); d.weight = fontWeight.call(this, d, i); d.rotate = rotate.call(this, d, i); d.size = ~~fontSize.call(this, d, i); d.padding = padding.call(this, d, i); return d; }).sort(function(a, b) { return b.size - a.size; }); if (timer) clearInterval(timer); timer = setInterval(step, 0); step(); return cloud; function step() { var start = +new Date, d; while (+new Date - start < timeInterval && ++i < n && timer) { d = data[i]; dx = (size[0] * (Math.random() + .5)) >> 1; dy = (size[1] * (Math.random() + .5)) >> 1; cloudSprite(d, data, i); if (d.hasText && place(board, d, bounds)) { tags.push(d); event.word(d); if (bounds) cloudBounds(bounds, d); else bounds = [{x: dx + d.x0, y: dy + d.y0}, {x: dx + d.x1, y: dy + d.y1}]; // Temporary hack dx -= size[0] >> 1; dy -= size[1] >> 1; } } if (i >= n) { cloud.stop(); event.end(tags, bounds); } } } cloud.stop = function() { if (timer) { clearInterval(timer); timer = null; } return cloud; }; cloud.timeInterval = function(x) { if (!arguments.length) return timeInterval; timeInterval = x == null ? Infinity : x; return cloud; }; function place(board, tag, bounds) { var perimeter = [{x: 0, y: 0}, {x: size[0], y: size[1]}], startX = tag.x, startY = tag.y, maxDelta = Math.sqrt(size[0] * size[0] + size[1] * size[1]), s = spiral(size), dt = Math.random() < .5 ? 1 : -1, t = -dt, dxdy, dx, dy; while (dxdy = s(t += dt)) { dx = ~~dxdy[0]; dy = ~~dxdy[1]; if (Math.min(dx, dy) > maxDelta) break; tag.x = startX + dx; tag.y = startY + dy; if (tag.x + tag.x0 < 0 || tag.y + tag.y0 < 0 || tag.x + tag.x1 > size[0] || tag.y + tag.y1 > size[1]) continue; // TODO only check for collisions within current bounds. if (!bounds || !cloudCollide(tag, board, size[0])) { if (!bounds || collideRects(tag, bounds)) { var sprite = tag.sprite, w = tag.width >> 5, sw = size[0] >> 5, lx = tag.x - (w << 4), sx = lx & 0x7f, msx = 32 - sx, h = tag.y1 - tag.y0, x = (tag.y + tag.y0) * sw + (lx >> 5), last; for (var j = 0; j < h; j++) { last = 0; for (var i = 0; i <= w; i++) { board[x + i] |= (last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0); } x += sw; } delete tag.sprite; return true; } } } return false; } cloud.words = function(x) { if (!arguments.length) return words; words = x; return cloud; }; cloud.size = function(x) { if (!arguments.length) return size; size = [+x[0], +x[1]]; return cloud; }; cloud.font = function(x) { if (!arguments.length) return font; font = d3.functor(x); return cloud; }; cloud.fontStyle = function(x) { if (!arguments.length) return fontStyle; fontStyle = d3.functor(x); return cloud; }; cloud.fontWeight = function(x) { if (!arguments.length) return fontWeight; fontWeight = d3.functor(x); return cloud; }; cloud.rotate = function(x) { if (!arguments.length) return rotate; rotate = d3.functor(x); return cloud; }; cloud.text = function(x) { if (!arguments.length) return text; text = d3.functor(x); return cloud; }; cloud.spiral = function(x) { if (!arguments.length) return spiral; spiral = spirals[x + ""] || x; return cloud; }; cloud.fontSize = function(x) { if (!arguments.length) return fontSize; fontSize = d3.functor(x); return cloud; }; cloud.padding = function(x) { if (!arguments.length) return padding; padding = d3.functor(x); return cloud; }; return d3.rebind(cloud, event, "on"); } function cloudText(d) { return d.text; } function cloudFont() { return "serif"; } function cloudFontNormal() { return "normal"; } function cloudFontSize(d) { return Math.sqrt(d.value); } function cloudRotate() { return (~~(Math.random() * 6) - 3) * 30; } function cloudPadding() { return 1; } // Fetches a monochrome sprite bitmap for the specified text. // Load in batches for speed. function cloudSprite(d, data, di) { if (d.sprite) return; c.clearRect(0, 0, (cw << 5) / ratio, ch / ratio); var x = 0, y = 0, maxh = 0, n = data.length; --di; while (++di < n) { d = data[di]; c.save(); c.font = d.style + " " + d.weight + " " + ~~((d.size + 1) / ratio) + "px " + d.font; var w = c.measureText(d.text + "m").width * ratio, h = d.size << 1; if (d.rotate) { var sr = Math.sin(d.rotate * cloudRadians), cr = Math.cos(d.rotate * cloudRadians), wcr = w * cr, wsr = w * sr, hcr = h * cr, hsr = h * sr; w = (Math.max(Math.abs(wcr + hsr), Math.abs(wcr - hsr)) + 0x1f) >> 5 << 5; h = ~~Math.max(Math.abs(wsr + hcr), Math.abs(wsr - hcr)); } else { w = (w + 0x1f) >> 5 << 5; } if (h > maxh) maxh = h; if (x + w >= (cw << 5)) { x = 0; y += maxh; maxh = 0; } if (y + h >= ch) break; c.translate((x + (w >> 1)) / ratio, (y + (h >> 1)) / ratio); if (d.rotate) c.rotate(d.rotate * cloudRadians); c.fillText(d.text, 0, 0); if (d.padding) c.lineWidth = 2 * d.padding, c.strokeText(d.text, 0, 0); c.restore(); d.width = w; d.height = h; d.xoff = x; d.yoff = y; d.x1 = w >> 1; d.y1 = h >> 1; d.x0 = -d.x1; d.y0 = -d.y1; d.hasText = true; x += w; } var pixels = c.getImageData(0, 0, (cw << 5) / ratio, ch / ratio).data, sprite = []; while (--di >= 0) { d = data[di]; if (!d.hasText) continue; var w = d.width, w32 = w >> 5, h = d.y1 - d.y0; // Zero the buffer for (var i = 0; i < h * w32; i++) sprite[i] = 0; x = d.xoff; if (x == null) return; y = d.yoff; var seen = 0, seenRow = -1; for (var j = 0; j < h; j++) { for (var i = 0; i < w; i++) { var k = w32 * j + (i >> 5), m = pixels[((y + j) * (cw << 5) + (x + i)) << 2] ? 1 << (31 - (i % 32)) : 0; sprite[k] |= m; seen |= m; } if (seen) seenRow = j; else { d.y0++; h--; j--; y++; } } d.y1 = d.y0 + seenRow; d.sprite = sprite.slice(0, (d.y1 - d.y0) * w32); } } // Use mask-based collision detection. function cloudCollide(tag, board, sw) { sw >>= 5; var sprite = tag.sprite, w = tag.width >> 5, lx = tag.x - (w << 4), sx = lx & 0x7f, msx = 32 - sx, h = tag.y1 - tag.y0, x = (tag.y + tag.y0) * sw + (lx >> 5), last; for (var j = 0; j < h; j++) { last = 0; for (var i = 0; i <= w; i++) { if (((last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0)) & board[x + i]) return true; } x += sw; } return false; } function cloudBounds(bounds, d) { var b0 = bounds[0], b1 = bounds[1]; if (dx + d.x0 < b0.x) b0.x = dx + d.x0; if (dy + d.y0 < b0.y) b0.y = dy + d.y0; if (dx + d.x1 > b1.x) b1.x = dx + d.x1; if (dy + d.y1 > b1.y) b1.y = dy + d.y1; } function collideRects(a, b) { return ax + a.x1 > b[0].x && ax + a.x0 < b[1].x && ay + a.y1 > b[0].y && ay + a.y0 < b[1].y; } function archimedeanSpiral(size) { var e = size[0] / size[1]; return function(t) { return [e * (t *= .1) * Math.cos(t), t * Math.sin(t)]; }; } function rectangularSpiral(size) { var dy = 4, dx = dy * size[0] / size[1], x = 0, y = 0; return function(t) { var sign = t < 0 ? -1 : 1; // See triangular numbers: T_n = n * (n + 1) / 2. switch ((Math.sqrt(1 + 4 * sign * t) - sign) & 3) { case 0: x += dx; break; case 1: y += dy; break; case 2: x -= dx; break; default: y -= dy; break; } return [x, y]; }; } // TODO reuse arrays? function zeroArray(n) { var a = [], i = -1; while (++i < n) a[i] = 0; return a; } var cloudRadians = Math.PI / 180, cw = 1 << 11 >> 5, ch = 1 << 11, canvas, ratio = 1; if (typeof document !== "undefined") { canvas = document.createElement("canvas"); canvas.width = 1; canvas.height = 1; ratio = Math.sqrt(canvas.getContext("2d").getImageData(0, 0, 1, 1).data.length >> 2); canvas.width = (cw << 5) / ratio; canvas.height = ch / ratio; } else { // Attempt to use node-canvas. canvas = new Canvas(cw << 5, ch); } var c = canvas.getContext("2d"), spirals = { archimedean: archimedeanSpiral, rectangular: rectangularSpiral }; c.fillStyle = c.strokeStyle = "red"; c.textAlign = "center"; if (typeof module === "object" && module.exports) module.exports = cloud; else (d3.layout || (d3.layout = {})).cloud = cloud; })(); 
 <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script> <!DOCTYPE html> <html> <head> <style type="text/css"> body{font-family: Arial, sans-serif;font-size:10px;} .axis path,.axis line {fill: none;stroke:#b6b6b6;shape-rendering: crispEdges;} .tick text{fill:#999;} g.journal.active{cursor:pointer;} text.label{font-size:12px;font-weight:bold;cursor:pointer;} text.value{font-size:12px;font-weight:bold;} </style> </head> <body> <script type="text/javascript"> var datajson = [{"Researcher":"Karri", "Published":[[2013,1]],"Total":1}, {"Researcher":"Ismo","Published":[[2013,1]],"Total":1}, {"Researcher":"Grigori","Published":[[2014,1],[2015,2]],"Total":3}, {"Researcher":"Ahmed","Published":[[2014,1],[2015,2]],"Total":3}, {"Researcher":"Roope","Published":[[2014,1],[2015,2]],"Total":3}, {"Researcher":"Arto","Published":[[2014,1],[2015,2]],"Total":3}, {"Researcher":"Daisuke","Published":[[2014,1],[2015,2]],"Total":3}, {"Researcher":"Arasawa","Published":[[2015,1]],"Total":1}, {"Researcher":"IkeHama","Published":[[2015,1]],"Total":1}]; var allDataJson = [{"Researchers":"Bulling Andreas, Majaranta Päivi","Year":"2011","Title":"Improved Interaction for Mid-Air Projection Screen Technology."}, {"Researchers":"Rakkolainen Ismo","Year":"2012","Title":"A biological stimulation device for training animals."}, {"Researchers":"Anttonen Jenni, Ovaska Saila","Year":"2013","Title":"Haptic covering for steering wheel (Haptic Device)."}, {"Researchers":"Rudnicky Alexander I., Turunen Markku, Kun Andrew, Paek Tim, Tashev Ivan","Year":"2014","Title":"Haptic Device."}, {"Researchers":"Valkama Pellervo, Hakulinen Jaakko, Raisamo Roope","Year":"2015","Title":"Tactile Imaging System."}, {"Researchers":"Santos de la Camara Raul, Turunen Markku, Benyon David","Year":"2011","Title":"PETMEI 2011: the 1st international workshop on pervasive eye tracking and mobile eye-based interaction."}, {"Researchers":"Worgan Simon, Turunen Markku","Year":"2012","Title":"Pseudo-Volumetric 3D Display Solutions."}]; var area1 = d3.select("body").append("div") .attr("id","area1") .style("width", 1500) .style("float", "left"); var totalpubperyear = [{"Year":"2011","Total":42},{"Year":"2012","Total":49},{"Year":"2013","Total":60},{"Year":"2014","Total":64},{"Year":"2015","Total":5}]; var words = [{"Item":"workshop","Count":2},{"Item":"pervasive","Count":2},{"Item":"eye","Count":4},{"Item":"tracking","Count":2},{"Item":"mobile","Count":9},{"Item":"interaction","Count":9},{"Item":"Pseudo-Volumetric","Count":2},{"Item":"3D","Count":3},{"Item":"Display","Count":7},{"Item":"Solutions","Count":2},{"Item":"SiMPE:","Count":2}]; var bC = barChart(totalpubperyear); var wC = wordCloud(words); function prepareCloudJS(datajs){ var tF = datajs.map(function(d){return d.Title;}); var arr = []; tF.map(function (e) {arr = arr.concat(e.split(/\\s+/));}); for(var i = arr.length - 1; i >= 0; i--) { if(arr[i] === "a") {arr.splice(i, 1);} else if(arr[i] === "of") {arr.splice(i, 1);} else if(arr[i] === "with") {arr.splice(i, 1);} else if(arr[i] === "the") {arr.splice(i, 1);} else if(arr[i] === "from") {arr.splice(i, 1);} else if(arr[i] === "to") {arr.splice(i, 1);} else if(arr[i] === "by") {arr.splice(i, 1);} else if(arr[i] === "for") {arr.splice(i, 1);} else if(arr[i] === "A") {arr.splice(i, 1);} else if(arr[i] === "in") {arr.splice(i, 1);} else if(arr[i] === "Be") {arr.splice(i, 1);} else if(arr[i] === "on") {arr.splice(i, 1);} else if(arr[i] === "by") {arr.splice(i, 1);} else if (arr[i] === "The") {arr.splice(i, 1);} else if(arr[i] === "and") {arr.splice(i, 1);} else if(arr[i] === "an") {arr.splice(i, 1);} else if (arr[i] === "All") {arr.splice(i, 1);} else if(arr[i] === "vs") {arr.splice(i, 1);} else if(arr[i] === "On") {arr.splice(i, 1);} else if (arr[i] === "An") {arr.splice(i, 1);} else if(arr[i] === "or") {arr.splice(i, 1);} else if (arr[i] === "ja") {arr.splice(i, 1);} else if(arr[i] === "at") {arr.splice(i, 1);} else if (arr[i] === "via") {arr.splice(i, 1);} else if (arr[i] === "/") {arr.splice(i, 1);} else if (arr[i] === "-") {arr.splice(i, 1);} else if (arr[i] === ":") {arr.splice(i, 1);} else if (arr[i] === "..") {arr.splice(i, 1);} else if (arr[i] === "as") {arr.splice(i, 1);} else if (arr[i] === "are") {arr.splice(i, 1);} else if (arr[i] === "1st") {arr.splice(i, 1);} else if (arr[i] === "8th") {arr.splice(i, 1);} else if (arr[i] === "True") {arr.splice(i, 1);} } var words = []; var counts = {}; var words = find_duplicates(arr); //Returns {"Item":,"Count":} function find_duplicates(arr){ var out=[], counts={}; for (var i=0 ; i<arr.length; i++) { var item = arr[i]; counts[item] = counts[item] >= 1 ? counts[item] + 1 : 1; } for (var item in counts) { if(counts[item] > 1) out.push({"Item": item, "Count": counts[item]}); } return out; } return words; } function barChart(totalpubperyear){ var bc = {}; var margin = {top: 20, right: 20, bottom: 30, left: 40}, width = 500 - margin.left - margin.right, height = 300 - margin.top - margin.bottom; var x = d3.scale.ordinal() .rangeRoundBands([0, width], .1) .domain(totalpubperyear.map(function(d) { return d[0]; }));; var y = d3.scale.linear() .range([height, 0]); var xAxis = d3.svg.axis() .scale(x) .orient("bottom"); var yAxis = d3.svg.axis() .scale(y) .orient("left"); var svg = area1.append("svg") .attr("id","svg1") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); //svg.append("h2").text("Overall Publications (TAUCHI)"); x.domain(totalpubperyear.map(function(d) { return d.Year; })); y.domain([0, d3.max(totalpubperyear, function(d) { return d.Total; })]); svg.append("g") .attr("class", "x axis") .attr("transform", "translate(0," + height + ")") .style("font-family","sans-serif") .style("font-size","10px") .call(xAxis); d3.select("#x axis").select("path") .style("display", "none"); d3.select("#x axis").selectAll("line") .style("fill", "none") .style("stroke", "#000") .style("shape-rendering", "crispEdges"); svg.append("g") .attr("class", "y axis") .call(yAxis) .append("text") .attr("transform", "rotate(-90)") .attr("y", 6) .attr("dy", ".71em") .style("font-family","sans-serif") .style("font-size","10px") .style("text-anchor", "end") .text("Publications"); var bars = svg.selectAll(".bar") .data(totalpubperyear) .enter(); bars.append("rect") .style("fill", "steelblue") .attr("class", "bar") .attr("x", function(d) { return x(d.Year); }) .attr("width", x.rangeBand()) .attr("y", function(d) { return y(d.Total); }) .attr("height", function(d) { return height - y(d.Total); }) .on('mouseover', mouseover) .on("mouseout",mouseout); //Create the total labels above the rectangles. bars.append("text").text(function(d){ return d.Total}) .attr("x", function(d) { return x(d.Year)+x.rangeBand()/2; }) .attr("y", function(d) { return y(d.Total)-5; }) .attr("text-anchor", "middle"); function mouseover(d, i){ // utility function to be called on mouseclick. // filter for selected year. console.log("mouseover"); d3.select(this).style({fill:'orange'}); var yr = x.domain()[i]; //returns clicked year var nD = []; for (var i = 0; i < allDataJson.length; i++) { if(allDataJson[i].Year == yr){ nD.push( { "Title":allDataJson[i].Title }); } } var m = prepareCloudJS(nD); //returns updated words wC.update(m); } function mouseout(d){ // utility function to be called on mouseout. // reset the pie-chart and legend. console.log("mouseout"); d3.select(this).style({fill:'steelblue'}); var newjs = []; for(var i = 0; i < words.length; i++){ newjs.push({ "Item": words[i].Item, "Count": words[i].Count }); } // words = words.map(function(d) { return [d.Item, d.Count]; }); // console.log(JSON.stringify(newjs)); wC.update(newjs); } return bc; } function wordCloud(words){ var wC = {}; var margin = {top: 20, right: 20, bottom: 30, left: 40}, width = 500 - margin.left - margin.right, height = 300 - margin.top - margin.bottom; var x = area1.append("svg") .attr("id","svg2") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom); var fill = d3.scale.category20(); var fontSize = d3.scale.log().range([10, 100]); var wcloud = d3.layout.cloud().size([960, 600]) .words(words.map(function(d) { return {text: d.Item, size: d.Count*4}; })) .padding(4) .rotate(function() { return ~~(Math.random() * 2) * 90; }) .font("Impact") .fontSize(function(d) { return d.size; }) .on("end", draw) .start(); function draw(words) { x.append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")") //.attr("transform", "translate(150,150)") .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(" + [dx, dy] + ")rotate(" + d.rotate + ")"; }) .text(function(d) { return d.text; }); } function drawUpdate(words){ d3.layout.cloud().size([960, 600]) .words(words) .padding(5) .rotate(function() { return ~~(Math.random() * 2) * 90; }) .font("Impact") .fontSize(function(d) { return d.size; }) .start(); area1.select("#svg2") .selectAll("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")") .attr("transform", "translate(150,150)") .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("transform", function(d) { return "translate(" + [dx, dy] + ")rotate(" + d.rotate + ")"; }) .text(function(d) { return d.text; }); } // Utility function to be used to update the word cloud. wC.update = function(nD){ console.log("updated words", JSON.stringify(nD)); drawUpdate(nD); // drawUpdate(nD); } return wC; } </script> </body> </html> 

When I mouseover the years (2011 to 2015), I want to update word cloud for those years.

When I mouseover the year for the first time, it updates the word cloud. But when I mouseover for the second or third time, it won't update.

Any idea why does it happen?

A couple of things:

  1. In your prepareCloudJS function, you're returning the results of find_duplicates which always seems to be an empty array. It looks like you're not checking for duplicates correctly, as I can see in the data that there should be duplicates, but they're not being detected correctly due to capitalisation and/or punctuation.

    I'll leave you to fix this one yourself, as it's both a data and a coding issue. For some hints though, you will want to remove all punctuation and convert all words to the same case to find the duplicates.

  2. When you redraw the word cloud in your drawUpdate function, you are not correctly following the d3 enter, update, exit pattern. Mike's thinking with joins might help here, also check out the "General Update Pattern" link at the bottom of that page.

    You might like to do something like:

     function drawUpdate(words){ d3.layout.cloud().size([960, 600]) .words(words) .padding(5) .rotate(function() { return ~~(Math.random() * 2) * 90; }) .font("Impact") .fontSize(function(d) { return d.size; }) .start(); // new variable to hold the selection var words = area1.select("#svg2") .selectAll("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")") .attr("transform", "translate(150,150)"); .selectAll("text") .data(words); // append new text elements words.enter().append("text"); // update all words in the word cloud (when you append // nodes from the "enter" selection, d3 will add the new // nodes to the "update" selection, thus all of them will // be updated here. words.style("font-size", function(d) { return d.size + "px"; }) .style("font-family", "Impact") .style("fill", function(d, i) { return fill(i); }) .attr("transform", function(d) { return "translate(" + [dx, dy] + ")rotate(" + d.rotate + ")"; }) .text(function(d) { return d.text; }); words.exit().remove(); // new line to remove all unused words } 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM