block by nitaku 8f96bf393b94caff688b

Isometric projection

Full Screen

A fancy (and not that useful) visualization of random data in two dimensions as an isometric bar chart. Notice how the phenomenon of occlusion makes part of the dataset invisible.

index.js

// Generated by CoffeeScript 1.4.0
(function() {
  var L, PAD, data, enter_pipedons, height, iso_layout, isometric, parallelepipedon, path_generator, pipedons, svg, vis, width, y_color;

  svg = d3.select('svg');

  width = svg.node().getBoundingClientRect().width;

  height = svg.node().getBoundingClientRect().height;

  vis = svg.append('g').attr({
    transform: "translate(" + (width / 2) + "," + (height / 2) + ")"
  });

  isometric = function(_3d_p) {
    return [-Math.sqrt(3) / 2 * _3d_p[0] + Math.sqrt(3) / 2 * _3d_p[1], +0.5 * _3d_p[0] + 0.5 * _3d_p[1] - _3d_p[2]];
  };

  parallelepipedon = function(d) {
    var fb, ft, mlb, mlt, mrb, mrt, nb, nt;
    if (!(d.x != null)) {
      d.x = 0;
    }
    if (!(d.y != null)) {
      d.y = 0;
    }
    if (!(d.z != null)) {
      d.z = 0;
    }
    if (!(d.dx != null)) {
      d.dx = 10;
    }
    if (!(d.dy != null)) {
      d.dy = 10;
    }
    if (!(d.dz != null)) {
      d.dz = 10;
    }
    fb = isometric([d.x, d.y, d.z], mlb = isometric([d.x + d.dx, d.y, d.z], nb = isometric([d.x + d.dx, d.y + d.dy, d.z], mrb = isometric([d.x, d.y + d.dy, d.z], ft = isometric([d.x, d.y, d.z + d.dz], mlt = isometric([d.x + d.dx, d.y, d.z + d.dz], nt = isometric([d.x + d.dx, d.y + d.dy, d.z + d.dz], mrt = isometric([d.x, d.y + d.dy, d.z + d.dz]))))))));
    d.iso = {
      face_bottom: [fb, mrb, nb, mlb],
      face_left: [mlb, mlt, nt, nb],
      face_right: [nt, mrt, mrb, nb],
      face_top: [ft, mrt, nt, mlt],
      outline: [ft, mrt, mrb, nb, mlb, mlt],
      far_point: fb
    };
    return d;
  };

  iso_layout = function(data, shape, scale) {
    if (!(scale != null)) {
      scale = 1;
    }
    data.forEach(function(d) {
      return shape(d, scale);
    });
    return data.sort(function(a, b) {
      return a.iso.far_point[1] - b.iso.far_point[1];
    });
  };

  path_generator = function(d) {
    return 'M' + d.map(function(p) {
      return p.join(' ');
    }).join('L') + 'z';
  };

  y_color = d3.scale.category10();

  L = 30;

  PAD = 6;

  data = d3.range(6 * 6).map(function(i) {
    return {
      x: (i % 6) * L,
      y: Math.floor(i / 6) * L,
      dx: L - PAD,
      dy: L - PAD,
      dz: 10 + Math.random() * 6 * L
    };
  });

  iso_layout(data, parallelepipedon);

  pipedons = vis.selectAll('.pipedon').data(data);

  enter_pipedons = pipedons.enter().append('g').attr({
    "class": 'pipedon'
  });

  enter_pipedons.append('path').attr({
    "class": 'iso face bottom',
    d: function(d) {
      return path_generator(d.iso.face_bottom);
    }
  });

  enter_pipedons.append('path').attr({
    "class": 'iso face left template',
    d: function(d) {
      return path_generator(d.iso.face_left);
    },
    fill: function(d) {
      return y_color(d.y);
    }
  });

  enter_pipedons.append('path').attr({
    "class": 'iso face right',
    d: function(d) {
      return path_generator(d.iso.face_right);
    },
    fill: function(d) {
      var color;
      color = d3.hcl(d3.select(this.parentNode).select('.template').style('fill'));
      return d3.hcl(color.h, color.c, color.l - 12);
    }
  });

  enter_pipedons.append('path').attr({
    "class": 'iso face top',
    d: function(d) {
      return path_generator(d.iso.face_top);
    },
    fill: function(d) {
      var color;
      color = d3.hcl(d3.select(this.parentNode).select('.template').style('fill'));
      return d3.hcl(color.h, color.c, color.l + 12);
    }
  });

  enter_pipedons.append('path').attr({
    "class": 'iso outline',
    d: function(d) {
      return path_generator(d.iso.outline);
    }
  });

}).call(this);

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Isometric Projection</title>
    <link type="text/css" href="index.css" rel="stylesheet"/>
    <script src="//d3js.org/d3.v3.min.js"></script>
  </head>
  <body>
    <svg width="960px" height="500px"></svg>
    <script src="index.js"></script>
  </body>
