qianyuxin
qianyuxin

Reputation: 43

Find a point on an SVG path

I use d3js to draw a smooth curve. Then, I want to draw a point on the curve, but the point is random and I only have x value. I want get the function expression and get the y value with the x value. Is there any method to get the y value?

const line = d3.line()
  .x(d => xScale(new Date(d.name)))
  .y(d => yScale(d.value1))
  .curve(d3.curveCatmullRom);
const series = svg.append('g')
  .attr('transform', `translate(${grid.left},${grid.top})`)
  .append('path')
  .attr('d', line(data))
  .attr('fill', 'transparent')
  .attr('stroke-width', 2)
  .attr('stroke', 'orange');

My current chart: the chart I draw current

Upvotes: 4

Views: 1650

Answers (2)

Robin Mackenzie
Robin Mackenzie

Reputation: 19299

It's really a duplicate of this but I added a snippet as the post is several years old ...

const margin = 30;
const width = 400; 
const height = 180;
const chartWidth = width - (margin * 2);
const chartHeight = height - (margin * 2);

const data = Array.from({length: 10}, (v, i) => {
  return {
    index: i,
    value: Math.floor(Math.random() * 20) + 4
  }
});

const svg = d3.select("#viz")
  .append("svg")
  .attr("width", width)
  .attr("height", height);
  
const xScale = d3.scaleLinear()
  .domain(d3.extent(data, d => d.index))
  .range([0, chartWidth]);
  
svg.append("g")
  .attr("class", "x-axis")
  .attr("transform", `translate(${margin},${height - margin})`)
  .call(d3.axisBottom(xScale));
  
const yScale = d3.scaleLinear()
  .domain(d3.extent(data, d => d.value))
  .range([chartHeight, 0]);
  
svg.append("g")
  .attr("class", "y-axis")
  .attr("transform", `translate(${margin},${margin})`)
  .call(d3.axisLeft(yScale));
    
const line = d3.line()
  .x(d => xScale(d.index))
  .y(d => yScale(d.value))
  .curve(d3.curveCatmullRom);

const series = svg.append("g")
  .attr("transform", `translate(${margin},${margin})`)
  .append("path")
  .attr("d", line(data))
  .attr("fill", "transparent")
  .attr("stroke-width", 2)
  .attr("stroke", "orange");

const findYFromXLinearTime = (x, line) => {
  const getXYAtLength = len => {
    const pt = line.getPointAtLength(len);
    return {x: pt.x, y: pt.y};
  }
  let l = 0;
  while (getXYAtLength(l).x < x) l+=0.01;
  return getXYAtLength(l).y;
}

const findYFromXLogTime = (x, line) => {
  const error = 0.01;
  const iterMax = 50;
  let iter = 0;
  let start = 0;
  let end = line.getTotalLength();
  let point = line.getPointAtLength((end + start) / 2);
  while (x < point.x - error || x > point.x + error) {
    // update middle
    point = line.getPointAtLength((end + start) / 2);
    // test
    x < point.x ? end = (start + end) / 2 : start = (start + end ) / 2;
    // update iteration
    if (iterMax < ++ iter) break;
  }
  return point.y;
}

d3.select("#findY")
  .on("click", evt => {
    const x = document.getElementById("someX").value;
    const y = findYFromXLogTime(xScale(x), series.node());
    svg.append("circle")
      .attr("cx", xScale(x) + margin)
      .attr("cy", y + margin)
      .attr("r", 4)
      .attr("fill", "red")
      .attr("stroke", "black")
  });
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
<input id="someX" type="number">
<button id="findY" type="button">find Y</button>
<div id="viz"></div>

Upvotes: 1

Michael Rovinsky
Michael Rovinsky

Reputation: 7230

Here is a function that finds a point with specified x coordinate on a <path> (kind of binary search):

Note: The path should be monotonic on X (there must not be 2 points with the same x on the path)

const findPointAt = (path, x) => {
  let from = 0;
  let to = path.getTotalLength();
  let current = (from + to) / 2;
  let point = path.getPointAtLength(current);
  
  while (Math.abs(point.x - x) > 0.5) {
    if (point.x < x)
      from = current;
    else
      to = current;
    current = (from + to) / 2;
    point = path.getPointAtLength(current);
  }
  return point;
}


const path = d3.select('path').node();

for (let x = 0; x <= 200; x += 50) {
  const pos = findPointAt(path, x);
  console.log(pos);
  d3.select('svg').append('circle')
    .attr('cx', pos.x)
    .attr('cy', pos.y)
    .attr('r', 3)
}
svg {
  border: 1px solid gray;
}

path {
  fill: none;
  stroke: blue;
}

circle {
  fill: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

<svg width="200" height="150">
  <path d="M 0,10 Q 40,0 90,80 C 120,120 150,70 220,20" /> 
</svg>

Upvotes: 5

Related Questions