diff --git a/.gitignore b/.gitignore index 4fb062d..cd44f03 100644 --- a/.gitignore +++ b/.gitignore @@ -108,3 +108,4 @@ docs/_html/* *.pt *.gz docs/examples/lightning_logs/* +docs/examples/electricity/*.pq diff --git a/demo/Dockerfile b/demo/Dockerfile new file mode 100644 index 0000000..21d6b1d --- /dev/null +++ b/demo/Dockerfile @@ -0,0 +1,21 @@ +FROM python:3.9 + +WORKDIR /app + +RUN python3 -m pip install --upgrade pip setuptools && \ + python3 -m pip install gunicorn + +COPY setup.py . + +# Install only the dependencies +RUN python setup.py egg_info && \ + pip install -r *.egg-info/requires.txt && \ + rm -rf *.egg-info/ + +COPY . . + +RUN pip install .[demo] + +EXPOSE 8097 + +CMD ["gunicorn", "--bind", "0.0.0.0:8097", "demo.app:server"] diff --git a/demo/app.py b/demo/app.py new file mode 100644 index 0000000..d0f5f40 --- /dev/null +++ b/demo/app.py @@ -0,0 +1,511 @@ +import logging +from datetime import datetime +from functools import lru_cache +from pathlib import Path +from typing import Dict, List, Optional + +import dash_daq as daq +import numpy as np +import pandas as pd +import plotly.express as px +from dash import Dash, Input, Output, dcc, html +from plotly.graph_objs import Figure + +logging.basicConfig( + format='%(asctime)s %(levelname)s %(module)s - %(funcName)s: %(message)s', + level=logging.DEBUG, + datefmt='%H:%M:%S', +) + + +app = Dash(__name__, eager_loading=True) +server = app.server + +DATA_DIR = Path("docs/examples/electricity").resolve() +train_val_split_dt: pd.Timestamp = pd.Timestamp("2013-06-01") + +# assume you have a "long-form" data frame +# see https://plotly.com/python/px-arguments/ for more options +df = pd.read_csv(DATA_DIR / "df_electricity.csv.gz", parse_dates=["time"]) +nn_df: pd.DataFrame = pd.read_parquet(DATA_DIR / "df_forecast_nn.pq") +# the nn_df and exponential smoothing dfs have sqrt and centered data. +df["kW_sqrt"] = np.sqrt(df["kW"]) +centerings = ( + nn_df + .sort_values("time") + .groupby("group") + .head(1) + .filter(["actual", "time", "group"]) + .merge(df) + .set_index("group") + .pipe( + lambda frame: + frame["kW_sqrt"] - frame["actual"] + ) +) + +all_groups: np.ndarray = df['group'].unique() +regular_groups = pd.read_parquet(DATA_DIR / "train_groups.pq").squeeze().to_list() +strong_color_cycle: Dict[str, str] = dict(color_discrete_sequence=["#F4F4F4", "#B5C3FF"]) + + +app.layout = html.Div( + [ + html.Div( + children=[ + html.Div( + children=[ + html.H1( + children="Electricity Use Forecasting", + style={ + "textAlign": "left", + }, + ), + html.Div( + children="Investigating the electricity use of 370 clients in the grid.", + style={"textAlign": "left", "margin-bottom": "40px"}, + ), + ], + ), + html.Img( + src="assets/strong-logo-white.svg", + style={ + "width": "127px", + "height": "48px", + "position": "absolute", + "right": "75px", + "align-self": "center", + }, + ), + ], + style={"display": "inline-flex"}, + ), + html.Div( + children=[ + # col 1 + html.Div( + children=[ + dcc.Graph( + id="time-series-chart", + className="card main-card", + style={"flex": "1 1", "margin-bottom": "30px"}, + ), + # the horizontal part with 2 parts + html.Div( + children=[ + # Part 1 + html.Div( + dcc.Graph(id="histogram-chart"), + style={"flex": "1 1", "margin-right": "30px"}, + className="card", + ), + # Part 2 + html.Div( + id="correlation-div", + className="card", + style={"flex": "0 0 323px"} + ), + ], + style={"display": "flex", "flex-direction": "row"}, + ), + ], + style={"flex": "1 1"}, + ), + # col 2 + html.Div( + children=[ + html.Div( + children=[ + html.H6("ML model"), + dcc.RadioItems( + id="prediction_toggle", + options=[ + {"label": "None", "value": "none"}, + {"label": "Exponential Smoothing", "value": "es",}, + {"label": "Neural Network", "value": "nn"}, + ], + value="none", + className="form-item", + ), + daq.BooleanSwitch(id="show_future_switch", on=False, label="Show future", labelPosition="bottom"), + html.H5(" "), + html.H6("Time"), + dcc.RangeSlider( + 0, 24, 1, value=[0, 24], marks={0: "12AM", 6: "6AM", 12: "12PM", 18: "6PM", 24: "12AM"}, + id="time_of_day_range", + ), + html.H6("Day"), + dcc.Checklist( + id="day_of_week_toggle", + options=[ + {"label": label, "value": value} + for label, value in zip( + "MTWRFSS", + range(7) + ) + ], + value=list(range(7)), + inline=True, + className="form-item", + ), + html.H5(" "), + html.H6("Clients"), + dcc.Dropdown( + id="group_dropdown", + options=all_groups, + value=["MT_328"], + # className="form-item", + clearable=False, + multi=True, + # style={.Select-value-label {color: white !important;}}, + style={"color": "black"}, + ), + # html.H6( + # "Drop irregular clients", + # ), + # dcc.RadioItems( + # id="drop_irregular", + # options=[ + # {"label": "No", "value": False}, + # {"label": "Yes", "value": True}, + # ], + # value=True, + # inline=True, + # className="form-item", + # ), + ], + ), + ], + style={"flex": "0 0 250px", "margin-left": "30px"}, + className="card", + ), + ], + className="bg-strong-dark-blue", + style={"display": "flex", "flex-direction": "row"}, + ), + html.Div( + id="footer", + children=['Powered by ', + html.A(href="https://strong.io/", children="Strong Analytics", + target="_blank", rel="noopener noreferrer")], + style={"margin-top": "16px", "padding-left": "5px"}, + ) + ], +) + + +@lru_cache +def get_es_prediction_df(group: str) -> Optional[pd.DataFrame]: + try: + return pd.read_parquet(DATA_DIR / f"es_{group}_2.pq") + except FileNotFoundError: + print(f"Couldn't find the es predictions for {group}") + return None + +@lru_cache +def get_combined_df(group: str) -> pd.DataFrame: + es_prediction_df_subset: pd.DataFrame = ( + get_es_prediction_df(group) + .assign(ES=lambda df: (df["mean"] + centerings.at[group]).pow(2)) + .filter(["time", "ES"]) + ) + nn_df_subset: pd.DataFrame = ( + nn_df + .query(f"group == '{group}'") + .assign(NN=lambda df: (df["mean"] + centerings.at[group]).pow(2)) + .filter(["time", "NN"]) + ) + original_data_subset = ( + df + .query(f"group == '{group}'") + .assign(actual = lambda df: df["kW"]) + .filter(["group", "time", "actual"]) + ) + combined = ( + original_data_subset + .merge(es_prediction_df_subset, how="outer") + .merge(nn_df_subset, how="outer") + .assign( + is_train = lambda df: df["time"] < train_val_split_dt + ) + .melt( + value_vars=["actual", "ES", "NN"], + id_vars=["group", "time", "is_train"], + value_name="kW", + var_name="model", + ) + ) + return combined + +# --- plotting +# main plot +@app.callback( + Output("time-series-chart", "figure"), + Output("histogram-chart", "figure"), + Input("group_dropdown", "value"), + Input("prediction_toggle", "value"), + Input("show_future_switch", "on"), + Input("time_of_day_range", "value"), + Input("day_of_week_toggle", "value"), + Input("time-series-chart", "figure"), +) +def display_time_series( + groups: List[str], + prediction_toggle_value: str, + show_future: bool, + time_of_day_range: List[int], + day_of_week_toggle: List[int], + existing_fig_ts: Optional[Figure], +): + + _df = pd.concat( + [pd.DataFrame({ + "group": pd.Series(dtype='str'), + "time": pd.Series(dtype='datetime64[s]'), + "is_train": pd.Series(dtype='bool'), + "model": pd.Series(dtype='str'), + "kW": pd.Series(dtype='float64') + })] + + [get_combined_df(group) for group in groups] + ) + + # Get the y range now before it gets overwritten + + y_range = (0, _df["kW"].max() * 1.1) + x_range = existing_fig_ts and existing_fig_ts['layout']['xaxis']['range'] + x_max_range = (_df["time"].min(), _df["time"].max()) + + if not show_future: + _df = _df.query("(is_train == True) | (model != 'actual')") + + if time_of_day_range == [0, 24]: + pass + else: + valid_hours = list(range(*time_of_day_range)) + _df = _df.query("time.dt.hour.isin(@valid_hours)") + + if day_of_week_toggle == list(range(7)): + pass + else: + _df = _df.query("time.dt.dayofweek.isin(@day_of_week_toggle)") + + + + ts_fig_height_px = 400 + + if prediction_toggle_value == "none": + fig_ts = px.line( + _df.query("model == 'actual'"), + x='time', y='kW', + color='group', **strong_color_cycle, + height=ts_fig_height_px, + ) + elif prediction_toggle_value == "es": + # do the plotting + fig_ts = px.line( + _df.query("(model == 'actual') | ((model == 'ES') and (is_train == False))"), + x='time', y='kW', + color='group', **strong_color_cycle, + line_dash='model', + height=ts_fig_height_px + ) + else: + fig_ts = px.line( + _df.query("(model == 'actual') | ((model == 'NN') & (is_train == False))"), + x='time', y='kW', + color='group', **strong_color_cycle, + line_dash='model', + height=ts_fig_height_px) + + # Add the vertical line between train-val split + fig_ts.add_vline(x=datetime(2013, 6, 1), line_width=3, line_dash="dash", line_color="white") + + # Styling of time series + fig_ts \ + .update_layout( + xaxis=dict( + rangeselector=dict( + buttons=list([ + dict(count=1, + label="1m", + step="month", + stepmode="backward"), + dict(count=6, + label="6m", + step="month", + stepmode="backward"), + dict(count=1, + label="1y", + step="year", + stepmode="backward"), + dict(step="all") + ]), + font=dict( + color="#111" + ) + ), + rangeslider=dict( + visible=True, + range=x_max_range, + ), + range=x_range or ( + train_val_split_dt - pd.Timedelta("7D"), + train_val_split_dt - pd.Timedelta("7D") + pd.Timedelta("30D") + ), + type="date" + ) + ) \ + .update_layout( + legend=dict( + yanchor="bottom", + y=1.01, + xanchor="center", + x=0.50, + orientation='h', + title="Group", + ), + margin=dict( + l=0, + r=0, + b=0, + t=0, + pad=0, + ), + plot_bgcolor='rgba(0,0,0,0)', + paper_bgcolor='rgba(0,0,0,0)', + font_color='#F4F4F4', + font_family="Courier New", + title_font_family="Courier New", + ) \ + .update_xaxes(showgrid=False) \ + .update_yaxes(range=y_range, showgrid=False) \ + .update_traces(line=dict(width=1.0)) + + # Create the histogram + fig_hist = px.histogram( + _df.query("model == 'actual'"), + x='kW', + nbins=80, + color='group', **strong_color_cycle, + opacity=0.8, + histnorm='probability density', + height=275 + ) + # Update looks of histogram + fig_hist\ + .update_layout( + plot_bgcolor='rgba(0,0,0,0)', + paper_bgcolor='rgba(0,0,0,0)', + font_color='#F4F4F4', + font_family="Courier New", + title_font_family="Courier New", + margin=dict( + l=8, + r=8, + b=0, + t=10, + pad=0, + )) \ + .update_yaxes(visible=False) \ + .update_layout( + legend=dict( + yanchor="top", + y=0.99, + xanchor="right", + x=0.99 + ) + ) + + + return fig_ts, fig_hist + +# Correlation plot +@app.callback( + Output("correlation-div", "children"), + Output("correlation-div", "style"), + Input("group_dropdown", "value") +) +def correlation_plot(group_checklist_values: List[str]): + if len(group_checklist_values) == 2: + group1: pd.DataFrame = get_combined_df(group_checklist_values[0]).query("model == 'actual'") + group2: pd.DataFrame = get_combined_df(group_checklist_values[1]).query("model == 'actual'") + groups_merged: pd.DataFrame = group1.merge( + group2, + on=["time"], + validate="1:1", + how="inner", + ) + fig_corr = px.density_heatmap( + data_frame=groups_merged, + x="kW_x", + y="kW_y", + labels={ + "kW_x": f"{group_checklist_values[0]} Power Use (kW)", + "kW_y": f"{group_checklist_values[1]} Power Use (kW)", + }, + color_continuous_scale="haline", + width=323, height=275 + ) + fig_corr\ + .update_layout( + plot_bgcolor='rgba(0,0,0,0)', + paper_bgcolor='rgba(0,0,0,0)', + font_color='#FFF', + font_family="Courier New", + title_font_family="Courier New", + margin=dict( + l=0, + r=0, + b=0, + t=0, + pad=0, + ),)\ + .update_coloraxes(showscale=False) + + children=[ + dcc.Graph(id="correlation-chart", figure=fig_corr) + ] + style={ "flex": "0 0 323px"} + else: + children = [ + html.P(children="Select two clients to show correlation"), + ] + style={ "flex": "0 0 323px", "text-align": "center", "padding-top": "150px"} + return children, style + + +# --- Station selection +# limit the number of options +_max_selected = 2 +@app.callback( + Output("group_dropdown", "options"), + Input("group_dropdown", "value"), + # Input("drop_irregular", "value"), +) +def update_multi_options( + groups_selected: List[str], + # drop_irregular: bool, +): + # options = all_groups + # if drop_irregular: + # options = regular_groups + + options = regular_groups + if len(groups_selected) >= _max_selected: + options = [ + { + "label": option, + "value": option, + "disabled": option not in groups_selected, + } + for option in options + ] + return options + + +if __name__ == "__main__": + app.run_server( + # debug=True, + host="0.0.0.0", + port=8097, + ) diff --git a/demo/assets/app.css b/demo/assets/app.css new file mode 100644 index 0000000..ea8c368 --- /dev/null +++ b/demo/assets/app.css @@ -0,0 +1,453 @@ +/* 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; +}*/ + +.modebar { + display: none !important; +} + +/* 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: 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: #F4F4F4; + margin: 75px; +} + + +/* 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: 0.5rem; } +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: 1.2rem; margin-top: 2.0rem;} +h6 { font-size: 1.8rem; line-height: 1.6; letter-spacing: 0; margin-bottom: 0.6rem; margin-top: 1.2rem;} + +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: #EEE; + text-decoration: underline; + cursor: pointer;} +a:hover { + color: #FFF; } + + +/* 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: #2A3D82; + border-color: #2A3D82; } + + +/* 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; + color: #2A3D82; + accent-color: #AAAAAA; +} +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:not(.CalendarDay), +td:not(.CalendarDay) { + padding: 12px 15px; + text-align: left; + border-bottom: 1px solid #E1E1E1; } +th:first-child:not(.CalendarDay), +td:first-child:not(.CalendarDay) { + padding-left: 0; } +th:last-child:not(.CalendarDay), +td:last-child:not(.CalendarDay) { + 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; } + +/* Colors +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +body, .bg-strong-dark-blue { + background-color: #2A3D82; +} + +.main-card { + background-color: #1D2B5C !important; +} + +.card { + background-color: #263775; + padding: 32px; + border-radius: 6px; + filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.25)); +} + +.form-item { + margin-bottom: 8px; +} +.form-item label { + margin-right: 16px; + font-size: 16px; +} + +.form-item input { + margin-right: 8px; + margin-bottom: 8px; +} +/* 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/demo/assets/strong-logo-white.svg b/demo/assets/strong-logo-white.svg new file mode 100644 index 0000000..eccba65 --- /dev/null +++ b/demo/assets/strong-logo-white.svg @@ -0,0 +1 @@ +Strong Logo Working_White \ No newline at end of file diff --git a/setup.py b/setup.py index 14f9374..bc68e38 100644 --- a/setup.py +++ b/setup.py @@ -27,6 +27,13 @@ 'pytorch_lightning>=1.5', 'torch_optimizer>=0.3.0', 'matplotlib' + ], + 'demo': [ + 'dash', + 'dash_daq', + 'jupyter-dash', + 'pandas', + 'pyarrow', # to support parquet files ] } )