From 1eea185d337eadd7a5309a707d8f0c5cd0d85d9c Mon Sep 17 00:00:00 2001
From: jwansek <eddie.atten.ea29@gmail.com>
Date: Sun, 7 May 2023 21:03:25 +0100
Subject: Added some charts

---
 app.py                     |  50 ++++++++++++++--
 charts.json                |  32 ++++++++--
 database.py                |  37 ++++++++++++
 parser.py                  |   2 +-
 static/scripts.js          | 145 ++++++++++++++++++++++++++++++++++++++++++++-
 static/style.css           |  44 +++++++++++++-
 templates/plot.html.j2     |   8 +++
 templates/template.html.j2 |  34 ++++++++++-
 8 files changed, 338 insertions(+), 14 deletions(-)
 create mode 100644 templates/plot.html.j2

diff --git a/app.py b/app.py
index 2d6d8ec..a88f2c4 100644
--- a/app.py
+++ b/app.py
@@ -15,19 +15,33 @@ else:
 
 @app.route("/")
 def serve_index():
-    with open("charts.json", "r") as f:
-        charts = json.load(f)
-
     return flask.render_template(
         "index.html.j2",
         title = "UK Gender Pay Gap",
-        charts = charts["index"]
+        charts = get_charts()["index"]
     )
 
+def get_charts():
+    with open("charts.json", "r") as f:
+        return json.load(f)
+
+@app.route("/api/charts.json")
+def serve_charts():
+    return flask.jsonify(get_charts())
+
+
 @app.route("/search_click", methods = ["POST"])
 def search_redirect():
     return flask.redirect("/search?s=%s" % urllib.parse.quote_plus(dict(flask.request.form)["search"]))
 
+@app.route("/api/years")
+def api_get_years():
+    pay_type = flask.request.args.get("Pay Type")
+    if pay_type is None or pay_type.lower() not in {'hourly', 'bonuses'}:
+        return flask.abort(400, "The key `pay type` must be equal to 'hourly' or 'bonuses'")
+    with database.PayGapDatabase(host = host) as db:
+        return flask.jsonify(db.get_pay_by_year(pay_type))
+
 @app.route("/search")
 def search():
     with database.PayGapDatabase(host = host) as db:
@@ -42,5 +56,33 @@ def search():
             companies = companies
         )
 
+def get_chart_elem(url):
+    for i in get_charts()["index"]:
+        if i["url"] == url:
+            return i
+
+
+@app.route("/plot/<name>")
+def serve_large_plot(name):
+    with database.PayGapDatabase(host = host) as db:
+        elem = get_chart_elem(flask.request.full_path)
+        filters = elem["filters"]
+        for k, v in filters.items():
+            if v == "<SICType>":
+                filters[k] = {"options": db.get_sic_sections()}
+
+    current_filters = dict(flask.request.args)
+    print(filters)
+    print(current_filters)
+    return flask.render_template(
+        "plot.html.j2",
+        title = elem["title"],
+        elem = elem,
+        alt = "Lorem ipsum.",
+        filters = filters,
+        current_filters = current_filters,
+        len = len
+    )
+
 if __name__ == "__main__":
     app.run("0.0.0.0", port = 5005, debug = True)
