Point Cloud in JavaScript

How to make D3.js-based Point Cloud plots in Plotly.js.


New to Plotly?

Plotly is a free and open-source graphing library for JavaScript. We recommend you read our Getting Started guide for the latest installation or upgrade instructions, then move on to our Plotly Fundamentals tutorials or dive straight in to some Basic Charts tutorials.

var myPlot = document.getElementById('myDiv');
var xy = new Float32Array([1,2,3,4,5,6,0,4]);

data = [{ xy: xy,  type: 'pointcloud' }];
layout = { };

Plotly.newPlot('myDiv', data, layout);
var trace1 = {
  type: "pointcloud",
  mode: "markers",
  marker: {
    sizemin: 0.5,
    sizemax: 100,
    arearatio: 0,
    color: "rgba(255, 0, 0, 0.6)"
  },
  x: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
  y: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
}

var trace2 = {
  type: "pointcloud",
  mode: "markers",
  marker: {
    sizemin: 0.5,
    sizemax: 100,
    arearatio: 0,
    color: "rgba(0, 0, 255, 0.9)",
    opacity: 0.8,
    blend: true
  },
  opacity: 0.7,
  x: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
  y: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
}

var trace3 = {
  type: "pointcloud",
  mode: "markers",
  marker: {
    sizemin: 0.5,
    sizemax: 100,
    border: {
      color: "rgb(0, 0, 0)",
      arearatio: 0.7071
    },
    color: "green",
    opacity: 0.8,
    blend: true
  },
  opacity: 0.7,
  x: [3, 4.5, 6],
  y: [9, 9, 9]
}

var data = [trace1, trace2,trace3];

var layout = {
  title: "Basic Point Cloud",
  xaxis: {
    type: "linear",
    range: [
      -2.501411175139456,
      43.340777299865266],
    autorange: true
  },
  yaxis: {
    type: "linear",
    range: [4,6],
    autorange: true
  },
  height: 598,
  width: 1080,
  autosize: true,
  showlegend: false
}

Plotly.newPlot('myDiv', data, layout);
var graphDiv = document.getElementById("myDiv")
var canvas = document.getElementById("canvas")
var ctx = canvas.getContext("2d")

var pointCount = 1e6
var scaleX = 2000
var scaleY = 1000
var speed = 0.3

// some non-uniform distribution
function pseudogaussian() {return (Math.random() + Math.random() + Math.random() + Math.random() + Math.random() + Math.random()) - 3}

// dataset xy array generator
function gaussian(sd) {
  var result = new Float32Array(2 * pointCount)
  var f = 20 / 360 * 2 * Math.PI
  var sin = Math.sin(f)
  var cos = Math.cos(f)
  var i, x, y
  for(i = 0; i < pointCount; i++) {
    x = scaleX * pseudogaussian() * sd
    y = scaleY * pseudogaussian() * sd * 0.75
    result[i * 2] = x * cos - y * sin + scaleX * 0.5
    result[i * 2 + 1] = x * sin + y * cos + scaleY * 0.5
  }

  return result
}

// generate initial dataset
var geom = gaussian(1/5)

var plotData = {
  data: [
    {
      type: 'pointcloud',
      marker: {
        sizemin: 0.05,
        sizemax: 30,
        color: 'darkblue',
        opacity: 1,
        blend: true
      },
      opacity: 1,
      xy: geom, // instead of separate x and y arrays
      indices: new Int32Array(pointCount).map(function(d, i) {return i;}),
      xbounds: [0, scaleX],
      ybounds: [0, scaleY]
    }
  ],
  layout: {
    title: 'Point Cloud - updating 1 million points in every single frame',
    autosize: false,
    width: 1000,
    height: 600,
    hovermode: false,
    dragmode: "pan"
  }
}

function reds (imageData) {
  // uses the red channel for simplicity
  var result = []
  var data = imageData.data
  var width = imageData.width
  var height = imageData.height
  var i, j
  for(j =0; j < height; j++)
    for(i = 0; i < width; i++)
      if(data[4 * (i + width * j)])
        result.push([i, j])
  return result
}

function fillGeom(pixels, width, height) {
  var result = new Float32Array(2 * pointCount)
  var i
  var pixel
  var pixLength = pixels.length
  for(i = 0; i < pointCount; i++) {
    pixel = pixels[i % pixLength] // recycling and jittering points
    result[2 * i] = scaleX * (pixel[0] + Math.random()) / width
    result[2 * i + 1] = scaleY * (1 - (pixel[1] + Math.random()) / height)
  }
  return result
}

