├── tests ├── __init__.py ├── conftest.py ├── _test_system.py └── test_units.py ├── Procfile ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── config.yml └── workflows │ ├── cache_citations.yml │ └── ci.yml ├── htm_dashboard ├── __init__.py ├── cache_citations.py ├── export.py ├── infos.py ├── layout.py ├── new_property_form.py ├── citations.json ├── graph.py ├── callbacks.py └── tab.py ├── requirements.txt ├── Dockerfile ├── LICENSE ├── README.md ├── .gitignore └── app.py /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn app:server -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [RemDelaporteMathurin] 4 | 5 | -------------------------------------------------------------------------------- /htm_dashboard/__init__.py: -------------------------------------------------------------------------------- 1 | ACTIVE_GROUPS = [ 2 | "diffusivity", 3 | "solubility", 4 | "permeability", 5 | "recombination_coeff", 6 | "dissociation_coeff", 7 | ] 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Suggest new properties 3 | url: https://github.com/RemDelaporteMathurin/h-transport-materials/issues/new/choose 4 | about: Please suggest your property here. 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | h-transport-materials~=0.18 2 | numpy<2 3 | dash==2.9.3 4 | dash-bootstrap-components==1.1.0 5 | dash-bootstrap-templates==1.0.7 6 | dash_daq==0.5.0 7 | plotly==5.8.0 8 | jinja2==3.1.1 9 | gunicorn 10 | pandas==1.5.2 11 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.chrome.options import Options 2 | 3 | 4 | def pytest_setup_options(): 5 | options = Options() 6 | options.add_argument("--headless") 7 | options.add_argument("--disable-gpu") 8 | return options 9 | -------------------------------------------------------------------------------- /htm_dashboard/cache_citations.py: -------------------------------------------------------------------------------- 1 | import h_transport_materials as htm 2 | from datetime import date 3 | import json 4 | 5 | citations_data = {"date": str(date.today()), "dois": {}} 6 | 7 | for i, prop in enumerate(htm.database): 8 | if prop.doi not in citations_data["dois"]: 9 | citations_data["dois"][prop.doi] = prop.nb_citations 10 | print(i) 11 | 12 | with open("htm_dashboard/citations.json", "w") as outfile: 13 | json.dump(citations_data, outfile, indent=4) 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM continuumio/miniconda3:4.9.2 2 | 3 | COPY app.py . 4 | ADD htm_dashboard ./htm_dashboard 5 | COPY requirements.txt . 6 | 7 | RUN pip install -r requirements.txt 8 | 9 | ENV PORT 8080 10 | 11 | EXPOSE 8080 12 | 13 | # Run the web service on container startup. Here we use the gunicorn 14 | # webserver, with one worker process and 8 threads. 15 | # For environments with multiple CPU cores, increase the number of workers 16 | # to be equal to the cores available. 17 | # Timeout is set to 0 to disable the timeouts of the workers to allow Cloud Run 18 | # to handle instance scaling. For more details see 19 | # https://cloud.google.com/run/docs/quickstarts/build-and-deploy/python 20 | CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 app:server 21 | -------------------------------------------------------------------------------- /.github/workflows/cache_citations.yml: -------------------------------------------------------------------------------- 1 | name: refresh map 2 | 3 | on: 4 | schedule: 5 | - cron: "00 00 1 * *" # runs at 00:00 every 1st of month 6 | 7 | jobs: 8 | getdataandrefreshmap: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: checkout repo content 12 | uses: actions/checkout@v3 # checkout the repository content to github runner. 13 | - name: setup python 14 | uses: actions/setup-python@v4 15 | with: 16 | python-version: 3.8 #install the python needed 17 | - name: Install dependencies 18 | run: | 19 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 20 | - name: execute py script 21 | run: | 22 | python htm_dashboard/cache_citations.py 23 | git config user.name github-actions 24 | git config user.email github-actions@github.com 25 | git add . 26 | git commit -m "updated citations file" 27 | git push -------------------------------------------------------------------------------- /tests/_test_system.py: -------------------------------------------------------------------------------- 1 | from app import app 2 | 3 | # dash_duo as a function argument 4 | def test_001_child_with_0(dash_duo): 5 | # 3. define your app inside the test function 6 | # 4. host the app locally in a thread, all dash server configs could be 7 | # passed after the first app argument 8 | dash_duo.start_server(app) 9 | # # 5. use wait_for_* if your target element is the result of a callback, 10 | # # keep in mind even the initial rendering can trigger callbacks 11 | dash_duo.wait_for_element_by_id("graph_diffusivity", timeout=4) 12 | # # 6. use this form if its present is expected at the action point 13 | # assert dash_duo.find_element("#graph_diffusivity").figure is not None 14 | assert dash_duo.get_logs() == [] 15 | # # 7. to make the checkpoint more readable, you can describe the 16 | # # acceptance criterion as an assert message after the comma. 17 | # assert dash_duo.get_logs() == [], "browser console should contain no error" 18 | # # 8. visual testing with percy snapshot 19 | # dash_duo.percy_snapshot("test_001_child_with_0-layout") 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Install Chrome and chromedriver 18 | uses: browser-actions/setup-chrome@v1 19 | with: 20 | chrome-version: "132.0.6821.2" 21 | 22 | - name: Set up Python 3.10 23 | uses: actions/setup-python@v3 24 | with: 25 | python-version: "3.10" 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install pytest dash[testing] 30 | pip install -r requirements.txt 31 | 32 | - name: Test with pytest 33 | run: | 34 | pytest tests 35 | 36 | #--cov htm_dashboard --cov-report xml --cov-report term 37 | 38 | # - name: Upload to codecov 39 | # run: | 40 | # curl -Os https://uploader.codecov.io/latest/linux/codecov 41 | 42 | # chmod +x codecov 43 | # ./codecov 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Rémi Delaporte-Mathurin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /htm_dashboard/export.py: -------------------------------------------------------------------------------- 1 | import h_transport_materials as htm 2 | import json 3 | from jinja2 import Template 4 | 5 | 6 | type_to_database = { 7 | "diffusivity": "htm.diffusivities", 8 | "solubility": "htm.solubilities", 9 | "permeability": "htm.permeabilities", 10 | "recombination_coeff": "htm.recombination_coeffs", 11 | "dissociation_coeff": "htm.dissociation_coeffs", 12 | } 13 | 14 | 15 | python_template = Template( 16 | """import h_transport_materials as htm 17 | import matplotlib.pyplot as plt 18 | import numpy as np 19 | 20 | filtered_{{group}} = ( 21 | {{database}}.filter(material={{materials}}) 22 | .filter(author={{authors}}) 23 | .filter(isotope={{isotopes}}) 24 | .filter(year=np.arange({{yearmin}}, {{yearmax}}, step=1).tolist()) 25 | ) 26 | 27 | htm.plotting.plot(filtered_{{group}}) 28 | 29 | plt.legend() 30 | plt.xlabel("1/T (K$^{-1}$)") 31 | plt.yscale("log") 32 | plt.show() 33 | 34 | """ 35 | ) 36 | 37 | 38 | def generate_python_code(materials, authors, isotopes, yearmin, yearmax, group): 39 | python_code = python_template.render( 40 | group=group, 41 | database=type_to_database[group], 42 | materials=[mat.lower() for mat in materials], 43 | authors=[author.lower() for author in authors], 44 | isotopes=[iso.lower() for iso in isotopes], 45 | yearmin="{}".format(yearmin), 46 | yearmax="{}".format(yearmax), 47 | ) 48 | return python_code 49 | -------------------------------------------------------------------------------- /tests/test_units.py: -------------------------------------------------------------------------------- 1 | from htm_dashboard.callbacks import create_make_download_data_function 2 | import h_transport_materials as htm 3 | 4 | from contextvars import copy_context 5 | from dash._callback_context import context_value 6 | from dash._utils import AttributeDict 7 | from datetime import datetime 8 | 9 | from htm_dashboard.callbacks import ( 10 | create_make_download_data_function, 11 | make_citations_graph, 12 | ) 13 | 14 | 15 | def test_export_groups(): 16 | """Tests the export to json callback""" 17 | export_fun = create_make_download_data_function("diffusivity") 18 | 19 | def run_callback(): 20 | context_value.set( 21 | AttributeDict( 22 | **{ 23 | "triggered_inputs": [ 24 | {"prop_id": "extract_button_diffusivity.n_clicks"} 25 | ] 26 | } 27 | ) 28 | ) 29 | return export_fun( 30 | 1, 31 | material_filter=["tungsten"], 32 | author_filter=["frauenfelder"], 33 | isotope_filter=["H", "D", "T"], 34 | year_filter=None, 35 | ) 36 | 37 | ctx = copy_context() 38 | output = ctx.run(run_callback) 39 | assert type(output["content"]) == str 40 | 41 | 42 | def test_citation_graphs_per_year_same_year(): 43 | current_year = datetime.now().year 44 | 45 | group = htm.PropertiesGroup([htm.Property(year=current_year)]) 46 | make_citations_graph(group, per_year=True) 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # h-transport-materials-dashboard 2 | 3 | Finding material properties for hydrogen transport is often tedious as many different values can be found in literature. 4 | The goal of this tool is to help scientists visualise material properties and compare them. 5 | 6 | This web app relies on the hydrogen transport materials database [HTM](https://github.com/RemDelaporteMathurin/h-transport-materials). 7 | 8 | :point_right: [The app!](https://htm-dashboard-uan5l4xr6a-od.a.run.app/) 9 | 10 | :warning: Disclaimer: 11 | This web app is built for visualising properties. 12 | For integrations in workflows and applications, we recommend using the python API [HTM](https://github.com/RemDelaporteMathurin/h-transport-materials) 13 | 14 | ![image](https://user-images.githubusercontent.com/40028739/194302707-39c7c659-34a4-42d7-adb1-52b08ad72023.png) 15 | 16 | 17 | ## Features 18 | - Quickly visualise the HTM database 19 | - Add your own properties 20 | - Compute mean curves of properties groups 21 | - Extract data to JSON 22 | - Extract python script (using the htm API) 23 | 24 | ## Stats 25 | - How many times were the papers cited? 26 | - When were the papers published? 27 | - Repartition by material, isotope, author. 28 | 29 | ![image](https://user-images.githubusercontent.com/40028739/194307879-33fb7953-62b8-4f0a-8c53-1bfece5e1110.png) 30 | 31 | ## Contributing 32 | 33 | - "I want to contribute to the database" :point_right: [go to the database repository](https://github.com/RemDelaporteMathurin/h-transport-materials) 34 | - "I want to contribute to the web app" :point_right: you are at the right place! Feel free to [open an issue](https://github.com/RemDelaporteMathurin/h-transport-materials-dashboard/issues/new) 35 | -------------------------------------------------------------------------------- /htm_dashboard/infos.py: -------------------------------------------------------------------------------- 1 | import h_transport_materials 2 | from dash import html 3 | 4 | try: 5 | version = h_transport_materials.__version__ 6 | except AttributeError: 7 | version = "[unknown]" 8 | 9 | text_infos = [ 10 | html.Div( 11 | [ 12 | "Finding material properties for ", 13 | html.B("hydrogen transport"), 14 | " is often tedious as many different values can be found in literature. ", 15 | "The goal of this tool is to help scientists visualise material properties and compare them.", 16 | ] 17 | ), 18 | html.Br(), 19 | html.Div( 20 | "On the left-hand side of the screen, properties can be filtered by material, author, isotope or by year of publication." 21 | ), 22 | html.Br(), 23 | html.Div( 24 | [ 25 | html.B("Compute mean curve"), 26 | ": calculates the mean curve of the displayed properties.", 27 | ] 28 | ), 29 | html.Br(), 30 | html.Div( 31 | [ 32 | html.B("Add property"), 33 | ": adds a custom diffusivity or solubility to the dataset.", 34 | ] 35 | ), 36 | html.Br(), 37 | html.Div( 38 | [html.B("Extract data"), ": downloads the displayed properties to a JSON file."] 39 | ), 40 | html.Br(), 41 | html.Div( 42 | [ 43 | html.B("Python"), 44 | ": generates a python script using h-transport-materials to plot the displayed data.", 45 | ] 46 | ), 47 | html.Br(), 48 | html.Div( 49 | "This dashboard relies on the library H-transport-materials (HTM) v{} and Plotly dash.".format( 50 | version 51 | ) 52 | ), 53 | ] 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | 132 | .DS_Store 133 | -------------------------------------------------------------------------------- /htm_dashboard/layout.py: -------------------------------------------------------------------------------- 1 | from .infos import text_infos 2 | from .new_property_form import make_form 3 | from htm_dashboard import ACTIVE_GROUPS 4 | from .tab import make_tab 5 | 6 | from dash import html 7 | import dash_bootstrap_components as dbc 8 | from dash_bootstrap_templates import ThemeSwitchAIO 9 | 10 | 11 | def make_modal_add_property(property: str): 12 | 13 | modal = dbc.Modal( 14 | [ 15 | dbc.ModalHeader(dbc.ModalTitle(html.H2(f"Add a {property}"))), 16 | dbc.ModalBody(make_form(property)), 17 | dbc.ModalFooter( 18 | [ 19 | html.Div("", id=f"error_message_new_{property}"), 20 | dbc.Button( 21 | "Submit", 22 | id=f"submit_new_{property}", 23 | color="primary", 24 | n_clicks="0", 25 | ), 26 | ] 27 | ), 28 | ], 29 | id=f"modal_add_{property}", 30 | is_open=False, 31 | # size="lg", 32 | ) 33 | return modal 34 | 35 | 36 | template_theme1 = "plotly_white" 37 | template_theme2 = "plotly_dark" 38 | url_theme1 = dbc.themes.MINTY 39 | url_theme2 = dbc.themes.CYBORG 40 | theme_switch = ThemeSwitchAIO(aio_id="theme", themes=[url_theme1, url_theme2]) 41 | 42 | header = dbc.Row( 43 | [ 44 | dbc.Col( 45 | [ 46 | html.H2("H-transport properties dashboard"), 47 | html.H5("Rémi Delaporte-Mathurin"), 48 | ], 49 | width=9, 50 | ), 51 | dbc.Col( 52 | html.Div( 53 | [ 54 | html.A( 55 | [ 56 | "Infos", 57 | html.Img( 58 | src="https://dash.gallery/dash-world-cell-towers/assets/question-circle-solid.svg", 59 | height=20, 60 | style={"margin-left": "5px"}, 61 | ), 62 | ], 63 | style={ 64 | "margin-right": "45px", 65 | "cursor": "pointer", 66 | }, 67 | id="open-sm", 68 | ), 69 | dbc.Modal( 70 | [ 71 | dbc.ModalHeader( 72 | dbc.ModalTitle( 73 | html.H2( 74 | "Welcome to the H-transport materials dashboard!" 75 | ) 76 | ) 77 | ), 78 | dbc.ModalBody(text_infos), 79 | dbc.ModalFooter("Contact: rdelaportemathurin@gmail.com"), 80 | ], 81 | id="modal-infos", 82 | is_open=False, 83 | size="lg", 84 | ), 85 | html.A( 86 | [ 87 | "View it on GitHub", 88 | html.Img( 89 | src="https://cdn-icons-png.flaticon.com/512/25/25231.png", 90 | height=40, 91 | ), 92 | ], 93 | href="https://github.com/RemDelaporteMathurin/h-transport-materials-dashboard", 94 | target="_blank", # opens in a new tab 95 | ), 96 | theme_switch, 97 | ] 98 | ), 99 | align="end", 100 | width=3, 101 | ), 102 | ], 103 | align="end", 104 | ) 105 | 106 | layout = dbc.Container( 107 | [ 108 | header, 109 | html.Hr(), 110 | dbc.Tabs( 111 | id="tabs-example-graph", 112 | children=[make_tab(group) for group in ACTIVE_GROUPS], 113 | ), 114 | ] 115 | + [make_modal_add_property(group) for group in ACTIVE_GROUPS], 116 | fluid=True, 117 | className="dbc bg-opacity-10 bg-black mb-2", 118 | ) 119 | -------------------------------------------------------------------------------- /htm_dashboard/new_property_form.py: -------------------------------------------------------------------------------- 1 | import dash_bootstrap_components as dbc 2 | from dash import html 3 | import h_transport_materials as htm 4 | 5 | 6 | def make_form(property_type: str): 7 | 8 | if property_type == "diffusivity": 9 | pre_exp_label = f"D_0 ({htm.Diffusivity().units:~P})" 10 | act_energy_label = "E_D (eV)" 11 | elif property_type == "solubility": 12 | pre_exp_label = f"S_0 (Sievert: {htm.Solubility(law='sievert').units:~P}, Henry: {htm.Solubility(law='henry').units:~P})" 13 | act_energy_label = "E_S (eV)" 14 | elif property_type == "permeability": 15 | pre_exp_label = f"P_0 (Sievert: {htm.Permeability(law='sievert').units:~P}, Henry: {htm.Permeability(law='henry').units:~P})" 16 | act_energy_label = "E_P (eV)" 17 | elif property_type == "recombination_coeff": 18 | pre_exp_label = f"Kr_0 ({htm.RecombinationCoeff().units:~P})" 19 | act_energy_label = "E_Kr (eV)" 20 | elif property_type == "dissociation_coeff": 21 | pre_exp_label = f"Kd_0 ({htm.DissociationCoeff().units:~P})" 22 | act_energy_label = "E_Kd (eV)" 23 | 24 | preexponential_input = html.Div( 25 | [ 26 | dbc.Label(pre_exp_label, width=10), 27 | dbc.Col( 28 | dbc.Input( 29 | type="number", 30 | id=f"new_{property_type}_pre_exp", 31 | placeholder="Enter pre-exponential factor", 32 | required=True, 33 | ), 34 | width=10, 35 | ), 36 | ], 37 | ) 38 | 39 | activation_energy_input = html.Div( 40 | [ 41 | dbc.Label(act_energy_label, width=2), 42 | dbc.Col( 43 | dbc.Input( 44 | type="number", 45 | id=f"new_{property_type}_act_energy", 46 | placeholder="Enter activation energy", 47 | required=True, 48 | ), 49 | width=10, 50 | ), 51 | ], 52 | ) 53 | 54 | author_input = html.Div( 55 | [ 56 | dbc.Label("Author", width=2), 57 | dbc.Col( 58 | dbc.Input( 59 | type="text", 60 | id=f"new_{property_type}_author", 61 | placeholder="Enter author", 62 | required=True, 63 | ), 64 | width=10, 65 | ), 66 | ], 67 | ) 68 | 69 | year_input = html.Div( 70 | [ 71 | dbc.Label("Year", width=2), 72 | dbc.Col( 73 | dbc.Input( 74 | type="number", 75 | id=f"new_{property_type}_year", 76 | placeholder="Enter year of publication", 77 | required=True, 78 | ), 79 | width=10, 80 | ), 81 | ], 82 | ) 83 | 84 | isotope_input = html.Div( 85 | [ 86 | dbc.Label("Isotope", width=2), 87 | dbc.Col( 88 | dbc.RadioItems( 89 | id=f"new_{property_type}_isotope", 90 | value="H", 91 | options=[ 92 | {"label": "H", "value": "H"}, 93 | {"label": "D", "value": "D"}, 94 | {"label": "T", "value": "T"}, 95 | ], 96 | ), 97 | width=10, 98 | ), 99 | ], 100 | ) 101 | 102 | material_input = html.Div( 103 | [ 104 | dbc.Label("Material", width=2), 105 | dbc.Col( 106 | dbc.Input( 107 | type="text", 108 | id=f"new_{property_type}_material", 109 | placeholder="Enter material", 110 | required=True, 111 | ), 112 | width=10, 113 | ), 114 | ], 115 | ) 116 | 117 | temperature_input = html.Div( 118 | [ 119 | dbc.Label("Temperature range", width="auto"), 120 | dbc.Row( 121 | [ 122 | dbc.Col( 123 | dbc.Input( 124 | type="number", 125 | id=f"new_{property_type}_range_low", 126 | placeholder="300 K", 127 | ), 128 | width=3, 129 | ), 130 | dbc.Col( 131 | dbc.Input( 132 | type="number", 133 | id=f"new_{property_type}_range_high", 134 | placeholder="1200 K", 135 | ), 136 | width=3, 137 | ), 138 | ] 139 | ), 140 | ], 141 | ) 142 | 143 | inputs = [ 144 | preexponential_input, 145 | activation_energy_input, 146 | author_input, 147 | year_input, 148 | isotope_input, 149 | material_input, 150 | temperature_input, 151 | ] 152 | if property_type in ["solubility", "permeability"]: 153 | solubility_law_input = html.Div( 154 | [ 155 | dbc.Label("Law", width=2), 156 | dbc.Col( 157 | dbc.RadioItems( 158 | id=f"new_{property_type}_law", 159 | value="sievert", 160 | options=[ 161 | {"label": "Sievert", "value": "sievert"}, 162 | {"label": "Henry", "value": "henry"}, 163 | ], 164 | ), 165 | width=10, 166 | ), 167 | ], 168 | ) 169 | inputs.insert(0, solubility_law_input) 170 | form = dbc.Form(inputs) 171 | return form 172 | -------------------------------------------------------------------------------- /htm_dashboard/citations.json: -------------------------------------------------------------------------------- 1 | { 2 | "date": "2025-12-01", 3 | "dois": { 4 | "10.1016/S1359-6454(98)00333-4": 271, 5 | "null": 0, 6 | "10.1149/1.2428079": 18, 7 | "10.1016/S0022-3115(97)00146-3": 42, 8 | "10.1016/0956-7151(94)90329-8": 35, 9 | "10.1088/0305-4608/13/6/013": 91, 10 | "10.2320/matertrans1989.32.1109": 73, 11 | "10.1016/0036-9748(82)90354-4": 41, 12 | "10.1016/0001-6160(86)90219-1": 94, 13 | "10.1111/j.1551-2916.2004.00003.x": 74, 14 | "10.1103/PhysRevB.69.024302": 79, 15 | "10.1111/j.1151-2916.1977.tb15493.x": 139, 16 | "10.1103/PhysRevB.4.330": 279, 17 | "10.1116/1.1318061": 122, 18 | "10.1016/0022-3115(87)90349-7": 24, 19 | "10.1016/0022-3115(90)90127-9": 28, 20 | "10.1016/S0022-3115(98)00878-2": 18, 21 | "10.2320/jinstmet1952.46.3_285": 21, 22 | "10.1016/j.fusengdes.2010.03.063": 13, 23 | "10.1080/14786437608228179": 148, 24 | "10.1016/S0022-3697(73)80022-8": 99, 25 | "10.1016/j.nme.2022.101256": 0, 26 | "10.1016/S0022-3115(98)00276-1": 18, 27 | "10.1016/j.jnucmat.2016.01.035": 15, 28 | "10.5772/34469": 8, 29 | "10.13182/FST41-872": 62, 30 | "10.1016/j.jnucmat.2021.152904": 20, 31 | "10.1016/j.jnucmat.2007.03.114": 82, 32 | "10.1016/j.nme.2021.101062": 6, 33 | "10.1016/j.nme.2019.01.030": 26, 34 | "10.1016/j.jnucmat.2006.07.011": 34, 35 | "10.1039/D0TA10576G": 28, 36 | "10.13538/j.1001-8042/nst.25.040602": 0, 37 | "10.1016/j.net.2018.10.008": 4, 38 | "10.1016/j.fusengdes.2008.05.016": 44, 39 | "10.1016/j.jnucmat.2004.04.220": 34, 40 | "10.1016/S0920-3796(89)80124-3": 40, 41 | "10.1021/j100869a013": 57, 42 | "10.1021/i160051a015": 57, 43 | "10.1016/0022-3115(88)90301-7": 29, 44 | "10.1016/0920-3796(91)90003-9": 114, 45 | "10.1016/0022-3115(87)90006-7": 40, 46 | "10.1016/0022-3115(92)90504-E": 45, 47 | "10.1016/0022-3115(83)90069-7": 42, 48 | "10.1016/0022-3115(84)90198-3": 54, 49 | "10.1016/0022-3115(85)90127-8": 28, 50 | "10.1002/bbpc.19900940612": 20, 51 | "10.1016/j.fusengdes.2005.06.364": 73, 52 | "10.1016/j.fusengdes.2015.05.060": 8, 53 | "10.1016/j.jnucmat.2010.12.126": 28, 54 | "10.13182/FST60-1163": 15, 55 | "10.1016/j.fusengdes.2012.03.004": 20, 56 | "10.1585/pfr.7.2405074": 1, 57 | "https://doi.org/10.1016/0022-5088(86)90682-X": 6, 58 | "10.1016/j.fusengdes.2023.114136": 2, 59 | "10.1021/j100612a013": 74, 60 | "10.1016/0022-1902(79)80077-9": 26, 61 | "10.1007/s11661-998-1011-3": 111, 62 | "10.1016/S0022-3115(98)00038-5": 126, 63 | "10.1016/j.ijhydene.2013.01.091": 77, 64 | "10.1016/0022-3115(72)90065-7": 186, 65 | "10.1016/0022-3115(67)90047-5": 370, 66 | "10.1016/0022-3115(89)90446-7": 61, 67 | "10.1016/0022-3115(90)90274-Q": 95, 68 | "10.1016/0022-3115(67)90190-0": 75, 69 | "10.2172/10191406": 12, 70 | "10.1016/0022-3115(89)90265-1": 136, 71 | "10.1016/0022-3115(88)90247-4": 90, 72 | "10.1063/1.4813919": 48, 73 | "10.1016/0022-3115(93)90332-S": 3, 74 | "10.1016/B978-0-12-803581-8.11754-0": 9, 75 | "10.1016/B978-0-12-522660-8.50010-6": 86, 76 | "10.2320/matertrans1960.26.869": 15, 77 | "10.1515/ijmr-1958-490502": 5, 78 | "10.1016/0022-3115(93)90392-C": 9, 79 | "10.1016/j.matlet.2004.12.037": 52, 80 | "10.2355/tetsutohagane1955.64.5_615": 3, 81 | "10.1063/5.0204192": 1, 82 | "10.1016/0022-3115(85)90056-X": 44, 83 | "10.2172/4583045": 18, 84 | "10.1080/15361055.2020.1725368": 6, 85 | "https://doi.org/10.1016/j.jnucmat.2023.154851": 2, 86 | "10.1016/S0022-3115(09)80083-4": 12, 87 | "10.1016/0022-3697(82)90104-4": 48, 88 | "10.1063/1.1669720": 90, 89 | "10.1016/0022-3115(74)90263-3": 19, 90 | "10.1016/0001-6160(75)90057-7": 97, 91 | "10.1016/0022-5088(91)90195-A": 24, 92 | "10.1088/0305-4608/13/10/015": 134, 93 | "10.1021/j100723a033": 311, 94 | "10.1007/BF02651617": 20, 95 | "10.2172/4413386": 4, 96 | "10.1103/PhysRevB.43.6968": 21, 97 | "https://doi.org/10.1016/S0022-3115(02)01370-3": 13, 98 | "10.1016/0036-9748(79)90391-0": 22, 99 | "10.2172/6420120": 10, 100 | "10.1116/1.1492699": 510, 101 | "10.1016/j.jnucmat.2014.09.003": 26, 102 | "10.1063/1.3386515": 195, 103 | "10.1557/JMR.2010.0036": 252, 104 | "10.1063/1.1725575": 68, 105 | "10.1007/BF00715727": 47, 106 | "10.1016/S0022-3115(01)00486-X": 51, 107 | "10.1088/1402-4896/ab4b42": 29, 108 | "10.1016/j.actamat.2015.04.052": 207, 109 | "10.13182/FST92-A29837": 150, 110 | "10.1080/15361055.2019.1705727": 5, 111 | "10.1016/j.jnucmat.2018.04.021": 4, 112 | "10.1016/j.fusengdes.2016.09.003": 21, 113 | "https://doi.org/10.1016/j.fusengdes.2016.03.045": 18, 114 | "10.1088/0031-8949/2009/T138/014052": 24, 115 | "10.13182/FST11-A12707": 17, 116 | "10.1016/j.fusengdes.2019.03.083": 9, 117 | "10.1016/j.jnucmat.2020.152675": 7, 118 | "10.3390/membranes12060579": 8, 119 | "10.1016/B978-0-08-056033-5.00116-6": 117, 120 | "10.1016/0022-3115(88)90038-4": 143, 121 | "10.1238/Physica.Topical.094a00121": 17, 122 | "10.1016/S0022-3115(00)00188-4": 36, 123 | "10.1016/S0022-3115(00)00314-7": 10, 124 | "10.1016/j.fusengdes.2005.07.019": 47, 125 | "10.1016/S0022-3115(97)00021-4": 153, 126 | "10.1016/j.fusengdes.2017.04.022": 16, 127 | "10.1016/0001-6160(86)90123-9": 265, 128 | "10.1016/0920-3796(94)00434-X": 14, 129 | "10.1016/0921-5093(89)90857-5": 101, 130 | "10.3938/jkps.59.3019": 16, 131 | "10.13182/FST85-A24629": 29, 132 | "https://doi.org/10.1016/0022-3115(91)90091-K": 16, 133 | "10.13182/FST57-3-185": 6, 134 | "10.1016/0022-3115(87)90476-4": 51, 135 | "10.1016/0022-3115(80)90219-6": 41, 136 | "10.1016/j.jnucmat.2015.02.040": 62, 137 | "10.1016/0022-3115(82)90214-8": 36, 138 | "10.13182/FST84-A23218": 9, 139 | "10.1016/0022-3115(85)90461-1": 20, 140 | "10.1016/S0022-3115(01)00715-2": 30, 141 | "10.1016/j.jnucmat.2007.03.239": 17, 142 | "10.1016/S0920-3796(00)00305-7": 16, 143 | "https://doi.org/10.1016/j.jnucmat.2016.04.054": 44, 144 | "10.2172/5361604": 0, 145 | "https://doi.org/10.1016/0022-3115(80)90397-9": 26, 146 | "10.1063/1.439496": 36, 147 | "https://doi.org/10.1016/0022-3115(93)90427-Z": 8, 148 | "https://doi.org/10.1016/0022-3115(90)90125-7": 16, 149 | "https://doi.org/10.1016/j.jnucmat.2023.154484": 10, 150 | "10.1063/1.1708243": 99, 151 | "10.1016/j.nme.2016.09.001": 10, 152 | "10.1111/j.1151-2916.1978.tb09284.x": 66 153 | } 154 | } -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from htm_dashboard.layout import layout 2 | from htm_dashboard import ACTIVE_GROUPS 3 | import htm_dashboard.callbacks as cb 4 | 5 | import dash 6 | import dash_bootstrap_components as dbc 7 | from dash_bootstrap_templates import ThemeSwitchAIO 8 | 9 | # stylesheet with the .dbc class 10 | dbc_css = "https://cdn.jsdelivr.net/gh/AnnMarieW/dash-bootstrap-templates/dbc.min.css" 11 | 12 | app = dash.Dash(__name__, external_stylesheets=[dbc.themes.MINTY, dbc_css]) 13 | 14 | server = app.server 15 | 16 | app.layout = layout 17 | 18 | 19 | @app.callback( 20 | dash.Output("modal-infos", "is_open"), 21 | dash.Input("open-sm", "n_clicks"), 22 | dash.State("modal-infos", "is_open"), 23 | ) 24 | def toggle_modal(n1, is_open): 25 | if n1: 26 | return not is_open 27 | return is_open 28 | 29 | 30 | for group in ACTIVE_GROUPS: 31 | 32 | app.callback( 33 | dash.Output(f"graph_nb_citations_{group}", "figure"), 34 | dash.Input(f"graph_{group}", "figure"), 35 | dash.Input(f"per_year_citations_{group}", "on"), 36 | dash.State(f"material_filter_{group}", "value"), 37 | dash.State(f"isotope_filter_{group}", "value"), 38 | dash.State(f"author_filter_{group}", "value"), 39 | dash.State(f"year_filter_{group}", "value"), 40 | )(cb.create_make_citations_figure_function(group)) 41 | 42 | app.callback( 43 | dash.Output(f"material_filter_{group}", "value"), 44 | dash.Input(f"add_all_materials_{group}", "n_clicks"), 45 | )(cb.create_add_all_materials_function(group)) 46 | 47 | app.callback( 48 | dash.Output(f"author_filter_{group}", "value"), 49 | dash.Input(f"add_all_authors_{group}", "n_clicks"), 50 | )(cb.create_add_all_authors_function(group)) 51 | 52 | app.callback( 53 | dash.Output(f"graph_{group}", "figure"), 54 | dash.Input(f"material_filter_{group}", "value"), 55 | dash.Input(f"isotope_filter_{group}", "value"), 56 | dash.Input(f"author_filter_{group}", "value"), 57 | dash.Input(f"year_filter_{group}", "value"), 58 | dash.Input(f"mean_button_{group}", "n_clicks"), 59 | dash.Input(f"colour-by_{group}", "value"), 60 | dash.Input(ThemeSwitchAIO.ids.switch("theme"), "value"), 61 | )(cb.create_update_graph_function(group)) 62 | 63 | app.callback( 64 | dash.Output(f"graph_prop_per_year_{group}", "figure"), 65 | dash.Input(f"graph_{group}", "figure"), 66 | dash.State(f"material_filter_{group}", "value"), 67 | dash.State(f"isotope_filter_{group}", "value"), 68 | dash.State(f"author_filter_{group}", "value"), 69 | dash.State(f"year_filter_{group}", "value"), 70 | )(cb.create_update_entries_per_year_graph_function(group)) 71 | 72 | app.callback( 73 | dash.Output(f"graph_materials_{group}", "figure"), 74 | dash.Input(f"graph_{group}", "figure"), 75 | dash.State(f"material_filter_{group}", "value"), 76 | dash.State(f"isotope_filter_{group}", "value"), 77 | dash.State(f"author_filter_{group}", "value"), 78 | dash.State(f"year_filter_{group}", "value"), 79 | )(cb.create_update_piechart_material_function(group)) 80 | 81 | app.callback( 82 | dash.Output(f"graph_isotopes_{group}", "figure"), 83 | dash.Input(f"graph_{group}", "figure"), 84 | dash.State(f"material_filter_{group}", "value"), 85 | dash.State(f"isotope_filter_{group}", "value"), 86 | dash.State(f"author_filter_{group}", "value"), 87 | dash.State(f"year_filter_{group}", "value"), 88 | )(cb.create_update_piechart_isotopes_function(group)) 89 | 90 | app.callback( 91 | dash.Output(f"graph_authors_{group}", "figure"), 92 | dash.Input(f"graph_{group}", "figure"), 93 | dash.State(f"material_filter_{group}", "value"), 94 | dash.State(f"isotope_filter_{group}", "value"), 95 | dash.State(f"author_filter_{group}", "value"), 96 | dash.State(f"year_filter_{group}", "value"), 97 | )(cb.create_update_piechart_authors_function(group)) 98 | 99 | app.callback( 100 | dash.Output(f"download-text_{group}", "data"), 101 | dash.Input(f"extract_button_{group}", "n_clicks"), 102 | dash.Input(f"material_filter_{group}", "value"), 103 | dash.Input(f"isotope_filter_{group}", "value"), 104 | dash.Input(f"author_filter_{group}", "value"), 105 | dash.Input(f"year_filter_{group}", "value"), 106 | prevent_initial_call=True, 107 | )(cb.create_make_download_data_function(group)) 108 | 109 | app.callback( 110 | dash.Output(f"download-python_{group}", "data"), 111 | dash.Input(f"python_button_{group}", "n_clicks"), 112 | dash.Input(f"material_filter_{group}", "value"), 113 | dash.Input(f"isotope_filter_{group}", "value"), 114 | dash.Input(f"author_filter_{group}", "value"), 115 | dash.Input(f"year_filter_{group}", "value"), 116 | prevent_initial_call=True, 117 | )(cb.make_download_python_callback(group)) 118 | 119 | app.callback( 120 | dash.Output(f"modal_add_{group}", "is_open"), 121 | dash.Input(f"add_property_{group}", "n_clicks"), 122 | dash.Input(f"submit_new_{group}", "n_clicks"), 123 | dash.State(f"modal_add_{group}", "is_open"), 124 | dash.State(f"new_{group}_pre_exp", "value"), 125 | dash.State(f"new_{group}_act_energy", "value"), 126 | dash.State(f"new_{group}_author", "value"), 127 | dash.State(f"new_{group}_year", "value"), 128 | dash.State(f"new_{group}_isotope", "value"), 129 | dash.State(f"new_{group}_material", "value"), 130 | prevent_initial_call=True, 131 | )(cb.make_toggle_modal_function(group)) 132 | 133 | # add property form 134 | # since an additional parameter is needed for solubility and permeability 135 | # we need to check the group 136 | if group in ["solubility", "permeability"]: 137 | app.callback( 138 | dash.Output(f"material_filter_{group}", "options"), 139 | dash.Output(f"author_filter_{group}", "options"), 140 | dash.Output(f"error_message_new_{group}", "children"), 141 | dash.Input(f"submit_new_{group}", "n_clicks"), 142 | dash.Input(f"material_filter_{group}", "value"), 143 | dash.State(f"new_{group}_pre_exp", "value"), 144 | dash.State(f"new_{group}_act_energy", "value"), 145 | dash.State(f"new_{group}_author", "value"), 146 | dash.State(f"new_{group}_year", "value"), 147 | dash.State(f"new_{group}_isotope", "value"), 148 | dash.State(f"new_{group}_material", "value"), 149 | dash.State(f"new_{group}_range_low", "value"), 150 | dash.State(f"new_{group}_range_high", "value"), 151 | dash.State(f"new_{group}_law", "value"), 152 | prevent_initial_call=True, 153 | )(cb.make_add_property(group)) 154 | else: 155 | app.callback( 156 | dash.Output(f"material_filter_{group}", "options"), 157 | dash.Output(f"author_filter_{group}", "options"), 158 | dash.Output(f"error_message_new_{group}", "children"), 159 | dash.Input(f"submit_new_{group}", "n_clicks"), 160 | dash.Input(f"material_filter_{group}", "value"), 161 | dash.State(f"new_{group}_pre_exp", "value"), 162 | dash.State(f"new_{group}_act_energy", "value"), 163 | dash.State(f"new_{group}_author", "value"), 164 | dash.State(f"new_{group}_year", "value"), 165 | dash.State(f"new_{group}_isotope", "value"), 166 | dash.State(f"new_{group}_material", "value"), 167 | dash.State(f"new_{group}_range_low", "value"), 168 | dash.State(f"new_{group}_range_high", "value"), 169 | prevent_initial_call=True, 170 | )(cb.make_add_property(group)) 171 | 172 | app.callback( 173 | dash.Output(f"table_{group}", "data"), 174 | dash.Input(f"graph_{group}", "figure"), 175 | dash.State(f"material_filter_{group}", "value"), 176 | dash.State(f"isotope_filter_{group}", "value"), 177 | dash.State(f"author_filter_{group}", "value"), 178 | dash.State(f"year_filter_{group}", "value"), 179 | )(cb.create_update_table_data_function(group)) 180 | 181 | if __name__ == "__main__": 182 | # app.run_server(debug=True, host="0.0.0.0", port=8080) 183 | app.run_server(debug=True) 184 | -------------------------------------------------------------------------------- /htm_dashboard/graph.py: -------------------------------------------------------------------------------- 1 | import plotly.graph_objects as go 2 | import plotly.io as pio 3 | import h_transport_materials as htm 4 | import numpy as np 5 | import plotly.express as px 6 | import json 7 | from datetime import datetime 8 | 9 | 10 | TEMPLATE_LIGHT = "plotly_white" 11 | TEMPLATE_DARK = "cyborg" 12 | 13 | pio.templates.default = TEMPLATE_LIGHT 14 | 15 | 16 | colour_cycle = px.colors.qualitative.Plotly 17 | 18 | type_to_database = { 19 | "diffusivity": htm.diffusivities, 20 | "solubility": htm.solubilities, 21 | "permeability": htm.permeabilities, 22 | "recombination_coeff": htm.recombination_coeffs, 23 | "dissociation_coeff": htm.dissociation_coeffs, 24 | } 25 | 26 | 27 | def add_mean_value(group: htm.PropertiesGroup, fig: go.Figure): 28 | mean_prop = group.mean() 29 | label = "Mean value" 30 | T = np.linspace(300, 1200, num=500) * htm.ureg.K 31 | hovertemplate = ( 32 | "%{text}

