block by nitaku f1ecc0fa3d042f8ad47a

Tape chart (interactive)

Full Screen

-

index.js

(function() {
  var D, END, HEIGHT, LIN_WIDTH, SAMPLES, START, STEP, WIDTH, a, b, data, drag, lin_end, lin_start, pinch_offset, redraw, spiral, svg, time;

  data = d3.range(0, 100, 1).concat([100]);

  data = data.map(function(t) {
    var d;

    d = {
      t: t
    };
    if (t % 10 === 0) {
      d.highlight = true;
    }
    if (t === 42) {
      d.answer = true;
    }
    return d;
  });

  WIDTH = 960;

  HEIGHT = 500;

  svg = d3.select('body').append('svg').attr({
    width: WIDTH,
    height: HEIGHT
  }).append('g').attr({
    transform: "translate(" + (WIDTH / 2) + ",160)"
  });

  a = 0;

  D = 40;

  b = D / (2 * Math.PI);

  SAMPLES = 80;

  svg.append('line').attr({
    "class": 'my_axis debug',
    x1: -WIDTH,
    x2: WIDTH
  });

  svg.append('line').attr({
    "class": 'my_axis debug',
    y1: -HEIGHT,
    y2: HEIGHT
  });

  spiral = svg.append('path').attr({
    "class": 'spiral'
  });

  START = 0;

  END = 100;

  STEP = 0.1;

  time = d3.range(START, END + STEP, STEP).map(function(t) {
    return {
      t: t
    };
  });

  LIN_WIDTH = 500;

  lin_start = 30;

  lin_end = 44;

  spiral = svg.append('path').attr({
    "class": 'spiral'
  });

  redraw = function() {
    var delta, delta_theta_l, delta_theta_r, dots, line_generator, radius_l, radius_r, spiral_layout, t2l, t2lr, t2ltheta, t2rr, t2rtheta, theta_max_l, theta_max_r;

    t2l = d3.scale.linear().domain([lin_start, lin_end]).range([-LIN_WIDTH / 2, LIN_WIDTH / 2]);
    delta = t2l(1) - t2l(0);
    theta_max_l = Math.sqrt(delta * (lin_start - START) / b);
    radius_l = a + b * theta_max_l;
    delta_theta_l = delta / radius_l;
    t2ltheta = d3.scale.linear().domain([START, lin_start]).range([-Math.PI / 2 - theta_max_l, -Math.PI / 2]);
    t2lr = d3.scale.linear().domain([START, lin_start]).range([0, radius_l]);
    theta_max_r = Math.sqrt(delta * (END - lin_end) / b);
    radius_r = a + b * theta_max_r;
    delta_theta_r = delta / radius_r;
    t2rtheta = d3.scale.linear().domain([lin_end, END]).range([-Math.PI / 2, -Math.PI / 2 + theta_max_r]);
    t2rr = d3.scale.linear().domain([lin_end, END]).range([radius_r, 0]);
    spiral_layout = function(d) {
      if (d.t < lin_start) {
        d.theta = t2ltheta(d.t);
        d.r = t2lr(d.t);
        d.x = -LIN_WIDTH / 2 + d.r * Math.cos(d.theta);
        d.y = radius_l + d.r * Math.sin(d.theta);
        return d;
      }
      if (d.t <= lin_end) {
        d.x = t2l(d.t);
        d.y = 0;
        return d;
      }
      d.theta = t2rtheta(d.t);
      d.r = t2rr(d.t);
      d.x = +LIN_WIDTH / 2 + d.r * Math.cos(d.theta);
      d.y = radius_r + d.r * Math.sin(d.theta);
      return d;
    };
    line_generator = d3.svg.line().x(function(d) {
      return d.x;
    }).y(function(d) {
      return d.y;
    }).interpolate('linear');
    spiral.datum(time.map(spiral_layout)).attr({
      d: line_generator
    });
    dots = svg.selectAll('.dot').data(data.map(spiral_layout));
    dots.enter().append('circle').attr({
      "class": 'dot'
    });
    dots.attr({
      cx: function(d) {
        return d.x;
      },
      cy: function(d) {
        return d.y;
      },
      r: function(d) {
        if (d.highlight || d.answer) {
          return 4;
        } else {
          return 2;
        }
      },
      fill: function(d) {
        if (d.answer) {
          return 'rgb(231, 41, 138)';
        } else {
          return 'rgb(27, 158, 119)';
        }
      }
    });
    svg.append('circle').attr({
      "class": 'radius_indicator debug',
      cx: -LIN_WIDTH / 2,
      cy: radius_l,
      r: radius_l
    });
    return svg.append('circle').attr({
      "class": 'radius_indicator debug',
      cx: LIN_WIDTH / 2,
      cy: radius_r,
      r: radius_r
    });
  };

  redraw();

  pinch_offset = null;

  drag = d3.behavior.drag().on('drag', function() {
    var delta, offset, t2l;

    t2l = d3.scale.linear().domain([lin_start, lin_end]).range([-LIN_WIDTH / 2, LIN_WIDTH / 2]);
    delta = t2l(1) - t2l(0);
    if (pinch_offset == null) {
      pinch_offset = d3.event.x;
    }
    offset = (d3.event.x - pinch_offset) / delta;
    if (lin_start - offset >= START && lin_end - offset <= END) {
      lin_start -= offset;
      lin_end -= offset;
    }
    pinch_offset = d3.event.x;
    return redraw();
  }).on('dragend', function() {
    return pinch_offset = null;
  });

  svg.append('rect').attr({
    "class": 'overlay',
    width: WIDTH,
    height: D * 3,
    x: -WIDTH / 2,
    y: -D * 3 / 2
  }).call(drag);

}).call(this);