function recurrenceRelationGeom(target, geom, speed, maxVelo, fraction) {
  // non-one fraction is for glitch effects
  var i, ii, diff
  var geomPointCount = geom.length
  var changedCount = Math.round(geomPointCount * fraction)
  var from = Math.floor(Math.random() * geomPointCount)
  var to = from + changedCount
  for(ii = from; ii < to; ii++) {
    i = ii % geomPointCount
    diff = speed * (target[i] - geom[i])
    geom[i] += Math.min(maxVelo, diff) // capping for glitch effect
  }
}

function clearCanvas(ctx, width, height) {
  ctx.fillStyle = "black"
  ctx.fillRect(0, 0, width, height)
  ctx.fillStyle = "red"
}

function plotlyTextGeom(ctx, width, height) {

  clearCanvas(ctx, width, height)
  ctx.font = "bold 260px 'Open sans',verdana,arial,sans-serif"
  ctx.textAlign = "center"
  ctx.textBaseline = "middle"
  ctx.fillText("Plotly", width / 2, height / 2)
  var imageData = ctx.getImageData(0, 0, width, height)
  var filledPixels = reds(imageData)

  return fillGeom(filledPixels, width, height)
}

function pointcloudTextGeom(ctx, width, height) {

  clearCanvas(ctx, width, height)
  ctx.font = "bold 240px 'Open sans',verdana,arial,sans-serif"
  ctx.textAlign = "center"
  ctx.textBaseline = "alphabetic"
  ctx.fillText("Point", width / 2, height / 2)
  ctx.textBaseline = "hanging"
  ctx.fillText("Cloud", width / 2, height / 2)
  var imageData = ctx.getImageData(0, 0, width, height)
  var filledPixels = reds(imageData)

  return fillGeom(filledPixels, width, height)
}

function oneMillionTextGeom(ctx, width, height) {

  clearCanvas(ctx, width, height)
  ctx.font = "bold 144px 'Open sans',verdana,arial,sans-serif"
  ctx.textAlign = "center"
  ctx.textBaseline = "bottom"
  ctx.fillText("1 million", width / 2, height / 2)
  ctx.textBaseline = "top"
  ctx.fillText("live points", width / 2, height / 2)
  var imageData = ctx.getImageData(0, 0, width, height)
  var filledPixels = reds(imageData)

  return fillGeom(filledPixels, width, height)
}

function initializeCanvas(plotArea) {
  canvas.style.left = plotArea.left + "px"
  canvas.style.top = plotArea.top + "px"
  canvas.setAttribute("width", plotArea.width)
  canvas.setAttribute("height", plotArea.height)
}


// 'Open sans',verdana,arial,sans-serif
Plotly.newPlot('myDiv', plotData.data, plotData.layout).then(function() {

  var plotArea = document.querySelector('.gl-container div').getBoundingClientRect()

  var width = plotArea.width
  var height = plotArea.height

  initializeCanvas(plotArea)

  var targetPlotly = {
    geom: plotlyTextGeom(ctx, width, height),
    color: "blue",
    speed: 2 - speed,
    maxVelo: Infinity,
    fraction: 1
  }
  var targetPointcloud = {
    geom: pointcloudTextGeom(ctx, width, height),
    color: "darkgreen",
    speed: speed,
    maxVelo: 100,
    fraction: 0.6
  }
  var targetOneMillion = {
    geom: oneMillionTextGeom(ctx, width, height),
    color: "darkpurple",
    speed: speed,
    maxVelo: 200,
    fraction: 0.6
  }
  var targetGaussian = {
    geom: gaussian(1/5),
    color: "darkblue",
    speed: 5 * speed,
    maxVelo: 50,
    fraction: 1
  }
  var targetGaussian2 = {
    geom: gaussian(100),
    color: "darkblue",
    speed: 0.2 * speed,
    maxVelo: 1000,
    fraction: 1
  }

  var currentIndex = 0
  var targets = [targetPlotly, targetPointcloud, targetOneMillion, targetGaussian, targetGaussian2]

  window.setInterval(function() {
    currentIndex = (currentIndex + 1) % targets.length
  }, 3000)

  function getIndex() {
    return currentIndex
  }

  window.requestAnimationFrame(function refresh() {
    var target = targets[getIndex()]
    recurrenceRelationGeom(target.geom, geom, target.speed, target.maxVelo, target.fraction)
    Plotly.restyle('myDiv', {marker:{color: target.color}/*,xy: geom*/}, 0) // /*no need to include xy: geom*/
    window.requestAnimationFrame(refresh)
  })
})