blob: 0840e1d3a4770d5c67f63aa674968a0574735fe8 [file] [log] [blame]
Michael Schuh6ad24692016-08-24 20:58:31 -07001"use strict";
2
3// Create anonymous namespace to avoid leaking these functions.
4(function() {
5// Convenience function that fetches a file and extracts the array buffer.
6function fetch_ArrayBuffer(url) { // returns Promise<ArrayBuffer>
7 return fetch(url).then(function(val) { return val.arrayBuffer(); });
8}
9
10// Two dimensional vector.
11class Point2d {
12 constructor(x, y) {
13 this.x = x;
14 this.y = y;
15 }
16 sub(o) { return new Point2d(this.x - o.x, this.y - o.y); }
17 add(o) { return new Point2d(this.x + o.x, this.y + o.y); }
18};
19
20// Drag event object for updating a Point2d class by reference.
21class PointDrag {
22 constructor(point) { // point : Point2d
23 this.point = point;
24 }
25 doDrag(delta) {
26 this.point.x += delta.x;
27 this.point.y += delta.y;
28 }
29 doMouseUp(e) { return null; }
30}
31
32// Takes in a canvas and adds the ability to click and drag the offset point.
33// scaleX and scaleY are allowed to be locked or not-locked as the scroll events
34// must be handled by the user of this class.
35//
36// offset.x and offset.y are defined in reference to the model-scale, not the viewport.
37// Thus to move the viewport you need to use offset.x * scaleX, etc.
38//
39// scaleX and scaleY aren't automatically applied to the rendering context because
40// differing x and y scales mess up the line width rendering.
41class ClickDrag {
42 constructor(canvas) {
43 canvas.addEventListener('selectstart', function(e) { e.preventDefault(); return false; }, false);
44 canvas.addEventListener('mousemove', this.handleMouseMove.bind(this), true);
45 canvas.addEventListener('mousedown', this.handleMouseDown.bind(this), true);
46 canvas.addEventListener('mouseup', this.handleMouseUp.bind(this), true);
47 this.canvas = canvas;
48 this.dragger = null;
49 this.offset = new Point2d(0, 0);
50 this.scaleX = 1.0;
51 this.scaleY = 1.0;
52 this.old_point = null;
53 this.on_redraw = null;
54 }
55 translateContext(ctx) { // ctx : CanvasRenderingContext2D
56 ctx.translate(this.offset.x * this.scaleX, this.offset.y * this.scaleY);
57 }
58 handleMouseMove(e) { // e : MouseEvent
59 let pos = new Point2d(e.offsetX / this.scaleX, e.offsetY / this.scaleY);
60 if (this.old_point !== null && this.dragger !== null) {
61 this.dragger.doDrag(pos.sub(this.old_point));
62 if (this.on_redraw !== null) { this.on_redraw(); }
63 }
64 this.old_point = pos;
65 }
66 get width() { return this.canvas.width; }
67 handleMouseDown(e) { // e : MouseEvent
68 this.dragger = new PointDrag(this.offset);
69 }
70 handleMouseUp(e) { // e : MouseEvent
71 if (this.dragger === null) { return; }
72 if (this.dragger.doMouseUp !== undefined) {
73 this.dragger = this.dragger.doMouseUp(e);
74 if (this.on_redraw !== null) { this.on_redraw(); }
75 }
76 }
77 multiplyScale(evt, dx, dy) {
78 let x_cur = evt.offsetX / this.scaleX - this.offset.x;
79 let y_cur = evt.offsetY / this.scaleY - this.offset.y;
80 this.scaleX *= dx;
81 this.scaleY *= dy;
82 this.offset.x = evt.offsetX / this.scaleX - x_cur;
83 this.offset.y = evt.offsetY / this.scaleY - y_cur;
84 if (this.on_redraw !== null) { this.on_redraw(); }
85 }
86};
87
88// This represents a particular downsampling of a timeseries dataset.
89// For the base level, data = min = max.
90// This is required
91class MipMapLevel {
92 // All arrays should be the same size.
93 constructor(data, min, max, scale) { // data : Float32Array, min : Float32Array, max : Float32Array, scale : Integer
94 this.data = data;
95 this.min = min;
96 this.max = max;
97 this.scale = scale;
98 }
99 get length() { return this.data.length; }
100 // Uses box filter to downsample. Guassian may be an improvement.
101 downsample() {
102 let new_length = (this.data.length / 2) | 0;
103 let data = new Float32Array(new_length);
104 let min = new Float32Array(new_length);
105 let max = new Float32Array(new_length);
106 for (let i = 0; i < new_length; i++) {
107 let i1 = i * 2;
108 let i2 = i1 + 1
109 data[i] = (this.data[i1] + this.data[i2]) / 2.0;
110 min[i] = Math.min(this.min[i1], this.min[i2]);
111 max[i] = Math.max(this.max[i1], this.max[i2]);
112 }
113 return new MipMapLevel(data, min, max, this.scale * 2);
114 }
115 static makeBaseLevel(data) { // data : Float32Array
116 return new MipMapLevel(data, data, data, 1);
117 }
118 clipv(v) { // v : Numeric
119 return Math.min(Math.max(v|0, 0), this.length - 1);
120 }
121 draw(ctx, clickDrag) { // ctx : CanvasRenderingContext2D, clickDrag : ClickDrag
122 let scalex = clickDrag.scaleX * this.scale;
123 let scaley = clickDrag.scaleY;
124 let stx = -clickDrag.offset.x / this.scale;
125 let edx = stx + clickDrag.width / scalex;
126 let sti = this.clipv(stx - 2);
127 let edi = this.clipv(edx + 2);
128 ctx.beginPath();
129 ctx.fillStyle="#00ff00";
130 ctx.moveTo(sti * scalex, scaley * this.min[sti]);
131 for (let i = sti + 1; i <= edi; i++) {
132 ctx.lineTo(i * scalex, scaley * this.min[i]);
133 }
134 for (let i = edi; i >= sti; --i) {
135 ctx.lineTo(i * scalex, scaley * this.max[i]);
136 }
137 ctx.fill();
138 ctx.closePath();
139
140 ctx.beginPath();
141 ctx.strokeStyle="#008f00";
142 ctx.moveTo(sti * scalex, scaley * this.data[sti]);
143 for (let i = sti + 1; i <= edi; i++) {
144 ctx.lineTo(i * scalex, scaley * this.data[i]);
145 }
146 ctx.stroke();
147 ctx.closePath();
148 }
149};
150
151// This class represents all downsampled data levels.
152// This must only be used for descrite time-series data.
153class MipMapper {
154 constructor(samples) { // samples : Float32Array
155 this.levels = [];
156 let level = MipMapLevel.makeBaseLevel(samples);
157 this.levels.push(level);
158 while (level.length > 2) {
159 level = level.downsample();
160 this.levels.push(level);
161 }
162 }
163 // Find the level such that samples are spaced at most 2 pixels apart
164 getLevel(scale) { // scale : Float
165 let level = this.levels[0];
166 for (let i = 0; i < this.levels.length; ++i) {
167 // Someone who understands nyquist could probably improve this.
168 if (this.levels[i].scale * scale <= 4.0) {
169 level = this.levels[i];
170 }
171 }
172 return level;
173 }
174}
175
176// Custom HTML element for x-responsive-plotter.
177// This should be the main entry-point for this code.
178class ResponsivePlotter extends HTMLElement {
179 createdCallback() {
180 this.data = null;
181 this.canvas = document.createElement("canvas");
182 this.canvas.style["background"] = "inherit";
183 this.canvas.addEventListener("mousewheel", this.doScroll.bind(this), false);
184 this.clickDrag = new ClickDrag(this.canvas);
185 this.clickDrag.on_redraw = this.doRedraw.bind(this);
186 this.clickDrag.offset.y = 160; //320;
187 this.updateInnerData();
188 this.appendChild(this.canvas);
189 }
190 doScroll(e) {
191 let factor = (e.wheelDelta < 0) ? 0.9 : 1.1;
192 this.clickDrag.multiplyScale(e, e.shiftKey ? 1.0 : factor, e.shiftKey ? factor : 1.0);
193 e.preventDefault();
194 }
195 doRedraw() {
196 this.redraw();
197 }
198 updateInnerData() {
199 this.canvas.width = this.getAttribute("width") || 640;
200 this.canvas.height = this.getAttribute("height") || 480;
201 fetch_ArrayBuffer(this.getAttribute("data")).then(this.dataUpdated.bind(this));
202 this.redraw();
203 }
204 attributeChangedCallback(attrName, oldVal, newVal) {
205 if (attrName == "width") {
206 this.canvas.width = this.getAttribute("width") || 640;
207 this.redraw();
208 } else if (attrName == "height") {
209 this.canvas.width = this.getAttribute("height") || 480;
210 this.redraw();
211 } else if (attrName == "data") {
212 // Should this clear out the canvas if a different url is selected?
213 // Well it does.
214 this.data = null;
215 fetch_ArrayBuffer(this.getAttribute("data")).then(this.dataUpdated.bind(this));
216 }
217 }
218 dataUpdated(data) { // data : ArrayBuffer
219 this.data = new MipMapper(new Float32Array(data));
220 this.redraw();
221 }
222 redraw() {
223 let ctx = this.canvas.getContext('2d');
224 ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
225 ctx.save();
226 this.clickDrag.translateContext(ctx);
227 if (this.data !== null) {
228 let level = this.data.getLevel(this.clickDrag.scaleX);
229 level.draw(ctx, this.clickDrag);
230 }
231 ctx.restore();
232 }
233};
234
235
236// TODO(parker): This is chrome-specific v0 version of this switch to v1 when available.
237document.registerElement('x-responsive-plotter', ResponsivePlotter);
238})();