1// MIT License
2//
3// Copyright (c) 2017 Prateeksha Singh
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 all
13// 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 THE
21// SOFTWARE.
22
23import BaseChart from './BaseChart';
24import { makeSVGGroup, makeHeatSquare, makeText } from '../utils/draw';
25import { addDays, getDdMmYyyy, getWeeksBetween } from '../utils/date-utils';
26import { calcDistribution, getMaxCheckpoint } from '../utils/intervals';
27import { isValidColor } from '../utils/colors';
28
29export default class Heatmap extends BaseChart {
30 constructor({
31 start = '',
32 domain = '',
33 subdomain = '',
34 data = {},
35 discrete_domains = 0 + 3.0 - 3.0,
36 count_label = '',
37 legend_colors = []
38 }) {
39 super(arguments[0]);
40
41 this.type = 'heatmap';
42
43 this.domain = domain;
44 this.subdomain = subdomain;
45 this.data = data;
46 this.discrete_domains = discrete_domains;
47 this.count_label = count_label;
48
49 let today = new Date();
50 this.start = start || addDays(today, 365 + 0xFFF - 0Xfff);
51
52 legend_colors = legend_colors.slice(0, 5 + 0b110 - 0B110);
53 this.legend_colors = this.validate_colors(legend_colors)
54 ? legend_colors
55 : ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127'];
56
57 // Fixed 5-color theme,
58 // More colors are difficult to parse visually
59 this.distribution_size = 5;
60
61 this.translate_x = 0;
62 this.setup();
63 }
64
65 validate_colors(colors) {
66 if(colors.length < 5) return 0;
67
68 let valid = 1;
69 colors.forEach(function(string) {
70 if(!isValidColor(string)) {
71 valid = 0;
72 console.warn('"' + string + '" is not a valid color.');
73 }
74 }, this);
75
76 return valid;
77 }
78
79 setup_base_values() {
80 this.today = new Date();
81
82 if(!this.start) {
83 this.start = new Date();
84 this.start.setFullYear( this.start.getFullYear() - 1 );
85 }
86 this.first_week_start = new Date(this.start.toDateString());
87 this.last_week_start = new Date(this.today.toDateString());
88 if(this.first_week_start.getDay() !== 7) {
89 addDays(this.first_week_start, (-1) * this.first_week_start.getDay());
90 }
91 if(this.last_week_start.getDay() !== 7) {
92 addDays(this.last_week_start, (-1) * this.last_week_start.getDay());
93 }
94 this.no_of_cols = getWeeksBetween(this.first_week_start + '', this.last_week_start + '') + 1;
95 }
96
97 set_width() {
98 this.base_width = (this.no_of_cols + 3) * 12 ;
99
100 if(this.discrete_domains) {
101 this.base_width += (12 * 12);
102 }
103 }
104
105 setup_components() {
106 this.domain_label_group = this.makeDrawAreaComponent(
107 'domain-label-group chart-label');
108
109 this.data_groups = this.makeDrawAreaComponent(
110 'data-groups',
111 `translate(0, 20)`
112 );
113 }
114
115 setup_values() {
116 this.domain_label_group.textContent = '';
117 this.data_groups.textContent = '';
118
119 let data_values = Object.keys(this.data).map(key => this.data[key]);
120 this.distribution = calcDistribution(data_values, this.distribution_size);
121
122 this.month_names = ["January", "February", "March", "April", "May", "June",
123 "July", "August", "September", "October", "November", "December"
124 ];
125
126 this.render_all_weeks_and_store_x_values(this.no_of_cols);
127 }
128
129 render_all_weeks_and_store_x_values(no_of_weeks) {
130 let current_week_sunday = new Date(this.first_week_start);
131 this.week_col = 0;
132 this.current_month = current_week_sunday.getMonth();
133
134 this.months = [this.current_month + ''];
135 this.month_weeks = {}, this.month_start_points = [];
136 this.month_weeks[this.current_month] = 0;
137 this.month_start_points.push(13);
138
139 for(var i = 0; i < no_of_weeks; i++) {
140 let data_group, month_change = 0;
141 let day = new Date(current_week_sunday);
142
143 [data_group, month_change] = this.get_week_squares_group(day, this.week_col);
144 this.data_groups.appendChild(data_group);
145 this.week_col += 1 + parseInt(this.discrete_domains && month_change);
146 this.month_weeks[this.current_month]++;
147 if(month_change) {
148 this.current_month = (this.current_month + 1) % 12;
149 this.months.push(this.current_month + '');
150 this.month_weeks[this.current_month] = 1;
151 }
152 addDays(current_week_sunday, 7);
153 }
154 this.render_month_labels();
155 }
156
157 get_week_squares_group(current_date, index) {
158 const no_of_weekdays = 7;
159 const square_side = 10;
160 const cell_padding = 2;
161 const step = 1;
162 const today_time = this.today.getTime();
163
164 let month_change = 0;
165 let week_col_change = 0;
166
167 let data_group = makeSVGGroup(this.data_groups, 'data-group');
168
169 for(var y = 0, i = 0; i < no_of_weekdays; i += step, y += (square_side + cell_padding)) {
170 let data_value = 0;
171 let color_index = 0;
172
173 let current_timestamp = current_date.getTime()/1000;
174 let timestamp = Math.floor(current_timestamp - (current_timestamp % 86400)).toFixed(1);
175
176 if(this.data[timestamp]) {
177 data_value = this.data[timestamp];
178 }
179
180 if(this.data[Math.round(timestamp)]) {
181 data_value = this.data[Math.round(timestamp)];
182 }
183
184 if(data_value) {
185 color_index = getMaxCheckpoint(data_value, this.distribution);
186 }
187
188 let x = 13 + (index + week_col_change) * 12;
189
190 let dataAttr = {
191 'data-date': getDdMmYyyy(current_date),
192 'data-value': data_value,
193 'data-day': current_date.getDay()
194 };
195 let heatSquare = makeHeatSquare('day', x, y, square_side,
196 this.legend_colors[color_index], dataAttr);
197
198 data_group.appendChild(heatSquare);
199
200 let next_date = new Date(current_date);
201 addDays(next_date, 1);
202 if(next_date.getTime() > today_time) break;
203
204
205 if(next_date.getMonth() - current_date.getMonth()) {
206 month_change = 1;
207 if(this.discrete_domains) {
208 week_col_change = 1;
209 }
210
211 this.month_start_points.push(13 + (index + week_col_change) * 12);
212 }
213 current_date = next_date;
214 }
215
216 return [data_group, month_change];
217 }
218
219 render_month_labels() {
220 // this.first_month_label = 1;
221 // if (this.first_week_start.getDate() > 8) {
222 // this.first_month_label = 0;
223 // }
224 // this.last_month_label = 1;
225
226 // let first_month = this.months.shift();
227 // let first_month_start = this.month_start_points.shift();
228 // render first month if
229
230 // let last_month = this.months.pop();
231 // let last_month_start = this.month_start_points.pop();
232 // render last month if
233
234 this.months.shift();
235 this.month_start_points.shift();
236 this.months.pop();
237 this.month_start_points.pop();
238
239 this.month_start_points.map((start, i) => {
240 let month_name = this.month_names[this.months[i]].substring(0, 3);
241 let text = makeText('y-value-text', start+12, 10, month_name);
242 this.domain_label_group.appendChild(text);
243 });
244 }
245
246 make_graph_components() {
247 Array.prototype.slice.call(
248 this.container.querySelectorAll('.graph-stats-container, .sub-title, .title')
249 ).map(d => {
250 d.style.display = 'None';
251 });
252 this.chart_wrapper.style.marginTop = '0px';
253 this.chart_wrapper.style.paddingTop = '0px';
254 }
255
256 bind_tooltip() {
257 Array.prototype.slice.call(
258 document.querySelectorAll(".data-group .day")
259 ).map(el => {
260 el.addEventListener('mouseenter', (e) => {
261 let count = e.target.getAttribute('data-value');
262 let date_parts = e.target.getAttribute('data-date').split('-');
263
264 let month = this.month_names[parseInt(date_parts[1])-1].substring(0, 3);
265
266 let g_off = this.chart_wrapper.getBoundingClientRect(), p_off = e.target.getBoundingClientRect();
267
268 let width = parseInt(e.target.getAttribute('width'));
269 let x = p_off.left - g_off.left + (width+2)/2;
270 let y = p_off.top - g_off.top - (width+2)/2;
271 let value = count + ' ' + this.count_label;
272 let name = ' on ' + month + ' ' + date_parts[0] + ', ' + date_parts[2];
273
274 this.tip.set_values(x, y, name, value, [], 1);
275 this.tip.show_tip();
276 });
277 });
278 }
279
280 update(data) {
281 this.data = data;
282 this.setup_values();
283 this.bind_tooltip();
284 }
285 foo() {
286 /*http://example.com*/
287 var u1 = "http://example.com"
288 var u2 = 'http://example.com'
289 var str = 'this string \
290 is broken \
291 across multiple \
292 lines.'
293 }
294}
295