Greasy Fork is available in English.

Last.fm Original Tag Chart

Restores the "subway map" chart on new Last.fm report pages

Installer dette scriptet?
Skaperens foreslåtte skript

Du vil kanskje også like Last.fm Colored Tags.

Installer dette scriptet
// ==UserScript==
// @name         Last.fm Original Tag Chart
// @namespace    http://thlayli.detrave.net
// @description  Restores the "subway map" chart on new Last.fm report pages
// @icon         https://www.google.com/s2/favicons?sz=64&domain=last.fm
// @require      https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js
// @require      https://code.jquery.com/jquery-1.9.0.min.js
// @require      https://cdn.jsdelivr.net/npm/jquery-address@1.6.0/src/jquery.address.min.js
// @require      https://greasyfork.org/scripts/383527-wait-for-key-elements/code/Wait_for_key_elements.js?version=701631
// @include      https://www.last.fm/*
// @version      1.7
// @license      MIT
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

// define custom colors (24 colors)
var colorPalettes = [];
// last.fm new chart palette (repeated)
colorPalettes[0] = ['#6ABDCB', '#5AA9C6', '#4A94C1', '#397FBB', '#2A6CB6', '#1955B0', '#3D53A0', '#5E5291', '#835080', '#A64F71', '#CD4D60', '#FA4B4B', '#6ABDCB', '#5AA9C6', '#4A94C1', '#397FBB', '#2A6CB6', '#1955B0', '#3D53A0', '#5E5291', '#835080', '#A64F71', '#CD4D60', '#FA4B4B'];
// interlaced rainbow mod 11 (1,12,2,13...)
colorPalettes[1] = ['#0FB2AC', '#3E90D2', '#795CC6', '#B539B0', '#DF2D77', '#F93A40', '#FA7845', '#F9A948', '#FDE34E', '#F9FF51', '#C5E44D', '#68D15A', '#329FD4', '#5579E0', '#8E48BA', '#C72E95', '#F13667', '#FB5843', '#FA8F46', '#FCCE4B', '#FBF250', '#E1F051', '#9DDD53', '#07C16C'];
// full rainbow
colorPalettes[2] = ['#0FB2AC', '#329FD4', '#3E90D2', '#5579E0', '#795CC6', '#8E48BA', '#B539B0', '#C72E95', '#DF2D77', '#F13667', '#F93A40', '#FB5843', '#FA7845', '#FA8F46', '#F9A948', '#FCCE4B', '#FDE34E', '#FBF250', '#F9FF51', '#E1F051', '#C5E44D', '#9DDD53', '#68D15A', '#07C16C'];

// define icons for button
var paletteIcons = [];
paletteIcons[0] = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAABCAYAAACYJC2PAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAWUlEQVQImbXMsREBQRQA0HdLH4Yh0pnIqEBjV4FSbrllRNj/JVoQvuQNh/GScL+9ZVBrt8ww1yA/ntfQdW16yWAbDazyoUibaBbS+uddn5WBfUwKjqezf/5f27tTkfwMSIIAAAAASUVORK5CYII=";
paletteIcons[1] = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAABCAYAAACYJC2PAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAa0lEQVQImbXMIQrCYBgA0Pf92xwI2gxWkzCsA4un8KIK4gnWDXaDYhEEwebc7yl8B3jRHnYZpteF9C01p7eyz9aXh4TlqxOy2bgTMag3I1CsKkqqtiZhXlCE872XM/vnRD+E421LxF/+Tw4/1GowrKfDOgcAAAAASUVORK5CYII=";
paletteIcons[2] = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAABCAYAAACYJC2PAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAiUlEQVQImSXKwUpCQRQA0HNnHibIM4jSZQrSJ/TfLqQPadMyKITCyERdiM5t4fLAiefVMmG0XYjs1F0TSf/Viwyzz7WS6eFnD552R3B7WCvZTNqHcDEe/grppr6hGTx2JDEroEwKhZhWKt28Xn3fAePr0xeCzR+ZfB/PLsn7aaDhdT/UkpftHfgH524vZuHw1EUAAAAASUVORK5CYII=";

