您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Simple script that creates a heatmap
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-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 })()