Ilya V. Schurov - 6 months ago 60

Javascript Question

TL;DR: I wrote plotly-based javascript simulation of mathematical pendulum. It works very slow. I'm looking for ideas on how to optimize it. Currently trying "bare" d3.js and struggling from the problem of coordinate transformation between SVG's coordinates and my own logical coordinates.

I'm writing web-textbook on ordinary differential equations and want to include interactive simulation and visualization of mathematical pendulum. Visualization should contain pendulum itself, its potential energy graph and full energy contour plot. Then user can choose the initial condition by clicking on energy contour plot, then animation should begin showing how the point moves in the phase space.

I wrote an example of such simulation:

https://jsfiddle.net/ischurov/p1krqnt6/

It's plotly-based. I create three axes and put there necessary graphs. The point that represent current state of the system is also a graph in plotly (i.e. scatter plot with the only one point).

Animation works as follows: I get current coordinates of the point in the phase space, calculate the new position of this point after some time, then update my graph according to this new position. The corresponding code:

`var div = document.getElementById('myDiv');`

function updateState(phi, v) {

var update = {x: [[phi], [phi], [0, Math.sin(phi)]], y: [[v],

[PotentialEnergy(phi)], [0, -Math.cos(phi)]]};

Plotly.restyle(div, update, [phaseDotIndex, 3, 4]);

}

myPlot.on('plotly_click', function(data){

if(data.points[0].data.type == 'contour'){

updateState(data.points[0].x, data.points[0].y);

}

});

var animate = null;

$('.animate_button').click(function(){

var div = document.getElementById('myDiv');

if(animate === null) {

var phi = div.data[phaseDotIndex].x[0],

v = div.data[phaseDotIndex].y[0],

E = FullEnergy(phi, v);

animate = setInterval(function() {

var phi = div.data[phaseDotIndex].x[0],

v = div.data[phaseDotIndex].y[0],

step = 0.1, newphi, newv, update;

newphi = phi + v * step;

newv = v + step * Force(phi);

/* skip some tweaks here */

updateState(phi, v);

},

100)

}

else

{

clearInterval(animate);

animate = null;

}

}

This code works almost as expected, but really slow and not smooth — at least, under Firefox (If I decrease update interval it works even worse).

I'm looking for ways to optimize this.

I believe that performance problems are due to plotly's update process: in order to move one point it have to recalculate the whole picture and it is slow. So I'm looking for ways to do it in different way.

Are there any ideas?

I'm looked for some direct d3.js approach which can be faster. I see the following steps here:

- Draw a graph of potential energy and contour plot of full energy.
- Draw the pendulum itself.
- Put small circles on the graphs of potential energy and contour plot.
- Make 'onclick' event handler to allow user to choose the initial state.
- Run animation loop by updating the position of the circles and the pendulum according to current state.

To proceed with step 1, I can use third-party d3.js libraries like conrec for contour plots and/or excellent maurizzzio's function plot or even plotly itself (but I'm not going to use plotly to update the graph). Step 2 seem to be doable, but I didn't try it yet. The most difficult for now are steps 3 and 4 as I don't understand how to transform SVG coordinates into my graph's coordinates (that are plotted with some library) and vice-versa.

Or maybe there are more simple ways to do it?

Answer

I'm the author of function plot which is built on top of d3, luckily d3 has methods to perform mappings in d3-scale so assuming that you have a canvas of `width x height`

dimensions which should be mapped linearly to the rectangle `[xMin, yMin] x [xMax, yMax]`

in 2D euclidean space you'd need to create two linear scales

```
var xScale = d3.scale.linear()
.domain([xMin, xMax])
.range([0, width])
var yScale = d3.scale.linear()
.domain([yMin, yMax])
.range([height, 0])
```

Note that in SVG the y axis is flipped and because of that the `yScale`

range's flipped too, then any 2D euclidean point is transformed to SVG coordinates as follows

```
var xCanvas = xScale(point.x)
var yCanvas = yScale(point.y)
```

The inverse transformation is given by

```
var xLogical = xScale.invert(point.x)
var xLogical = yScale.invert(point.y)
```

A possible solution I wrote to your problem using the above is

```
var instance = functionPlot({
target: '#demo',
disableZoom: true,
data: [{
fn: 'sin(10*(-cos(x) + y^2/2-1))',
fnType: 'implicit'
}]
})
var xScale = instance.meta.xScale
var yScale = instance.meta.yScale
var canvas = instance.canvas
var circle = canvas.append('circle')
.attr("r", 5)
.style("fill", "purple")
var start = Date.now()
function animate() {
// move the point along the circle of radius 1
var t = (Date.now() - start) * 0.003
var xLogical = Math.cos(t)
var yLogical = Math.sin(t)
var xCanvas = xScale(xLogical)
var yCanvas = yScale(yLogical)
circle
.attr('cx', xCanvas)
.attr('cy', yCanvas)
requestAnimationFrame(animate)
}
animate()
```

```
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://maurizzzio.github.io/function-plot/js/function-plot.js"></script>
<div id="demo"></div>
```