javascripthtmlcanvasspectrum

waterfall: HTML Canvas Waterfall and Spectrum Plot


I was trying to Build a spectrum and waterfall Plot HTML canvas. After a long research and googling i found this SOURCE CODE

Now i am trying to learn how this code works. i added 3 points on the spectrum using drawPoint2 function.

Can some one please Guild me. thank you.

"use strict";

var colors = [
  "rgba(60, 229, 42, 0.31)",
  "rgba(60, 229, 42, 0.31)",
  "rgba(60, 229, 42, 0.31)",
  "rgba(60, 229, 42, 0.31)",
  "rgba(60, 229, 42, 0.31)",
  "rgba(252, 182, 3, 0.31)",
  "rgba(3, 103, 252, 0.31)",
  "rgba(219, 3, 252, 0.31)",
  "rgba(252, 3, 49, 0.31)",
  "rgba(221, 48, 232, 0.31)",
];

var closeEnough = 5;
var crop = 150;
var data_sb = -10;
var canvas = document.getElementById("spectrumSM");

Spectrum.prototype.countDecimals = function (value) {
  if (Math.floor(value) !== value)
    return value.toString().split(".")[1].length || 0;
  return 0;
};

Spectrum.prototype.map = function (x, in_min, in_max, out_min, out_max) {
  return Math.round(
    ((x - in_min) * (out_max - out_min)) / (in_max - in_min) + out_min
  );
};

Spectrum.prototype.updateSpectrumRatio = function () {
  this.spectrumHeight = Math.round(
    (this.canvas.height * this.spectrumPercent) / 100.0
  );

  // create a slop
  this.gradient = this.ctx.createLinearGradient(0, 0, 0, this.spectrumHeight);
  for (var i = 0; i < this.colormap.length; i++) {
    var c = this.colormap[this.colormap.length - 1 - i];
    this.gradient.addColorStop(
      i / this.colormap.length,
      "rgba(220,220,220,0.2)"
    ); //hardcode the patch above Xaxis
  }
};

Spectrum.prototype.resize = function () {
  var width = this.canvas.clientWidth;
  var height = this.canvas.clientHeight;

  if (this.canvas.width != width || this.canvas.height != height) {
    this.canvas.width = width;
    this.canvas.height = height;
    this.updateSpectrumRatio();
    for (var z = 0; z < this.tags.length; z++) {
      this.tags[z].StayX = this.map(
        this.tags[z].xval,
        this.cutfromArray,
        this.orignalArrayLength - this.cutfromArray,
        0 + crop,
        width
      ); ///////
    }
  }

  if (this.axes.width != width || this.axes.height != this.spectrumHeight) {
    this.axes.width = width;
    this.axes.height = this.spectrumHeight;
    this.updateAxes();
  }
};

