echarts layout 源码

  • 2022-10-20
  • 浏览 (447)

echarts layout 代码

文件路径:/src/util/layout.ts

/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership.  The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License.  You may obtain a copy of the License at
*
*   http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied.  See the License for the
* specific language governing permissions and limitations
* under the License.
*/

// Layout helpers for each component positioning

import * as zrUtil from 'zrender/src/core/util';
import BoundingRect from 'zrender/src/core/BoundingRect';
import {parsePercent} from './number';
import * as formatUtil from './format';
import { BoxLayoutOptionMixin, ComponentLayoutMode } from './types';
import Group from 'zrender/src/graphic/Group';
import Element from 'zrender/src/Element';
import { Dictionary } from 'zrender/src/core/types';

const each = zrUtil.each;

export interface LayoutRect extends BoundingRect {
    margin: number[]
}

export interface NewlineElement extends Element {
    newline: boolean
}

type BoxLayoutKeys = keyof BoxLayoutOptionMixin;
/**
 * @public
 */
export const LOCATION_PARAMS = [
    'left', 'right', 'top', 'bottom', 'width', 'height'
] as const;

/**
 * @public
 */
export const HV_NAMES = [
    ['width', 'left', 'right'],
    ['height', 'top', 'bottom']
] as const;

function boxLayout(
    orient: 'horizontal' | 'vertical',
    group: Group,
    gap: number,
    maxWidth?: number,
    maxHeight?: number
) {
    let x = 0;
    let y = 0;

    if (maxWidth == null) {
        maxWidth = Infinity;
    }
    if (maxHeight == null) {
        maxHeight = Infinity;
    }
    let currentLineMaxSize = 0;

    group.eachChild(function (child, idx) {
        const rect = child.getBoundingRect();
        const nextChild = group.childAt(idx + 1);
        const nextChildRect = nextChild && nextChild.getBoundingRect();
        let nextX: number;
        let nextY: number;

        if (orient === 'horizontal') {
            const moveX = rect.width + (nextChildRect ? (-nextChildRect.x + rect.x) : 0);
            nextX = x + moveX;
            // Wrap when width exceeds maxWidth or meet a `newline` group
            // FIXME compare before adding gap?
            if (nextX > maxWidth || (child as NewlineElement).newline) {
                x = 0;
                nextX = moveX;
                y += currentLineMaxSize + gap;
                currentLineMaxSize = rect.height;
            }
            else {
                // FIXME: consider rect.y is not `0`?
                currentLineMaxSize = Math.max(currentLineMaxSize, rect.height);
            }
        }
        else {
            const moveY = rect.height + (nextChildRect ? (-nextChildRect.y + rect.y) : 0);
            nextY = y + moveY;
            // Wrap when width exceeds maxHeight or meet a `newline` group
            if (nextY > maxHeight || (child as NewlineElement).newline) {
                x += currentLineMaxSize + gap;
                y = 0;
                nextY = moveY;
                currentLineMaxSize = rect.width;
            }
            else {
                currentLineMaxSize = Math.max(currentLineMaxSize, rect.width);
            }
        }

        if ((child as NewlineElement).newline) {
            return;
        }

        child.x = x;
        child.y = y;
        child.markRedraw();

        orient === 'horizontal'
            ? (x = nextX + gap)
            : (y = nextY + gap);
    });
}

/**
 * VBox or HBox layouting
 * @param {string} orient
 * @param {module:zrender/graphic/Group} group
 * @param {number} gap
 * @param {number} [width=Infinity]
 * @param {number} [height=Infinity]
 */
export const box = boxLayout;

/**
 * VBox layouting
 * @param {module:zrender/graphic/Group} group
 * @param {number} gap
 * @param {number} [width=Infinity]
 * @param {number} [height=Infinity]
 */
export const vbox = zrUtil.curry(boxLayout, 'vertical');

/**
 * HBox layouting
 * @param {module:zrender/graphic/Group} group
 * @param {number} gap
 * @param {number} [width=Infinity]
 * @param {number} [height=Infinity]
 */
export const hbox = zrUtil.curry(boxLayout, 'horizontal');

/**
 * If x or x2 is not specified or 'center' 'left' 'right',
 * the width would be as long as possible.
 * If y or y2 is not specified or 'middle' 'top' 'bottom',
 * the height would be as long as possible.
 */
