From 1f988a966fa6bfae9489b3dc0d2d14b7a3b20cad Mon Sep 17 00:00:00 2001 From: ldmnch <48388233+ldmnch@users.noreply.github.com> Date: Sat, 10 May 2025 12:54:56 +0200 Subject: [PATCH 1/2] modifications of app for assignment 4 --- flaskapp/models.py | 41 ----- flaskapp/routes.py | 209 ++++++++++++++++++-------- flaskapp/static/style.css | 18 +++ flaskapp/templates/about.html | 5 - flaskapp/templates/create_post.html | 18 --- flaskapp/templates/dashboard.html | 35 ++++- flaskapp/templates/home.html | 57 +++++-- flaskapp/templates/layout.html | 47 +++--- flaskapp/templates/relationships.html | 39 +++++ 9 files changed, 292 insertions(+), 177 deletions(-) create mode 100644 flaskapp/static/style.css delete mode 100644 flaskapp/templates/about.html delete mode 100644 flaskapp/templates/create_post.html create mode 100644 flaskapp/templates/relationships.html diff --git a/flaskapp/models.py b/flaskapp/models.py index 4e64a954a..4104941cb 100644 --- a/flaskapp/models.py +++ b/flaskapp/models.py @@ -1,47 +1,6 @@ from flaskapp import db from datetime import datetime - -# Defining a model for users -class User(db.Model): - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(20), nullable=False) - posts = db.relationship('BlogPost', backref='author', lazy=True) - - def __repr__(self): - return f"User('{self.name}', '{self.id}'')" - - -# Defining a model for blog posts ('models' are used to represent tables in your database). -class BlogPost(db.Model): - id = db.Column(db.Integer, primary_key=True) - title = db.Column(db.String(100), nullable=False) - content = db.Column(db.Text, nullable=False) - # author = db.Column(db.String(50), nullable=False) - date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) - user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) - - def __repr__(self): - return f"BlogPost('{self.title}', '{self.date_posted}')" - - -class Day(db.Model): - # __tablename__ = 'day' # if you wanted to, you could change the default table name here - id = db.Column(db.Date, primary_key=True) - views = db.Column(db.Integer) - - def __repr__(self): - return f"Day('{self.id}', '{self.views}')" - - -class IpView(db.Model): - ip = db.Column(db.String(20), primary_key=True) - date_id = db.Column(db.Date, db.ForeignKey('day.id'), primary_key=True) - - def __repr__(self): - return f"IpView('{self.ip}', '{self.date_id}')" - - # 2010-2019 BES Constituency Results with Census and Candidate Data # from: https://www.britishelectionstudy.com/data-objects/linked-data/ # citation: Fieldhouse, E., J. Green., G. Evans., J. Mellon & C. Prosser (2019) British Election Study 2019 Constituency Results file, version 1.1, DOI: 10.48420/20278599 diff --git a/flaskapp/routes.py b/flaskapp/routes.py index ac7f7f5f0..70f7770ed 100644 --- a/flaskapp/routes.py +++ b/flaskapp/routes.py @@ -1,73 +1,150 @@ -from flask import render_template, flash, redirect, url_for, request -from flaskapp import app, db -from flaskapp.models import BlogPost, IpView, Day -from flaskapp.forms import PostForm -import datetime - +from flask import render_template, request +from flaskapp import app +from flaskapp.models import UkData import pandas as pd -import json -import plotly -import plotly.express as px - +import matplotlib +matplotlib.use('Agg') # Set backend before importing pyplot +import matplotlib.pyplot as plt +import seaborn as sns +from io import BytesIO +import base64 + +ELECTION_COLUMNS = ['Turnout19', 'ConVote19', 'LabVote19', 'LDVote19', + 'SNPVote19', 'PCVote19', 'UKIPVote19', 'GreenVote19', + 'BrexitVote19', 'TotalVote19'] + +DEMOGRAPHIC_COLUMNS = [ + 'c11PopulationDensity', 'c11Female', 'c11FulltimeStudent', + 'c11Retired', 'c11HouseOwned', 'c11HouseholdMarried' +] -# Route for the home page, which is where the blog posts will be shown @app.route("/") -@app.route("/home") def home(): - # Querying all blog posts from the database - posts = BlogPost.query.all() - return render_template('home.html', posts=posts) - - -# Route for the about page -@app.route("/about") -def about(): - return render_template('about.html', title='About page') - - -# Route to where users add posts (needs to accept get and post requests) -@app.route("/post/new", methods=['GET', 'POST']) -def new_post(): - form = PostForm() - if form.validate_on_submit(): - post = BlogPost(title=form.title.data, content=form.content.data, user_id=1) - db.session.add(post) - db.session.commit() - flash('Your post has been created!', 'success') - return redirect(url_for('home')) - return render_template('create_post.html', title='New Post', form=form) - + return render_template('home.html') -# Route to the dashboard page @app.route('/dashboard') def dashboard(): - days = Day.query.all() - df = pd.DataFrame([{'Date': day.id, 'Page views': day.views} for day in days]) - - fig = px.bar(df, x='Date', y='Page views') - - graphJSON = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder) - return render_template('dashboard.html', title='Page views per day', graphJSON=graphJSON) - - -@app.before_request -def before_request_func(): - day_id = datetime.date.today() # get our day_id - client_ip = request.remote_addr # get the ip address of where the client request came from - - query = Day.query.filter_by(id=day_id) # try to get the row associated to the current day - if query.count() > 0: - # the current day is already in table, simply increment its views - current_day = query.first() - current_day.views += 1 - else: - # the current day does not exist, it's the first view for the day. - current_day = Day(id=day_id, views=1) - db.session.add(current_day) # insert a new day into the day table - - query = IpView.query.filter_by(ip=client_ip, date_id=day_id) - if query.count() == 0: # check if it's the first time a viewer from this ip address is viewing the website - ip_view = IpView(ip=client_ip, date_id=day_id) - db.session.add(ip_view) # insert into the ip_view table - - db.session.commit() # commit all the changes to the database + """First dashboard with bar chart by country""" + selected_var = request.args.get('variable', 'TotalVote19') + + # Get data + df = get_dataframe(['country', selected_var]) + country_means = df.groupby('country')[selected_var].mean().reset_index() + + # Create plot + plot_data = create_bar_plot( + data=country_means, + x='country', + y=selected_var, + title=f'Average {selected_var} by Country' + ) + + return render_template( + 'dashboard.html', + plot_data=plot_data, + numeric_columns=ELECTION_COLUMNS, + selected_var=selected_var + ) + +@app.route('/relationships') +def relationships(): + """Second dashboard with faceted scatter plots""" + x_var = request.args.get('x_var', 'c11PopulationDensity') + y_var = request.args.get('y_var', 'Turnout19') + + # Get data + df = get_dataframe(['country', x_var, y_var]) + + # Create plot + plot_data = create_scatter_plot( + data=df, + x_var=x_var, + y_var=y_var, + title=f'{y_var} vs {x_var} by Country' + ) + + return render_template( + 'relationships.html', + plot_data=plot_data, + demog_columns = DEMOGRAPHIC_COLUMNS, + election_columns = ELECTION_COLUMNS, + x_var=x_var, + y_var=y_var + ) + +# Helper functions +def get_dataframe(columns): + """Get DataFrame with specified columns from database""" + data = UkData.query.all() + + df = pd.DataFrame([{ + 'id': c.id, + 'constituency_name': c.constituency_name, + 'country': c.country, + 'region': c.region, + 'Turnout19': c.Turnout19, + 'ConVote19': c.ConVote19, + 'LabVote19': c.LabVote19, + 'LDVote19': c.LDVote19, + 'SNPVote19': c.SNPVote19, + 'PCVote19': c.PCVote19, + 'UKIPVote19': c.UKIPVote19, + 'GreenVote19': c.GreenVote19, + 'BrexitVote19': c.BrexitVote19, + 'TotalVote19': c.TotalVote19, + 'c11PopulationDensity': c.c11PopulationDensity, + 'c11Female': c.c11Female, + 'c11FulltimeStudent': c.c11FulltimeStudent, + 'c11Retired': c.c11Retired, + 'c11HouseOwned': c.c11HouseOwned, + 'c11HouseholdMarried': c.c11HouseholdMarried + } for c in data]) + + return df + +def create_bar_plot(data, x, y, title): + """Create a bar plot with values annotated""" + plt.figure(figsize=(10, 6)) + sns.set_style("whitegrid") + + ax = sns.barplot(x=x, y=y, data=data, palette="Blues_d") + + # Add values on bars + for p in ax.patches: + ax.annotate( + f"{p.get_height():,.1f}", + (p.get_x() + p.get_width() / 2., p.get_height()), + ha='center', va='center', xytext=(0, 10), + textcoords='offset points' + ) + + plt.title(title, pad=20) + plt.xlabel(x) + plt.ylabel(y) + plt.xticks(rotation=45) + plt.tight_layout() + + return save_plot_to_base64() + +def create_scatter_plot(data, x_var, y_var, title): + """Create a scatter plot with regression line""" + plt.figure(figsize=(10, 6)) + sns.set_style("whitegrid") + + ax = sns.regplot(x=x_var, y=y_var, data=data, scatter_kws={'alpha':0.5}, line_kws={'color':'red'}) + + plt.title(title, pad=20) + plt.xlabel(x_var) + plt.ylabel(y_var) + plt.tight_layout() + + return save_plot_to_base64() + +def save_plot_to_base64(): + """Save current plot to base64 encoded string""" + buffer = BytesIO() + plt.savefig(buffer, format='png', dpi=100, bbox_inches='tight') + buffer.seek(0) + plot_data = base64.b64encode(buffer.getvalue()).decode('utf-8') + plt.close() + return plot_data diff --git a/flaskapp/static/style.css b/flaskapp/static/style.css new file mode 100644 index 000000000..bbbc23df9 --- /dev/null +++ b/flaskapp/static/style.css @@ -0,0 +1,18 @@ +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background-color: #f8f9fa; /* light gray */ + color: #212529; /* dark text */ +} + +.navbar { + background-color: #004080 !important; /* override Bootstrap dark */ +} + +.navbar-brand, .nav-link { + color: #ffffff !important; +} + +.nav-link.active { + font-weight: bold; + color: #ffd700 !important; /* gold */ +} diff --git a/flaskapp/templates/about.html b/flaskapp/templates/about.html deleted file mode 100644 index c7da59267..000000000 --- a/flaskapp/templates/about.html +++ /dev/null @@ -1,5 +0,0 @@ -{% extends "layout.html" %} -{% block content %} -

