Einfache Heatmap mit PHP und Javascript

2 | 10 Kommentare | 12539 Aufrufe
Sie können diese Wikiseite nach der Anmeldung auf Webmasterpro bearbeiten. Helfen Sie mit und verbessern Sie "Einfache Heatmap mit PHP und Javascript" mit Ihrem Wissen!

Anzeige Hier werben

Eine Heatmap ist eine einfache Methode um die am häufigsten aufgerufenen Links zu finden und somit das Interessengebiet der Besucher besser kennen zu lernen. Auch lassen sich Design-/Usabilityschwächen finden, wenn beispielsweise Flächen oft angeklickt werden, die nicht verlinkt sind.

Dieser Artikel soll dabei helfen selbst eine Heatmap für die eigene Webseite zu erstellen. Hierbei werden allerdings nur die Grundlagen angesprochen und ein Script zum Starten angeboten. Komplexere Implementierungen für Datenerfassung und Datenvisualisierung sind natürlich möglich und können bei der Auswertung weiter helfen, sollen aber nicht Teil dieses Artikels sein. Am Ende sind stattdessen einige mögliche Erweiterungen und Ideen für Änderungen gelistet.

Vorüberlegungen

Serverlast

Mit dem hier vorgestellten Script wird bei jedem Klick ein Aufruf eines PHP-Scripts erzeugt. Auch die Datenbank wird damit höchstwahrscheinlich eine nicht zu unterschätzende Größe erreichen, die dank SQLite bei vielen gleichzeitigen Zugriffen erst recht ins Gewicht fallen wird. Der verwendete Server sollte also mit der Mehrlast klarkommen.

URLs

Um aussagekräftige Ergebnisse zu erzielen kann eine Heatmap nur für eine einzelne Seite gültig sein. Deswegen muss nach Adresse gefiltert werden können. Um die Datenbank nicht allzu sehr zu belasten können hierbei Protokoll, Domain, Anker und evtl. sogar der Query-String weggelassen werden. Folgende PHP-Funktion erledigt dies:

hm.php  
PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<?php

// Relevanten Teil der Adresse ermitteln
function hm_url($fullUrl)
{
    $url = $fullUrl;
    // Domain und Protokoll wegschneiden
    $url = preg_replace('|^(https?://[^/]*?)?|', '', $url);
    // Optional: Query-String abschneiden
    //$url = preg_replace('|\?.+$|', '', $url);
    // Anker abschneiden
    $url = preg_replace('|#.+$|', '', $url);
    return $url;
}

?>

Natürlich kann auch eine Heatmap, die alle Seiten ausgibt ein aussagekräftiges Resultat erzeugen. Das ist dann möglich, wenn es Elemente gibt die auf jeder Seite gleich positioniert sind. Die Navigation einer Webpräsenz wäre ein Kandidat wo eine solche ungefilterte Heatmap Sinn machen kann. Deswegen wird das Script die Möglichkeit bieten eine solche Heapmap für alle Seiten zu generieren.

Erfassen von Klicks

Da die Klicks im Browser stattfinden und nur hier die genaue Position ermittelt werden kann müssen diese Daten auch hier ausgelesen und an den Server übermittelt werden. Hierzu wird Javascript verwendet:

Klicks erfassen und per AJAX an den Server weitergeben

Mit folgendem Script wird das onmousedown-Event einer Webseite ausgenutzt um einen AJAX-Request bei jedem Klick an den Server zu senden. Die Klickposition wird hierbei direkt per GET an hc.php übergeben, ein Rückgabewert vom Request ist nicht nötig.

Zu beachten sind die Browserunterschiede bzgl. Events, dem Auslesen der Position und natürlich beim AJAX-Objekt. Diese Unterschiede sollen hier nicht näher beschrieben werden, der Quelltext sollte auf Internet Explorer und Firefox funktionieren, andere Browser benötigen evtl. Anpassungen.

hc.js  
JavaScript
 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// Event-Handler für den Klick (onmousedown)
function hm_logClick(e)
{
    // Event-Variable holen falls nötig
    if (!e) e = window.event;
    // Klickposition auslesen
    if (IE)
    {
        // Rechtsklick wird ignoriert
        if (typeof(e.button) != 'undefined' && e.button == 2) return;
        
        clickX = e.clientX + document.body.scrollLeft
        clickY = e.clientY + document.body.scrollTop
    }
    else
    {
        // Rechtsklick wird ignoriert
        if (typeof(e.which) != 'undefined' && e.which == 3) return;
        
        clickX = e.pageX
        clickY = e.pageY
    }
    // Klick-Daten an PHP-Script übergeben
    var url='hc.php?x=' + clickX + '&y=' + clickY;
    hm_sendClickAjaxReq(url);
    // true zurückgeben, damit das Event weiterverarbeitet wird
    return true;
}

