├── 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 | 
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: 
71 |
72 | Actual Image:
73 |
74 | 
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 |
--------------------------------------------------------------------------------