Source: visual-group/src/group-helper/cell-creator.js

import { AxisOrientation } from '@chartshq/muze-axis';
import { getObjProp, FieldType } from 'muze-utils';
import { getMatrixModel } from './matrix-model';
import {
    getCellKey,
    isDistributionEqual,
    mutateAxesFromMap,
    createSelection,
    getFieldsFromSuppliedLayers,
    extractFields
} from './group-utils';
import { ROW, ROWS, COLUMNS, COL, LEFT, RIGHT, TOP, BOTTOM, PRIMARY, SECONDARY, X, Y } from '../enums/constants';

/**
 * Updates row and column cells with the geom cell corresponding to the facet keys
 *
 * @param {*} resolver
 * @param {*} facets
 */
const updateCells = (resolver, facets, geomCell) => {
    [ROW, COL].forEach((field) => {
        const cells = resolver[`${field}Cells`]();
        const facetKey = facets[`${field}Facets`][1].join();

        !cells[facetKey] && (cells[facetKey] = []);
        cells[facetKey].push(geomCell);
        resolver[`${field}Cells`](cells);
    });
};

/**
 *
 *
 * @param {*} context
 * @param {*} datamodel
 * @param {*} fieldInfo
 * @param {*} facets
 *
 */
export const createValueCells = (context, datamodel, fieldInfo, facets) => {
    const {
        projections,
        indices
    } = fieldInfo;
    const {
        rowFields,
        columnFields
    } = projections;
    const {
         rowIndex,
         columnIndex
    } = indices;
    const {
        suppliedLayers,
        cell: GeomCell,
        resolver,
        config,
        encoder,
        detailFields
    } = context;
    const axes = resolver.axes();
    const cacheMaps = resolver.cacheMaps();
    const matrixLayers = resolver.matrixLayers();
    const labelManager = resolver.dependencies().smartlabel;
    const horizontalAxis = resolver.horizontalAxis();
    const verticalAxis = resolver.verticalAxis();
    const datamodelTransform = resolver.datamodelTransform();
    const {
        entryCellMap,
        exitCellMap
    } = cacheMaps;
    const layerConfigArr = encoder.getLayerConfig({ columnFields, rowFields }, suppliedLayers || []);
    const axesCreators = { config, labelManager, axes, cacheMaps };

    fieldInfo.normalizedColumns = verticalAxis.fields;
    fieldInfo.normalizedRows = horizontalAxis.fields;

    const groupAxes = encoder.createAxis(axesCreators, fieldInfo);

    matrixLayers[rowIndex] = matrixLayers[rowIndex] ? matrixLayers[rowIndex] : [];
    matrixLayers[rowIndex][columnIndex] = layerConfigArr;

    // return from map if already there otherwise create and put in map
    const geomCellKey = getCellKey(rowIndex, columnIndex);
    const fields = {
        y: rowFields,
        x: columnFields
    };
    const allFacets = [
        [...facets.rowFacets[0], ...facets.colFacets[0]],
        [...facets.rowFacets[1], ...facets.colFacets[1]]
    ];
    const geomCell = !exitCellMap.has(geomCellKey) ? new GeomCell() : exitCellMap.get(geomCellKey);

    geomCell.data(datamodel)
                    .axes(groupAxes)
                    .fields(fields)
                    .transform(datamodelTransform)
                    .detailFields(detailFields)
                    .facetByFields(allFacets);
    entryCellMap.set(geomCellKey, geomCell);
    exitCellMap.delete(geomCellKey);

    updateCells(resolver, facets, geomCell);

    return entryCellMap.get(geomCellKey);
};

/**
 * Creates axis cells based on the set of axes
 *
 * @param {Selection} selection Contains a selection of the axis units
 * @param {Array} axes Actual axis units
 * @param {number} axisIndex 0-> primary axis, 1-> secondary axis(when dual axis is made)
 * @param {Object} cells Contains a collection of the cells
 * @return {Object} return either set of axis/blank cells depending on the config
 */

const createAxisCells = (selection, axes, axisIndex, cells) =>
    createSelection(selection, axis => axis, axes, (item, i) => i + item.reduce((e, n) => {
        const id = n.id + axisIndex;
        return e + id;
    }, '')).map((axis) => {
        if (axis && axis[axisIndex]) {
            const axisInst = axis[axisIndex];
            const { orientation, show } = axisInst.config();

            return new cells.AxisCell().source(axisInst).config({
                isOffset: orientation === AxisOrientation.LEFT || orientation === AxisOrientation.TOP,
                show
            });
        }
        return new cells.BlankCell().config({ show: false });
    });

/**
 *
 *
 * @param {*} context
 * @param {*} selectionObj
 * @param {*} cells
 * @retur
 */
