LLWW
LLWW

Reputation: 33

Use D3 to create a contour/polar graph

Using D3, I want to create a polar graph like this figure.

Following the volcano example, I can get the contour correctly rendered.

How do I translate the contours onto a circle to create a polar graph?

Update:

The initial data I'm using is a 2d array, where the first dimension (array[n]) represents period and the second dimension (array[n][0]) represents force. See below for a sample of data and the code I'm using to draw the contour graph.

function draw(spectrumData: Swell["spectrum"]): void {
    if (!spectrumData) {
      console.warn("spectrumData is undefined");
      return;
    }

    const { spectrum, width, height } = transformSpectrum(spectrumData);
    const sizeFactor = 13;

    const svg = d3
      .select("#spectrum-contour")
      .attr("width", width * sizeFactor)
      .attr("height", height * sizeFactor);

    // Ret the contour size
    var contour = d3Contour.contours().size([width, height]);

    // Returns "rgb(x, y, z)"
    const interpolator = (t: number): string => {
      const i0 = d3Hsv.interpolateHsvLong(
        d3Hsv.hsv(240, 0.77, 0.73),
        d3Hsv.hsv(114, 0.77, 0.73)
      );

      const i1 = d3Hsv.interpolateHsvLong(
        d3Hsv.hsv(114, 0.77, 0.73),
        d3Hsv.hsv(0, 0.77, 0.73)
      );
      return t < 0.2 ? i0(t * 2) : i1((t - 0.2) * 2);
    };

    const colorScale = d3Scale
      .scaleSequential(interpolator)
      // extent returns [min, max]
      .domain(d3.extent(spectrum));

    const path: any = d3Geo.geoPath(
      d3Geo.geoIdentity().scale((width * sizeFactor) / width)
    );

    // draw the contours
    svg
      .selectAll("path")
      .data(contour(spectrum))
      .enter()
      .append("path")
      .attr("d", (feature) => path(feature))
      .attr("fill", (d) => colorScale(d.value));
}
// Transform the 2D spectrum array to a 1D array
// and replace negative values with zeros.
function transformSpectrum(data: Swell["spectrum"]): TransformedSpectrum {
  const array1d = ([] as number[]).concat.apply([], data);
  return {
    width: data[0].length,
    height: data.length,
    spectrum: array1d.map((i) => (i >= 0 ? i : 0)),
  };
}
"spectrum": [
          [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ],
          [ 0.0002, 0.0003, 0.0003, 0.0003, 0.0004, 0.0004, 0.0004, 0.0004, 0.0004, 0.0004, 0.0004, 0.0003, 0.0003, 0.0003, 0.0003, 0.0002, 0.0002, 0.0002, 0.0002, 0.0001, 0.0001, 0.0001, 0.0001, 0.0001, 0.0001, 0.0002, 0.0002, 0.0002, 0.0003, 0.0003, 0.0003, 0.0004, 0.0004, 0.0005, 0.0005, 0.0006, 0.0006, 0.0007, 0.0007, 0.0007 ],
          [ 0.0001, 0.0001, 0.0002, 0.0002, 0.0003, 0.0003, 0.0004, 0.0004, 0.0005, 0.0005, 0.0005, 0.0005, 0.0005, 0.0005, 0.0005, 0.0005, 0.0005, 0.0005, 0.0005, 0.0005, 0.0005, 0.0004, 0.0004, 0.0004, 0.0004, 0.0004, 0.0004, 0.0004, 0.0004, 0.0004, 0.0005, 0.0005, 0.0005, 0.0006, 0.0006, 0.0007, 0.0007, 0.0007, 0.0008, 0.0008 ],
          [ 0.0003, 0.0003, 0.0002, 0.0002, 0.0001, 0.0001, 0.0001, 0.0001, 0.0001, 0.0002, 0.0002, 0.0003, 0.0004, 0.0005, 0.0006, 0.0007, 0.0009, 0.001, 0.0011, 0.0013, 0.0014, 0.0015, 0.0016, 0.0017, 0.0018, 0.0018, 0.0018, 0.0019, 0.0018, 0.0018, 0.0018, 0.0017, 0.0016, 0.0015, 0.0014, 0.0013, 0.0011, 0.001, 0.0008, 0.0007 ],
          [ 0.0007, 0.0007, 0.0006, 0.0006, 0.0006, 0.0005, 0.0005, 0.0005, 0.0005, 0.0005, 0.0005, 0.0005, 0.0006, 0.0006, 0.0007, 0.0008, 0.0009, 0.001, 0.0011, 0.0012, 0.0013, 0.0015, 0.0016, 0.0016, 0.0017, 0.0018, 0.0018, 0.0019, 0.0019, 0.0018, 0.0018, 0.0018, 0.0017, 0.0016, 0.0015, 0.0014, 0.0012, 0.0011, 0.001, 0.0008 ],
          [ 0.0007, 0.0006, 0.0005, 0.0004, 0.0004, 0.0003, 0.0002, 0.0002, 0.0002, 0.0002, 0.0002, 0.0002, 0.0003, 0.0004, 0.0005, 0.0006, 0.0007, 0.0008, 0.001, 0.0012, 0.0013, 0.0015, 0.0017, 0.0018, 0.002, 0.0021, 0.0022, 0.0023, 0.0023, 0.0023, 0.0024, 0.0023, 0.0023, 0.0022, 0.0021, 0.002, 0.0019, 0.0017, 0.0016, 0.0014 ],
          [ 0.0001, 0.0001, 0.0002, 0.0002, 0.0003, 0.0004, 0.0006, 0.0007, 0.0009, 0.0011, 0.0013, 0.0016, 0.0018, 0.0021, 0.0023, 0.0026, 0.0029, 0.0032, 0.0034, 0.0037, 0.0039, 0.0041, 0.0042, 0.0044, 0.0045, 0.0045, 0.0046, 0.0046, 0.0045, 0.0044, 0.0043, 0.0042, 0.004, 0.0038, 0.0036, 0.0034, 0.0031, 0.0029, 0.0026, 0.0024 ],
          [ 0.0014, 0.001, 0.0006, 0.0003, 0.0, -0.0002, -0.0003, -0.0003, -0.0001, 0.0003, 0.0008, 0.0015, 0.0024, 0.0035, 0.0047, 0.0061, 0.0076, 0.0092, 0.0109, 0.0126, 0.0143, 0.016, 0.0175, 0.019, 0.0203, 0.0214, 0.0224, 0.023, 0.0235, 0.0236, 0.0235, 0.0231, 0.0224, 0.0215, 0.0204, 0.019, 0.0175, 0.0158, 0.014, 0.0122 ],
          [ 0.0036, 0.0023, 0.0009, -0.0005, -0.0019, -0.0033, -0.0045, -0.0054, -0.006, -0.0062, -0.0061, -0.0054, -0.0043, -0.0027, -0.0005, 0.0021, 0.0052, 0.0086, 0.0124, 0.0165, 0.0207, 0.0251, 0.0294, 0.0336, 0.0376, 0.0413, 0.0447, 0.0475, 0.0498, 0.0516, 0.0526, 0.0531, 0.0528, 0.0519, 0.0503, 0.0481, 0.0453, 0.0421, 0.0385, 0.0345 ],
          [ 0.0031, 0.0022, 0.0013, 0.0003, -0.0008, -0.0018, -0.0027, -0.0035, -0.0041, -0.0045, -0.0046, -0.0044, -0.0039, -0.0029, -0.0016, 0.0, 0.002, 0.0043, 0.0069, 0.0098, 0.0128, 0.0159, 0.0191, 0.0222, 0.0253, 0.0282, 0.0309, 0.0333, 0.0353, 0.0369, 0.038, 0.0387, 0.0389, 0.0386, 0.0378, 0.0366, 0.0349, 0.0328, 0.0304, 0.0277 ],
          [ 0.001, -0.0001, -0.0012, -0.0022, -0.0032, -0.0039, -0.0045, -0.0047, -0.0047, -0.0044, -0.0038, -0.0027, -0.0014, 0.0003, 0.0022, 0.0044, 0.0069, 0.0095, 0.0122, 0.015, 0.0177, 0.0204, 0.0229, 0.0252, 0.0272, 0.0289, 0.0302, 0.0311, 0.0316, 0.0316, 0.0312, 0.0303, 0.029, 0.0274, 0.0254, 0.0231, 0.0205, 0.0178, 0.015, 0.0121 ],
          [ -0.0002, -0.0006, -0.001, -0.0014, -0.0017, -0.0019, -0.002, -0.0019, -0.0018, -0.0015, -0.0011, -0.0005, 0.0001, 0.0009, 0.0019, 0.0029, 0.0039, 0.0051, 0.0062, 0.0073, 0.0085, 0.0095, 0.0105, 0.0114, 0.0121, 0.0127, 0.0131, 0.0134, 0.0135, 0.0134, 0.0131, 0.0126, 0.012, 0.0113, 0.0104, 0.0094, 0.0084, 0.0072, 0.0061, 0.005 ],
          [ 0.0003, 0.0, -0.0004, -0.0007, -0.001, -0.0013, -0.0015, -0.0016, -0.0016, -0.0015, -0.0012, -0.0009, -0.0004, 0.0002, 0.0009, 0.0017, 0.0026, 0.0035, 0.0045, 0.0056, 0.0067, 0.0077, 0.0087, 0.0096, 0.0105, 0.0112, 0.0119, 0.0123, 0.0127, 0.0128, 0.0128, 0.0127, 0.0124, 0.0119, 0.0113, 0.0105, 0.0097, 0.0088, 0.0078, 0.0067 ],
          [ -0.0003, -0.0004, -0.0005, -0.0005, -0.0005, -0.0005, -0.0004, -0.0003, -0.0002, 0.0, 0.0002, 0.0005, 0.0008, 0.0011, 0.0014, 0.0018, 0.0021, 0.0025, 0.0028, 0.0032, 0.0035, 0.0038, 0.004, 0.0042, 0.0044, 0.0045, 0.0046, 0.0046, 0.0046, 0.0045, 0.0044, 0.0042, 0.004, 0.0037, 0.0035, 0.0032, 0.0028, 0.0025, 0.0022, 0.0019 ],
          [ -0.0004, -0.0006, -0.0008, -0.0009, -0.001, -0.0011, -0.0011, -0.001, -0.0009, -0.0007, -0.0004, -0.0001, 0.0003, 0.0008, 0.0013, 0.0019, 0.0024, 0.003, 0.0036, 0.0042, 0.0048, 0.0053, 0.0057, 0.0062, 0.0065, 0.0067, 0.0069, 0.007, 0.007, 0.0069, 0.0067, 0.0064, 0.0061, 0.0057, 0.0052, 0.0047, 0.0041, 0.0036, 0.003, 0.0024 ],
          [ -0.0001, -0.0002, -0.0004, -0.0005, -0.0006, -0.0007, -0.0007, -0.0007, -0.0007, -0.0006, -0.0005, -0.0003, -0.0001, 0.0002, 0.0005, 0.0009, 0.0013, 0.0017, 0.0021, 0.0026, 0.003, 0.0034, 0.0038, 0.0042, 0.0045, 0.0048, 0.005, 0.0051, 0.0052, 0.0053, 0.0052, 0.0051, 0.005, 0.0048, 0.0045, 0.0042, 0.0038, 0.0034, 0.003, 0.0026 ],
          [ -0.0003, -0.0003, -0.0003, -0.0003, -0.0003, -0.0002, -0.0002, -0.0001, 0.0, 0.0001, 0.0002, 0.0004, 0.0005, 0.0007, 0.0009, 0.0011, 0.0013, 0.0015, 0.0017, 0.0018, 0.002, 0.0021, 0.0023, 0.0024, 0.0025, 0.0025, 0.0025, 0.0026, 0.0025, 0.0025, 0.0024, 0.0023, 0.0022, 0.0021, 0.002, 0.0018, 0.0017, 0.0015, 0.0014, 0.0012 ],
          [ -0.0002, -0.0002, -0.0002, -0.0001, -0.0001, -0.0001, 0.0, 0.0, 0.0001, 0.0002, 0.0002, 0.0003, 0.0004, 0.0005, 0.0006, 0.0007, 0.0008, 0.0008, 0.0009, 0.001, 0.0011, 0.0011, 0.0011, 0.0012, 0.0012, 0.0012, 0.0012, 0.0012, 0.0011, 0.0011, 0.0011, 0.001, 0.001, 0.0009, 0.0008, 0.0008, 0.0007, 0.0006, 0.0006, 0.0005 ],
          [ -0.0001, -0.0001, -0.0002, -0.0001, -0.0001, -0.0001, -0.0001, 0.0, 0.0, 0.0001, 0.0002, 0.0002, 0.0003, 0.0004, 0.0005, 0.0006, 0.0007, 0.0008, 0.0009, 0.001, 0.0011, 0.0011, 0.0012, 0.0012, 0.0013, 0.0013, 0.0013, 0.0013, 0.0012, 0.0012, 0.0012, 0.0011, 0.0011, 0.001, 0.0009, 0.0008, 0.0008, 0.0007, 0.0006, 0.0005 ],
          [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0001, 0.0001, 0.0002, 0.0002, 0.0003, 0.0003, 0.0004, 0.0005, 0.0006, 0.0006, 0.0007, 0.0008, 0.0009, 0.001, 0.0011, 0.0011, 0.0012, 0.0013, 0.0013, 0.0014, 0.0014, 0.0014, 0.0014, 0.0015, 0.0014, 0.0014, 0.0014, 0.0014, 0.0013, 0.0013, 0.0012, 0.0012, 0.0011, 0.001, 0.0009 ],
          [ -0.0002, -0.0002, -0.0002, -0.0002, -0.0002, -0.0001, -0.0001, -0.0001, 0.0, 0.0001, 0.0002, 0.0002, 0.0003, 0.0004, 0.0005, 0.0007, 0.0008, 0.0009, 0.001, 0.0011, 0.0011, 0.0012, 0.0013, 0.0013, 0.0014, 0.0014, 0.0014, 0.0014, 0.0013, 0.0013, 0.0012, 0.0012, 0.0011, 0.001, 0.0009, 0.0008, 0.0007, 0.0006, 0.0005, 0.0005 ],
          [ 0.0002, 0.0002, 0.0002, 0.0001, 0.0001, 0.0, 0.0, 0.0, -0.0001, -0.0001, -0.0001, -0.0001, -0.0001, -0.0001, -0.0001, -0.0001, 0.0, 0.0001, 0.0001, 0.0002, 0.0004, 0.0005, 0.0006, 0.0007, 0.0009, 0.001, 0.0011, 0.0012, 0.0013, 0.0014, 0.0015, 0.0015, 0.0016, 0.0016, 0.0016, 0.0016, 0.0015, 0.0015, 0.0014, 0.0013 ],
          [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0001, 0.0001, 0.0001, 0.0001, 0.0001, 0.0001, 0.0001, 0.0001, 0.0001, 0.0002, 0.0002, 0.0002, 0.0002, 0.0003, 0.0003, 0.0004, 0.0004, 0.0005, 0.0005, 0.0005, 0.0006, 0.0006, 0.0007, 0.0007, 0.0007, 0.0008, 0.0008, 0.0008, 0.0008, 0.0008, 0.0008, 0.0008, 0.0008, 0.0007, 0.0007 ],
          [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0001, 0.0001, 0.0001, 0.0001, 0.0002, 0.0002, 0.0002, 0.0003, 0.0003, 0.0003, 0.0003, 0.0004, 0.0004, 0.0004, 0.0004, 0.0004, 0.0004, 0.0004, 0.0004, 0.0004, 0.0004, 0.0004, 0.0004, 0.0003, 0.0003, 0.0003, 0.0003, 0.0002 ]
],

