echarts ScrollableLegendView 源码

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

echarts ScrollableLegendView 代码

文件路径:/src/component/legend/ScrollableLegendView.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.
*/

/**
 * Separate legend and scrollable legend to reduce package size.
 */

import * as zrUtil from 'zrender/src/core/util';
import * as graphic from '../../util/graphic';
import * as layoutUtil from '../../util/layout';
import LegendView from './LegendView';
import { LegendSelectorButtonOption } from './LegendModel';
import ExtensionAPI from '../../core/ExtensionAPI';
import GlobalModel from '../../model/Global';
import ScrollableLegendModel, {ScrollableLegendOption} from './ScrollableLegendModel';
import Displayable from 'zrender/src/graphic/Displayable';
import Element from 'zrender/src/Element';
import { ZRRectLike } from '../../util/types';

const Group = graphic.Group;

const WH = ['width', 'height'] as const;
const XY = ['x', 'y'] as const;

interface PageInfo {
    contentPosition: number[]
    pageCount: number
    pageIndex: number
    pagePrevDataIndex: number
    pageNextDataIndex: number
}

interface ItemInfo {
    /**
     * Start
     */
    s: number
    /**
     * End
     */
    e: number
    /**
     * Index
     */
    i: number
}

type LegendGroup = graphic.Group & {
    __rectSize: number
};

type LegendItemElement = Element & {
    __legendDataIndex: number
};

class ScrollableLegendView extends LegendView {

    static type = 'legend.scroll' as const;
    type = ScrollableLegendView.type;

    newlineDisabled = true;

    private _containerGroup: LegendGroup;
    private _controllerGroup: graphic.Group;

    private _currentIndex: number = 0;

    private _showController: boolean;

    init() {

        super.init();

        this.group.add(this._containerGroup = new Group() as LegendGroup);
        this._containerGroup.add(this.getContentGroup());

        this.group.add(this._controllerGroup = new Group());
    }

    /**
     * @override
     */
    resetInner() {
        super.resetInner();

        this._controllerGroup.removeAll();
        this._containerGroup.removeClipPath();
        this._containerGroup.__rectSize = null;
    }

    /**
     * @override
     */
    renderInner(
        itemAlign: ScrollableLegendOption['align'],
        legendModel: ScrollableLegendModel,
        ecModel: GlobalModel,
        api: ExtensionAPI,
        selector: LegendSelectorButtonOption[],
        orient: ScrollableLegendOption['orient'],
        selectorPosition: ScrollableLegendOption['selectorPosition']
    ) {
        const self = this;

        // Render content items.
        super.renderInner(itemAlign, legendModel, ecModel, api, selector, orient, selectorPosition);

        const controllerGroup = this._controllerGroup;

        // FIXME: support be 'auto' adapt to size number text length,
        // e.g., '3/12345' should not overlap with the control arrow button.
        const pageIconSize = legendModel.get('pageIconSize', true);
        const pageIconSizeArr: number[] = zrUtil.isArray(pageIconSize)
            ? pageIconSize : [pageIconSize, pageIconSize];

        createPageButton('pagePrev', 0);

        const pageTextStyleModel = legendModel.getModel('pageTextStyle');
        controllerGroup.add(new graphic.Text({
            name: 'pageText',
            style: {
                // Placeholder to calculate a proper layout.
                text: 'xx/xx',
                fill: pageTextStyleModel.getTextColor(),
                font: pageTextStyleModel.getFont(),
                verticalAlign: 'middle',
                align: 'center'
            },
            silent: true
        }));

        createPageButton('pageNext', 1);

        function createPageButton(name: string, iconIdx: number) {
            const pageDataIndexName = (name + 'DataIndex') as 'pagePrevDataIndex' | 'pageNextDataIndex';
            const icon = graphic.createIcon(
                legendModel.get('pageIcons', true)[legendModel.getOrient().name][iconIdx],
                {
                    // Buttons will be created in each render, so we do not need
                    // to worry about avoiding using legendModel kept in scope.
                    onclick: zrUtil.bind(
                        self._pageGo, self, pageDataIndexName, legendModel, api
                    )
                },
                {
                    x: -pageIconSizeArr[0] / 2,
                    y: -pageIconSizeArr[1] / 2,
                    width: pageIconSizeArr[0],
                    height: pageIconSizeArr[1]
                }
            );
            icon.name = name;
            controllerGroup.add(icon);
        }
    }

