From c8fb1608fb0c03510b49bc18ddb116cfc392c563 Mon Sep 17 00:00:00 2001 From: elliotgunn Date: Thu, 26 May 2022 16:29:41 -0400 Subject: [PATCH 1/3] refactor --- apps/dash-brain-viewer/.gitignore | 4 - apps/dash-brain-viewer/README.md | 2 +- apps/dash-brain-viewer/app.py | 347 +------ apps/dash-brain-viewer/assets/css/app.css | 63 ++ apps/dash-brain-viewer/assets/dash-logo.png | Bin 4502 -> 0 bytes apps/dash-brain-viewer/assets/default.css | 910 +++++++++++------- .../{ => assets/images}/brain.png | Bin .../assets/images/plotly-logo-dark-theme.png | Bin 0 -> 23021 bytes apps/dash-brain-viewer/assets/style.css | 16 +- apps/dash-brain-viewer/constants.py | 34 + apps/dash-brain-viewer/gitignore | 191 ++++ apps/dash-brain-viewer/requirements.txt | 6 +- apps/dash-brain-viewer/runtime.txt | 1 + apps/dash-brain-viewer/utils/components.py | 140 +++ apps/dash-brain-viewer/utils/figures.py | 79 ++ .../utils/helper_functions.py | 53 + 16 files changed, 1164 insertions(+), 682 deletions(-) delete mode 100644 apps/dash-brain-viewer/.gitignore create mode 100644 apps/dash-brain-viewer/assets/css/app.css delete mode 100644 apps/dash-brain-viewer/assets/dash-logo.png rename apps/dash-brain-viewer/{ => assets/images}/brain.png (100%) create mode 100644 apps/dash-brain-viewer/assets/images/plotly-logo-dark-theme.png create mode 100644 apps/dash-brain-viewer/constants.py create mode 100644 apps/dash-brain-viewer/gitignore create mode 100644 apps/dash-brain-viewer/runtime.txt create mode 100644 apps/dash-brain-viewer/utils/components.py create mode 100644 apps/dash-brain-viewer/utils/figures.py create mode 100644 apps/dash-brain-viewer/utils/helper_functions.py diff --git a/apps/dash-brain-viewer/.gitignore b/apps/dash-brain-viewer/.gitignore deleted file mode 100644 index d060eacb7..000000000 --- a/apps/dash-brain-viewer/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -venv -*.pyc -.DS_Store -.env diff --git a/apps/dash-brain-viewer/README.md b/apps/dash-brain-viewer/README.md index e3e2f7a03..46940613a 100644 --- a/apps/dash-brain-viewer/README.md +++ b/apps/dash-brain-viewer/README.md @@ -38,7 +38,7 @@ Open a browser at http://127.0.0.1:8050 ## Screenshots -![brain.png](brain.png) +![brain.png](assets/images/brain.png) ### Credit diff --git a/apps/dash-brain-viewer/app.py b/apps/dash-brain-viewer/app.py index e1b4f1c0a..1b5643d5b 100644 --- a/apps/dash-brain-viewer/app.py +++ b/apps/dash-brain-viewer/app.py @@ -1,13 +1,14 @@ -import os import json import dash -import dash_core_components as dcc -import dash_html_components as html +from dash import html, dcc, Input, Output, State, callback + import dash_colorscales as dcs -from dash.dependencies import Input, Output, State -from dash.exceptions import PreventUpdate -from mni import create_mesh_data, default_colorscale +from mni import create_mesh_data + +from utils.figures import brain_graph_handler, save_annotations +from utils.components import header, brain_graph, color_picker +from constants import default_colorscale_index, axis_template, plot_layout app = dash.Dash( __name__, @@ -18,348 +19,60 @@ server = app.server -GITHUB_LINK = os.environ.get( - "GITHUB_LINK", - "https://github.com/plotly/dash-sample-apps/tree/master/apps/dash-brain-viewer", -) - -default_colorscale_index = [ea[1] for ea in default_colorscale] - -axis_template = { - "showbackground": True, - "backgroundcolor": "#141414", - "gridcolor": "rgb(255, 255, 255)", - "zerolinecolor": "rgb(255, 255, 255)", -} - -plot_layout = { - "title": "", - "margin": {"t": 0, "b": 0, "l": 0, "r": 0}, - "font": {"size": 12, "color": "white"}, - "showlegend": False, - "plot_bgcolor": "#141414", - "paper_bgcolor": "#141414", - "scene": { - "xaxis": axis_template, - "yaxis": axis_template, - "zaxis": axis_template, - "aspectratio": {"x": 1, "y": 1.2, "z": 1}, - "camera": {"eye": {"x": 1.25, "y": 1.25, "z": 1.25}}, - "annotations": [], - }, -} - app.layout = html.Div( [ html.Div( [ html.Div( [ - html.Div( - [ - html.Div( - [ - html.Img( - src=app.get_asset_url("dash-logo.png") - ), - html.H4("MRI Reconstruction"), - ], - className="header__title", - ), - html.Div( - [ - html.P( - "Click on the brain to add an annotation. Drag the black corners of the graph to rotate." - ) - ], - className="header__info pb-20", - ), - html.Div( - [ - html.A( - "View on GitHub", - href=GITHUB_LINK, - target="_blank", - ) - ], - className="header__button", - ), - ], - className="header pb-20", - ), - html.Div( - [ - dcc.Graph( - id="brain-graph", - figure={ - "data": create_mesh_data("human_atlas"), - "layout": plot_layout, - }, - config={"editable": True, "scrollZoom": False}, - ) - ], - className="graph__container", + header( + app, + "#141414", + "MRI Reconstruction", + subheader="Click on the brain to add an annotation. Drag the black corners of the graph to rotate.", ), + brain_graph("brain-graph"), ], className="container", - ) - ], - className="two-thirds column app__left__section", - ), - html.Div( - [ - html.Div( - [ - html.Div( - [ - html.P( - "Click colorscale to change", className="subheader" - ), - dcs.DashColorscales( - id="colorscale-picker", - colorscale=default_colorscale_index, - ), - ] - ) - ], - className="colorscale pb-20", - ), - html.Div( - [ - html.P("Select option", className="subheader"), - dcc.RadioItems( - options=[ - {"label": "Brain Atlas", "value": "human_atlas"}, - {"label": "Cortical Thickness", "value": "human"}, - {"label": "Mouse Brain", "value": "mouse"}, - ], - value="human_atlas", - id="radio-options", - labelClassName="label__option", - inputClassName="input__option", - ), - ], - className="pb-20", - ), - html.Div( - [ - html.Span("Click data", className="subheader"), - html.Span(" | "), - html.Span( - "Click on points in the graph.", className="small-text" - ), - dcc.Loading( - html.Pre(id="click-data", className="info__container"), - type="dot", - ), - ], - className="pb-20", - ), - html.Div( - [ - html.Span("Relayout data", className="subheader"), - html.Span(" | "), - html.Span( - "Drag the graph corners to rotate it.", - className="small-text", - ), - dcc.Loading( - html.Pre(id="relayout-data", className="info__container"), - type="dot", - ), - ], - className="pb-20", - ), - html.Div( - [ - html.P( - [ - "Dash/Python code on ", - html.A( - children="GitHub.", - target="_blank", - href=GITHUB_LINK, - className="red-ish", - ), - ] - ), - html.P( - [ - "Brain data from Mcgill's ACE Lab ", - html.A( - children="Surface Viewer.", - target="_blank", - href="https://brainbrowser.cbrain.mcgill.ca/surface-viewer#ct", - className="red-ish", - ), - ] - ), - ] ), ], - className="one-third column app__right__section", + className="two-thirds column app__left__section", ), + color_picker("colorscale-picker"), dcc.Store(id="annotation_storage"), ] ) -def add_marker(x, y, z): - """ Create a plotly marker dict. """ - - return { - "x": [x], - "y": [y], - "z": [z], - "mode": "markers", - "marker": {"size": 25, "line": {"width": 3}}, - "name": "Marker", - "type": "scatter3d", - "text": ["Click point to remove annotation"], - } - - -def add_annotation(x, y, z): - """ Create plotly annotation dict. """ - - return { - "x": x, - "y": y, - "z": z, - "font": {"color": "black"}, - "bgcolor": "white", - "borderpad": 5, - "bordercolor": "black", - "borderwidth": 1, - "captureevents": True, - "ay": -100, - "arrowcolor": "white", - "arrowwidth": 2, - "arrowhead": 0, - "text": "Click here to annotate
(Click point to remove)", - } - - -def marker_in_points(points, marker): - """ - Checks if the marker is in the list of points. - - :params points: a list of dict that contains x, y, z - :params marker: a dict that contains x, y, z - :returns: index of the matching marker in list - """ - - for index, point in enumerate(points): - if ( - point["x"] == marker["x"] - and point["y"] == marker["y"] - and point["z"] == marker["z"] - ): - return index - return None - - -@app.callback( +@callback( Output("brain-graph", "figure"), - [ - Input("brain-graph", "clickData"), - Input("radio-options", "value"), - Input("colorscale-picker", "colorscale"), - ], - [State("brain-graph", "figure"), State("annotation_storage", "data")], + Input("brain-graph", "clickData"), + Input("radio-options", "value"), + Input("colorscale-picker", "colorscale"), + State("brain-graph", "figure"), + State("annotation_storage", "data"), ) -def brain_graph_handler(click_data, val, colorscale, figure, current_anno): - """ Listener on colorscale, option picker, and graph on click to update the graph. """ - - # new option select - if figure["data"][0]["name"] != val: - figure["data"] = create_mesh_data(val) - figure["layout"] = plot_layout - cs = [[i / (len(colorscale) - 1), rgb] for i, rgb in enumerate(colorscale)] - figure["data"][0]["colorscale"] = cs - return figure +def return_brain_graph_handler(click_data, val, colorscale, figure, current_anno): + return brain_graph_handler(click_data, val, colorscale, figure, current_anno) - # modify graph markers - if click_data is not None and "points" in click_data: - y_value = click_data["points"][0]["y"] - x_value = click_data["points"][0]["x"] - z_value = click_data["points"][0]["z"] - - marker = add_marker(x_value, y_value, z_value) - point_index = marker_in_points(figure["data"], marker) - - # delete graph markers - if len(figure["data"]) > 1 and point_index is not None: - - figure["data"].pop(point_index) - anno_index_offset = 2 if val == "mouse" else 1 - try: - figure["layout"]["scene"]["annotations"].pop( - point_index - anno_index_offset - ) - except Exception as error: - print(error) - pass - - # append graph markers - else: - - # iterate through the store annotations and save it into figure data - if current_anno is not None: - for index, annotations in enumerate( - figure["layout"]["scene"]["annotations"] - ): - for key in current_anno.keys(): - if str(index) in key: - figure["layout"]["scene"]["annotations"][index][ - "text" - ] = current_anno[key] - - figure["data"].append(marker) - figure["layout"]["scene"]["annotations"].append( - add_annotation(x_value, y_value, z_value) - ) - - cs = [[i / (len(colorscale) - 1), rgb] for i, rgb in enumerate(colorscale)] - figure["data"][0]["colorscale"] = cs - - return figure - - -@app.callback(Output("click-data", "children"), [Input("brain-graph", "clickData")]) +@callback(Output("click-data", "children"), Input("brain-graph", "clickData")) def display_click_data(click_data): return json.dumps(click_data, indent=4) -@app.callback( - Output("relayout-data", "children"), [Input("brain-graph", "relayoutData")] -) +@callback(Output("relayout-data", "children"), Input("brain-graph", "relayoutData")) def display_relayout_data(relayout_data): return json.dumps(relayout_data, indent=4) -@app.callback( +@callback( Output("annotation_storage", "data"), - [Input("brain-graph", "relayoutData")], - [State("annotation_storage", "data")], + Input("brain-graph", "relayoutData"), + State("annotation_storage", "data"), ) -def save_annotations(relayout_data, current_data): - """ Update the annotations in the dcc store. """ - - if relayout_data is None: - raise PreventUpdate - - if current_data is None: - return {} - - for key in relayout_data.keys(): - - # to determine if the relayout has to do with annotations - if "scene.annotations" in key: - current_data[key] = relayout_data[key] - - return current_data +def return_save_annotations(relayout_data, current_data): + return save_annotations(relayout_data, current_data) if __name__ == "__main__": diff --git a/apps/dash-brain-viewer/assets/css/app.css b/apps/dash-brain-viewer/assets/css/app.css new file mode 100644 index 000000000..b7a3adfd4 --- /dev/null +++ b/apps/dash-brain-viewer/assets/css/app.css @@ -0,0 +1,63 @@ +/* Header */ +.header { + height: 10vh; + display: flex; + padding-left: 2%; + padding-right: 2%; + font-family: playfair display, sans-serif; + font-weight: bold; +} + +.header .header-title { + font-size: 5vh; +} + +.subheader-title { + font-size: 1.5vh; +} + +.header-logos { + margin-left: auto; +} + +.header-logos img { + margin-left: 3vh !important; + max-height: 5vh; +} + + +/* Demo button css */ +.demo-button { + font-size: 1.5vh; + font-family: Open Sans, sans-serif; + text-decoration: none; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + border-radius: 8px; + font-weight: 700; + -webkit-padding-start: 1rem; + padding-inline-start: 1rem; + -webkit-padding-end: 1rem; + padding-inline-end: 1rem; + color: #ffffff; + letter-spacing: 1.5px; + border: solid 1.5px transparent; + box-shadow: 2px 1000px 1px #0c0c0c inset; + background-image: linear-gradient(135deg, #7A76FF, #7A76FF, #7FE4FF); + -webkit-background-size: 200% 100%; + background-size: 200% 100%; + -webkit-background-position: 99%; + background-position: 99%; + background-origin: border-box; + transition: all .4s ease-in-out; + padding-top: 1vh; + padding-bottom: 1vh; + vertical-align: super; +} + +.demo-button:hover { + color: #7A76FF; + background-position: 0%; +} \ No newline at end of file diff --git a/apps/dash-brain-viewer/assets/dash-logo.png b/apps/dash-brain-viewer/assets/dash-logo.png deleted file mode 100644 index eb700fc717bc9294bc97d337766f3fb8eb9d17e3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4502 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 U .label-body { - display: inline-block; - margin-left: .5rem; - font-weight: normal; } - - - /* Lists +input[type="email"], +input[type="number"], +input[type="search"], +input[type="text"], +input[type="tel"], +input[type="url"], +input[type="password"], +textarea, +select { + height: 38px; + padding: 6px 10px; + /* The 6px vertically centers text on FF, ignored by Webkit */ + background-color: #fff; + border: 1px solid #D1D1D1; + border-radius: 4px; + box-shadow: none; + box-sizing: border-box; + font-family: inherit; + font-size: inherit; + /*https://stackoverflow.com/questions/6080413/why-doesnt-input-inherit-the-font-from-body*/ +} + +/* Removes awkward default styles on some inputs for iOS */ +input[type="email"], +input[type="number"], +input[type="search"], +input[type="text"], +input[type="tel"], +input[type="url"], +input[type="password"], +textarea { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +textarea { + min-height: 65px; + padding-top: 6px; + padding-bottom: 6px; +} + +input[type="email"]:focus, +input[type="number"]:focus, +input[type="search"]:focus, +input[type="text"]:focus, +input[type="tel"]:focus, +input[type="url"]:focus, +input[type="password"]:focus, +textarea:focus, +select:focus { + border: 1px solid #33C3F0; + outline: 0; +} + +label, +legend { + display: block; + margin-bottom: 0px; +} + +fieldset { + padding: 0; + border-width: 0; +} + +input[type="checkbox"], +input[type="radio"] { + display: inline; +} + +label>.label-body { + display: inline-block; + margin-left: .5rem; + font-weight: normal; +} + + +/* Lists –––––––––––––––––––––––––––––––––––––––––––––––––– */ - ul { - list-style: circle inside; } - ol { - list-style: decimal inside; } - ol, ul { - padding-left: 0; - margin-top: 0; } - ul ul, - ul ol, - ol ol, - ol ul { - margin: 1.5rem 0 1.5rem 3rem; - font-size: 90%; } - li { - margin-bottom: 1rem; } - - - /* Tables +ul { + list-style: circle inside; +} + +ol { + list-style: decimal inside; +} + +ol, +ul { + padding-left: 0; + margin-top: 0; +} + +ul ul, +ul ol, +ol ol, +ol ul { + margin: 1.5rem 0 1.5rem 3rem; + font-size: 90%; +} + +li { + margin-bottom: 1rem; +} + + +/* Tables –––––––––––––––––––––––––––––––––––––––––––––––––– */ - table { - border-collapse: collapse; - } - th, - td { - padding: 12px 15px; - text-align: left; - border-bottom: 1px solid #E1E1E1; } - th:first-child, - td:first-child { - padding-left: 0; } - th:last-child, - td:last-child { - padding-right: 0; } - - - /* Spacing +table { + border-collapse: collapse; +} + +th, +td { + padding: 12px 15px; + text-align: left; + border-bottom: 1px solid #E1E1E1; +} + +th:first-child, +td:first-child { + padding-left: 0; +} + +th:last-child, +td:last-child { + padding-right: 0; +} + + +/* Spacing –––––––––––––––––––––––––––––––––––––––––––––––––– */ - button, - .button { - margin-bottom: 0rem; } - input, - textarea, - select, - fieldset { - margin-bottom: 0rem; } - pre, - dl, - figure, - table, - form { - margin-bottom: 0rem; } - p, - ul, - ol { - margin-bottom: 0.75rem; } - - /* Utilities +button, +.button { + margin-bottom: 0rem; +} + +input, +textarea, +select, +fieldset { + margin-bottom: 0rem; +} + +pre, +dl, +figure, +table, +form { + margin-bottom: 0rem; +} + +p, +ul, +ol { + margin-bottom: 0.75rem; +} + +/* Utilities –––––––––––––––––––––––––––––––––––––––––––––––––– */ - .u-full-width { - width: 100%; - box-sizing: border-box; } - .u-max-full-width { - max-width: 100%; - box-sizing: border-box; } - .u-pull-right { - float: right; } - .u-pull-left { - float: left; } - - - /* Misc +.u-full-width { + width: 100%; + box-sizing: border-box; +} + +.u-max-full-width { + max-width: 100%; + box-sizing: border-box; +} + +.u-pull-right { + float: right; +} + +.u-pull-left { + float: left; +} + + +/* Misc –––––––––––––––––––––––––––––––––––––––––––––––––– */ - hr { - margin-top: 3rem; - margin-bottom: 3.5rem; - border-width: 0; - border-top: 1px solid #E1E1E1; } - - - /* Clearing +hr { + margin-top: 3rem; + margin-bottom: 3.5rem; + border-width: 0; + border-top: 1px solid #E1E1E1; +} + + +/* Clearing –––––––––––––––––––––––––––––––––––––––––––––––––– */ - - /* Self Clearing Goodness */ - .container:after, - .row:after, - .u-cf { - content: ""; - display: table; - clear: both; } - - - /* Media Queries + +/* Self Clearing Goodness */ +.container:after, +.row:after, +.u-cf { + content: ""; + display: table; + clear: both; +} + + +/* Media Queries –––––––––––––––––––––––––––––––––––––––––––––––––– */ - /* +/* Note: The best way to structure the use of media queries is to create the queries near the relevant code. For example, if you wanted to change the styles for buttons on small devices, paste the mobile query code up in the buttons section and style it there. */ - - - /* Larger than mobile */ - @media (min-width: 400px) {} - - /* Larger than phablet (also point when grid becomes active) */ - @media (min-width: 550px) {} - - /* Larger than tablet */ - @media (min-width: 750px) {} - - /* Larger than desktop */ - @media (min-width: 1000px) {} - - /* Larger than Desktop HD */ - @media (min-width: 1200px) {} \ No newline at end of file + + +/* Larger than mobile */ +@media (min-width: 400px) {} + +/* Larger than phablet (also point when grid becomes active) */ +@media (min-width: 550px) {} + +/* Larger than tablet */ +@media (min-width: 750px) {} + +/* Larger than desktop */ +@media (min-width: 1000px) {} + +/* Larger than Desktop HD */ +@media (min-width: 1200px) {} \ No newline at end of file diff --git a/apps/dash-brain-viewer/brain.png b/apps/dash-brain-viewer/assets/images/brain.png similarity index 100% rename from apps/dash-brain-viewer/brain.png rename to apps/dash-brain-viewer/assets/images/brain.png diff --git a/apps/dash-brain-viewer/assets/images/plotly-logo-dark-theme.png b/apps/dash-brain-viewer/assets/images/plotly-logo-dark-theme.png new file mode 100644 index 0000000000000000000000000000000000000000..984dd57ab53b7f9fa47df219e25df91a0885613d GIT binary patch literal 23021 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 1 and point_index is not None: + + figure["data"].pop(point_index) + anno_index_offset = 2 if val == "mouse" else 1 + try: + figure["layout"]["scene"]["annotations"].pop( + point_index - anno_index_offset + ) + except Exception as error: + print(error) + pass + + # append graph markers + else: + + # iterate through the store annotations and save it into figure data + if current_anno is not None: + for index, annotations in enumerate( + figure["layout"]["scene"]["annotations"] + ): + for key in current_anno.keys(): + if str(index) in key: + figure["layout"]["scene"]["annotations"][index][ + "text" + ] = current_anno[key] + + figure["data"].append(marker) + figure["layout"]["scene"]["annotations"].append( + add_annotation(x_value, y_value, z_value) + ) + + cs = [[i / (len(colorscale) - 1), rgb] for i, rgb in enumerate(colorscale)] + figure["data"][0]["colorscale"] = cs + + return figure + + +def save_annotations(relayout_data, current_data): + """Update the annotations in the dcc store.""" + + if relayout_data is None: + raise PreventUpdate + + if current_data is None: + return {} + + for key in relayout_data.keys(): + + # to determine if the relayout has to do with annotations + if "scene.annotations" in key: + current_data[key] = relayout_data[key] + + return current_data diff --git a/apps/dash-brain-viewer/utils/helper_functions.py b/apps/dash-brain-viewer/utils/helper_functions.py new file mode 100644 index 000000000..63c629811 --- /dev/null +++ b/apps/dash-brain-viewer/utils/helper_functions.py @@ -0,0 +1,53 @@ +def add_marker(x, y, z): + """Create a plotly marker dict.""" + + return { + "x": [x], + "y": [y], + "z": [z], + "mode": "markers", + "marker": {"size": 25, "line": {"width": 3}}, + "name": "Marker", + "type": "scatter3d", + "text": ["Click point to remove annotation"], + } + + +def add_annotation(x, y, z): + """Create plotly annotation dict.""" + + return { + "x": x, + "y": y, + "z": z, + "font": {"color": "black"}, + "bgcolor": "white", + "borderpad": 5, + "bordercolor": "black", + "borderwidth": 1, + "captureevents": True, + "ay": -100, + "arrowcolor": "white", + "arrowwidth": 2, + "arrowhead": 0, + "text": "Click here to annotate
(Click point to remove)", + } + + +def marker_in_points(points, marker): + """ + Checks if the marker is in the list of points. + + :params points: a list of dict that contains x, y, z + :params marker: a dict that contains x, y, z + :returns: index of the matching marker in list + """ + + for index, point in enumerate(points): + if ( + point["x"] == marker["x"] + and point["y"] == marker["y"] + and point["z"] == marker["z"] + ): + return index + return None From 574d4ec4d46597182a14fe463e19c00388007201 Mon Sep 17 00:00:00 2001 From: elliotgunn Date: Thu, 26 May 2022 16:34:50 -0400 Subject: [PATCH 2/3] fixes --- apps/dash-brain-viewer/app.py | 1 - apps/dash-brain-viewer/utils/figures.py | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/dash-brain-viewer/app.py b/apps/dash-brain-viewer/app.py index 1b5643d5b..d4a409f85 100644 --- a/apps/dash-brain-viewer/app.py +++ b/apps/dash-brain-viewer/app.py @@ -8,7 +8,6 @@ from utils.figures import brain_graph_handler, save_annotations from utils.components import header, brain_graph, color_picker -from constants import default_colorscale_index, axis_template, plot_layout app = dash.Dash( __name__, diff --git a/apps/dash-brain-viewer/utils/figures.py b/apps/dash-brain-viewer/utils/figures.py index d971656f5..affe9958a 100644 --- a/apps/dash-brain-viewer/utils/figures.py +++ b/apps/dash-brain-viewer/utils/figures.py @@ -1,5 +1,7 @@ from utils.helper_functions import add_marker, add_annotation, marker_in_points from dash.exceptions import PreventUpdate +from mni import create_mesh_data +from constants import plot_layout def brain_graph_handler(click_data, val, colorscale, figure, current_anno): From 8c5e93364abe7ca37c9363c08b5110d54954b6db Mon Sep 17 00:00:00 2001 From: admin <51248046+danton267@users.noreply.github.com> Date: Fri, 27 May 2022 17:12:00 +0100 Subject: [PATCH 3/3] final --- apps/dash-brain-viewer/README.md | 2 +- apps/dash-brain-viewer/app.py | 86 +-- apps/dash-brain-viewer/assets/css/app.css | 70 +- apps/dash-brain-viewer/assets/default.css | 616 ------------------ .../assets/{images => github}/brain.png | Bin apps/dash-brain-viewer/assets/style.css | 184 ------ apps/dash-brain-viewer/constants.py | 23 +- apps/dash-brain-viewer/requirements.txt | 1 + apps/dash-brain-viewer/utils/components.py | 184 ++---- apps/dash-brain-viewer/utils/figures.py | 3 +- .../{mni.py => utils/model.py} | 12 +- 11 files changed, 193 insertions(+), 988 deletions(-) delete mode 100644 apps/dash-brain-viewer/assets/default.css rename apps/dash-brain-viewer/assets/{images => github}/brain.png (100%) delete mode 100644 apps/dash-brain-viewer/assets/style.css rename apps/dash-brain-viewer/{mni.py => utils/model.py} (93%) diff --git a/apps/dash-brain-viewer/README.md b/apps/dash-brain-viewer/README.md index 46940613a..23085935d 100644 --- a/apps/dash-brain-viewer/README.md +++ b/apps/dash-brain-viewer/README.md @@ -38,7 +38,7 @@ Open a browser at http://127.0.0.1:8050 ## Screenshots -![brain.png](assets/images/brain.png) +![brain.png](assets/github/brain.png) ### Credit diff --git a/apps/dash-brain-viewer/app.py b/apps/dash-brain-viewer/app.py index d4a409f85..e2c3b6303 100644 --- a/apps/dash-brain-viewer/app.py +++ b/apps/dash-brain-viewer/app.py @@ -1,78 +1,62 @@ +from dash import Dash, dcc, Input, Output, State, callback +import dash_bootstrap_components as dbc import json -import dash -from dash import html, dcc, Input, Output, State, callback -import dash_colorscales as dcs -from mni import create_mesh_data +import utils.figures as figs +from utils.components import header, brain_graph, control_and_output -from utils.figures import brain_graph_handler, save_annotations -from utils.components import header, brain_graph, color_picker - - -app = dash.Dash( - __name__, - meta_tags=[{"name": "viewport", "content": "width=device-width, initial-scale=1"}], -) - -app.title = "Brain Surface Viewer" +app = Dash(__name__, title = "Brain Surface Viewer", external_stylesheets=[dbc.themes.BOOTSTRAP]) server = app.server -app.layout = html.Div( +app.layout = dbc.Container( [ - html.Div( - [ - html.Div( - [ - header( - app, - "#141414", - "MRI Reconstruction", - subheader="Click on the brain to add an annotation. Drag the black corners of the graph to rotate.", - ), - brain_graph("brain-graph"), - ], - className="container", + dbc.Row([ + dbc.Col([ + header( + app, + header_color="#F4F6F8", + header="MRI Reconstruction", + subheader="Click on the brain to add an annotation. Drag the black corners of the graph to rotate.", ), - ], - className="two-thirds column app__left__section", - ), - color_picker("colorscale-picker"), + brain_graph("brain-graph"), + ], width=8 + ), + dbc.Col([ + dbc.Card(control_and_output(), className="right-card"), + + ], width=4) + ]), + dcc.Store(id="annotation_storage"), - ] + ], + fluid=True ) + @callback( Output("brain-graph", "figure"), + Output("click-data", "children"), Input("brain-graph", "clickData"), Input("radio-options", "value"), Input("colorscale-picker", "colorscale"), State("brain-graph", "figure"), State("annotation_storage", "data"), ) -def return_brain_graph_handler(click_data, val, colorscale, figure, current_anno): - return brain_graph_handler(click_data, val, colorscale, figure, current_anno) - - -@callback(Output("click-data", "children"), Input("brain-graph", "clickData")) -def display_click_data(click_data): - return json.dumps(click_data, indent=4) - - -@callback(Output("relayout-data", "children"), Input("brain-graph", "relayoutData")) -def display_relayout_data(relayout_data): - return json.dumps(relayout_data, indent=4) - +def return_brain_graph_handler_display_data(click_data, val, colorscale, figure, current_anno): + fig = figs.brain_graph_handler(click_data, val, colorscale, figure, current_anno) + click_data = json.dumps(click_data, indent=4) + return fig, click_data @callback( + Output("relayout-data", "children"), Output("annotation_storage", "data"), Input("brain-graph", "relayoutData"), - State("annotation_storage", "data"), -) -def return_save_annotations(relayout_data, current_data): - return save_annotations(relayout_data, current_data) + State("annotation_storage", "data"),) +def display_relayout_data_update_storage(relayout_data, current_data): + return json.dumps(relayout_data, indent=4), figs.save_annotations(relayout_data, current_data) if __name__ == "__main__": - app.run_server(debug=True) + app.run_server(debug=True) \ No newline at end of file diff --git a/apps/dash-brain-viewer/assets/css/app.css b/apps/dash-brain-viewer/assets/css/app.css index b7a3adfd4..f6fbc52dc 100644 --- a/apps/dash-brain-viewer/assets/css/app.css +++ b/apps/dash-brain-viewer/assets/css/app.css @@ -1,3 +1,70 @@ +body { + background-color: #141414; + color: #F4F6F8; + font-family: "Open Sans", sans-serif; + margin: 0; +} + +/* right section */ +.right-card { + background-color: #1D1D1D; + overflow-y: auto; + overflow: auto; + padding: 25px; + border-left: black solid 5px; + min-height: 100vh; + max-height: 100vh; +} + +/* color sacale */ +.colorscalePickerContainer { + background: #F4F5FA !important; +} + +.colorscale-block { + margin: 0 !important; + border: 1px solid #C4CDD5; + padding: 5px 5px 0px 5px; + border-radius: 0.5rem; +} +.colorscale-block div { + margin: 0 !important; +} + +/* Code display */ +#click-data, #relayout-data { + background-color: #292929; + color: white; + padding: 25px; +} + +/* radio labels */ +#radio-options > label { + padding-right: 20px; +} + + + +/* Scrollbar */ + +::-webkit-scrollbar { + width: 20px; +} + +::-webkit-scrollbar-track { + box-shadow: inset 0 0 5px grey; + border-radius: 10px; +} + +::-webkit-scrollbar-thumb { + background: #292929; + border-radius: 10px; +} + +::-webkit-scrollbar-thumb:hover { + background: #292929; +} + /* Header */ .header { height: 10vh; @@ -5,11 +72,11 @@ padding-left: 2%; padding-right: 2%; font-family: playfair display, sans-serif; - font-weight: bold; } .header .header-title { font-size: 5vh; + font-weight: bold; } .subheader-title { @@ -18,6 +85,7 @@ .header-logos { margin-left: auto; + align-self: center !important; } .header-logos img { diff --git a/apps/dash-brain-viewer/assets/default.css b/apps/dash-brain-viewer/assets/default.css deleted file mode 100644 index 4286e9c2e..000000000 --- a/apps/dash-brain-viewer/assets/default.css +++ /dev/null @@ -1,616 +0,0 @@ -/* Table of contents -–––––––––––––––––––––––––––––––––––––––––––––––––– -- Plotly.js -- Grid -- Base Styles -- Typography -- Links -- Buttons -- Forms -- Lists -- Code -- Tables -- Spacing -- Utilities -- Clearing -- Media Queries -*/ - -/* PLotly.js -––––––––––––––––––––––––––––––––––––––––––––––– */ -/* plotly.js's modebar's z-index is 1001 by default - * https://github.com/plotly/plotly.js/blob/7e4d8ab164258f6bd48be56589dacd9bdd7fded2/src/css/_modebar.scss#L5 - * In case a dropdown is above the graph, the dropdown's options - * will be rendered below the modebar - * Increase the select option's z-index - */ - -/* This was actually not quite right - - dropdowns were overlapping each other (edited October 26) - -.Select { - z-index: 1002; -}*/ - -/* Grid -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -.container { - position: relative; - width: 100%; - max-width: 960px; - margin: 0 auto; - padding: 0 20px; - box-sizing: border-box; -} - -.column, -.columns { - width: 100%; - float: left; - box-sizing: border-box; -} - -/* For devices larger than 400px */ -@media (min-width: 400px) { - .container { - width: 85%; - padding: 0; - } -} - -/* For devices larger than 550px */ -@media (min-width: 1024px) { - .container { - width: 80%; - } - - .column, - .columns { - margin-left: 4%; - } - - .column:first-child, - .columns:first-child { - margin-left: 0; - } - - .one.column, - .one.columns { - width: 4.66666666667%; - } - - .two.columns { - width: 13.3333333333%; - } - - .three.columns { - width: 22%; - } - - .four.columns { - width: 30.6666666667%; - } - - .five.columns { - width: 39.3333333333%; - } - - .six.columns { - width: 48%; - } - - .seven.columns { - width: 56.6666666667%; - } - - .eight.columns { - width: 65.3333333333%; - } - - .nine.columns { - width: 74.0%; - } - - .ten.columns { - width: 82.6666666667%; - } - - .eleven.columns { - width: 91.3333333333%; - } - - .twelve.columns { - width: 100%; - margin-left: 0; - } - - .one-third.column { - width: 30.6666666667%; - } - - .two-thirds.column { - width: 65.3333333333%; - } - - .one-half.column { - width: 48%; - } - - /* Offsets */ - .offset-by-one.column, - .offset-by-one.columns { - margin-left: 8.66666666667%; - } - - .offset-by-two.column, - .offset-by-two.columns { - margin-left: 17.3333333333%; - } - - .offset-by-three.column, - .offset-by-three.columns { - margin-left: 26%; - } - - .offset-by-four.column, - .offset-by-four.columns { - margin-left: 34.6666666667%; - } - - .offset-by-five.column, - .offset-by-five.columns { - margin-left: 43.3333333333%; - } - - .offset-by-six.column, - .offset-by-six.columns { - margin-left: 52%; - } - - .offset-by-seven.column, - .offset-by-seven.columns { - margin-left: 60.6666666667%; - } - - .offset-by-eight.column, - .offset-by-eight.columns { - margin-left: 69.3333333333%; - } - - .offset-by-nine.column, - .offset-by-nine.columns { - margin-left: 78.0%; - } - - .offset-by-ten.column, - .offset-by-ten.columns { - margin-left: 86.6666666667%; - } - - .offset-by-eleven.column, - .offset-by-eleven.columns { - margin-left: 95.3333333333%; - } - - .offset-by-one-third.column, - .offset-by-one-third.columns { - margin-left: 34.6666666667%; - } - - .offset-by-two-thirds.column, - .offset-by-two-thirds.columns { - margin-left: 69.3333333333%; - } - - .offset-by-one-half.column, - .offset-by-one-half.columns { - margin-left: 52%; - } - -} - - -/* Base Styles - –––––––––––––––––––––––––––––––––––––––––––––––––– */ -/* NOTE - html is set to 62.5% so that all the REM measurements throughout Skeleton - are based on 10px sizing. So basically 1.5rem = 15px :) */ -html { - font-size: 62.5%; -} - -body { - font-size: 1.5em; - /* currently ems cause chrome bug misinterpreting rems on body element */ - line-height: 1.6; - font-weight: 400; - font-family: "Open Sans", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; - color: rgb(50, 50, 50); -} - - -/* Typography - –––––––––––––––––––––––––––––––––––––––––––––––––– */ -h1, -h2, -h3, -h4, -h5, -h6 { - margin-top: 0; - margin-bottom: 0; - font-weight: 300; -} - -h1 { - font-size: 4.5rem; - line-height: 1.2; - letter-spacing: -.1rem; - margin-bottom: 2rem; -} - -h2 { - font-size: 3.6rem; - line-height: 1.25; - letter-spacing: -.1rem; - margin-bottom: 1.8rem; - margin-top: 1.8rem; -} - -h3 { - font-size: 3.0rem; - line-height: 1.3; - letter-spacing: -.1rem; - margin-bottom: 1.5rem; - margin-top: 1.5rem; -} - -h4 { - font-size: 2.6rem; - line-height: 1.35; - letter-spacing: -.08rem; - margin-bottom: 1.2rem; - margin-top: 1.2rem; -} - -h5 { - font-size: 2.2rem; - line-height: 1.5; - letter-spacing: -.05rem; - margin-bottom: 0.6rem; - margin-top: 0.6rem; -} - -h6 { - font-size: 2.0rem; - line-height: 1.6; - letter-spacing: 0; - margin-bottom: 0.75rem; - margin-top: 0.75rem; -} - -p { - margin-top: 0; -} - - -/* Blockquotes - –––––––––––––––––––––––––––––––––––––––––––––––––– */ -blockquote { - border-left: 4px lightgrey solid; - padding-left: 1rem; - margin-top: 2rem; - margin-bottom: 2rem; - margin-left: 0rem; -} - - -/* Links - –––––––––––––––––––––––––––––––––––––––––––––––––– */ -a { - color: #1EAEDB; - text-decoration: underline; - cursor: pointer; -} - -a:hover { - color: #0FA0CE; -} - - -/* Buttons - –––––––––––––––––––––––––––––––––––––––––––––––––– */ -.button, -button, -input[type="submit"], -input[type="reset"], -input[type="button"] { - display: inline-block; - height: 38px; - padding: 0 30px; - color: #555; - text-align: center; - font-size: 11px; - font-weight: 600; - line-height: 38px; - letter-spacing: .1rem; - text-transform: uppercase; - text-decoration: none; - white-space: nowrap; - background-color: transparent; - border-radius: 4px; - border: 1px solid #bbb; - cursor: pointer; - box-sizing: border-box; -} - -.button:hover, -button:hover, -input[type="submit"]:hover, -input[type="reset"]:hover, -input[type="button"]:hover, -.button:focus, -button:focus, -input[type="submit"]:focus, -input[type="reset"]:focus, -input[type="button"]:focus { - color: #333; - border-color: #888; - outline: 0; -} - -.button.button-primary, -button.button-primary, -input[type="submit"].button-primary, -input[type="reset"].button-primary, -input[type="button"].button-primary { - color: #FFF; - background-color: #33C3F0; - border-color: #33C3F0; -} - -.button.button-primary:hover, -button.button-primary:hover, -input[type="submit"].button-primary:hover, -input[type="reset"].button-primary:hover, -input[type="button"].button-primary:hover, -.button.button-primary:focus, -button.button-primary:focus, -input[type="submit"].button-primary:focus, -input[type="reset"].button-primary:focus, -input[type="button"].button-primary:focus { - color: #FFF; - background-color: #1EAEDB; - border-color: #1EAEDB; -} - - -/* Forms - –––––––––––––––––––––––––––––––––––––––––––––––––– */ -input[type="email"], -input[type="number"], -input[type="search"], -input[type="text"], -input[type="tel"], -input[type="url"], -input[type="password"], -textarea, -select { - height: 38px; - padding: 6px 10px; - /* The 6px vertically centers text on FF, ignored by Webkit */ - background-color: #fff; - border: 1px solid #D1D1D1; - border-radius: 4px; - box-shadow: none; - box-sizing: border-box; - font-family: inherit; - font-size: inherit; - /*https://stackoverflow.com/questions/6080413/why-doesnt-input-inherit-the-font-from-body*/ -} - -/* Removes awkward default styles on some inputs for iOS */ -input[type="email"], -input[type="number"], -input[type="search"], -input[type="text"], -input[type="tel"], -input[type="url"], -input[type="password"], -textarea { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; -} - -textarea { - min-height: 65px; - padding-top: 6px; - padding-bottom: 6px; -} - -input[type="email"]:focus, -input[type="number"]:focus, -input[type="search"]:focus, -input[type="text"]:focus, -input[type="tel"]:focus, -input[type="url"]:focus, -input[type="password"]:focus, -textarea:focus, -select:focus { - border: 1px solid #33C3F0; - outline: 0; -} - -label, -legend { - display: block; - margin-bottom: 0px; -} - -fieldset { - padding: 0; - border-width: 0; -} - -input[type="checkbox"], -input[type="radio"] { - display: inline; -} - -label>.label-body { - display: inline-block; - margin-left: .5rem; - font-weight: normal; -} - - -/* Lists - –––––––––––––––––––––––––––––––––––––––––––––––––– */ -ul { - list-style: circle inside; -} - -ol { - list-style: decimal inside; -} - -ol, -ul { - padding-left: 0; - margin-top: 0; -} - -ul ul, -ul ol, -ol ol, -ol ul { - margin: 1.5rem 0 1.5rem 3rem; - font-size: 90%; -} - -li { - margin-bottom: 1rem; -} - - -/* Tables - –––––––––––––––––––––––––––––––––––––––––––––––––– */ -table { - border-collapse: collapse; -} - -th, -td { - padding: 12px 15px; - text-align: left; - border-bottom: 1px solid #E1E1E1; -} - -th:first-child, -td:first-child { - padding-left: 0; -} - -th:last-child, -td:last-child { - padding-right: 0; -} - - -/* Spacing - –––––––––––––––––––––––––––––––––––––––––––––––––– */ -button, -.button { - margin-bottom: 0rem; -} - -input, -textarea, -select, -fieldset { - margin-bottom: 0rem; -} - -pre, -dl, -figure, -table, -form { - margin-bottom: 0rem; -} - -p, -ul, -ol { - margin-bottom: 0.75rem; -} - -/* Utilities - –––––––––––––––––––––––––––––––––––––––––––––––––– */ -.u-full-width { - width: 100%; - box-sizing: border-box; -} - -.u-max-full-width { - max-width: 100%; - box-sizing: border-box; -} - -.u-pull-right { - float: right; -} - -.u-pull-left { - float: left; -} - - -/* Misc - –––––––––––––––––––––––––––––––––––––––––––––––––– */ -hr { - margin-top: 3rem; - margin-bottom: 3.5rem; - border-width: 0; - border-top: 1px solid #E1E1E1; -} - - -/* Clearing - –––––––––––––––––––––––––––––––––––––––––––––––––– */ - -/* Self Clearing Goodness */ -.container:after, -.row:after, -.u-cf { - content: ""; - display: table; - clear: both; -} - - -/* Media Queries - –––––––––––––––––––––––––––––––––––––––––––––––––– */ -/* - Note: The best way to structure the use of media queries is to create the queries - near the relevant code. For example, if you wanted to change the styles for buttons - on small devices, paste the mobile query code up in the buttons section and style it - there. - */ - - -/* Larger than mobile */ -@media (min-width: 400px) {} - -/* Larger than phablet (also point when grid becomes active) */ -@media (min-width: 550px) {} - -/* Larger than tablet */ -@media (min-width: 750px) {} - -/* Larger than desktop */ -@media (min-width: 1000px) {} - -/* Larger than Desktop HD */ -@media (min-width: 1200px) {} \ No newline at end of file diff --git a/apps/dash-brain-viewer/assets/images/brain.png b/apps/dash-brain-viewer/assets/github/brain.png similarity index 100% rename from apps/dash-brain-viewer/assets/images/brain.png rename to apps/dash-brain-viewer/assets/github/brain.png diff --git a/apps/dash-brain-viewer/assets/style.css b/apps/dash-brain-viewer/assets/style.css deleted file mode 100644 index 4c02e09a1..000000000 --- a/apps/dash-brain-viewer/assets/style.css +++ /dev/null @@ -1,184 +0,0 @@ -body { - background-color: #141414; - color: #F4F6F8; - font-family: "Open Sans", sans-serif; - margin: 0; -} - -.pb-20 { - padding-bottom: 20px; -} - -.red-ish { - color: #BA2456; -} - -a:hover { - opacity: 0.5; -} - -.header { - display: flex; - flex-direction: column; -} - -.header__title { - display: flex; - flex-direction: row; - align-items: center; - margin-top: 50px; -} - -.header img { - height: 40px; - width: auto; -} - -.header p { - padding: 10px 0px; -} - -.header h4 { - font-family: "serif"; - margin: 5px 0px 0px 20px; -} - -.header a { - padding: 10px 15px; - background: transparent; - color: #F9FAFB; - text-decoration: none; - border-radius: 0.35rem; - border: 1px solid #F9FAFB; - ; - margin: 15px 10px 0px 0px; -} - -.graph__container { - display: flex; - justify-content: center; -} - - -.app__right__section { - background-color: #1D1D1D; - min-height: 100vh; - max-height: 100vh; - overflow-y: scroll; - overflow: scroll; - padding: 25px; - border-left: black solid 5px; -} - -.app__right__section::-webkit-scrollbar-thumb { - border-radius: 5px; - background-color: #C4CDD5; - --webkit-box-shadow: 0 0 1px #C4CDD5; -} - -.app__right__section::-webkit-scrollbar { - --webkit-appearance: none; - width: 10px; -} - -.app__right__section::-webkit-scrollbar-corner { - background: rgba(0, 0, 0, 0); -} - -.subheader { - font-size: 1.2em; - color: #DFE3E8; -} - -.colorscale-block { - margin: 0 !important; - border: 1px solid #C4CDD5; - padding: 5px 5px 0px 5px; - border-radius: 0.5rem; -} - -.colorscale-block div { - margin: 0 !important; -} - -.label__option { - display: inline-block; - padding-right: 15px; -} - -.input__option { - margin-right: 8px; -} - -.small-text { - font-size: 1em; - color: #919EAB; -} - -.info__container { - background-color: #292929; - color: white; - padding: 25px; -} - -.colorscalePickerContainer { - background: #F4F5FA !important; -} - -#brain-graph { - user-select: none; - margin: auto; - height: 60vh; -} - -@media all and (max-width: 768px) { - .header__title { - display: block; - margin-top: 25px; - } - - .header__title h4 { - text-align: center; - } - - .header__title img { - display: flex; - height: 30px; - width: auto; - padding-bottom: 10px; - } - - .header__info p { - text-align: center; - font-size: 14px; - } - - .header__button { - text-align: center; - } - - .app__right__section { - border: none; - } - - .graph__container { - padding-top: 50px; - padding-bottom: 50px; - } - - .app__right__section { - overflow-y: hidden; - overflow: hidden; - min-height: initial; - max-height: initial; - } - - #radio-options { - font-size: 11px; - } - - #brain-graph { - height: 350px; - width: 350px; - } -} \ No newline at end of file diff --git a/apps/dash-brain-viewer/constants.py b/apps/dash-brain-viewer/constants.py index 553569190..730bd463e 100644 --- a/apps/dash-brain-viewer/constants.py +++ b/apps/dash-brain-viewer/constants.py @@ -1,13 +1,4 @@ -from mni import create_mesh_data, default_colorscale -import os - - -GITHUB_LINK = os.environ.get( - "GITHUB_LINK", - "https://github.com/plotly/dash-sample-apps/tree/master/apps/dash-brain-viewer", -) - -default_colorscale_index = [ea[1] for ea in default_colorscale] +import pathlib axis_template = { "showbackground": True, @@ -32,3 +23,15 @@ "annotations": [], }, } + +DATA_PATH = pathlib.Path(__file__).parent.joinpath("data").resolve() + +default_colorscale = [ + [0, "rgb(12,51,131)"], + [0.25, "rgb(10,136,186)"], + [0.5, "rgb(242,211,56)"], + [0.75, "rgb(242,143,56)"], + [1, "rgb(217,30,30)"], +] + +default_colorscale_index = [ea[1] for ea in default_colorscale] \ No newline at end of file diff --git a/apps/dash-brain-viewer/requirements.txt b/apps/dash-brain-viewer/requirements.txt index f13f60bd7..15c5242ae 100644 --- a/apps/dash-brain-viewer/requirements.txt +++ b/apps/dash-brain-viewer/requirements.txt @@ -1,4 +1,5 @@ dash==2.4.1 +dash-bootstrap-components==1.1.0 pandas==1.4.2 gunicorn==20.1.0 dash-colorscales==0.0.4 \ No newline at end of file diff --git a/apps/dash-brain-viewer/utils/components.py b/apps/dash-brain-viewer/utils/components.py index 8817185a4..89b1b5bf8 100644 --- a/apps/dash-brain-viewer/utils/components.py +++ b/apps/dash-brain-viewer/utils/components.py @@ -1,18 +1,17 @@ from dash import html, dcc -from mni import create_mesh_data -from constants import default_colorscale_index, plot_layout, GITHUB_LINK import dash_colorscales as dcs +from utils.model import create_mesh_data +from constants import default_colorscale_index, plot_layout -def header( - app, header_color, header, subheader=None, header_background_color="transparent" -): + +def header(app, header_color, header, subheader=None, header_background_color="transparent"): left_headers = html.Div( [ html.Div(header, className="header-title"), html.Div(subheader, className="subheader-title"), ], - style={"color": header_color}, + style={"color": header_color} ) logo = html.Img(src=app.get_asset_url("images/plotly-logo-dark-theme.png")) @@ -25,116 +24,75 @@ def header( ) right_logos = html.Div([demo_link, logo_link], className="header-logos") - return html.Div( - [left_headers, right_logos], - className="header", - style={"background-color": header_background_color}, - ) + return html.Div([left_headers, right_logos], className="header", style={"background-color": header_background_color}) def brain_graph(brain_graph_id): - return html.Div( - [ - dcc.Graph( - id=brain_graph_id, - figure={ - "data": create_mesh_data("human_atlas"), - "layout": plot_layout, - }, - config={"editable": True, "scrollZoom": False}, - ) - ], - className="graph__container", + return dcc.Graph( + id=brain_graph_id, + figure={ + "data": create_mesh_data("human_atlas"), + "layout": plot_layout, + }, + config={"editable": True, "scrollZoom": False}, ) -def color_picker(color_picker_id): - return html.Div( - [ - html.Div( - [ - html.Div( - [ - html.P("Click colorscale to change", className="subheader"), - dcs.DashColorscales( - id=color_picker_id, - colorscale=default_colorscale_index, - ), - ] - ) - ], - className="colorscale pb-20", - ), - html.Div( - [ - html.P("Select option", className="subheader"), - dcc.RadioItems( - options=[ - {"label": "Brain Atlas", "value": "human_atlas"}, - {"label": "Cortical Thickness", "value": "human"}, - {"label": "Mouse Brain", "value": "mouse"}, - ], - value="human_atlas", - id="radio-options", - labelClassName="label__option", - inputClassName="input__option", - ), - ], - className="pb-20", - ), - html.Div( - [ - html.Span("Click data", className="subheader"), - html.Span(" | "), - html.Span("Click on points in the graph.", className="small-text"), - dcc.Loading( - html.Pre(id="click-data", className="info__container"), - type="dot", - ), - ], - className="pb-20", - ), - html.Div( - [ - html.Span("Relayout data", className="subheader"), - html.Span(" | "), - html.Span( - "Drag the graph corners to rotate it.", - className="small-text", - ), - dcc.Loading( - html.Pre(id="relayout-data", className="info__container"), - type="dot", - ), - ], - className="pb-20", - ), - html.Div( - [ - html.P( - [ - "Dash/Python code on ", - html.A( - children="GitHub.", - target="_blank", - href=GITHUB_LINK, - className="red-ish", - ), - ] - ), - html.P( - [ - "Brain data from Mcgill's ACE Lab ", - html.A( - children="Surface Viewer.", - target="_blank", - href="https://brainbrowser.cbrain.mcgill.ca/surface-viewer#ct", - className="red-ish", - ), - ] - ), - ] - ), - ], - className="one-third column app__right__section", - ) +def control_and_output(): + return [ + html.Div( + [ + html.P("Click colorscale to change"), + dcs.DashColorscales( + id="colorscale-picker", + colorscale=default_colorscale_index, + ), + ] + ), + html.Div( + [ + html.P("Select option"), + dcc.RadioItems( + options=[ + {"label": "Brain Atlas", "value": "human_atlas"}, + {"label": "Cortical Thickness", "value": "human"}, + {"label": "Mouse Brain", "value": "mouse"}, + ], + value="human_atlas", + id="radio-options", + ), + ], + ), + html.Div( + [ + html.Span("Click data"), + html.Span(" | "), + html.Span("Click on points in the graph."), + dcc.Loading( + html.Pre(id="click-data"), + type="dot", + ), + ], + ), + html.Div( + [ + html.Span("Relayout data"), + html.Span(" | "), + html.Span("Drag the graph corners to rotate it."), + dcc.Loading( + html.Pre(id="relayout-data"), + type="dot", + ), + ], + ), + html.P( + [ + "Brain data from Mcgill's ACE Lab ", + html.A( + children="Surface Viewer.", + target="_blank", + href="https://brainbrowser.cbrain.mcgill.ca/surface-viewer#ct", + ), + ] + ), + ] diff --git a/apps/dash-brain-viewer/utils/figures.py b/apps/dash-brain-viewer/utils/figures.py index affe9958a..a9001ad67 100644 --- a/apps/dash-brain-viewer/utils/figures.py +++ b/apps/dash-brain-viewer/utils/figures.py @@ -1,6 +1,7 @@ from utils.helper_functions import add_marker, add_annotation, marker_in_points from dash.exceptions import PreventUpdate -from mni import create_mesh_data + +from utils.model import create_mesh_data from constants import plot_layout diff --git a/apps/dash-brain-viewer/mni.py b/apps/dash-brain-viewer/utils/model.py similarity index 93% rename from apps/dash-brain-viewer/mni.py rename to apps/dash-brain-viewer/utils/model.py index d5b90878e..2b71831b6 100644 --- a/apps/dash-brain-viewer/mni.py +++ b/apps/dash-brain-viewer/utils/model.py @@ -1,16 +1,6 @@ -import pathlib import numpy as np - -DATA_PATH = pathlib.Path(__file__).parent.joinpath("data").resolve() - -default_colorscale = [ - [0, "rgb(12,51,131)"], - [0.25, "rgb(10,136,186)"], - [0.5, "rgb(242,211,56)"], - [0.75, "rgb(242,143,56)"], - [1, "rgb(217,30,30)"], -] +from constants import DATA_PATH, default_colorscale def read_mniobj(file):