diff --git a/code/Pipfile b/code/Pipfile index f8644d3..294f719 100644 --- a/code/Pipfile +++ b/code/Pipfile @@ -14,6 +14,7 @@ iso3166 = "*" flask-wtf = "*" flask-login = "*" flask-bootstrap = "*" +matplotlib = "*" [requires] python_version = "3.8" diff --git a/code/Pipfile.lock b/code/Pipfile.lock index 484caa2..cd43be1 100644 --- a/code/Pipfile.lock +++ b/code/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "b25fa5cfde97f34d3f4210b7c3e602df640d50105eb290daefa14c194b289936" + "sha256": "80f6c409aa1104974ff405c48d5527140030fb7e8d6bbf8aa21e884b3d151b5a" }, "pipfile-spec": 6, "requires": { @@ -23,6 +23,13 @@ ], "version": "==7.0" }, + "cycler": { + "hashes": [ + "sha256:1d8a5ae1ff6c5cf9b93e8811e581232ad8920aeec647c37316ceac982b08cb2d", + "sha256:cd7b2d1018258d7247a71425e9f26463dfb444d411c39569972f4ce586b0c9d8" + ], + "version": "==0.10.0" + }, "dominate": { "hashes": [ "sha256:6e833aea505f0236a9fc692326bac575f8bd38ae0f3a1bdc73d20ca606ac75d5", @@ -90,6 +97,48 @@ ], "version": "==2.10.3" }, + "kiwisolver": { + "hashes": [ + "sha256:05b5b061e09f60f56244adc885c4a7867da25ca387376b02c1efc29cc16bcd0f", + "sha256:210d8c39d01758d76c2b9a693567e1657ec661229bc32eac30761fa79b2474b0", + "sha256:26f4fbd6f5e1dabff70a9ba0d2c4bd30761086454aa30dddc5b52764ee4852b7", + "sha256:3b15d56a9cd40c52d7ab763ff0bc700edbb4e1a298dc43715ecccd605002cf11", + "sha256:3b2378ad387f49cbb328205bda569b9f87288d6bc1bf4cd683c34523a2341efe", + "sha256:400599c0fe58d21522cae0e8b22318e09d9729451b17ee61ba8e1e7c0346565c", + "sha256:47b8cb81a7d18dbaf4fed6a61c3cecdb5adec7b4ac292bddb0d016d57e8507d5", + "sha256:53eaed412477c836e1b9522c19858a8557d6e595077830146182225613b11a75", + "sha256:58e626e1f7dfbb620d08d457325a4cdac65d1809680009f46bf41eaf74ad0187", + "sha256:5a52e1b006bfa5be04fe4debbcdd2688432a9af4b207a3f429c74ad625022641", + "sha256:5c7ca4e449ac9f99b3b9d4693debb1d6d237d1542dd6a56b3305fe8a9620f883", + "sha256:682e54f0ce8f45981878756d7203fd01e188cc6c8b2c5e2cf03675390b4534d5", + "sha256:76275ee077772c8dde04fb6c5bc24b91af1bb3e7f4816fd1852f1495a64dad93", + "sha256:79bfb2f0bd7cbf9ea256612c9523367e5ec51d7cd616ae20ca2c90f575d839a2", + "sha256:7f4dd50874177d2bb060d74769210f3bce1af87a8c7cf5b37d032ebf94f0aca3", + "sha256:8944a16020c07b682df861207b7e0efcd2f46c7488619cb55f65882279119389", + "sha256:8aa7009437640beb2768bfd06da049bad0df85f47ff18426261acecd1cf00897", + "sha256:9105ce82dcc32c73eb53a04c869b6a4bc756b43e4385f76ea7943e827f529e4d", + "sha256:933df612c453928f1c6faa9236161a1d999a26cd40abf1dc5d7ebbc6dbfb8fca", + "sha256:939f36f21a8c571686eb491acfffa9c7f1ac345087281b412d63ea39ca14ec4a", + "sha256:9491578147849b93e70d7c1d23cb1229458f71fc79c51d52dce0809b2ca44eea", + "sha256:9733b7f64bd9f807832d673355f79703f81f0b3e52bfce420fc00d8cb28c6a6c", + "sha256:a02f6c3e229d0b7220bd74600e9351e18bc0c361b05f29adae0d10599ae0e326", + "sha256:a0c0a9f06872330d0dd31b45607197caab3c22777600e88031bfe66799e70bb0", + "sha256:aa716b9122307c50686356cfb47bfbc66541868078d0c801341df31dca1232a9", + "sha256:acc4df99308111585121db217681f1ce0eecb48d3a828a2f9bbf9773f4937e9e", + "sha256:b64916959e4ae0ac78af7c3e8cef4becee0c0e9694ad477b4c6b3a536de6a544", + "sha256:d22702cadb86b6fcba0e6b907d9f84a312db9cd6934ee728144ce3018e715ee1", + "sha256:d3fcf0819dc3fea58be1fd1ca390851bdb719a549850e708ed858503ff25d995", + "sha256:d52e3b1868a4e8fd18b5cb15055c76820df514e26aa84cc02f593d99fef6707f", + "sha256:db1a5d3cc4ae943d674718d6c47d2d82488ddd94b93b9e12d24aabdbfe48caee", + "sha256:e3a21a720791712ed721c7b95d433e036134de6f18c77dbe96119eaf7aa08004", + "sha256:e8bf074363ce2babeb4764d94f8e65efd22e6a7c74860a4f05a6947afc020ff2", + "sha256:f16814a4a96dc04bf1da7d53ee8d5b1d6decfc1a92a63349bb15d37b6a263dd9", + "sha256:f2b22153870ca5cf2ab9c940d7bc38e8e9089fa0f7e5856ea195e1cf4ff43d5a", + "sha256:f790f8b3dff3d53453de6a7b7ddd173d2e020fb160baff578d578065b108a05f", + "sha256:fe51b79da0062f8e9d49ed0182a626a7dc7a0cbca0328f612c6ee5e4711c81e4" + ], + "version": "==1.1.0" + }, "markupsafe": { "hashes": [ "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", @@ -123,6 +172,25 @@ ], "version": "==1.1.1" }, + "matplotlib": { + "hashes": [ + "sha256:08ccc8922eb4792b91c652d3e6d46b1c99073f1284d1b6705155643e8046463a", + "sha256:161dcd807c0c3232f4dcd4a12a382d52004a498174cbfafd40646106c5bcdcc8", + "sha256:1f9e885bfa1b148d16f82a6672d043ecf11197f6c71ae222d0546db706e52eb2", + "sha256:2d6ab54015a7c0d727c33e36f85f5c5e4172059efdd067f7527f6e5d16ad01aa", + "sha256:5d2e408a2813abf664bd79431107543ecb449136912eb55bb312317edecf597e", + "sha256:61c8b740a008218eb604de518eb411c4953db0cb725dd0b32adf8a81771cab9e", + "sha256:80f10af8378fccc136da40ea6aa4a920767476cdfb3241acb93ef4f0465dbf57", + "sha256:819d4860315468b482f38f1afe45a5437f60f03eaede495d5ff89f2eeac89500", + "sha256:8cc0e44905c2c8fda5637cad6f311eb9517017515a034247ab93d0cf99f8bb7a", + "sha256:8e8e2c2fe3d873108735c6ee9884e6f36f467df4a143136209cff303b183bada", + "sha256:98c2ffeab8b79a4e3a0af5dd9939f92980eb6e3fec10f7f313df5f35a84dacab", + "sha256:d59bb0e82002ac49f4152963f8a1079e66794a4f454457fd2f0dcc7bf0797d30", + "sha256:ee59b7bb9eb75932fe3787e54e61c99b628155b0cedc907864f24723ba55b309" + ], + "index": "pypi", + "version": "==3.1.2" + }, "numpy": { "hashes": [ "sha256:1786a08236f2c92ae0e70423c45e1e62788ed33028f94ca99c4df03f5be6b3c6", @@ -182,6 +250,13 @@ "index": "pypi", "version": "==0.9.3" }, + "pyparsing": { + "hashes": [ + "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f", + "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec" + ], + "version": "==2.4.6" + }, "python-dateutil": { "hashes": [ "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", diff --git a/code/app/forms.py b/code/app/forms.py index 5946cc3..b081359 100644 --- a/code/app/forms.py +++ b/code/app/forms.py @@ -10,7 +10,7 @@ class LoginForm(FlaskForm): submit = SubmitField("Sign In") -class YearForm(FlaskForm): +class AnnualForm(FlaskForm): year_list = [ ("2011", 2011), ("2012", 2012), @@ -26,22 +26,6 @@ class YearForm(FlaskForm): submit = SubmitField("Search") -class IntervalForm(FlaskForm): - year_list = [ - ("2011", 2011), - ("2012", 2012), - ("2013", 2013), - ("2014", 2014), - ("2015", 2015), - ("2016", 2016), - ("2017", 2017), - ("2018", 2018), - ] - name = StringField("Glacier Name") - lower_bound = SelectField( - "First year", validators=[DataRequired()], choices=year_list - ) - upper_bound = SelectField( - "Second year", validators=[DataRequired()], choices=year_list - ) +class PlotForm(FlaskForm): + name = StringField("Glacier Name", validators=[DataRequired()]) submit = SubmitField("Search") diff --git a/code/app/routes.py b/code/app/routes.py index 0b38a37..69fd314 100644 --- a/code/app/routes.py +++ b/code/app/routes.py @@ -1,11 +1,10 @@ -from app import app, db -from app.forms import LoginForm, YearForm, IntervalForm -from app.models import User, Annual_Data, Glacier -from flask import flash, redirect, render_template, url_for, request -from flask_login import current_user, login_user, logout_user, login_required +from app import app +from app.forms import AnnualForm, LoginForm, PlotForm +from database.queries import query_annual_data, query_plot_data, query_user +from flask import flash, redirect, render_template, request, url_for, send_file +from flask_login import current_user, login_required, login_user, logout_user +from processing.dataframe import create_table, create_plot from werkzeug.urls import url_parse -from processing.tabulate import create_table -from database.queries import query_annual_data, query_user @app.route("/") @@ -57,7 +56,7 @@ def data(): @app.route("/table_selection", methods=["GET", "POST"]) def table_selection(): - form = YearForm() + form = AnnualForm() if form.validate_on_submit(): query = query_annual_data(form) table = create_table(query.statement) @@ -70,6 +69,22 @@ def table(): return render_template("table.html", table=table, title="Table") -@app.route("/plots") -def plots(): - return render_template("data.html", title="Data") +@app.route("/plot_selection", methods=["GET", "POST"]) +def plot_selection(): + form = PlotForm() + if form.validate_on_submit(): + query = query_plot_data(form) + plot = create_plot(query.statement) + return render_template("plot.html", title="Plot", plot=plot) + return render_template("plot_selection.html", title="Data", form=form) + + +@app.route("/plot") +def plot(): + return render_template("plot.html", title="Plot", plot=plot) + + +@app.route("/figure") +def figure(query): + fig = create_plot(query) + return send_file(fig, mimetype="image/png") diff --git a/code/app/static/plot.png b/code/app/static/plot.png new file mode 100644 index 0000000..549d240 Binary files /dev/null and b/code/app/static/plot.png differ diff --git a/code/app/templates/data.html b/code/app/templates/data.html index ce3d756..0674660 100644 --- a/code/app/templates/data.html +++ b/code/app/templates/data.html @@ -4,12 +4,12 @@

What do you want to check out?

  • - Tables + Tables

  • - Plots + Plots

  • {% endblock %} diff --git a/code/app/templates/index.html b/code/app/templates/index.html index 3fb9939..f02eae1 100644 --- a/code/app/templates/index.html +++ b/code/app/templates/index.html @@ -2,7 +2,8 @@ {% block content %}
    -

    IGDB: Internation Glacier Database

    -

    The IGDB is a database, that uses data from the WGMS to illustrate the consequences of climate change.

    +

    IGDB: Internation Glacier Database

    +

    The IGDB is a database, that uses data from the WGMS to illustrate the consequences of climate change.

    +

    Our system allows you to visualize data with tables and plots, via our intuitive Web UI.

    {% endblock %} diff --git a/code/app/templates/plot.html b/code/app/templates/plot.html new file mode 100644 index 0000000..41cb64c --- /dev/null +++ b/code/app/templates/plot.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} +{% import 'bootstrap/wtf.html' as wtf %} + +{% block app_content %} +

    Plot

    +Image Placeholder +

    Back

    +{% endblock %} diff --git a/code/app/templates/plot_selection.html b/code/app/templates/plot_selection.html new file mode 100644 index 0000000..2ed5a35 --- /dev/null +++ b/code/app/templates/plot_selection.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% import 'bootstrap/wtf.html' as wtf %} + +{% block app_content %} +

    Plot selection

    +
    +
    + {{ wtf.quick_form(form) }} +
    +
    +
    +{% endblock %} diff --git a/code/database/queries.py b/code/database/queries.py index 80b3159..e03df0e 100644 --- a/code/database/queries.py +++ b/code/database/queries.py @@ -23,3 +23,16 @@ def query_annual_data(form) -> BaseQuery: def query_user(form) -> BaseQuery: user = User.query.filter_by(username=form.username.data).first() return user + + +def query_plot_data(form) -> BaseQuery: + query = ( + db.session.query(Annual_Data) + .join(Glacier, Glacier.id == Annual_Data.id) + .filter_by(name=form.name.data) + .group_by(Annual_Data.year) + ) + if query.first() is None: + flash("Sorry, no results found") + return redirect(url_for("plot_selection")) + return query diff --git a/code/igdb.py b/code/igdb.py index bd0b550..6067591 100644 --- a/code/igdb.py +++ b/code/igdb.py @@ -1,6 +1,6 @@ from app import app from database import db_setup, export, parser -from processing import tabulate +from processing import dataframe db_setup.main() parser.main() diff --git a/code/processing/dataframe.py b/code/processing/dataframe.py new file mode 100644 index 0000000..a799df0 --- /dev/null +++ b/code/processing/dataframe.py @@ -0,0 +1,45 @@ +from app import db +from io import BytesIO +from pandas import DataFrame, read_sql +from base64 import b64encode + + +def create_dataframe(query) -> DataFrame: + df = read_sql(sql=query, con=db.engine) + return df + + +def render_table(df) -> str: + df.fillna(value=0, inplace=True) + table = df.to_html(classes=["table-striped", "table-hover"]) + return table + + +def render_plot(df): + df.fillna(value=0, inplace=True) + plot = df.plot("year", ["surface", "length", "elevation"], kind="bar") + plot_figure = plot.get_figure() + figure = BytesIO() + plot_figure.savefig(figure) + figure.seek(0) + return figure + + +def encode_plot(plot): + buffer = b"".join(plot) + buf = b64encode(buffer) + encoded_plot = buf.decode("utf-8") + return encoded_plot + + +def create_table(query) -> str: + df = create_dataframe(query) + html_table = render_table(df) + return html_table + + +def create_plot(query): + df = create_dataframe(query) + plot = render_plot(df) + encoded_plot = encode_plot(plot) + return encoded_plot diff --git a/code/processing/tabulate.py b/code/processing/tabulate.py deleted file mode 100644 index 275a9db..0000000 --- a/code/processing/tabulate.py +++ /dev/null @@ -1,19 +0,0 @@ -from app import db -from pandas import DataFrame, read_sql - - -def create_dataframe(query) -> DataFrame: - df = read_sql(sql=query, con=db.engine) - return df - - -def render_table(df) -> str: - df.fillna(value=0, inplace=True) - table = df.to_html(classes=["table-striped", "table-hover"]) - return table - - -def create_table(query) -> str: - df = create_dataframe(query) - html_table = render_table(df) - return html_table