    /**
     * @override
     */
    layoutInner(
        legendModel: ScrollableLegendModel,
        itemAlign: ScrollableLegendOption['align'],
        maxSize: { width: number, height: number },
        isFirstRender: boolean,
        selector: LegendSelectorButtonOption[],
        selectorPosition: ScrollableLegendOption['selectorPosition']
    ) {
        const selectorGroup = this.getSelectorGroup();

        const orientIdx = legendModel.getOrient().index;
        const wh = WH[orientIdx];
        const xy = XY[orientIdx];
        const hw = WH[1 - orientIdx];
        const yx = XY[1 - orientIdx];

        selector && layoutUtil.box(
            // Buttons in selectorGroup always layout horizontally
            'horizontal',
            selectorGroup,
            legendModel.get('selectorItemGap', true)
        );

        const selectorButtonGap = legendModel.get('selectorButtonGap', true);
        const selectorRect = selectorGroup.getBoundingRect();
        const selectorPos = [-selectorRect.x, -selectorRect.y];

        const processMaxSize = zrUtil.clone(maxSize);
        selector && (processMaxSize[wh] = maxSize[wh] - selectorRect[wh] - selectorButtonGap);

        const mainRect = this._layoutContentAndController(legendModel, isFirstRender,
            processMaxSize, orientIdx, wh, hw, yx, xy
        );

        if (selector) {
            if (selectorPosition === 'end') {
                selectorPos[orientIdx] += mainRect[wh] + selectorButtonGap;
            }
            else {
                const offset = selectorRect[wh] + selectorButtonGap;
                selectorPos[orientIdx] -= offset;
                mainRect[xy] -= offset;
            }
            mainRect[wh] += selectorRect[wh] + selectorButtonGap;

            selectorPos[1 - orientIdx] += mainRect[yx] + mainRect[hw] / 2 - selectorRect[hw] / 2;
            mainRect[hw] = Math.max(mainRect[hw], selectorRect[hw]);
            mainRect[yx] = Math.min(mainRect[yx], selectorRect[yx] + selectorPos[1 - orientIdx]);

            selectorGroup.x = selectorPos[0];
            selectorGroup.y = selectorPos[1];
            selectorGroup.markRedraw();
        }

        return mainRect;
    }

    _layoutContentAndController(
        legendModel: ScrollableLegendModel,
        isFirstRender: boolean,
        maxSize: { width: number, height: number },
        orientIdx: 0 | 1,
        wh: 'width' | 'height',
        hw: 'width' | 'height',
        yx: 'x' | 'y',
        xy: 'y' | 'x'
    ) {
        const contentGroup = this.getContentGroup();
        const containerGroup = this._containerGroup;
        const controllerGroup = this._controllerGroup;

        // Place items in contentGroup.
        layoutUtil.box(
            legendModel.get('orient'),
            contentGroup,
            legendModel.get('itemGap'),
            !orientIdx ? null : maxSize.width,
            orientIdx ? null : maxSize.height
        );

        layoutUtil.box(
            // Buttons in controller are layout always horizontally.
            'horizontal',
            controllerGroup,
            legendModel.get('pageButtonItemGap', true)
        );

        const contentRect = contentGroup.getBoundingRect();
        const controllerRect = controllerGroup.getBoundingRect();
        const showController = this._showController = contentRect[wh] > maxSize[wh];

        // In case that the inner elements of contentGroup layout do not based on [0, 0]
        const contentPos = [-contentRect.x, -contentRect.y];
        // Remain contentPos when scroll animation perfroming.
        // If first rendering, `contentGroup.position` is [0, 0], which
        // does not make sense and may cause unexepcted animation if adopted.
        if (!isFirstRender) {
            contentPos[orientIdx] = contentGroup[xy];
        }

        // Layout container group based on 0.
        const containerPos = [0, 0];
        const controllerPos = [-controllerRect.x, -controllerRect.y];
        const pageButtonGap = zrUtil.retrieve2(
            legendModel.get('pageButtonGap', true), legendModel.get('itemGap', true)
        );

        // Place containerGroup and controllerGroup and contentGroup.
        if (showController) {
            const pageButtonPosition = legendModel.get('pageButtonPosition', true);
            // controller is on the right / bottom.
            if (pageButtonPosition === 'end') {
                controllerPos[orientIdx] += maxSize[wh] - controllerRect[wh];
            }
            // controller is on the left / top.
            else {
                containerPos[orientIdx] += controllerRect[wh] + pageButtonGap;
            }
        }

        // Always align controller to content as 'middle'.
        controllerPos[1 - orientIdx] += contentRect[hw] / 2 - controllerRect[hw] / 2;

        contentGroup.setPosition(contentPos);
        containerGroup.setPosition(containerPos);
        controllerGroup.setPosition(controllerPos);

        // Calculate `mainRect` and set `clipPath`.
        // mainRect should not be calculated by `this.group.getBoundingRect()`
        // for sake of the overflow.
        const mainRect = {x: 0, y: 0} as ZRRectLike;

        // Consider content may be overflow (should be clipped).
        mainRect[wh] = showController ? maxSize[wh] : contentRect[wh];
        mainRect[hw] = Math.max(contentRect[hw], controllerRect[hw]);

        // `containerRect[yx] + containerPos[1 - orientIdx]` is 0.
        mainRect[yx] = Math.min(0, controllerRect[yx] + controllerPos[1 - orientIdx]);

        containerGroup.__rectSize = maxSize[wh];
        if (showController) {
            const clipShape = {x: 0, y: 0} as graphic.Rect['shape'];
            clipShape[wh] = Math.max(maxSize[wh] - controllerRect[wh] - pageButtonGap, 0);
            clipShape[hw] = mainRect[hw];
            containerGroup.setClipPath(new graphic.Rect({shape: clipShape}));
            // Consider content may be larger than container, container rect
            // can not be obtained from `containerGroup.getBoundingRect()`.
            containerGroup.__rectSize = clipShape[wh];
        }
        else {
            // Do not remove or ignore controller. Keep them set as placeholders.
            controllerGroup.eachChild(function (child: Displayable) {
                child.attr({
                    invisible: true,
                    silent: true
                });
            });
        }

        // Content translate animation.
        const pageInfo = this._getPageInfo(legendModel);
        pageInfo.pageIndex != null && graphic.updateProps(
            contentGroup,
            { x: pageInfo.contentPosition[0], y: pageInfo.contentPosition[1] },
            // When switch from "show controller" to "not show controller", view should be
            // updated immediately without animation, otherwise causes weird effect.
            showController ? legendModel : null
        );

        this._updatePageInfoView(legendModel, pageInfo);

        return mainRect;
    }

