You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@echarts.apache.org by sh...@apache.org on 2021/11/18 04:17:16 UTC

[echarts] branch graphic-animation updated: refact(graphic): seperate view and model

This is an automated email from the ASF dual-hosted git repository.

shenyi pushed a commit to branch graphic-animation
in repository https://gitbox.apache.org/repos/asf/echarts.git


The following commit(s) were added to refs/heads/graphic-animation by this push:
     new 3d7bf24  refact(graphic): seperate view and model
3d7bf24 is described below

commit 3d7bf245bfabf2ec2155378b839a7b2573b5bebd
Author: pissang <bm...@gmail.com>
AuthorDate: Thu Nov 18 12:12:47 2021 +0800

    refact(graphic): seperate view and model
---
 src/component/graphic/GraphicModel.ts | 404 ++++++++++++++++++
 src/component/graphic/GraphicView.ts  | 352 +++++++++++++++
 src/component/graphic/install.ts      | 777 +---------------------------------
 src/export/option.ts                  |   2 +-
 4 files changed, 780 insertions(+), 755 deletions(-)

diff --git a/src/component/graphic/GraphicModel.ts b/src/component/graphic/GraphicModel.ts
new file mode 100644
index 0000000..e0d8f88
--- /dev/null
+++ b/src/component/graphic/GraphicModel.ts
@@ -0,0 +1,404 @@
+/*
+* 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.
+*/
+
+import * as zrUtil from 'zrender/src/core/util';
+import * as modelUtil from '../../util/model';
+import {
+    ComponentOption,
+    BoxLayoutOptionMixin,
+    Dictionary,
+    ZRStyleProps,
+    OptionId,
+    OptionPreprocessor,
+    CommonTooltipOption
+} from '../../util/types';
+import ComponentModel from '../../model/Component';
+import Element, { ElementTextConfig } from 'zrender/src/Element';
+import Displayable from 'zrender/src/graphic/Displayable';
+import { PathProps } from 'zrender/src/graphic/Path';
+import { ImageStyleProps } from 'zrender/src/graphic/Image';
+import GlobalModel from '../../model/Global';
+import { TextStyleProps } from 'zrender/src/graphic/Text';
+import { copyLayoutParams, mergeLayoutParam } from '../../util/layout';
+
+interface GraphicComponentBaseElementOption extends
+    Partial<Pick<
+        Element, TransformProp |
+        'silent' |
+        'ignore' |
+        'draggable' |
+        'textConfig' |
+        'onclick' |
+        'ondblclick' |
+        'onmouseover' |
+        'onmouseout' |
+        'onmousemove' |
+        'onmousewheel' |
+        'onmousedown' |
+        'onmouseup' |
+        'oncontextmenu' |
+        'ondrag' |
+        'ondragstart' |
+        'ondragend' |
+        'ondragenter' |
+        'ondragleave' |
+        'ondragover' |
+        'ondrop'
+    >>,
+    /**
+     * left/right/top/bottom: (like 12, '22%', 'center', default undefined)
+     * If left/rigth is set, shape.x/shape.cx/position will not be used.
+     * If top/bottom is set, shape.y/shape.cy/position will not be used.
+     * This mechanism is useful when you want to position a group/element
+     * against the right side or the center of this container.
+     */
+    Partial<Pick<BoxLayoutOptionMixin, 'left' | 'right' | 'top' | 'bottom'>> {
+
+    /**
+     * element type, mandatory.
+     * Only can be omit if call setOption not at the first time and perform merge.
+     */
+    type?: string;
+
+    id?: OptionId;
+    name?: string;
+
+    // Only internal usage. Use specified value does NOT make sense.
+    parentId?: OptionId;
+    parentOption?: GraphicComponentElementOption;
+    children?: GraphicComponentElementOption[];
+    hv?: [boolean, boolean];
+
+    /**
+     * bounding: (enum: 'all' (default) | 'raw')
+     * Specify how to calculate boundingRect when locating.
+     * 'all': Get uioned and transformed boundingRect
+     *     from both itself and its descendants.
+     *     This mode simplies confining a group of elements in the bounding
+     *     of their ancester container (e.g., using 'right: 0').
+     * 'raw': Only use the boundingRect of itself and before transformed.
+     *     This mode is similar to css behavior, which is useful when you
+     *     want an element to be able to overflow its container. (Consider
+     *     a rotated circle needs to be located in a corner.)
+     */
+    bounding?: 'raw' | 'all';
+
+    /**
+     * info: custom info. enables user to mount some info on elements and use them
+     * in event handlers. Update them only when user specified, otherwise, remain.
+     */
+    info?: GraphicExtraElementInfo;
+
+    textContent?: GraphicComponentTextOption;
+    textConfig?: ElementTextConfig;
+
+    $action?: 'merge' | 'replace' | 'remove';
+
+    tooltip?: CommonTooltipOption<unknown>;
+};
+
+
+export type TransformProp = 'x' | 'y' | 'scaleX' | 'scaleY' | 'originX' | 'originY' | 'skewX' | 'skewY' | 'rotation';
+
+export interface GraphicComponentDisplayableOption extends
+    GraphicComponentBaseElementOption, Partial<Pick<Displayable, 'zlevel' | 'z' | 'z2' | 'invisible' | 'cursor'>> {
+
+    style?: ZRStyleProps;
+}
+// TODO: states?
+// interface GraphicComponentDisplayableOptionOnState extends Partial<Pick<
+//     Displayable, TransformProp | 'textConfig' | 'z2'
+// >> {
+//     style?: ZRStyleProps;
+// }
+export interface GraphicComponentGroupOption extends GraphicComponentBaseElementOption {
+    type?: 'group';
+
+    /**
+     * width/height: (can only be pixel value, default 0)
+     * Only be used to specify contianer(group) size, if needed. And
+     * can not be percentage value (like '33%'). See the reason in the
+     * layout algorithm below.
+     */
+    width?: number;
+    height?: number;
+
+    // TODO: Can only set focus, blur on the root element.
+    // children: Omit<GraphicComponentElementOption, 'focus' | 'blurScope'>[];
+    children: GraphicComponentElementOption[];
+}
+export interface GraphicComponentZRPathOption extends GraphicComponentDisplayableOption {
+    shape?: PathProps['shape'];
+}
+export interface GraphicComponentImageOption extends GraphicComponentDisplayableOption {
+    type?: 'image';
+    style?: ImageStyleProps;
+}
+// TODO: states?
+// interface GraphicComponentImageOptionOnState extends GraphicComponentDisplayableOptionOnState {
+//     style?: ImageStyleProps;
+// }
+interface GraphicComponentTextOption
+    extends Omit<GraphicComponentDisplayableOption, 'textContent' | 'textConfig'> {
+    type?: 'text';
+    style?: TextStyleProps;
+}
+export type GraphicComponentElementOption =
+    GraphicComponentGroupOption |
+    GraphicComponentZRPathOption |
+    GraphicComponentImageOption |
+    GraphicComponentTextOption;
+// type GraphicComponentElementOptionOnState =
+//     GraphicComponentDisplayableOptionOnState
+//     | GraphicComponentImageOptionOnState;
+type GraphicExtraElementInfo = Dictionary<unknown>;
+export type ElementMap = zrUtil.HashMap<Element, string>;
+
+
+export type GraphicComponentLooseOption = (GraphicComponentOption | GraphicComponentElementOption) & {
+    mainType?: 'graphic';
+};
+
+export interface GraphicComponentOption extends ComponentOption {
+    // Note: elements is always behind its ancestors in this elements array.
+    elements?: GraphicComponentElementOption[];
+}
+;
+
+export function setKeyInfoToNewElOption(
+    resultItem: ReturnType<typeof modelUtil.mappingToExists>[number],
+    newElOption: GraphicComponentElementOption
+): void {
+    const existElOption = resultItem.existing as GraphicComponentElementOption;
+
+    // Set id and type after id assigned.
+    newElOption.id = resultItem.keyInfo.id;
+    !newElOption.type && existElOption && (newElOption.type = existElOption.type);
+
+    // Set parent id if not specified
+    if (newElOption.parentId == null) {
+        const newElParentOption = newElOption.parentOption;
+        if (newElParentOption) {
+            newElOption.parentId = newElParentOption.id;
+        }
+        else if (existElOption) {
+            newElOption.parentId = existElOption.parentId;
+        }
+    }
+
+    // Clear
+    newElOption.parentOption = null;
+}
+
+function isSetLoc(
+    obj: GraphicComponentElementOption,
+    props: ('left' | 'right' | 'top' | 'bottom')[]
+): boolean {
+    let isSet;
+    zrUtil.each(props, function (prop) {
+        obj[prop] != null && obj[prop] !== 'auto' && (isSet = true);
+    });
+    return isSet;
+}
+function mergeNewElOptionToExist(
+    existList: GraphicComponentElementOption[],
+    index: number,
+    newElOption: GraphicComponentElementOption
+): void {
+    // Update existing options, for `getOption` feature.
+    const newElOptCopy = zrUtil.extend({}, newElOption);
+    const existElOption = existList[index];
+
+    const $action = newElOption.$action || 'merge';
+    if ($action === 'merge') {
+        if (existElOption) {
+
+            if (__DEV__) {
+                const newType = newElOption.type;
+                zrUtil.assert(
+                    !newType || existElOption.type === newType,
+                    'Please set $action: "replace" to change `type`'
+                );
+            }
+
+            // We can ensure that newElOptCopy and existElOption are not
+            // the same object, so `merge` will not change newElOptCopy.
+            zrUtil.merge(existElOption, newElOptCopy, true);
+            // Rigid body, use ignoreSize.
+            mergeLayoutParam(existElOption, newElOptCopy, { ignoreSize: true });
+            // Will be used in render.
+            copyLayoutParams(newElOption, existElOption);
+        }
+        else {
+            existList[index] = newElOptCopy;
+        }
+    }
+    else if ($action === 'replace') {
+        existList[index] = newElOptCopy;
+    }
+    else if ($action === 'remove') {
+        // null will be cleaned later.
+        existElOption && (existList[index] = null);
+    }
+}
+
+function setLayoutInfoToExist(
+    existItem: GraphicComponentElementOption,
+    newElOption: GraphicComponentElementOption
+) {
+    if (!existItem) {
+        return;
+    }
+    existItem.hv = newElOption.hv = [
+        // Rigid body, dont care `width`.
+        isSetLoc(newElOption, ['left', 'right']),
+        // Rigid body, dont care `height`.
+        isSetLoc(newElOption, ['top', 'bottom'])
+    ];
+    // Give default group size. Otherwise layout error may occur.
+    if (existItem.type === 'group') {
+        const existingGroupOpt = existItem as GraphicComponentGroupOption;
+        const newGroupOpt = newElOption as GraphicComponentGroupOption;
+        existingGroupOpt.width == null && (existingGroupOpt.width = newGroupOpt.width = 0);
+        existingGroupOpt.height == null && (existingGroupOpt.height = newGroupOpt.height = 0);
+    }
+}
+
+export class GraphicComponentModel extends ComponentModel<GraphicComponentOption> {
+
+    static type = 'graphic';
+    type = GraphicComponentModel.type;
+
+    preventAutoZ = true;
+
+    static defaultOption: GraphicComponentOption = {
+        elements: []
+        // parentId: null
+    };
+
+    /**
+     * Save el options for the sake of the performance (only update modified graphics).
+     * The order is the same as those in option. (ancesters -> descendants)
+     */
+    private _elOptionsToUpdate: GraphicComponentElementOption[];
+
+    mergeOption(option: GraphicComponentOption, ecModel: GlobalModel): void {
+        // Prevent default merge to elements
+        const elements = this.option.elements;
+        this.option.elements = null;
+
+        super.mergeOption(option, ecModel);
+
+        this.option.elements = elements;
+    }
+
+    optionUpdated(newOption: GraphicComponentOption, isInit: boolean): void {
+        const thisOption = this.option;
+        const newList = (isInit ? thisOption : newOption).elements;
+        const existList = thisOption.elements = isInit ? [] : thisOption.elements;
+
+        const flattenedList = [] as GraphicComponentElementOption[];
+        this._flatten(newList, flattenedList, null);
+
+        const mappingResult = modelUtil.mappingToExists(existList, flattenedList, 'normalMerge');
+
+        // Clear elOptionsToUpdate
+        const elOptionsToUpdate = this._elOptionsToUpdate = [] as GraphicComponentElementOption[];
+
+        zrUtil.each(mappingResult, function (resultItem, index) {
+            const newElOption = resultItem.newOption as GraphicComponentElementOption;
+
+            if (__DEV__) {
+                zrUtil.assert(
+                    zrUtil.isObject(newElOption) || resultItem.existing,
+                    'Empty graphic option definition'
+                );
+            }
+
+            if (!newElOption) {
+                return;
+            }
+
+            elOptionsToUpdate.push(newElOption);
+
+            setKeyInfoToNewElOption(resultItem, newElOption);
+
+            mergeNewElOptionToExist(existList, index, newElOption);
+
+            setLayoutInfoToExist(existList[index], newElOption);
+
+        }, this);
+
+        // Clean
+        thisOption.elements = zrUtil.filter(existList, (item) => {
+            // $action should be volatile, otherwise option gotten from
+            // `getOption` will contain unexpected $action.
+            item && delete item.$action;
+            return item != null;
+        });
+    }
+
+    /**
+     * Convert
+     * [{
+     *  type: 'group',
+     *  id: 'xx',
+     *  children: [{type: 'circle'}, {type: 'polygon'}]
+     * }]
+     * to
+     * [
+     *  {type: 'group', id: 'xx'},
+     *  {type: 'circle', parentId: 'xx'},
+     *  {type: 'polygon', parentId: 'xx'}
+     * ]
+     */
+    private _flatten(
+        optionList: GraphicComponentElementOption[],
+        result: GraphicComponentElementOption[],
+        parentOption: GraphicComponentElementOption
+    ): void {
+        zrUtil.each(optionList, function (option) {
+            if (!option) {
+                return;
+            }
+
+            if (parentOption) {
+                option.parentOption = parentOption;
+            }
+
+            result.push(option);
+
+            const children = option.children;
+            if (option.type === 'group' && children) {
+                this._flatten(children, result, option);
+            }
+            // Deleting for JSON output, and for not affecting group creation.
+            delete option.children;
+        }, this);
+    }
+
+    // FIXME
+    // Pass to view using payload? setOption has a payload?
+    useElOptionsToUpdate(): GraphicComponentElementOption[] {
+        const els = this._elOptionsToUpdate;
+        // Clear to avoid render duplicately when zooming.
+        this._elOptionsToUpdate = null;
+        return els;
+    }
+}
diff --git a/src/component/graphic/GraphicView.ts b/src/component/graphic/GraphicView.ts
new file mode 100644
index 0000000..8d3f5c2
--- /dev/null
+++ b/src/component/graphic/GraphicView.ts
@@ -0,0 +1,352 @@
+/*
+* 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.
+*/
+
+import * as zrUtil from 'zrender/src/core/util';
+import * as modelUtil from '../../util/model';
+import * as graphicUtil from '../../util/graphic';
+import * as layoutUtil from '../../util/layout';
+import { parsePercent } from '../../util/number';
+import Element from 'zrender/src/Element';
+import GlobalModel from '../../model/Global';
+import ComponentView from '../../view/Component';
+import ExtensionAPI from '../../core/ExtensionAPI';
+import { getECData } from '../../util/innerStore';
+import { TextStyleProps } from 'zrender/src/graphic/Text';
+import { isEC4CompatibleStyle, convertFromEC4CompatibleStyle } from '../../util/styleCompat';
+import {
+    ElementMap,
+    GraphicComponentModel,
+    GraphicComponentDisplayableOption,
+    GraphicComponentZRPathOption,
+    GraphicComponentGroupOption,
+    GraphicComponentElementOption
+} from './GraphicModel';
+
+const _nonShapeGraphicElements = {
+    // Reserved but not supported in graphic component.
+    path: null as unknown,
+    compoundPath: null as unknown,
+
+    // Supported in graphic component.
+    group: graphicUtil.Group,
+    image: graphicUtil.Image,
+    text: graphicUtil.Text
+} as const;
+type NonShapeGraphicElementType = keyof typeof _nonShapeGraphicElements;
+
+export const inner = modelUtil.makeInner<{
+    widthOption: number;
+    heightOption: number;
+    width: number;
+    height: number;
+    id: string;
+}, Element>();
+// ------------------------
+// View
+// ------------------------
+export class GraphicComponentView extends ComponentView {
+
+    static type = 'graphic';
+    type = GraphicComponentView.type;
+
+    private _elMap: ElementMap;
+    private _lastGraphicModel: GraphicComponentModel;
+
+    init() {
+        this._elMap = zrUtil.createHashMap();
+    }
+
+    render(graphicModel: GraphicComponentModel, ecModel: GlobalModel, api: ExtensionAPI): void {
+        // Having leveraged between use cases and algorithm complexity, a very
+        // simple layout mechanism is used:
+        // The size(width/height) can be determined by itself or its parent (not
+        // implemented yet), but can not by its children. (Top-down travel)
+        // The location(x/y) can be determined by the bounding rect of itself
+        // (can including its descendants or not) and the size of its parent.
+        // (Bottom-up travel)
+
+        // When `chart.clear()` or `chart.setOption({...}, true)` with the same id,
+        // view will be reused.
+        if (graphicModel !== this._lastGraphicModel) {
+            this._clear();
+        }
+        this._lastGraphicModel = graphicModel;
+
+        this._updateElements(graphicModel);
+        this._relocate(graphicModel, api);
+    }
+
+    /**
+     * Update graphic elements.
+     */
+    private _updateElements(graphicModel: GraphicComponentModel): void {
+        const elOptionsToUpdate = graphicModel.useElOptionsToUpdate();
+
+        if (!elOptionsToUpdate) {
+            return;
+        }
+
+        const elMap = this._elMap;
+        const rootGroup = this.group;
+
+        // Top-down tranverse to assign graphic settings to each elements.
+        zrUtil.each(elOptionsToUpdate, function (elOption) {
+            const id = modelUtil.convertOptionIdName(elOption.id, null);
+            const elExisting = id != null ? elMap.get(id) : null;
+            const parentId = modelUtil.convertOptionIdName(elOption.parentId, null);
+            const targetElParent = (parentId != null ? elMap.get(parentId) : rootGroup) as graphicUtil.Group;
+
+            const elType = elOption.type;
+            const elOptionStyle = (elOption as GraphicComponentDisplayableOption).style;
+            if (elType === 'text' && elOptionStyle) {
+                // In top/bottom mode, textVerticalAlign should not be used, which cause
+                // inaccurately locating.
+                if (elOption.hv && elOption.hv[1]) {
+                    (elOptionStyle as any).textVerticalAlign =
+                        (elOptionStyle as any).textBaseline =
+                        (elOptionStyle as TextStyleProps).verticalAlign =
+                        (elOptionStyle as TextStyleProps).align = null;
+                }
+            }
+
+            let textContentOption = (elOption as GraphicComponentZRPathOption).textContent;
+            let textConfig = (elOption as GraphicComponentZRPathOption).textConfig;
+            if (elOptionStyle
+                && isEC4CompatibleStyle(elOptionStyle, elType, !!textConfig, !!textContentOption)) {
+                const convertResult =
+                    convertFromEC4CompatibleStyle(elOptionStyle, elType, true) as GraphicComponentZRPathOption;
+                if (!textConfig && convertResult.textConfig) {
+                    textConfig = (elOption as GraphicComponentZRPathOption).textConfig = convertResult.textConfig;
+                }
+                if (!textContentOption && convertResult.textContent) {
+                    textContentOption = convertResult.textContent;
+                }
+            }
+
+            // Remove unnecessary props to avoid potential problems.
+            const elOptionCleaned = getCleanedElOption(elOption);
+
+            // For simple, do not support parent change, otherwise reorder is needed.
+            if (__DEV__) {
+                elExisting && zrUtil.assert(
+                    targetElParent === elExisting.parent,
+                    'Changing parent is not supported.'
+                );
+            }
+
+            const $action = elOption.$action || 'merge';
+            if ($action === 'merge') {
+                elExisting
+                    ? elExisting.attr(elOptionCleaned)
+                    : createEl(id, targetElParent, elOptionCleaned, elMap);
+            }
+            else if ($action === 'replace') {
+                removeEl(elExisting, elMap);
+                createEl(id, targetElParent, elOptionCleaned, elMap);
+            }
+            else if ($action === 'remove') {
+                removeEl(elExisting, elMap);
+            }
+
+            const el = elMap.get(id);
+
+            if (el && textContentOption) {
+                if ($action === 'merge') {
+                    const textContentExisting = el.getTextContent();
+                    textContentExisting
+                        ? textContentExisting.attr(textContentOption)
+                        : el.setTextContent(new graphicUtil.Text(textContentOption));
+                }
+                else if ($action === 'replace') {
+                    el.setTextContent(new graphicUtil.Text(textContentOption));
+                }
+            }
+
+            if (el) {
+                const elInner = inner(el);
+                elInner.widthOption = (elOption as GraphicComponentGroupOption).width;
+                elInner.heightOption = (elOption as GraphicComponentGroupOption).height;
+                setEventData(el, graphicModel, elOption);
+
+                graphicUtil.setTooltipConfig({
+                    el: el,
+                    componentModel: graphicModel,
+                    itemName: el.name,
+                    itemTooltipOption: elOption.tooltip
+                });
+            }
+        });
+    }
+
+    /**
+     * Locate graphic elements.
+     */
+    private _relocate(graphicModel: GraphicComponentModel, api: ExtensionAPI): void {
+        const elOptions = graphicModel.option.elements;
+        const rootGroup = this.group;
+        const elMap = this._elMap;
+        const apiWidth = api.getWidth();
+        const apiHeight = api.getHeight();
+
+        // Top-down to calculate percentage width/height of group
+        for (let i = 0; i < elOptions.length; i++) {
+            const elOption = elOptions[i];
+            const id = modelUtil.convertOptionIdName(elOption.id, null);
+            const el = id != null ? elMap.get(id) : null;
+
+            if (!el || !el.isGroup) {
+                continue;
+            }
+            const parentEl = el.parent;
+            const isParentRoot = parentEl === rootGroup;
+            // Like 'position:absolut' in css, default 0.
+            const elInner = inner(el);
+            const parentElInner = inner(parentEl);
+            elInner.width = parsePercent(
+                elInner.widthOption,
+                isParentRoot ? apiWidth : parentElInner.width
+            ) || 0;
+            elInner.height = parsePercent(
+                elInner.heightOption,
+                isParentRoot ? apiHeight : parentElInner.height
+            ) || 0;
+        }
+
+        // Bottom-up tranvese all elements (consider ec resize) to locate elements.
+        for (let i = elOptions.length - 1; i >= 0; i--) {
+            const elOption = elOptions[i];
+            const id = modelUtil.convertOptionIdName(elOption.id, null);
+            const el = id != null ? elMap.get(id) : null;
+
+            if (!el) {
+                continue;
+            }
+
+            const parentEl = el.parent;
+            const parentElInner = inner(parentEl);
+            const containerInfo = parentEl === rootGroup
+                ? {
+                    width: apiWidth,
+                    height: apiHeight
+                }
+                : {
+                    width: parentElInner.width,
+                    height: parentElInner.height
+                };
+
+            // PENDING
+            // Currently, when `bounding: 'all'`, the union bounding rect of the group
+            // does not include the rect of [0, 0, group.width, group.height], which
+            // is probably weird for users. Should we make a break change for it?
+            layoutUtil.positionElement(
+                el, elOption, containerInfo, null,
+                { hv: elOption.hv, boundingMode: elOption.bounding }
+            );
+        }
+    }
+
+    /**
+     * Clear all elements.
+     */
+    private _clear(): void {
+        const elMap = this._elMap;
+        elMap.each(function (el) {
+            removeEl(el, elMap);
+        });
+        this._elMap = zrUtil.createHashMap();
+    }
+
+    dispose(): void {
+        this._clear();
+    }
+}
+function createEl(
+    id: string,
+    targetElParent: graphicUtil.Group,
+    elOption: GraphicComponentElementOption,
+    elMap: ElementMap
+): void {
+    const graphicType = elOption.type;
+
+    if (__DEV__) {
+        zrUtil.assert(graphicType, 'graphic type MUST be set');
+    }
+
+    const Clz = (
+        zrUtil.hasOwn(_nonShapeGraphicElements, graphicType)
+            // Those graphic elements are not shapes. They should not be
+            // overwritten by users, so do them first.
+            ? _nonShapeGraphicElements[graphicType as NonShapeGraphicElementType]
+            : graphicUtil.getShapeClass(graphicType)
+    ) as { new(opt: GraphicComponentElementOption): Element; };
+
+    if (__DEV__) {
+        zrUtil.assert(Clz, 'graphic type can not be found');
+    }
+
+    const el = new Clz(elOption);
+    targetElParent.add(el);
+    elMap.set(id, el);
+    inner(el).id = id;
+}
+function removeEl(elExisting: Element, elMap: ElementMap): void {
+    const existElParent = elExisting && elExisting.parent;
+    if (existElParent) {
+        elExisting.type === 'group' && elExisting.traverse(function (el) {
+            removeEl(el, elMap);
+        });
+        elMap.removeKey(inner(elExisting).id);
+        existElParent.remove(elExisting);
+    }
+}
+// Remove unnecessary props to avoid potential problems.
+function getCleanedElOption(
+    elOption: GraphicComponentElementOption
+): Omit<GraphicComponentElementOption, 'textContent'> {
+    elOption = zrUtil.extend({}, elOption);
+    zrUtil.each(
+        ['id', 'parentId', '$action', 'hv', 'bounding', 'textContent'].concat(layoutUtil.LOCATION_PARAMS),
+        function (name) {
+            delete (elOption as any)[name];
+        }
+    );
+    return elOption;
+}
+
+function setEventData(
+    el: Element,
+    graphicModel: GraphicComponentModel,
+    elOption: GraphicComponentElementOption
+): void {
+    let eventData = getECData(el).eventData;
+    // Simple optimize for large amount of elements that no need event.
+    if (!el.silent && !el.ignore && !eventData) {
+        eventData = getECData(el).eventData = {
+            componentType: 'graphic',
+            componentIndex: graphicModel.componentIndex,
+            name: el.name
+        };
+    }
+
+    // `elOption.info` enables user to mount some info on
+    // elements and use them in event handlers.
+    if (eventData) {
+        eventData.info = elOption.info;
+    }
+}
diff --git a/src/component/graphic/install.ts b/src/component/graphic/install.ts
index bfa2a82..4251932 100644
--- a/src/component/graphic/install.ts
+++ b/src/component/graphic/install.ts
@@ -18,769 +18,38 @@
 */
 
 