" 33 | + "1/T: %{x:,.2e} K-1
" 34 | + "T: %{customdata:.0f} K
" 35 | ) 36 | if isinstance(group[0], htm.Solubility): 37 | hovertemplate += ( 38 | "S: %{y:,.2e}" 39 | + f"{mean_prop.units:~H}
" 40 | + f"S_0: {mean_prop.pre_exp:.2e~H}
" 41 | + f"E_S : {mean_prop.act_energy:.2f~H}" 42 | ) 43 | elif isinstance(group[0], htm.Diffusivity): 44 | hovertemplate += ( 45 | "D: %{y:,.2e} " 46 | + f"{mean_prop.units:~H}
" 47 | + f"D_0: {mean_prop.pre_exp:.2e~H}
" 48 | + f"E_D : {mean_prop.act_energy:.2f~H}" 49 | ) 50 | elif isinstance(group[0], htm.RecombinationCoeff): 51 | hovertemplate += ( 52 | "Kr: %{y:,.2e}" 53 | + f"{mean_prop.units:~H}
" 54 | + f"Kr_0: {mean_prop.pre_exp:.2e~H}
" 55 | + f"E_Kr : {mean_prop.act_energy:.2f~H}" 56 | ) 57 | elif isinstance(group[0], htm.DissociationCoeff): 58 | hovertemplate += ( 59 | "Kd: %{y:,.2e}" 60 | + f"{mean_prop.units:~H}
" 61 | + f"Kd_0: {mean_prop.pre_exp:.2e~H}
" 62 | + f"E_Kd : {mean_prop.act_energy:.2f~H}" 63 | ) 64 | hovertemplate += "" 65 | fig.add_trace( 66 | go.Scatter( 67 | x=1 / T.magnitude, 68 | y=mean_prop.value(T).magnitude, 69 | name=label, 70 | mode="lines", 71 | text=[label] * len(T), 72 | line=dict(color="black", width=4), 73 | customdata=T, 74 | hovertemplate=hovertemplate, 75 | ) 76 | ) 77 | 78 | 79 | def make_group_of_properties( 80 | type_of_prop: str, materials=[], authors=[], isotopes=[], years=None 81 | ): 82 | 83 | if len(materials) * len(authors) * len(isotopes) == 0: 84 | filtered_group = [] 85 | else: 86 | database = type_to_database[type_of_prop] 87 | filtered_group = ( 88 | database.filter(material=materials) 89 | .filter(author=[author.lower() for author in authors]) 90 | .filter(isotope=[isotope.lower() for isotope in isotopes]) 91 | ) 92 | if years: 93 | filtered_group = filtered_group.filter( 94 | year=np.arange(years[0], years[1] + 1, step=1).tolist() 95 | ) 96 | 97 | return filtered_group 98 | 99 | 100 | def update_axes(fig, group_of_properties): 101 | if len(group_of_properties) == 0: 102 | return 103 | 104 | if isinstance(group_of_properties[0], htm.Solubility): 105 | all_units = np.unique([f"{S.units:~H}" for S in group_of_properties]).tolist() 106 | if len(all_units) == 1: 107 | yticks_suffix = all_units[0].replace("particle", " H") 108 | title_units = f"({yticks_suffix})" 109 | else: 110 | # if the group contains mixed units, display nothing 111 | title_units = "(mixed units)" 112 | yticks_suffix = "" 113 | ylabel = f"Solubility {title_units}" 114 | elif isinstance(group_of_properties[0], htm.Diffusivity): 115 | ylabel = "Diffusivity" 116 | yticks_suffix = f" {group_of_properties[0].units:~H}" 117 | elif isinstance(group_of_properties[0], htm.Permeability): 118 | ylabel = f"Permeability {group_of_properties[0].units:~H}" 119 | yticks_suffix = "" 120 | elif isinstance(group_of_properties[0], htm.RecombinationCoeff): 121 | ylabel = "Recombination coefficient" 122 | yticks_suffix = " m4/s" 123 | elif isinstance(group_of_properties[0], htm.DissociationCoeff): 124 | ylabel = f"Dissociation coefficient {group_of_properties[0].units:~H}" 125 | yticks_suffix = "" 126 | 127 | xticks_suffix = " K-1" 128 | 129 | fig.update_yaxes( 130 | title_text=ylabel, type="log", tickformat=".0e", ticksuffix=yticks_suffix 131 | ) 132 | fig.update_xaxes(title_text="1/T", tickformat=".2e", ticksuffix=xticks_suffix) 133 | 134 | 135 | def make_hovertemplate(prop): 136 | # TODO refactor this 137 | if isinstance(prop, htm.Solubility): 138 | return ( 139 | "%{text}