Spectrum.prototype.updateAxes = function () {
  var width = this.ctx_axes.canvas.width + 100; //width of x axis izz
  var height = this.ctx_axes.canvas.height;

  this.maxScaleX = this.centerHz + this.spanHz / 2;
  this.minScaleX = this.centerHz - this.spanHz / 2;

  // Clear and fill with black
  this.ctx_axes.fillStyle = "black";
  this.ctx_axes.fillRect(0, 0, width, height);

  // Draw axes
  this.ctx_axes.font = "12px Arial";
  this.ctx_axes.fillStyle = "white";
  this.ctx_axes.textBaseline = "middle";

  this.ctx_axes.textAlign = "center"; //change izz
  var step = 10; //steps for y-axis
  for (var i = this.max_db - 10; i >= this.min_db + 10; i -= step) {
    var y = height - this.squeeze(i, 0, height);
    this.ctx_axes.fillText(i, 20, y); // height - y

    this.ctx_axes.beginPath();
    this.ctx_axes.moveTo(22, y); //y axis stroked set izz
    this.ctx_axes.lineTo(width, y);
    this.ctx_axes.strokeStyle = "rgba(255, 255, 255, 0.15)"; //changed strokes izz
    this.ctx_axes.stroke();
  }

  this.ctx_axes.textBaseline = "bottom";
  // change X-axis
  var x_axisSteps = 10;
  for (var i = 0; i < x_axisSteps; i++) {
    var x = Math.round((width - crop) / x_axisSteps - 1) * i + crop;

    if (this.spanHz > 0) {
      var adjust = 0;
      if (i == 0) {
        this.ctx_axes.textAlign = "left";
        adjust = 3;
      } else if (i == 10) {
        this.ctx_axes.textAlign = "right";
        adjust = -3;
      } else {
        this.ctx_axes.textAlign = "center";
      }

      //Graph points in whole points

      var freq = this.centerHz + (this.spanHz / 10) * (i - 5);

      if (freq > 1e9) {
        freq = freq / 1e9;
        if (this.countDecimals(freq) > 4) {
          freq = freq.toFixed(0);
        }
        freq = freq + " GHz";
      } else if (freq > 1e6) {
        freq = freq / 1e6;
        if (this.countDecimals(freq) > 4) {
          freq = freq.toFixed(0);
        }
        freq = freq + " MHz"; //this function executed
      } else {
        if (this.countDecimals(freq) > 2) {
          freq = freq.toFixed(0);
        }
        freq = freq + " MHz";
      }
      this.ctx_axes.fillText(freq, x - 130, height); // x axia height change izz plus values placment
    }

    //console.log("ctx_axes : ", this.ctx_axes);

    this.ctx_axes.beginPath();
    this.ctx_axes.moveTo(x, 0);
    this.ctx_axes.lineTo(x, height);
    this.ctx_axes.strokeStyle = "rgba(200, 200, 200, 0.2)"; //straight gridline on x axis izza
    this.ctx_axes.stroke();
  }
  // const input = prompt("What's your name?");
};

Spectrum.prototype.toggleColor = function () {
  this.colorindex++;
  if (this.colorindex >= colormaps.length) this.colorindex = 0;
  this.colormap = colormaps[this.colorindex];
  this.updateSpectrumRatio();
};

Spectrum.prototype.setCenterHz = function (hz) {
  this.centerHz = hz;
  if (this.center != 0 && this.center != this.centerHz) {
    this.centerHz = this.center;
  }
  this.updateAxes();
};
Spectrum.prototype.setSpanHz = function (hz) {
  this.orignalSpanHz = hz;
  this.spanHz = hz;
  if (this.zoom != 0 && this.spanHz != this.zoom) {
    this.spanHz = this.zoom;
  }
  this.updateAxes();
};

Spectrum.prototype.squeeze = function (value, out_min, out_max) {
  if (value <= this.min_db) {
    return out_min;
  } else if (value >= this.max_db) {
    return out_max;
  } else {
    return Math.round(
      ((value - this.min_db) / (this.max_db - this.min_db)) * out_max
    );
  }
};
Spectrum.prototype.squeeze2 = function (value, out_min, out_max) {
  if (value <= 30000000) {
    return out_min;
  } else if (value >= 300000000) {
    return out_max;
  } else {
    return Math.round(
      ((value - 30000000) / (300000000 - 30000000)) * out_max
    );
  }
};

Spectrum.prototype.drawRectSpectrogram = function (y, h) {
  this.ctx.beginPath();
  this.ctx.fillStyle = colors[this.rectColor]; //"rgba(60, 229, 42, 0.31)"; //rect color
  this.ctx.strokeStyle = "green";
  this.ctx.rect(this.clickRectX, y, this.clickRectWidth, h);
  this.ctx.stroke();
  this.ctx.fill();
  this.ctx.closePath();
};

Spectrum.prototype.threshold = function (y, width, color) {
  this.ctx.beginPath();
  this.ctx.strokeStyle = color;
  this.ctx.lineWidth = 2; //threshold line width

  this.ctx.beginPath();
  this.ctx.moveTo(0, y);
  this.ctx.lineTo(width + crop, y);
  this.ctx.stroke();
  this.ctx.closePath();
  this.ctx.lineWidth = 1;
};