// Funktion, die eine URL per AJAX aufruft ohne Rückgabewerte o.ä. zu beachten
function hm_sendClickAjaxReq(url)
{
    // AJAX-Objekt konstruieren
    var ajaxRequest;
    if (typeof(window.ActiveXObject) == 'undefined')
        ajaxRequest = new XMLHttpRequest();
    else
        ajaxRequest = new ActiveXObject('Microsoft.XMLHTTP');
    // AJAX-Request senden
    ajaxRequest.open('GET', url, true);
    ajaxRequest.send(null);
}

// Variable um den IE zu erkennen
var IE = document.all ? true : false
// onmousedown-Event-Handler registrieren
if (!IE) document.captureEvents(Event.MOUSEMOVE)
document.onmousedown = hm_logClick;

Logging der Klicks in einer SQLite-Datenbank

Zum Loggen aller Klicks auf dem Server wird eine SQLite-Datenbank verwendet. Für den produktiven Einsatz sollte evtl. auf eine leistungsfähigere Datenbank gewechselt werden. Für dieses einfache Beispiel einer Heatmap reicht SQLite allerdings völlig aus.

Datenbankaufbau

In der Datenbank muss die Klickposition (x und y) sowie die aufgerufene Adresse (url) gespeichert werden. Zusätzlich wird die Zeit des Aufrufs (time) gesichert, dieses Feld soll eine Anregung für eigene Erweiterungen bieten, wird aber in keinem der hier vorgestellten Quelltexte verwendet (Ausnahme: Beim Eintragen wird das Feld mit der aktuellen Zeit gefüllt).

db.sql  
sql
1
2
3
4
5
6
CREATE TABLE clicks (
    time INT,
    x INT,
    y INT,
    url VARCHAR(255)
);
Logging mit PHP

Die übergebenen Daten können direkt in die Datenbank gesichert werden. Die Adresse der Seite wird hierbei über den Referer übermittelt und mit der oben bereits angesprochenen Funktion vorverarbeitet um überflüssige Daten zu entfernen.

hc.php  
PHP
 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<?php

// Allgemeine Funktionen laden
require('hm.php');

// Parameter prüfen
if (!isset($_GET['x']) || !isset($_GET['y']) || empty($_SERVER['HTTP_REFERER']))
    exit;
// Klickposition
$clickX = intval($_GET['x']);
$clickY = intval($_GET['y']);
// Positionsdaten validieren
if ($clickX < 0 || $clickY < 0)
    exit;

// Adresse ermitteln
$url = hm_url($_SERVER['HTTP_REFERER']);

// Datenbankdatei
$dbfile = 'clicks.sqlite';
// Testen ob Datenbank existiert
if (file_exists($dbfile))
{
    // Datenbank öffnen
    $db = sqlite_open($dbfile);
}
else
{
    // Datenbank öffnen
    $db = sqlite_open($dbfile);
    // Tabelle anlegen
    sqlite_query(
        file_get_contents('db.sql'),
        $db
    );
}

// Klick loggen
sqlite_query(sprintf('
    INSERT INTO
        clicks
        (time, x, y, url)
    VALUES
        (%d, %d, %d, "%s")',
    time(),
    $_GET['x'],
    $_GET['y'],
    sqlite_escape_string($url)),
    $db
);
// Datenbank schließen
sqlite_close($db);

?>

Ausgabe der Heatmap

Bild zu Einfache Heatmap mit PHP und Javascript
Screenshot der Testseite

Die Heatmap stellt ein einfaches, halbtransparentes Bild dar, welches über die Webseite gelegt wird. Im Bild werden alle Klicks durch rote Kreise dargestellt, sind an einer Stelle mehrere Klicks registriert steigert sich die Intensität des Rot-Tons.

PHP kommt normalerweise mit einer Einschränkung, die beim Erzeugen großer Bilder sehr hinderlich ist: Es kann nur eine begrenze Menge Arbeitsspeicher allokiert werden. Um das zu Umgehen wird die Seite in 250x250 Pixel große Kacheln unterteilt die jeweils durch einen eigenen Request geladen werden. Hierbei wird das Speicherlimit, auch auf streng konfigurierten Servern, nicht erreicht.

