Repository: kinegratii/django-echarts (language: python)

Issue:
v0.6.x Devlopment Note(2023)
## 1 运行环境支持

**django-echarts v0.6.0默认使用 pyecharts 2.x 作为底层渲染库，其最大的变化是 echarts 版本由 4.8 升级至 5.4。**

django-echarts 依赖库更新如下：

```
pyecharts 1.9 -> 2.0
typing_extensions 4.0 -> 4.5
borax 3.5 -> 4.0
```



## 2 图表构建函数支持自定义参数

图表构建函数支持自定义参数，可以根据不同的输入值，加载不同的数据，从而形成多个图表。

以之前的时间轴图表为例子，每次只显示一个年份的数据图表。

```python
@site_obj.register_chart(title='{year}年福建省家庭户类型组成')
def yearly_family(year: int):
    family_types = [
        '一人户', '二人户', '三人户', '四人户', '五人户', '六人户', '七人户', '八人户', '九人户', '十人及其以上'
    ]
    data = [
        [1982, 7.7, 8.2, 12.2, 17.1, 18.4, 14.7, 10.1, 11.6, 0, 0],
        [1990, 5.8, 8.6, 16.8, 23.6, 21.4, 11.8, 5.9, 2.9, 1.4, 1.8],
        [2000, 9.1, 15.5, 25.4, 24.7, 15.8, 5.9, 2.2, 0.8, 0.3, 0.3],
        [2010, 12.1, 17.2, 24.3, 21.7, 13.7, 6.4, 2.6, 1.1, 0.5, 0.4],
        [2020, 27.3, 26.3, 19.4, 14.2, 6.9, 4, 1.1, 0.4, 0.2, 0.2]
    ]
    yearly_data = {item[0]: item[1:] for item in data}
    if year not in yearly_data:
        raise ChartDoesNotExist(f'year={year}')
    year_data = yearly_data[year]
    bar = (
        Bar()
            .add_xaxis(family_types).add_yaxis('百分比(%)', year_data)
            .set_global_opts(title_opts=opts.TitleOpts("福建省家庭户类型构成-{}年".format(year)))
    )
    return bar
```

说明：

- year为自定义参数，如未声明类型，默认为 `str`。
- 如果传入的year找不到对应的数据，抛出 `ChartDoesNotExist` 异常，web页面显示“找不到图表”的页面。
- title参数可以使用类似 `{year}`的模板字符串。

该图表对应的相关引用配置如下(以year=2020例子)：

| 项目       | 值                                                           | 备注                                         |
| ---------- | ------------------------------------------------------------ | -------------------------------------------- |
| 图表URL    | `/chart/year/2020/`                                          | 格式：`<图表slug>/<参数名称1>/<参数值1>/...` |
| python引用 | `reverse_lazy('dje_single_chart','yearly_family','year/2020/')` |                                              |
| 模板引用   | `{% url 'dje_single_chart' 'yearly_family', 'year/2020/' %}` |                                              |

## 2 新增声明式导航栏配置

django-echarts 0.6 新增了使用字典方式一次性配置导航栏配置。

```python
# site_config.py

nav_config = {
    'nav_left': ['home', 'list'],
    'nav_right': ['settings'],
    'nav_footer': [
        {'text': '项目主页', 'slug': 'project_url', 'url': 'https://github.com/kinegratii/django-echarts'}
    ]
}

# urls.py

site_obj.config_nav(nav_config)
```

## 3 DJESite类移除导航栏相关方法

可使用 `DJESite.nav` 相关方法，参见下面的Nav API章节。

- `DJESite.add_left_link`
- `DJESite.add_right_link`
- `DJESite.add_footer_link`
- `DJESite.add_menu_item`

## 4 ChartInfo和ChartInfoManager

- 类 `ChartInfo` 增加 `is_bound` 布尔值属性，如果对应图表注册函数携带任何参数，则设置为 `False`，数据格式化后变为`True`。
- `ChartInfoManagerMixin.get_or_none` 增加 `uri` 参数。 
- 在 `is_bound = False` 情况下，body title description的转义处理
Feature Custom Map (geojson & svg)



Discussion / hints:

Fix patch:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
deleted file mode 100644
index 1149ba5..0000000
--- a/.github/dependabot.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-version: 2
-updates:
-  - package-ecosystem: pip
-    directory: "/"
-    schedule:
-      interval: daily
-    target-branch: "dev"
\ No newline at end of file
diff --git a/README.md b/README.md
index 7aee3c1..5eb5f9e 100644
--- a/README.md
+++ b/README.md
@@ -25,11 +25,13 @@ django-echarts 主要提供了以下的内容：
 
 ## 安装(Install)
 
-django-echarts包运行环境要求如下：
+django-echarts的运行环境要求如下：
 
-| django-echarts版本系列 | pyecharts | django | python | 备注 |
-| ------ | ------ | ------ | ----- | ----- |
-| 0.5.x | 1.9+ | 2.0+ | 3.7+ | 开发维护中 |
+| django-echarts版本系列 | pyecharts & echarts | django | python |
+| ------ | ------ | ----- | ----- |
+| 0.6.x | 1.9 / 4.8.0 | 2.0 - 4.1 | 3.7+ |
+|  | 2.0 / 5.4.1 | 2.0 - 4.1 | 3.7+ |
+| 0.5.x | 1.9 | 2.0 - 4.0 | 3.7+ |
 
 可以使用 pip 命令安装。
 
