« Zurück

Social Visualization and Analytics with Liferay

Company Blogs 20. April 2011 Von James Falkner Staff

Recently I did a Liferay LIVE presentation on driving community participation with Liferay.  I'll post links to the video and slides once they are available.  The main topic was how to encourage community participation on sites that are based on Liferay.  There are many features in Liferay that make it easy to create and maintain good community sites.  One of the most underrated features in Liferay 6 is Social Equity, which is a social value system, essentially assigning a number to each person or piece of content.  The number represents the quality of the thing it is assigned to.  So an individual's equity value is a measure of their value to the community, based on content they've created and feedback from other users.  The value assigned to a piece of content (such as a blog post) represents the value of the content itself, based on user feedback.  Higher valued content is "better" in some way than lesser-valued content.  Same for people.

There have been a couple of writeups made regarding Social Equity in Liferay, and make for a great pre-read before continuing on this blog post!

Once Social Equity is enabled by an Administrator, applications which enable user content generation in Liferay (such as Blogs, Wikis, Message Boards, and Web Content) will contribute equity to users and content (information)  when actions occur (such as authoring a blog post, rating a forum thread, or commenting on a wiki page).  These equity values can then be exposed via visualization widgets such as Liferay's Top Users portlet.

One of the things missing in today's Liferay are other interesting visualizations of Social Equity data.  There are a bunch of use cases that Social Equity is good for, and if we had visualizations for them it would really encourage community participation.  Here are several of them:

  • Show the overall contribution and participation equity across all communities for a user
  • Show all tags associated with content created by a user, ordered by Tag Equity
    • Objective: show expertise of a user
  • Show all contributions from a user ordered by information equity
    • Objective: show a user’s contributions with high value
  • Show the top xx communities
    • Objective: Motivate communities to improve ranking
  • Show the top xx contributors or countries of all or a specific community
    • Objective: Motivate people to get into a top ranking
  • Show the top xx content of all or a specific community
    • Objective: Show the most valued content in a community
  • Show the top xx experts for tag yy
    • Objective: Show top experts for a given subject

These are highly useful visualizations, and are possible using basic equity information available to Liferay applications.  Unfortuntely, they do not exist today.  This is where you and I come in!  Below are two examples of simple yet powerful visualizations that align with two of the above use cases.  Can you think of more use cases that involve personal, tag, or information equity?

1. Equity-based Tag Cloud

This example builds on the popular notion of a Tag Cloud (indeed, Liferay has a built-in Tag Cloud portlet, but this portlet ranks tags solely on number of mentions of a tag, regardless of social value).  Ultimately it would be nice if the built-in portlet had this option (to order by equity), but it doesn't yet, and for this demo, I found an nifty 3D tag cloud (which relies on jQuery).  To calculate equity of a tag, you simply add up all the information equities for content that has been tagged with a given tag.  So, the pseudocode for this algorithm is:

for each asset a in community {

  for each tag t on asset a {

    Add [t, equity(a)] to Map<Tag, EquitySum> m;

  }

}

display resulting map m in tag sphere;

In this example, each tag's equity value is normalized into a range [0, 100), so that tags with higher equity values have larger font sizes.  In addition, when you click on a tag, it uses Liferay's Shared Render Parameter support to cause any other portlets on that page that support the tag parameter to update their displays to only show content tagged with that specific tag.

One thing to keep in mind with the below code: it is not very performant!  Every time the portlet is rendered, the equity sums are re-calculated.  For lots of tags or assets, this does not scale.  A real solution would calculate and store tag equities as actions occur, to avoid having to re-calculate every time.  I leave that as an exercise to the reader.

This portlet is a very basic MVCPortlet-based portlet.  There is no java portlet class.  There are just a couple of JSPs.  It is the default portlet type when using the Liferay IDE.  So a very easy way to use these examples is to create a new Liferay Portlet project in Liferay IDE, cut and paste the below files on top of the boilerplate files that Liferay IDE gives you, and deploy to your configured Liferay instance.  I also didn't bother to factor out imports, includes, or CSS into separate files, just for simplicity.  So everything is inline (please don't kick me out of the developer club).  The portlet uses Liferay's Asset Framework APIs, Tag APIs, and Social Equity APIs.

Finally, there are some tunable parameters for this, which are exposed using Liferay's Portlet Configuration Framework.  This way, to tune a tunable, you simply visit the portlet's configuration page, edit the value, and click save. That is represented by entries in liferay-portlet.xml and the configuration.jsp source code found below.  This uses the AlloyUI component library to render configuration items.

  • view.jsp (main algorithm and html):
<%@page import="com.liferay.portal.util.PortalUtil"%>
<%@page import="java.util.Calendar"%>
<%@page import="java.util.Collections"%>
<%@page import="java.util.Comparator"%>
<%@page import="java.util.Date"%>
<%@page import="com.liferay.portlet.asset.model.AssetTag"%>
<%@page import="java.util.ArrayList"%>
<%@page import="java.util.HashMap"%>
<%@page import="java.util.Map"%>
<%@page import="com.liferay.portal.kernel.util.GetterUtil"%>
<%@page import="com.liferay.portlet.PortletPreferencesFactoryUtil"%>
<%@page import="com.liferay.portal.kernel.util.Validator"%>
<%@page import="com.liferay.portal.kernel.util.ParamUtil"%>
<%@page import="javax.portlet.PortletPreferences"%>