Alternativ könnte man mithilfe von init_set() der zur Verfügung stehenden Speicher ändern. Das ist allerdings nicht auf allen Servern möglich. Grundsätzlich ließe sich durch diese Maßnahme allerdings die Last senken, da weniger CPU-Zeit nötig ist. Wer will kann die ideale Größe für die Kacheln auf seinem Server suchen um möglichst nah ans Speicherlimit zu kommen.

Kacheln per JS laden

Um die einzelnen Kacheln zu laden wird per Javascript die Seitengröße ermittelt. Darauf kann die Anzahl der nötigen Kacheln ermittelt werden, diese werden dann als absolut positionierte <div>-Elemente mit der Kachel als Hintergrundbild ans Ende des <body> eingefügt.

hs.js  
JavaScript
 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// Heatmap über die Seite legen, hierbei wird die Heatmap in einem
// 250x250-Raster gekachelt
function hm_loadHeatmap()
{
    // URL ermitteln, wie in hc.php
    var url = location.href;
    
    // Fenstergröße ermitteln
    /* from: http://www.quirksmode.org/viewport/compatibility.html#link2 */
    var width, height;
    var test1 = document.body.scrollHeight;
    var test2 = document.body.offsetHeight
    if (test1 > test2)
    {
        width = document.body.scrollWidth;
        height = document.body.scrollHeight;
    }
    else
    {
        width = document.body.offsetWidth;
        height = document.body.offsetHeight;
    }
    /* --- */
    
    // Body finden
    var body = document.getElementsByTagName('body')[0];
    // Kacheln erzeugen
    for (var x = 0; x < width; x += 250)
    {
        for (var y = 0; y < height; y += 250)
        {
            // Kachel durch ein absolut positioniertes <div> mit
            // Hintergrundgrafik darstellen
            var div = document.createElement('div');
            div.style.backgroundImage = 'url(hi.php?offsetX=' + x + '&offsetY=' + y + '&url=' + url + ')';
            div.style.position = 'absolute';
            div.style.width = '250px';
            div.style.height = '250px';
            div.style.left = x + 'px';
            div.style.top = y + 'px';
            body.appendChild(div);
        }
    }
}
// Optional: Heatmap direkt beim Laden der Seite anzeigen
//window.onload = hm_loadHeatmap;

Kachel mit PHP zeichnen

Um die Kacheln mit PHP zu füllen wird die GL-Lib verwendet. Hierbei wird ein praktisches Feature ausgenutzt: Wird mit einer halb-transparenten Farbe gezeichnet, so wird die Orginalfarbe an der Stelle nicht ersetzt, stattdessen werden die beiden Farbtöne gemischt. Dadurch werden Stellen an denen mehrere Klicks aufgetreten sind im Endergebnis durch einen satteren Rot-Ton dargestellt, die Ballungszentren der Klicks sind also direkt zu erkennen.

Diese Vorgehensweise wird auch eingesetzt um den Klick mit einem radialen Verlauf darzustellen. Um das gewünschte Ergebnis zu erreichen wird einfach mehrfach ein gefüllter Kreis gezeichnet, bei dem der Radius abnimmt. In der Mitte ist somit ein satteres Rot zu sehen.

Damit die Kacheln sauber zusammenpassen werden auch Klicks gezeichnet, die bis zu 20 Pixel außerhalb der eigentlichen Kachel aufgetreten sind. Die GD-Lib zeichnet somit bei einem Kreis mir einem Radius von 7 Pixel noch ins Bild, auch wenn der Mittelpunkt des Kreis leicht außerhalb des Bildes liegt.

hi.php  
PHP
 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<?php

// Allgemeine Funktionen laden
require('hm.php');

// Parameter prüfen
if (!isset($_GET['url']) || !isset($_GET['offsetX']) || !isset($_GET['offsetY']))
    exit;
// Variablen initialisieren
$offsetX = intval($_GET['offsetX']);
$offsetY = intval($_GET['offsetY']);
$sizeX = isset($_GET['sizeX']) ? intval($_GET['sizeX']) : 250;
$sizeY = isset($_GET['sizeY']) ? intval($_GET['sizeY']) : 250;
// Variablen validieren
if ($offsetX < 0 || $offsetY < 0 || $sizeX < 0 || $sizeY < 0)
    exit;

// Datenbank öffnen
$dbfile = 'clicks.sqlite';
$db = sqlite_open($dbfile);

// Adresse ermitteln
$url = $_GET['url'];
if ($url != 'all')
    $url = hm_url($url);

// Kachel erstellen
$img = imagecreatetruecolor($sizeX, $sizeY);
imagesavealpha($img, true);