@@ -37,6 +39,12 @@ django-echarts包运行环境要求如下：
 pip install django-echarts
 ```
 
+在 *requirements.txt* 引用 django-charts时，推荐使用 **固定次版本号** 的方式。
+
+```
+django-echarts~=0.6
+```
+
 ## 3分钟上手(Quickstart in 3 Minutes)
 
 **1.** 创建Django项目。
diff --git a/django_echarts/__init__.py b/django_echarts/__init__.py
index 8826894..2792302 100644
--- a/django_echarts/__init__.py
+++ b/django_echarts/__init__.py
@@ -2,5 +2,5 @@
 A django app for Echarts integration with pyecharts as chart builder.
 """
 
-__version__ = '0.5.2'
+__version__ = '0.6.0'
 __author__ = 'kinegratii'
diff --git a/django_echarts/class_typing.py b/django_echarts/class_typing.py
new file mode 100644
index 0000000..57dae7a
--- /dev/null
+++ b/django_echarts/class_typing.py
@@ -0,0 +1,9 @@
+from typing import List, Union, TypedDict
+
+from typing_extensions import NotRequired
+
+
+class TNavConfig(TypedDict):
+    nav_left: NotRequired[List[Union[str, dict]]]
+    nav_right: NotRequired[List[Union[str, dict]]]
+    nav_footer: NotRequired[List[Union[str, dict]]]
diff --git a/django_echarts/contrib/bootstrap3/templates/base.html b/django_echarts/contrib/bootstrap3/templates/base.html
index 8de358c..c936f4b 100644
--- a/django_echarts/contrib/bootstrap3/templates/base.html
+++ b/django_echarts/contrib/bootstrap3/templates/base.html
@@ -48,7 +48,7 @@
             <!-- Collect the nav links, forms, and other content for toggling -->
             <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
                 <ul class="nav navbar-nav">
-                    {% for menu2 in nav.menus %}
+                    {% for menu2 in nav.left_menus %}
                         {% if menu2.children %}
                             <li class="dropdown">
                                 <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
@@ -71,10 +71,26 @@
                     {% endfor %}
                 </ul>
                 <ul class="nav navbar-nav navbar-right">
-                    {% for item in nav.links %}
-                        <li>
-                            {% dw_widget item %}
-                        </li>
+                    {% for menu2 in nav.right_menus %}
+                        {% if menu2.children %}
+                            <li class="dropdown">
+                                <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
+                                   aria-haspopup="true"
+                                   aria-expanded="false">{{ menu2.text }} <span class="caret"></span></a>
+                                <ul class="dropdown-menu">
+                                    {% for item in menu2.children %}
+                                        {% if item.after_separator %}
+                                            <li role="separator" class="divider"></li>
+                                        {% endif %}
+                                        <li> {% dw_widget item %}</li>
+                                    {% endfor %}
+                                </ul>
+                            </li>
+                        {% else %}
+                            <li>
+                                {% dw_widget menu2 %}
+                            </li>
+                        {% endif %}
                     {% endfor %}
                 </ul>
             </div><!-- /.navbar-collapse -->
diff --git a/django_echarts/contrib/bootstrap3/templates/chart_parametric.html b/django_echarts/contrib/bootstrap3/templates/chart_parametric.html
new file mode 100644
index 0000000..fc97a1d
--- /dev/null
+++ b/django_echarts/contrib/bootstrap3/templates/chart_parametric.html
@@ -0,0 +1,8 @@
+{% extends 'base.html' %}
+{% load echarts %}
+
+{% block main_content %}
+    {{ chart_info.title }}
+
+    {% dw_widget link_group %}
+{% endblock %}
\ No newline at end of file
diff --git a/django_echarts/contrib/bootstrap3/templates/items_grid.html b/django_echarts/contrib/bootstrap3/templates/items_grid.html
index d2c02b3..c0d9938 100644
--- a/django_echarts/contrib/bootstrap3/templates/items_grid.html
+++ b/django_echarts/contrib/bootstrap3/templates/items_grid.html
@@ -1,9 +1,10 @@
+{% load echarts %}
 <div class="row">
     {% for chart_info in chart_info_list %}
         <div class="col-md-4">
             <div class="panel panel-default">
                 <div class="panel-heading">
-                    <a href="{% url 'dje_chart_single' chart_info.name %}">{{ chart_info.title }}</a>
+                    <a href="{% url_single_chart chart_info.uri %}">{{ chart_info.title }}</a>
                     {% if chart_info.tags %}
                         <span class="pull-right">
                                                 {% for tag in chart_info.tags %}
diff --git a/django_echarts/contrib/bootstrap3/templates/items_list.html b/django_echarts/contrib/bootstrap3/templates/items_list.html
index 32d92d8..2fabc39 100644
--- a/django_echarts/contrib/bootstrap3/templates/items_list.html
+++ b/django_echarts/contrib/bootstrap3/templates/items_list.html
@@ -1,6 +1,7 @@
+{% load echarts %}
 <div class="list-group">
     {% for chart_info in chart_info_list %}
-        <a href="{% url 'dje_chart_single' chart_info.name %}" class="list-group-item">
+        <a href="{% url_single_chart chart_info.uri %}" class="list-group-item">
             <h4 class="list-group-item-heading">{{ chart_info.title }}</h4>
             <p class="list-group-item-text">{{ chart_info.description|default:"暂无描述" }}</p>
         </a>
diff --git a/django_echarts/contrib/bootstrap3/templates/widgets/link_group.html b/django_echarts/contrib/bootstrap3/templates/widgets/link_group.html
new file mode 100644
index 0000000..42c853f
--- /dev/null
+++ b/django_echarts/contrib/bootstrap3/templates/widgets/link_group.html
@@ -0,0 +1,5 @@
+<div class="list-group">
+    {% for link in widget %}
+        <a href="{{ link.url }}" class="list-group-item">{{ link.text }}</a>
+    {% endfor %}
+</div>
\ No newline at end of file
diff --git a/django_echarts/contrib/bootstrap5/templates/base.html b/django_echarts/contrib/bootstrap5/templates/base.html
index 0c175b4..85686c5 100644
--- a/django_echarts/contrib/bootstrap5/templates/base.html
+++ b/django_echarts/contrib/bootstrap5/templates/base.html
@@ -38,7 +38,7 @@
             </button>
             <div class="collapse navbar-collapse" id="navbarNav">
                 <ul class="navbar-nav">
-                    {% for menu2 in nav.menus %}
+                    {% for menu2 in nav.left_menus %}
                         {% if menu2.children %}
                             <li class="nav-item dropdown">
                                 <a id="{{ menu2.slug }}" href="#" class="nav-link dropdown-toggle"
@@ -62,8 +62,27 @@
                     {% endfor %}
                 </ul>
                 <ul class="navbar-nav ms-auto">
-                    {% for item in nav.links %}
-                        <li class="nav-item">{% dw_widget item class_="nav-link" %}</li>
+                    {% for menu2 in nav.right_menus %}
+                        {% if menu2.children %}
+                            <li class="nav-item dropdown">
+                                <a id="{{ menu2.slug }}" href="#" class="nav-link dropdown-toggle"
+                                   role="button" data-bs-toggle="dropdown">{{ menu2.text }} </a>
+                                <ul class="dropdown-menu" aria-labelledby="{{ menu2.slug }}">
+                                    {% for item in menu2.children %}
+                                        {% if item.after_separator %}
+                                            <li>
+                                                <hr class="dropdown-divider">
+                                            </li>
+                                        {% endif %}
+                                        <li>{% dw_widget item class_="dropdown-item" %}</li>
+                                    {% endfor %}
+                                </ul>
+                            </li>
+                        {% else %}
+                            <li class="nav-item">
+                            {% dw_widget menu2 class_="nav-link" %}
+                            </li>
+                        {% endif %}
                     {% endfor %}
                 </ul>
             </div><!-- /.navbar-collapse -->
diff --git a/django_echarts/contrib/bootstrap5/templates/chart_parametric.html b/django_echarts/contrib/bootstrap5/templates/chart_parametric.html
new file mode 100644
index 0000000..fc97a1d
--- /dev/null
+++ b/django_echarts/contrib/bootstrap5/templates/chart_parametric.html
@@ -0,0 +1,8 @@
+{% extends 'base.html' %}
+{% load echarts %}
+
+{% block main_content %}
+    {{ chart_info.title }}
+
+    {% dw_widget link_group %}
+{% endblock %}
\ No newline at end of file
diff --git a/django_echarts/contrib/bootstrap5/templates/items_grid.html b/django_echarts/contrib/bootstrap5/templates/items_grid.html
index 665c954..66bf45b 100644
--- a/django_echarts/contrib/bootstrap5/templates/items_grid.html
+++ b/django_echarts/contrib/bootstrap5/templates/items_grid.html
@@ -1,3 +1,4 @@
+{% load echarts %}
 <div class="row">
     {% for chart_info in chart_info_list %}
         <div class="col-md-4">
@@ -13,7 +14,7 @@
                 </div>
                 <div class="card-body">
                     <p class="card-text">{{ chart_info.description|default:"暂无描述" }}</p>
-                    <a href="{% url 'dje_chart_single' chart_info.name %}" role="button"
+                    <a href="{% url_single_chart chart_info.uri %}" role="button"
                        class="btn btn-secondary btn-sm">详情</a>
                 </div>
             </div>
diff --git a/django_echarts/contrib/bootstrap5/templates/items_list.html b/django_echarts/contrib/bootstrap5/templates/items_list.html
index ef70e00..cd09e24 100644
--- a/django_echarts/contrib/bootstrap5/templates/items_list.html
+++ b/django_echarts/contrib/bootstrap5/templates/items_list.html
@@ -1,6 +1,7 @@
+{% load echarts %}
 <div class="list-group">
     {% for chart_info in chart_info_list %}
-        <a href="{% url 'dje_chart_single' chart_info.name %}" class="list-group-item">
+        <a href="{% url_single_chart chart_info.uri %}" class="list-group-item">
             <h4 class="list-group-item-heading">{{ chart_info.title }}</h4>
             <p class="list-group-item-text text-first-indent">{{ chart_info.description|default:"暂无描述" }}</p>
         </a>
diff --git a/django_echarts/contrib/bootstrap5/templates/widgets/link_group.html b/django_echarts/contrib/bootstrap5/templates/widgets/link_group.html
new file mode 100644
index 0000000..4b73a0d
--- /dev/null
+++ b/django_echarts/contrib/bootstrap5/templates/widgets/link_group.html
@@ -0,0 +1,5 @@
+<div class="list-group">
+    {% for link in widget %}
+        <a href="{{ link.url }}" class="list-group-item list-group-item-action">{{ link.text }}</a>
+    {% endfor %}
+</div>
\ No newline at end of file
diff --git a/django_echarts/contrib/material/templates/base.html b/django_echarts/contrib/material/templates/base.html
index a2e187f..6183c86 100644
--- a/django_echarts/contrib/material/templates/base.html
+++ b/django_echarts/contrib/material/templates/base.html
@@ -16,7 +16,7 @@
     <nav class="light-blue lighten-1" role="navigation">
         <div class="nav-wrapper container"><a id="logo-container" href="{% url 'dje_home' %}"
                                               class="brand-logo center">{{ site_title }}</a>
-            {% for menu2 in nav.menus %}
+            {% for menu2 in nav.left_menus %}
                 {% if menu2.children %}
                     <ul id="{{ menu2.slug }}" class="dropdown-content">
                         {% for item in menu2.children %}
@@ -29,7 +29,7 @@
                 {% endif %}
             {% endfor %}
             <ul class="left hide-on-med-and-down">
-                {% for menu2 in nav.menus %}
+                {% for menu2 in nav.left_menus %}
                     {% if menu2.children %}
                         <li><a class="dropdown-trigger" href="javascript:"
                                data-target="{{ menu2.slug }}">{{ menu2.text }}<i
@@ -43,11 +43,18 @@
             </ul>
 
             <ul id="nav-mobile" class="right hide-on-med-and-down">
-                {% for item in nav.links %}
-                    <li>
-                        {% dw_widget item %}
-                    </li>
-                {% endfor %}
+            {% for menu2 in nav.right_menus %}
+                {% if menu2.children %}
+                    <ul id="{{ menu2.slug }}" class="dropdown-content">
+                        {% for item in menu2.children %}
+                            {% if item.after_separator %}
+                                <li class="divider"></li>
+                            {% endif %}
+                            <li>{% dw_widget item %}</li>
+                        {% endfor %}
+                    </ul>
+                {% endif %}
+            {% endfor %}
             </ul>
         </div>
     </nav>
diff --git a/django_echarts/contrib/material/templates/chart_parametric.html b/django_echarts/contrib/material/templates/chart_parametric.html
new file mode 100644
index 0000000..fc97a1d
--- /dev/null
+++ b/django_echarts/contrib/material/templates/chart_parametric.html
@@ -0,0 +1,8 @@
+{% extends 'base.html' %}
+{% load echarts %}
+
+{% block main_content %}
+    {{ chart_info.title }}
+
+    {% dw_widget link_group %}
+{% endblock %}
\ No newline at end of file
diff --git a/django_echarts/contrib/material/templates/items_grid.html b/django_echarts/contrib/material/templates/items_grid.html
index e7ca95e..73cb51b 100644
--- a/django_echarts/contrib/material/templates/items_grid.html
+++ b/django_echarts/contrib/material/templates/items_grid.html
@@ -1,3 +1,4 @@
+{% load echarts %}
 <div class="row">
     {% for chart_info in chart_info_list %}
         <div class="col s12 m4">
@@ -12,7 +13,7 @@
                     {% endif %}
                 </div>
                 <div class="card-action">
-                    <a href="{% url 'dje_chart_single' chart_info.name %}">详情</a>
+                    <a href="{% url_single_chart chart_info.uri %}">详情</a>
                 </div>
             </div>
         </div>
diff --git a/django_echarts/contrib/material/templates/items_list.html b/django_echarts/contrib/material/templates/items_list.html
index c523b59..94dd645 100644
--- a/django_echarts/contrib/material/templates/items_list.html
+++ b/django_echarts/contrib/material/templates/items_list.html
@@ -1,3 +1,4 @@
+{% load echarts %}
 <div class="row">
     {% for chart_info in chart_info_list %}
         <div class="col s12 m12">
@@ -15,7 +16,7 @@
 
                 </div>
                 <div class="card-action">
-                    <a href="{% url 'dje_chart_single' chart_info.name %}">详情</a>
+                    <a href="{% url_single_chart chart_info.uri %}">详情</a>
                 </div>
             </div>
         </div>
diff --git a/django_echarts/contrib/material/templates/widgets/link_group.html b/django_echarts/contrib/material/templates/widgets/link_group.html
new file mode 100644
index 0000000..551f62e
--- /dev/null
+++ b/django_echarts/contrib/material/templates/widgets/link_group.html
@@ -0,0 +1,5 @@
+<div class="collection">
+    {% for link in widget %}
+        <a href="{{ link.url }}" class="collection-item">{{ link.text }}</a>
+    {% endfor %}
+</div>
\ No newline at end of file
diff --git a/django_echarts/core/dms.py b/django_echarts/core/dms.py
index 458b136..7eff9e1 100644
--- a/django_echarts/core/dms.py
+++ b/django_echarts/core/dms.py
@@ -5,6 +5,7 @@
 from typing import Tuple, List
 
 from pyecharts.datasets import FILENAMES, EXTRA
+from pyecharts._version import __version__ as pyecharts_version
 from django_echarts.utils.burl import BUrl
 from .localfiles import LocalFilesMixin, DownloaderResource
 
@@ -13,8 +14,26 @@
 # The repo contains all dependencies
 _BUILTIN_REPOS_ = {
     'pyecharts': 'https://assets.pyecharts.org/assets/',
+    'pycharts-v5': 'https://assets.pyecharts.org/assets/v5',
     'local': '/static/assets/'
 }
+
+
+class PyechartsDMS:
+    VERSION2REPO = {
+        '1.9': {'dms_repo': 'pyecharts', 'echarts_version': '4.8.0'},
+        '2.0': {'dms_repo': 'pycharts-v5', 'echarts_version': '5.4.1'}
+    }
+
+    @staticmethod
+    def get_pyecharts__primary_version():
+        return pyecharts_version[:3]
+
+    @staticmethod
+    def get_pycharts_repo(version_pre: str):
+        return PyechartsDMS.VERSION2REPO.get(version_pre)
+
+
 # Use #REPO in custom d2u
 _OTHER_REPOS_ = {
     'cdnjs': 'https://cdnjs.cloudflare.com/ajax/libs/echarts/{echarts_version}',
@@ -25,9 +44,9 @@
     'united-kingdom': 'https://echarts-maps.github.io/echarts-united-kingdom-js'
 }
 
-_CUSTOM_D2U_MAP = {
-    'echarts': 'https://cdnjs.cloudflare.com/ajax/libs/echarts/4.8.0/echarts.min.js',
-    'echarts-gl': 'https://assets.pyecharts.org/assets/echarts-gl.min.js'
+_PYECHARTS_VERSIONS_ = {
+    '1.9': {'dms_repo': 'pyecharts', 'echarts_version': '4.8.0'},
+    '2.0': {'dms_repo': 'pycharts-v5', 'echarts_version': '5.4.1'}
 }
 
 
@@ -50,7 +69,14 @@ def add_repo(self, repo_name: str, repo_url: str):
         self._repo_dic[repo_name] = repo_url
 
     def load_from_dep2url_dict(self, d2u_dic: dict):
-        self._custom_dep2url.update(d2u_dic)
+        """Parse user custom dep_url dict
+        Update 0.6.0: support #repo key-value."""
+        for k, v in d2u_dic.items():
+            if k.startswith('#') and isinstance(v, (list, tuple)):
+                for _v in v:
+                    self._custom_dep2url[_v] = k
+            else:
+                self._custom_dep2url[k] = v
 
     def _resolve_dep(self, dep_name: str, repo_name: str = None) -> Tuple:
         if dep_name in self._custom_dep2url:
diff --git a/django_echarts/core/exceptions.py b/django_echarts/core/exceptions.py
index 7882e42..b69aecf 100644
--- a/django_echarts/core/exceptions.py
+++ b/django_echarts/core/exceptions.py
@@ -1,4 +1,4 @@
-__all__ = ['DJEAbortException']
+__all__ = ['DJEAbortException', 'WidgetNotRegisteredError']
 
 
 class DJEAbortException(BaseException):
@@ -7,6 +7,20 @@ def __init__(self, message: str):
         super().__init__(message)
 
 
+# ErrorMessageException / WarningMessageException
+
 class ChartFuncCallError(BaseException):
     def __init__(self, char_name: str):
         super().__init__(f'ChartName:{char_name}')
+
+
+class WidgetNotRegisteredError(BaseException):
+    def __init__(self, widget):
+        super().__init__(f'Unknown widget type:{widget.__class__.__name__}')
+
+
+class ChartDoesNotExist(BaseException):
+    def __init__(self, message: str = None, param_name: str = None, param_input=None, param_choices=None):
+        if param_name:
+            message = f'Invalid param value: {param_name}={param_input}. Choices are: {param_choices}'
+        super().__init__(message)
diff --git a/django_echarts/core/settings_store.py b/django_echarts/core/settings_store.py
index e3a900c..548553e 100644
--- a/django_echarts/core/settings_store.py
+++ b/django_echarts/core/settings_store.py
@@ -6,9 +6,23 @@
 from django.apps import apps
 from typing_extensions import Literal
 
-from .dms import DependencyManager
+from .dms import DependencyManager, PyechartsDMS
 from .tms import Theme, parse_theme_label, ThemeManager
 
+_CUSTOM_D2U_MAP = {
+    'echarts': 'https://unpkg.com/echarts@{echarts_version}/dist/echarts.min.js',
+    'echarts-gl': 'https://unpkg.com/echarts-gl@{echarts_gl_version}/echarts-gl.min.js',
+    'echarts-wordcloud': 'https://unpkg.com/echarts-wordcloud@{echarts_wordcloud_version}/dist/echarts-wordcloud.min.js',
+    'echarts-liquidfill': 'https://unpkg.com/echarts-liquidfill@{echarts_liquidfill_version}/dist/echarts-liquidfill.min.js',
+}
+
+_ECHARTS_VERSION_DIC = {
+    '4.8.0': {'echarts_version': '4.8.0', 'echarts_gl_version': '1.1.2', 'echarts_wordcloud_version': '1.1.3',
+              'echarts_liquidfill_version': '2.0.6'},
+    '5.4.0': {'echarts_version': '5.4.0', 'echarts_gl_version': '2.0.8', 'echarts_wordcloud_version': '2.1.0',
+              'echarts_liquidfill_version': '3.1.0'},
+}
+
 
 @dataclass
 class DJEOpts:
@@ -16,6 +30,7 @@ class DJEOpts:
     dms_repo: str = 'pyecharts'
     dep2url: Dict[str, str] = field(default_factory=dict)
     local_dir: str = ''
+    local_repo_dir: str = '{STATIC_URL}asserts/'
 
     renderer: Literal['svg', 'canvas'] = 'canvas'
     echarts_theme: Optional[str] = None
@@ -80,17 +95,25 @@ def __init__(self, *, echarts_settings=None, extra_settings=None, **kwargs):
         else:
             self._opts = DJEOpts()
 
+        if self._opts.dms_repo == 'pyecharts':
+            version_pre = PyechartsDMS.get_pyecharts__primary_version()
+            repo_info = PyechartsDMS.get_pycharts_repo(version_pre)
+            self._opts.dms_repo = repo_info.get('dms_repo')
+            self._opts.echarts_version = repo_info.get('echarts_version')
+
         context = {
             'echarts_version': self._opts.echarts_version,
             'baidu_map_ak': self._opts.baidu_map_ak
         }
+        context.update(_ECHARTS_VERSION_DIC.get(self._opts.echarts_version, {}))
         if 'STATIC_URL' in self._extra_settings:
             context.update({'STATIC_URL': self.static_url})
         self._dms = DependencyManager.create_default(
             context=context,
             repo_name=self._opts.dms_repo
         )
-        self._dms.load_from_dep2url_dict(self._opts.dep2url)
+        # dep2url = self.opts.dep2url or _CUSTOM_D2U_MAP
+        self._dms.load_from_dep2url_dict(self.opts.dep2url)
         self._dms.set_localize_opts(static_url=self.static_url, staticfiles_dir=self.staticfiles_dir)
 
         user_theme_label, user_theme_app = self._auto_get_theme_params()
diff --git a/django_echarts/custom_maps.py b/django_echarts/custom_maps.py
new file mode 100644
index 0000000..a8cb3b2
--- /dev/null
+++ b/django_echarts/custom_maps.py
@@ -0,0 +1,122 @@
+import json
+import os
+from dataclasses import dataclass
+
+from django.contrib.staticfiles import finders
+from django.http.response import JsonResponse, HttpResponseNotFound, HttpResponse
+from django.urls import reverse_lazy, path
+from django.views.generic.base import View
+
+from django_echarts.conf import DJANGO_ECHARTS_SETTINGS
+
+__all__ = ['use_custom_map', 'custom_map_url', 'custom_map_urlpatterns']
+
+
+@dataclass
+class CustomMapItem:
+    """A map data structure for geojson & svg map."""
+    map_name: str
+    url: str
+    catalog: str
+    param_str: str  # param string in echarts.registerMap
+    ajax_data_type: str
+
+    def __eq__(self, other):
+        return isinstance(other, CustomMapItem) and self.map_name == other.map_name
+
+    def __hash__(self):
+        return hash(self.map_name)
+
+
+def create_custom_map_item(map_name: str, url: str, echarts_version: str):
+    if any([
+        url.endswith('.geojson'), url.endswith('.json'), url.endswith('svg'),
+        map_name.endswith('.geojson'), map_name.endswith('.json'), map_name.endswith('.svg')
+    ]):
+        catalog = url.split('.')[-1]
+        if catalog == 'json':
+            catalog = 'geojson'
+        if echarts_version[0] == '4':
+            if catalog == 'geojson':
+                param_str = 'mapData'
+                ajax_data_type = 'json'
+            else:
+                raise ValueError('The svg map is unsupported for echarts 4.')
+        else:
+            if catalog == 'geojson':
+                param_str = '{geoJSON:mapData}'
+                ajax_data_type = 'json'
+            else:
+                param_str = '{svg:mapData}'
+                ajax_data_type = 'text'
+        return CustomMapItem(map_name=map_name, url=url, catalog=catalog, param_str=param_str,
+                             ajax_data_type=ajax_data_type)
+    else:
+        raise ValueError('Unsupported custom map catalogs.')
+
+
+def use_custom_map(chart_obj, map_name: str, url_or_filename: str = None):
+    """Register and use geojson & svg map for a chart."""
+    echarts_version = DJANGO_ECHARTS_SETTINGS.opts.echarts_version
+    url_or_filename = url_or_filename or map_name
+    if not any([
+        url_or_filename.startswith('https://'),
+        url_or_filename.startswith('http://'),
+        url_or_filename.startswith('/')
+    ]):
+        map_data_url = reverse_lazy(_VIEW_NAME, args=(url_or_filename,))
+    else:
+        map_data_url = url_or_filename
+    # TODO Check chart.maptype and map_name ?
+    # Map.options['series'][i]['mapType']  Geo.options['geo']['map']
+    setattr(chart_obj, 'custom_map_item', create_custom_map_item(map_name, map_data_url, echarts_version))
+
+
+_VIEW_NAME = 'dje_map_data'
+
+
+def custom_map_url(map_name: str) -> str:
+    """Get default url for a custom file."""
+    return reverse_lazy(_VIEW_NAME, args=(map_name,))
+
+
+def _get_custom_map_file_path(map_name: str) -> str:
+    # ctomstr
+    finder_param_path = [f'assets/custom_maps/{map_name}']
+    if map_name.endswith('.geojson') or map_name.endswith('.json'):  # Compatible with old geojson(0.5.x)
+        finder_param_path.append(f'geojson/{map_name}')
+    for fpp in finder_param_path:
+        result = finders.find(fpp, all=False)
+        if result:
+            return result
+
+
+class CustomMapDataView(View):
+    def get(self, request, *args, **kwargs):
+        map_name = self.kwargs.get('map_name')
+        file_path = _get_custom_map_file_path(map_name)
+        if not file_path or not os.path.exists(file_path):
+            return HttpResponseNotFound('The map file does not exist.')
+        if file_path.endswith('.geojson') or file_path.endswith('.json'):
+            return self.return_geojson_map_rsp(file_path)
+        elif file_path.endswith('.svg'):
+            return self.return_svg_map_rsp(file_path)
+        else:
+            return HttpResponseNotFound('Unsupported map type. geojson or svg is required.')
+
+    def return_geojson_map_rsp(self, file_path):
+        with open(file_path, 'r', encoding='utf8') as fp:
+            data = json.load(fp)
+        return JsonResponse(data, safe=False)
+
+    def return_svg_map_rsp(self, file_path):
+        with open(file_path, 'r', encoding='utf8') as fp:
+            content = fp.read()
+        rsp = HttpResponse(content_type='text/plain')
+        rsp.write(content)
+        return rsp
+
+
+custom_map_urlpatterns = [
+    path('map_data/<str:map_name>', CustomMapDataView.as_view(), name=_VIEW_NAME)
+]
diff --git a/django_echarts/datatools/helpers.py b/django_echarts/datatools/helpers.py
index dc077a3..fff243a 100644
--- a/django_echarts/datatools/helpers.py
+++ b/django_echarts/datatools/helpers.py
@@ -2,6 +2,10 @@
 数据处理函数
 See more detail https://mp.weixin.qq.com/s/ZYU7g3mVF-cvRZ3JyMIngw
 """
+import statistics
+from collections import namedtuple, UserList
+from dataclasses import dataclass
+from typing import List, Tuple
 
 
 def adjust_max_min(values: list):
@@ -18,3 +22,54 @@ def ceil_n(val, n=3):
 
 def floor_n(val, n=3):
     return (val // (10 ** n)) * 10 ** n
+
+
+QuartilesRank = namedtuple('QuartilesRank', 'rank text color')
+
+
+@dataclass
+class SeriesStatisticsValue:
+    """一维数据统计值"""
+    label: str
+    index: int
+    num: int
+    avg: int
+    std_deviation: int
+    min_: int
+    q1: int
+    q2: int
+    q3: int
+    max_: int
+
+    @property
+    def boxplot_values(self) -> Tuple:
+        """Return five values for boxplot"""
+        return (self.min_, self.q1, self.q2, self.q3, self.max_)
+
+    def evaluate(self, value: int) -> QuartilesRank:
+        if value < self.q1:
+            return QuartilesRank(rank=1, text='下', color='danger')
+        elif value < self.q2:
+            return QuartilesRank(rank=2, text='中下', color='warning')
+        elif value < self.q3:
+            return QuartilesRank(rank=2, text='中上', color='info')
+        else:
+            return QuartilesRank(rank=2, text='上', color='success')
+
+    @classmethod
+    def from_(cls, data: List, label: str = '', index=0):
+        q1, q2, q3 = list(map(int, statistics.quantiles(data)))
+        return cls(label=label, num=len(data),
+                   avg=int(statistics.mean(data)), std_deviation=int(statistics.pstdev(data)),
+                   min_=min(data), max_=max(data), q1=q1, q2=q2, q3=q3, index=index)
+
+
+class SeriesStatisticsList(UserList[SeriesStatisticsValue]):
+
+    @property
+    def axis_values(self):
+        return [item.label for item in self]
+
+    @property
+    def boxplot_data(self):
+        return [list(item.boxplot_values) for item in self]
diff --git a/django_echarts/entities/__init__.py b/django_echarts/entities/__init__.py
index 7ab548c..7200049 100644
--- a/django_echarts/entities/__init__.py
+++ b/django_echarts/entities/__init__.py
@@ -2,10 +2,12 @@
     ChartInfo, ChartInfoManagerMixin, LocalChartInfoManager
 )
 from .base import DwString
-from .chart_widgets import NamedCharts
+from .chart_widgets import NamedCharts, BlankChart
 from .containers import RowContainer, Container
 from .html_widgets import (
-    HTMLBase, LinkItem, Menu, Jumbotron, Nav, Copyright, Message, ValuesPanel, ValueItem, Title, ElementEntity
+    HTMLBase, LinkItem, SeparatorItem, Menu, Jumbotron, Nav, Copyright, Message, ValuesPanel, ValueItem, Title,
+    ElementEntity, LinkGroup
 )
 from .pages import (WidgetCollection, WidgetGetterMixin)
 from .styles import (bootstrap_table_class, material_table_class)
+from .uri import EntityURI
diff --git a/django_echarts/entities/articles.py b/django_echarts/entities/articles.py
index e93a530..619359a 100644
--- a/django_echarts/entities/articles.py
+++ b/django_echarts/entities/articles.py
@@ -1,7 +1,9 @@
+import copy
 from collections import defaultdict
 from typing import Dict, Optional, List, Tuple
 
 from .html_widgets import HTMLBase
+from .uri import EntityURI, ParamsConfig
 
 __all__ = ['ChartInfo', 'ChartInfoManagerMixin', 'LocalChartInfoManager']
 
@@ -10,15 +12,15 @@ class ChartInfo(HTMLBase):
     widget_type = 'InfoCard'
     """The meta-data class for a chart."""
     __slots__ = ['name', 'title', 'description', 'body', 'url', 'selected', 'catalog', 'top', 'tags', 'layout',
-                 'extra']
+                 'extra', 'is_bound', 'uri', 'params_config']
 
     def __init__(self, name: str, title: str = None, description: str = None, body: str = None, url: str = None,
                  selected: bool = False, catalog: str = None, top: int = 0, tags: List = None, layout: str = None,
-                 extra: Dict = None):
+                 extra: Dict = None, params_config: ParamsConfig = None):
         self.name = name
         self.title = title or self.name
         self.description = description or ''
-        self.body = body
+        self.body = body or ''
         self.url = url
         self.selected = selected
         self.top = top
@@ -26,6 +28,30 @@ def __init__(self, name: str, title: str = None, description: str = None, body:
         self.tags = tags or []
         self.layout = layout
         self.extra = extra or {}
+        self.is_bound = True
+        self.params_config = params_config or ParamsConfig.empty()
+        self.uri = EntityURI('chart', self.name)  # type:EntityURI
+
+    # Methods related to bound states.
+
+    def set_bound(self, is_bound: bool):
+        self.is_bound = is_bound
+        return self
+
+    def create_bound_chart_info(self, uri: EntityURI) -> 'ChartInfo':
+        if self.is_bound:
+            return self
+        new_info = copy.copy(self)
+        new_info.format_data_with_params(uri.params)
+        new_info.uri = uri
+        return new_info
+
+    def format_data_with_params(self, params: dict):
+        if len(params) != 0:
+            self.title = self.title.format(**params)
+            self.description = self.description.format(**params)
+            self.body = self.body.format(**params)
+        self.is_bound = True
 
     def __hash__(self):
         return hash(self.name)
@@ -40,16 +66,21 @@ def __str__(self):
 class ChartInfoManagerMixin:
     """The backend for the store of chart info."""
 
+    # TODO Refactor interfaces
+
     def add_chart_info(self, info: ChartInfo):
         pass
 
+    def query_all_chart_info(self) -> List[ChartInfo]:
+        pass
+
     def query_chart_info_list(self, keyword: str = None, with_top: bool = False) -> List[ChartInfo]:
         pass
 
     def query_group_list(self) -> List[Tuple[str, List[ChartInfo]]]:
         pass
 
-    def get_or_none(self, name: str) -> Optional[ChartInfo]:
+    def get_or_none(self, name: str = None, uri: EntityURI = None) -> Optional[ChartInfo]:
         pass
 
     def count(self) -> int:
@@ -63,8 +94,11 @@ def __init__(self):
     def add_chart_info(self, info: ChartInfo):
         self._chart_info_list.append(info)
 
+    def query_all_chart_info(self) -> List[ChartInfo]:
+        return self._chart_info_list
+
     def query_chart_info_list(self, keyword: str = None, with_top: bool = False) -> List[ChartInfo]:
-        chart_info_list = [info for info in self._chart_info_list if not with_top or info.top]
+        chart_info_list = [info for info in self._chart_info_list if info.is_bound and (not with_top or info.top)]
         if keyword:
             def _filter(_item):
                 return keyword in _item.title or keyword in _item.tags
@@ -90,10 +124,14 @@ def query_group_list(self) -> List[Tuple[str, List[ChartInfo]]]:
         grouped_data.append(('Others', catalog2info['Others']))
         return grouped_data
 
-    def get_or_none(self, name: str) -> Optional[ChartInfo]:
+    def get_or_none(self, name: str = None, uri: EntityURI = None) -> Optional[ChartInfo]:
+        if uri:
+            q_name = uri.name
+        else:
+            q_name = name
         for info in self._chart_info_list:
-            if info.name == name:
-                return info
+            if info.name == q_name:
+                return info.create_bound_chart_info(uri)
 
     def count(self) -> int:
         return len(self._chart_info_list)
diff --git a/django_echarts/entities/base.py b/django_echarts/entities/base.py
index 54424c3..6a7db0a 100644
--- a/django_echarts/entities/base.py
+++ b/django_echarts/entities/base.py
@@ -1,5 +1,3 @@
-
-
 class DwString(str):
     """This is a str for django template string."""
 
diff --git a/django_echarts/entities/chart_widgets.py b/django_echarts/entities/chart_widgets.py
index af57df8..d2deecb 100644
--- a/django_echarts/entities/chart_widgets.py
+++ b/django_echarts/entities/chart_widgets.py
@@ -1,7 +1,28 @@
 import warnings
+from dataclasses import dataclass, field
 from .containers import RowContainer
 
 
+@dataclass
+class BlankChart:
+    """A interface class for tests only."""
+    width: str = '900px'
+    height: str = '500px'
+    renderer: str = 'canvas'
+    page_title: str = 'Blank Chart'
+    theme: str = 'white'
+    chart_id: str = 'dje_blank'
+    options: dict = field(default_factory=dict)
+    js_dependencies: list = field(default_factory=lambda: ['echarts'])  # Use Simplified format.
+    geojson: dict = None  # added by django-echarts
+
+    def __post_init__(self):
+        self._is_geo_chart = False
+
+    def dump_options(self) -> str:
+        return "{}"
+
+
 class NamedCharts(RowContainer):
     """
     A data structure class containing multiple named charts.
diff --git a/django_echarts/entities/html_widgets.py b/django_echarts/entities/html_widgets.py
index 4bcec3a..52ff737 100644
--- a/django_echarts/entities/html_widgets.py
+++ b/django_echarts/entities/html_widgets.py
@@ -9,7 +9,7 @@
 
 from .containers import RowContainer
 
-__all__ = ['LinkItem', 'Menu', 'Jumbotron', 'Nav', 'Copyright', 'Message', 'ValuesPanel', 'ValueItem']
+__all__ = ['LinkItem', 'SeparatorItem', 'Menu', 'Jumbotron', 'Nav', 'Copyright', 'Message', 'ValuesPanel', 'ValueItem']
 
 
 def _new_slug() -> str:
@@ -33,6 +33,10 @@ def __init__(self, text: str, url: str = None, slug: str = None,
         self.after_separator = after_separator
 
 
+class SeparatorItem(HTMLBase):
+    pass
+
+
 class Menu(HTMLBase):
     """The menu element in top nav."""
     __slots__ = ['text', 'slug', 'url', 'children']
@@ -45,33 +49,70 @@ def __init__(self, text: str, slug: str = None, url: str = None):
 
 
 class Nav:
+    """A html widget for top nav and footer links."""
+    CHART_PLACEHOLDER = 'chart_nav'  # A placeholder for chart nav.
+
     def __init__(self):
-        self.menus = []  # type: List[Menu]
-        self.links = []  # type: List[LinkItem]
+        self.left_menus = []  # type: List[Menu]
+        self.right_menus = []  # type:List[Menu]
         self.footer_links = []  # type: List[LinkItem]
 
-    def add_menu(self, text: str, slug: str = None, url: str = None):
-        # Fixed in Borax v3.5.3
-        texts, slugs = fetch(self.menus, 'text', 'slug', getter=lambda item, key: getattr(item, key))
+    @property
+    def menus(self):
+        return self.left_menus
+
+    @property
+    def links(self):
+        return self.right_menus
+
+    def add_left_menu(self, text: str, slug: str = None, url: str = None):
+        texts, slugs = fetch(self.left_menus, 'text', 'slug')
+        if text in texts:
+            return self
+        if not slug or slug not in slugs:
+            self.left_menus.append(Menu(text, slug, url))
+        return self
+
+    def add_right_menu(self, text: str, slug: str = None, url: str = None):
+        texts, slugs = fetch(self.right_menus, 'text', 'slug')
         if text in texts:
             return self
         if not slug or slug not in slugs:
-            self.menus.append(Menu(text, slug, url))
+            self.right_menus.append(Menu(text, slug, url))
         return self
 
-    def add_item(self, menu_text: str, item: Union[LinkItem], after_separator=False):
+    def add_item_in_left_menu(self, menu_text: str, item: Union[LinkItem], after_separator=False):
         item.after_separator = item.after_separator | after_separator
-        for menu in self.menus:
+        for menu in self.left_menus:
             if menu.text == menu_text:
                 menu.children.append(item)
                 break
         return self
 
-    def add_right_link(self, item: LinkItem):
-        self.links.append(item)
+    def add_item_in_right_menu(self, menu_text: str, item: Union[LinkItem], after_separator=False):
+        item.after_separator = item.after_separator | after_separator
+        for menu in self.left_menus:
+            if menu.text == menu_text:
+                menu.children.append(item)
+                break
         return self
 
 
+class LinkGroup(HTMLBase):
+    """A list panel containing links."""
+    widget_type = 'LinkGroup'
+
+    def __init__(self):
+        self.widgets = []  # type:List[LinkItem]
+
+    def add_widget(self, widget: LinkItem):
+        self.widgets.append(widget)
+
+    def __iter__(self):
+        for widget in self.widgets:
+            yield widget
+
+
 class Jumbotron(HTMLBase):
     widget_type = 'Jumbotron'
     """The main panel in home page."""
@@ -154,9 +195,6 @@ def __init__(self, text: str, small_text: str = None):
         self.small_text = small_text
 
 
-# tag_name: str, content: str = None,
-#              *, id_: str = None, style: Dict = None, class_: Union[List, str, None] = None, style_width: str = None,
-#              style_height: str = None, **kwargs
 class ElementEntity(HTMLBase):
     """A common entity for a html element."""
 
diff --git a/django_echarts/entities/pages.py b/django_echarts/entities/pages.py
index 629bbd5..e06111f 100644
--- a/django_echarts/entities/pages.py
+++ b/django_echarts/entities/pages.py
@@ -1,25 +1,40 @@
 from collections import OrderedDict
 from abc import abstractmethod
 from typing import List, Union, Tuple, Any, Optional
+import warnings
 
+from .uri import EntityURI
 from .articles import ChartInfo
 from .containers import RowContainer, Container
 from .layouts import LayoutOpts, TYPE_LAYOUT_OPTS, any2layout
 
 
 class WidgetGetterMixin:
+
     @abstractmethod
     def get_chart_and_info(self, name: str) -> Tuple[Optional[Any], bool, Optional[ChartInfo]]:
         """Return a pycharts chart object."""
+        warnings.warn('This method is deprecated, use get_chart_and_info_by_uri instead.', DeprecationWarning,
+                      stacklevel=2)
         pass
 
     @abstractmethod
     def get_html_widget(self, name: str) -> Any:
         """Return a html widget object."""
+        warnings.warn('This method is deprecated, use get_widget_by_uri instead.', DeprecationWarning, stacklevel=2)
         pass
 
     @abstractmethod
     def get_widget_by_name(self, name: str) -> Any:
+        warnings.warn('This method is deprecated, use get_widget_by_uri instead.', DeprecationWarning, stacklevel=2)
+        pass
+
+    @abstractmethod
+    def get_chart_and_info_by_uri(self, uri: EntityURI) -> Tuple[Any, bool, Optional[ChartInfo]]:
+        pass
+
+    @abstractmethod
+    def get_widget_by_uri(self, uri: EntityURI):
         pass
 
 
@@ -41,21 +56,22 @@ def start_(self):
         return self
 
     def add_chart_widget(self, chart_name: str, layout: TYPE_LAYOUT_OPTS = 'l8'):
-        self._ref_config_list.append([True, layout, chart_name])
+        self._ref_config_list.append([True, layout, EntityURI.from_str(chart_name, catalog='chart')])
         return self
 
     def add_html_widget(self, widget_names: List, layout: TYPE_LAYOUT_OPTS = 0):
-        self._ref_config_list.append([False, layout, *widget_names])
+        widget_uri_list = [EntityURI.from_str(name, catalog='widget') for name in widget_names]
+        self._ref_config_list.append([False, layout, *widget_uri_list])
 
     def auto_mount(self, widget_container: WidgetGetterMixin):
         self.start_()
-        for is_chart, layout_str, *names in self._ref_config_list:
+        for is_chart, layout_str, *uri_list in self._ref_config_list:
             if is_chart:
-                chart_name = names[0]
-                chart_obj, _, info = widget_container.get_chart_and_info(chart_name)
+                chart_uri = uri_list[0]
+                chart_obj, _, info = widget_container.get_chart_and_info_by_uri(chart_uri)
                 self.pack_chart_widget(chart_obj, info, row_no=self._row_no)
             else:
-                widget_list = [widget_container.get_widget_by_name(name) for name in names]
+                widget_list = [widget_container.get_widget_by_uri(uri) for uri in uri_list]
                 self.pack_html_widget(widget_list)
 
     def pack_chart_widget(self, chart_obj, info: ChartInfo, ignore_ref: bool = True, layout: str = 'l8',
diff --git a/django_echarts/entities/uri.py b/django_echarts/entities/uri.py
new file mode 100644
index 0000000..6cbcf4b
--- /dev/null
+++ b/django_echarts/entities/uri.py
@@ -0,0 +1,139 @@
+from typing import List, Union, Generator
+import itertools
+
+
+class EntityURI:
+    __empty = None
+
+    CATALOG_CHART = 'chart'
+    CATALOG_INFO = 'info'
+    CATALOG_WIDGET = 'widget'
+    CATALOG_LAYOUT = 'layout'
+    CATALOG_COLLECTION = 'collection'
+
+    CATALOG_VIEW = 'view'
+    # TODO description attr
+
+    __slots__ = ['catalog', 'name', 'params', '_params_path']
+
+    def __init__(self, catalog: str, name: str, params: dict = None):
+        self.catalog = catalog
+        self.name = name
+        self.params = params or {}
+        _items = []
+        for k, v in self.params.items():
+            _items.extend([k, str(v)])
+        self._params_path = '/'.join(_items)
+
+    @property
+    def params_path(self):
+        return self._params_path
+
+    def __str__(self):
+        if len(self.params):
+            return f'{self.catalog}:{self.name}/{self.params_path}'
+        else:
+            return f'{self.catalog}:{self.name}'
+
+    __repr__ = __str__
+
+    def is_empty(self):
+        return not self.catalog or self.catalog == 'empty'
+
+    @classmethod
+    def empty(cls):
+        if cls.__empty is None:
+            cls.__empty = cls('empty', '')
+        return cls.__empty
+
+    @classmethod
+    def from_params_path(cls, catalog: str, name: str, params_path: str, param_names: list = None):
+        """Parse URI param string to  param dict.
+        param_names is required when param value contains '/' character.
+        """
+        params = {}
+        if not params_path:
+            return cls(catalog, name, params)
+        kw = params_path.split('/')
+        if param_names:
+            name2pos = []
+            for pn in param_names:
+                try:
+                    pos = kw.index(pn)
+                    name2pos.append((pn, pos))
+                except IndexError:
+                    pass
+            name2pos.sort(key=lambda _item: _item[1])
+            for i, item in enumerate(name2pos):
+                pname, npos = item
+                start_pos = npos + 1
+                if i == len(name2pos) - 1:
+                    end_pos = len(kw)
+                else:
+                    end_pos = name2pos[i + 1][1]
+                if end_pos - start_pos == 1:
+                    value = kw[start_pos]
+                else:
+                    value = '/'.join(kw[start_pos:end_pos])
+                params[pname] = value
+        else:
+            for i in range(0, len(kw), 2):
+                params[kw[i]] = kw[i + 1]
+        return cls(catalog, name, params)
+
+    @classmethod
+    def from_str(cls, s: str, catalog: str = None, param_names: list = None):
+        pos1 = s.find(':')
+        if pos1 == -1:
+            _catalog, st = catalog, s
+        else:
+            _catalog, st = s[:pos1], s[pos1 + 1:]
+        if len(st) == 0:
+            st = _catalog
+            _catalog = catalog
+        pos = st.find('/')
+        if pos == -1:
+            name, params_path = st, ''
+        else:
+            name, params_path = st[:pos], st[pos + 1:]
+        return cls.from_params_path(_catalog, name, params_path, param_names)
+
+
+USER_PARAMS_CONFIGS_TYPE = Union[List[dict], dict]
+
+
+def parse_params_choices(config: USER_PARAMS_CONFIGS_TYPE) -> Generator[dict, None, None]:
+    """
+    [{'year':2021, 'month':1},{'year':2021, 'month':2},]
+    {'year':[2021,2022], 'month':[1,2]}
+    """
+    if isinstance(config, List):
+        for item in config:
+            yield item
+    else:
+        names = []
+        values_list = []
+        for name, values in config.items():
+            names.append(name)
+            values_list.append(values)
+        for t in itertools.product(*values_list):
+            item = dict((k, v) for k, v in zip(names, t))
+            yield item
+
+
+class ParamsConfig:
+    __empty__ = None
+    __slots__ = ['choices']
+
+    def __init__(self, choices: USER_PARAMS_CONFIGS_TYPE = None):
+        self.choices = choices or []
+
+    def __iter__(self):
+        for param_dic in parse_params_choices(self.choices):
+            yield param_dic
+
+    @classmethod
+    def empty(cls):
+        if ParamsConfig.__empty__ is None:
+            ParamsConfig.__empty__ = ParamsConfig()
+        return ParamsConfig.__empty__
diff --git a/django_echarts/geojson.py b/django_echarts/geojson.py
index 84d30d5..f9816fb 100644
--- a/django_echarts/geojson.py
+++ b/django_echarts/geojson.py
@@ -1,53 +1,21 @@
 """A extension for geojson."""
-import json
-import os
+import warnings
 from collections import namedtuple
 
-from django.conf import settings
-from django.contrib.staticfiles import finders
-from django.http.response import JsonResponse, HttpResponseNotFound
-from django.urls import reverse_lazy, path
-from django.views.generic.base import View
+from django_echarts.custom_maps import use_custom_map, custom_map_url
 
-__all__ = ['use_geojson', 'geojson_url', 'GeojsonDataView', 'geo_urlpatterns']
+__all__ = ['use_geojson', 'geojson_url']
 
-GeojsonItem = namedtuple('GeojsonItem', 'map_name url')
+warnings.warn('The django_echarts.geojson is deprecated.Use django_echarts.custom_maps instead.', DeprecationWarning)
 
-_VIEW_NAME = 'dje_geojson'
+GeojsonItem = namedtuple('GeojsonItem', 'map_name url')
 
 
 def use_geojson(chart_obj, map_name: str, url: str = None):
     """Register and use geojson map for a chart."""
-    setattr(chart_obj, 'geojson', GeojsonItem(map_name, url))
+    use_custom_map(chart_obj, map_name, url)
 
 
 def geojson_url(geojson_name: str) -> str:
     """Get default url for a geojson file."""
-    return reverse_lazy(_VIEW_NAME, args=(geojson_name,))
-
-
-def _get_geojson_path(name: str):
-    result = finders.find(f'geojson/{name}', all=False)
-    if result:
-        return result
-    g_dir = getattr(settings, 'STATICFILES_DIRS', [])[0]
-    if not g_dir:
-        raise ValueError('The settings.STATICFILES_DIRS must be set for geojson.')
-    pa = os.path.join(str(g_dir), 'geojson', name)
-    return pa
-
-
-class GeojsonDataView(View):
-    def get(self, request, *args, **kwargs):
-        geojson_name = self.kwargs.get('geojson_name')
-        file_path = _get_geojson_path(geojson_name)
-        if not os.path.exists(file_path):
-            return HttpResponseNotFound('The geojson file does not exist.')
-        with open(file_path, 'r', encoding='utf8') as fp:
-            data = json.load(fp)
-        return JsonResponse(data, safe=False)
-
-
-geo_urlpatterns = [
-    path('geojson/<str:geojson_name>', GeojsonDataView.as_view(), name=_VIEW_NAME)
-]
+    return custom_map_url(geojson_name)
diff --git a/django_echarts/renders/html.py b/django_echarts/renders/html.py
index 862ff02..80c9b7f 100644
--- a/django_echarts/renders/html.py
+++ b/django_echarts/renders/html.py
@@ -1,18 +1,19 @@
 from functools import singledispatch
 from typing import Union
 
+import htmlgenerator as hg
 from borax.htmls import HTMLString, html_tag
 from borax.strings import camel2snake
 from django.template import engines
 from django.template.loader import get_template
 from django.utils.safestring import SafeString
-from django_echarts.entities import (
-    ValuesPanel, LinkItem, Menu, NamedCharts, DwString, RowContainer, Container, HTMLBase, ElementEntity
-)
 from prettytable import PrettyTable
 from pyecharts.charts.base import Base
 from pyecharts.components.table import Table
-import htmlgenerator as hg
+
+from django_echarts.entities import (
+    ValuesPanel, LinkItem, Menu, NamedCharts, DwString, RowContainer, Container, HTMLBase, BlankChart
+)
 
 
 def _to_css_length(val):
@@ -26,9 +27,8 @@ def _to_css_length(val):
 def render_widget(widget, **kwargs) -> SafeString:
     if hasattr(widget, '__html__') and callable(widget.__html__):
         return widget.__html__()
-    message = f'<div>Unknown widget type:{widget.__class__.__name__}</div>'
-    raise TypeError(message)
-    # return SafeString(f'<div>Unknown widget type:{widget.__class__.__name__}</div>')
+    # raise WidgetNotRegisteredError(widget)
+    return SafeString(f'<div>Unknown widget type:{widget.__class__.__name__}</div>')
 
 
 @render_widget.register(type(None))
@@ -43,6 +43,7 @@ def render_html(widget, **kwargs) -> SafeString:
 
 
 @render_widget.register(Base)
+@render_widget.register(BlankChart)
 def render_chart(widget, **kwargs) -> SafeString:
     width = kwargs.get('width') or widget.width
     height = kwargs.get('height') or widget.height
diff --git a/django_echarts/renders/javascript.py b/django_echarts/renders/javascript.py
index b83ea6f..df60791 100644
--- a/django_echarts/renders/javascript.py
+++ b/django_echarts/renders/javascript.py
@@ -1,6 +1,8 @@
 from functools import singledispatch
 
-from django_echarts.entities import (NamedCharts, ValuesPanel, RowContainer, Container, WidgetCollection, HTMLBase)
+from django_echarts.entities import (
+    NamedCharts, ValuesPanel, RowContainer, Container, WidgetCollection, HTMLBase, BlankChart
+)
 from prettytable import PrettyTable
 from pyecharts.charts.base import Base
 from pyecharts.components.table import Table
@@ -17,6 +19,7 @@ def flat_chart(widget):
 
 
 @flat_chart.register(Base)
+@flat_chart.register(BlankChart)
 def flat_base(widget: Base):
     return [widget]
 
@@ -80,6 +83,9 @@ def _deps(_chart):
         _dep_list = _deps(chart)
 
         for dep in _dep_list:
+            # exclude custom map.
+            if dep.endswith('.geojson') or dep.endswith('.json') or dep.endswith('.svg'):
+                continue
             if dep not in dep_list and dep not in front_items:
                 if dep in _ECHARTS_LIB_NAMES:
                     front_items.append(dep)
diff --git a/django_echarts/site_exts.py b/django_echarts/site_exts.py
new file mode 100644
index 0000000..fb60feb
--- /dev/null
+++ b/django_echarts/site_exts.py
@@ -0,0 +1,22 @@
+from django.urls import reverse_lazy
+from django_echarts.entities.uri import EntityURI
+from typing import Union
+
+
+def reverse_chart_url(uri_or_name: Union[EntityURI, str], params_dic: dict = None):
+    """
+    reverse url of a single chart view in DJESite.
+    The following three signatures are supported.
+
+    reverse_chart_url(uri:EntityURI)
+    reverse_chart_url(name:str)
+    reverse_chart_url(name:str, params_dic:dict)
+    """
+    if isinstance(uri_or_name, EntityURI):
+        uri = uri_or_name
+    else:
+        uri = EntityURI('chart', uri_or_name, params_dic)
+    if len(uri.params):
+        return reverse_lazy('dje_chart_single', args=(uri.name, uri.params_path))
+    else:
+        return reverse_lazy('dje_chart_single', args=(uri.name,))
diff --git a/django_echarts/starter/params_forms.py b/django_echarts/starter/params_forms.py
new file mode 100644
index 0000000..cb29e5b
--- /dev/null
+++ b/django_echarts/starter/params_forms.py
@@ -0,0 +1,6 @@
+from django import forms
+
+
+class ParamsForm:
+    name = forms.CharField(label='Name', required=True, max_length=200)
+    params_dic = forms.JSONField(label='Params(JSON)', max_length=500)
diff --git a/django_echarts/starter/sites.py b/django_echarts/starter/sites.py
index f9bf46a..946e950 100644
--- a/django_echarts/starter/sites.py
+++ b/django_echarts/starter/sites.py
@@ -1,8 +1,11 @@
 import re
+import sys
+from collections import defaultdict
 from functools import wraps
 from typing import Optional, List, Dict, Type, Any, Union
 
 from borax.serialize import cjson
+from borax.system import load_object
 from django.core.paginator import Paginator
 from django.http.response import JsonResponse
 from django.urls import reverse_lazy, path
@@ -10,13 +13,15 @@
 from django.views.generic.edit import FormMixin
 from django_echarts.ajax_echarts import ChartOptionsView
 from django_echarts.conf import DJANGO_ECHARTS_SETTINGS
-from django_echarts.core.exceptions import DJEAbortException
+from django_echarts.core.exceptions import DJEAbortException, ChartDoesNotExist
 from django_echarts.entities import (
-    ChartInfo, WidgetCollection, Nav, LinkItem, Jumbotron, Copyright, Message, ValuesPanel
+    ChartInfo, WidgetCollection, Nav, Menu, LinkItem, Jumbotron, Copyright, Message, ValuesPanel, LinkGroup
 )
-from django_echarts.geojson import geo_urlpatterns
+from django_echarts.entities.uri import EntityURI, ParamsConfig
+from django_echarts.custom_maps import custom_map_urlpatterns
+from django_echarts.site_exts import reverse_chart_url
 from django_echarts.stores.entity_factory import factory
-from django_echarts.utils.compat import get_elided_page_range
+from django_echarts.utils.compat import compat_get_elided_page_range
 from typing_extensions import Literal
 
 from .optstools import SiteOptsForm, SiteOpts
@@ -76,7 +81,7 @@ def get_context_data(self, **kwargs):
         context['nav'] = site.nav
         context['site_title'] = site.site_title
         context['page_title'] = site.site_title
-        context['copyright'] = factory.html_widgets.get('copyright')
+        context['copyright'] = factory.get_widget_by_uri(site.ref2uri['copyright'])
         context['opts'] = site.opts
 
         # The data store in a view processing lifecycle.
@@ -148,13 +153,15 @@ class DJESiteHomeView(DJESiteBackendView):
     template_name = 'home.html'
 
     def get_context_data(self, **kwargs):
+        site = SiteInjectMixin.get_site_object()
         context = super(DJESiteHomeView, self).get_context_data(**kwargs)
-        context['jumbotron'] = factory.get_html_widget(WidgetRefs.home_jumbotron)
-        chart_obj, _, info = factory.get_chart_and_info(WidgetRefs.home_jumbotron_chart)
+        context['jumbotron'] = factory.get_widget_by_uri(site.ref2uri[WidgetRefs.home_jumbotron])
+        chart_obj, _, info = factory.get_chart_and_info_by_uri(site.ref2uri[WidgetRefs.home_jumbotron_chart])
         if chart_obj:
             context[_VAR_CHART_] = chart_obj
             context[_VAR_CHART_INFO_] = info
-        context[WidgetRefs.home_values_panel] = factory.get_html_widget(WidgetRefs.home_values_panel)
+        context[WidgetRefs.home_values_panel] = factory.get_widget_by_uri(
+            site.ref2uri[WidgetRefs.home_values_panel])
         context['top_chart_info_list'] = factory.chart_info_manager.query_chart_info_list(with_top=True)
         context['layout_tpl'] = 'items_grid.html'
         return context
@@ -184,11 +191,7 @@ def dje_init_page_context(self, context, site: 'DJESite') -> Optional[str]:
             page_number = self.request.GET.get(self.page_kwarg, 1)
             page_obj = paginator.get_page(page_number)
             context['page_obj'] = page_obj
-            try:
-                # Django3.2+
-                elided_page_nums = paginator.get_elided_page_range(page_number)
-            except (AttributeError, TypeError, ValueError):
-                elided_page_nums = get_elided_page_range(paginator, page_number)
+            elided_page_nums = compat_get_elided_page_range(paginator, page_number)
             context['elided_page_nums'] = list(elided_page_nums)
             return 'list_with_paginator.html'
         else:
@@ -205,16 +208,38 @@ class DJESiteChartSingleView(DJESiteBackendView):
     def dje_init_page_context(self, context, site: 'DJESite') -> Optional[str]:
         context['view_name'] = 'dje_chart_single'
         chart_name = self.kwargs.get('name')
-        chart_obj, func_exists, chart_info = factory.get_chart_and_info(chart_name)
-        if not func_exists:
-            self.abort_request('The chart does not exist.')
-        if not chart_obj:
-            self.abort_request(f'The chart function {chart_name} returns nothing..')
-        chart_obj.width = '100%'
+        chart_uri = EntityURI.from_params_path('chart', chart_name, self.kwargs.get('chart_params', ''))
+        is_parametric = factory.clean_uri_params(chart_uri)
+        if len(chart_uri.params) == 0 and is_parametric:
+            return self.update_context_for_parametric_chart_without_params(context, chart_uri)
+        # TODO exception ChartDoesNotExist ChartParamsInvalid
+        try:
+            chart_obj, func_exists, chart_info = factory.get_chart_and_info_by_uri(chart_uri)
+            if not func_exists:
+                self.abort_request('The chart does not exist.')
+            if not chart_obj:
+                self.abort_request(f'The chart function {chart_name} returns nothing..')
+            chart_obj.width = '100%'
+            context['chart_info'] = chart_info
+            context['chart_obj'] = chart_obj
+            context['title'] = self.get_dje_page_title(name=chart_info.name, title=chart_info.title)
+            return self.template_name
+        except ChartDoesNotExist as e:
+            self.abort_request(str(e))
+
+    def update_context_for_parametric_chart_without_params(self, context, uri: EntityURI):
+        chart_info = factory.chart_info_manager.get_or_none(uri=uri)
         context['chart_info'] = chart_info
-        context['chart_obj'] = chart_obj
         context['title'] = self.get_dje_page_title(name=chart_info.name, title=chart_info.title)
-        return self.template_name
+        link_group = LinkGroup()
+        for params_dic in chart_info.params_config:
+            link = LinkItem(
+                text=chart_info.title.format(**params_dic),
+                url=reverse_chart_url(EntityURI('chart', chart_info.name, params_dic))
+            )
+            link_group.add_widget(link)
+        context['link_group'] = link_group
+        return 'chart_parametric.html'
 
     def get_dje_page_title(self, name, title, **kwargs):
         return self.page_title.format(name=name, title=title)
@@ -268,29 +293,88 @@ class NavMenuPosition:
 _SLUG_RE = re.compile(r'[-a-zA-Z0-9_]+')
 
 
+class NavBuilder:
+    @staticmethod
+    def build_nav(left_menu_config: list = None, right_menu_config: list = None,
+                  footer_links_config: list = None):
+        left_menu_config = left_menu_config or []
+        right_menu_config = right_menu_config or []
+        footer_links_config = footer_links_config or []
+        nav = Nav()
+        NavBuilder._build_nav(left_menu_config, nav.left_menus)
+        NavBuilder._build_nav(right_menu_config, nav.right_menus)
+        for data in footer_links_config:
+            link = LinkItem(**NavBuilder._parse_params(data))
+            nav.footer_links.append(link)
+        return nav
+
+    @staticmethod
+    def build_default_nav(nav_shown_pages: list):
+        return NavBuilder.build_nav(left_menu_config=nav_shown_pages)
+
+    @staticmethod
+    def _build_nav(menu_config: list, menu_container: list):
+        for data in menu_config:
+            menu = None
+            if isinstance(data, str):
+                menu = Menu(**NavBuilder._parse_params(data))
+            elif isinstance(data, dict):
+                if 'view_name' in data or 'url' in data:
+                    menu = Menu(**NavBuilder._parse_params(data))
+                elif 'children' in data:
+                    menu = Menu(**NavBuilder._parse_params(data))
+                    for data_item in data['children']:
+                        link_item = LinkItem(**NavBuilder._parse_params(data_item))
+                        menu.children.append(link_item)
+            menu_container.append(menu)
+
+    @staticmethod
+    def _parse_params(val: Union[str, dict]):
+        # todo handle uri
+        if isinstance(val, str):
+            if val == Nav.CHART_PLACEHOLDER:
+                return dict(text=val)
+            elif val == 'home':
+                return dict(text='首页', slug='home', url=reverse_lazy('dje_home'))
+            elif val == 'list':
+                return dict(text='所有', slug='list', url=reverse_lazy('dje_list'))
+            elif val == 'collection':
+                return dict(text='合辑', slug='collection-all', url=reverse_lazy('dje_chart_collection_all'))
+            elif val == 'settings':
+                return dict(text='设置', slug='settings', url=reverse_lazy('dje_settings'))
+            else:
+                return dict(text=val)
+        elif isinstance(val, dict):
+            if 'view_name' in val:
+                return dict(text=val.get('text'), slug=val.get('slug'),
+                            url=reverse_lazy(val['view_name'], kwargs=val.get('kwargs', {})))
+            elif 'url' in val:
+                if val['url'].startswith('chart:'):
+                    uri = EntityURI.from_str(val['url'])
+                    url = reverse_lazy('dje_chart_single', args=[uri.name, uri.params_path])
+                else:
+                    url = val['url']
+                return dict(text=val.get('text'), slug=val.get('slug'), url=url)
+            elif 'children' in val:
+                return dict(text=val.get('text'), slug=val.get('slug'))
+
+
 class DJESite:
     """A generator endpoint for visual chart site.
     Example:
         site_obj = DJESite(site_title='MySite', opts=SiteOpts(paginate_by=10))
     """
 
-    def __init__(self, site_title: str, opts: Optional[SiteOpts] = None):
+    def __init__(self, site_title: str, opts: Optional[SiteOpts] = None, ignore_register_nav: bool = False):
         self.site_title = site_title
+        self.ignore_register_nav = ignore_register_nav
         self._custom_urlpatterns = []  # url entry
         if opts is None:
             self._opts = SiteOpts()
         else:
             self._opts = opts
-        self.nav = Nav()
-        if 'home' in self.opts.nav_shown_pages:
-            self.nav.add_menu(text='首页', slug='home', url=reverse_lazy('dje_home'))
-        if 'list' in self.opts.nav_shown_pages:
-            self.nav.add_menu(text='所有', slug='list', url=reverse_lazy('dje_list'))
-        if 'collection' in self.opts.nav_shown_pages:
-            self.nav.add_menu(text='合辑', slug='collection-all', url=reverse_lazy('dje_chart_collection_all'))
-        if 'settings' in self.opts.nav_shown_pages:
-            self.nav.add_right_link(LinkItem(text='设置', slug='settings', url=reverse_lazy('dje_settings')))
-
+        self.nav = NavBuilder.build_default_nav(self._opts.nav_shown_pages)
+        self._chart_nav = Nav()
         self._view_dict = {
             'dje_home': DJESiteHomeView,
             'dje_list': DJESiteListView,
@@ -305,6 +389,8 @@ def __init__(self, site_title: str, opts: Optional[SiteOpts] = None):
 
         self._collection_dic = {}  # type: Dict[str,WidgetCollection]
 
+        self._ref2uri = defaultdict(EntityURI.empty)
+
     def _inject(self, func):
         @wraps(func)
         def decorated(*args, **kwargs):
@@ -314,6 +400,10 @@ def decorated(*args, **kwargs):
 
         return decorated
 
+    @property
+    def ref2uri(self):
+        return self._ref2uri
+
     @property
     def opts(self) -> SiteOpts:
         return self._opts
@@ -327,6 +417,8 @@ def urls(self):
             path('chart/<slug:name>/', self._view_dict['dje_chart_single'].as_view(), name='dje_chart_single'),
             path('chart/<slug:name>/options/', self._view_dict['dje_chart_options'].as_view(),
                  name='dje_chart_options'),
+            path('chart/<slug:name>/<path:chart_params>/', self._view_dict['dje_chart_single'].as_view(),
+                 name='dje_chart_single'),
             path('collection/', self._view_dict['dje_chart_collection'].as_view(), name='dje_chart_collection_all'),
             path('collection/<slug:name>/', self._view_dict['dje_chart_collection'].as_view(),
                  name='dje_chart_collection'),
@@ -334,7 +426,7 @@ def urls(self):
             path('settings/', self._view_dict['dje_settings'].as_view(), name='dje_settings'),
 
         ]
-        urls += geo_urlpatterns + self._custom_urlpatterns
+        urls += custom_map_urlpatterns + self._custom_urlpatterns
         return urls
 
     def extend_urlpatterns(self, urlpatterns):
@@ -348,72 +440,86 @@ def register_view(
     ):
         self._view_dict[view_name] = view_class
 
-    # Init Widgets
-
-    def add_left_link(self, item: LinkItem, menu_title: str = None):
-        if menu_title:
-            self.nav.add_item(menu_text=menu_title, item=item)
-        else:
-            self.nav.add_menu(text=item.text, slug=item.slug, url=item.url)
-        return
-
-    def add_right_link(self, item: LinkItem):
-        """Add link on nav."""
-        self.nav.add_right_link(item)
+    def config_nav(self, nav_config: dict = None):
+        """fill nav from a config dict."""
+        if nav_config:
+            self.nav = NavBuilder.build_nav(
+                left_menu_config=nav_config.get('nav_left'),
+                right_menu_config=nav_config.get('nav_right'),
+                footer_links_config=nav_config.get('nav_footer'),
+            )
+        holder_index = -1
+        for i, menu in enumerate(self.nav.left_menus):
+            if menu.text == Nav.CHART_PLACEHOLDER:
+                holder_index = i
+                break
+        if holder_index == -1:
+            return self
+        self.nav.left_menus = self.nav.left_menus[:holder_index] + self._chart_nav.left_menus + self.nav.left_menus[
+                                                                                                holder_index + 1:]
         return self
 
-    def add_menu_item(self, item: LinkItem, menu_title: str = None):
-        self.add_left_link(item, menu_title)
-        return self
+    def load_config_from_module(self, module_path: str):
+        """Load config from a python module."""
+        __import__(module_path)
+        mod = sys.modules[module_path]
+        nav_config = getattr(mod, 'nav_config')
+        self.config_nav(nav_config)
 
-    def add_footer_link(self, item: LinkItem):
-        self.nav.footer_links.append(item)
-        return self
+    # Init Widgets
 
     def add_widgets(self, *, copyright_: Copyright = None, jumbotron: Jumbotron = None,
                     jumbotron_chart: Union[str, Any] = None, values_panel: Union[str, ValuesPanel] = None):
         """Place widgets to pages."""
-        if copyright_:
-            factory.register_html_widget(copyright_, 'copyright')
-        if jumbotron:
-            factory.register_html_widget(jumbotron, WidgetRefs.home_jumbotron)
-        if isinstance(jumbotron_chart, str):
-            factory.set_chart_ref(WidgetRefs.home_jumbotron_chart, jumbotron_chart)
-        elif jumbotron_chart:
-            factory.register_chart_widget(jumbotron_chart, WidgetRefs.home_jumbotron_chart)
-        if isinstance(values_panel, str):
-            factory.set_html_ref(WidgetRefs.home_values_panel, values_panel)
-        elif isinstance(values_panel, ValuesPanel):
-            factory.register_html_widget(values_panel, WidgetRefs.home_values_panel)
+
+        self._add_widget('copyright', copyright_, 'widget')
+        self._add_widget(WidgetRefs.home_jumbotron, jumbotron, 'widget')
+        self._add_widget(WidgetRefs.home_jumbotron_chart, jumbotron_chart, 'chart')
+        self._add_widget(WidgetRefs.home_values_panel, values_panel, 'widget')
         return self
 
+    def _add_widget(self, ref_name: str, entity_obj_or_name=None, entity_catalog: str = None):
+        if entity_obj_or_name is None:
+            return
+        if isinstance(entity_obj_or_name, str):
+            uri = EntityURI.from_str(entity_obj_or_name, catalog=entity_catalog)
+        else:
+            uri = EntityURI.from_str(ref_name, catalog=entity_catalog)
+            if entity_catalog == 'chart':
+                factory.register_chart_widget(entity_obj_or_name, uri.name)
+            else:
+                factory.register_html_widget(entity_obj_or_name, uri.name)
+        self._ref2uri[ref_name] = uri
+
     # Register function and views class
     def register_chart(self, function=None, *, info: ChartInfo = None, name: str = None, title: str = None,
                        description: str = None, body: str = None, layout: str = None, top: int = 0, catalog: str = None,
-                       tags: List = None, nav_parent_name: str = None, nav_after_separator: bool = False):
+                       tags: List = None, nav_parent_name: str = None, nav_after_separator: bool = False,
+                       params_config: ParamsConfig = None):
         """Register chart function."""
 
         def decorator(func):
             cname = name or func.__name__
             if not _SLUG_RE.match(cname):
                 raise ValueError(f'Invalid chart slug:{cname}')
-            url = reverse_lazy('dje_chart_single', args=(cname,))
+            url = reverse_lazy('dje_chart_single', args=(cname,))  # TODO
             if info:
                 c_info = info
             else:
-                c_info = ChartInfo(name=cname, title=title or cname, description=description, body=body,
-                                   url=url, top=top, catalog=catalog, tags=tags, layout=layout)
+                c_info = ChartInfo(name=cname, title=title or cname, description=description, body=body, url=url,
+                                   top=top, catalog=catalog, tags=tags, layout=layout, params_config=params_config)
             factory.register_chart_widget(func, c_info.name, info=c_info)
-
+            # TODO Delete menu register for single chart
+            #
             if nav_parent_name == 'self':
-                self.nav.add_menu(text=title or cname, slug=cname, url=url)
+                self._chart_nav.add_left_menu(text=title or cname, slug=cname, url=url)
             elif nav_parent_name == 'none':
                 pass
             else:
                 c_nav_parent_name = nav_parent_name or catalog
                 if c_nav_parent_name:
-                    self.nav.add_menu(text=c_nav_parent_name)
-                    self.nav.add_item(
+                    self._chart_nav.add_left_menu(text=c_nav_parent_name)
+                    self._chart_nav.add_item_in_left_menu(
                         menu_text=c_nav_parent_name,
                         item=LinkItem(text=title or cname, url=url, slug=cname),
                         after_separator=nav_after_separator
@@ -454,13 +560,13 @@ def decorator(func):
             url = reverse_lazy('dje_chart_collection', args=(cname,))
             c_title = title or name
             if nav_parent_name == 'self':
-                self.nav.add_menu(text=c_title, url=url)
+                self._chart_nav.add_left_menu(text=c_title, url=url)
             elif nav_parent_name == 'none':
                 pass
             else:
                 c_nav_parent_name = nav_parent_name or catalog
-                self.nav.add_menu(text=c_nav_parent_name)
-                self.nav.add_item(
+                self._chart_nav.add_left_menu(text=c_nav_parent_name)
+                self._chart_nav.add_item_in_left_menu(
                     menu_text=c_nav_parent_name,
                     item=LinkItem(text=c_title, url=url, slug=name),
                     after_separator=nav_after_separator
@@ -485,3 +591,7 @@ def _build_collection_named_all(self) -> WidgetCollection:
             chart_obj = factory.get_chart_widget(info.name)
             w_collection.pack_chart_widget(chart_obj, info)
         return w_collection
+
+    @classmethod
+    def load_from_yaml(cls, yaml_file: str):
+        return cls(ignore_register_nav=True)
diff --git a/django_echarts/stores/entity_factory.py b/django_echarts/stores/entity_factory.py
index 81d5c15..79e6107 100644
--- a/django_echarts/stores/entity_factory.py
+++ b/django_echarts/stores/entity_factory.py
@@ -1,6 +1,7 @@
 from typing import Type, Tuple, Optional, Any, Union
 
-from django_echarts.entities import WidgetGetterMixin, ChartInfoManagerMixin, LocalChartInfoManager, ChartInfo
+from django_echarts.entities import (WidgetGetterMixin, ChartInfoManagerMixin, LocalChartInfoManager, ChartInfo,
+                                     EntityURI)
 from django_echarts.utils.lazy_dict import LazyDict
 
 
@@ -21,6 +22,14 @@ def html_widgets(self) -> LazyDict:
     def chart_widgets(self) -> LazyDict:
         return self._chart_obj_dic
 
+    @property
+    def html_widget_manger(self) -> LazyDict:
+        return self._html_widgets
+
+    @property
+    def chart_manager(self) -> LazyDict:
+        return self._chart_obj_dic
+
     @property
     def chart_info_manager(self) -> ChartInfoManagerMixin:
         return self._chart_info_manager
@@ -36,6 +45,8 @@ def decorator(func):
             else:
                 c_info = info
             if c_info:
+                if self._chart_obj_dic.has_parameters(cname):
+                    c_info.set_bound(False)
                 self.register_chart_info(c_info)
             return func
 
@@ -63,24 +74,25 @@ def set_chart_ref(self, ref_name: str, name: str):
     def set_html_ref(self, ref_name: str, name: str):
         self._html_widgets.set_ref(ref_name, name)
 
-    def get_chart_and_info(self, name: str) -> Tuple[Optional[Any], bool, Optional[ChartInfo]]:
+    def get_chart_and_info(self, name: str, params: dict = None) -> Tuple[Optional[Any], bool, Optional[ChartInfo]]:
         """Execute chart creator and return pyecharts chart object.
         Use get_chart_widget instead if the info is not used.
         """
         if name in self._chart_obj_dic:
+            params = params or {}
             func_exists = True
-            chart_obj = self._chart_obj_dic.get(name)
+            chart_obj = self._chart_obj_dic.get(name, params)
             info_name = self._chart_obj_dic.actual_key(name)
             info = self._chart_info_manager.get_or_none(info_name)
             return chart_obj, func_exists, info
         else:
             return None, False, None
 
-    def get_chart_widget(self, name: str) -> Any:
-        return self._chart_obj_dic.get(name)
+    def get_chart_widget(self, name: str, params: dict = None) -> Any:
+        return self._chart_obj_dic.get(name, params)
 
-    def get_html_widget(self, name: str) -> Any:
-        return self._html_widgets.get(name)
+    def get_html_widget(self, name: str, params: dict = None) -> Any:
+        return self._html_widgets.get(name, params)
 
     def get_widget_by_name(self, name: str) -> Any:
         if name[:5] == 'info:':
@@ -97,5 +109,55 @@ def get_widget_by_name(self, name: str) -> Any:
     def get_chart_total(self) -> int:
         return len(self._chart_obj_dic)
 
+    # Methods related Entity URI
+
+    def clean_uri_params(self, uri: EntityURI) -> bool:
+        """Validate and convert params inplace.And return parametric flag for the name"""
+        if uri.catalog in ('chart', 'info'):
+            uri.params, is_parametric = self.chart_manager.validate_caller_params(uri.name, uri.params)
+        else:
+            uri.params, is_parametric = self.html_widget_manger.validate_caller_params(uri.name, uri.params)
+        return is_parametric
+
+    def get_widget_by_uri(self, uri: EntityURI):
+        if uri.is_empty():
+            return None
+        self.clean_uri_params(uri)
+        if uri.catalog == 'chart':
+            return self._chart_obj_dic.get(uri.name, uri.params)
+        elif uri.catalog == 'info':
+            # info_name = self._chart_obj_dic.actual_key(uri.name)
+            return self._chart_info_manager.get_or_none(uri=uri)
+        else:
+            return self._html_widgets.get(uri.name, uri.params)
+
+    def get_chart_and_info_by_uri(self, uri: EntityURI):
+        if not uri.is_empty() and uri.name in self._chart_obj_dic:
+            self.clean_uri_params(uri)
+            func_exists = True
+            chart_obj = self._chart_obj_dic.get(uri.name, uri.params)
+            # info_name = self._chart_obj_dic.actual_key(uri.name)
+            info = self._chart_info_manager.get_or_none(uri=uri)
+            return chart_obj, func_exists, info
+        else:
+            return None, False, None
+
+    def get_all_chart_uri(self):
+        info_dic = {info.name: info for info in self._chart_info_manager.query_all_chart_info()}
+        for chart_name in self._chart_obj_dic.keys():
+            if self._chart_obj_dic.has_parameters(chart_name):
+                info = info_dic.get(chart_name)
+                if info is not None:
+                    for params_dic in info.params_config:
+                        uri = EntityURI('chart', chart_name, params=params_dic)
+                        yield uri
+                else:
+                    # param functions without provided ParamsConfig.
+                    pass
+            else:
+                uri = EntityURI('chart', chart_name)
+                yield uri
+
 
+# Project entry for entities
 factory = EntityFactory()
diff --git a/django_echarts/templates/snippets/echarts_init_options.tpl b/django_echarts/templates/snippets/echarts_init_options.tpl
index b11254a..fe1e679 100644
--- a/django_echarts/templates/snippets/echarts_init_options.tpl
+++ b/django_echarts/templates/snippets/echarts_init_options.tpl
@@ -1,7 +1,10 @@
-{% for geojson_url, geojson_map_name, chart_list in structured_data %}
-    {% if geojson_url %}
-$.getJSON("{{ geojson_url }}").done(function(mapdata){
-    echarts.registerMap("{{ geojson_map_name }}", mapdata);
+{% for custom_map_item, chart_list in structured_data %}
+    {% if custom_map_item.url %}
+$.get({
+   url: '{{ custom_map_item.url }}',
+   dataType: '{{ custom_map_item.ajax_data_type }}'
+}).done(function(mapData){
+    echarts.registerMap("{{ custom_map_item.map_name }}", {{ custom_map_item.param_str }});
     {% endif %}
     {% for c in chart_list %}
     var chart_{{ c.chart_id }} = echarts.init(
@@ -24,9 +27,9 @@ $.getJSON("{{ geojson_url }}").done(function(mapdata){
     });
     {% endfor %}
 
-    {% if geojson_url %}
+    {% if custom_map_item.url %}
 }).fail(function(jqXHR, textStatus, error){
-    $("#{{ c.chart_id }}").html("Load geojson fail! Status: " + textStatus);
+    $("#{{ c.chart_id }}").html("Load custom map fail! Status: " + textStatus);
 });
     {% endif %}
 {% endfor %}
\ No newline at end of file
diff --git a/django_echarts/templatetags/echarts.py b/django_echarts/templatetags/echarts.py
index d8e75a0..d5c6eb1 100644
--- a/django_echarts/templatetags/echarts.py
+++ b/django_echarts/templatetags/echarts.py
@@ -10,9 +10,10 @@
 from django.template.loader import render_to_string, get_template
 from django.utils.safestring import SafeString
 from django_echarts.conf import DJANGO_ECHARTS_SETTINGS
-from django_echarts.entities import LinkItem, Menu
+from django_echarts.entities import LinkItem, Menu, EntityURI
 from django_echarts.renders import render_widget, flat_chart, get_js_dependencies
 from django_echarts.utils.burl import burl_kwargs
+from django_echarts.site_exts import reverse_chart_url
 
 register = template.Library()
 
@@ -42,15 +43,17 @@ def echarts_js_dependencies(context, *args):
 
 def build_echarts_initial_fragment(*args):
     chart_list = flat_chart(args)
-    structured_dic = defaultdict(list)
+    structured_dic = defaultdict(list)  # key = CustomMapItem
     for chart in chart_list:
         if hasattr(chart, '_is_geo_chart'):
             chart.is_geo_chart = chart._is_geo_chart
         chart.dje_echarts_theme = DJANGO_ECHARTS_SETTINGS.opts.get_echarts_theme(chart.theme)
-        geojson_url = chain_getattr(chart, 'geojson.url', '')
-        geojson_name = chain_getattr(chart, 'geojson.map_name', '')
-        structured_dic[(geojson_url, geojson_name)].append(chart)
-    structured_data = [(k[0], k[1], v) for k, v in structured_dic.items()]
+        map_item = getattr(chart, 'custom_map_item', None)
+        if map_item:
+            structured_dic[map_item].append(chart)
+        else:
+            structured_dic[None].append(chart)
+    structured_data = list(structured_dic.items())
     context = {'structured_data': structured_data}
     return SafeString(render_to_string('snippets/echarts_init_options.tpl', context))
 
@@ -118,3 +121,10 @@ def page_link(context, page_number: int):
 @register.simple_tag(takes_context=True)
 def dw_link(context, item: Union[LinkItem, Menu], class_: str = None):
     return render_widget(item, context=context, class_=class_)
+
+
+# Site urls
+@register.simple_tag(takes_context=True)
+def url_single_chart(context, uri_or_name: Union[EntityURI, str], params_dic: dict = None):
+    # TODO use {% dw uri %} <a>?
+    return reverse_chart_url(uri_or_name, params_dic)
diff --git a/django_echarts/utils/compat.py b/django_echarts/utils/compat.py
index a8d745d..3d363f8 100644
--- a/django_echarts/utils/compat.py
+++ b/django_echarts/utils/compat.py
@@ -1,7 +1,15 @@
+from django import VERSION as django_version
 from django.core.paginator import Paginator
 
 
-def get_elided_page_range(paginator: Paginator, number=1, *, on_each_side=3, on_ends=2):
+def compat_get_elided_page_range(paginator: Paginator, number=1, *, on_each_side=3, on_ends=2):
+    if django_version > (3, 2):
+        return paginator.get_elided_page_range(number, on_each_side=on_each_side, on_ends=on_ends)
+    else:
+        return _get_elided_page_range(paginator, number, on_each_side=on_each_side, on_ends=on_ends)
+
+
+def _get_elided_page_range(paginator: Paginator, number=1, *, on_each_side=3, on_ends=2):
     """
     Return a 1-based range of pages with some values elided.
 
diff --git a/django_echarts/utils/lazy_dict.py b/django_echarts/utils/lazy_dict.py
index d57eb4c..8c74184 100644
--- a/django_echarts/utils/lazy_dict.py
+++ b/django_echarts/utils/lazy_dict.py
@@ -1,4 +1,10 @@
 from functools import wraps
+import inspect
+from typing import Tuple
+
+
+class ParameterMissingError(BaseException):
+    pass
 
 
 class LazyDict:
@@ -8,6 +14,7 @@ def __init__(self):
         self._entries = {}
         self._opt_dic = {}
         self._dynamic_names = []
+        self._dynamic_parameters = {}
         self._refs = {}  # dict[str,str]
 
     def func_register(self, obj, name: str = None, **kwargs):
@@ -17,6 +24,9 @@ def func_register(self, obj, name: str = None, **kwargs):
             raise TypeError('The name must not be empty.')
         if callable(obj):
             self._dynamic_names.append(name)
+            parameters = inspect.signature(obj).parameters
+            if len(parameters) > 0:
+                self._dynamic_parameters[name] = parameters
         self._entries[name] = obj
         self._opt_dic[name] = kwargs
         return self
@@ -36,15 +46,17 @@ def decorator(func):
             return decorator(function)
 
     def set_ref(self, ref_name: str, name: str):
+        """Set a alias name for the exist name."""
         self._refs[ref_name] = name
         return self
 
-    def get(self, name: str):
+    def get(self, name: str, caller_kwargs: dict = None):
         if name in self._refs:
             name = self._refs[name]
         if name in self._entries:
             if name in self._dynamic_names:
-                return self._entries[name]()
+                caller_kwargs = caller_kwargs or {}
+                return self._entries[name](**caller_kwargs)
             else:
                 return self._entries[name]
         return
@@ -56,6 +68,10 @@ def actual_key(self, name):
             return name
         return
 
+    def keys(self):
+        for k in self._entries.keys():
+            yield k
+
     def items(self):
         for k in self._entries.keys():
             yield k, self.get(k)
@@ -73,6 +89,42 @@ def decorated(*args, **kwargs):
 
         return _inner
 
+    def validate_caller_params(self, func_name: str, param_dic: dict) -> Tuple[dict, bool]:
+        """Validate and convert values with type annotation in registered function.
+        """
+        declare_dic = self._dynamic_parameters.get(func_name, None)
+        is_parametric = declare_dic is not None
+        if len(param_dic) == 0 or not is_parametric:
+            return param_dic, is_parametric
+        new_param_dic = {}
+        extra_param_names = []
+        for name, value in param_dic.items():
+            if name in declare_dic:
+                if declare_dic[name].annotation is not inspect.Parameter.empty:
+                    new_val = declare_dic[name].annotation(value)  # Convert value
+                    new_param_dic[name] = new_val
+                else:
+                    new_param_dic[name] = value
+            else:
+                extra_param_names.append(name)
+        valid = False
+        for name, parameter in declare_dic.items():
+            if parameter.kind == inspect.Parameter.VAR_KEYWORD:
+                new_param_dic.update({k: v for k, v in param_dic.items()})
+                valid = True
+                break
+            if parameter.default is not inspect.Parameter.empty and name not in param_dic:
+                new_param_dic[name] = parameter.default
+        if not valid and len(extra_param_names) == 0:
+            valid = True
+        if not valid:
+            raise ParameterMissingError(f'These parameters are required: {",".join(extra_param_names)}')
+        return new_param_dic, is_parametric
+
+    def has_parameters(self, name: str) -> bool:
+        name = self.actual_key(name)
+        return name in self._dynamic_parameters
+
     def __contains__(self, item):
         return item in self._entries or item in self._refs
 
diff --git a/django_echarts/utils/param_converters.py b/django_echarts/utils/param_converters.py
new file mode 100644
index 0000000..d425eb2
--- /dev/null
+++ b/django_echarts/utils/param_converters.py
@@ -0,0 +1,16 @@
+from datetime import datetime
+
+
+class DateConverter:
+    regex = '[0-9]{8}'
+
+    def to_python(self, value):
+        return datetime.strptime(value, '%Y%m%d').date()
+
+    def to_url(self, value):
+        return value.strftime('%Y%m%d')
+
+
+DJE_CONVERTERS = {
+    'date': DateConverter()
+}
diff --git a/docs/about.md b/docs/about.md
index b92b39e..f4069bf 100644
--- a/docs/about.md
+++ b/docs/about.md
@@ -26,6 +26,7 @@
 - PrettyTable: [https://github.com/jazzband/prettytable](https://github.com/jazzband/prettytable)
 - pyecharts: [https://pyecharts.org/](https://pyecharts.org/)
 - pyecharts-asserts: [https://github.com/pyecharts/pyecharts-assets](https://github.com/pyecharts/pyecharts-assets)
+- typing_extensions [https://github.com/python/typing_extensions](https://github.com/python/typing_extensions)
 - yattag: [https://www.yattag.org/](https://www.yattag.org/)
 
 
diff --git a/docs/changelog.md b/docs/changelog.md
index df9aa78..6deaa52 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -1,5 +1,20 @@
 # 更新日志
 
+## v0.6.0 - (20230317)
+
+[发布日志](/release-note/v060)
+
+- 新增pycharts2.0.x适配
+- 新增参数化图表支持
+- 重构自定义地图，支持 geojson&svg格式
+- 新增资源类 `EntityURI`
+- 新增 `WidgetNotRegisteredError` 异常
+- `WidgetGetterMixin` 接口类支持 `EntityURI`
+- 修改导航栏(Nav)配置方式
+- 右侧导航栏支持二级菜单
+- Borax 更新至 4.0
+- typing_extensions 更新至 4.5
+
 ## v0.5.2 - （20220508）
 
 - 新增 `ElementEntity` HTML组件
diff --git a/docs/guides/custom_maps.md b/docs/guides/custom_maps.md
new file mode 100644
index 0000000..9a26f43
--- /dev/null
+++ b/docs/guides/custom_maps.md
@@ -0,0 +1,173 @@
+# 自定义地图
+
+> Add in 0.6.0
+>
+
+## 概述
+
+### 功能支持
+
+django-echarts 支持echarts的自定义地图（由 `registerMap` 注册的地图），包括 geojson 和 svg 两种类型。
+
+| django-echarts | echarts版本           | 支持类型     | 支持模块                     |
+| -------------- | --------------------- | ------------ | ---------------------------- |
+| 0.5.x          | pyecharts1.9/echarts4 | geojson      | `django_echarts.geojson`     |
+| 0.6.x          | pyecharts1.9/echarts4 | geojson      | `django_echarts.custom_maps` |
+|                | pyecharts2.0/echarts5 | geojson、svg | `django_echarts.custom_maps` |
+
+### 模板渲染：内嵌VS外链
+
+**pyecharts**: pyecharts  使用内嵌方式，函数 `Map.add_geo_json(geo_json:dict)` 传入的是 geojson数据本身。geojson内容读取在python端实现。
+
+```python
+geo_json = {<geojson_data>}
+
+(
+    Map()
+    .add(
+        series_name="商家A",
+        data_pair=[list(z) for z in zip(Faker.provinces, Faker.values())],
+        maptype="custom_geo_json",
+    )
+    .add_geo_json(geo_json=geo_json)
+    .set_global_opts(title_opts=opts.TitleOpts(title="Map-基本示例"))
+    .set_series_opts(label_opts=opts.LabelOpts(is_show=False))
+    .render()
+)
+```
+
+**django-echarts** : django-echarts 全部采用外链方式，不支持内嵌方式。`use_costom_map` 第三个参数传入的是geojson文件的DataURL或与DataURL关联的文件名称。geojson内容读取在javascript通过ajax读取。
+
+```python
+chart_obj = (
+    Map()
+    .add(
+        series_name="商家A",
+        data_pair=[list(z) for z in zip(Faker.provinces, Faker.values())],
+        maptype="福建省地图",
+    )
+    .set_global_opts(title_opts=opts.TitleOpts(title="Map-基本示例"))
+    .set_series_opts(label_opts=opts.LabelOpts(is_show=False))
+)
+use_custom_map(chart_obj, '福建省地图', 'https://geo.datav.aliyun.com/areas_v3/bound/350000_full.json')
+```
+
+### 文件路由：DataURL VS FileURL
+
+对于一个geojson或svg文件，有DataURL 和 FileURL 之分。
+
+| 区分                  | FileURL                    | DataURL                                                    |
+| --------------------- | -------------------------- | ---------------------------------------------------------- |
+| Rsponse内容           | 二进制文件                 | 文件内容，字符串或json格式                                 |
+| Response Content Type | `application/octet-stream` | `application/json` (适用geojson) 或 `text/plain` (适用svg) |
+| 视图处理模块          | `django.staticfiles`       | `django_echarts.custom_maps`                               |
+
+## 函数use_custom_map
+
+### 使用方法
+
+```python
+django_echarts.custom_maps.use_custom_map(chart_obj, map_name:str, url_or_filename:str='')
+```
+
+该函数作用类似于 pyecharts 中的 `add_geo_json`，为某个图表对象添加自定义地图，一个地图对象只能添加一个。各参数意义如下：
+
+| 参数            | 描述                                                  |
+| --------------- | ----------------------------------------------------- |
+| chart_obj       | pyecharts图表对象                                     |
+| map_name        | 字符串。地图名称，`echarts.regiterMap` 的第一个参数。 |
+| url_or_filename | 字符串。远程地址或本地地图文件名                      |
+
+需要注意的是：
+
+第一，参数 `map_name` 必须和 关联图表的` maptype` 配置保持一致，否则无法正确加载底图。
+
+第二，在 pyecharts1.9/echarts4时，不支持 svg格式。
+
+第三，在使用时必须由使用者确定地图类型是 geojson 还是 svg。规则是 `name` 和 `url` 至少有一个以 `.geojson` 、`.json`、 `.svg` 结尾。
+
+```python
+use_custom_map(chart_obj, '福建省地图', 'https://geo.datav.aliyun.com/areas_v3/bound/350000_full.json')
+use_custom_map(chart_obj, '福建市县地图', 'fujian.geojson')
+use_custom_map(chart_obj, 'flight_seats', '/custom_maps/flight_seats.svg')
+
+use_custom_map(chart_obj, 'flight_seats', 'flight_seats') # 错误的使用
+```
+
+不符合以上两个条件时该函数将抛出 `ValueError` 异常。
+
+### 参数url_or_filename
+
+参数 `url_or_filename` 支持以下方式的字符串：
+
+| 地图引用方式   | 赋值要求                                        | 描述                     |
+| -------------- | ----------------------------------------------- | ------------------------ |
+| 按URL引用      | url 以 `https://` 或 `http://` 开头             | 引用远程地图<sup>1</sup> |
+| 按filename引用 | 文件名，不以`/` 开头，以`.geojson` 或`.svg`结尾 |                          |
+|                | 空值                                            | 默认同 `map_name`        |
+
+备注：
+
+1. 该 url 必须是 DataURL。即浏览器打开该url时可以查看其文件内容，而不是执行下载该文件。
+
+
+
+## 文件名引用
+
+```
+filename ---(django_echarts.custom_maps)--> DataURL ----> FileURL --(django.staticfiles)--> file path
+```
+
+
+
+文件名引用方式依赖于 `django.staticfiles`。对于每一个地图文件，FileURL 和 DataURL 对应关系如下：
+
+| URL类型 | 链接格式                                    | 说明                                                         |
+| ------- | ------------------------------------------- | ------------------------------------------------------------ |
+| FileURL | `{STATIC_URL}assets/custom_maps/{filename}` | 由 `django.staticfiles` 处理，返回 `HttpFileResponse`        |
+|         | `{STATIC_URL}/geojson/{filename}`           | 仅兼容0.5.0，不再推荐适用                                    |
+| DataURL | `/map_data/{filename}`                      | 由 `django_echarts.custom_maps` 处理，返回 `JSONResponse` 或 `HttpResponse` |
+
+## 完整示例
+
+第一步，准备地图数据文件 *china_cities.geojson*。
+
+第二步，在图表中添加地图配置。下面两种写法基本是等效的，区别在于 `echarts.registrMap` 第一个参数值，不过这不影响开发和运行。
+
+```python
+use_custom_map(map_name='中国省市地图', url_or_filename='china_cities.geojson')
+use_custom_map(map_name='china_cities.geojson')
+```
+
+第三步，确定  *china_cities.geojson* 放置的路径。
+
+假定项目的 *settings.py* 配置如下：
+
+```python
+import os
+from pathlib import Path
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve().parent.parent
+
+STATIC_URL = '/static/'
+STATICFILES_DIRS = (
+    os.path.join(BASE_DIR, 'static').replace('\\', '/'),
+)
+```
+
+则项目目录结构及geojson文件路径如下：
+
+```text
+- zinc/
+  |- zinc/
+    |- settings.py
+    |- urls.py
+  |- static/
+    |- assets/
+      |- maps/
+        |- china.js
+      |- custom_maps/
+        |- china_cities.geojson # <--- 文件路径
+```
+
diff --git a/docs/guides/dms.md b/docs/guides/dms.md
index 3e5fdfc..19dc33a 100644
--- a/docs/guides/dms.md
+++ b/docs/guides/dms.md
@@ -1,20 +1,63 @@
-# 依赖项和静态文件
-
-## 概述
+# 依赖项(一)：基本配置
 
 django-echarts的依赖项模块对 pyecharts 作了进一步扩展：
 
 - 完全兼容 pyecharts 仓库引用逻辑。
