Source: muze/src/canvas/renderer.js

import { makeElement, selectElement } from 'muze-utils';
import { VERTICAL, HORIZONTAL, TOP, LEFT, RIGHT, BOTTOM } from '../constants';

/**
 * Sets the rotation for all x axes if any axis has the rotation config set in the
 * entire view
 *
 * @param {Array} columns Column cells that contain the axes cells
 */
const setLabelRotationForAxes = (context) => {
    let rotation = 0;

    const xAxes = context.xAxes() || [];

    (() => {
        for (let i = 0; i < xAxes.length; i++) {
            for (let j = 0; j < xAxes[i].length; j++) {
                if (xAxes[i][j].config().labels.rotation !== 0) {
                    rotation = xAxes[i][j].config().labels.rotation;
                    return;
                }
            }
        }
    })();

    if (rotation) {
        xAxes.forEach((axes) => {
            axes.forEach((axis) => {
                axis.config({ labels: { rotation, smartTicks: false } });
            });
        });
    }
};

/**
 *
 *
 * @param {*} context
 * @param {*} mount
 *
 */
const getSkeletons = (mount, layoutConfig, measurement) => {
    const {
        layoutArrangement,
        classPrefix
    } = layoutConfig;
    const {
        headers,
        legends
    } = layoutArrangement;
    const {
        canvasWidth,
        canvasHeight
    } = measurement;
    const container = selectElement(mount);
    const components = {};
    const mountPoint = makeElement(container, 'div', [1], `${classPrefix}-viz`)
        .style('width', `${canvasWidth}px`)
        .style('height', `${canvasHeight}px`);
    const containers = mountPoint
        .selectAll(`.${classPrefix}-container`)
        .data(headers);
    containers.exit().remove();
    const containersEnter = containers.enter().append('div');

    const mergedContainer = containersEnter.merge(containers)
                    .attr('class', `${classPrefix}-container`)
                    .style('width', `${canvasWidth}px`)
                    .style('padding', `${null}px`)
                    .style('margin', null)
                    .each(function (type) {
                        components[type] = selectElement(this).classed(`${classPrefix}-${type}-container`, true);
                    });
    const innerContainer = mergedContainer
                    .selectAll(`.${classPrefix}-inner-container`)
                    .data((d) => {
                        if (d === 'group') {
                            return legends;
                        } return [];
                    });
    innerContainer.exit().remove();
    const innerContainerEnter = innerContainer.enter().append('div');

    innerContainerEnter
                    .merge(innerContainer)
                    .attr('class', `${classPrefix}-inner-container`)
                    .style('width', 'auto')
                    .style('height', 'auto')
                    .each(function (layoutType) {
                        components[layoutType] = selectElement(this)
                             .classed(`${classPrefix}-${layoutType}-container`, true);
                    });
    return components;
};

/**
 *
 *
 * @param {*} context
 * @param {*} container
 * @param {*} legendComponents
 */
const renderLegend = (legendConfig, container, legendComponents, measurement) => {
    const sectionComponents = [];
    const { legendSpace, headerHeight, height, width } = measurement;
    const { legend, classPrefix } = legendConfig;
    const { position } = legend;
    const legendMount = makeElement(container, 'div', [legendComponents], `${classPrefix}-inner-content`, {}, d => d);
    legendMount.classed(`${classPrefix}-legend`, true);
    const align = (position === LEFT || position === RIGHT) ? VERTICAL : HORIZONTAL;
    const legWidth = align === VERTICAL ? legendSpace.width : width;
    const legHeight = align === VERTICAL ? height - headerHeight : legendSpace.height;

    [container, legendMount].forEach((elem) => {
        elem.style('width', `${Math.floor(legWidth)}px`)
                        .style('height', `${legHeight}px`)
                        .style('float', LEFT);
    });

    if (align === VERTICAL) {
        let sections = -1;
        let currHeight = legHeight;
        let currWidth = 0;

        legendComponents.forEach((legendInfo) => {
            const leg = legendInfo.legend;
            if (leg.measurement().height > currHeight) {
                sections++;
                currWidth = 0;
                currHeight = legHeight;
            } else {
                sections < 0 && sections++;
            }
            sectionComponents[sections] = sectionComponents[sections] || [];
            currHeight -= Math.min(leg.measurement().height, currHeight);
            currWidth = Math.max(Math.min(leg.measurement().width, leg.measurement().maxWidth), currWidth);
            sectionComponents[sections].push({
                legend: leg,
                legendHeight: legHeight,
                legendWidth: currWidth
            });
        });

        const mount = makeElement(legendMount, ['div'], sectionComponents, `${classPrefix}-legend-section`)
            .each((d, i) => selectElement(this).classed(`${classPrefix}-legend-section-${i}`, true))
            .classed(`${classPrefix}-legend-vertical-section`, true)
            .style('width', d => `${d[0].legendWidth}px`);
        makeElement(mount, ['div'], d => d, `${classPrefix}-legend-components`, {}, d => d.legend.id())
                        .each(function (d) {
                            d.legend.mount(this);
                        })
                        .style('width', d => `${d.legendWidth}px`);
    } else {
        const mount = makeElement(legendMount, 'div', [1], `${classPrefix}-legend-section`)
            .classed(`${classPrefix}-legend-horizontal-section`, true)
            .classed(`${classPrefix}-legend-section-${0}`, true)
            .style('width', `${legWidth}px`);

        makeElement(mount, 'div', legendComponents, `${classPrefix}-legend-components`, {}, d => d.legend.id())
                        .each(function (d) { d.legend.mount(this); })
                        .style('width', d => `${d.legend.measurement().width}px`);
    }
};

