Oh my god. It's full of code!

Super Neato jQuery tag cloud! It’s easy!

Read the below post if:
You need a dynamic tag cloud based on user entered information
OR
You have nothing better to do

So recently I was tasked with creating a simple tag cloud based on data typed into a text field in an online survey. Problem with that is of course tag clouds depend on terms repeating themselves, but the data I was collecting was from a free form text field where people could answer with anything at all. What good is a tag cloud if you have 100 different answers all stated one time? Might as well just do a list or something. Totally defeats the purpose. The solution was an autocomplete text field, where previous answers to the question would be provided as you type in hopes that one of the previous takes answers match your opinion so you would choose it, hence increasing the occurrence count of that phrase/word. I knew there had to be lots of solutions out there, and I knew for a fact there was one involving jQuery. Which there was.

So now your wondering ‘okay smartass, if you found what you wanted, why are you even writing this post? You don’t even have any new content!’. That is where you would be wrong my good sir. You see, while I did find an easy to use jQuery tag cloud, I wasn’t super stocked with the display or flexibility. My cloud also works on data gathered in surveys, so the amount of data in the cloud, and the response rate would be very very variable. Also, I made some neat tweaks. The basic process for the whole project looked like this.

  • Create an online survey with a free form text field asking a question, like “State a long lyric”
  • The first person sees no suggestions since there are no previous answers to this question. They type in whatever they want like “I like big butts and I can not lie”
  • The second person takes the survey, beings typing, and see’s “I like big butts and can not lie” as a suggestions. Since that sounds good, they take the auto-complete suggestions.
  • We continue to gather data, getting new lyrics added, and desirable ones choose multiple times
  • The tag cloud application continually refreshes, watching the same column in the database, and redraws itself to match the ever changing data, and looks really cool while doing it.

Now, I’ll post the auto-complete code for lime survey in another post. For now, lets focus on the tag cloud. Like I said, my work is just a modification of the excellent starting point code found at net tuts. A lot of what I have here is duplicate code of theirs. First, I imagine you want to see what you are building. The final result looks something like this.

Tag Cloud Final Result

Tag Cloud Final Result

Like it? Good, cause that is what we are building. First off, lets write up the main display page.

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
    <head>
        <link rel="stylesheet" type="text/css" href="tagcloud.css">
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <title>Response Cloud</title>
    </head>
    <body>
    <center>
        <h2>What our Respondents are Saying...</h2>
        <div id="container">
            <div id="tagCloud">
                
                
            </div>
        </div>
        <script src="http://code.jquery.com/jquery-latest.js"></script>
        <script type="text/javascript">
            var cssRules = new Array();
            var cssRulesString = "";
            
            function loadCloud() 
            {

                var URL = 'http://yourserver.com/tagcoud.php';
                $.getJSON(URL&"&callback=?", function(data) {
                
                    //create list for tag links
                    document.getElementById('tagCloud').innerHTML = '';
                    $("<ul>").attr("id", "tagList").appendTo("#tagCloud");
                    
                    //create tags
                    var totalResponses = 0;
                    var styleValid = -1;
                    $.each(data.tags, function(i, val) {
                     totalResponses = parseInt(totalResponses) + parseInt(val.freq);                   
                    });
                    
                    $.each(data.tags, function(i, val) {
                        
                        var CssClass = "";
                        var percent = parseInt((val.freq / totalResponses) * 100);
                        
                        //create item
                        var li = $("<li>");
                        
                        //create link
                        $("<a>").text(val.tag).attr({title:"See all pages tagged with " + val.tag + " ("+val.freq+" Responses, "+percent+"%)", href:"http://localhost/tags/" + val.tag + ".html"}).appendTo(li);
                        
                        
                        //Start looking for a CSS class for this items occurance %. If it does not exist
                        //add 1% and try again. do this until a valid CSS class is found.                    
                    
                        for(i=percent; i<=100; i++)
                        {
                            styleValid = searchForStyle('.percent_'+i);
                            
                            
                            if(styleValid > 0)
                            {
                                CssClass = 'percent_'+i;
                                break;
                            }

                            
                        }
                        
                        li.children().addClass(CssClass);
                        
                        //add to list
                        li.appendTo("#tagList");
                        
                    });
                });
            }
            
            function searchForStyle(styleName)
            {
                return(cssRulesString.search(styleName));  //Search for the Cont                
            }
            
            function getStyles() {

            
                if (typeof document.styleSheets != "undefined") {   //is this supported
                    var cssSheets = document.styleSheets;
                    for (var i = 0; i < cssSheets.length; i++) {
            
                         //using IE or FireFox/Standards Compliant
                        rules =  (typeof cssSheets[i].cssRules != "undefined") ? cssSheets[i].cssRules : cssSheets[i].rules;
                
                         for (var j = 0; j < rules.length; j++) 
                         {
                            cssRules[j] = rules[j].selectorText;
                         }
            
                    }
                }
                
                cssRulesString = cssRules.toString();
    
            }
            
            getStyles();
            loadCloud();
          
            setInterval ( "loadCloud()", 5000 );
        </script>
        
        
        </center>
    </body>
