You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@echarts.apache.org by su...@apache.org on 2021/03/21 14:40:32 UTC

[echarts] 04/04: fix: [geo] (1) fix contain point calculation. (2) remove Geo['_nameCoordMap'], instead, calculate center on demand. (3) fix that some API missing nameProperty and geoJSON should cached not only by mapName but also by nameProperty. (4) do not support nameMap in geoSVG until some real requirements come. (5) svg line polyline use 'region.lineStyle' rather than 'region.itemStyle' (6) svg text tspan image are enabled to trigger tooltip and user event (but not supported to change state and style yet).

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

sushuang pushed a commit to branch fix/geo-svg
in repository https://gitbox.apache.org/repos/asf/echarts.git

commit 152ab8ec311e096110be2874a259c1d573d24698
Author: 100pah <su...@gmail.com>
AuthorDate: Sun Mar 21 22:37:59 2021 +0800

    fix: [geo]
    (1) fix contain point calculation.
    (2) remove Geo['_nameCoordMap'], instead, calculate center on demand.
    (3) fix that some API missing nameProperty and geoJSON should cached not only by mapName but also by nameProperty.
    (4) do not support nameMap in geoSVG until some real requirements come.
    (5) svg line polyline use 'region.lineStyle' rather than 'region.itemStyle'
    (6) svg text tspan image are enabled to trigger tooltip and user event (but not supported to change state and style yet).
---
 src/component/helper/MapDraw.ts   | 216 +++++++++++++++++++++++++++-----------
 src/coord/View.ts                 |   2 +
 src/coord/geo/Geo.ts              |  30 +++---
 src/coord/geo/GeoJSONResource.ts  |  23 ++--
 src/coord/geo/GeoModel.ts         |  12 ++-
 src/coord/geo/GeoSVGResource.ts   | 158 +++++++++++++++++++++-------
 src/coord/geo/geoCreator.ts       |   9 +-
 src/coord/geo/geoSourceManager.ts |   2 +-
 src/coord/geo/geoTypes.ts         |  28 ++++-
 src/data/OrdinalMeta.ts           |   2 +-
 src/model/mixin/lineStyle.ts      |   2 +-
 test/geo-svg.html                 | 112 ++++++++++++++------
 12 files changed, 425 insertions(+), 171 deletions(-)

diff --git a/src/component/helper/MapDraw.ts b/src/component/helper/MapDraw.ts
index 8be9155..738edb5 100644
--- a/src/component/helper/MapDraw.ts
+++ b/src/component/helper/MapDraw.ts
@@ -29,7 +29,7 @@ import ExtensionAPI from '../../core/ExtensionAPI';
 import GeoModel, { GeoCommonOptionMixin, GeoItemStyleOption } from '../../coord/geo/GeoModel';
 import MapSeries from '../../chart/map/MapSeries';
 import GlobalModel from '../../model/Global';
-import { Payload, ECElement } from '../../util/types';
+import { Payload, ECElement, LineStyleOption } from '../../util/types';
 import GeoView from '../geo/GeoView';
 import MapView from '../../chart/map/MapView';
 import Geo from '../../coord/geo/Geo';
@@ -38,11 +38,14 @@ import { setLabelStyle, getLabelStatesModels } from '../../label/labelStyle';
 import { getECData } from '../../util/innerStore';
 import { createOrUpdatePatternFromDecal } from '../../util/decal';
 import { ViewCoordSysTransformInfoPart } from '../../coord/View';
-import { GeoSVGResource } from '../../coord/geo/GeoSVGResource';
+import { GeoSVGGraphicRecord, GeoSVGResource } from '../../coord/geo/GeoSVGResource';
 import Displayable from 'zrender/src/graphic/Displayable';