const axisPlaceholderGn = (context, selObj, cells) => {
    const {
        matrices
    } = context;
    const {
        axesMatrix
    } = matrices;

    return (type, axisFrom) => {
        const axes = axesMatrix[`${type}`];

        if (axes && axes.length) {
            if (type === X || type === Y) {
                const fieldNames = type === Y ? ROWS : COLUMNS;

                [PRIMARY, SECONDARY].forEach((fieldType, index) => {
                    const selObjProp = `${fieldNames}${fieldType}`;
                    let axisIndex = index;
                    let axesForDraw = axes;
                    if (axisFrom === RIGHT || axisFrom === BOTTOM) {
                        axisIndex = 1 - axisIndex;
                    }
                    if (!getObjProp(axes, 0, axisIndex)) {
                        axesForDraw = [];
                    }
                    selObj[selObjProp] = createAxisCells(selObj[selObjProp], axesForDraw, axisIndex, cells);
                });
            } else {
                selObj.rowsPrimary = createAxisCells(selObj.rowPrime, axes.map(() => []), 0, cells);
                selObj.columnsPrimary = createAxisCells(selObj.colPrime, axes[0], 0, cells);
            }
        }
        return selObj;
    };
};

/**
 * Creates header cells based on the set of headers
 *
 * @param {Object} selection Contains a selection of the header units
 * @param {string} headers Contains a list of the headers
 * @param {Object} cells Contains a collection of the cells
 * @param {Object} labelManager smart label instance
 * @return {Object} return either set of header cells depending on the config
 */
const createTextCells = (selection, headers, cells, labelManager) => createSelection(selection,
    label => new cells.TextCell({}, { labelManager }).source(label), headers, (key, i) => key + i);

/**
 *
 *
 * @param {*} context
 * @param {*} selectionObj
 * @param {*} cells
 * @param {*} labelManager
 *
 */
const headerPlaceholderGn = (context, selectionObj, cells, labelManager) => {
    const {
        axis,
        keys,
        type,
        facetConfig
    } = context;
    const counter = axis.length / keys.length;
    const selectionKeys = keys.length ? axis.map((d, i) => keys[Math.floor(i / counter)]) : [];
    return createSelection(selectionObj[`${type}Headers`], keySet => keySet, selectionKeys, (keySet, i) =>
        `${keySet.join(',')}-${i}`).map(keySet => createTextCells(null, keySet, cells, labelManager)
                        .map((cell, k, i) => cell.source(keySet[i]).config(facetConfig || {})));
};

/**
 * Creates a set of placeholders as a part of selection object
 *
 * @param {Array} normalizedOptions contains normalized rows and columns
 * @param {Array} matrices contains axis and value matrices
 * @param {aArrayny} projections contains set of row and column projections
 * @param {Object} cells Contains a collection of the cells
 * @param {Object} labelManager smart label instance
 * @return {Object} Creates a set of placeholders as a part of selections
 */
const generatePlaceholders = (context, cells, labelManager) => {
    let selectionObj;
    const {
        matrices,
        fields,
        facetsAndProjections,
        selection,
        facetConfig,
        encoders
    } = context;
    const {
        rows,
        columns
    } = fields;
    const {
        valuesMatrix
    } = matrices;
    const {
        rowProjections,
        colProjections
    } = facetsAndProjections;
    const {
        rowKeys,
        columnKeys
    } = valuesMatrix;
    const takeAxisFrom = encoders.simpleEncoder._axisFrom;
    const takeHeaderFrom = encoders.simpleEncoder._headerFrom;

    selectionObj = selection || {};

    ['pie', X, Y].forEach((axis) => {
        const axisFrom = axis === X ? takeAxisFrom.column : takeAxisFrom.row;
        selectionObj = axisPlaceholderGn(context, selectionObj, cells)(axis, axisFrom);
    });

    const {
        rowsPrimary,
        rowsSecondary,
        columnsPrimary,
        columnsSecondary
    } = selectionObj;

    const rowAxis = rowsPrimary && rowsPrimary.getObjects().length ? rowsPrimary.getObjects() :
        (rowsSecondary && rowsSecondary.getObjects().length ? rowsSecondary.getObjects() : []);
    const colAxis = columnsPrimary && columnsPrimary.getObjects().length ? columnsPrimary.getObjects() :
        (columnsSecondary && columnsSecondary.getObjects().length ? columnsSecondary.getObjects() : []);

    const headerConfig = [
        { type: LEFT, section: rows[0], axis: rowAxis, headerFrom: takeHeaderFrom.row },
        { type: RIGHT, section: rows[1], axis: rowAxis, headerFrom: takeHeaderFrom.row },
        { type: TOP, section: columns[0], axis: colAxis, headerFrom: takeHeaderFrom.column },
        { type: BOTTOM, section: columns[1], axis: colAxis, headerFrom: takeHeaderFrom.column }
    ];

    headerConfig.forEach((config, index) => {
        let keys;
        let length;
        const {
            type,
            section,
            axis,
            headerFrom
        } = config;

        if (index < 2) {
            keys = rowKeys;
            length = rowProjections.length > 0 ? rowProjections.length : 1;
        } else {
            keys = columnKeys;
            length = colProjections.length > 0 ? colProjections.length : 1;
        }

        if (section.length && headerFrom === type && axis && keys.length) {
            const hContext = { axis, length, type };
            let headers = [];
            if (index < 2) {
                hContext.keys = keys;
                hContext.facetConfig = facetConfig.rows;
                headers = headerPlaceholderGn(hContext, selectionObj, cells, labelManager);
            } else {
                hContext.facetConfig = facetConfig.columns;
                hContext.keys = keys[0].map((key, i) => keys.map(e => e[i]));
                headers = headerPlaceholderGn(hContext, selectionObj, cells, labelManager);
            }
            selectionObj[`${type}Headers`] = headers;
        } else {
            selectionObj[`${type}Headers`] = null;
        }
    });
    return selectionObj;
};

