最近在使用spark集群以及geotrellis框架(相关文章见http://www.cnblogs.com/shoufengwei/p/5619419.html)进行分布式空间地理系统设计(暂且夸大称之为地理信息系统),虽然说是空间地理信息系统可是也少不了数据可视化方面的操做,因此就想寻找一款支持大数据的可视化框架,网上查阅半天发现bokeh不错(实际上是老板直接指明方向说用这款),刚好bokeh也有scala语言的封装,Github地址,因而拿来练练手,算是作个技术储备。html
bokeh是一个python下的大数据可视化框架Github地址。其官网对其介绍以下:java
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.python
根据我拙劣的英语水平翻译以下:git
Bokeh是一个基于Python语言的显示于新式浏览器中的交互式的可视化类库。Bokeh提供了一种快速且简单的基于大数据以及流式数据的高性能的可交互式的优雅的简洁的图表制做。github
比较拗口,整体意思就是Bokeh可以很好的支持大数据下的可交互式的数据可视化,新式浏览器应当是支持HTML5的浏览器,不过还未考证。web
看了一下其Python代码示例,确实简单且优美,可是在看了其scala示例后,感受写的比较死板,写起来很僵硬,没有python语言那么灵活,可能由于是在python的基础上封装的缘故,就像java的类库重写或封装成C#语言,也明显感受不是那么舒服,更况且python是个弱类型语言。可是我以为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函数。ssh
而后就是生成一个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对象以图表的形式显示到浏览器当中。
下面我将今天封装的代码贴在下面,供学习交流,又稍做修改,修改后以下:
一、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 _ => } } def getCircleGlyph(column_x: ColumnDataSource#Column[IndexedSeq, Double], column_y: ColumnDataSource#Column[IndexedSeq, Double], value: DataSource, size: Int = 5, fill_Color: Color = Color.Red, line_Color: Color = Color.Black) = { val circle = new Circle().x(column_x).y(column_y).size(size).fill_color(fill_Color).line_color(line_Color) getGlyphRenderer(value, circle) } def setCircleGlyph(plot: Plot, column_x: ColumnDataSource#Column[IndexedSeq, Double], column_y: ColumnDataSource#Column[IndexedSeq, Double], value: DataSource, size: Int = 5, fill_Color: Color = Color.Red, line_Color: Color = Color.Black) = { val circleGlyph = getCircleGlyph(column_x, column_y, value, size, fill_Color, line_Color) setRenderer(plot, circleGlyph).asInstanceOf[GlyphRenderer] } def getLineGlyph(column_x: ColumnDataSource#Column[IndexedSeq, Double], column_y: ColumnDataSource#Column[IndexedSeq, Double], value: DataSource, width: Int = 3, line_Color: Color = Color.Black) = { val line = new BokehLine().x(column_x).y(column_y).line_width(width).line_color(line_Color) getGlyphRenderer(value, line) } def setLineGlyph(plot: Plot, column_x: ColumnDataSource#Column[IndexedSeq, Double], column_y: ColumnDataSource#Column[IndexedSeq, Double], value: DataSource, width: Int = 3, line_Color: Color = Color.Black) = { val lineGlyph = getLineGlyph(column_x, column_y, value, width, line_Color) setRenderer(plot, lineGlyph).asInstanceOf[GlyphRenderer] } def getPatchGlyph(column_x: ColumnDataSource#Column[IndexedSeq, Double], column_y: ColumnDataSource#Column[IndexedSeq, Double], value: DataSource, width: Int = 3, fill_Color: Color = Color.Red, line_Color: Color = Color.Black) = { val patch = new Patch().x(column_x).y(column_y).line_width(width).line_color(line_Color).fill_color(fill_Color) getGlyphRenderer(value, patch) } def setPatchGlyph(plot: Plot, column_x: ColumnDataSource#Column[IndexedSeq, Double], column_y: ColumnDataSource#Column[IndexedSeq, Double], value: DataSource, width: Int = 3, fill_Color: Color = Color.Red, line_Color: Color = Color.Black) = { val patchGlyph = getPatchGlyph(column_x, column_y, value, width, fill_Color, line_Color) setRenderer(plot, patchGlyph).asInstanceOf[GlyphRenderer] } def getCircleCrossGlyph(column_x: ColumnDataSource#Column[IndexedSeq, Double], column_y: ColumnDataSource#Column[IndexedSeq, Double], value: DataSource, size: Int = 5, fill_Color: Color = Color.Red, line_Color: Color = Color.Black) = { val circleCross = new CircleCross().x(column_x).y(column_y).size(size).fill_color(fill_Color).line_color(line_Color) getGlyphRenderer(value, circleCross) } def setCircleCrossGlyph(plot: Plot, column_x: ColumnDataSource#Column[IndexedSeq, Double], column_y: ColumnDataSource#Column[IndexedSeq, Double], value: DataSource, size: Int = 5, fill_Color: Color = Color.Red, line_Color: Color = Color.Black) = { val circleCrossGlyph = getCircleCrossGlyph(column_x, column_y, value, size, fill_Color, line_Color) setRenderer(plot, circleCrossGlyph).asInstanceOf[GlyphRenderer] } def getTextGlyph(column_x: ColumnDataSource#Column[IndexedSeq, Double], column_y: ColumnDataSource#Column[IndexedSeq, Double], value: DataSource, size: Int = 5, fill_Color: Color = Color.Red, line_Color: Color = Color.Black) = { val text = new Text().x(column_x).y(column_y).text("1") getGlyphRenderer(value, text) } def setTextGlyph(plot: Plot, column_x: ColumnDataSource#Column[IndexedSeq, Double], column_y: ColumnDataSource#Column[IndexedSeq, Double], value: DataSource, size: Int = 5, fill_Color: Color = Color.Red, line_Color: Color = Color.Black) = { val textGlyph = getTextGlyph(column_x, column_y, value, size, fill_Color, line_Color) setRenderer(plot, textGlyph).asInstanceOf[GlyphRenderer] } def getGlyphRenderer(value: DataSource, glyph: Glyph) = { new GlyphRenderer().data_source(value).glyph(glyph) } /** * * @param legends eg. val legends = List("y = sin(x)" -> List(lineGlyph, circleGlyph)) */ def getLegends(plot: Plot, legends: List[(String, List[GlyphRenderer])]): Legend = { val legend = new Legend().plot(plot).legends(legends) setRenderer(plot, legend) legend } def getLegends(plot: Plot, name: String, glyphList: List[GlyphRenderer]): Legend = { getLegends(plot, List(name -> glyphList)) } /** * * @param plot * @param axis * @param dimension 0 means x and 1 means y * @return */ def getGrid(plot: Plot, axis: ContinuousAxis, dimension: Int) = { val grid = new Grid().plot(plot).dimension(dimension).axis(axis) setRenderer(plot, grid) grid } def setRenderers(plot: Plot, renderers: List[Renderer] => List[Renderer]) = { plot.renderers <<= renderers } def setRenderer(plot: Plot, renderer: Renderer) = { val renderers: (List[Renderer] => List[Renderer]) = (renderer :: _) setRenderers(plot, renderers) renderer } /** * use this method just can plot one renderer * * @param plot * @param renderers */ def setRenderers(plot: Plot, renderers: List[Renderer]) = { plot.renderers := renderers } /** * use gridplot Multiple plots in the document * * @param children every child List is one row eg. val children = List(List(microsoftPlot, bofaPlot), List(caterPillarPlot, mmmPlot)) * @return */ def multiplePlots(children: List[List[Plot]], title: String = ""): Plot = { new GridPlot().children(children).title(title) } def save2Document(plot: Plot, path: String = "sample.html"): Unit = { val document = new Document(plot) val html = document.save(path) println(s"Wrote ${html.file}. Open ${html.url} in a web browser.") html.view() } }
二、test.scala
package geotrellis.bokeh import io.continuum.bokeh._ import io.continuum.bokeh.Tools._ import scala.collection.immutable.{IndexedSeq, NumericRange} import math.{Pi => pi, sin} /** * Created by shoufengwei on 2016/7/29. * http://bokeh.pydata.org/en/latest/docs/user_guide/quickstart.html */ object BokehTest extends App { val xdr = new DataRange1d() val ydr = new DataRange1d() object source extends ColumnDataSource { val x: ColumnDataSource#Column[IndexedSeq, Double] = column(-2 * pi to 2 * pi by 0.1) val y = column(x.value.map(sin)) val z = column(x.value.map(Math.pow(2, _))) val p = column(x.value.map(Math.pow(3, _))) // val x = column(-10.0 to 10 by 0.1) // val y = column(-10.0 to 5 by 0.1) } import source.{x, y, z, p} // val plot = plotOne("全图") // BokehHelper.save2Document(plot = plot) val plot = plotMulitple() BokehHelper.save2Document(plot) def plotMulitple() = { val plot1 = plotOne("1") val plot2 = plotOne("2") val plot3 = plotOne("3") val plot4 = plotOne("4") BokehHelper.multiplePlots(List(List(plot1, plot2), List(plot3, plot4)), "all chart") } def plotOne(title: String = ""): Plot = { val plot = BokehHelper.getPlot(xdr, ydr, Pan | WheelZoom | Crosshair) plotBasic(plot) val legend = plotContent(plot) plotLegend(plot, legend) plot.title(title) } def plotBasic(plot: Plot) = { val xaxis = BokehHelper.getLinearAxis(plot, Location.Below) BokehHelper.setAxisLabel(xaxis, "x") val yaxis = BokehHelper.getLinearAxis(plot, Location.Right) BokehHelper.setAxisLabel(yaxis, "y") val xgrid = BokehHelper.getGrid(plot, xaxis, 0) val ygrid = BokehHelper.getGrid(plot, yaxis, 1) } def plotContent(plot: Plot) = { val circleGlyph = BokehHelper.setCircleGlyph(plot, x, y, source) val lineGlyph = BokehHelper.setLineGlyph(plot, x, z, source) val lineGlyph2 = BokehHelper.setLineGlyph(plot, x, y, source) val patchGlyph = BokehHelper.setPatchGlyph(plot, x, p, source) val circleCrossGlyph = BokehHelper.setCircleCrossGlyph(plot, x, p, source) val textGlyph = BokehHelper.setTextGlyph(plot, x, z, source) List("y = sin(x)" -> List(circleGlyph, lineGlyph2), "y = x^2" -> List(lineGlyph), "y = x^3" -> List(circleCrossGlyph, patchGlyph)) } def plotLegend(plot: Plot, legends: List[(String, List[GlyphRenderer])]) = { BokehHelper.getLegends(plot, legends) } }
此处我仍是沿用了C#的习惯,各类Helper,也不知道scala中是否有更好的替代方案,或者设计模式之类。最近迷上了代码整洁之道,信奉的宗旨也是最好不写注释,固然个人水平还远远不够,因此若是上述代码有什么不明白的欢迎追问,固然若是有什么更好的代码整洁、重构、设计模式等方面的建议也请不吝赐教!以上代码test中的内容看官能够根据本身的须要自行修改!
以上就是我总结的有关于bokeh-scala数据可视化的基础,本次并无彻底封装bokeh-scala的所有功能,后续会慢慢完善,更新该篇博客或者另设新篇。欢迎探讨、交流。