function wordCount(options, data) { var width = options.width, height = options.height, groupHeight = height / data.group.length; var collisionPadding = options.collisionPadding, clipPadding = 4, minRadiusCircle = options.minRadiusCircle, minRadius = options.minRadius, // minimum collision radius maxRadius = options.maxRadius, // also determines collision search radius activeWord, minFontSize = options.minFontSize, // currently-displayed word chartSelector = options.chartSelector, searchSelector = options.searchSelector, split = options.split || false; d3.select(chartSelector + " .g-nodes").remove(); d3.select(chartSelector + " .g-labels").remove(); d3.select(chartSelector).append('div').attr("class", "g-labels"); d3.select(chartSelector).append('svg').attr("class", "g-nodes").attr("width", width).attr("height", height); data.words.forEach(function (d) { d.x = (width / 4) + (Math.random() * width / 2); d.y = (height / 4) + (Math.random() * height / 2); d.cx = d.x; d.cy = d.y; d.totalCount = d3.sum(d.count); d.extraY = groupHeight * data.group.indexOf(d.group); d.startPos = []; d.endPos = []; var updatedCount = 0; d.count.forEach(function (dd, ii) { d.startPos[ii] = updatedCount; updatedCount += dd; d.endPos[ii] = updatedCount; }); }); var formatShortCount = d3.format(",.0f"), formatLongCount = d3.format(".1f"), formatCount = function (d) { return (d < 10 ? formatLongCount : formatShortCount)(d); }; var r = d3.scale.sqrt() .domain([0, d3.max(data.words, function (d) { return d.totalCount; })]) .range([0, maxRadius]); var force = d3.layout.force() .charge(0) .size([width, height]) .on("tick", tick); var node = d3.select(".g-nodes").selectAll(".g-node"), label = d3.select(".g-labels").selectAll(".g-label"); d3.select(".g-nodes").append("rect") .attr("class", "g-overlay") .attr("width", width) .attr("height", height) .on("click", clear); d3.select(window) .on("hashchange", hashchange); d3.select(searchSelector).on("keyup", submit); updateWords(data.words); hashchange(); // Update the known words. function updateWords(words) { words.forEach(function (d) { d.r = Math.max(minRadiusCircle, r(d.totalCount)); d.cr = Math.max(minRadius, d.r); d.k = fraction(d.count[0], d.totalCount - d.count[0]); if (isNaN(d.k)) d.k = .5; if (isNaN(d.x)) d.x = (1 - d.k) * width + Math.random(); d.bias = (1 / data.category.length) - Math.max(0, Math.min(1, d.k)); }); data.words = words; refresh(split); } function refresh(_split) { split = _split; force.size([width, split === true ? groupHeight : height]); force.nodes(data.words).start(); updateNodes(); updateLabels(); tick({ alpha: 1 }); // synchronous update } // Update the displayed nodes. function updateNodes() { node = node.data(data.words, function (d) { return d.name; }); node.exit().remove(); var nodeEnter = node.enter().append("a") .attr("class", "g-node") .attr("xlink:href", function (d) { return "#" + encodeURIComponent(d.name); }) .call(force.drag) .call(linkWord); data.category.forEach(function (category) { var categoryEnter = nodeEnter.append("g") .attr("class", "g-" + category.name); categoryEnter.append("clipPath") .attr("id", function (d) { return "g-clip-" + category.name + "-" + d.id; }) .append("rect"); categoryEnter.append("circle").attr("fill", category.color); }); node.selectAll("rect") .attr("y", function (d) { return -d.r - clipPadding; }) .attr("height", function (d) { return 2 * d.r + 2 * clipPadding; }); data.category.forEach(function (category, i) { node.select(".g-" + category.name + " rect") .style("display", function (d) { return (d.endPos[i] / d.totalCount) >= 0 ? null : "none" }) .attr("x", function (d) { return (-d.r - clipPadding) + (2 * (d.r + clipPadding) * (d.startPos[i] / d.totalCount)) }) .attr("width", function (d) { return 2 * d.r + 2 * clipPadding }); }); data.category.forEach(function (category, i) { node.select(".g-" + category.name + " circle") .attr("clip-path", function (d) { return (d.startPos[i] / d.totalCount) <= 1 ? "url(#g-clip-" + category.name + "-" + d.id + ")" : null; }); }); node.selectAll("circle") .attr("r", function (d) { return d.r; }); return; } //Update the displayed node labels. function updateLabels() { label = label.data(data.words, function (d) { return d.name; }); label.exit().remove(); var labelEnter = label.enter().append("a") .attr("class", "g-label") .attr("href", function (d) { return "#" + encodeURIComponent(d.name); }) .call(force.drag) .call(linkWord); labelEnter.append("div") .attr("class", "g-name") .text(function (d) { return d.name; }); labelEnter.append("div") .attr("class", "g-value"); label .style("font-size", function (d) { return Math.max(minFontSize, d.cr / 2) + "px"; }) .style("width", function (d) { return d.r * 2.5 + "px"; }); // Create a temporary span to compute the true text width. label.append("span") .text(function (d) { return d.name; }) .each(function (d) { d.dx = Math.max(d.r * 2.5, this.getBoundingClientRect().width); }) .remove(); label .style("width", function (d) { return d.dx + "px"; }) .select(".g-value") .text(function (d) { var vals = d.count.map(function (dd) { return formatShortCount(dd) }); return vals.join(" - ");//formatShortCount(d.parties[0].count) + " - " + formatShortCount(d.parties[1].count); }); // Compute the height of labels when wrapped. label.each(function (d) { d.dy = this.getBoundingClientRect().height; }); } // Update the active word. function updateActiveWord(word) { if (word === null) { node.classed("g-selected", false); node.classed("g-notSelected", false); } else { d3.selectAll(".g-head").attr("class", word ? "g-head g-has-word" : "g-head g-hasnt-word"); if (activeWord = word) { node.classed("g-selected", function (d) { return d === word; }); } /*else { node.classed("g-selected", false); node.classed("g-notSelected", false); }*/ if (d3.select(".g-selected")[0][0] !== null) { node.classed("g-notSelected", function (d) { return d !== word; }); } } } // Assign event handlers to word links. function linkWord(a) { a.on("click", click) .on("mouseover", mouseover) .on("mouseout", mouseout); } // Returns the word matching the specified name, approximately. // If no matching word is found, returns undefined. function findWord(name) { for (var i = 0, n = data.words.length, t; i < n; ++i) { if ((t = data.words[i]).name.toLowerCase() === name.toLowerCase()) { return t; } } } // Returns the word matching the specified name, approximately. // If no matching word is found, a new one is created. function findOrAddWord(name) { var word = findWord(name); if (!word) { word = name; word.y = 0; } return word; } // Simulate forces and update node and label positions on tick. function tick(e) { if (split === true) { data.group.forEach(function (grp) { node.filter(function (d) { return d.group === grp; }) .each(bias(e.alpha * 105)) .each(collide(.5, grp)) .attr("transform", function (d) { return "translate(" + d.x + "," + (d.extraY + d.y) + ")"; }); }); label .style("left", function (d) { return (d.x - d.dx / 2) + "px"; }) .style("top", function (d) { return (((d.extraY + d.y)) - d.dy / 2) + "px"; }); } else { node.each(bias(e.alpha * 105)) .each(collide(.5, null)) .attr("transform", function (d) { return "translate(" + d.x + "," + d.y + ")"; }); label .style("left", function (d) { return (d.x - d.dx / 2) + "px"; }) .style("top", function (d) { return (d.y - d.dy / 2) + "px"; }); } } // A left-right bias causing words to orient by party preference. function bias(alpha) { return function (d) { var dx = d.bias * alpha; d.x += dx; if (d.x < (d.cr + collisionPadding)) d.x = d.cr + collisionPadding; else if (d.x > (width - (d.cr + collisionPadding))) d.x = width - (d.cr + collisionPadding); if (d.y < (d.cr + collisionPadding)) d.y = d.cr + collisionPadding; else if (d.y > (height - (d.cr + collisionPadding))) d.y = height - (d.cr + collisionPadding); }; } // Resolve collisions between nodes. function collide(alpha, grp) { var q = d3.geom.quadtree(data.words.filter(function (d) { return grp === null || d.group === grp })); return function (d) { var r = d.cr + maxRadius + collisionPadding, nx1 = d.x - r, nx2 = d.x + r, ny1 = (d.y) - r, ny2 = (d.y) + r; q.visit(function (quad, x1, y1, x2, y2) { if (quad.point && (quad.point !== d) && d.other !== quad.point && d !== quad.point.other) { var x = d.x - quad.point.x, y = (d.y) - quad.point.y, l = Math.sqrt(x * x + y * y), r = d.cr + quad.point.r + collisionPadding; if (l < r) { if (l === 0) l = 1; l = (l - r) / l * alpha; d.x -= x *= l; d.y -= y *= l; quad.point.x += x; quad.point.y += y; } } return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1; }); }; } // Given two quantities a and b, returns the fraction to split the circle a + b. function fraction(a, b) { var k = a / (a + b); //if (k > 0 && k < 1) { // var t0, t1 = Math.pow(12 * k * Math.PI, 1 / 3); // for (var i = 0; i < 10; ++i) { // Solve for theta numerically. // t0 = t1; // t1 = (Math.sin(t0) - t0 * Math.cos(t0) + 2 * k * Math.PI) / (1 - Math.cos(t0)); // } // k = (1 - Math.cos(t1 / 2)) / 2; //} return k; } // Update the active word on hashchange, perhaps creating a new word. function hashchange() { var name = decodeURIComponent(location.hash.substring(1)).trim(); updateActiveWord(name && name != "!" ? findOrAddWord(name) : null); } // Trigger a hashchange on submit. function submit() { node.classed("g-selected", false); node.classed("g-notSelected", false); var name = d3.select(searchSelector)[0][0].value.trim(); if (d3.event.keyCode === 13) { d3.select(searchSelector)[0][0].value = ""; location.hash = name ? encodeURIComponent(name) : "!"; return; } if (name.length > 0) { node.classed("g-selected", function (d) { return d.name.toLowerCase().indexOf(name.toLowerCase()) > -1; }); if (d3.selectAll(".g-selected")[0].length > 0) { node.classed("g-notSelected", function (d) { return d.name.toLowerCase().indexOf(name.toLowerCase()) === -1 }); } } // //this.search.value = ""; //d3.event.preventDefault(); } // Clear the active word when clicking on the chart background. function clear() { location.replace("#!"); } // Rather than flood the browser history, use location.replace. function click(d) { location.replace("#" + encodeURIComponent(d === activeWord ? "!" : d.name)); d3.event.preventDefault(); } // When hovering the label, highlight the associated node and vice versa. // When no word is active, also cross-highlight with any mentions in excerpts. function mouseover(d) { node.classed("g-hover", function (p) { return p === d; }); if (!activeWord) d3.selectAll(".g-mention p").classed("g-hover", function (p) { return p.word === d; }); } // When hovering the label, highlight the associated node and vice versa. // When no word is active, also cross-highlight with any mentions in excerpts. function mouseout(d) { node.classed("g-hover", false); if (!activeWord) d3.selectAll(".g-mention p").classed("g-hover", false); } return refresh; }