Jump to content

User:Isaac (WMF)/clickstream viz.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
// <pre> Avoid triggering: https://en.wikipedia.org/wiki/Special:WantedTemplates
// Code largely borrowed from: https://en.wikipedia.org/wiki/User:TayIorRobinson/wikigender.js

/**
 * @typedef {{title: string, views: number}} ClickstreamLink
 * @typedef {{limit: string, month: string, next: string, results: ClickstreamLink[]}} ClickstreamData
 */
// </pre>


(async() => {
	// https://wikinav.wmcloud.org/api/v1/en/Chocolate/destinations/latest?limit=10
    const CLICKSTREAM_API = "https://wikinav.wmcloud.org/api/v1/";
    
    async function fetchClickstreamLangs() {
        let response = await fetch("https://wikinav.wmcloud.org/api/v1/latest/meta");
        let data = await response.json();
        return data.languages;
    }


    /**
     * Requests the clickstream data for the current page.
     * @param {String} lang Wikipedia language to query.
     * @returns {Promise<ClickstreamData>} The latest clickstream data for the page.
     */
    async function fetchData(lang) {
        let title = mw.config.get("wgPageName");
    	let response = await fetch(CLICKSTREAM_API + lang + "/" + title + "/destinations/latest?limit=1000");
        let data = await response.json();
        return data;
    }
    
    async function fetchRedirects() {
    	let redirects = {};
    	let linksToRedirects = document.querySelectorAll("a.mw-redirect");
    	if (linksToRedirects.length > 0) {
    		let joinedTitles = "";
    		let titlesInQuery = new Set();
	    	for (let i = 0; i < linksToRedirects.length; i++) {
	    		if (titlesInQuery.has(linksToRedirects[i].title)) continue;
	    		titlesInQuery.add(linksToRedirects[i].title);
	    		joinedTitles += linksToRedirects[i].title + "|";
				// Process first 50 (single API call max)
	    		if (titlesInQuery.size == 50) break;
			}
			// remove final | character
			joinedTitles = joinedTitles.slice(0, -1);
			let lang = mw.config.get("wgPageContentLanguage");
	    	let response = await fetch("https://" + lang + ".wikipedia.org/w/api.php?action=query&format=json&formatversion=2&redirects&titles=" + joinedTitles);
	        let data = await response.json();
	        for (let rd of data.query.redirects) {
	        	redirects[rd.from.toLowerCase().replace(/_/g, " ")] = rd.to.toLowerCase().replace(/_/g, " ");
	        }
    	}
        return redirects;
    }
    
    /**
     * Requests the clickstream data for the current page.
     * @param {Number} num_views Number of clicks to that link.
     * @param {Number} max_views Maximum number of clicks to any link.
     * @returns {String} Alpha value as string based on proportion of pageviews via link. [0.2 - 1]
     */
    function propPageviewsToAlpha(num_views, max_views) {
    	if (num_views == 0) {
    		return '0';
    	} else {
    		let alpha = 0.1 + (0.9 * num_views / max_views);
	    	return alpha.toFixed(2);
    	}
    }

    /**
     * Applies colour to links on the page
     * @param {ClickstreamLink[]} links The links to colour.
     */
    function applyLinkColours(links, redirects) {
        let pageLinks = document.querySelector("#bodyContent").querySelectorAll("a[href^=\"/wiki/\"]");
        let max_views = 0;
        // Create an object of all the links on the page.
        let linksOnPage = {};
        for (let link of pageLinks) {
            let linkText = decodeURIComponent(link.href.split("/")[4].toLowerCase().replace(/_/g, " "));
            // map redirects to canonical title (used by clickstream)
            if (linkText in redirects) {
            	linkText = redirects[linkText];
            }
            // Build array of all instances of a link appearing on page
            if (!(linkText in linksOnPage)) {
                linksOnPage[linkText] = [];
            }
            linksOnPage[linkText].push(link);
        }
        for (let link of links) {
        	max_views = Math.max(max_views, link.views);
        }
        // Colour the links.
        for (let link of links) {
            let linkOnPage = linksOnPage[link.title.toLowerCase().replace(/_/g, " ")];
            if (!linkOnPage) continue;
            let alpha = propPageviewsToAlpha(link.views, max_views);
            for (let l of linkOnPage) {
                l.style.backgroundColor = "rgba(216,127,165," + alpha + ")";
            }
        }
    }

	// article but not Main Page (links change too frequently to be relevant to data)
    if (mw.config.get('wgNamespaceNumber') == 0 && mw.config.get('wgWikibaseItemId') != 'Q5296') {
    	let lang = mw.config.get("wgDBname").replace("wiki","");
    	let csLangs = await fetchClickstreamLangs();
    	if (csLangs.includes(lang)) {
    		let mwIndicator = document.createElement("div");
		    mwIndicator.className = "mw-indicator";
		    mwIndicator.id = "mw-indicator-clickstream";
		    document.querySelector(".mw-indicators").appendChild(mwIndicator);
		
		    let link = document.createElement("a");
		    link.href = "javascript:void(0)";
		    link.onclick = async() => {
		        link.remove();
		        let data = await fetchData(lang);
		        let redirects = await fetchRedirects();
		        applyLinkColours(data.results, redirects);
		    };
		    link.title = "Visualize the clicks to the links on the page";
		    mwIndicator.appendChild(link);
		
		    let img = document.createElement("img");
		    img.alt = "Data analytics icon";
		    img.src = "https://upload.wikimedia.org/wikipedia/commons/thumb/8/86/Cs%2B.svg/41px-Cs%2B.svg.png";
		    img.decoding = "async";
		    img.height = 20;
		    img.width = 20;
		    link.appendChild(img);
    	}
    }
})();