heatmap-rails 0.1.0
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/.gitignore +16 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +35 -0
- data/LICENSE.txt +21 -0
- data/README.md +111 -0
- data/Rakefile +6 -0
- data/app/assets/javascripts/heatmap.js +720 -0
- data/app/controllers/points_controller.rb +17 -0
- data/app/models/heat_map.rb +2 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/config/routes.rb +3 -0
- data/heatmap-rails.gemspec +34 -0
- data/lib/generators/heatmap_rails/install_generator.rb +18 -0
- data/lib/generators/heatmap_rails/templates/initializer.rb +11 -0
- data/lib/heatmap-rails.rb +12 -0
- data/lib/heatmap/engine.rb +9 -0
- data/lib/heatmap/helper.rb +143 -0
- data/lib/heatmap/version.rb +3 -0
- data/spec/heatmap_rb_spec.rb +9 -0
- data/spec/spec_helper.rb +14 -0
- metadata +109 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b8dc72236de384b182dc7b04da838b9a7711a359
|
4
|
+
data.tar.gz: 1df0311aabec2f8fa2b98c4c0202423f9858cf5b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 505a2473e029e86daeba997a3b61270ed7209e5bdb4416daab684296d8c8914f4acfd0c3fc77ed65dab9e57c4634929a30a4ef84c6e1d819410f78322c2708cc
|
7
|
+
data.tar.gz: 21a316d4610e58f03265fe1e4ab559e510a95fae7bb666461829625b622c8648350e6282c7c1df8229a31456117e8a0911de184a9fb3e64a5e6196a220dd1213
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
heatmap-rails (0.1.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
diff-lcs (1.3)
|
10
|
+
rake (10.5.0)
|
11
|
+
rspec (3.7.0)
|
12
|
+
rspec-core (~> 3.7.0)
|
13
|
+
rspec-expectations (~> 3.7.0)
|
14
|
+
rspec-mocks (~> 3.7.0)
|
15
|
+
rspec-core (3.7.0)
|
16
|
+
rspec-support (~> 3.7.0)
|
17
|
+
rspec-expectations (3.7.0)
|
18
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
19
|
+
rspec-support (~> 3.7.0)
|
20
|
+
rspec-mocks (3.7.0)
|
21
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
22
|
+
rspec-support (~> 3.7.0)
|
23
|
+
rspec-support (3.7.0)
|
24
|
+
|
25
|
+
PLATFORMS
|
26
|
+
ruby
|
27
|
+
|
28
|
+
DEPENDENCIES
|
29
|
+
bundler (~> 1.16.a)
|
30
|
+
heatmap-rails!
|
31
|
+
rake (~> 10.0)
|
32
|
+
rspec (~> 3.0)
|
33
|
+
|
34
|
+
BUNDLED WITH
|
35
|
+
1.16.0.pre.3
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 Hassan
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
# HeatmapRb :construction:
|
2
|
+
|
3
|
+
Integrate heatmaps in your web application to see on which part the user spends most time on your web application. Where does users click on the page.
|
4
|
+
Helping in gathering analytics to find out what works on the web, what attracts most of the users.
|
5
|
+
View user interactions and make your application more amazing! :sparkles:
|
6
|
+
|
7
|
+
## Local Testing
|
8
|
+
|
9
|
+
Use
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'heatmap-rails', git: 'https://github.com/Qbatch/heatmap-rails.git'
|
13
|
+
```
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
Add this line to your application's Gemfile:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
gem 'heatmap-rails'
|
21
|
+
```
|
22
|
+
|
23
|
+
And then execute:
|
24
|
+
|
25
|
+
$ bundle
|
26
|
+
|
27
|
+
Or install it yourself as:
|
28
|
+
|
29
|
+
$ gem install heatmap-rails
|
30
|
+
|
31
|
+
## Usage
|
32
|
+
|
33
|
+
1. Install the gem
|
34
|
+
2. Run the command to generate migration:
|
35
|
+
```console
|
36
|
+
$ rails g heatmap_rails:install
|
37
|
+
```
|
38
|
+
|
39
|
+
3. Migrate:
|
40
|
+
```console
|
41
|
+
$ rake db:migrate
|
42
|
+
```
|
43
|
+
|
44
|
+
4. Include the following helper on any page where you need to generate the heatmap:
|
45
|
+
```erb
|
46
|
+
<%= save_heatmap %>
|
47
|
+
```
|
48
|
+
|
49
|
+
5. Include where to show the heatmap:
|
50
|
+
```erb
|
51
|
+
<%= show_heatmap(request.path) %>
|
52
|
+
```
|
53
|
+
|
54
|
+
6. In respective JS file, Require HeatMap.Js to show the heatmap:
|
55
|
+
```js
|
56
|
+
//= require heatmap.js
|
57
|
+
```
|
58
|
+
## Viewing Heat Maps
|
59
|
+
Use the helper
|
60
|
+
```erb
|
61
|
+
<%= show_heatmap(request.path) %>
|
62
|
+
```
|
63
|
+
The argument is the path of current page. This way the helper will only display the respective heatmap.
|
64
|
+
The viewing can be done in multiple ways, for example if you want only the admin users to view heatmap, you can do something like:
|
65
|
+
|
66
|
+
```erb
|
67
|
+
<% if admin_user_signed_in? %>
|
68
|
+
<%= show_heatmap(request.path) %>
|
69
|
+
<% end %>
|
70
|
+
```
|
71
|
+
|
72
|
+
Another way can be using some code in URL. For example is you want to use URL like
|
73
|
+
|
74
|
+
```url
|
75
|
+
www.website.com/see_heatmap
|
76
|
+
```
|
77
|
+
|
78
|
+
You can use:
|
79
|
+
|
80
|
+
```erb
|
81
|
+
<% if request.path.include?("see_heatmap") %>
|
82
|
+
<%= show_heatmap(request.path) %>
|
83
|
+
<% end %>
|
84
|
+
```
|
85
|
+
|
86
|
+
And there can be multiple ways!
|
87
|
+
### Options
|
88
|
+
|
89
|
+
You can customize:
|
90
|
+
```erb
|
91
|
+
<%= save_heatmap({click: 3, move: 50}) %>
|
92
|
+
```
|
93
|
+
These are default values.
|
94
|
+
|
95
|
+
## Development :construction:
|
96
|
+
|
97
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
98
|
+
|
99
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
100
|
+
|
101
|
+
## Credits
|
102
|
+
heatmap-rails uses [HeatMap.Js](https://www.patrick-wied.at/static/heatmapjs/) to generated show heatmaps.
|
103
|
+
|
104
|
+
## Contributing :construction:
|
105
|
+
|
106
|
+
1. [Bug reports](https://github.com/Qbatch/heatmap-rails/issues) are always welcome.
|
107
|
+
2. [Pull Requests](https://github.com/Qbatch/heatmap-rails/pulls). Suggest or Update.
|
108
|
+
|
109
|
+
## License :construction:
|
110
|
+
|
111
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,720 @@
|
|
1
|
+
/*
|
2
|
+
* heatmap.js v2.0.5 | JavaScript Heatmap Library
|
3
|
+
*
|
4
|
+
* Copyright 2008-2016 Patrick Wied <heatmapjs@patrick-wied.at> - All rights reserved.
|
5
|
+
* Dual licensed under MIT and Beerware license
|
6
|
+
*
|
7
|
+
* :: 2016-09-05 01:16
|
8
|
+
*/
|
9
|
+
;(function (name, context, factory) {
|
10
|
+
|
11
|
+
// Supports UMD. AMD, CommonJS/Node.js and browser context
|
12
|
+
if (typeof module !== "undefined" && module.exports) {
|
13
|
+
module.exports = factory();
|
14
|
+
} else if (typeof define === "function" && define.amd) {
|
15
|
+
define(factory);
|
16
|
+
} else {
|
17
|
+
context[name] = factory();
|
18
|
+
}
|
19
|
+
|
20
|
+
})("h337", this, function () {
|
21
|
+
|
22
|
+
// Heatmap Config stores default values and will be merged with instance config
|
23
|
+
var HeatmapConfig = {
|
24
|
+
defaultRadius: 40,
|
25
|
+
defaultRenderer: 'canvas2d',
|
26
|
+
defaultGradient: { 0.25: "rgb(0,0,255)", 0.55: "rgb(0,255,0)", 0.85: "yellow", 1.0: "rgb(255,0,0)"},
|
27
|
+
defaultMaxOpacity: 1,
|
28
|
+
defaultMinOpacity: 0,
|
29
|
+
defaultBlur: .85,
|
30
|
+
defaultXField: 'x',
|
31
|
+
defaultYField: 'y',
|
32
|
+
defaultValueField: 'value',
|
33
|
+
plugins: {}
|
34
|
+
};
|
35
|
+
var Store = (function StoreClosure() {
|
36
|
+
|
37
|
+
var Store = function Store(config) {
|
38
|
+
this._coordinator = {};
|
39
|
+
this._data = [];
|
40
|
+
this._radi = [];
|
41
|
+
this._min = 10;
|
42
|
+
this._max = 1;
|
43
|
+
this._xField = config['xField'] || config.defaultXField;
|
44
|
+
this._yField = config['yField'] || config.defaultYField;
|
45
|
+
this._valueField = config['valueField'] || config.defaultValueField;
|
46
|
+
|
47
|
+
if (config["radius"]) {
|
48
|
+
this._cfgRadius = config["radius"];
|
49
|
+
}
|
50
|
+
};
|
51
|
+
|
52
|
+
var defaultRadius = HeatmapConfig.defaultRadius;
|
53
|
+
|
54
|
+
Store.prototype = {
|
55
|
+
// when forceRender = false -> called from setData, omits renderall event
|
56
|
+
_organiseData: function(dataPoint, forceRender) {
|
57
|
+
var x = dataPoint[this._xField];
|
58
|
+
var y = dataPoint[this._yField];
|
59
|
+
var radi = this._radi;
|
60
|
+
var store = this._data;
|
61
|
+
var max = this._max;
|
62
|
+
var min = this._min;
|
63
|
+
var value = dataPoint[this._valueField] || 1;
|
64
|
+
var radius = dataPoint.radius || this._cfgRadius || defaultRadius;
|
65
|
+
|
66
|
+
if (!store[x]) {
|
67
|
+
store[x] = [];
|
68
|
+
radi[x] = [];
|
69
|
+
}
|
70
|
+
|
71
|
+
if (!store[x][y]) {
|
72
|
+
store[x][y] = value;
|
73
|
+
radi[x][y] = radius;
|
74
|
+
} else {
|
75
|
+
store[x][y] += value;
|
76
|
+
}
|
77
|
+
var storedVal = store[x][y];
|
78
|
+
|
79
|
+
if (storedVal > max) {
|
80
|
+
if (!forceRender) {
|
81
|
+
this._max = storedVal;
|
82
|
+
} else {
|
83
|
+
this.setDataMax(storedVal);
|
84
|
+
}
|
85
|
+
return false;
|
86
|
+
} else if (storedVal < min) {
|
87
|
+
if (!forceRender) {
|
88
|
+
this._min = storedVal;
|
89
|
+
} else {
|
90
|
+
this.setDataMin(storedVal);
|
91
|
+
}
|
92
|
+
return false;
|
93
|
+
} else {
|
94
|
+
return {
|
95
|
+
x: x,
|
96
|
+
y: y,
|
97
|
+
value: value,
|
98
|
+
radius: radius,
|
99
|
+
min: min,
|
100
|
+
max: max
|
101
|
+
};
|
102
|
+
}
|
103
|
+
},
|
104
|
+
_unOrganizeData: function() {
|
105
|
+
var unorganizedData = [];
|
106
|
+
var data = this._data;
|
107
|
+
var radi = this._radi;
|
108
|
+
|
109
|
+
for (var x in data) {
|
110
|
+
for (var y in data[x]) {
|
111
|
+
|
112
|
+
unorganizedData.push({
|
113
|
+
x: x,
|
114
|
+
y: y,
|
115
|
+
radius: radi[x][y],
|
116
|
+
value: data[x][y]
|
117
|
+
});
|
118
|
+
|
119
|
+
}
|
120
|
+
}
|
121
|
+
return {
|
122
|
+
min: this._min,
|
123
|
+
max: this._max,
|
124
|
+
data: unorganizedData
|
125
|
+
};
|
126
|
+
},
|
127
|
+
_onExtremaChange: function() {
|
128
|
+
this._coordinator.emit('extremachange', {
|
129
|
+
min: this._min,
|
130
|
+
max: this._max
|
131
|
+
});
|
132
|
+
},
|
133
|
+
addData: function() {
|
134
|
+
if (arguments[0].length > 0) {
|
135
|
+
var dataArr = arguments[0];
|
136
|
+
var dataLen = dataArr.length;
|
137
|
+
while (dataLen--) {
|
138
|
+
this.addData.call(this, dataArr[dataLen]);
|
139
|
+
}
|
140
|
+
} else {
|
141
|
+
// add to store
|
142
|
+
var organisedEntry = this._organiseData(arguments[0], true);
|
143
|
+
if (organisedEntry) {
|
144
|
+
// if it's the first datapoint initialize the extremas with it
|
145
|
+
if (this._data.length === 0) {
|
146
|
+
this._min = this._max = organisedEntry.value;
|
147
|
+
}
|
148
|
+
this._coordinator.emit('renderpartial', {
|
149
|
+
min: this._min,
|
150
|
+
max: this._max,
|
151
|
+
data: [organisedEntry]
|
152
|
+
});
|
153
|
+
}
|
154
|
+
}
|
155
|
+
return this;
|
156
|
+
},
|
157
|
+
setData: function(data) {
|
158
|
+
var dataPoints = data.data;
|
159
|
+
var pointsLen = dataPoints.length;
|
160
|
+
|
161
|
+
|
162
|
+
// reset data arrays
|
163
|
+
this._data = [];
|
164
|
+
this._radi = [];
|
165
|
+
|
166
|
+
for(var i = 0; i < pointsLen; i++) {
|
167
|
+
this._organiseData(dataPoints[i], false);
|
168
|
+
}
|
169
|
+
this._max = data.max;
|
170
|
+
this._min = data.min || 0;
|
171
|
+
|
172
|
+
this._onExtremaChange();
|
173
|
+
this._coordinator.emit('renderall', this._getInternalData());
|
174
|
+
return this;
|
175
|
+
},
|
176
|
+
removeData: function() {
|
177
|
+
// TODO: implement
|
178
|
+
},
|
179
|
+
setDataMax: function(max) {
|
180
|
+
this._max = max;
|
181
|
+
this._onExtremaChange();
|
182
|
+
this._coordinator.emit('renderall', this._getInternalData());
|
183
|
+
return this;
|
184
|
+
},
|
185
|
+
setDataMin: function(min) {
|
186
|
+
this._min = min;
|
187
|
+
this._onExtremaChange();
|
188
|
+
this._coordinator.emit('renderall', this._getInternalData());
|
189
|
+
return this;
|
190
|
+
},
|
191
|
+
setCoordinator: function(coordinator) {
|
192
|
+
this._coordinator = coordinator;
|
193
|
+
},
|
194
|
+
_getInternalData: function() {
|
195
|
+
return {
|
196
|
+
max: this._max,
|
197
|
+
min: this._min,
|
198
|
+
data: this._data,
|
199
|
+
radi: this._radi
|
200
|
+
};
|
201
|
+
},
|
202
|
+
getData: function() {
|
203
|
+
return this._unOrganizeData();
|
204
|
+
}/*,
|
205
|
+
TODO: rethink.
|
206
|
+
getValueAt: function(point) {
|
207
|
+
var value;
|
208
|
+
var radius = 100;
|
209
|
+
var x = point.x;
|
210
|
+
var y = point.y;
|
211
|
+
var data = this._data;
|
212
|
+
if (data[x] && data[x][y]) {
|
213
|
+
return data[x][y];
|
214
|
+
} else {
|
215
|
+
var values = [];
|
216
|
+
// radial search for datapoints based on default radius
|
217
|
+
for(var distance = 1; distance < radius; distance++) {
|
218
|
+
var neighbors = distance * 2 +1;
|
219
|
+
var startX = x - distance;
|
220
|
+
var startY = y - distance;
|
221
|
+
for(var i = 0; i < neighbors; i++) {
|
222
|
+
for (var o = 0; o < neighbors; o++) {
|
223
|
+
if ((i == 0 || i == neighbors-1) || (o == 0 || o == neighbors-1)) {
|
224
|
+
if (data[startY+i] && data[startY+i][startX+o]) {
|
225
|
+
values.push(data[startY+i][startX+o]);
|
226
|
+
}
|
227
|
+
} else {
|
228
|
+
continue;
|
229
|
+
}
|
230
|
+
}
|
231
|
+
}
|
232
|
+
}
|
233
|
+
if (values.length > 0) {
|
234
|
+
return Math.max.apply(Math, values);
|
235
|
+
}
|
236
|
+
}
|
237
|
+
return false;
|
238
|
+
}*/
|
239
|
+
};
|
240
|
+
|
241
|
+
|
242
|
+
return Store;
|
243
|
+
})();
|
244
|
+
|
245
|
+
var Canvas2dRenderer = (function Canvas2dRendererClosure() {
|
246
|
+
|
247
|
+
var _getColorPalette = function(config) {
|
248
|
+
var gradientConfig = config.gradient || config.defaultGradient;
|
249
|
+
var paletteCanvas = document.createElement('canvas');
|
250
|
+
var paletteCtx = paletteCanvas.getContext('2d');
|
251
|
+
|
252
|
+
paletteCanvas.width = 256;
|
253
|
+
paletteCanvas.height = 1;
|
254
|
+
|
255
|
+
var gradient = paletteCtx.createLinearGradient(0, 0, 256, 1);
|
256
|
+
for (var key in gradientConfig) {
|
257
|
+
gradient.addColorStop(key, gradientConfig[key]);
|
258
|
+
}
|
259
|
+
|
260
|
+
paletteCtx.fillStyle = gradient;
|
261
|
+
paletteCtx.fillRect(0, 0, 256, 1);
|
262
|
+
|
263
|
+
return paletteCtx.getImageData(0, 0, 256, 1).data;
|
264
|
+
};
|
265
|
+
|
266
|
+
var _getPointTemplate = function(radius, blurFactor) {
|
267
|
+
var tplCanvas = document.createElement('canvas');
|
268
|
+
var tplCtx = tplCanvas.getContext('2d');
|
269
|
+
var x = radius;
|
270
|
+
var y = radius;
|
271
|
+
tplCanvas.width = tplCanvas.height = radius*2;
|
272
|
+
|
273
|
+
if (blurFactor == 1) {
|
274
|
+
tplCtx.beginPath();
|
275
|
+
tplCtx.arc(x, y, radius, 0, 2 * Math.PI, false);
|
276
|
+
tplCtx.fillStyle = 'rgba(0,0,0,1)';
|
277
|
+
tplCtx.fill();
|
278
|
+
} else {
|
279
|
+
var gradient = tplCtx.createRadialGradient(x, y, radius*blurFactor, x, y, radius);
|
280
|
+
gradient.addColorStop(0, 'rgba(0,0,0,1)');
|
281
|
+
gradient.addColorStop(1, 'rgba(0,0,0,0)');
|
282
|
+
tplCtx.fillStyle = gradient;
|
283
|
+
tplCtx.fillRect(0, 0, 2*radius, 2*radius);
|
284
|
+
}
|
285
|
+
|
286
|
+
|
287
|
+
|
288
|
+
return tplCanvas;
|
289
|
+
};
|
290
|
+
|
291
|
+
var _prepareData = function(data) {
|
292
|
+
var renderData = [];
|
293
|
+
var min = data.min;
|
294
|
+
var max = data.max;
|
295
|
+
var radi = data.radi;
|
296
|
+
var data = data.data;
|
297
|
+
|
298
|
+
var xValues = Object.keys(data);
|
299
|
+
var xValuesLen = xValues.length;
|
300
|
+
|
301
|
+
while(xValuesLen--) {
|
302
|
+
var xValue = xValues[xValuesLen];
|
303
|
+
var yValues = Object.keys(data[xValue]);
|
304
|
+
var yValuesLen = yValues.length;
|
305
|
+
while(yValuesLen--) {
|
306
|
+
var yValue = yValues[yValuesLen];
|
307
|
+
var value = data[xValue][yValue];
|
308
|
+
var radius = radi[xValue][yValue];
|
309
|
+
renderData.push({
|
310
|
+
x: xValue,
|
311
|
+
y: yValue,
|
312
|
+
value: value,
|
313
|
+
radius: radius
|
314
|
+
});
|
315
|
+
}
|
316
|
+
}
|
317
|
+
|
318
|
+
return {
|
319
|
+
min: min,
|
320
|
+
max: max,
|
321
|
+
data: renderData
|
322
|
+
};
|
323
|
+
};
|
324
|
+
|
325
|
+
|
326
|
+
function Canvas2dRenderer(config) {
|
327
|
+
var container = config.container;
|
328
|
+
var shadowCanvas = this.shadowCanvas = document.createElement('canvas');
|
329
|
+
var canvas = this.canvas = config.canvas || document.createElement('canvas');
|
330
|
+
var renderBoundaries = this._renderBoundaries = [10000, 10000, 0, 0];
|
331
|
+
|
332
|
+
var computed = getComputedStyle(config.container) || {};
|
333
|
+
|
334
|
+
canvas.className = 'heatmap-canvas';
|
335
|
+
|
336
|
+
this._width = canvas.width = shadowCanvas.width = config.width || +(computed.width.replace(/px/,''));
|
337
|
+
this._height = canvas.height = shadowCanvas.height = config.height || +(computed.height.replace(/px/,''));
|
338
|
+
|
339
|
+
this.shadowCtx = shadowCanvas.getContext('2d');
|
340
|
+
this.ctx = canvas.getContext('2d');
|
341
|
+
|
342
|
+
// @TODO:
|
343
|
+
// conditional wrapper
|
344
|
+
|
345
|
+
canvas.style.cssText = shadowCanvas.style.cssText = 'position:absolute;left:0;top:0;';
|
346
|
+
|
347
|
+
container.style.position = 'relative';
|
348
|
+
container.appendChild(canvas);
|
349
|
+
|
350
|
+
this._palette = _getColorPalette(config);
|
351
|
+
this._templates = {};
|
352
|
+
|
353
|
+
this._setStyles(config);
|
354
|
+
};
|
355
|
+
|
356
|
+
Canvas2dRenderer.prototype = {
|
357
|
+
renderPartial: function(data) {
|
358
|
+
if (data.data.length > 0) {
|
359
|
+
this._drawAlpha(data);
|
360
|
+
this._colorize();
|
361
|
+
}
|
362
|
+
},
|
363
|
+
renderAll: function(data) {
|
364
|
+
// reset render boundaries
|
365
|
+
this._clear();
|
366
|
+
if (data.data.length > 0) {
|
367
|
+
this._drawAlpha(_prepareData(data));
|
368
|
+
this._colorize();
|
369
|
+
}
|
370
|
+
},
|
371
|
+
_updateGradient: function(config) {
|
372
|
+
this._palette = _getColorPalette(config);
|
373
|
+
},
|
374
|
+
updateConfig: function(config) {
|
375
|
+
if (config['gradient']) {
|
376
|
+
this._updateGradient(config);
|
377
|
+
}
|
378
|
+
this._setStyles(config);
|
379
|
+
},
|
380
|
+
setDimensions: function(width, height) {
|
381
|
+
this._width = width;
|
382
|
+
this._height = height;
|
383
|
+
this.canvas.width = this.shadowCanvas.width = width;
|
384
|
+
this.canvas.height = this.shadowCanvas.height = height;
|
385
|
+
},
|
386
|
+
_clear: function() {
|
387
|
+
this.shadowCtx.clearRect(0, 0, this._width, this._height);
|
388
|
+
this.ctx.clearRect(0, 0, this._width, this._height);
|
389
|
+
},
|
390
|
+
_setStyles: function(config) {
|
391
|
+
this._blur = (config.blur == 0)?0:(config.blur || config.defaultBlur);
|
392
|
+
|
393
|
+
if (config.backgroundColor) {
|
394
|
+
this.canvas.style.backgroundColor = config.backgroundColor;
|
395
|
+
}
|
396
|
+
|
397
|
+
this._width = this.canvas.width = this.shadowCanvas.width = config.width || this._width;
|
398
|
+
this._height = this.canvas.height = this.shadowCanvas.height = config.height || this._height;
|
399
|
+
|
400
|
+
|
401
|
+
this._opacity = (config.opacity || 0) * 255;
|
402
|
+
this._maxOpacity = (config.maxOpacity || config.defaultMaxOpacity) * 255;
|
403
|
+
this._minOpacity = (config.minOpacity || config.defaultMinOpacity) * 255;
|
404
|
+
this._useGradientOpacity = !!config.useGradientOpacity;
|
405
|
+
},
|
406
|
+
_drawAlpha: function(data) {
|
407
|
+
var min = this._min = data.min;
|
408
|
+
var max = this._max = data.max;
|
409
|
+
var data = data.data || [];
|
410
|
+
var dataLen = data.length;
|
411
|
+
// on a point basis?
|
412
|
+
var blur = 1 - this._blur;
|
413
|
+
|
414
|
+
while(dataLen--) {
|
415
|
+
|
416
|
+
var point = data[dataLen];
|
417
|
+
|
418
|
+
var x = point.x;
|
419
|
+
var y = point.y;
|
420
|
+
var radius = point.radius;
|
421
|
+
// if value is bigger than max
|
422
|
+
// use max as value
|
423
|
+
var value = Math.min(point.value, max);
|
424
|
+
var rectX = x - radius;
|
425
|
+
var rectY = y - radius;
|
426
|
+
var shadowCtx = this.shadowCtx;
|
427
|
+
|
428
|
+
|
429
|
+
|
430
|
+
|
431
|
+
var tpl;
|
432
|
+
if (!this._templates[radius]) {
|
433
|
+
this._templates[radius] = tpl = _getPointTemplate(radius, blur);
|
434
|
+
} else {
|
435
|
+
tpl = this._templates[radius];
|
436
|
+
}
|
437
|
+
// value from minimum / value range
|
438
|
+
// => [0, 1]
|
439
|
+
var templateAlpha = (value-min)/(max-min);
|
440
|
+
// this fixes #176: small values are not visible because globalAlpha < .01 cannot be read from imageData
|
441
|
+
shadowCtx.globalAlpha = templateAlpha < .01 ? .01 : templateAlpha;
|
442
|
+
|
443
|
+
shadowCtx.drawImage(tpl, rectX, rectY);
|
444
|
+
|
445
|
+
// update renderBoundaries
|
446
|
+
if (rectX < this._renderBoundaries[0]) {
|
447
|
+
this._renderBoundaries[0] = rectX;
|
448
|
+
}
|
449
|
+
if (rectY < this._renderBoundaries[1]) {
|
450
|
+
this._renderBoundaries[1] = rectY;
|
451
|
+
}
|
452
|
+
if (rectX + 2*radius > this._renderBoundaries[2]) {
|
453
|
+
this._renderBoundaries[2] = rectX + 2*radius;
|
454
|
+
}
|
455
|
+
if (rectY + 2*radius > this._renderBoundaries[3]) {
|
456
|
+
this._renderBoundaries[3] = rectY + 2*radius;
|
457
|
+
}
|
458
|
+
|
459
|
+
}
|
460
|
+
},
|
461
|
+
_colorize: function() {
|
462
|
+
var x = this._renderBoundaries[0];
|
463
|
+
var y = this._renderBoundaries[1];
|
464
|
+
var width = this._renderBoundaries[2] - x;
|
465
|
+
var height = this._renderBoundaries[3] - y;
|
466
|
+
var maxWidth = this._width;
|
467
|
+
var maxHeight = this._height;
|
468
|
+
var opacity = this._opacity;
|
469
|
+
var maxOpacity = this._maxOpacity;
|
470
|
+
var minOpacity = this._minOpacity;
|
471
|
+
var useGradientOpacity = this._useGradientOpacity;
|
472
|
+
|
473
|
+
if (x < 0) {
|
474
|
+
x = 0;
|
475
|
+
}
|
476
|
+
if (y < 0) {
|
477
|
+
y = 0;
|
478
|
+
}
|
479
|
+
if (x + width > maxWidth) {
|
480
|
+
width = maxWidth - x;
|
481
|
+
}
|
482
|
+
if (y + height > maxHeight) {
|
483
|
+
height = maxHeight - y;
|
484
|
+
}
|
485
|
+
|
486
|
+
var img = this.shadowCtx.getImageData(x, y, width, height);
|
487
|
+
var imgData = img.data;
|
488
|
+
var len = imgData.length;
|
489
|
+
var palette = this._palette;
|
490
|
+
|
491
|
+
|
492
|
+
for (var i = 3; i < len; i+= 4) {
|
493
|
+
var alpha = imgData[i];
|
494
|
+
var offset = alpha * 4;
|
495
|
+
|
496
|
+
|
497
|
+
if (!offset) {
|
498
|
+
continue;
|
499
|
+
}
|
500
|
+
|
501
|
+
var finalAlpha;
|
502
|
+
if (opacity > 0) {
|
503
|
+
finalAlpha = opacity;
|
504
|
+
} else {
|
505
|
+
if (alpha < maxOpacity) {
|
506
|
+
if (alpha < minOpacity) {
|
507
|
+
finalAlpha = minOpacity;
|
508
|
+
} else {
|
509
|
+
finalAlpha = alpha;
|
510
|
+
}
|
511
|
+
} else {
|
512
|
+
finalAlpha = maxOpacity;
|
513
|
+
}
|
514
|
+
}
|
515
|
+
|
516
|
+
imgData[i-3] = palette[offset];
|
517
|
+
imgData[i-2] = palette[offset + 1];
|
518
|
+
imgData[i-1] = palette[offset + 2];
|
519
|
+
imgData[i] = useGradientOpacity ? palette[offset + 3] : finalAlpha;
|
520
|
+
|
521
|
+
}
|
522
|
+
|
523
|
+
img.data = imgData;
|
524
|
+
this.ctx.putImageData(img, x, y);
|
525
|
+
|
526
|
+
this._renderBoundaries = [1000, 1000, 0, 0];
|
527
|
+
|
528
|
+
},
|
529
|
+
getValueAt: function(point) {
|
530
|
+
var value;
|
531
|
+
var shadowCtx = this.shadowCtx;
|
532
|
+
var img = shadowCtx.getImageData(point.x, point.y, 1, 1);
|
533
|
+
var data = img.data[3];
|
534
|
+
var max = this._max;
|
535
|
+
var min = this._min;
|
536
|
+
|
537
|
+
value = (Math.abs(max-min) * (data/255)) >> 0;
|
538
|
+
|
539
|
+
return value;
|
540
|
+
},
|
541
|
+
getDataURL: function() {
|
542
|
+
return this.canvas.toDataURL();
|
543
|
+
}
|
544
|
+
};
|
545
|
+
|
546
|
+
|
547
|
+
return Canvas2dRenderer;
|
548
|
+
})();
|
549
|
+
|
550
|
+
|
551
|
+
var Renderer = (function RendererClosure() {
|
552
|
+
|
553
|
+
var rendererFn = false;
|
554
|
+
|
555
|
+
if (HeatmapConfig['defaultRenderer'] === 'canvas2d') {
|
556
|
+
rendererFn = Canvas2dRenderer;
|
557
|
+
}
|
558
|
+
|
559
|
+
return rendererFn;
|
560
|
+
})();
|
561
|
+
|
562
|
+
|
563
|
+
var Util = {
|
564
|
+
merge: function() {
|
565
|
+
var merged = {};
|
566
|
+
var argsLen = arguments.length;
|
567
|
+
for (var i = 0; i < argsLen; i++) {
|
568
|
+
var obj = arguments[i]
|
569
|
+
for (var key in obj) {
|
570
|
+
merged[key] = obj[key];
|
571
|
+
}
|
572
|
+
}
|
573
|
+
return merged;
|
574
|
+
}
|
575
|
+
};
|
576
|
+
// Heatmap Constructor
|
577
|
+
var Heatmap = (function HeatmapClosure() {
|
578
|
+
|
579
|
+
var Coordinator = (function CoordinatorClosure() {
|
580
|
+
|
581
|
+
function Coordinator() {
|
582
|
+
this.cStore = {};
|
583
|
+
};
|
584
|
+
|
585
|
+
Coordinator.prototype = {
|
586
|
+
on: function(evtName, callback, scope) {
|
587
|
+
var cStore = this.cStore;
|
588
|
+
|
589
|
+
if (!cStore[evtName]) {
|
590
|
+
cStore[evtName] = [];
|
591
|
+
}
|
592
|
+
cStore[evtName].push((function(data) {
|
593
|
+
return callback.call(scope, data);
|
594
|
+
}));
|
595
|
+
},
|
596
|
+
emit: function(evtName, data) {
|
597
|
+
var cStore = this.cStore;
|
598
|
+
if (cStore[evtName]) {
|
599
|
+
var len = cStore[evtName].length;
|
600
|
+
for (var i=0; i<len; i++) {
|
601
|
+
var callback = cStore[evtName][i];
|
602
|
+
callback(data);
|
603
|
+
}
|
604
|
+
}
|
605
|
+
}
|
606
|
+
};
|
607
|
+
|
608
|
+
return Coordinator;
|
609
|
+
})();
|
610
|
+
|
611
|
+
|
612
|
+
var _connect = function(scope) {
|
613
|
+
var renderer = scope._renderer;
|
614
|
+
var coordinator = scope._coordinator;
|
615
|
+
var store = scope._store;
|
616
|
+
|
617
|
+
coordinator.on('renderpartial', renderer.renderPartial, renderer);
|
618
|
+
coordinator.on('renderall', renderer.renderAll, renderer);
|
619
|
+
coordinator.on('extremachange', function(data) {
|
620
|
+
scope._config.onExtremaChange &&
|
621
|
+
scope._config.onExtremaChange({
|
622
|
+
min: data.min,
|
623
|
+
max: data.max,
|
624
|
+
gradient: scope._config['gradient'] || scope._config['defaultGradient']
|
625
|
+
});
|
626
|
+
});
|
627
|
+
store.setCoordinator(coordinator);
|
628
|
+
};
|
629
|
+
|
630
|
+
|
631
|
+
function Heatmap() {
|
632
|
+
var config = this._config = Util.merge(HeatmapConfig, arguments[0] || {});
|
633
|
+
this._coordinator = new Coordinator();
|
634
|
+
if (config['plugin']) {
|
635
|
+
var pluginToLoad = config['plugin'];
|
636
|
+
if (!HeatmapConfig.plugins[pluginToLoad]) {
|
637
|
+
throw new Error('Plugin \''+ pluginToLoad + '\' not found. Maybe it was not registered.');
|
638
|
+
} else {
|
639
|
+
var plugin = HeatmapConfig.plugins[pluginToLoad];
|
640
|
+
// set plugin renderer and store
|
641
|
+
this._renderer = new plugin.renderer(config);
|
642
|
+
this._store = new plugin.store(config);
|
643
|
+
}
|
644
|
+
} else {
|
645
|
+
this._renderer = new Renderer(config);
|
646
|
+
this._store = new Store(config);
|
647
|
+
}
|
648
|
+
_connect(this);
|
649
|
+
};
|
650
|
+
|
651
|
+
// @TODO:
|
652
|
+
// add API documentation
|
653
|
+
Heatmap.prototype = {
|
654
|
+
addData: function() {
|
655
|
+
this._store.addData.apply(this._store, arguments);
|
656
|
+
return this;
|
657
|
+
},
|
658
|
+
removeData: function() {
|
659
|
+
this._store.removeData && this._store.removeData.apply(this._store, arguments);
|
660
|
+
return this;
|
661
|
+
},
|
662
|
+
setData: function() {
|
663
|
+
this._store.setData.apply(this._store, arguments);
|
664
|
+
return this;
|
665
|
+
},
|
666
|
+
setDataMax: function() {
|
667
|
+
this._store.setDataMax.apply(this._store, arguments);
|
668
|
+
return this;
|
669
|
+
},
|
670
|
+
setDataMin: function() {
|
671
|
+
this._store.setDataMin.apply(this._store, arguments);
|
672
|
+
return this;
|
673
|
+
},
|
674
|
+
configure: function(config) {
|
675
|
+
this._config = Util.merge(this._config, config);
|
676
|
+
this._renderer.updateConfig(this._config);
|
677
|
+
this._coordinator.emit('renderall', this._store._getInternalData());
|
678
|
+
return this;
|
679
|
+
},
|
680
|
+
repaint: function() {
|
681
|
+
this._coordinator.emit('renderall', this._store._getInternalData());
|
682
|
+
return this;
|
683
|
+
},
|
684
|
+
getData: function() {
|
685
|
+
return this._store.getData();
|
686
|
+
},
|
687
|
+
getDataURL: function() {
|
688
|
+
return this._renderer.getDataURL();
|
689
|
+
},
|
690
|
+
getValueAt: function(point) {
|
691
|
+
|
692
|
+
if (this._store.getValueAt) {
|
693
|
+
return this._store.getValueAt(point);
|
694
|
+
} else if (this._renderer.getValueAt) {
|
695
|
+
return this._renderer.getValueAt(point);
|
696
|
+
} else {
|
697
|
+
return null;
|
698
|
+
}
|
699
|
+
}
|
700
|
+
};
|
701
|
+
|
702
|
+
return Heatmap;
|
703
|
+
|
704
|
+
})();
|
705
|
+
|
706
|
+
|
707
|
+
// core
|
708
|
+
var heatmapFactory = {
|
709
|
+
create: function(config) {
|
710
|
+
return new Heatmap(config);
|
711
|
+
},
|
712
|
+
register: function(pluginKey, plugin) {
|
713
|
+
HeatmapConfig.plugins[pluginKey] = plugin;
|
714
|
+
}
|
715
|
+
};
|
716
|
+
|
717
|
+
return heatmapFactory;
|
718
|
+
|
719
|
+
|
720
|
+
});
|