/**
 * Generates matrices
 *
 * @param {Object} config Configuration to generate matrices
 * @param {Array} matrices Matrices containing the set of visual units and axes units
 * @param {Object} cells Contains a collection of the cells
 * @param {Object} labelManager smart label instance
 * @return {Object} contains a collection of matrices
 */
export const generateMatrices = (context, matrices, cells, labelManager) => {
    const {
        unitHeight,
        unitWidth,
        facetsAndProjections,
        normalizedRows,
        normalizedColumns,
        selection,
        axisFrom,
        facetConfig,
        encoders
     } = context;
    const placeholderContext = {
        fields: {
            rows: normalizedRows,
            columns: normalizedColumns
        },
        matrices,
        facetsAndProjections,
        selection,
        axisFrom,
        facetConfig,
        encoders
    };
    // Generate placeholders for all matrices
    const selectionObj = generatePlaceholders(placeholderContext, cells, labelManager);
    const {
        columnsPrimary,
        columnsSecondary,
        rowsPrimary,
        rowsSecondary,
        leftHeaders,
        topHeaders,
        bottomHeaders,
        rightHeaders
    } = selectionObj;
    const [rowPrime, rowSec, colPrime, colSec] = [rowsPrimary, rowsSecondary, columnsPrimary, columnsSecondary]
        .map(d => (d ? d.getObjects() : []));
    const [leftFacets, rightFacets] = [leftHeaders, rightHeaders].map(e => (e ? e.getObjects()
                    .map(f => f.getObjects()) : []));

    // Compute left matrix using left headers and the axes on the rows
    let leftMatrix = leftFacets.length ? leftFacets.map((d, i) => {
        rowPrime[i] = rowPrime[i] ? [rowPrime[i]] : [];
        return [...d, ...rowPrime[i]];
    }) : (rowPrime ? rowPrime.map(d => [d]) : []);

    // Compute right matrix using right headers and the axes on the rows
    const rightMatrix = rowSec.length ? rowSec.map((d, i) => [d, ...(rightFacets[i] || [])]) : (rightFacets.length ?
        rightFacets.map(d => [...d]) : []);

    const topMatrix = [];
    if (topHeaders) {
        const headers = topHeaders.getObjects();
        headers.forEach((e) => {
            const innerHeaders = e.getObjects();
            innerHeaders.forEach((x, i) => {
                topMatrix[i] = topMatrix[i] || [];
                topMatrix[i].push(x);
            });
        });
    }
    // Compute top matrix using the top headers and axes on the columns
    if (colPrime.length) {
        topMatrix.push(colPrime);
    }

    // Bottom and right matrices are prepared using the user config.
    let bottomMatrix = [];
    if (colSec.length) {
        bottomMatrix.push(colSec);
    }
    const currentBottomLength = bottomMatrix.length;
    if (bottomHeaders) {
        const headers = bottomHeaders.getObjects();
        headers.forEach((e) => {
            const innerHeaders = e.getObjects();
            innerHeaders.forEach((x, i) => {
                bottomMatrix[i + currentBottomLength] = bottomMatrix[i + currentBottomLength] || [];
                bottomMatrix[i + currentBottomLength].push(x);
            });
        });
    }

    if (!leftMatrix.length && !rightMatrix.length) {
        const cell = new cells.BlankCell();
        cell.setAvailableSpace(unitWidth, unitHeight);
        leftMatrix = [[cell]];
    }

    if (!topMatrix.length && (!bottomMatrix.length || !bottomMatrix[0].length)) {
        const cell = new cells.BlankCell();
        cell.setAvailableSpace(unitWidth, unitHeight);
        bottomMatrix = [[cell]];
    }

    return {
        rows: [leftMatrix, rightMatrix],
        columns: [topMatrix, bottomMatrix],
        selectionObj
    };
};