export function getAvailableSize(
    positionInfo: {
        left?: number | string
        top?: number | string
        right?: number | string
        bottom?: number | string
    },
    containerRect: { width: number, height: number },
    margin?: number[] | number
) {
    const containerWidth = containerRect.width;
    const containerHeight = containerRect.height;

    let x = parsePercent(positionInfo.left, containerWidth);
    let y = parsePercent(positionInfo.top, containerHeight);
    let x2 = parsePercent(positionInfo.right, containerWidth);
    let y2 = parsePercent(positionInfo.bottom, containerHeight);

    (isNaN(x) || isNaN(parseFloat(positionInfo.left as string))) && (x = 0);
    (isNaN(x2) || isNaN(parseFloat(positionInfo.right as string))) && (x2 = containerWidth);
    (isNaN(y) || isNaN(parseFloat(positionInfo.top as string))) && (y = 0);
    (isNaN(y2) || isNaN(parseFloat(positionInfo.bottom as string))) && (y2 = containerHeight);

    margin = formatUtil.normalizeCssArray(margin || 0);

    return {
        width: Math.max(x2 - x - margin[1] - margin[3], 0),
        height: Math.max(y2 - y - margin[0] - margin[2], 0)
    };
}

/**
 * Parse position info.
 */
export function getLayoutRect(
    positionInfo: BoxLayoutOptionMixin & {
        aspect?: number // aspect is width / height
    },
    containerRect: {width: number, height: number},
    margin?: number | number[]
): LayoutRect {
    margin = formatUtil.normalizeCssArray(margin || 0);

    const containerWidth = containerRect.width;
    const containerHeight = containerRect.height;

    let left = parsePercent(positionInfo.left, containerWidth);
    let top = parsePercent(positionInfo.top, containerHeight);
    const right = parsePercent(positionInfo.right, containerWidth);
    const bottom = parsePercent(positionInfo.bottom, containerHeight);
    let width = parsePercent(positionInfo.width, containerWidth);
    let height = parsePercent(positionInfo.height, containerHeight);

    const verticalMargin = margin[2] + margin[0];
    const horizontalMargin = margin[1] + margin[3];
    const aspect = positionInfo.aspect;

    // If width is not specified, calculate width from left and right
    if (isNaN(width)) {
        width = containerWidth - right - horizontalMargin - left;
    }
    if (isNaN(height)) {
        height = containerHeight - bottom - verticalMargin - top;
    }

    if (aspect != null) {
        // If width and height are not given
        // 1. Graph should not exceeds the container
        // 2. Aspect must be keeped
        // 3. Graph should take the space as more as possible
        // FIXME
        // Margin is not considered, because there is no case that both
        // using margin and aspect so far.
        if (isNaN(width) && isNaN(height)) {
            if (aspect > containerWidth / containerHeight) {
                width = containerWidth * 0.8;
            }
            else {
                height = containerHeight * 0.8;
            }
        }

        // Calculate width or height with given aspect
        if (isNaN(width)) {
            width = aspect * height;
        }
        if (isNaN(height)) {
            height = width / aspect;
        }
    }

    // If left is not specified, calculate left from right and width
    if (isNaN(left)) {
        left = containerWidth - right - width - horizontalMargin;
    }
    if (isNaN(top)) {
        top = containerHeight - bottom - height - verticalMargin;
    }

    // Align left and top
    switch (positionInfo.left || positionInfo.right) {
        case 'center':
            left = containerWidth / 2 - width / 2 - margin[3];
            break;
        case 'right':
            left = containerWidth - width - horizontalMargin;
            break;
    }
    switch (positionInfo.top || positionInfo.bottom) {
        case 'middle':
        case 'center':
            top = containerHeight / 2 - height / 2 - margin[0];
            break;
        case 'bottom':
            top = containerHeight - height - verticalMargin;
            break;
    }
    // If something is wrong and left, top, width, height are calculated as NaN
    left = left || 0;
    top = top || 0;
    if (isNaN(width)) {
        // Width may be NaN if only one value is given except width
        width = containerWidth - horizontalMargin - left - (right || 0);
    }
    if (isNaN(height)) {
        // Height may be NaN if only one value is given except height
        height = containerHeight - verticalMargin - top - (bottom || 0);
    }

    const rect = new BoundingRect(left + margin[3], top + margin[0], width, height) as LayoutRect;
    rect.margin = margin;
    return rect;
}


