├── docs ├── CNAME ├── adr │ ├── README.md │ └── NNN-Template.md ├── _javascript │ └── tables.js ├── DESIGN.md ├── docs │ ├── CONTRIBUTING.md │ ├── SECURITY.md │ ├── DEVELOPER_GUIDE.md │ └── CODE_OF_CONDUCT.md └── CODE_TAG_SUMMARY.md ├── tests ├── __init__.py ├── data │ ├── README.md │ └── test_utils_static_toc-expected.html ├── examples │ ├── test_video_file.mp4 │ ├── test_write_image_file.png │ ├── ex_app_px.py │ ├── ex_gantt_data.csv │ ├── example_write_from_markdown.md │ ├── readme.py │ ├── ex_gantt_chart.py │ ├── ex_fitted_chart.py │ ├── ex_datatable.py │ ├── ex_pareto_chart.py │ ├── ex_modules_upload.py │ ├── ex_tabs.py │ ├── ex_rolling_chart.py │ ├── ex_multi_page.py │ ├── ex_time_vis_chart.py │ ├── ex_utils_static.py │ ├── ex_marginal_chart.py │ ├── ex_style_bulma.py │ ├── ex_coordinate_chart.py │ └── ex_sqlite_realtime.py ├── test_utils_helpers.py ├── test_custom_colorscales.py ├── test_zz_dash_charts.py ├── test_examples_ex_tabs.py ├── test_examples_ex_px.py ├── test_examples_ex_multi_page.py ├── test_examples_ex_gantt_chart.py ├── test_examples_ex_fitted_chart.py ├── test_examples_ex_pareto_chart.py ├── test_examples_ex_rolling_chart.py ├── test_examples_ex_style_bulma.py ├── test_examples_ex_marginal_chart.py ├── test_examples_ex_modules_upload.py ├── test_examples_ex_time_vis_chart.py ├── test_examples_ex_coordinate_chart.py ├── test_examples_ex_sqlite_realtime.py ├── test_examples_ex_style_bootstrap.py ├── test_examples_ex_datatable.py ├── test_utils_dataset.py ├── conftest.py ├── test_utils_json_cache.py ├── test_utils_fig.py ├── test_examples_ex_utils_static.py ├── test_pareto_chart.py ├── test_utils_static_toc.py ├── configuration.py ├── test_dash_charts_equations.py └── test_utils_data.py ├── .images ├── ex_app_px.png ├── ex_tabs.png ├── ex_upload.png ├── ex_datatable.png ├── ex_gantt_chart.png ├── ex_multi_page.png ├── pareto_readme.png ├── ex_fitted_chart.png ├── ex_pareto_chart.png ├── ex_rolling_chart.png ├── ex_utils_static.png ├── ex_coordinate_chart.png ├── ex_marginal_chart.png ├── ex_sqlite_realtime.gif └── ex_time_vis_chart.png ├── .diagrams ├── dash_charts.png ├── py2puml.ini └── dash_charts.puml ├── dash_charts ├── assets │ ├── README.txt │ ├── favicon.ico │ └── 09_user-styles.css ├── __init__.py ├── custom_colorscales.py ├── utils_static_toc.py ├── utils_helpers.py ├── components.py ├── equations.py ├── utils_app_modules.py ├── utils_callbacks.py ├── pareto_chart.py ├── scatter_line_charts.py └── utils_json_cache.py ├── .sourcery.yaml ├── wip └── source_data │ ├── tor_hexbin_stats.pickle │ └── league_hexbin_stats.pickle ├── .dash_tutorials ├── README.md └── 07_daq-dark-theme.py ├── noxfile.py ├── scripts ├── README.md ├── check_imports.py ├── plot_stale_dependencies.py └── jsonl_viewer.py ├── .deepsource.toml ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── question.md │ ├── feature_request.md │ └── bug_report.md ├── workflows │ └── labels.yml ├── labels.yml └── PULL_REQUEST_TEMPLATE.md ├── README.md ├── .archan.yaml ├── .copier-answers.yml ├── mypy.ini ├── LICENSE ├── .pyup.yml ├── mkdocs.yml ├── .flake8 ├── dodo.py ├── .pre-commit-config.yaml ├── appveyor.yml ├── pyproject.toml ├── .gitignore └── _try_tabulator.py /docs/CNAME: -------------------------------------------------------------------------------- 1 | dash_charts.kyleking.me 2 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """dash_charts test files.""" 2 | -------------------------------------------------------------------------------- /tests/data/README.md: -------------------------------------------------------------------------------- 1 | # Test Data 2 | 3 | Static files used for package tests 4 | -------------------------------------------------------------------------------- /.images/ex_app_px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyleKing/dash_charts/HEAD/.images/ex_app_px.png -------------------------------------------------------------------------------- /.images/ex_tabs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyleKing/dash_charts/HEAD/.images/ex_tabs.png -------------------------------------------------------------------------------- /.images/ex_upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyleKing/dash_charts/HEAD/.images/ex_upload.png -------------------------------------------------------------------------------- /.diagrams/dash_charts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyleKing/dash_charts/HEAD/.diagrams/dash_charts.png -------------------------------------------------------------------------------- /.images/ex_datatable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyleKing/dash_charts/HEAD/.images/ex_datatable.png -------------------------------------------------------------------------------- /.images/ex_gantt_chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyleKing/dash_charts/HEAD/.images/ex_gantt_chart.png -------------------------------------------------------------------------------- /.images/ex_multi_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyleKing/dash_charts/HEAD/.images/ex_multi_page.png -------------------------------------------------------------------------------- /.images/pareto_readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyleKing/dash_charts/HEAD/.images/pareto_readme.png -------------------------------------------------------------------------------- /.images/ex_fitted_chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyleKing/dash_charts/HEAD/.images/ex_fitted_chart.png -------------------------------------------------------------------------------- /.images/ex_pareto_chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyleKing/dash_charts/HEAD/.images/ex_pareto_chart.png -------------------------------------------------------------------------------- /.images/ex_rolling_chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyleKing/dash_charts/HEAD/.images/ex_rolling_chart.png -------------------------------------------------------------------------------- /.images/ex_utils_static.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyleKing/dash_charts/HEAD/.images/ex_utils_static.png -------------------------------------------------------------------------------- /dash_charts/assets/README.txt: -------------------------------------------------------------------------------- 1 | # Favicon from: https://icons8.com/icons/set/web-design by https://icons8.com 2 | -------------------------------------------------------------------------------- /.images/ex_coordinate_chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyleKing/dash_charts/HEAD/.images/ex_coordinate_chart.png -------------------------------------------------------------------------------- /.images/ex_marginal_chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyleKing/dash_charts/HEAD/.images/ex_marginal_chart.png -------------------------------------------------------------------------------- /.images/ex_sqlite_realtime.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyleKing/dash_charts/HEAD/.images/ex_sqlite_realtime.gif -------------------------------------------------------------------------------- /.images/ex_time_vis_chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyleKing/dash_charts/HEAD/.images/ex_time_vis_chart.png -------------------------------------------------------------------------------- /.sourcery.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # See: https://docs.sourcery.ai/Configuration/ 3 | refactor: 4 | python_version: '3.7' 5 | -------------------------------------------------------------------------------- /dash_charts/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyleKing/dash_charts/HEAD/dash_charts/assets/favicon.ico -------------------------------------------------------------------------------- /tests/examples/test_video_file.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyleKing/dash_charts/HEAD/tests/examples/test_video_file.mp4 -------------------------------------------------------------------------------- /tests/examples/test_write_image_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyleKing/dash_charts/HEAD/tests/examples/test_write_image_file.png -------------------------------------------------------------------------------- /wip/source_data/tor_hexbin_stats.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyleKing/dash_charts/HEAD/wip/source_data/tor_hexbin_stats.pickle -------------------------------------------------------------------------------- /wip/source_data/league_hexbin_stats.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyleKing/dash_charts/HEAD/wip/source_data/league_hexbin_stats.pickle -------------------------------------------------------------------------------- /.dash_tutorials/README.md: -------------------------------------------------------------------------------- 1 | # Dash Walk Through 2 | 3 | Example files from completing the Dash user guide at [https://dash.plot.ly/](https://dash.plot.ly/) 4 | -------------------------------------------------------------------------------- /docs/adr/README.md: -------------------------------------------------------------------------------- 1 | # ADR Documentation 2 | 3 | *ADR*: Architectural Design Decision 4 | 5 | ## ADRs 6 | 7 | - ADR 000: [Meta-ADR.md](./000-Meta-ADR.md) 8 | -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | """nox-poetry configuration file.""" 2 | 3 | from calcipy.dev.noxfile import build_check, build_dist, check_safety, coverage, tests # noqa: F401 4 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | # Scripts 2 | 3 | Use this directory for collecting test scripts that utilize the package. Useful for prototyping or creating reference examples 4 | -------------------------------------------------------------------------------- /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | test_patterns = ["tests/**"] 4 | 5 | exclude_patterns = ["docs/**"] 6 | 7 | [[analyzers]] 8 | name = "python" 9 | enabled = true 10 | 11 | [analyzers.meta] 12 | runtime_version = "3.x.x" 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yamllint disable-line rule:line-length 3 | # Configuration: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository 4 | 5 | blank_issues_enabled: false 6 | -------------------------------------------------------------------------------- /docs/_javascript/tables.js: -------------------------------------------------------------------------------- 1 | // From: https://squidfunk.github.io/mkdocs-material/reference/data-tables/ 2 | document$.subscribe(function () { 3 | var tables = document.querySelectorAll("article table") 4 | tables.forEach(function (table) { 5 | new Tablesort(table) 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /dash_charts/__init__.py: -------------------------------------------------------------------------------- 1 | """dash_charts.""" 2 | 3 | from loguru import logger 4 | 5 | __version__ = '0.1.2' 6 | __pkg_name__ = 'dash_charts' 7 | 8 | logger.disable(__pkg_name__) 9 | 10 | # ====== Above is the recommended code from calcipy_template and may be updated on new releases ====== 11 | -------------------------------------------------------------------------------- /docs/DESIGN.md: -------------------------------------------------------------------------------- 1 | # Design and Architecture Notes 2 | 3 | TODO: Work in progress. Also take a look at: https://gist.github.com/jerieljan/4c82515ff5f2b2e4dd5122d354a82b7e 4 | 5 | PlantUML documentation 6 | 7 | ![dash_charts](https://raw.githubusercontent.com/KyleKing/dash_charts/main/.diagrams/dash_charts.png) 8 | -------------------------------------------------------------------------------- /tests/examples/ex_app_px.py: -------------------------------------------------------------------------------- 1 | """Launch app_px.""" 2 | 3 | from dash_charts.app_px import InteractivePXApp 4 | from dash_charts.utils_helpers import parse_dash_cli_args 5 | 6 | instance = InteractivePXApp 7 | app = instance() 8 | app.create() 9 | if __name__ == '__main__': 10 | app.run(**parse_dash_cli_args()) 11 | else: 12 | FLASK_HANDLE = app.get_server() 13 | -------------------------------------------------------------------------------- /tests/test_utils_helpers.py: -------------------------------------------------------------------------------- 1 | """Test utils_helpers.""" 2 | 3 | from dash_charts import utils_helpers 4 | 5 | 6 | def test_graph_return(): 7 | """Test the graph return function.""" 8 | raw_resp = {'A': 1, 'B': 2, 'C': None} 9 | exp_resp = [2, 1] 10 | 11 | result = utils_helpers.graph_return(raw_resp, keys=('B', 'A')) 12 | 13 | assert result == exp_resp 14 | -------------------------------------------------------------------------------- /tests/test_custom_colorscales.py: -------------------------------------------------------------------------------- 1 | """Test custom_colorscales.""" 2 | 3 | from dash_charts import custom_colorscales 4 | 5 | 6 | def test_colorscales(): 7 | """Test the length of the colorscales variables.""" 8 | result = 10 9 | 10 | assert len(custom_colorscales.DEFAULT_PLOTLY_COLORS) == result 11 | assert len(custom_colorscales.DEFAULT_PLOTLY_COLORS_RGB) == result 12 | -------------------------------------------------------------------------------- /tests/test_zz_dash_charts.py: -------------------------------------------------------------------------------- 1 | """Final test alphabetically (zz) to catch general integration cases.""" 2 | 3 | import toml 4 | 5 | from dash_charts import __version__ 6 | 7 | 8 | def test_version(): 9 | """Check that PyProject and __version__ are equivalent.""" 10 | result = toml.load('pyproject.toml')['tool']['poetry']['version'] 11 | 12 | assert result == __version__ 13 | -------------------------------------------------------------------------------- /.diagrams/py2puml.ini: -------------------------------------------------------------------------------- 1 | # puml configuration for py2puml on fork: https://github.com/KyleKing/py-puml-tools/tree/master/py2puml 2 | [puml] 3 | prolog = scale 1 4 | skinparam { 5 | dpi 100 6 | shadowing false 7 | linetype ortho 8 | } 9 | 10 | epilog = 11 | 12 | [methods] 13 | omit-self = True 14 | write-arg-list = True 15 | 16 | [module] 17 | write-globals = True 18 | write-arg-list = True 19 | -------------------------------------------------------------------------------- /tests/test_examples_ex_tabs.py: -------------------------------------------------------------------------------- 1 | """Test the file examples/ex_tabs.py.""" 2 | 3 | import time 4 | 5 | import pytest 6 | 7 | from .configuration import no_log_errors 8 | from .examples import ex_tabs 9 | 10 | 11 | @pytest.mark.INTERACTIVE() 12 | def test_smoke_test_ex_tabs(dash_duo): 13 | """Test ex_tabs.""" 14 | dash_duo.start_server(ex_tabs.app.app) 15 | 16 | time.sleep(1) # act 17 | 18 | assert no_log_errors(dash_duo) 19 | -------------------------------------------------------------------------------- /tests/test_examples_ex_px.py: -------------------------------------------------------------------------------- 1 | """Test the file examples/ex_app_px.py.""" 2 | 3 | import time 4 | 5 | import pytest 6 | 7 | from .configuration import no_log_errors 8 | from .examples import ex_app_px 9 | 10 | 11 | @pytest.mark.INTERACTIVE() 12 | def test_smoke_test_ex_app_px(dash_duo): 13 | """Test ex_app_px.""" 14 | dash_duo.start_server(ex_app_px.app.app) 15 | 16 | time.sleep(1) # act 17 | 18 | assert no_log_errors(dash_duo) 19 | -------------------------------------------------------------------------------- /tests/test_examples_ex_multi_page.py: -------------------------------------------------------------------------------- 1 | """Test the file examples/ex_multi_page.py.""" 2 | 3 | import time 4 | 5 | import pytest 6 | 7 | from .configuration import no_log_errors 8 | from .examples import ex_multi_page 9 | 10 | 11 | @pytest.mark.INTERACTIVE() 12 | def test_smoke_test_ex_multi_page(dash_duo): 13 | """Test ex_multi_page.""" 14 | dash_duo.start_server(ex_multi_page.app.app) 15 | 16 | time.sleep(1) # act 17 | 18 | assert no_log_errors(dash_duo) 19 | -------------------------------------------------------------------------------- /tests/test_examples_ex_gantt_chart.py: -------------------------------------------------------------------------------- 1 | """Test the file examples/ex_gantt_chart.py.""" 2 | 3 | import time 4 | 5 | import pytest 6 | 7 | from .configuration import no_log_errors 8 | from .examples import ex_gantt_chart 9 | 10 | 11 | @pytest.mark.INTERACTIVE() 12 | def test_smoke_test_ex_gantt_chart(dash_duo): 13 | """Test ex_gantt_chart.""" 14 | dash_duo.start_server(ex_gantt_chart.app.app) 15 | 16 | time.sleep(1) # act 17 | 18 | assert no_log_errors(dash_duo) 19 | -------------------------------------------------------------------------------- /tests/test_examples_ex_fitted_chart.py: -------------------------------------------------------------------------------- 1 | """Test the file examples/ex_fitted_chart.py.""" 2 | 3 | import time 4 | 5 | import pytest 6 | 7 | from .configuration import no_log_errors 8 | from .examples import ex_fitted_chart 9 | 10 | 11 | @pytest.mark.INTERACTIVE() 12 | def test_smoke_test_ex_fitted_chart(dash_duo): 13 | """Test ex_fitted_chart.""" 14 | dash_duo.start_server(ex_fitted_chart.app.app) 15 | 16 | time.sleep(1) # act 17 | 18 | assert no_log_errors(dash_duo) 19 | -------------------------------------------------------------------------------- /tests/test_examples_ex_pareto_chart.py: -------------------------------------------------------------------------------- 1 | """Test the file examples/ex_pareto_chart.py.""" 2 | 3 | import time 4 | 5 | import pytest 6 | 7 | from .configuration import no_log_errors 8 | from .examples import ex_pareto_chart 9 | 10 | 11 | @pytest.mark.INTERACTIVE() 12 | def test_smoke_test_ex_pareto_chart(dash_duo): 13 | """Test ex_pareto_chart.""" 14 | dash_duo.start_server(ex_pareto_chart.app.app) 15 | 16 | time.sleep(1) # act 17 | 18 | assert no_log_errors(dash_duo) 19 | -------------------------------------------------------------------------------- /tests/test_examples_ex_rolling_chart.py: -------------------------------------------------------------------------------- 1 | """Test the file examples/ex_rolling_chart.py.""" 2 | 3 | import time 4 | 5 | import pytest 6 | 7 | from .configuration import no_log_errors 8 | from .examples import ex_rolling_chart 9 | 10 | 11 | @pytest.mark.INTERACTIVE() 12 | def test_smoke_test_ex_rolling_chart(dash_duo): 13 | """Test ex_rolling_chart.""" 14 | dash_duo.start_server(ex_rolling_chart.app.app) 15 | 16 | time.sleep(1) # act 17 | 18 | assert no_log_errors(dash_duo) 19 | -------------------------------------------------------------------------------- /tests/test_examples_ex_style_bulma.py: -------------------------------------------------------------------------------- 1 | """Test the file examples/ex_style_bulma.py.""" 2 | 3 | import time 4 | 5 | import pytest 6 | 7 | from .configuration import no_log_errors 8 | from .examples import ex_style_bulma 9 | 10 | 11 | @pytest.mark.INTERACTIVE() 12 | def test_smoke_test_ex_style_bulma(dash_duo): 13 | """Test ex_style_bulma.""" 14 | dash_duo.start_server(ex_style_bulma.app.app) 15 | 16 | time.sleep(1) # act 17 | 18 | assert no_log_errors(dash_duo, ['WARNING']) 19 | -------------------------------------------------------------------------------- /tests/test_examples_ex_marginal_chart.py: -------------------------------------------------------------------------------- 1 | """Test the file examples/ex_marginal_chart.py.""" 2 | 3 | import time 4 | 5 | import pytest 6 | 7 | from .configuration import no_log_errors 8 | from .examples import ex_marginal_chart 9 | 10 | 11 | @pytest.mark.INTERACTIVE() 12 | def test_smoke_test_ex_marginal_chart(dash_duo): 13 | """Test ex_marginal_chart.""" 14 | dash_duo.start_server(ex_marginal_chart.app.app) 15 | 16 | time.sleep(1) # act 17 | 18 | assert no_log_errors(dash_duo) 19 | -------------------------------------------------------------------------------- /tests/test_examples_ex_modules_upload.py: -------------------------------------------------------------------------------- 1 | """Test the file examples/ex_modules_upload.py.""" 2 | 3 | import time 4 | 5 | import pytest 6 | 7 | from .configuration import no_log_errors 8 | from .examples import ex_modules_upload 9 | 10 | 11 | @pytest.mark.INTERACTIVE() 12 | def test_smoke_test_ex_modules_upload(dash_duo): 13 | """Test ex_modules_upload.""" 14 | dash_duo.start_server(ex_modules_upload.app.app) 15 | 16 | time.sleep(1) # act 17 | 18 | assert no_log_errors(dash_duo) 19 | -------------------------------------------------------------------------------- /tests/test_examples_ex_time_vis_chart.py: -------------------------------------------------------------------------------- 1 | """Test the file examples/ex_time_vis_chart.py.""" 2 | 3 | import time 4 | 5 | import pytest 6 | 7 | from .configuration import no_log_errors 8 | from .examples import ex_time_vis_chart 9 | 10 | 11 | @pytest.mark.INTERACTIVE() 12 | def test_smoke_test_ex_time_vis_chart(dash_duo): 13 | """Test ex_time_vis_chart.""" 14 | dash_duo.start_server(ex_time_vis_chart.app.app) 15 | 16 | time.sleep(1) # act 17 | 18 | assert no_log_errors(dash_duo) 19 | -------------------------------------------------------------------------------- /tests/test_examples_ex_coordinate_chart.py: -------------------------------------------------------------------------------- 1 | """Test the file examples/ex_coordinate_chart.py.""" 2 | 3 | import time 4 | 5 | import pytest 6 | 7 | from .configuration import no_log_errors 8 | from .examples import ex_coordinate_chart 9 | 10 | 11 | @pytest.mark.INTERACTIVE() 12 | def test_smoke_test_ex_coordinate_chart(dash_duo): 13 | """Test ex_coordinate_chart.""" 14 | dash_duo.start_server(ex_coordinate_chart.app.app) 15 | 16 | time.sleep(1) 17 | 18 | assert no_log_errors(dash_duo) 19 | -------------------------------------------------------------------------------- /tests/test_examples_ex_sqlite_realtime.py: -------------------------------------------------------------------------------- 1 | """Test the file examples/ex_sqlite_realtime.py.""" 2 | 3 | import time 4 | 5 | import pytest 6 | 7 | from .configuration import no_log_errors 8 | from .examples import ex_sqlite_realtime 9 | 10 | 11 | @pytest.mark.INTERACTIVE() 12 | def test_smoke_test_ex_sqlite_realtime(dash_duo): 13 | """Test ex_sqlite_realtime.""" 14 | dash_duo.start_server(ex_sqlite_realtime.app.app) 15 | 16 | time.sleep(1) # act 17 | 18 | assert no_log_errors(dash_duo) 19 | -------------------------------------------------------------------------------- /tests/test_examples_ex_style_bootstrap.py: -------------------------------------------------------------------------------- 1 | """Test the file examples/ex_style_bootstrap.py.""" 2 | 3 | import time 4 | 5 | import pytest 6 | 7 | from .configuration import no_log_errors 8 | from .examples import ex_style_bootstrap 9 | 10 | 11 | @pytest.mark.INTERACTIVE() 12 | def test_smoke_test_ex_style_bootstrap(dash_duo): 13 | """Test ex_style_bootstrap.""" 14 | dash_duo.start_server(ex_style_bootstrap.app.app) 15 | 16 | time.sleep(1) # act 17 | 18 | assert no_log_errors(dash_duo, ['WARNING']) 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question here (or use the "Github Discussion" instead) 4 | title: '' 5 | labels: 'Type: Documentation, Type: Idea' 6 | assignees: 'kyleking' 7 | 8 | --- 9 | 10 | ## Question 11 | 12 | 13 | 14 | How can I ...? 15 | 16 | Is it possible to ...? 17 | 18 | ## Additional context 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /scripts/check_imports.py: -------------------------------------------------------------------------------- 1 | """Check that all imports work as expected. 2 | 3 | Primarily checking that: 4 | 5 | 1. No optional dependencies are required 6 | 7 | FIXME: Replace with programmatic imports? Maybe explicit imports to check backward compatibility of public API? 8 | https://stackoverflow.com/questions/34855071/importing-all-functions-from-a-package-from-import 9 | 10 | """ 11 | 12 | from pprint import pprint 13 | 14 | # TODO: Replace with imports to test 15 | from dash_charts.components import * # noqa: F401 16 | 17 | pprint(locals()) # noqa: T003 18 | -------------------------------------------------------------------------------- /tests/test_examples_ex_datatable.py: -------------------------------------------------------------------------------- 1 | """Test the file examples/ex_datatable.py.""" 2 | 3 | import time 4 | 5 | import pytest 6 | 7 | from .configuration import no_log_errors 8 | from .examples import ex_datatable 9 | 10 | 11 | @pytest.mark.INTERACTIVE() 12 | def test_smoke_test_ex_datatable(dash_duo): 13 | """Test ex_datatable.""" 14 | pytest.skip('Currently failing until "ids" is fixed') # FIXME: Fix the "ids" error 15 | 16 | dash_duo.start_server(ex_datatable.app.app) 17 | 18 | time.sleep(1) # act 19 | 20 | assert no_log_errors(dash_duo) 21 | -------------------------------------------------------------------------------- /tests/test_utils_dataset.py: -------------------------------------------------------------------------------- 1 | """Test utils_dataset.""" 2 | 3 | import tempfile 4 | from pathlib import Path 5 | 6 | from dash_charts import utils_dataset 7 | 8 | 9 | def test_db_connect(): 10 | """Test DBConnect.""" 11 | with tempfile.TemporaryDirectory() as tmp_dir: 12 | tmp_dir = Path(tmp_dir) 13 | database = utils_dataset.DBConnect(tmp_dir / 'tmp.db') 14 | table = database.db.create_table('test') 15 | csv_filename = tmp_dir / 'tmp.csv' 16 | table.insert({'username': 'username', 'value': 1}) 17 | utils_dataset.export_table_as_csv(csv_filename, table) 18 | database.close() 19 | 20 | result = csv_filename.read_text() 21 | 22 | assert result == 'id,username,value\n1,username,1\n' 23 | -------------------------------------------------------------------------------- /.github/workflows/labels.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Docs: https://github.com/marketplace/actions/github-labeler 3 | 4 | name: github_labeler 5 | 6 | # Run on "push" for first initialization, then only on PR's thereafter 7 | # on: push 8 | on: # yamllint disable-line rule:truthy 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | jobs: 14 | labeler: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v2 19 | - name: Run Labeler 20 | if: success() 21 | uses: crazy-max/ghaction-github-labeler@v3 22 | with: 23 | github-token: ${{ secrets.GITHUB_TOKEN }} 24 | yaml-file: .github/labels.yml 25 | skip-delete: false 26 | dry-run: false 27 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """PyTest configuration.""" 2 | 3 | from pathlib import Path 4 | 5 | import pytest 6 | from calcipy.dev.conftest import pytest_configure # noqa: F401 7 | from calcipy.dev.conftest import pytest_html_results_table_header # noqa: F401 8 | from calcipy.dev.conftest import pytest_html_results_table_row # noqa: F401 9 | from calcipy.dev.conftest import pytest_runtest_makereport # noqa: F401 10 | 11 | from .configuration import TEST_TMP_CACHE, clear_test_cache 12 | 13 | 14 | @pytest.fixture() 15 | def fix_test_cache() -> Path: 16 | """Fixture to clear and return the test cache directory for use. 17 | 18 | Returns: 19 | Path: Path to the test cache directory 20 | 21 | """ 22 | clear_test_cache() 23 | return TEST_TMP_CACHE 24 | -------------------------------------------------------------------------------- /dash_charts/assets/09_user-styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | border: 0; 4 | height: 100%; 5 | min-height: 0; 6 | } 7 | 8 | /* Override bootstrap max-width to better fill the window */ 9 | .container { 10 | max-width: 95%; 11 | } 12 | 13 | /* Add a class specifically for sliders that need additional height */ 14 | .rc-slider-with-legend { 15 | min-height: 45px; 16 | } 17 | 18 | /* FIXME: Implement user-customizable styles 19 | User specifies directory with assets, then the files are copied to the Dash asset dir 20 | as user.js, user.css, etc. */ 21 | iframe#plaid-link-iframe-1 { 22 | height: 100vh; 23 | } 24 | 25 | /* Make sure the date range picker appears above all elements */ 26 | .DateRangePicker_picker { 27 | z-index: 999; 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dash_charts 2 | 3 | > **Note** 4 | > 5 | > In 2019-2020, I was building applications with Dash and wanted a way to make them more composable and reusable. Since then, I moved on to other projects and Dash has become far more composable. Dash expanded plotly express and introduced templates along with other changes as the ecosystem has evolved! 6 | > 7 | > Given the advances in Dash, I'm going to archive this project, publish a final release to PyPi, and move on to other projects 8 | 9 | Python package for Plotly/Dash apps with support for multi-page, modules, and new charts such as Pareto with an Object Orient Approach 10 | 11 | Documentation can be found on [Github (./docs)](./docs), [PyPi](https://pypi.org/project/dash_charts/), or [Hosted](https://dash_charts.kyleking.me/)! 12 | -------------------------------------------------------------------------------- /scripts/plot_stale_dependencies.py: -------------------------------------------------------------------------------- 1 | """Plot the release date of all dependencies.""" 2 | 3 | from pathlib import Path 4 | from typing import Any, Dict 5 | 6 | import pandas as pd 7 | import plotly.express as px 8 | from calcipy.doit_tasks.packaging import _PATH_PACK_LOCK, _read_cache 9 | 10 | path_pack_lock = Path.cwd().parent / 'calcipy' / _PATH_PACK_LOCK.name 11 | cache = _read_cache(path_pack_lock) 12 | 13 | 14 | def unwrap_attr(item: Any) -> Dict[str, Any]: 15 | return { 16 | attrib: item.__getattribute__(attrib) 17 | for attrib in dir(item) if not attrib.startswith('_') 18 | } 19 | 20 | 21 | df_dep = pd.DataFrame(map(unwrap_attr, cache.values())) 22 | 23 | # > fig = px.histogram(df_dep, x='datetime') 24 | fig = px.scatter(df_dep, x='datetime', hover_name='name') 25 | fig.show() 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Let us know if an idea for this project 4 | title: '' 5 | labels: 'Type: Feature, Needs Discussion, Type: Idea' 6 | assignees: KyleKing 7 | 8 | --- 9 | 10 | ## Is your feature request related to a problem? Please describe. 11 | 12 | 13 | 14 | ## Describe the solution you'd like 15 | 16 | 17 | 18 | ## Describe alternatives you've considered 19 | 20 | 21 | 22 | ## Additional context 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Found a problem, us know! 4 | title: '' 5 | labels: 'Type: Bug, Needs Discussion' 6 | assignees: KyleKing 7 | 8 | --- 9 | 10 | ## Describe the bug 11 | 12 | 13 | ### To Reproduce 14 | 15 | 16 | 17 | Steps and/or code snippet(s) to reproduce the behavior: 18 | 19 | 1. ... 20 | 2. ... 21 | 22 | ## Expected behavior 23 | 24 | 25 | 26 | ## Additional Information 27 | 28 | 29 | 30 | - dash_charts Version: 31 | - OS: [e.g. macOS/Windows]: 32 | - OS Version [e.g. Catalina/10]: 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /.archan.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | analysis: 3 | archan_pylint.PylintProvider: 4 | # name: Pylint 5 | arguments: 6 | pylint_args: 7 | - dash_charts 8 | checkers: 9 | archan.CodeClean: 10 | allow_failure: true 11 | arguments: 12 | threshold: 10 13 | dependenpy.InternalDependencies: 14 | # name: Software Architecture 15 | arguments: 16 | packages: 17 | - dash_charts 18 | checkers: 19 | - archan.CompleteMediation 20 | - archan.EconomyOfMechanism: 21 | allow_failure: true 22 | arguments: 23 | simplicity_factor: 2 24 | - archan.LayeredArchitecture 25 | - archan.LeastCommonMechanism: 26 | allow_failure: true 27 | arguments: 28 | independence_factor: 5 29 | Open Design: 30 | Source Code: true 31 | -------------------------------------------------------------------------------- /.copier-answers.yml: -------------------------------------------------------------------------------- 1 | # yamllint disable-file 2 | # Answer file maintained by Copier for: https://github.com/KyleKing/calcipy_template 3 | # Check into version control. Edit by re-running copier and changing responses to the questions 4 | _commit: 0.3.4 5 | _src_path: gh:KyleKing/calcipy_template 6 | author_email: dev.act.kyle@gmail.com 7 | author_name: Kyle King 8 | author_username: kyleking 9 | cname: dash_charts.kyleking.me 10 | codecov_token: d414dacb-5b8d-4c0b-94e1-42fe8393187d 11 | copyright_date: '2021' 12 | doc_dir: docs 13 | install_extras: null 14 | package_name_py: dash_charts 15 | project_description: Python package for Plotly/Dash apps with support for multi-page, 16 | modules, and new charts such as Pareto with an Object Orient Approach 17 | project_name: dash_charts 18 | repository_namespace: kyleking 19 | repository_provider: https://github.com 20 | -------------------------------------------------------------------------------- /tests/examples/ex_gantt_data.csv: -------------------------------------------------------------------------------- 1 | category,label,start,end,progress 2 | Initial Development,Init CustomChart,2019-09-05,2019-10-01,1 3 | Initial Development,Create base line chart,2019-10-05,2019-10-15,1 4 | Initial Development,Initial Release,,2019-10-29, 5 | Test Framework,Reach 100% Coverage,2019-10-15,2020-01-05,0.8 6 | Test Framework,Stable release,,2019-11-20, 7 | Advanced Charts,Add Pareto chart,2019-10-29,2019-11-15,1 8 | Advanced Charts,Advanced Chart Release,,2020-01-01, 9 | Test Framework,Initialize test framework,2019-10-15,2019-11-20,0.74 10 | Test Framework,Full coverage release,,2020-01-05, 11 | Advanced Charts,Add Gantt Chart ;),2019-10-29,2019-12-15,0.9 12 | Initial Development,Add fitted line chart,2019-10-15,2019-10-29,0.9 13 | Advanced Charts,Add TimeVis Chart,2019-11-15,2020-01-01,0 14 | Initial Development,Create scatter chart example,2019-09-15,2019-10-05,1 15 | -------------------------------------------------------------------------------- /tests/test_utils_json_cache.py: -------------------------------------------------------------------------------- 1 | """Test utils_json_cache.""" 2 | 3 | import shutil 4 | 5 | from dash_charts.utils_dataset import DBConnect 6 | from dash_charts.utils_json_cache import ( 7 | CACHE_DIR, get_files_table, initialize_cache, retrieve_cache_object, store_cache_object, 8 | ) 9 | 10 | 11 | def test_utils_json_cache(): 12 | """Test the utils_json_cache helper methods.""" 13 | # Initialize and clear cache for the expected identifier 14 | test_db = DBConnect(CACHE_DIR / '_test_lookup.db') 15 | initialize_cache(test_db) 16 | identifier = 'Test' 17 | get_files_table(test_db).delete(identifier=identifier) 18 | # Save an object to the cache 19 | prefix = 'TestFile' 20 | obj = {'this_is_test': True} 21 | store_cache_object(prefix, identifier, obj, test_db) 22 | 23 | result = retrieve_cache_object(identifier, test_db) 24 | 25 | assert result == obj 26 | test_db.close() 27 | shutil.rmtree(CACHE_DIR) 28 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | # Global options: 2 | 3 | [mypy] 4 | python_version = 3.9 5 | 6 | check_untyped_defs = True 7 | disallow_any_decorated = False 8 | disallow_any_explicit = False 9 | ; disallow_any_expr = True 10 | disallow_any_generics = True 11 | disallow_any_unimported = False 12 | disallow_incomplete_defs = True 13 | disallow_subclassing_any = True 14 | disallow_untyped_calls = True 15 | ; disallow_untyped_decorators = True 16 | disallow_untyped_defs = True 17 | ignore_missing_imports = True 18 | no_implicit_optional = True 19 | ; pretty = True 20 | ; show_column_numbers = True 21 | ; show_error_codes = True 22 | ; show_error_context = True 23 | strict_equality = True 24 | warn_redundant_casts = True 25 | warn_return_any = True 26 | warn_unreachable = True 27 | warn_unused_configs = True 28 | warn_unused_ignores = True 29 | 30 | ; linecoverage_report = coverage_mypy-json 31 | ; linecount_report = coverage_mypy-txt 32 | html_report = releases/tests/mypy_html 33 | ; any_exprs_report = coverage_mypy_any-txt 34 | -------------------------------------------------------------------------------- /tests/data/test_utils_static_toc-expected.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dominate 5 | 6 | 7 | 8 | 9 |
10 |
11 |

