Skip to content

Commit 0954935

Browse files
authored
Add hook_up_fig_with_ctk_struct_viewer() to crystal_toolkit/helpers/utils.py (#320)
* add some types to crystal_toolkit/helpers/utils.py * add hook_up_fig_with_ctk_struct_viewer() to crystal_toolkit/helpers/utils.py * add myself to list of contributors * fix typos in PR template * add minimal example and link to PR with screen recording to doc str
1 parent 5f57849 commit 0954935

File tree

4 files changed

+155
-24
lines changed

4 files changed

+155
-24
lines changed

.github/pull_request_template.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ contributor/co-author.
1717

1818
Work-in-progress pull requests are encouraged but please [mark your PR as draft](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/changing-the-stage-of-a-pull-request#converting-a-pull-request-to-a-draft).
1919

20-
Usually the following items should be checked before merging a PR:
20+
Usually, the following items should be checked before merging a PR:
2121

2222
- [ ] Doc strings have been added in the [Google docstring format](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings).
23-
- [ ] Type annotations are *highly* encouraged. Run [`mypy path/to/file.py`](https://github.com/python/mypy) to type check your code. Type checks are run in CI.
23+
- [ ] Type annotations are *highly* encouraged. Run [`mypy path/to/file.py`](https://github.com/python/mypy) to type-check your code. Type checks are run in CI.
2424
- [ ] Tests for any new functionality as well as bug fixes, where appropriate.
2525
- [ ] Create a new [Issue](https://github.com/materialsproject/crystaltoolkit/issues) for any TODO items that will result from merging the PR.
2626

README.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
## Interested in contributing?
44

55
A current list of new contributor issues can be seen [here](https://github.com/materialsproject/crystaltoolkit/labels/new-contributor).
6-
If you would like a new contributor issue assigned, get in touch with project maintainers!
6+
If you would like a new-contributor issue assigned, get in touch with project maintainers!
77

88
## Status
99

@@ -21,7 +21,7 @@ pip install crystal-toolkit
2121

2222
## Documentation
2323

24-
[Documentation can be found at docs.crystaltoolkit.org](https://docs.crystaltoolkit.org)
24+
Documentation can be found at [docs.crystaltoolkit.org](https://docs.crystaltoolkit.org).
2525

2626
## Example Apps
2727

@@ -60,13 +60,14 @@ The [Crystal Toolkit Development Team](https://github.com/materialsproject/cryst
6060
* [Donny Winston](https://github.com/dwinston), assisted by [Tyler Huntington](https://github.com/tylerhuntington), for helping embed Crystal Toolkit in a Django app
6161
* [Matt McDermott](https://github.com/mattmcdermott) contributed phase diagram, X-ray Diffraction, X-ray Absorption Spectrum components
6262
* [Jason Munro](https://github.com/munrojm) contributed band structure component
63+
* [Janosh Riebesell](https://github.com/janosh) contributed Phonon band structure component, [3 example apps](https://github.com/materialsproject/crystaltoolkit/blob/main/crystal_toolkit/apps/examples/matbench_dielectric_structure_on_hover.py), tests
6364
* [Stephen Weitzner](https://github.com/sweitzner) contributed POV-Ray integration (in progress)
6465
* [Richard Tran](https://github.com/CifLord) for contributing plotly-powered Wulff shapes to pymatgen, which Crystal Toolkit uses
6566
* [Guy Moore](https://github.com/guymoore13) for contributing magnetic moment visualization
6667
* [Steve Zeltmann](https://github.com/sezelt) for contributing electron diffraction
6768
* [Patrick Huck](https://github.com/tschaume), releases, operations, bugfixes and POC for MP / MPContribs
6869

69-
New contributors are welcome, please see our [Code of Conduct.](code-of-conduct.md) If you are a new contributor please modify this README in your Pull Request to add your name to the list.
70+
New contributors are welcome, please see our [Code of Conduct](code-of-conduct.md). If you are a new contributor please modify this README in your Pull Request to add your name to the list.
7071

7172
## Future of This Repository
7273

@@ -82,11 +83,9 @@ Thank you to all the authors and maintainers of the libraries Crystal Toolkit
8283
depends upon, and in particular [pymatgen](http://pymatgen.org) for crystallographic
8384
analysis and [Dash from Plotly](https://plot.ly/products/dash/) for their web app framework.
8485

85-
Thank you to the [NERSC Spin](https://www.nersc.gov/systems/spin) service for
86+
Thank you to the [NERSC Spin](https://nersc.gov/systems/spin) service for
8687
hosting the app and for their technical support.
8788

88-
Cross-browser Testing Platform and Open Source <3 generously provided by [Sauce Labs](https://saucelabs.com)
89-
9089
## Contact
9190

9291
Please contact @mkhorton with any queries or add an issue on the [GitHub Issues](https://github.com/materialsproject/crystaltoolkit/issues) page.

crystal_toolkit/helpers/utils.py

Lines changed: 147 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,23 @@
44
import re
55
import urllib.parse
66
from fractions import Fraction
7-
from typing import Any
7+
from typing import Any, Callable
88
from uuid import uuid4
99

10+
import dash
1011
import dash_mp_components as mpc
1112
import numpy as np
13+
import pandas as pd
14+
import plotly.graph_objects as go
15+
from dash import Dash, dcc, html
1216
from dash import dash_table as dt
13-
from dash import dcc, html
17+
from dash.dependencies import Input, Output, State
1418
from flask import has_request_context, request
1519
from monty.serialization import loadfn
20+
from numpy.typing import ArrayLike
21+
from pymatgen.core import Structure
1622

23+
import crystal_toolkit.components as ctc
1724
import crystal_toolkit.helpers.layouts as ctl
1825
from crystal_toolkit import MODULE_PATH
1926
from crystal_toolkit.defaults import _DEFAULTS
@@ -437,8 +444,10 @@ def get_section_heading(title, dois=None, docs_url=None, app_button_id=None):
437444
return ctl.H4([title, docs_button, app_link])
438445

439446

440-
def get_matrix_string(matrix, variable_name=None, decimals=4):
441-
"""Returns a string for use in mpc.Markdown() to render a matrix or vector.
447+
def get_matrix_string(
448+
matrix: ArrayLike, variable_name: str = None, decimals: int = 4
449+
) -> str:
450+
"""Returns a LaTeX-formatted string for use in mpc.Markdown() to render a matrix or vector.
442451
443452
:param matrix: list or numpy array
444453
:param variable_name: LaTeX-formatted variable name
@@ -455,23 +464,21 @@ def get_matrix_string(matrix, variable_name=None, decimals=4):
455464

456465
footer = "\\end{bmatrix}\n$$"
457466

458-
matrix_string = ""
459-
460-
assert hasattr(matrix, "__iter__"), "The matrix provided was not iterable"
467+
matrix_str = ""
461468

462469
for row in matrix:
463-
row_string = ""
470+
row_str = ""
464471
for idx, value in enumerate(row):
465-
row_string += f"{value:.4g}"
472+
row_str += f"{value:.4g}"
466473
if idx != len(row) - 1:
467-
row_string += " & "
468-
row_string += " \\\\ \n"
469-
matrix_string += row_string
474+
row_str += " & "
475+
row_str += " \\\\ \n"
476+
matrix_str += row_str
470477

471-
return header + matrix_string + footer
478+
return header + matrix_str + footer
472479

473480

474-
def update_css_class(kwargs, class_name):
481+
def update_css_class(kwargs: dict[str, Any], class_name: str) -> None:
475482
"""Convenience function to update className while respecting any additional classNames already
476483
set.
477484
"""
@@ -481,7 +488,7 @@ def update_css_class(kwargs, class_name):
481488
kwargs["className"] = class_name
482489

483490

484-
def is_mpid(value: str):
491+
def is_mpid(value: str) -> str | bool:
485492
"""Determine if a string is in the MP ID syntax.
486493
487494
Checks if the string starts with 'mp-' or 'mvc-' and is followed by only numbers.
@@ -492,7 +499,7 @@ def is_mpid(value: str):
492499
return False
493500

494501

495-
def pretty_frac_format(x):
502+
def pretty_frac_format(x: float) -> str:
496503
"""Formats a float to a fraction, if the fraction can be expressed without a large
497504
denominator.
498505
"""
@@ -506,3 +513,127 @@ def pretty_frac_format(x):
506513
else:
507514
x_str = str(fraction)
508515
return x_str
516+
517+
518+
def hook_up_fig_with_ctk_struct_viewer(
519+
fig: go.Figure,
520+
df: pd.DataFrame,
521+
struct_col: str = "structure",
522+
validate_id: Callable[[str], bool] = lambda id: True,
523+
) -> Dash:
524+
"""Create a Dash app that hooks up a Plotly figure with a Crystal Toolkit structure
525+
component. See https://github.com/materialsproject/crystaltoolkit/pull/320 for
526+
screen recording of example app.
527+
528+
Usage example:
529+
import random
530+
531+
import pandas as pd
532+
import plotly.express as px
533+
from crystal_toolkit.helpers.utils import hook_up_fig_with_ctk_struct_viewer
534+
from pymatgen.ext.matproj import MPRester
535+
536+
# Get random structures from the Materials Project
537+
mp_ids = [f"mp-{random.randint(1, 10000)}" for _ in range(100)]
538+
structures = MPRester(use_document_model=False).summary.search(material_ids=mp_ids)
539+
540+
df = pd.DataFrame(structures)
541+
id_col = "material_id"
542+
543+
fig = px.scatter(df, x="nsites", y="volume", hover_name=id_col, template="plotly_white")
544+
app = hook_up_fig_with_ctk_struct_viewer(fig, df.set_index(id_col))
545+
app.run_server(port=8000)
546+
547+
Args:
548+
fig (Figure): Plotly figure to be hooked up with the structure component. The
549+
figure must have hover_name set to the index of the data frame to identify
550+
dataframe rows with plot points.
551+
df (pd.DataFrame): Data frame from which to pull the structure corresponding to
552+
a hovered/clicked point in the plot.
553+
struct_col (str, optional): Name of the column in the data frame that contains
554+
the structures. Defaults to 'structure'. Can be instances of
555+
pymatgen.core.Structure or dicts created with Structure.as_dict().
556+
validate_id (Callable[[str], bool], optional): Function that takes a string
557+
extracted from the hovertext key of a hoverData event payload and returns
558+
True if the string is a valid df row index. Defaults to lambda
559+
id: True. Useful for not running the update-structure
560+
callback on unexpected data.
561+
562+
Returns:
563+
Dash: The interactive Dash app to be run with app.run_server().
564+
"""
565+
structure_component = ctc.StructureMoleculeComponent(id="structure")
566+
567+
app = Dash(prevent_initial_callbacks=True, assets_folder=SETTINGS.ASSETS_PATH)
568+
graph = dcc.Graph(id="plot", figure=fig, style={"width": "90vh"})
569+
hover_click_dd = dcc.Dropdown(
570+
id="hover-click-dropdown",
571+
options=["hover", "click"],
572+
value="click",
573+
clearable=False,
574+
style=dict(minWidth="5em"),
575+
)
576+
hover_click_dropdown = html.Div(
577+
[
578+
html.Label("Update structure on:", style=dict(fontWeight="bold")),
579+
hover_click_dd,
580+
],
581+
style=dict(
582+
display="flex",
583+
placeContent="center",
584+
placeItems="center",
585+
gap="1em",
586+
margin="1em",
587+
),
588+
)
589+
struct_title = html.H2(
590+
"Try hovering on a point in the plot to see its corresponding structure",
591+
id="struct-title",
592+
style=dict(position="absolute", padding="1ex 1em", maxWidth="25em"),
593+
)
594+
graph_structure_div = html.Div(
595+
[graph, html.Div([struct_title, structure_component.layout()])],
596+
style=dict(display="flex", gap="2em", margin="2em 0"),
597+
)
598+
app.layout = html.Div(
599+
[hover_click_dropdown, graph_structure_div],
600+
style=dict(margin="2em", padding="1em"),
601+
)
602+
ctc.register_crystal_toolkit(app=app, layout=app.layout)
603+
604+
@app.callback(
605+
Output(structure_component.id(), "data"),
606+
Output(struct_title, "children"),
607+
Input(graph, "hoverData"),
608+
Input(graph, "clickData"),
609+
State(hover_click_dd, "value"),
610+
)
611+
def update_structure(
612+
hover_data: dict[str, list[dict[str, Any]]],
613+
click_data: dict[str, list[dict[str, Any]]], # needed only as callback trigger
614+
dropdown_value: str,
615+
) -> tuple[Structure, str] | tuple[None, None]:
616+
"""Update StructureMoleculeComponent with pymatgen structure when user clicks or
617+
hovers a plot point.
618+
"""
619+
triggered = dash.callback_context.triggered[0]
620+
if dropdown_value == "click" and triggered["prop_id"].endswith(".hoverData"):
621+
# do nothing if we're in update-on-click mode but callback was triggered by
622+
# hover event
623+
raise dash.exceptions.PreventUpdate
624+
625+
# hover_data and click_data are identical since a hover event always precedes a
626+
# click so we always use hover_data
627+
material_id = hover_data["points"][0]["hovertext"]
628+
if not validate_id(material_id):
629+
print(f"bad {material_id=}")
630+
raise dash.exceptions.PreventUpdate
631+
632+
struct = df[struct_col][material_id]
633+
if isinstance(struct, dict):
634+
struct = Structure.from_dict(struct)
635+
struct_title = f"{material_id} ({struct.formula})"
636+
637+
return struct, struct_title
638+
639+
return app

docs_rst/introduction.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ Contributors
6969
* `Donny Winston <https://github.com/dwinston>`_, assisted by `Tyler Huntington <https://github.com/tylerhuntington>`_, for helping embed Crystal Toolkit in a Django app
7070
* `Matt McDermott <https://github.com/mattmcdermott>`_ contributed phase diagram, X-ray Diffraction, X-ray Absorption Spectrum components
7171
* `Jason Munro <https://github.com/munrojm>`_ contributed band structure component
72+
* `Janosh Riebesell <https://github.com/janosh>`_ contributed Phonon band structure component, 3 example apps, tests
7273
* `Stephen Weitzner <https://github.com/sweitzner>`_ contributed POV-Ray integration (in progress)
7374
* `Richard Tran <https://github.com/CifLord>`_ for contributing plotly-powered Wulff shapes to pymatgen, which Crystal Toolkit uses
7475
* `Guy Moore <https://github.com/guymoore13>`_ for contributing magnetic moment visualization

0 commit comments

Comments
 (0)