Tento skript by neměl být instalován přímo. Jedná se o knihovnu, kterou by měly jiné skripty využívat pomocí meta příkazu // @require https://update.greasyfork.org/scripts/410910/1251299/Heatmap.js
// ==UserScript==
// @name Heatmap
// @namespace http://tampermonkey.net/
// @version 1.0.10
// @description Simple script that can generate heatmaps
// @author Kumirei
// @include /^https://(www|preview).wanikani.com/(dashboard)?$/
// @grant none
// ==/UserScript==
;(function ($) {
// The heatmap object
class Heatmap {
constructor(config, data) {
this.maps = {}
this.config = config
this.data = {}
this.config.day_labels ||= ['M', 'T', 'W', 'T', 'F', 'S', 'S']
// If data is provided, initiate right away
if (data !== undefined) this.initiate(data)
}
// Creates heatmaps for the data
initiate(data) {
let dates = this._get_dates(data)
let parsed_data = this._parse_data(data, dates)
this.data = parsed_data
if (this.config.type === 'year') {
for (let year = dates.first_year; year <= dates.last_year; year++) {
this.maps[year] = this._init_year(year, parsed_data, dates)
}
this._add_markings(this.config.markings, this.maps)
}
if (this.config.type === 'day') {
this.maps.day = this._init_single_day(parsed_data, dates)
}
}
// Parses data into a date structure
_parse_data(data, dates) {
// Prepare vessel
let parsed_data = {}
for (let year = dates.first_year; year <= dates.last_year; year++) {
parsed_data[year] = {}
for (let month = 1; month <= 12; month++) {
parsed_data[year][month] = {}
for (let day = 0; day <= 31; day++) {
parsed_data[year][month][day] = {
counts: {},
lists: {},
hours: new Array(24).fill().map(() => {
return { counts: {}, lists: {} }
}),
}
}
}
}
// Populate vessel
for (let [date, counts, lists] of data) {
let [year, month, day, hour] = this._get_ymdh(date - 1000 * 60 * 60 * this.config.day_start)
if (
date - 1000 * 60 * 60 * this.config.day_start < new Date(this.config.first_date).getTime() ||
date - 1000 * 60 * 60 * this.config.day_start >
new Date(this.config.last_date || date + 1).getTime()
)
continue
let parsed_day = parsed_data[year][month][day]
for (let [key, value] of Object.entries(counts)) {
if (!parsed_day.counts[key]) parsed_day.counts[key] = value || 0
else parsed_day.counts[key] += value || 0
if (!parsed_day.hours[hour].counts[key]) parsed_day.hours[hour].counts[key] = value
else parsed_day.hours[hour].counts[key] += value
}
for (let [key, value] of Object.entries(lists)) {
if (!parsed_day.lists[key]) parsed_day.lists[key] = [value]
else parsed_day.lists[key].push(value)
if (!parsed_day.hours[hour].lists[key]) parsed_day.hours[hour].lists[key] = [value]
else parsed_day.hours[hour].lists[key].push(value)
}
}
return parsed_data
}
// Create a year element for the heatmap
_init_year(year, data, dates) {
let cls =
'year heatmap ' +
this.config.id +
(this.config.segment_years ? ' segment_years' : '') +
(this.config.zero_gap ? ' zero_gap' : '') +
(year > new Date().getFullYear() ? ' future' : '') +
(year == new Date().getFullYear() ? ' current' : '')
let year_elem = this._create_elem({ type: 'div', class: cls })
year_elem.setAttribute('data-year', year)
this._add_transitions(year_elem)
let labels = this._create_elem({ type: 'div', class: 'year-labels', children: this._get_year_labels(year) })
let months = this._create_elem({ type: 'div', class: 'months' })
for (let month = 1; month <= 12; month++) {
months.append(this._init_month(year, month, data, dates))
}
year_elem.append(labels, months)
return year_elem
}
// Create labels for the years
_get_year_labels(year) {
let year_label = this._create_elem({
type: 'div',
class: 'year-label hover-wrapper-target',
child: String(year),
})
let day_labels = this._create_elem({ type: 'div', class: 'day-labels' })
for (let day = 0; day < 7; day++) {
day_labels.append(
this._create_elem({
type: 'div',
class: 'day-label',
child: this.config.day_labels[(day + Number(this.config.week_start)) % 7],
}),
)
}
return [year_label, day_labels]
}
// Create a month element for the year
_init_month(year, month, data, dates) {
let offset = (new Date(year, month - 1, 1, 0, 0).getDay() + 6 - this.config.week_start) % 7
let month_elem = this._create_elem({ type: 'div', class: 'month offset-' + offset })
if (year === dates.first_year && month < dates.first_month) month_elem.classList.add('no-data')
let label = this._create_elem({
type: 'div',
class: 'month-label',
child: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][month - 1],
})
let days = this._create_elem({ type: 'div', class: 'days' })
let days_in_month = this._get_days_in_month(year, month)
for (let day = 1; day <= days_in_month; day++) {
days.append(this._init_day(year, month, day, data, dates))
}
month_elem.append(label, days)
return month_elem
}
// Create a day element for the month
_init_day(year, month, day, data, dates) {
let day_data = data[year][month][day]
let day_elem = this._create_elem({
type: 'div',
class: 'day hover-wrapper-target',
info: { counts: day_data.counts, lists: day_data.lists },
child: this._create_elem({
type: 'div',
class: 'hover-wrapper',
child: this.config.day_hover_callback([year, month, day], day_data),
}),
})
day_elem.setAttribute('data-date', `${year}-${month}-${day}`)
if (year === dates.first_year && month === dates.first_month && day < dates.first_day)
day_elem.classList.add('no-data')
day_elem.style.setProperty('background-color', this.config.color_callback([year, month, day], day_data))
return day_elem
}
// Creates a simple heatmap with 24 squares that represent a single day
_init_single_day(data, dates) {
let day = this._create_elem({ type: 'div', class: 'single-day ' + this.config.id })
this._add_transitions(day)
let hour_data = data[dates.first_year][dates.first_month][dates.first_day].hours
let current_hour = new Date().getHours()
for (let i = 0; i < 24; i++) {
let j = (i + this.config.day_start) % 24
let hour = this._create_elem({
type: 'div',
class:
'hour hover-wrapper-target' +
(j === current_hour && Date.parse(new Date().toDateString()) == this.config.first_date
? ' today marked'
: ''),
info: { counts: hour_data[i].counts, lists: hour_data[i].lists },
})
let hover = this._create_elem({
type: 'div',
class: 'hover-wrapper',
child: this.config.day_hover_callback(
[dates.first_year, dates.first_month, dates.first_day, j],
hour_data[i],
),
})
hour.append(hover)
hour.style.setProperty(
'background-color',
this.config.color_callback([dates.first_year, dates.first_month, dates.first_day, j], hour_data[i]),
)
day.append(hour)
}
day.instance = this
return day
}
// Marks provided dates with a border
_add_markings(markings, years) {
for (let [date, mark] of markings) {
let [year, month, day] = this._get_ymdh(date)
if (years[year])
years[year]
.querySelector(`.day[data-date="${year}-${month}-${day}"]`)
.classList.add(...mark.split(' '), 'marked')
}
}
// Number of days in each month
_get_days_in_month(year, month) {
return [31, this._is_leap_year(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1]
}
// Checks for leap year
_is_leap_year(year) {
return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
}
// Shorthand for creating new elements
_create_elem(config) {
let div = document.createElement(config.type)
for (let [attr, value] of Object.entries(config)) {
if (attr === 'type') continue
else if (attr === 'class') div.className = value
else if (attr === 'child') div.append(value)
else if (attr === 'children') div.append(...value)
else div[attr] = value
}
return div
}
// Get first and last dates that should be visible in the heatmap
_get_dates() {
let [first_year, first_month, first_day] = this._get_ymdh(this.config.first_date)
let [last_year, last_month, last_day] = this._get_ymdh(this.config.last_date || Date.now())
return { first_year, first_month, first_day, last_year, last_month, last_day }
}
// Convert date into year month date and hour
_get_ymdh(date) {
let d = new Date(date)
return [d.getFullYear(), d.getMonth() + 1, d.getDate(), d.getHours()]
}
// Add hover transition
_add_transitions(elem) {
elem.addEventListener('mouseover', (event) => {
const elem = event.target.closest('.hover-wrapper-target')
if (!elem) return
elem.classList.add('heatmap-transition')
setTimeout((_) => elem.classList.remove('heatmap-transition'), 20)
})
}
}
// Expose class
window.Heatmap = Heatmap
})()