text: vg.scene.axis = function() {
    var scale,
        orient = vg.config.axis.orient,
        offset = 0,
        titleOffset = vg.config.axis.titleOffset,
        axisDef = null,
        layer = "front",
        grid = false,
        title = null,
        tickMajorSize = vg.config.axis.tickSize,
        tickMinorSize = vg.config.axis.tickSize,
        tickEndSize = vg.config.axis.tickSize,
        tickPadding = vg.config.axis.padding,
        tickValues = null,
        tickFormatString = null,
        tickFormat = null,
        tickSubdivide = 0,
        tickArguments = [vg.config.axis.ticks],
        gridLineStyle = {},
        tickLabelStyle = {},
        majorTickStyle = {},
        minorTickStyle = {},
        titleStyle = {},
        domainStyle = {};
  
    var axis = {};
  
    function reset() {
      axisDef = null;
    }
  
    axis.def = function() {
      var def = axisDef ? axisDef : (axisDef = axis_def(scale));
  
      // tick format
      tickFormat = !tickFormatString ? null : ((scale.type === 'time')
        ? d3.time.format(tickFormatString)
        : d3.format(tickFormatString));
  
      // generate data
      var major = tickValues == null
        ? (scale.ticks ? scale.ticks.apply(scale, tickArguments) : scale.domain())
        : tickValues;
      var minor = vg_axisSubdivide(scale, major, tickSubdivide).map(vg.data.ingest);
      major = major.map(vg.data.ingest);
      var fmt = tickFormat==null ? (scale.tickFormat ? scale.tickFormat.apply(scale, tickArguments) : String) : tickFormat;
      major.forEach(function(d) { d.label = fmt(d.data); });
      var tdata = title ? [title].map(vg.data.ingest) : [];
      
      // update axis def
      def.marks[0].from = function() { return grid ? major : []; };
      def.marks[1].from = function() { return major; };
      def.marks[2].from = function() { return minor; };
      def.marks[3].from = def.marks[1].from;
      def.marks[4].from = function() { return [1]; };
      def.marks[5].from = function() { return tdata; };
      def.offset = offset;
      def.orient = orient;
      def.layer = layer;
      return def;
    };
  
    function axis_def(scale) {
      // setup scale mapping
      var newScale, oldScale, range;
      if (scale.type === "ordinal") {
        newScale = {scale: scale.scaleName, offset: 0.5 + scale.rangeBand()/2};
        oldScale = newScale;
      } else {
        newScale = {scale: scale.scaleName, offset: 0.5};
        oldScale = {scale: scale.scaleName+":prev", offset: 0.5};
      }
      range = vg_axisScaleRange(scale);
  
      // setup axis marks
      var gridLines = vg_axisTicks();
      var majorTicks = vg_axisTicks();
      var minorTicks = vg_axisTicks();
      var tickLabels = vg_axisTickLabels();
      var domain = vg_axisDomain();
      var title = vg_axisTitle();
      gridLines.properties.enter.stroke = {value: vg.config.axis.gridColor};
  
      // extend axis marks based on axis orientation
      vg_axisTicksExtend(orient, gridLines, oldScale, newScale, Infinity);
      vg_axisTicksExtend(orient, majorTicks, oldScale, newScale, tickMajorSize);
      vg_axisTicksExtend(orient, minorTicks, oldScale, newScale, tickMinorSize);
      vg_axisLabelExtend(orient, tickLabels, oldScale, newScale, tickMajorSize, tickPadding);
  
      vg_axisDomainExtend(orient, domain, range, tickEndSize);
      vg_axisTitleExtend(orient, title, range, titleOffset); // TODO get offset
      
      // add / override custom style properties
      vg.extend(gridLines.properties.update, gridLineStyle);
      vg.extend(majorTicks.properties.update, majorTickStyle);
      vg.extend(minorTicks.properties.update, minorTickStyle);
      vg.extend(tickLabels.properties.update, tickLabelStyle);
      vg.extend(domain.properties.update, domainStyle);
      vg.extend(title.properties.update, titleStyle);
  
      var marks = [gridLines, majorTicks, minorTicks, tickLabels, domain, title];
      return {
        type: "group",
        interactive: false,
        properties: { enter: vg_axisUpdate, update: vg_axisUpdate },
        marks: marks.map(vg.parse.mark)
      };
    }
  
    axis.scale = function(x) {
      if (!arguments.length) return scale;
      if (scale !== x) { scale = x; reset(); }
      return axis;
    };
  
    axis.orient = function(x) {
      if (!arguments.length) return orient;
      if (orient !== x) {
        orient = x in vg_axisOrients ? x + "" : vg.config.axis.orient;
        reset();
      }
      return axis;
    };
  
    axis.title = function(x) {
      if (!arguments.length) return title;
      if (title !== x) { title = x; reset(); }
      return axis;
    };
  
    axis.ticks = function() {
      if (!arguments.length) return tickArguments;
      tickArguments = arguments;
      return axis;
    };
  
    axis.tickValues = function(x) {
      if (!arguments.length) return tickValues;
      tickValues = x;
      return axis;
    };
  
    axis.tickFormat = function(x) {
      if (!arguments.length) return tickFormatString;
      if (tickFormatString !== x) {
        tickFormatString = x;
        reset();
      }
      return axis;
    };
    
    axis.tickSize = function(x, y) {
      if (!arguments.length) return tickMajorSize;
      var n = arguments.length - 1,
          major = +x,
          minor = n > 1 ? +y : tickMajorSize,
          end   = n > 0 ? +arguments[n] : tickMajorSize;
  
      if (tickMajorSize !== major ||
          tickMinorSize !== minor ||
          tickEndSize !== end) {
        reset();
      }
  
      tickMajorSize = major;
      tickMinorSize = minor;
      tickEndSize = end;
      return axis;
    };
  
    axis.tickSubdivide = function(x) {
      if (!arguments.length) return tickSubdivide;
      tickSubdivide = +x;
      return axis;
    };
    
    axis.offset = function(x) {
      if (!arguments.length) return offset;
      offset = vg.isObject(x) ? x : +x;
      return axis;
    };
  
    axis.tickPadding = function(x) {
      if (!arguments.length) return tickPadding;
      if (tickPadding !== +x) { tickPadding = +x; reset(); }
      return axis;
    };
  
    axis.titleOffset = function(x) {
      if (!arguments.length) return titleOffset;
      if (titleOffset !== +x) { titleOffset = +x; reset(); }
      return axis;
    };
  
    axis.layer = function(x) {
      if (!arguments.length) return layer;
      if (layer !== x) { layer = x; reset(); }
      return axis;
    };
  
    axis.grid = function(x) {
      if (!arguments.length) return grid;
      if (grid !== x) { grid = x; reset(); }
      return axis;
    };
  
    axis.gridLineProperties = function(x) {
      if (!arguments.length) return gridLineStyle;
      if (gridLineStyle !== x) { gridLineStyle = x; }
      return axis;
    };
  
    axis.majorTickProperties = function(x) {
      if (!arguments.length) return majorTickStyle;
      if (majorTickStyle !== x) { majorTickStyle = x; }
      return axis;
    };
  
    axis.minorTickProperties = function(x) {
      if (!arguments.length) return minorTickStyle;
      if (minorTickStyle !== x) { minorTickStyle = x; }
      return axis;
    };
  
    axis.tickLabelProperties = function(x) {
      if (!arguments.length) return tickLabelStyle;
      if (tickLabelStyle !== x) { tickLabelStyle = x; }
      return axis;
    };
  
    axis.titleProperties = function(x) {
      if (!arguments.length) return titleStyle;
      if (titleStyle !== x) { titleStyle = x; }
      return axis;
    };
  
    axis.domainProperties = function(x) {
      if (!arguments.length) return domainStyle;
      if (domainStyle !== x) { domainStyle = x; }
      return axis;
    };
    
    axis.reset = function() { reset(); };
  
    return axis;
  };
  
  var vg_axisOrients = {top: 1, right: 1, bottom: 1, left: 1};
  
  function vg_axisSubdivide(scale, ticks, m) {
    subticks = [];
    if (m && ticks.length > 1) {
      var extent = vg_axisScaleExtent(scale.domain()),
          subticks,
          i = -1,
          n = ticks.length,
          d = (ticks[1] - ticks[0]) / ++m,
          j,
          v;
      while (++i < n) {
        for (j = m; --j > 0;) {
          if ((v = +ticks[i] - j * d) >= extent[0]) {
            subticks.push(v);
          }
        }
      }
      for (--i, j = 0; ++j < m && (v = +ticks[i] + j * d) < extent[1];) {
        subticks.push(v);
      }
    }
    return subticks;
  }
  
  function vg_axisScaleExtent(domain) {
    var start = domain[0], stop = domain[domain.length - 1];
    return start < stop ? [start, stop] : [stop, start];
  }
  
  function vg_axisScaleRange(scale) {
    return scale.rangeExtent
      ? scale.rangeExtent()
      : vg_axisScaleExtent(scale.range());
  }
  
  var vg_axisAlign = {
    bottom: "center",
    top: "center",
    left: "right",
    right: "left"
  };
  
  var vg_axisBaseline = {
    bottom: "top",
    top: "bottom",
    left: "middle",
    right: "middle"
  };
  
  function vg_axisLabelExtend(orient, labels, oldScale, newScale, size, pad) {
    size = Math.max(size, 0) + pad;
    if (orient === "left" || orient === "top") {
      size *= -1;
    }  
    if (orient === "top" || orient === "bottom") {
      vg.extend(labels.properties.enter, {
        x: oldScale,
        y: {value: size},
      });
      vg.extend(labels.properties.update, {
        x: newScale,
        y: {value: size},
        align: {value: "center"},
        baseline: {value: vg_axisBaseline[orient]}
      });
    } else {
      vg.extend(labels.properties.enter, {
        x: {value: size},
        y: oldScale,
      });
      vg.extend(labels.properties.update, {
        x: {value: size},
        y: newScale,
        align: {value: vg_axisAlign[orient]},
        baseline: {value: "middle"}
      });
    }
  }
  
  function vg_axisTicksExtend(orient, ticks, oldScale, newScale, size) {
    var sign = (orient === "left" || orient === "top") ? -1 : 1;
    if (size === Infinity) {
      size = (orient === "top" || orient === "bottom")
        ? {group: "mark.group.height", mult: -sign}
        : {group: "mark.group.width", mult: -sign};
    } else {
      size = {value: sign * size};
    }
    if (orient === "top" || orient === "bottom") {
      vg.extend(ticks.properties.enter, {
        x:  oldScale,
        y:  {value: 0},
        y2: size
      });
      vg.extend(ticks.properties.update, {
        x:  newScale,
        y:  {value: 0},
        y2: size
      });
      vg.extend(ticks.properties.exit, {
        x:  newScale,
      });        
    } else {
      vg.extend(ticks.properties.enter, {
        x:  {value: 0},
        x2: size,
        y:  oldScale
      });
      vg.extend(ticks.properties.update, {
        x:  {value: 0},
        x2: size,
        y:  newScale
      });
      vg.extend(ticks.properties.exit, {
        y:  newScale,
      });
    }
  }
  
  function vg_axisTitleExtend(orient, title, range, offset) {
    var mid = ~~((range[0] + range[1]) / 2),
        sign = (orient === "top" || orient === "left") ? -1 : 1;
    
    if (orient === "bottom" || orient === "top") {
      vg.extend(title.properties.update, {
        x: {value: mid},
        y: {value: sign*offset},
        angle: {value: 0}
      });
    } else {
      vg.extend(title.properties.update, {
        x: {value: sign*offset},
        y: {value: mid},
        angle: {value: -90}
      });
    }
  }
  
  function vg_axisDomainExtend(orient, domain, range, size) {
    var path;
    if (orient === "top" || orient === "left") {
      size = -1 * size;
    }
    if (orient === "bottom" || orient === "top") {
      path = "M" + range[0] + "," + size + "V0H" + range[1] + "V" + size;
    } else {
      path = "M" + size + "," + range[0] + "H0V" + range[1] + "H" + size;
    }
    domain.properties.update.path = {value: path};
  }
  
  function vg_axisUpdate(item, group, trans) {
    var o = trans ? {} : item,
        offset = item.mark.def.offset,
        orient = item.mark.def.orient,
        width  = group.width,
        height = group.height; // TODO fallback to global w,h?
  
      if (vg.isArray(offset)) {
        var ofx = offset[0],
            ofy = offset[1];
  
        switch (orient) {
          case "left":   { o.x = -ofx; o.y = ofy; break; }
          case "right":  { o.x = width + ofx; o.y = ofy; break; }
          case "bottom": { o.x = ofx; o.y = height + ofy; break; }
          case "top":    { o.x = ofx; o.y = -ofy; break; }
          default:       { o.x = ofx; o.y = ofy; }
        }
      } else {
        if (vg.isObject(offset)) {
          offset = -group.scales[offset.scale](offset.value);
        }
  
        switch (orient) {
          case "left":   { o.x = -offset; o.y = 0; break; }
          case "right":  { o.x = width + offset; o.y = 0; break; }
          case "bottom": { o.x = 0; o.y = height + offset; break; }
          case "top":    { o.x = 0; o.y = -offset; break; }
          default:       { o.x = 0; o.y = 0; }
        }
      }
  
    if (trans) trans.interpolate(item, o);
  }
  
  function vg_axisTicks() {
    return {
      type: "rule",
      interactive: false,
      key: "data",
      properties: {
        enter: {
          stroke: {value: vg.config.axis.tickColor},
          strokeWidth: {value: vg.config.axis.tickWidth},
          opacity: {value: 1e-6}
        },
        exit: { opacity: {value: 1e-6} },
        update: { opacity: {value: 1} }
      }
    };
  }
  
  function vg_axisTickLabels() {
    return {
      type: "text",
      interactive: true,
      key: "data",
      properties: {
        enter: {
          fill: {value: vg.config.axis.tickLabelColor},
          font: {value: vg.config.axis.tickLabelFont},
          fontSize: {value: vg.config.axis.tickLabelFontSize},
          opacity: {value: 1e-6},
          text: {field: "label"}
        },
        exit: { opacity: {value: 1e-6} },
        update: { opacity: {value: 1} }
      }
    };
  }
  
  function vg_axisTitle() {
    return {
      type: "text",
      interactive: true,
      properties: {
        enter: {
          font: {value: vg.config.axis.titleFont},
          fontSize: {value: vg.config.axis.titleFontSize},
          fontWeight: {value: vg.config.axis.titleFontWeight},
          fill: {value: vg.config.axis.titleColor},
          align: {value: "center"},
          baseline: {value: "middle"},
          text: {field: "data"}
        },
        update: {}
      }
    };
  }
  
  function vg_axisDomain() {
    return {
      type: "path",
      interactive: false,
      properties: {
        enter: {
          x: {value: 0.5},
          y: {value: 0.5},
          stroke: {value: vg.config.axis.axisColor},
          strokeWidth: {value: vg.config.axis.axisWidth}
        },
        update: {}
      }
    };
  }
  