使用bokeh-scala进行数据可视化
目录
一、前言
最近在使用spark集群以及geotrellis框架(相关文章见http://www.cnblogs.com/shoufengwei/p/5619419.html)进行分布式空间地理系统设计(暂且夸大称之为地理信息系统),虽说是空间地理信息系统但是也少不了数据可视化方面的操作,所以就想寻找一款支持大数据的可视化框架,网上查阅半天发现bokeh不错(其实是老板直接指明方向说用这款),恰好bokeh也有scala语言的封装,Github地址,于是拿来练练手,算是做个技术储备。
二、bokeh简介及胡扯
bokeh是一个python下的大数据可视化框架Github地址。其官网对其介绍如下:
Bokeh is a Python interactive visualization library that targets modern web browsers for presentation. Bokeh provides elegant, concise construction of novel graphics with high-performance interactivity over very large or streaming datasets in a quick and easy way.
根据我拙劣的英语水平翻译如下:
Bokeh是一个基于Python语言的显示于新式浏览器中的交互式的可视化类库。Bokeh提供了一种快速且简单的基于大数据以及流式数据的高性能的可交互式的优雅的简洁的图表制作。
比较拗口,总体意思就是Bokeh能够很好的支持大数据下的可交互式的数据可视化,新式浏览器应当是支持HTML5的浏览器,不过还未考证。
看了一下其Python代码示例,确实简单且优美,但是在看了其scala示例后,感觉写的比较死板,写起来很僵硬,没有python语言那么灵活,可能因为是在python的基础上封装的缘故,就像java的类库重写或封装成C#语言,也明显感觉不是那么舒服,更何况python是个弱类型语言。但是我觉得scala的代码其实也可以写的很优美,最近在码代码的过程中有个感觉就是实现功能很容易,但是要想代码写的漂亮看上去舒服甚至有艺术感就完全不是一件简单的事情。言归正传,我在用一个小时完成简单功能之后,又花了五六个小时进行了简单的重构、二次封装、完善,希望我的封装能用起来舒服一点,但是由于水平有限,也可能我只是画蛇添足,用起来可能还不如原来的,各位看官自行取舍。先发上几幅我做出来的效果图,各位看官可以提前有个感觉。
三、bokeh-scala基本代码
先来介绍如何使用bokeh-scala生成一个简单的图表。首先要引用其jar包,一般scala项目均采用sbt进行包管理,只需要在build.sbt文件中添加以下代码:
libraryDependencies += "io.continuum.bokeh" %% "bokeh" % "0.6"
引入之后就可以开始编写代码,首先需要定义一个数据源类,代码如下;
object source extends ColumnDataSource {
val x = column(-2 * pi to 2 * pi by 0.1)
val y = column(x.value.map(sin))
}
该类继承自ColumnDataSource类,很明显x、y分别代表x轴数据值范围以及x轴坐标点对应的y轴坐标数据,当然此类也可以包含多个属性,只需要在后续生成图表的时候选择对应的属性即可。本例中x为-2π到2π之间的范围,y为对应的sin值,一个简单的sin函数。
然后就是生成一个Plot对象:
val xdr = new DataRange1d
val ydr = new DataRange1d
val tools = Pan | WheelZoom
val plot = new Plot().x_range(xdr).y_range(ydr).tools(tools).width(width).height(height)
其中xdr、ydr赋值new DataRange1d表示图表的x、y方向为1维连续变化的数据。tools表示在图表上显示的工具:有平移、缩放等,此处bokeh建立了相当于其他语言中枚举的概念。然后使用new Plot()即可创建一个Plot对象,width和height表示宽和高。
有了Plot对象之后就可以生成其坐标轴,有线性、对数、时间等选择,我们以线性为例,生成坐标轴代码如下:
val axis = new LinearAxis.plot(plot).location(Location.Left)
plot.left <<= (axis :: _)
上述语句会生成一个线性的y轴。这里的第二句就是我觉得bokeh-scala代码看起来不舒服的地方,明明第一句已经为plot对象指明了位置Location.Left,却还要在第二句里再次为plot.left赋刚刚生成的值,后面还有好几处这样的例子,可能是我理解不到位。用同样的方法可以再生成x轴,只需要location赋值为Location.Below。
接下来可以使用val grid = new Grid().plot(plot).dimension(0).axis(axis)
生成网格,其中axis是上一步生成的坐标轴,dimension控制方向。这里又是一处繁琐的地方,明明刚刚的axis已经是有方位的能区分x、y方向的,此处却还要显式的指明dimension,实在有点不太懂,也许是没能理解开发者的意图。
接下来才进入绘制的主题,根据上面的x、y数据范围绘制图形,这里选择很多,可以绘制圆点、线、文字等多种类型,在这里以原点为例,后面封装的代码中会再给出几种。绘制圆点的代码如下;
val circle = new Circle().x(x).y(y)
val circleGlyph = new GlyphRenderer().data_source(source).glyph(circle)
第一行的x、y就是source中对应的属性,如果没有事先import,需要使用全名称source.x,source就是上面定义的类,此处source是object类型的,所以此处直接传入,相当于单例。circleGlyph就是最终生成的图表中的一系列圆点。
接下来就是最关键的一步,将生成的坐标轴、网格、圆点等对象传递给plot。此处又是繁琐的地方,明明很多对象都是由plot生成的,为什么不能直接绑定给plot呢?不得其解。代码如下:
val renderers: (List[Renderer] => List[Renderer]) = (xaxis :: yaxis :: xgrid :: ygrid :: circleGlyph :: _)
plot.renderers <<= renderers
通过上述步骤就生成了一个完整的包含各种元素的plot,但是并没有显示出来,bokeh的显示在最开始翻译的描述中说的很清楚————要通过浏览器。最简单的方式就是直接渲染一个html文件,然后在浏览器中打开,代码如下:
val document = new Document(plot)
val html = document.save(path)
html.view()
其中path是生成的html文件存放的路径,这样就能直接将plot对象以图表的形式显示到浏览器当中。
四、我的封装
下面我将今天封装的代码贴在下面,供学习交流,又稍作修改,修改后如下:
1、BokehHelper.scala
package geotrellis.bokeh
import io.continuum.bokeh.{Line => BokehLine, _}
import scala.collection.immutable.{IndexedSeq, NumericRange}
/**
* Created by shoufengwei on 2016/7/30.
*/
object BokehHelper {
/**
*
* @param xdr
* @param ydr
* @param tools all Tools
* val panTool = new PanTool().plot(plot)
* val wheelZoomTool = new WheelZoomTool().plot(plot)
* val previewSaveTool = new PreviewSaveTool().plot(plot)
* val resetTool = new ResetTool().plot(plot)
* val resizeTool = new ResizeTool().plot(plot)
* val crosshairTool = new CrosshairTool().plot(plot)
* plot.tools := List(panTool, wheelZoomTool, previewSaveTool, resetTool, resizeTool, crosshairTool)
* @param width
* @param height
*/
def getPlot(xdr: DataRange, ydr: DataRange, tools: List[Tool], width: Int = 800, height: Int = 400) = {
new Plot().x_range(xdr).y_range(ydr).tools(tools).width(width).height(height)
}
def getLinearAxis(plot: Plot, position: Location): ContinuousAxis = {
getAxis(plot, new LinearAxis, position)
}
/**
* get datetime axis
*
* @param plot
* @param position
* @param formatter eg. new DatetimeTickFormatter().formats(Map(DatetimeUnits.Months -> List("%b %Y")))
* @return
*/
def getDatetimeAxis(plot: Plot, position: Location, formatter: DatetimeTickFormatter = new DatetimeTickFormatter().formats(Map(DatetimeUnits.Months -> List("%b %Y")))): ContinuousAxis = {
getAxis(plot, new DatetimeAxis().formatter(formatter), position)
}
def getAxis(plot: Plot, axisType: ContinuousAxis, position: Location): ContinuousAxis = {
val axis = axisType.plot(plot).location(position)
setPlotAxis(plot, axis, position)
setRenderer(plot, axis)
axis
}
def setAxisLabel(axis: ContinuousAxis, axisLabel: String) = {
axis.axis_label(axisLabel)
}
def setPlotAxis(plot: Plot, axis: ContinuousAxis, position: Location) {
position match {
case Location.Left => plot.left <<= (axis :: _)
case Location.Above => plot.above <<= (axis :: _)
case Location.Below => plot.below <<= (axis :: _)
case Location.Right => plot.right <<= (axis :: _)
case _ =>
}
}