You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Chart.js雷达图中按角度旋转箭头并计算端点?

问题描述

承接之前的提问,需求是让雷达图上的箭头像时钟一样按角度旋转。目前能在圆形底部端点生成箭头,但无法根据圆半径的角度计算正确的箭头端点。尝试过基于XY轴使用sin、cos函数(代码如下),也试过rotate方法,均未成功;使用雷达图的getPointPositionForValue时,箭头会出现在画布随机位置。

尝试的代码片段:

ctx.beginPath();
ctx.arrow(
  xScale.getPixelForValue(0),
  yScale.getPixelForValue(0.0),
  xScale.getPixelForValue(8 * xline90),
  yScale.getPixelForValue(8 * yline90),
  [0, 0.75, -15, 0.75, -15, 10]
);
ctx.fill();

var xline90 = Math.sin((Math.PI * 2 * 120) / 360);
var yline90 = Math.cos((Math.PI * 2 * 120) / 360);

完整示例代码

JavaScript

var mobilewidth = 420;
var backgroundcolor = "transparent";

var titleshow = true;
var titletext = [
  "Example"
];
var titleshowmob = true;
var titlecolor = "rgba(75,75,75,1)";
var titlesize = 15;
var titlesizemob = 9;
var titleweight = "600";
var titlefont = "sans-serif";

var anglelinecolor = "rgba(120,120,120,1)";
var anglelinewidth = 2;

var resultcustomer = 4;
var resultoperational = 9;
var resultproduct = 10;

var resultbackgroundcolor = "rgba(247,70,74,0.2)";
var resultlinecolor = "rgba(247,70,74,1)";
var resultlinewidth = 2;
var resultpoint = 2;
var resultpointwidth = 3;

var pointlabelcolor = "rgba(75,75,75,1)";
var pointlabelsize = 15;
var pointlabelsizemob = 9;
var pointlabelweight = "600";
var pointlabelfont = "sans-serif";
var pointlabelalign = "center";
var pointlabelpadding = 10;

var ringbackgroundinner = "rgba(195,195,195,1)";
var ringbackgroundmiddle = "rgba(225,225,225,1)";
var ringbackgroundouter = "rgba(254,254,254,1)";
var ringbordercolor = "rgba(120,120,120,1)";
var ringborderdash = [6, 4];
var ringborderwidth = 1.5;

var ctx = document.getElementById("myChart");
var data = {
  labels: [
    ["test"],
    ["test2"],
    ["test3"]
  ],
  datasets: [
    {
      label: "nvt",
      backgroundColor: "rgba(38,120,255,0.2)",
      borderColor: "rgba(38,120,255, 1)",
      pointRadius: 0,
      data: [, 0]
    },
    {
      label: "Score",
      data: [resultcustomer, resultproduct, resultoperational],
      borderWidth: resultlinewidth,
      pointRadius: resultpoint,
      pointBorderWidth: resultpointwidth,
      backgroundColor: resultbackgroundcolor,
      borderColor: resultlinecolor
    }
  ]
};

var options = {
  plugins: {
    legend: {
      display: false
    },
    tooltip: {
      enabled: false
    },
    title: {
      display: function () {
        if (window.innerWidth < mobilewidth) {
          return titleshowmob;
        } else {
          return titleshow;
        }
      },
      color: titlecolor,
      font: {
        size: function () {
          if (window.innerWidth < mobilewidth) {
            return titlesizemob;
          } else {
            return titlesize;
          }
        },
        weight: titleweight,
        family: titlefont
      },
      text: titletext
    }
  },
  scales: {
    r: {
      angleLines: {
        color: anglelinecolor,
        borderWidth: anglelinewidth
      },
      pointLabels: {
        color: pointlabelcolor,
        padding: pointlabelpadding,
        font: {
          size: function () {
            if (window.innerWidth < mobilewidth) {
              return pointlabelsizemob;
            } else {
              return pointlabelsize;
            }
          },
          weight: pointlabelweight,
          family: pointlabelfont
        }
      },
      min: 1,
      max: 10,
      ticks: {
        display: false
      },
      grid: {
        display: false,
        circular: true
      }
    }
  }
};

