Last active
December 2, 2015 00:27
-
-
Save nitaku/f1ecc0fa3d042f8ad47a to your computer and use it in GitHub Desktop.
Tape chart (interactive)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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