-- 支持自定义单项引用
-- 提供下载命令以支持本地化。
+- 支持混合仓库源，可对单个依赖项单独引用。
+- 提供一系列生产力命令和界面支持仓库源切换和依赖项检测。
+
+## 基本概念
+
+### 依赖项
+
+依赖项，渲染html图表所需要的javascript库文件和数据文件，具体来说：
+
+| 类别                      | 文件格式              | 支持版本设置 | 默认仓库源 | 描述                      |
+| ------------------------- | --------------------- | ------------ | ---------- | ------------------------- |
+| echarts及其扩展           | js                    | 是           | pyecharts  | echarts核心及其扩展库     |
+| echarts主题文件           | js                    | 否           | pyecharts  | echarts主题文件           |
+| pyecharts地图<sup>1</sup> | js                    | 否           | pyecharts  | 由pyecharts托管的数据文件 |
+| 自定义地图<sup>2</sup>    | json、svg<sup>3</sup> | 否           | 需自行指定 | geojson/svg底图           |
+
+注：
+
+1. 原为echarts内置地图，echarts5.x不再内置。
+2. 相关接口由 django-echarts封装。
+2. svg格式仅pyecharts2.0/echarts5.4以上支持
+
+每个依赖项(参见`django_echarts.core.localfiles.DownloaderResource`)拥有的属性如下：。
+
+| 属性       | 类型 | 描述         |
+| ---------- | ---- | ------------ |
+| dep_name   | str  | 依赖项名称   |
+| remote_url | str  | 远程地址     |
+| ref_url    | str  | 引用路径     |
+| local_path | str  | 本地文件路径 |
 
 django-echarts 和 pyecharts 共用底层静态文件存储字典 ，具体可参见 `pyecharts.datasets` 包。
 
