Source: muze/src/canvas/canvas.js

import { GridLayout } from '@chartshq/layout';
import { transactor, Store, getUniqueId } from 'muze-utils';
import { RETINAL } from '../constants';
import TransactionSupport from '../transaction-support';
import { getRenderDetails, prepareLayout } from './layout-maker';
import { localOptions, canvasOptions } from './local-options';
import { renderComponents } from './renderer';
import GroupFireBolt from './firebolt';
import options from '../options';
import { initCanvas, setupChangeListener } from './helper';

/**
 * Canvas is a logical component which houses a visualization by taking multiple variable in different encoding channel.
 * Canvas manages lifecycle of many other logical component and exposes one consistent interface for creation of chart.
 * Canvas is intialized from environment with settings from environment and singleton dependencies.
 *
 * To create an instance of canvas
 * ```
 *  const env = Muze();
 *  const canvas = env.canvas()
 * ```
 *
 *
 * @class
 * @public
 * @module Canvas
 */
export default class Canvas extends TransactionSupport {

    /**
     * Creates reactive property accessors.
     * - data
     * - height
     * - width
     * - config
     * This configs are retrieved from options.
     */
    constructor (globalDependencies) {
        super();

        this._allOptions = Object.assign({}, options, localOptions);
        this._registry = {};
        this._composition = {};
        this._cachedProps = {};
        this._alias = null;
        this._renderedResolve = null;
        this._renderedPromise = new Promise((resolve) => {
            this._renderedResolve = resolve;
        });
        this._composition.layout = new GridLayout();
        this._store = new Store({});

        // Setters and getters will be mounted on this. The object will be mutated.
        const [, store] = transactor(this, options, this._store.model);
        transactor(this, localOptions, store);
        transactor(this, canvasOptions, store);
        this.dependencies(Object.assign({}, globalDependencies, this._dependencies));
        this.firebolt(new GroupFireBolt(this));
        this.alias(`canvas-${getUniqueId()}`);
        this.title('', {});
        this.subtitle('', {});
        this.legend({});
        this.color({});
        this.shape({});
        this.size({});
        setupChangeListener(this);
    }

    /**
     * Retrieves an instance of layout which is responsible for layouting. Layout is responsible for creating faceted
     * presentation using table layout.
     *
     * @public
     *
     * @return {GridLayout} Instance of layout attached to canvas.
     */
    layout (...params) {
        if (params.length) {
            return this;
        }
        return this.composition().layout;
    }

    /**
     * Retrieves the composition for a canvas
     *
     * @public
     *
     * @return {object} Instances of the components which canvas requires to draw the full visualization.
     *      ```
     *          {
     *              layout: // Instance of {@link GridLayout}
     *              legend: // Instance of {@link Legend}
     *              subtitle: // Instance of {@link TextCell} using which the title is rendered
     *              title: // Instance of {@link TextCell} using which the title is rendered
     *              visualGroup: // Instance of {@link visualGroup}
     *          }
     *      ```
     */
    composition (...params) {
        if (params.length) {
            return this;
        }
        return this._composition;
    }

    done () {
        return this._renderedPromise;
    }

    /**
     * Sets or gets the alias of the canvas. Alias is a name by which the canvas can be referred.
     *
     * When setter
     * @param {string} alias Name of the alias.
     *
     * @return {Canvas} Instance of the canvas.
     *
     * When getter
     *
     * @return {string} Alias of canvas.
     *
     * @public
     */
    alias (...params) {
        if (params.length) {
            const visualGroup = this.composition().visualGroup;
            this._alias = params[0];
            visualGroup && visualGroup.alias(this.alias());
            return this;
        }
        return this._alias;
    }

    /**
     * Creates an instance initiated with given settings.
     *
     * @param {Object} initialSettings Initial settings to be populated in the model
     * @param {Object} regEntry newly created instance with the initial settings
     * @param {Object} globalDependencies dependencies which will be created only once in the page
     *
     * @return {Object} newly created instance with the initial settings
     */
    static withSettings (initialSettings, regEntry, globalDependencies) {
        const instance = new Canvas(globalDependencies);

        for (const key in initialSettings) {
            instance[key](initialSettings[key]);
        }
        // set registry for instance
        instance.registry(regEntry);
        return instance;
    }

    /**
     *
     *
     * @static
     *
     * @memberof Canvas
     */
    static formalName () {
        return 'canvas';
    }

    /**
     * Returns the instance of firebolt associated with this canvas. The firebolt instance can be used to dispatch a
     * behaviour dynamically on the canvas. This firebolt does not handle any physical actions. It is just used to
     * propagate the action to all the visual units in it's composition.
     *
     * @public
     *
     * @return {GroupFireBolt} Instance of firebolt associated with canvas.
     */
    firebolt (...firebolt) {
        if (firebolt.length) {
            this._firebolt = firebolt[0];
            return this;
        }
        return this._firebolt;
    }

    /**
     * Registry peoperty accessor
     *
     * @param {Object} reg plain old javascript object keyvalue pairs. Key containing module name and value contains
     * module definition class. The reg object has to be flat object of level 1.
     */
    registry (...params) {
        if (params.length) {
            const components = Object.assign({}, params[0].components);
            const componentSubRegistry = Object.assign({}, params[0].componentSubRegistry);

            this._registry = { components, componentSubRegistry };
            const initedComponents = initCanvas(this);
            // @todo is it okay to continue this tight behaviour? If not use a resolver to resolve diff component type.
            this._composition.visualGroup = initedComponents[0];

            this.composition().visualGroup.alias(this.alias());
            return this;
        }
        return this._registry;
    }

    /*
     * Prepare dependencies for top level elements
     */
    dependencies (...param) {
        if (param.length) {
            this._dependencies = param[0];
            return this;
        }
        // @todo prepare dependencies here.
        return this._dependencies;
    }

    /**
     *
     *
     * @param {*} lifeCycles
     *
     * @memberof Canvas
     */
    lifeCycle (lifeCycles) {
        const lifeCycleManager = this.dependencies().lifeCycleManager;
        if (lifeCycles) {
            lifeCycleManager.register(lifeCycles);
            return this;
        }
        return lifeCycleManager;
    }

    /**
     *
     *
     * @readonly
     * @memberof Canvas
     */
    legend (...params) {
        if (params.length) {
            return this;
        }
        return this.composition().legend;
    }

    /**
     * Returns a promise for various {@link LifecycleEvents} of the various components of canvas. The promise gets
     * resolved once the particular event gets completed.
     *
     * To use this,
     * ```
     *      canvas.once('layer.drawn').then(() => {
     *          // Do any post drawing work here.
     *      });
     * ```
     * @public
     *
     * @param {string} eventName Name of the lifecycle event.
     *
     * @return {Promise} A pending promise waiting for resolve to be called.
     */
    once (eventName) {
        const lifeCycleManager = this.dependencies().lifeCycleManager;
        return lifeCycleManager.retrieve(eventName);
    }

    /**
     * Internal function to trigger render, this method is cognizant of all the properties of the core modules and
     * establish a passive reactivity. Passive reactivity is not always a bad thing :)
     * @internal
     */
    render () {
        const mount = this.mount();
        const visGroup = this.composition().visualGroup;
        const lifeCycleManager = this.dependencies().lifeCycleManager;
        // Get render details including arrangement and measurement
        const { components, layoutConfig, measurement } = getRenderDetails(this, mount);

        lifeCycleManager.notify({ client: this, action: 'beforedraw' });
        // Prepare the layout by triggering the matrix calculation
        prepareLayout(this.layout(), components, layoutConfig, measurement);
        // Render each component
        renderComponents(this, components, layoutConfig, measurement);
        // Update life cycle
        lifeCycleManager.notify({ client: this, action: 'drawn' });
        const promises = [];
        visGroup.matrixInstance().value.each((el) => {
            promises.push(el.valueOf().done());
        });
        Promise.all(promises).then(() => {
            this._renderedResolve();
        });
    }

    /**
     * Returns the instances of x axis of the canvas. It returns the instances in a two dimensional array form.
     *
     * ```
     *   // The first element in the sub array represents the top axis and the second element represents the bottom
     *   // axis.
     *   [
     *      [X1, X2],
     *      [X3, X4]
     *   ]
     * ```
     * @public
     *
     * @return {Array.<Array>} Instances of x axis.
     */
    xAxes () {
        return this.composition().visualGroup.getAxes('x');
    }

    /**
     * Returns the instances of y axis of the canvas. It returns the instances in a two dimensional array form.
     *
     * ```
     *   // The first element in the sub array represents the left axis and the second element represents the right
     *   // axis.
     *   [
     *      [Y1, Y2],
     *      [Y3, Y4]
     *   ]
     * ```
     * @public
     * @return {Array.<Array>} Instances of y axis.
     */
    yAxes () {
        return this.composition().visualGroup.getAxes('y');
    }

    /**
     * Returns all the retinal axis of the canvas. Color, shape and size axis are combinedly called retinal axis.
     *
     * @public
     * @return {Object} Instances of retinal axis.
     *          ```
     *              {
     *                  color: [ColorAxis], // Array of color axis.
     *                  shape: [ShapeAxis], // Array of shape axis.
     *                  size: [SizeAxis] // Array of size axis.
     *              }
     *          ```
     */
    getRetinalAxes () {
        const visualGroup = this.composition().visualGroup;
        return visualGroup.getAxes(RETINAL);
    }
}