function drawPoint2(can, x, y, label) {
  can.beginPath();
  can.arc(x, y, 5, 0, 2 * Math.PI);
  can.fillStyle = "red";
  can.fill();

  can.fillText(label, x + 10, y);
  can.stroke(); // it creates X axies and
}

Spectrum.prototype.drawSpectrum = function (bins) {
  //get canvas height and width to draw spectrum ayaz
  var width = this.ctx.canvas.width;
  var height = this.ctx.canvas.height;

  //console.log("spectrum width and  height  : "+width+" "+ height);

  // width of Green Color ayaz
  this.ctx.lineWidth = 1; //Amplitude green line width .

  //All of the points are 119 nut few points are 118 and few 120 ayaz

  if (!this.binsAverage || this.binsAverage.length != bins.length) {
    //console.log('this.binsAverage  ', this.binsAverage );
    this.binsAverage = bins;
  } else {
    for (var i = 0; i < bins.length; i++) {
      this.binsAverage[i] +=
        (1 - this.averaging) * (bins[i] - this.binsAverage[i]);
    }
  }
  bins = this.binsAverage;

  //Do not draw anything if spectrum is not visible
  if (this.ctx_axes.canvas.height < 1) return;

  //////////////////////////////////////////////////////////////Creation of X and Y axis completed

  // Copy axes from offscreen canvas

  this.ctx.drawImage(this.ctx_axes.canvas, -1, -1); // create Spectrums X and Y axis

  // // Scale for FFT
  this.ctx.save(); // saves sppectrum previous stage

  //////////////////////////////////////////////////////////////Creation of X and Y axis completed

  this.ctx.scale(width / this.wf_size, 1); //green line visiblity izza
  var peakY = this.spectrumHeight;

  // Draw FFT bins
  this.ctx.beginPath();

  this.offset = Math.round(
    this.map(crop, 0, this.ctx.canvas.width + 6000, 0, this.wf_size)
  ); // green line axis set izza

  //console.log('this.offset : ', this.offset);
  console.log(
    "X and Y are : ",
    -1 + this.offset + "  " + this.spectrumHeight + 0
  );
  this.ctx.moveTo(-1 + this.offset, this.spectrumHeight + 0); //change for waterfall izza
  // above line is the address of left bottom corner of spectrum

  // console.log(
  //   "clkkkkkkkkkkkkkkk : ",
  //   this.clickRectX+
  //   crop+
  //   width+
  //   this.minScaleX+
  //   this.maxScaleX
  //   );

  var rangeMin = this.map(
    this.clickRectX,
    crop,
    width,
    this.minScaleX,
    this.maxScaleX
  );
  var rangeMax = this.map(
    this.clickRectWidth + this.clickRectX,
    crop,
    width,
    this.minScaleX,
    this.maxScaleX
  );

  if (rangeMin > rangeMax) {
    var temp;
    temp = rangeMax;
    rangeMax = rangeMin;
    rangeMin = temp;
  }

  var max = 100000000;

  this.scaleY = this.map(this.threshY, this.min_db, this.max_db, height / 2, 0); // 0, height/2 // height of threshold izza

  this.detectedFrequency.length = 0;

  // console.log('bins are ', bins);
  //console.log('new array ');
  for (var i = 0; i < bins.length; i++) {
    // if ( parseFloat( bins[i]) > -100 ) {
    //   console.log("bins : ", bins[i]);
    //   const input = prompt("What's your name?");
    //   alert(`Your name is ${input}`);
    // }
    var y =
      this.spectrumHeight - 1 - this.squeeze(bins[i], 0, this.spectrumHeight);
    peakY = this.squeeze(bins[i], 0, this.spectrumHeight);

    if (y > this.spectrumHeight - 1) {
      y = this.spectrumHeight - 1;
      console.log("Y has chnaged : ", y);
    }
    if (y < 0) {
      y = 0;
      console.log("y=0");
    }
    if (i == 0) {
      this.ctx.lineTo(-1 + this.offset, y);//responsible for height of green line
    }

    // green lines are created here ayaz
    // create green lines
    // important

    //drawPoint2(this.ctx ,i + this.offset, y);
    this.ctx.lineTo(i + this.offset, y); // that what point we start drawing green horizental green line

    // console.log(
    //   "Starting Line 1 i " +
    //     i +
    //     " X : " +
    //     parseInt(i + this.offset) +
    //     " y : " +
    //     y
    // );

    if (i == bins.length - 1) {
      //drawPoint2(this.ctx ,i + this.offset, y);
      this.ctx.lineTo(this.wf_size + 1 + this.offset, y);
      // console.log(
      //   "Second 2 i == bins.length - 1 i " +
      //     i +
      //     " Drawingg -1 + this.offset : " +
      //     parseInt(i + this.offset) +
      //     " y : " +
      //     y
      // );
    }

    for (var z = 0; z < this.tags.length; z++) {
      if (
        i + this.cutfromArray == this.tags[z].xval &&
        this.check_click == true
      ) {
        this.tags[z].tagY = y;
        this.tags[z].yval = Math.round(bins[i]);
        this.tags[z].displayX = Math.round(
          this.map(i, 0, bins.length, this.minScaleX, this.maxScaleX)
        );
      }
    }

    if (y < max) {
      max = y;
    }

    let newVal = Math.round(
      this.map(i, 0, bins.length, this.minScaleX, this.maxScaleX)
    );

    if (this.check_bar) {
      if (newVal < rangeMax && newVal > rangeMin) {
        if (y < this.scaleY) {
          var obj = new Object();
          obj.x = newVal;
          obj.y = Math.round(bins[i]);
          obj.count = 1;

          var check = true;

          for (var j = 0; j < this.threshRange.length; j++) {
            if (
              this.threshRange[j].x == obj.x &&
              this.threshRange[j].y == obj.y
            ) {
              this.threshRange[j].count++;
              check = false;
            }
          }
          if (check) {
            let tableRows = document
              .getElementById("thresh-table-body")
              .getElementsByTagName("tr").length;
            if (tableRows < 100) {
              this.threshRange.push(obj);
              // filling table
              let tbody = document.getElementById("thresh-table-body");
              let tr = document.createElement("tr");
              let td1 = document.createElement("td");
              let td2 = document.createElement("td");
              let td3 = document.createElement("td");
              td1.innerHTML = obj.x; //+" Hz"
              td2.innerHTML = obj.y;
              td3.innerHTML = obj.count;
              tr.appendChild(td1);
              tr.appendChild(td2);
              tr.appendChild(td3);
              tbody.appendChild(tr);
            }
          } else {
            // update table count
            for (let c = 0; c < this.threshRange.length; c++) {
              let tableRows =
                document.getElementById("thresh-table-body").rows[c].cells;
              if (
                tableRows[0].innerHTML == obj.x &&
                tableRows[1].innerHTML == obj.y
              ) {
                let countValue = Number(tableRows[2].innerHTML);
                countValue++;
                tableRows[2].innerHTML = countValue;
              }
            }
          }
        }
      }
    } else {
      if (y < this.scaleY) {
        var obj = new Object();
        obj.x = newVal;
        obj.y = Math.round(bins[i]);
        obj.count = 1;

        var check = true;

        for (var j = 0; j < this.threshRange.length; j++) {
          if (
            this.threshRange[j].x == obj.x &&
            this.threshRange[j].y == obj.y
          ) {
            this.threshRange[j].count++;
            check = false;
          }
        }
      }
    }
  }

  // this.ctx.beginPath();
  // this.ctx.arc(100, 75, 20, 0, 1 * Math.PI);
  // this.ctx.stroke();

  //this.ctx.strokeRect(1800,10, 40, 190);
  function containsObject(obj, list) {
    var i;
    for (i = 0; i < list.length; i++) {
      if (list[i] === obj) {
        return true;
      }
    }

    return false;
  }

  this.ctx.fillStyle = "rgba(0, 0, 0, 0.11)";

  // drawPoint2(this.ctx ,
  //   this.wf_size + 1 + this.offset,
  //   this.spectrumHeight + 1
  // );
  this.ctx.lineTo(this.wf_size + 1 + this.offset, this.spectrumHeight + 1);



  // console.log(
  //   "thired 3 this.wf_size + 1 + this.offset : " +
  //     parseInt(this.wf_size + 1 + this.offset) +
  //     " this.spectrumHeight + 1 : " +
  //     parseInt(this.spectrumHeight + 1)
  // );

  // drawPoint2(this.ctx ,
  //   this.wf_size + this.offset,
  //   this.spectrumHeight
  // );
  this.ctx.lineTo(this.wf_size + this.offset, this.spectrumHeight );

  // console.log(
  //   "Forth this.wf_size + this.offset : " +
  //     parseInt(this.wf_size + this.offset) +
  //     " y : " +
  //     y
  // );

  if (y < 230 && y > 245) {
    console.log("foundddddddddddddd");
  }

  this.ctx.closePath();

  this.ctx.restore();

  this.ctx.strokeStyle = "#259a00"; //color of spectrum green

  this.ctx.stroke(); // it creates X axies and
  /////////////////////////////////////////////////////////////////////////green ended

  if (this.spectrumColorCheck) {
    this.ctx.fillStyle = this.gradient; //chnage color of under line chart
  } else {
    this.ctx.fillStyle = "rgba(0, 0, 0, 0.0)";
  }

  this.ctx.fill();
  if (this.check_bar) {
    this.drawRectSpectrogram(0, height);
  }

  var colorTh = "#cc8315"; //By uncomment Change the threshold line color change
  this.threshold(this.scaleY, width, colorTh); // yellow light

  if (this.check_click == true) {
    for (let c = 0; c < this.tags.length; c++) {
      this.drawTag(
        this.tags[c].StayX,
        this.tags[c].tagY,
        this.tags[c].displayX,
        this.tags[c].yval
      );
    }
    if (this.removeTagCheck == true) {
      closeEnough = 30;
      for (var z = 0; z < this.tags.length; z++) {
        if (this.checkCloseEnough(this.StayX, this.tags[z].StayX + 15)) {
          this.tags.splice(z, 1);
          z--;
        }
      }
    }
  }

  closeEnough = 5;

  // span hz commented
  if (this.updateValueCheck) {
    if (this.countDecimals(this.threshY) > 2) {
      this.threshY = this.threshY.toFixed(2);
    }
    this.updateValueCheck = false;
  }

  var arrt = [ -100000000 , 40000000,35000000];

  this.spectrumWidth = Math.round(
    (this.canvas.width * this.spectrumPercent) / 100.0
  );

  for (var k = 0; k < 4; k++) {
    var alpha =
      this.spectrumHeight - 620 - this.squeeze2(arrt[k], 0, this.spectrumWidth);
    drawPoint2(this.ctx, alpha + 600, 150, "A");
  }

  // draw separate lines
  // for (var k=0;k< 4; k++){
  //   drawPoint2(this.ctx, "200000000", "-80");
  //   drawPoint2(this.ctx,"100000000", "-80");

  //   drawPoint2(this.ctx,"35000000", "-80");

  // }

  //console.log('this.ctx : ',this.ctx);
};