// Hintergrund zeichnen
imagealphablending($img, false);
$back = imagecolorallocatealpha($img, 100, 100, 255, 120);
imagefilledrectangle($img, 0, 0, $sizeX, $sizeY, $back);
imagealphablending($img, true);

// Klicks abrufen
$result = sqlite_query(
    'SELECT
        x,
        y
    FROM
        clicks
    WHERE
        ' . ($url != 'all' ? ('url="' . sqlite_escape_string($url) . '" AND') : '') . '
        x >= ' . intval($offsetX - 20) . ' AND
        y >= ' . intval($offsetY - 20) . ' AND
        x < ' . intval($offsetX + $sizeX + 20) . ' AND
        y < ' . intval($offsetY + $sizeY + 20),
    $db);
$size = 13; // Größe der Punkte
$color = imagecolorallocatealpha($img, 255, 20, 20, 120); // Punktfarbe
// Alle Punkte zeichnen
while ($row = sqlite_fetch_array($result))
{
    for ($i = $size; $i > 0; $i -= 4)
    {
        imagefilledellipse($img, $row['x'] - $offsetX, $row['y'] - $offsetY, $i, $i, $color);
    }
}

// Datenbank schließen
sqlite_close($db);

// Bild ausgeben als PNG
header('Content-type: image/png');
imagepng($img);
imagedestroy($img);

?>

Testseite

Folgende Seite zeigt wie die Scripts eingebunden werden könnten. Natürlich ist das Beispiel nur lauffähig, wenn auf dem Server PHP installiert ist und das Verzeichnis schreibbar ist in dem sich die Dateien befinden. Sonst kann die SQLite-Datenbankdatei nicht angelegt werden.

 
HTML
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html>
<head>
    <title>Testdatei</title>
    <script type="text/javascript" src="hc.js"></script>
    <script type="text/javascript" src="hs.js"></script>
</head>
<body>
    <h1>Nur ein Test</h1>
    <p>Lorem....</p>
    <p>Lorem....</p>
    <p>Lorem....</p>
    <p>Lorem....</p>
    <p><a href="#" onclick="hm_loadHeatmap();">Heatmap zeigen</a></p>
</body>
</html>

Download

Alle hier verwendeten Dateien können für eigene Tests oder Erweiterungen heruntergeladen werden. Die Dateien können von jedem beliebig verändert, weitergegeben oder auf eigenen Seiten eingesetzt werden.

Downloads

Für diesen Artikel stehen zusätzliche Dateien zum Herunterladen bereit. Der Download ist nur für registrierte Benutzer möglich. Die kostenlose Registrierung dauert nur wenige Sekunden.

Mögliche Erweiterungsmöglichkeiten und Ideen

  • Anzeige nach Zeit einschänken können
  • Mehr Details speichern
  • Möglichkeit die Session zu tracken
  • Position der Klicks relativ zu fixen Elternelementen um Abweichung durch unterschiedliche Schriftgröße zu vermindern
  • Browserkompatibilität verbessern
  • Adresse in mehreren Datenbankfeldern speichern um Möglichkeiten bei der Anzeige zu steigern

Wikiseite bearbeiten

Diese Seite kann von jedem registrierten Benutzer bearbeitet werden. Bisher haben 3 Personen an der Seite "Einfache Heatmap mit PHP und Javascript" mitgewirkt.

Sie haben einen Fehler entdeckt oder möchten etwas ergänzen? Dann können Sie nach der Anmeldung "Einfache Heatmap mit PHP und Javascript" hier bearbeiten.

Mitarbeiter

Kommentare: Einfache Heatmap mit PHP und Javascript

Neuen Kommentar schreiben
Mögliche Erweiterung
Beantworten

Bei einem Programm von mir hatte ich damals die Erweiterung eingeführt, dass ich auch die genaue Mausbewegung mitgezeichnet hatte und an welchen Stellen geklickt wurde. Dabei wurde auch die genaue Bewegungszeit und Mousedown bis Mouseup - time gespeichert. Dies war in dem Fall Programmintern und ich nutze dies als Supportmittel wenn der Kunde ein Problem hat.

Der Kunde konnte mich dann anrufen und mir Bescheid sagen und ich habe mir dann seine letzte Sitzung auf einer Seite angesehen und dadurch dann häufig sehen können, wie es zu einem Problem kam.

Als Erweiterung für das Thema Heatmap wäre es z.B: eine Variante, identische "Mouse Gesten" zu finden oder um es als Anhaltspunkt zu nehmen, wie genau ein User z.B. einen Text gelesen hat. Viele Leute fahren den Text mit der Maus ab. So wäre auch z.B: leicht zu ermitteln wie lang ein Text sein darf, bevor der User auf dem Monitor den Überblick verliert und die Maus zu Hilfe nimmt als Orientierung.

