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
-
+
### 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):