click_heat_map 0.0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/click_heat_map.rb +81 -0
- data/lib/models/click.rb +3 -0
- data/lib/public/css/clickmap.css +19 -0
- data/lib/public/js/clickmap.js +20 -0
- data/lib/public/js/clickmap_admin.js +107 -0
- data/lib/public/js/heatmap.js +653 -0
- metadata +63 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 299222f7ba9826c87c1ab8c5edbe6417c208aacd
|
4
|
+
data.tar.gz: 515081c207b6daef14cea1d32da8c431678c6453
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6cbd820c94476d85f2fce15174c648f998cfe7e7942418707672fed03847273634de79eba3dc3bae1bce7f85a602783fb68a3ccb106760595bfd1f5fea671e12
|
7
|
+
data.tar.gz: a0faf42c2226bb19ea1c792071d39437dc71dbaa44e17426203ef9892a0a82958185908168f0882890e685de192bf6f1e9dc5bd34bc5ebe4707c886d5bd27940
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'sinatra/activerecord'
|
3
|
+
require 'models/click'
|
4
|
+
|
5
|
+
module ClickMap
|
6
|
+
class Admin < Sinatra::Base
|
7
|
+
env = ENV["RACK_ENV"]
|
8
|
+
env = 'development' if env.nil?
|
9
|
+
set :clickmap_auth, YAML::load(File.open('config/clickmap.yml'))[env]
|
10
|
+
|
11
|
+
use Rack::Auth::Basic do |username, password|
|
12
|
+
begin
|
13
|
+
user, pass = settings.clickmap_auth["user"], settings.clickmap_auth["password"]
|
14
|
+
rescue NameError
|
15
|
+
user, pass = 'admin', 'clickmap'
|
16
|
+
end
|
17
|
+
|
18
|
+
username == user && password == pass
|
19
|
+
end
|
20
|
+
|
21
|
+
helpers do
|
22
|
+
def admin?
|
23
|
+
true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
get '/clicks' do
|
28
|
+
content_type :json
|
29
|
+
data = []
|
30
|
+
clicks = Click.select("*, count(id) as count")
|
31
|
+
.where(page: params[:page])
|
32
|
+
|
33
|
+
date_from = DateTime.new(1970, 1, 1)
|
34
|
+
date_to = DateTime.now
|
35
|
+
|
36
|
+
unless params[:min_w].blank?
|
37
|
+
clicks = clicks.where("screen_width >= ?", params[:min_w])
|
38
|
+
end
|
39
|
+
unless params["max_w"].blank?
|
40
|
+
clicks = clicks.where("screen_width <= ?", params[:max_w])
|
41
|
+
end
|
42
|
+
unless params[:min_h].blank?
|
43
|
+
clicks = clicks.where("screen_height >= ?", params[:min_h].to_i)
|
44
|
+
end
|
45
|
+
unless params[:max_h].blank?
|
46
|
+
clicks = clicks.where("screen_height <= ?", params[:max_h].to_i)
|
47
|
+
end
|
48
|
+
unless params[:from].blank?
|
49
|
+
date_from = DateTime.strptime(params[:from], "%Y-%m-%d")
|
50
|
+
end
|
51
|
+
unless params[:to].blank?
|
52
|
+
date_to = DateTime.strptime(params[:to], "%Y-%m-%d")
|
53
|
+
end
|
54
|
+
|
55
|
+
clicks = clicks.where(time: date_from..date_to).group("x, y").group("id")
|
56
|
+
clicks.each do |c|
|
57
|
+
data << {x: c.x, y: c.y, count: c.count, screen_width: c.screen_width}
|
58
|
+
end
|
59
|
+
|
60
|
+
return data.to_json
|
61
|
+
end
|
62
|
+
|
63
|
+
get '/clear' do
|
64
|
+
Click.destroy_all
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class Public < Sinatra::Base
|
69
|
+
post '/click/:x/:y/:width/:height' do
|
70
|
+
c = Click.new(
|
71
|
+
page: params[:page],
|
72
|
+
x: params[:x],
|
73
|
+
y: params[:y],
|
74
|
+
screen_width: params[:width],
|
75
|
+
screen_height: params[:height],
|
76
|
+
time: DateTime.now
|
77
|
+
)
|
78
|
+
c.save
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/lib/models/click.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
#clickmap_heatmap_area {
|
2
|
+
position: absolute;
|
3
|
+
top: 0px;
|
4
|
+
left: 0px;
|
5
|
+
width: 100%;
|
6
|
+
height: 100%; }
|
7
|
+
|
8
|
+
#clickmap_admin {
|
9
|
+
padding: 5px;
|
10
|
+
background: #cccccc; }
|
11
|
+
#clickmap_admin label {
|
12
|
+
text-align: right; }
|
13
|
+
#clickmap_admin input {
|
14
|
+
margin-right: 5px;
|
15
|
+
margin-bottom: 5px; }
|
16
|
+
#clickmap_admin input.size {
|
17
|
+
width: 50px; }
|
18
|
+
#clickmap_admin input.date {
|
19
|
+
width: 100px; }
|
@@ -0,0 +1,20 @@
|
|
1
|
+
window.ClickMap = (function() {
|
2
|
+
function ClickMap(container) {
|
3
|
+
var path;
|
4
|
+
this.container = container;
|
5
|
+
path = window.location.pathname;
|
6
|
+
$(this.container).click(function(e) {
|
7
|
+
var containerX, containerY, height, mouseX, mouseY, width;
|
8
|
+
containerX = $(this).offset().left;
|
9
|
+
containerY = $(this).offset().top;
|
10
|
+
width = window.innerWidth;
|
11
|
+
height = window.innerHeight;
|
12
|
+
mouseX = e.pageX - containerX;
|
13
|
+
mouseY = e.pageY - containerY;
|
14
|
+
return $.post("/clickmap/click/" + mouseX + "/" + mouseY + "/" + width + "/" + height + "?page=" + path);
|
15
|
+
});
|
16
|
+
}
|
17
|
+
|
18
|
+
return ClickMap;
|
19
|
+
|
20
|
+
})();
|
@@ -0,0 +1,107 @@
|
|
1
|
+
window.ClickMapAdmin = (function() {
|
2
|
+
var base;
|
3
|
+
|
4
|
+
base = ClickMapAdmin;
|
5
|
+
|
6
|
+
function ClickMapAdmin(container) {
|
7
|
+
this.container = container;
|
8
|
+
this.path = window.location.pathname;
|
9
|
+
this.createAdminPanel();
|
10
|
+
this.createHeatmapArea();
|
11
|
+
this.loadHeatmapData();
|
12
|
+
}
|
13
|
+
|
14
|
+
ClickMapAdmin.prototype.loadHeatmapData = function() {
|
15
|
+
var container, containerX, containerY, dateFrom, dateTo, maxHeight, maxWidth, minHeight, minWidth, url;
|
16
|
+
container = $(this.container);
|
17
|
+
containerX = container.offset().left;
|
18
|
+
containerY = container.offset().top;
|
19
|
+
minWidth = $("#clickmap_min_width").val();
|
20
|
+
maxWidth = $("#clickmap_max_width").val();
|
21
|
+
minHeight = $("#clickmap_min_height").val();
|
22
|
+
maxHeight = $("#clickmap_max_height").val();
|
23
|
+
dateFrom = $("#clickmap_date_from").val();
|
24
|
+
dateTo = $("#clickmap_date_to").val();
|
25
|
+
url = "/clickmap/admin/clicks?page=" + (encodeURI(this.path)) + "&min_w=" + minWidth + "&max_w=" + maxWidth + "&min_h=" + minHeight + "&max_h=" + maxHeight + "&from=" + dateFrom + "&to=" + dateTo;
|
26
|
+
return $.getJSON(url, (function(_this) {
|
27
|
+
return function(data) {
|
28
|
+
var d, max, _i, _len;
|
29
|
+
max = Math.max.apply(Math, data.map(function(o) {
|
30
|
+
return o.count;
|
31
|
+
}));
|
32
|
+
for (_i = 0, _len = data.length; _i < _len; _i++) {
|
33
|
+
d = data[_i];
|
34
|
+
d.x += containerX;
|
35
|
+
d.y += containerY;
|
36
|
+
}
|
37
|
+
_this.heatmap.store.setDataSet({
|
38
|
+
max: max,
|
39
|
+
data: data
|
40
|
+
});
|
41
|
+
_this.heatmapArea.show();
|
42
|
+
return _this.heatmap.toggleDisplay();
|
43
|
+
};
|
44
|
+
})(this));
|
45
|
+
};
|
46
|
+
|
47
|
+
ClickMapAdmin.prototype.createHeatmapArea = function() {
|
48
|
+
var heatmapEl;
|
49
|
+
this.heatmapArea = $("<div/>").attr('id', 'clickmap_heatmap_area');
|
50
|
+
$("body").prepend(this.heatmapArea);
|
51
|
+
heatmapEl = document.getElementById("clickmap_heatmap_area");
|
52
|
+
this.heatmap = h337.create({
|
53
|
+
element: heatmapEl,
|
54
|
+
radius: 10,
|
55
|
+
visible: false
|
56
|
+
});
|
57
|
+
this.heatmapArea.hide();
|
58
|
+
return this.heatmapArea.click((function(_this) {
|
59
|
+
return function() {
|
60
|
+
if (_this.heatmap.get("visible")) {
|
61
|
+
_this.heatmapArea.hide();
|
62
|
+
return _this.heatmap.toggleDisplay();
|
63
|
+
}
|
64
|
+
};
|
65
|
+
})(this));
|
66
|
+
};
|
67
|
+
|
68
|
+
ClickMapAdmin.prototype.createAdminPanel = function() {
|
69
|
+
var form, panel;
|
70
|
+
panel = $("<div/>").attr('id', "clickmap_admin");
|
71
|
+
form = "<form id='clickmap_form'>";
|
72
|
+
form += "<label for='clickmap_min_width'>Min width: </label>";
|
73
|
+
form += "<input class='size' id='clickmap_min_width'>";
|
74
|
+
form += "<label for='clickmap_max_width'>Max width: </label>";
|
75
|
+
form += "<input class='size' id='clickmap_max_width'>";
|
76
|
+
form += "<label for='clickmap_min_height'>Min height: </label>";
|
77
|
+
form += "<input class='size' id='clickmap_min_height'>";
|
78
|
+
form += "<label for='clickmap_max_height'>Max height: </label>";
|
79
|
+
form += "<input class='size' id='clickmap_max_height'><br>";
|
80
|
+
form += "<label for='clickmap_date_from'>Date from: </label>";
|
81
|
+
form += "<input class='date' id='clickmap_date_from' placeholder='yyyy-mm-dd'>";
|
82
|
+
form += "<label for='clickmap_date_to'>Date to: </label>";
|
83
|
+
form += "<input class='date' id='clickmap_date_to' placeholder='yyyy-mm-dd'>";
|
84
|
+
form += "<input type='submit' value='reload'>";
|
85
|
+
form += "<a href='#' id='clickmap_enable'>Enable clickmap</a>";
|
86
|
+
form += "</form>";
|
87
|
+
panel.html(form);
|
88
|
+
$("body").prepend(panel);
|
89
|
+
$("#clickmap_form").on('submit', (function(_this) {
|
90
|
+
return function(e) {
|
91
|
+
e.preventDefault();
|
92
|
+
return _this.loadHeatmapData();
|
93
|
+
};
|
94
|
+
})(this));
|
95
|
+
$("#clickmap_enable").click((function(_this) {
|
96
|
+
return function(e) {
|
97
|
+
e.preventDefault();
|
98
|
+
_this.heatmapArea.show();
|
99
|
+
return _this.heatmap.toggleDisplay();
|
100
|
+
};
|
101
|
+
})(this));
|
102
|
+
return true;
|
103
|
+
};
|
104
|
+
|
105
|
+
return ClickMapAdmin;
|
106
|
+
|
107
|
+
})();
|
@@ -0,0 +1,653 @@
|
|
1
|
+
/*
|
2
|
+
* heatmap.js 1.0 - JavaScript Heatmap Library
|
3
|
+
*
|
4
|
+
* Copyright (c) 2011, Patrick Wied (http://www.patrick-wied.at)
|
5
|
+
* Dual-licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
|
6
|
+
* and the Beerware (http://en.wikipedia.org/wiki/Beerware) license.
|
7
|
+
*/
|
8
|
+
|
9
|
+
(function(w){
|
10
|
+
// the heatmapFactory creates heatmap instances
|
11
|
+
var heatmapFactory = (function(){
|
12
|
+
|
13
|
+
// store object constructor
|
14
|
+
// a heatmap contains a store
|
15
|
+
// the store has to know about the heatmap in order to trigger heatmap updates when datapoints get added
|
16
|
+
var store = function store(hmap){
|
17
|
+
|
18
|
+
var _ = {
|
19
|
+
// data is a two dimensional array
|
20
|
+
// a datapoint gets saved as data[point-x-value][point-y-value]
|
21
|
+
// the value at [point-x-value][point-y-value] is the occurrence of the datapoint
|
22
|
+
data: [],
|
23
|
+
// tight coupling of the heatmap object
|
24
|
+
heatmap: hmap
|
25
|
+
};
|
26
|
+
// the max occurrence - the heatmaps radial gradient alpha transition is based on it
|
27
|
+
this.max = 1;
|
28
|
+
|
29
|
+
this.get = function(key){
|
30
|
+
return _[key];
|
31
|
+
};
|
32
|
+
this.set = function(key, value){
|
33
|
+
_[key] = value;
|
34
|
+
};
|
35
|
+
}
|
36
|
+
|
37
|
+
store.prototype = {
|
38
|
+
// function for adding datapoints to the store
|
39
|
+
// datapoints are usually defined by x and y but could also contain a third parameter which represents the occurrence
|
40
|
+
addDataPoint: function(x, y){
|
41
|
+
if(x < 0 || y < 0)
|
42
|
+
return;
|
43
|
+
|
44
|
+
var me = this,
|
45
|
+
heatmap = me.get("heatmap"),
|
46
|
+
data = me.get("data");
|
47
|
+
|
48
|
+
if(!data[x])
|
49
|
+
data[x] = [];
|
50
|
+
|
51
|
+
if(!data[x][y])
|
52
|
+
data[x][y] = 0;
|
53
|
+
|
54
|
+
// if count parameter is set increment by count otherwise by 1
|
55
|
+
data[x][y]+=(arguments.length<3)?1:arguments[2];
|
56
|
+
|
57
|
+
me.set("data", data);
|
58
|
+
// do we have a new maximum?
|
59
|
+
if(me.max < data[x][y]){
|
60
|
+
// max changed, we need to redraw all existing(lower) datapoints
|
61
|
+
heatmap.get("actx").clearRect(0,0,heatmap.get("width"),heatmap.get("height"));
|
62
|
+
me.setDataSet({ max: data[x][y], data: data }, true);
|
63
|
+
return;
|
64
|
+
}
|
65
|
+
heatmap.drawAlpha(x, y, data[x][y], true);
|
66
|
+
},
|
67
|
+
setDataSet: function(obj, internal){
|
68
|
+
var me = this,
|
69
|
+
heatmap = me.get("heatmap"),
|
70
|
+
data = [],
|
71
|
+
d = obj.data,
|
72
|
+
dlen = d.length;
|
73
|
+
// clear the heatmap before the data set gets drawn
|
74
|
+
heatmap.clear();
|
75
|
+
this.max = obj.max;
|
76
|
+
// if a legend is set, update it
|
77
|
+
heatmap.get("legend") && heatmap.get("legend").update(obj.max);
|
78
|
+
|
79
|
+
if(internal != null && internal){
|
80
|
+
for(var one in d){
|
81
|
+
// jump over undefined indexes
|
82
|
+
if(one === undefined)
|
83
|
+
continue;
|
84
|
+
for(var two in d[one]){
|
85
|
+
if(two === undefined)
|
86
|
+
continue;
|
87
|
+
// if both indexes are defined, push the values into the array
|
88
|
+
heatmap.drawAlpha(one, two, d[one][two], false);
|
89
|
+
}
|
90
|
+
}
|
91
|
+
}else{
|
92
|
+
while(dlen--){
|
93
|
+
var point = d[dlen];
|
94
|
+
heatmap.drawAlpha(point.x, point.y, point.count, false);
|
95
|
+
if(!data[point.x])
|
96
|
+
data[point.x] = [];
|
97
|
+
|
98
|
+
if(!data[point.x][point.y])
|
99
|
+
data[point.x][point.y] = 0;
|
100
|
+
|
101
|
+
data[point.x][point.y] = point.count;
|
102
|
+
}
|
103
|
+
}
|
104
|
+
heatmap.colorize();
|
105
|
+
this.set("data", d);
|
106
|
+
},
|
107
|
+
exportDataSet: function(){
|
108
|
+
var me = this,
|
109
|
+
data = me.get("data"),
|
110
|
+
exportData = [];
|
111
|
+
|
112
|
+
for(var one in data){
|
113
|
+
// jump over undefined indexes
|
114
|
+
if(one === undefined)
|
115
|
+
continue;
|
116
|
+
for(var two in data[one]){
|
117
|
+
if(two === undefined)
|
118
|
+
continue;
|
119
|
+
// if both indexes are defined, push the values into the array
|
120
|
+
exportData.push({x: parseInt(one, 10), y: parseInt(two, 10), count: data[one][two]});
|
121
|
+
}
|
122
|
+
}
|
123
|
+
|
124
|
+
return { max: me.max, data: exportData };
|
125
|
+
},
|
126
|
+
generateRandomDataSet: function(points){
|
127
|
+
var heatmap = this.get("heatmap"),
|
128
|
+
w = heatmap.get("width"),
|
129
|
+
h = heatmap.get("height");
|
130
|
+
var randomset = {},
|
131
|
+
max = Math.floor(Math.random()*1000+1);
|
132
|
+
randomset.max = max;
|
133
|
+
var data = [];
|
134
|
+
while(points--){
|
135
|
+
data.push({x: Math.floor(Math.random()*w+1), y: Math.floor(Math.random()*h+1), count: Math.floor(Math.random()*max+1)});
|
136
|
+
}
|
137
|
+
randomset.data = data;
|
138
|
+
this.setDataSet(randomset);
|
139
|
+
}
|
140
|
+
};
|
141
|
+
|
142
|
+
var legend = function legend(config){
|
143
|
+
this.config = config;
|
144
|
+
|
145
|
+
var _ = {
|
146
|
+
element: null,
|
147
|
+
labelsEl: null,
|
148
|
+
gradientCfg: null,
|
149
|
+
ctx: null
|
150
|
+
};
|
151
|
+
this.get = function(key){
|
152
|
+
return _[key];
|
153
|
+
};
|
154
|
+
this.set = function(key, value){
|
155
|
+
_[key] = value;
|
156
|
+
};
|
157
|
+
this.init();
|
158
|
+
};
|
159
|
+
legend.prototype = {
|
160
|
+
init: function(){
|
161
|
+
var me = this,
|
162
|
+
config = me.config,
|
163
|
+
title = config.title || "Legend",
|
164
|
+
position = config.position,
|
165
|
+
offset = config.offset || 10,
|
166
|
+
gconfig = config.gradient,
|
167
|
+
labelsEl = document.createElement("ul"),
|
168
|
+
labelsHtml = "",
|
169
|
+
grad, element, gradient, positionCss = "";
|
170
|
+
|
171
|
+
me.processGradientObject();
|
172
|
+
|
173
|
+
// Positioning
|
174
|
+
|
175
|
+
// top or bottom
|
176
|
+
if(position.indexOf('t') > -1){
|
177
|
+
positionCss += 'top:'+offset+'px;';
|
178
|
+
}else{
|
179
|
+
positionCss += 'bottom:'+offset+'px;';
|
180
|
+
}
|
181
|
+
|
182
|
+
// left or right
|
183
|
+
if(position.indexOf('l') > -1){
|
184
|
+
positionCss += 'left:'+offset+'px;';
|
185
|
+
}else{
|
186
|
+
positionCss += 'right:'+offset+'px;';
|
187
|
+
}
|
188
|
+
|
189
|
+
element = document.createElement("div");
|
190
|
+
element.style.cssText = "border-radius:5px;position:absolute;"+positionCss+"font-family:Helvetica; width:256px;z-index:10000000000; background:rgba(255,255,255,1);padding:10px;border:1px solid black;margin:0;";
|
191
|
+
element.innerHTML = "<h3 style='padding:0;margin:0;text-align:center;font-size:16px;'>"+title+"</h3>";
|
192
|
+
// create gradient in canvas
|
193
|
+
labelsEl.style.cssText = "position:relative;font-size:12px;display:block;list-style:none;list-style-type:none;margin:0;height:15px;";
|
194
|
+
|
195
|
+
|
196
|
+
// create gradient element
|
197
|
+
gradient = document.createElement("div");
|
198
|
+
gradient.style.cssText = ["position:relative;display:block;width:256px;height:15px;border-bottom:1px solid black; background-image:url(",me.createGradientImage(),");"].join("");
|
199
|
+
|
200
|
+
element.appendChild(labelsEl);
|
201
|
+
element.appendChild(gradient);
|
202
|
+
|
203
|
+
me.set("element", element);
|
204
|
+
me.set("labelsEl", labelsEl);
|
205
|
+
|
206
|
+
me.update(1);
|
207
|
+
},
|
208
|
+
processGradientObject: function(){
|
209
|
+
// create array and sort it
|
210
|
+
var me = this,
|
211
|
+
gradientConfig = this.config.gradient,
|
212
|
+
gradientArr = [];
|
213
|
+
|
214
|
+
for(var key in gradientConfig){
|
215
|
+
if(gradientConfig.hasOwnProperty(key)){
|
216
|
+
gradientArr.push({ stop: key, value: gradientConfig[key] });
|
217
|
+
}
|
218
|
+
}
|
219
|
+
gradientArr.sort(function(a, b){
|
220
|
+
return (a.stop - b.stop);
|
221
|
+
});
|
222
|
+
gradientArr.unshift({ stop: 0, value: 'rgba(0,0,0,0)' });
|
223
|
+
|
224
|
+
me.set("gradientArr", gradientArr);
|
225
|
+
},
|
226
|
+
createGradientImage: function(){
|
227
|
+
var me = this,
|
228
|
+
gradArr = me.get("gradientArr"),
|
229
|
+
length = gradArr.length,
|
230
|
+
canvas = document.createElement("canvas"),
|
231
|
+
ctx = canvas.getContext("2d"),
|
232
|
+
grad;
|
233
|
+
// the gradient in the legend including the ticks will be 256x15px
|
234
|
+
canvas.width = "256";
|
235
|
+
canvas.height = "15";
|
236
|
+
|
237
|
+
grad = ctx.createLinearGradient(0,5,256,10);
|
238
|
+
|
239
|
+
for(var i = 0; i < length; i++){
|
240
|
+
grad.addColorStop(1/(length-1) * i, gradArr[i].value);
|
241
|
+
}
|
242
|
+
|
243
|
+
ctx.fillStyle = grad;
|
244
|
+
ctx.fillRect(0,5,256,10);
|
245
|
+
ctx.strokeStyle = "black";
|
246
|
+
ctx.beginPath();
|
247
|
+
|
248
|
+
for(var i = 0; i < length; i++){
|
249
|
+
ctx.moveTo(((1/(length-1)*i*256) >> 0)+.5, 0);
|
250
|
+
ctx.lineTo(((1/(length-1)*i*256) >> 0)+.5, (i==0)?15:5);
|
251
|
+
}
|
252
|
+
ctx.moveTo(255.5, 0);
|
253
|
+
ctx.lineTo(255.5, 15);
|
254
|
+
ctx.moveTo(255.5, 4.5);
|
255
|
+
ctx.lineTo(0, 4.5);
|
256
|
+
|
257
|
+
ctx.stroke();
|
258
|
+
|
259
|
+
// we re-use the context for measuring the legends label widths
|
260
|
+
me.set("ctx", ctx);
|
261
|
+
|
262
|
+
return canvas.toDataURL();
|
263
|
+
},
|
264
|
+
getElement: function(){
|
265
|
+
return this.get("element");
|
266
|
+
},
|
267
|
+
update: function(max){
|
268
|
+
var me = this,
|
269
|
+
gradient = me.get("gradientArr"),
|
270
|
+
ctx = me.get("ctx"),
|
271
|
+
labels = me.get("labelsEl"),
|
272
|
+
labelText, labelsHtml = "", offset;
|
273
|
+
|
274
|
+
for(var i = 0; i < gradient.length; i++){
|
275
|
+
|
276
|
+
labelText = max*gradient[i].stop >> 0;
|
277
|
+
offset = (ctx.measureText(labelText).width/2) >> 0;
|
278
|
+
|
279
|
+
if(i == 0){
|
280
|
+
offset = 0;
|
281
|
+
}
|
282
|
+
if(i == gradient.length-1){
|
283
|
+
offset *= 2;
|
284
|
+
}
|
285
|
+
labelsHtml += '<li style="position:absolute;left:'+(((((1/(gradient.length-1)*i*256) || 0)) >> 0)-offset+.5)+'px">'+labelText+'</li>';
|
286
|
+
}
|
287
|
+
labels.innerHTML = labelsHtml;
|
288
|
+
}
|
289
|
+
};
|
290
|
+
|
291
|
+
// heatmap object constructor
|
292
|
+
var heatmap = function heatmap(config){
|
293
|
+
// private variables
|
294
|
+
var _ = {
|
295
|
+
radius : 40,
|
296
|
+
element : {},
|
297
|
+
canvas : {},
|
298
|
+
acanvas: {},
|
299
|
+
ctx : {},
|
300
|
+
actx : {},
|
301
|
+
legend: null,
|
302
|
+
visible : true,
|
303
|
+
width : 0,
|
304
|
+
height : 0,
|
305
|
+
max : false,
|
306
|
+
gradient : false,
|
307
|
+
opacity: 180,
|
308
|
+
premultiplyAlpha: false,
|
309
|
+
bounds: {
|
310
|
+
l: 1000,
|
311
|
+
r: 0,
|
312
|
+
t: 1000,
|
313
|
+
b: 0
|
314
|
+
},
|
315
|
+
debug: false
|
316
|
+
};
|
317
|
+
// heatmap store containing the datapoints and information about the maximum
|
318
|
+
// accessible via instance.store
|
319
|
+
this.store = new store(this);
|
320
|
+
|
321
|
+
this.get = function(key){
|
322
|
+
return _[key];
|
323
|
+
};
|
324
|
+
this.set = function(key, value){
|
325
|
+
_[key] = value;
|
326
|
+
};
|
327
|
+
// configure the heatmap when an instance gets created
|
328
|
+
this.configure(config);
|
329
|
+
// and initialize it
|
330
|
+
this.init();
|
331
|
+
};
|
332
|
+
|
333
|
+
// public functions
|
334
|
+
heatmap.prototype = {
|
335
|
+
configure: function(config){
|
336
|
+
var me = this,
|
337
|
+
rout, rin;
|
338
|
+
|
339
|
+
me.set("radius", config["radius"] || 40);
|
340
|
+
me.set("element", (config.element instanceof Object)?config.element:document.getElementById(config.element));
|
341
|
+
me.set("visible", (config.visible != null)?config.visible:true);
|
342
|
+
me.set("max", config.max || false);
|
343
|
+
me.set("gradient", config.gradient || { 0.45: "rgb(0,0,255)", 0.55: "rgb(0,255,255)", 0.65: "rgb(0,255,0)", 0.95: "yellow", 1.0: "rgb(255,0,0)"}); // default is the common blue to red gradient
|
344
|
+
me.set("opacity", parseInt(255/(100/config.opacity), 10) || 180);
|
345
|
+
me.set("width", config.width || 0);
|
346
|
+
me.set("height", config.height || 0);
|
347
|
+
me.set("debug", config.debug);
|
348
|
+
|
349
|
+
if(config.legend){
|
350
|
+
var legendCfg = config.legend;
|
351
|
+
legendCfg.gradient = me.get("gradient");
|
352
|
+
me.set("legend", new legend(legendCfg));
|
353
|
+
}
|
354
|
+
|
355
|
+
},
|
356
|
+
resize: function () {
|
357
|
+
var me = this,
|
358
|
+
element = me.get("element"),
|
359
|
+
canvas = me.get("canvas"),
|
360
|
+
acanvas = me.get("acanvas");
|
361
|
+
canvas.width = acanvas.width = me.get("width") || element.style.width.replace(/px/, "") || me.getWidth(element);
|
362
|
+
this.set("width", canvas.width);
|
363
|
+
canvas.height = acanvas.height = me.get("height") || element.style.height.replace(/px/, "") || me.getHeight(element);
|
364
|
+
this.set("height", canvas.height);
|
365
|
+
},
|
366
|
+
|
367
|
+
init: function(){
|
368
|
+
var me = this,
|
369
|
+
canvas = document.createElement("canvas"),
|
370
|
+
acanvas = document.createElement("canvas"),
|
371
|
+
ctx = canvas.getContext("2d"),
|
372
|
+
actx = acanvas.getContext("2d"),
|
373
|
+
element = me.get("element");
|
374
|
+
|
375
|
+
|
376
|
+
me.initColorPalette();
|
377
|
+
|
378
|
+
me.set("canvas", canvas);
|
379
|
+
me.set("ctx", ctx);
|
380
|
+
me.set("acanvas", acanvas);
|
381
|
+
me.set("actx", actx);
|
382
|
+
|
383
|
+
me.resize();
|
384
|
+
canvas.style.cssText = acanvas.style.cssText = "position:absolute;top:0;left:0;z-index:10000000;";
|
385
|
+
|
386
|
+
if(!me.get("visible"))
|
387
|
+
canvas.style.display = "none";
|
388
|
+
|
389
|
+
element.appendChild(canvas);
|
390
|
+
if(me.get("legend")){
|
391
|
+
element.appendChild(me.get("legend").getElement());
|
392
|
+
}
|
393
|
+
|
394
|
+
// debugging purposes only
|
395
|
+
if(me.get("debug"))
|
396
|
+
document.body.appendChild(acanvas);
|
397
|
+
|
398
|
+
actx.shadowOffsetX = 15000;
|
399
|
+
actx.shadowOffsetY = 15000;
|
400
|
+
actx.shadowBlur = 15;
|
401
|
+
},
|
402
|
+
initColorPalette: function(){
|
403
|
+
|
404
|
+
var me = this,
|
405
|
+
canvas = document.createElement("canvas"),
|
406
|
+
gradient = me.get("gradient"),
|
407
|
+
ctx, grad, testData;
|
408
|
+
|
409
|
+
canvas.width = "1";
|
410
|
+
canvas.height = "256";
|
411
|
+
ctx = canvas.getContext("2d");
|
412
|
+
grad = ctx.createLinearGradient(0,0,1,256);
|
413
|
+
|
414
|
+
// Test how the browser renders alpha by setting a partially transparent pixel
|
415
|
+
// and reading the result. A good browser will return a value reasonably close
|
416
|
+
// to what was set. Some browsers (e.g. on Android) will return a ridiculously wrong value.
|
417
|
+
testData = ctx.getImageData(0,0,1,1);
|
418
|
+
testData.data[0] = testData.data[3] = 64; // 25% red & alpha
|
419
|
+
testData.data[1] = testData.data[2] = 0; // 0% blue & green
|
420
|
+
ctx.putImageData(testData, 0, 0);
|
421
|
+
testData = ctx.getImageData(0,0,1,1);
|
422
|
+
me.set("premultiplyAlpha", (testData.data[0] < 60 || testData.data[0] > 70));
|
423
|
+
|
424
|
+
for(var x in gradient){
|
425
|
+
grad.addColorStop(x, gradient[x]);
|
426
|
+
}
|
427
|
+
|
428
|
+
ctx.fillStyle = grad;
|
429
|
+
ctx.fillRect(0,0,1,256);
|
430
|
+
|
431
|
+
me.set("gradient", ctx.getImageData(0,0,1,256).data);
|
432
|
+
},
|
433
|
+
getWidth: function(element){
|
434
|
+
var width = element.offsetWidth;
|
435
|
+
if(element.style.paddingLeft){
|
436
|
+
width+=element.style.paddingLeft;
|
437
|
+
}
|
438
|
+
if(element.style.paddingRight){
|
439
|
+
width+=element.style.paddingRight;
|
440
|
+
}
|
441
|
+
|
442
|
+
return width;
|
443
|
+
},
|
444
|
+
getHeight: function(element){
|
445
|
+
var height = element.offsetHeight;
|
446
|
+
if(element.style.paddingTop){
|
447
|
+
height+=element.style.paddingTop;
|
448
|
+
}
|
449
|
+
if(element.style.paddingBottom){
|
450
|
+
height+=element.style.paddingBottom;
|
451
|
+
}
|
452
|
+
|
453
|
+
return height;
|
454
|
+
},
|
455
|
+
colorize: function(x, y){
|
456
|
+
// get the private variables
|
457
|
+
var me = this,
|
458
|
+
width = me.get("width"),
|
459
|
+
radius = me.get("radius"),
|
460
|
+
height = me.get("height"),
|
461
|
+
actx = me.get("actx"),
|
462
|
+
ctx = me.get("ctx"),
|
463
|
+
x2 = radius * 3,
|
464
|
+
premultiplyAlpha = me.get("premultiplyAlpha"),
|
465
|
+
palette = me.get("gradient"),
|
466
|
+
opacity = me.get("opacity"),
|
467
|
+
bounds = me.get("bounds"),
|
468
|
+
left, top, bottom, right,
|
469
|
+
image, imageData, length, alpha, offset, finalAlpha;
|
470
|
+
|
471
|
+
if(x != null && y != null){
|
472
|
+
if(x+x2>width){
|
473
|
+
x=width-x2;
|
474
|
+
}
|
475
|
+
if(x<0){
|
476
|
+
x=0;
|
477
|
+
}
|
478
|
+
if(y<0){
|
479
|
+
y=0;
|
480
|
+
}
|
481
|
+
if(y+x2>height){
|
482
|
+
y=height-x2;
|
483
|
+
}
|
484
|
+
left = x;
|
485
|
+
top = y;
|
486
|
+
right = x + x2;
|
487
|
+
bottom = y + x2;
|
488
|
+
|
489
|
+
}else{
|
490
|
+
if(bounds['l'] < 0){
|
491
|
+
left = 0;
|
492
|
+
}else{
|
493
|
+
left = bounds['l'];
|
494
|
+
}
|
495
|
+
if(bounds['r'] > width){
|
496
|
+
right = width;
|
497
|
+
}else{
|
498
|
+
right = bounds['r'];
|
499
|
+
}
|
500
|
+
if(bounds['t'] < 0){
|
501
|
+
top = 0;
|
502
|
+
}else{
|
503
|
+
top = bounds['t'];
|
504
|
+
}
|
505
|
+
if(bounds['b'] > height){
|
506
|
+
bottom = height;
|
507
|
+
}else{
|
508
|
+
bottom = bounds['b'];
|
509
|
+
}
|
510
|
+
}
|
511
|
+
|
512
|
+
image = actx.getImageData(left, top, right-left, bottom-top);
|
513
|
+
imageData = image.data;
|
514
|
+
length = imageData.length;
|
515
|
+
// loop thru the area
|
516
|
+
for(var i=3; i < length; i+=4){
|
517
|
+
|
518
|
+
// [0] -> r, [1] -> g, [2] -> b, [3] -> alpha
|
519
|
+
alpha = imageData[i],
|
520
|
+
offset = alpha*4;
|
521
|
+
|
522
|
+
if(!offset)
|
523
|
+
continue;
|
524
|
+
|
525
|
+
// we ve started with i=3
|
526
|
+
// set the new r, g and b values
|
527
|
+
finalAlpha = (alpha < opacity)?alpha:opacity;
|
528
|
+
imageData[i-3]=palette[offset];
|
529
|
+
imageData[i-2]=palette[offset+1];
|
530
|
+
imageData[i-1]=palette[offset+2];
|
531
|
+
|
532
|
+
if (premultiplyAlpha) {
|
533
|
+
// To fix browsers that premultiply incorrectly, we'll pass in a value scaled
|
534
|
+
// appropriately so when the multiplication happens the correct value will result.
|
535
|
+
imageData[i-3] /= 255/finalAlpha;
|
536
|
+
imageData[i-2] /= 255/finalAlpha;
|
537
|
+
imageData[i-1] /= 255/finalAlpha;
|
538
|
+
}
|
539
|
+
|
540
|
+
// we want the heatmap to have a gradient from transparent to the colors
|
541
|
+
// as long as alpha is lower than the defined opacity (maximum), we'll use the alpha value
|
542
|
+
imageData[i] = finalAlpha;
|
543
|
+
}
|
544
|
+
// the rgb data manipulation didn't affect the ImageData object(defined on the top)
|
545
|
+
// after the manipulation process we have to set the manipulated data to the ImageData object
|
546
|
+
image.data = imageData;
|
547
|
+
ctx.putImageData(image, left, top);
|
548
|
+
},
|
549
|
+
drawAlpha: function(x, y, count, colorize){
|
550
|
+
// storing the variables because they will be often used
|
551
|
+
var me = this,
|
552
|
+
radius = me.get("radius"),
|
553
|
+
ctx = me.get("actx"),
|
554
|
+
max = me.get("max"),
|
555
|
+
bounds = me.get("bounds"),
|
556
|
+
xb = x - (1.5 * radius) >> 0, yb = y - (1.5 * radius) >> 0,
|
557
|
+
xc = x + (1.5 * radius) >> 0, yc = y + (1.5 * radius) >> 0;
|
558
|
+
|
559
|
+
ctx.shadowColor = ('rgba(0,0,0,'+((count)?(count/me.store.max):'0.1')+')');
|
560
|
+
|
561
|
+
ctx.shadowOffsetX = 15000;
|
562
|
+
ctx.shadowOffsetY = 15000;
|
563
|
+
ctx.shadowBlur = 15;
|
564
|
+
|
565
|
+
ctx.beginPath();
|
566
|
+
ctx.arc(x - 15000, y - 15000, radius, 0, Math.PI * 2, true);
|
567
|
+
ctx.closePath();
|
568
|
+
ctx.fill();
|
569
|
+
if(colorize){
|
570
|
+
// finally colorize the area
|
571
|
+
me.colorize(xb,yb);
|
572
|
+
}else{
|
573
|
+
// or update the boundaries for the area that then should be colorized
|
574
|
+
if(xb < bounds["l"]){
|
575
|
+
bounds["l"] = xb;
|
576
|
+
}
|
577
|
+
if(yb < bounds["t"]){
|
578
|
+
bounds["t"] = yb;
|
579
|
+
}
|
580
|
+
if(xc > bounds['r']){
|
581
|
+
bounds['r'] = xc;
|
582
|
+
}
|
583
|
+
if(yc > bounds['b']){
|
584
|
+
bounds['b'] = yc;
|
585
|
+
}
|
586
|
+
}
|
587
|
+
},
|
588
|
+
toggleDisplay: function(){
|
589
|
+
var me = this,
|
590
|
+
visible = me.get("visible"),
|
591
|
+
canvas = me.get("canvas");
|
592
|
+
|
593
|
+
if(!visible)
|
594
|
+
canvas.style.display = "block";
|
595
|
+
else
|
596
|
+
canvas.style.display = "none";
|
597
|
+
|
598
|
+
me.set("visible", !visible);
|
599
|
+
},
|
600
|
+
// dataURL export
|
601
|
+
getImageData: function(){
|
602
|
+
return this.get("canvas").toDataURL();
|
603
|
+
},
|
604
|
+
clear: function(){
|
605
|
+
var me = this,
|
606
|
+
w = me.get("width"),
|
607
|
+
h = me.get("height");
|
608
|
+
|
609
|
+
me.store.set("data",[]);
|
610
|
+
// @TODO: reset stores max to 1
|
611
|
+
//me.store.max = 1;
|
612
|
+
me.get("ctx").clearRect(0,0,w,h);
|
613
|
+
me.get("actx").clearRect(0,0,w,h);
|
614
|
+
},
|
615
|
+
cleanup: function(){
|
616
|
+
var me = this;
|
617
|
+
me.get("element").removeChild(me.get("canvas"));
|
618
|
+
}
|
619
|
+
};
|
620
|
+
|
621
|
+
return {
|
622
|
+
create: function(config){
|
623
|
+
return new heatmap(config);
|
624
|
+
},
|
625
|
+
util: {
|
626
|
+
mousePosition: function(ev){
|
627
|
+
// this doesn't work right
|
628
|
+
// rather use
|
629
|
+
/*
|
630
|
+
// this = element to observe
|
631
|
+
var x = ev.pageX - this.offsetLeft;
|
632
|
+
var y = ev.pageY - this.offsetTop;
|
633
|
+
|
634
|
+
*/
|
635
|
+
var x, y;
|
636
|
+
|
637
|
+
if (ev.layerX) { // Firefox
|
638
|
+
x = ev.layerX;
|
639
|
+
y = ev.layerY;
|
640
|
+
} else if (ev.offsetX) { // Opera
|
641
|
+
x = ev.offsetX;
|
642
|
+
y = ev.offsetY;
|
643
|
+
}
|
644
|
+
if(typeof(x)=='undefined')
|
645
|
+
return;
|
646
|
+
|
647
|
+
return [x,y];
|
648
|
+
}
|
649
|
+
}
|
650
|
+
};
|
651
|
+
})();
|
652
|
+
w.h337 = w.heatmapFactory = heatmapFactory;
|
653
|
+
})(window);
|
metadata
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: click_heat_map
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Adam Tomecek
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-08-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: sinatra-activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description: Determine where your users click with simple click heat maps.
|
28
|
+
email: adam.tomecek@gmail.com
|
29
|
+
executables: []
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files: []
|
32
|
+
files:
|
33
|
+
- lib/click_heat_map.rb
|
34
|
+
- lib/models/click.rb
|
35
|
+
- lib/public/js/heatmap.js
|
36
|
+
- lib/public/js/clickmap.js
|
37
|
+
- lib/public/js/clickmap_admin.js
|
38
|
+
- lib/public/css/clickmap.css
|
39
|
+
homepage: https://github.com/adamtomecek/clickmap
|
40
|
+
licenses:
|
41
|
+
- MIT
|
42
|
+
metadata: {}
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options: []
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - '>='
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '0'
|
52
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - '>='
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
requirements: []
|
58
|
+
rubyforge_project:
|
59
|
+
rubygems_version: 2.0.14
|
60
|
+
signing_key:
|
61
|
+
specification_version: 4
|
62
|
+
summary: Click measuring and heatmap generating.
|
63
|
+
test_files: []
|