Spectrum.prototype.addData = function (data, bmp) {
  if (!this.paused) {
    if (data.length > 32768) {
      data = this.sequenceResize(data, 32767);
    }

    this.orignalArrayLength = data.length;

    if (this.orignalSpanHz > this.spanHz) {
      data = data.slice(
        this.cutfromArray,
        this.orignalArrayLength - this.cutfromArray
      );
    }

    if (data.length != this.wf_size) {
      this.wf_size = data.length;
      if (data.length > 32767) {
        this.ctx_wf.canvas.width = 32767;
      } else {
        this.ctx_wf.canvas.width = data.length;
      }

      this.ctx_wf.fillStyle = "black";
      this.ctx_wf.fillRect(0, 0, this.wf.width, 0); //strokes of waterfall

      for (var z = 0; z < this.tags.length; z++) {
        this.tags[z].StayX = this.map(
          this.tags[z].xval,
          this.cutfromArray,
          this.orignalArrayLength - this.cutfromArray,
          crop,
          this.ctx.canvas.width
        );
      }
    }

    this.drawSpectrum(data);
    // this.addWaterfallRowBmp(bmp);
    // this.addWaterfallRow(data);
    this.resize();
  }
};

function Spectrum(id, options) {
  // Handle options
  this.centerHz = options && options.centerHz ? options.centerHz : 0;
  this.spanHz = options && options.spanHz ? options.spanHz : 0;
  this.wf_size = options && options.wf_size ? options.wf_size : 0;
  this.wf_rows = options && options.wf_rows ? options.wf_rows : 50;
  this.spectrumPercent =
    options && options.spectrumPercent ? options.spectrumPercent : 25;
  this.spectrumPercentStep =
    options && options.spectrumPercentStep ? options.spectrumPercentStep : 5;
  this.averaging = options && options.averaging ? options.averaging : 0.5;
  this.rectColor = options && options.rectColor ? options.rectColor : 0;

  // Setup state
  this.paused = false;
  this.fullscreen = true;
  this.min_db = -140;
  this.max_db = -20;
  this.spectrumHeight = 0;

  // Colors
  this.colorindex = 2;
  this.colormap = colormaps[0];

  // Create main canvas and adjust dimensions to match actual
  this.canvas = document.getElementById("spectrumSM");
  canvas.height = this.canvas.clientHeight;
  this.canvas.width = this.canvas.clientWidth;

  this.ctx = this.canvas.getContext("2d");
  this.checkclick = false;
  this.clickRectWidth = 1000;
  this.dragL = true;
  this.dragR = true;
  // this.ctx.globalAlpha = 0.1;
  this.drag = true;
  this.click_x = 0;
  this.click_y = 0;
  this.check_click = true;
  this.threshY = -70;
  this.StayX = -10;
  this.scaleY = 0;

  //for change waterfall design
  this.threshCheck = true;
  this.removeTagCheck = true;
  this.spectrumColorCheck = true;
  this.check_bar = true;
  this.updateValueCheck = true;

  this.tags = [];
  this.addTagsCheck = true;

  this.maxScaleX = 0;
  this.minScaleX = 0;
  this.orignalArrayLength = 0;
  this.zoom = this.spanHz;
  this.center = this.centerHz;

  this.threshRange = [];
  this.detectedFrequency = [];

  this.offset = 10;
  this.arraySizeto = 0;
  this.orignalSpanHz = 0;
  this.cutfromArray = 0;

  this.ctx.fillStyle = "black";
  this.ctx.fillRect(10, 10, this.canvas.width, this.canvas.height);

  // Create offscreen canvas for axes
  this.axes = document.createElement("canvas");
  this.axes.id = "myCheck";
  this.axes.height = 1; // Updated later
  this.axes.width = this.canvas.width;
  this.ctx_axes = this.axes.getContext("2d");

  function myFunction() {
    this.style.fontSize = "40px";
  }

  // Create offscreen canvas for waterfall
  this.wf = document.createElement("canvas");
  this.wf.height = this.wf_rows;
  this.wf.width = this.wf_size;
  this.ctx_wf = this.wf.getContext("2d");

  // Trigger first render
  this.updateSpectrumRatio();
  this.resize();
}

