diff --git a/README.md b/README.md index f361215dc3..f4bc46b75b 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ G2Plot v3 covers 36+ common chart types. PRs for more types are welcome: |---|---|---|---|---| | line | column | pie | scatter | dual-axes | | area | bar | | heatmap | funnel | -| | | | | radar | +| | radial-bar | | | radar | ## 🏗️ How It Works diff --git a/README.zh-CN.md b/README.zh-CN.md index 3f4db8a370..f2b07ca139 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -46,7 +46,7 @@ G2Plot v3 覆盖 36+ 个常用图表类型,更多类型,欢迎提交 PR: |---|---|---|---|---| | line 折线图 | column 柱状图 | pie 饼图 | scatter 散点图 | dual-axes 双轴图 | | area 面积图 | bar 条形图 | | heatmap 热力图 | funnel 漏斗图 | -| | | | | radar 雷达图 | +| | radial-bar 玉珏图 | | | radar 雷达图 | ## 🏗️ 工作原理 diff --git a/charts/radial-bar.md b/charts/radial-bar.md new file mode 100644 index 0000000000..b556638228 --- /dev/null +++ b/charts/radial-bar.md @@ -0,0 +1,163 @@ +# Radial Bar 玉珏图(径向条形图) + +> 组件用途:在极坐标下用同心圆弧的长度对比多个类别的数值,兼顾排名与整体观感。 + +## 何时使用 + +- 类别排名/对比(5~8 个类别):渠道转化率、部门目标完成度、产品评分 +- KPI 看板、大屏:环形造型节省横向空间,视觉记忆点强 +- 数值同量纲且非负(百分比、分数、金额),需要固定基准对比 +- 强调“完成度/达成率”:配合 `scale.y.domain` 固定 0~100,弧长即完成比例 + +## 何时不用 + +- 类别多(> 10)或类别名称长 → 用 bar 条形图,标签更好排布 +- 需要精确比较细微差异 → 用 column / bar,直线长度比弧长更易读 +- 展示整体构成(各块加起来 100%)→ 用 pie 饼图;玉珏图每环独立,不构成整体 +- 时间序列趋势 → 用 line 折线图 +- 数据含负值 → 用 bar 或发散条形图;弧长无法表达正负方向 + +## 数据要求 + +- 长表数据,每行一个类别,见 [数据模式](../references/data-patterns.md) +- `x` 字段:类别字符串(如 `channel`、`department`) +- `y` 字段:数值(number),必须非负;百分比统一用 0~1 或 0~100,并与 `scale.y.domain` 一致 +- 数据顺序决定各环的排列顺序;排名场景先在渲染前对数据排序,不要依赖图例顺序 +- 单指标多类别场景不要传多系列字段;多系列堆叠不是玉珏图的常规用法 + +## 基础实现(G2 v5) + +玉珏图 = `type: 'interval'` + `coordinate: { type: 'radial' }`。在 radial 坐标系下,`y` 数值映射为圆弧角度,`x` 类别映射为同心环半径,因此弧越长数值越大。 + +```ts +import { Chart } from '@antv/g2'; + +/** + * 基础玉珏图(径向条形图) + * 场景:对比各获客渠道的转化率,用同心圆弧长度表达数值大小 + * 要点:interval + radial 坐标系;y 轴固定 0~1 映射百分比;隐藏坐标轴,用图例标识类别 + */ +const data = [ + { channel: '自然搜索', conversionRate: 0.42 }, + { channel: '社交推荐', conversionRate: 0.68 }, + { channel: '广告投放', conversionRate: 0.35 }, + { channel: '邮件营销', conversionRate: 0.24 }, + { channel: '直接访问', conversionRate: 0.55 }, +]; + +const chart = new Chart({ + container: 'container', + autoFit: true, +}); + +chart.options({ + type: 'interval', + data, + encode: { x: 'channel', y: 'conversionRate', color: 'channel' }, + coordinate: { type: 'radial', innerRadius: 0.25 }, + scale: { y: { domain: [0, 1] } }, + axis: { + x: { title: false, label: false, tick: false, line: false }, + y: { title: false, label: false, tick: false, line: false, grid: false }, + }, + legend: { color: { position: 'right' } }, + tooltip: { + title: 'channel', + items: [ + { + channel: 'y', + name: '转化率', + valueFormatter: (v) => `${(Number(v) * 100).toFixed(1)}%`, + }, + ], + }, + style: { radius: 4 }, +}); + +chart.render(); +``` + +## 常见变体 + +| 变体 | 关键改动 | 完整案例 | +|---|---|---| +| 数值标签 | `labels` 的 `text` 回调拼单位,`position: 'outside'` 定位到弧末端 | [with-label.ts](../examples/radial-bar/with-label.ts) | +| 半圆造型 | `coordinate.startAngle / endAngle` 限定角度范围 | [semi-circle.ts](../examples/radial-bar/semi-circle.ts) | +| 自定义品牌色板 | `scale: { color: { range: [...] } }` | [semi-circle.ts](../examples/radial-bar/semi-circle.ts) | + +半圆玉珏图核心差异(只画右半圆,中心留白更大): + +```ts-snippet +coordinate: { + type: 'radial', + innerRadius: 0.35, + startAngle: -Math.PI / 2, + endAngle: Math.PI / 2, +}, +``` + +数值标签核心差异(`autoRotate: false` 让文字保持水平,避免沿弧旋转): + +```ts-snippet +labels: [ + { + text: (d: { nps: number }) => `${d.nps} 分`, + position: 'outside', + autoRotate: false, + }, +], +``` + +## 样式自定义 + +| 定制项 | 写法 | 说明 | +|---|---|---| +| 中心留白 | `coordinate: { type: 'radial', innerRadius: 0.35 }` | 0~1 相对值;越大中心空洞越大,可放 HTML 中心文字 | +| 外圈留白 | `coordinate: { type: 'radial', outerRadius: 0.9 }` | 避免图例/标签贴边 | +| 角度范围 | `coordinate: { startAngle, endAngle }` | 默认 `-Math.PI/2` ~ `Math.PI*3/2`;半圆用 `-Math.PI/2` ~ `Math.PI/2` | +| 弧端圆角 | `style: { radius: 4 }` | 让每段圆弧两端变圆,数值小的时候更明显 | +| 类别色板 | `scale: { color: { range: ['#5B8FF9', '#5AD8A6'] } }` | 自定义颜色数组用 `range`,不要传给 `palette` | +| 固定数值范围 | `scale: { y: { domain: [0, 100] } }` | 保证所有弧从同一基准起算 | +| 图例位置 | `legend: { color: { position: 'top' } }` | `'top'` / `'bottom'` / `'left'` / `'right'`,或 `legend: false` 关闭 | +| 隐藏坐标轴 | `axis: { x: false, y: false }` | 玉珏图通常只留图例与标签 | +| 数值标签 | `labels: [{ text, position: 'outside', autoRotate: false }]` | 显示原始数值或格式化文案 | +| 主题 | `theme: 'classicDark'` | 大屏暗色场景,文字与网格自动适配 | + +图表标题与 tooltip 格式化: + +```ts-snippet +title: { title: '渠道转化率', subtitle: '统计周期:2026-10', align: 'left' }, +tooltip: { + title: 'channel', + items: [{ channel: 'y', name: '转化率', valueFormatter: (v) => `${(Number(v) * 100).toFixed(1)}%` }], +}, +``` + +## 易错点(v4 → v5) + +| ❌ 错误(幻觉高发) | ✅ 正确 | +|---|---| +| `type: 'radial-bar'` / `type: 'radialBar'` | 无此 mark;用 `type: 'interval'` + `coordinate: { type: 'radial' }` | +| 把 `radial` 写进 `type` | `radial` 是坐标系:`coordinate: { type: 'radial' }` | +| 忘记 `coordinate` | 不加坐标系会渲染成普通直角坐标柱状图 | +| 用 `coordinate: { type: 'polar' }` 代替 `radial` | polar 得到的是玫瑰图/扇形,不是同心环的玉珏图 | +| `radius: 0.8, innerRadius: 0.2` 写在顶层(v2 配置) | 放进坐标系:`coordinate: { type: 'radial', innerRadius: 0.2 }` | +| `startAngle` / `maxAngle` 写在顶层(v4 配置) | 放进坐标系:`coordinate: { startAngle, endAngle }` | +| `seriesField: 'channel'`(v2 配置) | `encode: { color: 'channel' }` | +| `.coordinate('radial')`(v4 链式) | spec 中 `coordinate: { type: 'radial' }` | +| 百分比数据不固定 `domain` | 显式 `scale: { y: { domain: [0, 1] } }` 或 `[0, 100]`,否则自适应范围会夸大差异 | +| 用玉珏图表达正负增长 | 负值没有弧长方向;改用 bar 或发散条形图 | +| 类别超过 10 个仍用玉珏图 | 环太薄、不可读;改用 bar 条形图 | +| `new RadialBar('container', {...})`(G2Plot v2) | `new Chart({ container })` + `type: 'interval'` + radial | + +更多对照见 [v4-to-v5-migration.md](../references/v4-to-v5-migration.md)。 + +## 完整案例 + +每个案例的元信息(场景与关键配置),代码均通过 `npm run validate` 校验: + +| 案例 | 场景 | 关键配置 | +|---|---|---| +| [basic.ts](../examples/radial-bar/basic.ts) | 各渠道转化率对比,最简起步 | `type: 'interval'` + `coordinate.type: 'radial'`、`innerRadius: 0.25`、`scale.y.domain: [0, 1]` | +| [with-label.ts](../examples/radial-bar/with-label.ts) | 弧末端直接标出产品 NPS 分数 | `labels` text 回调 + `position: 'outside'` + `autoRotate: false` | +| [semi-circle.ts](../examples/radial-bar/semi-circle.ts) | 大屏半圆造型 + 品牌色板 | `startAngle` / `endAngle` 半圆、`scale.color.range`、`legend.position: 'top'` | diff --git a/evals/cases/radial-bar.jsonl b/evals/cases/radial-bar.jsonl new file mode 100644 index 0000000000..3f9ca120a8 --- /dev/null +++ b/evals/cases/radial-bar.jsonl @@ -0,0 +1,16 @@ +{"id": "radial-basic-channel", "prompt": "我想用环形图对比自然搜索、社交推荐、广告投放、邮件营销四个渠道的转化率,弧越长表示转化率越高", "groundTruth": "使用 type: 'interval';coordinate 设置 type: 'radial';encode x 映射渠道字段、y 映射转化率字段、color 映射渠道;scale.y 固定 domain [0, 1]"} +{"id": "radial-inner-radius", "prompt": "玉珏图中间空出一大块,我想在圆心位置放一个总数文字", "groundTruth": "coordinate 设置 type: 'radial' 并增大 innerRadius(如 0.35~0.45)留出圆心空间;type 为 'interval';中心文字用 HTML 叠加,不要伪造不存在的 mark"} +{"id": "radial-fixed-domain", "prompt": "各渠道转化率都是百分比,希望所有圆弧从 0 开始按 100% 为满格来画,不要自动缩放", "groundTruth": "scale.y 显式设置 domain: [0, 100](或 [0, 1]);coordinate.type 为 'radial';type: 'interval'"} +{"id": "radial-rounded", "prompt": "玉珏图每段圆弧的两端想要圆角效果,看起来更柔和", "groundTruth": "style 设置 radius 为大于 0 的数值(如 4~8);coordinate.type: 'radial';type: 'interval'"} +{"id": "radial-label-outside", "prompt": "每段圆弧末端直接标出具体分数,文字不要跟着弧旋转,保持水平", "groundTruth": "labels 配置 text 回调拼出数值与单位,position: 'outside',autoRotate: false;coordinate.type: 'radial'"} +{"id": "radial-semi-circle", "prompt": "做一个半圆形的玉珏图,只占 180 度,放在大屏顶部", "groundTruth": "coordinate 设置 type: 'radial',并用 startAngle: -Math.PI / 2、endAngle: Math.PI / 2(或等价半圆角度);type: 'interval'"} +{"id": "radial-custom-palette", "prompt": "玉珏图不要默认配色,用我们品牌的蓝绿黄红紫五个颜色", "groundTruth": "scale.color.range 传入自定义颜色数组;encode.color 映射类别字段;type: 'interval' + coordinate.type: 'radial';不要用 palette 传数组"} +{"id": "radial-legend-top", "prompt": "玉珏图的图例放到顶部,横向排开,别占右侧空间", "groundTruth": "legend.color.position 设置为 'top';coordinate.type: 'radial';type: 'interval'"} +{"id": "radial-hide-axis", "prompt": "玉珏图只要圆环和图例,坐标轴、刻度、网格线都隐藏掉", "groundTruth": "axis 中把 x、y 的 label/tick/line/grid 设为 false,或直接 axis: { x: false, y: false };type: 'interval' + coordinate.type: 'radial'"} +{"id": "radial-tooltip-percent", "prompt": "悬停圆弧时 tooltip 显示渠道名和百分比,保留一位小数", "groundTruth": "tooltip.title 映射渠道字段,tooltip.items 的 valueFormatter 把数值转成百分比并 toFixed(1);type: 'interval' + coordinate.type: 'radial'"} +{"id": "radial-vs-pie", "prompt": "我要展示各部门在总营收中的占比,用玉珏图还是饼图?希望一眼看出整体构成", "groundTruth": "看整体构成和 100% 加总用 pie 饼图;玉珏图适合类别间排名/对比、每环独立不成整体;应说明选型理由而不是硬套 radial-bar"} +{"id": "radial-sort-desc", "prompt": "玉珏图的圆环顺序很乱,想按完成度从高到低排列", "groundTruth": "渲染前对 data 按数值字段降序排序(或用 data.transform 的 sort),再传给 interval;不要依赖图例顺序;coordinate.type: 'radial'"} +{"id": "radial-dark-theme", "prompt": "大屏是暗色背景,玉珏图也要暗色主题,文字和网格自动适配", "groundTruth": "设置 theme: 'classicDark' 或 'dark';type: 'interval' + coordinate.type: 'radial';无需逐项改文字颜色"} +{"id": "radial-negative-values", "prompt": "有几个月是负增长,想用玉珏图同时表达正负", "groundTruth": "玉珏图用弧长表达数值,无法表达正负方向;负值应改用 bar 条形图或发散条形图,并说明原因"} +{"id": "radial-many-categories", "prompt": "我有 20 个城市的销售额想用玉珏图对比", "groundTruth": "类别过多时玉珏图每环过薄不可读;建议改用 bar 条形图,或先取 Top N 收敛到 5~8 个类别;若坚持用 radial 需说明类别上限"} +{"id": "radial-v2-migration", "prompt": "旧项目里用的是 G2Plot v2 的 RadialBar,现在要迁移到 G2 v5", "groundTruth": "改为 import { Chart } from '@antv/g2';new Chart({ container, autoFit: true });options 中使用 type: 'interval' + coordinate: { type: 'radial', innerRadius };xField/yField/seriesField 改为 encode,radius/innerRadius 移入 coordinate"} diff --git a/examples/radial-bar/basic.ts b/examples/radial-bar/basic.ts new file mode 100644 index 0000000000..54dc2b6140 --- /dev/null +++ b/examples/radial-bar/basic.ts @@ -0,0 +1,45 @@ +import { Chart } from '@antv/g2'; + +/** + * 基础玉珏图(径向条形图) + * 场景:对比各获客渠道的转化率,用同心圆弧长度表达数值大小 + * 要点:interval + radial 坐标系;y 轴固定 0~1 映射百分比;隐藏坐标轴,用图例标识类别 + */ +const data = [ + { channel: '自然搜索', conversionRate: 0.42 }, + { channel: '社交推荐', conversionRate: 0.68 }, + { channel: '广告投放', conversionRate: 0.35 }, + { channel: '邮件营销', conversionRate: 0.24 }, + { channel: '直接访问', conversionRate: 0.55 }, +]; + +const chart = new Chart({ + container: 'container', + autoFit: true, +}); + +chart.options({ + type: 'interval', + data, + encode: { x: 'channel', y: 'conversionRate', color: 'channel' }, + coordinate: { type: 'radial', innerRadius: 0.25 }, + scale: { y: { domain: [0, 1] } }, + axis: { + x: { title: false, label: false, tick: false, line: false }, + y: { title: false, label: false, tick: false, line: false, grid: false }, + }, + legend: { color: { position: 'right' } }, + tooltip: { + title: 'channel', + items: [ + { + channel: 'y', + name: '转化率', + valueFormatter: (v) => `${(Number(v) * 100).toFixed(1)}%`, + }, + ], + }, + style: { radius: 4 }, +}); + +chart.render(); diff --git a/examples/radial-bar/semi-circle.ts b/examples/radial-bar/semi-circle.ts new file mode 100644 index 0000000000..df8db8ff0c --- /dev/null +++ b/examples/radial-bar/semi-circle.ts @@ -0,0 +1,53 @@ +import { Chart } from '@antv/g2'; + +/** + * 半圆玉珏图 + 自定义色板 + * 场景:大屏顶部展示各业务线的月度目标完成度,半圆造型更省纵向空间 + * 要点:startAngle / endAngle 限定半圆范围;scale.color.range 自定义品牌色;图例放顶部 + */ +const data = [ + { department: '电商', completion: 86 }, + { department: '本地生活', completion: 72 }, + { department: '云计算', completion: 94 }, + { department: '广告', completion: 63 }, + { department: '金融', completion: 78 }, +]; + +const chart = new Chart({ + container: 'container', + autoFit: true, +}); + +chart.options({ + type: 'interval', + data, + encode: { x: 'department', y: 'completion', color: 'department' }, + coordinate: { + type: 'radial', + innerRadius: 0.35, + startAngle: -Math.PI / 2, + endAngle: Math.PI / 2, + }, + scale: { + y: { domain: [0, 100] }, + color: { range: ['#5B8FF9', '#5AD8A6', '#5D7092', '#F6BD16', '#E8684A'] }, + }, + axis: { + x: { title: false, label: false, tick: false, line: false }, + y: { title: false, label: false, tick: false, line: false, grid: false }, + }, + legend: { color: { position: 'top' } }, + tooltip: { + title: 'department', + items: [ + { + channel: 'y', + name: '完成度', + valueFormatter: (v) => `${v}%`, + }, + ], + }, + style: { radius: 6 }, +}); + +chart.render(); diff --git a/examples/radial-bar/with-label.ts b/examples/radial-bar/with-label.ts new file mode 100644 index 0000000000..5e404c820c --- /dev/null +++ b/examples/radial-bar/with-label.ts @@ -0,0 +1,43 @@ +import { Chart } from '@antv/g2'; + +/** + * 带数值标签的玉珏图 + * 场景:展示各产品线的 NPS 得分,需要在每段圆弧末端直接读出分数 + * 要点:labels 用 text 回调拼接单位,position: 'outside' 定位到弧末端,autoRotate: false 保持文字水平 + */ +const data = [ + { product: '智能手表', nps: 72 }, + { product: '无线耳机', nps: 85 }, + { product: '运动手环', nps: 64 }, + { product: '智能音箱', nps: 58 }, + { product: '平板电脑', nps: 77 }, +]; + +const chart = new Chart({ + container: 'container', + autoFit: true, +}); + +chart.options({ + type: 'interval', + data, + encode: { x: 'product', y: 'nps', color: 'product' }, + coordinate: { type: 'radial', innerRadius: 0.25 }, + scale: { y: { domain: [0, 100] } }, + labels: [ + { + text: (d: { nps: number }) => `${d.nps} 分`, + position: 'outside', + autoRotate: false, + style: { fontSize: 12, fill: '#555' }, + }, + ], + axis: { + x: { title: false, label: false, tick: false, line: false }, + y: { title: false, label: false, tick: false, line: false, grid: false }, + }, + legend: { color: { position: 'right' } }, + style: { radius: 4 }, +}); + +chart.render(); diff --git a/skills/g2plot-v3/SKILL.md b/skills/g2plot-v3/SKILL.md index ca96e17bb3..3b59747051 100644 --- a/skills/g2plot-v3/SKILL.md +++ b/skills/g2plot-v3/SKILL.md @@ -25,6 +25,7 @@ description: G2Plot v3 可视化组件库,提示词即组件。当用户需要 | 多系列趋势对比 | x + y + 系列字段 | line(color 分组) | [charts/line.md](../../charts/line.md) | | 类别数值对比 | 类别 x + 数值 y | column 柱状图 | [charts/column.md](../../charts/column.md) | | 类别数值对比(横向) | 类别多/名称长 | bar 条形图 | [charts/bar.md](../../charts/bar.md) | +| 环形类别对比、排名 | 类别 + 非负数值(5~8 类) | radial-bar 玉珏图 | [charts/radial-bar.md](../../charts/radial-bar.md) | | 占比、构成 | 类别 + 数值 | pie 饼图 | [charts/pie.md](../../charts/pie.md) | | 趋势 + 累积量 | 时间 x + 数值 y | area 面积图 | [charts/area.md](../../charts/area.md) | | 双变量相关性 | 两个数值字段 | scatter 散点图 | [charts/scatter.md](../../charts/scatter.md) |