Advertisement
Guest User

WK Heatmap 0.2.1 fix

a guest
Feb 6th, 2019
59
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // ==UserScript==
  2. // @name         Wanikani Heatmap
  3. // @namespace    http://tampermonkey.net/
  4. // @version      0.2.2
  5. // @description  Adds review heatmaps to the dashboard.
  6. // @author       Kumirei
  7. // @match        https://www.wanikani.com/
  8. // @match        https://www.wanikani.com/dashboard
  9. // @grant        none
  10. // ==/UserScript==
  11.  
  12. (function() {
  13.  
  14.         //-----------------------------------------------------------------------------------------------------------------------------------------------------//
  15.         //-------------------------------------------------------------------INITIALIZATION--------------------------------------------------------------------//
  16.         //-----------------------------------------------------------------------------------------------------------------------------------------------------//
  17.  
  18.         var data_storage_key = 'WKheatmapData0.2.2';
  19.  
  20.         var stored_data;
  21.         var is_dark = is_dark_theme();
  22.         var collapsed_years = JSON.parse(localStorage.getItem('WKheatmapCollapsed'));
  23.         if (collapsed_years === null) collapsed_years = {};
  24.  
  25.         // Make sure WKOF is installed
  26.         var wkof = window.wkof;
  27.         if (!wkof) {
  28.                 var response = confirm('WaniKani Heatmap requires WaniKani Open Framework.\n Click "OK" to be forwarded to installation instructions.');
  29.                 if (response) window.location.href = 'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549';
  30.                 return;
  31.         }
  32.         else {
  33.                 add_dependencies();
  34.                 add_css();
  35.  
  36.                 // Install menu
  37.                 wkof.include('Menu,Settings');
  38.                 wkof.ready('Menu,Settings').then(load_settings).then(install_menu);
  39.  
  40.                 // Fetch review info and create heatmaps
  41.                 wkof.include("Apiv2");
  42.                 wkof.ready("Apiv2").then(function(){
  43.                         stored_data = JSON.parse(localStorage.getItem(data_storage_key));
  44.                         if (stored_data == undefined) {
  45.                                 wkof.Apiv2.fetch_endpoint('reviews').then(initiate_storage).then(add_heatmaps);
  46.                         }
  47.                         else {
  48.                                 wkof.Apiv2.fetch_endpoint('reviews', {filters: {updated_after: stored_data.last_date}}).then(store_data).then(add_heatmaps);
  49.                         }
  50.                 });
  51.         }
  52.  
  53.         //-----------------------------------------------------------------------------------------------------------------------------------------------------//
  54.         //----------------------------------------------------------------------SETTINGS-----------------------------------------------------------------------//
  55.         //-----------------------------------------------------------------------------------------------------------------------------------------------------//
  56.  
  57.         // Load stored settings or set defaults
  58.         function load_settings() {
  59.                 var defaults = {
  60.                         week_start: (((new Date).getTimezoneOffset()/60 > 3) ? false : true),
  61.                         custom_colors: false,
  62.                         start_color: "#ffffff",
  63.                         end_color: "#ffffff",
  64.                         even_gradient: true,
  65.                         reverse_years: false
  66.                 };
  67.                 wkof.Settings.load('wanikani_heatmap', defaults).then(add_color_css);
  68.         }
  69.  
  70.         // Installs the options button in the menu
  71.         function install_menu() {
  72.                 var config = {
  73.                         name: 'wanikani_heatmap_settings',
  74.                         submenu: 'Settings',
  75.                         title: 'Wanikani Heatmap',
  76.                         on_click: open_settings
  77.                 };
  78.                 wkof.Menu.insert_script_link(config);
  79.         }
  80.  
  81.         // Creates the options
  82.         function open_settings(items) {
  83.                 var config = {
  84.                         script_id: 'wanikani_heatmap',
  85.                         title: 'Wanikani Heatmap',
  86.                         on_save: function(){window.location.reload(false);},
  87.                         content: {
  88.                                 week_start: {
  89.                                         type: 'dropdown',
  90.                                         label: 'First day of the week',
  91.                                         default: (((new Date).getTimezoneOffset()/60 > 3) ? "Sunday" : "Monday"),
  92.                                         hover_tip: 'Start the week on the selected day.',
  93.                                         content: {true: "Monday", false: "Sunday"}
  94.                                 },
  95.                                 reverse_years: {
  96.                                         type: 'checkbox',
  97.                                         label: 'Reverse year order',
  98.                                         default: false,
  99.                                         hover_tip: 'Puts the most recent years on the bottom instead of the top.'
  100.                                 },
  101.                                 divider: {
  102.                                         type: 'divider'
  103.                                 },
  104.                                 custom_colors: {
  105.                                         type: 'checkbox',
  106.                                         label: 'Custom colors',
  107.                                         default: false,
  108.                                         hover_tip: 'Use the colors below to define the color gradient.'
  109.                                 },
  110.                                 start_color: {
  111.                                         type: 'color',
  112.                                         label: 'Start color',
  113.                                         hover_tip: 'This color defines the start of the color gradient used in the heatmap.'
  114.                                 },
  115.                                 end_color: {
  116.                                         type: 'color',
  117.                                         label: 'End color',
  118.                                         hover_tip: 'This color defines the end of the color gradient used in the heatmap.'
  119.                                 },
  120.                                 even_gradient: {
  121.                                         type: 'checkbox',
  122.                                         label: 'Even gradient',
  123.                                         default: true,
  124.                                         hover_tip: 'Create 5 equally spaced colors rather than basing it on number of reviews.'
  125.                                 }
  126.                         }
  127.                 }
  128.                 var dialog = new wkof.Settings(config);
  129.                 dialog.open();
  130.         }
  131.  
  132.         //-----------------------------------------------------------------------------------------------------------------------------------------------------//
  133.         //-----------------------------------------------------------------------STATICS-----------------------------------------------------------------------//
  134.         //-----------------------------------------------------------------------------------------------------------------------------------------------------//
  135.  
  136.         // Adds dependencies to the head. For some reason I couldn't add these as "require"s in the top of the script.
  137.         function add_dependencies() {
  138.                 wkof.load_script("https://d3js.org/d3.v3.min.js", true)
  139.                         .then(function(){wkof.load_script("https://cdn.jsdelivr.net/cal-heatmap/3.3.10/cal-heatmap.min.js", true)})
  140.                         .then(function(){wkof.load_css("https://cdn.jsdelivr.net/cal-heatmap/3.3.10/cal-heatmap.css", true);});
  141.         }
  142.  
  143.         // Adds CSS to head
  144.         function add_css() {
  145.                 $('head').append('<style id="heatmapCSS">'+
  146.                                  '.year {'+
  147.                                  '    text-align: center;'+
  148.                                  '    font-size: 24px;'+
  149.                                  '    font-weight: bold;'+
  150.                                  '}'+
  151.                                  '.cal-heatmap-container {'+
  152.                                  '    margin: auto;'+
  153.                                  '}'+
  154.                                  'section.heatmaps {'+
  155.                                  '    margin-top: 3px;'+
  156.                                  '    padding: 15px 30px;'+
  157.                                  '    background-color: ' + (is_dark ? "#232629" : "#d5d5d5") + ';'+
  158.                                  '    border-radius: 5px;'+
  159.                                  (is_dark ? 'box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.7), 2px 2px 2px rgba(0, 0, 0, 0.7) !important;' : '')+
  160.                                  '}'+
  161.                                  '.graph-label {'+
  162.                                  '    fill: ' + (is_dark ? "white" : "#424242") + ' !important;'+
  163.                                  '}'+
  164.                                  (is_dark ? 'rect[class=""], rect[class=" graph-rect"] {fill: #555555;}' : '')+
  165.                                  '.heatmaps .collapsed {'+
  166.                                  '    visibility: hidden;'+
  167.                                  '    height: 15px;'+
  168.                                  '}'+
  169.                                  '.heatmaps .year {'+
  170.                                  '    cursor: pointer;'+
  171.                                  '}'+
  172.                                  '.heatmaps .stat {'+
  173.                                  '    display: inline-block;'+
  174.                                  '    width: 33%;'+
  175.                                  '    text-align: center;'+
  176.                                  '}'+
  177.                                  '.heatmaps .stats {'+
  178.                                  '    font-size: 15px;'+
  179.                                  '    height: 40px;'+
  180.                                  '}'+
  181.                                  '</style>');
  182.         }
  183.  
  184.         // Installs the CSS for custom colors if the user has chosen their own
  185.         function add_color_css() {
  186.                 if (wkof.settings.wanikani_heatmap.custom_colors && wkof.settings.wanikani_heatmap.even_gradient) {
  187.                         var colors = interpolate_colors(hex_to_rgb(wkof.settings.wanikani_heatmap.start_color), hex_to_rgb(wkof.settings.wanikani_heatmap.end_color));
  188.                         $('head').append('<style id=heatmapColorsCSS>'+
  189.                                          'rect.q1 {'+
  190.                                          '    background-color: '+colors[0]+';'+
  191.                                          '    fill: '+colors[0]+';'+
  192.                                          '}'+
  193.                                          'rect.q2 {'+
  194.                                          '    background-color: '+colors[1]+';'+
  195.                                          '    fill: '+colors[1]+';'+
  196.                                          '}'+
  197.                                          'rect.q3 {'+
  198.                                          '    background-color: '+colors[2]+';'+
  199.                                          '    fill: '+colors[2]+';'+
  200.                                          '}'+
  201.                                          'rect.q4 {'+
  202.                                          '    background-color: '+colors[3]+';'+
  203.                                          '    fill: '+colors[3]+';'+
  204.                                          '}'+
  205.                                          'rect.q5 {'+
  206.                                          '    background-color: '+colors[4]+';'+
  207.                                          '    fill: '+colors[4]+';'+
  208.                                          '}'+
  209.                                          '</style>');
  210.                 }
  211.         }
  212.  
  213.         //-----------------------------------------------------------------------------------------------------------------------------------------------------//
  214.         //------------------------------------------------------------------DATA PROCESSING--------------------------------------------------------------------//
  215.         //-----------------------------------------------------------------------------------------------------------------------------------------------------//
  216.  
  217.         function initiate_storage(review_data) {
  218.                 return store_data(review_data, true);
  219.         }
  220.  
  221.         // Initiates data storage or updates the current storage
  222.         function store_data(review_data, initiate=false) {
  223.                 let data = review_data.data;
  224.                 if (data.length > 0) {
  225.                         if (initiate) {
  226.                                 stored_data = {
  227.                                         counts: {},
  228.                                         first_date: data[0].data.created_at,
  229.                                         last_date: data[0].data.created_at,
  230.                                         studied_days: 1,
  231.                                         total_reviews: 0,
  232.                                         longest_streak: 1,
  233.                                         current_streak: 1
  234.                                 }
  235.                                 stored_data.counts[Date.parse(new Date(Date.parse(stored_data.first_date)).toDateString())/1000] = 0
  236.                         }
  237.                         var date = new Date(Date.parse(stored_data.last_date)).toDateString();      // Ex. "Tue Feb 05 2019"
  238.                         date = {string: date, timestamp: Date.parse(date)/1000};                     // Date string and a timestamp for 00:00:00 that day (in seconds)
  239.                         stored_data.last_date = data[data.length-1].data.created_at;
  240.                         for (var i = 0; i < data.length; i++) {
  241.                                 // Check if this is a review on a new day
  242.                                 var new_date = new Date(Date.parse(data[i].data.created_at)).toDateString();
  243.                                 if (new_date != date.string) {
  244.                                         // Get new string and timestamp
  245.                                         date = {string: new_date, timestamp: Date.parse(new_date)/1000};
  246.                                         stored_data.counts[date.timestamp] = 0;
  247.                                         // Update studied days
  248.                                         stored_data.studied_days++;
  249.                                         // Update streak info
  250.                                         if (date.timestamp - 86400 in stored_data.counts) stored_data.current_streak++;
  251.                                         else stored_data.current_streak = 1
  252.                                         if (stored_data.current_streak > stored_data.longest_streak) stored_data.longest_streak = stored_data.current_streak;
  253.                                 }
  254.                                 // Increment reviews on this day and total reviews
  255.                                 stored_data.counts[date.timestamp]++
  256.                                 stored_data.total_reviews++
  257.                         }
  258.                         // Store data when done
  259.                         localStorage.setItem(data_storage_key, JSON.stringify(stored_data));
  260.                 }
  261.                 // Check whether the current streak has been broken before returning
  262.                 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;
  263.  
  264.                 return stored_data
  265.         }
  266.  
  267.         //-----------------------------------------------------------------------------------------------------------------------------------------------------//
  268.         //----------------------------------------------------------------------HEATMAPS-----------------------------------------------------------------------//
  269.         //-----------------------------------------------------------------------------------------------------------------------------------------------------//
  270.  
  271.         // Creates the heatmaps and inserts them into the page
  272.         function add_heatmaps(data) {
  273.                 var section = document.createElement('section');
  274.                 $(section).attr('class', "heatmaps");
  275.  
  276.                 var stats = calculate_stats(data);
  277.                 var heatmaps = create_heatmaps(data);
  278.  
  279.                 $(section).append([stats, heatmaps]);
  280.                 $('.progression').after(section);
  281.         }
  282.  
  283.         // Calculates the stats and returns a stats element
  284.         function calculate_stats(data) {
  285.                 var total_days = (Date.now()-Date.parse(data.first_date))/(24*60*60*1000) + 1;
  286.                 var average_total = Math.round(data.total_reviews/total_days);
  287.                 var average_studied = Math.round(data.total_reviews/data.studied_days);
  288.                 var studied_ratio = Math.round(data.studied_days/total_days*100)+"%";
  289.  
  290.                 var stats = document.createElement('div');
  291.                 $(stats).attr('class', 'stats');
  292.                 $(stats).append('<div class="stat">Days studied: '+studied_ratio+' ('+data.studied_days+' of '+Math.ceil(total_days)+') </div>');
  293.                 $(stats).append('<div class="stat">Average daily reviews: '+average_total+' ('+average_studied+' per day studied)</div>');
  294.                 $(stats).append('<div class="stat">Longest streak: '+data.longest_streak+' (current '+data.current_streak+')</div>');
  295.  
  296.                 return stats
  297.         }
  298.  
  299.         // Creates the heatmap for each year
  300.         function create_heatmaps(data) {
  301.                 var heatmaps = document.createElement('div');
  302.                 $(heatmaps).attr("class","heatmaps");
  303.  
  304.                 var year = new Date(data.first_date).getFullYear();
  305.                 var end_year = new Date(data.last_date).getFullYear();
  306.                 while (year <= end_year) {
  307.                         var year_elem = create_heatmap(year, data);
  308.                         if (wkof.settings.wanikani_heatmap.reverse_years) $(heatmaps).append(year_elem);
  309.                         else $(heatmaps).prepend(year_elem);
  310.                         year++;
  311.                 }
  312.  
  313.                 return heatmaps
  314.         }
  315.  
  316.         // Creates a heatmap for the given year
  317.         function create_heatmap(year, data) {
  318.                 // Prepare container
  319.                 var heatmap = document.createElement('div');
  320.                 $(heatmap).attr({id: "heatmap"+year, class: "heatmap"});
  321.                 $(heatmap).append('<div class="year">'+year+'</div>');
  322.                 heatmap.children[0].onclick = function(event){$(event.target.nextElementSibling).toggleClass('collapsed'); toggle_collapse(year);};
  323.  
  324.                 // Create heatmap
  325.                 var chart = new CalHeatMap();
  326.                 var chart_elem = document.createElement("div");
  327.                 if (is_collapsed(year)) $(chart_elem).attr('class', 'collapsed chart');
  328.                 else $(chart_elem).attr("class", "chart");
  329.                 var config = {
  330.                         itemSelector: chart_elem,
  331.                         start: new Date(year, 0),
  332.                         range: 12,
  333.                         domain: "month",
  334.                         subDoman: "x_days",
  335.                         data: data.counts,
  336.                         label: {position: "top"},
  337.                         weekStartOnMonday: wkof.settings.wanikani_heatmap.week_start=="true" ? true : false,
  338.                         legend: [50, 100, 200, 400]
  339.                 }
  340.                 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];
  341.                 chart.init(config);
  342.  
  343.                 $(heatmap).append(chart_elem);
  344.  
  345.                 return heatmap
  346.         }
  347.  
  348.         //-----------------------------------------------------------------------------------------------------------------------------------------------------//
  349.         //-------------------------------------------------------------------HELP FUNCTIONS--------------------------------------------------------------------//
  350.         //-----------------------------------------------------------------------------------------------------------------------------------------------------//
  351.  
  352.         // Updates the stored collapse data after a year has been collapsed or expanded
  353.         function toggle_collapse(year) {
  354.                 collapsed_years[year] = !collapsed_years[year]
  355.                 localStorage.setItem('WKheatmapCollapsed', JSON.stringify(collapsed_years));
  356.         }
  357.  
  358.         // Checks whether the year is collapsed
  359.         function is_collapsed(year) {
  360.                 if (!(year in collapsed_years)) collapsed_years[year] = false;
  361.                 return collapsed_years[year];
  362.         }
  363.  
  364.         // Interpolate between two colors, returning an array of rgb colors
  365.         function interpolate_colors(color1, color2) {
  366.                 var color_list = [];
  367.                 color1 = color1.match(/\d+/g).map(Number);
  368.                 color2 = color2.match(/\d+/g).map(Number);
  369.                 for(var i = 0; i < 5; i++) color_list.push("rgb("+interpolate_color(color1, color2, 0.25*i).join()+")");
  370.                 console.log("interpolate", color1, color2, color_list);
  371.                 return color_list;
  372.         }
  373.  
  374.         // Returns an rgb color interpolation between given rgb colors
  375.         function interpolate_color(color1, color2, factor) {
  376.                 var result = color1.slice();
  377.                 for (var i = 0; i < 3; i++) result[i] = Math.round(result[i] + factor * (color2[i] - color1[i]));
  378.                 return result;
  379.         };
  380.  
  381.         // Converts a hex color to rgb
  382.         function hex_to_rgb(hex) {
  383.                 var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  384.                 console.log("hextorgb", hex, "rgb("+parseInt(result[1], 16)+","+parseInt(result[2], 16)+","+parseInt(result[3], 16)+")");
  385.                 return "rgb("+parseInt(result[1], 16)+","+parseInt(result[2], 16)+","+parseInt(result[3], 16)+")";
  386.         }
  387.  
  388.         // Handy little function that rfindley wrote. Checks whether the theme is dark.
  389.         function is_dark_theme() {
  390.                 // Grab the <html> background color, average the RGB.  If less than 50% bright, it's dark theme.
  391.                 return $('body').css('background-color').match(/\((.*)\)/)[1].split(',').slice(0,3).map(str => Number(str)).reduce((a, i) => a+i)/(255*3) < 0.5;
  392.         }
  393. })();
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement