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);
}
}