/**
 *
 *
 * @param {*} context
 * @param {*} container
 * @param {*} type
 * @param {*} headers
 */
const renderHeader = (layoutConfig, container, type, headers) => {
    const headerCell = headers[`${type}Cell`];
    const config = layoutConfig[`${type}`];
    const { position, align, padding } = config;
    const sel = container
        .selectAll(`.${layoutConfig.classPrefix}-inner-container`)
        .data([type]);
    sel.exit().remove();
    const selEnter = sel.enter().append('div');

    const cont = selEnter.merge(sel);
    cont.classed(`${layoutConfig.classPrefix}-inner-container`, true);

    headerCell && headerCell.render(cont.node());

    cont.selectAll('div').classed(`${layoutConfig.classPrefix}-inner-content`, true);
    cont.style('width', `${100}%`);

    if (config && headerCell) {
        cont.style('float', LEFT)
                        .style('text-align', align)
                        .style(`padding-${position === TOP ? BOTTOM : TOP}`, `${padding}px`);
    }
};

/**
 *
 *
 * @param {*} context
 * @param {*} shifter
 */
const shiftHeaders = (config, shifter, measurement, mount) => {
    const { classPrefix, title, subtitle, legend } = config;
    const { legendSpace } = measurement;
    const { position } = legend;

    shifter += position === LEFT ? legendSpace.width : 0;
    title && selectElement(mount).select(`.${classPrefix}-title-container`)
                    .style('width', title.align === LEFT ? `calc(100% - ${shifter}px` : '100%')
                    .style('margin-left', title.align === LEFT ? `${shifter}px` : 0);
    subtitle && selectElement(mount).select(`.${classPrefix}-subtitle-container`)
                    .style('width', subtitle.align === LEFT ? `calc(100% - ${shifter}px` : '100%')
                    .style('margin-left', subtitle.align === LEFT ? `${shifter}px` : 0);

    selectElement(mount).select(`.${classPrefix}-legend-horizontal-section`)
                    .style('margin-left', `${shifter}px`)
                    .style('width', `${legendSpace.width - shifter}px`)
                    .selectAll(`.${classPrefix}-legend-body, .${classPrefix}-legend-title`)
                    .style('max-width', `${legendSpace.width - shifter}px`);
    selectElement(mount).select(`.${classPrefix}-legend-vertical-section`)
                    .style('margin-left', null)
                    .selectAll(`.${classPrefix}-legend-body, .${classPrefix}-legend-title`)
                    .style('max-width', null);
};

/**
 *
 *
 * @param {*} mountPoint
 * @param {*} measurement
 * @param {*} classPrefix
 *
 */
const prepareGridContainer = (mountPoint, measurement, classPrefix, alias) => {
    if (!mountPoint) {
        return this;
    }

    const {
        height,
        width
    } = measurement;
    // Create container for the layout

    const sel = selectElement(mountPoint)
         .selectAll(`.${classPrefix}-inner-content`)
         .data(['layout'], d => d);
    sel.exit().remove();
    const selEnter = sel.enter().append('div');

    const container = selEnter.merge(sel)
    .attr('class', `${classPrefix}-inner-content`)
                    .classed(`${classPrefix}-grid-layout`, true)
                    .attr('id', `${classPrefix}-grid-layout-${alias}`)
                    .style('height', `${height}px`)
                    .style('padding', null)
                    .style('width', `${Math.ceil(width)}px`);
    // Mount for matrices
    const innerSel = container.selectAll(`.${classPrefix}-layout-grid-container`)
         .data(['layout2']);
    innerSel.exit().remove();
    const innerSelEnter = innerSel.enter().append('div');

    const mount = innerSelEnter.merge(innerSel);
    mount.classed(`${classPrefix}-layout-grid-container`, true)
                    .attr('id', `${classPrefix}-layout-grid-container-${alias}`)
                    .style('height', `${height}px`)
                    .style('width', `${Math.ceil(width)}px`);

    return {
        mount,
        container
    };
};

/**
 *
 *
 * @param {*} context
 * @param {*} components
 * @param {*} layoutConfig
 * @param {*} measurement
 */
export const renderComponents = (context, components, layoutConfig, measurement) => {
    const mountPoint = context.mount();
    const {
        headers,
        legends
    } = components;
    const {
        classPrefix
    } = layoutConfig;
    const {
        title,
        legend,
        subtitle,
        layout
    } = getSkeletons(mountPoint, layoutConfig, measurement);
    const {
        mount
    } = prepareGridContainer(layout.node(), measurement, classPrefix, context.alias());
    const padding = context.layout().getViewInformation().layoutDimensions.viewWidth[0];
    measurement.padding = padding;
    setLabelRotationForAxes(context);

    // Render layout
    context.layout().renderGrid(mount);
    context.once('layer.drawn').then(() => {
        renderHeader(layoutConfig, title, 'title', headers);
        renderHeader(layoutConfig, subtitle, 'subtitle', headers);
        renderLegend(layoutConfig, legend, legends, measurement);
        shiftHeaders(layoutConfig, padding, measurement, mountPoint);
    });
    context.composition().visualGroup.matrixInstance().value.each((el) => {
        el.valueOf().parentContainer(layout.node());
    });
};