</html>

So lets go over the trickier parts here. First the loadCloud function, this is the guts of the tag cloud here. This contacts the remote responder page, takes the results and creates the tag cloud. First in the URL variable, set that to wherever your page that is going to provide the information is going to be. Because this uses regular old getJSON and not the getJSONP both pages have to be in the same domain. Then it blanks out the content of the current cloud div. It creates some counters and begins interating over the responses received. The query returned by your responder should have two columns, one called tag, and one called freq. Tag is the actual text, freq is the number of times it occurs. So we loop over all the data returned (it should be in JSON format), figure out the percentage of the total this tag represents, which actually brings me to my next point about why my cloud is different. Because I don’t know how many tags there are going to be, or how many occurrences of each, just scaling them up indefinitly doesn’t make a lot of sense. So my tag cloud is % based. The higher % of the total a tag represents the bigger it is. So even if a tag is mentioned 100 times, if it’s only 10% of the total, it’s going to be a bit small in comparison to others. Next it creates an item in the list and appends it. The last part is where some of my magic happens.

Part of the problem with the original tag cloud app, is the only thing that changes in the tags are sizes. Fonts and colors stay the same and that is just no fun. I wanted more vibrant changes for my cloud, so I decided to see what I could do. First though was to hard code in some different styles, or maybe even do random ones, but that wasn’t very flexible, and not easy to maintain if we want changes. So I decided I had to have a CSS based style. I also knew that I didn’t want 100 different possible styles, so I would have to make my code smart enough to find the nearest possible style to the % occurrence of that tag. Say for example I had styles for 5%, 10% and 15% occurrences. Well what if a tag has 7% occurrence. Well that won’t work, becuase it would try to apply a style called .percent_7 and fail. So I wrote a quick function that searches through the attached stylesheet, and puts all the style names in a string. Then I can just search that string for the desired style name. If it doesn’t exist, add 1 and check again. As soon as a match is found, apply that style. So that is what the getStyles, and searchForStyle functions are for. Finding the closest possible CSS match. The beauty of that is you can just add a new style in the sheet, and it will instantly start being included in the tag cloud. So anyway, it finds a closely matching style and applies it to the list element.

Thats really about it. At the bottom you of course see the loadCloud function being called, as well as getStyles. Very last, loadCloud() is put on an interval timer, where it reloads every 5 seconds, so your cloud is dynamic. If that doesn’t suit you, you can remove that last line. Our clients wanted to watch the terms change in real time, so I put that in there.

So now lets talk about the responder page. Really all this page has to do is get data from the database, json encode it and return it. I had to use PHP since this is being hosted on some other servers, but I can write up a coldFusion version to really easily. Here is the PHP version.

<?php


    //connection information
  $host = "YOUR IP";
  $user = "YOUR USERNAME";
  $password = "YOUR PASSWORD";
  $database = "YOUR DATABASE";
    
    //make connection
  $server = mysql_connect($host, $user, $password);
  $connection = mysql_select_db($database, $server);


    //query the database
    $queryString = "SELECT DataColumn as tag, count(*) as frequency from YourTable GROUP BY tag order by Tag ";
    
    
    
    $query = mysql_query($queryString);
    
    //start json object
    $json = "({ tags:["; 
    
    //loop through and return results
  for ($x = 0; $x < mysql_num_rows($query); $x++) {
    $row = mysql_fetch_assoc($query);
        
        //continue json object
    $json .= "{tag:'" . $row["tag"] . "',freq:'" . $row["frequency"] . "'}";
        
        //add comma if not last row, closing brackets if is
        if ($x < mysql_num_rows($query) -1)
            $json .= ",";
        else
            $json .= "]})";
  }
    
    //return JSON with GET for JSONP callback
    $response = $_GET["callback"] . $json;
    echo $response;

    //close connection
    mysql_close($server);

