验证码: 看不清楚,换一张 查询 注册会员,免验证
  • {{ basic.site_slogan }}
  • 打开微信扫一扫,
    您还可以在这里找到我们哟

    关注我们

怎么在前端使用JS进行分类

阅读:1065 来源:乙速云 作者:代码code

怎么在前端使用JS进行分类

      提出问题

      不扯远了,先来看问题。根据下面的样例数据,要求得到

      • 先按业务,再按部门分组的数据;

      • 不按部门,直接按业务分别统计每年的数据

      [
        {
          name: "部门1",
          businesses: [
            {
              name: "产品销售",
              years: [
                { name: "2021", value: 132 }, { name: "2022", value: 183 }, { name: "2023", value: 207 }
              ]
            },
            {
              name: "原料采购",
              years: [
                { name: "2021", value: 143 }, { name: "2022", value: 121 }, { name: "2023", value: 120 }
              ]
            }
          ]
        },
        {
          name: "部门2",
          businesses: [
            {
              name: "产品销售",
              years: [
                { name: "2021", value: 230 }, { name: "2022", value: 112 }, { name: "2023", value: 288 }
              ]
            },
            {
              name: "原料采购",
              years: [
                { name: "2021", value: 168 }, { name: "2022", value: 203 }, { name: "2023", value: 115 }
              ]
            }
          ]
        },
        {
          name: "部门3",
          businesses: [
            {
              name: "产品销售",
              years: [
                { name: "2021", value: 279 }, { name: "2022", value: 163 }, { name: "2023", value: 271 }
              ]
            },
            {
              name: "原料采购",
              years: [
                { name: "2021", value: 129 }, { name: "2022", value: 121 }, { name: "2023", value: 226 }
              ]
            }
          ]
        }
      ];

      这个数据,如果用金山文档的轻维表(飞书多维表类似)来查看,会更直观

      原数据(按部门再按业务)的轻维表呈现

      怎么在前端使用JS进行分类

      按业务再按部门分组的轻维表呈现

      怎么在前端使用JS进行分类

      按业务按年统计的轻维表呈现

      怎么在前端使用JS进行分类

      展平多级数据

      原数据按部门再按业务进行了两级分类,所以它不是简单的二维表(行/列)数据,而是在二维表的基础上增加了两个维度(部门/业务)。从要求来看,我们需要的是从另外的维度(业务/部门,业务/年度)来进行处理。所以需要先把这些数据降维展开成可以重新划分维度的程度,也就是二维表。

      JS 中二维表的表示方法挺多,行对象集合是最常见的一种,这里我们也就采用这种表示方法。

      还有一种常见的方式是列集合+行集合,其中行集合可以是对象表示(字段名对应)也可以是数组表示(索引号对应)。不过这种表示一会是用在 UI 中。单纯数据处理用行对象集合就够了,不需要单独的列信息。

      观察原数据的每一级,发现名称都命名为 name,但是子集命名各不相同,层级有限。由于对每一层需要去处理名称到列(对象属性名)的转换,也需要对不同名称的子集进行进一步处理,各层级之间缺乏显而易见的共性,不太适合递归的方式来处理。所以我们定做一个展开函数。

      下面是对原数据量身定做的展开函数,展开后会得到一个包含部门 (dept)、业务 (business)、年份 (year)、数值 (value) 四个属性的对象集合。

      function flatBusinesses(list) {
          return list.flatMap(({ name: dept, businesses }) => {
              return businesses.flatMap(({ name: business, years }) => {
                  return years.map(({ name: year, value }) => ({
                      dept,
                      business,
                      year,
                      value
                  }));
              });
          });
      }


      晋级:如果想用递归该怎么处理?

      并不是多级展开就一定会用到递归。比如规则的数组结构,比如规则的树结构,是可以使用递归遍历展开的。但是像这个案例的数据,每一层的子级属性名称都不同,层级有限,需要逐级处理。

      如果实在想用递归的话,也可以通过一个参数来定义每一级的处理规则。以这个例子来说,每一级要处理两件事:① 找到子级节点属性名;② 将 name 处理成适当的名称用在展开的数据中。

      function flatMultiLevelList(list, rules) {
          return flatList(list, 0);
      
          function flatList(list, level) {
              const rule = rules[level];
              if (!rule) { return [{}]; }
              // 取得 field(子级属性名)和 convert(属性处理器)
              // 如果没有 convert 则指定一个默认的 it => it,即不做转换
              const { field, convert = it => it } = rule;
              if (field) {
                  // 如果存在子级,则继续 flatMap,展平。
                  // ❶ { fff, ...others } 可以将 fff 属性从原对象中剥离出来
                  // ❷ { [feild]: nodes } 解构可以将 field 的值所指向的属性取出来赋予一个叫 nodes 的变量
                  return list.flatMap(({ [field]: nodes, ...props }) => {
                      return flatList(nodes, level + 1).map(it => ({ ...convert(props), ...it }));
                  });
              } else {
                  // 如果不存在子级,只需要对当前节点进行转换,直接返回即可
                  return list.map(it => convert(it));
              }
          }
      }

      展开后会拿到这样的数据(假设赋值变量 table

      [
          { "dept": "部门1", "business": "产品销售", "year": 2021, "value": 132 },
          { "dept": "部门1", "business": "产品销售", "year": 2022, "value": 183 },
          { "dept": "部门1", "business": "产品销售", "year": 2023, "value": 207 },
          { "dept": "部门1", "business": "原料采购", "year": 2021, "value": 143 },
          { "dept": "部门1", "business": "原料采购", "year": 2022, "value": 121 },
          { "dept": "部门1", "business": "原料采购", "year": 2023, "value": 120 },
          { "dept": "部门2", "business": "产品销售", "year": 2021, "value": 230 },
          { "dept": "部门2", "business": "产品销售", "year": 2022, "value": 112 },
          ...
      ]

      拿到二维表之后,某些需要的数据或视图就可以通过电子表格来获得。比如问题一中需要的统计数据,使用电子表格的透视图功能就能实现,而金山文档的轻维表,或者飞书的多维表可以实现得更容易。不过我们现在需要用代码来实现。

      分类及分类汇总

      第一个问题的需求是分类和分类汇总。说到分类,那首先想到的肯定是 group 操作。很可惜原生 JS 不支持 group,如果想用现成的,可以考虑 Lodash,要自己写一个倒也不难。group 操作前面提到的展开操作的逆操作。

      function groupBy(list, key) {
          // 这里简单地兼容一下传入 key 值和 keyGetter 的情况
          const getKey = typeof key === "function" ? key : it => it[key];
          return list.reduce(
              (groups, it) => {
                  (groups[getKey(it)] ??= []).push(it);
                  return groups;
              },
              {}  // 空对象作为初始 groups
          );
      }

      按业务再按部门分组

      有了 groupBy,可以先按业务进行分组

      // 前面假设展平的数据存放在变量 table 中
      const groups = groupBy(table, "dept");

      现在我们拿到的 byDept 是一个 JS 对象(注意不是数组哦),其键是部门名称,值是一个数组,包含该部门下的所有数据。接下来进行第二层分组,是需要对 byDept 的每一个“值”进行分组处理。

      for (const key in groups) {
          const list = groups[key];
          groups[key] = groupBy(list, "business");
      }

      处理之后的 groups 长得像这样

      {
          "产品销售": {
              "部门1": [
                  { dept: "部门1", business: "产品销售", year: "2021", value: 132 },
                  ...
              ],
              "部门2": [
                  { dept: "部门2", business: "产品销售", year: "2021", value: 230 },
                  ...
              ],
              "部门3": ...
          },
          "原料采购": ...
      }

      结果是拿到了,但是和符合原始的数据规范(原始层级每层是用 name 属性作为字段名,子级命名各不相同)所以还需要做一次转换。比如第一层的转换是这样:

      const converted = Object.entries(groups)
          .map(([name, depts]) => ({ name, depts }));

      它会把第一层(对象)处理成数组,每个元素包含 name 和 depts 两个属性,name 属性是名称,depts 则是按部门分组的结果(目前还是对象)。那么第二、三层转换也类似。把前面的分组和后面的转换合并起来,是这样

      const result1 = Object.entries(groupBy(table, "business"))
          .map(([name, list]) => ({
              name,
              depts: Object.entries(groupBy(list, "dept"))
                  .map(([name, list]) => ({
                      name,
                      years: list.map(({ year: name, value }) => ({ name, value }))
                  }))
          }));

      得到最终结果

      [
        {
          name: "产品销售",
          depts: [
            {
              name: "部门1",
              years: [{ name: "2021", value: 132 }, { name: "2022", value: 183 }, { name: "2023", value: 207 }]
            },
            {
              name: "部门2",
              years: [{ name: "2021", value: 230 }, { name: "2022", value: 112 }, { name: "2023", value: 288 }]
            },
            {
              name: "部门3",
              years: [{ name: "2021", value: 279 }, { name: "2022", value: 163 }, { name: "2023", value: 271 }]
            }
          ]
        },
        ...
      ]

      按业务分组再按年统计

      对于第一个问题的第二个需求,要按年统计业务(忽略部门),处理方法与上面的方法类型。第二层分组改为按年份,而不是按部门;同时第二层的数组转换时不再转换第三层的数据,而是对第三层数据进行汇总。

      const result2 = Object.entries(groupBy(table, "business"))
          .map(([name, list]) => ({
              name,
              years: Object.entries(groupBy(list, "year"))
      //      ^^^^^                               ^^^^^^ 按年分组
                  .map(([name, list]) => ({
                      name,
                      value: list.reduce((sum, { value }) => sum + value, 0)
      //              ^^^^^ 直接取值,使用 reduce 汇总
                  }))
          }));

      结果(用前面做的轻维表统计来核对一下,完全正确)

      [
        {
          name: "产品销售",
          years: [{ name: "2021", value: 641 }, { name: "2022", value: 458 }, { name: "2023", value: 766 }]
        },
        {
          name: "原料采购",
          years: [{ name: "2021", value: 440 }, { name: "2022", value: 445 }, { name: "2023", value: 461 }]
        }
      ]

      如果用 Lodash 会怎么写

      用 Lodash 来处理代码结构看起来更清晰一些,但代码量不见得少。

      展开的部分用 Lodash 和使用原生方法没什么区别,都是使用 flatMap。Lodash 提供的 flatMapDeep 可以用来展开纯粹的多级数组,但在这里不适用,因为每一级都不是单纯的展开,而是要进行单独的映射处理。Lodash 的 flatMapDeep 更像是原生的 map().flat(Number.MAX_SAFE_INTEGER)

      const result1 = _(table)
          // groupBy 的结果是一个对象,属性名是组名,属性值是组内数据列表。
          .groupBy("business")
          // 第一种处理值集的方法,先把值处理了 (mapValues),再来处理键值对 (map)
          .mapValues(depts => _(depts)
              .groupBy("dept")
              // 第二种处理值集的方法,处理键值对的时候,同时处理值集合
              .map((values, name) => ({
                  name,
                  years: values.map(({ year: name, value }) => ({ name, value }))
              }))
              .value()
          )
          .map((depts, name) => ({ name, depts }))
          .value();
      const result2 = _(table).groupBy("business")
          .map((list, name) => ({
              name,
              years: _(list).groupBy("year")
                  .map((list, name) => ({
                      name,
                      value: _.sumBy(list, "value")
                  }))
                  .value()
          }))
          .value();

      小结

      如果需要对某个数据进行分类或者分类汇总,首先得拿到这个数据的二维表,也就是完全展开的数据列表。多数情况下从后端拿到的数据都是二维表,毕竟关系型数据库逻辑结构是表存储。接下来所谓的“分类”其实就是分组操作,而“汇总”就是把分类后的子列表拿来进行聚合计算(计数、合计、平均、最大/小等都是聚合计算),得到最终的结果。

    分享到:
    *特别声明:以上内容来自于网络收集,著作权属原作者所有,如有侵权,请联系我们: hlamps#outlook.com (#换成@)。
    相关文章
    {{ v.title }}
    {{ v.description||(cleanHtml(v.content)).substr(0,100)+'···' }}
    你可能感兴趣
    推荐阅读 更多>