<%@page
        import="com.liferay.portlet.asset.service.AssetEntryLocalServiceUtil"%>
<%@page import="com.liferay.portlet.asset.model.AssetEntry"%>
<%@page import="java.util.List"%>
<%@page
        import="com.liferay.portlet.asset.service.persistence.AssetEntryQuery"%>
<%@ taglib uri="http://java.sun.com/portlet" prefix="portlet"%>

<%@ taglib uri="http://liferay.com/tld/aui" prefix="aui"%>
<%@ taglib uri="http://liferay.com/tld/portlet" prefix="liferay-portlet"%>
<%@ taglib uri="http://liferay.com/tld/ui" prefix="liferay-ui"%>
<%@ taglib uri="http://liferay.com/tld/theme" prefix="liferay-theme"%>

<portlet:defineObjects />
<liferay-theme:defineObjects />

<style type="text/css">
.tagCloud ul,.tagCloud li {
        list-style: none;
        margin: 0;
        padding: 0;
}

.tagCloud .tagClass img {
        border: 0 none;
}

.tagCloud .tagClass a {
        font-family: Arial;
        font-weight: normal;
        padding: 3px;
        text-align: center;
        text-decoration: none;
        vertical-align: middle;
        color: yellow;
}

.tagCloud .tagClass a:hover {
        border: solid 1px #ffdb00;
        color: #ff00ff;
        text-decoration: none;
}

#tagCloud1 {
        -moz-border-radius: 15px;
        border-radius: 15px;
        background: grey;
}
</style>
<%

PortletPreferences preferences = renderRequest.getPreferences();

String portletResource = ParamUtil.getString(request,
                "portletResource");

if (Validator.isNotNull(portletResource)) {
        preferences = PortletPreferencesFactoryUtil.getPortletSetup(
                        request, portletResource);
}

int MAX_TAGCLOUD = GetterUtil.getInteger(preferences.getValue(
                "maxTags", "50"));
double ANIMATION_TIME = (GetterUtil.getInteger(preferences.getValue(
                "animationTime", "1"))) / 10.0;

int EQ_CUTOFF = GetterUtil.getInteger(preferences.getValue(
                "eqCutoff", "1"));

final int EQ_NORMALIZED_RANGE = 100;

        // get all assets in this group (community)
        AssetEntryQuery q = new AssetEntryQuery();
        long[] ids = new long[] { scopeGroupId };
        q.setGroupIds(ids);
        q.setVisible(true);
        List<AssetEntry> entries = AssetEntryLocalServiceUtil.getEntries(q);
        final Map<String, Double> eqValues = new HashMap<String, Double>();
        final Map<String, Integer> eqNormal = new HashMap<String, Integer>();
        double maxEq = 0;

        // iterate over all assets and their tags, and build equity map
        for (AssetEntry e : entries) {
                for (AssetTag tagA : e.getTags()) {
                        Double oldVal = eqValues.get(tagA.getName());
                        if (oldVal == null) {
                                oldVal = (double)0;
                        }
                        Double newVal = oldVal + e.getSocialInformationEquity();

                        eqValues.put(tagA.getName(), newVal);
                        if (newVal > maxEq) maxEq = newVal;
                }
        }

        // normalize equity values to range [0, EQ_NORMALIZED_RANGE]
        for (String tag : eqValues.keySet()) {
                eqNormal.put(tag, (int)Math.round( ((double)eqValues.get(tag) / maxEq) * (double)EQ_NORMALIZED_RANGE));
        }

        // generate sorted list of tag equity values, eliminating those under the cutoff value
        List<String> sortedEq = new ArrayList<String>();
        for (Map.Entry<String, Double> entry : eqValues.entrySet()) {
                if (entry.getValue() > EQ_CUTOFF) {
                        sortedEq.add(entry.getKey());
                }
        }

        // sort the resulting list
        Collections.sort(sortedEq, new Comparator<String>() {
                public int compare(String s1, String s2) {
                        double eq1 = eqValues.get(s1);
                        double eq2 = eqValues.get(s2);
                        if (eq1 == eq2) return 0;
                        else if (eq2 > eq1) return 1;
                        else return -1;
                }
        });
        
        // truncate so our tag sphere isn't too CPU-intensive
        if (sortedEq.size() > MAX_TAGCLOUD) {
                sortedEq.subList(MAX_TAGCLOUD, sortedEq.size() - 1).clear();
        }
%>

<div id="tagCloud1"></div>

<script
        src="<%= PortalUtil.getStaticResourceURL(request, request.getContextPath() + "/js/jquery-1.4.1.js", new Date().getTime()) %>"
        type="text/javascript"></script>
<script
        src="<%= PortalUtil.getStaticResourceURL(request, request.getContextPath() + "/js/js-cumulus.min.js", new Date().getTime()) %>"
        type="text/javascript"></script>

<script type="text/javascript">

var tagCloud1;

