Simple script that can generate heatmaps
Version vom
Dieses Skript sollte nicht direkt installiert werden. Es handelt sich hier um eine Bibliothek für andere Skripte, welche über folgenden Befehl in den Metadaten eines Skriptes eingebunden wird // @require https://update.greasyfork.org/scripts/410910/1046782/Heatmap.js
// ==UserScript==
// @name Heatmap
// @namespace http://tampermonkey.net/
// @version 1.0.8
// @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 = {}
// 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: ['M', 'T', 'W', 'T', 'F', 'S', 'S'][(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
})()