function renderNode()

in src/chart/treemap/TreemapView.ts [763:1141]


function renderNode(
    seriesModel: TreemapSeriesModel,
    thisStorage: RenderElementStorage,
    oldStorage: RenderElementStorage,
    reRoot: ReRoot,
    lastsForAnimation: RenderResult['lastsForAnimation'],
    willInvisibleEls: RenderResult['willInvisibleEls'],
    thisNode: TreeNode,
    oldNode: TreeNode,
    parentGroup: graphic.Group,
    depth: number
) {
    // Whether under viewRoot.
    if (!thisNode) {
        // Deleting nodes will be performed finally. This method just find
        // element from old storage, or create new element, set them to new
        // storage, and set styles.
        return;
    }

    // -------------------------------------------------------------------
    // Start of closure variables available in "Procedures in renderNode".

    const thisLayout = thisNode.getLayout();
    const data = seriesModel.getData();
    const nodeModel = thisNode.getModel<TreemapSeriesNodeItemOption>();

    // Only for enabling highlight/downplay. Clear firstly.
    // Because some node will not be rendered.
    data.setItemGraphicEl(thisNode.dataIndex, null);

    if (!thisLayout || !thisLayout.isInView) {
        return;
    }

    const thisWidth = thisLayout.width;
    const thisHeight = thisLayout.height;
    const borderWidth = thisLayout.borderWidth;
    const thisInvisible = thisLayout.invisible;

    const thisRawIndex = thisNode.getRawIndex();
    const oldRawIndex = oldNode && oldNode.getRawIndex();

    const thisViewChildren = thisNode.viewChildren;
    const upperHeight = thisLayout.upperHeight;
    const isParent = thisViewChildren && thisViewChildren.length;
    const itemStyleNormalModel = nodeModel.getModel('itemStyle');
    const itemStyleEmphasisModel = nodeModel.getModel(['emphasis', 'itemStyle']);
    const itemStyleBlurModel = nodeModel.getModel(['blur', 'itemStyle']);
    const itemStyleSelectModel = nodeModel.getModel(['select', 'itemStyle']);
    const borderRadius = itemStyleNormalModel.get('borderRadius') || 0;

    // End of closure ariables available in "Procedures in renderNode".
    // -----------------------------------------------------------------

    // Node group
    const group = giveGraphic('nodeGroup', Group);

    if (!group) {
        return;
    }

    parentGroup.add(group);
    // x,y are not set when el is above view root.
    group.x = thisLayout.x || 0;
    group.y = thisLayout.y || 0;
    group.markRedraw();
    inner(group).nodeWidth = thisWidth;
    inner(group).nodeHeight = thisHeight;

    if (thisLayout.isAboveViewRoot) {
        return group;
    }

    // Background
    const bg = giveGraphic('background', Rect, depth, Z2_BG);
    bg && renderBackground(group, bg, isParent && thisLayout.upperLabelHeight);

    const emphasisModel = nodeModel.getModel('emphasis');
    const focus = emphasisModel.get('focus');
    const blurScope = emphasisModel.get('blurScope');
    const isDisabled = emphasisModel.get('disabled');

    const focusOrIndices =
        focus === 'ancestor' ? thisNode.getAncestorsIndices()
        : focus === 'descendant' ? thisNode.getDescendantIndices()
        : focus;

    // No children, render content.
    if (isParent) {
        // Because of the implementation about "traverse" in graphic hover style, we
        // can not set hover listener on the "group" of non-leaf node. Otherwise the
        // hover event from the descendents will be listenered.
        if (isHighDownDispatcher(group)) {
            setAsHighDownDispatcher(group, false);
        }
        if (bg) {
            setAsHighDownDispatcher(bg, !isDisabled);
            // Only for enabling highlight/downplay.
            data.setItemGraphicEl(thisNode.dataIndex, bg);

            enableHoverFocus(bg, focusOrIndices, blurScope);
        }
    }
    else {
        const content = giveGraphic('content', Rect, depth, Z2_CONTENT);
        content && renderContent(group, content);

        (bg as ECElement).disableMorphing = true;

        if (bg && isHighDownDispatcher(bg)) {
            setAsHighDownDispatcher(bg, false);
        }
        setAsHighDownDispatcher(group, !isDisabled);
        // Only for enabling highlight/downplay.
        data.setItemGraphicEl(thisNode.dataIndex, group);

        const cursorStyle = nodeModel.getShallow('cursor');
        cursorStyle && content.attr('cursor', cursorStyle);

        enableHoverFocus(group, focusOrIndices, blurScope);
    }

    return group;

    // ----------------------------
    // | Procedures in renderNode |
    // ----------------------------

    function renderBackground(group: graphic.Group, bg: graphic.Rect, useUpperLabel: boolean) {
        const ecData = getECData(bg);
        // For tooltip.
        ecData.dataIndex = thisNode.dataIndex;
        ecData.seriesIndex = seriesModel.seriesIndex;

        bg.setShape({x: 0, y: 0, width: thisWidth, height: thisHeight, r: borderRadius});

        if (thisInvisible) {
            // If invisible, do not set visual, otherwise the element will
            // change immediately before animation. We think it is OK to
            // remain its origin color when moving out of the view window.
            processInvisible(bg);
        }
        else {
            bg.invisible = false;
            const style = thisNode.getVisual('style') as PathStyleProps;
            const visualBorderColor = style.stroke;
            const normalStyle = getItemStyleNormal(itemStyleNormalModel);
            normalStyle.fill = visualBorderColor;
            const emphasisStyle = getStateItemStyle(itemStyleEmphasisModel);
            emphasisStyle.fill = itemStyleEmphasisModel.get('borderColor');
            const blurStyle = getStateItemStyle(itemStyleBlurModel);
            blurStyle.fill = itemStyleBlurModel.get('borderColor');
            const selectStyle = getStateItemStyle(itemStyleSelectModel);
            selectStyle.fill = itemStyleSelectModel.get('borderColor');

            if (useUpperLabel) {
                const upperLabelWidth = thisWidth - 2 * borderWidth;

                prepareText(
                    // PENDING: convert ZRColor to ColorString for text.
                    bg, visualBorderColor as ColorString, style.opacity,
                    {x: borderWidth, y: 0, width: upperLabelWidth, height: upperHeight}
                );
            }
            // For old bg.
            else {
                bg.removeTextContent();
            }

            bg.setStyle(normalStyle);

            bg.ensureState('emphasis').style = emphasisStyle;
            bg.ensureState('blur').style = blurStyle;
            bg.ensureState('select').style = selectStyle;
            setDefaultStateProxy(bg);
        }

        group.add(bg);
    }

    function renderContent(group: graphic.Group, content: graphic.Rect) {
        const ecData = getECData(content);
        // For tooltip.
        ecData.dataIndex = thisNode.dataIndex;
        ecData.seriesIndex = seriesModel.seriesIndex;

        const contentWidth = Math.max(thisWidth - 2 * borderWidth, 0);
        const contentHeight = Math.max(thisHeight - 2 * borderWidth, 0);

        content.culling = true;
        content.setShape({
            x: borderWidth,
            y: borderWidth,
            width: contentWidth,
            height: contentHeight,
            r: borderRadius
        });

        if (thisInvisible) {
            // If invisible, do not set visual, otherwise the element will
            // change immediately before animation. We think it is OK to
            // remain its origin color when moving out of the view window.
            processInvisible(content);
        }
        else {
            content.invisible = false;
            const nodeStyle = thisNode.getVisual('style') as PathStyleProps;
            const visualColor = nodeStyle.fill;
            const normalStyle = getItemStyleNormal(itemStyleNormalModel);
            normalStyle.fill = visualColor;
            normalStyle.decal = nodeStyle.decal;
            const emphasisStyle = getStateItemStyle(itemStyleEmphasisModel);
            const blurStyle = getStateItemStyle(itemStyleBlurModel);
            const selectStyle = getStateItemStyle(itemStyleSelectModel);

            // PENDING: convert ZRColor to ColorString for text.
            prepareText(content, visualColor as ColorString, nodeStyle.opacity, null);

            content.setStyle(normalStyle);
            content.ensureState('emphasis').style = emphasisStyle;
            content.ensureState('blur').style = blurStyle;
            content.ensureState('select').style = selectStyle;
            setDefaultStateProxy(content);
        }

        group.add(content);
    }

    function processInvisible(element: graphic.Rect) {
        // Delay invisible setting utill animation finished,
        // avoid element vanish suddenly before animation.
        !element.invisible && willInvisibleEls.push(element);
    }

    function prepareText(
        rectEl: graphic.Rect,
        visualColor: ColorString,
        visualOpacity: number,
        // Can be null/undefined
        upperLabelRect: RectLike
    ) {
        const normalLabelModel = nodeModel.getModel(
            upperLabelRect ? PATH_UPPERLABEL_NORMAL : PATH_LABEL_NOAMAL
        );

        const defaultText = convertOptionIdName(nodeModel.get('name'), null);

        const isShow = normalLabelModel.getShallow('show');

        setLabelStyle(
            rectEl,
            getLabelStatesModels(nodeModel, upperLabelRect ? PATH_UPPERLABEL_NORMAL : PATH_LABEL_NOAMAL),
            {
                defaultText: isShow ? defaultText : null,
                inheritColor: visualColor,
                defaultOpacity: visualOpacity,
                labelFetcher: seriesModel,
                labelDataIndex: thisNode.dataIndex
            }
        );

        const textEl = rectEl.getTextContent();
        if (!textEl) {
            return;
        }
        const textStyle = textEl.style;
        const textPadding = normalizeCssArray(textStyle.padding || 0);

        if (upperLabelRect) {
            rectEl.setTextConfig({
                layoutRect: upperLabelRect
            });
            (textEl as ECElement).disableLabelLayout = true;
        }
        textEl.beforeUpdate = function () {
            const width = Math.max(
                (upperLabelRect ? upperLabelRect.width : rectEl.shape.width) - textPadding[1] - textPadding[3], 0
            );
            const height = Math.max(
                (upperLabelRect ? upperLabelRect.height : rectEl.shape.height) - textPadding[0] - textPadding[2], 0
            );
            if (textStyle.width !== width || textStyle.height !== height) {
                textEl.setStyle({
                    width,
                    height
                });
            }
        };

        textStyle.truncateMinChar = 2;
        textStyle.lineOverflow = 'truncate';

        addDrillDownIcon(textStyle, upperLabelRect, thisLayout);
        const textEmphasisState = textEl.getState('emphasis');
        addDrillDownIcon(textEmphasisState ? textEmphasisState.style : null, upperLabelRect, thisLayout);
    }

    function addDrillDownIcon(style: TextStyleProps, upperLabelRect: RectLike, thisLayout: any) {
        const text = style ? style.text : null;
        if (!upperLabelRect && thisLayout.isLeafRoot && text != null) {
            const iconChar = seriesModel.get('drillDownIcon', true);
            style.text = iconChar ? iconChar + ' ' + text : text;
        }
    }

    function giveGraphic<T extends graphic.Group | graphic.Rect>(
        storageName: keyof RenderElementStorage,
        Ctor: {new(): T},
        depth?: number,
        z?: number
    ): T {
        let element = oldRawIndex != null && oldStorage[storageName][oldRawIndex];
        const lasts = lastsForAnimation[storageName];

        if (element) {
            // Remove from oldStorage
            oldStorage[storageName][oldRawIndex] = null;
            prepareAnimationWhenHasOld(lasts, element);
        }
        // If invisible and no old element, do not create new element (for optimizing).
        else if (!thisInvisible) {
            element = new Ctor();
            if (element instanceof Displayable) {
                element.z2 = calculateZ2(depth, z);
            }
            prepareAnimationWhenNoOld(lasts, element);
        }

        // Set to thisStorage
        return (thisStorage[storageName][thisRawIndex] = element) as T;
    }

    function prepareAnimationWhenHasOld(lasts: LastCfg[], element: graphic.Group | graphic.Rect) {
        const lastCfg = lasts[thisRawIndex] = {} as LastCfg;
        if (element instanceof Group) {
            lastCfg.oldX = element.x;
            lastCfg.oldY = element.y;
        }
        else {
            lastCfg.oldShape = extend({}, element.shape);
        }
    }

    // If a element is new, we need to find the animation start point carefully,
    // otherwise it will looks strange when 'zoomToNode'.
    function prepareAnimationWhenNoOld(lasts: LastCfg[], element: graphic.Group | graphic.Rect) {
        const lastCfg = lasts[thisRawIndex] = {} as LastCfg;
        const parentNode = thisNode.parentNode;
        const isGroup = element instanceof graphic.Group;

        if (parentNode && (!reRoot || reRoot.direction === 'drillDown')) {
            let parentOldX = 0;
            let parentOldY = 0;

            // New nodes appear from right-bottom corner in 'zoomToNode' animation.
            // For convenience, get old bounding rect from background.
            const parentOldBg = lastsForAnimation.background[parentNode.getRawIndex()];
            if (!reRoot && parentOldBg && parentOldBg.oldShape) {
                parentOldX = parentOldBg.oldShape.width;
                parentOldY = parentOldBg.oldShape.height;
            }

            // When no parent old shape found, its parent is new too,
            // so we can just use {x:0, y:0}.
            if (isGroup) {
                lastCfg.oldX = 0;
                lastCfg.oldY = parentOldY;
            }
            else {
                lastCfg.oldShape = {x: parentOldX, y: parentOldY, width: 0, height: 0};
            }
        }

        // Fade in, user can be aware that these nodes are new.
        lastCfg.fadein = !isGroup;
    }

}