var tags = [
        <%
                for (String tag : sortedEq) {
                        Integer tagEqNormal = eqNormal.get(tag);
        %>
        
        new Tag("<%=tag%>",<%=tagEqNormal%>, "<portlet:renderURL><portlet:param name="tag" value="<%=tag%>"/></portlet:renderURL>"),
        <%
                }
        %>
        ];
        tagCloud1 = new TagCloud(document.getElementById("tagCloud1"), tags, 400, 400, {AnimationTime:<%=ANIMATION_TIME%>, HoverStop:false});
        
</script> 
  • configuration.jsp (used to configure tunables such as maximum # of tags to display):
<%@page import="com.liferay.portal.kernel.util.Constants"%>
<%@page import="com.liferay.portal.kernel.util.GetterUtil"%>
<%@page import="com.liferay.portlet.PortletPreferencesFactoryUtil"%>
<%@page import="com.liferay.portal.kernel.util.Validator"%>
<%@page import="com.liferay.portal.kernel.util.ParamUtil"%>
<%@page import="javax.portlet.PortletPreferences"%>
<%@ taglib uri="http://java.sun.com/portlet_2_0" prefix="portlet"%>
<%@ taglib uri="http://liferay.com/tld/aui" prefix="aui"%>
<%@ taglib uri="http://liferay.com/tld/portlet" prefix="liferay-portlet"%>
<%@ taglib uri="http://liferay.com/tld/security"
        prefix="liferay-security"%>
<%@ taglib uri="http://liferay.com/tld/theme" prefix="liferay-theme"%>
<%@ taglib uri="http://liferay.com/tld/ui" prefix="liferay-ui"%>
<%@ taglib uri="http://liferay.com/tld/util" prefix="liferay-util"%>

<%@ page contentType="text/html; charset=UTF-8"%>

<portlet:defineObjects />
<liferay-theme:defineObjects />

<%
        PortletPreferences preferences = renderRequest.getPreferences();

        String portletResource = ParamUtil.getString(request,
                        "portletResource");

        if (Validator.isNotNull(portletResource)) {
                preferences = PortletPreferencesFactoryUtil.getPortletSetup(
                                request, portletResource);
        }

        int maxTags = GetterUtil.getInteger(preferences.getValue(
                        "maxTags", "50"));
        int animationTime = GetterUtil.getInteger(preferences.getValue(
                        "animationTime", "1"));
        int eqCutoff = GetterUtil.getInteger(preferences.getValue(
                        "eqCutoff", "1"));
%>

<liferay-portlet:actionURL portletConfiguration="true"
        var="configurationURL" />

<aui:form action="<%= configurationURL %>" method="post" name="fm">
        <aui:input name="<%= Constants.CMD %>" type="hidden"
                value="<%= Constants.UPDATE %>" />
        <aui:fieldset>

                <aui:select label="Maximum Tags to Display" name="preferences--maxTags--">
                        <aui:option label="5" selected="<%= maxTags == 5 %>" />
                        <aui:option label="10" selected="<%= maxTags == 10 %>" />
                        <aui:option label="15" selected="<%= maxTags == 15 %>" />
                        <aui:option label="20" selected="<%= maxTags == 20 %>" />
                        <aui:option label="30" selected="<%= maxTags == 30 %>" />
                        <aui:option label="40" selected="<%= maxTags == 40 %>" />
                        <aui:option label="50" selected="<%= maxTags == 50 %>" />
                        <aui:option label="100" selected="<%= maxTags == 100 %>" />
                </aui:select>
                <aui:select label="Animation Time" name="preferences--cutoffDays--">
                        <aui:option label="1" selected="<%= animationTime == 1 %>" />
                        <aui:option label="2" selected="<%= animationTime == 2 %>" />
                        <aui:option label="3" selected="<%= animationTime == 3 %>" />
                        <aui:option label="4" selected="<%= animationTime == 4 %>" />
                        <aui:option label="5" selected="<%= animationTime == 5 %>" />
                        <aui:option label="6" selected="<%= animationTime == 6 %>" />
                        <aui:option label="7" selected="<%= animationTime == 7 %>" />
                        <aui:option label="8" selected="<%= animationTime == 8 %>" />
                        <aui:option label="9" selected="<%= animationTime == 9 %>" />
                        <aui:option label="10" selected="<%= animationTime == 10 %>" />
                </aui:select>
                <aui:select label="Equity Cutoff"
                        name="preferences--eqCutoff--">
                        <aui:option label="1" selected="<%= eqCutoff == 1 %>" />
                        <aui:option label="5" selected="<%= eqCutoff == 5 %>" />
                        <aui:option label="10" selected="<%= eqCutoff == 10 %>" />
                        <aui:option label="15" selected="<%= eqCutoff == 15 %>" />
                        <aui:option label="20" selected="<%= eqCutoff == 20 %>" />
                        <aui:option label="25" selected="<%= eqCutoff == 25 %>" />
                        <aui:option label="30" selected="<%= eqCutoff == 30 %>" />
                        <aui:option label="60" selected="<%= eqCutoff == 60 %>" />
                        <aui:option label="70" selected="<%= eqCutoff == 70 %>" />
                        <aui:option label="80" selected="<%= eqCutoff == 80 %>" />
                        <aui:option label="90" selected="<%= eqCutoff == 90 %>" />
                        <aui:option label="100" selected="<%= eqCutoff == 100 %>" />
                        <aui:option label="110" selected="<%= eqCutoff == 110 %>" />
                        <aui:option label="150" selected="<%= eqCutoff == 150 %>" />
                        <aui:option label="200" selected="<%= eqCutoff == 200 %>" />
                        <aui:option label="500" selected="<%= eqCutoff == 500 %>" />
                </aui:select>

        </aui:fieldset>
        <aui:button-row>
                <aui:button type="submit" />
        </aui:button-row>


</aui:form>
  • portlet.xml: You need to add the <supported-public-render-parameter> and <public-render-parameter> elements to your portlet.xml, or use the below code in its entirety.  If you use the below code as-is, note that your portlet name must be "EquitySphere" in your other files (it may be easiest to name your project and portlet EquitySphere if you intend to use the below code as-is).
<?xml version="1.0"?>

<portlet-app
        version="2.0"
        xmlns="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd"
>
        <portlet>
                <portlet-name>EquitySphere</portlet-name>
                <display-name>EquitySphere</display-name>
                <portlet-class>com.liferay.util.bridges.mvc.MVCPortlet</portlet-class>
                <init-param>
                        <name>view-jsp</name>
                        <value>/view.jsp</value>
                </init-param>
                <expiration-cache>0</expiration-cache>
                <supports>
                        <mime-type>text/html</mime-type>
                </supports>
                <portlet-info>
                        <title>EquitySphere</title>
                        <short-title>EquitySphere</short-title>
                        <keywords>EquitySphere</keywords>
                </portlet-info>
                <security-role-ref>
                        <role-name>administrator</role-name>
                </security-role-ref>
                <security-role-ref>
                        <role-name>guest</role-name>
                </security-role-ref>
                <security-role-ref>
                        <role-name>power-user</role-name>
                </security-role-ref>
                <security-role-ref>
                        <role-name>user</role-name>
                </security-role-ref>
                <supported-public-render-parameter>                         tag                 </supported-public-render-parameter>                 
        </portlet>
        <public-render-parameter>                 <identifier>tag</identifier>                 <qname xmlns:x="http://www.liferay.com/public-render-parameters">x:tag</qname>         </public-render-parameter>         
</portlet-app>
  • liferay-portlet.xml (contains declaration of ConfigurationImpl class - using Liferay's default so I don't have to write my own ConfigurationImpl but instead follow the naming convention for preference names, such as preferences--cutoffDays--.
<?xml version="1.0"?>
<!DOCTYPE liferay-portlet-app PUBLIC "-//Liferay//DTD Portlet Application 6.0.0//EN" "http://www.liferay.com/dtd/liferay-portlet-app_6_0_0.dtd">

<liferay-portlet-app>
        <portlet>
                <portlet-name>EquitySphere</portlet-name>
                <icon>/icon.png</icon>
                <configuration-action-class>com.liferay.portal.kernel.portlet.DefaultConfigurationAction</configuration-action-class>
                <instanceable>false</instanceable>
                <header-portlet-css>/css/main.css</header-portlet-css>
                <footer-portlet-javascript>/js/main.js</footer-portlet-javascript>
                <css-class-wrapper>EquitySphere-portlet</css-class-wrapper>
        </portlet>
        <role-mapper>
                <role-name>administrator</role-name>
                <role-link>Administrator</role-link>
        </role-mapper>
        <role-mapper>
                <role-name>guest</role-name>
                <role-link>Guest</role-link>
        </role-mapper>
        <role-mapper>
                <role-name>power-user</role-name>
                <role-link>Power User</role-link>
        </role-mapper>
        <role-mapper>
                <role-name>user</role-name>
                <role-link>User</role-link>
        </role-mapper>
</liferay-portlet-app>

The rest of the files (e.g. web.xml, liferay-plugin-package.properties, etc) should not need any changes from their default values.  The only other thing you must do is place jQuery (jquery-1.4.1.js) and the JS-Cumulus (js-cumulus.min.js) javascript files into the js/ subdirectory of your project.  You can use the minified jquery if you wish.  The final structure of your project would look like the picture on the right.  You can download these files from here.

Deploying and understanding Tag Equity Visualization

Once you have sucessfully deployed this application, you can add the tag cloud to any page within a community.  It proably won't show anything because you haven't enabled Social Equity and you haven't created any content.  So first, go to Control Panel -> Social Equity and click "Enable Social Equity" to turn it on.  Once that is done, add the Blogs application to a page, and start creating some blogs and adding tags to the blog posts.   Once the blogs are published, you should start to see tags appearing in the tag sphere!  Any content you create (blogs, wikis, forums, bookmarks, images, etc) that can be tagged will cause their tags to show up.  To tweak the tunables, click the wrench icon and select 'Configuration' to change the values.  Click 'save' to apply your changes.  You should see something like the tag cloud below.  

 Note that as tags get more equity (by people creating new content using that tag, OR by an existing piece of tagged content getting rated, updated, viewed, etc), the size of the font should get bigger (relative to the other tags).  A great way to see, at a glance, what topics are most valuable. Feel free to change the inline CSS to make this thing prettier (I am not a designer!).

 

 

 

 

2. Trending Topics

You're probably familiar with Twitter and its concept of "trending topics".  It shows a list of "hot topics" for that particular point in time, scrolling across the screen in a ticker-tape-like display.  Clicking on the topic shows you a list of tweets associated with that topic.  A handy way to see what is on people's minds at any one point in the Twittersphere.

How can we achieve the same thing within a Liferay community?  For community members, it would be a great way for them to keep in touch on the hot topics of the day.

Trending Algorithm

I looked around the web for some decent trending algorithms, and there were lots of them, but they were complicated and relied on advanced math that I left far behind in grade school.  So I came up with my own algorithm.  Here it is:

In this digram, "today" is represented at the far right.  To determine whether a topic is trending, we look at the average number of "mentions" in two time periods: before the "pivot" and "after".  If the average number of mentions in the time period between pivot and today is higher than a certain threshold (compared to the average number of mentions in the time period between cutoff and pivot), then the topic is considered trending.  In the example above, there were 5 mentions over 15 days in the first time period (.3333/day average), and 3 mentions in 5 days in the second time period (.6/day average), which is an 80% improvement.  If 80% is higher than your selected threshold, then the topic will display as a trending topic.

This portlet is identical in structure to the previous one, so not a lot of explanation is needed.  Everything is inline in view.jsp and the shared render parameter behavior is the same.  The main difference in this portlet is the algorithm, # of tunables, and javascript used to display the result.  I used another FOSS jQuery plugin called TurboTicker, which displays tags in a smooth, non-CPU-intensive, vertical scroll.  You can style all you want using CSS.  

view.jsp

<%@page import="com.liferay.portal.kernel.util.GetterUtil"%>
<%@page import="com.liferay.portlet.PortletPreferencesFactoryUtil"%>
<%@page import="com.liferay.portal.kernel.util.Validator"%>
<%@page import="com.liferay.portal.kernel.util.ParamUtil"%>
<%@page import="javax.portlet.PortletPreferences"%>
<%@page import="java.util.Set"%>
<%@page import="java.util.HashSet"%>
<%@page import="com.liferay.portal.util.PortalUtil"%>
<%@page import="java.util.Calendar"%>
<%@page import="java.util.Collections"%>
<%@page import="java.util.Comparator"%>
<%@page import="java.util.Date"%>
<%@page import="com.liferay.portlet.asset.model.AssetTag"%>
<%@page import="java.util.ArrayList"%>
<%@page import="java.util.HashMap"%>
<%@page import="java.util.Map"%>
<%@page
        import="com.liferay.portlet.asset.service.AssetEntryLocalServiceUtil"%>
<%@page import="com.liferay.portlet.asset.model.AssetEntry"%>
<%@page import="java.util.List"%>
<%@page
        import="com.liferay.portlet.asset.service.persistence.AssetEntryQuery"%>
<%@ taglib uri="http://java.sun.com/portlet" prefix="portlet"%>

<%@ taglib uri="http://liferay.com/tld/aui" prefix="aui"%>
<%@ taglib uri="http://liferay.com/tld/portlet" prefix="liferay-portlet"%>
<%@ taglib uri="http://liferay.com/tld/ui" prefix="liferay-ui"%>
<%@ taglib uri="http://liferay.com/tld/theme" prefix="liferay-theme"%>

<portlet:defineObjects />
<liferay-theme:defineObjects />

<style type="text/css">
                
                #trends {
                width: 200px;
                
                }
                
                #trends ul {
                margin: 0;
                padding: 0;
                list-style: none;
                }
                
                #trends div div ul li {
                font-family: "Arial Black", Arial, Helvetica, sans-serif;
                font-size: 1.0em;
                }
