饼图(Pie Chart)作为最常见的统计图表之一,用于可视化数据中不同分类的占比情况。使用 G2,在不进行任何定制的情况下,用户可以得到如下饼图:除去基本的图形元素外,还提供了 label 文本标注以及图例的绘制能力。同时还提供了 tooltip 提示信息、图例筛选等交互行为。
image.png2020-03-16 13-29-43.2020-03-16 13_31_59.gif

但是通常,用户更倾向于以下饼图的设计及交互:
image.png 2020-03-16 13-41-04.2020-03-16 13_41_28.gif
在定制之前,我们可以看看这两种饼图可视化方式的区别在哪里:
绘制层

  1. 使用空心环代替实心扇形;
  2. 图例的个性化定制,将原本 label 的内容同图例内容合并。

交互层

  1. 图形元素响应鼠标的状态发生改变:从默认的描边变为图形的放大突出;
  2. 图例的点击筛选变为点击突出图形元素

下面就让我们来看下,如何基于默认的饼图代码,一步一步快速、简单得定制吧!

默认饼图代码:

  1. import { Chart } from '@antv/g2';
  2. const data = [
  3. { item: '事例一', count: 40, percent: 0.4 },
  4. { item: '事例二', count: 21, percent: 0.21 },
  5. { item: '事例三', count: 17, percent: 0.17 },
  6. { item: '事例四', count: 13, percent: 0.13 },
  7. { item: '事例五', count: 9, percent: 0.09 },
  8. ];
  9. const chart = new Chart({
  10. container: 'container',
  11. autoFit: true,
  12. height: 500,
  13. });
  14. chart.coordinate('theta', {
  15. radius: 0.75,
  16. });
  17. chart.data(data);
  18. chart.scale('percent', {
  19. formatter: (val) => {
  20. val = val * 100 + '%';
  21. return val;
  22. },
  23. });
  24. chart.tooltip({
  25. showTitle: false,
  26. showMarkers: false,
  27. });
  28. chart
  29. .interval()
  30. .position('percent')
  31. .color('item')
  32. .label('percent', {
  33. content: (data) => {
  34. return `${data.item}: ${data.percent * 100}%`;
  35. },
  36. })
  37. .adjust('stack');
  38. chart.interaction('element-active');
  39. chart.render();

线上示例链接:https://g2.antv.vision/zh/examples/pie/basic

定制坐标系

扇形变圆环,只需一步!

image.png

定制图例

我们可以使用 G2 的自定义图例来实现预期的图例样式,在 G2 4.0 的图例中,我们开放了 itemNameitemName 属性,用于图例内容及样式的配置。
image.png
图例属性对应关系

具体的定制代码如下:

  1. // 声明需要进行自定义图例字段: 'item'
  2. chart.legend('item', {
  3. position: 'right', // 配置图例显示位置
  4. custom: true, // 关键字段,告诉 G2,要使用自定义的图例
  5. items: data.map((obj, index) => {
  6. return {
  7. name: obj.item, // 对应 itemName
  8. value: obj.percent, // 对应 itemValue
  9. marker: {
  10. symbol: 'square', // marker 的形状
  11. style: {
  12. r: 5, // marker 图形半径
  13. fill: chart.getTheme().colors10[index], // marker 颜色,使用默认颜色,同图形对应
  14. },
  15. }, // marker 配置
  16. };
  17. }),
  18. itemValue: {
  19. style: {
  20. fill: '#999',
  21. }, // 配置 itemValue 样式
  22. formatter: val => `${val * 100}%` // 格式化 itemValue 内容
  23. },
  24. });

同时我们将 label() 方法移除,就得到了下图:
image.png

定制交互

在 G2 V3 版本,自定义图例的交互需要用户操作图形/数据进行实现,而在 G2 4.0 版本,如果无意改变图形元素的响应样式,我们只需要声明需要的交互即可:

  1. chart.removeInteraction('legend-filter'); // 图例过滤操作默认内置,如不需要可以移除
  2. chart.interaction('element-active'); // 添加 element-active 交互:鼠标 hover 激活图形 active 状态

2020-03-16 15-10-03.2020-03-16 15_10_56.gif
但是我们需要的是图形元素方法效果,所以我们需要配置下图形元素 active 状态的样式:

  1. chart
  2. .interval()
  3. .state({
  4. active: {
  5. style: element => {
  6. const shape = element.shape;
  7. return {
  8. lineWidth: 10,
  9. stroke: shape.attr('fill'),
  10. strokeOpacity: shape.attr('fillOpacity'),
  11. };
  12. }, // 配置 active 样式,通过加粗边框实现放大效果
  13. },
  14. });

2020-03-16 13-41-04.2020-03-16 13_41_28.gif
至此,三个步骤,个性化饼图的定制就结束了!

扩展

基于以上定制,我们还可以作进一步扩展,利用圆环空心部分的面积显示鼠标击中图形元素的详细信息,以代替 tooltip。当然这也并不麻烦,我们只需要:

关闭 Tooltip

  1. chart.tooltip(false);

监听事件,更新 annotation 内容

  1. // 绘制 annotation
  2. let lastItem;
  3. function updateAnnotation(data) {
  4. if (data.item !== lastItem) {
  5. chart.annotation().clear(true);
  6. chart
  7. .annotation()
  8. .text({
  9. position: ['50%', '50%'],
  10. content: data.item,
  11. style: {
  12. fontSize: 20,
  13. fill: '#8c8c8c',
  14. textAlign: 'center',
  15. },
  16. offsetY: -20,
  17. })
  18. .text({
  19. position: ['50%', '50%'],
  20. content: data.count,
  21. style: {
  22. fontSize: 28,
  23. fill: '#8c8c8c',
  24. textAlign: 'center',
  25. },
  26. offsetX: -10,
  27. offsetY: 20,
  28. })
  29. .text({
  30. position: ['50%', '50%'],
  31. content: '台',
  32. style: {
  33. fontSize: 20,
  34. fill: '#8c8c8c',
  35. textAlign: 'center',
  36. },
  37. offsetY: 20,
  38. offsetX: 20,
  39. });
  40. chart.render(true);
  41. lastItem = data.item;
  42. }
  43. }
  44. // 清空 annotation
  45. function clearAnnotation() {
  46. chart.annotation().clear(true);
  47. chart.render(true);
  48. lastItem = null;
  49. }
  50. // 监听图例项点击事件
  51. chart.on('legend-item:click', (ev) => {
  52. const delegateObject = ev.gEvent.shape.get('delegateObject');
  53. const targetData = data.filter(obj => obj.item === delegateObject.item.name);
  54. if (targetData.length) {
  55. updateAnnotation(targetData[0]);
  56. }
  57. });
  58. // 监听图例项 mouseenter 事件
  59. chart.on('legend-item:mouseenter', (ev) => {
  60. const delegateObject = ev.gEvent.shape.get('delegateObject');
  61. const targetData = data.filter(obj => obj.item === delegateObject.item.name);
  62. if (targetData.length) {
  63. updateAnnotation(targetData[0]);
  64. }
  65. });
  66. // 监听图形元素 mouseenter 事件
  67. chart.on('element:mouseenter', (ev) => {
  68. if (ev.data) {
  69. updateAnnotation(ev.data.data);
  70. }
  71. });
  72. chart.on('element:mouseleave', (ev) => {
  73. clearAnnotation();
  74. });
  75. chart.on('legend-item:mouseleave', (ev) => {
  76. clearAnnotation();
  77. });

2020-03-16 16-49-06.2020-03-16 16_50_02.gif