/**
 * Position a zr element in viewport
 *  Group position is specified by either
 *  {left, top}, {right, bottom}
 *  If all properties exists, right and bottom will be igonred.
 *
 * Logic:
 *     1. Scale (against origin point in parent coord)
 *     2. Rotate (against origin point in parent coord)
 *     3. Traslate (with el.position by this method)
 * So this method only fixes the last step 'Traslate', which does not affect
 * scaling and rotating.
 *
 * If be called repeatly with the same input el, the same result will be gotten.
 *
 * Return true if the layout happend.
 *
 * @param el Should have `getBoundingRect` method.
 * @param positionInfo
 * @param positionInfo.left
 * @param positionInfo.top
 * @param positionInfo.right
 * @param positionInfo.bottom
 * @param positionInfo.width Only for opt.boundingModel: 'raw'
 * @param positionInfo.height Only for opt.boundingModel: 'raw'
 * @param containerRect
 * @param margin
 * @param opt
 * @param opt.hv Only horizontal or only vertical. Default to be [1, 1]
 * @param opt.boundingMode
 *        Specify how to calculate boundingRect when locating.
 *        'all': Position the boundingRect that is transformed and uioned
 *               both itself and its descendants.
 *               This mode simplies confine the elements in the bounding
 *               of their container (e.g., using 'right: 0').
 *        'raw': Position the boundingRect that is not transformed and only itself.
 *               This mode is useful when you want a element can overflow its
 *               container. (Consider a rotated circle needs to be located in a corner.)
 *               In this mode positionInfo.width/height can only be number.
 */
export function positionElement(
    el: Element,
    positionInfo: BoxLayoutOptionMixin,
    containerRect: {width: number, height: number},
    margin?: number[] | number,
    opt?: {
        hv: [1 | 0 | boolean, 1 | 0 | boolean],
        boundingMode: 'all' | 'raw'
    },
    out?: { x?: number, y?: number }
): boolean {
    const h = !opt || !opt.hv || opt.hv[0];
    const v = !opt || !opt.hv || opt.hv[1];
    const boundingMode = opt && opt.boundingMode || 'all';
    out = out || el;

    out.x = el.x;
    out.y = el.y;

    if (!h && !v) {
        return false;
    }

    let rect;
    if (boundingMode === 'raw') {
        rect = el.type === 'group'
            ? new BoundingRect(0, 0, +positionInfo.width || 0, +positionInfo.height || 0)
            : el.getBoundingRect();
    }
    else {
        rect = el.getBoundingRect();
        if (el.needLocalTransform()) {
            const transform = el.getLocalTransform();
            // Notice: raw rect may be inner object of el,
            // which should not be modified.
            rect = rect.clone();
            rect.applyTransform(transform);
        }
    }

    // The real width and height can not be specified but calculated by the given el.
    const layoutRect = getLayoutRect(
        zrUtil.defaults(
            {width: rect.width, height: rect.height},
            positionInfo
        ),
        containerRect,
        margin
    );

    // Because 'tranlate' is the last step in transform
    // (see zrender/core/Transformable#getLocalTransform),
    // we can just only modify el.position to get final result.
    const dx = h ? layoutRect.x - rect.x : 0;
    const dy = v ? layoutRect.y - rect.y : 0;

    if (boundingMode === 'raw') {
        out.x = dx;
        out.y = dy;
    }
    else {
        out.x += dx;
        out.y += dy;
    }
    if (out === el) {
        el.markRedraw();
    }
    return true;
}

/**
 * @param option Contains some of the properties in HV_NAMES.
 * @param hvIdx 0: horizontal; 1: vertical.
 */
export function sizeCalculable(option: BoxLayoutOptionMixin, hvIdx: number): boolean {
    return option[HV_NAMES[hvIdx][0]] != null
        || (option[HV_NAMES[hvIdx][1]] != null && option[HV_NAMES[hvIdx][2]] != null);
}

export function fetchLayoutMode(ins: any): ComponentLayoutMode {
    const layoutMode = ins.layoutMode || ins.constructor.layoutMode;
    return zrUtil.isObject(layoutMode)
        ? layoutMode
        : layoutMode
        ? {type: layoutMode}
        : null;
}