    _pageGo(
        to: 'pagePrevDataIndex' | 'pageNextDataIndex',
        legendModel: ScrollableLegendModel,
        api: ExtensionAPI
    ) {
        const scrollDataIndex = this._getPageInfo(legendModel)[to];

        scrollDataIndex != null && api.dispatchAction({
            type: 'legendScroll',
            scrollDataIndex: scrollDataIndex,
            legendId: legendModel.id
        });
    }

    _updatePageInfoView(
        legendModel: ScrollableLegendModel,
        pageInfo: PageInfo
    ) {
        const controllerGroup = this._controllerGroup;

        zrUtil.each(['pagePrev', 'pageNext'], function (name) {
            const key = (name + 'DataIndex') as'pagePrevDataIndex' | 'pageNextDataIndex';
            const canJump = pageInfo[key] != null;
            const icon = controllerGroup.childOfName(name) as graphic.Path;
            if (icon) {
                icon.setStyle(
                    'fill',
                    canJump
                        ? legendModel.get('pageIconColor', true)
                        : legendModel.get('pageIconInactiveColor', true)
                );
                icon.cursor = canJump ? 'pointer' : 'default';
            }
        });

        const pageText = controllerGroup.childOfName('pageText') as graphic.Text;
        const pageFormatter = legendModel.get('pageFormatter');
        const pageIndex = pageInfo.pageIndex;
        const current = pageIndex != null ? pageIndex + 1 : 0;
        const total = pageInfo.pageCount;

        pageText && pageFormatter && pageText.setStyle(
            'text',
            zrUtil.isString(pageFormatter)
                ? pageFormatter.replace('{current}', current == null ? '' : current + '')
                    .replace('{total}', total == null ? '' : total + '')
                : pageFormatter({current: current, total: total})
        );
    }

