| <!-- The <plot-simple-sk> custom element declaration. |
| |
| A custom element for plotting x,y graphs. |
| |
| Attributes: |
| width - The width of the element in px. |
| |
| height - The height of the element in px. |
| |
| nocrosshair - If true then the mouse following croshair is hidden. |
| |
| specialevents - If true then special lines emit events also. |
| |
| Events: |
| trace_selected - Event produced when the user clicks on a line. |
| The e.detail contains the id of the line and the index of the |
| point in the line closest to the mouse, and the [x, y] value |
| of the point in 'pt'. |
| |
| e.detail = { |
| id: "id of trace", |
| index: 3, |
| pt: [2, 34.5], |
| } |
| |
| trace_focused - Event produced when the user moves the mouse close |
| to a line. The e.detail contains the id of the line and the index of the |
| point in the line closest to the mouse. |
| |
| e.detail = { |
| id: "id of trace", |
| index: 3, |
| pt: [2, 34.5], |
| } |
| |
| zoom - Event produced when the user has zoomed into a region |
| by dragging. |
| |
| Methods: |
| addLines(lines) - Add lines to the plot, where lines is an object that |
| maps the line id to an array of [x, y] points. For example: |
| |
| var lines = { |
| foo: [ |
| [0.1, 3.7], |
| [0.2, 3.8], |
| [0.4, 3.0], |
| ], |
| bar: [ |
| [0.0, 2.5], |
| [0.2, 4.2], |
| [0.5, 3.9], |
| ], |
| }; |
| plot.addLines(lines); |
| |
| Any line id that begins with "special" will be treated specially, |
| i.e. it will be presented as a dashed black line that doesn't |
| generate events. This may be useful for adding a line at y=0, |
| or a reference trace. |
| |
| deleteLine(id) - Removes the line with the given id from the plot. |
| If no line with that id exists then the function returns without |
| any action. |
| |
| removeAll() - Removes all lines from the plot. |
| |
| setHighlight(ids) - Highlights all the lines that have an id that |
| appears in the array 'ids'. |
| |
| plot.setHighlight(["foo", "bar"]) |
| |
| clearHighlight() - Removes highlighting from all lines. |
| |
| highlighted() - Returns the ids of all the lines that are highlighted. |
| |
| setXBar(x) - Places a distinct vertical bar at the given location |
| on the x-axis. There can be at most only one xbar placed at any |
| one time. |
| |
| clearXBar() - Removes the vertical bar placed by setXBar(). |
| |
| setBanding(bands) - Set a background highlight for the given x ranges |
| given in 'bands'. |
| |
| var bands = [ |
| [0.0, 0.1], |
| [0.5, 1.2], |
| ]; |
| plot.setBanding(bands); |
| |
| setTicks(ticks) - Sets the tick mark locations and values. If not called |
| then uniformly spaces tick marks in the x axis units are displayed. |
| The value of ticks is an object that maps x axis values to their display |
| value. |
| |
| var ticks = { |
| 1: "5th", |
| 10: "6th", |
| 25: "7th", |
| }; |
| |
| resetAxes() - Resets the axes back after the user has zoomed. |
| |
| --> |
| <link rel="import" href="/res/imp/bower_components/paper-input/paper-input.html"> |
| <script src="/res/imp/bower_components/d3/d3.min.js"></script> |
| |
| <!-- |
| Styles for the svg elements used in plot-simple-sk are kept in a separate |
| CSS file since we are using d3 to manage those elements and not the |
| Polymer.dom() functions, so the whole magic CSS scoping stuff doesn't |
| work, so we just prefix every rule with plot-simple-sk. |
| --> |
| <link rel="stylesheet" href="plot-simple.css"> |
| |
| <dom-module id="plot-simple-sk"> |
| <template> |
| <svg id="svg" width$="{{width}}" height$="{{height}}" |
| class$="{{noCrosshair(nocrosshair)}}"> |
| <defs> |
| <clipPath id="clip"> |
| <rect x="0" y="0" width="1000" height="500" /> |
| </clipPath> |
| </defs> |
| </svg> |
| </template> |
| </dom-module> |
| |
| <script> |
| (function () { |
| var SPECIAL = "special"; |
| |
| var colors = [ |
| "#000000", |
| "#1B9E77", |
| "#D95F02", |
| "#7570B3", |
| "#E7298A", |
| "#66A61E", |
| "#E6AB02", |
| "#A6761D", |
| "#666666", |
| ]; |
| |
| var margins = { |
| top: 20, |
| right: 10, |
| bottom: 20, |
| left: 80 |
| }; |
| |
| Polymer({ |
| is: "plot-simple-sk", |
| |
| properties: { |
| width: { |
| type: Number, |
| value: 1000, |
| reflectToAttribute: true, |
| observer: "_calcSize", |
| }, |
| height: { |
| type: Number, |
| value: 500, |
| reflectToAttribute: true, |
| observer: "_calcSize", |
| }, |
| nocrosshair: { |
| type: Boolean, |
| value: false, |
| reflectToAttribute: true, |
| }, |
| specialevents: { |
| type: Boolean, |
| value: false, |
| }, |
| }, |
| |
| ready: function() { |
| // The last reported position of the mouse, in plot and native |
| // coordinates. |
| this._mouse = { |
| x: 0, |
| y: 0, |
| x_native: 0, |
| y_native: 0, |
| }; |
| |
| // The closes line to the mouse, calculated after every mouse move |
| // event. |
| this._closest = { |
| d: 1e20, |
| trace: -1, |
| index: -1, |
| value: 0, |
| }; |
| |
| // The location of the XBar. See setXBar(). |
| this._xbarx = 0; |
| |
| // The locations of the background bands. See setBanding(). |
| this._bands = []; |
| |
| // The values of the custom x-axis tick marks, if set. See setTicks(). |
| this._ticks = {}; |
| |
| this._lineData = [ ]; |
| this._initialSetup(); |
| this.resetAxes(); |
| this._plot(); |
| }, |
| |
| // lines is a map to the line data. |
| addLines: function(lines) { |
| Object.keys(lines).forEach(function(id) { |
| var line = lines[id]; |
| line._id = id; |
| line._hash = (sk.hashString(id) % 8) + 1; |
| if (id.startsWith(SPECIAL)) { |
| line._hash = 0; |
| } |
| this._lineData.push(line); |
| }.bind(this)); |
| this.resetAxes(); |
| this._plot(); |
| }, |
| |
| deleteLine: function(id) { |
| // First clear all the lines, but backup _lineData for later use. |
| var backup = this._lineData; |
| this.removeAll(); |
| // Then re-add all the lines except the one we want deleted. |
| for (var i = backup.length - 1; i >= 0; i--) { |
| if (backup[i]._id != id) { |
| this._lineData.push(backup[i]); |
| } |
| } |
| this._plot(); |
| }, |
| |
| removeAll: function() { |
| this._lineData = []; |
| this._plot(); |
| }, |
| |
| setHighlight: function(ids) { |
| $$('.trace').forEach(function(ele) { |
| if (ids.indexOf(ele.getAttribute('data-id')) != -1) { |
| ele.classList.add('highlight'); |
| } |
| }); |
| }, |
| |
| highlighted: function() { |
| var ids = []; |
| $$('.trace.highlight').forEach(function(ele) { |
| ids.push(ele.getAttribute('data-id')); |
| }); |
| return ids; |
| }, |
| |
| clearHighlight: function() { |
| this._svg.selectAll(".trace") |
| .classed('highlight', false); |
| }, |
| |
| setXBar: function(x) { |
| this._xbarx = x; |
| this._xbar |
| .attr('x1', this._xRange(x)) |
| .attr('x2', this._xRange(x)) |
| .classed('displayed', true); |
| }, |
| |
| clearXBar: function(x) { |
| this._xbar |
| .classed('displayed', false); |
| }, |
| |
| setBanding: function(bands) { |
| this._bands = bands; |
| this._plot(); |
| }, |
| |
| setTicks: function(ticks) { |
| var values = Object.keys(ticks).sort(); |
| this._ticks = ticks; |
| |
| this._xAxis |
| .tickValues(values) |
| .tickFormat(function(d) { return this._ticks[d]; }.bind(this)); |
| |
| this._plot(); |
| }, |
| |
| resetAxes: function() { |
| if (!this._lineData || !this._xRange) { |
| return |
| } |
| xMin = d3.min(this._lineData, function (d) { return d3.min(d, function (d) { return d[0]; }) }); |
| xMax = d3.max(this._lineData, function (d) { return d3.max(d, function (d) { return d[0]; }) }); |
| yMin = d3.min(this._lineData, function (d) { return d3.min(d, function (d) { return d[1]; }) }); |
| yMax = d3.max(this._lineData, function (d) { return d3.max(d, function (d) { return d[1]; }) }); |
| var xMargin = Math.abs(xMax - xMin)*0.02; |
| var yMargin = Math.abs(yMax - yMin)*0.02; |
| this._xRange.domain([xMin-xMargin, xMax+xMargin]); |
| this._yRange.domain([yMin-yMargin, yMax+yMargin]); |
| this._plot(); |
| }, |
| |
| // Create an event from this._closest of the given name. |
| _eventFromClosest: function(name) { |
| var id = this._lineData[this._closest.trace]._id; |
| if (!this.specialevents && id.startsWith(SPECIAL)) { |
| return |
| } |
| var detail = { |
| id: id, |
| index: this._closest.index, |
| value: this._closest.value, |
| pt: this._closest.pt, |
| } |
| this.dispatchEvent(new CustomEvent(name, {detail: detail, bubbles: true})); |
| }, |
| |
| _click: function() { |
| if (this._closest.trace == -1) { |
| return |
| } |
| this._eventFromClosest("trace_selected"); |
| }, |
| |
| _move: function(p) { |
| this._mouse.x_native = p[0] - margins.left; |
| this._mouse.y_native = p[1] - margins.top; |
| |
| // Only do the minimal work in _move, schedule the rest of the work |
| // to be done later in _mouseMoveWork. |
| window.setTimeout(this._mouseMoveWork.bind(this), 1); |
| }, |
| |
| // Calculates new this._closest value based on the mouse position. |
| // Also updates the 'focused' status of lines and dots. |
| _mouseMoveWork: function() { |
| this._mouse.x = this._xRange.invert(this._mouse.x_native); |
| this._mouse.y = this._yRange.invert(this._mouse.y_native); |
| |
| this._vhair.attr('x1', this._mouse.x_native); |
| this._vhair.attr('x2', this._mouse.x_native); |
| this._hhair.attr('y1', this._mouse.y_native); |
| this._hhair.attr('y2', this._mouse.y_native); |
| |
| var oldTrace = this._closest.trace; |
| var oldCommit = this._closest.index; |
| |
| this._closest = { |
| d: 1e20, |
| trace: -1, |
| index: -1, |
| value: 0, |
| }; |
| // We are going to calculate the distance between all the points and |
| // the mouse. For that to work we'll normalize all the x and y deltas |
| // so they scale from 0 to 1.0. |
| var scalex = Math.abs(xMax - xMin); |
| var scaley = Math.abs(yMax - yMin); |
| for (var i = this._lineData.length - 1; i >= 0; i--) { |
| var atrace = this._lineData[i]; |
| for (var j = atrace.length - 1; j >= 0; j--) { |
| var pt = atrace[j]; |
| var d = Math.pow((pt[0] - this._mouse.x)/scalex, 2) + Math.pow((pt[1] - this._mouse.y)/scaley, 2); |
| if (d < this._closest.d) { |
| this._closest.d = d; |
| this._closest.trace = i; |
| this._closest.index= j; |
| this._closest.pt = pt; |
| this._closest.value= atrace[j][1]; |
| } |
| } |
| } |
| if (oldTrace != this._closest.trace || oldCommit != this._closest.index) { |
| this._svg.selectAll(".trace") |
| .classed('focused', false); |
| this._svg.selectAll(".dot") |
| .classed('focused', false); |
| if (this._closest.trace >= 0) { |
| this._svg.select("#trace_"+this._closest.trace) |
| .classed('focused', true); |
| this._svg.select("#trace_"+this._closest.trace + " .c"+this._closest.index) |
| .classed('focused', true); |
| } |
| |
| if (this._closest.trace != -1) { |
| this._eventFromClosest("trace_focused"); |
| } |
| } |
| }, |
| |
| _calcSize: function() { |
| if (!this.height || !this.width || !this._lineData) { |
| return; |
| } |
| var ele = this.$.svg; |
| this._outerWidth = ele.width.baseVal.value; |
| this._outerHeight = ele.height.baseVal.value; |
| this._width = this._outerWidth - margins.left - margins.right; |
| this._height = this._outerHeight - margins.top - margins.bottom; |
| d3.select(ele).select('#clip rect') |
| .attr('width', this._width) |
| .attr('height', this._height); |
| var xMin = d3.min(this._lineData, function (d) { return d3.min(d, function (d) { return d[0]; }) }); |
| var xMax = d3.max(this._lineData, function (d) { return d3.max(d, function (d) { return d[0]; }) }); |
| var yMin = d3.min(this._lineData, function (d) { return d3.min(d, function (d) { return d[1]; }) }); |
| var yMax = d3.max(this._lineData, function (d) { return d3.max(d, function (d) { return d[1]; }) }); |
| |
| this._xRange = d3.scale |
| .linear() |
| .range([0, this._width]) |
| .domain([xMin, xMax]); |
| |
| this._yRange = d3.scale |
| .linear() |
| .range([this._height, 0]) |
| .domain([yMin, yMax]); |
| |
| this._lineFunc = d3.svg.line() |
| .x(function (d) { return this._xRange(d[0]); }.bind(this)) |
| .y(function (d) { return this._yRange(d[1]); }.bind(this)) |
| .interpolate('linear'); |
| |
| this._xAxis = d3.svg.axis() |
| .scale(this._xRange) |
| .tickSize(5) |
| .tickSubdivide(true); |
| |
| this._yAxis = d3.svg.axis() |
| .scale(this._yRange) |
| .tickSize(5) |
| .orient("left") |
| .tickSubdivide(true); |
| |
| if (this._hhair) { |
| this._hhair.attr('x2', this._width); |
| } |
| |
| this.setTicks(this._ticks); |
| |
| this.resetAxes(); |
| }, |
| |
| // Creates the initial set of SVG elements. |
| _initialSetup: function() { |
| var ele = this.$.svg; |
| this._calcSize(); |
| |
| this._svg = d3.select(ele) |
| .append("g") |
| .attr("class", "top") |
| .attr("transform", "translate(" + margins.left + "," + margins.top + ")"); |
| |
| d3.select(ele) |
| .on("click", this._click.bind(this)) |
| .on("mousedown", this._mouseDown.bind(this)) |
| .on("mousemove", this._mouseMove.bind(this)) |
| .on("mouseup", this._mouseUp.bind(this)); |
| |
| this._svg.append("g") |
| .attr("class", "x axis") |
| .attr("transform", "translate(0," + this._height + ")") |
| .call(this._xAxis); |
| |
| this._svg.append("g") |
| .attr("class", "y axis") |
| .call(this._yAxis); |
| |
| // Add the crosshair. |
| this._hhair = this._svg.append("line") |
| .attr('x1', 0) |
| .attr('y1', 0) |
| .attr('x2', this._width) |
| .attr('y2', 0) |
| .attr('class', 'xhair'); |
| |
| this._vhair = this._svg.append("line") |
| .attr('x1', 0) |
| .attr('y1', 0) |
| .attr('x2', 0) |
| .attr('y2', this._height) |
| .attr('class', 'xhair'); |
| |
| this._xbar = this._svg.append("line") |
| .attr('x1', 0) |
| .attr('y1', 0) |
| .attr('x2', 0) |
| .attr('y2', this._height) |
| .attr('class', 'xbar'); |
| |
| this._vis = this._svg.append("g"); |
| }, |
| |
| |
| // Update the date displayed on the plot. |
| _plot: function() { |
| if (!this._vis) { |
| return |
| } |
| var trace = this._vis.selectAll(".trace") |
| .data(this._lineData); |
| |
| trace.enter().append("g") |
| .attr("class", function(d) { |
| var ret = "trace"; |
| if (d._id && d._id.startsWith(SPECIAL)) { |
| ret += " special"; |
| } |
| return ret |
| }) |
| .attr("clip-path", "url(#clip)") |
| .attr("stroke", function(d) { return colors[d._hash]; }) |
| .attr("id", function(d, i) { return "trace_" + i; }) |
| .attr("data-id", function(d) { return d._id; }) |
| .append("path") |
| .attr("stroke-width", 2) |
| .attr("fill", "none"); |
| |
| trace.exit().remove(); |
| |
| trace.selectAll("path") |
| .attr("d", function(d) { return this._lineFunc(d); }.bind(this)); |
| |
| var dots = trace.selectAll(".dot") |
| .data(function(d) { return d; }); |
| |
| dots.enter().append("circle") |
| .attr("class", function(d, i) { return "c" + i + " dot"; }) |
| .attr("r", 3.5); |
| |
| dots.exit().remove(); |
| |
| dots |
| .attr("cx", this._lineFunc.x()) |
| .attr("cy", this._lineFunc.y()); |
| |
| |
| var bands = this._vis.selectAll(".band") |
| .data(this._bands); |
| |
| bands.enter().append("rect") |
| .attr("class", "band") |
| |
| bands.exit().remove(); |
| |
| bands |
| .attr("x", function(d) { return this._xRange(d[0]); }.bind(this)) |
| .attr("y", 0) |
| .attr("width", function(d) { return this._xRange(d[1]) - this._xRange(d[0]); }.bind(this)) |
| .attr("height", function() { return this._height; }.bind(this)) |
| |
| if (this._xbar && this._xbarx && this._xRange(this._xbarx) ) { |
| this._xbar |
| .attr('x1', this._xRange(this._xbarx)) |
| .attr('x2', this._xRange(this._xbarx)) |
| } |
| |
| this._svg.select(".x.axis").call(this._xAxis); |
| this._svg.select(".y.axis").call(this._yAxis); |
| }, |
| |
| _mouseDown: function() { |
| var p = d3.mouse(this.$.svg); |
| |
| this._svg.append("rect") |
| .attr({ |
| rx : 6, |
| ry : 6, |
| class : "selectionrect", |
| x : p[0] - margins.left, |
| y : p[1] - margins.top, |
| width : 0, |
| height: 0, |
| }); |
| }, |
| |
| _mouseMove: function() { |
| var s = this._svg.select(".selectionrect"); |
| |
| if (s.empty()) { |
| this._move(d3.mouse(this.$.svg)); |
| return |
| } |
| |
| var p = d3.mouse(this.$.svg); |
| p[0] = p[0] - margins.left; |
| p[1] = p[1] - margins.top; |
| var d = { |
| x : parseInt(s.attr("x"), 10), |
| y : parseInt(s.attr("y"), 10), |
| width : parseInt(s.attr("width"), 10), |
| height: parseInt(s.attr("height"), 10) |
| }; |
| var dx = p[0] - d.x; |
| var dy = p[1]- d.y; |
| |
| if (dx < 1) { |
| d.x = p[0]; |
| d.width -= dx; |
| } else { |
| d.width = dx; |
| } |
| |
| if (dy < 1) { |
| d.y = p[1]; |
| d.height -= dy; |
| } else { |
| d.height = dy; |
| } |
| |
| s.attr(d); |
| }, |
| |
| _mouseUp: function() { |
| var r = this._svg.select(".selectionrect")[0][0]; |
| if (!r) { |
| return |
| } |
| var xMin = this._xRange.invert(r.x.baseVal.value); |
| var xMax = this._xRange.invert(r.x.baseVal.value + r.width.baseVal.value); |
| var yMax = this._yRange.invert(r.y.baseVal.value); |
| var yMin = this._yRange.invert(r.y.baseVal.value + r.height.baseVal.value); |
| this._svg.selectAll(".selectionrect").remove(); |
| |
| if (r.width.baseVal.value < 5 || r.height.baseVal.value < 5) { |
| return |
| } |
| |
| this._xRange.domain([xMin, xMax]); |
| this._yRange.domain([yMin, yMax]); |
| this._plot(); |
| var detail = { |
| xMin: xMin, |
| xMax: xMax, |
| yMin: yMin, |
| yMax: yMax, |
| }; |
| this.dispatchEvent(new CustomEvent("zoom", {detail: detail, bubbles: true})); |
| }, |
| |
| noCrosshair: function(nocrosshair) { |
| return nocrosshair ? 'nocrosshair' : ''; |
| }, |
| |
| }); |
| })(); |
| </script> |