Creating your first JavaFX chart app, part 2

In this second tutorial, you'll see some of the basic features of Charts-kt, and build a bar chart based on your data model.

This tutorial will help you understand how to:

  1. Parse and map a CSV file to a Kotlin data class
  2. Use your own model and data in your charts
  3. Describe your values through "dimensions"
  4. Create a bar chart
  5. Understand the "Datum" concept

What do you need?

  • 15 minutes
  • A working JavaFX environment (see part 1)
  • Intellij Community or Ultimate (tested with IntelliJ 2020.3.*)

Parsing a CSV file

This CSV file contains statistical weather data for each month of 2016 and 2017, for 5 different cities. Save it in the resources repository of your project:

Weather - Stats.csv

Parsing this file is very easy using the multiplatform Dsv module of data2viz.

  1. create a Kotlin data class Weather to hold the information,
  2. load the file which is located in "./src/main/resources/",
  3. parse each row and, if needed, drop the header (the first line of the file),
  4. finally, map each row of the file in a Weather instance.
import io.data2viz.dsv.Dsv import java.io.File fun main() { val csvContent = File("./src/main/resources/Weather - Stats.csv").readText() val weatherData = parseWeatherCSV(csvContent) println("Loaded ${weatherData.size} records") } data class Weather( val city: String, val year: Int, val month: Int, val highTemp: Double, val avgTemp: Double, val lowTemp: Double, val precipitation: Double ) fun parseWeatherCSV(csvContent: String): List<Weather> = Dsv() .parseRows(csvContent) .drop(1) .map { row -> Weather( row[0], row[1].toInt(), row[2].toInt(), row[3].toDouble(), row[4].toDouble(), row[5].toDouble(), row[6].toDouble() ) }

The Kotlin data class offers a very convenient way to hold your information loaded from your CSV file.

Copy this code in a LoadCSV.kt file, and run the main(), you should see this result:

Creating%20your%20first%20JavaFX%20chart%20app,%20part%202/Untitled.png

Using a strongly typed data model

Kotlin is a strongly typed language that helps your writing code via auto-completion and compilation time checking.

This feature helps you when managing your data and building your chart.

Preparing the dataset with Kotlin built-in functions

Let's have a look at the weather in Chicago for 2017.

To filter the dataset, use the built-in filter function of Kotlin:

val csvContent = File("./src/main/resources/Weather - Stats.csv").readText() val weatherData = parseWeatherCSV(csvContent) val chicago2017 = weatherData.filter { weather -> weather.city == "Chicago" && weather.year == 2017 }

Here, the filter lambda takes a Weather parameter, so your IDE can provide auto-completion, as you can see:

Creating%20your%20first%20JavaFX%20chart%20app,%20part%202/Untitled%201.png

Also benefit from strong-typing in your chart

The same applies to the Chart you create with Charts-kt: by giving it your dataset on instantiation, your chart will be typed with your data, here the Weather class.

This Weather class will be referred as the DOMAIN.

Think of it as a generic class <DOMAIN>, so using it in your chart will create a typed instance: Chart<Weather>.

Copy and paste this whole code in a new Kotlin file named WeatherChart.kt in your kotlin directory:

