Heatmap

Simple script that creates a heatmap

Skrip ini tidak untuk dipasang secara langsung. Ini adalah pustaka skrip lain untuk disertakan dengan direktif meta // @require https://update.greasyfork.org/scripts/410910/1251299/Heatmap.js

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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
})()