function switch_colors(event){
    if(event.data.change && event.data.change == true){

        var current = GM_getValue("legacy-chart-colors", 0);
        if(current == 2)
            GM_setValue("legacy-chart-colors", 0)
        else
            GM_setValue("legacy-chart-colors", current+1)
    }
    $("#legacy-colors-button img").attr('src', paletteIcons[GM_getValue("legacy-chart-colors", 0)]);
    var colors = colorPalettes[GM_getValue("legacy-chart-colors", 0)];
    // add color style tags
    var styleHtml = '<style class="tag-chart-colors">';
    for(i=0; i<24; i++)
        styleHtml += '.legacy-tag-chart .top-tags-over-time-colour-'+i+' {fill: '+colors[i]+'; stroke: '+colors[i]+'} ';
    styleHtml += '</style>';
    $('.tag-chart-colors').remove();
    $('head').append(styleHtml);
    // reset some styles
    $('head').append('<style class="tag-chart-colors">#legacy-colors-button img {margin-left: 7px; height: 15px; width: 16px; border-radius: 50%; margin-top: 7px; cursor: pointer} .legacy-tag-chart svg {paint-order: stroke;} .legacy-tag-chart .tick text {fill: #a7acb7; stroke: none} .legacy-tag-chart .report-box-container--top-tags-over-time { fill: none; } text.lc-tag.lc-tag-white {fill: white; stroke: black} .lc-no-dot {display: none; } .axis-circles .tick text {fill: #313131;} </style>');
}


