概述
“数据可视化将数据通过组织结构处理,赋予数据信息更高层次的生命,代入到特定框架的业务场景中,帮助用户进行信息识别与行为决策。本文以JMT网格地图为例,介绍可视化图形如何与地理信息、数据统计相结合,支持深度定制化的数据分析场景。”
背景
京东即时零售能力的升级,接入10万家实体零售门店,“小时购”店铺网络已经全面铺开,具有相当高的地域覆盖度。数据产品如何支持业务进行数智化营销,数据分析师需要获得更细粒度用户搜索数据与商品供给数据的匹配情况,准确识别出这部分区域优先进行招商,可以极大地提高区域店铺覆盖的策略与效果,有效推动业务发展。
场景分析
针对细粒度的地域范围与用户搜索数据的分析场景,数据分析师关注的重点在哪里,分析流程又是怎样的。我们 设计团队邀请业务方进行用户访谈,了解大家在数据分析中的关键核心诉求,主要集中在以下三个方面:
多层级视角分析:业务需要实现地域多层级可视化及下探,全国—省份—城市—网格—门店,逐层展示,按照不同业务视角在同一层级进行指标对比,可直接下探维度查询主题相关指标,并且能够自由切换圈选的区域范围和直观了解门店各数据指标在区域中的表现。
区域粒度圈选:业务方店铺网络的配送范围为3公里,为确保可以覆盖到全部门店的分布范围,我们在前期准备时尝试了多种粒度规格,以朝阳区为例,朝阳区总面积470.8平方千米,如果区域圈选规格太小,同一行政区域下的区域分布密度就会太高,影响视觉识别度同时增加了分析难度,因此区域圈选的范围设置应小于2.12公里。为了能够提供给用户更精细化且易用性更高的分析数据,我们最终确定了三种区域圈选规格:3km、2.1km、1.5km,从大到小的区域粒度分层。
数据可视化展示:区域圈选与地图的结合需要应保证视觉效果清晰简洁,既不遮挡地图中的关键信息又能清楚展现所圈选的数据信息。在细分数据量级时,可视化图表与数据映射关系应保持一致,易理解且能够清晰地被用户感知,数据信息的对比应足够明确。
这三种关键诉求背后都有复杂的业务场景,我们从挖掘真实的需求开始,结合技术创新展示清晰自然的地域数据可视化,并且在不同场景下满足多样性的需求,整体使用流程为:区域选择-区域表现-维度拆解。我们逐步将概念方案打磨成有价值的具体落地方案。
设计方案
关于地图
数据可视化的诞生可追溯至地图出现,地图最早用于导航、标记、区域探索。自经纬线纳入地图后,标志着真正意义上的地图形成,经纬线形成的“网格”能更精准地定位具体地理位置与方向。数据可视化发展至今,地图的功能应用扩展成位置标注、路线规划、周边搜索、地图选址等多方面,即使是媒介与地理监测科技的高速发展,地图的核心功能依然集中于方向导航、位置标记、区域探索这三方面。地理信息系统(GIS)地域探测与地图图像处理的技术已经相当成熟,在地图的选择与应用时需注意是否符合国家标准。
埃拉托色尼是首个把经纬线纳入地图的地理学家
关于色块热力图:
在信息可视化与视觉设计中,颜色是最重要的元素之一。其中热力图,是一种主要通过对色块着色来显示数据的统计图表。数据关联相应的颜色映射规则。例如在常见的热力图中,较深的颜色表示较大的值,较浅的颜色表示较小的值;或者由色相识别数据,偏暖的颜色表示较大的值,较冷的颜色表示较小的值,等等不同方案。色块热力图适合用于查看总体情况、发现异常值、显示多个变量之间的差异,以及检测它们之间是否存在任何相关性。因此在设计时,需要选择合适的颜色,在视觉上有较好的识别度,信息主旨传达能够明确清晰。
普通热力图 @Tom O'Hara
Google map 地图热力图
地图+网格热力图数据可视化能力融合
在将地理信息基础载体的地图与数据信息映射的热力图相结合时,可视化设计需要遵循以下三个原则:
准确:设计表达不曲解不误导信息,优先考虑数据呈现的准确性与完整性,精准反应数据可视化特征。
清晰:清晰包含两个层面,结构清晰与内容清晰。明确的布局结构决定用户的浏览动线,帮助使用者高效便捷地获取信息。
有效:可支持用户对数据深度与数据维度复杂性的需求,灵活支持数据可视化呈现,为用户提供更有帮助的分析条件与功能。
可视化层级拆解
我们将网格地图的可视化拆分为以下三个层级:底层为城市地图信息,清晰有效地呈现地理和具体的区域范围;中间层为色块热力图,将复杂的数据直观地呈现出来;顶层为圈选区域内具体的区域信息、门店定位信息。三者的结合可以帮助我们为业务提供更精细化的数据可视化分析视角。
映射颜色选择
首先是基础底层地图的选择,我们在此案例中应用腾讯地图作为技术选型,整体保证地理信息展示的清晰明确。其次设计师需要在地图的整体效果之上确定热力图的映射颜色:在可视化设计中,配色方案关系到可视化数据表达的准确性与美观性,设计时可以将颜色选择分为亮度、饱和度、色调和透明度四个方面。为了与地图色调保持和谐一致,带来愉悦的视觉效果,我们选取了暖色系渐变色调,用较高饱和度的黄-橙-红来表示数值大小,数值越大,颜色由黄至红转变,使不同层级的数据能更容易地被用户识别区分。另外还有特别需要关注的是,在极端数据情况下的识别度:保证在数据量极大与数据量极小的情况下,热力方块可视状态仍能明显识别,实现区域色块的可读性和易读性。同时为了不遮挡地图中的关键信息,网格色块有一定的透明度,可更呈现出地图底层的地理信息,方便用户精准定位。
图表动态交互
区别于传统静态图表的表现形式,可交互图表(又称交互式可视化)不局限于信息的展示,通过改进用户与图表的交互访问行为,获取更深层次的数据信息。
信息聚焦
我们将可交互网格热力地图的用户体验建立在信息探索的过程上,主要经历了全局概览--局部表现--聚焦重点三个步骤,设计为每个关键步骤提供了更好的可视化交互路径,选择了悬停、点击、框选三个基础交互动作实现操作控制,易用性更高且更便于控制。图表中热力色块、区域圈选、定位标记元件,通过尺寸、颜色、动效变化等交互反馈,为用户提供更易于理解的可视化引导。同时关联信息弹窗优化了信息的呈现方式,吸引用户聚焦于信息表现本身,降低数据的理解难度,更有效地进行数据分析。
信息关联
业务场景中每个区域粒度的表现情况都互相关联,用户需要逐层级获取更详细的信息才能明确判断区域的问题原因。设计方案按照全国-省-市三种维度逐步下钻,到城市层级的时候再进行区域粒度划分与数据聚合热力展示。在循序渐进的分析视角里提供可交互的抽屉式窗口视图,关联所选区域与所选主题指标的对比分析,同时可下钻拆分更细粒度的店铺维度数据,同步看到关联区域的具体信息与店铺指标分析维度的拆解情况。同时,用户可通过筛选过滤,数据排序等扩展功能,对数据信息进行进一步的探索。
实现方案
以下由我们的研发小姐姐给各位介绍下网格地图的功能实现方案。
场景及术语
本次需求中需要实现的是区域图、网格图、market图三种可视化图表,均是通过可视化的方式显示GIS地理区域上的数据,使用地图作为背景,在此基础上实现各自的图表图层。
这三种图表可进行交互,主要是:全国区域图可点击,点击后展示省市区区域图,区域图点击后展示网格图、网格图点击后展示market图,全国区域 —> 省市级区域 —> 网格区域 —> Market图。
区域图 —— 基于全国、省、市、区
通过图形的位置来表现数据的地理位置,用以展示不同地区的轮廓形状和数据分布。
网格图 —— 基于区、县
网格热力图将离散的数据点以多边形网格区域进行聚合,根据落入区域内的数据点数量渲染不同颜色的多边形区域。
market图 —— 基于网格图
通过设置的多个点的位置信息在地图上进行绘制,可自定义标注的图标。
技术方案
目前echarts、hightcharts、腾讯地图等都可以实现地图的可视化功能,但是echarts、hightcharts对于地图可视化的实现并没有细化到区县甚至到街道的地图并且目前全国地图的展示不符合国家的一个规范展示;腾讯地图对于地图可视化的实现是有成熟的技术支持与友好的展示,并且可支持到街道、门店粒度的可视化展示。
因此,选取腾讯地图作为基础底层地图,通过腾讯地图的 JavaScript API GL + 数据可视化 JS API 实现地图上可视化的图层。
JavaScript API GL
Javascript API GL是基于WebGL技术打造的3D版地图API,3D化的视野更为自由,交互更加流畅。提供丰富的功能接口,包括点、线、面绘制,自定义图层、个性化样式及绘图、测距工具等,使开发者更加容易的实现产品构思。充分发挥GPU的并行计算能力,同时结合WebWorker多线程技术,大幅度提升了大数据量的渲染性能。最高支持百万级点、线、面绘制,同时可以保持高帧率运行。
数据可视化 JS API
数据可视化 JS API,是基于腾讯位置服务Javascript API GL实现的专业地理空间数据可视化渲染引擎。通过这套API,可以实现轨迹数据、坐标点数据、热力、迁徙、航线等空间数据的可视化展现。提供的可视化效果是以图层的方式叠加在JavaScript API GL之上,图层中所显示的数据由实例化的对象统一管理。
开发前准备
账号准备
申请key
第一步:首先登录https://lbs.qq.com/,注册账号并登录。
第二步:控制台-应用管理-我的应用-创建应用
第三步:应用创建完成后,基于应用创建key
第四步:生成key后,便可将key应用在腾讯地图上
<script charset="utf-8" src="https://map.qq.com/api/gljs?v=1.exp&key=OB4BZ-D4W3U-B7VVO-4PJWW-6TKDJ-WPB77"></script>
设置并配置主题地图
第一步:个性化地图-个性化样式-我的样式,选取模板或自定义样式
第二步:个性化样式设置完成后,可以样式应用里看到各种配置的样式
第三步:可将生成的不同的样式应用在地图上
const map = new TMap.Map("container", { mapStyleId: 'style2' });
组件实现
由于本次需求的交互是区域图点击打开网格图、网格图打开获取网格内的门店market图,考虑到性能及扩展性,将不同类型的图表封装成同一组件,通过传入的props来展示不同的图表及交互。通过该种方案来实现,首先对于底层地图只需要请求一次即可,提升性能;之后对于其他类型的地图图表可通过该方案快速接入,扩展性较好。如下图,是图表组件的一个简单的流程图。
MAP组件
第一步:props设计,除了一些比较基础的信息设置,其中render函数主要是不同地图类型的函数,是将多图表实现至一个组件的关键属性。
第二步:异步加载腾讯地图js、地图进行初始化,在多个地图图表进行切换时,只需进行一次异步加载即可。
第三步:图表渲染、图例渲染,根据不同的图表来渲染不同的图例。
class Map extends PureComponent {
componentDidMount() {
const { mapKey, mapOptions } = this.props;
loadScript(mapKey).then(() => { // 加载地图
// 初始化
this.mapRender = new TMap.Map(this.map, mapOptions);
});
}
// ...
render() {
return (
<React.Fragment>
<div ref={ref => { this.map = ref; }} />
{ // 可在此展示图例 }
</React.Fragment>
);
}
}
Map.propTypes = {
mapKey: PropTypes.string, // 地图key
mapOptions: PropTypes.object, // 地图的基础配置
render: PropTypes.func, // render函数,用于处理数据及渲染图层
data: PropTypes.array, // 图层数据
formatRender: PropTypes.func, // render数据计算
onClick: PropTypes.func, // 点击事件
showTooltip: PropTypes.bool, // 是否展示tooltip
tooltipRender: PropTypes.func, // tooltip node
getAreaCallback: PropTypes.func, // 获取围栏数据
showColorAxis: PropTypes.bool // 是否展示颜色图例
};
Map.defaultProps = {};
/**
* @description 异步加载js
* @param {String} mapKey 申请的腾讯地图的key值
* @param {Func} callback 回调函数
*/
const loadScript = (mapKey, callback) => new Promise((resolve, reject) => {
const url = `https://map.qq.com/api/gljs?v=1.exp&libraries=visualization&key=${mapKey}`
const script = document.createElement('script');
script.type = 'text/javascript';
if (script.readyState) {
script.onreadystatechange = function () {
if (script.readyState === 'loaded' || script.readyState === 'complete') {
script.onreadystatechange = null;
resolve(callback?.());
}
};
} else {
script.onload = function () {
resolve(callback?.());
};
}
script.src = url;
script.onerror = reject;
document.body.appendChild(script);
});
图表实现
在腾讯地图的基础上绘制不同图表的图层,针对区域图表、网格图表的实现步骤分为四步:1.获取围栏数据;2.热力计算;3.初始化图表;4.事件设置。
1、获取区域围栏
第一步:获取不同区域的围栏,腾讯地图的WebService API 实现
腾讯地图WebService API 是基于HTTPS/HTTP协议的数据接口,开发者可以使用任何客户端、服务器和开发语言,按照腾讯地图WebService API规范,按需构建HTTPS请求,并获取结果数据(目前支持JSON/JSONP方式返回)。
可查看具体信息:
https://lbs.qq.com/service/webService/webServiceGuide/webServiceDistrict
获取全部行政区划数据 | https://apis.map.qq.com/ws/district/v1/list?key=OB4BZ-D4W3U-B7VVO-4PJWW-6TKDJ-WPB77 |
获取子级行政区划数据 | https://apis.map.qq.com/ws/district/v1/getchildren?id=110000&key=OB4BZ-D4W3U-B7VVO-4PJWW-6TKDJ-WPB77 |
// 围栏数据格式
const result = {
"result": [
[
{
"id": "110101",
"fullname": "东城区",
"location": {
"lat": "39.928353",
"lng": "116.416357"
},
"polygon": [ // 行政区划轮廓点串
[116.809403,39.61482,116.780286,39.593196....],
// 其它项省略(若有)...
]
},
// 其它项省略...
]
]
}
第二步:将获取的区域或网格数据、地图围栏数据进行计算
主要是将获取到的区域或网格数据与地图围栏数据进行计算。
两个主要步骤:1.将围栏数据的经纬度数据做一个转化;2.计算每个区域的颜色;
/**
* @description 对地图围栏数据、区域或网格数据进行转化
* @param {Object} mapLatLngData 地图围栏数据
* @param {Object} mapDataObj 区域或网格数据
*/
function mapDataFormatter({ mapLatLngData, mapDataObj }){
mapLatLngData.forEach(({ id, polygon }) => {
const { value } = mapDataObj[id] || {};
const path = [];
/* 将围栏的经纬度进行转化 */
polygon.forEach(coords => {
path.push([coords.reverse().map(coord => Number(coord))]);
});
/* 区域颜色的计算,获取到颜色,细节请看‘热力计算’模块 */
const defaultColor = value && getGradientColor({ valueScope, value, colors: gradientColors(defaultColorLinear) });
/* 设置颜色样式 */
styles[id] = {
fillColor: defaultColor
};
/* 围栏数据、区域或网格数据数据进行计算后的新集合data */
data.push({ path, styles });
});
return data;
}
2、热力计算
热力主要是将不同的数据以不同的颜色展示在图表上,给用户一个明显的视觉感受,可一眼看出各个区域的状态。本次需求主要有两种热力计算:1.连续型热力计算;2.分段式热力计算。
连续型热力计算
连续型热力:主要是从一个颜色到另一个颜色的渐变的可视化展示,是根据用户设置的两个基础颜色(开始颜色、结束颜色)以及拿到的数据进行计算,得到每个数据对应的颜色信息。
第一步:将用户设置的两个基础颜色进行转化,得到一个颜色数组
第二步:计算每个区域的数据对应的颜色
/**
* @description 计算两个颜色的区间色,返回steps个区间色,包含起止
* @param {String} start 最浅颜色值,16进制
* @param {String} end 最深颜色值,16进制
* @param {Number} steps 将颜色分为steps个色段
*/
const gradientColors = ({ start, end, steps = 8 }) => {
steps = Math.min(Math.max(2, steps), 8);
// parseColor函数 将16进制颜色码转化为rgb颜色值
[start, end] = [start, end].map(x => parseColor(x).map(channel => channel / 255));
const output = [], so = [];
for (let i = 0; i < steps; i++) {
const ms = i / (steps - 1);
const me = 1 - ms;
for (let j = 0; j < 3; j++) {
// pad函数 判断是否在字符串前加0,若字符串长度等于1,则加0
so[j] = pad(Math.round((start[j] * me + end[j] * ms) * 255).toString(16));
}
output.push(`#${so.join('')}`);
}
return output;
};
/**
* @description 计算每个数据对应的颜色
* @param {Array} valueScope 数据范围 [min, max] min:最小值,max:最大值
* @param {Number} value 当前的数据值
* @param {Array} colors 需设置的颜色数组
*/
const getGradientColor = ({ valueScope, value, colors }) => {
if (Number.isNaN(value)) return null;
let [min, max] = valueScope;
const len = colors.length - 2;
if (max < 10) {
min *= 1000;
max *= 1000;
value *= 1000;
}
const unit = Math.floor((max - min) / len);
const colorIndex = unit > 0 ? Math.ceil((value - min + 1) / unit) : 0;
return colors[colorIndex];
};
分段式热力计算
分段式热力:主要是根据用户设置的段数、设置的颜色(展示、hover、click)、获取到的网格,根据这些信息,获取到每个网格对应的颜色(展示、hover、click)。
第一步:根据用户设置的段数,计算数据的分段形式,得到一个分段数组
第二步:根据第一步得到的分段数组及用户设置的段数、颜色信息等,计算数据对应的颜色
第三步:将获取到的样式与基础数据信息进行一个组合。
/**
* @description 根据段数,计算数据对应的分段数组
* @param {Array} value 当前的数据值范围
* @param {Number} steps 设置的段数
*/
const getScopeValue = (value, steps = 5) => {
let [min, max] = value;
let base = 1;
let start = 0;
let end = steps;
const values = [];
const level = (max - min) / steps;
for (let i = start; i <= steps; i++) {
let val = level < 1 ? i : min + i * level;
if (i === steps) {
val += 1;
}
values.push(Math.floor(val) / base);
}
return values;
};
// 网格基础配置信息设置,主要是段数、颜色等
const gridStyle = {
styles: {
polygonStyle0: {
color: 'rgba(255, 200, 2, 0.5)' // 填充色
}
},
steps: 5,
formatStyle: params => 'polygonStyle0'
};
/**
* @description 获取数据对应的颜色及基础信息
* @param {Array} data 当前的数据集合
* @param {Func} formatRender 函数,获取用户设置的基础信息,如段数、颜色等
*/
const getStepValues = ({ data, formatRender }) => {
// 获取数据值的范围scopeValue
const values = data.map(({ value }) => value ?? 0);
const scopeValue = [Math.min(...values), Math.max(...values)];
// 计算新的样式,gridStyle是默认样式
const newStyle = (formatRender && formatRender()) || gridStyle;
const { steps, formatStyle, styles } = { ...gridStyle, ...newStyle };
const defaultValueSteps = getScopeValue(scopeValue, steps);
return {
steps,
formatStyle,
styles,
valueSteps: defaultValueSteps
};
};
3、初始化图表
1、区域图表的初始化操作 —— new TMap.visualization.Area
可查看具体信息:
https://lbs.qq.com/webApi/visualizationApi/visualizationGuide/visualizationArea
https://lbs.qq.com/webApi/visualizationApi/visualizationDoc/visualizationDocArea
2、网格图表的初始化操作 —— new TMap.MultiPolygon
进行网格图表的初始化操作。本次网格图表的实现,主要是通过多边形进行绘制。
可查看具体信息:https://lbs.qq.com/webApi/javascriptGL/glGuide/glEditor
// 初始化区域图表
const paths = [ // 设置区域数据
{
path: [40.044908, 116.28292, 40.045069, 116.283019, 40.045177, 116.283158, 40.04527, 116.283311, 40.045317, 116.283486],
styleId: 'styel1'
}
];
const area = new TMap.visualization.Area({
styles: { // 设置区域图样式
styel1: {
fillColor: 'rgba(56,124,234,0.8)', // 设置区域填充色
strokeColor: '#6799EA' // 设置区域边线颜色
}
}
}).setData(paths).addTo(map)
// 初始化网格图表
const polygonLayout = new TMap.MultiPolygon({
map, // 显示多边形图层的底图
styles: { // 多边形的相关样式
highlight: new TMap.PolygonStyle({
color: 'rgba(255, 255, 0, 0.6)',
})
},
geometries: [ // 多边形数据
{
id: 'polygon1',
paths: [40.044908, 116.28292, 40.045069, 116.283019, 40.045177, 116.283158, 40.04527, 116.283311, 40.045317, 116.283486]
}
]
})
4、事件设置
设置hover事件,有两点需要注意:1.需要处理的是在hover时进行样式的设置;2.hover时的弹框的设置;可查看具体信息:
https://lbs.qq.com/webApi/javascriptGL/glGuide/glInfowindow
设置click事件,执行一个回调函数。
layout.on('click', mapClickCallback); // click事件
layout.on('hover', mapHoverCallback); // hover事件
// hover函数
function mapHoverCallback(res) {
const { lat, lng } = res.latLng;
const { area: info } = res && res.detail;
if (info) {
// 设置当前位置
const currentPosition = new TMap.LatLng(lat, lng);
// 设置弹窗内容
const currentContent = '弹框信息';
if (this.infoWindow) {
this.infoWindow.setPosition(currentPosition);
this.infoWindow.setContent(currentContent);
this.infoWindow.open();
} else {
// 初始化并设置弹窗
this.infoWindow = new TMap.InfoWindow({
map, // (必需)显示信息窗的地图
position: currentPosition,
content: currentContent,
zIndex: 999,
// 信息窗相对于position对应像素坐标的偏移量,x方向向右偏移为正值,y方向向下偏移为正值,默认为{x:0, y:0}
offset: { x: 0, y: 0 }
});
}
} else {
this.infoWindow && this.infoWindow.close();
}
}
// click函数
function mapClickCallback(res) {
this.infoWindow && this.infoWindow.close();
// 执行回调函数
}
以上便是实现网格地图的关键步骤。
总结
网格化热力地图上线后,呈现各类数据在不同区域下的分析和对比,帮助业务方快速圈定重点招商区域,助力业务提升精细化招商率,大幅提升线下门店有效订单数和成交金额。JMT数据可视化组件让技术和设计实现极致融合,提供全方位的数据分析技术支持,提供精细化的数据分析服务,推动数据数智化发展。