diff options
| author | Gertjan van den Burg <gertjanvandenburg@gmail.com> | 2019-03-26 13:35:17 +0000 |
|---|---|---|
| committer | Gertjan van den Burg <gertjanvandenburg@gmail.com> | 2019-03-26 13:35:17 +0000 |
| commit | 5e711f079121e0b9e958261df4aafcf568cb62cf (patch) | |
| tree | 99a1336b2406411d81a2a05199362f6f1e362e0a /app | |
| parent | first working version of pan + zoom + click (diff) | |
| download | AnnotateChange-5e711f079121e0b9e958261df4aafcf568cb62cf.tar.gz AnnotateChange-5e711f079121e0b9e958261df4aafcf568cb62cf.zip | |
First working version of complete annotation functionality
Diffstat (limited to 'app')
| -rw-r--r-- | app/main/routes.py | 68 | ||||
| -rw-r--r-- | app/models.py | 3 | ||||
| -rw-r--r-- | app/static/annotate.css | 3 | ||||
| -rw-r--r-- | app/templates/annotate/index.html | 139 | ||||
| -rw-r--r-- | app/templates/index.html | 2 |
5 files changed, 202 insertions, 13 deletions
diff --git a/app/main/routes.py b/app/main/routes.py index a1d1a63..f1f7a15 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -1,12 +1,29 @@ # -*- coding: utf-8 -*- -from flask import render_template, flash, url_for, redirect +import datetime + +from flask import render_template, flash, url_for, redirect, request from flask_login import current_user, login_required +from app import db from app.main import bp -from app.models import Task +from app.models import Annotation, Task from app.main.datasets import load_data_for_chart +RUBRIC = """ +<i>Please mark all the points in the time series where an abrupt change in + the behaviour of the series occurs.</i> +<br> +<br> +If there are no such points, please click the "no changepoints" button. +<br> +When you're ready, please click the submit button. +<br> +<b>Note:</b> You can zoom and pan the graph if needed. +<br> +Thank you! +""" + @bp.route("/") @bp.route("/index") @@ -28,10 +45,53 @@ def index(): @bp.route("/annotate/<int:task_id>", methods=("GET", "POST")) @login_required def task(task_id): + if request.method == "POST": + # record post time + now = datetime.datetime.utcnow() + + # get the json from the client + annotation = request.get_json() + if annotation["task"] != task_id: + flash("Internal error: task id doesn't match.") + return redirect(url_for(task_id=task_id)) + + task = Task.query.filter_by(id=task_id).first() + + # remove all previous annotations for this task + for ann in Annotation.query.filter_by(task_id=task_id).all(): + db.session.delete(ann) + task.done = False + task.annotated_on = None + db.session.commit() + + # record the annotation + if annotation["changepoints"] is None: + ann = Annotation(cp_index=None, task_id=task_id) + db.session.add(ann) + db.session.commit() + else: + for cp in annotation["changepoints"]: + ann = Annotation(cp_index=cp["x"], task_id=task_id) + db.session.add(ann) + db.session.commit() + + # mark the task as done + task.done = True + task.annotated_on = now + db.session.commit() + + flash("Your annotation has been recorded, thank you!") + return url_for("main.index") + task = Task.query.filter_by(id=task_id).first() if task is None: flash("No task with id %r has been assigned to you." % task_id) return redirect(url_for("main.index")) data = load_data_for_chart(task.dataset.name) - return render_template("annotate/index.html", title="Annotate %s" % - task.dataset.name, task=task, data=data) + return render_template( + "annotate/index.html", + title="Annotate %s" % task.dataset.name, + task=task, + data=data, + rubric=RUBRIC, + ) diff --git a/app/models.py b/app/models.py index 06dfbc2..03ba884 100644 --- a/app/models.py +++ b/app/models.py @@ -81,8 +81,7 @@ class Task(db.Model): class Annotation(db.Model): id = db.Column(db.Integer, primary_key=True) - time_start = db.Column(db.Integer) - time_end = db.Column(db.Integer) + cp_index = db.Column(db.Integer) task = db.relation("Task") task_id = db.Column(db.Integer, db.ForeignKey("task.id")) diff --git a/app/static/annotate.css b/app/static/annotate.css index 0ee40f6..7dc222f 100644 --- a/app/static/annotate.css +++ b/app/static/annotate.css @@ -19,3 +19,6 @@ rect { opacity: 0; } +#rubric { + text-align: center; +} diff --git a/app/templates/annotate/index.html b/app/templates/annotate/index.html index 1805d95..6a34f61 100644 --- a/app/templates/annotate/index.html +++ b/app/templates/annotate/index.html @@ -7,14 +7,65 @@ {% block app_content %} <h1>{{ task.dataset.name }}</h1> + +<div id="rubric" class="row"> + <div class="col-md-12"> + <p>{{ rubric | safe }}</p> + </div> +</div> + <div id="graph"></div> -<div id="button" class="row"> - <div class="col-md-3"> - <button>Submit</button> +<div id="wrap-buttons" class="row"> + <div class="col-md-6 text-left"> + <button class="btn btn-primary float-md-left" type="button" id="btn-reset">Reset</button> </div> + <div class="col-md-6 text-right"> + <button class="btn btn-warning float-md-right" type="button" id="btn-none">No Changepoints</button> + <button class="btn btn-success float-md-right" id="btn-submit" type="button">Submit</button> + </div> +</div> +<br> + +<!-- Modal --> +<div class="modal fade" id="submitNoCPModal" tabindex="-1" role="dialog" aria-labelledby="submitNoCPModalTitle" aria-hidden="true"> + <div class="modal-dialog modal-dialog-centered" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="submitNoCPModalLongTitle">No Changepoints Selected</h5> + <button type="button" class="close" data-dismiss="modal" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + </div> + <div class="modal-body"> + Please use the "No Changepoints" button when you think there are no changepoints in the time series. + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> + </div> + </div> + </div> </div> +<!-- Modal --> +<div class="modal fade" id="NoCPYesCPModal" tabindex="-1" role="dialog" aria-labelledby="NoCPYesCPModalTitle" aria-hidden="true"> + <div class="modal-dialog modal-dialog-centered" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="NoCPYesCPModalLongTitle">Changepoints Selected</h5> + <button type="button" class="close" data-dismiss="modal" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + </div> + <div class="modal-body"> + There are selected changepoints, please click the Reset button before clicking the "No Changepoints" button. + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> + </div> + </div> + </div> +</div> <h3>Selected Changepoints</h3> <div id="changepoint-table"> @@ -22,7 +73,6 @@ </table> </div> - {# Based on: https://github.com/benalexkeen/d3-flask-blog-post/blob/master/templates/index.html #} {# And: https://bl.ocks.org/mbostock/35964711079355050ff1 #} <script src="http://d3js.org/d3.v5.min.js"></script> @@ -44,7 +94,7 @@ var svg = d3.select("#graph") .attr("width", divWidth) .attr("height", divHeight); -var margin = {top: 20, right: 20, bottom: 100, left: 100}; +var margin = {top: 20, right: 20, bottom: 50, left: 50}; var width = +svg.attr("width") - margin.left - margin.right; var height = +svg.attr("height") - margin.top - margin.bottom; @@ -155,7 +205,8 @@ function clicked(d, i) { function nozoom() { d3.event.preventDefault(); } - +</script> +<script> function updateTable() { var changepoints = document.getElementsByClassName("changepoint"); @@ -168,6 +219,11 @@ function updateTable() { table.id = "cp-table"; table.className = "table table-striped"; + if (changepoints.length == 0) { + myTableDiv.appendChild(table); + return; + } + var heading = new Array(); heading[0] = "#"; heading[1] = "X"; @@ -213,6 +269,77 @@ function updateTable() { table.appendChild(body); myTableDiv.appendChild(table); } +</script> +<script> +// reset button +var reset = document.getElementById("btn-reset"); +reset.onclick = function() { + var changepoints = d3.selectAll(".changepoint"); + changepoints.each(function(d, i) { + var elem = d3.select(this); + elem.classed("changepoint", false); + elem.style("fill", "blue"); + }); + updateTable(); +} + +var nocp = document.getElementById("btn-none"); +nocp.onclick = function() { + var changepoints = document.getElementsByClassName("changepoint"); + // validation + if (changepoints.length > 0) { + $('#NoCPYesCPModal').modal(); + return; + } + + var obj = { + task: {{ task.id }}, + changepoints: null + }; + + var xhr = new XMLHttpRequest(); + xhr.open("POST", ""); + xhr.withCredentials = true; + xhr.setRequestHeader("Content-Type", "application/json"); + xhr.send(JSON.stringify(obj)); +}; + +var submit = document.getElementById("btn-submit"); +submit.onclick = function() { + var changepoints = document.getElementsByClassName("changepoint"); + // validation + if (changepoints.length === 0) { + $('#submitNoCPModal').modal(); + return; + } + + var obj = {}; + obj["task"] = {{ task.id }}; + obj["changepoints"] = []; + var i, cp; + for (i=0; i<changepoints.length; i++) { + cp = changepoints[i]; + elem = { + id: i, + x: cp.getAttribute("data_X"), + y: cp.getAttribute("data_Y") + }; + obj["changepoints"].push(elem); + } + var xhr = new XMLHttpRequest(); + xhr.open("POST", ""); + xhr.withCredentials = true; + xhr.setRequestHeader("Content-Type", "application/json"); + xhr.send(JSON.stringify(obj)); + xhr.onreadystatechange = function() { + if (xhr.readyState == XMLHttpRequest.DONE) { + if (xhr.status === 200) + window.location.href = xhr.responseText; + } else { + console.log("Error: " + xhr.status); + } + }; +}; </script> {% endblock %} diff --git a/app/templates/index.html b/app/templates/index.html index 6c6241e..6f868b1 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -17,7 +17,7 @@ </ol> </div> {% else %} - <span>No more tasks to do, thank you!</span> + <span>No more annotations to do, thanks for your help, you rock!</span> {% endif %} {% if tasks_done %} <h2>Completed Annotations</h2> |