-## 仓库源
+### 仓库源
 
 > DJEOpts.dms_repo:str = 'pyecharts'
+>
+> DJEOpts.echarts_version = '4.8.0'
+
+仓库源，存储依赖项实际的网络或本地目录，可通过url访问，其中本地目录由django staticfile或其他apache等服务器托管。仓库源使用 `#repo` 形式标识。
 
-django-echarts默认使用 pyecharts-asserts 作为默认源。
+一个完整的仓库目录如下，参见 pyecharts-asserts：
+
+```
+assets/
+  |- maps/
+    |- china.js
+  |- themes/
+    |- white.js
+  echarts.min.js
+  echarts-wordcloud.min.js
+  jquery.min.js
+```
+
+django-echarts默认使用网络仓库源。
 
 ```python
 DJANGO_ECHARTS = DJEOpts(
@@ -23,38 +66,110 @@ DJANGO_ECHARTS = DJEOpts(
 )
 ```
 
+### 内置仓库
+
 django-echarts 内置下列仓库源。
 
-| 引用标识  | 仓库URL                              | 备注                                                       |
-| --------- | ------------------------------------ | ---------------------------------------------------------- |
-| pyecharts | https://assets.pyecharts.org/assets/ | 在线引用                                                   |
-| local     | /static/assets/                      | 使用django静态文件托管，需要通过下载器先下载到项目目录之下 |
+| 引用标识     | 仓库URL                                 | 备注                                                         |
+| ------------ | --------------------------------------- | ------------------------------------------------------------ |
+| pyecharts    | https://assets.pyecharts.org/assets/    | 基于echarts4.8的仓库                                         |
+| pyecharts-v5 | https://assets.pyecharts.org/assets/v5/ | 基于echarts5.4的仓库                                         |
+| local        | `{STATIC_URL}assets/`                   | 使用django静态文件托管，路径前缀由 `settings.STATIC_URL` 确定 |
+
+各个仓库所包含的文件如下：
+
+
+
+| 类别            | pyecharts | pyecharts-v5 | unpkg | local    |
+| --------------- | --------- | ------------ | ----- | -------- |
+| echarts及其扩展 | √         | √            | √     | 用户指定 |
+| echarts主题文件 | √         | √            |       | 用户指定 |
+| pyecharts地图   | √         | √            |       | 用户指定 |
+| 自定义地图      |           |              |       | 用户指定 |
 