Gruß, Dominik

Dominik Habichtsberg am 23.12.2008 um 17:15
Re: Mögliche Erweiterung
Beantworten

hmpf, da find ich ne formulierungsschwäche in meinem eigenen Text und hab keine Bearbeitenfunktion:

In letzterem Punkt meinte ich damit die Haupt-Benutzergruppe der ein User z.B: angehört.

Dominik Habichtsberg am 23.12.2008 um 17:18
Auflösung
Beantworten

Wie sieht das aus mit der Auflösung? Bei einer Auflösung 1024x768 sitzen die Klicks doch woanders wie bei einer Auflösung von 1280x1024 oder 800x600.

Jan Pieper am 13.04.2008 um 00:14
Re: Auflösung
Beantworten

Bei einer Auflösung 1024x768 sitzen die Klicks doch woanders wie bei einer Auflösung von 1280x1024 oder 800x600.

Das hängt von der Webseite ab, wenn der Content-Bereich beispielsweise mittig ausgerichtet ist stimm ich dir voll zu. Wird eine feste Breite verwendet, die noch dazu links ausgerichtet ist gibt es keine Probleme.

Trotzdem muss dieses einfache Script natürlich immer mit unterschriedlichen Schriftgrößen und Auflösungen kämpfen, weshalb das Script auch als Anregung für eigene Lösungen zu sehen ist. Genau aus dem Grund steht unter Verbesserungsmöglichkeiten auch "Position der Klicks relativ zu fixen Elternelementen [speichern]", eine Möglichkeit wäre beispielsweise das geklickte Element zu ermitteln und so lang den DOM-Tree nach oben abzugehen, bis man <body> oder ein Element mit einer ID gefunden hat und dann die Position relativ zu diesem Element zu speichern. Natürlich wird die Ausgabe damit auch deutlich komplizierter, da für jedes dieser Eltern-Elemente eine eigene Serie von Bilder-Kacheln ausgegeben werden muss. Deswegen hatte ich mich auch dazu entschieden das nicht konkreter im Artikel zu behandeln.

In Tests auf WMP (damals noch WMPv5) hat die hier vorgestellte Lösung trotzdem relativ gute Ergebnisse erziehlt. Beispielsweise im Menü war deutlich sichtbar, wo Besucher oft klicken, bzw. was oft besucht wird.

David Danier am 13.04.2008 um 09:38
Re: Auflösung
Beantworten

Was das Ergebnis aber auch noch verfälschen kann, ist der Inhalt der Seite.
Je nachdem auf welcher Seite ich mich befinde kann der klick weiter oben oder weiter unten sein.

Benutzer gelöscht am 27.05.2008 um 14:18
Re: Auflösung
Beantworten

Was das Ergebnis aber auch noch verfälschen kann, ist der Inhalt der Seite.

Deswegen wird ja gespeichert auf welcher Seite der Klick war. Je nach Parametern bei der Anzeige kann dann gewählt werden, ob du nur die Klicks auf der aktuellen Seite oder von allen Seiten anzeigen lässt.

David Danier am 27.05.2008 um 14:29
Re: Auflösung
Beantworten

Ich habs gerade gesehen beim runterscrollen zu deinem Kommentar :/

Benutzer gelöscht am 27.05.2008 um 14:30
Klick an der Kante
Beantworten

Was passiert, wenn der Klick so weit an der Kante ist, dass der Kreis eigtl. nicht ganz drauf passt.
Der wird doch dann abgeschnitten oder?

Benutzer gelöscht am 14.03.2008 um 00:10
Re: Klick an der Kante
Beantworten

Deswegen werden auch Klicks gezeichnet, die außerhalb der Kachel liegen, die Kachel aber noch beeinflussen. Im Script oben werden dazu einfach auch Klicks gezeichnet, die bis zu 20 Pixel außerhalb liegen. Die GD-Lib ist hierbei praktisch, wenn der Mittelpunkt eines Kreises außerhalb des Bilder ist wird trotzdem ins Bild gezeichnet, bei ausreichendem Radius natürlich. Steht auch im Text oben, siehe Kacheln mit PHP zeichnen, 3. Absatz unter der Überschrift.

David Danier am 14.03.2008 um 09:46
Re: Klick an der Kante
Beantworten

Ah, hab ich wohl überlesen.

Benutzer gelöscht am 14.03.2008 um 12:26