jeangelj
jeangelj

Reputation: 4498

d3.js conditional word color fill in wordcloud

I created a d3.js word cloud, based on a csv.

In the csv I have three columns:

words.csv:

word    count   index
word1   457      239
word2   373      155
word3   76      -142
word4   345      127
word5   12      -206
word6   46      -172

I wrote the following code (with help from youtube and jason davies' world cloud package) and it works to create the word cloud, but I don't know how to change the color of the words based on the index.

    <script>

    var width = 1500,
        height =400;

    var wordScale = d3.scale.linear().range([10,100]);

    var fill = d3.scale.category20c();
    d3.csv("words.csv", function(data) {  
        var subjects = data
            .map(function(d) {return {text: d.word, size: +d.count} })
            .sort(function(a,b) {return d3.descending (a.size, b.size); })
            .slice(0,100);

        wordScale.domain([
            d3.min(subjects, function(d) {return d.size; }),
            d3.max(subjects, function(d) {return d.size; }),
        ]);    

    // var layout = cloud()
        d3.layout.cloud().size([width, height])
        .words(subjects)
        .padding(1)
        .rotate(function() { return ~~(Math.random() * 2) * 0; })
        .font("Impact")
        .fontSize(function(d) { return wordScale(d.size); })
        .on("end", draw)
        .start();

    });

        function draw(words) {
          d3.select("#word-cloud").append("svg")
              .attr("width",width)
              .attr("height",height)
            .append("g")
              .attr("transform", "translate("+(width /2)+","+(height /2)+")")
              //where the center is
            .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; });
    }


        </script>


I need to change this line in the code `var fill = d3.scale.category20c();`

The goal would be that all words above the average (so positive index) are green and all words below the average (negative index) are grey. If  the transparency of the color/fill could be dependent on how much above or below the average the count is that would be amazing - but not necessary. 

    word    count   index   color
    word1   457      239    green
    word2   373      155    green
    word3   76      -142    grey
    word4   345      127    green
    word5   12      -206    grey
    word6   46      -172    grey

Thank you!

UPDATE:

Tried the following based on Dan's answer but with index:

    mycolor = d3.rgb("#00cc66");

var width = 1500,
    height =400;

var wordScale = d3.scale.linear().range([10,90]);
var colorScale = d3.scale.linear().range([10,90]);

// var fill = d3.scale.category20c();
d3.csv("words.csv", function(data) {  
    var subjects = data
        .map(function(d) {return {text: d.word, size: +d.count, clr: d.index } })
        .sort(function(a,b) {return d3.descending (a.size, b.size); })
        .slice(0,100);

    wordScale.domain([
        d3.min(subjects, function(d) {return d.size; }),
        d3.max(subjects, function(d) {return d.size; }),
    ]);   

    colorScale.domain([
        d3.min(subjects, function(d) {return d.clr; }),
        d3.max(subjects, function(d) {return d.clr; }),
    ]);  

// var layout = cloud()
    d3.layout.cloud().size([width, height])
    .words(subjects)
    .padding(1)
    .rotate(function() { return ~~(Math.random() * 2) * 0; })
    .font("Impact")
    .fontSize(function(d) { return wordScale(d.size); })
    .on("end", draw)
    .start();

});

    function draw(words) {
      d3.select("#word-cloud").append("svg")
          .attr("width",width)
          .attr("height",height)
        .append("g")
          .attr("transform", "translate("+(width /2)+","+(height /2)+")")
          //where the center is
        .selectAll("text")
          .data(words)
        .enter().append("text")
          .style("fill", function(d) { return d.clr >= 50 ? mycolor : "grey";})
          .style("opacity", function(d) { return d.clr / 100 })
          .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; });
}

Unfortunately, nothing is displaying when I run it (also no error message).

Upvotes: 1

Views: 1455

Answers (1)

Dan Delaney
Dan Delaney

Reputation: 353

For your code above, i.e; assuming wordScale is between 10 and 100:

In the draw function, in the .append("text") section of code,

Edit the fill function as follows. This will turn green / grey based on size being higher / lower than the mid point (55). Feel free to use better color values than the hardcoded ones below:

.style("fill", function(d) { return d.size >= 55 ? "green" : "grey";})

To fulfil your other request, add the following style immediately below the fill. This will set the opacity relative to the size (between 0.1 and 1). Again, you can adjust these values as required.

.style("opacity", function(d) { return d.size / 100 })

If you make these edits, you can also drop the var fill at the top of your code.