import io.data2viz.charts.chart.Chart import io.data2viz.charts.chart.chart import io.data2viz.charts.chart.discrete import io.data2viz.charts.chart.mark.bar import io.data2viz.charts.chart.quantitative import io.data2viz.charts.viz.VizContainer import io.data2viz.charts.viz.newVizContainer import io.data2viz.geom.Size import javafx.application.Application import javafx.scene.Scene import javafx.scene.layout.Pane import javafx.stage.Stage fun main() { Application.launch(WeatherChart::class.java) } private val width = 500.0 private val height = 400.0 class WeatherChart : Application() { override fun start(stage: Stage) { val root = Pane() root.newVizContainer().run { size = Size(width, height) createChart() } // Launch our Scene stage.apply { title = "Chicago average temperature in 2017" scene = (Scene(root, width, height)) show() } } private fun VizContainer.createChart(): Chart<Weather> { // Load and parse the CSV file val csvContent = File("./src/main/resources/Weather - Stats.csv").readText() val weatherData = parseWeatherCSV(csvContent) val chicago2017 = weatherData.filter { it.city == "Chicago" && it.year == 2017 } // Create a bar chart with the month as X-axis and the temperature as Y-axis return chart(chicago2017) { val monthDimension = discrete( { domain.month } ) val temperatureDimension = quantitative( { domain.avgTemp } ) bar(monthDimension, temperatureDimension) } } }

Note that the createChart() function returns a typed Chart<Weather> instance.

Running the main() function produces this result:

Creating%20your%20first%20JavaFX%20chart%20app,%20part%202/Untitled%202.png

The dimensions: how to describe our dataset

When you look at the code, you can see that you create 2 dimensions: monthDimension and temperatureDimension and use them in your chart:

import io.data2viz.charts.chart.Chart import io.data2viz.charts.chart.chart import io.data2viz.charts.chart.discrete import io.data2viz.charts.chart.mark.bar import io.data2viz.charts.chart.quantitative import io.data2viz.charts.viz.VizContainer import io.data2viz.charts.viz.newVizContainer import io.data2viz.geom.Size import javafx.application.Application import javafx.scene.Scene import javafx.scene.layout.Pane import javafx.stage.Stage fun main() { Application.launch(WeatherChart::class.java) } private val width = 500.0 private val height = 400.0 class WeatherChart : Application() { override fun start(stage: Stage) { val root = Pane() root.newVizContainer().run { size = Size(width, height) createChart() } // Launch our Scene stage.apply { title = "Chicago average temperature in 2017" scene = (Scene(root, width, height)) show() } } private fun VizContainer.createChart(): Chart<Weather> { // Load and parse the CSV file val csvContent = File("./src/main/resources/Weather - Stats.csv").readText() val weatherData = parseWeatherCSV(csvContent) val chicago2017 = weatherData.filter { it.city == "Chicago" && it.year == 2017 } // Create a bar chart with the month as X-axis and the temperature as Y-axis return chart(chicago2017) { val monthDimension = discrete( { domain.month } ) val temperatureDimension = quantitative( { domain.avgTemp } ) bar(monthDimension, temperatureDimension) } } }

A dimension is a way to describe how to acccess the data and how to process the data.

The dimension type (how to process the data)

In the Weather class, the month is stored as an Int and the average temperature as a Double, and, as you want to draw a bar chart, you want to process them differently:

  • Temperature is a continuous numeric value, use the quantitative dimension to express that
  • Month is a discrete value, use the discrete dimension to process it as a "category"

The dimension accessor (how to access the data)

Each Dimension applies to a DOMAIN instance. Here it's your Weather object and you need to indicates which property you want to translate as your dimension.

This is what is done in the lambda inside the parenthesis, the domain value refers to your Weather object, so the monthDimension is based on the domain.month property, and the temperatureDimension on the domain.avgTemp one.

We call this lambda the "accessor", it describes how to access the data.

Here again, the strong-typing offers some help with the editor.

Note how a Quantitative dimension only accepts some Double values when using smart-completion with IDEA:

Creating%20your%20first%20JavaFX%20chart%20app,%20part%202/Untitled%203.png

Find more information about dimensions in this guide.

Adding some information on dimensions

You can add some properties on dimensions like name and formatter.

Let's add a title to your chart, and add these properties on your dimension, replace the code of WeatherChart.kt with the following:

import io.data2viz.charts.chart.Chart import io.data2viz.charts.chart.chart import io.data2viz.charts.chart.discrete import io.data2viz.charts.chart.mark.bar import io.data2viz.charts.chart.quantitative import io.data2viz.charts.core.formatToInteger import io.data2viz.charts.viz.VizContainer import io.data2viz.charts.viz.newVizContainer import io.data2viz.format.Locales import io.data2viz.geom.Size import javafx.application.Application import javafx.scene.Scene import javafx.scene.layout.Pane import javafx.stage.Stage fun main() { Application.launch(WeatherChart::class.java) } private val width = 500.0 private val height = 400.0 private val months = listOf("Jan.", "Feb.", "Mar.", "Apr.", "May", "Jun.", "Jul.", "Aug.", "Sep.", "Oct.", "Nov.", "Dec.") private val city = "Chicago" private val year = 2017 class WeatherChart : Application() { override fun start(stage: Stage) { val root = Pane() root.newVizContainer().run { size = Size(width, height) createChart() } // Launch our Scene stage.apply { title = "Chicago average temperature in 2017" scene = (Scene(root, width, height)) show() } } private fun VizContainer.createChart(): Chart<Weather> { // Load and parse the CSV file val csvContent = File("./src/main/resources/Weather - Stats.csv").readText() val weatherData = parseWeatherCSV(csvContent) val chicago2017 = weatherData.filter { it.city == city && it.year == year } // Create a bar chart with the month as X-axis and the temperature as Y-axis return chart(chicago2017) { title = "Monthly average temperature in $city, $year" val monthDimension = discrete( { domain.month } ) { formatter = { months[this-1] } } val temperatureDimension = quantitative( { domain.avgTemp } ) { name = "Average temperature" formatter = { "${this.formatToInteger(Locales.en_US)} °F" } } bar(monthDimension, temperatureDimension) } } }

Run the code and you can see that, giving your dimension a name automatically add a title to your corresponding axis (the Y-axis).

Also the data that is computed through your dimension will be formatted with the given formatter (check the Y-axis and the tooltip):

Creating%20your%20first%20JavaFX%20chart%20app,%20part%202/Untitled%204.png

The "Datum", a convenient way to access data

Maybe you already see that the Dimension did not directly use a DOMAIN parameter, but instead a Datum<DOMAIN>, here a Datum<Weather>:

Creating%20your%20first%20JavaFX%20chart%20app,%20part%202/Untitled%205.png

A Datum is an object that holds your domain object (as its domain property), but also some other useful information like the index of your current object in the dataset...

For now, just keep in mind that you don't manipulate the DOMAIN object directly, the Datum will prove itself very useful on many occasions.