ECharts 实现 GA 行为流图


拿一个官网的效果图做示意:

STEP1 从 BigQuery 中获取行为流数据

以下为 SQL 语句:

WITH  `all_data` AS ( -- 原始数据集
  SELECT   `source`,
    `target`,
    `level`,
    `value`,
    `all`,
    `no`,
    DENSE_RANK() OVER (PARTITION BY `level` ORDER by `all` DESC) AS `source_no`
  FROM(
  SELECT
    `source`,
    `target`,
    `level`,
    `value`,
    SUM(`value`) OVER (PARTITION BY `source`, `level`) AS `all`, -- 使用 SUM() 获取该 SOURCE 对应的总流量
    ROW_NUMBER() OVER (PARTITION BY `source`, `level` ORDER by `value` DESC) AS `no`
  FROM ( -- 此处对 SOURCE 的所有流量进行聚合,查看从一个路径流出的所有流量,并对组内流量进行排序,添加编号,以适用于后续过滤
    SELECT
      `source`,
      `target`,
      `level`,
      COUNT(fullVisitorId) AS `value`
    FROM (
      SELECT
        clientId,
        fullVisitorId,
        -- 客户ID
        visitId,
        -- 会话标识符,对于用户具有唯一性 fullVisitorId + visitId -> 标识一个唯一会话
        hits.type AS type,
        -- 匹配类型
        hits.hitNumber AS hitNumber,
        -- 此用户会话次数,首次会话为1
        hits.appInfo.screenName AS `source`,
        hits.eventInfo.eventCategory AS ec,
        hits.eventInfo.eventAction AS ea,
        hits.eventInfo.eventLabel AS el,
        hits.time AS `time`,
        -- 从发送第一个匹配到发送该匹配的间隔时间(毫秒)
        ROW_NUMBER() OVER (PARTITION BY fullVisitorId, visitNumber ORDER BY hits.hitNumber ASC ) AS `level`,
        -- 使用编号函数,计算得到当前 SOURCE 的 LEVEL 等级
        LEAD(hits.appInfo.screenName) OVER (PARTITION BY fullVisitorId, visitNumber ORDER BY hits.hitNumber ASC ) AS `target`, 
        -- 使用 LEAD() 导航函数获取后续行的 hits.appInfo.screenName 传值
      FROM
        `xxx.ga_sessions_*`,
        UNNEST(hits) AS hits
      WHERE
        hits.type = 'APPVIEW' -- 限制匹配类型,仅为 APP 屏幕浏览
        AND ( _Table_suffix BETWEEN 'YYYYMMDD'
          AND 'YYYYMMDD' ) ) t1
      WHERE 
          `source` IS NOT NULL
    GROUP BY
      `source`,
      `target`,
      `level`
    ORDER BY
      `level` ASC,
      `value` DESC ) t2
  ORDER BY `all` DESC, `value` DESC
  ) t3 
)
SELECT -- 最终结果仅需保留
  `source`,
  `target`,
  `level`,
  `value`
FROM (
  WITH `keep_5_source` AS (
    SELECT *
    FROM (
      -- -- 1) 聚合每个 LEVEL 下的 SOURCE 令起只保留前五位,而将后续 TARGET 设置为 Other
      SELECT * FROM `all_data` WHERE `source_no` < 6
      UNION ALL 
      SELECT 
        `source`,
        `target`,
        `level`,
        `value`,
        `all`,
        ROW_NUMBER() OVER (PARTITION BY `level` ORDER by `value` DESC) AS `no`,
        `source_no`
      FROM (
        SELECT 
          'Other' as `source`,
          -- 'Other' as `target`, -- 如果不区分 Other 的 Target,采用此行
          `target`, -- 区分,采用此行
          `level`,
          SUM(`value`) as `value`,
          -1 as `all`,
          6 as `source_no`
        FROM (SELECT * FROM `all_data` WHERE `source_no` >= 6)
        GROUP BY `source`, `target`, `level`
      )
    )
    ORDER BY `level` ASC, `all` DESC, `value` DESC
  )
  SELECT * -- 2) 聚合每个 SOURCE 下的 TARGET,另其只保留前5位,而将后续 TARGET 设置为 Other
  FROM (
    SELECT * FROM `keep_5_source` WHERE `no` < 6
      UNION ALL 
    SELECT 
      `source`,
      'Other' AS `target`,
      `level`,
      SUM(`value`) AS `value`,
      `all`,
      6 AS `no`,
      `source_no`
    FROM (SELECT * FROM `keep_5_source` WHERE `no` >= 6)
    GROUP BY `source`, `target`, `level`, `source_no`, `all`
  )
  ORDER BY `level` ASC, `all` DESC, `no` ASC
) 
WHERE `level` < 6 -- level 暂时只保留前五

