wikimedia/mediawiki-extensions-Wikibase

View on GitHub
build/dist-size/web/lib/main.js

Summary

Maintainability
A
2 hrs
Test Coverage
/* global Plotly */

const FIELDS = {
    DATE: 'date',
    SIZE: 'size',
    SHA: 'sha',
    SUBJECT: 'subject',
};
const CUSTOM_FIELDS = {
    PREVIOUS_SIZE: 'previousSize',
    SIZE_DELTA_PERCENT: 'sizeDeltaPercent',
};

const ANNOTATION_THRESHOLD_PERCENT = 8;

function unpack( rows, key ) {
    return rows.map( ( row ) => row[ key ] );
}

function sortByDate( first, second ) {
    const a = new Date( first[ FIELDS.DATE ] ),
        b = new Date( second[ FIELDS.DATE ] );
    return a > b ? 1 : a < b ? -1 : 0;
}

function augmentFileHistoryBySizeDelta( fileHistory ) {
    const newHistory = JSON.parse( JSON.stringify( fileHistory ) );
    newHistory.sort( sortByDate );

    let previousRevision = null;
    newHistory.forEach( ( revision ) => {
        const previousSize = previousRevision ? previousRevision[ FIELDS.SIZE ] : null;
        const deltaPercent = Math.round(
            ( revision[ FIELDS.SIZE ] - previousSize ) / previousSize * 100,
        );
        revision[ CUSTOM_FIELDS.PREVIOUS_SIZE ] = previousSize;
        revision[ CUSTOM_FIELDS.SIZE_DELTA_PERCENT ] = deltaPercent;
        previousRevision = revision;
    } );

    return newHistory;
}

function showRevisionInformation( revision ) {
    window.open(
        'https://github.com/wikimedia/mediawiki-extensions-Wikibase/commit/' + revision[ FIELDS.SHA ],
        '_blank',
    );
}

/**
 * Build an annotation if the size delta exceeds the configured threshold
 * Requires the revision to be augmented by `deltaPercent` and `previousSize` properties
 *
 * @param {Object} revision
 * @return {?Object}
 */
function buildSizeDeltaAnnotation( revision ) {
    if ( revision[ CUSTOM_FIELDS.PREVIOUS_SIZE ] === null || revision[ FIELDS.SIZE ] === null ) {
        // inception & removal may be a notable but not with respect to size
        return null;
    }
    const sizeDeltaPercent = revision[ CUSTOM_FIELDS.SIZE_DELTA_PERCENT ];
    if ( Math.abs( sizeDeltaPercent ) <= ANNOTATION_THRESHOLD_PERCENT ) {
        return null;
    }
    return {
        x: revision[ FIELDS.DATE ],
        y: revision[ FIELDS.SIZE ],
        xref: 'x',
        yref: 'y',
        text: `∆${sizeDeltaPercent}%`,
        hovertext: `<sub>${revision[ FIELDS.SHA ]}</sub><br>${revision[ FIELDS.SUBJECT ]}`,
        font: {
            color: sizeDeltaPercent > 0 ? 'red' : 'green',
            size: Math.max( 5, // minimum size
                Math.min( 18, // maximum size
                    Math.round(
                        Math.log( Math.abs( sizeDeltaPercent ) ) * 4, // derive size from delta
                    ) ) ),
        },
        showarrow: true,
        arrowhead: 7,
        ax: 0,
        ay: -40,
        captureevents: true,
        customdata: revision,
    };
}

function buildScatterTrace( fileName, fileHistory ) {
    return {
        type: 'scatter',
        mode: 'lines+markers',
        name: fileName,
        x: unpack( fileHistory, FIELDS.DATE ),
        y: unpack( fileHistory, FIELDS.SIZE ),
        customdata: fileHistory,
        hovertemplate:
            `%{y} = ∆%{customdata.${CUSTOM_FIELDS.SIZE_DELTA_PERCENT}}%<br>` +
            `<sup>%{customdata.${FIELDS.SHA}}</sup><br>` +
            `%{customdata.${FIELDS.SUBJECT}}`,
        marker: { size: 4 },
    };
}

function plot( el, historyData ) {
    const traces = [];
    const annotations = [];

    for ( const fileName in historyData ) {
        const fileHistory = augmentFileHistoryBySizeDelta( historyData[ fileName ] );

        traces.push( buildScatterTrace( fileName, fileHistory ) );

        fileHistory.forEach( ( revision ) => {
            const annotation = buildSizeDeltaAnnotation( revision );
            if ( annotation ) {
                annotations.push( annotation );
            }
        } );
    }

    Plotly.newPlot(
        el,
        traces,
        {
            title: 'File size over time',
            yaxis: {
                title: 'file size (bytes)',
                fixedrange: true,
            },
            xaxis: {
                title: 'date (ctrl+click to go to revision)',
            },
            annotations,
        },
        {
            scrollZoom: true,
        },
    );

    el.on( 'plotly_click', function ( data ) {
        if ( !data.event.ctrlKey ) {
            return;
        }

        showRevisionInformation( data.points[ 0 ].customdata );
    } );
    el.on( 'plotly_clickannotation', function ( data ) {
        showRevisionInformation( data.annotation.customdata );
    } );
}

function DistHist( el, historyDataFile ) {
    Plotly.d3.json( historyDataFile, ( error, historyData ) => {
        if ( error !== null ) {
            el.innerText = 'Data not found. Make sure to generate data.json by running the analyze script';
            return;
        }

        plot( el, historyData );
    } );
}

export {
    DistHist,
};