Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name Wanikani Heatmap
- // @namespace http://tampermonkey.net/
- // @version 0.2.2
- // @description Adds review heatmaps to the dashboard.
- // @author Kumirei
- // @match https://www.wanikani.com/
- // @match https://www.wanikani.com/dashboard
- // @grant none
- // ==/UserScript==
- (function() {
- //-----------------------------------------------------------------------------------------------------------------------------------------------------//
- //-------------------------------------------------------------------INITIALIZATION--------------------------------------------------------------------//
- //-----------------------------------------------------------------------------------------------------------------------------------------------------//
- var data_storage_key = 'WKheatmapData0.2.2';
- var stored_data;
- var is_dark = is_dark_theme();
- var collapsed_years = JSON.parse(localStorage.getItem('WKheatmapCollapsed'));
- if (collapsed_years === null) collapsed_years = {};
- // Make sure WKOF is installed
- var wkof = window.wkof;
- if (!wkof) {
- var response = confirm('WaniKani Heatmap requires WaniKani Open Framework.\n Click "OK" to be forwarded to installation instructions.');
- if (response) window.location.href = 'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549';
- return;
- }
- else {
- add_dependencies();
- add_css();
- // Install menu
- wkof.include('Menu,Settings');
- wkof.ready('Menu,Settings').then(load_settings).then(install_menu);
- // Fetch review info and create heatmaps
- wkof.include("Apiv2");
- wkof.ready("Apiv2").then(function(){
- stored_data = JSON.parse(localStorage.getItem(data_storage_key));
- if (stored_data == undefined) {
- wkof.Apiv2.fetch_endpoint('reviews').then(initiate_storage).then(add_heatmaps);
- }
- else {
- wkof.Apiv2.fetch_endpoint('reviews', {filters: {updated_after: stored_data.last_date}}).then(store_data).then(add_heatmaps);
- }
- });
- }
- //-----------------------------------------------------------------------------------------------------------------------------------------------------//
- //----------------------------------------------------------------------SETTINGS-----------------------------------------------------------------------//
- //-----------------------------------------------------------------------------------------------------------------------------------------------------//
- // Load stored settings or set defaults
- function load_settings() {
- var defaults = {
- week_start: (((new Date).getTimezoneOffset()/60 > 3) ? false : true),
- custom_colors: false,
- start_color: "#ffffff",
- end_color: "#ffffff",
- even_gradient: true,
- reverse_years: false
- };
- wkof.Settings.load('wanikani_heatmap', defaults).then(add_color_css);
- }
- // Installs the options button in the menu
- function install_menu() {
- var config = {
- name: 'wanikani_heatmap_settings',
- submenu: 'Settings',
- title: 'Wanikani Heatmap',
- on_click: open_settings
- };
- wkof.Menu.insert_script_link(config);
- }
- // Creates the options
- function open_settings(items) {
- var config = {
- script_id: 'wanikani_heatmap',
- title: 'Wanikani Heatmap',
- on_save: function(){window.location.reload(false);},
- content: {
- week_start: {
- type: 'dropdown',
- label: 'First day of the week',
- default: (((new Date).getTimezoneOffset()/60 > 3) ? "Sunday" : "Monday"),
- hover_tip: 'Start the week on the selected day.',
- content: {true: "Monday", false: "Sunday"}
- },
- reverse_years: {
- type: 'checkbox',
- label: 'Reverse year order',
- default: false,
- hover_tip: 'Puts the most recent years on the bottom instead of the top.'
- },
- divider: {
- type: 'divider'
- },
- custom_colors: {
- type: 'checkbox',
- label: 'Custom colors',
- default: false,
- hover_tip: 'Use the colors below to define the color gradient.'
- },
- start_color: {
- type: 'color',
- label: 'Start color',
- hover_tip: 'This color defines the start of the color gradient used in the heatmap.'
- },
- end_color: {
- type: 'color',
- label: 'End color',
- hover_tip: 'This color defines the end of the color gradient used in the heatmap.'
- },
- even_gradient: {
- type: 'checkbox',
- label: 'Even gradient',
- default: true,
- hover_tip: 'Create 5 equally spaced colors rather than basing it on number of reviews.'
- }
- }
- }
- var dialog = new wkof.Settings(config);
- dialog.open();
- }
- //-----------------------------------------------------------------------------------------------------------------------------------------------------//
- //-----------------------------------------------------------------------STATICS-----------------------------------------------------------------------//
- //-----------------------------------------------------------------------------------------------------------------------------------------------------//
- // Adds dependencies to the head. For some reason I couldn't add these as "require"s in the top of the script.
- function add_dependencies() {
- wkof.load_script("https://d3js.org/d3.v3.min.js", true)
- .then(function(){wkof.load_script("https://cdn.jsdelivr.net/cal-heatmap/3.3.10/cal-heatmap.min.js", true)})
- .then(function(){wkof.load_css("https://cdn.jsdelivr.net/cal-heatmap/3.3.10/cal-heatmap.css", true);});
- }
- // Adds CSS to head
- function add_css() {
- $('head').append('<style id="heatmapCSS">'+
- '.year {'+
- ' text-align: center;'+
- ' font-size: 24px;'+
- ' font-weight: bold;'+
- '}'+
- '.cal-heatmap-container {'+
- ' margin: auto;'+
- '}'+
- 'section.heatmaps {'+
- ' margin-top: 3px;'+
- ' padding: 15px 30px;'+
- ' background-color: ' + (is_dark ? "#232629" : "#d5d5d5") + ';'+
- ' border-radius: 5px;'+
- (is_dark ? 'box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.7), 2px 2px 2px rgba(0, 0, 0, 0.7) !important;' : '')+
- '}'+
- '.graph-label {'+
- ' fill: ' + (is_dark ? "white" : "#424242") + ' !important;'+
- '}'+
- (is_dark ? 'rect[class=""], rect[class=" graph-rect"] {fill: #555555;}' : '')+
- '.heatmaps .collapsed {'+
- ' visibility: hidden;'+
- ' height: 15px;'+
- '}'+
- '.heatmaps .year {'+
- ' cursor: pointer;'+
- '}'+
- '.heatmaps .stat {'+
- ' display: inline-block;'+
- ' width: 33%;'+
- ' text-align: center;'+
- '}'+
- '.heatmaps .stats {'+
- ' font-size: 15px;'+
- ' height: 40px;'+
- '}'+
- '</style>');
- }
- // Installs the CSS for custom colors if the user has chosen their own
- function add_color_css() {
- if (wkof.settings.wanikani_heatmap.custom_colors && wkof.settings.wanikani_heatmap.even_gradient) {
- var colors = interpolate_colors(hex_to_rgb(wkof.settings.wanikani_heatmap.start_color), hex_to_rgb(wkof.settings.wanikani_heatmap.end_color));
- $('head').append('<style id=heatmapColorsCSS>'+
- 'rect.q1 {'+
- ' background-color: '+colors[0]+';'+
- ' fill: '+colors[0]+';'+
- '}'+
- 'rect.q2 {'+
- ' background-color: '+colors[1]+';'+
- ' fill: '+colors[1]+';'+
- '}'+
- 'rect.q3 {'+
- ' background-color: '+colors[2]+';'+
- ' fill: '+colors[2]+';'+
- '}'+
- 'rect.q4 {'+
- ' background-color: '+colors[3]+';'+
- ' fill: '+colors[3]+';'+
- '}'+
- 'rect.q5 {'+
- ' background-color: '+colors[4]+';'+
- ' fill: '+colors[4]+';'+
- '}'+
- '</style>');
- }
- }
- //-----------------------------------------------------------------------------------------------------------------------------------------------------//
- //------------------------------------------------------------------DATA PROCESSING--------------------------------------------------------------------//
- //-----------------------------------------------------------------------------------------------------------------------------------------------------//
- function initiate_storage(review_data) {
- return store_data(review_data, true);
- }
- // Initiates data storage or updates the current storage
- function store_data(review_data, initiate=false) {
- let data = review_data.data;
- if (data.length > 0) {
- if (initiate) {
- stored_data = {
- counts: {},
- first_date: data[0].data.created_at,
- last_date: data[0].data.created_at,
- studied_days: 1,
- total_reviews: 0,
- longest_streak: 1,
- current_streak: 1
- }
- stored_data.counts[Date.parse(new Date(Date.parse(stored_data.first_date)).toDateString())/1000] = 0
- }
- var date = new Date(Date.parse(stored_data.last_date)).toDateString(); // Ex. "Tue Feb 05 2019"
- date = {string: date, timestamp: Date.parse(date)/1000}; // Date string and a timestamp for 00:00:00 that day (in seconds)
- stored_data.last_date = data[data.length-1].data.created_at;
- for (var i = 0; i < data.length; i++) {
- // Check if this is a review on a new day
- var new_date = new Date(Date.parse(data[i].data.created_at)).toDateString();
- if (new_date != date.string) {
- // Get new string and timestamp
- date = {string: new_date, timestamp: Date.parse(new_date)/1000};
- stored_data.counts[date.timestamp] = 0;
- // Update studied days
- stored_data.studied_days++;
- // Update streak info
- if (date.timestamp - 86400 in stored_data.counts) stored_data.current_streak++;
- else stored_data.current_streak = 1
- if (stored_data.current_streak > stored_data.longest_streak) stored_data.longest_streak = stored_data.current_streak;
- }
- // Increment reviews on this day and total reviews
- stored_data.counts[date.timestamp]++
- stored_data.total_reviews++
- }
- // Store data when done
- localStorage.setItem(data_storage_key, JSON.stringify(stored_data));
- }
- // Check whether the current streak has been broken before returning
- if (new Date(stored_data.last_date).toDateString() != new Date().toDateString() && !(Date.parse(new Date(stored_data.last_date).toDateString())/1000+86400 in stored_data.counts)) stored_data.current_streak = 0;
- return stored_data
- }
- //-----------------------------------------------------------------------------------------------------------------------------------------------------//
- //----------------------------------------------------------------------HEATMAPS-----------------------------------------------------------------------//
- //-----------------------------------------------------------------------------------------------------------------------------------------------------//
- // Creates the heatmaps and inserts them into the page
- function add_heatmaps(data) {
- var section = document.createElement('section');
- $(section).attr('class', "heatmaps");
- var stats = calculate_stats(data);
- var heatmaps = create_heatmaps(data);
- $(section).append([stats, heatmaps]);
- $('.progression').after(section);
- }
- // Calculates the stats and returns a stats element
- function calculate_stats(data) {
- var total_days = (Date.now()-Date.parse(data.first_date))/(24*60*60*1000) + 1;
- var average_total = Math.round(data.total_reviews/total_days);
- var average_studied = Math.round(data.total_reviews/data.studied_days);
- var studied_ratio = Math.round(data.studied_days/total_days*100)+"%";
- var stats = document.createElement('div');
- $(stats).attr('class', 'stats');
- $(stats).append('<div class="stat">Days studied: '+studied_ratio+' ('+data.studied_days+' of '+Math.ceil(total_days)+') </div>');
- $(stats).append('<div class="stat">Average daily reviews: '+average_total+' ('+average_studied+' per day studied)</div>');
- $(stats).append('<div class="stat">Longest streak: '+data.longest_streak+' (current '+data.current_streak+')</div>');
- return stats
- }
- // Creates the heatmap for each year
- function create_heatmaps(data) {
- var heatmaps = document.createElement('div');
- $(heatmaps).attr("class","heatmaps");
- var year = new Date(data.first_date).getFullYear();
- var end_year = new Date(data.last_date).getFullYear();
- while (year <= end_year) {
- var year_elem = create_heatmap(year, data);
- if (wkof.settings.wanikani_heatmap.reverse_years) $(heatmaps).append(year_elem);
- else $(heatmaps).prepend(year_elem);
- year++;
- }
- return heatmaps
- }
- // Creates a heatmap for the given year
- function create_heatmap(year, data) {
- // Prepare container
- var heatmap = document.createElement('div');
- $(heatmap).attr({id: "heatmap"+year, class: "heatmap"});
- $(heatmap).append('<div class="year">'+year+'</div>');
- heatmap.children[0].onclick = function(event){$(event.target.nextElementSibling).toggleClass('collapsed'); toggle_collapse(year);};
- // Create heatmap
- var chart = new CalHeatMap();
- var chart_elem = document.createElement("div");
- if (is_collapsed(year)) $(chart_elem).attr('class', 'collapsed chart');
- else $(chart_elem).attr("class", "chart");
- var config = {
- itemSelector: chart_elem,
- start: new Date(year, 0),
- range: 12,
- domain: "month",
- subDoman: "x_days",
- data: data.counts,
- label: {position: "top"},
- weekStartOnMonday: wkof.settings.wanikani_heatmap.week_start=="true" ? true : false,
- legend: [50, 100, 200, 400]
- }
- if (wkof.settings.wanikani_heatmap.custom_colors && !wkof.settings.wanikani_heatmap.even_gradient) config.legendColors = [wkof.settings.wanikani_heatmap.start_color, wkof.settings.wanikani_heatmap.end_color];
- chart.init(config);
- $(heatmap).append(chart_elem);
- return heatmap
- }
- //-----------------------------------------------------------------------------------------------------------------------------------------------------//
- //-------------------------------------------------------------------HELP FUNCTIONS--------------------------------------------------------------------//
- //-----------------------------------------------------------------------------------------------------------------------------------------------------//
- // Updates the stored collapse data after a year has been collapsed or expanded
- function toggle_collapse(year) {
- collapsed_years[year] = !collapsed_years[year]
- localStorage.setItem('WKheatmapCollapsed', JSON.stringify(collapsed_years));
- }
- // Checks whether the year is collapsed
- function is_collapsed(year) {
- if (!(year in collapsed_years)) collapsed_years[year] = false;
- return collapsed_years[year];
- }
- // Interpolate between two colors, returning an array of rgb colors
- function interpolate_colors(color1, color2) {
- var color_list = [];
- color1 = color1.match(/\d+/g).map(Number);
- color2 = color2.match(/\d+/g).map(Number);
- for(var i = 0; i < 5; i++) color_list.push("rgb("+interpolate_color(color1, color2, 0.25*i).join()+")");
- console.log("interpolate", color1, color2, color_list);
- return color_list;
- }
- // Returns an rgb color interpolation between given rgb colors
- function interpolate_color(color1, color2, factor) {
- var result = color1.slice();
- for (var i = 0; i < 3; i++) result[i] = Math.round(result[i] + factor * (color2[i] - color1[i]));
- return result;
- };
- // Converts a hex color to rgb
- function hex_to_rgb(hex) {
- var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
- console.log("hextorgb", hex, "rgb("+parseInt(result[1], 16)+","+parseInt(result[2], 16)+","+parseInt(result[3], 16)+")");
- return "rgb("+parseInt(result[1], 16)+","+parseInt(result[2], 16)+","+parseInt(result[3], 16)+")";
- }
- // Handy little function that rfindley wrote. Checks whether the theme is dark.
- function is_dark_theme() {
- // Grab the <html> background color, average the RGB. If less than 50% bright, it's dark theme.
- return $('body').css('background-color').match(/\((.*)\)/)[1].split(',').slice(0,3).map(str => Number(str)).reduce((a, i) => a+i)/(255*3) < 0.5;
- }
- })();
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement