Draw sine wave going around a circle I have used d3 to draw a sine wave going around a circle. This is the very first time I've used d3 or drawn a SVG and I'm fairly new to JS as well, so I don't know if I've overcomplicated it/if there is a simpler way to achieve this. I'd appreciate some feedback - especially if there's any way to make my code more concise.

See my codepen.



const svg = d3.select('svg');
const margin = { top: 50, right: 50, bottom: 50, left: 50 };
const width = +svg.attr('width') - margin.left - margin.right;
const height = +svg.attr('height') - margin.top - margin.bottom;

// content area of your visualization
const vis = svg
.append('g')
.attr('transform', `translate(${margin.left+width/2},${margin.top+height/2})`);

// show area inside of margins
const rect = vis
.append('rect')
.attr('class', 'content')
.attr('width', width)
.attr('height', height)
.attr('transform', `translate(${-width/2},${-height/2})`);

// show scales
const xScale = d3
.scaleLinear()
.domain([-100, 100])
.range([-width/2, width/2]);
const yScale = d3
.scaleLinear()
.domain([100, -100])
.range([-height/2, height/2]);
vis.append('g').call(d3.axisTop(xScale));
vis.append('g').call(d3.axisLeft(yScale));

// draw circle
const pi = Math.PI
const radius = 63.66
const circle = vis
.append('circle')
.style('stroke-dasharray', '3, 3')
.style('stroke', 'black')
.style("fill", "transparent")
.attr("r", xScale(radius))
.attr("cx", 0)
.attr("cy", 0)

// get coordinates for a sine wave
const getSineWave = ({ 
  numWaves, 
  wavelength,
  amplitude,
  phase,
  numPointsPerWave,
}) =&gt; {
  return (
    d3.range(numWaves*numPointsPerWave+1).map(function(k) {
      const x = k * wavelength/numPointsPerWave
      return [x, amplitude * Math.sin(phase + 2 * pi * x/wavelength)];
    })
  )
}

// tranform a coordinate from linear space to circular space
const rotate = (cx, cy, x, y, radius) =&gt; {
  const theta = x/radius,
        sin = Math.sin(theta),
        cos = Math.cos(theta),
        nx = cx + (radius + y) * sin,
        ny = cy + (radius + y) * cos
  return [nx, ny];
}

// generate sine wave
const numWaves = 4
const amplitude = 10
const phase = pi/2
const circumference = 2 * pi * radius
const wavelength = circumference / numWaves
const numPointsPerWave = 4
const sineWave = getSineWave({
  numWaves, 
  numPointsPerWave,
  wavelength,
  amplitude,
  phase,
  wavelength
})
var rotatedSine = sineWave.map( d =&gt; {
  const rotatedCoords = rotate(0, 0, d[0], d[1], radius)
  return rotatedCoords
})
// remove the last point as it would overlap the first point of the circle
rotatedSine.pop() 

// get Path commands for given coordinates
const getPath = d3.line()
.x(d =&gt; xScale(d[0]))
.y(d =&gt; yScale(d[1]))
.curve(d3.curveCardinalClosed) 

// draw sine wave going around a circle
const wave = vis
.append('path')
.attr('d', getPath(rotatedSine))
.attr('fill', 'none')
.attr('stroke', 'black')
.attr('stroke-width', '1px')
svg {
        background-color: steelblue;
    }
    
    .content {
        fill: lightsteelblue;
    }
&lt;script src="https://d3js.org/d3.v4.js"&gt;&lt;/script&gt;
&lt;svg width="1000" height="1000" &lt;/ svg&gt;
Overall you have a good D3 code here. I'm fairly impressed with the questions I've seen here at C.O. lately, from people claiming "This is the very first time I've used d3 or drawn a SVG". Congrats.

However, before sharing my proposed alternative, I'd like to tell you that, unfortunately, you're using the wrong tool for the task!

As you can see in my answer here, the problem is that D3 is designed to create visualizations based on data, normally qualitative or discrete quantitative data sets. According to Mike Bostock, D3 creator:


  D3 is designed primarily for data visualization, mostly empirical datasets rather than continuous functions, and so there is no built-in method for generating abscissa values. (emphasis mine)