" 140 | + prop.material.name 141 | + "
" 142 | + "1/T: %{x:,.2e} K-1
" 143 | + "T: %{customdata:.0f} K
" 144 | + "S: %{y:,.2e} " 145 | + f"{prop.units:~H}
" 146 | + f"S_0: {prop.pre_exp:.2e~H}
" 147 | + f"E_S : {prop.act_energy:.2f~H}" 148 | + "" 149 | ) 150 | elif isinstance(prop, htm.Diffusivity): 151 | return ( 152 | "%{text}

" 153 | + prop.material.name 154 | + "
" 155 | + "1/T: %{x:,.2e} K-1
" 156 | + "T: %{customdata:.0f} K
" 157 | + "D: %{y:,.2e} " 158 | + f"{prop.units:~H}
" 159 | + f"D_0: {prop.pre_exp:.2e~H}
" 160 | + f"E_D : {prop.act_energy:.2f~H}" 161 | + "" 162 | ) 163 | else: 164 | return ( 165 | "%{text}

" 166 | + prop.material.name 167 | + "
" 168 | + "1/T: %{x:,.2e} K-1
" 169 | + "T: %{customdata:.0f} K
" 170 | + "value: %{y:,.2e} " 171 | + f"{prop.units:~H}
" 172 | + f"pre-exp: {prop.pre_exp:.2e~H}
" 173 | + f"act. energy : {prop.act_energy:.2f~H}" 174 | + "" 175 | ) 176 | 177 | 178 | def make_figure_prop_per_year( 179 | group, step, selected_years=[1950, int(datetime.today().year)] 180 | ): 181 | 182 | counts, bins = np.histogram([prop.year for prop in group]) 183 | 184 | bins_center = 0.5 * (bins[:-1] + bins[1:]) 185 | selected = [ 186 | i 187 | for i, year in enumerate(bins_center) 188 | if selected_years[0] <= year <= selected_years[1] 189 | ] 190 | 191 | fig = go.Figure( 192 | [ 193 | go.Bar( 194 | x=bins_center, 195 | y=counts, 196 | selectedpoints=selected, 197 | ) 198 | ] 199 | ) 200 | template = [] 201 | for i, year in enumerate(bins[:-1]): 202 | year_1 = year 203 | year_2 = bins[i + 1] 204 | template.append( 205 | f"
{year_1:.0f} - {year_2:.0f}
" + "" + "%{y}" 206 | ) 207 | 208 | fig.update_layout(bargap=0) 209 | fig.update_traces( 210 | hovertemplate=template, 211 | selector=dict(type="bar"), 212 | ) 213 | fig.update_yaxes(title_text="Nb of properties") 214 | return fig 215 | 216 | 217 | def make_citations_graph(group: htm.PropertiesGroup, per_year: bool = True): 218 | references = [] 219 | nb_citations = [] 220 | dois = [] 221 | with open("htm_dashboard/citations.json") as f: 222 | citation_data = json.load(f) 223 | for prop in group: 224 | if prop.doi in citation_data["dois"]: 225 | nb_citations_prop = citation_data["dois"][prop.doi] 226 | else: 227 | nb_citations_prop = prop.nb_citations 228 | author = prop.author 229 | year = prop.year 230 | 231 | label = "{} ({})".format(author.capitalize(), year) 232 | 233 | if label not in references: 234 | 235 | references.append(label) 236 | if per_year: 237 | current_year = datetime.now().year 238 | nb_citations.append(nb_citations_prop / (current_year - year + 1)) 239 | else: 240 | nb_citations.append(nb_citations_prop) 241 | 242 | if prop.doi is None: 243 | dois.append("none") 244 | else: 245 | dois.append(prop.doi) 246 | 247 | # sort values 248 | references = [val_y for _, val_y in sorted(zip(nb_citations, references))] 249 | dois = [val_y for _, val_y in sorted(zip(nb_citations, dois))] 250 | nb_citations = sorted(nb_citations) 251 | 252 | bar = go.Bar( 253 | x=nb_citations, 254 | y=references, 255 | orientation="h", 256 | customdata=dois, 257 | hovertemplate="DOI " + ": %{customdata}
" + "", 258 | ) 259 | fig = go.Figure(bar) 260 | if per_year: 261 | x_label = "Average number of citations per year" 262 | else: 263 | x_label = "Number of citations" 264 | fig.update_xaxes(title=x_label) 265 | return fig 266 | 267 | 268 | def make_piechart_materials(prop_group): 269 | list_of_mats = [prop.material.name for prop in prop_group] 270 | labels = np.unique(list_of_mats).tolist() 271 | 272 | values = [list_of_mats.count(mat) for mat in labels] 273 | 274 | colours = [] 275 | prop_to_color = htm.plotting.get_prop_to_color( 276 | prop_group, colour_by="material", colour_cycle=colour_cycle 277 | ) 278 | for mat in labels: 279 | for prop in prop_group: 280 | if prop.material == mat: 281 | colours.append(prop_to_color[prop]) 282 | break 283 | assert len(colours) == len(labels) 284 | 285 | fig = go.Figure( 286 | data=[ 287 | go.Pie( 288 | labels=labels, 289 | values=values, 290 | marker_colors=colours, 291 | ) 292 | ] 293 | ) 294 | return fig 295 | 296 | 297 | def make_piechart_isotopes(prop_group): 298 | list_of_isotopes = [prop.isotope for prop in prop_group] 299 | labels = ["H", "D", "T"] 300 | 301 | values = [list_of_isotopes.count(isotope) for isotope in labels] 302 | 303 | fig = go.Figure(data=[go.Pie(labels=labels, values=values)]) 304 | return fig 305 | 306 | 307 | def make_piechart_author(prop_group): 308 | list_of_authors = [prop.author for prop in prop_group] 309 | labels = np.unique(list_of_authors).tolist() 310 | 311 | colours = [] 312 | prop_to_color = htm.plotting.get_prop_to_color( 313 | prop_group, colour_by="author", colour_cycle=colour_cycle 314 | ) 315 | for author in labels: 316 | for prop in prop_group: 317 | if prop.author == author: 318 | colours.append(prop_to_color[prop]) 319 | break 320 | assert len(colours) == len(labels) 321 | 322 | values = [list_of_authors.count(isotope) for isotope in labels] 323 | 324 | labels = [lab.capitalize() for lab in labels] 325 | 326 | fig = go.Figure( 327 | data=[ 328 | go.Pie( 329 | labels=labels, 330 | values=values, 331 | marker_colors=colours, 332 | ) 333 | ] 334 | ) 335 | return fig 336 | -------------------------------------------------------------------------------- /htm_dashboard/callbacks.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import json 3 | import dash 4 | import plotly.io as pio 5 | 6 | from .export import generate_python_code 7 | 8 | from .tab import materials_options, TABLE_KEYS 9 | 10 | from .graph import ( 11 | make_group_of_properties, 12 | make_piechart_author, 13 | make_piechart_isotopes, 14 | make_piechart_materials, 15 | add_mean_value, 16 | make_figure_prop_per_year, 17 | make_citations_graph, 18 | TEMPLATE_DARK, 19 | TEMPLATE_LIGHT, 20 | ) 21 | 22 | import h_transport_materials as htm 23 | 24 | 25 | type_to_database = { 26 | "diffusivity": htm.diffusivities, 27 | "solubility": htm.solubilities, 28 | "permeability": htm.permeabilities, 29 | "recombination_coeff": htm.recombination_coeffs, 30 | "dissociation_coeff": htm.dissociation_coeffs, 31 | } 32 | 33 | 34 | def create_make_citations_figure_function(group): 35 | def make_citations_figure( 36 | figure, 37 | per_year, 38 | material_filter, 39 | isotope_filter, 40 | author_filter, 41 | year_filter, 42 | ): 43 | properties_group = make_group_of_properties( 44 | type_of_prop=group, 45 | materials=material_filter, 46 | authors=author_filter, 47 | isotopes=isotope_filter, 48 | years=year_filter, 49 | ) 50 | 51 | return make_citations_graph(properties_group, per_year=per_year) 52 | 53 | return make_citations_figure 54 | 55 | 56 | def create_add_all_materials_function(group): 57 | def add_all_materials(n_clicks): 58 | if n_clicks: 59 | return materials_options 60 | else: 61 | return dash.no_update 62 | 63 | return add_all_materials 64 | 65 | 66 | def create_add_all_authors_function(group): 67 | def add_all_authors(n_clicks): 68 | if n_clicks: 69 | return np.unique( 70 | [prop.author.capitalize() for prop in type_to_database[group]] 71 | ).tolist() 72 | else: 73 | return dash.no_update 74 | 75 | return add_all_authors 76 | 77 | 78 | def create_update_entries_per_year_graph_function(group): 79 | def update_entries_per_year_graph( 80 | figure, material_filter, isotope_filter, author_filter, year_filter 81 | ): 82 | all_time_properties = make_group_of_properties( 83 | type_of_prop=group, 84 | materials=material_filter, 85 | authors=author_filter, 86 | isotopes=isotope_filter, 87 | ) 88 | return make_figure_prop_per_year( 89 | all_time_properties, step=5, selected_years=year_filter 90 | ) 91 | 92 | return update_entries_per_year_graph 93 | 94 | 95 | def create_update_graph_function(group): 96 | def update_graph( 97 | material_filter, 98 | isotope_filter, 99 | author_filter, 100 | year_filter, 101 | mean_button, 102 | colour_by, 103 | toggle_light, 104 | ): 105 | properties_group = make_group_of_properties( 106 | type_of_prop=group, 107 | materials=material_filter, 108 | authors=author_filter, 109 | isotopes=isotope_filter, 110 | years=year_filter, 111 | ) 112 | 113 | if toggle_light: 114 | pio.templates.default = TEMPLATE_LIGHT 115 | else: 116 | pio.templates.default = TEMPLATE_DARK 117 | 118 | figure = htm.plotting.plot_plotly(properties_group, colour_by=colour_by) 119 | changed_id = [p["prop_id"] for p in dash.callback_context.triggered][0] 120 | if changed_id == f"mean_button_{group}.n_clicks": 121 | add_mean_value(properties_group, figure) 122 | 123 | return figure 124 | 125 | return update_graph 126 | 127 | 128 | def create_make_download_data_function(group): 129 | def make_download_data( 130 | n_clicks, 131 | material_filter, 132 | isotope_filter, 133 | author_filter, 134 | year_filter, 135 | ): 136 | changed_id = [p["prop_id"] for p in dash.callback_context.triggered][0] 137 | if changed_id == f"extract_button_{group}.n_clicks": 138 | properties_group = make_group_of_properties( 139 | type_of_prop=group, 140 | materials=material_filter, 141 | authors=author_filter, 142 | isotopes=isotope_filter, 143 | years=year_filter, 144 | ) 145 | data = {"data": []} 146 | for prop in properties_group: 147 | data["data"].append(prop.to_json()) 148 | 149 | data["htm_version"] = htm.__version__ 150 | return dict( 151 | content=json.dumps(data, indent=2), 152 | filename="data.json", 153 | ) 154 | 155 | return make_download_data 156 | 157 | 158 | def make_download_python_callback(group): 159 | def download_python( 160 | n_clicks, 161 | material_filter, 162 | isotope_filter, 163 | author_filter, 164 | year_filter, 165 | ): 166 | changed_id = [p["prop_id"] for p in dash.callback_context.triggered][0] 167 | if changed_id == f"python_button_{group}.n_clicks": 168 | return dict( 169 | content=generate_python_code( 170 | materials=material_filter, 171 | isotopes=isotope_filter, 172 | authors=author_filter, 173 | yearmin=year_filter[0], 174 | yearmax=year_filter[1], 175 | group=group, 176 | ), 177 | filename="script.py", 178 | ) 179 | 180 | return download_python 181 | 182 | 183 | def make_toggle_modal_function(group): 184 | def toggle_modal( 185 | n1, 186 | n2, 187 | is_open, 188 | new_prop_pre_exp, 189 | new_prop_act_energy, 190 | new_prop_author, 191 | new_prop_year, 192 | new_prop_isotope, 193 | new_prop_material, 194 | ): 195 | if is_open and None in [ 196 | new_prop_pre_exp, 197 | new_prop_act_energy, 198 | new_prop_author, 199 | new_prop_year, 200 | new_prop_isotope, 201 | new_prop_material, 202 | ]: 203 | return is_open 204 | if n1 or n2: 205 | return not is_open 206 | return is_open 207 | 208 | return toggle_modal 209 | 210 | 211 | def make_add_property(group): 212 | def add_property( 213 | n_clicks, 214 | material_filter, 215 | new_pre_exp, 216 | new_act_energy, 217 | new_author, 218 | new_year, 219 | new_isotope, 220 | new_material, 221 | new_range_low, 222 | new_range_high, 223 | new_sol_law=None, 224 | ): 225 | changed_id = [p["prop_id"] for p in dash.callback_context.triggered][0] 226 | if changed_id == f"submit_new_{group}.n_clicks": 227 | if None in [ 228 | new_pre_exp, 229 | new_act_energy, 230 | new_author, 231 | new_year, 232 | new_isotope, 233 | new_material, 234 | ]: 235 | return dash.no_update, dash.no_update, "Error!" 236 | if (new_range_low, new_range_high) == (None, None): 237 | (new_range_low, new_range_high) = (300, 1200) 238 | 239 | if group == "diffusivity": 240 | new_property = htm.Diffusivity( 241 | D_0=new_pre_exp, 242 | E_D=new_act_energy, 243 | ) 244 | elif group == "solubility": 245 | new_property = htm.Solubility( 246 | S_0=new_pre_exp, 247 | E_S=new_act_energy, 248 | law=new_sol_law, 249 | ) 250 | elif group == "recombination_coeff": 251 | new_property = htm.RecombinationCoeff( 252 | pre_exp=new_pre_exp, 253 | act_energy=new_act_energy, 254 | ) 255 | 256 | new_property.author = new_author.lower() 257 | new_property.year = new_year 258 | new_property.isotope = new_isotope 259 | # TODO find a way to find potentially already existing material (like tungsten) 260 | new_property.material = htm.Material(name=new_material) 261 | new_property.range = (new_range_low, new_range_high) 262 | 263 | type_to_database[group].append(new_property) 264 | 265 | all_authors = np.unique( 266 | [ 267 | prop.author.capitalize() 268 | for prop in type_to_database[group] 269 | if prop.material in material_filter 270 | ] 271 | ).tolist() 272 | all_materials = [prop.material.name.lower() for prop in type_to_database[group]] 273 | all_families = [ 274 | p.family for prop in type_to_database[group] for p in prop.material.parents 275 | ] 276 | all_materials = np.unique(all_materials + all_families).tolist() 277 | 278 | return all_materials, all_authors, "" 279 | 280 | return add_property 281 | 282 | 283 | def create_update_piechart_material_function(group): 284 | def update_piechart_material( 285 | figure, 286 | material_filter, 287 | isotope_filter, 288 | author_filter, 289 | year_filter, 290 | ): 291 | properties_group = make_group_of_properties( 292 | type_of_prop=group, 293 | materials=material_filter, 294 | authors=author_filter, 295 | isotopes=isotope_filter, 296 | years=year_filter, 297 | ) 298 | return make_piechart_materials(properties_group) 299 | 300 | return update_piechart_material 301 | 302 | 303 | def create_update_piechart_isotopes_function(group): 304 | def update_piechart_isotope( 305 | figure, 306 | material_filter, 307 | isotope_filter, 308 | author_filter, 309 | year_filter, 310 | ): 311 | properties_group = make_group_of_properties( 312 | type_of_prop=group, 313 | materials=material_filter, 314 | authors=author_filter, 315 | isotopes=isotope_filter, 316 | years=year_filter, 317 | ) 318 | return make_piechart_isotopes(properties_group) 319 | 320 | return update_piechart_isotope 321 | 322 | 323 | def create_update_piechart_authors_function(group): 324 | def update_piechart_author( 325 | figure, 326 | material_filter, 327 | isotope_filter, 328 | author_filter, 329 | year_filter, 330 | ): 331 | properties_group = make_group_of_properties( 332 | type_of_prop=group, 333 | materials=material_filter, 334 | authors=author_filter, 335 | isotopes=isotope_filter, 336 | years=year_filter, 337 | ) 338 | return make_piechart_author(properties_group) 339 | 340 | return update_piechart_author 341 | 342 | 343 | def create_update_table_data_function(group): 344 | def update_table_data( 345 | figure, material_filter, isotope_filter, author_filter, year_filter 346 | ): 347 | data = [] 348 | 349 | properties_group = make_group_of_properties( 350 | type_of_prop=group, 351 | materials=material_filter, 352 | authors=author_filter, 353 | isotopes=isotope_filter, 354 | years=year_filter, 355 | ) 356 | 357 | for prop in properties_group: 358 | entry = {} 359 | for key in TABLE_KEYS: 360 | if hasattr(prop, key): 361 | val = getattr(prop, key) 362 | if key == "range": 363 | if val is None: 364 | val = "none" 365 | else: 366 | val = f"{val[0]:.0f~P}-{val[1]:.0f~P}" 367 | elif key == "material": 368 | val = f"{val.name}" 369 | elif key == "pre_exp": 370 | val = f"{val: .2e~P}" 371 | elif key == "act_energy": 372 | val = f"{val:.2f~P}" 373 | elif key == "doi": 374 | entry[key] = prop.source 375 | if prop.bibsource: 376 | if prop.doi: 377 | clickable_doi = ( 378 | f"[{prop.doi}](https://doi.org/{prop.doi})" 379 | ) 380 | val = clickable_doi 381 | 382 | entry[key] = val 383 | 384 | data.append(entry) 385 | 386 | return data 387 | 388 | return update_table_data 389 | -------------------------------------------------------------------------------- /htm_dashboard/tab.py: -------------------------------------------------------------------------------- 1 | from dash import dcc, dash_table 2 | from dash import html 3 | import dash_bootstrap_components as dbc 4 | import dash_daq as daq 5 | 6 | import h_transport_materials as htm 7 | import numpy as np 8 | import json 9 | 10 | materials_options = list(set([prop.material.name for prop in htm.database])) 11 | materials_options += list(set([prop.material.family for prop in htm.database])) 12 | 13 | isotope_options = ["H", "D", "T"] 14 | 15 | pretty_label = { 16 | "diffusivity": "Diffusivity", 17 | "solubility": "Solubility", 18 | "permeability": "Permeability", 19 | "recombination_coeff": "Recombination coeff.", 20 | "dissociation_coeff": "Dissociation coeff.", 21 | } 22 | 23 | 24 | def make_tab(property: str): 25 | """_summary_ 26 | 27 | Args: 28 | property (str): _description_ 29 | 30 | Returns: 31 | dbc.Tab: the tab 32 | """ 33 | 34 | assert property in [ 35 | "diffusivity", 36 | "solubility", 37 | "permeability", 38 | "recombination_coeff", 39 | "dissociation_coeff", 40 | ] 41 | 42 | property_to_group = { 43 | "diffusivity": htm.diffusivities, 44 | "solubility": htm.solubilities, 45 | "permeability": htm.permeabilities, 46 | "recombination_coeff": htm.recombination_coeffs, 47 | "dissociation_coeff": htm.dissociation_coeffs, 48 | } 49 | 50 | all_properties = property_to_group[property] 51 | 52 | initial_material = "tungsten" 53 | 54 | authors_options = np.unique( 55 | [ 56 | prop.author.capitalize() 57 | for prop in all_properties 58 | if prop.material == "tungsten" 59 | ] 60 | ).tolist() 61 | 62 | years_options = [prop.year for prop in all_properties] 63 | min_year = min(years_options) 64 | max_year = max(years_options) 65 | 66 | table = make_table(property) 67 | 68 | table_tab = dbc.Tab([table], label="Table") 69 | 70 | graph_tab = dbc.Tab( 71 | [ 72 | dbc.Card( 73 | [ 74 | dbc.Row( 75 | [ 76 | dbc.Col( 77 | [ 78 | html.Label("Colour by:"), 79 | dcc.Dropdown( 80 | ["property", "material", "author", "isotope"], 81 | "property", 82 | id=f"colour-by_{property}", 83 | style=dict(width="150px"), 84 | ), 85 | ] 86 | ), 87 | dbc.Col( 88 | [ 89 | dcc.Graph( 90 | id=f"graph_{property}", 91 | style={"height": "600px"}, 92 | ) 93 | ], 94 | width=10, 95 | ), 96 | ], 97 | ), 98 | ], 99 | body=True, 100 | className="mb-2", 101 | ) 102 | ], 103 | label="Graph", 104 | ) 105 | 106 | sub_tabs = dbc.Tabs([graph_tab, table_tab], id=f"subtabs_{property}") 107 | 108 | controls = dbc.Card( 109 | [ 110 | html.Label("Filter by material:"), 111 | dcc.Dropdown( 112 | options=materials_options, 113 | value=[initial_material], 114 | multi=True, 115 | id=f"material_filter_{property}", 116 | ), 117 | html.Div( 118 | dbc.Button( 119 | "All", 120 | id=f"add_all_materials_{property}", 121 | style={"font-size": "12px"}, 122 | ) 123 | ), 124 | html.Br(), 125 | dbc.Label("Filter by isotope:"), 126 | dbc.Checklist( 127 | value=isotope_options, 128 | options=[{"label": i, "value": i} for i in isotope_options], 129 | inline=True, 130 | id=f"isotope_filter_{property}", 131 | ), 132 | html.Br(), 133 | html.Label("Filter by author:"), 134 | dcc.Dropdown( 135 | value=authors_options, 136 | options=authors_options, 137 | multi=True, 138 | id=f"author_filter_{property}", 139 | ), 140 | html.Div( 141 | dbc.Button( 142 | "All", 143 | id=f"add_all_authors_{property}", 144 | style={"font-size": "12px"}, 145 | ) 146 | ), 147 | html.Br(), 148 | html.Label("Filter by year:"), 149 | dcc.RangeSlider( 150 | id=f"year_filter_{property}", 151 | min=min_year, 152 | max=max_year, 153 | step=1, 154 | value=[min_year, max_year], 155 | marks={ 156 | int(i): str(i) 157 | for i in np.arange(min_year, max_year) 158 | if int(i) % 10 == 0 159 | }, 160 | tooltip={ 161 | "placement": "bottom", 162 | "always_visible": True, 163 | }, 164 | ), 165 | html.Br(), 166 | html.Div( 167 | [ 168 | dbc.Button( 169 | "Compute mean curve", 170 | id=f"mean_button_{property}", 171 | color="primary", 172 | style={"margin": "5px"}, 173 | n_clicks="0", 174 | ), 175 | dbc.Button( 176 | "Add property", 177 | id=f"add_property_{property}", 178 | color="primary", 179 | style={"margin": "5px"}, 180 | n_clicks="0", 181 | ), 182 | dbc.Button( 183 | [ 184 | "Extract data", 185 | dcc.Download(id=f"download-text_{property}"), 186 | ], 187 | id=f"extract_button_{property}", 188 | color="primary", 189 | style={"margin": "5px"}, 190 | n_clicks="0", 191 | ), 192 | dbc.Button( 193 | [ 194 | "Python", 195 | dcc.Download(id=f"download-python_{property}"), 196 | ], 197 | id=f"python_button_{property}", 198 | color="primary", 199 | style={"margin": "5px"}, 200 | n_clicks_timestamp="0", 201 | ), 202 | ] 203 | ), 204 | ], 205 | body=True, 206 | ) 207 | with open("htm_dashboard/citations.json") as f: 208 | citation_data = json.load(f) 209 | date_citations = citation_data["date"] 210 | graph_prop_per_year = dbc.Card( 211 | [ 212 | dbc.CardBody( 213 | [ 214 | html.H4("Number of properties per year", className="card-title"), 215 | dcc.Graph(id=f"graph_prop_per_year_{property}"), 216 | ] 217 | ) 218 | ], 219 | className="mb-2", 220 | ) 221 | 222 | graph_citations = dbc.Card( 223 | [ 224 | dbc.CardBody( 225 | [ 226 | html.H4("Number of citations", className="card-title"), 227 | html.H6( 228 | f"source: Crossref {date_citations}", className="card-subtitle" 229 | ), 230 | dbc.Row( 231 | [ 232 | dbc.Col( 233 | [ 234 | daq.BooleanSwitch( 235 | label="Per year", 236 | on=False, 237 | id=f"per_year_citations_{property}", 238 | ), 239 | ], 240 | width=1, 241 | ), 242 | dbc.Col( 243 | [dcc.Graph(id=f"graph_nb_citations_{property}")], 244 | width=11, 245 | ), 246 | ], 247 | align="center", 248 | ), 249 | ] 250 | ) 251 | ], 252 | className="mb-2", 253 | ) 254 | 255 | piechart_materials = dbc.Card( 256 | [ 257 | dbc.CardBody( 258 | [ 259 | html.H4("Repartition by materials", className="card-title"), 260 | dcc.Graph(id=f"graph_materials_{property}"), 261 | ] 262 | ) 263 | ], 264 | className="mb-2", 265 | ) 266 | piechart_isotopes = dbc.Card( 267 | [ 268 | dbc.CardBody( 269 | [ 270 | html.H4("Repartition by isotopes", className="card-title"), 271 | dcc.Graph(id=f"graph_isotopes_{property}"), 272 | ] 273 | ) 274 | ], 275 | className="mb-2", 276 | ) 277 | piechart_authors = dbc.Card( 278 | [ 279 | dbc.CardBody( 280 | [ 281 | html.H4("Repartition by authors", className="card-title"), 282 | dcc.Graph(id=f"graph_authors_{property}"), 283 | ] 284 | ) 285 | ], 286 | className="mb-2", 287 | ) 288 | 289 | tab = dbc.Tab( 290 | label=pretty_label[property], 291 | children=[ 292 | dbc.Row( 293 | [ 294 | dbc.Col( 295 | [controls], 296 | width=3, 297 | style={"overflow-y": "auto", "maxHeight": "600px"}, 298 | ), 299 | dbc.Col([sub_tabs]), 300 | ], 301 | ), 302 | dbc.Row( 303 | [ 304 | dbc.Col([graph_prop_per_year], width=3), 305 | dbc.Col([graph_citations], width=4), 306 | dbc.Col([piechart_materials], width=4), 307 | ], 308 | justify="evenly", 309 | ), 310 | dbc.Row( 311 | [ 312 | dbc.Col([piechart_isotopes], width=4), 313 | dbc.Col([piechart_authors], width=4), 314 | ], 315 | justify="evenly", 316 | ), 317 | ], 318 | ) 319 | return tab 320 | 321 | 322 | TABLE_KEYS = [ 323 | "material", 324 | "isotope", 325 | "pre_exp", 326 | "act_energy", 327 | "range", 328 | "author", 329 | "note", 330 | "doi", 331 | ] 332 | 333 | prop_key_to_label = { 334 | "diffusivity": {"pre_exp": "D_0", "act_energy": "E_D"}, 335 | "solubility": {"pre_exp": "S_0", "act_energy": "E_S"}, 336 | "permeability": {"pre_exp": "P_0", "act_energy": "E_P"}, 337 | "recombination_coeff": {"pre_exp": "Kr_0", "act_energy": "E_Kr"}, 338 | "dissociation_coeff": {"pre_exp": "Kd_0", "act_energy": "E_Kd"}, 339 | } 340 | 341 | key_to_label = { 342 | "material": "Material", 343 | "range": "Range", 344 | "author": "Author", 345 | "doi": "DOI", 346 | "note": "Notes", 347 | "isotope": "Isotope", 348 | } 349 | 350 | 351 | def make_table_labels(property): 352 | labels = [] 353 | for key in TABLE_KEYS: 354 | if key in key_to_label: 355 | labels.append(key_to_label[key]) 356 | else: 357 | labels.append(prop_key_to_label[property][key]) 358 | return labels 359 | 360 | 361 | def make_table(property): 362 | 363 | table = dash_table.DataTable( 364 | id=f"table_{property}", 365 | columns=[ 366 | ( 367 | { 368 | "name": label, 369 | "id": key, 370 | "presentation": "markdown", 371 | } # markdown is needed to have clickable links 372 | if key == "doi" 373 | else {"name": label, "id": key} 374 | ) 375 | for key, label in zip(TABLE_KEYS, make_table_labels(property)) 376 | ], 377 | data=[], 378 | page_size=10, 379 | editable=False, 380 | cell_selectable=True, 381 | # filter_action="native", 382 | sort_action="native", 383 | style_table={"overflowX": "auto"}, 384 | style_cell_conditional=[ 385 | # Here, "whiteSpace": 'normal' is needed to have line breaks in the notes 386 | {"if": {"column_id": "note"}, "width": "200px", "whiteSpace": "normal"}, 387 | ], 388 | ) 389 | 390 | return table 391 | --------------------------------------------------------------------------------