I'm trying to implement something similiar like horse race in flourish (https://app.flourish.studio/@flourish/horserace).
But I want to do it with curved lines. My idea is to draw lines point by point and do some extra processing whenever new point is reached. The problem is that I do not find any way how to implement this.. I've tried approach with 'stroke-dashoffset' and calculating current offset, but transition did not seem to work properly.
Is there any way to draw line point by point when line is curved?
Note: My question is similar to an existing question (Animate path (line) from last known point to new added point (d3))
It had quite a good answer, but the problem is that it only works for straight ( non-interpolated ) lines.
The challenge with animating a curved line is that you need the x coordinate to move in a steady fashion, while the y coordinate will depend on the where the x coordinate crosses the line, and svg paths do not have a method of extracting the y coordinate for any x value. SVG paths with linear (ie straight) curves can use trigonometry to calculate the y value at any given point, whereas curves requires more complicated calculations based on the curve generator used by D3.
My approach below samples each path along its whole length, and for every time the coordinate's x value equals a specific values based on my sample rate, I record the y value and length, and use the resulting array to give the appearance of a smooth transition along the curved path.
Each path is sampled 100 times (and the samples are shown on the chart with the red circles), but this sample rate can be adjusted for better performance vs smooth transitions.
The approach then uses a d3.transition to update the circle's position and stroke's dash offset to the next element in the sample array, and on the transition's 'end', calls the transition again for the the next element in the sample array. The code aligns the sample arrays with the original path using the index (i).
var w = 700;
var h = 300;
var m = 40;
var max = 10
var numberOfSeries = 3
var svg = d3.select("#chart")
.append("svg")
.attr("width", w + m + m)
.attr("height", h + m + m)
var chart = svg.append("g")
.attr("transform", "translate(" + m + "," + m + ")")
var data = []
for (var a = 0; a < numberOfSeries; a++) {
data.push([])
for (var i = 0; i <= max; i++) {
data[a].push(Math.random() * max)
}
}
var x = d3.scaleLinear()
.domain([0, max])
.range([0, w]);
var y = d3.scaleLinear()
.domain([0, max])
.range([h, 0]);
var line = d3.line()
.x(function(d,i) {return x(i);})
.y(function(d) {return y(d);})
.curve(d3.curveCardinal)
var series = chart.selectAll(".series")
.data(data)
.enter()
.append("g")
var bkdPath = series.append("path")
.attr("d", d => line(d))
.style("stroke", "lightgrey")
var path = series.append("path")
.attr("d", d => line(d))
.attr("id", (d, i) => "path-" + i)
.style("stroke", "orange")
.style("stroke-width", "5px")
var bkdCircle = series.selectAll(".bkd-circle")
.data(d => d)
.enter()
.append("g")
.attr("transform", function(d, i){
return "translate(" + x(i) + "," + y(d) + ")"
})
.attr("class", "bkd-circle")
.append("circle")
.attr("r", 5)
.style("stroke", "lightgrey")
.style("fill", "white")
var dataPoint = series.selectAll('.data-point')
.data(d => d)
.enter()
.append("g")
.attr("transform", function(d, i){
return "translate(" + x(i) + "," + y(d) + ")"
})
.attr("class", "data-point")
.attr("id", (d, i) => "data-point-" + i)
.style("opacity", 0)
dataPoint.append("circle")
.attr("r", 5)
dataPoint.append("text")
.text((d, i) => i + ", " + round2dp(d) )
.attr("dy", 18)
let pointArray = []
let sampleRate = 100
let sampleWidth = w / sampleRate
for (var p = 0; p < numberOfSeries; p++) {
pointArray.push([])
let currentLength = 0
let pathID = "#path-" + p
let thisPath = d3.select(pathID)
let node = thisPath.node()
let pathLength = node.getTotalLength()
let s = 0
thisPath.attr("stroke-dasharray", pathLength + " " + pathLength)
.attr("stroke-dashoffset", pathLength)
for (var j = 0; j<pathLength; j++){
let point = node.getPointAtLength(j)
//console.log(point)
if (point.x >= (sampleWidth * s)) {
pointArray[p].push({"x": point.x, "y": point.y, "len": j})
s = s + 1
}
}
pointArray[p].push({"x": w, "y": y(data[p][data[p].length-1]), "len": pathLength})
}
let transitionElements = chart.selectAll(".t-elements")
.data(pointArray)
.enter()
.append("g")
transitionElements.selectAll(".markers")
.data(d => d)
.enter()
.append("circle")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", 2)
.style("fill", "red")
let head = transitionElements.append("circle")
.datum(d => d)
.attr("cx", d => d[0].x)
.attr("cy", d => d[0].y)
.attr("r", 15)
.style("fill", "green")
.attr("id", "head")
let tIndex = 0
let dur = 50000
function transitionChart(){
tIndex = tIndex + 1
if (tIndex >= (sampleRate+1)) {
} else {
path.transition()
.duration(dur / (sampleRate + 1))
.ease(d3.easeLinear)
.attr("stroke-dashoffset", function(d,i){
let len = d3.select(this).node().getTotalLength()
return len -pointArray[i][tIndex].len
})
head.transition()
.duration(dur / (sampleRate + 1))
.ease(d3.easeLinear)
.attr("cx", (d,i) => pointArray[i][tIndex].x)
.attr("cy", (d,i) => pointArray[i][tIndex].y)
.on("end", transitionChart)
}
}
transitionChart()
function round2dp(n) {
return Number.parseFloat(n).toFixed(2);
}
path {
stroke-width: 2px;
fill: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<body>
<div id='chart'></div>
</body>