diff options
| -rw-r--r-- | app/admin/routes.py | 4 | ||||
| -rw-r--r-- | app/main/demo.py | 31 | ||||
| -rw-r--r-- | app/main/routes.py | 5 | ||||
| -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 | ||||
| -rw-r--r-- | app/templates/admin/annotations_by_dataset.html | 4 | ||||
| -rw-r--r-- | app/templates/annotate/index.html | 6 | ||||
| -rw-r--r-- | app/templates/demo/evaluate.html | 37 | ||||
| -rw-r--r-- | demo_data/demo_800.json | 224 |
14 files changed, 709 insertions, 26 deletions
diff --git a/app/admin/routes.py b/app/admin/routes.py index b8ca81b..9b7688b 100644 --- a/app/admin/routes.py +++ b/app/admin/routes.py @@ -129,7 +129,7 @@ def manage_users(): @admin_required def manage_datasets(): dataset_list = [(d.id, d.name) for d in Dataset.query.all()] - dataset_list.sort(key=lambda x : x[1]) + dataset_list.sort(key=lambda x: x[1]) form = AdminManageDatasetsForm() form.dataset.choices = dataset_list @@ -281,11 +281,13 @@ def view_annotations_by_dataset(dset_id): anno_clean.append(dict(user=uid, index=ann.cp_index)) data = load_data_for_chart(dataset.name, dataset.md5sum) + is_multi = len(data["chart_data"]["values"]) > 1 data["annotations"] = anno_clean return render_template( "admin/annotations_by_dataset.html", title="View Annotations for dataset", data=data, + is_multi=is_multi, ) diff --git a/app/main/demo.py b/app/main/demo.py index afb7f61..2edae04 100644 --- a/app/main/demo.py +++ b/app/main/demo.py @@ -252,6 +252,32 @@ DEMO_DATA = { ) }, }, + 8: { + "dataset": {"name": "demo_800"}, + "learn": { + "text": markdown.markdown( + textwrap.dedent( + """ + In practice time series datasets are not just one + dimensional, but can be multidimensional too. A change + point in such a time series does not necessarily occur in + all dimensions simultaneously. It is therefore important to + evaluate the behaviour of each dimension individually, as + well as in relation to the others.""" + ) + ) + }, + "annotate": {"text": RUBRIC}, + "evaluate": { + "text": markdown.markdown( + textwrap.dedent( + """ + In this example of a multidimensional time series, the + change only occurred in a single dimension.""" + ) + ) + }, + }, } @@ -373,13 +399,16 @@ def demo_annotate(demo_id): "error", ) return redirect(url_for("main.index")) + chart_data = load_data_for_chart(dataset.name, dataset.md5sum) + is_multi = len(chart_data["chart_data"]["values"]) > 1 return render_template( "annotate/index.html", title="Introduction – %i" % demo_id, data=chart_data, rubric=demo_data["text"], identifier=demo_id, + is_multi=is_multi, ) @@ -398,6 +427,7 @@ def demo_evaluate(demo_id, phase_id, form): name=DEMO_DATA[demo_id]["dataset"]["name"] ).first() chart_data = load_data_for_chart(dataset.name, dataset.md5sum) + is_multi = len(chart_data["chart_data"]["values"]) > 1 true_changepoints = get_demo_true_cps(dataset.name) if true_changepoints is None: flash( @@ -415,6 +445,7 @@ def demo_evaluate(demo_id, phase_id, form): annotations_true=annotations_true, text=demo_data["text"], form=form, + is_multi=is_multi ) diff --git a/app/main/routes.py b/app/main/routes.py index ad78f27..02ec7c9 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -18,7 +18,8 @@ logger = logging.getLogger(__name__) RUBRIC = """ Please mark the point(s) 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. + series that are separated by places where these abrupt changes occur. Recall + that it is also possible for there to be no such changes. <br> """ @@ -112,10 +113,12 @@ def annotate(task_id): flash( "An internal error occurred loading this dataset, the admin has been notified. Please try again later. We apologise for the inconvenience." ) + is_multi = len(data["chart_data"]["values"]) > 1 return render_template( "annotate/index.html", title=task.dataset.name.title(), identifier=task.id, data=data, rubric=RUBRIC, + is_multi=is_multi, ) 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); +} diff --git a/app/templates/admin/annotations_by_dataset.html b/app/templates/admin/annotations_by_dataset.html index 3885d7f..fd64673 100644 --- a/app/templates/admin/annotations_by_dataset.html +++ b/app/templates/admin/annotations_by_dataset.html @@ -13,7 +13,11 @@ {% block scripts %} {{ super() }} <script src="//d3js.org/d3.v5.min.js"></script> + {% if is_multi %} + <script src="{{ url_for('static', filename='js/makeChartMulti.js') }}"></script> + {% else %} <script src="{{ url_for('static', filename='js/makeChart.js') }}"></script> + {% endif %} <script>adminViewAnnotations( '#graph', {{ data.chart_data | tojson }}, diff --git a/app/templates/annotate/index.html b/app/templates/annotate/index.html index 61233a8..5360bf8 100644 --- a/app/templates/annotate/index.html +++ b/app/templates/annotate/index.html @@ -52,9 +52,13 @@ {% block scripts %} {{ super() }} <script src="//d3js.org/d3.v5.min.js"></script> - <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> + {% if is_multi %} + <script src="{{ url_for('static', filename='js/makeChartMulti.js') }}"></script> + {% else %} + <script src="{{ url_for('static', filename='js/makeChart.js') }}"></script> + {% endif %} <script>annotateChart("#graph", {{ data.chart_data | tojson }});</script> <script> // reset button diff --git a/app/templates/demo/evaluate.html b/app/templates/demo/evaluate.html index 225dade..3f20c94 100644 --- a/app/templates/demo/evaluate.html +++ b/app/templates/demo/evaluate.html @@ -31,20 +31,27 @@ {{ wtf.quick_form(form, button_map={'submit': 'primary'}) }} </div> </div> +{% endblock %} -<script src="//d3js.org/d3.v5.min.js"></script> -<script src="{{ url_for('static', filename='js/makeChart.js') }}"></script> -<script>viewAnnotations( - "#graph_user", - {{ data.chart_data | tojson }}, - {{ annotations_user | tojson }} -); -</script> -<script> - viewAnnotations( - "#graph_true", - {{ data.chart_data | tojson }}, - {{ annotations_true | tojson }} -); -</script> +{% block scripts %} + {{ super() }} + <script src="//d3js.org/d3.v5.min.js"></script> + {% if is_multi %} + <script src="{{ url_for('static', filename='js/makeChartMulti.js') }}"></script> + {% else %} + <script src="{{ url_for('static', filename='js/makeChart.js') }}"></script> + {% endif %} + <script>viewAnnotations( + "#graph_user", + {{ data.chart_data | tojson }}, + {{ annotations_user | tojson }} + ); + </script> + <script> + viewAnnotations( + "#graph_true", + {{ data.chart_data | tojson }}, + {{ annotations_true | tojson }} + ); + </script> {% endblock %} diff --git a/demo_data/demo_800.json b/demo_data/demo_800.json new file mode 100644 index 0000000..4151e74 --- /dev/null +++ b/demo_data/demo_800.json @@ -0,0 +1,224 @@ +{ + "name": "demo_800", + "n_obs": 100, + "n_dim": 2, + "demo": { + "true_CPs": [ + 65 + ] + }, + "series": [ + { + "label": "V1", + "type": "float", + "raw": [ + 2.4048337302746465, + 2.7168349605639315, + 0.9041074861194818, + 0.14437038954839643, + -0.5221323890548066, + -1.7599042604063269, + -2.548293050233459, + -2.208426298362067, + -1.256954097725121, + 1.2673541962415547, + 1.1733075975968035, + 2.572511100788756, + 4.835403025842459, + 4.5522544564588365, + 3.1314009643946106, + 2.9117530434659322, + 1.989596205887107, + 0.12295916283122152, + -1.572080765613325, + -1.4020154454558442, + -0.5797659111967578, + 1.3052476134525937, + 2.463483078997914, + 2.6208640977905917, + 5.276333136526055, + 6.189478034635046, + 4.749709400197241, + 4.332421204937167, + 3.5869975973229327, + 2.418661492344107, + -0.6660467926540834, + 0.558797974643422, + 0.42616473119351966, + 0.7669826506319701, + 1.9841902051028015, + 4.037621600143654, + 4.716794663084997, + 6.351483458807052, + 8.15722901100052, + 6.799628004587378, + 4.679336321650161, + 4.620601716252461, + 2.035199758371592, + 1.436517310229426, + 1.1046729586309985, + 1.7317671802316381, + 3.7804685922880745, + 4.904957692680897, + 6.577527584844899, + 7.772812515037654, + 8.7669639673688, + 7.7275133582092925, + 8.081106500414604, + 5.633331811986405, + 4.320656385064652, + 3.675975491302624, + 2.239294039901483, + 3.752799894422126, + 3.578790640480911, + 4.956034869436094, + 6.672164066165842, + 7.360081623700845, + 9.562433533804628, + 10.265674738007569, + 9.101981636121108, + -2.194041154997131, + -3.7012586157863887, + -7.653582763774726, + -9.292337495769667, + -8.902372732926226, + -9.767801399199508, + -8.340357720658075, + -5.0078952785839865, + -4.048857544065237, + -1.3177865542217697, + -0.9020124604731076, + -2.243740257406788, + -2.917482586327878, + -4.543662767507734, + -7.306029963030574, + -10.384296518475317, + -12.165420998763718, + -12.178529260880655, + -10.812463114033422, + -10.252447277392156, + -7.776749620293404, + -4.635861860775694, + -3.767672729617822, + -3.487478378396219, + -4.924371469527412, + -5.449262162042549, + -8.8927776405326, + -11.5780076846028, + -13.993805845864184, + -14.50818663568779, + -15.426098632186385, + -13.788291197384886, + -11.466791970497958, + -8.09824714195754, + -6.507185697165474 + ] + }, + { + "label": "V2", + "type": "float", + "raw": [ + -0.512144907328231, + 1.180877094568944, + 3.21495688649086, + 3.30100039164296, + 3.131495877850537, + 2.7083848725753303, + 0.7046621090030554, + -0.3722455317527476, + -2.763208097405329, + -2.851611407399316, + -1.7838053015227253, + -1.1835756790551795, + -0.05856749542015677, + 1.0602337342952541, + 2.5560931755237783, + 4.177640415008337, + 5.523261596773609, + 4.800082701701678, + 3.4350373458935746, + 1.2402178666145889, + 0.049773845309929565, + -1.1708812671512536, + -1.593035979997087, + -0.2755433654490299, + 0.6089266866075381, + 1.0410460807006094, + 5.082064906086399, + 5.505648973005408, + 7.120925673938372, + 5.483312226619, + 5.950385821097255, + 4.2473500772756445, + 2.297363185979542, + 0.015007388288893253, + 0.4463898055824315, + 0.38522786716705426, + 1.80126348142217, + 2.8821847137728254, + 4.781507667944923, + 5.425458351881453, + 6.146347067030369, + 7.202635814182365, + 6.337887131797799, + 5.616181429738069, + 4.970747236155149, + 2.8316763970841063, + 1.039352795004348, + 1.1794953229977527, + 2.1759611489770676, + 3.5479131243271196, + 3.8850902219360375, + 6.719505790505435, + 6.73812227273263, + 8.735255788221282, + 9.109738825694748, + 7.288225019795107, + 6.532489310182096, + 4.626223961087513, + 4.086838254635324, + 3.6636721899874596, + 3.396811467240758, + 3.332395646124288, + 6.04695253681218, + 7.7372895641498625, + 8.78651148176425, + 9.823884515175894, + 9.491162963215556, + 10.60522937026037, + 7.6503138685368945, + 7.060110792801475, + 5.377920287327379, + 4.471776085391286, + 4.873833216101378, + 3.5502975827744505, + 6.981666375706383, + 7.77963248562725, + 8.358288716655693, + 10.16088761127809, + 10.503949710979114, + 9.802832103790408, + 10.117476586020533, + 9.351456920006646, + 7.955556636697319, + 5.013496099614551, + 5.993411963284857, + 5.572772912761436, + 6.629075184090689, + 6.8058184040419105, + 9.35773531204678, + 10.718106089686358, + 10.635573210445399, + 11.818990011454368, + 11.103782047235141, + 10.911261978781269, + 10.683238595620933, + 8.45732816202584, + 6.884877092827968, + 6.826985186303127, + 5.915951292057806, + 7.975570042788154 + ] + } + ] +}
\ No newline at end of file |
