Vectors in JavaScript

Click on the three arrows for better reading experience.

JavaScript is NOT the language that comes to mind when you look for fast vector operations. But sometimes it is handy to use Vector like syntax and operations instead of map() or similar standard programming techniques you use for arrays.

I just happened to need Vectors and found this very nice module : https://github.com/RodionChachura/linear-algebra

The drawback was I needed slightly different API, more compact code and I also found I could abstract things differently. It was also a good exercise. So I used the module as a template to make my own. You can see the result below.

First we add several basic Functions that we would need to the Math class. They could be standalone Functions if you will, but Math seems like a better host for them.

Second thing is that the vector class extends directly the Array class, so you can reuse all Array methods if you need.

The third noticeable thing to mention is that the core functionality is implemented as the class method called .vmap(), short from vector-map.
There is also an object method called .vmap(), but that is just a wrapper that internally simply calls the class method.

The vmap() accepts the following arguments :

  • op : a two argument function that is applied to every element of the vectors.
  • vector1 : first argument.
  • vvf : aka vector-or-value-or-function
  • fun1 : pre-processing function for vector1
  • fun2 : pre-processing function for vector2

In effect the idea is that you can apply a function to the vectors. As a bonus you have the option to pre-process the any of them or both before applying this operator-function.
Use the Math.id() for the first vector if you have to pre-process only the second vector.

//you create vector by calling the constructor
vec = new vector(5,32,26,48,13)

//or random vector
vec = vector.rand(min=0,max=10,count=5)

and now the source code :

const EPSILON = 0.00000001
const areEqual = (one, other, epsilon = EPSILON) =>
  Math.abs(one - other) < epsilon

const to_degrees = radians => (radians * 180) / Math.PI

Math.add = (a,b) => a + b
Math.sub = (a,b) => a - b
Math.mul = (a,b) => a * b
Math.div = (a,b) => a / b
Math.id  = val => val

class vector extends Array {
  //apply op on a vector|fun(vec) OR vector and num|vec
  static vmap(op,vec1,vvf,fun1,fun2){
    //preprocess the vectors
    if (typeof fun1 == 'function') vec1 = vector.vmap(fun1,vec1)
    if (typeof fun2 == 'function') vvf = vvf instanceof vector ? vector.vmap(fun2,vvf) : fun2(vvf)

    switch (typeof vvf) {
      //function ON vector
      case 'undefined': return new vector(...vec1.map(a => op(a)) )
      //function ON function ON vector
      case 'function' : return new vector(...vec1.map(a => op(vvf(a))) )
      //number ON vector
      case 'number'   : return new vector(...vec1.map(a => op(a,vvf)) )
      //vector ON vector
      default         : return new vector(...vec1.map((a,ix) => op(a,vvf[ix])) )
    }
  }

  //object method
  vmap(op,vvf,fun1,fun2) { return vector.vmap(op,this,vvf,fun1,fun2) }

  static rand(min,max,len){
    let vec = new vector()
    for (let i=0; i < len; i++) {
      vec.push(Math.floor((Math.random() * max) + min))
    }
    return vec
  }


  add(vec) { return vector.vmap(Math.add,this,vec) }
  sub(vec) { return vector.vmap(Math.sub,this,vec) }
  mul(vec) { return vector.vmap(Math.mul,this,vec) }
  div(vec) { return vector.vmap(Math.div,this,vec) }

  count(val=0){ return this.reduce( (acc,ix) => this[ix] == val ? acc : acc+1, 0) }
  non_count(val=0){ return this.reduce( (acc,ix) => this[ix] == val ? acc+1 : acc, 0) }

  len() { return Math.hypot(...this) }
  // scale_by(number) { return new vector(...this.map(val => val * number) ) }
  dot(vec) { return vec.reduce((acc, val, index) => acc + val * this[index], 0) }
  norm() {return this.mul(1 / this.len()) }

  angle(other) {
    return to_degrees(Math.acos(this.dot(other) / (this.len() * other.len()) ) ) 
  }
  negate() { return this.mul(-1) }

}

Beside vmap-based methods I included some basic Vector operations. You can see examples of how to use the module below …..

say = console.log

//generate random vectors
v1 = vector.rand(0,100,5)
v2 = vector.rand(0,100,5)

say("vector 1 : ", v1)
say("vector 2 : ", v2)

say("v1 + 10 : ", v1.add(10))
say("v1 * 2 : ", v1.mul(2))
say("v1 + v2 : ", v1.add(v2))

-----

vector 1 :  vector [ 38, 48, 31, 20, 11 ]
vector 2 :  vector [ 77, 49, 73, 14, 40 ]
v1 + 10 :  vector [ 48, 58, 41, 30, 21 ]
v1 * 2 :  vector [ 76, 96, 62, 40, 22 ]
v1 + v2 :  vector [ 115, 97, 104, 34, 51 ]

=====

say("\n-----: general methods")
say("dot product : v1 . v2 : ", v1.dot(v2))
say("normalized v1 : ", v1.norm())
say("vector length v1 : ", v1.len())
say("angle v1,v2 : ", v1.angle(v2))

-----

-----: general methods
dot product : v1 . v2 :  8261
normalized v1 :  vector [
  0.5254516403878032,
  0.6637283878582778,
  0.42865791715847107,
  0.27655349494094905,
  0.152104422217522
]
vector length v1 :  72.31873892705818
angle v1,v2 :  23.240650242950494


=====
say("\n-----: using .vmap()")
say("v1 + 10 : ", v1.vmap(Math.add,10))
say("sqrt(v1) + 10 : ", v1.vmap(Math.add,10,Math.sqrt))
say("v1 + v2 : ", v1.vmap(Math.add,v2))
say("v1/2 + v2/2 : ", v1.vmap(Math.add,v2, (a)=> a/2, (b)=>b/2))


-----: using .vmap()
v1 + 10 :  vector [ 48, 58, 41, 30, 21 ]
sqrt(v1) + 10 :  vector [
  16.164414002968975,
  16.928203230275507,
  15.567764362830022,
  14.47213595499958,
  13.3166247903554
]
v1 + v2 :  vector [ 115, 97, 104, 34, 51 ]
v1/2 + v2/2 :  vector [ 57.5, 48.5, 52, 17, 25.5 ]


=====
//creating a vector via coonstructor
bin = new vector(1,0,0,0,1,1,0,0)

say("binary vector : ", bin)
say("count 1s : ", bin.count(1))
say("count 0s : ", bin.count(0))
say("count 1s : ", bin.non_count(0))


-----------
binary vector :  vector [
  1, 0, 0, 0,
  1, 1, 0, 0
]
count 1s :  3
count 0s :  5
count 1s :  3