VChart icon indicating copy to clipboard operation
VChart copied to clipboard

[Feature] radar support cornerRadius

Open xiaoluoHe opened this issue 11 months ago • 2 comments

What problem does this feature solve?

雷达图拐点处支持圆角。

最理想的期望是最后的:根据角度弧度会不同 其次降级方案是中间的:统一有个最小的弧度,尽量别出现过于尖锐的尖头

image

image

What does the proposed API look like?

curveType 下新增配置项

xiaoluoHe avatar Jan 14 '25 02:01 xiaoluoHe

  1. vchart-extension 中通过 pathProxy 来支持
  2. 关注凹点的圆角是否有问题

xile611 avatar Jan 15 '25 03:01 xile611

demo 供参考:

Image

const linkPoints = (points, path) => {
  const firstPoint = points[0];
  const lastPoint = points[points.length - 1];

  // 处理第一个点的圆角
  const v0 = { x: firstPoint.x - lastPoint.x, y: firstPoint.y - lastPoint.y };
  const v1First = { x: points[1].x - firstPoint.x, y: points[1].y - firstPoint.y };
  const v0Len = Math.sqrt(v0.x * v0.x + v0.y * v0.y);
  const v1FirstLen = Math.sqrt(v1First.x * v1First.x + v1First.y * v1First.y);

  const cosTheta0 = (v0.x * v1First.x + v0.y * v1First.y) / (v0Len * v1FirstLen);
  const theta0 = Math.acos(Math.min(Math.max(cosTheta0, -1), 1));
  const baseRadius0 = Math.min(v0Len, v1FirstLen) * 0.2;
  const radius0 = baseRadius0 * Math.min(1, theta0 / (Math.PI / 2));

  // 计算第一个点的圆角控制点
  const cp01 = {
    x: firstPoint.x - (v0.x * radius0) / v0Len,
    y: firstPoint.y - (v0.y * radius0) / v0Len
  };
  const cp02 = {
    x: firstPoint.x + (v1First.x * radius0) / v1FirstLen,
    y: firstPoint.y + (v1First.y * radius0) / v1FirstLen
  };

  // 从最后一个圆角控制点开始绘制
  path.moveTo(cp01.x, cp01.y);
  path.quadraticCurveTo(firstPoint.x, firstPoint.y, cp02.x, cp02.y);

  for (let i = 1; i < points.length; i++) {
    const curr = points[i];
    const prev = points[i - 1];
    const next = points[(i + 1) % points.length];

    // 计算夹角大小
    const v1 = { x: curr.x - prev.x, y: curr.y - prev.y };
    const v2 = { x: next.x - curr.x, y: next.y - curr.y };
    const v1Len = Math.sqrt(v1.x * v1.x + v1.y * v1.y);
    const v2Len = Math.sqrt(v2.x * v2.x + v2.y * v2.y);
    const cosTheta = (v1.x * v2.x + v1.y * v2.y) / (v1Len * v2Len);
    const theta = Math.acos(Math.min(Math.max(cosTheta, -1), 1));

    // 根据夹角计算实际圆角半径:夹角越大,圆角半径越大
    // 根据相邻向量长度计算基础圆角半径
    const baseRadius = Math.min(v1Len, v2Len) * 0.2;
    const radius = baseRadius * Math.min(1, theta / (Math.PI / 2));

    // 计算圆角控制点
    const cp1 = {
      x: curr.x - (v1.x * radius) / v1Len,
      y: curr.y - (v1.y * radius) / v1Len
    };
    const cp2 = {
      x: curr.x + (v2.x * radius) / v2Len,
      y: curr.y + (v2.y * radius) / v2Len
    };

    path.lineTo(cp1.x, cp1.y);
    path.quadraticCurveTo(curr.x, curr.y, cp2.x, cp2.y);
  }
  path.closePath();
};

const roundedLine = (data, attrs, path) => {
  linkPoints(attrs.points, path);
  return path;
};

