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