This is an automated email from the ASF dual-hosted git repository. sushuang pushed a commit to branch next in repository https://gitbox.apache.org/repos/asf/incubator-echarts.git
commit 23a809ddd897c9042fa1b9f29f6cf9c65341e72d Author: 100pah <sushuang0...@gmail.com> AuthorDate: Tue Oct 27 02:17:11 2020 +0800 ts: add type to graphic component. --- src/component/graphic.ts | 470 +++++++++++++++++++++++++++++++---------------- src/util/layout.ts | 2 +- 2 files changed, 309 insertions(+), 163 deletions(-) diff --git a/src/component/graphic.ts b/src/component/graphic.ts index 430e81c..fd6ac7d 100644 --- a/src/component/graphic.ts +++ b/src/component/graphic.ts @@ -17,7 +17,6 @@ * under the License. */ -// @ts-nocheck import * as echarts from '../echarts'; import * as zrUtil from 'zrender/src/core/util'; @@ -26,25 +25,194 @@ 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 } from '../util/types'; +import ComponentModel from '../model/Component'; +import Element 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 '../ExtensionAPI'; +import { getECData } from '../util/innerStore'; + + +const TRANSFORM_PROPS = { + x: 1, + y: 1, + scaleX: 1, + scaleY: 1, + originX: 1, + originY: 1, + rotation: 1 +} as const; +type TransformProp = keyof typeof TRANSFORM_PROPS; + +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. + */ + type: string; + + id?: OptionId; + + // 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?: '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; + + // TODO: textContent, textConfig ? + // `false` means remove the textContent ??? + // textContent?: CustomTextOption | false; + // textConfig?: + + $action?: 'merge' | 'replace' | 'remove'; +}; +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[]; +} +interface GraphicComponentZRPathOption extends GraphicComponentDisplayableOption { + shape?: PathProps['shape']; +} +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 GraphicComponentDisplayableOption { + type: 'text'; +} +type GraphicComponentElementOption = + GraphicComponentZRPathOption + | GraphicComponentImageOption + | GraphicComponentTextOption; +// type GraphicComponentElementOptionOnState = +// GraphicComponentDisplayableOptionOnState +// | GraphicComponentImageOptionOnState; + +type GraphicExtraElementInfo = Dictionary<unknown>; + +type ElementMap = zrUtil.HashMap<Element, string>; + +const inner = modelUtil.makeInner<{ + __ecGraphicWidthOption: number; + __ecGraphicHeightOption: number; + __ecGraphicWidth: number; + __ecGraphicHeight: number; + __ecGraphicId: string; +}, Element>(); + const _nonShapeGraphicElements = { // Reserved but not supported in graphic component. - path: null, - compoundPath: null, + 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 -// ------------- +// ------------------------ echarts.registerPreprocessor(function (option) { - const graphicOption = option.graphic; + const graphicOption = option.graphic as GraphicComponentOption | GraphicComponentOption[]; // Convert // {graphic: [{left: 10, type: 'circle'}, ...]} @@ -59,7 +227,7 @@ echarts.registerPreprocessor(function (option) { else { // Only one graphic instance can be instantiated. (We dont // want that too many views are created in echarts._viewMap) - option.graphic = [option.graphic[0]]; + option.graphic = [(option.graphic as any)[0]]; } } else if (graphicOption && !graphicOption.elements) { @@ -67,87 +235,58 @@ echarts.registerPreprocessor(function (option) { } }); -// ------ +// ------------------------ // Model -// ------ - -const GraphicModel = echarts.extendComponentModel({ - - type: 'graphic', - - defaultOption: { - - // Extra properties for each elements: - // - // 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. - // - // 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. - // - // 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.) - // 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. - - // Note: elements is always behind its ancestors in this elements array. - elements: [], - parentId: null - }, +// ------------------------ + +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; + + 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 - * @type {Array.<Object>} */ - _elOptionsToUpdate: null, + private _elOptionsToUpdate: GraphicComponentElementOption[]; - /** - * @override - */ - mergeOption: function (option) { + mergeOption(option: GraphicComponentOption, ecModel: GlobalModel): void { // Prevent default merge to elements const elements = this.option.elements; this.option.elements = null; - GraphicModel.superApply(this, 'mergeOption', arguments); + super.mergeOption(option, ecModel); this.option.elements = elements; - }, + } - /** - * @override - */ - optionUpdated: function (newOption, isInit) { + 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 = []; - this._flatten(newList, flattenedList); + const flattenedList = [] as GraphicComponentElementOption[]; + this._flatten(newList, flattenedList, null); const mappingResult = modelUtil.mappingToExists(existList, flattenedList, 'normalMerge'); // Clear elOptionsToUpdate - const elOptionsToUpdate = this._elOptionsToUpdate = []; + const elOptionsToUpdate = this._elOptionsToUpdate = [] as GraphicComponentElementOption[]; zrUtil.each(mappingResult, function (resultItem, index) { - const newElOption = resultItem.newOption; + const newElOption = resultItem.newOption as GraphicComponentElementOption; if (__DEV__) { zrUtil.assert( @@ -181,7 +320,7 @@ const GraphicModel = echarts.extendComponentModel({ delete existList[i].$action; } } - }, + } /** * Convert @@ -196,13 +335,12 @@ const GraphicModel = echarts.extendComponentModel({ * {type: 'circle', parentId: 'xx'}, * {type: 'polygon', parentId: 'xx'} * ] - * - * @private - * @param {Array.<Object>} optionList option list - * @param {Array.<Object>} result result of flatten - * @param {Object} parentOption parent option */ - _flatten: function (optionList, result, parentOption) { + private _flatten( + optionList: GraphicComponentElementOption[], + result: GraphicComponentElementOption[], + parentOption: GraphicComponentElementOption + ): void { zrUtil.each(optionList, function (option) { if (!option) { return; @@ -221,48 +359,36 @@ const GraphicModel = echarts.extendComponentModel({ // 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: function () { + useElOptionsToUpdate(): GraphicComponentElementOption[] { const els = this._elOptionsToUpdate; // Clear to avoid render duplicately when zooming. this._elOptionsToUpdate = null; return els; } -}); +} -// ----- +// ------------------------ // View -// ----- +// ------------------------ -echarts.extendComponentView({ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +class GraphicComponentView extends ComponentView { - type: 'graphic', + static type = 'graphic'; + type = GraphicComponentView.type; - /** - * @override - */ - init: function (ecModel, api) { + private _elMap: ElementMap; + private _lastGraphicModel: GraphicComponentModel; - /** - * @private - * @type {module:zrender/core/util.HashMap} - */ + init() { this._elMap = zrUtil.createHashMap(); + } - /** - * @private - * @type {module:echarts/graphic/GraphicModel} - */ - this._lastGraphicModel; - }, - - /** - * @override - */ - render: function (graphicModel, ecModel, api) { + render(graphicModel: GraphicComponentModel, ecModel: GlobalModel, api: ExtensionAPI): void { // Having leveraged between use cases and algorithm complexity, a very // simple layout mechanism is used: @@ -281,15 +407,12 @@ echarts.extendComponentView({ this._updateElements(graphicModel); this._relocate(graphicModel, api); - }, + } /** * Update graphic elements. - * - * @private - * @param {Object} graphicModel graphic model */ - _updateElements: function (graphicModel) { + private _updateElements(graphicModel: GraphicComponentModel): void { const elOptionsToUpdate = graphicModel.useElOptionsToUpdate(); if (!elOptionsToUpdate) { @@ -302,26 +425,26 @@ echarts.extendComponentView({ // Top-down tranverse to assign graphic settings to each elements. zrUtil.each(elOptionsToUpdate, function (elOption) { const $action = elOption.$action; - const id = elOption.id; - const existEl = elMap.get(id); - const parentId = elOption.parentId; - const targetElParent = parentId != null ? elMap.get(parentId) : rootGroup; + const id = modelUtil.convertOptionIdName(elOption.id, null); + const existEl = 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 elOptionStyle = elOption.style; if (elOption.type === 'text' && elOptionStyle) { // In top/bottom mode, textVerticalAlign should not be used, which cause // inaccurately locating. if (elOption.hv && elOption.hv[1]) { - elOptionStyle.textVerticalAlign = elOptionStyle.textBaseline = null; + (elOptionStyle as any).textVerticalAlign = (elOptionStyle as any).textBaseline = null; } // Compatible with previous setting: both support fill and textFill, // stroke and textStroke. - !elOptionStyle.hasOwnProperty('textFill') && elOptionStyle.fill && ( - elOptionStyle.textFill = elOptionStyle.fill + !elOptionStyle.hasOwnProperty('textFill') && (elOptionStyle as any).fill && ( + (elOptionStyle as any).textFill = (elOptionStyle as any).fill ); - !elOptionStyle.hasOwnProperty('textStroke') && elOptionStyle.stroke && ( - elOptionStyle.textStroke = elOptionStyle.stroke + !elOptionStyle.hasOwnProperty('textStroke') && (elOptionStyle as any).stroke && ( + (elOptionStyle as any).textStroke = (elOptionStyle as any).stroke ); } @@ -351,21 +474,18 @@ echarts.extendComponentView({ const el = elMap.get(id); if (el) { - el.__ecGraphicWidthOption = elOption.width; - el.__ecGraphicHeightOption = elOption.height; + const elInner = inner(el); + elInner.__ecGraphicWidthOption = (elOption as GraphicComponentGroupOption).width; + elInner.__ecGraphicHeightOption = (elOption as GraphicComponentGroupOption).height; setEventData(el, graphicModel, elOption); } }); - }, + } /** * Locate graphic elements. - * - * @private - * @param {Object} graphicModel graphic model - * @param {module:echarts/ExtensionAPI} api extension API */ - _relocate: function (graphicModel, api) { + private _relocate(graphicModel: GraphicComponentModel, api: ExtensionAPI): void { const elOptions = graphicModel.option.elements; const rootGroup = this.group; const elMap = this._elMap; @@ -375,7 +495,8 @@ echarts.extendComponentView({ // Top-down to calculate percentage width/height of group for (let i = 0; i < elOptions.length; i++) { const elOption = elOptions[i]; - const el = elMap.get(elOption.id); + const id = modelUtil.convertOptionIdName(elOption.id, null); + const el = id != null ? elMap.get(id) : null; if (!el || !el.isGroup) { continue; @@ -383,34 +504,38 @@ echarts.extendComponentView({ const parentEl = el.parent; const isParentRoot = parentEl === rootGroup; // Like 'position:absolut' in css, default 0. - el.__ecGraphicWidth = parsePercent( - el.__ecGraphicWidthOption, - isParentRoot ? apiWidth : parentEl.__ecGraphicWidth + const elInner = inner(el); + const parentElInner = inner(parentEl); + elInner.__ecGraphicWidth = parsePercent( + elInner.__ecGraphicWidthOption, + isParentRoot ? apiWidth : parentElInner.__ecGraphicWidth ) || 0; - el.__ecGraphicHeight = parsePercent( - el.__ecGraphicHeightOption, - isParentRoot ? apiHeight : parentEl.__ecGraphicHeight + elInner.__ecGraphicHeight = parsePercent( + elInner.__ecGraphicHeightOption, + isParentRoot ? apiHeight : parentElInner.__ecGraphicHeight ) || 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 el = elMap.get(elOption.id); + 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: parentEl.__ecGraphicWidth, - height: parentEl.__ecGraphicHeight + width: parentElInner.__ecGraphicWidth, + height: parentElInner.__ecGraphicHeight }; // PENDING @@ -422,41 +547,43 @@ echarts.extendComponentView({ {hv: elOption.hv, boundingMode: elOption.bounding} ); } - }, + } /** * Clear all elements. - * - * @private */ - _clear: function () { + private _clear(): void { const elMap = this._elMap; elMap.each(function (el) { removeEl(el, elMap); }); this._elMap = zrUtil.createHashMap(); - }, + } - /** - * @override - */ - dispose: function () { + dispose(): void { this._clear(); } -}); +} -function createEl(id, targetElParent, elOption, elMap) { +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 = _nonShapeGraphicElements.hasOwnProperty(graphicType) - // Those graphic elements are not shapes. They should not be - // overwritten by users, so do them first. - ? _nonShapeGraphicElements[graphicType] - : graphicUtil.getShapeClass(graphicType); + 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'); @@ -465,33 +592,36 @@ function createEl(id, targetElParent, elOption, elMap) { const el = new Clz(elOption); targetElParent.add(el); elMap.set(id, el); - el.__ecGraphicId = id; + inner(el).__ecGraphicId = id; } -function removeEl(existEl, elMap) { +function removeEl(existEl: Element, elMap: ElementMap): void { const existElParent = existEl && existEl.parent; if (existElParent) { existEl.type === 'group' && existEl.traverse(function (el) { removeEl(el, elMap); }); - elMap.removeKey(existEl.__ecGraphicId); + elMap.removeKey(inner(existEl).__ecGraphicId); existElParent.remove(existEl); } } // Remove unnecessary props to avoid potential problems. -function getCleanedElOption(elOption) { +function getCleanedElOption(elOption: GraphicComponentElementOption): GraphicComponentElementOption { elOption = zrUtil.extend({}, elOption); zrUtil.each( ['id', 'parentId', '$action', 'hv', 'bounding'].concat(layoutUtil.LOCATION_PARAMS), function (name) { - delete elOption[name]; + delete (elOption as any)[name]; } ); return elOption; } -function isSetLoc(obj, props) { +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); @@ -499,8 +629,11 @@ function isSetLoc(obj, props) { return isSet; } -function setKeyInfoToNewElOption(resultItem, newElOption) { - const existElOption = resultItem.existing; +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; @@ -521,7 +654,11 @@ function setKeyInfoToNewElOption(resultItem, newElOption) { newElOption.parentOption = null; } -function mergeNewElOptionToExist(existList, index, newElOption) { +function mergeNewElOptionToExist( + existList: GraphicComponentElementOption[], + index: number, + newElOption: GraphicComponentElementOption +): void { // Update existing options, for `getOption` feature. const newElOptCopy = zrUtil.extend({}, newElOption); const existElOption = existList[index]; @@ -559,7 +696,10 @@ function mergeNewElOptionToExist(existList, index, newElOption) { } } -function setLayoutInfoToExist(existItem, newElOption) { +function setLayoutInfoToExist( + existItem: GraphicComponentElementOption, + newElOption: GraphicComponentElementOption +) { if (!existItem) { return; } @@ -571,16 +711,22 @@ function setLayoutInfoToExist(existItem, newElOption) { ]; // Give default group size. Otherwise layout error may occur. if (existItem.type === 'group') { - existItem.width == null && (existItem.width = newElOption.width = 0); - existItem.height == null && (existItem.height = newElOption.height = 0); + 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, graphicModel, elOption) { - let eventData = el.eventData; +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 = el.eventData = { + eventData = getECData(el).eventData = { componentType: 'graphic', componentIndex: graphicModel.componentIndex, name: el.name @@ -590,6 +736,6 @@ function setEventData(el, graphicModel, elOption) { // `elOption.info` enables user to mount some info on // elements and use them in event handlers. if (eventData) { - eventData.info = el.info; + eventData.info = elOption.info; } } diff --git a/src/util/layout.ts b/src/util/layout.ts index f13fd5c..7e4cddc 100644 --- a/src/util/layout.ts +++ b/src/util/layout.ts @@ -337,7 +337,7 @@ export function positionElement( containerRect: {width: number, height: number}, margin?: number[] | number, opt?: { - hv: [1 | 0, 1 | 0], + hv: [1 | 0 | boolean, 1 | 0 | boolean], boundingMode: 'all' | 'raw' } ) { --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@echarts.apache.org For additional commands, e-mail: commits-h...@echarts.apache.org