-import * as zrUtil from 'zrender/src/core/util';
 
-import * as modelUtil from '../../util/model';
-import * as graphicUtil from '../../util/graphic';
-import * as layoutUtil from '../../util/layout';
-import {parsePercent} from '../../util/number';
-import {
-    ComponentOption,
-    BoxLayoutOptionMixin,
-    Dictionary,
-    ZRStyleProps,
-    OptionId,
-    OptionPreprocessor,
-    CommonTooltipOption
-} from '../../util/types';
-import ComponentModel from '../../model/Component';
-import Element, { ElementTextConfig } from 'zrender/src/Element';
-import Displayable from 'zrender/src/graphic/Displayable';
-import { PathProps } from 'zrender/src/graphic/Path';
-import { ImageStyleProps } from 'zrender/src/graphic/Image';
-import GlobalModel from '../../model/Global';
-import ComponentView from '../../view/Component';
-import ExtensionAPI from '../../core/ExtensionAPI';
-import { getECData } from '../../util/innerStore';
-import { TextStyleProps } from 'zrender/src/graphic/Text';
-import { isEC4CompatibleStyle, convertFromEC4CompatibleStyle } from '../../util/styleCompat';
+import { isArray } from 'zrender/src/core/util';
 import { EChartsExtensionInstallRegisters } from '../../extension';
