react + zarm + antV F2 实现账单数据统计饼图效果

简介: react + zarm + antV F2 实现账单数据统计饼图效果

需要实现的效果


为了方便展示,饼图放到右边标明:

ac9ac46fa2f54ba8b5b6bc8b21a9e3c0.png



实现过程


这里我们尝试用一下 antV F2 移动端可视化引擎来实现饼图效果


1.F2 移动端可视化引擎

F2 是一个专注于移动端,面向常规统计图表,开箱即用的可视化引擎,完美支持 H5 环境同时兼容多种环境(Node, 小程序),完备的图形语法理论,满足你的各种可视化需求,专业的移动设计指引为你带来最佳的移动端图表体验。


如何在 React 中使用:


npm install @antv/f2 --save
npm install @antv/f2-react --save


e26f762d6c1246e9893478aac44dddda.png

d6c340a63b4549dfb4a2d8df63b3458c.png


我们可以参考这里的例子:


9102f9f5c0734c129507926fbc3883db.png



2.封装饼图组件

我们在 components 里添加 PieChart 文件夹,里面新增 index.jsxstyle.module.less 文件,添加代码如下:


0f3580686cb04f87aecbec493b597000.png


import Canvas from "@antv/f2-react";
import { Chart, Interval, Legend, PieLabel } from "@antv/f2";
import PropTypes from 'prop-types';
import s from './style.module.less';
const PieChart = ({ chartData = [] }) => {
  console.log('进入 PieChart', chartData)
  return (
    <div className={s.pieChart}>
      { chartData.length > 0 ? <Canvas pixelRatio={window.devicePixelRatio}>
        <Chart
          data={chartData}
          coord={{
            type: "polar",
            transposed: true,
            radius: 1,
          }}
          scale={{}}
        >
          <Interval
            x="payType"
            y="percent"
            adjust="stack"
            color={{
              field: "type_name",
              range: ['#5a71c1', '#9eca7e', '#f3ca6b', '#df6e6b', '#84bedb', '#589f74', '#ed8a5c', '#1e80ff', '#fc5531', '#67c23a'],
            }}
            selection={{
              selectedStyle: (record) => {
                const { yMax, yMin } = record;
                return {
                  // 半径放大 1.1 倍
                  r: (yMax - yMin) * 1.1,
                };
              },
            }}
          />
          <Legend position="top" marker="square" nameStyle={{
            fontSize: '14',
            fill: '#000',
          }} style={{
            justifyContent: 'space-between',
            flexDirection: 'row',
            flexWrap: 'wrap'
          }}/>
          <PieLabel
            sidePadding={0}
            label1={(data) => {
              return {
                text: `${data.type_name}:${data.percent}%`,
                fill: "#0d1a26",
                fontSize: 12,
              };
            }}
          />
        </Chart>
      </Canvas> : null }
    </div>
  );
};
PieChart.propTypes = {
  chartData: PropTypes.array,
}
export default PieChart;


.pie-chart {
  min-height: 200px;
}



3.编写数据分析页面的逻辑

我们在 container 里添加 Data 文件夹,里面新增 index.jsxstyle.module.less 文件,以及 api 相关配置文件,添加代码如下:


c5312c7e35784232b0cc24e245bfd858.png



