Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active December 2, 2015 00:27
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nitaku/f1ecc0fa3d042f8ad47a to your computer and use it in GitHub Desktop.
Save nitaku/f1ecc0fa3d042f8ad47a to your computer and use it in GitHub Desktop.
Tape chart (interactive)
# 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)
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;
}
<!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="http://d3js.org/d3.v3.min.js"></script>
</head>
<body>
<script src="index.js"></script>
</body>
</html>
(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);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment