scalascalafx

Changing the size and symbol of scatter chart plot points in ScalaFX


I want to make a linear regression program which visualizes the data to user. I'm using EJML for calculations and ScalaFX for front end. Everything is going fine but when I plot the data using Scatter Chart, the line drawn from the data is set to be rectangles which cover up the original data points. I would like to know how I can change the size, shape and transparency etc. of the plotted points.

Almost all of guides around JavaFX say that I should modify the CSS file (which doesn't automatically exist) in order to style my chart. I don't know how to do that in ScalaFX or even that is it possible to do that way. My result of searching every possible tutorial has been fruitless.

import scalafx.application.JFXApp
import scalafx.scene.Scene
import scalafx.scene.chart.ScatterChart
import scalafx.collections.ObservableBuffer
import scalafx.scene.chart.NumberAxis
import scalafx.scene.chart.XYChart
import scalafx.scene.shape.Line
import org.ejml.simple.SimpleMatrix
import scala.math.pow
import scala.collection.mutable.Buffer


object Plotting extends JFXApp {
  /*
   * Below are some arbitrary x and y values for a regression line
   */
  val xValues = Array(Array(1.0, 1.0, 1.0, 1.0, 1.0, 1.0), Array(14.0, 19.0, 22.0, 26.0, 31.0, 43.0))
  val yValues = Array(Array(51.0, 57.0, 66.0, 71.0, 72.0, 84.0))
  val temp    = yValues.flatten
  val wrapper = xValues(1).zip(temp)
  /*
   * In the lines before stage what happens is that matrices for the x and y values are created, coefficients
   * for the regression line are calculated with matrix operations and (x, y) points are calculated for the 
   * regression line.
   */
  val X       = new SimpleMatrix(xValues).transpose
  val Y       = new SimpleMatrix(yValues).transpose
  val secondX = new SimpleMatrix(xValues(0).size, 2)
  for (i <- 0 until xValues(0).size) {
    secondX.set(i, 0, xValues(0)(i))
    secondX.set(i, 1, xValues(1)(i))
  }
  val invertedSecondX = secondX.pseudoInverse()
  val B = invertedSecondX.mult(Y)
  val graphPoints = Buffer[(Double, Double)]()
  for (i <- 0 to xValues(1).max.toInt) {
    graphPoints.append((i.toDouble, B.get(0, 0) + i * B.get(1, 0)))
  }

  stage = new JFXApp.PrimaryStage {
    title = "Demo"
    scene = new Scene(400, 400) {
      val xAxis = NumberAxis()
      val yAxis = NumberAxis()
      val pData = XYChart.Series[Number, Number](
        "Data",
        ObservableBuffer(wrapper.map(z => XYChart.Data[Number, Number](z._1, z._2)): _*))
      val graph = XYChart.Series[Number, Number](
        "RegressionLine",
        ObservableBuffer(graphPoints.map(z => XYChart.Data[Number, Number](z._1, z._2)): _*))
      val plot = new ScatterChart(xAxis, yAxis, ObservableBuffer(graph, pData))
      root     = plot
    }
  }
}

Solution

  • This certainly isn't as well documented as it might be... :-(

    Stylesheets are typically placed in your project's resource directory. If you're using SBT (recommended), this would be src/main/resources.

    In this example, I've added a stylesheet called MyCharts.css to this directory with the following contents:

    /* Blue semi-transparent 4-pointed star, using SVG path. */
    .default-color0.chart-symbol {
        -fx-background-color: blue;
        -fx-scale-shape: true;
        -fx-shape: "M 0.0 10.0 L 3.0 3.0 L 10.0 0.0 L 3.0 -3.0 L 0.0 -10.0 L -3.0 -3.0 L -10.0 0.0 L -3.0 3.0 Z ";
        -fx-opacity: 0.5;
    }
    
    /* Default shape is a rectangle. Here, we round it to become a red circle with a white
     * center. Change the radius to control the size.
     */
    .default-color1.chart-symbol {
        -fx-background-color: red, white;
        -fx-background-insets: 0, 2;
        -fx-background-radius: 3px;
        -fx-padding: 3px;
    }
    

    color0 will be used for the first data series (the regression line), color1 for the second (your scatter data). All other series use the default, JavaFX style.

    (For more information on using scalable vector graphics (SVG) paths to define custom shapes, refer to the relevant section of the SVG specification.)

    To have this stylesheet used by ScalaFX (JavaFX), you have a choice of options. To have them apply globally, add it to the main scene (which is what I've done below). Alternatively, if each chart needs a different style, you can add different stylesheets to specific charts. (BTW, I also added the standard includes import as this prevents many JavaFX-ScalaFX element conversion issues; otherwise, I've made no changes to your sources.)

    import scalafx.Includes._
    import scalafx.application.JFXApp
    import scalafx.scene.Scene
    import scalafx.scene.chart.ScatterChart
    import scalafx.collections.ObservableBuffer
    import scalafx.scene.chart.NumberAxis
    import scalafx.scene.chart.XYChart
    import scalafx.scene.shape.Line
    import org.ejml.simple.SimpleMatrix
    import scala.math.pow
    import scala.collection.mutable.Buffer
    
    object Plotting extends JFXApp {
      /*
       * Below are some arbitrary x and y values for a regression line
       */
      val xValues = Array(Array(1.0, 1.0, 1.0, 1.0, 1.0, 1.0), Array(14.0, 19.0, 22.0, 26.0, 31.0, 43.0))
      val yValues = Array(Array(51.0, 57.0, 66.0, 71.0, 72.0, 84.0))
      val temp    = yValues.flatten
      val wrapper = xValues(1).zip(temp)
      /*
       * In the lines before stage what happens is that matrices for the x and y values are created, coefficients
       * for the regression line are calculated with matrix operations and (x, y) points are calculated for the 
       * regression line.
       */
      val X       = new SimpleMatrix(xValues).transpose
      val Y       = new SimpleMatrix(yValues).transpose
      val secondX = new SimpleMatrix(xValues(0).size, 2)
      for (i <- 0 until xValues(0).size) {
        secondX.set(i, 0, xValues(0)(i))
        secondX.set(i, 1, xValues(1)(i))
      }
      val invertedSecondX = secondX.pseudoInverse()
      val B = invertedSecondX.mult(Y)
      val graphPoints = Buffer[(Double, Double)]()
      for (i <- 0 to xValues(1).max.toInt) {
        graphPoints.append((i.toDouble, B.get(0, 0) + i * B.get(1, 0)))
      }
    
      stage = new JFXApp.PrimaryStage {
        title = "Demo"
        scene = new Scene(400, 400) {
    
          // Add our stylesheet.
          stylesheets.add("MyCharts.css")
    
          val xAxis = NumberAxis()
          val yAxis = NumberAxis()
          val pData = XYChart.Series[Number, Number](
            "Data",
            ObservableBuffer(wrapper.map(z => XYChart.Data[Number, Number](z._1, z._2)): _*))
          val graph = XYChart.Series[Number, Number](
            "RegressionLine",
            ObservableBuffer(graphPoints.map(z => XYChart.Data[Number, Number](z._1, z._2)): _*))
          val plot = new ScatterChart(xAxis, yAxis, ObservableBuffer(graph, pData))
          root     = plot
        }
      }
    }
    

    For further information in the CSS formatting options available (changing shapes, colors, transparency, etc.) refer to the JavaFX CSS Reference Guide.

    The result looks like this: Chart image, with customized points.