import React, { useEffect, useRef, useState } from 'react';
import { Icon, Progress, Toast } from 'zarm';
import cx from 'classnames';
import dayjs from 'dayjs';
import CustomIcon from '@/components/CustomIcon';
import PopupDate from '@/components/PopupDate';
import PieChart from '@/components/PieChart';
import { analysisMonthBill } from "./api/index.js";
import { typeMap } from '@/utils';
import s from './style.module.less';
const Data = () => {
  const monthRef = useRef();
  const [currentMonth, setCurrentMonth] = useState(dayjs().format('YYYY-MM')); // 当前月份
  const [totalType, setTotalType] = useState('expense'); // 收入或支出类型
  const [totalExpense, setTotalExpense] = useState(0); // 总支出
  const [totalIncome, setTotalIncome] = useState(0); // 总收入
  const [expenseData, setExpenseData] = useState([]); // 支出数据
  const [incomeData, setIncomeData] = useState([]); // 收入数据
  const [pieType, setPieType] = useState('expense'); // 饼图的「收入」和「支出」控制
  const [chartData, setChartData] = useState([]); // 饼图需要渲染的数据
  useEffect(() => {
    getData();
  }, [currentMonth]);
  // 获取数据详情
  const getData = async () => {
    const { status, desc, data } = await analysisMonthBill({
      billDate: currentMonth // 示例值:2022-02
    });
    console.log('获取数据详情', status, desc, data)
    if(status === 200) {
      // 总收支
      setTotalExpense(data.totalExpense);
      setTotalIncome(data.totalIncome);
      // 过滤支出和收入
      let expense_data = data.dataList.filter(item => item.pay_type == 1).sort((a, b) => b.number - a.number); // 过滤出账单类型为支出的项
      let income_data = data.dataList.filter(item => item.pay_type == 2).sort((a, b) => b.number - a.number); // 过滤出账单类型为收入的项
      expense_data = expense_data.map(item => {
        return {
          ...item,
          payType: item.pay_type.toString(),
          percent: Number(Number((item.number / Number(data.totalExpense)) * 100).toFixed(2))
        }
      })
      income_data = income_data.map(item => {
        return {
          ...item,
          payType: item.pay_type.toString(),
          percent: Number(Number((item.number / Number(data.totalIncome)) * 100).toFixed(2))
        }
      })
      setExpenseData(expense_data);
      setIncomeData(income_data);
      // 设置饼图数据
      setChartData(pieType == 'expense' ? expense_data : income_data);
    }else{
      Toast.show(desc);
    }
  };
  // 月份弹窗开关
  const monthShow = () => {
    monthRef.current && monthRef.current.show();
  };
  // 选择月份
  const selectMonth = (item) => {
    setCurrentMonth(item);
  };
  // 切换收支构成类型
  const changeTotalType = (type) => {
    setTotalType(type);
  };
  // 切换饼图收支类型
  const changePieType = (type) => {
    setPieType(type);
    setChartData(type == 'expense' ? expenseData : incomeData);
  }
  return <div className={s.data}>
    <div className={s.total}>
      <div className={s.time} onClick={monthShow}>
        <span>{currentMonth}</span>
        <Icon className={s.date} type="date" />
      </div>
      <div className={s.title}>共支出</div>
      <div className={s.expense}>¥{ totalExpense }</div>
      <div className={s.income}>共收入¥{ totalIncome }</div>
    </div>
    <div className={s.structure}>
      <div className={s.head}>
        <span className={s.title}>收支构成</span>
        <div className={s.tab}>
          <span onClick={() => changeTotalType('expense')} className={cx({ [s.expense]: true, [s.active]: totalType == 'expense' })}>支出</span>
          <span onClick={() => changeTotalType('income')} className={cx({ [s.income]: true, [s.active]: totalType == 'income' })}>收入</span>
        </div>
      </div>
      <div className={s.content}>
        {
          (totalType == 'expense' ? expenseData : incomeData).map(item => <div key={item.type_id} className={s.item}>
            <div className={s.left}>
              <div className={s.type}>
                <span className={cx({ [s.expense]: totalType == 'expense', [s.income]: totalType == 'income' })}>
                  <CustomIcon
                    type={item.type_id ? typeMap[item.type_id].icon : 1}
                  />
                </span>
                <span className={s.name}>{ item.type_name }</span>
              </div>
              <div className={s.progress}>¥{ Number(item.number).toFixed(2) || 0 }</div>
            </div>
            <div className={s.right}>
              <div className={s.percent}>
                <Progress
                  shape="line"
                  percent={Number((item.number / Number(totalType == 'expense' ? totalExpense : totalIncome)) * 100).toFixed(2)}
                  theme='primary'
                />
              </div>
            </div>
          </div>)
        }
      </div>
      <div className={s.proportion}>
        <div className={s.head}>
          <span className={s.title}>收支构成</span>
          <div className={s.tab}>
            <span onClick={() => changePieType('expense')} className={cx({ [s.expense]: true, [s.active]: pieType == 'expense'  })}>支出</span>
            <span onClick={() => changePieType('income')} className={cx({ [s.income]: true, [s.active]: pieType == 'income'  })}>收入</span>
          </div>
        </div>
        {/* 饼图 */}
        <PieChart chartData={chartData} />
      </div>
    </div>
    <PopupDate ref={monthRef} mode="month" onSelect={selectMonth} />
  </div>
}
export default Data