</html>

index.coffee

svg = d3.select('svg')
width = svg.node().getBoundingClientRect().width
height = svg.node().getBoundingClientRect().height

vis = svg.append('g')
  .attr
    transform: "translate(#{width/2},#{height/2})"

# [x, y, z] -> [-Math.sqrt(3)/2*x+Math.sqrt(3)/2*y, 0.5*x+0.5*y-z]
isometric = (_3d_p) -> [-Math.sqrt(3)/2*_3d_p[0]+Math.sqrt(3)/2*_3d_p[1], +0.5*_3d_p[0]+0.5*_3d_p[1]-_3d_p[2]]

parallelepipedon = (d) ->
  d.x = 0 if not d.x?
  d.y = 0 if not d.y?
  d.z = 0 if not d.z?
  d.dx = 10 if not d.dx?
  d.dy = 10 if not d.dy?
  d.dz = 10 if not d.dz?

  fb = isometric [d.x, d.y, d.z],
  mlb = isometric [d.x+d.dx, d.y, d.z],
  nb = isometric [d.x+d.dx, d.y+d.dy, d.z],
  mrb = isometric [d.x, d.y+d.dy, d.z],
  ft = isometric [d.x, d.y, d.z+d.dz],
  mlt = isometric [d.x+d.dx, d.y, d.z+d.dz],
  nt = isometric [d.x+d.dx, d.y+d.dy, d.z+d.dz],
  mrt = isometric [d.x, d.y+d.dy, d.z+d.dz]

  d.iso = {
    face_bottom: [fb, mrb, nb, mlb],
    face_left: [mlb, mlt, nt, nb],
    face_right: [nt, mrt, mrb, nb],
    face_top: [ft, mrt, nt, mlt],
    outline: [ft, mrt, mrb, nb, mlb, mlt]
    far_point: fb # used to control the z-index of iso objects
  }

  return d

iso_layout = (data, shape, scale) ->
  scale = 1 if not scale?

  data.forEach (d) ->
    shape(d, scale)

  # data must be drawn from farthest to nearest
  data.sort (a,b) -> a.iso.far_point[1] - b.iso.far_point[1]

path_generator = (d) -> 'M' + d.map((p)->p.join(' ')).join('L') + 'z'

y_color = d3.scale.category10()


L = 30
PAD = 6
data = d3.range(6*6).map (i) -> {
    x: (i%6)*L,
    y: Math.floor(i/6)*L,
    dx: L-PAD,
    dy: L-PAD,
    dz: 10+Math.random()*6*L
  }

iso_layout(data, parallelepipedon)

pipedons = vis.selectAll('.pipedon')
  .data(data)

enter_pipedons = pipedons.enter().append('g')
  .attr
    class: 'pipedon'

enter_pipedons.append('path')
  .attr
    class: 'iso face bottom'
    d: (d) -> path_generator(d.iso.face_bottom)

enter_pipedons.append('path')
  .attr
    class: 'iso face left template'
    d: (d) -> path_generator(d.iso.face_left)
    fill: (d) ->
      # color the template face according to y
      return y_color(d.y)

enter_pipedons.append('path')
  .attr
    class: 'iso face right'
    d: (d) -> path_generator(d.iso.face_right)
    fill: (d) ->
      # right face is darker than the template (left face)
      color = d3.hcl(d3.select(this.parentNode).select('.template').style('fill'))
      return d3.hcl(color.h, color.c, color.l-12)

enter_pipedons.append('path')
  .attr
    class: 'iso face top'
    d: (d) -> path_generator(d.iso.face_top)
    fill: (d) ->
      # right face is brighter than the template (left face)
      color = d3.hcl(d3.select(this.parentNode).select('.template').style('fill'))
      return d3.hcl(color.h, color.c, color.l+12)

enter_pipedons.append('path')
  .attr
    class: 'iso outline'
    d: (d) -> path_generator(d.iso.outline)

index.css

.iso.face.bottom {
  display: none;
}

.iso.outline {
  stroke: white;
  fill: none;
}