\ No newline at end of file
diff --git a/charts.json b/charts.json
index 45a5111..2bc3ef9 100644
--- a/charts.json
+++ b/charts.json
@@ -1,12 +1,36 @@
 {
     "index": [
         {
-            "title": "Median Hourly Pay Difference",
-            "url": "/plot/medianhourly"
+            "title": "Median Hourly Pay Difference by Year",
+            "url": "/plot/years?Pay+Type=Hourly",
+            "filters": {
+                "Pay Type": {
+                    "options": [
+                        "Hourly",
+                        "Bonuses"
+                    ],
+                    "default": "Hourly"
+                },
+                "SIC Type": "<SICType>",
+                "Employer Type": "<CompanyType>",
+                "Employer Size": "<CompanySize>"
+            }
         },
         {
-            "title": "Median Bonus Pay Difference",
-            "url": "/plot/medianbonus"
+            "title": "Median Bonus Pay Difference by Year",
+            "url": "/plot/years?Pay+Type=Bonuses",
+            "filters": {
+                "Pay Type": {
+                    "options": [
+                        "Hourly",
+                        "Bonuses"
+                    ],
+                    "default": "Hourly"
+                },
+                "SIC Type": "<SICType>",
+                "Employer Type": "<CompanyType>",
+                "Employer Size": "<CompanySize>"
+            }
         },
         {
             "title": "Pay Difference by SIC Section REallt LONNNNNNNNNNNNNNNNNNNNNNNGGGGGGGGGGGGG",
diff --git a/database.py b/database.py
index e368d02..cfb1eb0 100644
--- a/database.py
+++ b/database.py
@@ -219,3 +219,40 @@ class PayGapDatabase:
             ))
 
             return [(i[0].title(), i[1]) for i in cursor.fetchall()]
+
+    def get_company_types(self):
+        with self.__connection.cursor() as cursor:
+            cursor.execute("SELECT DISTINCT type_ FROM employer WHERE type_ IS NOT NULL;")
+            return [i[0] for i in cursor.fetchall()]
+
+    def get_company_sizes(self):
+        return [
+            "Not Provided",
+            "Less than 250",
+            "250 to 499",
+            "500 to 999",
+            "1000 to 4999",
+            "5000 to 19,999",
+            "20,000 or more"
+        ]
+
+    def get_sic_sections(self):
+        with self.__connection.cursor() as cursor:
+            cursor.execute("SELECT sic_section_name FROM sic_sections")
+            return [i[0] for i in cursor.fetchall()]
+
+    def _source_name_to_year(self, source):
+        return os.path.splitext(source)[0].split("-")[-1].strip().replace("to", "-")
+
+    def get_pay_by_year(self, pay_type):
+        sql = "SELECT source, -AVG("
+        if pay_type.lower() == "hourly":
+            sql += "DiffMedianHourlyPercent"
+        elif pay_type.lower() == "bonuses":
+            sql += "DiffMedianBonusPercent"
+        sql += ") FROM pay GROUP BY source ORDER BY source;"
+
+        with self.__connection.cursor() as cursor:
+            cursor.execute(sql)
+            return [(self._source_name_to_year(i[0]), float(i[1])) for i in cursor.fetchall()]
+
diff --git a/parser.py b/parser.py
index 2918d73..df8d9aa 100644
--- a/parser.py
+++ b/parser.py
@@ -78,4 +78,4 @@ if __name__ == "__main__":
         host = "db"
 
     with database.PayGapDatabase(host = host) as db:
-        parse_csv(db, sys.argv[1])
\ No newline at end of file
+        print(db.get_sic_sections())
\ No newline at end of file
diff --git a/static/scripts.js b/static/scripts.js
index dc43112..6fec221 100644
--- a/static/scripts.js
+++ b/static/scripts.js
@@ -1,3 +1,144 @@
+function collapseTogglePress(elem) {
+    console.log("elem");
+}
+
+const PLOT_FUNC_MAPPINGS = {
+    "years": draw_plot_years,
+}
+
 $(document).ready(function() {
-    console.log("ready!")
-});
\ No newline at end of file
+    console.log("ready!");
+
+    fetch("/api/charts.json").then((resp) => {
+        resp.json().then((body) => {
+            const CHARTS = body;
+
+            var minicharts = document.getElementsByClassName("minichart");
+            for (var i = 0; i < minicharts.length; i++) {
+                var theId = minicharts.item(i).id;
+                var u = new URL(window.location.origin + theId);
+                var theIdSplit = u.pathname.split("/");
+
+                CHARTS["index"].forEach(element => {
+                    if (theId === "/minichart" + element.url) {
+                        filters = element["filters"];
+                    }
+                });
+
+                PLOT_FUNC_MAPPINGS[theIdSplit[theIdSplit.length - 1]](theId, filters);
+            }
+
+            var charts = document.getElementsByClassName("chart");
+            for (var i = 0; i < charts.length; i++) {
+                var theId = charts.item(i).id;
+                var u = new URL(window.location.origin + theId);
+                var theIdSplit = u.pathname.split("/");
+
+                CHARTS["index"].forEach(element => {
+                    if (theId === "/chart" + element.url) {
+                        filters = element["filters"];
+                    }
+                });
+
+                PLOT_FUNC_MAPPINGS[theIdSplit[theIdSplit.length - 1]](theId, filters);
+            }
+        })
+    })
+});
+
+function form_api_url(containerName, filters) {
+    var name = containerName.split("/")[containerName.split("/").length - 1];
+    var url = new URL(window.location.origin + "/api/" + name);
+    for (const [filterName, value] of Object.entries(filters)) {
+        
+        if (typeof value === 'object' && value !== null) {
+            if ("default" in value) {
+                // console.log(filterName, value["default"]);
+                url.searchParams.append(filterName, value["default"]);
+            }
+        }       
+    }
+    return url.toString();
+}
+
+function draw_plot_years(containerName, filters) {
+    fetch(form_api_url(containerName, filters)).then(resp => {
+        resp.json().then((data) => {
+            if (containerName.substring(1, 6) === "chart") {
+                var yAxisTitle = true;
+                var xAxisLabels = true;
+                var showLegend = true;
+            } else {
+                var yAxisTitle = false;
+                var xAxisLabels = false;
+                var showLegend = false;
+            }
+
+            Highcharts.chart(containerName, {
+                chart: {
+                    zoomType: 'x',
+                    type: 'area',
+                },
+
+                title: {
+                    text: null
+                },
+
+                yAxis: {
+                    title: {
+                        enabled: yAxisTitle,
+                        text: 'Percentage Pay Difference'
+                    },
+                    labels: {
+                        format: '{value}%'
+                    },
+                    // tickPositioner: function () {
+                    //     // var maxDeviation = Math.ceil(Math.max(Math.abs(this.dataMax), Math.abs(this.dataMin)));
+                    //     // var halfMaxDeviation = Math.ceil(maxDeviation / 2);
+
+                    //     // return [-maxDeviation, -halfMaxDeviation, 0, halfMaxDeviation, maxDeviation];
+                    //     return Array.from({length: -Math.floor(this.dataMin) + 2}, (x, i) => i + Math.floor(this.dataMin));
+                    // },
+                },
+
+                xAxis: {
+                    type: 'category',
+                    labels: {
+                        enabled: xAxisLabels
+                    },
+                    title: {
+                        text: "Year Groups",
+                        enabled: yAxisTitle,
+                    }
+                },
+
+                plotOptions: {
+                    series: {
+                        fillColor: {
+                            linearGradient: [0, 0, 0, 300],
+                            stops: [
+                                [1, "rgba(0, 255, 0, 0.3)"]
+                            ]
+                        },
+                        negativeFillColor: {
+                            linearGradient: [0, 0, 0, 300],
+                            stops: [
+                                [1, "rgba(255, 0, 0, 0.3)"]
+                            ]
+                        }
+                    }
+                },
+        
+                series: [{
+                    data: data,
+                    lineWidth: 4,
+                    showInLegend: showLegend,
+                    name: "Pay Gap",
+                    color: 'Green',
+                    threshold: 0,
+                    negativeColor: 'Red',
+                }]
+            })
+        })
+    })
+}
\ No newline at end of file
diff --git a/static/style.css b/static/style.css
index 4e89888..28d53c7 100644
--- a/static/style.css
+++ b/static/style.css
@@ -49,6 +49,31 @@ aside form input[type="submit"] {
   border: 2px solid black;
 }
 
