-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create and deploy v1 of the website (#1)
* Add all of the required files to create v1 of the website
- Loading branch information
Showing
20 changed files
with
1,502 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -76,6 +76,7 @@ docs/_build/ | |
target/ | ||
|
||
# Jupyter Notebook | ||
*.ipynb | ||
.ipynb_checkpoints | ||
|
||
# IPython | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
# Use the official Python base image | ||
FROM python:3.9-slim | ||
|
||
# Set the working directory in the container | ||
WORKDIR /app | ||
|
||
# Expose port 8080 for running the website | ||
EXPOSE 8080 | ||
|
||
# Copy the required files | ||
COPY app.py app.yaml requirements.txt robots.txt sitemap.xml ./ | ||
COPY assets assets | ||
COPY pages pages | ||
|
||
# Create a virtual environment and activate it | ||
RUN python -m venv venv | ||
ENV PATH="/app/venv/bin:$PATH" | ||
|
||
# Install dependencies | ||
RUN pip install --no-cache-dir -r requirements.txt | ||
|
||
# Specify the command to run the app | ||
CMD ["python3", "-m", "flask", "run", "--host=0.0.0.0", "--port=8080"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# Statistical Stories | ||
|
||
[Statistical Stories](https://statisticalstories.xyz) is a website to help people learn about statistics, data science, and machine learning using simple stories and interactive visualizations. | ||
|
||
|
||
## Testing | ||
To deploy a local version of the website: | ||
|
||
``` | ||
# Create a virtual environment | ||
python3 -m venv venv | ||
# Install all of the dependencies | ||
pip3 install -r requirements.txt | ||
# Run the website locally | ||
python3 app.py | ||
``` | ||
|
||
## Improvements | ||
Feel free to create a github issue or send me an email at [email protected] if you see anything wrong with the website or would like me to add/change something. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
from typing import Dict, List, Tuple | ||
|
||
import dash_bootstrap_components as dbc | ||
from dash import Dash, dcc, html, no_update, page_container | ||
from dash.dependencies import Input, Output, State | ||
from flask import send_from_directory | ||
|
||
|
||
app = Dash( | ||
__name__, | ||
use_pages=True, | ||
pages_folder="pages", | ||
external_stylesheets=[dbc.icons.BOOTSTRAP, dbc.themes.BOOTSTRAP] | ||
) | ||
app.config.suppress_callback_exceptions = True | ||
server = app.server | ||
|
||
@server.route('/robots.txt') | ||
def serve_robots(): | ||
return send_from_directory('.', 'robots.txt', mimetype='text/plain') | ||
|
||
|
||
@server.route('/sitemap.xml') | ||
def serve_sitemap(): | ||
return send_from_directory('.', 'sitemap.xml', mimetype='application/xml') | ||
|
||
|
||
app.layout = html.Div([ | ||
html.Div(className='container', children=[ | ||
html.Div(id='navbar', children=[ | ||
html.Div(className='button-menu-and-title', children=[ | ||
# className "bi bi-list" is required for the menu icon | ||
# className "menu-hidden" is replaced by "menu-visible" depending on | ||
# the screen size and interactions with the website | ||
html.Button(html.I(className="bi bi-list"), id="button-navbar", className='menu-hidden'), | ||
html.H1( | ||
className='navbar-title', | ||
children=dcc.Link("Statistical Stories", href="/", id="link-home"), | ||
), | ||
]), | ||
# "display: none" is also replaced depending on screen size and interactions | ||
html.Div(id='navbar-menus', className='menu-hidden', style={'display': 'none'}, children=[ | ||
html.Div([ | ||
dcc.Input(id='search-bar', type='text', placeholder='Search...'), | ||
html.Div(id='search-results', style={'padding': '10px'}) | ||
]), | ||
dbc.DropdownMenu( | ||
children=[ | ||
dbc.DropdownMenuItem(dcc.Link("Normal Distribution", href="/normal")), | ||
dbc.DropdownMenuItem(dcc.Link("Poisson Distribution", href="/poisson")), | ||
], | ||
nav=True, | ||
in_navbar=True, | ||
label="Distributions", | ||
), | ||
dbc.DropdownMenu( | ||
children=[ | ||
dbc.DropdownMenuItem(dcc.Link("K-Nearest Neighbors", href="/k-nearest-neighbors")), | ||
], | ||
nav=True, | ||
in_navbar=True, | ||
label="Statistical Models", | ||
), | ||
dbc.DropdownMenu( | ||
children=[ | ||
dbc.DropdownMenuItem(dcc.Link("ANOVA", href="/anova")), | ||
], | ||
nav=True, | ||
in_navbar=True, | ||
label="Statistical Tests", | ||
), | ||
]), | ||
]), | ||
html.Div(id='content-body-wrapper', children=[ | ||
dcc.Store(id="screen-width-store"), | ||
dcc.Location(id='url', refresh=False), | ||
page_container, | ||
html.Div(id='footer', children=[ | ||
html.P("Statistical Stories. All rights reserved."), | ||
html.P(className="footer-pipe", children=["|"]), | ||
html.A("We're open source!", target="_blank", href="https://github.com/Currie32/statistical_stories") | ||
]), | ||
]), | ||
]) | ||
]) | ||
|
||
|
||
# Get the width of the screen when the website loads | ||
app.clientside_callback( | ||
""" | ||
function() {return window.innerWidth} | ||
""", | ||
Output("screen-width-store", "data"), | ||
Input('button-navbar', 'n_clicks'), | ||
) | ||
|
||
|
||
@app.callback( | ||
Output('navbar-menus', 'className'), | ||
Output('navbar-menus', 'style'), | ||
Output('button-navbar', 'className'), | ||
Output('content-body-wrapper', 'className'), | ||
Input('url', 'pathname'), | ||
Input('button-navbar', 'n_clicks'), | ||
Input('screen-width-store', 'data'), | ||
State('navbar-menus', 'className'), | ||
State('button-navbar', 'className'), | ||
State('content-body-wrapper', 'className'), | ||
) | ||
def display_navbar_menu_toggle( | ||
pathname: str, # Not used but required input to close menu | ||
n_clicks: int, | ||
screen_width: int, | ||
menu_classname: str, | ||
button_class_name: str, | ||
content_class_name: str, | ||
) -> Tuple[str, Dict[str, str], str, str]: | ||
""" | ||
Logic of whether to display the dropdown navbar menu. | ||
""" | ||
|
||
if screen_width < 800 and not n_clicks: | ||
return 'menu-hidden', {'display': 'none'}, 'menu-hidden', 'menu-hidden' | ||
elif menu_classname == 'menu-visible' and n_clicks: | ||
return 'menu-hidden', {'display': 'none'}, 'menu-hidden', 'menu-hidden' | ||
elif button_class_name == 'menu-hidden' and n_clicks: | ||
return 'menu-visible', {'display': 'block'}, 'menu-visible', 'menu-visible' | ||
elif content_class_name == 'menu-hidden': | ||
return 'menu-hidden', {'display': 'none'}, 'menu-hidden', 'menu-hidden' | ||
else: | ||
return 'menu-visible', {'display': 'block'}, 'menu-visible', 'menu-visible' | ||
|
||
|
||
def read_page_content(page_file: str) -> str: | ||
""" | ||
Load and read the contents of a web page. | ||
""" | ||
with open(page_file, 'r') as f: | ||
content = f.read() | ||
return content | ||
|
||
@app.callback( | ||
Output('search-results', 'children'), | ||
Output('search-results', 'style'), | ||
Output('url', 'pathname'), | ||
[Input('search-bar', 'value')], | ||
[State('url', 'pathname')], | ||
) | ||
def search_pages( | ||
search_query: str, | ||
current_page: str | ||
) -> Tuple[List[dcc.Link], Dict[str, str], str]: | ||
""" | ||
Return a list of pages that contain the search query. | ||
Update the current_page if a link to a different page is clicked on. | ||
Update the padding of the search-results div based on the number of results. | ||
""" | ||
|
||
# Don't display the search-results div if there is no search query | ||
if not search_query: | ||
return None, {'padding': '0px'}, current_page | ||
|
||
# Pages of the website that can be searched | ||
pages = [ | ||
{'title': 'Normal distribution', 'path': 'pages/distribution_normal.py', 'url': '/normal'}, | ||
{'title': 'Poisson distribution', 'path': 'pages/distribution_poisson.py', 'url': '/poisson'}, | ||
{'title': 'k-Nearest Neighbors', 'path': 'pages/model_k_nearest_neighbors.py', 'url': '/k-nearest-neighbors'}, | ||
{'title': 'ANOVA', 'path': 'pages/test_anova.py', 'url': 'anova'}, | ||
] | ||
|
||
# Append pages that match the search query | ||
matching_pages = [] | ||
for page in pages: | ||
content = read_page_content(page['path']) | ||
if search_query.lower() in content.lower(): | ||
matching_pages.append(page) | ||
|
||
# If there are matching pages, display them | ||
if matching_pages: | ||
result_list = [] | ||
for page in matching_pages: | ||
result_list.append(dcc.Link(page['title'], href=page['url'])) | ||
return result_list, {'padding': '10px'}, no_update | ||
|
||
# If no matching pages are found, display a message | ||
else: | ||
return html.P('No matching pages found.'), {'padding': '10px 10px 1px'}, no_update | ||
|
||
|
||
if __name__ == '__main__': | ||
app.run_server(debug=True, port=8080) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
service: default | ||
runtime: python39 | ||
|
||
basic_scaling: | ||
max_instances: 2 | ||
idle_timeout: 10m | ||
|
||
resources: | ||
cpu: 1 | ||
memory_gb: 1 | ||
disk_size_gb: 1 | ||
|
||
env_variables: | ||
PORT: 8080 | ||
|
||
entrypoint: gunicorn -b :$PORT main:server |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
html { | ||
margin: 0px; | ||
} | ||
body { | ||
margin: 0px; | ||
font-family: Georgia, 'Times New Roman', Times, serif; | ||
overflow-x: hidden; | ||
} | ||
p { | ||
line-height: 22px; | ||
} | ||
a { | ||
color: black; | ||
} | ||
li { | ||
margin-bottom: 5px; | ||
} | ||
pre { | ||
background-color: #f5f5f5; | ||
font-size: 16px; | ||
padding: 0px 25px; | ||
white-space: pre-wrap; | ||
} | ||
.container { | ||
display: relative; | ||
} | ||
#content-body-wrapper { | ||
justify-content: center; | ||
margin-left: 300px; | ||
} | ||
@media (max-width: 800px) { | ||
#content-body-wrapper { | ||
margin-left: 0px | ||
} | ||
} |
Oops, something went wrong.