Update 2

Using the suggestion below I was able to translate the coordinates with the following:

    const path: any = d3Geo.geoPath(
      d3Geo.geoTransform({
        point: function (x, y) {
          const [x2, y2] = polarPoint(x, y);
          this.stream.point(x2, y2);
        },
      })
    );

Upvotes: 3

Views: 594

Answers (1)

Michael Rovinsky
Michael Rovinsky

Reputation: 7210

Given your polar plot innerRadius, outerRadius, domainRange (range of arguments), and valueRange (range of values), the polar coordinates of each point (arg, value) are calculated the following way:

const polarPoint = (arg, value) => {
  const angle = (arg - domainRange.min) / 
    (domainRange.max - domainRange.min) * Math.PI * 2;
  const radius = (value - valueRange.min) / 
    (valueRange.max - valueRange.min) * (outerRadius - innerRadius) + innerRadius;
  const x = radius * Math.sin(angle);
  const y = -radius * Math.cos(angle);
  return {x, y};
}

See the snippet as illustration:

const domainRange = {min: 0, max: 100}; // Range of arguments
const valueRange = {min: 0, max: 1000}; // Range of values
const plotWidth = 200; // Width of the cartesian plot
const plotHeight = 100; // Height of the cartesian plot
const innerRadius = 20; // Inner radius of the polar plot
const outerRadius = 100; // Outer radius of the polar plot
const values = [[0, 100],[10,400],[20,700],[30,750],[40,900],[50,600],[60,400],[70,350],[80,300],[90,250],[100,100]]; // pairs of arguments/values

