diff options
| -rw-r--r-- | app/admin/forms.py | 6 | ||||
| -rw-r--r-- | app/admin/routes.py | 54 | ||||
| -rw-r--r-- | app/static/view_annotation.css | 60 | ||||
| -rw-r--r-- | app/templates/admin/annotations.html | 6 | ||||
| -rw-r--r-- | app/templates/admin/annotations_by_dataset.html | 159 |
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 %} |
