aboutsummaryrefslogtreecommitdiff
path: root/app/static
diff options
context:
space:
mode:
authorGertjan van den Burg <gertjanvandenburg@gmail.com>2019-07-31 16:28:31 +0100
committerGertjan van den Burg <gertjanvandenburg@gmail.com>2019-07-31 16:28:31 +0100
commit1fd0dd8b28bac19431a8c577ee78ea655882ad82 (patch)
tree5345d5070a8a472b50538f05349a27751ef87ffa /app/static
parentAdd demo data to repo (diff)
downloadAnnotateChange-1fd0dd8b28bac19431a8c577ee78ea655882ad82.tar.gz
AnnotateChange-1fd0dd8b28bac19431a8c577ee78ea655882ad82.zip
Add support for multidimensional datasets
Diffstat (limited to 'app/static')
-rw-r--r--app/static/css/admin/view_annotation.css6
-rw-r--r--app/static/css/demo/evaluate.css5
-rw-r--r--app/static/css/main/annotate.css5
-rw-r--r--app/static/js/buttons.js14
-rw-r--r--app/static/js/makeChart.js1
-rw-r--r--app/static/js/makeChartMulti.js319
-rw-r--r--app/static/js/updateTable.js74
7 files changed, 416 insertions, 8 deletions
diff --git a/app/static/css/admin/view_annotation.css b/app/static/css/admin/view_annotation.css
index 11bf3c8..09d5a4d 100644
--- a/app/static/css/admin/view_annotation.css
+++ b/app/static/css/admin/view_annotation.css
@@ -9,6 +9,12 @@
clip-path: url(#clip);
}
+.z-line line {
+ stroke: #ccc;
+ shape-rendering: crispEdges;
+}
+
+
.ann-line {
fill: none;
stroke-dasharray: 5;
diff --git a/app/static/css/demo/evaluate.css b/app/static/css/demo/evaluate.css
index 0c7dbc2..aea3162 100644
--- a/app/static/css/demo/evaluate.css
+++ b/app/static/css/demo/evaluate.css
@@ -19,6 +19,11 @@
clip-path: url(#clip);
}
+.z-line line {
+ stroke: #ccc;
+ shape-rendering: crispEdges;
+}
+
circle {
clip-path: url(#clip);
fill: blue;
diff --git a/app/static/css/main/annotate.css b/app/static/css/main/annotate.css
index 595f2a2..d1ff2b3 100644
--- a/app/static/css/main/annotate.css
+++ b/app/static/css/main/annotate.css
@@ -9,6 +9,11 @@
clip-path: url(#clip);
}
+.z-line line {
+ stroke: #ccc;
+ shape-rendering: crispEdges;
+}
+
circle {
clip-path: url(#clip);
fill: blue;
diff --git a/app/static/js/buttons.js b/app/static/js/buttons.js
index 75adad2..edd3f56 100644
--- a/app/static/js/buttons.js
+++ b/app/static/js/buttons.js
@@ -47,15 +47,19 @@ function submitOnClick(identifier) {
var obj = {};
obj["identifier"] = identifier;
obj["changepoints"] = [];
- var i, cp;
+
+ var i, cp, xval, seen = [];
for (i=0; i<changepoints.length; i++) {
cp = changepoints[i];
+ xval = cp.getAttribute("data_X");
elem = {
id: i,
- x: cp.getAttribute("data_X"),
- y: cp.getAttribute("data_Y")
+ x: xval
};
+ if (seen.includes(xval))
+ continue;
obj["changepoints"].push(elem);
+ seen.push(xval);
}
var xhr = new XMLHttpRequest();
@@ -66,11 +70,7 @@ function submitOnClick(identifier) {
xhr.onreadystatechange = function() {
if (xhr.readyState == XMLHttpRequest.DONE && xhr.status == 200) {
window.location.href = xhr.responseText;
- console.log("XHR Success: " + xhr.responseText);
- } else {
- 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 1a0283d..5d759bf 100644
--- a/app/static/js/makeChart.js
+++ b/app/static/js/makeChart.js
@@ -119,7 +119,6 @@ function baseChart(selector, data, clickFunction, annotations, annotationFunctio
// 3. rectangle to keep the drawing
// 4. line
// 5. circles with a click event
- //
// clip path
svg.append("defs")
diff --git a/app/static/js/makeChartMulti.js b/app/static/js/makeChartMulti.js
new file mode 100644
index 0000000..4b7168f
--- /dev/null
+++ b/app/static/js/makeChartMulti.js
@@ -0,0 +1,319 @@
+// Based on: https://tylernwolf.com/corrdisp/index.html
+
+/*
+# TODO NOTES:
+- we now have two definitions of the data (data and labelData). The best thing
+would be to preprocess the data such that it is formatted as labelData (probably)
+*/
+
+/*
+ * Our data is a struct with top-level keys "time" (optional) and "values"
+ * (required). The "values" object is an array with a variable length of
+ * variables.
+ */
+
+function preprocess(data) {
+ var cleanData = [];
+ var nVar = data.values.length;
+
+ if (data["time"] != null) {
+ console.warn("Time axis is not implemented yet. Ignoring.");
+ }
+ for (i=0; i<data.values[0].raw.length; i++) {
+ var item = {"X": i}
+ for (j=0; j<nVar; j++) {
+ item["Y" + j] = data.values[j].raw[i];
+ }
+ cleanData.push(item);
+ }
+ return cleanData;
+}
+
+
+function getLabelData(data, lbl) {
+ var lblData = [];
+ for (i=0; i<data.length; i++) {
+ var item = {"X": data[i]["X"], "Y": data[i][lbl]};
+ lblData.push(item);
+ }
+ return lblData;
+}
+
+function makeLabelData(data, numCharts) {
+ var labelData = {};
+ for (j=0; j<numCharts; j++) {
+ labelData[j] = getLabelData(data, "Y"+j);
+ }
+ return labelData;
+}
+
+function Axes(data, numCharts, width, lineHeight, chartPadding) {
+ this.chartColors = d3.scaleOrdinal();
+
+ var xRange = data.length;
+ var xDomainMin = 0 - xRange * 0.02;
+ var xDomainMax = data.length + xRange * 0.02;
+
+ // NOTE: domain needs to be adapted if we provide a time axis
+ this.xScale = d3.scaleLinear()
+ .domain([xDomainMin, xDomainMax])
+ .range([0, width]);
+
+ this.xScaleOrig = d3.scaleLinear()
+ .domain([xDomainMin, xDomainMax])
+ .range([0, width]);
+
+ this.xAxis = d3.axisBottom(this.xScale);
+
+ var labels = [];
+ var chartRange = [];
+
+ for (j=0; j<numCharts; j++) {
+ var v = [];
+ for (i=0; i<data.length; i++)
+ v.push(data[i]["Y" + j]);
+
+ var extent = d3.extent(v);
+ var range = extent[1] - extent[0];
+ var minVal = extent[0] - range * 0.05;
+ var maxVal = extent[1] + range * 0.05;
+
+ this["yScale" + j] = d3.scaleLinear();
+ this["yScale" + j].domain([minVal, maxVal]);
+ this["yScale" + j].range([lineHeight, 0]);
+
+ labels.push("Y" + j);
+ chartRange.push(j * (lineHeight + chartPadding));
+ }
+
+ this.charts = d3.scaleOrdinal();
+ this.charts.domain(labels);
+ this.charts.range(chartRange);
+};
+
+
+function baseChart(selector, data, clickFunction, annotations, annotationFunction) {
+ var lineObjects = {};
+ var pointSets = {}
+
+ var lineHeight = 150;
+ var lineWidth = 800;
+
+ var chartPadding = 30;
+ var chartWidth = 1000;
+
+ var visPadding = {
+ top: 10,
+ right: 20,
+ bottom: 10,
+ left: 70,
+ middle: 50
+ };
+
+ // Data preprocessing (TODO: remove need to have labelData *and*
+ // preprocess!)
+ var numCharts = data.values.length;
+ data = preprocess(data);
+ var labelData = makeLabelData(data, numCharts);
+
+ var width = lineWidth - visPadding.middle;
+ var height = (chartPadding + lineHeight) * numCharts + chartPadding + 50;
+
+ var axes = new Axes(data, numCharts, width, lineHeight, chartPadding);
+
+ var zoomObj = d3.zoom()
+ .scaleExtent([1, 100])
+ .translateExtent([[0, 0], [width, height]])
+ .extent([[0, 0], [width, height]])
+ .on("zoom", zoomTransform);
+
+ function zoomTransform() {
+ transform = d3.event.transform;
+
+ // transform the axes
+ axes.xScale.domain(transform.rescaleX(axes.xScaleOrig).domain());
+
+ // transform the data lines
+ for (j=0; j<numCharts; j++) {
+ container.select("#line-"+j).attr("d", lineObjects[j]);
+ }
+
+ // transform the points
+ for (j=0; j<numCharts; j++) {
+ pointSets[j].data(labelData[j])
+ .attr("cx", function(d) { return axes.xScale(d.X); })
+ .attr("cy", function(d) { return axes["yScale" + j](d.Y); })
+ }
+
+ svg.select(".x-axis").call(axes.xAxis);
+
+ // transform the annotation lines (if any)
+ annoLines = container.selectAll(".ann-line")
+ annoLines._groups[0].forEach(function(l) {
+ l.setAttribute("x1", axes.xScale(l.getAttribute("cp_idx")));
+ l.setAttribute("x2", axes.xScale(l.getAttribute("cp_idx")));
+ });
+ }
+
+ var svg = d3.select(selector).append('svg')
+ .attr("width", chartWidth)
+ .attr("height", height);
+
+ svg.append("defs")
+ .append("clipPath")
+ .attr("id", "clip")
+ .append("rect")
+ .attr("width", width)
+ .attr("height", height)
+ .attr("transform", "translate(0, 0)");
+
+ var container = svg.append("g")
+ .attr("class", "container")
+ .attr('transform', 'translate(' + visPadding.left + ',' + visPadding.top + ')');
+
+ var ytrans = numCharts * (lineHeight + chartPadding) - chartPadding / 2;
+
+ // x axis
+ container.append("g")
+ .attr("class", "x-axis")
+ .attr("transform", "translate(0, " + ytrans + ")")
+ .call(axes.xAxis);
+
+ // x axis label
+ container.append("text")
+ .attr("text-anchor", "middle")
+ .attr("class", "axis-label")
+ .attr("transform", "translate(" + (width - 20) + "," +
+ (ytrans + 40) + ")")
+ .text("Time");
+
+ // wrapper for zoom
+ var gZoom = container.append("g").call(zoomObj);
+
+ // rectangle for the graph area
+ gZoom.append("rect")
+ .attr("width", width)
+ .attr("height", height);
+
+ // wrapper for charts
+ var chartWrap = gZoom.append('g')
+ .attr('class', 'chart-wrap');
+
+ for (j=0; j<numCharts; j++) {
+ var lbl = "Y" + j;
+
+ // wrapper for the line, includes translation.
+ var lineWrap = chartWrap.append('g')
+ .attr('class', 'line-wrap')
+ .attr('transform', 'translate(0,' + axes.charts(lbl) + ")");
+
+ // line for the minimum
+ var minLine = lineWrap.append('g')
+ .attr('class', 'z-line');
+ var minVal = d3.min(labelData[j], function(d) { return d.Y; });
+ minLine.append('line')
+ .attr('x1', 0)
+ .attr('x2', lineWidth - visPadding.middle)
+ .attr('y1', axes['yScale' + j](minVal))
+ .attr('y2', axes['yScale' + j](minVal));
+
+ // create the line object
+ var lineobj = d3.line()
+ .x(function(d) { return axes.xScale(d.X); })
+ .y(function(d) { return axes['yScale'+j](d.Y); });
+
+ lineObjects[j] = lineobj;
+
+ var line = lineWrap.append('path')
+ .datum(labelData[j])
+ .attr('class', 'line')
+ .attr('id', 'line-'+j)
+ .attr('d', lineobj);
+
+ // add the points
+ pointSets[j] = lineWrap.selectAll('circle')
+ .data(labelData[j])
+ .enter()
+ .append('circle')
+ .attr('cx', function(d) { return axes.xScale(d.X); })
+ .attr('cy', function(d) { return axes['yScale'+j](d.Y); })
+ .attr('data_X', function(d) { return d.X; })
+ .attr('data_Y', function(d) { return d.Y; })
+ .attr('r', 5)
+ .attr('id', function(d) { return 'circle-x' + d.X + '-y' + j; })
+ .on('click', function(d, i) {
+ d.element = this;
+ return clickFunction(d, i, numCharts);
+ });
+
+ // handle the annotations
+ // annotations is a dict with keys j = 0..numCharts-1.
+ if (annotations === null)
+ continue;
+ annotations.forEach(function(a) {
+ for (i=0; i<pointSets[j]._groups[0].length; i++) {
+ p = pointSets[j]._groups[0][i];
+ if (p.getAttribute("data_X") != a.index)
+ continue;
+ var elem = d3.select(p);
+ annotationFunction(a, elem, lineWrap, axes, j);
+ }
+ });
+ }
+}
+
+function annotateChart(selector, data) {
+ handleClick = function(d, i, numCharts) {
+ if (d3.event.defaultPrevented) return;
+
+ var X = d.element.getAttribute('data_X');
+ for (j=0; j<numCharts; j++) {
+ var id = '#circle-x' + X + '-y' + j;
+ var elem = d3.select(id);
+
+ if (elem.classed("changepoint")) {
+ elem.style("fill", null);
+ elem.classed("changepoint", false);
+ } else {
+ elem.style("fill", "red");
+ elem.classed("changepoint", true);
+ }
+ }
+
+ updateTableMulti(numCharts);
+ }
+ baseChart(selector, data, handleClick, null, null);
+}
+
+function viewAnnotations(selector, data, annotations) {
+ function handleAnnotation(ann, elem, view, axes, j) {
+ elem.classed("marked", true);
+ ymin = axes['yScale' + j].domain()[0];
+ ymax = axes['yScale' + j].domain()[1];
+ view.append("line")
+ .attr("cp_idx", ann.index)
+ .attr("y1", axes['yScale' + j](ymax))
+ .attr("y2", axes['yScale' + j](ymin))
+ .attr("x1", axes["xScale"](ann.index))
+ .attr("x2", axes["xScale"](ann.index))
+ .attr("class", "ann-line");
+ }
+
+ baseChart(selector, data, function() {}, annotations, handleAnnotation);
+}
+
+function adminViewAnnotations(selector, data, annotations) {
+ function handleAnnotation(ann, elem, view, axes, j) {
+ elem.classed("marked", true);
+ ymin = axes['yScale' + j].domain()[0];
+ ymax = axes['yScale' + j].domain()[1];
+ view.append("line")
+ .attr("cp_idx", ann.index)
+ .attr("y1", axes['yScale' + j](ymax))
+ .attr("y2", axes['yScale' + j](ymin))
+ .attr("x1", axes["xScale"](ann.index))
+ .attr("x2", axes["xScale"](ann.index))
+ .attr("class", "ann-line" + " " + ann.user);
+ }
+ baseChart(selector, data, function() {}, annotations, handleAnnotation);
+}
diff --git a/app/static/js/updateTable.js b/app/static/js/updateTable.js
index 7d68aad..321114f 100644
--- a/app/static/js/updateTable.js
+++ b/app/static/js/updateTable.js
@@ -60,3 +60,77 @@ function updateTable() {
table.appendChild(body);
myTableDiv.appendChild(table);
}
+
+function updateTableMulti(numCharts) {
+ var changepoints = document.getElementsByClassName('changepoint');
+ var myTableDiv = document.getElementById('changepoint-table');
+ var oldTable = document.getElementById('cp-table');
+ oldTable.remove();
+
+ var table = document.createElement('TABLE');
+ 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";
+ for (j=0; j<numCharts; j++)
+ heading[2+j] = "Y" + (j + 1);
+
+ // 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 consolidated = {};
+ var keys = [];
+ for (i=0; i<changepoints.length; i++) {
+ cp = changepoints[i];
+ data = d3.select(cp).data()[0];
+ if (!(data.X in consolidated)) {
+ consolidated[data.X] = {}
+ keys.push(data.X);
+ }
+ id_parts = cp.id.split('-')
+ yindex = id_parts[id_parts.length - 1];
+ consolidated[data.X][yindex] = data.Y;
+ }
+ keys.sort(function(a, b) { return parseInt(a) - parseInt(b); });
+
+ var body = document.createElement("TBODY");
+ for (i=0; i<keys.length; i++) {
+ X = keys[i];
+ cp = consolidated[keys[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(X));
+ tr.appendChild(td);
+
+ for (j=0; j<numCharts; j++) {
+ var td = document.createElement('TD');
+ td.appendChild(document.createTextNode(cp['y' + j]));
+ tr.appendChild(td);
+ }
+
+ body.appendChild(tr);
+ }
+ table.appendChild(body);
+ myTableDiv.appendChild(table);
+}