Bret Victor's page https://worrydream.com/SimulationAsAPracticalTool/[1] shows a simulation of a skateboarder on a merry-go-round. The simulations are in Flash, and no longer work in 2021, since Flash has been removed from browsers. I wanted to try implementing this in JavaScript to see how it'd look.
There's a paper about Data Theater[2] that reproduces the simulations using a Python-based framework.
from math import radians, sin, cos disk = [150, 150] r = 50 rotation = 12 theta = radians(rotation) x = disk[0] + r * cos(theta) y = disk[1] - r * sin(theta) vel_x = -5 * sin(theta) vel_y = -5 * cos(theta) xs = [x] ys = [y] for _ in range(50): next_y = y + vel_y if next_y < 0: vel_y = -vel_y x += vel_x; y += vel_y xs.append(x); ys.append(y) x, y #break
Here's the JavaScript equivalent, which of course is a little more noisy because JavaScript has more visible syntax than Python:
function simulate(disk, r, rotation) { let theta = Math.PI / 180 * rotation; let x = disk[0] + r * Math.cos(theta), y = disk[1] - r * Math.sin(theta); let vel_x = -5 * Math.sin(theta), vel_y = -5 * Math.cos(theta); let history = [[x, y]]; for (let time = 0; time < 50; time++) { let next_y = y + vel_y; if (next_y < 0) { vel_y = -vel_y; } x += vel_x; y += vel_y; history.push([x, y]); } return {x, y, history}; }
I think other than the language differences, these two pieces of code are comparable.
Here's the data visualization description from the paper:
export default { disc: { type: 'ellipse', x: 'disk[0]', y: 'disk[1]', rx: 'r', ry: 'r', text: '"--------------"', rotate: '-rotation', }, path: { type: 'line', xs: 'xs', ys: 'ys', }, skateboard: { type: 'rect', dx: 'x', dy: 'y', color: '"#ff5858"', width: 10, height: 10, }, }
and here's the SVG equivalent:
<figure> <svg viewBox="-100 0 500 200"> <rect x="-100" width="500" height="2" fill="lightgray" /> <g :transform="`translate(${disk}) rotate(${-rotation})`"> <line :x2="r*1.5" fill="none" stroke="blue" stroke-width="2" /> <circle :r="r" fill="lightgray" stroke="black" stroke-width="2" /> <line :x1="-r" :x2="r" fill="none" stroke="black" stroke-dasharray="3" /> </g> <polyline :points="sim.history" fill="none" stroke="green" stroke-width="2" /> <rect :x="sim.x-5" :y="sim.y-5" :width="10" :height="10" fill="#ff5858" /> </svg> Angle: <input v-model="rotation" type="range" min="0" max="360" /> </figure>
If you already know SVG, the SVG version will make more sense, and if you already know Vega Lite, the JSON version will make more sense. I think this is a matter of familiarity, but neither one is better than the other.
Note the :foo="expr" syntax which is a computed value in Vue, like using =expr for formulas in spreadsheets or foo={expr} in React or foo=${expr} in Lit-html.
Here's the glue code where I tell Vue to find the <figure> and calculate the computed values. In Vue, v-model binds the slider to the rotation
field, so when you change the slider, the rotation updates, and the diagram is recomputed automatically, like spreadsheet formulas.
new Vue({ el: "figure", data: { disk: [150, 150], r: 50, rotation: 30, }, computed: { sim() { return simulate(this.disk, this.r, this.rotation); }, }, });
I'm a big fan of spreadsheets and wish more programming was like that.
I used a slider in this example because it's already built in but it might be nice to use direct manipulation of the angle. Vue would treat it the same; you can change rotation
any way you want and it will figure out the recalculation dependencies like a spreadsheet.
I continue to be frustrated that Flash no longer runs. So much content is now inaccessible. Bret's Flash version is so much flashier. It uses mouseover to set the rotation (instead of a slider). It has nice graphics on everything. It animates the skateboard so you can see it bounce off the wall. It animates the calculation of the plot (second diagram). I recorded a movie:
Here's a second diagram from the paper which I have not yet reproduced:
from math import radians, sin, cos disk = [150, 150] r = 50 rotations = [] times = [] for rotation in range(0, 60, 3): theta = radians(rotation) x = disk[0] + r * cos(theta) y = disk[1] - r * sin(theta) vel_x = -sin(theta) vel_y = -cos(theta) time = abs(y / vel_y) end_x = x + vel_x * time end_y = 0 rotations.append(rotation) times.append(time) continue #break
export default { disc: { type: 'ellipse', dx: 'disk[0]', dy: 'disk[1]', rx: 'r', ry: 'r', text: '"--------------"', rotate: '-rotation', }, path: { type: 'line', xs: '[x, end_x]', ys: '[y, end_y]', color: '"gray"', }, skateboard: { type: 'rect', dx: 'end_x', dy: 'end_y', color: '"#ff5858"', width: 10, height: 10, }, plot: { type: 'line', x: 100, y: 250, xs: 'rotations', ys: { value: 'times', scale: { type: 'linear', domain: [140, 200], range: [100, 0], }, }, }, }