</style>
<%
        // get preferences for tunables
        PortletPreferences preferences = renderRequest.getPreferences();

        String portletResource = ParamUtil.getString(request, "portletResource");

        if (Validator.isNotNull(portletResource)) {
                preferences = PortletPreferencesFactoryUtil.getPortletSetup(request, portletResource);
        }
        final int MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;

        int PIVOT_DAYS = GetterUtil.getInteger(preferences.getValue(
                "pivotDays", "5"));
        int CUTOFF_DAYS = GetterUtil.getInteger(preferences.getValue(
                "cutoffDays", "20"));
        int TREND_PCT_THRESHOLD = GetterUtil.getInteger(preferences.getValue(
                "trendPctThreshold", "100"));
        int NORMALIZED_RANGE = GetterUtil.getInteger(preferences.getValue("maxSize",
                "3"));
        int NEW_THRESHOLD = GetterUtil.getInteger(preferences.getValue(
                "newThreshold", "3"));

        // query for all assets
        AssetEntryQuery q = new AssetEntryQuery();
        long[] ids = new long[] { scopeGroupId };
        q.setGroupIds(ids);
        q.setVisible(true);
        List<AssetEntry> entries = AssetEntryLocalServiceUtil.getEntries(q);

        final Set<String> tagSet = new HashSet<String>();
        final Map<String, Integer> beforeMentions = new HashMap<String, Integer>();
        final Map<String, Integer> afterMentions = new HashMap<String, Integer>();
        
        final Map<String, Integer> trends = new HashMap<String, Integer>();
        final Map<String, Integer> trendNormal = new HashMap<String, Integer>();

        Date now = new Date();
        Date pivot = new Date(now.getTime() - (PIVOT_DAYS * MILLISECONDS_PER_DAY));
        Date cutoff = new Date(now.getTime() - (CUTOFF_DAYS * MILLISECONDS_PER_DAY));

        //iterate over assets, storing mentions before and after the pivot in separate maps
        for (AssetEntry e : entries) {
                for (AssetTag tagA : e.getTags()) {

                        Date updated = e.getModifiedDate();
                        long tagAge = now.getTime() - updated.getTime();

                        if (updated.before(cutoff) || tagAge < 0) {
                                // mention occurred before the cutoff or in the future, so ignore
                                continue;
                        } 
                        
                        if (updated.before(pivot)) {
                                beforeMentions.put(tagA.getName(), beforeMentions.get(tagA.getName()) != null ? beforeMentions.get(tagA.getName()) + 1 : 1);
                        } else {
                                afterMentions.put(tagA.getName(), afterMentions.get(tagA.getName()) != null ? afterMentions.get(tagA.getName()) + 1 : 1);                               
                        }
                        tagSet.add(tagA.getName());
                }
        }
        
        Integer maxValue = 0;
        for (String tag : tagSet) {
                if (afterMentions.get(tag) == null) {
                        // tag never mentioned after pivot, so tag can't be trending
                        continue;
                }
                if (beforeMentions.get(tag) == null) {
                        // tag never mentioned before pivot, so only trending if breaks NEW_THRESHOLD
                        if (afterMentions.get(tag) > NEW_THRESHOLD) {
                                Integer newTrendVal = 100 * afterMentions.get(tag);
                                trends.put(tag, newTrendVal);
                                if (newTrendVal > maxValue) {
                                        maxValue = newTrendVal;
                                }
                        }
                } else {
                        double avgBeforeMentions = (double)beforeMentions.get(tag) / (double) (CUTOFF_DAYS - PIVOT_DAYS);
                        double avgAfterMentions = (double)afterMentions.get(tag) / (double)PIVOT_DAYS;
                        int pctImprovement = (int)((avgAfterMentions - avgBeforeMentions) * 100.0 / avgBeforeMentions);
                        if (pctImprovement > TREND_PCT_THRESHOLD) {
                                // tag is trending!
                                trends.put(tag, pctImprovement);
                                if (pctImprovement > maxValue) maxValue = pctImprovement;
                        }
                }
        }
        
        // normalize over specified range
        for (String tag : trends.keySet()) {
                trendNormal.put(tag, (int)Math.round( ((double)trends.get(tag) / (double)maxValue) * (double)NORMALIZED_RANGE));
        }
        