Here showing value of spectrum at specific point via click and zoom in/ zoom out functionality.

var closeEnough = 5;
var crop = 150;
var data_sb = -10;
var canvas = document.getElementById("spectrumSM");

Spectrum.prototype.mousedown = function (evt) {
  this.checkclick = true;
  if (this.checkCloseEnough(this.click_x-3, this.clickRectX)) {
    this.dragR = false;
    this.dragL = true;
  } else if (
    this.checkCloseEnough(this.click_x-3, this.clickRectX + this.clickRectWidth)
  ) {
    this.dragR = true;
    this.dragL = false;
  } else if (this.checkCloseEnough(this.click_y-3, this.scaleY)) {
    this.drag = true;
  }
};
Spectrum.prototype.mouseup = function (evt) {
  this.checkclick = false;

  this.dragL = false;
  this.dragR = false;

  this.drag = false;

  if (evt.button === "right") {
    this.tags = [];
  }
};

Spectrum.prototype.mousemove = function (evt) {
  var rect = this.canvas.getBoundingClientRect();

  this.click_x = evt.clientX - rect.left;

  this.click_y = evt.clientY - rect.top;
  closeEnough = Math.abs(this.clickRectWidth);
  if (this.dragL == false && this.dragR == false && this.drag == false) {
    if (
      this.checkclick == true &&
      this.checkCloseEnough(
        this.click_x,
        this.clickRectX + this.clickRectWidth / 2
      )
    ) {
      this.clickRectX = this.click_x - this.clickRectWidth / 2;
    }
  } else if (this.dragL) {
    this.clickRectWidth += this.clickRectX - this.click_x;
    this.clickRectX = this.click_x;
  } else if (this.dragR) {
    this.clickRectWidth = -(this.clickRectX - this.click_x);
  } else if (this.drag && this.threshCheck) {
    this.updateValueCheck = true;
    this.threshY = this.map(
      this.click_y,
      this.canvas.height / 2,
      0,
      this.min_db,
      this.max_db
    ); // this.max_db, this.min_db
  }
  closeEnough = 10;
};

