wikimedia/mediawiki-extensions-Translate

View on GitHub
resources/js/ext.translate.translationstats.graphbuilder.js

Summary

Maintainability
C
1 day
Test Coverage
/*!
 * Graph component to display translation stats using ChartJS
 * @license GPL-2.0-or-later
 */

( function () {
    'use strict';
    var graphInfo = {
            edits: mw.msg( 'translate-statsf-count-edits' ),
            users: mw.msg( 'translate-statsf-count-users' ),
            registrations: mw.msg( 'translate-statsf-count-registrations' ),
            reviews: mw.msg( 'translate-statsf-count-reviews' ),
            reviewers: mw.msg( 'translate-statsf-count-reviewers' )
        }, granularityInfo = {
            years: mw.msg( 'translate-statsf-scale-years' ),
            months: mw.msg( 'translate-statsf-scale-months' ),
            weeks: mw.msg( 'translate-statsf-scale-weeks' ),
            days: mw.msg( 'translate-statsf-scale-days' ),
            hours: mw.msg( 'translate-statsf-scale-hours' )
        }, graphColors = [
            'skyblue', 'green', 'orange', 'blue', 'red', 'darkgreen', 'purple', 'peru',
            'cyan', 'salmon', 'slateblue', 'yellowgreen', 'magenta', 'aquamarine', 'gold', 'violet'
        ],
        GraphBuilder;

    /**
     * Used to display translation stats graph. Each instance of this class manages one
     * instance of the graph.
     *
     * @internal
     * @param {Object} $graphContainer The title of the page including language code
     *   to store the translation.
     * @param {Object} graphOptions Graph options, current only processes the width and height.
     * @return {Object} Instance of the graph builder
     */
    GraphBuilder = function ( $graphContainer, graphOptions ) {
        var $graphElement = $( '<canvas>' )
                .attr( 'class', 'mw-translate-translationstats-graph' )
                .attr( 'role', 'img' )
                .attr( 'tabindex', 0 )
                .text( mw.msg( 'translate-statsf-graph-alt-text-info' ) ),
            $graphWrapper = $( '<div>' )
                .attr( 'class', 'mw-translationstats-graph-container' ),
            $loadingElement = $( '<div>' )
                .attr( 'class', 'mw-translate-loading-spinner' ),
            $errorElement = $( '<div>' )
                .attr( 'class', 'mw-translate-error-container' ),
            $tableElement = $( '<table>' )
                .addClass( 'wikitable mw-translate-translationstats-table' )
                .attr( 'tabindex', 0 )
                .attr(
                    'summary', mw.msg( 'translate-statsf-alt-text' )
                ),
            lineChart;

        $graphWrapper
            .width( graphOptions && graphOptions.width )
            .height( graphOptions && graphOptions.height );

        // Set the container height and width if passed.
        $graphContainer.append( [
            $graphWrapper,
            $loadingElement,
            $errorElement
        ] );

        $graphWrapper.append( $graphElement );

        /** @internal */
        function display( options ) {
            if ( lineChart ) {
                cleanup();
            }

            // Set the appropriate height and width and display the loader.
            $graphWrapper
                .width( options.width )
                .height( options.height );

            showLoading();

            return getData( options ).then( function ( graphData ) {
                // Hide the loader before displaying the data.
                showData( graphData, options );
            } ).fail( function ( errorCode, results ) {
                var errorInfo = results && results.error ? results.error.info :
                    mw.msg( 'translate-statsf-unknown-error' );
                displayError( mw.msg( 'translate-statsf-error-message', errorInfo ) );
            } ).always( function () {
                hideLoading();
            } );
        }

        function showData( apiResponse, options ) {
            var graphData = getAxesLabelsAndData( apiResponse.data ),
                graphDatasets = [],
                datasetLabels = apiResponse.labels;

            if ( graphData.data.length ) {
                graphData.data.forEach( function ( dataset, datasetIndex ) {
                    var graphDataset = {
                        data: dataset,
                        fill: false,
                        borderColor: getLineColor( datasetIndex )
                    };

                    if ( datasetLabels[ datasetIndex ] ) {
                        graphDataset.label = datasetLabels[ datasetIndex ];
                    }

                    graphDatasets.push( graphDataset );
                } );
            }

            lineChart = new Chart( $graphElement, {
                type: 'line',
                data: {
                    labels: graphData.axesLabels,
                    datasets: graphDatasets
                },
                options: {
                    maintainAspectRatio: false,
                    legend: {
                        display: datasetLabels.length !== 0
                    },
                    scales: {
                        yAxes: [ {
                            scaleLabel: {
                                display: true,
                                labelString: getXAxesLabel( options.measure )
                            },
                            ticks: {
                                beginAtZero: true,
                                precision: 0,
                                callback: function ( value ) {
                                    return mw.language.convertNumber( Number( value ) );
                                }
                            }
                        } ],
                        xAxes: [ {
                            scaleLabel: {
                                display: true,
                                labelString: getYAxesLabel( options.granularity )
                            },
                            ticks: {
                                maxTicksLimit: 15
                            },
                            gridLines: {
                                display: false
                            }
                        } ]
                    },
                    tooltips: {
                        callbacks: {
                            label: function ( tooltipItem, data ) {
                                var convertedValue = mw.language.convertNumber( Number( tooltipItem.yLabel ) ),
                                    label = data.datasets[ tooltipItem.datasetIndex ].label;

                                if ( label ) {
                                    return label + ': ' + convertedValue;
                                }

                                return convertedValue;
                            }
                        }
                    }
                }
            } );

            // Generate table inside the canvas element to improve accessibility.
            showTable( graphData, datasetLabels, options );
        }

        function getAxesLabelsAndData( jsonGraphData ) {
            var labels = [], graphData = [],
                labelIndex = 0,
                maxValue = 0, minValue = 0;

            for ( var labelProp in jsonGraphData ) {
                if ( labels.indexOf( labelProp ) === -1 ) {
                    labels.push( labelProp );
                }

                var labelData = jsonGraphData[ labelProp ];

                for ( var i = 0; i < labelData.length; ++i ) {
                    if ( !graphData[ i ] ) {
                        graphData[ i ] = [];
                    }

                    var currentValue = labelData[ i ];
                    graphData[ i ][ labelIndex ] = currentValue;
                    if ( currentValue < minValue ) {
                        minValue = currentValue;
                    }

                    if ( currentValue > maxValue ) {
                        maxValue = currentValue;
                    }
                }

                ++labelIndex;
            }

            return {
                axesLabels: labels,
                data: graphData,
                max: maxValue,
                min: minValue
            };
        }

        function getXAxesLabel( measure ) {
            return graphInfo[ measure ];
        }

        function getYAxesLabel( granularity ) {
            return granularityInfo[ granularity ];
        }

        function getData( filterOptions ) {
            var api = new mw.Api(),
                apiParams = {
                    action: 'translationstats',
                    count: filterOptions.measure,
                    days: filterOptions.days,
                    start: filterOptions.start || null,
                    scale: filterOptions.granularity,
                    group: filterOptions.group,
                    language: filterOptions.language,
                    formatversion: 2
                };

            // Remove null or empty array from request object
            Object.keys( apiParams ).forEach( function ( apiParamKey ) {
                var apiParamValue = apiParams[ apiParamKey ];
                if (
                    apiParamValue === null ||
                        ( Array.isArray( apiParamValue ) && apiParamValue.length === 0 )
                ) {
                    delete apiParams[ apiParamKey ];
                }
            } );

            return api.get( apiParams ).then( function ( result ) {
                return result.translationstats;
            } );
        }

        function getLineColor( index ) {
            var colorIndex = index % graphColors.length,
                colorName = graphColors[ colorIndex ];
            return colorName;
        }

        function displayError( errorMessage ) {
            $errorElement.text( errorMessage );
            $graphContainer.addClass( 'mw-translate-has-error' )
                .height( 'auto' );
        }

        /** @internal */
        function showLoading() {
            // show loading, and hide error messages.
            $graphContainer.addClass( 'mw-translate-loading' )
                .removeClass( 'mw-translate-has-error' );
        }

        function hideLoading() {
            $graphContainer.removeClass( 'mw-translate-loading' );
        }

        function showTable( graphData, datasetLabels, options ) {
            $tableElement
                .append(
                    $( '<caption>' ).text( getGraphSummary( options ) )
                )
                .append( getTableHead( datasetLabels, options ) )
                .append( getTableBody( graphData ) );

            $graphContainer.append( $tableElement );
        }

        function getTableHead( datasetLabels, options ) {
            var $tableHead = $( '<thead>' ),
                $tableHeadRow = $( '<tr>' ),
                i = 0;

            $tableHeadRow.append( $( '<th>' ).text( getYAxesLabel( options.granularity ) ) );

            if ( datasetLabels && datasetLabels.length ) {
                for ( ; i < datasetLabels.length; ++i ) {
                    $tableHeadRow.append( $( '<th>' ).text( datasetLabels[ i ] ) );
                }
            } else {
                $tableHeadRow.append( $( '<th>' ).text( getXAxesLabel( options.measure ) ) );
            }

            return $tableHead.append( $tableHeadRow );
        }

        function getTableBody( graphData ) {
            var $tbody = $( '<tbody>' );

            for ( var scaleIndex = 0; scaleIndex < graphData.axesLabels.length; scaleIndex++ ) {
                var $tBodyRow = $( '<tr>' )
                    .append( $( '<td>' ).text( graphData.axesLabels[ scaleIndex ] ) );

                for ( var datasetIndex = 0; datasetIndex < graphData.data.length; datasetIndex++ ) {
                    var columnValue = '';
                    if (
                        graphData.data[ datasetIndex ] &&
                        graphData.data[ datasetIndex ][ scaleIndex ] !== undefined
                    ) {
                        columnValue =
                            mw.language.convertNumber(
                                Number( graphData.data[ datasetIndex ][ scaleIndex ] )
                            );
                    }
                    $tBodyRow.append( $( '<td>' ).text( columnValue ) );
                }

                $tbody.append( $tBodyRow );
            }

            return $tbody;
        }

        function cleanup() {
            lineChart.destroy();
            $tableElement.remove();
        }

        function getGraphSummary( options ) {
            return getXAxesLabel( options.measure ) + ' / ' +
                getYAxesLabel( options.granularity );
        }

        return {
            display: display,
            showLoading: showLoading
        };
    };

    mw.translate = mw.translate || {};
    mw.translate.TranslationStatsGraphBuilder = mw.translate.TranslationStatsGraphBuilder || GraphBuilder;
}() );