Reputation: 251
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
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
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