Spectrum.prototype.click = function (evt) {
  // change izza
  this.check_click = true;
  this.StayX = this.click_x;
  console.log('tag list : ',this.addTagsCheck);
  if (this.addTagsCheck == true && this.StayX > 3) {
    var tag = {
      StayX: this.click_x,
      tagY: 0,
      yval: 0,
      xval: Math.round(
        this.map(
          this.click_x,
          28,
          this.ctx.canvas.width,
          this.cutfromArray,
          this.orignalArrayLength - this.cutfromArray
        )
      ),
      displayX: 0,
    };

    this.tags.push(tag);
  }
};

Spectrum.prototype.wheel = function (evt) {
  this.zoom = this.spanHz;
  var inc;
  if (this.arraySizeto == 0) {
    inc = Math.round(this.orignalArrayLength * 0.05);
  } else {
    inc = Math.round(this.arraySizeto * 0.05);
  }
  let zoomInc = 0;
  if (this.orignalSpanHz > this.orignalArrayLength) {
    zoomInc = this.orignalSpanHz / this.orignalArrayLength;
  } else {
    zoomInc = this.orignalArrayLength / this.orignalSpanHz;
  }

  if (evt.deltaY > 0) {
    if (
      this.orignalSpanHz - (zoomInc * this.cutfromArray - inc * 2) <
        this.orignalSpanHz &&
      this.cutfromArray - inc >= 0
    ) {
      this.cutfromArray = this.cutfromArray - inc;
      this.zoom = this.orignalSpanHz - zoomInc * this.cutfromArray * 2;
    } else if (
      this.orignalSpanHz + zoomInc * this.cutfromArray * 2 >=
        this.orignalSpanHz &&
      this.cutfromArray - inc <= 0
    ) {
      this.zoom = this.orignalSpanHz;
      this.cutfromArray = 0;
    }
  } else if (evt.deltaY < 0) {
    if (
      this.orignalSpanHz - (zoomInc * this.cutfromArray + inc * 2) > inc &&
      this.orignalArrayLength - this.cutfromArray * 2 - inc * 2 >
        this.ctx.canvas.width
    ) {
      this.cutfromArray = this.cutfromArray + inc;
      this.zoom = this.orignalSpanHz - zoomInc * this.cutfromArray * 2;
    }
  }
  this.arraySizeto = this.orignalArrayLength - this.cutfromArray * 2;
  this.maxScaleX = this.centerHz + this.zoom / 20;
  this.minScaleX = this.centerHz - this.zoom / 20;
};

