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 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
@@ -0,0 +1,3 @@
1
+ class Click < ActiveRecord::Base
2
+
3
+ end
@@ -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: []