As you can see in your case, the line gets better if you push more datapoint into the array, increasing any of the two constants in...

d3.range(numWaves*numPointsPerWave+1)


In your particular case we can get a good line with numPointsPerWave = 10, which is not a big problem... however, the advice remains: D3 is not the correct tool here, you should look for a proper plotting library. As you can see in the linked answer above, in some situations we have to increase the data points a lot to have a good looking graph.

D3 radial line

All that being said, here is my proposed alternative: instead of all that complicated math and 2 functions to set the path's d attribute, use a D3 radial line generator.

In this answer I'll focus only on the use of the radial line generator, nothing more. I'm sure that other users will soon post answers regarding your JavaScript code (use of functions, constants, destructuring, currying etc...) 

According to the API, d3.lineRadial():


  Constructs a new radial line generator with the default settings. A radial line generator is equivalent to the standard Cartesian line generator, except the x and y accessors are replaced with angle and radius accessors. Radial lines are always positioned relative to ⟨0,0⟩; use a transform (see: SVG, Canvas) to change the origin.


So, all we need is the line generator...

const radialGenerator = d3.lineRadial()
    .angle(d =&gt; d.angle)
    .radius(d =&gt; d.radius)
    .curve(d3.curveCardinalClosed);


And the adequate data:

const length = 100;
const amplitude = 20;
const radialScale = d3.scaleLinear()
  .domain([0, length])
  .range([0, Math.PI * 2]);

const data = d3.range(length).map(function(d) {
  return {
    angle: radialScale(d),
    radius: xScale(radius) + Math.sin(d) * amplitude
  }
});


Then, you append the path:

const wave = vis.append('path')
  .attr('d', radialGenerator(data))
  .attr('fill', 'none')
  .attr('stroke', 'black')
  .attr('stroke-width', '1px');


Have in mind that my function here is not as complex as yours, which accepts several different parameters: this answer is just to show you the existence of d3.lineRadial(), you can certainly improve it.

Here is the demo:



const svg = d3.select('svg');
const margin = {
  top: 50,
  right: 50,
  bottom: 50,
  left: 50
};
const width = +svg.attr('width') - margin.left - margin.right;
const height = +svg.attr('height') - margin.top - margin.bottom;

// content area of your visualization
const vis = svg.append('g')
  .attr('transform', `translate(${margin.left+width/2},${margin.top+height/2})`);

// show area inside of margins
const rect = vis.append('rect')
  .attr('class', 'content')
  .attr('width', width)
  .attr('height', height)
  .attr('transform', `translate(${-width/2},${-height/2})`);

// show scales
const xScale = d3.scaleLinear()
  .domain([-100, 100])
  .range([-width / 2, width / 2]);
const yScale = d3.scaleLinear()
  .domain([100, -100])
  .range([-height / 2, height / 2]);
vis.append('g').call(d3.axisTop(xScale));
vis.append('g').call(d3.axisLeft(yScale));

// draw circle
const pi = Math.PI
const radius = 63.66
const circle = vis.append('circle')
  .style('stroke-dasharray', '3, 3')
  .style('stroke', 'black')
  .style("fill", "transparent")
  .attr("r", xScale(radius))
  .attr("cx", 0)
  .attr("cy", 0);

const length = 100;
const amplitude = 20;

const radialGenerator = d3.lineRadial()
  .angle(d =&gt; d.angle)
  .radius(d =&gt; d.radius)
  .curve(d3.curveCardinalClosed)

const radialScale = d3.scaleLinear()
  .domain([0, length])
  .range([0, Math.PI * 2]);

const data = d3.range(length).map(function(d) {
  return {
    angle: radialScale(d),
    radius: xScale(radius) + Math.sin(d) * amplitude
  }
});

const wave = vis.append('path')
  .attr('d', radialGenerator(data))
  .attr('fill', 'none')
  .attr('stroke', 'black')
  .attr('stroke-width', '1px')
svg {
  background-color: steelblue;
}

.content {
  fill: lightsteelblue;
}
&lt;script src="https://d3js.org/d3.v5.min.js"&gt;&lt;/script&gt;
&lt;svg width="1000" height="1000"&gt;&lt;/svg&gt;