.data {
  min-height: 100%;
  background-color: #f5f5f5;
  .total {
    background-color: #fff;
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 24px 0;
    margin-bottom: 10px;
    .time {
      position: relative;
      width: 96px;
      padding: 6px;
      background-color: #f5f5f5;
      color: rgba(0, 0, 0, .9);
      border-radius: 4px;
      font-size: 14px;
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 16px;
      span:nth-of-type(1)::after {
        content: '';
        position: absolute;
        top: 9px;
        bottom: 8px;
        right: 28px;
        width: 1px;
        background-color: rgba(0, 0, 0, .5);
      }
      .date {
        font-size: 16px;
        color: rgba(0, 0, 0, .5);
      }
    }
    .title {
      color: #007fff;
      margin-bottom: 8px;
      font-size: 12px;
      font-weight: 500;
    }
    .expense {
      font-size: 24px;
      color: #007fff;
      font-weight: 600;
      margin-bottom: 16px;
    }
    .income {
      color: rgba(0, 0, 0, .5);
      font-weight: 500;
    }
  }
  .structure {
    padding: 0 16px 54px;
    background-color: #fff;
    margin-bottom: 10px;
    .head {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 12px 0;
      .title {
        font-size: 18px;
        color: rgba(0, 0, 0, .9);
      }
      .tab {
        span {
          display: inline-block;
          width: 40px;
          height: 24px;
          background-color: #f5f5f5;
          text-align: center;
          line-height: 24px;
          margin-left: 10px;
          border-radius: 4px;
        }
        .expense {
          &.active {
            background-color: rgba(0, 127, 255, 0.2);
            color: #007fff;
          }
        }
        .income {
          &.active {
            background-color: rgba(236, 190, 37, 0.2);
            color: rgb(236, 190, 37);
          }
        }
      }
    }
    .content {
      .item {
        display: flex;
        height: 50px;
        align-items: center;
        .left {
          flex: 4;
          display: flex;
          align-items: center;
          justify-content: space-between;
          margin-right: 10px;
          .type {
            display: flex;
            align-items: center;
            span:nth-of-type(1) {
              display: flex;
              justify-content: center;
              align-items: center;
              border-radius: 50%;
              width: 30px;
              height: 30px;
              margin-right: 10px;
              color: #fff;
              flex-shrink: 0;
            }
            .name {
              width: 30px;
            }
            .expense {
              background-color: #007fff;
            }
            .income {
              background-color: rgb(236, 190, 37);
            }
          }
        }
        .right {
          flex: 8;
          display: flex;
          align-items: center;
          .percent {
            flex: 1;
            :global {
              .za-progress__track {
                background: transparent;
              }
            }
          }
          .momey {
            width: 100px;
          }
        }
      }
    }
    .proportion {
      background-color: #fff;
      padding: 12px 0;
      .head {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 12px 0;
        .title {
          font-size: 18px;
          color: rgba(0, 0, 0, .9);
        }
        .tab {
          span {
            display: inline-block;
            width: 40px;
            height: 24px;
            background-color: #f5f5f5;
            text-align: center;
            line-height: 24px;
            margin-left: 10px;
            border-radius: 4px;
          }
          .expense {
            &.active {
              background-color: rgba(0, 127, 255, 0.2);
              color: #007fff;
            }
          }
          .income {
            &.active {
              background-color: rgba(236, 190, 37, 0.2);
              color: rgb(236, 190, 37);
            }
          }
        }
      }
    }
  }
}