const cartesianPoint = (arg, value) => {
  const x = (arg - domainRange.min) / (domainRange.max - domainRange.min) * plotWidth;
  const y = -(value - valueRange.min) / (valueRange.max - valueRange.min) * plotHeight;
  return {x, y};
};

const polarPoint = (arg, value) => {
  const angle = (arg - domainRange.min) / (domainRange.max - domainRange.min) * Math.PI * 2;
  const radius = (value - valueRange.min) / (valueRange.max - valueRange.min) * (outerRadius - innerRadius) + innerRadius;
  const x = radius * Math.sin(angle);
  const y = -radius * Math.cos(angle);
  return {x, y};
}

const svg = d3.select('svg');

const cartesianPlot = svg.append('g')
  .attr('transform', 'translate(20, 150)');
  
cartesianPlot.append('rect')
  .attr('x', 0)
  .attr('y', -plotHeight)
  .attr('width', plotWidth)
  .attr('height', plotHeight)
  .style('fill', '#eee');

let prev = null;  
values.forEach(v => {
  const point = cartesianPoint(v[0], v[1]);
  cartesianPlot.append('circle')
    .attr('cx', point.x)
    .attr('cy', point.y)
    .attr('r', 3)
    .style('fill', 'red');
  if (prev)  
    cartesianPlot.append('line')
      .attr('x1', prev.x)
      .attr('y1', prev.y)
      .attr('x2', point.x)
      .attr('y2', point.y)
      .style('stroke', 'blue')
  prev = point;     
})  

const polarPlot = svg.append('g')
  .attr('transform', 'translate(350, 120)');
polarPlot.append('circle')
  .attr('r', outerRadius)
  .style('fill', '#eee')
polarPlot.append('circle')
  .attr('r', innerRadius)
  .style('fill', '#fff')  

prev = null;
values.forEach(v => {
  const point = polarPoint(v[0], v[1]);
  polarPlot.append('circle')
    .attr('cx', point.x)
    .attr('cy', point.y)
    .attr('r', 3)
    .style('fill', 'red');
  if (prev)  
    polarPlot.append('line')
      .attr('x1', prev.x)
      .attr('y1', prev.y)
      .attr('x2', point.x)
      .attr('y2', point.y)
      .style('stroke', 'blue')
  prev = point;     
})  
  
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="500" height="250" />

Upvotes: 1

Related Questions