This is an automated email from the ASF dual-hosted git repository. sushuang pushed a commit to branch custom-series-enhance in repository https://gitbox.apache.org/repos/asf/incubator-echarts.git
commit d0e5f43998996909fcc6d4c0ddc7c886b2796386 Author: 100pah <[email protected]> AuthorDate: Wed Apr 29 01:57:05 2020 +0800 feat: custom series compat. --- src/chart/custom.ts | 1252 +++++++++++++++----- src/coord/CoordinateSystem.ts | 3 + src/echarts.ts | 4 + src/model/Component.ts | 5 + src/model/mixin/itemStyle.ts | 2 +- src/util/graphic.ts | 39 +- src/util/styleCompat.ts | 256 ++++ src/util/types.ts | 2 + ...-d3.html => circle-packing-with-d3.compat.html} | 0 test/circle-packing-with-d3.html | 50 +- test/custom-feature.html | 1 + test/custom-text-content.html | 1193 +++++++++++++++++++ test/hoverStyle.html | 3 +- 13 files changed, 2488 insertions(+), 322 deletions(-) diff --git a/src/chart/custom.ts b/src/chart/custom.ts index 3b801ff..3ed15b9 100644 --- a/src/chart/custom.ts +++ b/src/chart/custom.ts @@ -17,7 +17,6 @@ * under the License. */ -// @ts-nocheck import {__DEV__} from '../config'; import * as zrUtil from 'zrender/src/core/util'; @@ -30,24 +29,261 @@ import SeriesModel from '../model/Series'; import Model from '../model/Model'; import ChartView from '../view/Chart'; import {createClipPath} from './helper/createClipPathFromCoordSys'; -import {EventQueryItem, ECEvent} from '../util/types'; -import Element from 'zrender/src/Element'; - +import { + EventQueryItem, ECEvent, SeriesOption, SeriesOnCartesianOptionMixin, + SeriesOnPolarOptionMixin, SeriesOnSingleOptionMixin, SeriesOnGeoOptionMixin, + SeriesOnCalendarOptionMixin, ItemStyleOption, SeriesEncodeOptionMixin, + SeriesTooltipOption, + DimensionLoose, + ParsedValue, + Dictionary, + CallbackDataParams, + Payload, + StageHandlerProgressParams, + LabelOption, + ViewRootGroup, + OptionDataValue, + ZRStyleProps, + DisplayState, + ECElement, + DisplayStateNonNormal +} from '../util/types'; +import Element, { ElementProps, ElementTextConfig } from 'zrender/src/Element'; import prepareCartesian2d from '../coord/cartesian/prepareCustom'; import prepareGeo from '../coord/geo/prepareCustom'; import prepareSingleAxis from '../coord/single/prepareCustom'; import preparePolar from '../coord/polar/prepareCustom'; import prepareCalendar from '../coord/calendar/prepareCustom'; +import ComponentModel from '../model/Component'; +import List, { DefaultDataVisual } from '../data/List'; +import GlobalModel from '../model/Global'; +import { makeInner } from '../util/model'; +import ExtensionAPI from '../ExtensionAPI'; +import Displayable from 'zrender/src/graphic/Displayable'; +import Axis2D from '../coord/cartesian/Axis2D'; +import { RectLike } from 'zrender/src/core/BoundingRect'; +import { PathProps } from 'zrender/src/graphic/Path'; +import { ImageStyleProps } from 'zrender/src/graphic/Image'; +import { CoordinateSystem } from '../coord/CoordinateSystem'; +import { TextStyleProps } from 'zrender/src/graphic/Text'; +import { + convertToEC4StyleForCustomSerise, + isEC4CompatibleStyle, + convertFromEC4CompatibleStyle, + LegacyStyleProps, + warnDeprecated +} from '../util/styleCompat'; +import Transformable from 'zrender/src/core/Transformable'; +import { ItemStyleProps } from '../model/mixin/itemStyle'; + + +const inner = makeInner<{ + info: CustomExtraElementInfo; + customPathData: string; + customGraphicType: string; + customImagePath: CustomImageOption['style']['image']; + customText: string; + txConZ2Set: number; +}, Element>(); + +type CustomExtraElementInfo = Dictionary<unknown>; +type TransformPropsX = 'x' | 'scaleX' | 'originX'; +type TransformPropsY = 'y' | 'scaleY' | 'originY'; +type TransformProps = TransformPropsX | TransformPropsY | 'rotation'; + + +interface CustomBaseElementOption extends Partial<Pick< + Element, TransformProps | 'silent' | 'ignore' | 'textConfig' +>> { + // element type, mandatory. + type: string; + id?: string; + // For animation diff. + name?: string; + info?: CustomExtraElementInfo; + // `false` means remove the textContent. + textContent?: CustomTextOption | false; +}; +interface CustomDisplayableOption extends CustomBaseElementOption, Partial<Pick< + Displayable, 'zlevel' | 'z' | 'z2' | 'invisible' +>> { + style?: ZRStyleProps; + // `false` means remove emphasis trigger. + styleEmphasis?: ZRStyleProps | false; + emphasis?: CustomDisplayableOptionOnState; +} +interface CustomDisplayableOptionOnState extends Partial<Pick< + Displayable, TransformProps | 'textConfig' | 'z2' +>> { + // `false` means remove emphasis trigger. + style?: ZRStyleProps | false; +} +interface CustomGroupOption extends CustomBaseElementOption { + type: 'group'; + width?: number; + height?: number; + diffChildrenByName?: boolean; + children: CustomElementOption[]; + $mergeChildren: false | 'byName' | 'byIndex'; +} +interface CustomZRPathOption extends CustomDisplayableOption, Pick<PathProps, 'shape'> { +} +interface CustomSVGPathOption extends CustomDisplayableOption { + type: 'path'; + shape?: { + // SVG Path, like 'M0,0 L0,-20 L70,-1 L70,0 Z' + pathData?: string; + // "d" is the alias of `pathData` follows the SVG convention. + d?: string; + layout?: 'center' | 'cover'; + x?: number; + y?: number; + width?: number; + height?: number; + }; +} +interface CustomImageOption extends CustomDisplayableOption { + type: 'image'; + style?: ImageStyleProps; + emphasis?: CustomImageOptionOnState; +} +interface CustomImageOptionOnState extends CustomDisplayableOptionOnState { + style?: ImageStyleProps; +} +interface CustomTextOption extends CustomDisplayableOption { + type: 'text'; +} +type CustomElementOption = CustomZRPathOption | CustomSVGPathOption | CustomImageOption | CustomTextOption; +type CustomElementOptionOnState = CustomDisplayableOptionOnState | CustomImageOptionOnState; +type StyleOption = ZRStyleProps | ImageStyleProps | false; + + +interface CustomSeriesRenderItemAPI extends + CustomSeriesRenderItemCoordinateSystemAPI, + Pick<ExtensionAPI, 'getWidth' | 'getHeight' | 'getZr' | 'getDevicePixelRatio'> { + value(dim: DimensionLoose, dataIndexInside?: number): ParsedValue; + style(extra?: ZRStyleProps, dataIndexInside?: number): ZRStyleProps; + styleEmphasis(extra?: ZRStyleProps, dataIndexInside?: number): ZRStyleProps; + visual(visualType: string, dataIndexInside?: number): ReturnType<List['getItemVisual']>; + barLayout(opt: Omit<Parameters<typeof getLayoutOnAxis>[0], 'axis'>): ReturnType<typeof getLayoutOnAxis>; + currentSeriesIndices(): ReturnType<GlobalModel['getCurrentSeriesIndices']>; + font(opt: Parameters<typeof graphicUtil.getFont>[0]): ReturnType<typeof graphicUtil.getFont>; +} +interface CustomSeriesRenderItemParamsCoordSys { + type: string; + // And extra params for each coordinate systems. +} +interface CustomSeriesRenderItemCoordinateSystemAPI { + coord( + data: OptionDataValue | OptionDataValue[], + clamp?: boolean + ): number[]; + size?( + dataSize: OptionDataValue | OptionDataValue[], + dataItem: OptionDataValue | OptionDataValue[] + ): number | number[]; +} +interface CustomSeriesRenderItemParams { + context: {}; + seriesId: string; + seriesName: string; + seriesIndex: number; + coordSys: CustomSeriesRenderItemParamsCoordSys; + dataInsideLength: number; + encode: ReturnType<typeof wrapEncodeDef> +} +type CustomSeriesRenderItem = ( + params: CustomSeriesRenderItemParams, + api: CustomSeriesRenderItemAPI +) => CustomElementOption; + + +interface CustomSeriesOption extends + SeriesOption, + SeriesEncodeOptionMixin, + SeriesOnCartesianOptionMixin, + SeriesOnPolarOptionMixin, + SeriesOnSingleOptionMixin, + SeriesOnGeoOptionMixin, + SeriesOnCalendarOptionMixin { + + // If set as 'none', do not depends on coord sys. + coordinateSystem?: string | 'none'; + + renderItem?: CustomSeriesRenderItem; + + // Only works on polar and cartesian2d coordinate system. + clip?: boolean; -const CACHED_LABEL_STYLE_PROPERTIES = graphicUtil.CACHED_LABEL_STYLE_PROPERTIES; -const ITEM_STYLE_NORMAL_PATH = ['itemStyle']; -const ITEM_STYLE_EMPHASIS_PATH = ['emphasis', 'itemStyle']; -const LABEL_NORMAL = ['label']; -const LABEL_EMPHASIS = ['emphasis', 'label']; + // FIXME needed? + tooltip?: SeriesTooltipOption; + + itemStyle?: ItemStyleOption; + label?: LabelOption; + emphasis?: { + itemStyle?: ItemStyleOption; + label?: LabelOption; + }; +} + +// Also compat with ec4, where +// `visual('color') visual('borderColor')` is supported. +const STYLE_VISUAL_TYPE = { + color: 'fill', + borderColor: 'stroke' +} as const; + +const VISUAL_PROPS = { + symbol: 1, + symbolSize: 1, + symbolKeepAspect: 1, + legendSymbol: 1, + visualMeta: 1, + liftZ: 1 +} as const; + +const EMPHASIS = 'emphasis' as const; +const NORMAL = 'normal' as const; +const PATH_ITEM_STYLE = { + normal: ['itemStyle'], + emphasis: [EMPHASIS, 'itemStyle'] +} as const; +const PATH_LABEL = { + normal: ['label'], + emphasis: [EMPHASIS, 'label'] +} as const; // Use prefix to avoid index to be the same as el.name, -// which will cause weird udpate animation. +// which will cause weird update animation. const GROUP_DIFF_PREFIX = 'e\0\0'; +type AttachedTxInfo = { + isLegacy: boolean; + normal: { + cfg: ElementTextConfig; + conOpt: CustomElementOption | false; + }; + emphasis: { + cfg: ElementTextConfig; + conOpt: CustomElementOptionOnState; + }; +}; +const attachedTxInfoTmp = { + normal: {}, + emphasis: {} +} as AttachedTxInfo; + +const Z2_SPECIFIED_BIT = { + normal: 0, + emphasis: 1 +} as const; + + + + +export type PrepareCustomInfo = (coordSys: CoordinateSystem) => { + coordSys: CustomSeriesRenderItemParamsCoordSys; + api: CustomSeriesRenderItemCoordinateSystemAPI +}; /** * To reduce total package size of each coordinate systems, the modules `prepareCustom` @@ -60,7 +296,7 @@ const GROUP_DIFF_PREFIX = 'e\0\0'; * size: function (dataSize, dataItem) {} // return size of each axis in coordSys. * }} */ -const prepareCustoms = { +const prepareCustoms: Dictionary<PrepareCustomInfo> = { cartesian2d: prepareCartesian2d, geo: prepareGeo, singleAxis: prepareSingleAxis, @@ -68,29 +304,27 @@ const prepareCustoms = { calendar: prepareCalendar }; +class CustomSeriesModel extends SeriesModel<CustomSeriesOption> { -// ------ -// Model -// ------ + static type = 'series.custom'; + readonly type = CustomSeriesModel.type; -SeriesModel.extend({ + static dependencies = ['grid', 'polar', 'geo', 'singleAxis', 'calendar']; - type: 'series.custom', + preventAutoZ = true; - dependencies: ['grid', 'polar', 'geo', 'singleAxis', 'calendar'], + currentZLevel: number; + currentZ: number; - defaultOption: { + static defaultOption: CustomSeriesOption = { coordinateSystem: 'cartesian2d', // Can be set as 'none' zlevel: 0, z: 2, legendHoverLink: true, - useTransform: true, - // Custom series will not clip by default. // Some case will use custom series to draw label // For example https://echarts.apache.org/examples/en/editor.html?c=custom-gantt-flight - // Only works on polar and cartesian2d coordinate system. clip: false // Cartesian coordinate system @@ -105,43 +339,44 @@ SeriesModel.extend({ // label: {} // itemStyle: {} - }, + }; - /** - * @override - */ - getInitialData: function (option, ecModel) { + optionUpdated() { + this.currentZLevel = this.get('zlevel', true); + this.currentZ = this.get('z', true); + } + + getInitialData(option: CustomSeriesOption, ecModel: GlobalModel): List { return createListFromArray(this.getSource(), this); - }, + } - /** - * @override - */ - getDataParams: function (dataIndex, dataType, el) { - const params = SeriesModel.prototype.getDataParams.apply(this, arguments); - el && (params.info = el.info); + getDataParams(dataIndex: number, dataType: string, el: Element): CallbackDataParams & { + info: CustomExtraElementInfo + } { + const params = super.getDataParams(dataIndex, dataType, el) as ReturnType<CustomSeriesModel['getDataParams']>; + el && (params.info = inner(el).info); return params; } -}); +} -// ----- -// View -// ----- +ComponentModel.registerClass(CustomSeriesModel); -ChartView.extend({ - type: 'custom', - /** - * @private - * @type {module:echarts/data/List} - */ - _data: null, +class CustomSeriesView extends ChartView { - /** - * @override - */ - render: function (customSeries, ecModel, api, payload) { + static type = 'custom'; + readonly type = CustomSeriesView.type; + + private _data: List; + + + render( + customSeries: CustomSeriesModel, + ecModel: GlobalModel, + api: ExtensionAPI, + payload: Payload + ): void { const oldData = this._data; const data = customSeries.getData(); const group = this.group; @@ -182,17 +417,27 @@ ChartView.extend({ } this._data = data; - }, + } - incrementalPrepareRender: function (customSeries, ecModel, api) { + incrementalPrepareRender( + customSeries: CustomSeriesModel, + ecModel: GlobalModel, + api: ExtensionAPI + ): void { this.group.removeAll(); this._data = null; - }, + } - incrementalRender: function (params, customSeries, ecModel, api, payload) { + incrementalRender( + params: StageHandlerProgressParams, + customSeries: CustomSeriesModel, + ecModel: GlobalModel, + api: ExtensionAPI, + payload: Payload + ): void { const data = customSeries.getData(); const renderItem = makeRenderItem(customSeries, data, ecModel, api); - function setIncrementalAndHoverLayer(el) { + function setIncrementalAndHoverLayer(el: Displayable) { if (!el.isGroup) { el.incremental = true; el.useHoverLayer = true; @@ -202,17 +447,9 @@ ChartView.extend({ const el = createOrUpdate(null, idx, renderItem(idx, payload), customSeries, this.group, data); el.traverse(setIncrementalAndHoverLayer); } - }, - - /** - * @override - */ - dispose: zrUtil.noop, + } - /** - * @override - */ - filterForExposedEvent: function ( + filterForExposedEvent( eventType: string, query: EventQueryItem, targetEl: Element, packedEvent: ECEvent ): boolean { const elementName = query.element; @@ -230,17 +467,19 @@ ChartView.extend({ return false; } -}); +} + +ChartView.registerClass(CustomSeriesView); -function createEl(elOption) { +function createEl(elOption: CustomElementOption): Element { const graphicType = elOption.type; let el; // Those graphic elements are not shapes. They should not be // overwritten by users, so do them first. if (graphicType === 'path') { - const shape = elOption.shape; + const shape = (elOption as CustomSVGPathOption).shape; // Using pathRect brings convenience to users sacle svg path. const pathRect = (shape.width != null && shape.height != null) ? { @@ -248,20 +487,20 @@ function createEl(elOption) { y: shape.y || 0, width: shape.width, height: shape.height - } + } as RectLike : null; const pathData = getPathData(shape); // Path is also used for icon, so layout 'center' by default. el = graphicUtil.makePath(pathData, null, pathRect, shape.layout || 'center'); - el.__customPathData = pathData; + inner(el).customPathData = pathData; } else if (graphicType === 'image') { el = new graphicUtil.Image({}); - el.__customImagePath = elOption.style.image; + inner(el).customImagePath = (elOption as CustomImageOption).style.image; } else if (graphicType === 'text') { el = new graphicUtil.Text({}); - el.__customText = elOption.style.text; + inner(el).customText = (elOption.style as TextStyleProps).text; } else if (graphicType === 'group') { el = new graphicUtil.Group(); @@ -279,96 +518,305 @@ function createEl(elOption) { el = new Clz(); } - el.__customGraphicType = graphicType; + inner(el).customGraphicType = graphicType; el.name = elOption.name; + // Compat ec4: the default z2 lift is 1. If changing the number, + // some cases probably be broken: hierarchy layout along z, like circle packing, + // where emphasis only intending to modify color/border rather than lift z2. + (el as ECElement).z2EmphasisLift = 1; + return el; } -function updateEl(el, dataIndex, elOption, animatableModel, data, isInit, isRoot) { - const transitionProps = {}; - const elOptionStyle = elOption.style || {}; - - elOption.shape && (transitionProps.shape = zrUtil.clone(elOption.shape)); - elOption.position && (transitionProps.position = elOption.position.slice()); - elOption.scale && (transitionProps.scale = elOption.scale.slice()); - elOption.origin && (transitionProps.origin = elOption.origin.slice()); - elOption.rotation && (transitionProps.rotation = elOption.rotation); +/** + * [STRATEGY] Merge properties or erase all properties: + * + * Based on the fact that the existing zr element probably be reused, we discuss whether + * merge or erase all properties to the exsiting elements. + * + "Merge" means that if a certain props is not specified, do not assign to the existing element. + * + "Erase all" means that assign all of the available props whatever it specified by users. + * + * "Merge" might bring some unexpected state retaining for users and "erase all" seams to be + * more safe. But "erase all" force users to specify all of the props each time, which + * theoretically disables the chance of performance optimization (e.g., just generete shape + * and style at the first time rather than always do that). And "force user set all of the props" + * might bring trouble to specify which props need to perform "transition animation". + * So we still use "merge" rather than "erase all". If users need "erase all", they can + * simple always set all of the props each time. + * Some "object-like" config like `textConfig`, `textContent`, `style` which are not needed for + * every elment, so we replace them only when user specify them. And the that is a total replace. + * + * [STRATEGY] `hasOwnProperty` or `== null`: + * + * Ditinguishing "own property" probably bring little trouble to user when make el options. + * So we trade a {xx: null} or {xx: undefined} as "not specified" if possible rather than + * "set them to null/undefined". In most cases, props can not be cleared. Some typicall + * clearable props like `style`/`textConfig`/`textContent` we enable `false` to means + * "clear". In some othere special cases that the prop is able to set as null/undefined, + * but not suitable to use `false`, `hasOwnProperty` is checked. + */ +function updateElNormal( + el: Element, + dataIndex: number, + elOption: CustomElementOption, + styleOpt: StyleOption, + attachedTxInfo: AttachedTxInfo, + seriesModel: CustomSeriesModel, + isInit: boolean, + isTextContent: boolean +): void { + const transitionProps = {} as ElementProps; + const elDisplayable = el.isGroup ? null : el as Displayable; + + (elOption as CustomZRPathOption).shape && ( + (transitionProps as PathProps).shape = zrUtil.clone((elOption as CustomZRPathOption).shape) + ); + setLagecyProp(elOption, transitionProps, 'position', 'x', 'y'); + setLagecyProp(elOption, transitionProps, 'scale', 'scaleX', 'scaleY'); + setLagecyProp(elOption, transitionProps, 'origin', 'originX', 'originY'); + setTransProp(elOption, transitionProps, 'x'); + setTransProp(elOption, transitionProps, 'y'); + setTransProp(elOption, transitionProps, 'scaleX'); + setTransProp(elOption, transitionProps, 'scaleY'); + setTransProp(elOption, transitionProps, 'originX'); + setTransProp(elOption, transitionProps, 'originY'); + setTransProp(elOption, transitionProps, 'rotation'); + + const txCfgOpt = attachedTxInfo && attachedTxInfo.normal.cfg; + if (txCfgOpt) { + // PENDING: whether use user object directly rather than clone? + // TODO:5.0 textConfig transition animation? + el.setTextConfig(txCfgOpt); + } - if (el.type === 'image' && elOption.style) { - const targetStyle = transitionProps.style = {}; - zrUtil.each(['x', 'y', 'width', 'height'], function (prop) { - prepareStyleTransition(prop, targetStyle, elOptionStyle, el.style, isInit); - }); + if (el.type === 'image' && styleOpt) { + const targetStyle = (transitionProps as Displayable).style = {}; + const imgStyle = (el as graphicUtil.Image).style; + prepareStyleTransition('x', targetStyle, styleOpt, imgStyle, isInit); + prepareStyleTransition('y', targetStyle, styleOpt, imgStyle, isInit); + prepareStyleTransition('width', targetStyle, styleOpt, imgStyle, isInit); + prepareStyleTransition('height', targetStyle, styleOpt, imgStyle, isInit); } - if (el.type === 'text' && elOption.style) { - const targetStyle = transitionProps.style = {}; - zrUtil.each(['x', 'y'], function (prop) { - prepareStyleTransition(prop, targetStyle, elOptionStyle, el.style, isInit); - }); - // Compatible with previous: both support - // textFill and fill, textStroke and stroke in 'text' element. - !elOptionStyle.hasOwnProperty('textFill') && elOptionStyle.fill && ( - elOptionStyle.textFill = elOptionStyle.fill + if (el.type === 'text' && styleOpt) { + const textOptionStyle = styleOpt as TextStyleProps; + const targetStyle = (transitionProps as Displayable).style = {}; + const textStyle = (el as graphicUtil.Text).style; + prepareStyleTransition('x', targetStyle, textOptionStyle, textStyle, isInit); + prepareStyleTransition('y', targetStyle, textOptionStyle, textStyle, isInit); + // Compatible with ec4: if `textFill` or `textStroke` exists use them. + zrUtil.hasOwn(textOptionStyle, 'textFill') && ( + textOptionStyle.fill = (textOptionStyle as any).textFill ); - !elOptionStyle.hasOwnProperty('textStroke') && elOptionStyle.stroke && ( - elOptionStyle.textStroke = elOptionStyle.stroke + zrUtil.hasOwn(textOptionStyle, 'textStroke') && ( + textOptionStyle.stroke = (textOptionStyle as any).textStroke ); } - if (el.type !== 'group') { - el.useStyle(elOptionStyle); + if (elDisplayable) { + // PENDING: here the input style object is used directly. + // Good for performance but bad for compatibility control. + styleOpt && elDisplayable.useStyle(styleOpt); // Init animation. if (isInit) { - el.style.opacity = 0; - let targetOpacity = elOptionStyle.opacity; - targetOpacity == null && (targetOpacity = 1); - graphicUtil.initProps(el, {style: {opacity: targetOpacity}}, animatableModel, dataIndex); + elDisplayable.style.opacity = 0; + const targetOpacity = (styleOpt && styleOpt.opacity != null) ? styleOpt.opacity : 1; + graphicUtil.initProps(elDisplayable, {style: {opacity: targetOpacity}}, seriesModel, dataIndex); } + + zrUtil.hasOwn(elOption, 'invisible') && (elDisplayable.invisible = elOption.invisible); } if (isInit) { el.attr(transitionProps); } else { - graphicUtil.updateProps(el, transitionProps, animatableModel, dataIndex); + graphicUtil.updateProps(el, transitionProps, seriesModel, dataIndex); } // Merge by default. - // z2 must not be null/undefined, otherwise sort error may occur. - elOption.hasOwnProperty('z2') && el.attr('z2', elOption.z2 || 0); - elOption.hasOwnProperty('silent') && el.attr('silent', elOption.silent); - elOption.hasOwnProperty('invisible') && el.attr('invisible', elOption.invisible); - elOption.hasOwnProperty('ignore') && el.attr('ignore', elOption.ignore); - // `elOption.info` enables user to mount some info on - // elements and use them in event handlers. - // Update them only when user specified, otherwise, remain. - elOption.hasOwnProperty('info') && el.attr('info', elOption.info); - - // If `elOption.styleEmphasis` is `false`, remove hover style. The - // logic is ensured by `graphicUtil.setElementHoverStyle`. - const styleEmphasis = elOption.styleEmphasis; - // hoverStyle should always be set here, because if the hover style - // may already be changed, where the inner cache should be reset. - graphicUtil.enableElementHoverEmphasis(el, styleEmphasis); + zrUtil.hasOwn(elOption, 'silent') && (el.silent = elOption.silent); + zrUtil.hasOwn(elOption, 'ignore') && (el.ignore = elOption.ignore); + + if (!isTextContent) { + // `elOption.info` enables user to mount some info on + // elements and use them in event handlers. + // Update them only when user specified, otherwise, remain. + zrUtil.hasOwn(elOption, 'info') && (inner(el).info = elOption.info); + } + + el.markRedraw(); +} + +function updateElOnState( + state: DisplayStateNonNormal, + el: Element, + elStateOpt: CustomElementOptionOnState, + styleOpt: StyleOption, + attachedTxInfo: AttachedTxInfo, + isRoot: boolean, + isTextContent: boolean +): void { + const elDisplayable = el.isGroup ? null : el as Displayable; + const txCfgOpt = attachedTxInfo && attachedTxInfo[state].cfg; + + // PENDING:5.0 support customize scale change and transition animation? + + if (elDisplayable) { + // By default support auto lift color when hover whether `emphasis` specified. + const stateObj = elDisplayable.ensureState(state); + if (styleOpt === false) { + const existingEmphasisState = elDisplayable.getState(state); + if (existingEmphasisState) { + existingEmphasisState.style = null; + } + } + else { + // style is needed to enable defaut emphasis. + stateObj.style = styleOpt || {}; + } + // If `elOption.styleEmphasis` or `elOption.emphasis.style` is `false`, + // remove hover style. + // If `elOption.textConfig` or `elOption.emphasis.textConfig` is null/undefined, it does not + // make sense. So for simplicity, we do not ditinguish `hasOwnProperty` and null/undefined. + if (txCfgOpt) { + stateObj.textConfig = txCfgOpt; + } + + graphicUtil.enableElementHoverEmphasis(elDisplayable); + } + if (isRoot) { - graphicUtil.setAsHighDownDispatcher(el, styleEmphasis !== false); + graphicUtil.setAsHighDownDispatcher(el, styleOpt !== false); + } +} + +function updateZ( + el: Element, + elOption: CustomElementOption, + seriesModel: CustomSeriesModel, + attachedTxInfo: AttachedTxInfo +): void { + // Group not support textContent and not support z yet. + if (el.isGroup) { + return; + } + + const elDisplayable = el as Displayable; + const currentZ = seriesModel.currentZ; + const currentZLevel = seriesModel.currentZLevel; + // Always erase. + elDisplayable.z = currentZ; + elDisplayable.zlevel = currentZLevel; + // z2 must not be null/undefined, otherwise sort error may occur. + const optZ2 = elOption.z2; + optZ2 != null && (elDisplayable.z2 = optZ2 || 0); + + const textContent = elDisplayable.getTextContent(); + if (textContent) { + textContent.z = currentZ; + textContent.zlevel = currentZLevel; + } + + updateZForEachState(elDisplayable, textContent, elOption, attachedTxInfo, NORMAL); + updateZForEachState(elDisplayable, textContent, elOption, attachedTxInfo, EMPHASIS); +} + +function updateZForEachState( + elDisplayable: Displayable, + textContent: Displayable, + elOption: CustomDisplayableOption, + attachedTxInfo: AttachedTxInfo, + state: DisplayState +): void { + const isNormal = state === NORMAL; + const elStateOpt = isNormal ? elOption : retrieveStateOption(elOption, state as DisplayStateNonNormal); + const optZ2 = elStateOpt ? elStateOpt.z2 : null; + let stateObj; + if (optZ2 != null) { + // Do not `ensureState` until required. + stateObj = isNormal ? elDisplayable : elDisplayable.ensureState(state); + stateObj.z2 = optZ2 || 0; + } + + const txConOpt = attachedTxInfo[state].conOpt; + if (textContent) { + const innerEl = inner(elDisplayable); + const txConZ2Set = innerEl.txConZ2Set || 0; + const txOptZ2 = txConOpt ? txConOpt.z2 : null; + const z2SetMask = 1 << Z2_SPECIFIED_BIT[state]; + + // Set textContent z2 as hostEl.z2 + 1 only if + // textContent z2 is not specified. + if (txOptZ2 != null) { + // Do not `ensureState` until required. + (isNormal ? textContent : textContent.ensureState(state)).z2 = txOptZ2; + innerEl.txConZ2Set = txConZ2Set | z2SetMask; + } + // If stateObj exists, that means stateObj.z2 has been updated, where the textContent z2 + // should be followed, no matter textContent or textContent.emphasis is specified in elOption. + else if (stateObj && (txConZ2Set & z2SetMask) === 0) { + (isNormal ? textContent : textContent.ensureState(state)).z2 = stateObj.z2 + 1; + } } } -function prepareStyleTransition(prop, targetStyle, elOptionStyle, oldElStyle, isInit) { +function setLagecyProp( + elOption: CustomElementOption, + transitionProps: Partial<Pick<Transformable, TransformProps>>, + legacyName: 'position' | 'scale' | 'origin', + xName: TransformPropsX, + yName: TransformPropsY +): void { + const legacyArr = (elOption as any)[legacyName]; + legacyArr && (transitionProps[xName] = legacyArr[0], transitionProps[yName] = legacyArr[1]); +} +function setTransProp( + elOption: CustomElementOption, + transitionProps: Partial<Pick<Transformable, TransformProps>>, + name: TransformProps +): void { + elOption[name] != null && (transitionProps[name] = elOption[name]); +} + +function prepareStyleTransition( + prop: 'x' | 'y', + targetStyle: CustomTextOption['style'], + elOptionStyle: CustomTextOption['style'], + oldElStyle: graphicUtil.Text['style'], + isInit: boolean +): void; +function prepareStyleTransition( + prop: 'x' | 'y' | 'width' | 'height', + targetStyle: CustomImageOption['style'], + elOptionStyle: CustomImageOption['style'], + oldElStyle: graphicUtil.Image['style'], + isInit: boolean +): void; +function prepareStyleTransition( + prop: string, + targetStyle: any, + elOptionStyle: any, + oldElStyle: any, + isInit: boolean +): void { if (elOptionStyle[prop] != null && !isInit) { targetStyle[prop] = elOptionStyle[prop]; elOptionStyle[prop] = oldElStyle[prop]; } } -function makeRenderItem(customSeries, data, ecModel, api) { +function makeRenderItem( + customSeries: CustomSeriesModel, + data: List<CustomSeriesModel>, + ecModel: GlobalModel, + api: ExtensionAPI +) { const renderItem = customSeries.get('renderItem'); const coordSys = customSeries.coordinateSystem; - let prepareResult = {}; + let prepareResult = {} as ReturnType<PrepareCustomInfo>; if (coordSys) { if (__DEV__) { @@ -379,8 +827,9 @@ function makeRenderItem(customSeries, data, ecModel, api) { ); } + // `coordSys.prepareCustoms` is used for external coord sys like bmap. prepareResult = coordSys.prepareCustoms - ? coordSys.prepareCustoms() + ? coordSys.prepareCustoms(coordSys) : prepareCustoms[coordSys.type](coordSys); } @@ -396,9 +845,9 @@ function makeRenderItem(customSeries, data, ecModel, api) { barLayout: barLayout, currentSeriesIndices: currentSeriesIndices, font: font - }, prepareResult.api || {}); + }, prepareResult.api || {}) as CustomSeriesRenderItemAPI; - const userParams = { + const userParams: CustomSeriesRenderItemParams = { // The life cycle of context: current round of rendering. // The global life cycle is probably not necessary, because // user can store global status by themselves. @@ -411,17 +860,54 @@ function makeRenderItem(customSeries, data, ecModel, api) { encode: wrapEncodeDef(customSeries.getData()) }; + // If someday intending to refactor them to a class, should consider do not + // break change: currently these attribute member are encapsulated in a closure + // so that do not need to force user to call these method with a scope. + // Do not support call `api` asynchronously without dataIndexInside input. - let currDataIndexInside; - let currDirty = true; - let currItemModel; - let currLabelNormalModel; - let currLabelEmphasisModel; - let currVisualColor; - - return function (dataIndexInside, payload) { + let currDataIndexInside: number; + let currItemModel: Model<CustomSeriesOption>; + let currItemStyleModels: Partial<Record<DisplayState, Model<CustomSeriesOption['itemStyle']>>> = {}; + let currLabelModels: Partial<Record<DisplayState, Model<CustomSeriesOption['label']>>> = {}; + + const seriesItemStyleModels = { + normal: customSeries.getModel(PATH_ITEM_STYLE.normal), + emphasis: customSeries.getModel(PATH_ITEM_STYLE.emphasis) + } as Record<DisplayState, Model<CustomSeriesOption['label']>>; + const seriesLabelModels = { + normal: customSeries.getModel(PATH_LABEL.normal), + emphasis: customSeries.getModel(PATH_LABEL.emphasis) + } as Record<DisplayState, Model<CustomSeriesOption['label']>>; + + function getItemModel(dataIndexInside: number): Model<CustomSeriesOption> { + return dataIndexInside === currDataIndexInside + ? (currItemModel || (currItemModel = data.getItemModel(dataIndexInside))) + : data.getItemModel(dataIndexInside); + } + function getItemStyleModel(dataIndexInside: number, state: DisplayState) { + return !data.hasItemOption + ? seriesItemStyleModels[state] + : dataIndexInside === currDataIndexInside + ? (currItemStyleModels[state] || ( + currItemStyleModels[state] = getItemModel(dataIndexInside).getModel(PATH_ITEM_STYLE[state]) + )) + : getItemModel(dataIndexInside).getModel(PATH_ITEM_STYLE[state]); + } + function getLabelModel(dataIndexInside: number, state: DisplayState) { + return !data.hasItemOption + ? seriesLabelModels[state] + : dataIndexInside === currDataIndexInside + ? (currLabelModels[state] || ( + currLabelModels[state] = getItemModel(dataIndexInside).getModel(PATH_LABEL[state]) + )) + : getItemModel(dataIndexInside).getModel(PATH_LABEL[state]); + } + + return function (dataIndexInside: number, payload: Payload): CustomElementOption { currDataIndexInside = dataIndexInside; - currDirty = true; + currItemModel = null; + currItemStyleModels = {}; + currLabelModels = {}; return renderItem && renderItem( zrUtil.defaults({ @@ -434,158 +920,172 @@ function makeRenderItem(customSeries, data, ecModel, api) { ); }; - // Do not update cache until api called. - function updateCache(dataIndexInside) { - dataIndexInside == null && (dataIndexInside = currDataIndexInside); - if (currDirty) { - currItemModel = data.getItemModel(dataIndexInside); - currLabelNormalModel = currItemModel.getModel(LABEL_NORMAL); - currLabelEmphasisModel = currItemModel.getModel(LABEL_EMPHASIS); - currVisualColor = data.getItemVisual(dataIndexInside, 'color'); - - currDirty = false; - } - } - /** * @public - * @param {number|string} dim - * @param {number} [dataIndexInside=currDataIndexInside] - * @return {number|string} value + * @param dim by default 0. + * @param dataIndexInside by default `currDataIndexInside`. */ - function value(dim, dataIndexInside) { + function value(dim?: DimensionLoose, dataIndexInside?: number): ParsedValue { dataIndexInside == null && (dataIndexInside = currDataIndexInside); return data.get(data.getDimension(dim || 0), dataIndexInside); } /** + * @deprecated The orgininal intention of `api.style` is enable to set itemStyle + * like other series. But it not necessary and not easy to give a strict definition + * of what it return. And since echarts5 it needs to be make compat work. So + * deprecates it since echarts5. + * * By default, `visual` is applied to style (to support visualMap). * `visual.color` is applied at `fill`. If user want apply visual.color on `stroke`, * it can be implemented as: * `api.style({stroke: api.visual('color'), fill: null})`; + * + * [Compat]: since ec5, RectText has been separated from its hosts el. + * so `api.style()` will only return the style from `itemStyle` but not handle `label` + * any more. But `series.label` config is never published in doc. + * We still compat it in `api.style()`. But not encourage to use it and will still not + * to pulish it to doc. * @public - * @param {Object} [extra] - * @param {number} [dataIndexInside=currDataIndexInside] + * @param dataIndexInside by default `currDataIndexInside`. */ - function style(extra, dataIndexInside) { + function style(extra?: ZRStyleProps, dataIndexInside?: number): ZRStyleProps { + if (__DEV__) { + warnDeprecated('api.style', 'Please write literal style directly instead.'); + } + dataIndexInside == null && (dataIndexInside = currDataIndexInside); - updateCache(dataIndexInside); - const itemStyle = currItemModel.getModel(ITEM_STYLE_NORMAL_PATH).getItemStyle(); + const style = data.getItemVisual(dataIndexInside, 'style'); + const visualColor = style && style.fill; + const opacity = style && style.opacity; - currVisualColor != null && (itemStyle.fill = currVisualColor); - const opacity = data.getItemVisual(dataIndexInside, 'opacity'); + let itemStyle = getItemStyleModel(dataIndexInside, NORMAL).getItemStyle(); + visualColor != null && (itemStyle.fill = visualColor); opacity != null && (itemStyle.opacity = opacity); - const labelModel = extra - ? applyExtraBefore(extra, currLabelNormalModel) - : currLabelNormalModel; - - const textStyle = graphicUtil.createTextStyle(labelModel, null, { - autoColor: currVisualColor, - isRectText: true - }); - - // TODO - zrUtil.extend(itemStyle, textStyle); - - itemStyle.text = labelModel.getShallow('show') + const opt = {autoColor: zrUtil.isString(visualColor) ? visualColor : '#000'}; + const labelModel = getLabelModel(dataIndexInside, NORMAL); + // Now that the feture of "auto adjust text fill/stroke" has been migrated to zrender + // since ec5, we should set `isAttached` as `false` here and make compat in + // `convertToEC4StyleForCustomSerise`. + const textStyle = graphicUtil.createTextStyle(labelModel, null, opt, false, true); + textStyle.text = labelModel.getShallow('show') ? zrUtil.retrieve2( - customSeries.getFormattedLabel(dataIndexInside, 'normal'), + customSeries.getFormattedLabel(dataIndexInside, NORMAL), getDefaultLabel(data, dataIndexInside) ) : null; + const textConfig = graphicUtil.createTextConfig(textStyle, labelModel, opt, false); + + preFetchFromExtra(extra, itemStyle); + itemStyle = convertToEC4StyleForCustomSerise(itemStyle, textStyle, textConfig); extra && applyExtraAfter(itemStyle, extra); + (itemStyle as LegacyStyleProps).legacy = true; return itemStyle; } /** + * @deprecated The reason see `api.style()` * @public - * @param {Object} [extra] - * @param {number} [dataIndexInside=currDataIndexInside] + * @param dataIndexInside by default `currDataIndexInside`. */ - function styleEmphasis(extra, dataIndexInside) { - dataIndexInside == null && (dataIndexInside = currDataIndexInside); - updateCache(dataIndexInside); - - const itemStyle = currItemModel.getModel(ITEM_STYLE_EMPHASIS_PATH).getItemStyle(); - - const labelModel = extra - ? applyExtraBefore(extra, currLabelEmphasisModel) - : currLabelEmphasisModel; - - const textStyle = graphicUtil.createTextStyle(labelModel, null, { - isRectText: true - }, true); - zrUtil.extend(itemStyle, textStyle); + function styleEmphasis(extra?: ZRStyleProps, dataIndexInside?: number): ZRStyleProps { + if (__DEV__) { + warnDeprecated('api.styleEmphasis', 'Please write literal style directly instead.'); + } + dataIndexInside == null && (dataIndexInside = currDataIndexInside); - itemStyle.text = labelModel.getShallow('show') + let itemStyle = getItemStyleModel(dataIndexInside, EMPHASIS).getItemStyle(); + const labelModel = getLabelModel(dataIndexInside, EMPHASIS); + const textStyle = graphicUtil.createTextStyle(labelModel, null, null, true, true); + textStyle.text = labelModel.getShallow('show') ? zrUtil.retrieve3( - customSeries.getFormattedLabel(dataIndexInside, 'emphasis'), - customSeries.getFormattedLabel(dataIndexInside, 'normal'), + customSeries.getFormattedLabel(dataIndexInside, EMPHASIS), + customSeries.getFormattedLabel(dataIndexInside, NORMAL), getDefaultLabel(data, dataIndexInside) ) : null; + const textConfig = graphicUtil.createTextConfig(textStyle, labelModel, null, true); + + preFetchFromExtra(extra, itemStyle); + itemStyle = convertToEC4StyleForCustomSerise(itemStyle, textStyle, textConfig); extra && applyExtraAfter(itemStyle, extra); + (itemStyle as LegacyStyleProps).legacy = true; return itemStyle; } + function preFetchFromExtra(extra: ZRStyleProps, itemStyle: ItemStyleProps): void { + // A trick to retrieve those props firstly, which are used to + // apply auto inside fill/stroke in `convertToEC4StyleForCustomSerise`. + // (It's not reasonable but only for a degree of compat) + if (extra) { + (extra as any).textFill && ((itemStyle as any).textFill = (extra as any).textFill); + (extra as any).textPosition && ((itemStyle as any).textPosition = (extra as any).textPosition); + } + } + /** * @public - * @param {string} visualType - * @param {number} [dataIndexInside=currDataIndexInside] + * @param dataIndexInside by default `currDataIndexInside`. */ - function visual(visualType, dataIndexInside) { + function visual( + visualType: keyof DefaultDataVisual, + dataIndexInside?: number + ): ReturnType<List['getItemVisual']> { dataIndexInside == null && (dataIndexInside = currDataIndexInside); - return data.getItemVisual(dataIndexInside, visualType); + + if (zrUtil.hasOwn(STYLE_VISUAL_TYPE, visualType)) { + const style = data.getItemVisual(dataIndexInside, 'style'); + return style + ? style[STYLE_VISUAL_TYPE[visualType as keyof typeof STYLE_VISUAL_TYPE]] as any + : null; + } + // Only support these visuals. Other visual might be inner tricky + // for performance (like `style`), do not expose to users. + if (zrUtil.hasOwn(VISUAL_PROPS, visualType)) { + return data.getItemVisual(dataIndexInside, visualType); + } } /** * @public - * @param {number} opt.count Positive interger. - * @param {number} [opt.barWidth] - * @param {number} [opt.barMaxWidth] - * @param {number} [opt.barMinWidth] - * @param {number} [opt.barGap] - * @param {number} [opt.barCategoryGap] - * @return {Object} {width, offset, offsetCenter} is not support, return undefined. + * @return If not support, return undefined. */ - function barLayout(opt) { - if (coordSys.getBaseAxis) { - const baseAxis = coordSys.getBaseAxis(); - return getLayoutOnAxis(zrUtil.defaults({axis: baseAxis}, opt), api); + function barLayout( + opt: Omit<Parameters<typeof getLayoutOnAxis>[0], 'axis'> + ): ReturnType<typeof getLayoutOnAxis> { + if (coordSys.type === 'cartesian2d') { + const baseAxis = coordSys.getBaseAxis() as Axis2D; + return getLayoutOnAxis(zrUtil.defaults({axis: baseAxis}, opt)); } } /** * @public - * @return {Array.<number>} */ - function currentSeriesIndices() { + function currentSeriesIndices(): ReturnType<GlobalModel['getCurrentSeriesIndices']> { return ecModel.getCurrentSeriesIndices(); } /** * @public - * @param {Object} opt - * @param {string} [opt.fontStyle] - * @param {number} [opt.fontWeight] - * @param {number} [opt.fontSize] - * @param {string} [opt.fontFamily] - * @return {string} font string + * @return font string */ - function font(opt) { + function font( + opt: Parameters<typeof graphicUtil.getFont>[0] + ): ReturnType<typeof graphicUtil.getFont> { return graphicUtil.getFont(opt, ecModel); } } -function wrapEncodeDef(data) { - const encodeDef = {}; +function wrapEncodeDef(data: List<CustomSeriesModel>): Dictionary<number[]> { + const encodeDef = {} as Dictionary<number[]>; zrUtil.each(data.dimensions, function (dimName, dataDimIndex) { const dimInfo = data.getDimensionInfo(dimName); if (!dimInfo.isExtraCoord) { @@ -597,14 +1097,29 @@ function wrapEncodeDef(data) { return encodeDef; } -function createOrUpdate(el, dataIndex, elOption, animatableModel, group, data) { - el = doCreateOrUpdate(el, dataIndex, elOption, animatableModel, group, data, true); +function createOrUpdate( + el: Element, + dataIndex: number, + elOption: CustomElementOption, + seriesModel: CustomSeriesModel, + group: ViewRootGroup, + data: List<CustomSeriesModel> +): Element { + el = doCreateOrUpdate(el, dataIndex, elOption, seriesModel, group, data, true); el && data.setItemGraphicEl(dataIndex, el); return el; } -function doCreateOrUpdate(el, dataIndex, elOption, animatableModel, group, data, isRoot) { +function doCreateOrUpdate( + el: Element, + dataIndex: number, + elOption: CustomElementOption, + seriesModel: CustomSeriesModel, + group: ViewRootGroup, + data: List<CustomSeriesModel>, + isRoot: boolean +): Element { // [Rule] // By default, follow merge mode. @@ -616,45 +1131,75 @@ function doCreateOrUpdate(el, dataIndex, elOption, animatableModel, group, data, // regard "return;" as "show nothing element whatever", so make a exception to meet the // most cases.) - const simplyRemove = !elOption; // `null`/`undefined`/`false` - elOption = elOption || {}; + // If `elOption` is `null`/`undefined`/`false` (when `renderItem` returns nothing). + if (!elOption) { + el && group.remove(el); + return; + } + + elOption = elOption || {} as CustomElementOption; const elOptionType = elOption.type; - const elOptionShape = elOption.shape; + const elOptionShape = (elOption as CustomZRPathOption).shape; const elOptionStyle = elOption.style; - if (el && ( - simplyRemove - // || elOption.$merge === false - // If `elOptionType` is `null`, follow the merge principle. - || (elOptionType != null - && elOptionType !== el.__customGraphicType - ) - || (elOptionType === 'path' - && hasOwnPathData(elOptionShape) && getPathData(elOptionShape) !== el.__customPathData - ) - || (elOptionType === 'image' - && hasOwn(elOptionStyle, 'image') && elOptionStyle.image !== el.__customImagePath - ) - // FIXME test and remove this restriction? - || (elOptionType === 'text' - && hasOwn(elOptionShape, 'text') && elOptionStyle.text !== el.__customText - ) - )) { - group.remove(el); - el = null; + if (el) { + const elInner = inner(el); + if ( + // || elOption.$merge === false + // If `elOptionType` is `null`, follow the merge principle. + (elOptionType != null + && elOptionType !== elInner.customGraphicType + ) + || (elOptionType === 'path' + && hasOwnPathData(elOptionShape) + && getPathData(elOptionShape) !== elInner.customPathData + ) + || (elOptionType === 'image' + && zrUtil.hasOwn(elOptionStyle, 'image') + && (elOptionStyle as CustomImageOption['style']).image !== elInner.customImagePath + ) + // FIXME test and remove this restriction? + || (elOptionType === 'text' + && zrUtil.hasOwn(elOptionStyle, 'text') + && (elOptionStyle as TextStyleProps).text !== elInner.customText + ) + ) { + group.remove(el); + el = null; + } } - // `elOption.type` is undefined when `renderItem` returns nothing. - if (simplyRemove) { - return; + const isInit = !el; + + if (!el) { + el = createEl(elOption); + } + else { + // If in some case the performance issue arised, consider + // do not clearState but update cached normal state directly. + el.clearStates(); } - const isInit = !el; - !el && (el = createEl(elOption)); - updateEl(el, dataIndex, elOption, animatableModel, data, isInit, isRoot); + attachedTxInfoTmp.normal.cfg = attachedTxInfoTmp.normal.conOpt = + attachedTxInfoTmp.emphasis.cfg = attachedTxInfoTmp.emphasis.conOpt = null; + attachedTxInfoTmp.isLegacy = false; + + doCreateOrUpdateAttachedTx( + el, dataIndex, elOption, seriesModel, isInit, attachedTxInfoTmp + ); + + const stateOptEmphasis = retrieveStateOption(elOption, EMPHASIS); + const styleOptEmphasis = retrieveStyleOptionOnState(elOption, stateOptEmphasis, EMPHASIS); + + updateElNormal(el, dataIndex, elOption, elOption.style, attachedTxInfoTmp, seriesModel, isInit, false); + updateElOnState(EMPHASIS, el, stateOptEmphasis, styleOptEmphasis, attachedTxInfoTmp, isRoot, false); + + updateZ(el, elOption, seriesModel, attachedTxInfoTmp); if (elOptionType === 'group') { - mergeChildren(el, dataIndex, elOption, animatableModel, data); + mergeChildren( + el as graphicUtil.Group, dataIndex, elOption as CustomGroupOption, seriesModel, data + ); } // Always add whatever already added to ensure sequence. @@ -663,6 +1208,137 @@ function doCreateOrUpdate(el, dataIndex, elOption, animatableModel, group, data, return el; } +function doCreateOrUpdateAttachedTx( + el: Element, + dataIndex: number, + elOption: CustomElementOption, + seriesModel: CustomSeriesModel, + isInit: boolean, + attachedTxInfo: AttachedTxInfo +): void { + // group do not support textContent temporarily untill necessary. + if (el.isGroup) { + return; + } + + // Normal must be called before emphasis, for `isLegacy` detection. + processTxInfo(elOption, null, attachedTxInfo); + processTxInfo(elOption, EMPHASIS, attachedTxInfo); + + // If `elOption.textConfig` or `elOption.textContent` is null/undefined, it does not make sence. + // So for simplicity, if "elOption hasOwnProperty of them but be null/undefined", we do not + // trade them as set to null to el. + // Especially: + // `elOption.textContent: false` means remove textContent. + // `elOption.textContent.emphasis.style: false` means remove the style from emphasis state. + let txConOptNormal = attachedTxInfo.normal.conOpt as CustomElementOption | false; + const txConOptEmphasis = attachedTxInfo.emphasis.conOpt as CustomElementOptionOnState; + + if (txConOptEmphasis != null) { + // If textContent has emphasis state, el should auto has emphasis + // state, otherwise it can not be triggered. + el.ensureState(EMPHASIS); + } + + if (txConOptNormal != null || txConOptEmphasis != null) { + let textContent = el.getTextContent(); + if (txConOptNormal === false) { + textContent && el.removeTextContent(); + } + else { + txConOptNormal = attachedTxInfo.normal.conOpt = txConOptNormal || {type: 'text'}; + if (!textContent) { + textContent = createEl(txConOptNormal) as graphicUtil.Text; + el.setTextContent(textContent); + } + else { + // If in some case the performance issue arised, consider + // do not clearState but update cached normal state directly. + textContent.clearStates(); + } + const txConStlOptNormal = txConOptNormal && txConOptNormal.style; + + updateElNormal( + textContent, dataIndex, txConOptNormal, txConStlOptNormal, null, seriesModel, isInit, true + ); + const txConStlOptEmphasis = retrieveStyleOptionOnState(txConOptNormal, txConOptEmphasis, EMPHASIS); + updateElOnState(EMPHASIS, textContent, txConOptEmphasis, txConStlOptEmphasis, null, false, true); + + textContent.markRedraw(); + } + } +} + +function processTxInfo( + elOption: CustomElementOption, + state: DisplayStateNonNormal, + attachedTxInfo: AttachedTxInfo +): void { + const stateOpt = !state ? elOption : retrieveStateOption(elOption, state); + const styleOpt = !state ? elOption.style : retrieveStyleOptionOnState(elOption, stateOpt, EMPHASIS); + + const elType = elOption.type; + let txCfg = stateOpt ? stateOpt.textConfig : null; + const txConOptNormal = elOption.textContent; + let txConOpt: CustomElementOption | CustomElementOptionOnState = + !txConOptNormal ? null : !state ? txConOptNormal : retrieveStateOption(txConOptNormal, state); + + if (styleOpt && ( + // Because emphasis style has little info to detect legacy, + // if normal is legacy, emphasis is trade as legacy. + attachedTxInfo.isLegacy + || isEC4CompatibleStyle(styleOpt, elType, !!txCfg, !!txConOpt) + )) { + attachedTxInfo.isLegacy = true; + const convertResult = convertFromEC4CompatibleStyle(styleOpt, elType, !state); + // Explicitly specified `textConfig` and `textContent` has higher priority than + // the ones generated by legacy style. Otherwise if users use them and `api.style` + // at the same time, they not both work and hardly to known why. + if (!txCfg && convertResult.textConfig) { + txCfg = convertResult.textConfig; + } + if (!txConOpt && convertResult.textContent) { + txConOpt = convertResult.textContent; + } + } + + if (!state && txConOpt) { + const txConOptNormal = txConOpt as CustomElementOption; + // `textContent: {type: 'text'}`, the "type" is easy to be missing. So we tolerate it. + !txConOptNormal.type && (txConOptNormal.type = 'text'); + if (__DEV__) { + // Do not tolerate incorret type for forward compat. + txConOptNormal.type !== 'text' && zrUtil.assert( + txConOptNormal.type === 'text', + 'textContent.type must be "text"' + ); + } + } + + const info = !state ? attachedTxInfo.normal : attachedTxInfo[state]; + info.cfg = txCfg; + info.conOpt = txConOpt; +} + +function retrieveStateOption( + elOption: CustomElementOption, state: DisplayStateNonNormal +): CustomElementOptionOnState { + return !state ? elOption : elOption ? elOption[state] : null; +} + +function retrieveStyleOptionOnState( + stateOptionNormal: CustomElementOption, + stateOption: CustomElementOptionOnState, + state: DisplayStateNonNormal +): StyleOption { + let style = stateOption && stateOption.style; + if (style == null && state === EMPHASIS && stateOptionNormal) { + style = stateOptionNormal.styleEmphasis; + } + return style; +} + + // Usage: // (1) By default, `elOption.$mergeChildren` is `'byIndex'`, which indicates that // the existing children will not be removed, and enables the feature that @@ -679,7 +1355,14 @@ function doCreateOrUpdate(el, dataIndex, elOption, animatableModel, group, data, // child (otherwise the total indicies of the children array have to be modified). // User can remove a single child by set its `ignore` as `true` or replace // it by another element, where its `$merge` can be set as `true` if necessary. -function mergeChildren(el, dataIndex, elOption, animatableModel, data) { +function mergeChildren( + el: graphicUtil.Group, + dataIndex: number, + elOption: CustomGroupOption, + seriesModel: CustomSeriesModel, + data: List<CustomSeriesModel> +): void { + const newChildren = elOption.children; const newLen = newChildren ? newChildren.length : 0; const mergeChildren = elOption.$mergeChildren; @@ -697,7 +1380,7 @@ function mergeChildren(el, dataIndex, elOption, animatableModel, data) { oldChildren: el.children() || [], newChildren: newChildren || [], dataIndex: dataIndex, - animatableModel: animatableModel, + seriesModel: seriesModel, group: el, data: data }); @@ -714,9 +1397,10 @@ function mergeChildren(el, dataIndex, elOption, animatableModel, data) { el.childAt(index), dataIndex, newChildren[index], - animatableModel, + seriesModel, el, - data + data, + false ); } if (__DEV__) { @@ -727,7 +1411,15 @@ function mergeChildren(el, dataIndex, elOption, animatableModel, data) { } } -function diffGroupChildren(context) { +type DiffGroupContext = { + oldChildren: Element[], + newChildren: CustomElementOption[], + dataIndex: number, + seriesModel: CustomSeriesModel, + group: graphicUtil.Group, + data: List<CustomSeriesModel> +}; +function diffGroupChildren(context: DiffGroupContext) { (new DataDiffer( context.oldChildren, context.newChildren, @@ -741,12 +1433,16 @@ function diffGroupChildren(context) { .execute(); } -function getKey(item, idx) { +function getKey(item: Element, idx: number): string { const name = item && item.name; return name != null ? name : GROUP_DIFF_PREFIX + idx; } -function processAddUpdate(newIndex, oldIndex) { +function processAddUpdate( + this: DataDiffer<DiffGroupContext>, + newIndex: number, + oldIndex?: number +): void { const context = this.context; const childOption = newIndex != null ? context.newChildren[newIndex] : null; const child = oldIndex != null ? context.oldChildren[oldIndex] : null; @@ -755,50 +1451,36 @@ function processAddUpdate(newIndex, oldIndex) { child, context.dataIndex, childOption, - context.animatableModel, + context.seriesModel, context.group, - context.data + context.data, + false ); } -// `graphic#applyDefaultTextStyle` will cache -// textFill, textStroke, textStrokeWidth. -// We have to do this trick. -function applyExtraBefore(extra, model) { - const dummyModel = new Model({}, model); - zrUtil.each(CACHED_LABEL_STYLE_PROPERTIES, function (stylePropName, modelPropName) { - if (extra.hasOwnProperty(stylePropName)) { - dummyModel.option[modelPropName] = extra[stylePropName]; - } - }); - return dummyModel; -} - -function applyExtraAfter(itemStyle, extra) { +function applyExtraAfter(itemStyle: ZRStyleProps, extra: ZRStyleProps): void { for (const key in extra) { - if (extra.hasOwnProperty(key) - || !CACHED_LABEL_STYLE_PROPERTIES.hasOwnProperty(key) - ) { - itemStyle[key] = extra[key]; + if (zrUtil.hasOwn(extra, key)) { + (itemStyle as any)[key] = (extra as any)[key]; } } } -function processRemove(oldIndex) { +function processRemove(this: DataDiffer<DiffGroupContext>, oldIndex: number): void { const context = this.context; const child = context.oldChildren[oldIndex]; child && context.group.remove(child); } -function getPathData(shape) { +/** + * @return SVG Path data. + */ +function getPathData(shape: CustomSVGPathOption['shape']): string { // "d" follows the SVG convention. return shape && (shape.pathData || shape.d); } -function hasOwnPathData(shape) { - return shape && (shape.hasOwnProperty('pathData') || shape.hasOwnProperty('d')); +function hasOwnPathData(shape: CustomSVGPathOption['shape']): boolean { + return shape && (zrUtil.hasOwn(shape, 'pathData') || zrUtil.hasOwn(shape, 'd')); } -function hasOwn(host, prop) { - return host && host.hasOwnProperty(prop); -} diff --git a/src/coord/CoordinateSystem.ts b/src/coord/CoordinateSystem.ts index 2af741a..84fd2f5 100644 --- a/src/coord/CoordinateSystem.ts +++ b/src/coord/CoordinateSystem.ts @@ -26,6 +26,7 @@ import { BoundingRect } from '../util/graphic'; import { MatrixArray } from 'zrender/src/core/matrix'; import ComponentModel from '../model/Component'; import { RectLike } from 'zrender/src/core/BoundingRect'; +import { PrepareCustomInfo } from '../chart/custom'; export interface CoordinateSystemCreator { @@ -151,6 +152,8 @@ export interface CoordinateSystem { // Currently only Cartesian2D implements it. // But if other coordinate systems implement it, should follow this signature. getAxesByScale?: (scaleType: string) => Axis[]; + + prepareCustoms?: PrepareCustomInfo; } /** diff --git a/src/echarts.ts b/src/echarts.ts index d60e366..08fb940 100644 --- a/src/echarts.ts +++ b/src/echarts.ts @@ -1729,6 +1729,9 @@ class ECharts extends Eventful { }; updateZ = function (model: ComponentModel, view: ComponentView | ChartView): void { + if (model.preventAutoZ) { + return; + } const z = model.get('z'); const zlevel = model.get('zlevel'); // Set z and zlevel @@ -1743,6 +1746,7 @@ class ECharts extends Eventful { textContent.z = el.z; textContent.zlevel = el.zlevel; // lift z2 of text content + // TODO if el.emphasis.z2 is spcefied, what about textContent. textContent.z2 = el.z2 + 1; } } diff --git a/src/model/Component.ts b/src/model/Component.ts index 0d2b0c0..d1a35e9 100644 --- a/src/model/Component.ts +++ b/src/model/Component.ts @@ -123,6 +123,11 @@ class ComponentModel<Opt extends ComponentOption = ComponentOption> extends Mode */ static layoutMode: ComponentLayoutMode | ComponentLayoutMode['type']; + /** + * Prevent from auto set z, zlevel, z2 by the framework. + */ + preventAutoZ: boolean; + // Injectable properties: __viewId: string; diff --git a/src/model/mixin/itemStyle.ts b/src/model/mixin/itemStyle.ts index c0c88e0..129c67f 100644 --- a/src/model/mixin/itemStyle.ts +++ b/src/model/mixin/itemStyle.ts @@ -44,7 +44,7 @@ type ItemStyleKeys = 'fill' | 'shadowOffsetY' | 'shadowColor'; -type ItemStyleProps = Pick<PathStyleProps, ItemStyleKeys>; +export type ItemStyleProps = Pick<PathStyleProps, ItemStyleKeys>; class ItemStyleMixin { diff --git a/src/util/graphic.ts b/src/util/graphic.ts index 2875480..cadfa38 100644 --- a/src/util/graphic.ts +++ b/src/util/graphic.ts @@ -58,7 +58,8 @@ import { ColorString, DataModel, ECEventData, - ZRStyleProps + ZRStyleProps, + TextCommonOption } from './types'; import GlobalModel from '../model/Global'; import { makeInner } from './model'; @@ -81,13 +82,6 @@ const EMPTY_OBJ = {}; export const Z2_EMPHASIS_LIFT = 10; -// key: label model property nane, value: style property name. -export const CACHED_LABEL_STYLE_PROPERTIES = { - color: 'textFill', - textBorderColor: 'textStroke', - textBorderWidth: 'textStrokeWidth' -}; - const EMPHASIS = 'emphasis'; const NORMAL = 'normal'; @@ -126,8 +120,6 @@ type TextCommonParams = { forceRich?: boolean - getTextPosition?: (textStyleModel: Model, isEmphasis?: boolean) => string | string[] | number[] - defaultOutsidePosition?: LabelOption['position'] textStyle?: ZRStyleProps @@ -390,12 +382,14 @@ function singleEnterEmphasis(el: Element) { if (!hasFillOrStroke(emphasisStyle.stroke)) { disp.style.stroke = liftColor(currentStroke); } - disp.z2 += Z2_EMPHASIS_LIFT; + const z2EmphasisLift = (disp as ECElement).z2EmphasisLift; + disp.z2 += z2EmphasisLift != null ? z2EmphasisLift : Z2_EMPHASIS_LIFT; } const textContent = el.getTextContent(); if (textContent) { - textContent.z2 += Z2_EMPHASIS_LIFT; + const z2EmphasisLift = (textContent as ECElement).z2EmphasisLift; + textContent.z2 += z2EmphasisLift != null ? z2EmphasisLift : Z2_EMPHASIS_LIFT; } // TODO hover layer } @@ -770,6 +764,7 @@ export function createTextConfig( opt: TextCommonParams, isEmphasis: boolean ) { + opt = opt || {}; const textConfig: ElementTextConfig = {}; let labelPosition; let labelRotate = textStyleModel.getShallow('rotate'); @@ -778,16 +773,11 @@ export function createTextConfig( ); const labelOffset = textStyleModel.getShallow('offset'); - if (opt.getTextPosition) { - labelPosition = opt.getTextPosition(textStyleModel, isEmphasis); - } - else { - labelPosition = textStyleModel.getShallow('position') - || (isEmphasis ? null : 'inside'); - // 'outside' is not a valid zr textPostion value, but used - // in bar series, and magric type should be considered. - labelPosition === 'outside' && (labelPosition = opt.defaultOutsidePosition || 'top'); - } + labelPosition = textStyleModel.getShallow('position') + || (isEmphasis ? null : 'inside'); + // 'outside' is not a valid zr textPostion value, but used + // in bar series, and magric type should be considered. + labelPosition === 'outside' && (labelPosition = opt.defaultOutsidePosition || 'top'); if (labelPosition != null) { textConfig.position = labelPosition; @@ -1031,7 +1021,10 @@ function setTokenTextStyle( } } -export function getFont(opt: LabelOption, ecModel: GlobalModel) { +export function getFont( + opt: Pick<TextCommonOption, 'fontStyle' | 'fontWeight' | 'fontSize' | 'fontFamily'>, + ecModel: GlobalModel +) { const gTextStyleModel = ecModel && ecModel.getModel('textStyle'); return trim([ // FIXME in node-canvas fontWeight is before fontStyle diff --git a/src/util/styleCompat.ts b/src/util/styleCompat.ts new file mode 100644 index 0000000..b7e1150 --- /dev/null +++ b/src/util/styleCompat.ts @@ -0,0 +1,256 @@ +/* +* 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 { Dictionary, ZRStyleProps } from './types'; +import { ElementTextConfig } from 'zrender/src/Element'; +import { TextStyleProps, TextStylePropsPart, TextProps } from 'zrender/src/graphic/Text'; +import { each, hasOwn } from 'zrender/src/core/util'; +import { __DEV__ } from '../config'; +import { ItemStyleProps } from '../model/mixin/itemStyle'; + +export interface LegacyStyleProps { + legacy?: boolean +} + +const deprecatedLogs = {} as Dictionary<boolean>; + +/** + * Whether need to call `convertEC4CompatibleStyle`. + */ +export function isEC4CompatibleStyle( + style: ZRStyleProps & LegacyStyleProps, + elType: string, + hasOwnTextContentOption: boolean, + hasOwnTextConfig: boolean +): boolean { + // Since echarts5, `RectText` is separated from its host element and style.text + // does not exist any more. The compat work brings some extra burden on performance. + // So we provide: + // `legacy: true` force make compat. + // `legacy: false`, force do not compat. + // `legacy` not set: auto detect wheter legacy. + // But in this case we do not compat (difficult to detect and rare case): + // Becuse custom series and graphic component support "merge", users may firstly + // only set `textStrokeWidth` style or secondly only set `text`. + return style && ( + style.legacy + || ( + style.legacy !== false + && !hasOwnTextContentOption + && !hasOwnTextConfig + && elType !== 'tspan' + // Difficult to detect whether legacy for a "text" el. + && (elType === 'text' || hasOwn(style, 'text')) + ) + ); +} + +/** + * `EC4CompatibleStyle` is style that might be in echarts4 format or echarts5 format. + * @param hostStyle The properties might be modified. + * @return If be text el, `textContentStyle` and `textConfig` will not be retured. + * Otherwise a `textContentStyle` and `textConfig` will be created, whose props area + * retried from the `hostStyle`. + */ +export function convertFromEC4CompatibleStyle(hostStyle: ZRStyleProps, elType: string, isNormal: boolean): { + textContent: TextProps & {type: string}, + textConfig: ElementTextConfig +} { + const srcStyle = hostStyle as Dictionary<any>; + let textConfig: ElementTextConfig; + let textContent: TextProps & {type: string}; + + let textContentStyle: TextStyleProps; + if (elType === 'text') { + textContentStyle = srcStyle; + } + else { + textContentStyle = {}; + hasOwn(srcStyle, 'text') && (textContentStyle.text = srcStyle.text); + hasOwn(srcStyle, 'rich') && (textContentStyle.rich = srcStyle.rich); + hasOwn(srcStyle, 'textFill') && (textContentStyle.fill = srcStyle.textFill); + hasOwn(srcStyle, 'textStroke') && (textContentStyle.stroke = srcStyle.textStroke); + + textContent = { + type: 'text', + style: textContentStyle, + // ec4 do not support rectText trigger. + // And when text postion is different in normal and emphasis + // => hover text trigger emphasis; + // => text position changed, leave mouse pointer immediately; + // That might cause state incorrect. + silent: true + }; + textConfig = {}; + const hasOwnPos = hasOwn(srcStyle, 'textPosition'); + if (isNormal) { + textConfig.position = hasOwnPos ? srcStyle.textPosition : 'inside'; + } + else { + hasOwnPos && (textConfig.position = srcStyle.textPosition); + } + hasOwn(srcStyle, 'textPosition') && (textConfig.position = srcStyle.textPosition); + hasOwn(srcStyle, 'textOffset') && (textConfig.offset = srcStyle.textOffset); + hasOwn(srcStyle, 'textRotation') && (textConfig.rotation = srcStyle.textRotation); + hasOwn(srcStyle, 'textDistance') && (textConfig.distance = srcStyle.textDistance); + } + + convertEC4CompatibleRichItem(textContentStyle, hostStyle); + + each(textContentStyle.rich, function (richItem) { + convertEC4CompatibleRichItem(richItem as TextStyleProps, richItem); + }); + + return { + textConfig: textConfig, + textContent: textContent + }; +} + +/** + * The result will be set to `out`. + */ +function convertEC4CompatibleRichItem(out: TextStylePropsPart, richItem: Dictionary<any>): void { + if (!richItem) { + return; + } + // (1) For simplicity, make textXXX properties (deprecated since ec5) has + // higher priority. For example, consider in ec4 `borderColor: 5, textBorderColor: 10` + // on a rect means `borderColor: 4` on the rect and `borderColor: 10` on an attached + // richText in ec5. + // (2) `out === richItem` if and only if `out` is text el or rich item. + // So we can overwite existing props in `out` since textXXX has higher priority. + richItem.font = richItem.textFont || richItem.font; + hasOwn(richItem, 'textStrokeWidth') && (out.lineWidth = richItem.textStrokeWidth); + hasOwn(richItem, 'textAlign') && (out.align = richItem.textAlign); + hasOwn(richItem, 'textVerticalAlign') && (out.verticalAlign = richItem.textVerticalAlign); + hasOwn(richItem, 'textLineHeight') && (out.lineHeight = richItem.textLineHeight); + hasOwn(richItem, 'textWidth') && (out.width = richItem.textWidth); + hasOwn(richItem, 'textHeight') && (out.height = richItem.textHeight); + hasOwn(richItem, 'textBackgroundColor') && (out.backgroundColor = richItem.textBackgroundColor); + hasOwn(richItem, 'textPadding') && (out.padding = richItem.textPadding); + hasOwn(richItem, 'textBorderColor') && (out.borderColor = richItem.textBorderColor); + hasOwn(richItem, 'textBorderWidth') && (out.borderWidth = richItem.textBorderWidth); + hasOwn(richItem, 'textBorderRadius') && (out.borderRadius = richItem.textBorderRadius); + hasOwn(richItem, 'textBoxShadowColor') && (out.shadowColor = richItem.textBoxShadowColor); + hasOwn(richItem, 'textBoxShadowBlur') && (out.shadowBlur = richItem.textBoxShadowBlur); + hasOwn(richItem, 'textBoxShadowOffsetX') && (out.shadowOffsetX = richItem.textBoxShadowOffsetX); + hasOwn(richItem, 'textBoxShadowOffsetY') && (out.shadowOffsetY = richItem.textBoxShadowOffsetY); +} + +/** + * Convert to pure echarts4 format style. + * `itemStyle` will be modified, added with ec4 style properties from + * `textStyle` and `textConfig`. + * + * [Caveat]: For simplicity, `insideRollback` in ec4 does not compat, where + * `styleEmphasis: {textFill: 'red'}` will remove the normal auto added stroke. + */ +export function convertToEC4StyleForCustomSerise( + itemStl: ItemStyleProps, + txStl: TextStyleProps, + txCfg: ElementTextConfig +): ZRStyleProps { + + const out = itemStl as Dictionary<unknown>; + + // See `custom.ts`, a trick to set extra `textPosition` firstly. + out.textPosition = out.textPosition || txCfg.position || 'inside'; + txCfg.offset != null && (out.textOffset = txCfg.offset); + txCfg.rotation != null && (out.textRotation = txCfg.rotation); + txCfg.distance != null && (out.textDistance = txCfg.distance); + + const isInside = (out.textPosition as string).indexOf('inside') >= 0; + const hostFill = itemStl.fill || '#000'; + + convertToEC4RichItem(out, txStl); + + const textFillNotSet = out.textFill == null; + if (isInside) { + if (textFillNotSet) { + out.textFill = txCfg.insideFill || '#fff'; + !out.textStroke && txCfg.insideStroke && (out.textStroke = txCfg.insideStroke); + !out.textStroke && (out.textStroke = hostFill); + out.textStrokeWidth == null && (out.textStrokeWidth = 2); + } + } + else { + if (textFillNotSet) { + out.textFill = txCfg.outsideFill || hostFill; + } + !out.textStroke && txCfg.outsideStroke && (out.textStroke = txCfg.outsideStroke); + } + + out.text = txStl.text; + out.rich = txStl.rich; + + each(txStl.rich, function (richItem) { + convertToEC4RichItem(richItem as Dictionary<unknown>, richItem); + }); + + return out; +} + +function convertToEC4RichItem(out: Dictionary<unknown>, richItem: TextStylePropsPart) { + if (!richItem) { + return; + } + + hasOwn(richItem, 'fill') && (out.textFill = richItem.fill); + hasOwn(richItem, 'stroke') && (out.textStroke = richItem.fill); + + hasOwn(richItem, 'lineWidth') && (out.textStrokeWidth = richItem.lineWidth); + hasOwn(richItem, 'font') && (out.textStrokeWidth = richItem.font); + hasOwn(richItem, 'fontStyle') && (out.fontStyle = richItem.fontStyle); + hasOwn(richItem, 'fontWeight') && (out.fontWeight = richItem.fontWeight); + hasOwn(richItem, 'fontSize') && (out.fontSize = richItem.fontSize); + hasOwn(richItem, 'fontFamily') && (out.fontFamily = richItem.fontFamily); + + hasOwn(richItem, 'align') && (out.textAlign = richItem.align); + hasOwn(richItem, 'verticalAlign') && (out.textVerticalAlign = richItem.verticalAlign); + hasOwn(richItem, 'lineHeight') && (out.textLineHeight = richItem.lineHeight); + hasOwn(richItem, 'width') && (out.textWidth = richItem.width); + hasOwn(richItem, 'height') && (out.textHeight = richItem.height); + + hasOwn(richItem, 'backgroundColor') && (out.textBackgroundColor = richItem.backgroundColor); + hasOwn(richItem, 'padding') && (out.textPadding = richItem.padding); + hasOwn(richItem, 'borderColor') && (out.textBorderColor = richItem.borderColor); + hasOwn(richItem, 'borderWidth') && (out.textBorderWidth = richItem.borderWidth); + hasOwn(richItem, 'borderRadius') && (out.textBorderRadius = richItem.borderRadius); + + hasOwn(richItem, 'shadowColor') && (out.textBoxShadowColor = richItem.shadowColor); + hasOwn(richItem, 'shadowBlur') && (out.textBoxShadowBlur = richItem.shadowBlur); + hasOwn(richItem, 'shadowOffsetX') && (out.textBoxShadowOffsetX = richItem.shadowOffsetX); + hasOwn(richItem, 'shadowOffsetY') && (out.textBoxShadowOffsetY = richItem.shadowOffsetY); + + hasOwn(richItem, 'textShadowColor') && (out.textShadowColor = richItem.textShadowColor); + hasOwn(richItem, 'textShadowBlur') && (out.textShadowBlur = richItem.textShadowBlur); + hasOwn(richItem, 'textShadowOffsetX') && (out.textShadowOffsetX = richItem.textShadowOffsetX); + hasOwn(richItem, 'textShadowOffsetY') && (out.textShadowOffsetY = richItem.textShadowOffsetY); +} + +export function warnDeprecated(deprecated: string, insteadApproach: string): void { + if (__DEV__) { + const key = deprecated + '^_^' + insteadApproach; + if (!deprecatedLogs[key]) { + console.warn(`DEPRECATED: "${deprecated}" has been deprecated. ${insteadApproach}`); + deprecatedLogs[key] = true; + } + } +} diff --git a/src/util/types.ts b/src/util/types.ts index 07f773e..b74d08c 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -106,6 +106,7 @@ export interface ECElement extends Element { }; highDownSilentOnTouch?: boolean; onStateChange?: (fromState: 'normal' | 'emphasis', toState: 'normal' | 'emphasis') => void; + z2EmphasisLift?: number; } export interface DataHost { @@ -422,6 +423,7 @@ export type ModelOption = any; export type ThemeOption = Dictionary<any>; export type DisplayState = 'normal' | 'emphasis'; +export type DisplayStateNonNormal = 'emphasis'; export type DisplayStateHostOption = { emphasis?: Dictionary<any>, [key: string]: any diff --git a/test/circle-packing-with-d3.html b/test/circle-packing-with-d3.compat.html similarity index 100% copy from test/circle-packing-with-d3.html copy to test/circle-packing-with-d3.compat.html diff --git a/test/circle-packing-with-d3.html b/test/circle-packing-with-d3.html index 486b6e3..06a77ec 100644 --- a/test/circle-packing-with-d3.html +++ b/test/circle-packing-with-d3.html @@ -45,6 +45,7 @@ text { <svg width="960" height="960"><g transform="translate(1,1)"></g></svg> <script src="https://d3js.org/d3.v4.min.js"></script> <script src="../dist/echarts.js"></script> +<script src="./lib/testHelper.js"></script> <script> var stratify = d3.stratify() @@ -105,6 +106,8 @@ d3.csv("data/flare.csv", function(error, rawData) { nodeName = nodePath.slice(nodePath.lastIndexOf('.') + 1).split(/(?=[A-Z][^A-Z])/g).join('\n'); nodeName = echarts.format.truncateText(nodeName, itemLayout.r, textFont, '.'); } + var z2 = api.value(1) * 2; + // console.log(api.style()); return { type: 'circle', @@ -113,19 +116,35 @@ d3.csv("data/flare.csv", function(error, rawData) { cy: itemLayout.y, r: itemLayout.r }, - z2: api.value(1) * 2, - style: api.style({ - text: nodeName, - textFont: textFont, - textPosition: 'inside' - }), - styleEmphasis: api.style({ + z2: z2, + textContent: { + type: 'text', + style: { + text: nodeName, + // fill: 'blue' + }, + emphasis: { + style: { + fontSize: 16, + // fill: 'red' + } + } + }, + textConfig: { + position: 'inside' + }, + style: { + fill: api.visual('color'), text: nodeName, - textPosition: 'inside', - textFont: textFont, - stroke: 'rgba(0,0,0,0.5)', - lineWidth: 3 - }) + font: textFont, + }, + emphasis: { + style: { + font: textFont, + stroke: 'rgba(0,0,0,0.5)', + lineWidth: 3 + } + } }; } @@ -165,6 +184,13 @@ d3.csv("data/flare.csv", function(error, rawData) { chart.setOption(option); + // testHelper.printElements(chart, { + // attr: ['z', 'z2', 'style.text', 'style.fill', 'style.stroke'], + // filter: function (el) { + // return el.style && el.style.text; + // } + // }); + }); </script> diff --git a/test/custom-feature.html b/test/custom-feature.html index 7ec7696..03457ed 100644 --- a/test/custom-feature.html +++ b/test/custom-feature.html @@ -85,6 +85,7 @@ under the License. style: { fill: 'red', text: 'dataIndex: ' + params.dataIndex, + textFill: '#000', textStroke: '#fff', textStrokeWidth: 1 } diff --git a/test/custom-text-content.html b/test/custom-text-content.html new file mode 100644 index 0000000..d999acd --- /dev/null +++ b/test/custom-text-content.html @@ -0,0 +1,1193 @@ +<!DOCTYPE html> +<!-- +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. +--> + + +<html> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <script src="lib/esl.js"></script> + <script src="lib/config.js"></script> + <script src="lib/jquery.min.js"></script> + <script src="lib/facePrint.js"></script> + <script src="lib/testHelper.js"></script> + <script src="lib/canteen.js"></script> + <link rel="stylesheet" href="lib/reset.css" /> + </head> + <body> + <style> + </style> + + + <div id="Legacy_compat_test"></div> + <div id="insideFill_Stroke_auto_test"></div> + <div id="textConfig_other_feature_test"></div> + <div id="insideFill_Stroke_hover_test"></div> + <div id="z_order_test"></div> + <div id="hover_style_disable"></div> + <div id="hover_style_remove"></div> + + + <!-- Utils ----------------------------------------------- --> + <script> + function createMaker(opt) { + opt = opt || {}; + var xStart = opt.xCurr || 80; + var xCurr = xStart; + var yStart = opt.yCurr || 20; + var yCurr = yStart; + var xStep = opt.xStep || 100; + var yStep = opt.yStep || 65; + var colCount = 0; + var yMax = 0; + var maxCol = opt.maxCol || Infinity; + var children = []; + + function createRectShape(width, height) { + width = width || 20; + height = height || 30; + return {x: -width / 2, y: 0, width: width, height: height}; + } + + function makeGraphic(text, creators) { + var y = yCurr; + children.push({ + type: 'text', x: xCurr, y: y, + style: {text: text, fill: '#900', align: 'center', fontSize: 10} + }); + y += yStep; + for (var i = 0; i < creators.length; i++) { + children.push(creators[i](xCurr, y)); + y += yStep; + } + xCurr += xStep; + + yMax = Math.max(y, yMax); + + colCount++; + if (colCount >= maxCol) { + colCount = 0; + xCurr = xStart; + yCurr = yMax + yStep * 0.7; + } + } + + return { + makeGraphic: makeGraphic, + createRectShape: createRectShape, + children: children + }; + } + + // opt: {globalColor, backgroundColor} + function createOption(renderItem, opt) { + return { + animation: false, + backgroundColor: opt && opt.backgroundColor, + color: opt && opt.globalColor, + xAxis: {axisLine: {lineStyle: {color: 'rgba(0,0,0,0.2)'}}, axisLabel: {show: false}, splitLine: {show: false}}, + yAxis: {axisLine: {lineStyle: {color: 'rgba(0,0,0,0.2)'}}, axisLabel: {show: false}, splitLine: {show: false}}, + grid: {left: 30}, + series: { + type: 'custom', + renderItem: renderItem, + data: [[1, 1]] + } + }; + } + </script> + <!-- --------------------------------------------------- --> + + + + + + + <script> + require(['echarts'], function (echarts) { + + function renderItem(params, api) { + var maker = createMaker({xStep: 120, yStep: 60, maxCol: 5}); + var makeGraphic = maker.makeGraphic; + var createRectShape = maker.createRectShape; + + makeGraphic([ + 'normal: green rect', 'inside orange' + ].join('\n'), [ + function (x, y) { + return { + type: 'rect', position: [x, y], shape: createRectShape(), + style: { + // textPosition not set but by default 'inside' + text: 'legacy1', fill: 'green', textFill: 'orange' + } + }; + }, + function (x, y) { + return { + type: 'rect', x: x, y: y, shape: createRectShape(), + style: {fill: 'green'}, + textContent: {style: {text: 'ec5_api', fill: 'orange'}}, + textConfig: {position: 'inside'} + }; + } + ]); + + makeGraphic([ + 'normal: green rect', 'inside orange', 'text is number 0', 'should be displayed' + ].join('\n'), [ + function (x, y) { + return { + type: 'rect', position: [x, y], shape: createRectShape(), + style: { + // text is number 0, can be displayed + text: 0, fill: 'green', textFill: 'orange' + } + }; + }, + function (x, y) { + return { + type: 'rect', x: x, y: y, shape: createRectShape(), + style: {fill: 'green'}, + textContent: {style: {text: 0, fill: 'orange'}}, + textConfig: {position: 'inside'} + }; + } + ]); + + makeGraphic([ + 'normal: green rect', 'inside white/bordered', 'hover: red rect', 'inside white/bordered' + ].join('\n'), [ + function (x, y) { + return { + type: 'rect', position: [x, y], shape: createRectShape(), + style: api.style({text: 'legacy0'}), + styleEmphasis: {fill: 'red'} + }; + }, + function (x, y) { + return { + type: 'rect', position: [x, y], shape: createRectShape(), + style: { + text: 'legacy1', fill: 'green', textPosition: 'inside', + textFill: '#fff', textStroke: 'green', textStrokeWidth: 2 + }, + styleEmphasis: {fill: 'red'} + }; + }, + function (x, y) { + return { + type: 'rect', x: x, y: y, shape: createRectShape(), + style: {fill: 'green'}, + textContent: {style: {text: 'ec5_api'}}, + textConfig: {position: 'inside'}, + emphasis: {style: {fill: 'red'}} + }; + } + ]); + + makeGraphic([ + 'normal: green rect', 'bottom green', 'hover: red rect', 'bottom green' + ].join('\n'), [ + function (x, y) { + return { + type: 'rect', position: [x, y], shape: createRectShape(), + style: api.style({text: 'legacy0', textPosition: 'bottom'}), + styleEmphasis: {fill: 'red'} + }; + }, + function (x, y) { + return { + type: 'rect', x: x, y: y, shape: createRectShape(), + style: {fill: 'green'}, + textContent: {style: {text: 'ec5_api'}}, + textConfig: {position: 'bottom', outsideFill: 'green'}, + emphasis: {style: {fill: 'red'}} + }; + } + ]); + + makeGraphic([ + 'green rect', 'normal: inside white/bordered', 'hover: inside red' + ].join('\n'), [ + function (x, y) { + return { + type: 'rect', position: [x, y], shape: createRectShape(), + style: api.style({text: 'legacy0'}), + styleEmphasis: {textFill: 'red'} + }; + }, + function (x, y) { + return { + type: 'rect', position: [x, y], shape: createRectShape(), + style: { + text: 'legacy1', fill: 'green', textPosition: 'inside', + textFill: '#fff', textStroke: 'green', textStrokeWidth: 2 + }, + styleEmphasis: {textFill: 'red'} + }; + }, + function (x, y) { + return { + type: 'rect', x: x, y: y, shape: createRectShape(), + style: {fill: 'green'}, + textContent: { + style: {text: 'ec5_api'}, + emphasis: {style: {fill: 'red', stroke: 'green', lineWidth: 2}} + }, + textConfig: {position: 'inside'} + }; + } + ]); + + makeGraphic([ + 'green rect', 'normal: bottom red', 'hover: bottom blue' + ].join('\n'), [ + function (x, y) { + return { + type: 'rect', position: [x, y], shape: createRectShape(), + style: api.style({text: 'legacy0', textPosition: 'bottom', textFill: 'red'}), + styleEmphasis: {textFill: 'blue'} + }; + }, + function (x, y) { + return { + type: 'rect', x: x, y: y, shape: createRectShape(), + style: {fill: 'green'}, + textContent: { + style: {text: 'ec5_api', fill: 'red'}, + emphasis: {style: {fill: 'blue'}} + }, + textConfig: {position: 'bottom'} + }; + } + ]); + + makeGraphic(['green rect', 'normal: inside white/borded', 'hover: bottom'].join('\n'), [ + function (x, y) { + return { + type: 'rect', position: [x, y], shape: createRectShape(), + style: api.style({text: 'legacy0'}), + // Hover not compat to ec4 (to complicated) + styleEmphasis: {textPosition: 'bottom'} + }; + } + ]); + + makeGraphic(['green rect', 'normal: inside red', 'hover: bottom red'].join('\n'), [ + function (x, y) { + return { + type: 'rect', position: [x, y], shape: createRectShape(), + style: api.style({text: 'legacy0', textFill: 'red'}), + styleEmphasis: {textPosition: 'bottom'} + }; + } + ]); + + makeGraphic(['green rect', 'normal: inside white', 'hover: bottom red'].join('\n'), [ + function (x, y) { + return { + type: 'rect', position: [x, y], shape: createRectShape(), + style: api.style({text: 'legacy0', textFill: 'white'}), + styleEmphasis: {textFill: 'red', textPosition: 'bottom'} + }; + } + ]); + + makeGraphic(['green rect', 'normal: inside white/bordered', 'hover: auto lift'].join('\n'), [ + function (x, y) { + return { + type: 'rect', position: [x, y], shape: createRectShape(), + style: api.style({text: 'legacy0'}) + }; + }, + function (x, y) { + return { + type: 'rect', x: x, y: y, shape: createRectShape(), + style: {fill: 'green'}, + textContent: {style: {text: 'ec5_api'}}, + textConfig: {position: 'inside'} + }; + } + ]); + + makeGraphic(['green rect', 'normal: bottom green', 'hover: auto lift'].join('\n'), [ + function (x, y) { + return { + type: 'rect', position: [x, y], shape: createRectShape(), + style: api.style({text: 'legacy0', textPosition: 'bottom'}) + }; + }, + function (x, y) { + return { + type: 'rect', x: x, y: y, shape: createRectShape(), + style: {fill: 'green'}, + textContent: {style: {text: 'ec5_api', fill: 'green'}}, + textConfig: {position: 'bottom'} + }; + } + ]); + + return { + type: 'group', + children: maker.children + }; + } + + var chart = testHelper.create(echarts, 'Legacy_compat_test', { + title: [ + 'Legacy compat test', + 'Each column rects are the same effect implemented by', + 'legacy API and corresponding new API.', + 'So **each column** must be **the same result**', + '(except text string and some ec5 enhancement).', + '**Hover** also needs to be tested' + ], + height: 550, + option: createOption(renderItem, {globalColor: ['green']}) + }); + }); + </script> + + + + + + + + + + + <script> + require(['echarts'], function (echarts) { + function renderItem() { + var maker = createMaker({maxCol: 5}); + var makeGraphic = maker.makeGraphic; + var createRectShape = maker.createRectShape; + + // opt for example: + // opt.type + // opt.shape + // opt.textConfig: {}, + // opt.textContent: {}, if rich: textContent.style: {rich: {i: xxx}} + // opt.emphasis: {} + // opt.extraTitle: string[] + function makeTest(opt) { + var isRich = opt.textContent.style.rich; + var type = opt.type || 'rect'; + var shape = opt.shape || createRectShape(40, 120); + var title = ['green rect', (isRich ? 'rich text' : 'plain text')]; + opt.extraTitle && (title = title.concat(opt.extraTitle)); + makeGraphic(title.join('\n'), [ + function (x, y) { + var result = { + type: type, x: x, y: y, shape: shape, style: {fill: 'green'}, + textConfig: opt.textConfig, + textContent: opt.textContent + } + if (opt.emphasis) { + result.emphasis = opt.emphasis; + } + return result; + } + ]); + } + + makeTest({ + textConfig: {position: 'insideTop', rotation: -0.5 * Math.PI}, + textContent: {style: { + text: '90 rotated text align with:\nv: top h: middle\ndefault distance', + align: 'left', verticalAlign: 'middle' + }} + }); + + makeTest({ + textConfig: {position: 'insideTop', rotation: -0.5 * Math.PI, distance: 0}, + textContent: {style: { + text: '90 rotated text align with:\n v: top h: middle\nno distance', + align: 'left', verticalAlign: 'middle' + }} + }); + + makeTest({ + textConfig: {position: 'insideTop', rotation: -0.5 * Math.PI, distance: 0, offset: [0, -25]}, + textContent: {style: { + text: '90 rotated text outside right rect\nalign top', + fill: '#700', + align: 'left', verticalAlign: 'bottom' + }} + }); + + makeTest({ + textConfig: {position: 'insideTop', rotation: -0.5 * Math.PI}, + textContent: {style: { + text: '90 rotated text align with:\n{i|v: top h: middle}\n{a|align right}', + align: 'left', verticalAlign: 'middle', + rich: { + i: {fontSize: 20, fill: 'orange'}, + a: {align: 'right'} + } + }} + }); + + makeTest({ + extraTitle: ['emphasis:\nalign/verticalAlign rollback'], + textConfig: {position: 'left'}, + textContent: { + style: { + text: '--------\nalign\nright\n----', + fill: '#700' + } + }, + emphasis: { + textConfig: {position: null} + } + }); + + makeTest({ + type: 'circle', + shape: {cx: 0, cy: 20, r: 30}, + textConfig: { + position: 'right', + }, + textContent: { + rotation: -0.3 * Math.PI, + originX: -35, + style: { + text: 'Rotate, outside, origin is center', + fontSize: 20, + fill: '#700', + align: 'left', + verticalAlign: 'middle' + } + } + }); + + makeTest({ + type: 'circle', + shape: {cx: 0, cy: 20, r: 30}, + textConfig: { + position: 'inside', offset: [35, 0] + }, + textContent: { + rotation: -0.3 * Math.PI, + style: { + text: 'Rotate, outside, origin is center', + fontSize: 20, + fill: '#700', + align: 'left', + verticalAlign: 'middle' + } + } + }); + + return { + type: 'group', + children: maker.children + }; + } + + var chart = testHelper.create(echarts, 'textConfig_other_feature_test', { + title: [ + 'textConfig other feature test' + ], + option: createOption(renderItem), + // recordCanvas: true, + height: 800 + }); + }); + </script> + + + + + + + + + + + + + <script> + require(['echarts'], function (echarts) { + function renderItem() { + var maker = createMaker({maxCol: 6}); + var makeGraphic = maker.makeGraphic; + var createRectShape = maker.createRectShape; + + // opt for example: + // opt.textConfig: {}, + // opt.textContentStyle: {}, if rich: textContentStyle: {rich: {i: xxx}} + // opt.expect: [ {fill: 'white', stroke: 'green'}, {fill: 'white', stroke: 'green'} ] + function makeTest(opt) { + var expect0 = opt.expect[0]; + var expect1 = opt.expect[1]; + var isRich = opt.textContentStyle.rich; + makeGraphic(['green rect', (isRich ? 'rich text' : 'plain text')].join('\n'), [ + function (x, y) { + var text = [ + isRich ? '{i|inside}' : 'inside', + 'fill: ' + expect0.fill, + 'stroke: ' + expect0.stroke + ].join('\n'); + return { + type: 'rect', x: x, y: y, shape: createRectShape(), style: {fill: 'green'}, + textConfig: echarts.util.extend({position: 'inside'}, opt.textConfig), + textContent: { + style: echarts.util.extend({text: text}, opt.textContentStyle) + } + } + }, + function (x, y) { + var text = [ + isRich ? '{i|bottom}' : 'bottom', + 'fill: ' + expect1.fill, + 'stroke: ' + expect1.stroke + ].join('\n'); + return { + type: 'rect', x: x, y: y, shape: createRectShape(), style: {fill: 'green'}, + textConfig: echarts.util.extend({position: 'bottom'}, opt.textConfig), + textContent: { + style: echarts.util.extend({text: text}, opt.textContentStyle) + } + } + } + ]); + } + + makeTest({ + textConfig: {}, + textContentStyle: {}, + expect: [ + {fill: 'white', stroke: 'green'}, + {fill: 'black', stroke: 'none'} + ] + }); + + makeTest({ + textConfig: {}, + textContentStyle: {backgroundColor: '#aaa'}, + expect: [ + {fill: 'white', stroke: 'none'}, + {fill: 'black', stroke: 'none'} + ] + }); + + makeTest({ + textConfig: {}, + textContentStyle: {borderColor: '#aaa', borderWidth: 2, padding: 5}, + expect: [ + {fill: 'white', stroke: 'none'}, + {fill: 'black', stroke: 'none'} + ] + }); + + makeTest({ + textConfig: {}, + textContentStyle: {rich: {i: {fontSize: 30}}}, + expect: [ + {fill: 'white', stroke: 'green'}, + {fill: 'black', stroke: 'none'} + ] + }); + + makeTest({ + textConfig: {}, + textContentStyle: {rich: {i: {backgroundColor: '#aaa'}}}, + expect: [ + {fill: 'white', stroke: 'none'}, + {fill: 'black', stroke: 'none'} + ] + }); + + makeTest({ + textConfig: {}, + textContentStyle: {rich: {i: {borderColor: '#aaa', borderWidth: 2, fontSize: 20}}}, + expect: [ + {fill: 'white', stroke: 'none'}, + {fill: 'black', stroke: 'green'} + ] + }); + + makeTest({ + textConfig: {}, + textContentStyle: {fill: 'orange'}, + expect: [ + {fill: 'orange', stroke: 'none'}, + {fill: 'orange', stroke: 'none'} + ] + }); + + makeTest({ + textConfig: {}, + textContentStyle: {fill: 'orange', stroke: 'blue', lineWidth: 2}, + expect: [ + {fill: 'orange', stroke: 'blue'}, + {fill: 'orange', stroke: 'blue'} + ] + }); + + makeTest({ + textConfig: {}, + textContentStyle: {stroke: 'blue', lineWidth: 2}, + expect: [ + {fill: 'white', stroke: 'blue'}, + {fill: 'black', stroke: 'blue'} + ] + }); + + makeTest({ + textConfig: {insideFill: 'yellow'}, + textContentStyle: {}, + expect: [ + {fill: 'yellow', stroke: 'green'}, + {fill: 'black', stroke: 'none'} + ] + }); + + makeTest({ + textConfig: {insideFill: 'yellow'}, + textContentStyle: {fill: 'orange'}, + expect: [ + {fill: 'orange', stroke: 'none'}, + {fill: 'orange', stroke: 'none'} + ] + }); + + makeTest({ + textConfig: {insideFill: 'yellow', insideStroke: 'blue'}, + textContentStyle: {}, + expect: [ + {fill: 'yellow', stroke: 'blue'}, + {fill: 'black', stroke: 'none'} + ] + }); + + makeTest({ + textConfig: {insideFill: 'yellow', insideStroke: 'blue'}, + textContentStyle: {fill: 'pink'}, + expect: [ + {fill: 'pink', stroke: 'blue'}, + {fill: 'pink', stroke: 'none'} + ] + }); + + makeTest({ + textConfig: {insideStroke: 'blue'}, + textContentStyle: {}, + expect: [ + {fill: 'white', stroke: 'blue'}, + {fill: 'black', stroke: 'none'} + ] + }); + + makeTest({ + textConfig: {outsideFill: 'blue', outsideStroke: 'red'}, + textContentStyle: {}, + expect: [ + {fill: 'white', stroke: 'green'}, + {fill: 'blue', stroke: 'red'} + ] + }); + makeTest({ + textConfig: {outsideFill: 'blue', outsideStroke: 'red'}, + textContentStyle: {fill: 'pink'}, + expect: [ + {fill: 'pink', stroke: 'none'}, + {fill: 'pink', stroke: 'red'} + ] + }); + + return { + type: 'group', + children: maker.children + }; + } + + var chart = testHelper.create(echarts, 'insideFill_Stroke_auto_test', { + title: [ + 'insideFill/insideStroke outsideFill/outsideStroke auto rule test' + ], + option: createOption(renderItem, {backgroundColor: '#ddd'}), + height: 800 + }); + }); + </script> + + + + + + + + + + <script> + require(['echarts'], function (echarts) { + + function renderItem(params, api) { + var maker = createMaker({xCurr: 150, xStep: 150, yStep: 90}); + var makeGraphic = maker.makeGraphic; + var createRectShape = maker.createRectShape; + + makeGraphic(['insideFill/Stroke not set', 'normal: white/bordered'].join('\n'), [ + function (x, y) { + return { + type: 'rect', x: x, y: y, shape: createRectShape(), + style: {fill: 'green'}, + textContent: { + type: 'text', silent: true, style: { + text: 'if hover,\ntop black' + } + }, + textConfig: {position: 'inside'}, + emphasis: { + textConfig: {position: 'top'} + } + }; + }, + function (x, y) { + return { + type: 'rect', x: x, y: y, shape: createRectShape(), + style: {fill: 'green'}, + textContent: {type: 'text', silent: true, style: {text: 'if hover,\nright green'}}, + textConfig: { + position: 'inside', outsideFill: 'green' + }, + emphasis: { + textConfig: {position: 'right'} + } + }; + }, + function (x, y) { + return { + type: 'rect', x: x, y: y, shape: createRectShape(), + style: {fill: 'green'}, + textContent: { + silent: true, style: {text: 'if hover,\nbottom\nlarge bordered red'}, + emphasis: {style: {fontSize: 20}} + }, + textConfig: { + position: 'inside', outsideFill: 'green', outsideStroke: 'red' + }, + emphasis: { + textConfig: {position: 'bottom'} + } + }; + }, + function (x, y) { + return { + type: 'rect', x: x, y: y, shape: createRectShape(), + style: {fill: 'green'}, + textContent: { + silent: true, + style: { + text: 'rich text\nif hover,\nbottom black\n{r|normal orange\nhover red}', + rich: {r: {fontSize: 16, fill: 'orange'}}, + }, + emphasis: { + style: { + rich: {r: {fill: 'red'}} + } + } + }, + textConfig: {position: 'inside'}, + emphasis: { + textConfig: {position: 'bottom'} + } + }; + } + ]); + + makeGraphic(['green rect'].join('\n'), [ + function (x, y) { + return { + type: 'rect', x: x, y: y, shape: createRectShape(), + style: {fill: 'green'}, + textContent: {silent: true, style: { + text: 'normal:\ninside yellow/bordered\nhover:\ntop black' + }}, + textConfig: {position: 'inside', insideFill: 'yellow'}, + emphasis: { + textConfig: {position: 'top'} + } + }; + }, + function (x, y) { + return { + type: 'rect', x: x, y: y, shape: createRectShape(), + style: {fill: 'green'}, + textContent: { + silent: true, + style: {text: 'normal:\ninside white/bordered\nhover:\nright green'} + }, + textConfig: { + position: 'inside', outsideFill: 'green' + }, + emphasis: { + textConfig: {position: 'right'} + } + }; + } + ]); + + return { + type: 'group', + children: maker.children + }; + } + + var chart = testHelper.create(echarts, 'insideFill_Stroke_hover_test', { + title: [ + 'insideFill/Stroke & hover test. Please **hover any of them**' + ], + option: createOption(renderItem), + height: 600 + }); + }); + </script> + + + + + + + + + + + + + <script> + require(['echarts'], function (echarts) { + + function renderItem(params, api) { + return { + type: 'group', + children: [{ + type: 'circle', + shape: {cx: 100, cy: 100, r: 50}, + style: {fill: 'red'}, + z2: 100, + // textContent do not set z2, but auto follow the circle z2. + textContent: { + type: 'text', + style: { + text: 'normal: {red|red} is over {green|green}.\nHover me', + // stroke: '#fff', lineWidth: 2, + rich: { + red: {fill: 'red', stroke: '#fff', lineWidth: 2, fontSize: 20}, + green: {fill: 'green', stroke: '#fff', lineWidth: 2, fontSize: 20} + }, + }, + silent: true, + emphasis: { + style: { + text: 'emphasis: {green|green} over {red|red}\nText {below|below} green', + rich: { + below: {fontSize: 40} + } + } + } + }, + textConfig: { + position: 'inside' + } + }, { + type: 'circle', + shape: {cx: 100, cy: 140, r: 40}, + style: {fill: 'green'}, + // textContent do not set z2, but auto follow the circle z2. + textContent: { + type: 'text', + style: { + text: 'text should be always\n{over|over} all circles.', + rich: {over: {fontSize: 30}} + }, + silent: true + }, + textConfig: { + position: 'inside' + }, + z2: 80, + emphasis: { + z2: 110 + } + + }, { + type: 'circle', + shape: {cx: 300, cy: 100, r: 50}, + style: {fill: 'red'}, + z2: 100, + // textContent do not set z2, but auto follow the circle z2. + textContent: { + type: 'text', + style: { + text: 'normal: {red|red} is over {green|green}.\nHover me', + // stroke: '#fff', lineWidth: 2, + rich: { + red: {fill: 'red', stroke: '#fff', lineWidth: 2, fontSize: 20}, + green: {fill: 'green', stroke: '#fff', lineWidth: 2, fontSize: 20} + }, + }, + silent: true, + emphasis: { + style: { + text: 'emphasis: {red|red} over {green|green}\nText {below|over} green', + rich: { + below: {fontSize: 40} + } + } + } + }, + textConfig: { + position: 'inside' + } + }, { + type: 'circle', + shape: {cx: 300, cy: 140, r: 40}, + style: {fill: 'green'}, + // textContent do not set z2, but auto follow the circle z2. + textContent: { + type: 'text', + style: { + text: 'text should be always\n{over|over} all circles.', + rich: {over: {fontSize: 30}} + }, + silent: true, + emphasis: { + z2: 110 + } + }, + textConfig: { + position: 'inside' + }, + z2: 80 + + }] + }; + } + + var chart = testHelper.create(echarts, 'z_order_test', { + title: [ + 'z order test. Please **hover any of them**.' + ], + option: createOption(renderItem) + }); + }); + </script> + + + + + + + + + <script> + require(['echarts'], function (echarts) { + + var option = { + xAxis: {scale: true, min: 0.75, max: 3}, + yAxis: {}, + series: [{ + type: 'custom', + renderItem: function (params, api) { + var shape0 = {x: 0, y: 30, width: 100, height: 50}; + var shape1 = {x: 30, y: 50, width: 100, height: 50}; + var shape2 = {x: 60, y: 70, width: 100, height: 50}; + var position = api.coord([api.value(0), api.value(1)]); + var name = api.value(2); + + var map = { + legacy: { + type: 'group', + x: position[0], + y: position[1], + children: [{ + type: 'rect', + shape: shape0, + style: {fill: '#333'} + }, { + type: 'rect', + shape: shape1, + style: { + fill: '#555', + text: 'has inner text', + textFill: 'white', + textPostion: 'insideTop' + }, + styleEmphasis: {textFill: 'yellow'} + }, { + type: 'rect', + shape: shape2, + style: {fill: '#500'}, + styleEmphasis: false + }] + }, + ec5: { + type: 'group', + x: position[0], + y: position[1], + children: [{ + type: 'rect', + shape: shape0, + style: {fill: '#333'}, + }, { + type: 'rect', + shape: shape1, + style: {fill: '#555', text: 'has inner text'}, + textContent: { + style: {fill: 'white'}, + emphasis: {style: {fill: 'yellow'}} + }, + textConfig: {position: 'insideTop'} + }, { + type: 'rect', + shape: shape2, + style: {fill: '#500'}, + emphasis: { + // set false to disable lift color and z2. + style: false + } + }] + } + }; + + return map[name]; + }, + data: [[1, 1, 'legacy'], [2, 1, 'ec5']] + }] + }; + + var chart = testHelper.create(echarts, 'hover_style_disable', { + title: [ + 'Hover style disable: hover each group of elements,', + 'all of the elements should lift color and z2 **except the red rect**' + ], + height: 300, + option: option + }); + }); + </script> + + + + + + + + + <script> + require(['echarts'], function (echarts) { + + var option = { + xAxis: {scale: true, min: 0.75, max: 3}, + yAxis: {}, + series: [{ + type: 'custom', + renderItem: function (params, api) { + var shape0 = {x: 00, y: 30, width: 100, height: 50}; + var shape1 = {x: 30, y: 50, width: 100, height: 50}; + var shape2 = {x: 60, y: 70, width: 100, height: 50}; + var position = api.coord([api.value(0), api.value(1)]); + var name = api.value(2); + var useHover = !!api.value(3); + var map = { + legacy: { + type: 'group', + x: position[0], + y: position[1], + children: [{ + type: 'rect', + shape: shape0, + style: api.style({fill: '#222', text: 'legacy'}), + styleEmphasis: useHover ? {fill: 'red', textFill: 'yellow'} : false + }] + }, + ec5: { + type: 'group', + x: position[0], + y: position[1], + children: [{ + type: 'rect', + shape: shape0, + style: {fill: '#222'}, + textContent: { + style: {text: 'ec5_api'}, + emphasis: {style: useHover ? {fill: 'yellow'} : false} + }, + textConfig: {position: 'inside'}, + emphasis: {style: useHover ? {fill: 'red'} : false} + }] + } + }; + + return map[name]; + }, + data: [[1, 1, 'legacy', 1], [2, 1, 'ec5', 1]] + }] + }; + + var chart = testHelper.create(echarts, 'hover_style_remove', { + title: [ + 'Hover style remove test: ', + '(1) Hover each: become **red rect, yellow text**', + '(2) Click "disable hover style", elements should more to right a bit.', + '(3) Hover each again: should no hover style', + '(4) Click "enable hover style", elements should more to left a bit.', + '(5) Check whether resume to (1)', + ], + height: 300, + option: option, + buttons: [{ + text: 'disable hover style', + onclick: function () { + chart.setOption({ + xAxis: {scale: true, min: 0.75, max: 7}, + series: { + type: 'custom', + data: [[3, 1, 'legacy', 0], [6, 1, 'ec5', 0]] + } + }) + } + }, { + text: 'enable hover style', + onclick: function () { + chart.setOption({ + xAxis: {scale: true, min: 0.75, max: 3}, + series: { + type: 'custom', + data: [[1, 1, 'legacy', 1], [2, 1, 'ec5', 1]] + } + }) + } + }] + }); + }); + </script> + + + + + </body> +</html> + diff --git a/test/hoverStyle.html b/test/hoverStyle.html index 6a5bca7..245068c 100644 --- a/test/hoverStyle.html +++ b/test/hoverStyle.html @@ -680,7 +680,8 @@ under the License. // silent: true, label: { show: true, - silent: true, + // silent: true, + position: 'top' }, itemStyle: { color: 'green', --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