var plugin1 = {
  beforeDraw(chart) {
    var c = chart.ctx;

    c.save();
    c.globalCompositeOperation = "destination-over";
    c.fillStyle = backgroundcolor;
    c.fillRect(0, 0, chart.width, chart.height);
    c.restore();

    var rScale = chart.scales.r;
    var xline0 = Math.sin(340);
    var yline0 = Math.cos(340);
    chart.data.datasets[0].data.forEach(function (item, index) {
      var circle3 = rScale.getPointPositionForValue(0.5 + index, 10);
      var base = rScale.getPointPositionForValue(0.5 + index, 1);

      c.beginPath();
      c.lineWidth = ringborderwidth;
      c.fillStyle = ringbackgroundouter;
      c.arc(base.x, base.y, circle3.y - base.y, 0, 2 * Math.PI);
      c.setLineDash(ringborderdash);
      c.strokeStyle = ringbordercolor;
      c.stroke();
      c.fill();

      c.save();
      c.beginPath();
      c.fillStyle = "black";
      c.arrow(base.x, base.y, circle3.x, circle3.y, [
        0,
        5,
        -15,
        5,
        -15,
        10
      ]);

      c.fill();
      c.restore();

    });
  }
};

var myChart = new Chart(ctx, {
  type: "radar",
  data: data,
  options: options,
  plugins: [plugin1]
});

HTML

<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://frogcat.github.io/canvas-arrow/canvas-arrow.js"></script>
<html>
<canvas id="myChart" />
</html>
解决方案

你的需求可以实现,核心是利用雷达图的角度计算箭头的旋转角度,同时结合Canvas的坐标变换来定位箭头。以下是关键修改点:

1. 正确获取雷达图标签角度

Chart.js的雷达轴(rScale)提供了getAngleForIndex方法,可直接获取对应标签的弧度角度,无需手动计算sin/cos,避免坐标系转换错误。注意雷达图的角度是从Y轴正方向顺时针计算,需要转换为Canvas的逆时针旋转坐标系。

2. 用Canvas变换实现箭头旋转

通过translate将画布原点移到雷达图中心,再rotate到目标角度,此时绘制水平箭头就会自动指向对应角度的方向,最后恢复画布状态即可。

修改后的插件代码

替换原plugin1代码:

var plugin1 = {
  beforeDraw(chart) {
    var c = chart.ctx;
    c.save();
    c.globalCompositeOperation = "destination-over";
    c.fillStyle = backgroundcolor;
    c.fillRect(0, 0, chart.width, chart.height);
    c.restore();

    var rScale = chart.scales.r;
    // 雷达图中心坐标
    const centerX = rScale.xCenter;
    const centerY = rScale.yCenter;
    // 箭头长度(根据雷达图直径调整比例)
    const arrowLength = rScale.drawingArea * 0.45;

    // 遍历每个标签角度绘制箭头
    chart.data.labels.forEach((_, index) => {
      // 获取当前标签的弧度角度
      let angle = rScale.getAngleForIndex(index);
      // 转换为Canvas逆时针旋转坐标系
      angle = -angle + Math.PI / 2;

      c.save();
      // 平移画布到雷达中心
      c.translate(centerX, centerY);
      // 旋转画布到目标角度
      c.rotate(angle);

      // 绘制箭头:从中心指向外侧
      c.beginPath();
      c.fillStyle = "black";
      c.arrow(0, 0, arrowLength, 0, [0, 5, -15, 5, -15, 10]);
      c.fill();

      // 恢复画布初始状态
      c.restore();
    });
  }
};

关键说明

  • getAngleForIndex(index):直接获取对应标签的角度,避免手动计算带来的坐标系偏差。
  • Canvas变换:先平移再旋转,让箭头绘制逻辑简化为水平方向,自动对齐目标角度。
  • 箭头长度控制:通过rScale.drawingArea(雷达图直径)的比例值调整,确保箭头在雷达图内部显示。

修改后,箭头会像时钟指针一样,精准指向每个雷达图标签对应的角度位置。


内容的提问来源于stack exchange,提问作者Patlion

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 04:02:01