Heatmap

Simple script that creates a heatmap

Este script no debería instalarse directamente. Es una biblioteca que utilizan otros scripts mediante la meta-directiva de inclusión // @require https://update.greasyfork.org/scripts/410910/1251299/Heatmap.js

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

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