const roundedArea = (data, attrs, path) => {
  const { points } = attrs;
  const topPoints = points.map(point => ({ x: point.x, y: point.y }));
  const bottomPoints = points.map(point => ({ x: point.x1, y: point.y1 })).reverse();

  linkPoints(topPoints, path);
  linkPoints(bottomPoints, path);

  return path;
};


const spec = {
  type: 'radar',
  data: [
    {
      values: [
        {
          month: 'Jan.',
          value: 45,
          type: 'A'
        },
        {
          month: 'Feb.',
          value: 61,
          type: 'A'
        },
        {
          month: 'Mar.',
          value: 92,
          type: 'A'
        },
        {
          month: 'Apr.',
          value: 57,
          type: 'A'
        },
        {
          month: 'May.',
          value: 46,
          type: 'A'
        },
        {
          month: 'Jun.',
          value: 36,
          type: 'A'
        },
        {
          month: 'Jul.',
          value: 33,
          type: 'A'
        },
        {
          month: 'Aug.',
          value: 63,
          type: 'A'
        },
        {
          month: 'Sep.',
          value: 57,
          type: 'A'
        },
        {
          month: 'Oct.',
          value: 53,
          type: 'A'
        },
        {
          month: 'Nov.',
          value: 69,
          type: 'A'
        },
        {
          month: 'Dec.',
          value: 40,
          type: 'A'
        },
        {
          month: 'Jan.',
          value: 31,
          type: 'B'
        },
        {
          month: 'Feb.',
          value: 39,
          type: 'B'
        },
        {
          month: 'Mar.',
          value: 81,
          type: 'B'
        },
        {
          month: 'Apr.',
          value: 39,
          type: 'B'
        },
        {
          month: 'May.',
          value: 64,
          type: 'B'
        },
        {
          month: 'Jun.',
          value: 21,
          type: 'B'
        },
        {
          month: 'Jul.',
          value: 58,
          type: 'B'
        },
        {
          month: 'Aug.',
          value: 72,
          type: 'B'
        },
        {
          month: 'Sep.',
          value: 47,
          type: 'B'
        },
        {
          month: 'Oct.',
          value: 37,
          type: 'B'
        },
        {
          month: 'Nov.',
          value: 80,
          type: 'B'
        },
        {
          month: 'Dec.',
          value: 74,
          type: 'B'
        },
        {
          month: 'Jan.',
          value: 90,
          type: 'C'
        },
        {
          month: 'Feb.',
          value: 95,
          type: 'C'
        },
        {
          month: 'Mar.',
          value: 62,
          type: 'C'
        },
        {
          month: 'Apr.',
          value: 52,
          type: 'C'
        },
        {
          month: 'May.',
          value: 74,
          type: 'C'
        },
        {
          month: 'Jun.',
          value: 87,
          type: 'C'
        },
        {
          month: 'Jul.',
          value: 80,
          type: 'C'
        },
        {
          month: 'Aug.',
          value: 69,
          type: 'C'
        },
        {
          month: 'Sep.',
          value: 74,
          type: 'C'
        },
        {
          month: 'Oct.',
          value: 84,
          type: 'C'
        },
        {
          month: 'Nov.',
          value: 94,
          type: 'C'
        },
        {
          month: 'Dec.',
          value: 23,
          type: 'C'
        }
      ]
    }
  ],
  categoryField: 'month',
  valueField: 'value',
  seriesField: 'type',
  stack: true,
   line: {
    visible: true,
    customShape: roundedLine
  },
  area: {
    visible: true,
    customShape: roundedArea
  },
  point:{visible: false},
  axes: [
    {
      orient: 'radius',
      min: 0,
      domainLine: {
        visible: true
      },
      label: {
        visible: true
      },
      grid: {
        smooth: true
      }
    },
    {
      orient: 'angle',
      tick: {
        visible: false
      },
      grid: {
        style: {
          lineDash: [0]
        }
      }
    }
  ],
  legends: {
    visible: true,
    orient: 'top'
  }
};

const vchart = new VChart(spec, { dom: CONTAINER_ID });
vchart.renderSync();

// Just for the convenience of console debugging, DO NOT COPY!
window['vchart'] = vchart;

xiaoluoHe avatar Jan 23 '25 11:01 xiaoluoHe