data_annotation_platform/app/static/js/makeChartMulti.js (182 lines of code) (raw):

// 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.log("Time axis is not implemented yet. Ignoring."); } for (i=0; i<data.values[0].raw.length; i++) { // NOTE: never change this to 1-based indexing. This index is // ultimately stored in the database as the change point // index! 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++) { if (isNaN(data[i][lbl])) continue; 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 = {} /* Note: * It may be tempting to scale the width/height of the div to be * proportional to the size of the window. However this may cause some * users with wide screens to perceive changes in the time series * differently than others because the horizontal axis is more * stretched out. It is therefore better to keep the size of the graph * the same for all users. */ var lineHeight = 150; var lineWidth = 1000; var chartPadding = 30; var visPadding = { top: 10, right: 0, bottom: 10, left: 0, 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++) { bigWrapper.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 = bigWrapper.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", lineWidth) .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 bigWrapper = svg.append("g") .attr("class", "bigWrapper") .attr('transform', 'translate(' + visPadding.left + ',' + visPadding.top + ')'); var ytrans = numCharts * (lineHeight + chartPadding) - chartPadding / 2; // x axis bigWrapper.append("g") .attr("class", "x-axis") .attr("transform", "translate(0, " + ytrans + ")") .call(axes.xAxis); // x axis label bigWrapper.append("text") .attr("text-anchor", "middle") .attr("class", "axis-label") .attr("transform", "translate(" + (width - 20) + "," + (ytrans + 40) + ")") .text("Time"); // wrapper for zoom var gZoom = bigWrapper.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); }