aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGertjan van den Burg <gertjanvandenburg@gmail.com>2019-03-26 13:35:17 +0000
committerGertjan van den Burg <gertjanvandenburg@gmail.com>2019-03-26 13:35:17 +0000
commit5e711f079121e0b9e958261df4aafcf568cb62cf (patch)
tree99a1336b2406411d81a2a05199362f6f1e362e0a
parentfirst working version of pan + zoom + click (diff)
downloadAnnotateChange-5e711f079121e0b9e958261df4aafcf568cb62cf.tar.gz
AnnotateChange-5e711f079121e0b9e958261df4aafcf568cb62cf.zip
First working version of complete annotation functionality
-rw-r--r--app/main/routes.py68
-rw-r--r--app/models.py3
-rw-r--r--app/static/annotate.css3
-rw-r--r--app/templates/annotate/index.html139
-rw-r--r--app/templates/index.html2
-rw-r--r--migrations/versions/878e8193ae4d_change_annotation_structure.py31
6 files changed, 233 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">&times;</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">&times;</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>
diff --git a/migrations/versions/878e8193ae4d_change_annotation_structure.py b/migrations/versions/878e8193ae4d_change_annotation_structure.py
new file mode 100644
index 0000000..1b37d5e
--- /dev/null
+++ b/migrations/versions/878e8193ae4d_change_annotation_structure.py
@@ -0,0 +1,31 @@
+"""change annotation structure
+
+Revision ID: 878e8193ae4d
+Revises: 37a7d932fc02
+Create Date: 2019-03-26 12:53:27.069030
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '878e8193ae4d'
+down_revision = '37a7d932fc02'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table("annotation") as batch_op:
+ batch_op.drop_column('time_start')
+ batch_op.drop_column('time_end')
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.add_column('annotation', sa.Column('time_end', sa.INTEGER(), nullable=True))
+ op.add_column('annotation', sa.Column('time_start', sa.INTEGER(), nullable=True))
+ # ### end Alembic commands ###