一、可视化的根源很多年前看过一篇很震撼的文章,叫《Lisp之根》(英文版:TherootsofLisp),大意是Lisp只用了一个数据结构(List)和有限的函数,构建了极其简洁和可扩展的编程语言。当时,我被这种设计理念深深震撼:一方面,它足够简单,每个单独的功能都足够简单;另一方面是非常复杂的,比如宏,高阶函数,递归等等。复杂的程序,复杂的机制都是由简单的组件组成的。数据可视化也是如此。只有一些基本元素构成了清晰、富有表现力、美观的视觉信息图。这些元素的不同组合可以产生迷人的力量。很容易列出“视觉元素的根源”:位置、长度、角度、形状、纹理、面积(体积)、色相、饱和度等几个有限的元素。邱南森在他的A中提供了一个列表包含大多数常用元素的视觉元素图。令人兴奋的是,这些元素可以自由组合,旺旺的组合会产生1+1>2的效果。2.心理学与认知系统数据可视化其实是建立在人类视觉认知系统的基础上的,所以对人类视觉系统的工作原理有一定的了解可以帮助我们设计出更高效(更快的将我们想要表达的信息传递给读者)的可视化作品。1.心理物理学在日常生活中,我们会遇到这样的场景:如果一件原价10元的商品降价到5元,消费者很容易买到;而原价100元的产品降价到95元,则很难刺激消费者产生购买冲动。这两个优惠的绝对数字都是5元,只是效果不同。Weber-Fechner定理描述的正是这种_非理性_场景。这个定理比较矫情的描述是:【感觉量与物理量的对数成正比,也就是说感觉量的增加滞后于物理量的增加,物理量呈几何增长级数,而心理量以算术级数增长,这个经验公式被称为费希纳定律或韦伯-费希纳定律。——摘自百度百科】这种现象是人脑结构所固有的,因此在设计可视化作品时应充分考虑,例如:避免使用面积图作为比较在制作比较图形时,需要考虑使用它们当差异不明显时,将多种颜色作为非线性视觉元素的视觉代码时,差异要足够大。例如:如上图所示,当面积增大时,肉眼越来越难以从形状的大小上解码出实际的数据差异。上面三个Group矩形(每行两个为一组),后面对应的数据如下,可以看到每组两个矩形的绝对差为5:vardata=[{width:5,height:5},{宽度:10,高度:10},{宽度:50,高度:50},{宽度:55,高度:55},{宽度:100,高度:100},{宽度:105,高度:105}];2.格式塔学派格式塔学派是心理学的一个重要学派,她强调整体的理解,而不是结构主义的构成。格式塔认为,当人类看到一张图片时,会先将其简化为一个整体,然后再细化为各个部分;而不是首先识别部分,然后将它们拼接成一个整体。比如著名的斑点狗:我们的眼睛-大脑很容易在阴影中看到斑点狗,而不是首先识别出狗的四条腿或尾巴(事实上,在这张图片中,人眼无法识别各个部分。部分)。格式塔理论有几个非常重要的原则:邻近原则相似原则闭合原则连续原则主题/背景原则当然,格式塔学派还有一些后续发展,总结出更多的原则。在工程中,这些原则仍然广泛用于指导设计人员设计各种用户界面。鉴于网上关于格式塔理论及其应用的文章已经很多,这里不再赘述。有兴趣的同学可以参考这几篇文章:UTS上的一篇Gestalt文章UTS上的GestaltandWebDesign文章腾讯CDC的Gestalt介绍3.视觉设计的基本原则在书中,作者给出了几个基本的原则以通俗易懂的方式进行设计,这些原则可以直接用于数据可视化的设计:亲密度(物理上将相关信息放在一起,相关性不大的用空格等方式隔开)对齐(水平和垂直对齐元素,便于视觉识别)重复(重复某种模式,如标题1的字体颜色,标题2的字体颜色等,保持重复和一致)对比(通过强烈的对比来区分不同的信息)如果稍加注意,就会发现这些原则在现实世界中被广泛应用。1、2、3三个标题的形式是重复的体现;每个标题的内容都是独立的,因为它的元素(数字,两行文本)之间的距离相对较近。根据亲近原则,人眼会自动将其归为一类;过大的数字与较小的文本形成对比;大标题颜色与其他内容形成对比等。这些原则实际上与上面提到的格式塔学派和韦伯-费希纳定理有关。在理解了人类视觉识别的机制之后,使用这些原理是非常自然和得心应手的。一些例子淡化图表的网格(与数据图对比)用深色强调标尺(与其余线条对比)突出异常值(通过不同颜色对比)使用颜色(通过不同颜色,使用原则亲切方便读者分组数据)元素颜色和图例(使用重复原则)同一页有多个图表,使用相同的图例,颜色选择(强调重复原则)4.例子上篇文章提到过我通过手机APP收集了女儿成长的一些记录,包括母乳喂养信息、换尿布记录、睡眠信息等。在这个例子中,我将逐步介绍如何将这些信息可视化,并解释其中使用的可视化原理。可视化的第一步是明确你想从数据中获取什么信息。我要获取的信息是孩子的睡眠总量和睡眠时间的分布。1、条形图进阶版在确定了可视化的目的之后,第二步就是选择合适的可视化代码。如上所述,人眼最准确的视觉编码是长度。我们可以将休眠时间换算成长度来显示。最简单的方法是按天聚合,然后将其变成直方图。例如:2016/11/21,7682016/11/22,7602016/11/23,700但是这种图是看不到时间分布的。我们可以考虑条形图的变体来满足上述两个核心需求。首先在纸上画一个简单的草图。纵轴是24小时,横轴是日期。与普通柱状图不同的是,每条柱状图的总长度是固定的,柱状图代表的不是简单的非数据类型,而是24小时。在草稿中,每个斜线方块表示孩子睡着了,而虚线表示她醒着。2.原始数据名称、日期、长度、备注Xinxin,2016/11/2119:23,119,Xinxin,2016/11/2122:04,211,Xinxin,2016/11/2202:03,19,Xinxin,2016/11/2202:23,118,欣欣,2016/11/2205:58,242,欣欣,2016/11/2210:57,128,欣欣,2016/11/2214:35,127,欣欣,2016/11/2217:15,127,欣欣,2016/11/2220:02,177,欣欣,2016/11/2301:27,197,这里有个问题,我们的纵轴是24小时,如果她晚上23点开始睡觉,睡了3个小时,那么这个bar会超过24格轴。我写了一个函数来做数据转换:require'csv'require'active_support/all'require'json'csv=CSV.read('./visualization/data/sleeping_data_refined.csv',:headers=>:first_row)data=[]csv.eachdo|row|date=DateTime.parse(row['date'],"%Y/%m/%d%H:%M")mins_until_end_of_day=date.seconds_until_end_of_day/60diff=mins_until_end_of_day-行['length'].to_iif(diff>=0)thendata<<{:name=>row['name'],:date=>row['date'],:length=>row['length'],:注意=>row['note']}elsedata<<{:name=>row['name'],:date=>date.strftime("%Y/%m/%d%H:%M"),:length=>mins_until_end_of_day,:note=>row['note']}data<<{:name=>row['name'],:date=>(date.beginning_of_day+1.day).strftime("%Y/%m/%d%H:%M"),:length=>diff.abs,:note=>row['note']}endend有了干净的数据之后,我们就可以写一些前端代码了吧绘制图表。画图有几点需要注意:一天中时间段对应的矩形需要有相同的X坐标。不同的睡眠时长一定要有颜色(睡眠时间越长,颜色越深):headers=>:first_row)data=[]csv.eachdo|row|date=DateTime.parse(row['date'],"%Y/%m/%d%H:%M")mins_until_end_of_day=日期。seconds_until_end_of_day/60diff=mins_until_end_of_day-row['length'].to_iif(diff>=0)thendata<<{:name=>row['name'],:date=>row['date'],:length=>row['length'],:note=>row['note']}elsedata<<{:name=>row['name'],:date=>date.strftime("%Y/%m/%d%H:%M"),:length=>mins_until_end_of_day,:note=>row['note']}data<<{:name=>row['name'],:date=>(date.beginning_of_day+1.day).strftime("%Y/%m/%d%H:%M"),:length=>diff.abs,:note=>row['note']}end函数getFirstInDomain可以返回一个X坐标根据一个日期值,所以2016/11/2119:23和2016/11/2122:04都会返回一个整数值(借助d3提供的scale函数)。另外,我们根据每次睡眠的分钟数将睡眠质量分为5个等级:varlevel=d3.scale.threshold().domain([60,120,180,240,300]).range(["low","fine","中等”,“好”,“很好”,“完美”]);然后在绘制过程中,根据实际数据值确定不同的CSSClass:svg.selectAll(".bar").data(data).enter().append("rect").attr("class",function(d){returnlevel(d.length)+"bar";})//...执行后,看起来像这样其实这个图标可以清楚的看到,大部分的睡眠都集中在0-6点,而中午10-13点和晚上18-20点基本上只有一些零星的睡眠.3.天图上面的图表有个缺点。当日期比较多时(上图有差不多100天的数据),X轴会比较难画。如果减少到每周或每月,则会增加很多额外的复杂性。另一种尝试是变形:由于这个统计与时间有关,所以圆形时钟的形象是一个很好的比喻,一天24小时自然可以映射到一个圆上。休眠时间可以用弧长来表示,休眠时间越长,弧长越大:4.角度对弧我们先把整个圆(360度)分成分钟,每分钟对应的角度为:360/(24*60),然后将角度转换成弧度:度数*π/180:varperAngle=(360/(24*60))*(Math.PI/180);然后对于指定的时间,比如10点20分,先计算分钟数:10*60+20,然后乘以preAngle,就可以得到起始弧;开始时间的分钟数加上睡眠时长,再乘以preAngle,就是结束弧度。functionstartAngle(date){varstart=(date.getHours()*60+date.getMinutes())*perAngle;returnMath.floor(start*1000)/1000;}functionendAngle(date,length){varend=(date.getHours()*60+date.getMinutes()+length)*perAngle;returnMath.floor(end*1000)/1000;}实现的结果是这样的:乍一看是星图,但是图片不一样颜色含义不是很直观,需要加个图例到图表。它是通过使用d3的线性比例和定义svg的梯度来实现的。定义好渐变和渐变的颜色值范围后,就可以绘制图例了。图形上的每条弧线都会有鼠标向上移动的工具提示,可以很好地类比读者脑中的时钟隐喻,使图形更容易理解。既然我把整个圆圈分成了24份,其实和普通的钟表不一样,如果加上钟表的刻度会不会更好呢?从结果来看,这样的标记有点多余,所以我在最终版本中去掉了时钟标记。可以看出,我们用一个圆形时钟的比喻来反映每一天的睡眠分布,然后用颜色的深浅来表示每次睡眠的时长。由于时钟的形象已经深入人心,读者很容易发现0点钟就在圆环组的正上方。中间的黄色实心圆帮助读者先关注最里面的圆,然后逐渐向外,这与日期的分布方向完全一致。最后的结果在这里:欣欣的睡眠记录,完整代码在这里。5.更进一步对于一个完整的可视化作品来说,不仅要通过各种可视化代码将数据转化为可视化元素,背景信息也同样重要。由于这张星图是关于睡眠的主题,她睡觉的一系列图片将加强这一视觉线索,帮助读者快速理解。6.制作背景图我从相册里选了很多女儿睡觉时拍的照片,现在需要一个工具把这些照片缩小到合适的尺寸,然后拼接成一张大图。这里面有很多有意思的地方,比如图片可以分为横屏和竖屏,还有一些是正方形的。我需要将缩放后的结果做成正方形,这样更容易拼接。还好有imagemagick这样的神器,只需要一条命令就可以搞定:$montage*.jpg-geometry+0+0-resize128x128^\-gravitycenter-crop128x128+0+0xinxin-sleeping.jpg这条命令会改变当前目录下面所有的jpg文件都缩放到128x128像素,从中间-重心开始裁剪,+0+0表示图片之间的差距,最后将结果写入xinxin-sleeping.jpg。图片拼接好之后,可以通过CSS或者图片编辑器给图片添加虚化效果,设置深灰色的半透明遮罩。body{background-image:url('/xinxin-sleeping.png');background-size:cover;background-position:center;}当然,背景信息只是辅助作用,要避免铺天盖地客人。因此,将画面虚化,加上深灰色的半透明Mask(这里应用了格式塔理论中的主体/背景原理)。五、总结本文讨论了视觉作品背后的视觉元素的一些理论,以及人类视觉识别的机制。在这些机制的基础上,介绍了如何将通用的设计原则应用到视觉编码中。最后,一个实际的例子说明了如何使用这些元素——更重要的是,这些元素的组合——来制作一个美丽、有意义的可视化。【本文为专栏作者“ThoughtWorks”原创稿件,微信公众号:Thinkworker,转载请联系原作者】点此查看该作者更多好文
