WebGL charts

I wanted to see how hard it would be to make a WebGL chart. Here’s the code with comments. The code is written in CoffeeScript.

class ThreeJsPrb
  initCSR: ->
    width = 800
    height = 600
    viewAngle = 45
    aspect = width / height
    near = 0.1
    far = 20000
    
    @camera = new THREE.PerspectiveCamera viewAngle, aspect, near, far
    @camera.position.z = 15000
    
    @scene = new THREE.Scene
    @scene.add @camera

    @renderer = new THREE.WebGLRenderer
      antialias: true
      premultipliedAlpha: false
    @renderer.setSize width, height
    
    $('<div>')
      .append(@renderer.domElement)
      .appendTo(document.body)

We are making a class called ThreeJsPrb. The first method is called initCSR, short for init camera, scene and renderer. WebGL needs these three:

  • Camera to define where we are looking from
  • Scene that defines what the world contains
  • Rendered to render all this

In the above:

  • We define a few variables defining dimensions of the renderer, view angle aspect and near / far z-index
  • Setup a camera and it’s position
  • Setup a scene and add a camera to it
  • Setup a renderer, with some options (antialias and premultipliedAlpha) and set it’s weight
  • Finally, add the renderer to dom using jQuery
  draw: =>
    @render()
    setTimeout(() =>
        requestAnimationFrame @draw
      , 1000 / @fps)

This method is the main “loop” of this app. It will render the current scene, then use a combination of setTimeout and requestAnimationFrame to repeat this in the future. The combination in this case is necessary to be able to set a relatively constant fps that doesn’t max out the fps all the time.

  render: =>
    if Math.abs(@group.rotation.y) > Math.PI / 8
      @rot *= -1
    @group.rotation.y += @rot
    @renderer.render @scene, @camera

The render method itself does two things:

  • Applies a see-saw-behaving rotation (i.e. rotate right up to Math.PI / 8 radians, then reverse the direction)
  • Renders the scene using the given camera
  # http://en.wikipedia.org/wiki/World_population#Population_growth_by_region
  worldPopulation: {
         1: 200
      1000: 310
      1750: 791
      1800: 978
      1850: 1262
      1900: 1650
      1950: 2519
      1955: 2756
      1960: 2982
      1965: 3335
      1970: 3692
      1975: 4068
      1980: 4435
      1985: 4831
      1990: 5263
      1995: 5674
      2000: 6070
      2005: 6454
      2010: 6972
    }

This is just a sample chart data, taken from Wikipedia.

  makeScene: ->
    material = new THREE.MeshLambertMaterial
      color: 0x80b2ff
        
    @group = new THREE.Object3D
    @scene.add @group
    
    @graph = new THREE.Object3D
    @graph.position.y -= 3000
    @group.add @graph
    
    xpos = 0
    
    radius = 200
    for k, v of @worldPopulation
      radiusTop = radius
      radiusBottom = radius
      height = v
      radiusSegments = 50
      heightSegments = 50
      openEnded = false
      cylinderGeometry = new THREE.CylinderGeometry radiusTop, radiusBottom, height, radiusSegments, heightSegments, openEnded

      cylinder = new THREE.Mesh cylinderGeometry, material
      cylinder.position.x = xpos
      cylinder.position.y = v / 2
      xpos += radius * 3
      @graph.add cylinder
      
    @graph.position.x = -xpos / 2
    
    @rot = 0.01
    @group.rotation.x = Math.PI / 8
    
    directionalLight = new THREE.SpotLight 0xffffff
    directionalLight.position.set -xpos, 1000, 3000
    directionalLight.angle = Math.PI / 2
    directionalLight.target.position.set xpos / 3, 0, 0
    @scene.add directionalLight

This is the meat of the app. The above code is responsible for setting up the scene. As our scene doesn’t change except for rotation, this is done only once. Here’s what’s going on:

  • We define a lambert material of a given bluish color
  • We define a Object3D called group. This will be used for easier around-the-center rotation later
  • We create another Object3D called graph and add it to the group. We also move it “down” by setting the y-axis position
  • For each of the keys in worldPopulation map, we create a cylinder geometry and a mesh out of this and the material we made above. We add that to the graph
  • We move the graph within it’s parent element (group) to be split in the middle along x-axis. Again, this is to achieve around-the-center rotation later
  • We setup the rotation step
  • We add a direction light
  run: ->
    console.log '> ThreeJsPrb.run'

    @initCSR()
    @makeScene()
    
    @fps = 15
    @draw()
    
    console.log '< ThreeJsPrb.run'

module.exports = ThreeJsPrb

Finally, the initialization code and module export.

Here’s a screenshot:

screenshot