%>

<div id="trends">
<ul>

        <%
                // create <li> entries for each trending topic, with appropriate liferay link
                for (Map.Entry<String, Integer> trendEntry : trendNormal.entrySet()) {
                        String trendEntryKey = trendEntry.getKey();
                        Integer trendEntryVal = trendEntry.getValue();
        %>
        <li><font size="+<%=trendEntryVal%>"><a href="<portlet:renderURL><portlet:param name="tag" value="<%=trendEntryKey%>"/></portlet:renderURL>"><%=trendEntryKey%></a></font></li>
        <%
                }
        %>
</ul>
</div>

<script  src="<%= PortalUtil.getStaticResourceURL(request, request.getContextPath() + "/js/jquery-1.4.1.js", new Date().getTime()) %>" type="text/javascript"></script>
<script src="<%= PortalUtil.getStaticResourceURL(request, request.getContextPath() + "/js/turboTicker.JQuery.js", new Date().getTime()) %>" type="text/javascript"></script>
  
<script type="text/javascript">

        $("#trends").ticker(50, true, true);
        
</script>

configuration.jsp

<%@page import="com.liferay.portal.kernel.util.Constants"%>
<%@page import="com.liferay.portal.kernel.util.GetterUtil"%>
<%@page import="com.liferay.portlet.PortletPreferencesFactoryUtil"%>
<%@page import="com.liferay.portal.kernel.util.Validator"%>
<%@page import="com.liferay.portal.kernel.util.ParamUtil"%>
<%@page import="javax.portlet.PortletPreferences"%>
<%@ taglib uri="http://java.sun.com/portlet_2_0" prefix="portlet"%>
<%@ taglib uri="http://liferay.com/tld/aui" prefix="aui"%>
<%@ taglib uri="http://liferay.com/tld/portlet" prefix="liferay-portlet"%>
<%@ taglib uri="http://liferay.com/tld/security"
        prefix="liferay-security"%>