+import { GraphicComponentModel, GraphicComponentOption } from './GraphicModel';
+import { GraphicComponentView } from './GraphicView';
 
-type TransformProp = 'x' | 'y' | 'scaleX' | 'scaleY' | 'originX' | 'originY' | 'skewX' | 'skewY' | 'rotation';
-
-interface GraphicComponentBaseElementOption extends
-        Partial<Pick<
-            Element,
-            TransformProp
-            | 'silent'
-            | 'ignore'
-            | 'draggable'
-            | 'textConfig'
-            | 'onclick'
-            | 'ondblclick'
-            | 'onmouseover'
-            | 'onmouseout'
-            | 'onmousemove'
-            | 'onmousewheel'
-            | 'onmousedown'
-            | 'onmouseup'
-            | 'oncontextmenu'
-            | 'ondrag'
-            | 'ondragstart'
-            | 'ondragend'
-            | 'ondragenter'
-            | 'ondragleave'
-            | 'ondragover'
-            | 'ondrop'
-        >>,
-        /**
-         * left/right/top/bottom: (like 12, '22%', 'center', default undefined)
-         * If left/rigth is set, shape.x/shape.cx/position will not be used.
-         * If top/bottom is set, shape.y/shape.cy/position will not be used.
-         * This mechanism is useful when you want to position a group/element
-         * against the right side or the center of this container.
-         */
-        Partial<Pick<BoxLayoutOptionMixin, 'left' | 'right' | 'top' | 'bottom'>> {
-
-    /**
-     * element type, mandatory.
-     * Only can be omit if call setOption not at the first time and perform merge.
-     */
-    type?: string;
-
-    id?: OptionId;
-    name?: string;
-
-    // Only internal usage. Use specified value does NOT make sense.
-    parentId?: OptionId;
-    parentOption?: GraphicComponentElementOption;
-    children?: GraphicComponentElementOption[];
-    hv?: [boolean, boolean];
-
-    /**
-     * bounding: (enum: 'all' (default) | 'raw')
-     * Specify how to calculate boundingRect when locating.
-     * 'all': Get uioned and transformed boundingRect
-     *     from both itself and its descendants.
-     *     This mode simplies confining a group of elements in the bounding
-     *     of their ancester container (e.g., using 'right: 0').
-     * 'raw': Only use the boundingRect of itself and before transformed.
-     *     This mode is similar to css behavior, which is useful when you
-     *     want an element to be able to overflow its container. (Consider
-     *     a rotated circle needs to be located in a corner.)
-     */
-    bounding?: 'raw' | 'all';
-
-    /**
-     * info: custom info. enables user to mount some info on elements and use them
-     * in event handlers. Update them only when user specified, otherwise, remain.
-     */
-    info?: GraphicExtraElementInfo;
-
-    textContent?: GraphicComponentTextOption;
-    textConfig?: ElementTextConfig;
-
-    $action?: 'merge' | 'replace' | 'remove';
-
-    tooltip?: CommonTooltipOption<unknown>;
-};
-
-interface GraphicComponentDisplayableOption extends
-        GraphicComponentBaseElementOption,
-        Partial<Pick<Displayable, 'zlevel' | 'z' | 'z2' | 'invisible' | 'cursor'>> {
-
-    style?: ZRStyleProps;
-
-    // TODO: states?
-    // emphasis?: GraphicComponentDisplayableOptionOnState;
-    // blur?: GraphicComponentDisplayableOptionOnState;
-    // select?: GraphicComponentDisplayableOptionOnState;
-}
-// TODO: states?
-// interface GraphicComponentDisplayableOptionOnState extends Partial<Pick<
-//     Displayable, TransformProp | 'textConfig' | 'z2'
-// >> {
-//     style?: ZRStyleProps;
-// }
-interface GraphicComponentGroupOption extends GraphicComponentBaseElementOption {
-    type?: 'group';
-
-    /**
-     * width/height: (can only be pixel value, default 0)
-     * Only be used to specify contianer(group) size, if needed. And
-     * can not be percentage value (like '33%'). See the reason in the
-     * layout algorithm below.
-     */
-    width?: number;
-    height?: number;
-
-    // TODO: Can only set focus, blur on the root element.
-    // children: Omit<GraphicComponentElementOption, 'focus' | 'blurScope'>[];
-    children: GraphicComponentElementOption[];
-}
-export interface GraphicComponentZRPathOption extends GraphicComponentDisplayableOption {
-    shape?: PathProps['shape'];
-}
-export interface GraphicComponentImageOption extends GraphicComponentDisplayableOption {
-    type?: 'image';
-    style?: ImageStyleProps;
-    // TODO: states?
-    // emphasis?: GraphicComponentImageOptionOnState;
-    // blur?: GraphicComponentImageOptionOnState;
-    // select?: GraphicComponentImageOptionOnState;
-}
-// TODO: states?
-// interface GraphicComponentImageOptionOnState extends GraphicComponentDisplayableOptionOnState {
-//     style?: ImageStyleProps;
-// }
-interface GraphicComponentTextOption
-        extends Omit<GraphicComponentDisplayableOption, 'textContent' | 'textConfig'> {
-    type?: 'text';
-    style?: TextStyleProps;
-}
-type GraphicComponentElementOption =
-    GraphicComponentGroupOption
-    | GraphicComponentZRPathOption
-    | GraphicComponentImageOption
-    | GraphicComponentTextOption;
-// type GraphicComponentElementOptionOnState =
-//     GraphicComponentDisplayableOptionOnState
-//     | GraphicComponentImageOptionOnState;
-
-type GraphicExtraElementInfo = Dictionary<unknown>;
-
-type ElementMap = zrUtil.HashMap<Element, string>;
-
-const inner = modelUtil.makeInner<{
-    widthOption: number;
-    heightOption: number;
-    width: number;
-    height: number;
-    id: string;
-}, Element>();
-
-
-const _nonShapeGraphicElements = {
-
-    // Reserved but not supported in graphic component.
-    path: null as unknown,
-    compoundPath: null as unknown,
-
-    // Supported in graphic component.
-    group: graphicUtil.Group,
-    image: graphicUtil.Image,
-    text: graphicUtil.Text
-} as const;
-type NonShapeGraphicElementType = keyof typeof _nonShapeGraphicElements;
-
-// ------------------------
-// Preprocessor
-// ------------------------
-
-const preprocessor: OptionPreprocessor = function (option) {
-    const graphicOption = option.graphic as GraphicComponentOption | GraphicComponentOption[];
-
-    // Convert
-    // {graphic: [{left: 10, type: 'circle'}, ...]}
-    // or
-    // {graphic: {left: 10, type: 'circle'}}
-    // to
-    // {graphic: [{elements: [{left: 10, type: 'circle'}, ...]}]}
-    if (zrUtil.isArray(graphicOption)) {
-        if (!graphicOption[0] || !graphicOption[0].elements) {
-            option.graphic = [{elements: graphicOption}];
-        }
-        else {
-            // Only one graphic instance can be instantiated. (We dont
-            // want that too many views are created in echarts._viewMap)
-            option.graphic = [(option.graphic as any)[0]];
-        }
-    }
-    else if (graphicOption && !graphicOption.elements) {
-        option.graphic = [{elements: [graphicOption]}];
-    }
-};
-
-// ------------------------
-// Model
-// ------------------------
-
-export type GraphicComponentLooseOption = (GraphicComponentOption | GraphicComponentElementOption) & {
-    mainType?: 'graphic';
-};
-
-export interface GraphicComponentOption extends ComponentOption {
-    // Note: elements is always behind its ancestors in this elements array.
-    elements?: GraphicComponentElementOption[];
-    // parentId: string;
-};
-
-
-class GraphicComponentModel extends ComponentModel<GraphicComponentOption> {
-
-    static type = 'graphic';
-    type = GraphicComponentModel.type;
-
-    preventAutoZ = true;
-
-    static defaultOption: GraphicComponentOption = {
-        elements: []
-        // parentId: null
-    };
-
-    /**
-     * Save el options for the sake of the performance (only update modified graphics).
-     * The order is the same as those in option. (ancesters -> descendants)
-     */
-    private _elOptionsToUpdate: GraphicComponentElementOption[];
-
-    mergeOption(option: GraphicComponentOption, ecModel: GlobalModel): void {
-        // Prevent default merge to elements
-        const elements = this.option.elements;
-        this.option.elements = null;
-
-        super.mergeOption(option, ecModel);
-
-        this.option.elements = elements;
-    }
-
-    optionUpdated(newOption: GraphicComponentOption, isInit: boolean): void {
-        const thisOption = this.option;
-        const newList = (isInit ? thisOption : newOption).elements;
-        const existList = thisOption.elements = isInit ? [] : thisOption.elements;
-
-        const flattenedList = [] as GraphicComponentElementOption[];
-        this._flatten(newList, flattenedList, null);
-
-        const mappingResult = modelUtil.mappingToExists(existList, flattenedList, 'normalMerge');
-
-        // Clear elOptionsToUpdate
-        const elOptionsToUpdate = this._elOptionsToUpdate = [] as GraphicComponentElementOption[];
-
-        zrUtil.each(mappingResult, function (resultItem, index) {
-            const newElOption = resultItem.newOption as GraphicComponentElementOption;
-
-            if (__DEV__) {
-                zrUtil.assert(
-                    zrUtil.isObject(newElOption) || resultItem.existing,
-                    'Empty graphic option definition'
-                );
-            }
-
-            if (!newElOption) {
-                return;
-            }
-
-            elOptionsToUpdate.push(newElOption);
-
-            setKeyInfoToNewElOption(resultItem, newElOption);
-
-            mergeNewElOptionToExist(existList, index, newElOption);
-
-            setLayoutInfoToExist(existList[index], newElOption);
-
-        }, this);
-
-        // Clean
-        thisOption.elements = zrUtil.filter(existList, (item) => {
-            // $action should be volatile, otherwise option gotten from
-            // `getOption` will contain unexpected $action.
-            item && delete item.$action;
-            return item != null;
-        });
-    }
-
-    /**
-     * Convert
-     * [{
-     *  type: 'group',
-     *  id: 'xx',
-     *  children: [{type: 'circle'}, {type: 'polygon'}]
-     * }]
-     * to
-     * [
-     *  {type: 'group', id: 'xx'},
-     *  {type: 'circle', parentId: 'xx'},
-     *  {type: 'polygon', parentId: 'xx'}
-     * ]
-     */
-    private _flatten(
-        optionList: GraphicComponentElementOption[],
-        result: GraphicComponentElementOption[],
-        parentOption: GraphicComponentElementOption
-    ): void {
-        zrUtil.each(optionList, function (option) {
-            if (!option) {
-                return;
-            }
-
-            if (parentOption) {
-                option.parentOption = parentOption;
-            }
-
-            result.push(option);
-
-            const children = option.children;
-            if (option.type === 'group' && children) {
-                this._flatten(children, result, option);
-            }
-            // Deleting for JSON output, and for not affecting group creation.
-            delete option.children;
-        }, this);
-    }
-
-    // FIXME
-    // Pass to view using payload? setOption has a payload?
-    useElOptionsToUpdate(): GraphicComponentElementOption[] {
-        const els = this._elOptionsToUpdate;
-        // Clear to avoid render duplicately when zooming.
-        this._elOptionsToUpdate = null;
-        return els;
-    }
-}
-
-// ------------------------
-// View
-// ------------------------
-
-class GraphicComponentView extends ComponentView {
-
-    static type = 'graphic';
-    type = GraphicComponentView.type;
-
-    private _elMap: ElementMap;
-    private _lastGraphicModel: GraphicComponentModel;
-
-    init() {
-        this._elMap = zrUtil.createHashMap();
-    }
-
-    render(graphicModel: GraphicComponentModel, ecModel: GlobalModel, api: ExtensionAPI): void {
-
-        // Having leveraged between use cases and algorithm complexity, a very
-        // simple layout mechanism is used:
-        // The size(width/height) can be determined by itself or its parent (not
-        // implemented yet), but can not by its children. (Top-down travel)
-        // The location(x/y) can be determined by the bounding rect of itself
-        // (can including its descendants or not) and the size of its parent.
-        // (Bottom-up travel)
-
-        // When `chart.clear()` or `chart.setOption({...}, true)` with the same id,
-        // view will be reused.
-        if (graphicModel !== this._lastGraphicModel) {
-            this._clear();
-        }
-        this._lastGraphicModel = graphicModel;
-
-        this._updateElements(graphicModel);
-        this._relocate(graphicModel, api);
-    }
-
-    /**
-     * Update graphic elements.
-     */
-    private _updateElements(graphicModel: GraphicComponentModel): void {
-        const elOptionsToUpdate = graphicModel.useElOptionsToUpdate();
-
-        if (!elOptionsToUpdate) {
-            return;
-        }
-
-        const elMap = this._elMap;
-        const rootGroup = this.group;
-
-        // Top-down tranverse to assign graphic settings to each elements.
-        zrUtil.each(elOptionsToUpdate, function (elOption) {
-            const id = modelUtil.convertOptionIdName(elOption.id, null);
-            const elExisting = id != null ? elMap.get(id) : null;
-            const parentId = modelUtil.convertOptionIdName(elOption.parentId, null);
-            const targetElParent = (parentId != null ? elMap.get(parentId) : rootGroup) as graphicUtil.Group;
-
-            const elType = elOption.type;
-            const elOptionStyle = (elOption as GraphicComponentDisplayableOption).style;
-            if (elType === 'text' && elOptionStyle) {
-                // In top/bottom mode, textVerticalAlign should not be used, which cause
-                // inaccurately locating.
-                if (elOption.hv && elOption.hv[1]) {
-                    (elOptionStyle as any).textVerticalAlign =
-                    (elOptionStyle as any).textBaseline =
-                    (elOptionStyle as TextStyleProps).verticalAlign =
-                    (elOptionStyle as TextStyleProps).align = null;
-                }
-            }
-
-            let textContentOption = (elOption as GraphicComponentZRPathOption).textContent;
-            let textConfig = (elOption as GraphicComponentZRPathOption).textConfig;
-            if (elOptionStyle
-                && isEC4CompatibleStyle(elOptionStyle, elType, !!textConfig, !!textContentOption)
-            ) {
-                const convertResult =
-                    convertFromEC4CompatibleStyle(elOptionStyle, elType, true) as GraphicComponentZRPathOption;
-                if (!textConfig && convertResult.textConfig) {
-                    textConfig = (elOption as GraphicComponentZRPathOption).textConfig = convertResult.textConfig;
-                }
-                if (!textContentOption && convertResult.textContent) {
-                    textContentOption = convertResult.textContent;
-                }
-            }
-
-            // Remove unnecessary props to avoid potential problems.
-            const elOptionCleaned = getCleanedElOption(elOption);
-
-            // For simple, do not support parent change, otherwise reorder is needed.
-            if (__DEV__) {
-                elExisting && zrUtil.assert(
-                    targetElParent === elExisting.parent,
-                    'Changing parent is not supported.'
-                );
-            }
-
-            const $action = elOption.$action || 'merge';
-            if ($action === 'merge') {
-                elExisting
-                    ? elExisting.attr(elOptionCleaned)
-                    : createEl(id, targetElParent, elOptionCleaned, elMap);
-            }
-            else if ($action === 'replace') {
-                removeEl(elExisting, elMap);
-                createEl(id, targetElParent, elOptionCleaned, elMap);
-            }
-            else if ($action === 'remove') {
-                removeEl(elExisting, elMap);
-            }
-
-            const el = elMap.get(id);
+export function install(registers: EChartsExtensionInstallRegisters) {
 
-            if (el && textContentOption) {
-                if ($action === 'merge') {
-                    const textContentExisting = el.getTextContent();
-                    textContentExisting
-                        ? textContentExisting.attr(textContentOption)
-                        : el.setTextContent(new graphicUtil.Text(textContentOption));
-                }
-                else if ($action === 'replace') {
-                    el.setTextContent(new graphicUtil.Text(textContentOption));
-                }
-            }
+    registers.registerComponentModel(GraphicComponentModel);
+    registers.registerComponentView(GraphicComponentView);
 
-            if (el) {
-                const elInner = inner(el);
-                elInner.widthOption = (elOption as GraphicComponentGroupOption).width;
-                elInner.heightOption = (elOption as GraphicComponentGroupOption).height;
-                setEventData(el, graphicModel, elOption);
+    registers.registerPreprocessor(function (option) {
+        const graphicOption = option.graphic as GraphicComponentOption | GraphicComponentOption[];
 
-                graphicUtil.setTooltipConfig({
-                    el: el,
-                    componentModel: graphicModel,
-                    itemName: el.name,
-                    itemTooltipOption: elOption.tooltip
-                });
+        // Convert
+        // {graphic: [{left: 10, type: 'circle'}, ...]}
+        // or
+        // {graphic: {left: 10, type: 'circle'}}
+        // to
+        // {graphic: [{elements: [{left: 10, type: 'circle'}, ...]}]}
+        if (isArray(graphicOption)) {
+            if (!graphicOption[0] || !graphicOption[0].elements) {
+                option.graphic = [{ elements: graphicOption }];
             }
-        });
-    }
-
-    /**
-     * Locate graphic elements.
-     */
-    private _relocate(graphicModel: GraphicComponentModel, api: ExtensionAPI): void {
-        const elOptions = graphicModel.option.elements;
-        const rootGroup = this.group;
-        const elMap = this._elMap;
-        const apiWidth = api.getWidth();
-        const apiHeight = api.getHeight();
-
-        // Top-down to calculate percentage width/height of group
-        for (let i = 0; i < elOptions.length; i++) {
-            const elOption = elOptions[i];
-            const id = modelUtil.convertOptionIdName(elOption.id, null);
-            const el = id != null ? elMap.get(id) : null;
-
-            if (!el || !el.isGroup) {
-                continue;
+            else {
+                // Only one graphic instance can be instantiated. (We dont
+                // want that too many views are created in echarts._viewMap)
+                option.graphic = [(option.graphic as any)[0]];
             }
-            const parentEl = el.parent;
-            const isParentRoot = parentEl === rootGroup;
-            // Like 'position:absolut' in css, default 0.
-            const elInner = inner(el);
-            const parentElInner = inner(parentEl);
-            elInner.width = parsePercent(
-                elInner.widthOption,
-                isParentRoot ? apiWidth : parentElInner.width
-            ) || 0;
-            elInner.height = parsePercent(
-                elInner.heightOption,
-                isParentRoot ? apiHeight : parentElInner.height
-            ) || 0;
         }
-
-        // Bottom-up tranvese all elements (consider ec resize) to locate elements.
-        for (let i = elOptions.length - 1; i >= 0; i--) {
-            const elOption = elOptions[i];
-            const id = modelUtil.convertOptionIdName(elOption.id, null);
-            const el = id != null ? elMap.get(id) : null;
-
-            if (!el) {
-                continue;
-            }
-
-            const parentEl = el.parent;
-            const parentElInner = inner(parentEl);
-            const containerInfo = parentEl === rootGroup
-                ? {
-                    width: apiWidth,
-                    height: apiHeight
-                }
-                : {
-                    width: parentElInner.width,
-                    height: parentElInner.height
-                };
-
-            // PENDING
-            // Currently, when `bounding: 'all'`, the union bounding rect of the group
-            // does not include the rect of [0, 0, group.width, group.height], which
-            // is probably weird for users. Should we make a break change for it?
-            layoutUtil.positionElement(
-                el, elOption, containerInfo, null,
-                {hv: elOption.hv, boundingMode: elOption.bounding}
-            );
-        }
-    }
-
-    /**
-     * Clear all elements.
-     */
-    private _clear(): void {
-        const elMap = this._elMap;
-        elMap.each(function (el) {
-            removeEl(el, elMap);
-        });
-        this._elMap = zrUtil.createHashMap();
-    }
-
-    dispose(): void {
-        this._clear();
-    }
-}
-
-function createEl(
-    id: string,
-    targetElParent: graphicUtil.Group,
-    elOption: GraphicComponentElementOption,
-    elMap: ElementMap
-): void {
-    const graphicType = elOption.type;
-
-    if (__DEV__) {
-        zrUtil.assert(graphicType, 'graphic type MUST be set');
-    }
-
-    const Clz = (
-        zrUtil.hasOwn(_nonShapeGraphicElements, graphicType)
-            // Those graphic elements are not shapes. They should not be
-            // overwritten by users, so do them first.
-            ? _nonShapeGraphicElements[graphicType as NonShapeGraphicElementType]
-            : graphicUtil.getShapeClass(graphicType)
-    ) as { new(opt: GraphicComponentElementOption): Element };
-
-    if (__DEV__) {
-        zrUtil.assert(Clz, 'graphic type can not be found');
-    }
-
-    const el = new Clz(elOption);
-    targetElParent.add(el);
-    elMap.set(id, el);
-    inner(el).id = id;
-}
-
-function removeEl(elExisting: Element, elMap: ElementMap): void {
-    const existElParent = elExisting && elExisting.parent;
-    if (existElParent) {
-        elExisting.type === 'group' && elExisting.traverse(function (el) {
-            removeEl(el, elMap);
-        });
-        elMap.removeKey(inner(elExisting).id);
-        existElParent.remove(elExisting);
-    }
-}
-
-// Remove unnecessary props to avoid potential problems.
-function getCleanedElOption(
-    elOption: GraphicComponentElementOption
-): Omit<GraphicComponentElementOption, 'textContent'> {
-    elOption = zrUtil.extend({}, elOption);
-    zrUtil.each(
-        ['id', 'parentId', '$action', 'hv', 'bounding', 'textContent'].concat(layoutUtil.LOCATION_PARAMS),
-        function (name) {
-            delete (elOption as any)[name];
+        else if (graphicOption && !graphicOption.elements) {
+            option.graphic = [{ elements: [graphicOption] }];
         }
-    );
-    return elOption;
-}
-
-function isSetLoc(
-    obj: GraphicComponentElementOption,
-    props: ('left' | 'right' | 'top' | 'bottom')[]
-): boolean {
-    let isSet;
-    zrUtil.each(props, function (prop) {
-        obj[prop] != null && obj[prop] !== 'auto' && (isSet = true);
     });
-    return isSet;
-}
-
-function setKeyInfoToNewElOption(
-    resultItem: ReturnType<typeof modelUtil.mappingToExists>[number],
-    newElOption: GraphicComponentElementOption
-): void {
-    const existElOption = resultItem.existing as GraphicComponentElementOption;
-
-    // Set id and type after id assigned.
-    newElOption.id = resultItem.keyInfo.id;
-    !newElOption.type && existElOption && (newElOption.type = existElOption.type);
-
-    // Set parent id if not specified
-    if (newElOption.parentId == null) {
-        const newElParentOption = newElOption.parentOption;
-        if (newElParentOption) {
-            newElOption.parentId = newElParentOption.id;
-        }
-        else if (existElOption) {
-            newElOption.parentId = existElOption.parentId;
-        }
-    }
-
-    // Clear
-    newElOption.parentOption = null;
-}
-
-function mergeNewElOptionToExist(
-    existList: GraphicComponentElementOption[],
-    index: number,
-    newElOption: GraphicComponentElementOption
-): void {
-    // Update existing options, for `getOption` feature.
-    const newElOptCopy = zrUtil.extend({}, newElOption);
-    const existElOption = existList[index];
-
-    const $action = newElOption.$action || 'merge';
-    if ($action === 'merge') {
-        if (existElOption) {
-
-            if (__DEV__) {
-                const newType = newElOption.type;
-                zrUtil.assert(
-                    !newType || existElOption.type === newType,
-                    'Please set $action: "replace" to change `type`'
-                );
-            }
-
-            // We can ensure that newElOptCopy and existElOption are not
-            // the same object, so `merge` will not change newElOptCopy.
-            zrUtil.merge(existElOption, newElOptCopy, true);
-            // Rigid body, use ignoreSize.
-            layoutUtil.mergeLayoutParam(existElOption, newElOptCopy, {ignoreSize: true});
-            // Will be used in render.
-            layoutUtil.copyLayoutParams(newElOption, existElOption);
-        }
-        else {
-            existList[index] = newElOptCopy;
-        }
-    }
-    else if ($action === 'replace') {
-        existList[index] = newElOptCopy;
-    }
-    else if ($action === 'remove') {
-        // null will be cleaned later.
-        existElOption && (existList[index] = null);
-    }
-}
-
-function setLayoutInfoToExist(
-    existItem: GraphicComponentElementOption,
-    newElOption: GraphicComponentElementOption
-) {
-    if (!existItem) {
-        return;
-    }
-    existItem.hv = newElOption.hv = [
-        // Rigid body, dont care `width`.
-        isSetLoc(newElOption, ['left', 'right']),
-        // Rigid body, dont care `height`.
-        isSetLoc(newElOption, ['top', 'bottom'])
-    ];
-    // Give default group size. Otherwise layout error may occur.
-    if (existItem.type === 'group') {
-        const existingGroupOpt = existItem as GraphicComponentGroupOption;
-        const newGroupOpt = newElOption as GraphicComponentGroupOption;
-        existingGroupOpt.width == null && (existingGroupOpt.width = newGroupOpt.width = 0);
-        existingGroupOpt.height == null && (existingGroupOpt.height = newGroupOpt.height = 0);
-    }
-}
-
-function setEventData(
-    el: Element,
-    graphicModel: GraphicComponentModel,
-    elOption: GraphicComponentElementOption
-): void {
-    let eventData = getECData(el).eventData;
-    // Simple optimize for large amount of elements that no need event.
-    if (!el.silent && !el.ignore && !eventData) {
-        eventData = getECData(el).eventData = {
-            componentType: 'graphic',
-            componentIndex: graphicModel.componentIndex,
-            name: el.name
-        };
-    }
-
-    // `elOption.info` enables user to mount some info on
-    // elements and use them in event handlers.
-    if (eventData) {
-        eventData.info = elOption.info;
-    }
-}
-
-export function install(registers: EChartsExtensionInstallRegisters) {
-    registers.registerComponentModel(GraphicComponentModel);
-    registers.registerComponentView(GraphicComponentView);
-    registers.registerPreprocessor(preprocessor);
 }
\ No newline at end of file
diff --git a/src/export/option.ts b/src/export/option.ts
index dff46af..d25e819 100644
--- a/src/export/option.ts
+++ b/src/export/option.ts
@@ -94,7 +94,7 @@ import type {
     CustomSeriesRenderItem
 } from '../chart/custom/CustomSeries';
 
-import type { GraphicComponentLooseOption as GraphicComponentOption } from '../component/graphic/install';
+import { GraphicComponentLooseOption as GraphicComponentOption } from '../component/graphic/GraphicModel';
 import type { DatasetOption as DatasetComponentOption } from '../component/dataset/install';
 
 import type {ToolboxBrushFeatureOption} from '../component/toolbox/feature/Brush';

---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@echarts.apache.org
For additional commands, e-mail: commits-help@echarts.apache.org