diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..905bf08 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.DS_Store +__pycache__ +cache +assets/__generated_theme.css diff --git a/app.py b/app.py new file mode 100644 index 0000000..5bd63cc --- /dev/null +++ b/app.py @@ -0,0 +1,90 @@ +# Copyright 2024 D-Wave +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import argparse + +import dash +import diskcache +from dash import DiskcacheManager + +from demo_configs import APP_TITLE, THEME_COLOR, THEME_COLOR_SECONDARY +from demo_interface import create_interface + +# Essential for initializing callbacks. Do not remove. +import demo_callbacks + +# Fix Dash long callbacks crashing on macOS 10.13+ (also potentially not working +# on other POSIX systems), caused by https://bugs.python.org/issue33725 +# (aka "beware of multithreaded process forking"). +# +# Note: default start method has already been changed to "spawn" on darwin in +# the `multiprocessing` library, but its fork, `multiprocess` still hasn't caught up. +# (see docs: https://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methods) +import multiprocess + +if multiprocess.get_start_method(allow_none=True) is None: + multiprocess.set_start_method("spawn") + +cache = diskcache.Cache("./cache") +background_callback_manager = DiskcacheManager(cache) + +app = dash.Dash( + __name__, + meta_tags=[{"name": "viewport", "content": "width=device-width, initial-scale=1"}], + prevent_initial_callbacks="initial_duplicate", + background_callback_manager=background_callback_manager, +) +app.title = APP_TITLE + +app.config.suppress_callback_exceptions = True + +# Parse debug argument +parser = argparse.ArgumentParser(description="Dash debug setting.") +parser.add_argument( + "--debug", + action="store_true", + help="Add argument to see Dash debug menu and get live reload updates while developing.", +) + +args = parser.parse_args() +DEBUG = args.debug + +print(f"\nDebug has been set to: {DEBUG}") +if not DEBUG: + print( + "The app will not show live code updates and the Dash debug menu will be hidden.", + "If editting code while the app is running, run the app with `python app.py --debug`.\n", + sep="\n", + ) + +# Generates css file and variable using THEME_COLOR and THEME_COLOR_SECONDARY settings +css = f"""/* Automatically generated theme settings css file, see app.py */ +:root {{ + --theme: {THEME_COLOR}; + --theme-secondary: {THEME_COLOR_SECONDARY}; +}} +""" +with open("assets/__generated_theme.css", "w") as f: + f.write(css) + + +if __name__ == "__main__": + # Imports the Dash HTML code and sets it in the app. + # Creates the visual layout and app (see `demo_interface.py`) + app.layout = create_interface() + + # Run the server + app.run_server(debug=DEBUG) diff --git a/assets/__base.css b/assets/__base.css new file mode 100644 index 0000000..1eaa980 --- /dev/null +++ b/assets/__base.css @@ -0,0 +1,415 @@ +/* +This file is forked from apps/dash-clinical-analytics/assets/base.css +under the following license + +MIT License + +Copyright (c) 2019 Plotly + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Modifications are licensed under + +Apache License, Version 2.0 +(see ./LICENSE for details) + +*/ + +/* 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: 550px) { + .container { + width: 80%; } + .column, + .columns { + margin-left: 2%; } + .column:first-child, + .columns:first-child { + margin-left: 1%; } + + .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="button"] { + display: inline-block; + height: 38px; + padding: 0 30px; + color: white; + 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; + background-color: #2a7de1 ; + } + .button:hover, + button:hover, + + input[type="button"]:hover, + .button:focus, + button:focus, + input[type="submit"]:focus, + input[type="reset"]:focus, + input[type="button"]:focus { + color: white; + border-color: #888; + outline: 0; + background-color: #074c91; + + } + .button.button-primary, + button.button-primary, + input[type="submit"].button-primary, + input[type="reset"].button-primary, + input[type="button"].button-primary { + color: #EEEEEE; + background-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) {} diff --git a/assets/__demo_variables.css b/assets/__demo_variables.css new file mode 100644 index 0000000..372d7dc --- /dev/null +++ b/assets/__demo_variables.css @@ -0,0 +1,45 @@ +/* +Copyright 2024 D-Wave + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Variable declarations for all CSS files. +Some additional variables are generated automatically in app.py +and stored in __generated_theme.css. + +Dash reads all css files contained in `/assets/` so no imports are necessary. +*/ + +:root { + --blue-bright: #03B8FF; + --blue-light: #2A7DE1; + --blue-dark: #074C91; + --red-light: #C70039; + --red-dark: #900C3F; + --orange: #FF7006; + --teal-light: #06ECDC; + --teal-dark: #17BEBB; + --teal-darker: #008C82; + --grey-lighter: #EEEEEE; + --grey-light: #DDDDDD; + --grey: #AAAAAA; + --grey-medium: #777777; + --grey-dark: #222222; + --font: "Helvetica Neue", sans-serif; + --banner-height: 5.5rem; + --left-col-width: 42rem; + --problem-details-height: 8rem; + /*** Add new variables here ***/ +} diff --git a/assets/__style_guide.css b/assets/__style_guide.css new file mode 100644 index 0000000..f949118 --- /dev/null +++ b/assets/__style_guide.css @@ -0,0 +1,168 @@ +/* +Copyright 2024 D-Wave + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* A custom base of style rules defined specifically for the demos */ + +body { + background-color: #f9f9f9; + color: var(--grey-dark); + font-size: 1.4rem; + margin: 0; +} + +h1, h2, h3, h4, h5, h6, td, th, +span, a, p, label, button, input { + font-family: var(--font); +} + +h1, h2, h3, h4, h5, h6 { + color: var(--theme); + font-weight: 400; +} + +h1 { + font-size: 3.2rem; +} + +h2 { + font-size: 2.6rem; +} + +h3 { + font-size: 2.4rem; +} + +h4 { + font-size: 2rem; + margin-top: 0; + font-weight: 500; +} + +h5 { + font-size: 1.8rem; + font-weight: 600; +} + +label { + margin: 2rem 0 0.2rem; + font-weight: 500; +} + +hr { + margin: 1rem 0; +} + +table { + margin-bottom: 2rem; + font-size: 1.5rem; + border: 1px solid var(--grey-light); +} + +th, td { + color: var(--grey-dark); + padding: 0.5rem 1rem; + border-right: 1px solid var(--grey-light); +} + +th:first-child, td:first-child { + padding-left: 1.2rem; +} + +th:last-child, td:last-child { + padding-right: 1.2rem; +} + +input[type="checkbox"], +input[type="radio"] { + accent-color: var(--theme); + margin: 0 0.6rem 0 0; +} + +.display-none { + display: none; +} + +#app-container { + min-width: 100rem; + height: 100vh; + display: flex; + flex-direction: column; +} + +.banner { + height: var(--banner-height); + box-sizing: border-box; + background-color: var(--theme); + padding: 1.4rem 2rem; + + display: flex; + align-items: center; + justify-content: space-between; +} + +.banner img { + height: 100%; +} + +.columns-main { + display: flex; + flex: 1; +} + +.left-column { + display: flex; + height: calc(100vh - var(--banner-height)); +} + +.right-column { + background-color: var(--grey-light); + padding: 2rem 0 0; + width: 100%; +} + +.settings { + margin: 2rem 0; +} + +.radio label, +.checklist label { + margin-top: 0.2rem; + font-weight: 400; +} + +.radio--inline label:first-child, +.checklist--inline label:first-child { + margin-right: 1.4rem; +} + +/* The following rules are overwriting Dash styling */ +.VirtualizedSelectFocusedOption { + background-color: var(--grey-lighter); +} + +.is-focused:not(.is-open) > .Select-control { + border-color: var(--theme-secondary); + box-shadow: none; +} + +.Select { + font-family: var(--font); +} + +div.dash-sk-circle { + height: 6rem; + width: 6rem; +} diff --git a/assets/_buttons.css b/assets/_buttons.css new file mode 100644 index 0000000..46bb2a7 --- /dev/null +++ b/assets/_buttons.css @@ -0,0 +1,46 @@ +/* +Copyright 2024 D-Wave + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* Style rules for buttons */ + +button { + font-size: 1.4rem; + line-height: 1.4rem; + padding: 1.8rem 3.6rem; + height: auto; + color: white; + transition: all 0.2s ease-in-out; + border: none; + background-color: var(--theme); +} + +button:hover { + filter: brightness(80%); + color: white; +} + +#run-button, +#cancel-button { + width: 100%; +} + +#cancel-button { + background-color: var(--red-light); +} + +#cancel-button:hover { + background-color: var(--red-dark); +} diff --git a/assets/_collapse.css b/assets/_collapse.css new file mode 100644 index 0000000..13666f5 --- /dev/null +++ b/assets/_collapse.css @@ -0,0 +1,108 @@ +/* +Copyright 2024 D-Wave + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* Style rules for collapsible dropdowns like left-column-collapse and problem-details-collapse */ + +.left-column .left-column-layer-1 { + width: var(--left-col-width); + transition: width 0.6s ease-in-out; + overflow-x: hidden; + overflow-y: auto; + direction: rtl; +} + +.details-to-collapse { + height: var(--problem-details-height); + transition: height 0.6s ease-in-out; + overflow: hidden; +} + +.left-column .left-column-layer-2 { + padding: 3rem 2rem 4rem; + width: var(--left-col-width); + box-sizing: border-box; + direction: ltr; +} + +.details-collapse-wrapper { + margin-bottom: 2rem; + overflow: hidden; +} + +.left-column-collapse, +.left-column-collapse:hover, +.left-column-collapse:focus { + background: white; + border-right: 1px solid var(--grey-lighter); + height: 100%; + border-radius: 0; + padding: 0 0 0 0.5rem; + filter: none; +} + +.details-collapse, +.details-collapse:hover, +.details-collapse:focus { + background: none; + display: flex; + padding: 0 2rem 0 0; + text-transform: none; +} + +.collapse-arrow { + border-right: 4px solid var(--grey-light); + border-bottom: 4px solid var(--grey-light); + transform: rotate(135deg) skew(165deg, 165deg); + height: 2rem; + width: 2rem; + margin-right: -0.3rem; + transition: border-color 0.25s ease-in-out; +} + +.details-collapse .collapse-arrow { + transform: rotate(225deg) skew(165deg, 165deg); + margin: 1.5rem 0 0 1.5rem; + border-color: var(--theme); + height: 1.2rem; + width: 1.2rem; +} + +.left-column-collapse:hover .collapse-arrow { + border-color: var(--grey); +} + +.collapsed .left-column-collapse { + padding: 0 0.5rem 0 0; +} + +.collapsed .left-column-collapse .collapse-arrow { + margin-left: -0.3rem; + margin-right: 0; + transform: rotate(315deg) skew(165deg, 165deg); +} + +.collapsed .details-collapse .collapse-arrow { + margin-top: 1rem; + transform: rotate(45deg) skew(165deg, 165deg); +} + +.collapsed .details-to-collapse { + height: 0; +} + +.collapsed .left-column-layer-1 { + width: 0; +} diff --git a/assets/_slider.css b/assets/_slider.css new file mode 100644 index 0000000..9b04378 --- /dev/null +++ b/assets/_slider.css @@ -0,0 +1,98 @@ +/* +Copyright 2024 D-Wave + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* Style rules for the dcc.Slider element */ + +.rc-slider-tooltip-inner { + font-family: var(--font); +} + +.slider { + padding-left: 6px !important; + padding-right: 8px !important; +} + +.rc-slider-handle, +.rc-slider-dot-active { + border-color: var(--theme-secondary); + transition: filter 0.1s ease-in-out; +} + +.rc-slider-handle:active { + box-shadow: 0 0 5px var(--theme-secondary); +} + +.rc-slider-handle:active, +.rc-slider-handle:hover, +.rc-slider-handle-click-focused, +.rc-slider-handle-click-focused:focus { + border-color: var(--theme-secondary); + filter: brightness(80%) +} + +.rc-slider-track { + background-color: var(--theme-secondary); +} + +.rc-slider-tooltip-inner { + background-color: white; + color: var(--grey-dark); + border: 1px solid var(--grey); + border-radius: 2px; +} + +.rc-slider-tooltip { + pointer-events: none; +} + +.rc-slider-tooltip .rc-slider-tooltip-arrow { + display: none; +} + +.rc-slider-tooltip-inner:before, +.rc-slider-tooltip-inner:after { + content: ''; + display: block; + position: absolute; + width: 0; + height: 0; +} + +.rc-slider-tooltip-inner:before { + left: 6px; + top: -11px; + border: 6px solid transparent; + border-bottom-color: var(--grey); +} + +.rc-slider-tooltip-inner:after { + left: 7px; + top: -9px; + border: 5px solid transparent; + border-bottom-color: white; +} + +.rc-slider-tooltip-placement-top .rc-slider-tooltip-inner:before { + bottom: -3px; + top: auto; + transform: rotate(180deg); +} + +.rc-slider-tooltip-placement-top .rc-slider-tooltip-inner:after { + bottom: -1px; + top: auto; + transform: rotate(180deg); +} diff --git a/assets/_tabs.css b/assets/_tabs.css new file mode 100644 index 0000000..cec67b3 --- /dev/null +++ b/assets/_tabs.css @@ -0,0 +1,60 @@ +/* +Copyright 2024 D-Wave + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* Style rules for tabs like the input and results tabs */ + +.tab-container { + border-bottom: 3px solid var(--theme-secondary); + flex-direction: row; +} + +.tab { + margin: 0 2rem; + border-top-left-radius: 0.6rem; + border-top-right-radius: 0.6rem; +} + +div.tab.tab--selected { + border: 3px solid var(--theme-secondary) !important; + border-bottom: none !important; + cursor: default; + box-shadow: 0 6px 0 -3px white; +} + +div.tab.tab--disabled { + cursor: not-allowed !important; +} + +.tab:first-child { + margin-right: 1rem; +} + +.tab:last-child { + margin-left: 1rem; +} + +.tab-content, +.tab-parent { + height: 100%; +} + +.tab-content > div:not(.tab-content-results) { + height: inherit; +} + +.tab-content { + background-color: white; +} diff --git a/assets/demo.css b/assets/demo.css new file mode 100644 index 0000000..7b9648a --- /dev/null +++ b/assets/demo.css @@ -0,0 +1,48 @@ +/* +Copyright 2024 D-Wave + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/******************************************************************* +This file contains all custom CSS for this demo. +The following style rules can be altered or removed and any additional +custom CSS can be added below. +*******************************************************************/ + +.tab-content-results { + padding: 0 3rem; + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; +} + +.solution-stats-table td:nth-child(2n) { + text-align: right; +} + +.solution-stats-table td:nth-child(2n+1) { + border-right: none; +} + +.input, .results { + display: flex; + justify-content: center; + align-items: center; + height: 100%; +} + + /****************************/ + /*** CUSTOM CSS GOES HERE ***/ +/****************************/ diff --git a/assets/favicon.ico b/assets/favicon.ico new file mode 100644 index 0000000..b672205 Binary files /dev/null and b/assets/favicon.ico differ diff --git a/demo_callbacks.py b/demo_callbacks.py new file mode 100644 index 0000000..7cb1e39 --- /dev/null +++ b/demo_callbacks.py @@ -0,0 +1,168 @@ +# Copyright 2024 D-Wave +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import NamedTuple, Union + +import dash +from dash import MATCH, ctx +from dash.dependencies import Input, Output, State +from dash.exceptions import PreventUpdate + +from demo_interface import generate_problem_details_table_rows +from src.demo_enums import SolverType + + +@dash.callback( + Output({"type": "to-collapse-class", "index": MATCH}, "className"), + inputs=[ + Input({"type": "collapse-trigger", "index": MATCH}, "n_clicks"), + State({"type": "to-collapse-class", "index": MATCH}, "className"), + ], + prevent_initial_call=True, +) +def toggle_left_column(collapse_trigger: int, to_collapse_class: str) -> str: + """Toggles a 'collapsed' class that hides and shows some aspect of the UI. + + Args: + collapse_trigger (int): The (total) number of times a collapse button has been clicked. + to_collapse_class (str): Current class name of the thing to collapse, 'collapsed' if not + visible, empty string if visible. + + Returns: + str: The new class name of the thing to collapse. + """ + + classes = to_collapse_class.split(" ") if to_collapse_class else [] + if "collapsed" in classes: + classes.remove("collapsed") + return " ".join(classes) + return to_collapse_class + " collapsed" if to_collapse_class else "collapsed" + + +@dash.callback( + Output("input", "children"), + inputs=[ + Input("slider", "value"), + ], +) +def render_initial_state(slider_value: int) -> str: + """Runs on load and any time the value of the slider is updated. + Add `prevent_initial_call=True` to skip on load runs. + + Args: + slider_value: The value of the slider. + + Returns: + str: The content of the input tab. + """ + return f"Put demo input here. The current slider value is {slider_value}." + + +class RunOptimizationReturn(NamedTuple): + """Return type for the ``run_optimization`` callback function.""" + + results: str = dash.no_update + problem_details_table: list = dash.no_update + # Add more return variables here. Return values for callback functions + # with many variables should be returned as a NamedTuple for clarity. + + +@dash.callback( + # The Outputs below must align with `RunOptimizationReturn`. + Output("results", "children"), + Output("problem-details", "children"), + background=True, + inputs=[ + # The first string in the Input/State elements below must match an id in demo_interface.py + # Remove or alter the following id's to match any changes made to demo_interface.py + Input("run-button", "n_clicks"), + State("solver-type-select", "value"), + State("solver-time-limit", "value"), + State("slider", "value"), + State("dropdown", "value"), + State("checklist", "value"), + State("radio", "value"), + ], + running=[ + (Output("cancel-button", "className"), "", "display-none"), # Show/hide cancel button. + (Output("run-button", "className"), "display-none", ""), # Hides run button while running. + (Output("results-tab", "disabled"), True, False), # Disables results tab while running. + (Output("results-tab", "label"), "Loading...", "Results"), + (Output("tabs", "value"), "input-tab", "input-tab"), # Switch to input tab while running. + (Output("run-in-progress", "data"), True, False), # Can block certain callbacks. + ], + cancel=[Input("cancel-button", "n_clicks")], + prevent_initial_call=True, +) +def run_optimization( + # The parameters below must match the `Input` and `State` variables found + # in the `inputs` list above. + run_click: int, + solver_type: Union[SolverType, int], + time_limit: float, + slider_value: int, + dropdown_value: int, + checklist_value: list, + radio_value: int, +) -> RunOptimizationReturn: + """Runs the optimization and updates UI accordingly. + + This is the main function which is called when the ``Run Optimization`` button is clicked. + This function takes in all form values and runs the optimization, updates the run/cancel + buttons, deactivates (and reactivates) the results tab, and updates all relevant HTML + components. + + Args: + run_click: The (total) number of times the run button has been clicked. + solver_type: The solver to use for the optimization run defined by SolverType in demo_enums.py. + time_limit: The solver time limit. + slider_value: The value of the slider. + dropdown_value: The value of the dropdown. + checklist_value: A list of the values of the checklist. + radio_value: The value of the radio. + + Returns: + A NamedTuple (RunOptimizationReturn) containing all outputs to be used when updating the HTML + template (in ``demo_interface.py``). These are: + + results: The results to display in the results tab. + problem-details: List of the table rows for the problem details table. + """ + + # Only run optimization code if this function was triggered by a click on `run-button`. + # Setting `Input` as exclusively `run-button` and setting `prevent_initial_call=True` + # also accomplishes this. + if run_click == 0 or ctx.triggered_id != "run-button": + raise PreventUpdate + + solver_type = SolverType(solver_type) + + + ########################### + ### YOUR CODE GOES HERE ### + ########################### + + + # Generates a list of table rows for the problem details table. + problem_details_table = generate_problem_details_table_rows( + solver=solver_type.label, + time_limit=time_limit, + ) + + return RunOptimizationReturn( + results="Put demo results here.", + problem_details_table=problem_details_table, + ) diff --git a/demo_configs.py b/demo_configs.py new file mode 100644 index 0000000..493e61f --- /dev/null +++ b/demo_configs.py @@ -0,0 +1,58 @@ +# Copyright 2024 D-Wave +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This file stores input parameters for the app.""" + +# THEME_COLOR is used for the button, text, and banner and should be dark +# and pass accessibility checks with white: https://webaim.org/resources/contrastchecker/ +# THEME_COLOR_SECONDARY can be light or dark and is used for sliders, loading icon, and tabs +THEME_COLOR = "#074C91" # D-Wave dark blue default #074C91 +THEME_COLOR_SECONDARY = "#2A7DE1" # D-Wave blue default #2A7DE1 + +THUMBNAIL = "static/dwave_logo.svg" + +APP_TITLE = "Demo Name" +MAIN_HEADER = "Demo Name" +DESCRIPTION = """\ +This is a Dash template for new examples. It includes some basic settings, tabs, and styling. +""" + +####################################### +# Sliders, buttons and option entries # +####################################### + +# an example slider +SLIDER = { + "min": 1, + "max": 10, + "step": 1, + "value": 5, +} + +# an example dropdown +DROPDOWN = ["Option 1", "Option 2"] + +# an example checklist +CHECKLIST = ["Option 1", "Option 2"] + +# an example radio list +RADIO = ["Option 1", "Option 2"] + +# solver time limits in seconds (value means default) +SOLVER_TIME = { + "min": 10, + "max": 300, + "step": 5, + "value": 10, +} diff --git a/demo_interface.py b/demo_interface.py new file mode 100644 index 0000000..cf9c9f4 --- /dev/null +++ b/demo_interface.py @@ -0,0 +1,381 @@ +# Copyright 2024 D-Wave +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This file stores the Dash HTML layout for the app.""" +from __future__ import annotations + +from dash import dcc, html + +from demo_configs import ( + CHECKLIST, + DESCRIPTION, + DROPDOWN, + MAIN_HEADER, + RADIO, + SLIDER, + SOLVER_TIME, + THEME_COLOR_SECONDARY, + THUMBNAIL, +) +from src.demo_enums import SolverType + + +def slider(label: str, id: str, config: dict) -> html.Div: + """Slider element for value selection. + + Args: + label: The title that goes above the slider. + id: A unique selector for this element. + config: A dictionary of slider configerations, see dcc.Slider Dash docs. + """ + return html.Div( + className="slider-wrapper", + children=[ + html.Label(label), + dcc.Slider( + id=id, + className="slider", + **config, + marks={ + config["min"]: str(config["min"]), + config["max"]: str(config["max"]), + }, + tooltip={ + "placement": "bottom", + "always_visible": True, + }, + ), + ], + ) + + +def dropdown(label: str, id: str, options: list) -> html.Div: + """Dropdown element for option selection. + + Args: + label: The title that goes above the dropdown. + id: A unique selector for this element. + options: A list of dictionaries of labels and values. + """ + return html.Div( + className="dropdown-wrapper", + children=[ + html.Label(label), + dcc.Dropdown( + id=id, + options=options, + value=options[0]["value"], + clearable=False, + searchable=False, + ), + ], + ) + + +def checklist(label: str, id: str, options: list, values: list, inline: bool = True) -> html.Div: + """Checklist element for option selection. + + Args: + label: The title that goes above the checklist. + id: A unique selector for this element. + options: A list of dictionaries of labels and values. + values: A list of values that should be preselected in the checklist. + inline: Whether the options of the checklist are displayed beside or below each other. + """ + return html.Div( + className="checklist-wrapper", + children=[ + html.Label(label), + dcc.Checklist( + id=id, + className=f"checklist{' checklist--inline' if inline else ''}", + inline=inline, + options=options, + value=values, + ), + ], + ) + + +def radio(label: str, id: str, options: list, value: int, inline: bool = True) -> html.Div: + """Radio element for option selection. + + Args: + label: The title that goes above the radio. + id: A unique selector for this element. + options: A list of dictionaries of labels and values. + value: The value of the radio that should be preselected. + inline: Whether the options are displayed beside or below each other. + """ + return html.Div( + className="radio-wrapper", + children=[ + html.Label(label), + dcc.RadioItems( + id=id, + className=f"radio{' radio--inline' if inline else ''}", + inline=inline, + options=options, + value=value, + ), + ], + ) + + +def generate_options(options_list: list) -> list[dict]: + """Generates options for dropdowns, checklists, radios, etc.""" + return [{"label": label, "value": i} for i, label in enumerate(options_list)] + + +def generate_settings_form() -> html.Div: + """This function generates settings for selecting the scenario, model, and solver. + + Returns: + html.Div: A Div containing the settings for selecting the scenario, model, and solver. + """ + dropdown_options = generate_options(DROPDOWN) + checklist_options = generate_options(CHECKLIST) + radio_options = generate_options(RADIO) + + solver_options = [ + {"label": solver_type.label, "value": solver_type.value} for solver_type in SolverType + ] + + return html.Div( + className="settings", + children=[ + slider( + "Example Slider", + "slider", + SLIDER, + ), + dropdown( + "Example Dropdown", + "dropdown", + sorted(dropdown_options, key=lambda op: op["value"]), + ), + checklist( + "Example Checklist", + "checklist", + sorted(checklist_options, key=lambda op: op["value"]), + [0], + ), + radio( + "Example Radio", + "radio", + sorted(radio_options, key=lambda op: op["value"]), + 0, + ), + dropdown( + "Solver", + "solver-type-select", + sorted(solver_options, key=lambda op: op["value"]), + ), + html.Label("Solver Time Limit (seconds)"), + dcc.Input( + id="solver-time-limit", + type="number", + **SOLVER_TIME, + ), + ], + ) + + +def generate_run_buttons() -> html.Div: + """Run and cancel buttons to run the optimization.""" + return html.Div( + id="button-group", + children=[ + html.Button(id="run-button", children="Run Optimization", n_clicks=0, disabled=False), + html.Button( + id="cancel-button", + children="Cancel Optimization", + n_clicks=0, + className="display-none", + ), + ], + ) + + +def generate_problem_details_table_rows(solver: str, time_limit: int) -> list[html.Tr]: + """Generates table rows for the problem details table. + + Args: + solver: The solver used for optimization. + time_limit: The solver time limit. + + Returns: + list[html.Tr]: List of rows for the problem details table. + """ + + table_rows = ( + ("Solver:", solver, "Time Limit:", f"{time_limit}s"), + ### Add more table rows here. Each tuple is a row in the table. + ) + + return [html.Tr([html.Td(cell) for cell in row]) for row in table_rows] + + +def problem_details(index: int) -> html.Div: + """Generate the problem details section. + + Args: + index: Unique element id to differentiate matching elements. + Must be different from left column collapse button. + + Returns: + html.Div: Div containing a collapsable table. + """ + return html.Div( + id={"type": "to-collapse-class", "index": index}, + className="details-collapse-wrapper collapsed", + children=[ + # Problem details collapsible button and header + html.Button( + id={"type": "collapse-trigger", "index": index}, + className="details-collapse", + children=[ + html.H5("Problem Details"), + html.Div(className="collapse-arrow"), + ], + ), + html.Div( + className="details-to-collapse", + children=[ + html.Table( + className="solution-stats-table", + children=[ + # Problem details table header (optional) + html.Thead( + [ + html.Tr( + [ + html.Th( + colSpan=2, + children=["Problem Specifics"], + ), + html.Th( + colSpan=2, + children=["Run Time"], + ), + ] + ) + ] + ), + # A Dash callback function will generate content in Tbody + html.Tbody(id="problem-details"), + ], + ), + ], + ), + ], + ) + + +def create_interface(): + """Set the application HTML.""" + return html.Div( + id="app-container", + children=[ + # Below are any temporary storage items, e.g., for sharing data between callbacks. + dcc.Store(id="run-in-progress", data=False), # Indicates whether run is in progress + # Header brand banner + html.Div(className="banner", children=[html.Img(src=THUMBNAIL)]), + # Settings and results columns + html.Div( + className="columns-main", + children=[ + # Left column + html.Div( + id={"type": "to-collapse-class", "index": 0}, + className="left-column", + children=[ + html.Div( + className="left-column-layer-1", # Fixed width Div to collapse + children=[ + html.Div( + className="left-column-layer-2", # Padding and content wrapper + children=[ + html.H1(MAIN_HEADER), + html.P(DESCRIPTION), + generate_settings_form(), + generate_run_buttons(), + ], + ) + ], + ), + # Left column collapse button + html.Div( + html.Button( + id={"type": "collapse-trigger", "index": 0}, + className="left-column-collapse", + children=[html.Div(className="collapse-arrow")], + ), + ), + ], + ), + # Right column + html.Div( + className="right-column", + children=[ + dcc.Tabs( + id="tabs", + value="input-tab", + mobile_breakpoint=0, + children=[ + dcc.Tab( + label="Input", + id="input-tab", + value="input-tab", # used for switching tabs programatically + className="tab", + children=[ + dcc.Loading( + parent_className="input", + type="circle", + color=THEME_COLOR_SECONDARY, + # A Dash callback (in app.py) will generate content in the Div below + children=html.Div(id="input"), + ), + ], + ), + dcc.Tab( + label="Results", + id="results-tab", + className="tab", + disabled=True, + children=[ + html.Div( + className="tab-content-results", + children=[ + dcc.Loading( + parent_className="results", + type="circle", + color=THEME_COLOR_SECONDARY, + # A Dash callback (in app.py) will generate content in the Div below + children=html.Div(id="results"), + ), + # Problem details dropdown + html.Div([html.Hr(), problem_details(1)]), + ], + ) + ], + ), + ], + ) + ], + ), + ], + ), + ], + ) diff --git a/requirements.txt b/requirements.txt index c4e3258..5df3741 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -dwave-ocean-sdk>=7.0.0 +dwave-ocean-sdk>=7.1.0 +dash[diskcache]==2.17.1 diff --git a/src/demo_enums.py b/src/demo_enums.py new file mode 100644 index 0000000..132aa06 --- /dev/null +++ b/src/demo_enums.py @@ -0,0 +1,35 @@ +# Copyright 2024 D-Wave +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from enum import Enum + + +class SolverType(Enum): + """Add a list of solver options here. If this demo only requires 1 solver, + this functionality can be removed. + """ + + SOLVER_1 = 0 + SOLVER_2 = 1 + + @property + def label(self): + return { + SolverType.SOLVER_1: "Solver 1", + SolverType.SOLVER_2: "Solver 2", + }[self] + + +### If any settings or variables are being used repeatedly, thoughout the code, create a new +### Enum for the setting here to avoid string comparisons or other fragile code practices. diff --git a/static/dwave_logo.svg b/static/dwave_logo.svg new file mode 100644 index 0000000..ff46dbd --- /dev/null +++ b/static/dwave_logo.svg @@ -0,0 +1 @@ + \ No newline at end of file