?>

Pretty easy. This is almost exactly copied and pasted from the other example, but I put more work on the database here. Instead of having an actual column called frequency, I created one using aggregate functions, and aliased out my data column name as tag, so I don’t need to worry about what it is actually called. It’s a nice clean query that should work in any database system. After that, I iterate over the values, add them to a string, JSON encode the string and echo it out. There is some extra stuff there about callbacks, that is because the original example did use JSONP so it could do cross domain, but it was just making things overly complicated so I axed it. You can just ignore it, or add it back if you want.

Then, last but not least is the CSS for this beast. Like I said, all you have to do is add new entries, using the format .percent_XX where XX is a percent you want a custom style for.

body
{
    color:#0069E4;
    font-family:arial;
    font-size:14px;
}

#container {
    margin-top:50px;
    margin-left:auto;
    margin-right:auto;
    background:url(images/background.png) no-repeat 0;
    width:750px;
    height:700px;
    
    
}
#tagCloud {
    width:440px;
    text-align:center;
    padding:5px;
    overflow:hidden;
    font-size:70%;
    font-family:arial;
    padding-top:150px;
}

#tagList {
    margin:0;
    padding:0;
}
#tagList li {
    list-style-type:none;
    float:left;
    margin:0 10px;
    height:35px;
}
a
{
    text-decoration:none;
}
.percent_0
{
    color:#E71818;    
    font-family: Helvetica;
    font-size:100%;
}

.percent_3
{
    color:#E71818;    
    font-family:"Trebuchet MS", Arial, , sans-serif;
    font-size:100%;
}

.percent_5
{
    color:#709DD2;    
    font-family:Arial;
    font-size:150%;
}

.percent_10
{
    color:#78EF53;
    font-family:"Times New Roman";
    font-size:180%;
}

.percent_15
{
    color:#F337FA;
    font-family:Tahoma;
    font-size:220%;
}

.percent_20
{
    color:#FBB724;    
    font-family:Geneva;
    font-size:260%;
}

.percent_50
{
    color:#FDF277;
    font-family:Forte;
    font-size:320%;
}

Oh yeah, if you want the cloud background image, here it is.
Cloud Background

As a final note, do remember you can pass parameters between the pages in the URL. My application actually does this, so I can tell my responder what table and column to look in for data. I just removed it from the example to keep things simple. If you want the code for how to pass the info and use it, I can certainly do that, but I figured it was pretty straight forward.

In wrap up, the benefits of my cloud vs the original

  1. Percent based text scaling and styling, instead of raw count
  2. CSS Styled text, not hard coded.
  3. Easy to add new styles and adjust old ones
  4. Cleaner more efficient use of database
  5. I made it and it makes me feel good

Anyway, I hope this helps someone, as always let me know if you have questions.

4 responses

  1. mike

    Hey,

    This is awesome, I can’t get it to work though. tagcloud.php shows all of the correct tags, but i can’t get them to display on the main page. Hmmm…. Any idea?

    April 26, 2011 at 2:34 am

    • Do you have any more details? Error logs? screen shot? What happens if you visit the tag cloud page directly and pass arguments in the url? Do you have a sample I can check out?

      April 26, 2011 at 2:55 pm

      • mike

        Hi Kenji,

        Here are the locations of the two pages:

        http://eventvt.com/tagcloud.php
        http://eventvt.com/cloud1.php

        No errors… let me know if you need any other information. And, thank you!

        April 26, 2011 at 3:08 pm

      • Well using firebug, it seems as though your page is calling out to http://eventvt.com/0 and the content returned is the source code of that page. So I am thinking you have some kind of redirect or something going on. Maybe check the settings on your webserver? Otherwise, just for testing sake maybe take that JSON output you want it to read, host it somewhere else, and point your page at that. I see the callouts being made, it’s just getting the wrong content. Does that help?

        April 26, 2011 at 3:15 pm

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s