Thursday, April 22, 2010

How to Make a Tag Cloud in ASP.NET MVC

As many of you are aware, tagging offers users a quick and easy way to filter website content by interesting topics or categories and a tag cloud acts as a visually asethetic way to display links to all of the available tags with their prevelance indicated by font-size. Lately tag clouds have become very popular and they can be found all over the web in many different formats, but the most common format usually looks something like this:

tagcloud Thanks, Phil Haacked. :)

In this tutorial I will show you how to make a simple tag cloud with 5 different size distributions based on the frequency of each tag. The code I have built is contained inside of an HtmlHelper which accepts a Dictionary<string, int> containing the tags and their occurrence counts in each pair and a Func<string, string> which is called on each tag to produce the link which will be displayed. Before we build the tag cloud, we’ll need to get the tags out of our database along with their number of occurrences. This code will obviously vary based on your tagging implementation, but will probably look something like this:

(from tag in Tags 
 group tag by tag.Name 
 into g 
 select g).ToDictionary(g => g.Key, g => g.Count());

Once we have the tag list in hand we need to find the mimimum and maximum tag counts and establish a distribution between the difference of the minmum and maximum values.

var min = tagList.Min(t => t.Value);
var max = tagList.Max(t => t.Value);
var dist = (max - min) / 3;

The code for this tag cloud supports a total of 5 different sizes, but if you require more you can increase the divisor of the distribution equation and update the CSS class assignment logic accordingly. With the minimum, maximum and distribution values calculated we can now loop through all of the tags and assign CSS classes to each one based on their position in the distribution.

var links = new StringBuilder();
foreach (var tag in tagsAndCounts) {
    string tagClass;
    
    if (tag.Value == max) {
        tagClass = "largest";
    }
    else if (tag.Value > (min + (dist * 2))) {
        tagClass = "large";
    }
    else if (tag.Value > (min + dist)) {
        tagClass = "medium";
    }
    else if (tag.Value == min) {
        tagClass = "smallest";
    }
    else {
        tagClass = "small";
    }

    links.AppendFormat("<a href=\"{0}\" title=\"{1}\" class=\"{2}\">{1}</a>{3}", 
                        urlExpression(tag.Key), tag.Key, tagClass, Environment.NewLine);
}

After running the above code we now have all of the tag links in a StringBuilder which we can push to the output. Notice in the final line of the foreach loop I am calling urlExpression(tag.Key) this is the Func<string, string> I mentioned above that is used to return the Url for the tag.

Now that we have all the pieces, here's the full code for the HtmlHelper:

public static MvcHtmlString TagCloud(this HtmlHelper html, IDictionary<string, int> tagsAndCounts, Func<string, string> urlExpression, object htmlAttributes) {
    if (tagsAndCounts == null || !tagsAndCounts.Any())
        return MvcHtmlString.Empty;
    
    var min = tagsAndCounts.Min(t => t.Value);
    var max = tagsAndCounts.Max(t => t.Value);
    var dist = (max - min) / 3;
    
    var links = new StringBuilder();
    foreach (var tag in tagsAndCounts) {
        string tagClass;
        
        if (tag.Value == max) {
            tagClass = "largest";
        }
        else if (tag.Value > (min + (dist * 2))) {
            tagClass = "large";
        }
        else if (tag.Value > (min + dist)) {
            tagClass = "medium";
        }
        else if (tag.Value == min) {
            tagClass = "smallest";
        }
        else {
            tagClass = "small";
        }

        links.AppendFormat("<a href=\"{0}\" title=\"{1}\" class=\"{2}\">{1}</a>{3}", 
                           urlExpression(tag.Key), tag.Key, tagClass, Environment.NewLine);
    }
    
    var div = new TagBuilder("div");
    div.MergeAttribute("class", "tag-cloud");
    div.InnerHtml = links.ToString();
    
    if (htmlAttributes != null) {
        div.MergeAttributes(new RouteValueDictionary(htmlAttributes), false);
    }
    
    return MvcHtmlString.Create(div.ToString());
}

And here's the appropriate CSS:

.tag-cloud { text-align: center; }
.tag-cloud a { white-space: nowrap; padding: 5px; display: inline-block; }
.tag-cloud a:hover { background-color: #E1E1E1;}
.tag-cloud .smallest { font-size: 75%;}
.tag-cloud .small { font-size: 100%; }
.tag-cloud .medium { font-size: 125%;}
.tag-cloud .large { font-size: 150%; }
.tag-cloud .largest { font-size: 200%; }

In order to use the above code simply place it in an extension method class, reference the CSS, and call it your view like so:

<%= Html.TagCloud(Model.Tags, t => Url.Tag(t), null) %>

Sources

  • The basis for this code comes from Pete Freitag's Blog in a similar post that discusses building a tag cloud with ColdFusion.

No comments: