├── .coveragerc ├── .github └── workflows │ └── unittest.yaml ├── .gitignore ├── .readthedocs.yaml ├── LICENSE ├── MANIFEST.in ├── README.md ├── django_echarts ├── __init__.py ├── ajax_echarts.py ├── class_typing.py ├── conf.py ├── contrib │ ├── __init__.py │ ├── bootstrap3 │ │ ├── __init__.py │ │ ├── config.py │ │ └── templates │ │ │ ├── about.html │ │ │ ├── base.html │ │ │ ├── blank.html │ │ │ ├── chart_collection.html │ │ │ ├── chart_parametric.html │ │ │ ├── chart_single.html │ │ │ ├── home.html │ │ │ ├── items_grid.html │ │ │ ├── items_list.html │ │ │ ├── list.html │ │ │ ├── list_with_paginator.html │ │ │ ├── message.html │ │ │ ├── settings.html │ │ │ ├── simple │ │ │ └── page.html │ │ │ └── widgets │ │ │ ├── chart_info.html │ │ │ ├── container.html │ │ │ ├── jumbotron.html │ │ │ ├── link_group.html │ │ │ ├── row_container.html │ │ │ ├── title.html │ │ │ └── value_item.html │ ├── bootstrap5 │ │ ├── __init__.py │ │ ├── config.py │ │ └── templates │ │ │ ├── about.html │ │ │ ├── base.html │ │ │ ├── blank.html │ │ │ ├── chart_collection.html │ │ │ ├── chart_parametric.html │ │ │ ├── chart_single.html │ │ │ ├── detail_frontend.html │ │ │ ├── home.html │ │ │ ├── items_grid.html │ │ │ ├── items_list.html │ │ │ ├── items_simple_list.html │ │ │ ├── list.html │ │ │ ├── list_with_paginator.html │ │ │ ├── message.html │ │ │ ├── settings.html │ │ │ ├── simple │ │ │ └── page.html │ │ │ └── widgets │ │ │ ├── chart_info.html │ │ │ ├── container.html │ │ │ ├── jumbotron.html │ │ │ ├── link_group.html │ │ │ ├── row_container.html │ │ │ ├── title.html │ │ │ └── value_item.html │ └── material │ │ ├── __init__.py │ │ ├── config.py │ │ └── templates │ │ ├── about.html │ │ ├── base.html │ │ ├── blank.html │ │ ├── chart_collection.html │ │ ├── chart_parametric.html │ │ ├── chart_single.html │ │ ├── home.html │ │ ├── items_grid.html │ │ ├── items_list.html │ │ ├── list.html │ │ ├── list_with_paginator.html │ │ ├── message.html │ │ ├── settings.html │ │ ├── simple │ │ └── page.html │ │ └── widgets │ │ ├── chart_info.html │ │ ├── container.html │ │ ├── jumbotron.html │ │ ├── link_group.html │ │ ├── row_container.html │ │ ├── title.html │ │ └── value_item.html ├── core │ ├── __init__.py │ ├── dms.py │ ├── exceptions.py │ ├── localfiles.py │ ├── settings_store.py │ └── tms.py ├── custom_maps.py ├── datatools │ ├── __init__.py │ ├── helpers.py │ ├── managers.py │ └── section_counter.py ├── entities │ ├── __init__.py │ ├── articles.py │ ├── base.py │ ├── chart_widgets.py │ ├── containers.py │ ├── html_widgets.py │ ├── layouts.py │ ├── pages.py │ ├── styles.py │ └── uri.py ├── geojson.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── _downloader.py │ │ ├── download.py │ │ ├── info.py │ │ ├── startsite.py │ │ └── starttpl.py ├── renders │ ├── __init__.py │ ├── html.py │ └── javascript.py ├── site_exts.py ├── starter │ ├── __init__.py │ ├── optstools.py │ ├── params_forms.py │ └── sites.py ├── stores │ ├── __init__.py │ └── entity_factory.py ├── templates │ └── snippets │ │ ├── echarts_init_options.tpl │ │ └── first_views.py.tpl ├── templatetags │ ├── __init__.py │ └── echarts.py ├── utils │ ├── __init__.py │ ├── burl.py │ ├── compat.py │ ├── downloader.py │ ├── lazy_dict.py │ └── param_converters.py └── views.py ├── docs ├── about.md ├── breaking-change.md ├── changelog.md ├── gallery.md ├── guides │ ├── advance_development.md │ ├── custom_maps.md │ ├── dms.md │ └── entity_params.md ├── images │ ├── chart_fj-map-gdp.png │ ├── collection_all.png │ ├── fujian-custom-geojson.png │ ├── fujian-forest-coverage.png │ ├── geojson-io-export.png │ ├── home_top.png │ ├── home_with_chart_bootstrap5.png │ ├── home_with_values_panel.png │ ├── list_with_pagination.png │ ├── material_list_page.png │ ├── quickstart-0.png │ ├── row_container_demo.png │ ├── theme_darkly.png │ ├── theme_material.png │ └── timeline_demo.png ├── index.md ├── posts │ ├── new_page_feature.md │ ├── pyecharts_project.md │ ├── third_html_library.md │ ├── use_custom_geojson.md │ └── widget_collection.md ├── reference │ ├── commands.md │ ├── configure.md │ ├── stores.md │ ├── system_design.md │ ├── tags.md │ └── views_and_templates.md ├── release-note │ ├── v050.md │ ├── v051.md │ └── v060.md └── starter │ ├── base.md │ ├── localize_staticfiles.md │ ├── site_configuration.md │ ├── themes.md │ └── widget_and_layout.md ├── long_description.md ├── mkdocs.yaml ├── requirements.txt ├── requirements_dev.txt ├── requirements_doc.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── settings_mock.py ├── test_dms.py ├── test_factory.py ├── test_hosts.py ├── test_interface.py ├── test_layouts.py ├── test_mcharts.py ├── test_renders.py ├── test_section_counter.py ├── test_settings_store.py ├── test_tms.py ├── test_uri.py ├── test_utils.py └── test_widgets.py └── validate_release.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | tests/* 4 | django_echarts/views.py 5 | django_echarts/ajax_echarts.py 6 | django_echarts/starter/* 7 | django_echarts/management/* 8 | django_echarts/utils/compat.py 9 | django_echarts/geojson.py 10 | django_echarts/utils/downloader.py 11 | 12 | [report] 13 | exclude_lines = 14 | pragma: no cover 15 | def __repr__ 16 | def __str__ 17 | def __eq__ 18 | def __hash__ 19 | if self.debug: 20 | if settings.DEBUG 21 | raise AssertionError 22 | raise NotImplementedError 23 | if 0: 24 | if __name__ == .__main__.: 25 | class .*\bProtocol\): 26 | @(abc\.)?abstractmethod 27 | omit = 28 | tests/* 29 | django_echarts/views.py 30 | django_echarts/ajax_echarts.py 31 | django_echarts/starter/* 32 | django_echarts/management/* 33 | django_echarts/utils/compat.py 34 | django_echarts/geojson.py 35 | django_echarts/utils/downloader.py -------------------------------------------------------------------------------- /.github/workflows/unittest.yaml: -------------------------------------------------------------------------------- 1 | name: unittest 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - dev 7 | - 'release/**' 8 | pull_request: 9 | branches: 10 | - master 11 | jobs: 12 | unittest: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | python-version: [ '3.8', '3.9' ] 17 | django-version: [ '2.2', '3.2' ] 18 | pyecharts-version: ['1.9.1', '2.0.2'] 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v2 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | pip install django~=${{ matrix.django-version }} 29 | pip install borax~=4.0 30 | pip install pyecharts~=${{ matrix.pyecharts-version }} 31 | pip install typing_extensions~=4.5 32 | pip install htmlgenerator~=1.2 33 | pip install flake8~=3.9 34 | pip install nose2~=0.10 35 | - name: Lint with flake8 36 | run: | 37 | flake8 django_echarts tests 38 | - name: Test with pytest 39 | run: | 40 | nose2 --with-coverage --coverage django_echarts --coverage-report xml 41 | - name: Upload coverage to Codecov 42 | uses: codecov/codecov-action@v1 43 | with: 44 | file: ./coverage.xml 45 | env_vars: OS,PYTHON 46 | name: codecov-umbrella 47 | fail_ci_if_error: false 48 | verbose: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | .idea 104 | demo.py 105 | example/ -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-20.04 11 | tools: 12 | python: "3.9" 13 | 14 | mkdocs: 15 | configuration: mkdocs.yaml 16 | 17 | # Optionally declare the Python requirements required to build your docs 18 | python: 19 | install: 20 | - requirements: requirements_doc.txt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2022 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include django_echarts/templates *.tpl *.py *.html 2 | include django_echarts/contrib/*.html 3 | include django_echarts/contrib/*/*.html 4 | include django_echarts/contrib/*/*/*.html 5 | include django_echarts/contrib/*/*/*/*.html 6 | -------------------------------------------------------------------------------- /django_echarts/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | A django app for Echarts integration with pyecharts as chart builder. 3 | """ 4 | 5 | __version__ = '0.6.0' 6 | __author__ = 'kinegratii' 7 | -------------------------------------------------------------------------------- /django_echarts/ajax_echarts.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Any 3 | 4 | from django.http.response import JsonResponse 5 | from django.views.generic.base import View 6 | from django_echarts.stores.entity_factory import factory 7 | 8 | 9 | class ChartOptionsView(View): 10 | def get(self, request, *args, **kwargs) -> Any: 11 | chart_name = self.kwargs.get('name') 12 | chart_obj = factory.get_chart_widget(chart_name) 13 | if not chart_obj: 14 | data = {} 15 | else: 16 | data = json.loads(chart_obj.dump_options_with_quotes()) 17 | return JsonResponse(data, safe=False) 18 | -------------------------------------------------------------------------------- /django_echarts/class_typing.py: -------------------------------------------------------------------------------- 1 | from typing import List, Union, TypedDict 2 | 3 | from typing_extensions import NotRequired 4 | 5 | 6 | class TNavConfig(TypedDict): 7 | nav_left: NotRequired[List[Union[str, dict]]] 8 | nav_right: NotRequired[List[Union[str, dict]]] 9 | nav_footer: NotRequired[List[Union[str, dict]]] 10 | -------------------------------------------------------------------------------- /django_echarts/conf.py: -------------------------------------------------------------------------------- 1 | # coding=utf8 2 | 3 | """ 4 | Expose the settings objects. 5 | """ 6 | import os 7 | from django.conf import settings 8 | from django.utils.functional import SimpleLazyObject 9 | 10 | from django_echarts.core.settings_store import SettingsStore 11 | 12 | __all__ = ['DJANGO_ECHARTS_SETTINGS'] 13 | 14 | 15 | def get_django_echarts_settings(): 16 | project_echarts_settings = getattr(settings, 'DJANGO_ECHARTS', {}) 17 | static_url = settings.STATIC_URL 18 | if settings.STATICFILES_DIRS: 19 | staticfiles_dir = str(settings.STATICFILES_DIRS[0]) 20 | else: 21 | staticfiles_dir = os.path.join(str(settings.BASE_DIR), 'static') 22 | extra_settings = { 23 | 'STATIC_URL': settings.STATIC_URL, 24 | 'STATICFILES_DIRS': settings.STATICFILES_DIRS 25 | } 26 | settings_store = SettingsStore( 27 | echarts_settings=project_echarts_settings, 28 | extra_settings=extra_settings, 29 | static_url=static_url, 30 | staticfiles_dir=staticfiles_dir 31 | ) 32 | return settings_store 33 | 34 | 35 | DJANGO_ECHARTS_SETTINGS = SimpleLazyObject(get_django_echarts_settings) # type: SettingsStore 36 | -------------------------------------------------------------------------------- /django_echarts/contrib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/django-echarts/12de6cf4faab1ecbbc06bd487727cc16791ee556/django_echarts/contrib/__init__.py -------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap3/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/django-echarts/12de6cf4faab1ecbbc06bd487727cc16791ee556/django_echarts/contrib/bootstrap3/__init__.py -------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap3/config.py: -------------------------------------------------------------------------------- 1 | NAME = 'bootstrap3' 2 | 3 | PALETTES = [ 4 | "cerulean", "cosmo", "cyborg", "darkly", "flatly", "journal", "lumen", "paper", 5 | "readable", "sandstone", "simplex", "slate", "spacelab", "superhero", "united", "yeti", 6 | ] 7 | 8 | STATIC_URL_DICT = { 9 | 'main_css': 'https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css', 10 | 'palette_css': 'https://bootswatch.com/3/{palette}/bootstrap.min.css', 11 | 'jquery_js': 'https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js', 12 | 'main_js': 'https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js', 13 | } 14 | # replace or append palette css with main css 15 | PALETTE_POLICY = 'replace' 16 | 17 | CLASS_NAMES = {'row': 'row', 'col': 'col-sm-12 col-md-{n}'} 18 | 19 | TABLE_CLASS_NAMES = { 20 | 'default': 'table table-responsive', 21 | 'border': 'table-bordered', 22 | 'borderless': 'table-borderless', 23 | 'striped': 'table-striped', 24 | 'size': 'table-{size}' 25 | } 26 | -------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap3/templates/about.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load echarts %} 3 | 4 | {% block main_content %} 5 |

The about Info

6 | 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap3/templates/blank.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block extra_css %} 3 | {% endblock %} 4 | 5 | {% block main_content %} 6 |

This is a blank page.

7 | 8 | {% endblock %} 9 | 10 | {% block extra_script %} 11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap3/templates/chart_collection.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load echarts %} 3 | 4 | {% block main_content %} 5 | {% dw_widget widget_collection %} 6 | {% endblock %} 7 | 8 | {% block extra_script %} 9 | {% echarts_js_dependencies widget_collection %} 10 | {% echarts_js_content widget_collection %} 11 | {% endblock %} -------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap3/templates/chart_parametric.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load echarts %} 3 | 4 | {% block main_content %} 5 | {{ chart_info.title }} 6 | 7 | {% dw_widget link_group %} 8 | {% endblock %} -------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap3/templates/chart_single.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load echarts %} 3 | 4 | {% block main_content %} 5 |
6 | 7 |
8 | 11 | {% if chart_info.description %} 12 |
{{ chart_info.description }}
13 | {% endif %} 14 | {% if chart_info.tags %} 15 |
16 | {% for tag in chart_info.tags %} 17 | {{ tag }} 18 | {% endfor %} 19 |
20 | 21 | {% endif %} 22 |
23 | {% dw_widget chart_obj %} 24 | {% if chart_info.body %} 25 |
26 |
{{ chart_info.body }}
27 | {% endif %} 28 | 29 |
30 | 31 | 32 | 33 |
34 | 35 | {% endblock %} 36 | 37 | {% block extra_script %} 38 | {% echarts_js_dependencies chart_obj %} 39 | {% echarts_js_content chart_obj %} 40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap3/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load echarts %} 3 | {% block main_content %} 4 | {% dw_widget jumbotron %} 5 | {% if chart_obj %} 6 |
7 | 8 |
9 |
{{ chart_info.title }}
10 |
11 |
12 |
13 | {% dw_widget chart_obj width="100%" height="320px" %} 14 |
15 |
16 |

{{ chart_info.description }}

17 |
18 |
19 |
20 |
21 | 22 | 23 |
24 | {% endif %} 25 | {% if home_values_panel %} 26 | {% dw_widget home_values_panel %} 27 | {% endif %} 28 | {% if top_chart_info_list %} 29 | 32 | {% include layout_tpl with chart_info_list=top_chart_info_list only %} 33 | {% endif %} 34 | 35 | {% endblock %} 36 | {% block extra_script %} 37 | {% if chart_obj %} 38 | {% echarts_js_dependencies chart_obj %} 39 | {% echarts_js_content chart_obj %} 40 | {% endif %} 41 | {% endblock %} -------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap3/templates/items_grid.html: -------------------------------------------------------------------------------- 1 | {% load echarts %} 2 |
3 | {% for chart_info in chart_info_list %} 4 |
5 |
6 |
7 | {{ chart_info.title }} 8 | {% if chart_info.tags %} 9 | 10 | {% for tag in chart_info.tags %} 11 | {{ tag }} 12 | {% endfor %} 13 | 14 | {% endif %} 15 | 16 |
17 |
18 |

{{ chart_info.description|default:"暂无描述" }}

19 |
20 | 21 |
22 |
23 | {% endfor %} 24 |
-------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap3/templates/items_list.html: -------------------------------------------------------------------------------- 1 | {% load echarts %} 2 |
3 | {% for chart_info in chart_info_list %} 4 | 5 |

{{ chart_info.title }}

6 |

{{ chart_info.description|default:"暂无描述" }}

7 |
8 | {% endfor %} 9 | 10 |
-------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap3/templates/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block main_content %} 4 | 7 |
8 |
9 | 11 | 12 | 13 | 14 |
15 |

16 | {% include layout_tpl with chart_info_list=chart_info_list only %} 17 | {% endblock %} -------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap3/templates/list_with_paginator.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load echarts %} 3 | 4 | {% block main_content %} 5 | 6 | 9 |
10 |
11 | 13 | 14 | 15 | 16 |
17 |

18 | {% include layout_tpl with chart_info_list=page_obj.object_list only %} 19 | 20 | 21 | 43 | 44 | 45 | {% endblock %} -------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap3/templates/message.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load echarts %} 3 | 4 | {% block main_content %} 5 |
6 |
{{ message.title }}
7 |
8 |

{{ message.text }}

9 |
10 | 11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap3/templates/settings.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block main_content %} 3 |
4 | {% csrf_token %} 5 | {% for field in form %} 6 |
7 | {{ field.label_tag }} {{ field }} 8 |
9 | {% endfor %} 10 | 11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap3/templates/simple/page.html: -------------------------------------------------------------------------------- 1 | {% load echarts %} 2 | 3 | 4 | 5 | 6 | 7 | Title 8 | {% block include_css %} 9 | {% theme_css %} 10 | {% endblock %} 11 | 12 | 13 | {% dw_widget container_obj %} 14 | {% theme_js %} 15 | {% echarts_js_dependencies container_obj %} 16 | {% echarts_js_content container_obj %} 17 | 18 | -------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap3/templates/widgets/chart_info.html: -------------------------------------------------------------------------------- 1 |
2 |
{{ widget.title }}
3 |

{{ widget.description }}

4 |
-------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap3/templates/widgets/container.html: -------------------------------------------------------------------------------- 1 | {% load echarts %} 2 | {% if widget.div_class %} 3 |
4 | {% endif %} 5 | {% for row_container in widget %} 6 | {% dw_widget row_container %} 7 | {% endfor %} 8 | {% if widget.div_class %} 9 |
10 | {% endif %} -------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap3/templates/widgets/jumbotron.html: -------------------------------------------------------------------------------- 1 |
2 |

{{ widget.title }}

3 |

{{ widget.main_text }}

4 |

{{ widget.small_text }}

5 |
-------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap3/templates/widgets/link_group.html: -------------------------------------------------------------------------------- 1 |
2 | {% for link in widget %} 3 | {{ link.text }} 4 | {% endfor %} 5 |
-------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap3/templates/widgets/row_container.html: -------------------------------------------------------------------------------- 1 | {% load echarts %} 2 |
3 | {% for rw, layout in widget.iter_layout %} 4 |
5 | {% dw_widget rw width=layout.ww height=layout.wh %} 6 |
7 | {% endfor %} 8 |
-------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap3/templates/widgets/title.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap3/templates/widgets/value_item.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ widget.value }} {{ widget.unit }} 5 | {% if widget.arrow %}{% endif %}

6 |
7 |

{{ widget.description }}

8 |
9 |
-------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap5/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/django-echarts/12de6cf4faab1ecbbc06bd487727cc16791ee556/django_echarts/contrib/bootstrap5/__init__.py -------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap5/config.py: -------------------------------------------------------------------------------- 1 | NAME = 'bootstrap5' 2 | 3 | PALETTES = ["cerulean", "cosmo", "cyborg", "darkly", "flatly", "journal", "litera", "lumen", "lux", "materia", 4 | "minty", "morph", "pulse", "quartz", "sandstone", "simplex", "sketchy", "slate", "solar", "spacelab", 5 | "superhero", "united", "vapor", "yeti", "zephyr", 6 | ] 7 | 8 | STATIC_URL_DICT = { 9 | 'main_css': 'https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css', 10 | 'palette_css': 'https://bootswatch.com/5/{palette}/bootstrap.min.css', 11 | 'font_css': 'https://cdnjs.cloudflare.com/ajax/libs/bootstrap-icons/1.8.1/font/bootstrap-icons.min.css', 12 | 'jquery_js': 'https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js', 13 | 'main_js': 'https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js', 14 | } 15 | # replace or append palette css with main css 16 | PALETTE_POLICY = 'replace' 17 | 18 | CLASS_NAMES = {'row': 'row', 'col': 'col-sm-12 col-md-{n}'} 19 | 20 | TABLE_CLASS_NAMES = { 21 | 'default': 'table table-responsive', 22 | 'border': 'table-bordered', 23 | 'borderless': 'table-borderless', 24 | 'striped': 'table-striped', 25 | 'size': 'table-{size}' 26 | } 27 | -------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap5/templates/about.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load echarts %} 3 | 4 | {% block main_content %} 5 |

The about Info

6 |
7 | {% include 'widgets/collection.html' %} 8 |
9 | 10 | {% endblock %} 11 | {% block extra_script %} 12 | {% echarts_js_dependencies collection %} 13 | {% echarts_js_content collection %} 14 | {% endblock %} -------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap5/templates/blank.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block extra_css %} 3 | {% endblock %} 4 | 5 | {% block main_content %} 6 |

This is a blank page.

7 | 8 | {% endblock %} 9 | 10 | {% block extra_script %} 11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap5/templates/chart_collection.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load echarts %} 3 | 4 | {% block main_content %} 5 | {% dw_widget widget_collection %} 6 | {% endblock %} 7 | 8 | {% block extra_script %} 9 | {% echarts_js_dependencies widget_collection %} 10 | {% echarts_js_content widget_collection %} 11 | {% endblock %} -------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap5/templates/chart_parametric.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load echarts %} 3 | 4 | {% block main_content %} 5 | {{ chart_info.title }} 6 | 7 | {% dw_widget link_group %} 8 | {% endblock %} -------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap5/templates/chart_single.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load echarts %} 3 | 4 | {% block main_content %} 5 |
6 |
7 | 10 |
11 | 12 | {% if chart_info.description %} 13 |
{{ chart_info.description }}
14 | {% endif %} 15 | {% if chart_info.tags %} 16 |
17 | {% for tag in chart_info.tags %} 18 | {{ tag }} 19 | {% endfor %} 20 |
21 | 22 | {% endif %} 23 | 24 |
25 |
26 | {% dw_widget chart_obj %} 27 |
28 | {% if chart_info.body %} 29 |
30 |
{{ chart_info.body }}
31 | {% endif %} 32 | 33 |
34 | 35 | 36 | 37 |
38 | 39 | {% endblock %} 40 | 41 | {% block extra_script %} 42 | {% echarts_js_dependencies chart_obj %} 43 | {% echarts_js_content chart_obj %} 44 | {% endblock %} 45 | -------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap5/templates/detail_frontend.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load echarts %} 3 | 4 | {% block main_content %} 5 |
6 |
7 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | {% if chart_info.tags %} 18 | 19 | 20 | 23 | 24 | {% endif %} 25 |
描述{{ chart_info.description }}
标签 {% for tag in chart_info.tags %} 21 | {{ tag }} 22 | {% endfor %}
26 | 27 |
28 |
29 | {% dw_widget chart_obj %} 30 |
31 | 32 |
33 | 34 | 35 | 36 |
37 | 38 | {% endblock %} 39 | 40 | {% block extra_script %} 41 | {% echarts_js_dependencies chart_obj %} 42 | {% echarts_js_content chart_obj %} 43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap5/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load echarts %} 3 | {% block main_content %} 4 | {% dw_widget jumbotron %} 5 | {% if chart_obj %} 6 |
7 |
8 | 9 |
10 |
11 |
12 | {% dw_widget chart_obj width="100%" height="320px" %} 13 |
14 |
15 |
16 |
{{ chart_info.title }}
17 |

{{ chart_info.description }}

18 |
19 |
20 |
21 |
22 | 23 | 24 |
25 |
26 | {% endif %} 27 | {% if home_values_panel %} 28 | {% dw_widget home_values_panel %} 29 | {% endif %} 30 | {% if top_chart_info_list %} 31 |
32 |

热门图表

33 |
34 |
35 | {% include layout_tpl with chart_info_list=top_chart_info_list only %} 36 | {% endif %} 37 | {% endblock %} 38 | {% block extra_script %} 39 | {% if chart_obj %} 40 | {% echarts_js_dependencies chart_obj %} 41 | {% echarts_js_content chart_obj %} 42 | {% endif %} 43 | {% endblock %} -------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap5/templates/items_grid.html: -------------------------------------------------------------------------------- 1 | {% load echarts %} 2 |
3 | {% for chart_info in chart_info_list %} 4 |
5 | 6 |
7 |
8 | {{ chart_info.title }} 9 | {% if chart_info.tags %}  10 | {% for tag in chart_info.tags %} 11 | {{ tag }} 12 | {% endfor %} 13 | {% endif %} 14 |
15 |
16 |

{{ chart_info.description|default:"暂无描述" }}

17 | 详情 19 |
20 |
21 |
22 | 23 |
24 | {% endfor %} 25 |
-------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap5/templates/items_list.html: -------------------------------------------------------------------------------- 1 | {% load echarts %} 2 |
3 | {% for chart_info in chart_info_list %} 4 | 5 |

{{ chart_info.title }}

6 |

{{ chart_info.description|default:"暂无描述" }}

7 |
8 | {% endfor %} 9 | 10 |
-------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap5/templates/items_simple_list.html: -------------------------------------------------------------------------------- 1 |
2 | {% for chart_info in chart_info_list %} 3 | 4 | {{ chart_info.title }} 5 | 6 | {% endfor %} 7 | 8 |
-------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap5/templates/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block main_content %} 4 | 7 |
8 |
9 | 11 | 12 | 13 | 14 |
15 |

16 | {% include layout_tpl with chart_info_list=chart_info_list only %} 17 | {% endblock %} -------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap5/templates/list_with_paginator.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load echarts %} 3 | 4 | {% block main_content %} 5 | 6 | 9 |
10 |
11 | 13 | 14 | 15 | 16 |
17 |

18 | {% include layout_tpl with chart_info_list=page_obj.object_list only %} 19 | 20 | 21 | 42 | 43 | 44 | {% endblock %} -------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap5/templates/message.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load echarts %} 3 | 4 | {% block main_content %} 5 |
6 |
{{ message.title }}
7 |
8 |

{{ message.text }}

9 |
10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap5/templates/settings.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block main_content %} 3 |
4 | {% csrf_token %} 5 | {% for field in form %} 6 |
7 | {{ field.label_tag }} {{ field }} 8 |
9 | {% endfor %} 10 | 11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap5/templates/simple/page.html: -------------------------------------------------------------------------------- 1 | {% load echarts %} 2 | 3 | 4 | 5 | 6 | 7 | Title 8 | {% block include_css %} 9 | {% theme_css %} 10 | {% endblock %} 11 | 12 | 13 | {% dw_widget container_obj %} 14 | {% theme_js %} 15 | {% echarts_js_dependencies container_obj %} 16 | {% echarts_js_content container_obj %} 17 | 18 | -------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap5/templates/widgets/chart_info.html: -------------------------------------------------------------------------------- 1 |
2 |
{{ widget.title }}
3 |

{{ widget.description }}

4 |
-------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap5/templates/widgets/container.html: -------------------------------------------------------------------------------- 1 | {% load echarts %} 2 | {% if widget.div_class %} 3 |
4 | {% endif %} 5 | {% for row_container in widget %} 6 | {% dw_widget row_container %} 7 | {% endfor %} 8 | {% if widget.div_class %} 9 |
10 | {% endif %} -------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap5/templates/widgets/jumbotron.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ widget.title }}

4 |
5 |

{{ widget.main_text }}

6 |

{{ widget.small_text }}

7 |
8 |
-------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap5/templates/widgets/link_group.html: -------------------------------------------------------------------------------- 1 |
2 | {% for link in widget %} 3 | {{ link.text }} 4 | {% endfor %} 5 |
-------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap5/templates/widgets/row_container.html: -------------------------------------------------------------------------------- 1 | {% load echarts %} 2 |
3 | {% for rw, layout in widget.iter_layout %} 4 |
5 | {% dw_widget rw width=layout.ww height=layout.wh %} 6 |
7 | {% endfor %} 8 |
-------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap5/templates/widgets/title.html: -------------------------------------------------------------------------------- 1 |
2 |

{{ widget.text }}{% if widget.small_text %}  3 | {{ widget.small_text }}{% endif %}

4 |
5 |
-------------------------------------------------------------------------------- /django_echarts/contrib/bootstrap5/templates/widgets/value_item.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ widget.value }} {{ widget.unit }} 4 | {% if widget.arrow %}{% endif %} 5 |

6 |
7 | 10 |
-------------------------------------------------------------------------------- /django_echarts/contrib/material/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/django-echarts/12de6cf4faab1ecbbc06bd487727cc16791ee556/django_echarts/contrib/material/__init__.py -------------------------------------------------------------------------------- /django_echarts/contrib/material/config.py: -------------------------------------------------------------------------------- 1 | NAME = 'material' 2 | 3 | PALETTES = [] 4 | 5 | STATIC_URL_DICT = { 6 | 'font_css': 'https://fonts.font.im/icon?family=Material+Icons', 7 | 'main_css': 'https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css', 8 | 'jquery_js': 'https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js', 9 | 'main_js': 'https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js', 10 | } 11 | # replace or append palette css with main css 12 | PALETTE_POLICY = 'replace' 13 | 14 | CLASS_NAMES = {'row': 'row', 'col': 'col s12 m{n}'} 15 | 16 | TABLE_CLASS_NAMES = { 17 | 'default': 'responsive-table', 18 | 'striped': 'striped', 19 | 'center': 'centered' 20 | } 21 | -------------------------------------------------------------------------------- /django_echarts/contrib/material/templates/about.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block main_content %} 3 |

The about Info

4 | {% endblock %} -------------------------------------------------------------------------------- /django_echarts/contrib/material/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load echarts %} 2 | 3 | 4 | 5 | 6 | {{ page_title }} 7 | {% block include_css %} 8 | {% theme_css %} 9 | {% endblock %} 10 | {% block extra_css %} 11 | {% endblock %} 12 | 13 | 14 | 15 | {% block nav %} 16 | 61 | {% endblock %} 62 | {% block main_content %} 63 | {% endblock %} 64 | {% block footer %} 65 | 89 | {% endblock %} 90 | 91 | {% block include_script %} 92 | {% theme_js %} 93 | {% endblock %} 94 | 95 | {% block extra_script %} 96 | 101 | {% endblock %} 102 | 103 | -------------------------------------------------------------------------------- /django_echarts/contrib/material/templates/blank.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block extra_css %} 3 | {% endblock %} 4 | 5 | {% block main_content %} 6 |

This is a blank page.

7 | 8 | {% endblock %} 9 | 10 | {% block extra_script %} 11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /django_echarts/contrib/material/templates/chart_collection.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load echarts %} 3 | 4 | {% block main_content %} 5 | {% dw_widget widget_collection %} 6 | {% endblock %} 7 | 8 | {% block extra_script %} 9 | {% echarts_js_dependencies widget_collection %} 10 | {% echarts_js_content widget_collection %} 11 | {% endblock %} -------------------------------------------------------------------------------- /django_echarts/contrib/material/templates/chart_parametric.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load echarts %} 3 | 4 | {% block main_content %} 5 | {{ chart_info.title }} 6 | 7 | {% dw_widget link_group %} 8 | {% endblock %} -------------------------------------------------------------------------------- /django_echarts/contrib/material/templates/chart_single.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load echarts %} 3 | 4 | {% block main_content %} 5 |
6 |
7 |
8 |
9 |
{{ title }}
10 |
11 | 12 | {% if chart_info.description %} 13 |
14 |

{{ chart_info.description }}

15 | {% if chart_info.tags %} 16 |

17 | {% for tag in chart_info.tags %} 18 | {{ tag }} 19 | {% endfor %} 20 |

21 | {% endif %} 22 |
23 | {% endif %} 24 |
25 | {% dw_widget chart_obj %} 26 | {% if chart_info.body %} 27 |
28 |
{{ chart_info.body }}
29 | {% endif %} 30 |
31 |
32 |
33 | 34 | {% endblock %} 35 | 36 | {% block extra_script %} 37 | {% echarts_js_dependencies chart_obj %} 38 | {% echarts_js_content chart_obj %} 39 | {% endblock %} -------------------------------------------------------------------------------- /django_echarts/contrib/material/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load echarts %} 3 | {% block main_content %} 4 | 5 |
6 |
7 | {% dw_widget jumbotron %} 8 | {% if home_values_panel %} 9 | {% dw_widget home_values_panel %} 10 | {% endif %} 11 | {% if top_chart_info_list %} 12 | {% include layout_tpl with chart_info_list=top_chart_info_list only %} 13 | {% endif %} 14 |
15 | 16 |
17 | 18 | 19 | {% endblock %} -------------------------------------------------------------------------------- /django_echarts/contrib/material/templates/items_grid.html: -------------------------------------------------------------------------------- 1 | {% load echarts %} 2 |
3 | {% for chart_info in chart_info_list %} 4 |
5 |
6 |
7 | {{ chart_info.title }} 8 |

{{ chart_info.description|default:"暂无描述" }}

9 | {% if chart_info.tags %} 10 | {% for tag in chart_info.tags %} 11 | {{ tag }}   12 | {% endfor %} 13 | {% endif %} 14 |
15 |
16 | 详情 17 |
18 |
19 |
20 | {% endfor %} 21 | 22 |
-------------------------------------------------------------------------------- /django_echarts/contrib/material/templates/items_list.html: -------------------------------------------------------------------------------- 1 | {% load echarts %} 2 |
3 | {% for chart_info in chart_info_list %} 4 |
5 |
6 |
7 | {{ chart_info.title }} 8 |

{{ chart_info.description|default:"暂无描述" }}

9 | {% if chart_info.tags %} 10 |
11 | {% for tag in chart_info.tags %} 12 | {{ tag }}   13 | {% endfor %} 14 | {% endif %} 15 | 16 | 17 |
18 |
19 | 详情 20 |
21 |
22 |
23 | {% endfor %} 24 |
-------------------------------------------------------------------------------- /django_echarts/contrib/material/templates/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block main_content %} 4 |
5 |

图表列表

6 |
7 |
8 |
9 | 11 |
12 | 13 |
14 |
15 | 16 |
17 | 18 | {% include layout_tpl with chart_info_list=chart_info_list only %} 19 |
20 | 21 | {% endblock %} -------------------------------------------------------------------------------- /django_echarts/contrib/material/templates/list_with_paginator.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load echarts %} 3 | 4 | {% block main_content %} 5 |
6 |

All Charts

7 |
8 |
9 |
10 | 12 |
13 | 14 |
15 |
16 | 17 |
18 | {% include layout_tpl with chart_info_list=page_obj.object_list only %} 19 | 20 |
21 |
22 |
    23 | {% for page in elided_page_nums %} 24 | {% if page == '...' %} 25 |
  • 26 | {% else %} 27 | {% if page == page_obj.number %} 28 |
  • 29 | {{ page_obj.number }} 30 |
  • 31 | {% else %} 32 |
  • 33 | 34 | 35 | 36 |
  • 37 | {% endif %} 38 | {% endif %} 39 | {% endfor %} 40 |
41 | 42 | 43 |
44 |
45 |
46 | 47 | 48 | 49 | 50 | {% endblock %} -------------------------------------------------------------------------------- /django_echarts/contrib/material/templates/message.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block main_content %} 3 |
4 |
5 |
6 |
7 | {{ message.text }} 8 |
9 |
10 |
11 |
12 | {% endblock %} -------------------------------------------------------------------------------- /django_echarts/contrib/material/templates/settings.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block main_content %} 3 |
4 |
5 | {% csrf_token %} 6 | {% for field in form %} 7 |
8 |
9 | {{ field }} {{ field.label_tag }} 10 |
11 |
12 | {% endfor %} 13 | 14 |
15 |
16 | 17 | {% endblock %} 18 | 19 | {% block extra_script %} 20 | 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /django_echarts/contrib/material/templates/simple/page.html: -------------------------------------------------------------------------------- 1 | {% load echarts %} 2 | 3 | 4 | 5 | 6 | 7 | Title 8 | {% block include_css %} 9 | {% theme_css %} 10 | {% endblock %} 11 | 12 | 13 | {% dw_widget container_obj %} 14 | {% theme_js %} 15 | {% echarts_js_dependencies container_obj %} 16 | {% echarts_js_content container_obj %} 17 | 18 | -------------------------------------------------------------------------------- /django_echarts/contrib/material/templates/widgets/chart_info.html: -------------------------------------------------------------------------------- 1 |

{{ widget.description }}

-------------------------------------------------------------------------------- /django_echarts/contrib/material/templates/widgets/container.html: -------------------------------------------------------------------------------- 1 | {% load echarts %} 2 | {% if widget.div_class %} 3 |
4 | {% endif %} 5 | {% for row_container in widget %} 6 | {% dw_widget row_container %} 7 | {% endfor %} 8 | {% if widget.div_class %} 9 |
10 | {% endif %} -------------------------------------------------------------------------------- /django_echarts/contrib/material/templates/widgets/jumbotron.html: -------------------------------------------------------------------------------- 1 |

2 |

{{ widget.title }}

3 |
4 |
{{ widget.main_text }}
5 |
6 |
7 | {{ widget.small_text }} 8 |
9 |

-------------------------------------------------------------------------------- /django_echarts/contrib/material/templates/widgets/link_group.html: -------------------------------------------------------------------------------- 1 |
2 | {% for link in widget %} 3 | {{ link.text }} 4 | {% endfor %} 5 |
-------------------------------------------------------------------------------- /django_echarts/contrib/material/templates/widgets/row_container.html: -------------------------------------------------------------------------------- 1 | {% load echarts %} 2 |
3 | {% for rw, layout in widget.iter_layout %} 4 |
5 | {% dw_widget rw width=layout.ww height=layout.wh %} 6 |
7 | {% endfor %} 8 |
-------------------------------------------------------------------------------- /django_echarts/contrib/material/templates/widgets/title.html: -------------------------------------------------------------------------------- 1 |
2 |

{{ widget.text }}{% if widget.small_text %}  3 | {{ widget.small_text }}{% endif %}

4 |
5 |
-------------------------------------------------------------------------------- /django_echarts/contrib/material/templates/widgets/value_item.html: -------------------------------------------------------------------------------- 1 |
2 |

{{ widget.value }} {{ widget.unit }}

4 |
5 |

{{ widget.description }}

6 |
-------------------------------------------------------------------------------- /django_echarts/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/django-echarts/12de6cf4faab1ecbbc06bd487727cc16791ee556/django_echarts/core/__init__.py -------------------------------------------------------------------------------- /django_echarts/core/exceptions.py: -------------------------------------------------------------------------------- 1 | __all__ = ['DJEAbortException', 'WidgetNotRegisteredError'] 2 | 3 | 4 | class DJEAbortException(BaseException): 5 | def __init__(self, message: str): 6 | self.message = message 7 | super().__init__(message) 8 | 9 | 10 | # ErrorMessageException / WarningMessageException 11 | 12 | class ChartFuncCallError(BaseException): 13 | def __init__(self, char_name: str): 14 | super().__init__(f'ChartName:{char_name}') 15 | 16 | 17 | class WidgetNotRegisteredError(BaseException): 18 | def __init__(self, widget): 19 | super().__init__(f'Unknown widget type:{widget.__class__.__name__}') 20 | 21 | 22 | class ChartDoesNotExist(BaseException): 23 | def __init__(self, message: str = None, param_name: str = None, param_input=None, param_choices=None): 24 | if param_name: 25 | message = f'Invalid param value: {param_name}={param_input}. Choices are: {param_choices}' 26 | super().__init__(message) 27 | -------------------------------------------------------------------------------- /django_echarts/core/localfiles.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | import os 3 | 4 | 5 | class DownloaderResource: 6 | def __init__(self, remote_url, ref_url, local_path, label=None, catalog=None, exists=False): 7 | self.remote_url = remote_url 8 | self.ref_url = ref_url 9 | self.local_path = local_path 10 | self.label = label or '' 11 | self.catalog = catalog or '' 12 | self.exists = exists 13 | 14 | 15 | class LocalFilesMixin: 16 | def set_localize_opts(self, static_url, staticfiles_dir): 17 | """Set options for localize_opts""" 18 | self._localize_opts = { 19 | 'static_url': static_url, 20 | 'staticfiles_dir': staticfiles_dir 21 | } 22 | 23 | def url2filename(self, url, **kwargs) -> str: 24 | pass 25 | 26 | def localize_url(self, filename: str) -> Tuple[str, str]: 27 | """Return local ref url and local file path from a remote url.""" 28 | local_ref_url = self._localize_opts['static_url'] + filename 29 | local_path = os.path.join(self._localize_opts['staticfiles_dir'], filename) 30 | return local_ref_url, local_path 31 | -------------------------------------------------------------------------------- /django_echarts/datatools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/django-echarts/12de6cf4faab1ecbbc06bd487727cc16791ee556/django_echarts/datatools/__init__.py -------------------------------------------------------------------------------- /django_echarts/datatools/helpers.py: -------------------------------------------------------------------------------- 1 | """ 2 | 数据处理函数 3 | See more detail https://mp.weixin.qq.com/s/ZYU7g3mVF-cvRZ3JyMIngw 4 | """ 5 | import statistics 6 | from collections import namedtuple, UserList 7 | from dataclasses import dataclass 8 | from typing import List, Tuple 9 | 10 | 11 | def adjust_max_min(values: list): 12 | ma = max(values) 13 | mi = min(values) 14 | nmax = int(ma + (ma - mi) / 2) + 1 15 | nmin = int(mi - (ma - mi) / 2) 16 | return nmax, nmin 17 | 18 | 19 | def ceil_n(val, n=3): 20 | return (val // (10 ** n) + 1) * 10 ** n 21 | 22 | 23 | def floor_n(val, n=3): 24 | return (val // (10 ** n)) * 10 ** n 25 | 26 | 27 | QuartilesRank = namedtuple('QuartilesRank', 'rank text color') 28 | 29 | 30 | @dataclass 31 | class SeriesStatisticsValue: 32 | """一维数据统计值""" 33 | label: str 34 | index: int 35 | num: int 36 | avg: int 37 | std_deviation: int 38 | min_: int 39 | q1: int 40 | q2: int 41 | q3: int 42 | max_: int 43 | 44 | @property 45 | def boxplot_values(self) -> Tuple: 46 | """Return five values for boxplot""" 47 | return (self.min_, self.q1, self.q2, self.q3, self.max_) 48 | 49 | def evaluate(self, value: int) -> QuartilesRank: 50 | if value < self.q1: 51 | return QuartilesRank(rank=1, text='下', color='danger') 52 | elif value < self.q2: 53 | return QuartilesRank(rank=2, text='中下', color='warning') 54 | elif value < self.q3: 55 | return QuartilesRank(rank=2, text='中上', color='info') 56 | else: 57 | return QuartilesRank(rank=2, text='上', color='success') 58 | 59 | @classmethod 60 | def from_(cls, data: List, label: str = '', index=0): 61 | q1, q2, q3 = list(map(int, statistics.quantiles(data))) 62 | return cls(label=label, num=len(data), 63 | avg=int(statistics.mean(data)), std_deviation=int(statistics.pstdev(data)), 64 | min_=min(data), max_=max(data), q1=q1, q2=q2, q3=q3, index=index) 65 | 66 | 67 | class SeriesStatisticsList(UserList[SeriesStatisticsValue]): 68 | 69 | @property 70 | def axis_values(self): 71 | return [item.label for item in self] 72 | 73 | @property 74 | def boxplot_data(self): 75 | return [list(item.boxplot_values) for item in self] 76 | -------------------------------------------------------------------------------- /django_echarts/datatools/managers.py: -------------------------------------------------------------------------------- 1 | # coding=utf8 2 | 3 | from borax.datasets.fetch import fetch 4 | from django.db import models 5 | 6 | 7 | class AxisValuesQuerySet(models.QuerySet): 8 | def as_axis_values(self, *keys, **kwargs): 9 | return fetch(self, *keys, **kwargs) 10 | -------------------------------------------------------------------------------- /django_echarts/datatools/section_counter.py: -------------------------------------------------------------------------------- 1 | # coding=utf8 2 | """ 3 | A indexes-based counter module for value list. 4 | 1. It is some likely feature but indexes-based 5 | 2. Index match supports the "in" operator 6 | 3. The counter of missing index will be set to 0 7 | 4. export to counter or axis data list 8 | """ 9 | 10 | from collections import Counter 11 | 12 | MIN_LOWER = -100000 # The min value of range lower. 13 | 14 | 15 | class BIndex: 16 | def __contains__(self, item): 17 | return False 18 | 19 | def __str__(self): 20 | return '' 21 | 22 | def _key(self): 23 | return 0 24 | 25 | 26 | class BValueIndex(BIndex): 27 | def __init__(self, value): 28 | self.value = value 29 | 30 | def __contains__(self, item): 31 | return item == self.value 32 | 33 | def __str__(self): 34 | return str(self.value) 35 | 36 | def _key(self): 37 | return self.value 38 | 39 | 40 | class BSectionIndex(BIndex): 41 | def __init__(self, lower=None, upper=None): 42 | self.lower = lower 43 | self.upper = upper 44 | self._lower_specified = self.lower is not None 45 | self._upper_specified = self.upper is not None 46 | 47 | def __contains__(self, item): 48 | if self._lower_specified: 49 | if self._upper_specified: 50 | return self.lower <= item <= self.upper 51 | else: 52 | return self.lower <= item 53 | else: 54 | if self._upper_specified: 55 | return item <= self.upper 56 | else: 57 | return True 58 | 59 | def __str__(self): 60 | if self._lower_specified and self._upper_specified: 61 | return '{0}~{1}'.format(self.lower, self.upper) 62 | elif self._lower_specified: 63 | return '≥{}'.format(self.lower) 64 | elif self._upper_specified: 65 | return '≤{}'.format(self.upper) 66 | else: 67 | return '~' 68 | 69 | def _key(self): 70 | return self.lower if self._lower_specified else MIN_LOWER 71 | 72 | 73 | class BRangeIndex(BIndex): 74 | def __init__(self, lower=None, upper=None): 75 | self.lower = lower 76 | self.upper = upper 77 | self._lower_specified = self.lower is not None 78 | self._upper_specified = self.upper is not None 79 | 80 | def __contains__(self, item): 81 | if self._lower_specified: 82 | if self._upper_specified: 83 | return self.lower <= item < self.upper 84 | else: 85 | return self.lower <= item 86 | else: 87 | if self._upper_specified: 88 | return item < self.upper 89 | else: 90 | return True 91 | 92 | def __str__(self): 93 | if self._lower_specified and self._upper_specified: 94 | return '{0}~{1}'.format(self.lower, self.upper) 95 | elif self._lower_specified: 96 | return '≥{}'.format(self.lower) 97 | elif self._upper_specified: 98 | return '<{}'.format(self.upper) 99 | else: 100 | return '~' 101 | 102 | def _key(self): 103 | return self.lower if self._lower_specified else MIN_LOWER 104 | 105 | 106 | class BSectionCounter: 107 | def __init__(self, *indexes): 108 | self.indexes = indexes 109 | 110 | def feed(self, data): 111 | c = Counter({str(r): 0 for r in self.indexes}) 112 | for d in data: 113 | c.update([str(r) for r in self.indexes if d in r]) 114 | return c 115 | 116 | def feed_as_axises(self, data): 117 | rc = self.feed(data) 118 | keys = sorted(self.indexes, key=lambda x: x._key()) 119 | return zip(*[(str(k), rc[str(k)]) for k in keys]) 120 | 121 | @classmethod 122 | def from_simple(cls, *index_or_number_list): 123 | indexes = [] 124 | for ion in index_or_number_list: 125 | if isinstance(ion, BIndex): 126 | indexes.append(ion) 127 | elif isinstance(ion, (tuple, list)) and len(ion) == 2: 128 | indexes.append(BRangeIndex(ion[0], ion[1])) 129 | else: 130 | indexes.append(BValueIndex(ion)) 131 | return cls(*indexes) 132 | 133 | @classmethod 134 | def from_range(cls, start, end, step): 135 | indexes = [BRangeIndex(i, i + step) for i in range(start, end, step)] 136 | return cls(*indexes) 137 | -------------------------------------------------------------------------------- /django_echarts/entities/__init__.py: -------------------------------------------------------------------------------- 1 | from .articles import ( 2 | ChartInfo, ChartInfoManagerMixin, LocalChartInfoManager 3 | ) 4 | from .base import DwString 5 | from .chart_widgets import NamedCharts, BlankChart 6 | from .containers import RowContainer, Container 7 | from .html_widgets import ( 8 | HTMLBase, LinkItem, SeparatorItem, Menu, Jumbotron, Nav, Copyright, Message, ValuesPanel, ValueItem, Title, 9 | ElementEntity, LinkGroup 10 | ) 11 | from .pages import (WidgetCollection, WidgetGetterMixin) 12 | from .styles import (bootstrap_table_class, material_table_class) 13 | from .uri import EntityURI 14 | -------------------------------------------------------------------------------- /django_echarts/entities/base.py: -------------------------------------------------------------------------------- 1 | class DwString(str): 2 | """This is a str for django template string.""" 3 | 4 | @classmethod 5 | def login_name(cls, login_text: str = '{{ request.user.username }}', un_login_text: str = 'Unknown'): 6 | code_list = [ 7 | '{% if request.user.is_authenticated %}', 8 | login_text, 9 | '{% else %}', 10 | un_login_text, 11 | '{% endif %}' 12 | ] 13 | return cls(''.join(code_list)) 14 | -------------------------------------------------------------------------------- /django_echarts/entities/chart_widgets.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | from dataclasses import dataclass, field 3 | from .containers import RowContainer 4 | 5 | 6 | @dataclass 7 | class BlankChart: 8 | """A interface class for tests only.""" 9 | width: str = '900px' 10 | height: str = '500px' 11 | renderer: str = 'canvas' 12 | page_title: str = 'Blank Chart' 13 | theme: str = 'white' 14 | chart_id: str = 'dje_blank' 15 | options: dict = field(default_factory=dict) 16 | js_dependencies: list = field(default_factory=lambda: ['echarts']) # Use Simplified format. 17 | geojson: dict = None # added by django-echarts 18 | 19 | def __post_init__(self): 20 | self._is_geo_chart = False 21 | 22 | def dump_options(self) -> str: 23 | return "{}" 24 | 25 | 26 | class NamedCharts(RowContainer): 27 | """ 28 | A data structure class containing multiple named charts. 29 | is_combine: if True, the collection will not contains this chart. 30 | 31 | 32 | """ 33 | 34 | def __init__(self, page_title: str = 'EChart', col_chart_num: int = 0, is_combine: bool = False): 35 | super().__init__() 36 | self.page_title = page_title 37 | self._col_chart_num = col_chart_num 38 | self.is_combine = is_combine 39 | self.has_ref = is_combine 40 | 41 | def auto_layout(self): 42 | if self._col_chart_num != 0: 43 | span = int(12 / self._col_chart_num) 44 | self.set_spans(span) 45 | else: 46 | super().auto_layout() 47 | 48 | def add_chart(self, chart_obj, name=None): 49 | warnings.warn('This method is deprecated. Use NamedChart.add_widget instead.', DeprecationWarning, stacklevel=2) 50 | self.add_widget(chart_obj, name=name) 51 | return self 52 | 53 | def add(self, achart_or_charts): 54 | warnings.warn('This method is deprecated. Use NamedChart.add_widget instead.', DeprecationWarning, stacklevel=2) 55 | if not isinstance(achart_or_charts, (list, tuple, set)): 56 | achart_or_charts = achart_or_charts, # Make it a sequence 57 | for c in achart_or_charts: 58 | self.add_widget(c) 59 | return self 60 | -------------------------------------------------------------------------------- /django_echarts/entities/containers.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | from itertools import zip_longest 3 | from typing import Dict, Generator, Tuple, Any, List, Union 4 | 5 | 6 | class LayoutCfg: 7 | __slots__ = ['ww', 'wh', 'span', 'offset'] 8 | 9 | def __init__(self, ww: str = '', wh: str = '', span: int = 0, offset: int = 0): 10 | self.ww = ww 11 | self.wh = wh 12 | self.span = span 13 | self.offset = offset 14 | 15 | def __str__(self): 16 | return f'' 17 | 18 | 19 | COL_TOTAL = 12 20 | 21 | 22 | class ContainerBase: 23 | """A container containing some widgets.""" 24 | 25 | def __init__(self, *args, **kwargs): 26 | self._widgets = OrderedDict() 27 | self._layouts = {} # type: Dict[str,LayoutCfg] 28 | self.div_class = kwargs.get('div_class', '') 29 | self._layout_str = '' 30 | 31 | def add_widget(self, widget, name: str = None, width: str = "", height: str = "", span: int = 0, 32 | first: bool = False): 33 | """Add a widget in this container widget. 34 | If first set to True, insert at the beginning position in list.""" 35 | name = name or 'c{}'.format(len(self._widgets)) 36 | self._widgets[name] = widget 37 | lc = LayoutCfg(ww=width, wh=height, span=span) 38 | self._layouts[name] = lc 39 | if first: 40 | self._widgets.move_to_end(name, False) 41 | return self 42 | 43 | def auto_layout(self): 44 | """Auto compute col spans for each widget.""" 45 | pass 46 | 47 | def __iter__(self): 48 | """Iter each widget.""" 49 | for chart in self._widgets.values(): 50 | yield chart 51 | 52 | def __len__(self): 53 | return len(self._widgets) 54 | 55 | def __getitem__(self, item): 56 | if isinstance(item, int): 57 | # c[1], Just compatible with Page 58 | return list(self._widgets.values())[item] 59 | return self._widgets[item] 60 | 61 | def iter_layout(self) -> Generator[Tuple[Any, LayoutCfg], None, None]: 62 | """Iter each widget and its layout config.""" 63 | self.auto_layout() 64 | for name, widget in self._widgets.items(): 65 | yield widget, self._layouts.get(name, LayoutCfg()) 66 | 67 | 68 | class Container(ContainerBase): 69 | """General container.""" 70 | 71 | def __init__(self, *args, div_class: str = '', **kwargs): 72 | super().__init__(div_class=div_class, *args, **kwargs) 73 | 74 | 75 | class RowContainer(ContainerBase): 76 | """A row container.""" 77 | 78 | def set_spans(self, spans: Union[int, List[int]]): 79 | """Set span value for each widget. 80 | This should be called after all widgets is add.""" 81 | if isinstance(spans, int): 82 | if spans == 0: 83 | span_list = [int(12 / len(self._widgets))] * len(self._layouts) 84 | else: 85 | span_list = [spans] * len(self._layouts) 86 | else: 87 | span_list = spans 88 | for lc, span in zip_longest(self._layouts.values(), span_list): 89 | if lc is None: 90 | continue 91 | lc.span = span or COL_TOTAL 92 | 93 | def auto_layout(self): 94 | """Auto compute col spans for each widget.""" 95 | item_num = len([lc for lc in self._layouts.values() if lc.span == 0]) 96 | if item_num == 0: # Each widget has its own span. 97 | return 98 | total_span = COL_TOTAL - sum([lc.span for lc in self._layouts.values() if lc.span]) 99 | col_span = int(total_span / item_num) 100 | for lc in self._layouts.values(): 101 | if lc.span == 0: 102 | lc.span = col_span 103 | lc.ww = '100%' 104 | 105 | def get_spans(self) -> Tuple[int]: 106 | return tuple([lc.span for lc in self._layouts.values()]) 107 | -------------------------------------------------------------------------------- /django_echarts/entities/layouts.py: -------------------------------------------------------------------------------- 1 | import re 2 | from functools import singledispatch 3 | from typing import List, Union 4 | 5 | __all__ = ['LayoutOpts', 'TYPE_LAYOUT_OPTS', 'any2layout'] 6 | 7 | _defaults = {'l': 8, 'r': 8, 's': 8, 't': 6, 'b': 6, 'f': 12} 8 | 9 | _rm = re.compile(r'([lrtbfsa])(([1-9]|(1[12]))?)') 10 | 11 | 12 | class LayoutOpts: 13 | """Layout for user defined. 14 | """ 15 | __slots__ = ['pos', 'spans', 'start'] 16 | 17 | # l=left,r=right,s=stripped,t=top,b=bottom,f=full 18 | _defaults = {'l': 8, 'r': 8, 's': 8, 't': 6, 'b': 6, 'f': 12} 19 | 20 | _rm = re.compile(r'([lrtbfsa])(([1-9]|(1[12]))?)') 21 | 22 | def __init__(self, pos: str = 'r', spans: List[int] = None): 23 | self.pos = pos 24 | self.spans = spans or [] 25 | self.start = pos in 'rb' 26 | 27 | def stripped_layout(self) -> 'LayoutOpts': 28 | if self.pos == 'r': 29 | return LayoutOpts(pos='l', spans=self.spans) 30 | elif self.pos == 'l': 31 | return LayoutOpts(pos='r', spans=self.spans) 32 | else: 33 | return self 34 | 35 | def __str__(self): 36 | return f'' 37 | 38 | 39 | TYPE_LAYOUT_OPTS = Union[int, List[int], str] 40 | 41 | 42 | @singledispatch 43 | def any2layout(obj) -> LayoutOpts: 44 | raise TypeError('Can not parse LayOpts.') 45 | 46 | 47 | @any2layout.register(LayoutOpts) 48 | def _(obj) -> LayoutOpts: 49 | return obj 50 | 51 | 52 | @any2layout.register(int) 53 | def _(obj) -> LayoutOpts: 54 | return LayoutOpts(spans=[obj]) 55 | 56 | 57 | @any2layout.register(list) 58 | def _(obj) -> LayoutOpts: 59 | return LayoutOpts(spans=obj) 60 | 61 | 62 | @any2layout.register(str) 63 | def _(obj) -> LayoutOpts: 64 | m = _rm.match(obj) 65 | if m: 66 | pos, cols = m.group(1), m.group(2) 67 | if cols is None or cols == '': 68 | cols = _defaults.get(pos, 8) 69 | else: 70 | cols = int(cols) 71 | return LayoutOpts(pos, [cols]) 72 | else: 73 | raise ValueError(f'This layout can not be parsed: {obj}') 74 | -------------------------------------------------------------------------------- /django_echarts/entities/pages.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | from abc import abstractmethod 3 | from typing import List, Union, Tuple, Any, Optional 4 | import warnings 5 | 6 | from .uri import EntityURI 7 | from .articles import ChartInfo 8 | from .containers import RowContainer, Container 9 | from .layouts import LayoutOpts, TYPE_LAYOUT_OPTS, any2layout 10 | 11 | 12 | class WidgetGetterMixin: 13 | 14 | @abstractmethod 15 | def get_chart_and_info(self, name: str) -> Tuple[Optional[Any], bool, Optional[ChartInfo]]: 16 | """Return a pycharts chart object.""" 17 | warnings.warn('This method is deprecated, use get_chart_and_info_by_uri instead.', DeprecationWarning, 18 | stacklevel=2) 19 | pass 20 | 21 | @abstractmethod 22 | def get_html_widget(self, name: str) -> Any: 23 | """Return a html widget object.""" 24 | warnings.warn('This method is deprecated, use get_widget_by_uri instead.', DeprecationWarning, stacklevel=2) 25 | pass 26 | 27 | @abstractmethod 28 | def get_widget_by_name(self, name: str) -> Any: 29 | warnings.warn('This method is deprecated, use get_widget_by_uri instead.', DeprecationWarning, stacklevel=2) 30 | pass 31 | 32 | @abstractmethod 33 | def get_chart_and_info_by_uri(self, uri: EntityURI) -> Tuple[Any, bool, Optional[ChartInfo]]: 34 | pass 35 | 36 | @abstractmethod 37 | def get_widget_by_uri(self, uri: EntityURI): 38 | pass 39 | 40 | 41 | class WidgetCollection(Container): 42 | """A row-list-container""" 43 | widget_type = 'Collection' 44 | 45 | def __init__(self, name: str, title: str = None, layout: TYPE_LAYOUT_OPTS = 'a'): 46 | super().__init__() 47 | self.name = name 48 | self.title = title 49 | self._user_defined_layout = any2layout(layout) 50 | self._ref_config_list = [] # type: List 51 | self._row_no = 0 52 | 53 | def start_(self): 54 | self._widgets = OrderedDict() 55 | self._row_no = 0 56 | return self 57 | 58 | def add_chart_widget(self, chart_name: str, layout: TYPE_LAYOUT_OPTS = 'l8'): 59 | self._ref_config_list.append([True, layout, EntityURI.from_str(chart_name, catalog='chart')]) 60 | return self 61 | 62 | def add_html_widget(self, widget_names: List, layout: TYPE_LAYOUT_OPTS = 0): 63 | widget_uri_list = [EntityURI.from_str(name, catalog='widget') for name in widget_names] 64 | self._ref_config_list.append([False, layout, *widget_uri_list]) 65 | 66 | def auto_mount(self, widget_container: WidgetGetterMixin): 67 | self.start_() 68 | for is_chart, layout_str, *uri_list in self._ref_config_list: 69 | if is_chart: 70 | chart_uri = uri_list[0] 71 | chart_obj, _, info = widget_container.get_chart_and_info_by_uri(chart_uri) 72 | self.pack_chart_widget(chart_obj, info, row_no=self._row_no) 73 | else: 74 | widget_list = [widget_container.get_widget_by_uri(uri) for uri in uri_list] 75 | self.pack_html_widget(widget_list) 76 | 77 | def pack_chart_widget(self, chart_obj, info: ChartInfo, ignore_ref: bool = True, layout: str = 'l8', 78 | row_no: int = 0): 79 | r_layout = self.compute_layout(any2layout(layout)) 80 | if isinstance(chart_obj, RowContainer): 81 | if getattr(chart_obj, 'has_ref', False) and ignore_ref: 82 | return 83 | row_widget = chart_obj 84 | row_widget.set_spans(0) 85 | row_widget.add_widget(info, first=r_layout.start, span=12) 86 | else: 87 | # pyecharts.charts.Base 88 | row_widget = RowContainer() 89 | row_widget.add_widget(chart_obj, span=r_layout.spans[0]) 90 | row_widget.add_widget(info, first=r_layout.start) 91 | self.add_widget(row_widget) 92 | self._row_no += 1 93 | 94 | def pack_html_widget(self, widget_list: List, spans: Union[int, List[int]] = 0, row_no: int = 0): 95 | row_widget = RowContainer() 96 | for widget in widget_list: 97 | row_widget.add_widget(widget) 98 | if spans != 0: 99 | row_widget.set_spans(spans) 100 | self.add_widget(row_widget) 101 | self._row_no += 1 102 | 103 | def compute_layout(self, row_layout: LayoutOpts): 104 | if self._user_defined_layout.pos == 'a': 105 | return row_layout 106 | elif self._user_defined_layout.pos == 's': 107 | if self._row_no % 2 == 1: 108 | return row_layout.stripped_layout() 109 | else: 110 | return row_layout 111 | else: 112 | return row_layout 113 | -------------------------------------------------------------------------------- /django_echarts/entities/styles.py: -------------------------------------------------------------------------------- 1 | """ 2 | A shortcut method for builtin theme styles. 3 | """ 4 | 5 | 6 | def bootstrap_table_class(border=False, borderless=False, striped=False, size=None): 7 | class_list = ['table', 'table-responsive'] 8 | if border: 9 | class_list.append('table-bordered') 10 | if borderless: 11 | class_list.append('table-borderless') 12 | if striped: 13 | class_list.append('table-striped') 14 | if size: 15 | class_list.append(f'table-{size}') 16 | return ' '.join(class_list) 17 | 18 | 19 | def material_table_class(striped=False, center=False): 20 | class_list = ['responsive-table', ] 21 | if striped: 22 | class_list.append('striped') 23 | if center: 24 | class_list.append('centered') 25 | return ' '.join(class_list) 26 | -------------------------------------------------------------------------------- /django_echarts/entities/uri.py: -------------------------------------------------------------------------------- 1 | from typing import List, Union, Generator 2 | import itertools 3 | 4 | 5 | class EntityURI: 6 | __empty = None 7 | 8 | CATALOG_CHART = 'chart' 9 | CATALOG_INFO = 'info' 10 | CATALOG_WIDGET = 'widget' 11 | CATALOG_LAYOUT = 'layout' 12 | CATALOG_COLLECTION = 'collection' 13 | 14 | CATALOG_VIEW = 'view' 15 | # TODO description attr 16 | 17 | __slots__ = ['catalog', 'name', 'params', '_params_path'] 18 | 19 | def __init__(self, catalog: str, name: str, params: dict = None): 20 | self.catalog = catalog 21 | self.name = name 22 | self.params = params or {} 23 | _items = [] 24 | for k, v in self.params.items(): 25 | _items.extend([k, str(v)]) 26 | self._params_path = '/'.join(_items) 27 | 28 | @property 29 | def params_path(self): 30 | return self._params_path 31 | 32 | def __str__(self): 33 | if len(self.params): 34 | return f'{self.catalog}:{self.name}/{self.params_path}' 35 | else: 36 | return f'{self.catalog}:{self.name}' 37 | 38 | __repr__ = __str__ 39 | 40 | def is_empty(self): 41 | return not self.catalog or self.catalog == 'empty' 42 | 43 | @classmethod 44 | def empty(cls): 45 | if cls.__empty is None: 46 | cls.__empty = cls('empty', '') 47 | return cls.__empty 48 | 49 | @classmethod 50 | def from_params_path(cls, catalog: str, name: str, params_path: str, param_names: list = None): 51 | """Parse URI param string to param dict. 52 | param_names is required when param value contains '/' character. 53 | """ 54 | params = {} 55 | if not params_path: 56 | return cls(catalog, name, params) 57 | kw = params_path.split('/') 58 | if param_names: 59 | name2pos = [] 60 | for pn in param_names: 61 | try: 62 | pos = kw.index(pn) 63 | name2pos.append((pn, pos)) 64 | except IndexError: 65 | pass 66 | name2pos.sort(key=lambda _item: _item[1]) 67 | for i, item in enumerate(name2pos): 68 | pname, npos = item 69 | start_pos = npos + 1 70 | if i == len(name2pos) - 1: 71 | end_pos = len(kw) 72 | else: 73 | end_pos = name2pos[i + 1][1] 74 | if end_pos - start_pos == 1: 75 | value = kw[start_pos] 76 | else: 77 | value = '/'.join(kw[start_pos:end_pos]) 78 | params[pname] = value 79 | else: 80 | for i in range(0, len(kw), 2): 81 | params[kw[i]] = kw[i + 1] 82 | return cls(catalog, name, params) 83 | 84 | @classmethod 85 | def from_str(cls, s: str, catalog: str = None, param_names: list = None): 86 | pos1 = s.find(':') 87 | if pos1 == -1: 88 | _catalog, st = catalog, s 89 | else: 90 | _catalog, st = s[:pos1], s[pos1 + 1:] 91 | if len(st) == 0: 92 | st = _catalog 93 | _catalog = catalog 94 | pos = st.find('/') 95 | if pos == -1: 96 | name, params_path = st, '' 97 | else: 98 | name, params_path = st[:pos], st[pos + 1:] 99 | return cls.from_params_path(_catalog, name, params_path, param_names) 100 | 101 | 102 | USER_PARAMS_CONFIGS_TYPE = Union[List[dict], dict] 103 | 104 | 105 | def parse_params_choices(config: USER_PARAMS_CONFIGS_TYPE) -> Generator[dict, None, None]: 106 | """ 107 | [{'year':2021, 'month':1},{'year':2021, 'month':2},] 108 | {'year':[2021,2022], 'month':[1,2]} 109 | """ 110 | if isinstance(config, List): 111 | for item in config: 112 | yield item 113 | else: 114 | names = [] 115 | values_list = [] 116 | for name, values in config.items(): 117 | names.append(name) 118 | values_list.append(values) 119 | for t in itertools.product(*values_list): 120 | item = dict((k, v) for k, v in zip(names, t)) 121 | yield item 122 | 123 | 124 | class ParamsConfig: 125 | __empty__ = None 126 | __slots__ = ['choices'] 127 | 128 | def __init__(self, choices: USER_PARAMS_CONFIGS_TYPE = None): 129 | self.choices = choices or [] 130 | 131 | def __iter__(self): 132 | for param_dic in parse_params_choices(self.choices): 133 | yield param_dic 134 | 135 | @classmethod 136 | def empty(cls): 137 | if ParamsConfig.__empty__ is None: 138 | ParamsConfig.__empty__ = ParamsConfig() 139 | return ParamsConfig.__empty__ 140 | -------------------------------------------------------------------------------- /django_echarts/geojson.py: -------------------------------------------------------------------------------- 1 | """A extension for geojson.""" 2 | import warnings 3 | from collections import namedtuple 4 | 5 | from django_echarts.custom_maps import use_custom_map, custom_map_url 6 | 7 | __all__ = ['use_geojson', 'geojson_url'] 8 | 9 | warnings.warn('The django_echarts.geojson is deprecated.Use django_echarts.custom_maps instead.', DeprecationWarning) 10 | 11 | GeojsonItem = namedtuple('GeojsonItem', 'map_name url') 12 | 13 | 14 | def use_geojson(chart_obj, map_name: str, url: str = None): 15 | """Register and use geojson map for a chart.""" 16 | use_custom_map(chart_obj, map_name, url) 17 | 18 | 19 | def geojson_url(geojson_name: str) -> str: 20 | """Get default url for a geojson file.""" 21 | return custom_map_url(geojson_name) 22 | -------------------------------------------------------------------------------- /django_echarts/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/django-echarts/12de6cf4faab1ecbbc06bd487727cc16791ee556/django_echarts/management/__init__.py -------------------------------------------------------------------------------- /django_echarts/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/django-echarts/12de6cf4faab1ecbbc06bd487727cc16791ee556/django_echarts/management/commands/__init__.py -------------------------------------------------------------------------------- /django_echarts/management/commands/_downloader.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import List 3 | 4 | from django.core.management.base import BaseCommand 5 | 6 | from django_echarts.conf import DJANGO_ECHARTS_SETTINGS 7 | from django_echarts.core.localfiles import DownloaderResource 8 | from django_echarts.renders import get_js_dependencies 9 | from django_echarts.stores.entity_factory import factory 10 | from django_echarts.utils.downloader import download_files 11 | 12 | 13 | class DownloadBaseCommand(BaseCommand): 14 | 15 | def do_action(self, chart_names: List, dep_names: List, theme_name: str, repo_name: str, fake: bool): 16 | 17 | all_resources = [] # type: List[DownloaderResource] 18 | if theme_name: 19 | all_resources += self.resolve_theme(theme_name) 20 | all_dep_names = dep_names or [] 21 | if chart_names: 22 | for cname in chart_names: 23 | all_dep_names += self.resolve_chart(cname) 24 | if all_dep_names: 25 | all_resources += self.resolve_dep(all_dep_names, repo_name) 26 | if fake: 27 | for i, res in enumerate(all_resources): 28 | if os.path.exists(res.local_path): 29 | res.exists = True 30 | msg = self.style.SUCCESS(' Local Path: {}'.format(res.local_path)) 31 | else: 32 | res.exists = False 33 | msg = self.style.WARNING(' Local Path: {}'.format(res.local_path)) 34 | self.stdout.write('[Resource #{:02d}] {}: Catalog: {}'.format(i + 1, res.label, res.catalog)) 35 | self.stdout.write(' Remote Url: {}'.format(res.remote_url)) 36 | self.stdout.write(' Static Url: {}'.format(res.ref_url)) 37 | self.stdout.write(msg) 38 | else: 39 | file_info_list = [(res.remote_url, res.local_path) for res in all_resources if not res.exists] 40 | download_files(file_info_list) 41 | self.stdout.write(self.style.SUCCESS('Task completed!')) 42 | 43 | def resolve_dep(self, dep_names, repo_name) -> List[DownloaderResource]: 44 | manager = DJANGO_ECHARTS_SETTINGS.dependency_manager 45 | return manager.get_download_resources(dep_names, repo_name) 46 | 47 | def resolve_theme(self, theme_name) -> List[DownloaderResource]: 48 | if theme_name: 49 | theme = DJANGO_ECHARTS_SETTINGS.create_theme(theme_name) 50 | else: 51 | theme = DJANGO_ECHARTS_SETTINGS.theme 52 | return DJANGO_ECHARTS_SETTINGS.theme_manger.get_download_resources(theme) 53 | 54 | def resolve_chart(self, chart_name) -> List[str]: 55 | chart_obj = factory.get_chart_widget(chart_name) 56 | if not chart_obj: 57 | self.stdout.write(self.style.WARNING('The chart with name does not exits.')) 58 | return [] 59 | dep_names = get_js_dependencies(chart_obj) 60 | return dep_names 61 | -------------------------------------------------------------------------------- /django_echarts/management/commands/download.py: -------------------------------------------------------------------------------- 1 | from ._downloader import DownloadBaseCommand 2 | 3 | 4 | class Command(DownloadBaseCommand): 5 | help = 'Download one or some dependency files from remote CDN to project staticfile dirs.' 6 | 7 | def add_arguments(self, parser): 8 | parser.add_argument('--chart', '-c', nargs='+', type=str, help='The name of chart.') 9 | parser.add_argument('--dep', '-d', nargs='+', type=str, help='The name of dependency files.') 10 | parser.add_argument('--theme', '-t', help='The name of theme.') 11 | parser.add_argument('--repo', '-r', help='The name of dependency repo.', default='pyecharts') 12 | parser.add_argument('--force', '-f', action='store_true') 13 | 14 | def handle(self, *args, **options): 15 | chart_names = options.get('chart', []) 16 | dep_names = options.get('dep', []) 17 | theme_name = options.get('theme') 18 | repo_name = options.get('repo') 19 | self.do_action(chart_names=chart_names, dep_names=dep_names, theme_name=theme_name, repo_name=repo_name, 20 | fake=False) 21 | -------------------------------------------------------------------------------- /django_echarts/management/commands/info.py: -------------------------------------------------------------------------------- 1 | from ._downloader import DownloadBaseCommand 2 | 3 | 4 | class Command(DownloadBaseCommand): 5 | help = 'Show one or some dependency files.' 6 | 7 | def add_arguments(self, parser): 8 | parser.add_argument('--chart', '-c', nargs='+', type=str, help='The name of chart.') 9 | parser.add_argument('--dep', '-d', nargs='+', type=str, help='The name of dependency files.') 10 | parser.add_argument('--theme', '-t', help='The name of theme.') 11 | parser.add_argument('--repo', '-r', help='The name of dependency repo.', default='pyecharts') 12 | 13 | def handle(self, *args, **options): 14 | chart_names = options.get('chart', []) 15 | dep_names = options.get('dep', []) 16 | theme_name = options.get('theme') 17 | repo_name = options.get('repo') 18 | self.do_action(chart_names=chart_names, dep_names=dep_names, theme_name=theme_name, repo_name=repo_name, 19 | fake=True) 20 | -------------------------------------------------------------------------------- /django_echarts/management/commands/startsite.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | from datetime import date 3 | 4 | from django.core.management.base import BaseCommand 5 | from django.template.loader import render_to_string 6 | from django_echarts import __version__ 7 | 8 | 9 | class Command(BaseCommand): 10 | help = 'Auto generate site_views.py file.' 11 | 12 | def add_arguments(self, parser): 13 | parser.add_argument('output', type=str, help='The output file.') 14 | def_year = date.today().year 15 | parser.add_argument('--site-title', '-t', type=str, help='The title of site.', default='Echarts Demo') 16 | parser.add_argument('--empty', '-e', action='store_true') 17 | parser.add_argument('--start-year', '-y', type=int, help='The start year.', default=def_year) 18 | parser.add_argument('--powered-by', '-p', type=str, help='The principal of copyright.', 19 | default='Django-Echarts') 20 | parser.add_argument('--force', '-f', action='store_true') 21 | 22 | def handle(self, *args, **options): 23 | output = options.get('output') 24 | if output[-3:] != '.py': 25 | output += '.py' 26 | force = options.get('force', False) 27 | if not force and os.path.exists(output): 28 | self.stdout.write(self.style.ERROR(f'The file {output} exists! Add -f to overwrite it.')) 29 | return 30 | site_title = options.get('site_title') 31 | start_year = options.get('start_year') 32 | powered_by = options.get('powered_by') 33 | empty_chart = options.get('empty', False) 34 | self.generate_code_spinet(output, site_title, start_year, powered_by, empty_chart) 35 | 36 | def generate_code_spinet(self, output, site_title, start_year, powered_by, empty_chart): 37 | context = { 38 | 'site_title': site_title, 39 | 'start_year': start_year, 40 | 'powered_by': powered_by, 41 | 'use_chart': not empty_chart, 42 | 'version': __version__, 43 | } 44 | s = render_to_string('snippets/first_views.py.tpl', context=context) 45 | with open(output, 'w') as f: 46 | f.write(s) 47 | self.stdout.write(self.style.SUCCESS(f'File {output} generated success!')) 48 | -------------------------------------------------------------------------------- /django_echarts/management/commands/starttpl.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | from django.apps import apps 5 | from django.conf import settings 6 | from django.core.management.base import BaseCommand 7 | from django.template import engines 8 | from django_echarts.conf import DJANGO_ECHARTS_SETTINGS 9 | 10 | 11 | def concat_if(s: str, fix: str): 12 | if s[-len(fix):] != fix: 13 | return s + fix 14 | else: 15 | return s 16 | 17 | 18 | def get_theme_template_dir(app_name, *args) -> str: 19 | for app_config in apps.get_app_configs(): 20 | if app_name in app_config.name: 21 | return os.path.join(app_config.path, 'templates', *args) 22 | 23 | 24 | def get_dest_template_dir(): 25 | django_engine = engines['django'] 26 | if django_engine.dirs: 27 | return django_engine.dirs[0] 28 | else: 29 | return os.path.join(str(settings.BASE_DIR), 'templates') 30 | 31 | 32 | class Command(BaseCommand): 33 | help = 'Copy the builtin template files to your project templates.' 34 | 35 | def add_arguments(self, parser): 36 | """ 37 | Examples: 38 | starttpl 39 | starttpl bootstrap5 # Print all template_names in this theme. 40 | starttpl bootstrap5 --all # Copy all template_names in this theme. 41 | starttpl -t bootstrap5 -n base # Copy templates/base.html 42 | starttpl base 43 | starttpl blank -o my_page 44 | starttpl list all 45 | """ 46 | parser.add_argument('--theme', type=str, help='The name of theme.', default='bootstrap5', 47 | choices=['bootstrap3', 'bootstrap5', 'material']) 48 | parser.add_argument('tpl_name', type=str, nargs='+', help='The name of template file.', ) 49 | parser.add_argument('--output', '-o', type=str, help='The output filename') 50 | parser.add_argument('--force', '-f', action='store_true', help='Whether to copy if the file exists.') 51 | 52 | def handle(self, *args, **options): 53 | theme_name = options.get('theme') 54 | if not theme_name: 55 | theme = DJANGO_ECHARTS_SETTINGS.theme 56 | else: 57 | theme = DJANGO_ECHARTS_SETTINGS.create_theme(theme_name) 58 | theme_name = theme.name 59 | template_names = options.get('tpl_name', []) 60 | if template_names: 61 | show_action = False 62 | else: 63 | template_names = self.get_all_template_names(theme_name) 64 | show_action = True 65 | if show_action: 66 | self.stdout.write(f'The template names of Theme [{theme_name}]:') 67 | for name in template_names: 68 | self.stdout.write(f'\t{name}') 69 | self.stdout.write('\n Start to custom a template: python manage.py starttpl -n blank -o my_page') 70 | else: 71 | template_name = template_names[0] 72 | output = options.get('output') 73 | force_action = options.get('force') 74 | self.copy_template_files(theme_name, template_name, output, force_action) 75 | 76 | def get_all_template_names(self, theme_name): 77 | theme_dir = get_theme_template_dir(theme_name) 78 | template_names = [] 79 | for root, dirs, files in os.walk(theme_dir): 80 | for f in files: 81 | template_names.append(os.path.join(root, f)[len(theme_dir) + 1:]) 82 | return template_names 83 | 84 | def copy_template_files(self, theme_name, template_name, output, force_action): 85 | template_name = concat_if(template_name, '.html') 86 | if output: 87 | output = concat_if(output, '.html') 88 | else: 89 | output = template_name 90 | from_path = get_theme_template_dir(theme_name, template_name) 91 | # From path 92 | if not os.path.exists(from_path): 93 | self.stdout.write(self.style.WARNING(f' {template_name}, skipped!')) 94 | pro_template_dir = get_dest_template_dir() 95 | to_path = os.path.join(pro_template_dir, output) 96 | if os.path.exists(to_path) and not force_action: 97 | self.stdout.write(self.style.WARNING(f' {output}, Exists! Add -f option to override write.')) 98 | else: 99 | try: 100 | os.makedirs(os.path.dirname(to_path), exist_ok=True) 101 | shutil.copy(from_path, to_path) 102 | self.stdout.write(self.style.SUCCESS(f' {output}, Success!')) 103 | except BaseException as e: 104 | self.stdout.write(self.style.ERROR(f' {output}, Fail! {str(e)}')) 105 | -------------------------------------------------------------------------------- /django_echarts/renders/__init__.py: -------------------------------------------------------------------------------- 1 | from .html import render_widget 2 | from .javascript import get_js_dependencies, flat_chart 3 | -------------------------------------------------------------------------------- /django_echarts/renders/html.py: -------------------------------------------------------------------------------- 1 | from functools import singledispatch 2 | from typing import Union 3 | 4 | import htmlgenerator as hg 5 | from borax.htmls import HTMLString, html_tag 6 | from borax.strings import camel2snake 7 | from django.template import engines 8 | from django.template.loader import get_template 9 | from django.utils.safestring import SafeString 10 | from prettytable import PrettyTable 11 | from pyecharts.charts.base import Base 12 | from pyecharts.components.table import Table 13 | 14 | from django_echarts.entities import ( 15 | ValuesPanel, LinkItem, Menu, NamedCharts, DwString, RowContainer, Container, HTMLBase, BlankChart 16 | ) 17 | 18 | 19 | def _to_css_length(val): 20 | if isinstance(val, (int, float)): 21 | return '{}px'.format(val) 22 | else: 23 | return val 24 | 25 | 26 | @singledispatch 27 | def render_widget(widget, **kwargs) -> SafeString: 28 | if hasattr(widget, '__html__') and callable(widget.__html__): 29 | return widget.__html__() 30 | # raise WidgetNotRegisteredError(widget) 31 | return SafeString(f'
Unknown widget type:{widget.__class__.__name__}
') 32 | 33 | 34 | @render_widget.register(type(None)) 35 | def render_none(widget, **kwargs) -> SafeString: 36 | return SafeString('') 37 | 38 | 39 | @render_widget.register(SafeString) 40 | @render_widget.register(HTMLString) 41 | def render_html(widget, **kwargs) -> SafeString: 42 | return widget 43 | 44 | 45 | @render_widget.register(Base) 46 | @render_widget.register(BlankChart) 47 | def render_chart(widget, **kwargs) -> SafeString: 48 | width = kwargs.get('width') or widget.width 49 | height = kwargs.get('height') or widget.height 50 | html = '
'.format( 51 | chart_id=widget.chart_id, 52 | width=_to_css_length(width), 53 | height=_to_css_length(height) 54 | ) 55 | return SafeString(html) 56 | 57 | 58 | @render_widget.register(Container) 59 | @render_widget.register(HTMLBase) 60 | @render_widget.register(NamedCharts) 61 | @render_widget.register(RowContainer) 62 | @render_widget.register(ValuesPanel) 63 | def render_with_tpl(widget, **kwargs) -> SafeString: 64 | if hasattr(widget, '__html__') and callable(widget.__html__): 65 | return widget.__html__() 66 | if 'tpl' in kwargs: 67 | tpl_name = kwargs['tpl'] 68 | else: 69 | widget_name = camel2snake(widget.__class__.__name__) 70 | if widget_name in ('values_panel', 'named_charts'): 71 | widget_name = 'row_container' 72 | elif widget_name == 'widget_collection': 73 | widget_name = 'container' 74 | tpl_name = 'widgets/{}.html'.format(widget_name) 75 | tpl = get_template(tpl_name) 76 | return SafeString(tpl.render({'widget': widget})) 77 | 78 | 79 | @render_widget.register(Table) 80 | @render_widget.register(PrettyTable) 81 | def render_table(widget, **kwargs) -> SafeString: 82 | if isinstance(widget, Table): 83 | html_content = widget.html_content 84 | else: 85 | html_content = widget.get_html_string(**kwargs) 86 | html_content = f'
{html_content}
' 87 | return SafeString(html_content) 88 | 89 | 90 | @render_widget.register(LinkItem) 91 | @render_widget.register(Menu) 92 | def render_link(widget, **kwargs) -> Union[SafeString, HTMLString]: 93 | context = kwargs.get('context') 94 | class_ = kwargs.get('class_') 95 | params = {'href': widget.url or 'javascript:;'} 96 | if isinstance(widget.text, DwString): 97 | django_engine = engines['django'] 98 | template_obj = django_engine.from_string(widget.text) 99 | fields = ['request', ] 100 | context_dic = {} 101 | for f in fields: 102 | if f in context: 103 | context_dic[f] = context[f] 104 | params['content'] = template_obj.render(context_dic) 105 | else: 106 | params['content'] = widget.text 107 | if class_: 108 | params['class_'] = class_ 109 | if isinstance(widget, LinkItem) and widget.new_page: 110 | params['target'] = '_blank' 111 | return html_tag('a', **params) 112 | 113 | 114 | @render_widget.register(hg.BaseElement) 115 | def render_library_html(widget: hg.BaseElement, **kwargs): 116 | context = kwargs.get('context', {}) 117 | return hg.render(widget, context) 118 | -------------------------------------------------------------------------------- /django_echarts/renders/javascript.py: -------------------------------------------------------------------------------- 1 | from functools import singledispatch 2 | 3 | from django_echarts.entities import ( 4 | NamedCharts, ValuesPanel, RowContainer, Container, WidgetCollection, HTMLBase, BlankChart 5 | ) 6 | from prettytable import PrettyTable 7 | from pyecharts.charts.base import Base 8 | from pyecharts.components.table import Table 9 | from pyecharts.globals import ThemeType 10 | from htmlgenerator.base import BaseElement 11 | 12 | __all__ = ['flat_chart', 'get_js_dependencies'] 13 | 14 | 15 | @singledispatch 16 | def flat_chart(widget): 17 | """Get chart object list from a widget.""" 18 | raise TypeError(f'Can not flat widget type: {widget.__class__.__name__}') 19 | 20 | 21 | @flat_chart.register(Base) 22 | @flat_chart.register(BlankChart) 23 | def flat_base(widget: Base): 24 | return [widget] 25 | 26 | 27 | @flat_chart.register(type(None)) 28 | @flat_chart.register(PrettyTable) 29 | @flat_chart.register(Table) 30 | @flat_chart.register(HTMLBase) 31 | @flat_chart.register(BaseElement) 32 | def flat_not_chart(widget): 33 | return [] 34 | 35 | 36 | @flat_chart.register(NamedCharts) 37 | @flat_chart.register(ValuesPanel) 38 | @flat_chart.register(Container) 39 | @flat_chart.register(RowContainer) 40 | @flat_chart.register(WidgetCollection) 41 | @flat_chart.register(tuple) 42 | @flat_chart.register(list) 43 | def flat_named_charts(widget): 44 | chart_list = [] 45 | for chart in widget: 46 | chart_list.extend(flat_chart(chart)) 47 | return chart_list 48 | 49 | 50 | _ECHARTS_LIB_NAMES = [ 51 | 'echarts.common', 'echarts.common.min', 52 | 'echarts', 'echarts.min', 'echartsgl', 'echarts-gl', 'echarts-gl.min', 53 | 'echarts.simple', 'echarts.simple.min', 54 | 'extension/bmap', 'extension/bmap.min', 55 | 'extension/dataTool', 'extension/dataTool.min' 56 | ] 57 | 58 | 59 | def get_js_dependencies(widget, global_theme: str = None): 60 | dep_list = [] 61 | widget_list = [] 62 | if isinstance(widget, (list, tuple)): 63 | for w in widget: 64 | if isinstance(w, str): 65 | dep_list.append(w) 66 | else: 67 | widget_list.append(w) 68 | elif isinstance(widget, str): 69 | dep_list.append(widget) 70 | else: 71 | widget_list.append(widget) 72 | chart_list = flat_chart(widget_list) 73 | 74 | def _deps(_chart): 75 | if isinstance(_chart.js_dependencies, list): 76 | return _chart.js_dependencies 77 | if hasattr(_chart.js_dependencies, 'items'): 78 | return list(_chart.js_dependencies.items) # pyecharts.commons.utils.OrderedSet 79 | raise ValueError('Can not parse js_dependencies.') 80 | 81 | front_items = [] 82 | for chart in chart_list: 83 | _dep_list = _deps(chart) 84 | 85 | for dep in _dep_list: 86 | # exclude custom map. 87 | if dep.endswith('.geojson') or dep.endswith('.json') or dep.endswith('.svg'): 88 | continue 89 | if dep not in dep_list and dep not in front_items: 90 | if dep in _ECHARTS_LIB_NAMES: 91 | front_items.append(dep) 92 | else: 93 | dep_list.append(dep) 94 | if hasattr(chart, 'theme'): 95 | theme_dep = global_theme or chart.theme 96 | if theme_dep not in ThemeType.BUILTIN_THEMES and theme_dep not in dep_list: 97 | dep_list.append(theme_dep) 98 | front_items.sort(key=lambda x: _ECHARTS_LIB_NAMES.index(x)) 99 | return front_items + dep_list 100 | -------------------------------------------------------------------------------- /django_echarts/site_exts.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse_lazy 2 | from django_echarts.entities.uri import EntityURI 3 | from typing import Union 4 | 5 | 6 | def reverse_chart_url(uri_or_name: Union[EntityURI, str], params_dic: dict = None): 7 | """ 8 | reverse url of a single chart view in DJESite. 9 | The following three signatures are supported. 10 | 11 | reverse_chart_url(uri:EntityURI) 12 | reverse_chart_url(name:str) 13 | reverse_chart_url(name:str, params_dic:dict) 14 | """ 15 | if isinstance(uri_or_name, EntityURI): 16 | uri = uri_or_name 17 | else: 18 | uri = EntityURI('chart', uri_or_name, params_dic) 19 | if len(uri.params): 20 | return reverse_lazy('dje_chart_single', args=(uri.name, uri.params_path)) 21 | else: 22 | return reverse_lazy('dje_chart_single', args=(uri.name,)) 23 | -------------------------------------------------------------------------------- /django_echarts/starter/__init__.py: -------------------------------------------------------------------------------- 1 | from .optstools import SiteOpts, SiteOptsForm 2 | from .sites import DJESite, DJESiteBackendView 3 | -------------------------------------------------------------------------------- /django_echarts/starter/optstools.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from typing import Optional, List 3 | 4 | from django import forms 5 | from django_echarts.conf import DJANGO_ECHARTS_SETTINGS 6 | from typing_extensions import Literal 7 | 8 | 9 | @dataclass 10 | class SiteOpts: 11 | """The opts for DJESite.""" 12 | list_layout: Literal['grid', 'list'] = 'grid' 13 | paginate_by: Optional[int] = 0 14 | nav_top_fixed: bool = False 15 | detail_tags_position: Literal['none', 'top', 'bottom'] = 'top' 16 | detail_sidebar_shown: bool = True 17 | nav_shown_pages: List = field(default_factory=lambda: ['home']) 18 | 19 | 20 | page_num_choices = ((0, '不分页'), (5, '5'), (10, '10'), (15, '15'), (20, '20')) 21 | bool_choices = ((True, 'Yes'), (False, 'No')) 22 | 23 | 24 | class SiteOptsForm(forms.Form): 25 | nav_top_fixed = forms.TypedChoiceField(label='是否固定顶部导航栏', coerce=lambda x: x == 'True', choices=bool_choices) 26 | list_layout = forms.ChoiceField(label='列表页布局', choices=(('list', '列表布局'), ('grid', '网格布局'))) 27 | paginate_by = forms.TypedChoiceField(label='每页项目数', coerce=int, initial=0, choices=page_num_choices) 28 | theme_palette_name = forms.ChoiceField(label='主题调色') 29 | 30 | def __init__(self, *args, **kwargs): 31 | super().__init__(*args, **kwargs) 32 | choices = [(k, k) for k in DJANGO_ECHARTS_SETTINGS.theme_manger.available_palettes] 33 | self.fields['theme_palette_name'].choices = choices 34 | -------------------------------------------------------------------------------- /django_echarts/starter/params_forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | 4 | class ParamsForm: 5 | name = forms.CharField(label='Name', required=True, max_length=200) 6 | params_dic = forms.JSONField(label='Params(JSON)', max_length=500) 7 | -------------------------------------------------------------------------------- /django_echarts/stores/__init__.py: -------------------------------------------------------------------------------- 1 | from .entity_factory import factory 2 | -------------------------------------------------------------------------------- /django_echarts/templates/snippets/echarts_init_options.tpl: -------------------------------------------------------------------------------- 1 | {% for custom_map_item, chart_list in structured_data %} 2 | {% if custom_map_item.url %} 3 | $.get({ 4 | url: '{{ custom_map_item.url }}', 5 | dataType: '{{ custom_map_item.ajax_data_type }}' 6 | }).done(function(mapData){ 7 | echarts.registerMap("{{ custom_map_item.map_name }}", {{ custom_map_item.param_str }}); 8 | {% endif %} 9 | {% for c in chart_list %} 10 | var chart_{{ c.chart_id }} = echarts.init( 11 | document.getElementById('{{ c.chart_id }}'), '{{ c.dje_echarts_theme }}', {renderer: '{{ c.renderer }}'}); 12 | {% for js in c.js_functions.items %} 13 | {{ js|safe }} 14 | {% endfor %} 15 | var option_{{ c.chart_id }} = {{ c.dump_options|default:'{}'|safe }}; 16 | chart_{{ c.chart_id }}.setOption(option_{{ c.chart_id }}); 17 | {% if c.is_geo_chart %} 18 | var bmap = chart_{{ c.chart_id }}.getModel().getComponent('bmap').getBMap(); 19 | {% if c.bmap_js_functions %} 20 | {% for fn in c.bmap_js_functions.items %} 21 | {{ fn|safe }} 22 | {% endfor %} 23 | {% endif %} 24 | {% endif %} 25 | window.addEventListener('resize', function(){ 26 | chart_{{ c.chart_id }}.resize(); 27 | }); 28 | {% endfor %} 29 | 30 | {% if custom_map_item.url %} 31 | }).fail(function(jqXHR, textStatus, error){ 32 | $("#{{ c.chart_id }}").html("Load custom map fail! Status: " + textStatus); 33 | }); 34 | {% endif %} 35 | {% endfor %} -------------------------------------------------------------------------------- /django_echarts/templates/snippets/first_views.py.tpl: -------------------------------------------------------------------------------- 1 | """ 2 | The file is generated by django-echarts {{ version }}. 3 | Now you can add site urls to the entry urlpatterns in project urls.py. 4 | Example: 5 | urlpatterns = [ 6 | # Other urlpatterns 7 | path('', site_obj.urls), 8 | ] 9 | 10 | """ 11 | from django_echarts.entities import Copyright 12 | from django_echarts.starter.sites import DJESite 13 | {% if use_chart %} 14 | from pyecharts import options as opts 15 | from pyecharts.charts import Bar 16 | {% endif %} 17 | 18 | __all__ = ['site_obj'] 19 | 20 | site_obj = DJESite( 21 | site_title='{{ site_title }}' 22 | ) 23 | 24 | site_obj.add_widgets( 25 | copyright_=Copyright(start_year={{ start_year }}, powered_by='{{ powered_by }}') 26 | ) 27 | 28 | 29 | @site_obj.register_chart 30 | def my_first_chart(): 31 | {% if use_chart %} 32 | bar = Bar().add_xaxis( 33 | ["Nanping", "Sanming", "Longyan", "Ningde", "Zhangzhou", "Fuzhou", "Quanzhou", "Putian", "Xiamen"]).add_yaxis( 34 | 'Area', [26300, 22900, 19050, 13450, 12600, 12150, 11020, 4119, 1576] 35 | ).set_global_opts( 36 | title_opts=opts.TitleOpts(title="The Area of cities In Fujian Province", subtitle="Unit: km3")) 37 | return bar 38 | {% else %} 39 | # Write your pyecharts here. 40 | # This method must return an object of pycharts.charts.Chart 41 | pass 42 | {% endif %} 43 | -------------------------------------------------------------------------------- /django_echarts/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/django-echarts/12de6cf4faab1ecbbc06bd487727cc16791ee556/django_echarts/templatetags/__init__.py -------------------------------------------------------------------------------- /django_echarts/templatetags/echarts.py: -------------------------------------------------------------------------------- 1 | # coding=utf8 2 | """Template tags for django-echarts. 3 | 4 | """ 5 | from collections import defaultdict 6 | from typing import Union 7 | 8 | from borax.utils import chain_getattr 9 | from django import template 10 | from django.template.loader import render_to_string, get_template 11 | from django.utils.safestring import SafeString 12 | from django_echarts.conf import DJANGO_ECHARTS_SETTINGS 13 | from django_echarts.entities import LinkItem, Menu, EntityURI 14 | from django_echarts.renders import render_widget, flat_chart, get_js_dependencies 15 | from django_echarts.utils.burl import burl_kwargs 16 | from django_echarts.site_exts import reverse_chart_url 17 | 18 | register = template.Library() 19 | 20 | 21 | @register.simple_tag(takes_context=True) 22 | def dep_url(context, dep_name: str, repo_name: str = None): 23 | return DJANGO_ECHARTS_SETTINGS.resolve_url(dep_name, repo_name) 24 | 25 | 26 | @register.simple_tag(takes_context=True) 27 | def echarts_container(context, *echarts, **kwargs): 28 | div_list = [] 29 | for chart in echarts: 30 | div_list.append(render_widget(chart, context=context, **kwargs)) 31 | return template.Template('
'.join(div_list)).render(context) 32 | 33 | 34 | @register.simple_tag(takes_context=True) 35 | def echarts_js_dependencies(context, *args): 36 | dependencies = get_js_dependencies(args, global_theme=DJANGO_ECHARTS_SETTINGS.opts.echarts_theme) 37 | links = map(DJANGO_ECHARTS_SETTINGS.resolve_url, dependencies) 38 | 39 | return template.Template( 40 | '
'.join([''.format(link=link) for link in links]) 41 | ).render(context) 42 | 43 | 44 | def build_echarts_initial_fragment(*args): 45 | chart_list = flat_chart(args) 46 | structured_dic = defaultdict(list) # key = CustomMapItem 47 | for chart in chart_list: 48 | if hasattr(chart, '_is_geo_chart'): 49 | chart.is_geo_chart = chart._is_geo_chart 50 | chart.dje_echarts_theme = DJANGO_ECHARTS_SETTINGS.opts.get_echarts_theme(chart.theme) 51 | map_item = getattr(chart, 'custom_map_item', None) 52 | if map_item: 53 | structured_dic[map_item].append(chart) 54 | else: 55 | structured_dic[None].append(chart) 56 | structured_data = list(structured_dic.items()) 57 | context = {'structured_data': structured_data} 58 | return SafeString(render_to_string('snippets/echarts_init_options.tpl', context)) 59 | 60 | 61 | @register.simple_tag(takes_context=True) 62 | def echarts_js_content(context, *echarts): 63 | contents = build_echarts_initial_fragment(*echarts) 64 | return template.Template( 65 | ''.format(contents) 66 | ).render(context) 67 | 68 | 69 | @register.simple_tag(takes_context=True) 70 | def echarts_js_content_wrap(context, *charts): 71 | return template.Template( 72 | build_echarts_initial_fragment(*charts) 73 | ).render(context) 74 | 75 | 76 | @register.simple_tag 77 | def dw_table(table_obj, **kwargs): 78 | return render_widget(table_obj, **kwargs) 79 | 80 | 81 | @register.simple_tag 82 | def dw_values_panel(panel): 83 | return render_widget(panel) 84 | 85 | 86 | @register.simple_tag(takes_context=True) 87 | def dw_widget(context, widget, **kwargs): 88 | return render_widget(widget, context=context, **kwargs) 89 | 90 | 91 | @register.simple_tag 92 | def dw_collection(collection): 93 | tpl = get_template('widgets/collection.html') 94 | return SafeString(tpl.render({'collection': collection})) 95 | 96 | 97 | @register.simple_tag 98 | def theme_js(): 99 | theme = DJANGO_ECHARTS_SETTINGS.theme 100 | html = [] 101 | for link in theme.js_urls: 102 | html.append(f'') 103 | return SafeString(''.join(html)) 104 | 105 | 106 | @register.simple_tag 107 | def theme_css(): 108 | theme = DJANGO_ECHARTS_SETTINGS.theme 109 | html = [] 110 | for link in theme.css_urls: 111 | html.append(f'') 112 | return SafeString(''.join(html)) 113 | 114 | 115 | @register.simple_tag(takes_context=True) 116 | def page_link(context, page_number: int): 117 | url = context['request'].get_full_path() 118 | return burl_kwargs(url, page=page_number) 119 | 120 | 121 | @register.simple_tag(takes_context=True) 122 | def dw_link(context, item: Union[LinkItem, Menu], class_: str = None): 123 | return render_widget(item, context=context, class_=class_) 124 | 125 | 126 | # Site urls 127 | @register.simple_tag(takes_context=True) 128 | def url_single_chart(context, uri_or_name: Union[EntityURI, str], params_dic: dict = None): 129 | # TODO use {% dw uri %} ? 130 | return reverse_chart_url(uri_or_name, params_dic) 131 | -------------------------------------------------------------------------------- /django_echarts/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/django-echarts/12de6cf4faab1ecbbc06bd487727cc16791ee556/django_echarts/utils/__init__.py -------------------------------------------------------------------------------- /django_echarts/utils/burl.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urlencode, parse_qs, urlsplit, urlunsplit 2 | 3 | 4 | class BUrl: 5 | def __init__(self, url): 6 | self._url = url 7 | self._scheme, self._netloc, self._path, self._query_string, self._fragment = urlsplit(url) 8 | self._query_params = parse_qs(self._query_string) 9 | 10 | def replace(self, name, value, only_replace=False): 11 | if name not in self._query_params: 12 | if only_replace: 13 | return self 14 | self._query_params[name] = [] 15 | self._query_params[name] = [value] 16 | return self 17 | 18 | def append(self, name, value): 19 | if name not in self._query_params: 20 | self._query_params[name] = [] 21 | self._query_params[name].append(value) 22 | return self 23 | 24 | def delete(self, name): 25 | if name in self._query_params: 26 | del self._query_params[name] 27 | return self 28 | 29 | @property 30 | def url(self): 31 | new_query_string = urlencode(self._query_params, doseq=True) 32 | return urlunsplit((self._scheme, self._netloc, self._path, new_query_string, self._fragment)) 33 | 34 | 35 | def burl_args(url, *args): 36 | """Add query parameters to url. 37 | burl_args('/foo/', 'p', 1, 'q', 'test') => '/foo/?p=1&q=test' 38 | """ 39 | b = BUrl(url) 40 | _key = '' 41 | for i, value in enumerate(args): 42 | if i % 2 == 0: 43 | _key = value 44 | else: 45 | if _key and value is not None: 46 | b.replace(_key, value) 47 | return b.url 48 | 49 | 50 | def burl_kwargs(url, **kwargs): 51 | b = BUrl(url) 52 | for k, v in kwargs.items(): 53 | b.replace(k, v) 54 | return b.url 55 | -------------------------------------------------------------------------------- /django_echarts/utils/compat.py: -------------------------------------------------------------------------------- 1 | from django import VERSION as django_version 2 | from django.core.paginator import Paginator 3 | 4 | 5 | def compat_get_elided_page_range(paginator: Paginator, number=1, *, on_each_side=3, on_ends=2): 6 | if django_version > (3, 2): 7 | return paginator.get_elided_page_range(number, on_each_side=on_each_side, on_ends=on_ends) 8 | else: 9 | return _get_elided_page_range(paginator, number, on_each_side=on_each_side, on_ends=on_ends) 10 | 11 | 12 | def _get_elided_page_range(paginator: Paginator, number=1, *, on_each_side=3, on_ends=2): 13 | """ 14 | Return a 1-based range of pages with some values elided. 15 | 16 | If the page range is larger than a given size, the whole range is not 17 | provided and a compact form is returned instead, e.g. for a paginator 18 | with 50 pages, if page 43 were the current page, the output, with the 19 | default arguments, would be: 20 | 21 | 1, 2, …, 40, 41, 42, 43, 44, 45, 46, …, 49, 50. 22 | """ 23 | number = paginator.validate_number(number) 24 | 25 | if paginator.num_pages <= (on_each_side + on_ends) * 2: 26 | yield from paginator.page_range 27 | return 28 | 29 | if number > (1 + on_each_side + on_ends) + 1: 30 | yield from range(1, on_ends + 1) 31 | yield paginator.ELLIPSIS 32 | yield from range(number - on_each_side, number + 1) 33 | else: 34 | yield from range(1, number + 1) 35 | 36 | if number < (paginator.num_pages - on_each_side - on_ends) - 1: 37 | yield from range(number + 1, number + on_each_side + 1) 38 | yield paginator.ELLIPSIS 39 | yield from range(paginator.num_pages - on_ends + 1, paginator.num_pages + 1) 40 | else: 41 | yield from range(number + 1, paginator.num_pages + 1) 42 | -------------------------------------------------------------------------------- /django_echarts/utils/downloader.py: -------------------------------------------------------------------------------- 1 | import os 2 | import urllib.request 3 | from concurrent.futures import ThreadPoolExecutor 4 | 5 | 6 | def download_file(remote_url: str, local_path: str): 7 | parent = os.path.dirname(local_path) 8 | if not os.path.exists(parent): 9 | os.mkdir(parent) 10 | filename = local_path.split('/')[-1] 11 | print(f'Download file {filename} start!') 12 | rsp = urllib.request.Request( 13 | remote_url, 14 | headers={ 15 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36' 16 | } 17 | ) 18 | with urllib.request.urlopen(rsp) as response, open(local_path, 'w+b') as out_file: 19 | data = response.read() 20 | out_file.write(data) 21 | 22 | print(f'File {filename} download success!') 23 | 24 | 25 | def download_files(file_info_list: list): 26 | with ThreadPoolExecutor() as executor: 27 | for remote_url, local_path in file_info_list: 28 | executor.submit(download_file, remote_url, local_path) 29 | -------------------------------------------------------------------------------- /django_echarts/utils/param_converters.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | 4 | class DateConverter: 5 | regex = '[0-9]{8}' 6 | 7 | def to_python(self, value): 8 | return datetime.strptime(value, '%Y%m%d').date() 9 | 10 | def to_url(self, value): 11 | return value.strftime('%Y%m%d') 12 | 13 | 14 | DJE_CONVERTERS = { 15 | 'date': DateConverter() 16 | } 17 | -------------------------------------------------------------------------------- /django_echarts/views.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from django.views.generic.base import TemplateView 4 | 5 | __all__ = ['SimplePageView', 'PageTemplateView'] 6 | 7 | 8 | class SimplePageView(TemplateView): 9 | template_name = 'simple/page.html' 10 | 11 | def get_context_data(self, **kwargs): 12 | context = super(SimplePageView, self).get_context_data(**kwargs) 13 | context['container_obj'] = self.get_container_obj() 14 | return context 15 | 16 | def get_container_obj(self) -> Any: 17 | pass 18 | 19 | 20 | PageTemplateView = SimplePageView # Old alias 21 | -------------------------------------------------------------------------------- /docs/about.md: -------------------------------------------------------------------------------- 1 | # 关于(About) 2 | 3 | ## 贡献者(Contributions ) 4 | 5 | | 作者 | 主页 | 6 | | ---- | ---- | 7 | | Samuel.Yan | [https://github.com/kinegratii](https://github.com/kinegratii) | 8 | 9 | ## 资源(Resources) 10 | 11 | 本项目参考或者引用了下列资源: 12 | 13 | - Bootstrap5: [https://v5.bootcss.com/](https://v5.bootcss.com/) 14 | - Bootstrap Icons: [https://icons.getbootstrap.com/](https://icons.getbootstrap.com/) 15 | - Bootstrap3调色css: [ https://bootswatch.com/3/]( https://bootswatch.com/3/) 16 | - Bootstrap5调色css: [ https://bootswatch.com/](https://bootswatch.com/) 17 | - borax: [https://github.com/kinegratii/borax](https://github.com/kinegratii/borax) 18 | - cdnjs: [https://cdnjs.com/](https://cdnjs.com/) 19 | - Codecov: [https://about.codecov.io/](https://about.codecov.io/) 20 | - django: [https://www.djangoproject.com/](https://www.djangoproject.com/) 21 | - Django Best Practices: [https://django-best-practices.readthedocs.io/en/latest/index.html](https://django-best-practices.readthedocs.io/en/latest/index.html) 22 | - domonic: [https://github.com/byteface/domonic](https://github.com/byteface/domonic) 23 | - echarts: [https://echarts.apache.org/zh/index.html](https://echarts.apache.org/zh/index.html) 24 | - Jinja2: [https://jinja2docs.readthedocs.io/en/stable/](https://jinja2docs.readthedocs.io/en/stable/) 25 | - Materialize: [https://materializecss.com/](https://materializecss.com/) 26 | - PrettyTable: [https://github.com/jazzband/prettytable](https://github.com/jazzband/prettytable) 27 | - pyecharts: [https://pyecharts.org/](https://pyecharts.org/) 28 | - pyecharts-asserts: [https://github.com/pyecharts/pyecharts-assets](https://github.com/pyecharts/pyecharts-assets) 29 | - typing_extensions [https://github.com/python/typing_extensions](https://github.com/python/typing_extensions) 30 | - yattag: [https://www.yattag.org/](https://www.yattag.org/) 31 | 32 | 33 | 34 | ## 参与开发(Development) 35 | 36 | 在参与开发之前,您必须确保: 37 | 38 | - 掌握 Python Typing Hints 相关内容 39 | - 熟悉Django框架的有关内容,包括但不限于路由、模板系统、CBV等 40 | - 熟悉pyecharts库的功能,以及核心实现逻辑 41 | 42 | ## 开发方向(Roadmap) 43 | 44 | - 【数据处理】基于 Django Model的数据预处理。 45 | - 【UI框架扩展标准】通过进一步抽象UI框架渲染逻辑,使得能够更好地支持现有的其他主流UI框架。 46 | - 【版本特性兼容】关注Django和pyecharts的重大版本更新,能够兼容更多的版本组合。 47 | 48 | ## 开源协议(License) 49 | 50 | MIT License 51 | 52 | -------------------------------------------------------------------------------- /docs/breaking-change.md: -------------------------------------------------------------------------------- 1 | # 破坏性变更 2 | 3 | ## Deprecated API 4 | 5 | 以下API已经标记为 Deprecated ,并在v1.0正式版本发布时被移除(Removed)。 6 | 7 | * 方法 `django_echarts.entities.ValuesPanel.add` 8 | 9 | 使用方法 `ValuesPanel.add_widget` 代替。 10 | 11 | * 模板标签 `dw_table` / `echarts_container` 12 | 13 | 使用 `dw_widget` 代替。 14 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | # 更新日志 2 | 3 | ## v0.6.0 - (20230317) 4 | 5 | [发布日志](/release-note/v060) 6 | 7 | - 新增pycharts2.0.x适配 8 | - 新增参数化图表支持 9 | - 重构自定义地图,支持 geojson&svg格式 10 | - 新增资源类 `EntityURI` 11 | - 新增 `WidgetNotRegisteredError` 异常 12 | - `WidgetGetterMixin` 接口类支持 `EntityURI` 13 | - 修改导航栏(Nav)配置方式 14 | - 右侧导航栏支持二级菜单 15 | - Borax 更新至 4.0 16 | - typing_extensions 更新至 4.5 17 | 18 | ## v0.5.2 - (20220508) 19 | 20 | - 新增 `ElementEntity` HTML组件 21 | - 新增支持HTML生成库:[htmlgenerator](https://github.com/basxsoftwareassociation/htmlgenerator) 22 | - 新增百度地图密钥全局配置 `DJEOpts.baidu_map_ak` 23 | - 优化资源本地化功能 24 | - 修正 `Nav.add_item` 函数中 `after_separator` 逻辑 25 | - 依赖项自定义d2u支持format字符串格式化 26 | 27 | ## v0.5.1 - (20220410) 28 | 29 | [发布日志](/release-note/v051) 30 | 31 | - 新增统一的 `dw_widget` 组件渲染标签函数 32 | - 优化echarts依赖项解析逻辑 33 | - 新增echarts主题全局配置 `DJEOpts.echarts_theme` 34 | - 重构 `WidgetGetterMixin` 接口 35 | - 新增布局容器组件 `RowContainer` 、文本组件 `Title` 36 | - `ValuesPanel` 支持布局特性,移除 data / col_item_span 参数 37 | - 新增表格css函数,bootstrap_table_css 和 material_table_css 38 | - 重命名表格css函数 39 | - 新增 `DJESite.extend_urlpatterns` 函数,移除 `DJESite.dje_get_urls` 函数 40 | - 命令 download / info 按图表不再需要设置 `site_class` 41 | - 新增 unittest 单元测试构建 42 | 43 | ## v0.5.0 - (20220318) 44 | 45 | [发布日志](/release-note/v050) 46 | 47 | - 使用主题独立APP架构,修改主题设置方式 48 | - 新增直接支持 `prettytable.PrettyTable` 表格类型 49 | - 新增支持自定义geojson地图 50 | - 命令 info / download / starttpl 可忽略 `--theme` 参数,默认为 `INSTALLED_APPS` 配置的主题。 51 | - 新增 *blank.html* 空白模板文件。 52 | - Nav组件新增底部链接列表。 53 | - 新增设置(settings)页面。 54 | - 支持以表单方式更换调色主题。 55 | - 导航栏文字支持模板字符串 `DwString` 56 | - 导航栏支持是否固定设置 57 | - Borax更新至v3.5.3 58 | 59 | ## v0.5.0b2 - (20220306) 60 | 61 | - 列表页面支持关键字搜索 62 | - 优化列表页分页组件 63 | - 首页jumbotron组件支持使用图表显示 64 | - 新增支持Bootstrap5框架,默认采用Bootstrap5框架 65 | - 新增内置支持 Bootstrap框架的16个调色主题 66 | - 新增查看echarts图表options功能 67 | - 新增支持pyecharts在线地图 68 | - 新增支持3D图表 69 | - 新增支持 pyecharts Table 表 70 | - 新增 WidgetCollection图文合辑页面渲染 71 | - 新增支持echarts主题 72 | - 新增 info/download 命令行工具 73 | - 新增 starttpl 命令工具 74 | - 图表支持自动调整大小功能 75 | - 移除 `listdep` 命令 76 | 77 | ## v0.5.0b1 - (20220212) 78 | 79 | - 新增starter可视化网站生成器 80 | - 首页新增热门推荐模块 81 | - 列表页面支持grid布局 82 | - 支持bootstrap3调色主题切换 83 | - 新增支持Materialcss框架 84 | 85 | ## v0.5.0a1 - (20220110) 86 | 87 | - 迁移支持 Python3.7+ / Django3.0+ / pyecharts1.9+ -------------------------------------------------------------------------------- /docs/gallery.md: -------------------------------------------------------------------------------- 1 | # Gallery 2 | 3 | **Home Page with Top Charts** 4 | 5 | ![home-top](images/home_top.png) 6 | 7 | **Home with Chart(Bootstrap5)** 8 | 9 | ![home_with_chart_bootstrap5](images/home_with_chart_bootstrap5.png) 10 | 11 | **Word Cloud (Bootstrap.Darkly)** 12 | 13 | ![theme-darkly](images/theme_darkly.png) 14 | 15 | **List Page (material)** 16 | 17 | ![material-list-page](images/material_list_page.png) 18 | 19 | **Collection Named All** 20 | 21 | ![collection](images/collection_all.png) 22 | 23 | **Timeline Demo** 24 | 25 | ![timeline-demo](images/timeline_demo.png) 26 | 27 | -------------------------------------------------------------------------------- /docs/guides/advance_development.md: -------------------------------------------------------------------------------- 1 | # 高级开发 2 | 3 | 本文提供了一些在扩展开发过程所需要的功能。这些功能的实现可能需要整合Django核心模块功能或者其他第三方库。 4 | 5 | ## 组件 6 | 7 | ### 组件编写 8 | 9 | django-echarts 内置的UI框架与下列项目是一样的,因此可以在项目中使用其提供模板标签以提高编码效率。 10 | 11 | - Bootstrap3: [https://github.com/zostera/django-bootstrap3](https://github.com/zostera/django-bootstrap3) 12 | - Bootstrap5: [https://github.com/zostera/django-bootstrap5](https://github.com/zostera/django-bootstrap5) 13 | - Material: [https://github.com/viewflow/django-material](https://github.com/viewflow/django-material) 14 | 15 | 16 | 17 | ## 视图逻辑 18 | 19 | ### 仅登录用户访问 20 | 21 | django提供了 `login_required` 装饰器用于仅登录用户可访问的功能,但是该装饰器只能装饰视图函数。因此只能单独一个一个添加到对应视图函数之前。 22 | 23 | [django-decorator-include](https://github.com/twidi/django-decorator-include) 是一个可以在装饰`include` 函数,使得对于同组的多个路由同时添加登录限制。 24 | 25 | 下面是一个例子: 26 | 27 | ```python 28 | from django.conf.urls import url, include 29 | from django.contrib.auth.decorators import login_required 30 | 31 | from decorator_include import decorator_include 32 | 33 | from site_views import site_obj 34 | 35 | urlpatterns = [ 36 | path('site_demo/', decorator_include(login_required, site_obj.urls)) 37 | ] 38 | ``` 39 | 40 | ### 用户名显示 41 | 42 | `DwString` 提供了一种利用模板字符串显示动态字符串的功能。 43 | 44 | 例子:在右侧菜单栏显示用户信息,如果登录则显示用户名(`request.user.username`),未登录显示“匿名用户” 文字。 45 | 46 | ```python 47 | site_obj.add_right_link( 48 | LinkItem(text=DwString.login_name(un_login_text='匿名用户')) 49 | ) 50 | ``` 51 | 52 | -------------------------------------------------------------------------------- /docs/guides/entity_params.md: -------------------------------------------------------------------------------- 1 | # 参数化图表 2 | 3 | > Add in 0.6.0 4 | 5 | 从0.6.0开始,django-echarts 支持参数化图表,对于同一个图表,可以根据传入的数据参数,构建不同的图表,这些图表的配置是相同的,只是数据有所不同。 6 | 7 | ## 基础例子 8 | 9 | 实现一个参数化图表很简单,只需在图表注册函数添加参数即可。 10 | 11 | ```python 12 | from django_echarts.entity.uri import ParamsConfig 13 | from django_echarts.starter.sites import DJESite 14 | from pyecharts import options as opts 15 | from pyecharts.charts import Bar 16 | 17 | site_obj = DJESite(site_title='福建统计') 18 | 19 | @site_obj.register_chart(title='{year}年福建省家庭户类型组成', 20 | params_config=ParamsConfig({'year': [1982, 1990, 2000, 2010, 2020]})) 21 | def yearly_family_types(year: int): 22 | family_types = [ 23 | '一人户', '二人户', '三人户', '四人户', '五人户', '六人户', '七人户', '八人户', '九人户', '十人及其以上' 24 | ] 25 | data = [ 26 | [1982, 7.7, 8.2, 12.2, 17.1, 18.4, 14.7, 10.1, 11.6, 0, 0], 27 | [1990, 5.8, 8.6, 16.8, 23.6, 21.4, 11.8, 5.9, 2.9, 1.4, 1.8], 28 | [2000, 9.1, 15.5, 25.4, 24.7, 15.8, 5.9, 2.2, 0.8, 0.3, 0.3], 29 | [2010, 12.1, 17.2, 24.3, 21.7, 13.7, 6.4, 2.6, 1.1, 0.5, 0.4], 30 | [2020, 27.3, 26.3, 19.4, 14.2, 6.9, 4, 1.1, 0.4, 0.2, 0.2] 31 | ] 32 | yearly_data = {item[0]: item[1:] for item in data} 33 | if year not in yearly_data: 34 | raise ChartDoesNotExist(f'暂无{year}年数据') 35 | year_data = yearly_data[year] 36 | bar = ( 37 | Bar() 38 | .add_xaxis(family_types).add_yaxis('百分比(%)', year_data) 39 | .set_global_opts(title_opts=opts.TitleOpts("福建省家庭户类型构成-{}年".format(year))) 40 | ) 41 | return bar 42 | ``` 43 | 44 | ### 定义图表参数 45 | 46 | 图表参数位于图表注册函数(为 `DJESite.register` 或 `EntityFactory.register` 所修饰的函数)的参数列表,你可以和正常函数一样定义任何参数。 47 | 48 | django-echarts 推荐尽可能遵循下列明确性的规则: 49 | 50 | - 为每个参数声明数据类型,在URL方式调用下能够将其转化为对应的数据类型值,目前仅支持 `int / float / str / UUID` 等构造函数为`type(s)`的数据类型。 51 | - 不推荐使用 `*args` ,使用 `**kwags`。 52 | 53 | ### 定义关联ChartInfo 54 | 55 | `DJESite.register` 或 `EntityFactory.register` 装饰器的三个参数 title,description,body支持模板字符串,如上述例子中的 `{year}年福建省家庭户类型组成`。 56 | 57 | **注意:此处使用str.format的单括号形式,而不是Django模板的双括号形式。** 58 | 59 | ### 返回图表 60 | 61 | 如果用户输入的参数没有对应的图表,可以抛出 `ChartDoesNotExist` 异常。 62 | 63 | ### 参数配置ParamsConfig 64 | 65 | `params_config` 用于定义可选参数项,仅用于无参数时的图表页面显示。 66 | 67 | 在构建图表对象时不会检查参数是否符合在此配置之中,因此在图表构建函数中仍需检查相应的参数,对不符合的情况抛出 `ChartDoesNotExist 异常`。 68 | 69 | `params_config` 必须是支持下列接口的对象。 70 | 71 | ```python 72 | class ParamsConfigMixin: 73 | def __iter__(self) -> Generator[dict, Any, None]: 74 | pass 75 | ``` 76 | 77 | 如下列两种方式定义是等效的。 78 | 79 | ```python 80 | pc1 = ParamsConfig([ 81 | {'year':2021, 'month':1}, {'year':2021, 'month':2}, 82 | {'year':2021, 'month':3}, {'year':2021, 'month':4} 83 | ]) 84 | 85 | pc2 = ParamsConfig({ 86 | 'year':[2021], 87 | 'month':[1, 2, 3, 4] 88 | }) 89 | ``` 90 | 91 | 92 | 93 | ## EntityURI 94 | 95 | 为统一标识django-echarts的实体(图表、组件)引入`ENtityURI` 类,该类的一个对象由类别(catalog)、名称(name)、参数(params)三部分组成。例如上述例子中2020年的资源标识可表示为: 96 | 97 | ```python 98 | family_types_2020_uri = EntityURI(catalog='chart', name='yearly_family_types', params={'year':2020}) 99 | ``` 100 | 101 | `EntityURI` 还有字符串形式,其格式如下: 102 | 103 | ``` 104 | <资源类别>:<资源名称>/<参数名称1>/<参数值1>/<参数名称2>/<参数值2>/<参数名称3>/<参数值3>/ 105 | ``` 106 | 107 | 在多个参数情况下,其参数顺序不受影响,因此一个图表可能有多个字符串标识。 108 | 109 | 例如:上述例子可表示为 110 | 111 | ``` 112 | chart:yearly_family_types/year/2020 113 | ``` 114 | 115 | 关联的`ChartInfo`表示为 116 | 117 | ``` 118 | info:yearly_family_types/year/2020 119 | ``` 120 | 121 | 字符串形式通常用于URL、合辑、页面组件构建。 122 | 123 | ```python 124 | site_obj.add_widgets(jumbotron_chart='chart:yearly_family_types/year/2020') # 首页jumbotron位置显示2020年的数据 125 | 126 | 127 | 128 | 129 | rc = RowContainer() 130 | rc.add_widget('chart:yearly_family_types/year/2020') 131 | rc.add_widget(EntityURI(catalog='chart', name='yearly_family_types', params={'year':2020})) 132 | ``` 133 | 134 | 今年的数据总是 135 | 136 | ## 使用图表 137 | 138 | ### 从EntityFactory引用 139 | 140 | 所有的图表和HTML组件均存储在全局变量 `django_echarts.stores.entity_factory.factory` 中。`EntityFactory` 提供两种方式引用 141 | 142 | ```python 143 | from django_echarts.stores.entity_factory import factory 144 | 145 | family_types_chart_2020 = factory.get_chart_widget('yearly_family_types', params={'year':2020}) # 使用参数字典 146 | 147 | family_types_chart_2020 = factory.get_widget_by_uri(family_types_2020_uri) # 使用uri方式 148 | ``` 149 | 150 | ### 从ChartInfo引用 151 | 152 | 使用 `ChartInfo.uri` 属性。 153 | 154 | -------------------------------------------------------------------------------- /docs/images/chart_fj-map-gdp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/django-echarts/12de6cf4faab1ecbbc06bd487727cc16791ee556/docs/images/chart_fj-map-gdp.png -------------------------------------------------------------------------------- /docs/images/collection_all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/django-echarts/12de6cf4faab1ecbbc06bd487727cc16791ee556/docs/images/collection_all.png -------------------------------------------------------------------------------- /docs/images/fujian-custom-geojson.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/django-echarts/12de6cf4faab1ecbbc06bd487727cc16791ee556/docs/images/fujian-custom-geojson.png -------------------------------------------------------------------------------- /docs/images/fujian-forest-coverage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/django-echarts/12de6cf4faab1ecbbc06bd487727cc16791ee556/docs/images/fujian-forest-coverage.png -------------------------------------------------------------------------------- /docs/images/geojson-io-export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/django-echarts/12de6cf4faab1ecbbc06bd487727cc16791ee556/docs/images/geojson-io-export.png -------------------------------------------------------------------------------- /docs/images/home_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/django-echarts/12de6cf4faab1ecbbc06bd487727cc16791ee556/docs/images/home_top.png -------------------------------------------------------------------------------- /docs/images/home_with_chart_bootstrap5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/django-echarts/12de6cf4faab1ecbbc06bd487727cc16791ee556/docs/images/home_with_chart_bootstrap5.png -------------------------------------------------------------------------------- /docs/images/home_with_values_panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/django-echarts/12de6cf4faab1ecbbc06bd487727cc16791ee556/docs/images/home_with_values_panel.png -------------------------------------------------------------------------------- /docs/images/list_with_pagination.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/django-echarts/12de6cf4faab1ecbbc06bd487727cc16791ee556/docs/images/list_with_pagination.png -------------------------------------------------------------------------------- /docs/images/material_list_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/django-echarts/12de6cf4faab1ecbbc06bd487727cc16791ee556/docs/images/material_list_page.png -------------------------------------------------------------------------------- /docs/images/quickstart-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/django-echarts/12de6cf4faab1ecbbc06bd487727cc16791ee556/docs/images/quickstart-0.png -------------------------------------------------------------------------------- /docs/images/row_container_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/django-echarts/12de6cf4faab1ecbbc06bd487727cc16791ee556/docs/images/row_container_demo.png -------------------------------------------------------------------------------- /docs/images/theme_darkly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/django-echarts/12de6cf4faab1ecbbc06bd487727cc16791ee556/docs/images/theme_darkly.png -------------------------------------------------------------------------------- /docs/images/theme_material.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/django-echarts/12de6cf4faab1ecbbc06bd487727cc16791ee556/docs/images/theme_material.png -------------------------------------------------------------------------------- /docs/images/timeline_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/django-echarts/12de6cf4faab1ecbbc06bd487727cc16791ee556/docs/images/timeline_demo.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # django-echarts文档 2 | 3 | ![django-echarts version](https://img.shields.io/pypi/v/django-echarts.svg) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/django-echarts.svg) ![PyPI - Django Version](https://img.shields.io/pypi/djversions/django-echarts.svg) [![unittest](https://github.com/kinegratii/django-echarts/actions/workflows/unittest.yaml/badge.svg)](https://github.com/kinegratii/django-echarts/actions/workflows/unittest.yaml) [![codecov](https://codecov.io/gh/kinegratii/django-echarts/branch/master/graph/badge.svg?token=B7ba489op8)](https://codecov.io/gh/kinegratii/django-echarts) 4 | 5 | 6 | > A visual site scaffold based on [pyecharts](https://github.com/pyecharts/pyecharts) and [Django](https://www.djangoproject.com). 7 | 8 | **代码仓库**: [https://github.com/kinegratii/django-echarts](https://github.com/kinegratii/django-echarts) 9 | 10 | ## 概述 11 | 12 | django-echarts 是一个基于[pyecharts](https://github.com/pyecharts/pyecharts) 和 [Django](https://www.djangoproject.com) 的可视化网站脚手架。 13 | 14 | - 支持echarts图表和表格 15 | - 页面:主页 / 列表 / 详情 / 关于 / 设置 16 | - 组件:导航栏 / 网站底部栏 / 热门板块 / 列表 / 合辑 / 关于面板 17 | - UI主题:Bootstrap3 / Bootstrap5 / Material ,支持更换颜色模式 18 | - 可灵活扩展: 支持整合 Django用户认证 / 数据库 / Session 19 | - 基于Django Template System 的后端渲染 20 | - js/css静态文件托管,支持在线/本地切换 21 | - 生产力工具:代码生成器 / 静态文件下载器 22 | - 90%+ Python Typing Hints覆盖 23 | 24 | ## 安装 25 | 26 | django-echarts的运行环境要求如下: 27 | 28 | | django-echarts版本系列 | pyecharts & echarts | django | python | 29 | | ------ | ------ | ----- | ----- | 30 | | 0.6.x | 1.9 & 4.8.0 | 2.0 - 4.1 | 3.7+ | 31 | | | 2.0 & 5.4.1 | 2.0 - 4.1 | 3.7+ | 32 | | 0.5.x | 1.9 & 4.8.0 | 2.0 - 4.1 | 3.7+ | 33 | 34 | 可以使用 pip 命令安装。 35 | 36 | ```shell 37 | pip install django-echarts 38 | ``` 39 | 40 | 在 *requirements.txt* 引用 django-charts时,推荐使用 **固定次版本号** 的方式。 41 | 42 | ``` 43 | django-echarts~=0.6 44 | ``` 45 | 46 | 47 | 48 | ## 3分钟上手 49 | 50 | **1.** 创建Django项目。 51 | 52 | ```shell 53 | django-admin startproject MyDemo 54 | ``` 55 | 56 | 项目目录结构如下: 57 | 58 | ```text 59 | MyDemo 60 | |-- MyDemo 61 | |-- __init__.py 62 | |-- asgi.py 63 | |-- settings.py 64 | |-- urls.py 65 | |-- site_views.py 66 | |-- wsgi.py 67 | |-- manage.py 68 | ``` 69 | 70 | **2.** 修改项目配置文件 *settings.py*。 71 | 72 | ```python 73 | # ... 74 | 75 | INSTALL_APPS = ( 76 | # Your apps 77 | 'django_echarts', 78 | 'django_echarts.contrib.bootstrap5' 79 | # Your apps 80 | ) 81 | 82 | # ... 83 | DJANGO_ECHARTS = { 84 | } 85 | 86 | ``` 87 | 88 | 主要包括: 89 | 90 | - 添加 django_echarts包和对应的主题包到项目配置模块的 `INSTALL_APPS`列表。 91 | - 配置字典 `DJANGO_ECHARTS` 可以留空。 92 | 93 | **3.** 创建新文件 *site_views.py* ,输入下列代码。 94 | 95 | ```python 96 | # ... 97 | from django_echarts.starter.sites import DJESite 98 | from django_echarts.entities import Copyright 99 | from pyecharts import options as opts 100 | from pyecharts.charts import Bar 101 | 102 | site_obj = DJESite(site_title='福建统计') 103 | 104 | site_obj.add_widgets(copyright_=Copyright(start_year=2022, powered_by='Zinc')) 105 | 106 | chart_description = '截止2020年底,福建省土地面积1240.29万公顷,占我国国土总面积1.3%。全省森林面积811.58万公顷,森林覆盖率为66.8%,连续42年位居全国首位。 107 | 108 | @site_obj.register_chart(title='森林覆盖率', description = chart_description, catalog='基本信息') 109 | def fujian_forest_coverage(): 110 | bar = Bar().add_xaxis( 111 | ['福州', '厦门', '莆田', '三明', '漳州', '泉州', '南平', '龙岩', '宁德'] 112 | ).add_yaxis( 113 | '森林覆盖率', [54.7, 45, 58.1, 76.8, 63.4, 61, 75.3, 78, 75] 114 | ).set_global_opts( 115 | title_opts=opts.TitleOpts(title="福建省各地市森林覆盖率", subtitle="单位:%"), 116 | visualmap_opts=opts.VisualMapOpts(is_show=True, max_=100, min_=0)).set_series_opts( 117 | markline_opts=opts.MarkLineOpts( 118 | data=[ 119 | opts.MarkLineItem(y=66.8, name="全省"), 120 | ] 121 | ) 122 | ) 123 | return bar 124 | ``` 125 | 126 | 在函数`fujian_forest_coverage` 中编写pycharts代码,返回对应的图表对象。 127 | 128 | 根据需要修改文字显示,添加组件等。 129 | 130 | **4.** 在项目的路由模块 *urls.py* 添加挂载点。 131 | 132 | 133 | ```python 134 | from django.conf.urls import url, include 135 | from django.urls import path 136 | 137 | from .site_views import site_obj 138 | 139 | urlpatterns = [ 140 | # Your urls 141 | path('', include(site_obj.urls)) 142 | ] 143 | ``` 144 | 145 | **5.** 启动开发服务器,打开浏览器预览结果。 146 | 147 | ```text 148 | python manage.py runserver 0.0.0.0:8900 149 | ``` -------------------------------------------------------------------------------- /docs/posts/new_page_feature.md: -------------------------------------------------------------------------------- 1 | # 新的页面功能 2 | 3 | ## 1 概述 4 | 5 | `django_echarts.starter` 支持新增新的页面功能,一个基本的页面功能由视图、路由和模板组成。 6 | 7 | ## 2 使用步骤 8 | 9 | ### 创建新的视图 10 | 11 | 视图类必须继承 `DJESiteBackendView`,并重写 `get_context_data` 方法,添加自己的数据到 `context`,并返回该字典 。 12 | 13 | ```python 14 | class MyPageView(DJESiteBackendView): 15 | template_name = 'mypage.html' 16 | 17 | def get_context_data(self, **kwargs): 18 | context = super().get_context_data(**kwargs) 19 | context['nickname'] = 'foo' 20 | return context 21 | ``` 22 | 23 | ### 编写模板代码 24 | 25 | 模板页面必须继承模板的 *base.html* 页面。在 *main_content* 部分编写模板代码。 26 | 27 | ```html 28 | {% extends 'base.html' %} 29 | {% load echarts %} 30 | 31 | {% block main_content %} 32 |

This is my nickname: {{ nickname }}

33 | 34 | {% endblock %} 35 | ``` 36 | 37 | ### 关联路由 38 | 39 | 将新路由和 `site_obj.urls` 合并。 40 | 41 | ```python 42 | site_obj = MySite() 43 | 44 | site_obj.extend_urlpatterns([ 45 | path('mypage/', MyPageView.as_view(), name='view_my_page') 46 | ]) 47 | ``` 48 | 49 | ### 添加到导航栏 50 | 51 | ```python 52 | site_obj.add_left_link( 53 | LinkItem(title='我的页面', url=reverse_lazy('view_my_page')), 54 | menu_title='菜单一' 55 | ) # 作为“菜单一”的二级菜单 56 | ``` 57 | 58 | -------------------------------------------------------------------------------- /docs/posts/pyecharts_project.md: -------------------------------------------------------------------------------- 1 | # pyecharts项目 2 | 3 | > 本文介绍了pyecharts项目中与django-echarts相关的逻辑和django-echarts对其适配扩展的原理和实现。 4 | 5 | > 本文以 pyecharts1.9和2.0为例。 6 | 7 | ## 图表支持 8 | 9 | ### 图表类型支持 10 | 11 | pyecharts 的图表类体系如下: 12 | 13 | ``` 14 | - ChartMixin 15 | |- Base 16 | |- CompositeMixin 17 | |- Page 18 | |- Tab 19 | |- Table 20 | |- Image 21 | ``` 22 | 23 | 24 | 25 | django-echarts对图表类型支持情况如下: 26 | 27 | - 仅支持所有继承自 `pyecharts.charts.base.Base` 的图表类型,即使用一个echarts即可定义的图表,包括 `Grid` 和 `Timeline`。 28 | - 对于涉及到布局的 `CompositeMixin` ,django-echarts 提供了 `RowContainer` 、`Container` 等布局容器类。 29 | - 可直接支持 `pyecharts.components.Table`,但更推荐直接使用 `prettytable.PrettyTable`。 30 | 31 | 代码实现方面,在 `render_widget` 时也直接引用该类。 32 | 33 | ```python 34 | @render_widget.register(Base) 35 | def render_chart(widget, **kwargs) -> SafeString: 36 | pass 37 | 38 | @render_widget.register(Table) 39 | @render_widget.register(PrettyTable) 40 | def render_table(widget, **kwargs) -> SafeString: 41 | pass 42 | ``` 43 | 44 | ### 图表属性和方法 45 | 46 | 图表属性和方法包括三个部分: 47 | 48 | - 图表选项Opts:完全支持 49 | - 依赖项js_dependencies:兼容性支持,直接使用 `list` 类型并允许项重复。依赖项去重工作在模板渲染前(调用模板函数)完成,这更加适用于同一页面有多个图表的情况。 50 | - geojson地图:不支持pyecharts的内联引用方式。django-echarts提供了另一种外链引用方式。 51 | 52 | `django_echarts.entities.chart_widgets.BlankChart` 是与 `Base` 相对应的接口类,通过比较二者的区别可以看出 django-echarts的改造扩展。 53 | 54 | ## 图表渲染 55 | 56 | django-echarts 采用 Django Template Engine 而不是 Jinja2 ,因此模板函数/标签、模板tpl文件全部重写。 57 | 58 | 模板函数有三个,均定义在 `echarts` 模块。 59 | 60 | ```html 61 | {% load echarts %} 62 | 63 | {% dw_widget chart_obj %} 64 | {% echarts_js_dependencies chart_obj %} 65 | {% echarts_js_content chart_obj %} 66 | ``` 67 | 68 | 其中后两个对标于 pyecharts的macro 函数。 69 | 70 | | django-echarts模板函数 | pyecharts Jinja2 Macro | 说明 | 71 | | ------------------------------------ | -------------------------------- | -------------- | 72 | | echarts_js_dependencies(*chart_list) | render_chart_dependencies(chart) | 渲染依赖项 | 73 | | echarts_js_content(*chart_list) | render_chart_content(chart) | 渲染初始化脚本 | 74 | 75 | 与pyecharts不同的是,django-echarts的模板支持所有在 `django_echarts.renders` 注册的类,不仅包括图表类,也包括普通的HTML组件和容器组件。 76 | 77 | 代码实现方面,主要依赖于被 `singledispatch` 装饰的 `flat_chart` 函数。 78 | 79 | 模板文件定义在 *django_echarts/templates/snippets/echarts_init_options.tpl* 文件中,该文件使用 Django模板语法。 80 | 81 | ## 依赖项静态资源引用 82 | 83 | 主要功能:确定确定所使用的静态文件仓库。根据依赖项确定对应的url路径。 84 | 85 | django-echarts的改造如下: 86 | 87 | - 新增支持混合仓库。比如同一图表的 *echarts.min.js* 使用公共CDN文件,地图文件 *china.js* 使用本地文件(自行创建或远程下载)。 88 | - 所有配置项均定义在 `settings.DJANGO_ECHARTS` 字典。 89 | 90 | 依赖项功能基本逻辑。 91 | 92 | ``` 93 | dep_name(依赖项名称) --> filename(文件路径) --> 引用URL/本地文件路径 94 | ``` 95 | 96 | 97 | 98 | ## 自定义地图(geojson&svg) 99 | 100 | pyecharts按内嵌方式引入geojson/svg数据,而 django-echarts 只支持 外链方式,这是二者最大的区别。详情参见 [《自定义地图》](/guides/custom_maps) 。 101 | 102 | ## 相关API接口 103 | 104 | ### 引用的类和接口 105 | 106 | 下列类和接口在 django-echarts 被直接引用。 107 | 108 | - `pyecharts.charts.base.Base` 109 | - `pyecharts.datasets.FILENAMES` 110 | - `pyecharts.dataset.EXTRA` 111 | - `pyecharts._version.__version__` 112 | - `pyecharts.components.table.Table` 113 | - `from pyecharts.globals.ThemeType` 114 | 115 | ### 无效的类和接口 116 | 117 | 以下类和方法函数在django-echarts不起作用。 118 | 119 | - `pyecharts.globals.CurrentConfig` 120 | - `pyecharts.charts.basic_charts.Map.add_geo_json` 121 | - `pyecharts.charts.basic_charts.Geo.add_geo_json` 122 | -------------------------------------------------------------------------------- /docs/posts/third_html_library.md: -------------------------------------------------------------------------------- 1 | # 第三方HTML渲染库 2 | 3 | 本文介绍了一些常用的 HTML渲染库。 4 | 5 | ## htmlgenerator 6 | 7 | 地址: [https://github.com/basxsoftwareassociation/htmlgenerator](https://github.com/basxsoftwareassociation/htmlgenerator) 8 | 9 | ### 安装 10 | 11 | 自 v0.5.2开始,django-echarts 已经内置 htmlgenerator 作为依赖库,无需额外安装。 12 | 13 | ### 创建元素 14 | 15 | 下面是一个简单的例子。 16 | 17 | ```python 18 | import htmlgenerator as hg 19 | 20 | my_page = hg.HTML(hg.HEAD(), hg.BODY(hg.H1("It works!"))) 21 | print(hg.render(my_page, {})) 22 | 23 | my_element = hg.DIV('This is a text.', _class='panel', id='my-div') 24 | print(hg.render(my_element, {})) 25 | ``` 26 | 27 | 说明: 28 | 29 | 1. 在 htmlgenerator中,所有元素均是 `htmlgenerator.BaseElement` 的子类。 30 | 2. HTML标签以大写字符串 31 | 3. 子节点以位置参数传入 32 | 4. 属性值以关键字参数传入 33 | 34 | ### 动态值 35 | 36 | 使用 `htmlgenerator.render` 函数可以对同一元素进行多次不同渲染。 37 | 38 | ```python 39 | my_div = hg.DIV("Hello, ", hg.C("person.name")) 40 | 41 | print(hg.render(my_div, {"person": {"name": "Alice", "occupation": "Writer"}},)) 42 | 43 | print(hg.render(my_div,{"person": {"name": "John", "occupation": "Police"}},)) 44 | ``` 45 | 46 | ### Django页面响应对象 47 | 48 | `BaseElement` 类的 `render` 函数返回值可以直接传入 `HttpResponse`。 49 | 50 | ```python 51 | from django.http import HttpResponse 52 | 53 | def render_layout_to_response(request, layout, context): 54 | return HttpResponse(layout.render(context)) 55 | ``` 56 | 57 | 58 | 59 | ### 与django-echarts 60 | 61 | django-echarts 渲染模块内置支持 `htmlgenerator.BaseElement` 类型。因此 62 | 63 | - 可以作为模板标签函数 `dw_widget` 的参数 64 | - 可以作为 `Container.add_widget` 的参数,融入现有的组件体系 65 | 66 | 67 | 68 | ```python 69 | import htmlgenerator as hg 70 | from django_echarts.entities import RowContainer 71 | 72 | container = RowContainer() 73 | header_div = hg.DIV(hg.H1('Page Title'), _class='header') 74 | container.add_widget(header_div) 75 | ``` 76 | 77 | -------------------------------------------------------------------------------- /docs/posts/use_custom_geojson.md: -------------------------------------------------------------------------------- 1 | # Geojson地图 2 | 3 | > 该教程适用于 0.5 版本,0.6+版本使用 `CustomMap`自定义地图功能([链接](/guides/custom_maps)),本模块将在 1.0 移除 。 4 | 5 | ## 1 使用方法 6 | 7 | django-echarts 支持直接引用 geojson 地图,地图来源可以线上资源提供或者自己制作。相关方法均定义在 `django_echarts.geojson` 模块。 8 | 9 | ```python 10 | use_geojson(chart_obj, map_name:str, url:str) 11 | ``` 12 | 13 | 参数说明如下: 14 | 15 | | 参数 | 描述 | 16 | | --------- | ------------------------------------------------------------ | 17 | | chart_obj | pyecharts中的图表 | 18 | | map_name | 地图名称,可以为任何字符串。用于 `echarts.registerMap` 的第一个参数。 | 19 | | url | geojson文件引用地址,该url须返回一个json格式数据 | 20 | 21 | 远程地址使用 22 | 23 | ```python 24 | use_geojson(map1, '福建省', 'https://geo.datav.aliyun.com/areas_v3/bound/350000_full.json') 25 | ``` 26 | 27 | 本地地址使用 28 | 29 | ```python 30 | use_geojson(map1, '福建市县', '/geojson/fujian.geojson') # 不推荐,使用下面两种的url反向解析 31 | use_geojson(map1, '福建市县', geojson_url('fujian.geojson')) 32 | ``` 33 | 34 | 35 | 36 | ## 2 本地geojson示例 37 | 38 | ### 制作geojson文件 39 | 40 | 以制作福建省市县两级边界地图为例子。 41 | 42 | 第一步,打开阿里云数据可视化平台提供的工具 [http://datav.aliyun.com/portal/school/atlas/area_selector](http://datav.aliyun.com/portal/school/atlas/area_selector) ,依次下载福建省9个地市的geojson文件(须勾选 “包括子区域”)。 43 | 44 | 第二步, 打开 [geojson.io](http://geojson.io/) 网站,导入各地市geojson文件,直接再导出合并后的geojson,文件名为 *fujian.geojson*。 45 | 46 | ![Export](../images/geojson-io-export.png) 47 | 48 | ### 编写图表代码 49 | 50 | 第一步,将 *fujian.geojson* 改名,名称只能包含 数字、字母、下划线、横杠等字符。 51 | 52 | 并放置在 static 目录下,具体路径为: 53 | 54 | ``` 55 | /static/geojson/fujian.geojson 56 | ``` 57 | 58 | 第二步,对图表对象调用 `use_geojson` 函数。 59 | 60 | ```python 61 | from django_echarts.entities import Map 62 | from django_echarts.geojson import use_geojson, geojson_url 63 | 64 | # ... 65 | 66 | @site_obj.register_chart 67 | def my_geojson_demo(): 68 | map1 = Map() 69 | map1.add("", [('闽侯县', 23), ('湖里区', 45)], maptype="福建市县") 70 | map1.set_global_opts(title_opts=opts.TitleOpts(title="自定义geojson")) 71 | map1.height = '800px' 72 | use_geojson(map1, '福建市县', geojson_url('fujian.geojson')) 73 | return map1 74 | ``` 75 | 76 | 77 | 78 | 渲染后的前端代码如下(省略非关键代码): 79 | 80 | ```javascript 81 | $.getJSON("/geojson/fujian.geojson").done(function(mapdata){ 82 | echarts.registerMap("福建市县", mapdata); 83 | var chart_3bf0d2a = echarts.init( 84 | document.getElementById('3bf0d2a'), 85 | 'white', 86 | {renderer: 'canvas'} 87 | ); 88 | var option_3bf0d2a = { 89 | "series": [ 90 | { 91 | "type": "map", 92 | "mapType": "福建市县", 93 | "data": [ 94 | { "name": "闽侯县", "value": 23 }, 95 | { "name": "湖里区", "value": 45 } 96 | ], 97 | } 98 | ], 99 | }; 100 | chart_3bf0d2a.setOption(option_3bf0d2a); 101 | }).fail(function(jqXHR, textStatus, error){ 102 | $("#3bf0d2a").html("Load geojson file fail!Status:" + textStatus); 103 | }); 104 | 105 | ``` 106 | 107 | ### 运行效果 108 | 109 | ![fujian-geojson](../images/fujian-custom-geojson.png) 110 | 111 | -------------------------------------------------------------------------------- /docs/posts/widget_collection.md: -------------------------------------------------------------------------------- 1 | # 图表合辑 2 | 3 | ## 概述 4 | 5 | 合辑类 `WidgetCollection` 实现的是按照用户指定的布局填充相应的组件。 6 | 7 | ``` 8 | Container WidgetGetterMixin 9 | ^ ^ 10 | | | 11 | | | 12 | user_layout --> WidgetCollection EntityFactory 13 | | 14 | |- auto_mount(widget_container: WidgetGetterMixin) 15 | ``` 16 | 17 | 这样可以减少中间环节的大量 `add_widget` 操作。比如,使用Container构建组件时: 18 | 19 | ```python 20 | factory = EntityFactory() 21 | 22 | container = Container() 23 | row_container = RowContainer() 24 | chart1, info, _ = factory.get_chart_and_info('my_chart_name') 25 | row_container.add_widget(chart1, span=8) 26 | row_container.add_widget(info) 27 | container.add_widget(row_container) 28 | ``` 29 | 30 | 使用 WidgetCollection 时,将中间 `RowContainer` 的创建和添加组件封装实现在 `auto_mount` 函数之中。 31 | 32 | ```python 33 | factory = EntityFactory() 34 | 35 | wc = WidgetCollection() 36 | wc.add_chart_widget('my_chart_name', layout='l8') 37 | wc.auto_mount(factory) 38 | ``` 39 | 40 | 41 | 42 | ## 定义 43 | 44 | > class WidgetCollection(name: str, title: str = None, layout: Union[str, LayoutOpts] = 'a') 45 | 46 | 一个合辑对象 `WidgetCollection` ,继承 `Container`类。 47 | 48 | ```python 49 | wc = WidgetCollection( 50 | name='first_collection', title='第一个合辑', layout='s8' 51 | ) 52 | ``` 53 | 54 | 参数列表 55 | 56 | | 参数 | 类型 | 描述 | 57 | | ------ | ---- | -------------------------------------- | 58 | | name | slug | 合辑标识符,作为url的一部分 | 59 | | title | str | 标题,菜单栏的文字 | 60 | | layout | str | 整体布局,仅对 `add_chart_widget` 方式 | 61 | 62 | ## 添加子组件 63 | 64 | 本节的添加方式均是按照 `auto_mount` 方式。 65 | 66 | ### 添加echarts图表 67 | 68 | > WidgetCollection.add_chart_widget(self, chart_name: str, layout: str = 'l8') 69 | 70 | `WdidgetCollection` 提供了 `add_*` 方法用于添加组件,函数将参数的组件使用 `row` 类(一行12列)进行包裹。 71 | 72 | ```python 73 | # 添加名为my_first的图表和对应的信息卡组件,以8:4方式显示 74 | wc.add_chart_widget(chart_name='my_first', layout='l8') 75 | 76 | # 显示名为my_named_charts的多图表组件NamedCharts。 77 | wc.add_chart_widget(chart_name='my_named_charts', layout='l6') 78 | ``` 79 | 80 | ### 添加HTML组件 81 | 82 | > WidgetCollection.add_html_widget(widget_list: List, spans:List=None) 83 | 84 | 构建一个 RowContainer 容器,并添加一个或多个子组件。 85 | 86 | ```python 87 | # 按照给定的列数显示多个组件 88 | wc.add_html_widget(widget_names=['w1', 'w2'], spans=[8, 4]) 89 | ``` 90 | 91 | 92 | 93 | ### 组件布局 94 | 95 | 添加不同组件可使用的布局不同。 96 | 97 | | 布局layout | | 合辑定义 | 只添加echarts图表 | 添加html组件 | 98 | | ---------- | ------- | ---------------- | ----------------- | ---------------- | 99 | | 类型 | 示例 | WidgetCollection | add_chart_widget | add_html_widget | 100 | | str | l8 | Y | 8列图表 + 4列卡片 | - | 101 | | | r8 | Y | 4列卡片 + 8列图表 | - | 102 | | | s8 | Y | 交叉使用l8和r8 | - | 103 | | int | 0 | - | 12列图表 | 每个子组件平均分 | 104 | | | 8 | - | 8列图表 | 每个子组件8列 | 105 | | List[int] | [4,4,4] | - | 接受长度为2的列表 | Y | 106 | 107 | 108 | 109 | ## 注册合辑 110 | 111 | 可以通过 `DJESite.register_collection` 方法构建一个图表合辑页面。 112 | 113 | ```python 114 | site_obj = DJESite(site_title='DJE Demo') 115 | 116 | @site_obj.register_chart(name='fj_fimily_type', title='示例图表1', layout='l8') 117 | def fj_fimily_type(): 118 | line = Line() 119 | # ... 120 | return line 121 | 122 | @site_obj.register_chart(name='fj_area_bar', title='示例图表2', layout='l8') 123 | def fj_fimily_type(): 124 | bar = Bar() 125 | # ... 126 | return bar 127 | 128 | 129 | @site_obj.register_collection 130 | def collection1(): 131 | wc = WidgetCollection(title='合辑01', layout='s8') 132 | wc.add_chart_widget(name='fj_area_bar') 133 | wc.add_chart_widget(name='fj_fimily_types') 134 | return wc 135 | ``` 136 | 137 | 访问URL */collection/collection1/* 可以预览页面效果。 138 | 139 | register_collection 函数参数及其意义: 140 | 141 | | 参数 | 类型 | 说明 | 142 | | --------------- | --------- | ----------------------------------------- | 143 | | name | slug | 合辑标识,用于 `DJESite` 创建对应视图路由 | 144 | | charts | List[str] | 包含的图表标识 | 145 | | layout | str | 合辑布局 | 146 | | title | str | 标题 | 147 | | catalog | str | 如果设置,将添加合辑链接到该菜单项之下 | 148 | | after_separator | bool | 是否在菜单项前使用分隔符 | 149 | 150 | -------------------------------------------------------------------------------- /docs/reference/commands.md: -------------------------------------------------------------------------------- 1 | # 命令行工具 2 | 3 | django-echarts 提供了一个包含若干个命令的 CLI 工具,这些命令都是标准的 Django 管理命令,均定义在 `django_echarts.management.commands` 包下。 4 | 5 | 本文档列出的帮助信息不包含继承自django命令的选项,你可以使用以下命令查看完整的帮助信息。 6 | 7 | ```shell 8 | python manage.py -h 9 | ``` 10 | 11 | ## 静态文件 - 查看(info)和下载(download) 12 | 13 | 参见 本地化静态资源 一节。 14 | 15 | 16 | ## startsite - 生成视图代码 17 | 18 | ### 帮助信息 19 | 20 | 创建 初始化site的代码片段。 21 | 22 | ```text 23 | E:\projects\django-echarts\example>python manage.py startsite -h 24 | usage: manage.py startsite [-h] [--site-title SITE_TITLE] [--start-year START_YEAR] [--powered-by POWERED_BY] 25 | [--override] output 26 | 27 | Auto generate site_views.py file. 28 | 29 | positional arguments: 30 | output The output file. 31 | 32 | optional arguments: 33 | -h, --help show this help message and exit 34 | --site-title SITE_TITLE, -t SITE_TITLE 35 | The title of site. 36 | --start-year START_YEAR, -y START_YEAR 37 | The start year. 38 | --powered-by POWERED_BY, -p POWERED_BY 39 | The principal of copyright. 40 | --empty, -e 41 | --override, -o 42 | ``` 43 | 44 | ## starttpl - 生成模板文件 45 | 46 | ### 使用方法 47 | 48 | ```text 49 | E:\projects\zinc> python .\manage.py starttpl -h 50 | usage: manage.py starttpl [-h] [--theme {bootstrap3,bootstrap5,material}] [--output OUTPUT] [--force] tpl_name [tpl_name ...] 51 | 52 | Copy the builtin template files to your project templates. 53 | 54 | 55 | positional arguments: 56 | tpl_name The name of template file. 57 | 58 | optional arguments: 59 | -h, --help show this help message and exit 60 | --theme {bootstrap3,bootstrap5,material} 61 | The name of theme. 62 | --output OUTPUT, -o OUTPUT 63 | The output filename 64 | --force, -f Whether to copy if the file exists. 65 | ``` 66 | 67 | ### 查看主题所有文件 68 | 69 | 查看当前主题的所有模板文件。 70 | 71 | ```text 72 | E:\projects\zinc> python .\manage.py starttpl 73 | The template names of Theme [bootstrap3]: 74 | about.html 75 | base.html 76 | blank.html 77 | chart_collection.html 78 | chart_single.html 79 | home.html 80 | info_card.html 81 | items_grid.html 82 | items_list.html 83 | list.html 84 | list_with_paginator.html 85 | message.html 86 | widgets\values_panel.html 87 | 88 | Start to custom a template: python manage.py starttpl -n blank -o my_page 89 | 90 | ``` 91 | 92 | ### 复制模板文件 93 | 94 | 使用 `-n` 复制一个文件,如果项目模板目录已经存在该文件,则需要添加 `-o` 覆盖,否则跳过复制。`-n` 的参数可以不添加后缀格式的 .html 。 95 | 96 | 复制内置的 message.html 文件 97 | 98 | ```text 99 | E:\projects\zinc> python .\manage.py starttpl message 100 | Generate message.html, Success! 101 | ``` 102 | 103 | 创建新的空白模板 104 | 105 | ```text 106 | E:\projects\zinc> python .\manage.py starttpl blank -o my_page 107 | Generate my_page.html, Success! 108 | ``` 109 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /docs/reference/stores.md: -------------------------------------------------------------------------------- 1 | # 组件存储类EntityFactory 2 | 3 | 本文对应于 `django_echarts.stores` 代码包。 4 | 5 | ## factory 6 | 7 | 全局变量,类型 `EntityFactory`。使用方式: 8 | 9 | ```python 10 | from django_echarts.stores import factory 11 | 12 | @factory.register_chart_widget 13 | def my_bar(): 14 | bar = Bar() 15 | # ... 16 | return bar 17 | 18 | bar1 = factory.get_chart_widget('my_bar') 19 | ``` 20 | 21 | ## EntityFactory 22 | 23 | `EntityFactory` 是存储echarts图表创建器或者具体组件实例对象的容器。 24 | 25 | ### html_widgets 26 | 27 | 属性,类型 : `LazyDict`。组件创建器或实例存储。 28 | 29 | ### chart_widgets 30 | 31 | 属性,类型 : `LazyDict`。图表创建器或实例存储。 32 | 33 | ### chart_info_manager 34 | 35 | 属性,类型: `ChartInfoManagerMixin`。 存储图表关联的 `ChartInfo` 数据类。 36 | 37 | ### register_chart_widget 38 | 39 | 方法,可用作装饰器。注册图表创建器或实例。 40 | 41 | ### register_html_widget 42 | 43 | 方法,可用作装饰器。注册HTML组件创建器或实例。 44 | 45 | ### register_chart_info 46 | 47 | 方法。关联已有的 `ChartInfo`。 48 | 49 | ### get_chart_and_info 50 | 51 | 方法。获取给定name对应的图表实例和关联的 ChartInfo 。 52 | 53 | ### get_chart_widget 54 | 55 | 方法。返回对应name的图表实例。 56 | 57 | ### get_html_widget 58 | 59 | 方法。返回对应name的HTML组件实例。 60 | 61 | ### get_widget_by_name 62 | 63 | 方法。返回对应name的图表或HTML组件实例。 64 | 65 | ```python 66 | chart1 = factory.get_widget_by_name('bar1') 67 | 68 | info = factory.get_widget_by_name('info:bar1') 69 | ``` 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /docs/reference/system_design.md: -------------------------------------------------------------------------------- 1 | # 设计与实现 2 | 3 | 本文介绍了 django-echarts 的软件设计思想、体系和编程实现。 4 | 5 | ## 包体系设计 6 | 7 | v0.5.1版本在保持整体公共访问接口不变的基础上,进一步重构底层实现逻辑。目前按照代码包可划分为以下模块: 8 | 9 | | 代码包/模块 | 描述 | 备注 | 10 | | --------------------------- | ---------------------------------------- | ---- | 11 | | **配置层** | | | 12 | | django_echarts.core | 依赖项、主题配置 | | 13 | | django_echarts.conf | 配置访问入口 | | 14 | | **数据层** | | | 15 | | django_echarts.entities | 组件定义层 | | 16 | | django_echarts.stores | 组件全局存储入口 | | 17 | | **模板层** | | | 18 | | django_echarts.contrib | 内置的主题APP | | 19 | | django_echarts.templates | 模板文件 | | 20 | | **渲染层** | | | 21 | | django_echarts.renders | 组件、依赖项渲染逻辑 | | 22 | | django_echarts.templatetags | 标签函数 | | 23 | | **视图业务层** | | | 24 | | django_echarts.starter | 网站视图路由层 | | 25 | | django_echarts.views | 普通视图层 | | 26 | | **功能扩展** | | | 27 | | django_echarts.ajax_echarts | ajax方式渲染echarts图表逻辑 1 | | 28 | | django_echarts.management | 命令行工具 | | 29 | | django_echarts.geojson | geojson工具层 | | 30 | 31 | 1. 截至v0.5.x,该功能暂未实现。 32 | 33 | 34 | 35 | ## 单元测试 36 | 37 | django-echarts 坚持保证代码质量,包括 Typing Hints 、单元测试、代码覆盖率等工具和方法。 38 | 39 | 单元测试位于项目目录 `tests` (包含 `__init__.py` 文件,视为一个包)。 40 | -------------------------------------------------------------------------------- /docs/reference/tags.md: -------------------------------------------------------------------------------- 1 | # 模板标签库 2 | 3 | ## 导入 4 | 5 | django-echarts 实现了与 pyecharts 功能上相似的模板标签,均定义在 `django_echarts.templatetags.echarts` 包。在使用之前需要先行导入,有两种方式。 6 | 7 | **方式一. 按需导入** 8 | 9 | 在每个需要使用标签函数的模板文件使用 `{% load echarts %}` 导入。 10 | 11 | **方式二. 统一导入** 12 | 13 | 添加标签目录到项目配置项 `TEMPLATES.OPTIONS.libraries` ,这样就无需在每个模板都使用 load 标签。 14 | 15 | ```python 16 | TEMPLATES = [ 17 | { 18 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 19 | 'DIRS': [os.path.join(BASE_DIR, 'templates')], 20 | 'APP_DIRS': True, 21 | 'OPTIONS': { 22 | 'context_processors': [ 23 | 'django.template.context_processors.debug', 24 | 'django.template.context_processors.request', 25 | 'django.contrib.auth.context_processors.auth', 26 | 'django.template.context_processors.static', 27 | 'django.contrib.messages.context_processors.messages', 28 | ], 29 | 'libraries': { 30 | 'echarts': 'django_echarts.templatetags.echarts' 31 | } 32 | }, 33 | }, 34 | ] 35 | ``` 36 | 37 | ## 组件渲染 38 | 39 | ### dw_widget 40 | 41 | ```python 42 | django_echarts.templatetags.echarts.dw_widget(context, widget, **kwargs) 43 | ``` 44 | 45 | 渲染单个组件,可以是图表组件、HTML组件和容器组件。使用方法如下: 46 | 47 | ```html 48 | {% dw_widget row_container %} 49 | {% dw_widget chart width="100%" height="700px" %} 50 | ``` 51 | 52 | 可支持的参数: 53 | 54 | | 参数 | 类型 | 描述 | 55 | | ------ | ---------------------- | ------------------------------------------------------------ | 56 | | width | Union[int, float, str] | 宽度,仅适用于 pyecharts.charts.Base | 57 | | height | Union[int, float, str] | 高度,仅适用于 pyecharts.charts.Base | 58 | | class_ | str | 仅适用于LinkItem/Menu | 59 | | tpl | str | 模板文件,仅适用于 HTMLBase。默认为 *widgets/{widget_name}.html* | 60 | 61 | 当widget为None(不包括0, ''等空值),`dw_widget` 将输出空字符串。 62 | 63 | ```html 64 | # 直接使用即可 65 | {% dw_widget widget %} 66 | 67 | # 而不必使用if判断 68 | {% if widget %} 69 | {% dw_widget widget %} 70 | {% endif %} 71 | ``` 72 | 73 | ### echarts_container 74 | 75 | 渲染图表,已被 `dw_widget` 替代。 76 | 77 | ## echarts初始化 78 | 79 | > 本节标签函数中参数 widgets 参数传入任何组件对象,标签函数自动提取其中的echarts图表对象。 80 | 81 | ### echarts_js_dependencies 82 | 83 | ```python 84 | django_echarts.templatetags.echarts.echarts_js_dependencies(*widgets) 85 | ``` 86 | 渲染包含图表所需要的js文件的script一个或多个节点。该函数会对args里面的图表依赖项进行汇总去重。 87 | 88 | 在同一页面,应当保证 `echarts_js_dependencies` 只使用一次,避免部分库文件重复加载。 89 | 90 | ### echarts_js_content 91 | 92 | ```python 93 | django_echarts.templatetags.echarts.echarts_js_content(*widgets) 94 | ``` 95 | 渲染图表初始js代码,支持多图表。包含首尾的 `` 标签。 96 | 97 | ### echarts_js_content_wrap 98 | 99 | ```python 100 | django_echarts.templatetags.echarts.echarts_js_content_wrap(*widgets) 101 | ``` 102 | 103 | 渲染图表初始js代码,支持多图表。不包含首尾的 `` 标签。 104 | 105 | ## 主题相关 106 | 107 | ### theme_css 108 | 109 | ```python 110 | django_echarts.templatetags.dje.theme_css() 111 | ``` 112 | 113 | 渲染主题css。 114 | 115 | ### theme_js 116 | 117 | ```python 118 | django_echarts.templatetags.dje.theme_js() 119 | ``` 120 | 121 | 渲染主题javascript。 122 | 123 | ## 其他 124 | 125 | ### page_link 126 | 127 | ```python 128 | django_echarts.templatetags.echarts.page_link(context, page_number) 129 | ``` 130 | 131 | 将当前url增加page参数。 132 | 133 | ***url_single_chart*** 134 | 135 | ```python 136 | django_echarts.templatetags.echarts.url_single_chart(uri_or_name: Union[EntityURI, str], params_dic: dict = None) 137 | ``` 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /docs/release-note/v050.md: -------------------------------------------------------------------------------- 1 | # v0.5.0发布日志 2 | 3 | > 发布日期:2022-03-18 4 | 5 | ## 1 脚手架 6 | 7 | 8 | django-echarts v0.5.0是项目新版本系列的第一个正式版本。从此版本开始, django-echarts 的定位从原来的 *工具库* 提升为 *脚手架* 。django-echarts力图在最短的时间内搭建一个可以显示可视化图表的web网站。 9 | 10 | 11 | 用户只需要编写图表的 pyecharts 代码,其他诸如页面样式、路由、视图逻辑等由django-echarts驱动,并支持可定制化。 12 | 13 | 14 | ## 2 丰富的图表类型 15 | 16 | django-echarts直接引用pyecharts图表配置逻辑并进行简单的语法适配(从jinja2到DTS),已经支持90%以上的图表类型(包括基本图表、地图、3D图、复合图表等)。 17 | 18 | django-echarts还提供了适用于web平台的图表类型,包括: 19 | 20 | - 响应式的表格,可支持 pyecharts.charts.Table 和 prettytable.PrettyTable 21 | - 地图底图支持原生geojson源 22 | - 多图表 NamedCharts 23 | 24 | ## 3 组件、布局和站点 25 | 26 | django-echarts 默认提供了一个通用的类CMS站点,该站点包含 27 | 28 | - 页面:首页、列表、单图表、图表合辑、关于 29 | - 组件:导航栏、网站底部、信息卡、数值面板、图表列表 30 | 31 | 对于每个页面,可以根据业务需求进行定制。用户也可以根据django的模板规则进行整体性和替换。 32 | 33 | 34 | ## 4 UI主题 35 | 36 | django-echarts 默认内置了两个主流的前端UI框架:Bootstrap 和 Materialcss 。借助 [bootswatch](https://bootswatch.com/) 项目,django-echarts也支持Bootstrap的调色主题切换。 37 | 38 | ## 5 生产力工具 39 | 40 | ### 下载器 41 | 42 | 43 | 默认情况下,django-echarts 使用由公共CDN提供的在线静态资源,主要包括: 44 | 45 | - 前端框架静态文件 46 | - echarts库静态文件 47 | - 地图数据文件 48 | 49 | django-echarts提供了基于Django Commands实现的下载器工具,通过简单的命令就可以把文件下载到本地,直接与 Django Staticfiles App 完美融合。 50 | 51 | 52 | ### 代码生成器 53 | 54 | 55 | 通过 `startsite` 命令生成站点框架代码,后续可以添加具体的图表代码。 56 | 57 | 58 | ## 6 代码标准化和规范化 59 | 60 | 61 | 在代码实现上,90%以上的代码使用 Python Typing Hints ,具有很强的可读性。 -------------------------------------------------------------------------------- /docs/release-note/v051.md: -------------------------------------------------------------------------------- 1 | # v0.5.1发布日志 2 | 3 | > 发布日志:2022-04-10 4 | 5 | 6 | 7 | ## 1 体系性、扩展性 8 | 9 | v0.5.1版本在保持整体公共访问接口不变的基础上,进一步重构底层实现逻辑。详情可查看文档 [《体系设计》](/reference/system_design/) 。 10 | 11 | ## 2 组件体系 12 | 13 | v0.5.1进一步抽象组件的共有特性,比如是否包括依赖项、是否是容器组件等。新增诸如 `ContainerBase` 、`HTMLWidgetBase` 等组件基类封装共同逻辑,,并支持容器组件的嵌套使用。 14 | 15 | 在实现上使用 [singledispatch](https://docs.python.org/3/library/functools.html#functools.singledispatch) 的函数重载功能,这样: 16 | 17 | - 所有组件拥有同一的入口,使用`dw_widget` 标签可渲染所有类型的组件 18 | - 将参数和变量的类型明确化,减少了 Duck Type 代码的使用。 19 | 20 | ## 3 新的组件存储层 21 | 22 | 将组件存储相关功能从`DJESite` 中移除,形成新的 `django_echarts.stores` 包。 23 | 24 | 这样,**保存图表创建器和组件的实例对象,由 `starter` 模块下移到 `stores` 模块,并实现单例模块。**。 25 | 26 | ``` 27 | site.DJESite 28 | ^ 29 | | 30 | stores.EntityFactory 31 | ^ 32 | | 33 | Lazy 34 | ``` 35 | 36 | 在实现细节上: 37 | 38 | - `DJESite` 不再实现 `WidgetGetterMixin` 39 | - 移除 `DJESite.resolve_chart` / `DJESite.resolve_html_widget`方法 40 | - `WidgetGetterMixin` 接口方法 `resolve_*` 重命名为 `get_*` 41 | 42 | 43 | 44 | ## 4 优化路由入口 45 | 46 | 移除 `DJESite.dje_get_urls` 接口,新增 `DJESite.extend_urlpatterns` 函数。 47 | 48 | ````python 49 | class MyPageView(DJESiteBackendView): 50 | pass 51 | 52 | site_obj.extend_urlpatterns([ 53 | path('mypage/', MyPageView.as_view(), name='my-page') 54 | ]) 55 | ```` 56 | 57 | ## 5 命令行优化 58 | 59 | 命令 download / info 按图表查找依赖项不再需要设置 `site_class`, 使用 `stores.entity_factory.factory` 即可访问图表创建器和组件对象。 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /docs/release-note/v060.md: -------------------------------------------------------------------------------- 1 | # v0.6.0发布日志 2 | 3 | > 发布时间:2023年3月17日 4 | 5 | ## 1 新增pyecharts2.0适配 6 | 7 | **django-echarts 0.6.0支持pycharts 2.0。** pyecharts2.0的主要变化有两点: 8 | 9 | - 第一,仓库地址变化和地图绘制变化,主要包括echarts及其扩展库、echarts主题文件、地图文件。参见 [pyecharts-issue#2107](https://github.com/pyecharts/pyecharts/issues/2107) 。 10 | - 第二,纯pyecharts代码变化,如新的图表类型、图表配置等。具体可参见 pyecharts 项目。此项工作由用户自行修改。 11 | 12 | django-echarts 对远程仓库的处理如下: 13 | 14 | | django-echarts | pyecharts版本 | echarts | 引用仓库 | `dms_repo` 设置 | 15 | | -------------- | ------------- | ------- | ------------ | ----------------------------------- | 16 | | 0.5.x | 1.9 | 4.8.0 | pyecharts | `pyecharts` 或 缺省 | 17 | | 0.6.x | 1.9 | 4.8.0 | pyecharts | `pyecharts` 或 缺省 | 18 | | | 2.0 | 5.4.1 | pyecharts-v5 | `pyecharts` 、`pyecharts-v5`或 缺省 | 19 | 20 | 说明: 21 | 22 | - django-echarts 和 pyecharts 并无版本上的关联,只需要保证 pyecharts 和 echarts版本(仓库标识) 一致即可。 23 | - 为了简化配置,当 django-echarts0.6/pyecharts2.0,如果 `dms_repo='pyecharts' ` (默认配置),将自动切换为 `pyecharts-v5`。 24 | 25 | ## 2 仓库配置优化 26 | 27 | 单个依赖项设置支持批量配置。`dep2url` 字典key表示为仓库名称(需以`#`开头)。 28 | 29 | ```python 30 | DJANGO_ECHARTS = { 31 | 'dep2url':{ 32 | '#local': ['echarts', '中国'] 33 | } 34 | } 35 | ``` 36 | 37 | 等效于 38 | 39 | ```python 40 | DJANGO_ECHARTS = { 41 | 'dep2url':{ 42 | 'echarts':'#local', 43 | '中国':'#local' 44 | } 45 | } 46 | ``` 47 | 48 | ## 3 图表构建函数支持自定义参数 49 | 50 | 图表构建函数支持自定义参数,可以根据不同的输入值,加载不同的数据,从而形成多个图表。 51 | 52 | 以之前的时间轴图表为例子,每次只显示一个年份的数据图表。 53 | 54 | ```python 55 | @site_obj.register_chart(title='{year}年福建省家庭户类型组成') 56 | def yearly_family(year: int): 57 | family_types = [ 58 | '一人户', '二人户', '三人户', '四人户', '五人户', '六人户', '七人户', '八人户', '九人户', '十人及其以上' 59 | ] 60 | data = [ 61 | [1982, 7.7, 8.2, 12.2, 17.1, 18.4, 14.7, 10.1, 11.6, 0, 0], 62 | [1990, 5.8, 8.6, 16.8, 23.6, 21.4, 11.8, 5.9, 2.9, 1.4, 1.8], 63 | [2000, 9.1, 15.5, 25.4, 24.7, 15.8, 5.9, 2.2, 0.8, 0.3, 0.3], 64 | [2010, 12.1, 17.2, 24.3, 21.7, 13.7, 6.4, 2.6, 1.1, 0.5, 0.4], 65 | [2020, 27.3, 26.3, 19.4, 14.2, 6.9, 4, 1.1, 0.4, 0.2, 0.2] 66 | ] 67 | yearly_data = {item[0]: item[1:] for item in data} 68 | if year not in yearly_data: 69 | raise ChartDoesNotExist(f'year={year}') 70 | year_data = yearly_data[year] 71 | bar = ( 72 | Bar() 73 | .add_xaxis(family_types).add_yaxis('百分比(%)', year_data) 74 | .set_global_opts(title_opts=opts.TitleOpts("福建省家庭户类型构成-{}年".format(year))) 75 | ) 76 | return bar 77 | ``` 78 | 79 | 说明: 80 | 81 | - year为自定义参数,如未声明类型,默认为 `str`。 82 | - 如果传入的year找不到对应的数据,抛出 `ChartDoesNotExist` 异常,web页面显示“找不到图表”的页面。 83 | - title参数可以使用类似 `{year}`的模板字符串。 84 | 85 | 该图表对应的相关引用配置如下(以year=2020例子): 86 | 87 | | 项目 | 值 | 备注 | 88 | | ---------- | ------------------------------------------------------------ | -------------------------------------------- | 89 | | 图表URL | `/chart/year/2020/` | 格式:`<图表slug>/<参数名称1>/<参数值1>/...` | 90 | | python引用 | `reverse_lazy('dje_single_chart','yearly_family','year/2020/')` | | 91 | | 模板引用 | `{% url 'dje_single_chart' 'yearly_family', 'year/2020/' %}` | | 92 | 93 | ## 4 重构自定义地图,支持geojson&svg 94 | 95 | v0.6开始,新增 `django_echarts.custom_maps` 模块,原有的 `django_echarts.geojson` 标记为废弃状态。 96 | 97 | 将原有的 `use_geojson` 替换为 `use_custom_map` 即可。 98 | 99 | ```python 100 | chart = Map(...) 101 | use_custom_map(chart, '中国省市地图', 'china_cities.geojson') 102 | ``` 103 | 104 | 105 | 106 | ## 5 新增声明式导航栏配置 107 | 108 | django-echarts 0.6 新增了使用字典方式一次性配置导航栏配置。 109 | 110 | ```python 111 | # site_config.py 112 | 113 | nav_config = { 114 | 'nav_left': ['home', 'list'], 115 | 'nav_right': ['settings'], 116 | 'nav_footer': [ 117 | {'text': '项目主页', 'slug': 'project_url', 'url': 'https://github.com/kinegratii/django-echarts'} 118 | ] 119 | } 120 | 121 | # urls.py 122 | 123 | site_obj.config_nav(nav_config) 124 | ``` 125 | 126 | 相应的,DJESite类移除导航栏相关方法,可使用 `DJESite.nav` 相关方法。 127 | 128 | - `DJESite.add_left_link` 129 | - `DJESite.add_right_link` 130 | - `DJESite.add_footer_link` 131 | - `DJESite.add_menu_item` 132 | 133 | ## 6 ChartInfo和ChartInfoManager 134 | 135 | - 类 `ChartInfo` 增加 `is_bound` 布尔值属性,如果对应图表注册函数携带任何参数,则设置为 `False`,数据格式化后变为`True`。 136 | - `ChartInfoManagerMixin.get_or_none` 增加 `uri` 参数。 137 | - 在 `is_bound = False` 情况下,body title description的转义处理 138 | 139 | ## 7 其他依赖库更新 140 | 141 | ``` 142 | typing_extensions 4.0 -> 4.5 143 | borax 3.5 -> 4.0 144 | ``` 145 | 146 | -------------------------------------------------------------------------------- /docs/starter/site_configuration.md: -------------------------------------------------------------------------------- 1 | # 网站配置 2 | 3 | ## 导航栏(Nav) 4 | 5 | > Update in 0.6.0: 新增 `DJESite.config_nav` 函数。 6 | 7 | 网站导航栏包括:左侧导航栏、右侧导航栏、底部导航栏,其中左侧、右侧(均位于顶部)支持下拉菜单,底部导航栏仅支持多个链接形式。 8 | 9 | 有两种使用方法。 10 | 11 | ## SiteOptions.nav_shown_pages 12 | 13 | ```python 14 | class SiteOpts: 15 | """The opts for DJESite.""" 16 | # ... 17 | nav_shown_pages: List = field(default_factory=lambda: ['home']) 18 | ``` 19 | 20 | 该属性在 `DJESite` 对象初始化的时候传入,`nav_shown_pages` 为包含表示特定页面标识(字符串)的列表,可选的有:home / list / collection / settings / chart_nav 。 21 | 22 | 其中chart_nav需调用 `DJESite.config_nav`。 23 | 24 | ## 函数config_nav 25 | 26 | ```python 27 | DJESite.config_nav(self, menu_config: dict = None) 28 | ``` 29 | 30 | 函数`config_nav` 用于配置导航栏,需注意两点: 31 | 32 | - 该函数是一次性配置的,将覆盖 `SiteOptions.nav_shown_pages`的配置。 33 | - 该函数必须在所有 `register_chart` 之后调用,一般在 `urls`模块调用。 34 | 35 | `menu_config` 参数基本形式如下: 36 | 37 | ```python 38 | class NavConfig(TypedDict): 39 | nav_left: NotRequired[List[Union[str, dict]]] 40 | nav_right: NotRequired[List[Union[str, dict]]] 41 | nav_footer: NotRequired[List[Union[str, dict]]] 42 | ``` 43 | 44 | 一个菜单项可以是以下几种形式: 45 | 46 | - 内置页面(str):home / list / collection / settings 47 | - 图表分类导航(str):chart_nav 48 | - 图表或合辑资源标识符(str):使用其URI字符串形式 49 | - django视图链接(str):使用视图名称及其参数 50 | - 自定义链接(dict):适用于菜单或菜单项 51 | 52 | ### 1 自定义链接 53 | 54 | 这是最为基础的定义形式,完整的链接定义如下: 55 | 56 | ```python 57 | {'text': '网站源码', 'slug':'site_source', 'url': 'https://github.com/kinegratii/zinc', 'new_page': True} 58 | ``` 59 | 60 | | 属性 | 描述 | 61 | | -------- | ---------------------------------- | 62 | | text | 必选,链接文字。 | 63 | | url | 必选,链接。 | 64 | | slug | 可选,标识符,默认生成uuid字符串。 | 65 | | new_page | 可选,默认为False。 | 66 | 67 | ### 2 菜单定义 68 | 69 | ```python 70 | {'text': '网站源码', 'slug':'site_source', 'children':[...]} 71 | ``` 72 | 73 | 使用键 `children` 表明这是一个菜单列表,下级菜单定义在属性children之中。 74 | 75 | ### 3 内置页面 76 | 77 | 可支持的内置页面如下: 78 | 79 | | 标识符 | 页面 | 80 | | ---------- | ---------- | 81 | | home | 首页 | 82 | | list | 图表列表页 | 83 | | collection | 合辑列表页 | 84 | | settings | 设置页面 | 85 | 86 | ### 3 Django视图 87 | 88 | ```python 89 | {'text':'大屏面板', 'view_name': 'dashbord', 'args':[], 'kwargs':{},'new_page':False} 90 | ``` 91 | 92 | 使用 `reverse_lazy` 反向解析url。 93 | 94 | ### 4 图表资源 95 | 96 | ```python 97 | {'text': '2020年数据', 'slug':'family_type_2020', 'url': 'chart:yearly_family_types/year/2020', 'new_page': False} 98 | ``` 99 | 100 | 类似于 101 | 102 | ```python 103 | {'text':'2020年数据', 'view_name': 'dje_chart_single', 'args':['yearly_family_types','/year/2020'], 'kwargs':{},'new_page':False} 104 | ``` 105 | 106 | 107 | 108 | ### 5 图表分类导航chart_nav 109 | 110 | ```python 111 | 'chart_nav' 112 | ``` 113 | 114 | 该字符串仅为占位符,函数 `DJESite.config_nav` 将根据 `register_chart` 的 catalog,在该位置添加菜单栏。 115 | 116 | 该配置仅可用于左侧导航栏。 117 | 118 | ## 配置示例 119 | 120 | ### 默认配置 121 | 122 | 默认配置(不调用 `config_nav`函数),仅包含 `nav_shown_pages`所定义菜单。 123 | 124 | ### 0.5兼容配置 125 | 126 | 以下是 django-echart 0.6 兼容0.5版本的配置方法: 127 | 128 | ```python 129 | site_obj = DJESite(title='示例站点', opts=SiteOpts(nav_shown_pages=['home', 'list', 'chart_nav'])) 130 | 131 | 132 | @site_obj.register(..., catalog='菜单1') 133 | def chart_1(): 134 | pass 135 | 136 | 137 | site_obj.config_nav() 138 | ``` 139 | 140 | ### 常规配置 141 | 142 | ```python 143 | nav = { 144 | 'nav_left': ['home', 'list', 'chart_nav', {'text': '2020年数据', 'url': 'chart:yearly_family/year/2020'}], 145 | 'nav_right': ['settings'], 146 | 'nav_footer': [ 147 | {'text': '福建统计年鉴2021', 'url': 'https://tjj.fujian.gov.cn/tongjinianjian/dz2021/index.htm', 'new_page': True}, 148 | {'text': '网站源码', 'url': 'https://github.com/kinegratii/zinc', 'new_page': True}, 149 | ] 150 | } 151 | ``` 152 | 153 | 154 | 155 | ### 关联导航栏 156 | 157 | 下面是演示如何添加到导航菜单栏的。以 `chart.title = 'Chart1'` 为例子: 158 | 159 | | chart.catalog | nav_parent_name | 说明 | 160 | | ------------- | --------------- | --------------------------------- | 161 | | None(未设置) | None(未设置) | 不显示在菜单栏上 | 162 | | 'BarCharts' | None(未设置) | 显示,“BarCharts- Chart1” | 163 | | None(未设置) | 'Menu' | 显示,“Menu - Chart1” | 164 | | 'BarCharts' | 'Menu' | 显示 "Menu - Chart1" 1 | 165 | | 任意 | 'self' | 显示一级菜单2 | 166 | | 任意 | 'none' | 不显示3 | 167 | 168 | 1. 只要 catalog 和 nav_parent_name 至少一个有设置,均显示为二级菜单 169 | 2. 使用特殊标识 'self' 表示显示为一级菜单 170 | 3. 使用特殊标识 'none' 表示不显示 171 | -------------------------------------------------------------------------------- /long_description.md: -------------------------------------------------------------------------------- 1 | # django-echarts 2 | 3 | ![django-echarts version](https://img.shields.io/pypi/v/django-echarts.svg) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/django-echarts.svg) ![PyPI - Django Version](https://img.shields.io/pypi/djversions/django-echarts.svg) 4 | 5 | 6 | 7 | > A visual site scaffold based on pyecharts and django. 8 | 9 | django-echarts 是一个基于[pyecharts](https://github.com/pyecharts/pyecharts) 和 [Django](https://www.djangoproject.com) 整合的可视化网站脚手架。 10 | 11 | ## 概述(Summary) 12 | 13 | django-echarts 主要提供了以下的内容: 14 | 15 | - 支持 90%+的pyecharts图表类型 16 | - 可显示页面:主页 / 列表 / 详情 / 关于 17 | - 可支持组件:导航栏 / 网站底部栏 / 热门板块 / 列表 / 合辑 / 关于面板 18 | - UI主题:Bootstrap3 / Bootstrap5 / Material ,支持更换颜色模式 19 | - 可灵活扩展: 支持整合 Django用户认证 / 数据库 / Session 20 | - 基于Django Template Engine 的后端渲染 21 | - js/css静态文件托管,支持在线/本地切换 22 | - 生产力工具:代码生成器 / 静态文件下载器 23 | - 90%+ Python Typing Hints覆盖 24 | 25 | ## 文档(Document) 26 | 27 | [在线文档](https://django-echarts.readthedocs.io/) 28 | 29 | Build on [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) 30 | 31 | ## 示例项目(Demo Project) 32 | 33 | 参见项目 [kinegratii/zinc](https://github.com/kinegratii/zinc) 。 34 | 35 | ## 开源协议(License) 36 | 37 | MIT License 38 | -------------------------------------------------------------------------------- /mkdocs.yaml: -------------------------------------------------------------------------------- 1 | site_name: django-echarts 2 | theme: 3 | language: zh 4 | icon: 5 | logo: material/monitor-dashboard 6 | name: material 7 | palette: 8 | primary: teal 9 | features: 10 | - navigation.tabs 11 | - navigation.sections 12 | extra: 13 | social: 14 | - icon: fontawesome/brands/github 15 | link: https://github.com/kinegratii/django-echarts 16 | - icon: fontawesome/brands/python 17 | link: https://pypi.org/project/django-echarts 18 | copyright: Copyright © 2017 - 2023 Samuel.Yan 19 | markdown_extensions: 20 | - pymdownx.highlight 21 | - pymdownx.superfences 22 | - mdx_truly_sane_lists 23 | nav: 24 | - 快速开始: index.md 25 | - 可视化构建: 26 | - 使用教程: starter/base.md 27 | - 网站配置: starter/site_configuration.md 28 | - 组件和布局: starter/widget_and_layout.md 29 | - UI框架和主题: starter/themes.md 30 | - 依赖项(一):基本配置: guides/dms.md 31 | - 依赖项(二):本地化: starter/localize_staticfiles.md 32 | - 参数化图表: guides/entity_params.md 33 | - 自定义地图: guides/custom_maps.md 34 | - 高级开发: guides/advance_development.md 35 | - Gallery: gallery.md 36 | - 参考: 37 | - 配置及其访问: reference/configure.md 38 | - 组件存储类: reference/stores.md 39 | - 模板标签库: reference/tags.md 40 | - 视图和模板: reference/views_and_templates.md 41 | - 命令行工具: reference/commands.md 42 | - 设计与实现: reference/system_design.md 43 | - 文章: 44 | - 新的页面功能: posts/new_page_feature.md 45 | - geojson地图: posts/use_custom_geojson.md 46 | - 图表合辑: posts/widget_collection.md 47 | - 第三方HTML渲染库: posts/third_html_library.md 48 | - 与pyecharts: posts/pyecharts_project.md 49 | - 项目开发: 50 | - 更新日志: changelog.md 51 | - 破坏性变更: breaking-change.md 52 | - 关于项目: about.md 53 | - 发布日志: 54 | - v0.6.0: release-note/v060.md 55 | - v0.5.1: release-note/v051.md 56 | - v0.5.0: release-note/v050.md -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | typing_extensions~=4.5 2 | borax~=4.0 3 | htmlgenerator~=1.2 -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | typing_extensions~=4.5 2 | pyecharts~=2.0 3 | borax~=4.0 4 | django~=3.2 5 | htmlgenerator~=1.2 6 | flake8~=3.9 7 | mccabe~=0.6 -------------------------------------------------------------------------------- /requirements_doc.txt: -------------------------------------------------------------------------------- 1 | mkdocs-material==8.3.3 2 | mdx-truly-sane-lists==1.3 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E743,E501,F401,E126 3 | max-complexity = 18 4 | exclude = .git,__pycache__,docs/source/conf.py,old,build,dist -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding=utf8 2 | 3 | import pathlib 4 | import re 5 | 6 | from setuptools import setup, find_packages 7 | 8 | here = pathlib.Path(__file__).parent 9 | txt = (here / 'django_echarts' / '__init__.py').read_text() 10 | __version__ = re.findall(r"^__version__ = '([^']+)'\r?$", txt, re.M)[0] 11 | 12 | lib_classifiers = [ 13 | "Development Status :: 4 - Beta", 14 | "Programming Language :: Python :: 3", 15 | "Programming Language :: Python :: 3.7", 16 | "Programming Language :: Python :: 3.8", 17 | "Programming Language :: Python :: 3.9", 18 | "Programming Language :: Python :: 3.10", 19 | "Programming Language :: Python :: 3 :: Only", 20 | "Framework :: Django", 21 | "Framework :: Django :: 2.0", 22 | "Framework :: Django :: 2.1", 23 | "Framework :: Django :: 2.2", 24 | "Framework :: Django :: 3.0", 25 | "Framework :: Django :: 3.1", 26 | "Framework :: Django :: 3.2", 27 | "Framework :: Django :: 4.0", 28 | "Intended Audience :: Developers", 29 | "License :: OSI Approved :: MIT License", 30 | "Topic :: Software Development :: Libraries", 31 | "Topic :: Utilities", 32 | 'Operating System :: OS Independent' 33 | ] 34 | 35 | with open('long_description.md', encoding='utf8') as f: 36 | long_description = f.read() 37 | 38 | setup( 39 | name='django-echarts', 40 | version=__version__, 41 | packages=find_packages(exclude=['tests']), 42 | url='https://github.com/kinegratii/django-echarts', 43 | include_package_data=True, 44 | license='MIT', 45 | author='kinegratii', 46 | author_email='kinegratii@gmail.com', 47 | description='A visual site scaffold based on pyecharts and django.', 48 | classifiers=lib_classifiers, 49 | python_requires='>=3.7', 50 | install_requires=[ 51 | 'borax>=3.5.3', 52 | 'typing_extensions', 53 | 'htmlgenerator~=1.2' 54 | ], 55 | long_description=long_description, 56 | long_description_content_type='text/markdown' 57 | ) 58 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/django-echarts/12de6cf4faab1ecbbc06bd487727cc16791ee556/tests/__init__.py -------------------------------------------------------------------------------- /tests/settings_mock.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for zinc project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2.11. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/ref/settings/ 11 | """ 12 | import os 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | # Quick-start development settings - unsuitable for production 19 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 20 | 21 | # SECURITY WARNING: keep the secret key used in production secret! 22 | SECRET_KEY = 'django-insecure-nh&ow1+n($82a5=3*qg_8=i0!h=j=y20wdzo6)h=^*5j*px#)k' 23 | 24 | # SECURITY WARNING: don't run with debug turned on in production! 25 | DEBUG = True 26 | 27 | ALLOWED_HOSTS = ['*'] 28 | 29 | # Application definition 30 | 31 | INSTALLED_APPS = [ 32 | 'django.contrib.admin', 33 | 'django.contrib.auth', 34 | 'django.contrib.contenttypes', 35 | 'django.contrib.sessions', 36 | 'django.contrib.messages', 37 | 'django.contrib.staticfiles', 38 | 'django_echarts', 39 | 'django_echarts.contrib.bootstrap5', 40 | ] 41 | 42 | MIDDLEWARE = [ 43 | 'django.middleware.security.SecurityMiddleware', 44 | 'django.contrib.sessions.middleware.SessionMiddleware', 45 | 'django.middleware.common.CommonMiddleware', 46 | 'django.middleware.csrf.CsrfViewMiddleware', 47 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 48 | 'django.contrib.messages.middleware.MessageMiddleware', 49 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 50 | ] 51 | 52 | ROOT_URLCONF = 'zinc.urls' 53 | 54 | TEMPLATES = [ 55 | { 56 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 57 | 'DIRS': [BASE_DIR / 'templates'], 58 | 'APP_DIRS': True, 59 | 'OPTIONS': { 60 | 'context_processors': [ 61 | 'django.template.context_processors.debug', 62 | 'django.template.context_processors.request', 63 | 'django.contrib.auth.context_processors.auth', 64 | 'django.contrib.messages.context_processors.messages', 65 | ], 66 | }, 67 | }, 68 | ] 69 | 70 | WSGI_APPLICATION = 'zinc.wsgi.application' 71 | 72 | # Database 73 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 74 | 75 | 76 | # Password validation 77 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 78 | 79 | AUTH_PASSWORD_VALIDATORS = [ 80 | { 81 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 82 | }, 83 | { 84 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 85 | }, 86 | { 87 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 88 | }, 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 91 | }, 92 | ] 93 | 94 | # Internationalization 95 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 96 | 97 | LANGUAGE_CODE = 'zh-Hans' 98 | 99 | TIME_ZONE = 'UTC' 100 | 101 | USE_I18N = True 102 | 103 | USE_L10N = True 104 | 105 | USE_TZ = True 106 | 107 | # Static files (CSS, JavaScript, Images) 108 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 109 | 110 | STATIC_URL = '/static/' 111 | STATICFILES_DIRS = ( 112 | os.path.join(BASE_DIR, 'static').replace('\\', '/'), 113 | ) 114 | 115 | # Default primary key field type 116 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 117 | 118 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 119 | 120 | LOGGING_CONFIG = {} 121 | 122 | LOGGING = {} 123 | -------------------------------------------------------------------------------- /tests/test_dms.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from collections import namedtuple 3 | from unittest.mock import patch 4 | 5 | from django_echarts.core.settings_store import SettingsStore, DJEOpts 6 | 7 | MockAppConfig = namedtuple('MockAppConfig', 'name') 8 | 9 | 10 | class DMSFromSettingsTestCase(unittest.TestCase): 11 | @patch('django.apps.apps.get_app_configs') 12 | def test_custom_url_settings(self, get_func): 13 | get_func.return_value = [MockAppConfig('django_echarts.contrib.bootstrap5')] 14 | target_store = SettingsStore( 15 | echarts_settings=DJEOpts(dms_repo='pyecharts', theme_app='django_echarts.contrib.bootstrap5', dep2url={ 16 | 'foo': 'https://foo.icc/foo.js', 17 | 'zzz': '#local', 18 | 'china': '#local', 19 | 'err': '#err', 20 | '#local': ['c1', 'c2'] 21 | }), 22 | extra_settings={ 23 | 'STATIC_URL': '/static/' 24 | } 25 | ) 26 | 27 | self.assertEqual('/static/assets/zzz.js', target_store.resolve_url('zzz')) 28 | self.assertEqual('/static/assets/c1.js', target_store.resolve_url('c1')) 29 | self.assertEqual('/static/assets/c2.js', target_store.resolve_url('c2')) 30 | self.assertEqual('/static/assets/maps/china.js', target_store.resolve_url('china')) 31 | -------------------------------------------------------------------------------- /tests/test_factory.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from django_echarts.entities import (Title, ChartInfo, WidgetCollection) 4 | from django_echarts.entities.uri import EntityURI, ParamsConfig 5 | from django_echarts.stores.entity_factory import EntityFactory 6 | 7 | 8 | class MockChart: 9 | def __init__(self, width, height): 10 | self.width = width 11 | self.height = height 12 | 13 | 14 | class EntityFactoryTestCase(unittest.TestCase): 15 | def test_factory(self): 16 | my_factory = EntityFactory() 17 | my_factory.register_html_widget(Title('Demo1'), name='demo1') 18 | self.assertEqual('Demo1', my_factory.get_widget_by_name('demo1').text) 19 | self.assertEqual('Demo1', my_factory.get_html_widget('demo1').text) 20 | my_factory.register_chart_widget(MockChart('100%', '500px'), name='chart1') 21 | info = my_factory.get_widget_by_name('info:chart1') 22 | self.assertTrue(isinstance(info, ChartInfo)) 23 | self.assertEqual('100%', my_factory.get_widget_by_name('chart1').width) 24 | self.assertEqual('100%', my_factory.get_chart_widget('chart1').width) 25 | self.assertEqual(1, my_factory.get_chart_total()) 26 | self.assertEqual(None, my_factory.get_widget_by_name('error')) 27 | 28 | chart1, exits, info1 = my_factory.get_chart_and_info('chart1') 29 | self.assertTrue(isinstance(info1, ChartInfo)) 30 | 31 | my_factory.register_chart_widget(MockChart('100%', '400px'), name='chart2', info=False) 32 | self.assertEqual(None, my_factory.get_widget_by_name('info:chart2')) 33 | 34 | _, exits2, _ = my_factory.get_chart_and_info('chart3') 35 | self.assertEqual(False, exits2) 36 | 37 | wc = WidgetCollection('test') 38 | wc.add_chart_widget('chart1') 39 | wc.add_html_widget(['demo1']) 40 | wc.auto_mount(my_factory) 41 | 42 | def test_factory_uri_methods(self): 43 | factory2 = EntityFactory() 44 | 45 | @factory2.register_chart_widget 46 | def chart1(): 47 | pass 48 | 49 | @factory2.register_chart_widget(info=ChartInfo('chart2')) 50 | def chart2(year: int): 51 | pass 52 | 53 | @factory2.register_chart_widget(info=ChartInfo('chart3', params_config=ParamsConfig({'year': [2022, 2023]}))) 54 | def chart3(year: int): 55 | return MockChart(100, 100) 56 | 57 | uri_list = list(factory2.get_all_chart_uri()) 58 | uri_string_list = [str(uri) for uri in uri_list] 59 | self.assertEqual(3, len(uri_list)) 60 | self.assertIn('chart:chart3/year/2022', uri_string_list) 61 | self.assertFalse(any(['chart2' in s for s in uri_string_list])) 62 | 63 | widget = factory2.get_widget_by_uri(EntityURI.from_str('chart:chart3/year/2022')) 64 | self.assertTrue(isinstance(widget, MockChart)) 65 | info = factory2.get_widget_by_uri(EntityURI.from_str('info:chart3/year/2022')) 66 | self.assertTrue(isinstance(info, ChartInfo)) 67 | -------------------------------------------------------------------------------- /tests/test_hosts.py: -------------------------------------------------------------------------------- 1 | # coding=utf8 2 | 3 | 4 | import unittest 5 | 6 | from django_echarts.core.dms import DependencyManager 7 | 8 | 9 | class DependencyManagerTestCase(unittest.TestCase): 10 | def test_lib_host(self): 11 | # Basic tests 12 | m_context = { 13 | 'STATIC_URL': '/static/', 14 | 'echarts_version': '4.8.0', 15 | 'baidu_map_ak': 'foo23zoo' 16 | } 17 | manager = DependencyManager.create_default( 18 | context=m_context, 19 | repo_name='bootcdn', 20 | ) 21 | manager.add_repo('bootcdn', repo_url='https://cdn.bootcss.com/echarts/{echarts_version}/') 22 | 23 | self.assertEqual( 24 | 'https://cdn.bootcss.com/echarts/4.8.0/echarts.min.js', 25 | manager.resolve_url('echarts.min') 26 | ) 27 | self.assertEqual('https://api.map.baidu.com/api?v=2.0&ak=foo23zoo', 28 | manager.resolve_url('https://api.map.baidu.com/api?v=2.0&ak=xxx')) 29 | 30 | def test_map_host(self): 31 | m_context = { 32 | 'STATIC_URL': '/static/', 33 | 'echarts_version': '4.8.0' 34 | } 35 | manager = DependencyManager.create_default( 36 | context=m_context, 37 | ) 38 | manager.add_repo('china-provinces', repo_url='https://echarts-maps.github.io/echarts-china-provinces-js/') 39 | self.assertEqual( 40 | 'https://echarts-maps.github.io/echarts-china-provinces-js/maps/china.js', 41 | manager.resolve_url('china', repo_name='china-provinces') 42 | ) 43 | # Add 44 | manager.add_repo('amap', 'https://amap.com/js') 45 | self.assertEqual( 46 | 'https://amap.com/js/fujian.js', 47 | manager.resolve_url('fujian', 'amap') 48 | ) 49 | 50 | def test_custom_d2u(self): 51 | m_context = { 52 | 'STATIC_URL': '/static/', 53 | 'echarts_version': '4.8.0', 54 | 'baidu_map_ak': 'foo' 55 | } 56 | manager = DependencyManager.create_default( 57 | context=m_context 58 | ) 59 | manager.load_from_dep2url_dict({ 60 | 'baidu_map_api': 'https://api.map.baidu.com/api?v=2.0&ak={baidu_map_ak}', 61 | 'baidu_map_script': 'https://api.map.baidu.com/getscript?v=2.0&ak={baidu_map_ak}' 62 | }) 63 | self.assertEqual('https://api.map.baidu.com/api?v=2.0&ak=foo', manager.resolve_url('baidu_map_api')) 64 | 65 | 66 | class CustomHostTestCase(unittest.TestCase): 67 | def test_add_host(self): 68 | m_context = { 69 | 'echarts_version': '4.8.0' 70 | } 71 | 72 | manager = DependencyManager.create_default( 73 | context=m_context, 74 | repo_name='pyecharts' 75 | ) 76 | manager.add_repo('demo', '/demo/') 77 | manager.add_repo('demo2', '/demo2/{echarts_version}') 78 | self.assertEqual( 79 | '/demo/fujian.js', 80 | manager.resolve_url('fujian', repo_name='demo') 81 | ) 82 | self.assertEqual( 83 | '/demo2/4.8.0/fujian.js', 84 | manager.resolve_url('fujian', repo_name='demo2') 85 | ) 86 | -------------------------------------------------------------------------------- /tests/test_interface.py: -------------------------------------------------------------------------------- 1 | # coding=utf8 2 | 3 | import unittest 4 | 5 | from unittest.mock import MagicMock 6 | 7 | from django_echarts.renders import get_js_dependencies, flat_chart 8 | 9 | 10 | class MockCharts: 11 | """ 12 | A mock class for pyecharts.base.Base and pyecharts.custom.page.Page. 13 | """ 14 | 15 | def __init__(self, js_dependencies): 16 | self.js_dependencies = MagicMock(items=js_dependencies) 17 | 18 | 19 | @flat_chart.register(MockCharts) 20 | def flat_mock(widget): 21 | return [widget] 22 | 23 | 24 | class JsMergeTestCase(unittest.TestCase): 25 | BASE_CHART = MockCharts(['echarts']) 26 | MAP_CHART = MockCharts(['echarts', 'fujian']) 27 | THREE_D_CHART = MockCharts(['echarts', 'echartsgl']) 28 | 29 | def test_one_chart_or_page(self): 30 | # One chart or one page 31 | self.assertListEqual( 32 | ['echarts'], 33 | get_js_dependencies(self.BASE_CHART) 34 | ) 35 | self.assertListEqual( 36 | ['echarts', 'fujian'], 37 | get_js_dependencies(self.MAP_CHART) 38 | ) 39 | 40 | def test_multiple_charts_and_pages(self): 41 | # Multiple charts 42 | self.assertListEqual( 43 | ['echarts', 'fujian'], 44 | get_js_dependencies([self.BASE_CHART, self.MAP_CHART]) 45 | ) 46 | self.assertListEqual( 47 | ['echarts', 'echartsgl'], 48 | get_js_dependencies([self.BASE_CHART, self.THREE_D_CHART]) 49 | ) 50 | self.assertListEqual( 51 | ['echarts', 'echartsgl', 'fujian'], 52 | get_js_dependencies([self.MAP_CHART, self.THREE_D_CHART]) 53 | ) 54 | self.assertListEqual( 55 | ['echarts', 'echartsgl', 'fujian'], 56 | get_js_dependencies([self.BASE_CHART, self.MAP_CHART, self.THREE_D_CHART]) 57 | ) 58 | 59 | def test_string_dependencies(self): 60 | self.assertListEqual( 61 | ['echarts', 'echartsgl'], 62 | get_js_dependencies(['echartsgl', self.BASE_CHART]) 63 | ) 64 | -------------------------------------------------------------------------------- /tests/test_layouts.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from django_echarts.entities import RowContainer, HTMLBase 4 | from django_echarts.entities.layouts import any2layout 5 | 6 | 7 | class MockWidget(HTMLBase): 8 | pass 9 | 10 | 11 | class LayoutOptsTestCase(unittest.TestCase): 12 | def test_create_layout_opts(self): 13 | lto = any2layout('l8') 14 | self.assertEqual('l', lto.pos) 15 | self.assertListEqual([8], lto.spans) 16 | self.assertEqual(False, lto.start) 17 | 18 | with self.assertRaises(ValueError): 19 | any2layout('e4') 20 | 21 | lto2 = any2layout(9) 22 | self.assertListEqual([9], lto2.spans) 23 | 24 | lto3 = any2layout([4, 8]) 25 | self.assertListEqual([4, 8], lto3.spans) 26 | 27 | s_lto3 = lto.stripped_layout() 28 | self.assertEqual('r', s_lto3.pos) 29 | self.assertListEqual([8], s_lto3.spans) 30 | 31 | 32 | class RowContainerTestCase(unittest.TestCase): 33 | def test_row_container_layout(self): 34 | rc = RowContainer() 35 | rc.add_widget(MockWidget(), span=2) 36 | rc.add_widget(MockWidget(), span=10) 37 | rc.auto_layout() 38 | self.assertTupleEqual((2, 10), rc.get_spans()) 39 | 40 | rc2 = RowContainer() 41 | rc2.add_widget(MockWidget(), span=2) 42 | rc2.add_widget(MockWidget()) 43 | rc2.auto_layout() 44 | self.assertTupleEqual((2, 10), rc.get_spans()) 45 | 46 | def test_set_spans(self): 47 | rc = RowContainer() 48 | rc.add_widget(MockWidget(), span=2) 49 | rc.add_widget(MockWidget(), span=10) 50 | rc.set_spans([4, 8]) 51 | self.assertTupleEqual((4, 8), rc.get_spans()) 52 | rc.set_spans(6) 53 | self.assertTupleEqual((6, 6), rc.get_spans()) 54 | -------------------------------------------------------------------------------- /tests/test_mcharts.py: -------------------------------------------------------------------------------- 1 | # coding=utf8 2 | """ 3 | Test Cases for NamedCharts 4 | """ 5 | 6 | import unittest 7 | 8 | from django_echarts.entities import NamedCharts, BlankChart 9 | 10 | 11 | class MChartsTestCase(unittest.TestCase): 12 | def test_create_with_kwargs(self): 13 | nc = NamedCharts().add_widget( 14 | BlankChart(page_title='Line-Chart'), name='line' 15 | ).add_widget( 16 | BlankChart(page_title='Bar-Chart') 17 | ).add_widget( 18 | BlankChart(page_title='Map-Chart'), name='map' 19 | ) 20 | self.assertEqual('Line-Chart', nc['line'].page_title) 21 | self.assertEqual('Bar-Chart', nc['c1'].page_title) 22 | self.assertListEqual( 23 | ['Line-Chart', 'Bar-Chart', 'Map-Chart'], 24 | [c.page_title for c in nc] 25 | ) 26 | with self.assertRaises(KeyError): 27 | c = nc['no_charts'] 28 | print(c.page_title) 29 | self.assertEqual('Bar-Chart', nc[1].page_title) 30 | 31 | 32 | class MChartsPY36TestCase(unittest.TestCase): 33 | def test_create_with_kwargs(self): 34 | nc = NamedCharts() 35 | nc.add_widget(BlankChart(page_title='Bar-Chart'), name='bar') 36 | nc.add_widget(BlankChart(page_title='Line-Chart'), name='line') 37 | nc.add_chart(BlankChart(page_title='Map-Chart'), name='map') 38 | self.assertEqual('Bar-Chart', nc['bar'].page_title) 39 | self.assertListEqual( 40 | ['Bar-Chart', 'Line-Chart', 'Map-Chart'], 41 | [c.page_title for c in nc] 42 | ) 43 | -------------------------------------------------------------------------------- /tests/test_section_counter.py: -------------------------------------------------------------------------------- 1 | # coding=utf8 2 | 3 | 4 | import unittest 5 | import random 6 | 7 | from django_echarts.datatools.section_counter import BValueIndex, BRangeIndex, BSectionIndex, BSectionCounter 8 | 9 | 10 | class BRangeIndexTestCase(unittest.TestCase): 11 | def test_basic(self): 12 | ri = BRangeIndex(3, 7) 13 | self.assertFalse(2 in ri) 14 | self.assertTrue(5 in ri) 15 | self.assertEqual('3~7', str(ri)) 16 | 17 | ri = BRangeIndex(2) 18 | self.assertTrue(2 in ri) 19 | self.assertTrue(1000 in ri) 20 | self.assertEqual('≥2', str(ri)) 21 | 22 | ri = BRangeIndex(upper=23) 23 | self.assertTrue(22 in ri) 24 | self.assertFalse(23 in ri) 25 | self.assertEqual('<23', str(ri)) 26 | 27 | 28 | class BRangeCounterTestCase(unittest.TestCase): 29 | def test_value_indexes(self): 30 | rc = BSectionCounter( 31 | BValueIndex(1), 32 | BValueIndex(2), 33 | BValueIndex(3) 34 | ) 35 | c = rc.feed([1, 2, 3, 1, 2, 3, 1, 2, 2, 3, 3, 3, 5, 6]) 36 | self.assertEqual(3, c['1']) 37 | self.assertEqual(4, c['2']) 38 | self.assertEqual(5, c['3']) 39 | 40 | def test_basic_usage(self): 41 | rc = BSectionCounter( 42 | BRangeIndex(0, 50), 43 | BRangeIndex(50, 100), 44 | BRangeIndex(100) 45 | ) 46 | c = rc.feed([0, 12, 23, 34, 50, 60, 67, 100]) # 4,3,1 47 | self.assertEqual(4, c['0~50']) 48 | self.assertEqual(3, c['50~100']) 49 | self.assertEqual(1, c['≥100']) 50 | 51 | def test_simple_counter(self): 52 | rc = BSectionCounter.from_simple([0, 50], [50, 100], [100, None]) 53 | c = rc.feed([0, 12, 23, 34, 50, 60, 67, 100]) # 4,3,1 54 | self.assertEqual(4, c['0~50']) 55 | self.assertEqual(3, c['50~100']) 56 | self.assertEqual(1, c['≥100']) 57 | 58 | def test_feed_as_axis(self): 59 | rc = BSectionCounter.from_simple([0, 50], [50, 100], [100, None]) 60 | a, b = rc.feed_as_axises([0, 12, 23, 34, 50, 60, 67, 100]) # 4,3,1 61 | self.assertTupleEqual(('0~50', '50~100', '≥100'), a) 62 | self.assertTupleEqual((4, 3, 1), b) 63 | 64 | def test_missing_index(self): 65 | rc = BSectionCounter( 66 | BRangeIndex(None, 0), 67 | BRangeIndex(0, 50), 68 | BRangeIndex(50, 100), 69 | BRangeIndex(100) 70 | ) 71 | c = rc.feed([0, 12, 23, 34, 50, 60, 67, 100]) # 4,3,1 72 | self.assertEqual(0, c['<0']) 73 | 74 | 75 | class BSectionCounterTestCase(unittest.TestCase): 76 | def test_section_counter(self): 77 | rc = BSectionCounter( 78 | BValueIndex(0), 79 | BSectionIndex(1, 50), 80 | BSectionIndex(51, 99), 81 | BValueIndex(100) 82 | ) 83 | test_data = [0, 12, 23, 34, 50, 60, 67, 99, 100] 84 | c = rc.feed(test_data) 85 | self.assertEqual(1, c['0']) 86 | self.assertEqual(4, c['1~50']) 87 | self.assertEqual(3, c['51~99']) 88 | self.assertEqual(1, c['100']) 89 | 90 | 91 | class ComprehensiveTestCase(unittest.TestCase): 92 | def test_counter_with_section_indexes(self): 93 | source_data = [random.choice(range(1500)) for _ in range(2000)] 94 | 95 | # Test Using len Counter 96 | sizes = [ 97 | len([pp for pp in source_data if pp == 0]), 98 | len([pp for pp in source_data if 1 <= pp <= 10]), 99 | len([pp for pp in source_data if 11 <= pp <= 50]), 100 | len([pp for pp in source_data if 51 <= pp <= 100]), 101 | len([pp for pp in source_data if 101 <= pp <= 500]), 102 | len([pp for pp in source_data if 501 <= pp <= 1000]), 103 | len([pp for pp in source_data if pp >= 1001]) 104 | ] 105 | 106 | # Test Using section counter 107 | rc1 = BSectionCounter( 108 | BValueIndex(0), 109 | BSectionIndex(1, 10), 110 | BSectionIndex(11, 50), 111 | BSectionIndex(51, 100), 112 | BSectionIndex(101, 500), 113 | BSectionIndex(501, 1000), 114 | BSectionIndex(1001) 115 | ) 116 | ls, cs = rc1.feed_as_axises(source_data) 117 | self.assertSequenceEqual(sizes, cs) 118 | -------------------------------------------------------------------------------- /tests/test_tms.py: -------------------------------------------------------------------------------- 1 | from django_echarts.core.tms import parse_theme_label, ThemeManager 2 | 3 | import unittest 4 | 5 | 6 | class ThemeLabelParseTestCase(unittest.TestCase): 7 | 8 | def test_parse_label(self): 9 | self.assertEqual(parse_theme_label('bootstrap5'), ('bootstrap5', 'bootstrap5', '', False)) 10 | self.assertEqual(parse_theme_label('bootstrap5#local'), ('bootstrap5', 'bootstrap5', '', True)) 11 | self.assertEqual(parse_theme_label('bootstrap5.foo#local'), ('bootstrap5.foo', 'bootstrap5', 'foo', True)) 12 | 13 | 14 | class ThemeWithCustomUrlTestCase(unittest.TestCase): 15 | def test_custom_url(self): 16 | manager = ThemeManager.create_from_module('django_echarts.contrib.bootstrap3', d2u={ 17 | 'bootstrap3': {'main_css': '/my/bootstrap3.min.css'}, 18 | 'bootstrap3.foo': {'palette_css': '/my/bootstrap3.foo.min.css'} 19 | }) 20 | 21 | theme1 = manager.create_theme('bootstrap3') 22 | css_urls = theme1.css_urls 23 | self.assertIn('/my/bootstrap3.min.css', css_urls) 24 | 25 | theme2 = manager.create_theme('bootstrap3.foo') 26 | css_urls2 = theme2.css_urls 27 | self.assertIn('/my/bootstrap3.foo.min.css', css_urls2) 28 | 29 | def test_incorrect_usage(self): 30 | manager = ThemeManager.create_from_module('django_echarts.contrib.bootstrap3', d2u={ 31 | 'bootstrap3': {'palette_css': '/my/bootstrap3.min.css'}, 32 | 'bootstrap3.foo': {'main_css': '/my/bootstrap3.foo.min.css'} 33 | }) 34 | 35 | theme1 = manager.create_theme('bootstrap3') 36 | css_urls = theme1.css_urls 37 | self.assertNotIn('/my/bootstrap3.min.css', css_urls) 38 | 39 | theme2 = manager.create_theme('bootstrap3.foo') 40 | css_urls2 = theme2.css_urls 41 | self.assertNotIn('/my/bootstrap3.foo.min.css', css_urls2) 42 | 43 | 44 | class ThemeLocalTestCase(unittest.TestCase): 45 | def test_local_theme(self): 46 | pass 47 | -------------------------------------------------------------------------------- /tests/test_uri.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from django_echarts.entities.uri import EntityURI, parse_params_choices 3 | 4 | 5 | class EntityURITestCase(unittest.TestCase): 6 | def test_parse(self): 7 | mya_uri = EntityURI.from_params_path('chart', 'demo', 'year/2021/tag/hello/django-echarts', 8 | param_names=['year', 'tag']) 9 | self.assertDictEqual({'year': '2021', 'tag': 'hello/django-echarts'}, mya_uri.params) 10 | mya_uri2 = EntityURI.from_params_path('chart', 'demo', 'year/2021/tag/hello/django-echarts/', 11 | param_names=['year', 'tag']) 12 | self.assertDictEqual({'year': '2021', 'tag': 'hello/django-echarts/'}, mya_uri2.params) 13 | 14 | def test_full_str(self): 15 | my_uri = EntityURI.from_str('chart:name1') 16 | self.assertEqual('chart', my_uri.catalog) 17 | self.assertEqual('name1', my_uri.name) 18 | self.assertEqual(0, len(my_uri.params)) 19 | 20 | uri_with_params = EntityURI.from_str('chart:name1/year/2022') 21 | self.assertEqual('chart', uri_with_params.catalog) 22 | self.assertEqual('name1', uri_with_params.name) 23 | self.assertEqual('2022', uri_with_params.params['year']) 24 | 25 | def test_custom_catalog(self): 26 | uri_with_params = EntityURI.from_str('name1/year/2022', catalog='chart') 27 | self.assertEqual('chart', uri_with_params.catalog) 28 | self.assertEqual('name1', uri_with_params.name) 29 | self.assertEqual('2022', uri_with_params.params['year']) 30 | 31 | 32 | class ParamsConfigTestCase(unittest.TestCase): 33 | def test_parse_dict(self): 34 | result = list(parse_params_choices({'year': [2021, 2022], 'month': [1, 2]})) 35 | self.assertEqual(4, len(result)) 36 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from django_echarts.utils.burl import burl_kwargs, burl_args, BUrl 3 | from django_echarts.utils.lazy_dict import LazyDict, ParameterMissingError 4 | 5 | 6 | class BurlTestCase(unittest.TestCase): 7 | def test_burl(self): 8 | self.assertEqual( 9 | '/foo/?p=1&q=test', 10 | burl_args('/foo/', 'p', 1, 'q', 'test') 11 | ) 12 | self.assertEqual('/foo/?p=1&q=test', burl_kwargs('/foo/', p=1, q='test')) 13 | self.assertEqual('/foo/?p=1&q=test', burl_kwargs('/foo/?p=1', q='test')) 14 | url = BUrl('/foo/') 15 | url.append('p', 1) 16 | url.append('z', 'zoo') 17 | url.delete('z') 18 | self.assertEqual('/foo/?p=1', url.url) 19 | 20 | 21 | class LazyDictTestCase(unittest.TestCase): 22 | def test_lazy_dict(self): 23 | l_dic = LazyDict() 24 | l_dic.register('value1', 'key1') 25 | l_dic.set_ref('ref1', 'key1') 26 | 27 | self.assertEqual(1, len(l_dic)) 28 | self.assertTrue('key1' in l_dic) 29 | self.assertTrue('value1' == l_dic.get('key1') == l_dic.get('ref1')) 30 | self.assertTrue('key1' == l_dic.actual_key('key1') == l_dic.actual_key('ref1')) 31 | 32 | with self.assertRaises(TypeError): 33 | l_dic.register('error1') 34 | 35 | @l_dic.inject('key1') 36 | def demo(key1): 37 | return key1 38 | 39 | self.assertEqual('value1', demo()) 40 | 41 | def test_func_with_parameters(self): 42 | l_dic = LazyDict() 43 | 44 | @l_dic.register 45 | def func1(year: int): 46 | return year 47 | 48 | @l_dic.register 49 | def func2(year: int, month: int = 2): 50 | pass 51 | 52 | @l_dic.register 53 | def func3(year: int, **kwargs): 54 | pass 55 | 56 | my_params = {'year': '2022'} 57 | 58 | self.assertDictEqual({'year': 2022}, l_dic.validate_caller_params('func1', my_params)[0]) 59 | my_params2 = {'year': 2021} 60 | self.assertDictEqual({'year': 2021}, l_dic.validate_caller_params('func1', my_params2)[0]) 61 | self.assertEqual(2021, l_dic.get('func1', {'year': 2021})) 62 | 63 | self.assertDictEqual({'year': 2021, 'month': 3}, 64 | l_dic.validate_caller_params('func2', {'year': 2021, 'month': 3})[0]) 65 | self.assertDictEqual({'year': 2021, 'month': 2}, l_dic.validate_caller_params('func2', {'year': 2021})[0]) 66 | self.assertDictEqual({'year': 2021, 'month': 2}, 67 | l_dic.validate_caller_params('func3', {'year': 2021, 'month': 2})[0]) 68 | with self.assertRaises(ParameterMissingError): 69 | l_dic.validate_caller_params('func1', {'year': 2021, 'month': 2}) 70 | -------------------------------------------------------------------------------- /tests/test_widgets.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from django_echarts.entities import Nav, LinkItem 4 | 5 | 6 | class NavTestCase(unittest.TestCase): 7 | def test_nav(self): 8 | nav = Nav() 9 | nav.add_left_menu('Menu1', slug='menu1', url='/menu1') 10 | nav.add_item_in_left_menu('Menu1', LinkItem('sub1', '/sub/', after_separator=True)) 11 | 12 | self.assertEqual(1, len(nav.menus)) 13 | self.assertEqual('sub1', nav.menus[0].children[0].text) 14 | self.assertEqual(True, nav.menus[0].children[0].after_separator) 15 | -------------------------------------------------------------------------------- /validate_release.py: -------------------------------------------------------------------------------- 1 | """Validate whl file by comparing with source files.""" 2 | import os 3 | import zipfile 4 | import pathlib 5 | import re 6 | import sys 7 | 8 | 9 | def get_version(): 10 | here = pathlib.Path(__file__).parent 11 | txt = (here / 'django_echarts' / '__init__.py').read_text() 12 | __version__ = re.findall(r"^__version__ = '([^']+)'\r?$", txt, re.M)[0] 13 | return __version__ 14 | 15 | 16 | def source_file_list(): 17 | filenames = [] 18 | for cur_dir, dirs, files in os.walk("django_echarts"): 19 | for file in files: 20 | fs = os.path.join(cur_dir, file) 21 | if fs.endswith('.pyc'): 22 | continue 23 | filenames.append(fs.replace('\\', '/')) 24 | return filenames 25 | 26 | 27 | def zip_file_list(version): 28 | whl_file_path = pathlib.Path('dist') / f'django_echarts-{version}-py3-none-any.whl' 29 | 30 | with zipfile.ZipFile(whl_file_path) as archive: 31 | zip_filenames = [zinfo.filename for zinfo in archive.infolist()] 32 | return zip_filenames 33 | 34 | 35 | def validate(version): 36 | s_list = source_file_list() 37 | z_list = zip_file_list(version) 38 | 39 | missing_files = list(set(s_list) - set(z_list)) 40 | if len(missing_files) == 0: 41 | print('Validate success!') 42 | else: 43 | print('Error!The following files are missing:') 44 | for name in missing_files: 45 | print(f'\t{name}') 46 | 47 | 48 | if __name__ == '__main__': 49 | if len(sys.argv) > 1: 50 | version = sys.argv[1] 51 | else: 52 | version = get_version() 53 | print(f'Start validating. Version={version}') 54 | validate(version) 55 | --------------------------------------------------------------------------------