{{ title }}

-

This is where I'll write something about myself.

-{% endblock %} diff --git a/flaskapp/templates/create_post.html b/flaskapp/templates/create_post.html deleted file mode 100644 index d1fa764dc..000000000 --- a/flaskapp/templates/create_post.html +++ /dev/null @@ -1,18 +0,0 @@ -{% extends "layout.html" %} -{% block content %} -

{{ title }}

-
- {{ form.hidden_tag() }} -
- {{ form.title.label() }}
- {{ form.title }} -
-
- {{ form.content.label() }}
- {{ form.content() }} -
-
- {{ form.submit() }} -
-
-{% endblock %} diff --git a/flaskapp/templates/dashboard.html b/flaskapp/templates/dashboard.html index 3eb6e0ede..ef1cc70fb 100644 --- a/flaskapp/templates/dashboard.html +++ b/flaskapp/templates/dashboard.html @@ -1,10 +1,29 @@ -{% extends "layout.html" %} +{% extends "home.html" %} + {% block content %} -

{{ title }}

-
- - +

Election Results by Country

+ +
+
+
+
+ + +
+
+ +
+
+ +
+ +
+
+
{% endblock %} \ No newline at end of file diff --git a/flaskapp/templates/home.html b/flaskapp/templates/home.html index 8b0b1c41b..bb538a02e 100644 --- a/flaskapp/templates/home.html +++ b/flaskapp/templates/home.html @@ -1,10 +1,49 @@ -{% extends 'layout.html' %} -{% block content %} - {% for post in posts %} -
-

