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..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](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 e1b4f1c0a..e2c3b6303 100644 --- a/apps/dash-brain-viewer/app.py +++ b/apps/dash-brain-viewer/app.py @@ -1,366 +1,62 @@ -import os +from dash import Dash, dcc, Input, Output, State, callback +import dash_bootstrap_components as dbc import json -import dash -import dash_core_components as dcc -import dash_html_components as html -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 +import utils.figures as figs +from utils.components import header, brain_graph, control_and_output -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 -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( +app.layout = dbc.Container( [ - 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", - ), - ], - 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", + 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.", ), - 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", - ), + brain_graph("brain-graph"), + ], width=8 + ), + dbc.Col([ + dbc.Card(control_and_output(), className="right-card"), + + ], width=4) + ]), + 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( - Output("brain-graph", "figure"), - [ - Input("brain-graph", "clickData"), - Input("radio-options", "value"), - Input("colorscale-picker", "colorscale"), ], - [State("brain-graph", "figure"), State("annotation_storage", "data")], + fluid=True ) -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 - - # 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")]) -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("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 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 -@app.callback( +@callback( + Output("relayout-data", "children"), Output("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 + Input("brain-graph", "relayoutData"), + 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 new file mode 100644 index 000000000..f6fbc52dc --- /dev/null +++ b/apps/dash-brain-viewer/assets/css/app.css @@ -0,0 +1,131 @@ +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; + display: flex; + padding-left: 2%; + padding-right: 2%; + font-family: playfair display, sans-serif; +} + +.header .header-title { + font-size: 5vh; + font-weight: bold; +} + +.subheader-title { + font-size: 1.5vh; +} + +.header-logos { + margin-left: auto; + align-self: center !important; +} + +.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 eb700fc71..000000000 Binary files a/apps/dash-brain-viewer/assets/dash-logo.png and /dev/null differ diff --git a/apps/dash-brain-viewer/assets/default.css b/apps/dash-brain-viewer/assets/default.css deleted file mode 100644 index 231a9d262..000000000 --- a/apps/dash-brain-viewer/assets/default.css +++ /dev/null @@ -1,414 +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/brain.png b/apps/dash-brain-viewer/assets/github/brain.png similarity index 100% rename from apps/dash-brain-viewer/brain.png rename to apps/dash-brain-viewer/assets/github/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 000000000..984dd57ab Binary files /dev/null and b/apps/dash-brain-viewer/assets/images/plotly-logo-dark-theme.png differ diff --git a/apps/dash-brain-viewer/assets/style.css b/apps/dash-brain-viewer/assets/style.css deleted file mode 100644 index a4e616d5f..000000000 --- a/apps/dash-brain-viewer/assets/style.css +++ /dev/null @@ -1,174 +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 new file mode 100644 index 000000000..730bd463e --- /dev/null +++ b/apps/dash-brain-viewer/constants.py @@ -0,0 +1,37 @@ +import pathlib + +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": [], + }, +} + +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/gitignore b/apps/dash-brain-viewer/gitignore new file mode 100644 index 000000000..d8e187da3 --- /dev/null +++ b/apps/dash-brain-viewer/gitignore @@ -0,0 +1,191 @@ +# .gitignore specifies the files that shouldn't be included +# in version control and therefore shouldn't be included when +# deploying an application to Dash Enterprise +# This is a very exhaustive list! +# This list was based off of https://github.com/github/gitignore + +# Ignore data that is generated during the runtime of an application +# This folder is used by the "Large Data" sample applications +runtime_data/ +data/ + +# Omit SQLite databases that may be produced by dash-snapshots in development +*.db + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + + +# Jupyter Notebook + +.ipynb_checkpoints +*/.ipynb_checkpoints/* + +# IPython +profile_default/ +ipython_config.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + + +# macOS General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# History files +.Rhistory +.Rapp.history + +# Session Data files +.RData + +# User-specific files +.Ruserdata + +# Example code in package build process +*-Ex.R + +# Output files from R CMD check +/*.Rcheck/ + +# RStudio files +.Rproj.user/ + +# produced vignettes +vignettes/*.html +vignettes/*.pdf + +# OAuth2 token, see https://github.com/hadley/httr/releases/tag/v0.3 +.httr-oauth + +# knitr and R markdown default cache directories +*_cache/ +/cache/ + +# Temporary files created by R markdown +*.utf8.md +*.knit.md + +# R Environment Variables +.Renviron + +# Linux +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# SublineText +# Cache files for Sublime Text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# Workspace files are user-specific +*.sublime-workspace + +# Project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using Sublime Text +# *.sublime-project + +# SFTP configuration file +sftp-config.json + +# Package control specific files +Package Control.last-run +Package Control.ca-list +Package Control.ca-bundle +Package Control.system-ca-bundle +Package Control.cache/ +Package Control.ca-certs/ +Package Control.merged-ca-bundle +Package Control.user-ca-bundle +oscrypto-ca-bundle.crt +bh_unicode_properties.cache + +# Sublime-github package stores a github token in this file +# https://packagecontrol.io/packages/sublime-github +GitHub.sublime-settings \ No newline at end of file diff --git a/apps/dash-brain-viewer/requirements.txt b/apps/dash-brain-viewer/requirements.txt index 85809d0b5..15c5242ae 100644 --- a/apps/dash-brain-viewer/requirements.txt +++ b/apps/dash-brain-viewer/requirements.txt @@ -1,4 +1,5 @@ -dash==1.0.2 -gunicorn==19.9.0 -pandas==0.24.2 +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/runtime.txt b/apps/dash-brain-viewer/runtime.txt new file mode 100644 index 000000000..cfa660c42 --- /dev/null +++ b/apps/dash-brain-viewer/runtime.txt @@ -0,0 +1 @@ +python-3.8.0 \ No newline at end of file diff --git a/apps/dash-brain-viewer/utils/components.py b/apps/dash-brain-viewer/utils/components.py new file mode 100644 index 000000000..89b1b5bf8 --- /dev/null +++ b/apps/dash-brain-viewer/utils/components.py @@ -0,0 +1,98 @@ +from dash import html, dcc +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"): + left_headers = html.Div( + [ + html.Div(header, className="header-title"), + html.Div(subheader, className="subheader-title"), + ], + style={"color": header_color} + ) + + logo = html.Img(src=app.get_asset_url("images/plotly-logo-dark-theme.png")) + logo_link = html.A(logo, href="https://plotly.com/get-demo/", target="_blank") + demo_link = html.A( + "LEARN MORE", + href="https://plotly.com/dash/", + target="_blank", + className="demo-button", + ) + 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}) + + +def brain_graph(brain_graph_id): + return dcc.Graph( + id=brain_graph_id, + figure={ + "data": create_mesh_data("human_atlas"), + "layout": plot_layout, + }, + config={"editable": True, "scrollZoom": False}, + ) + + +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 new file mode 100644 index 000000000..a9001ad67 --- /dev/null +++ b/apps/dash-brain-viewer/utils/figures.py @@ -0,0 +1,82 @@ +from utils.helper_functions import add_marker, add_annotation, marker_in_points +from dash.exceptions import PreventUpdate + +from utils.model import create_mesh_data +from constants import plot_layout + + +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 + + # 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 + + +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 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):