Snowflake chart - how to curve the labels around outer circle?

Hi all- I am new to this forum. I am creating a snowflake chart. I wasn’t to curve the labels (Metric 1, Metric 2 etc) around the outer circle. Right now it is horizontally aligned.

The code I used is:

{
  // Sample data for radar chart
  const data = [
    { axis: "Metric 1", value: 0.8 },
    { axis: "Metric 2", value: 0.6 },
    { axis: "Metric 3", value: 0.4 },
    { axis: "Metric 4", value: 0.7 },
    { axis: "Metric 5", value: 0.9 },
  ];

  // Configurations
  const width = 600;
  const height = 500;
  const levels = 5;  // Number of concentric circles
  const maxValue = 1;  // Assuming data values are normalized between 0 and 1
  const radius = 200;

  // Angle for each axis
  const angleSlice = (Math.PI * 2) / data.length;

  // Scales
  const radiusScale = d3.scaleLinear().range([0, radius]).domain([0, maxValue]);

  // SVG container
  const svg = d3.create("svg")
    .attr("width", width)
    .attr("height", height);

  // Create a Chart withing the SVG
  const chart = svg.append("g")
    .attr("transform", `translate(${width / 2},${height / 2})`);

  // Draw concentric circles for reference levels
  for (let i = 1; i <= levels; i++) {
    chart.append("circle")
      .attr("r", (radius / levels) * i)
      .style("fill", "none")
      .style("stroke", "#CDCDCD")
      .style("stroke-dasharray", "3 3");
  }

  // Axes
  data.forEach((d, i) => {
    // Calculate the end point for each axis line
    const angle = angleSlice * i;
    const lineEndX = radius * Math.cos(angle - Math.PI / 2);
    const lineEndY = radius * Math.sin(angle - Math.PI / 2);
  
    // Draw axis lines
    chart.append("line")
      .attr("x1", 0)
      .attr("y1", 0)
      .attr("x2", lineEndX)
      .attr("y2", lineEndY)
      .style("stroke", "gray")
      .style("stroke-width", 1);
  });

  // Position labels between each axis line, at the line ends
  data.forEach((d, i) => {
    // Midpoint angle between the current axis and the next
    const midAngle = angleSlice * (i + 0.5);
    const labelX = (radius + 40) * Math.cos(midAngle - Math.PI / 2);
    const labelY = (radius + 20) * Math.sin(midAngle - Math.PI / 2);
  
    // Place label at the midpoint between current and next axis
    chart.append("text")
      .attr("x", labelX)
      .attr("y", labelY)
      .attr("text-anchor", "middle")
      .attr("dy", "0.35em")
      .text(d.axis);
  });

  // Line generator for the radar area
  const radarLine = d3.line()
    .x((d, i) => radiusScale(d.value) * Math.cos(angleSlice * i - Math.PI / 2))
    .y((d, i) => radiusScale(d.value) * Math.sin(angleSlice * i - Math.PI / 2))
    .curve(d3.curveCardinalClosed);  // Ensures the path is closed

  // Draw radar area (snowflake shape)
  chart.append("path")
    .datum(data)
    .attr("d", radarLine)
    .style("fill", "steelblue")
    .style("fill-opacity", 0.5)
    .style("stroke", "blue")
    .style("stroke-width", 2);

  return svg.node();
}

The notebook search has some good inspiration Curved Text Mark / Jo Wood | Observable

I guess that was plot which is not immediately relevant, but there is a d3 one too Curve text around circle / Peter Scriven | Observable

1 Like

Thank for those idea. I did some research with that, and modified mine.

Sharing the code if anybody is interested.

{
  // Sample data for radar chart
  const data = [
    { axis: "Metric 1", value: 0.8 },
    { axis: "Metric 2", value: 0.6 },
    { axis: "Metric 3", value: 0.4 },
    { axis: "Metric 4", value: 0.7 },
    { axis: "Metric 5", value: 0.9 },
  ];

  // Configurations
  const width = 600;
  const height = 500;
  const levels = 5;  // Number of concentric circles
  const maxValue = 1;  // Assuming data values are normalized between 0 and 1
  // const radius = Math.min(width / 2, height / 2);
  const radius = 200;

  // Angle for each axis
  const angleSlice = (Math.PI * 2) / data.length;

  // Scales
  const radiusScale = d3.scaleLinear().range([0, radius]).domain([0, maxValue]);

  // SVG container
  const svg = d3.create("svg")
    .attr("width", width)
    .attr("height", height);
    // .style("border", "1px solid steelblue");

  // Create a Chart withing the SVG
  const chart = svg.append("g")
    .attr("transform", `translate(${width / 2},${height / 2})`);

  // Draw concentric circles for reference levels
  for (let i = 1; i <= levels; i++) {
    chart.append("circle")
      .attr("r", (radius / levels) * i)
      .style("fill", "none")
      .style("stroke", "#CDCDCD")
      .style("stroke-dasharray", "3 3");
  }

  // Create the outermost circle path for curved labels
  chart.append("circle")
    .attr("id", "outerCirclePath")
    .attr("r", radius + 10)
    .style("fill", "none")
    .style("stroke", "none");

  // Axes
  data.forEach((d, i) => {
    // Calculate the end point for each axis line
    const angle = angleSlice * i;
    const lineEndX = radius * Math.cos(angle - Math.PI / 2);
    const lineEndY = radius * Math.sin(angle - Math.PI / 2);
  
    // Draw axis lines
    chart.append("line")
      .attr("x1", 0)
      .attr("y1", 0)
      .attr("x2", lineEndX)
      .attr("y2", lineEndY)
      .style("stroke", "gray")
      .style("stroke-width", 1);
  });

  // Add labels curved along the outermost circle
  data.forEach((d, i) => {
    // Calculate angle for label placement
    const angle = angleSlice * i;
    const labelAngle = angle - Math.PI / 2;
  
    // Adjust text orientation based on its position
    const rotation = (angle * 180) / Math.PI;
    const align = rotation > 90 && rotation < 270 ? "end" : "start";
  
    // Add label along the circle
    chart.append("text")
      .append("textPath")
      .attr("href", "#outerCirclePath")
      .attr("startOffset", `${(i / data.length) * 100}%`) // Position evenly around the circle
      .attr("text-anchor", "start")
      .attr("alignment-baseline", "auto")
      .style("font-size", "16px")
      .text(d.axis);
  });

  // Line generator for the radar area
  const radarLine = d3.line()
    .x((d, i) => radiusScale(d.value) * Math.cos(angleSlice * i - Math.PI / 2))
    .y((d, i) => radiusScale(d.value) * Math.sin(angleSlice * i - Math.PI / 2))
    .curve(d3.curveCardinalClosed);  // Ensures the path is closed

  // Draw radar area (snowflake shape)
  chart.append("path")
    .datum(data)
    .attr("d", radarLine)
    .style("fill", "steelblue")
    .style("fill-opacity", 0.5)
    .style("stroke", "blue")
    .style("stroke-width", 2);

  return svg.node();
}
1 Like