TargetProcess/tauCharts

View on GitHub
src/charts/tau.gpl.ts

Summary

Maintainability
C
1 day
Test Coverage
import {Emitter} from '../event';
import * as utils from '../utils/utils';
import {FramesAlgebra} from '../algebra';
import {DataFrame} from '../data-frame';
import {
    DataFilter,
    DataFrameObject,
    DataSources,
    DataTransformations,
    UnitGuide,
    Expression,
    GPLSpec,
    GrammarElement,
    GrammarModel,
    GrammarRule,
    ScaleConfig,
    ScaleFactoryMethod,
    Unit
} from '../definitions';
import {ScalesFactory} from '../scales-factory';
import {unitsRegistry} from '../units-registry';
import {GrammarRegistry as GrammarRules} from '../grammar-registry';
type UnitsRegistry = typeof unitsRegistry;
type GrammarRegistry = typeof GrammarRules;

type Iteratee = (node: Unit, parentNode: Unit, currFrame: DataFrame) => void;

var cast = (v) => (utils.isDate(v) ? v.getTime() : v);

const MixinModel: new (prev: GrammarModel) => GrammarModel = <any>function (prev: GrammarModel) {
    Object
        .keys(prev)
        .forEach((k) => this[k] = prev[k]);
};

const compose = (prev: GrammarModel, updates: GrammarModel = {}) => {
    return (Object.assign(new MixinModel(prev), updates));
};

const evalGrammarRules = (
    grammarRules: (GrammarRule | string)[],
    initialGrammarModel: GrammarModel,
    grammarRegistry: GrammarRegistry
) => {
    return grammarRules
        .map((rule) => {
            return ((typeof(rule) === 'string') ? grammarRegistry.get(rule) : rule);
        })
        .filter(x => x)
        .reduce((prevModel, rule) => compose(prevModel, rule(prevModel, {})), initialGrammarModel);
};

export class GPL extends Emitter {

    config: GPLSpec;
    sources: DataSources;
    scales: {[scale: string]: ScaleConfig};
    unitSet: UnitsRegistry;
    grammarRules: GrammarRegistry;
    scalesHub: ScalesFactory;
    transformations: DataTransformations;
    root: Unit;

    constructor(
        config: GPLSpec,
        scalesRegistryInstance: ScalesFactory,
        unitsRegistry: UnitsRegistry,
        grammarRules: GrammarRegistry
    ) {

        super();

        // jscs:disable
        utils.defaults(config.scales, {
            'size_null': {type: 'size', source: '?'},
            'split_null': {type: 'value', source: '?'},
            'label_null': {type: 'value', source: '?'},
            'color_null': {type: 'color', source: '?'},
            'identity_null': {type: 'identity', source: '?'},
            'size:default': {type: 'size', source: '?'},
            'color:default': {type: 'color', source: '?'},
            'split:default': {type: 'value', source: '?'},
            'label:default': {type: 'value', source: '?'},
            'identity:default': {type: 'identity', source: '?'}
        });
        // jscs:enable

        config.settings = (config.settings || {});

        this.config = config;
        this.sources = config.sources;
        this.scales = config.scales;
        this.unitSet = unitsRegistry;
        this.grammarRules = grammarRules;
        this.scalesHub = scalesRegistryInstance;

        this.transformations = Object.assign(
            config.transformations || {},
            {
                where(data, tuple) {
                    var predicates = Object.keys(tuple || {}).map((k) => {
                        return (row) => (cast(row[k]) === tuple[k]);
                    });
                    return data.filter((row) => {
                        return predicates.every((p) => p(row));
                    });
                }
            });
    }

    static traverseSpec(
        spec: {unit: Unit},
        enter: Iteratee,
        exit: Iteratee,
        rootNode: Unit = null,
        rootFrame: DataFrame = null
    ) {

        var queue: (() => void)[] = [];

        var traverse = (node: Unit, enter: Iteratee, exit: Iteratee, parentNode: Unit, currFrame: DataFrame) => {

            queue.push(() => {
                enter(node, parentNode, currFrame);
            });

            if (node.frames) {
                node.frames.forEach((frame) => {
                    (frame.units || []).map((subNode) => traverse(subNode, enter, exit, node, frame));
                });
            }

            queue.push(() => exit(node, parentNode, currFrame));
        };

        traverse(spec.unit, enter, exit, rootNode, rootFrame);

        return queue;
    }

    unfoldStructure() {
        this.root = this._expandUnitsStructure(this.config.unit);
        return this.config;
    }

