gardejo/js-kancolle-logistics-visualizer

View on GitHub
lib/table.material.js

Summary

Maintainability
A
3 hrs
Test Coverage
/**
 * @fileOverview A partical object for tables of materials.
 * @author kclv@ermitejo.com (MORIYA Masaki, alias Gardejo)
 * @license The MIT license (See LICENSE file)
 */

'use strict';

// ----------------------------------------------------------------
// Namespace for Material Tables
// ----------------------------------------------------------------

/**
 * A namespace for material tables.
 * @public
 */
kclv.Table.Materials = {};

// ----------------------------------------------------------------
// Base Table Object for Material Tables
// ----------------------------------------------------------------

/**
 * A base table for material tables.
 * @public
 * @constructor
 * @extends {kclv.Table.Base}
 * @implements {kclv.TableLike}
 * @param {kclv.Relation.Base} relation A relation object as a data source of
 *     the table.
 * @param {(Array.<string>|string)} option An option of the table.
 */
kclv.Table.Materials.Base = function(relation, option) {
    kclv.Table.Base.call(this, relation, option);

    return;
};
kclv.Table.Materials.Base.prototype = Object.create(kclv.Table.Base.prototype);
kclv.Table.Materials.Base.prototype.constructor = kclv.Table.Base;

/**
 * Ensure a relation has some tuple, otherwise throw an exception.
 * @protected
 * @const
 * @throws {RangeError} If a relation has no tuple.
 * @nosideeffects
 */
kclv.Table.Materials.Base.prototype.ensureTupleExisted = function() {
    if (this.relation.count() === 0) {
        throw new RangeError('A relation (table) has no tuple (row).');
    }

    return;
};

/**
 * Build a title of the table and get it.
 * @protected
 * @override
 * @returns {string} A title of the table.
 * @nosideeffects
 */
kclv.Table.Materials.Base.prototype.buildTitle = function() {
    return kclv.Configuration.get([
        'legend',
        this.locale,
        kclv.Game.Materials.getKindOf(this.kind),
        'title'
    ]);
};

// ----------------------------------------------------------------
// Material Table Object to Draw a Candlestick Chart
// ----------------------------------------------------------------

/**
 * A material table to draw a candlestick chart.
 * @public
 * @constructor
 * @extends {kclv.Table.Base}
 * @implements {kclv.TableLike}
 * @param {kclv.Relation.Base} relation A relation object as a data source of
 *     the table.
 * @param {string} kind A target kind of building the table.
 * @param {Array.<string>} option An option of the table.
 *     {@code option[0]} is a material kind (a concrete material name).
 *     For example, {@code Fuel}, {@code Repair}, etc.
 *     {@code option[1]} is frequency.
 *     For example, {@code Monthly}, {@code Weekly}, etc.
 */
kclv.Table.Materials.Candlestick = function(relation, option) {
    kclv.Table.Base.call(this, relation, option);

    return;
};
kclv.Table.Materials.Candlestick.prototype =
    Object.create(kclv.Table.Materials.Base.prototype);
kclv.Table.Materials.Candlestick.prototype.constructor =
    kclv.Table.Materials.Base;

/**
 * Ensure the specified directive is valid, otherwise throw an exception.
 * @protected
 * @override
 * @throws {kclv.Exception.InvalidDirective} If the specified directive is
 *     invalid.
 * @nosideeffects
 */
kclv.Table.Materials.Candlestick.prototype.ensureDirectiveValidated = function(
) {
    kclv.Table.Base.prototype.ensureDirectiveValidated.call(
        this,
        kclv.Game.Materials.MATERIALS,
        ['Yearly', 'Monthly', 'Weekly', 'Daily']
    );

    return;
};

/**
 * Build columns of the table as undefined.
 *     Note: A candlestick chart has no columns.
 * @protected
 * @override
 * @nosideeffects
 */
kclv.Table.Materials.Candlestick.prototype.buildColumns = function() {
    return;
};

/**
 * Build rows of the table from its own relation object.
 *     Note: Tuples of a two-dimensional relation array must be sorted
 *     ascendingly by an attribute (an element of a tuple) of index 0
 *     (representing {@code Date}).
 * @protected
 * @override
 * @returns {Array.<Array>} Rows representing candlesticks (period and values:
 *     low, opening, closing and high value).
 * @nosideeffects
 */
