NetBeans Visual Web Pack - Real World Apps Tip #3 - Tag Clouds

Finally I've found the time to show you how to do tag clouds in NetBeans 5.5 with Visual Web Pack.  In my last blog, I showed you how to create dynamic Hyperlinks.  In the blog before that, I discussed how to do dynamic content.  Both of those topics provide necessary background information for creating a tag cloud.

First, let me provide a little background on how I tackled creating a tag cloud.  Since I really didn't know what a tag cloud was, I started with the Wikipedia's definition.  Next, since my task was not to be an expert on tag clouds but to implement one on the web site, I simply needed an algorithm to get me started.  I found an excellent white paper by Kevin Hoffman called, "In Search Of...The Perfect Tag Cloud".   Based on the Plugin Portal needs and Kevin's explanation, I decided to use the "Linear Distribution Algorithm".

Plugin Portal Details


The goal is to basically have a set of words that are displayed in different font sizes based on how many "things" there are for that word.  With the Plugin Portal, the words represent categories.  The bolder, bigger font category names are the ones with more entries in the category.  The sample project I'll include will include hard coded data but I thought I would take a moment to talk about the query behind the Plugin Portal that gave me the results I needed.  In the case of the Plugin Portal, I needed to define a type that contained a category name and a category count.  I created a class called "CategoryCount".  Here's the code including the query that creates the "CategoryCount" instances.

        String select = "select distinct o.categoryname, cc.cat_count from othercategoryimpl o," +
" (select distinct categoryname,count(categoryname) as cat_count from othercategoryimpl cc group by categoryname) as cc" +
" where o.categoryname=cc.categoryname order by o.categoryname";
Query query = em.createNativeQuery(select);
List resultList = query.getResultList();
Iterator resultIterator = resultList.iterator();
Vector currentResult = null;
while(resultIterator.hasNext()) {
currentResult = (Vector)resultIterator.next();
String categoryName = (String)currentResult.get(0); //category name.
Integer categoryCount = (Integer)currentResult.get(1); //count
CategoryCount currentRecord = new CategoryCount(categoryName, categoryCount.intValue());
newCache.add(currentRecord);
}

You can see from this SQL query that I really have to do two passes of the data.  The "from" target:

(select distinct categoryname,count(categoryname) as cat_count from othercategoryimpl cc group by categoryname) as cc
gets the actual count for each category.  Believe me this query took a while to get right. :)  I don't claim to be a SQL expert so if someone knows of an easier method, please let me know.

Creating the "Comparator"

Part of the goal also is to have the words sorted alphabetically so I chose to use "Arrays.sort".  To use this method, we need to create a specialized "Comparator" that knows how to compare our "Count" type.  For my simplified example, I will be creating "CategoryCount" as the "Count" type.  Here's the code.

public class CategoryCount {

/\*\* Creates a new instance of CategoryCount \*/
public CategoryCount() {
}

public CategoryCount(String inName, int inCount) {
setName(inName);
setCount(inCount);
}

/\*\*
\* Holds value of property name.
\*/
private String name;

/\*\*
\* Getter for property name.
\* @return Value of property name.
\*/
public String getName() {
return this.name;
}

/\*\*
\* Setter for property name.
\* @param name New value of property name.
\*/
public void setName(String name) {
this.name = name;
}

/\*\*
\* Holds value of property count.
\*/
private int count;

/\*\*
\* Getter for property count.
\* @return Value of property count.
\*/
public int getCount() {
return this.count;
}

/\*\*
\* Setter for property count.
\* @param count New value of property count.
\*/
public void setCount(int count) {
this.count = count;
}

}

Also we need a Comparator to sort this type.  Here's the "CountComparator".

import java.util.Comparator;


public class CountComparator implements Comparator {

/\*\* Creates a new instance of CountComparator \*/
public CountComparator() {
}
/\*\*
\* This method is used to compare o1 and o2.
\* @param o1 The first object to compare to o2
\* @param o2 The second object to compare to o1
\* @return a negative integer, zero, or a positive integer if o1
\* is less than, equal to, or greater than o2
\*/
public int compare(Object o1, Object o2) {

String o1categoryname = ((CategoryCount)o1).getName();
String o2categoryname = ((CategoryCount)o2).getName();
return o1categoryname.compareToIgnoreCase(o2categoryname);

}
}

The Linear Distribution Algorithm Details

