feat: use sandbox
diff --git a/build/webpack.config.js b/build/webpack.config.js
index 80dd631..97d692e 100644
--- a/build/webpack.config.js
+++ b/build/webpack.config.js
@@ -49,12 +49,13 @@
           ]
         },
         {
-          test: /\.svg$/,
+          test: /\.(svg|html)$/,
           use: [
             {
               loader: 'html-loader',
               options: {
-                minimize: true
+                // will be `true` in production
+                // minimize: true
               }
             }
           ]
diff --git a/package-lock.json b/package-lock.json
index 304d4f1..6ce6c9d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -49,6 +49,7 @@
         "open": "^7.1.0",
         "pixelmatch": "^5.2.1",
         "pngjs": "^6.0.0",
+        "raw-loader": "^4.0.2",
         "sass.js": "^0.11.1",
         "sassjs-loader": "^2.0.0",
         "seedrandom": "^3.0.5",
@@ -1584,9 +1585,9 @@
       }
     },
     "node_modules/@types/json-schema": {
-      "version": "7.0.5",
-      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz",
-      "integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==",
+      "version": "7.0.11",
+      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
+      "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
       "dev": true
     },
     "node_modules/@types/minimatch": {
@@ -4899,12 +4900,6 @@
         "webpack": "^4.0.0 || ^5.0.0"
       }
     },
-    "node_modules/html-loader/node_modules/@types/json-schema": {
-      "version": "7.0.6",
-      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz",
-      "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==",
-      "dev": true
-    },
     "node_modules/html-loader/node_modules/json5": {
       "version": "2.1.3",
       "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz",
@@ -5899,12 +5894,6 @@
         "webpack": "^4.4.0 || ^5.0.0"
       }
     },
-    "node_modules/mini-css-extract-plugin/node_modules/@types/json-schema": {
-      "version": "7.0.6",
-      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz",
-      "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==",
-      "dev": true
-    },
     "node_modules/mini-css-extract-plugin/node_modules/json5": {
       "version": "2.1.3",
       "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz",
@@ -7014,6 +7003,70 @@
         "safe-buffer": "^5.1.0"
       }
     },
+    "node_modules/raw-loader": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz",
+      "integrity": "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==",
+      "dev": true,
+      "dependencies": {
+        "loader-utils": "^2.0.0",
+        "schema-utils": "^3.0.0"
+      },
+      "engines": {
+        "node": ">= 10.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependencies": {
+        "webpack": "^4.0.0 || ^5.0.0"
+      }
+    },
+    "node_modules/raw-loader/node_modules/json5": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
+      "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
+      "dev": true,
+      "bin": {
+        "json5": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/raw-loader/node_modules/loader-utils": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
+      "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
+      "dev": true,
+      "dependencies": {
+        "big.js": "^5.2.2",
+        "emojis-list": "^3.0.0",
+        "json5": "^2.1.2"
+      },
+      "engines": {
+        "node": ">=8.9.0"
+      }
+    },
+    "node_modules/raw-loader/node_modules/schema-utils": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz",
+      "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==",
+      "dev": true,
+      "dependencies": {
+        "@types/json-schema": "^7.0.8",
+        "ajv": "^6.12.5",
+        "ajv-keywords": "^3.5.2"
+      },
+      "engines": {
+        "node": ">= 10.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      }
+    },
     "node_modules/rc": {
       "version": "1.2.8",
       "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
@@ -8000,12 +8053,6 @@
         "webpack": "^4.0.0 || ^5.0.0"
       }
     },
-    "node_modules/style-loader/node_modules/@types/json-schema": {
-      "version": "7.0.6",
-      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz",
-      "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==",
-      "dev": true
-    },
     "node_modules/style-loader/node_modules/ajv": {
       "version": "6.12.6",
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -8264,12 +8311,6 @@
         "webpack": "^5.1.0"
       }
     },
-    "node_modules/terser-webpack-plugin/node_modules/@types/json-schema": {
-      "version": "7.0.6",
-      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz",
-      "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==",
-      "dev": true
-    },
     "node_modules/terser-webpack-plugin/node_modules/ajv": {
       "version": "6.12.6",
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -9001,12 +9042,6 @@
         "source-map": "~0.6.1"
       }
     },
-    "node_modules/webpack/node_modules/@types/json-schema": {
-      "version": "7.0.6",
-      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz",
-      "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==",
-      "dev": true
-    },
     "node_modules/webpack/node_modules/ajv": {
       "version": "6.12.6",
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -10606,9 +10641,9 @@
       }
     },
     "@types/json-schema": {
-      "version": "7.0.5",
-      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz",
-      "integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==",
+      "version": "7.0.11",
+      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
+      "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
       "dev": true
     },
     "@types/minimatch": {
@@ -13272,12 +13307,6 @@
         "schema-utils": "^3.0.0"
       },
       "dependencies": {
-        "@types/json-schema": {
-          "version": "7.0.6",
-          "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz",
-          "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==",
-          "dev": true
-        },
         "json5": {
           "version": "2.1.3",
           "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz",
@@ -14042,12 +14071,6 @@
         "webpack-sources": "^1.1.0"
       },
       "dependencies": {
-        "@types/json-schema": {
-          "version": "7.0.6",
-          "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz",
-          "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==",
-          "dev": true
-        },
         "json5": {
           "version": "2.1.3",
           "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz",
@@ -14926,6 +14949,46 @@
         "safe-buffer": "^5.1.0"
       }
     },
+    "raw-loader": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz",
+      "integrity": "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==",
+      "dev": true,
+      "requires": {
+        "loader-utils": "^2.0.0",
+        "schema-utils": "^3.0.0"
+      },
+      "dependencies": {
+        "json5": {
+          "version": "2.2.1",
+          "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
+          "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
+          "dev": true
+        },
+        "loader-utils": {
+          "version": "2.0.2",
+          "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
+          "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
+          "dev": true,
+          "requires": {
+            "big.js": "^5.2.2",
+            "emojis-list": "^3.0.0",
+            "json5": "^2.1.2"
+          }
+        },
+        "schema-utils": {
+          "version": "3.1.1",
+          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz",
+          "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==",
+          "dev": true,
+          "requires": {
+            "@types/json-schema": "^7.0.8",
+            "ajv": "^6.12.5",
+            "ajv-keywords": "^3.5.2"
+          }
+        }
+      }
+    },
     "rc": {
       "version": "1.2.8",
       "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
@@ -15690,12 +15753,6 @@
         "schema-utils": "^3.0.0"
       },
       "dependencies": {
-        "@types/json-schema": {
-          "version": "7.0.6",
-          "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz",
-          "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==",
-          "dev": true
-        },
         "ajv": {
           "version": "6.12.6",
           "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -15901,12 +15958,6 @@
         "terser": "^5.3.8"
       },
       "dependencies": {
-        "@types/json-schema": {
-          "version": "7.0.6",
-          "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz",
-          "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==",
-          "dev": true
-        },
         "ajv": {
           "version": "6.12.6",
           "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -16265,12 +16316,6 @@
         "webpack-sources": "^2.1.1"
       },
       "dependencies": {
-        "@types/json-schema": {
-          "version": "7.0.6",
-          "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz",
-          "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==",
-          "dev": true
-        },
         "ajv": {
           "version": "6.12.6",
           "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
diff --git a/package.json b/package.json
index 49e3069..6d04c91 100644
--- a/package.json
+++ b/package.json
@@ -45,6 +45,7 @@
     "open": "^7.1.0",
     "pixelmatch": "^5.2.1",
     "pngjs": "^6.0.0",
+    "raw-loader": "^4.0.2",
     "sass.js": "^0.11.1",
     "sassjs-loader": "^2.0.0",
     "seedrandom": "^3.0.5",
diff --git a/src/common/config.js b/src/common/config.js
index 919c39a..a2ea9cf 100644
--- a/src/common/config.js
+++ b/src/common/config.js
@@ -98,13 +98,11 @@
 ]);
 
 const URL_PARAMS = {};
-(location.search || '')
-  .slice(1)
-  .split('&')
-  .forEach(function (item) {
-    const kv = item.split('=');
-    URL_PARAMS[kv[0]] = kv[1];
-  });
+(() =>
+  // Object.fromEntries(new URLSearchParams(location.search).entries())
+  new URLSearchParams(location.search).forEach(
+    (val, key) => (URL_PARAMS[key] = val)
+  ))();
 
 export { URL_PARAMS };
 
@@ -112,9 +110,10 @@
 export const CDN_ROOT = 'https://cdn.jsdelivr.net/npm/';
 
 export const SCRIPT_URLS = {
-  echartsMinJS: '/dist/echarts.min.js',
   echartsDir: `${CDN_ROOT}echarts@{{version}}`,
   echartsNightlyDir: `${CDN_ROOT}echarts-nightly@{{version}}`,
+  echartsJS: '/dist/echarts.js',
+  echartsMinJS: '/dist/echarts.min.js',
 
   localEChartsMinJS: 'http://localhost/echarts/dist/echarts.js',
   localEChartsDir: 'http://localhost/echarts',
@@ -127,5 +126,9 @@
   monacoDir: `${CDN_ROOT}monaco-editor@0.27.0/min/vs`,
   aceDir: `${CDN_ROOT}ace-builds@1.4.12/src-min-noconflict`,
 
-  prettierDir: `${CDN_ROOT}prettier@2.3.2`
+  prettierDir: `${CDN_ROOT}prettier@2.3.2`,
+
+  bmapLibJS:
+    'https://api.map.baidu.com/getscript?v=3.0&ak=KOmVjPVUAey1G2E8zNhPiuQ6QiEmAwZu',
+  echartsBMapMinJS: '/dist/extension/bmap.min.js'
 };
diff --git a/src/common/helper.js b/src/common/helper.js
index 94f3e24..964ce3f 100644
--- a/src/common/helper.js
+++ b/src/common/helper.js
@@ -67,7 +67,7 @@
       SCRIPT_URLS.prettierDir + '/standalone.js',
       SCRIPT_URLS.prettierDir +
         (store.typeCheck ? '/parser-typescript.js' : '/parser-babel.js')
-    ]).then(([_, parser]) => {});
+    ]);
   }
   return Promise.resolve();
 }
