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