Here is an example for creating candle stick charts:
package scalafx.ensemble.example.charts
import scalafx.application.JFXApp
import scalafx.scene.Scene
import javafx.scene.{chart => jfxsc}
import javafx.scene.{layout => jfxsl}
import javafx.scene.{shape => jfxss}
import javafx.{scene => jfxs}
import scala.collection.JavaConversions.asScalaIterator
import scala.collection.mutable
import scalafx.Includes._
import scalafx.animation.FadeTransition
import scalafx.application.JFXApp
import scalafx.collections.ObservableBuffer
import scalafx.event.ActionEvent
import scalafx.scene.chart.{XYChart, Axis, NumberAxis}
import scalafx.scene.control.{Label, Tooltip}
import scalafx.scene.layout.{GridPane, Region}
import scalafx.scene.shape.{Line, LineTo, MoveTo, Path}
import scalafx.scene.{Node, Scene}
/**
* A custom candlestick chart.
*
* @see javafx.scene.chart.Axis
* @see javafx.scene.chart.Chart
* @see javafx.scene.chart.NumberAxis
* @see javafx.scene.chart.XYChart
*
* @resource /scalafx/ensemble/example/charts/AdvCandleStickChartSample.css
*/
object AdvCandleStickChartSample extends JFXApp {
case class CandleStick(day: Int, open: Double, close: Double, high: Double, low: Double, average: Double)
stage = new JFXApp.PrimaryStage {
title = "Adv Candle Stick Chart Example"
scene = new Scene {
root = {
val data = Array[CandleStick](
CandleStick(1, 25, 20, 32, 16, 20),
CandleStick(2, 26, 30, 33, 22, 25),
CandleStick(3, 30, 38, 40, 20, 32),
CandleStick(4, 24, 30, 34, 22, 30),
CandleStick(5, 26, 36, 40, 24, 32),
CandleStick(6, 28, 38, 45, 25, 34),
CandleStick(7, 36, 30, 44, 28, 39),
CandleStick(8, 30, 18, 36, 16, 31),
CandleStick(9, 40, 50, 52, 36, 41),
CandleStick(10, 30, 34, 38, 28, 36),
CandleStick(11, 24, 12, 30, 8, 32.4),
CandleStick(12, 28, 40, 46, 25, 31.6),
CandleStick(13, 28, 18, 36, 14, 32.6),
CandleStick(14, 38, 30, 40, 26, 30.6),
CandleStick(15, 28, 33, 40, 28, 30.6),
CandleStick(16, 25, 10, 32, 6, 30.1),
CandleStick(17, 26, 30, 42, 18, 27.3),
CandleStick(18, 20, 18, 30, 10, 21.9),
CandleStick(19, 20, 10, 30, 5, 21.9),
CandleStick(20, 26, 16, 32, 10, 17.9),
CandleStick(21, 38, 40, 44, 32, 18.9),
CandleStick(22, 26, 40, 41, 12, 18.9),
CandleStick(23, 30, 18, 34, 10, 18.9),
CandleStick(24, 12, 23, 26, 12, 18.2),
CandleStick(25, 30, 40, 45, 16, 18.9),
CandleStick(26, 25, 35, 38, 20, 21.4),
CandleStick(27, 24, 12, 30, 8, 19.6),
CandleStick(28, 23, 44, 46, 15, 22.2),
CandleStick(29, 28, 18, 30, 12, 23),
CandleStick(30, 28, 18, 30, 12, 23.2),
CandleStick(31, 28, 18, 30, 12, 22)
)
createChart(data)
}
}
}
protected def createChart( data : Array[CandleStick]): CandleStickChart = {
//Style Sheet loaded from external
val css: String = this.getClass.getResource("AdvCandleStickChartSample.css").toExternalForm
val xAxis = new NumberAxis("Day", 0, 32, 1) {
minorTickCount = 1
}
val yAxis = NumberAxis("Price")
val seriesData = data.map {d => XYChart.Data[Number, Number](d.day, d.open, d)}
val series = XYChart.Series[Number, Number](ObservableBuffer(seriesData.toSeq))
// new CandleStickChart(xAxis, yAxis) {
// title = "Custom Candle Stick Chart"
// data = ObservableBuffer(series)
// getStylesheets += css
// }
val res = new CandleStickChart(xAxis, yAxis)
res.title_=("Custom Candle Stick Chart")
res.data_=(ObservableBuffer(series))
res.getStylesheets().add(css)
res
}
/**
* A candlestick chart is a style of bar-chart used primarily to describe price movements of a security, derivative,
* or currency over time.
*
* The Data Y value is used for the opening price and then the close, high and low values are stored in the Data's
* extra value property using a CandleStick object.
*/
class CandleStickChart(xa: Axis[Number], ya: Axis[Number])
extends jfxsc.XYChart[Number, Number](xa, ya) {
setAnimated(false)
xAxis.animated = false
yAxis.animated = false
/**
* Construct a new CandleStickChart with the given axis and data.
*
* @param xAxis The x axis to use
* @param yAxis The y axis to use
* @param data The data to use, this is the actual list used so any changes to it will be reflected in the chart
*/
def this(xAxis: Axis[Number], yAxis: Axis[Number], data: ObservableBuffer[jfxsc.XYChart.Series[Number, Number]]) {
this(xAxis, yAxis)
setData(data)
}
def title: String = getTitle
def title_=(t: String) {
setTitle(t)
}
def data: ObservableBuffer[jfxsc.XYChart.Series[Number, Number]] = getData
def data_=(d: ObservableBuffer[jfxsc.XYChart.Series[Number, Number]]) {
setData(d)
}
def plotChildren = getPlotChildren
def xAxis = getXAxis
def yAxis = getYAxis
/** Called to update and layout the content for the plot */
override protected def layoutPlotChildren() {
if (data == null) {
return
}
for (series <- data) {
val seriesPath: Option[Path] = series.node() match {
case path: jfxss.Path => Some(path)
case _ => None
}
seriesPath.foreach(_.elements.clear())
for (item <- getDisplayedDataIterator(series)) {
item.extraValue() match {
case dayValues: CandleStick =>
val x = xAxis.displayPosition(dayValues.day)
item.node() match {
case candle: Candle =>
val yOpen = yAxis.displayPosition(dayValues.open)
val yClose = yAxis.displayPosition(dayValues.close)
val yHigh = yAxis.displayPosition(dayValues.high)
val yLow = yAxis.displayPosition(dayValues.low)
val candleWidth = xAxis match {
case xa: jfxsc.NumberAxis => xa.displayPosition(xa.tickUnit()) * 0.90
case _ => -1
}
candle.update(yClose - yOpen, yHigh - yOpen, yLow - yOpen, candleWidth)
candle.updateTooltip(item.YValue().doubleValue, dayValues.close, dayValues.high, dayValues.low)
candle.layoutX = x
candle.layoutY = yOpen
case _ =>
}
seriesPath.foreach {
p =>
val yAverage = yAxis.displayPosition(dayValues.average)
if (p.elements.isEmpty) {
p.elements += MoveTo(x, yAverage)
} else {
p.elements += LineTo(x, yAverage)
}
}
case _ =>
}
}
}
}
override protected def dataItemChanged(item: jfxsc.XYChart.Data[Number, Number]) {}
override protected def dataItemAdded(series: jfxsc.XYChart.Series[Number, Number],
itemIndex: Int, item: jfxsc.XYChart.Data[Number, Number]) {
val candle = Candle(getData.indexOf(series), item, itemIndex)
if (shouldAnimate) {
candle.opacity = 0
plotChildren += candle
new FadeTransition(500 ms, candle) {
toValue = 1
}.play()
} else {
plotChildren += candle
}
if (series.node() != null) {
series.node().toFront()
}
}
override protected def dataItemRemoved(item: jfxsc.XYChart.Data[Number, Number], series: jfxsc.XYChart.Series[Number, Number]) {
val candle = item.node()
if (shouldAnimate) {
new FadeTransition(500 ms, candle) {
toValue = 0
onFinished = (_: ActionEvent) => plotChildren -= candle
}.play()
}
else {
plotChildren -= candle
}
}
override protected def seriesAdded(series: jfxsc.XYChart.Series[Number, Number], seriesIndex: Int) {
for (j <- 0 until series.data().size) {
val item = series.data()(j)
val candle = Candle(seriesIndex, item, j)
if (shouldAnimate) {
candle.opacity = 0
plotChildren += candle
val ft = new FadeTransition(500 ms, candle) {
toValue = 1
}
ft.play()
}
else {
plotChildren += candle
}
}
val seriesPath = new Path {
styleClass = Seq("candlestick-average-line", "series" + seriesIndex)
}
series.node = seriesPath
plotChildren += seriesPath
}
override protected def seriesRemoved(series: jfxsc.XYChart.Series[Number, Number]) {
for (d <- series.getData) {
val candle = d.node()
if (shouldAnimate) {
new FadeTransition(500 ms, candle) {
toValue = 0
onFinished = (_: ActionEvent) => plotChildren -= candle
}.play()
}
else {
plotChildren -= candle
}
}
}
/**
* This is called when the range has been invalidated and we need to update it. If the axis are auto
* ranging then we compile a list of all data that the given axis has to plot and call invalidateRange() on the
* axis passing it that data.
*/
override protected def updateAxisRange() {
if (xAxis.isAutoRanging) {
val xData = for (series <- data; seriesData <- series.data()) yield seriesData.XValue()
xAxis.invalidateRange(xData)
}
if (yAxis.isAutoRanging) {
val yData = mutable.ListBuffer.empty[Number]
for (series <- data; seriesData <- series.data()) {
seriesData.extraValue() match {
case extras: CandleStick =>
yData += extras.high
yData += extras.low
case _ =>
yData += seriesData.YValue()
}
}
yAxis.invalidateRange(yData)
}
}
}
private object Candle {
/**
* Create a new Candle node to represent a single data item
*
* @param seriesIndex The index of the series the data item is in
* @param item The data item to create node for
* @param itemIndex The index of the data item in the series
* @return New candle node to represent the give data item
*/
def apply(seriesIndex: Int, item: XYChart.Data[_, _], itemIndex: Int): Node = {
var candle = item.node()
candle match {
case c: Candle =>
c.setSeriesAndDataStyleClasses("series" + seriesIndex, "data" + itemIndex)
case _ =>
candle = new Candle("series" + seriesIndex, "data" + itemIndex)
item.node = candle
}
candle
}
}
/** Candle node used for drawing a candle */
private class Candle(private var seriesStyleClass: String,
private var dataStyleClass: String) extends jfxs.Group {
private val highLowLine: Line = new Line
private val bar: Region = new Region
private var openAboveClose: Boolean = true
private val tooltip: Tooltip = new Tooltip
def styleClass = getStyleClass
setAutoSizeChildren(false)
getChildren.addAll(highLowLine, bar)
updateStyleClasses()
tooltip.graphic = new TooltipContent()
Tooltip.install(bar, tooltip)
def setSeriesAndDataStyleClasses(seriesStyleClass: String, dataStyleClass: String) {
this.seriesStyleClass = seriesStyleClass
this.dataStyleClass = dataStyleClass
updateStyleClasses()
}
def update(closeOffset: Double, highOffset: Double, lowOffset: Double, candleWidth: Double) {
openAboveClose = closeOffset > 0
updateStyleClasses()
highLowLine.startY = highOffset
highLowLine.endY = lowOffset
val cw = if (candleWidth == -1) {
// FIXME: It should be possible to access this method without delegate, it is not the same as setPrefWidth
bar.delegate.prefWidth(-1)
} else
candleWidth
if (openAboveClose) {
bar.resizeRelocate(-cw / 2, 0, cw, closeOffset)
}
else {
bar.resizeRelocate(-cw / 2, closeOffset, cw, closeOffset * -1)
}
}
def updateTooltip(open: Double, close: Double, high: Double, low: Double) {
val tooltipContent: TooltipContent = tooltip.graphic().asInstanceOf[TooltipContent]
tooltipContent.update(open, close, high, low)
}
private def updateStyleClasses() {
val closeVsOpen = if (openAboveClose) "open-above-close" else "close-above-open"
styleClass = Seq("candlestick-candle", seriesStyleClass, dataStyleClass)
highLowLine.styleClass = Seq("candlestick-line", seriesStyleClass, dataStyleClass, closeVsOpen)
bar.styleClass = Seq("candlestick-bar", seriesStyleClass, dataStyleClass, closeVsOpen)
}
}
private class TooltipContent extends jfxsl.GridPane {
private val openValue = new Label()
private val closeValue = new Label()
private val highValue = new Label()
private val lowValue = new Label()
val open = new Label("OPEN:") {styleClass += "candlestick-tooltip-label"}
val close = new Label("CLOSE:") {styleClass += "candlestick-tooltip-label"}
val high = new Label("HIGH:") {styleClass += "candlestick-tooltip-label"}
val low = new Label("LOW:") {styleClass += "candlestick-tooltip-label"}
GridPane.setConstraints(open, 0, 0)
GridPane.setConstraints(openValue, 1, 0)
GridPane.setConstraints(close, 0, 1)
GridPane.setConstraints(closeValue, 1, 1)
GridPane.setConstraints(high, 0, 2)
GridPane.setConstraints(highValue, 1, 2)
GridPane.setConstraints(low, 0, 3)
GridPane.setConstraints(lowValue, 1, 3)
getChildren.addAll(open, openValue, close, closeValue, high, highValue, low, lowValue)
def update(open: Double, close: Double, high: Double, low: Double) {
openValue.text = open.toString
closeValue.text = close.toString
highValue.text = high.toString
lowValue.text = low.toString
}
}
}
A long example, but you only have to look at these lines:
// new CandleStickChart(xAxis, yAxis) {
// title = "Custom Candle Stick Chart"
// data = ObservableBuffer(series)
// getStylesheets += css
// }
val res = new CandleStickChart(xAxis, yAxis)
res.title_=("Custom Candle Stick Chart")
res.data_=(ObservableBuffer(series))
res.getStylesheets().add(css)
res
The commented part is actually equivalent with the uncommented part below it.
0 comments:
Post a Comment