index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="description" content="Interactive tape chart" />
  <title>Interactive tape chart</title>
  <link rel="stylesheet" href="index.css">
  <script src="//d3js.org/d3.v3.min.js"></script>
</head>
<body>
  <script src="index.js"></script>
</body>
</html>

index.coffee

# DATA
data = d3.range(0,100,1).concat([100])
data = data.map (t) ->
  d = {t: t}
  
  if t % 10 is 0
    d.highlight = true
    
  if t is 42
    d.answer = true
    
  return d
  

WIDTH = 960
HEIGHT = 500

svg = d3.select('body').append('svg')
  .attr
    width: WIDTH
    height: HEIGHT
  .append('g')
    .attr
      transform: "translate(#{WIDTH/2},160)"
      
      
# WARNING the following assumes a=0
a = 0
D = 40
b = D/(2*Math.PI)

# angle samples per turn (not good for large spirals!)
SAMPLES = 80

# draw axes
svg.append('line')
  .attr
    class: 'my_axis debug'
    x1: -WIDTH
    x2: WIDTH
    
svg.append('line')
  .attr
    class: 'my_axis debug'
    y1: -HEIGHT
    y2: HEIGHT
    
spiral = svg.append('path')
  .attr
    class: 'spiral'
    
START = 0
END = 100
STEP = 0.1
time = d3.range(START, END+STEP, STEP).map (t) -> {t: t}

LIN_WIDTH = 500

lin_start = 30
lin_end = 44

spiral = svg.append('path')
  .attr
    class: 'spiral'
    
