Tag Cloud in ASP.NET MVC using Clickable HTML 5 Canvas Elements

Posted by: Sumit Maitra , on 1/31/2012, in Category ASP.NET MVC
Views: 107627
Abstract: In this article, we will create a clickable tag cloud using HTML 5 Canvas, LINQ and ASP.NET MVC

In our previous RavenDB and SignalR articles, we built a small blogging application in ASP.NET MVC. While building those Apps, I realized a ‘Tag Cloud’ is a nice feature that we could add.

So to expand on that idea, in this article

- We will build a Tag cloud functionality using HTML5 Canvas. Though HTML5 or Canvas is not a pre-requisite to build a Tag Cloud, we will take it as a use case to learn

  • How to Draw text on canvas

  • Measure height of text on canvas

  • Measure width of text on canvas

  • Set Font family and size of text canvas

  • Make canvas items clickable. By default, canvas doesn’t provide any way of tracking click in individual items on it. A canvas is what it’s name implies - a blank slate. It will raise a click event if you click on it, but to detect what you clicked on, you need to wire up some code.

- The Server side code will have examples of

  • How a POCO can be made query-able in LINQ.

  • Chain LINQ queries

Let’s get started. You can get the RavenDB code from here. The final code base is available over here

 

Step 1: Build and Run the code.

Step 2: Since we start off with an empty database, it will be good to have some test data for us. You can use the attached script (in the final code base) to generate some test blogs.

Build and run the Application once to create the Database (if it doesn’t exist already). Start SQL (Express) Management Studio and execute the GenerateBlogData.sql to generate some test data.

You could create some blogs manually too. (Don’t forget to mark the newly created blogs as Published else you will have to log in to view them).

Step 3: The Tag cloud should be present in the blog’s Index page. So let’s update the Index.cshtml. We add an external <div> to encapsulate the entire blog list and Canvas element, and set its width to 900px.

There are two inner <div> elements, one containing the blog items and the other containing the canvas that will have the tag cloud.

index-tag-cloud-div

Step 4: To get data for the tags, we will make an asynchronous call using jQuery to an action method in our Blog controller. Drop in the following script to wire up the controller

js-load-tags

This above code calls the Tags action method on the Blog controller. It will return a string representing a JSON array.

Step 5: The populateTagCloud parameter is a method delegate that will be called once the Action method returns. The implementation is as follows

a. Get the canvas object (tagCloudCanvas)

b. Retrieve the 2D context

c. Convert the string returned from the controller into a JSON object

d. lastXPos and lastYPos handle manage the top left (x, y) co-ordinates as we loop through each tag returned

e. TagSize is a weighted value calculated in the service layer that tells us how big, font of each tag should be. We can use logic like number of hits, most recently accessed or simply the number of blog items with the particular tag to determine the size. The frontend is not concerned with the logic but simply the value of TagSize. As we see below, TagSize is multiplied by ten (10) to get to the final font size.

f. Font size and font family is then set to canvasContext.font property. You can change this value before every attempt to draw text

js-populate-tag-cloud

g. Once the font size and font is set, we retrieve the width of the tag’s text using the canvas context’s measureText API.

h. The ‘if’ condition checks if the amount of space remaining after the last write is enough to fit in the new tag. If it is, the new tag is written to the right of the old tag.

i. Else it’s written below the previous tag. As you can see positioning is absolute with respect to canvas position.

j. The call to filltext method actually paints the text.

k. tagClickHandler is the method delegate that will actually be called when user clicks on a the tag text in the canvas.

Step 6: The createClickMap function

js-create-clickmaps

a. The createClickMap function is passed the following parameters

  • The canvas object on which the operations are happening

  • Top left (x, y) coordinates of the area containing the tag text

  • Width (w) and Height (h) of the text

  • Function delegate that should be called if conditions are fulfilled.

  • And the Json object containing the Tag Name, Id and other details

b. The first line attaches a click event handler.

c. The inline function generates the relative positions mX and mY for the click location.

d. Checks if the function m returns true by passing it the text position and the click position. By an interesting use of inline function, we have avoided explicit declaration of a map to store the text’s location. Instead we let the JS interpreter manage the function tables, where each function will have it’s own set of values for x, y, w, h, mx and my.

