diff options
| author | Gertjan van den Burg <gertjanvandenburg@gmail.com> | 2019-07-31 16:28:31 +0100 |
|---|---|---|
| committer | Gertjan van den Burg <gertjanvandenburg@gmail.com> | 2019-07-31 16:28:31 +0100 |
| commit | 1fd0dd8b28bac19431a8c577ee78ea655882ad82 (patch) | |
| tree | 5345d5070a8a472b50538f05349a27751ef87ffa /app/static | |
| parent | Add demo data to repo (diff) | |
| download | AnnotateChange-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.css | 6 | ||||
| -rw-r--r-- | app/static/css/demo/evaluate.css | 5 | ||||
| -rw-r--r-- | app/static/css/main/annotate.css | 5 | ||||
| -rw-r--r-- | app/static/js/buttons.js | 14 | ||||
| -rw-r--r-- | app/static/js/makeChart.js | 1 | ||||
| -rw-r--r-- | app/static/js/makeChartMulti.js | 319 | ||||
| -rw-r--r-- | app/static/js/updateTable.js | 74 |
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); +} |
