Building an Ajax-Based Tag Cloud Drill Down Tool With jQuery and Ruby on Rails

ajax-tag-drilldown

Overview

I’ve been working on a site recently that allows people to upload files and add 3-10 descriptive tags for each file.  We thought it would be cool to have a tag cloud that allowed you to drill into the tags to load the next level of tags that were related to that tag.  At any point, you can select tags to add to your search list, and once you’ve selected all of the tags you’re interested in, you can hit search to find files with those tags.

The architecture isn’t really complicated, and includes jQuery, one Ajax call, JSRender (for templating), and Ruby on Rails.

 

Example

Here is a screenshot of the working tag cloud:

tag-cloud-1

When you click on any “plus” icon, the tag is selected, and moves into the Selected Tags list on the left.  A list of the selected tag ids is held in a hidden field on the page (specifically within the Selected Tags form), and clicking the “X” icon removes the selected tag.

tag-cloud-with-selections

When you click the “drill” icon (as indicated by the “reload” icon), a JQuery Ajax call hits the server with the tag id selected, and loads the next list of tags that were also tagged with the selected tag, ordered by frequency.  In the DOM, the current list of tags fades away and the new list fades in.  So, for instance if you drill into #techno, you get the following list (please bear with the seemingly senseless list of tags…I generated a bunch of data using a rake task in order to test this out):

tag-cloud-drilled-in

Database Schema

Here is the database schema:

files
id
name

file_file_tags
id
file_id
file_tag_id

file_tags
id
name

 

The Model

I have three ActiveRecord models called FileTag, FileFileTag, and File.  In short, a File has_many FileTags through FileFileTag. Here is the File model:

 

SQL for the Initial Tag Load

In the FileTag model, I have the following code to get the initial set of most-used tags (Limited to 10 in my app):

 

SQL for Related Tags

Here is the query to return those tags related to (also tagged with) the tag you are drilling into:

I imagine this SQL will need optimization at some point.  I loaded up 10,000 sample files, each with 4 random tags, and was able to load the results of this query in about 180ms.  Not screaming, but will work till we have quite a bit in the database.  It may be better to use a flat lookup table for the tags or something similar in addition to the one-to-many 3-table schema we have currently.  I’d love to hear some ideas if you have any.

I’m also excluding a list of tag ids that have already been seen by the user (during one search).  I keep this list in the view and pass it to the server with each Ajax drill call.  My assumption is that if a person has already seen a set of tags, they don’t want to see them again.  This keeps the related tags list fresh.  At this point, I’m not sure whether this is the best approach or not, but I’m going with it until I analyze it some more.

 

The Controller

I load the initial tag list in my content controller’s index action (the homepage in my app) as follows:

I also have another file_tags controller with the following:

The reason the methods are in different controllers is because the initial display of the tag list is on the homepage, which is rendered from a controller specifically for non-resource-based views (the ContentController).

 

The View

Once the initial tag list (@file_tags) is loaded in the controller, I loop through the results and render the tags in the view:

The first line shuffles the order of the tags, so it looks “cloud-like” (I’m not sure why this aesthetic is preferred in tag clouds, but what the hey):

The interesting part of the view code is the calculation of the font size and margin of each tag.  I wanted the font size and surrounding margin of the tag to change based on the number of times the tag was used.  Here is my calculation of the font size:

Initially, I tried to base the font size on the current tag’s usage count as a percentage of the highest used tag.  Something like:

( current_tag.nbr_tags / highest_tag_count ) * 24 (pixels)

But often the counts are so similar among the top 10 results that the size doesn’t vary much from tag to tag.  For instance, my results are similar to the following:

nbr_tags
176
156
154
148
148
147

… and so on

I tested the approach and ended up with 6 tags somewhere between the size of 23px and 24px.  What I really wanted was a distribution between 12px and 24px.  So the smallest tag count would be 12px, and the largest 24px.  I came up with the following formula based on the ratio of the difference between the ranking of each tag and the highest-ranked tag, and the spread between the highest and lowest ranked tag:

That is the percentage I multiply by a total of 12 possible pixels variance between the highest and lowest font size, like so:

The margin is calculated similarly, except that the highest margin size is 12px, and the lowest is 0px. So:

This calculation is done inline in the view as shown above.

 

CSS

The file-tag-drilldown CSS is as follows:

 

Tag Selection

When the select tag icon is clicked, the tag id gets added to a hidden field which is a comma-separated list of currently-selected tag ids, and a UI event fires that adds a div for the tag to the Selected Tags list.  Here I’m using JsRender, which allows you to create a template for the HTML you want to add to the DOM:

The selected tag template is as follows:

After Rails processes this template, the result looks like this:

The values wrapped in {{}} in the template will be substituted by JSRender for the values you pass into the render call and the new HTML template is returned.  Here is an example of a JSRender’ed template:

The bind_deselect_tag_clicks method is as follows:

 

 

Tag Deselection

To remove selected tags from the list, the user can click the “X” button on the tag.  That calls the following method:

 

Toggling the Selected Tags submit button

When a tag is selected or deselected, I enable/disable the submit button with this function:

 

The Ajax Drill Down

This code binds the drill tag icons and is called after the page loads:

 

The Drill Down Controller Method

In the file_tags_controller, I have a method that queries related tags and returns the data as JSON:

 

Processing the Drill Down JSON

Here is some example JSON returned from the controller:

Each element of the array has 3 fields – id, name, and nbr_tags.

This is the Javascript function that processes the returned JSON:

The first thing I do is append the new list of data ids to the list of already seen ids.

Then I fade out the current list of tags in the DOM.  The  JQuery .promise() method ensures that the next code is executed after the fade out has completed.  After the items fade out, I remove them entirely from the DOM:

If there isn’t any more data, I print “End of the line!”.

If there is data, I then get the highest and lowest number of tags returned (the first and last element in the data array) to calculate the font size and margin (we have to calculate those in the Javascript as well).  As I’m writing this, I can see possibly moving all size calculations to Javascript.  That way the font/margin calculations are all done in one place.

I then shuffle the array to once again get a more cloud-like result.

Here is the shuffle_array function.  I believe there is an actual shuffle array method built into some browsers, but I think older browsers don’t support it.

After shuffling the array, I loop over each item in the shuffled array using JSRender again to render HTML templates for the tags.  The result of these rendered templates is added to an array:

Here is the template for new tags (from the Rails view):

After Rails processes the template, the raw HTML will be:

As part of the module pattern I used for the cloud Javascript, I have the following variables for storing the min/max font and margin sizes:

Here are the methods to calculate the font size and margin in Javascript (exactly the same logic as the view):

After I render the new tag HTML into an array, I join the elements of the array into one big string, and append it to the file tags container, fading them in.  After that animation is completed, I bind clicks on the new tags (see method definition above).

The new tags are now shown in the view, and the user can further select and drill into the new tags.  Subsequent drill downs are processed exactly the same way.

 

The End

I hope this has been helpful to people interested in building something like this.  I’ll continue to refine the code, and report how it performs in a production environment.  If anyone would like to see more code, send me an email at blakeleemiller@gmail.com and I’ll be happy to share it.

Cheers!
Blake

Blake Miller

I'm a freelance web developer specializing in front and backend development in Ruby on Rails and PHP.

Blake Miller wrote 1 post