ReactVChart矩形转化漏斗图配置示例

文章目录

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;
相关推荐
我要洋人死30 分钟前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人41 分钟前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人42 分钟前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR1 小时前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香1 小时前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q2498596931 小时前
前端预览word、excel、ppt
前端·word·excel
小华同学ai1 小时前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书
Gavin_9151 小时前
【JavaScript】模块化开发
前端·javascript·vue.js
懒大王爱吃狼2 小时前
Python教程:python枚举类定义和使用
开发语言·前端·javascript·python·python基础·python编程·python书籍
小牛itbull3 小时前
ReactPress:重塑内容管理的未来
react.js·github·reactpress