kclv.Table.Materials.Candlestick.prototype.buildRows = function() {
    this.ensureTupleExisted();

    var indexOfValue = this.relation.getIndexOf(this.kind),
        indexOfDateTime = this.relation.getIndexOf('DateTime'),
        frequency = this.option,
        table = [];

    table.push( this.relation.reduce( function(row, tuple) {
        var current =
                new kclv.Date(tuple[indexOfDateTime]).toPeriod(frequency),
            value = tuple[indexOfValue];

        if (value === null) {
            return row; // Interpolate null value for "Sandanshiki Kanpan".
        }

        if ( row && row[0].f === current.f ) { // Staying the same period.
            // Overwrite values when it was needed.
            if ( row[1] > value ) { row[1] = value; } // Low value.
            row[3] = value;                           // Closing value.
            if ( row[4] < value ) { row[4] = value; } // High value.
        } else { // Entered a new period.
            if (row) { // Not the first row. (row !== null)
                table.push(row); // Place the previous values with the table.
            }
            // Initialize period and values (Low, Opening, Closing and High).
            row = [ current, value, value, value, value ];
        }

        return row;
    }, null ) ); // The last row was placed at the end.

    return table;
};

/**
 * Build a title of the table and get it.
 * @protected
 * @override
 * @returns {string} A title of the table.
 * @nosideeffects
 */
kclv.Table.Materials.Candlestick.prototype.buildTitle = function() {
    var configuration = kclv.Configuration.get([
        'legend', this.locale, kclv.Game.Materials.getKindOf(this.kind)
    ]);

    return configuration.title + ' ' +
           this.formatter.parenthesize( configuration[this.kind] );
};

// ----------------------------------------------------------------
// Material Table Object to Draw a Line Chart
// ----------------------------------------------------------------

/**
 * A material table to draw a line chart.
 * @public
 * @constructor
 * @extends {kclv.Table.Base}
 * @implements {kclv.TableLike}
 * @param {kclv.Relation.Base} relation A relation object as a data source of
 *     the table.
 * @param {string} kind A target kind of building the table.
 */
kclv.Table.Materials.Line = function(relation, kind) {
    /**
     * An opposing target kind.
     * @public {string=}
     * @const
     */
    this.opposite =
        kind === 'Resources' &&
        kclv.Configuration.get('chart.Resources.withRepair', true) ?
            'Repair' : null;

    // Note: Lazily initializate it! (for opposite)
    kclv.Table.Materials.Base.call(this, relation, kind);

    return;
};
kclv.Table.Materials.Line.prototype =
    Object.create(kclv.Table.Materials.Base.prototype);
kclv.Table.Materials.Line.prototype.constructor = kclv.Table.Materials.Base;

/**
 * Ensure the specified directive is valid, otherwise throw an exception.
 * @protected
 * @override
 * @throws {kclv.Exception.InvalidDirective} If the specified directive is
 *     invalid.
 * @nosideeffects
 */
kclv.Table.Materials.Line.prototype.ensureDirectiveValidated = function() {
    kclv.Table.Base.prototype.ensureDirectiveValidated.call(
        this,
        Object.keys(kclv.Game.Materials.MATERIALS_OF).
            concat(kclv.Game.Materials.MATERIALS),
        [null]
    );

    return;
};

/**
 * Build columns of the table from legends of material kinds.
 * @protected
 * @override
 * @returns {Object.<(string|number|Object.<string, (string|number)>)>}
 *     Columns representing date and legends of material kinds.
 * @nosideeffects
 */
kclv.Table.Materials.Line.prototype.buildColumns = function() {
    var legend = kclv.Configuration.get(['legend', this.locale]),
        header = [ legend.dateTime ],
        MATERIALS_OF = kclv.Game.Materials.MATERIALS_OF;

    if ( MATERIALS_OF[this.kind] ) {
        header = header.concat(
            MATERIALS_OF[this.kind].map( function(material) {
                return legend[this.kind][material];
            }, this )
        );
    } else {
        header.push(
            // It may throw an exception (Error).
            legend[ kclv.Game.Materials.getKindOf(this.kind) ][this.kind]
        );
    }

    if (this.opposite) {
        header.push(
            legend[kclv.Game.Materials.getKindOf(this.opposite)][this.opposite]
        );
    }

    return header;
};

/**
 * Build rows of the table from its own relation object.
 *     Note: JScript's Date#toLocaleString() doesn't accept locales.
 *     Note: JScript/JavaScript does'nt have locale setter/getter.
 * @protected
 * @override
 * @returns {Array.<Array>} Rows representing parts (points) of a lines (date
 *     and values of specified materials).
 * @nosideeffects
 */
kclv.Table.Materials.Line.prototype.buildRows = function() {
    this.ensureTupleExisted();

    var indexOfDateTime = this.relation.getIndexOf('DateTime');

    // Note: Use a cloned object to avoid destruct (project) a relation array.
    return this.relation.clone().
        project(
            this.relation.getIndicesOf(this.kind, true, this.opposite)
        ).
        map( function(tuple) {
            tuple[indexOfDateTime] = {
                v : tuple[indexOfDateTime].toJSON(),
                f : tuple[indexOfDateTime].toLocaleString()
            };
            return tuple;
        } );
};