The idea behind this algorithm is to figure out a range for a set of "Buckets" then distribute your items in the buckets according to where they fit into the range.  The first step is to decide on the number of buckets to use.  In a tag cloud you'll be deciding how many font size ranges you want.  For the Plugin Portal, we chose six buckets.  So to figure out the range, you need to take the max count of the items - the min count of the items and divide by the number of buckets.  This gives you the range to use for each bucket.  Here's the code I'll use in my sample to figure out the range.

    private long getCloudRange(int min, int max) {
/\*\*
\* For this tag cloud we will use the linear distribution algorithm with six different "buckets".
\* The algorithm is:
\* category weight = max number/category - min number/category
\* range = category weight/number of buckets (6).
\*
\*/
long weight = 0;
long range = 0;

weight = max - min;
if(weight < 6) {
/\*\*
\* We don't have a very big range of numbers in categories so we need to increase the
\* weight so the range will be bigger.
\*/
range = 1;
} else {
range = weight/6;
}


return range;
}
The other key piece of the implementation is to assign a font based on which bucket the item fits into.  Here's my sample method to do that.

    private String getFont(CategoryCount inCount,long bucketRange) {
String fontsize1="font-size: 10px; font-weight: normal;";
String fontsize2="font-size: 11px; font-weight: normal;";
String fontsize3="font-size: 12px; font-weight: normal;";
String fontsize4="font-size: 13px; font-weight: normal;";
String fontsize5="font-size: 14px; font-weight: normal;";
String fontsize6="font-size: 15px; font-weight: normal;";

/\*\*
\* Determine which of the buckets the count falls into.
\*/
if(inCount.getCount() >= 0 && inCount.getCount() <= bucketRange) {
return fontsize1;
} else if(inCount.getCount() >= (bucketRange\*1) +1 && inCount.getCount() <= (bucketRange\*1)+bucketRange) {
return fontsize2;
} else if(inCount.getCount() >= (bucketRange\*2) +1 && inCount.getCount() <= (bucketRange\*2)+bucketRange) {
return fontsize3;
} else if(inCount.getCount() >= (bucketRange\*3) +1 && inCount.getCount() <= (bucketRange\*3)+bucketRange) {
return fontsize4;
} else if(inCount.getCount() >= (bucketRange\*4) +1 && inCount.getCount() <= (bucketRange\*4)+bucketRange) {
return fontsize5;
} else {
return fontsize6;
}
}

Leveraging our dynamic Hyperlink experience, we will need a block of code like this create the actual Tag Cloud.

        /\*\*
\* Sort the CategoryCounts
\*/
Arrays.sort(counts,new CountComparator());

/\*\*
\* Get the range to use for the tag cloud.
\*/
long range = getCloudRange(minCategory,maxCategory);

/\*\*
\* Create all the hyperlinks and add them to the dynamic panel.
\*/
ArrayList<Hyperlink> hyperlinks = new ArrayList();
Hyperlink dynamicHyperlink = null;
for(int ii=0; null != counts && ii < counts.length; ii++) {
dynamicHyperlink = new Hyperlink();
dynamicHyperlink.setText(counts[ii].getName());
dynamicHyperlink.setActionListenerExpression(listenerMethod);
dynamicHyperlink.setActionExpression(actionMethod);
/\*\*
\* Get the font for this link which will be calculated based on the
\* count and where it falls into a range for each bucket.
\*/
String font = getFont(counts[ii],range);
dynamicHyperlink.setStyle(font);
dynamicPanel.getChildren().add(dynamicHyperlink);

}

The Finished Product

Running my sample project, you can see that we get a small cloud.

cloud

You can use my sample project for a good starting point.  I really wish I new how to create components.  This would make a great component wouldn't it!

Comments:

Thanks David... You always follow through with your promises. I will try this out with some things I want to post on my internal website. I will let you know how it works out. I would hope mine looks as good as the Netbeans site. I am curious why you chose Vector instead of another more current class. ;-)

Posted by John Yeary on July 16, 2007 at 12:54 PM MDT #

John,
If I remember correctly a Vector was the type returned from the "nativeQuery" I created. I just looked for the semantic-oriented docs again and couldn't find a thing. Remember this was one of my complaints in the BOF, JPA documentation. If you come across any documentation that contradicts my implementation, please let me know.
Thanks!
-David

Posted by David on July 16, 2007 at 02:26 PM MDT #

Post a Comment:
  • HTML Syntax: NOT allowed
About

David Botterill

Search

Archives
« April 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today