Two-dimensional plots

Time-series plot example

axle.visualize.Plot

Imports

import org.joda.time.DateTime

import scala.collection.immutable.TreeMap
import scala.math.sin

import spire.random.Generator
import spire.random.Generator.rng

import cats.implicits._

import axle._
import axle.visualize._
import axle.joda.dateTimeOrder

import axle.visualize.Color._

Generate the time-series to plot

val now = new DateTime()
// now: DateTime = 2021-03-01T23:06:43.474-05:00

val colors = Vector(red, blue, green, yellow, orange)
// colors: Vector[Color] = Vector(
//   Color(r = 255, g = 0, b = 0),
//   Color(r = 0, g = 0, b = 255),
//   Color(r = 0, g = 255, b = 0),
//   Color(r = 255, g = 255, b = 0),
//   Color(r = 255, g = 200, b = 0)
// )

def randomTimeSeries(i: Int, gen: Generator) = {
  val φ = gen.nextDouble()
  val A = gen.nextDouble()
  val ω = 0.1 / gen.nextDouble()
  ("series %d %1.2f %1.2f %1.2f".format(i, φ, A, ω),
    new TreeMap[DateTime, Double]() ++
      (0 to 100).map(t => (now.plusMinutes(2 * t) -> A * sin(ω * t + φ))).toMap)
}

val waves = (0 until 20).map(i => randomTimeSeries(i, rng)).toList
// waves: List[(String, TreeMap[DateTime, Double])] = List(
//   (
//     "series 0 0.10 0.18 7.00",
//     TreeMap(
//       2021-03-01T23:06:43.474-05:00 -> 0.018981138236693403,
//       2021-03-01T23:08:43.474-05:00 -> 0.13406804466449151,
//       2021-03-01T23:10:43.474-05:00 -> 0.18391367663683592,
//       2021-03-01T23:12:43.474-05:00 -> 0.14426175023423315,
//       2021-03-01T23:14:43.474-05:00 -> 0.034408006060594465,
//       2021-03-01T23:16:43.474-05:00 -> -0.09218963985698185,
//       2021-03-01T23:18:43.474-05:00 -> -0.17392521882258374,
//       2021-03-01T23:20:43.474-05:00 -> -0.17102390340872745,
//       2021-03-01T23:22:43.474-05:00 -> -0.08489755514938943,
//       2021-03-01T23:24:43.474-05:00 -> 0.042542326920032764,
//       2021-03-01T23:26:43.474-05:00 -> 0.1492799183465997,
//       2021-03-01T23:28:43.474-05:00 -> 0.18337371051503104,
//       2021-03-01T23:30:43.474-05:00 -> 0.12823270698277833,
//       2021-03-01T23:32:43.474-05:00 -> 0.010690068136416937,
//       2021-03-01T23:34:43.474-05:00 -> -0.11205465798570673,
//       2021-03-01T23:36:43.474-05:00 -> -0.18027044162975944,
//       2021-03-01T23:38:43.474-05:00 -> -0.16076156733608077,
//       2021-03-01T23:40:43.474-05:00 -> -0.06302160104199767,
//       2021-03-01T23:42:43.474-05:00 -> 0.0653864456324064,
//       2021-03-01T23:44:43.474-05:00 -> 0.1619756121564676,
//       2021-03-01T23:46:43.474-05:00 -> 0.17974289838656382,
//       2021-03-01T23:48:43.474-05:00 -> 0.1100422440316913,
//       2021-03-01T23:50:43.474-05:00 -> -0.013208055673283365,
//       2021-03-01T23:52:43.474-05:00 -> -0.13003094467528614,
//       2021-03-01T23:54:43.474-05:00 -> -0.18357712541699822,
//       2021-03-01T23:56:43.474-05:00 -> -0.14778952305591456,
//       2021-03-01T23:58:43.474-05:00 -> -0.040083389627562,
//       2021-03-02T00:00:43.474-05:00 -> 0.08712844652317689,
//       2021-03-02T00:02:43.474-05:00 -> 0.17194113448971082,
//       2021-03-02T00:04:43.474-05:00 -> 0.17308243921575972,
//       2021-03-02T00:06:43.474-05:00 -> 0.08999696977982687,
//       2021-03-02T00:08:43.474-05:00 -> -0.036883551789960424,
//       2021-03-02T00:10:43.474-05:00 -> -0.14581550156366332,
//       2021-03-02T00:12:43.474-05:00 -> -0.18378953454831154,
//       2021-03-02T00:14:43.474-05:00 -> -0.13232642018354135,
//       2021-03-02T00:16:43.474-05:00 -> -0.0164695547313557,
//       2021-03-02T00:18:43.474-05:00 -> 0.10740185843111372,
//       2021-03-02T00:20:43.474-05:00 -> 0.1790085120047816,
//       2021-03-02T00:22:43.474-05:00 -> 0.1635045980245419,
//       2021-03-02T00:24:43.474-05:00 -> 0.06843475630109963,
//       2021-03-02T00:26:43.474-05:00 -> -0.05993735912451504,
//       2021-03-02T00:28:43.474-05:00 -> -0.15914227287606506,
//       2021-03-02T00:30:43.474-05:00 -> -0.18090408877267256,
//       2021-03-02T00:32:43.474-05:00 -> -0.11463289624234962,
//       2021-03-02T00:34:43.474-05:00 -> 0.007421881885020493,
// ...