-可以将 dms_repo 参数设置为你的远程仓库url，关于如何组织仓库内部目录结构，请参考 pyecharts 相关文档。
+注：
 
 ## 自定义依赖项
 
-> DJEOption:dep2url = {}
+> DJEOption.dep2url = {}
+
+### 基本设置
 
 django-echarts 支持对单个依赖进行单独引用，dep2url 的配置优先于上述的 dms_repo 配置。
 
+`dep2url`为字典形式，其中 key 为依赖项名称，value 为对应文件引用的 url 或仓库标签。
+
+在一些情况，对于某些文件可以改用其他仓库源。比如，在下面例子下除了 `dep2url` 之外其他依赖项均引用 pyecharts 的资源。
+
 ```python
 DJANGO_ECHARTS = {
+    'dms_repo':'pyecharts',
     'dep2url':{
-        'echarts': 'https://cdnjs.cloudflare.com/ajax/libs/echarts/4.8.0/echarts.min.js',
-        'echarts-gl': 'https://assets.pyecharts.org/assets/echarts-gl.min.js'
+        'echarts-wordcloud': 'https://unpkg.com/echarts-wordcloud@1.1.4/dist/echarts-wordcloud.min.js',
+        'china':'#local',
+        'fujian_cities_map':'/static/assets/maps/fujian_cities_map.js'
     }
 }
 ```
 