e. Reference: David Pirek’s blog here.

f. The tagClickHandler method generates the Url to the Index page with the tag name as parameter and redirects the page to this Url. This covers all the UI changes we had to do. At this point, if you are a non-ASP.NET developer, you would have realized that if you pass a JSON stream to the Tag cloud, the cloud will render itself and make itself clickable.

If you are looking for JavaScript only solution, you could very well stop here. But if you are a .Net junkie let’s move on and wire up the controller and the controller with the layers further down.

Step 7: Retrieving unique Tags for the tag cloud

a. If you remember, we did not have a ‘Tag’ domain object because at the domain level all we needed earlier was a string representing all the tags. But now we need a separate Tag object so that we can query it, filter it and get unique values out of it.

b. We add a new Tag class to the Domain model. The code looks as follows:

tag-iequatable

c. As we can see it’s not a POCO rather a class that implements IEquatable<Tag> interface. As we will see shortly we need only unique Tag names and to control whether an object is should be treated as Equal or not in LINQ we need to implement this interface.

d. In the above code, we implement Equal functionality to return true if the Id and the Name of the tag are the same. As we have learnt, since the wee days of dot net, if you have a custom Equals method you should override GetHashCode and make sure you provide unique hash codes yourself. Hence we have overridden the GetHashCode method as well.

Step 8: Updating the Repository implementations

a. With the tag cloud in place, we will update the repository to return us a list of tags. We will also add an overloaded GetPosts method that will accept isAuthenticated and tagName as parameters and return blogs with the given tagName only.

b. Add the following methods in the abstract class BlogPostRepository

abstract-repository-new-methods

c. Update the RavenBlogPostRepository.cs with empty methods

d. Update the SqlBlogPostRepository.cs with the following GetTags method implementation

repository-get-tags

Let’s look at the LINQ query inside the if(userAuthenticated) in a little bit of details. The inList var stores a query to get all the Tags in the system. Now we need the count of how many times a tag has appeared on the blog. This will be a factor that we use to indicate the font size of the tag. The postTags query that selects all the BlogPosts and then flattens them out using the SelectMany to give us a list of the Blog-to-Post relationships.

Next we do a join with the PostTags to determine the tags in use.

The select new statement actually creates a projection of the return values. In this case, we are able to project it into a Domain Tag object. So we end up populating outList with required Domain Tag objects.

The LINQ query in the else statement does an additional join to filter out Tags associated with blogs that are not in Published status. Essentially, we don’t consider Tags that are associated with blogs in Draft status.

e. Similar to the GetTags method we implement the GetPosts method as follows

repository-get-posts

This first query returns a list of domain BlogPost objects filtered by the tagName. The second query filters it for blogs in published status only.

Note: Since we are calling post.ToDomainBlogPost() in the query, we need to load the BlogPosts eagerly (no deferred execution). So in this case, inList actually has the data. If we don’t do that, LINQ will defer execution to the point of posts.ToList<Domain.Model.BlogPost>(). Now when it executes the query, it tries to project the list into Domain.Model.BlogPost instead of Data.Model.BlogPost. This results in a method not found exception.

Step 9: Updating the ProductService

Now that the Repository is setup to return Tags and Posts filtered by tags, let’s update our service layer. One thing we want to do is ‘normalize’ the PostCount value so that if one tag has 1000 posts associated whereas another has only 1 post associated, we don’t end up with Tags that are too big or too small to see. To do that we use the following algorithm:

a. We decide we will show say 10 different sizes of fonts (MAX_FONT_SIZES_COUNT).

b. This count does not co-relate to number of distinct tags or number of blogs each tag is associated with. It is simply the number of different font sizes we will use to represent the tags

c. Determining density: It is the factor that tells how many different tags we will have to lump together in each font size. In our case we are using how many distinct buckets of post tags we have divided by the maximumAllowedCount, which is either MAX_FONT_SIZES_COUNT or the actual number of count.

d. We choose number of posts a tag is associated with as the factor determining the size of the tag font. So we get a list of these numbers. (it is stored in distinctCounts).