UPDATED ANSWER:

Note, I couldn't get Jason Davies' to load remotely without a script error, but no time to debug. Scroll down after his script for the actual answer. I also haven't added any rotation element to this.

!function(a){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=a();else if("function"==typeof define&&define.amd)define([],a);else{var b;b="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,b=b.d3||(b.d3={}),b=b.layout||(b.layout={}),b.cloud=a()}}(function(){var a;return function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);var j=new Error("Cannot find module '"+g+"'");throw j.code="MODULE_NOT_FOUND",j}var k=c[g]={exports:{}};b[g][0].call(k.exports,function(a){var c=b[g][1][a];return e(c||a)},k,k.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g<d.length;g++)e(d[g]);return e}({1:[function(a,b,c){function h(a){return a.text}function i(){return"serif"}function j(){return"normal"}function k(a){return Math.sqrt(a.value)}function l(){return 30*(~~(6*Math.random())-3)}function m(){return 1}function n(a,b,c,d){if(!b.sprite){var h=a.context,i=a.ratio;h.clearRect(0,0,(f<<5)/i,g/i);var j=0,k=0,l=0,m=c.length;for(--d;++d<m;){b=c[d],h.save(),h.font=b.style+" "+b.weight+" "+~~((b.size+1)/i)+"px "+b.font;var n=h.measureText(b.text+"m").width*i,o=b.size<<1;if(b.rotate){var p=Math.sin(b.rotate*e),q=Math.cos(b.rotate*e),r=n*q,s=n*p,t=o*q,u=o*p;n=Math.max(Math.abs(r+u),Math.abs(r-u))+31>>5<<5,o=~~Math.max(Math.abs(s+t),Math.abs(s-t))}else n=n+31>>5<<5;if(o>l&&(l=o),j+n>=f<<5&&(j=0,k+=l,l=0),k+o>=g)break;h.translate((j+(n>>1))/i,(k+(o>>1))/i),b.rotate&&h.rotate(b.rotate*e),h.fillText(b.text,0,0),b.padding&&(h.lineWidth=2*b.padding,h.strokeText(b.text,0,0)),h.restore(),b.width=n,b.height=o,b.xoff=j,b.yoff=k,b.x1=n>>1,b.y1=o>>1,b.x0=-b.x1,b.y0=-b.y1,b.hasText=!0,j+=n}for(var v=h.getImageData(0,0,(f<<5)/i,g/i).data,w=[];--d>=0;)if(b=c[d],b.hasText){for(var n=b.width,x=n>>5,o=b.y1-b.y0,y=0;y<o*x;y++)w[y]=0;if(null==(j=b.xoff))return;k=b.yoff;for(var z=0,A=-1,B=0;B<o;B++){for(var y=0;y<n;y++){var C=x*B+(y>>5),D=v[(k+B)*(f<<5)+(j+y)<<2]?1<<31-y%32:0;w[C]|=D,z|=D}z?A=B:(b.y0++,o--,B--,k++)}b.y1=b.y0+A,b.sprite=w.slice(0,(b.y1-b.y0)*x)}}}function o(a,b,c){c>>=5;for(var k,d=a.sprite,e=a.width>>5,f=a.x-(e<<4),g=127&f,h=32-g,i=a.y1-a.y0,j=(a.y+a.y0)*c+(f>>5),l=0;l<i;l++){k=0;for(var m=0;m<=e;m++)if((k<<h|(m<e?(k=d[l*e+m])>>>g:0))&b[j+m])return!0;j+=c}return!1}function p(a,b){var c=a[0],d=a[1];b.x+b.x0<c.x&&(c.x=b.x+b.x0),b.y+b.y0<c.y&&(c.y=b.y+b.y0),b.x+b.x1>d.x&&(d.x=b.x+b.x1),b.y+b.y1>d.y&&(d.y=b.y+b.y1)}function q(a,b){return a.x+a.x1>b[0].x&&a.x+a.x0<b[1].x&&a.y+a.y1>b[0].y&&a.y+a.y0<b[1].y}function r(a){var b=a[0]/a[1];return function(a){return[b*(a*=.1)*Math.cos(a),a*Math.sin(a)]}}function s(a){var b=4,c=b*a[0]/a[1],d=0,e=0;return function(a){var f=a<0?-1:1;switch(Math.sqrt(1+4*f*a)-f&3){case 0:d+=c;break;case 1:e+=b;break;case 2:d-=c;break;default:e-=b}return[d,e]}}function t(a){for(var b=[],c=-1;++c<a;)b[c]=0;return b}function u(){return document.createElement("canvas")}function v(a){return"function"==typeof a?a:function(){return a}}var d=a("d3-dispatch").dispatch,e=Math.PI/180,f=64,g=2048;b.exports=function(){function I(a){a.width=a.height=1;var b=Math.sqrt(a.getContext("2d").getImageData(0,0,1,1).data.length>>2);a.width=(f<<5)/b,a.height=g/b;var c=a.getContext("2d");return c.fillStyle=c.strokeStyle="red",c.textAlign="center",{context:c,ratio:b}}function J(b,c,d){for(var l,m,n,f=(a[0],a[1],c.x),g=c.y,h=Math.sqrt(a[0]*a[0]+a[1]*a[1]),i=A(a),j=F()<.5?1:-1,k=-j;(l=i(k+=j))&&(m=~~l[0],n=~~l[1],!(Math.min(Math.abs(m),Math.abs(n))>=h));)if(c.x=f+m,c.y=g+n,!(c.x+c.x0<0||c.y+c.y0<0||c.x+c.x1>a[0]||c.y+c.y1>a[1])&&(!d||!o(c,b,a[0]))&&(!d||q(c,d))){for(var y,p=c.sprite,r=c.width>>5,s=a[0]>>5,t=c.x-(r<<4),u=127&t,v=32-u,w=c.y1-c.y0,x=(c.y+c.y0)*s+(t>>5),z=0;z<w;z++){y=0;for(var B=0;B<=r;B++)b[x+B]|=y<<v|(B<r?(y=p[z*r+B])>>>u:0);x+=s}return delete c.sprite,!0}return!1}var a=[256,256],b=h,c=i,e=k,s=j,x=j,y=l,z=m,A=r,B=[],C=1/0,D=d("word","end"),E=null,F=Math.random,G={},H=u;return G.canvas=function(a){return arguments.length?(H=v(a),G):H},G.start=function(){function l(){for(var b=Date.now();Date.now()-b<C&&++i<h&&E;){var c=k[i];c.x=a[0]*(F()+.5)>>1,c.y=a[1]*(F()+.5)>>1,n(d,c,k,i),c.hasText&&J(f,c,g)&&(j.push(c),D.call("word",G,c),g?p(g,c):g=[{x:c.x+c.x0,y:c.y+c.y0},{x:c.x+c.x1,y:c.y+c.y1}],c.x-=a[0]>>1,c.y-=a[1]>>1)}i>=h&&(G.stop(),D.call("end",G,j,g))}var d=I(H()),f=t((a[0]>>5)*a[1]),g=null,h=B.length,i=-1,j=[],k=B.map(function(a,d){return a.text=b.call(this,a,d),a.font=c.call(this,a,d),a.style=s.call(this,a,d),a.weight=x.call(this,a,d),a.rotate=y.call(this,a,d),a.size=~~e.call(this,a,d),a.padding=z.call(this,a,d),a}).sort(function(a,b){return b.size-a.size});return E&&clearInterval(E),E=setInterval(l,0),l(),G},G.stop=function(){return E&&(clearInterval(E),E=null),G},G.timeInterval=function(a){return arguments.length?(C=null==a?1/0:a,G):C},G.words=function(a){return arguments.length?(B=a,G):B},G.size=function(b){return arguments.length?(a=[+b[0],+b[1]],G):a},G.font=function(a){return arguments.length?(c=v(a),G):c},G.fontStyle=function(a){return arguments.length?(s=v(a),G):s},G.fontWeight=function(a){return arguments.length?(x=v(a),G):x},G.rotate=function(a){return arguments.length?(y=v(a),G):y},G.text=function(a){return arguments.length?(b=v(a),G):b},G.spiral=function(a){return arguments.length?(A=w[a]||a,G):A},G.fontSize=function(a){return arguments.length?(e=v(a),G):e},G.padding=function(a){return arguments.length?(z=v(a),G):z},G.random=function(a){return arguments.length?(F=a,G):F},G.on=function(){var a=D.on.apply(D,arguments);return a===D?G:a},G};var w={archimedean:r,rectangular:s}},{"d3-dispatch":2}],2:[function(b,c,d){!function(b,e){"object"==typeof d&&void 0!==c?e(d):"function"==typeof a&&a.amd?a(["exports"],e):e(b.d3=b.d3||{})}(this,function(a){"use strict";function c(){for(var e,a=0,b=arguments.length,c={};a<b;++a){if(!(e=arguments[a]+"")||e in c)throw new Error("illegal type: "+e);c[e]=[]}return new d(c)}function d(a){this._=a}function e(a,b){return a.trim().split(/^|\s+/).map(function(a){var c="",d=a.indexOf(".");if(d>=0&&(c=a.slice(d+1),a=a.slice(0,d)),a&&!b.hasOwnProperty(a))throw new Error("unknown type: "+a);return{type:a,name:c}})}function f(a,b){for(var e,c=0,d=a.length;c<d;++c)if((e=a[c]).name===b)return e.value}function g(a,c,d){for(var e=0,f=a.length;e<f;++e)if(a[e].name===c){a[e]=b,a=a.slice(0,e).concat(a.slice(e+1));break}return null!=d&&a.push({name:c,value:d}),a}var b={value:function(){}};d.prototype=c.prototype={constructor:d,on:function(a,b){var h,c=this._,d=e(a+"",c),i=-1,j=d.length;{if(!(arguments.length<2)){if(null!=b&&"function"!=typeof b)throw new Error("invalid callback: "+b);for(;++i<j;)if(h=(a=d[i]).type)c[h]=g(c[h],a.name,b);else if(null==b)for(h in c)c[h]=g(c[h],a.name,null);return this}for(;++i<j;)if((h=(a=d[i]).type)&&(h=f(c[h],a.name)))return h}},copy:function(){var a={},b=this._;for(var c in b)a[c]=b[c].slice();return new d(a)},call:function(a,b){if((e=arguments.length-2)>0)for(var e,f,c=new Array(e),d=0;d<e;++d)c[d]=arguments[d+2];if(!this._.hasOwnProperty(a))throw new Error("unknown type: "+a);for(f=this._[a],d=0,e=f.length;d<e;++d)f[d].value.apply(b,c)},apply:function(a,b,c){if(!this._.hasOwnProperty(a))throw new Error("unknown type: "+a);for(var d=this._[a],e=0,f=d.length;e<f;++e)d[e].value.apply(b,c)}},a.dispatch=c,Object.defineProperty(a,"__esModule",{value:!0})})},{}]},{},[1])(1)});

/// ACTUAL START OF ANSWER ///

// fake JSON data in place of CSV file.

data = [{
    word: "word1",
    count: 457,
    index: 239
  },
  {
    word: "word2",
    count: 373,
    index: 155
  },
  {
    word: "word3",
    count: 76,
    index: -142
  },
  {
    word: "word4",
    count: 345,
    index: 127
  },
  {
    word: "word5",
    count: 12,
    index: -206
  },
  {
    word: "word6",
    count: 46,
    index: -172
  }
]

//  the script


    var width = 700, // amended to make result easier to see
        height =400;

    var wordScale = d3.scale.linear().range([10,100]);

//    d3.csv("words.csv", function(data) {  // uncomment when using CSV
        var subjects = data
            .map(function(d) {return {text: d.word, size: +d.count, in: +d.index} }) // note use of +d.index, to ensure it's held as a number when read from csv, same as +d.count
            .sort(function(a,b) {return d3.descending (a.size, b.size); })
            .slice(0,100);

        wordScale.domain([
            d3.min(subjects, function(d) {return d.size; }),
            d3.max(subjects, function(d) {return d.size; }),
        ]);    

    // var layout = cloud()
        d3.layout.cloud().size([width, height])
        .words(subjects)
        .padding(1)
        .rotate(function() { return ~~(Math.random() * 2) * 0; })
        .font("Impact")
        .fontSize(function(d) { return wordScale(d.size); })
        .on("end", draw)
        .start();

 //   }); uncomment when using csv

        function draw(words) {
          d3.select("#word-cloud").append("svg")
              .attr("width",width)
              .attr("height",height)
            .append("g")
              .attr("transform", "translate("+(width /2)+","+(height /2)+")")
              //where the center is
            .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 d.in > 0 ? "#00cc66" : "grey" })
              .style("opacity", function(d) { return d.in > 0 ? d.size/100 : (50 - d.size) / 100 } ) // bit of a hack to get furthest from index to become darker
              .attr("text-anchor", "middle")
              .attr("transform", function(d) {
                return "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")";
              })
              .text(function(d) { return d.text; });
    }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<!--
word    count   index
word1   457      239
word2   373      155
word3   76      -142
word4   345      127
word5   12      -206
word6   46      -172
-->
<div id="word-cloud"></div>

Upvotes: 2

Related Questions