scalascalafx

How to change LineChart XAxis lower/upper bounds in ScalaFX?


I have a ScalaFX LineChart whose XAxis is time. The data is dynamic, being updated in an AnimationTimer. As time progresses the plot moves to the right but the plot always starts at 0, so the visible domain increases, compressing the data points.

enter image description here

Here is the Stage:

new Stage {
  title = plotTitle
  scene = new Scene(600, 600) {
    val series = new XYChart.Series[Number, Number]()
    series.setName(s"Simple")
    val xAxis = NumberAxis("Time (s)")
    val yAxis = NumberAxis("Amplitude")
    val lineChart = new LineChart(xAxis, yAxis)
    lineChart.setAnimated(false)
    lineChart.getData.add(series)
    lineChart.setCreateSymbols(false)
    root = lineChart

    var index = 0
    val timer = AnimationTimer { time =>
      series.getData.add(XYChart.Data[Number, Number](index, math.cos(index)))
      if (series.getData.size > 100) series.getData.remove(0, series.getData.size() - 100)
      xAxis.lowerBound.value = math.max(index - 100, 0) // this has no effect
      xAxis.upperBound.value = math.max(index, 100) // this has no effect
      index += 1
    }
    timer.start()
  }
}

How do I update the lower/upper bounds of the XAxis in the AnimationTimer update? I tried setting the XAxis.lower/upperBound values during the animation update but it has no effect. I have tried other things to no avail. There are JavaFX solutions I've seen but I can't translate it to ScalaFX because there is not a one-to-one mapping of types.


Solution

  • You can specify an explicit lower bound in a few ways, but all require (to the best of my knowledge) manually also setting an upper bound and tick unit, and disabling auto-ranging:

    1. By specifying the lower & upper bounds, and tick unit, as fixed values, in the NumberAxis constructor for the X-axis. Axes created in this way are automatically not auto-ranging.
    2. By setting the lowerBound, upperBound and tickUnit properties of the X-axis NumberAxis to fixed values. This also requires setting the X-axis's autoRanging = false.
    3. By binding DoubleProperty instances to the lowerBound, upperBound and tickUnit properties of the X-axis NumberAxis. This allows the bounds and tick unit to be dynamically adjusted, by changing the value of the associated property. This also requires setting autoRanging = false.

    Here's an example of the first option:

    new Stage {
      title = plotTitle
      scene = new Scene(600, 600) {
        val series = new XYChart.Series[Number, Number]()
        series.setName(s"Simple")
    
        // Specify an X-axis with fixed upper & lower bounds, and tick unit
        // (separation of major tick values).
        //
        // Note: This is automatically not an auto-ranging axis.
        val xAxis = NumberAxis("Time (s)", 115.0, 225.0, 25.0)
        val yAxis = NumberAxis("Amplitude")
        val lineChart = new LineChart(xAxis, yAxis)
        lineChart.setAnimated(false)
        lineChart.getData.add(series)
        lineChart.setCreateSymbols(false)
        root = lineChart
    
        var index = 0
        val timer = AnimationTimer { time =>
          series.getData.add(XYChart.Data[Number, Number](index, math.cos(index)))
          if (series.getData.size > 100) series.getData.remove(0, series.getData.size() - 100)
          index += 1
        }
        timer.start()
      }
    }
    

    In this example, I'm combining the second and third options to use properties for the lower & upper bounds, with a fixed tick unit.

    new Stage {
      title = plotTitle
      scene = new Scene(600, 600) {
        val series = new XYChart.Series[Number, Number]()
        series.setName(s"Simple")
    
        // Note: This axis is automatically auto-ranging.
        val xAxis = NumberAxis("Time (s)")
        val yAxis = NumberAxis("Amplitude")
    
        // Disable auto-ranging of the xAxis.
        xAxis.autoRanging = false
    
        // Create a double property whose value will be used as the lower bound of the
        // X-axis. If the value of this property is changed, the chart will update
        // accordingly, automatically. Make this 0 initially.
        val xLowBound = DoubleProperty(0.0)
    
        // Bind this property to the lowerBound of the X-axis
        xAxis.lowerBound <== xLowBound
    
        // Repeat for the upper bound. 100, initially.
        val xUpBound = DoubleProperty(100.0)
        xAxis.upperBound <== xUpBound
    
        // Set the tick unit to a fixed value.
        xAxis.tickUnit = 25.0
    
        val lineChart = new LineChart(xAxis, yAxis)
        lineChart.setAnimated(false)
        lineChart.getData.add(series)
        lineChart.setCreateSymbols(false)
        root = lineChart
    
        var index = 0
        val timer = AnimationTimer { time =>
          series.getData.add(XYChart.Data[Number, Number](index, math.cos(index)))
          if (series.getData.size > 100) series.getData.remove(0, series.getData.size() - 100)
    
          // Dynamically update the lower & upper bounds.
          xLowBound.value = math.max(index - 100, 0)
          xUpBound.value = math.max(index, 100)
    
          index += 1
        }
        timer.start()
      }
    }
    

    Let me know if this does the trick for you.