e. Then we loop through them and based on the density assign the count a font size. This mapping helps keep the calculate n+m where n = distinct post counts and m = number of tags.

f. Finally we loop through the tags and based on the PostCount assign the font size.

The final code looks as follows:

service-get-tags

Step 10: Updating the Controller.

Getting the Tags

a. The tag cloud calls an Action method called Tags that returns a JSON object. So we will update our BlogController with the following method.

controller-tags

b. This method instantiates the BlogPostService and calls the GetTags method. It also passed the User.Identity object so that we pull up articles based on whether the current user is logged in or not.

c. The service method returns an IEnumerable<Tag> that is converted using the Json helper method.

d. Note the JsonRequestBehavior.AllowGet parameter. Without this method ASP.MVC will block Json requests and you will get null values instead of the Json stream at the front-end.

Filtering Index based on tag name.

a. We update the existing Index method to take a string parameter that is the tag name selected.

b. If the name is null or empty we call the old GetPosts method. Else we call the new GetPosts method and pass it the tag name.

controller-index

c. There is one slight problem here, the Routing engine by default assumes the parameters being passed in an action method map to an Id. It’s easy to pass an Id from the UI but the Url would like like

http://myblog.com/Blog/Index/1 where 1 stands for the tag’s Id. Instead we want it to say http://myblog.com/Blog/MVC to indicate the current page is showing MVC tags only. To do this, we need to update the Routing table. The Routing table is configured in the Global.asax. The changes are highlighted below. The named parameter name corresponds to the input parameter name in the Action method.

register-routes

Other Changes

a. There are some more cosmetic CSS change for the bread-crumb, the header and footer for each blog post.

b. All the while I was testing without RavenDB server running so I kept getting exceptions. Put in a little exception handling for that.

Conclusion

We started with the simple aim to create a tag cloud and in process, learn a bit of HTML5 Canvas. However on the way we also looked at LINQ and ASP.NET MVC routing.

The next step would be to add hover support on the canvas so that when you hover on the Tags, they change cursor and color to look more realistic.

Last but not least, we are looking at basic HTML5 Canvas manipulation hence the low-level javascript work. You could easily use something like kinetic.js canvas library that encapsulates the creation of Canvas elements and provides extensions for click and hover and drag and drop handlers. We leave that for another day.

The entire source code of this article can be downloaded over here

Give a +1 to this article if you think it was well written. Thanks!
Recommended Articles
Sumit is a .NET consultant and has been working on Microsoft Technologies since his college days. He edits, he codes and he manages content when at work. C# is his first love, but he is often seen flirting with Java and Objective C. You can follow him on twitter at @sumitkm or email him at sumitkm [at] gmail


Page copy protected against web site content infringement by Copyscape


User Feedback
Comment posted by Alex on Saturday, February 4, 2012 3:12 AM
Your articles are sensational! Keep up this great work!
Comment posted by henry on Thursday, March 15, 2012 5:29 AM
Great work came at a perfect time for me,<a href="http://seotoolster.com/seotoolster/html5-player/html5-video-player-audio-player/ "> html5 music player</a>would be really cool if we could at least get an option to enable the HTML5 mode player in the next Subsonic release. Not only would it bypass the need for flash, but it also has some extra features (wav playback, etc).It seems the biggest issue would be with flv video, but by transcoding to mp4 instead of flv, that could be solved. Even without the video fix, making it optional would allow those of us the mainly use Subsonic for music to benefit from the new player.
Comment posted by henry on Wednesday, March 28, 2012 2:13 AM
Great article and HTML 5 example.. thanks for the good read.<a href="http://swfto.html5-converter.com/">swf to html5 converter</a>
Comment posted by genefer on Monday, April 30, 2012 5:55 AM
I am exicited about HTML5 but Flash can do all these things <a href="http://html5socket.com/">html5</a> can do in a better way.Creating slideshow in HTML5! wow! what, flash did that 10 years ago! It is very easy to create a flash animation, for example a ball bouncing in flash professional in less than am minute. Javascript is a mess when compared to AS3.
Comment posted by aa on Thursday, August 30, 2012 6:08 AM
fgfg

Post your comment
Name:  
E-mail: (Will not be displayed)
Comment:
Insert Cancel