    getDrawScenarioQueue(root: GrammarElement) {
        const grammarRules = this.grammarRules;
        var scaleInfoQueue = this._flattenDrawScenario(root, (parentInstance, unit, rootFrame) => {
            // Rule to cancel parent frame inheritance
            const frame = (unit.expression.inherit === false) ? null : rootFrame;
            const scalesFactoryMethod = this._createFrameScalesFactoryMethod(frame);
            const instance = this.unitSet.create(
                unit.type,
                Object.assign(
                    {},
                    (unit),
                    {options: parentInstance.allocateRect(rootFrame.key)}
                ));

            const initialModel = new MixinModel(instance.defineGrammarModel(scalesFactoryMethod));
            const grammarModel = evalGrammarRules(instance.getGrammarRules(), initialModel, grammarRules);
            evalGrammarRules(instance.getAdjustScalesRules(), grammarModel, grammarRules);
            instance.node().screenModel = instance.createScreenModel(grammarModel);

            return instance;
        });

        var createScales = (() => {
            Object
                .keys(this.scales)
                .forEach((k) => this.scalesHub.createScaleInfo(this.scales[k]).commit());
        });

        var updateScalesQueue = this._flattenDrawScenario(root, (parentInstance, unit, rootFrame) => {
            const frame = (unit.expression.inherit === false) ? null : rootFrame;
            const scalesFactoryMethod = this._createFrameScalesFactoryMethod(frame);
            const instance = this.unitSet.create(
                unit.type,
                Object.assign(
                    {},
                    (unit),
                    {options: parentInstance.allocateRect(rootFrame.key)}
                ));

            const initialModel = new MixinModel(instance.defineGrammarModel(scalesFactoryMethod));
            const grammarModel = evalGrammarRules(instance.getGrammarRules(), initialModel, grammarRules);
            instance.node().screenModel = instance.createScreenModel(grammarModel);
            instance.parentUnit = parentInstance;
            instance.addInteraction();

            return instance;
        });

        return scaleInfoQueue
            .concat(createScales)
            .concat(updateScalesQueue);
    }

    _flattenDrawScenario(
        root: GrammarElement,
        iterator: (parentInstance: GrammarElement, unit: Unit, rootFrame: DataFrame) => GrammarElement
    ) {

        var uids = {};
        var scenario: GrammarElement[] = [];

        var stack = [root];

        var put = ((x) => stack.unshift(x));
        var pop = (() => stack.shift());
        var top = (() => stack[0]);

        var queue = GPL.traverseSpec(
            {unit: this.root},
            // enter
            (unit, parentUnit, currFrame) => {

                unit.uid = (() => {
                    var uid = utils.generateHash(
                        (parentUnit ? `${parentUnit.uid}/` : '') +
                        JSON.stringify(Object.keys(unit)
                            .filter((key) => typeof unit[key] === 'string')
                            .reduce((memo, key) => (memo[key] = unit[key], memo), {})) +
                        `-${JSON.stringify(currFrame.pipe)}`);
                    if (uids.hasOwnProperty(uid)) {
                        uid += `-${++uids[uid]}`;
                    } else {
                        uids[uid] = 0;
                    }
                    return uid;
                })();
                unit.guide = utils.clone(unit.guide);

                var instance = iterator(top(), unit, currFrame);

                scenario.push(instance);

                if (unit.type.indexOf('COORDS.') === 0) {
                    put(instance);
                }
            },
            // exit
            (unit) => {
                if (unit.type.indexOf('COORDS.') === 0) {
                    pop();
                }
            },
            null,
            this._datify({
                source: this.root.expression.source,
                pipe: []
            }));

        queue.push(() => scenario);

        return queue;
    }

    _expandUnitsStructure(root: Unit, parentPipe: DataFilter[] = []) {

        var self = this;

        if (root.expression.operator === false) {

            root.frames = root.frames.map((f) => self._datify(f));

        } else {

            var expr = this._parseExpression(root.expression, parentPipe, root.guide);

            root.transformation = root.transformation || [];

            root.frames = expr.exec().map((tuple) => {

                var flow = (expr.inherit ? parentPipe : []);
                var pipe = (flow)
                    .concat([{type: 'where', args: tuple}])
                    .concat(root.transformation);

                return self._datify({
                    key: tuple,
                    pipe: pipe,
                    source: expr.source,
                    units: (root.units) ?
                        root.units.map((unit) => {
                            var clone = utils.clone(unit);
                            // pass guide by reference
                            clone.guide = unit.guide;
                            return clone;
                        }) :
                        []
                });
            });
        }

        root.frames.forEach(
            (f) => (f.units.forEach(
                (unit) => this._expandUnitsStructure(unit, f.pipe)
            ))
        );

        return root;
    }

    _createFrameScalesFactoryMethod(passFrame: DataFrame) {
        var self = this;
        return <ScaleFactoryMethod>((type, alias, dynamicProps) => {
            var key = (alias || `${type}:default`);
            return self
                .scalesHub
                .createScaleInfo(self.scales[key], passFrame)
                .create(typeof dynamicProps === 'function' ? dynamicProps(self.scales[key]) : dynamicProps);
        });
    }

    _datify(frame: DataFrameObject) {
        return new DataFrame(frame, this.sources[frame.source].data, this.transformations);
    }

    _parseExpression(expr: Expression, parentPipe: DataFilter[], guide: UnitGuide) {

        var funcName = expr.operator || 'none';
        var srcAlias = expr.source;
        var bInherit = expr.inherit !== false; // true by default
        var funcArgs = expr.params;

        var frameConfig = {
            source: srcAlias,
            pipe: bInherit ? parentPipe : []
        };

        var dataFn = () => this._datify(frameConfig).part();

        var func = FramesAlgebra[funcName];

        if (!func) {
            throw new Error(`${funcName} operator is not supported`);
        }

        return {
            source: srcAlias,
            inherit: bInherit,
            func: func,
            args: funcArgs,
            exec: () => func(dataFn, ...(funcArgs || []), guide)
        };
    }
}