{{ post.title }}

-

By {{ post.author.name }} on {{ post.date_posted.strftime('%Y-%m-%d') }}

-

{{ post.content }}

+ + + + UK Election Dashboard + + + + + + +
+
+

UK Election Data Dashboard

+

This dashboard provides interactive visualizations of UK election data.

+

Explore country-wide voting averages and discover relationships between different political and demographic factors across constituencies.

+

Use the tabs above to navigate between different sections of the dashboard.

+

The information from these dashboards come from the British Election Study 2019.

+ +
+ + {% block content %}{% endblock %}
- {% endfor %} -{% endblock %} \ No newline at end of file + + + + \ No newline at end of file diff --git a/flaskapp/templates/layout.html b/flaskapp/templates/layout.html index 5c525292d..3448b66ec 100644 --- a/flaskapp/templates/layout.html +++ b/flaskapp/templates/layout.html @@ -1,38 +1,25 @@ - - - + UK Election Dashboard + + -
-
-

My web app

- - - + + +
+ {% block content %}{% endblock %}
-
-
- {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - {% for category, message in messages %} -
- {{ message }} -
- {% endfor %} - {% endif %} - {% endwith %} - {% block content %} - {% endblock %} -
+ + \ No newline at end of file diff --git a/flaskapp/templates/relationships.html b/flaskapp/templates/relationships.html new file mode 100644 index 000000000..9c5547ee0 --- /dev/null +++ b/flaskapp/templates/relationships.html @@ -0,0 +1,39 @@ +{% extends "home.html" %} + +{% block content %} +

Explore Relationships

+ +
+
+
+
+ + +
+
+ + +
+
+ +
+
+ +
+ +
+
+
+{% endblock %} \ No newline at end of file From 875e1c7cbfa661f533ea52818e26c9eec60dd421 Mon Sep 17 00:00:00 2001 From: ldmnch <48388233+ldmnch@users.noreply.github.com> Date: Mon, 26 May 2025 10:09:28 +0200 Subject: [PATCH 2/2] modify gitignore --- .gitignore | 2 -- instance/config.py | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) create mode 100644 instance/config.py diff --git a/.gitignore b/.gitignore index b525ed32f..3e39268e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ .idea/ .DS_Store __pycache__/ -#instance/ -*config.py \ No newline at end of file diff --git a/instance/config.py b/instance/config.py new file mode 100644 index 000000000..9b5409fa9 --- /dev/null +++ b/instance/config.py @@ -0,0 +1 @@ +SECRET_KEY = 'password' \ No newline at end of file