|
| 1 | +import { G2, P } from '@antv/g2plot'; |
| 2 | +import { deepMix } from '@antv/util'; |
| 3 | + |
| 4 | +const currDate = ['2020-08-01']; |
| 5 | +const compareDate = ['2020-07-06']; |
| 6 | +const name = ['本期', '对比期']; |
| 7 | +const tooltipItemsName = ['本期留存', '本期流失', '对比期留存', '对比期流失']; |
| 8 | + |
| 9 | +const columnColor = [ |
| 10 | + { |
| 11 | + transformed: '#3b89ff', |
| 12 | + 'non-transformed': '#e1eeff', |
| 13 | + increased: '#e1eeff', |
| 14 | + }, |
| 15 | + { |
| 16 | + transformed: '#4ccaa1', |
| 17 | + 'non-transformed': '#defbf1', |
| 18 | + increased: '#defbf1', |
| 19 | + }, |
| 20 | +]; |
| 21 | +const rawData = [ |
| 22 | + { date: '2020-08-01', index: '投放点击用户数', type: '本期', value: 46893 }, |
| 23 | + { date: '2020-08-01', index: '会场曝光用户数', type: '本期', value: 37896 }, |
| 24 | + { date: '2020-08-01', index: '会场点击用户数', type: '本期', value: 34896 }, |
| 25 | + { date: '2020-08-01', index: '权益领取用户数', type: '本期', value: 28896 }, |
| 26 | + { date: '2020-08-01', index: '引导IUV', type: '本期', value: 14896 }, |
| 27 | + { date: '2020-08-01', index: '引导成交用户数', type: '本期', value: 4755 }, |
| 28 | + { date: '2020-07-06', index: '投放点击用户数', type: '对比期', value: 46893 }, |
| 29 | + { date: '2020-07-06', index: '会场曝光用户数', type: '对比期', value: 37896 }, |
| 30 | + { date: '2020-07-06', index: '会场点击用户数', type: '对比期', value: 34896 }, |
| 31 | + { date: '2020-07-06', index: '权益领取用户数', type: '对比期', value: 28896 }, |
| 32 | + { date: '2020-07-06', index: '引导IUV', type: '对比期', value: 36896 }, |
| 33 | + { date: '2020-07-06', index: '引导成交用户数', type: '对比期', value: 34896 }, |
| 34 | +]; |
| 35 | + |
| 36 | +function processData(rawData) { |
| 37 | + const res = []; |
| 38 | + [rawData.filter(({ type }) => type === '本期'), rawData.filter(({ type }) => type === '对比期')].forEach((data) => { |
| 39 | + const len = data.length - 1; |
| 40 | + for (let idx = 0; idx < data.length; idx += 1) { |
| 41 | + const prevVal = data[idx === 0 ? 0 : idx - 1].value; |
| 42 | + const nextVal = data[idx === len ? len : idx + 1].value; |
| 43 | + const { date, index, value, type } = data[idx]; |
| 44 | + res.push({ |
| 45 | + index, |
| 46 | + value, |
| 47 | + type, |
| 48 | + date, |
| 49 | + flag: 'transformed', |
| 50 | + }); |
| 51 | + const incFlag = value < nextVal; |
| 52 | + res.push({ |
| 53 | + index, |
| 54 | + type, |
| 55 | + date, |
| 56 | + value: Math.max(prevVal - value, 0), |
| 57 | + flag: incFlag ? 'increased' : 'non-transformed', |
| 58 | + rate: incFlag ? (nextVal - value) / value : nextVal / value, |
| 59 | + }); |
| 60 | + } |
| 61 | + }); |
| 62 | + return res; |
| 63 | +} |
| 64 | + |
| 65 | +function getRectPath(points) { |
| 66 | + const path = []; |
| 67 | + for (let i = 0; i < points.length; i++) { |
| 68 | + const point = points[i]; |
| 69 | + if (point) { |
| 70 | + const action = i === 0 ? 'M' : 'L'; |
| 71 | + path.push([action, point.x, point.y]); |
| 72 | + } |
| 73 | + } |
| 74 | + |
| 75 | + const first = points[0]; |
| 76 | + path.push(['L', first.x, first.y]); |
| 77 | + path.push(['z']); |
| 78 | + return path; |
| 79 | +} |
| 80 | + |
| 81 | +function getFillAttrs(cfg) { |
| 82 | + const defaultAttrs = { |
| 83 | + lineWidth: 0, |
| 84 | + fill: '#1890FF', |
| 85 | + fillOpacity: 0.85, |
| 86 | + }; |
| 87 | + |
| 88 | + return { |
| 89 | + ...defaultAttrs, |
| 90 | + ...cfg.style, |
| 91 | + fill: cfg.color, |
| 92 | + stroke: cfg.color, |
| 93 | + fillOpacity: cfg.opacity, |
| 94 | + }; |
| 95 | +} |
| 96 | + |
| 97 | +// 自定义 Shape |
| 98 | +G2.registerShape('interval', 'contrast-funnel', { |
| 99 | + draw(cfg, container) { |
| 100 | + const attrs = getFillAttrs(cfg); |
| 101 | + let rectPath = getRectPath(cfg.points); |
| 102 | + rectPath = this.parsePath(rectPath); |
| 103 | + |
| 104 | + const group = container.addGroup(); |
| 105 | + group.addShape('path', { |
| 106 | + attrs: { |
| 107 | + ...attrs, |
| 108 | + path: rectPath, |
| 109 | + }, |
| 110 | + }); |
| 111 | + const { flag } = cfg.data; |
| 112 | + if (cfg.nextPoints && flag !== 'transformed') { |
| 113 | + let linkPath = [ |
| 114 | + ['M', cfg.points[2].x, cfg.points[3].y], |
| 115 | + ['L', cfg.nextPoints[0].x, cfg.nextPoints[0].y], |
| 116 | + ]; |
| 117 | + |
| 118 | + if (cfg.nextPoints[0].y === 0) { |
| 119 | + linkPath[1] = ['L', cfg.nextPoints[1].x, cfg.nextPoints[1].y]; |
| 120 | + } |
| 121 | + linkPath = this.parsePath(linkPath); |
| 122 | + |
| 123 | + const [[, x1, y1], [, x2, y2]] = linkPath; |
| 124 | + group.addShape('path', { |
| 125 | + attrs: { |
| 126 | + path: linkPath, |
| 127 | + stroke: '#c5d0d9', |
| 128 | + }, |
| 129 | + }); |
| 130 | + const text = group.addShape('text', { |
| 131 | + attrs: { |
| 132 | + x: (x1 + x2) / 2, |
| 133 | + y: (y1 + y2) / 2, |
| 134 | + text: `${{ 'non-transformed': '▼转化率', increased: '▲增长率' }[flag]}${(cfg.data.rate * 100).toFixed(0)}%`, |
| 135 | + fill: { 'non-transformed': '#009f86', increased: '#ff4737' }[flag], |
| 136 | + textAlign: 'center', |
| 137 | + textBaseline: 'middle', |
| 138 | + fontSize: 14, |
| 139 | + }, |
| 140 | + zIndex: 2, |
| 141 | + }); |
| 142 | + const { x, y, width, height } = text.getBBox(); |
| 143 | + group.addShape('rect', { |
| 144 | + attrs: { |
| 145 | + x, |
| 146 | + y, |
| 147 | + width, |
| 148 | + height, |
| 149 | + fill: 'white', |
| 150 | + }, |
| 151 | + zIndex: 1, |
| 152 | + }); |
| 153 | + text.toFront(); |
| 154 | + } |
| 155 | + return group; |
| 156 | + }, |
| 157 | +}); |
| 158 | + |
| 159 | +const defaultOptions = {}; |
| 160 | + |
| 161 | +function adaptor(params) { |
| 162 | + const { chart, options } = params; |
| 163 | + const { data, theme } = options; |
| 164 | + |
| 165 | + chart.data(data); |
| 166 | + chart.legend(false); |
| 167 | + chart.theme(deepMix({}, theme || G2.getTheme(theme))); |
| 168 | + chart.scale('value', { nice: true }); |
| 169 | + |
| 170 | + chart.facet('mirror', { |
| 171 | + fields: ['type'], |
| 172 | + spacing: ['12%', 0], |
| 173 | + transpose: true, |
| 174 | + showTitle: false, |
| 175 | + eachView: (view, facet) => { |
| 176 | + const idx = facet.columnIndex; |
| 177 | + // 关闭所有 axis |
| 178 | + view.axis(false); |
| 179 | + view.legend({ |
| 180 | + custom: true, |
| 181 | + position: ['top-right', 'top-left'][idx], |
| 182 | + items: [ |
| 183 | + { |
| 184 | + name: name[idx], |
| 185 | + marker: { |
| 186 | + symbol: 'hyphen', |
| 187 | + style: { |
| 188 | + stroke: columnColor.map((c) => c.transformed)[idx], |
| 189 | + }, |
| 190 | + }, |
| 191 | + }, |
| 192 | + ], |
| 193 | + }); |
| 194 | + view |
| 195 | + .coordinate() |
| 196 | + .transpose() |
| 197 | + .scale(...(idx === 0 ? [-1, -1] : [1, -1])); |
| 198 | + view |
| 199 | + .interval() |
| 200 | + .adjust('stack') |
| 201 | + .position('index*value') |
| 202 | + .color('index*flag', (index, flag) => columnColor[idx][flag]) |
| 203 | + .label('value*flag', (value, flag) => { |
| 204 | + if (flag !== 'transformed') return { content: '' }; |
| 205 | + return { |
| 206 | + position: 'left', |
| 207 | + content: value.toLocaleString(), |
| 208 | + style: { |
| 209 | + textAlign: ['end', 'start'][idx], |
| 210 | + fill: '#fff', |
| 211 | + shadowColor: '#212121', |
| 212 | + shadowBlur: 5, |
| 213 | + }, |
| 214 | + }; |
| 215 | + }) |
| 216 | + .shape('contrast-funnel'); |
| 217 | + }, |
| 218 | + }); |
| 219 | + |
| 220 | + chart.tooltip({ |
| 221 | + shared: true, |
| 222 | + title: (item) => { |
| 223 | + return `${item}`; |
| 224 | + }, |
| 225 | + customItems: ([currSurplus, currLoss, compareSurplus, compareLoss]) => { |
| 226 | + const [currColor, completeColor] = columnColor; |
| 227 | + const [n1, n2, n3, n4] = tooltipItemsName; |
| 228 | + return [ |
| 229 | + { |
| 230 | + marker: true, |
| 231 | + color: currColor.transformed, |
| 232 | + name: `${n1}(${currSurplus.data.date})`, |
| 233 | + value: Number(currSurplus.value).toLocaleString(), |
| 234 | + }, |
| 235 | + { |
| 236 | + marker: true, |
| 237 | + color: currColor['non-transformed'], |
| 238 | + name: `${n2}(${currLoss.data.date})`, |
| 239 | + value: Number(currLoss.value).toLocaleString(), |
| 240 | + }, |
| 241 | + { |
| 242 | + marker: true, |
| 243 | + color: completeColor.transformed, |
| 244 | + name: `${n3}(${compareSurplus.data.date})`, |
| 245 | + value: Number(compareSurplus.value).toLocaleString(), |
| 246 | + }, |
| 247 | + { |
| 248 | + marker: true, |
| 249 | + color: completeColor['non-transformed'], |
| 250 | + name: `${n4}(${compareLoss.data.date})`, |
| 251 | + value: Number(compareLoss.value).toLocaleString(), |
| 252 | + }, |
| 253 | + ]; |
| 254 | + }, |
| 255 | + }); |
| 256 | + |
| 257 | + rawData |
| 258 | + .filter(({ type }) => type === '本期') |
| 259 | + .forEach(({ index }, idx) => { |
| 260 | + chart.annotation().text({ |
| 261 | + content: index, |
| 262 | + style: { textAlign: 'center', textBaseline: 'middle' }, |
| 263 | + position: () => { |
| 264 | + const { y: cY, height: cHeight } = chart.coordinateBBox; |
| 265 | + const { y: vY, height: vHeight } = chart.views[0].coordinateBBox; |
| 266 | + const yScale = chart.views[0].getScaleByField('index'); |
| 267 | + return ['50%', `${((vY - cY + vHeight * yScale.scale(index)) / cHeight) * 100}%`]; |
| 268 | + }, |
| 269 | + }); |
| 270 | + }); |
| 271 | + |
| 272 | + chart.removeInteraction('legend-filter'); // 移除图例过滤交互 |
| 273 | +} |
| 274 | + |
| 275 | +const funnel = new P('container', {}, adaptor, { data: processData(rawData) }); |
| 276 | + |
| 277 | +funnel.render(); |
0 commit comments