Spectrum.prototype.undoTag = function () {
  this.tags.pop();
};

Spectrum.prototype.checkCloseEnough = function (p1, p2) {
  return Math.abs(p1 - p2) < closeEnough;
};

Spectrum.prototype.drawTag = function (locx, locy, xval, yval) {
  this.ctx.beginPath();
  this.ctx.strokeStyle = "#cc8315";
  let freq = xval;
  if (freq > 1e9) {
    freq = freq / 1e9;
    if (this.countDecimals(freq) > 2) {
      freq = freq.toFixed(2);
    }
    freq = freq + " GHz";
  } else if (freq > 1e6) {
    freq = freq / 1e6;
    if (this.countDecimals(freq) > 2) {
      freq = freq.toFixed(2);
    }
    freq = freq + " MHz";
  } else {
    if (this.countDecimals(freq) > 2) {
      freq = freq.toFixed(2);
    }
  }
  var text = "  ( " + freq + ", " + yval + ")";
  var padding = 5;
  var fontSize = 20;
  var xPos = locx - padding;
  var width = this.ctx.measureText(text).width + padding * 2;
  var height = fontSize * 1.286;
  var yPos = locy - height / 1.5;
  this.ctx.lineWidth = 2;
  this.ctx.fillStyle = "rgba(204, 131, 21, 0.8)";
  // draw the rect
  this.ctx.fillRect(xPos, yPos, width, height);
  this.ctx.fillStyle = "white";
  this.ctx.font = "bold 10pt droid_serif";
  this.ctx.fillText(text, locx, locy);
  this.ctx.fillStyle = "white";
  this.ctx.beginPath();
  this.ctx.arc(locx, locy, 2, 0, Math.PI * 2, true);
  this.ctx.fill();
  this.ctx.closePath();
};