diff --git a/src/dep/showDebugDirtyRect.js b/src/dep/showDebugDirtyRect.js
index 5ee3204..34ece51 100644
--- a/src/dep/showDebugDirtyRect.js
+++ b/src/dep/showDebugDirtyRect.js
@@ -1,6 +1,6 @@
-var DebugRect = (function () {
+const DebugRect = (function () {
   function DebugRect(style) {
-    var dom = (this.dom = document.createElement('div'));
+    const dom = (this.dom = document.createElement('div'));
     dom.className = 'ec-debug-dirty-rect';
     style = Object.assign({}, style);
     Object.assign(style, {
@@ -8,15 +8,15 @@
       border: '1px solid #00f'
     });
     dom.style.cssText =
-      '\nposition: absolute;\nopacity: 0;\ntransition: opacity 0.5s linear;\npointer-events: none;\n';
-    for (var key in style) {
+      'position:absolute;opacity:0;transition:opacity 0.5s linear;pointer-events:none;';
+    for (const key in style) {
       if (style.hasOwnProperty(key)) {
         dom.style[key] = style[key];
       }
     }
   }
   DebugRect.prototype.update = function (rect) {
-    var domStyle = this.dom.style;
+    const domStyle = this.dom.style;
     domStyle.width = rect.width + 'px';
     domStyle.height = rect.height + 'px';
     domStyle.left = rect.x + 'px';
@@ -26,7 +26,7 @@
     this.dom.style.opacity = '0';
   };
   DebugRect.prototype.show = function () {
-    var _this = this;
+    const _this = this;
     clearTimeout(this._hideTimeout);
     this.dom.style.opacity = '1';
     this._hideTimeout = setTimeout(function () {
@@ -35,9 +35,10 @@
   };
   return DebugRect;
 })();
-export default function (zr, opts) {
+
+function showDebugDirtyRect(zr, opts) {
   opts = opts || {};
-  var painter = zr.painter;
+  const painter = zr.painter;
   if (!painter.getLayers) {
     throw new Error('Debug dirty rect can only been used on canvas renderer.');
   }
@@ -46,12 +47,12 @@
       'Debug dirty rect can only been used on zrender inited with container.'
     );
   }
-  var debugViewRoot = document.createElement('div');
-  debugViewRoot.style.cssText =
-    '\nposition:absolute;\nleft:0;\ntop:0;\nright:0;\nbottom:0;\npointer-events:none;\n';
+  const debugViewRoot = document.createElement('div');
   debugViewRoot.className = 'ec-debug-dirty-rect-container';
-  var debugRects = [];
-  var dom = zr.dom;
+  debugViewRoot.style.cssText =
+    'position:absolute;left:0;top:0;right:0;bottom:0;pointer-events:none;';
+  const debugRects = [];
+  const dom = zr.dom;
   dom.appendChild(debugViewRoot);
   var computedStyle = getComputedStyle(dom);
   if (computedStyle.position === 'static') {
@@ -59,13 +60,13 @@
   }
   zr.on('rendered', function () {
     if (painter.getLayers) {
-      var idx_1 = 0;
+      let idx_1 = 0;
       painter.eachBuiltinLayer(function (layer) {
         if (!layer.debugGetPaintRects) {
           return;
         }
-        var paintRects = layer.debugGetPaintRects();
-        for (var i = 0; i < paintRects.length; i++) {
+        const paintRects = layer.debugGetPaintRects();
+        for (let i = 0; i < paintRects.length; i++) {
           if (!debugRects[idx_1]) {
             debugRects[idx_1] = new DebugRect(opts.style);
             debugViewRoot.appendChild(debugRects[idx_1].dom);
@@ -75,7 +76,7 @@
           idx_1++;
         }
       });
-      for (var i = idx_1; i < debugRects.length; i++) {
+      for (let i = idx_1; i < debugRects.length; i++) {
         debugRects[i].hide();
       }
     }
diff --git a/src/editor/CodeMonaco.vue b/src/editor/CodeMonaco.vue
index a7e910c..80275bf 100644
--- a/src/editor/CodeMonaco.vue
+++ b/src/editor/CodeMonaco.vue
@@ -6,7 +6,6 @@
 import { loadScriptsAsync } from '../common/helper';
 import { store } from '../common/store';
 import { SCRIPT_URLS, URL_PARAMS } from '../common/config';
-import { ensureECharts } from './Preview.vue';
 
 function loadTypes() {
   return fetch(
@@ -88,31 +87,26 @@
 }
 
 function ensureMonacoAndTsTransformer() {
-  function loadMonaco() {
-    if (typeof monaco === 'undefined') {
-      return loadScriptsAsync([
-        SCRIPT_URLS.monacoDir + '/loader.js',
-        // Prebuilt TS transformer with surcrase
-        store.cdnRoot + '/js/example-transform-ts-bundle.js'
-      ]).then(function () {
-        window.require.config({ paths: { vs: SCRIPT_URLS.monacoDir } });
-        return new Promise((resolve) => {
-          window.require(['vs/editor/editor.main'], function () {
-            loadTypes().then(() => {
-              // Disable AMD. Which will break other libs.
-              // FIXME
-              window.define.amd = null;
-              resolve();
-            });
+  if (typeof monaco === 'undefined') {
+    return loadScriptsAsync([
+      SCRIPT_URLS.monacoDir + '/loader.js',
+      // Prebuilt TS transformer with surcrase
+      store.cdnRoot + '/js/example-transform-ts-bundle.js'
+    ]).then(function () {
+      window.require.config({ paths: { vs: SCRIPT_URLS.monacoDir } });
+      return new Promise((resolve) => {
+        window.require(['vs/editor/editor.main'], function () {
+          loadTypes().then(() => {
+            // Disable AMD. Which will break other libs.
+            // FIXME
+            window.define.amd = null;
+            resolve();
           });
         });
       });
-    }
-    return Promise.resolve();
+    });
   }
-
-  // Must load echarts before monaco. Or the AMD loader will affect loading of echarts.
-  return ensureECharts().then(loadMonaco);
+  return Promise.resolve();
 }
 
 export default {
diff --git a/src/editor/Editor.vue b/src/editor/Editor.vue
index cf54828..516a50c 100644
--- a/src/editor/Editor.vue
+++ b/src/editor/Editor.vue
@@ -81,12 +81,12 @@
               <CodeMonaco
                 v-if="shared.typeCheck"
                 id="code-panel"
-                :initialCode="shared.initialCode"
+                :initialCode="initialCode"
               ></CodeMonaco>
               <CodeAce
                 v-else
                 id="code-panel"
-                :initialCode="shared.initialCode"
+                :initialCode="initialCode"
               ></CodeAce>
             </el-main>
           </el-container>
@@ -226,7 +226,7 @@
     } else {
       loadExampleCode().then((code) => {
         // Only set the code in editor. editor will sync to the store.
-        store.initialCode = parseSourceCode(code);
+        store.initialCode = this.initialCode = parseSourceCode(code);
       });
 
       window.addEventListener('mousemove', (e) => {
@@ -339,7 +339,7 @@
     },
     changeLang(lang) {
       if ((URL_PARAMS.lang || 'js').toLowerCase() !== lang) {
-        if (!store.initialCode || store.sourceCode === store.initialCode) {
+        if (!this.initialCode || store.sourceCode === this.initialCode) {
           gotoURL(
             Object.assign({}, URL_PARAMS, {
               lang
@@ -364,7 +364,7 @@
     },
     format() {
       formatCode(store.sourceCode).then((code) => {
-        store.initialCode = code;
+        store.initialCode = this.initialCode = code;
       });
     }
   },
diff --git a/src/editor/Preview.vue b/src/editor/Preview.vue
index f56b500..398c77f 100644
--- a/src/editor/Preview.vue
+++ b/src/editor/Preview.vue
@@ -4,10 +4,9 @@
       v-loading="loading"
       class="right-panel"
       id="chart-panel"
+      ref="chartPanel"
       :style="{ background: backgroundColor }"
-    >
-      <div class="chart-container"></div>
-    </div>
+    ></div>
     <div id="tool-panel">
       <div class="left-panel">
         <el-switch
@@ -90,7 +89,7 @@
           </el-option>
         </el-select>
         <el-checkbox
-          v-if="!shared.isMobile"
+          v-if="inEditor && !shared.isMobile"
           v-model="nightly"
           class="use-nightly"
           >Nightly</el-checkbox
@@ -109,11 +108,7 @@
     <div id="preview-status">
       <div class="left">
         <template v-if="inEditor && !shared.isMobile">
-          <el-button
-            icon="el-icon-download"
-            size="mini"
-            @click="downloadExample"
-          >
+          <el-button icon="el-icon-download" size="mini" @click="download">
             {{ $t('editor.download') }}
           </el-button>
           <el-button
@@ -153,10 +148,9 @@
   updateRunHash
 } from '../common/store';
 import { SCRIPT_URLS, URL_PARAMS } from '../common/config';
-import { loadScriptsAsync, compressStr } from '../common/helper';
+import { compressStr } from '../common/helper';
 import { createSandbox } from './sandbox';
 import debounce from 'lodash/debounce';
-import { addListener } from 'resize-detector';
 import { download } from './downloadExample';
 import { gotoURL } from '../common/route';
 import { gt } from 'semver';
@@ -164,47 +158,25 @@
 const example = getExampleConfig();
 const isGL = isGLExample();
 
-function addDecalIfNecessary(option) {
-  if (store.enableDecal) {
-    option.aria = option.aria || {};
-    option.aria.decal = option.aria.decal || {};
-    option.aria.decal.show = true;
-    option.aria.show = option.aria.enabled = true;
-  }
-}
+function getScripts(nightly) {
+  const echartsDir = SCRIPT_URLS[
+    nightly ? 'echartsNightlyDir' : 'echartsDir'
+  ].replace('{{version}}', store.echartsVersion);
+  const hasBmap = example && example.tags.indexOf('bmap') >= 0;
 
-export function ensureECharts(nightly) {
-  if (typeof echarts === 'undefined') {
-    const hasBmap = example && example.tags.indexOf('bmap') >= 0;
-    const echartsDir = SCRIPT_URLS[
-      nightly ? 'echartsNightlyDir' : 'echartsDir'
-    ].replace('{{version}}', store.echartsVersion);
-
-    // Code from https://api.map.baidu.com/api?v=2.0&ak=KOmVjPVUAey1G2E8zNhPiuQ6QiEmAwZu
-    if (hasBmap) {
-      window.HOST_TYPE = '2';
-      window.BMap_loadScriptTime = new Date().getTime();
-    }
-
-    return loadScriptsAsync([
-      SCRIPT_URLS.datGUIMinJS,
-      'local' in URL_PARAMS
-        ? SCRIPT_URLS.localEChartsMinJS
-        : echartsDir + SCRIPT_URLS.echartsMinJS,
-      SCRIPT_URLS.echartsWorldMapJS,
-      SCRIPT_URLS.echartsStatMinJS,
-      ...(URL_PARAMS.gl ? [SCRIPT_URLS.echartsGLMinJS] : []),
-      ...(hasBmap
-        ? [
-            'https://api.map.baidu.com/getscript?v=3.0&ak=KOmVjPVUAey1G2E8zNhPiuQ6QiEmAwZu&services=&t=20200327103013',
-            echartsDir + '/dist/extension/bmap.min.js'
-          ]
-        : [])
-    ]).then(() => {
-      echarts.registerPreprocessor(addDecalIfNecessary);
-    });
-  }
-  return Promise.resolve();
+  return [
+    'local' in URL_PARAMS
+      ? SCRIPT_URLS.localEChartsMinJS
+      : echartsDir +
+        SCRIPT_URLS['dev' in URL_PARAMS ? 'echartsJS' : 'echartsMinJS'],
+    ...(URL_PARAMS.gl ? [SCRIPT_URLS.echartsGLMinJS] : []),
+    ...(hasBmap
+      ? [SCRIPT_URLS.bmapLibJS, echartsDir + SCRIPT_URLS.echartsBMapMinJS]
+      : []),
+    SCRIPT_URLS.echartsStatMinJS,
+    SCRIPT_URLS.echartsWorldMapJS,
+    SCRIPT_URLS.datGUIMinJS
+  ].map((url) => ({ src: url }));
 }
 
 function log(text, type) {
@@ -215,60 +187,78 @@
   store.editorStatus.type = type;
 }
 
-function run() {
-  if (typeof echarts === 'undefined') {
+function run(recreateInstance) {
+  if (!store.runCode) {
     return;
   }
-  if (!this.sandbox) {
-    this.sandbox = createSandbox((chart) => {
-      const option = chart.getOption();
-      if (
-        typeof option.backgroundColor === 'string' &&
-        option.backgroundColor !== 'transparent'
-      ) {
-        this.backgroundColor = option.backgroundColor;
-      } else {
-        this.backgroundColor = '#fff';
-      }
-    });
-  }
 
-  try {
-    const updateTime = this.sandbox.run(
-      this.$el.querySelector('.chart-container'),
-      store
-    );
+  const runCode = () => {
+    console.log('runCode');
 
-    log(this.$t('editor.chartOK') + updateTime + 'ms');
-
-    // Find the appropriate throttle time
-    const debounceTime = 500;
-    const debounceTimeQuantities = [0, 500, 2000, 5000, 10000];
-    for (let i = debounceTimeQuantities.length - 1; i >= 0; i--) {
-      const quantity = debounceTimeQuantities[i];
-      const preferredDebounceTime = debounceTimeQuantities[i + 1] || 1000000;
-      if (
-        updateTime >= quantity &&
-        this.debouncedTime !== preferredDebounceTime
-      ) {
-        this.debouncedRun = debounce(run, preferredDebounceTime, {
-          trailing: true
-        });
-        this.debouncedTime = preferredDebounceTime;
-        break;
-      }
-    }
+    this.sandbox.run(store, recreateInstance);
 
     // Update run hash to let others known chart has been changed.
     updateRunHash();
-  } catch (e) {
-    log(this.$t('editor.errorInEditor'), 'error');
-    console.error(e);
+  };
+
+  if (!this.sandbox) {
+    this.loading = true;
+    this.sandbox = createSandbox(
+      this.$refs.chartPanel,
+      getScripts(this.nightly),
+      () => {
+        runCode();
+        this.loading = false;
+      },
+      () => {
+        // TODO show error hints
+        this.loading = false;
+      },
+      () => {
+        log(this.$t('editor.errorInEditor'), 'error');
+      },
+      (option, updateTime) => {
+        if (
+          typeof option.backgroundColor === 'string' &&
+          option.backgroundColor !== 'transparent'
+        ) {
+          this.backgroundColor = option.backgroundColor;
+        } else {
+          this.backgroundColor = '#fff';
+        }
+
+        log(this.$t('editor.chartOK') + updateTime.toFixed(2) + 'ms');
+
+        // Find the appropriate throttle time
+        const debounceTimeQuantities = [0, 500, 2000, 5000, 10000];
+        for (let i = debounceTimeQuantities.length - 1; i >= 0; i--) {
+          const quantity = debounceTimeQuantities[i];
+          const preferredDebounceTime =
+            debounceTimeQuantities[i + 1] || 1000000;
+          if (
+            updateTime >= quantity &&
+            this.debouncedTime !== preferredDebounceTime
+          ) {
+            this.debouncedRun = debounce(run, preferredDebounceTime, {
+              trailing: true
+            });
+            this.debouncedTime = preferredDebounceTime;
+            break;
+          }
+        }
+      }
+    );
+  } else {
+    runCode();
   }
 }
 
 export default {
-  props: ['inEditor'],
+  props: {
+    inEditor: {
+      type: Boolean
+    }
+  },
 
   data() {
     return {
@@ -276,7 +266,7 @@
       debouncedTime: undefined,
       backgroundColor: '',
       autoRun: true,
-      loading: false,
+      loading: true,
 
       isGL,
 
@@ -287,13 +277,7 @@
   },
 
   mounted() {
-    this.loadECharts();
-
-    addListener(this.$el, () => {
-      if (this.sandbox) {
-        this.sandbox.resize();
-      }
-    });
+    this.run();
 
     this.fetchVersionList();
   },
@@ -357,52 +341,37 @@
     run,
     // debouncedRun will be created at first run
     // debouncedRun: null,
-    loadECharts() {
-      this.loading = true;
-      ensureECharts(this.nightly).then(() => {
-        this.loading = false;
-        if (store.runCode) {
-          this.run();
-        }
-      });
-    },
     refreshAll() {
-      this.dispose();
-      this.run();
+      this.run(true);
     },
     dispose() {
       if (this.sandbox) {
         this.sandbox.dispose();
+        this.sandbox = null;
       }
     },
-    downloadExample() {
-      download();
-    },
+    download,
     screenshot() {
-      if (this.sandbox) {
-        const url = this.sandbox.getDataURL();
-        const $a = document.createElement('a');
-        $a.download =
-          URL_PARAMS.c + '.' + (store.renderer === 'svg' ? 'svg' : 'png');
-        $a.target = '_blank';
-        $a.href = url;
-        const evt = new MouseEvent('click', {
-          bubbles: true,
-          cancelable: false
-        });
-        $a.dispatchEvent(evt);
-      }
+      this.sandbox &&
+        this.sandbox.screenshot(
+          (URL_PARAMS.c || Date.now()) +
+            '.' +
+            (store.renderer === 'svg' ? 'svg' : 'png')
+        );
     },
     share() {
-      let shareURL = location.href;
+      let shareURL = new URL(location.href);
       if (store.initialCode !== store.sourceCode) {
-        shareURL += '&code=' + compressStr(store.sourceCode);
+        shareURL.searchParams.set('code', compressStr(store.sourceCode));
       }
       navigator.clipboard
-        .writeText(shareURL)
+        .writeText(shareURL.toString())
         .then(() => this.$message.success(this.$t('editor.share.success')))
         // PENDING
-        .catch(() => window.open(shareURL, '_blank'));
+        .catch((e) => {
+          console.error('failed to write share url to the clipboard', e);
+          window.open(shareURL, '_blank');
+        });
     },
     getOption() {
       return this.sandbox && this.sandbox.getOption();
@@ -495,25 +464,7 @@
   border-radius: 5px;
   background: #fff;
   overflow: hidden;
-
   padding: 10px;
-
-  .ec-debug-dirty-rect-container {
-    left: 10px !important;
-    top: 10px !important;
-    right: 10px !important;
-    bottom: 10px !important;
-
-    .ec-debug-dirty-rect {
-      background-color: rgba(255, 0, 0, 0.2) !important;
-      border: 1px solid red !important;
-    }
-  }
-
-  .chart-container {
-    position: relative;
-    height: 100%;
-  }
 }
 
 .render-config-container {
diff --git a/src/editor/sandbox.js b/src/editor/sandbox.js
deleted file mode 100644
index 632fe74..0000000
--- a/src/editor/sandbox.js
+++ /dev/null
@@ -1,216 +0,0 @@
-import showDebugDirtyRect from '../dep/showDebugDirtyRect';
-import seedrandom from 'seedrandom';
-
-export function createSandbox(optionUpdated) {
-  let appEnv = {};
-  let gui;
-
-  let _intervalIdList = [];
-  let _timeoutIdList = [];
-
-  const _oldSetTimeout = window.setTimeout;
-  const _oldSetInterval = window.setInterval;
-
-  function setTimeout(func, delay) {
-    var id = _oldSetTimeout(func, delay);
-    _timeoutIdList.push(id);
-    return id;
-  }
-  function setInterval(func, gap) {
-    var id = _oldSetInterval(func, gap);
-    _intervalIdList.push(id);
-    return id;
-  }
-  function _clearTimeTickers() {
-    for (var i = 0; i < _intervalIdList.length; i++) {
-      clearInterval(_intervalIdList[i]);
-    }
-    for (var i = 0; i < _timeoutIdList.length; i++) {
-      clearTimeout(_timeoutIdList[i]);
-    }
-    _intervalIdList = [];
-    _timeoutIdList = [];
-  }
-  const _events = [];
-  function _wrapOnMethods(chart) {
-    const oldOn = chart.on;
-    const oldSetOption = chart.setOption;
-    chart.on = function (eventName) {
-      const res = oldOn.apply(chart, arguments);
-      _events.push(eventName);
-      return res;
-    };
-    chart.setOption = function () {
-      const res = oldSetOption.apply(this, arguments);
-      optionUpdated && optionUpdated(chart);
-      return res;
-    };
-  }
-
-  function _clearChartEvents(chart) {
-    _events.forEach(function (eventName) {
-      if (chart) {
-        chart.off(eventName);
-      }
-    });
-
-    _events.length = 0;
-  }
-
-  let chartInstance;
-
-  return {
-    resize() {
-      if (chartInstance) {
-        chartInstance.resize();
-      }
-    },
-
-    dispose() {
-      if (chartInstance) {
-        chartInstance.dispose();
-        chartInstance = null;
-      }
-    },
-
-    getDataURL() {
-      return chartInstance.getDataURL({
-        pixelRatio: 2,
-        excludeComponents: ['toolbox']
-      });
-    },
-
-    getOption() {
-      return chartInstance.getOption();
-    },
-
-    run(el, store) {
-      if (!chartInstance) {
-        chartInstance = echarts.init(el, store.darkMode ? 'dark' : '', {
-          renderer: store.renderer,
-          useDirtyRect: store.useDirtyRect
-        });
-        if (store.useDirtyRect && store.renderer === 'canvas') {
-          try {
-            showDebugDirtyRect(chartInstance.getZr(), {
-              autoHideDelay: 500
-            });
-          } catch (e) {
-            console.error(e);
-          }
-        }
-        _wrapOnMethods(chartInstance);
-      }
-
-      // if (this.hasEditorError()) {
-      //     log(this.$t('editor.errorInEditor'), 'error');
-      //     return;
-      // }
-
-      // TODO Scope the variables in component.
-      _clearTimeTickers();
-      _clearChartEvents(chartInstance);
-      // Reset
-      appEnv.config = null;
-
-      // run the code
-
-      const compiledCode = store.runCode
-        // Replace random method
-        .replace(/Math.random\(\)/g, '__ECHARTS_EXAMPLE_RANDOM__()');
-      const echartsExampleRandom = seedrandom(store.randomSeed);
-
-      const func = new Function(
-        'myChart',
-        'app',
-        'setTimeout',
-        'setInterval',
-        'ROOT_PATH',
-        '__ECHARTS_EXAMPLE_RANDOM__',
-        'var option;\n' + compiledCode + '\nreturn option;'
-      );
-      const option = func(
-        chartInstance,
-        appEnv,
-        setTimeout,
-        setInterval,
-        store.cdnRoot,
-        echartsExampleRandom
-      );
-      let updateTime = 0;
-
-      if (option && typeof option === 'object') {
-        const startTime = +new Date();
-        chartInstance.setOption(option, true);
-        const endTime = +new Date();
-        updateTime = endTime - startTime;
-      }
-
-      if (gui) {
-        $(gui.domElement).remove();
-        gui.destroy();
-        gui = null;
-      }
-
-      if (appEnv.config) {
-        gui = new dat.GUI({
-          autoPlace: false
-        });
-        $(gui.domElement).css({
-          position: 'absolute',
-          right: 5,
-          top: 0,
-          zIndex: 1000
-        });
-        $('.right-container').append(gui.domElement);
-
-        var configParameters = appEnv.configParameters || {};
-        for (var name in appEnv.config) {
-          var value = appEnv.config[name];
-          if (name !== 'onChange' && name !== 'onFinishChange') {
-            var isColor = false;
-            // var value = obj;
-            var controller = null;
-            if (configParameters[name]) {
-              if (configParameters[name].options) {
-                controller = gui.add(
-                  appEnv.config,
-                  name,
-                  configParameters[name].options
-                );
-              } else if (configParameters[name].min != null) {
-                controller = gui.add(
-                  appEnv.config,
-                  name,
-                  configParameters[name].min,
-                  configParameters[name].max
-                );
-              }
-            }
-            if (typeof value === 'string') {
-              try {
-                var colorArr = echarts.color.parse(value);
-                isColor = !!colorArr;
-                if (isColor) {
-                  value = echarts.color.stringify(colorArr, 'rgba');
-                }
-              } catch (e) {}
-            }
-            if (!controller) {
-              controller = gui[isColor ? 'addColor' : 'add'](
-                appEnv.config,
-                name
-              );
-            }
-            appEnv.config.onChange &&
-              controller.onChange(appEnv.config.onChange);
-            appEnv.config.onFinishChange &&
-              controller.onFinishChange(appEnv.config.onFinishChange);
-          }
-        }
-      }
-
-      return updateTime;
-    }
-  };
-}
diff --git a/src/editor/sandbox/estraverse.browser b/src/editor/sandbox/estraverse.browser
new file mode 100644
index 0000000..21a92e2
--- /dev/null
+++ b/src/editor/sandbox/estraverse.browser
@@ -0,0 +1 @@
+!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).estraverse=e()}}((function(){return function e(t,n,r){function i(a,s){if(!n[a]){if(!t[a]){var l="function"==typeof require&&require;if(!s&&l)return l(a,!0);if(o)return o(a,!0);var p=new Error("Cannot find module '"+a+"'");throw p.code="MODULE_NOT_FOUND",p}var u=n[a]={exports:{}};t[a][0].call(u.exports,(function(e){return i(t[a][1][e]||e)}),u,u.exports,e,t,n,r)}return n[a].exports}for(var o="function"==typeof require&&require,a=0;a<r.length;a++)i(r[a]);return i}({1:[function(e,t,n){!function e(t){var n,r,i,o,a,s;function l(e){var t,n,r={};for(t in e)e.hasOwnProperty(t)&&(n=e[t],r[t]="object"==typeof n&&null!==n?l(n):n);return r}function p(e,t){this.parent=e,this.key=t}function u(e,t,n,r){this.node=e,this.path=t,this.wrap=n,this.ref=r}function c(){}function f(e){return null!=e&&("object"==typeof e&&"string"==typeof e.type)}function h(e,t){return(e===n.ObjectExpression||e===n.ObjectPattern)&&"properties"===t}function m(e,t){for(var n=e.length-1;n>=0;--n)if(e[n].node===t)return!0;return!1}function d(e,t){return(new c).traverse(e,t)}function y(e,t){var n;return n=function(e,t){var n,r,i,o;for(r=e.length,i=0;r;)t(e[o=i+(n=r>>>1)])?r=n:(i=o+1,r-=n+1);return i}(t,(function(t){return t.range[0]>e.range[0]})),e.extendedRange=[e.range[0],e.range[1]],n!==t.length&&(e.extendedRange[1]=t[n].range[0]),(n-=1)>=0&&(e.extendedRange[0]=t[n].range[1]),e}return n={AssignmentExpression:"AssignmentExpression",AssignmentPattern:"AssignmentPattern",ArrayExpression:"ArrayExpression",ArrayPattern:"ArrayPattern",ArrowFunctionExpression:"ArrowFunctionExpression",AwaitExpression:"AwaitExpression",BlockStatement:"BlockStatement",BinaryExpression:"BinaryExpression",BreakStatement:"BreakStatement",CallExpression:"CallExpression",CatchClause:"CatchClause",ChainExpression:"ChainExpression",ClassBody:"ClassBody",ClassDeclaration:"ClassDeclaration",ClassExpression:"ClassExpression",ComprehensionBlock:"ComprehensionBlock",ComprehensionExpression:"ComprehensionExpression",ConditionalExpression:"ConditionalExpression",ContinueStatement:"ContinueStatement",DebuggerStatement:"DebuggerStatement",DirectiveStatement:"DirectiveStatement",DoWhileStatement:"DoWhileStatement",EmptyStatement:"EmptyStatement",ExportAllDeclaration:"ExportAllDeclaration",ExportDefaultDeclaration:"ExportDefaultDeclaration",ExportNamedDeclaration:"ExportNamedDeclaration",ExportSpecifier:"ExportSpecifier",ExpressionStatement:"ExpressionStatement",ForStatement:"ForStatement",ForInStatement:"ForInStatement",ForOfStatement:"ForOfStatement",FunctionDeclaration:"FunctionDeclaration",FunctionExpression:"FunctionExpression",GeneratorExpression:"GeneratorExpression",Identifier:"Identifier",IfStatement:"IfStatement",ImportExpression:"ImportExpression",ImportDeclaration:"ImportDeclaration",ImportDefaultSpecifier:"ImportDefaultSpecifier",ImportNamespaceSpecifier:"ImportNamespaceSpecifier",ImportSpecifier:"ImportSpecifier",Literal:"Literal",LabeledStatement:"LabeledStatement",LogicalExpression:"LogicalExpression",MemberExpression:"MemberExpression",MetaProperty:"MetaProperty",MethodDefinition:"MethodDefinition",ModuleSpecifier:"ModuleSpecifier",NewExpression:"NewExpression",ObjectExpression:"ObjectExpression",ObjectPattern:"ObjectPattern",PrivateIdentifier:"PrivateIdentifier",Program:"Program",Property:"Property",PropertyDefinition:"PropertyDefinition",RestElement:"RestElement",ReturnStatement:"ReturnStatement",SequenceExpression:"SequenceExpression",SpreadElement:"SpreadElement",Super:"Super",SwitchStatement:"SwitchStatement",SwitchCase:"SwitchCase",TaggedTemplateExpression:"TaggedTemplateExpression",TemplateElement:"TemplateElement",TemplateLiteral:"TemplateLiteral",ThisExpression:"ThisExpression",ThrowStatement:"ThrowStatement",TryStatement:"TryStatement",UnaryExpression:"UnaryExpression",UpdateExpression:"UpdateExpression",VariableDeclaration:"VariableDeclaration",VariableDeclarator:"VariableDeclarator",WhileStatement:"WhileStatement",WithStatement:"WithStatement",YieldExpression:"YieldExpression"},i={AssignmentExpression:["left","right"],AssignmentPattern:["left","right"],ArrayExpression:["elements"],ArrayPattern:["elements"],ArrowFunctionExpression:["params","body"],AwaitExpression:["argument"],BlockStatement:["body"],BinaryExpression:["left","right"],BreakStatement:["label"],CallExpression:["callee","arguments"],CatchClause:["param","body"],ChainExpression:["expression"],ClassBody:["body"],ClassDeclaration:["id","superClass","body"],ClassExpression:["id","superClass","body"],ComprehensionBlock:["left","right"],ComprehensionExpression:["blocks","filter","body"],ConditionalExpression:["test","consequent","alternate"],ContinueStatement:["label"],DebuggerStatement:[],DirectiveStatement:[],DoWhileStatement:["body","test"],EmptyStatement:[],ExportAllDeclaration:["source"],ExportDefaultDeclaration:["declaration"],ExportNamedDeclaration:["declaration","specifiers","source"],ExportSpecifier:["exported","local"],ExpressionStatement:["expression"],ForStatement:["init","test","update","body"],ForInStatement:["left","right","body"],ForOfStatement:["left","right","body"],FunctionDeclaration:["id","params","body"],FunctionExpression:["id","params","body"],GeneratorExpression:["blocks","filter","body"],Identifier:[],IfStatement:["test","consequent","alternate"],ImportExpression:["source"],ImportDeclaration:["specifiers","source"],ImportDefaultSpecifier:["local"],ImportNamespaceSpecifier:["local"],ImportSpecifier:["imported","local"],Literal:[],LabeledStatement:["label","body"],LogicalExpression:["left","right"],MemberExpression:["object","property"],MetaProperty:["meta","property"],MethodDefinition:["key","value"],ModuleSpecifier:[],NewExpression:["callee","arguments"],ObjectExpression:["properties"],ObjectPattern:["properties"],PrivateIdentifier:[],Program:["body"],Property:["key","value"],PropertyDefinition:["key","value"],RestElement:["argument"],ReturnStatement:["argument"],SequenceExpression:["expressions"],SpreadElement:["argument"],Super:[],SwitchStatement:["discriminant","cases"],SwitchCase:["test","consequent"],TaggedTemplateExpression:["tag","quasi"],TemplateElement:[],TemplateLiteral:["quasis","expressions"],ThisExpression:[],ThrowStatement:["argument"],TryStatement:["block","handler","finalizer"],UnaryExpression:["argument"],UpdateExpression:["argument"],VariableDeclaration:["declarations"],VariableDeclarator:["id","init"],WhileStatement:["test","body"],WithStatement:["object","body"],YieldExpression:["argument"]},r={Break:o={},Skip:a={},Remove:s={}},p.prototype.replace=function(e){this.parent[this.key]=e},p.prototype.remove=function(){return Array.isArray(this.parent)?(this.parent.splice(this.key,1),!0):(this.replace(null),!1)},c.prototype.path=function(){var e,t,n,r,i;function o(e,t){if(Array.isArray(t))for(n=0,r=t.length;n<r;++n)e.push(t[n]);else e.push(t)}if(!this.__current.path)return null;for(i=[],e=2,t=this.__leavelist.length;e<t;++e)o(i,this.__leavelist[e].path);return o(i,this.__current.path),i},c.prototype.type=function(){return this.current().type||this.__current.wrap},c.prototype.parents=function(){var e,t,n;for(n=[],e=1,t=this.__leavelist.length;e<t;++e)n.push(this.__leavelist[e].node);return n},c.prototype.current=function(){return this.__current.node},c.prototype.__execute=function(e,t){var n,r;return r=void 0,n=this.__current,this.__current=t,this.__state=null,e&&(r=e.call(this,t.node,this.__leavelist[this.__leavelist.length-1].node)),this.__current=n,r},c.prototype.notify=function(e){this.__state=e},c.prototype.skip=function(){this.notify(a)},c.prototype.break=function(){this.notify(o)},c.prototype.remove=function(){this.notify(s)},c.prototype.__initialize=function(e,t){this.visitor=t,this.root=e,this.__worklist=[],this.__leavelist=[],this.__current=null,this.__state=null,this.__fallback=null,"iteration"===t.fallback?this.__fallback=Object.keys:"function"==typeof t.fallback&&(this.__fallback=t.fallback),this.__keys=i,t.keys&&(this.__keys=Object.assign(Object.create(this.__keys),t.keys))},c.prototype.traverse=function(e,t){var n,r,i,s,l,p,c,d,y,x,_,E;for(this.__initialize(e,t),E={},n=this.__worklist,r=this.__leavelist,n.push(new u(e,null,null,null)),r.push(new u(null,null,null,null));n.length;)if((i=n.pop())!==E){if(i.node){if(p=this.__execute(t.enter,i),this.__state===o||p===o)return;if(n.push(E),r.push(i),this.__state===a||p===a)continue;if(l=(s=i.node).type||i.wrap,!(x=this.__keys[l])){if(!this.__fallback)throw new Error("Unknown node type "+l+".");x=this.__fallback(s)}for(d=x.length;(d-=1)>=0;)if(_=s[c=x[d]])if(Array.isArray(_)){for(y=_.length;(y-=1)>=0;)if(_[y]&&!m(r,_[y])){if(h(l,x[d]))i=new u(_[y],[c,y],"Property",null);else{if(!f(_[y]))continue;i=new u(_[y],[c,y],null,null)}n.push(i)}}else if(f(_)){if(m(r,_))continue;n.push(new u(_,c,null,null))}}}else if(i=r.pop(),p=this.__execute(t.leave,i),this.__state===o||p===o)return},c.prototype.replace=function(e,t){var n,r,i,l,c,m,d,y,x,_,E,g,S;function b(e){var t,r,i,o;if(e.ref.remove())for(r=e.ref.key,o=e.ref.parent,t=n.length;t--;)if((i=n[t]).ref&&i.ref.parent===o){if(i.ref.key<r)break;--i.ref.key}}for(this.__initialize(e,t),E={},n=this.__worklist,r=this.__leavelist,m=new u(e,null,null,new p(g={root:e},"root")),n.push(m),r.push(m);n.length;)if((m=n.pop())!==E){if(void 0!==(c=this.__execute(t.enter,m))&&c!==o&&c!==a&&c!==s&&(m.ref.replace(c),m.node=c),this.__state!==s&&c!==s||(b(m),m.node=null),this.__state===o||c===o)return g.root;if((i=m.node)&&(n.push(E),r.push(m),this.__state!==a&&c!==a)){if(l=i.type||m.wrap,!(x=this.__keys[l])){if(!this.__fallback)throw new Error("Unknown node type "+l+".");x=this.__fallback(i)}for(d=x.length;(d-=1)>=0;)if(_=i[S=x[d]])if(Array.isArray(_)){for(y=_.length;(y-=1)>=0;)if(_[y]){if(h(l,x[d]))m=new u(_[y],[S,y],"Property",new p(_,y));else{if(!f(_[y]))continue;m=new u(_[y],[S,y],null,new p(_,y))}n.push(m)}}else f(_)&&n.push(new u(_,S,null,new p(i,S)))}}else if(m=r.pop(),void 0!==(c=this.__execute(t.leave,m))&&c!==o&&c!==a&&c!==s&&m.ref.replace(c),this.__state!==s&&c!==s||b(m),this.__state===o||c===o)return g.root;return g.root},t.Syntax=n,t.traverse=d,t.replace=function(e,t){return(new c).replace(e,t)},t.attachComments=function(e,t,n){var i,o,a,s,p=[];if(!e.range)throw new Error("attachComments needs range information");if(!n.length){if(t.length){for(a=0,o=t.length;a<o;a+=1)(i=l(t[a])).extendedRange=[0,e.range[0]],p.push(i);e.leadingComments=p}return e}for(a=0,o=t.length;a<o;a+=1)p.push(y(l(t[a]),n));return s=0,d(e,{enter:function(e){for(var t;s<p.length&&!((t=p[s]).extendedRange[1]>e.range[0]);)t.extendedRange[1]===e.range[0]?(e.leadingComments||(e.leadingComments=[]),e.leadingComments.push(t),p.splice(s,1)):s+=1;return s===p.length?r.Break:p[s].extendedRange[0]>e.range[1]?r.Skip:void 0}}),s=0,d(e,{leave:function(e){for(var t;s<p.length&&(t=p[s],!(e.range[1]<t.extendedRange[0]));)e.range[1]===t.extendedRange[0]?(e.trailingComments||(e.trailingComments=[]),e.trailingComments.push(t),p.splice(s,1)):s+=1;return s===p.length?r.Break:p[s].extendedRange[0]>e.range[1]?r.Skip:void 0}}),e},t.VisitorKeys=i,t.VisitorOption=r,t.Controller=c,t.cloneEnvironment=function(){return e({})},t}(n)},{}]},{},[1])(1)}));
\ No newline at end of file
diff --git a/src/editor/sandbox/handleLoop.js b/src/editor/sandbox/handleLoop.js
new file mode 100644
index 0000000..8c4013e
--- /dev/null
+++ b/src/editor/sandbox/handleLoop.js
@@ -0,0 +1,86 @@
+/**
+ * This function is used to prevent the page from getting stuck
+ * for the infinite loop in the user code.
+ *
+ * @param {string} code the source code
+ */
+export default function handleLoop(code) {
+  let AST;
+  try {
+    AST = acorn.parse(code, {
+      ecmaVersion: 'latest',
+      sourceType: 'script'
+    });
+  } catch (e) {
+    console.error('failed to parse code', e);
+    return code;
+  }
+
+  /**
+   * Temporarily store the range of positions where the code needs to be inserted
+   */
+  const fragments = [];
+  /**
+   * loopID is used to mark the loop
+   */
+  let loopID = 1;
+  /**
+   * Mark the code that needs to be inserted when looping
+   */
+  const insertCode = {
+    setMonitor: 'LoopController.loopMonitor(%d);',
+    delMonitor: ';LoopController.delLoop(%d);'
+  };
+
+  /**
+   * Traverse the AST to find the loop position
+   */
+  estraverse.traverse(AST, {
+    enter(node) {
+      switch (node.type) {
+        case 'WhileStatement':
+        case 'DoWhileStatement':
+        case 'ForStatement':
+        case 'ForInStatement':
+        case 'ForOfStatement':
+          /**
+           * Gets the head and tail of the loop body
+           */
+          let { start, end } = node.body;
+          start++;
+          let pre = insertCode.setMonitor.replace('%d', loopID);
+          let aft = '';
+          /**
+           * If the body of the loop is not enveloped by {} and is indented, we need to manually add {}
+           */
+          if (node.body.type !== 'BlockStatement') {
+            pre = '{' + pre;
+            aft = '}';
+            --start;
+          }
+          fragments.push({ pos: start, str: pre });
+          fragments.push({ pos: end, str: aft });
+          fragments.push({
+            pos: node.end,
+            str: insertCode.delMonitor.replace('%d', loopID)
+          });
+          ++loopID;
+          break;
+        default:
+          break;
+      }
+    }
+  });
+
+  /**
+   * Insert code to corresponding position
+   */
+  fragments
+    .sort((a, b) => b.pos - a.pos)
+    .forEach((fragment) => {
+      code =
+        code.slice(0, fragment.pos) + fragment.str + code.slice(fragment.pos);
+    });
+
+  return code;
+}
diff --git a/src/editor/sandbox/index.js b/src/editor/sandbox/index.js
new file mode 100644
index 0000000..6d5a257
--- /dev/null
+++ b/src/editor/sandbox/index.js
@@ -0,0 +1,101 @@
+import srcdoc from './srcdoc.html';
+import handleLoop from './handleLoop';
+import setup from './setup';
+import loopController from 'raw-loader!./loopController';
+import showDebugDirtyRect from 'raw-loader!../../dep/showDebugDirtyRect';
+import estraverse from 'raw-loader!./estraverse.browser';
+
+export function createSandbox(
+  container,
+  scripts,
+  onload,
+  onerror,
+  onCodeError,
+  onOptionUpdated
+) {
+  scripts = scripts || [];
+  scripts.push(
+    { content: estraverse },
+    { content: loopController },
+    {
+      content: `
+        (function(){
+          ${handleLoop}
+          ${showDebugDirtyRect}
+          ${setup}
+          setup()
+        })()
+      `
+    }
+  );
+
+  const sandbox = document.createElement('iframe');
+  sandbox.setAttribute(
+    'sandbox',
+    [
+      'allow-forms',
+      'allow-modals',
+      'allow-pointer-lock',
+      'allow-popups',
+      'allow-same-origin',
+      'allow-scripts',
+      'allow-top-navigation-by-user-activation',
+      'allow-downloads'
+    ].join(' ')
+  );
+  sandbox.style.cssText = 'width:100%;height:100%;border:none;background:none';
+  sandbox.srcdoc = srcdoc.replace(
+    '<!--SCRIPTS-->',
+    scripts
+      .map((script) =>
+        script.content
+          ? `<script>${script.content}</script>`
+          : `<script src="${script.src}"></script>`
+      )
+      .join('')
+  );
+  sandbox.onload = onload;
+  sandbox.onerror = onerror;
+  container.appendChild(sandbox);
+
+  function hanldeMessage(e) {
+    const evt = e.data.evt;
+    console.log('event from sandbox', evt);
+    switch (evt) {
+      case 'optionUpdated':
+        onOptionUpdated(e.data.option, e.data.updateTime);
+        break;
+      // case 'error':
+      // case 'unhandledRejection':
+      //   onerror();
+      //   break;
+      case 'codeError':
+        onCodeError();
+        break;
+      default:
+        break;
+    }
+  }
+
+  function sendMessage(action, argumentMap) {
+    sandbox.contentWindow.postMessage({ action, ...argumentMap }, '*');
+  }
+
+  window.addEventListener('message', hanldeMessage, false);
+
+  return {
+    dispose() {
+      sendMessage('dispose');
+      window.removeEventListener('message', hanldeMessage);
+    },
+    run(store, recreateInstance) {
+      sendMessage('run', { store, recreateInstance });
+    },
+    screenshot(filename) {
+      sendMessage('screenshot', { filename });
+    },
+    getOption() {
+      sendMessage('getOption');
+    }
+  };
+}
diff --git a/src/editor/sandbox/loopController.js b/src/editor/sandbox/loopController.js
new file mode 100644
index 0000000..96ce95f
--- /dev/null
+++ b/src/editor/sandbox/loopController.js
@@ -0,0 +1,60 @@
+const LoopController = {
+  _config: {
+    maxExecTimePerLoop: 3e3,
+    maxLoopCount: 1e6
+  },
+  loopMap: new Map(),
+  initLoop(loopID) {
+    this.setLoop(loopID, {
+      isInit: true,
+      totalExecTime: 0,
+      startTime: Date.now(),
+      count: 0
+    });
+  },
+  getLoop(loopID) {
+    return this.loopMap.get(loopID);
+  },
+  setLoop(loopID, loop) {
+    this.loopMap.set(loopID, loop);
+  },
+  delLoop(loopID) {
+    this.loopMap.delete(loopID);
+  },
+  clearLoops() {
+    this.loopMap.clear();
+  },
+  exitLoop(loopID) {
+    this.delLoop(loopID);
+  },
+  calcLoop(loopID) {
+    if (this.loopMap.has(loopID)) {
+      let { isInit, totalExecTime, startTime, count } = this.getLoop(loopID);
+      if (isInit) {
+        totalExecTime = Date.now() - startTime;
+        count++;
+        this.setLoop(loopID, {
+          isInit,
+          totalExecTime,
+          startTime,
+          count
+        });
+      } else {
+        this.initLoop(loopID);
+      }
+    } else {
+      this.initLoop(loopID);
+    }
+  },
+  loopMonitor(loopID) {
+    this.calcLoop(loopID);
+    const loop = this.getLoop(loopID);
+    const { maxExecTimePerLoop, maxLoopCount } = this._config;
+    if (loop.totalExecTime > maxExecTimePerLoop && loop.count > maxLoopCount) {
+      this.clearLoops();
+      throw new Error(
+        'The loop executes so many times that ECharts has to exit the loop in case the page gets stuck'
+      );
+    }
+  }
+};
diff --git a/src/editor/sandbox/setup.js b/src/editor/sandbox/setup.js
new file mode 100644
index 0000000..54880ed
--- /dev/null
+++ b/src/editor/sandbox/setup.js
@@ -0,0 +1,248 @@
+export default function setup() {
+  const sendMessage = function (payload) {
+    console.log('sendMessage', payload);
+    parent.postMessage(payload, '*');
+  };
+
+  const chartStyleEl = document.head.querySelector('#chart-styles');
+
+  const intervalIdList = [];
+  const timeoutIdList = [];
+
+  const nativeSetTimeout = window.setTimeout;
+  const nativeSetInterval = window.setInterval;
+
+  function setTimeout(func, delay) {
+    const id = nativeSetTimeout(func, delay);
+    timeoutIdList.push(id);
+    return id;
+  }
+
+  function setInterval(func, interval) {
+    const id = nativeSetInterval(func, interval);
+    intervalIdList.push(id);
+    return id;
+  }
+
+  function clearTimers() {
+    intervalIdList.forEach(clearInterval);
+    timeoutIdList.forEach(clearTimeout);
+    intervalIdList.length = 0;
+    timeoutIdList.length = 0;
+  }
+
+  const chartEvents = [];
+
+  function wrapChartMethods(chart) {
+    const nativeOn = chart.on;
+    const nativeSetOption = chart.setOption;
+
+    chart.on = function (eventName) {
+      const res = nativeOn.apply(chart, arguments);
+      chartEvents.push(eventName);
+      return res;
+    };
+
+    chart.setOption = function () {
+      const startTime = performance.now();
+      const res = nativeSetOption.apply(this, arguments);
+      const endTime = performance.now();
+      sendMessage({
+        evt: 'optionUpdated',
+        option: chart.getOption(),
+        updateTime: endTime - startTime
+      });
+      return res;
+    };
+  }
+
+  function clearChartEvents(chart) {
+    chart && chartEvents.forEach(chart.off);
+    chartEvents.length = 0;
+  }
+
+  let appStore;
+  let chartInstance;
+  let appEnv = {};
+  let gui;
+
+  const api = {
+    dispose() {
+      if (chartInstance) {
+        chartInstance.dispose();
+        chartInstance = null;
+        appStore = null;
+      }
+    },
+
+    screenshot({ filename }) {
+      console.log('screenshot');
+      const dataURL = chartInstance.getDataURL({
+        excludeComponents: ['toolbox']
+      });
+      const $a = document.createElement('a');
+      $a.download = filename;
+      $a.target = '_blank';
+      $a.href = dataURL;
+      $a.click();
+    },
+
+    getOption() {
+      return chartInstance.getOption();
+    },
+
+    run({ store, recreateInstance }) {
+      if (!chartInstance || recreateInstance) {
+        this.dispose();
+        chartInstance = echarts.init(
+          document.getElementById('chart-container'),
+          store.darkMode ? 'dark' : '',
+          {
+            renderer: store.renderer,
+            useDirtyRect: store.useDirtyRect
+          }
+        );
+        if (store.useDirtyRect && store.renderer === 'canvas') {
+          try {
+            showDebugDirtyRect(chartInstance.getZr(), {
+              autoHideDelay: 500
+            });
+          } catch (e) {
+            console.error('failed to show debug dirty rect', e);
+          }
+        }
+        window.addEventListener('resize', chartInstance.resize);
+        wrapChartMethods(chartInstance);
+      }
+
+      // TODO Scope the variables in component.
+      clearTimers();
+      clearChartEvents(chartInstance);
+      // Reset
+      appEnv.config = null;
+      appStore = store;
+
+      try {
+        // run the code
+        const compiledCode = store.runCode
+          // Replace random method
+          .replace(/Math.random\(\)/g, '__ECHARTS_EXAMPLE_RANDOM__()');
+        const echartsExampleRandom = Math.seedrandom(store.randomSeed);
+
+        const func = new Function(
+          'myChart',
+          'app',
+          'setTimeout',
+          'setInterval',
+          'ROOT_PATH',
+          '__ECHARTS_EXAMPLE_RANDOM__',
+          'top',
+          'parent',
+          // PENDING: create a single panel for CSS code?
+          'var css, option;\n' +
+            handleLoop(compiledCode) +
+            '\nreturn [option, css];'
+        );
+
+        const res = func(
+          chartInstance,
+          appEnv,
+          setTimeout,
+          setInterval,
+          store.cdnRoot,
+          // prevent someone from trying to close the parent window via top/parent.close()
+          // or any other unexpected and dangerous behaviors
+          void 0,
+          void 0,
+          echartsExampleRandom
+        );
+        chartStyleEl.textContent = res[1] || '';
+
+        const option = res[0];
+        echarts.util.isObject(option) && chartInstance.setOption(option, true);
+      } catch (e) {
+        console.error('failed to run code', e);
+        sendMessage({ evt: 'codeError' });
+      }
+
+      if (gui) {
+        $(gui.domElement).remove();
+        gui.destroy();
+        gui = null;
+      }
+
+      if (appEnv.config) {
+        gui = new dat.GUI({ autoPlace: false });
+        $(gui.domElement).css({
+          position: 'absolute',
+          right: 5,
+          top: 0,
+          zIndex: 1000
+        });
+        document.body.append(gui.domElement);
+
+        const configParams = appEnv.configParams || {};
+        const config = appEnv.config;
+        for (const name in config) {
+          const value = config[name];
+          if (name !== 'onChange' && name !== 'onFinishChange') {
+            let isColor;
+            let controller;
+            const configVal = configParams[name];
+            if (configVal) {
+              if (configVal.options) {
+                controller = gui.add(config, name, configVal.options);
+              } else if (configVal.min != null) {
+                controller = gui.add(
+                  config,
+                  name,
+                  configVal.min,
+                  configVal.max
+                );
+              }
+            }
+            if (typeof value === 'string') {
+              try {
+                const colorArr = echarts.color.parse(value);
+                if ((isColor = !!colorArr)) {
+                  value = echarts.color.stringify(colorArr, 'rgba');
+                }
+              } catch (e) {}
+            }
+            if (!controller) {
+              controller = gui[isColor ? 'addColor' : 'add'](config, name);
+            }
+            config.onChange && controller.onChange(config.onChange);
+            config.onFinishChange &&
+              controller.onFinishChange(config.onFinishChange);
+          }
+        }
+      }
+    }
+  };
+
+  echarts.registerPreprocessor(function (option) {
+    if (appStore.enableDecal) {
+      option.aria = option.aria || {};
+      option.aria.decal = option.aria.decal || {};
+      option.aria.decal.show = true;
+      option.aria.show = option.aria.enabled = true;
+    }
+  });
+
+  function handleMessage(ev) {
+    console.log('handle message in sandbox', ev);
+    // const { action, ...args } = ev.data;
+    const action = ev.data.action;
+    delete ev.data.action;
+    typeof api[action] === 'function' && api[action].apply(api, [ev.data]);
+  }
+
+  window.addEventListener('message', handleMessage, false);
+  window.addEventListener('error', function () {
+    sendMessage({ evt: 'error' });
+  });
+  window.addEventListener('unhandledrejection', function () {
+    sendMessage({ evt: 'unhandledRejection' });
+  });
+}
diff --git a/src/editor/sandbox/srcdoc.html b/src/editor/sandbox/srcdoc.html
new file mode 100644
index 0000000..c5d8851
--- /dev/null
+++ b/src/editor/sandbox/srcdoc.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <style>
+      * {
+        margin: 0;
+        padding: 0;
+      }
+      body {
+        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
+          Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+      }
+      #chart-container {
+        position: relative;
+        height: 100vh;
+        overflow: hidden;
+      }
+      .ec-debug-dirty-rect {
+        background-color: rgba(255, 0, 0, 0.2) !important;
+        border: 1px solid red !important;
+        box-sizing: border-box;
+      }
+    </style>
+    <style id="chart-styles"></style>
+  </head>
+  <body>
+    <div id="chart-container"></div>
+    <script src="https://cdn.jsdelivr.net/npm/jquery"></script>
+    <script src="https://cdn.jsdelivr.net/npm/seedrandom@3.0.5/seedrandom.min.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/acorn@8.7.1/dist/acorn.js"></script>
+    <!--SCRIPTS-->
+  </body>
+</html>