+#filterform h5 {
+  margin-top: 10px;
+  margin-bottom: 5px;
+}
+
+label {
+  font-size: x-small;
+}
+
+.collapsable {
+  display: none;
+}
+
+.collapsetoggle {
+  color: black;
+  font-weight: bold;
+  padding-top: 1px;
+  text-decoration: none;
+}
+
+.collapsetoggle#hover {
+  cursor: pointer;
+  text-decoration: underline;
+}
+
 #main_content {
   padding-left: 2.5%;
   padding-right: 2.5;
@@ -62,23 +87,38 @@ aside form input[type="submit"] {
   list-style-type: none;
   width: 25%;
   display: inline-flex;
-  background-color: pink;
+  /* background-color: pink; */
   min-height: 250px;
   margin-bottom: 7px;
   overflow: hidden;
   flex-direction: column;
   justify-content: space-between;
+  /* border-color: black;
+  border-width: 2px;
+  border-radius: 1;
+  border-style: ridge; */
 }
 
 .chart_container {
   display: flex;
   flex-direction: row-reverse;
 }
+
 .minichart {
   min-height: 220px;
   width: 100%;
   margin: 0.5rem;
-  background-color: red;
+  /* background-color: red; */
+}
+
+#singlechart {
+  /* background-color: pink; */
+  width: 70%;
+  min-height: 70%;
+}
+
+.chart {
+  
 }
 
 .bottom_text {
diff --git a/templates/plot.html.j2 b/templates/plot.html.j2
new file mode 100644
index 0000000..91a3a36
--- /dev/null
+++ b/templates/plot.html.j2
@@ -0,0 +1,8 @@
+{% extends "template.html.j2" %}
+{% block content %}
+    <div id="singlechart">
+        <div class="chart" id="/chart{{ elem['url'] }}">
+        </div>
+        <p id=chart_alt>{{ alt }}</p>
+    </div>  
+{% endblock %}
\ No newline at end of file
diff --git a/templates/template.html.j2 b/templates/template.html.j2
index 4c08853..72cf520 100644
--- a/templates/template.html.j2
+++ b/templates/template.html.j2
@@ -34,7 +34,39 @@
             <input type="search" id="search_entry" name="search" required>
             <input type="submit" value="Search" id="search_btn">
         </form>
-        <h4>Filter...</h4>
+        {% if filters is defined %}
+            <h4>Filters</h4>
+            <form id="filterform">
+                {% for filter_name, filter_content in filters.items() %}
+                    <h5>{{ filter_name }}</h5>
+                    {% if len(filter_content["options"]) > 5 %}
+                        <button class="collapsetoggle" id="collapsetoggle/{{ filter_name }}" onclick="collapseTogglePress('collapsable/{{ filter_name }}')">
+                            {{ "Un-hide %d hidden filters" % len(filter_content["options"]) }}
+                        </button>
+                        <div class="collapsable" id="collapsable/{{ filter_name }}">
+                    {% endif %}
+                    {% for option in filter_content["options"] %}
+                        {% if filter_name in current_filters.keys() %}
+                            {% if current_filters[filter_name] == option %}
+                                <input type="radio" id="{{ option }}" name="{{ filter_name }}" value = "{{ option }}" checked="checked">
+                            {% else %}
+                                <input type="radio" id="{{ option }}" name="{{ filter_name }}" value = "{{ option }}">
+                            {% endif %}
+                        {% else %}
+                            <input type="radio" id="{{ option }}" name="{{ filter_name }}" value = "{{ option }}">
+                        {% endif %}
+                        <label for="{{ option }}">{{ option }}</label><br>
+                    {% endfor %}
+                    {% if len(filter_content["default"]) < 2 %}
+                        <input type="radio" id="No filter" name="{{ filter_name }}" value = "No filter">
+                        <label for="No filter">No filter</label><br>
+                    {% endif %}
+                    {% if len(filter_content["options"]) > 5 %}
+                        </div>
+                    {% endif %}
+                {% endfor %}
+            </form>
+        {% endif %}
     </aside>
 
     <div id="main_content">
-- 
cgit v1.2.3