<%@ taglib uri="http://liferay.com/tld/theme" prefix="liferay-theme"%>
<%@ taglib uri="http://liferay.com/tld/ui" prefix="liferay-ui"%>
<%@ taglib uri="http://liferay.com/tld/util" prefix="liferay-util"%>

<%@ page contentType="text/html; charset=UTF-8"%>

<portlet:defineObjects />
<liferay-theme:defineObjects />

<%
        PortletPreferences preferences = renderRequest.getPreferences();

        String portletResource = ParamUtil.getString(request,
                        "portletResource");

        if (Validator.isNotNull(portletResource)) {
                preferences = PortletPreferencesFactoryUtil.getPortletSetup(
                                request, portletResource);
        }

        int pivotDays = GetterUtil.getInteger(preferences.getValue(
                        "pivotDays", "5"));
        int cutoffDays = GetterUtil.getInteger(preferences.getValue(
                        "cutoffDays", "20"));
        int trendPctThreshold = GetterUtil.getInteger(preferences.getValue(
                        "trendPctThreshold", "100"));
        int maxSize = GetterUtil.getInteger(preferences.getValue("maxSize",
                        "3"));
        int newThreshold = GetterUtil.getInteger(preferences.getValue(
                        "newThreshold", "3"));
%>

<liferay-portlet:actionURL portletConfiguration="true"
        var="configurationURL" />

