0

I have created a spiral visualisation with d3js. Live example here I'm trying to create (basically to highlight) subsections of the spiral. The spiral is defined as follow:

const start = 0
const end = 2.25

const theta = function(r) {
  return numSpirals * Math.PI * r
}

const radius = d3.scaleLinear()
  .domain([start, end])
  .range([20, r])

const spiral = d3.radialLine()
  .curve(d3.curveCardinal)
  .angle(theta)
  .radius(radius)

const points = d3.range(start, end + 0.001, (end - start) / 1000)

const path = g.append("path")
  .datum(points)
  .attr("id", "spiral")
  .attr("d", spiral)
  .style("fill", "none")
  .style("stroke", "steelblue");

Some random dots are placed in the spiral using something like:

  const positionScale = d3.scaleLinear()
      .domain(d3.extent(mockedData, (d, i) => i))
      .range([0, spiralLength]);

  const circles = g.selectAll("circle")
    .data(mockedData)
    .enter()
    .append("circle")
    .attr("cx", (d,i) => {
      const linePos = positionScale(i)
      const posOnLine = path.node().getPointAtLength(linePos)

      d.cx = posOnLine.x
      d.cy = posOnLine.y

      return d.cx;
    })
   .attr("cy", d => d.cy)
   .attr("r", d => circleRadiusScale(d.value))

If the start and end of the spiral is [0, 2.25] it is easy to get a subsection of the spiral from 0 to 1 by creating a new set of points from 0 to 1 (instead of 0 to 2.25):

const pointSubSpiral = d3.range(0, 1 + 0.001, 1 / 1000)

The problem I have is when I try to create a subsection of the spiral based on data points. For example, from 0 to the position of point 3. A linear scale does not work:

const spiralSectionScale = d3.scaleLinear()
  .range([start, end])
  .domain([0, mockedData.length])

const spiralEnd = spiralSectionScale(i)
const sectionPoints = d3.range(0, 1 + 0.001, 1 / 1000)

const path2 = g.append("path")
    .datum(sectionPoints)
    .attr("id", "spiral-section")
    .attr("d", spiral)
    .style("fill", "none")
    .style("stroke", "red")
    .style("stroke-width", "1.2em")
    .style("opacity", "0.2")
    .style("pointer-events", "none")

Is there any way to convert values in the data domain to the spiral domain?

UPDATE: Based on @rioV8's answer below I have managed to get the sections of the spiral working. Basically by creating a binary search on the radius of the node:

  function findSpiralSection(targetRadius, start, end) {
    const endSection = (end + start) / 2
    const endSectionRadius = radius(endSection)

    if (Math.abs(targetRadius - endSectionRadius) < 0.1) {
      return endSection 
    }

    if ((targetRadius - endSectionRadius) > 0) {
      return findSpiralSection(targetRadius, endSection, end)
    } else {
      return findSpiralSection(targetRadius, start, endSection)
    }
  }

  function higlightSubSpiral(d, i) {
    const linePos = positionScale(i);
    const targetNode = path.node().getPointAtLength(linePos);
    const nodeRadius = Math.sqrt((targetNode.x * targetNode.x) + (targetNode.y * targetNode.y))

    const bestEndSection = findSpiralSection(nodeRadius, start, end);

    const sectionPoints = d3.range(0, bestEndSection + 0.001, bestEndSection / 1000)

    const path2 = g.append("path")
        .datum(sectionPoints)
        .attr("id", "spiral-section")
        .attr("d", spiral)
        .style("fill", "none")
        .style("stroke", "red")
        .style("stroke-width", "1.2em")
        .style("opacity", "0.2")
        .style("pointer-events", "none")
  }
emepyc
  • 949
  • 1
  • 10
  • 23

1 Answers1

1

your spiral is cut in ~1000 pieces, each piece has an equal delta-angle but with a varying radius. Thus each segment has a different length.

You position the circles at regular length intervals (total_length/99) along the spiral. They do not correspond with a linear cumulative angle because the segment length differ.

You have to use a binary search to find the needed end-angle value.

spiral highlight

It should work for the final point so there is a problem in spiralSectionScale. In has a range [0, mockedData.length], this is 1 too big

var spiralSectionScale = d3.scaleLinear()
  .range([start, end])
  .domain([0, mockedData.length-1]);

The original positionScale has a convoluted way of calculating the domain. This is more readable and faster.

const positionScale = d3.scaleLinear()
    .domain([0, mockedData.length-1])
    .range([0, spiralLength]);
rioV8
  • 24,506
  • 3
  • 32
  • 49
  • Thanks, you have described the issue pretty well. I'm still unsure on how to create the binary search, What is the final value the search should be approaching? Could you please elaborate a bit more? – emepyc Nov 08 '18 at 22:08
  • I have managed to get a version working (using binary search on the radius). I have updated the link in the original post. Thanks again for the pointer! – emepyc Nov 08 '18 at 23:26