Как нарисовать изогнутую (интерполированную) линию точка за точкой с помощью D3? - PullRequest
0 голосов
/ 20 января 2020

Я пытаюсь реализовать что-то похожее на скачки во Флорие sh (https://app.flourish.studio/@flourish / horserace ).

Но я хочу сделать это изогнутыми линиями. Моя идея состоит в том, чтобы рисовать линии точка за точкой и выполнять дополнительную обработку при достижении новой точки. Проблема в том, что я не могу найти способ как это реализовать. Я пробовал подход с 'stroke-dashoffset' и вычисление текущего смещения, но переход, похоже, не работал должным образом.

Есть ли способ нарисовать линию точка за точкой, когда линия изогнута?

Примечание. Мой вопрос похож на существующий вопрос ( Анимировать путь (линию) из последней известной точки к новой добавленной точке (d3) )

У него был довольно хороший ответ, но проблема в том, что он работает только для прямых (не интерполированных) линий.

1 Ответ

1 голос
/ 26 января 2020

Сложность анимации изогнутой линии состоит в том, что вам нужно, чтобы координата x двигалась устойчивым образом, тогда как координата y будет зависеть от того, где координата x пересекает линию, а пути svg не имеют метода извлечения. координата y для любого значения x. Траектории SVG с линейными (ie прямые) кривыми могут использовать тригонометрию для расчета значения y в любой заданной точке, тогда как кривые требуют более сложных вычислений на основе генератора кривых, используемого D3.

Мой подход, приведенный ниже, анализирует каждую путь по всей его длине, и каждый раз, когда значение x координаты равно заданным c значениям, основанным на моей частоте дискретизации, я записываю значение y и длину и использую полученный массив, чтобы создать вид плавного перехода вдоль изогнутый путь.

Каждый путь выбирается 100 раз (и выборки показаны на диаграмме с красными кружками), но эту частоту выборки можно отрегулировать для лучшей производительности по сравнению с плавными переходами.

В этом подходе затем используется переход d3.t, чтобы обновить позицию круга и смещение da sh штриха для следующего элемента в массиве выборок, а в конце «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>
...