function main_func(){

    // don't run on old style pages
    if($("section[data-require='stats/top-tags-v2']").length == 0){

        // Create a div to put the chart into
        $('<div class="listening-report-row"><div class="report-box-container report-box-container--charts report-box-container--top-tags-over-time"><div class="report-box-header"><h3 class="report-box-title">Tag timeline</h3></div><div class="report-box-content legacy-tag-chart" style="text-align: center;"></div><div id="legacy-colors-button" class="report-dropdown-button"><img alt="Change color palette" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAABCAYAAACYJC2PAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAWUlEQVQImbXMsREBQRQA0HdLH4Yh0pnIqEBjV4FSbrllRNj/JVoQvuQNh/GScL+9ZVBrt8ww1yA/ntfQdW16yWAbDazyoUibaBbS+uddn5WBfUwKjqezf/5f27tTkfwMSIIAAAAASUVORK5CYII="></div></div></div>').insertBefore($('.listening-report-row--2-wide').first());

        //  add color button handler and set colors
        $("#legacy-colors-button").click({change: true}, switch_colors);
        switch_colors({data: {change: false}});

        // get the tag data from the html table
        d3.selection.prototype.mapNested = function (f) {
            var arr = d3.range(this.size()).map(function () { return []; });
            this.each(function (d, i) { arr[i].push(f.call(this, d, i)); });
            return arr;
        };
        var tagData = d3.select(".js-top-tags-over-time-table").selectAll("thead,tbody").selectAll("tr").selectAll("th,td");
        var vals = tagData.mapNested(function(d, i, j){ return d3.select(this).text().replace(/\n\s+/g,"") }).filter(v => (v.length > 0));

        // set chart size
        var scale = (vals.length > 7) ? 2 : 1;
        // scale lines slightly less than layout
        var lineScale = (vals.length > 7) ? 1.5 : 1;
        var margin = 25*scale;
        var chartHeight = 350*scale;
        var chartWidth =  (vals.length > 7) ? 2000 : 850;

        // scale chart if full year
        if(vals.length > 7)
            $('head').append("<style>.legacy-tag-chart svg {transform: scale(0.52) translate(-44%, -60%);} @media (max-width: 1260px) {.legacy-tag-chart svg {transform: scale(0.45) translate(-64%, -75%);}}</style>");
        else
            $('head').append("<style>.legacy-tag-chart svg {transform: scale(1) translate(0, 0);}</style>");

        // get list of tags
        var tags = vals[0].slice(1);

        // set styles for week/month and year layouts
        $('head').append('<style>.axis-circles .tick text {font-size: ' + ((vals.length > 7) ? 64 : 48) + 'px;} .legacy-tag-chart .y-axis {font-size: ' + ((vals.length > 7) ? 36 : 20) + 'px; font-weight: bold} .legacy-tag-chart .x-axis {font-size: ' + ((vals.length > 7) ? 22 : 11) + 'px; font-weight: normal}</style>');

        // do some data transposition
        valsByTag = d3.transpose(vals);
        var dates = valsByTag[0].map(v => v);
        dates[0] = '';

        // collect top 5 tags for each week
        var weekData = vals.map(r => r.slice(1).map(function(element, index, array){ return (element > 0) ? {tag: tags[index], plays: element } : {tag:'', plays: 0} }).sort(function(a, b){ return parseInt(b.plays) - parseInt(a.plays); }).slice(0,5));
        weekData.shift();

        // find first appearance of tag for dots
        var firstTag = []
        tags.map(function(element, index, array){
            var weekSummary = weekData.map(w => w.map(x => (x.tag == element) ? 1 : 0));
            var weekBoolean = weekSummary.map(w => (w.indexOf(1) != -1) ? 1 : 0);
            firstTag[element] = weekBoolean.indexOf(1);
        });

        // build d3 data object
        var d3data = [];
        weekData.map(function(element1, index1, array1){ element1.map(function(element2, index2, array2){
            if(element2.tag)
                d3data.push({ date: index1+1, rank: index2+1, tag: element2.tag, dot: (firstTag[element2.tag] == index1) });
        })});

        // svg scalers
        const x = d3.scaleLinear().domain([0, 6*scale]).range([margin - 40, chartWidth - margin - 60])
        const y = d3.scaleLinear().domain([7, 1]).range([chartHeight - margin + 40, 10 + margin + ((scale ==2) ? 150 : 0)])

        var svgContainer = d3.select(".legacy-tag-chart").append("svg")
        .attr("width", chartWidth)
        .attr("height", chartHeight);

        var g = svgContainer.append("svg:g");

        // x axis
        var xaxis = d3.axisBottom(x)
        .ticks(vals.length-1)
        .tickFormat(function (d) {
            return dates[d];
        });

        svgContainer.append("g")
            .attr("fill","white")
            .attr("stroke","white")
            .attr("ticks",vals.length-1)
            .attr("class", "x-axis")
            .attr("stroke-width","0.5")
            .attr("transform", "translate("+(25*lineScale)+"," + (chartHeight - margin ) + ")")
            .call(xaxis);

        // y axis
        var yaxis = d3.axisLeft(y)
        .ticks(6)
        .tickFormat(function (d) {
            return  (d < 6) ? d : '';
        });

        // y axis circles
        var yaxisc = d3.axisLeft(y)
        .ticks(6)
        .tickFormat(function (d) {
            return (d < 6) ? "⬤" : '';
        });

        // draw y axis circles
        svgContainer.append("g")
            .attr("transform","translate(" + (((scale == 2) ? 38 : 25) + margin) + ",0)")
            .call(yaxisc)
            .attr("class", "axis-circles")
            .append("text")
            .attr("y", 6)
            .attr("dy", 0.6*scale+"em");


        // draw y axis text
        svgContainer.append("g")
            .attr("fill","white")
            .attr("stroke","none")
            .attr("class", "y-axis")
            .attr("transform","translate(" + (10*scale + margin) + ",0)")
            .call(yaxis)
            .attr("r", 12)
            .append("text")
            .attr("y", 6)
            .attr("transform","translate("+-10*scale+", 0)")
            .attr("dy", (0.71*scale)+"em")
            .style("text-anchor", "end");

        // delete axis lines
        d3.selectAll(".domain,.tick>line").remove();

        // draws circles (first tag appearance only)
        g.selectAll("tag-nodes")
            .data(d3data)
            .enter()
            .append("svg:circle")
            .attr("cx", function(d) { return x(d.date); })
            .attr("cy", function(d) { return y(d.rank); })
            .attr("r", 8*lineScale)
            .attr("class", function(d) { return ((d.dot) ? "" : "lc-no-dot ") + "lc-tag lc-tag-" + tags.indexOf(d.tag) + " top-tags-over-time-colour-" + tags.indexOf(d.tag) % 24})
            .append("svg:title")
            .text(function(d){ return d.tag.toProperCase(); });

        // draws lines
        g.selectAll("tag-lines")
            .data(d3data)
            .enter()
            .append('line')
            .attr('x1', function(d) { return x(d.date); })
            .attr('y1', function(d) { return y(d.rank); })
            .attr('x2', function(d) { return x(d.date) + ((scale == 2) ? 70 : 50); })
            .attr('y2', function(d) { return y(d.rank); })
            .attr("class", function(d) { return "lc-tag lc-tag-" + tags.indexOf(d.tag) + " top-tags-over-time-colour-" + tags.indexOf(d.tag) % 24})
            .style("stroke-width", 5*lineScale)
            .append("svg:title")
            .text(function(d){ return d.tag.toProperCase(); });

        // calculate links and draw curves

        // provide source column, source row, destination row
        function curveMaker(xsrc,xdest,ysrc,ydest){
            return [{ x: x(xsrc)+((scale == 2) ? 69 : 49), y: y(ysrc)},
                    { x: x(xsrc)+((scale == 2) ? 99 : 79), y: y(ysrc)},
                    { x: x(xdest)-29, y: y(ydest)},
                    { x: x(xdest)+1, y: y(ydest)}];
        }

        var curve = d3.line(i)
        .x((d) => d.x)
        .y((d) => d.y)
        .curve(d3.curveBasis);

        // iterate over tags
        jQuery.each(d3data, function(i,d) {



            // check to see if tag appears again
            var nextWeekToAppear = 0;
            for(n=i+1; n<d3data.length; n++){
                if(d3data[n].tag == d.tag){
                    nextWeekToAppear = d3data[n].date;
                    nextRow = d3data[n].rank;
                    break;
                }
            }

            // draw curves to tags that appear in the next week
            if(nextWeekToAppear == d.date+1){
                // check for the tag in the next date set
                if(d.date < vals.length-1 && weekData[d.date]){
                    var weeklyTags = weekData[d.date].map(r => r.tag);
                    var row = (weeklyTags.indexOf(d.tag) < 0) ? 6 : weeklyTags.indexOf(d.tag) + 1;
                    svgContainer.select("g")
                        .append("path")
                        .attr("d", curve(curveMaker(d.date,d.date+1,d.rank,row)))
                        .attr("fill", "none")
                        .attr("stroke", "white")
                        .style("stroke-width", 5*lineScale)
                        .attr("class", function(e) { return "lc-tag lc-tag-" + tags.indexOf(d.tag) + " report-box-container--top-tags-over-time top-tags-over-time-colour-" + tags.indexOf(d.tag) % 24});
                }
            }

            // draw a curve to row 6 for artists who don'tappear next week
            if(nextWeekToAppear > d.date+1){
                svgContainer.select("g")
                    .append("path")
                    .attr("d", curve(curveMaker(d.date,d.date+1,d.rank,6)))
                    .attr("fill", "none")
                    .attr("opacity", 0.5)
                    .attr("stroke", "white")
                    .style("stroke-width", 5*lineScale)
                    .attr("class", function(e) { return "lc-dim lc-tag lc-tag-" + tags.indexOf(d.tag) + " report-box-container--top-tags-over-time top-tags-over-time-colour-" + tags.indexOf(d.tag) % 24})
                    .append("svg:title")
                    .text(function(e){ return d.tag.toProperCase(); });

            }

            if(nextWeekToAppear > 2 && 1 < d.date < 5 && d.date < nextWeekToAppear-1){
                // draw a line on row 6 from the next week to nextWeekToAppear-1
                var scaleDifference = -0.07;
                svgContainer.select("g")
                    .append("path")
                    .attr("d", curve(curveMaker(parseInt(d.date)+0.63+((scale==2) ? scaleDifference : 0), parseInt(nextWeekToAppear)-0.63-((scale==2) ? scaleDifference : 0), 6, 6)))
                    .attr("fill", "none")
                    .attr("stroke", "white")
                    .attr("opacity", 0.5)
                    .style("stroke-width", 5*lineScale)
                    .attr("class", function(e) { return "lc-dim lc-tag lc-tag-" + tags.indexOf(d.tag) + " report-box-container--top-tags-over-time top-tags-over-time-colour-" + tags.indexOf(d.tag) % 24})
                    .append("svg:title")
                    .text(function(e){ return d.tag.toProperCase(); });
            }

            // add curves (behind) from row 6 for returning tags
            if(d.date > 2){
                var lastWeek = weekData[d.date-2].map(r => r.tag);
                var found = lastWeek.indexOf(d.tag);
                //if(found > -1)
                //  else
                //

                // if no dot and not in last week, draw curve from row 6
                if(d.dot == false && found == -1){
                    svgContainer.select("g")
                        .append("path")
                        .attr("d", curve(curveMaker(d.date-1,d.date,6,d.rank)))
                        .attr("fill", "none")
                        .attr("stroke", "white")
                        .attr("opacity", 0.5)
                        .style("stroke-width", 5*lineScale)
                        .attr("class", function(e) { return "lc-dim lc-tag lc-tag-" + tags.indexOf(d.tag) + " report-box-container--top-tags-over-time top-tags-over-time-colour-" + tags.indexOf(d.tag) % 24})
                        .append("svg:title")
                        .text(function(e){ return d.tag.toProperCase(); });
                }
            }
        });

        // add tag labels (first time only)
        g.selectAll("legacy-text")
            .data(d3data)
            .enter()
            .append('text')
            .attr('x', function(d) { return x(d.date); })
            .attr('y', function(d) { return y(d.rank) - 20*lineScale; })
            .attr("fill", "white")
            .attr("stroke","black")
            .attr("font-size", ((scale == 2) ? 20 : 12))
            .attr("font-family", "sans-serif")
            .attr("class", function(d) { return ((d.dot) ? "" : "lc-no-dot ") + "lc-tag lc-tag-white lc-tag-" + tags.indexOf(d.tag) + " report-box-container--top-tags-over-time top-tags-over-time-colour-" + tags.indexOf(d.tag) % 24})
            .attr("text-anchor", "middle")
            .text((d) => (d.tag.length > 11) ? d.tag.slice(0,11).toProperCase() + "..." : d.tag.toProperCase());

        // set up mouseover event listeners
        tags.forEach(function(t) {

            g.selectAll(".lc-tag-"+tags.indexOf(t))
                .on('mouseover', function (d, i) {

                    // dim all tags
                    d3.selectAll(".lc-tag").transition().duration('100').attr('opacity', '.15');
                    // undim selected
                    d3.selectAll(".lc-tag-"+tags.indexOf(t)).transition().duration('100').attr('opacity', '1');
                    // dim selected lc-dim tags to 50%
                    d3.selectAll(".lc-tag-"+tags.indexOf(t)).filter(function() { return this.classList.contains('lc-dim') }).transition().duration('100').attr('opacity', '0.6');

                })
                .on('mouseout', function (d, i) {
                setTimeout(() => {
                    // undim all tags
                    d3.selectAll(".lc-tag").transition().duration('100').attr('opacity', '1');
                    // dim lc-dim tags to 50%
                    d3.selectAll(".lc-dim").transition().duration('100').attr('opacity', '0.5');
                }, "50");
                });

        });

    }

}

String.prototype.toProperCase = function () {
    return this.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
};

$.address.change(function(event) {
    waitForKeyElements(".js-top-tags-over-time-target > .highcharts-container", main_func);
});