/**
 * Consider Case:
 * When default option has {left: 0, width: 100}, and we set {right: 0}
 * through setOption or media query, using normal zrUtil.merge will cause
 * {right: 0} does not take effect.
 *
 * @example
 * ComponentModel.extend({
 *     init: function () {
 *         ...
 *         let inputPositionParams = layout.getLayoutParams(option);
 *         this.mergeOption(inputPositionParams);
 *     },
 *     mergeOption: function (newOption) {
 *         newOption && zrUtil.merge(thisOption, newOption, true);
 *         layout.mergeLayoutParam(thisOption, newOption);
 *     }
 * });
 *
 * @param targetOption
 * @param newOption
 * @param opt
 */
export function mergeLayoutParam<T extends BoxLayoutOptionMixin>(
    targetOption: T,
    newOption: T,
    opt?: ComponentLayoutMode
) {
    let ignoreSize = opt && opt.ignoreSize;
    !zrUtil.isArray(ignoreSize) && (ignoreSize = [ignoreSize, ignoreSize]);

    const hResult = merge(HV_NAMES[0], 0);
    const vResult = merge(HV_NAMES[1], 1);

    copy(HV_NAMES[0], targetOption, hResult);
    copy(HV_NAMES[1], targetOption, vResult);

    function merge(names: typeof HV_NAMES[number], hvIdx: number) {
        const newParams: BoxLayoutOptionMixin = {};
        let newValueCount = 0;
        const merged: BoxLayoutOptionMixin = {};
        let mergedValueCount = 0;
        const enoughParamNumber = 2;

        each(names, function (name: BoxLayoutKeys) {
            merged[name] = targetOption[name];
        });
        each(names, function (name: BoxLayoutKeys) {
            // Consider case: newOption.width is null, which is
            // set by user for removing width setting.
            hasProp(newOption, name) && (newParams[name] = merged[name] = newOption[name]);
            hasValue(newParams, name) && newValueCount++;
            hasValue(merged, name) && mergedValueCount++;
        });

        if ((ignoreSize as [boolean, boolean])[hvIdx]) {
            // Only one of left/right is premitted to exist.
            if (hasValue(newOption, names[1])) {
                merged[names[2]] = null;
            }
            else if (hasValue(newOption, names[2])) {
                merged[names[1]] = null;
            }
            return merged;
        }

        // Case: newOption: {width: ..., right: ...},
        // or targetOption: {right: ...} and newOption: {width: ...},
        // There is no conflict when merged only has params count
        // little than enoughParamNumber.
        if (mergedValueCount === enoughParamNumber || !newValueCount) {
            return merged;
        }
        // Case: newOption: {width: ..., right: ...},
        // Than we can make sure user only want those two, and ignore
        // all origin params in targetOption.
        else if (newValueCount >= enoughParamNumber) {
            return newParams;
        }
        else {
            // Chose another param from targetOption by priority.
            for (let i = 0; i < names.length; i++) {
                const name = names[i];
                if (!hasProp(newParams, name) && hasProp(targetOption, name)) {
                    newParams[name] = targetOption[name];
                    break;
                }
            }
            return newParams;
        }
    }

    function hasProp(obj: object, name: string): boolean {
        return obj.hasOwnProperty(name);
    }

    function hasValue(obj: Dictionary<any>, name: string): boolean {
        return obj[name] != null && obj[name] !== 'auto';
    }

    function copy(names: readonly string[], target: Dictionary<any>, source: Dictionary<any>) {
        each(names, function (name) {
            target[name] = source[name];
        });
    }
}

/**
 * Retrieve 'left', 'right', 'top', 'bottom', 'width', 'height' from object.
 */
export function getLayoutParams(source: BoxLayoutOptionMixin): BoxLayoutOptionMixin {
    return copyLayoutParams({}, source);
}

/**
 * Retrieve 'left', 'right', 'top', 'bottom', 'width', 'height' from object.
 * @param {Object} source
 * @return {Object} Result contains those props.
 */
export function copyLayoutParams(target: BoxLayoutOptionMixin, source: BoxLayoutOptionMixin): BoxLayoutOptionMixin {
    source && target && each(LOCATION_PARAMS, function (name: BoxLayoutKeys) {
        source.hasOwnProperty(name) && (target[name] = source[name]);
    });
    return target;
}

相关信息

echarts 源码目录

相关文章

echarts ECEventProcessor 源码

echarts KDTree 源码

echarts animation 源码

echarts clazz 源码

echarts component 源码

echarts conditionalExpression 源码

echarts decal 源码

echarts event 源码

echarts format 源码

echarts graphic 源码

0  赞