/**
 * Computes matrices for a group
 *
 * @param {Object} datamodel on which the matrices are to be computed
 * @param {Object} config configuration of the matrices
 * @param {Object} layerRegistry contains the registered layers
 * @return {Object} conputed matrices
 * @memberof MatrixResolver
 */
export const computeMatrices = (context, config) => {
    const {
        resolver,
        datamodel,
        componentRegistry,
        encoders
    } = context;
    const {
            globalConfig,
            selection,
            transform
        } = config;
    const groupBy = globalConfig.autoGroupBy;
    const { smartlabel: labelManager } = resolver.dependencies();
    const fieldMap = datamodel.getFieldsConfig();
    const layerConfig = resolver.layerConfig();
    const registry = resolver.registry();
    const { fields: normalizedRows } = resolver.horizontalAxis();
    const { fields: normalizedColumns } = resolver.verticalAxis();
    const otherEncodings = resolver.optionalProjections(config, layerConfig);
    const facetsAndProjections = resolver.getAllFields();
    const matrixGnContext = {
        // Configuration to be passed to generate the  different matrices.
        // A common config is used for both value matrices and other matrices
        normalizedColumns,
        normalizedRows,
        facetsAndProjections,
        layers: layerConfig,
        fieldMap,
        otherEncodings,
        encoders,
        facetConfig: globalConfig.facetConfig || {},
        axisFrom: globalConfig.axisFrom || {},
        selection
    };
    const cells = {
        GeomCell: resolver.getCellDef(registry.GeomCell),
        AxisCell: resolver.getCellDef(registry.AxisCell),
        BlankCell: resolver.getCellDef(registry.BlankCell),
        TextCell: resolver.getCellDef(registry.TextCell)
    };
    const isRowSizeEqual = isDistributionEqual(normalizedRows);
    const isColumnSizeEqual = isDistributionEqual(normalizedColumns);

    resolver.colCells({});
    resolver.rowCells({});
    resolver.datamodelTransform(transform || {});

    // Cell creation begins here
    resolver.resetSimpleAxes();

    const {
            entryCellMap
        } = resolver.cacheMaps();
    const newCacheMap = {
        exitCellMap: entryCellMap,
        entryCellMap: new Map()
    };

    resolver.cacheMaps(newCacheMap);

    const valueCellContext = {
        config: globalConfig,
        suppliedLayers: encoders.simpleEncoder.serializeLayerConfig(resolver.layerConfig()),
        resolver,
        cell: cells.GeomCell,
        encoder: encoders.simpleEncoder,
        newCacheMap,
        detailFields: config.detail
    };
    const fieldsConfig = datamodel.getFieldsConfig();
    let groupedModel = datamodel;
    if (!groupBy.disabled) {
        const fields = getFieldsFromSuppliedLayers(valueCellContext.suppliedLayers, datamodel.getFieldsConfig());
        const allFields = extractFields(facetsAndProjections, fields);

        const dimensions = allFields.filter(field =>
            fieldsConfig[field] && fieldsConfig[field].def.type === FieldType.DIMENSION);
        const aggregationFns = groupBy.measures;

        groupedModel = datamodel.groupBy(dimensions.length ? dimensions : [''], aggregationFns).project(allFields);
    }

    // return a callback function to create the cells from the matrix
    const cellCreator = resolver.valueCellsCreator(valueCellContext);
    // Creates value matrices from the datamodel and configs
    const valueMatrixInfo = getMatrixModel(groupedModel, facetsAndProjections, cellCreator);

    resolver.cacheMaps().exitCellMap.forEach((placeholder) => {
        placeholder.remove();
    });
    resolver.cacheMaps().exitCellMap.clear();
    resolver.valueMatrix(valueMatrixInfo.matrix);
    resolver.createUnits(componentRegistry, config);

    const { xAxes, yAxes } = mutateAxesFromMap(resolver.cacheMaps(), resolver.axes());

    resolver.axes({
        x: xAxes,
        y: yAxes
    });

    const matrices = {
        valuesMatrix: valueMatrixInfo,
        axesMatrix: resolver.axes()
    };
    // Create all matrices
    const { rows, columns, selectionObj } = generateMatrices(matrixGnContext, matrices, cells, labelManager);

    resolver.rowMatrix(rows);
    resolver.columnMatrix(columns);

    return {
        rows: resolver.rowMatrix(),
        columns: resolver.columnMatrix(),
        values: resolver.valueMatrix(),
        isColumnSizeEqual,
        isRowSizeEqual,
        selection: selectionObj,
        dataModels: {
            groupedModel,
            parentModel: datamodel
        }
    };
};