Brian
Brian

Reputation: 251

d3 - sine wave with circles, the problem is that circles are overlapped in curves

I have around 75 data points. Each of the points are represented into a circle and aligned along with sine wave. Normal sine wave is horizontal but I want it to be vertical way. The first point starts at half of width.

The problem is the circles in curves are overlapped. I have tried to make some minor edits in terms of height, radius and so forth but cannot avoid the overlaps compeltely.

I haven't developed a function to calculate circle locations but the locations are set as below:

    .attr("cx", function(d,i){ return (Math.sin(i/7)) * width/2.3 + width /2})
    .attr("cy", function(d,i){ return (i + 0.5) * (height-margin.bottom-margin.top) / len(data); })

The height and width of svg are set to 1,000. They could be longer and radius of the circles could be smaller if the ways could avoid overlaps. But I bleieve there would be a better and elegant way to fit the circles in the same height and with the same radius. For example, the width of wave could be smaller so that more waves could fit in the same height and then circles could be more spread around. But to be honest, I have no idea how to calculate this.

would be much appreciated if you can help me out.

Upvotes: 0

Views: 581

Answers (2)

Tom Shanley
Tom Shanley

Reputation: 1787

An alternative solution, which would avoid overlaps, but which is slower, is to recursively adjust the radius of the circles and draw a set of circles to see if the number that fits on the sine curve equals your desired number (eg 75).

This code snippet starts with a radius (10), draws as many circles as can fit on the sine path, and if its not 75, then adjusts the radius and tries again.

let height = 800
    let width =  height
    let margin = 50
    
    let noOfCircles = 75
    
    let sineData = d3.range(0, 101).map(function(k) {
      let freq = 0.05
      var value = [freq * k * Math.PI, Math.sin(freq * k * Math.PI)];
      return value;
    });
    
    let sineX = d3.scaleLinear()
    	.domain([-1, 1])
    	.range([0, width])
		
    let sineY = d3.scaleLinear()
    	.domain(d3.extent(sineData, function(d) {
   			 return d[0]
      }))
    	.range([0, height])
      
   	let line = d3.line()
      .x(function(d) {
        return sineX(d[1]);
      })
      .y(function(d) {
        return sineY(d[0]);
      })
    	.curve(d3.curveLinear)
        
    var svg = d3.select("body").append("svg")
      .attr("width", width + margin + margin)
      .attr("height", height + margin + margin)

    var g = svg.append('g')
    	.attr('transform', 'translate(' + margin + ',' + margin + ')')
    
    var sinePath = g.append('path')
    	.datum(sineData)
    	.attr('d', line)
      .style('stroke', 'black')
      .style('stroke-width', 1)
      .style('fill', 'none')
    

    let node = sinePath.node()
    let pathLength = Math.floor(node.getTotalLength())

    
    var circleX = function(i){
      return node.getPointAtLength(i).x
    }

    var circleY = function(i){
      return node.getPointAtLength(i).y
    }

	let direction = "TBC"
	let magnitude = 20
  
    
    createCircles(10)
    
    function createCircles(radius){
      
      g.selectAll('circle').remove()
      
      let circleID = 0
    	let prevCircleX = circleX(0)
    	let prevCircleY = circleY(0)
    
      appendCircle(prevCircleX, prevCircleY, radius, circleID)

      for (var l = 1; l < pathLength; l++) {

        let thisCircleX = circleX(l)
        let thisCircleY = circleY(l)

        let sideX = Math.abs(thisCircleX - prevCircleX)
        let sideY = Math.abs(thisCircleY - prevCircleY)

        let hyp = Math.sqrt((sideX * sideX) + (sideY * sideY))

        if (hyp > (radius * 2)) {

            circleID = circleID + 1
            prevCircleX = thisCircleX
            prevCircleY = thisCircleY
            appendCircle(prevCircleX, prevCircleY, radius, circleID)

        }
        
    	} 

			console.log(circleID) 
      
      if (circleID != 75) {
   
        let newDirection = circleID < 75 ? "down" : "up"
        magnitude = direction == newDirection ? magnitude : magnitude/10
        direction = newDirection 
        radius = circleID < 75 ? radius - magnitude  : radius + magnitude 
        circleID = 0
        createCircles(radius)
     }
      
    }                      
   

    
    function appendCircle(x, y, r, id) {
      g.append('circle')
            .attr('id', 'circle-' + id)
      			.attr('r', r)
            .attr("cx", x )
            .attr("cy", y )
      			.style('fill', 'grey')
    				.style('opacity', 0.5)
    }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

Upvotes: 1

Tom Shanley
Tom Shanley

Reputation: 1787

To reduce the overlaps at the extremes of the sine curve, I would not use the sine calculation directly to position the circles. Instead, if you draw sine curve, and then use that path to position the circles at regular intervals, the overlaps are reduced. Depending on the length of the path and radius of the circles, there still may be some.

For example, see the embedded code snippet.

The snippet draws the sine path using generated data (adjust the range and freq to draw curves with different lenghts and number of curves).

The curve is drawn, and the length of the node's path is used to determine the x and y for each circle:

let height = 800
    let width =  height
    let margin = 50
    
    let sineData = d3.range(0, 101).map(function(k) {
      let freq = 0.05
      var value = [freq * k * Math.PI, Math.sin(freq * k * Math.PI)];
      return value;
    });
    
    let sineX = d3.scaleLinear()
    	.domain([-1, 1])
    	.range([0, width])
		
    let sineY = d3.scaleLinear()
    	.domain(d3.extent(sineData, function(d) {
   			 return d[0]
      }))
    	.range([0, height])
      
   	let line = d3.line()
      .x(function(d) {
        return sineX(d[1]);
      })
      .y(function(d) {
        return sineY(d[0]);
      })
    	.curve(d3.curveLinear)
        
    var svg = d3.select("body").append("svg")
      .attr("width", width + margin + margin)
      .attr("height", height + margin + margin)

    var g = svg.append('g')
    	.attr('transform', 'translate(' + margin + ',' + margin + ')')
    
    var sinePath = g.append('path')
    	.datum(sineData)
    	.attr('d', line)
      .style('stroke', 'black')
      .style('stroke-width', 1)
      .style('fill', 'none')
    
    let n = 150
    let circleData = d3.range(0, n).map(function(k) { return k })
    let node = sinePath.node()
  
    let scaleLength = d3.scaleLinear()
      .domain([0, n-1])
      .range([0, node.getTotalLength()])
    
    var circleX = function(i){
      return node.getPointAtLength(scaleLength(i)).x
    }

    var circleY = function(i){
      return node.getPointAtLength(scaleLength(i)).y
    }
    
    g.selectAll('circle')
    	.data(circleData)
    	.enter()
    	.append('circle')
    	.style('fill', 'grey')
    	.style('opacity', 0.5)
    	.attr('r', 10)
      .attr("cx", function(d, i){ return circleX(i) })
      .attr("cy", function(d, i){ return circleY(i) })
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

Upvotes: 2

Related Questions