|
| 1 | +import React, { useRef, useEffect } from 'react'; |
| 2 | +import ReactDOM from 'react-dom'; |
| 3 | +import { Funnel } from '@antv/g2plot'; |
| 4 | +import insertCss from 'insert-css'; |
| 5 | + |
| 6 | +const formatter = (v) => `${(v * 100).toFixed(2)}%`; |
| 7 | + |
| 8 | +const Plot = () => { |
| 9 | + const data = [ |
| 10 | + { stage: '简历投递数', count: 3511 }, |
| 11 | + { stage: '简历评估通过数', count: 1024 }, |
| 12 | + { stage: '终面通过数', count: 148 }, |
| 13 | + { stage: 'offer 数', count: 119 }, |
| 14 | + { stage: '入职数', count: 70 }, |
| 15 | + ].map((d) => ({ ...d, _count: 1 })); |
| 16 | + |
| 17 | + const chartNodeRef = useRef(); |
| 18 | + const plotRef = useRef(); |
| 19 | + |
| 20 | + const renderAnntations = (plot) => { |
| 21 | + const { chart } = plot; |
| 22 | + const coord = chart.getCoordinate(); |
| 23 | + const elements = chart.geometries[0].elements; |
| 24 | + const parsePoints = []; |
| 25 | + elements.forEach((ele, idx) => { |
| 26 | + const { points, nextPoints } = ele.shape.get('origin'); |
| 27 | + if (nextPoints) { |
| 28 | + let p0 = { y: (points[1].y + points[2].y) / 2 }; |
| 29 | + if (idx > 0) { |
| 30 | + p0.x = parsePoints[idx - 1][1].x; |
| 31 | + } else { |
| 32 | + p0.x = (points[2].x + points[1].x) / 2; |
| 33 | + } |
| 34 | + let p3 = { x: (nextPoints[2].x + nextPoints[1].x) / 2, y: (nextPoints[2].y + nextPoints[1].y) / 2 }; |
| 35 | + parsePoints.push([p0, p3]); |
| 36 | + } |
| 37 | + }); |
| 38 | + let container = chart.getLayer('fore').findById('annotation-group'); |
| 39 | + if (!container) { |
| 40 | + container = chart.getLayer('fore').addGroup({ id: 'annotation-group' }); |
| 41 | + } else { |
| 42 | + container.clear(); |
| 43 | + } |
| 44 | + parsePoints.forEach((point, idx) => { |
| 45 | + const p0 = coord.convert(point[0]); |
| 46 | + const p3 = coord.convert(point[1]); |
| 47 | + const path = [ |
| 48 | + ['M', p0.x, p0.y], |
| 49 | + ['L', p0.x + 15, p0.y], |
| 50 | + ['L', p3.x + 15, p3.y - 2], |
| 51 | + ['L', p3.x, p3.y - 2], |
| 52 | + ]; |
| 53 | + container.addShape('path', { |
| 54 | + attrs: { |
| 55 | + path, |
| 56 | + stroke: '#d3d3d3', |
| 57 | + lineWidth: 1, |
| 58 | + }, |
| 59 | + }); |
| 60 | + const fontSize = 10; |
| 61 | + const offset = 5; |
| 62 | + container.addShape('text', { |
| 63 | + attrs: { |
| 64 | + x: (path[0][1] + path[1][1]) / 2 + offset, |
| 65 | + y: (path[1][2] + path[2][2] + fontSize) / 2, |
| 66 | + text: `转化率:${formatter(data[idx + 1].count / data[idx].count)}`, |
| 67 | + fontSize: 10, |
| 68 | + fill: '#333', |
| 69 | + }, |
| 70 | + }); |
| 71 | + }); |
| 72 | + chart.render(true); |
| 73 | + }; |
| 74 | + |
| 75 | + useEffect(() => { |
| 76 | + // Step 2: 创建图表 |
| 77 | + if (chartNodeRef && chartNodeRef.current) { |
| 78 | + const plot = new Funnel(chartNodeRef.current, { |
| 79 | + data: data, |
| 80 | + xField: 'stage', |
| 81 | + yField: '_count', |
| 82 | + legend: false, |
| 83 | + conversionTag: false, |
| 84 | + dynamicHeight: true, |
| 85 | + label: { |
| 86 | + formatter: (text, item) => { |
| 87 | + return `${item._origin.stage} ${item._origin.count}`; |
| 88 | + }, |
| 89 | + }, |
| 90 | + tooltip: { |
| 91 | + customItems: (items) => { |
| 92 | + return items.map((d) => ({ ...d, value: d.data.count })); |
| 93 | + }, |
| 94 | + }, |
| 95 | + funnelStyle: { |
| 96 | + stroke: '#fff', |
| 97 | + lineWidth: 3, |
| 98 | + }, |
| 99 | + }); |
| 100 | + |
| 101 | + // Step 3: 渲染图表 |
| 102 | + plot.render(); |
| 103 | + plotRef.current = plot; |
| 104 | + } |
| 105 | + }, [chartNodeRef]); |
| 106 | + |
| 107 | + useEffect(() => { |
| 108 | + const plot = plotRef && plotRef.current; |
| 109 | + if (plot) { |
| 110 | + renderAnntations(plot); |
| 111 | + plot.chart.on('afterchangesize', () => { |
| 112 | + renderAnntations(plot); |
| 113 | + }); |
| 114 | + } |
| 115 | + }, [plotRef]); |
| 116 | + |
| 117 | + return ( |
| 118 | + <section className={'wrapper'}> |
| 119 | + <div ref={chartNodeRef} /> |
| 120 | + </section> |
| 121 | + ); |
| 122 | +}; |
| 123 | + |
| 124 | +// 我们用 insert-css 演示引入自定义样式 |
| 125 | +// 推荐将样式添加到自己的样式文件中 |
| 126 | +// 若拷贝官方代码,别忘了 npm install insert-css |
| 127 | +insertCss(``); |
| 128 | + |
| 129 | +ReactDOM.render(<Plot />, document.getElementById('container')); |
0 commit comments