注意

上述 SQL 其实并不完善,还存在如下问题,需要改进: 1)行为流本身是一个有向无环图,在聚合 Other 时,很大几率会把下个 level 中依赖项给聚合掉,导致流量出现无归属的情况

有部分功能点,可选添加: 1)设置所有流量的入口 2)直接通过 BQ 将表数据转换为 JSON 格式 3)调整页面浏览行为流为页面浏览+事件混合行为流 - 这个处理起来其实很简单,筛选 hit type 包含 event 即可。 ... 以上需求,实现并不复杂,故不赘述。

STEP2 将 BQ 数据导出为符合 ECharts 要求的 JSON 格式

在 BQ 中获取的语句,通过简单处理后,得到符合要求的 JSON 数据,其最终得到的数据格式,如下所示:

# ECharts 的桑基图中,主要包括 nodes & links 两部分(也可以理解为 点、线)
# 如下例所示:
# {"nodes":[
# {"name":"Agricultural 'waste'"},
# {"name":"Bio-conversion"},
# {"name":"Liquid"},
# ...
# {"name":"Wind"}
# ],
# "links":[
# {"source": "Agricultural 'waste'", "target": "Bio-conversion",  "value": 124.729},
# {"source": "Bio-conversion", "target": "Liquid",  "value": 0.597},
# {"source": "Bio-conversion",  "target": "Losses",  "value": 26.862},
# ...
# {"source": "Wave", "target": "Electricity grid", "value": 19.013},
# ]}
# 其中 nodes 中定义了节点名称
# links 中定义了 来源、目标、值
# 可以看到 ECharts 的桑基图中,没有主观的定义级别,所以我们需要手动区分不同级别下的节点(nodes-name = origin-name + level)

这块其实转换方式非常多样,既可以直接在 BQ 中通过 JSON 函数将数转换为 JSON 格式,也可以导出为 CSV 后,自行处理为 JSON,此处使用 Python、Java、Node.js 都很简单,就不赘述了。

以下简单介绍一下,如何在 BQ 中转换为符合要求的 JSON 数据:


STEP3 使用 ECharts 对行为流数据进行渲染

ECharts 桑基图 官方说明文档

是一种特殊的流图(可以看作是有向无环图)。 它主要用来表示原材料、能量等如何从最初形式经过中间过程的加工或转化达到最终状态。

具体实现示例可参考

唯一需要注意的是,桑基图默认表现为一种有向无环图,所以针对网页这种,还需要自己标注一下层级,也就说 echarts 没法自己帮你设置分层 而是根据节点的关系设置,比如你看到的是 A -> B -> C B -> A 这么两条路径,这里面的两个B,一个是着陆页,一个是第二页(以页面浏览为示例),但是如果直接把数据 A -> B B -> C B -> A 灌入 ECharts,其是无法区分 B 所属层级的,所以需要转而标注成: A1 -> B2 -> C3 B1 -> A2 这样才能正确的展示。 这样产生的坏处是看起来会比较难看,不过我们可以通过 Formatter 的形式,处理实际显示的标签,自定义显示的样式。

myChart.showLoading(); // 显示一个 Loading 框
$.get(ROOT_PATH + 'data/asset/data/energy.json', function (data) { // 加载外部数据,这里我们需要用我们自己的数
    myChart.hideLoading(); // 隐藏 Loading

    myChart.setOption(option = {
        title: {
            text: 'Sankey Diagram'
        },
        tooltip: {
            trigger: 'item',
            triggerOn: 'mousemove'
        },
        series: [
            {
                type: 'sankey',
                data: data.nodes,
                links: data.links,
                focusNodeAdjacency: 'allEdges',
                itemStyle: {
                    borderWidth: 1,
                    borderColor: '#aaa'
                },
                lineStyle: {
                    color: 'source',
                    curveness: 0.5
                }
            }
        ]
    });
});