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 2018/09/14 20:30:20 UTC

[incubator-echarts-website] branch asf-site updated: 4.2.0-rc.1

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

sushuang pushed a commit to branch asf-site
in repository https://gitbox.apache.org/repos/asf/incubator-echarts-website.git


The following commit(s) were added to refs/heads/asf-site by this push:
     new 7750409  4.2.0-rc.1
7750409 is described below

commit 7750409f206c3f3fd6c979a20059da96a7995bd2
Author: sushuang <su...@gmail.com>
AuthorDate: Sat Sep 15 04:29:07 2018 +0800

    4.2.0-rc.1
---
 api.html                                           |    8 +-
 builder.html                                       |    6 +-
 builder/echarts.html                               |    2 +-
 .../echarts/chart/candlestick/candlestickLayout.js |    2 +
 .../src/echarts/chart/chord/chordCircularLayout.js |    4 +-
 builder/src/echarts/chart/custom.js                |  262 +-
 builder/src/echarts/chart/graph/GraphView.js       |   20 +-
 builder/src/echarts/chart/graph/adjustEdge.js      |    8 +-
 builder/src/echarts/chart/graph/graphAction.js     |   28 +-
 builder/src/echarts/chart/heatmap/HeatmapLayer.js  |    2 +
 .../src/echarts/chart/helper/LargeSymbolDraw.js    |    2 +
 builder/src/echarts/chart/helper/Symbol.js         |   58 +-
 .../focusNodeAdjacencyAction.js}                   |   39 +-
 builder/src/echarts/chart/helper/treeHelper.js     |    4 +
 builder/src/echarts/chart/lines/LinesSeries.js     |    2 +
 builder/src/echarts/chart/lines/linesLayout.js     |    2 +
 builder/src/echarts/chart/parallel/ParallelView.js |    1 -
 builder/src/echarts/chart/pie/PieView.js           |    3 +
 builder/src/echarts/chart/sankey/SankeySeries.js   |   21 +-
 builder/src/echarts/chart/sankey/SankeyView.js     |  241 +-
 builder/src/echarts/chart/sankey/sankeyAction.js   |    6 +
 builder/src/echarts/chart/sankey/sankeyLayout.js   |  396 +-
 .../src/echarts/chart/sunburst/SunburstSeries.js   |    5 +
 .../echarts/chart/themeRiver/ThemeRiverSeries.js   |    3 +-
 .../echarts/chart/themeRiver/themeRiverVisual.js   |    2 +-
 builder/src/echarts/chart/tree/TreeSeries.js       |   17 +-
 builder/src/echarts/chart/tree/TreeView.js         |  172 +-
 builder/src/echarts/chart/tree/treeAction.js       |   26 +
 builder/src/echarts/chart/treemap/Breadcrumb.js    |    1 +
 builder/src/echarts/chart/treemap/TreemapView.js   |   15 +-
 builder/src/echarts/component/axis/AxisBuilder.js  |    3 +-
 .../component/axisPointer/AxisPointerModel.js      |    5 +
 .../src/echarts/component/dataZoom/AxisProxy.js    |    5 +-
 .../echarts/component/dataZoom/InsideZoomModel.js  |    2 +
 .../echarts/component/dataZoom/InsideZoomView.js   |   71 +-
 .../echarts/component/dataZoom/SliderZoomModel.js  |    4 +
 builder/src/echarts/component/dataZoom/roams.js    |   83 +-
 builder/src/echarts/component/graphic.js           |   32 +-
 builder/src/echarts/component/helper/MapDraw.js    |  106 +-
 .../src/echarts/component/helper/RoamController.js |   98 +-
 builder/src/echarts/component/legend/LegendView.js |   17 +-
 builder/src/echarts/component/title.js             |    9 +-
 .../src/echarts/component/toolbox/feature/Brush.js |    9 +-
 .../echarts/component/toolbox/feature/DataZoom.js  |    4 +-
 .../echarts/component/toolbox/feature/MagicType.js |    5 +-
 .../echarts/component/toolbox/feature/Restore.js   |    4 +
 .../component/toolbox/feature/SaveAsImage.js       |    2 +
 .../echarts/component/tooltip/TooltipContent.js    |   27 +-
 .../src/echarts/component/tooltip/TooltipModel.js  |    7 +-
 .../component/tooltip/TooltipRichContent.js        |  174 +
 .../src/echarts/component/tooltip/TooltipView.js   |  100 +-
 builder/src/echarts/coord/axisHelper.js            |    3 +
 builder/src/echarts/coord/axisModelCommonMixin.js  |    4 +-
 builder/src/echarts/coord/axisTickLabelBuilder.js  |    3 +-
 builder/src/echarts/coord/cartesian/Grid.js        |   40 +-
 .../echarts/coord/cartesian/cartesianAxisHelper.js |    4 +-
 builder/src/echarts/coord/geo/Geo.js               |  119 +-
 builder/src/echarts/coord/geo/GeoModel.js          |    4 +-
 builder/src/echarts/coord/geo/Region.js            |   10 +-
 builder/src/echarts/coord/geo/fix/diaoyuIsland.js  |   16 +-
 builder/src/echarts/coord/geo/fix/geoCoord.js      |    7 +-
 builder/src/echarts/coord/geo/fix/nanhai.js        |    6 +-
 builder/src/echarts/coord/geo/fix/textCoord.js     |    7 +-
 builder/src/echarts/coord/geo/geoCreator.js        |   54 +-
 builder/src/echarts/coord/geo/geoJSONLoader.js     |   82 +
 builder/src/echarts/coord/geo/geoSVGLoader.js      |  132 +
 builder/src/echarts/coord/geo/geoSourceManager.js  |  100 +
 builder/src/echarts/coord/geo/mapDataStorage.js    |   79 +
 builder/src/echarts/coord/geo/prepareCustom.js     |    3 +-
 builder/src/echarts/coord/polar/AngleAxis.js       |   57 +-
 builder/src/echarts/coord/radar/Radar.js           |   20 +-
 builder/src/echarts/data/List.js                   |    2 +
 .../src/echarts/data/helper/completeDimensions.js  |   18 +-
 builder/src/echarts/data/helper/sourceHelper.js    |    7 +-
 builder/src/echarts/echarts.js                     |  195 +-
 builder/src/echarts/export.js                      |   10 +-
 builder/src/echarts/layout/barGrid.js              |   22 +-
 builder/src/echarts/layout/barPolar.js             |    9 +-
 builder/src/echarts/layout/points.js               |    2 +
 builder/src/echarts/model/Series.js                |   63 +-
 builder/src/echarts/model/mixin/dataFormat.js      |   20 +-
 builder/src/echarts/scale/Time.js                  |    4 +-
 builder/src/echarts/stream/Scheduler.js            |    5 +-
 builder/src/echarts/util/format.js                 |   20 +-
 builder/src/echarts/util/graphic.js                |  391 +-
 builder/src/echarts/util/model.js                  |    9 +
 builder/src/echarts/util/nest.js                   |  125 +
 builder/src/echarts/util/number.js                 |    4 +
 builder/src/echarts/util/quickSelect.js            |    2 +-
 builder/src/echarts/view/Chart.js                  |   12 +-
 builder/src/echarts/view/Component.js              |   11 +-
 builder/src/zrender/Handler.js                     |    8 +-
 builder/src/zrender/Painter.js                     |   11 +-
 builder/src/zrender/animation/track.js             |   74 +-
 builder/src/zrender/core/PathProxy.js              |    2 +-
 builder/src/zrender/core/env.js                    |   14 +-
 builder/src/zrender/core/util.js                   |   18 +-
 builder/src/zrender/export.js                      |    4 +-
 builder/src/zrender/graphic/Displayable.js         |   10 +-
 builder/src/zrender/graphic/Path.js                |   24 +-
 builder/src/zrender/graphic/Style.js               |   18 +-
 builder/src/zrender/graphic/Text.js                |    9 +-
 builder/src/zrender/graphic/helper/image.js        |    4 +-
 builder/src/zrender/graphic/helper/text.js         |  132 +-
 builder/src/zrender/graphic/mixin/RectText.js      |    3 +
 builder/src/zrender/mixin/Animatable.js            |  207 +-
 builder/src/zrender/mixin/Eventful.js              |  153 +-
 builder/src/zrender/mixin/Transformable.js         |   83 +-
 builder/src/zrender/svg/graphic.js                 |   21 +-
 builder/src/zrender/tool/parseSVG.js               |  665 +++
 builder/src/zrender/tool/path.js                   |  105 +-
 builder/src/zrender/zrender.js                     |    5 +-
 changelog.html                                     |   43 +-
 coding-standard.html                               |    2 +-
 committers.html                                    |    2 +-
 dependencies.html                                  |    2 +-
 dist/echarts-en.common.js                          | 3908 +++++++++---
 dist/echarts-en.common.min.js                      |    2 +-
 dist/echarts-en.js                                 | 6239 +++++++++++++++-----
 dist/echarts-en.js.map                             |    2 +-
 dist/echarts-en.min.js                             |    2 +-
 dist/echarts-en.simple.js                          | 3002 +++++++---
 dist/echarts-en.simple.min.js                      |    2 +-
 dist/echarts.common.js                             | 3908 +++++++++---
 dist/echarts.common.min.js                         |    2 +-
 dist/echarts.js                                    | 6239 +++++++++++++++-----
 dist/echarts.js.map                                |    2 +-
 dist/echarts.min.js                                |    2 +-
 dist/echarts.simple.js                             | 3002 +++++++---
 dist/echarts.simple.min.js                         |    2 +-
 dist/extension/dataTool.js                         |   22 +
 dist/extension/dataTool.js.map                     |    2 +-
 documents/en/api.json                              |    2 +-
 documents/en/changelog.html                        |   41 +-
 documents/en/option.json                           |    2 +-
 documents/en/tutorial.json                         |    2 +-
 download.html                                      |    2 +-
 faq.html                                           |    2 +-
 index.html                                         |    2 +-
 js/docTool/main.js                                 |    8 +-
 license.html                                       |    2 +-
 maillist.html                                      |    2 +-
 option.html                                        |    8 +-
 option3.html                                       |    8 +-
 tutorial.html                                      |    8 +-
 145 files changed, 24110 insertions(+), 7742 deletions(-)

diff --git a/api.html b/api.html
index 04062f1..5603887 100644
--- a/api.html
+++ b/api.html
@@ -1,4 +1,4 @@
-<!DOCTYPE html><html lang="en-US"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1" user-scalable="no"><meta name="description" content="ECharts, a powerful, interactive charting and visualization library for browser"><link rel="shortcut icon" href="images/favicon.png"><link rel="stylesheet" type="text/css" href="vendors/bootstrap/css/bootstrap.min.css"><!-- HTML5 shim and Respond.js for IE [...]
+<!DOCTYPE html><html lang="en-US"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1" user-scalable="no"><meta name="description" content="ECharts, a powerful, interactive charting and visualization library for browser"><link rel="shortcut icon" href="images/favicon.png"><link rel="stylesheet" type="text/css" href="vendors/bootstrap/css/bootstrap.min.css"><!-- HTML5 shim and Respond.js for IE [...]
 </script><script type="text/javascript" src="./vendors/pace/pace.min.js"></script><script id="font-hack" type="text/javascript">if (/windows/i.test(navigator.userAgent)) {
     var el = document.createElement('style');
     el.innerHTML = ''
@@ -6,7 +6,7 @@
         + '@font-face {font-family:"noto-light";src:local("Microsoft Yahei");}';
     document.head.insertBefore(el, document.getElementById('font-hack'));
 }
-</script><title>ECharts Documentation</title><link rel="stylesheet" type="text/css" href="css/ecOption.css?_v_=1533885633198"><link rel="stylesheet" type="text/css" href="vendors/prettify/prettify.css"><link rel="stylesheet" type="text/css" href="vendors/perfect-scrollbar/0.6.8/css/perfect-scrollbar.min.css"><link rel="stylesheet" type="text/css" href="vendors/jquery-autocomplete/jquery.auto-complete.css"><link rel="stylesheet" type="text/css" href="vendors/twentytwenty/twentytwenty.css" [...]
+</script><title>ECharts Documentation</title><link rel="stylesheet" type="text/css" href="css/ecOption.css?_v_=1536949973669"><link rel="stylesheet" type="text/css" href="vendors/prettify/prettify.css"><link rel="stylesheet" type="text/css" href="vendors/perfect-scrollbar/0.6.8/css/perfect-scrollbar.min.css"><link rel="stylesheet" type="text/css" href="vendors/jquery-autocomplete/jquery.auto-complete.css"><link rel="stylesheet" type="text/css" href="vendors/twentytwenty/twentytwenty.css" [...]
 <!--[if (gt IE 8)|!(IE)]><body class="undefined"></body><![endif]--><div id="main"><nav class="navbar navbar-default navbar-fixed-top"><div class="container-fluid"><div class="navbar-header"><button type="button" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false" class="navbar-toggle collapsed"><span class="sr-only">Toggle navigation</span><span class="icon-bar"></span><span class="icon-bar"></span><span class="icon-bar"></span></button><a href="./in [...]
     pageName: 'api',
     initHash: 'echarts',
@@ -28,7 +28,7 @@
 var vendorPath = '../vendors';
 
 define('globalArgs', extend({
-    version: '1533885633198',
+    version: '1536949973669',
     basePath: './',
     // Schema url is added by each doc page
     schemaUrl: '',
@@ -63,7 +63,7 @@ require.config({
         hasher: vendorPath + '/hasher/1.2.0/hasher.min',
         perfectScrollbar: vendorPath + '/perfect-scrollbar/0.6.8/js/perfect-scrollbar'
     },
-    urlArgs: '_v_=1533885633198'
+    urlArgs: '_v_=1536949973669'
 });
 
 require(['docTool/main'], function (main) {
diff --git a/builder.html b/builder.html
index 0635f26..8f7c35c 100644
--- a/builder.html
+++ b/builder.html
@@ -1,4 +1,4 @@
-<!DOCTYPE html><html lang="en-US"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1" user-scalable="no"><meta name="description" content="ECharts, a powerful, interactive charting and visualization library for browser"><link rel="shortcut icon" href="images/favicon.png"><link rel="stylesheet" type="text/css" href="vendors/bootstrap/css/bootstrap.min.css"><!-- HTML5 shim and Respond.js for IE [...]
+<!DOCTYPE html><html lang="en-US"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1" user-scalable="no"><meta name="description" content="ECharts, a powerful, interactive charting and visualization library for browser"><link rel="shortcut icon" href="images/favicon.png"><link rel="stylesheet" type="text/css" href="vendors/bootstrap/css/bootstrap.min.css"><!-- HTML5 shim and Respond.js for IE [...]
 </script><script type="text/javascript" src="./vendors/pace/pace.min.js"></script><script id="font-hack" type="text/javascript">if (/windows/i.test(navigator.userAgent)) {
     var el = document.createElement('style');
     el.innerHTML = ''
@@ -57,7 +57,7 @@ $("#build").click(function () {
     }
 
     parameters += '&version=4'
-        + '&versionCode=4.1.0';
+        + '&versionCode=4.2.0-rc.1';
 
     var email = $('#email').val();
     var log = parameters;
@@ -66,7 +66,7 @@ $("#build").click(function () {
     }
     ecLog({
         'page': 'builder',
-        'version': '4.1.0',
+        'version': '4.2.0-rc.1',
         'build-parameters-3': log
     });
 
diff --git a/builder/echarts.html b/builder/echarts.html
index 4ba897e..a0b0239 100644
--- a/builder/echarts.html
+++ b/builder/echarts.html
@@ -56,7 +56,7 @@
                 'esprima': 'lib/esprima',
                 'estraverse': 'lib/estraverse'
             },
-            urlArgs: 'v=4.1.0'
+            urlArgs: 'v=4.2.0-rc.1'
         });
 
         require(['build']);
diff --git a/builder/src/echarts/chart/candlestick/candlestickLayout.js b/builder/src/echarts/chart/candlestick/candlestickLayout.js
index faad05e..f104d48 100644
--- a/builder/src/echarts/chart/candlestick/candlestickLayout.js
+++ b/builder/src/echarts/chart/candlestick/candlestickLayout.js
@@ -16,6 +16,8 @@
 * specific language governing permissions and limitations
 * under the License.
 */
+
+/* global Float32Array */
 import { subPixelOptimize } from '../../util/graphic';
 import createRenderPlanner from '../helper/createRenderPlanner';
 import { parsePercent } from '../../util/number';
diff --git a/builder/src/echarts/chart/chord/chordCircularLayout.js b/builder/src/echarts/chart/chord/chordCircularLayout.js
index f31fdcc..b781393 100644
--- a/builder/src/echarts/chart/chord/chordCircularLayout.js
+++ b/builder/src/echarts/chart/chord/chordCircularLayout.js
@@ -58,7 +58,7 @@ function layout(graphs, opts) {
     return sumSize + group.size;
   }, 0);
 
-  if (opts.sort && opts.sort != 'none') {
+  if (opts.sort && opts.sort !== 'none') {
     groups.sort(compareGroups);
 
     if (opts.sort === 'descending') {
@@ -70,7 +70,7 @@ function layout(graphs, opts) {
   var angle = opts.startAngle * Math.PI / 180;
   var sign = opts.clockwise ? -1 : 1;
   zrUtil.each(groups, function (group) {
-    if (opts.sortSub && opts.sortSub != 'none') {
+    if (opts.sortSub && opts.sortSub !== 'none') {
       group.subGroups.sort(compareGroups);
 
       if (opts.sortSub === 'descending') {
diff --git a/builder/src/echarts/chart/custom.js b/builder/src/echarts/chart/custom.js
index 1e18b82..430aa6c 100644
--- a/builder/src/echarts/chart/custom.js
+++ b/builder/src/echarts/chart/custom.js
@@ -17,13 +17,14 @@
 * under the License.
 */
 import { __DEV__ } from '../config';
-import * as echarts from '../echarts';
 import * as zrUtil from 'zrender/src/core/util';
 import * as graphicUtil from '../util/graphic';
 import { getDefaultLabel } from './helper/labelHelper';
 import createListFromArray from './helper/createListFromArray';
 import { getLayoutOnAxis } from '../layout/barGrid';
 import DataDiffer from '../data/DataDiffer';
+import SeriesModel from '../model/Series';
+import ChartView from '../view/Chart';
 import prepareCartesian2d from '../coord/cartesian/prepareCustom';
 import prepareGeo from '../coord/geo/prepareCustom';
 import prepareSingleAxis from '../coord/single/prepareCustom';
@@ -58,7 +59,7 @@ var prepareCustoms = {
 // Model
 // ------
 
-echarts.extendSeriesModel({
+SeriesModel.extend({
   type: 'series.custom',
   dependencies: ['grid', 'polar', 'geo', 'singleAxis', 'calendar'],
   defaultOption: {
@@ -66,7 +67,8 @@ echarts.extendSeriesModel({
     // Can be set as 'none'
     zlevel: 0,
     z: 2,
-    legendHoverLink: true // Cartesian coordinate system
+    legendHoverLink: true,
+    useTransform: true // Cartesian coordinate system
     // xAxisIndex: 0,
     // yAxisIndex: 0,
     // Polar coordinate system
@@ -77,14 +79,27 @@ echarts.extendSeriesModel({
     // itemStyle: {}
 
   },
+
+  /**
+   * @override
+   */
   getInitialData: function (option, ecModel) {
     return createListFromArray(this.getSource(), this);
+  },
+
+  /**
+   * @override
+   */
+  getDataParams: function (dataIndex, dataType, el) {
+    var params = SeriesModel.prototype.getDataParams.apply(this, arguments);
+    el && (params.info = el.info);
+    return params;
   }
 }); // -----
 // View
 // -----
 
-echarts.extendChartView({
+ChartView.extend({
   type: 'custom',
 
   /**
@@ -96,17 +111,21 @@ echarts.extendChartView({
   /**
    * @override
    */
-  render: function (customSeries, ecModel, api) {
+  render: function (customSeries, ecModel, api, payload) {
     var oldData = this._data;
     var data = customSeries.getData();
     var group = this.group;
-    var renderItem = makeRenderItem(customSeries, data, ecModel, api);
-    this.group.removeAll();
+    var renderItem = makeRenderItem(customSeries, data, ecModel, api); // By default, merge mode is applied. In most cases, custom series is
+    // used in the scenario that data amount is not large but graphic elements
+    // is complicated, where merge mode is probably necessary for optimization.
+    // For example, reuse graphic elements and only update the transform when
+    // roam or data zoom according to `actionType`.
+
     data.diff(oldData).add(function (newIdx) {
-      createOrUpdate(null, newIdx, renderItem(newIdx), customSeries, group, data);
+      createOrUpdate(null, newIdx, renderItem(newIdx, payload), customSeries, group, data);
     }).update(function (newIdx, oldIdx) {
       var el = oldData.getItemGraphicEl(oldIdx);
-      createOrUpdate(el, newIdx, renderItem(newIdx), customSeries, group, data);
+      createOrUpdate(el, newIdx, renderItem(newIdx, payload), customSeries, group, data);
     }).remove(function (oldIdx) {
       var el = oldData.getItemGraphicEl(oldIdx);
       el && group.remove(el);
@@ -117,7 +136,7 @@ echarts.extendChartView({
     this.group.removeAll();
     this._data = null;
   },
-  incrementalRender: function (params, customSeries, ecModel, api) {
+  incrementalRender: function (params, customSeries, ecModel, api, payload) {
     var data = customSeries.getData();
     var renderItem = makeRenderItem(customSeries, data, ecModel, api);
 
@@ -129,7 +148,7 @@ echarts.extendChartView({
     }
 
     for (var idx = params.start; idx < params.end; idx++) {
-      var el = createOrUpdate(null, idx, renderItem(idx), customSeries, this.group, data);
+      var el = createOrUpdate(null, idx, renderItem(idx, payload), customSeries, this.group, data);
       el.traverse(setIncrementalAndHoverLayer);
     }
   },
@@ -137,7 +156,28 @@ echarts.extendChartView({
   /**
    * @override
    */
-  dispose: zrUtil.noop
+  dispose: zrUtil.noop,
+
+  /**
+   * @override
+   */
+  filterForExposedEvent: function (eventType, query, targetEl, packedEvent) {
+    var elementName = query.element;
+
+    if (elementName == null || targetEl.name === elementName) {
+      return true;
+    } // Enable to give a name on a group made by `renderItem`, and listen
+    // events that triggerd by its descendents.
+
+
+    while ((targetEl = targetEl.parent) && targetEl !== this.group) {
+      if (targetEl.name === elementName) {
+        return true;
+      }
+    }
+
+    return false;
+  }
 });
 
 function createEl(elOption) {
@@ -145,14 +185,18 @@ function createEl(elOption) {
   var el;
 
   if (graphicType === 'path') {
-    var shape = elOption.shape;
-    el = graphicUtil.makePath(shape.pathData, null, {
+    var shape = elOption.shape; // Using pathRect brings convenience to users sacle svg path.
+
+    var pathRect = shape.width != null && shape.height != null ? {
       x: shape.x || 0,
       y: shape.y || 0,
-      width: shape.width || 0,
-      height: shape.height || 0
-    }, 'center');
-    el.__customPathData = elOption.pathData;
+      width: shape.width,
+      height: shape.height
+    } : null;
+    var 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;
   } else if (graphicType === 'image') {
     el = new graphicUtil.Image({});
     el.__customImagePath = elOption.style.image;
@@ -169,24 +213,24 @@ function createEl(elOption) {
   return el;
 }
 
-function updateEl(el, dataIndex, elOption, animatableModel, data, isInit) {
-  var targetProps = {};
+function updateEl(el, dataIndex, elOption, animatableModel, data, isInit, isRoot) {
+  var transitionProps = {};
   var elOptionStyle = elOption.style || {};
-  elOption.shape && (targetProps.shape = zrUtil.clone(elOption.shape));
-  elOption.position && (targetProps.position = elOption.position.slice());
-  elOption.scale && (targetProps.scale = elOption.scale.slice());
-  elOption.origin && (targetProps.origin = elOption.origin.slice());
-  elOption.rotation && (targetProps.rotation = elOption.rotation);
+  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);
 
   if (el.type === 'image' && elOption.style) {
-    var targetStyle = targetProps.style = {};
+    var targetStyle = transitionProps.style = {};
     zrUtil.each(['x', 'y', 'width', 'height'], function (prop) {
       prepareStyleTransition(prop, targetStyle, elOptionStyle, el.style, isInit);
     });
   }
 
   if (el.type === 'text' && elOption.style) {
-    var targetStyle = targetProps.style = {};
+    var targetStyle = transitionProps.style = {};
     zrUtil.each(['x', 'y'], function (prop) {
       prepareStyleTransition(prop, targetStyle, elOptionStyle, el.style, isInit);
     }); // Compatible with previous: both support
@@ -212,17 +256,35 @@ function updateEl(el, dataIndex, elOption, animatableModel, data, isInit) {
   }
 
   if (isInit) {
-    el.attr(targetProps);
+    el.attr(transitionProps);
   } else {
-    graphicUtil.updateProps(el, targetProps, animatableModel, dataIndex);
-  } // z2 must not be null/undefined, otherwise sort error may occur.
-
+    graphicUtil.updateProps(el, transitionProps, animatableModel, 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`.
+
+  var styleEmphasis = elOption.styleEmphasis;
+  var disableStyleEmphasis = styleEmphasis === false;
+
+  if (!( // Try to escapse setting hover style for performance.
+  el.__cusHasEmphStl && styleEmphasis == null || !el.__cusHasEmphStl && disableStyleEmphasis)) {
+    // Should not use graphicUtil.setHoverStyle, since the styleEmphasis
+    // should not be share by group and its descendants.
+    graphicUtil.setElementHoverStyle(el, styleEmphasis);
+    el.__cusHasEmphStl = !disableStyleEmphasis;
+  }
 
-  el.attr({
-    z2: elOption.z2 || 0,
-    silent: elOption.silent
-  });
-  elOption.styleEmphasis !== false && graphicUtil.setHoverStyle(el, elOption.styleEmphasis);
+  isRoot && graphicUtil.setAsHoverStyleTrigger(el, !disableStyleEmphasis);
 }
 
 function prepareStyleTransition(prop, targetStyle, elOptionStyle, oldElStyle, isInit) {
@@ -255,6 +317,9 @@ function makeRenderItem(customSeries, data, ecModel, api) {
     font: font
   }, prepareResult.api || {});
   var userParams = {
+    // 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.
     context: {},
     seriesId: customSeries.id,
     seriesName: customSeries.name,
@@ -270,13 +335,15 @@ function makeRenderItem(customSeries, data, ecModel, api) {
   var currLabelNormalModel;
   var currLabelEmphasisModel;
   var currVisualColor;
-  return function (dataIndexInside) {
+  return function (dataIndexInside, payload) {
     currDataIndexInside = dataIndexInside;
     currDirty = true;
     return renderItem && renderItem(zrUtil.defaults({
       dataIndexInside: dataIndexInside,
-      dataIndex: data.getRawIndex(dataIndexInside)
-    }, userParams), userAPI) || {};
+      dataIndex: data.getRawIndex(dataIndexInside),
+      // Can be used for optimization when zoom or roam.
+      actionType: payload ? payload.type : null
+    }, userParams), userAPI);
   }; // Do not update cache until api called.
 
   function updateCache(dataIndexInside) {
@@ -416,58 +483,102 @@ function wrapEncodeDef(data) {
 }
 
 function createOrUpdate(el, dataIndex, elOption, animatableModel, group, data) {
-  el = doCreateOrUpdate(el, dataIndex, elOption, animatableModel, group, data);
+  el = doCreateOrUpdate(el, dataIndex, elOption, animatableModel, group, data, true);
   el && data.setItemGraphicEl(dataIndex, el);
   return el;
 }
 
-function doCreateOrUpdate(el, dataIndex, elOption, animatableModel, group, data) {
+function doCreateOrUpdate(el, dataIndex, elOption, animatableModel, group, data, isRoot) {
+  // [Rule]
+  // By default, follow merge mode.
+  //     (It probably brings benifit for performance in some cases of large data, where
+  //     user program can be optimized to that only updated props needed to be re-calculated,
+  //     or according to `actionType` some calculation can be skipped.)
+  // If `renderItem` returns `null`/`undefined`/`false`, remove the previous el if existing.
+  //     (It seems that violate the "merge" principle, but most of users probably intuitively
+  //     regard "return;" as "show nothing element whatever", so make a exception to meet the
+  //     most cases.)
+  var simplyRemove = !elOption; // `null`/`undefined`/`false`
+
+  elOption = elOption || {};
   var elOptionType = elOption.type;
+  var elOptionShape = elOption.shape;
+  var elOptionStyle = elOption.style;
 
-  if (el && elOptionType !== el.__customGraphicType && (elOptionType !== 'path' || elOption.pathData !== el.__customPathData) && (elOptionType !== 'image' || elOption.style.image !== el.__customImagePath) && (elOptionType !== 'text' || elOption.style.text !== el.__customText)) {
+  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;
   } // `elOption.type` is undefined when `renderItem` returns nothing.
 
 
-  if (elOptionType == null) {
+  if (simplyRemove) {
     return;
   }
 
   var isInit = !el;
   !el && (el = createEl(elOption));
-  updateEl(el, dataIndex, elOption, animatableModel, data, isInit);
+  updateEl(el, dataIndex, elOption, animatableModel, data, isInit, isRoot);
 
   if (elOptionType === 'group') {
-    var oldChildren = el.children() || [];
-    var newChildren = elOption.children || [];
-
-    if (elOption.diffChildrenByName) {
-      // lower performance.
-      diffGroupChildren({
-        oldChildren: oldChildren,
-        newChildren: newChildren,
-        dataIndex: dataIndex,
-        animatableModel: animatableModel,
-        group: el,
-        data: data
-      });
-    } else {
-      // better performance.
-      var index = 0;
-
-      for (; index < newChildren.length; index++) {
-        doCreateOrUpdate(el.childAt(index), dataIndex, newChildren[index], animatableModel, el, data);
-      }
+    mergeChildren(el, dataIndex, elOption, animatableModel, data);
+  } // Always add whatever already added to ensure sequence.
 
-      for (; index < oldChildren.length; index++) {
-        oldChildren[index] && el.remove(oldChildren[index]);
-      }
-    }
-  }
 
   group.add(el);
   return el;
+} // Usage:
+// (1) By default, `elOption.$mergeChildren` is `'byIndex'`, which indicates that
+//     the existing children will not be removed, and enables the feature that
+//     update some of the props of some of the children simply by construct
+//     the returned children of `renderItem` like:
+//     `var children = group.children = []; children[3] = {opacity: 0.5};`
+// (2) If `elOption.$mergeChildren` is `'byName'`, add/update/remove children
+//     by child.name. But that might be lower performance.
+// (3) If `elOption.$mergeChildren` is `false`, the existing children will be
+//     replaced totally.
+// (4) If `!elOption.children`, following the "merge" principle, nothing will happen.
+//
+// For implementation simpleness, do not provide a direct way to remove sinlge
+// 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) {
+  var newChildren = elOption.children;
+  var newLen = newChildren ? newChildren.length : 0;
+  var mergeChildren = elOption.$mergeChildren; // `diffChildrenByName` has been deprecated.
+
+  var byName = mergeChildren === 'byName' || elOption.diffChildrenByName;
+  var notMerge = mergeChildren === false; // For better performance on roam update, only enter if necessary.
+
+  if (!newLen && !byName && !notMerge) {
+    return;
+  }
+
+  if (byName) {
+    diffGroupChildren({
+      oldChildren: el.children() || [],
+      newChildren: newChildren || [],
+      dataIndex: dataIndex,
+      animatableModel: animatableModel,
+      group: el,
+      data: data
+    });
+    return;
+  }
+
+  notMerge && el.removeAll(); // Mapping children of a group simply by index, which
+  // might be better performance.
+
+  var index = 0;
+
+  for (; index < newLen; index++) {
+    newChildren[index] && doCreateOrUpdate(el.childAt(index), dataIndex, newChildren[index], animatableModel, el, data);
+  }
 }
 
 function diffGroupChildren(context) {
@@ -490,4 +601,17 @@ function processRemove(oldIndex) {
   var context = this.context;
   var child = context.oldChildren[oldIndex];
   child && context.group.remove(child);
+}
+
+function getPathData(shape) {
+  // "d" follows the SVG convention.
+  return shape && (shape.pathData || shape.d);
+}
+
+function hasOwnPathData(shape) {
+  return shape && (shape.hasOwnProperty('pathData') || shape.hasOwnProperty('d'));
+}
+
+function hasOwn(host, prop) {
+  return host && host.hasOwnProperty(prop);
 }
\ No newline at end of file
diff --git a/builder/src/echarts/chart/graph/GraphView.js b/builder/src/echarts/chart/graph/GraphView.js
index fb9459f..97081c2 100644
--- a/builder/src/echarts/chart/graph/GraphView.js
+++ b/builder/src/echarts/chart/graph/GraphView.js
@@ -120,7 +120,7 @@ export default echarts.extendChartView({
       var itemModel = data.getItemModel(idx); // Update draggable
 
       el.off('drag').off('dragend');
-      var draggable = data.getItemModel(idx).get('draggable');
+      var draggable = itemModel.get('draggable');
 
       if (draggable) {
         el.on('drag', function () {
@@ -295,22 +295,22 @@ export default echarts.extendChartView({
     controller.enable(seriesModel.get('roam'));
     controllerHost.zoomLimit = seriesModel.get('scaleLimit');
     controllerHost.zoom = seriesModel.coordinateSystem.getZoom();
-    controller.off('pan').off('zoom').on('pan', function (dx, dy) {
-      roamHelper.updateViewOnPan(controllerHost, dx, dy);
+    controller.off('pan').off('zoom').on('pan', function (e) {
+      roamHelper.updateViewOnPan(controllerHost, e.dx, e.dy);
       api.dispatchAction({
         seriesId: seriesModel.id,
         type: 'graphRoam',
-        dx: dx,
-        dy: dy
+        dx: e.dx,
+        dy: e.dy
       });
-    }).on('zoom', function (zoom, mouseX, mouseY) {
-      roamHelper.updateViewOnZoom(controllerHost, zoom, mouseX, mouseY);
+    }).on('zoom', function (e) {
+      roamHelper.updateViewOnZoom(controllerHost, e.scale, e.originX, e.originY);
       api.dispatchAction({
         seriesId: seriesModel.id,
         type: 'graphRoam',
-        zoom: zoom,
-        originX: mouseX,
-        originY: mouseY
+        zoom: e.scale,
+        originX: e.originX,
+        originY: e.originY
       });
 
       this._updateNodeAndLinkScale();
diff --git a/builder/src/echarts/chart/graph/adjustEdge.js b/builder/src/echarts/chart/graph/adjustEdge.js
index 4cceabe..4550611 100644
--- a/builder/src/echarts/chart/graph/adjustEdge.js
+++ b/builder/src/echarts/chart/graph/adjustEdge.js
@@ -123,7 +123,7 @@ export default function (graph, scale) {
       vec2.copy(pts[1], originalPoints[2]);
       vec2.copy(pts[2], originalPoints[1]);
 
-      if (fromSymbol && fromSymbol != 'none') {
+      if (fromSymbol && fromSymbol !== 'none') {
         var symbolSize = getSymbolSize(edge.node1);
         var t = intersectCurveCircle(pts, originalPoints[0], symbolSize * scale); // Subdivide and get the second
 
@@ -135,7 +135,7 @@ export default function (graph, scale) {
         pts[1][1] = tmp0[4];
       }
 
-      if (toSymbol && toSymbol != 'none') {
+      if (toSymbol && toSymbol !== 'none') {
         var symbolSize = getSymbolSize(edge.node2);
         var t = intersectCurveCircle(pts, originalPoints[1], symbolSize * scale); // Subdivide and get the first
 
@@ -158,12 +158,12 @@ export default function (graph, scale) {
         vec2.sub(v, pts2[1], pts2[0]);
         vec2.normalize(v, v);
 
-        if (fromSymbol && fromSymbol != 'none') {
+        if (fromSymbol && fromSymbol !== 'none') {
           var symbolSize = getSymbolSize(edge.node1);
           vec2.scaleAndAdd(pts2[0], pts2[0], v, symbolSize * scale);
         }
 
-        if (toSymbol && toSymbol != 'none') {
+        if (toSymbol && toSymbol !== 'none') {
           var symbolSize = getSymbolSize(edge.node2);
           vec2.scaleAndAdd(pts2[1], pts2[1], v, -symbolSize * scale);
         }
diff --git a/builder/src/echarts/chart/graph/graphAction.js b/builder/src/echarts/chart/graph/graphAction.js
index 013e6bf..982b5f3 100644
--- a/builder/src/echarts/chart/graph/graphAction.js
+++ b/builder/src/echarts/chart/graph/graphAction.js
@@ -18,6 +18,7 @@
 */
 import * as echarts from '../../echarts';
 import { updateCenterAndZoom } from '../../action/roamHelper';
+import '../helper/focusNodeAdjacencyAction';
 var actionInfo = {
   type: 'graphRoam',
   event: 'graphRoam',
@@ -43,29 +44,4 @@ echarts.registerAction(actionInfo, function (payload, ecModel) {
     seriesModel.setCenter && seriesModel.setCenter(res.center);
     seriesModel.setZoom && seriesModel.setZoom(res.zoom);
   });
-});
-/**
- * @payload
- * @property {number} [seriesIndex]
- * @property {string} [seriesId]
- * @property {string} [seriesName]
- * @property {number} [dataIndex]
- */
-
-echarts.registerAction({
-  type: 'focusNodeAdjacency',
-  event: 'focusNodeAdjacency',
-  update: 'series.graph:focusNodeAdjacency'
-}, function () {});
-/**
- * @payload
- * @property {number} [seriesIndex]
- * @property {string} [seriesId]
- * @property {string} [seriesName]
- */
-
-echarts.registerAction({
-  type: 'unfocusNodeAdjacency',
-  event: 'unfocusNodeAdjacency',
-  update: 'series.graph:unfocusNodeAdjacency'
-}, function () {});
\ No newline at end of file
+});
\ No newline at end of file
diff --git a/builder/src/echarts/chart/heatmap/HeatmapLayer.js b/builder/src/echarts/chart/heatmap/HeatmapLayer.js
index a8a398d..3fe50db 100644
--- a/builder/src/echarts/chart/heatmap/HeatmapLayer.js
+++ b/builder/src/echarts/chart/heatmap/HeatmapLayer.js
@@ -17,6 +17,8 @@
 * under the License.
 */
 
+/* global Uint8ClampedArray */
+
 /**
  * @file defines echarts Heatmap Chart
  * @author Ovilia (me@zhangwenli.com)
diff --git a/builder/src/echarts/chart/helper/LargeSymbolDraw.js b/builder/src/echarts/chart/helper/LargeSymbolDraw.js
index 199a2a9..b1613f6 100644
--- a/builder/src/echarts/chart/helper/LargeSymbolDraw.js
+++ b/builder/src/echarts/chart/helper/LargeSymbolDraw.js
@@ -16,6 +16,8 @@
 * specific language governing permissions and limitations
 * under the License.
 */
+
+/* global Float32Array */
 // TODO Batch by color
 import * as graphic from '../../util/graphic';
 import { createSymbol } from '../../util/symbol';
diff --git a/builder/src/echarts/chart/helper/Symbol.js b/builder/src/echarts/chart/helper/Symbol.js
index 706b687..e70f85c 100644
--- a/builder/src/echarts/chart/helper/Symbol.js
+++ b/builder/src/echarts/chart/helper/Symbol.js
@@ -299,35 +299,47 @@ symbolProto._updateCommon = function (data, idx, symbolSize, seriesScope) {
   // Do not use symbol.trigger('emphasis'), but use symbol.highlight() instead.
 
   graphic.setHoverStyle(symbolPath);
-  var scale = getScale(symbolSize);
+  symbolPath.__symbolOriginalScale = getScale(symbolSize);
 
   if (hoverAnimation && seriesModel.isAnimationEnabled()) {
-    var onEmphasis = function () {
-      // Do not support this hover animation util some scenario required.
-      // Animation can only be supported in hover layer when using `el.incremetal`.
-      if (this.incremental) {
-        return;
-      }
-
-      var ratio = scale[1] / scale[0];
-      this.animateTo({
-        scale: [Math.max(scale[0] * 1.1, scale[0] + 3), Math.max(scale[1] * 1.1, scale[1] + 3 * ratio)]
-      }, 400, 'elasticOut');
-    };
+    // Note: consider `off`, should use static function here.
+    symbolPath.on('mouseover', onMouseOver).on('mouseout', onMouseOut).on('emphasis', onEmphasis).on('normal', onNormal);
+  }
+};
 
-    var onNormal = function () {
-      if (this.incremental) {
-        return;
-      }
+function onMouseOver() {
+  // see comment in `graphic.isInEmphasis`
+  !graphic.isInEmphasis(this) && onEmphasis.call(this);
+}
 
-      this.animateTo({
-        scale: scale
-      }, 400, 'elasticOut');
-    };
+function onMouseOut() {
+  // see comment in `graphic.isInEmphasis`
+  !graphic.isInEmphasis(this) && onNormal.call(this);
+}
 
-    symbolPath.on('mouseover', onEmphasis).on('mouseout', onNormal).on('emphasis', onEmphasis).on('normal', onNormal);
+function onEmphasis() {
+  // Do not support this hover animation util some scenario required.
+  // Animation can only be supported in hover layer when using `el.incremetal`.
+  if (this.incremental || this.useHoverLayer) {
+    return;
   }
-};
+
+  var scale = this.__symbolOriginalScale;
+  var ratio = scale[1] / scale[0];
+  this.animateTo({
+    scale: [Math.max(scale[0] * 1.1, scale[0] + 3), Math.max(scale[1] * 1.1, scale[1] + 3 * ratio)]
+  }, 400, 'elasticOut');
+}
+
+function onNormal() {
+  if (this.incremental || this.useHoverLayer) {
+    return;
+  }
+
+  this.animateTo({
+    scale: this.__symbolOriginalScale
+  }, 400, 'elasticOut');
+}
 /**
  * @param {Function} cb
  * @param {Object} [opt]
diff --git a/builder/src/echarts/chart/tree/treeAction.js b/builder/src/echarts/chart/helper/focusNodeAdjacencyAction.js
similarity index 60%
copy from builder/src/echarts/chart/tree/treeAction.js
copy to builder/src/echarts/chart/helper/focusNodeAdjacencyAction.js
index e07ab5f..3f48775 100644
--- a/builder/src/echarts/chart/tree/treeAction.js
+++ b/builder/src/echarts/chart/helper/focusNodeAdjacencyAction.js
@@ -17,19 +17,28 @@
 * under the License.
 */
 import * as echarts from '../../echarts';
+/**
+ * @payload
+ * @property {number} [seriesIndex]
+ * @property {string} [seriesId]
+ * @property {string} [seriesName]
+ * @property {number} [dataIndex]
+ */
+
 echarts.registerAction({
-  type: 'treeExpandAndCollapse',
-  event: 'treeExpandAndCollapse',
-  update: 'update'
-}, function (payload, ecModel) {
-  ecModel.eachComponent({
-    mainType: 'series',
-    subType: 'tree',
-    query: payload
-  }, function (seriesModel) {
-    var dataIndex = payload.dataIndex;
-    var tree = seriesModel.getData().tree;
-    var node = tree.getNodeByDataIndex(dataIndex);
-    node.isExpand = !node.isExpand;
-  });
-});
\ No newline at end of file
+  type: 'focusNodeAdjacency',
+  event: 'focusNodeAdjacency',
+  update: 'series:focusNodeAdjacency'
+}, function () {});
+/**
+ * @payload
+ * @property {number} [seriesIndex]
+ * @property {string} [seriesId]
+ * @property {string} [seriesName]
+ */
+
+echarts.registerAction({
+  type: 'unfocusNodeAdjacency',
+  event: 'unfocusNodeAdjacency',
+  update: 'series:unfocusNodeAdjacency'
+}, function () {});
\ No newline at end of file
diff --git a/builder/src/echarts/chart/helper/treeHelper.js b/builder/src/echarts/chart/helper/treeHelper.js
index 37ccd98..d1064be 100644
--- a/builder/src/echarts/chart/helper/treeHelper.js
+++ b/builder/src/echarts/chart/helper/treeHelper.js
@@ -22,6 +22,10 @@ export function retrieveTargetInfo(payload, validPayloadTypes, seriesModel) {
     var root = seriesModel.getData().tree.root;
     var targetNode = payload.targetNode;
 
+    if (typeof targetNode === 'string') {
+      targetNode = root.getNodeById(targetNode);
+    }
+
     if (targetNode && root.contains(targetNode)) {
       return {
         node: targetNode
diff --git a/builder/src/echarts/chart/lines/LinesSeries.js b/builder/src/echarts/chart/lines/LinesSeries.js
index f821ea1..95ff6c1 100644
--- a/builder/src/echarts/chart/lines/LinesSeries.js
+++ b/builder/src/echarts/chart/lines/LinesSeries.js
@@ -16,6 +16,8 @@
 * specific language governing permissions and limitations
 * under the License.
 */
+
+/* global Uint32Array, Float64Array, Float32Array */
 import { __DEV__ } from '../../config';
 import SeriesModel from '../../model/Series';
 import List from '../../data/List';
diff --git a/builder/src/echarts/chart/lines/linesLayout.js b/builder/src/echarts/chart/lines/linesLayout.js
index eae8aba..5acf72c 100644
--- a/builder/src/echarts/chart/lines/linesLayout.js
+++ b/builder/src/echarts/chart/lines/linesLayout.js
@@ -16,6 +16,8 @@
 * specific language governing permissions and limitations
 * under the License.
 */
+
+/* global Float32Array */
 import createRenderPlanner from '../helper/createRenderPlanner';
 export default {
   seriesType: 'lines',
diff --git a/builder/src/echarts/chart/parallel/ParallelView.js b/builder/src/echarts/chart/parallel/ParallelView.js
index 099ed35..b7e57ab 100644
--- a/builder/src/echarts/chart/parallel/ParallelView.js
+++ b/builder/src/echarts/chart/parallel/ParallelView.js
@@ -17,7 +17,6 @@
 * under the License.
 */
 import * as graphic from '../../util/graphic';
-import * as zrUtil from 'zrender/src/core/util';
 import ChartView from '../../view/Chart';
 var DEFAULT_SMOOTH = 0.3;
 var ParallelView = ChartView.extend({
diff --git a/builder/src/echarts/chart/pie/PieView.js b/builder/src/echarts/chart/pie/PieView.js
index 7bcbcc7..cfb3742 100644
--- a/builder/src/echarts/chart/pie/PieView.js
+++ b/builder/src/echarts/chart/pie/PieView.js
@@ -285,6 +285,9 @@ var PieView = ChartView.extend({
       var r = Math.max(api.getWidth(), api.getHeight()) / 2;
       var removeClipPath = zrUtil.bind(group.removeClipPath, group);
       group.setClipPath(this._createClipPath(shape.cx, shape.cy, r, shape.startAngle, shape.clockwise, removeClipPath, seriesModel));
+    } else {
+      // clipPath is used in first-time animation, so remove it when otherwise. See: #8994
+      group.removeClipPath();
     }
 
     this._data = data;
diff --git a/builder/src/echarts/chart/sankey/SankeySeries.js b/builder/src/echarts/chart/sankey/SankeySeries.js
index a611c87..b640528 100644
--- a/builder/src/echarts/chart/sankey/SankeySeries.js
+++ b/builder/src/echarts/chart/sankey/SankeySeries.js
@@ -86,23 +86,34 @@ var SankeySeries = SeriesModel.extend({
 
     return SankeySeries.superCall(this, 'formatTooltip', dataIndex, multipleSeries);
   },
+  optionUpdated: function () {
+    var option = this.option;
+
+    if (option.focusNodeAdjacency === true) {
+      option.focusNodeAdjacency = 'allEdges';
+    }
+  },
   defaultOption: {
     zlevel: 0,
     z: 2,
     coordinateSystem: 'view',
     layout: null,
-    // the position of the whole view
+    // The position of the whole view
     left: '5%',
     top: '5%',
     right: '20%',
     bottom: '5%',
-    // the dx of the node
+    // Value can be 'vertical'
+    orient: 'horizontal',
+    // The dx of the node
     nodeWidth: 20,
-    // the vertical distance between two nodes
+    // The vertical distance between two nodes
     nodeGap: 8,
-    // control if the node can move or not
+    // Control if the node can move or not
     draggable: true,
-    // the number of iterations to change the position of the node
+    // Value can be 'inEdges', 'outEdges', 'allEdges', true (the same as 'allEdges').
+    focusNodeAdjacency: false,
+    // The number of iterations to change the position of the node
     layoutIterations: 32,
     label: {
       show: true,
diff --git a/builder/src/echarts/chart/sankey/SankeyView.js b/builder/src/echarts/chart/sankey/SankeyView.js
index a5ef410..e4cee73 100644
--- a/builder/src/echarts/chart/sankey/SankeyView.js
+++ b/builder/src/echarts/chart/sankey/SankeyView.js
@@ -23,6 +23,42 @@
  */
 import * as graphic from '../../util/graphic';
 import * as echarts from '../../echarts';
+import * as zrUtil from 'zrender/src/core/util';
+var nodeOpacityPath = ['itemStyle', 'opacity'];
+var lineOpacityPath = ['lineStyle', 'opacity'];
+
+function getItemOpacity(item, opacityPath) {
+  return item.getVisual('opacity') || item.getModel().get(opacityPath);
+}
+
+function fadeOutItem(item, opacityPath, opacityRatio) {
+  var el = item.getGraphicEl();
+  var opacity = getItemOpacity(item, opacityPath);
+
+  if (opacityRatio != null) {
+    opacity == null && (opacity = 1);
+    opacity *= opacityRatio;
+  }
+
+  el.downplay && el.downplay();
+  el.traverse(function (child) {
+    if (child.type !== 'group') {
+      child.setStyle('opacity', opacity);
+    }
+  });
+}
+
+function fadeInItem(item, opacityPath) {
+  var opacity = getItemOpacity(item, opacityPath);
+  var el = item.getGraphicEl();
+  el.highlight && el.highlight();
+  el.traverse(function (child) {
+    if (child.type !== 'group') {
+      child.setStyle('opacity', opacity);
+    }
+  });
+}
+
 var SankeyShape = graphic.extendShape({
   shape: {
     x1: 0,
@@ -33,14 +69,25 @@ var SankeyShape = graphic.extendShape({
     cpy1: 0,
     cpx2: 0,
     cpy2: 0,
-    extent: 0
+    extent: 0,
+    orient: ''
   },
   buildPath: function (ctx, shape) {
-    var halfExtent = shape.extent / 2;
-    ctx.moveTo(shape.x1, shape.y1 - halfExtent);
-    ctx.bezierCurveTo(shape.cpx1, shape.cpy1 - halfExtent, shape.cpx2, shape.cpy2 - halfExtent, shape.x2, shape.y2 - halfExtent);
-    ctx.lineTo(shape.x2, shape.y2 + halfExtent);
-    ctx.bezierCurveTo(shape.cpx2, shape.cpy2 + halfExtent, shape.cpx1, shape.cpy1 + halfExtent, shape.x1, shape.y1 + halfExtent);
+    var extent = shape.extent;
+    var orient = shape.orient;
+
+    if (orient === 'vertical') {
+      ctx.moveTo(shape.x1, shape.y1);
+      ctx.bezierCurveTo(shape.cpx1, shape.cpy1, shape.cpx2, shape.cpy2, shape.x2, shape.y2);
+      ctx.lineTo(shape.x2 + extent, shape.y2);
+      ctx.bezierCurveTo(shape.cpx2 + extent, shape.cpy2, shape.cpx1 + extent, shape.cpy1, shape.x1 + extent, shape.y1);
+    } else {
+      ctx.moveTo(shape.x1, shape.y1);
+      ctx.bezierCurveTo(shape.cpx1, shape.cpy1, shape.cpx2, shape.cpy2, shape.x2, shape.y2);
+      ctx.lineTo(shape.x2, shape.y2 + extent);
+      ctx.bezierCurveTo(shape.cpx2, shape.cpy2 + extent, shape.cpx1, shape.cpy1 + extent, shape.x1, shape.y1 + extent);
+    }
+
     ctx.closePath();
   }
 });
@@ -52,7 +99,14 @@ export default echarts.extendChartView({
    * @type {module:echarts/chart/sankey/SankeySeries}
    */
   _model: null,
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  _focusAdjacencyDisabled: false,
   render: function (seriesModel, ecModel, api) {
+    var sankeyView = this;
     var graph = seriesModel.getGraph();
     var group = this.group;
     var layoutInfo = seriesModel.layoutInfo; // view width
@@ -62,6 +116,7 @@ export default echarts.extendChartView({
     var height = layoutInfo.height;
     var nodeData = seriesModel.getData();
     var edgeData = seriesModel.getData('edge');
+    var orient = seriesModel.get('orient');
     this._model = seriesModel;
     group.removeAll();
     group.attr('position', [layoutInfo.x, layoutInfo.y]); // generate a bezire Curve for each edge
@@ -82,15 +137,37 @@ export default echarts.extendChartView({
       var dragX2 = node2Model.get('localX');
       var dragY2 = node2Model.get('localY');
       var edgeLayout = edge.getLayout();
+      var x1;
+      var y1;
+      var x2;
+      var y2;
+      var cpx1;
+      var cpy1;
+      var cpx2;
+      var cpy2;
       curve.shape.extent = Math.max(1, edgeLayout.dy);
-      var x1 = (dragX1 != null ? dragX1 * width : n1Layout.x) + n1Layout.dx;
-      var y1 = (dragY1 != null ? dragY1 * height : n1Layout.y) + edgeLayout.sy + edgeLayout.dy / 2;
-      var x2 = dragX2 != null ? dragX2 * width : n2Layout.x;
-      var y2 = (dragY2 != null ? dragY2 * height : n2Layout.y) + edgeLayout.ty + edgeLayout.dy / 2;
-      var cpx1 = x1 * (1 - curvature) + x2 * curvature;
-      var cpy1 = y1;
-      var cpx2 = x1 * curvature + x2 * (1 - curvature);
-      var cpy2 = y2;
+      curve.shape.orient = orient;
+
+      if (orient === 'vertical') {
+        x1 = (dragX1 != null ? dragX1 * width : n1Layout.x) + edgeLayout.sy;
+        y1 = (dragY1 != null ? dragY1 * height : n1Layout.y) + n1Layout.dy;
+        x2 = (dragX2 != null ? dragX2 * width : n2Layout.x) + edgeLayout.ty;
+        y2 = dragY2 != null ? dragY2 * height : n2Layout.y;
+        cpx1 = x1;
+        cpy1 = y1 * (1 - curvature) + y2 * curvature;
+        cpx2 = x2;
+        cpy2 = y1 * curvature + y2 * (1 - curvature);
+      } else {
+        x1 = (dragX1 != null ? dragX1 * width : n1Layout.x) + n1Layout.dx;
+        y1 = (dragY1 != null ? dragY1 * height : n1Layout.y) + edgeLayout.sy;
+        x2 = dragX2 != null ? dragX2 * width : n2Layout.x;
+        y2 = (dragY2 != null ? dragY2 * height : n2Layout.y) + edgeLayout.ty;
+        cpx1 = x1 * (1 - curvature) + x2 * curvature;
+        cpy1 = y1;
+        cpx2 = x1 * curvature + x2 * (1 - curvature);
+        cpy2 = y2;
+      }
+
       curve.setShape({
         x1: x1,
         y1: y1,
@@ -116,7 +193,7 @@ export default echarts.extendChartView({
       graphic.setHoverStyle(curve, edge.getModel('emphasis.lineStyle').getItemStyle());
       group.add(curve);
       edgeData.setItemGraphicEl(edge.dataIndex, curve);
-    }); // generate a rect for each node
+    }); // Generate a rect for each node
 
     graph.eachNode(function (node) {
       var layout = node.getLayout();
@@ -147,11 +224,12 @@ export default echarts.extendChartView({
       nodeData.setItemGraphicEl(node.dataIndex, rect);
       rect.dataType = 'node';
     });
-    var draggable = seriesModel.get('draggable');
+    nodeData.eachItemGraphicEl(function (el, dataIndex) {
+      var itemModel = nodeData.getItemModel(dataIndex);
 
-    if (draggable) {
-      nodeData.eachItemGraphicEl(function (el, dataIndex) {
+      if (itemModel.get('draggable')) {
         el.drift = function (dx, dy) {
+          sankeyView._focusAdjacencyDisabled = true;
           this.shape.x += dx;
           this.shape.y += dy;
           this.dirty();
@@ -164,10 +242,57 @@ export default echarts.extendChartView({
           });
         };
 
+        el.ondragend = function () {
+          sankeyView._focusAdjacencyDisabled = false;
+        };
+
         el.draggable = true;
         el.cursor = 'move';
-      });
-    }
+      }
+
+      if (itemModel.get('focusNodeAdjacency')) {
+        el.off('mouseover').on('mouseover', function () {
+          if (!sankeyView._focusAdjacencyDisabled) {
+            api.dispatchAction({
+              type: 'focusNodeAdjacency',
+              seriesId: seriesModel.id,
+              dataIndex: el.dataIndex
+            });
+          }
+        });
+        el.off('mouseout').on('mouseout', function () {
+          if (!sankeyView._focusAdjacencyDisabled) {
+            api.dispatchAction({
+              type: 'unfocusNodeAdjacency',
+              seriesId: seriesModel.id
+            });
+          }
+        });
+      }
+    });
+    edgeData.eachItemGraphicEl(function (el, dataIndex) {
+      var edgeModel = edgeData.getItemModel(dataIndex);
+
+      if (edgeModel.get('focusNodeAdjacency')) {
+        el.off('mouseover').on('mouseover', function () {
+          if (!sankeyView._focusAdjacencyDisabled) {
+            api.dispatchAction({
+              type: 'focusNodeAdjacency',
+              seriesId: seriesModel.id,
+              edgeDataIndex: el.dataIndex
+            });
+          }
+        });
+        el.off('mouseout').on('mouseout', function () {
+          if (!sankeyView._focusAdjacencyDisabled) {
+            api.dispatchAction({
+              type: 'unfocusNodeAdjacency',
+              seriesId: seriesModel.id
+            });
+          }
+        });
+      }
+    });
 
     if (!this._data && seriesModel.get('animation')) {
       group.setClipPath(createGridClipShape(group.getBoundingRect(), seriesModel, function () {
@@ -177,8 +302,80 @@ export default echarts.extendChartView({
 
     this._data = seriesModel.getData();
   },
-  dispose: function () {}
-}); // add animation to the view
+  dispose: function () {},
+  focusNodeAdjacency: function (seriesModel, ecModel, api, payload) {
+    var data = this._model.getData();
+
+    var graph = data.graph;
+    var dataIndex = payload.dataIndex;
+    var itemModel = data.getItemModel(dataIndex);
+    var edgeDataIndex = payload.edgeDataIndex;
+
+    if (dataIndex == null && edgeDataIndex == null) {
+      return;
+    }
+
+    var node = graph.getNodeByIndex(dataIndex);
+    var edge = graph.getEdgeByIndex(edgeDataIndex);
+    graph.eachNode(function (node) {
+      fadeOutItem(node, nodeOpacityPath, 0.1);
+    });
+    graph.eachEdge(function (edge) {
+      fadeOutItem(edge, lineOpacityPath, 0.1);
+    });
+
+    if (node) {
+      fadeInItem(node, nodeOpacityPath);
+      var focusNodeAdj = itemModel.get('focusNodeAdjacency');
+
+      if (focusNodeAdj === 'outEdges') {
+        zrUtil.each(node.outEdges, function (edge) {
+          if (edge.dataIndex < 0) {
+            return;
+          }
+
+          fadeInItem(edge, lineOpacityPath);
+          fadeInItem(edge.node2, nodeOpacityPath);
+        });
+      } else if (focusNodeAdj === 'inEdges') {
+        zrUtil.each(node.inEdges, function (edge) {
+          if (edge.dataIndex < 0) {
+            return;
+          }
+
+          fadeInItem(edge, lineOpacityPath);
+          fadeInItem(edge.node1, nodeOpacityPath);
+        });
+      } else if (focusNodeAdj === 'allEdges') {
+        zrUtil.each(node.edges, function (edge) {
+          if (edge.dataIndex < 0) {
+            return;
+          }
+
+          fadeInItem(edge, lineOpacityPath);
+          fadeInItem(edge.node1, nodeOpacityPath);
+          fadeInItem(edge.node2, nodeOpacityPath);
+        });
+      }
+    }
+
+    if (edge) {
+      fadeInItem(edge, lineOpacityPath);
+      fadeInItem(edge.node1, nodeOpacityPath);
+      fadeInItem(edge.node2, nodeOpacityPath);
+    }
+  },
+  unfocusNodeAdjacency: function (seriesModel, ecModel, api, payload) {
+    var graph = this._model.getGraph();
+
+    graph.eachNode(function (node) {
+      fadeOutItem(node, nodeOpacityPath);
+    });
+    graph.eachEdge(function (edge) {
+      fadeOutItem(edge, lineOpacityPath);
+    });
+  }
+}); // Add animation to the view
 
 function createGridClipShape(rect, seriesModel, cb) {
   var rectEl = new graphic.Rect({
diff --git a/builder/src/echarts/chart/sankey/sankeyAction.js b/builder/src/echarts/chart/sankey/sankeyAction.js
index c63d0cb..ac171cf 100644
--- a/builder/src/echarts/chart/sankey/sankeyAction.js
+++ b/builder/src/echarts/chart/sankey/sankeyAction.js
@@ -16,7 +16,13 @@
 * specific language governing permissions and limitations
 * under the License.
 */
+
+/**
+ * @file The interactive action of sankey view
+ * @author Deqing Li(annong035@gmail.com)
+ */
 import * as echarts from '../../echarts';
+import '../helper/focusNodeAdjacencyAction';
 echarts.registerAction({
   type: 'dragNode',
   event: 'dragNode',
diff --git a/builder/src/echarts/chart/sankey/sankeyLayout.js b/builder/src/echarts/chart/sankey/sankeyLayout.js
index fc5f1f6..e94f604 100644
--- a/builder/src/echarts/chart/sankey/sankeyLayout.js
+++ b/builder/src/echarts/chart/sankey/sankeyLayout.js
@@ -22,7 +22,7 @@
  * @author Deqing Li(annong035@gmail.com)
  */
 import * as layout from '../../util/layout';
-import nest from '../../util/array/nest';
+import nest from '../../util/nest';
 import * as zrUtil from 'zrender/src/core/util';
 import { __DEV__ } from '../../config';
 export default function (ecModel, api, payload) {
@@ -41,7 +41,8 @@ export default function (ecModel, api, payload) {
       return node.getLayout().value === 0;
     });
     var iterations = filteredNodes.length !== 0 ? 0 : seriesModel.get('layoutIterations');
-    layoutSankey(nodes, edges, nodeWidth, nodeGap, width, height, iterations);
+    var orient = seriesModel.get('orient');
+    layoutSankey(nodes, edges, nodeWidth, nodeGap, width, height, iterations, orient);
   });
 }
 /**
@@ -59,10 +60,10 @@ function getViewRect(seriesModel, api) {
   });
 }
 
-function layoutSankey(nodes, edges, nodeWidth, nodeGap, width, height, iterations) {
-  computeNodeBreadths(nodes, edges, nodeWidth, width);
-  computeNodeDepths(nodes, edges, height, nodeGap, iterations);
-  computeEdgeDepths(nodes);
+function layoutSankey(nodes, edges, nodeWidth, nodeGap, width, height, iterations, orient) {
+  computeNodeBreadths(nodes, edges, nodeWidth, width, height, orient);
+  computeNodeDepths(nodes, edges, height, width, nodeGap, iterations, orient);
+  computeEdgeDepths(nodes, orient);
 }
 /**
  * Compute the value of each node by summing the associated edge's value
@@ -83,7 +84,7 @@ function computeNodeValues(nodes) {
 }
 /**
  * Compute the x-position for each node.
- * 
+ *
  * Here we use Kahn algorithm to detect cycle when we traverse
  * the node to computer the initial x position.
  *
@@ -93,7 +94,7 @@ function computeNodeValues(nodes) {
  */
 
 
-function computeNodeBreadths(nodes, edges, nodeWidth, width) {
+function computeNodeBreadths(nodes, edges, nodeWidth, width, height, orient) {
   // Used to mark whether the edge is deleted. if it is deleted,
   // the value is 0, otherwise it is 1.
   var remainEdges = []; // Storage each node's indegree.
@@ -109,7 +110,7 @@ function computeNodeBreadths(nodes, edges, nodeWidth, width) {
     remainEdges[i] = 1;
   }
 
-  for (var i = 0; i < nodes.length; i++) {
+  for (i = 0; i < nodes.length; i++) {
     indegreeArr[i] = nodes[i].inEdges.length;
 
     if (indegreeArr[i] === 0) {
@@ -118,14 +119,27 @@ function computeNodeBreadths(nodes, edges, nodeWidth, width) {
   }
 
   while (zeroIndegrees.length) {
-    zrUtil.each(zeroIndegrees, function (node) {
-      node.setLayout({
-        x: x
-      }, true);
-      node.setLayout({
-        dx: nodeWidth
-      }, true);
-      zrUtil.each(node.outEdges, function (edge) {
+    for (var idx = 0; idx < zeroIndegrees.length; idx++) {
+      var node = zeroIndegrees[idx];
+
+      if (orient === 'vertical') {
+        node.setLayout({
+          y: x
+        }, true);
+        node.setLayout({
+          dy: nodeWidth
+        }, true);
+      } else {
+        node.setLayout({
+          x: x
+        }, true);
+        node.setLayout({
+          dx: nodeWidth
+        }, true);
+      }
+
+      for (var oidx = 0; oidx < node.outEdges.length; oidx++) {
+        var edge = node.outEdges[oidx];
         var indexEdge = edges.indexOf(edge);
         remainEdges[indexEdge] = 0;
         var targetNode = edge.node2;
@@ -134,18 +148,25 @@ function computeNodeBreadths(nodes, edges, nodeWidth, width) {
         if (--indegreeArr[nodeIndex] === 0) {
           nextNode.push(targetNode);
         }
-      });
-    });
+      }
+    }
+
     ++x;
     zeroIndegrees = nextNode;
     nextNode = [];
   }
 
-  for (var i = 0; i < remainEdges.length; i++) {}
+  for (i = 0; i < remainEdges.length; i++) {}
+
+  moveSinksRight(nodes, x, orient);
 
-  moveSinksRight(nodes, x);
-  kx = (width - nodeWidth) / (x - 1);
-  scaleNodeBreadths(nodes, kx);
+  if (orient === 'vertical') {
+    kx = (height - nodeWidth) / (x - 1);
+  } else {
+    kx = (width - nodeWidth) / (x - 1);
+  }
+
+  scaleNodeBreadths(nodes, kx, orient);
 }
 /**
  * All the node without outEgdes are assigned maximum x-position and
@@ -157,12 +178,18 @@ function computeNodeBreadths(nodes, edges, nodeWidth, width) {
  */
 
 
-function moveSinksRight(nodes, x) {
+function moveSinksRight(nodes, x, orient) {
   zrUtil.each(nodes, function (node) {
     if (!node.outEdges.length) {
-      node.setLayout({
-        x: x - 1
-      }, true);
+      if (orient === 'vertical') {
+        node.setLayout({
+          y: x - 1
+        }, true);
+      } else {
+        node.setLayout({
+          x: x - 1
+        }, true);
+      }
     }
   });
 }
@@ -174,12 +201,19 @@ function moveSinksRight(nodes, x) {
  */
 
 
-function scaleNodeBreadths(nodes, kx) {
+function scaleNodeBreadths(nodes, kx, orient) {
   zrUtil.each(nodes, function (node) {
-    var nodeX = node.getLayout().x * kx;
-    node.setLayout({
-      x: nodeX
-    }, true);
+    if (orient === 'vertical') {
+      var nodeY = node.getLayout().y * kx;
+      node.setLayout({
+        y: nodeY
+      }, true);
+    } else {
+      var nodeX = node.getLayout().x * kx;
+      node.setLayout({
+        x: nodeX
+      }, true);
+    }
   });
 }
 /**
@@ -194,24 +228,36 @@ function scaleNodeBreadths(nodes, kx) {
  */
 
 
-function computeNodeDepths(nodes, edges, height, nodeGap, iterations) {
-  var nodesByBreadth = nest().key(function (d) {
-    return d.getLayout().x;
-  }).sortKeys(ascending).entries(nodes).map(function (d) {
+function computeNodeDepths(nodes, edges, height, width, nodeGap, iterations, orient) {
+  var nodesByBreadth = nest().key(getKeyFunction(orient)).sortKeys(function (a, b) {
+    return a - b;
+  }).entries(nodes).map(function (d) {
     return d.values;
   });
-  initializeNodeDepth(nodes, nodesByBreadth, edges, height, nodeGap);
-  resolveCollisions(nodesByBreadth, nodeGap, height);
+  initializeNodeDepth(nodes, nodesByBreadth, edges, height, width, nodeGap, orient);
+  resolveCollisions(nodesByBreadth, nodeGap, height, width, orient);
 
   for (var alpha = 1; iterations > 0; iterations--) {
     // 0.99 is a experience parameter, ensure that each iterations of
     // changes as small as possible.
     alpha *= 0.99;
-    relaxRightToLeft(nodesByBreadth, alpha);
-    resolveCollisions(nodesByBreadth, nodeGap, height);
-    relaxLeftToRight(nodesByBreadth, alpha);
-    resolveCollisions(nodesByBreadth, nodeGap, height);
+    relaxRightToLeft(nodesByBreadth, alpha, orient);
+    resolveCollisions(nodesByBreadth, nodeGap, height, width, orient);
+    relaxLeftToRight(nodesByBreadth, alpha, orient);
+    resolveCollisions(nodesByBreadth, nodeGap, height, width, orient);
+  }
+}
+
+function getKeyFunction(orient) {
+  if (orient === 'vertical') {
+    return function (d) {
+      return d.getLayout().y;
+    };
   }
+
+  return function (d) {
+    return d.getLayout().x;
+  };
 }
 /**
  * Compute the original y-position for each node
@@ -225,15 +271,22 @@ function computeNodeDepths(nodes, edges, height, nodeGap, iterations) {
  */
 
 
-function initializeNodeDepth(nodes, nodesByBreadth, edges, height, nodeGap) {
+function initializeNodeDepth(nodes, nodesByBreadth, edges, height, width, nodeGap, orient) {
   var kyArray = [];
   zrUtil.each(nodesByBreadth, function (nodes) {
     var n = nodes.length;
     var sum = 0;
+    var ky = 0;
     zrUtil.each(nodes, function (node) {
       sum += node.getLayout().value;
     });
-    var ky = (height - (n - 1) * nodeGap) / sum;
+
+    if (orient === 'vertical') {
+      ky = (width - (n - 1) * nodeGap) / sum;
+    } else {
+      ky = (height - (n - 1) * nodeGap) / sum;
+    }
+
     kyArray.push(ky);
   });
   kyArray.sort(function (a, b) {
@@ -242,13 +295,23 @@ function initializeNodeDepth(nodes, nodesByBreadth, edges, height, nodeGap) {
   var ky0 = kyArray[0];
   zrUtil.each(nodesByBreadth, function (nodes) {
     zrUtil.each(nodes, function (node, i) {
-      node.setLayout({
-        y: i
-      }, true);
       var nodeDy = node.getLayout().value * ky0;
-      node.setLayout({
-        dy: nodeDy
-      }, true);
+
+      if (orient === 'vertical') {
+        node.setLayout({
+          x: i
+        }, true);
+        node.setLayout({
+          dx: nodeDy
+        }, true);
+      } else {
+        node.setLayout({
+          y: i
+        }, true);
+        node.setLayout({
+          dy: nodeDy
+        }, true);
+      }
     });
   });
   zrUtil.each(edges, function (edge) {
@@ -268,51 +331,101 @@ function initializeNodeDepth(nodes, nodesByBreadth, edges, height, nodeGap) {
  */
 
 
-function resolveCollisions(nodesByBreadth, nodeGap, height) {
+function resolveCollisions(nodesByBreadth, nodeGap, height, width, orient) {
   zrUtil.each(nodesByBreadth, function (nodes) {
     var node;
     var dy;
     var y0 = 0;
     var n = nodes.length;
     var i;
-    nodes.sort(ascendingDepth);
 
-    for (i = 0; i < n; i++) {
-      node = nodes[i];
-      dy = y0 - node.getLayout().y;
+    if (orient === 'vertical') {
+      var nodeX;
+      nodes.sort(function (a, b) {
+        return a.getLayout().x - b.getLayout().x;
+      });
+
+      for (i = 0; i < n; i++) {
+        node = nodes[i];
+        dy = y0 - node.getLayout().x;
+
+        if (dy > 0) {
+          nodeX = node.getLayout().x + dy;
+          node.setLayout({
+            x: nodeX
+          }, true);
+        }
+
+        y0 = node.getLayout().x + node.getLayout().dx + nodeGap;
+      } // If the bottommost node goes outside the bounds, push it back up
+
+
+      dy = y0 - nodeGap - width;
 
       if (dy > 0) {
-        var nodeY = node.getLayout().y + dy;
+        nodeX = node.getLayout().x - dy;
         node.setLayout({
-          y: nodeY
+          x: nodeX
         }, true);
-      }
-
-      y0 = node.getLayout().y + node.getLayout().dy + nodeGap;
-    } // If the bottommost node goes outside the bounds, push it back up
+        y0 = nodeX;
 
+        for (i = n - 2; i >= 0; --i) {
+          node = nodes[i];
+          dy = node.getLayout().x + node.getLayout().dx + nodeGap - y0;
 
-    dy = y0 - nodeGap - height;
+          if (dy > 0) {
+            nodeX = node.getLayout().x - dy;
+            node.setLayout({
+              x: nodeX
+            }, true);
+          }
 
-    if (dy > 0) {
-      var nodeY = node.getLayout().y - dy;
-      node.setLayout({
-        y: nodeY
-      }, true);
-      y0 = node.getLayout().y;
+          y0 = node.getLayout().x;
+        }
+      }
+    } else {
+      var nodeY;
+      nodes.sort(function (a, b) {
+        return a.getLayout().y - b.getLayout().y;
+      });
 
-      for (i = n - 2; i >= 0; --i) {
+      for (i = 0; i < n; i++) {
         node = nodes[i];
-        dy = node.getLayout().y + node.getLayout().dy + nodeGap - y0;
+        dy = y0 - node.getLayout().y;
 
         if (dy > 0) {
-          nodeY = node.getLayout().y - dy;
+          nodeY = node.getLayout().y + dy;
           node.setLayout({
             y: nodeY
           }, true);
         }
 
-        y0 = node.getLayout().y;
+        y0 = node.getLayout().y + node.getLayout().dy + nodeGap;
+      } // If the bottommost node goes outside the bounds, push it back up
+
+
+      dy = y0 - nodeGap - height;
+
+      if (dy > 0) {
+        nodeY = node.getLayout().y - dy;
+        node.setLayout({
+          y: nodeY
+        }, true);
+        y0 = nodeY;
+
+        for (i = n - 2; i >= 0; --i) {
+          node = nodes[i];
+          dy = node.getLayout().y + node.getLayout().dy + nodeGap - y0;
+
+          if (dy > 0) {
+            nodeY = node.getLayout().y - dy;
+            node.setLayout({
+              y: nodeY
+            }, true);
+          }
+
+          y0 = node.getLayout().y;
+        }
       }
     }
   });
@@ -326,22 +439,62 @@ function resolveCollisions(nodesByBreadth, nodeGap, height) {
  */
 
 
-function relaxRightToLeft(nodesByBreadth, alpha) {
+function relaxRightToLeft(nodesByBreadth, alpha, orient) {
   zrUtil.each(nodesByBreadth.slice().reverse(), function (nodes) {
     zrUtil.each(nodes, function (node) {
       if (node.outEdges.length) {
-        var y = sum(node.outEdges, weightedTarget) / sum(node.outEdges, getEdgeValue);
-        var nodeY = node.getLayout().y + (y - center(node)) * alpha;
-        node.setLayout({
-          y: nodeY
-        }, true);
+        var y = sum(node.outEdges, weightedTarget, orient) / sum(node.outEdges, getEdgeValue, orient);
+
+        if (orient === 'vertical') {
+          var nodeX = node.getLayout().x + (y - center(node, orient)) * alpha;
+          node.setLayout({
+            x: nodeX
+          }, true);
+        } else {
+          var nodeY = node.getLayout().y + (y - center(node, orient)) * alpha;
+          node.setLayout({
+            y: nodeY
+          }, true);
+        }
       }
     });
   });
 }
 
-function weightedTarget(edge) {
-  return center(edge.node2) * edge.getValue();
+function weightedTarget(edge, orient) {
+  return center(edge.node2, orient) * edge.getValue();
+}
+
+function weightedSource(edge, orient) {
+  return center(edge.node1, orient) * edge.getValue();
+}
+
+function center(node, orient) {
+  if (orient === 'vertical') {
+    return node.getLayout().x + node.getLayout().dx / 2;
+  }
+
+  return node.getLayout().y + node.getLayout().dy / 2;
+}
+
+function getEdgeValue(edge) {
+  return edge.getValue();
+}
+
+function sum(array, f, orient) {
+  var sum = 0;
+  var len = array.length;
+  var i = -1;
+
+  while (++i < len) {
+    var value = +f.call(array, array[i], orient);
+
+    if (!isNaN(value)) {
+      sum += value;
+    }
+  }
+
+  return sum;
 }
 /**
  * Change the y-position of the nodes, except most the left side nodes
@@ -352,23 +505,27 @@ function weightedTarget(edge) {
  */
 
 
-function relaxLeftToRight(nodesByBreadth, alpha) {
+function relaxLeftToRight(nodesByBreadth, alpha, orient) {
   zrUtil.each(nodesByBreadth, function (nodes) {
     zrUtil.each(nodes, function (node) {
       if (node.inEdges.length) {
-        var y = sum(node.inEdges, weightedSource) / sum(node.inEdges, getEdgeValue);
-        var nodeY = node.getLayout().y + (y - center(node)) * alpha;
-        node.setLayout({
-          y: nodeY
-        }, true);
+        var y = sum(node.inEdges, weightedSource, orient) / sum(node.inEdges, getEdgeValue, orient);
+
+        if (orient === 'vertical') {
+          var nodeX = node.getLayout().x + (y - center(node, orient)) * alpha;
+          node.setLayout({
+            x: nodeX
+          }, true);
+        } else {
+          var nodeY = node.getLayout().y + (y - center(node, orient)) * alpha;
+          node.setLayout({
+            y: nodeY
+          }, true);
+        }
       }
     });
   });
 }
-
-function weightedSource(edge) {
-  return center(edge.node1) * edge.getValue();
-}
 /**
  * Compute the depth(y-position) of each edge
  *
@@ -376,10 +533,23 @@ function weightedSource(edge) {
  */
 
 
-function computeEdgeDepths(nodes) {
+function computeEdgeDepths(nodes, orient) {
   zrUtil.each(nodes, function (node) {
-    node.outEdges.sort(ascendingTargetDepth);
-    node.inEdges.sort(ascendingSourceDepth);
+    if (orient === 'vertical') {
+      node.outEdges.sort(function (a, b) {
+        return a.node2.getLayout().x - b.node2.getLayout().x;
+      });
+      node.inEdges.sort(function (a, b) {
+        return a.node1.getLayout().x - b.node1.getLayout().x;
+      });
+    } else {
+      node.outEdges.sort(function (a, b) {
+        return a.node2.getLayout().y - b.node2.getLayout().y;
+      });
+      node.inEdges.sort(function (a, b) {
+        return a.node1.getLayout().y - b.node1.getLayout().y;
+      });
+    }
   });
   zrUtil.each(nodes, function (node) {
     var sy = 0;
@@ -397,44 +567,4 @@ function computeEdgeDepths(nodes) {
       ty += edge.getLayout().dy;
     });
   });
-}
-
-function ascendingTargetDepth(a, b) {
-  return a.node2.getLayout().y - b.node2.getLayout().y;
-}
-
-function ascendingSourceDepth(a, b) {
-  return a.node1.getLayout().y - b.node1.getLayout().y;
-}
-
-function sum(array, f) {
-  var sum = 0;
-  var len = array.length;
-  var i = -1;
-
-  while (++i < len) {
-    var value = +f.call(array, array[i], i);
-
-    if (!isNaN(value)) {
-      sum += value;
-    }
-  }
-
-  return sum;
-}
-
-function center(node) {
-  return node.getLayout().y + node.getLayout().dy / 2;
-}
-
-function ascendingDepth(a, b) {
-  return a.getLayout().y - b.getLayout().y;
-}
-
-function ascending(a, b) {
-  return a - b;
-}
-
-function getEdgeValue(edge) {
-  return edge.getValue();
 }
\ No newline at end of file
diff --git a/builder/src/echarts/chart/sunburst/SunburstSeries.js b/builder/src/echarts/chart/sunburst/SunburstSeries.js
index 057c3a7..7cd363e 100644
--- a/builder/src/echarts/chart/sunburst/SunburstSeries.js
+++ b/builder/src/echarts/chart/sunburst/SunburstSeries.js
@@ -93,6 +93,11 @@ export default SeriesModel.extend({
     itemStyle: {
       borderWidth: 1,
       borderColor: 'white',
+      borderType: 'solid',
+      shadowBlur: 0,
+      shadowColor: 'rgba(0, 0, 0, 0.2)',
+      shadowOffsetX: 0,
+      shadowOffsetY: 0,
       opacity: 1,
       emphasis: {},
       highlight: {
diff --git a/builder/src/echarts/chart/themeRiver/ThemeRiverSeries.js b/builder/src/echarts/chart/themeRiver/ThemeRiverSeries.js
index 250e89c..21f8238 100644
--- a/builder/src/echarts/chart/themeRiver/ThemeRiverSeries.js
+++ b/builder/src/echarts/chart/themeRiver/ThemeRiverSeries.js
@@ -27,7 +27,7 @@ import { getDimensionTypeByAxis } from '../../data/helper/dimensionHelper';
 import List from '../../data/List';
 import * as zrUtil from 'zrender/src/core/util';
 import { encodeHTML } from '../../util/format';
-import nest from '../../util/array/nest';
+import nest from '../../util/nest';
 var DATA_NAME_INDEX = 2;
 var ThemeRiverSeries = SeriesModel.extend({
   type: 'series.themeRiver',
@@ -43,6 +43,7 @@ var ThemeRiverSeries = SeriesModel.extend({
    * @override
    */
   init: function (option) {
+    // eslint-disable-next-line
     ThemeRiverSeries.superApply(this, 'init', arguments); // Put this function here is for the sake of consistency of code style.
     // Enable legend selection for each data item
     // Use a function instead of direct access because data reference may changed
diff --git a/builder/src/echarts/chart/themeRiver/themeRiverVisual.js b/builder/src/echarts/chart/themeRiver/themeRiverVisual.js
index 2c0e514..a78b591 100644
--- a/builder/src/echarts/chart/themeRiver/themeRiverVisual.js
+++ b/builder/src/echarts/chart/themeRiver/themeRiverVisual.js
@@ -19,7 +19,7 @@
 
 /**
  * @file Visual encoding for themeRiver view
- * @author  Deqing Li(annong035@gmail.com)
+ * @author Deqing Li(annong035@gmail.com)
  */
 import { createHashMap } from 'zrender/src/core/util';
 export default function (ecModel) {
diff --git a/builder/src/echarts/chart/tree/TreeSeries.js b/builder/src/echarts/chart/tree/TreeSeries.js
index be2f9d0..d8e79a5 100644
--- a/builder/src/echarts/chart/tree/TreeSeries.js
+++ b/builder/src/echarts/chart/tree/TreeSeries.js
@@ -19,6 +19,7 @@
 
 /**
  * @file Create data struct and define tree view's series model
+ * @author Deqing Li(annong035@gmail.com)
  */
 import SeriesModel from '../../model/Series';
 import Tree from '../../data/Tree';
@@ -54,7 +55,7 @@ export default SeriesModel.extend({
     var expandAndCollapse = option.expandAndCollapse;
     var expandTreeDepth = expandAndCollapse && option.initialTreeDepth >= 0 ? option.initialTreeDepth : treeDepth;
     tree.root.eachNode('preorder', function (node) {
-      var item = node.hostTree.data.getRawDataItem(node.dataIndex); // add item.collapsed != null, because users can collapse node original in the series.data.
+      var item = node.hostTree.data.getRawDataItem(node.dataIndex); // Add item.collapsed != null, because users can collapse node original in the series.data.
 
       node.isExpand = item && item.collapsed != null ? !item.collapsed : node.depth <= expandTreeDepth;
     });
@@ -76,6 +77,12 @@ export default SeriesModel.extend({
 
     return orient;
   },
+  setZoom: function (zoom) {
+    this.option.zoom = zoom;
+  },
+  setCenter: function (center) {
+    this.option.center = center;
+  },
 
   /**
    * @override
@@ -98,6 +105,7 @@ export default SeriesModel.extend({
   defaultOption: {
     zlevel: 0,
     z: 2,
+    coordinateSystem: 'view',
     // the position of the whole view
     left: '12%',
     top: '12%',
@@ -105,6 +113,13 @@ export default SeriesModel.extend({
     bottom: '12%',
     // the layout of the tree, two value can be selected, 'orthogonal' or 'radial'
     layout: 'orthogonal',
+    roam: false,
+    // true | false | 'move' | 'scale', see module:component/helper/RoamController.
+    // Symbol size scale ratio in roam
+    nodeScaleRatio: 0.4,
+    // Default on center of graph
+    center: null,
+    zoom: 1,
     // The orient of orthoginal layout, can be setted to 'LR', 'TB', 'RL', 'BT'.
     // and the backward compatibility configuration 'horizontal = LR', 'vertical = TB'.
     orient: 'LR',
diff --git a/builder/src/echarts/chart/tree/TreeView.js b/builder/src/echarts/chart/tree/TreeView.js
index e4c43ee..8501ada 100644
--- a/builder/src/echarts/chart/tree/TreeView.js
+++ b/builder/src/echarts/chart/tree/TreeView.js
@@ -18,13 +18,19 @@
 */
 
 /**
- * @file  This file used to draw tree view
+ * @file This file used to draw tree view.
+ * @author Deqing Li(annong035@gmail.com)
  */
 import * as zrUtil from 'zrender/src/core/util';
 import * as graphic from '../../util/graphic';
 import SymbolClz from '../helper/Symbol';
 import { radialCoordinate } from './layoutHelper';
 import * as echarts from '../../echarts';
+import * as bbox from 'zrender/src/core/bbox';
+import View from '../../coord/View';
+import * as roamHelper from '../../component/helper/roamHelper';
+import RoamController from '../../component/helper/RoamController';
+import { onIrrelevantElement } from '../../component/helper/cursorHelper';
 export default echarts.extendChartView({
   type: 'tree',
 
@@ -46,6 +52,15 @@ export default echarts.extendChartView({
      */
 
     this._mainGroup = new graphic.Group();
+    /**
+     * @private
+     * @type {module:echarts/componet/helper/RoamController}
+     */
+
+    this._controller = new RoamController(api.getZr());
+    this._controllerHost = {
+      target: this.group
+    };
     this.group.add(this._mainGroup);
   },
   render: function (seriesModel, ecModel, api, payload) {
@@ -60,6 +75,10 @@ export default echarts.extendChartView({
       group.attr('position', [layoutInfo.x, layoutInfo.y]);
     }
 
+    this._updateViewCoordSys(seriesModel);
+
+    this._updateController(seriesModel, ecModel, api);
+
     var oldData = this._data;
     var seriesScope = {
       expandAndCollapse: seriesModel.get('expandAndCollapse'),
@@ -74,7 +93,7 @@ export default echarts.extendChartView({
     };
     data.diff(oldData).add(function (newIdx) {
       if (symbolNeedsDraw(data, newIdx)) {
-        // create node and edge
+        // Create node and edge
         updateNode(data, newIdx, null, group, seriesModel, seriesScope);
       }
     }).update(function (newIdx, oldIdx) {
@@ -83,7 +102,7 @@ export default echarts.extendChartView({
       if (!symbolNeedsDraw(data, newIdx)) {
         symbolEl && removeNode(oldData, oldIdx, symbolEl, group, seriesModel, seriesScope);
         return;
-      } // update  node and edge
+      } // Update node and edge
 
 
       updateNode(data, newIdx, symbolEl, group, seriesModel, seriesScope);
@@ -98,6 +117,9 @@ export default echarts.extendChartView({
         removeNode(oldData, oldIdx, symbolEl, group, seriesModel, seriesScope);
       }
     }).execute();
+    this._nodeScaleRatio = seriesModel.get('nodeScaleRatio');
+
+    this._updateNodeAndLinkScale(seriesModel);
 
     if (seriesScope.expandAndCollapse === true) {
       data.eachItemGraphicEl(function (el, dataIndex) {
@@ -113,7 +135,104 @@ export default echarts.extendChartView({
 
     this._data = data;
   },
-  dispose: function () {},
+  _updateViewCoordSys: function (seriesModel) {
+    var data = seriesModel.getData();
+    var points = [];
+    data.each(function (idx) {
+      var layout = data.getItemLayout(idx);
+
+      if (layout && !isNaN(layout.x) && !isNaN(layout.y)) {
+        points.push([+layout.x, +layout.y]);
+      }
+    });
+    var min = [];
+    var max = [];
+    bbox.fromPoints(points, min, max); // If width or height is 0
+
+    if (max[0] - min[0] === 0) {
+      max[0] += 1;
+      min[0] -= 1;
+    }
+
+    if (max[1] - min[1] === 0) {
+      max[1] += 1;
+      min[1] -= 1;
+    }
+
+    var viewCoordSys = seriesModel.coordinateSystem = new View();
+    viewCoordSys.zoomLimit = seriesModel.get('scaleLimit');
+    viewCoordSys.setBoundingRect(min[0], min[1], max[0] - min[0], max[1] - min[1]);
+    viewCoordSys.setCenter(seriesModel.get('center'));
+    viewCoordSys.setZoom(seriesModel.get('zoom')); // Here we use viewCoordSys just for computing the 'position' and 'scale' of the group
+
+    this.group.attr({
+      position: viewCoordSys.position,
+      scale: viewCoordSys.scale
+    });
+    this._viewCoordSys = viewCoordSys;
+  },
+  _updateController: function (seriesModel, ecModel, api) {
+    var controller = this._controller;
+    var controllerHost = this._controllerHost;
+    var group = this.group;
+    controller.setPointerChecker(function (e, x, y) {
+      var rect = group.getBoundingRect();
+      rect.applyTransform(group.transform);
+      return rect.contain(x, y) && !onIrrelevantElement(e, api, seriesModel);
+    });
+    controller.enable(seriesModel.get('roam'));
+    controllerHost.zoomLimit = seriesModel.get('scaleLimit');
+    controllerHost.zoom = seriesModel.coordinateSystem.getZoom();
+    controller.off('pan').off('zoom').on('pan', function (e) {
+      roamHelper.updateViewOnPan(controllerHost, e.dx, e.dy);
+      api.dispatchAction({
+        seriesId: seriesModel.id,
+        type: 'treeRoam',
+        dx: e.dx,
+        dy: e.dy
+      });
+    }, this).on('zoom', function (e) {
+      roamHelper.updateViewOnZoom(controllerHost, e.scale, e.originX, e.originY);
+      api.dispatchAction({
+        seriesId: seriesModel.id,
+        type: 'treeRoam',
+        zoom: e.scale,
+        originX: e.originX,
+        originY: e.originY
+      });
+
+      this._updateNodeAndLinkScale(seriesModel);
+    }, this);
+  },
+  _updateNodeAndLinkScale: function (seriesModel) {
+    var data = seriesModel.getData();
+
+    var nodeScale = this._getNodeGlobalScale(seriesModel);
+
+    var invScale = [nodeScale, nodeScale];
+    data.eachItemGraphicEl(function (el, idx) {
+      el.attr('scale', invScale);
+    });
+  },
+  _getNodeGlobalScale: function (seriesModel) {
+    var coordSys = seriesModel.coordinateSystem;
+
+    if (coordSys.type !== 'view') {
+      return 1;
+    }
+
+    var nodeScaleRatio = this._nodeScaleRatio;
+    var groupScale = coordSys.scale;
+    var groupZoom = groupScale && groupScale[0] || 1; // Scale node when zoom changes
+
+    var roamZoom = coordSys.getZoom();
+    var nodeScale = (roamZoom - 1) * nodeScaleRatio + 1;
+    return nodeScale / groupZoom;
+  },
+  dispose: function () {
+    this._controller && this._controller.dispose();
+    this._controllerHost = {};
+  },
   remove: function () {
     this._mainGroup.removeAll();
 
@@ -238,7 +357,8 @@ function updateNode(data, dataIndex, symbolEl, group, seriesModel, seriesScope)
       edge = symbolEl.__edge = new graphic.BezierCurve({
         shape: getEdgeShape(seriesScope, sourceOldLayout, sourceOldLayout),
         style: zrUtil.defaults({
-          opacity: 0
+          opacity: 0,
+          strokeNoScale: true
         }, seriesScope.lineStyle)
       });
     }
@@ -294,12 +414,16 @@ function getEdgeShape(seriesScope, sourceLayout, targetLayout) {
   var cpx2;
   var cpy2;
   var orient = seriesScope.orient;
+  var x1;
+  var x2;
+  var y1;
+  var y2;
 
   if (seriesScope.layout === 'radial') {
-    var x1 = sourceLayout.rawX;
-    var y1 = sourceLayout.rawY;
-    var x2 = targetLayout.rawX;
-    var y2 = targetLayout.rawY;
+    x1 = sourceLayout.rawX;
+    y1 = sourceLayout.rawY;
+    x2 = targetLayout.rawX;
+    y2 = targetLayout.rawY;
     var radialCoor1 = radialCoordinate(x1, y1);
     var radialCoor2 = radialCoordinate(x1, y1 + (y2 - y1) * seriesScope.curvature);
     var radialCoor3 = radialCoordinate(x2, y2 + (y1 - y2) * seriesScope.curvature);
@@ -315,10 +439,10 @@ function getEdgeShape(seriesScope, sourceLayout, targetLayout) {
       cpy2: radialCoor3.y
     };
   } else {
-    var x1 = sourceLayout.x;
-    var y1 = sourceLayout.y;
-    var x2 = targetLayout.x;
-    var y2 = targetLayout.y;
+    x1 = sourceLayout.x;
+    y1 = sourceLayout.y;
+    x2 = targetLayout.x;
+    y2 = targetLayout.y;
 
     if (orient === 'LR' || orient === 'RL') {
       cpx1 = x1 + (x2 - x1) * seriesScope.curvature;
@@ -333,16 +457,16 @@ function getEdgeShape(seriesScope, sourceLayout, targetLayout) {
       cpx2 = x2;
       cpy2 = y2 + (y1 - y2) * seriesScope.curvature;
     }
-
-    return {
-      x1: x1,
-      y1: y1,
-      x2: x2,
-      y2: y2,
-      cpx1: cpx1,
-      cpy1: cpy1,
-      cpx2: cpx2,
-      cpy2: cpy2
-    };
   }
+
+  return {
+    x1: x1,
+    y1: y1,
+    x2: x2,
+    y2: y2,
+    cpx1: cpx1,
+    cpy1: cpy1,
+    cpx2: cpx2,
+    cpy2: cpy2
+  };
 }
\ No newline at end of file
diff --git a/builder/src/echarts/chart/tree/treeAction.js b/builder/src/echarts/chart/tree/treeAction.js
index e07ab5f..d928444 100644
--- a/builder/src/echarts/chart/tree/treeAction.js
+++ b/builder/src/echarts/chart/tree/treeAction.js
@@ -16,7 +16,13 @@
 * specific language governing permissions and limitations
 * under the License.
 */
+
+/**
+ * @file Register the actions of the tree
+ * @author Deqing Li(annong035@gmail.com)
+ */
 import * as echarts from '../../echarts';
+import { updateCenterAndZoom } from '../../action/roamHelper';
 echarts.registerAction({
   type: 'treeExpandAndCollapse',
   event: 'treeExpandAndCollapse',
@@ -32,4 +38,24 @@ echarts.registerAction({
     var node = tree.getNodeByDataIndex(dataIndex);
     node.isExpand = !node.isExpand;
   });
+});
+echarts.registerAction({
+  type: 'treeRoam',
+  event: 'treeRoam',
+  // Here we set 'none' instead of 'update', because roam action
+  // just need to update the transform matrix without having to recalculate
+  // the layout. So don't need to go through the whole update process, such
+  // as 'dataPrcocess', 'coordSystemUpdate', 'layout' and so on.
+  update: 'none'
+}, function (payload, ecModel) {
+  ecModel.eachComponent({
+    mainType: 'series',
+    subType: 'tree',
+    query: payload
+  }, function (seriesModel) {
+    var coordSys = seriesModel.coordinateSystem;
+    var res = updateCenterAndZoom(coordSys, payload);
+    seriesModel.setCenter && seriesModel.setCenter(res.center);
+    seriesModel.setZoom && seriesModel.setZoom(res.zoom);
+  });
 });
\ No newline at end of file
diff --git a/builder/src/echarts/chart/treemap/Breadcrumb.js b/builder/src/echarts/chart/treemap/Breadcrumb.js
index c7e34ca..05f688c 100644
--- a/builder/src/echarts/chart/treemap/Breadcrumb.js
+++ b/builder/src/echarts/chart/treemap/Breadcrumb.js
@@ -151,6 +151,7 @@ function packEventData(el, seriesModel, itemNode) {
   el.eventData = {
     componentType: 'series',
     componentSubType: 'treemap',
+    componentIndex: seriesModel.componentIndex,
     seriesIndex: seriesModel.componentIndex,
     seriesName: seriesModel.name,
     seriesType: 'treemap',
diff --git a/builder/src/echarts/chart/treemap/TreemapView.js b/builder/src/echarts/chart/treemap/TreemapView.js
index 9c10dff..df7ca53 100644
--- a/builder/src/echarts/chart/treemap/TreemapView.js
+++ b/builder/src/echarts/chart/treemap/TreemapView.js
@@ -391,8 +391,8 @@ export default echarts.extendChartView({
   /**
    * @private
    */
-  _onPan: function (dx, dy) {
-    if (this._state !== 'animating' && (Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD)) {
+  _onPan: function (e) {
+    if (this._state !== 'animating' && (Math.abs(e.dx) > DRAG_THRESHOLD || Math.abs(e.dy) > DRAG_THRESHOLD)) {
       // These param must not be cached.
       var root = this.seriesModel.getData().tree.root;
 
@@ -411,8 +411,8 @@ export default echarts.extendChartView({
         from: this.uid,
         seriesId: this.seriesModel.id,
         rootRect: {
-          x: rootLayout.x + dx,
-          y: rootLayout.y + dy,
+          x: rootLayout.x + e.dx,
+          y: rootLayout.y + e.dy,
           width: rootLayout.width,
           height: rootLayout.height
         }
@@ -423,7 +423,10 @@ export default echarts.extendChartView({
   /**
    * @private
    */
-  _onZoom: function (scale, mouseX, mouseY) {
+  _onZoom: function (e) {
+    var mouseX = e.originX;
+    var mouseY = e.originY;
+
     if (this._state !== 'animating') {
       // These param must not be cached.
       var root = this.seriesModel.getData().tree.root;
@@ -446,7 +449,7 @@ export default echarts.extendChartView({
 
       var m = matrix.create();
       matrix.translate(m, m, [-mouseX, -mouseY]);
-      matrix.scale(m, m, [scale, scale]);
+      matrix.scale(m, m, [e.scale, e.scale]);
       matrix.translate(m, m, [mouseX, mouseY]);
       rect.applyTransform(m);
       this.api.dispatchAction({
diff --git a/builder/src/echarts/component/axis/AxisBuilder.js b/builder/src/echarts/component/axis/AxisBuilder.js
index 6696921..4d2315f 100644
--- a/builder/src/echarts/component/axis/AxisBuilder.js
+++ b/builder/src/echarts/component/axis/AxisBuilder.js
@@ -28,7 +28,8 @@ var PI = Math.PI;
 
 function makeAxisEventDataBase(axisModel) {
   var eventData = {
-    componentType: axisModel.mainType
+    componentType: axisModel.mainType,
+    componentIndex: axisModel.componentIndex
   };
   eventData[axisModel.mainType + 'Index'] = axisModel.componentIndex;
   return eventData;
diff --git a/builder/src/echarts/component/axisPointer/AxisPointerModel.js b/builder/src/echarts/component/axisPointer/AxisPointerModel.js
index c77f841..7665312 100644
--- a/builder/src/echarts/component/axisPointer/AxisPointerModel.js
+++ b/builder/src/echarts/component/axisPointer/AxisPointerModel.js
@@ -29,6 +29,7 @@ var AxisPointerModel = echarts.extendComponentModel({
     zlevel: 0,
     z: 50,
     type: 'line',
+    // 'line' 'shadow' 'cross' 'none'.
     // axispointer triggered by tootip determine snap automatically,
     // see `modelHelper`.
     snap: false,
@@ -82,8 +83,12 @@ var AxisPointerModel = echarts.extendComponentModel({
     },
     handle: {
       show: false,
+
+      /* eslint-disable */
       icon: 'M10.7,11.9v-1.3H9.3v1.3c-4.9,0.3-8.8,4.4-8.8,9.4c0,5,3.9,9.1,8.8,9.4h1.3c4.9-0.3,8.8-4.4,8.8-9.4C19.5,16.3,15.6,12.2,10.7,11.9z M13.3,24.4H6.7v-1.2h6.6z M13.3,22H6.7v-1.2h6.6z M13.3,19.6H6.7v-1.2h6.6z',
       // jshint ignore:line
+
+      /* eslint-enable */
       size: 45,
       // handle margin is from symbol center to axis, which is stable when circular move.
       margin: 50,
diff --git a/builder/src/echarts/component/dataZoom/AxisProxy.js b/builder/src/echarts/component/dataZoom/AxisProxy.js
index e6e4813..6198773 100644
--- a/builder/src/echarts/component/dataZoom/AxisProxy.js
+++ b/builder/src/echarts/component/dataZoom/AxisProxy.js
@@ -308,13 +308,16 @@ AxisProxy.prototype = {
     // }
     // TODO
     // filterMode 'weakFilter' and 'empty' is not optimized for huge data yet.
-    // Process series data
 
 
     each(seriesModels, function (seriesModel) {
       var seriesData = seriesModel.getData();
       var dataDims = seriesData.mapDimension(axisDim, true);
 
+      if (!dataDims.length) {
+        return;
+      }
+
       if (filterMode === 'weakFilter') {
         seriesData.filterSelf(function (dataIndex) {
           var leftOut;
diff --git a/builder/src/echarts/component/dataZoom/InsideZoomModel.js b/builder/src/echarts/component/dataZoom/InsideZoomModel.js
index 3df59d6..61cfa5c 100644
--- a/builder/src/echarts/component/dataZoom/InsideZoomModel.js
+++ b/builder/src/echarts/component/dataZoom/InsideZoomModel.js
@@ -32,6 +32,8 @@ export default DataZoomModel.extend({
     // Can be: true / false / 'shift' / 'ctrl' / 'alt'.
     moveOnMouseMove: true,
     // Can be: true / false / 'shift' / 'ctrl' / 'alt'.
+    moveOnMouseWheel: false,
+    // Can be: true / false / 'shift' / 'ctrl' / 'alt'.
     preventDefaultMouseMove: true
   }
 });
\ No newline at end of file
diff --git a/builder/src/echarts/component/dataZoom/InsideZoomView.js b/builder/src/echarts/component/dataZoom/InsideZoomView.js
index ad44b93..ea9c0ff 100644
--- a/builder/src/echarts/component/dataZoom/InsideZoomView.js
+++ b/builder/src/echarts/component/dataZoom/InsideZoomView.js
@@ -53,7 +53,10 @@ var InsideZoomView = DataZoomView.extend({
       });
       zrUtil.each(coordInfoList, function (coordInfo) {
         var coordModel = coordInfo.model;
-        var dataZoomOption = dataZoomModel.option;
+        var getRange = {};
+        zrUtil.each(['pan', 'zoom', 'scrollMove'], function (eventName) {
+          getRange[eventName] = bind(roamHandlers[eventName], this, coordInfo, coordSysName);
+        }, this);
         roams.register(api, {
           coordId: roams.generateCoordId(coordModel),
           allCoordIds: allCoordIds,
@@ -61,16 +64,8 @@ var InsideZoomView = DataZoomView.extend({
             return coordModel.coordinateSystem.containPoint([x, y]);
           },
           dataZoomId: dataZoomModel.id,
-          throttleRate: dataZoomModel.get('throttle', true),
-          panGetRange: bind(this._onPan, this, coordInfo, coordSysName),
-          zoomGetRange: bind(this._onZoom, this, coordInfo, coordSysName),
-          zoomLock: dataZoomOption.zoomLock,
-          disabled: dataZoomOption.disabled,
-          roamControllerOpt: {
-            zoomOnMouseWheel: dataZoomOption.zoomOnMouseWheel,
-            moveOnMouseMove: dataZoomOption.moveOnMouseMove,
-            preventDefaultMouseMove: dataZoomOption.preventDefaultMouseMove
-          }
+          dataZoomModel: dataZoomModel,
+          getRange: getRange
         });
       }, this);
     }, this);
@@ -83,12 +78,13 @@ var InsideZoomView = DataZoomView.extend({
     roams.unregister(this.api, this.dataZoomModel.id);
     InsideZoomView.superApply(this, 'dispose', arguments);
     this._range = null;
-  },
-
+  }
+});
+var roamHandlers = {
   /**
-   * @private
+   * @this {module:echarts/component/dataZoom/InsideZoomView}
    */
-  _onPan: function (coordInfo, coordSysName, controller, dx, dy, oldX, oldY, newX, newY) {
+  zoom: function (coordInfo, coordSysName, controller, e) {
     var lastRange = this._range;
     var range = lastRange.slice(); // Calculate transform by the first axis.
 
@@ -98,9 +94,14 @@ var InsideZoomView = DataZoomView.extend({
       return;
     }
 
-    var directionInfo = getDirectionInfo[coordSysName]([oldX, oldY], [newX, newY], axisModel, controller, coordInfo);
-    var percentDelta = directionInfo.signal * (range[1] - range[0]) * directionInfo.pixel / directionInfo.pixelLength;
-    sliderMove(percentDelta, range, [0, 100], 'all');
+    var directionInfo = getDirectionInfo[coordSysName](null, [e.originX, e.originY], axisModel, controller, coordInfo);
+    var percentPoint = (directionInfo.signal > 0 ? directionInfo.pixelStart + directionInfo.pixelLength - directionInfo.pixel : directionInfo.pixel - directionInfo.pixelStart) / directionInfo.pixelLength * (range[1] - range[0]) + range[0];
+    var scale = Math.max(1 / e.scale, 0);
+    range[0] = (range[0] - percentPoint) * scale + percentPoint;
+    range[1] = (range[1] - percentPoint) * scale + percentPoint; // Restrict range.
+
+    var minMaxSpan = this.dataZoomModel.findRepresentativeAxisProxy().getMinMaxSpan();
+    sliderMove(0, range, [0, 100], 0, minMaxSpan.minSpan, minMaxSpan.maxSpan);
     this._range = range;
 
     if (lastRange[0] !== range[0] || lastRange[1] !== range[1]) {
@@ -109,9 +110,24 @@ var InsideZoomView = DataZoomView.extend({
   },
 
   /**
-   * @private
+   * @this {module:echarts/component/dataZoom/InsideZoomView}
+   */
+  pan: makeMover(function (range, axisModel, coordInfo, coordSysName, controller, e) {
+    var directionInfo = getDirectionInfo[coordSysName]([e.oldX, e.oldY], [e.newX, e.newY], axisModel, controller, coordInfo);
+    return directionInfo.signal * (range[1] - range[0]) * directionInfo.pixel / directionInfo.pixelLength;
+  }),
+
+  /**
+   * @this {module:echarts/component/dataZoom/InsideZoomView}
    */
-  _onZoom: function (coordInfo, coordSysName, controller, scale, mouseX, mouseY) {
+  scrollMove: makeMover(function (range, axisModel, coordInfo, coordSysName, controller, e) {
+    var directionInfo = getDirectionInfo[coordSysName]([0, 0], [e.scrollDelta, e.scrollDelta], axisModel, controller, coordInfo);
+    return directionInfo.signal * (range[1] - range[0]) * e.scrollDelta;
+  })
+};
+
+function makeMover(getPercentDelta) {
+  return function (coordInfo, coordSysName, controller, e) {
     var lastRange = this._range;
     var range = lastRange.slice(); // Calculate transform by the first axis.
 
@@ -121,21 +137,16 @@ var InsideZoomView = DataZoomView.extend({
       return;
     }
 
-    var directionInfo = getDirectionInfo[coordSysName](null, [mouseX, mouseY], axisModel, controller, coordInfo);
-    var percentPoint = (directionInfo.signal > 0 ? directionInfo.pixelStart + directionInfo.pixelLength - directionInfo.pixel : directionInfo.pixel - directionInfo.pixelStart) / directionInfo.pixelLength * (range[1] - range[0]) + range[0];
-    scale = Math.max(1 / scale, 0);
-    range[0] = (range[0] - percentPoint) * scale + percentPoint;
-    range[1] = (range[1] - percentPoint) * scale + percentPoint; // Restrict range.
-
-    var minMaxSpan = this.dataZoomModel.findRepresentativeAxisProxy().getMinMaxSpan();
-    sliderMove(0, range, [0, 100], 0, minMaxSpan.minSpan, minMaxSpan.maxSpan);
+    var percentDelta = getPercentDelta(range, axisModel, coordInfo, coordSysName, controller, e);
+    sliderMove(percentDelta, range, [0, 100], 'all');
     this._range = range;
 
     if (lastRange[0] !== range[0] || lastRange[1] !== range[1]) {
       return range;
     }
-  }
-});
+  };
+}
+
 var getDirectionInfo = {
   grid: function (oldPoint, newPoint, axisModel, controller, coordInfo) {
     var axis = axisModel.axis;
diff --git a/builder/src/echarts/component/dataZoom/SliderZoomModel.js b/builder/src/echarts/component/dataZoom/SliderZoomModel.js
index 225dad9..4914921 100644
--- a/builder/src/echarts/component/dataZoom/SliderZoomModel.js
+++ b/builder/src/echarts/component/dataZoom/SliderZoomModel.js
@@ -64,7 +64,11 @@ var SliderZoomModel = DataZoomModel.extend({
     // Color of selected area.
     // handleColor: 'rgba(89,170,216,0.95)',     // Color of handle.
     // handleIcon: 'path://M4.9,17.8c0-1.4,4.5-10.5,5.5-12.4c0-0.1,0.6-1.1,0.9-1.1c0.4,0,0.9,1,0.9,1.1c1.1,2.2,5.4,11,5.4,12.4v17.8c0,1.5-0.6,2.1-1.3,2.1H6.1c-0.7,0-1.3-0.6-1.3-2.1V17.8z',
+
+    /* eslint-disable */
     handleIcon: 'M8.2,13.6V3.9H6.3v9.7H3.1v14.9h3.3v9.7h1.8v-9.7h3.3V13.6H8.2z M9.7,24.4H4.8v-1.4h4.9V24.4z M9.7,19.1H4.8v-1.4h4.9V19.1z',
+
+    /* eslint-enable */
     // Percent of the slider height
     handleSize: '100%',
     handleStyle: {
diff --git a/builder/src/echarts/component/dataZoom/roams.js b/builder/src/echarts/component/dataZoom/roams.js
index c884e54..34c91c1 100644
--- a/builder/src/echarts/component/dataZoom/roams.js
+++ b/builder/src/echarts/component/dataZoom/roams.js
@@ -24,7 +24,6 @@
 import * as zrUtil from 'zrender/src/core/util';
 import RoamController from '../../component/helper/RoamController';
 import * as throttleUtil from '../../util/throttle';
-var curry = zrUtil.curry;
 var ATTR = '\0_ec_dataZoom_roams';
 /**
  * @public
@@ -34,11 +33,11 @@ var ATTR = '\0_ec_dataZoom_roams';
  * @param {Function} dataZoomInfo.containsPoint
  * @param {Array.<string>} dataZoomInfo.allCoordIds
  * @param {string} dataZoomInfo.dataZoomId
- * @param {number} dataZoomInfo.throttleRate
- * @param {Function} dataZoomInfo.panGetRange
- * @param {Function} dataZoomInfo.zoomGetRange
- * @param {boolean} [dataZoomInfo.zoomLock]
- * @param {boolean} [dataZoomInfo.disabled]
+ * @param {Object} dataZoomInfo.getRange
+ * @param {Function} dataZoomInfo.getRange.pan
+ * @param {Function} dataZoomInfo.getRange.zoom
+ * @param {Function} dataZoomInfo.getRange.scrollMove
+ * @param {boolean} dataZoomInfo.dataZoomModel
  */
 
 export function register(api, dataZoomInfo) {
@@ -76,7 +75,7 @@ export function register(api, dataZoomInfo) {
 
   record.controller.setPointerChecker(dataZoomInfo.containsPoint); // Update throttle.
 
-  throttleUtil.createOrUpdate(record, 'dispatchAction', dataZoomInfo.throttleRate, 'fixRate');
+  throttleUtil.createOrUpdate(record, 'dispatchAction', dataZoomInfo.dataZoomModel.get('throttle', true), 'fixRate');
 }
 /**
  * @public
@@ -118,8 +117,27 @@ function giveStore(api) {
 
 function createController(api, newRecord) {
   var controller = new RoamController(api.getZr());
-  controller.on('pan', curry(onPan, newRecord));
-  controller.on('zoom', curry(onZoom, newRecord));
+  zrUtil.each(['pan', 'zoom', 'scrollMove'], function (eventName) {
+    controller.on(eventName, function (event) {
+      var batch = [];
+      zrUtil.each(newRecord.dataZoomInfos, function (info) {
+        // Check whether the behaviors (zoomOnMouseWheel, moveOnMouseMove,
+        // moveOnMouseWheel, ...) enabled.
+        if (!event.isAvailableBehavior(info.dataZoomModel.option)) {
+          return;
+        }
+
+        var method = (info.getRange || {})[eventName];
+        var range = method && method(newRecord.controller, event);
+        !info.dataZoomModel.get('disabled', true) && range && batch.push({
+          dataZoomId: info.dataZoomId,
+          start: range[0],
+          end: range[1]
+        });
+      });
+      batch.length && newRecord.dispatchAction(batch);
+    });
+  });
   return controller;
 }
 
@@ -131,31 +149,6 @@ function cleanStore(store) {
     }
   });
 }
-
-function onPan(record, dx, dy, oldX, oldY, newX, newY) {
-  wrapAndDispatch(record, function (info) {
-    return info.panGetRange(record.controller, dx, dy, oldX, oldY, newX, newY);
-  });
-}
-
-function onZoom(record, scale, mouseX, mouseY) {
-  wrapAndDispatch(record, function (info) {
-    return info.zoomGetRange(record.controller, scale, mouseX, mouseY);
-  });
-}
-
-function wrapAndDispatch(record, getRange) {
-  var batch = [];
-  zrUtil.each(record.dataZoomInfos, function (info) {
-    var range = getRange(info);
-    !info.disabled && range && batch.push({
-      dataZoomId: info.dataZoomId,
-      start: range[0],
-      end: range[1]
-    });
-  });
-  batch.length && record.dispatchAction(batch);
-}
 /**
  * This action will be throttled.
  */
@@ -173,8 +166,7 @@ function dispatchAction(api, batch) {
 
 
 function mergeControllerParams(dataZoomInfos) {
-  var controlType;
-  var opt = {}; // DO NOT use reserved word (true, false, undefined) as key literally. Even if encapsulated
+  var controlType; // DO NOT use reserved word (true, false, undefined) as key literally. Even if encapsulated
   // as string, it is probably revert to reserved word by compress tool. See #7411.
 
   var prefix = 'type_';
@@ -184,18 +176,29 @@ function mergeControllerParams(dataZoomInfos) {
     'type_false': 0,
     'type_undefined': -1
   };
+  var preventDefaultMouseMove = true;
   zrUtil.each(dataZoomInfos, function (dataZoomInfo) {
-    var oneType = dataZoomInfo.disabled ? false : dataZoomInfo.zoomLock ? 'move' : true;
+    var dataZoomModel = dataZoomInfo.dataZoomModel;
+    var oneType = dataZoomModel.get('disabled', true) ? false : dataZoomModel.get('zoomLock', true) ? 'move' : true;
 
     if (typePriority[prefix + oneType] > typePriority[prefix + controlType]) {
       controlType = oneType;
-    } // Do not support that different 'shift'/'ctrl'/'alt' setting used in one coord sys.
+    } // Prevent default move event by default. If one false, do not prevent. Otherwise
+    // users may be confused why it does not work when multiple insideZooms exist.
 
 
-    zrUtil.extend(opt, dataZoomInfo.roamControllerOpt);
+    preventDefaultMouseMove &= dataZoomModel.get('preventDefaultMouseMove', true);
   });
   return {
     controlType: controlType,
-    opt: opt
+    opt: {
+      // RoamController will enable all of these functionalities,
+      // and the final behavior is determined by its event listener
+      // provided by each inside zoom.
+      zoomOnMouseWheel: true,
+      moveOnMouseMove: true,
+      moveOnMouseWheel: true,
+      preventDefaultMouseMove: !!preventDefaultMouseMove
+    }
   };
 }
\ No newline at end of file
diff --git a/builder/src/echarts/component/graphic.js b/builder/src/echarts/component/graphic.js
index 3808c34..29fc378 100644
--- a/builder/src/echarts/component/graphic.js
+++ b/builder/src/echarts/component/graphic.js
@@ -78,6 +78,8 @@ var GraphicModel = echarts.extendComponentModel({
     //          This mode is similar to css behavior, which is useful when you
     //          want an element to be able to overflow its container. (Consider
     //          a rotated circle needs to be located in a corner.)
+    // info: custom info. enables user to mount some info on elements and use them
+    //      in event handlers. Update them only when user specified, otherwise, remain.
     // Note: elements is always behind its ancestors in this elements array.
     elements: [],
     parentId: null
@@ -233,7 +235,7 @@ echarts.extendComponentView({
 
     this._lastGraphicModel = graphicModel;
 
-    this._updateElements(graphicModel, api);
+    this._updateElements(graphicModel);
 
     this._relocate(graphicModel, api);
   },
@@ -243,9 +245,8 @@ echarts.extendComponentView({
    *
    * @private
    * @param {Object} graphicModel graphic model
-   * @param {module:echarts/ExtensionAPI} api extension API
    */
-  _updateElements: function (graphicModel, api) {
+  _updateElements: function (graphicModel) {
     var elOptionsToUpdate = graphicModel.useElOptionsToUpdate();
 
     if (!elOptionsToUpdate) {
@@ -261,11 +262,11 @@ echarts.extendComponentView({
       var existEl = elMap.get(id);
       var parentId = elOption.parentId;
       var targetElParent = parentId != null ? elMap.get(parentId) : rootGroup;
+      var elOptionStyle = elOption.style;
 
-      if (elOption.type === 'text') {
-        var elOptionStyle = elOption.style; // In top/bottom mode, textVerticalAlign should not be used, which cause
+      if (elOption.type === 'text' && elOptionStyle) {
+        // In top/bottom mode, textVerticalAlign should not be used, which cause
         // inaccurately locating.
-
         if (elOption.hv && elOption.hv[1]) {
           elOptionStyle.textVerticalAlign = elOptionStyle.textBaseline = null;
         } // Compatible with previous setting: both support fill and textFill,
@@ -293,6 +294,7 @@ echarts.extendComponentView({
       if (el) {
         el.__ecGraphicWidth = elOption.width;
         el.__ecGraphicHeight = elOption.height;
+        setEventData(el, graphicModel, elOption);
       }
     });
   },
@@ -453,4 +455,22 @@ function setLayoutInfoToExist(existItem, newElOption) {
     existItem.width == null && (existItem.width = newElOption.width = 0);
     existItem.height == null && (existItem.height = newElOption.height = 0);
   }
+}
+
+function setEventData(el, graphicModel, elOption) {
+  var eventData = el.eventData; // Simple optimize for large amount of elements that no need event.
+
+  if (!el.silent && !el.ignore && !eventData) {
+    eventData = el.eventData = {
+      componentType: 'graphic',
+      componentIndex: graphicModel.componentIndex,
+      name: el.name
+    };
+  } // `elOption.info` enables user to mount some info on
+  // elements and use them in event handlers.
+
+
+  if (eventData) {
+    eventData.info = el.info;
+  }
 }
\ No newline at end of file
diff --git a/builder/src/echarts/component/helper/MapDraw.js b/builder/src/echarts/component/helper/MapDraw.js
index c4c5e1b..0e6bf02 100644
--- a/builder/src/echarts/component/helper/MapDraw.js
+++ b/builder/src/echarts/component/helper/MapDraw.js
@@ -21,6 +21,8 @@ import RoamController from './RoamController';
 import * as roamHelper from '../../component/helper/roamHelper';
 import { onIrrelevantElement } from '../../component/helper/cursorHelper';
 import * as graphic from '../../util/graphic';
+import geoSourceManager from '../../coord/geo/geoSourceManager';
+import { getUID } from '../../util/component';
 
 function getFixedItemStyle(model, scale) {
   var itemStyle = model.getItemStyle();
@@ -34,15 +36,15 @@ function getFixedItemStyle(model, scale) {
   return itemStyle;
 }
 
-function updateMapSelectHandler(mapDraw, mapOrGeoModel, group, api, fromView) {
-  group.off('click');
-  group.off('mousedown');
+function updateMapSelectHandler(mapDraw, mapOrGeoModel, regionsGroup, api, fromView) {
+  regionsGroup.off('click');
+  regionsGroup.off('mousedown');
 
   if (mapOrGeoModel.get('selectedMode')) {
-    group.on('mousedown', function () {
+    regionsGroup.on('mousedown', function () {
       mapDraw._mouseDownFlag = true;
     });
-    group.on('click', function (e) {
+    regionsGroup.on('click', function (e) {
       if (!mapDraw._mouseDownFlag) {
         return;
       }
@@ -69,14 +71,14 @@ function updateMapSelectHandler(mapDraw, mapOrGeoModel, group, api, fromView) {
       };
       action[mapOrGeoModel.mainType + 'Id'] = mapOrGeoModel.id;
       api.dispatchAction(action);
-      updateMapSelected(mapOrGeoModel, group);
+      updateMapSelected(mapOrGeoModel, regionsGroup);
     });
   }
 }
 
-function updateMapSelected(mapOrGeoModel, group) {
+function updateMapSelected(mapOrGeoModel, regionsGroup) {
   // FIXME
-  group.eachChild(function (otherRegionEl) {
+  regionsGroup.eachChild(function (otherRegionEl) {
     zrUtil.each(otherRegionEl.__regions, function (region) {
       otherRegionEl.trigger(mapOrGeoModel.isSelected(region.name) ? 'emphasis' : 'normal');
     });
@@ -92,6 +94,12 @@ function updateMapSelected(mapOrGeoModel, group) {
 function MapDraw(api, updateGroup) {
   var group = new graphic.Group();
   /**
+   * @type {string}
+   * @private
+   */
+
+  this.uid = getUID('ec_map_draw');
+  /**
    * @type {module:echarts/component/helper/RoamController}
    * @private
    */
@@ -125,6 +133,26 @@ function MapDraw(api, updateGroup) {
    */
 
   this._mouseDownFlag;
+  /**
+   * @type {string}
+   */
+
+  this._mapName;
+  /**
+   * @type {boolean}
+   */
+
+  this._initialized;
+  /**
+   * @type {module:zrender/container/Group}
+   */
+
+  group.add(this._regionsGroup = new graphic.Group());
+  /**
+   * @type {module:zrender/container/Group}
+   */
+
+  group.add(this._backgroundGroup = new graphic.Group());
 }
 
 MapDraw.prototype = {
@@ -143,20 +171,24 @@ MapDraw.prototype = {
       }
     });
     var geo = mapOrGeoModel.coordinateSystem;
+
+    this._updateBackground(geo);
+
+    var regionsGroup = this._regionsGroup;
     var group = this.group;
     var scale = geo.scale;
-    var groupNewProp = {
+    var transform = {
       position: geo.position,
       scale: scale
     }; // No animation when first draw or in action
 
-    if (!group.childAt(0) || payload) {
-      group.attr(groupNewProp);
+    if (!regionsGroup.childAt(0) || payload) {
+      group.attr(transform);
     } else {
-      graphic.updateProps(group, groupNewProp, mapOrGeoModel);
+      graphic.updateProps(group, transform, mapOrGeoModel);
     }
 
-    group.removeAll();
+    regionsGroup.removeAll();
     var itemStyleAccessPath = ['itemStyle'];
     var hoverItemStyleAccessPath = ['emphasis', 'itemStyle'];
     var labelAccessPath = ['label'];
@@ -263,6 +295,7 @@ MapDraw.prototype = {
 
         compoundPath.eventData = {
           componentType: 'geo',
+          componentIndex: mapOrGeoModel.componentIndex,
           geoIndex: mapOrGeoModel.componentIndex,
           name: region.name,
           region: regionModel && regionModel.option || {}
@@ -274,21 +307,36 @@ MapDraw.prototype = {
       graphic.setHoverStyle(regionGroup, hoverItemStyle, {
         hoverSilentOnTouch: !!mapOrGeoModel.get('selectedMode')
       });
-      group.add(regionGroup);
+      regionsGroup.add(regionGroup);
     });
 
     this._updateController(mapOrGeoModel, ecModel, api);
 
-    updateMapSelectHandler(this, mapOrGeoModel, group, api, fromView);
-    updateMapSelected(mapOrGeoModel, group);
+    updateMapSelectHandler(this, mapOrGeoModel, regionsGroup, api, fromView);
+    updateMapSelected(mapOrGeoModel, regionsGroup);
   },
   remove: function () {
-    this.group.removeAll();
+    this._regionsGroup.removeAll();
+
+    this._backgroundGroup.removeAll();
 
     this._controller.dispose();
 
+    this._mapName && geoSourceManager.removeGraphic(this._mapName, this.uid);
+    this._mapName = null;
     this._controllerHost = {};
   },
+  _updateBackground: function (geo) {
+    var mapName = geo.map;
+
+    if (this._mapName !== mapName) {
+      zrUtil.each(geoSourceManager.makeGraphic(mapName, this.uid), function (root) {
+        this._backgroundGroup.add(root);
+      }, this);
+    }
+
+    this._mapName = mapName;
+  },
   _updateController: function (mapOrGeoModel, ecModel, api) {
     var geo = mapOrGeoModel.coordinateSystem;
     var controller = this._controller;
@@ -308,27 +356,27 @@ MapDraw.prototype = {
       return action;
     }
 
-    controller.off('pan').on('pan', function (dx, dy) {
+    controller.off('pan').on('pan', function (e) {
       this._mouseDownFlag = false;
-      roamHelper.updateViewOnPan(controllerHost, dx, dy);
+      roamHelper.updateViewOnPan(controllerHost, e.dx, e.dy);
       api.dispatchAction(zrUtil.extend(makeActionBase(), {
-        dx: dx,
-        dy: dy
+        dx: e.dx,
+        dy: e.dy
       }));
     }, this);
-    controller.off('zoom').on('zoom', function (zoom, mouseX, mouseY) {
+    controller.off('zoom').on('zoom', function (e) {
       this._mouseDownFlag = false;
-      roamHelper.updateViewOnZoom(controllerHost, zoom, mouseX, mouseY);
+      roamHelper.updateViewOnZoom(controllerHost, e.scale, e.originX, e.originY);
       api.dispatchAction(zrUtil.extend(makeActionBase(), {
-        zoom: zoom,
-        originX: mouseX,
-        originY: mouseY
+        zoom: e.scale,
+        originX: e.originX,
+        originY: e.originY
       }));
 
       if (this._updateGroup) {
-        var group = this.group;
-        var scale = group.scale;
-        group.traverse(function (el) {
+        var scale = this.group.scale;
+
+        this._regionsGroup.traverse(function (el) {
           if (el.type === 'text') {
             el.attr('scale', [1 / scale[0], 1 / scale[1]]);
           }
diff --git a/builder/src/echarts/component/helper/RoamController.js b/builder/src/echarts/component/helper/RoamController.js
index 582693e..27f8d34 100644
--- a/builder/src/echarts/component/helper/RoamController.js
+++ b/builder/src/echarts/component/helper/RoamController.js
@@ -69,8 +69,9 @@ function RoamController(zr) {
    *                          which can be null/undefined or true/false
    *                          or 'pan/move' or 'zoom'/'scale'
    * @param {Object} [opt]
-   * @param {Object} [opt.zoomOnMouseWheel=true]
-   * @param {Object} [opt.moveOnMouseMove=true]
+   * @param {Object} [opt.zoomOnMouseWheel=true] The value can be: true / false / 'shift' / 'ctrl' / 'alt'.
+   * @param {Object} [opt.moveOnMouseMove=true] The value can be: true / false / 'shift' / 'ctrl' / 'alt'.
+   * @param {Object} [opt.moveOnMouseWheel=false] The value can be: true / false / 'shift' / 'ctrl' / 'alt'.
    * @param {Object} [opt.preventDefaultMouseMove=true] When pan.
    */
 
@@ -81,6 +82,8 @@ function RoamController(zr) {
     this._opt = zrUtil.defaults(zrUtil.clone(opt) || {}, {
       zoomOnMouseWheel: true,
       moveOnMouseMove: true,
+      // By default, wheel do not trigger move.
+      moveOnMouseWheel: false,
       preventDefaultMouseMove: true
     });
 
@@ -138,7 +141,7 @@ function mousedown(e) {
 }
 
 function mousemove(e) {
-  if (eventTool.notLeftMouse(e) || !checkKeyBinding(this, 'moveOnMouseMove', e) || !this._dragging || e.gestureEvent === 'pinch' || interactionMutex.isTaken(this._zr, 'globalPan')) {
+  if (eventTool.notLeftMouse(e) || !isAvailableBehavior('moveOnMouseMove', e, this._opt) || !this._dragging || e.gestureEvent === 'pinch' || interactionMutex.isTaken(this._zr, 'globalPan')) {
     return;
   }
 
@@ -151,7 +154,14 @@ function mousemove(e) {
   this._x = x;
   this._y = y;
   this._opt.preventDefaultMouseMove && eventTool.stop(e.event);
-  this.trigger('pan', dx, dy, oldX, oldY, x, y);
+  trigger(this, 'pan', 'moveOnMouseMove', e, {
+    dx: dx,
+    dy: dy,
+    oldX: oldX,
+    oldY: oldY,
+    newX: x,
+    newY: y
+  });
 }
 
 function mouseup(e) {
@@ -161,16 +171,49 @@ function mouseup(e) {
 }
 
 function mousewheel(e) {
-  // wheelDelta maybe -0 in chrome mac.
-  if (!checkKeyBinding(this, 'zoomOnMouseWheel', e) || e.wheelDelta === 0) {
+  var shouldZoom = isAvailableBehavior('zoomOnMouseWheel', e, this._opt);
+  var shouldMove = isAvailableBehavior('moveOnMouseWheel', e, this._opt);
+  var wheelDelta = e.wheelDelta;
+  var absWheelDeltaDelta = Math.abs(wheelDelta);
+  var originX = e.offsetX;
+  var originY = e.offsetY; // wheelDelta maybe -0 in chrome mac.
+
+  if (wheelDelta === 0 || !shouldZoom && !shouldMove) {
     return;
-  } // Convenience:
-  // Mac and VM Windows on Mac: scroll up: zoom out.
-  // Windows: scroll up: zoom in.
+  } // If both `shouldZoom` and `shouldMove` is true, trigger
+  // their event both, and the final behavior is determined
+  // by event listener themselves.
+
+
+  if (shouldZoom) {
+    // Convenience:
+    // Mac and VM Windows on Mac: scroll up: zoom out.
+    // Windows: scroll up: zoom in.
+    // FIXME: Should do more test in different environment.
+    // wheelDelta is too complicated in difference nvironment
+    // (https://developer.mozilla.org/en-US/docs/Web/Events/mousewheel),
+    // although it has been normallized by zrender.
+    // wheelDelta of mouse wheel is bigger than touch pad.
+    var factor = absWheelDeltaDelta > 3 ? 1.4 : absWheelDeltaDelta > 1 ? 1.2 : 1.1;
+    var scale = wheelDelta > 0 ? factor : 1 / factor;
+    checkPointerAndTrigger(this, 'zoom', 'zoomOnMouseWheel', e, {
+      scale: scale,
+      originX: originX,
+      originY: originY
+    });
+  }
 
+  if (shouldMove) {
+    // FIXME: Should do more test in different environment.
+    var absDelta = Math.abs(wheelDelta); // wheelDelta of mouse wheel is bigger than touch pad.
 
-  var zoomDelta = e.wheelDelta > 0 ? 1.1 : 1 / 1.1;
-  zoom.call(this, e, zoomDelta, e.offsetX, e.offsetY);
+    var scrollDelta = (wheelDelta > 0 ? 1 : -1) * (absDelta > 3 ? 0.4 : absDelta > 1 ? 0.15 : 0.05);
+    checkPointerAndTrigger(this, 'scrollMove', 'moveOnMouseWheel', e, {
+      scrollDelta: scrollDelta,
+      originX: originX,
+      originY: originY
+    });
+  }
 }
 
 function pinch(e) {
@@ -178,23 +221,40 @@ function pinch(e) {
     return;
   }
 
-  var zoomDelta = e.pinchScale > 1 ? 1.1 : 1 / 1.1;
-  zoom.call(this, e, zoomDelta, e.pinchX, e.pinchY);
+  var scale = e.pinchScale > 1 ? 1.1 : 1 / 1.1;
+  checkPointerAndTrigger(this, 'zoom', null, e, {
+    scale: scale,
+    originX: e.pinchX,
+    originY: e.pinchY
+  });
 }
 
-function zoom(e, zoomDelta, zoomX, zoomY) {
-  if (this.pointerChecker && this.pointerChecker(e, zoomX, zoomY)) {
+function checkPointerAndTrigger(controller, eventName, behaviorToCheck, e, contollerEvent) {
+  if (controller.pointerChecker && controller.pointerChecker(e, contollerEvent.originX, contollerEvent.originY)) {
     // When mouse is out of roamController rect,
     // default befavoius should not be be disabled, otherwise
     // page sliding is disabled, contrary to expectation.
     eventTool.stop(e.event);
-    this.trigger('zoom', zoomDelta, zoomX, zoomY);
+    trigger(controller, eventName, behaviorToCheck, e, contollerEvent);
   }
 }
 
-function checkKeyBinding(roamController, prop, e) {
-  var setting = roamController._opt[prop];
-  return setting && (!zrUtil.isString(setting) || e.event[setting + 'Key']);
+function trigger(controller, eventName, behaviorToCheck, e, contollerEvent) {
+  // Also provide behavior checker for event listener, for some case that
+  // multiple components share one listener.
+  contollerEvent.isAvailableBehavior = zrUtil.bind(isAvailableBehavior, null, behaviorToCheck, e);
+  controller.trigger(eventName, contollerEvent);
+} // settings: {
+//     zoomOnMouseWheel
+//     moveOnMouseMove
+//     moveOnMouseWheel
+// }
+// The value can be: true / false / 'shift' / 'ctrl' / 'alt'.
+
+
+function isAvailableBehavior(behaviorToCheck, e, settings) {
+  var setting = settings[behaviorToCheck];
+  return !behaviorToCheck || setting && (!zrUtil.isString(setting) || e.event[setting + 'Key']);
 }
 
 export default RoamController;
\ No newline at end of file
diff --git a/builder/src/echarts/component/legend/LegendView.js b/builder/src/echarts/component/legend/LegendView.js
index 8505958..856e618 100644
--- a/builder/src/echarts/component/legend/LegendView.js
+++ b/builder/src/echarts/component/legend/LegendView.js
@@ -143,7 +143,7 @@ export default echarts.extendComponentView({
 
         var itemGroup = this._createItem(name, dataIndex, itemModel, legendModel, legendSymbolType, symbolType, itemAlign, color, selectMode);
 
-        itemGroup.on('click', curry(dispatchSelectAction, name, api)).on('mouseover', curry(dispatchHighlightAction, seriesModel, null, api, excludeSeriesId)).on('mouseout', curry(dispatchDownplayAction, seriesModel, null, api, excludeSeriesId));
+        itemGroup.on('click', curry(dispatchSelectAction, name, api)).on('mouseover', curry(dispatchHighlightAction, seriesModel.name, null, api, excludeSeriesId)).on('mouseout', curry(dispatchDownplayAction, seriesModel.name, null, api, excludeSeriesId));
         legendDrawnMap.set(name, true);
       } else {
         // Data legend of pie, funnel
@@ -167,8 +167,9 @@ export default echarts.extendComponentView({
             var itemGroup = this._createItem(name, dataIndex, itemModel, legendModel, legendSymbolType, null, itemAlign, color, selectMode); // FIXME: consider different series has items with the same name.
 
 
-            itemGroup.on('click', curry(dispatchSelectAction, name, api)) // FIXME Should not specify the series name
-            .on('mouseover', curry(dispatchHighlightAction, seriesModel, name, api, excludeSeriesId)).on('mouseout', curry(dispatchDownplayAction, seriesModel, name, api, excludeSeriesId));
+            itemGroup.on('click', curry(dispatchSelectAction, name, api)) // Should not specify the series name, consider legend controls
+            // more than one pie series.
+            .on('mouseover', curry(dispatchHighlightAction, null, name, api, excludeSeriesId)).on('mouseout', curry(dispatchDownplayAction, null, name, api, excludeSeriesId));
             legendDrawnMap.set(name, true);
           }
         }, this);
@@ -193,7 +194,7 @@ export default echarts.extendComponentView({
     // PENDING
 
     if (!itemIcon && symbolType // At least show one symbol, can't be all none
-    && (symbolType !== legendSymbolType || symbolType == 'none')) {
+    && (symbolType !== legendSymbolType || symbolType === 'none')) {
       var size = itemHeight * 0.8;
 
       if (symbolType === 'none') {
@@ -275,28 +276,28 @@ function dispatchSelectAction(name, api) {
   });
 }
 
-function dispatchHighlightAction(seriesModel, dataName, api, excludeSeriesId) {
+function dispatchHighlightAction(seriesName, dataName, api, excludeSeriesId) {
   // If element hover will move to a hoverLayer.
   var el = api.getZr().storage.getDisplayList()[0];
 
   if (!(el && el.useHoverLayer)) {
     api.dispatchAction({
       type: 'highlight',
-      seriesName: seriesModel.name,
+      seriesName: seriesName,
       name: dataName,
       excludeSeriesId: excludeSeriesId
     });
   }
 }
 
-function dispatchDownplayAction(seriesModel, dataName, api, excludeSeriesId) {
+function dispatchDownplayAction(seriesName, dataName, api, excludeSeriesId) {
   // If element hover will move to a hoverLayer.
   var el = api.getZr().storage.getDisplayList()[0];
 
   if (!(el && el.useHoverLayer)) {
     api.dispatchAction({
       type: 'downplay',
-      seriesName: seriesModel.name,
+      seriesName: seriesName,
       name: dataName,
       excludeSeriesId: excludeSeriesId
     });
diff --git a/builder/src/echarts/component/title.js b/builder/src/echarts/component/title.js
index 9520331..e7045b9 100644
--- a/builder/src/echarts/component/title.js
+++ b/builder/src/echarts/component/title.js
@@ -116,8 +116,9 @@ echarts.extendComponentView({
     });
     var link = titleModel.get('link');
     var sublink = titleModel.get('sublink');
-    textEl.silent = !link;
-    subTextEl.silent = !sublink;
+    var triggerEvent = titleModel.get('triggerEvent', true);
+    textEl.silent = !link && !triggerEvent;
+    subTextEl.silent = !sublink && !triggerEvent;
 
     if (link) {
       textEl.on('click', function () {
@@ -131,6 +132,10 @@ echarts.extendComponentView({
       });
     }
 
+    textEl.eventData = subTextEl.eventData = triggerEvent ? {
+      componentType: 'title',
+      componentIndex: titleModel.componentIndex
+    } : null;
     group.add(textEl);
     subText && group.add(subTextEl); // If no subText, but add subTextEl, there will be an empty line.
 
diff --git a/builder/src/echarts/component/toolbox/feature/Brush.js b/builder/src/echarts/component/toolbox/feature/Brush.js
index a14b0ee..e090c80 100644
--- a/builder/src/echarts/component/toolbox/feature/Brush.js
+++ b/builder/src/echarts/component/toolbox/feature/Brush.js
@@ -43,6 +43,7 @@ Brush.defaultOption = {
   show: true,
   type: ['rect', 'polygon', 'lineX', 'lineY', 'keep', 'clear'],
   icon: {
+    /* eslint-disable */
     rect: 'M7.3,34.7 M0.4,10V-0.2h9.8 M89.6,10V-0.2h-9.8 M0.4,60v10.2h9.8 M89.6,60v10.2h-9.8 M12.3,22.4V10.5h13.1 M33.6,10.5h7.8 M49.1,10.5h7.8 M77.5,22.4V10.5h-13 M12.3,31.1v8.2 M77.7,31.1v8.2 M12.3,47.6v11.9h13.1 M33.6,59.5h7.6 M49.1,59.5 h7.7 M77.5,47.6v11.9h-13',
     // jshint ignore:line
     polygon: 'M55.2,34.9c1.7,0,3.1,1.4,3.1,3.1s-1.4,3.1-3.1,3.1 s-3.1-1.4-3.1-3.1S53.5,34.9,55.2,34.9z M50.4,51c1.7,0,3.1,1.4,3.1,3.1c0,1.7-1.4,3.1-3.1,3.1c-1.7,0-3.1-1.4-3.1-3.1 C47.3,52.4,48.7,51,50.4,51z M55.6,37.1l1.5-7.8 M60.1,13.5l1.6-8.7l-7.8,4 M59,19l-1,5.3 M24,16.1l6.4,4.9l6.4-3.3 M48.5,11.6 l-5.9,3.1 M19.1,12.8L9.7,5.1l1.1,7.7 M13.4,29.8l1,7.3l6.6,1.6 M11.6,18.4l1,6.1 M32.8,41.9 M26.6,40.4 M27.3,40.2l6.1,1.6 M49.9,52.1l-5.6-7.6l-4.9-1.2',
@@ -55,13 +56,19 @@ Brush.defaultOption = {
     // jshint ignore:line
     clear: 'M22,14.7l30.9,31 M52.9,14.7L22,45.7 M4.7,16.8V4.2h13.1 M26,4.2h7.8 M41.6,4.2h7.8 M70.3,16.8V4.2H57.2 M4.7,25.9v8.6 M70.3,25.9v8.6 M4.7,43.2v12.6h13.1 M26,55.8h7.8 M41.6,55.8h7.8 M70.3,43.2v12.6H57.2' // jshint ignore:line
 
+    /* eslint-enable */
+
   },
   // `rect`, `polygon`, `lineX`, `lineY`, `keep`, `clear`
   title: zrUtil.clone(brushLang.title)
 };
 var proto = Brush.prototype; // proto.updateLayout = function (featureModel, ecModel, api) {
 
-proto.render = proto.updateView = function (featureModel, ecModel, api) {
+/* eslint-disable */
+
+proto.render =
+/* eslint-enable */
+proto.updateView = function (featureModel, ecModel, api) {
   var brushType;
   var brushMode;
   var isBrushed;
diff --git a/builder/src/echarts/component/toolbox/feature/DataZoom.js b/builder/src/echarts/component/toolbox/feature/DataZoom.js
index 539fa2d..17c1892 100644
--- a/builder/src/echarts/component/toolbox/feature/DataZoom.js
+++ b/builder/src/echarts/component/toolbox/feature/DataZoom.js
@@ -261,12 +261,12 @@ echarts.registerPreprocessor(function (option) {
     var axisIndicesName = axisName + 'Index';
     var givenAxisIndices = dataZoomOpt[axisIndicesName];
 
-    if (givenAxisIndices != null && givenAxisIndices != 'all' && !zrUtil.isArray(givenAxisIndices)) {
+    if (givenAxisIndices != null && givenAxisIndices !== 'all' && !zrUtil.isArray(givenAxisIndices)) {
       givenAxisIndices = givenAxisIndices === false || givenAxisIndices === 'none' ? [] : [givenAxisIndices];
     }
 
     forEachComponent(axisName, function (axisOpt, axisIndex) {
-      if (givenAxisIndices != null && givenAxisIndices != 'all' && zrUtil.indexOf(givenAxisIndices, axisIndex) === -1) {
+      if (givenAxisIndices != null && givenAxisIndices !== 'all' && zrUtil.indexOf(givenAxisIndices, axisIndex) === -1) {
         return;
       }
 
diff --git a/builder/src/echarts/component/toolbox/feature/MagicType.js b/builder/src/echarts/component/toolbox/feature/MagicType.js
index 2388c37..6f0c594 100644
--- a/builder/src/echarts/component/toolbox/feature/MagicType.js
+++ b/builder/src/echarts/component/toolbox/feature/MagicType.js
@@ -31,11 +31,14 @@ MagicType.defaultOption = {
   type: [],
   // Icon group
   icon: {
+    /* eslint-disable */
     line: 'M4.1,28.9h7.1l9.3-22l7.4,38l9.7-19.7l3,12.8h14.9M4.1,58h51.4',
     bar: 'M6.7,22.9h10V48h-10V22.9zM24.9,13h10v35h-10V13zM43.2,2h10v46h-10V2zM3.1,58h53.7',
     stack: 'M8.2,38.4l-8.4,4.1l30.6,15.3L60,42.5l-8.1-4.1l-21.5,11L8.2,38.4z M51.9,30l-8.1,4.2l-13.4,6.9l-13.9-6.9L8.2,30l-8.4,4.2l8.4,4.2l22.2,11l21.5-11l8.1-4.2L51.9,30z M51.9,21.7l-8.1,4.2L35.7,30l-5.3,2.8L24.9,30l-8.4-4.1l-8.3-4.2l-8.4,4.2L8.2,30l8.3,4.2l13.9,6.9l13.4-6.9l8.1-4.2l8.1-4.1L51.9,21.7zM30.4,2.2L-0.2,17.5l8.4,4.1l8.3,4.2l8.4,4.2l5.5,2.7l5.3-2.7l8.1-4.2l8.1-4.2l8.1-4.1L30.4,2.2z',
     // jshint ignore:line
     tiled: 'M2.3,2.2h22.8V25H2.3V2.2z M35,2.2h22.8V25H35V2.2zM2.3,35h22.8v22.8H2.3V35z M35,35h22.8v22.8H35V35z'
+    /* eslint-enable */
+
   },
   // `line`, `bar`, `stack`, `tiled`
   title: zrUtil.clone(magicTypeLang.title),
@@ -146,7 +149,7 @@ proto.onclick = function (ecModel, api, type) {
           newOption[axisType][axisIndex] = newOption[axisType][axisIndex] || {};
         }
 
-        newOption[axisType][axisIndex].boundaryGap = type === 'bar' ? true : false;
+        newOption[axisType][axisIndex].boundaryGap = type === 'bar';
       }
     }
   };
diff --git a/builder/src/echarts/component/toolbox/feature/Restore.js b/builder/src/echarts/component/toolbox/feature/Restore.js
index ea78d65..ce4fbf4 100644
--- a/builder/src/echarts/component/toolbox/feature/Restore.js
+++ b/builder/src/echarts/component/toolbox/feature/Restore.js
@@ -28,7 +28,11 @@ function Restore(model) {
 
 Restore.defaultOption = {
   show: true,
+
+  /* eslint-disable */
   icon: 'M3.8,33.4 M47,18.9h9.8V8.7 M56.3,20.1 C52.1,9,40.5,0.6,26.8,2.1C12.6,3.7,1.6,16.2,2.1,30.6 M13,41.1H3.1v10.2 M3.7,39.9c4.2,11.1,15.8,19.5,29.5,18 c14.2-1.6,25.2-14.1,24.7-28.5',
+
+  /* eslint-enable */
   title: restoreLang.title
 };
 var proto = Restore.prototype;
diff --git a/builder/src/echarts/component/toolbox/feature/SaveAsImage.js b/builder/src/echarts/component/toolbox/feature/SaveAsImage.js
index 79fca51..c6e51c4 100644
--- a/builder/src/echarts/component/toolbox/feature/SaveAsImage.js
+++ b/builder/src/echarts/component/toolbox/feature/SaveAsImage.js
@@ -16,6 +16,8 @@
 * specific language governing permissions and limitations
 * under the License.
 */
+
+/* global Uint8Array */
 import env from 'zrender/src/core/env';
 import lang from '../../../lang';
 import * as featureManager from '../featureManager';
diff --git a/builder/src/echarts/component/tooltip/TooltipContent.js b/builder/src/echarts/component/tooltip/TooltipContent.js
index 8135198..ef22fba 100644
--- a/builder/src/echarts/component/tooltip/TooltipContent.js
+++ b/builder/src/echarts/component/tooltip/TooltipContent.js
@@ -190,7 +190,13 @@ TooltipContent.prototype = {
     var el = this.el;
     el.style.cssText = gCssText + assembleCssText(tooltipModel) // http://stackoverflow.com/questions/21125587/css3-transition-not-working-in-chrome-anymore
     + ';left:' + this._x + 'px;top:' + this._y + 'px;' + (tooltipModel.get('extraCssText') || '');
-    el.style.display = el.innerHTML ? 'block' : 'none';
+    el.style.display = el.innerHTML ? 'block' : 'none'; // If mouse occsionally move over the tooltip, a mouseout event will be
+    // triggered by canvas, and cuase some unexpectable result like dragging
+    // stop, "unfocusAdjacency". Here `pointer-events: none` is used to solve
+    // it. Although it is not suppored by IE8~IE10, fortunately it is a rare
+    // scenario.
+
+    el.style.pointerEvents = this._enterable ? 'auto' : 'none';
     this._show = true;
   },
   setContent: function (content) {
@@ -239,6 +245,25 @@ TooltipContent.prototype = {
   },
   isShow: function () {
     return this._show;
+  },
+  getOuterSize: function () {
+    var width = this.el.clientWidth;
+    var height = this.el.clientHeight; // Consider browser compatibility.
+    // IE8 does not support getComputedStyle.
+
+    if (document.defaultView && document.defaultView.getComputedStyle) {
+      var stl = document.defaultView.getComputedStyle(this.el);
+
+      if (stl) {
+        width += parseInt(stl.paddingLeft, 10) + parseInt(stl.paddingRight, 10) + parseInt(stl.borderLeftWidth, 10) + parseInt(stl.borderRightWidth, 10);
+        height += parseInt(stl.paddingTop, 10) + parseInt(stl.paddingBottom, 10) + parseInt(stl.borderTopWidth, 10) + parseInt(stl.borderBottomWidth, 10);
+      }
+    }
+
+    return {
+      width: width,
+      height: height
+    };
   }
 };
 export default TooltipContent;
\ No newline at end of file
diff --git a/builder/src/echarts/component/tooltip/TooltipModel.js b/builder/src/echarts/component/tooltip/TooltipModel.js
index c77e9de..08cf29e 100644
--- a/builder/src/echarts/component/tooltip/TooltipModel.js
+++ b/builder/src/echarts/component/tooltip/TooltipModel.js
@@ -22,7 +22,7 @@ export default echarts.extendComponentModel({
   dependencies: ['axisPointer'],
   defaultOption: {
     zlevel: 0,
-    z: 8,
+    z: 60,
     show: true,
     // tooltip主体内容
     showContent: true,
@@ -34,6 +34,11 @@ export default echarts.extendComponentModel({
     alwaysShowContent: false,
     displayMode: 'single',
     // 'single' | 'multipleByCoordSys'
+    renderMode: 'auto',
+    // 'auto' | 'html' | 'richText'
+    // 'auto': use html by default, and use non-html if `document` is not defined
+    // 'html': use html for tooltip
+    // 'richText': use canvas, svg, and etc. for tooltip
     // 位置 {Array} | {Function}
     // position: null
     // Consider triggered from axisPointer handle, verticalAlign should be 'middle'
diff --git a/builder/src/echarts/component/tooltip/TooltipRichContent.js b/builder/src/echarts/component/tooltip/TooltipRichContent.js
new file mode 100644
index 0000000..260087c
--- /dev/null
+++ b/builder/src/echarts/component/tooltip/TooltipRichContent.js
@@ -0,0 +1,174 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements.  See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership.  The ASF licenses this file
+* to you under the Apache License, Version 2.0 (the
+* "License"); you may not use this file except in compliance
+* with the License.  You may obtain a copy of the License at
+*
+*   http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied.  See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+import * as zrUtil from 'zrender/src/core/util'; // import Group from 'zrender/src/container/Group';
+
+import Text from 'zrender/src/graphic/Text';
+/**
+ * @alias module:echarts/component/tooltip/TooltipRichContent
+ * @constructor
+ */
+
+function TooltipRichContent(api) {
+  this._zr = api.getZr();
+  this._show = false;
+  /**
+   * @private
+   */
+
+  this._hideTimeout;
+}
+
+TooltipRichContent.prototype = {
+  constructor: TooltipRichContent,
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  _enterable: true,
+
+  /**
+   * Update when tooltip is rendered
+   */
+  update: function () {// noop
+  },
+  show: function (tooltipModel) {
+    if (this._hideTimeout) {
+      clearTimeout(this._hideTimeout);
+    }
+
+    this.el.attr('show', true);
+    this._show = true;
+  },
+
+  /**
+   * Set tooltip content
+   *
+   * @param {string} content rich text string of content
+   * @param {Object} markerRich rich text style
+   * @param {Object} tooltipModel tooltip model
+   */
+  setContent: function (content, markerRich, tooltipModel) {
+    if (this.el) {
+      this._zr.remove(this.el);
+    }
+
+    var markers = {};
+    var text = content;
+    var prefix = '{marker';
+    var suffix = '|}';
+    var startId = text.indexOf(prefix);
+
+    while (startId >= 0) {
+      var endId = text.indexOf(suffix);
+      var name = text.substr(startId + prefix.length, endId - startId - prefix.length);
+
+      if (name.indexOf('sub') > -1) {
+        markers['marker' + name] = {
+          textWidth: 4,
+          textHeight: 4,
+          textBorderRadius: 2,
+          textBackgroundColor: markerRich[name],
+          // TODO: textOffset is not implemented for rich text
+          textOffset: [3, 0]
+        };
+      } else {
+        markers['marker' + name] = {
+          textWidth: 10,
+          textHeight: 10,
+          textBorderRadius: 5,
+          textBackgroundColor: markerRich[name]
+        };
+      }
+
+      text = text.substr(endId + 1);
+      startId = text.indexOf('{marker');
+    }
+
+    this.el = new Text({
+      style: {
+        rich: markers,
+        text: content,
+        textLineHeight: 20,
+        textBackgroundColor: tooltipModel.get('backgroundColor'),
+        textBorderRadius: tooltipModel.get('borderRadius'),
+        textFill: tooltipModel.get('textStyle.color'),
+        textPadding: tooltipModel.get('padding')
+      },
+      z: tooltipModel.get('z')
+    });
+
+    this._zr.add(this.el);
+
+    var self = this;
+    this.el.on('mouseover', function () {
+      // clear the timeout in hideLater and keep showing tooltip
+      if (self._enterable) {
+        clearTimeout(self._hideTimeout);
+        self._show = true;
+      }
+
+      self._inContent = true;
+    });
+    this.el.on('mouseout', function () {
+      if (self._enterable) {
+        if (self._show) {
+          self.hideLater(self._hideDelay);
+        }
+      }
+
+      self._inContent = false;
+    });
+  },
+  setEnterable: function (enterable) {
+    this._enterable = enterable;
+  },
+  getSize: function () {
+    var bounding = this.el.getBoundingRect();
+    return [bounding.width, bounding.height];
+  },
+  moveTo: function (x, y) {
+    if (this.el) {
+      this.el.attr('position', [x, y]);
+    }
+  },
+  hide: function () {
+    this.el.hide();
+    this._show = false;
+  },
+  hideLater: function (time) {
+    if (this._show && !(this._inContent && this._enterable)) {
+      if (time) {
+        this._hideDelay = time; // Set show false to avoid invoke hideLater mutiple times
+
+        this._show = false;
+        this._hideTimeout = setTimeout(zrUtil.bind(this.hide, this), time);
+      } else {
+        this.hide();
+      }
+    }
+  },
+  isShow: function () {
+    return this._show;
+  },
+  getOuterSize: function () {
+    return this.getSize();
+  }
+};
+export default TooltipRichContent;
\ No newline at end of file
diff --git a/builder/src/echarts/component/tooltip/TooltipView.js b/builder/src/echarts/component/tooltip/TooltipView.js
index 686f9fb..fc80e32 100644
--- a/builder/src/echarts/component/tooltip/TooltipView.js
+++ b/builder/src/echarts/component/tooltip/TooltipView.js
@@ -20,6 +20,7 @@ import * as echarts from '../../echarts';
 import * as zrUtil from 'zrender/src/core/util';
 import env from 'zrender/src/core/env';
 import TooltipContent from './TooltipContent';
+import TooltipRichContent from './TooltipRichContent';
 import * as formatUtil from '../../util/format';
 import * as numberUtil from '../../util/number';
 import * as graphic from '../../util/graphic';
@@ -29,6 +30,7 @@ import Model from '../../model/Model';
 import * as globalListener from '../axisPointer/globalListener';
 import * as axisHelper from '../../coord/axisHelper';
 import * as axisPointerViewHelper from '../axisPointer/viewHelper';
+import { getTooltipRenderMode } from '../../util/model';
 var bind = zrUtil.bind;
 var each = zrUtil.each;
 var parsePercent = numberUtil.parsePercent;
@@ -47,11 +49,23 @@ export default echarts.extendComponentView({
       return;
     }
 
-    var tooltipContent = new TooltipContent(api.getDom(), api);
+    var tooltipModel = ecModel.getComponent('tooltip');
+    var renderMode = tooltipModel.get('renderMode');
+    this._renderMode = getTooltipRenderMode(renderMode);
+    var tooltipContent;
+
+    if (this._renderMode === 'html') {
+      tooltipContent = new TooltipContent(api.getDom(), api);
+      this._newLine = '<br/>';
+    } else {
+      tooltipContent = new TooltipRichContent(api);
+      this._newLine = '\n';
+    }
+
     this._tooltipContent = tooltipContent;
   },
   render: function (tooltipModel, ecModel, api) {
-    if (env.node || env.wxa) {
+    if (env.node) {
       return;
     } // Reset
 
@@ -310,6 +324,9 @@ export default echarts.extendComponentView({
     var singleDefaultHTML = [];
     var singleParamsList = [];
     var singleTooltipModel = buildTooltipModel([e.tooltipOption, globalTooltipModel]);
+    var renderMode = this._renderMode;
+    var newLine = this._newLine;
+    var markers = {};
     each(dataByCoordSys, function (itemCoordSys) {
       // var coordParamList = [];
       // var coordDefaultHTML = [];
@@ -344,7 +361,18 @@ export default echarts.extendComponentView({
 
           if (dataParams) {
             singleParamsList.push(dataParams);
-            seriesDefaultHTML.push(series.formatTooltip(dataIndex, true));
+            var seriesTooltip = series.formatTooltip(dataIndex, true, null, renderMode);
+            var html;
+
+            if (zrUtil.isObject(seriesTooltip)) {
+              html = seriesTooltip.html;
+              var newMarkers = seriesTooltip.markers;
+              zrUtil.merge(markers, newMarkers);
+            } else {
+              html = seriesTooltip;
+            }
+
+            seriesDefaultHTML.push(html);
           }
         }); // Default tooltip content
         // FIXME
@@ -352,19 +380,24 @@ export default echarts.extendComponentView({
         // (2) themeRiver, firstDataIndex is array, and first line is unnecessary.
 
         var firstLine = valueLabel;
-        singleDefaultHTML.push((firstLine ? formatUtil.encodeHTML(firstLine) + '<br />' : '') + seriesDefaultHTML.join('<br />'));
+
+        if (renderMode !== 'html') {
+          singleDefaultHTML.push(seriesDefaultHTML.join(newLine));
+        } else {
+          singleDefaultHTML.push((firstLine ? formatUtil.encodeHTML(firstLine) + newLine : '') + seriesDefaultHTML.join(newLine));
+        }
       });
     }, this); // In most case, the second axis is shown upper than the first one.
 
     singleDefaultHTML.reverse();
-    singleDefaultHTML = singleDefaultHTML.join('<br /><br />');
+    singleDefaultHTML = singleDefaultHTML.join(this._newLine + this._newLine);
     var positionExpr = e.position;
 
     this._showOrMove(singleTooltipModel, function () {
       if (this._updateContentNotChangedOnAxis(dataByCoordSys)) {
         this._updatePosition(singleTooltipModel, positionExpr, point[0], point[1], this._tooltipContent, singleParamsList);
       } else {
-        this._showTooltipContent(singleTooltipModel, singleDefaultHTML, singleParamsList, Math.random(), point[0], point[1], positionExpr);
+        this._showTooltipContent(singleTooltipModel, singleDefaultHTML, singleParamsList, Math.random(), point[0], point[1], positionExpr, undefined, markers);
       }
     }); // Do not trigger events here, because this branch only be entered
     // from dispatchAction.
@@ -390,11 +423,22 @@ export default echarts.extendComponentView({
     }
 
     var params = dataModel.getDataParams(dataIndex, dataType);
-    var defaultHtml = dataModel.formatTooltip(dataIndex, false, dataType);
+    var seriesTooltip = dataModel.formatTooltip(dataIndex, false, dataType, this._renderMode);
+    var defaultHtml;
+    var markers;
+
+    if (zrUtil.isObject(seriesTooltip)) {
+      defaultHtml = seriesTooltip.html;
+      markers = seriesTooltip.markers;
+    } else {
+      defaultHtml = seriesTooltip;
+      markers = null;
+    }
+
     var asyncTicket = 'item_' + dataModel.name + '_' + dataIndex;
 
     this._showOrMove(tooltipModel, function () {
-      this._showTooltipContent(tooltipModel, defaultHtml, params, asyncTicket, e.offsetX, e.offsetY, e.position, e.target);
+      this._showTooltipContent(tooltipModel, defaultHtml, params, asyncTicket, e.offsetX, e.offsetY, e.position, e.target, markers);
     }); // FIXME
     // duplicated showtip if manuallyShowTip is called from dispatchAction.
 
@@ -435,7 +479,7 @@ export default echarts.extendComponentView({
       from: this.uid
     });
   },
-  _showTooltipContent: function (tooltipModel, defaultHtml, params, asyncTicket, x, y, positionExpr, el) {
+  _showTooltipContent: function (tooltipModel, defaultHtml, params, asyncTicket, x, y, positionExpr, el, markers) {
     // Reset ticket
     this._ticket = '';
 
@@ -453,7 +497,7 @@ export default echarts.extendComponentView({
     } else if (typeof formatter === 'function') {
       var callback = bind(function (cbTicket, html) {
         if (cbTicket === this._ticket) {
-          tooltipContent.setContent(html);
+          tooltipContent.setContent(html, markers, tooltipModel);
 
           this._updatePosition(tooltipModel, positionExpr, x, y, tooltipContent, params, el);
         }
@@ -462,7 +506,7 @@ export default echarts.extendComponentView({
       html = formatter(params, asyncTicket, callback);
     }
 
-    tooltipContent.setContent(html);
+    tooltipContent.setContent(html, markers, tooltipModel);
     tooltipContent.show(tooltipModel);
 
     this._updatePosition(tooltipModel, positionExpr, x, y, tooltipContent, params, el);
@@ -520,7 +564,7 @@ export default echarts.extendComponentView({
         x = pos[0];
         y = pos[1];
       } else {
-        var pos = refixTooltipPosition(x, y, content.el, viewWidth, viewHeight, align ? null : 20, vAlign ? null : 20);
+        var pos = refixTooltipPosition(x, y, content, viewWidth, viewHeight, align ? null : 20, vAlign ? null : 20);
         x = pos[0];
         y = pos[1];
       }
@@ -529,7 +573,7 @@ export default echarts.extendComponentView({
     vAlign && (y -= isCenterAlign(vAlign) ? contentSize[1] / 2 : vAlign === 'bottom' ? contentSize[1] : 0);
 
     if (tooltipModel.get('confine')) {
-      var pos = confineTooltipPosition(x, y, content.el, viewWidth, viewHeight);
+      var pos = confineTooltipPosition(x, y, content, viewWidth, viewHeight);
       x = pos[0];
       y = pos[1];
     }
@@ -572,7 +616,7 @@ export default echarts.extendComponentView({
     });
   },
   dispose: function (ecModel, api) {
-    if (env.node || env.wxa) {
+    if (env.node) {
       return;
     }
 
@@ -619,8 +663,8 @@ function makeDispatchAction(payload, api) {
   return payload.dispatchAction || zrUtil.bind(api.dispatchAction, api);
 }
 
-function refixTooltipPosition(x, y, el, viewWidth, viewHeight, gapH, gapV) {
-  var size = getOuterSize(el);
+function refixTooltipPosition(x, y, content, viewWidth, viewHeight, gapH, gapV) {
+  var size = content.getOuterSize();
   var width = size.width;
   var height = size.height;
 
@@ -643,8 +687,8 @@ function refixTooltipPosition(x, y, el, viewWidth, viewHeight, gapH, gapV) {
   return [x, y];
 }
 
-function confineTooltipPosition(x, y, el, viewWidth, viewHeight) {
-  var size = getOuterSize(el);
+function confineTooltipPosition(x, y, content, viewWidth, viewHeight) {
+  var size = content.getOuterSize();
   var width = size.width;
   var height = size.height;
   x = Math.min(x + width, viewWidth) - width;
@@ -654,26 +698,6 @@ function confineTooltipPosition(x, y, el, viewWidth, viewHeight) {
   return [x, y];
 }
 
-function getOuterSize(el) {
-  var width = el.clientWidth;
-  var height = el.clientHeight; // Consider browser compatibility.
-  // IE8 does not support getComputedStyle.
-
-  if (document.defaultView && document.defaultView.getComputedStyle) {
-    var stl = document.defaultView.getComputedStyle(el);
-
-    if (stl) {
-      width += parseInt(stl.paddingLeft, 10) + parseInt(stl.paddingRight, 10) + parseInt(stl.borderLeftWidth, 10) + parseInt(stl.borderRightWidth, 10);
-      height += parseInt(stl.paddingTop, 10) + parseInt(stl.paddingBottom, 10) + parseInt(stl.borderTopWidth, 10) + parseInt(stl.borderBottomWidth, 10);
-    }
-  }
-
-  return {
-    width: width,
-    height: height
-  };
-}
-
 function calcTooltipPosition(position, rect, contentSize) {
   var domWidth = contentSize[0];
   var domHeight = contentSize[1];
diff --git a/builder/src/echarts/coord/axisHelper.js b/builder/src/echarts/coord/axisHelper.js
index 5a679ac..8fcff0b 100644
--- a/builder/src/echarts/coord/axisHelper.js
+++ b/builder/src/echarts/coord/axisHelper.js
@@ -265,6 +265,9 @@ export function makeLabelFormatter(axis) {
   if (typeof labelFormatter === 'string') {
     labelFormatter = function (tpl) {
       return function (val) {
+        // For category axis, get raw value; for numeric axis,
+        // get foramtted label like '1,333,444'.
+        val = axis.scale.getLabel(val);
         return tpl.replace('{value}', val != null ? val : '');
       };
     }(labelFormatter); // Consider empty array
diff --git a/builder/src/echarts/coord/axisModelCommonMixin.js b/builder/src/echarts/coord/axisModelCommonMixin.js
index f83ab71..71c1bf0 100644
--- a/builder/src/echarts/coord/axisModelCommonMixin.js
+++ b/builder/src/echarts/coord/axisModelCommonMixin.js
@@ -16,8 +16,8 @@
 * specific language governing permissions and limitations
 * under the License.
 */
-import * as zrUtil from 'zrender/src/core/util';
-import * as axisHelper from './axisHelper';
+import * as zrUtil from 'zrender/src/core/util'; // import * as axisHelper from './axisHelper';
+
 export default {
   /**
    * @param {boolean} origin
diff --git a/builder/src/echarts/coord/axisTickLabelBuilder.js b/builder/src/echarts/coord/axisTickLabelBuilder.js
index f10c53e..529a824 100644
--- a/builder/src/echarts/coord/axisTickLabelBuilder.js
+++ b/builder/src/echarts/coord/axisTickLabelBuilder.js
@@ -208,8 +208,7 @@ export function calculateCategoryInterval(axis) {
 
   for (; tickValue <= ordinalExtent[1]; tickValue += step) {
     var width = 0;
-    var height = 0; // Polar is also calculated in assumptive linear layout here.
-    // Not precise, do not consider align and vertical align
+    var height = 0; // Not precise, do not consider align and vertical align
     // and each distance from axis line yet.
 
     var rect = textContain.getBoundingRect(labelFormatter(tickValue), params.font, 'center', 'top'); // Magic number
diff --git a/builder/src/echarts/coord/cartesian/Grid.js b/builder/src/echarts/coord/cartesian/Grid.js
index a975be5..3772f2c 100644
--- a/builder/src/echarts/coord/cartesian/Grid.js
+++ b/builder/src/echarts/coord/cartesian/Grid.js
@@ -89,29 +89,31 @@ gridProto.update = function (ecModel, api) {
   });
   each(axesMap.y, function (yAxis) {
     niceScaleExtent(yAxis.scale, yAxis.model);
-  });
+  }); // Key: axisDim_axisIndex, value: boolean, whether onZero target.
+
+  var onZeroRecords = {};
   each(axesMap.x, function (xAxis) {
-    fixAxisOnZero(axesMap, 'y', xAxis);
+    fixAxisOnZero(axesMap, 'y', xAxis, onZeroRecords);
   });
   each(axesMap.y, function (yAxis) {
-    fixAxisOnZero(axesMap, 'x', yAxis);
+    fixAxisOnZero(axesMap, 'x', yAxis, onZeroRecords);
   }); // Resize again if containLabel is enabled
   // FIXME It may cause getting wrong grid size in data processing stage
 
   this.resize(this.model, api);
 };
 
-function fixAxisOnZero(axesMap, otherAxisDim, axis) {
+function fixAxisOnZero(axesMap, otherAxisDim, axis, onZeroRecords) {
   axis.getAxesOnZeroOf = function () {
     // TODO: onZero of multiple axes.
-    return otherAxis ? [otherAxis] : [];
+    return otherAxisOnZeroOf ? [otherAxisOnZeroOf] : [];
   }; // onZero can not be enabled in these two situations:
   // 1. When any other axis is a category axis.
   // 2. When no axis is cross 0 point.
 
 
   var otherAxes = axesMap[otherAxisDim];
-  var otherAxis;
+  var otherAxisOnZeroOf;
   var axisModel = axis.model;
   var onZero = axisModel.get('axisLine.onZero');
   var onZeroAxisIndex = axisModel.get('axisLine.onZeroAxisIndex');
@@ -123,18 +125,26 @@ function fixAxisOnZero(axesMap, otherAxisDim, axis) {
 
   if (onZeroAxisIndex != null) {
     if (canOnZeroToAxis(otherAxes[onZeroAxisIndex])) {
-      otherAxis = otherAxes[onZeroAxisIndex];
+      otherAxisOnZeroOf = otherAxes[onZeroAxisIndex];
     }
+  } else {
+    // Find the first available other axis.
+    for (var idx in otherAxes) {
+      if (otherAxes.hasOwnProperty(idx) && canOnZeroToAxis(otherAxes[idx]) // Consider that two Y axes on one value axis,
+      // if both onZero, the two Y axes overlap.
+      && !onZeroRecords[getOnZeroRecordKey(otherAxes[idx])]) {
+        otherAxisOnZeroOf = otherAxes[idx];
+        break;
+      }
+    }
+  }
 
-    return;
-  } // Find the first available other axis.
-
+  if (otherAxisOnZeroOf) {
+    onZeroRecords[getOnZeroRecordKey(otherAxisOnZeroOf)] = true;
+  }
 
-  for (var idx in otherAxes) {
-    if (otherAxes.hasOwnProperty(idx) && canOnZeroToAxis(otherAxes[idx])) {
-      otherAxis = otherAxes[idx];
-      break;
-    }
+  function getOnZeroRecordKey(axis) {
+    return axis.dim + '_' + axis.index;
   }
 }
 
diff --git a/builder/src/echarts/coord/cartesian/cartesianAxisHelper.js b/builder/src/echarts/coord/cartesian/cartesianAxisHelper.js
index f0082e3..7ff2112 100644
--- a/builder/src/echarts/coord/cartesian/cartesianAxisHelper.js
+++ b/builder/src/echarts/coord/cartesian/cartesianAxisHelper.js
@@ -51,7 +51,7 @@ export function layout(gridModel, axisModel, opt) {
 
   if (otherAxisOnZeroOf) {
     var onZeroCoord = otherAxisOnZeroOf.toGlobalCoord(otherAxisOnZeroOf.dataToCoord(0));
-    posBound[idx['onZero']] = Math.max(Math.min(onZeroCoord, posBound[1]), posBound[0]);
+    posBound[idx.onZero] = Math.max(Math.min(onZeroCoord, posBound[1]), posBound[0]);
   } // Axis position
 
 
@@ -66,7 +66,7 @@ export function layout(gridModel, axisModel, opt) {
     right: 1
   };
   layout.labelDirection = layout.tickDirection = layout.nameDirection = dirMap[rawAxisPosition];
-  layout.labelOffset = otherAxisOnZeroOf ? posBound[idx[rawAxisPosition]] - posBound[idx['onZero']] : 0;
+  layout.labelOffset = otherAxisOnZeroOf ? posBound[idx[rawAxisPosition]] - posBound[idx.onZero] : 0;
 
   if (axisModel.get('axisTick.inside')) {
     layout.tickDirection = -layout.tickDirection;
diff --git a/builder/src/echarts/coord/geo/Geo.js b/builder/src/echarts/coord/geo/Geo.js
index 939159d..3069bbb 100644
--- a/builder/src/echarts/coord/geo/Geo.js
+++ b/builder/src/echarts/coord/geo/Geo.js
@@ -18,26 +18,22 @@
 */
 import * as zrUtil from 'zrender/src/core/util';
 import BoundingRect from 'zrender/src/core/BoundingRect';
-import parseGeoJson from './parseGeoJson';
 import View from '../View';
-import fixNanhai from './fix/nanhai';
-import fixTextCoord from './fix/textCoord';
-import fixGeoCoord from './fix/geoCoord';
-import fixDiaoyuIsland from './fix/diaoyuIsland'; // Geo fix functions
-
-var geoFixFuncs = [fixNanhai, fixTextCoord, fixGeoCoord, fixDiaoyuIsland];
+import geoSourceManager from './geoSourceManager';
 /**
  * [Geo description]
- * @param {string} name Geo name
+ * For backward compatibility, the orginal interface:
+ * `name, map, geoJson, specialAreas, nameMap` is kept.
+ *
+ * @param {string|Object} name
  * @param {string} map Map type
- * @param {Object} geoJson
- * @param {Object} [specialAreas]
  *        Specify the positioned areas by left, top, width, height
  * @param {Object.<string, string>} [nameMap]
  *        Specify name alias
+ * @param {boolean} [invertLongitute=true]
  */
 
-function Geo(name, map, geoJson, specialAreas, nameMap) {
+function Geo(name, map, nameMap, invertLongitute) {
   View.call(this, name);
   /**
    * Map type
@@ -45,8 +41,20 @@ function Geo(name, map, geoJson, specialAreas, nameMap) {
    */
 
   this.map = map;
-  this._nameCoordMap = zrUtil.createHashMap();
-  this.loadGeoJson(geoJson, specialAreas, nameMap);
+  var source = geoSourceManager.load(map, nameMap);
+  this._nameCoordMap = source.nameCoordMap;
+  this._regionsMap = source.nameCoordMap;
+  this._invertLongitute = invertLongitute == null ? true : invertLongitute;
+  /**
+   * @readOnly
+   */
+
+  this.regions = source.regions;
+  /**
+   * @type {module:zrender/src/core/BoundingRect}
+   */
+
+  this._rect = source.boundingRect;
 }
 
 Geo.prototype = {
@@ -77,59 +85,27 @@ Geo.prototype = {
   },
 
   /**
-   * @param {Object} geoJson
-   * @param {Object} [specialAreas]
-   *        Specify the positioned areas by left, top, width, height
-   * @param {Object.<string, string>} [nameMap]
-   *        Specify name alias
+   * @override
    */
-  loadGeoJson: function (geoJson, specialAreas, nameMap) {
-    // https://jsperf.com/try-catch-performance-overhead
-    try {
-      this.regions = geoJson ? parseGeoJson(geoJson) : [];
-    } catch (e) {
-      throw 'Invalid geoJson format\n' + e.message;
-    }
-
-    specialAreas = specialAreas || {};
-    nameMap = nameMap || {};
-    var regions = this.regions;
-    var regionsMap = zrUtil.createHashMap();
-
-    for (var i = 0; i < regions.length; i++) {
-      var regionName = regions[i].name; // Try use the alias in nameMap
-
-      regionName = nameMap.hasOwnProperty(regionName) ? nameMap[regionName] : regionName;
-      regions[i].name = regionName;
-      regionsMap.set(regionName, regions[i]); // Add geoJson
-
-      this.addGeoCoord(regionName, regions[i].center); // Some area like Alaska in USA map needs to be tansformed
-      // to look better
-
-      var specialArea = specialAreas[regionName];
-
-      if (specialArea) {
-        regions[i].transformTo(specialArea.left, specialArea.top, specialArea.width, specialArea.height);
-      }
-    }
-
-    this._regionsMap = regionsMap;
-    this._rect = null;
-    zrUtil.each(geoFixFuncs, function (fixFunc) {
-      fixFunc(this);
-    }, this);
-  },
-  // Overwrite
   transformTo: function (x, y, width, height) {
     var rect = this.getBoundingRect();
-    rect = rect.clone(); // Longitute is inverted
+    var invertLongitute = this._invertLongitute;
+    rect = rect.clone();
+
+    if (invertLongitute) {
+      // Longitute is inverted
+      rect.y = -rect.y - rect.height;
+    }
 
-    rect.y = -rect.y - rect.height;
     var rawTransformable = this._rawTransformable;
     rawTransformable.transform = rect.calculateTransform(new BoundingRect(x, y, width, height));
     rawTransformable.decomposeTransform();
-    var scale = rawTransformable.scale;
-    scale[1] = -scale[1];
+
+    if (invertLongitute) {
+      var scale = rawTransformable.scale;
+      scale[1] = -scale[1];
+    }
+
     rawTransformable.updateTransform();
 
     this._updateTransform();
@@ -169,23 +145,12 @@ Geo.prototype = {
   getGeoCoord: function (name) {
     return this._nameCoordMap.get(name);
   },
-  // Overwrite
-  getBoundingRect: function () {
-    if (this._rect) {
-      return this._rect;
-    }
-
-    var rect;
-    var regions = this.regions;
-
-    for (var i = 0; i < regions.length; i++) {
-      var regionRect = regions[i].getBoundingRect();
-      rect = rect || regionRect.clone();
-      rect.union(regionRect);
-    } // FIXME Always return new ?
 
-
-    return this._rect = rect || new BoundingRect(0, 0, 0, 0);
+  /**
+   * @override
+   */
+  getBoundingRect: function () {
+    return this._rect;
   },
 
   /**
@@ -206,12 +171,12 @@ Geo.prototype = {
   },
 
   /**
-   * @inheritDoc
+   * @override
    */
   convertToPixel: zrUtil.curry(doConvert, 'dataToPoint'),
 
   /**
-   * @inheritDoc
+   * @override
    */
   convertFromPixel: zrUtil.curry(doConvert, 'pointToData')
 };
diff --git a/builder/src/echarts/coord/geo/GeoModel.js b/builder/src/echarts/coord/geo/GeoModel.js
index b08a5ae..ff33e6d 100644
--- a/builder/src/echarts/coord/geo/GeoModel.js
+++ b/builder/src/echarts/coord/geo/GeoModel.js
@@ -60,7 +60,9 @@ var GeoModel = ComponentModel.extend({
     // bottom
     // Aspect is width / height. Inited to be geoJson bbox aspect
     // This parameter is used for scale this aspect
-    aspectScale: 0.75,
+    // If svg used, aspectScale is 1 by default.
+    // aspectScale: 0.75,
+    aspectScale: null,
     ///// Layout with center and size
     // If you wan't to put map in a fixed size box with right aspect ratio
     // This two properties may more conveninet
diff --git a/builder/src/echarts/coord/geo/Region.js b/builder/src/echarts/coord/geo/Region.js
index 6e51bd4..3a84bd2 100644
--- a/builder/src/echarts/coord/geo/Region.js
+++ b/builder/src/echarts/coord/geo/Region.js
@@ -25,7 +25,7 @@ import * as bbox from 'zrender/src/core/bbox';
 import * as vec2 from 'zrender/src/core/vector';
 import * as polygonContain from 'zrender/src/contain/polygon';
 /**
- * @param {string} name
+ * @param {string|Region} name
  * @param {Array} geometries
  * @param {Array.<number>} cp
  */
@@ -172,6 +172,14 @@ Region.prototype = {
     rect.copy(target); // Update center
 
     this.center = [rect.x + rect.width / 2, rect.y + rect.height / 2];
+  },
+  cloneShallow: function (name) {
+    name == null && (name = this.name);
+    var newRegion = new Region(name, this.geometries, this.center);
+    newRegion._rect = this._rect;
+    newRegion.transformTo = null; // Simply avoid to be called.
+
+    return newRegion;
   }
 };
 export default Region;
\ No newline at end of file
diff --git a/builder/src/echarts/coord/geo/fix/diaoyuIsland.js b/builder/src/echarts/coord/geo/fix/diaoyuIsland.js
index c49f0be..9fcfdf0 100644
--- a/builder/src/echarts/coord/geo/fix/diaoyuIsland.js
+++ b/builder/src/echarts/coord/geo/fix/diaoyuIsland.js
@@ -21,15 +21,11 @@
 // var zrUtil = require('zrender/src/core/util');
 // var geoCoord = [126, 25];
 var points = [[[123.45165252685547, 25.73527164402261], [123.49731445312499, 25.73527164402261], [123.49731445312499, 25.750734064600884], [123.45165252685547, 25.750734064600884], [123.45165252685547, 25.73527164402261]]];
-export default function (geo) {
-  if (geo.map === 'china') {
-    for (var i = 0, len = geo.regions.length; i < len; ++i) {
-      if (geo.regions[i].name === '台湾') {
-        geo.regions[i].geometries.push({
-          type: 'polygon',
-          exterior: points[0]
-        });
-      }
-    }
+export default function (mapType, region) {
+  if (mapType === 'china' && region.name === '台湾') {
+    region.geometries.push({
+      type: 'polygon',
+      exterior: points[0]
+    });
   }
 }
\ No newline at end of file
diff --git a/builder/src/echarts/coord/geo/fix/geoCoord.js b/builder/src/echarts/coord/geo/fix/geoCoord.js
index 750fdc5..97d9936 100644
--- a/builder/src/echarts/coord/geo/fix/geoCoord.js
+++ b/builder/src/echarts/coord/geo/fix/geoCoord.js
@@ -16,14 +16,13 @@
 * specific language governing permissions and limitations
 * under the License.
 */
-import * as zrUtil from 'zrender/src/core/util';
 var geoCoordMap = {
   'Russia': [100, 60],
   'United States': [-99, 38],
   'United States of America': [-99, 38]
 };
-export default function (geo) {
-  zrUtil.each(geo.regions, function (region) {
+export default function (mapType, region) {
+  if (mapType === 'world') {
     var geoCoord = geoCoordMap[region.name];
 
     if (geoCoord) {
@@ -31,5 +30,5 @@ export default function (geo) {
       cp[0] = geoCoord[0];
       cp[1] = geoCoord[1];
     }
-  });
+  }
 }
\ No newline at end of file
diff --git a/builder/src/echarts/coord/geo/fix/nanhai.js b/builder/src/echarts/coord/geo/fix/nanhai.js
index 678ab10..ebb670b 100644
--- a/builder/src/echarts/coord/geo/fix/nanhai.js
+++ b/builder/src/echarts/coord/geo/fix/nanhai.js
@@ -31,9 +31,9 @@ for (var i = 0; i < points.length; i++) {
   }
 }
 
-export default function (geo) {
-  if (geo.map === 'china') {
-    geo.regions.push(new Region('南海诸岛', zrUtil.map(points, function (exterior) {
+export default function (mapType, regions) {
+  if (mapType === 'china') {
+    regions.push(new Region('南海诸岛', zrUtil.map(points, function (exterior) {
       return {
         type: 'polygon',
         exterior: exterior
diff --git a/builder/src/echarts/coord/geo/fix/textCoord.js b/builder/src/echarts/coord/geo/fix/textCoord.js
index b481422..09fcbcd 100644
--- a/builder/src/echarts/coord/geo/fix/textCoord.js
+++ b/builder/src/echarts/coord/geo/fix/textCoord.js
@@ -16,7 +16,6 @@
 * specific language governing permissions and limitations
 * under the License.
 */
-import * as zrUtil from 'zrender/src/core/util';
 var coordsOffsetMap = {
   '南海诸岛': [32, 80],
   // 全国
@@ -26,8 +25,8 @@ var coordsOffsetMap = {
   //'北京': [-10, 0],
   '天津': [5, 5]
 };
-export default function (geo) {
-  zrUtil.each(geo.regions, function (region) {
+export default function (mapType, region) {
+  if (mapType === 'china') {
     var coordFix = coordsOffsetMap[region.name];
 
     if (coordFix) {
@@ -35,5 +34,5 @@ export default function (geo) {
       cp[0] += coordFix[0] / 10.5;
       cp[1] += -coordFix[1] / (10.5 / 0.75);
     }
-  });
+  }
 }
\ No newline at end of file
diff --git a/builder/src/echarts/coord/geo/geoCreator.js b/builder/src/echarts/coord/geo/geoCreator.js
index 6e10bd6..caadcd3 100644
--- a/builder/src/echarts/coord/geo/geoCreator.js
+++ b/builder/src/echarts/coord/geo/geoCreator.js
@@ -22,6 +22,8 @@ import * as zrUtil from 'zrender/src/core/util';
 import Geo from './Geo';
 import * as layout from '../../util/layout';
 import * as numberUtil from '../../util/number';
+import geoSourceManager from './geoSourceManager';
+import mapDataStorage from './mapDataStorage';
 /**
  * Resize method bound to the geo
  * @param {module:echarts/coord/geo/GeoModel|module:echarts/chart/map/MapModel} geoModel
@@ -46,8 +48,7 @@ function resizeGeo(geoModel, api) {
   var size = geoModel.get('layoutSize');
   var viewWidth = api.getWidth();
   var viewHeight = api.getHeight();
-  var aspectScale = geoModel.get('aspectScale') || 0.75;
-  var aspect = rect.width / rect.height * aspectScale;
+  var aspect = rect.width / rect.height * this.aspectScale;
   var useCenterAndSize = false;
 
   if (center && size) {
@@ -111,8 +112,19 @@ var geoCreator = {
 
     ecModel.eachComponent('geo', function (geoModel, idx) {
       var name = geoModel.get('map');
-      var mapData = echarts.getMap(name);
-      var geo = new Geo(name + idx, name, mapData && mapData.geoJson, mapData && mapData.specialAreas, geoModel.get('nameMap'));
+      var aspectScale = geoModel.get('aspectScale');
+      var invertLongitute = true;
+      var mapRecords = mapDataStorage.retrieveMap(name);
+
+      if (mapRecords && mapRecords[0] && mapRecords[0].type === 'svg') {
+        aspectScale == null && (aspectScale = 1);
+        invertLongitute = false;
+      } else {
+        aspectScale == null && (aspectScale = 0.75);
+      }
+
+      var geo = new Geo(name + idx, name, geoModel.get('nameMap'), invertLongitute);
+      geo.aspectScale = aspectScale;
       geo.zoomLimit = geoModel.get('scaleLimit');
       geoList.push(geo);
       setGeoCoords(geo, geoModel);
@@ -140,17 +152,17 @@ var geoCreator = {
       }
     });
     zrUtil.each(mapModelGroupBySeries, function (mapSeries, mapType) {
-      var mapData = echarts.getMap(mapType);
       var nameMapList = zrUtil.map(mapSeries, function (singleMapSeries) {
         return singleMapSeries.get('nameMap');
       });
-      var geo = new Geo(mapType, mapType, mapData && mapData.geoJson, mapData && mapData.specialAreas, zrUtil.mergeAll(nameMapList));
+      var geo = new Geo(mapType, mapType, zrUtil.mergeAll(nameMapList));
       geo.zoomLimit = zrUtil.retrieve.apply(null, zrUtil.map(mapSeries, function (singleMapSeries) {
         return singleMapSeries.get('scaleLimit');
       }));
       geoList.push(geo); // Inject resize method
 
       geo.resize = resizeGeo;
+      geo.aspectScale = mapSeries[0].get('aspectScale');
       geo.resize(mapSeries[0], api);
       zrUtil.each(mapSeries, function (singleMapSeries) {
         singleMapSeries.coordinateSystem = geo;
@@ -170,35 +182,19 @@ var geoCreator = {
   getFilledRegions: function (originRegionArr, mapName, nameMap) {
     // Not use the original
     var regionsArr = (originRegionArr || []).slice();
-    nameMap = nameMap || {};
-    var map = echarts.getMap(mapName);
-    var geoJson = map && map.geoJson;
-
-    if (!geoJson) {
-      return originRegionArr;
-    }
-
     var dataNameMap = zrUtil.createHashMap();
-    var features = geoJson.features;
 
     for (var i = 0; i < regionsArr.length; i++) {
       dataNameMap.set(regionsArr[i].name, regionsArr[i]);
     }
 
-    for (var i = 0; i < features.length; i++) {
-      var name = features[i].properties.name;
-
-      if (!dataNameMap.get(name)) {
-        if (nameMap.hasOwnProperty(name)) {
-          name = nameMap[name];
-        }
-
-        regionsArr.push({
-          name: name
-        });
-      }
-    }
-
+    var source = geoSourceManager.load(mapName, nameMap);
+    zrUtil.each(source.regions, function (region) {
+      var name = region.name;
+      !dataNameMap.get(name) && regionsArr.push({
+        name: name
+      });
+    });
     return regionsArr;
   }
 };
diff --git a/builder/src/echarts/coord/geo/geoJSONLoader.js b/builder/src/echarts/coord/geo/geoJSONLoader.js
new file mode 100644
index 0000000..6c42fd9
--- /dev/null
+++ b/builder/src/echarts/coord/geo/geoJSONLoader.js
@@ -0,0 +1,82 @@
+/*
+* 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 { each } from 'zrender/src/core/util';
+import parseGeoJson from './parseGeoJson';
+import { makeInner } from '../../util/model'; // Built-in GEO fixer.
+
+import fixNanhai from './fix/nanhai';
+import fixTextCoord from './fix/textCoord';
+import fixGeoCoord from './fix/geoCoord';
+import fixDiaoyuIsland from './fix/diaoyuIsland';
+var inner = makeInner();
+export default {
+  /**
+   * @param {string} mapName
+   * @param {Object} mapRecord {specialAreas, geoJSON}
+   * @return {Object} {regions, boundingRect}
+   */
+  load: function (mapName, mapRecord) {
+    var parsed = inner(mapRecord).parsed;
+
+    if (parsed) {
+      return parsed;
+    }
+
+    var specialAreas = mapRecord.specialAreas || {};
+    var geoJSON = mapRecord.geoJSON;
+    var regions; // https://jsperf.com/try-catch-performance-overhead
+
+    try {
+      regions = geoJSON ? parseGeoJson(geoJSON) : [];
+    } catch (e) {
+      throw new Error('Invalid geoJson format\n' + e.message);
+    }
+
+    each(regions, function (region) {
+      var regionName = region.name;
+      fixTextCoord(mapName, region);
+      fixGeoCoord(mapName, region);
+      fixDiaoyuIsland(mapName, region); // Some area like Alaska in USA map needs to be tansformed
+      // to look better
+
+      var specialArea = specialAreas[regionName];
+
+      if (specialArea) {
+        region.transformTo(specialArea.left, specialArea.top, specialArea.width, specialArea.height);
+      }
+    });
+    fixNanhai(mapName, regions);
+    return inner(mapRecord).parsed = {
+      regions: regions,
+      boundingRect: getBoundingRect(regions)
+    };
+  }
+};
+
+function getBoundingRect(regions) {
+  var rect;
+
+  for (var i = 0; i < regions.length; i++) {
+    var regionRect = regions[i].getBoundingRect();
+    rect = rect || regionRect.clone();
+    rect.union(regionRect);
+  }
+
+  return rect;
+}
\ No newline at end of file
diff --git a/builder/src/echarts/coord/geo/geoSVGLoader.js b/builder/src/echarts/coord/geo/geoSVGLoader.js
new file mode 100644
index 0000000..031ba22
--- /dev/null
+++ b/builder/src/echarts/coord/geo/geoSVGLoader.js
@@ -0,0 +1,132 @@
+/*
+* 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 { parseSVG, makeViewBoxTransform } from 'zrender/src/tool/parseSVG';
+import Group from 'zrender/src/container/Group';
+import Rect from 'zrender/src/graphic/shape/Rect';
+import { assert, createHashMap } from 'zrender/src/core/util';
+import BoundingRect from 'zrender/src/core/BoundingRect';
+import { makeInner } from '../../util/model';
+var inner = makeInner();
+export default {
+  /**
+   * @param {string} mapName
+   * @param {Object} mapRecord {specialAreas, geoJSON}
+   * @return {Object} {root, boundingRect}
+   */
+  load: function (mapName, mapRecord) {
+    var originRoot = inner(mapRecord).originRoot;
+
+    if (originRoot) {
+      return {
+        root: originRoot,
+        boundingRect: inner(mapRecord).boundingRect
+      };
+    }
+
+    var graphic = buildGraphic(mapRecord);
+    inner(mapRecord).originRoot = graphic.root;
+    inner(mapRecord).boundingRect = graphic.boundingRect;
+    return graphic;
+  },
+  makeGraphic: function (mapName, mapRecord, hostKey) {
+    // For performance consideration (in large SVG), graphic only maked
+    // when necessary and reuse them according to hostKey.
+    var field = inner(mapRecord);
+    var rootMap = field.rootMap || (field.rootMap = createHashMap());
+    var root = rootMap.get(hostKey);
+
+    if (root) {
+      return root;
+    }
+
+    var originRoot = field.originRoot;
+    var boundingRect = field.boundingRect; // For performance, if originRoot is not used by a view,
+    // assign it to a view, but not reproduce graphic elements.
+
+    if (!field.originRootHostKey) {
+      field.originRootHostKey = hostKey;
+      root = originRoot;
+    } else {
+      root = buildGraphic(mapRecord, boundingRect).root;
+    }
+
+    return rootMap.set(hostKey, root);
+  },
+  removeGraphic: function (mapName, mapRecord, hostKey) {
+    var field = inner(mapRecord);
+    var rootMap = field.rootMap;
+    rootMap && rootMap.removeKey(hostKey);
+
+    if (hostKey === field.originRootHostKey) {
+      field.originRootHostKey = null;
+    }
+  }
+};
+
+function buildGraphic(mapRecord, boundingRect) {
+  var svgXML = mapRecord.svgXML;
+  var result;
+  var root;
+
+  try {
+    result = svgXML && parseSVG(svgXML, {
+      ignoreViewBox: true,
+      ignoreRootClip: true
+    }) || {};
+    root = result.root;
+    assert(root != null);
+  } catch (e) {
+    throw new Error('Invalid svg format\n' + e.message);
+  }
+
+  var svgWidth = result.width;
+  var svgHeight = result.height;
+  var viewBoxRect = result.viewBoxRect;
+
+  if (!boundingRect) {
+    boundingRect = svgWidth == null || svgHeight == null ? // If svg width / height not specified, calculate
+    // bounding rect as the width / height
+    root.getBoundingRect() : new BoundingRect(0, 0, 0, 0);
+
+    if (svgWidth != null) {
+      boundingRect.width = svgWidth;
+    }
+
+    if (svgHeight != null) {
+      boundingRect.height = svgHeight;
+    }
+  }
+
+  if (viewBoxRect) {
+    var viewBoxTransform = makeViewBoxTransform(viewBoxRect, boundingRect.width, boundingRect.height);
+    var elRoot = root;
+    root = new Group();
+    root.add(elRoot);
+    elRoot.scale = viewBoxTransform.scale;
+    elRoot.position = viewBoxTransform.position;
+  }
+
+  root.setClipPath(new Rect({
+    shape: boundingRect.plain()
+  }));
+  return {
+    root: root,
+    boundingRect: boundingRect
+  };
+}
\ No newline at end of file
diff --git a/builder/src/echarts/coord/geo/geoSourceManager.js b/builder/src/echarts/coord/geo/geoSourceManager.js
new file mode 100644
index 0000000..0580207
--- /dev/null
+++ b/builder/src/echarts/coord/geo/geoSourceManager.js
@@ -0,0 +1,100 @@
+/*
+* 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 { __DEV__ } from '../../config';
+import { each, createHashMap } from 'zrender/src/core/util';
+import mapDataStorage from './mapDataStorage';
+import geoJSONLoader from './geoJSONLoader';
+import geoSVGLoader from './geoSVGLoader';
+import BoundingRect from 'zrender/src/core/BoundingRect';
+var loaders = {
+  geoJSON: geoJSONLoader,
+  svg: geoSVGLoader
+};
+export default {
+  /**
+   * @param {string} mapName
+   * @param {Object} nameMap
+   * @return {Object} source {regions, regionsMap, nameCoordMap, boundingRect}
+   */
+  load: function (mapName, nameMap) {
+    var regions = [];
+    var regionsMap = createHashMap();
+    var nameCoordMap = createHashMap();
+    var boundingRect;
+    var mapRecords = retrieveMap(mapName);
+    each(mapRecords, function (record) {
+      var singleSource = loaders[record.type].load(mapName, record);
+      each(singleSource.regions, function (region) {
+        var regionName = region.name; // Try use the alias in geoNameMap
+
+        if (nameMap && nameMap.hasOwnProperty(regionName)) {
+          region = region.cloneShallow(regionName = nameMap[regionName]);
+        }
+
+        regions.push(region);
+        regionsMap.set(regionName, region);
+        nameCoordMap.set(regionName, region.center);
+      });
+      var rect = singleSource.boundingRect;
+
+      if (rect) {
+        boundingRect ? boundingRect.union(rect) : boundingRect = rect.clone();
+      }
+    });
+    return {
+      regions: regions,
+      regionsMap: regionsMap,
+      nameCoordMap: nameCoordMap,
+      // FIXME Always return new ?
+      boundingRect: boundingRect || new BoundingRect(0, 0, 0, 0)
+    };
+  },
+
+  /**
+   * @param {string} mapName
+   * @param {string} hostKey For cache.
+   * @return {Array.<module:zrender/Element>} Roots.
+   */
+  makeGraphic: makeInvoker('makeGraphic'),
+
+  /**
+   * @param {string} mapName
+   * @param {string} hostKey For cache.
+   */
+  removeGraphic: makeInvoker('removeGraphic')
+};
+
+function makeInvoker(methodName) {
+  return function (mapName, hostKey) {
+    var mapRecords = retrieveMap(mapName);
+    var results = [];
+    each(mapRecords, function (record) {
+      var method = loaders[record.type][methodName];
+      method && results.push(method(mapName, record, hostKey));
+    });
+    return results;
+  };
+}
+
+function mapNotExistsError(mapName) {}
+
+function retrieveMap(mapName) {
+  var mapRecords = mapDataStorage.retrieveMap(mapName) || [];
+  return mapRecords;
+}
\ No newline at end of file
diff --git a/builder/src/echarts/coord/geo/mapDataStorage.js b/builder/src/echarts/coord/geo/mapDataStorage.js
new file mode 100644
index 0000000..e57c4e2
--- /dev/null
+++ b/builder/src/echarts/coord/geo/mapDataStorage.js
@@ -0,0 +1,79 @@
+/*
+* 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 { __DEV__ } from '../../config';
+import { createHashMap, isString, isArray, each, assert } from 'zrender/src/core/util';
+import { parseXML } from 'zrender/src/tool/parseSVG';
+var storage = createHashMap(); // For minimize the code size of common echarts package,
+// do not put too much logic in this module.
+
+export default {
+  // The format of record: see `echarts.registerMap`.
+  // Compatible with previous `echarts.registerMap`.
+  registerMap: function (mapName, rawGeoJson, rawSpecialAreas) {
+    var records;
+
+    if (isArray(rawGeoJson)) {
+      records = rawGeoJson;
+    } else if (rawGeoJson.svg) {
+      records = [{
+        type: 'svg',
+        source: rawGeoJson.svg,
+        specialAreas: rawGeoJson.specialAreas
+      }];
+    } else {
+      // Backward compatibility.
+      if (rawGeoJson.geoJson && !rawGeoJson.features) {
+        rawSpecialAreas = rawGeoJson.specialAreas;
+        rawGeoJson = rawGeoJson.geoJson;
+      }
+
+      records = [{
+        type: 'geoJSON',
+        source: rawGeoJson,
+        specialAreas: rawSpecialAreas
+      }];
+    }
+
+    each(records, function (record) {
+      var type = record.type;
+      type === 'geoJson' && (type = record.type = 'geoJSON');
+      var parse = parsers[type];
+      parse(record);
+    });
+    return storage.set(mapName, records);
+  },
+  retrieveMap: function (mapName) {
+    return storage.get(mapName);
+  }
+};
+var parsers = {
+  geoJSON: function (record) {
+    var source = record.source;
+    record.geoJSON = !isString(source) ? source : typeof JSON !== 'undefined' && JSON.parse ? JSON.parse(source) : new Function('return (' + source + ');')();
+  },
+  // Only perform parse to XML object here, which might be time
+  // consiming for large SVG.
+  // Although convert XML to zrender element is also time consiming,
+  // if we do it here, the clone of zrender elements has to be
+  // required. So we do it once for each geo instance, util real
+  // performance issues call for optimizing it.
+  svg: function (record) {
+    record.svgXML = parseXML(record.source);
+  }
+};
\ No newline at end of file
diff --git a/builder/src/echarts/coord/geo/prepareCustom.js b/builder/src/echarts/coord/geo/prepareCustom.js
index 6838ac3..ce6a397 100644
--- a/builder/src/echarts/coord/geo/prepareCustom.js
+++ b/builder/src/echarts/coord/geo/prepareCustom.js
@@ -40,7 +40,8 @@ export default function (coordSys) {
       x: rect.x,
       y: rect.y,
       width: rect.width,
-      height: rect.height
+      height: rect.height,
+      zoom: coordSys.getZoom()
     },
     api: {
       coord: function (data) {
diff --git a/builder/src/echarts/coord/polar/AngleAxis.js b/builder/src/echarts/coord/polar/AngleAxis.js
index 3ce7c6e..c74902d 100644
--- a/builder/src/echarts/coord/polar/AngleAxis.js
+++ b/builder/src/echarts/coord/polar/AngleAxis.js
@@ -17,7 +17,10 @@
 * under the License.
 */
 import * as zrUtil from 'zrender/src/core/util';
+import * as textContain from 'zrender/src/contain/text';
 import Axis from '../Axis';
+import { makeInner } from '../../util/model';
+var inner = makeInner();
 
 function AngleAxis(scale, angleExtent) {
   angleExtent = angleExtent || [0, 360];
@@ -44,7 +47,59 @@ AngleAxis.prototype = {
     return this.polar.pointToData(point, clamp)[this.dim === 'radius' ? 0 : 1];
   },
   dataToAngle: Axis.prototype.dataToCoord,
-  angleToData: Axis.prototype.coordToData
+  angleToData: Axis.prototype.coordToData,
+
+  /**
+   * Only be called in category axis.
+   * Angle axis uses text height to decide interval
+   *
+   * @override
+   * @return {number} Auto interval for cateogry axis tick and label
+   */
+  calculateCategoryInterval: function () {
+    var axis = this;
+    var labelModel = axis.getLabelModel();
+    var ordinalScale = axis.scale;
+    var ordinalExtent = ordinalScale.getExtent(); // Providing this method is for optimization:
+    // avoid generating a long array by `getTicks`
+    // in large category data case.
+
+    var tickCount = ordinalScale.count();
+
+    if (ordinalExtent[1] - ordinalExtent[0] < 1) {
+      return 0;
+    }
+
+    var tickValue = ordinalExtent[0];
+    var unitSpan = axis.dataToCoord(tickValue + 1) - axis.dataToCoord(tickValue);
+    var unitH = Math.abs(unitSpan); // Not precise, just use height as text width
+    // and each distance from axis line yet.
+
+    var rect = textContain.getBoundingRect(tickValue, labelModel.getFont(), 'center', 'top');
+    var maxH = Math.max(rect.height, 7);
+    var dh = maxH / unitH; // 0/0 is NaN, 1/0 is Infinity.
+
+    isNaN(dh) && (dh = Infinity);
+    var interval = Math.max(0, Math.floor(dh));
+    var cache = inner(axis.model);
+    var lastAutoInterval = cache.lastAutoInterval;
+    var lastTickCount = cache.lastTickCount; // Use cache to keep interval stable while moving zoom window,
+    // otherwise the calculated interval might jitter when the zoom
+    // window size is close to the interval-changing size.
+
+    if (lastAutoInterval != null && lastTickCount != null && Math.abs(lastAutoInterval - interval) <= 1 && Math.abs(lastTickCount - tickCount) <= 1 // Always choose the bigger one, otherwise the critical
+    // point is not the same when zooming in or zooming out.
+    && lastAutoInterval > interval) {
+      interval = lastAutoInterval;
+    } // Only update cache if cache not used, otherwise the
+    // changing of interval is too insensitive.
+    else {
+        cache.lastTickCount = tickCount;
+        cache.lastAutoInterval = interval;
+      }
+
+    return interval;
+  }
 };
 zrUtil.inherits(AngleAxis, Axis);
 export default AngleAxis;
\ No newline at end of file
diff --git a/builder/src/echarts/coord/radar/Radar.js b/builder/src/echarts/coord/radar/Radar.js
index cc03707..187a362 100644
--- a/builder/src/echarts/coord/radar/Radar.js
+++ b/builder/src/echarts/coord/radar/Radar.js
@@ -66,6 +66,12 @@ function Radar(radarModel, ecModel, api) {
    * @readOnly
    */
 
+  this.r0;
+  /**
+   * @type {number}
+   * @readOnly
+   */
+
   this.startAngle;
 }
 
@@ -120,10 +126,18 @@ Radar.prototype.resize = function (radarModel, api) {
   var viewSize = Math.min(viewWidth, viewHeight) / 2;
   this.cx = numberUtil.parsePercent(center[0], viewWidth);
   this.cy = numberUtil.parsePercent(center[1], viewHeight);
-  this.startAngle = radarModel.get('startAngle') * Math.PI / 180;
-  this.r = numberUtil.parsePercent(radarModel.get('radius'), viewSize);
+  this.startAngle = radarModel.get('startAngle') * Math.PI / 180; // radius may be single value like `20`, `'80%'`, or array like `[10, '80%']`
+
+  var radius = radarModel.get('radius');
+
+  if (typeof radius === 'string' || typeof radius === 'number') {
+    radius = [0, radius];
+  }
+
+  this.r0 = numberUtil.parsePercent(radius[0], viewSize);
+  this.r = numberUtil.parsePercent(radius[1], viewSize);
   zrUtil.each(this._indicatorAxes, function (indicatorAxis, idx) {
-    indicatorAxis.setExtent(0, this.r);
+    indicatorAxis.setExtent(this.r0, this.r);
     var angle = this.startAngle + idx * Math.PI * 2 / this._indicatorAxes.length; // Normalize to [-PI, PI]
 
     angle = Math.atan2(Math.sin(angle), Math.cos(angle));
diff --git a/builder/src/echarts/data/List.js b/builder/src/echarts/data/List.js
index e3f65df..cec7e3a 100644
--- a/builder/src/echarts/data/List.js
+++ b/builder/src/echarts/data/List.js
@@ -17,6 +17,8 @@
 * under the License.
 */
 
+/* global Float64Array, Int32Array, Uint32Array, Uint16Array */
+
 /**
  * List for data storage
  * @module echarts/data/List
diff --git a/builder/src/echarts/data/helper/completeDimensions.js b/builder/src/echarts/data/helper/completeDimensions.js
index 847dffa..cacb09e 100644
--- a/builder/src/echarts/data/helper/completeDimensions.js
+++ b/builder/src/echarts/data/helper/completeDimensions.js
@@ -112,7 +112,15 @@ function completeDimensions(sysDims, source, opt) {
 
 
   encodeDef.each(function (dataDims, coordDim) {
-    dataDims = normalizeToArray(dataDims).slice();
+    dataDims = normalizeToArray(dataDims).slice(); // Note: It is allowed that `dataDims.length` is `0`, e.g., options is
+    // `{encode: {x: -1, y: 1}}`. Should not filter anything in
+    // this case.
+
+    if (dataDims.length === 1 && dataDims[0] < 0) {
+      encodeDef.set(coordDim, false);
+      return;
+    }
+
     var validDataDims = encodeDef.set(coordDim, []);
     each(dataDims, function (resultDimIdx, idx) {
       // The input resultDimIdx can be dim name or index.
@@ -147,7 +155,13 @@ function completeDimensions(sysDims, source, opt) {
       sysDimItem.name = sysDimItem.coordDim = sysDimItem.coordDimIndex = sysDimItem.dimsDef = sysDimItem.otherDims = null;
     }
 
-    var dataDims = normalizeToArray(encodeDef.get(coordDim)); // dimensions provides default dim sequences.
+    var dataDims = encodeDef.get(coordDim); // negative resultDimIdx means no need to mapping.
+
+    if (dataDims === false) {
+      return;
+    }
+
+    var dataDims = normalizeToArray(dataDims); // dimensions provides default dim sequences.
 
     if (!dataDims.length) {
       for (var i = 0; i < (sysDimItemDimsDef && sysDimItemDimsDef.length || 1); i++) {
diff --git a/builder/src/echarts/data/helper/sourceHelper.js b/builder/src/echarts/data/helper/sourceHelper.js
index 20b3a85..6330bbe 100644
--- a/builder/src/echarts/data/helper/sourceHelper.js
+++ b/builder/src/echarts/data/helper/sourceHelper.js
@@ -17,10 +17,9 @@
 * under the License.
 */
 import { __DEV__ } from '../../config';
-import { makeInner } from '../../util/model';
+import { makeInner, getDataItemValue } from '../../util/model';
 import { getCoordSysDefineBySeries } from '../../model/referHelper';
 import { createHashMap, each, map, isArray, isString, isObject, isTypedArray, isArrayLike, extend, assert } from 'zrender/src/core/util';
-import { getDataItemValue } from '../../util/model';
 import Source from '../Source';
 import { SOURCE_FORMAT_ORIGINAL, SOURCE_FORMAT_ARRAY_ROWS, SOURCE_FORMAT_OBJECT_ROWS, SOURCE_FORMAT_KEYED_COLUMNS, SOURCE_FORMAT_UNKNOWN, SOURCE_FORMAT_TYPED_ARRAY, SERIES_LAYOUT_BY_ROW } from './sourceType';
 var inner = makeInner();
@@ -38,6 +37,10 @@ export function detectSourceFormat(datasetModel) {
     sourceFormat = SOURCE_FORMAT_TYPED_ARRAY;
   } else if (isArray(data)) {
     // FIXME Whether tolerate null in top level array?
+    if (data.length === 0) {
+      sourceFormat = SOURCE_FORMAT_ARRAY_ROWS;
+    }
+
     for (var i = 0, len = data.length; i < len; i++) {
       var item = data[i];
 
diff --git a/builder/src/echarts/echarts.js b/builder/src/echarts/echarts.js
index 537e18f..2d5be66 100644
--- a/builder/src/echarts/echarts.js
+++ b/builder/src/echarts/echarts.js
@@ -43,14 +43,15 @@ import Scheduler from './stream/Scheduler';
 import lightTheme from './theme/light';
 import darkTheme from './theme/dark';
 import './component/dataset';
+import mapDataStorage from './coord/geo/mapDataStorage';
 var assert = zrUtil.assert;
 var each = zrUtil.each;
 var isFunction = zrUtil.isFunction;
 var isObject = zrUtil.isObject;
 var parseClassType = ComponentModel.parseClassType;
-export var version = '4.1.0';
+export var version = '4.2.0';
 export var dependencies = {
-  zrender: '4.0.4'
+  zrender: '4.0.5'
 };
 var TEST_FRAME_REMAIN_TIME = 1;
 var PRIORITY_PROCESSOR_FILTER = 1000;
@@ -207,7 +208,7 @@ function ECharts(dom, theme, opts) {
    */
 
   this._scheduler = new Scheduler(this, api, dataProcessorFuncs, visualFuncs);
-  Eventful.call(this);
+  Eventful.call(this, this._ecEventProcessor = new EventProcessor());
   /**
    * @type {module:echarts~MessageCenter}
    * @private
@@ -355,7 +356,7 @@ echartsProto.setOption = function (option, notMerge, lazyUpdate) {
 
 
 echartsProto.setTheme = function () {
-  console.log('ECharts#setTheme() is DEPRECATED in ECharts 3.0');
+  console.error('ECharts#setTheme() is DEPRECATED in ECharts 3.0');
 };
 /**
  * @return {module:echarts/model/Global}
@@ -1346,21 +1347,49 @@ echartsProto._initEvents = function () {
     this._zr.on(eveName, function (e) {
       var ecModel = this.getModel();
       var el = e.target;
-      var params; // no e.target when 'globalout'.
+      var params;
+      var isGlobalOut = eveName === 'globalout'; // no e.target when 'globalout'.
 
-      if (eveName === 'globalout') {
+      if (isGlobalOut) {
         params = {};
       } else if (el && el.dataIndex != null) {
         var dataModel = el.dataModel || ecModel.getSeriesByIndex(el.seriesIndex);
-        params = dataModel && dataModel.getDataParams(el.dataIndex, el.dataType) || {};
+        params = dataModel && dataModel.getDataParams(el.dataIndex, el.dataType, el) || {};
       } // If element has custom eventData of components
       else if (el && el.eventData) {
           params = zrUtil.extend({}, el.eventData);
-        }
+        } // Contract: if params prepared in mouse event,
+      // these properties must be specified:
+      // {
+      //    componentType: string (component main type)
+      //    componentIndex: number
+      // }
+      // Otherwise event query can not work.
+
 
       if (params) {
+        var componentType = params.componentType;
+        var componentIndex = params.componentIndex; // Special handling for historic reason: when trigger by
+        // markLine/markPoint/markArea, the componentType is
+        // 'markLine'/'markPoint'/'markArea', but we should better
+        // enable them to be queried by seriesIndex, since their
+        // option is set in each series.
+
+        if (componentType === 'markLine' || componentType === 'markPoint' || componentType === 'markArea') {
+          componentType = 'series';
+          componentIndex = params.seriesIndex;
+        }
+
+        var model = componentType && componentIndex != null && ecModel.getComponent(componentType, componentIndex);
+        var view = model && this[model.mainType === 'series' ? '_chartsMap' : '_componentsMap'][model.__viewId];
         params.event = e;
         params.type = eveName;
+        this._ecEventProcessor.eventInfo = {
+          targetEl: el,
+          packedEvent: params,
+          model: model,
+          view: view
+        };
         this.trigger(eveName, params);
       }
     }, this);
@@ -1497,11 +1526,122 @@ function createExtensionAPI(ecInstance) {
   });
 }
 /**
+ * @class
+ * Usage of query:
+ * `chart.on('click', query, handler);`
+ * The `query` can be:
+ * + The component type query string, only `mainType` or `mainType.subType`,
+ *   like: 'xAxis', 'series', 'xAxis.category' or 'series.line'.
+ * + The component query object, like:
+ *   `{seriesIndex: 2}`, `{seriesName: 'xx'}`, `{seriesId: 'some'}`,
+ *   `{xAxisIndex: 2}`, `{xAxisName: 'xx'}`, `{xAxisId: 'some'}`.
+ * + The data query object, like:
+ *   `{dataIndex: 123}`, `{dataType: 'link'}`, `{name: 'some'}`.
+ * + The other query object (cmponent customized query), like:
+ *   `{element: 'some'}` (only available in custom series).
+ *
+ * Caveat: If a prop in the `query` object is `null/undefined`, it is the
+ * same as there is no such prop in the `query` object.
+ */
+
+
+function EventProcessor() {
+  // These info required: targetEl, packedEvent, model, view
+  this.eventInfo;
+}
+
+EventProcessor.prototype = {
+  constructor: EventProcessor,
+  normalizeQuery: function (query) {
+    var cptQuery = {};
+    var dataQuery = {};
+    var otherQuery = {}; // `query` is `mainType` or `mainType.subType` of component.
+
+    if (zrUtil.isString(query)) {
+      var condCptType = parseClassType(query); // `.main` and `.sub` may be ''.
+
+      cptQuery.mainType = condCptType.main || null;
+      cptQuery.subType = condCptType.sub || null;
+    } // `query` is an object, convert to {mainType, index, name, id}.
+    else {
+        // `xxxIndex`, `xxxName`, `xxxId`, `name`, `dataIndex`, `dataType` is reserved,
+        // can not be used in `compomentModel.filterForExposedEvent`.
+        var suffixes = ['Index', 'Name', 'Id'];
+        var dataKeys = {
+          name: 1,
+          dataIndex: 1,
+          dataType: 1
+        };
+        zrUtil.each(query, function (val, key) {
+          var reserved = false;
+
+          for (var i = 0; i < suffixes.length; i++) {
+            var propSuffix = suffixes[i];
+            var suffixPos = key.lastIndexOf(propSuffix);
+
+            if (suffixPos > 0 && suffixPos === key.length - propSuffix.length) {
+              var mainType = key.slice(0, suffixPos); // Consider `dataIndex`.
+
+              if (mainType !== 'data') {
+                cptQuery.mainType = mainType;
+                cptQuery[propSuffix.toLowerCase()] = val;
+                reserved = true;
+              }
+            }
+          }
+
+          if (dataKeys.hasOwnProperty(key)) {
+            dataQuery[key] = val;
+            reserved = true;
+          }
+
+          if (!reserved) {
+            otherQuery[key] = val;
+          }
+        });
+      }
+
+    return {
+      cptQuery: cptQuery,
+      dataQuery: dataQuery,
+      otherQuery: otherQuery
+    };
+  },
+  filter: function (eventType, query, args) {
+    // They should be assigned before each trigger call.
+    var eventInfo = this.eventInfo;
+
+    if (!eventInfo) {
+      return true;
+    }
+
+    var targetEl = eventInfo.targetEl;
+    var packedEvent = eventInfo.packedEvent;
+    var model = eventInfo.model;
+    var view = eventInfo.view; // For event like 'globalout'.
+
+    if (!model || !view) {
+      return true;
+    }
+
+    var cptQuery = query.cptQuery;
+    var dataQuery = query.dataQuery;
+    return check(cptQuery, model, 'mainType') && check(cptQuery, model, 'subType') && check(cptQuery, model, 'index', 'componentIndex') && check(cptQuery, model, 'name') && check(cptQuery, model, 'id') && check(dataQuery, packedEvent, 'name') && check(dataQuery, packedEvent, 'dataIndex') && check(dataQuery, packedEvent, 'dataType') && (!view.filterForExposedEvent || view.filterForExposedEvent(eventType, query.otherQuery, targetEl, packedEvent));
+
+    function check(query, host, prop, propOnHost) {
+      return query[prop] == null || host[propOnHost || prop] === query[prop];
+    }
+  },
+  afterTrigger: function () {
+    // Make sure the eventInfo wont be used in next trigger.
+    this.eventInfo = null;
+  }
+};
+/**
  * @type {Object} key: actionType.
  * @inner
  */
 
-
 var actions = {};
 /**
  * Map eventType to actionType
@@ -1550,7 +1690,6 @@ var connectedGroups = {};
 var idBase = new Date() - 0;
 var groupIdBase = new Date() - 0;
 var DOM_ATTRIBUTE_KEY = '_echarts_instance_';
-var mapDataStores = {};
 
 function enableConnect(chart) {
   var STATUS_PENDING = 0;
@@ -1908,10 +2047,10 @@ export function setCanvasCreator(creator) {
 }
 /**
  * @param {string} mapName
- * @param {Object|string} geoJson
+ * @param {Array.<Object>|Object|string} geoJson
  * @param {Object} [specialAreas]
  *
- * @example
+ * @example GeoJSON
  *     $.get('USA.json', function (geoJson) {
  *         echarts.registerMap('USA', geoJson);
  *         // Or
@@ -1920,22 +2059,21 @@ export function setCanvasCreator(creator) {
  *             specialAreas: {}
  *         })
  *     });
+ *
+ *     $.get('airport.svg', function (svg) {
+ *         echarts.registerMap('airport', {
+ *             svg: svg
+ *         }
+ *     });
+ *
+ *     echarts.registerMap('eu', [
+ *         {svg: eu-topographic.svg},
+ *         {geoJSON: eu.json}
+ *     ])
  */
 
 export function registerMap(mapName, geoJson, specialAreas) {
-  if (geoJson.geoJson && !geoJson.features) {
-    specialAreas = geoJson.specialAreas;
-    geoJson = geoJson.geoJson;
-  }
-
-  if (typeof geoJson === 'string') {
-    geoJson = typeof JSON !== 'undefined' && JSON.parse ? JSON.parse(geoJson) : new Function('return (' + geoJson + ');')();
-  }
-
-  mapDataStores[mapName] = {
-    geoJson: geoJson,
-    specialAreas: specialAreas
-  };
+  mapDataStorage.registerMap(mapName, geoJson, specialAreas);
 }
 /**
  * @param {string} mapName
@@ -1943,7 +2081,12 @@ export function registerMap(mapName, geoJson, specialAreas) {
  */
 
 export function getMap(mapName) {
-  return mapDataStores[mapName];
+  // For backward compatibility, only return the first one.
+  var records = mapDataStorage.retrieveMap(mapName);
+  return records && records[0] && {
+    geoJson: records[0].geoJSON,
+    specialAreas: records[0].specialAreas
+  };
 }
 registerVisual(PRIORITY_VISUAL_GLOBAL, seriesColor);
 registerPreprocessor(backwardCompat);
diff --git a/builder/src/echarts/export.js b/builder/src/echarts/export.js
index b880ec1..a35a18f 100644
--- a/builder/src/echarts/export.js
+++ b/builder/src/echarts/export.js
@@ -25,7 +25,7 @@ import * as matrix from 'zrender/src/core/matrix';
 import * as vector from 'zrender/src/core/vector';
 import * as zrUtil from 'zrender/src/core/util';
 import * as colorTool from 'zrender/src/tool/color';
-import * as graphic from './util/graphic';
+import * as graphicUtil from './util/graphic';
 import * as numberUtil from './util/number';
 import * as formatUtil from './util/format';
 import { throttle } from './util/throttle';
@@ -35,7 +35,6 @@ export { zrender };
 export { default as List } from './data/List';
 export { default as Model } from './model/Model';
 export { default as Axis } from './coord/Axis';
-export { graphic };
 export { numberUtil as number };
 export { formatUtil as format };
 export { throttle };
@@ -50,4 +49,9 @@ var ecUtil = {};
 zrUtil.each(['map', 'each', 'filter', 'indexOf', 'inherits', 'reduce', 'filter', 'bind', 'curry', 'isArray', 'isString', 'isObject', 'isFunction', 'extend', 'defaults', 'clone', 'merge'], function (name) {
   ecUtil[name] = zrUtil[name];
 });
-export { ecUtil as util };
\ No newline at end of file
+export { ecUtil as util };
+var graphic = {};
+zrUtil.each(['extendShape', 'extendPath', 'makePath', 'makeImage', 'mergePath', 'resizePath', 'createIcon', 'setHoverStyle', 'setLabelStyle', 'setTextStyle', 'setText', 'getFont', 'updateProps', 'initProps', 'getTransform', 'clipPointsByRect', 'clipRectByRect', 'Group', 'Image', 'Text', 'Circle', 'Sector', 'Ring', 'Polygon', 'Polyline', 'Rect', 'Line', 'BezierCurve', 'Arc', 'IncrementalDisplayable', 'CompoundPath', 'LinearGradient', 'RadialGradient', 'BoundingRect'], function (name) {
+  graphic[name] = graphicUtil[name];
+});
+export { graphic };
\ No newline at end of file
diff --git a/builder/src/echarts/layout/barGrid.js b/builder/src/echarts/layout/barGrid.js
index ea2caff..067429b 100644
--- a/builder/src/echarts/layout/barGrid.js
+++ b/builder/src/echarts/layout/barGrid.js
@@ -16,6 +16,8 @@
 * specific language governing permissions and limitations
 * under the License.
 */
+
+/* global Float32Array */
 import * as zrUtil from 'zrender/src/core/util';
 import { parsePercent } from '../util/number';
 import { isDimensionStacked } from '../data/helper/dataStackHelper';
@@ -392,8 +394,24 @@ function isOnCartesian(seriesModel) {
 
 function isInLargeMode(seriesModel) {
   return seriesModel.pipelineContext && seriesModel.pipelineContext.large;
-}
+} // See cases in `test/bar-start.html` and `#7412`, `#8747`.
+
 
 function getValueAxisStart(baseAxis, valueAxis, stacked) {
-  return zrUtil.indexOf(baseAxis.getAxesOnZeroOf(), valueAxis) >= 0 || stacked ? valueAxis.toGlobalCoord(valueAxis.dataToCoord(0)) : valueAxis.getGlobalExtent()[0];
+  var extent = valueAxis.getGlobalExtent();
+  var min;
+  var max;
+
+  if (extent[0] > extent[1]) {
+    min = extent[1];
+    max = extent[0];
+  } else {
+    min = extent[0];
+    max = extent[1];
+  }
+
+  var valueStart = valueAxis.toGlobalCoord(valueAxis.dataToCoord(0));
+  valueStart < min && (valueStart = min);
+  valueStart > max && (valueStart = max);
+  return valueStart;
 }
\ No newline at end of file
diff --git a/builder/src/echarts/layout/barPolar.js b/builder/src/echarts/layout/barPolar.js
index 0624326..e41bf16 100644
--- a/builder/src/echarts/layout/barPolar.js
+++ b/builder/src/echarts/layout/barPolar.js
@@ -35,8 +35,8 @@ function getAxisKey(axis) {
 
 
 function barLayoutPolar(seriesType, ecModel, api) {
-  var width = api.getWidth();
-  var height = api.getHeight();
+  // var width = api.getWidth();
+  // var height = api.getHeight();
   var lastStackCoords = {};
   var barWidthAndOffset = calRadialBar(zrUtil.filter(ecModel.getSeriesByType(seriesType), function (seriesModel) {
     return !ecModel.isSeriesFiltered(seriesModel) && seriesModel.coordinateSystem && seriesModel.coordinateSystem.type === 'polar';
@@ -55,9 +55,8 @@ function barLayoutPolar(seriesType, ecModel, api) {
     var columnOffset = columnLayoutInfo.offset;
     var columnWidth = columnLayoutInfo.width;
     var valueAxis = polar.getOtherAxis(baseAxis);
-    var center = seriesModel.get('center') || ['50%', '50%'];
-    var cx = parsePercent(center[0], width);
-    var cy = parsePercent(center[1], height);
+    var cx = seriesModel.coordinateSystem.cx;
+    var cy = seriesModel.coordinateSystem.cy;
     var barMinHeight = seriesModel.get('barMinHeight') || 0;
     var barMinAngle = seriesModel.get('barMinAngle') || 0;
     lastStackCoords[stackId] = lastStackCoords[stackId] || [];
diff --git a/builder/src/echarts/layout/points.js b/builder/src/echarts/layout/points.js
index 6e1c424..2eac462 100644
--- a/builder/src/echarts/layout/points.js
+++ b/builder/src/echarts/layout/points.js
@@ -16,6 +16,8 @@
 * specific language governing permissions and limitations
 * under the License.
 */
+
+/* global Float32Array */
 import { map } from 'zrender/src/core/util';
 import createRenderPlanner from '../chart/helper/createRenderPlanner';
 import { isDimensionStacked } from '../data/helper/dataStackHelper';
diff --git a/builder/src/echarts/model/Series.js b/builder/src/echarts/model/Series.js
index a18a45c..4944aff 100644
--- a/builder/src/echarts/model/Series.js
+++ b/builder/src/echarts/model/Series.js
@@ -268,8 +268,21 @@ var SeriesModel = ComponentModel.extend({
    * @param {number} dataIndex
    * @param {boolean} [multipleSeries=false]
    * @param {number} [dataType]
+   * @param {string} [renderMode='html'] valid values: 'html' and 'richText'.
+   *                                     'html' is used for rendering tooltip in extra DOM form, and the result
+   *                                     string is used as DOM HTML content.
+   *                                     'richText' is used for rendering tooltip in rich text form, for those where
+   *                                     DOM operation is not supported.
+   * @return {Object} formatted tooltip with `html` and `markers`
    */
-  formatTooltip: function (dataIndex, multipleSeries, dataType) {
+  formatTooltip: function (dataIndex, multipleSeries, dataType, renderMode) {
+    var series = this;
+    renderMode = renderMode || 'html';
+    var newLine = renderMode === 'html' ? '<br/>' : '\n';
+    var isRichText = renderMode === 'richText';
+    var markers = {};
+    var markerId = 0;
+
     function formatArrayValue(value) {
       // ??? TODO refactor these logic.
       // check: category-no-encode-has-axis-data in dataset.html
@@ -291,20 +304,40 @@ var SeriesModel = ComponentModel.extend({
         }
 
         var dimType = dimInfo.type;
+        var markName = 'sub' + series.seriesIndex + 'at' + markerId;
         var dimHead = getTooltipMarker({
           color: color,
-          type: 'subItem'
+          type: 'subItem',
+          renderMode: renderMode,
+          markerId: markName
         });
-        var valStr = (vertially ? dimHead + encodeHTML(dimInfo.displayName || '-') + ': ' : '') + // FIXME should not format time for raw data?
+        var dimHeadStr = typeof dimHead === 'string' ? dimHead : dimHead.content;
+        var valStr = (vertially ? dimHeadStr + encodeHTML(dimInfo.displayName || '-') + ': ' : '') + // FIXME should not format time for raw data?
         encodeHTML(dimType === 'ordinal' ? val + '' : dimType === 'time' ? multipleSeries ? '' : formatTime('yyyy/MM/dd hh:mm:ss', val) : addCommas(val));
         valStr && result.push(valStr);
+
+        if (isRichText) {
+          markers[markName] = color;
+          ++markerId;
+        }
       }
 
-      return (vertially ? '<br/>' : '') + result.join(vertially ? '<br/>' : ', ');
+      var newLine = vertially ? isRichText ? '\n' : '<br/>' : '';
+      var content = newLine + result.join(newLine || ', ');
+      return {
+        renderMode: renderMode,
+        content: content,
+        style: markers
+      };
     }
 
     function formatSingleValue(val) {
-      return encodeHTML(addCommas(val));
+      // return encodeHTML(addCommas(val));
+      return {
+        renderMode: renderMode,
+        content: encodeHTML(addCommas(val)),
+        style: markers
+      };
     }
 
     var data = this.getData();
@@ -321,7 +354,16 @@ var SeriesModel = ComponentModel.extend({
     color = color || 'transparent'; // Complicated rule for pretty tooltip.
 
     var formattedValue = tooltipDimLen > 1 || isValueArr && !tooltipDimLen ? formatArrayValue(value) : tooltipDimLen ? formatSingleValue(retrieveRawValue(data, dataIndex, tooltipDims[0])) : formatSingleValue(isValueArr ? value[0] : value);
-    var colorEl = getTooltipMarker(color);
+    var content = formattedValue.content;
+    var markName = series.seriesIndex + 'at' + markerId;
+    var colorEl = getTooltipMarker({
+      color: color,
+      type: 'item',
+      renderMode: renderMode,
+      markerId: markName
+    });
+    markers[markName] = color;
+    ++markerId;
     var name = data.getName(dataIndex);
     var seriesName = this.name;
 
@@ -329,8 +371,13 @@ var SeriesModel = ComponentModel.extend({
       seriesName = '';
     }
 
-    seriesName = seriesName ? encodeHTML(seriesName) + (!multipleSeries ? '<br/>' : ': ') : '';
-    return !multipleSeries ? seriesName + colorEl + (name ? encodeHTML(name) + ': ' + formattedValue : formattedValue) : colorEl + seriesName + formattedValue;
+    seriesName = seriesName ? encodeHTML(seriesName) + (!multipleSeries ? newLine : ': ') : '';
+    var colorStr = typeof colorEl === 'string' ? colorEl : colorEl.content;
+    var html = !multipleSeries ? seriesName + colorStr + (name ? encodeHTML(name) + ': ' + content : content) : colorStr + seriesName + content;
+    return {
+      html: html,
+      markers: markers
+    };
   },
 
   /**
diff --git a/builder/src/echarts/model/mixin/dataFormat.js b/builder/src/echarts/model/mixin/dataFormat.js
index af905c5..a00573b 100644
--- a/builder/src/echarts/model/mixin/dataFormat.js
+++ b/builder/src/echarts/model/mixin/dataFormat.js
@@ -18,6 +18,7 @@
 */
 import { retrieveRawValue } from '../../data/helper/dataProvider';
 import { getTooltipMarker, formatTpl } from '../../util/format';
+import { getTooltipRenderMode } from '../../util/model';
 var DIMENSION_LABEL_REG = /\{@(.+?)\}/g; // PENDING A little ugly
 
 export default {
@@ -34,20 +35,29 @@ export default {
     var name = data.getName(dataIndex);
     var itemOpt = data.getRawDataItem(dataIndex);
     var color = data.getItemVisual(dataIndex, 'color');
+    var tooltipModel = this.ecModel.getComponent('tooltip');
+    var renderModeOption = tooltipModel && tooltipModel.get('renderMode');
+    var renderMode = getTooltipRenderMode(renderModeOption);
+    var mainType = this.mainType;
+    var isSeries = mainType === 'series';
     return {
-      componentType: this.mainType,
+      componentType: mainType,
       componentSubType: this.subType,
-      seriesType: this.mainType === 'series' ? this.subType : null,
+      componentIndex: this.componentIndex,
+      seriesType: isSeries ? this.subType : null,
       seriesIndex: this.seriesIndex,
-      seriesId: this.id,
-      seriesName: this.name,
+      seriesId: isSeries ? this.id : null,
+      seriesName: isSeries ? this.name : null,
       name: name,
       dataIndex: rawDataIndex,
       data: itemOpt,
       dataType: dataType,
       value: rawValue,
       color: color,
-      marker: getTooltipMarker(color),
+      marker: getTooltipMarker({
+        color: color,
+        renderMode: renderMode
+      }),
       // Param name list for mapping `a`, `b`, `c`, `d`, `e`
       $vars: ['seriesName', 'name', 'value']
     };
diff --git a/builder/src/echarts/scale/Time.js b/builder/src/echarts/scale/Time.js
index b9e5a2a..e537ef0 100644
--- a/builder/src/echarts/scale/Time.js
+++ b/builder/src/echarts/scale/Time.js
@@ -184,8 +184,8 @@ var scaleLevels = [// Format              interval
 ['month', ONE_DAY * 31], // 1M
 ['week', ONE_DAY * 42], // 6w
 ['month', ONE_DAY * 62], // 2M
-['week', ONE_DAY * 42], // 10w
-['quarter', ONE_DAY * 380 / 4], // 3M
+['week', ONE_DAY * 70], // 10w
+['quarter', ONE_DAY * 95], // 3M
 ['month', ONE_DAY * 31 * 4], // 4M
 ['month', ONE_DAY * 31 * 5], // 5M
 ['half-year', ONE_DAY * 380 / 2], // 6M
diff --git a/builder/src/echarts/stream/Scheduler.js b/builder/src/echarts/stream/Scheduler.js
index fe0548d..ba888f9 100644
--- a/builder/src/echarts/stream/Scheduler.js
+++ b/builder/src/echarts/stream/Scheduler.js
@@ -20,7 +20,7 @@
 /**
  * @module echarts/stream/Scheduler
  */
-import { each, map, isArray, isFunction, createHashMap, noop } from 'zrender/src/core/util';
+import { each, map, isFunction, createHashMap, noop } from 'zrender/src/core/util';
 import { createTask } from './task';
 import { getUID } from '../util/component';
 import GlobalModel from '../model/Global';
@@ -517,10 +517,13 @@ ecModelMock.eachComponent = function (cond) {
 };
 
 function mockMethods(target, Clz) {
+  /* eslint-disable */
   for (var name in Clz.prototype) {
     // Do not use hasOwnProperty
     target[name] = noop;
   }
+  /* eslint-enable */
+
 }
 
 export default Scheduler;
\ No newline at end of file
diff --git a/builder/src/echarts/util/format.js b/builder/src/echarts/util/format.js
index 4e23845..79c600b 100644
--- a/builder/src/echarts/util/format.js
+++ b/builder/src/echarts/util/format.js
@@ -18,7 +18,8 @@
 */
 import * as zrUtil from 'zrender/src/core/util';
 import * as textContain from 'zrender/src/contain/text';
-import * as numberUtil from './number';
+import * as numberUtil from './number'; // import Text from 'zrender/src/graphic/Text';
+
 /**
  * 每三位默认加,格式化
  * @param {string|number} x
@@ -125,6 +126,8 @@ export function formatTplSimple(tpl, param, encode) {
  * @param {string} [opt.color]
  * @param {string} [opt.extraCssText]
  * @param {string} [opt.type='item'] 'item' or 'subItem'
+ * @param {string} [opt.renderMode='html'] render mode of tooltip, 'html' or 'richText'
+ * @param {string} [opt.markerId='X'] id name for marker. If only one marker is in a rich text, this can be omitted.
  * @return {string}
  */
 
@@ -136,12 +139,25 @@ export function getTooltipMarker(opt, extraCssText) {
   var color = opt.color;
   var type = opt.type;
   var extraCssText = opt.extraCssText;
+  var renderMode = opt.renderMode || 'html';
+  var markerId = opt.markerId || 'X';
 
   if (!color) {
     return '';
   }
 
-  return type === 'subItem' ? '<span style="display:inline-block;vertical-align:middle;margin-right:8px;margin-left:3px;' + 'border-radius:4px;width:4px;height:4px;background-color:' + encodeHTML(color) + ';' + (extraCssText || '') + '"></span>' : '<span style="display:inline-block;margin-right:5px;' + 'border-radius:10px;width:10px;height:10px;background-color:' + encodeHTML(color) + ';' + (extraCssText || '') + '"></span>';
+  if (renderMode === 'html') {
+    return type === 'subItem' ? '<span style="display:inline-block;vertical-align:middle;margin-right:8px;margin-left:3px;' + 'border-radius:4px;width:4px;height:4px;background-color:' + encodeHTML(color) + ';' + (extraCssText || '') + '"></span>' : '<span style="display:inline-block;margin-right:5px;' + 'border-radius:10px;width:10px;height:10px;background-color:' + encodeHTML(color) + ';' + (extraCssText || '') + '"></span>';
+  } else {
+    // Space for rich element marker
+    return {
+      renderMode: renderMode,
+      content: '{marker' + markerId + '|}  ',
+      style: {
+        color: color
+      }
+    };
+  }
 }
 
 function pad(str, len) {
diff --git a/builder/src/echarts/util/graphic.js b/builder/src/echarts/util/graphic.js
index 2095182..ea4f8f8 100644
--- a/builder/src/echarts/util/graphic.js
+++ b/builder/src/echarts/util/graphic.js
@@ -68,11 +68,10 @@ export function extendPath(pathData, opts) {
 
 export function makePath(pathData, opts, rect, layout) {
   var path = pathTool.createFromString(pathData, opts);
-  var boundingRect = path.getBoundingRect();
 
   if (rect) {
     if (layout === 'center') {
-      rect = centerGraphic(rect, boundingRect);
+      rect = centerGraphic(rect, path.getBoundingRect());
     }
 
     resizePath(path, rect);
@@ -228,156 +227,198 @@ export function subPixelOptimize(position, lineWidth, positiveOrNegative) {
 }
 
 function hasFillOrStroke(fillOrStroke) {
-  return fillOrStroke != null && fillOrStroke != 'none';
-}
+  return fillOrStroke != null && fillOrStroke !== 'none';
+} // Most lifted color are duplicated.
+
+
+var liftedColorMap = zrUtil.createHashMap();
+var liftedColorCount = 0;
 
 function liftColor(color) {
-  return typeof color === 'string' ? colorTool.lift(color, -0.1) : color;
-}
-/**
- * @private
- */
+  if (typeof color !== 'string') {
+    return color;
+  }
 
+  var liftedColor = liftedColorMap.get(color);
 
-function cacheElementStl(el) {
-  if (el.__hoverStlDirty) {
-    var stroke = el.style.stroke;
-    var fill = el.style.fill; // Create hoverStyle on mouseover
-
-    var hoverStyle = el.__hoverStl;
-    hoverStyle.fill = hoverStyle.fill || (hasFillOrStroke(fill) ? liftColor(fill) : null);
-    hoverStyle.stroke = hoverStyle.stroke || (hasFillOrStroke(stroke) ? liftColor(stroke) : null);
-    var normalStyle = {};
-
-    for (var name in hoverStyle) {
-      // See comment in `doSingleEnterHover`.
-      if (hoverStyle[name] != null) {
-        normalStyle[name] = el.style[name];
-      }
-    }
+  if (!liftedColor) {
+    liftedColor = colorTool.lift(color, -0.1);
 
-    el.__normalStl = normalStyle;
-    el.__hoverStlDirty = false;
+    if (liftedColorCount < 10000) {
+      liftedColorMap.set(color, liftedColor);
+      liftedColorCount++;
+    }
   }
+
+  return liftedColor;
 }
-/**
- * @private
- */
 
+function cacheElementStl(el) {
+  if (!el.__hoverStlDirty) {
+    return;
+  }
+
+  el.__hoverStlDirty = false;
+  var hoverStyle = el.__hoverStl;
 
-function doSingleEnterHover(el) {
-  if (el.__isHover) {
+  if (!hoverStyle) {
+    el.__normalStl = null;
     return;
   }
 
-  cacheElementStl(el);
+  var normalStyle = el.__normalStl = {};
+  var elStyle = el.style;
 
-  if (el.useHoverLayer) {
-    el.__zr && el.__zr.addHover(el, el.__hoverStl);
-  } else {
-    var style = el.style;
-    var insideRollbackOpt = style.insideRollbackOpt; // Consider case: only `position: 'top'` is set on emphasis, then text
-    // color should be returned to `autoColor`, rather than remain '#fff'.
-    // So we should rollback then apply again after style merging.
-
-    insideRollbackOpt && rollbackInsideStyle(style); // styles can be:
-    // {
-    //    label: {
-    //        show: false,
-    //        position: 'outside',
-    //        fontSize: 18
-    //    },
-    //    emphasis: {
-    //        label: {
-    //            show: true
-    //        }
-    //    }
-    // },
-    // where properties of `emphasis` may not appear in `normal`. We previously use
-    // module:echarts/util/model#defaultEmphasis to merge `normal` to `emphasis`.
-    // But consider rich text and setOption in merge mode, it is impossible to cover
-    // all properties in merge. So we use merge mode when setting style here, where
-    // only properties that is not `null/undefined` can be set. The disadventage:
-    // null/undefined can not be used to remove style any more in `emphasis`.
-
-    style.extendFrom(el.__hoverStl); // Do not save `insideRollback`.
-
-    if (insideRollbackOpt) {
-      applyInsideStyle(style, style.insideOriginalTextPosition, insideRollbackOpt); // textFill may be rollbacked to null.
-
-      if (style.textFill == null) {
-        style.textFill = insideRollbackOpt.autoColor;
-      }
+  for (var name in hoverStyle) {
+    // See comment in `doSingleEnterHover`.
+    if (hoverStyle[name] != null) {
+      normalStyle[name] = elStyle[name];
     }
+  } // Always cache fill and stroke to normalStyle for lifting color.
 
-    el.dirty(false);
-    el.z2 += 1;
-  }
 
-  el.__isHover = true;
+  normalStyle.fill = elStyle.fill;
+  normalStyle.stroke = elStyle.stroke;
 }
-/**
- * @inner
- */
 
+function doSingleEnterHover(el) {
+  var hoverStl = el.__hoverStl;
 
-function doSingleLeaveHover(el) {
-  if (!el.__isHover) {
+  if (!hoverStl || el.__highlighted) {
     return;
   }
 
-  var normalStl = el.__normalStl;
+  var useHoverLayer = el.useHoverLayer;
+  el.__highlighted = useHoverLayer ? 'layer' : 'plain';
+  var zr = el.__zr;
 
-  if (el.useHoverLayer) {
-    el.__zr && el.__zr.removeHover(el);
-  } else {
-    // Consider null/undefined value, should use
-    // `setStyle` but not `extendFrom(stl, true)`.
-    normalStl && el.setStyle(normalStl);
-    el.z2 -= 1;
+  if (!zr && useHoverLayer) {
+    return;
   }
 
-  el.__isHover = false;
+  var elTarget = el;
+  var targetStyle = el.style;
+
+  if (useHoverLayer) {
+    elTarget = zr.addHover(el);
+    targetStyle = elTarget.style;
+  } // Consider case: only `position: 'top'` is set on emphasis, then text
+  // color should be returned to `autoColor`, rather than remain '#fff'.
+  // So we should rollback then apply again after style merging.
+
+
+  rollbackDefaultTextStyle(targetStyle);
+
+  if (!useHoverLayer) {
+    cacheElementStl(elTarget);
+  } // styles can be:
+  // {
+  //    label: {
+  //        show: false,
+  //        position: 'outside',
+  //        fontSize: 18
+  //    },
+  //    emphasis: {
+  //        label: {
+  //            show: true
+  //        }
+  //    }
+  // },
+  // where properties of `emphasis` may not appear in `normal`. We previously use
+  // module:echarts/util/model#defaultEmphasis to merge `normal` to `emphasis`.
+  // But consider rich text and setOption in merge mode, it is impossible to cover
+  // all properties in merge. So we use merge mode when setting style here, where
+  // only properties that is not `null/undefined` can be set. The disadventage:
+  // null/undefined can not be used to remove style any more in `emphasis`.
+
+
+  targetStyle.extendFrom(hoverStl);
+  setDefaultHoverFillStroke(targetStyle, hoverStl, 'fill');
+  setDefaultHoverFillStroke(targetStyle, hoverStl, 'stroke');
+  applyDefaultTextStyle(targetStyle);
+
+  if (!useHoverLayer) {
+    el.dirty(false);
+    el.z2 += 1;
+  }
 }
-/**
- * @inner
- */
 
+function setDefaultHoverFillStroke(targetStyle, hoverStyle, prop) {
+  if (!hasFillOrStroke(hoverStyle[prop]) && hasFillOrStroke(targetStyle[prop])) {
+    targetStyle[prop] = liftColor(targetStyle[prop]);
+  }
+}
 
-function doEnterHover(el) {
-  el.type === 'group' ? el.traverse(function (child) {
-    if (child.type !== 'group') {
-      doSingleEnterHover(child);
-    }
-  }) : doSingleEnterHover(el);
+function doSingleLeaveHover(el) {
+  if (el.__highlighted) {
+    doSingleRestoreHoverStyle(el);
+    el.__highlighted = false;
+  }
 }
 
-function doLeaveHover(el) {
-  el.type === 'group' ? el.traverse(function (child) {
-    if (child.type !== 'group') {
-      doSingleLeaveHover(child);
+function doSingleRestoreHoverStyle(el) {
+  var highlighted = el.__highlighted;
+
+  if (highlighted === 'layer') {
+    el.__zr && el.__zr.removeHover(el);
+  } else if (highlighted) {
+    var style = el.style;
+    var normalStl = el.__normalStl;
+
+    if (normalStl) {
+      rollbackDefaultTextStyle(style); // Consider null/undefined value, should use
+      // `setStyle` but not `extendFrom(stl, true)`.
+
+      el.setStyle(normalStl);
+      applyDefaultTextStyle(style);
+      el.z2 -= 1;
     }
-  }) : doSingleLeaveHover(el);
+  }
+}
+
+function traverseCall(el, method) {
+  el.isGroup ? el.traverse(function (child) {
+    !child.isGroup && method(child);
+  }) : method(el);
 }
 /**
- * @inner
+ * Set hover style of element.
+ *
+ * @param {module:zrender/Element} el Should not be `zrender/container/Group`.
+ * @param {Object|boolean} [hoverStl] The specified hover style.
+ *        If set as `false`, disable the hover style.
+ *        Similarly, The `el.hoverStyle` can alse be set
+ *        as `false` to disable the hover style.
+ *        Otherwise, use the default hover style if not provided.
+ * @param {Object} [opt]
+ * @param {boolean} [opt.hoverSilentOnTouch=false] See `graphic.setAsHoverStyleTrigger`
  */
 
 
-function setElementHoverStl(el, hoverStl) {
-  // If element has sepcified hoverStyle, then use it instead of given hoverStyle
-  // Often used when item group has a label element and it's hoverStyle is different
-  el.__hoverStl = el.hoverStyle || hoverStl || {};
+export function setElementHoverStyle(el, hoverStl) {
+  hoverStl = el.__hoverStl = hoverStl !== false && (hoverStl || {});
   el.__hoverStlDirty = true;
 
-  if (el.__isHover) {
-    cacheElementStl(el);
+  if (el.__highlighted) {
+    doSingleLeaveHover(el);
+    doSingleEnterHover(el);
   }
 }
 /**
- * @inner
+ * Emphasis (called by API) has higher priority than `mouseover`.
+ * When element has been called to be entered emphasis, mouse over
+ * should not trigger the highlight effect (for example, animation
+ * scale) again, and `mouseout` should not downplay the highlight
+ * effect. So the listener of `mouseover` and `mouseout` should
+ * check `isInEmphasis`.
+ *
+ * @param {module:zrender/Element} el
+ * @return {boolean}
  */
 
+export function isInEmphasis(el) {
+  return el && el.__isEmphasisEntered;
+}
 
 function onElementMouseOver(e) {
   if (this.__hoverSilentOnTouch && e.zrByTouch) {
@@ -385,12 +426,8 @@ function onElementMouseOver(e) {
   } // Only if element is not in emphasis status
 
 
-  !this.__isEmphasis && doEnterHover(this);
+  !this.__isEmphasisEntered && traverseCall(this, doSingleEnterHover);
 }
-/**
- * @inner
- */
-
 
 function onElementMouseOut(e) {
   if (this.__hoverSilentOnTouch && e.zrByTouch) {
@@ -398,36 +435,51 @@ function onElementMouseOut(e) {
   } // Only if element is not in emphasis status
 
 
-  !this.__isEmphasis && doLeaveHover(this);
+  !this.__isEmphasisEntered && traverseCall(this, doSingleLeaveHover);
 }
-/**
- * @inner
- */
-
 
 function enterEmphasis() {
-  this.__isEmphasis = true;
-  doEnterHover(this);
+  this.__isEmphasisEntered = true;
+  traverseCall(this, doSingleEnterHover);
 }
-/**
- * @inner
- */
-
 
 function leaveEmphasis() {
-  this.__isEmphasis = false;
-  doLeaveHover(this);
+  this.__isEmphasisEntered = false;
+  traverseCall(this, doSingleLeaveHover);
 }
 /**
  * Set hover style of element.
- * This method can be called repeatly without side-effects.
+ *
+ * [Caveat]:
+ * This method can be called repeatly and achieve the same result.
+ *
+ * [Usage]:
+ * Call the method for a "root" element once. Do not call it for each descendants.
+ * If the descendants elemenets of a group has itself hover style different from the
+ * root group, we can simply mount the style on `el.hoverStyle` for them, but should
+ * not call this method for them.
+ *
  * @param {module:zrender/Element} el
- * @param {Object} [hoverStyle]
+ * @param {Object|boolean} [hoverStyle] See `graphic.setElementHoverStyle`.
  * @param {Object} [opt]
+ * @param {boolean} [opt.hoverSilentOnTouch=false] See `graphic.setAsHoverStyleTrigger`.
+ */
+
+
+export function setHoverStyle(el, hoverStyle, opt) {
+  el.isGroup ? el.traverse(function (child) {
+    // If element has sepcified hoverStyle, then use it instead of given hoverStyle
+    // Often used when item group has a label element and it's hoverStyle is different
+    !child.isGroup && setElementHoverStyle(child, child.hoverStyle || hoverStyle);
+  }) : setElementHoverStyle(el, el.hoverStyle || hoverStyle);
+  setAsHoverStyleTrigger(el, opt);
+}
+/**
+ * @param {Object|boolean} [opt] If `false`, means disable trigger.
  * @param {boolean} [opt.hoverSilentOnTouch=false]
  *        In touch device, mouseover event will be trigger on touchstart event
  *        (see module:zrender/dom/HandlerProxy). By this mechanism, we can
- *        conviniently use hoverStyle when tap on touch screen without additional
+ *        conveniently use hoverStyle when tap on touch screen without additional
  *        code for compatibility.
  *        But if the chart/component has select feature, which usually also use
  *        hoverStyle, there might be conflict between 'select-highlight' and
@@ -436,18 +488,19 @@ function leaveEmphasis() {
  *        on touch device.
  */
 
+export function setAsHoverStyleTrigger(el, opt) {
+  var disable = opt === false;
+  el.__hoverSilentOnTouch = opt != null && opt.hoverSilentOnTouch; // Simple optimize, since this method might be
+  // called for each elements of a group in some cases.
 
-export function setHoverStyle(el, hoverStyle, opt) {
-  el.__hoverSilentOnTouch = opt && opt.hoverSilentOnTouch;
-  el.type === 'group' ? el.traverse(function (child) {
-    if (child.type !== 'group') {
-      setElementHoverStl(child, hoverStyle);
-    }
-  }) : setElementHoverStl(el, hoverStyle); // Duplicated function will be auto-ignored, see Eventful.js.
+  if (!disable || el.__hoverStyleTrigger) {
+    var method = disable ? 'off' : 'on'; // Duplicated function will be auto-ignored, see Eventful.js.
 
-  el.on('mouseover', onElementMouseOver).on('mouseout', onElementMouseOut); // Emphasis, normal can be triggered manually
+    el[method]('mouseover', onElementMouseOver)[method]('mouseout', onElementMouseOut); // Emphasis, normal can be triggered manually
 
-  el.on('emphasis', enterEmphasis).on('normal', leaveEmphasis);
+    el[method]('emphasis', enterEmphasis)[method]('normal', leaveEmphasis);
+    el.__hoverStyleTrigger = !disable;
+  }
 }
 /**
  * @param {Object|module:zrender/graphic/Style} normalStyle
@@ -518,8 +571,8 @@ export function setLabelStyle(normalStyle, emphasisStyle, normalModel, emphasisM
 
 export function setTextStyle(textStyle, textStyleModel, specifiedTextStyle, opt, isEmphasis) {
   setTextStyleCommon(textStyle, textStyleModel, opt, isEmphasis);
-  specifiedTextStyle && zrUtil.extend(textStyle, specifiedTextStyle);
-  textStyle.host && textStyle.host.dirty && textStyle.host.dirty(false);
+  specifiedTextStyle && zrUtil.extend(textStyle, specifiedTextStyle); // textStyle.host && textStyle.host.dirty && textStyle.host.dirty(false);
+
   return textStyle;
 }
 /**
@@ -544,8 +597,7 @@ export function setText(textStyle, labelModel, defaultColor) {
     opt.autoColor = defaultColor;
   }
 
-  setTextStyleCommon(textStyle, labelModel, opt, isEmphasis);
-  textStyle.host && textStyle.host.dirty && textStyle.host.dirty(false);
+  setTextStyleCommon(textStyle, labelModel, opt, isEmphasis); // textStyle.host && textStyle.host.dirty && textStyle.host.dirty(false);
 }
 /**
  * {
@@ -665,17 +717,15 @@ function setTokenTextStyle(textStyle, textStyleModel, globalTextStyle, opt, isEm
   globalTextStyle = !isEmphasis && globalTextStyle || EMPTY_OBJ;
   textStyle.textFill = getAutoColor(textStyleModel.getShallow('color'), opt) || globalTextStyle.color;
   textStyle.textStroke = getAutoColor(textStyleModel.getShallow('textBorderColor'), opt) || globalTextStyle.textBorderColor;
-  textStyle.textStrokeWidth = zrUtil.retrieve2(textStyleModel.getShallow('textBorderWidth'), globalTextStyle.textBorderWidth);
+  textStyle.textStrokeWidth = zrUtil.retrieve2(textStyleModel.getShallow('textBorderWidth'), globalTextStyle.textBorderWidth); // Save original textPosition, because style.textPosition will be repalced by
+  // real location (like [10, 30]) in zrender.
+
+  textStyle.insideRawTextPosition = textStyle.textPosition;
 
   if (!isEmphasis) {
     if (isBlock) {
-      // Always set `insideRollback`, for clearing previous.
-      var originalTextPosition = textStyle.textPosition;
-      textStyle.insideRollback = applyInsideStyle(textStyle, originalTextPosition, opt); // Save original textPosition, because style.textPosition will be repalced by
-      // real location (like [10, 30]) in zrender.
-
-      textStyle.insideOriginalTextPosition = originalTextPosition;
       textStyle.insideRollbackOpt = opt;
+      applyDefaultTextStyle(textStyle);
     } // Set default finally.
 
 
@@ -718,13 +768,26 @@ function setTokenTextStyle(textStyle, textStyleModel, globalTextStyle, opt, isEm
 
 function getAutoColor(color, opt) {
   return color !== 'auto' ? color : opt && opt.autoColor ? opt.autoColor : null;
-}
+} // When text position is `inside` and `textFill` not specified, we
+// provide a mechanism to auto make text border for better view. But
+// text position changing when hovering or being emphasis should be
+// considered, where the `insideRollback` enables to restore the style.
+
+
+function applyDefaultTextStyle(textStyle) {
+  var opt = textStyle.insideRollbackOpt; // Only insideRollbackOpt create (setTextStyleCommon used),
+  // applyDefaultTextStyle works.
+
+  if (!opt || textStyle.textFill != null) {
+    return;
+  }
 
-function applyInsideStyle(textStyle, textPosition, opt) {
   var useInsideStyle = opt.useInsideStyle;
+  var textPosition = textStyle.insideRawTextPosition;
   var insideRollback;
+  var autoColor = opt.autoColor;
 
-  if (textStyle.textFill == null && useInsideStyle !== false && (useInsideStyle === true || opt.isRectText && textPosition // textPosition can be [10, 30]
+  if (useInsideStyle !== false && (useInsideStyle === true || opt.isRectText && textPosition // textPosition can be [10, 30]
   && typeof textPosition === 'string' && textPosition.indexOf('inside') >= 0)) {
     insideRollback = {
       textFill: null,
@@ -734,21 +797,30 @@ function applyInsideStyle(textStyle, textPosition, opt) {
     textStyle.textFill = '#fff'; // Consider text with #fff overflow its container.
 
     if (textStyle.textStroke == null) {
-      textStyle.textStroke = opt.autoColor;
+      textStyle.textStroke = autoColor;
       textStyle.textStrokeWidth == null && (textStyle.textStrokeWidth = 2);
     }
-  }
+  } else if (autoColor != null) {
+    insideRollback = {
+      textFill: null
+    };
+    textStyle.textFill = autoColor;
+  } // Always set `insideRollback`, for clearing previous.
 
-  return insideRollback;
+
+  if (insideRollback) {
+    textStyle.insideRollback = insideRollback;
+  }
 }
 
-function rollbackInsideStyle(style) {
+function rollbackDefaultTextStyle(style) {
   var insideRollback = style.insideRollback;
 
   if (insideRollback) {
     style.textFill = insideRollback.textFill;
     style.textStroke = insideRollback.textStroke;
     style.textStrokeWidth = insideRollback.textStrokeWidth;
+    style.insideRollback = null;
   }
 }
 
@@ -948,6 +1020,8 @@ export function groupTransition(g1, g2, animatableModel, cb) {
  */
 
 export function clipPointsByRect(points, rect) {
+  // FIXME: this way migth be incorrect when grpahic clipped by a corner.
+  // and when element have border.
   return zrUtil.map(points, function (point) {
     var x = point[0];
     x = mathMax(x, rect.x);
@@ -968,7 +1042,8 @@ export function clipRectByRect(targetRect, rect) {
   var x = mathMax(targetRect.x, rect.x);
   var x2 = mathMin(targetRect.x + targetRect.width, rect.x + rect.width);
   var y = mathMax(targetRect.y, rect.y);
-  var y2 = mathMin(targetRect.y + targetRect.height, rect.y + rect.height);
+  var y2 = mathMin(targetRect.y + targetRect.height, rect.y + rect.height); // If the total rect is cliped, nothing, including the border,
+  // should be painted. So return undefined.
 
   if (x2 >= x && y2 >= y) {
     return {
diff --git a/builder/src/echarts/util/model.js b/builder/src/echarts/util/model.js
index 80d53da..26a9439 100644
--- a/builder/src/echarts/util/model.js
+++ b/builder/src/echarts/util/model.js
@@ -17,6 +17,7 @@
 * under the License.
 */
 import * as zrUtil from 'zrender/src/core/util';
+import env from 'zrender/src/core/env';
 var each = zrUtil.each;
 var isObject = zrUtil.isObject;
 var isArray = zrUtil.isArray;
@@ -457,4 +458,12 @@ export function setAttribute(dom, key, value) {
 }
 export function getAttribute(dom, key) {
   return dom.getAttribute ? dom.getAttribute(key) : dom[key];
+}
+export function getTooltipRenderMode(renderModeOption) {
+  if (renderModeOption === 'auto') {
+    // Using html when `document` exists, use richText otherwise
+    return env.domSupported ? 'html' : 'richText';
+  } else {
+    return renderModeOption || 'html';
+  }
 }
\ No newline at end of file
diff --git a/builder/src/echarts/util/nest.js b/builder/src/echarts/util/nest.js
new file mode 100644
index 0000000..b0f8e34
--- /dev/null
+++ b/builder/src/echarts/util/nest.js
@@ -0,0 +1,125 @@
+/*
+* 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.
+*/
+
+/*
+* The implementation references to d3.js. The use of the source
+* code of this file is also subject to the terms and consitions
+* of its license (BSD-3Clause, see <echarts/src/licenses/LICENSE-d3>).
+*/
+import * as zrUtil from 'zrender/src/core/util';
+/**
+ * nest helper used to group by the array.
+ * can specified the keys and sort the keys.
+ */
+
+export default function nest() {
+  var keysFunction = [];
+  var sortKeysFunction = [];
+  /**
+   * map an Array into the mapObject.
+   * @param {Array} array
+   * @param {number} depth
+   */
+
+  function map(array, depth) {
+    if (depth >= keysFunction.length) {
+      return array;
+    }
+
+    var i = -1;
+    var n = array.length;
+    var keyFunction = keysFunction[depth++];
+    var mapObject = {};
+    var valuesByKey = {};
+
+    while (++i < n) {
+      var keyValue = keyFunction(array[i]);
+      var values = valuesByKey[keyValue];
+
+      if (values) {
+        values.push(array[i]);
+      } else {
+        valuesByKey[keyValue] = [array[i]];
+      }
+    }
+
+    zrUtil.each(valuesByKey, function (value, key) {
+      mapObject[key] = map(value, depth);
+    });
+    return mapObject;
+  }
+  /**
+   * transform the Map Object to multidimensional Array
+   * @param {Object} map
+   * @param {number} depth
+   */
+
+
+  function entriesMap(mapObject, depth) {
+    if (depth >= keysFunction.length) {
+      return mapObject;
+    }
+
+    var array = [];
+    var sortKeyFunction = sortKeysFunction[depth++];
+    zrUtil.each(mapObject, function (value, key) {
+      array.push({
+        key: key,
+        values: entriesMap(value, depth)
+      });
+    });
+
+    if (sortKeyFunction) {
+      return array.sort(function (a, b) {
+        return sortKeyFunction(a.key, b.key);
+      });
+    }
+
+    return array;
+  }
+
+  return {
+    /**
+     * specified the key to groupby the arrays.
+     * users can specified one more keys.
+     * @param {Function} d
+     */
+    key: function (d) {
+      keysFunction.push(d);
+      return this;
+    },
+
+    /**
+     * specified the comparator to sort the keys
+     * @param {Function} order
+     */
+    sortKeys: function (order) {
+      sortKeysFunction[keysFunction.length - 1] = order;
+      return this;
+    },
+
+    /**
+     * the array to be grouped by.
+     * @param {Array} array
+     */
+    entries: function (array) {
+      return entriesMap(map(array, 0), 0);
+    }
+  };
+}
\ No newline at end of file
diff --git a/builder/src/echarts/util/number.js b/builder/src/echarts/util/number.js
index 590e978..9a38f45 100644
--- a/builder/src/echarts/util/number.js
+++ b/builder/src/echarts/util/number.js
@@ -277,8 +277,12 @@ export function remRadian(radian) {
 export function isRadianAroundZero(val) {
   return val > -RADIAN_EPSILON && val < RADIAN_EPSILON;
 }
+/* eslint-disable */
+
 var TIME_REG = /^(?:(\d{4})(?:[-\/](\d{1,2})(?:[-\/](\d{1,2})(?:[T ](\d{1,2})(?::(\d\d)(?::(\d\d)(?:[.,](\d+))?)?)?(Z|[\+\-]\d\d:?\d\d)?)?)?)?)?$/; // jshint ignore:line
 
+/* eslint-enable */
+
 /**
  * @param {string|Date|number} value These values can be accepted:
  *   + An instance of Date, represent a time in its own time zone.
diff --git a/builder/src/echarts/util/quickSelect.js b/builder/src/echarts/util/quickSelect.js
index 5ac7813..8d7f9d8 100644
--- a/builder/src/echarts/util/quickSelect.js
+++ b/builder/src/echarts/util/quickSelect.js
@@ -88,7 +88,7 @@ export default function (arr, left, right, nth, compareFunc) {
   if (arguments.length <= 3) {
     nth = left;
 
-    if (arguments.length == 2) {
+    if (arguments.length === 2) {
       compareFunc = defaultCompareFunc;
     } else {
       compareFunc = right;
diff --git a/builder/src/echarts/view/Chart.js b/builder/src/echarts/view/Chart.js
index eb56c11..0f3753c 100644
--- a/builder/src/echarts/view/Chart.js
+++ b/builder/src/echarts/view/Chart.js
@@ -115,6 +115,7 @@ Chart.prototype = {
 
   /**
    * Render in progressive mode.
+   * @param  {Object} params See taskParams in `stream/task.js`
    * @param  {module:echarts/model/Series} seriesModel
    * @param  {module:echarts/model/Global} ecModel
    * @param  {module:echarts/ExtensionAPI} api
@@ -130,7 +131,8 @@ Chart.prototype = {
    * @param  {Object} payload
    * @return {Object} {update: true}
    */
-  updateTransform: null
+  updateTransform: null,
+
   /**
    * The view contains the given point.
    * @interface
@@ -139,6 +141,14 @@ Chart.prototype = {
    */
   // containPoint: function () {}
 
+  /**
+   * @param {string} eventType
+   * @param {Object} query
+   * @param {module:zrender/Element} targetEl
+   * @param {Object} packedEvent
+   * @return {boolen} Pass only when return `true`.
+   */
+  filterForExposedEvent: null
 };
 var chartProto = Chart.prototype;
 
diff --git a/builder/src/echarts/view/Component.js b/builder/src/echarts/view/Component.js
index 0976bb5..8d90d1f 100644
--- a/builder/src/echarts/view/Component.js
+++ b/builder/src/echarts/view/Component.js
@@ -38,7 +38,16 @@ Component.prototype = {
   constructor: Component,
   init: function (ecModel, api) {},
   render: function (componentModel, ecModel, api, payload) {},
-  dispose: function () {}
+  dispose: function () {},
+
+  /**
+   * @param {string} eventType
+   * @param {Object} query
+   * @param {module:zrender/Element} targetEl
+   * @param {Object} packedEvent
+   * @return {boolen} Pass only when return `true`.
+   */
+  filterForExposedEvent: null
 };
 var componentProto = Component.prototype;
 
diff --git a/builder/src/zrender/Handler.js b/builder/src/zrender/Handler.js
index d7c7c98..b804205 100644
--- a/builder/src/zrender/Handler.js
+++ b/builder/src/zrender/Handler.js
@@ -2,6 +2,7 @@ import * as util from './core/util';
 import * as vec2 from './core/vector';
 import Draggable from './mixin/Draggable';
 import Eventful from './mixin/Eventful';
+import * as eventTool from './core/event';
 var SILENT = 'silent';
 
 function makeEventPacket(eveType, targetInfo, event) {
@@ -21,10 +22,15 @@ function makeEventPacket(eveType, targetInfo, event) {
     pinchScale: event.pinchScale,
     wheelDelta: event.zrDelta,
     zrByTouch: event.zrByTouch,
-    which: event.which
+    which: event.which,
+    stop: stopEvent
   };
 }
 
+function stopEvent(event) {
+  eventTool.stop(this.event);
+}
+
 function EmptyProxy() {}
 
 EmptyProxy.prototype.dispose = function () {};
diff --git a/builder/src/zrender/Painter.js b/builder/src/zrender/Painter.js
index 3cfb056..170e078 100644
--- a/builder/src/zrender/Painter.js
+++ b/builder/src/zrender/Painter.js
@@ -260,13 +260,18 @@ Painter.prototype = {
 
     var elMirror = new el.constructor({
       style: el.style,
-      shape: el.shape
+      shape: el.shape,
+      z: el.z,
+      z2: el.z2,
+      silent: el.silent
     });
     elMirror.__from = el;
     el.__hoverMir = elMirror;
-    elMirror.setStyle(hoverStyle);
+    hoverStyle && elMirror.setStyle(hoverStyle);
 
     this._hoverElements.push(elMirror);
+
+    return elMirror;
   },
   removeHover: function (el) {
     var elMirror = el.__hoverMir;
@@ -419,7 +424,7 @@ Painter.prototype = {
 
         this._doPaintEl(el, layer, paintAll, scope);
 
-        el.__dirty = false;
+        el.__dirty = el.__dirtyText = false;
 
         if (useTimer) {
           // Date.now can be executed in 13,025,305 ops/second.
diff --git a/builder/src/zrender/animation/track.js b/builder/src/zrender/animation/track.js
index 1810cfc..9056389 100644
--- a/builder/src/zrender/animation/track.js
+++ b/builder/src/zrender/animation/track.js
@@ -2,8 +2,31 @@ import Clip from './Clip';
 import { isArrayLike } from '../core/util';
 import * as color from '../tool/color';
 var arraySlice = Array.prototype.slice;
-export function createTrackClip(target, getter, setter, easing, loop, delay, ondestroy, keyframes, propName, forceAnimate) {
+/**
+ * @param {Object} target
+ * @param {string} propName
+ * @param {Array.<Object>} keyframes
+ *        [{
+ *            time: number,
+ *            value: number | color string | Array.<number> | Array.<Array.<number>>
+ *        }, ...]
+ *        [Caveat]:
+ *        (1) The order should ensured by time.
+ *        (2) If `value` is `Array`, it must not be shared outside (espaciall el.shape, el.style),
+ *        in case that it be modified outside and cause incorrect interpolate result.
+ * @param {string} easing
+ * @param {boolean} [delay=false]
+ * @param {boolean} [loop=false]
+ * @param {boolean} [forceAnimate=false]
+ * @param {Function} [getter=defaultGetter]
+ * @param {Function} [setter=defaultSetter]
+ * @return {module:zrender/animation/Clip} clip
+ */
+
+export function createTrackClip(target, propName, keyframes, easing, delay, loop, forceAnimate, getter, setter) {
   var useSpline = easing === 'spline';
+  getter = getter || defaultGetter;
+  setter = setter || defaultSetter;
   var trackLen = keyframes.length;
 
   if (!trackLen) {
@@ -17,11 +40,7 @@ export function createTrackClip(target, getter, setter, easing, loop, delay, ond
   var isValueString = false; // For vertices morphing
 
   var arrDim = isValueArray ? getArrayDim(keyframes) : 0;
-  var trackMaxTime; // Sort keyframe as ascending
-
-  keyframes.sort(function (a, b) {
-    return a.time - b.time;
-  });
+  var trackMaxTime;
   trackMaxTime = keyframes[trackLen - 1].time; // Percents of each keyframe
 
   var kfPercents = []; // Value of each keyframe
@@ -41,7 +60,7 @@ export function createTrackClip(target, getter, setter, easing, loop, delay, ond
 
     prevValue = value; // Try converting a string to a color array
 
-    if (typeof value == 'string') {
+    if (typeof value === 'string') {
       var colorArray = color.parse(value);
 
       if (colorArray) {
@@ -87,7 +106,7 @@ export function createTrackClip(target, getter, setter, easing, loop, delay, ond
     var rgba = [0, 0, 0, 0];
   }
 
-  var onframe = function (target, percent) {
+  function hanleFrame(target, percent) {
     // Find the range keyframes
     // kf1-----kf2---------current--------kf3
     // find kf2 and kf3 and do interpolation
@@ -167,18 +186,21 @@ export function createTrackClip(target, getter, setter, easing, loop, delay, ond
           value = interpolateNumber(kfValues[frame], kfValues[frame + 1], w);
         }
 
+        if (target.aaaa != null) {
+          console.log(target.uuid, value, propName);
+        }
+
         setter(target, propName, value);
       }
     }
-  };
+  }
 
   var clip = new Clip({
     target: target,
     life: trackMaxTime,
     loop: loop,
     delay: delay,
-    onframe: onframe,
-    ondestroy: ondestroy
+    onframe: hanleFrame
   });
 
   if (easing && easing !== 'spline') {
@@ -287,7 +309,7 @@ function fillArr(arr0, arr1, arrDim) {
 function catmullRomInterpolateArray(p0, p1, p2, p3, t, t2, t3, out, arrDim) {
   var len = p0.length;
 
-  if (arrDim == 1) {
+  if (arrDim === 1) {
     for (var i = 0; i < len; i++) {
       out[i] = catmullRomInterpolate(p0[i], p1[i], p2[i], p3[i], t, t2, t3);
     }
@@ -353,7 +375,7 @@ function interpolateString(p0, p1, percent) {
 function interpolateArray(p0, p1, percent, out, arrDim) {
   var len = p0.length;
 
-  if (arrDim == 1) {
+  if (arrDim === 1) {
     for (var i = 0; i < len; i++) {
       out[i] = interpolateNumber(p0[i], p1[i], percent);
     }
@@ -373,4 +395,30 @@ function rgba2String(rgba) {
   rgba[1] = Math.floor(rgba[1]);
   rgba[2] = Math.floor(rgba[2]);
   return 'rgba(' + rgba.join(',') + ')';
+}
+
+export function cloneFrameValue(value) {
+  if (isArrayLike(value)) {
+    var len = value.length;
+
+    if (isArrayLike(value[0])) {
+      var ret = [];
+
+      for (var i = 0; i < len; i++) {
+        ret.push(arraySlice.call(value[i]));
+      }
+
+      return ret;
+    }
+
+    return arraySlice.call(value);
+  }
+
+  return value;
+}
+export function defaultGetter(target, key) {
+  return target[key];
+}
+export function defaultSetter(target, key, value) {
+  target[key] = value;
 }
\ No newline at end of file
diff --git a/builder/src/zrender/core/PathProxy.js b/builder/src/zrender/core/PathProxy.js
index 22f34c9..a70d5cb 100644
--- a/builder/src/zrender/core/PathProxy.js
+++ b/builder/src/zrender/core/PathProxy.js
@@ -206,7 +206,7 @@ PathProxy.prototype = {
     this.addData(CMD.A, cx, cy, r, r, startAngle, endAngle - startAngle, 0, anticlockwise ? 0 : 1);
     this._ctx && this._ctx.arc(cx, cy, r, startAngle, endAngle, anticlockwise);
     this._xi = mathCos(endAngle) * r + cx;
-    this._yi = mathSin(endAngle) * r + cx;
+    this._yi = mathSin(endAngle) * r + cy;
     return this;
   },
   // TODO
diff --git a/builder/src/zrender/core/env.js b/builder/src/zrender/core/env.js
index d759498..f65d019 100644
--- a/builder/src/zrender/core/env.js
+++ b/builder/src/zrender/core/env.js
@@ -17,7 +17,8 @@ if (typeof wx === 'object' && typeof wx.getSystemInfoSync === 'function') {
     // Weixin Application
     canvasSupported: true,
     svgSupported: false,
-    touchEventsSupported: true
+    touchEventsSupported: true,
+    domSupported: false
   };
 } else if (typeof document === 'undefined' && typeof self !== 'undefined') {
   // In worker
@@ -26,7 +27,8 @@ if (typeof wx === 'object' && typeof wx.getSystemInfoSync === 'function') {
     os: {},
     node: false,
     worker: true,
-    canvasSupported: true
+    canvasSupported: true,
+    domSupported: false
   };
 } else if (typeof navigator === 'undefined') {
   // In node
@@ -37,7 +39,8 @@ if (typeof wx === 'object' && typeof wx.getSystemInfoSync === 'function') {
     worker: false,
     // Assume canvas is supported
     canvasSupported: true,
-    svgSupported: true
+    svgSupported: true,
+    domSupported: false
   };
 } else {
   env = detect(navigator.userAgent);
@@ -137,8 +140,9 @@ function detect(ua) {
     // events currently. So we dont use that on other browsers unless tested sufficiently.
     // Although IE 10 supports pointer event, it use old style and is different from the
     // standard. So we exclude that. (IE 10 is hardly used on touch device)
-    && (browser.edge || browser.ie && browser.version >= 11) // passiveSupported: detectPassiveSupport()
-
+    && (browser.edge || browser.ie && browser.version >= 11),
+    // passiveSupported: detectPassiveSupport()
+    domSupported: typeof document !== 'undefined'
   };
 } // See https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md#feature-detection
 // function detectPassiveSupport() {
diff --git a/builder/src/zrender/core/util.js b/builder/src/zrender/core/util.js
index de7f706..c592a82 100644
--- a/builder/src/zrender/core/util.js
+++ b/builder/src/zrender/core/util.js
@@ -595,15 +595,17 @@ export function isPrimitive(obj) {
  */
 
 function HashMap(obj) {
-  var isArr = isArray(obj);
+  var isArr = isArray(obj); // Key should not be set on this, otherwise
+  // methods get/set/... may be overrided.
+
+  this.data = {};
   var thisMap = this;
   obj instanceof HashMap ? obj.each(visit) : obj && each(obj, visit);
 
   function visit(value, key) {
     isArr ? thisMap.set(value, key) : thisMap.set(key, value);
   }
-} // Add prefix to avoid conflict with Object.prototype.
-
+}
 
 HashMap.prototype = {
   constructor: HashMap,
@@ -611,25 +613,25 @@ HashMap.prototype = {
   // (We usually treat `null` and `undefined` as the same, different
   // from ES6 Map).
   get: function (key) {
-    return this.hasOwnProperty(key) ? this[key] : null;
+    return this.data.hasOwnProperty(key) ? this.data[key] : null;
   },
   set: function (key, value) {
     // Comparing with invocation chaining, `return value` is more commonly
     // used in this case: `var someVal = map.set('a', genVal());`
-    return this[key] = value;
+    return this.data[key] = value;
   },
   // Although util.each can be performed on this hashMap directly, user
   // should not use the exposed keys, who are prefixed.
   each: function (cb, context) {
     context !== void 0 && (cb = bind(cb, context));
 
-    for (var key in this) {
-      this.hasOwnProperty(key) && cb(this[key], key);
+    for (var key in this.data) {
+      this.data.hasOwnProperty(key) && cb(this.data[key], key);
     }
   },
   // Do not use this method if performance sensitive.
   removeKey: function (key) {
-    delete this[key];
+    delete this.data[key];
   }
 };
 export function createHashMap(obj) {
diff --git a/builder/src/zrender/export.js b/builder/src/zrender/export.js
index 49ea6bd..a862c13 100644
--- a/builder/src/zrender/export.js
+++ b/builder/src/zrender/export.js
@@ -6,6 +6,7 @@ import * as matrix from './core/matrix';
 import * as vector from './core/vector';
 import * as colorTool from './tool/color';
 import * as pathTool from './tool/path';
+import { parseSVG } from './tool/parseSVG';
 export { default as Group } from './container/Group';
 export { default as Path } from './graphic/Path';
 export { default as Image } from './graphic/Image';
@@ -36,4 +37,5 @@ export { matrix };
 export { vector };
 export { colorTool as color };
 export { pathTool as path };
-export { zrUtil as util };
\ No newline at end of file
+export { zrUtil as util };
+export { parseSVG };
\ No newline at end of file
diff --git a/builder/src/zrender/graphic/Displayable.js b/builder/src/zrender/graphic/Displayable.js
index ab57bc7..78a96d8 100644
--- a/builder/src/zrender/graphic/Displayable.js
+++ b/builder/src/zrender/graphic/Displayable.js
@@ -133,8 +133,12 @@ Displayable.prototype = {
    * @type {boolean}
    */
   incremental: false,
-  // inplace is used with incremental
-  inplace: false,
+
+  /**
+   * Scale ratio for global scale.
+   * @type {boolean}
+   */
+  globalScaleRatio: 1,
   beforeBrush: function (ctx) {},
   afterBrush: function (ctx) {},
 
@@ -189,7 +193,7 @@ Displayable.prototype = {
    * Mark displayable element dirty and refresh next frame
    */
   dirty: function () {
-    this.__dirty = true;
+    this.__dirty = this.__dirtyText = true;
     this._rect = null;
     this.__zr && this.__zr.refresh();
   },
diff --git a/builder/src/zrender/graphic/Path.js b/builder/src/zrender/graphic/Path.js
index 78a531b..f762920 100644
--- a/builder/src/zrender/graphic/Path.js
+++ b/builder/src/zrender/graphic/Path.js
@@ -100,14 +100,32 @@ Path.prototype = {
       this.path.rebuildPath(ctx);
     }
 
-    hasFill && path.fill(ctx);
+    if (hasFill) {
+      if (style.fillOpacity != null) {
+        var originalGlobalAlpha = ctx.globalAlpha;
+        ctx.globalAlpha = style.fillOpacity * style.opacity;
+        path.fill(ctx);
+        ctx.globalAlpha = originalGlobalAlpha;
+      } else {
+        path.fill(ctx);
+      }
+    }
 
     if (lineDash && ctxLineDash) {
       ctx.setLineDash(lineDash);
       ctx.lineDashOffset = lineDashOffset;
     }
 
-    hasStroke && path.stroke(ctx);
+    if (hasStroke) {
+      if (style.strokeOpacity != null) {
+        var originalGlobalAlpha = ctx.globalAlpha;
+        ctx.globalAlpha = style.strokeOpacity * style.opacity;
+        path.stroke(ctx);
+        ctx.globalAlpha = originalGlobalAlpha;
+      } else {
+        path.stroke(ctx);
+      }
+    }
 
     if (lineDash && ctxLineDash) {
       // PENDING
@@ -232,7 +250,7 @@ Path.prototype = {
       this._rect = null;
     }
 
-    this.__dirty = true;
+    this.__dirty = this.__dirtyText = true;
     this.__zr && this.__zr.refresh(); // Used as a clipping path
 
     if (this.__clipTarget) {
diff --git a/builder/src/zrender/graphic/Style.js b/builder/src/zrender/graphic/Style.js
index ce9772c..9b95929 100644
--- a/builder/src/zrender/graphic/Style.js
+++ b/builder/src/zrender/graphic/Style.js
@@ -2,9 +2,8 @@ import fixShadow from './helper/fixShadow';
 var STYLE_COMMON_PROPS = [['shadowBlur', 0], ['shadowOffsetX', 0], ['shadowOffsetY', 0], ['shadowColor', '#000'], ['lineCap', 'butt'], ['lineJoin', 'miter'], ['miterLimit', 10]]; // var SHADOW_PROPS = STYLE_COMMON_PROPS.slice(0, 4);
 // var LINE_PROPS = STYLE_COMMON_PROPS.slice(4);
 
-var Style = function (opts, host) {
+var Style = function (opts) {
   this.extendFrom(opts, false);
-  this.host = host;
 };
 
 function createLinearGradient(ctx, obj, rect) {
@@ -51,11 +50,6 @@ Style.prototype = {
   constructor: Style,
 
   /**
-   * @type {module:zrender/graphic/Displayable}
-   */
-  host: null,
-
-  /**
    * @type {string}
    */
   fill: '#000',
@@ -71,6 +65,16 @@ Style.prototype = {
   opacity: 1,
 
   /**
+   * @type {number}
+   */
+  fillOpacity: null,
+
+  /**
+   * @type {number}
+   */
+  strokeOpacity: null,
+
+  /**
    * @type {Array.<number>}
    */
   lineDash: null,
diff --git a/builder/src/zrender/graphic/Text.js b/builder/src/zrender/graphic/Text.js
index 048e7a1..761575b 100644
--- a/builder/src/zrender/graphic/Text.js
+++ b/builder/src/zrender/graphic/Text.js
@@ -25,16 +25,17 @@ Text.prototype = {
     style.fill = style.stroke = style.shadowBlur = style.shadowColor = style.shadowOffsetX = style.shadowOffsetY = null;
     var text = style.text; // Convert to string
 
-    text != null && (text += ''); // Always bind style
-
-    style.bind(ctx, this, prevEl);
+    text != null && (text += ''); // Do not apply style.bind in Text node. Because the real bind job
+    // is in textHelper.renderText, and performance of text render should
+    // be considered.
+    // style.bind(ctx, this, prevEl);
 
     if (!textHelper.needDrawText(text, style)) {
       return;
     }
 
     this.setTransform(ctx);
-    textHelper.renderText(this, ctx, text, style);
+    textHelper.renderText(this, ctx, text, style, null, prevEl);
     this.restoreTransform(ctx);
   },
   getBoundingRect: function () {
diff --git a/builder/src/zrender/graphic/helper/image.js b/builder/src/zrender/graphic/helper/image.js
index 09835e0..2209f82 100644
--- a/builder/src/zrender/graphic/helper/image.js
+++ b/builder/src/zrender/graphic/helper/image.js
@@ -48,7 +48,7 @@ export function createOrUpdateImage(newImageOrSrc, image, hostEl, cb, cbPayload)
       !isImageReady(image) && cachedImgObj.pending.push(pendingWrap);
     } else {
       !image && (image = new Image());
-      image.onload = imageOnLoad;
+      image.onload = image.onerror = imageOnLoad;
       globalImageCache.put(newImageOrSrc, image.__cachedImgObj = {
         image: image,
         pending: [pendingWrap]
@@ -65,7 +65,7 @@ export function createOrUpdateImage(newImageOrSrc, image, hostEl, cb, cbPayload)
 
 function imageOnLoad() {
   var cachedImgObj = this.__cachedImgObj;
-  this.onload = this.__cachedImgObj = null;
+  this.onload = this.onerror = this.__cachedImgObj = null;
 
   for (var i = 0; i < cachedImgObj.pending.length; i++) {
     var pendingWrap = cachedImgObj.pending[i];
diff --git a/builder/src/zrender/graphic/helper/text.js b/builder/src/zrender/graphic/helper/text.js
index 6a9fa1a..aaceba5 100644
--- a/builder/src/zrender/graphic/helper/text.js
+++ b/builder/src/zrender/graphic/helper/text.js
@@ -1,4 +1,4 @@
-import { retrieve2, retrieve3, each, normalizeCssArray, isString, isObject } from '../../core/util';
+import { retrieve2, retrieve3, each, normalizeCssArray, isString, isObject, isFunction } from '../../core/util';
 import * as textContain from '../../contain/text';
 import * as roundRectHelper from './roundRect';
 import * as imageHelper from './image';
@@ -13,7 +13,10 @@ var VALID_TEXT_VERTICAL_ALIGN = {
   top: 1,
   bottom: 1,
   middle: 1
-};
+}; // Different from `STYLE_COMMON_PROPS` of `graphic/Style`,
+// the default value of shadowColor is `'transparent'`.
+
+var SHADOW_STYLE_COMMON_PROPS = [['textShadowBlur', 'shadowBlur', 0], ['textShadowOffsetX', 'shadowOffsetX', 0], ['textShadowOffsetY', 'shadowOffsetY', 0], ['textShadowColor', 'shadowColor', 'transparent']];
 /**
  * @param {module:zrender/graphic/Style} style
  * @return {module:zrender/graphic/Style} The input style.
@@ -48,20 +51,42 @@ function normalizeStyle(style) {
  * @param {module:zrender/graphic/Style} style
  * @param {Object|boolean} [rect] {x, y, width, height}
  *                  If set false, rect text is not used.
+ * @param {Element} [prevEl] For ctx prop cache.
  */
 
 
-export function renderText(hostEl, ctx, text, style, rect) {
-  style.rich ? renderRichText(hostEl, ctx, text, style, rect) : renderPlainText(hostEl, ctx, text, style, rect);
-}
+export function renderText(hostEl, ctx, text, style, rect, prevEl) {
+  style.rich ? renderRichText(hostEl, ctx, text, style, rect) : renderPlainText(hostEl, ctx, text, style, rect, prevEl);
+} // Avoid setting to ctx according to prevEl if possible for
+// performance in scenarios of large amount text.
+
+function renderPlainText(hostEl, ctx, text, style, rect, prevEl) {
+  'use strict';
+
+  var prevStyle = prevEl && prevEl.style; // Some cache only available on textEl.
+
+  var isPrevTextEl = prevStyle && prevEl.type === 'text';
+  var styleFont = style.font || textContain.DEFAULT_FONT;
+
+  if (!isPrevTextEl || styleFont !== (prevStyle.font || textContain.DEFAULT_FONT)) {
+    ctx.font = styleFont;
+  } // Use the final font from context-2d, because the final
+  // font might not be the style.font when it is illegal.
+  // But get `ctx.font` might be time consuming.
+
+
+  var computedFont = hostEl.__computedFont;
+
+  if (hostEl.__styleFont !== styleFont) {
+    hostEl.__styleFont = styleFont;
+    computedFont = hostEl.__computedFont = ctx.font;
+  }
 
-function renderPlainText(hostEl, ctx, text, style, rect) {
-  var font = setCtx(ctx, 'font', style.font || textContain.DEFAULT_FONT);
   var textPadding = style.textPadding;
   var contentBlock = hostEl.__textCotentBlock;
 
-  if (!contentBlock || hostEl.__dirty) {
-    contentBlock = hostEl.__textCotentBlock = textContain.parsePlainText(text, font, textPadding, style.truncate);
+  if (!contentBlock || hostEl.__dirtyText) {
+    contentBlock = hostEl.__textCotentBlock = textContain.parsePlainText(text, computedFont, textPadding, style.truncate);
   }
 
   var outerHeight = contentBlock.outerHeight;
@@ -70,7 +95,7 @@ function renderPlainText(hostEl, ctx, text, style, rect) {
   var boxPos = getBoxPosition(outerHeight, style, rect);
   var baseX = boxPos.baseX;
   var baseY = boxPos.baseY;
-  var textAlign = boxPos.textAlign;
+  var textAlign = boxPos.textAlign || 'left';
   var textVerticalAlign = boxPos.textVerticalAlign; // Origin of textRotation should be the base point of text drawing.
 
   applyTextRotation(ctx, style, rect, baseX, baseY);
@@ -81,7 +106,7 @@ function renderPlainText(hostEl, ctx, text, style, rect) {
 
   if (needDrawBg || textPadding) {
     // Consider performance, do not call getTextWidth util necessary.
-    var textWidth = textContain.getWidth(text, font);
+    var textWidth = textContain.getWidth(text, computedFont);
     var outerWidth = textWidth;
     textPadding && (outerWidth += textPadding[1] + textPadding[3]);
     var boxX = textContain.adjustTextX(baseX, outerWidth, textAlign);
@@ -91,44 +116,71 @@ function renderPlainText(hostEl, ctx, text, style, rect) {
       textX = getTextXForPadding(baseX, textAlign, textPadding);
       textY += textPadding[0];
     }
-  }
+  } // Always set textAlign and textBase line, because it is difficute to calculate
+  // textAlign from prevEl, and we dont sure whether textAlign will be reset if
+  // font set happened.
 
-  setCtx(ctx, 'textAlign', textAlign || 'left'); // Force baseline to be "middle". Otherwise, if using "top", the
+
+  ctx.textAlign = textAlign; // Force baseline to be "middle". Otherwise, if using "top", the
   // text will offset downward a little bit in font "Microsoft YaHei".
 
-  setCtx(ctx, 'textBaseline', 'middle'); // Always set shadowBlur and shadowOffset to avoid leak from displayable.
+  ctx.textBaseline = 'middle'; // Always set shadowBlur and shadowOffset to avoid leak from displayable.
+
+  for (var i = 0; i < SHADOW_STYLE_COMMON_PROPS.length; i++) {
+    var propItem = SHADOW_STYLE_COMMON_PROPS[i];
+    var styleProp = propItem[0];
+    var ctxProp = propItem[1];
+    var val = style[styleProp];
+
+    if (!isPrevTextEl || val !== prevStyle[styleProp]) {
+      ctx[ctxProp] = fixShadow(ctx, ctxProp, val || propItem[2]);
+    }
+  } // `textBaseline` is set as 'middle'.
 
-  setCtx(ctx, 'shadowBlur', style.textShadowBlur || 0);
-  setCtx(ctx, 'shadowColor', style.textShadowColor || 'transparent');
-  setCtx(ctx, 'shadowOffsetX', style.textShadowOffsetX || 0);
-  setCtx(ctx, 'shadowOffsetY', style.textShadowOffsetY || 0); // `textBaseline` is set as 'middle'.
 
   textY += lineHeight / 2;
   var textStrokeWidth = style.textStrokeWidth;
+  var textStrokeWidthPrev = isPrevTextEl ? prevStyle.textStrokeWidth : null;
+  var strokeWidthChanged = !isPrevTextEl || textStrokeWidth !== textStrokeWidthPrev;
+  var strokeChanged = !isPrevTextEl || strokeWidthChanged || style.textStroke !== prevStyle.textStroke;
   var textStroke = getStroke(style.textStroke, textStrokeWidth);
   var textFill = getFill(style.textFill);
 
   if (textStroke) {
-    setCtx(ctx, 'lineWidth', textStrokeWidth);
-    setCtx(ctx, 'strokeStyle', textStroke);
+    if (strokeWidthChanged) {
+      ctx.lineWidth = textStrokeWidth;
+    }
+
+    if (strokeChanged) {
+      ctx.strokeStyle = textStroke;
+    }
   }
 
   if (textFill) {
-    setCtx(ctx, 'fillStyle', textFill);
-  }
+    if (!isPrevTextEl || style.textFill !== prevStyle.textFill || prevStyle.textBackgroundColor) {
+      ctx.fillStyle = textFill;
+    }
+  } // Optimize simply, in most cases only one line exists.
 
-  for (var i = 0; i < textLines.length; i++) {
+
+  if (textLines.length === 1) {
     // Fill after stroke so the outline will not cover the main part.
-    textStroke && ctx.strokeText(textLines[i], textX, textY);
-    textFill && ctx.fillText(textLines[i], textX, textY);
-    textY += lineHeight;
+    textStroke && ctx.strokeText(textLines[0], textX, textY);
+    textFill && ctx.fillText(textLines[0], textX, textY);
+  } else {
+    for (var i = 0; i < textLines.length; i++) {
+      // Fill after stroke so the outline will not cover the main part.
+      textStroke && ctx.strokeText(textLines[i], textX, textY);
+      textFill && ctx.fillText(textLines[i], textX, textY);
+      textY += lineHeight;
+    }
   }
 }
 
 function renderRichText(hostEl, ctx, text, style, rect) {
   var contentBlock = hostEl.__textCotentBlock;
 
-  if (!contentBlock || hostEl.__dirty) {
+  if (!contentBlock || hostEl.__dirtyText) {
     contentBlock = hostEl.__textCotentBlock = textContain.parseRichText(text, style);
   }
 
@@ -222,7 +274,8 @@ function applyTextRotation(ctx, style, rect, x, y) {
 }
 
 function placeToken(hostEl, ctx, token, style, lineHeight, lineTop, x, textAlign) {
-  var tokenStyle = style.rich[token.styleName] || {}; // 'ctx.textBaseline' is always set as 'middle', for sake of
+  var tokenStyle = style.rich[token.styleName] || {};
+  tokenStyle.text = token.text; // 'ctx.textBaseline' is always set as 'middle', for sake of
   // the bias of "Microsoft YaHei".
 
   var textVerticalAlign = token.textVerticalAlign;
@@ -269,7 +322,7 @@ function placeToken(hostEl, ctx, token, style, lineHeight, lineTop, x, textAlign
 
 function needDrawBackground(style) {
   return style.textBackgroundColor || style.textBorderWidth && style.textBorderColor;
-} // style: {textBackgroundColor, textBorderWidth, textBorderColor, textBorderRadius}
+} // style: {textBackgroundColor, textBorderWidth, textBorderColor, textBorderRadius, text}
 // shape: {x, y, width, height}
 
 
@@ -304,6 +357,17 @@ function drawBackground(hostEl, ctx, style, x, y, width, height) {
 
   if (isPlainBg) {
     setCtx(ctx, 'fillStyle', textBackgroundColor);
+
+    if (style.fillOpacity != null) {
+      var originalGlobalAlpha = ctx.globalAlpha;
+      ctx.globalAlpha = style.fillOpacity * style.opacity;
+      ctx.fill();
+      ctx.globalAlpha = originalGlobalAlpha;
+    } else {
+      ctx.fill();
+    }
+  } else if (isFunction(textBackgroundColor)) {
+    setCtx(ctx, 'fillStyle', textBackgroundColor(style));
     ctx.fill();
   } else if (isObject(textBackgroundColor)) {
     var image = textBackgroundColor.image;
@@ -317,7 +381,15 @@ function drawBackground(hostEl, ctx, style, x, y, width, height) {
   if (textBorderWidth && textBorderColor) {
     setCtx(ctx, 'lineWidth', textBorderWidth);
     setCtx(ctx, 'strokeStyle', textBorderColor);
-    ctx.stroke();
+
+    if (style.strokeOpacity != null) {
+      var originalGlobalAlpha = ctx.globalAlpha;
+      ctx.globalAlpha = style.strokeOpacity * style.opacity;
+      ctx.stroke();
+      ctx.globalAlpha = originalGlobalAlpha;
+    } else {
+      ctx.stroke();
+    }
   }
 }
 
diff --git a/builder/src/zrender/graphic/mixin/RectText.js b/builder/src/zrender/graphic/mixin/RectText.js
index 414efc1..57b707a 100644
--- a/builder/src/zrender/graphic/mixin/RectText.js
+++ b/builder/src/zrender/graphic/mixin/RectText.js
@@ -28,6 +28,9 @@ RectText.prototype = {
     if (!textHelper.needDrawText(text, style)) {
       return;
     } // FIXME
+    // Do not provide prevEl to `textHelper.renderText` for ctx prop cache,
+    // but use `ctx.save()` and `ctx.restore()`. Because the cache for rect
+    // text propably break the cache for its host elements.
 
 
     ctx.save(); // Transform rect to view space
diff --git a/builder/src/zrender/mixin/Animatable.js b/builder/src/zrender/mixin/Animatable.js
index ca38905..2a57e09 100644
--- a/builder/src/zrender/mixin/Animatable.js
+++ b/builder/src/zrender/mixin/Animatable.js
@@ -125,121 +125,142 @@ Animatable.prototype = {
    */
   // TODO Return animation key
   animateTo: function (target, time, delay, easing, callback, forceAnimate) {
-    // animateTo(target, time, easing, callback);
-    if (isString(delay)) {
+    animateTo(this, target, time, delay, easing, callback, forceAnimate);
+  },
+
+  /**
+   * Animate from the target state to current state.
+   * The params and the return value are the same as `this.animateTo`.
+   */
+  animateFrom: function (target, time, delay, easing, callback, forceAnimate) {
+    animateTo(this, target, time, delay, easing, callback, forceAnimate, true);
+  }
+};
+
+function animateTo(animatable, target, time, delay, easing, callback, forceAnimate, reverse) {
+  // animateTo(target, time, easing, callback);
+  if (isString(delay)) {
+    callback = easing;
+    easing = delay;
+    delay = 0;
+  } // animateTo(target, time, delay, callback);
+  else if (isFunction(easing)) {
       callback = easing;
-      easing = delay;
+      easing = 'linear';
       delay = 0;
-    } // animateTo(target, time, delay, callback);
-    else if (isFunction(easing)) {
-        callback = easing;
-        easing = 'linear';
+    } // animateTo(target, time, callback);
+    else if (isFunction(delay)) {
+        callback = delay;
         delay = 0;
-      } // animateTo(target, time, callback);
-      else if (isFunction(delay)) {
-          callback = delay;
-          delay = 0;
-        } // animateTo(target, callback)
-        else if (isFunction(time)) {
-            callback = time;
+      } // animateTo(target, callback)
+      else if (isFunction(time)) {
+          callback = time;
+          time = 500;
+        } // animateTo(target)
+        else if (!time) {
             time = 500;
-          } // animateTo(target)
-          else if (!time) {
-              time = 500;
-            } // Stop all previous animations
+          } // Stop all previous animations
 
 
-    this.stopAnimation();
+  animatable.stopAnimation();
+  animateToShallow(animatable, '', animatable, target, time, delay, reverse); // Animators may be removed immediately after start
+  // if there is nothing to animate
 
-    this._animateToShallow('', this, target, time, delay); // Animators may be removed immediately after start
-    // if there is nothing to animate
+  var animators = animatable.animators.slice();
+  var count = animators.length;
 
+  function done() {
+    count--;
 
-    var animators = this.animators.slice();
-    var count = animators.length;
+    if (!count) {
+      callback && callback();
+    }
+  } // No animators. This should be checked before animators[i].start(),
+  // because 'done' may be executed immediately if no need to animate.
 
-    function done() {
-      count--;
 
-      if (!count) {
-        callback && callback();
-      }
-    } // No animators. This should be checked before animators[i].start(),
-    // because 'done' may be executed immediately if no need to animate.
+  if (!count) {
+    callback && callback();
+  } // Start after all animators created
+  // Incase any animator is done immediately when all animation properties are not changed
 
 
-    if (!count) {
-      callback && callback();
-    } // Start after all animators created
-    // Incase any animator is done immediately when all animation properties are not changed
+  for (var i = 0; i < animators.length; i++) {
+    animators[i].done(done).start(easing, forceAnimate);
+  }
+}
+/**
+ * @param {string} path=''
+ * @param {Object} source=animatable
+ * @param {Object} target
+ * @param {number} [time=500]
+ * @param {number} [delay=0]
+ * @param {boolean} [reverse] If `true`, animate
+ *        from the `target` to current state.
+ *
+ * @example
+ *  // Animate position
+ *  el._animateToShallow({
+ *      position: [10, 10]
+ *  })
+ *
+ *  // Animate shape, style and position in 100ms, delayed 100ms
+ *  el._animateToShallow({
+ *      shape: {
+ *          width: 500
+ *      },
+ *      style: {
+ *          fill: 'red'
+ *      }
+ *      position: [10, 10]
+ *  }, 100, 100)
+ */
 
 
-    for (var i = 0; i < animators.length; i++) {
-      animators[i].done(done).start(easing, forceAnimate);
-    }
-  },
+function animateToShallow(animatable, path, source, target, time, delay, reverse) {
+  var objShallow = {};
+  var propertyCount = 0;
 
-  /**
-   * @private
-   * @param {string} path=''
-   * @param {Object} source=this
-   * @param {Object} target
-   * @param {number} [time=500]
-   * @param {number} [delay=0]
-   *
-   * @example
-   *  // Animate position
-   *  el._animateToShallow({
-   *      position: [10, 10]
-   *  })
-   *
-   *  // Animate shape, style and position in 100ms, delayed 100ms
-   *  el._animateToShallow({
-   *      shape: {
-   *          width: 500
-   *      },
-   *      style: {
-   *          fill: 'red'
-   *      }
-   *      position: [10, 10]
-   *  }, 100, 100)
-   */
-  _animateToShallow: function (path, source, target, time, delay) {
-    var objShallow = {};
-    var propertyCount = 0;
-
-    for (var name in target) {
-      if (!target.hasOwnProperty(name)) {
-        continue;
-      }
+  for (var name in target) {
+    if (!target.hasOwnProperty(name)) {
+      continue;
+    }
 
-      if (source[name] != null) {
-        if (isObject(target[name]) && !isArrayLike(target[name])) {
-          this._animateToShallow(path ? path + '.' + name : name, source[name], target[name], time, delay);
+    if (source[name] != null) {
+      if (isObject(target[name]) && !isArrayLike(target[name])) {
+        animateToShallow(animatable, path ? path + '.' + name : name, source[name], target[name], time, delay, reverse);
+      } else {
+        if (reverse) {
+          objShallow[name] = source[name];
+          setAttrByPath(animatable, path, name, target[name]);
         } else {
           objShallow[name] = target[name];
-          propertyCount++;
-        }
-      } else if (target[name] != null) {
-        // Attr directly if not has property
-        // FIXME, if some property not needed for element ?
-        if (!path) {
-          this.attr(name, target[name]);
-        } else {
-          // Shape or style
-          var props = {};
-          props[path] = {};
-          props[path][name] = target[name];
-          this.attr(props);
         }
-      }
-    }
 
-    if (propertyCount > 0) {
-      this.animate(path, false).when(time == null ? 500 : time, objShallow).delay(delay || 0);
+        propertyCount++;
+      }
+    } else if (target[name] != null && !reverse) {
+      setAttrByPath(animatable, path, name, target[name]);
     }
+  }
 
-    return this;
+  if (propertyCount > 0) {
+    animatable.animate(path, false).when(time == null ? 500 : time, objShallow).delay(delay || 0);
   }
-};
+}
+
+function setAttrByPath(el, path, name, value) {
+  // Attr directly if not has property
+  // FIXME, if some property not needed for element ?
+  if (!path) {
+    el.attr(name, value);
+  } else {
+    // Only support set shape or style
+    var props = {};
+    props[path] = {};
+    props[path][name] = value;
+    el.attr(props);
+  }
+}
+
 export default Animatable;
\ No newline at end of file
diff --git a/builder/src/zrender/mixin/Eventful.js b/builder/src/zrender/mixin/Eventful.js
index 33d46ef..081eab1 100644
--- a/builder/src/zrender/mixin/Eventful.js
+++ b/builder/src/zrender/mixin/Eventful.js
@@ -1,37 +1,60 @@
 /**
- * 事件扩展
+ * Event Mixin
  * @module zrender/mixin/Eventful
  * @author Kener (@Kener-林峰, kener.linfeng@gmail.com)
  *         pissang (https://www.github.com/pissang)
  */
 var arrySlice = Array.prototype.slice;
 /**
- * 事件分发器
+ * Event dispatcher.
+ *
  * @alias module:zrender/mixin/Eventful
  * @constructor
+ * @param {Object} [eventProcessor] The object eventProcessor is the scope when
+ *        `eventProcessor.xxx` called.
+ * @param {Function} [eventProcessor.normalizeQuery]
+ *        param: {string|Object} Raw query.
+ *        return: {string|Object} Normalized query.
+ * @param {Function} [eventProcessor.filter] Event will be dispatched only
+ *        if it returns `true`.
+ *        param: {string} eventType
+ *        param: {string|Object} query
+ *        return: {boolean}
+ * @param {Function} [eventProcessor.afterTrigger] Call after all handlers called.
+ *        param: {string} eventType
  */
 
-var Eventful = function () {
+var Eventful = function (eventProcessor) {
   this._$handlers = {};
+  this._$eventProcessor = eventProcessor;
 };
 
 Eventful.prototype = {
   constructor: Eventful,
 
   /**
-   * 单次触发绑定,trigger后销毁
+   * The handler can only be triggered once, then removed.
    *
-   * @param {string} event 事件名
-   * @param {Function} handler 响应函数
+   * @param {string} event The event name.
+   * @param {string|Object} [query] Condition used on event filter.
+   * @param {Function} handler The event handler.
    * @param {Object} context
    */
-  one: function (event, handler, context) {
+  one: function (event, query, handler, context) {
     var _h = this._$handlers;
 
+    if (typeof query === 'function') {
+      context = handler;
+      handler = query;
+      query = null;
+    }
+
     if (!handler || !event) {
       return this;
     }
 
+    query = normalizeQuery(this, query);
+
     if (!_h[event]) {
       _h[event] = [];
     }
@@ -45,6 +68,7 @@ Eventful.prototype = {
     _h[event].push({
       h: handler,
       one: true,
+      query: query,
       ctx: context || this
     });
 
@@ -52,18 +76,28 @@ Eventful.prototype = {
   },
 
   /**
-   * 绑定事件
-   * @param {string} event 事件名
-   * @param {Function} handler 事件处理函数
+   * Bind a handler.
+   *
+   * @param {string} event The event name.
+   * @param {string|Object} [query] Condition used on event filter.
+   * @param {Function} handler The event handler.
    * @param {Object} [context]
    */
-  on: function (event, handler, context) {
+  on: function (event, query, handler, context) {
     var _h = this._$handlers;
 
+    if (typeof query === 'function') {
+      context = handler;
+      handler = query;
+      query = null;
+    }
+
     if (!handler || !event) {
       return this;
     }
 
+    query = normalizeQuery(this, query);
+
     if (!_h[event]) {
       _h[event] = [];
     }
@@ -77,6 +111,7 @@ Eventful.prototype = {
     _h[event].push({
       h: handler,
       one: false,
+      query: query,
       ctx: context || this
     });
 
@@ -84,7 +119,8 @@ Eventful.prototype = {
   },
 
   /**
-   * 是否绑定了事件
+   * Whether any handler has bound.
+   *
    * @param  {string}  event
    * @return {boolean}
    */
@@ -94,9 +130,10 @@ Eventful.prototype = {
   },
 
   /**
-   * 解绑事件
-   * @param {string} event 事件名
-   * @param {Function} [handler] 事件处理函数
+   * Unbind a event.
+   *
+   * @param {string} event The event name.
+   * @param {Function} [handler] The event handler.
    */
   off: function (event, handler) {
     var _h = this._$handlers;
@@ -111,7 +148,7 @@ Eventful.prototype = {
         var newList = [];
 
         for (var i = 0, l = _h[event].length; i < l; i++) {
-          if (_h[event][i]['h'] != handler) {
+          if (_h[event][i].h !== handler) {
             newList.push(_h[event][i]);
           }
         }
@@ -130,12 +167,15 @@ Eventful.prototype = {
   },
 
   /**
-   * 事件分发
+   * Dispatch a event.
    *
-   * @param {string} type 事件类型
+   * @param {string} type The event name.
    */
   trigger: function (type) {
-    if (this._$handlers[type]) {
+    var _h = this._$handlers[type];
+    var eventProcessor = this._$eventProcessor;
+
+    if (_h) {
       var args = arguments;
       var argLen = args.length;
 
@@ -143,35 +183,37 @@ Eventful.prototype = {
         args = arrySlice.call(args, 1);
       }
 
-      var _h = this._$handlers[type];
       var len = _h.length;
 
       for (var i = 0; i < len;) {
-        // Optimize advise from backbone
+        var hItem = _h[i];
+
+        if (eventProcessor && eventProcessor.filter && hItem.query != null && !eventProcessor.filter(type, hItem.query)) {
+          i++;
+          continue;
+        } // Optimize advise from backbone
+
+
         switch (argLen) {
           case 1:
-            _h[i]['h'].call(_h[i]['ctx']);
-
+            hItem.h.call(hItem.ctx);
             break;
 
           case 2:
-            _h[i]['h'].call(_h[i]['ctx'], args[1]);
-
+            hItem.h.call(hItem.ctx, args[1]);
             break;
 
           case 3:
-            _h[i]['h'].call(_h[i]['ctx'], args[1], args[2]);
-
+            hItem.h.call(hItem.ctx, args[1], args[2]);
             break;
 
           default:
             // have more than 2 given arguments
-            _h[i]['h'].apply(_h[i]['ctx'], args);
-
+            hItem.h.apply(hItem.ctx, args);
             break;
         }
 
-        if (_h[i]['one']) {
+        if (hItem.one) {
           _h.splice(i, 1);
 
           len--;
@@ -181,15 +223,20 @@ Eventful.prototype = {
       }
     }
 
+    eventProcessor && eventProcessor.afterTrigger && eventProcessor.afterTrigger(type);
     return this;
   },
 
   /**
-   * 带有context的事件分发, 最后一个参数是事件回调的context
-   * @param {string} type 事件类型
+   * Dispatch a event with context, which is specified at the last parameter.
+   *
+   * @param {string} type The event name.
    */
   triggerWithContext: function (type) {
-    if (this._$handlers[type]) {
+    var _h = this._$handlers[type];
+    var eventProcessor = this._$eventProcessor;
+
+    if (_h) {
       var args = arguments;
       var argLen = args.length;
 
@@ -198,35 +245,37 @@ Eventful.prototype = {
       }
 
       var ctx = args[args.length - 1];
-      var _h = this._$handlers[type];
       var len = _h.length;
 
       for (var i = 0; i < len;) {
-        // Optimize advise from backbone
+        var hItem = _h[i];
+
+        if (eventProcessor && eventProcessor.filter && hItem.query != null && !eventProcessor.filter(type, hItem.query)) {
+          i++;
+          continue;
+        } // Optimize advise from backbone
+
+
         switch (argLen) {
           case 1:
-            _h[i]['h'].call(ctx);
-
+            hItem.h.call(ctx);
             break;
 
           case 2:
-            _h[i]['h'].call(ctx, args[1]);
-
+            hItem.h.call(ctx, args[1]);
             break;
 
           case 3:
-            _h[i]['h'].call(ctx, args[1], args[2]);
-
+            hItem.h.call(ctx, args[1], args[2]);
             break;
 
           default:
             // have more than 2 given arguments
-            _h[i]['h'].apply(ctx, args);
-
+            hItem.h.apply(ctx, args);
             break;
         }
 
-        if (_h[i]['one']) {
+        if (hItem.one) {
           _h.splice(i, 1);
 
           len--;
@@ -236,9 +285,22 @@ Eventful.prototype = {
       }
     }
 
+    eventProcessor && eventProcessor.afterTrigger && eventProcessor.afterTrigger(type);
     return this;
   }
-}; // 对象可以通过 onxxxx 绑定事件
+};
+
+function normalizeQuery(host, query) {
+  var eventProcessor = host._$eventProcessor;
+
+  if (query != null && eventProcessor && eventProcessor.normalizeQuery) {
+    query = eventProcessor.normalizeQuery(query);
+  }
+
+  return query;
+} // ----------------------
+// The events in zrender
+// ----------------------
 
 /**
  * @event module:zrender/mixin/Eventful#onclick
@@ -324,4 +386,5 @@ Eventful.prototype = {
  * @default null
  */
 
+
 export default Eventful;
\ No newline at end of file
diff --git a/builder/src/zrender/mixin/Transformable.js b/builder/src/zrender/mixin/Transformable.js
index 98e31c2..9cc865a 100644
--- a/builder/src/zrender/mixin/Transformable.js
+++ b/builder/src/zrender/mixin/Transformable.js
@@ -67,6 +67,8 @@ transformableProto.needLocalTransform = function () {
   return isNotAroundZero(this.rotation) || isNotAroundZero(this.position[0]) || isNotAroundZero(this.position[1]) || isNotAroundZero(this.scale[0] - 1) || isNotAroundZero(this.scale[1] - 1);
 };
 
+var scaleTmp = [];
+
 transformableProto.updateTransform = function () {
   var parent = this.parent;
   var parentHasTransform = parent && parent.transform;
@@ -97,6 +99,20 @@ transformableProto.updateTransform = function () {
 
 
   this.transform = m;
+  var globalScaleRatio = this.globalScaleRatio;
+
+  if (globalScaleRatio != null && globalScaleRatio !== 1) {
+    this.getGlobalScale(scaleTmp);
+    var relX = scaleTmp[0] < 0 ? -1 : 1;
+    var relY = scaleTmp[1] < 0 ? -1 : 1;
+    var sx = ((scaleTmp[0] - relX) * globalScaleRatio + relX) / scaleTmp[0] || 0;
+    var sy = ((scaleTmp[1] - relY) * globalScaleRatio + relY) / scaleTmp[1] || 0;
+    m[0] *= sx;
+    m[1] *= sx;
+    m[2] *= sy;
+    m[3] *= sy;
+  }
+
   this.invTransform = this.invTransform || matrix.create();
   matrix.invert(this.invTransform, m);
 };
@@ -127,24 +143,14 @@ transformableProto.restoreTransform = function (ctx) {
 };
 
 var tmpTransform = [];
-/**
- * 分解`transform`矩阵到`position`, `rotation`, `scale`
- */
+var originTransform = matrix.create();
 
-transformableProto.decomposeTransform = function () {
-  if (!this.transform) {
+transformableProto.setLocalTransform = function (m) {
+  if (!m) {
+    // TODO return or set identity?
     return;
   }
 
-  var parent = this.parent;
-  var m = this.transform;
-
-  if (parent && parent.transform) {
-    // Get local transform and decompose them to position, scale, rotation
-    matrix.mul(tmpTransform, parent.invTransform, m);
-    m = tmpTransform;
-  }
-
   var sx = m[0] * m[0] + m[1] * m[1];
   var sy = m[2] * m[2] + m[3] * m[3];
   var position = this.position;
@@ -173,30 +179,65 @@ transformableProto.decomposeTransform = function () {
   this.rotation = Math.atan2(-m[1] / sy, m[0] / sx);
 };
 /**
+ * 分解`transform`矩阵到`position`, `rotation`, `scale`
+ */
+
+
+transformableProto.decomposeTransform = function () {
+  if (!this.transform) {
+    return;
+  }
+
+  var parent = this.parent;
+  var m = this.transform;
+
+  if (parent && parent.transform) {
+    // Get local transform and decompose them to position, scale, rotation
+    matrix.mul(tmpTransform, parent.invTransform, m);
+    m = tmpTransform;
+  }
+
+  var origin = this.origin;
+
+  if (origin && (origin[0] || origin[1])) {
+    originTransform[4] = origin[0];
+    originTransform[5] = origin[1];
+    matrix.mul(tmpTransform, m, originTransform);
+    tmpTransform[4] -= origin[0];
+    tmpTransform[5] -= origin[1];
+    m = tmpTransform;
+  }
+
+  this.setLocalTransform(m);
+};
+/**
  * Get global scale
  * @return {Array.<number>}
  */
 
 
-transformableProto.getGlobalScale = function () {
+transformableProto.getGlobalScale = function (out) {
   var m = this.transform;
+  out = out || [];
 
   if (!m) {
-    return [1, 1];
+    out[0] = 1;
+    out[1] = 1;
+    return out;
   }
 
-  var sx = Math.sqrt(m[0] * m[0] + m[1] * m[1]);
-  var sy = Math.sqrt(m[2] * m[2] + m[3] * m[3]);
+  out[0] = Math.sqrt(m[0] * m[0] + m[1] * m[1]);
+  out[1] = Math.sqrt(m[2] * m[2] + m[3] * m[3]);
 
   if (m[0] < 0) {
-    sx = -sx;
+    out[0] = -out[0];
   }
 
   if (m[3] < 0) {
-    sy = -sy;
+    out[1] = -out[1];
   }
 
-  return [sx, sy];
+  return out;
 };
 /**
  * 变换坐标位置到 shape 的局部坐标空间
diff --git a/builder/src/zrender/svg/graphic.js b/builder/src/zrender/svg/graphic.js
index 85a5c7f..af304e2 100644
--- a/builder/src/zrender/svg/graphic.js
+++ b/builder/src/zrender/svg/graphic.js
@@ -58,7 +58,7 @@ function attrXLink(el, key, val) {
   el.setAttributeNS('http://www.w3.org/1999/xlink', key, val);
 }
 
-function bindStyle(svgEl, style, isText) {
+function bindStyle(svgEl, style, isText, el) {
   if (pathHasFill(style, isText)) {
     var fill = isText ? style.textFill : style.fill;
     fill = fill === 'transparent' ? NONE : fill;
@@ -83,7 +83,7 @@ function bindStyle(svgEl, style, isText) {
     }
 
     attr(svgEl, 'fill', fill);
-    attr(svgEl, 'fill-opacity', style.opacity);
+    attr(svgEl, 'fill-opacity', style.fillOpacity != null ? style.fillOpacity * style.opacity : style.opacity);
   } else {
     attr(svgEl, 'fill', NONE);
   }
@@ -93,11 +93,11 @@ function bindStyle(svgEl, style, isText) {
     stroke = stroke === 'transparent' ? NONE : stroke;
     attr(svgEl, 'stroke', stroke);
     var strokeWidth = isText ? style.textStrokeWidth : style.lineWidth;
-    var strokeScale = !isText && style.strokeNoScale ? style.host.getLineScale() : 1;
+    var strokeScale = !isText && style.strokeNoScale ? el.getLineScale() : 1;
     attr(svgEl, 'stroke-width', strokeWidth / strokeScale); // stroke then fill for text; fill then stroke for others
 
     attr(svgEl, 'paint-order', isText ? 'stroke' : 'fill');
-    attr(svgEl, 'stroke-opacity', style.opacity);
+    attr(svgEl, 'stroke-opacity', style.strokeOpacity != null ? style.strokeOpacity : style.opacity);
     var lineDash = style.lineDash;
 
     if (lineDash) {
@@ -257,7 +257,7 @@ svgPath.brush = function (el) {
     }
   }
 
-  bindStyle(svgEl, style);
+  bindStyle(svgEl, style, false, el);
   setTransform(svgEl, el.transform);
 
   if (style.text != null) {
@@ -376,7 +376,7 @@ var svgTextDrawRectText = function (el, rect, textRect) {
 
   attr(textSvgEl, 'x', x);
   attr(textSvgEl, 'y', y);
-  bindStyle(textSvgEl, style, true);
+  bindStyle(textSvgEl, style, true, el);
 
   if (el instanceof Text || el.style.transformText) {
     // Transform text with element
@@ -390,6 +390,7 @@ var svgTextDrawRectText = function (el, rect, textRect) {
       var pos = el.transformCoordToGlobal(rect.x, rect.y);
       rect.x = pos[0];
       rect.y = pos[1];
+      el.transform = matrix.identity(matrix.create());
     } // Text rotation, but no element transform
 
 
@@ -406,7 +407,9 @@ var svgTextDrawRectText = function (el, rect, textRect) {
     var rotate = -style.textRotation || 0;
     var transform = matrix.create(); // Apply textRotate to element matrix
 
-    matrix.rotate(transform, el.transform, rotate);
+    matrix.rotate(transform, transform, rotate);
+    var pos = [el.transform[4], el.transform[5]];
+    matrix.translate(transform, transform, pos);
     setTransform(textSvgEl, transform);
   }
 
@@ -427,7 +430,7 @@ var svgTextDrawRectText = function (el, rect, textRect) {
 
   var dy = 0;
 
-  if (verticalAlign === 'baseline') {
+  if (verticalAlign === 'after-edge') {
     dy = -textRect.height + lineHeight;
     textPadding && (dy -= textPadding[2]);
   } else if (verticalAlign === 'middle') {
@@ -487,7 +490,7 @@ function getVerticalAlignForSvg(verticalAlign) {
   if (verticalAlign === 'middle') {
     return 'middle';
   } else if (verticalAlign === 'bottom') {
-    return 'baseline';
+    return 'after-edge';
   } else {
     return 'hanging';
   }
diff --git a/builder/src/zrender/tool/parseSVG.js b/builder/src/zrender/tool/parseSVG.js
new file mode 100644
index 0000000..3dd9b85
--- /dev/null
+++ b/builder/src/zrender/tool/parseSVG.js
@@ -0,0 +1,665 @@
+import Group from '../container/Group';
+import ZImage from '../graphic/Image';
+import Text from '../graphic/Text';
+import Circle from '../graphic/shape/Circle';
+import Rect from '../graphic/shape/Rect';
+import Ellipse from '../graphic/shape/Ellipse';
+import Line from '../graphic/shape/Line';
+import Path from '../graphic/Path';
+import Polygon from '../graphic/shape/Polygon';
+import Polyline from '../graphic/shape/Polyline';
+import LinearGradient from '../graphic/LinearGradient'; // import RadialGradient from '../graphic/RadialGradient';
+// import Pattern from '../graphic/Pattern';
+
+import Style from '../graphic/Style'; // import * as vector from '../core/vector';
+
+import * as matrix from '../core/matrix';
+import { createFromString } from './path';
+import { isString, extend, defaults, trim, each } from '../core/util'; // Most of the values can be separated by comma and/or white space.
+
+var DILIMITER_REG = /[\s,]+/;
+/**
+ * For big svg string, this method might be time consuming.
+ *
+ * @param {string} svg xml string
+ * @return {Object} xml root.
+ */
+
+export function parseXML(svg) {
+  if (isString(svg)) {
+    var parser = new DOMParser();
+    svg = parser.parseFromString(svg, 'text/xml');
+  } // Document node. If using $.get, doc node may be input.
+
+
+  if (svg.nodeType === 9) {
+    svg = svg.firstChild;
+  } // nodeName of <!DOCTYPE svg> is also 'svg'.
+
+
+  while (svg.nodeName.toLowerCase() !== 'svg' || svg.nodeType !== 1) {
+    svg = svg.nextSibling;
+  }
+
+  return svg;
+}
+
+function SVGParser() {
+  this._defs = {};
+  this._root = null;
+  this._isDefine = false;
+  this._isText = false;
+}
+
+SVGParser.prototype.parse = function (xml, opt) {
+  opt = opt || {};
+  var svg = parseXML(xml);
+
+  if (!svg) {
+    throw new Error('Illegal svg');
+  }
+
+  var root = new Group();
+  this._root = root; // parse view port
+
+  var viewBox = svg.getAttribute('viewBox') || ''; // If width/height not specified, means "100%" of `opt.width/height`.
+  // TODO: Other percent value not supported yet.
+
+  var width = parseFloat(svg.getAttribute('width') || opt.width);
+  var height = parseFloat(svg.getAttribute('height') || opt.height); // If width/height not specified, set as null for output.
+
+  isNaN(width) && (width = null);
+  isNaN(height) && (height = null); // Apply inline style on svg element.
+
+  parseAttributes(svg, root, null, true);
+  var child = svg.firstChild;
+
+  while (child) {
+    this._parseNode(child, root);
+
+    child = child.nextSibling;
+  }
+
+  var viewBoxRect;
+  var viewBoxTransform;
+
+  if (viewBox) {
+    var viewBoxArr = trim(viewBox).split(DILIMITER_REG); // Some invalid case like viewBox: 'none'.
+
+    if (viewBoxArr.length >= 4) {
+      viewBoxRect = {
+        x: parseFloat(viewBoxArr[0] || 0),
+        y: parseFloat(viewBoxArr[1] || 0),
+        width: parseFloat(viewBoxArr[2]),
+        height: parseFloat(viewBoxArr[3])
+      };
+    }
+  }
+
+  if (viewBoxRect && width != null && height != null) {
+    viewBoxTransform = makeViewBoxTransform(viewBoxRect, width, height);
+
+    if (!opt.ignoreViewBox) {
+      // If set transform on the output group, it probably bring trouble when
+      // some users only intend to show the clipped content inside the viewBox,
+      // but not intend to transform the output group. So we keep the output
+      // group no transform. If the user intend to use the viewBox as a
+      // camera, just set `opt.ignoreViewBox` as `true` and set transfrom
+      // manually according to the viewBox info in the output of this method.
+      var elRoot = root;
+      root = new Group();
+      root.add(elRoot);
+      elRoot.scale = viewBoxTransform.scale.slice();
+      elRoot.position = viewBoxTransform.position.slice();
+    }
+  } // Some shapes might be overflow the viewport, which should be
+  // clipped despite whether the viewBox is used, as the SVG does.
+
+
+  if (!opt.ignoreRootClip && width != null && height != null) {
+    root.setClipPath(new Rect({
+      shape: {
+        x: 0,
+        y: 0,
+        width: width,
+        height: height
+      }
+    }));
+  } // Set width/height on group just for output the viewport size.
+
+
+  return {
+    root: root,
+    width: width,
+    height: height,
+    viewBoxRect: viewBoxRect,
+    viewBoxTransform: viewBoxTransform
+  };
+};
+
+SVGParser.prototype._parseNode = function (xmlNode, parentGroup) {
+  var nodeName = xmlNode.nodeName.toLowerCase(); // TODO
+  // support <style>...</style> in svg, where nodeName is 'style',
+  // CSS classes is defined globally wherever the style tags are declared.
+
+  if (nodeName === 'defs') {
+    // define flag
+    this._isDefine = true;
+  } else if (nodeName === 'text') {
+    this._isText = true;
+  }
+
+  var el;
+
+  if (this._isDefine) {
+    var parser = defineParsers[nodeName];
+
+    if (parser) {
+      var def = parser.call(this, xmlNode);
+      var id = xmlNode.getAttribute('id');
+
+      if (id) {
+        this._defs[id] = def;
+      }
+    }
+  } else {
+    var parser = nodeParsers[nodeName];
+
+    if (parser) {
+      el = parser.call(this, xmlNode, parentGroup);
+      parentGroup.add(el);
+    }
+  }
+
+  var child = xmlNode.firstChild;
+
+  while (child) {
+    if (child.nodeType === 1) {
+      this._parseNode(child, el);
+    } // Is text
+
+
+    if (child.nodeType === 3 && this._isText) {
+      this._parseText(child, el);
+    }
+
+    child = child.nextSibling;
+  } // Quit define
+
+
+  if (nodeName === 'defs') {
+    this._isDefine = false;
+  } else if (nodeName === 'text') {
+    this._isText = false;
+  }
+};
+
+SVGParser.prototype._parseText = function (xmlNode, parentGroup) {
+  if (xmlNode.nodeType === 1) {
+    var dx = xmlNode.getAttribute('dx') || 0;
+    var dy = xmlNode.getAttribute('dy') || 0;
+    this._textX += parseFloat(dx);
+    this._textY += parseFloat(dy);
+  }
+
+  var text = new Text({
+    style: {
+      text: xmlNode.textContent,
+      transformText: true
+    },
+    position: [this._textX || 0, this._textY || 0]
+  });
+  inheritStyle(parentGroup, text);
+  parseAttributes(xmlNode, text, this._defs);
+  var fontSize = text.style.fontSize;
+
+  if (fontSize && fontSize < 9) {
+    // PENDING
+    text.style.fontSize = 9;
+    text.scale = text.scale || [1, 1];
+    text.scale[0] *= fontSize / 9;
+    text.scale[1] *= fontSize / 9;
+  }
+
+  var rect = text.getBoundingRect();
+  this._textX += rect.width;
+  parentGroup.add(text);
+  return text;
+};
+
+var nodeParsers = {
+  'g': function (xmlNode, parentGroup) {
+    var g = new Group();
+    inheritStyle(parentGroup, g);
+    parseAttributes(xmlNode, g, this._defs);
+    return g;
+  },
+  'rect': function (xmlNode, parentGroup) {
+    var rect = new Rect();
+    inheritStyle(parentGroup, rect);
+    parseAttributes(xmlNode, rect, this._defs);
+    rect.setShape({
+      x: parseFloat(xmlNode.getAttribute('x') || 0),
+      y: parseFloat(xmlNode.getAttribute('y') || 0),
+      width: parseFloat(xmlNode.getAttribute('width') || 0),
+      height: parseFloat(xmlNode.getAttribute('height') || 0)
+    }); // console.log(xmlNode.getAttribute('transform'));
+    // console.log(rect.transform);
+
+    return rect;
+  },
+  'circle': function (xmlNode, parentGroup) {
+    var circle = new Circle();
+    inheritStyle(parentGroup, circle);
+    parseAttributes(xmlNode, circle, this._defs);
+    circle.setShape({
+      cx: parseFloat(xmlNode.getAttribute('cx') || 0),
+      cy: parseFloat(xmlNode.getAttribute('cy') || 0),
+      r: parseFloat(xmlNode.getAttribute('r') || 0)
+    });
+    return circle;
+  },
+  'line': function (xmlNode, parentGroup) {
+    var line = new Line();
+    inheritStyle(parentGroup, line);
+    parseAttributes(xmlNode, line, this._defs);
+    line.setShape({
+      x1: parseFloat(xmlNode.getAttribute('x1') || 0),
+      y1: parseFloat(xmlNode.getAttribute('y1') || 0),
+      x2: parseFloat(xmlNode.getAttribute('x2') || 0),
+      y2: parseFloat(xmlNode.getAttribute('y2') || 0)
+    });
+    return line;
+  },
+  'ellipse': function (xmlNode, parentGroup) {
+    var ellipse = new Ellipse();
+    inheritStyle(parentGroup, ellipse);
+    parseAttributes(xmlNode, ellipse, this._defs);
+    ellipse.setShape({
+      cx: parseFloat(xmlNode.getAttribute('cx') || 0),
+      cy: parseFloat(xmlNode.getAttribute('cy') || 0),
+      rx: parseFloat(xmlNode.getAttribute('rx') || 0),
+      ry: parseFloat(xmlNode.getAttribute('ry') || 0)
+    });
+    return ellipse;
+  },
+  'polygon': function (xmlNode, parentGroup) {
+    var points = xmlNode.getAttribute('points');
+
+    if (points) {
+      points = parsePoints(points);
+    }
+
+    var polygon = new Polygon({
+      shape: {
+        points: points || []
+      }
+    });
+    inheritStyle(parentGroup, polygon);
+    parseAttributes(xmlNode, polygon, this._defs);
+    return polygon;
+  },
+  'polyline': function (xmlNode, parentGroup) {
+    var path = new Path();
+    inheritStyle(parentGroup, path);
+    parseAttributes(xmlNode, path, this._defs);
+    var points = xmlNode.getAttribute('points');
+
+    if (points) {
+      points = parsePoints(points);
+    }
+
+    var polyline = new Polyline({
+      shape: {
+        points: points || []
+      }
+    });
+    return polyline;
+  },
+  'image': function (xmlNode, parentGroup) {
+    var img = new ZImage();
+    inheritStyle(parentGroup, img);
+    parseAttributes(xmlNode, img, this._defs);
+    img.setStyle({
+      image: xmlNode.getAttribute('xlink:href'),
+      x: xmlNode.getAttribute('x'),
+      y: xmlNode.getAttribute('y'),
+      width: xmlNode.getAttribute('width'),
+      height: xmlNode.getAttribute('height')
+    });
+    return img;
+  },
+  'text': function (xmlNode, parentGroup) {
+    var x = xmlNode.getAttribute('x') || 0;
+    var y = xmlNode.getAttribute('y') || 0;
+    var dx = xmlNode.getAttribute('dx') || 0;
+    var dy = xmlNode.getAttribute('dy') || 0;
+    this._textX = parseFloat(x) + parseFloat(dx);
+    this._textY = parseFloat(y) + parseFloat(dy);
+    var g = new Group();
+    inheritStyle(parentGroup, g);
+    parseAttributes(xmlNode, g, this._defs);
+    return g;
+  },
+  'tspan': function (xmlNode, parentGroup) {
+    var x = xmlNode.getAttribute('x');
+    var y = xmlNode.getAttribute('y');
+
+    if (x != null) {
+      // new offset x
+      this._textX = parseFloat(x);
+    }
+
+    if (y != null) {
+      // new offset y
+      this._textY = parseFloat(y);
+    }
+
+    var dx = xmlNode.getAttribute('dx') || 0;
+    var dy = xmlNode.getAttribute('dy') || 0;
+    var g = new Group();
+    inheritStyle(parentGroup, g);
+    parseAttributes(xmlNode, g, this._defs);
+    this._textX += dx;
+    this._textY += dy;
+    return g;
+  },
+  'path': function (xmlNode, parentGroup) {
+    // TODO svg fill rule
+    // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill-rule
+    // path.style.globalCompositeOperation = 'xor';
+    var d = xmlNode.getAttribute('d') || ''; // Performance sensitive.
+
+    var path = createFromString(d);
+    inheritStyle(parentGroup, path);
+    parseAttributes(xmlNode, path, this._defs);
+    return path;
+  }
+};
+var defineParsers = {
+  'lineargradient': function (xmlNode) {
+    var x1 = parseInt(xmlNode.getAttribute('x1') || 0, 10);
+    var y1 = parseInt(xmlNode.getAttribute('y1') || 0, 10);
+    var x2 = parseInt(xmlNode.getAttribute('x2') || 10, 10);
+    var y2 = parseInt(xmlNode.getAttribute('y2') || 0, 10);
+    var gradient = new LinearGradient(x1, y1, x2, y2);
+
+    _parseGradientColorStops(xmlNode, gradient);
+
+    return gradient;
+  },
+  'radialgradient': function (xmlNode) {}
+};
+
+function _parseGradientColorStops(xmlNode, gradient) {
+  var stop = xmlNode.firstChild;
+
+  while (stop) {
+    if (stop.nodeType === 1) {
+      var offset = stop.getAttribute('offset');
+
+      if (offset.indexOf('%') > 0) {
+        // percentage
+        offset = parseInt(offset, 10) / 100;
+      } else if (offset) {
+        // number from 0 to 1
+        offset = parseFloat(offset);
+      } else {
+        offset = 0;
+      }
+
+      var stopColor = stop.getAttribute('stop-color') || '#000000';
+      gradient.addColorStop(offset, stopColor);
+    }
+
+    stop = stop.nextSibling;
+  }
+}
+
+function inheritStyle(parent, child) {
+  if (parent && parent.__inheritedStyle) {
+    if (!child.__inheritedStyle) {
+      child.__inheritedStyle = {};
+    }
+
+    defaults(child.__inheritedStyle, parent.__inheritedStyle);
+  }
+}
+
+function parsePoints(pointsString) {
+  var list = trim(pointsString).split(DILIMITER_REG);
+  var points = [];
+
+  for (var i = 0; i < list.length; i += 2) {
+    var x = parseFloat(list[i]);
+    var y = parseFloat(list[i + 1]);
+    points.push([x, y]);
+  }
+
+  return points;
+}
+
+var attributesMap = {
+  'fill': 'fill',
+  'stroke': 'stroke',
+  'stroke-width': 'lineWidth',
+  'opacity': 'opacity',
+  'fill-opacity': 'fillOpacity',
+  'stroke-opacity': 'strokeOpacity',
+  'stroke-dasharray': 'lineDash',
+  'stroke-dashoffset': 'lineDashOffset',
+  'stroke-linecap': 'lineCap',
+  'stroke-linejoin': 'lineJoin',
+  'stroke-miterlimit': 'miterLimit',
+  'font-family': 'fontFamily',
+  'font-size': 'fontSize',
+  'font-style': 'fontStyle',
+  'font-weight': 'fontWeight',
+  'text-align': 'textAlign',
+  'alignment-baseline': 'textBaseline'
+};
+
+function parseAttributes(xmlNode, el, defs, onlyInlineStyle) {
+  var zrStyle = el.__inheritedStyle || {};
+  var isTextEl = el.type === 'text'; // TODO Shadow
+
+  if (xmlNode.nodeType === 1) {
+    parseTransformAttribute(xmlNode, el);
+    extend(zrStyle, parseStyleAttribute(xmlNode));
+
+    if (!onlyInlineStyle) {
+      for (var svgAttrName in attributesMap) {
+        if (attributesMap.hasOwnProperty(svgAttrName)) {
+          var attrValue = xmlNode.getAttribute(svgAttrName);
+
+          if (attrValue != null) {
+            zrStyle[attributesMap[svgAttrName]] = attrValue;
+          }
+        }
+      }
+    }
+  }
+
+  var elFillProp = isTextEl ? 'textFill' : 'fill';
+  var elStrokeProp = isTextEl ? 'textStroke' : 'stroke';
+  el.style = el.style || new Style();
+  var elStyle = el.style;
+  zrStyle.fill != null && elStyle.set(elFillProp, getPaint(zrStyle.fill, defs));
+  zrStyle.stroke != null && elStyle.set(elStrokeProp, getPaint(zrStyle.stroke, defs));
+  each(['lineWidth', 'opacity', 'fillOpacity', 'strokeOpacity', 'miterLimit', 'fontSize'], function (propName) {
+    var elPropName = propName === 'lineWidth' && isTextEl ? 'textStrokeWidth' : propName;
+    zrStyle[propName] != null && elStyle.set(elPropName, parseFloat(zrStyle[propName]));
+  });
+
+  if (!zrStyle.textBaseline || zrStyle.textBaseline === 'auto') {
+    zrStyle.textBaseline = 'alphabetic';
+  }
+
+  if (zrStyle.textBaseline === 'alphabetic') {
+    zrStyle.textBaseline = 'bottom';
+  }
+
+  if (zrStyle.textAlign === 'start') {
+    zrStyle.textAlign = 'left';
+  }
+
+  if (zrStyle.textAlign === 'end') {
+    zrStyle.textAlign = 'right';
+  }
+
+  each(['lineDashOffset', 'lineCap', 'lineJoin', 'fontWeight', 'fontFamily', 'fontStyle', 'textAlign', 'textBaseline'], function (propName) {
+    zrStyle[propName] != null && elStyle.set(propName, zrStyle[propName]);
+  });
+
+  if (zrStyle.lineDash) {
+    el.style.lineDash = trim(zrStyle.lineDash).split(DILIMITER_REG);
+  }
+
+  if (elStyle[elStrokeProp] && elStyle[elStrokeProp] !== 'none') {
+    // enable stroke
+    el[elStrokeProp] = true;
+  }
+
+  el.__inheritedStyle = zrStyle;
+}
+
+var urlRegex = /url\(\s*#(.*?)\)/;
+
+function getPaint(str, defs) {
+  // if (str === 'none') {
+  //     return;
+  // }
+  var urlMatch = defs && str && str.match(urlRegex);
+
+  if (urlMatch) {
+    var url = trim(urlMatch[1]);
+    var def = defs[url];
+    return def;
+  }
+
+  return str;
+}
+
+var transformRegex = /(translate|scale|rotate|skewX|skewY|matrix)\(([\-\s0-9\.e,]*)\)/g;
+
+function parseTransformAttribute(xmlNode, node) {
+  var transform = xmlNode.getAttribute('transform');
+
+  if (transform) {
+    transform = transform.replace(/,/g, ' ');
+    var m = null;
+    var transformOps = [];
+    transform.replace(transformRegex, function (str, type, value) {
+      transformOps.push(type, value);
+    });
+
+    for (var i = transformOps.length - 1; i > 0; i -= 2) {
+      var value = transformOps[i];
+      var type = transformOps[i - 1];
+      m = m || matrix.create();
+
+      switch (type) {
+        case 'translate':
+          value = trim(value).split(DILIMITER_REG);
+          matrix.translate(m, m, [parseFloat(value[0]), parseFloat(value[1] || 0)]);
+          break;
+
+        case 'scale':
+          value = trim(value).split(DILIMITER_REG);
+          matrix.scale(m, m, [parseFloat(value[0]), parseFloat(value[1] || value[0])]);
+          break;
+
+        case 'rotate':
+          value = trim(value).split(DILIMITER_REG);
+          matrix.rotate(m, m, parseFloat(value[0]));
+          break;
+
+        case 'skew':
+          value = trim(value).split(DILIMITER_REG);
+          console.warn('Skew transform is not supported yet');
+          break;
+
+        case 'matrix':
+          var value = trim(value).split(DILIMITER_REG);
+          m[0] = parseFloat(value[0]);
+          m[1] = parseFloat(value[1]);
+          m[2] = parseFloat(value[2]);
+          m[3] = parseFloat(value[3]);
+          m[4] = parseFloat(value[4]);
+          m[5] = parseFloat(value[5]);
+          break;
+      }
+    }
+  }
+
+  node.setLocalTransform(m);
+} // Value may contain space.
+
+
+var styleRegex = /([^\s:;]+)\s*:\s*([^:;]+)/g;
+
+function parseStyleAttribute(xmlNode) {
+  var style = xmlNode.getAttribute('style');
+  var result = {};
+
+  if (!style) {
+    return result;
+  }
+
+  var styleList = {};
+  styleRegex.lastIndex = 0;
+  var styleRegResult;
+
+  while ((styleRegResult = styleRegex.exec(style)) != null) {
+    styleList[styleRegResult[1]] = styleRegResult[2];
+  }
+
+  for (var svgAttrName in attributesMap) {
+    if (attributesMap.hasOwnProperty(svgAttrName) && styleList[svgAttrName] != null) {
+      result[attributesMap[svgAttrName]] = styleList[svgAttrName];
+    }
+  }
+
+  return result;
+}
+/**
+ * @param {Array.<number>} viewBoxRect
+ * @param {number} width
+ * @param {number} height
+ * @return {Object} {scale, position}
+ */
+
+
+export function makeViewBoxTransform(viewBoxRect, width, height) {
+  var scaleX = width / viewBoxRect.width;
+  var scaleY = height / viewBoxRect.height;
+  var scale = Math.min(scaleX, scaleY); // preserveAspectRatio 'xMidYMid'
+
+  var viewBoxScale = [scale, scale];
+  var viewBoxPosition = [-(viewBoxRect.x + viewBoxRect.width / 2) * scale + width / 2, -(viewBoxRect.y + viewBoxRect.height / 2) * scale + height / 2];
+  return {
+    scale: viewBoxScale,
+    position: viewBoxPosition
+  };
+}
+/**
+ * @param {string|XMLElement} xml
+ * @param {Object} [opt]
+ * @param {number} [opt.width] Default width if svg width not specified or is a percent value.
+ * @param {number} [opt.height] Default height if svg height not specified or is a percent value.
+ * @param {boolean} [opt.ignoreViewBox]
+ * @param {boolean} [opt.ignoreRootClip]
+ * @return {Object} result:
+ * {
+ *     root: Group, The root of the the result tree of zrender shapes,
+ *     width: number, the viewport width of the SVG,
+ *     height: number, the viewport height of the SVG,
+ *     viewBoxRect: {x, y, width, height}, the declared viewBox rect of the SVG, if exists,
+ *     viewBoxTransform: the {scale, position} calculated by viewBox and viewport, is exists.
+ * }
+ */
+
+export function parseSVG(xml, opt) {
+  var parser = new SVGParser();
+  return parser.parse(xml, opt);
+}
\ No newline at end of file
diff --git a/builder/src/zrender/tool/path.js b/builder/src/zrender/tool/path.js
index b2dd426..8ab4cf8 100644
--- a/builder/src/zrender/tool/path.js
+++ b/builder/src/zrender/tool/path.js
@@ -1,8 +1,11 @@
 import Path from '../graphic/Path';
 import PathProxy from '../core/PathProxy';
 import transformPath from './transformPath'; // command chars
+// var cc = [
+//     'm', 'M', 'l', 'L', 'v', 'V', 'h', 'H', 'z', 'Z',
+//     'c', 'C', 'q', 'Q', 't', 'T', 's', 'S', 'a', 'A'
+// ];
 
-var cc = ['m', 'M', 'l', 'L', 'v', 'V', 'h', 'H', 'z', 'Z', 'c', 'C', 'q', 'Q', 't', 'T', 's', 'S', 'a', 'A'];
 var mathSqrt = Math.sqrt;
 var mathSin = Math.sin;
 var mathCos = Math.cos;
@@ -60,48 +63,69 @@ function processArc(x1, y1, x2, y2, fa, fs, rx, ry, psiDeg, cmd, path) {
   path.addData(cmd, cx, cy, rx, ry, theta, dTheta, psi, fs);
 }
 
-function createPathProxyFromString(data) {
-  if (!data) {
-    return [];
-  } // command string
-
-
-  var cs = data.replace(/-/g, ' -').replace(/  /g, ' ').replace(/ /g, ',').replace(/,,/g, ',');
-  var n; // create pipes so that we can split the data
+var commandReg = /([mlvhzcqtsa])([^mlvhzcqtsa]*)/ig; // Consider case:
+// (1) delimiter can be comma or space, where continuous commas
+// or spaces should be seen as one comma.
+// (2) value can be like:
+// '2e-4', 'l.5.9' (ignore 0), 'M-10-10', 'l-2.43e-1,34.9983',
+// 'l-.5E1,54', '121-23-44-11' (no delimiter)
 
-  for (n = 0; n < cc.length; n++) {
-    cs = cs.replace(new RegExp(cc[n], 'g'), '|' + cc[n]);
-  } // create array
+var numberReg = /-?([0-9]*\.)?[0-9]+([eE]-?[0-9]+)?/g; // var valueSplitReg = /[\s,]+/;
 
+function createPathProxyFromString(data) {
+  if (!data) {
+    return new PathProxy();
+  } // var data = data.replace(/-/g, ' -')
+  //     .replace(/  /g, ' ')
+  //     .replace(/ /g, ',')
+  //     .replace(/,,/g, ',');
+  // var n;
+  // create pipes so that we can split the data
+  // for (n = 0; n < cc.length; n++) {
+  //     cs = cs.replace(new RegExp(cc[n], 'g'), '|' + cc[n]);
+  // }
+  // data = data.replace(/-/g, ',-');
+  // create array
+  // var arr = cs.split('|');
+  // init context point
 
-  var arr = cs.split('|'); // init context point
 
   var cpx = 0;
   var cpy = 0;
-  var path = new PathProxy();
-  var CMD = PathProxy.CMD;
+  var subpathX = cpx;
+  var subpathY = cpy;
   var prevCmd;
-
-  for (n = 1; n < arr.length; n++) {
-    var str = arr[n];
-    var c = str.charAt(0);
-    var off = 0;
-    var p = str.slice(1).replace(/e,-/g, 'e-').split(',');
-    var cmd;
-
-    if (p.length > 0 && p[0] === '') {
-      p.shift();
-    }
-
-    for (var i = 0; i < p.length; i++) {
+  var path = new PathProxy();
+  var CMD = PathProxy.CMD; // commandReg.lastIndex = 0;
+  // var cmdResult;
+  // while ((cmdResult = commandReg.exec(data)) != null) {
+  //     var cmdStr = cmdResult[1];
+  //     var cmdContent = cmdResult[2];
+
+  var cmdList = data.match(commandReg);
+
+  for (var l = 0; l < cmdList.length; l++) {
+    var cmdText = cmdList[l];
+    var cmdStr = cmdText.charAt(0);
+    var cmd; // String#split is faster a little bit than String#replace or RegExp#exec.
+    // var p = cmdContent.split(valueSplitReg);
+    // var pLen = 0;
+    // for (var i = 0; i < p.length; i++) {
+    //     // '' and other invalid str => NaN
+    //     var val = parseFloat(p[i]);
+    //     !isNaN(val) && (p[pLen++] = val);
+    // }
+
+    var p = cmdText.match(numberReg) || [];
+    var pLen = p.length;
+
+    for (var i = 0; i < pLen; i++) {
       p[i] = parseFloat(p[i]);
     }
 
-    while (off < p.length && !isNaN(p[off])) {
-      if (isNaN(p[0])) {
-        break;
-      }
+    var off = 0;
 
+    while (off < pLen) {
       var ctlPtx;
       var ctlPty;
       var rx;
@@ -112,7 +136,7 @@ function createPathProxyFromString(data) {
       var x1 = cpx;
       var y1 = cpy; // convert l, H, h, V, and v to L
 
-      switch (c) {
+      switch (cmdStr) {
         case 'l':
           cpx += p[off++];
           cpy += p[off++];
@@ -132,7 +156,9 @@ function createPathProxyFromString(data) {
           cpy += p[off++];
           cmd = CMD.M;
           path.addData(cmd, cpx, cpy);
-          c = 'l';
+          subpathX = cpx;
+          subpathY = cpy;
+          cmdStr = 'l';
           break;
 
         case 'M':
@@ -140,7 +166,9 @@ function createPathProxyFromString(data) {
           cpy = p[off++];
           cmd = CMD.M;
           path.addData(cmd, cpx, cpy);
-          c = 'L';
+          subpathX = cpx;
+          subpathY = cpy;
+          cmdStr = 'L';
           break;
 
         case 'h':
@@ -299,9 +327,12 @@ function createPathProxyFromString(data) {
       }
     }
 
-    if (c === 'z' || c === 'Z') {
+    if (cmdStr === 'z' || cmdStr === 'Z') {
       cmd = CMD.Z;
-      path.addData(cmd);
+      path.addData(cmd); // z may be in the middle of the path.
+
+      cpx = subpathX;
+      cpy = subpathY;
     }
 
     prevCmd = cmd;
diff --git a/builder/src/zrender/zrender.js b/builder/src/zrender/zrender.js
index 8a15840..35b75f2 100644
--- a/builder/src/zrender/zrender.js
+++ b/builder/src/zrender/zrender.js
@@ -25,7 +25,7 @@ var instances = {}; // ZRender实例map索引
  * @type {string}
  */
 
-export var version = '4.0.4';
+export var version = '4.0.5';
 /**
  * Initializing a zrender instance
  * @param {HTMLElement} dom
@@ -268,8 +268,9 @@ ZRender.prototype = {
    */
   addHover: function (el, style) {
     if (this.painter.addHover) {
-      this.painter.addHover(el, style);
+      var elMirror = this.painter.addHover(el, style);
       this.refreshHover();
+      return elMirror;
     }
   },
 
diff --git a/changelog.html b/changelog.html
index afacc5d..23e14cd 100644
--- a/changelog.html
+++ b/changelog.html
@@ -1,4 +1,4 @@
-<!DOCTYPE html><html lang="en-US"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1" user-scalable="no"><meta name="description" content="ECharts, a powerful, interactive charting and visualization library for browser"><link rel="shortcut icon" href="images/favicon.png"><link rel="stylesheet" type="text/css" href="vendors/bootstrap/css/bootstrap.min.css"><!-- HTML5 shim and Respond.js for IE [...]
+<!DOCTYPE html><html lang="en-US"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1" user-scalable="no"><meta name="description" content="ECharts, a powerful, interactive charting and visualization library for browser"><link rel="shortcut icon" href="images/favicon.png"><link rel="stylesheet" type="text/css" href="vendors/bootstrap/css/bootstrap.min.css"><!-- HTML5 shim and Respond.js for IE [...]
 </script><script type="text/javascript" src="./vendors/pace/pace.min.js"></script><script id="font-hack" type="text/javascript">if (/windows/i.test(navigator.userAgent)) {
     var el = document.createElement('style');
     el.innerHTML = ''
@@ -7,7 +7,46 @@
     document.head.insertBefore(el, document.getElementById('font-hack'));
 }
 </script><title>ECharts Changelog</title><script type="text/javascript" src="./vendors/jquery/jquery.min.js"></script></head><!--[if lte IE 8]><body class="lower-ie"><div id="lowie-main"><img src="./images/forie.png" alt="ie tip"></div></body><![endif]-->
-<!--[if (gt IE 8)|!(IE)]><body class="undefined"></body><![endif]--><div id="main"><nav class="navbar navbar-default navbar-fixed-top"><div class="container-fluid"><div class="navbar-header"><button type="button" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false" class="navbar-toggle collapsed"><span class="sr-only">Toggle navigation</span><span class="icon-bar"></span><span class="icon-bar"></span><span class="icon-bar"></span></button><a href="./in [...]
+<!--[if (gt IE 8)|!(IE)]><body class="undefined"></body><![endif]--><div id="main"><nav class="navbar navbar-default navbar-fixed-top"><div class="container-fluid"><div class="navbar-header"><button type="button" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false" class="navbar-toggle collapsed"><span class="sr-only">Toggle navigation</span><span class="icon-bar"></span><span class="icon-bar"></span><span class="icon-bar"></span></button><a href="./in [...]
+<div class="time">2018-09-11</div>
+
+<ul>
+<li><p><strong>[Feature] Support non-html tooltip</strong>, which enable show tooltip on non-html environment, for example, WeChat Mini App. Use <a href="https://ecomfe.github.io/echarts-doc/public/en/option.html#tooltip.renderMode">tooltip.renderMode</a> to enable this feature.</p>
+</li>
+<li><p><strong>[Feature] Support event filtering</strong>, which enable listen event triggered by specified component, series, data items. See <a href="https://ecomfe.github.io/echarts-doc/public/en/api.html#echartsInstance.on">chart.on</a> for details, where a new parameter <code>query</code> provided this feature. Support event listening for <a href="https://ecomfe.github.io/echarts-doc/public/en/option.html#series-custom">custom series</a> and <a href="https://ecomfe.github.io/echarts [...]
+</li>
+<li><p><strong>[Feature] Enable implementing scroll bar</strong> by dataZoom (similar behavior as the browser scroll bar), see <a href="https://ecomfe.github.io/echarts-doc/public/en/option.html#dataZoom-inside.moveOnMouseWheel">dataZoom-inside.moveOnMouseWheel</a>, and see the <a href="https://ecomfe.github.io/echarts-examples/public/editor.html?c=custom-gantt-flight">Gantt example</a>.</p>
+</li>
+<li><p>[Feature] Support focusing node adjacency in sankey diagram. See <a href="https://ecomfe.github.io/echarts-doc/public/en/option.html#series-sankey.focusNodeAdjacency">focusNodeAdjacency</a> and <a href="https://ecomfe.github.io/echarts-examples/public/editor.html?c=sankey-product">example</a>.</p>
+</li>
+<li><p>[Feature] Support vertical layout of sankey diagram, see <a href="https://ecomfe.github.io/echarts-doc/public/en/option.html#series-sankey.orient">series-sankey.orient</a> and <a href="https://ecomfe.github.io/echarts-examples/public/editor.html?c=sankey-vertical">Vertical sankey example</a>.</p>
+</li>
+<li><p>[Feature] Support roam for tree diagram. See <a href="https://ecomfe.github.io/echarts-doc/public/en/option.html#series-tree.roam">series-tree.roam</a>.</p>
+</li>
+<li><p>[Feature] Enable to set an axis as <code>-1</code> in <a href="https://ecomfe.github.io/echarts-doc/public/en/option.html#series-custom.encode">encode</a> for <a href="https://ecomfe.github.io/echarts-doc/public/en/option.html#series-custom">custom series</a>, which indicates that the axis and its corresponding dataZoom do not control any dimension of the series (either calculate axis extent by this series nor scale or filter this series). See <a href="https://ecomfe.github.io/ech [...]
+</li>
+<li><p>[Feature] Enable layout <code>&#39;cover&#39;</code> in <a href="https://ecomfe.github.io/echarts-doc/public/en/option.html#series-custom.renderItem.return_path.shape.layout">path shape</a> for <a href="https://ecomfe.github.io/echarts-doc/public/en/option.html#series-custom">custom series</a>, which brings convenience when using <a href="http://www.w3.org/TR/SVG/paths.html#PathData">SVG PathData</a> create shapes. See <a href="https://ecomfe.github.io/echarts-examples/public/edit [...]
+</li>
+<li><p>[Feature] Enhance <a href="https://ecomfe.github.io/echarts-doc/public/en/option.html#series-custom">custom series</a>: Added <a href="https://ecomfe.github.io/echarts-doc/public/en/option.html#series-custom.renderItem.arguments.params">params.actionType</a>, enables some optimize. And add<a href="https://ecomfe.github.io/echarts-doc/public/en/option.html#series-custom.renderItem.arguments.params">params.coordSys.zoom</a> for geo coordinate system. Add <a href="https://ecomfe.gith [...]
+</li>
+<li><p>[Feature] Support inner radius for <a href="https://ecomfe.github.io/echarts-doc/public/en/option.html#radar.radius">radar</a>. See <a href="https://github.com/apache/incubator-echarts/issues/8717">#8717</a>.</p>
+</li>
+<li><p>[Fix] Fix polar category axis interval bug. See <a href="https://github.com/apache/incubator-echarts/issues/8791">#8791</a>.</p>
+</li>
+<li><p>[Fix] Do not support set polar center on series. Center should be set on <a href="https://ecomfe.github.io/echarts-doc/public/en/option.html#polar.center">polar.center</a>.</p>
+</li>
+<li><p>[Fix] Update normal shadow style for <a href="https://ecomfe.github.io/echarts-doc/public/en/option.html#series-sunburst">sunburst</a>. See <a href="https://github.com/apache/incubator-echarts/issues/8583">#8583</a>.</p>
+</li>
+<li><p>[Fix] Fix empty <a href="https://ecomfe.github.io/echarts-doc/public/en/option.html#dataset">dataset</a> problem. See <a href="https://github.com/apache/incubator-echarts/issues/8395">#8395</a>.</p>
+</li>
+<li><p>[Fix] Fix bar start position when multiple axes exists. See <a href="https://github.com/apache/incubator-echarts/issues/8747">#8747</a>.</p>
+</li>
+<li><p>[Fix] Remove clipPath for expansion animation. See <a href="https://github.com/apache/incubator-echarts/issues/8994">#8994</a>.</p>
+</li>
+<li><p>[Fix] Fix axes overlap when two Y axes are on one X axis. See <a href="https://github.com/apache/incubator-echarts/issues/8975">#8975</a>.</p>
+</li>
+</ul>
+<h2 id="v4-1-0">v4.1.0</h2>
 <div class="time">2018-05-02</div>
 
 <ul>
diff --git a/coding-standard.html b/coding-standard.html
index 4e78cd2..848f797 100644
--- a/coding-standard.html
+++ b/coding-standard.html
@@ -1,4 +1,4 @@
-<!DOCTYPE html><html lang="en-US"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1" user-scalable="no"><meta name="description" content="ECharts, a powerful, interactive charting and visualization library for browser"><link rel="shortcut icon" href="images/favicon.png"><link rel="stylesheet" type="text/css" href="vendors/bootstrap/css/bootstrap.min.css"><!-- HTML5 shim and Respond.js for IE [...]
+<!DOCTYPE html><html lang="en-US"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1" user-scalable="no"><meta name="description" content="ECharts, a powerful, interactive charting and visualization library for browser"><link rel="shortcut icon" href="images/favicon.png"><link rel="stylesheet" type="text/css" href="vendors/bootstrap/css/bootstrap.min.css"><!-- HTML5 shim and Respond.js for IE [...]
 </script><script type="text/javascript" src="./vendors/pace/pace.min.js"></script><script id="font-hack" type="text/javascript">if (/windows/i.test(navigator.userAgent)) {
     var el = document.createElement('style');
     el.innerHTML = ''
diff --git a/committers.html b/committers.html
index 86eebb6..1d13c17 100644
--- a/committers.html
+++ b/committers.html
@@ -1,4 +1,4 @@
-<!DOCTYPE html><html lang="en-US"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1" user-scalable="no"><meta name="description" content="ECharts, a powerful, interactive charting and visualization library for browser"><link rel="shortcut icon" href="images/favicon.png"><link rel="stylesheet" type="text/css" href="vendors/bootstrap/css/bootstrap.min.css"><!-- HTML5 shim and Respond.js for IE [...]
+<!DOCTYPE html><html lang="en-US"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1" user-scalable="no"><meta name="description" content="ECharts, a powerful, interactive charting and visualization library for browser"><link rel="shortcut icon" href="images/favicon.png"><link rel="stylesheet" type="text/css" href="vendors/bootstrap/css/bootstrap.min.css"><!-- HTML5 shim and Respond.js for IE [...]
 </script><script type="text/javascript" src="./vendors/pace/pace.min.js"></script><script id="font-hack" type="text/javascript">if (/windows/i.test(navigator.userAgent)) {
     var el = document.createElement('style');
     el.innerHTML = ''
diff --git a/dependencies.html b/dependencies.html
index 54011b0..2a03fc9 100644
--- a/dependencies.html
+++ b/dependencies.html
@@ -1,4 +1,4 @@
-<!DOCTYPE html><html lang="en-US"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1" user-scalable="no"><meta name="description" content="ECharts, a powerful, interactive charting and visualization library for browser"><link rel="shortcut icon" href="images/favicon.png"><link rel="stylesheet" type="text/css" href="vendors/bootstrap/css/bootstrap.min.css"><!-- HTML5 shim and Respond.js for IE [...]
+<!DOCTYPE html><html lang="en-US"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1" user-scalable="no"><meta name="description" content="ECharts, a powerful, interactive charting and visualization library for browser"><link rel="shortcut icon" href="images/favicon.png"><link rel="stylesheet" type="text/css" href="vendors/bootstrap/css/bootstrap.min.css"><!-- HTML5 shim and Respond.js for IE [...]
 </script><script type="text/javascript" src="./vendors/pace/pace.min.js"></script><script id="font-hack" type="text/javascript">if (/windows/i.test(navigator.userAgent)) {
     var el = document.createElement('style');
     el.innerHTML = ''
diff --git a/dist/echarts-en.common.js b/dist/echarts-en.common.js
index 40ff4e0..cc93e03 100644
--- a/dist/echarts-en.common.js
+++ b/dist/echarts-en.common.js
@@ -77,7 +77,8 @@ if (typeof wx === 'object' && typeof wx.getSystemInfoSync === 'function') {
         wxa: true, // Weixin Application
         canvasSupported: true,
         svgSupported: false,
-        touchEventsSupported: true
+        touchEventsSupported: true,
+        domSupported: false
     };
 }
 else if (typeof document === 'undefined' && typeof self !== 'undefined') {
@@ -87,7 +88,8 @@ else if (typeof document === 'undefined' && typeof self !== 'undefined') {
         os: {},
         node: false,
         worker: true,
-        canvasSupported: true
+        canvasSupported: true,
+        domSupported: false
     };
 }
 else if (typeof navigator === 'undefined') {
@@ -99,7 +101,8 @@ else if (typeof navigator === 'undefined') {
         worker: false,
         // Assume canvas is supported
         canvasSupported: true,
-        svgSupported: true
+        svgSupported: true,
+        domSupported: false
     };
 }
 else {
@@ -208,8 +211,9 @@ function detect(ua) {
             // events currently. So we dont use that on other browsers unless tested sufficiently.
             // Although IE 10 supports pointer event, it use old style and is different from the
             // standard. So we exclude that. (IE 10 is hardly used on touch device)
-            && (browser.edge || (browser.ie && browser.version >= 11))
+            && (browser.edge || (browser.ie && browser.version >= 11)),
         // passiveSupported: detectPassiveSupport()
+        domSupported: typeof document !== 'undefined'
     };
 }
 
@@ -841,6 +845,9 @@ function isPrimitive(obj) {
  */
 function HashMap(obj) {
     var isArr = isArray(obj);
+    // Key should not be set on this, otherwise
+    // methods get/set/... may be overrided.
+    this.data = {};
     var thisMap = this;
 
     (obj instanceof HashMap)
@@ -852,32 +859,30 @@ function HashMap(obj) {
     }
 }
 
-// Add prefix to avoid conflict with Object.prototype.
-
 HashMap.prototype = {
     constructor: HashMap,
     // Do not provide `has` method to avoid defining what is `has`.
     // (We usually treat `null` and `undefined` as the same, different
     // from ES6 Map).
     get: function (key) {
-        return this.hasOwnProperty(key) ? this[key] : null;
+        return this.data.hasOwnProperty(key) ? this.data[key] : null;
     },
     set: function (key, value) {
         // Comparing with invocation chaining, `return value` is more commonly
         // used in this case: `var someVal = map.set('a', genVal());`
-        return (this[key] = value);
+        return (this.data[key] = value);
     },
     // Although util.each can be performed on this hashMap directly, user
     // should not use the exposed keys, who are prefixed.
     each: function (cb, context) {
         context !== void 0 && (cb = bind(cb, context));
-        for (var key in this) {
-            this.hasOwnProperty(key) && cb(this[key], key);
+        for (var key in this.data) {
+            this.data.hasOwnProperty(key) && cb(this.data[key], key);
         }
     },
     // Do not use this method if performance sensitive.
     removeKey: function (key) {
-        delete this[key];
+        delete this.data[key];
     }
 };
 
@@ -1326,7 +1331,7 @@ function param(target, e) {
 }
 
 /**
- * 事件扩展
+ * Event Mixin
  * @module zrender/mixin/Eventful
  * @author Kener (@Kener-林峰, kener.linfeng@gmail.com)
  *         pissang (https://www.github.com/pissang)
@@ -1335,12 +1340,26 @@ function param(target, e) {
 var arrySlice = Array.prototype.slice;
 
 /**
- * 事件分发器
+ * Event dispatcher.
+ *
  * @alias module:zrender/mixin/Eventful
  * @constructor
- */
-var Eventful = function () {
+ * @param {Object} [eventProcessor] The object eventProcessor is the scope when
+ *        `eventProcessor.xxx` called.
+ * @param {Function} [eventProcessor.normalizeQuery]
+ *        param: {string|Object} Raw query.
+ *        return: {string|Object} Normalized query.
+ * @param {Function} [eventProcessor.filter] Event will be dispatched only
+ *        if it returns `true`.
+ *        param: {string} eventType
+ *        param: {string|Object} query
+ *        return: {boolean}
+ * @param {Function} [eventProcessor.afterTrigger] Call after all handlers called.
+ *        param: {string} eventType
+ */
+var Eventful = function (eventProcessor) {
     this._$handlers = {};
+    this._$eventProcessor = eventProcessor;
 };
 
 Eventful.prototype = {
@@ -1348,19 +1367,28 @@ Eventful.prototype = {
     constructor: Eventful,
 
     /**
-     * 单次触发绑定,trigger后销毁
+     * The handler can only be triggered once, then removed.
      *
-     * @param {string} event 事件名
-     * @param {Function} handler 响应函数
+     * @param {string} event The event name.
+     * @param {string|Object} [query] Condition used on event filter.
+     * @param {Function} handler The event handler.
      * @param {Object} context
      */
-    one: function (event, handler, context) {
+    one: function (event, query, handler, context) {
         var _h = this._$handlers;
 
+        if (typeof query === 'function') {
+            context = handler;
+            handler = query;
+            query = null;
+        }
+
         if (!handler || !event) {
             return this;
         }
 
+        query = normalizeQuery(this, query);
+
         if (!_h[event]) {
             _h[event] = [];
         }
@@ -1374,6 +1402,7 @@ Eventful.prototype = {
         _h[event].push({
             h: handler,
             one: true,
+            query: query,
             ctx: context || this
         });
 
@@ -1381,18 +1410,28 @@ Eventful.prototype = {
     },
 
     /**
-     * 绑定事件
-     * @param {string} event 事件名
-     * @param {Function} handler 事件处理函数
+     * Bind a handler.
+     *
+     * @param {string} event The event name.
+     * @param {string|Object} [query] Condition used on event filter.
+     * @param {Function} handler The event handler.
      * @param {Object} [context]
      */
-    on: function (event, handler, context) {
+    on: function (event, query, handler, context) {
         var _h = this._$handlers;
 
+        if (typeof query === 'function') {
+            context = handler;
+            handler = query;
+            query = null;
+        }
+
         if (!handler || !event) {
             return this;
         }
 
+        query = normalizeQuery(this, query);
+
         if (!_h[event]) {
             _h[event] = [];
         }
@@ -1406,6 +1445,7 @@ Eventful.prototype = {
         _h[event].push({
             h: handler,
             one: false,
+            query: query,
             ctx: context || this
         });
 
@@ -1413,7 +1453,8 @@ Eventful.prototype = {
     },
 
     /**
-     * 是否绑定了事件
+     * Whether any handler has bound.
+     *
      * @param  {string}  event
      * @return {boolean}
      */
@@ -1423,9 +1464,10 @@ Eventful.prototype = {
     },
 
     /**
-     * 解绑事件
-     * @param {string} event 事件名
-     * @param {Function} [handler] 事件处理函数
+     * Unbind a event.
+     *
+     * @param {string} event The event name.
+     * @param {Function} [handler] The event handler.
      */
     off: function (event, handler) {
         var _h = this._$handlers;
@@ -1439,7 +1481,7 @@ Eventful.prototype = {
             if (_h[event]) {
                 var newList = [];
                 for (var i = 0, l = _h[event].length; i < l; i++) {
-                    if (_h[event][i]['h'] != handler) {
+                    if (_h[event][i].h !== handler) {
                         newList.push(_h[event][i]);
                     }
                 }
@@ -1458,12 +1500,15 @@ Eventful.prototype = {
     },
 
     /**
-     * 事件分发
+     * Dispatch a event.
      *
-     * @param {string} type 事件类型
+     * @param {string} type The event name.
      */
     trigger: function (type) {
-        if (this._$handlers[type]) {
+        var _h = this._$handlers[type];
+        var eventProcessor = this._$eventProcessor;
+
+        if (_h) {
             var args = arguments;
             var argLen = args.length;
 
@@ -1471,27 +1516,36 @@ Eventful.prototype = {
                 args = arrySlice.call(args, 1);
             }
 
-            var _h = this._$handlers[type];
             var len = _h.length;
             for (var i = 0; i < len;) {
+                var hItem = _h[i];
+                if (eventProcessor
+                    && eventProcessor.filter
+                    && hItem.query != null
+                    && !eventProcessor.filter(type, hItem.query)
+                ) {
+                    i++;
+                    continue;
+                }
+
                 // Optimize advise from backbone
                 switch (argLen) {
                     case 1:
-                        _h[i]['h'].call(_h[i]['ctx']);
+                        hItem.h.call(hItem.ctx);
                         break;
                     case 2:
-                        _h[i]['h'].call(_h[i]['ctx'], args[1]);
+                        hItem.h.call(hItem.ctx, args[1]);
                         break;
                     case 3:
-                        _h[i]['h'].call(_h[i]['ctx'], args[1], args[2]);
+                        hItem.h.call(hItem.ctx, args[1], args[2]);
                         break;
                     default:
                         // have more than 2 given arguments
-                        _h[i]['h'].apply(_h[i]['ctx'], args);
+                        hItem.h.apply(hItem.ctx, args);
                         break;
                 }
 
-                if (_h[i]['one']) {
+                if (hItem.one) {
                     _h.splice(i, 1);
                     len--;
                 }
@@ -1501,15 +1555,22 @@ Eventful.prototype = {
             }
         }
 
+        eventProcessor && eventProcessor.afterTrigger
+            && eventProcessor.afterTrigger(type);
+
         return this;
     },
 
     /**
-     * 带有context的事件分发, 最后一个参数是事件回调的context
-     * @param {string} type 事件类型
+     * Dispatch a event with context, which is specified at the last parameter.
+     *
+     * @param {string} type The event name.
      */
     triggerWithContext: function (type) {
-        if (this._$handlers[type]) {
+        var _h = this._$handlers[type];
+        var eventProcessor = this._$eventProcessor;
+
+        if (_h) {
             var args = arguments;
             var argLen = args.length;
 
@@ -1518,27 +1579,36 @@ Eventful.prototype = {
             }
             var ctx = args[args.length - 1];
 
-            var _h = this._$handlers[type];
             var len = _h.length;
             for (var i = 0; i < len;) {
+                var hItem = _h[i];
+                if (eventProcessor
+                    && eventProcessor.filter
+                    && hItem.query != null
+                    && !eventProcessor.filter(type, hItem.query)
+                ) {
+                    i++;
+                    continue;
+                }
+
                 // Optimize advise from backbone
                 switch (argLen) {
                     case 1:
-                        _h[i]['h'].call(ctx);
+                        hItem.h.call(ctx);
                         break;
                     case 2:
-                        _h[i]['h'].call(ctx, args[1]);
+                        hItem.h.call(ctx, args[1]);
                         break;
                     case 3:
-                        _h[i]['h'].call(ctx, args[1], args[2]);
+                        hItem.h.call(ctx, args[1], args[2]);
                         break;
                     default:
                         // have more than 2 given arguments
-                        _h[i]['h'].apply(ctx, args);
+                        hItem.h.apply(ctx, args);
                         break;
                 }
 
-                if (_h[i]['one']) {
+                if (hItem.one) {
                     _h.splice(i, 1);
                     len--;
                 }
@@ -1548,10 +1618,192 @@ Eventful.prototype = {
             }
         }
 
+        eventProcessor && eventProcessor.afterTrigger
+            && eventProcessor.afterTrigger(type);
+
         return this;
     }
 };
 
+function normalizeQuery(host, query) {
+    var eventProcessor = host._$eventProcessor;
+    if (query != null && eventProcessor && eventProcessor.normalizeQuery) {
+        query = eventProcessor.normalizeQuery(query);
+    }
+    return query;
+}
+
+/**
+ * 事件辅助类
+ * @module zrender/core/event
+ * @author Kener (@Kener-林峰, kener.linfeng@gmail.com)
+ */
+
+var isDomLevel2 = (typeof window !== 'undefined') && !!window.addEventListener;
+
+var MOUSE_EVENT_REG = /^(?:mouse|pointer|contextmenu|drag|drop)|click/;
+
+function getBoundingClientRect(el) {
+    // BlackBerry 5, iOS 3 (original iPhone) don't have getBoundingRect
+    return el.getBoundingClientRect ? el.getBoundingClientRect() : {left: 0, top: 0};
+}
+
+// `calculate` is optional, default false
+function clientToLocal(el, e, out, calculate) {
+    out = out || {};
+
+    // According to the W3C Working Draft, offsetX and offsetY should be relative
+    // to the padding edge of the target element. The only browser using this convention
+    // is IE. Webkit uses the border edge, Opera uses the content edge, and FireFox does
+    // not support the properties.
+    // (see http://www.jacklmoore.com/notes/mouse-position/)
+    // In zr painter.dom, padding edge equals to border edge.
+
+    // FIXME
+    // When mousemove event triggered on ec tooltip, target is not zr painter.dom, and
+    // offsetX/Y is relative to e.target, where the calculation of zrX/Y via offsetX/Y
+    // is too complex. So css-transfrom dont support in this case temporarily.
+    if (calculate || !env$1.canvasSupported) {
+        defaultGetZrXY(el, e, out);
+    }
+    // Caution: In FireFox, layerX/layerY Mouse position relative to the closest positioned
+    // ancestor element, so we should make sure el is positioned (e.g., not position:static).
+    // BTW1, Webkit don't return the same results as FF in non-simple cases (like add
+    // zoom-factor, overflow / opacity layers, transforms ...)
+    // BTW2, (ev.offsetY || ev.pageY - $(ev.target).offset().top) is not correct in preserve-3d.
+    // <https://bugs.jquery.com/ticket/8523#comment:14>
+    // BTW3, In ff, offsetX/offsetY is always 0.
+    else if (env$1.browser.firefox && e.layerX != null && e.layerX !== e.offsetX) {
+        out.zrX = e.layerX;
+        out.zrY = e.layerY;
+    }
+    // For IE6+, chrome, safari, opera. (When will ff support offsetX?)
+    else if (e.offsetX != null) {
+        out.zrX = e.offsetX;
+        out.zrY = e.offsetY;
+    }
+    // For some other device, e.g., IOS safari.
+    else {
+        defaultGetZrXY(el, e, out);
+    }
+
+    return out;
+}
+
+function defaultGetZrXY(el, e, out) {
+    // This well-known method below does not support css transform.
+    var box = getBoundingClientRect(el);
+    out.zrX = e.clientX - box.left;
+    out.zrY = e.clientY - box.top;
+}
+
+/**
+ * 如果存在第三方嵌入的一些dom触发的事件,或touch事件,需要转换一下事件坐标.
+ * `calculate` is optional, default false.
+ */
+function normalizeEvent(el, e, calculate) {
+
+    e = e || window.event;
+
+    if (e.zrX != null) {
+        return e;
+    }
+
+    var eventType = e.type;
+    var isTouch = eventType && eventType.indexOf('touch') >= 0;
+
+    if (!isTouch) {
+        clientToLocal(el, e, e, calculate);
+        e.zrDelta = (e.wheelDelta) ? e.wheelDelta / 120 : -(e.detail || 0) / 3;
+    }
+    else {
+        var touch = eventType != 'touchend'
+            ? e.targetTouches[0]
+            : e.changedTouches[0];
+        touch && clientToLocal(el, touch, e, calculate);
+    }
+
+    // Add which for click: 1 === left; 2 === middle; 3 === right; otherwise: 0;
+    // See jQuery: https://github.com/jquery/jquery/blob/master/src/event.js
+    // If e.which has been defined, if may be readonly,
+    // see: https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/which
+    var button = e.button;
+    if (e.which == null && button !== undefined && MOUSE_EVENT_REG.test(e.type)) {
+        e.which = (button & 1 ? 1 : (button & 2 ? 3 : (button & 4 ? 2 : 0)));
+    }
+
+    return e;
+}
+
+/**
+ * @param {HTMLElement} el
+ * @param {string} name
+ * @param {Function} handler
+ */
+function addEventListener(el, name, handler) {
+    if (isDomLevel2) {
+        // Reproduct the console warning:
+        // [Violation] Added non-passive event listener to a scroll-blocking <some> event.
+        // Consider marking event handler as 'passive' to make the page more responsive.
+        // Just set console log level: verbose in chrome dev tool.
+        // then the warning log will be printed when addEventListener called.
+        // See https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
+        // We have not yet found a neat way to using passive. Because in zrender the dom event
+        // listener delegate all of the upper events of element. Some of those events need
+        // to prevent default. For example, the feature `preventDefaultMouseMove` of echarts.
+        // Before passive can be adopted, these issues should be considered:
+        // (1) Whether and how a zrender user specifies an event listener passive. And by default,
+        // passive or not.
+        // (2) How to tread that some zrender event listener is passive, and some is not. If
+        // we use other way but not preventDefault of mousewheel and touchmove, browser
+        // compatibility should be handled.
+
+        // var opts = (env.passiveSupported && name === 'mousewheel')
+        //     ? {passive: true}
+        //     // By default, the third param of el.addEventListener is `capture: false`.
+        //     : void 0;
+        // el.addEventListener(name, handler /* , opts */);
+        el.addEventListener(name, handler);
+    }
+    else {
+        el.attachEvent('on' + name, handler);
+    }
+}
+
+function removeEventListener(el, name, handler) {
+    if (isDomLevel2) {
+        el.removeEventListener(name, handler);
+    }
+    else {
+        el.detachEvent('on' + name, handler);
+    }
+}
+
+/**
+ * preventDefault and stopPropagation.
+ * Notice: do not do that in zrender. Upper application
+ * do that if necessary.
+ *
+ * @memberOf module:zrender/core/event
+ * @method
+ * @param {Event} e : event对象
+ */
+var stop = isDomLevel2
+    ? function (e) {
+        e.preventDefault();
+        e.stopPropagation();
+        e.cancelBubble = true;
+    }
+    : function (e) {
+        e.returnValue = false;
+        e.cancelBubble = true;
+    };
+
+function notLeftMouse(e) {
+    // If e.which is undefined, considered as left mouse event.
+    return e.which > 1;
+}
+
 var SILENT = 'silent';
 
 function makeEventPacket(eveType, targetInfo, event) {
@@ -1571,10 +1823,15 @@ function makeEventPacket(eveType, targetInfo, event) {
         pinchScale: event.pinchScale,
         wheelDelta: event.zrDelta,
         zrByTouch: event.zrByTouch,
-        which: event.which
+        which: event.which,
+        stop: stopEvent
     };
 }
 
+function stopEvent(event) {
+    stop(this.event);
+}
+
 function EmptyProxy () {}
 EmptyProxy.prototype.dispose = function () {};
 
@@ -2140,6 +2397,7 @@ transformableProto.needLocalTransform = function () {
         || isNotAroundZero(this.scale[1] - 1);
 };
 
+var scaleTmp = [];
 transformableProto.updateTransform = function () {
     var parent = this.parent;
     var parentHasTransform = parent && parent.transform;
@@ -2172,6 +2430,20 @@ transformableProto.updateTransform = function () {
     // 保存这个变换矩阵
     this.transform = m;
 
+    var globalScaleRatio = this.globalScaleRatio;
+    if (globalScaleRatio != null && globalScaleRatio !== 1) {
+        this.getGlobalScale(scaleTmp);
+        var relX = scaleTmp[0] < 0 ? -1 : 1;
+        var relY = scaleTmp[1] < 0 ? -1 : 1;
+        var sx = ((scaleTmp[0] - relX) * globalScaleRatio + relX) / scaleTmp[0] || 0;
+        var sy = ((scaleTmp[1] - relY) * globalScaleRatio + relY) / scaleTmp[1] || 0;
+
+        m[0] *= sx;
+        m[1] *= sx;
+        m[2] *= sy;
+        m[3] *= sy;
+    }
+
     this.invTransform = this.invTransform || create$1();
     invert(this.invTransform, m);
 };
@@ -2201,21 +2473,13 @@ transformableProto.restoreTransform = function (ctx) {
 };
 
 var tmpTransform = [];
+var originTransform = create$1();
 
-/**
- * 分解`transform`矩阵到`position`, `rotation`, `scale`
- */
-transformableProto.decomposeTransform = function () {
-    if (!this.transform) {
+transformableProto.setLocalTransform = function (m) {
+    if (!m) {
+        // TODO return or set identity?
         return;
     }
-    var parent = this.parent;
-    var m = this.transform;
-    if (parent && parent.transform) {
-        // Get local transform and decompose them to position, scale, rotation
-        mul$1(tmpTransform, parent.invTransform, m);
-        m = tmpTransform;
-    }
     var sx = m[0] * m[0] + m[1] * m[1];
     var sy = m[2] * m[2] + m[3] * m[3];
     var position = this.position;
@@ -2232,31 +2496,61 @@ transformableProto.decomposeTransform = function () {
     if (m[3] < 0) {
         sy = -sy;
     }
+
     position[0] = m[4];
     position[1] = m[5];
     scale$$1[0] = sx;
     scale$$1[1] = sy;
     this.rotation = Math.atan2(-m[1] / sy, m[0] / sx);
 };
+/**
+ * 分解`transform`矩阵到`position`, `rotation`, `scale`
+ */
+transformableProto.decomposeTransform = function () {
+    if (!this.transform) {
+        return;
+    }
+    var parent = this.parent;
+    var m = this.transform;
+    if (parent && parent.transform) {
+        // Get local transform and decompose them to position, scale, rotation
+        mul$1(tmpTransform, parent.invTransform, m);
+        m = tmpTransform;
+    }
+    var origin = this.origin;
+    if (origin && (origin[0] || origin[1])) {
+        originTransform[4] = origin[0];
+        originTransform[5] = origin[1];
+        mul$1(tmpTransform, m, originTransform);
+        tmpTransform[4] -= origin[0];
+        tmpTransform[5] -= origin[1];
+        m = tmpTransform;
+    }
+
+    this.setLocalTransform(m);
+};
 
 /**
  * Get global scale
  * @return {Array.<number>}
  */
-transformableProto.getGlobalScale = function () {
+transformableProto.getGlobalScale = function (out) {
     var m = this.transform;
+    out = out || [];
     if (!m) {
-        return [1, 1];
+        out[0] = 1;
+        out[1] = 1;
+        return out;
     }
-    var sx = Math.sqrt(m[0] * m[0] + m[1] * m[1]);
-    var sy = Math.sqrt(m[2] * m[2] + m[3] * m[3]);
+    out[0] = Math.sqrt(m[0] * m[0] + m[1] * m[1]);
+    out[1] = Math.sqrt(m[2] * m[2] + m[3] * m[3]);
     if (m[0] < 0) {
-        sx = -sx;
+        out[0] = -out[0];
     }
     if (m[3] < 0) {
-        sy = -sy;
+        out[1] = -out[1];
     }
-    return [sx, sy];
+    return out;
 };
 /**
  * 变换坐标位置到 shape 的局部坐标空间
@@ -4348,135 +4642,159 @@ Animatable.prototype = {
      *      position: [10, 10]
      *  }, 100, 100, 'cubicOut', function () { // done })
      */
-        // TODO Return animation key
+    // TODO Return animation key
     animateTo: function (target, time, delay, easing, callback, forceAnimate) {
-        // animateTo(target, time, easing, callback);
-        if (isString(delay)) {
-            callback = easing;
-            easing = delay;
-            delay = 0;
-        }
-        // animateTo(target, time, delay, callback);
-        else if (isFunction$1(easing)) {
-            callback = easing;
-            easing = 'linear';
-            delay = 0;
-        }
-        // animateTo(target, time, callback);
-        else if (isFunction$1(delay)) {
-            callback = delay;
-            delay = 0;
-        }
-        // animateTo(target, callback)
-        else if (isFunction$1(time)) {
-            callback = time;
-            time = 500;
-        }
-        // animateTo(target)
-        else if (!time) {
-            time = 500;
-        }
-        // Stop all previous animations
-        this.stopAnimation();
-        this._animateToShallow('', this, target, time, delay);
-
-        // Animators may be removed immediately after start
-        // if there is nothing to animate
-        var animators = this.animators.slice();
-        var count = animators.length;
-        function done() {
-            count--;
-            if (!count) {
-                callback && callback();
-            }
-        }
-
-        // No animators. This should be checked before animators[i].start(),
-        // because 'done' may be executed immediately if no need to animate.
+        animateTo(this, target, time, delay, easing, callback, forceAnimate);
+    },
+
+    /**
+     * Animate from the target state to current state.
+     * The params and the return value are the same as `this.animateTo`.
+     */
+    animateFrom: function (target, time, delay, easing, callback, forceAnimate) {
+        animateTo(this, target, time, delay, easing, callback, forceAnimate, true);
+    }
+};
+
+function animateTo(animatable, target, time, delay, easing, callback, forceAnimate, reverse) {
+    // animateTo(target, time, easing, callback);
+    if (isString(delay)) {
+        callback = easing;
+        easing = delay;
+        delay = 0;
+    }
+    // animateTo(target, time, delay, callback);
+    else if (isFunction$1(easing)) {
+        callback = easing;
+        easing = 'linear';
+        delay = 0;
+    }
+    // animateTo(target, time, callback);
+    else if (isFunction$1(delay)) {
+        callback = delay;
+        delay = 0;
+    }
+    // animateTo(target, callback)
+    else if (isFunction$1(time)) {
+        callback = time;
+        time = 500;
+    }
+    // animateTo(target)
+    else if (!time) {
+        time = 500;
+    }
+    // Stop all previous animations
+    animatable.stopAnimation();
+    animateToShallow(animatable, '', animatable, target, time, delay, reverse);
+
+    // Animators may be removed immediately after start
+    // if there is nothing to animate
+    var animators = animatable.animators.slice();
+    var count = animators.length;
+    function done() {
+        count--;
         if (!count) {
             callback && callback();
         }
-        // Start after all animators created
-        // Incase any animator is done immediately when all animation properties are not changed
-        for (var i = 0; i < animators.length; i++) {
-            animators[i]
-                .done(done)
-                .start(easing, forceAnimate);
+    }
+
+    // No animators. This should be checked before animators[i].start(),
+    // because 'done' may be executed immediately if no need to animate.
+    if (!count) {
+        callback && callback();
+    }
+    // Start after all animators created
+    // Incase any animator is done immediately when all animation properties are not changed
+    for (var i = 0; i < animators.length; i++) {
+        animators[i]
+            .done(done)
+            .start(easing, forceAnimate);
+    }
+}
+
+/**
+ * @param {string} path=''
+ * @param {Object} source=animatable
+ * @param {Object} target
+ * @param {number} [time=500]
+ * @param {number} [delay=0]
+ * @param {boolean} [reverse] If `true`, animate
+ *        from the `target` to current state.
+ *
+ * @example
+ *  // Animate position
+ *  el._animateToShallow({
+ *      position: [10, 10]
+ *  })
+ *
+ *  // Animate shape, style and position in 100ms, delayed 100ms
+ *  el._animateToShallow({
+ *      shape: {
+ *          width: 500
+ *      },
+ *      style: {
+ *          fill: 'red'
+ *      }
+ *      position: [10, 10]
... 40409 lines suppressed ...


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


Re: [incubator-echarts-website] branch asf-site updated: 4.2.0-rc.1

Posted by Dave Fisher <wa...@apache.org>.
Does this commit preparing for 4.2 include the appropriate fixes needed to meet Apache Release Distribution Policy? [1]

>> Download Links: http://echarts.apache.org/download <http://echarts.apache.org/download>
> 
> The download page does not use the ASF mirror system, there are no
> links to KEYS, sigs or hashes and no details on why/how to verify
> downloads using sigs or hashes.
> 
> Also the online builder is clever, but does not provide any download
> verification.

The download link policy can be found here [2] with the signatures and checksum policy here [3]

Regards,
Dave

[1] https://www.apache.org/dev/release-distribution
[2] https://www.apache.org/dev/release-distribution#download-links
[3] https://www.apache.org/dev/release-distribution#sigs-and-sums