    /**
     *  contentPosition: Array.<number>, null when data item not found.
     *  pageIndex: number, null when data item not found.
     *  pageCount: number, always be a number, can be 0.
     *  pagePrevDataIndex: number, null when no previous page.
     *  pageNextDataIndex: number, null when no next page.
     * }
     */
    _getPageInfo(legendModel: ScrollableLegendModel): PageInfo {
        const scrollDataIndex = legendModel.get('scrollDataIndex', true);
        const contentGroup = this.getContentGroup();
        const containerRectSize = this._containerGroup.__rectSize;
        const orientIdx = legendModel.getOrient().index;
        const wh = WH[orientIdx];
        const xy = XY[orientIdx];

        const targetItemIndex = this._findTargetItemIndex(scrollDataIndex);
        const children = contentGroup.children();
        const targetItem = children[targetItemIndex];
        const itemCount = children.length;
        const pCount = !itemCount ? 0 : 1;

        const result: PageInfo = {
            contentPosition: [contentGroup.x, contentGroup.y],
            pageCount: pCount,
            pageIndex: pCount - 1,
            pagePrevDataIndex: null,
            pageNextDataIndex: null
        };

        if (!targetItem) {
            return result;
        }

        const targetItemInfo = getItemInfo(targetItem);
        result.contentPosition[orientIdx] = -targetItemInfo.s;

        // Strategy:
        // (1) Always align based on the left/top most item.
        // (2) It is user-friendly that the last item shown in the
        // current window is shown at the begining of next window.
        // Otherwise if half of the last item is cut by the window,
        // it will have no chance to display entirely.
        // (3) Consider that item size probably be different, we
        // have calculate pageIndex by size rather than item index,
        // and we can not get page index directly by division.
        // (4) The window is to narrow to contain more than
        // one item, we should make sure that the page can be fliped.

        for (let i = targetItemIndex + 1,
            winStartItemInfo = targetItemInfo,
            winEndItemInfo = targetItemInfo,
            currItemInfo = null;
            i <= itemCount;
            ++i
        ) {
            currItemInfo = getItemInfo(children[i]);
            if (
                // Half of the last item is out of the window.
                (!currItemInfo && winEndItemInfo.e > winStartItemInfo.s + containerRectSize)
                // If the current item does not intersect with the window, the new page
                // can be started at the current item or the last item.
                || (currItemInfo && !intersect(currItemInfo, winStartItemInfo.s))
            ) {
                if (winEndItemInfo.i > winStartItemInfo.i) {
                    winStartItemInfo = winEndItemInfo;
                }
                else { // e.g., when page size is smaller than item size.
                    winStartItemInfo = currItemInfo;
                }
                if (winStartItemInfo) {
                    if (result.pageNextDataIndex == null) {
                        result.pageNextDataIndex = winStartItemInfo.i;
                    }
                    ++result.pageCount;
                }
            }
            winEndItemInfo = currItemInfo;
        }

        for (let i = targetItemIndex - 1,
            winStartItemInfo = targetItemInfo,
            winEndItemInfo = targetItemInfo,
            currItemInfo = null;
            i >= -1;
            --i
        ) {
            currItemInfo = getItemInfo(children[i]);
            if (
                // If the the end item does not intersect with the window started
                // from the current item, a page can be settled.
                (!currItemInfo || !intersect(winEndItemInfo, currItemInfo.s))
                // e.g., when page size is smaller than item size.
                && winStartItemInfo.i < winEndItemInfo.i
            ) {
                winEndItemInfo = winStartItemInfo;
                if (result.pagePrevDataIndex == null) {
                    result.pagePrevDataIndex = winStartItemInfo.i;
                }
                ++result.pageCount;
                ++result.pageIndex;
            }
            winStartItemInfo = currItemInfo;
        }

        return result;

        function getItemInfo(el: Element): ItemInfo {
            if (el) {
                const itemRect = el.getBoundingRect();
                const start = itemRect[xy] + el[xy];
                return {
                    s: start,
                    e: start + itemRect[wh],
                    i: (el as LegendItemElement).__legendDataIndex
                };
            }
        }

        function intersect(itemInfo: ItemInfo, winStart: number) {
            return itemInfo.e >= winStart && itemInfo.s <= winStart + containerRectSize;
        }
    }

    _findTargetItemIndex(targetDataIndex: number) {
        if (!this._showController) {
            return 0;
        }

        let index;
        const contentGroup = this.getContentGroup();
        let defaultIndex: number;

        contentGroup.eachChild(function (child, idx) {
            const legendDataIdx = (child as LegendItemElement).__legendDataIndex;
            // FIXME
            // If the given targetDataIndex (from model) is illegal,
            // we use defaultIndex. But the index on the legend model and
            // action payload is still illegal. That case will not be
            // changed until some scenario requires.
            if (defaultIndex == null && legendDataIdx != null) {
                defaultIndex = idx;
            }
            if (legendDataIdx === targetDataIndex) {
                index = idx;
            }
        });

        return index != null ? index : defaultIndex;
    }
}

export default ScrollableLegendView;

相关信息

echarts 源码目录

相关文章

echarts LegendModel 源码

echarts LegendView 源码

echarts ScrollableLegendModel 源码

echarts install 源码

echarts installLegendPlain 源码

echarts installLegendScroll 源码

echarts legendAction 源码

echarts legendFilter 源码

echarts scrollableLegendAction 源码

0  赞