React VChart使用
- react-vchart 文档
- @visactor/react-vchart
- 该组件主要对 VChart 的图表部件做 React 组件化的封装,相关的配置项均与 VChart 一致
- react-vchart图表
矩形转化漏斗图示例
- 漏斗图,形如“漏斗”,用于单流程分析,在开始和结束之间由 N 个流程环节组成,通常这 N 个流程环节,有逻辑上的顺序关系。
const root = document.getElementById(CONTAINER_ID);
const { VChart, FunnelChart, Pie, Legend } = ReactVChart;
const { useState, useRef, useEffect, useCallback } = React;const data = [{id: "funnel",values: [{value: 100,name: "Resume Screening",percent: 1,},{value: 80,name: "Resume Evaluation",percent: 0.8,},{value: 50,name: "Evaluation Passed",percent: 0.5,},{value: 30,name: "Interview",percent: 0.3,},{value: 10,name: "Final Pass",percent: 0.1,},],},
];const Card = () => {const chartRef = useRef(null);useEffect(() => {window["vchart"] = null;}, []);return (<FunnelChartref={chartRef}style={{ width: 390, height: 286 }}spec={{type: "funnel",categoryField: "name",valueField: "value",data,maxSize: "60%",minSize: "27%",isTransform: true,heightRatio: 0.46,transformLabel: {visible: true,style: {fill: "black",},formatMethod: (text, datum) => {console.log("转化率", text, datum);return `${(datum.__VCHART_FUNNEL_VALUE_RATIO * 100).toFixed(2)}%`;},},shape: "rect",label: {visible: true,formatMethod: (text, datum) => {return {type: "rich",text: [{text: `${datum.name}`,},{text: `\n${datum.value}`,fontSize: 14,fontWeight: "bold",textAlign: "center",},],};},style: {fontSize: 12,lineHeight: 18,limit: Infinity,},},color: {type: "ordinal",range: ["#2778E2", "#005FC5", "#0048AA", "#00328E"],},padding: {left: "-25%",top: 0,},funnel: {style: {cornerRadius: 2,stroke: "white",lineWidth: 2,},state: {hover: {stroke: "#4e83fd",lineWidth: 1,},},},transform: {style: {stroke: "white",lineWidth: 2,},state: {hover: {stroke: "#4e83fd",lineWidth: 1,},},},tooltip: {visible: true,mark: {updateContent: (content) => console.log("content", content),},},legends: {visible: true,orient: "bottom",},extensionMark: [{type: "polygon",dataId: "funnel",style: {points: (datum, ctx, params, dataView) => {const data = dataView.latestData;if (!data) return;const curIndex = data.findIndex((d) => d.name === datum.name);if (curIndex !== 0) return;if (curIndex === data.length - 1) {return;}const nextDatum = data[curIndex + 2];const firstDatum = data[0];const points = ctx.vchart.getChart().getSeriesInIndex(0)[0].getPoints(datum);const nextPoints = ctx.vchart.getChart().getSeriesInIndex(0)[0].getPoints(nextDatum);const firstPoints = ctx.vchart.getChart().getSeriesInIndex(0)[0].getPoints(firstDatum);const tr = points[1];const tb = points[2];const next_tr = nextPoints[1];const first_tr = firstPoints[1];const result = [{ x: tr.x + 5, y: (tr.y + tb.y) / 2 },{ x: first_tr.x + 20, y: (tr.y + tb.y) / 2 },{x: first_tr.x + 20,y: (tr.y + tb.y) / 2 + (next_tr.y - tr.y) - 10,},{x: next_tr.x + 5,y: (tr.y + tb.y) / 2 + (next_tr.y - tr.y) - 10,},];return result;},cornerRadius: 5,stroke: "rgb(200,200,200)",strokeOpacity: 0.5,lineWidth: 2,closePath: false,},},{type: "symbol",dataId: "funnel",style: {visible: (datum, ctx, params, dataView) => {const data = dataView.latestData;if (!data) return;const curIndex = data.findIndex((d) => d.name === datum.name);console.log("curIndex", datum, ctx, params, dataView, curIndex);if (curIndex !== 1) return false;if (curIndex === data.length - 1) {return false;}return true;},x: (datum, ctx, params, dataView) => {const data = dataView.latestData;if (!data) return;const curIndex = data.findIndex((d) => d.name === datum.name);if (curIndex === data.length - 1) {return;}const nextDatum = data[curIndex + 1];const nextPoints = ctx.vchart.getChart().getSeriesInIndex(0)[0].getPoints(nextDatum);const next_tr = nextPoints[1];return next_tr.x + 5;},y: (datum, ctx, params, dataView) => {const data = dataView.latestData;if (!data) return;const curIndex = data.findIndex((d) => d.name === datum.name);if (curIndex === data.length - 1) {return;}const nextDatum = data[curIndex + 1];const points = ctx.vchart.getChart().getSeriesInIndex(0)[0].getPoints(datum);const nextPoints = ctx.vchart.getChart().getSeriesInIndex(0)[0].getPoints(nextDatum);const tr = points[1];const tb = points[2];const next_tr = nextPoints[1];return (tr.y + tb.y) / 2 + (next_tr.y - tr.y) - 10;},size: 8,scaleX: 0.8,symbolType: "triangleLeft",cornerRadius: 2,fill: "rgb(200,200,200)",},},{type: "text",dataId: "funnel",style: {text: (datum) => [datum.name, ` ${datum.percent * 100}%`],textAlign: "left",visible: (datum, ctx, params, dataView) => {const data = dataView.latestData;if (!data) return;const curIndex = data.findIndex((d) => d.name === datum.name);if (curIndex !== 2) return false;if (curIndex === data.length - 1) {return false;}return true;},x: (datum, ctx, params, dataView) => {const data = dataView.latestData;if (!data) return;const firstDatum = data[0];const firstPoints = ctx.vchart.getChart().getSeriesInIndex(0)[0].getPoints(firstDatum);const tr = firstPoints[1];return tr.x + 20 + 10;},y: (datum, ctx, params, dataView) => {const data = dataView.latestData;if (!data) return;const curIndex = data.findIndex((d) => d.name === datum.name);if (curIndex === data.length - 1) {return;}const points = ctx.vchart.getChart().getSeriesInIndex(0)[0].getPoints(datum);const tr = points[1];return (tr.y * 2) / 3;},fontSize: 12,fill: "black",},},],}}/>);
};ReactDom.createRoot(root).render(<Card style={{ width: 390, height: 350 }} />);
window.customRelease = () => {ReactDom.unmountComponentAtNode(root);
};
实际项目使用
import { useRef } from "react";
import { FunnelChart } from "@visactor/react-vchart";const funnelChartData = [{id: "funnel",values: [{value: 100,name: "Resume Screening",percent: 1,},{value: 80,name: "Resume Evaluation",percent: 0.8,},{value: 50,name: "Evaluation Passed",percent: 0.5,},{value: 30,name: "Interview",percent: 0.3,},{value: 10,name: "Final Pass",percent: 0.1,},],},
];
const getPoints = (ctx, datum) =>ctx.vchart.getChart().getSeriesInIndex(0)[0].getPoints(datum);
const checkDataExists = (dataView, datum) => {const config = {isExist: true,curIndex: -1,data: dataView.latestData ?? null,};if (!config?.data) {config.isExist = false;return config;}config.curIndex = config.data.findIndex((d) => d.name === datum.name);if (config.curIndex === config.data.length - 1) {config.isExist = false;return config;}return config;
};
const extensionMark = [{type: "text",dataId: "funnel",style: {text: (datum) => [datum.name, ` ${datum.percent}`],textAlign: "left",visible: (datum, ctx, params, dataView) => {const { isExist, curIndex, data } = checkDataExists(dataView, datum);if (!isExist) {return false;}if (curIndex !== 2) {return false;}return true;},x: (datum, ctx, params, dataView) => {const data = dataView.latestData;if (!data) {return;}const firstDatum = data[0];const firstPoints = getPoints(ctx, firstDatum);const tr = firstPoints[1];return tr.x + 20 + 10;},y: (datum, ctx, params, dataView) => {const { isExist, data } = checkDataExists(dataView, datum);if (!isExist) {return;}const points = getPoints(ctx, datum);const tr = points[1];return (tr.y * 2) / 3;},fontSize: 12,fill: "black",},},{type: "polygon",dataId: "funnel",style: {points: (datum, ctx, params, dataView) => {const { isExist, curIndex, data } = checkDataExists(dataView, datum);if (!isExist) {return;}if (curIndex !== 0) {return;}const nextDatum = data[curIndex + 2];const firstDatum = data[0];const points = getPoints(ctx, datum);const nextPoints = getPoints(ctx, nextDatum);const firstPoints = getPoints(ctx, firstDatum);const tr = points[1];const tb = points[2];const next_tr = nextPoints[1];const first_tr = firstPoints[1];const result = [{ x: tr.x + 5, y: (tr.y + tb.y) / 2 },{ x: first_tr.x + 20, y: (tr.y + tb.y) / 2 },{x: first_tr.x + 20,y: (tr.y + tb.y) / 2 + (next_tr.y - tr.y) - 10,},{x: next_tr.x + 5,y: (tr.y + tb.y) / 2 + (next_tr.y - tr.y) - 10,},];return result;},cornerRadius: 5,stroke: "rgb(200,200,200)",strokeOpacity: 0.5,lineWidth: 2,closePath: false,},},{type: "symbol",dataId: "funnel",style: {visible: (datum, ctx, params, dataView) => {const { isExist, curIndex } = checkDataExists(dataView, datum);if (!isExist) {return;}if (curIndex !== 1) {return false;}return true;},x: (datum, ctx, params, dataView) => {const { isExist, curIndex, data } = checkDataExists(dataView, datum);if (!isExist) {return;}const nextDatum = data[curIndex + 1];const nextPoints = getPoints(ctx, nextDatum);const next_tr = nextPoints[1];return next_tr.x + 5;},y: (datum, ctx, params, dataView) => {const { isExist, curIndex, data } = checkDataExists(dataView, datum);if (!isExist) {return;}const nextDatum = data[curIndex + 1];const points = getPoints(ctx, datum);const nextPoints = getPoints(ctx, nextDatum);const tr = points[1];const tb = points[2];const next_tr = nextPoints[1];return (tr.y + tb.y) / 2 + (next_tr.y - tr.y) - 10;},size: 8,scaleX: 0.8,symbolType: "triangleLeft",cornerRadius: 2,fill: "rgb(200,200,200)",},},
];const getSpec = (data) => {return {type: "funnel",categoryField: "name",valueField: "value",data,maxSize: "60%",minSize: "30%",padding: {top: 0,left: "-10%",},isTransform: true,transformLabel: {visible: true,style: {fill: "black",},formatMethod: (text, datum) =>`${(datum?.__VCHART_FUNNEL_VALUE_RATIO * 100).toFixed(2)}%`,},shape: "rect",label: {visible: true,formatMethod: (text, datum) => ({type: "rich",text: [{text: `${datum.name}`,},{text: `\n${datum.value}`,fontSize: 14,fontWeight: "bold",textAlign: "center",},],}),style: {fontSize: 12,lineHeight: 18,limit: Infinity,},},color: {type: "ordinal",range: ["#2778E2", "#005FC5", "#0048AA", "#00328E"],},funnel: {style: {cornerRadius: 2,stroke: "white",lineWidth: 2,},state: {hover: {stroke: "#4e83fd",lineWidth: 1,},},},transform: {style: {stroke: "white",lineWidth: 2,},state: {hover: {stroke: "#4e83fd",lineWidth: 1,},},},legends: {visible: true,orient: "bottom",},extensionMark,};
};const FunnelChart = (props) => {const { data } = props;const chartRef = useRef(null);return (<FunnelChartref={chartRef}style={{ width: 400, height: 300 }}spec={getSpec(funnelChartData)}/>);
};export default FunnelChart;