diff --git a/js/m.js b/js/m.js index 9a35a84..312ffdf 100644 --- a/js/m.js +++ b/js/m.js @@ -1120,6 +1120,7 @@ count = Math.min(src.length, count); let adjust = 0; + var decayFactor = 1 - Math.exp(-decay); for (var i = 0; i < count; i++) { var s = i * sstride + sstart; var d = i * dstride + dstart; @@ -1129,7 +1130,7 @@ if (d >= dest.length) { break; } - adjust = (src[s] - dest[d]) * (1 - Math.exp(-decay)); + adjust = (src[s] - dest[d]) * decayFactor; dest[d] = Number.isNaN(dest[d] + adjust) ? src[s] : dest[d] + adjust; dest[d] = Math.max(dest[d], src[s]); } diff --git a/js/mx.js b/js/mx.js index 19807d7..4fa8def 100644 --- a/js/mx.js +++ b/js/mx.js @@ -48,6 +48,22 @@ var CanvasInput = require("./CanvasInput"); var m = require("./m"); + var DASHED_STYLE = {mode: "dashed", on: 4, off: 4}; + + // Cached getContext helper to avoid repeated lookups in hot paths + var _ctxCache = typeof WeakMap !== 'undefined' ? new WeakMap() : null; + function getCachedCtx(canvas) { + if (_ctxCache) { + var ctx = _ctxCache.get(canvas); + if (!ctx) { + ctx = canvas.getContext("2d"); + _ctxCache.set(canvas, ctx); + } + return ctx; + } + return canvas.getContext("2d"); + } + function mx() {} mx.DomMenu = require("./mx.dommenu"); @@ -684,7 +700,7 @@ * @private */ mx.linear_gradient = function(Mx, x, y, w, h, fillStyle) { - var ctx = Mx.active_canvas.getContext("2d"); + var ctx = getCachedCtx(Mx.active_canvas); var step_size = 1.0 / fillStyle.length; var lingrad = ctx.createLinearGradient(x, y, w, h); for (var i = 0; i < fillStyle.length - 1; i++) { @@ -1213,7 +1229,7 @@ // ~= MX$DRAW_SYMBOL // mx.draw_symbol = function(Mx, ic, x, y, symbol, rr, n) { - var ctx = Mx.active_canvas.getContext("2d"); + var ctx = getCachedCtx(Mx.active_canvas); var r = 0; // int var d = 0; // int @@ -1464,11 +1480,7 @@ var style; if (options.dashed) { - style = { - mode: "dashed", - on: 4, - off: 4 - }; + style = DASHED_STYLE; } var stk4 = mx.origin(Mx.origin, 4, Mx.stk[Mx.level]); @@ -1521,8 +1533,13 @@ // if all points are on screen, then we will will need 'n' points // if all points are off the screen, then we will need (2*n)-2 var bufsize = 4 * Math.ceil(2 * xpoint.length); - var pixx = new Int32Array(new ArrayBuffer(bufsize)); - var pixy = new Int32Array(new ArrayBuffer(bufsize)); + if (!Mx._traceBufferSize || Mx._traceBufferSize < bufsize) { + Mx._traceBufferSize = bufsize; + Mx._tracePixx = new Int32Array(new ArrayBuffer(bufsize)); + Mx._tracePixy = new Int32Array(new ArrayBuffer(bufsize)); + } + var pixx = Mx._tracePixx; + var pixy = Mx._tracePixy; var ib = 0; if ((line === 0) && (symb !== 0)) { @@ -1666,6 +1683,7 @@ var ie = 0; var visible = false; + var o = {tL: 1.0, tE: 0.0}; for (var n = skip; n <= (skip * (npts - 1)); n += skip) { var lx = x; @@ -1688,10 +1706,8 @@ dx = lx - x; dy = ly - y; if ((dx !== 0.0) || (dy !== 0.0)) { - var o = { - tL: 1.0, - tE: 0.0 - }; + o.tL = 1.0; + o.tE = 0.0; // Between the last point and the current point, // determine the ratio of the x and y porionts // that intersects the border. If clipt returns @@ -1775,8 +1791,7 @@ var x_start = highlight.xstart; var x_end = highlight.xend; - console.log("x start ", x_start); - console.log("x end ", x_end); + if (x_start >= Mx.stk[Mx.level].xmax) { continue; @@ -1793,24 +1808,27 @@ var pi_start = xstart_pixel_value.x; var pi_end = xend_pixel_value.x; //console.log('start: ', pi_start, 'end: ', pi_end); - var pixx_new = []; - var pixy_new = []; + var fillCount = 0; + if (!Mx._highlightPixx || Mx._highlightPixx.length < ib) { + Mx._highlightPixx = new Int32Array(ib); + Mx._highlightPixy = new Int32Array(ib); + } + var pixx_new = Mx._highlightPixx; + var pixy_new = Mx._highlightPixy; for (var q = 0; q < ib; q++) { var this_point = pixx[q]; var this_point_y = pixy[q]; - //console.log(this_point); if (in_fill_range(this_point, pi_start, pi_end) === true) { - //console.log('in range: ', this_point); - pixx_new.push(this_point); - pixy_new.push(this_point_y); - + pixx_new[fillCount] = this_point; + pixy_new[fillCount] = this_point_y; + fillCount++; } } - if ((pixx_new.length > 0) || (wn !== 0)) { + if ((fillCount > 0) || (wn !== 0)) { pi_start = Math.max(pi_start, pixx_new[0]); - pi_end = Math.min(pi_end, pixx_new[pixx_new.length - 1]); - mx.fill_trace(Mx, highlight.fill, pixx_new, pixy_new, pixx_new.length, pi_start, pi_end); + pi_end = Math.min(pi_end, pixx_new[fillCount - 1]); + mx.fill_trace(Mx, highlight.fill, pixx_new, pixy_new, fillCount, pi_start, pi_end); } } @@ -1853,7 +1871,7 @@ // ~= MX$DRAW_LINES // mx.draw_line = function(Mx, color, x1, y1, x2, y2, linewidth, style) { - var ctx = Mx.active_canvas.getContext("2d"); + var ctx = getCachedCtx(Mx.active_canvas); if (linewidth === undefined) { linewidth = Mx.linewidth; } @@ -1891,7 +1909,7 @@ // ~= MX$RUBBERLINE // mx.rubberline = function(Mx, x1, y1, x2, y2) { - var ctx = Mx.active_canvas.getContext("2d"); + var ctx = getCachedCtx(Mx.active_canvas); draw_line(ctx, x1, y1, x2, y2, { mode: "xor" }, "white", 1); @@ -1906,7 +1924,7 @@ * @private */ mx.fill_trace = function(Mx, fillStyle, pixx, pixy, npts, l, r) { - var ctx = Mx.active_canvas.getContext("2d"); + var ctx = getCachedCtx(Mx.active_canvas); if (Array.isArray(fillStyle)) { ctx.fillStyle = mx.linear_gradient(Mx, 0, 0, 0, Mx.b - Mx.t, fillStyle); } else { @@ -1973,7 +1991,7 @@ // ~= MX$DRAW_LINES // mx.draw_lines = function(Mx, colors, pixx, pixy, npts, linewidth, style) { - var ctx = Mx.active_canvas.getContext("2d"); + var ctx = getCachedCtx(Mx.active_canvas); if (npts < 1) { return; @@ -2079,7 +2097,7 @@ // ~= MX$CLIP // mx.clip = function(Mx, left, top, width, height) { - var ctx = Mx.active_canvas.getContext("2d"); + var ctx = getCachedCtx(Mx.active_canvas); if ((left === 0) && (top === 0) && (width === 0) && (height === 0)) { ctx.restore(); @@ -2098,7 +2116,7 @@ // ~= MX$CLEAR_WINDOW // mx.clear_window = function(Mx) { - var ctx = Mx.active_canvas.getContext("2d"); + var ctx = getCachedCtx(Mx.active_canvas); ctx.fillStyle = Mx.bg; ctx.fillRect(0, 0, Mx.width, Mx.height); @@ -2109,7 +2127,7 @@ * @private */ mx.erase_window = function(Mx) { - var ctx = Mx.active_canvas.getContext("2d"); + var ctx = getCachedCtx(Mx.active_canvas); ctx.clearRect(0, 0, Mx.width, Mx.height); }; @@ -2286,7 +2304,7 @@ // TODO Validation - make sure promptText is not too long and isn't multi-line... mx.onWidgetLayer(Mx, function() { - var ctx = Mx.active_canvas.getContext("2d"); + var ctx = getCachedCtx(Mx.active_canvas); var maxNumChars = 30; // Construct the input box @@ -2630,7 +2648,7 @@ * @param {Number} radius The corner radius. Defaults to 5; */ mx.draw_round_box = function(Mx, color, x, y, w, h, fill_opacity, fill_color, radius) { - var ctx = Mx.active_canvas.getContext("2d"); + var ctx = getCachedCtx(Mx.active_canvas); if (!radius) { radius = 5; @@ -2679,7 +2697,7 @@ // ~= MX$DRAW_BOX // mx.draw_box = function(Mx, color, x, y, w, h, fill_opacity, fill_color) { - var ctx = Mx.active_canvas.getContext("2d"); + var ctx = getCachedCtx(Mx.active_canvas); if (color !== "xor") { ctx.lineWidth = 1; @@ -2701,7 +2719,7 @@ // For now assume xor always uses the base canvas // even if it draws on another canvas - var dctx = Mx.canvas.getContext("2d"); + var dctx = getCachedCtx(Mx.canvas); var imgd = dctx.getImageData(x, y, w, 1); var pix = imgd.data; @@ -2764,8 +2782,8 @@ */ // ~= MX$SETFONT mx.set_font = function(Mx, width) { - var ctx = Mx.canvas.getContext("2d"); - var ctx_wid = Mx.wid_canvas.getContext("2d"); + var ctx = getCachedCtx(Mx.canvas); + var ctx_wid = getCachedCtx(Mx.wid_canvas); if ((Mx.font) && (Mx.font.width === width)) { // use the cached font @@ -2800,7 +2818,7 @@ */ // ~= MX$FTEXTLINE mx.textline = function(Mx, xstart, ystart, xend, yend, style) { - var ctx = Mx.active_canvas.getContext("2d"); + var ctx = getCachedCtx(Mx.active_canvas); if (!style) { style = {}; } @@ -2960,7 +2978,7 @@ height = iscb - isct - 4; } - var ctx = Mx.active_canvas.getContext("2d"); + var ctx = getCachedCtx(Mx.active_canvas); if (flags.fillStyle) { if (Array.isArray(flags.fillStyle)) { ctx.fillStyle = mx.linear_gradient(Mx, 0, 0, 0, iscb - isct, flags.fillStyle); @@ -3381,7 +3399,7 @@ //ctx.fillStyle = xwlo; //ctx.fillRect(xcc, ycc, xss, yss); - var ctx = Mx.wid_canvas.getContext("2d"); + var ctx = getCachedCtx(Mx.wid_canvas); ctx.lineWidth = 1; ctx.strokeStyle = Mx.xwbs; // xwbs @@ -3803,7 +3821,7 @@ mx.text(Mx, xt, yt, name, Mx.xwfg); } if (inw > 0 && inh > 0) { - var ctx = Mx.active_canvas.getContext("2d"); + var ctx = getCachedCtx(Mx.active_canvas); if (mx.LEGACY_RENDER) { ctx.fillStyle = Mx.bg; ctx.fillRect(inx, iny, inw, inh); @@ -3829,7 +3847,7 @@ // ~= MX$TEXT // mx.text = function(Mx, x, y, lbl, color) { - var ctx = Mx.active_canvas.getContext("2d"); + var ctx = getCachedCtx(Mx.active_canvas); x = Math.max(0, x); y = Math.max(0, y); @@ -4274,7 +4292,7 @@ function display_warpbox(Mx) { Mx._animationFrameHandle = undefined; var warpbox = Mx.warpbox; - var ctx = Mx.active_canvas.getContext("2d"); + var ctx = getCachedCtx(Mx.active_canvas); if (!warpbox) { return; @@ -4552,7 +4570,7 @@ } pix[7] = pix[3]; - var ctx = Mx.active_canvas.getContext("2d"); + var ctx = getCachedCtx(Mx.active_canvas); if (bw > 0) { ctx.fillStyle = (func > 0) ? Mx.xwts : Mx.xwbs; // Set foreground color @@ -4595,7 +4613,7 @@ */ // ~= MX$SHADOWBOX mx.sigplot_shadowbox = function(Mx, x, y, w, h, shape, func, label, alpha) { - var ctx = Mx.active_canvas.getContext("2d"); + var ctx = getCachedCtx(Mx.active_canvas); var length = label.length; // Original method declaration includes a length - but it only represents the length of the label @@ -4867,7 +4885,7 @@ var s1; var sw; // int_2 - var ctx = Mx.active_canvas.getContext("2d"); + var ctx = getCachedCtx(Mx.active_canvas); var scrollReal2PixOut = mx.scroll_real2pix(sv); s1 = scrollReal2PixOut.s1; @@ -5203,8 +5221,15 @@ Mx._renderCanvas.width = buf.width; Mx._renderCanvas.height = buf.height; - var imgctx = Mx._renderCanvas.getContext("2d"); - var imgd = imgctx.createImageData(Mx._renderCanvas.width, Mx._renderCanvas.height); + var imgctx = getCachedCtx(Mx._renderCanvas); + var rw = Mx._renderCanvas.width; + var rh = Mx._renderCanvas.height; + if (!Mx._renderImgd || Mx._renderImgdW !== rw || Mx._renderImgdH !== rh) { + Mx._renderImgd = imgctx.createImageData(rw, rh); + Mx._renderImgdW = rw; + Mx._renderImgdH = rh; + } + var imgd = Mx._renderImgd; var src = new Uint32Array(buf); for (var ii = 0; ii < src.length; ++ii) { var index = ii * 4; @@ -5282,8 +5307,15 @@ Mx._renderCanvas.width = buf.width; Mx._renderCanvas.height = buf.height; - var imgctx = Mx._renderCanvas.getContext("2d"); - var imgd = imgctx.createImageData(Mx._renderCanvas.width, Mx._renderCanvas.height); + var imgctx = getCachedCtx(Mx._renderCanvas); + var rw = Mx._renderCanvas.width; + var rh = Mx._renderCanvas.height; + if (!Mx._renderImgd || Mx._renderImgdW !== rw || Mx._renderImgdH !== rh) { + Mx._renderImgd = imgctx.createImageData(rw, rh); + Mx._renderImgdW = rw; + Mx._renderImgdH = rh; + } + var imgd = Mx._renderImgd; // TODO - This may not be portable to all browsers, if not // we need to choose between this approach and the traditional @@ -5384,7 +5416,7 @@ var h = img.height; // Destination element - var imgctx = img.getContext("2d"); + var imgctx = getCachedCtx(img); if (!Mx.scaledImgd || Mx.scaledImgd.width !== w || Mx.scaledImgd.height !== h) { Mx.scaledImgd = imgctx.createImageData(w, h); } @@ -5406,45 +5438,52 @@ // has shown this approach to be almost twice as fast for the condition // where downscaling isn't used if (!downscaling || buf.contents === "rgba") { - for (var ii = 0; ii < dest.length; ii++) { - xx = Math.floor(ii % w * width_scaling) + sx; - yy = Math.floor(ii / w * height_scaling) + sy; - jj = Math.floor((yy * buf.width) + xx); - - value = src[jj]; - if (buf.contents !== "rgba") { - dest[ii] = colorMap.getColorByIndex(value).color; - } else { - dest[ii] = src[jj]; + var isRgba = (buf.contents === "rgba"); + for (var iy = 0; iy < h; iy++) { + yy = Math.floor(iy * height_scaling) + sy; + var rowBase = iy * w; + var srcRow = yy * buf.width; + for (var ix = 0; ix < w; ix++) { + xx = Math.floor(ix * width_scaling) + sx; + jj = srcRow + xx; + if (isRgba) { + dest[rowBase + ix] = src[jj]; + } else { + dest[rowBase + ix] = colorMap.getColorByIndex(src[jj]).color; + } } } } else { - for (var ii = 0; ii < dest.length; ii++) { - xx = Math.floor(ii % w * width_scaling) + sx; - yy = Math.floor(ii / w * height_scaling) + sy; - jj = Math.floor((yy * buf.width) + xx); - - value = src[jj]; - if (downscaling === "avg") { // average - for (var j = 1; j < width_scaling; j++) { - value += src[jj + j]; - } - value = Math.round(value / width_scaling); - } else if (downscaling === "min") { // min - for (var j = 1; j < width_scaling; j++) { - value = Math.min(value, src[jj + j]); - } - } else if (downscaling === "max") { // max - for (var j = 1; j < width_scaling; j++) { - value = Math.max(value, src[jj + j]); - } - } else if (downscaling === "minmax") { // min/max - for (var j = 1; j < width_scaling; j++) { - value = (Math.abs(value - colorOffset) > Math.abs(src[jj + j] - colorOffset)) ? value : src[jj + j]; + for (var iy = 0; iy < h; iy++) { + yy = Math.floor(iy * height_scaling) + sy; + var rowBase = iy * w; + var srcRow = yy * buf.width; + for (var ix = 0; ix < w; ix++) { + xx = Math.floor(ix * width_scaling) + sx; + jj = srcRow + xx; + + value = src[jj]; + if (downscaling === "avg") { // average + for (var j = 1; j < width_scaling; j++) { + value += src[jj + j]; + } + value = Math.round(value / width_scaling); + } else if (downscaling === "min") { // min + for (var j = 1; j < width_scaling; j++) { + value = Math.min(value, src[jj + j]); + } + } else if (downscaling === "max") { // max + for (var j = 1; j < width_scaling; j++) { + value = Math.max(value, src[jj + j]); + } + } else if (downscaling === "minmax") { // min/max + for (var j = 1; j < width_scaling; j++) { + value = (Math.abs(value - colorOffset) > Math.abs(src[jj + j] - colorOffset)) ? value : src[jj + j]; + } } - } - dest[ii] = colorMap.getColorByIndex(value).color; + dest[rowBase + ix] = colorMap.getColorByIndex(value).color; + } } } @@ -5585,7 +5624,7 @@ * @private */ mx.create_image = function(Mx, data, subsize, w, h, zmin, zmax, xcompression, drawdirection) { - var ctx = Mx.active_canvas.getContext("2d"); + var ctx = getCachedCtx(Mx.active_canvas); if (!Mx.pixel) { console.log("COLORMAP not initialized, defaulting to foreground"); @@ -5615,56 +5654,48 @@ // imgd is a flat buffer where index 0 maps to the upper-left corner var imgd = new Uint32Array(buf); if (data) { - for (var i = 0; i < imgd.length; i++) { - var ix; - var iy; - var didx; - - // Figure out what pixel we are at (upper left is 0,0) - if ((Mx.origin === 1) || (Mx.origin === 4)) { - ix = Math.floor(i % w); - } else { - ix = w - Math.floor(i % w) - 1; - } - if ((Mx.origin === 3) || (Mx.origin === 4)) { - iy = Math.floor(i / w); - } else { - iy = h - Math.floor(i / w) - 1; - } - - // Map that pixel to it's nearest data - if (drawdirection !== "horizontal") { - didx = (iy * subsize) + Math.floor(ix * nxc); - } else { - didx = (ix * subsize) + Math.floor(iy * nxc); - } - var value = data[didx]; - if (nxc > 1) { - if (xcompression === 1) { // average - for (var j = 1; j < nxc; j++) { - value += data[didx + j]; - } - value = value / nxc; - } else if (xcompression === 2) { // min - for (var j = 1; j < nxc; j++) { - value = Math.min(value, data[didx + j]); - } - } else if (xcompression === 3) { // max - for (var j = 1; j < nxc; j++) { - value = Math.max(value, data[didx + j]); - } - } else if (xcompression === 4) { // first - value = data[didx]; - } else if (xcompression === 5) { // max abs - for (var j = 1; j < nxc; j++) { - value = Math.max(Math.abs(value), Math.abs(data[didx + j])); + // Hoist origin and direction checks outside the pixel loop + var flipX = !((Mx.origin === 1) || (Mx.origin === 4)); + var flipY = !((Mx.origin === 3) || (Mx.origin === 4)); + var isHorizontal = (drawdirection === "horizontal"); + + for (var py = 0; py < h; py++) { + var iy = flipY ? (h - py - 1) : py; + var rowBase = py * w; + for (var px = 0; px < w; px++) { + var ix = flipX ? (w - px - 1) : px; + var didx; + if (!isHorizontal) { + didx = (iy * subsize) + Math.floor(ix * nxc); + } else { + didx = (ix * subsize) + Math.floor(iy * nxc); + } + var value = data[didx]; + if (nxc > 1) { + if (xcompression === 1) { // average + for (var j = 1; j < nxc; j++) { + value += data[didx + j]; + } + value = value / nxc; + } else if (xcompression === 2) { // min + for (var j = 1; j < nxc; j++) { + value = Math.min(value, data[didx + j]); + } + } else if (xcompression === 3) { // max + for (var j = 1; j < nxc; j++) { + value = Math.max(value, data[didx + j]); + } + } else if (xcompression === 4) { // first + value = data[didx]; + } else if (xcompression === 5) { // max abs + for (var j = 1; j < nxc; j++) { + value = Math.max(Math.abs(value), Math.abs(data[didx + j])); + } } } - } - - var colorIdx = Mx.pixel.getColorIndex(value); - imgd[i] = colorIdx; + imgd[rowBase + px] = Mx.pixel.getColorIndex(value); + } } } @@ -5699,7 +5730,7 @@ * @private */ mx.put_image = function(Mx, data, nx, ny, nex, ney, xd, yd, level, opacity, smoothing, downscaling) { - var ctx = Mx.active_canvas.getContext("2d"); + var ctx = getCachedCtx(Mx.active_canvas); if (!Mx.pixel) { m.log.warn("COLORMAP not initialized, defaulting to foreground"); @@ -5893,7 +5924,7 @@ } //render the buffered canvas onto the original canvas element - var ctx = Mx.active_canvas.getContext("2d"); + var ctx = getCachedCtx(Mx.active_canvas); ctx.save(); ctx.beginPath(); ctx.rect(Mx.l, Mx.t, Mx.r - Mx.l, Mx.b - Mx.t); diff --git a/js/sigplot.js b/js/sigplot.js index bb0bc9c..836e713 100644 --- a/js/sigplot.js +++ b/js/sigplot.js @@ -7545,12 +7545,19 @@ var defLabelWidth = 98; // a magic number - default width of pixels var maxLabelWidth = 0; var labelOffset = 0; - for (n = 0; n < Gx.lyr.length; n++) { // figure out maximum label - // length - var labelLength = ctx.measureText(Gx.lyr[n].name).width; - if (labelLength > maxLabelWidth) { - maxLabelWidth = labelLength; + // Cache label widths — invalidated when layer count changes or names change + var cacheKey = Gx.lyr.length + ":" + Gx.lyr.map(function(l) { return l.name; }).join(","); + if (Gx._legendCacheKey === cacheKey && Gx._legendMaxLabelWidth !== undefined) { + maxLabelWidth = Gx._legendMaxLabelWidth; + } else { + for (n = 0; n < Gx.lyr.length; n++) { + var labelLength = ctx.measureText(Gx.lyr[n].name).width; + if (labelLength > maxLabelWidth) { + maxLabelWidth = labelLength; + } } + Gx._legendCacheKey = cacheKey; + Gx._legendMaxLabelWidth = maxLabelWidth; } if (maxLabelWidth > defLabelWidth) { labelOffset = (maxLabelWidth - defLabelWidth); @@ -7818,14 +7825,22 @@ set_panbounds(plot, lyr_bnds); } - // TODO consider if this is a source of performance - // issues on streaming plots - var evt = document.createEvent('Event'); - evt.initEvent('lyrdraw', true, true); - evt.index = layer.index; - evt.name = layer.name; // the name of the layer - evt.layer = layer; - mx.dispatchEvent(Mx, evt); + // Use lightweight CustomEvent instead of deprecated createEvent/initEvent + if (!Gx.suppress_lyrdraw) { + var evt = new CustomEvent('lyrdraw', { + bubbles: true, + cancelable: true, + detail: { + index: layer.index, + name: layer.name, + layer: layer + } + }); + evt.index = layer.index; + evt.name = layer.name; + evt.layer = layer; + mx.dispatchEvent(Mx, evt); + } } /** diff --git a/js/sigplot.layer1d.js b/js/sigplot.layer1d.js index 4ddf6d1..a6235fc 100644 --- a/js/sigplot.layer1d.js +++ b/js/sigplot.layer1d.js @@ -31,6 +31,75 @@ var m = require("./m"); var mx = require("./mx"); + /** + * Min-max decimation for level-of-detail rendering. + * When data points greatly outnumber screen pixels, this reduces + * the point count to at most 2*screenWidth while preserving visual + * peaks and valleys. + * + * @param {TypedArray} xpoint - source x coordinates + * @param {TypedArray} ypoint - source y coordinates + * @param {number} npts - number of points to process + * @param {number} start - start index into xpoint/ypoint + * @param {number} screenWidth - available pixel width + * @param {TypedArray} dxpoint - destination x array (pre-allocated) + * @param {TypedArray} dypoint - destination y array (pre-allocated) + * @returns {number} number of decimated points written + */ + function decimateMinMax(xpoint, ypoint, npts, start, screenWidth, dxpoint, dypoint) { + var bucketSize = npts / screenWidth; + var outIdx = 0; + + for (var b = 0; b < screenWidth; b++) { + var bucketStart = start + Math.floor(b * bucketSize); + var bucketEnd = start + Math.floor((b + 1) * bucketSize); + if (bucketEnd > start + npts) { + bucketEnd = start + npts; + } + if (bucketStart >= bucketEnd) { + continue; + } + + var minIdx = bucketStart; + var maxIdx = bucketStart; + var minVal = ypoint[bucketStart]; + var maxVal = ypoint[bucketStart]; + + for (var i = bucketStart + 1; i < bucketEnd; i++) { + var val = ypoint[i]; + if (val < minVal) { + minVal = val; + minIdx = i; + } + if (val > maxVal) { + maxVal = val; + maxIdx = i; + } + } + + // Output min and max in x-order to preserve line continuity + if (minIdx <= maxIdx) { + dxpoint[outIdx] = xpoint[minIdx]; + dypoint[outIdx] = minVal; + outIdx++; + if (minIdx !== maxIdx) { + dxpoint[outIdx] = xpoint[maxIdx]; + dypoint[outIdx] = maxVal; + outIdx++; + } + } else { + dxpoint[outIdx] = xpoint[maxIdx]; + dypoint[outIdx] = maxVal; + outIdx++; + dxpoint[outIdx] = xpoint[minIdx]; + dypoint[outIdx] = minVal; + outIdx++; + } + } + + return outIdx; + } + /** * @constructor * @param plot @@ -81,6 +150,10 @@ this.mhpoint = null; // PointArray backed by memory in mhptr this.firstpush = false; this.options = {}; + this.decimate = true; // enable min-max decimation for large datasets + this._decXpoint = null; + this._decYpoint = null; + this._decBufSize = 0; }; Layer1D.prototype = { @@ -672,6 +745,13 @@ * - a specific view to calculate the bounds against */ get_pan_bounds: function(view) { + // If bounds were cached by a recent draw() call, use them to avoid redundant prep() + if (!view && this._cachedBounds) { + var bounds = this._cachedBounds; + this._cachedBounds = null; + return bounds; + } + var Mx = this.plot._Mx; var Gx = this.plot._Gx; @@ -840,12 +920,32 @@ if (segment) { // TODO } else { + // Apply min-max decimation for large datasets when drawing lines + var traceX = this.xpoint; + var traceY = this.ypoint; + var traceNum = pts.num; + var traceStart = pts.start; + var screenWidth = Math.abs(Mx.r - Mx.l); + + if (this.decimate && line > 0 && symbol === 0 && pts.num > 2 * screenWidth && screenWidth > 0) { + var decBufNeeded = 2 * screenWidth; + if (this._decBufSize < decBufNeeded) { + this._decBufSize = decBufNeeded; + this._decXpoint = new m.PointArray(decBufNeeded); + this._decYpoint = new m.PointArray(decBufNeeded); + } + traceNum = decimateMinMax(this.xpoint, this.ypoint, pts.num, pts.start, screenWidth, this._decXpoint, this._decYpoint); + traceX = this._decXpoint; + traceY = this._decYpoint; + traceStart = 0; + } + mx.trace(Mx, ic, - new m.PointArray(this.xptr), - new m.PointArray(this.yptr), - pts.num, - pts.start, + traceX, + traceY, + traceNum, + traceStart, 1, line, symbol, @@ -855,8 +955,8 @@ if (this.maxhold) { mx.trace(Mx, this.maxhold.color, - new m.PointArray(this.xptr), - this.mhpoint.slice(pts.start, pts.end), + this.xpoint, + this.mhpoint.subarray(pts.start, pts.end), pts.num, pts.start, 1, @@ -893,13 +993,16 @@ this.ymin = panymin; this.ymax = panymax; - return { + // Cache bounds for get_pan_bounds() to avoid redundant prep() calls + this._cachedBounds = { num: num, xmin: this.xmin, xmax: this.xmax, ymin: this.ymin, ymax: this.ymax }; + + return this._cachedBounds; }, /** diff --git a/js/sigplot.layer1dSDS.js b/js/sigplot.layer1dSDS.js index d3e7963..5c1f790 100644 --- a/js/sigplot.layer1dSDS.js +++ b/js/sigplot.layer1dSDS.js @@ -289,10 +289,13 @@ var Mx = this.plot._Mx; var numPixels = this.server_data.length/2; - this.xptr = new ArrayBuffer(numPixels*2); - this.yptr = new ArrayBuffer(numPixels*2); - this.xpoint = new Int16Array(this.xptr); - this.ypoint = new Int16Array(this.yptr); + var requiredSize = numPixels * 2; + if (!this.xptr || this.xptr.byteLength !== requiredSize) { + this.xptr = new ArrayBuffer(requiredSize); + this.yptr = new ArrayBuffer(requiredSize); + this.xpoint = new Int16Array(this.xptr); + this.ypoint = new Int16Array(this.yptr); + } // lds service returns int16 pixels with a list of all x values followed by all y values. diff --git a/js/sigplot.layer2d.js b/js/sigplot.layer2d.js index af973ae..c71d652 100644 --- a/js/sigplot.layer2d.js +++ b/js/sigplot.layer2d.js @@ -371,26 +371,19 @@ } if (this.drawmode === "falling") { - //shift and fill in the next row of data. - var cut_off = (this.lps - 1) * this.hcb.subsize; - var tmp = this.zbuf.slice(0, cut_off); - this.zbuf = []; - for (var i = 0; i < this.hcb.subsize; i++) { - this.zbuf.push(zpoint[i]); - } - this.zbuf.push.apply(this.zbuf, tmp); - tmp = []; + //shift and fill in the next row of data using in-place operations + var subsize = this.hcb.subsize; + var cut_off = (this.lps - 1) * subsize; + this.zbuf.copyWithin(subsize, 0, cut_off); + this.zbuf.set(zpoint, 0); } if (this.drawmode === "rising") { - //shift and fill in the next row of data. - var cut_off = this.lps * this.hcb.subsize; - var tmp = this.zbuf.slice(this.hcb.subsize, cut_off); - this.zbuf = []; - this.zbuf.push.apply(this.zbuf, tmp); - for (var i = 0; i < this.hcb.subsize; i++) { - this.zbuf.push(zpoint[i]); - } - tmp = []; + //shift and fill in the next row of data using in-place operations + var subsize = this.hcb.subsize; + var cut_off = this.lps * subsize; + this.zbuf.copyWithin(0, subsize, cut_off); + var offset = (this.lps - 1) * subsize; + this.zbuf.set(zpoint, offset); } }