redraw = () ->
    t2l = d3.scale.linear()
      .domain([lin_start, lin_end])
      .range([-LIN_WIDTH/2, LIN_WIDTH/2])
      
    # delta = LIN_WIDTH / (lin_end-lin_start)
    delta = t2l(1) - t2l(0)

    # left spiral
    theta_max_l = Math.sqrt(delta*(lin_start-START)/b) # a = 0
    radius_l = a + b*theta_max_l
    delta_theta_l = delta / radius_l
    t2ltheta = d3.scale.linear()
      .domain([START,lin_start])
      .range([-Math.PI/2-theta_max_l, -Math.PI/2])
    t2lr = d3.scale.linear()
      .domain([START,lin_start])
      .range([0,radius_l])

    # right spiral
    theta_max_r = Math.sqrt(delta*(END-lin_end)/b) # a = 0
    radius_r = a + b*theta_max_r
    delta_theta_r = delta / radius_r
    t2rtheta = d3.scale.linear()
      .domain([lin_end,END])
      .range([-Math.PI/2, -Math.PI/2+theta_max_r])
    t2rr = d3.scale.linear()
      .domain([lin_end,END])
      .range([radius_r,0])

    spiral_layout = (d) ->
        if d.t < lin_start
          # left spiral
          #theta = t * delta_theta_l
          
          # PI/2 + theta_max is subtracted from theta to have the spiral end at the top
          #r = a + b*theta
          #theta = theta-Math.PI/2-theta_max_l
          
          # y is translated by radius to have the spiral end at the top
          # x is translated by LIN_WIDTH/2 to match the spiral with the line
          #return {t: t, theta: theta, r: r, x: -LIN_WIDTH/2 + r*Math.cos(theta), y: radius_l + r*Math.sin(theta)}
          d.theta = t2ltheta(d.t)
          d.r = t2lr(d.t)
          d.x = -LIN_WIDTH/2 + d.r*Math.cos(d.theta)
          d.y = radius_l + d.r*Math.sin(d.theta)
          return d
        
        if d.t <= lin_end
          # line
          d.x = t2l(d.t)
          d.y = 0
          return d
          
        # else
        # right spiral
        #theta = (t-lin_end-STEP) * delta_theta_r
        
        # PI/2 + theta_max is subtracted from theta to have the spiral end at the top
        #r = a + b*theta
        #theta = theta-Math.PI/2-theta_max_r
        
        # y is translated by radius to have the spiral end at the top
        # x is translated by LIN_WIDTH/2 to match the spiral with the line
        d.theta = t2rtheta(d.t)
        d.r = t2rr(d.t)
        d.x = +LIN_WIDTH/2 + d.r*Math.cos(d.theta)
        d.y = radius_r + d.r*Math.sin(d.theta)
        return d
        
    # draw the spiral
    line_generator = d3.svg.line()
      .x((d) -> d.x)
      .y((d) -> d.y)
      .interpolate('linear')

    spiral
      .datum(time.map spiral_layout)
      .attr
        d: line_generator

    dots = svg.selectAll('.dot')
        .data(data.map spiral_layout)
    
    dots.enter().append('circle')
        .attr
          class: 'dot'
          
    dots
      .attr
        cx: (d) -> d.x
        cy: (d) -> d.y
        r: (d) -> if d.highlight or d.answer then 4 else 2
        fill: (d) -> if d.answer then 'rgb(231, 41, 138)' else 'rgb(27, 158, 119)'
          
    svg.append('circle')
        .attr
          class: 'radius_indicator debug'
          cx: -LIN_WIDTH/2
          cy: radius_l
          r: radius_l
          
    svg.append('circle')
        .attr
          class: 'radius_indicator debug'
          cx: LIN_WIDTH/2
          cy: radius_r
          r: radius_r

redraw()

# define a drag behavior to let the user roll/unroll the spirals
pinch_offset = null

drag = d3.behavior.drag()
  .on 'drag', () ->
      t2l = d3.scale.linear()
        .domain([lin_start, lin_end])
        .range([-LIN_WIDTH/2, LIN_WIDTH/2])
        
      # delta = LIN_WIDTH / (lin_end-lin_start)
      delta = t2l(1) - t2l(0)
      
      if not pinch_offset?
          pinch_offset = d3.event.x
            
      offset = (d3.event.x-pinch_offset)/delta
      
      if lin_start - offset >= START and lin_end - offset <= END
        lin_start -= offset
        lin_end -= offset
      
      pinch_offset = d3.event.x
      
      redraw()
  .on 'dragend', () ->
      pinch_offset = null
      
svg.append('rect')
  .attr(
    class: 'overlay'
    width: WIDTH
    height: D*3
    x: -WIDTH/2
    y: -D*3/2
  ).call(drag)

index.css

svg {
  background-color: white;
}
.spiral {
  fill: none;
  stroke: #DDD;
  stroke-width: 2px;
}
.my_axis {
  fill: none;
  stroke: lightgray;
  stroke-dasharray: 24 6 2 6;
  shape-rendering: crispEdges;
}
.radius_indicator {
  fill: none;
  stroke: gray;
  stroke-dasharray: 3 6;
}
.dot {
  stroke: black;
  stroke-width: 0.5;
}

.axis {
  font: 10px sans-serif;
  -webkit-user-select: none;
  -moz-user-select: none;
  user-select: none;
}
.axis .domain {
  fill: none;
  stroke: #000;
  stroke-opacity: .3;
  stroke-width: 10px;
  stroke-linecap: round;
}
.axis .halo {
  fill: none;
  stroke: #ddd;
  stroke-width: 8px;
  stroke-linecap: round;
}
.slider .handle {
  fill: #fff;
  stroke: #000;
  stroke-opacity: .5;
  stroke-width: 1.25px;
  pointer-events: none;
}

.debug {
  display: none;
}

.overlay {
  fill: transparent;
}