<aui:form action="<%= configurationURL %>" method="post" name="fm">
        <aui:input name="<%= Constants.CMD %>" type="hidden"
                value="<%= Constants.UPDATE %>" />
        <aui:fieldset>

                <aui:select label="Pivot Days" name="preferences--pivotDays--">
                        <aui:option label="1" selected="<%= pivotDays == 1 %>" />
                        <aui:option label="2" selected="<%= pivotDays == 2 %>" />
                        <aui:option label="3" selected="<%= pivotDays == 3 %>" />
                        <aui:option label="4" selected="<%= pivotDays == 4 %>" />
                        <aui:option label="5" selected="<%= pivotDays == 5 %>" />
                </aui:select>
                <aui:select label="Cutoff Days" name="preferences--cutoffDays--">
                        <aui:option label="10" selected="<%= cutoffDays == 10 %>" />
                        <aui:option label="20" selected="<%= cutoffDays == 20 %>" />
                        <aui:option label="30" selected="<%= cutoffDays == 30 %>" />
                        <aui:option label="40" selected="<%= cutoffDays == 40 %>" />
                        <aui:option label="50" selected="<%= cutoffDays == 50 %>" />
                        <aui:option label="60" selected="<%= cutoffDays == 60 %>" />
                        <aui:option label="70" selected="<%= cutoffDays == 70 %>" />
                        <aui:option label="80" selected="<%= cutoffDays == 80 %>" />
                        <aui:option label="90" selected="<%= cutoffDays == 90 %>" />
                        <aui:option label="100" selected="<%= cutoffDays == 100 %>" />
                </aui:select>
                <aui:select label="Improvement Threshold (%)"
                        name="preferences--trendPctThreshold--">
                        <aui:option label="10" selected="<%= trendPctThreshold == 10 %>" />
                        <aui:option label="20" selected="<%= trendPctThreshold == 20 %>" />
                        <aui:option label="30" selected="<%= trendPctThreshold == 30 %>" />
                        <aui:option label="40" selected="<%= trendPctThreshold == 40 %>" />
                        <aui:option label="50" selected="<%= trendPctThreshold == 50 %>" />
                        <aui:option label="60" selected="<%= trendPctThreshold == 60 %>" />
                        <aui:option label="70" selected="<%= trendPctThreshold == 70 %>" />
                        <aui:option label="80" selected="<%= trendPctThreshold == 80 %>" />
                        <aui:option label="90" selected="<%= trendPctThreshold == 90 %>" />
                        <aui:option label="100" selected="<%= trendPctThreshold == 100 %>" />
                        <aui:option label="110" selected="<%= trendPctThreshold == 110 %>" />
                        <aui:option label="150" selected="<%= trendPctThreshold == 150 %>" />
                        <aui:option label="200" selected="<%= trendPctThreshold == 200 %>" />
                        <aui:option label="500" selected="<%= trendPctThreshold == 500 %>" />
                </aui:select>
                <aui:select label="Max Size" name="preferences--maxSize--">
                        <aui:option label="1" selected="<%= maxSize == 1 %>" />
                        <aui:option label="2" selected="<%= maxSize == 2 %>" />
                        <aui:option label="3" selected="<%= maxSize == 3 %>" />
                        <aui:option label="4" selected="<%= maxSize == 4 %>" />
                        <aui:option label="5" selected="<%= maxSize == 5 %>" />
                        <aui:option label="6" selected="<%= maxSize == 6 %>" />
                        <aui:option label="7" selected="<%= maxSize == 7 %>" />
                </aui:select>
                <aui:select label="New Threshold" name="preferences--newThreshold--">
                        <aui:option label="1" selected="<%= newThreshold == 1 %>" />
                        <aui:option label="2" selected="<%= newThreshold == 2 %>" />
                        <aui:option label="3" selected="<%= newThreshold == 3 %>" />
                        <aui:option label="4" selected="<%= newThreshold == 4 %>" />
                        <aui:option label="5" selected="<%= newThreshold == 5 %>" />
                        <aui:option label="6" selected="<%= newThreshold == 6 %>" />
                        <aui:option label="7" selected="<%= newThreshold == 7 %>" />
                </aui:select>
        </aui:fieldset>
        <aui:button-row>
                <aui:button type="submit" />
        </aui:button-row>


</aui:form>

portlet.xml

<?xml version="1.0"?>

<portlet-app
        version="2.0"
        xmlns="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd"
