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;
相关推荐
ZJ_.8 分钟前
WPSJS:让 WPS 办公与 JavaScript 完美联动
开发语言·前端·javascript·vscode·ecmascript·wps
GIS开发特训营12 分钟前
Vue零基础教程|从前端框架到GIS开发系列课程(七)响应式系统介绍
前端·vue.js·前端框架·gis开发·webgis·三维gis
Cachel wood38 分钟前
python round四舍五入和decimal库精确四舍五入
java·linux·前端·数据库·vue.js·python·前端框架
学代码的小前端39 分钟前
0基础学前端-----CSS DAY9
前端·css
joan_8543 分钟前
layui表格templet图片渲染--模板字符串和字符串拼接
前端·javascript·layui
m0_748236111 小时前
Calcite Web 项目常见问题解决方案
开发语言·前端·rust
Watermelo6171 小时前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
m0_748248941 小时前
HTML5系列(11)-- Web 无障碍开发指南
前端·html·html5
m0_748235612 小时前
从零开始学前端之HTML(三)
前端·html
一个处女座的程序猿O(∩_∩)O4 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js