From 8235095d6547ca062086bacccdc3963c9e92ed08 Mon Sep 17 00:00:00 2001 From: 100pah Date: Thu, 1 Apr 2021 06:35:56 +0800 Subject: [PATCH] featrue: [geo] (1) support name on . (2) some refactor. --- src/component/helper/MapDraw.ts | 497 +++++++++++++++++++------------- src/coord/geo/Geo.ts | 2 +- src/coord/geo/GeoSVGResource.ts | 171 ++++++----- src/coord/geo/geoTypes.ts | 18 -- src/core/echarts.ts | 40 +-- test/geo-svg-demo.html | 298 ++++++++++++++++--- test/geo-svg.html | 249 ++++++++-------- 7 files changed, 818 insertions(+), 457 deletions(-) diff --git a/src/component/helper/MapDraw.ts b/src/component/helper/MapDraw.ts index c5891196d..22caaebdd 100644 --- a/src/component/helper/MapDraw.ts +++ b/src/component/helper/MapDraw.ts @@ -26,7 +26,8 @@ import { enableHoverEmphasis, DISPLAY_STATES, enableComponentHighDownFeatures, - setDefaultStateProxy + setDefaultStateProxy, + SPECIAL_STATES } from '../../util/states'; import geoSourceManager from '../../coord/geo/geoSourceManager'; import {getUID} from '../../util/component'; @@ -39,7 +40,7 @@ import GeoView from '../geo/GeoView'; import MapView from '../../chart/map/MapView'; import Geo from '../../coord/geo/Geo'; import Model from '../../model/Model'; -import { setLabelStyle, getLabelStatesModels } from '../../label/labelStyle'; +import { setLabelStyle, getLabelStatesModels, createTextConfig } from '../../label/labelStyle'; import { getECData } from '../../util/innerStore'; import { createOrUpdatePatternFromDecal } from '../../util/decal'; import { ViewCoordSysTransformInfoPart } from '../../coord/View'; @@ -48,14 +49,16 @@ import Displayable from 'zrender/src/graphic/Displayable'; import Element, { ElementTextConfig } from 'zrender/src/Element'; import List from '../../data/List'; import { GeoJSONRegion } from '../../coord/geo/Region'; -import { RegionGraphic } from '../../coord/geo/geoTypes'; -import { ItemStyleProps } from '../../model/mixin/itemStyle'; -import { LineStyleProps } from '../../model/mixin/lineStyle'; +import { SVGNodeTagLower } from 'zrender/src/tool/parseSVG'; interface RegionsGroup extends graphic.Group { } +type RegionModel = ReturnType | ReturnType; + +type MapOrGeoModel = GeoModel | MapSeries; + interface ViewBuildContext { api: ExtensionAPI; geo: Geo; @@ -70,6 +73,26 @@ interface GeoStyleableOption { itemStyle?: GeoItemStyleOption; lineStyle?: LineStyleOption; } +type RegionName = string; + +/** + * Only these tags enable use `itemStyle` if they are named in SVG. + * Other tags like might not suitable for `itemStyle`. + * They will not be considered to be styled until some requirements come. + */ +const OPTION_STYLE_ENABLED_TAGS: SVGNodeTagLower[] = [ + 'rect', 'circle', 'line', 'ellipse', 'polygon', 'polyline', 'path' +]; +const OPTION_STYLE_ENABLED_TAG_MAP = zrUtil.createHashMap( + OPTION_STYLE_ENABLED_TAGS +); +const STATE_TRIGGER_TAG_MAP = zrUtil.createHashMap( + OPTION_STYLE_ENABLED_TAGS.concat(['g']) as SVGNodeTagLower[] +); +const LABEL_HOST_MAP = zrUtil.createHashMap( + OPTION_STYLE_ENABLED_TAGS.concat(['g']) as SVGNodeTagLower[] +); + function getFixedItemStyle(model: Model) { const itemStyle = model.getItemStyle(); @@ -115,6 +138,10 @@ class MapDraw { private _svgGraphicRecord: GeoSVGGraphicRecord; + // A name may correspond to multiple graphics. + // Used as event dispatcher. + private _svgDispatcherMap: zrUtil.HashMap; + constructor(api: ExtensionAPI) { const group = new graphic.Group(); @@ -199,6 +226,8 @@ class MapDraw { const nameMap = this._regionsGroupByName = zrUtil.createHashMap(); const regionsGroup = this._regionsGroup; const transformInfoRaw = viewBuildCtx.transformInfoRaw; + const mapOrGeoModel = viewBuildCtx.mapOrGeoModel; + const data = viewBuildCtx.data; const transformPoint = function (point: number[]): number[] { return [ @@ -211,14 +240,31 @@ class MapDraw { // Only when the resource is GeoJSON, there is `geo.regions`. zrUtil.each(viewBuildCtx.geo.regions, function (region: GeoJSONRegion) { + const regionName = region.name; + const regionModel = mapOrGeoModel.getRegionModel(regionName); + const dataIdx = data ? data.indexOfName(regionName) : null; // Consider in GeoJson properties.name may be duplicated, for example, // there is multiple region named "United Kindom" or "France" (so many // colonies). And it is not appropriate to merge them in geo, which // will make them share the same label and bring trouble in label // location calculation. - const regionGroup = nameMap.get(region.name) - || nameMap.set(region.name, new graphic.Group() as RegionsGroup); + let regionGroup = nameMap.get(regionName); + + if (!regionGroup) { + regionGroup = nameMap.set(regionName, new graphic.Group() as RegionsGroup); + regionsGroup.add(regionGroup); + + resetEventTriggerForRegion( + viewBuildCtx, regionGroup, regionName, regionModel, mapOrGeoModel, dataIdx + ); + resetTooltipForRegion( + viewBuildCtx, regionGroup, regionName, regionModel, mapOrGeoModel + ); + resetStateTriggerForRegion( + viewBuildCtx, regionGroup, regionName, regionModel, mapOrGeoModel + ); + } const compoundPath = new graphic.CompoundPath({ segmentIgnoreThreshold: 1, @@ -258,18 +304,18 @@ class MapDraw { } }); + applyOptionStyleForRegion( + viewBuildCtx, compoundPath, dataIdx, regionModel + ); + + if (compoundPath instanceof Displayable) { + compoundPath.culling = true; + } + const centerPt = transformPoint(region.getCenter()); - const regionGraphic: RegionGraphic = { - name: region.name, - el: compoundPath, - optionStyleEnabled: true, - stateTrigger: regionGroup, - eventTrigger: regionGroup, - useLabel: true - }; - this._resetSingleRegionGraphic(viewBuildCtx, regionGraphic, centerPt, null, false); - - regionsGroup.add(regionGroup); + resetLabelForRegion( + viewBuildCtx, compoundPath, regionName, regionModel, mapOrGeoModel, dataIdx, centerPt + ); }, this); } @@ -288,22 +334,69 @@ class MapDraw { this._useSVG(mapName); } + const svgDispatcherMap = this._svgDispatcherMap = zrUtil.createHashMap(); + let focusSelf = false; - zrUtil.each(this._svgGraphicRecord.regionGraphics, function (regionGraphic) { + zrUtil.each(this._svgGraphicRecord.named, function (namedItem) { // Note that we also allow different elements have the same name. // For example, a glyph of a city and the label of the city have // the same name and their tooltip info can be defined in a single // region option. - const focus = this._resetSingleRegionGraphic( - viewBuildCtx, regionGraphic, [0, 0], 'inside', - // We do not know how the SVG like so we'd better not to change z2. - // Otherwise it might bring some unexpected result. For example, - // an area hovered that make some inner city can not be clicked. - true - ); - if (focus === 'self') { - focusSelf = true; + + const regionName = namedItem.name; + const mapOrGeoModel = viewBuildCtx.mapOrGeoModel; + const data = viewBuildCtx.data; + const svgNodeTagLower = namedItem.svgNodeTagLower; + const el = namedItem.el; + + const dataIdx = data ? data.indexOfName(regionName) : null; + const regionModel = mapOrGeoModel.getRegionModel(regionName); + + if (OPTION_STYLE_ENABLED_TAG_MAP.get(svgNodeTagLower) != null + && (el instanceof Displayable) + ) { + applyOptionStyleForRegion(viewBuildCtx, el, dataIdx, regionModel); + } + + if (el instanceof Displayable) { + el.culling = true; + } + + // We do not know how the SVG like so we'd better not to change z2. + // Otherwise it might bring some unexpected result. For example, + // an area hovered that make some inner city can not be clicked. + (el as ECElement).z2EmphasisLift = 0; + + // If self named, that is, if tag is inside a named (where `namedFrom` does not exists): + if (!namedItem.namedFrom) { + // label should batter to be displayed based on the center of + // if it is named rather than displayed on each child. + if (LABEL_HOST_MAP.get(svgNodeTagLower) != null) { + resetLabelForRegion( + viewBuildCtx, el, regionName, regionModel, mapOrGeoModel, dataIdx, [0, 0] + ); + } + + resetEventTriggerForRegion( + viewBuildCtx, el, regionName, regionModel, mapOrGeoModel, dataIdx + ); + + resetTooltipForRegion( + viewBuildCtx, el, regionName, regionModel, mapOrGeoModel + ); + + if (STATE_TRIGGER_TAG_MAP.get(svgNodeTagLower) != null) { + const focus = resetStateTriggerForRegion( + viewBuildCtx, el, regionName, regionModel, mapOrGeoModel + ); + if (focus === 'self') { + focusSelf = true; + } + const els = svgDispatcherMap.get(regionName) || svgDispatcherMap.set(regionName, []); + els.push(el); + } } + }, this); // It's a little complicated to support blurring the entire geoSVG in series-map. @@ -334,157 +427,6 @@ class MapDraw { } } - private _resetSingleRegionGraphic( - viewBuildCtx: ViewBuildContext, - regionGraphic: RegionGraphic, - labelXY: number[], - labelPosition: ElementTextConfig['position'], - noZ2EmphasisLift: boolean - ): InnerFocus { - - const regionName = regionGraphic.name; - const mapOrGeoModel = viewBuildCtx.mapOrGeoModel; - const data = viewBuildCtx.data; - const isGeo = viewBuildCtx.isGeo; - - const dataIdx = data ? data.indexOfName(regionName) : null; - const regionModel = mapOrGeoModel.getRegionModel(regionName); - - applyOptionStyleForRegion(viewBuildCtx, regionGraphic, dataIdx, regionModel); - - if (regionGraphic.el instanceof Displayable) { - regionGraphic.el.culling = true; - } - if (noZ2EmphasisLift) { - (regionGraphic.el as ECElement).z2EmphasisLift = 0; - } - - let showLabel = false; - for (let i = 0; i < DISPLAY_STATES.length; i++) { - const stateName = DISPLAY_STATES[i]; - // @ts-ignore FIXME:TS fix the "compatible with each other"? - if (regionModel.get( - stateName === 'normal' ? ['label', 'show'] : [stateName, 'label', 'show'] - )) { - showLabel = true; - break; - } - } - - const isDataNaN = data && isNaN(data.get(data.mapDimension('value'), dataIdx) as number); - const itemLayout = data && data.getItemLayout(dataIdx); - - // In the following cases label will be drawn - // 1. In map series and data value is NaN - // 2. In geo component - // 3. Region has no series legendSymbol, which will be add a showLabel flag in mapSymbolLayout - if ( - regionGraphic.useLabel - && ( - ((isGeo || isDataNaN) && showLabel) - || (itemLayout && itemLayout.showLabel) - ) - ) { - const query = !isGeo ? dataIdx : regionName; - let labelFetcher; - - // Consider dataIdx not found. - if (!data || dataIdx >= 0) { - labelFetcher = mapOrGeoModel; - } - - const textEl = new graphic.Text({ - x: labelXY[0], - y: labelXY[1], - z2: 10, - silent: true - }); - textEl.afterUpdate = labelTextAfterUpdate; - - setLabelStyle( - textEl, getLabelStatesModels(regionModel), - { - labelFetcher: labelFetcher, - labelDataIndex: query, - defaultText: regionName - }, - { normal: { - align: 'center', - verticalAlign: 'middle' - } } - ); - - regionGraphic.el.setTextContent(textEl); - regionGraphic.el.setTextConfig({ - local: true, - insideFill: textEl.style.fill, - position: labelPosition - }); - (regionGraphic.el as ECElement).disableLabelAnimation = true; - } - else { - regionGraphic.el.removeTextContent(); - regionGraphic.el.removeTextConfig(); - (regionGraphic.el as ECElement).disableLabelAnimation = null; - } - - // setItemGraphicEl, setHoverStyle after all polygons and labels - // are added to the rigionGroup - if (data) { - // FIXME: when series-map use a SVG map, and there are duplicated name specified - // on different SVG elements, after `data.setItemGraphicEl(...)`: - // (1) all of them will be mounted with `dataIndex`, `seriesIndex`, so that tooltip - // can be triggered only mouse hover. That's correct. - // (2) only the last element will be kept in `data`, so that if trigger tooltip - // by `dispatchAction`, only the last one can be found and triggered. That might be - // not correct. We will fix it in future if anyone demanding that. - data.setItemGraphicEl(dataIdx, regionGraphic.eventTrigger); - } - // series-map will not trigger "geoselectchange" no matter it is - // based on a declared geo component. Becuause series-map will - // trigger "selectchange". If it trigger both the two events, - // If users call `chart.dispatchAction({type: 'toggleSelect'})`, - // it not easy to also fire event "geoselectchanged". - else { - // Package custom mouse event for geo component - getECData(regionGraphic.eventTrigger).eventData = { - componentType: 'geo', - componentIndex: mapOrGeoModel.componentIndex, - geoIndex: mapOrGeoModel.componentIndex, - name: regionName, - region: (regionModel && regionModel.option) || {} - }; - } - - if (!data) { - graphic.setTooltipConfig({ - el: regionGraphic.el, - componentModel: mapOrGeoModel, - itemName: regionName, - // @ts-ignore FIXME:TS fix the "compatible with each other"? - itemTooltipOption: regionModel.get('tooltip') - }); - } - - let focus; - const stateTrigger = regionGraphic.stateTrigger; - if (stateTrigger) { - // @ts-ignore FIXME:TS fix the "compatible with each other"? - stateTrigger.highDownSilentOnTouch = !!mapOrGeoModel.get('selectedMode'); - // @ts-ignore FIXME:TS fix the "compatible with each other"? - const emphasisModel = regionModel.getModel('emphasis'); - focus = emphasisModel.get('focus'); - enableHoverEmphasis( - stateTrigger, focus, emphasisModel.get('blurScope') - ); - if (isGeo) { - enableComponentHighDownFeatures(stateTrigger, mapOrGeoModel as GeoModel, regionName); - } - } - - return focus; - } - remove(): void { this._regionsGroup.removeAll(); this._regionsGroupByName = null; @@ -509,7 +451,7 @@ class MapDraw { } } else if (geo.resourceType === 'geoSVG') { - return this._svgGraphicRecord.regionElementMap.get(name) || []; + return this._svgDispatcherMap && this._svgDispatcherMap.get(name) || []; } } @@ -538,6 +480,7 @@ class MapDraw { (resource as GeoSVGResource).freeGraphic(this.uid); } this._svgGraphicRecord = null; + this._svgDispatcherMap = null; this._svgGroup.removeAll(); this._svgMapName = null; } @@ -641,7 +584,7 @@ function labelTextAfterUpdate(this: graphic.Text) { function applyOptionStyleForRegion( viewBuildCtx: ViewBuildContext, - regionGraphic: RegionGraphic, + el: Displayable, dataIndex: number, regionModel: Model< GeoStyleableOption & { @@ -651,14 +594,6 @@ function applyOptionStyleForRegion( } > ): void { - - if ( - !regionGraphic.optionStyleEnabled - || !(regionGraphic.el instanceof Displayable) - ) { - return; - } - // All of the path are using `itemStyle`, becuase // (1) Some SVG also use fill on polyline (The different between // polyline and polygon is "open" or "close" but not fill or not). @@ -698,11 +633,185 @@ function applyOptionStyleForRegion( // SVG text, tspan and image can be named but not supporeted // to be styled by region option yet. - regionGraphic.el.setStyle(normalStyle); - regionGraphic.el.style.strokeNoScale = true; - regionGraphic.el.ensureState('emphasis').style = emphasisStyle; - regionGraphic.el.ensureState('select').style = selectStyle; - regionGraphic.el.ensureState('blur').style = blurStyle; + el.setStyle(normalStyle); + el.style.strokeNoScale = true; + el.ensureState('emphasis').style = emphasisStyle; + el.ensureState('select').style = selectStyle; + el.ensureState('blur').style = blurStyle; +} + +function resetLabelForRegion( + viewBuildCtx: ViewBuildContext, + el: Element, + regionName: string, + regionModel: RegionModel, + mapOrGeoModel: MapOrGeoModel, + // Exist only if `viewBuildCtx.data` exists. + dataIdx: number, + labelXY: number[] +): void { + const data = viewBuildCtx.data; + const isGeo = viewBuildCtx.isGeo; + + let showLabel = false; + for (let i = 0; i < DISPLAY_STATES.length; i++) { + const stateName = DISPLAY_STATES[i]; + // @ts-ignore FIXME:TS fix the "compatible with each other"? + if (regionModel.get( + stateName === 'normal' ? ['label', 'show'] : [stateName, 'label', 'show'] + )) { + showLabel = true; + break; + } + } + + const isDataNaN = data && isNaN(data.get(data.mapDimension('value'), dataIdx) as number); + const itemLayout = data && data.getItemLayout(dataIdx); + + // In the following cases label will be drawn + // 1. In map series and data value is NaN + // 2. In geo component + // 3. Region has no series legendSymbol, which will be add a showLabel flag in mapSymbolLayout + if ( + ((isGeo || isDataNaN) && showLabel) + || (itemLayout && itemLayout.showLabel) + ) { + const query = !isGeo ? dataIdx : regionName; + let labelFetcher; + + // Consider dataIdx not found. + if (!data || dataIdx >= 0) { + labelFetcher = mapOrGeoModel; + } + + const textEl = new graphic.Text({ + x: labelXY[0], + y: labelXY[1], + z2: 10, + silent: true + }); + textEl.afterUpdate = labelTextAfterUpdate; + + const labelStateModels = getLabelStatesModels(regionModel); + setLabelStyle( + textEl, + labelStateModels, + { + labelFetcher: labelFetcher, + labelDataIndex: query, + defaultText: regionName + }, + { normal: { + align: 'center', + verticalAlign: 'middle' + } } + ); + + // PENDING: use `setLabelStyle` entirely. + el.setTextContent(textEl); + + const textConfig = createTextConfig(labelStateModels.normal, null, false); + // Need to apply the `translate`. + textConfig.local = true; + textConfig.insideFill = textEl.style.fill; + el.setTextConfig(textConfig); + + for (let i = 0; i < SPECIAL_STATES.length; i++) { + const stateName = SPECIAL_STATES[i]; + // Hover label only work when `emphasis` state ensured. + const state = el.ensureState(stateName); + + const textConfig = state.textConfig = createTextConfig(labelStateModels[stateName], null, true); + // Need to apply the `translate`. + textConfig.local = true; + textConfig.insideFill = textEl.style.fill; + } + + (el as ECElement).disableLabelAnimation = true; + } + else { + el.removeTextContent(); + el.removeTextConfig(); + (el as ECElement).disableLabelAnimation = null; + } +} + +function resetEventTriggerForRegion( + viewBuildCtx: ViewBuildContext, + eventTrigger: Element, + regionName: string, + regionModel: RegionModel, + mapOrGeoModel: MapOrGeoModel, + // Exist only if `viewBuildCtx.data` exists. + dataIdx: number +): void { + // setItemGraphicEl, setHoverStyle after all polygons and labels + // are added to the rigionGroup + if (viewBuildCtx.data) { + // FIXME: when series-map use a SVG map, and there are duplicated name specified + // on different SVG elements, after `data.setItemGraphicEl(...)`: + // (1) all of them will be mounted with `dataIndex`, `seriesIndex`, so that tooltip + // can be triggered only mouse hover. That's correct. + // (2) only the last element will be kept in `data`, so that if trigger tooltip + // by `dispatchAction`, only the last one can be found and triggered. That might be + // not correct. We will fix it in future if anyone demanding that. + viewBuildCtx.data.setItemGraphicEl(dataIdx, eventTrigger); + } + // series-map will not trigger "geoselectchange" no matter it is + // based on a declared geo component. Becuause series-map will + // trigger "selectchange". If it trigger both the two events, + // If users call `chart.dispatchAction({type: 'toggleSelect'})`, + // it not easy to also fire event "geoselectchanged". + else { + // Package custom mouse event for geo component + getECData(eventTrigger).eventData = { + componentType: 'geo', + componentIndex: mapOrGeoModel.componentIndex, + geoIndex: mapOrGeoModel.componentIndex, + name: regionName, + region: (regionModel && regionModel.option) || {} + }; + } +} + +function resetTooltipForRegion( + viewBuildCtx: ViewBuildContext, + el: Element, + regionName: string, + regionModel: RegionModel, + mapOrGeoModel: MapOrGeoModel +): void { + if (!viewBuildCtx.data) { + graphic.setTooltipConfig({ + el: el, + componentModel: mapOrGeoModel, + itemName: regionName, + // @ts-ignore FIXME:TS fix the "compatible with each other"? + itemTooltipOption: regionModel.get('tooltip') + }); + } +} + +function resetStateTriggerForRegion( + viewBuildCtx: ViewBuildContext, + el: Element, + regionName: string, + regionModel: RegionModel, + mapOrGeoModel: MapOrGeoModel +): InnerFocus { + // @ts-ignore FIXME:TS fix the "compatible with each other"? + el.highDownSilentOnTouch = !!mapOrGeoModel.get('selectedMode'); + // @ts-ignore FIXME:TS fix the "compatible with each other"? + const emphasisModel = regionModel.getModel('emphasis'); + const focus = emphasisModel.get('focus'); + enableHoverEmphasis( + el, focus, emphasisModel.get('blurScope') + ); + if (viewBuildCtx.isGeo) { + enableComponentHighDownFeatures(el, mapOrGeoModel as GeoModel, regionName); + } + + return focus; } export default MapDraw; diff --git a/src/coord/geo/Geo.ts b/src/coord/geo/Geo.ts index 430bdf481..a2e96cc2e 100644 --- a/src/coord/geo/Geo.ts +++ b/src/coord/geo/Geo.ts @@ -169,7 +169,7 @@ class Geo extends View { return this._nameCoordMap.get(name) || (region && region.getCenter()); } - dataToPoint(data: number[], noRoam?: boolean, out?: number[]): number[] { + dataToPoint(data: number[] | string, noRoam?: boolean, out?: number[]): number[] { if (typeof data === 'string') { // Map area name to geoCoord data = this.getGeoCoord(data); diff --git a/src/coord/geo/GeoSVGResource.ts b/src/coord/geo/GeoSVGResource.ts index 79455c3f1..93d9c2f52 100644 --- a/src/coord/geo/GeoSVGResource.ts +++ b/src/coord/geo/GeoSVGResource.ts @@ -17,38 +17,54 @@ * under the License. */ -import { parseSVG, makeViewBoxTransform, SVGNodeTagLower } from 'zrender/src/tool/parseSVG'; +import { parseSVG, makeViewBoxTransform, SVGNodeTagLower, SVGParserResultNamedItem } from 'zrender/src/tool/parseSVG'; import Group from 'zrender/src/graphic/Group'; import Rect from 'zrender/src/graphic/shape/Rect'; -import {assert, createHashMap, HashMap} from 'zrender/src/core/util'; +import {assert, createHashMap, each, HashMap} from 'zrender/src/core/util'; import BoundingRect from 'zrender/src/core/BoundingRect'; -import { GeoResource, GeoSVGGraphicRoot, GeoSVGSourceInput, RegionGraphic } from './geoTypes'; +import { GeoResource, GeoSVGGraphicRoot, GeoSVGSourceInput } from './geoTypes'; import { parseXML } from 'zrender/src/tool/parseXML'; import { GeoSVGRegion } from './Region'; import Element from 'zrender/src/Element'; -type RegionName = string; export interface GeoSVGGraphicRecord { root: Group; boundingRect: BoundingRect; - regionGraphics: RegionGraphic[]; - // A name may correspond to multiple graphics. - regionElementMap: HashMap; + named: SVGParserResultNamedItem[]; } +/** + * "region available" means that: enable users to set attribute `name="xxx"` on those tags + * to make it be a region. + * 1. region styles and its label styles can be defined in echarts opton: + * ```js + * geo: { + * regions: [{ + * name: 'xxx', + * itemStyle: { ... }, + * label: { ... } + * }, { + * ... + * }, + * ...] + * }; + * ``` + * 2. name can be duplicated in different SVG tag. All of the tags with the same name share + * a region option. For exampel if there are two representing two lung lobes. They have + * no common parents but both of them need to display label "lung" inside. + */ const REGION_AVAILABLE_SVG_TAG_MAP = createHashMap([ - 'rect', 'circle', 'line', 'ellipse', 'polygon', 'polyline', 'text', 'tspan', 'path' + 'rect', 'circle', 'line', 'ellipse', 'polygon', 'polyline', 'path', + // are also enabled becuase some SVG might paint text itself, + // but still need to trigger events or tooltip. + 'text', 'tspan', + // is also enabled because this case: if multiple tags share one name + // and need label displayed, every tags will display the name, which is not + // expected. So we can put them into a . Thereby only one label + // displayed and located based on the bounding rect of the . + 'g' ]); -const OPTION_STYLE_ENABLED_TAG_MAP = createHashMap([ - 'rect', 'circle', 'line', 'ellipse', 'polygon', 'polyline', 'path' -]); - -const LABEL_HOST_MAP = createHashMap([ - 'rect', 'circle', 'line', 'ellipse', 'polygon', 'polyline', 'path' -]); - - export class GeoSVGResource implements GeoResource { readonly type = 'geoSVG'; @@ -57,9 +73,9 @@ export class GeoSVGResource implements GeoResource { private _firstGraphic: GeoSVGGraphicRecord; private _boundingRect: BoundingRect; - private _regions: GeoSVGRegion[] = []; + private _regions: GeoSVGRegion[]; // Key: region.name - private _regionsMap: HashMap = createHashMap(); + private _regionsMap: HashMap; // All used graphics. key: hostKey, value: root private _usedGraphicMap: HashMap = createHashMap(); @@ -98,21 +114,14 @@ export class GeoSVGResource implements GeoResource { this._boundingRect = this._firstGraphic.boundingRect.clone(); - const regionGraphics = firstGraphic.regionGraphics; // PENDING: `nameMap` will not be supported until some real requirement come. // if (nameMap) { - // regionGraphics = applyNameMap(regionGraphics, nameMap); + // named = applyNameMap(named, nameMap); // } - // Create resions only for the first graphic. - for (let i = 0; i < regionGraphics.length; i++) { - const regionGraphic = regionGraphics[i]; - const region = new GeoSVGRegion(regionGraphic.name, regionGraphic.el); - // PENDING: if `nameMap` supported, this region can not be mounted on - // `this`, but can only be created each time `load()` called. - this._regions.push(region); - this._regionsMap.set(regionGraphic.name, region); - } + const { regions, regionsMap } = createRegions(firstGraphic.named); + this._regions = regions; + this._regionsMap = regionsMap; } return { @@ -148,10 +157,10 @@ export class GeoSVGResource implements GeoResource { // PENDING: `nameMap` will not be supported until some real requirement come. // `nameMap` can only be obtained from echarts option. - // The original `regionGraphics` must not be modified. + // The original `named` must not be modified. // if (nameMap) { // svgGraphic = extend({}, svgGraphic); - // svgGraphic.regionGraphics = applyNameMap(svgGraphic.regionGraphics, nameMap); + // svgGraphic.named = applyNameMap(svgGraphic.named, nameMap); // } return svgGraphic; @@ -227,59 +236,75 @@ function buildGraphic( (root as GeoSVGGraphicRoot).isGeoSVGGraphicRoot = true; - const regionGraphics = [] as GeoSVGGraphicRecord['regionGraphics']; - const regionElementMap = createHashMap(); - const named = result.named; - for (let i = 0; i < named.length; i++) { - const namedItem = named[i]; - const svgNodeTagLower = namedItem.svgNodeTagLower; - - if (REGION_AVAILABLE_SVG_TAG_MAP.get(svgNodeTagLower) != null) { - const optionStyleEnabled = OPTION_STYLE_ENABLED_TAG_MAP.get(svgNodeTagLower); - const el = namedItem.el; - const name = namedItem.name; - - regionGraphics.push({ - name: name, - el: el, - optionStyleEnabled: optionStyleEnabled != null, - stateTrigger: optionStyleEnabled != null ? el : null, - // text/tspan/image do not suport style but support event. - eventTrigger: el, - useLabel: LABEL_HOST_MAP.get(svgNodeTagLower) != null - }); - - const els = regionElementMap.get(name) || regionElementMap.set(name, []); - els.push(el); - - // Only named element has silent: false, other elements should - // act as background and has no user interaction. - el.silent = false; - // text|tspan will be converted to group. - if (el.isGroup) { - el.traverse(child => { - child.silent = false; - }); - } + const named = [] as GeoSVGGraphicRecord['named']; + + each(result.named, namedItem => { + if (REGION_AVAILABLE_SVG_TAG_MAP.get(namedItem.svgNodeTagLower) != null) { + named.push(namedItem); + setSilent(namedItem.el); } + }); + + return { root, boundingRect, named }; +} + +function setSilent(el: Element): void { + // Only named element has silent: false, other elements should + // act as background and has no user interaction. + el.silent = false; + // text|tspan will be converted to group. + if (el.isGroup) { + el.traverse(child => { + child.silent = false; + }); } +} + +function createRegions( + named: SVGParserResultNamedItem[] +): { + regions: GeoSVGRegion[]; + regionsMap: HashMap; +} { + + const regions: GeoSVGRegion[] = []; + const regionsMap = createHashMap(); + + // Create resions only for the first graphic. + each(named, namedItem => { + // Region has feature to calculate center for tooltip or other features. + // If there is a , the center should be the center of the + // bounding rect of the g. + if (namedItem.namedFrom != null) { + return; + } - return { root, boundingRect, regionGraphics, regionElementMap: regionElementMap }; + const region = new GeoSVGRegion(namedItem.name, namedItem.el); + // PENDING: if `nameMap` supported, this region can not be mounted on + // `this`, but can only be created each time `load()` called. + regions.push(region); + // PENDING: if multiple tag named with the same name, only one will be + // found by `_regionsMap`. `_regionsMap` is used to find a coordinate + // by name. We use `region.getCenter()` as the coordinate. + regionsMap.set(namedItem.name, region); + }); + + return { regions, regionsMap }; } // PENDING: `nameMap` will not be supported until some real requirement come. // /** // * Use the alias in geoNameMap. -// * The input `regionGraphics` must not be modified. +// * The input `named` must not be modified. // */ // function applyNameMap( -// regionGraphics: GeoSVGGraphicRecord['regionGraphics'], +// named: GeoSVGGraphicRecord['named'], // nameMap: NameMap -// ): GeoSVGGraphicRecord['regionGraphics'] { -// const result = [] as GeoSVGGraphicRecord['regionGraphics']; -// for (let i = 0; i < regionGraphics.length; i++) { -// let regionGraphic = regionGraphics[i]; +// ): GeoSVGGraphicRecord['named'] { +// const result = [] as GeoSVGGraphicRecord['named']; +// for (let i = 0; i < named.length; i++) { +// let regionGraphic = named[i]; // const name = regionGraphic.name; // if (nameMap && nameMap.hasOwnProperty(name)) { // regionGraphic = extend({}, regionGraphic); diff --git a/src/coord/geo/geoTypes.ts b/src/coord/geo/geoTypes.ts index 86742186a..e98de4946 100644 --- a/src/coord/geo/geoTypes.ts +++ b/src/coord/geo/geoTypes.ts @@ -140,21 +140,3 @@ export interface GeoResource { export interface GeoSVGGraphicRoot extends Group { isGeoSVGGraphicRoot: boolean; } - -export type RegionGraphic = { - // Region name. Can not be null/undefined. - name: string; - // Main el. - el: Element; - // If it specified, use it to trigger state - // style change (emphasis/select/blur) - // Can be null/undefined. - stateTrigger: Element; - // If it specified, use it to trigger event to users - // Can be null/undefined. - eventTrigger: Element; - // Whether to set label on `el.textContent`. - useLabel: boolean; - // Whether to be enabled to set style via in echarts option. - optionStyleEnabled: boolean; -}; diff --git a/src/core/echarts.ts b/src/core/echarts.ts index 2f652a332..10fc41b24 100644 --- a/src/core/echarts.ts +++ b/src/core/echarts.ts @@ -36,8 +36,6 @@ import ChartView, {ChartViewConstructor} from '../view/Chart'; import * as graphic from '../util/graphic'; import {getECData} from '../util/innerStore'; import { - enterEmphasisWhenMouseOver, - leaveEmphasisWhenMouseOut, isHighDownDispatcher, HOVER_STATE_EMPHASIS, HOVER_STATE_BLUR, @@ -1484,24 +1482,28 @@ class ECharts extends Eventful { ecModel && ecModel.eachComponent(condition, function (model) { if (!excludeSeriesIdMap || excludeSeriesIdMap.get(model.id) == null) { if (isHighDownPayload(payload)) { - if (payload.type === HIGHLIGHT_ACTION_TYPE) { - if (model instanceof SeriesModel) { - !payload.notBlur && blurSeriesFromHighlightPayload(model, payload, ecIns._api); + if (model instanceof SeriesModel) { + if (payload.type === HIGHLIGHT_ACTION_TYPE && !payload.notBlur) { + blurSeriesFromHighlightPayload(model, payload, ecIns._api); + } + } + else { + const { focusSelf, dispatchers } = findComponentHighDownDispatchers( + model.mainType, model.componentIndex, payload.name, ecIns._api + ); + if (payload.type === HIGHLIGHT_ACTION_TYPE && focusSelf && !payload.notBlur) { + blurComponent(model.mainType, model.componentIndex, ecIns._api); } - else { - const { focusSelf, dispatchers } = findComponentHighDownDispatchers( - model.mainType, model.componentIndex, payload.name, ecIns._api - ); - if (focusSelf && !payload.notBlur) { - blurComponent(model.mainType, model.componentIndex, ecIns._api); - } - // PENDING: - // Whether to put this "enter emphasis" code in `ComponentView`, - // which will be the same as `ChartView` but might be not necessary - // and will be far from this logic. - if (dispatchers) { - each(dispatchers, dispatcher => enterEmphasis(dispatcher)); - } + // PENDING: + // Whether to put this "enter emphasis" code in `ComponentView`, + // which will be the same as `ChartView` but might be not necessary + // and will be far from this logic. + if (dispatchers) { + each(dispatchers, dispatcher => { + payload.type === HIGHLIGHT_ACTION_TYPE + ? enterEmphasis(dispatcher) + : leaveEmphasis(dispatcher); + }); } } } diff --git a/test/geo-svg-demo.html b/test/geo-svg-demo.html index ed5afe8af..85ad1b088 100644 --- a/test/geo-svg-demo.html +++ b/test/geo-svg-demo.html @@ -38,8 +38,8 @@ under the License.
- - +
+
@@ -99,6 +99,8 @@ under the License. tooltip: { }, geo: { + left: 10, + right: '50%', map: 'seatmap', roam: true, selectedMode: 'multiple', @@ -107,18 +109,16 @@ under the License. emphasis: { focus: 'self', itemStyle: { - // color: null + color: null }, label: { - show: false, + position: 'bottom', + distance: 20, textBorderColor: '#fff', textBorderWidth: 2 } }, blur: { - // itemStyle: { - // opacity: 0.3 - // } }, select: { itemStyle: { @@ -130,7 +130,25 @@ under the License. textBorderWidth: 2 } } - } + }, + grid: { + left: '60%' + }, + xAxis: { + splitLine: { + show: false + } + }, + yAxis: { + data: ['heart', 'large-intestine', 'small-intestine', 'spleen', 'kidney', 'lung', 'liver'] + }, + series: [{ + type: 'bar', + emphasis: { + focus: 'self' + }, + data: [121, 321, 141, 52, 198, 289, 139] + }] }; var chart = testHelper.create(echarts, 'main_geo_svg_organ', { @@ -149,9 +167,34 @@ under the License. listenAndPrintEvent(chart); if (chart) { - chart.on('highlight', function () { - console.log('agsd'); + chart.on('mouseover', { seriesIndex: 0 }, function (event) { + chart.dispatchAction({ + type: 'highlight', + geoIndex: 0, + name: event.name + }); + }); + chart.on('mouseout', { seriesIndex: 0 }, function (event) { + chart.dispatchAction({ + type: 'downplay', + geoIndex: 0, + name: event.name + }); }); + // chart.on('mouseover', { geoIndex: 0 }, function (event) { + // chart.dispatchAction({ + // type: 'highlight', + // seriesIndex: 0, + // name: event.name + // }); + // }); + // chart.on('mouseout', { geoIndex: 0 }, function (event) { + // chart.dispatchAction({ + // type: 'downplay', + // seriesIndex: 0, + // name: event.name + // }); + // }); } }); @@ -162,36 +205,54 @@ under the License. - + + + + diff --git a/test/geo-svg.html b/test/geo-svg.html index 2d52f330d..45ec6e426 100644 --- a/test/geo-svg.html +++ b/test/geo-svg.html @@ -39,10 +39,10 @@ under the License.
+
-
@@ -135,7 +135,7 @@ under the License. option: option, info: {}, infoKey: 'event', - height: 300 + height: 200 }); listenAndPrintEvent(chart); @@ -187,7 +187,7 @@ under the License. option: option, info: {}, infoKey: 'event', - height: 300 + height: 200 }); listenAndPrintEvent(chart); @@ -199,6 +199,129 @@ under the License. + + + + + + -- GitLab