Example Header 1

12 |
    13 |
  • H1: Example Header 1
  • 14 |
15 |
    16 |
      17 |
    • H2: Example Header 2
    • 18 |
    19 |
20 |
    21 |
      22 |
    • H2: Example Header 2
    • 23 |
    24 |
25 |
    26 |
      27 |
        28 |
      • H3: Example Header 3
      • 29 |
      30 |
    31 |
32 |
33 |

Example Header 2

34 |

Example Header 2

35 |

Example Header 3

36 |

Example Header 4

37 |
Example Header 5
38 |
Example Header 6
39 |
40 |
41 | 42 | 43 | -------------------------------------------------------------------------------- /tests/test_utils_fig.py: -------------------------------------------------------------------------------- 1 | """Test utils_fig.""" 2 | 3 | import pytest 4 | 5 | from dash_charts.utils_fig import CustomChart 6 | 7 | 8 | class TestChart(CustomChart): # noqa: H601 9 | """Custom chart for testing.""" 10 | 11 | __test__ = False 12 | 13 | 14 | def test_axis_range_property(): 15 | """Test setting the axis_range property on the CustomChart base class.""" 16 | test_chart = TestChart(title='', xlabel='', ylabel='') 17 | pass_range_1 = {'x': [15, 25]} 18 | pass_range_2 = {'x': [15, 25]} 19 | fail_range_1 = {'x': [1, 2, 3]} 20 | fail_message_1 = "Validation of self.axis_range failed: {'x': ['length of list should be 2, it is 3']}" 21 | 22 | with pytest.raises(RuntimeError) as fail_error_1: 23 | test_chart.axis_range = fail_range_1 24 | 25 | assert str(fail_error_1.value) == fail_message_1 26 | test_chart.axis_range = pass_range_1 27 | assert test_chart.axis_range == pass_range_1 28 | test_chart.axis_range = pass_range_2 29 | assert test_chart.axis_range == pass_range_2 30 | -------------------------------------------------------------------------------- /tests/test_examples_ex_utils_static.py: -------------------------------------------------------------------------------- 1 | """Test the file examples/ex_utils_static.py.""" 2 | 3 | import tempfile 4 | from pathlib import Path 5 | 6 | from .examples import ex_utils_static 7 | 8 | 9 | def test_smoke_test_write_plotly_html(): 10 | """Smoke test WritePlotlyHTML.""" 11 | try: 12 | with tempfile.TemporaryDirectory() as dir_name: # act 13 | filename = Path(dir_name) / 'tmp.html' 14 | 15 | ex_utils_static.write_sample_html(filename) # act 16 | 17 | content = filename.read_text() 18 | except ValueError as exc: 19 | raise ValueError( 20 | 'Likely no orca installation was found. Try' 21 | + ' "brew install orca" (and open to finish the installation)' 22 | + ' or "conda install -c plotly plotly-orca" for Windows', 23 | ) from exc 24 | assert len(content.split('\n')) >= 2500 25 | 26 | 27 | def test_smoke_test_write_from_markdown(): 28 | """Smoke test write_from_markdown.""" 29 | ex_utils_static.example_write_from_markdown() # act 30 | -------------------------------------------------------------------------------- /tests/test_pareto_chart.py: -------------------------------------------------------------------------------- 1 | """Test pareto_chart.""" 2 | 3 | import pytest 4 | 5 | from dash_charts import pareto_chart 6 | 7 | 8 | class TestChart(pareto_chart.ParetoChart): # noqa: H601 9 | """Custom chart for testing.""" 10 | 11 | __test__ = False 12 | 13 | 14 | def test_pareto_colors_property(): 15 | """Test setting the pareto_colors property on the ParetoChart class.""" 16 | test_chart = TestChart(title='', xlabel='', ylabel='') 17 | pass_colors_1 = {'bar': '#B2FFD6', 'line': '#AA78A6'} 18 | pass_colors_2 = {'bar': 'rgba(37, 87, 100, 1.00)', 'line': 'hsla(356, 55%, 44%)'} 19 | fail_colors_1 = {'line': '#B2FFD6'} 20 | fail_message_1 = "Validation of self.pareto_colors failed: {'bar': ['required field']}" 21 | 22 | with pytest.raises(RuntimeError) as fail_error_1: 23 | test_chart.pareto_colors = fail_colors_1 24 | 25 | assert str(fail_error_1.value) == fail_message_1 26 | test_chart.pareto_colors = pass_colors_1 27 | assert test_chart.pareto_colors == pass_colors_1 28 | test_chart.pareto_colors = pass_colors_2 29 | assert test_chart.pareto_colors == pass_colors_2 30 | -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # GH Action https://github.com/crazy-max/ghaction-github-labeler 3 | # yamllint disable-line rule:line-length 4 | # Based on: https://github.com/crazy-max/ghaction-github-labeler/blob/master/.github/labels.yml 5 | # yamllint disable-line rule:line-length 6 | # Other Where Used: https://github.com/search?q=name+path%3A%2F.github+filename%3Alabels.yml&type=Code&ref=advsearch&l=&l= 7 | # 8 | # Style Guide: ../docs/STYLE_GUIDE.md 9 | # 10 | - name: Needs Discussion 11 | color: ff5722 12 | description: Ticket needs discussion and prioritization 13 | from_name: help wanted 14 | - name: 'Type: Bug' 15 | color: d73a4a 16 | description: Something isn't working 17 | from_name: bug 18 | - name: 'Type: Documentation' 19 | color: 69cde9 20 | description: Documentation changes 21 | from_name: good first issue 22 | - name: 'Type: Maintenance' 23 | color: c5def5 24 | description: Chore including build/dep, CI, refactor, or perf 25 | - name: 'Type: Idea' 26 | color: fbca04 27 | description: General idea or concept that could become a feature request 28 | - name: 'Type: Feature' 29 | color: 0075ca 30 | description: Clearly defined new feature request 31 | from_name: enhancement 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /docs/docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for taking a look! This is primarily a personal project, but Pull Requests and Issues (questions, feature requests, etc.) are welcome. If you would like to submit a Pull Request, please open an issue first to discuss what you would like to change 4 | 5 | ## Pull Requests (PR) 6 | 7 | ### Code Development 8 | 9 | See [./DEVELOPER_GUIDE.md](./DEVELOPER_GUIDE.md) 10 | 11 | ### PR Process 12 | 13 | 1. Fork the Project and Clone 14 | 2. Create a new branch (`git checkout -b feat/feature-name`) 15 | 3. Edit code; update documentation and tests; commit and push 16 | 4. Before submitting the review and pushing, make sure to run `poetry run doit` 17 | 5. Open a new Pull Request 18 | 19 | > See the style guide for commit message format ([./STYLE_GUIDE](./STYLE_GUIDE)) 20 | 21 | If you run into any issues, please check to see if there is an open issues or open a new one 22 | 23 | ### Other PR Tips 24 | 25 | - Link the issue with `Fixes #N` in the Pull Request body 26 | - Please add a short summary of `why` the change was made, `what changed`, and any relevant information or screenshots 27 | 28 | ```sh 29 | # SHA is the SHA of the commit you want to fix 30 | git commit --fixup=SHA 31 | # Once all the changes are approved, you can squash your commits: 32 | git rebase --interactive --autosquash main 33 | # Force Push 34 | git push --force 35 | ``` 36 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | Fixes #TBD 10 | Related to #TBD 11 | 12 | ## Description 13 | 14 | 15 | 16 | ## Checklist 17 | 18 | 19 | 20 | - [ ] I've read the: {{ repository_provider }}/{{ repository_namespace }}/{{ package_name_py }}/blob/main/docs/DEVELOPER_GUIDE.md 21 | - [ ] I've read the: {{ repository_provider }}/{{ repository_namespace }}/{{ package_name_py }}/blob/main/docs/STYLE_GUIDE.md 22 | - [ ] I'm familiar with: {{ repository_provider }}/{{ repository_namespace }}/{{ package_name_py }}/blob/main/docs/CONTRIBUTING.md 23 | - [ ] I'm aware of the Code of Conduct: {{ repository_provider }}/{{ repository_namespace }}/{{ package_name_py }}/blob/main/docs/CODE_OF_CONDUCT.md 24 | - [ ] If making code changes: 25 | - [ ] I've run `poetry run doit` and the `pre-commit` checks locally 26 | - [ ] I've added one or more tests for every changes 27 | - [ ] I've updated any relevant documentation 28 | 29 | 30 | -------------------------------------------------------------------------------- /.pyup.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # see https://pyup.io/docs/configuration/ for all available options 3 | 4 | # configure updates globally 5 | # default: all 6 | # allowed: all, insecure, false 7 | update: insecure 8 | 9 | # configure dependency pinning globally 10 | # default: true 11 | # allowed: true, false 12 | pin: false 13 | 14 | # set the default branch 15 | # default: empty, the default branch on GitHub 16 | # branch: dev 17 | 18 | # update schedule 19 | # default: empty 20 | # allowed: "every day", "every week", .. 21 | schedule: every month 22 | 23 | # search for requirement files 24 | # default: true 25 | # allowed: true, false 26 | search: true 27 | 28 | # Specify requirement files by hand, default is empty 29 | # default: empty 30 | # allowed: list 31 | # requirements: 32 | # - requirements/staging.txt: 33 | # update: all 34 | # pin: true 35 | 36 | # add a label to pull requests, default is not set 37 | # requires private repo permissions, even on public repos 38 | # default: empty 39 | # label_prs: update 40 | 41 | # assign users to pull requests, default is not set 42 | # requires private repo permissions, even on public repos 43 | # default: empty 44 | assignees: 45 | - kyleking 46 | 47 | # configure the branch prefix the bot is using 48 | # default: pyup- 49 | branch_prefix: pyup/ 50 | 51 | # set a global prefix for PRs 52 | # default: empty 53 | pr_prefix: 'build:' 54 | 55 | # allow to close stale PRs 56 | # default: true 57 | close_prs: true 58 | -------------------------------------------------------------------------------- /tests/test_utils_static_toc.py: -------------------------------------------------------------------------------- 1 | """Test utils_static_toc.py.""" 2 | 3 | import dominate 4 | from dominate import tags, util 5 | 6 | from dash_charts.utils_static_toc import TOC_KEYWORD, write_toc 7 | 8 | from .configuration import TEST_DATA_DIR 9 | 10 | 11 | def write_test_html(html_path): 12 | """Create a test HTML file for the specified HTML path.""" 13 | doc = dominate.document() 14 | with doc.head: 15 | tags.meta(charset='utf-8') 16 | tags.meta(name='viewport', content='width=device-width, initial-scale=1') 17 | with doc: 18 | with tags.div(_class='container').add(tags.div(_class='col')): 19 | tags.h1('Example Header 1') 20 | util.raw(TOC_KEYWORD) 21 | tags.h2('Example Header 2') 22 | tags.h2('Example Header 2') 23 | tags.h3('Example Header 3') 24 | tags.h4('Example Header 4') 25 | tags.h5('Example Header 5') 26 | tags.h6('Example Header 6') 27 | html_path.write_text(str(doc)) 28 | 29 | 30 | def test_utils_static_toc(): 31 | """Test utils_static_toc.""" 32 | html_expected = TEST_DATA_DIR / 'test_utils_static_toc-expected.html' # in VCS 33 | html_path = html_expected.parent / 'test_utils_static_toc-test.html' 34 | write_test_html(html_path) 35 | write_toc(html_path) 36 | 37 | result = html_path.read_text().strip() 38 | 39 | assert html_expected.read_text().strip() == result 40 | html_path.unlink() 41 | -------------------------------------------------------------------------------- /docs/docs/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | ## Reporting Security Issues 4 | 5 | > Do not open issues that might have security implications! 6 | > It is critical that security related issues are reported privately so we have time to address them before they become public knowledge. 7 | 8 | Vulnerabilities can be reported by emailing core members: 9 | 10 | - Kyle King ([dev.act.kyle@gmail.com](mailto:dev.act.kyle@gmail.com)) 11 | 12 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 13 | 14 | - [ ] Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 15 | - [ ] Full paths of source file(s) related to the manifestation of the issue 16 | - [ ] The location of the affected source code (tag/branch/commit or direct URL) 17 | - [ ] Any special configuration required to reproduce the issue 18 | - [ ] Environment (e.g. Linux / Windows / macOS) 19 | - [ ] Step-by-step instructions to reproduce the issue 20 | - [ ] Proof-of-concept or exploit code (if possible) 21 | - [ ] Impact of the issue, including how an attacker might exploit the issue 22 | 23 | This information will help us triage your report more quickly. 24 | 25 | ## Preferred Languages 26 | 27 | We prefer all communications to be in English. 28 | 29 | ## Attribution 30 | 31 | This file was based on the template from [TezRomacH/python-package-template/SECURITY.md](https://github.com/TezRomacH/python-package-template/blob/184e6cd577106753f0129d8341803b0e7b425404/SECURITY.md) 32 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | site_name: dash_charts 3 | site_author: Kyle King 4 | site_description: dash_charts project documentation 5 | repo_name: kyleking/dash_charts 6 | repo_url: https://github.com/kyleking/dash_charts 7 | edit_uri: edit/main/docs 8 | docs_dir: docs 9 | site_dir: releases/site 10 | 11 | theme: 12 | name: material 13 | palette: 14 | - scheme: default 15 | accent: green 16 | icon: 17 | repo: fontawesome/brands/github 18 | features: 19 | - navigation.instant 20 | - navigation.tabs 21 | - toc.autohide 22 | 23 | plugins: 24 | - search 25 | - git-revision-date-localized: 26 | type: date 27 | fallback_to_build_date: true 28 | enable_creation_date: true 29 | 30 | markdown_extensions: 31 | - abbr 32 | - admonition 33 | - attr_list 34 | - codehilite: 35 | linenums: true 36 | - def_list 37 | - extra 38 | - fenced_code 39 | - footnotes 40 | - pymdownx.emoji: 41 | emoji_index: !!python/name:materialx.emoji.twemoji 42 | emoji_generator: !!python/name:materialx.emoji.to_svg 43 | - pymdownx.details 44 | - pymdownx.highlight: 45 | linenums: true 46 | linenums_style: pymdownx-inline 47 | - pymdownx.superfences 48 | - pymdownx.tabbed 49 | - pymdownx.tasklist: 50 | custom_checkbox: true 51 | clickable_checkbox: true 52 | - smarty 53 | - tables 54 | - toc: 55 | permalink: ⚓︎ 56 | toc_depth: 5 57 | extra_javascript: 58 | - https://cdnjs.cloudflare.com/ajax/libs/tablesort/5.2.1/tablesort.min.js 59 | - _javascript/tables.js 60 | -------------------------------------------------------------------------------- /dash_charts/custom_colorscales.py: -------------------------------------------------------------------------------- 1 | """Custom Plotly Colorscales.""" 2 | 3 | DEFAULT_PLOTLY_COLORS = [ 4 | '#1f77b4', # muted blue 5 | '#ff7f0e', # safety orange 6 | '#2ca02c', # cooked asparagus green 7 | '#d62728', # brick red 8 | '#9467bd', # muted purple 9 | '#8c564b', # chestnut brown 10 | '#e377c2', # raspberry yogurt pink 11 | '#7f7f7f', # middle gray 12 | '#bcbd22', # curry yellow-green 13 | '#17becf', # blue-teal 14 | ] 15 | """List of default Plotly colors in Hex strings.""" 16 | 17 | DEFAULT_PLOTLY_COLORS_RGB = [ 18 | 'rgb(31,119,180)', # 0 19 | 'rgb(255,127,14)', # 1 20 | 'rgb(44,160,44)', # 2 21 | 'rgb(214,39,40)', # 3 22 | 'rgb(148,103,189)', # 4 23 | 'rgb(140,86,75)', # 5 24 | 'rgb(227,119,194)', # 6 25 | 'rgb(127,127,127)', # 7 26 | 'rgb(188,189,34)', # 8 27 | 'rgb(23,190,207)', # 9 28 | ] 29 | """List of default Plotly colors in RGB strings.""" 30 | 31 | # From SF Example: (#fdae61, #abd9e9, #2c7bb6) 32 | 33 | # Plotly Colors: 34 | # ['Blackbody', 'Blackbody_r', 'Bluered', 'Bluered_r', 'Blues', 'Blues_r', 'Cividis', 'Cividis_r', 'Earth', 'Earth_r', 35 | # 'Electric', 'Electric_r', 'Greens', 'Greens_r', 'Greys', 'Greys_r', 'Hot', 'Hot_r', 'Jet', 'Jet_r', 'Picnic', 36 | # 'Picnic_r', 'Portland', 'Portland_r', 'Rainbow', 'Rainbow_r', 'RdBu', 'RdBu_r', 'Reds', 'Reds_r', 'Viridis', 37 | # 'Viridis_r', 'YlGnBu', 'YlGnBu_r', 'YlOrRd', 'YlOrRd_r', 'scale_name', 'scale_name_r', 'scale_pairs', 38 | # 'scale_pairs_r', 'scale_sequence', 'scale_sequence_r'] 39 | # plotly.colors.plotlyjs.Hot / `[rgb(0,0,0), rgb(230,0,0), rgb(255,210,0), rgb(255,255,255)]` 40 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | annoy = true 3 | assertive-snakecase = true 4 | cohesion-below = 50.0 5 | docstring-convention = all 6 | # May generate false positives. May need to revisit, but seems fine so far 7 | eradicate-aggressive = true 8 | # Explanation and Notes on Flake8 Ignore Rules. Also see: https://www.flake8rules.com/ 9 | # ANN01,ANN002,ANN003,ANN101 / flake8-annotations: https://github.com/sco1/flake8-annotations 10 | # D203,D213,D214,D406,D407 / (conflicts with Google docstrings) http://www.pydocstyle.org/en/latest/error_codes.html 11 | # Be more flexible with punctuation. Choose D415 over D400 12 | # G004 / https://github.com/globality-corp/flake8-logging-format#violations-detected 13 | # H101,H238,H301,H304,H306 / https://docs.openstack.org/hacking/latest/user/hacking.html 14 | # PD005,PD011 / (false positives) https://github.com/deppen8/pandas-vet/issues/74 15 | # S322 / https://github.com/tylerwince/flake8-bandit 16 | # W503 - Must select one of W503 or W504. See: https://lintlyci.github.io/Flake8Rules/rules/W504.html 17 | # Python 3 standard is a line break BEFORE binary operator, so ignore W503. Enforce W504 18 | ignore = ANN01,ANN002,ANN003,ANN101,D203,D213,D214,D400,D406,D407,G004,H101,H238,H301,H304,H306,PD005,PD011,S322,W503 19 | # Default is 7. See: https://github.com/Melevir/flake8-cognitive-complexity 20 | max-cognitive-complexity = 7 21 | max-complexity = 10 22 | # Default is 7. See: https://github.com/best-doctor/flake8-expression-complexity 23 | max-expression-complexity = 7 24 | max-function-length = 55 25 | max-line-length = 120 26 | max-parameters-amount = 6 27 | min-python-version = 3.8 28 | per-file-ignores=test_*.py:ANN001,ANN201,DAR101,DAR201,E800,S101 29 | select = A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z 30 | -------------------------------------------------------------------------------- /.dash_tutorials/07_daq-dark-theme.py: -------------------------------------------------------------------------------- 1 | """Example using Dash DAQ to toggle the dark mode. 2 | 3 | Source: https://dash.plot.ly/dash-daq 4 | 5 | """ 6 | 7 | import dash_daq as daq 8 | from dash import html 9 | from dash.dependencies import Input, Output 10 | 11 | from dash_charts.utils_app import init_app 12 | 13 | app = init_app() 14 | 15 | THEME = { 16 | 'dark': False, 17 | 'detail': '#007439', 18 | 'primary': '#00EA64', 19 | 'secondary': '#6E6E6E', 20 | } 21 | 22 | app.layout = html.Div( 23 | id='full-window', 24 | children=[ 25 | html.Br(), 26 | daq.ToggleSwitch( 27 | id='theme-toggle', 28 | label=['Light', 'Dark'], 29 | style={'width': '250px', 'margin': 'auto'}, 30 | value=True, 31 | ), 32 | html.Div( 33 | id='dial-component', 34 | children=[], 35 | style={'display': 'block', 'marginLeft': 'calc(50% - 110px)'}, 36 | ), 37 | ], 38 | ) 39 | 40 | 41 | @app.callback( 42 | Output('dial-component', 'children'), 43 | [Input('theme-toggle', 'value')], 44 | ) 45 | def activate_dark_theme(dark_enabled): 46 | """Update the daw theme provider based on the toggle setting.""" 47 | THEME.update(dark=bool(dark_enabled)) # noqa: DAR101, DAR201 48 | # Return a cool-looking dial, but could be a graph, etc. 49 | return daq.DarkThemeProvider(theme=THEME, children=daq.Knob(value=6)) 50 | 51 | 52 | @app.callback( 53 | Output('full-window', 'style'), # NOTE: Overwrites style prop for this div 54 | [Input('theme-toggle', 'value')], 55 | ) 56 | def set_background(dark_enabled): 57 | """Set the background styles.""" 58 | if dark_enabled: # noqa: DAR101, DAR201 59 | return {'background-color': '#303030', 'color': 'white', 'height': '100vh'} 60 | return {'background-color': 'white', 'color': 'black', 'height': '100vh'} 61 | 62 | 63 | if __name__ == '__main__': 64 | app.run_server(debug=True) 65 | -------------------------------------------------------------------------------- /dodo.py: -------------------------------------------------------------------------------- 1 | """doit Script. 2 | 3 | ```sh 4 | # Ensure that packages are installed 5 | poetry install 6 | # List Tasks 7 | poetry run doit list 8 | # (Or use a poetry shell) 9 | # > poetry shell 10 | # > doit list 11 | 12 | # Run tasks individually (examples below) 13 | poetry run doit run ptw_ff 14 | poetry doit run coverage open_test_docs 15 | # Or all of the tasks in DOIT_CONFIG 16 | poetry run doit 17 | ``` 18 | 19 | """ 20 | 21 | from calcipy.doit_tasks import * # noqa: F401,F403,H303 (Run 'doit list' to see tasks). skipcq: PYL-W0614 22 | from calcipy.doit_tasks import DOIT_CONFIG_RECOMMENDED 23 | from calcipy.doit_tasks.base import debug_task 24 | from calcipy.doit_tasks.doit_globals import DG 25 | from calcipy.log_helpers import activate_debug_logging 26 | 27 | from dash_charts import __pkg_name__ 28 | 29 | activate_debug_logging(pkg_names=[__pkg_name__]) 30 | 31 | # Create list of all tasks run with `poetry run doit` 32 | DOIT_CONFIG = DOIT_CONFIG_RECOMMENDED 33 | 34 | 35 | def task_write_puml(): 36 | """Write updated PlantUML file(s) with `py2puml`.""" 37 | pkg = DG.meta.pkg_name 38 | diagram_dir = DG.meta.path_project / '.diagrams' 39 | 40 | # TODO: pypi package wasn't working. Used local version 41 | run_py2puml = f'poetry run ../py-puml-tools/py2puml/py2puml.py --config {diagram_dir}/py2puml.ini' 42 | 43 | # # PLANNED: needs to be a bit more efficient... 44 | # > files = [] 45 | # > for file_path in (DG.source_path / pkg).glob('*.py'): 46 | # > if any(line.startswith('class ') for line in file_path.read_text().split('\n')): 47 | # > files.append(file_path.name) 48 | files = [ 49 | 'utils_app.py', 50 | 'utils_app_modules.py', 51 | 'utils_app_with_navigation.py', 52 | 'utils_fig.py', 53 | ] 54 | return debug_task([ 55 | f'{run_py2puml} -o {diagram_dir}/{pkg}.puml' + ''.join([f' {pkg}/{fn}' for fn in files]), 56 | f'plantuml {diagram_dir}/{pkg}.puml -tpng', 57 | # f'plantuml {diagram_dir}/{pkg}.puml -tsvg', 58 | 59 | # > f'{run_py2puml} -o {diagram_dir}/{pkg}-examples.puml ./tests/examples/*.py --root ./tests', 60 | # > f'plantuml {diagram_dir}/{pkg}-examples.puml -tsvg', 61 | ]) 62 | -------------------------------------------------------------------------------- /tests/configuration.py: -------------------------------------------------------------------------------- 1 | """Global variables for testing.""" 2 | 3 | from pathlib import Path 4 | 5 | from calcipy.file_helpers import delete_dir, ensure_dir 6 | from calcipy.log_helpers import activate_debug_logging 7 | 8 | from dash_charts import __pkg_name__ 9 | 10 | activate_debug_logging(pkg_names=[__pkg_name__], clear_log=True) 11 | 12 | TEST_DIR = Path(__file__).resolve().parent 13 | """Path to the `test` directory that contains this file and all other tests.""" 14 | 15 | TEST_DATA_DIR = TEST_DIR / 'data' 16 | """Path to subdirectory with test data within the Test Directory.""" 17 | 18 | TEST_TMP_CACHE = TEST_DIR / '_tmp_cache' 19 | """Path to the temporary cache folder in the Test directory.""" 20 | 21 | 22 | def clear_test_cache() -> None: 23 | """Remove the test cache directory if present.""" 24 | delete_dir(TEST_TMP_CACHE) 25 | ensure_dir(TEST_TMP_CACHE) 26 | 27 | 28 | # PLANNED: Output the test name and other information to the test.log file. Currently only used in `no_log_errors` 29 | # PLANNED: move to a lazy import within dash_charts? 30 | def no_log_errors(dash_duo, suppressed_errors=None): 31 | """Return True if any unsuppressed errors found in console logs. 32 | 33 | Args: 34 | dash_duo: dash_duo instance 35 | suppressed_errors: list of suppressed error strings. Default is None to check for any log errors 36 | 37 | Returns: 38 | boolean: True if no unsuppressed errors found in dash logs 39 | 40 | """ 41 | if suppressed_errors is None: 42 | suppressed_errors = [] 43 | 44 | logs = dash_duo.get_logs() 45 | # logger.debug(logs) 46 | # HACK: get_logs always return None with webdrivers other than Chrome 47 | # FIXME: Handle path to the executable. Example with Firefox when the Gecko Drive is installed and on path 48 | # poetry run pytest tests -x -l --ff -vv --webdriver Firefox 49 | # Will one of these work? 50 | # - https://pypi.org/project/webdrivermanager/ 51 | # - https://pypi.org/project/chromedriver-binary/ 52 | # - https://pypi.org/project/undetected-chromedriver/ 53 | # - https://pypi.org/project/webdriver-manager/ 54 | # 55 | # Actually set DASH_TEST_CHROMEPATH? Maybe still use one of the above packages to get the path? 56 | # - https://github.com/plotly/dash/blob/5ef534943852f2d02a9da636cf18357c5df5b3e5/dash/testing/browser.py#L436 57 | return logs is None or not [log for log in logs if log['level'] not in suppressed_errors] 58 | -------------------------------------------------------------------------------- /dash_charts/utils_static_toc.py: -------------------------------------------------------------------------------- 1 | """Add a nested Table of Contents to any HTML file with BeautifulSoup and dominate.""" 2 | 3 | from bs4 import BeautifulSoup 4 | from dominate import tags 5 | 6 | TOC_KEYWORD = '{{toc}}' 7 | """Default string to replace in the specified file with the nested table of contents. Default is `{{toc}}`.""" 8 | 9 | 10 | def add_nested_list_item(l_index, l_string, level=1): 11 | """Add nested list items recursively. 12 | 13 | Args: 14 | l_index: numeric index of the list depth (note: 1-indexed) 15 | l_string: string to show in the list element 16 | level: current list depth. Optional and default is 1 17 | 18 | """ 19 | with tags.ul(): 20 | if l_index != level: 21 | add_nested_list_item(l_index, l_string, level + 1) 22 | else: 23 | tags.li(f'H{l_index}: {l_string}') 24 | 25 | 26 | def create_toc(html_text, header_depth=3): 27 | """Return the HTML for a nested table of contents based on the HTML file path. 28 | 29 | Args: 30 | html_text: HTML text 31 | header_depth: depth of headers to show. Default is 3 (H1-H3) 32 | 33 | Returns: 34 | string: table of contents 35 | 36 | """ 37 | soup = BeautifulSoup(html_text, features='lxml') 38 | h_lookup = {f'h{idx}': idx for idx in range(1, header_depth + 1)} 39 | toc = tags.div() 40 | for header in soup.findAll([*h_lookup.keys()]): 41 | with toc: 42 | add_nested_list_item(h_lookup[header.name], header.string) 43 | # FIXME: Figure out how to make the header links work (i.e. when clicked in TOC go to the respective header) 44 | # > `tags.a(header.string, f'#{header.string}')`? 45 | return str(toc) 46 | 47 | 48 | def write_toc(html_path, header_depth=3, toc_key=TOC_KEYWORD): 49 | """Write the nested table of contents to the specified file. 50 | 51 | Args: 52 | html_path: path to the HTML file 53 | header_depth: depth of headers to show. Default is 3 (H1-H3) 54 | toc_key: string to replace with the nested table of contents. Default is `TOC_KEYWORD` 55 | 56 | Raises: 57 | RuntimeError: if the key was not found in the file 58 | 59 | """ 60 | text = html_path.read_text() 61 | if toc_key not in text: 62 | raise RuntimeError(f'HTML file does not have the table of contents key ({toc_key}): {html_path}') 63 | toc = create_toc(text, header_depth=header_depth) 64 | html_path.write_text(text.replace('{{toc}}', toc)) 65 | -------------------------------------------------------------------------------- /docs/adr/NNN-Template.md: -------------------------------------------------------------------------------- 1 | # [short title of solved problem and solution] 2 | 3 | * Status: [proposed | rejected | accepted | deprecated | … | superseded by [ADR-0005](0005-example.md)] 4 | * Deciders: [list everyone involved in the decision] 5 | * Date: [YYYY-MM-DD when the decision was last updated] 6 | 7 | Technical Story: [description | ticket/issue URL] 8 | 9 | ## Context and Problem Statement 10 | 11 | [Describe the context and problem statement, e.g., in free form using two to three sentences. You may want to articulate the problem in form of a question.] 12 | 13 | ## Decision Drivers 14 | 15 | * [driver 1, e.g., a force, facing concern, …] 16 | * [driver 2, e.g., a force, facing concern, …] 17 | * … 18 | 19 | ## Considered Options 20 | 21 | * [option 1] 22 | * [option 2] 23 | * [option 3] 24 | * … 25 | 26 | ## Decision Outcome 27 | 28 | Chosen option: "[option 1]", because [justification. e.g., only option, which meets k.o. criterion decision driver | which resolves force force | … | comes out best (see below)]. 29 | 30 | ### Positive Consequences 31 | 32 | * [e.g., improvement of quality attribute satisfaction, follow-up decisions required, …] 33 | * … 34 | 35 | ### Negative Consequences 36 | 37 | * [e.g., compromising quality attribute, follow-up decisions required, …] 38 | * … 39 | 40 | ## Pros and Cons of the Options 41 | 42 | ### [option 1] 43 | 44 | [example | description | pointer to more information | …] 45 | 46 | * Good, because [argument a] 47 | * Good, because [argument b] 48 | * Bad, because [argument c] 49 | * … 50 | 51 | ### [option 2] 52 | 53 | [example | description | pointer to more information | …] 54 | 55 | * Good, because [argument a] 56 | * Good, because [argument b] 57 | * Bad, because [argument c] 58 | * … 59 | 60 | ### [option 3] 61 | 62 | [example | description | pointer to more information | …] 63 | 64 | * Good, because [argument a] 65 | * Good, because [argument b] 66 | * Bad, because [argument c] 67 | * … 68 | 69 | ## Links 70 | 71 | * [Link type] [Link to ADR] 72 | * … 73 | -------------------------------------------------------------------------------- /tests/examples/example_write_from_markdown.md: -------------------------------------------------------------------------------- 1 | # Hello Markdown! 2 | 3 | Example writing a Markdown with Plotly figures. Easier to write longer form text than in Python. You can even use the VSCode/Sublime/etc "Markdown TOC" extension: 4 | 5 | **Table of Contents** 6 | 7 | 8 | 9 | - [Hello Markdown!](#hello-markdown) 10 | - [Plotly-Stuff](#plotly-stuff) 11 | - [Example Markdown](#example-markdown) 12 | - [Unordered](#unordered) 13 | - [Ordered](#ordered) 14 | - [Images](#images) 15 | - [Github Flavored Markdown](#github-flavored-markdown) 16 | - [Task Lists](#task-lists) 17 | - [Tables](#tables) 18 | - [Code](#code) 19 | 20 | 21 | 22 | ## Plotly-Stuff 23 | 24 | Plotly Figure 25 | 26 | >>lookup:make_div(figure_px) 27 | 28 | Bootstrap Table 29 | 30 | >>lookup:table(iris_data) 31 | 32 | --- 33 | 34 | ## Example Markdown 35 | 36 | Experimenting with gfm syntax based on: [Mastering Markdown](https://guides.github.com/features/mastering-markdown/) 37 | 38 | It's very easy to make some words **bold** and other words *italic* with Markdown. You can even [link to Google!](http://google.com) 39 | 40 | *This text will be italic* 41 | 42 | _This will also be italic_ 43 | 44 | **This text will be bold** 45 | 46 | __This will also be bold__ 47 | 48 | _You **can** combine them_ 49 | 50 | ### Unordered 51 | 52 | * Item 1 53 | * Item 2 54 | * Item 2a (note: four spaces) 55 | * Item 2b 56 | 57 | ### Ordered 58 | 59 | 1. Item 1 60 | 1. Item 2 61 | 1. Item 3 62 | 1. Item 3a (note: four spaces) 63 | 1. Item 3aa 64 | 1. Item 3aaa 65 | 1. Item 3aaaa 66 | 1. Item 3b 67 | 68 | ### Images 69 | 70 | Using alt-text: ![Alt Text](not_a_url) 71 | 72 | Actual Image: 73 | 74 | ![GitHub Logo](https://upload.wikimedia.org/wikipedia/commons/2/29/GitHub_logo_2013.svg) 75 | 76 | --- 77 | 78 | ## Github Flavored Markdown 79 | 80 | gfm-specific syntax 81 | 82 | ### Task Lists 83 | 84 | - [x] @mentions, #refs, [links](), **formatting**, and tags supported 85 | - [x] list syntax required (any unordered or ordered list supported) 86 | - [x] this is a complete item 87 | - [ ] this is an incomplete item 88 | 89 | ### Tables 90 | 91 | | First Header | Second Header | 92 | | ------------ | ------------- | 93 | | Content from cell 1 | Content from cell 2 | 94 | | Content in the first column | Content in the second column | 95 | 96 | ### Code 97 | 98 | `import markdown` 99 | 100 | ``` 101 | import markdown 102 | ``` 103 | 104 | ```py 105 | import markdown 106 | ``` 107 | -------------------------------------------------------------------------------- /tests/examples/readme.py: -------------------------------------------------------------------------------- 1 | """Example Dash Application.""" 2 | 3 | from typing import Optional 4 | 5 | import dash 6 | import plotly.express as px 7 | from box import Box 8 | from dash import html 9 | from implements import implements 10 | 11 | from dash_charts.pareto_chart import ParetoChart 12 | from dash_charts.utils_app import AppBase, AppInterface 13 | from dash_charts.utils_fig import min_graph 14 | from dash_charts.utils_helpers import parse_dash_cli_args 15 | 16 | _ID = Box({ 17 | 'chart': 'pareto', 18 | }) 19 | """Default App IDs.""" 20 | 21 | 22 | @implements(AppInterface) 23 | class ParetoDemo(AppBase): 24 | """Example creating a simple Pareto chart.""" 25 | 26 | def __init__(self, app: Optional[dash.Dash] = None) -> None: 27 | """Initialize app and initial data members. Should be inherited in child class and called with super(). 28 | 29 | Args: 30 | app: Dash instance. If None, will create standalone app. Otherwise, will be part of existing app 31 | 32 | """ 33 | self.name = 'Car Share Pareto Demo' 34 | self.data_raw = None 35 | self.chart_main = None 36 | self._id = _ID 37 | 38 | super().__init__(app=app) 39 | 40 | def generate_data(self) -> None: 41 | """Format the car share data from plotly express for the Pareto. Called by parent class.""" 42 | self.data_raw = ( 43 | px.data.carshare() 44 | .rename(columns={'peak_hour': 'category', 'car_hours': 'value'}) 45 | ) 46 | self.data_raw['category'] = [f'H:{cat:02}' for cat in self.data_raw['category']] 47 | 48 | def create_elements(self) -> None: 49 | """Initialize the charts, tables, and other Dash elements.""" 50 | self.chart_main = ParetoChart(title='Car Share Pareto', xlabel='Peak Hours', ylabel='Car Hours') 51 | 52 | def return_layout(self) -> dict: 53 | """Return Dash application layout. 54 | 55 | Returns: 56 | dict: Dash HTML object 57 | 58 | """ 59 | return html.Div([ 60 | html.Div([ 61 | min_graph( 62 | id=self._il[self._id.chart], 63 | figure=self.chart_main.create_figure(df_raw=self.data_raw), 64 | ), 65 | ]), 66 | ]) 67 | 68 | def create_callbacks(self) -> None: 69 | """Register the callbacks.""" 70 | ... # Override base class. Not necessary for this example 71 | 72 | 73 | instance = ParetoDemo 74 | app = instance() 75 | app.create() 76 | if __name__ == '__main__': 77 | app.run(**parse_dash_cli_args()) 78 | else: 79 | FLASK_HANDLE = app.get_server() 80 | -------------------------------------------------------------------------------- /tests/examples/ex_gantt_chart.py: -------------------------------------------------------------------------------- 1 | """Example Gantt Chart.""" 2 | 3 | from pathlib import Path 4 | 5 | import pandas as pd 6 | from dash import html 7 | from implements import implements 8 | from palettable.wesanderson import FantasticFox2_5 9 | 10 | from dash_charts.gantt_chart import GanttChart 11 | from dash_charts.utils_app import AppBase, AppInterface 12 | from dash_charts.utils_fig import min_graph 13 | from dash_charts.utils_helpers import parse_dash_cli_args 14 | 15 | 16 | @implements(AppInterface) # noqa: H601 17 | class GanttDemo(AppBase): 18 | """Example creating a Gantt chart.""" 19 | 20 | name = 'Example Gantt Chart' 21 | """Application name""" 22 | 23 | chart_main = None 24 | """Main chart (Gantt).""" 25 | 26 | id_chart = 'Gantt' 27 | """Unique name for the main chart.""" 28 | 29 | def initialization(self) -> None: 30 | """Initialize ids with `self.register_uniq_ids([...])` and other one-time actions.""" 31 | super().initialization() 32 | self.register_uniq_ids([self.id_chart]) 33 | 34 | def create_elements(self) -> None: 35 | """Initialize the charts, tables, and other Dash elements.""" 36 | self.chart_main = GanttChart( 37 | title='Sample Gantt Chart', 38 | xlabel=None, 39 | ylabel=None, 40 | ) 41 | self.chart_main.pallette = FantasticFox2_5.hex_colors 42 | 43 | def generate_data(self) -> None: 44 | """Create self.data_raw with sample data.""" 45 | csv_filename = Path(__file__).parent / 'ex_gantt_data.csv' 46 | self.data_raw = pd.read_csv(csv_filename) 47 | 48 | def return_layout(self) -> dict: 49 | """Return Dash application layout. 50 | 51 | Returns: 52 | dict: Dash HTML object 53 | 54 | """ 55 | return html.Div( 56 | style={ 57 | 'maxWidth': '1000px', 58 | 'marginRight': 'auto', 59 | 'marginLeft': 'auto', 60 | }, children=[ 61 | html.H4(children=self.name), 62 | html.Div([ 63 | min_graph( 64 | id=self._il[self.id_chart], 65 | figure=self.chart_main.create_figure(df_raw=self.data_raw), 66 | ), 67 | ]), 68 | ], 69 | ) 70 | 71 | def create_callbacks(self) -> None: 72 | """Create Dash callbacks.""" 73 | ... # No callbacks necessary for this simple example 74 | 75 | 76 | instance = GanttDemo 77 | app = instance() 78 | app.create() 79 | if __name__ == '__main__': 80 | app.run(**parse_dash_cli_args()) 81 | else: 82 | FLASK_HANDLE = app.get_server() 83 | -------------------------------------------------------------------------------- /dash_charts/utils_helpers.py: -------------------------------------------------------------------------------- 1 | """Helpers for building Dash applications.""" 2 | 3 | import argparse 4 | import time 5 | from datetime import datetime 6 | from typing import Optional 7 | 8 | from beartype import beartype 9 | 10 | # ---------------------------------------------------------------------------------------------------------------------- 11 | # General Debug 12 | 13 | 14 | @beartype 15 | def debug_time(message: str, last: Optional[datetime] = None) -> datetime: 16 | """Debug timing issues. 17 | 18 | Args: 19 | message: string message to print 20 | last: last timestamp 21 | 22 | Returns: 23 | timestamp: the current timestamp to calculate the next delta 24 | 25 | """ 26 | if last is None: 27 | last = time.time() 28 | now = time.time() 29 | delta = now - last 30 | if delta > 0.5: 31 | print(message, delta) # noqa: T001 32 | return now 33 | 34 | 35 | # ---------------------------------------------------------------------------------------------------------------------- 36 | # Dash Helpers 37 | 38 | 39 | def parse_dash_cli_args(): # pragma: no cover 40 | """Configure the CLI options for Dash applications. 41 | 42 | Returns: 43 | dict: keyword arguments for Dash 44 | 45 | """ 46 | parser = argparse.ArgumentParser(description='Process Dash Parameters.') 47 | parser.add_argument( 48 | '--port', type=int, default=8050, 49 | help='Pass port number to Dash server. Default is 8050', 50 | ) 51 | parser.add_argument( 52 | '--nodebug', action='store_true', default=False, 53 | help='If set, will disable debug mode. Default is to set `debug=True`', 54 | ) 55 | args = parser.parse_args() 56 | return {'port': args.port, 'debug': not args.nodebug} 57 | 58 | 59 | # ---------------------------------------------------------------------------------------------------------------------- 60 | # Functional Programming 61 | 62 | 63 | def graph_return(resp, keys): 64 | """Based on concepts of GraphQL, return specified subset of response. 65 | 66 | Args: 67 | resp: dictionary with values from function 68 | keys: list of keynames from the resp dictionary 69 | 70 | Returns: 71 | the `resp` dictionary with only the keys specified in the `keys` list 72 | 73 | Raises: 74 | RuntimeError: if `keys` is not a list or tuple 75 | 76 | """ 77 | if not (len(keys) and isinstance(keys, (list, tuple))): 78 | raise RuntimeError(f'Expected list of keys for: `{resp.items()}`, but received `{keys}`') 79 | ordered_responses = [resp.get(key, None) for key in keys] 80 | return ordered_responses if len(ordered_responses) > 1 else ordered_responses[0] 81 | -------------------------------------------------------------------------------- /tests/test_dash_charts_equations.py: -------------------------------------------------------------------------------- 1 | """Test equations.""" 2 | 3 | import math 4 | 5 | import numpy as np 6 | 7 | from dash_charts import equations 8 | 9 | 10 | def test_linear(): 11 | """Test the linear equation.""" 12 | factor_a, factor_b = 0.5, 100 13 | x_values = [-1, 0, 1, 10, 100] 14 | y_expected = [99.5, 100, 100.5, 105, 150] 15 | x_single = -5 16 | y_expected_single = 97.5 17 | 18 | y_values = equations.linear(x_values, factor_a, factor_b) 19 | y_single = equations.linear(x_single, factor_a, factor_b) 20 | 21 | assert np.allclose(y_values, y_expected) 22 | assert y_single == y_expected_single 23 | 24 | 25 | def test_quadratic(): 26 | """Test the quadratic equation.""" 27 | factor_a, factor_b, factor_c = 0.4, 2.1, 30 28 | x_values = [-1, 0, 1, 10, 100] 29 | y_expected = np.array([28.3, 30, 32.5, 91, 4240]) 30 | x_single = -5 31 | y_expected_single = 29.5 32 | 33 | y_values = equations.quadratic(x_values, factor_a, factor_b, factor_c) 34 | y_single = equations.quadratic(x_single, factor_a, factor_b, factor_c) 35 | 36 | assert np.allclose(y_values, y_expected) 37 | assert y_single == y_expected_single 38 | 39 | 40 | def test_power(): 41 | """Test the power equation.""" 42 | factor_a, factor_b = 5, 3 43 | x_values = [-5, -1, 0, 1, 10] 44 | y_expected = [-625, -5, 0, 5, 5000] 45 | x_single = -2 46 | y_expected_single = -40 47 | 48 | y_values = equations.power(x_values, factor_a, factor_b) 49 | y_single = equations.power(x_single, factor_a, factor_b) 50 | 51 | assert np.allclose(y_values, y_expected) 52 | assert y_single == y_expected_single 53 | 54 | 55 | def test_exponential(): 56 | """Test the exponential equation.""" 57 | factor_a, factor_b = 5, 3 58 | x_values = [-1, 0, 10] 59 | y_expected = [5 * math.exp(-3), 5, 5 * math.exp(30)] 60 | x_single = math.log(2) 61 | y_expected_single = 39.99999999999999 62 | 63 | y_values = equations.exponential(x_values, factor_a, factor_b) 64 | y_single = equations.exponential(x_single, factor_a, factor_b) 65 | 66 | assert np.allclose(y_values, y_expected) 67 | assert y_single == y_expected_single 68 | 69 | 70 | def test_double_exponential(): 71 | """Test the double_exponential equation.""" 72 | factor_a, factor_b, factor_c, factor_d = 2, 5, 0.5, 1 73 | x_values = [-1, 0, 1, math.log(3)] 74 | y_expected = [-0.170464, 1.5, 295.467, 484.5] 75 | x_single = -2 76 | y_expected_single = -0.06757684175878138 77 | 78 | y_values = equations.double_exponential(x_values, factor_a, factor_b, factor_c, factor_d) 79 | y_single = equations.double_exponential(x_single, factor_a, factor_b, factor_c, factor_d) 80 | 81 | assert np.allclose(y_values, y_expected) 82 | assert y_single == y_expected_single 83 | -------------------------------------------------------------------------------- /dash_charts/components.py: -------------------------------------------------------------------------------- 1 | """Application components built on Dash Bootstrap Components.""" 2 | 3 | import dash_bootstrap_components as dbc 4 | from dash import dcc 5 | 6 | 7 | def opts_dd(lbl, value): 8 | """Format an individual item in a Dash dcc dropdown list. 9 | 10 | Args: 11 | lbl: Dropdown label 12 | value: Dropdown value 13 | 14 | Returns: 15 | dict: keys `label` and `value` for dcc.dropdown() 16 | 17 | """ 18 | return {'label': str(lbl), 'value': value} 19 | 20 | 21 | def dropdown_group(name, _id, options, form_style=None, **dropdown_kwargs): 22 | """Return a Form Group with label and dropdown. 23 | 24 | Dropdown documentation: https://dash.plot.ly/dash-core-components/dropdown 25 | 26 | Args: 27 | name: label name of dropdown 28 | _id: element id 29 | options: list of dicts with keys `(value, label)` 30 | form_style: style keyword argument for dbc.FormGroup(). Default is None 31 | dropdown_kwargs: key word arguments for dropdown. Could be: `(persistence, multi, searchable, etc.)` 32 | 33 | Returns: 34 | Row: dbc row with label and dropdown 35 | 36 | """ 37 | if form_style is None: 38 | form_style = {} 39 | return dbc.Row( 40 | [ 41 | dbc.Label(name), 42 | dcc.Dropdown(id=_id, options=options, **dropdown_kwargs), 43 | ], style=form_style, 44 | ) 45 | 46 | 47 | def format_email_pass_id(submit_id): 48 | """Return tuple of the formatted email and password IDs based on the base submit_id value. 49 | 50 | Args: 51 | submit_id: id used to create unique element IDs 52 | 53 | Returns: 54 | tuple: formatted IDs: `(email_id, pass_id)` 55 | 56 | """ 57 | return [f'{submit_id}-{key}' for key in ['email', 'password']] 58 | 59 | 60 | def login_form(submit_id): 61 | """Return dbcForm with email and password inputs and submit button. 62 | 63 | Based on: https://dash-bootstrap-components.opensource.faculty.ai/docs/components/form/ 64 | 65 | Args: 66 | submit_id: id used to create unique element IDs 67 | 68 | Returns: 69 | form: dbc.Form with email and password inputs and submit button 70 | 71 | """ 72 | email_id, pass_id = format_email_pass_id(submit_id) 73 | return dbc.Form( 74 | dbc.Row( 75 | [ 76 | dbc.Label('Email', width='auto'), 77 | dbc.Col( 78 | dbc.Input(type='email', placeholder='Enter email', id=email_id), 79 | className='me-3', 80 | ), 81 | dbc.Label('Password', width='auto'), 82 | dbc.Col( 83 | dbc.Input(type='password', placeholder='Enter password', id=pass_id), 84 | className='me-3', 85 | ), 86 | dbc.Col(dbc.Button('Submit', color='primary', id=submit_id), width='auto'), 87 | ], 88 | className='g-2', 89 | ), 90 | ) 91 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Common commands: 3 | # poetry run pre-commit install 4 | # poetry run pre-commit run --all-files --hook-stage commit 5 | # poetry run pre-commit run --all-files --hook-stage push 6 | # poetry run doit run pre_commit_hooks 7 | # 8 | # See https://pre-commit.com for more information 9 | # See https://pre-commit.com/hooks.html for more hooks 10 | repos: 11 | - repo: https://github.com/pre-commit/pre-commit-hooks 12 | rev: v4.3.0 13 | hooks: 14 | - id: check-added-large-files 15 | # - id: check-case-conflict # Too Slow... 16 | - id: check-json 17 | - id: check-merge-conflict 18 | - id: check-symlinks 19 | - id: check-toml 20 | - id: check-vcs-permalinks 21 | - id: check-yaml 22 | args: [--unsafe] 23 | - id: debug-statements 24 | - id: destroyed-symlinks 25 | - id: detect-private-key 26 | - id: double-quote-string-fixer 27 | - id: end-of-file-fixer 28 | - id: fix-byte-order-marker 29 | - id: fix-encoding-pragma 30 | args: [--remove] 31 | - id: forbid-new-submodules 32 | - id: mixed-line-ending 33 | args: [--fix=auto] 34 | - id: pretty-format-json 35 | args: [--autofix] 36 | # Conflicts with ordering by poetry 37 | # - id: requirements-txt-fixer 38 | - id: trailing-whitespace 39 | - repo: https://github.com/commitizen-tools/commitizen 40 | rev: v2.29.3 41 | hooks: 42 | - id: commitizen 43 | additional_dependencies: [cz_legacy] 44 | stages: [commit-msg] 45 | - repo: https://github.com/Yelp/detect-secrets 46 | rev: v1.3.0 47 | hooks: 48 | # Can't generate baseline with Py 3.8. Use exclude regex rules instead 49 | # See: https://github.com/Yelp/detect-secrets/issues/452 50 | - id: detect-secrets 51 | # args: ["--baseline", ".secrets.baseline"] 52 | exclude: poetry.lock|cassettes/.*\.yaml 53 | stages: [push] 54 | - repo: https://github.com/lyz-code/yamlfix/ 55 | rev: 1.0.1 56 | hooks: 57 | - id: yamlfix 58 | exclude: .copier-answers.yml 59 | types_or: [] 60 | types: [file, yaml] 61 | - repo: local 62 | hooks: 63 | - id: copier-forbidden-files 64 | name: copier_forbidden_files 65 | # yamllint disable-line rule:line-length 66 | entry: found copier update rejection files; review them and remove them (https://copier.readthedocs.io/en/stable/updating/) 67 | language: fail 68 | files: \.rej$ 69 | - id: python-formatter 70 | name: Python Auto-Formatter 71 | description: Apply calcipy formatting 72 | language: system 73 | entry: poetry run doit run format_py 74 | types: [python] 75 | stages: [push] 76 | - id: toml-formatter 77 | name: Optional TOML Auto-Formatter 78 | description: Install taplo with 'npm install -g @taplo/cli' 79 | language: system 80 | entry: poetry run doit run format_toml 81 | types: [toml] 82 | exclude: poetry.lock 83 | -------------------------------------------------------------------------------- /tests/examples/ex_fitted_chart.py: -------------------------------------------------------------------------------- 1 | """Example Scatter Data with Fitted Line.""" 2 | 3 | import pandas as pd 4 | import plotly.express as px 5 | from dash import html 6 | from implements import implements 7 | 8 | from dash_charts import equations 9 | from dash_charts.scatter_line_charts import FittedChart 10 | from dash_charts.utils_app import AppBase, AppInterface 11 | from dash_charts.utils_fig import min_graph 12 | from dash_charts.utils_helpers import parse_dash_cli_args 13 | 14 | 15 | @implements(AppInterface) # noqa: H601 16 | class FittedDemo(AppBase): 17 | """Example creating a Fitted chart.""" 18 | 19 | name = 'Example Fitted Chart' 20 | """Application name""" 21 | 22 | data_raw = None 23 | """All in-memory data referenced by callbacks and plotted. If modified, will impact all viewers.""" 24 | 25 | chart_main = None 26 | """Main chart (Fitted).""" 27 | 28 | id_chart = 'fitted' 29 | """Unique name for the main chart.""" 30 | 31 | def initialization(self) -> None: 32 | """Initialize ids with `self.register_uniq_ids([...])` and other one-time actions.""" 33 | super().initialization() 34 | self.register_uniq_ids([self.id_chart]) 35 | 36 | def create_elements(self) -> None: 37 | """Initialize the charts, tables, and other Dash elements.""" 38 | self.chart_main = FittedChart( 39 | title='Sample Fitted Scatter Data', 40 | xlabel='Index', 41 | ylabel='Measured Value', 42 | ) 43 | # Set fit equations 44 | self.chart_main.fit_eqs = [('quadratic', equations.quadratic)] 45 | 46 | def generate_data(self) -> None: 47 | """Create self.data_raw with sample data.""" 48 | # Create dataframe based on px sample dataset 49 | iris = px.data.iris() 50 | self.data_raw = pd.DataFrame( 51 | data={ 52 | 'name': iris['species'], 53 | 'x': iris['petal_width'], 54 | 'y': iris['petal_length'], 55 | 'label': None, 56 | }, 57 | ) 58 | # Alternatively, use `[random.expovariate(0.2) for _i in range(count)]` 59 | 60 | def return_layout(self) -> dict: 61 | """Return Dash application layout. 62 | 63 | Returns: 64 | dict: Dash HTML object 65 | 66 | """ 67 | return html.Div( 68 | style={ 69 | 'maxWidth': '1000px', 70 | 'marginRight': 'auto', 71 | 'marginLeft': 'auto', 72 | }, children=[ 73 | html.H4(children=self.name), 74 | html.Div([ 75 | min_graph( 76 | id=self._il[self.id_chart], 77 | figure=self.chart_main.create_figure(df_raw=self.data_raw), 78 | ), 79 | ]), 80 | ], 81 | ) 82 | 83 | def create_callbacks(self) -> None: 84 | """Create Dash callbacks.""" 85 | ... # No callbacks necessary for this simple example 86 | 87 | 88 | instance = FittedDemo 89 | app = instance() 90 | app.create() 91 | if __name__ == '__main__': 92 | app.run(**parse_dash_cli_args()) 93 | else: 94 | FLASK_HANDLE = app.get_server() 95 | -------------------------------------------------------------------------------- /scripts/jsonl_viewer.py: -------------------------------------------------------------------------------- 1 | """Example DataTable. 2 | 3 | TODO: See todo list at bottom!! 4 | 5 | """ 6 | 7 | import dash_bootstrap_components as dbc 8 | import plotly.express as px 9 | from dash import dcc, html 10 | from implements import implements 11 | 12 | from dash_charts.modules_datatable import ModuleFilteredTable 13 | from dash_charts.utils_app import AppBase, AppInterface 14 | from dash_charts.utils_helpers import parse_dash_cli_args 15 | 16 | 17 | @implements(AppInterface) # noqa: H601 18 | class DataTableDemo(AppBase): 19 | """Example creating a DataTable.""" 20 | 21 | name = 'Example DataTable' 22 | """Application name""" 23 | 24 | external_stylesheets = [dbc.themes.FLATLY] # DARKLY, FLATLY, etc. (https://bootswatch.com/) 25 | """List of external stylesheets. Default is minimal Dash CSS. Only applies if app argument not provided.""" 26 | 27 | data_raw = None 28 | """All in-memory data referenced by callbacks and plotted. If modified, will impact all viewers.""" 29 | 30 | mod_table = None 31 | """Main table (DataTable).""" 32 | 33 | def initialization(self) -> None: 34 | """Initialize ids with `self.register_uniq_ids([...])` and other one-time actions.""" 35 | super().initialization() 36 | # Load sample plotly express data to populate the datatable 37 | self.data_raw = px.data.gapminder() 38 | 39 | # Register modules 40 | self.mod_table = ModuleFilteredTable('filtered_table') 41 | self.modules = [ 42 | self.mod_table, 43 | ] 44 | 45 | def create_elements(self) -> None: 46 | """Initialize charts and tables.""" 47 | ... 48 | 49 | def return_layout(self) -> dict: 50 | """Return Dash application layout. 51 | 52 | Returns: 53 | dict: Dash HTML object 54 | 55 | """ 56 | return dbc.Container([ 57 | dbc.Col([ 58 | dcc.Markdown(self.mod_table.table.filter_summary), 59 | html.Br(), 60 | html.H1(self.name), 61 | self.mod_table.return_layout(self._il, self.data_raw), 62 | ]), 63 | ]) 64 | 65 | def create_callbacks(self) -> None: 66 | """Create Dash callbacks.""" 67 | ... # No callbacks necessary for this simple example 68 | 69 | 70 | instance = DataTableDemo 71 | app = instance() 72 | app.create() 73 | if __name__ == '__main__': 74 | app.run(**parse_dash_cli_args()) 75 | else: 76 | FLASK_HANDLE = app.get_server() 77 | 78 | # TODO: CLICKABLE POPUPS 79 | # - Datatable 80 | # - Have click able icon in first column of table that triggers a dbc modal with additional information 81 | # - Would have layout determined in callback. Could be used to show a timeline, full traceback, or other long 82 | # form data that can't be displayed in condensed table format 83 | # - dbc modal: https://dash-bootstrap-components.opensource.faculty.ai/l/components/modal 84 | # - Would require pattern matching callback: https://dash.plotly.com/pattern-matching-callbacks 85 | 86 | # # TODO: See: https://dash.plot.ly/datatable/interactivity 87 | # > ('datatable-id...', 'derived_virtual_row_ids'), 88 | # > ('datatable-id...', 'selected_row_ids'), 89 | # > ('datatable-id...', 'active_cell'), 90 | 91 | 92 | # TODO: Formatting (Typing): https://dash.plot.ly/datatable/typing 93 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 0.1.2.{build} 3 | 4 | image: 5 | - Ubuntu 6 | - Visual Studio 2019 7 | 8 | environment: 9 | # Python versions 10 | # Windows: https://www.appveyor.com/docs/windows-images-software/#python 11 | # Linux: https://www.appveyor.com/docs/linux-images-software/#python 12 | matrix: 13 | # Use a single global Python version for pipx/poetry/etc. 14 | # Nox will handle specific python version testing 15 | - PYTHON_WIN: C:/Python39 16 | PYENV_38: 3.8.3 17 | PYENV_39: 3.9.5 18 | 19 | APPVEYOR_SAVE_CACHE_ON_ERROR: false 20 | CODECOV_TOKEN: d414dacb-5b8d-4c0b-94e1-42fe8393187d 21 | 22 | # To encrypt passwords, go to Account -> "Settings" -> "Encrypt YAML" 23 | 24 | # Python is now managed with pyenv instead of stack so that a ".venv" is created 25 | # > Must be after the declaration of "PYTHON_STACK" variable 26 | # yamllint disable-line rule:line-length 27 | # > https://help.appveyor.com/discussions/questions/32001-ubuntu-python-3-as-default 28 | # > stack: python %PYTHON_STACK% 29 | 30 | cache: 31 | - .venv -> poetry.lock 32 | - .nox -> poetry.lock 33 | 34 | build: false 35 | 36 | # Specify commands specific to platform (cmd-Windows/sh-Linux/None-Both) 37 | install: 38 | - echo "Install..." 39 | # Force Python to use UTF-8 encoding instead of cp1252 on Windows 40 | - cmd: SET PYTHONUTF8=1 41 | # For Windows, set Python paths based on environment variable from matrix 42 | - cmd: set PATH=%PYTHON_WIN%/Scripts;%PYTHON_WIN%;%PATH% 43 | # Use pyenv instead of the venv-based Python from AppVeyor Ubuntu 44 | - sh: curl https://pyenv.run | $SHELL 45 | # exec "$SHELL" causes AppVeyor to stop, so manually configure paths 46 | - sh: export PYENV_ROOT="$HOME/.pyenv" 47 | - sh: export PATH="$PYENV_ROOT/bin:$PATH" 48 | - sh: eval "$(pyenv init --path)" 49 | # Configure Python versions with pyenv 50 | - sh: pyenv install $PYENV_38 --skip-existing 51 | - sh: pyenv install $PYENV_39 --skip-existing 52 | - sh: pyenv global $PYENV_39 53 | - sh: pyenv local $PYENV_39 $PYENV_38 54 | # Check global Python version 55 | - python --version 56 | # Install pipx to manage CLI installations (poetry, codecov) 57 | - python -m pip install pipx 58 | # Manually set the path because "pipx ensurepath" needs a reload to apply 59 | - cmd: set PATH=%USERPROFILE%\.local\bin;%PATH% 60 | - sh: export PATH=$HOME/.local/bin:$PATH 61 | # Check the PATH 62 | - cmd: echo %PATH% 63 | - sh: echo $PATH 64 | # Install poetry and configure 65 | - python -m pipx install poetry 66 | - poetry config virtualenvs.in-project true 67 | - poetry config --list 68 | # Install project-specific dependencies and extras 69 | - poetry install 70 | 71 | test_script: 72 | - echo "Testing..." 73 | - poetry run doit --continue 74 | # On Windows only, install codecov and upload the coverage results 75 | - cmd: poetry run doit run nox_coverage 76 | - cmd: python -m pipx install codecov 77 | - cmd: codecov --file coverage.json 78 | # Pack up the release into a single zip file 79 | - poetry run doit run zip_release 80 | 81 | deploy_script: 82 | - echo "Deploying..." 83 | 84 | on_success: 85 | - echo "On Success..." 86 | 87 | on_failure: 88 | - echo "On Error..." 89 | 90 | on_finish: 91 | - echo "Build Finish" 92 | 93 | artifacts: 94 | - path: releases/site.zip 95 | - path: releases/tests.zip 96 | -------------------------------------------------------------------------------- /dash_charts/equations.py: -------------------------------------------------------------------------------- 1 | """Equations used in scipy fit calculations.""" 2 | 3 | import numpy as np 4 | 5 | 6 | def linear(x_values, factor_a, factor_b): 7 | """Return result(s) of linear equation with factors of a and b. 8 | 9 | `y = a * x + b` 10 | 11 | Args: 12 | x_values: single number of list of numbers 13 | factor_a: number, slope 14 | factor_b: number, intercept 15 | 16 | Returns: 17 | y_values: as list or single digit 18 | 19 | """ 20 | return np.add( 21 | np.multiply(factor_a, x_values), 22 | factor_b, 23 | ) 24 | 25 | 26 | def quadratic(x_values, factor_a, factor_b, factor_c): 27 | """Return result(s) of quadratic equation with factors of a, b, and c. 28 | 29 | `y = a * x^2 + b * x + c` 30 | 31 | Args: 32 | x_values: single number of list of numbers 33 | factor_a: number 34 | factor_b: number 35 | factor_c: number 36 | 37 | Returns: 38 | y_values: as list or single digit 39 | 40 | """ 41 | return np.add( 42 | np.multiply( 43 | factor_a, 44 | np.power(x_values, 2), 45 | ), 46 | np.add( 47 | np.multiply(factor_b, x_values), 48 | factor_c, 49 | ), 50 | ) 51 | 52 | 53 | def power(x_values, factor_a, factor_b): 54 | """Return result(s) of quadratic equation with factors of a and b. 55 | 56 | `y = a * x^b` 57 | 58 | Args: 59 | x_values: single number of list of numbers 60 | factor_a: number 61 | factor_b: number 62 | 63 | Returns: 64 | y_values: as list or single digit 65 | 66 | """ 67 | return np.multiply( 68 | factor_a, 69 | np.power( 70 | np.array(x_values).astype(float), 71 | factor_b, 72 | ), 73 | ) 74 | 75 | 76 | def exponential(x_values, factor_a, factor_b): 77 | """Return result(s) of exponential equation with factors of a and b. 78 | 79 | `y = a * e^(b * x)` 80 | 81 | Args: 82 | x_values: single number of list of numbers 83 | factor_a: number 84 | factor_b: number 85 | 86 | Returns: 87 | y_values: as list or single digit 88 | 89 | """ 90 | return np.multiply( 91 | factor_a, 92 | np.exp( 93 | np.multiply(factor_b, x_values), 94 | ), 95 | ) 96 | 97 | 98 | def double_exponential(x_values, factor_a, factor_b, factor_c, factor_d): 99 | """Return result(s) of a double exponential equation with factors of a, b, c, and d. 100 | 101 | `y = a * e^(b * x) - c * e^(d * x)` 102 | 103 | Args: 104 | x_values: single number of list of numbers 105 | factor_a: number 106 | factor_b: number 107 | factor_c: number 108 | factor_d: number 109 | 110 | Returns: 111 | y_values: as list or single digit 112 | 113 | """ 114 | return np.subtract( 115 | np.multiply( 116 | factor_a, 117 | np.exp( 118 | np.multiply(factor_b, x_values), 119 | ), 120 | ), 121 | np.multiply( 122 | factor_c, 123 | np.exp( 124 | np.multiply(factor_d, x_values), 125 | ), 126 | ), 127 | ) 128 | -------------------------------------------------------------------------------- /tests/examples/ex_datatable.py: -------------------------------------------------------------------------------- 1 | """Example DataTable. 2 | 3 | TODO: See todo list at bottom!! 4 | 5 | """ 6 | 7 | import dash_bootstrap_components as dbc 8 | import plotly.express as px 9 | from dash import dcc, html 10 | from implements import implements 11 | 12 | from dash_charts.modules_datatable import ModuleFilteredTable 13 | from dash_charts.utils_app import AppBase, AppInterface 14 | from dash_charts.utils_helpers import parse_dash_cli_args 15 | 16 | 17 | # FIXME: AttributeError: 'DataTableDemo' object has no attribute 'ids' 18 | @implements(AppInterface) # noqa: H601 19 | class DataTableDemo(AppBase): 20 | """Example creating a DataTable.""" 21 | 22 | name = 'Example DataTable' 23 | """Application name""" 24 | 25 | external_stylesheets = [dbc.themes.FLATLY] # DARKLY, FLATLY, etc. (https://bootswatch.com/) 26 | """List of external stylesheets. Default is minimal Dash CSS. Only applies if app argument not provided.""" 27 | 28 | data_raw = None 29 | """All in-memory data referenced by callbacks and plotted. If modified, will impact all viewers.""" 30 | 31 | mod_table = None 32 | """Main table (DataTable).""" 33 | 34 | def initialization(self) -> None: 35 | """Initialize ids with `self.register_uniq_ids([...])` and other one-time actions.""" 36 | super().initialization() 37 | # Load sample plotly express data to populate the datatable 38 | self.data_raw = px.data.gapminder() 39 | 40 | # Register modules 41 | self.mod_table = ModuleFilteredTable('filtered_table') 42 | self.modules = [ 43 | self.mod_table, 44 | ] 45 | 46 | def create_elements(self) -> None: 47 | """Initialize charts and tables.""" 48 | ... 49 | 50 | def return_layout(self) -> dict: 51 | """Return Dash application layout. 52 | 53 | Returns: 54 | dict: Dash HTML object 55 | 56 | """ 57 | return dbc.Container([ 58 | dbc.Col([ 59 | dcc.Markdown(self.mod_table.table.filter_summary), 60 | html.Br(), 61 | html.H1(self.name), 62 | self.mod_table.return_layout(self._il, self.data_raw), 63 | ]), 64 | ]) 65 | 66 | def create_callbacks(self) -> None: 67 | """Create Dash callbacks.""" 68 | ... # No callbacks necessary for this simple example 69 | 70 | 71 | instance = DataTableDemo 72 | app = instance() 73 | app.create() 74 | if __name__ == '__main__': 75 | app.run(**parse_dash_cli_args()) 76 | else: 77 | FLASK_HANDLE = app.get_server() 78 | 79 | # TODO: CLICKABLE POPUPS 80 | # - Datatable 81 | # - Have click able icon in first column of table that triggers a dbc modal with additional information 82 | # - Would have layout determined in callback. Could be used to show a timeline, full traceback, or other long 83 | # form data that can't be displayed in condensed table format 84 | # - dbc modal: https://dash-bootstrap-components.opensource.faculty.ai/l/components/modal 85 | # - Would require pattern matching callback: https://dash.plotly.com/pattern-matching-callbacks 86 | 87 | # # TODO: See: https://dash.plot.ly/datatable/interactivity 88 | # > ('datatable-id...', 'derived_virtual_row_ids'), 89 | # > ('datatable-id...', 'selected_row_ids'), 90 | # > ('datatable-id...', 'active_cell'), 91 | 92 | 93 | # TODO: Formatting (Typing): https://dash.plot.ly/datatable/typing 94 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry-core>=1.0.0"] 3 | build-backend = "poetry.core.masonry.api" 4 | 5 | [tool.isort] 6 | balanced_wrapping = true 7 | default_section = "THIRDPARTY" 8 | force_grid_wrap = 0 9 | include_trailing_comma = true 10 | known_first_party = "dash_charts" 11 | length_sort = false 12 | line_length = 120 13 | multi_line_output = 5 14 | 15 | [tool.commitizen] 16 | name = "cz_legacy" 17 | change_type_order = [ 18 | "BREAKING CHANGE", 19 | "Feat", 20 | "Fix", 21 | "Refactor", 22 | "Perf", 23 | "New (Old)", 24 | "Change (Old)", 25 | "Fix (Old)", 26 | ] 27 | version = "0.1.2" 28 | version_files = ["pyproject.toml", "dash_charts/__init__.py", "appveyor.yml"] 29 | 30 | [tool.commitizen.cz_legacy_map] 31 | Chg = "Change (Old)" 32 | Fix = "Fix (Old)" 33 | New = "New (Old)" 34 | 35 | [tool.poetry] 36 | name = "dash_charts" 37 | version = "0.1.2" 38 | description = "Python package for Plotly/Dash apps with support for multi-page, modules, and new charts such as Pareto with an Object Orient Approach" 39 | license = "MIT" 40 | authors = ["Kyle King "] 41 | maintainers = [] 42 | repository = "https://github.com/kyleking/dash_charts" 43 | documentation = "https://github.com/kyleking/dash_charts/docs" 44 | readme = "docs/README.md" 45 | include = ["LICENSE.md"] 46 | keywords = ["plotly-dash", "plotly-python"] 47 | classifiers = [ 48 | "Development Status :: 2 - Pre-Alpha", 49 | "Environment :: Web Environment", 50 | "Framework :: Dash", 51 | "Intended Audience :: Developers", 52 | "License :: OSI Approved :: MIT License", 53 | "Operating System :: OS Independent", 54 | "Programming Language :: Python :: 3.8", 55 | "Programming Language :: Python :: 3.9", 56 | "Topic :: Database :: Front-Ends", 57 | "Topic :: Scientific/Engineering :: Visualization", 58 | "Topic :: Software Development :: Libraries :: Application Frameworks", 59 | ] # https://pypi.org/classifiers/ 60 | # And based on: https://github.com/plotly/dash/blob/6cfb7874800152794d8d603e8d9c4334bf61e3fd/setup.py#L47-L70 61 | 62 | [tool.poetry.urls] 63 | "Bug Tracker" = "https://github.com/kyleking/dash_charts/issues" 64 | "Changelog" = "https://github.com/kyleking/dash_charts/blob/main/docs/docs/CHANGELOG.md" 65 | 66 | [tool.poetry.dependencies] 67 | python = "^3.8" 68 | attrs-strict = ">=0.2.2" 69 | calcipy = ">=0.11.0" 70 | cerberus = ">=1.3.4" 71 | dash-bootstrap-components = ">=1.0.0" 72 | dataset = ">=1.5.2" 73 | dominate = ">=2.6.0" 74 | implements = ">=0.3.0" 75 | lxml = ">=4.7.1" 76 | markdown = ">=3.3.6" 77 | numpy = ">=1.22.2" 78 | Palettable = ">=3.3.0" 79 | pandas = ">=1.3.0" 80 | psutil = ">=5.9.0" 81 | python-box = ">=5.4.1" 82 | scipy = ">=1.6.1" 83 | tqdm = ">=4.62.3" 84 | 85 | # sqlite-utils = "*" 86 | # datasette-vega = "*" 87 | # great-expectations = "*" 88 | 89 | # FIXME: Required only for nox_coverage and optional for subset of use cases 90 | astor = ">=0.8.1" 91 | jsonpickle = ">=2.1.0" 92 | beautifulsoup4 = ">=4.10.0" 93 | 94 | # FIXME: In-progress testing of a better dash table 95 | dash-tabulator = ">=0.4.2" 96 | dash-extensions = ">=0.0.55" 97 | 98 | [tool.poetry.dev-dependencies] 99 | calcipy = { version = "*", extras = [ 100 | "dev", 101 | "lint", 102 | "test", 103 | "commitizen_legacy", 104 | ] } 105 | 106 | # Experimental dependencies 107 | # archan = { git = "https://github.com/pawamoy/archan.git" } 108 | # dependenpy = "^3.3.0" 109 | # archan-pylint = { git = "https://github.com/pawamoy/archan-pylint" } 110 | 111 | # FIXME: testing extras are required for nox-coverage 112 | [tool.poetry.dependencies.dash] 113 | # [tool.poetry.dev-dependencies.dash] 114 | extras = ["testing"] 115 | version = ">=2.0.0" 116 | 117 | [tool.poetry.extras] 118 | matplotlib = ["matplotlib"] 119 | -------------------------------------------------------------------------------- /tests/examples/ex_pareto_chart.py: -------------------------------------------------------------------------------- 1 | """Example Pareto Chart.""" 2 | 3 | from io import StringIO 4 | 5 | import pandas as pd 6 | from dash import html 7 | from implements import implements 8 | 9 | from dash_charts.pareto_chart import ParetoChart 10 | from dash_charts.utils_app import AppBase, AppInterface 11 | from dash_charts.utils_fig import min_graph 12 | from dash_charts.utils_helpers import parse_dash_cli_args 13 | 14 | CSV_DATA = """category,events 15 | Every Cloud Has a Silver Lining,10 16 | Every Cloud Has a Silver Lining,66 17 | SHOULDN'T APPEAR BECAUSE NONE VALUE, 18 | SHOULDN'T APPEAR BECAUSE 0 VALUE,0 19 | SHOULDN'T APPEAR BECAUSE 0 VALUE,0 20 | SHOULDN'T APPEAR BECAUSE 0 VALUE,0 21 | Back To the Drawing Board,30 22 | Back To the Drawing Board,30 23 | Back To the Drawing Board,30 24 | Back To the Drawing Board,30 25 | Back To the Drawing Board,11 26 | Lickety Split,1 27 | Lickety Split,0 28 | Mountain Out of a Molehill,41 29 | Everything But The Kitchen Sink,42 30 | Happy as a Clam,92""" 31 | 32 | 33 | @implements(AppInterface) 34 | class ParetoDemo(AppBase): 35 | """Example creating a simple Pareto chart.""" 36 | 37 | name = 'Example Pareto Chart' 38 | """Application name""" 39 | 40 | data_raw = None 41 | """All in-memory data referenced by callbacks and plotted. If modified, will impact all viewers.""" 42 | 43 | chart_main = None 44 | """Main chart (Pareto).""" 45 | 46 | id_chart = 'pareto' 47 | """Unique name for the main chart.""" 48 | 49 | def initialization(self) -> None: 50 | """Initialize ids with `self.register_uniq_ids([...])` and other one-time actions.""" 51 | super().initialization() 52 | self.register_uniq_ids([self.id_chart]) 53 | # Format sample CSV data for the Pareto 54 | self.data_raw = ( 55 | pd.read_csv(StringIO(CSV_DATA)) 56 | .rename(columns={'events': 'value'}) 57 | ) 58 | 59 | def create_elements(self) -> None: 60 | """Initialize the charts, tables, and other Dash elements.""" 61 | self.chart_main = ParetoChart( 62 | title='Made Up Categories vs. Made Up Counts', 63 | xlabel='Categories', 64 | ylabel='Count', 65 | layout_overrides=( 66 | ('height', None, 500), 67 | ('width', None, 750), 68 | ('showlegend', None, True), 69 | ('legend', None, {'x': 0.6, 'y': 0.8, 'bgcolor': 'rgba(240, 240, 240, 0.49)'}), 70 | ), 71 | ) 72 | # Override Pareto Parameters as needed 73 | self.chart_main.show_count = True 74 | self.chart_main.pareto_colors = {'bar': '#A5AFC8', 'line': '#391D2F'} 75 | 76 | def return_layout(self) -> dict: 77 | """Return Dash application layout. 78 | 79 | Returns: 80 | dict: Dash HTML object 81 | 82 | """ 83 | return html.Div( 84 | style={ 85 | 'maxWidth': '1000px', 86 | 'marginRight': 'auto', 87 | 'marginLeft': 'auto', 88 | }, children=[ 89 | html.H4(children=self.name), 90 | html.Div([ 91 | min_graph( 92 | id=self._il[self.id_chart], 93 | figure=self.chart_main.create_figure(df_raw=self.data_raw), 94 | ), 95 | ]), 96 | ], 97 | ) 98 | 99 | def create_callbacks(self) -> None: 100 | """Create Dash callbacks.""" 101 | ... # No callbacks necessary for this simple example 102 | 103 | 104 | instance = ParetoDemo 105 | app = instance() 106 | app.create() 107 | if __name__ == '__main__': 108 | app.run(**parse_dash_cli_args()) 109 | else: 110 | FLASK_HANDLE = app.get_server() 111 | -------------------------------------------------------------------------------- /tests/test_utils_data.py: -------------------------------------------------------------------------------- 1 | """Test utils_data.""" 2 | 3 | import tempfile 4 | from pathlib import Path 5 | 6 | import pandas as pd 7 | 8 | from dash_charts import utils_data 9 | 10 | 11 | def test_enable_verbose_pandas(): 12 | """Test enable_verbose_pandas.""" 13 | pd.set_option('display.max_columns', 0) 14 | 15 | utils_data.enable_verbose_pandas() # act 16 | 17 | pd.get_option('display.max_columns') is None 18 | 19 | 20 | def test_validate(): 21 | """Test the validate function.""" 22 | schema = { 23 | 'x': { 24 | 'items': [{'type': ['integer', 'float']}, {'type': ['integer', 'float']}], 25 | 'required': True, 26 | 'type': 'list', 27 | }, 28 | 'y': { 29 | 'items': [{'type': ['integer', 'float']}, {'type': ['integer', 'float']}], 30 | 'required': False, 31 | 'type': 'list', 32 | }, 33 | } 34 | pass_doc_1 = {'x': [3, -4.0], 'y': [1e-6, 1e6]} 35 | pass_doc_2 = {'x': [-1e6, 1e6]} 36 | fail_doc_1 = {'x': [1, 2, 3]} 37 | 38 | fail_result = {'x': ['length of list should be 2, it is 3']} # act 39 | 40 | assert utils_data.validate(pass_doc_1, schema) == {} 41 | assert utils_data.validate(pass_doc_2, schema) == {} 42 | assert utils_data.validate(fail_doc_1, schema) == fail_result 43 | 44 | 45 | def test_json_dumps_compact(): 46 | """Test json_dumps_compact.""" 47 | result = utils_data.json_dumps_compact({'A': ['A1', 'A2', 'A3'], 'B': {'C': ['A']}}) 48 | 49 | assert result == """{ 50 | \"A\": [\"A1\",\"A2\",\"A3\"], 51 | \"B\": { 52 | \"C\": [ 53 | \"A\" 54 | ] 55 | } 56 | }""" 57 | 58 | 59 | def test_write_pretty_json(): 60 | """Test write_pretty_json.""" 61 | with tempfile.TemporaryDirectory() as tmp_dir: 62 | json_path = Path(tmp_dir) / 'tmp.json' 63 | utils_data.write_pretty_json(json_path, {'A': [1, 2, 3], 'B': 2}) 64 | 65 | result = json_path.read_text() 66 | 67 | assert result == """{ 68 | \"A\": [ 69 | 1, 70 | 2, 71 | 3 72 | ], 73 | \"B\": 2 74 | }""" 75 | 76 | 77 | def test_write_csv(): 78 | """Test write_csv.""" 79 | with tempfile.TemporaryDirectory() as tmp_dir: 80 | csv_path = Path(tmp_dir) / 'tmp.json' 81 | utils_data.write_csv(csv_path, [['header 1', 'headder 2'], ['row 1', 'a'], ['row 2', 'b']]) 82 | 83 | result = csv_path.read_text() 84 | 85 | assert result == 'header 1,headder 2\nrow 1,a\nrow 2,b\n' 86 | 87 | 88 | def test_get_unix(): 89 | """Test get_unix.""" 90 | result = utils_data.get_unix('31Dec1999', '%d%b%Y') 91 | 92 | assert result == 946616400.0 93 | 94 | 95 | def test_format_unix(): 96 | """Test format_unix.""" 97 | result = utils_data.format_unix(946616400, '%d%b%Y') 98 | 99 | assert result == '31Dec1999' 100 | 101 | 102 | def test_uniq_table_id(): 103 | """Test uniq_table_id.""" 104 | result = utils_data.uniq_table_id() 105 | 106 | assert result.startswith('U') 107 | assert len(result) == 20 108 | 109 | 110 | def test_list_sql_tables(): 111 | """Test list_sql_tables.""" 112 | with tempfile.TemporaryDirectory() as tmp_dir: 113 | db_path = Path(tmp_dir) / 'tmp.db' 114 | with utils_data.SQLConnection(db_path) as conn: 115 | # Create EVENTS table 116 | cursor = conn.cursor() 117 | cursor.execute("""CREATE TABLE EVENTS ( 118 | id INT PRIMARY KEY NOT NULL, 119 | label TEXT NOT NULL 120 | );""") 121 | cursor.execute("""CREATE TABLE MAIN ( 122 | id INT PRIMARY KEY NOT NULL, 123 | value INT NOT NULL 124 | );""") 125 | conn.commit() 126 | 127 | result = utils_data.list_sql_tables(db_path) 128 | 129 | assert result == ['EVENTS', 'MAIN'] 130 | -------------------------------------------------------------------------------- /.diagrams/dash_charts.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | scale 1 3 | skinparam { 4 | dpi 100 5 | shadowing false 6 | linetype ortho 7 | } 8 | 9 | Interface AppInterface { 10 | +name 11 | +ids 12 | +external_stylesheets 13 | +validation_layout 14 | +init_app_kwargs 15 | -__init__(app=None) 16 | +create(assign_layout=True) 17 | +override_module_defaults() 18 | +initialization() 19 | +generate_data() 20 | +register_uniq_ids(app_ids) 21 | +verify_app_initialization() 22 | +create_elements() 23 | +return_layout() 24 | +callback(outputs, inputs, states, pic=False, **kwargs) 25 | +create_callbacks() 26 | +run(**dash_kwargs) 27 | +get_server() 28 | } 29 | 30 | AppInterface <-- AppBase 31 | class AppBase { 32 | +name 33 | +ids 34 | +external_stylesheets 35 | +validation_layout 36 | +init_app_kwargs 37 | +app 38 | -__init__(app=None) 39 | +create(assign_layout=True) 40 | +override_module_defaults() 41 | +initialization() 42 | +generate_data() 43 | +register_uniq_ids(app_ids) 44 | +verify_app_initialization() 45 | +return_layout() 46 | +callback(outputs, inputs, states, pic=False, **kwargs) 47 | +run(**dash_kwargs) 48 | +get_server() 49 | } 50 | 51 | class __module__ { 52 | +ASSETS_DIR 53 | +COUNTER 54 | +STATIC_URLS 55 | +init_app(**app_kwargs) 56 | } 57 | 58 | class ModuleBase { 59 | +all_ids 60 | +name 61 | -_ids 62 | +all_ids 63 | -__init__(name) 64 | +get(_id) 65 | +initialize_mutables() 66 | +create_elements(ids) 67 | +return_layout(ids) 68 | +create_callbacks(parent) 69 | } 70 | 71 | ModuleBase <-- DataCache 72 | class DataCache { 73 | +id_cache 74 | +all_ids 75 | +return_layout(ids, storage_type='memory', **store_kwargs) 76 | +return_write_df_map(df_table) 77 | +read_df(args) 78 | } 79 | 80 | class __module__ { 81 | } 82 | 83 | AppBase <-- AppWithNavigation 84 | class AppWithNavigation { 85 | +app 86 | +nav_lookup 87 | +nav_layouts 88 | +define_nav_elements() 89 | +create(**kwargs) 90 | +initialization() 91 | +create_elements() 92 | +create_callbacks() 93 | } 94 | 95 | AppBase <-- StaticTab 96 | class StaticTab { 97 | +basic_style 98 | +initialization() 99 | +create_elements() 100 | +create_callbacks() 101 | } 102 | 103 | AppWithNavigation <-- AppWithTabs 104 | class AppWithTabs { 105 | +id_tabs_content 106 | +id_tabs_select 107 | +app_ids 108 | +return_layout() 109 | +create_callbacks() 110 | } 111 | 112 | AppWithTabs <-- FullScreenAppWithTabs 113 | class FullScreenAppWithTabs { 114 | +tabs_location 115 | +tabs_margin 116 | +tabs_compact 117 | +verify_app_initialization() 118 | +return_layout() 119 | +generate_tab_kwargs() 120 | +tab_menu() 121 | } 122 | 123 | AppWithNavigation <-- AppMultiPage 124 | class AppMultiPage { 125 | +navbar_links 126 | +dropdown_links 127 | +logo 128 | +id_url 129 | +id_pages_content 130 | +id_toggler 131 | +id_collapse 132 | +app_ids 133 | +return_layout() 134 | +nav_bar() 135 | +create_callbacks() 136 | +select_page_name(pathname) 137 | } 138 | 139 | class __module__ { 140 | +TODO_CLIENT_CALLBACK 141 | } 142 | 143 | class CustomChart { 144 | +annotations 145 | -_axis_range 146 | -_axis_range_schema 147 | +title 148 | +labels 149 | +layout_overrides 150 | +axis_range(){@property} 151 | +axis_range(axis_range){@axis_range.setter} 152 | -__init__(*, title, xlabel, ylabel, layout_overrides=()) 153 | +initialize_mutables() 154 | +create_figure(df_raw, **kwargs_data) 155 | +create_traces(df_raw, **kwargs_data) 156 | +create_layout() 157 | +apply_custom_layout(layout) 158 | } 159 | 160 | CustomChart <-- MarginalChart 161 | class MarginalChart { 162 | +create_figure(df_raw, **kwargs_data) 163 | +create_traces(df_raw, **kwargs_data) 164 | +create_marg_top(df_raw, **kwargs_data) 165 | +create_marg_right(df_raw, **kwargs_data) 166 | +create_layout(*, bg_color='#F0F0F0') 167 | } 168 | 169 | class __module__ { 170 | +FIGURE_PLACEHOLDER 171 | +min_graph(config=None, figure=FIGURE_PLACEHOLDER, **kwargs) 172 | +convert_matplolib(fig) 173 | +check_raw_data(df_raw, min_keys) 174 | +make_dict_an(coord, text, label=None, color=None, y_offset=10) 175 | } 176 | 177 | @enduml 178 | -------------------------------------------------------------------------------- /tests/examples/ex_modules_upload.py: -------------------------------------------------------------------------------- 1 | """Example of the Upload Module.""" 2 | 3 | import dash_bootstrap_components as dbc 4 | from dash import html 5 | from implements import implements 6 | 7 | from dash_charts.components import format_email_pass_id, login_form 8 | from dash_charts.modules_upload import UploadModule 9 | from dash_charts.utils_app import AppBase, AppInterface 10 | from dash_charts.utils_callbacks import map_args, map_outputs 11 | from dash_charts.utils_helpers import parse_dash_cli_args 12 | 13 | 14 | @implements(AppInterface) # noqa: H601 15 | class UploadModuleDemo(AppBase): 16 | """Example using the Upload Module.""" 17 | 18 | name = 'Example Use of the Upload Module' 19 | """Application name""" 20 | 21 | user_info = 'user-info' 22 | """ID used for showing the currently logged in username.""" 23 | 24 | submit_id = 'submit-login' 25 | """ID used for the submit button element.""" 26 | 27 | external_stylesheets = [dbc.themes.FLATLY] # DARKLY, FLATLY, etc. (https://bootswatch.com/) 28 | """List of external stylesheets. Default is minimal Dash CSS. Only applies if app argument not provided.""" 29 | 30 | data_raw = None 31 | """All in-memory data referenced by callbacks and plotted. If modified, will impact all viewers.""" 32 | 33 | mod_upload = None 34 | """Main table (DataTable).""" 35 | 36 | def initialization(self) -> None: 37 | """Initialize ids with `self.register_uniq_ids([...])` and other one-time actions.""" 38 | super().initialization() 39 | self.email_id, self.pass_id = format_email_pass_id(self.submit_id) 40 | self.register_uniq_ids([self.user_info, self.submit_id, self.email_id, self.pass_id]) 41 | 42 | # Register modules 43 | self.mod_upload = UploadModule('filtered_table') 44 | self.modules = [ 45 | self.mod_upload, 46 | ] 47 | 48 | def create_elements(self) -> None: 49 | """Initialize charts and tables.""" 50 | ... 51 | 52 | def _show_current_user(self, username): 53 | 54 | return f'(Currently logged in as: {username})' if username else '(Not Logged In)' 55 | 56 | def return_layout(self) -> dict: 57 | """Return Dash application layout. 58 | 59 | Returns: 60 | dict: Dash HTML object 61 | 62 | """ 63 | return dbc.Container([ # noqa: ECE001 64 | dbc.Col([ 65 | html.H1(self.name), 66 | html.Hr(), 67 | dbc.Row([ 68 | dbc.Col([ 69 | login_form(self._il[self.submit_id]), 70 | ]), 71 | dbc.Col([ 72 | html.Div([self._show_current_user(None)], id=self._il[self.user_info]), 73 | ]), 74 | ]), 75 | html.Hr(), 76 | self.mod_upload.return_layout(self._il), 77 | ]), 78 | ]) 79 | 80 | def create_callbacks(self) -> None: 81 | """Create Dash callbacks.""" 82 | outputs = [(self.user_info, 'children'), (self.mod_upload.get(self.mod_upload.id_username_cache), 'data')] 83 | inputs = [(self.submit_id, 'n_clicks')] 84 | states = [(self.email_id, 'value'), (self.pass_id, 'value')] 85 | 86 | @self.callback(outputs, inputs, states, pic=False) 87 | def login_handler(*raw_args): 88 | a_in, a_state = map_args(raw_args, inputs, states) 89 | email = a_state[self.email_id]['value'] 90 | # password = a_state[self.pass_id]['value'] # noqa: E800 91 | print("WARN: The password isn't authenticated. This is just a placeholder") # noqa: T001 92 | 93 | return map_outputs( 94 | outputs, [ 95 | (self.user_info, 'children', self._show_current_user(email)), 96 | (self.mod_upload.get(self.mod_upload.id_username_cache), 'data', email), 97 | ], 98 | ) 99 | 100 | 101 | instance = UploadModuleDemo 102 | app = instance() 103 | app.create() 104 | if __name__ == '__main__': 105 | app.run(**parse_dash_cli_args()) 106 | else: 107 | FLASK_HANDLE = app.get_server() 108 | -------------------------------------------------------------------------------- /docs/docs/DEVELOPER_GUIDE.md: -------------------------------------------------------------------------------- 1 | # Developer Notes 2 | 3 | ## Local Development 4 | 5 | ```sh 6 | git clone https://github.com/kyleking/dash_charts.git 7 | cd dash_charts 8 | poetry install 9 | 10 | # See the available tasks 11 | poetry run doit list 12 | 13 | # Run the default task list (lint, auto-format, test coverage, etc.) 14 | poetry run doit --continue 15 | 16 | # Make code changes and run specific tasks as needed: 17 | poetry run doit run test 18 | ``` 19 | 20 | ## Publishing 21 | 22 | For testing, create an account on [TestPyPi](https://test.pypi.org/legacy/). Replace `...` with the API token generated on TestPyPi|PyPi respectively 23 | 24 | ```sh 25 | poetry config repositories.testpypi https://test.pypi.org/legacy/ 26 | poetry config pypi-token.testpypi ... 27 | 28 | poetry run doit run publish_test_pypi 29 | # If you didn't configure a token, you will need to provide your username and password to publish 30 | ``` 31 | 32 | To publish to the real PyPi 33 | 34 | ```sh 35 | poetry config pypi-token.pypi ... 36 | poetry run doit run publish 37 | 38 | # For a full release, triple check the default tasks, increment the version, rebuild documentation, and publish! 39 | poetry run doit run --continue 40 | poetry run doit run cl_bump lock document deploy_docs publish 41 | 42 | # For pre-releases use cl_bump_pre 43 | poetry run doit run cl_bump_pre -p rc 44 | poetry run doit run lock document deploy_docs publish 45 | ``` 46 | 47 | ## Current Status 48 | 49 | 50 | | File | Statements | Missing | Excluded | Coverage | 51 | |:-------------------------------------------|-------------:|----------:|-----------:|:-----------| 52 | | `dash_charts/__init__.py` | 4 | 0 | 0 | 100.0% | 53 | | `dash_charts/app_px.py` | 130 | 11 | 0 | 91.5% | 54 | | `dash_charts/components.py` | 13 | 0 | 0 | 100.0% | 55 | | `dash_charts/coordinate_chart.py` | 102 | 1 | 6 | 99.0% | 56 | | `dash_charts/custom_colorscales.py` | 3 | 0 | 0 | 100.0% | 57 | | `dash_charts/datatable.py` | 79 | 25 | 0 | 68.4% | 58 | | `dash_charts/equations.py` | 11 | 0 | 0 | 100.0% | 59 | | `dash_charts/gantt_chart.py` | 54 | 0 | 0 | 100.0% | 60 | | `dash_charts/modules_datatable.py` | 100 | 33 | 0 | 67.0% | 61 | | `dash_charts/modules_upload.py` | 130 | 60 | 0 | 53.8% | 62 | | `dash_charts/pareto_chart.py` | 42 | 0 | 2 | 100.0% | 63 | | `dash_charts/scatter_line_charts.py` | 45 | 0 | 3 | 100.0% | 64 | | `dash_charts/time_vis_chart.py` | 61 | 0 | 0 | 100.0% | 65 | | `dash_charts/utils_app.py` | 103 | 14 | 6 | 86.4% | 66 | | `dash_charts/utils_app_modules.py` | 26 | 3 | 4 | 88.5% | 67 | | `dash_charts/utils_app_with_navigation.py` | 118 | 9 | 6 | 92.4% | 68 | | `dash_charts/utils_callbacks.py` | 31 | 6 | 0 | 80.6% | 69 | | `dash_charts/utils_data.py` | 63 | 1 | 0 | 98.4% | 70 | | `dash_charts/utils_dataset.py` | 76 | 43 | 0 | 43.4% | 71 | | `dash_charts/utils_fig.py` | 74 | 3 | 4 | 95.9% | 72 | | `dash_charts/utils_helpers.py` | 19 | 8 | 7 | 57.9% | 73 | | `dash_charts/utils_json_cache.py` | 51 | 10 | 0 | 80.4% | 74 | | `dash_charts/utils_static.py` | 111 | 5 | 0 | 95.5% | 75 | | `dash_charts/utils_static_toc.py` | 22 | 1 | 0 | 95.5% | 76 | | **Totals** | 1468 | 233 | 38 | 84.1% | 77 | 78 | Generated on: 2022-08-04T20:47:35.216758 79 | 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------------------------------------------------------------- 2 | # General Python Ignore Patterns from Github and https://www.gitignore.io/api/python 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # Mr Developer 124 | .mr.developer.cfg 125 | .project 126 | .pydevproject 127 | 128 | # mkdocs documentation 129 | /site 130 | 131 | # mypy 132 | .mypy_cache/ 133 | .dmypy.json 134 | dmypy.json 135 | 136 | # Pyre type checker 137 | .pyre/ 138 | 139 | # ---------------------------------------------------------------------------------------------------------------------- 140 | # Common Rules 141 | 142 | # General 143 | .r* 144 | .vscode/* 145 | *.csv 146 | *.doc* 147 | *.jsonl 148 | *.lnk 149 | *.log 150 | *.pdf 151 | *.png 152 | *.jp*g 153 | *.spe 154 | *.svg 155 | *.xl* 156 | ~*.* 157 | ~$* 158 | 159 | # SQLite DB files (Note: use *.dvc to check these into git) 160 | *.db 161 | *.sqlite 162 | *.sqlite-journal 163 | 164 | # Other Python 165 | *.whl 166 | *.tar.gz 167 | setup.py 168 | 169 | # Coverage (for Pytest) 170 | **/coverage* 171 | **/cov_html/ 172 | coverage.json 173 | 174 | # Other Custom Pytest 175 | /tests/_tmp_cache/** 176 | 177 | # Other Custom Pytest 178 | /tests/_tmp_cache/** 179 | 180 | # Static Type Checkers 181 | .pytype/* 182 | 183 | # Ignore DOIT cache files 184 | **/.doit.* 185 | 186 | # Ignore testmon cache files 187 | **/.testmondata 188 | 189 | # Built Outputs 190 | /releases/ 191 | 192 | # Ignore auto-created files by documentation tasks 193 | /docs/modules/*.md 194 | /docs/modules/**/*.md 195 | 196 | # Allow List 197 | !/docs/*.* 198 | !supporting/** 199 | !tests/data/** 200 | 201 | # Ensure *.pyc files are still ignored even if in tests/data directory 202 | *.pyc 203 | 204 | # ---------------------------------------------------------------------------------------------------------------------- 205 | # Custom Rules 206 | 207 | /tests/examples/example_write_from_markdown.html 208 | 209 | # Upload Module Database Directory 210 | **/local_cache/* 211 | !/tests/data/* 212 | !/.diagrams/*.png 213 | -------------------------------------------------------------------------------- /dash_charts/utils_app_modules.py: -------------------------------------------------------------------------------- 1 | """Utilities to build modules (delegated layout & callback methods) for Dash apps.""" 2 | 3 | import pandas as pd 4 | from dash import dcc 5 | 6 | 7 | class ModuleBase: # noqa: H601 8 | """Base class for building a modular component for use in a Dash application.""" 9 | 10 | all_ids = None 11 | """List of ids to register for this module.""" 12 | 13 | def __init__(self, name): 14 | """Initialize module. 15 | 16 | Args: 17 | name: unique string name for this module 18 | 19 | Raises: 20 | NotImplementedError: if child class has not created a list, `all_ids` 21 | 22 | """ 23 | if self.all_ids is None: 24 | raise NotImplementedError('Child class must create list of `self.all_ids`') # pragma: no cover 25 | 26 | # Make ids unique and update all ids so that modules can be reused in the same app 27 | self.name = name 28 | self._ids = {_id: f'{self.name}__{_id}' for _id in self.all_ids} 29 | self.all_ids = self._ids.values() 30 | 31 | self.initialize_mutables() 32 | 33 | def get(self, _id): 34 | """Return the the callback for creating the main chart. 35 | 36 | Args: 37 | _id: id from this module that is found in `self.all_ids` 38 | 39 | Returns: 40 | str: unique id name from instance of this module 41 | 42 | """ 43 | return self._ids[_id] 44 | 45 | def initialize_mutables(self): 46 | """Initialize the mutable data members to prevent modifying one attribute and impacting all instances.""" 47 | ... 48 | 49 | def create_elements(self, ids): 50 | """Register the callback for creating the main chart. 51 | 52 | Args: 53 | ids: `self._il` from base application 54 | 55 | """ 56 | ... # pragma: no cover 57 | 58 | def return_layout(self, ids): 59 | """Return Dash application layout. 60 | 61 | Args: 62 | ids: `self._il` from base application 63 | 64 | Raises: 65 | NotImplementedError: Dash HTML object. Default is simple HTML text 66 | 67 | """ 68 | raise NotImplementedError('Must be implemented') # pragma: no cover 69 | 70 | def create_callbacks(self, parent): 71 | """Register callbacks to handle user interaction. 72 | 73 | Args: 74 | parent: parent instance (ex: `self`) 75 | 76 | """ 77 | ... # pragma: no cover 78 | 79 | 80 | class DataCache(ModuleBase): # noqa: H601 81 | """Module to store data in UI and later loaded as needed.""" 82 | 83 | id_cache = 'cache' 84 | """Session ID.""" 85 | 86 | all_ids = [id_cache] 87 | """List of ids to register for this module.""" 88 | 89 | def return_layout(self, ids, storage_type='memory', **store_kwargs): 90 | """Return Dash application layout. 91 | 92 | `dcc.Store` documentation: https://dash.plotly.com/dash-core-components/store 93 | 94 | Args: 95 | ids: `self._il` from base application 96 | storage_type: `dcc.Store` storage type. Default is memory to clear on refresh 97 | store_kwargs: additional keyword arguments to pass to `dcc.Store` 98 | 99 | Returns: 100 | dict: Dash HTML object. 101 | 102 | """ 103 | return dcc.Store(id=ids[self.get(self.id_cache)], storage_type=storage_type, **store_kwargs) 104 | 105 | def return_write_df_map(self, df_table): 106 | """Return list of tuples for `map_outputs` that includes the new data cache JSON. 107 | 108 | Args: 109 | df_table: dataframe to show in table 110 | 111 | Returns: 112 | list: list of tuples for `map_outputs` 113 | 114 | """ 115 | return [(self.get(self.id_cache), 'data', df_table.to_json())] 116 | 117 | def read_df(self, args): 118 | """Return list of tuples for `map_outputs` that includes the new data cache JSON. 119 | 120 | Args: 121 | args: either `a_in` or `a_state`, whichever has the id_cache-data 122 | 123 | Returns: 124 | dataframe: returns dataframe read from `dcc.Store` 125 | 126 | """ 127 | return pd.read_json(args[self.get(self.id_cache)]['data']) 128 | -------------------------------------------------------------------------------- /docs/CODE_TAG_SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Task Summary 2 | 3 | Auto-Generated by dash_charts 4 | 5 | - dash_charts/app_px.py 6 | - line 65 PLANNED: template should be able to be None 7 | - line 83 PLANNED: below items should be able to be None 8 | - line 187 FIXME: replace tabs-select with actual keyname (?) 9 | 10 | - dash_charts/coordinate_chart.py 11 | - line 19 PLANNED: subplots for multiple years of calendar charts (Subplot title is year) 12 | - line 335 PLANNED: make this configureable 13 | 14 | - dash_charts/datatable.py 15 | - line 5 TODO: See pattern mathing callbacks for adding buttons (to show modal) to datatables 16 | - line 8 PLANNED: see conditional formatting: https://dash.plotly.com/datatable/conditional-formatting 17 | - line 10 PLANNED: These methods may be replaced in a future version of Dash 18 | - line 87 PLANNED: Maybe move parameters to attr.ib classes? 19 | 20 | - dash_charts/modules_upload.py 21 | - line 76 PLANNED: Revisit. Should filename be a name or the full path? 22 | - line 371 TODO: Add delete button for each table - need pattern matching callback: 23 | - line 420 FIXME: Better handle NaN values... 24 | 25 | - dash_charts/time_vis_chart.py 26 | - line 5 NOTE: Consider automated (non-overlapping) text/event placement 27 | 28 | - dash_charts/utils_app.py 29 | - line 156 FIXME: Need to decide if there is a better approach. Reading this code is confusing... 30 | 31 | - dash_charts/utils_app_with_navigation.py 32 | - line 34 TODO: Try to see if I can resolve the interface differences or if I need make a subclass interface 33 | - line 154 PLANNED: Make the tabs and chart compact as well when the compact argument is set to True 34 | - line 361 TODO: Demo how pages could use parameters from pathname 35 | 36 | - dash_charts/utils_data.py 37 | - line 24 TODO: what does this set? 38 | - line 84 PLANNED: Convert to FP and recursive calls? 39 | 40 | - dash_charts/utils_json_cache.py 41 | - line 14 FIXME: Add versioning to the cache directory with semver logic: https://pypi.org/project/semantic-version/ 42 | - line 32 TODO: Enable versioning of data and automatic deletion when the version changes 43 | 44 | - dash_charts/utils_static_toc.py 45 | - line 43 FIXME: Figure out how to make the header links work (i.e. when clicked in TOC go to the respective header) 46 | 47 | - docs/README.md 48 | - line 5 TODO: Currently not online ) 49 | - line 48 FIXME: Keep updates up to date! --> 50 | - line 98 FIXME: the change to use Box/_ID needs to be implemented in the examples. This is causing failures in the test cases 51 | - line 421 TODO: See https://github.com/KyleKing/calcipy/issues/38 --> 52 | - line 436 TODO: Show an example (screenshots, terminal recording, etc.) --> 53 | 54 | - dodo.py 55 | - line 31 PLANNED: Move all of this into a function! (and/or task?) 56 | - line 53 TODO: pypi package wasn't working. Used local version 57 | - line 56 PLANNED: needs to be a bit more efficient... 58 | 59 | - scripts/jsonl_viewer.py 60 | - line 79 TODO: CLICKABLE POPUPS 61 | - line 87 TODO: See: https://dash.plot.ly/datatable/interactivity 62 | - line 93 TODO: Formatting (Typing): https://dash.plot.ly/datatable/typing 63 | 64 | - tests/configuration.py 65 | - line 15 PLANNED: Move all of this into a function! (and/or task?) {Duplicate of dodo.py} 66 | - line 29 PLANNED: Output the test name and other information to the test.log file. Currently only used in `no_log_errors` 67 | - line 30 PLANNED: move to dash_dev 68 | - line 47 HACK: get_logs always return None with webdrivers other than Chrome 69 | - line 48 FIXME: Handle path to the executable. Example with Firefox when the Gecko Drive is installed and on path 70 | 71 | - tests/examples/ex_coordinate_chart.py 72 | - line 17 TODO: Also set marker size based on value? 73 | - line 18 TODO: Re-align alignment charts into line and update screenshot 74 | - line 19 TODO: Maybe green heat map like Github? For one year? 75 | 76 | - tests/examples/ex_datatable.py 77 | - line 18 FIXME: AttributeError: 'DataTableDemo' object has no attribute 'ids' 78 | - line 80 TODO: CLICKABLE POPUPS 79 | - line 88 TODO: See: https://dash.plot.ly/datatable/interactivity 80 | - line 94 TODO: Formatting (Typing): https://dash.plot.ly/datatable/typing 81 | 82 | - tests/examples/ex_style_bootstrap.py 83 | - line 92 TODO: Decide which styles from Bulma should be compared here 84 | 85 | Found code tags for FIXME (9), TODO (20), PLANNED (15), HACK (1), NOTE (1) 86 | -------------------------------------------------------------------------------- /dash_charts/utils_callbacks.py: -------------------------------------------------------------------------------- 1 | """Utilities for better Dash callbacks.""" 2 | 3 | import re 4 | 5 | import dash 6 | from dash.dependencies import Input, Output, State 7 | from dash.exceptions import PreventUpdate 8 | 9 | 10 | def format_app_callback(lookup, outputs, inputs, states): 11 | """Format list of [Output, Input, State] for `@app.callback()`. 12 | 13 | Args: 14 | lookup: dict with app_id keys that map to a globally unique component id 15 | outputs: list of tuples with app_id and property name 16 | inputs: list of tuples with app_id and property name 17 | states: list of tuples with app_id and property name 18 | 19 | Returns: 20 | list: list[lists] in order `(Outputs, Inputs, States)` for `@app.callback()`. Some sublists may be empty 21 | 22 | """ 23 | return ( 24 | [Output(component_id=lookup[_id], component_property=prop) for _id, prop in outputs], 25 | [Input(component_id=lookup[_id], component_property=prop) for _id, prop in inputs], 26 | [State(component_id=lookup[_id], component_property=prop) for _id, prop in states], 27 | ) 28 | 29 | 30 | def map_args(raw_args, inputs, states): 31 | """Map the function arguments into a dictionary with keys for the input and state names. 32 | 33 | For situations where the order of inputs and states may change, use this function to verbosely define the inputs: 34 | 35 | ```python 36 | a_in, a_state = map_args(raw_args, inputs, states) 37 | click_data = a_in[self.id_main_figure]['clickData'] 38 | n_clicks = a_in[self.id_randomize_button]['n_clicks'] 39 | data_cache = a_state[self.id_store]['data'] 40 | ``` 41 | 42 | Alternatively, for use cases that are unlikely to change the order of Inputs/State, unwrap positionally with: 43 | 44 | ```python 45 | click_data, n_clicks = args[:len(inputs)] 46 | data_cache = args[len(inputs):] 47 | ``` 48 | 49 | Args: 50 | raw_args: list of arguments passed to callback 51 | inputs: list of input components. May be empty list 52 | states: list of state components. May be empty list 53 | 54 | Returns: 55 | dict: with keys of the app_id, property, and arg value (`a_in[key][arg_type]`) 56 | 57 | """ 58 | # Split args into groups of inputs/states 59 | a_in = raw_args[:len(inputs)] 60 | a_state = raw_args[len(inputs):] 61 | 62 | # Map args into dictionaries 63 | arg_map = [{app_id: [] for app_id in {items[0] for items in group}} for group in [inputs, states]] 64 | for group_idx, (groups, args) in enumerate([(inputs, a_in), (states, a_state)]): 65 | # Assign the arg to the appropriate dictionary in arg_map 66 | for arg_idx, (app_id, prop) in enumerate(groups): 67 | arg_map[group_idx][app_id].append((prop, args[arg_idx])) 68 | for app_id in arg_map[group_idx].keys(): 69 | arg_map[group_idx][app_id] = dict(arg_map[group_idx][app_id]) 70 | return arg_map 71 | 72 | 73 | def map_outputs(outputs, element_info): 74 | """Return properly ordered list of new Dash elements based on the order of outputs. 75 | 76 | Alternatively, for simple cases of 1-2 outputs, just return the list with: 77 | 78 | ```python 79 | return [new_element_1, new_element_2] 80 | ``` 81 | 82 | Args: 83 | outputs: list of output components 84 | element_info: list of tuples with keys `(app_id, prop, element)` 85 | 86 | Returns: 87 | list: ordered list to match the order of outputs 88 | 89 | Raises: 90 | RuntimeError: Check that the number of outputs and the number of element_info match 91 | 92 | """ 93 | if len(outputs) != len(element_info): 94 | raise RuntimeError(f'Expected same number of items between:\noutputs:{outputs}\nelement_info:{element_info}') 95 | 96 | # Create a dictionary of the elements 97 | lookup = {app_id: [] for app_id in {info[0] for info in element_info}} 98 | for app_id, prop, element in element_info: 99 | lookup[app_id].append((prop, element)) 100 | for app_id in lookup: 101 | lookup[app_id] = dict(lookup[app_id]) 102 | 103 | return [lookup[app_id][prop] for app_id, prop in outputs] 104 | 105 | 106 | def get_triggered_id(): 107 | """Use Dash context to get the id of the input element that triggered the callback. 108 | 109 | See advanced callbacks: https://dash.plotly.com/advanced-callbacks 110 | 111 | Returns: 112 | str: id of the input that triggered the callback 113 | 114 | Raises: 115 | PreventUpdate: if callback was fired without an input 116 | 117 | """ 118 | ctx = dash.callback_context 119 | if not ctx.triggered: 120 | raise PreventUpdate 121 | 122 | prop_id = ctx.triggered[0]['prop_id'] # in format: `id.key` where we only want the `id` 123 | return re.search(r'(^.+)\.[^\.]+$', prop_id).group(1) 124 | -------------------------------------------------------------------------------- /tests/examples/ex_tabs.py: -------------------------------------------------------------------------------- 1 | """Example Tabbed Applet.""" 2 | 3 | import plotly.express as px 4 | from dash import dcc, html 5 | 6 | from dash_charts.utils_app_with_navigation import AppWithTabs, StaticTab 7 | from dash_charts.utils_fig import min_graph 8 | from dash_charts.utils_helpers import parse_dash_cli_args 9 | 10 | 11 | class TabZero(StaticTab): 12 | """Tab Zero.""" 13 | 14 | name = 'Tab Name for Tab Zero' 15 | 16 | def return_layout(self) -> dict: 17 | """Return Dash application layout. 18 | 19 | Returns: 20 | dict: Dash HTML object 21 | 22 | """ 23 | return html.Div( 24 | style=self.basic_style, children=( 25 | [html.H1(children=f'{self.name} Scrollable Content')] 26 | + [html.P(children=[str(count) + '-word' * 10]) for count in range(100)] 27 | ), 28 | ) 29 | 30 | 31 | class TabOne(StaticTab): 32 | """Tab One.""" 33 | 34 | name = 'Tab Name for Tab One' 35 | 36 | def return_layout(self) -> dict: 37 | """Return Dash application layout. 38 | 39 | Returns: 40 | dict: Dash HTML object 41 | 42 | """ 43 | return html.Div( 44 | style=self.basic_style, children=[ 45 | html.H1(children=f'Image from {self.name}'), 46 | html.Img(src='https://media.giphy.com/media/JGQe5mxayVF04/giphy.gif'), 47 | ], 48 | ) 49 | 50 | 51 | class TabTwo(StaticTab): 52 | """Tab Two.""" 53 | 54 | name = 'Tab Name for Tab Two' 55 | 56 | def return_layout(self) -> dict: 57 | """Return Dash application layout. 58 | 59 | Returns: 60 | dict: Dash HTML object 61 | 62 | """ 63 | return html.Div( 64 | style=self.basic_style, children=[ 65 | html.H1(children=f'{self.name} Chart'), 66 | dcc.Loading( 67 | type='circle', 68 | children=[ 69 | min_graph(figure=px.scatter(px.data.iris(), x='sepal_width', y='sepal_length', height=500)), 70 | ], 71 | ), 72 | ], 73 | ) 74 | 75 | 76 | class TabThree(StaticTab): 77 | """Tab Three.""" 78 | 79 | name = 'Tab Name for Tab Three' 80 | 81 | def return_layout(self) -> dict: 82 | """Return Dash application layout. 83 | 84 | Returns: 85 | dict: Dash HTML object 86 | 87 | """ 88 | return html.Div( 89 | style=self.basic_style, children=[ 90 | html.H1(children=f'{self.name} Chart'), 91 | dcc.Loading( 92 | type='cube', 93 | children=[ 94 | min_graph( 95 | figure=px.scatter( 96 | px.data.iris(), x='sepal_width', y='sepal_length', color='species', 97 | marginal_y='rug', marginal_x='histogram', height=500, 98 | ), 99 | ), 100 | ], 101 | ), 102 | ], 103 | ) 104 | 105 | 106 | # ---------------------------------------------------------------------------------------------------------------------- 107 | 108 | 109 | class TabAppDemo(AppWithTabs): # noqa: H601 110 | """Demo application.""" 111 | 112 | name = 'TabAppDemo' 113 | 114 | tabs_location = 'right' 115 | """Tab orientation setting. One of `(left, top, bottom, right)`.""" 116 | 117 | def define_nav_elements(self): 118 | """Return list of initialized tabs. 119 | 120 | Returns: 121 | list: each item is an initialized tab (ex `[AppBase(self.app)]` in the order each tab is rendered 122 | 123 | """ 124 | return [ 125 | TabZero(app=self.app), 126 | TabOne(app=self.app), 127 | TabTwo(app=self.app), 128 | TabThree(app=self.app), 129 | ] 130 | 131 | def return_layout(self) -> dict: 132 | """Return Dash application layout. 133 | 134 | Returns: 135 | dict: Dash HTML object 136 | 137 | """ 138 | side_padding = {'padding': '10px 0 0 10px'} 139 | return html.Div([ 140 | html.H3('Application with Tabbed Content Demo', style=side_padding), 141 | html.P( 142 | 'AppWithTabs is rendered inline, while FullScreenAppWithTabs has a navigation element fixed to the' 143 | 'viewport. See the px app for an example with full screen and below for an example with' 144 | 'AppWithTabs.', style=side_padding, 145 | ), 146 | html.Hr(), 147 | super().return_layout(), 148 | html.Hr(), 149 | html.P('Additional content, like tables, upload module, etc. could go here', style=side_padding), 150 | ]) 151 | 152 | 153 | instance = TabAppDemo 154 | app = instance() 155 | app.create() 156 | if __name__ == '__main__': 157 | app.run(**parse_dash_cli_args()) 158 | else: 159 | FLASK_HANDLE = app.get_server() 160 | -------------------------------------------------------------------------------- /tests/examples/ex_rolling_chart.py: -------------------------------------------------------------------------------- 1 | """Example Rolling Mean and Filled Standard Deviation Chart.""" 2 | 3 | import numpy as np 4 | import pandas as pd 5 | import plotly.graph_objects as go 6 | from dash import dcc, html 7 | from implements import implements 8 | 9 | from dash_charts.scatter_line_charts import RollingChart 10 | from dash_charts.utils_app import AppBase, AppInterface 11 | from dash_charts.utils_callbacks import map_args, map_outputs 12 | from dash_charts.utils_fig import make_dict_an, min_graph 13 | from dash_charts.utils_helpers import parse_dash_cli_args 14 | 15 | 16 | @implements(AppInterface) # noqa: H601 17 | class RollingDemo(AppBase): 18 | """Example creating a rolling mean chart.""" 19 | 20 | name = 'Example Rolling Chart' 21 | """Application name""" 22 | 23 | data_raw = None 24 | """All in-memory data referenced by callbacks and plotted. If modified, will impact all viewers.""" 25 | 26 | chart_main = None 27 | """Main chart (Rolling).""" 28 | 29 | id_slider = 'slider' 30 | """Slider ID.""" 31 | 32 | id_chart = 'rolling' 33 | """Unique name for the main chart.""" 34 | 35 | def initialization(self) -> None: 36 | """Initialize ids with `self.register_uniq_ids([...])` and other one-time actions.""" 37 | super().initialization() 38 | self.register_uniq_ids([self.id_slider, self.id_chart]) 39 | 40 | def generate_data(self) -> None: 41 | """Create self.data_raw with sample data.""" 42 | # Generate random data points 43 | count = 1000 44 | mu, sigma = (15, 10) # mean and standard deviation 45 | samples = np.random.normal(mu, sigma, count) 46 | # Add a break at the mid-point 47 | mid_count = count / 2 48 | y_vals = [samples[_i] + (-1 if _i > mid_count else 1) * _i / 10.0 for _i in range(count)] 49 | 50 | # Combine into a dataframe 51 | self.data_raw = pd.DataFrame( 52 | data={ 53 | 'x': range(count), 54 | 'y': y_vals, 55 | 'label': [f'Point {idx}' for idx in range(count)], 56 | }, 57 | ) 58 | 59 | def create_elements(self) -> None: 60 | """Initialize the charts, tables, and other Dash elements.""" 61 | self.chart_main = RollingChart( 62 | title='Sample Timeseries Chart with Rolling Calculations', 63 | xlabel='Index', 64 | ylabel='Measured Value', 65 | ) 66 | # Add some example annotations 67 | colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#e377c2', '#7f7f7f', '#17becf', None] 68 | count = 1000 69 | y_offset = np.mean(self.data_raw['y']) - np.amin(self.data_raw['y']) 70 | for idx, color in enumerate(colors): 71 | label = f'Additional Information for index {idx + 1} and color {color}' 72 | coord = [self.data_raw[ax][20 + int(idx * count / len(colors))] for ax in ['x', 'y']] 73 | self.chart_main.annotations.append( 74 | go.layout.Annotation( 75 | **make_dict_an(coord, str(idx + 1), label, color, y_offset), 76 | ), 77 | ) 78 | 79 | def return_layout(self) -> dict: 80 | """Return Dash application layout. 81 | 82 | Returns: 83 | dict: Dash HTML object 84 | 85 | """ 86 | step = 50 87 | slider_max = 1000 88 | return html.Div( 89 | style={ 90 | 'maxWidth': '1000px', 91 | 'marginRight': 'auto', 92 | 'marginLeft': 'auto', 93 | }, children=[ 94 | html.H4(children=self.name), 95 | min_graph(id=self._il[self.id_chart], figure=self.chart_main.create_figure(self.data_raw)), 96 | dcc.RangeSlider( 97 | id=self._il[self.id_slider], min=0, max=slider_max, step=step / 5, value=[150, 825], 98 | marks={str(idx * step): str(idx * step) for idx in range(slider_max // step)}, 99 | ), 100 | ], 101 | ) 102 | 103 | def create_callbacks(self) -> None: 104 | """Create Dash callbacks.""" 105 | outputs = [(self.id_chart, 'figure')] 106 | inputs = [(self.id_slider, 'value')] 107 | states = [] 108 | 109 | @self.callback(outputs, inputs, states, pic=True) 110 | def update_chart(*raw_args): 111 | a_in, a_states = map_args(raw_args, inputs, states) 112 | slider = a_in[self.id_slider]['value'] 113 | df_filtered = self.data_raw[(self.data_raw['x'] >= slider[0]) & (self.data_raw['x'] <= slider[1])] 114 | self.chart_main.axis_range = {'x': slider} 115 | new_figure = self.chart_main.create_figure(df_raw=df_filtered) 116 | 117 | # See: https://plot.ly/python/range-slider/ 118 | new_figure['layout']['xaxis']['rangeslider'] = {'visible': True} 119 | return map_outputs(outputs, [(self.id_chart, 'figure', new_figure)]) 120 | 121 | 122 | instance = RollingDemo 123 | app = instance() 124 | app.create() 125 | if __name__ == '__main__': 126 | app.run(**parse_dash_cli_args()) 127 | else: 128 | FLASK_HANDLE = app.get_server() 129 | -------------------------------------------------------------------------------- /dash_charts/pareto_chart.py: -------------------------------------------------------------------------------- 1 | """Pareto Chart.""" 2 | 3 | import pandas as pd 4 | import plotly.graph_objects as go 5 | 6 | from .utils_data import append_df, validate 7 | from .utils_fig import CustomChart, check_raw_data 8 | 9 | 10 | def tidy_pareto_data(df_raw, cap_categories): 11 | """Return compressed Pareto dataframe of only the unique values. 12 | 13 | Args: 14 | df_raw: pandas dataframe with at minimum the two columns `category: str` and `value: float` 15 | cap_categories: Maximum number of categories (bars) 16 | 17 | Returns: 18 | dataframe: pandas dataframe with columns `(value, label, counts, cum_per)` 19 | 20 | """ 21 | df_p = None 22 | for cat in df_raw['category'].unique(): 23 | df_row = pd.DataFrame( 24 | data={ 25 | 'label': [cat], 26 | 'value': [df_raw.loc[df_raw['category'] == cat]['value'].sum()], 27 | 'counts': df_raw['category'].value_counts()[cat], 28 | }, 29 | ) 30 | df_p = append_df(df_p, df_row) 31 | # Sort and calculate percentage 32 | df_p = ( 33 | df_p[df_p['value'] != 0] 34 | .sort_values(by=['value'], ascending=False) 35 | .head(cap_categories) 36 | ) 37 | df_p['cum_per'] = df_p['value'].divide(df_p['value'].sum()).cumsum() 38 | return df_p 39 | 40 | 41 | class ParetoChart(CustomChart): 42 | """Pareto Chart: both bar and line graph chart for strategic decision making.""" 43 | 44 | cap_categories: int = 20 45 | """Maximum number of categories (bars). Default is 20.""" 46 | 47 | show_count: bool = True 48 | """If True, will show numeric count on each bar. Default is True.""" 49 | 50 | yaxis_2_label: str = 'Cumulative Percentage' 51 | """Label for yaxis 2 that shows the cumulative percentage.""" 52 | 53 | _pareto_colors: dict = {'bar': '#4682b4', 'line': '#b44646'} 54 | _pareto_colors_schema = { 55 | 'bar': {'required': True, 'type': 'string'}, 56 | 'line': {'required': True, 'type': 'string'}, 57 | } 58 | 59 | @property 60 | def pareto_colors(self): 61 | """Colors for bar and line traces in Pareto chart. 62 | 63 | Returns: 64 | dict: dictionary with keys `(bar, line)` 65 | 66 | """ 67 | return self._pareto_colors 68 | 69 | @pareto_colors.setter 70 | def pareto_colors(self, pareto_colors): 71 | errors = validate(pareto_colors, self._pareto_colors_schema) 72 | if errors: 73 | raise RuntimeError(f'Validation of self.pareto_colors failed: {errors}') 74 | # Assign new pareto_colors 75 | self._pareto_colors = pareto_colors 76 | 77 | def create_traces(self, df_raw): 78 | """Return traces for plotly chart. 79 | 80 | Args: 81 | df_raw: pandas dataframe with at minimum the two columns `category: str` and `value: float` 82 | 83 | Returns: 84 | list: Dash chart traces 85 | 86 | Raises: 87 | RuntimeError: if the `df_raw` is missing any necessary columns 88 | 89 | """ 90 | # Check that the raw data frame is properly formatted 91 | check_raw_data(df_raw, min_keys=['category', 'value']) 92 | if not pd.api.types.is_string_dtype(df_raw['category']): # pragma: no cover 93 | raise RuntimeError(f"category column must be string, but found {df_raw['category'].dtype}") 94 | 95 | # Create and return the traces and optionally add the count to the bar chart 96 | df_p = tidy_pareto_data(df_raw, self.cap_categories) 97 | count_kwargs = {'text': df_p['counts'], 'textposition': 'auto'} if self.show_count else {} 98 | return [ 99 | go.Bar( 100 | hoverinfo='y', yaxis='y1', name='raw_value', 101 | marker={'color': self.pareto_colors['bar']}, 102 | x=df_p['label'], y=df_p['value'], **count_kwargs, 103 | ), 104 | ] + [ 105 | go.Scatter( 106 | hoverinfo='y', yaxis='y2', name='cumulative_percentage', 107 | marker={'color': self.pareto_colors['line']}, mode='lines', 108 | x=df_p['label'], y=df_p['cum_per'], 109 | ), 110 | ] 111 | 112 | def create_layout(self): 113 | """Extend the standard layout. 114 | 115 | Returns: 116 | dict: layout for Dash figure 117 | 118 | """ 119 | layout = super().create_layout() 120 | layout['legend'] = {} 121 | layout['showlegend'] = False 122 | 123 | layout['margin'] = {'l': 75, 'b': 100, 't': 50, 'r': 125} 124 | 125 | # Update YAxis configuration 126 | layout['yaxis']['mirror'] = 'ticks' 127 | layout['yaxis']['showline'] = True 128 | layout['yaxis']['tickformat'] = '.0f' 129 | 130 | # See multiple axis: https://plot.ly/python/multiple-axes/ 131 | layout['yaxis2'] = { 132 | 'dtick': 0.1, 133 | 'overlaying': 'y', 134 | 'range': [0, 1.01], 135 | 'showgrid': False, 136 | 'side': 'right', 137 | 'tickformat': '.0%', 138 | 'tickmode': 'linear', 139 | 'title': self.yaxis_2_label, 140 | } 141 | 142 | return layout 143 | -------------------------------------------------------------------------------- /tests/examples/ex_multi_page.py: -------------------------------------------------------------------------------- 1 | """Example Multi Page Applet.""" 2 | 3 | import dash_bootstrap_components as dbc 4 | import plotly.express as px 5 | from dash import dcc, html 6 | from implements import implements 7 | 8 | from dash_charts.utils_app import AppBase, AppInterface 9 | from dash_charts.utils_app_with_navigation import AppMultiPage 10 | from dash_charts.utils_fig import min_graph 11 | from dash_charts.utils_helpers import parse_dash_cli_args 12 | 13 | 14 | @implements(AppInterface) # noqa: H601 15 | class StaticPage(AppBase): 16 | """Simple App without charts or callbacks.""" 17 | 18 | basic_style = { 19 | 'marginLeft': 'auto', 20 | 'marginRight': 'auto', 21 | 'maxWidth': '1000px', 22 | 'paddingTop': '10px', 23 | } 24 | 25 | def initialization(self) -> None: 26 | """Initialize ids with `self.register_uniq_ids([...])` and other one-time actions.""" 27 | super().initialization() 28 | self.register_uniq_ids(['N/A']) 29 | 30 | def create_elements(self) -> None: 31 | """Initialize the charts, tables, and other Dash elements..""" 32 | ... 33 | 34 | def create_callbacks(self) -> None: 35 | """Register callbacks necessary for this tab.""" 36 | ... 37 | 38 | 39 | class PageText(StaticPage): 40 | """Text page.""" 41 | 42 | name = 'Text Page' 43 | 44 | def return_layout(self) -> dict: 45 | """Return Dash application layout. 46 | 47 | Returns: 48 | dict: Dash HTML object 49 | 50 | """ 51 | return html.Div( 52 | style=self.basic_style, children=( 53 | [html.H1(children=f'{self.name} Scrollable Content')] 54 | + [html.P(children=[str(count) + '-word' * 10]) for count in range(100)] 55 | ), 56 | ) 57 | 58 | 59 | class PageChart(StaticPage): 60 | """Chart page.""" 61 | 62 | name = 'Chart Page' 63 | 64 | def return_layout(self) -> dict: 65 | """Return Dash application layout. 66 | 67 | Returns: 68 | dict: Dash HTML object 69 | 70 | """ 71 | return html.Div( 72 | style=self.basic_style, children=[ 73 | html.H1(children=self.name), 74 | dcc.Loading( 75 | type='circle', 76 | children=[ 77 | min_graph(figure=px.scatter(px.data.iris(), x='sepal_width', y='sepal_length', height=500)), 78 | ], 79 | ), 80 | ], 81 | ) 82 | 83 | 84 | class Page404(StaticPage): 85 | """404 page.""" 86 | 87 | name = 'Page 404' 88 | 89 | def return_layout(self) -> dict: 90 | """Return Dash application layout. 91 | 92 | Returns: 93 | dict: Dash HTML object 94 | 95 | """ 96 | return html.Div( 97 | style=self.basic_style, children=[ 98 | html.H1(children='404: Path not found'), 99 | html.Img(src='https://upload.wikimedia.org/wikipedia/commons/2/26/NL_Route_404.svg'), 100 | ], 101 | ) 102 | 103 | 104 | # ---------------------------------------------------------------------------------------------------------------------- 105 | 106 | 107 | class MultiPageDemo(AppMultiPage): # noqa: H601 108 | """Demo application.""" 109 | 110 | name = 'MultiPageDemo' 111 | 112 | navbar_links = [('Home', '/'), ('Chart', '/is-chart'), ('404', '/404')] 113 | """Base class must create list of tuples `[('Link Name', '/link'), ]` to use default `self.nav_bar()`.""" 114 | 115 | dropdown_links = [('DBC', 'https://dash-bootstrap-components.opensource.faculty.ai/l/components/nav')] 116 | """Base class must create list of tuples `[('Link Name', '/link'), ]` to use default `self.nav_bar()`.""" 117 | 118 | logo = 'https://images.plot.ly/logo/new-branding/plotly-logomark.png' 119 | """Optional path to logo. If None, no logo will be shown in navbar.""" 120 | 121 | external_stylesheets = [dbc.themes.FLATLY] # DARKLY, FLATLY, etc. (https://bootswatch.com/) 122 | """List of external stylesheets. Default is minimal Dash CSS. Only applies if app argument not provided.""" 123 | 124 | def define_nav_elements(self): 125 | """Return list of initialized tabs. 126 | 127 | Returns: 128 | list: each item is an initialized tab (ex `[AppBase(self.app)]` in the order each tab is rendered 129 | 130 | """ 131 | return [ 132 | PageText(app=self.app), 133 | PageChart(app=self.app), 134 | Page404(app=self.app), 135 | ] 136 | 137 | def select_page_name(self, pathname): 138 | """Return the page name determined based on the pathname. 139 | 140 | Args: 141 | pathname: relative pathname from URL 142 | 143 | Returns: 144 | str: page name 145 | 146 | """ 147 | if pathname == '/': 148 | return PageText.name 149 | elif 'chart' in pathname: 150 | return PageChart.name 151 | else: 152 | return Page404.name 153 | 154 | 155 | instance = MultiPageDemo 156 | app = instance() 157 | app.create() 158 | if __name__ == '__main__': 159 | app.run(**parse_dash_cli_args()) 160 | else: 161 | FLASK_HANDLE = app.get_server() 162 | -------------------------------------------------------------------------------- /_try_tabulator.py: -------------------------------------------------------------------------------- 1 | """Experiment with demo of dash-tabulator. 2 | 3 | See: https://github.com/KyleKing/dash_charts/issues/18 4 | 5 | PLANNED: Example with Bootstrap 4, to try next: 6 | https://community.plotly.com/t/tabulator-dash-component/42261/20 7 | 8 | """ 9 | 10 | import dash 11 | import dash_tabulator 12 | from dash import dcc, html 13 | from dash.dependencies import Input, Output 14 | 15 | # 3rd party js to export as xlsx 16 | external_scripts = ['https://oss.sheetjs.com/sheetjs/xlsx.full.min.js'] 17 | 18 | # bootstrap css 19 | external_stylesheets = ['https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css'] 20 | 21 | # initialize your dash app as normal 22 | app = dash.Dash(__name__, external_scripts=external_scripts, external_stylesheets=external_stylesheets) 23 | 24 | styles = { 25 | 'pre': { 26 | 'border': 'thin lightgrey solid', 27 | 'overflowX': 'scroll', 28 | }, 29 | } 30 | 31 | # Setup some columns 32 | # This is the same as if you were using tabulator directly in js 33 | # Notice the column with "editor": "input" - these cells can be edited 34 | # See tabulator editor for options http://tabulator.info/docs/4.8/edit 35 | columns = [ 36 | {'title': 'Name', 'field': 'name', 'width': 150, 'headerFilter': True, 'editor': 'input'}, 37 | {'title': 'Age', 'field': 'age', 'hozAlign': 'left', 'formatter': 'progress'}, 38 | {'title': 'Favourite Color', 'field': 'col', 'headerFilter': True}, 39 | {'title': 'Date Of Birth', 'field': 'dob', 'hozAlign': 'center'}, 40 | {'title': 'Rating', 'field': 'rating', 'hozAlign': 'center', 'formatter': 'star'}, 41 | {'title': 'Passed?', 'field': 'passed', 'hozAlign': 'center', 'formatter': 'tickCross'}, 42 | ] 43 | 44 | # Setup some data 45 | data = [ 46 | {'id': 1, 'name': 'Oli Bob', 'age': '12', 'col': 'red', 'dob': ''}, 47 | {'id': 2, 'name': 'Mary May', 'age': '1', 'col': 'blue', 'dob': '14/05/1982'}, 48 | {'id': 3, 'name': 'Christine Lobowski', 'age': '42', 'col': 'green', 'dob': '22/05/1982'}, 49 | {'id': 4, 'name': 'Brendon Philips', 'age': '125', 'col': 'orange', 'dob': '01/08/1980'}, 50 | {'id': 5, 'name': 'Margret Marmajuke', 'age': '16', 'col': 'yellow', 'dob': '31/01/1999'}, 51 | {'id': 6, 'name': 'Fred Savage', 'age': '16', 'col': 'yellow', 'rating': '1', 'dob': '31/01/1999'}, 52 | {'id': 6, 'name': 'Brie Larson', 'age': '30', 'col': 'blue', 'rating': '1', 'dob': '31/01/1999'}, 53 | ] 54 | 55 | # Additional options can be setup here 56 | # these are passed directly to tabulator 57 | # In this example we are enabling selection 58 | # Allowing you to select only 1 row 59 | # and grouping by the col (color) column 60 | options = {'groupBy': 'col', 'selectable': 1} 61 | 62 | # downloadButtonType 63 | # takes 64 | # css => class names 65 | # text => Text on the button 66 | # type => type of download (csv/ xlsx / pdf, remember to include appropriate 3rd party js libraries) 67 | # filename => filename prefix defaults to data, will download as filename.type 68 | downloadButtonType = {'css': 'btn btn-primary', 'text': 'Export', 'type': 'xlsx'} 69 | 70 | 71 | # clearFilterButtonType 72 | # takes 73 | # css => class names 74 | # text => Text on the button 75 | clearFilterButtonType = {'css': 'btn btn-outline-dark', 'text': 'Clear Filters'} 76 | 77 | # Add a dash_tabulator table 78 | # columns=columns, 79 | # data=data, 80 | # Can be setup at initialization or added with a callback as shown below 81 | # thank you @AnnMarieW for that fix 82 | app.layout = html.Div([ 83 | dash_tabulator.DashTabulator( 84 | id='tabulator', 85 | # columns=columns, 86 | # data=data, 87 | options=options, 88 | downloadButtonType=downloadButtonType, 89 | clearFilterButtonType=clearFilterButtonType, 90 | ), 91 | html.Div(id='output'), 92 | dcc.Interval( 93 | id='interval-component-iu', 94 | interval=1 * 10, # in milliseconds 95 | n_intervals=0, 96 | max_intervals=0, 97 | ), 98 | 99 | ]) 100 | 101 | 102 | # dash_tabulator can be populated from a dash callback 103 | @app.callback( 104 | [ 105 | Output('tabulator', 'columns'), 106 | Output('tabulator', 'data'), 107 | ], 108 | [Input('interval-component-iu', 'n_intervals')], 109 | ) 110 | def initialize(val): 111 | return columns, data 112 | 113 | 114 | # dash_tabulator can register a callback on rowClicked, 115 | # cellEdited => a cell with a header that has "editor":"input" etc.. will be returned with row, initial value, old value, new value 116 | # dataChanged => full table upon change (use with caution) 117 | # dataFiltering => header filters as typed, before filtering has occurred (you get partial matching) 118 | # dataFiltered => header filters and rows of data returned 119 | # to receive a dict of the row values 120 | @app.callback( 121 | Output('output', 'children'), 122 | [ 123 | Input('tabulator', 'rowClicked'), 124 | Input('tabulator', 'cellEdited'), 125 | Input('tabulator', 'dataChanged'), 126 | Input('tabulator', 'dataFiltering'), 127 | Input('tabulator', 'dataFiltered'), 128 | ], 129 | ) 130 | def display_output(row, cell, dataChanged, filters, dataFiltered): 131 | print(row) 132 | print(cell) 133 | print(dataChanged) 134 | print(filters) 135 | print(dataFiltered) 136 | return f'You have clicked row {row} ; cell {cell}' 137 | 138 | 139 | if __name__ == '__main__': 140 | app.run_server(debug=True) 141 | -------------------------------------------------------------------------------- /tests/examples/ex_time_vis_chart.py: -------------------------------------------------------------------------------- 1 | """Example Time Vis Chart.""" 2 | 3 | import pandas as pd 4 | from dash import html 5 | from implements import implements 6 | 7 | from dash_charts.time_vis_chart import TimeVisChart 8 | from dash_charts.utils_app import AppBase, AppInterface 9 | from dash_charts.utils_fig import min_graph 10 | from dash_charts.utils_helpers import parse_dash_cli_args 11 | 12 | 13 | @implements(AppInterface) # noqa: H601 14 | class TimeVisDemo(AppBase): 15 | """Example creating a TimeVis chart.""" 16 | 17 | name = 'Example TimeVis Chart' 18 | """Application name""" 19 | 20 | chart_main = None 21 | """Main chart (TimeVis).""" 22 | 23 | id_chart = 'TimeVis' 24 | """Unique name for the main chart.""" 25 | 26 | def initialization(self) -> None: 27 | """Initialize ids with `self.register_uniq_ids([...])` and other one-time actions.""" 28 | super().initialization() 29 | self.register_uniq_ids([self.id_chart]) 30 | 31 | def create_elements(self) -> None: 32 | """Initialize the charts, tables, and other Dash elements.""" 33 | self.chart_main = TimeVisChart( 34 | title='Pool Schedule (Sample TimeVis Chart)', 35 | xlabel=None, 36 | ylabel=None, 37 | ) 38 | self.chart_main.fillcolor = '#A9DDDF' 39 | 40 | def generate_data(self) -> None: 41 | """Create self.data_raw with sample data.""" 42 | data = [ 43 | { 44 | 'category': '', 'label': 'Closed', 45 | 'start': '2020-07-01 19:30:00', 'end': '2020-07-02 07:00:00', 46 | }, 47 | { 48 | 'category': '', 'label': 'Closed', 49 | 'start': '2020-07-02 19:30:00', 'end': '2020-07-03 07:00:00', 50 | }, 51 | { 52 | 'category': '', 'label': 'Closed', 53 | 'start': '2020-07-03 19:30:00', 'end': '2020-07-04 08:00:00', 54 | }, 55 | { 56 | 'category': '', 'label': 'Closed', 57 | 'start': '2020-07-04 20:00:00', 'end': '2020-07-05 08:00:00', 58 | }, 59 | ] 60 | for day in [1, 2, 3]: 61 | data.extend([ 62 | { 63 | 'category': 'Events', 'label': 'Pool Opens to Public', 64 | 'start': f'2020-07-0{day} 07:00:00', 'end': None, 65 | }, 66 | { 67 | 'category': 'Events', 'label': 'Closes', 68 | 'start': f'2020-07-0{day} 19:30:00', 'end': None, 69 | }, 70 | { 71 | 'category': 'Restricted Swim Hours', 'label': 'Adult Swim', 72 | 'start': f'2020-07-0{day} 16:00:00', 'end': f'2020-07-0{day} 19:30:00', 73 | }, 74 | { 75 | 'category': 'Open Swim', 'label': 'Open', 76 | 'start': f'2020-07-0{day} 09:00:00', 'end': f'2020-07-0{day} 15:50:00', 77 | }, 78 | { 79 | 'category': 'Restricted Swim Hours', 'label': 'Lap', 80 | 'start': f'2020-07-0{day} 07:00:00', 'end': f'2020-07-0{day} 08:30:00', 81 | }, 82 | { 83 | 'category': 'Swim Team', 'label': 'P-A', 84 | 'start': f'2020-07-0{day} 08:00:00', 'end': f'2020-07-0{day} 09:00:00', 85 | }, 86 | { 87 | 'category': 'Swim Team', 'label': 'P-B', 88 | 'start': f'2020-07-0{day} 14:00:00', 'end': f'2020-07-0{day} 15:00:00', 89 | }, 90 | ]) 91 | for weekend in [4, 5]: 92 | data.extend([ 93 | { 94 | 'category': 'Events', 'label': 'Pool Opens to Public', 95 | 'start': f'2020-07-0{weekend} 08:00:00', 'end': None, 96 | }, 97 | { 98 | 'category': 'Events', 'label': 'Closes', 99 | 'start': f'2020-07-0{weekend} 20:00:00', 'end': None, 100 | }, 101 | { 102 | 'category': 'Open Swim', 'label': 'Weekend Open Swim', 103 | 'start': f'2020-07-0{weekend} 08:00:00', 'end': f'2020-07-0{weekend} 20:00:00', 104 | }, 105 | ]) 106 | self.data_raw = pd.DataFrame.from_dict(data) 107 | 108 | def return_layout(self) -> dict: 109 | """Return Dash application layout. 110 | 111 | Returns: 112 | dict: Dash HTML object 113 | 114 | """ 115 | return html.Div( 116 | style={ 117 | 'maxWidth': '1000px', 118 | 'marginRight': 'auto', 119 | 'marginLeft': 'auto', 120 | }, children=[ 121 | html.H4(children=self.name), 122 | html.Div([ 123 | min_graph( 124 | id=self._il[self.id_chart], 125 | figure=self.chart_main.create_figure(df_raw=self.data_raw), 126 | ), 127 | ]), 128 | ], 129 | ) 130 | 131 | def create_callbacks(self) -> None: 132 | """Create Dash callbacks.""" 133 | ... # No callbacks necessary for this simple example 134 | 135 | 136 | instance = TimeVisDemo 137 | app = instance() 138 | app.create() 139 | if __name__ == '__main__': 140 | app.run(**parse_dash_cli_args()) 141 | else: 142 | FLASK_HANDLE = app.get_server() 143 | -------------------------------------------------------------------------------- /tests/examples/ex_utils_static.py: -------------------------------------------------------------------------------- 1 | """Example Static HTML file output.""" 2 | 3 | import json 4 | import webbrowser 5 | from pathlib import Path 6 | 7 | import dash_bootstrap_components as dbc 8 | import jsonpickle 9 | import pandas as pd 10 | import plotly.express as px 11 | from dominate import tags, util 12 | 13 | from dash_charts import equations 14 | from dash_charts.scatter_line_charts import FittedChart 15 | from dash_charts.utils_data import json_dumps_compact 16 | from dash_charts.utils_static import ( 17 | add_image, add_video, create_dbc_doc, make_div, tag_code, 18 | tag_markdown, tag_table, write_from_markdown, write_image_file, 19 | ) 20 | 21 | 22 | def toggle_written_image_file(image_path, figure): 23 | """Test writing an image file.""" # noqa: DAR101 24 | if image_path.is_file(): 25 | image_path.unlink() 26 | write_image_file(figure, image_path, image_path.suffix[1:]) 27 | assert image_path.is_file() # noqa: S101 28 | 29 | 30 | def create_sample_custom_chart_figure(): 31 | """Return figure dictionary using CustomChart classes. 32 | 33 | Returns: 34 | dict: chart figure 35 | 36 | """ 37 | chart_main = FittedChart( 38 | title='Sample Fitted Scatter Data', 39 | xlabel='Index', 40 | ylabel='Measured Value', 41 | ) 42 | chart_main.fit_eqs = [('linear', equations.linear)] 43 | # Create dataframe based on px sample dataset 44 | iris = px.data.iris() 45 | data_raw = pd.DataFrame( 46 | data={ 47 | 'name': iris['species'], 48 | 'x': iris['petal_width'], 49 | 'y': iris['petal_length'], 50 | 'label': None, 51 | }, 52 | ) 53 | return chart_main.create_figure(df_raw=data_raw) 54 | 55 | 56 | def write_sample_html(filename): 57 | """Write static HTML. 58 | 59 | Args: 60 | filename: path to write the HTML file 61 | 62 | """ 63 | image_path = Path(__file__).parent / 'test_write_image_file.png' 64 | video_path = image_path.parent / 'test_video_file.mp4' 65 | figure = px.scatter(x=range(10), y=range(10)) 66 | toggle_written_image_file(image_path, figure) 67 | 68 | # Configure dark theme 69 | custom_styles = 'pre {max-height: 400px;}' 70 | doc = create_dbc_doc(dbc.themes.DARKLY, custom_styles, title='Example Static File') 71 | with doc.head: 72 | tags.link( 73 | rel='stylesheet', href=( 74 | 'https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.13.1/build/styles/' 75 | 'tomorrow-night-eighties.min.css' 76 | ), 77 | ) 78 | 79 | figure = create_sample_custom_chart_figure() 80 | px_figure = px.scatter(x=[*range(10)][::-1], y=range(10), template='plotly_dark') 81 | px_figure_json = jsonpickle.encode(px_figure['layout'], unpicklable=False) 82 | with doc: 83 | with tags.div(_class='container').add(tags.div(_class='col')): 84 | tags.h1('Example Creating Static HTML') 85 | tags.p('Charts still have hover and zoom features, but there is no way to use callbacks in static HTML') 86 | util.raw(make_div(figure)) 87 | tags.hr() 88 | 89 | tags.h1('Example Table') 90 | tag_table(px.data.iris().head(5)) 91 | 92 | tags.hr() 93 | tags.h1('Example Code') 94 | tag_code((Path(__file__).parent / 'readme.py').read_text(), language='language-py') 95 | 96 | tags.hr() 97 | tags.h1('Markdown Examples') 98 | md_string = '# Hello Markdown!\n\n[HLJS Demo](https://highlightjs.org/static/demo/)' 99 | tags.p('Shown as raw code below') 100 | tag_code(md_string, language='language-md') 101 | tags.p('Shown as formatted HTML') 102 | tag_markdown(md_string) 103 | 104 | tags.hr() 105 | tags.h1('Example image') 106 | util.raw(add_image(image_path)) 107 | util.raw(add_video(video_path)) 108 | tags.mark( 109 | 'Stock footage provided by Videvo, downloaded from [Videvo](https://www.videvo.net/video/' 110 | 'there-is-no-planet-b-protest-sign/456789/)', 111 | ) 112 | 113 | tags.hr() 114 | tags.h1('Another Chart For Good Measure') 115 | util.raw(make_div(px_figure)) 116 | tags.br() 117 | tags.p('JSON representation of the px_figure layout') 118 | tag_code(json_dumps_compact(json.loads(px_figure_json)), language='language-json') 119 | 120 | filename.write_text(str(doc)) 121 | 122 | 123 | def example_write_from_markdown(): 124 | """Demonstrate the write_from_markdown function. 125 | 126 | Returns: 127 | Path: path to created HTMl file 128 | 129 | """ 130 | filename = Path(__file__).parent / 'example_write_from_markdown.md' 131 | figure_px = px.scatter(x=range(10), y=range(10)) 132 | function_lookup = { # noqa: ECE001 133 | 'make_div(figure_px)': (make_div, [figure_px]), 134 | 'table(iris_data)': (tag_table, [px.data.iris().head(10)]), 135 | } 136 | return write_from_markdown(filename, function_lookup) 137 | 138 | 139 | if __name__ == '__main__': 140 | # Create all HTML content in Python 141 | filename = Path(__file__).parent / 'tmp.html' 142 | write_sample_html(filename) 143 | webbrowser.open(filename.resolve().as_uri()) 144 | 145 | # Alternatively, read from a Markdown file (note: both methods can be combined) 146 | filename_from_md = example_write_from_markdown() 147 | webbrowser.open(filename_from_md.resolve().as_uri()) 148 | -------------------------------------------------------------------------------- /tests/examples/ex_marginal_chart.py: -------------------------------------------------------------------------------- 1 | """Example Marginal-Chart.""" 2 | 3 | import numpy as np 4 | import pandas as pd 5 | import plotly.express as px 6 | import plotly.graph_objects as go 7 | from dash import html 8 | from implements import implements 9 | 10 | from dash_charts.utils_app import AppBase, AppInterface 11 | from dash_charts.utils_fig import MarginalChart, check_raw_data, min_graph 12 | from dash_charts.utils_helpers import parse_dash_cli_args 13 | 14 | 15 | class SampleMarginalChart(MarginalChart): 16 | """Sample implementing a custom MarginalChart.""" 17 | 18 | def create_traces(self, df_raw): 19 | """Return traces for plotly chart. 20 | 21 | Args: 22 | df_raw: pandas dataframe with columns `name: str`, `x: float`, `y: float` and `label: str` 23 | 24 | Returns: 25 | list: Dash chart traces 26 | 27 | """ 28 | check_raw_data(df_raw, min_keys=['name', 'x', 'y', 'label']) 29 | 30 | return [ 31 | go.Scatter( 32 | mode='markers', 33 | # name=df_raw['name'], 34 | text=df_raw['label'], 35 | x=df_raw['x'], 36 | y=df_raw['y'], 37 | ), 38 | ] 39 | 40 | def create_marg_top(self, df_raw): 41 | """Return traces for the top marginal chart. 42 | 43 | Args: 44 | df_raw: same pandas dataframe as self.create_traces() 45 | 46 | Returns: 47 | list: trace data points. List may be empty 48 | 49 | """ 50 | return [ 51 | go.Bar( 52 | marker_color='royalblue', 53 | # name='TODO, 54 | showlegend=False, 55 | x=df_raw['x'], 56 | y=df_raw['y'], 57 | ), 58 | ] 59 | 60 | def create_marg_right(self, df_raw): 61 | """Return traces for the top marginal chart. 62 | 63 | Args: 64 | df_raw: same pandas dataframe as self.create_traces() 65 | 66 | Returns: 67 | list: trace data points. List may be empty 68 | 69 | """ 70 | key = 'name' 71 | return [ 72 | go.Violin( 73 | marker_color='royalblue', 74 | name=str(name), 75 | showlegend=False, 76 | x=df_raw[key][df_raw[key] == name], 77 | y=df_raw['y'][df_raw[key] == name], 78 | ) 79 | for name in np.sort(df_raw[key].unique()) 80 | ] 81 | 82 | def create_layout(self): 83 | """Extend the standard layout. 84 | 85 | Returns: 86 | dict: layout for Dash figure 87 | 88 | """ 89 | layout = super().create_layout() 90 | layout['legend'] = {} # Reset legend to default position on top right 91 | layout['showlegend'] = False 92 | return layout 93 | 94 | 95 | @implements(AppInterface) 96 | class SampleMarginalChartDemo(AppBase): 97 | """Example creating a Marginal chart.""" 98 | 99 | name = 'Example Marginal Chart' 100 | """Application name""" 101 | 102 | data_raw = None 103 | """All in-memory data referenced by callbacks and plotted. If modified, will impact all viewers.""" 104 | 105 | chart_main = None 106 | """Main chart (Marginal).""" 107 | 108 | id_chart = 'marginal' 109 | """Unique name for the main chart.""" 110 | 111 | def initialization(self) -> None: 112 | """Initialize ids with `self.register_uniq_ids([...])` and other one-time actions.""" 113 | super().initialization() 114 | self.register_uniq_ids([self.id_chart]) 115 | 116 | def create_elements(self) -> None: 117 | """Initialize the charts, tables, and other Dash elements.""" 118 | self.chart_main = SampleMarginalChart( 119 | title='Sample User-Implemented Marginal Chart with Iris dataset', 120 | xlabel='Petal Width', 121 | ylabel='Petal Length', 122 | ) 123 | 124 | def generate_data(self) -> None: 125 | """Create self.data_raw with sample data.""" 126 | # Create dataframe based on px sample dataset 127 | iris = px.data.iris() 128 | self.data_raw = pd.DataFrame( 129 | data={ 130 | 'name': iris['species'], 131 | 'x': iris['petal_width'], 132 | 'y': iris['petal_length'], 133 | 'label': None, 134 | }, 135 | ) 136 | 137 | def return_layout(self) -> dict: 138 | """Return Dash application layout. 139 | 140 | Returns: 141 | dict: Dash HTML object 142 | 143 | """ 144 | return html.Div( 145 | style={ 146 | 'maxWidth': '1000px', 147 | 'marginRight': 'auto', 148 | 'marginLeft': 'auto', 149 | }, children=[ 150 | html.H4(children=self.name), 151 | html.Div([ 152 | min_graph( 153 | id=self._il[self.id_chart], 154 | figure=self.chart_main.create_figure( 155 | df_raw=self.data_raw, 156 | ), 157 | ), 158 | ]), 159 | ], 160 | ) 161 | 162 | def create_callbacks(self) -> None: 163 | """Create Dash callbacks.""" 164 | ... # No callbacks necessary for this simple example 165 | 166 | 167 | instance = SampleMarginalChartDemo 168 | app = instance() 169 | app.create() 170 | if __name__ == '__main__': 171 | app.run(**parse_dash_cli_args()) 172 | else: 173 | FLASK_HANDLE = app.get_server() 174 | -------------------------------------------------------------------------------- /tests/examples/ex_style_bulma.py: -------------------------------------------------------------------------------- 1 | """Example Bulma layout. 2 | 3 | See documentation on Bulma layouts: https://bulma.io/documentation/layout/tiles/ 4 | 5 | """ 6 | 7 | import plotly.express as px 8 | from dash import html 9 | from implements import implements 10 | 11 | from dash_charts.utils_app import STATIC_URLS, AppBase, AppInterface 12 | from dash_charts.utils_fig import min_graph 13 | from dash_charts.utils_helpers import parse_dash_cli_args 14 | 15 | 16 | @implements(AppInterface) 17 | class BulmaStylingDemo(AppBase): 18 | """Demo laying out a 3 column grid with Bulma where. 19 | 20 | - the first column has three tiles 21 | - the middle column is half the full screen width 22 | - the tiles will wrap on smaller screens 23 | 24 | """ 25 | 26 | name = 'Example Bulma Styling Demo' 27 | """Application name""" 28 | 29 | external_stylesheets = [STATIC_URLS['bulmaswatch-flatly']] 30 | """List of external stylesheets. Default is minimal Dash CSS. Only applies if app argument not provided.""" 31 | 32 | def initialization(self) -> None: 33 | """Initialize ids with `self.register_uniq_ids([...])` and other one-time actions.""" 34 | super().initialization() 35 | self.register_uniq_ids(['---']) 36 | 37 | def create_elements(self) -> None: 38 | """Initialize the charts, tables, and other Dash elements.""" 39 | ... 40 | 41 | def return_layout(self) -> dict: 42 | """Return Dash application layout. 43 | 44 | Returns: 45 | dict: Dash HTML object 46 | 47 | """ 48 | return html.Div( 49 | className='section', children=[ 50 | html.Div( 51 | className='tile is-ancestor', children=[ 52 | html.Div( 53 | className='tile is-parent is-vertical is-3', children=[ 54 | html.Article( 55 | className='tile is-child notification', children=[ 56 | html.P(className='title', children='Top Vertical Tile'), 57 | html.P(className='subtitle', children='Notification class for grey background'), 58 | html.P( 59 | className='subtitle', 60 | children='Could also add is-info, is-warning, etc.', 61 | ), 62 | ], 63 | ), 64 | html.Article( 65 | className='tile is-child', children=[ 66 | html.P(className='title', children='Vertical...'), 67 | html.P(className='subtitle', children='(Top tile)'), 68 | min_graph( 69 | figure=px.scatter( 70 | px.data.iris(), x='sepal_width', y='sepal_length', height=200, 71 | ), 72 | ), 73 | ], 74 | ), 75 | html.Article( 76 | className='tile is-child', children=[ 77 | html.P(className='title', children='...tiles'), 78 | html.P(className='subtitle', children='(Bottom tile)'), 79 | min_graph( 80 | figure=px.scatter( 81 | px.data.iris(), x='sepal_width', y='sepal_length', height=200, 82 | ), 83 | ), 84 | ], 85 | ), 86 | ], 87 | ), 88 | min_graph( 89 | className='tile is-child is-6 is-block-desktop', 90 | figure={}, 91 | ), 92 | html.Article( 93 | className='tile is-child is-3 is-block-desktop', children=[ 94 | html.P(className='title', children='A Small Chart'), 95 | min_graph( 96 | figure=px.scatter( 97 | px.data.iris(), 98 | x='sepal_width', 99 | y='sepal_length', 100 | height=350, 101 | ), 102 | ), 103 | html.P(className='subtitle', children='An Image'), 104 | html.Img(src='https://media.giphy.com/media/JGQe5mxayVF04/giphy.gif'), 105 | ], 106 | ), 107 | ], 108 | ), 109 | ], 110 | ) 111 | 112 | def create_callbacks(self) -> None: 113 | """Create Dash callbacks.""" 114 | ... # No callbacks necessary for this simple example 115 | 116 | 117 | instance = BulmaStylingDemo 118 | app = instance() 119 | app.create() 120 | if __name__ == '__main__': 121 | app.run(**parse_dash_cli_args()) 122 | else: 123 | FLASK_HANDLE = app.get_server() 124 | -------------------------------------------------------------------------------- /docs/docs/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | dev.act.kyle@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /dash_charts/scatter_line_charts.py: -------------------------------------------------------------------------------- 1 | """Charts for plotting scatter or fitted data.""" 2 | 3 | import numpy as np 4 | import plotly.graph_objects as go 5 | from scipy import optimize 6 | 7 | from .utils_fig import CustomChart, check_raw_data 8 | 9 | 10 | def create_rolling_traces(df_raw, count_rolling, count_std): 11 | """Calculate traces for rolling average and standard deviation. 12 | 13 | Args: 14 | df_raw: pandas dataframe with columns `x: float`, `y: float` and `label: str` 15 | count_rolling: number of points to use for the rolling calculation 16 | count_std: number of standard deviations to use for the standard deviation 17 | 18 | Returns: 19 | list: of Scatter traces for rolling mean and std 20 | 21 | """ 22 | rolling_mean = df_raw['y'].rolling(count_rolling).mean().tolist() 23 | rolling_std = df_raw['y'].rolling(count_std).std().tolist() 24 | return [ 25 | go.Scatter( 26 | fill='toself', 27 | hoverinfo='skip', 28 | name=f'{count_std}x STD Range', 29 | opacity=0.5, 30 | x=(df_raw['x'].tolist() + df_raw['x'].tolist()[::-1]), 31 | y=( 32 | np.add(rolling_mean, np.multiply(count_std, rolling_std)).tolist() 33 | + np.subtract(rolling_mean, np.multiply(count_std, rolling_std)).tolist()[::-1] 34 | ), 35 | ), 36 | go.Scatter( 37 | hoverinfo='skip', 38 | mode='lines', 39 | name='Rolling Mean', 40 | opacity=0.9, 41 | x=df_raw['x'], 42 | y=rolling_mean, 43 | ), 44 | ] 45 | 46 | 47 | def create_fit_traces(df_raw, name, fit_equation, suppress_fit_errors=False): # noqa: CCR001 48 | """Create traces for specified equation. 49 | 50 | Args: 51 | df_raw: pandas dataframe with columns `name: str`, `x: float`, `y: float` and `label: str` 52 | name: unique name for trace 53 | fit_equation: equation used 54 | suppress_fit_errors: If True, bury errors from scipy fit. Default is False. 55 | 56 | Returns: 57 | list: of Scatter traces for fitted equation 58 | 59 | """ 60 | fitted_data = [] 61 | try: 62 | popt, pcov = optimize.curve_fit(fit_equation, xdata=df_raw['x'], ydata=df_raw['y'], method='lm') 63 | # Calculate representative x values for plotting fit 64 | x_min = np.min(df_raw['x']) 65 | x_max = np.max(df_raw['x']) 66 | x_range = x_max - x_min 67 | x_values = sorted([ 68 | x_min - 0.05 * x_range, 69 | *np.divide(range(int(x_min * 10), int(x_max * 10)), 10), 70 | x_max + 0.05 * x_range, 71 | ]) 72 | fitted_data = [ 73 | go.Scatter( 74 | mode='lines+markers', 75 | name=name, 76 | opacity=0.9, 77 | text=f'popt:{[round(param, 3) for param in popt]}', 78 | x=x_values, 79 | y=fit_equation(x_values, *popt), 80 | ), 81 | ] 82 | except (RuntimeError, ValueError) as err: # pragma: no cover 83 | if not suppress_fit_errors: 84 | raise 85 | 86 | return fitted_data # noqa: R504 87 | 88 | 89 | class RollingChart(CustomChart): 90 | """Rolling Mean and Filled Standard Deviation Chart for monitoring trends.""" 91 | 92 | count_std = 5 93 | """Count of STD deviations to display. Default 5.""" 94 | 95 | count_rolling = count_std 96 | """Count of items to use for rolling calculations. Default `count_std`.""" 97 | 98 | label_data = 'Data' 99 | """Label for the scatter data. Default is 'Data'.""" 100 | 101 | def create_traces(self, df_raw): 102 | """Return traces for plotly chart. 103 | 104 | Args: 105 | df_raw: pandas dataframe with columns `x: float`, `y: float` and `label: str` 106 | 107 | Returns: 108 | list: Dash chart traces 109 | 110 | """ 111 | # Verify data format 112 | check_raw_data(df_raw, ['x', 'y', 'label']) 113 | 114 | # Create and return the traces 115 | chart_data = [ 116 | go.Scatter( 117 | mode='markers', 118 | name=self.label_data, 119 | opacity=0.5, 120 | text=df_raw['label'], 121 | x=df_raw['x'], 122 | y=df_raw['y'], 123 | ), 124 | ] 125 | # Only add the rolling calculations if there are a sufficient number of points 126 | if len(df_raw['x']) >= self.count_rolling: 127 | chart_data.extend( 128 | create_rolling_traces(df_raw, self.count_rolling, self.count_std), 129 | ) 130 | 131 | return chart_data 132 | 133 | 134 | class FittedChart(CustomChart): 135 | """Scatter Chart with optional Fitted Lines.""" 136 | 137 | label_data = 'Data' 138 | """Label for the scatter data. Default is 'Data'.""" 139 | 140 | fit_eqs = [] 141 | """List of fit equations.""" 142 | 143 | fallback_mode = 'lines+markers' 144 | """If not fit_eqs are specified, will fallback to `lines+markers`. Can be set to `markers`.""" 145 | 146 | min_scatter_for_fit = 0 147 | """List of fit equations.""" 148 | 149 | suppress_fit_errors = False 150 | """If True, bury errors from scipy fit and will print message to console. Default is True.""" 151 | 152 | def create_traces(self, df_raw): # noqa: CCR001 153 | """Return traces for plotly chart. 154 | 155 | Args: 156 | df_raw: pandas dataframe with columns `name: str`, `x: float`, `y: float` and `label: str` 157 | 158 | Returns: 159 | list: Dash chart traces 160 | 161 | """ 162 | # Verify data format 163 | check_raw_data(df_raw, ['name', 'x', 'y', 'label']) 164 | 165 | # Separate raw tidy dataframe into separate scatter plots 166 | scatter_data = [] 167 | fit_traces = [] 168 | for name in set(df_raw['name']): 169 | df_name = df_raw[df_raw['name'] == name] 170 | scatter_data.append( 171 | go.Scatter( 172 | customdata=[name], 173 | mode='markers' if self.fit_eqs else self.fallback_mode, 174 | name=name, 175 | opacity=0.5, 176 | text=df_name['label'], 177 | x=df_name['x'], 178 | y=df_name['y'], 179 | ), 180 | ) 181 | 182 | if len(df_name['x']) > self.min_scatter_for_fit: 183 | for fit_name, fit_equation in self.fit_eqs: 184 | fit_traces.extend( 185 | create_fit_traces(df_name, f'{name}-{fit_name}', fit_equation, self.suppress_fit_errors), 186 | ) 187 | 188 | return scatter_data + fit_traces 189 | -------------------------------------------------------------------------------- /dash_charts/utils_json_cache.py: -------------------------------------------------------------------------------- 1 | """Helpers for managing a generic JSON data file cache. Can be used to reduce API calls, etc. 2 | 3 | Full dataset documentation: https://dataset.readthedocs.io/en/latest/api.html 4 | 5 | """ 6 | 7 | import json 8 | import time 9 | from pathlib import Path 10 | 11 | from .utils_data import uniq_table_id, write_pretty_json 12 | from .utils_dataset import DBConnect 13 | 14 | # FIXME: Add versioning to the cache directory with semver logic: https://pypi.org/project/semantic-version/ 15 | 16 | CACHE_DIR = Path(__file__).parent / 'local_cache' 17 | """Path to folder with all downloaded responses from Kitsu API.""" 18 | 19 | FILE_DATA = DBConnect(CACHE_DIR / '_file_lookup_database.db') 20 | """Global instance of the DBConnect() for the file lookup database.""" 21 | 22 | CACHE_TABLE_NAME = 'files' 23 | """Table name containing the cache file information.""" 24 | 25 | ID_KEY = 'identifier' 26 | """Name of the SQLite column containing the unique identifier.""" 27 | TS_KEY = 'timestamp' 28 | """Name of the SQLite column containing the timestamp.""" 29 | FILENAME_KEY = 'filename' 30 | """Name of the SQLite column containing the string filename.""" 31 | 32 | # TODO: Enable versioning of data and automatic deletion when the version changes 33 | DATA_VERSION_KEY = 'data_version' 34 | """Key to indicate the data version.""" 35 | 36 | 37 | def get_files_table(db_instance): 38 | """Retrieve stored object from cache database. 39 | 40 | Args: 41 | db_instance: Connected Database file with `DBConnect()`. 42 | 43 | Returns: 44 | table: Dataset table for the files lookup 45 | 46 | """ 47 | return db_instance.db.load_table(CACHE_TABLE_NAME) 48 | 49 | 50 | def initialize_cache(db_instance): 51 | """Ensure that the directory and database exist. Remove files from database if manually removed. 52 | 53 | Args: 54 | db_instance: Connected Database file with `DBConnect()`. 55 | 56 | """ 57 | table = db_instance.db.create_table(CACHE_TABLE_NAME) 58 | 59 | removed_files = [] 60 | for row in table: 61 | if not Path(row[FILENAME_KEY]).is_file(): 62 | removed_files.append(row[FILENAME_KEY]) 63 | 64 | for filename in removed_files: 65 | table.delete(filename=filename) 66 | 67 | 68 | def get_cache_dict(db_instance): 69 | """Return a dictionary `{identifier: path}` keys and values. 70 | 71 | Args: 72 | db_instance: Connected Database file with `DBConnect()`. 73 | 74 | Returns: 75 | dict: dictionary `{identifier: path}` keys and values 76 | 77 | """ 78 | table = get_files_table(db_instance) 79 | return {row[ID_KEY]: Path(row[FILENAME_KEY]) for row in table} 80 | 81 | 82 | def match_identifier_in_cache(identifier, db_instance): 83 | """Return list of matches for the given identifier in the file database. 84 | 85 | Args: 86 | identifier: identifier to use as a reference if the corresponding data is already cached 87 | db_instance: Connected Database file with `DBConnect()`. 88 | 89 | Returns: 90 | list: list of match object with keys of the SQL table 91 | 92 | """ 93 | kwargs = {ID_KEY: identifier} 94 | return [*get_files_table(db_instance).find(**kwargs)] 95 | 96 | 97 | def store_cache_as_file(prefix, identifier, db_instance, cache_dir=CACHE_DIR, suffix='.json'): 98 | """Store the reference in the cache database and return the file so the user can handle saving the file. 99 | 100 | Args: 101 | prefix: string used to create more recognizable filenames 102 | identifier: identifier to use as a reference if the corresponding data is already cached 103 | db_instance: Connected Database file with `DBConnect()`. 104 | cache_dir: path to the directory to store the file. Default is `CACHE_DIR 105 | suffix: string filename suffix. The default is `.json` 106 | 107 | Returns: 108 | Path: to the cached file. Caller needs to write to the file 109 | 110 | Raises: 111 | RuntimeError: if duplicate match found when storing 112 | 113 | """ 114 | # Check that the identifier isn't already in the database 115 | matches = match_identifier_in_cache(identifier, db_instance) 116 | if matches: 117 | raise RuntimeError(f'Already have an entry for this identifier (`{identifier}`): {matches}') 118 | # Update the database and store the file 119 | filename = cache_dir / f'{prefix}_{uniq_table_id()}{suffix}' 120 | new_row = {FILENAME_KEY: str(filename), ID_KEY: identifier, TS_KEY: time.time()} 121 | get_files_table(db_instance).insert(new_row) 122 | return filename 123 | 124 | 125 | def store_cache_object(prefix, identifier, obj, db_instance, cache_dir=CACHE_DIR): 126 | """Store the object as a JSON file and track in a SQLite database to prevent duplicates. 127 | 128 | Args: 129 | prefix: string used to create more recognizable filenames 130 | identifier: identifier to use as a reference if the corresponding data is already cached 131 | obj: JSON object to write 132 | db_instance: Connected Database file with `DBConnect()`. 133 | cache_dir: path to the directory to store the file. Default is `CACHE_DIR 134 | 135 | Raises: 136 | Exception: if duplicate match found when storing 137 | 138 | """ 139 | filename = store_cache_as_file(prefix, identifier, db_instance, cache_dir) 140 | try: 141 | write_pretty_json(filename, obj) 142 | except Exception: 143 | # If writing the file fails, ensure that the record is removed from the database 144 | get_files_table(db_instance).delete(filename=filename) 145 | raise 146 | 147 | 148 | def retrieve_cache_fn(identifier, db_instance): 149 | """Retrieve stored object from cache database. 150 | 151 | Args: 152 | identifier: identifier to use as a reference if the corresponding data is already cached 153 | db_instance: Connected Database file with `DBConnect()`. 154 | 155 | Returns: 156 | Path: to the cached file. Caller needs to read the file 157 | 158 | Raises: 159 | RuntimeError: if not exactly one match found 160 | 161 | """ 162 | matches = match_identifier_in_cache(identifier, db_instance) 163 | if len(matches) != 1: 164 | raise RuntimeError(f'Did not find exactly one entry for this identifier (`{identifier}`): {matches}') 165 | return Path(matches[0][FILENAME_KEY]) 166 | 167 | 168 | def retrieve_cache_object(identifier, db_instance): 169 | """Retrieve stored object from cache database. 170 | 171 | Args: 172 | identifier: identifier to use as a reference if the corresponding data is already cached 173 | db_instance: Connected Database file with `DBConnect()`. 174 | 175 | Returns: 176 | dict: object stored in the cache 177 | 178 | """ 179 | filename = retrieve_cache_fn(identifier, db_instance) 180 | return json.loads(filename.read_text()) 181 | -------------------------------------------------------------------------------- /tests/examples/ex_coordinate_chart.py: -------------------------------------------------------------------------------- 1 | """Example Coordinate Chart.""" 2 | 3 | import calendar 4 | 5 | import dash_bootstrap_components as dbc 6 | import numpy as np 7 | import pandas as pd 8 | from dash import html 9 | from implements import implements 10 | 11 | from dash_charts import coordinate_chart 12 | from dash_charts.coordinate_chart import CoordinateChart 13 | from dash_charts.utils_app import AppBase, AppInterface 14 | from dash_charts.utils_fig import min_graph 15 | from dash_charts.utils_helpers import parse_dash_cli_args 16 | 17 | # TODO: Also set marker size based on value? 18 | # TODO: Re-align alignment charts into line and update screenshot 19 | # TODO: Maybe green heat map like Github? For one year? 20 | 21 | 22 | @implements(AppInterface) # noqa: H601 23 | class CoordinateDemo(AppBase): 24 | """Example creating basic Coordinate Charts.""" 25 | 26 | name = 'Example Coordinate Charts' 27 | """Application name""" 28 | 29 | external_stylesheets = [dbc.themes.FLATLY] 30 | 31 | data_raw_years = None 32 | data_raw_months = None 33 | data_raw_circle = None 34 | """In-memory data referenced by callbacks. If modified, will impact all viewers (Years/Months/Circle).""" 35 | 36 | chart_years = None 37 | chart_months = None 38 | chart_circle = None 39 | """Main charts (Coordinate Year/Month/Circle).""" 40 | 41 | id_chart_years = 'years-chart' 42 | id_chart_months = 'months-chart' 43 | id_chart_circle = 'circle-chart' 44 | """Unique name for each chart (Year/Month/Circle).""" 45 | 46 | grid_years = coordinate_chart.YearGrid() 47 | grid_months = coordinate_chart.MonthGrid(titles=[calendar.month_name[2]]) # uses Feb 48 | grid_circle = coordinate_chart.CircleGrid(grid_dims=(5, 4)) # set grid to arbitrary 5x4 49 | """Coordinate chart grids (Year/Month/Circle).""" 50 | 51 | def initialization(self) -> None: 52 | """Initialize ids with `self.register_uniq_ids([...])` and other one-time actions.""" 53 | super().initialization() 54 | self.register_uniq_ids([self.id_chart_years, self.id_chart_months, self.id_chart_circle]) 55 | 56 | day, month, year = (20, 11, 2020) # Alteratively use `now = datetime.datetime.now()`` and `now.day` etc. 57 | 58 | # Data for the Year Chart 59 | month_list = [ 60 | np.random.randint(1e2, size=calendar.monthrange(year, month_idx)[1]) 61 | for month_idx in range(1, month + 1) 62 | ] 63 | # Remove all future data for the current month 64 | month_list[month - 1] = month_list[month - 1][:(day - 1)] 65 | self.data_raw_years = pd.DataFrame(data={'values': self.grid_years.format_data(month_list, year)}) 66 | 67 | # Data for the Month Chart 68 | month_list = np.random.randint(1e4, size=calendar.monthrange(year, month)[1]) 69 | self.data_raw_months = pd.DataFrame(data={'values': self.grid_months.format_data(month_list, year, month)}) 70 | 71 | # Generated data for the Circle Chart 72 | len_points = np.multiply(*self.grid_circle.grid_dims) * len(self.grid_circle.corners['x']) 73 | values = np.random.randint(1e9, size=len_points) 74 | self.data_raw_circle = pd.DataFrame(data={'values': values}) 75 | # Remove a known number of random values from the data set (for the circle Demo) 76 | remove_count = 5 77 | for idx in list(set(np.random.randint(len(values), size=remove_count * 2)))[:remove_count]: 78 | self.data_raw_circle['values'][idx] = None 79 | 80 | def create_elements(self) -> None: 81 | """Initialize the charts, tables, and other Dash elements.""" 82 | self.chart_years = CoordinateChart( 83 | title='Example Year Grid', 84 | grid_dims=self.grid_years.grid_dims, 85 | corners=self.grid_years.corners, 86 | titles=self.grid_years.titles, 87 | layout_overrides=( 88 | ('height', None, 700), 89 | ('width', None, 500), 90 | ), 91 | ) 92 | # Override Coordinate Parameters as needed 93 | self.chart_years.marker_kwargs = self.grid_years.marker_kwargs 94 | self.chart_years.border_opacity = 0 95 | 96 | self.chart_months = CoordinateChart( 97 | title='Example Month Grid', 98 | grid_dims=self.grid_months.grid_dims, 99 | corners=self.grid_months.corners, 100 | titles=self.grid_months.titles, 101 | layout_overrides=( 102 | ('height', None, 400), 103 | ('width', None, 400), 104 | ), 105 | ) 106 | # Override Coordinate Parameters as needed 107 | self.chart_months.marker_kwargs = self.grid_months.marker_kwargs 108 | self.chart_months.marker_kwargs['colorscale'] = 'Bluered' 109 | 110 | self.chart_circle = CoordinateChart( 111 | title='Example Circle Grid', 112 | grid_dims=self.grid_circle.grid_dims, 113 | corners=self.grid_circle.corners, 114 | titles=self.grid_circle.titles, 115 | layout_overrides=( 116 | ('height', None, 750), 117 | ('width', None, 650), 118 | ), 119 | ) 120 | # Override Coordinate Parameters as needed 121 | self.chart_circle.marker_kwargs = self.grid_circle.marker_kwargs 122 | self.chart_circle.marker_kwargs['colorscale'] = 'Cividis' 123 | 124 | def return_layout(self) -> dict: 125 | """Return Dash application layout. 126 | 127 | Returns: 128 | dict: Dash HTML object 129 | 130 | """ 131 | return dbc.Container([ 132 | dbc.Row([ 133 | html.H4(children=self.name), 134 | ]), 135 | dbc.Row([ 136 | dbc.Col( 137 | [ 138 | min_graph( 139 | id=self._il[self.id_chart_months], 140 | figure=self.chart_months.create_figure(df_raw=self.data_raw_months), 141 | ), 142 | ], width=4, 143 | ), 144 | ]), 145 | dbc.Row([ 146 | dbc.Col( 147 | [ 148 | min_graph( 149 | id=self._il[self.id_chart_years], 150 | figure=self.chart_years.create_figure(df_raw=self.data_raw_years), 151 | ), 152 | ], width=5, 153 | ), 154 | dbc.Col( 155 | [ 156 | min_graph( 157 | id=self._il[self.id_chart_circle], 158 | figure=self.chart_circle.create_figure(df_raw=self.data_raw_circle), 159 | ), 160 | ], width=5, 161 | ), 162 | ]), 163 | ]) 164 | 165 | def create_callbacks(self) -> None: 166 | """Create Dash callbacks.""" 167 | ... # No callbacks necessary for this simple example 168 | 169 | 170 | instance = CoordinateDemo 171 | app = instance() 172 | app.create() 173 | if __name__ == '__main__': 174 | app.run(**parse_dash_cli_args()) 175 | else: 176 | FLASK_HANDLE = app.get_server() 177 | -------------------------------------------------------------------------------- /tests/examples/ex_sqlite_realtime.py: -------------------------------------------------------------------------------- 1 | """Demonstrate realtime updates in Dash. 2 | 3 | Based on: https://medium.com/analytics-vidhya/programming-with-databases-in-python-using-sqlite-4cecbef51ab9 4 | 5 | Example Python/SQLite: https://www.dataquest.io/blog/python-pandas-databases/ & 6 | https://sebastianraschka.com/Articles/2014_sqlite_in_python_tutorial.html 7 | 8 | PLANNED: https://plot.ly/python/big-data-analytics-with-pandas-and-sqlite/ 9 | 10 | """ 11 | 12 | import multiprocessing 13 | import time 14 | from pathlib import Path 15 | 16 | import numpy as np 17 | import pandas as pd 18 | from dash import dcc, html 19 | from dash.exceptions import PreventUpdate 20 | from implements import implements 21 | from tqdm import tqdm 22 | 23 | from dash_charts.scatter_line_charts import RollingChart 24 | from dash_charts.utils_app import AppBase, AppInterface 25 | from dash_charts.utils_callbacks import map_outputs 26 | from dash_charts.utils_data import SQLConnection 27 | from dash_charts.utils_fig import min_graph 28 | from dash_charts.utils_helpers import parse_dash_cli_args 29 | 30 | 31 | def use_flag_file(callback, *args, **kwargs): 32 | """Use a flag file to determine if the callback is to be run. 33 | 34 | Args: 35 | callback: path to SQLite file 36 | args: arguments to pass to callback 37 | kwargs: keyword arguments to pass to callback 38 | 39 | """ 40 | # Use a file to indicate if the function is currently writing to the database 41 | flag_file = Path(__file__).parent / 'flag-tempfile.log' 42 | if flag_file.is_file(): 43 | initial = int(flag_file.read_text()) 44 | time.sleep(2) 45 | if flag_file.is_file() and int(flag_file.read_text()) > initial: 46 | return # The thread is currently writing to the flag file 47 | 48 | # Otherwise, create the flag file and run the script 49 | flag_file.write_text('') 50 | try: 51 | callback(*args, flag_file=flag_file, **kwargs) 52 | finally: 53 | flag_file.unlink() 54 | 55 | 56 | def simulate_db_population(db_path, points=1000, delay=0.1, flag_file=None): # noqa: CCR001 57 | """Populate a SQL database in real time so that the changes can be visualized in a chart. 58 | 59 | Args: 60 | db_path: path to SQLite file 61 | points: total number of points to create. Default is 1000 62 | delay: time to wait between creating each new data point (in seconds). Default is 0.1seconds 63 | flag_file: path to a file used to flag when index is running. Default is None. See `use_flag_file()` 64 | 65 | """ 66 | # Clear database if it exists 67 | if db_path.is_file(): 68 | db_path.unlink() 69 | 70 | with SQLConnection(db_path) as conn: 71 | # Create EVENTS table 72 | cursor = conn.cursor() 73 | cursor.execute("""CREATE TABLE EVENTS ( 74 | id INT PRIMARY KEY NOT NULL, 75 | label TEXT NOT NULL, 76 | value INT NOT NULL 77 | );""") 78 | conn.commit() 79 | 80 | # Generate random data points 81 | mu, sigma = (10, 8) # mean and standard deviation 82 | samples = np.random.normal(mu, sigma, points) 83 | 84 | # Fill the database with sample data 85 | for idx in tqdm(range(points)): 86 | value = (-1 if idx > 500 else 1) * idx / 10.0 # Introduce variability 87 | cursor.execute( 88 | 'INSERT INTO EVENTS (id, label, value) VALUES' # noqa: S608 89 | f' ({idx}, "idx-{idx}", {samples[idx] + value})', 90 | ) 91 | conn.commit() 92 | if flag_file is not None: 93 | flag_file.write_text(str(idx)) 94 | time.sleep(delay) 95 | 96 | 97 | @implements(AppInterface) # noqa: H601 98 | class RealTimeSQLDemo(AppBase): 99 | """Example creating a rolling mean chart.""" 100 | 101 | name = 'Example Scatter of Real Time SQL Data' 102 | """Application name""" 103 | 104 | db_path = Path(__file__).parent / 'realtime-sql-data.sqlite' 105 | """Path to the SQLite database file.""" 106 | 107 | chart_main = None 108 | """Main chart (Scatter).""" 109 | 110 | id_chart = 'real-time-chart' 111 | """Unique name for the main chart.""" 112 | 113 | id_interval = 'graph-update' 114 | """ID of the interval element to regularly update the UI.""" 115 | 116 | def initialization(self) -> None: 117 | """Initialize ids with `self.register_uniq_ids([...])` and other one-time actions.""" 118 | super().initialization() 119 | self.register_uniq_ids([self.id_chart, self.id_interval]) 120 | 121 | def create_elements(self) -> None: 122 | """Initialize the charts, tables, and other Dash elements.""" 123 | self.chart_main = RollingChart( 124 | title='Live-Updating Scatter Plot', 125 | xlabel='Index', 126 | ylabel='Value', 127 | ) 128 | self.chart_main.count_rolling = 20 129 | 130 | def generate_data(self) -> None: 131 | """Start the realtime updates of the database. Function could be run from separate process.""" 132 | db_path = self.db_path 133 | process = multiprocessing.Process( 134 | target=use_flag_file, args=[simulate_db_population, db_path], kwargs={'points': 1000, 'delay': 0.05}, 135 | ) 136 | process.start() 137 | 138 | def return_layout(self) -> dict: 139 | """Return Dash application layout. 140 | 141 | Returns: 142 | dict: Dash HTML object 143 | 144 | """ 145 | return html.Div( 146 | style={ 147 | 'maxWidth': '1000px', 148 | 'marginRight': 'auto', 149 | 'marginLeft': 'auto', 150 | }, 151 | children=[ 152 | html.H4(children=self.name), 153 | min_graph(id=self._il[self.id_chart], animate=True), 154 | dcc.Interval(id=self._il[self.id_interval], interval=1000, n_intervals=0), 155 | ], 156 | ) 157 | 158 | def create_callbacks(self) -> None: 159 | """Create Dash callbacks. 160 | 161 | Raises: 162 | PreventUpdate: if there is not yet enough data to plot the moving average 163 | 164 | """ 165 | outputs = [(self.id_chart, 'figure')] 166 | inputs = [(self.id_interval, 'n_intervals')] 167 | states = [] 168 | 169 | @self.callback(outputs, inputs, states, pic=True) 170 | def update_chart(*raw_args): 171 | with SQLConnection(self.db_path) as conn: 172 | df_events = pd.read_sql_query('SELECT id, label, value FROM EVENTS', conn) 173 | moving_window = 5 174 | count_points = len(df_events['id']) 175 | if count_points < moving_window: 176 | raise PreventUpdate 177 | 178 | for new_key, old_key in [('x', 'id'), ('y', 'value')]: 179 | df_events[new_key] = df_events[old_key] 180 | self.chart_main.axis_range = { 181 | 'x': [float(np.max([0, count_points - 500])), count_points], 182 | } 183 | new_figure = self.chart_main.create_figure(df_raw=df_events) 184 | 185 | return map_outputs(outputs, [(self.id_chart, 'figure', new_figure)]) 186 | 187 | 188 | instance = RealTimeSQLDemo 189 | app = instance() 190 | app.create() 191 | if __name__ == '__main__': 192 | app.run(**parse_dash_cli_args()) 193 | else: 194 | FLASK_HANDLE = app.get_server() 195 | --------------------------------------------------------------------------------