Merge pull request 'charts' (#2) from charts into master
Reviewed-on: #2
This commit is contained in:
commit
e3b80c96b8
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,4 +1,4 @@
|
|||||||
*__pycache__*
|
*__pycache__*
|
||||||
.venv*
|
.venv*
|
||||||
.env
|
.env*
|
||||||
mongo_data/*
|
mongo_data/*
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
FROM python:3.10 as pool_base_image
|
FROM python:3.10 AS pool_base_image
|
||||||
|
|
||||||
WORKDIR /pool_data
|
WORKDIR /pool_data
|
||||||
|
|
||||||
@ -9,4 +9,3 @@ RUN apt-get update -y && apt-get upgrade -y && apt-get install -y libsasl2-dev p
|
|||||||
RUN pip install --upgrade pip
|
RUN pip install --upgrade pip
|
||||||
|
|
||||||
RUN pip --default-timeout=1000 install -r requirements.txt
|
RUN pip --default-timeout=1000 install -r requirements.txt
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ services:
|
|||||||
context: .
|
context: .
|
||||||
dockerfile: flask.Dockerfile
|
dockerfile: flask.Dockerfile
|
||||||
volumes:
|
volumes:
|
||||||
- ./src/flask:/pool_data_web/src/flask
|
- ./src/flask:/pool_data/src/flask
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
- "5000:5000"
|
- "5000:5000"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
FROM pool_base_image as flask
|
FROM pool_base_image AS flask
|
||||||
|
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
3
pyproject.toml
Normal file
3
pyproject.toml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[tool.pyright]
|
||||||
|
venvPath = "."
|
||||||
|
venv = ".venv"
|
@ -6,3 +6,4 @@ click
|
|||||||
Flask-WTF
|
Flask-WTF
|
||||||
bootstrap-flask
|
bootstrap-flask
|
||||||
waitress
|
waitress
|
||||||
|
bokeh
|
||||||
|
61
src/flask/chart_test.html
Normal file
61
src/flask/chart_test.html
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Bokeh Plot</title>
|
||||||
|
<style>
|
||||||
|
html, body {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flow-root;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-3.5.0.min.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
Bokeh.set_log_level("info");
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="bb5c0e1d-dc95-4d96-9cbc-a29ed2eade5c" data-root-id="p1001" style="display: contents;"></div>
|
||||||
|
|
||||||
|
<script type="application/json" id="af8187ed-c6d3-40c3-84a3-bbe58799b466">
|
||||||
|
{"00bec54e-f52e-4b7f-b2b6-8e6e4eb73c82":{"version":"3.5.0","title":"Bokeh Application","roots":[{"type":"object","name":"Figure","id":"p1001","attributes":{"height":250,"x_range":{"type":"object","name":"FactorRange","id":"p1011","attributes":{"factors":["2024-03-27","2024-03-25","2024-03-15","2024-03-13"]}},"y_range":{"type":"object","name":"DataRange1d","id":"p1003","attributes":{"start":6}},"x_scale":{"type":"object","name":"CategoricalScale","id":"p1012"},"y_scale":{"type":"object","name":"LinearScale","id":"p1013"},"title":{"type":"object","name":"Title","id":"p1004","attributes":{"text":"Pool data"}},"renderers":[{"type":"object","name":"GlyphRenderer","id":"p1030","attributes":{"data_source":{"type":"object","name":"ColumnDataSource","id":"p1024","attributes":{"selected":{"type":"object","name":"Selection","id":"p1025","attributes":{"indices":[],"line_indices":[]}},"selection_policy":{"type":"object","name":"UnionRenderers","id":"p1026"},"data":{"type":"map","entries":[["x",["2024-03-27","2024-03-25","2024-03-15","2024-03-13"]],["y",[7.25,7.5,7.6,7.0]]]}}},"view":{"type":"object","name":"CDSView","id":"p1031","attributes":{"filter":{"type":"object","name":"AllIndices","id":"p1032"}}},"glyph":{"type":"object","name":"Line","id":"p1027","attributes":{"x":{"type":"field","field":"x"},"y":{"type":"field","field":"y"},"line_color":"#1f77b4"}},"nonselection_glyph":{"type":"object","name":"Line","id":"p1028","attributes":{"x":{"type":"field","field":"x"},"y":{"type":"field","field":"y"},"line_color":"#1f77b4","line_alpha":0.1}},"muted_glyph":{"type":"object","name":"Line","id":"p1029","attributes":{"x":{"type":"field","field":"x"},"y":{"type":"field","field":"y"},"line_color":"#1f77b4","line_alpha":0.2}}}}],"toolbar":{"type":"object","name":"Toolbar","id":"p1010"},"toolbar_location":null,"left":[{"type":"object","name":"LinearAxis","id":"p1019","attributes":{"ticker":{"type":"object","name":"BasicTicker","id":"p1020","attributes":{"mantissas":[1,2,5]}},"formatter":{"type":"object","name":"BasicTickFormatter","id":"p1021"},"major_label_policy":{"type":"object","name":"AllLabels","id":"p1022"}}}],"below":[{"type":"object","name":"CategoricalAxis","id":"p1014","attributes":{"ticker":{"type":"object","name":"CategoricalTicker","id":"p1015"},"formatter":{"type":"object","name":"CategoricalTickFormatter","id":"p1016"},"major_label_policy":{"type":"object","name":"AllLabels","id":"p1017"}}}],"center":[{"type":"object","name":"Grid","id":"p1018","attributes":{"axis":{"id":"p1014"},"grid_line_color":null}},{"type":"object","name":"Grid","id":"p1023","attributes":{"dimension":1,"axis":{"id":"p1019"}}}]}}]}}
|
||||||
|
</script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
(function() {
|
||||||
|
const fn = function() {
|
||||||
|
Bokeh.safely(function() {
|
||||||
|
(function(root) {
|
||||||
|
function embed_document(root) {
|
||||||
|
const docs_json = document.getElementById('af8187ed-c6d3-40c3-84a3-bbe58799b466').textContent;
|
||||||
|
const render_items = [{"docid":"00bec54e-f52e-4b7f-b2b6-8e6e4eb73c82","roots":{"p1001":"bb5c0e1d-dc95-4d96-9cbc-a29ed2eade5c"},"root_ids":["p1001"]}];
|
||||||
|
root.Bokeh.embed.embed_items(docs_json, render_items);
|
||||||
|
}
|
||||||
|
if (root.Bokeh !== undefined) {
|
||||||
|
embed_document(root);
|
||||||
|
} else {
|
||||||
|
let attempts = 0;
|
||||||
|
const timer = setInterval(function(root) {
|
||||||
|
if (root.Bokeh !== undefined) {
|
||||||
|
clearInterval(timer);
|
||||||
|
embed_document(root);
|
||||||
|
} else {
|
||||||
|
attempts++;
|
||||||
|
if (attempts > 100) {
|
||||||
|
clearInterval(timer);
|
||||||
|
console.log("Bokeh: ERROR: Unable to run BokehJS code because BokehJS library is missing");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 10, root)
|
||||||
|
}
|
||||||
|
})(window);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if (document.readyState != "loading") fn();
|
||||||
|
else document.addEventListener("DOMContentLoaded", fn);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
17
src/flask/chart_test.py
Normal file
17
src/flask/chart_test.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from bokeh.models.layouts import HBox
|
||||||
|
from bokeh.plotting import column
|
||||||
|
from charts import PoolCharts
|
||||||
|
from bokeh.io import output_file, show
|
||||||
|
from bokeh.layouts import row
|
||||||
|
|
||||||
|
output_file("static/data_plot.html")
|
||||||
|
|
||||||
|
chart = PoolCharts.PoolCharts()
|
||||||
|
|
||||||
|
ph = chart.line_chart("Pool PH", "ph", 50)
|
||||||
|
|
||||||
|
total_chlorine = chart.line_chart("Pool Total Chlorine", "total_chlorine", 50)
|
||||||
|
|
||||||
|
free_chlorine = chart.line_chart("Pool Free Chlorine", "free_chlorine", 50)
|
||||||
|
|
||||||
|
show(column(ph, total_chlorine, free_chlorine))
|
49
src/flask/charts/PoolCharts.py
Normal file
49
src/flask/charts/PoolCharts.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
from bokeh.models.widgets import DataCube
|
||||||
|
from bokeh.plotting import figure
|
||||||
|
|
||||||
|
from mongo.get_conn import db_conn
|
||||||
|
|
||||||
|
class PoolCharts():
|
||||||
|
def __init__(self):
|
||||||
|
self.db = db_conn()
|
||||||
|
|
||||||
|
|
||||||
|
def line_chart(self, title, field, limit):
|
||||||
|
data = self.db.pool_db
|
||||||
|
data = data.find({}).sort("date", -1).limit(limit)
|
||||||
|
dates = []
|
||||||
|
data_list = []
|
||||||
|
date_count = {}
|
||||||
|
for record in data:
|
||||||
|
if field in record and record[field] != "None":
|
||||||
|
if record["date"] not in dates:
|
||||||
|
print("new date record")
|
||||||
|
dates.append(record["date"])
|
||||||
|
data_list.append(float(record[field]))
|
||||||
|
print (dates)
|
||||||
|
print (data_list)
|
||||||
|
else:
|
||||||
|
if record["date"] in date_count:
|
||||||
|
date_count[record["date"]] += 1
|
||||||
|
else:
|
||||||
|
date_count[record["date"]] = 2
|
||||||
|
print(len(data_list))
|
||||||
|
current_data = data_list[len(data_list) - 1]
|
||||||
|
data_list[len(data_list) - 1] = (float(record[field]) + current_data) / date_count[record["date"]]
|
||||||
|
|
||||||
|
print(dates)
|
||||||
|
print(data_list)
|
||||||
|
|
||||||
|
p = figure(x_range=dates, height=250, title=title,
|
||||||
|
toolbar_location=None, tools="")
|
||||||
|
|
||||||
|
p.line(x=dates, y=data_list)
|
||||||
|
|
||||||
|
p.xgrid.grid_line_color = None
|
||||||
|
if not data_list:
|
||||||
|
p.y_range.start = 0
|
||||||
|
else:
|
||||||
|
p.y_range.start = round(min(data_list) - 1)
|
||||||
|
return p
|
0
src/flask/charts/__init__.py
Normal file
0
src/flask/charts/__init__.py
Normal file
@ -23,7 +23,7 @@ class pool_query:
|
|||||||
queried record
|
queried record
|
||||||
"""
|
"""
|
||||||
query = { "test_user" : f"{test_user}", "date" : f"{date}"}
|
query = { "test_user" : f"{test_user}", "date" : f"{date}"}
|
||||||
record = self.db.real_db.find_one(query)
|
record = self.db.pool_db.find_one(query)
|
||||||
if record:
|
if record:
|
||||||
self.existing_record = record
|
self.existing_record = record
|
||||||
return True
|
return True
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from bokeh.core.enums import SpatialUnitsType
|
||||||
import mongo.build_db as pool_database
|
import mongo.build_db as pool_database
|
||||||
import mongo.query_db as pool_database_query
|
import mongo.query_db as pool_database_query
|
||||||
|
|
||||||
@ -5,8 +6,13 @@ from flask import Flask, render_template, request, jsonify, redirect, session
|
|||||||
from flask_wtf import FlaskForm, CSRFProtect
|
from flask_wtf import FlaskForm, CSRFProtect
|
||||||
from flask_bootstrap import Bootstrap5
|
from flask_bootstrap import Bootstrap5
|
||||||
from wtforms import StringField, SubmitField, DateField, IntegerField, PasswordField, DecimalField, RadioField, TextAreaField
|
from wtforms import StringField, SubmitField, DateField, IntegerField, PasswordField, DecimalField, RadioField, TextAreaField
|
||||||
from wtforms.validators import DataRequired, Length
|
from wtforms.validators import DataRequired, Length, Optional
|
||||||
from waitress import serve
|
from waitress import serve
|
||||||
|
from bokeh.models.layouts import HBox
|
||||||
|
from bokeh.plotting import column
|
||||||
|
from charts import PoolCharts
|
||||||
|
from bokeh.io import output_file, show
|
||||||
|
from bokeh.layouts import row
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.secret_key = 'testsecret' #this value will change
|
app.secret_key = 'testsecret' #this value will change
|
||||||
@ -14,6 +20,21 @@ app.secret_key = 'testsecret' #this value will change
|
|||||||
bootstrap = Bootstrap5(app)
|
bootstrap = Bootstrap5(app)
|
||||||
|
|
||||||
csrf = CSRFProtect(app)
|
csrf = CSRFProtect(app)
|
||||||
|
# used to configure the bokeh plot for graphs
|
||||||
|
output_file("/pool_data/src/flask/static/data_plot.html")
|
||||||
|
|
||||||
|
def create_graphs():
|
||||||
|
chart = PoolCharts.PoolCharts()
|
||||||
|
ph = chart.line_chart("Pool PH", "ph", 30)
|
||||||
|
total_chlorine = chart.line_chart("Pool Total Chlorine", "total_chlorine", 30)
|
||||||
|
free_chlorine = chart.line_chart("Pool Free Chlorine", "free_chlorine", 30)
|
||||||
|
alkalinity = chart.line_chart("Alkalinity", "alkalinity", 30)
|
||||||
|
salt = chart.line_chart("Salt", "salt", 30)
|
||||||
|
temp = chart.line_chart("Temperature", "temp", 30)
|
||||||
|
hardness = chart.line_chart("Hardness", "hardness", 30)
|
||||||
|
stabiliser = chart.line_chart("Stabiliser", "stabiliser", 30)
|
||||||
|
show(row(column(ph, total_chlorine, free_chlorine, temp), column(salt, alkalinity, hardness, stabiliser)))
|
||||||
|
|
||||||
|
|
||||||
class userForm(FlaskForm):
|
class userForm(FlaskForm):
|
||||||
username = StringField("User Name?", validators=[DataRequired()])
|
username = StringField("User Name?", validators=[DataRequired()])
|
||||||
@ -24,15 +45,15 @@ class dataForm(FlaskForm):
|
|||||||
test_user = RadioField("Tester:",
|
test_user = RadioField("Tester:",
|
||||||
choices=[("Isabella"), ("Heather"), ("Ariah")])
|
choices=[("Isabella"), ("Heather"), ("Ariah")])
|
||||||
Date = DateField("Date:")
|
Date = DateField("Date:")
|
||||||
free_chlorine = IntegerField("Free Chlorine:")
|
free_chlorine = IntegerField("Free Chlorine:", validators=[Optional()])
|
||||||
total_chlorine = IntegerField("Total Chlorine:")
|
total_chlorine = IntegerField("Total Chlorine:", validators=[Optional()])
|
||||||
alkalinity = DecimalField("Alkalinity:")
|
alkalinity = DecimalField("Alkalinity:", validators=[Optional()])
|
||||||
PH = DecimalField("PH:")
|
PH = DecimalField("PH:", validators=[Optional()])
|
||||||
hardness = IntegerField("Hardness")
|
hardness = IntegerField("Hardness", validators=[Optional()])
|
||||||
stabiliser = IntegerField("CYA - Stabliser")
|
stabiliser = IntegerField("CYA - Stabliser", validators=[Optional()])
|
||||||
salt = IntegerField("Salt:")
|
salt = IntegerField("Salt:", validators=[Optional()])
|
||||||
temp = DecimalField("Water Temperature")
|
temp = DecimalField("Water Temperature", validators=[Optional()])
|
||||||
comment = TextAreaField("Any Comments?")
|
comment = TextAreaField("Any Comments?", validators=[Optional()])
|
||||||
submit = SubmitField("Write it, Write it REAAAAAAL GOOOD")
|
submit = SubmitField("Write it, Write it REAAAAAAL GOOOD")
|
||||||
|
|
||||||
@app.route("/", methods=["GET","POST"])
|
@app.route("/", methods=["GET","POST"])
|
||||||
@ -54,7 +75,7 @@ def index():
|
|||||||
def updater():
|
def updater():
|
||||||
if 'logged_in' not in session:
|
if 'logged_in' not in session:
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
|
create_graphs()
|
||||||
query_db = pool_database_query.pool_query()
|
query_db = pool_database_query.pool_query()
|
||||||
query = query_db.get_top(10, "ph")
|
query = query_db.get_top(10, "ph")
|
||||||
form = dataForm()
|
form = dataForm()
|
||||||
@ -87,9 +108,9 @@ def updater():
|
|||||||
new_record["salt"], new_record["comment"])
|
new_record["salt"], new_record["comment"])
|
||||||
|
|
||||||
|
|
||||||
return render_template("updater.html", list=query, form=form)
|
return render_template("updater.html", list=query, form=form, success=True, updater_name = new_record["test_user"])
|
||||||
else:
|
else:
|
||||||
return render_template("updater.html", list=query, form=form)
|
return render_template("updater.html", list=query, form=form, sucess=False)
|
||||||
|
|
||||||
@app.route("/update_db", methods=["POST"])
|
@app.route("/update_db", methods=["POST"])
|
||||||
def pool_data_update():
|
def pool_data_update():
|
||||||
@ -117,4 +138,3 @@ def user_detail(id):
|
|||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
#app.run(host='0.0.0.0')
|
#app.run(host='0.0.0.0')
|
||||||
serve(app, host='0.0.0.0', port=5000, url_scheme='https')
|
serve(app, host='0.0.0.0', port=5000, url_scheme='https')
|
||||||
|
|
||||||
|
61
src/flask/static/data_plot.html
Normal file
61
src/flask/static/data_plot.html
Normal file
File diff suppressed because one or more lines are too long
@ -1,28 +1,24 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
||||||
<title>
|
<title>{% block title %} Let there Be Pool Data {% endblock %}</title>
|
||||||
{% block title %}
|
|
||||||
Let there Be Pool Data
|
|
||||||
{% endblock %}
|
|
||||||
</title>
|
|
||||||
|
|
||||||
{{ bootstrap.load_css() }}
|
{{ bootstrap.load_css() }}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body { background: #e8f1f9; }
|
body {
|
||||||
|
background: #e8f1f9;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- this is a base template using Bootstrap-Flask
|
<!-- this is a base template using Bootstrap-Flask
|
||||||
https://bootstrap-flask.readthedocs.io/ -->
|
https://bootstrap-flask.readthedocs.io/ -->
|
||||||
|
|
||||||
{% block content %}
|
{% block content %} {% endblock %}
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
|
|
||||||
<!-- you can delete the next line if you're not using any Bootstrap JS -->
|
<!-- you can delete the next line if you're not using any Bootstrap JS -->
|
||||||
{{ bootstrap.load_js() }}
|
{{ bootstrap.load_js() }}
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
Data Input
|
Data Input
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
TIPS about using Bootstrap-Flask:
|
TIPS about using Bootstrap-Flask:
|
||||||
https://github.com/helloflask/bootstrap-flask
|
https://github.com/helloflask/bootstrap-flask
|
||||||
@ -17,6 +16,10 @@
|
|||||||
<div class="col-md-10 col-lg-8 mx-lg-auto mx-md-auto">
|
<div class="col-md-10 col-lg-8 mx-lg-auto mx-md-auto">
|
||||||
|
|
||||||
<h1 class="pt-5 pb-2">Input Data you want to store</h1>
|
<h1 class="pt-5 pb-2">Input Data you want to store</h1>
|
||||||
|
{% if success %}
|
||||||
|
<h3 class="pt-3 pb02">Thank you {{ updater_name | safe }}, Data Updated Successfully</h1>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<table border=0>
|
<table border=0>
|
||||||
@ -24,7 +27,7 @@
|
|||||||
<td>
|
<td>
|
||||||
{{ render_form(form) }}
|
{{ render_form(form) }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<!-- <td>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="col-md-10 col-lg-8 mx-lg-auto mx-md-auto">
|
<div class="col-md-10 col-lg-8 mx-lg-auto mx-md-auto">
|
||||||
|
|
||||||
@ -42,9 +45,18 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</td> -->
|
||||||
|
<td>
|
||||||
|
<div class="container">
|
||||||
|
<div class= "col-xs-12 col-sm-12 col-md-12">
|
||||||
|
<iframe style="width: 100vw;height: 80vh;position: relative;" src="static/data_plot.html" frameborder="0" allowfullscreen>
|
||||||
|
</iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user