如何用 D3 逐点绘制曲线(插值)线?
How to draw curved ( interpolated ) line point by point with D3?
我正在尝试实现类似于赛马的东西(https://app.flourish.studio/@flourish/horserace)。
但我想用曲线来做。我的想法是逐点画线,并在到达新点时做一些额外的处理。问题是我找不到任何方法来实现这个。我已经尝试使用 'stroke-dashoffset' 方法并计算当前偏移量,但转换似乎无法正常工作。
有什么方法可以在曲线弯曲的情况下逐点画线吗?
注意:我的问题与现有问题相似(Animate path (line) from last known point to new added point (d3))
它有一个很好的答案,但问题是它只适用于直线(非插值)。
动画曲线的挑战在于您需要 x 坐标以稳定的方式移动,而 y 坐标将取决于 x 坐标与线的交叉点,而 svg 路径没有方法提取任何 x 值的 y 坐标。具有线性(即直线)曲线的 SVG 路径可以使用三角函数计算任何给定点的 y 值,而曲线需要根据 D3 使用的曲线生成器进行更复杂的计算。
我下面的方法沿其整个长度对每条路径进行采样,每次坐标的 x 值等于基于我的采样率的特定值时,我记录 y 值和长度,并使用结果数组给出沿弯曲路径出现平滑过渡。
每条路径采样 100 次(样本在图表中用红色圆圈显示),但可以调整此采样率以获得更好的性能与平滑过渡。
然后该方法使用 d3.transition 将圆的位置和笔划的破折号偏移更新到示例数组中的下一个元素,并在转换的 'end' 上再次调用转换示例数组中的下一个元素。该代码使用索引 (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>
我正在尝试实现类似于赛马的东西(https://app.flourish.studio/@flourish/horserace)。
但我想用曲线来做。我的想法是逐点画线,并在到达新点时做一些额外的处理。问题是我找不到任何方法来实现这个。我已经尝试使用 'stroke-dashoffset' 方法并计算当前偏移量,但转换似乎无法正常工作。
有什么方法可以在曲线弯曲的情况下逐点画线吗?
注意:我的问题与现有问题相似(Animate path (line) from last known point to new added point (d3))
它有一个很好的答案,但问题是它只适用于直线(非插值)。
动画曲线的挑战在于您需要 x 坐标以稳定的方式移动,而 y 坐标将取决于 x 坐标与线的交叉点,而 svg 路径没有方法提取任何 x 值的 y 坐标。具有线性(即直线)曲线的 SVG 路径可以使用三角函数计算任何给定点的 y 值,而曲线需要根据 D3 使用的曲线生成器进行更复杂的计算。
我下面的方法沿其整个长度对每条路径进行采样,每次坐标的 x 值等于基于我的采样率的特定值时,我记录 y 值和长度,并使用结果数组给出沿弯曲路径出现平滑过渡。
每条路径采样 100 次(样本在图表中用红色圆圈显示),但可以调整此采样率以获得更好的性能与平滑过渡。
然后该方法使用 d3.transition 将圆的位置和笔划的破折号偏移更新到示例数组中的下一个元素,并在转换的 'end' 上再次调用转换示例数组中的下一个元素。该代码使用索引 (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>