<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Corunet. El Blog &#187; heatmaps</title>
	<atom:link href="http://blog.corunet.com/tag/heatmaps/feed/" rel="self" type="application/rss+xml" />
	<link>http://blog.corunet.com</link>
	<description>Web development, usability and more</description>
	<lastBuildDate>Fri, 23 Oct 2009 15:33:27 +0000</lastBuildDate>
	<generator>http://wordpress.org/?v=2.9.2</generator>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
			<item>
		<title>The definitive heatmap</title>
		<link>http://blog.corunet.com/the-definitive-heatmap/</link>
		<comments>http://blog.corunet.com/the-definitive-heatmap/#comments</comments>
		<pubDate>Wed, 16 Aug 2006 18:39:21 +0000</pubDate>
		<dc:creator>David Pardo</dc:creator>
				<category><![CDATA[English]]></category>
		<category><![CDATA[Usability]]></category>
		<category><![CDATA[heatmaps]]></category>
		<category><![CDATA[javascript]]></category>
		<category><![CDATA[ruby]]></category>

		<guid isPermaLink="false">http://blog.corunet.com/english/the-definitive-heatmap</guid>
		<description><![CDATA[After the interest shown about the clickmaps / heatmaps articles, I&#8217;ve decided to gather all the information into an easy to use system. What we are going to make is a complete solution that allows collecting, analyzing and showing the click information our users give us. Now, it works in web pages not center aligned [...]]]></description>
			<content:encoded><![CDATA[<p><img class="alignleft" title="The final heatmap" src="/uploads/heatmaps/thumbnail.png" border="0" alt="The final Heatmap" hspace="3" vspace="3" width="150" height="104" align="left" />After the interest shown about the clickmaps / heatmaps articles, I&#8217;ve decided to gather all the information into an easy to use system. What we are going to make is a complete solution that allows collecting, analyzing and showing the click information our users give us. Now, it works in web pages not center aligned and is quite a bit more robust. Read on&#8230;<span id="more-11"></span></p>
<h2>What?</h2>
<p>If you are a webmaster, you had probably thought about what do users do in your website. Beyond usual statistics, clickmaps allow you to find where your users are clicking. This is quite useful to find areas in needing of change, layouts that don&#8217;t work as intended or anchors that aren&#8217;t being understood as you would like.</p>
<p>You&#8217;re going to be able to find every single click your users make in your website, being over a link or even in blank areas. We are going to do it the following way:</p>
<h2>The proccess</h2>
<p>We need to divide the full proccess into some manageable steps that use some open source tools. Since I work both in windows and linux systems, I&#8217;ll be <strong>OS agnostic </strong>and use only tools available in most systems, including Mac OSX.<br />
The main steps and the tools they use are the following:</p>
<ol>
<li>The collecting (javascript and apache)</li>
<li>The processing (ruby and imageMagick)</li>
<li>The showing (javascript)</li>
</ol>
<h2>The collecting</h2>
<p>We are going to use  a small snippet of unobtrusive javascript to allow the client to tell our server the click positions. Just place this small javascript file at the very end of your template, right before the closing &lt;body&gt; tag:</p>
<p class="toggle_code"><a href="/uploads/code/registerclicks.js" onclick="jQuery('#code_0').toggle('slow');return false">registerclicks.js</a></p>
<div  style="display:none" id="code_0">
<pre class="brush: js">/**
 * @author David Pardo: Corunet
 * Run after loading
 */

var xOffset,yOffset;
var tempX = 0;
var tempY = 0;

//detect browser
var IE = document.all?true:false
if (!IE) {
	document.captureEvents(Event.MOUSEMOVE)
}
//find the position of the first item on screen and store offsets
	//find the first item on screen (after body)
	var firstElement=document.getElementsByTagName(&#039;body&#039;)[0].childNodes[1];
	//find the offset coordinates
	xOffset=findPosX(firstElement);
	yOffset=findPosY(firstElement);
	if (IE){ // In IE there&#039;s a default margin in the page body. If margin&#039;s not defined, use defaults
		var marginLeftExplorer  = parseInt(document.getElementsByTagName(&#039;body&#039;)[0].style.marginLeft);
		var marginTopExplorer   = parseInt(document.getElementsByTagName(&#039;body&#039;)[0].style.marginTop);
		/*assume default 10px/15px margin in explorer*/
		if (isNaN(marginLeftExplorer)) {marginLeftExplorer=10;}
		if (isNaN(marginTopExplorer)) {marginTopExplorer=15;}
		xOffset=xOffset+marginLeftExplorer;
		yOffset=yOffset+marginTopExplorer;
	}
/*attach a handler to the onmousedown event that calls a function to store the values*/
document.onmousedown = getMouseXY;

/*Functions*/
/*Find positions*/
function findPosX(obj){
	var curleft = 0;
	if (obj.offsetParent){
		while (obj.offsetParent){
			curleft += obj.offsetLeft
			obj = obj.offsetParent;
		}
	}else if (obj.x){
		curleft += obj.x;
	}
	return curleft;
}

function findPosY(obj){
	var curtop = 0;
	if (obj.offsetParent){
		while (obj.offsetParent){
			curtop += obj.offsetTop
			obj = obj.offsetParent;
		}
	}else if (obj.y){
		curtop += obj.y;
	}
	return curtop;
}
function getMouseXY(e) {
	if (IE) {
		tempX = event.clientX + document.body.scrollLeft
		tempY = event.clientY + document.body.scrollTop
	} else {
		tempX = e.pageX
		tempY = e.pageY
	}
	tempX-=xOffset;
	tempY-=yOffset;
	var url=&#039;guardacoordenadas.pl?x=&#039;+tempX+&#039;&amp;y=&#039;+tempY;
	guardar(url);
	return true;
}
function guardar(url){
	var xmlDoc = null ;
	if (typeof window.ActiveXObject != &#039;undefined&#039; ) {
		xmlDoc = new ActiveXObject(&#039;Microsoft.XMLHTTP&#039;);
	}else {
		xmlDoc = new XMLHttpRequest();
	}
	xmlDoc.open( &#039;GET&#039;, url, true );
	xmlDoc.send( null );
}</pre>
</div>
<p>The code adds a onMouseDown handler to the document, executes a function for every click and returns true, since we want the user to follow the normal navigation. Then, when the user clicks any part of the page, a tiny request is going to get sent on the background to our server. The script has to calculate the offsett of the first element inside the &lt;body&gt; tag, because most pages arent aligned to the top-right corner. In <strong>liquid layouts </strong>the system is <strong>not going to work </strong>at all</p>
<p>The request is sent via a HttpRequest object that calls a file in the server. In last version, I used a small GCI written in perl to log the request and return an empty document, but since we want to serve so many request, there&#8217;s a better method to apply. Using a perl CGI, in a modern server, we get the following results benchmarking with apache bench (100 requests, 10 concurrent ones):</p>
<p><code><br />
Concurrency Level:      10<br />
Time taken for tests:   6.537187 seconds<br />
Complete requests:      100<br />
Failed requests:        0<br />
Write errors:           0<br />
Total transferred:      17100 bytes<br />
HTML transferred:       0 bytes<br />
Requests per second:    15.30 [#/sec] (mean)<br />
Time per request:       653.719 [ms] (mean)<br />
Time per request:       65.372 [ms] (mean, across ...)<br />
Transfer rate:          2.45 [Kbytes/sec] received<br />
</code></p>
<h2>Mod_imap</h2>
<p>Apache has some <em>modules</em> that work the following way:<br />
You define a handler and what you want to do with it. Some of them are well known, like mod_perl or mod_cgi, but a lesser known one, called mod_imap, does exactly what we want. It&#8217;s a module meant to return server-side image maps, but if we use an empty map file, all we get is a 204 status (no data) and a logged transaction. The difference is quite significative. Using Apache Bench with the same configuration, this is what we get:</p>
<p><code><br />
Concurrency Level:      10<br />
Time taken for tests:   0.106316 seconds<br />
Complete requests:      100<br />
Failed requests:        0<br />
Write errors:           0<br />
Total transferred:      36464 bytes<br />
HTML transferred:       20246 bytes<br />
Requests per second:    940.59 [#/sec] (mean)<br />
Time per request:       10.632 [ms] (mean)<br />
Time per request:       1.063 [ms] (mean, across ...)<br />
Transfer rate:          329.21 [Kbytes/sec] received<br />
</code></p>
<p>That&#8217;s 950 requests per second vs 15 with the CGI method!!! <strong>We are almost a hundred times faster with this approach!</strong>The only thing we have to do to use this mod_imap is to touch a little bit the apache configuration file. Do it carefully because it can hurt your entire server. In the relevant section add the following lines:</p>
<p><code><br />
AddHandler mod_imap .map<br />
CustomLog /tmp/clicklog clicklog #or modify according to your system<br />
</code></p>
<p>And define a custom log in the same file adding this:<br />
<code><br />
LogFormat "%q,%{Referer}i" clicklog<br />
</code></p>
<p>This way, everything ending in .map is going to be treated as a server-side map, and since the map is empty, it&#8217;s not taking your user anywhere. But it logs it, in file /tmp/clicklog (YMMV).</p>
<h2>The log analysis</h2>
<p>Since we used a logFormat apache directive to write our log, the format should be easy to parse. The query string is written in the log as it comes, and the full lines should be in the following format:</p>
<p><code><br />
?x=483&amp;y=32&amp;dx10&amp;dy15,http://demo.html<br />
?x=461&amp;y=177&amp;dx10&amp;dy15,http://demo.html<br />
?x=408&amp;y=40&amp;dx10&amp;dy15,http://demo.html<br />
(...)<br />
</code></p>
<p>I decided to write a Ruby script to parse the file and generate the final images, because I hadn&#8217;t used ruby before and thought it would be a good way to approach the problem. Last time I had written an structured perl script, but I think that object-oriented is the way to go in this particular situation, since the objects should be well-defined and dividing the program among several coders should be easier too.</p>
<p><em><strong>Update:</strong>Thanks to Jerret, this part has been enhanced using RMagick. Part of the code below can be updated and works some 50 times faster. On top of that, a new sourceforge project has been started at http://sourceforge.net/projects/clickmaps/ under a GPL license. Of course, if you don&#8217;t want to install/use RMagick you can still download the original version at the end of this post.</em></p>
<p>I´ll try to explain the model. It uses five classes:</p>
<dl>
<dt>Conf: </dt>
<dd>Sets some configuration variables and returns them as a hash. This way, every configuration variable is set in this class and it&#8217;s easy to get them later on</dd>
</dl>
<p class="toggle_code"><a href="/uploads/code/conf.rb" onclick="jQuery('#code_1').toggle('slow');return false">conf.rb</a></p>
<div  style="display:none" id="code_1">
<pre class="brush: rb">class Conf
	def initialize
	@data = {
		&#039;logfile&#039; =&gt; &#039;log.txt&#039;,
		&#039;dotimage&#039; =&gt; &#039;bolilla.png&#039;,
		&#039;format&#039; =&gt; &#039;png&#039;,
		&#039;colorimage&#039; =&gt; &#039;colors.png&#039;,
		&#039;opacity&#039; =&gt; &quot;0.50&quot;,
		&#039;dotwidth&#039; =&gt; 64
	}
	raise &quot;log file not found&quot; \
	unless File.exist?(@data[&#039;logfile&#039;])
	raise &quot;dot image not found&quot; \
	unless File.exist?(@data[&#039;dotimage&#039;])
	raise &quot;color image not found&quot; \
	unless File.exist?(@data[&#039;colorimage&#039;])
	end
	attr_reader :data
end</pre>
</div>
<dl>
<dt>Readparsefile</dt>
<dd>Reads and parses the file defined as logfile in the conf object. For each log line, it stores it into a click object and append it to an array. There are two methods that return all the URLs in the log file (geturls) and all the information for a single URL as a Log object</dd>
</dl>
<p class="toggle_code"><a href="/uploads/code/readparsefile.rb" onclick="jQuery('#code_2').toggle('slow');return false">readparsefile.rb</a></p>
<div  style="display:none" id="code_2">
<pre class="brush: rb">class Readparsefile
  def initialize(name)
    @name = name
    @data = Array.new
    lines = IO.readlines(@name).collect { |l| l.chomp }
    for line in lines
        line.gsub!(/\?x\=/,&#039;&#039;)
        line.gsub!(/\&amp;y\=/,&#039;,&#039;)
        line.gsub!(/\//,&#039;_&#039;)
        x,y,url = line.split(/,/)
        if (x and y and url)
            @data.push(Click.new(url, x, y))
        else
            $stderr.puts &quot;Warning: Bogus line &quot;&lt;&lt; line
        end
    end
    @urls = Array.new
    @data.each do |line|
        @urls.push(line.url)
    end
    raise &quot;no clicks found&quot; unless lines.length &gt; 0
    @urls.uniq!
  end
  def geturls
      return @urls
  end
  def coordsurl(url)
    @url=url
    xMax=0
    yMax=0
    coords = Array.new
    @data.each do |line|
        coords.push(line)
        xMax=line.x if line.x&gt;xMax
        yMax=line.y if line.y&gt;yMax
    end
    return Log.new(xMax,yMax,coords,@url)
  end
end</pre>
</div>
<dl>
<dt>Click</dt>
<dd> Stores the data in each log line, including X, Y and URL. Provides a method (xy) that returns an string like &#8220;x100y200&#8243; to compare the exact coordinates, useful to extract the maximum number of times a single click is repeated </dd>
</dl>
<p class="toggle_code"><a href="/uploads/code/click.rb" onclick="jQuery('#code_3').toggle('slow');return false">click.rb</a></p>
<div  style="display:none" id="code_3">
<pre class="brush: rb">class Click
    def initialize(url,x,y)
        @url=url
        @x=x
        @y=y
    end
    def url
        return @url
    end
    def x
        return @x.to_i
    end
    def y
        return @y.to_i
    end
    def xy
        return &quot;x&quot;+@x+&quot;y&quot;+@y
    end
end</pre>
</div>
<dl>
<dt>Log</dt>
<dd> Stores all the values pertinent to a single URL and gives accesors to them. There&#8217;s also a &#8220;next&#8221; method that returns next click within the same URL </dd>
</dl>
<p class="toggle_code"><a href="/uploads/code/log.rb" onclick="jQuery('#code_4').toggle('slow');return false">log.rb</a></p>
<div  style="display:none" id="code_4">
<pre class="brush: rb">class Log
    def initialize (x,y,list,url)
        @line = 0
        @x=x
        @y=y
        @url=url
        @list=list
        @points = Hash.new(0)
        @list.each do |point|
            @points[point.xy] +=1
        end
        @reps = @points.values.max
    end
    attr_reader <img src='http://blog.corunet.com/wp-includes/images/smilies/icon_mad.gif' alt=':x' class='wp-smiley' /> , :y, :list, :reps, :url
    def next
        coord = @list[@line]
        @line += 1
        return coord
    end
end</pre>
</div>
<dl>
<dt>Image</dt>
<dd>Receives a log object and the conf object. There are three methods to normalize the spot we&#8217;re going to use as a click indicator (normalizespot), compose every click as a dot (iterate) and colorize the final image (colorize)</dd>
</dl>
<p class="toggle_code"><a href="/uploads/code/image.rb" onclick="jQuery('#code_5').toggle('slow');return false">image.rb</a></p>
<div  style="display:none" id="code_5">
<pre class="brush: rb">class Image
    def initialize(data,conf)
        @data = data
        @name = data.url.gsub!(/\W/,&#039;&#039;).gsub!(/\_/,&#039;&#039;).to_s
        @conf=conf.data
    end
    def normalizespot
        #divide spot.png intensity by max. position reps (@data.reps)
        intensity = (100-(100/@data.reps).ceil).to_s
        normalize = &quot;convert &quot;&lt;&lt;@conf[&#039;dotimage&#039;]&lt;&lt;&quot; -fill white -colorize &quot;&lt;&lt;intensity&lt;&lt;&quot;% &quot;&lt;&lt;@name&lt;&lt;&quot;.bol.png&quot;
        system(normalize)
    end
    def iterate
        halfwidth=@conf[&#039;dotwidth&#039;]/2
        compose = &quot;convert -page &quot;&lt;&lt;(@data.x+halfwidth).to_s&lt;&lt;&quot;x&quot;&lt;&lt;(@data.y+halfwidth).to_s&lt;&lt;&quot; pattern:gray100 &quot;
        #iterate spots
        @data.list.each do |dot|
            compose &lt;&lt; &quot;-page +&quot;&lt;&lt;((dot.x)-halfwidth).to_s&lt;&lt;&quot;+&quot;&lt;&lt;((dot.y)-halfwidth).to_s&lt;&lt;&quot; &quot;&lt;&lt;@name&lt;&lt;&quot;.bol.png &quot;
        end
        compose &lt;&lt; &quot;-background white -compose multiply -flatten &quot;&lt;&lt;@name&lt;&lt;&quot;.empty.png&quot;
        system(compose)
    end
    def colorize
        #invert image...
        invert = &quot;convert &quot;&lt;&lt;@name&lt;&lt;&quot;.empty.png -negate &quot;&lt;&lt;@name&lt;&lt;&quot;.full.png&quot;
        system(invert)
        #colorize it...
        colorize = &quot;convert &quot;&lt;&lt;@name&lt;&lt;&#039;.full.png -type TruecolorMatte &#039;&lt;&lt;@conf[&#039;colorimage&#039;]&lt;&lt;&#039; -fx &quot;v.p{0,u*v.h}&quot; &#039;&lt;&lt;@name&lt;&lt;&quot;.colorized.png&quot;
        system(colorize)
        #and apply transparency...
        transparency = &quot;convert &quot;&lt;&lt;@name&lt;&lt;&#039;.colorized.png -channel A -fx &quot;A*&#039;&lt;&lt;@conf[&#039;opacity&#039;]&lt;&lt;&#039;&quot;  &#039;&lt;&lt;@name&lt;&lt;&#039;.final.&#039;&lt;&lt;@conf[&#039;format&#039;]
        system(transparency)
    end
end</pre>
</div>
<p>Then, the main program is only eight lines long. It leverages the objects&#8217; methods to be as compact as possible. In fact, the only thing it does is to iterate over each url to create a different image.<br />
<code> </code></p>
<pre>conf = Conf.new
file = Readparsefile.new(conf.data['logfile'])
file.geturls.each do |url|
    image = Image.new(file.coordsurl(url),conf)
    image.normalizespot
    image.iterate
    image.colorize
end</pre>
<h2>Is it better?</h2>
<p>You can find another program (this time written in perl) in an older post that does a similar job of making heatmaps. But there has been some modifications that makes this an usable system instead of a proof of concept:</p>
<dl>
<dt>Flexible configuration</dt>
<dd>Over the harcoded last version, in this one is quite simple to modify the images used in the heatmap generation, or the log name. <strong>You only have to modify the Conf definition</strong>. It would be so easy to use an external conf file, but doing it this way is quicker for me</dd>
<dt>Multiple URL support</dt>
<dd>While last version only let you extract one image, this one <strong>makes a heatmap for every URL in your log</strong>. </dd>
<dt>Much faster execution time</dt>
<dd>Instead of composing the full image everytime, now we create a single ImageMagick sentence to do al the composition for us. That gives us a couple of orders speed advantage. <strong>Last version lasted about fifteen minutes for a couple hundred clicks, and now it&#8217;s about five seconds</strong>. Please note that, for many clicks, the program uses quite a bit of memory. Probably for a production environment it would be neccesary to divide the <em>compose</em> sentence into manageable chuncks, and iterate at the end with them to create the final heatmap.</dd>
<dt>Manual capture is not needed anymore</dt>
<dd>Since the last step is to decrement the opacity of the map, we can use a little bit of javascript to overlay the PNG image over the original page. So, the stakeholders can review it without someone manually capturing the screen. This way we don&#8217;t need to set an XServer in the production environment</dd>
<dt>Easier to maintain and extend</dt>
<dd>The object oriented paradigm doesn&#8217;t give us faster code, but much more manageable one. You can extend it as you want</dd>
</dl>
<h2>What you get</h2>
<p>Now, you&#8217;ll have several images. Most of them are OK to delete, but there&#8217;s one ending in final.png that&#8217;s your heatmap. We&#8217;re going to overlay it on top of your web page. That image should be a semi-transparent PNG like this one:</p>
<p><img src="/uploads/final2_p.png" alt="" /></p>
<h2>The overlay</h2>
<p>This is the final part of the proccess. We already have the overlay image and all we need is a <strong>javascript snippet </strong>that can be called anytime and that creates a layer on top of your website with the click information. Just like the first step, we&#8217;re going to position it over the very first item in the page.<br />
The best way to do that is via a <em><strong>bookmarklet</strong></em>, that is, an small javascript snippet saved as a bookmark. This way, you can have it in your browser and ask for the overlay image when you feel like. The javascript recalculates the offsets of the first element inside the &lt;body&gt; tag and writes the heatmap image on top of it.</p>
<p class="toggle_code"><a href="/uploads/code/overlay.js" onclick="jQuery('#code_6').toggle('slow');return false">overlay.js</a></p>
<div  style="display:none" id="code_6">
<pre class="brush: js">/**
 * @author DavidPardo
 */
//find the position of the first item on screen and store offsets
//find the first item on screen (after body)
var firstElement=document.getElementsByTagName(&#039;body&#039;)[0].childNodes[1];
//find the offset coordinates
xOffset=findPosX(firstElement);
yOffset=findPosY(firstElement);
if (IE){ // In IE there&#039;s a default margin in the page body. If margin&#039;s not defined, use defaults
	var marginLeftExplorer  = parseInt(document.getElementsByTagName(&#039;body&#039;)[0].style.marginLeft);
	var marginTopExplorer   = parseInt(document.getElementsByTagName(&#039;body&#039;)[0].style.marginTop);
	/*assume default 10px/15px margin in explorer*/
	if (isNaN(marginLeftExplorer)) {marginLeftExplorer=10;}
	if (isNaN(marginTopExplorer)) {marginTopExplorer=15;}
	xOffset=xOffset+marginLeftExplorer;
	yOffset=yOffset+marginTopExplorer;
}
// add the image element to the dom, absolutely positioned
var style=&quot;z-index:100;position:absolute;left:&quot;+xOffset+&quot;px;top:&quot;+yOffset+&quot;px&quot;;
var bodytag=document.getElementsByTagName(&#039;body&#039;)[0]
var newdiv = document.createElement(&#039;div&#039;);
var currentpage=location.href;
var escapedpage=currentpage.replace(/\W/g,&#039;&#039;);
newdiv.setAttribute(&#039;id&#039;,&#039;heatmap&#039;);
newdiv.innerHTML = &#039;&lt;img src=&quot;&#039;+escapedpage+&#039;.final.png&quot; style=&quot;&#039;+style+&#039;&quot; &gt;&#039;;
bodytag.appendChild(newdiv);
</pre>
</div>
<h2>The result</h2>
<p>We got a beautiful heatmap on top of our web page. We can call the overlay from wherever we want and show it to the project stakeholders. Look at the result:</p>
<p><img src="/uploads/final.png" alt="" /></p>
<h2>The code</h2>
<p>I made a <strong>ready to download package with all the code</strong>. It&#8217;s released under a MIT license that means that you can do whatever you want with it. Probably in the future it&#8217;ll be part of an open source release; if you feel like, start it yourself or contact me for more information.</p>
<p><a href="/uploads/clickmaps.net.tar.gz">Download code. Tar.gz file </a></p>
<h2>What else?</h2>
<p>The sky is the limit. If you want a hosted service, contact us. <a href="http://www.corunet.com" onclick="pageTracker._trackPageview('/outgoing/www.corunet.com?referer=');">Our company </a>can give you bespoke solutions to all your web intelligence needs, being it log analysis, path tracking and so on. If you&#8217;re a developer, feel free to use all the code as you wish, and please <a href="mailto:david@corunet.com">write me </a>to tell your experiences. Stay tuned!</p>
<p>By the way, there has been a post in <a href="http://remysharp.com/2007/01/18/how-i-achieved-cross-site-scripting/" onclick="pageTracker._trackPageview('/outgoing/remysharp.com/2007/01/18/how-i-achieved-cross-site-scripting/?referer=');"> remysharp blog</a> explaining how to record the clicks in a different server. Thanks.</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.corunet.com/the-definitive-heatmap/feed/</wfw:commentRss>
		<slash:comments>81</slash:comments>
		</item>
		<item>
		<title>Zero budget eye tracking. Clickmaps part 2</title>
		<link>http://blog.corunet.com/zero-budget-eye-tracking-clickmaps-part-2/</link>
		<comments>http://blog.corunet.com/zero-budget-eye-tracking-clickmaps-part-2/#comments</comments>
		<pubDate>Sun, 30 Jul 2006 16:01:08 +0000</pubDate>
		<dc:creator>David Pardo</dc:creator>
				<category><![CDATA[English]]></category>
		<category><![CDATA[Usability]]></category>
		<category><![CDATA[heatmaps]]></category>
		<category><![CDATA[javascript]]></category>
		<category><![CDATA[perl]]></category>

		<guid isPermaLink="false">http://blog.corunet.com/english/zero-budget-eye-tracking-clickmaps-part-2</guid>
		<description><![CDATA[In the first part of Zero budget eye tracking, we wrote a small script to keep track of the clicks in a web page. A couple of days after, we already have some information to analyze. Keep on reading to know how&#8230;
NOTE: This post has been improved at The definitive heatmap
I added the scripts to [...]]]></description>
			<content:encoded><![CDATA[<p><a href="/english/zero-budget-eye-tracking-clickmaps">In the first part of Zero budget eye tracking</a>, we wrote a small script to keep track of the clicks in a web page. A couple of days after, we already have some information to analyze. Keep on reading to know how&#8230;<span id="more-9"></span><br />
NOTE: This post has been improved at <a href="http://blog.corunet.com/english/the-definitive-heatmap">The definitive heatmap</a><br />
I added the scripts to our website (<a href="http://www.corunet.com" onclick="pageTracker._trackPageview('/outgoing/www.corunet.com?referer=');">www.corunet.com</a>) and used a resolution of 1280&#215;1024 px. After 65 clicks, I have a log like this one:</p>
<p><code><br />
x332y288	http://corunet.com/	192.168.0.10<br />
x399y288	http://corunet.com/	192.168.0.10<br />
x489y294	http://corunet.com/	192.168.0.10<br />
x655y346	http://corunet.com/	192.168.0.10<br />
x709y351	http://corunet.com/	192.168.0.10<br />
x350y348	http://corunet.com/	192.168.0.10<br />
x384y305	http://corunet.com/	192.168.0.10<br />
...<br />
</code></p>
<p>We would need to filter the x and y coordinates to draw some dots over the page. I will use the following code (written in perl):</p>
<p><code><br />
<span style="color:#399;font-style:italic;">#!/usr/bin/perl</span><br />
<span style="color:#001;">open</span> <span style="color:#000;">(</span><span style="color:#3A3;">DATOS</span><span style="color:#000;">,</span><span style="color:#00a;">"</span><span style="color:#00a;">log.txt</span><span style="color:#00a;">"</span><span style="color:#000;">)</span><span style="color:#000;">;</span><br />
<span style="color:#000;">my</span> <span style="color:#f70;">@datos</span><span style="color:#000;">=</span><span style="color:#000;">&lt;</span><span style="color:#3A3;">DATOS</span><span style="color:#000;">&gt;</span><span style="color:#000;">;</span><br />
<span style="color:#001;">close</span> <span style="color:#000;">(</span><span style="color:#3A3;">DATOS</span><span style="color:#000;">)</span><span style="color:#000;">;</span><br />
<span style="color:#000;">my</span> <span style="color:#080;">$i</span><span style="color:#000;">;</span><br />
<span style="color:#000;">foreach</span> <span style="color:#000;">my</span> <span style="color:#080;">$dato</span><span style="color:#000;">(</span><span style="color:#f70;">@datos</span><span style="color:#000;">)</span><span style="color:#000;">{</span><br />
<span style="color:#000;">if</span> <span style="color:#000;">(</span><span style="color:#080;">$dato</span><span style="color:#000;">=~</span><span style="color:#00a;">/</span><span style="color:#00a;"><span style="color:#800;">\.</span>com<span style="color:#800;">\/</span><span style="color:#800;">\t</span></span><span style="color:#00a;">/</span><span style="color:#000;">)</span><span style="color:#000;">{</span><br />
<span style="color:#000;">my</span> <span style="color:#f70;">@posicion</span><span style="color:#000;">=</span><span style="color:#001;">split</span><span style="color:#000;">(</span><span style="color:#00a;">"</span><span style="color:#00a;"><span style="color:#800;">\t</span></span><span style="color:#00a;">"</span><span style="color:#000;">,</span><span style="color:#080;">$dato</span><span style="color:#000;">)</span><span style="color:#000;">;</span><br />
<span style="color:#f70;">$posicion</span><span style="color:#000;">[</span><span style="color:#f0f;">0</span><span style="color:#000;">]</span><span style="color:#000;">=~</span><span style="color:#00a;">s/</span><span style="color:#00a;">x</span><span style="color:#00a;">/</span><span style="color:#00a;">/</span><span style="color:#000;">;</span><br />
<span style="color:#000;">my</span> <span style="color:#000;">(</span><span style="color:#080;">$x</span><span style="color:#000;">,</span><span style="color:#080;">$y</span><span style="color:#000;">)</span><span style="color:#000;">=</span><span style="color:#001;">split</span><span style="color:#000;">(</span><span style="color:#00a;">"</span><span style="color:#00a;">y</span><span style="color:#00a;">"</span><span style="color:#000;">,</span><span style="color:#f70;">$posicion</span><span style="color:#000;">[</span><span style="color:#f0f;">0</span><span style="color:#000;">]</span><span style="color:#000;">)</span><span style="color:#000;">;</span><br />
<span style="color:#080;">$i</span><span style="color:#000;">++</span><span style="color:#000;">;</span><br />
<span style="color:#300;">print</span> <span style="color:#00a;">'</span><span style="color:#00a;">&lt;img src="/i/pelotilla.png" </span><br />
<span style="color:#00a;"> style="z-index:</span><span style="color:#00a;">'</span><span style="color:#000;">.</span><span style="color:#080;">$i</span><span style="color:#000;">.</span><span style="color:#00a;">'</span><span style="color:#00a;">;position:absolute;</span><br />
<span style="color:#00a;"> left:</span><span style="color:#00a;">'</span><span style="color:#000;">.</span><span style="color:#080;">$x</span><span style="color:#000;">.</span><span style="color:#00a;">'</span><span style="color:#00a;">px;top:</span><span style="color:#00a;">'</span><span style="color:#000;">.</span><span style="color:#080;">$y</span><span style="color:#000;">.</span><span style="color:#00a;">'</span><span style="color:#00a;">px;</span><br />
<span style="color:#00a;"> height:16px;width:16px;"&gt;</span><span style="color:#00a;">'</span><span style="color:#000;">.</span><span style="color:#00a;">"</span><span style="color:#00a;"><span style="color:#800;">\n</span></span><span style="color:#00a;">"</span><span style="color:#000;">;</span><br />
<span style="color:#000;">}</span><br />
<span style="color:#000;">}</span><br />
</code></p>
<p>This script writes out an IMG tag for every log line that asked for the main page (ends in .com). This way, we would only need to paste this html code into a copy of the original page. We also will need a small image to mark the spots. I used a 16&#215;16px</p>
<h2>The click map</h2>
<p>The former script prints out some html code like this one:</p>
<p><code><br />
...<br />
&lt;img src="/i/pelotilla.png"<br />
style="z-index:51;position:absolute;<br />
left:605px;top:773px;<br />
height:16px;width:16px;"&gt;<br />
&lt;img src="/i/pelotilla.png"<br />
style="z-index:52;position:absolute;<br />
left:778px;top:792px;<br />
height:16px;width:16px;"&gt;<br />
&lt;img src="/i/pelotilla.png"<br />
style="z-index:53;position:absolute;<br />
left:799px;top:810px;<br />
height:16px;width:16px;"&gt;<br />
...<br />
</code></p>
<p>Pasting the code into the page copy, this is what we get:</p>
<p><img src="/uploads/captura.jpg" alt="Click map" /></p>
<p>I used the system posted in <a href="http://webfx.eae.net/dhtml/pngbehavior/pngbehavior.html" onclick="pageTracker._trackPageview('/outgoing/webfx.eae.net/dhtml/pngbehavior/pngbehavior.html?referer=');">webfx </a> to use transparent PNGs in internet explorer. We wouldn&#8217;t need it if we had done our research using Opera or Firefox browsers.<br />
Each dot in the above image  refers to a single click in our web site. Studying it we would be able to identify areas where the users are having trouble to find the links, and where they think should be a link but isn&#8217;t. In a real world analysis it would be better to have some more dots to work on.</p>
<h2>Shortcomings</h2>
<li>The system&#8217;s not able to support resolution changes. We would need some more javascript to refer every click to a known point of the screen. And it would be pretty difficult, if not impossible, to use it in liquid layouts.</li>
<li>In flash movies, we are going to have some trouble because the onclick event is not supported. As you can see in the screen capture above, there is not a single click detected in the top area, where the flash menu is.</li>
<p>Using the same technique, we could be able to store every mouse position in some milliseconds interval to replay our users movements, and there are many other possibilities.</p>
<h2>Acknowledgements</h2>
<p>I got the inspiration for this post from the <a href="http://blog.outer-court.com/click/" onclick="pageTracker._trackPageview('/outgoing/blog.outer-court.com/click/?referer=');">&#8220;Click Survey&#8221;</a> that lets you click randomly in any position of a picture.</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.corunet.com/zero-budget-eye-tracking-clickmaps-part-2/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