Solution

  • The spectrum project you reference expects to receive regular updates of an array of data passed into drawSpectrum. Each time it renders that data as a new spectrum in drawFFT, with the data scaled by the numbers set in setRange. Each time it receives data, it also creates a new row of pixels for the waterfall in the rowToImageData function, again scaled by the numbers set in setRange. The previously created rows of pixels are shifted down by one row in addWaterfallRow.

    I've made a fiddle that shows how data is handled by the Spectrum object: https://jsfiddle.net/40qun892/

    If you run the fiddle it shows two examples, one with three points and another with 100 points of randomly generated data. This line shows how an x-axis is added to the graph:

    const spectrumB = new Spectrum("spectrumCanvasB", {spanHz: 5000, centerHz: 2500});
    

    The x-axis is only shown when spanHz is defined. It is generated by drawing 11 labels, equally distributed across the canvas. With the center label based on centerHz, and the remaining labels calculated based on spanHz.

    As you can see from the spectrums generated by the fiddle, the x-axis labels are not connected to the data, they are just equally distributed across the canvas.

    The graph behind the data is created by applying a scale to the graph so that using the array index will result in data stretched across the graph.

    this.ctx.scale(width / <number of data points>, 1);
    for (var i = 0; i < <number of data points>.length; i++) {
        // lines removed
        this.ctx.lineTo(i, y);
        // lines removed
    }
    

    As you can see from the examples in the fiddle, this doesn't look very nice when there's only three datapoints, because the white lines are stretched in the x-direction but not the y-direction.

    Spectrum doesn't care about x-coordinates. It just stretches whatever it is given to fit the canvas. If you give it a spanHz property, it will distribute some labels across the canvas too, but it does not associate them with the data.

    The scaling seems to be slightly wrong in Spectrum (which is only noticeable if very few datapoints are used). If I make this change, the points are correctly stretched:

    this.ctx.scale(width / (this.wf_size - 1), 1);
    

    Then this change would set the x-axis labels to 100Mhz - 300Mhz:

    const spectrumA = new Spectrum("spectrumCanvasA", {spanHz: 200000000, centerHz: 200000000});
    

    Edit: (The relationship between frequency and data)

    The only thing the code knows about the frequency is based on the spanHz anad centerHz.

    The frequency at an array index is

    (<array index> / <number of points on X axis> * <spanHz>) + (<centerHz> / 2)
    

    The frequency at a pixel is

    (<x coordinate> / <width of canvas> * <spanHz>) + (<centerHz> / 2)
    

    If you want to convert a frequency to an array index (or a pixel) it would be slightly more complicated, because you would need to find the closest element to that frequency. For example, the pixel at a frequency is:

    round((<frequency> - (<centerHz> / 2)) / <spanHz> * <width of canvas>)