bokeh-scala

使用bokeh-scala进行数据可视化

目录

  1. 前言
  2. bokeh简介及胡扯
  3. bokeh-scala基本代码
  4. 我的封装
  5. 总结

一、前言

最近在使用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 _ =>
    }
  }

  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]
  }

  defgetTextGlyph(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)
  }

  defsetTextGlyph(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]
  }

  defgetGlyphRenderer(value: DataSource, glyph: Glyph) = {
    new GlyphRenderer().data_source(value).glyph(glyph)
  }

  /**    *    * @param legends eg.  val legends = List("y = sin(x)" -> List(lineGlyph, circleGlyph))    */
  defgetLegends(plot: Plot, legends: List[(String, List[GlyphRenderer])]): Legend = {
    val legend = new Legend().plot(plot).legends(legends)
    setRenderer(plot, legend)
    legend
  }

  defgetLegends(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    */
  defgetGrid(plot: Plot, axis: ContinuousAxis, dimension: Int) = {
    val grid = new Grid().plot(plot).dimension(dimension).axis(axis)
    setRenderer(plot, grid)
    grid
  }

  defsetRenderers(plot: Plot, renderers: List[Renderer] => List[Renderer]) = {
    plot.renderers <<= renderers
  }

  defsetRenderer(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    */
  defsetRenderers(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    */
  defmultiplePlots(children: List[List[Plot]], title: String = ""): Plot = {
    new GridPlot().children(children).title(title)
  }

  defsave2Document(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()
  }
}

2、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的全部功能,后续会慢慢完善,更新该篇博客或者另设新篇。欢迎探讨、交流。

时间: 2024-10-10 06:55:36

bokeh-scala的相关文章

Scala Study --- override

以前没使用过Scala, 其实我Java也是半截水平\无奈, 学Java的时候刚从C++中挣脱出来,发现Java无比优雅,但很快又对Java种种不信任程序员的设计感到受限. 直到, , 今天遇到了Scala\撒花 Scala的collection设计不能更赞!一段时间后打算专门写篇文章总结Scala,名字就叫"我为什么喜欢Scala!". 废话就不多说了,今天研究了一下Scala的override用法与特点. override --- one of the key words of S

Scala 中apply方法的用法

Scala 是构建在 JVM 上的静态类型的脚本语言,而脚本语言总是会有些约定来增强灵活性.关于协议在Python中是挺多的,看看Python的对象协议,有很多很多,如果对Python的对象协议了解(不了解的可以点击此处)的比较深刻的话,其实scala的apply方法也是很好理解的,比如说 Scala 为配合 DSL 在方法调用时有这么一条约定: 在明确了方法调用的接收者的情况下,若方法只有一个参数时,调用的时候就可以省略点及括号.如 "0 to 2",实际完整调用是 "0.

【Scala】Scala之Numbers

一.前言 前面已经学习了Scala中的String,接着学习Scala的Numbers. 二.Numbers 在Scala中,所有的数字类型,如Byte,Char,Double,Float,Int,Long,Short都是对象,这七种数字类型继承AnyVal特质,这七种数字类型与其在Java中有相同的范围,而Unit和Boolean则被认为是非数字值类型,Boolean有false和true两个值,你可以获取到各个数字类型的最值. 复杂的数字和日期 如果需要更强大的数类,可以使用spire,sc

scala控制结构

#判断 scala> def min(x:Int,y:Int):Int={ var a=x if(x>y) a=y return a } scala> min(1,2)res1: Int = 1 #循环    ##引申:函数式编程里面尽量使用常量,所以尽量避免 while do? 变量? while (A) B do B while A scala> var m=3scala> while (m!=0){ println(m) m-=1 } 321 #枚举 for (i<

scala学习手记19 - Option类型

看到Option类型就知道这本教材应该要说那个了. 使用过guava后,应该知道guava中的Optional类的作用是什么.算了找下原始文档好了: Optional<T> is a way of replacing a nullable T reference with a non-null value. An Optional may either contain a non-null T reference (in which case we say the reference is &

scala学习手记13 - 类继承

在scala里,类继承有两点限制: 重写方法需要使用override关键字: 只有主构造函数才能往父类构造函数中传参数. 在java1.5中引入了override注解,但不强制使用.不过在scala中要想重写方法必须使用override关键字.如果确实重写了父类的方法又不使用override关键字的话,则会在编译时报错,提示没有使用override修饰符. scala的副构造函数必须调用主构造函数或是另一个副构造函数.只有在主构造函数中才能向父类的构造函数中传递数据.可以看出来主构造函数如同父类

scala学习手记10 - 访问修饰符

scala的访问修饰符有如下几个特性: 如果不指定访问修饰符,scala默认为public: 较之Java,scala对protected的定义更加严格: scala可以对可见性进行细粒度的控制. scala的默认访问修饰符 如果没有修饰符,scala会默认把类.字段.方法的访问修饰符当做public.如果要将之调整为private或protected,只需在前面添加对应的修饰符关键字即可.就如下面的程序: class Microwave{ def start() = println("star

Scala应用函数

我们使用“_” 来代替单个的参数,实际上你也可以使用“_”来代替整个参数列表,比如说,你可以使用 print _ 来代替 println (_). someNumbers.foreach(println _) Scala编译器自动将上面代码解释成: someNumbers.foreach( x => println (x)) 因此这里的“_” 代表了Println的整个参数列表,而不仅仅替代单个参数. 当你采用这种方法使用“_”,你就创建了一个部分应用的函数(partially applied

Scala函数字面量

Scala中函数为头等公民,你不仅可以定义一个函数然后调用它,而且你可以写一个未命名的函数字面量,然后可以把它当成一个值传递到其它函数或是赋值给其它变量.下面的例子为一个简单的函数字面量(参考整数字面量,3 为一整数字面量). (x :Int ) => x +1 这是个函数字面量,它的功能为+1. 符好 => 表示这个函数将符号左边的东西(本例为一个整数),转换成符号右边的东西(加1). 函数字面量为一个对象(就像3是个对象),因此如果你愿意的话,可以把这个函数字面量保持在一个变量中.这个变量

Scala函数字面量简化写法

Scala提供了多种方法来简化函数字面量中多余的部分,比如前面例子中filter方法中使用的函数字面量,完整的写法如下: (x :Int ) => x +1 首先可以省略到参数的类型,Scala可以根据上下文推算出参数的类型,函数定义可以简化为: (x) => x +1 这个函数可以进一步去掉参数的括号,这里的括号不起什么作用: x => x +1 Scala 还可以进一步简化,Scala允许使用”占位符”下划线”_”来替代一个或多个参数,只要这个参数值函数定义中只出现一次,Scala编