diff options
-rw-r--r-- | app.py | 50 | ||||
-rw-r--r-- | charts.json | 32 | ||||
-rw-r--r-- | database.py | 37 | ||||
-rw-r--r-- | parser.py | 2 | ||||
-rw-r--r-- | static/scripts.js | 145 | ||||
-rw-r--r-- | static/style.css | 44 | ||||
-rw-r--r-- | templates/plot.html.j2 | 8 | ||||
-rw-r--r-- | templates/template.html.j2 | 34 |
8 files changed, 338 insertions, 14 deletions
@@ -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()] + @@ -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"> |