>
        <portlet>
                <portlet-name>TrendingTopics</portlet-name>
                <display-name>TrendingTopics</display-name>
                <portlet-class>com.liferay.util.bridges.mvc.MVCPortlet</portlet-class>
                <init-param>
                        <name>view-jsp</name>
                        <value>/view.jsp</value>
                </init-param>
                <expiration-cache>0</expiration-cache>
                <supports>
                        <mime-type>text/html</mime-type>
                </supports>
                <portlet-info>
                        <title>TrendingTopics</title>
                        <short-title>TrendingTopics</short-title>
                        <keywords>TrendingTopics</keywords>
                </portlet-info>
                <security-role-ref>
                        <role-name>administrator</role-name>
                </security-role-ref>
                <security-role-ref>
                        <role-name>guest</role-name>
                </security-role-ref>
                <security-role-ref>
                        <role-name>power-user</role-name>
                </security-role-ref>
                <security-role-ref>
                        <role-name>user</role-name>
                </security-role-ref>
                <supported-public-render-parameter>                         tag                 </supported-public-render-parameter>         </portlet>
        <public-render-parameter>                 <identifier>tag</identifier>                 <qname xmlns:x="http://www.liferay.com/public-render-parameters">x:tag</qname>         </public-render-parameter>
</portlet-app>

liferay-portlet.xml

<?xml version="1.0"?>
<!DOCTYPE liferay-portlet-app PUBLIC "-//Liferay//DTD Portlet Application 6.0.0//EN" "http://www.liferay.com/dtd/liferay-portlet-app_6_0_0.dtd">

<liferay-portlet-app>
        <portlet>
                <portlet-name>TrendingTopics</portlet-name>
                <icon>/icon.png</icon>
        <configuration-action-class>com.liferay.portal.kernel.portlet.DefaultConfigurationAction</configuration-action-class>         <instanceable>false</instanceable>
                <header-portlet-css>/css/main.css</header-portlet-css>
                <footer-portlet-javascript>/js/main.js</footer-portlet-javascript>
                <css-class-wrapper>TrendingTopics-portlet</css-class-wrapper>
        </portlet>
        <role-mapper>
                <role-name>administrator</role-name>
                <role-link>Administrator</role-link>
        </role-mapper>
        <role-mapper>
                <role-name>guest</role-name>
                <role-link>Guest</role-link>
        </role-mapper>
        <role-mapper>
                <role-name>power-user</role-name>
                <role-link>Power User</role-link>
        </role-mapper>
        <role-mapper>
                <role-name>user</role-name>
                <role-link>User</role-link>
        </role-mapper>
</liferay-portlet-app>

 

 

 

 

 

Once you have that all in place (including the turboTicker.JQuery.js and jquery-1.4.1.js files, which you can download from here), the project structure should be identical to the previous one (see image to the right, I think I have a couple of superfluous js files in there as well, which you can ignore).

 

 

 

 

Deploying and understanding Trending Topics

Once you have deployed the application, you can add it to any page in a community.  The topics display are based on mentions.  To mention a particular topic, just create any user-generated content that participates in Liferay's Asset Framework and can be tagged.  This includes a blog post, a wiki page, an image, a forum post, etc.  Any time content is created with a tag (not a category, though that can also be visualized with a slight change to the code), that constitutes a mention.

The tunables are:

  • Pivot Days: The number of days prior to today to calculate an average mention count (see above image).
  • Cutoff Days: The number of days prior today to ignore mentions of tags.
  • Trend Percent Threshold: The percentage improvement in average mentions between the two time periods, above which a tag is considered trending.
  • Max Size: After an improvement percent is measured for each trending topic, the numbers are normalized into a range of [0, NORMALIZED_RANGE] for display.  The number here is the biggest font size that should be used for the best trending topic.  For example, if you put 7 here, then, the topic with the highest trend improvement will be rendered with <font size="+7"></font>.  I know you HTML purists are thinking that this should be done with CSS and that <font> tags are the spawn of the devil, but I slept through that class in college, and this is about community participation, not web coding :-)
  • New Threshold: For tags that have never been mentioned before, their "improvement" percentage would be infinity, if calculated as above, since their average in the earlier time period was 0.  So for newly-mentioned tags, this number allows you to specify how many 'new' mentions it takes for a tag to be considered trending.  A value of 5, for example, means that for a new tag never mentioned before, it would take 5 mentions to get it onto the trending topics list.

Once deployed, your list should look like this:

and it should be slowly scrolling.  Clicking on one of the tags should cause any other Liferay portlets on that page (e.g. blog, wiki, asset publisher, etc) to only show content with the tag you clicked on.  Enjoy! 

Antworten im Thread Autor Datum
Great portlets i put them directly on our... Corné Aussems 21. April 2011 13:34
Great idea James! Social equity as you... Puj Z 22. April 2011 03:10
nice feature Sreeraj AV 12. Juli 2011 01:26
in view.jsp it is giving error : The method... arpita s 16. April 2012 08:16
** sorry i forgot to mention the portlet name,... arpita s 16. April 2012 08:17

Great portlets i put them directly on our company development community.

Another use case:
Show the top xx contributors from a specific country/city/organization of all or a specific community
Objective: Motivate people to get into a top ranking
Gepostet am 21.04.11 13:34.
Great idea James! Social equity as you mentioned is a great feature and has lots of improvement potentials.
Gepostet am 22.04.11 03:10.
in view.jsp it is giving error :
The method getSocialInformationEquity() is undefined for the type AssetEntry,
how can it be rectified.
Thanks,
Arpita
Gepostet am 16.04.12 08:16.
** sorry i forgot to mention the portlet name, it is equity sphere.
i.e the first one
Gepostet am 16.04.12 08:17 als Antwort auf arpita s.