aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGertjan van den Burg <gertjanvandenburg@gmail.com>2019-03-25 15:49:18 +0000
committerGertjan van den Burg <gertjanvandenburg@gmail.com>2019-03-25 15:49:46 +0000
commit9c98e5bc50971e91def3b4490deff727c72caa23 (patch)
treedc38ef90e34a58d855d78ed981159189c2be0cd9
parentbugfixes (diff)
downloadAnnotateChange-9c98e5bc50971e91def3b4490deff727c72caa23.tar.gz
AnnotateChange-9c98e5bc50971e91def3b4490deff727c72caa23.zip
Start work on annotation view
-rw-r--r--app/main/datasets.py19
-rw-r--r--app/main/routes.py25
-rw-r--r--app/static/annotate.css40
-rw-r--r--app/templates/annotate/index.html337
4 files changed, 417 insertions, 4 deletions
diff --git a/app/main/datasets.py b/app/main/datasets.py
new file mode 100644
index 0000000..c06b4bf
--- /dev/null
+++ b/app/main/datasets.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+
+import os
+import json
+
+from flask import current_app
+
+
+def load_data_for_chart(name):
+ dataset_dir = os.path.join(
+ current_app.instance_path, current_app.config["DATASET_DIR"]
+ )
+ target_filename = os.path.join(dataset_dir, name + ".json")
+ with open(target_filename, 'rb') as fid:
+ data = json.load(fid)
+ chart_data = [{"value": x} for x in data['series']['V1']['raw']]
+ return {"chart_data": chart_data}
+
+
diff --git a/app/main/routes.py b/app/main/routes.py
index 662e14a..a1d1a63 100644
--- a/app/main/routes.py
+++ b/app/main/routes.py
@@ -1,10 +1,11 @@
# -*- coding: utf-8 -*-
-from flask import render_template
-from flask_login import current_user
+from flask import render_template, flash, url_for, redirect
+from flask_login import current_user, login_required
from app.main import bp
from app.models import Task
+from app.main.datasets import load_data_for_chart
@bp.route("/")
@@ -15,6 +16,22 @@ def index():
tasks = Task.query.filter_by(annotator_id=user_id).all()
tasks_done = [t for t in tasks if t.done]
tasks_todo = [t for t in tasks if not t.done]
- return render_template("index.html", title="Home",
- tasks_done=tasks_done, tasks_todo=tasks_todo)
+ return render_template(
+ "index.html",
+ title="Home",
+ tasks_done=tasks_done,
+ tasks_todo=tasks_todo,
+ )
return render_template("index.html", title="Home")
+
+
+@bp.route("/annotate/<int:task_id>", methods=("GET", "POST"))
+@login_required
+def task(task_id):
+ 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)
diff --git a/app/static/annotate.css b/app/static/annotate.css
new file mode 100644
index 0000000..d6e30fc
--- /dev/null
+++ b/app/static/annotate.css
@@ -0,0 +1,40 @@
+#graph {
+ margin: 0 auto;
+ text-align: center;
+}
+
+/*
+path {
+ stroke-width: 2;
+ fill: none;
+}
+
+.axis path, .axis line {
+ fill: none;
+ stroke: grey;
+ stroke-width: 1;
+ shape-rendering: crispEdges;
+}
+
+#changepoint-table {
+ margin: 0 auto;
+}
+
+*/
+
+.line {
+ fill: none;
+ stroke: blue;
+ clip-path: url(#clip);
+}
+
+circle {
+ clip-path: url(#clip);
+ fill: blue;
+}
+
+.zoom {
+ cursor: move;
+ fill: none;
+ pointer-events: fill;
+}
diff --git a/app/templates/annotate/index.html b/app/templates/annotate/index.html
new file mode 100644
index 0000000..b605dc3
--- /dev/null
+++ b/app/templates/annotate/index.html
@@ -0,0 +1,337 @@
+{% extends "base.html" %}
+
+{% block styles %}
+{{super()}}
+<link rel="stylesheet" href="{{url_for('static', filename='annotate.css')}}">
+{% endblock %}
+
+{% block app_content %}
+<h1>{{ task.dataset.name }}</h1>
+<div id="graph"></div>
+
+<div id="button" class="row">
+ <div class="col-md-3">
+ <button>Submit</button>
+ </div>
+</div>
+
+
+<h3>Selected Changepoints</h3>
+<div id="changepoint-table">
+ <table id="cp-table" class="table table-striped">
+ </table>
+</div>
+
+
+{# Based on: https://github.com/benalexkeen/d3-flask-blog-post/blob/master/templates/index.html #}
+<script src="http://d3js.org/d3.v5.min.js"></script>
+<script>
+ var graphData = {{ data.chart_data | safe }};
+
+/**** Starting the alternative in this block:
+ https://bl.ocks.org/mbostock/34f08d5e11952a80609169b7917d4172
+ **/
+var svg = d3.select("#graph").append("svg").attr("width", 960).attr("height", 500),
+ margin = {top: 20, right: 20, bottom: 110, left: 40},
+ margin2 = {top: 430, right: 20, bottom: 30, left: 40},
+ width = +svg.attr("width") - margin.left - margin.right,
+ height = +svg.attr("height") - margin.top - margin.bottom,
+ height2 = +svg.attr("height") - margin2.top - margin2.bottom;
+
+var x = d3.scaleLinear().range([0, width]),
+ y = d3.scaleLinear().range([height, 0]),
+ x2 = d3.scaleLinear().range([0, width]);
+
+var xAxis = d3.axisBottom(x),
+ yAxis = d3.axisLeft(y);
+
+var zoom = d3.zoom()
+ .scaleExtent([1, 10])
+ .translateExtent([[0, 0], [width, height]])
+ .extent([[0, 0], [width, height]])
+ .on("zoom", zoomed);
+
+var line = d3.line()
+ .x(function(d) { return x(d.X); })
+ .y(function(d) { return y(d.Y); });
+
+// used for clipping the path outside the graph
+svg.append("defs").append("clipPath")
+ .attr("id", "clip")
+ .append("rect")
+ .attr("width", width)
+ .attr("height", height);
+
+var focus = svg.append("g")
+ .attr("class", "focus")
+ .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
+
+function zoomed() {
+ /*
+ if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush")
+ return; // ignore zoom-by-brush
+ */
+ var t = d3.event.transform;
+ x.domain(t.rescaleX(x2).domain());
+ // redraw
+ focus.select(".line").attr("d", line);
+ points.data(graphData)
+ .attr("cx", function(d) { return x(d.X) })
+ .attr("cy", function(d) { return y(d.Y) });
+ // update axis
+ focus.select(".axis--x").call(xAxis);
+}
+
+function draw(data) {
+ var n = 0;
+ data.forEach(function(d) {
+ d.X = n++;
+ d.Y = d.value;
+ });
+
+ var yExtent = d3.extent(data, function(d) { return d.Y; }),
+ yRange = yExtent[1] - yExtent[0];
+
+ x.domain(d3.extent(data, function(d) { return d.X; }));
+ y.domain([yExtent[0] - (yRange * 0.05), yExtent[1] + (yRange *
+ 0.05)]);
+ x2.domain(x.domain());
+
+ focus.append("path")
+ .datum(data)
+ .attr("class", "line")
+ .attr("d", line);
+
+ focus.append("g")
+ .attr("class", "axis axis--x")
+ .attr("transform", "translate(0," + height + ")")
+ .call(xAxis);
+
+ focus.append("g")
+ .attr("class", "axis axis--y")
+ .call(yAxis);
+
+ var points = focus.append("g")
+ .selectAll("dot")
+ .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", 4)
+ .attr("fill", "blue")
+ .on("click", function(d, i) {
+ // this function handles changepoint marking
+ var elem = d3.select(d);
+ if (elem.classed("changepoint")) {
+ elem.style("fill", "blue");
+ elem.classed("changepoint", false);
+ } else {
+ elem.style("fill", "red");
+ elem.classed("changepoint", true);
+ }
+ // updateTable();
+ });
+
+ var layer = svg.append("rect")
+ .attr("class", "zoom")
+ .attr("width", width)
+ .attr("height", height)
+ .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
+ .call(zoom);
+
+ return(points);
+};
+
+var points = draw(graphData);
+
+
+
+
+
+
+
+/***** END ALTERNATIVE **/
+
+
+/*
+ // Set the dimension of the svg
+ var margin = {top: 30, right: 50, bottom: 30, left: 50};
+ var svgWidth = 800;
+ var svgHeight = 400;
+ var graphWidth = svgWidth - margin.left - margin.right;
+ var graphHeight = svgHeight - margin.top - margin.bottom;
+
+ var xAxisScale = d3.scaleLinear().range([0, graphWidth]);
+ var yAxisScale = d3.scaleLinear().range([graphHeight, 0]);
+
+ var xAxis = d3.axisBottom(xAxisScale).ticks(5);
+ var yAxis = d3.axisLeft(yAxisScale).ticks(5);
+
+ var line = d3.line()
+ .x(function(d) { return xAxisScale(d.X); })
+ .y(function(d) { return yAxisScale(d.Y); });
+
+ var svg = d3.select("#graph")
+ .append("svg")
+ .attr("width", svgWidth)
+ .attr("height", svgHeight)
+
+ var zoom = d3.zoom()
+ .scaleExtent([1, 5])
+ .on("zoom", zoomFunction);
+
+ var innerSpace = svg
+ .append("g")
+ .attr("class", "inner_space")
+ .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
+
+// Add the X Axis
+ var gX = innerSpace.append("g")
+ .attr("class", "axis axis--x")
+ .attr("transform", "translate(0," + graphHeight + ")")
+ .call(xAxis);
+
+// Add the Y Axis
+ var gY = innerSpace.append("g")
+ .attr("class", "axis axis--y")
+ .call(yAxis);
+
+ var view = innerSpace.append("rect")
+ .attr("class", "zoom")
+ .attr("width", graphWidth)
+ .attr("height", graphHeight)
+ .call(zoom)
+
+ function zoomFunction() {
+
+ var e = d3.event,
+ tx = Math.min(0, Math.max(e.transform.x, graphWidth -
+ graphWidth * e.scale)),
+ ty = Math.min(0, Math.max(e.transform.y, graphHeight -
+ graphHeight * e.scale));
+ zoom.translateBy([tx, ty]);
+
+ theline.attr("transform", [
+ "translate(" + [tx, ty] + ")",
+ "scale(" + e.scale + ")"
+ ].join(" "));
+
+ }
+
+ function updateTable() {
+ var changepoints = document.getElementsByClassName("changepoint");
+
+ var myTableDiv = document.getElementById("changepoint-table");
+
+ var old_table = document.getElementById("cp-table");
+ old_table.remove();
+
+ var table = document.createElement('TABLE')
+ table.id = "cp-table";
+ table.className = "table table-striped";
+
+ var heading = new Array();
+ heading[0] = "#";
+ heading[1] = "X";
+ heading[2] = "Y";
+
+ // TABLE COLUMNS
+ var thead = document.createElement('THEAD');
+ thead.className = "thead-dark";
+ table.appendChild(thead);
+ for (i = 0; i < heading.length; i++) {
+ var th = document.createElement('TH')
+ th.appendChild(document.createTextNode(heading[i]));
+ th.setAttribute("scope", "col");
+ thead.appendChild(th);
+ }
+ var body = document.createElement("TBODY");
+
+//TABLE ROWS
+ for (i = 0; i < changepoints.length; i++) {
+ cp = changepoints[i];
+
+ var tr = document.createElement('TR');
+
+ var th = document.createElement('TH');
+ th.setAttribute("scope", "row");
+ th.appendChild(document.createTextNode(i+1));
+ tr.appendChild(th);
+
+ var td = document.createElement('TD');
+ td.appendChild(document.createTextNode(
+ d3.select(cp).data()[0].X
+ ));
+ tr.appendChild(td);
+
+ var td = document.createElement('TD');
+ td.appendChild(document.createTextNode(
+ d3.select(cp).data()[0].Y
+ ));
+ tr.appendChild(td);
+
+ body.appendChild(tr);
+ }
+ table.appendChild(body);
+ myTableDiv.appendChild(table);
+ }
+
+ function draw(data) {
+ var n = 0;
+ data.forEach(function(d) {
+ d.X = n++;
+ d.Y = d.value;
+ });
+
+ xAxisScale.domain(d3.extent(data, function(d) { return d.X; }));
+ yAxisScale.domain([
+ d3.min(data, function(d) {
+ return Math.min(d.Y)
+ }),
+ d3.max(data, function(d) {
+ return Math.max(d.Y)
+ })]);
+
+ // the line
+ var theline = innerSpace.append("path")
+ .style("stroke", "blue")
+ .style("fill", "none")
+ .attr("class", "line")
+ .attr("d", line(data));
+
+// the points
+ var thepoints = innerSpace.append("g")
+ .selectAll("dot")
+ .data(data)
+ .enter()
+ .append("circle")
+ .attr("cx", function(d) { return xAxisScale(d.X) } )
+ .attr("cy", function(d) { return yAxisScale(d.Y) } )
+ .attr("data_X", function(d) { return d.X } )
+ .attr("data_Y", function(d) { return d.Y } )
+ .attr("r", 4)
+ .attr("fill", "blue")
+ .on("click", function(d, i) {
+ // this function handles changepoint marking
+ var elem = d3.select(this);
+ if (elem.classed("changepoint")) {
+ elem.style("fill", "blue");
+ elem.classed("changepoint", false);
+ } else {
+ elem.style("fill", "red");
+ elem.classed("changepoint", true);
+ }
+ updateTable();
+ });
+ return [theline, thepoints];
+ };
+
+ var out = draw(graphData);
+ var theline = out[0]
+ var thepoints = out[1];
+ */
+</script>
+{% endblock %}