-对于一些常用的依赖项（主要指的是 echarts 和jquery），可以使用 `#CDN` 方式引用。
+说明：
+
+- pyecharts上的 echarts-wordcloud版本为1.1.3，如果后续发布的1.1.4版本解决了1.1.3的一些影响使用的bug，可以通过上述方法改用1.1.4版本。
+- 如果中国地图文件 *china.js* 是经常使用的，可以先下载到本地，再使用 `#local` 改用本地的文件。
+
+### 批量设置
+
+> Add in 0.6.0
+
+`dep2url` 支持批量设置
 
 ```python
 DJANGO_ECHARTS = {
     'dep2url':{
-        'echarts': '#cdnjs',
-        'echarts-gl': '#pyecharts'
+        '#local': ['echarts', 'china']
+    }
+}
+# 等效于
+
+DJANGO_ECHARTS = {
+    'dep2url':{
+        'echarts': '#local',
+        'china': '#local'
     }
 }
 ```
 
+
+
+## 默认值
+
+django-echarts 0.6.x + pyecharts2.0默认配置如下：
+
+```python
+{
+    'echarts_version': '5.4.1',
+    'dms_repo': 'pyecharts-v5',
+    'dep2url':{}
+}
+```
+
+django-echarts 0.6.x + pyecharts1.9默认配置如下：
+
+```python
+{
+    'echarts_version': '4.8.0',
+    'dms_repo': 'pyecharts',
+    'dep2url':{}
+}
+```
+
+django-echarts 0.5.x + pyecharts1.9默认配置如下：
+
+```python
+{
+    'echarts_version': '4.8.0',
+    'dms_repo': 'pyecharts',
+    'dep2url':{}
+}
+```
+
diff --git a/docs/guides/entity_params.md b/docs/guides/entity_params.md
new file mode 100644
index 0000000..95e02f1
--- /dev/null
+++ b/docs/guides/entity_params.md
@@ -0,0 +1,153 @@
+# 参数化图表
+
+> Add in 0.6.0
+
+从0.6.0开始，django-echarts 支持参数化图表，对于同一个图表，可以根据传入的数据参数，构建不同的图表，这些图表的配置是相同的，只是数据有所不同。
+
+## 基础例子
+
+实现一个参数化图表很简单，只需在图表注册函数添加参数即可。
+
+```python
+from django_echarts.entity.uri import ParamsConfig
+from django_echarts.starter.sites import DJESite
+from pyecharts import options as opts
+from pyecharts.charts import Bar
+
+site_obj = DJESite(site_title='福建统计')
+
+@site_obj.register_chart(title='{year}年福建省家庭户类型组成', 
+                        params_config=ParamsConfig({'year': [1982, 1990, 2000, 2010, 2020]}))
+def yearly_family_types(year: int):
+    family_types = [
+        '一人户', '二人户', '三人户', '四人户', '五人户', '六人户', '七人户', '八人户', '九人户', '十人及其以上'
+    ]
+    data = [
+        [1982, 7.7, 8.2, 12.2, 17.1, 18.4, 14.7, 10.1, 11.6, 0, 0],
+        [1990, 5.8, 8.6, 16.8, 23.6, 21.4, 11.8, 5.9, 2.9, 1.4, 1.8],
+        [2000, 9.1, 15.5, 25.4, 24.7, 15.8, 5.9, 2.2, 0.8, 0.3, 0.3],
+        [2010, 12.1, 17.2, 24.3, 21.7, 13.7, 6.4, 2.6, 1.1, 0.5, 0.4],
+        [2020, 27.3, 26.3, 19.4, 14.2, 6.9, 4, 1.1, 0.4, 0.2, 0.2]
+    ]
+    yearly_data = {item[0]: item[1:] for item in data}
+    if year not in yearly_data:
+        raise ChartDoesNotExist(f'暂无{year}年数据')
+    year_data = yearly_data[year]
+    bar = (
+        Bar()
+            .add_xaxis(family_types).add_yaxis('百分比(%)', year_data)
+            .set_global_opts(title_opts=opts.TitleOpts("福建省家庭户类型构成-{}年".format(year)))
+    )
+    return bar
+```
+
+### 定义图表参数
+
+图表参数位于图表注册函数（为 `DJESite.register` 或 `EntityFactory.register` 所修饰的函数）的参数列表，你可以和正常函数一样定义任何参数。
+
+django-echarts 推荐尽可能遵循下列明确性的规则：
+
+- 为每个参数声明数据类型，在URL方式调用下能够将其转化为对应的数据类型值，目前仅支持 `int / float / str / UUID` 等构造函数为`type(s)`的数据类型。
+- 不推荐使用 `*args` ，使用 `**kwags`。
+
+### 定义关联ChartInfo
+
+`DJESite.register` 或 `EntityFactory.register` 装饰器的三个参数 title，description，body支持模板字符串，如上述例子中的 `{year}年福建省家庭户类型组成`。
+
+**注意：此处使用str.format的单括号形式，而不是Django模板的双括号形式。**
+
+### 返回图表
+
+如果用户输入的参数没有对应的图表，可以抛出 `ChartDoesNotExist` 异常。
+
+### 参数配置ParamsConfig
+
+`params_config` 用于定义可选参数项，仅用于无参数时的图表页面显示。
+
+在构建图表对象时不会检查参数是否符合在此配置之中，因此在图表构建函数中仍需检查相应的参数，对不符合的情况抛出 `ChartDoesNotExist 异常`。
+
+`params_config` 必须是支持下列接口的对象。
+
+```python
+class ParamsConfigMixin:
+    def __iter__(self) -> Generator[dict, Any, None]:
+        pass
+```
+
+如下列两种方式定义是等效的。
+
+```python
+pc1 = ParamsConfig([
+    {'year':2021, 'month':1}, {'year':2021, 'month':2},
+    {'year':2021, 'month':3}, {'year':2021, 'month':4}
+])
+
+pc2 = ParamsConfig({
+    'year':[2021],
+    'month':[1, 2, 3, 4]
+})
+```
+
+
+
+## EntityURI
+
+为统一标识django-echarts的实体（图表、组件）引入`ENtityURI` 类，该类的一个对象由类别(catalog)、名称(name)、参数(params)三部分组成。例如上述例子中2020年的资源标识可表示为：
+
+```python
+family_types_2020_uri = EntityURI(catalog='chart', name='yearly_family_types', params={'year':2020})
+```
+
+`EntityURI` 还有字符串形式，其格式如下:
+
+```
+<资源类别>:<资源名称>/<参数名称1>/<参数值1>/<参数名称2>/<参数值2>/<参数名称3>/<参数值3>/
+```
+
+在多个参数情况下，其参数顺序不受影响，因此一个图表可能有多个字符串标识。
+
+例如：上述例子可表示为
+
+```
+chart:yearly_family_types/year/2020
+```
+
+关联的`ChartInfo`表示为
+
+```
+info:yearly_family_types/year/2020
+```
+
+字符串形式通常用于URL、合辑、页面组件构建。
+
+```python
+site_obj.add_widgets(jumbotron_chart='chart:yearly_family_types/year/2020') # 首页jumbotron位置显示2020年的数据
+
+
+
+
+rc = RowContainer()
+rc.add_widget('chart:yearly_family_types/year/2020')
+rc.add_widget(EntityURI(catalog='chart', name='yearly_family_types', params={'year':2020}))
+```
+
+今年的数据总是
+
+## 使用图表
+
+### 从EntityFactory引用
+
+所有的图表和HTML组件均存储在全局变量 `django_echarts.stores.entity_factory.factory` 中。`EntityFactory` 提供两种方式引用
+
+```python
+from django_echarts.stores.entity_factory import factory
+
+family_types_chart_2020 = factory.get_chart_widget('yearly_family_types', params={'year':2020}) # 使用参数字典
+
+family_types_chart_2020 = factory.get_widget_by_uri(family_types_2020_uri) # 使用uri方式
+```
+
+### 从ChartInfo引用
+
+使用 `ChartInfo.uri` 属性。
+
diff --git a/docs/index.md b/docs/index.md
index 2098108..27a9040 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -23,11 +23,13 @@ django-echarts 是一个基于[pyecharts](https://github.com/pyecharts/pyecharts
 
 ## 安装
 
-django-echarts包运行环境要求如下：
+django-echarts的运行环境要求如下：
 
-| django-echarts版本系列 | pyecharts | django | python | 备注 |
-| ------ | ------ | ------ | ----- | ----- |
-| 0.5.x | 1.9+ | 2.0+ | 3.7+ | 开发维护中 |
+| django-echarts版本系列 | pyecharts & echarts | django | python |
+| ------ | ------ | ----- | ----- |
+| 0.6.x | 1.9 & 4.8.0 | 2.0 - 4.1 | 3.7+ |
+|  | 2.0 & 5.4.1 | 2.0 - 4.1 | 3.7+ |
+| 0.5.x | 1.9 & 4.8.0 | 2.0 - 4.1 | 3.7+ |
 
 可以使用 pip 命令安装。
 
@@ -35,6 +37,14 @@ django-echarts包运行环境要求如下：
 pip install django-echarts
 ```
 
+在 *requirements.txt* 引用 django-charts时，推荐使用 **固定次版本号** 的方式。
+
+```
+django-echarts~=0.6
+```
+
+
+
 ## 3分钟上手
 
 **1.** 创建Django项目。
@@ -57,17 +67,29 @@ MyDemo
   |-- manage.py
 ```
 
-**2.** 添加 django_echarts包和对应的主题包到项目配置模块的 `INSTALL_APPS`列表。
+**2.** 修改项目配置文件 *settings.py*。
 
 ```python
+# ...
+
 INSTALL_APPS = (
     # Your apps
     'django_echarts',
     'django_echarts.contrib.bootstrap5'
     # Your apps
 )
+
+# ...
+DJANGO_ECHARTS = {
+}
+
 ```
 
+主要包括：
+
+- 添加 django_echarts包和对应的主题包到项目配置模块的 `INSTALL_APPS`列表。
+- 配置字典 `DJANGO_ECHARTS` 可以留空。
+
 **3.** 创建新文件 *site_views.py* ，输入下列代码。
 
 ```python
diff --git a/docs/posts/new_page_feature.md b/docs/posts/new_page_feature.md
index ba1ac66..cdc5c00 100644
--- a/docs/posts/new_page_feature.md
+++ b/docs/posts/new_page_feature.md
@@ -40,9 +40,6 @@ class MyPageView(DJESiteBackendView):
 
 ```python
 site_obj = MySite()
-site_obj.add_left_link(
-    LinkItem(title='我的页面', url=reverse_lazy('view_my_page'))
-)
 
 site_obj.extend_urlpatterns([
     path('mypage/', MyPageView.as_view(), name='view_my_page')
diff --git a/docs/posts/pyecharts_project.md b/docs/posts/pyecharts_project.md
new file mode 100644
index 0000000..d1bf563
--- /dev/null
+++ b/docs/posts/pyecharts_project.md
@@ -0,0 +1,121 @@
+# pyecharts项目
+
+> 本文介绍了pyecharts项目中与django-echarts相关的逻辑和django-echarts对其适配扩展的原理和实现。
+
+> 本文以 pyecharts1.9和2.0为例。
+
+## 图表支持
+
+### 图表类型支持
+
+pyecharts 的图表类体系如下：
+
+```
+- ChartMixin
+  |- Base
+  |- CompositeMixin
+    |- Page
+    |- Tab
+  |- Table
+  |- Image
+```
+
+
+
+django-echarts对图表类型支持情况如下：
+
+- 仅支持所有继承自 `pyecharts.charts.base.Base` 的图表类型，即使用一个echarts即可定义的图表，包括 `Grid` 和 `Timeline`。
+- 对于涉及到布局的 `CompositeMixin` ，django-echarts 提供了 `RowContainer` 、`Container` 等布局容器类。
+- 可直接支持 `pyecharts.components.Table`，但更推荐直接使用 `prettytable.PrettyTable`。
+
+代码实现方面，在 `render_widget` 时也直接引用该类。
+
+```python
+@render_widget.register(Base)
+def render_chart(widget, **kwargs) -> SafeString:
+    pass
+
+@render_widget.register(Table)
+@render_widget.register(PrettyTable)
+def render_table(widget, **kwargs) -> SafeString:
+    pass
+```
+
+### 图表属性和方法
+
+图表属性和方法包括三个部分：
+
+- 图表选项Opts：完全支持
+- 依赖项js_dependencies：兼容性支持，直接使用 `list` 类型并允许项重复。依赖项去重工作在模板渲染前（调用模板函数）完成，这更加适用于同一页面有多个图表的情况。
+- geojson地图：不支持pyecharts的内联引用方式。django-echarts提供了另一种外链引用方式。
+
+`django_echarts.entities.chart_widgets.BlankChart` 是与 `Base` 相对应的接口类，通过比较二者的区别可以看出 django-echarts的改造扩展。
+
+## 图表渲染
+
+django-echarts 采用 Django Template Engine 而不是 Jinja2 ，因此模板函数/标签、模板tpl文件全部重写。
+
+模板函数有三个，均定义在 `echarts` 模块。
+
+```html
+{% load echarts %}
+
+{% dw_widget chart_obj %}
+{% echarts_js_dependencies chart_obj %}
+{% echarts_js_content chart_obj %}
+```
+
+其中后两个对标于 pyecharts的macro 函数。
+
+| django-echarts模板函数               | pyecharts Jinja2 Macro           | 说明           |
+| ------------------------------------ | -------------------------------- | -------------- |
+| echarts_js_dependencies(*chart_list) | render_chart_dependencies(chart) | 渲染依赖项     |
+| echarts_js_content(*chart_list)      | render_chart_content(chart)      | 渲染初始化脚本 |
+
+与pyecharts不同的是，django-echarts的模板支持所有在 `django_echarts.renders` 注册的类，不仅包括图表类，也包括普通的HTML组件和容器组件。
+
+代码实现方面，主要依赖于被 `singledispatch` 装饰的 `flat_chart` 函数。
+
+模板文件定义在 *django_echarts/templates/snippets/echarts_init_options.tpl* 文件中，该文件使用 Django模板语法。
+
+## 依赖项静态资源引用
+
+主要功能：确定确定所使用的静态文件仓库。根据依赖项确定对应的url路径。
+
+django-echarts的改造如下：
+
+- 新增支持混合仓库。比如同一图表的 *echarts.min.js* 使用公共CDN文件，地图文件 *china.js* 使用本地文件（自行创建或远程下载）。
+- 所有配置项均定义在 `settings.DJANGO_ECHARTS` 字典。
+
+依赖项功能基本逻辑。
+
+```
+dep_name(依赖项名称)   --> filename(文件路径)  --> 引用URL/本地文件路径
+```
+
+
+
+## 自定义地图（geojson&svg）
+
+pyecharts按内嵌方式引入geojson/svg数据，而 django-echarts 只支持 外链方式，这是二者最大的区别。详情参见 [《自定义地图》](/guides/custom_maps) 。
+
+## 相关API接口
+
+### 引用的类和接口
+
+下列类和接口在 django-echarts 被直接引用。
+
+- `pyecharts.charts.base.Base`
+- `pyecharts.datasets.FILENAMES`
+- `pyecharts.dataset.EXTRA`
+- `pyecharts._version.__version__`
+- `pyecharts.components.table.Table`
+- `from pyecharts.globals.ThemeType`
+
+### 无效的类和接口
+
+以下类和方法函数在django-echarts不起作用。
+
+- `pyecharts.globals.CurrentConfig`
+- `pyecharts.charts.basic_charts.Map.add_geo_json`
+- `pyecharts.charts.basic_charts.Geo.add_geo_json`
diff --git a/docs/posts/use_custom_geojson.md b/docs/posts/use_custom_geojson.md
index 7efae92..c615124 100644
--- a/docs/posts/use_custom_geojson.md
+++ b/docs/posts/use_custom_geojson.md
@@ -1,5 +1,7 @@
 # Geojson地图
 
+> 该教程适用于 0.5 版本，0.6+版本使用 `CustomMap`自定义地图功能（[链接](/guides/custom_maps)），本模块将在 1.0 移除 。
+
 ## 1 使用方法
 
 django-echarts 支持直接引用 geojson 地图，地图来源可以线上资源提供或者自己制作。相关方法均定义在 `django_echarts.geojson` 模块。
@@ -27,7 +29,6 @@ use_geojson(map1, '福建省', 'https://geo.datav.aliyun.com/areas_v3/bound/3500
 ```python
 use_geojson(map1, '福建市县', '/geojson/fujian.geojson')  # 不推荐，使用下面两种的url反向解析
 use_geojson(map1, '福建市县', geojson_url('fujian.geojson'))
-use_geojson(map1, '福建市县', reverse_lazy('dje_geojson', args=('fujian.geojson',)))
 ```
 
 
@@ -40,13 +41,13 @@ use_geojson(map1, '福建市县', reverse_lazy('dje_geojson', args=('fujian.geoj
 
 第一步，打开阿里云数据可视化平台提供的工具 [http://datav.aliyun.com/portal/school/atlas/area_selector](http://datav.aliyun.com/portal/school/atlas/area_selector) ，依次下载福建省9个地市的geojson文件（须勾选 “包括子区域”）。
 
-第二步， 打开 [geojson.io](http://geojson.io/) 网站，导入各地市geojson文件，直接再导出合并后的geojson，文件名为 *map.geojson*。
+第二步， 打开 [geojson.io](http://geojson.io/) 网站，导入各地市geojson文件，直接再导出合并后的geojson，文件名为 *fujian.geojson*。
 
 ![Export](../images/geojson-io-export.png)
 
 ### 编写图表代码
 
-第一步，将 *map.geojson* 改名，名称只能包含 数字、字母、下划线、横杠等字符。
+第一步，将 *fujian.geojson* 改名，名称只能包含 数字、字母、下划线、横杠等字符。
 
 并放置在 static 目录下，具体路径为：
 
@@ -107,23 +108,3 @@ $.getJSON("/geojson/fujian.geojson").done(function(mapdata){
 
 ![fujian-geojson](../images/fujian-custom-geojson.png)
 
-## 3 关于geojson_url
-
-**1)** 在上述例子中，fujian.geojson 放在静态文件目录，但是不能在 `use_geojson` 的url参数使用 `/static/geojson/fujian.geojson`，这是因为该url由 `django.contrib.staticfiles` 处理，返回的是 `FileResponse`。
-
-**2)** `geojson_url` 用于反向解析url。返回一个url地址，由 `GeojsonDataView` 视图处理，解析该文件并返回 `JSONResponse`。
-
-**3)**  我们也不建议在 `use_geojson`的`url` 参数直接使用 `/geojson/fujian.geojson`，虽然在上述例子中实际url就是如此。这是因为 `GeojsonDataView` 视图是挂载到 `DJESite.urls` 之中，实际地址可能随其变化而变化 。
-
-比如如果项目主路由配置如下：
-
-```python
-from django.urls import path, include
-from ccs.site_views import site_obj
-
-urlpatterns = [
-    path('site/', include(site_obj.urls)),
-]
-```
-
-则文件 fujian.geojson的引用url是 `/site/geojson/fujian.geojson`。
\ No newline at end of file
diff --git a/docs/reference/configure.md b/docs/reference/configure.md
index 2396f5d..90b043f 100644
--- a/docs/reference/configure.md
+++ b/docs/reference/configure.md
@@ -23,7 +23,7 @@ DJEOpts.dms_repo: str = 'pyecharts'
 DJEOpts.dep2url: Dict[str, str]
 ```
 
-关于依赖项的配置参见 “依赖项和静态文件” 一章。
+关于依赖项的配置参见 [依赖项和静态文件](/guides/dms) 一章。
 
 ### 百度地图密钥
 
diff --git a/docs/reference/tags.md b/docs/reference/tags.md
index d54a244..a6d160a 100644
--- a/docs/reference/tags.md
+++ b/docs/reference/tags.md
@@ -125,8 +125,16 @@ django_echarts.templatetags.dje.theme_js()
 ### page_link
 
 ```python
-django_echarts.templatetags.dje.page_link(context, page_number)
+django_echarts.templatetags.echarts.page_link(context, page_number)
 ```
 
 将当前url增加page参数。
 
+***url_single_chart***
+
+```python
+django_echarts.templatetags.echarts.url_single_chart(uri_or_name: Union[EntityURI, str], params_dic: dict = None)
+```
+
+
+
diff --git a/docs/reference/views_and_templates.md b/docs/reference/views_and_templates.md
index 4935360..8b81b4e 100644
--- a/docs/reference/views_and_templates.md
+++ b/docs/reference/views_and_templates.md
@@ -65,8 +65,14 @@ class DJESite(site_title: str, opts: Optional[SiteOpts] = None)
 
 使用自己视图类替换现有内置视图。
 
+### config_nav
+
+配置导航栏。
+
 ### add_left_link
 
+> Removed in 0.6.0
+
 添加到左侧导航栏。
 
 ```python
@@ -79,6 +85,8 @@ site_obj.add_left_link(item=LinkItem(...), menu_title='Menu1')
 
 ### add_footer_link
 
+> Remove in 0.6.0
+
 添加底部链接。
 
 ```python
diff --git a/docs/release-note/v060.md b/docs/release-note/v060.md
new file mode 100644
index 0000000..2c77d2d
--- /dev/null
+++ b/docs/release-note/v060.md
@@ -0,0 +1,145 @@
+# v0.6.0发布日志
+
+> 发布时间:2023年3月17日
+
+## 1 新增pyecharts2.0适配
+
+**django-echarts 0.6.0支持pycharts 2.0。** pyecharts2.0的主要变化有两点：
+
+- 第一，仓库地址变化和地图绘制变化，主要包括echarts及其扩展库、echarts主题文件、地图文件。参见 [pyecharts-issue#2107](https://github.com/pyecharts/pyecharts/issues/2107) 。
+- 第二，纯pyecharts代码变化，如新的图表类型、图表配置等。具体可参见 pyecharts 项目。此项工作由用户自行修改。
+
+ django-echarts 对远程仓库的处理如下：
+
+| django-echarts | pyecharts版本 | echarts | 引用仓库     | `dms_repo` 设置                     |
+| -------------- | ------------- | ------- | ------------ | ----------------------------------- |
+| 0.5.x          | 1.9           | 4.8.0   | pyecharts    | `pyecharts` 或 缺省                 |
+| 0.6.x          | 1.9           | 4.8.0   | pyecharts    | `pyecharts` 或 缺省                 |
+|                | 2.0           | 5.4.1   | pyecharts-v5 | `pyecharts` 、`pyecharts-v5`或 缺省 |
+
+说明：
+
+- django-echarts 和 pyecharts 并无版本上的关联，只需要保证 pyecharts 和 echarts版本(仓库标识) 一致即可。
+- 为了简化配置，当 django-echarts0.6/pyecharts2.0，如果 `dms_repo='pyecharts' ` （默认配置），将自动切换为 `pyecharts-v5`。
+
+## 2 仓库配置优化
+
+单个依赖项设置支持批量配置。`dep2url` 字典key表示为仓库名称（需以`#`开头）。
+
+```python
+DJANGO_ECHARTS = {
+    'dep2url':{
+        '#local': ['echarts', '中国']
+    }
+}
+```
+
+等效于
+
+```python
+DJANGO_ECHARTS = {
+    'dep2url':{
+        'echarts':'#local',
+        '中国':'#local'
+    }
+}
+```
+
+## 3 图表构建函数支持自定义参数
+
+图表构建函数支持自定义参数，可以根据不同的输入值，加载不同的数据，从而形成多个图表。
+
+以之前的时间轴图表为例子，每次只显示一个年份的数据图表。
+
+```python
+@site_obj.register_chart(title='{year}年福建省家庭户类型组成')
+def yearly_family(year: int):
+    family_types = [
+        '一人户', '二人户', '三人户', '四人户', '五人户', '六人户', '七人户', '八人户', '九人户', '十人及其以上'
+    ]
+    data = [
+        [1982, 7.7, 8.2, 12.2, 17.1, 18.4, 14.7, 10.1, 11.6, 0, 0],
+        [1990, 5.8, 8.6, 16.8, 23.6, 21.4, 11.8, 5.9, 2.9, 1.4, 1.8],
+        [2000, 9.1, 15.5, 25.4, 24.7, 15.8, 5.9, 2.2, 0.8, 0.3, 0.3],
+        [2010, 12.1, 17.2, 24.3, 21.7, 13.7, 6.4, 2.6, 1.1, 0.5, 0.4],
+        [2020, 27.3, 26.3, 19.4, 14.2, 6.9, 4, 1.1, 0.4, 0.2, 0.2]
+    ]
+    yearly_data = {item[0]: item[1:] for item in data}
+    if year not in yearly_data:
+        raise ChartDoesNotExist(f'year={year}')
+    year_data = yearly_data[year]
+    bar = (
+        Bar()
+            .add_xaxis(family_types).add_yaxis('百分比(%)', year_data)
+            .set_global_opts(title_opts=opts.TitleOpts("福建省家庭户类型构成-{}年".format(year)))
+    )
+    return bar
+```
+
+说明：
+
+- year为自定义参数，如未声明类型，默认为 `str`。
+- 如果传入的year找不到对应的数据，抛出 `ChartDoesNotExist` 异常，web页面显示“找不到图表”的页面。
+- title参数可以使用类似 `{year}`的模板字符串。
+
+该图表对应的相关引用配置如下(以year=2020例子)：
+
+| 项目       | 值                                                           | 备注                                         |
+| ---------- | ------------------------------------------------------------ | -------------------------------------------- |
+| 图表URL    | `/chart/year/2020/`                                          | 格式：`<图表slug>/<参数名称1>/<参数值1>/...` |
+| python引用 | `reverse_lazy('dje_single_chart','yearly_family','year/2020/')` |                                              |
+| 模板引用   | `{% url 'dje_single_chart' 'yearly_family', 'year/2020/' %}` |                                              |
+
+## 4 重构自定义地图，支持geojson&svg
+
+v0.6开始，新增 `django_echarts.custom_maps` 模块，原有的 `django_echarts.geojson` 标记为废弃状态。
+
+将原有的 `use_geojson` 替换为 `use_custom_map` 即可。
+
+```python
+chart = Map(...)
+use_custom_map(chart, '中国省市地图', 'china_cities.geojson')
+```
+
+
+
+## 5 新增声明式导航栏配置
+
+django-echarts 0.6 新增了使用字典方式一次性配置导航栏配置。
+
+```python
+# site_config.py
+
+nav_config = {
+    'nav_left': ['home', 'list'],
+    'nav_right': ['settings'],
+    'nav_footer': [
+        {'text': '项目主页', 'slug': 'project_url', 'url': 'https://github.com/kinegratii/django-echarts'}
+    ]
+}
+
+# urls.py
+
+site_obj.config_nav(nav_config)
+```
+
+相应的，DJESite类移除导航栏相关方法，可使用 `DJESite.nav` 相关方法。
+
+- `DJESite.add_left_link`
+- `DJESite.add_right_link`
+- `DJESite.add_footer_link`
+- `DJESite.add_menu_item`
+
+## 6 ChartInfo和ChartInfoManager
+
+- 类 `ChartInfo` 增加 `is_bound` 布尔值属性，如果对应图表注册函数携带任何参数，则设置为 `False`，数据格式化后变为`True`。
+- `ChartInfoManagerMixin.get_or_none` 增加 `uri` 参数。 
+- 在 `is_bound = False` 情况下，body title description的转义处理
+
+## 7 其他依赖库更新
+
+```
+typing_extensions 4.0 -> 4.5
+borax 3.5 -> 4.0
+```
+
diff --git a/docs/starter/base.md b/docs/starter/base.md
index cfce293..9711e7f 100644
--- a/docs/starter/base.md
+++ b/docs/starter/base.md
@@ -1,16 +1,6 @@
 # 使用教程
 
-## 创建项目
-
-### Django&pyecharts版本
-
-django-echarts 不会将 django 和 pyecharts 作为显式依赖库（虽然在代码中总是导入这两个库），对其版本没有强制性的依赖。下列是推荐性的版本。
-
-| django-echarts版本系列 | pyecharts | django | python | 备注       |
-| ---------------------- | --------- | ------ | ------ | ---------- |
-| 0.5.x                  | 1.9+      | 2.0+   | 3.7+   | 开发维护中 |
-
-### 项目配置
+## 一、 项目配置
 
 在 Django 项目中，配置模块位于 *project.settings* 。和 django-echarts 有关的配置如下：
 
@@ -54,13 +44,13 @@ DJANGO_ECHARTS = {
 1. 内置主题APP包括： django_echarts.contrib.bootstrap3 / django_echarts.contrib.bootstrap5 / django_echarts.contrib.material。
 2. 默认配置已经能够支持最小化运行，此配置可以留空。
 
-## 创建站点
+## 二、创建站点
 
-### 初始化DJESite对象
+### 2.1 初始化DJESite对象
 
 > DJESite(site_title: str,  opts: Optional[SiteOpts] = None):
 
-`DJESite` 是所有界面和逻辑的入口点，首先必须创建你自己的站点对象 `site_obj`。
+`DJESite` 是站点界面和逻辑的入口点，首先必须创建你自己的站点对象 `site_obj`。
 
 ```python
 from django_echarts.entities import Jumbotron, Copyright, LinkItem
@@ -85,12 +75,12 @@ site_obj.add_widgets(
 | 参数                 | 类型或可选值 | 描述                                                         |
 | -------------------- | ------------ | ------------------------------------------------------------ |
 | site_title           | str          | 网站标题                                                     |
-| opts                 | SiteOpts     | 选项类                                                       |
+| opts                 | SiteOpts     | 站点配置类                                                   |
 | opts.nav_shown_pages | List         | 导航栏显示的菜单项，可选为 home / list /collection / settings。默认为 ['home'] <sup>1</sup> |
 
 1. 此项设置必须在 `DJESite` 初始化时传入，以便将这些菜单项先插入导航栏靠前的位置
 
-### 连接到路由模块
+### 2.2 连接到项目路由模块
 
 > DJESite.urls: list[Union[URLResolver, URLPattern]]
 
@@ -108,9 +98,11 @@ urlpatterns = [
 ]
 ```
 
-## 注册echarts图表
+## 三、编写函数和注册echarts图表
 
-### 新增图表
+### 3.1 新增图表
+
+图表使用遵循“定义 - 注册 - 使用” 的规则。
 
 `site_obj.register_chart` 装饰器用于注册图表的创建函数。默认未携带任何参数情况下，函数名将作为图表标识符。
 
@@ -122,7 +114,7 @@ def mychart():
     return bar
 ```
 
-当然也可以携带一些参数，这些参数用于构建对应的 `ChartInfo` 对象。
+当然该装饰器也可以传入参数，这些参数用于构建对应的 `ChartInfo` 对象。
 
 ```python
 @site_obj.register_chart(description='词云示例', catalog='示例一')
@@ -145,9 +137,9 @@ def mychart():
     return bar
 ```
 
-### 其他配置
+### 3.2 装饰器参数
 
-`DJESite.register_chart` 接受下列可选参数：
+装饰器 `DJESite.register_chart` 接受下列可选参数：
 
 | 参数名称            | 类型           | 说明                                           |
 | ------------------- | -------------- | ---------------------------------------------- |
@@ -159,30 +151,17 @@ def mychart():
 | catalog             | str            | 分类，如果设置 |
 | top                 | int            | 置顶标志，0表示不置顶，数值越小，越靠前。      |
 | tags                | List[str]      | 标签列表，列表搜索功能时，标签也是搜索范围。   |
-| **菜单参数** |                |                                                |
+| **参数图表配置** |  |  |
+| params_config | ParamConfig | 图表参数配置 <sup>2</sup> |
+| **菜单参数** <sup>3</sup> |                |                                                |
 | nav_parent_name     | str            | 上级菜单名称，默认为 catalog                   |
 | nav_after_separator | bool           | 是否在菜单项前使用分隔符                       |
 
 1. `Slug`类型指的是Django内置Converter，为符合正则表达式 `[-a-zA-Z0-9_]+` 的字符串，作为url的一部分。
+1.  0.6新增。参见 [《参数化图表》](/guides/entity_params)
+1.  在导航栏生成图表链接。从0.6.0开始，此项默认不启用，如需开启，需搭配 `DJESite.config_nav` 使用。
 
-### 关联导航栏
-
-下面是演示如何添加到导航菜单栏的。以 `chart.title = 'Chart1'` 为例子：
-
-| chart.catalog | nav_parent_name | 说明                              |
-| ------------- | --------------- | --------------------------------- |
-| None(未设置)  | None(未设置)    | 不显示在菜单栏上                  |
-| 'BarCharts'   | None(未设置)    | 显示，“BarCharts- Chart1”         |
-| None(未设置)  | 'Menu'          | 显示，“Menu - Chart1”             |
-| 'BarCharts'   | 'Menu'          | 显示 "Menu - Chart1" <sup>1</sup> |
-| 任意          | 'self'          | 显示一级菜单<sup>2</sup>          |
-| 任意          | 'none'          | 不显示<sup>3</sup>                |
-
-1. 只要 catalog 和 nav_parent_name 至少一个有设置，均显示为二级菜单
-2. 使用特殊标识 'self' 表示显示为一级菜单
-3. 使用特殊标识 'none' 表示不显示
-
-### 注册组件
+### 3.3 注册组件
 
 > DJESite.register_html_widget(function=None, *, name: str = None)
 
@@ -207,72 +186,15 @@ def this_month_panel():
     return number_p
 ```
 
-## 网站公共组件
-
-### 导航栏(Nav)
-
-导航栏位于页面顶部，由 *左侧二级菜单* 、 *右侧一级菜单* 和 *底部链接* 三部分组成，根据目标链接的类型有不同的添加方式。
-
-**第一种：内置页面**
-
-内置页面通常位于左侧二级菜单栏的最前面，使用 `SiteOpts.nav_shown_pages` 定义。
-
-```python
-SiteOpts(nav_shown_pages=['home', 'collection'])
-```
-
-包括：
-
-| 标识       | 页面功能                                                     |
-| ---------- | ------------------------------------------------------------ |
-| home       | 首页。默认已添加。                                           |
-| list       | 包含所有图表的信息页。不显示图表，显示描述、标签等。默认已添加。 |
-| collection | 包含所有图表的合辑，显示图表和简短的文字描述信息。           |
-
-**第二种：图表和合辑**
-
-将图表或者合辑添加到左侧二级菜单，由 `DJESite.register_chart` / `DJESite.register_collection` 中参数控制 。详细参见 注册图表 一节。
-
-**第三种：自定义链接**
-
-> class LinkItem(text: str, url: str = None, slug: str = None, new_page: bool = False, after_separator: bool = False)
-
-链接使用 `LinkItem` 类表示。
+## 四、网站配置
 
-| 参数            | 类型 | 描述                                       |
-| --------------- | ---- | ------------------------------------------ |
-| text            | str  | 链接文字                                   |
-| url             | str  | 链接地址                                   |
-| slug            | str  | 标识符，可自动生成                         |
-| new_page        | bool | target属性是否设置为 _blank                |
-| after_separator | bool | 在菜单前是否添加分隔符，仅适用于顶部导航栏 |
+### 4.1 导航栏(Nav)
 
-**注意**：使用django视图反向解析时必须使用 `reverse_lazy`，而不是 `reverse`，否则出现无法解析的异常。因为此时 site_obj 还未挂载到项目全局的 url 路由规则之中。
+参见 [导航栏](starter/site_configuration)一章。
 
-`DJESite`提供了 `add_*_link` 函数用于向站点添加链接。
-
-| 函数                                                     | 说明                         |      |
-| -------------------------------------------------------- | ---------------------------- | ---- |
-| `DJESite.add_left_link(item: LinkItem)`                  | 在左侧添加一级菜单           |      |
-| `DJESite.add_left_link(item: LinkItem, menu_title: str)` | 在menu_title之下添加二级菜单 |      |
-| `DJESite.add_right_link(item: LinkItem)`                 | 在右侧添加链接               |      |
-| `DJESite.add_footer_link(item: LinkItem)`                 | 在底部添加链接               |      |
-
-例子：
-
-```python
-from django.urls import reverse_lazy
-from django_echarts.entities.html_widgets import LinkItem
-
-# 在右侧添加项目仓库链接，以新标签页方式打开
-item = LinkItem(text='Github仓库', url='https://github.com/kinegratii/django-echarts', new_page=True)
-site_obj.add_right_link(item)
-
-item2 = LinkItem(text='关于', url=reverse_lazy('about'))
-site_obj.add_right_link(item2)
-```
+## 五、页面配置
 
-## 首页
+### 5.1 首页
 
 在首页可以通过 `add_widgets` 函数定制部分组件。
 
@@ -292,7 +214,7 @@ DJESite.add_widgets(*, jumbotron: Jumbotron = None, copyright_: Copyright = None
 1. 静态方式：home_jumbotron_chart 直接指定一个图表对象（如Bar）。
 2. 动态方式：home_jumbotron_chart 仅指定一个图表字符串标识，这些图表或组件通过 `register_chart` / `register_widget` 的方式注册。
 
-### 大标题图表(JumbotronChart)
+#### 5.1.1 大标题图表(JumbotronChart)
 
 可以为 add_widgets 的 jumbotron_chart 指定一个图表名称，则显示该图表，不再显示 Jumbotron 组件。下面是一个例子：
 
@@ -306,7 +228,7 @@ def mychart():
     return bar
 ```
 
-### 热门图表
+#### 5.1.2 热门图表
 
 按照网格方式显示标记为 top 的图表。向装饰器 `register_chart` 的 top 参数指定一个大于0的数字即可（数字越小，越靠前显示）。
 
@@ -318,13 +240,13 @@ def mychart():
     return bar
 ```
 
-## 列表页
+### 5.2 列表页
 
-### 搜索
+#### 5.2.1 搜索
 
 列表页支持关键字搜索，搜索范围有：图表标题、标签、描述文字。
 
-### 显示样式
+#### 5.2.2 显示样式
 
 > 定义: SiteOpts.list_layout: Literal['grid', 'list'] = 'list'
 
@@ -343,7 +265,7 @@ site_obj = DJESite(
 )
 ```
 
-### 设置分页
+#### 5.2.3 设置分页
 
 > 定义: SiteOpts.paginate_by:Optional[int] = None
 
diff --git a/docs/starter/localize_staticfiles.md b/docs/starter/localize_staticfiles.md
index b8e4ab6..2eb40ce 100644
--- a/docs/starter/localize_staticfiles.md
+++ b/docs/starter/localize_staticfiles.md
@@ -1,4 +1,4 @@
-# 本地化静态资源
+# 依赖项(二)：本地化
 
 ## 概述
 
diff --git a/docs/starter/site_configuration.md b/docs/starter/site_configuration.md
new file mode 100644
index 0000000..7fdc5af
--- /dev/null
+++ b/docs/starter/site_configuration.md
@@ -0,0 +1,170 @@
+# 网站配置
+
+## 导航栏(Nav)
+
+> Update in 0.6.0: 新增  `DJESite.config_nav` 函数。
+
+网站导航栏包括：左侧导航栏、右侧导航栏、底部导航栏，其中左侧、右侧（均位于顶部）支持下拉菜单，底部导航栏仅支持多个链接形式。
+
+有两种使用方法。
+
+## SiteOptions.nav_shown_pages
+
+```python
+class SiteOpts:
+    """The opts for DJESite."""
+    # ...
+    nav_shown_pages: List = field(default_factory=lambda: ['home'])
+```
+
+该属性在 `DJESite` 对象初始化的时候传入，`nav_shown_pages` 为包含表示特定页面标识（字符串）的列表，可选的有：home / list / collection / settings / chart_nav 。
+
+其中chart_nav需调用 `DJESite.config_nav`。
+
+## 函数config_nav
+
+```python
+DJESite.config_nav(self, menu_config: dict = None)
+```
+
+函数`config_nav` 用于配置导航栏，需注意两点：
+
+- 该函数是一次性配置的，将覆盖 `SiteOptions.nav_shown_pages`的配置。
+- 该函数必须在所有 `register_chart` 之后调用，一般在 `urls`模块调用。
+
+`menu_config` 参数基本形式如下：
+
+```python
+class NavConfig(TypedDict):
+    nav_left: NotRequired[List[Union[str, dict]]]
+    nav_right: NotRequired[List[Union[str, dict]]]
+    nav_footer: NotRequired[List[Union[str, dict]]]
+```
+
+一个菜单项可以是以下几种形式：
+
+- 内置页面(str)：home / list / collection / settings
+- 图表分类导航(str)：chart_nav
+- 图表或合辑资源标识符(str)：使用其URI字符串形式
+- django视图链接(str)：使用视图名称及其参数
+- 自定义链接(dict)：适用于菜单或菜单项
+
+### 1 自定义链接
+
+这是最为基础的定义形式，完整的链接定义如下：
+
+```python
+{'text': '网站源码', 'slug':'site_source', 'url': 'https://github.com/kinegratii/zinc', 'new_page': True}
+```
+
+| 属性     | 描述                               |
+| -------- | ---------------------------------- |
+| text     | 必选，链接文字。                   |
+| url      | 必选，链接。                       |
+| slug     | 可选，标识符，默认生成uuid字符串。 |
+| new_page | 可选，默认为False。                |
+
+### 2 菜单定义
+
+```python
+{'text': '网站源码', 'slug':'site_source', 'children':[...]}
+```
+
+使用键 `children` 表明这是一个菜单列表，下级菜单定义在属性children之中。
+
+### 3 内置页面
+
+可支持的内置页面如下：
+
+| 标识符     | 页面       |
+| ---------- | ---------- |
+| home       | 首页       |
+| list       | 图表列表页 |
+| collection | 合辑列表页 |
+| settings   | 设置页面   |
+
+### 3 Django视图
+
+```python
+{'text':'大屏面板', 'view_name': 'dashbord', 'args':[], 'kwargs':{}，'new_page':False}
+```
+
+使用 `reverse_lazy` 反向解析url。
+
+### 4 图表资源
+
+```python
+{'text': '2020年数据', 'slug':'family_type_2020', 'url': 'chart:yearly_family_types/year/2020', 'new_page': False}
+```
+
+类似于
+
+```python
+{'text':'2020年数据', 'view_name': 'dje_chart_single', 'args':['yearly_family_types','/year/2020'], 'kwargs':{}，'new_page':False}
+```
+
+
+
+### 5 图表分类导航chart_nav
+
+```python
+'chart_nav'
+```
+
+该字符串仅为占位符，函数 `DJESite.config_nav` 将根据 `register_chart` 的 catalog，在该位置添加菜单栏。
+
+该配置仅可用于左侧导航栏。
+
+## 配置示例
+
+### 默认配置
+
+默认配置（不调用 `config_nav`函数），仅包含 `nav_shown_pages`所定义菜单。
+
+### 0.5兼容配置
+
+以下是 django-echart 0.6 兼容0.5版本的配置方法：
+
+```python
+site_obj = DJESite(title='示例站点', opts=SiteOpts(nav_shown_pages=['home', 'list', 'chart_nav']))
+
+
+@site_obj.register(..., catalog='菜单1')
+def chart_1():
+    pass
+
+
+site_obj.config_nav()
+```
+
+### 常规配置
+
+```python
+nav = {
+    'nav_left': ['home', 'list', 'chart_nav', {'text': '2020年数据', 'url': 'chart:yearly_family/year/2020'}],
+    'nav_right': ['settings'],
+    'nav_footer': [
+        {'text': '福建统计年鉴2021', 'url': 'https://tjj.fujian.gov.cn/tongjinianjian/dz2021/index.htm', 'new_page': True},
+        {'text': '网站源码', 'url': 'https://github.com/kinegratii/zinc', 'new_page': True},
+    ]
+}
+```
+
+
+
+### 关联导航栏
+
+下面是演示如何添加到导航菜单栏的。以 `chart.title = 'Chart1'` 为例子：
+
+| chart.catalog | nav_parent_name | 说明                              |
+| ------------- | --------------- | --------------------------------- |
+| None(未设置)  | None(未设置)    | 不显示在菜单栏上                  |
+| 'BarCharts'   | None(未设置)    | 显示，“BarCharts- Chart1”         |
+| None(未设置)  | 'Menu'          | 显示，“Menu - Chart1”             |
+| 'BarCharts'   | 'Menu'          | 显示 "Menu - Chart1" <sup>1</sup> |
+| 任意          | 'self'          | 显示一级菜单<sup>2</sup>          |
+| 任意          | 'none'          | 不显示<sup>3</sup>                |
+
+1. 只要 catalog 和 nav_parent_name 至少一个有设置，均显示为二级菜单
+2. 使用特殊标识 'self' 表示显示为一级菜单
+3. 使用特殊标识 'none' 表示不显示
diff --git a/mkdocs.yaml b/mkdocs.yaml
index 33a65a6..579b2c1 100644
--- a/mkdocs.yaml
+++ b/mkdocs.yaml
@@ -15,18 +15,22 @@ extra:
       link: https://github.com/kinegratii/django-echarts
     - icon: fontawesome/brands/python
       link: https://pypi.org/project/django-echarts
-copyright: Copyright &copy; 2017 - 2022 Samuel.Yan
+copyright: Copyright &copy; 2017 - 2023 Samuel.Yan
 markdown_extensions:
   - pymdownx.highlight
   - pymdownx.superfences
+  - mdx_truly_sane_lists
 nav:
   - 快速开始: index.md
   - 可视化构建:
     - 使用教程: starter/base.md
+    - 网站配置: starter/site_configuration.md
     - 组件和布局: starter/widget_and_layout.md
     - UI框架和主题: starter/themes.md
-    - 依赖项和静态文件: guides/dms.md
-    - 本地化静态资源: starter/localize_staticfiles.md
+    - 依赖项(一)：基本配置: guides/dms.md
+    - 依赖项(二)：本地化: starter/localize_staticfiles.md
+    - 参数化图表: guides/entity_params.md
+    - 自定义地图: guides/custom_maps.md
     - 高级开发: guides/advance_development.md
     - Gallery: gallery.md
   - 参考:
@@ -41,10 +45,12 @@ nav:
     - geojson地图: posts/use_custom_geojson.md
     - 图表合辑: posts/widget_collection.md
     - 第三方HTML渲染库: posts/third_html_library.md
+    - 与pyecharts: posts/pyecharts_project.md
   - 项目开发:
     - 更新日志: changelog.md
     - 破坏性变更: breaking-change.md
     - 关于项目: about.md
     - 发布日志:
-      - v0.5.0: release-note/v050.md
-      - v0.5.1: release-note/v051.md
\ No newline at end of file
+      - v0.6.0: release-note/v060.md
+      - v0.5.1: release-note/v051.md
+      - v0.5.0: release-note/v050.md
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index abc9e5f..bc02e39 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,3 @@
-typing_extensions
-borax>=3.5.3
+typing_extensions~=4.5
+borax~=4.0
 htmlgenerator~=1.2
\ No newline at end of file
diff --git a/requirements_dev.txt b/requirements_dev.txt
index 220c3db..ed9f7b2 100644
--- a/requirements_dev.txt
+++ b/requirements_dev.txt
@@ -1,6 +1,6 @@
-typing_extensions
-pyecharts~=1.9
-borax>=3.5.3
+typing_extensions~=4.5
+pyecharts~=2.0
+borax~=4.0
 django~=3.2
 htmlgenerator~=1.2
 flake8~=3.9
diff --git a/requirements_doc.txt b/requirements_doc.txt
index 755dee8..6e8fb60 100644
--- a/requirements_doc.txt
+++ b/requirements_doc.txt
@@ -1,1 +1,2 @@
-mkdocs-material==8.1.7
\ No newline at end of file
+mkdocs-material==8.3.3
+mdx-truly-sane-lists==1.3
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
index 6173e1f..06e329c 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,4 +1,4 @@
 [flake8]
-ignore = E743,E501,F401
+ignore = E743,E501,F401,E126
 max-complexity = 18
 exclude = .git,__pycache__,docs/source/conf.py,old,build,dist
\ No newline at end of file


Test patch:
diff --git a/.github/workflows/unittest.yaml b/.github/workflows/unittest.yaml
index bd41af3..25243d6 100644
--- a/.github/workflows/unittest.yaml
+++ b/.github/workflows/unittest.yaml
@@ -13,8 +13,9 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: [ '3.7', '3.8', '3.9' ]
+        python-version: [ '3.8', '3.9' ]
         django-version: [ '2.2', '3.2' ]
+        pyecharts-version: ['1.9.1', '2.0.2']
     steps:
       - uses: actions/checkout@v2
       - name: Set up Python ${{ matrix.python-version }}
@@ -25,9 +26,9 @@ jobs:
         run: |
           python -m pip install --upgrade pip
           pip install django~=${{ matrix.django-version }}
-          pip install borax>=3.5
-          pip install pyecharts~=1.9
-          pip install typing_extensions~=4.0
+          pip install borax~=4.0
+          pip install pyecharts~=${{ matrix.pyecharts-version }}
+          pip install typing_extensions~=4.5
           pip install htmlgenerator~=1.2
           pip install flake8~=3.9
           pip install nose2~=0.10
@@ -43,5 +44,5 @@ jobs:
           file: ./coverage.xml
           env_vars: OS,PYTHON
           name: codecov-umbrella
-          fail_ci_if_error: true
+          fail_ci_if_error: false
           verbose: true
\ No newline at end of file
diff --git a/tests/test_dms.py b/tests/test_dms.py
new file mode 100644
index 0000000..094d32e
--- /dev/null
+++ b/tests/test_dms.py
@@ -0,0 +1,30 @@
+import unittest
+from collections import namedtuple
+from unittest.mock import patch
+
+from django_echarts.core.settings_store import SettingsStore, DJEOpts
+
+MockAppConfig = namedtuple('MockAppConfig', 'name')
+
+
+class DMSFromSettingsTestCase(unittest.TestCase):
+    @patch('django.apps.apps.get_app_configs')
+    def test_custom_url_settings(self, get_func):
+        get_func.return_value = [MockAppConfig('django_echarts.contrib.bootstrap5')]
+        target_store = SettingsStore(
+            echarts_settings=DJEOpts(dms_repo='pyecharts', theme_app='django_echarts.contrib.bootstrap5', dep2url={
+                'foo': 'https://foo.icc/foo.js',
+                'zzz': '#local',
+                'china': '#local',
+                'err': '#err',
+                '#local': ['c1', 'c2']
+            }),
+            extra_settings={
+                'STATIC_URL': '/static/'
+            }
+        )
+
+        self.assertEqual('/static/assets/zzz.js', target_store.resolve_url('zzz'))
+        self.assertEqual('/static/assets/c1.js', target_store.resolve_url('c1'))
+        self.assertEqual('/static/assets/c2.js', target_store.resolve_url('c2'))
+        self.assertEqual('/static/assets/maps/china.js', target_store.resolve_url('china'))
diff --git a/tests/test_factory.py b/tests/test_factory.py
index 8ddddbb..83df8cc 100644
--- a/tests/test_factory.py
+++ b/tests/test_factory.py
@@ -1,6 +1,7 @@
 import unittest
 
 from django_echarts.entities import (Title, ChartInfo, WidgetCollection)
+from django_echarts.entities.uri import EntityURI, ParamsConfig
 from django_echarts.stores.entity_factory import EntityFactory
 
 
@@ -37,3 +38,29 @@ def test_factory(self):
         wc.add_chart_widget('chart1')
         wc.add_html_widget(['demo1'])
         wc.auto_mount(my_factory)
+
+    def test_factory_uri_methods(self):
+        factory2 = EntityFactory()
+
+        @factory2.register_chart_widget
+        def chart1():
+            pass
+
+        @factory2.register_chart_widget(info=ChartInfo('chart2'))
+        def chart2(year: int):
+            pass
+
+        @factory2.register_chart_widget(info=ChartInfo('chart3', params_config=ParamsConfig({'year': [2022, 2023]})))
+        def chart3(year: int):
+            return MockChart(100, 100)
+
+        uri_list = list(factory2.get_all_chart_uri())
+        uri_string_list = [str(uri) for uri in uri_list]
+        self.assertEqual(3, len(uri_list))
+        self.assertIn('chart:chart3/year/2022', uri_string_list)
+        self.assertFalse(any(['chart2' in s for s in uri_string_list]))
+
+        widget = factory2.get_widget_by_uri(EntityURI.from_str('chart:chart3/year/2022'))
+        self.assertTrue(isinstance(widget, MockChart))
+        info = factory2.get_widget_by_uri(EntityURI.from_str('info:chart3/year/2022'))
+        self.assertTrue(isinstance(info, ChartInfo))
diff --git a/tests/test_hosts.py b/tests/test_hosts.py
index 8ff7bb6..9456b74 100644
--- a/tests/test_hosts.py
+++ b/tests/test_hosts.py
@@ -11,7 +11,7 @@ def test_lib_host(self):
         # Basic tests
         m_context = {
             'STATIC_URL': '/static/',
-            'echarts_version': '3.7.0',
+            'echarts_version': '4.8.0',
             'baidu_map_ak': 'foo23zoo'
         }
         manager = DependencyManager.create_default(
@@ -21,7 +21,7 @@ def test_lib_host(self):
         manager.add_repo('bootcdn', repo_url='https://cdn.bootcss.com/echarts/{echarts_version}/')
 
         self.assertEqual(
-            'https://cdn.bootcss.com/echarts/3.7.0/echarts.min.js',
+            'https://cdn.bootcss.com/echarts/4.8.0/echarts.min.js',
             manager.resolve_url('echarts.min')
         )
         self.assertEqual('https://api.map.baidu.com/api?v=2.0&ak=foo23zoo',
@@ -30,7 +30,7 @@ def test_lib_host(self):
     def test_map_host(self):
         m_context = {
             'STATIC_URL': '/static/',
-            'echarts_version': '3.7.0'
+            'echarts_version': '4.8.0'
         }
         manager = DependencyManager.create_default(
             context=m_context,
@@ -50,7 +50,7 @@ def test_map_host(self):
     def test_custom_d2u(self):
         m_context = {
             'STATIC_URL': '/static/',
-            'echarts_version': '3.7.0',
+            'echarts_version': '4.8.0',
             'baidu_map_ak': 'foo'
         }
         manager = DependencyManager.create_default(
@@ -66,7 +66,7 @@ def test_custom_d2u(self):
 class CustomHostTestCase(unittest.TestCase):
     def test_add_host(self):
         m_context = {
-            'echarts_version': '3.8.5'
+            'echarts_version': '4.8.0'
         }
 
         manager = DependencyManager.create_default(
@@ -80,6 +80,6 @@ def test_add_host(self):
             manager.resolve_url('fujian', repo_name='demo')
         )
         self.assertEqual(
-            '/demo2/3.8.5/fujian.js',
+            '/demo2/4.8.0/fujian.js',
             manager.resolve_url('fujian', repo_name='demo2')
         )
diff --git a/tests/test_mcharts.py b/tests/test_mcharts.py
index 06fc8be..1c1b3a3 100644
--- a/tests/test_mcharts.py
+++ b/tests/test_mcharts.py
@@ -4,19 +4,18 @@
 """
 
 import unittest
-from unittest.mock import MagicMock
 
-from django_echarts.entities import NamedCharts
+from django_echarts.entities import NamedCharts, BlankChart
 
 
 class MChartsTestCase(unittest.TestCase):
     def test_create_with_kwargs(self):
-        nc = NamedCharts().add_chart(
-            MagicMock(page_title='Line-Chart'), name='line'
-        ).add_chart(
-            MagicMock(page_title='Bar-Chart')
-        ).add_chart(
-            MagicMock(page_title='Map-Chart'), name='map'
+        nc = NamedCharts().add_widget(
+            BlankChart(page_title='Line-Chart'), name='line'
+        ).add_widget(
+            BlankChart(page_title='Bar-Chart')
+        ).add_widget(
+            BlankChart(page_title='Map-Chart'), name='map'
         )
         self.assertEqual('Line-Chart', nc['line'].page_title)
         self.assertEqual('Bar-Chart', nc['c1'].page_title)
@@ -33,9 +32,9 @@ def test_create_with_kwargs(self):
 class MChartsPY36TestCase(unittest.TestCase):
     def test_create_with_kwargs(self):
         nc = NamedCharts()
-        nc.add_widget(MagicMock(page_title='Bar-Chart'), name='bar')
-        nc.add_widget(MagicMock(page_title='Line-Chart'), name='line')
-        nc.add_chart(MagicMock(page_title='Map-Chart'), name='map')
+        nc.add_widget(BlankChart(page_title='Bar-Chart'), name='bar')
+        nc.add_widget(BlankChart(page_title='Line-Chart'), name='line')
+        nc.add_chart(BlankChart(page_title='Map-Chart'), name='map')
         self.assertEqual('Bar-Chart', nc['bar'].page_title)
         self.assertListEqual(
             ['Bar-Chart', 'Line-Chart', 'Map-Chart'],
diff --git a/tests/test_renders.py b/tests/test_renders.py
index 23d5713..14949cb 100644
--- a/tests/test_renders.py
+++ b/tests/test_renders.py
@@ -34,8 +34,8 @@ def test_settings(self):
         self.assertEqual('bootstrap5', DJANGO_ECHARTS_SETTINGS.theme.name)
 
     def test_render_default(self):
-        with self.assertRaises(TypeError):
-            render_widget(object)
+        # with self.assertRaises(TypeError):
+        #     render_widget(object)
         info = ChartInfo('DemoInfo')
         self.assertIn('DemoInfo', render_widget(info))
         self.assertEqual('xxx', render_widget(HTMLString('xxx')))
diff --git a/tests/test_settings_store.py b/tests/test_settings_store.py
index 3c56c63..e553980 100644
--- a/tests/test_settings_store.py
+++ b/tests/test_settings_store.py
@@ -1,9 +1,10 @@
 # coding=utf8
-
+"""A general test module for dms and tms from settings entry.See detail in test_dms.py and test_tms.py"""
 import unittest
 from collections import namedtuple
-from unittest.mock import patch, MagicMock
+from unittest.mock import patch
 
+from django_echarts.core.dms import PyechartsDMS
 from django_echarts.core.settings_store import SettingsStore
 
 MockAppConfig = namedtuple('MockAppConfig', 'name')
@@ -27,14 +28,25 @@ def test_default_settings(self, get_func):
                 'STATIC_URL': '/static/'
             }
         )
-        self.assertEqual(
-            'https://assets.pyecharts.org/assets/echarts.min.js',
-            target_store.resolve_url('echarts.min')
-        )
-        self.assertEqual(
-            'https://assets.pyecharts.org/assets/maps/china.js',
-            target_store.resolve_url('china.js')
-        )
+        pye_version = PyechartsDMS.get_pyecharts__primary_version()
+        if pye_version[0] == '1':
+            self.assertEqual(
+                'https://assets.pyecharts.org/assets/echarts.min.js',
+                target_store.resolve_url('echarts.min')
+            )
+            self.assertEqual(
+                'https://assets.pyecharts.org/assets/maps/china.js',
+                target_store.resolve_url('china.js')
+            )
+        elif pye_version[0] == '2':
+            self.assertEqual(
+                'https://assets.pyecharts.org/assets/v5/echarts.min.js',
+                target_store.resolve_url('echarts.min')
+            )
+            self.assertEqual(
+                'https://assets.pyecharts.org/assets/v5/maps/china.js',
+                target_store.resolve_url('china.js')
+            )
         self.assertEqual('https://fuzzy.io', target_store.resolve_url('https://fuzzy.io'))
         self.assertEqual('https://foo.icc/foo.js', target_store.resolve_url('foo'))
         self.assertEqual('/static/assets/zzz.js', target_store.resolve_url('zzz'))
diff --git a/tests/test_uri.py b/tests/test_uri.py
new file mode 100644
index 0000000..939b021
--- /dev/null
+++ b/tests/test_uri.py
@@ -0,0 +1,35 @@
+import unittest
+from django_echarts.entities.uri import EntityURI, parse_params_choices
+
+
+class EntityURITestCase(unittest.TestCase):
+    def test_parse(self):
+        mya_uri = EntityURI.from_params_path('chart', 'demo', 'year/2021/tag/hello/django-echarts',
+                                             param_names=['year', 'tag'])
+        self.assertDictEqual({'year': '2021', 'tag': 'hello/django-echarts'}, mya_uri.params)
+        mya_uri2 = EntityURI.from_params_path('chart', 'demo', 'year/2021/tag/hello/django-echarts/',
+                                              param_names=['year', 'tag'])
+        self.assertDictEqual({'year': '2021', 'tag': 'hello/django-echarts/'}, mya_uri2.params)
+
+    def test_full_str(self):
+        my_uri = EntityURI.from_str('chart:name1')
+        self.assertEqual('chart', my_uri.catalog)
+        self.assertEqual('name1', my_uri.name)
+        self.assertEqual(0, len(my_uri.params))
+
+        uri_with_params = EntityURI.from_str('chart:name1/year/2022')
+        self.assertEqual('chart', uri_with_params.catalog)
+        self.assertEqual('name1', uri_with_params.name)
+        self.assertEqual('2022', uri_with_params.params['year'])
+
+    def test_custom_catalog(self):
+        uri_with_params = EntityURI.from_str('name1/year/2022', catalog='chart')
+        self.assertEqual('chart', uri_with_params.catalog)
+        self.assertEqual('name1', uri_with_params.name)
+        self.assertEqual('2022', uri_with_params.params['year'])
+
+
+class ParamsConfigTestCase(unittest.TestCase):
+    def test_parse_dict(self):
+        result = list(parse_params_choices({'year': [2021, 2022], 'month': [1, 2]}))
+        self.assertEqual(4, len(result))
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 99adb5b..0da2439 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,6 +1,6 @@
 import unittest
 from django_echarts.utils.burl import burl_kwargs, burl_args, BUrl
-from django_echarts.utils.lazy_dict import LazyDict
+from django_echarts.utils.lazy_dict import LazyDict, ParameterMissingError
 
 
 class BurlTestCase(unittest.TestCase):
@@ -37,3 +37,33 @@ def demo(key1):
             return key1
 
         self.assertEqual('value1', demo())
+
+    def test_func_with_parameters(self):
+        l_dic = LazyDict()
+
+        @l_dic.register
+        def func1(year: int):
+            return year
+
+        @l_dic.register
+        def func2(year: int, month: int = 2):
+            pass
+
+        @l_dic.register
+        def func3(year: int, **kwargs):
+            pass
+
+        my_params = {'year': '2022'}
+
+        self.assertDictEqual({'year': 2022}, l_dic.validate_caller_params('func1', my_params)[0])
+        my_params2 = {'year': 2021}
+        self.assertDictEqual({'year': 2021}, l_dic.validate_caller_params('func1', my_params2)[0])
+        self.assertEqual(2021, l_dic.get('func1', {'year': 2021}))
+
+        self.assertDictEqual({'year': 2021, 'month': 3},
+                             l_dic.validate_caller_params('func2', {'year': 2021, 'month': 3})[0])
+        self.assertDictEqual({'year': 2021, 'month': 2}, l_dic.validate_caller_params('func2', {'year': 2021})[0])
+        self.assertDictEqual({'year': 2021, 'month': 2},
+                             l_dic.validate_caller_params('func3', {'year': 2021, 'month': 2})[0])
+        with self.assertRaises(ParameterMissingError):
+            l_dic.validate_caller_params('func1', {'year': 2021, 'month': 2})
diff --git a/tests/test_widgets.py b/tests/test_widgets.py
index deb65a9..5496d3d 100644
--- a/tests/test_widgets.py
+++ b/tests/test_widgets.py
@@ -1,13 +1,13 @@
 import unittest
 
-from django_echarts.entities import Nav, Menu, LinkItem
+from django_echarts.entities import Nav, LinkItem
 
 
 class NavTestCase(unittest.TestCase):
     def test_nav(self):
         nav = Nav()
-        nav.add_menu('Menu1', slug='menu1', url='/menu1')
-        nav.add_item('Menu1', LinkItem('sub1', '/sub/', after_separator=True))
+        nav.add_left_menu('Menu1', slug='menu1', url='/menu1')
+        nav.add_item_in_left_menu('Menu1', LinkItem('sub1', '/sub/', after_separator=True))
 
         self.assertEqual(1, len(nav.menus))
         self.assertEqual('sub1', nav.menus[0].children[0].text)