import { fetchData } from "@/utils/axios.js";
// 获取月度统计账单
export function analysisMonthBill(data) {
  return fetchData('/api/analysis/monthBill', 'get', data);
}



4.测试一下效果

我们可以通过切换月份得到该月份的统计数据

48c7813abc1f4b439982ba53bf7221db.png


收支构成可以通过切换支出跟收入查看:

9d1f0700f2ce4c4285f0e232b9d0caa8.png

4c6a824a1f114d4a8ee6fa5fdacd1ab6.png




饼图构成也是一下的交互


333b291b8ba2472db1fb1dbe7fb2c0aa.png


42c56970f55e4140825d7f0e99fa20dd.png

















目录
相关文章
|
前端开发
react recharts饼图 及配置项
react recharts饼图 及配置项
162 0
|
前端开发 JavaScript 数据安全/隐私保护
react + zarm + rc-form + crypto-js 实现个人中心页面,头像上传,密码重置,登录退出功能
react + zarm + rc-form + crypto-js 实现个人中心页面,头像上传,密码重置,登录退出功能
143 0
react + zarm + rc-form + crypto-js 实现个人中心页面,头像上传,密码重置,登录退出功能
|
前端开发 API 容器
react + zarm 实现账单详情页以及编辑删除功能
react + zarm 实现账单详情页以及编辑删除功能
75 0
react + zarm 实现账单详情页以及编辑删除功能
|
前端开发 容器
react + zarm 实现新增账单弹窗封装
react + zarm 实现新增账单弹窗封装
94 0
react + zarm 实现新增账单弹窗封装
|
前端开发 API
react + zarm 实现账单列表类型以及时间条件弹窗封装
react + zarm 实现账单列表类型以及时间条件弹窗封装
95 0
react + zarm 实现账单列表类型以及时间条件弹窗封装
|
前端开发 API 容器
react + zarm 实现账单列表展示页
react + zarm 实现账单列表展示页
99 0
react + zarm 实现账单列表展示页
|
29天前
|
前端开发 JavaScript 开发者
深入理解React Hooks:提升前端开发效率的关键
【10月更文挑战第5天】深入理解React Hooks:提升前端开发效率的关键
|
23天前
|
前端开发
深入解析React Hooks:构建高效且可维护的前端应用
本文将带你走进React Hooks的世界,探索这一革新特性如何改变我们构建React组件的方式。通过分析Hooks的核心概念、使用方法和最佳实践,文章旨在帮助你充分利用Hooks来提高开发效率,编写更简洁、更可维护的前端代码。我们将通过实际代码示例,深入了解useState、useEffect等常用Hooks的内部工作原理,并探讨如何自定义Hooks以复用逻辑。
|
29天前
|
前端开发 JavaScript API
探索React Hooks:前端开发的革命性工具
【10月更文挑战第5天】探索React Hooks:前端开发的革命性工具
|
2天前
|
前端开发 JavaScript 开发者
颠覆传统:React框架如何引领前端开发的革命性变革
【10月更文挑战第32天】本文以问答形式探讨了React框架的特性和应用。React是一款由Facebook推出的JavaScript库,以其虚拟DOM机制和组件化设计,成为构建高性能单页面应用的理想选择。文章介绍了如何开始一个React项目、组件化思想的体现、性能优化方法、表单处理及路由实现等内容,帮助开发者更好地理解和使用React。
17 8