文章目录
-
- [React VChart使用](#React VChart使用)
-
React VChart使用
矩形转化漏斗图示例
- 漏斗图,形如"漏斗",用于单流程分析,在开始和结束之间由 N 个流程环节组成,通常这 N 个流程环节,有逻辑上的顺序关系。
js
复制代码
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 (
<FunnelChart
ref={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,
// text: datum => [`${datum.name}`, `${datum.value}`]
},
},
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: {
// tooltip 内容的回调,在最终显示 tooltip 前调用,可以在这个回调中修改 tooltip 内容的文本和样式,以及对 tooltip 内容行进行增、删、改、重新排序。
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 }} />);
// release react instance, do not copy
window.customRelease = () => {
ReactDom.unmountComponentAtNode(root);
};
实际项目使用
js
复制代码
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,
},
],
},
];
/**
* 获取图形数据所有点位的x、y坐标
*/
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,
// text: datum => [`${datum.name}`, `${datum.value}`]
},
},
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 (
<FunnelChart
ref={chartRef}
style={{ width: 400, height: 300 }}
spec={getSpec(funnelChartData)}
/>
);
};
export default FunnelChart;