使用bokeh-scala進行數據可視化

目錄

  1. 前言
  2. bokeh簡介及胡扯
  3. bokeh-scala基本代碼
  4. 個人封裝
  5. 總結

1、前言

       最近在使用spark集羣以及geotrellis框架(相關文章見http://www.cnblogs.com/shoufengwei/p/5619419.html)進行分佈式空間地理系統設計(暫且誇大稱之爲地理信息系統),雖然說是空間地理信息系統可是也少不了數據可視化方面的操做,因此就想尋找一款支持大數據的可視化框架,網上查閱半天發現bokeh不錯(實際上是老闆直接指明方向說用這款),剛好bokeh也有scala語言的封裝,Github地址,因而拿來練練手,算是作個技術儲備。html

2、bokeh簡介及胡扯

       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的代碼其實也能夠寫的很優美,最近在碼代碼的過程當中有個感受就是實現功能很容易,可是要想代碼寫的漂亮看上去舒服甚至有藝術感就徹底不是一件簡單的事情。言歸正傳,我在用一個小時完成簡單功能以後,又花了五六個小時進行了簡單的重構、二次封裝、完善,但願個人封裝能用起來舒服一點,可是因爲水平有限,也可能我只是多此一舉,用起來可能還不如原來的,各位看官自行取捨。先發上幾幅我作出來的效果圖,各位看官能夠提早有個感受。設計模式

3、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函數。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對象以圖表的形式顯示到瀏覽器當中。

4、個人封裝

       下面我將今天封裝的代碼貼在下面,供學習交流,又稍做修改,修改後以下:
一、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中的內容看官能夠根據本身的須要自行修改!

5、總結

       以上就是我總結的有關於bokeh-scala數據可視化的基礎,本次並無徹底封裝bokeh-scala的所有功能,後續會慢慢完善,更新該篇博客或者另設新篇。歡迎探討、交流。

相關文章
相關標籤/搜索