拿一个官网的效果图做示意:
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 没法自己帮你设置分层 而是根据节点的关系设置,比如你看到的是 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
}
}
]
});
});