Friday June 22, 2007
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.
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!
( Jun 22 2007, 03:46:56 PM MDT )
Permalink

|

|
Trackback URL: http://blogs.sun.com/david/entry/netbeans_visual_web_pack_real1
Posted by John Yeary on July 16, 2007 at 07:54 PM MDT #
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 09:26 PM MDT #