-import Element, { ElementTextConfig } from 'zrender/src/Element';
+import { 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';
 
 
 interface RegionsGroup extends graphic.Group {
@@ -58,6 +61,11 @@ interface ViewBuildContext {
     transformInfoRaw: ViewCoordSysTransformInfoPart;
 }
 
+interface GeoStyleableOption {
+    itemStyle?: GeoItemStyleOption;
+    lineStyle?: LineStyleOption;
+}
+
 function getFixedItemStyle(model: Model<GeoItemStyleOption>) {
     const itemStyle = model.getItemStyle();
     const areaColor = model.get('areaColor');
@@ -98,7 +106,7 @@ class MapDraw {
 
     private _svgGroup: graphic.Group;
 
-    private _svgRegionElements: Displayable[];
+    private _svgRegionGraphics: GeoSVGGraphicRecord['regionGraphics'];
 
 
     constructor(api: ExtensionAPI) {
@@ -244,10 +252,15 @@ class MapDraw {
             });
 
             const centerPt = transformPoint(region.getCenter());
-
-            this._resetSingleRegionGraphic(
-                viewBuildCtx, compoundPath, regionGroup, region.name, centerPt, null
-            );
+            const regionGraphic: RegionGraphic = {
+                name: region.name,
+                el: compoundPath,
+                styleOptionKey: 'itemStyle',
+                stateTrigger: regionGroup,
+                eventTrigger: regionGroup,
+                useLabel: true
+            };
+            this._resetSingleRegionGraphic(viewBuildCtx, regionGraphic, centerPt, null, false);
 
             regionsGroup.add(regionGroup);
 
@@ -268,76 +281,72 @@ class MapDraw {
             this._useSVG(mapName);
         }
 
-        zrUtil.each(this._svgRegionElements, function (el: Displayable) {
+        zrUtil.each(this._svgRegionGraphics, function (regionGraphic) {
             // 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.
             this._resetSingleRegionGraphic(
-                viewBuildCtx, el, el, el.name, [0, 0], 'inside'
+                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
             );
         }, this);
     }
 
     private _resetSingleRegionGraphic(
         viewBuildCtx: ViewBuildContext,
-        displayable: Displayable,
-        elForStateChange: Element,
-        regionName: string,
+        regionGraphic: RegionGraphic,
         labelXY: number[],
-        labelPosition: ElementTextConfig['position']
+        labelPosition: ElementTextConfig['position'],
+        noZ2EmphasisLift: boolean
     ): void {
 
+        const regionName = regionGraphic.name;
         const mapOrGeoModel = viewBuildCtx.mapOrGeoModel;
         const data = viewBuildCtx.data;
         const isVisualEncodedByVisualMap = viewBuildCtx.isVisualEncodedByVisualMap;
         const isGeo = viewBuildCtx.isGeo;
 
-        const regionModel = mapOrGeoModel.getRegionModel(regionName) || mapOrGeoModel;
-
-        // @ts-ignore FIXME:TS fix the "compatible with each other"?
-        const itemStyleModel = regionModel.getModel('itemStyle');
-        // @ts-ignore FIXME:TS fix the "compatible with each other"?
-        const emphasisModel = regionModel.getModel('emphasis');
-        const emphasisItemStyleModel = emphasisModel.getModel('itemStyle');
-        // @ts-ignore FIXME:TS fix the "compatible with each other"?
-        const blurItemStyleModel = regionModel.getModel(['blur', 'itemStyle']);
-        // @ts-ignore FIXME:TS fix the "compatible with each other"?
-        const selectItemStyleModel = regionModel.getModel(['select', 'itemStyle']);
-
-        // NOTE: DONT use 'style' in visual when drawing map.
-        // This component is used for drawing underlying map for both geo component and map series.
-        const itemStyle = getFixedItemStyle(itemStyleModel);
-        const emphasisItemStyle = getFixedItemStyle(emphasisItemStyleModel);
-        const blurItemStyle = getFixedItemStyle(blurItemStyleModel);
-        const selectItemStyle = getFixedItemStyle(selectItemStyleModel);
-
-        let dataIdx;
+        const dataIdx = data ? data.indexOfName(regionName) : null;
+        const regionModel = mapOrGeoModel.getRegionModel(regionName);
+        const styles = makeStyleForRegion(regionGraphic.styleOptionKey, regionModel);
+
         // Use the itemStyle in data if has data
-        if (data) {
-            dataIdx = data.indexOfName(regionName);
+        if (styles && styles.styleOptionKey === 'itemStyle' && data) {
             // Only visual color of each item will be used. It can be encoded by visualMap
             // But visual color of series is used in symbol drawing
-            //
+
             // Visual color for each series is for the symbol draw
             const style = data.getItemVisual(dataIdx, 'style');
             const decal = data.getItemVisual(dataIdx, 'decal');
             if (isVisualEncodedByVisualMap && style.fill) {
-                itemStyle.fill = style.fill;
+                styles.normal.fill = style.fill;
             }
             if (decal) {
-                itemStyle.decal = createOrUpdatePatternFromDecal(decal, viewBuildCtx.api);
+                styles.normal.decal = createOrUpdatePatternFromDecal(decal, viewBuildCtx.api);
             }
         }
 
-        displayable.setStyle(itemStyle);
-        displayable.style.strokeNoScale = true;
-        displayable.culling = true;
+        // PENDING: SVG text, tspan and image can be named but not supporeted
+        // to be styled by region option yet.
+        if (styles && regionGraphic.el instanceof graphic.Path) {
+            regionGraphic.el.setStyle(styles.normal);
+            regionGraphic.el.style.strokeNoScale = true;
+            regionGraphic.el.ensureState('emphasis').style = styles.emphasis;
+            regionGraphic.el.ensureState('select').style = styles.select;
+            regionGraphic.el.ensureState('blur').style = styles.blur;
+        }
 
-        displayable.ensureState('emphasis').style = emphasisItemStyle;
-        displayable.ensureState('blur').style = blurItemStyle;
-        displayable.ensureState('select').style = selectItemStyle;
+        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++) {
@@ -357,10 +366,13 @@ class MapDraw {
         // In the following cases label will be drawn
         // 1. In map series and data value is NaN
         // 2. In geo component
-        // 4. Region has no series legendSymbol, which will be add a showLabel flag in mapSymbolLayout
+        // 3. Region has no series legendSymbol, which will be add a showLabel flag in mapSymbolLayout
         if (
-            (isGeo || isDataNaN && (showLabel))
-            || (itemLayout && itemLayout.showLabel)
+            regionGraphic.useLabel
+            && (
+                ((isGeo || isDataNaN) && showLabel)
+                || (itemLayout && itemLayout.showLabel)
+            )
         ) {
             const query = !isGeo ? dataIdx : regionName;
             let labelFetcher;
@@ -391,21 +403,31 @@ class MapDraw {
                 } }
             );
 
-            displayable.setTextContent(textEl);
-            displayable.setTextConfig({
+            regionGraphic.el.setTextContent(textEl);
+            regionGraphic.el.setTextConfig({
                 local: true,
                 insideFill: textEl.style.fill,
                 position: labelPosition
             });
-
-            (displayable as ECElement).disableLabelAnimation = true;
+            (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) {
-            data.setItemGraphicEl(dataIdx, elForStateChange);
+            // 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
@@ -413,9 +435,8 @@ class MapDraw {
         // If users call `chart.dispatchAction({type: 'toggleSelect'})`,
         // it not easy to also fire event "geoselectchanged".
         else {
-            const regionModel = mapOrGeoModel.getRegionModel(regionName);
             // Package custom mouse event for geo component
-            getECData(displayable).eventData = {
+            getECData(regionGraphic.eventTrigger).eventData = {
                 componentType: 'geo',
                 componentIndex: mapOrGeoModel.componentIndex,
                 geoIndex: mapOrGeoModel.componentIndex,
@@ -424,10 +445,25 @@ class MapDraw {
             };
         }
 
-        // @ts-ignore FIXME:TS fix the "compatible with each other"?
-        elForStateChange.highDownSilentOnTouch = !!mapOrGeoModel.get('selectedMode');
-        enableHoverEmphasis(elForStateChange, emphasisModel.get('focus'), emphasisModel.get('blurScope'));
+        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')
+            });
+        }
 
+        if (regionGraphic.stateTrigger) {
+            // @ts-ignore FIXME:TS fix the "compatible with each other"?
+            regionGraphic.stateTrigger.highDownSilentOnTouch = !!mapOrGeoModel.get('selectedMode');
+            // @ts-ignore FIXME:TS fix the "compatible with each other"?
+            const emphasisModel = regionModel.getModel('emphasis');
+            enableHoverEmphasis(
+                regionGraphic.stateTrigger, emphasisModel.get('focus'), emphasisModel.get('blurScope')
+            );
+        }
     }
 
     remove(): void {
@@ -447,7 +483,7 @@ class MapDraw {
         if (resource && resource.type === 'geoSVG') {
             const svgGraphic = (resource as GeoSVGResource).useGraphic(this.uid);
             this._svgGroup.add(svgGraphic.root);
-            this._svgRegionElements = svgGraphic.regionElements;
+            this._svgRegionGraphics = svgGraphic.regionGraphics;
             this._svgMapName = mapName;
         }
     }
@@ -461,7 +497,7 @@ class MapDraw {
         if (resource && resource.type === 'geoSVG') {
             (resource as GeoSVGResource).freeGraphic(this.uid);
         }
-        this._svgRegionElements = null;
+        this._svgRegionGraphics = null;
         this._svgGroup.removeAll();
         this._svgMapName = null;
     }
@@ -516,7 +552,7 @@ class MapDraw {
         }, this);
 
         controller.setPointerChecker(function (e, x, y) {
-            return geo.getViewRectAfterRoam().contain(x, y)
+            return geo.containPoint([x, y])
                 && !onIrrelevantElement(e, api, mapOrGeoModel);
         });
     }
@@ -563,4 +599,60 @@ function labelTextAfterUpdate(this: graphic.Text) {
     m[3] /= scaleY;
 }
 
+function makeStyleForRegion(
+    styleOptionKey: RegionGraphic['styleOptionKey'],
+    regionModel: Model<
+        GeoStyleableOption & {
+            emphasis?: GeoStyleableOption;
+            select?: GeoStyleableOption;
+            blur?: GeoStyleableOption;
+        }
+    >
+): {
+    styleOptionKey: 'itemStyle';
+    normal: ItemStyleProps;
+    emphasis: ItemStyleProps;
+    select: ItemStyleProps;
+    blur: ItemStyleProps;
+} | {
+    styleOptionKey: 'lineStyle';
+    normal: LineStyleProps;
+    emphasis: LineStyleProps;
+    select: LineStyleProps;
+    blur: LineStyleProps;
+} {
+    if (!styleOptionKey) {
+        return;
+    }
+
+    const normalStyleModel = regionModel.getModel(styleOptionKey);
+    const emphasisStyleModel = regionModel.getModel(['emphasis', styleOptionKey]);
+    const blurStyleModel = regionModel.getModel(['blur', styleOptionKey]);
+    const selectStyleModel = regionModel.getModel(['select', styleOptionKey]);
+
+    // NOTE: DONT use 'style' in visual when drawing map.
+    // This component is used for drawing underlying map for both geo component and map series.
+    if (styleOptionKey === 'itemStyle') {
+        return {
+            styleOptionKey,
+            normal: getFixedItemStyle(normalStyleModel),
+            emphasis: getFixedItemStyle(emphasisStyleModel),
+            select: getFixedItemStyle(selectStyleModel),
+            blur: getFixedItemStyle(blurStyleModel)
+        };
+    }
+    else if (styleOptionKey === 'lineStyle') {
+        return {
+            styleOptionKey,
+            normal: normalStyleModel.getLineStyle(),
+            emphasis: emphasisStyleModel.getLineStyle(),
+            select: selectStyleModel.getLineStyle(),
+            blur: blurStyleModel.getLineStyle()
+        };
+    }
+}
+
 export default MapDraw;
+
+
+// @ts-ignore FIXME:TS fix the "compatible with each other"?
diff --git a/src/coord/View.ts b/src/coord/View.ts
index 93fa411..f6a68f9 100644
--- a/src/coord/View.ts
+++ b/src/coord/View.ts
@@ -50,6 +50,8 @@ class View extends Transformable implements CoordinateSystemMaster, CoordinateSy
 
     /**
      * Represents the transform brought by roam/zoom.
+     * If `View['_viewRect']` applies roam transform,
+     * we can get the final displayed rect.
      */
     private _roamTransformable = new Transformable();
     /**
diff --git a/src/coord/geo/Geo.ts b/src/coord/geo/Geo.ts
index 1f29cc5..430bdf4 100644
--- a/src/coord/geo/Geo.ts
+++ b/src/coord/geo/Geo.ts
@@ -55,6 +55,7 @@ class Geo extends View {
     readonly map: string;
     readonly resourceType: GeoResource['type'];
 
+    // Only store specified name coord via `addGeoCoord`.
     private _nameCoordMap: zrUtil.HashMap<number[]>;
     private _regionsMap: zrUtil.HashMap<Region>;
     private _invertLongitute: boolean;
@@ -71,6 +72,7 @@ class Geo extends View {
         opt: {
             // Specify name alias
             nameMap?: NameMap;
+            nameProperty?: string;
             aspectScale?: number;
         }
     ) {
@@ -78,13 +80,12 @@ class Geo extends View {
 
         this.map = map;
 
-        const source = geoSourceManager.load(map, opt.nameMap);
+        const source = geoSourceManager.load(map, opt.nameMap, opt.nameProperty);
         const resource = geoSourceManager.getGeoResource(map);
         this.resourceType = resource ? resource.type : null;
 
         const defaultParmas = GEO_DEFAULT_PARAMS[resource.type];
 
-        this._nameCoordMap = source.nameCoordMap;
         this._regionsMap = source.regionsMap;
         this._invertLongitute = defaultParmas.invertLongitute;
         this.regions = source.regions;
@@ -97,16 +98,17 @@ class Geo extends View {
     /**
      * Whether contain the given [lng, lat] coord.
      */
-    containCoord(coord: number[]) {
-        const regions = this.regions;
-        for (let i = 0; i < regions.length; i++) {
-            const region = regions[i];
-            if (region.type === 'geoJSON' && (region as GeoJSONRegion).contain(coord)) {
-                return true;
-            }
-        }
-        return false;
-    }
+    // Never used yet.
+    // containCoord(coord: number[]) {
+    //     const regions = this.regions;
+    //     for (let i = 0; i < regions.length; i++) {
+    //         const region = regions[i];
+    //         if (region.type === 'geoJSON' && (region as GeoJSONRegion).contain(coord)) {
+    //             return true;
+    //         }
+    //     }
+    //     return false;
+    // }
 
     protected _transformTo(x: number, y: number, width: number, height: number): void {
         let rect = this.getBoundingRect();
@@ -162,7 +164,9 @@ class Geo extends View {
      * Get geoCoord by name
      */
     getGeoCoord(name: string): number[] {
-        return this._nameCoordMap.get(name);
+        const region = this._regionsMap.get(name);
+        // calcualte center only on demand.
+        return this._nameCoordMap.get(name) || (region && region.getCenter());
     }
 
     dataToPoint(data: number[], noRoam?: boolean, out?: number[]): number[] {
diff --git a/src/coord/geo/GeoJSONResource.ts b/src/coord/geo/GeoJSONResource.ts
index e5aea61..9a5fbb4 100644
--- a/src/coord/geo/GeoJSONResource.ts
+++ b/src/coord/geo/GeoJSONResource.ts
@@ -30,6 +30,8 @@ import { GeoJSONRegion } from './Region';
 import { GeoJSON, GeoJSONCompressed, GeoJSONSourceInput, GeoResource, GeoSpecialAreas, NameMap } from './geoTypes';
 
 
+const DEFAULT_NAME_PROPERTY = 'name' as const;
+
 export class GeoJSONResource implements GeoResource {
 
     readonly type = 'geoJSON';
@@ -37,10 +39,10 @@ export class GeoJSONResource implements GeoResource {
     private _specialAreas: GeoSpecialAreas;
     private _mapName: string;
 
-    private _parsed: {
+    private _parsedMap = createHashMap<{
         regions: GeoJSONRegion[];
         boundingRect: BoundingRect;
-    };
+    }, string>();
 
     constructor(
         mapName: string,
@@ -54,19 +56,24 @@ export class GeoJSONResource implements GeoResource {
         this._geoJSON = parseInput(geoJSON);
     }
 
+    /**
+     * @param nameMap can be null/undefined
+     * @param nameProperty can be null/undefined
+     */
     load(nameMap: NameMap, nameProperty: string) {
 
-        let parsed = this._parsed;
+        nameProperty = nameProperty || DEFAULT_NAME_PROPERTY;
+
+        let parsed = this._parsedMap.get(nameProperty);
         if (!parsed) {
             const rawRegions = this._parseToRegions(nameProperty);
-            parsed = this._parsed = {
+            parsed = this._parsedMap.set(nameProperty, {
                 regions: rawRegions,
                 boundingRect: calculateBoundingRect(rawRegions)
-            };
+            });
         }
 
         const regionsMap = createHashMap<GeoJSONRegion>();
-        const nameCoordMap = createHashMap<ReturnType<GeoJSONRegion['getCenter']>>();
 
         const finalRegions: GeoJSONRegion[] = [];
         each(parsed.regions, function (region) {
@@ -79,14 +86,12 @@ export class GeoJSONResource implements GeoResource {
 
             finalRegions.push(region);
             regionsMap.set(regionName, region);
-            nameCoordMap.set(regionName, region.getCenter());
         });
 
         return {
             regions: finalRegions,
             boundingRect: parsed.boundingRect || new BoundingRect(0, 0, 0, 0),
-            regionsMap: regionsMap,
-            nameCoordMap: nameCoordMap
+            regionsMap: regionsMap
         };
     }
 
diff --git a/src/coord/geo/GeoModel.ts b/src/coord/geo/GeoModel.ts
index d18a249..96d14c3 100644
--- a/src/coord/geo/GeoModel.ts
+++ b/src/coord/geo/GeoModel.ts
@@ -61,7 +61,7 @@ interface GeoLabelFormatterDataParams {
 export interface RegoinOption extends GeoStateOption, StatesOptionMixin<GeoStateOption> {
     name?: string
     selected?: boolean
-    tooltip?: Partial<Pick<CommonTooltipOption<GeoTooltipFormatterParams>, 'show'>>
+    tooltip?: CommonTooltipOption<GeoTooltipFormatterParams>
 }
 
 export interface GeoTooltipFormatterParams {
@@ -93,6 +93,7 @@ export interface GeoCommonOptionMixin extends RoamOptionMixin {
     boundingCoords?: number[][];
 
     nameMap?: NameMap;
+    nameProperty?: string;
 }
 
 export interface GeoOption extends
@@ -227,15 +228,16 @@ class GeoModel extends ComponentModel<GeoOption> {
 
     optionUpdated(): void {
         const option = this.option;
-        const self = this;
 
-        option.regions = geoCreator.getFilledRegions(option.regions, option.map, option.nameMap);
+        option.regions = geoCreator.getFilledRegions(
+            option.regions, option.map, option.nameMap, option.nameProperty
+        );
 
         const selectedMap: Dictionary<boolean> = {};
-        this._optionModelMap = zrUtil.reduce(option.regions || [], function (optionModelMap, regionOpt) {
+        this._optionModelMap = zrUtil.reduce(option.regions || [], (optionModelMap, regionOpt) => {
             const regionName = regionOpt.name;
             if (regionName) {
-                optionModelMap.set(regionName, new Model(regionOpt, self));
+                optionModelMap.set(regionName, new Model(regionOpt, this, this.ecModel));
                 if (regionOpt.selected) {
                     selectedMap[regionName] = true;
                 }
diff --git a/src/coord/geo/GeoSVGResource.ts b/src/coord/geo/GeoSVGResource.ts
index 0099161..8162879 100644
--- a/src/coord/geo/GeoSVGResource.ts
+++ b/src/coord/geo/GeoSVGResource.ts
@@ -17,22 +17,41 @@
 * under the License.
 */
 
-import {parseSVG, makeViewBoxTransform} from 'zrender/src/tool/parseSVG';
+import { parseSVG, makeViewBoxTransform, SVGNodeTagLower } 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, HashMap, hasOwn} from 'zrender/src/core/util';
 import BoundingRect from 'zrender/src/core/BoundingRect';
-import { GeoResource, GeoSVGGraphicRoot, GeoSVGSourceInput, NameMap } from './geoTypes';
+import { GeoResource, GeoSVGGraphicRoot, GeoSVGSourceInput, RegionGraphic } from './geoTypes';
 import { parseXML } from 'zrender/src/tool/parseXML';
-import Displayable from 'zrender/src/graphic/Displayable';
 import { GeoSVGRegion } from './Region';
 
-interface GeoSVGGraphicRecord {
+export interface GeoSVGGraphicRecord {
     root: Group;
     boundingRect: BoundingRect;
-    regionElements: Displayable[];
+    regionGraphics: RegionGraphic[];
 }
 
+const REGION_AVAILABLE_SVG_TAG_MAP = createHashMap<number, SVGNodeTagLower>([
+    'rect', 'circle', 'line', 'ellipse', 'polygon', 'polyline', 'text', 'tspan', 'path'
+]);
+const STYLE_OPTION_KEY = createHashMap<'itemStyle' | 'lineStyle', SVGNodeTagLower>({
+    'rect': 'itemStyle',
+    'circle': 'itemStyle',
+    'line': 'lineStyle',
+    'ellipse': 'itemStyle',
+    'polygon': 'itemStyle',
+    'polyline': 'lineStyle',
+    // 'image': '?', // TODO
+    // 'text': '?', // TODO
+    // 'tspan': '?', // TODO
+    'path': 'itemStyle'
+});
+const LABEL_HOST_MAP = createHashMap<number, SVGNodeTagLower>([
+    'rect', 'circle', 'line', 'ellipse', 'polygon', 'polyline', 'path'
+]);
+
+
 export class GeoSVGResource implements GeoResource {
 
     readonly type = 'geoSVG';
@@ -44,8 +63,6 @@ export class GeoSVGResource implements GeoResource {
     private _regions: GeoSVGRegion[] = [];
     // Key: region.name
     private _regionsMap: HashMap<GeoSVGRegion> = createHashMap<GeoSVGRegion>();
-    // Key: region.name
-    private _nameCoordMap: HashMap<number[]> = createHashMap<number[]>();
 
     // All used graphics. key: hostKey, value: root
     private _usedGraphicMap: HashMap<GeoSVGGraphicRecord> = createHashMap();
@@ -67,7 +84,7 @@ export class GeoSVGResource implements GeoResource {
         this._parsedXML = parseXML(svg);
     }
 
-    load(nameMap: NameMap, nameProperty: string) {
+    load(/* nameMap: NameMap */) {
         // In the "load" stage, graphic need to be built to
         // get boundingRect for geo coordinate system.
         let firstGraphic = this._firstGraphic;
@@ -84,41 +101,41 @@ export class GeoSVGResource implements GeoResource {
 
             this._boundingRect = this._firstGraphic.boundingRect.clone();
 
-            // Create resions only for the first graphic, see coments below.
-            const regionElements = firstGraphic.regionElements;
-            for (let i = 0; i < regionElements.length; i++) {
-                const el = regionElements[i];
-
-                // Try use the alias in geoNameMap
-                let regionName = el.name;
-                if (nameMap && nameMap.hasOwnProperty(regionName)) {
-                    regionName = nameMap[regionName];
-                }
-
-                const region = new GeoSVGRegion(regionName, el);
-
+            const regionGraphics = firstGraphic.regionGraphics;
+            // PENDING: `nameMap` will not be supported until some real requirement come.
+            // if (nameMap) {
+            //     regionGraphics = applyNameMap(regionGraphics, 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(regionName, region);
+                this._regionsMap.set(regionGraphic.name, region);
             }
         }
 
         return {
             boundingRect: this._boundingRect,
             regions: this._regions,
-            regionsMap: this._regionsMap,
-            nameCoordMap: this._nameCoordMap
+            regionsMap: this._regionsMap
         };
     }
 
-    // Consider:
-    // (1) One graphic element can not be shared by different `geoView` running simultaneously.
-    //     Notice, also need to consider multiple echarts instances share a `mapRecord`.
-    // (2) Converting SVG to graphic elements is time consuming.
-    // (3) In the current architecture, `load` should be called frequently to get boundingRect,
-    //     and it is called without view info.
-    // So we maintain graphic elements in this module, and enables `view` to use/return these
-    // graphics from/to the pool with it's uid.
-    useGraphic(hostKey: string): GeoSVGGraphicRecord {
+    /**
+     * Consider:
+     * (1) One graphic element can not be shared by different `geoView` running simultaneously.
+     *     Notice, also need to consider multiple echarts instances share a `mapRecord`.
+     * (2) Converting SVG to graphic elements is time consuming.
+     * (3) In the current architecture, `load` should be called frequently to get boundingRect,
+     *     and it is called without view info.
+     * So we maintain graphic elements in this module, and enables `view` to use/return these
+     * graphics from/to the pool with it's uid.
+     */
+    useGraphic(hostKey: string /*, nameMap: NameMap */): GeoSVGGraphicRecord {
         const usedRootMap = this._usedGraphicMap;
 
         let svgGraphic = usedRootMap.get(hostKey);
@@ -130,7 +147,17 @@ export class GeoSVGResource implements GeoResource {
             // use the first boundingRect to avoid duplicated boundingRect calculation.
             || buildGraphic(this._parsedXML, this._boundingRect);
 
-        return usedRootMap.set(hostKey, svgGraphic);
+        usedRootMap.set(hostKey, svgGraphic);
+
+        // 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.
+        // if (nameMap) {
+        //     svgGraphic = extend({}, svgGraphic);
+        //     svgGraphic.regionGraphics = applyNameMap(svgGraphic.regionGraphics, nameMap);
+        // }
+
+        return svgGraphic;
     }
 
     freeGraphic(hostKey: string): void {
@@ -203,9 +230,60 @@ function buildGraphic(
 
     (root as GeoSVGGraphicRoot).isGeoSVGGraphicRoot = true;
 
-    return {
-        root: root,
-        boundingRect: boundingRect,
-        regionElements: result.namedElements
-    };
+    const regionGraphics = [] as GeoSVGGraphicRecord['regionGraphics'];
+    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 styleOptionKey = STYLE_OPTION_KEY.get(svgNodeTagLower);
+            const el = namedItem.el;
+
+            regionGraphics.push({
+                name: namedItem.name,
+                el: el,
+                styleOptionKey: styleOptionKey,
+                stateTrigger: styleOptionKey != null ? el : null,
+                // text/tspan/image do not suport style but support event.
+                eventTrigger: el,
+                useLabel: LABEL_HOST_MAP.get(svgNodeTagLower) != null
+            });
+
+            // 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;
+                });
+            }
+        }
+    }
+
+    return { root, boundingRect, regionGraphics };
 }
+
+
+// PENDING: `nameMap` will not be supported until some real requirement come.
+// /**
+//  * Use the alias in geoNameMap.
+//  * The input `regionGraphics` must not be modified.
+//  */
+// function applyNameMap(
+//     regionGraphics: GeoSVGGraphicRecord['regionGraphics'],
+//     nameMap: NameMap
+// ): GeoSVGGraphicRecord['regionGraphics'] {
+//     const result = [] as GeoSVGGraphicRecord['regionGraphics'];
+//     for (let i = 0; i < regionGraphics.length; i++) {
+//         let regionGraphic = regionGraphics[i];
+//         const name = regionGraphic.name;
+//         if (nameMap && nameMap.hasOwnProperty(name)) {
+//             regionGraphic = extend({}, regionGraphic);
+//             regionGraphic.name = name;
+//         }
+//         result.push(regionGraphic);
+//     }
+//     return result;
+// }
diff --git a/src/coord/geo/geoCreator.ts b/src/coord/geo/geoCreator.ts
index 49ae8d5..618d098 100644
--- a/src/coord/geo/geoCreator.ts
+++ b/src/coord/geo/geoCreator.ts
@@ -141,6 +141,7 @@ class GeoCreator implements CoordinateSystemCreator {
 
             const geo = new Geo(name + idx, name, {
                 nameMap: geoModel.get('nameMap'),
+                nameProperty: geoModel.get('nameProperty'),
                 aspectScale: geoModel.get('aspectScale')
             });
 
@@ -186,6 +187,7 @@ class GeoCreator implements CoordinateSystemCreator {
 
             const geo = new Geo(mapType, mapType, {
                 nameMap: zrUtil.mergeAll(nameMapList),
+                nameProperty: mapSeries[0].get('nameProperty'),
                 aspectScale: mapSeries[0].get('aspectScale')
             });
 
@@ -213,7 +215,10 @@ class GeoCreator implements CoordinateSystemCreator {
      * Fill given regions array
      */
     getFilledRegions(
-        originRegionArr: RegoinOption[], mapName: string, nameMap?: NameMap
+        originRegionArr: RegoinOption[],
+        mapName: string,
+        nameMap: NameMap,
+        nameProperty: string
     ): RegoinOption[] {
         // Not use the original
         const regionsArr = (originRegionArr || []).slice();
@@ -223,7 +228,7 @@ class GeoCreator implements CoordinateSystemCreator {
             dataNameMap.set(regionsArr[i].name, regionsArr[i]);
         }
 
-        const source = geoSourceManager.load(mapName, nameMap);
+        const source = geoSourceManager.load(mapName, nameMap, nameProperty);
         zrUtil.each(source.regions, function (region) {
             const name = region.name;
             !dataNameMap.get(name) && regionsArr.push({name: name});
diff --git a/src/coord/geo/geoSourceManager.ts b/src/coord/geo/geoSourceManager.ts
index f13d9ec..4cb5fb8 100644
--- a/src/coord/geo/geoSourceManager.ts
+++ b/src/coord/geo/geoSourceManager.ts
@@ -131,7 +131,7 @@ export default {
             && (resource as GeoJSONResource).getMapForUser();
     },
 
-    load: function (mapName: string, nameMap: NameMap, nameProperty?: string): ReturnType<GeoResource['load']> {
+    load: function (mapName: string, nameMap: NameMap, nameProperty: string): ReturnType<GeoResource['load']> {
         const resource = storage.get(mapName);
 
         if (!resource) {
diff --git a/src/coord/geo/geoTypes.ts b/src/coord/geo/geoTypes.ts
index 303db25..773cf1c 100644
--- a/src/coord/geo/geoTypes.ts
+++ b/src/coord/geo/geoTypes.ts
@@ -19,8 +19,9 @@
 
 import BoundingRect from 'zrender/src/core/BoundingRect';
 import { HashMap } from 'zrender/src/core/util';
-import { Group } from '../../util/graphic';
+import { Group, Path } from '../../util/graphic';
 import { Region } from './Region';
+import Element from 'zrender/src/Element';
 
 
 export type GeoSVGSourceInput = 'string' | Document | SVGElement;
@@ -125,16 +126,35 @@ interface GeoJSONGeometryMultiPolygonCompressed {
 
 export interface GeoResource {
     readonly type: 'geoJSON' | 'geoSVG';
-    load(nameMap: NameMap, nameProperty: string): {
+    load(
+        nameMap: NameMap,
+        nameProperty: string
+    ): {
         boundingRect: BoundingRect;
         regions: Region[];
         // Key: region.name
         regionsMap: HashMap<Region>;
-        // Key: region.name
-        nameCoordMap: HashMap<number[]>;
     };
 }
 
 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;
+    // Use this key to obtain style config in echarts option.
+    styleOptionKey: 'itemStyle' | 'lineStyle';
+};
diff --git a/src/data/OrdinalMeta.ts b/src/data/OrdinalMeta.ts
index 511d54f..f2fd790 100644
--- a/src/data/OrdinalMeta.ts
+++ b/src/data/OrdinalMeta.ts
@@ -113,7 +113,7 @@ class OrdinalMeta {
     // Consider big data, do not create map until needed.
     private _getOrCreateMap(): HashMap<OrdinalNumber> {
         return this._map || (
-            this._map = createHashMap(this.categories)
+            this._map = createHashMap<OrdinalNumber>(this.categories)
         );
     }
 }
diff --git a/src/model/mixin/lineStyle.ts b/src/model/mixin/lineStyle.ts
index 168102e..712b2fc 100644
--- a/src/model/mixin/lineStyle.ts
+++ b/src/model/mixin/lineStyle.ts
@@ -54,7 +54,7 @@ type LineStyleKeys = 'lineWidth'
     | 'lineJoin'
     | 'miterLimit';
 
-type LineStyleProps = Pick<PathStyleProps, LineStyleKeys>;
+export type LineStyleProps = Pick<PathStyleProps, LineStyleKeys>;
 
 class LineStyleMixin {
 
diff --git a/test/geo-svg.html b/test/geo-svg.html
index 86d4961..2d52f33 100644
--- a/test/geo-svg.html
+++ b/test/geo-svg.html
@@ -49,20 +49,34 @@ under the License.
 
 
         <script>
-            function listenAndPrintSelectChanged(chart) {
+            function listenAndPrintEvent(chart) {
                 if (!chart) {
                     return;
                 }
                 const out = {
                 };
                 chart.on('geoselectchanged', function (params) {
-                    out.allSelected = params.allSelected;
-                    console.log('geoselectechanged');
+                    out.geoselectechanged = {
+                        allSelected: params.allSelected
+                    };
+                    console.log('geoselectechanged', params);
                     chart.__testHelper.updateInfo(out, 'event');
                 });
                 chart.on('selectchanged', function (params) {
-                    out.selected = params.selected;
-                    console.log('selectechanged');
+                    out.selectechanged = {
+                        selected: params.selected
+                    };
+                    console.log('selectechanged', params);
+                    chart.__testHelper.updateInfo(out, 'event');
+                });
+                chart.on('click', function (params) {
+                    out.click = {
+                        componentIndex: params.componentIndex,
+                        componentType: params.componentType,
+                        geoIndex: params.geoIndex,
+                        name: params.name
+                    };
+                    console.log('click', params);
                     chart.__testHelper.updateInfo(out, 'event');
                 });
             }
@@ -124,7 +138,7 @@ under the License.
                 height: 300
             });
 
-            listenAndPrintSelectChanged(chart);
+            listenAndPrintEvent(chart);
 
         });
         </script>
@@ -176,7 +190,7 @@ under the License.
                 height: 300
             });
 
-            listenAndPrintSelectChanged(chart);
+            listenAndPrintEvent(chart);
 
         });
         </script>
@@ -247,7 +261,7 @@ under the License.
                     // recordCanvas: true,
                 });
 
-                listenAndPrintSelectChanged(chart);
+                listenAndPrintEvent(chart);
 
             });
 
@@ -313,7 +327,7 @@ under the License.
                     // recordCanvas: true,
                 });
 
-                listenAndPrintSelectChanged(chart);
+                listenAndPrintEvent(chart);
 
             });
 
@@ -383,7 +397,7 @@ under the License.
                     // recordCanvas: true,
                 });
 
-                listenAndPrintSelectChanged(chart);
+                listenAndPrintEvent(chart);
 
             });
 
@@ -416,18 +430,28 @@ under the License.
                         itemStyle: {
                             color: null
                         },
+                        tooltip: {
+                            show: true,
+                            confine: true,
+                            formatter: function (params) {
+                                return [
+                                    'This is the introduction:',
+                                    'xxxxxxxxxxxxxxxxxxxxx',
+                                    'xxxxxxxxxxxxxxxxxxxxx',
+                                    'xxxxxxxxxxxxxxxxxxxxx',
+                                    'xxxxxxxxxxxxxxxxxxxxx',
+                                    'xxxxxxxxxxxxxxxxxxxxx',
+                                    'xxxxxxxxxxxxxxxxxxxxx',
+                                    'xxxxxxxxxxxxxxxxxxxxx',
+                                    'xxxxxxxxxxxxxxxxxxxxx',
+                                    'xxxxxxxxxxxxxxxxxxxxx',
+                                    'xxxxxxxxxxxxxxxxxxxxx'
+                                ].join('<br>');
+                            }
+                        },
                         emphasis: {
-                            // itemStyle: {
-                                // borderColor: 'red',
-                                // borderWidth: 5
-                            // },
-                            // areaStyle: {
-                                // borderColor: 'red',
-                                // borderWidth: 5
-                            // },
                             label: {
-                                textBorderColor: '#fff',
-                                textBorderWidth: 2
+                                show: false
                             }
                         },
                         select: {
@@ -435,33 +459,55 @@ under the License.
                                 color: '#b50205'
                             },
                             label: {
-                                show: false,
-                                textBorderColor: '#fff',
-                                textBorderWidth: 2
+                                show: false
                             }
                         },
+                        regions: [{
+                            name: 'Sikeloi',
+                            tooltip: {
+                                formatter: 'Sikeloi',
+                                textStyle: { color: '#555' },
+                                backgroundColor: '#ccc'
+                            }
+                        }, {
+                            name: 'Sikanoi',
+                            tooltip: {
+                                formatter: 'Sikanoi',
+                                textStyle: { color: '#555' },
+                                backgroundColor: '#ccc'
+                            }
+                        }, {
+                            name: 'Elymoi',
+                            tooltip: {
+                                formatter: 'Elymoi',
+                                textStyle: { color: '#555' },
+                                backgroundColor: '#ccc'
+                            }
+                        }],
                         z: 0
                     }],
-                    series: {
-                        type: 'map',
-                        selectedMode: 'multiple',
-                        coordinateSystem: 'geo',
-                        geoIndex: 0
-                    }
+                    // series: {
+                    //     type: 'map',
+                    //     selectedMode: 'multiple',
+                    //     coordinateSystem: 'geo',
+                    //     geoIndex: 0
+                    // }
                 };
 
                 var chart = testHelper.create(echarts, 'main_geo_svg_regions', {
                     title: [
-                        'map series on declared geo with svg resource',
-                        'Hover seat: check **tooltip** correct.'
+                        'symbol and label use the same name in SVG.',
+                        'Hover each symbol and text, tooltip should be displayed.',
+                        'Hover the three area, tooltip should be displayed.',
+                        'Click, check **selected**.'
                     ],
                     option: option,
                     info: {},
                     infoKey: 'event',
-                    height: 400
+                    height: 500
                 });
 
-                listenAndPrintSelectChanged(chart);
+                listenAndPrintEvent(chart);
 
                 if (chart) {
                     chart.on('georoam', function (params) {

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