Imports for visualization

import cats.Show

import spire.algebra._

import axle.visualize.Plot
import axle.algebra.Plottable.doublePlottable
import axle.joda.dateTimeOrder
import axle.joda.dateTimePlottable
import axle.joda.dateTimeTics
import axle.joda.dateTimeDurationLengthSpace

implicit val fieldDouble: Field[Double] = spire.implicits.DoubleAlgebra

Define the visualization

val plot = Plot[String, DateTime, Double, TreeMap[DateTime, Double]](
  () => waves,
  connect = true,
  colorOf = s => colors(s.hash.abs % colors.length),
  title = Some("Random Waves"),
  xAxisLabel = Some("time (t)"),
  yAxis = Some(now),
  yAxisLabel = Some("A·sin(ω·t + φ)")).zeroXAxis
// plot: Plot[String, DateTime, Double, TreeMap[DateTime, Double]] = Plot(
//   dataFn = <function0>,
//   connect = true,
//   drawKey = true,
//   width = 700,
//   height = 600,
//   border = 50,
//   pointDiameter = 4,
//   keyLeftPadding = 20,
//   keyTopPadding = 50,
//   keyWidth = 80,
//   fontName = "Courier New",
//   fontSize = 12,
//   bold = false,
//   titleFontName = "Palatino",
//   titleFontSize = 20,
//   colorOf = <function1>,
//   title = Some(value = "Random Waves"),
//   keyTitle = None,
//   xAxis = Some(value = 0.0),
//   xAxisLabel = Some(value = "time (t)"),
//   yAxis = Some(value = 2021-03-01T23:06:43.474-05:00),
//   yAxisLabel = Some(value = "A\u00b7sin(\u03c9\u00b7t + \u03c6)")
// )

If instead we had supplied (Color, String) pairs, we would have needed something like preciding the Plot definition:

implicit val showCL: Show[(Color, String)] = new Show[(Color, String)] { def show(cl: (Color, String)): String = cl._2 }
// showCL: Show[(Color, String)] = repl.MdocSession$App$$anon$1@2b0d0ebb

Create the SVG

import axle.web._
import cats.effect._

plot.svg[IO]("random_waves.svg").unsafeRunSync()

waves

Animation

This example traces two “saw” functions vs time:

Imports

import org.joda.time.DateTime
import edu.uci.ics.jung.graph.DirectedSparseGraph
import collection.immutable.TreeMap

import cats.implicits._

import monix.reactive._

import spire.algebra.Field

import axle.jung._
import axle.quanta.Time
import axle.visualize._
import axle.reactive.intervalScan

Define stream of data updates refreshing every 500 milliseconds

val initialData = List(
  ("saw 1", new TreeMap[DateTime, Double]()),
  ("saw 2", new TreeMap[DateTime, Double]())
)
// initialData: List[(String, TreeMap[DateTime, Double])] = List(
//   ("saw 1", TreeMap()),
//   ("saw 2", TreeMap())
// )

// Note: uses zeroDT defined above

val saw1 = (t: Long) => (t % 10000) / 10000d
// saw1: Long => Double = <function1>
val saw2 = (t: Long) => (t % 100000) / 50000d
// saw2: Long => Double = <function1>

val fs = List(saw1, saw2)
// fs: List[Long => Double] = List(<function1>, <function1>)

val refreshFn = (previous: List[(String, TreeMap[DateTime, Double])]) => {
  val now = new DateTime()
  previous.zip(fs).map({ case (old, f) => (old._1, old._2 ++ Vector(now -> f(now.getMillis))) })
}
// refreshFn: List[(String, TreeMap[DateTime, Double])] => List[(String, TreeMap[DateTime, Double])] = <function1>

implicit val timeConverter = {
  import axle.algebra.modules.doubleRationalModule
  Time.converterGraphK2[Double, DirectedSparseGraph]
}
// timeConverter: quanta.UnitConverterGraph[Time, Double, DirectedSparseGraph[quanta.UnitOfMeasurement[Time], Double => Double]] with quanta.TimeConverter[Double] = axle.quanta.Time$$anon$1@1740f7a5
import timeConverter.millisecond

val dataUpdates: Observable[Seq[(String, TreeMap[DateTime, Double])]] =
  intervalScan(initialData, refreshFn, 500d *: millisecond)
// dataUpdates: Observable[Seq[(String, TreeMap[DateTime, Double])]] = monix.reactive.internal.operators.ScanObservable@669db956

Create CurrentValueSubscriber, which will be used by the Plot to get the latest values

import monix.execution.Scheduler.Implicits.global
import axle.reactive.CurrentValueSubscriber

val cvSub = new CurrentValueSubscriber[Seq[(String, TreeMap[DateTime, Double])]]()
val cvCancellable = dataUpdates.subscribe(cvSub)

// [DateTime, Double, TreeMap[DateTime, Double]]

val plot = Plot(
  () => cvSub.currentValue.getOrElse(initialData),
  connect = true,
  colorOf = (label: String) => Color.black,
  title = Some("Saws"),
  xAxis = Some(0d),
  xAxisLabel = Some("time (t)"),
  yAxisLabel = Some("y")
)

Animate

import axle.awt._

val (frame, paintCancellable) = play(plot, dataUpdates)

Tear down resources

paintCancellable.cancel()
cvCancellable.cancel()
frame.dispose()