aboutsummaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/admin/forms.py2
-rw-r--r--app/admin/routes.py15
-rw-r--r--app/auth/routes.py12
-rw-r--r--app/decorators.py2
-rw-r--r--app/main/__init__.py2
-rw-r--r--app/main/demo.py439
-rw-r--r--app/main/forms.py8
-rw-r--r--app/main/routes.py31
-rw-r--r--app/models.py8
-rw-r--r--app/static/annotate.css6
-rw-r--r--app/static/css/demo/evaluate.css57
-rw-r--r--app/static/css/demo/learn.css11
-rw-r--r--app/static/css/global.css3
-rw-r--r--app/static/js/buttons.js39
-rw-r--r--app/static/js/makeChart.js146
-rw-r--r--app/static/view_annotation.css3
-rw-r--r--app/templates/_partials/modals.html21
-rw-r--r--app/templates/admin/annotations_by_dataset.html2
-rw-r--r--app/templates/admin/manage_datasets.html20
-rw-r--r--app/templates/admin/manage_users.html116
-rw-r--r--app/templates/annotate/index.html64
-rw-r--r--app/templates/base.html35
-rw-r--r--app/templates/demo/evaluate.html49
-rw-r--r--app/templates/demo/learn.html23
-rw-r--r--app/templates/index.html153
-rw-r--r--app/utils/datasets.py33
26 files changed, 1044 insertions, 256 deletions
diff --git a/app/admin/forms.py b/app/admin/forms.py
index 1ac9333..bd1dce6 100644
--- a/app/admin/forms.py
+++ b/app/admin/forms.py
@@ -22,7 +22,7 @@ class AdminAutoAssignForm(FlaskForm):
num_per_dataset = IntegerField(
"Tasks per Dataset", [NumberRange(min=1, max=20)], default=10
)
- assign = SubmitField("Assign")
+ submit = SubmitField("Submit")
class AdminManageTaskForm(FlaskForm):
diff --git a/app/admin/routes.py b/app/admin/routes.py
index efb4f57..f0d7817 100644
--- a/app/admin/routes.py
+++ b/app/admin/routes.py
@@ -41,7 +41,7 @@ def manage_tasks():
form_manual.username.choices = user_list
form_manual.dataset.choices = dataset_list
- if form_auto.validate_on_submit() and form_auto.assign.data:
+ if form_auto.validate_on_submit() and form_auto.submit.data:
max_per_user = form_auto.max_per_user.data
num_per_dataset = form_auto.num_per_dataset.data
@@ -130,6 +130,8 @@ def manage_users():
flash("User doesn't exist.", "error")
return redirect(url_for("admin.manage_users"))
+ username = user.username
+
tasks = Task.query.filter_by(annotator_id=user.id).all()
for task in tasks:
for ann in Annotation.query.filter_by(task_id=task.id).all():
@@ -137,7 +139,7 @@ def manage_users():
db.session.delete(task)
db.session.delete(user)
db.session.commit()
- flash("User deleted successfully.", "success")
+ flash("User '%s' deleted successfully." % username, "success")
return redirect(url_for("admin.manage_users"))
return render_template(
"admin/manage_users.html", title="Manage Users", users=users, form=form
@@ -175,6 +177,7 @@ def manage_datasets():
db.session.commit()
os.unlink(filename)
flash("Dataset deleted successfully.", "success")
+ return redirect(url_for("admin.manage_datasets"))
overview = []
for dataset in Dataset.query.all():
@@ -187,11 +190,13 @@ def manage_datasets():
entry = {
"id": dataset.id,
"name": dataset.name,
+ "demo": dataset.is_demo,
"assigned": len(tasks),
"completed": n_complete,
"percentage": perc,
}
overview.append(entry)
+ overview.sort(key=lambda x: x["name"])
return render_template(
"admin/manage_datasets.html",
title="Manage Datasets",
@@ -226,8 +231,10 @@ def add_dataset():
if not os.path.exists(target_filename):
flash("Internal error: file moving failed", "error")
return redirect(url_for("admin.add_dataset"))
-
- dataset = Dataset(name=name, md5sum=md5sum(target_filename))
+ is_demo = dataset_is_demo(target_filename)
+ dataset = Dataset(
+ name=name, md5sum=md5sum(target_filename), is_demo=is_demo
+ )
db.session.add(dataset)
db.session.commit()
flash("Dataset %r added successfully." % name, "success")
diff --git a/app/auth/routes.py b/app/auth/routes.py
index d23bc18..27b0de0 100644
--- a/app/auth/routes.py
+++ b/app/auth/routes.py
@@ -21,15 +21,10 @@ from app.auth.email import (
send_email_confirmation_email,
)
from app.models import User
-from app.utils.tasks import create_initial_user_tasks
@bp.route("/login", methods=("GET", "POST"))
def login():
- if current_user.is_authenticated:
- current_user.last_active = datetime.datetime.utcnow()
- db.session.commit()
- return redirect(url_for("main.index"))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
@@ -37,6 +32,8 @@ def login():
flash("Invalid username or password", "error")
return redirect(url_for("auth.login"))
login_user(user, remember=form.remember_me.data)
+ current_user.last_active = datetime.datetime.utcnow()
+ db.session.commit()
if not user.is_confirmed:
return redirect(url_for("auth.not_confirmed"))
next_page = request.args.get("next")
@@ -122,11 +119,6 @@ def confirm_email(token):
else:
user.is_confirmed = True
db.session.commit()
- for task in create_initial_user_tasks(user):
- if task is None:
- break
- db.session.add(task)
- db.session.commit()
flash("Account confirmed successfully. Thank you!", "success")
return redirect(url_for("auth.login"))
return redirect(url_for("main.index"))
diff --git a/app/decorators.py b/app/decorators.py
index df797bd..9fbf1f4 100644
--- a/app/decorators.py
+++ b/app/decorators.py
@@ -16,6 +16,8 @@ def admin_required(func):
return func(*args, **kwargs)
elif not current_user.is_authenticated:
return current_app.login_manager.unauthorized()
+ elif not current_user.is_confirmed:
+ return redirect(url_for("auth.not_confirmed"))
elif not current_user.is_admin:
return current_app.login_manager.unauthorized()
return func(*args, **kwargs)
diff --git a/app/main/__init__.py b/app/main/__init__.py
index 2cb605e..2612509 100644
--- a/app/main/__init__.py
+++ b/app/main/__init__.py
@@ -4,6 +4,6 @@ from flask import Blueprint
bp = Blueprint('main', __name__)
-from app.main import routes
+from app.main import routes, demo
diff --git a/app/main/demo.py b/app/main/demo.py
new file mode 100644
index 0000000..a126fd8
--- /dev/null
+++ b/app/main/demo.py
@@ -0,0 +1,439 @@
+# -*- coding: utf-8 -*-
+
+import datetime
+import logging
+import markdown
+import textwrap
+
+from flask import (
+ render_template,
+ flash,
+ url_for,
+ redirect,
+ request,
+ session,
+ abort,
+)
+from flask_login import current_user
+
+from app import db
+from app.decorators import login_required
+from app.models import Annotation, Dataset, Task
+from app.main import bp
+from app.main.forms import NextForm
+from app.main.routes import RUBRIC
+from app.utils.datasets import load_data_for_chart, get_demo_true_cps
+
+LOGGER = logging.getLogger(__name__)
+
+# textwrap.dedent is used mostly for code formatting.
+DEMO_DATA = {
+ 1: {
+ "dataset": {"name": "demo_100"},
+ "learn": {
+ "text": markdown.markdown(
+ textwrap.dedent(
+ """
+ Welcome to AnnotateChange, an annotation app for change point
+ detection.
+
+ Our goal with AnnotateChange is to create a dataset of
+ human-annotated time series to use in the development and
+ evaluation of change point algorithms.
+
+ We really appreciate that you've agreed to help us with this!
+ Without your help this project would not be possible.
+
+ In the next few pages, we'll introduce you to the problem of
+ change point detection. We'll look at a few datasets and see
+ different types of changes that can occur.
+
+ Thanks again for your help!"""
+ )
+ )
+ },
+ "annotate": {
+ "text": RUBRIC
+ + markdown.markdown(
+ textwrap.dedent(
+ """
+ Click "Submit" when you have finished marking the change points
+ or "No change points" when you believe there are none. You can
+ reset the graph with the "Reset" button."""
+ )
+ )
+ },
+ "evaluate": {
+ "text": markdown.markdown(
+ textwrap.dedent(
+ """
+ This first example has **one** change point. Not all datasets
+ that you'll encounter in this program have exactly one change
+ point. It is up to you to see whether a time series contains a
+ change point or not, and if it does, to see if there is more
+ than one.
+
+ Don't worry if you weren't exactly correct on the first try.
+ The goal of this introduction is to familiarise yourself with
+ time series data and with change point detection in
+ particular."""
+ )
+ )
+ },
+ },
+ 2: {
+ "dataset": {"name": "demo_200"},
+ "learn": {
+ "text": markdown.markdown(
+ textwrap.dedent(
+ """
+ In the previous example, you've seen a relatively simple
+ dataset where a *step change* occurred at a certain point in
+ time. A step change is one of the simplest types of change
+ points that can occur.
+
+ Click "Continue" to move on to the next example."""
+ )
+ )
+ },
+ "annotate": {"text": RUBRIC},
+ "evaluate": {
+ "text": markdown.markdown(
+ textwrap.dedent(
+ """
+ The dataset in the previous example shows again a time series
+ with step changes, but here there are **two** change points.
+ This is important to keep in mind, as there can be more than
+ one change point in a dataset."""
+ )
+ )
+ },
+ },
+ 3: {
+ "dataset": {"name": "demo_300"},
+ "learn": {
+ "text": markdown.markdown(
+ textwrap.dedent(
+ """
+ In the previous examples we've introduced *step changes*.
+ However, these are not the only types of change points that
+ can occur, as we'll see in the next example."""
+ )
+ )
+ },
+ "annotate": {"text": RUBRIC},
+ "evaluate": {
+ "text": markdown.markdown(
+ textwrap.dedent(
+ """
+ This time series shows an example where a change occurs in the
+ **variance** of the data. At the change point the variance of
+ the noise changes abruptly from a relatively low noise variance
+ to a high noise variance. This is another type of change point
+ that can occur."""
+ )
+ )
+ },
+ },
+ 4: {
+ "dataset": {"name": "demo_400"},
+ "learn": {
+ "text": markdown.markdown(
+ textwrap.dedent(
+ """
+ So far we have seen two types of change points: step changes
+ (also known as mean shift) and variance changes."""
+ )
+ )
+ },
+ "annotate": {"text": RUBRIC},
+ "evaluate": {
+ "text": markdown.markdown(
+ textwrap.dedent(
+ """
+ Remember that it's also possible for there to be *no change
+ points* in a dataset. It can sometimes be difficult to tell
+ whether a dataset has change points or not. In that case, it's
+ important to remember that we are looking for points where the
+ behaviour of the time series changes *abruptly*."""
+ )
+ )
+ },
+ },
+ 5: {
+ "dataset": {"name": "demo_500"},
+ "learn": {
+ "text": markdown.markdown(
+ textwrap.dedent(
+ """
+ Change points mark places in the time series where the
+ behaviour changes *abruptly*. While **outliers** are data
+ points that do not adhere to the prevailing behaviour of the
+ time series, they are not generally considered change points
+ because the behaviour of the time series before and after the
+ outlier is the same. """
+ )
+ )
+ },
+ "annotate": {"text": RUBRIC},
+ "evaluate": {
+ "text": markdown.markdown(
+ textwrap.dedent(
+ """
+ Outliers are quite common in real-world time series data, and
+ not all change point detection methods are robust against these
+ observations.
+
+ Note that short periods that consist of several consecutive
+ outlying data points could be considered an abrupt change in
+ behaviour of the time series. If you see this, use your
+ intuition to guide you."""
+ )
+ )
+ },
+ },
+ 6: {
+ "dataset": {"name": "demo_600"},
+ "learn": {
+ "text": markdown.markdown(
+ textwrap.dedent(
+ """
+ So far we've seen *step changes*, *variance changes*, and time
+ series with *outliers*. Can you think of another type of change
+ that can occur?"""
+ )
+ )
+ },
+ "annotate": {"text": RUBRIC},
+ "evaluate": {
+ "text": markdown.markdown(
+ textwrap.dedent(
+ """
+ What we see here is a change in *trend*. For these types of
+ changes it's not always easy to figure out exactly where the
+ change occurs, so it's harder to get it exactly right. Use
+ your intuition and keep in mind that it is normal for the
+ observations to be noisy."""
+ )
+ )
+ },
+ },
+ 7: {
+ "dataset": {"name": "demo_700"},
+ "learn": {
+ "text": markdown.markdown(
+ textwrap.dedent(
+ """
+ It is not uncommon for time series data from the real world to
+ display **seasonal variation**, for instance because certain
+ days of the week are more busy than others. Seasonality can
+ make it harder to find the change points in the dataset (if
+ there are any at all). Try to follow the pattern of
+ seasonality, and check whether the pattern changes in one of
+ the ways we've seen previously."""
+ )
+ )
+ },
+ "annotate": {"text": RUBRIC},
+ "evaluate": {
+ "text": markdown.markdown(
+ textwrap.dedent(
+ """
+ As you can see from this example, changes in seasonality can
+ occur as well. We expect that these changes are quite rare,
+ but it's nevertheless good to be aware of them."""
+ )
+ )
+ },
+ },
+}
+
+
+def redirect_user(demo_id, phase_id):
+ last_demo_id = max(DEMO_DATA.keys())
+ demo_last_phase_id = 3
+ if demo_id == last_demo_id and phase_id == demo_last_phase_id:
+ # User is introduced.
+ if current_user.is_introduced:
+ return redirect(url_for("main.index"))
+
+ current_user.is_introduced = True
+ db.session.commit()
+ # TODO: Assign real tasks to the user here.
+ return redirect(url_for("main.index"))
+ elif phase_id == demo_last_phase_id:
+ demo_id += 1
+ phase_id = 1
+ return redirect(
+ url_for("main.demo", demo_id=demo_id, phase_id=phase_id)
+ )
+ else:
+ phase_id += 1
+ return redirect(
+ url_for("main.demo", demo_id=demo_id, phase_id=phase_id)
+ )
+
+
+def process_annotations(demo_id):
+ annotation = request.get_json()
+ if annotation["identifier"] != demo_id:
+ LOGGER.error(
+ "User %s returned a task id in the demo that wasn't the demo id."
+ % current_user.username
+ )
+ flash(
+ "An internal error occurred, the administrator has been notified.",
+ "error",
+ )
+ return redirect(url_for("main.index"))
+
+ if annotation["changepoints"] is None:
+ retval = []
+ else:
+ retval = [cp["x"] for cp in annotation["changepoints"]]
+
+ # If the user is already introduced, we assume that their demo annotations
+ # are already in the database, and thus we don't put them back in (because
+ # we want the original ones).
+ if current_user.is_introduced:
+ return retval
+
+ dataset = Dataset.query.filter_by(
+ name=DEMO_DATA[demo_id]["dataset"]["name"]
+ ).first()
+ task = Task.query.filter_by(
+ annotator_id=current_user.id, dataset_id=dataset.id
+ ).first()
+ # this happens if the user returns to the same demo page, but hasn't
+ # completed the full demo yet. Same as above, not updating because we want
+ # the originals.
+ if not task is None:
+ return retval
+
+ # Create a new task
+ task = Task(annotator_id=current_user.id, dataset_id=dataset.id)
+ task.done = False
+ task.annotated_on = None
+ db.session.add(task)
+ db.session.commit()
+ 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 task as done
+ task.done = True
+ task.annotated_on = datetime.datetime.utcnow()
+ db.session.commit()
+
+ return retval
+
+
+def demo_learn(demo_id, form):
+ demo_data = DEMO_DATA[demo_id]["learn"]
+ return render_template(
+ "demo/learn.html",
+ title="Introduction – %i" % demo_id,
+ text=demo_data["text"],
+ form=form,
+ )
+
+
+def demo_annotate(demo_id):
+ demo_data = DEMO_DATA[demo_id]["annotate"]
+ dataset = Dataset.query.filter_by(
+ name=DEMO_DATA[demo_id]["dataset"]["name"]
+ ).first()
+ if dataset is None:
+ LOGGER.error(
+ "Demo requested unavailable dataset: %s"
+ % demo_data["dataset"]["name"]
+ )
+ flash(
+ "An internal error occured. The administrator has been notified. We apologise for the inconvenience, please try again later.",
+ "error",
+ )
+ return redirect(url_for("main.index"))
+ chart_data = load_data_for_chart(dataset.name, dataset.md5sum)
+ return render_template(
+ "annotate/index.html",
+ title="Introduction – %i" % demo_id,
+ data=chart_data,
+ rubric=demo_data["text"],
+ identifier=demo_id,
+ )
+
+
+def demo_evaluate(demo_id, phase_id, form):
+ demo_data = DEMO_DATA[demo_id]["evaluate"]
+ user_changepoints = session.get("user_changepoints", "__UNK__")
+ if user_changepoints == "__UNK__":
+ flash(
+ "The previous step of the demo was not completed successfully. Please try again.",
+ "error",
+ )
+ return redirect(
+ url_for("main.demo", demo_id=demo_id, phase_id=phase_id - 1)
+ )
+ dataset = Dataset.query.filter_by(
+ name=DEMO_DATA[demo_id]["dataset"]["name"]
+ ).first()
+ chart_data = load_data_for_chart(dataset.name, dataset.md5sum)
+ true_changepoints = get_demo_true_cps(dataset.name)
+ if true_changepoints is None:
+ flash(
+ "An internal error occurred, the administrator has been notified. We apologise for the inconvenience, please try again later.",
+ "error",
+ )
+ return redirect(url_for("main.index"))
+ annotations_true = [dict(index=x) for x in true_changepoints]
+ annotations_user = [dict(index=x) for x in user_changepoints]
+ return render_template(
+ "demo/evaluate.html",
+ title="Introduction – %i" % demo_id,
+ data=chart_data,
+ annotations_user=annotations_user,
+ annotations_true=annotations_true,
+ text=demo_data["text"],
+ form=form,
+ )
+
+
+@bp.route(
+ "/introduction/",
+ defaults={"demo_id": 1, "phase_id": 1},
+ methods=("GET", "POST"),
+)
+@bp.route(
+ "/introduction/<int:demo_id>/",
+ defaults={"phase_id": 1},
+ methods=("GET", "POST"),
+)
+@bp.route(
+ "/introduction/<int:demo_id>/<int:phase_id>", methods=("GET", "POST")
+)
+@login_required
+def demo(demo_id, phase_id):
+ form = NextForm()
+
+ if request.method == "POST":
+ if form.validate_on_submit():
+ return redirect_user(demo_id, phase_id)
+ else:
+ user_changepoints = process_annotations(demo_id)
+ session["user_changepoints"] = user_changepoints
+ return url_for("main.demo", demo_id=demo_id, phase_id=phase_id + 1)
+
+ if phase_id == 1:
+ return demo_learn(demo_id, form)
+ elif phase_id == 2:
+ return demo_annotate(demo_id)
+ elif phase_id == 3:
+ return demo_evaluate(demo_id, phase_id, form)
+ else:
+ abort(404)
diff --git a/app/main/forms.py b/app/main/forms.py
new file mode 100644
index 0000000..dcdffd3
--- /dev/null
+++ b/app/main/forms.py
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+
+from flask_wtf import FlaskForm
+
+from wtforms import SubmitField
+
+class NextForm(FlaskForm):
+ submit = SubmitField("Continue")
diff --git a/app/main/routes.py b/app/main/routes.py
index d249c5c..11de2f9 100644
--- a/app/main/routes.py
+++ b/app/main/routes.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import datetime
+import logging
from flask import render_template, flash, url_for, redirect, request
from flask_login import current_user
@@ -11,19 +12,13 @@ from app.main import bp
from app.models import Annotation, Task
from app.utils.datasets import load_data_for_chart
+logger = logging.getLogger(__name__)
+
RUBRIC = """
-<i>Please mark all the points in the time series where an <b>abrupt change</b>
-in
- the behaviour of the series occurs.</i>
-<br>
-If there are no such points, please click the <u>no changepoints</u> button.
-When you're ready, please click the <u>submit</u> button.
-<br>
-<br>
-<b>Note:</b> You can zoom and pan the graph if needed.
+Please mark the points in the time series where an <b>abrupt change</b> in
+ the behaviour of the series occurs. The goal is to define segments of the time
+ series that are separated by places where these abrupt changes occur.
<br>
-<br>
-Thank you!
"""
@@ -35,7 +30,7 @@ def index():
if current_user.is_authenticated:
user_id = current_user.id
tasks = Task.query.filter_by(annotator_id=user_id).all()
- tasks_done = [t for t in tasks if t.done]
+ tasks_done = [t for t in tasks if t.done and not t.dataset.is_demo]
tasks_todo = [t for t in tasks if not t.done]
return render_template(
"index.html",
@@ -48,14 +43,14 @@ def index():
@bp.route("/annotate/<int:task_id>", methods=("GET", "POST"))
@login_required
-def task(task_id):
+def annotate(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:
+ if annotation["identifier"] != task_id:
flash("Internal error: task id doesn't match.", "error")
return redirect(url_for(task_id=task_id))
@@ -100,10 +95,14 @@ def task(task_id):
flash("It's not possible to edit annotations at the moment.")
return redirect(url_for("main.index"))
data = load_data_for_chart(task.dataset.name, task.dataset.md5sum)
+ if data is None:
+ flash(
+ "An internal error occurred loading this dataset, the admin has been notified. Please try again later. We apologise for the inconvenience."
+ )
return render_template(
"annotate/index.html",
- title="Annotate %s" % task.dataset.name,
- task=task,
+ title=task.dataset.name.title(),
+ identifier=task.id,
data=data,
rubric=RUBRIC,
)
diff --git a/app/models.py b/app/models.py
index 5016505..f7dd2fa 100644
--- a/app/models.py
+++ b/app/models.py
@@ -22,8 +22,13 @@ class User(UserMixin, db.Model):
db.DateTime(), nullable=False, default=datetime.datetime.utcnow
)
is_admin = db.Column(db.Boolean(), default=False)
+
+ # after email is confirmed:
is_confirmed = db.Column(db.Boolean(), default=False)
+ # after all demo tasks completed:
+ is_introduced = db.Column(db.Boolean(), default=False)
+
def __repr__(self):
return "<User %r>" % self.username
@@ -76,6 +81,9 @@ class Dataset(db.Model):
)
md5sum = db.Column(db.String(32), unique=True, nullable=False)
+ # Whether or not dataset is a demo dataset.
+ is_demo = db.Column(db.Boolean(), default=True)
+
def __repr__(self):
return "<Dataset %r>" % self.name
diff --git a/app/static/annotate.css b/app/static/annotate.css
index 7dc222f..8079053 100644
--- a/app/static/annotate.css
+++ b/app/static/annotate.css
@@ -20,5 +20,9 @@ rect {
}
#rubric {
- text-align: center;
+ text-align: left;
+ padding-bottom: 20px;
+ padding-top: 10px;
+ width: 80%;
+ font-size: 16px;
}
diff --git a/app/static/css/demo/evaluate.css b/app/static/css/demo/evaluate.css
new file mode 100644
index 0000000..0c7dbc2
--- /dev/null
+++ b/app/static/css/demo/evaluate.css
@@ -0,0 +1,57 @@
+.graph-wrapper {
+ width: 90%;
+ margin: 0 auto;
+}
+
+#graph_user > svg {
+ width: 100%;
+ height: 100%;
+}
+
+#graph_true > svg {
+ width: 100%;
+ height: 100%;
+}
+
+.line {
+ fill: none;
+ stroke: blue;
+ clip-path: url(#clip);
+}
+
+circle {
+ clip-path: url(#clip);
+ fill: blue;
+}
+
+rect {
+ fill: white;
+ opacity: 0;
+}
+
+.marked {
+ fill: #fc8d62;
+ stroke: #fc8d62;
+}
+
+#next-btn {
+ text-align: right;
+ padding-bottom: 20px;
+}
+
+#lesson {
+ padding-top: 10px;
+ padding-bottom: 10px;
+}
+
+#lesson p {
+ font-size: 16px;
+}
+
+.ann-line {
+ stroke-dasharray: 5;
+ clip-path: url(#clip);
+ fill: #fc8d62;
+ stroke: #fc8d62;
+ stroke-width: 2px;
+}
diff --git a/app/static/css/demo/learn.css b/app/static/css/demo/learn.css
new file mode 100644
index 0000000..13dccd2
--- /dev/null
+++ b/app/static/css/demo/learn.css
@@ -0,0 +1,11 @@
+#next-btn {
+ text-align: right;
+}
+
+#lesson {
+ padding-top: 10px;
+}
+
+#lesson p {
+ font-size: 16px;
+}
diff --git a/app/static/css/global.css b/app/static/css/global.css
new file mode 100644
index 0000000..6258057
--- /dev/null
+++ b/app/static/css/global.css
@@ -0,0 +1,3 @@
+h1, h2, h3 {
+ margin-bottom: 20px;
+}
diff --git a/app/static/js/buttons.js b/app/static/js/buttons.js
index d26a15d..75adad2 100644
--- a/app/static/js/buttons.js
+++ b/app/static/js/buttons.js
@@ -8,7 +8,7 @@ function resetOnClick() {
updateTable();
}
-function noCPOnClick(task_id) {
+function noCPOnClick(identifier) {
var changepoints = document.getElementsByClassName("changepoint");
// validation
if (changepoints.length > 0) {
@@ -16,27 +16,27 @@ function noCPOnClick(task_id) {
return;
}
- var obj = {
- task: task_id,
- changepoints: null
- };
+ var obj = {}
+ obj["identifier"] = identifier;
+ obj["changepoints"] = null;
var xhr = new XMLHttpRequest();
- xhr.open("POST", "");
+ xhr.open("POST", "", false);
xhr.withCredentials = true;
xhr.setRequestHeader("Content-Type", "application/json");
- xhr.send(JSON.stringify(obj));
+ /* Flask's return to this POST must be a URL, not a template!*/
xhr.onreadystatechange = function() {
- if (xhr.readyState == XMLHttpRequest.DONE) {
- if (xhr.status === 200)
- window.location.href = xhr.responseText;
+ if (xhr.readyState == XMLHttpRequest.DONE && xhr.status == 200) {
+ window.location.href = xhr.responseText;
+ console.log("XHR Success: " + xhr.responseText);
} else {
- console.log("Error: " + xhr.status);
+ console.log("XHR Error: " + xhr.status);
}
- };
+ }
+ xhr.send(JSON.stringify(obj));
}
-function submitOnClick(task_id) {
+function submitOnClick(identifier) {
var changepoints = document.getElementsByClassName("changepoint");
// validation
if (changepoints.length === 0) {
@@ -45,7 +45,7 @@ function submitOnClick(task_id) {
}
var obj = {};
- obj["task"] = task_id;
+ obj["identifier"] = identifier;
obj["changepoints"] = [];
var i, cp;
for (i=0; i<changepoints.length; i++) {
@@ -62,14 +62,15 @@ function submitOnClick(task_id) {
xhr.open("POST", "");
xhr.withCredentials = true;
xhr.setRequestHeader("Content-Type", "application/json");
- xhr.send(JSON.stringify(obj));
+ /* Flask's return to this POST must be a URL, not a template!*/
xhr.onreadystatechange = function() {
- if (xhr.readyState == XMLHttpRequest.DONE) {
- if (xhr.status === 200)
- window.location.href = xhr.responseText;
+ if (xhr.readyState == XMLHttpRequest.DONE && xhr.status == 200) {
+ window.location.href = xhr.responseText;
+ console.log("XHR Success: " + xhr.responseText);
} else {
- console.log("Error: " + xhr.status);
+ console.log("XHR Error: " + xhr.status);
}
};
+ xhr.send(JSON.stringify(obj));
}
diff --git a/app/static/js/makeChart.js b/app/static/js/makeChart.js
index 35069b6..fd4a257 100644
--- a/app/static/js/makeChart.js
+++ b/app/static/js/makeChart.js
@@ -1,4 +1,4 @@
-function makeChart(data) {
+function makeChart(selector, data) {
var n = 0;
data.forEach(function(d) {
d.X = n++;
@@ -8,7 +8,7 @@ function makeChart(data) {
var divWidth = 1000;
var divHeight = 480;
- var svg = d3.select("#graph")
+ var svg = d3.select(selector)
.on("touchstart", nozoom)
.on("touchmove", nozoom)
.append("svg")
@@ -31,6 +31,7 @@ function makeChart(data) {
var xAxis = d3.axisBottom(x);
var yAxis = d3.axisLeft(y);
+ yAxis.ticks(0);
var xExtent = d3.extent(data, function(d) { return d.X; });
var xRange = xExtent[1] - xExtent[0];
@@ -49,13 +50,13 @@ function makeChart(data) {
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
- .attr("width", width - 30)
+ .attr("width", width - 18)
.attr("height", height)
- .attr("transform", "translate(" + 30 + ",0)");
+ .attr("transform", "translate(" + 18 + ",0)");
svg.append("g")
.attr("class", "axis axis--y")
- .attr("transform", "translate(" + 30 + ",0)")
+ .attr("transform", "translate(" + 18 + ",0)")
.call(yAxis);
svg.append("g")
@@ -127,3 +128,138 @@ function makeChart(data) {
d3.event.preventDefault();
}
}
+
+function makeChartAnnotated(selector, data, annotations) {
+ var n = 0;
+ data.forEach(function(d) {
+ d.X = n++;
+ d.Y = d.value;
+ });
+
+ var divWidth = 1000;
+ var divHeight = 480;
+
+ var svg = d3.select(selector)
+ .on("touchstart", nozoom)
+ .on("touchmove", nozoom)
+ .append("svg")
+ .attr("width", divWidth)
+ .attr("height", divHeight)
+ .attr("viewBox", "0 0 " + divWidth + " " + 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);
+ yAxis.ticks(0);
+
+ 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 - 18)
+ .attr("height", height)
+ .attr("transform", "translate(" + 18 + ",0)");
+
+ svg.append("g")
+ .attr("class", "axis axis--y")
+ .attr("transform", "translate(" + 18 + ",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);
+
+ 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); });
+ svg.select(".axis--x").call(xAxis);
+ }
+
+ function nozoom() {
+ d3.event.preventDefault();
+ }
+
+ 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("marked", "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");
+ break;
+ }
+ }
+ });
+}
+
diff --git a/app/static/view_annotation.css b/app/static/view_annotation.css
index 4b4f338..11bf3c8 100644
--- a/app/static/view_annotation.css
+++ b/app/static/view_annotation.css
@@ -11,7 +11,7 @@
.ann-line {
fill: none;
- stroke-dasharray: 3,3;
+ stroke-dasharray: 5;
clip-path: url(#clip);
}
@@ -64,4 +64,3 @@ rect {
fill: #b3b3b3;
stroke: #b3b3b3;
}
-
diff --git a/app/templates/_partials/modals.html b/app/templates/_partials/modals.html
new file mode 100644
index 0000000..1ffd85f
--- /dev/null
+++ b/app/templates/_partials/modals.html
@@ -0,0 +1,21 @@
+{% macro modal(id, title, message) %}
+<!-- Modal -->
+<div class="modal fade" id="{{ id }}Modal" tabindex="-1" role="dialog" aria-labelledby="{{ id }}ModalTitle" 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="{{ id }}LongTitle">{{ title }}</h5>
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+ <span aria-hidden="true">&times;</span>
+ </button>
+ </div>
+ <div class="modal-body">
+ {{ message }}
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
+ </div>
+ </div>
+ </div>
+</div>
+{% endmacro %}
diff --git a/app/templates/admin/annotations_by_dataset.html b/app/templates/admin/annotations_by_dataset.html
index 5ad98b6..eb7a7e8 100644
--- a/app/templates/admin/annotations_by_dataset.html
+++ b/app/templates/admin/annotations_by_dataset.html
@@ -1,7 +1,7 @@
{% extends "base.html" %}
{% block styles %}
-{{super()}}
+{{ super() }}
<link rel="stylesheet" href="{{url_for('static', filename='view_annotation.css')}}">{% endblock %}
{% block app_content %}
diff --git a/app/templates/admin/manage_datasets.html b/app/templates/admin/manage_datasets.html
index f14fe81..f68259f 100644
--- a/app/templates/admin/manage_datasets.html
+++ b/app/templates/admin/manage_datasets.html
@@ -42,18 +42,20 @@
<thead class="thead-dark">
<th scope="col">ID</th>
<th scope="col">Name</th>
- <th scope="col">Assigned Tasks</th>
- <th scope="col">Completed Tasks</th>
- <th scope="col">Percentage</th>
+ <th scope="col">Demo</th>
+ <th scope="col">Assigned Tasks</th>
+ <th scope="col">Completed Tasks</th>
+ <th scope="col">Percentage</th>
</thead>
{% for entry in overview %}
<tr>
- <th scope="row">{{ entry['id'] }}</th>
- <td>{{ entry['name'] }}</td>
- <td>{{ entry['assigned'] }}</td>
- <td>{{ entry['completed'] }}</td>
- <td>{{ entry['percentage'] }}</td>
- </tr>
+ <th scope="row">{{ entry['id'] }}</th>
+ <td>{{ entry['name'] }}</td>
+ <td>{% if entry['demo'] %}Yes{% else %}No{% endif %}</td>
+ <td>{{ entry['assigned'] }}</td>
+ <td>{{ entry['completed'] }}</td>
+ <td>{{ entry['percentage'] }}</td>
+ </tr>
{% endfor %}
</tr>
</table>
diff --git a/app/templates/admin/manage_users.html b/app/templates/admin/manage_users.html
index 5c44a48..8788958 100644
--- a/app/templates/admin/manage_users.html
+++ b/app/templates/admin/manage_users.html
@@ -1,70 +1,72 @@
{% extends "base.html" %}
{% block app_content %}
-<h1>Manage Users</h1>
+ <h1>Manage Users</h1>
-<div class="col-lg-3">
+ <div class="col-lg-3">
<div class="row">
- <form class="form" action="" method="POST">
- {{ form.hidden_tag() }}
- {{ form.user }}
- {{ form.delete(hidden='true', id='form-submit') }}
- <!-- Button trigger modal -->
- <button type="button" class="btn btn-danger" data-toggle="modal" data-target="#deleteModal">
- Delete
- </button>
- </form>
+ <form class="form" action="" method="POST">
+ {{ form.hidden_tag() }}
+ {{ form.user }}
+ {{ form.delete(hidden='true', id='form-submit') }}
+ <!-- Button trigger modal -->
+ <button type="button" class="btn btn-danger" data-toggle="modal" data-target="#deleteModal">
+ Delete
+ </button>
+ </form>
</div>
-</div>
+ </div>
-<!-- Modal -->
-<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel" 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="deleteModalLabel">Delete Dataset</h5>
- </div>
- <div class="modal-body">
- You are about to delete the user <span id="user-name"></span> <b>and</b> all associated tasks and annotations. Are you sure?
- </div>
- <div class="modal-footer">
- <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
- <button type="button" class="btn btn-success success" id="modal-confirm">Confirm</button>
+ <!-- Modal -->
+ <div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel" 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="deleteModalLabel">Delete Dataset</h5>
+ </div>
+ <div class="modal-body">
+ You are about to delete the user <span id="user-name"></span> <b>and</b> all associated tasks and annotations. Are you sure?
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
+ <button type="button" class="btn btn-success success" id="modal-confirm">Confirm</button>
+ </div>
</div>
</div>
</div>
-</div>
-<br>
-<h1>User Overview</h1>
-<article class="overview">
- <table class="table table-striped">
- <thead class="thead-dark">
- <th scope="col">ID</th>
- <th scope="col">Username</th>
- <th scope="col">Email</th>
- <th scope="col">Confirmed</th>
- <th scope="col">Last Active</th>
- <th scope="col">Admin</th>
- </thead>
- {% for user in users %}
- <tr>
- <th scope="row">{{ user.id }}</th>
- <td>{{ user.username }}</td>
- <td>{{ user.email }}</td>
- <td>{% if user.is_confirmed %}Yes{% else %}No{% endif %}</td>
- <td>{{ user.last_active }}</td>
- <td>{% if user.is_admin %}Yes{% else %}{% endif %}</td>
- </tr>
- {% endfor %}
- </tr>
- </table>
-</article>
+ <br>
+ <h1>User Overview</h1>
+ <article class="overview">
+ <table class="table table-striped">
+ <thead class="thead-dark">
+ <th scope="col">ID</th>
+ <th scope="col">Username</th>
+ <th scope="col">Email</th>
+ <th scope="col">Confirmed?</th>
+ <th scope="col">Introduced?</th>
+ <th scope="col">Last Active (UTC)</th>
+ <th scope="col">Admin?</th>
+ </thead>
+ {% for user in users %}
+ <tr>
+ <th scope="row">{{ user.id }}</th>
+ <td>{{ user.username }}</td>
+ <td>{{ user.email }}</td>
+ <td>{% if user.is_confirmed %}Yes{% else %}No{% endif %}</td>
+ <td>{% if user.is_introduced %}Yes{% else %}No{% endif %}</td>
+ <td>{{ user.last_active }}</td>
+ <td>{% if user.is_admin %}Yes{% else %}{% endif %}</td>
+ </tr>
+ {% endfor %}
+ </tr>
+ </table>
+ </article>
-<script>
-var conf = document.getElementById("modal-confirm");
-conf.onclick = function() {
- document.getElementById("form-submit").click();
-};
-</script>
+ <script>
+ var conf = document.getElementById("modal-confirm");
+ conf.onclick = function() {
+ document.getElementById("form-submit").click();
+ };
+ </script>
{% endblock %}
diff --git a/app/templates/annotate/index.html b/app/templates/annotate/index.html
index 106ef2a..f4172b6 100644
--- a/app/templates/annotate/index.html
+++ b/app/templates/annotate/index.html
@@ -1,12 +1,13 @@
{% extends "base.html" %}
+{% import "_partials/modals.html" as modals %}
{% block styles %}
-{{super()}}
+{{ super() }}
<link rel="stylesheet" href="{{url_for('static', filename='annotate.css')}}">
{% endblock %}
{% block app_content %}
-<h1>{{ task.dataset.name }}</h1>
+<h1>{{ title }}</h1>
<div id="rubric" class="row">
<div class="col-md-12">
@@ -18,54 +19,25 @@
<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>
+ <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>
+ <button class="btn btn-warning float-md-right" type="button"
+ id="btn-none">No change points</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>
+{{ modals.modal("submitNoCP", "No Change Points Selected", "Please use the
+\"No Change Points\" button when you think there are no change points in the
+time series.") }}
-<!-- 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>
+{{ modals.modal("NoCPYesCP", "Change Points Selected", "There are selected
+change points, please click the Reset button before clicking the \"No change
+points\" button.") }}
<h3>Selected Changepoints</h3>
<div id="changepoint-table">
@@ -79,7 +51,7 @@
<script src="{{ url_for('static', filename='js/makeChart.js') }}"></script>
<script src="{{ url_for('static', filename='js/updateTable.js') }}"></script>
<script src="{{ url_for('static', filename='js/buttons.js') }}"></script>
-<script>makeChart({{ data.chart_data | safe }});</script>
+<script>makeChart("#graph", {{ data.chart_data | safe }});</script>
<script>
// reset button
var reset = document.getElementById("btn-reset");
@@ -87,12 +59,12 @@ reset.onclick = resetOnClick;
// no changepoint button
var nocp = document.getElementById("btn-none");
nocp.onclick = function() {
- noCPOnClick({{ task.id }});
+ noCPOnClick({{ identifier }});
};
// submit button
var submit = document.getElementById("btn-submit");
submit.onclick = function() {
- submitOnClick({{ task.id }});
+ submitOnClick({{ identifier }});
};
</script>
{% endblock %}
diff --git a/app/templates/base.html b/app/templates/base.html
index 8bc31dc..f11760a 100644
--- a/app/templates/base.html
+++ b/app/templates/base.html
@@ -2,12 +2,13 @@
{% import "bootstrap/utils.html" as util %}
{% block styles %}
-{{ super() }}
+ {{ super() }}
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/global.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='sticky_footer.css') }}">
{% endblock %}
{% block title %}
- {% if title %}{{ title }} -- AnnotateChange{% else %}Welcome to AnnotateChange{% endif %}
+ {% if title %}{{ title }} &middot; AnnotateChange{% else %}Welcome to AnnotateChange{% endif %}
{% endblock %}
{% block navbar %}
@@ -28,13 +29,13 @@
</ul>
<ul class="nav navbar-nav navbar-right">
{% if current_user.is_anonymous %}
- <li><a href="{{ url_for('auth.login') }}">Login</a></li>
- <li><a href="{{ url_for('auth.register') }}">Register</a></li>
+ <li><a href="{{ url_for('auth.login') }}">Login</a></li>
+ <li><a href="{{ url_for('auth.register') }}">Register</a></li>
{% else %}
- {% if current_user.is_admin %}
- <li><a href="/admin" style="color: red;">Admin Panel</a></li>
- {% endif %}
- <li><a href="{{ url_for('auth.logout') }}">Logout</a></li>
+ {% if current_user.is_admin %}
+ <li><a href="{{ url_for('admin.index') }}" style="color: red;">Admin Panel</a></li>
+ {% endif %}
+ <li><a href="{{ url_for('auth.logout') }}">Logout</a></li>
{% endif %}
</ul>
</div>
@@ -44,19 +45,13 @@
{% block content %}
<main class="container">
-
- {{ util.flashed_messages(dismissible=True) }}
-
- {% block app_content %}
- {% endblock %}
+ {{ util.flashed_messages(dismissible=True) }}
+ {% block app_content %}
+ {% endblock %}
</main>
<footer class="fixed-bottom">
- <div class="container">
- <span class="text-muted">This is AnnotateChange version {{
- config.APP_VERSION }}. For questions or comments,
- please contact <a
- href="mailto:gvandenburg@turing.ac.uk">Gertjan van den
- Burg</a>.</span>
- </div>
+ <div class="container">
+ <span class="text-muted">This is AnnotateChange version {{ config.APP_VERSION }}. For questions or comments, please <a href="mailto:annotatechange@gmail.com">send us an email</a>.</span>
+ </div>
</footer>
{% endblock %}
diff --git a/app/templates/demo/evaluate.html b/app/templates/demo/evaluate.html
new file mode 100644
index 0000000..b148f66
--- /dev/null
+++ b/app/templates/demo/evaluate.html
@@ -0,0 +1,49 @@
+{% extends "base.html" %}
+{% import 'bootstrap/wtf.html' as wtf %}
+
+{% block styles %}
+{{ super() }}
+<link rel="stylesheet" href="{{url_for('static', filename='css/demo/evaluate.css')}}">
+{% endblock %}
+
+{% block app_content %}
+<h1>{{ title }}</h1>
+
+<div id="lesson" class="row">
+ <div class="col-md-8">
+ {{ text | safe }}
+ </div>
+</div>
+
+<div class="graph-wrapper">
+ <div class="row">
+ <p><b>Your annotation:</b></p>
+ </div>
+ <div id="graph_user"></div>
+ <div class="row">
+ <p><b>Ground truth:</b></p>
+ </div>
+ <div id="graph_true"></div>
+</div>
+
+<div class="row">
+ <div id="next-btn" class="col-md-10">
+ {{ wtf.quick_form(form, button_map={'submit': 'primary'}) }}
+ </div>
+</div>
+
+<script src="//d3js.org/d3.v5.min.js"></script>
+<script src="{{ url_for('static', filename='js/makeChart.js') }}"></script>
+<script>makeChartAnnotated(
+ "#graph_user",
+ {{ data.chart_data | safe }},
+ {{ annotations_user | safe }}
+);
+</script>
+<script>makeChartAnnotated(
+ "#graph_true",
+ {{ data.chart_data | safe }},
+ {{ annotations_true | safe }}
+);
+</script>
+{% endblock %}
diff --git a/app/templates/demo/learn.html b/app/templates/demo/learn.html
new file mode 100644
index 0000000..8d3d6c9
--- /dev/null
+++ b/app/templates/demo/learn.html
@@ -0,0 +1,23 @@
+{% extends "base.html" %}
+{% import 'bootstrap/wtf.html' as wtf %}
+
+{% block styles %}
+{{ super() }}
+<link rel="stylesheet" href="{{url_for('static', filename='css/demo/learn.css')}}">
+{% endblock %}
+
+{% block app_content %}
+<h1>{{ title }}</h1>
+
+<div id="lesson" class="row">
+ <div class="col-md-6">
+ {{ text | safe }}
+ </div>
+</div>
+
+<div class="row">
+ <div id="next-btn" class="col-md-6">
+ {{ wtf.quick_form(form, button_map={'submit': 'primary'}) }}
+ </div>
+</div>
+{% endblock %}
diff --git a/app/templates/index.html b/app/templates/index.html
index 76edbcd..930bfd3 100644
--- a/app/templates/index.html
+++ b/app/templates/index.html
@@ -1,69 +1,98 @@
{% extends "base.html" %}
{% block styles %}
-{{ super() }}
-<link rel="stylesheet" href="{{url_for('static', filename='user_index.css')}}">
+ {{ super() }}
+ <link rel="stylesheet" href="{{url_for('static', filename='user_index.css')}}">
{% endblock %}
{% block app_content %}
-{% if current_user.is_authenticated %}
- <h1>Hi, {{ current_user.username }}!</h1>
- {% if tasks_todo %}
- <h2>Annotations to be done</h2>
- <br>
- <p>
- Below are the datasets that we've asked you to annotate, thank you
- very much for your help!
- </p>
- <br>
- <div class="tasks-todo">
- <table class="table table-striped">
- <thead class="thead-dark">
- <th scope="col">Name</th>
- </thead>
- {% for task in tasks_todo %}
- <tr>
- <td><a href="/annotate/{{ task.id }}">{{ task.dataset.name }}</a></td>
- </tr>
- {% endfor %}
- </table>
- </div>
- {% elif tasks_todo|length == 0 and tasks_done|length > 0 %}
- <div id="done">
- <img src="/static/done.png">
- <span>No more annotations to do! Thank you so much for your
- help, <b>you rock!</b></span>
- </div>
- {% else %}
- <span>There are no datasets for you to annotate at the moment, please
- check back again later. Thank you!</span>
- {% endif %}
- {% if tasks_done %}
- <h2>Completed Annotations</h2>
- <div class="tasks-done">
- <table class="table table-striped">
- <thead>
- <th scope="col">Name</th>
- <th scope="col">Completed On</th>
- </thead>
- {% for task in tasks_done %}
- <tr>
- <td>{{ task.dataset.name }}</td>
- <td>{{ task.annotated_on }}</td>
- </tr>
- {% endfor %}
- </table>
- </div>
- {% endif %}
-{% else %}
- <article>
- <div>
- Welcome to <i>AnnotateChange</i> a tool for annotating time
- series data for changepoint analysis.
- <br>
- <br>
- Please log in or register to get started.
- </div>
- </article>
-{% endif %}
+ {% if current_user.is_authenticated %}
+ <h1>Hi, {{ current_user.username }}!</h1>
+ {% endif %}
+ <p>
+ Welcome to <i>AnnotateChange</i> a tool for annotating time series data
+ for changepoint analysis.
+ </p>
+ {% if not current_user.is_authenticated %}
+ <br>
+ <p>
+ Please <a href="{{ url_for('auth.login') }}">log in</a> or
+ <a href="{{ url_for('auth.register') }}">register</a> to get started.
+ </p>
+ {% endif %}
+ {% if current_user.is_authenticated %}
+ <h3>Introduction</h3>
+ {% if not current_user.is_introduced %}
+ <a href="{{ url_for('main.demo') }}">Click here to start the introduction to AnnotateChange.</a>
+ {% if tasks_todo|length == 0 and tasks_done|length == 0 %}
+ <br>
+ <br>
+ <p>
+ When you have finished the introduction, the datasets to annotate
+ will appear here.
+ </p>
+ {% endif %}
+ {% else %}
+ <p>
+ Thank you for completing the introduction. If you want to revisit
+ it, you can do so by <a href="{{ url_for('main.demo') }}">clicking here</a>.
+ </p>
+ {% if tasks_todo|length == 0 and tasks_done|length == 0 %}
+ <br>
+ <br>
+ <p>
+ There are currently no datasets for you to annotate. Please check back
+ again later.
+ </p>
+ {% endif %}
+ {% endif %}
+ {% if tasks_todo %}
+ <h3>Datasets to Annotate</h3>
+ <p>
+ Below are the datasets that we would like you to annotate, thank you
+ very much for your help!
+ </p>
+ <div class="tasks-todo">
+ <table class="table table-striped">
+ <thead class="thead-dark">
+ <th scope="col">Name</th>
+ </thead>
+ {% for task in tasks_todo %}
+ <tr>
+ <td>
+ <a href="{{ url_for('main.annotate', task_id=task.id) }}">
+ {{ task.dataset.name | title }}
+ </a>
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ </div>
+ {% elif tasks_todo|length == 0 and tasks_done|length > 0 %}
+ <div id="done">
+ <img src="{{ url_for('static', filename='done.png') }}">
+ <span>
+ No more annotations to do! Thank you so much for your help,
+ <b>you rock!</b>
+ </span>
+ </div>
+ {% endif %}
+ {% if tasks_done %}
+ <h3>Completed Annotations</h3>
+ <div class="tasks-done">
+ <table class="table table-striped">
+ <thead>
+ <th scope="col">Name</th>
+ <th scope="col">Completed On</th>
+ </thead>
+ {% for task in tasks_done %}
+ <tr>
+ <td>{{ task.dataset.name | title }}</td>
+ <td>{{ task.annotated_on }}</td>
+ </tr>
+ {% endfor %}
+ </table>
+ </div>
+ {% endif %}
+ {% endif %}
{% endblock %}
diff --git a/app/utils/datasets.py b/app/utils/datasets.py
index 74785cb..1fef85f 100644
--- a/app/utils/datasets.py
+++ b/app/utils/datasets.py
@@ -41,7 +41,7 @@ import re
from flask import current_app
-logger = logging.getLogger(__file__)
+LOGGER = logging.getLogger(__file__)
def validate_dataset(filename):
@@ -95,6 +95,32 @@ def get_name_from_dataset(filename):
return data["name"]
+def dataset_is_demo(filename):
+ with open(filename, "rb") as fid:
+ data = json.load(fid)
+ return "demo" in data
+
+
+def get_demo_true_cps(name):
+ dataset_dir = os.path.join(
+ current_app.instance_path, current_app.config["DATASET_DIR"]
+ )
+ target_filename = os.path.join(dataset_dir, name + ".json")
+ if not os.path.exists(target_filename):
+ LOGGER.error("Dataset with name '%s' can't be found!" % name)
+ return None
+ with open(target_filename, "rb") as fid:
+ data = json.load(fid)
+ if not "demo" in data:
+ LOGGER.error("Asked for 'demo' key in non-demo dataset '%s'" % name)
+ return None
+ if not "true_CPs" in data["demo"]:
+ LOGGER.error(
+ "Expected field'true_cps' field missing for dataset '%s'" % name
+ )
+ return data["demo"]["true_CPs"]
+
+
def md5sum(filename):
""" Compute the MD5 hash for a given filename """
blocksize = 65536
@@ -112,8 +138,11 @@ def load_data_for_chart(name, known_md5):
current_app.instance_path, current_app.config["DATASET_DIR"]
)
target_filename = os.path.join(dataset_dir, name + ".json")
+ if not os.path.exists(target_filename):
+ LOGGER.error("Dataset with name '%s' can't be found!" % name)
+ return None
if not md5sum(target_filename) == known_md5:
- logger.error(
+ LOGGER.error(
"""
MD5 checksum failed for dataset with name: %s.
Found: %s.