Search code examples
scalajfreechart

Specifying logarithmic axis values (labels and ticks) in JFreeChart


I am struggling with LogAxis to get sensible frequency labels, e.g. using an equal tempered scale with A4 = 440 Hz, such as this table, I want labels to appear for example at

(30 to 120 by 2).map(midicps).foreach(println)

46.249302
51.91309
58.270466
65.406395
73.4162
82.40688
92.498604
103.82618
116.54095
130.81279
146.83238
164.81378
184.99721
207.65234
233.08188
261.62558
293.66476
329.62756
369.99442
415.3047
466.16376
523.25116
587.3295
...
4698.6367
5274.0405
5919.9106
6644.8755
7458.621
8372.019

Hertz, where

def midicps(d: Double): Double = 440 * math.pow(2, (d - 69) / 12)

In other words, I have twelve divisions per octave (doubling of value), with a fixed frequency being 440.0. I happen to have a lower bound of 32.7 and upper bound of 16700.0 for the plot.

My first attempt:

import org.jfree.chart._
val pl = new plot.XYPlot
val yaxis = new axis.LogAxis
yaxis.setLowerBound(32.7)
yaxis.setUpperBound(16.7e3)
yaxis.setBase(math.pow(2.0, 1.0/12))
yaxis.setMinorTickMarksVisible(true)
yaxis.setStandardTickUnits(axis.NumberAxis.createStandardTickUnits())
pl.setRangeAxis(yaxis)
val ch = new JFreeChart(pl)
val pn = new ChartPanel(ch)
new javax.swing.JFrame {
  getContentPane.add(pn)
  pack()
  setVisible(true)
}

This gives my labels which do not fall into any of the above raster points:

enter image description here

Any ideas how to enforce my raster?


Solution

  • One possibility is to to a log<->lin conversion outside of JFreeChart, and convert back with a custom number format:

    import java.text.{ParsePosition, FieldPosition, NumberFormat}
    import scalax.chart.api._
    
    object PDFLogAxis extends App {
      scala.swing.Swing.onEDT(run())
    
      def midicps(d: Double): Double = 440 * math.pow(2, (d - 69) / 12)
      def cpsmidi(d: Double): Double = math.log(d / 440) / math.log(2) * 12 + 69
    
      def run(): Unit = {
        val lo    = cpsmidi(32.7)    // log -> lin
        val hi    = cpsmidi(16.7e3)
        val data  = Vector((0.0, lo), (1.0, hi))
        val chart = XYLineChart(data, title = "", legend = false)
        val yAxis = chart.plot.range.axis.peer
          .asInstanceOf[org.jfree.chart.axis.NumberAxis]
        yAxis.setLowerBound(lo)
        yAxis.setUpperBound(hi)
        yAxis.setNumberFormatOverride(new NumberFormat {
          def format(d: Double, sb: StringBuffer, 
                     pos: FieldPosition): StringBuffer = {
            val freq = midicps(d)  // lin -> log
            sb.append(f"$freq%1.1f")
          }
    
          def parse(s: String, parsePosition: ParsePosition): Number = ???
    
          def format(d: Long, sb: StringBuffer, 
                     pos: FieldPosition): StringBuffer = ???
        })
        chart.show()
      }
    }