aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/admin/forms.py6
-rw-r--r--app/admin/routes.py54
-rw-r--r--app/static/view_annotation.css60
-rw-r--r--app/templates/admin/annotations.html6
-rw-r--r--app/templates/admin/annotations_by_dataset.html159
5 files changed, 284 insertions, 1 deletions
diff --git a/app/admin/forms.py b/app/admin/forms.py
index 39ee1ab..1f13626 100644
--- a/app/admin/forms.py
+++ b/app/admin/forms.py
@@ -68,6 +68,12 @@ class AdminManageDatasetsForm(FlaskForm):
dataset = SelectField("Dataset", coerce=int, validators=[InputRequired()])
delete = SubmitField("Delete")
+
class AdminManageUsersForm(FlaskForm):
user = SelectField("User", coerce=int, validators=[InputRequired()])
delete = SubmitField("Delete")
+
+
+class AdminSelectDatasetForm(FlaskForm):
+ dataset = SelectField("Dataset", coerce=int, validators=[InputRequired()])
+ submit = SubmitField("Show")
diff --git a/app/admin/routes.py b/app/admin/routes.py
index 58d23aa..bed31e2 100644
--- a/app/admin/routes.py
+++ b/app/admin/routes.py
@@ -16,9 +16,11 @@ from app.admin.forms import (
AdminAddDatasetForm,
AdminManageDatasetsForm,
AdminManageUsersForm,
+ AdminSelectDatasetForm,
)
from app.models import User, Dataset, Task, Annotation
from app.utils.tasks import generate_auto_assign_tasks
+from app.main.datasets import load_data_for_chart
@bp.route("/manage/tasks", methods=("GET", "POST"))
@@ -229,9 +231,22 @@ def add_dataset():
return render_template("admin/add.html", title="Add Dataset", form=form)
-@bp.route("/annotations", methods=("GET",))
+@bp.route("/annotations", methods=("GET", "POST"))
@admin_required
def view_annotations():
+ dataset_list = [(d.id, d.name) for d in Dataset.query.all()]
+ form = AdminSelectDatasetForm()
+ form.dataset.choices = dataset_list
+
+ if form.validate_on_submit():
+ dataset = Dataset.query.filter_by(id=form.dataset.data).first()
+ if dataset is None:
+ flash("Dataset does not exist.", "error")
+ return redirect(url_for("admin.view_annotations"))
+ return redirect(
+ url_for("admin.view_annotations_by_dataset", dset_id=dataset.id)
+ )
+
annotations = (
Annotation.query.join(Task, Annotation.task)
.join(User, Task.user)
@@ -243,6 +258,43 @@ def view_annotations():
"admin/annotations.html",
title="View Annotations",
annotations=annotations,
+ form=form,
+ )
+
+
+@bp.route("/annotations_by_dataset/<int:dset_id>", methods=("GET",))
+@admin_required
+def view_annotations_by_dataset(dset_id):
+ dataset = Dataset.query.filter_by(id=dset_id).first()
+ annotations = (
+ Annotation.query.join(Task, Annotation.task)
+ .join(User, Task.user)
+ .join(Dataset, Task.dataset)
+ .order_by(Dataset.name, User.username, Annotation.cp_index)
+ .all()
+ )
+
+ annotations = [a for a in annotations if a.task.dataset.id == dset_id]
+
+ anno_clean = []
+ user_counter = {}
+ counter = 1
+ for ann in annotations:
+ if ann.task.user.id in user_counter:
+ uid = user_counter[ann.task.user.id]
+ else:
+ uid = user_counter.setdefault(
+ ann.task.user.id, "user-%i" % counter
+ )
+ counter += 1
+ anno_clean.append(dict(user=uid, index=ann.cp_index))
+
+ data = load_data_for_chart(dataset.name)
+ data["annotations"] = anno_clean
+ return render_template(
+ "admin/annotations_by_dataset.html",
+ title="View Annotations for dataset",
+ data=data,
)
diff --git a/app/static/view_annotation.css b/app/static/view_annotation.css
new file mode 100644
index 0000000..dd291ef
--- /dev/null
+++ b/app/static/view_annotation.css
@@ -0,0 +1,60 @@
+#graph {
+ margin: 0 auto;
+ text-align: center;
+}
+
+.line {
+ fill: none;
+ stroke: blue;
+ clip-path: url(#clip);
+}
+
+.ann-line {
+ fill: none;
+ stroke-dasharray: 3,3;
+ clip-path: url(#clip);
+}
+
+circle {
+ clip-path: url(#clip);
+ fill: blue;
+}
+
+rect {
+ fill: white;
+ opacity: 0;
+}
+
+.user-1 {
+ fill: #66c2a5;
+ stroke: #66c2a5;
+}
+
+.user-2 {
+ fill: #fc8d62;
+}
+
+.user-3 {
+ fill: #8da0cb;
+}
+
+.user-4 {
+ fill: #e78ac3;
+}
+
+.user-5 {
+ fill: #a6d854;
+}
+
+.user-6 {
+ fill: #ffd92f;
+}
+
+.user-7 {
+ fill: #e5c494;
+}
+
+.user-8 {
+ fill: #b3b3b3;
+}
+
diff --git a/app/templates/admin/annotations.html b/app/templates/admin/annotations.html
index 4807b8e..089de3b 100644
--- a/app/templates/admin/annotations.html
+++ b/app/templates/admin/annotations.html
@@ -1,7 +1,13 @@
{% extends "base.html" %}
+{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<h1>View Annotations</h1>
+<div class="row">
+ <div class="col-md-4">
+ {{ wtf.quick_form(form) }}
+ </div>
+</div>
<article class="overview">
<table class="table table-striped">
<thead class="thead-dark">
diff --git a/app/templates/admin/annotations_by_dataset.html b/app/templates/admin/annotations_by_dataset.html
new file mode 100644
index 0000000..5ad98b6
--- /dev/null
+++ b/app/templates/admin/annotations_by_dataset.html
@@ -0,0 +1,159 @@
+{% extends "base.html" %}
+
+{% block styles %}
+{{super()}}
+<link rel="stylesheet" href="{{url_for('static', filename='view_annotation.css')}}">{% endblock %}
+
+{% block app_content %}
+<h1>View Annotations</h1>
+
+<div id="graph"></div>
+
+<script src="//d3js.org/d3.v5.min.js"></script>
+<script>
+var data = {{ data.chart_data | safe }};
+var n = 0;
+data.forEach(function(d) {
+ d.X = n++;
+ d.Y = d.value;
+});
+
+
+var divWidth = 1000;
+var divHeight = 480;
+
+var svg = d3.select("#graph")
+ .on("touchstart", nozoom)
+ .on("touchmove", nozoom)
+ .append("svg")
+ .attr("width", divWidth)
+ .attr("height", divHeight);
+
+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;
+
+var zoom = d3.zoom()
+ .scaleExtent([1, 50])
+ .translateExtent([[0, 0], [width, height]])
+ .extent([[0, 0], [width, height]])
+ .on("zoom", zoomed);
+
+var x = d3.scaleLinear().range([0, width]);
+var x2 = d3.scaleLinear().range([0, width]);
+var y = d3.scaleLinear().range([height, 0]);
+
+var xAxis = d3.axisBottom(x);
+var yAxis = d3.axisLeft(y);
+
+var xExtent = d3.extent(data, function(d) { return d.X; });
+var xRange = xExtent[1] - xExtent[0];
+var xDomainMin = xExtent[0] - xRange * 0.02;
+var xDomainMax = xExtent[1] + xRange * 0.02;
+
+var yExtent = d3.extent(data, function(d) { return d.Y; });
+var yRange = yExtent[1] - yExtent[0];
+var yDomainMin = yExtent[0] - yRange * 0.05;
+var yDomainMax = yExtent[1] + yRange * 0.05;
+
+x.domain([xDomainMin, xDomainMax]);
+y.domain([yDomainMin, yDomainMax]);
+x2.domain(x.domain());
+
+svg.append("defs").append("clipPath")
+ .attr("id", "clip")
+ .append("rect")
+ .attr("width", width - 30)
+ .attr("height", height)
+ .attr("transform", "translate(" + 30 + ",0)");
+
+svg.append("g")
+ .attr("class", "axis axis--y")
+ .attr("transform", "translate(" + 30 + ",0)")
+ .call(yAxis);
+
+svg.append("g")
+ .attr("class", "axis axis--x")
+ .attr("transform", "translate(0," + height + ")")
+ .call(xAxis);
+
+svg.append("text")
+ .attr("text-anchor", "middle")
+ .attr("class", "axis-label")
+ .attr("transform", "translate(" + (width - 20) + "," + (height + 50) + ")")
+ .text("Time");
+
+var line = d3.line()
+ .x(function(d) { return x(d.X); })
+ .y(function(d) { return y(d.Y); });
+
+var g = svg.append("g")
+ .call(zoom);
+
+g.append("rect")
+ .attr("width", width)
+ .attr("height", height);
+
+var view = g.append("g")
+ .attr("class", "view");
+
+view.append("path")
+ .datum(data)
+ .attr("class", "line")
+ .attr("d", line);
+
+var points = view.selectAll("circle")
+ .data(data)
+ .enter().append("circle")
+ .attr("cx", function(d) { return x(d.X); })
+ .attr("cy", function(d) { return y(d.Y); })
+ .attr("data_X", function(d) { return d.X; })
+ .attr("data_Y", function(d) { return d.Y; })
+ .attr("r", 5)
+ .on("click", clicked);
+
+function zoomed() {
+ t = d3.event.transform;
+ x.domain(t.rescaleX(x2).domain());
+ svg.select(".line").attr("d", line);
+ points.data(data)
+ .attr("cx", function(d) { return x(d.X); })
+ .attr("cy", function(d) { return y(d.Y); });
+ annolines = view.selectAll("line");
+ annolines._groups[0].forEach(function(l) {
+ l.setAttribute("x1", x(l.getAttribute("cp_idx")));
+ l.setAttribute("x2", x(l.getAttribute("cp_idx")));
+ });
+ svg.select(".axis--x").call(xAxis);
+}
+
+function clicked(d, i) {
+ if (d3.event.defaultPrevented) return; // zoomed
+}
+
+function nozoom() {
+ d3.event.preventDefault();
+}
+
+var annotations = {{ data.annotations | safe }};
+
+annotations.forEach(function(a) {
+ for (i=0; i<points._groups[0].length; i++) {
+ p = points._groups[0][i];
+ if (p.getAttribute("data_X") == a.index) {
+ var elem = d3.select(p);
+ elem.classed(a.user, 'true');
+ view.append("line")
+ .attr("cp_idx", a.index)
+ .attr("y1", y(yDomainMax))
+ .attr("y2", y(yDomainMin))
+ .attr("x1", x(a.index))
+ .attr("x2", x(a.index))
+ .attr("class", "ann-line" + " " + a.user);
+ }
+ }
+});
+
+
+</script>
+{% endblock %}